배운 내용
- 경사하강법
- np.random.rand()
- np.random.randn()
- np.zeros_like()
- np.ones()
- np.zeros()
- LinearRegression
- sns.regplot()
- plt.subplots()
- pd.Series(data, index)
- Series.sortvalue(ascending)
- cross_val_score()
- PolynomialFeatures
- Pipeline
- 편향 - 분산 트레이드 오프 (Bias - Variance Trade-off)
- Ridge (L2 regularization)
- Lasso (L1 regularization)
- ElasticNet (L2 + L1)
- StandardScaler
- MinMaxScaler
- np.log1p()
- LogisticRegression
- RandomForestRegressor
회귀
회귀(regression)는 데이터 값이 평균과 같은 일정한 값으로 돌아가려는 경향을 이용한 통계학 기법입니다. 사람의 키는 유전학 적으로 부모의 키가 큰 사람의 자식은 부모보다 작고, 부모의 키가 작은 사람의 자식은 부모보다 큰데, 이것이 회귀의 한 예입니다.
통계학 용어를 빌리면, 여러 개의 독립 변수와 한 개의 종속 변수 간의 상관관계를 모델링하는 기법을 통칭합니다. 여기서 종속 변수는 Y, X1 등은 독립 변수입니다.
예를 들면, Y가 아파트 값이라고 하면 X1 .. Xn은 방 개수, 방 크기, 위치 .. 등을 의미합니다. W1~Wn은 독립변수의 값에 영향을 미치는 회귀 계수(Regression coefficients)입니다. 머신러닝 관점에서, 독립변수는 피처에 해당하며 종속변수는 결정값에 해당합니다. 머신러닝 회귀 예측의 핵심은 주어진 피처와 결정값 데이터 기반에서 학습을 통해 최적의 회귀 계수를 찾는 것입니다.
회귀는 회귀 계수의 선형/비선형 여부, 독립변수의 개수, 종속변수의 개수에 따라 여러 가지 유형으로 나눌 수 있습니다. 회귀에서 가장 중요한 것은 회귀 계수입니다. 회귀 계수가 선형/비선형 여부에 따라 선형 회귀, 비선형 회귀로 나눌 수 있습니다. 그리고 독립변수의 개수가 한 개인지 여러 개인지에 따라 단일 회귀, 다중 회귀로 나뉩니다.
회귀
회귀는 예측값이 카테고리와 같은 이산형 클래스 값이 아닌 연속형 숫자값입니다. 회귀 중에서 가장 많이 쓰이는 회귀는 선형 회귀입니다. 선형 회귀는 실제 값과 예측값의 차이(오류의 제곱값)를 최소화 하는 직선형 회귀선을 최적화하는 방식입니다.
선형 회귀 모델은 규제(Regularization) 방법에 따라 다시 별도의 유형으로 나뉠 수 있습니다. 규제는 일반적인 선형 회귀의 과적합 문제를 해결하기 위해 패널티 값을 적용하는 것을 말합니다. 대표적인 선형 회귀 모델은 아래와 같습니다.
- 일반 선형 회귀: 예측값과 실제 값의 RSS(Residual Sum of Squares)를 최소화할 수 있도록 회귀 계수를 최적화하며, 규제(Regularization)를 적용하지 않은 모델입니다.
- 릿지(Ridge): 릿지 회귀는 선형 회귀에 L2 규제를 추가한 회귀 모델입니다. L2 규제는 상대적으로 큰 회귀 계수 값의 예측 영향도를 감소시키기 위해서 회귀 계수값을 더 작게 만드는 규제 모델입니다.
- 라쏘(Lasso): 라쏘 회귀는 선형 회귀에 L1 규제를 적용한 방식입니다. L1 규제는 예측 영향력이 작은 피처의 회귀 계수를 0으로 만들어 회귀 예측 시 피처가 선택되지 않게 하는 것입니다. 이러한 특성 때문에 L1 규제는 피처 선택 기능으로 불립니다.
- 엘라스틱넷(ElasticNet): L1, L2 규제를 함께 결합한 모델입니다. 주로 피처가 많은 데이터 세트에 적용합니다. L1 규제로 피처의 개수를 줄이고 L2 규제로 계수 값의 크기를 조절합니다.
- 로지스틱 회귀(Logistic Regression): 선형 모델이며, 이진 분류 뿐만 아니라 희소 영역 분류(텍스트 분류) 영역에서 뛰어난 예측 성능을 보입니다.
단순 선형 회귀를 통한 회귀 이해
단순 선형 회귀는 독립변수도 하나, 종속변수도 하나인 선형 회귀입니다. 예를 들어 주택 가격이 집의 크기로만 결정된다면, 일반 적으로 주택의 크기가 크면 가격이 높아지는 경향이 있기 때문에 주택가격은 주택 크기에 대해 선형(직선 형태)의 관계로 표현할 수 있습니다.
여기서 회귀 계수는 w0, w1입니다. 실제 값은 Yi + 오류값입니다. 오류값은 잔차라고도 부릅니다.
최적의 회귀 모델을 만든다는 것은 전체 데이터의 잔차(오류값) 합이 최소가 되는 모델을 만든다는 의미입니다. 동시에 오류값 합이 최소가 될 수 있는 회귀 계수를 찾는다는 의미입니다. 오류값은 +나 -일 수 있어 보통 절대값을 취해서 더하거나 제곱을 구해서 더합니다.
오류값을 제곱해 더하는 방식을 RSS(Residual Sum of Square)라 합니다. 일반적으로 미분 등의 계산을 편리하게 하기 위해서 RSS방식으로 오류 합을 구합니다.
RSS를 최소화하는 회귀 계수를 찾는 것이 회귀의 핵심이므로, 중심 변수는 W이며, 학습데이터로 입력되는 독립변수 X, 종속변수 Y
는 RSS에서 모두 상수로 간주합니다. 일반적으로 RSS는 학습 데이터의 건수로 나누어 아래와 같이 정규화된 식으로 표현됩니다.
회귀에서 이 RSS는 비용(Cost)이며 w변수(회귀 계수)로 구성되는 RSS를 비용 함수라고 합니다. 머신러닝 회귀 알고리즘은 비용함수가 반환하는 값(오류값)을 감소시켜 더 이상 감소하지 않는 최소의 오류값을 구하는 것입니다. 비용 함수를 손실 함수(loss function)라고도 합니다.
비용 최소화하기 - 경사 하강법(Gradient Descent) 소개
경사 하강법은 '데이터를 기반으로 알고리즘이 스스로 학습한다'는 머신러닝의 개념을 가능하게 해준 핵심 기법 중 하나입니다. 경사 하강법은 '점진적으로' 반복적인 계산을 통해 W 파라미터 값을 업데이트하면서 오류 값이 최소가 되는 W 파라미터를 구하는 방식입니다.
비용 함수를 미분값이 계속 감소하는 방향으로 순차적으로 업데이트할 때 |미분값|이 최소인 지점(여기선 0이 되는 지점)의 w를 반환합니다.
RSS(w0, w1) = R(w)로 할 때 W 파라미터가 두개이므로 w0, w1에 대해 각 편미분을 구해야 합니다.
학습률을 η라할 때 경사 하강법은 w1, w0를 아래 식으로 계속 업데이트를 해서 최소인 지점에 R(w)의 미분값이 최소인 지점의 w를 반환합니다.
w1, w0의 초기값은 임의로 선택하고, 위 식을 통해 업데이트 해가며 최적의 W를 찾아갑니다. 지금까지 정리한 수식과 절차를 이용해 경사 하강법을 파이썬 코드로 구현해보겠습니다.
회귀식 y=4x+6을 근사하기 위한 100개의 데이터 세트를 만들고, 여기에 경사 하강법을 이용해 회귀 계수 w1, w0를 도출하겠습니다. 먼저 단순 선형 회귀로 예측할만한 데이터 세트를 만들어보겠습니다.
np.random.rand() & np.random.randn()
np.random.rand(5)
'''
# 0~1의 균일 분포 표준정규분포 난수 5개 생성
array([0.20747008, 0.42468547, 0.37416998, 0.46357542, 0.27762871])
'''
np.random.randn(3,2)
'''
# 평균 0, 표준편차 1의 가우시안 표준정규분포 난수 array(10,2) 생성
array([[ 0.20707557, 0.52346303],
[-0.69939482, 0.91370581],
[-0.67278478, 0.1333245 ]])
'''
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0)
# y = 4X + 6을 근사(w1=4, w0=6). 임의의 값은 노이즈를 위해 만듦.
X = 2*np.random.rand(100, 1)
y = 6 + 4*X + np.random.randn(100, 1)
# X, y 데이터 세트 산점도 시각화
plt.scatter(X, y)
'''
결과1
'''
비용함수를 정의해보겠습니다.
def get_cost(y, y_pred):
N = len(y)
cost = np.sum(np.square(y - y_pred))/N
return cost
np.zeros_like() & np.ones()
가중치 업데이트 수식의 η*(W에 대한 편미분) 부분을 파이썬으로 코딩해보겠습니다.
# 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
np.zeros()
get_weight_updates( ) 함수를 경사 하강 방식으로 반복적으로 수행하여 w1과 w0를 업데이트하는 함수인 gradient_descent_steps( ) 함수를 생성하겠습니다. w0, w1이 (1, 1)인 이유는 w0, w1이 상수이기 때문이다.
# 입력 인자 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
gradient_descent_steps( ) 를 호출해 w1과 w0를 구해보겠습니다. 그리고 RSS의 차이를 계산하는 get_cost( ) 함수로 경사 하강법의 예측 오류도 계산하겠습니다.
w1, w0 = gradient_descent_steps(X, y, iters=1000)
# w1[0, 0]으로 쓰는 이유는 w1이 2차원 배열 형태이기 때문이다.
# w1 = array([[4.02181364]])
print("w1:{0:.3f} w0:{1:.3f}".format(w1[0, 0], w0[0, 0]))
y_pred = w1[0, 0]*X + w0
print('Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y, y_pred)))
'''
w1:4.022 w0:6.162
Gradient Descent Total Cost:0.9935
'''
실제 값인 w1=4, w0=6과 유사한 4.022, 6.162가 도출됐습니다. 예측 오류 비용은 0.9935가 도출됐습니다. 앞에서 구한 y_pred에 기반해 회귀선을 그려보겠습니다.
plt.scatter(X, y)
plt.plot(X, y_pred)
'''
결과2
'''
경사 하강법은 모든 학습 데이터에 대해 반복적으로 비용함수 최소화를 위한 값을 업데이트하기 때문에 수행 시간이 매우 오래 걸립니다. 그 때문에 실전에서는 대부분 확률적 경사 하강법(Stochastic Gradient Descent)를 이용합니다. 확률적 경사 하강법은 모든 입력 데이터가 아닌 일부 데이터만 이용해 W가 업데이트되는 값을 계산하므로 빠른 속도를 보장합니다.
따라서 대용량 데이터의 경우 확률적 경사 하강법이나 미니배치 확률적 경사 하강법을 이용해 최적 비용함수를 도출합니다. 확률적 경사 하강법을 stochastic_gradient_descent_steps( ) 함수로 구현해 보겠습니다. 확률적 경사 하강법은 단지 앞에서 구현한 gradient_descent_steps( )에 batch_size만큼 데이터를 추출해서 W를 업데이트 하는데 사용할 뿐입니다.
def stochastic_gradient_descent_steps(X, y, batch_size=10, iters=1000):
w0 = np.zeros((1, 1))
w1 = np.zeros((1, 1))
prev_cost = 100000
iter_index = 0
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])
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
stochastic_gradient_descent_steps()를 이용해 w1, w0 및 예측 오류 비용을 계산해보겠습니다.
w1, w0 = stochastic_gradient_descent_steps(X, y, iters=10000)
print("w1:", round(w1[0, 0], 3), "w0:", round(w0[0, 0], 3))
y_pred = w1[0, 0]*X + w0
print('Stochastic Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y, y_pred)))
'''
w1: 3.988 w0: 6.244
Stochastic Gradient Descent Total Cost:0.9942
'''
비용 면에서도 큰 차이가 없고, w0, w1 또한 큰 차이가 없음을 알 수 있습니다. 큰 데이터를 처리할 경우에 경사 하강법은 시간이 매우 오래 걸리므로 일반적으로 확률적 경사 하강법을 이용합니다.
지금까지는 피처가 1개, 즉 독립변수가 1개인 단순 선형 회귀에서 경사 하강법을 살펴봤습니다. 피처가 1개일 경우 2개의 회귀 계수 W가 도출됐습니다. 그렇다면 피처가 M(X1, X2 .. , XM)개 있다면, 그에 따른 회귀 계수는 M+1개 도출됩니다. Feat 0은 w0를 W 배열 안에 포함시키기 위해 모든 데이터 값이 1인 피처를 추가한 것입니다. Feat 1은 X1 피처, Feat 2는 X2 피처 입니다.
사이킷런 LinearRegression을 이용한 보스턴 주택 가격 예측
사이킷런의 linear_models 모듈은 다양한 선형 기반 회귀를 클래스로 구현해 제공합니다. 선형 모델 중 규제가 적용되지 않은 선형 회귀를 사이킷런에서 구현한 클래스인 LinearRegression을 이용해 보스턴 주택 가격 예측 회귀를 구현할 것입니다.
LinearRegression 클래스 - Ordinary Least Squares
LinearRegression 클래스는 RSS(Residual Sum of Squares)를 최소화해 OLS(Ordinary Least squares) 추정 방식으로 구현한 클래스입니다. LinearRegression 클래스는 fit( ) 메서드로 X, y 배열을 입력 받으면 회귀 계수(Coefficients)인 W를 coef_속성에 저장합니다.
from sklearn.linear_model import LinearRegression
LinearRegression 클래스의 입력 파라미터는 아래와 같습니다.
- fit_intercept: 불린 값으로 default는 True입니다. Intercept(절편) 값을 계산할 것인지 말지를 지정합니다. 만일 False로 지정하면 intercept가 사용되지 않고 0으로 지정됩니다.
- normalize: 불린 값으로 default는 False입니다. fit_intercept가 False인 경우에는 이 파라미터가 무시 됩니다. 만일 True이면 회귀를 수행하기 전에 입력 데이터 세트를 정규화합니다.
LinearRegression 클래스의 속성은 아래와 같습니다.
- coef_: fit( ) 메서드를 수행했을 때 회귀 계수가 배열 형태로 저장되게 하는 속성, Shape는 (Target 값 개수, 피처 개수)
- intercept_: intercept 값
Ordinary Least Squares 기반의 회귀 계수 계산은 입력 피처의 독립성에 많은 영향을 받습니다. 피처 간의 상관관계가 매우 높은 경우 분산이 매우 커져서 오류에 민감해집니다. 이러한 현상을 다중 공선성(multi-collinearity) 문제라고 합니다. 일반적으로 상관관계가 높은 피처가 많은 경우 독립적인 중요한 피처만 남기고 제거하거나 규제를 적용합니다. 또한 매우 많은 피처가 다중 공선성 문제를 가지고 있다면, PCA를 통해 차원 축소를 수행하는 것도 고려해볼 수 있습니다.
회귀 평가 지표
예측된 회귀 모델을 평가하기 위한 지표는 실제 값과 회귀 예측값의 차이에 기반한 지표가 중심입니다. 보통 오류로 절댓값의 평균이나 제곱, 오류에 루트를 씌운 평균값을 사용합니다. 일반적으로 회귀의 성능을 평가하는 지표는 다음과 같습니다.
평가 지표 | 설명 | 수식 |
MAE | Mean Absolute Error(MAE)이며 실제값과 예측값 차이를 절대값으로 변환해 평균한 것입니다. | |
MSE | Mean Squared Error(MSE)이며 실제값과 예측값의 차이를 제곱해 평균한 것입니다. | |
RMSE | MSE 값은 오류의 제곱으 구하므로 실제 오류 평균보다 더 커지는 특성이 있으므로 MSE에 루트를 씌운 것이 RMSE(Root Mean Squared Error)입니다. | |
R² | 분산 기반으로 예측 성능을 평가합니다. (실제값의 분산/ 예측값의 분산) 입니다. 1에 가까울 수록 예측 정확도가 높습니다. |
이 밖에도 MSE, RMSE에 로그를 적용한 MSLE(Mean Squared Log Error)와 RMSLE(Root Mean Squared Log Error)도 사용합니다. 사이킷런은 RMSE를 제공하지 않아 계산하는 함수를 직접 만들어야 합니다. 다음은 각 평가 방법에 대한 사이킷런의 API 및 cross_val_score나 GridSearchCV에서 평가시 사용되는 scoring 파라미터의 적용값입니다.
평가 방법 | 사이킷런 평가 지표 API | Scoring 함수 적용 값 |
MAE | metrics.mean_absolute_error | 'neg_mean_absolute_error' |
MSE | metrics.mean_squared_error | 'neg_mean_squared_error' |
R² | metrics.r2_score | 'r2' |
주의해야할 점이 사이킷런 평가 지표 API의 경우 평가 지표값을 그대로 반환하는데, Scoring 함수는 평가 지표값에 -1을 곱해서 반환합니다. 이는 사이킷런의 Scoring 함수가 score 값이 클 수록 좋은 평가 결과로 자동 평가하기 때문입니다. GridSearchCV는 의 경우 자동으로 가장 높은 Score의 파라미터를 Best Estimator에 적용하기까지 합니다.
LinearRegression을 이용해 보스턴 주택 가격 회귀 구현
이제 LinearRegression 클래스를 이용해 선형 회귀 모델을 만들겠습니다. 보스턴 주택 가격 데이터의 피처는 다음을 의미합니다.
- CRIM: 지역별 범죄 발생률
- ZN: 25,000 평방 피트를 초과하는 거주 지역의 비율
- INDUS: 비상업 지역 넓이 비율
- CHAS: 찰스강에 대한 더미 변수(강의 경계에 위치한 경우는 1, 아니면 0)
- NOX: 일산화질소 농도
- RM: 거주할 수 있는 방 개수
- AGE: 1940년 이전에 건출된 소유 주택의 비율
- DIS: 5개 주요 고용센터까지의 가중 거리
- RAD: 고속도로 접근 용이도
- TAC: 10,000 달러 당 재산세율
- PTRATIO: 지역의 교사와 학생 수 비율
- B: 지역의 흑인 거주 비율
- LSTAT: 하위 계층의 비율
- MEDV: 본인 소유의 주택 가격 (중앙값)
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from scipy import stats
from sklearn.datasets import load_boston
%matplotlib inline
# boston 데이터 세트 로드
boston = load_boston()
# boston 데이터 세트 DataFrame 변환
bostonDF = pd.DataFrame(boston.data, columns=boston.feature_names)
# boston 데이터 세트의 target 배열은 주택 가격임. 이를 Price 칼럼으로 DataFrame에 추가함.
bostonDF['PRICE'] = boston.target
print('Boston 데이터 세트 크기:', bostonDF.shape)
bostonDF.head()
'''
Boston 데이터 세트 크기: (506, 14)
결과1
'''
데이에 Null이나, Null 값을 대체한 값이 있는지 확인해보겠습니다.
bostonDF.info()
'''
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 506 entries, 0 to 505
Data columns (total 14 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 CRIM 506 non-null float64
1 ZN 506 non-null float64
2 INDUS 506 non-null float64
3 CHAS 506 non-null float64
4 NOX 506 non-null float64
5 RM 506 non-null float64
6 AGE 506 non-null float64
7 DIS 506 non-null float64
8 RAD 506 non-null float64
9 TAX 506 non-null float64
10 PTRATIO 506 non-null float64
11 B 506 non-null float64
12 LSTAT 506 non-null float64
13 PRICE 506 non-null float64
dtypes: float64(14)
memory usage: 55.5 KB
'''
bostonDF.describe()
'''
결과2
'''
Null 값으로 명시된 값은 없고 Null을 대체한 값도 없어 보입니다.
sns.regplot() & plt.subplots
각 칼럼이 회귀 결과에 미치는 영향이 어느 정도인지 시각화해보겠습니다. 총 14개의 칼럼 중 8개의 칼럼에 대해 피처(칼럼) 값이 증가할 수록 PRICE 값이 어떻게 변하는지 확인합니다. Seaborn의 regplot( ) API는 X, Y, 산점도와 함께 선형 회귀 직선을 그려 줍니다. matplotlib.subplots( )를 이용해 각 ax 마다 피처(칼럼)와 PRICE의 관계를 표현합니다.
plt.subplots()의 ncols=4, nrosw=2이면 2개의 행과 4개의 열에 총 8개의 그래프를 그릴 수 있습니다.
# 2개의 행과 4개의 열을 가진 subplots를 이용. axs는 2x4개의 ax를 가짐.
fig, axs = plt.subplots(figsize=(16, 8), ncols=4, nrows=2)
lm_features = ['RM', 'ZN', 'INDUS', 'NOX', 'AGE', 'PTRATIO', 'LSTAT', 'RAD']
for i, feature in enumerate(lm_features):
row = int(i/4)
col = i%4
# 시본의 regplot을 이용해 산점도와 선형 회귀 직선을 함께 표현
sns.regplot(x=feature, y='PRICE', data=bostonDF, ax=axs[row][col])
'''
결과3
'''
직선에 가장 수렴하는 그래프의 피처는 RM과 LSTAT입니다. 이는 RM과 LSTAT가 PRICE에 끼치는 영향이 크다는 의미입니다. 특히, RM(방 개수)는 양 방향의 선형성(Positive Linearity)이 가장 크고, LSTAT(하위 계층 비율)는 음 방향의 선형성(Negative Linearity)이 가장 큽니다.
mean_squared_error( ) & r2_score( )
LinearRegression 클래스를 이용해 보스턴 주택 가격의 회귀 모델을 만들겠습니다. train_test_split()과 metrics 모듈의 API를 이용해 MSE와 R2 Score를 측정하겠습니다.
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
y_target = bostonDF['PRICE']
X_data = bostonDF.drop(['PRICE'], axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(X_data, y_target, test_size=0.3,\
random_state=156)
# 선형 회귀 OLS로 학습/예측/평가 수행.
lr = LinearRegression()
lr.fit(X_train, y_train)
y_preds = lr.predict(X_test)
mse = mean_squared_error(y_test, y_preds)
rmse = np.sqrt(mse)
print('MSE: {0:.3f}, RMSE: {1:.3f}'.format(mse, rmse))
print('Variance score: {0:.3f}'.format(r2_score(y_test, y_preds)))
'''
MSE: 17.297, RMSE: 4.159
Variance score: 0.757
'''
이번엔 LinearRegression 모델의 intercept(절편)과 coefficients(회귀 계수) 값을 보겠습니다. 각각 LinearRegression 객체의 intercept_, coef_ 속성에 값이 저장돼 있습니다.
print('절편 값:', lr.intercept_)
print('회귀 계수값:', np.round(lr.coef_, 1))
'''
절편 값: 40.995595172164755
회귀 계수값: [ -0.1 0.1 0. 3. -19.8 3.4 0. -1.7 0.4 -0. -0.9 0. -0.6]
'''
pd.Series(data, index) & Series.sortvalue(ascending)
회귀 계수 값을 각 피처에 매핑하고 높은 값 순으로 출력해보겠습니다.
coeff = pd.Series(data=np.round(lr.coef_, 1), index=X_data.columns)
coeff.sort_values(ascending=False)
'''
RM 3.4
CHAS 3.0
RAD 0.4
ZN 0.1
B 0.0
TAX -0.0
AGE 0.0
INDUS 0.0
CRIM -0.1
LSTAT -0.6
PTRATIO -0.9
DIS -1.7
NOX -19.8
dtype: float64
'''
양의 회귀 계수중 RM이 가장 크며, 음의 회귀 계수 중 NOX가 가장 큽니다. NOX가 너무 커보입니다.
cross_val_score( )
5개의 폴드 세트에서 cross_val_score( )를 이용해 교차 검증으로 MSE와 RMSE를 측정해보겠습니다. 앞서 말했듯 scikit - learn은 RMSE를 제공하지 않으므로 만들어 사용하겠습니다.
from sklearn.model_selection import cross_val_score
y_target = bostonDF['PRICE']
X_data = bostonDF.drop(['PRICE'], axis=1, inplace=False)
lr = LinearRegression()
# cross_val_score( )로 5폴드 세트로 MSE를 구한 뒤 이를 기반으로 다시 RMSE를 구함.
neg_mse_scores = cross_val_score(lr, X_data, y_target, scoring="neg_mean_squared_error", cv=5)
rmse_scores = np.sqrt(-1*neg_mse_scores)
avg_rmse = np.mean(rmse_scores)
# cross_val_score(scoring="neg_mean_squared_error")로 반환된 값은 모두 음수
print(' 5 folds의 개별 Negative MSE scores:', np.round(neg_mse_scores, 2))
print(' 5 folds의 개별 RMSE scores:', np.round(rmse_scores, 2))
print(' 5 folds의 평균 RMSE: {0:.3f}'.format(avg_rmse))
'''
5 folds의 개별 Negative MSE scores: [-12.46 -26.05 -33.07 -80.76 -33.31]
5 folds의 개별 RMSE scores: [3.53 5.1 5.75 8.99 5.77]
5 folds의 평균 RMSE: 5.829
'''
다항 회귀와 과(대)적합/과소적합 이해
다항 회귀 이해
지금까지 회귀는 y = w0 + w1*x1 + w2*x2 ... + wn*xn 과 같이 독립 변수(feature)와 종속 변수(target)의 관계가 일차 방정식 형태로 표현됐습니다. 회귀가 독립 변수의 단항식이 아닌 2차, 3차 방정식과 같은 다항식으로 표현되는 것을 다항(Polynomial) 회귀라고 합니다. 다항 회귀와 비선형 회귀를 혼동할 수 있는데, 비선형 회귀는 독립 변수가 선형/비선형인지 여부가 아닌, 회귀 계수가 선병/비선형인지 여부로 나뉩니다.
독립 변수가 비선형인 경우 Z를 위와 같이 표현하면 여전히 선형 회귀이기 때문입니다. 피처 X에 대한 Target Y값의 관계를 단순 선형 회귀 직선형으로 표현한 것보다 다항 회귀 곡선형으로 표현한 것이 더 예측 성능이 높습니다.
PolynomialFeatures
사이킷런은 다항 회귀를 위한 클래스를 명시적으로 제공하지 않지만, 다항 회귀역시 선형 회귀이기 때문에 비선형 함수를 선형 모델에 적용시키는 방법을 사용해 구현해야 합니다. 이를 위해 사이킷런은 PolynomialFeatures 클래스를 통해 입력 받은 단항식 피처를 degree에 해당하는 Polynomial(다항식) 피처로 변환합니다.
다음 예제는 단항값 [x₁, x₂]를 2차 다항값 [ 1, x₁, x₂, x₁², x₁x₂, x₂² ]으로 변환하는 예제입니다. degree=2는 단항값의 값들의 2차 식 까지의 모든 경우의 수를 반환합니다. 예를 들면, [x₁=0, x₂=1]을 [1, 0, 1, 0, 0, 1]로 반환합니다.
from sklearn.preprocessing import PolynomialFeatures
import numpy as np
# 다항식으로 변환된 단항식 생성, [[0, 1], [2, 3]]의 2 x 2 행렬 생성
X = np.arange(4).reshape(2, 2)
print('일차 단항식 계수 피처:\n', X)
# degree = 2인 2차 다항식으로 변환하기 위해 PolynomialFeatures를 이용해 반환
poly = PolynomialFeatures(degree=2)
poly.fit(X)
poly_ftr = poly.transform(X)
print('변환된 2차 다항식 계수 피처:\n', poly_ftr)
'''
일차 단항식 계수 피처:
[[0 1]
[2 3]]
변환된 2차 다항식 계수 피처:
[[1. 0. 1. 0. 0. 1.]
[1. 2. 3. 4. 6. 9.]]
'''
X에 대한 레이블 데이터 y = 1 + 2x₁ + 3x₁² + 4x₂³으로 3차 다항 회귀의 결정 함수 식을 만들어 보겠습니다.
def polynomial_func(X):
y = 1 + 2*X[:, 0] + 3*X[:, 0]**2 + 4*X[:, 1]**3
return y
X = np.arange(4).reshape(2, 2)
print('일차 단항식 계수 feature: \n', X)
y = polynomial_func(X)
print('삼차 다항식 결정값: \n', y)
'''
일차 단항식 계수 feature:
[[0 1]
[2 3]]
삼차 다항식 결정값:
[ 5 125]
'''
단항식 계수를 삼차 다항식 계수로 변환하고, 이를 선형 회귀에 적용하면 다항 회귀로 구현됩니다. PolynomialFeatures(degree=3)은 단항 계수 피처 [x₁, x₂]를 3차 다항 계수 [1, x₁, x₂, x₁², x₁x₂, x₂², x₁³, x₁²x₂, x₁x₂², x₂³] 로 변환합니다.
from sklearn.linear_model import LinearRegression
# 3차 다항식 변환
poly_ftr = PolynomialFeatures(degree=3).fit_transform(X)
print('3차 다항식 계수 feature:\n', poly_ftr)
# LinearRegression에 3차 다항식 계수 feature와 3차 다항식 결정값으로 학습 후 회귀 계수 확인
model = LinearRegression()
model.fit(poly_ftr, y)
print('Polynomial 회귀 계수\n', np.round(model.coef_, 2))
print('Polynomial 회귀 shape', model.coef_.shape)
'''
3차 다항식 계수 feature:
[[ 1. 0. 1. 0. 0. 1. 0. 0. 0. 1.]
[ 1. 2. 3. 4. 6. 9. 8. 12. 18. 27.]]
Polynomial 회귀 계수
[0. 0.18 0.18 0.36 0.54 0.72 0.72 1.08 1.62 2.34]
Polynomial 회귀 shape (10,)
'''
[0. 0.18 0.18 0.36 0.54 0.72 0.72 1.08 1.62 2.34]는 [1, 2, 0, 3, 0, 0, 0, 0, 0, 4]와 차이는 있지만 다항 회귀로 근사함을 알 수 있습니다. 이 처럼 사이킷런은 PolynomialFeatures로 변환 후에 LinearRegression 클래스로 다항 회귀를 구현합니다.
Pipeline
Pipeline 객체를 이용하면, 한 번에 피처 변환과 선형 회귀 적용을 할 수 있습니다.
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
import numpy as np
def polynomial_func(X):
y = 1 + 2*X[:, 0] + 3*X[:, 0]**2 + 4*X[:, 1]**3
return y
# Pipeline 객체로 Streamline하게 Polynomial Feature 변환과 Linear Regression을 연결
model = Pipeline([('poly', PolynomialFeatures(degree=3)),
('linear', LinearRegression())])
X = np.arange(4).reshape(2, 2)
y = polynomial_func(X)
model = model.fit(X, y)
print('Polynomial 회귀 계수\n', np.round(model.named_steps['linear'].coef_, 2))
'''
Polynomial 회귀 계수
[0. 0.18 0.18 0.36 0.54 0.72 0.72 1.08 1.62 2.34]
'''
다항 회귀를 이용한 과소적합 및 과적합 이해
다항 회귀는 피처의 직선적 관계가 아닌 복잡한 다항 관계를 모델링할 수 있습니다. 차수가 높을 수록 복잡한 피처 간의 관계까지 모델링이 가능하지만, 차수(degree)를 높일 수록 학습 데이터에만 너무 맞춘 학습이 이뤄져 과적합 문제가 발생할 수 있습니다.
사이킷런 홈페이지에서 다항 회귀를 이용해 과소적합과 과적합 문제를 잘 보여주는 예제입니다. 간략히 설명하자면, 피처 X와 target Y가 잡음(Noise)이 포함된 다항식의 코사인(Cosine) 그래프 관계를 가지도록 만들어 줍니다. 그리고 이에 기반해 다항 회귀의 차수를 변화시키면서 그에 따른 회귀 예측 곡선과 예측 정확도를 비교하는 예제입니다.
학습 데이터는 30개의 임의 데이터인 X, 그리고 CosX + Noise인 target y로 구성됩니다.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
%matplotlib inline
# 임의의 값으로 구성된 X값에 대해 코사인 변환 값을 반환.
def true_fun(X):
return np.cos(1.5*np.pi*X)
# X는 0부터 1까지 30개의 임의의 값을 순서대로 샘플링한 데이터입니다.
np.random.seed(0)
n_samples = 30
X = np.sort(np.random.rand(n_samples))
# y 값은 코사인 기반의 true_func()에서 약간의 노이즈 변동 값을 더한 값입니다.
y = true_fun(X) + np.random.randn(n_samples)*0.1
다항식 차수를 1, 4, 15로 변경하며 예측 결과를 비교하겠습니다. cross_val_score()로 MSE를 구해 예측 성능을 평가하고 0~1까지 균일하게 구성된 100개의 테스트용 데이터 세트를 이용해 차수별 회귀 예측 곡선을 그려보겠습니다.
plt.figure(figsize=(14, 5))
degrees=[1, 4, 15]
# 다항 회귀의 차수(degree)를 1, 4, 15로 각각 변화시키면서 비교합니다.
for i in range(len(degrees)):
ax = plt.subplot(1, len(degrees), i+1)
plt.setp(ax, xticks=(), yticks=())
# 개별 degree별로 Polynomial 변환합니다.
polynomial_features = PolynomialFeatures(degree=degrees[i], include_bias=False)
linear_regression=LinearRegression()
pipeline = Pipeline([('polynomial_features', polynomial_features),
('linear_regression', linear_regression)])
# 피처가 X 1개임
pipeline.fit(X.reshape(-1, 1), y)
# 교차 검증으로 다항 회귀를 평가합니다.
scores = cross_val_score(pipeline, X.reshape(-1, 1), y, scoring="neg_mean_squared_error", cv=10)
# Pipeline을 구성하는 세부 객체를 접근하는 named_steps['객체명']을 이용해 회귀계수 추출
coefficients = pipeline.named_steps['linear_regression'].coef_
print('\nDegree {0} 회귀 계수는 {1} 입니다.'.format(degrees[i], np.round(coefficients, 2)))
print('Degree {0} MSE는 {1} 입니다.'.format(degrees[i], -1*np.mean(scores)))
# 0부터 1까지 테스트 데이터 세트를 100개로 나눠 예측을 수행합니다.
# 테스트 데이터 세트에 회귀 예측을 수행하고 예측 곡선과 실제 곡선을 그려서 비교합니다.
X_test = np.linspace(0, 1, 100)
# 예측값 곡선
plt.plot(X_test, pipeline.predict(X_test[:, np.newaxis]), label="Model")
# 실제값 곡선
plt.plot(X_test, true_fun(X_test), '--', label="True function")
plt.scatter(X, y, edgecolor='b', s=20, label="Samples")
plt.xlabel("x"); plt.ylabel('y'); plt.xlim((0, 1)); plt.ylim((-2, 2)); plt.legend(loc='best')
plt.title("Degree {}\nMSE = {:.2e}(+/- {:.2e})".format(degrees[i], -scores.mean(), scores.std()))
plt.show()
'''
Degree 1 회귀 계수는 [-1.61] 입니다.
Degree 1 MSE는 0.4077289625098685 입니다.
Degree 4 회귀 계수는 [ 0.47 -17.79 23.59 -7.26] 입니다.
Degree 4 MSE는 0.04320874987232058 입니다.
Degree 15 회귀 계수는 [-2.98291000e+03 1.03898770e+05 -1.87415056e+06 2.03715126e+07
-1.44872551e+08 7.09311979e+08 -2.47064676e+09 6.24558367e+09
-1.15676035e+10 1.56894317e+10 -1.54005437e+10 1.06456871e+10
-4.91375763e+09 1.35919168e+09 -1.70380199e+08] 입니다.
Degree 15 MSE는 180526263.29386473 입니다.
결과1
'''
MSE는 교차 검증 세트를 10개를 평균낸 것입니다. 실선은 테스트 데이터로 예측한 모델의 다항 회귀 예측 곡선입니다. 점선은 X, Y의 코사인 곡선입니다.
- degree 1은 단순 선형 회귀와 같이 너무 단순해 데이터의 패턴을 반영하지 못해서 과소 적합 모델이 되었습니다. MSE는 약 0.408입니다.
- degree 4는 실제 데이터 세트와 유사한 모습입니다. MSE는 0.043으로 가장 뛰어난 예측 성능을 나타냅니다.
- degree 15는 MSE 값이 182815432가 될 정도로 높은 오류 값이 발생했습니다. 데이터 세트의 변동 잡음 값에 지나치게 반응한 결과 과적합 모델이 됐습니다.
가장 중요한건 과적합, 과소 적합이 아닌 균형 잡힌(Balanced) 모델을 사용해 학습 데이터의 패턴을 잘 반영해야 합니다.
편향 - 분산 트레이드 오프(Bias-Variance Trade off)
앞의 Degree 1과 같이 매우 단순한 모델로서 지나치게 한 방향성으로 치우친 경향을 가진 모델을 고편향(High Bias)성을 가졌다고 표현합니다. 반대로 Degree 15와 같이 데이터 하나 하나의 특성을 반영하면서 매우 복잡한 모델이 되어 높은 변동성을 가진 모델을 고분산(High Variance)성을 가졌다고 표현합니다.
* 데이터를 너무 무시하면 고편향성이고, 너무 반영하면 고분산성을 가지는 것 같다.
분산이 높으면 정답을 중심으로 답이 멀리 떨어져 있고, 편향성이 높으면 중심이 정답과 멀리 떨어져 있다. 일반적으로 편향과 분산은 한 쪽이 높으면 한 쪽이 낮아지는 경향이 있습니다. 즉, 편향이 높으면 분산은 낮아지고(과소적합), 반대로 분산이 높으면 편향이 낮아집니다(과적합).
편향이 높으면 전체 오류가 높습니다. 편향을 점점 낮추면 동시에 분산이 높아지고 전체 오류도 낮아지게 됩니다. 반대로 편향을 낮추고 분산을 높이면서 전체 오류가 가장 낮아지는 '골디락스' 지점을 통과하면서 분산을 지속적으로 높이면 전체 오류 값이 오히려 증가하면서 예측 성능이 다시 저하됩니다.
높은 편향/낮은 분산에서 과소적합이 되기 쉽고, 낮은 편향/높은 분산에서 과적합이 되기 쉽습니다. 편향과 분산을 서로 Trade-off하며 오류값(Cost)을 최대로 낮아지게 하는 모델을 만드는 것이 중요합니다.
규제 선형 모델 - 릿지, 라쏘, 엘라스틱넷
규제 선형 모델의 개요
이전까지 선형 모델의 비용 함수는 RSS를 최소화하는, 즉 학습 데이터의 실제값과 학습을 토대로 한 예측값의 차이를 최소화하는 것만 고려했습니다. 이는 학습 데이터에 지나치게 맞추게 되어서 회귀 계수가 쉽게 쉽게 커졌습니다. 즉, 빠르게 판단했다고 볼 수 있습니다. 이는 테스트 데이터 세트에서 예측 성능이 저하되기 쉽습니다. 이를 반영해 과적합 방지를 위해 회귀 계수 값이 커지지 않도록 하는 방법을 추가했습니다.
이렇게 회귀 계수의 크기를 제어해 과적합을 개선하려면 아래와 같은 비용 함수의 최솟값으로 변경할 수 있습니다.
alpha는 학습 데이터 적합 정도와 회귀 계수 값의 크기 제어를 수행하는 튜닝 파라미터입니다. 비용 함수의 목표가 (RSS+회귀계수제어)를 최소화하는 W를 찾는 것일 때 alpha가 하는 역할을 알아보겠습니다.
alpha값을 크게 하면 비용 함수는 회귀 계수 W의 값을 작게 해 과적합을 개선할 수 있으며, alpha 값을 작게 하면 회귀 계수 W가 더 커져도 상쇄 가능하므로 학습데이터 적합을 더 개선할 수 있습니다.
규제(Regularization)는 alpha 값을 증가시켜 회귀 계수 값의 크기를 감소시키는 것입니다. 이는 과적합 개선 효과가 있습니다. 규제는 크게 L2 방식과 L1 방식으로 구분됩니다.
L2 규제를 적용한 회귀를 릿지(Ridge) 회귀라고 합니다. alpha*|w|^2와 같이 W의 제곱에 비례해 페널티를 부여하는 방식을 말합니다. 라쏘(Lasso) 회귀는 L1 규제를 적용한 회귀로, alpha*|w|₁와 같이 W의 절댓값에 대해 페널티를 부여합니다. L1 규제를 적용하면 영향력이 크지 않은 회귀 계수 값을 0으로 변환합니다.
릿지 회귀: Ridge
사이킷런은 Ridge 클래스를 통해 릿지 회귀를 구현합니다. 주요 생성 파라미터는 alpha입니다. 앞에서 LinearRegression 예제에서 사용한 피처 데이터 세트 X_data와 Target 데이터 y_target을 사용해 성능을 평가해보겠습니다.
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score
# alpha=10으로 설정해 릿지 회귀 수행.
ridge = Ridge(alpha=10)
neg_mse_scores = cross_val_score(ridge, X_data, y_target, scoring="neg_mean_squared_error", cv=5)
rmse_scores = np.sqrt(-1*neg_mse_scores)
avg_rmse = np.mean(rmse_scores)
print('5 folds의 개별 Negative MSE scores: ', np.round(neg_mse_scores, 3))
print('5 folds의 개별 RMSE scores: ', np.round(rmse_scores, 3))
print('5 folds의 평균 RMSE: {0:.3f}'.format(avg_rmse))
'''
5 folds의 개별 Negative MSE scores: [-11.422 -24.294 -28.144 -74.599 -28.517]
5 folds의 개별 RMSE scores: [3.38 4.929 5.305 8.637 5.34 ]
5 folds의 평균 RMSE: 5.518
'''
릿지의 평균 RMS인 5.524가 LinearRegression의 평균 MSE인 5.829 보다 뛰어난 예측 성능을 보여줍니다.
이번엔 alpha 값을 0, 0.1, 1, 10, 100으로 변화시키며 RMSE와 회귀 계수 값의 변화를 살펴보겠습니다.
# 릿지에 사용될 alpha 파라미터 값을 정의
alphas = [0, 0.1, 1, 10, 100]
# alphas list 값을 반복하면서 alpha에 따른 평균 rmse를 구함.
for alpha in alphas:
ridge = Ridge(alpha=alpha)
# cross_val_score를 이용해 5 폴드의 평균 RMSE를 계산
neg_mse_scores = cross_val_score(ridge, X_data, y_target, scoring="neg_mean_squared_error", cv=5)
avg_rmse = np.mean(np.sqrt(-1*neg_mse_scores))
print('alpha {0} 일 때, 5 folds의 평균 RMSE: {1:.3f}'.format(alpha, avg_rmse))
'''
alpha 0 일 때, 5 folds의 평균 RMSE: 5.829
alpha 0.1 일 때, 5 folds의 평균 RMSE: 5.788
alpha 1 일 때, 5 folds의 평균 RMSE: 5.653
alpha 10 일 때, 5 folds의 평균 RMSE: 5.518
alpha 100 일 때, 5 folds의 평균 RMSE: 5.330
'''
이번엔 alpha 값에 따른 회귀 계수 값을 막대 그래프로 시각화해보겠습니다. 회귀 계수를 Ridge 객체의 coef_속성에서 추출해 Series 객체로 만들고 시본 가로 막대로 표시합니다. DataFrame에 alpha값 별 회귀 계수로 저장합니다.
# 각 alpha에 따른 회귀 계수 값을 시각화하기 위해 5개의 열로 된 맷플롯립 축 생성
fig, axs = plt.subplots(figsize=(18, 6), nrows=1, ncols=5)
# 각 alpha에 따른 회귀 계수 값을 데이터로 저장하기 위한 DataFrame 생성
coeff_df = pd.DataFrame()
# alphas 리스트 값을 차례로 입력해 회귀 계수 값 시각화 및 데이터 저장. pos는 axis의 위치 지정
for pos, alpha in enumerate(alphas):
ridge = Ridge(alpha=alpha)
ridge.fit(X_data, y_target)
# alpha에 따른 피처별로 회귀 계수를 Series로 변환하고 이를 DataFrame의 칼럼으로 추가.
coeff = pd.Series(data=ridge.coef_, index=X_data.columns)
colname='alpha:'+str(alpha)
coeff_df[colname] = coeff
# 막대 그래프로 각 alpha값에서의 회귀 계수를 시각화. 회귀 계수값이 높은 순으로 표현
coeff = coeff.sort_values(ascending=False)
axs[pos].set_title(colname)
axs[pos].set_xlim(-3, 6)
sns.barplot(x=coeff.values, y=coeff.index, ax=axs[pos])
# for문 바깥에서 맷플롯립의 show 호출 및 alpha에 따른 피처별 회귀 계수를 DataFrame으로 표시
plt.show()
'''
결과1
'''
alpha값을 증가시킬 수록 회귀 계수 값은 지속적으로 작아짐을 알 수 있습니다. 특히 NOX의 경우 회귀 계수가 크게 작아졌습니다.
DataFrame에 저장된 alpha값의 변화에 따른 릿지 회귀 계수 값을 구해보겠습니다.
ridge_alphas = [0, 0.1, 1, 10, 100]
sort_column = 'alpha:'+str(ridge_alphas[0])
coeff_df.sort_values(by=sort_column, ascending=False)
'''
결과2
'''
alpha값이 증가함에 따라 회귀 계수가 지속적으로 감소하지만, 릿지 회귀의 경우 회귀 계수를 0으로 만들지는 않습니다.
라쏘 회귀: Lasso
W의 절댓값에 페널티를 부여하는 L1 규제를 선형 회귀에 적용한 것이 라쏘(Lasso) 회귀입니다. L2 규제가 회귀 계수의 크기를 감소시키는 데 반해, L1 규제는 불필요한 회귀 계수를 급격하게 감소시켜 0으로 만들고 제거합니다. 이러한 측면에서 L1 규제는 적절한 피처만 회귀에 포함시키는 피처 선택의 특성을 가지고 있습니다.
사이킷런은 Lasso 클래스를 통해 라쏘 회귀를 구현합니다. 주요 생성 파라미터는 alpha입니다. 라쏘 또한 alpha값에 따른 RMSE와 각 피처의 회귀 계수를 출력해보겠습니다. 이번엔 이를 함수로 만들어 구현해보겠습니다.
from sklearn.linear_model import Lasso, ElasticNet
# alpha값에 따른 회귀 모델의 폴드 평균 RMSE를 출력하고, 회귀 계수값들을 DataFrame으로 반환
def get_linear_reg_eval(model_name, params=None, X_data_n=None, y_target_n=None, verbose=True):
coeff_df = pd.DataFrame()
if verbose : print('######', model_name, '######')
for param in params:
if model_name == 'Ridge': model = Ridge(alpha=param)
elif model_name == 'Lasso': model = Lasso(alpha=param)
elif model_name == 'ElasticNet': model = ElasticNet(alpha=param, l1_ratio=0.7)
neg_mse_scores = cross_val_score(model, X_data_n, y_target_n,\
scoring="neg_mean_squared_error", cv=5)
avg_rmse = np.mean(np.sqrt(-1*neg_mse_scores))
print('alpha {0}일 때 5폴드 세트의 평균 RMSE: {1:.3f}'.format(param, avg_rmse))
# cross_val_score는 evaluaion metric만 반환하므로 모델을 다시 학습하여 회귀 계수 추출
model.fit(X_data_n, y_target_n)
# alpha에 따른 피처별 회귀 계수를 Series로 변환하고 이를 DataFrame의 칼럼으로 추가.
coeff = pd.Series(data=model.coef_, index=X_data_n.columns)
colname = 'alpha:'+str(param)
coeff_df[colname] = coeff
return coeff_df
# end of get_linear_regre_eval
# Lasso에 사용될 alpha 파라미터 값을 정의하고, get_linear_reg_eval() 함수 호출
lasso_alphas = [0.07, 0.1, 0.5, 1, 3]
coeff_lasso_df = get_linear_reg_eval('Lasso', params=lasso_alphas, X_data_n=X_data,\
y_target_n=y_target)
'''
###### Lasso ######
alpha 0.07일 때 5폴드 세트의 평균 RMSE: 5.612
alpha 0.1일 때 5폴드 세트의 평균 RMSE: 5.615
alpha 0.5일 때 5폴드 세트의 평균 RMSE: 5.669
alpha 1일 때 5폴드 세트의 평균 RMSE: 5.776
alpha 3일 때 5폴드 세트의 평균 RMSE: 6.189
'''
alpha가 0.07일 때 가장 좋은 RMSE를 보여줬고, Ridge 보단 떨어지지만 LinearRegression보다 높은 5.612가 반환됐습니다.
다음은 alpha 값에 따른 피처별 회귀 계수입니다.
# 반환된 coeff_lasso_df를 첫 번째 칼럼 순으로 내림차순 정렬해 회귀 계수 DataFrame 출력
sort_column = 'alpha:'+str(lasso_alphas[0])
coeff_lasso_df.sort_values(by=sort_column, ascending=False)
'''
결과3
'''
alpha 크기가 증가함에 따라 일부 피처의 회귀 계수가 0으로 바뀜을 알 수 있습니다. 회귀 계수가 0인 피처는 회귀 식에서 제외되면서 피처 선택의 효과를 얻을 수 있습니다.
엘라스틱넷 회귀
Elastic Net 회귀는 L2 규제와 L1 규제를 결합한 회귀입니다. 따라서 엘라스틱넷 회귀 비용함수는 아래와 같습니다.
Lasso 회귀는 서로 상관관계가 높은 피처들의 경우 중요한 피처만 선택하고 나머지는 회귀 계수를 0으로 만들어 회귀 계수 값이 급변할 수 있습니다. 이에 L2 규제를 추가해 변동을 완화합니다. Elastic Net 회귀는 L1, L2 규제를 모두 활용하므로 수행시간이 상대적으로 오래 걸린다는 것입니다.
사이킷런의 ElasticNet 클래스를 통해 엘라스틱넷 회귀를 구현합니다. ElasticNet 클래스의 주요 생성 파라미터는 alpha와 l1_ratio입니다. ElsticNet의 alpha는 Lasso, Ridge의 alpha와는 다릅니다.
엘라스틱넷의 규제가 a*L1 + b*L2라 했을 때, alpha = a+b입니다. l1_ratio = a/(a+b)입니다. l1_ratio = 0이면, a=0이므로 L2 규제와 같고, l1_ratio =1이면, b=0이므로 L1 규제와 같습니다. 이렇게 적당한 값을 조절할 수 있습니다.
get_linear_reg_eval( ) 함수를 통해 RMSE와 alpha에 따른 회귀 계수를 구할 것인데, 앞서 말했듯 ElasticNet의 alpha는 Lasso, Ridge와는 차이가 있고, l1_ratio는 함수를 정의할 때 이미 0.7로 고정해뒀음을 상기해야합니다.
# 엘라스틱넷에 사용될 alpha 파라미터의 값들을 정의하고, get_linear_reg_eval() 함수 호출
# l1_ratio는 0.7로 고정
elastic_alphas = [0.07, 0.1, 0.5, 1, 3]
coeff_elatic_df = get_linear_reg_eval('ElasticNet', params=elastic_alphas,\
X_data_n=X_data, y_target_n=y_target)
'''
###### ElasticNet ######
alpha 0.07일 때 5폴드 세트의 평균 RMSE: 5.542
alpha 0.1일 때 5폴드 세트의 평균 RMSE: 5.526
alpha 0.5일 때 5폴드 세트의 평균 RMSE: 5.467
alpha 1일 때 5폴드 세트의 평균 RMSE: 5.597
alpha 3일 때 5폴드 세트의 평균 RMSE: 6.068
'''
# 반환된 coeff_elastic_df를 첫 번째 칼럼순으로 내림차 순 정렬해 회귀계수 DataFrame 출력
sort_column = 'alpha:'+str(elastic_alphas[0])
coeff_elatic_df.sort_values(by=sort_column, ascending=False)
'''
결과4
'''
alpha가 0.5일 때 RMSE가 5.468로 가장 좋은 예측 성능을 보이고, 회귀 계수 값들이 라쏘보다는 0 값이 적음을 알 수 있습니다. 규제 선형 회귀 기법인 Ridge, Lasso, ElasticNet을 살펴봤습니다. 이들은 각각 특정 상황에 가장 좋은 성능을 발휘합니다. 따라서 각각 알고리즘을 하이퍼 파라미터를 변경하면서 비교하고, 최적의 예측 성능을 찾아내야 합니다.
선형 회귀 모델을 위한 데이터 변환
선형 회귀의 경우 최적의 하이퍼 파라미터를 찾아내는 것 못지 않게, 먼저 데이터 분포도의 정규화와 인코딩 방법이 매우 중요합니다. 선형 회귀 모델의 경우 피처와 타깃값 간에 선형의 관계가 있다고 가정하기 때문에 최적의 선형 함수를 찾아내 결과값을 예측합니다.
선형 회귀 모델은 피처값과 타깃값이 정규 분포( 평균을 중심으로 종 모양으로 데이터 값이 분포된 형태) 형태를 매우 선호합니다. 또한, 정규 분포가 아니라 특정값의 분포가 치우진 왜곡(Skew)된 형태의 분포도의 경우 예측 성능에 부정적 영향을 미칠 가능성이 높습니다. 특히, 타깃값이 영향을 많이 받고, 이를 스케일링/정규화를 통해 예방할 수 있습니다.
사이킷런을 이용해 피처 데이터 세트에 적용하는 변환 작업은 아래와 같은 방법이 있습니다.
- StandardScaler(평균 0, 분산1인 정규 분포), MinMaxScaler(최솟값 0, 최댓값 1으로 스케일링), 예측 성능 향상을 기대하기 어려운 경우가 많습니다.
- 1번 방법을 통해 예측 성능의 변화가 없을 경우, 스케일링/정규화를 수행한 데이터 세트에 다시 다항 특성을 적용하여 변환하는 방법입니다. 피처 개수가 많으면, 다항 변환으로 생성되는 피처의 개수가 기하 급수적으로 늘어나서 과적합의 단점이 있습니다.
- log 변환(원래 값에 log 함수를 적용하면 보다 정규 분포에 가까운 형태로 값이 분포됩니다.), 가장 많이 사용하는 방법입니다.
타깃값의 경우 일반적으로 log 변환을 적용합니다. 정규 분포나 다른 정규값으로 변환하면 다시 원본 타깃값으로 복원하기 어려울 수 있습니다.
StandardScaler() & MinMaxScaler() & np.log1p()
보스턴 주택가격 피처 데이터 세트에 위에서 언급한 StandardScaler, MinMaxScaler, Log 변환을 적용해 각 경우별 RMSE로 각 경우별 예측 성능을 측정해 보겠습니다. 이를 위해 get_scaled_data( ) 함수를 생성합니다. log함수 구현시 np.log( ) 가 아닌 np.log1p( )를 이용하는데, log( ) 함수를 적용하면 언더 플로우가 발생하기 쉬워서 1+log( ) 함수를 적용하는데 이를 구현한 것이 np.log1p( ) 이기 때문입니다.
# method는 표준 정규 분포 변환(Standard), 최댓값/최솟값 정규화(MinMax), 로그 변환(Log) 결정
# p_degree는 다항식 특성을 추가할 때 적용, p_degree는 2이상 부여하지 않음.
def get_scaled_data(method='None', p_degree=None, input_data=None):
if method == 'Standard':
scaled_data = StandardScaler().fit_transform(input_data)
elif method == 'MinMax':
scaled_data = MinMaxScaler().fit_transform(input_data)
elif method == 'Log':
scaled_data = np.log1p(input_data)
else:
scaled_data = input_data
if p_degree != None:
scaled_data = PolynomialFeatures(degree=p_degree,\
include_bias=False).fit_transform(scaled_data)
return scaled_data
Ridge 클래스의 alpha 값을 변화시키며 피처 데이터 세트에 여러 가지 방법으로 변환한 데이터 세트를 입력받을 경우 RMSE 값이 어떻게 변하는지 살펴보겠습니다.
# Ridge의 alpha 값을 다르게 적용하고 다양한 데이터 변환 방법에 따른 RMSE 추출.
alphas = [0.1, 1, 10, 100]
# 5개 방식으로 변환. 먼저 원본 그대로, 표준 정규 분포, 표준 정규 분포 + 다항식 특성
# 최대/최소 정규화, 최대/최소 정규화 + 다항식 특성, 로그 변환
scale_methods = [(None, None), ('Standard', None), ('Standard', 2),\
('MinMax', None), ('MinMax', 2), ('Log', None)]
for scale_method in scale_methods:
X_data_scaled = get_scaled_data(method=scale_method[0], p_degree=scale_method[1],\
input_data=X_data)
print('\n## 변환 유형:{0}, Polynomial Degree:{1}'.format(scale_method[0], scale_method[1]))
get_linear_reg_eval('Ridge', params=alphas, X_data_n=X_data_scaled,\
y_target_n=y_target, verbose=False)
'''
## 변환 유형:None, Polynomial Degree:None
alpha 0.1일 때 5폴드 세트의 평균 RMSE: 5.788
alpha 1일 때 5폴드 세트의 평균 RMSE: 5.653
alpha 10일 때 5폴드 세트의 평균 RMSE: 5.518
alpha 100일 때 5폴드 세트의 평균 RMSE: 5.330
## 변환 유형:Standard, Polynomial Degree:None
alpha 0.1일 때 5폴드 세트의 평균 RMSE: 5.826
alpha 1일 때 5폴드 세트의 평균 RMSE: 5.803
alpha 10일 때 5폴드 세트의 평균 RMSE: 5.637
alpha 100일 때 5폴드 세트의 평균 RMSE: 5.421
## 변환 유형:Standard, Polynomial Degree:2
alpha 0.1일 때 5폴드 세트의 평균 RMSE: 8.827
alpha 1일 때 5폴드 세트의 평균 RMSE: 6.871
alpha 10일 때 5폴드 세트의 평균 RMSE: 5.485
alpha 100일 때 5폴드 세트의 평균 RMSE: 4.634
## 변환 유형:MinMax, Polynomial Degree:None
alpha 0.1일 때 5폴드 세트의 평균 RMSE: 5.764
alpha 1일 때 5폴드 세트의 평균 RMSE: 5.465
alpha 10일 때 5폴드 세트의 평균 RMSE: 5.754
alpha 100일 때 5폴드 세트의 평균 RMSE: 7.635
## 변환 유형:MinMax, Polynomial Degree:2
alpha 0.1일 때 5폴드 세트의 평균 RMSE: 5.298
alpha 1일 때 5폴드 세트의 평균 RMSE: 4.323
alpha 10일 때 5폴드 세트의 평균 RMSE: 5.185
alpha 100일 때 5폴드 세트의 평균 RMSE: 6.538
## 변환 유형:Log, Polynomial Degree:None
alpha 0.1일 때 5폴드 세트의 평균 RMSE: 4.770
alpha 1일 때 5폴드 세트의 평균 RMSE: 4.676
alpha 10일 때 5폴드 세트의 평균 RMSE: 4.836
alpha 100일 때 5폴드 세트의 평균 RMSE: 6.241
'''
# 위 코드를 그대로 수행하면 에러가 발생하는데,
# get_linear_reg_eval() 함수를 아래 처럼 바꿔주면 잘 실행된다.
def get_linear_reg_eval(model_name, params=None, X_data_n=None, y_target_n=None, verbose=True):
coeff_df = pd.DataFrame()
...
coeff = pd.Series(data=model.coef_)
...
return coeff_df
결과를 보면, 단순히 StandardScaler, MinMaxScaler로 피처 데이터 세트를 변경해도 성능상 개선이 없습니다. StandardScaler + 2차 다항식 변환 했을 때 alpha=100에서 성능이 개선됐으며, MinMaxScaler + 2차 다항식 변환과 alpha=1에서 성능이 개선됐습니다. 다항식 변환은 수행 시간이 늘어난다는 단점이 있습니다. 한편, 로그 변환은 alpha가 0, 0.1, 1, 10인 경우 모두 좋은 성능 향상이 있음을 알 수 있습니다.
일반적으로 선형 회귀에 적용하려는 데이터 세트에 데이터 값의 분포가 심하게 왜곡돼 있을 경우 로그 변환을 적용하는 것이 좋은 결과를 기대할 수 있습니다.
로지스틱 회귀
로지스틱 회귀는 선형 회귀 방식을 분류에 적용한 알고리즘입니다. 즉, 로지스틱 회귀는 분류에 사용됩니다. 회귀가 선형인지 비선형인지는 독립변수가 아닌 가중치 변수 W가 선형인지 아닌지를 따릅니다. 로지스틱 회귀는 선형 함수가 아닌 시그모이드(Sigmoid) 함수의 최적선을 찾고 시그모이드 함수의 반환 값을 확률로 간주해 확률에 따라 분류를 결정한다는 것입니다.
가령, 종양의 크기에 따라 악성(Yes=1)인지 양성(No=0)인지 회귀를 이용해 1과 0의 값을 확률로 예측하는 것입니다.
위스콘신 유방암 데이터 세트를 이용해 로지스틱 회귀로 암 여부를 판단해보겠습니다.
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
cancer = load_breast_cancer()
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
# StandardScaler()로 평균이 0, 분산이 1로 데이터 분포도 변환
scaler = StandardScaler()
data_scaled = scaler.fit_transform(cancer.data)
# train, test 데이터 세트 분리
X_train, X_test, y_train, y_test = train_test_split(data_scaled, cancer.target,\
test_size=0.3, random_state=0)
선형 회귀 계열 로지스틱 회귀는 데이터의 정규 분포도에 따라 예측 영향을 받을 수 있으므로 정규 분포 형태의 표준 스케일링을 적용한 뒤 train, test 데이터를 분리했습니다.
from sklearn.metrics import accuracy_score, roc_auc_score
# 로지스틱 회귀를 이용해 학습 및 예측 수행
lr_clf = LogisticRegression()
lr_clf.fit(X_train, y_train)
lr_preds = lr_clf.predict(X_test)
# 정확도와 roc_auc 측정
print('accuracy: {:0.3f}'.format(accuracy_score(y_test, lr_preds)))
print('roc_auc: {:0.3f}'.format(roc_auc_score(y_test, lr_preds)))
'''
accuracy: 0.977
roc_auc: 0.972
'''
LogisticRegression 클래스의 주요 하이퍼 파라미터로 penalty와 C가 있습니다. penalty는 규제(Regularizaion)의 유형을 설정하며 'l2', 'l1' 등으로 설정할 수 있습니다. default는 'l2'입니다. C = 1/alpha로 값이 작을 수록 규제 강도가 큽니다.
from sklearn.model_selection import GridSearchCV
params = {
'penalty': ['l2'],
'C': [0.01, 0.1, 1, 1, 5, 10]
}
grid_clf = GridSearchCV(lr_clf, param_grid=params, scoring='accuracy', cv=3)
grid_clf.fit(data_scaled, cancer.target)
print('최적 하이퍼 파라미터:{0}, 최적 평균 정확도:{1:.3f}'.format(grid_clf.best_params_,
grid_clf.best_score_))
'''
최적 하이퍼 파라미터:{'C': 1, 'penalty': 'l2'}, 최적 평균 정확도:0.975
'''
로지스틱 회귀는 가볍고 빠르지만, 이진 분류 예측 성능도 뛰어납니다. 때문에 로지스틱 분류를 이진 분류의 기본 모델로 사용하는 경우가 많고, 희소한 데이터 세트 분류에서도 뛰어난 성능을 보여서 텍스트 분류에서도 자주 사용됩니다.
번외
lr_clf = LogisticRegression(solver='saga')
lr_clf.fit(X_train, y_train)
lr_preds = lr_clf.predict(X_test)
params = {
'penalty': ['l1'],
'C': [0.01, 0.1, 1, 1, 5, 10]
}
grid_clf = GridSearchCV(lr_clf, param_grid=params, scoring='accuracy', cv=3)
grid_clf.fit(data_scaled, cancer.target)
print('최적 하이퍼 파라미터:{0}, 최적 평균 정확도:{1:.3f}'.format(grid_clf.best_params_,
grid_clf.best_score_))
'''
최적 하이퍼 파라미터:{'C': 5, 'penalty': 'l1'}, 최적 평균 정확도:0.977
'''
penalty를 'l1'으로 하면, LogisticRegresion에 solver 파라미터를 'saga'로 주어야 합니다. saga로 주고 실행시키면 결과는 출력되지만 아래와 같은 에러가 떴습니다.
usr/local/lib/python3.7/dist-packages/sklearn/linear_model/_sag.py:354:
ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
ConvergenceWarning
아래 더보기에 이유를 간략히 찾아보았습니다.
I've often had LogisticRegression "not converge" yet be quite stable (meaning the coefficients don't change much between iterations).
Maybe there's some multicolinearity that's leading to coefficients that change substantially without actually affecting many predictions/scores.
반복하는 동안 W 계수가 많이 바뀌지도 않았고, 바뀌어도 다중 공산성(multicolinearity)으로 예측 점수에 영향을 많이 주지 못했다는 뜻인 것 같습니다.
Another possibility (that seems to be the case, thanks for testing things out) is that you're getting near-perfect separation on the training set. In unpenalized logistic regression, a linearly separable dataset won't have a best fit: the coefficients will blow up to infinity (to push the probabilities to 0 and 1). When you add regularization, it prevents those gigantic coefficients. So, with large values of C, i.e. little regularization, you still get large coefficients and so convergence may be slow, but the partially-converged model may still be quite good on the test set; whereas with large regularization you get much smaller coefficients, and worse performance on both the training and test sets.
If you're worried about nonconvergence, you can try increasing n_iter (more), increasing tol, changing the solver, or scaling features (though with the tf-idf, I wouldn't think that'd help).
I'd look for the largest C that gives you good results, then go about trying to get that to converge with more iterations and/or different solvers.
I've often had LogisticRegression "not converge" yet be quite stable (meaning the coefficients don't change much between iterations). Maybe there's some multicolinearity that's leading to coefficients that change substantially without actually affecting many predictions/scores.
다중 공산성(은 피처 X간에 비슷한 상관관계를 가진다는 뜻, 즉 피처 X1 Y간의 그래프와 X2과 Y간의 그래프를 그려보면 X1, X2가 비슷한 그래프가 그려진다는 뜻이다.
회귀 트리
이제까지 회귀는 선형, 비선형 함수와 같은 회귀 함수를 기반으로 결과를 예측했습니다. 회귀 트리는 결정 트리와 같이 트리를 기반으로 하는 회귀 방식입니다.
회귀 트리는 분류에서 언급했던 분류 트리와 크게 다르지 않습니다. 분류 트리가 특정 클래스 레이블을 결정하는 것과는 달리 회귀 트리는 리프 노드에 속한 데이터 값의 평균값을 구해 회귀 예측값을 계산합니다.
그림에서 보면 왼쪽의 규칙 노드로 분류하고 오른쪽에 구역 별로 x1 혹은 x2를 평균낸 값을 통해 리프 노드에 결정값으로 합니다.
결정 트리, 랜덤 포레스트, GBM, XGBoost, LightGBM 등 트리 기반 알고리즘은 모두 분류뿐만 아니라 회귀도 가능합니다. 트리 생성이 CART 알고리즘에 기반하고 있기 때문입니다. 사이킷런에서는 아래 표와 같이 Estimator 클래스를 제공합니다.
알고리즘 | 회귀 Estimator 클래스 | 분류 Estimator 클래스 |
Decision Tree | DecisionTreeRegressor | DecisionTreeClassifier |
Gradient Boosting | GradientBoostingRegressor | GradientBoostingClassifier |
XGBoost | XGBRegressor | XGBClassifier |
LightGBM | LGBMRegressor | LGBMClassifier |
RandomForestRegressor
RandomForestRegressor를 이용해 보스턴 주택 가격 예측을 수행해보겠습니다.
from sklearn.datasets import load_boston
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestRegressor
import pandas as pd
import numpy as np
# 보스턴 데이터 세트 로드
boston = load_boston()
bostonDF = pd.DataFrame(boston.data, columns=boston.feature_names)
bostonDF['PRICE'] = boston.target
y_target = bostonDF['PRICE']
X_data = bostonDF.drop(['PRICE'], axis=1, inplace=False)
rf = RandomForestRegressor(random_state=0, n_estimators=1000)
neg_mse_scores = cross_val_score(rf, X_data, y_target, scoring="neg_mean_squared_error", cv=5)
rmse_scores = np.sqrt(-1*neg_mse_scores)
avg_rmse = np.mean(rmse_scores)
print('5 교차 검증의 개별 Negative MSE scores: ', np.round(neg_mse_scores, 2))
print('5 교차 검증의 개별 RMSE scores: ', np.round(rmse_scores, 2))
print('5 교차 검증의 평균 RMSE: {0:.3f}'.format(avg_rmse))
'''
5 교차 검증의 개별 Negative MSE scores: [ -7.88 -13.14 -20.57 -46.23 -18.88]
5 교차 검증의 개별 RMSE scores: [2.81 3.63 4.54 6.8 4.34]
5 교차 검증의 평균 RMSE: 4.423
'''
GBM, XGBoost, LightGBM의 Regressor를 모두 이용해 보스턴 주택 가격 예측을 수행하겠습니다. 이를 위해 get_model_cv_prediction( ) 함수를 만들겠습니다.
def get_model_cv_prediction(model, X_data, y_target):
neg_mse_scores = cross_val_score(model, X_data, y_target, scoring="neg_mean_squared_error", cv=5)
rmse_scores = np.sqrt(-1*neg_mse_scores)
avg_rmse = np.mean(rmse_scores)
print('#####', model.__class__.__name__, '#####')
print('5 교차 검증의 평균 RMSE: {0:.3f}'.format(avg_rmse))
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import GradientBoostingRegressor
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
dt_reg = DecisionTreeRegressor(random_state=0, max_depth=4)
rf_reg = RandomForestRegressor(random_state=0, n_estimators=1000)
gb_reg = GradientBoostingRegressor(random_state=0, n_estimators=1000)
xgb_reg = XGBRegressor(n_estimators=1000)
lgb_reg = LGBMRegressor(n_estimators=1000)
# 트리 기반의 회귀 모델을 반복하면서 평가 수행
models = [dt_reg, rf_reg, gb_reg, xgb_reg, lgb_reg]
for model in models:
get_model_cv_prediction(model, X_data, y_target)
'''
##### DecisionTreeRegressor #####
5 교차 검증의 평균 RMSE: 5.978
##### RandomForestRegressor #####
5 교차 검증의 평균 RMSE: 4.423
##### GradientBoostingRegressor #####
5 교차 검증의 평균 RMSE: 4.269
##### XGBRegressor #####
5 교차 검증의 평균 RMSE: 4.089
##### LGBMRegressor #####
5 교차 검증의 평균 RMSE: 4.646
'''
회귀 트리 Regressor 클래스는 선형 회귀와 다른 트리 기반 방식이므로, 회귀 계수를 제공하는 coef_ 속성이 없고 대신 피처별 중요도를 알 수 있는 feature_importances_를 제공합니다.
import seaborn as sns
%matplotlib inline
rf_reg = RandomForestRegressor(n_estimators=1000)
# 앞 예제에서 만들어진 X_data, y_target 데이터 세트를 적용해 학습합니다.
rf_reg.fit(X_data, y_target)
feature_series = pd.Series(data=rf_reg.feature_importances_, index=X_data.columns)
feature_series = feature_series.sort_values(ascending=False)
sns.barplot(x=feature_series, y=feature_series.index)
'''
결과1
'''
사이킷런 회귀 트리 Regressor의 하이퍼 파라미터는 분류 트리 Classifier의 하이퍼 파라미터와 거의 동일하므로 분류 트리 Classifier 하이퍼 파라미터를 참고하시면 되겠습니다.
선형 회귀 vs 회귀 트리
회귀 트리 Regressor와 선형 회귀를 비교해 시각화해보겠습니다. 결정 트리의 하이퍼 파라미터인 max_depth의 크기를 변화 시키며 어떻게 회귀 트리 예측선이 변화하는지 살펴보겠습니다. 2차원 평면상에서 회귀 예측선을 쉽게 표현하기 위해 Price와 가장 양의 상관관계를 가진 RM 칼럼만 이용해 선형 호귀와 결정 트리 회귀로 PRICE 예측 회귀선을 표현하겠습니다.
보스턴 데이터 세트의 개수를 100개만 샘플링해서 직관적으로 데이터의 분포도를 시각화하겠습니다. 먼저 데이터 세트의 산점도 형태를 살펴보겠습니다.
import matplotlib.pyplot as plt
bostonDF_sample = bostonDF[['RM', 'PRICE']]
bostonDF_sample = bostonDF_sample.sample(n=100, random_state=0)
print(bostonDF_sample.sample)
plt.figure()
plt.scatter(bostonDF_sample.RM, bostonDF_sample.PRICE, c="darkorange")
'''
<bound method NDFrame.sample of
RM PRICE
329 6.333 22.6
371 6.216 50.0
219 6.373 23.0
403 5.349 8.3
78 6.232 21.2
.. ... ...
246 6.108 24.3
374 4.138 13.8
56 6.383 24.7
455 6.525 14.1
60 5.741 18.7
[100 rows x 2 columns]>
'''
'''
결과2
'''
LinearRegressor와 DecisionTreeRegressor를 max_depth를 2, 7로 해서 학습해보고, 산점도를 볼 때 Regressor에 RM 값을 4.5 ~ 8.5까지 100개의 테스트 데이터 세트를 추출하고 이를 넣어서 예측값을 구해보겠습니다.
import numpy as np
from sklearn.linear_model import LinearRegression
# 선형 회귀와 결정 트리 기반의 Regressor 생성. DecisionTreeRegressor의 max_depth는 각각 2, 7
lr_reg = LinearRegression()
rf_reg2 = DecisionTreeRegressor(max_depth=2)
rf_reg7 = DecisionTreeRegressor(max_depth=7)
# 실제 예측을 적용할 테스트용 데이터 세트를 4.5 ~ 8.5 까지의 100개 데이터 세트로 생성.
X_test = np.arange(4.5, 8.5, 0.04).reshape(-1, 1)
# 보스턴 주택 가격 데이터에서 시각화를 위해 피처는 RM만, 그리고 결정 데이터인 PRICE 추출
X_feature = bostonDF_sample['RM'].values.reshape(-1, 1)
y_target = bostonDF_sample['PRICE'].values.reshape(-1, 1)
# 학습과 예측 수행.
lr_reg.fit(X_feature, y_target)
rf_reg2.fit(X_feature, y_target)
rf_reg7.fit(X_feature, y_target)
pred_lr = lr_reg.predict(X_test)
pred_rf2 = rf_reg2.predict(X_test)
pred_rf7 = rf_reg7.predict(X_test)
fig, (ax1, ax2, ax3) = plt.subplots(figsize=(14, 4), ncols=3)
# X축 값을 4.5 ~ 8.5로 변환하며 입력했을 때 선형 회귀와 결정 트리 회귀 예측선 시각화
# 선형 회귀로 학습된 모델 회귀 예측선
ax1.set_title('Linear Regression')
ax1.scatter(bostonDF_sample.RM, bostonDF_sample.PRICE, c="darkorange")
ax1.plot(X_test, pred_lr, label="linear", linewidth=2)
# DecisionTreeRegressor, max_depth=2일 때 회귀 예측선
ax2.set_title('Decision Tree Regression: \n max_depth=2')
ax2.scatter(bostonDF_sample.RM, bostonDF_sample.PRICE, c="darkorange")
ax2.plot(X_test, pred_rf2, label="max_depth:3", linewidth=2)
# DecisionTreeRegressor, max_depth=7일 때 회귀 예측선
ax3.set_title(('Decision tree Regression: \n max_depth=7'))
ax3.scatter(bostonDF_sample.RM, bostonDF_sample.PRICE, c="darkorange")
ax3.plot(X_test, pred_rf7, label="max_dept:7", linewidth=2)
'''
결과3
'''
선형 회귀는 직선으로 예측 회귀선을 표현합니다.
회귀 트리의 경우 분할되는 데이터 지점에 따라 브랜치를 만들며 계단 형태로 회귀선을 만듭니다.
회귀 트리의 경우 max_depth=7인 경우 학습 데이터 세트의 이상치(outlier) 데이터도 학습하며 복잡하게 데이터 구역(평균값을 통한 분할)을 너무 많이 만들어서 과적합 됐음을 알 수 있습니다.
정리
이번 장에서는 머신러닝 기반의 회귀에 대해서 배웠습니다. 선형 회귀는 실제값과 예측값의 차이인 오류를 최소로 줄일 수 있는 선형 함수를 찾아서 이 선형 함수에 독립변수(피처)를 입력해 종속변수(타깃, 예측값)를 예측하는 것입니다. 이 최적의 선형 함수를 찾기 위해 (실제값 - 예측값)^2을 회귀 계수 W를 변수로 하는 비용 함수로 만들고, 이 비용함수가 최소가 되는 W를 찾아 선형 함수를 도출할 수 있었습니다. 그리고 이 비용함수를 최소화할 수 있는 방법 중 하나로 경사 하강법을 소개했습니다.
비용함수 최적화 기법은 머신러닝 전반에 걸쳐서 매우 중요한 개념입니다.
|실제값 - 예측값|에만 초점을 맞추는 것을 단순 선형 회귀라고 합니다. 이는 학습 데이터에 쉽게 과적화되는 문제점이 있습니다. 이를 규제(Regularization)를 통해 해결합니다. L2 규제를 적용한 규제 선형 회귀를 Ridge, L1 규제를 적용한 것을 Lasso, L2 + L1 규제를 적용한 것을 ElasticNet이라고 부릅니다. 일반적으로 선형 회귀는 규제 선형 회귀를 많이 사용합니다.
선형 회귀를 분류에 적용한 모델이 바로 로지스틱 회귀입니다. 로지스틱 회귀는 이름은 회귀이지만 실제로는 분류를 위한 알고리즘 입니다. 선형 함수 대신 최적의 시그모이드 함수를 도출하고, 독립 변수(피처)를 이 시그모이드 함수에 입력해 반환된 결과를 확률값으로 변환해 예측 레이블을 결정합니다. 로지스틱 회귀는 특히 이진 분류나 희소 행렬로 표현되는 텍스트 기반의 분류에서 높은 예측 성능을 나타냅니다.
선형( 또는 비선형) 회귀와 같이 최적의 선형(또는 비선형) 함수를 찾아내는 대신 회귀 트리를 이용해 예측하는 방법도 있습니다. 회귀트리는 분류를 위한 분류 트리와 크게 다르지 않고, 리프 노드에서 예측 결정 값을 만드는 과정에 차이가 있습니다. 회귀 트리는 리프 노드에 속한 데이터 값(구역을 분류해서 포함된 데이터 값)의 평균값을 구해 회귀 예측값을 계산합니다. Decision Tree, RandomForest, GBM, XGBoost, LightGBM 모두 회귀 트리를 이용한 회귀 방법을 제공합니다.
선형 모델을 기반으로 하는 선형 회귀는 데이터 값의 분포도, 인코딩 방법에 많은 영향을 받을 수 있습니다. 선형회귀는 데이터 분포도가 정규 분포의 형태를 선호하며, 특히 Target값의 분포도가 Skew(왜곡)되지 않고 정규 분포 형태여야 예측 성능을 저하시키지 않습니다. 데이터 세트가 왜곡된 데이터 분포도를 가지고 있을 때 보통 log 변환을 적용합니다. 또 선형 회귀의 경우, 카테고리형 데이터가 있을 경우 레이블 인코딩을 통한 숫자형 변환보다는 원-핫 인코딩으로 변환해주어야 합니다. 회귀 트리의 경우 인코딩 방식에 크게 영향을 받지 않습니다.
2개의 회귀 예제를 실습해보며 데이터 정제와 변환, 선형 회귀/ 회귀 트리의 최적화를 통해 어떻게 회귀 모델을 향상시키는지 실습했습니다. 특히 스태킹 모델을 회귀에 적용하면 일반적으로 훌륭한 예측 성능을 도출할 수 있습니다.
출처: 파이썬 머신러닝 완벽 가이드(권철민)
사진 출처:
https://gdcoder.com/decision-tree-regressor-explained-in-depth/
https://needjarvis.tistory.com/564
https://nittaku.tistory.com/478
http://scott.fortmann-roe.com/docs/BiasVariance.html
https://m.blog.naver.com/samsjang/221006905415
https://www.inflearn.com/questions/20372
https://ichi.pro/ko/gyeongsa-hagang-beob-algolijeum-gaeyo-166033569320163
https://walkingwithus.tistory.com/606
'파이썬 머신 러닝 완벽 가이드' 카테고리의 다른 글
회귀 실습 - 캐글 주택 가격: 고급 회귀 기법 (0) | 2021.11.24 |
---|---|
회귀 실습 - 자전거 대여 수요 예측 (0) | 2021.11.23 |
분류 - 스태킹 앙상블 (0) | 2021.11.16 |
분류 실습 - 캐글 신용카드 사기 검출 (0) | 2021.11.14 |
분류 실습 - 캐글 산탄데르 고객 만족 예측 (0) | 2021.11.14 |