[파이썬 머신러닝 완벽 가이드] 5장. 회귀 - 1
# 22.01.23 머신러닝 스터디 5장. 회귀 - 1
<파이썬 머신러닝 완벽 가이드>¶
회귀 1 ( p290 ~ p319)¶
1. 회귀란?¶
: 데이터 값이 평균과 같은 일정한 값으로돌아가려는 경향을 이용한 통계적 기법
여러개의 독립변수 -> 1개의 종속변수
이 때 독립변수 개수가 1개 : 단일회귀, 여러개 : 다중 회귀
회귀 계수(Regression coefficients) : 독립변수가 한 단위 변화함에 따라 종속변수에 미치는 영향력 크기
이 때 회귀계수가 선형 : 선형회귀, 비선형 : 비선형 회귀
- 대표적인 선형 회귀 모델
- 일반 선형 회귀 : 예측값과 실제 값의 RSS(Residual Sum of Squares)를 최소화할 수 있도록 회귀 계수를 최적화하며, 규제를 적용하지 않은 모델
- 릿지(Ridge) : 선형 회귀에 L2 규제를 추가한 회귀 모델. 상대적으로 큰 회귀 계수 값의 예측 영향도를 감소시키기 위해서 회귀 계수값을 더 작게 만드는 규제 모델
- 라쏘(Lasso) : 선형 회귀에 L1 규제를 적용한 방식. 예측 영향력이 작은 피처의 회귀 계수를 0으로 만들어 회귀 예측 시 피처가 선택되지 않게 하는 것 ( 피처 선택 기능 )
- 엘라스틱넷(ElasticNet) : L2, L1 규제를 함께 결합한 모델. 주로 피처가 많은 데이터 세트에서 적용되고 L1 규제로 피처의 개수를 줄이고 L2 규제로 계수 값의 크기를 조정한다.
- 로지스틱 회귀(Logistic Regression) : 강력한 분류 알고리즘. 일반적으로 이진 분류뿐만 아니라 희소 영역의 분류에서 뛰어난 예측 성능을 보인다. ex) 텍스트 분류
2. 단순 선형 회귀를 통한 회귀 이해¶
단순선형 회귀 : 독립변수도 하나, 종속변수도 하나인 선형 회귀 ( 직선 형태로 표현 )
ŷ = w0 + w1*x
다음과 같은 독립변수가 1개인 단순 선형 회귀에서는 기울기인 w1와 절편인 w0이 회귀 계수가 된다. 또한 위의 회귀 모델로 예측한 값과 실제 값과의 차이를 잔차라고 부르며 결국 최적의 회귀 모델을 만들기 위해서는 전체 데이터의 잔차 합이 최소가 되는 모델을 만들어야 한다. ( = 최적의 회귀 계수를 찾는다 )
오류 값은 +나 -가 될 수 있고 보통 오류 합을 계산할 때는 절댓값을 취해서 더하거나 (Mean Absolute Error) 오류 값의 제곱을 구해서 더하는 방식 (Residual Sum of Square)을 취한다. 일반적으로는 미분 등의 계산을 편리하게 하기 위해 RSS방식을 택한다. ( ERROR^2 = RSS )
회귀에서 RSS는 비용(Cost)이며 회귀계수로 구성되는 RSS를 비용 함수라고 한다. 머신러닝 회귀 알고리즘은 데이터를 계속 학습하면서 이 비용 함수가 반환하는 오류 값을 지속해서 감소시키고 최종적으로는 더 이상 감소하지 않는 최소의 오류 값을 구하는 것! ( = 손실 함수 )
3. 경사하강법¶
<참고자료>
https://angeloyeo.github.io/2020/08/16/gradient_descent.html
그래서 비용함수가 최소가 되는 w를 어떻게 구하는데??
기본적인 방정식을 매번 풀이하기에는 피처 수가 많아졌을 때 고차원 방정식의 해결이 어려워진다.
-> 최초 w에서 미분을 적용한 뒤 이 미분 값이 계속 감소하는 방향으로 순차적으로 w를 업데이트. 마침내 1차 함수의 기울기가 감소하지 않는 지점을 비용 함수가 최소인 지점으로 간주하고 그 때의 w를 반환한다.
비용함수 RSS를 미분해서 미분 함수의 최솟값을 구해야 하는데 이 때 RSS는 2개의 파라미터를 가지고 있기 때문에 w0, w1에 각각 편미분을 적용해야 한다.
결국 시그마 안에 들어가는 식의 형태는 모두 (실제값 - 예측값)의 형태를 지닌다.
위 편미분 결과 값을 반복적으로 보정하면서 w1, w0를 업데이트 해주는데 이 때 업데이트는 새로운 값을 이전 값에서 -= 해준다. 이 때 편미분 값이 너무 클 수 있기 때문에 보정계수 η(에타)를 곱해주는 데 이를 '학습률'이라고 한다.
정리
- w1, w0를 임의의 값으로 설정하고 첫 비용 함수의 값을 계산
- w1, w0의 값을 위의 편미분 값으로 업데이트 해주고 다시 비용 함수의 값을 계산
- 비용 함수의 값이 감소했으면 다시 2번 반복, 더 이상 비용 함수의 값이 감소하지 않으면 그때의 w1, w0를 구하고 반복을 중지
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0)
# y = 4x + 6을 근사할거임
X = 2 * np.random.rand(100,1)# rand => 0 ~ 1 사이의 균일 분포에서 난수 matrix array생성 ( m * n 크기)
y = 6 + 4 * X + np.random.randn(100,1) # randn => 가우시안 표준 정규 분포에서 난수 matrix array생성 ( m * n 크기)
plt.scatter(X,y)
plt.show()
# X, y 모두 array로 1개의 column이라고 생각하면 될듯
# 비용 함수 정의하기 ( RSS )
# 1 / N 시그마 1부터 N 까지 (실제값 - 예측값) ^2
def get_cost(y, y_pred):
N = len(y)
cost = np.sum(np.square(y - y_pred)) / N
return cost
# 경사하강법을 구현
# w1, w0을 0으로 초기화한 뒤 iters 개수만큼 반복하면서 w1과 w0을 업데이트 ( 편미분 적용 )
# 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) # n * 1
w0_update = np.zeros_like(w0) # n * 1
# 예측 배열 계산하고 예측과 실제 값의 차이 계산
y_pred = np.dot(X, w1.T) + w0 # 예측 값 n*1행렬 dot 1*n 행렬 -> n * n 행렬 ( X값이 1 ~ n, 각각의 가중치가 1 ~ n)
diff = y - y_pred # n*1행렬 - n*n행렬?? -> 자동으로 n*1행렬을 n*n 행렬로 확장해서 -를 계산해준다!! ( n * n 행렬)
# 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)) # 1 * n 행렬 dot n * n 행렬
w0_update = -(2/N)*learning_rate*(np.dot(w0_factors.T, diff)) # 1 * n 행렬 dot n * n 행렬 -> 1 * n 행렬
return w1_update, w0_update # 1 * n 행렬
# n*1행렬 - n*n행렬? 예시로 확인
y[0]
array([9.22535819])
np.dot(X,X.T)[0]
array([1.20478505, 1.57002233, 1.32321872, 1.196157 , 0.9300299 , 1.41790165, 0.96061508, 1.95766826, 2.11548454, 0.84175153, 1.73803757, 1.1610587 , 1.2470021 , 2.03191974, 0.15594219, 0.19127095, 0.04438452, 1.82781206, 1.70825173, 1.90989766, 2.14831585, 1.75435605, 1.01306442, 1.71345981, 0.25964241, 1.40478919, 0.31469688, 2.07378823, 1.14558962, 0.91028829, 0.58076677, 1.69963962, 1.00136585, 1.24785691, 0.04124838, 1.35586681, 1.34370559, 1.35432683, 2.07176676, 1.49676875, 0.78921116, 0.95939615, 1.53147768, 0.13221021, 1.46372231, 1.47222048, 0.46184316, 0.28302597, 0.69244535, 0.79843753, 1.25172675, 0.96284173, 2.16973164, 0.22401428, 0.45853754, 0.35411537, 1.43373867, 0.55603941, 1.0236706 , 0.53657626, 0.34897862, 0.24230147, 1.44081017, 0.30334668, 0.43154822, 0.80944541, 1.80228868, 0.21316197, 1.83950192, 0.21096042, 2.14357656, 1.02880843, 2.1442387 , 1.32778956, 1.62287134, 0.08602716, 0.62083312, 0.26386198, 0.65010296, 0.2606375 , 0.69805385, 0.9094125 , 0.14082005, 1.5201522 , 1.24383412, 0.58259735, 1.14866239, 0.20622328, 1.26434886, 2.04004121, 0.69933977, 1.46513532, 0.28932979, 1.57252017, 0.63531989, 0.40215157, 1.28754488, 0.04414117, 1.81973393, 0.01030776])
(y - np.dot(X,X.T))[0]
array([8.02057314, 7.65533586, 7.90213947, 8.0292012 , 8.29532829, 7.80745655, 8.26474311, 7.26768993, 7.10987365, 8.38360666, 7.48732062, 8.06429949, 7.97835609, 7.19343845, 9.069416 , 9.03408725, 9.18097367, 7.39754613, 7.51710646, 7.31546053, 7.07704235, 7.47100214, 8.21229377, 7.51189838, 8.96571578, 7.820569 , 8.91066131, 7.15156996, 8.07976857, 8.3150699 , 8.64459142, 7.52571857, 8.22399234, 7.97750128, 9.18410981, 7.86949139, 7.8816526 , 7.87103136, 7.15359143, 7.72858944, 8.43614703, 8.26596204, 7.69388051, 9.09314798, 7.76163588, 7.75313771, 8.76351503, 8.94233222, 8.53291284, 8.42692066, 7.97363144, 8.26251646, 7.05562655, 9.00134391, 8.76682065, 8.87124282, 7.79161952, 8.66931878, 8.20168759, 8.68878193, 8.87637957, 8.98305672, 7.78454802, 8.92201151, 8.79380997, 8.41591278, 7.42306951, 9.01219623, 7.38585627, 9.01439777, 7.08178163, 8.19654976, 7.08111949, 7.89756863, 7.60248685, 9.13933103, 8.60452507, 8.96149621, 8.57525523, 8.96472069, 8.52730434, 8.31594569, 9.08453814, 7.70520599, 7.98152407, 8.64276084, 8.0766958 , 9.01913491, 7.96100933, 7.18531698, 8.52601842, 7.76022287, 8.9360284 , 7.65283802, 8.5900383 , 8.82320662, 7.93781332, 9.18121702, 7.40562426, 9.21505043])
# 경사 하강 방식으로 업데이트를 반복적으로 수행!!
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()를 호출해 업데이트
for _ in range(iters):
w1_update, w0_update = get_weight_updates(w1, w0, X, y, learning_rate = 0.01) # w0, w1이 1*1행렬이므로 1개의 값만 출력된다
w1 -= w1_update
w0 -= w0_update
return w1, w0
w1, w0 = 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('Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y,y_pred)))
w1 : 4.022 , w0 : 6.162 Gradient Descent Total Cost:0.9935
# 회귀선 그리기
plt.scatter(X,y)
plt.plot(X, y_pred)
plt.show()
위의 방식같은 일반적인 경사하강법은 너무 오래걸리기 때문에 확률적 경사 하강법이나 미니 배치 확률적 경사 하강법을 이용해 최적 비용함수를 도출한다.
# (미니 배치) 확률적 경사 하강법
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 값으로 저장
stochastic_random_index = np.random.permutation(X.shape[0]) # permutation = array를 복사해서 셔플 ( Shuffle은 해당 array)
sample_X = X[stochastic_random_index[0:batch_size]]
sample_y = y[stochastic_random_index[0:batch_size]] # 랜덤으로 섞어서 batch_size만큼의 행을 위에서부터 선택
# sample 사이즈로 X, y선택
w1_update, w0_update = get_weight_updates(w1, w0, sample_X, sample_y, learning_rate = 0.01)
w1 -= w1_update
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('Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y,y_pred)))
w1 : 4.028 , w0 : 6.156 Gradient Descent Total Cost:0.9937
지금까지는 단순 선형 회귀에서 경사 하강법을 적용하였는데 우리가 진짜 쓰게 될 피처가 여러개인 경우에는 어떻게 회귀 계수를 도출할 수 있을까?
피처가 M ( X1,X2....X100 ) 개일 경우에는 회귀 계수도 M + 1개로 도출 되고 Y = w0 + w1*X1 ...w100*X100으로 예측 회귀식을 만들 수 있다. 또한 이 때의 예측 행렬 y_pred는 굳이 개별적으로 X의 개별 원소와 w1의 값을 곱하지 않고 데이터의 개수가 N이고 피처가 M개인 입력행렬 X_mat과 회귀 계수 w1,w2...w100을 배열로 표기한 W와의 행렬곱으로 구할 수 있다.
y_pred = np.dot(X, w1.T) + w0 -> np.dot(X_mat, W^T) + w0
이 때 w0을 W배열에 포함시키기 위해 X_mat의 맨 처음 열에 모든 데이터의 값이 1인 피처를 추가해주면 Yhat = X_mat * W^T로 도출할 수 있다.
4. 선형 회귀를 이용한 보스턴 주택 가격 예측¶
LinearRegression 클래스는 예측값과 실제 값의 RSS를 최소화해 OLS(Ordinary Least Squares) 추정 방식으로 구현한 클래스이다. LinearRegression은 fit()으로 X, y배열을 입력 받으면 회귀계수를 coef_ 속성에 저장한다. OLS기반의 회귀 계수 계산은 입력 피처의 독립성에 많은 영향을 받는다. 피처 간의 상관관계가 매우 높은 경우 분산이 매우 커져서 오류에 매우 민감해진다. ( 다중 공선성 )
따라서 상관관계가 높은 피처가 많은 경우 독립적인 중요한 피처만 남기고 제거하거나 규제를 적용한다. 매우 많은 피처가 다중 공선성 문제를 가지고 있다면 PCA를 통해 차원 축소를 수행하는 것도 방법이다.
입력 파라미터
- fit_intercept : 불린 값, default = True, intercept값을 계산할지 말지를 지정
- normalize : 불린 값, default = False, fit_intercept=False인 경우에는 이 파라미터가 무시된다. True이면 회귀를 수행하기 전에 입력 데이터 세트를 정규화한다.
속성
- coef_ : fit() 메소드를 수행했을 때 회귀 계수를 배열 형태로 저장
- intercept : intercept값
회귀 평가 지표
회귀의 평가를 위한 지표는 실제 값과 회귀 예측값의 차이 값을 기반으로 한 지표가 중심이다. 실제 값과 예측값의 차이를 그냥 더하면 +와 -가 섞이기 때문에 오류가 상쇄되게 된다. 따라서 오류의 절댓값 평균이나 제곱, 또는 제곱한 뒤 다시 루트를 씌원 평균값을 구한다.
MAE ( Mean Absolute Error ) : 실제 값과 예측값의 차이를 절댓값으로 변환해 평균한 것
MSE ( Mean Squared Error ) : 실제 값과 예측값의 차이를 제곱해 평균한 것
RMSE ( Root Mean Squared Error ) : MSE 값은 오류의 제곱을 구하므로 실제 오류 평균보다 더 커지는 특성이 있으므로 MSE에 루트를 씌움
R^2 : 분산 기반으로 예측 성능을 평가. 실제 값의 분산 대비 예측값의 분산 비율을 지표로 하며, 1에 가까울수록 예측 정확도가 높다.
이 밖에 MSE나 RMSE에 로그를 적용한 MSLE ( Mean Squared Log Error ) 와 RMSLE ( Root Mean Squared Log Error ) 도 사용한다.
사이킷런은 RMSE를 제공하지 않기 때문에 따로 MSE에 제곱근을 씌워서 계산하는 함수를 만들어야 한다.
MAE나 MSE의 scoring 파라미터를 보면 'neg_mean_absolute_error'와 같이 neg라는 접두어가 붙어 있는 데 이는 음수(negative) 값을 가진 다는 의미이다. 하지만 MAE는 절댓값의 합이기 때문에 음수가 될 수 없다. 그렇다면 왜 음수값으로 scoring을 할까??
그 이유는 사이킷런의 Scoring 함수가 score값이 클수록 좋은 평가 결과라고 자동 인식하는데에 있다. 그런데 실제 값과 예측값의 오류 차이를 기반으로 하는 회귀 평가 지표의 경우 값이 커지면 오히려 나쁜 모델이기 때문에 원래의 평가 지표에 -1을 곱해서 작은 오류 값이 더 큰 숫자로 인식하게 됩니다.
보스턴 주택 가격 회귀 구현
- CRIM: 지역별 범죄 발생률
- ZN: 25,000평방피트를 초과하는 거주 지역의 비율
- INDUS: 비상업 지역 넓이 비율
- CHAS: 찰스강에 대한 더미 변수(강의 경계에 위치한 경우는 1, 아니면 0)
- NOX: 일산화질소 농도
- RM: 거주할 수 있는 방 개수
- AGE: 1940년 이전에 건축된 소유 주택의 비율
- DIS: 5개 주요 고용센터까지의 가중 거리
- RAD: 고속도로 접근 용이도
- TAX: 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)
CRIM | ZN | INDUS | CHAS | NOX | RM | AGE | DIS | RAD | TAX | PTRATIO | B | LSTAT | PRICE | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.00632 | 18.0 | 2.31 | 0.0 | 0.538 | 6.575 | 65.2 | 4.0900 | 1.0 | 296.0 | 15.3 | 396.90 | 4.98 | 24.0 |
1 | 0.02731 | 0.0 | 7.07 | 0.0 | 0.469 | 6.421 | 78.9 | 4.9671 | 2.0 | 242.0 | 17.8 | 396.90 | 9.14 | 21.6 |
2 | 0.02729 | 0.0 | 7.07 | 0.0 | 0.469 | 7.185 | 61.1 | 4.9671 | 2.0 | 242.0 | 17.8 | 392.83 | 4.03 | 34.7 |
3 | 0.03237 | 0.0 | 2.18 | 0.0 | 0.458 | 6.998 | 45.8 | 6.0622 | 3.0 | 222.0 | 18.7 | 394.63 | 2.94 | 33.4 |
4 | 0.06905 | 0.0 | 2.18 | 0.0 | 0.458 | 7.147 | 54.2 | 6.0622 | 3.0 | 222.0 | 18.7 | 396.90 | 5.33 | 36.2 |
# regplot() : X, Y 산점도와 함께 선형 회귀 직선을 그려준다.
# 2개의 행과 4개의 열을 가진 subplots를 이용. axs는 4*2개의 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])
RM(방 개수)과 LSTAT(하위 계층의 비율)의 PRICE영향도가 두드러지게 나타난다
방의 크기가 클수록 가격이 증가, 하위 계층의 비율이 적을 수록 가격이 증가
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)
# Linear Regression 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으로 생성한 주택가격 모델의 절편과 회귀 계수
print('절편 값 : ', lr.intercept_)
print('회귀 계수값 :', np.round(lr.coef_, 1))
절편 값 : 40.995595172164315 회귀 계수값 : [ -0.1 0.1 0. 3. -19.8 3.4 0. -1.7 0.4 -0. -0.9 0. -0.6]
# coef_ 속성이 회귀 계수 값만 가지고 있기 때문에 회귀 계수 값으로 다시 매핑
# 회귀 계수를 큰 값 순으로 정렬하기 위해 Series로 생성. index가 컬럼명에 유의
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 INDUS 0.0 AGE 0.0 TAX -0.0 B 0.0 CRIM -0.1 LSTAT -0.6 PTRATIO -0.9 DIS -1.7 NOX -19.8 dtype: float64
RM이 양의 값으로 회귀 계수가 가장 크며, NOX피처의 회귀 계수는 - 값이 너무 커보인다.
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 Fold 셋으로 MSE 를 구한 뒤 이를 기반으로 다시 RMSE 구함.
neg_mse_scores = cross_val_score(lr, X_data, y_target, scoring="neg_mean_squared_error", cv = 5)
# neg_mse_scores은 음수값이 반환
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
5. 다항 회귀와 과적합 / 과소적합 이해¶
회귀가 독립변수의 단항식이 아닌 2차, 3차 방정식과 같은 다항식으로 표혀되는 것을 다항 회귀라고 한다. ( 일차 방정식 형태로 표현 X ) 이 떄 주의해야 할 것은 다항 회귀와 비선형 회귀를 혼동하지 않는 것이다. 선형 / 비선형을 나누는 것은 회귀 계수가 선형 / 비선형 인지에 따른 것이지 독립변수의 선형 / 비선형이랑은 무관하기 때문이다. ( 다항 회귀 == 선형 회귀!! )
하지만 사이킷런은 다항 회귀를 위한 클래스를 제공하지 않기 때문에 비선형 함수를 선형 모델에 적용시키는 방법을 사용해 구현한다. 사이킷런은 PllynomialFeatures 클래스를 통해 피처를 Polynomial 피처로 변환한다. (입력받은 단항식 피처를 degree에 해당하는 다항식 피처로 변환 )
x1, x2 -> 1, x1, x2, x1^2, x1x2, x2^2
from sklearn.preprocessing import PolynomialFeatures
import numpy as np
# 다항식으로 변환한 단항식 생성
X = np.arange(4).reshape(2,2)
print('일차 단항식 계수 피처 \n', X)
# degree = 2 인 2차 다항식으로 변환
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.]]
3차 다항 회귀 함수를 임의로 설정
$$ y = 1 + 2x_1^2 +3x_1^2 + 4x_2^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('일차 단항식 계수 피처 \n', X)
y = polynomial_func(X)
print('삼차 다항식 결정값 : \n', y)
일차 단항식 계수 피처 [[0 1] [2 3]] 삼차 다항식 결정값 : [ 5 125]
3차 다항식 변환 $$ x1, x2 -> 1, x_1, x_2, x_1^2, x_1x_2, x_2^2, x_1^3, x_1^2x_2, x_1x_2^2, x_2^3$$
from sklearn.linear_model import LinearRegression
# 3차 다항식변환
poly_ftr = PolynomialFeatures(degree = 3).fit_transform(X) # 단항 계수 피처 -> 3차 다항계수로
print('3차 다항식 계수 feature : \n', poly_ftr)
# Linear Regression에 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,)
하지만 이렇게 피처 변환과 선형 회귀 적용을 별도로 하는 것 보다 사이킷런의 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]
다항 회귀를 이용한 과소적합 및 과적합 이해
다항 회귀는 피처의 직선적 관계가 아닌 복잡한 다항 관계를 모델링할 수 있다. 차수가 높아질수록 더 복잡한 피처 간의 관계까지 모델링이 가능하지만, 다항 회귀의 차수를 높일수록 학습 데이터에만 너무 맞춘 학습이 이뤄져서 정작 테스트 데이터 환경에서는 오히려 예측 정확도가 떨어진다. (과적합 문제)
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_cos(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_cos에 약간의 노이즈 변동 값을 더한 값
y = true_cos(X) + np.random.randn(n_samples) * 0.1
# 다항 회귀의 차수를 변화시키면서 그에 따른 회귀 예측 곡선과 예측 정확도를 비교할 것!!
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=()) # setp() : 선 두께, 색깔 등 좀 더 다양한 조정을 할 수 있음
# 개별 degree별로 Polynomial 변환합니다.
polynomial_features = PolynomialFeatures(degree = degrees[i], include_bias=False)
linear_regression = LinearRegression()
pipeline = Pipeline([("polynomial_features", polynomial_features),
("linear_regression", linear_regression)])
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_ # 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") # newaxis : numpy array의 차원을 늘려준다
# 실제 값 곡선
plt.plot(X_test, true_cos(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 회귀 계수는 [-2.] 입니다. Degree 1 MSE 는 0.40772896250986845 입니다. Degree 4 회귀 계수는 [ 0. -18. 24. -7.] 입니다. Degree 4 MSE 는 0.0432087498723184 입니다. Degree 15 회귀 계수는 [-2.98300000e+03 1.03900000e+05 -1.87417000e+06 2.03717200e+07 -1.44874017e+08 7.09319141e+08 -2.47067172e+09 6.24564702e+09 -1.15677216e+10 1.56895933e+10 -1.54007040e+10 1.06457993e+10 -4.91381016e+09 1.35920642e+09 -1.70382078e+08] 입니다. Degree 15 MSE 는 182581084.8263125 입니다.
위의 실선으로 표현된 예측 곡선이 다항 회귀 곡선이다. 점선은 실제 데이터 세트 X, Y의 코사인 곡선이다.
- Degree 1 : 단순한 직선으로서 학습 데ㅣ터의 패턴을 제대로 반영하지 못한다. ( 과소적합 )
- Degree 4 : 실제 데이터 세트와 유사하다. MSE값도 가장 낮은 것을 확인할 수 있다.
- Degree 15 : MSE 값이 말도 안되게 큰 수치를 기록, 변동 잡음까지 지나치게 반영하여 테스트 값의 실제 곡선과는 완전히 다른 곡선이 만들어졌다. ( 과적합 )
편향 - 분산 트레이드 오프
머신러닝이 극복해야 할 가장 중요한 이슈 중의 하나로 위의 예시에서 Degree 1인 경우에는 지나치게 한 방향으로 치우친 고편향(High Bias) 모델, Degree 15의 경우 지나치게 높은 변동성을 가지는 고분산(High Variance) 모델로 볼 수 있다.
일반적으로 편향과 분산은 한 쪽이 높으면 한 쪽이 낮아지는 경향이 있다.높은 편향 / 낮은 분산에서 과소적합되기 쉬우며 낮은 편향 / 높은 분산에서 과적합 되기가 쉽다. 따라서 편향과 분산이 서로 트레이드 오프를 이루면서 오류 Cost가 최소가 되는 모델을 구축하는 것이 가장 효율적인 머신러닝 예측 모델을 만드는 방법이다. ( 전체 오류가 가장 낮아지는 '골디락스' 지점 )
# 업로드용 창 맞추기
from IPython.core.display import display, HTML
display(HTML("<style>.container { width:90% !important; }</style>"))