LinearRegression 클래스

→ 선형 회귀(규제 적용 x)

 

class sklearn.linear_model.LinearRegression(fit_intercept=True, normalize=False, copy_X=True, n_jobs=1)

 

○ LinearRegression 클래스는 실제값과 예측값의 RSS를 최소화해 OLS(Ordinary Least Squares; 최소 제곱법) 추정 방식으로 구현한 클래스

○ 주요 입력 파라미터

- fit_intercept: w0(절편, bias) 값을 계산할 것인지 말지를 지정. 만일 False로 지정하면 절편이 사용되지 않고 0으로 지정됨

- normalize: 회귀를 수행하기 전에 입력 데이터 세트를 정규화할 것인지 결정(사용 x, 전처리 과정에서 스케일링 진행 → 디폴트로 나둠)

○ 주요 속성

- coef_: 회귀계수가 배열 형태로 저장

- intercept_: w0(절편, bias)값

 

선형 회귀의 다중 공선성 문제

선형 회귀와 같은 OLS 기반의 회귀 계수를 계산하는 회귀 모델은 입력 피처의 독립성에 많은 영향을 받는다. 즉, 피처간의 상관관계가 매우 높은 경우 분산이 매우 커져서 오류에 민감해진다. 이를 다중 공선성(multi-collinearity)라고 한다.

일반적으로 상관관계가 높은 피처가 많은 경우 독립적인 중요한 피처만 남기고 제거하거나 규제를 적용한다. 

 

 

회귀 평가 지표

"실제값과 예측값의 차이를 기반"

→ 실제값과 예측값의 차이를 그냥 더하면 +와 -가 섞여서 오류가 상쇄되서 정확한 지표가 될 수 없기 때문에 오류의 절대값 평균이나 제곱의 평균, 또는 제곱한 뒤 루트를 씌운 평균값을 구함

→ 실제값과 예측값의 차이, 다시 말해 오류가 작을수록 좋은 평가 결과이기 때문에 평가 지표가 작을수록 좋은 결과임

R^2은 설명력으로 회귀모델이 데이터를 얼마나 잘 설명하는지를 나타냄

mean_squared_error() 함수는 squared 파라미터가 기본적으로 True이다. 즉 MSE는 사이킷런에서 mean_squared_error(실제값, 예측값, squared=True)이며 RMSE는 mean_squared_error(실제값, 예측값, squared=False)를 이용해 구하지만 헷갈리기 때문에 RMSE를 구할때는 MSE에 np.sqrt()를 이용해 구하자!

 

사이킷런 scoring 함수에 회귀 평가 적용 시 유의 사항

cross_val_score, GridSearchCV와 같은 scoring 함수에 회귀 평가 지표를 적용할 때 유의 사항이 있다.

'neg_'라는 접두어가 붙어있는데 이는 Negative(음수)를 의미하며, 기존의 회귀 평가 지표 값에 음수(-1)를 곱한다는 의미이다. 이렇게 'neg_'가 붙는 이유는 사이킷런의 scoring 함수가 score값이 클수록 좋은 평가 결과로 평가하기 때문이다. 하지만 실제값과 예측값의 차이를 기반으로 하는 회귀 평가 지표의 경우 값이 커지면 오히려 나쁜 모델이라는 의미이므로 음수(-1)를 곱해주는 것이다.

 

 

경사 하강법(Gradient Descent)

비용함수가 최소가 되는 회귀계수를 어떻게 구할 수 있을까?

솔루션이 바로 지금 소개할 경사 하강법(Gradient Descent, GD)이다.

 

경사 하강법은 비용함수 RSS를 최소화하는 방법을 직관적으로 제공하는 뛰어난 방식으로, 점진적인 반복 계산을 통해 회귀계수를 업데이트하면서 오류값이 최소가 되는 회귀계수를 구하는 방법이다.

 

그렇다면 솔루션인 '경사 하강법'을 통해 어떻게 하면 오류가 작아지는 방향으로 회귀계수를 업데이트할 수 있을까?

→ 미분을 통해 비용함수의 최소값을 찾음(미분은 증가 또는 감소의 방향성을 나타냄)

비용함수를 보면 제곱을 했기 때문에 회귀계수에 대한 2차 함수로서 아래 그림과 같은 포물선 형태이다. 2차 함수의 최저점은 2차 함수의 미분값인 1차 함수의 기울기가 가장 최소일 때다.

 

경사 하강법은 비용함수를 최초의 회귀계수(w) 값에서부터 미분을 적용한 뒤, 이 미분 값(기울기)이 계속 감소하는 방향으로 순차적으로 회귀계수를 업데이트한다.마침내 더 이상 미분된 1차 함수의 기울기가 감소하지 않는 지점을 비용함수가 최소인 지점으로 간주하고, 그때의 회귀계수를 반환한다.

 

경사 하강법 수식 정리 및 파이썬 코드 구현

경사 하강법 수식 정리 전 알아야 할 'Chain Rule'

 

 

<경사 하강법 수식 정리>

1. 비용함수 RSS(w0, w1)을 편의상 R(w)로 지칭. R(w)를 미분해 미분 함수의 최소값을 구해야 하는데, R(w)는 두 개의 회귀계수인 w0와 w1을 각각 가지고 있기 때문에 일반적인 미분을 적용할 수 없고, w0, w1 각 변수에 편미분을 적용해야 한다.

2. w1, w0의 편미분 결과값을 반복적으로 보정하면서 w1, w0값을 업데이트하면 비용함수 R(w)가 최소가 되는 w1, w0값을 구할 수 있다.

  • 이때, 편미분 값이 너무 클 수 있기 때문에 보정 계수 η를 곱하는데, 이를 '학습률'이라고 함
  • 업데이트는 이전 회귀계수에서 편미분 결과값을 빼면서 적용함(아래와 같음)

→ 새로운 w1, 새로운 w0를 반복적으로 업데이트 하면서 비용함수가 최소가 되는 값을 찾음

 

경사 하강법의 프로세스를 정리를 하자면

 

 

<경사 하강법 파이썬 코드 구현>

● 실습을 위한 y=4x+6 시뮬레이션 데이터 생성(단순 선형회귀로 예측할 만한 데이터 생성)

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

np.random.seed(0)

noise = np.random.randn(100,1)
X = 2 * np.random.rand(100,1)
y = 6 + 4 * X + noise

plt.scatter(X, y)

● 경사하강법

# w1과 w0를 업데이트할 w1_update, w0_update를 반환하는 함수 생성
def get_weight_updates(w1, w0, X, y, learning_rate=0.01):
    
    N = len(y)
    
    # w1_update, w0_update를 먼저 w1, w0의 shape와 동일한 크기를 가진 0값으로 초기화
    w1_update = np.zeros_like(w1)
    w0_update = np.zeros_like(w0)
    
    # 예측 배열 계산, 예측과 실제값의 차이 계산
    y_pred = np.dot(X, w1.T) + w0
    diff = y - y_pred
    
    # w0_update를 dot 행렬 연산으로 구하기 위해 모두 1값을 가진 행렬 생성
    w0_factors = np.ones((N,1))
    
    # w1과 w0를 업데이트할 w1_update, w0_update 계산
    w1_update = -(2/N) * learning_rate * (np.dot(X.T, diff))
    w0_update = -(2/N) * learning_rate * (np.dot(w0_factors.T, diff))
    
    return w1_update, w0_update
# get_weight_updates 함수를 이용해 경사 하강 방식으로 반복적으로 수행하여 w1, w0를 업데이트하는 함수 생성

# 입력 인자 iters로 주어진 횟수만큼 반복적으로 w1과 w0를 업데이트
def gradient_descent_steps(X, y, iters=10000):
    
    # w0와 w1을 모두 0으로 초기화
    w0 = np.zeros((1,1))
    w1 = np.zeros((1,1))
    
    # 인자로 주어진 iters만큼 반복적으로 get_weight_updates()를 호출해 w1, w0 업데이트 수행
    for ind in range(iters):
        w1_update, w0_update = get_weight_updates(w1, w0, X, y, learning_rate=0.01)
        w1 = w1 - w1_update
        w0 = w0 - w0_update
        
    return w1, w0

● 비용함수(RSS) 값 계산, 회귀계수 계산

# 비용함수 정의
def get_cost(y, y_pred):
    N = len(y)
    cost = np.sum(np.square(y - y_pred)) / N
    return cost

# 회귀계수 계산
w1, w0 = gradient_descent_steps(X, y, iters=1000)
print('w1:{0:.3f}, w0:{1:.3f}'.format(w1[0,0], w0[0,0]))

# 비용함수 값 계산
y_pred = w1[0,0] * X + w0
print('cost:{0:.4f}'.format(get_cost(y, y_pred)))

● 앞에서 구한 y_pred에 기반해 회귀선 시각화

plt.scatter(X, y)
plt.plot(X, y_pred)

 

Reference)

 

예측값에 대한 설명 - 인프런 | 질문 & 답변

예측값에 대한 설명 부분에서, 100개의 데이터 X(1, 2, ... 100)이 있다면 예측값(y_pred)은 w0+X(1)w1, w0+X(2)w1, ... w0+X(100)w1 이 되어야하는것 아닌가요? - 질문 & 답변 | 인프런

www.inflearn.com

 

 

경사하강법 질문드립니다 - 인프런 | 질문 & 답변

강사님 안녕하세요 ㅎㅎ 경사하강법 강의를 듣다가 잘 이해가 가지 않아 질문드립니다. 강의 5.4의 get_weight_updates 함수에서 y_pred 에서 X의 개수가 여러개고 W1이 1개인데 왜 w1.T인지 설명해주실수

www.inflearn.com

 

 

get_cost 함수 w1[0,0]가 들어가는 이유 - 인프런 | 질문 & 답변

안녕하세요 강사님 수업 잘 듣고있습니다!파이썬 코드로 경사 하강법 구현하기에서 def get_cost 함수 y_pred = w1[0,0] * X + w0 부분에 질문이 있습니다.현재 코드가 w1와 w0 둘다 shape이 (1,1)이라서 그런

www.inflearn.com


손실함수가 최소가 되는 회귀계수를 찾아가는 경사 하강법에도 단점이 존재한다. 경사 하강법은 모든 학습 데이터에 대해 반복적으로 비용함수 최소화를 위한 값을 업데이트하기 때문에 수행 시간이 매우 오래 걸린다. 

 

때문에 실전에서는 대부분 확률적 경사 하강법(SGD, Stochastic Gradient Descent)을 이용한다. 확률적 경사 하강법은 모든 학습 데이터가 아닌 일부 데이터만 이용해 회귀계수가 업데이트되는 값을 계산하므로 경사 하강법에 비해서 빠른 속도를 보장한다. 

 

→ 일반적으로 말하는 SGD는 실제로 미니 배치 경사 하강법(mini-BGD)이므로, 앞으로 SGD를 떠올릴 때 미니 배치 경사 하강법을 떠올리면 된다. 

 

Reference)

 

[혼공머] 배치와 미니 배치, 확률적 경사하강법

👩‍🔬 이번에는 혼공머 책의 챕터 4-2 파트입니다.📚 혼자공부하는머신러닝+딥러닝, 한빛미디어📄 Gradient Descent - 경사하강법, 편미분, Local Minimum📑 경사하강법(Gradient Descent)🔗 배치와 미니

velog.io

 

 

Optimizer 종류

위에서 언급했듯이, 경사 하강법의 문제점은 다음과 같다.

첫째, 한번 학습할 때마다 모든 데이터셋을 이용함

둘째, 학습률 정하기

셋째, 지역 최소점(Local Minima) 

넷째, 메모리 한계 

 

이런 경사 하강법의 문제들을 개선하기 위해 여러 Optimizer 종류들이 존재한다. 

 

아래의 링크를 보면서 수식에 너무 얽매이지 말고 이런것들이 있구나 정도로만 이해하고, 추후 딥러닝을 공부할 때 집중적으로 다룰 것이다.

 

Reference)

 

[ML] 신경망에서의 Optimizer - 역할과 종류

지난 포스트에서 손실함수란 무엇이고, 어떻게 하면 손실함수의 최솟값이 되는 점을 찾는지 살펴보았다. 우리는 '최적화(optimization)'를 통해 파라미터를 잘 찾아서 문제 해결에 적합한 모델을 결

heeya-stupidbutstudying.tistory.com

 

 

4-3. 경사하강법 (Gradient Descent)

파이썬 머신러닝 완벽 가이드 - Chapter 5. 회귀

velog.io


● 확률적 경사하강법

- 전체 X, y 데이터에서 랜덤하게 batch_size만큼 데이터를 추출해 사용(일부 데이터)

def stochastic_gradient_descent_steps(X, y, batch_size=10, iters=1000):
    w0 = np.zeros((1,1))
    w1 = np.zeros((1,1))
    
    for ind in range(iters):
        np.random.seed(ind)
        
        # 전체 X, y 데이터에서 랜덤하게 batch_size만큼 데이터를 추출해 sample_X, sample_y로 저장
        stochastic_random_index = np.random.permutation(X.shape[0]) # shuffle된 배열 생성
        sample_X = X[stochastic_random_index[0:batch_size]]
        sample_y = y[stochastic_random_index[0:batch_size]]
        
        # 랜덤하게 batch_size만큼 추출된 데이터 기반으로 w1_update, w0_update 계산 후 업데이트
        w1_update, w0_update = get_weight_updates(w1, w0, sample_X, sample_y, learning_rate=0.01)
        w1 = w1 - w1_update
        w0 = w0 - w0_update
        
    return w1, w0
w1, w0 = stochastic_gradient_descent_steps(X, y, iters=1000)
print('w1:', round(w1[0,0], 3), 'w0:', round(w0[0,0], 3))

y_pred = w1[0,0] * X + w0
print('cost:{0:.4f}'.format(get_cost(y, y_pred)))

지금까지는 단순 선형회귀에서 경사 하강법을 적용해 봤다. 피처가 여러개인 경우 어떻게 회귀계수를 도출할 수 있을까?

피처가 1개인 경우와 유사하게 내적을 이용한다.(page 319 ~ 320)

단순 선형 회귀는 독립변수와 종속변수가 각각 1개인 선형 회귀이다.

 

예를 들어, 주택 가격이 주택의 크기로만 결정되는 단순 선형 회귀로 가정하면 집이 넓으면 가격이 높아지는 경향이 있기 때문에 아래 그림과 같이 주택 가격은 주택 크기에 대해 선형(직선 형태)의 관계로 표현할 수 있다.

위에서 볼 수 있듯이 실제 데이터는 우리가 예측한 회귀 모델에 딱 들어맞을 순 없다. 따라서 실제 데이터와 우리가 예측한 회귀 모델의 차이에 따른 오류값(잔차)이 존재할 수밖에 없다.

결국 우리는 현실을 잘 반영하는 예측 회귀 모델을 구하는 것이 관건이다. 즉, 예측 회귀 모델이 현실을 잘 반영하는지 안하는지에 대한 평가는 실제 데이터와 예측 회귀 모델 사이의 잔차가 작으면 현실을 잘 반영하는 좋은 모델이다.


→ 머신러닝에서 최적의 회귀 모델을 만든다는 것은 바로 전체 데이터의 잔차 합이 최소가 되는 모델을 만든다는 의미이고 동시에 잔차 합이 최소가 될 수 있는 최적의 회귀계수를 찾는다는 의미임

 

RSS

예측한 회귀 모델 위아래로 실제 데이터가 분포해 있으므로 잔차는 +나 -가 될 수 있다. 그래서 전체 데이터의 잔차 합을 구하기 위해 단순히 더했다가는 뜻하지 않게 잔차 합이 0이 되거나 크게 줄어들 수 있다.

→ 잔차 합을 계산할 때는

첫째, 잔차에 절대값을 취해서 더하거나(Mean Absolute Error)

둘째, 잔차에 제곱을 취해서 더함(RSS, Residual Sum of Square)

 

일반적으로 미분 등의 계산을 편리하게 하기 위해서 RSS 방식으로 잔차 합을 구함

 

 

RSS = (#1 주택 가격 - (w0+w1*#1 주택 크기))^2

+ (#2 주택 가격 - (w0+w1*#2 주택 크기))^2

+ (#3 주택 가격 - (w0+w1*#3 주택 크기))^2

+ ... (모든 학습 데이터에 대해 RSS 수행)

 

 

 

 

RSS는 학습 데이터의 건수로 나누어서 다음과 같이 정규화된 식으로 표현할 수 있다.

● RSS는 이제 변수가 w0, w1인 식으로 표현할 수 있으며, 이 RSS를 최소로 하는 w0, w1, 즉 회귀계수를 학습을 통해서 찾는 것이 머신러닝 기반 회귀의 핵심 사항임

→ RSS는 회귀식의 독립변수 x, 종속변수 y가 중심 변수가 아니라 w 변수(회귀계수)가 중심 변수임을 인지하는 것이 중요!(학습 데이터로 입력되는 독립변수와 종속변수는 RSS에서 모두 상수로 간주)

● 회귀에서 RSS는 비용(Cost)이며, 회귀계수로 구성되는 RSS를 비용 함수라고 함(비용 함수를 손실 함수(loss function)라고도 부른다.)

 

머신러닝 회귀 알고리즘은 데이터를 계속 학습하면서 이 비용 함수가 반환하는 값을 지속해서 감소시키고, 최종적으로는 더 이상 감소하지 않는 최소의 오류값을 구한다.

이 비용이 최소가 되는 파라미터(여기선 회귀계수)를 찾기 위해 경사 하강법을 사용한다.

지도학습

1. 분류

결정값이 카테고리 값(이산값)

2. 회귀

결정값이 숫자값(연속값)


회귀 개요

회귀분석은 데이터 값이 평균과 같은 일정한 값으로 돌아가려는 경향을 이용한 통계학 기법이다. 통계학 용어를 빌리자면 회귀는 여러 개의 독립변수와 한 개의 종속변수 간의 상관관계를 모델링하는 기법을 통칭한다.

 

예를 들어 아파트의 방 개수, 방 크기, 주변 학군 등 여러 개의 독립변수에 따라 아파트 가격이라는 종속변수가 어떤 관계를 나타내는지 모델링하고 예측하는 것

 

Y: 종속변수

X1, X2, ..., Xn: 독립변수

W1, W2, ..., Wn: 각 독립변수의 값에 영향을 미치는 회귀계수(Regression coefficients)

 

머신러닝 관점에서 독립변수는 피처에 해당되며, 종속변수는 결정값에 해당된다.

따라서 머신러닝 회귀 예측의 핵심은 '주어진 피처와 결정값 데이터 기반에서 학습을 통해 최적의 회귀계수를 찾아내는 것'


회귀의 유형

회귀는 회귀계수의 선형/비선형 여부, 독립변수의 개수, 종속변수의 개수에 따라 여러 가지 유형으로 나눌 수 있다.

1. 선형/비선형

회귀계수가 선형이면 선형 회귀 / 회귀계수가 비선형이면 비선형 회귀

2. 독립변수의 개수

독립변수 개수가 1개이면 단일 회귀 / 독립변수 개수가 여러개이면 다중 회귀

3. 종속변수의 개수

종속변수 개수가 1개이면 단변량 회귀 / 종속변수 개수가 여러개이면 다변량 회귀


선형 회귀의 종류

여러 가지 회귀 중에서 선형 회귀가 가장 많이 사용된다. 선형 회귀는 실제값과 예측값의 차이를 최소화하는 직선형 회귀선을 최적화하는 방식

 

선형 회귀 모델은 규제(Regularization) 방법에 따라 다시 별도의 유형으로 나뉠 수 있다. 여기서 규제는 일반적인 선형 회귀의 과적합 문제를 해결하기 위해서 회귀계수에 패널티 값을 적용하는 것이다.

 

● 일반 선형 회귀

예측값과 실제값의 RSS(Residual Sum of Squares)를 최소화할 수 있도록 회귀계수를 최적화하며, 규제(Regularization)를 적용하지 않은 모델

 

● 릿지(Ridge) 회귀

릿지 회귀는 선형 회귀에 L2 규제를 적용한 회귀 모델

L2 규제는 상대적으로 큰 회귀계수 값의 예측 영향도를 감소시키기 위해서 회귀계수 값을 더 작게 만드는 규제

 

● 라쏘(Lasso) 회귀

라쏘 회귀는 선형 회귀에 L1 규제를 적용한 회귀 모델

L1 규제는 예측 영향력이 작은 피처의 회귀계수를 0으로 만들어 회귀 예측 시 피처가 선택되지 않게 하는 것 → 이러한 특성으로 L1 규제는 피처 선택 기능으로도 불림

 

● 엘라스틱넷(ElasticNet) 회귀

L2, L1 규제를 함께 결합한 모델

주로 피처가 많은 데이터 세트에서 적용되며, L1 규제로 피처의 개수를 줄임과 동시에 L2 규제로 회귀계수 값의 크기를 조정

 

● 로지스틱(Logistic) 회귀

로지스틱 회귀는 회귀라는 이름이 붙어 있지만, 사실은 분류에 사용되는 선형 모델 → 로지스틱 회귀는 매우 강력한 분류 알고리즘

이진 분류뿐만 아니라 희소 영역의 분류(예를 들어 텍스트 분류)에서 뛰어난 예측 성능을 보임 → 이진 분류에 아주 효과적

 

 

 

○ permutation(순열) importance

- RFE와 같이 피처를 제거하면서 재학습을 수행할 필요가 없는 장점이 있음

- 사이킷런 feature selection 다른 모듈에 비해 일반적으로 많이 사용

- 테스트 데이터(검증 데이터)에 특정 피처의 값들을 반복적으로 무작위로 섞은 뒤 모델 성능이 얼마나 저하되는지를 기준으로 해당 피처의 중요도를 산정

permutation importance 프로세스

●모델 학습 후 원본 테스트 데이터로 기준 평가 성능을 설정

●원본 테스트(검증) 데이터의 개별 feature 별로 아래 수행

  • 설정된 iteration만큼 해당 feature 값들을 shuffle
  • 각각 모델 성능 평가 후 평균
  • 기준 평가 성능과 위에서 구한 평균을 비교해 모델 성능이 얼마나 저하되었는지 평가해서 피처 중요도 산정(중요한 피처면 저하가 큼) → 원본에서 평균적으로 얼마나 성능이 감소했나
# 실습을 위한 데이터 세트
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split

diabetes = load_diabetes()
X_train, X_val, y_train, y_val = train_test_split(diabetes.data, diabetes.target, random_state=0)
# 모델 학습 및 R2 Score 평가(기준 평가 성능)
from sklearn.linear_model import Ridge # 사용할 모델
from sklearn.metrics import r2_score # 평가 척도

model = Ridge(alpha=1e-2).fit(X_train, y_train)
y_pred = model.predict(X_val)
print('r2 score:', r2_score(y_val, y_pred))

import numpy as np

# permutation_importance:원본 테스트(검증) 데이터 지정
from sklearn.inspection import permutation_importance

r = permutation_importance(model, X_val, y_val, n_repeats=30, random_state=0) # n_repeats:iteration 만큼 shuffle

for i in r.importances_mean.argsort()[::-1]: # 평균 permutation importance가 높은 순으로 인덱스를 내림차순 정렬
    if r.importances_mean[i] -2 * r.importances_std[i] > 0: # 표준편차의 2 배 값보다 큰 평균을 가진 피처들로 선별
        # 위 조건을 만족하는 피처들의 평균 permutation importance값과 표준편차 출력
        print(diabetes.feature_names[i], "   ", np.round(r.importances_mean[i], 4), ' +/-', np.round(r.importances_std[i], 5))

 - 표준편차의 2 배 값보다 큰 평균을 가진 피처들로 선별했는가는 단순히 경험적인 결과이다. 일반적으로 permutation importance 사용할 때 경험적으로 표준편차의 2 배 값 정도를 추천하지만 꼭 2배일 필요는 없다. 2~3배 사이, 예를 들어 2.5배, 2.8배, 3배 정도의 기준을 가지고 feature selection을 적용한 뒤 가장 성능이 뛰어난 결과를 적용해 보면 된다.

- permutation_importance를 적용해 버리면, 바로 피처 중요도가 permutation importance 방식으로 적용해버린다. 그러니까, permutation_importance.importances_mean 속성은 계산된 피처 중요도를 가지고 있다. 이중에서 높은 것부터 출력이 된다. 즉, 위의 결과에서 각 피처의 importance_mean의 값을 보니 s5가 0.2042로 모델 성능이 가장 저하해서 가장 중요한 피처이다.

 

 

Reference)

1. https://scikit-learn.org/stable/modules/permutation_importance.html

2. https://soohee410.github.io/iml_permutation_importance

3. https://blog.naver.com/passiona2z/222617818847

4. https://www.inflearn.com/questions/672420/permutation-importance-%EC%86%8C%EA%B0%9C-%EB%B0%8F-%EC%8B%A4%EC%8A%B5

5. https://www.inflearn.com/questions/483915/permutation-importance-%EC%88%9C%EC%84%9C%EA%B0%80-%EC%9E%98-%EC%9D%B4%ED%95%B4%EA%B0%80%EC%A7%80-%EC%95%8A%EC%8A%B5%EB%8B%88%EB%8B%A4


Feature Importance 문제점

왜 Feature Importance는 절대적인 Feature Selection 기준이 될 수 없는가?

○ Feature Importance는 최적 tree 구조를 만들기 위한 피처들의 impurity(불순도)가 중요 기준임. 결정값과 관련이 없어도 Feature Importance가 높아 질 수 있음

○ Feature Importance는 학습 데이터를 기반으로 생성됨. 테스트 데이터에서는 달라질 수 있음

○ Feature Importance는 number형의 높은 cardinality feature에 biased 되어 있음

Feature Selection

모델을 구성하는 주요 피처들을 선택

  • 불필요한 다수의 피처들로 인해 모델 성능을 떨어뜨릴 가능성 제거
  • 설명 가능한 모델이 될 수 있도록 피처들을 선별
  • 다수의 피처로 모델 학습시 과적합 이슈 발생(차원의 저주)

●Feature Selection 하는 방법

-피처값의 분포: 특정 피처의 값이 딱 하나라면, 이 피처가 있으나 없으나 피처와 타겟값 간의 패턴에 변별력이 없음

-결측값이 많은 피처: 결측값이 많은 피처는 제거 대상이 될 수 있음

-피처간 높은 상관도: '데이터 전처리-변수 분포 문제- 특징 간 상관성 제거' 참고

-결정값과의 독립성등을 고려: '데이터 전처리-차원의 저주 문제' 참고

-모델의 피처 중요도 기반: RFE, SelectFromModel, Permutation importance

 

 

 

○ RFE(Recursive Feature Elimination)

- 모델 최초 학습 후 feature 중요도 선정

- feature 중요도가 낮은 속성들을 차례로 제거해 가면서 반복적으로 학습/평가를 수행하여 최적 feature 추출

- 수행시간이 오래 걸리고, 낮은 속성들을 제거해 나가는 메커니즘이 정확한 feature selection을 찾는 목표에 정확히 부합하지 않을 수 있음

# 실습을 위한 임의의 분류용 데이터 생성
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=1000, n_features=25, n_informative=3,
                          n_redundant=2, n_repeated=0, n_classes=8,
                          n_clusters_per_class=1, random_state=0)

# 피처들을 데이터프레임 형태로 만들어보자
import pandas as pd

columns_list = ['col1', 'col2', 'col3', 'col4', 'col5', 'col6', 'col7', 'col8', 'col9', 'col10', 'col11', 'col12', 'col13',
               'col14', 'col15', 'col16', 'col17', 'col18', 'col19', 'col20', 'col21', 'col22', 'col23', 'col24', 'col25']
df_X = pd.DataFrame(X, columns=columns_list)
# RFE
from sklearn.feature_selection import RFE
from sklearn.svm import SVC # 사용할 모델

svc = SVC(kernel='linear')

rfe = RFE(estimator=svc, n_features_to_select=5, step=1) # step=1: 하나씩 피처 제거, n_features_to_select=5: 5개 피처를 선택
rfe.fit(X, y)
print(rfe.support_) # 선택된 피처는 True로 반환
print('선택된 피처:', df_X.columns[rfe.support_])

# 선택된 피처를 통한 최종 데이터프레임 생성
feature_selection = df_X.columns[rfe.support_]
df_X = df_X[feature_selection]
df = df_X # 데이터프레임 이름 변경
df['target'] = y

df.head()

○ RFECV(Recursive Feature Elimination with Cross Validation)

- RFECV는 RFE와 로직은 똑같지만, 각 feature 개수마다 교차검증을 활용해 각 feature 개수별 성능을 평균내어 가장 높은 성능을 가지는 feature 개수에 해당하는 feature들을 최종 feature selection 결과로 사용

→ 몇 개의 피처가 최적일지 알 수 있음

# 실습을 위한 임의의 분류용 데이터 생성
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=1000, n_features=25, n_informative=3,
                          n_redundant=2, n_repeated=0, n_classes=8,
                          n_clusters_per_class=1, random_state=0)

# 피처들을 데이터프레임 형태로 만들어보자
import pandas as pd

columns_list = ['col1', 'col2', 'col3', 'col4', 'col5', 'col6', 'col7', 'col8', 'col9', 'col10', 'col11', 'col12', 'col13',
               'col14', 'col15', 'col16', 'col17', 'col18', 'col19', 'col20', 'col21', 'col22', 'col23', 'col24', 'col25']
df_X = pd.DataFrame(X, columns=columns_list)
# RFECV
from sklearn.feature_selection import RFECV
from sklearn.model_selection import StratifiedKFold
from sklearn.svm import SVC # 사용할 모델

svc = SVC(kernel='linear')

rfecv = RFECV(estimator=svc, step=1, cv=StratifiedKFold(3), scoring='accuracy')
rfecv.fit(X, y)
# 몇 개의 피처가 최적일지 교차검증 성능 시각화
import matplotlib.pyplot as plt

plt.figure()
plt.xlabel('Number of features selected')
plt.ylabel('Cross validation score')
plt.plot(range(1, len(rfecv.grid_scores_) + 1), rfecv.grid_scores_)
plt.show() # cv를 3으로 해서 3개의 그래프가 나옴
# 0~5 사이에 특정 값이 최적의 피처 개수인듯

print('최적 피처 개수:', rfecv.n_features_)
print('선택된 피처:', df_X.columns[rfecv.support_])

# 선택된 피처를 통한 최종 데이터프레임 생성
feature_selection = df_X.columns[rfecv.support_]
df_X = df_X[feature_selection]
df = df_X # 데이터프레임 이름 변경
df['target'] = y

df.head()

○ SelectFromModel

- 모델 최초 학습 후 선정된 feature 중요도에 따라 평균/중앙값 등의 특정 비율 이상인 feature들을 선택

(이 비율은 threshold로 지정)

# 실습을 위한 데이터 세트
from sklearn.datasets import load_diabetes

diabetes = load_diabetes()
X = diabetes.data
y = diabetes.target
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LassoCV # 사용할 모델

lasso = LassoCV()
lasso.fit(X,y) # 모델 학습

# 회귀계수 절대값 시각화
importance = np.abs(lasso.coef_)
plt.bar(height=importance, x=diabetes.feature_names)
plt.title('feature importances via coefficients')
plt.show()

# selectfrommodel
from sklearn.feature_selection import SelectFromModel

threshold = np.sort(importance)[-3] + 0.01 # 회귀계수가 3번째로 큰 값에 0.01을 더해서 threshold 지정하기로 결정
sfm = SelectFromModel(lasso, threshold=threshold)
sfm.fit(X,y)

print(sfm.threshold_) # threshold 값
print(sfm.get_support()) # 선택된 피처는 True로 반환
print('선택된 피처:', np.array(diabetes.feature_names)[sfm.get_support()])

# 다른 threshold 지정해보기
sfm = SelectFromModel(lasso, threshold='1.5 * median')
sfm.fit(X, y)

print(sfm.threshold_)
print(sfm.get_support())
print('선택된 피처:', np.array(diabetes.feature_names)[sfm.get_support()])

SelectFromModel Reference)

https://bizzengine.tistory.com/163

 

Automatic Feature Selection

개요 새로운 특성을 만드는 방법이 많으므로, 데이터의 차원이 원복 특성의 수 이상으로 증가하기 쉽습니다. 그러나, 특성이 더 추가되면 모델은 더 복잡해지고 과대적합될 가능성도 높아집니

bizzengine.tistory.com


사이킷런의 Recursive Feature Elimination이나 SelectFromModel의 경우 트리기반의 분류에서는 Feature importance, 회귀에서는 회귀 계수를 기반으로 반복적으로 모델 평가하면서 피처들을 선택하는 방식인데, 이런 방식의 Feature Selection은 오히려 모델의 성능을 떨어뜨릴 가능성이 높다. 그러니까 정확하지 않은 feature selection 방법이다.

 Permutation importance가 보다 정확한 방식

 

이어서 permutation importance 정리 및 실습 진행

 

Reference)

https://www.inflearn.com/questions/806218/feature-selection

스태킹(Stacking)

개별 알고리즘의 예측 결과 데이터 세트를 최종적인 메타 데이터 세트로 만들어 별도의 ML 알고리즘으로 최종 학습을 수행하고 테스트 데이터를 기반으로 다시 최종 예측을 수행하는 방식

→기반 모델들이 예측한 값들을 Stacking 형태로 만들어서 메타 모델이 이를 학습하고 예측하는 모델

 

●스태킹 모델은 다음과 같은 두 종류의 모델이 필요함

  1. 개별적인 기반 모델
  2. 이 개별 기반 모델의 예측 데이터를 학습 데이터로 만들어서 학습하는 최종 메타 모델

즉, 스태킹 모델은 여러 개별 모델의 예측 데이터를 각각 스태킹 형태로 결합해 최종 메타 모델의 학습용 피처 데이터 세트와 테스트용 피처 데이터 세트를 만드는 것

 

기본 스태킹

# 필요한 모듈 불러오기
import numpy as np

from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 데이터 로드
cancer_data = load_breast_cancer()

X_data = cancer_data.data
y_label = cancer_data.target
# 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(X_data, y_label, test_size=0.2, random_state=0)
# 개별 ML 모델 생성
knn_clf = KNeighborsClassifier(n_neighbors=4)
rf_clf = RandomForestClassifier(n_estimators=100, random_state=0)
dt_clf = DecisionTreeClassifier()
ada_clf = AdaBoostClassifier(n_estimators=100)

# 스태킹으로 만들어진 데이터 세트를 학습, 예측할 최종 모델
lr_final = LogisticRegression()
# 개별 ML 모델 학습
knn_clf.fit(X_train, y_train)
rf_clf.fit(X_train, y_train)
dt_clf.fit(X_train, y_train)
ada_clf.fit(X_train, y_train)
# 개별 ML 모델 예측
knn_pred = knn_clf.predict(X_test)
rf_pred = rf_clf.predict(X_test)
dt_pred = dt_clf.predict(X_test)
ada_pred = ada_clf.predict(X_test)

# 개별 ML 모델의 정확도 확인
print('KNN 정확도:{0:.4f}'.format(accuracy_score(y_test, knn_pred)))
print('랜덤 포레스트 정확도:{0:.4f}'.format(accuracy_score(y_test, rf_pred)))
print('결정트리 정확도:{0:.4f}'.format(accuracy_score(y_test, dt_pred)))
print('에이다부스트 정확도:{0:.4f}'.format(accuracy_score(y_test, ada_pred)))

# 개별 ML 모델의 예측 결과를 피처로 만듬
pred = np.array([knn_pred, rf_pred, dt_pred, ada_pred]) # 예측 결과를 행 형태로 붙임 
print(pred.shape)

pred = np.transpose(pred) # transpose를 이용해 행과 열의 위치 교환. 컬럼별로 각 알고리즘의 예측 결과를 피처로 만듬
print(pred.shape)

# 최종 메타 모델인 로지스틱 회귀 학습/예측/평가
lr_final.fit(pred, y_test)
final = lr_final.predict(pred)

print('최종 메타 모델의 예측 정확도:{0:.4f}'.format(accuracy_score(y_test, final)))

메타 모델인 로지스틱 회귀 기반에서 최종 학습할 때 레이블 데이터 세트로 학습 데이터가 아닌 테스트용 레이블 데이터 세트를 기반으로 학습했기에 과적합 문제가 존재 → 과적합을 개선하기 위한 'CV 세트 기반의 스태킹'

 

CV 세트 기반의 스태킹

개별 모델들이 각각 교차검증으로 메타 모델을 위한 학습용 스태킹 데이터 생성과 예측을 위한 테스트용 스태킹 데이터를 생성한 뒤 이를 기반으로 메타 모델이 학습과 예측을 수행한다. 

 

이해를 돕기 위해 다음과 같이 2단계의 스텝으로 구분하자.

Step1: 각 모델별로 원본 학습/테스트 데이터를 예측한 결과값을 기반으로 메타 모델을 위한 학습용/테스트용 데이터를 생성

Step2: Step1에서 개별 모델들이 생성한 학습용 데이터를 모두 스태킹 형태로 합쳐서 메타 모델이 학습할 최종 학습용 데이터 세트를 생성함. 마찬가지로 각 모델들이 생성한 테스트용 데이터를 모두 스태킹 형태로 합쳐서 메타 모델이 예측할 최종 테스트 데이터 세트를 생성함. 메타 모델은 최종적으로 생성된 학습 데이터 세트와 원본 학습 데이터의 레이블 데이터를 기반으로 학습한 뒤, 최종적으로 생성된 테스트 데이터 세트를 예측하고, 원본 테스트 데이터의 레이블 데이터를 기반으로 평가함

 

Step1의 과정을 도식화해서 로직을 알아보자.

 

●Step1

from sklearn.model_selection import KFold

# 개별 기반 모델에서 최종 메타 모델이 사용할 학습 및 테스트용 데이터를 생성하기 위한 함수 생성
def get_stacking_base_datasets(model, X_train_n, y_train_n, X_test_n, n_folds):
    kf = KFold(n_splits=n_folds, shuffle=False) # ,random_state=0)   # Default shuffle=False 
    # 추후에 메타 모델이 사용할 학습 데이터 반환을 위한 넘파이 배열 초기화
    train_fold_pred = np.zeros((X_train_n.shape[0], 1)) # 2차원, np.zeros((a,b))
    test_pred = np.zeros((X_test_n.shape[0], n_folds)) # 2차원
    print(model.__class__.__name__, 'model 시작')
    
    for folder_counter, (train_index, valid_index) in enumerate(kf.split(X_train_n)):
        # 입력된 학습 데이터에서 기반 모델이 학습/예측할 폴드 데이터 세트 추출
        X_tr = X_train_n[train_index]
        y_tr = y_train_n[train_index]
        X_te = X_train_n[valid_index]
        
        # 폴드 세트 내부에서 다시 만들어진 학습 데이터로 기반 모델의 학습 수행
        model.fit(X_tr, y_tr)
        # 폴드 세트 내부에서 다시 만들어진 검증 데이터로 기반 모델에서 예측 후 데이터 저장
        train_fold_pred[valid_index, :] = model.predict(X_te).reshape(-1, 1) # 2차원, reshape로 1차원에서 2차원으로 변경
        # 입력된 원본 테스트 데이터를 폴드 세트내 학습된 기반 모델에서 예측 후 데이터 저장
        test_pred[:, folder_counter] = model.predict(X_test_n)
        
    # 폴드 세트 내에서 원본 테스트 데이터를 예측한 데이터를 평균하여 테스트 데이터로 생성
    test_pred_mean = np.mean(test_pred, axis=1).reshape(-1, 1) # 2차원, reshape로 1차원에서 2차원으로 변경
    
    # train_fold_pred는 최종 메타 모델이 사용하는 학습 데이터, test_pred_mean은 테스트 데이터
    return train_fold_pred, test_pred_mean
knn_train, knn_test = get_stacking_base_datasets(knn_clf, X_train, y_train, X_test, 7)
rf_train, rf_test = get_stacking_base_datasets(rf_clf, X_train, y_train, X_test, 7)
dt_train, dt_test = get_stacking_base_datasets(dt_clf, X_train, y_train, X_test, 7)
ada_train, ada_test = get_stacking_base_datasets(ada_clf, X_train, y_train, X_test, 7)

 

●Step2

Stack_final_X_train = np.concatenate((knn_train, rf_train, dt_train, ada_train), axis=1)
Stack_final_X_test = np.concatenate((knn_test, rf_test, dt_test, ada_test), axis=1)
print('원본 학습 피처 데이터 shape:', X_train.shape, '원본 테스트 피처 shape:', X_test.shape)
print('스태킹 학습 피처 데이터 shape:', Stack_final_X_train.shape,
     '스태킹 테스트 피처 데이터 shape:', Stack_final_X_test.shape)

lr_final.fit(Stack_final_X_train, y_train)
stack_final = lr_final.predict(Stack_final_X_test)

print('최종 메타 모델의 예측 정확도:{0:.4f}'.format(accuracy_score(y_test, stack_final)))

 

스태킹 모델은 다른 앙상블 모델과 마찬가지로 분류뿐만 아니라 회귀에도 적용 가능하다.

 

위의 실습에서는 개별 모델의 알고리즘에서 파라미터 튜닝을 최적으로 하지 않았지만,

일반적으로 스태킹 모델의 파라미터 튜닝은 개별 알고리즘 모델의 파라미터를 최적으로 튜닝하는 것을 말한다. 즉 스태킹을 이루는 모델은 최적으로 파라미터를 튜닝한 상태에서 스태킹 모델을 만드는 것이 일반적이다.

●해당 데이터 세트는 불균형 데이터 세트로, 레이블인 Class 속성은 매우 불균형한 분포를 가짐

Class 0 → 정상적인 신용카드 트랜잭션 데이터

Class 1 → 신용카드 사기 트랜잭션

●전체 데이터의 약 0.172%만이 레이블 값이 1, 즉 사기 트랜잭션임

●일반적으로 사기 검출, 이상 검출과 같은 데이터 세트는 이처럼 레이블 값이 극도로 불균형한 분포를 가지기 쉬움. 왜냐하면 사기와 같은 이상 현상은 전체 데이터에서 차지하는 비중이 매우 적을 수밖에 없기 때문


1. Data load

●필요한 모듈 및 패키지 불러오기

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline

●데이터 불러오기

card_df = pd.read_csv(r'C:\Users\82102\PerfectGuide\4장\creditcard.csv')
card_df.head()

card_df.info()
card_df.isnull().sum()
card_df.describe()

●데이터 불균형 확인

card_df['Class'].value_counts()

  • Time 피처의 경우 데이터 생성 관련한 작업용 속성으로서 큰 의미가 없기에 제거
  • Amount 피처는 신용카드 트랜잭션 금액
  • Class는 레이블로서 0의 경우 정상, 1의 경우 사기 트랜잭션
  • 결측값은 없음
  • Class는 int형, 나머지 피처들은 float형

2. 데이터 1차 가공 및 모델 학습/예측/평가

●Time 피처 삭제

# 인자로 입력받은 DataFrame을 복사한 뒤 Time 피처만 삭제하고 복사된 DataFrame 반환하는 함수 생성
def get_preprocessed_df(df=None):
    df_copy = df.copy()
    df_copy.drop('Time', axis=1, inplace=True)
    return df_copy

●학습 / 테스트 데이터 세트 분리

  • stratify 방식 사용: 불균형 데이터 세트이므로 학습 / 테스트 데이터 세트는 원본 데이터와 같은 레이블 값 분포로 맞춰주고 → 학습 / 테스트 데이터 세트의 레이블 값 분포를 동일하게 설정
from sklearn.model_selection import train_test_split

# 사전 데이터 가공 후 학습과 테스트 데이터 세트를 반환하는 함수 생성
def get_train_test_dataset(df=None):
    # Time 피처를 삭제한 df를 받아옴
    df_copy = get_preprocessed_df(df)
    
    # 피처, 레이블 분리
    X_features = df_copy.iloc[:, :-1]
    y_target = df_copy.iloc[:, -1]
    
    # 학습 데이터 세트, 테스트 데이터 세트 분리
    X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.3, random_state=0, stratify=y_target)
    
    return X_train, X_test, y_train, y_test

●원본 데이터 함수 적용

X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)
  • 학습 데이터 세트와 테스트 데이터 세트의 레이블 값 비율이 서로 비슷하게 분할됐는지 확인
print('학습 데이터 레이블 값 비율')
print(y_train.value_counts() / y_train.shape[0] * 100)
print('\n')
print('테스트 데이터 레이블 값 비율')
print(y_test.value_counts() / y_test.shape[0] * 100)

●모델 성능 평가 함수 선언

from sklearn.metrics import *

def get_clf_eval(y_test, pred=None, pred_proba=None):
    confusion = confusion_matrix(y_test, pred)
    accuracy = accuracy_score(y_test, pred)
    precision = precision_score(y_test, pred)
    recall = recall_score(y_test, pred)
    f1 = f1_score(y_test, pred)
    roc_auc = roc_auc_score(y_test, pred_proba)
    
    print('오차 행렬')
    print(confusion)
    print('정확도:{0:.4f}, 정밀도:{1:.4f}, 재현율:{2:.4f}, F1:{3:.4f}, AUC:{4:.4f}'.format(accuracy, precision, recall, f1, roc_auc))

●모델 학습/예측/평가

1) LogisticRegression

from sklearn.linear_model import LogisticRegression

lr_clf = LogisticRegression(max_iter=1000)
lr_clf.fit(X_train, y_train)
lr_pred = lr_clf.predict(X_test)
lr_pred_proba = lr_clf.predict_proba(X_test)[:, 1]

get_clf_eval(y_test, lr_pred, lr_pred_proba)

  • 매번, 모델 학습/예측/평가 하는 코드 작성이 귀찮으므로 반복적으로 모델을 변경해 학습/예측/평가 하는 함수 생성
def get_model_train_eval(model, ftr_train=None, ftr_test=None, tgt_train=None, tgt_test=None):
    model.fit(ftr_train, tgt_train)
    pred = model.predict(ftr_test)
    pred_proba = model.predict_proba(ftr_test)[:, 1]
    
    get_clf_eval(tgt_test, pred, pred_proba)

2) LightGBM

LightGBM 2.1.0 이상의 버전에서 boost_from_average 파라미터의 default가 False에서 True로 변경됨. 레이블 값이 극도로 불균형한 분포를 이루는 경우 boost_from_average=True 설정은 재현율 및 ROC-AUC 성능을 매우 크게 저하시킴. 따라서 레이블 값이 극도로 불균형할 경우 boost_from_average를 False로 설정하는 것이 유리

# 본 데이터 세트는 극도로 불균형한 레이블 값 분포도를 가지므로 boost_from_average=False로 설정
from lightgbm import LGBMClassifier

lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False)

get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

3. 데이터 분포도 변환 후 모델 학습/예측/평가

왜곡된 분포도를 가지는 데이터를 재가공한 뒤에 모델을 다시 테스트 해보자

 

●Amount 피처 분포도 확인

Amount 피처는 신용 카드 사용 금액으로 정상/사기 트랜잭션을 결정하는 매우 중요한 피처일 가능성이 높다.

import seaborn as sns

plt.figure(figsize=(8,4))
plt.xticks(range(0, 30000, 1000), rotation=60)
sns.histplot(card_df['Amount'], bins=100, kde=True)
plt.show()

Amount 피처는 1000 이하인 데이터가 대부분이며, 26000까지 드물지만 많은 금액을 사용한 경우가 발생하면서 꼬리가 긴 형태의 분포 곡선을 가진다

 

대부분의 선형 모델은 중요 피처들의 값이 정규분포 형태를 유지하는 것을 선호하기 때문에 Amount 피처를 표준 정규분포 형태로 변환하자

 

●standardscaler를 이용해 Amount 피처 변환

from sklearn.preprocessing import StandardScaler

def get_preprocessed_df(df=None):
    df_copy = df.copy()
    
    # Amount 피처를 표준 정규분포 형태로 변환
    scaler = StandardScaler()
    amount_n = scaler.fit_transform(df_copy['Amount'].values.reshape(-1,1))
    
    # 변환된 Amount 피처를 Amount_Scaled로 피처명 변경후 DataFrame 맨 앞 컬럼으로 추가
    df_copy.insert(0, 'Amount_Scaled', amount_n)
    
    # 기존 Time, Amount 피처 삭제
    df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
    
    return df_copy

●모델 학습/예측/평가

# Amount를 표준 정규분포 형태로 변환 후 모델 학습/예측/평가
X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)

print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression(max_iter=1000)
get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

print('### LightGBM 예측 성능 ###')
lgmb_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False)
get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

●로그변환을 이용해 Amount 피처 변환

def get_preprocessed_df(df=None):
    df_copy = df.copy()
    
    # Amount 피처를 로그변환
    amount_n = np.log1p(df_copy['Amount'])
    
    # 변환된 Amount 피처를 Amount_Scaled로 피처명 변경후 DataFrame 맨 앞 컬럼으로 추가
    df_copy.insert(0, 'Amount_Scaled', amount_n)
    
    # 기존 Time, Amount 피처 삭제
    df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
    
    return df_copy

●모델 학습/예측/평가

# Amount를 로그변환 후 모델 학습/예측/평가
X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)

print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression(max_iter=1000)
get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

print('### LightGBM 예측 성능 ###')
lgmb_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False)
get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

4. 이상치 데이터 제거 후 모델 학습/예측/평가

●IQR을 이용해 이상치 데이터 제거

  • 모든 피처들의 이상치를 검출하는 것은 시간이 많이 소모되며, 결정값과 상관성이 높지 않은 피처들의 경우는 이상치를 제거하더라도 크게 성능 향상에 기여하지 않기 때문에 매우 많은 피처가 있을 경우 이들 중 결정값과 가장 상관성이 높은 피처들을 위주로 이상치를 검출하는 것이 좋다.
import seaborn as sns

plt.figure(figsize=(9,9))
corr = card_df.corr() # DataFrame의 각 피처별로 상관관계를 구함
sns.heatmap(corr, cmap='RdBu') # 상관관계를 시본의 heatmap으로 시각화

상관관계 히트맵에서 Class와 음의 상관관계가 가장 높은 V14, V17 중 V14에 대해서만 이상치를 찾아서 제거해보자

# 이상치 데이터 검출
import numpy as np

def get_outlier(df=None, column=None, weight=1.5):
    quantile_25 = np.percentile(df[column].values, 25)
    quantile_75 = np.percentile(df[column].values, 75)
    
    iqr = quantile_75 - quantile_25
    iqr_weight = iqr * weight
    
    lowest = quantile_25 - iqr_weight
    highest = quantile_75 + iqr_weight
    
    # 최소와 최대 사이에 있지 않은 값은 이상치로 간주하고 인덱스 반환
    outlier_index = df[column][(df[column] < lowest) | (df[column] > highest)].index
    
    return outlier_index
outlier_index = get_outlier(df=card_df, column='V14', weight=1.5)
print('이상치 데이터 인덱스:', outlier_index)
print('이상치 데이터 개수:', len(outlier_index))

  • 현재 creditcard 데이터는 전체 데이터를 기반으로 IQR을 설정하면 너무 많은 데이터가 이상치로 설정되서 삭제되는 범위가 너무 커짐
  • 일반적으로 이상치가 이렇게 많다는 것은 말이 안된다고 판단, 그리고 이상치 제거는 가능한 최소치로 해줘야함
  • 전체 데이터를 IQR로 하면 실제 사기 데이터에 잘 동작하는 데이터도 삭제 시켜버리고 있음

→creditcard 데이터(레이블이 불균형한 분포를 가진 불균형한 데이터)를 통해 생성한 모델은 실제값 0을 0으로 예측하는 것은 기본적으로 잘됨. 하지만 카드 사기 검출 모델에서 중요한 것은 실제 사기 즉 1을 1로 예측하는 것이 중요하므로 타겟값이 1인 데이터에 대해서 적절한 이상치를 제거하는 것이 필요함. 때문에 현재 데이터에서는 1의 데이터만 일정 수준에서 이상치를 제거해 주는게 좋음

# 타겟값이 1인 데이터에 대해서 이상치 데이터 검출
import numpy as np

def get_outlier(df=None, column=None, weight=1.5):
    fraud = df[df['Class']==1][column]
    
    quantile_25 = np.percentile(fraud, 25)
    quantile_75 = np.percentile(fraud, 75)
    
    iqr = quantile_75 - quantile_25
    iqr_weight = iqr * weight
    
    lowest = quantile_25 - iqr_weight
    highest = quantile_75 + iqr_weight
    
    # 최소와 최대 사이에 있지 않은 값은 이상치로 간주하고 인덱스 반환
    outlier_index = fraud[(fraud < lowest) | (fraud > highest)].index
    
    return outlier_index
outlier_index = get_outlier(df=card_df, column='V14', weight=1.5)
print('이상치 데이터 인덱스:', outlier_index)

# 이상치 데이터 제거
def get_preprocessed_df(df=None):
    df_copy = df.copy()
    
    # Amount 피처를 로그변환
    amount_n = np.log1p(df_copy['Amount'])
    
    # 변환된 Amount 피처를 Amount_Scaled로 피처명 변경후 DataFrame 맨 앞 컬럼으로 추가
    df_copy.insert(0, 'Amount_Scaled', amount_n)
    
    # 기존 Time, Amount 피처 삭제
    df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
    
    # 이상치 데이터 삭제하는 로직 추가
    outlier_index = get_outlier(df=df_copy, column='V14', weight=1.5)
    df_copy.drop(outlier_index, axis=0, inplace=True)
    
    return df_copy

●모델 학습/예측/평가

X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)

print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression(max_iter=1000)
get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

print('### LightGBM 예측 성능 ###')
lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False)
get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

5. SMOTE 오버 샘플링 적용 후 모델 학습/예측/평가

재샘플링, 즉 오버 샘플링 및 언더 샘플링을 적용시 올바른 평가를 위해 반드시 학습 데이터 세트에만 적용해야함

●SMOTE 오버 샘플링 적용

from imblearn.over_sampling import SMOTE

smote = SMOTE(random_state=0)
X_train_over, y_train_over = smote.fit_resample(X_train, y_train) # X_train, y_train은 4번 과정까지 전처리 된 후 split된 데이터

# 데이터 확인
print('SMOTE 적용 전 학습용 데이터 세트:', X_train.shape, y_train.shape)
print('SMOTE 적용 후 학습용 데이터 세트:', X_train_over.shape, y_train_over.shape)
print('SMOTE 적용 후 레이블 값 분포')
print(pd.Series(y_train_over).value_counts())

●모델 학습/예측/평가

print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression(max_iter=1000)
get_model_train_eval(lr_clf, ftr_train=X_train_over, ftr_test=X_test, tgt_train=y_train_over, tgt_test=y_test)

print('### LightGBM 예측 성능 ###')
lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False)
get_model_train_eval(lgbm_clf, ftr_train=X_train_over, ftr_test=X_test, tgt_train=y_train_over, tgt_test=y_test)

로지스틱 회귀는 SMOTE 적용 후 재현율이 92.47%로 크게 증가했지만 정밀도가 5.4%로 급격하게 감소했음. 재현율이 높더라도 이 정도로 저조한 정밀도로는 현실 업무에 적용할 수 없음.

로지스틱 회귀 모델에 어떠한 문제가 발생하고 있는지 분류 결정 임계값(threshold)에 따른 정밀도와 재현율 곡선을 통해 알아보자.

precisions, recalls, thresholds = precision_recall_curve(y_test, lr_clf.predict_proba(X_test)[:, 1])
    
# x축을 threshold값, y축은 정밀도, 재현율 값으로 각각 plot 수행, 정밀도는 점선으로 표시
plt.figure(figsize=(8,6))
threshold_boundary = thresholds.shape[0]
plt.plot(thresholds, precisions[0:threshold_boundary], linestyle='--', label='precision')
plt.plot(thresholds, recalls[0:threshold_boundary], label='recall')
    
# threshold 값 x축의 scale을 0.1 단위로 변경
start, end = plt.xlim()
plt.xticks(np.round(np.arange(start, end, 0.1), 2))
    
# x, y축 labe과 legend, grid 설정
plt.xlabel('Threshold value')
plt.ylabel('Precision and Recall value')
plt.legend()
plt.grid()
plt.show()


(번외로, LightGBM 모델도 precision_recall_curve를 그리려면 다음과 같이 함수를 만드는 것이 편리)

from sklearn.metrics import precision_recall_curve

def precision_recall_curve_plot(y_test, pred_proba_c1):
    # threshold ndarray와 이 threshold에 따른 정밀도, 재현율 ndarray 추출
    precisions, recalls, thresholds = precision_recall_curve(y_test, pred_proba_c1)
    
    # x축을 threshold값, y축은 정밀도, 재현율 값으로 각각 plot 수행, 정밀도는 점선으로 표시
    plt.figure(figsize=(8,6))
    threshold_boundary = thresholds.shape[0]
    plt.plot(thresholds, precisions[0:threshold_boundary], linestyle='--', label='precision')
    plt.plot(thresholds, recalls[0:threshold_boundary], label='recall')
    
    # threshold 값 x축의 scale을 0.1 단위로 변경
    start, end = plt.xlim()
    plt.xticks(np.round(np.arange(start, end, 0.1), 2))
    
    # x, y축 labe과 legend, grid 설정
    plt.xlabel('Threshold value')
    plt.ylabel('Precision and Recall value')
    plt.legend()
    plt.grid()
    plt.show()
precision_recall_curve_plot(y_test, lr_clf.predict_proba(X_test)[:, 1])
precision_recall_curve_plot(y_test, lgbm_clf.predict_proba(X_test)[:, 1])

threshold가 0.99 이하에서는 재현율이 매우 좋고, 정밀도가 극단적으로 낮다가 0.99 이상에서는 반대로 재현율이 대폭 떨어지고 정밀도가 높아짐. threshold를 조정하더라도 threshold의 민감도가 너무 심해 올바른 재현율/정밀도 성능을 얻을 수 없으므로 로지스틱 회귀 모델의 경우 SMOTE 적용 후 올바른 예측 모델이 생성되지 못했음.

 

LightGBM의 모델 성능 결과를 보면, 재현율은 증가했지만 정밀도가 감소했음을 알 수 있음. 이처럼 SMOTE를 적용하면 재현율은 높아지나, 정밀도는 낮아지는 것이 일반적임. 때문에 정밀도 지표보다 재현율 지표를 높이는 것이 중요한 업무에서SMOTE를 사용하면 좋음!

+ Recent posts