릿지와 랏소로 과대 적합을 막아라
지난번에 농어의 길이로 무게를 선형 회귀(1차), 다항 회귀(2차)로 예측해보았다.
그럼 이번엔 길이, 높이, 두께 의 3개의 데이터 세트와 각각의 무게가 주어졌을 때 회귀분석을 어떻게 하는지 알아보자.
하나의 특성치가 아닌 두 개 이상의 특성치가 존재하고, 각각이 2차 항으로 한 특성치의 제곱항과 여러 특성치의 곱으로도 새로운 항이 존재할 때, 다중 회귀 분석을 하게 된다.
데이터는 농어의 길이, 높이, 두께 데이터와 무게 데이터가 세트로 존재한다. 파이썬은 target 열인 무게 변수를 별도로 두고 train, test를 나누기 때문에 아래와 같이 길이, 높이, 두께를 perch_full, 무기를 perch_weight로 두고 시작한다. pandas로 csv 파일을 읽은 후 다시 numpy로 배열을 만든다.
import pandas as pd
df=pd.read_csv('http://bit.ly/perch_csv_data')
perch_full=df.to_numpy()
import numpy as np
perch_weight = np.array([5.9, 32.0, 40.0, 51.5, 70.0, 100.0, 78.0, 80.0, 85.0, 85.0, 110.0,
115.0, 125.0, 130.0, 120.0, 120.0, 130.0, 135.0, 110.0, 130.0,
150.0, 145.0, 150.0, 170.0, 225.0, 145.0, 188.0, 180.0, 197.0,
218.0, 300.0, 260.0, 265.0, 250.0, 250.0, 300.0, 320.0, 514.0,
556.0, 840.0, 685.0, 700.0, 700.0, 690.0, 900.0, 650.0, 820.0,
850.0, 900.0, 1015.0, 820.0, 1100.0, 1000.0, 1100.0, 1000.0,
1000.0])
다음은 train과 test를 0.75:0.25로 나눔. (이것은 train_test_split 패키지를 이용한다.)
from sklearn.model_selection import train_test_split
train_x, test_x, train_y, test_y= train_test_split(perch_full, perch_weight, random_state=42)
그다음이 중요하다. 파이썬은 3개의 항이 존재할 때 각각의 제곱과 서로 곱으로 새로운 값을 만드는 PolynomialFeatures 라는 패키지가 존재한다. 이걸 불러온 후 위에서 만든 train_x를 훈련시킨 후 (fit)
길이, 높이, 두께, 길이^2, 높이^2, 두께^2, 길이 x 높이, 높이 x 두께, 길이 x 두께의 총 9개 열의 새로운 변수 train_poly를 만든다. (transform)
from sklearn.preprocessing import PolynomialFeatures
poly=PolynomialFeatures(include_bias=False)
poly.fit(train_x)
train_poly=poly.transform(train_x)
train_poly의 형태와 각각의 열이 어떻게 된 것인지 알아보자.
print(train_poly.shape)
poly.get_feature_names()
(42, 9)
['x0', 'x1', 'x2', 'x0^2', 'x0 x1', 'x0 x2', 'x1^2', 'x1 x2', 'x2^2']
test 데이터도 마찬가지로 test_poly를 만들자.
test_poly=poly.transform(test_x)
다음은 파이썬의 선형 회귀 패키지를 통해 train_poly로 (9개의 항) train_y를 예측해보자. (fit) 그리고 얼마나 잘 맞았는지 score() 함수로 살펴보자.
from sklearn.linear_model import LinearRegression
lr=LinearRegression()
lr.fit(train_poly, train_y)
print(lr.score(train_poly, train_y))
print(lr.score(test_poly, test_y))
0.9903183436982124
0.9714559911594132
train은 99%, test는 97%의 R-square 정합률을 보여주었다.
9개의 항의 계수가 각각 어떻게 나왔는지 살펴보자. 제곱항들의 계수가 작고, 각각 길이, 높이, 두께와 서로의 곱으로 만들어진 항의 계수가 크게 나옴을 알 수 있었다.
['x0', 'x1', 'x2', 'x0^2', 'x0 x1', 'x0 x2', 'x1^2', 'x1 x2', 'x2^2']
print(lr.coef_)
[ 34.80604039 -88.68430232 -184.11606694 -2.2696004 8.74890226
9.41670602 27.76120631 -119.89306061 93.68198334]
다중 회귀 분석을 R로 다시 해보면 파이썬 보다 좀 복잡한 수식이 들어감을 알 수 있다.
농어의 길이, 높이, 두께 데이터를 csv 파일로 불러오고, 무게 데이터와 묶어서 하나의 dataframe으로 만드는 것에서부터 시작한다.
df<-read.csv('http://bit.ly/perch_csv_data')
target<-c(5.9, 32.0, 40.0, 51.5, 70.0, 100.0, 78.0, 80.0, 85.0, 85.0, 110.0,
115.0, 125.0, 130.0, 120.0, 120.0, 130.0, 135.0, 110.0, 130.0,
150.0, 145.0, 150.0, 170.0, 225.0, 145.0, 188.0, 180.0, 197.0,
218.0, 300.0, 260.0, 265.0, 250.0, 250.0, 300.0, 320.0, 514.0,
556.0, 840.0, 685.0, 700.0, 700.0, 690.0, 900.0, 650.0, 820.0,
850.0, 900.0, 1015.0, 820.0, 1100.0, 1000.0, 1100.0, 1000.0,
1000.0)
df<-cbind(df, target)
다음은 df를 train과 test로 나눔.
set.seed(123)
idx<-sample(1:nrow(df), nrow(df)*0.75, replace=F)
train<-df[idx, ]
test<-df[-idx,]
파이썬에서는 다중 회귀 변수들을 만든 것이 쉬웠지만 R은 아래와 같이 좀 복잡한 방법을 쓴다. train에 길이, 높이, 두께, 무게가 모두 있으므로 c(1:3) 열만 가지고 다중 회귀 변수를 만든다. 그리고 새로 만든 변수의 각 열의 이름을 지정해준다.
formula <- as.formula(paste(' ~ .^2 + ',paste('poly(',colnames(train[,c(1:3)]),',2, raw=TRUE)[, 2]',collapse = ' + ')))
train_poly<-model.matrix(formula, data=train[,c(1:3)])
train_poly<-train_poly[,-1]
train_p<-as.data.frame(cbind(train_poly, train$target))
colnames(train_p)<- c("l", "h","w", "l2", "h2", "w2", "lxh", "lxw", "hxw", "y")
test 데이터도 똑같이 해 준다.
formula <- as.formula(paste(' ~ .^2 + ',paste('poly(',colnames(test[,c(1:3)]),',2, raw=TRUE)[, 2]',collapse = ' + ')))
test_poly<-model.matrix(formula, data=test[,c(1:3)])
test_poly<-test_poly[,-1]
test_p<-as.data.frame(cbind(test_poly, test$target))
colnames(test_p)<- c("l", "h","w", "l2", "h2", "w2", "lxh", "lxw", "hxw", "y")
이렇게 되면 회귀 분석을 할 준비가 다 되었다. y 값을 제외하고 나머지 변수를 이용하여 회귀모델을 만들고 R-square를 구할 수 있다. 그리고 각 항의 계수도 알 수 있는데, p-value가 0.05보다 작은 Intercept와 길이 x 두께가 가장 유의한 것을 알 수 있다.
lm <-lm(y~., train_p)
summary(lm)
이 모델을 이용하여 test 데이터를 예측해보고, 원래 데이터(y)와 비교해 보자. 예측값(pre)을 x축으로 원래 값을 y축을 그려보면 대략 직선 위에 존재함을 알 수 있다.
pre<-predict.lm(lm, test_p[,c(1:9)], interval = "none")
plot(pre, test_p$y)
위에서 실시한 다중회귀 분석에는 문제점이 있다. 바로 길이, 높이, 두께의 각각의 값이 서로 다른 범주의 값이므로 표준화가 필요하다는 것이다. 그래야 다중회귀 함수의 각 항의 계수가 의미가 있게 된다.
파이썬에는 표준화하는 패키지 StardardScaler()가 있다. 이걸 불러온 후, train_poly와 test_poly를 변환한다. (transform)
from sklearn.preprocessing import StandardScaler
ss=StandardScaler()
ss.fit(train_poly)
train_scaled=ss.transform(train_poly)
test_scaled=ss.transform(test_poly)
그러고 나서 이 변수들을 이용하여 위에서 했던 선형 회귀 모델을 만든 후 train_scaled를 학습시킨 후 R-square와 각 항의 계수를 알아보자.
lr_s=LinearRegression()
lr_s.fit(train_scaled, train_y)
print(lr_s.score(train_scaled, train_y))
print(lr_s.score(test_scaled, test_y))
print(lr_s.coef_)
0.9903183436982123
0.9714559911594125
[ 303.79797864 -254.82826832 -321.53684302 -1191.04767776
1407.14218929 908.51794857 1374.63985081 -3567.76517863
1696.94993949]
R-square 값은 동일하게 나오나 각 항의 계수는 달라졌다. 길이, 높이, 두께의 제곱항과 각각의 서로 곱에 해당하는 항의 계수 값이 커졌다.
R에서도 표준화를 손쉽게 할 수 있는 scale() 함수가 있다. 무게 값을 제외한 변수들을 표준화 변수로 만든다.
train_scaled<-as.data.frame(cbind(scale(train_p[,c(1:9)]), train_p[,10]))
test_scaled<-as.data.frame(cbind(scale(test_p[,c(1:9)]), test_p[,10]))
그러고 나서 이 표준화 변수로 선형 회귀 분석을 다시 한다. R-square는 변하지 않았지만 각 항에 대한 계수 값들이 변했다. 파이썬과 유사하게 제곱항과 각 항의 서로 곱으로 된 항이 계수의 절댓값이 컸지만 길이의 제곱항은 왠지 모르게 작게 나왔다.
어쨌든 이렇게 표준화 변화된 변수로 시행한 다중회귀 분석 모델을 이용하여 test 데이터 세트를 넣어 예측한 무게와 실제 값의 차이를 비교해보면 아래와 같다.
예측한 값과 실제 test_y 값의 R-square 값은 아래와 같이 구했다.
다중회귀 분석에서 항의 수를 무한정 늘이면 과대 적합이 될 수 있다. 이 때문에 항의 수에 규제를 걸 수 있는 방법이 랏쏘와 릿지 모델이다. 랏소는 필요 없는 항의 계수를 0으로 만드는 것이고, 릿지는 회귀 계수를 전체적으로 작게 만들어서 모형을 단순화하는 것이다.
#릿지 모델
from sklearn.linear_model import Ridge
ridge=Ridge()
ridge.fit(train_scaled, train_y)
print(ridge.score(train_scaled, train_y))
print(ridge.score(test_scaled, test_y))
print(np.around(ridge.coef_))
0.9857915060511934
0.9835057194929057
[-15. -14. -32. 74. 78. 61. 82. 64. 50.]
#랏쏘 회귀
from sklearn.linear_model import Lasso
lasso=Lasso()
lasso.fit(train_scaled, train_y)
print(lasso.score(train_scaled, train_y))
print(lasso.score(test_scaled, test_y))
print(np.around(lasso.coef_))
0.9865912554645588
0.9846056618190413
[ -0. -75. -0. 48. 147. 9. 158. 0. 63.]
랏쏘 회귀분석을 하니 3개 항의 계수가 0이 되었다.
R로 하기 위해서는 glmnet 패키지를 설치해야 한다.
install.packages("glmnet")
library(glmnet)
glmnet에서 alpha=1 이면 릿지 모델, alpha=0 이면 랏쏘 모델이 된다. 여기서 lambda 값이 있는데, 이 값의 최적 값을 찾는 작업을 한 후 릿지와 랏쏘를 진행하기로 하자.
lamda_list=10^seq(2, -2, by=-.1)
train_ridge<- data.matrix(train_scaled[,c(1:9)])
train_y<-train_scaled$y
ridge <- glmnet(train_ridge, train_y , alpha = 1, lambda =lamda_list)
ridge_cv <- cv.glmnet(train_ridge, train_y, alpha = 1, lambda = lamda_list)
best_lambda <- ridge_cv$lambda.min
best_lambda
0.316227766016838
이렇게 구해진 값을 넣어서 릿지와 랏쏘 모델을 만든다.
best_ridge <- glmnet(train_ridge, train_y, alpha = 1, lambda = 0.31)
best_lasso <- glmnet(train_ridge, train_y, alpha = 0, lambda = 0.31)
#릿지 회귀
ridge.fit<-glmnet(train_ridge, train_y, family = "gaussian", alpha=0, lambda=best_lambda)
round(coef(ridge.fit))
10 x 1 sparse Matrix of class "dgCMatrix"
s0
(Intercept) 355
l -92
h -57
w -50
l2 212
h2 132
w2 155
lxh 79
lxw -27
hxw -12
#랏쏘 회귀
lasso.fit<-glmnet(train_ridge, train_y, family = "gaussian", alpha=1, lambda=best_lambda)
round(coef(lasso.fit))
10 x 1 sparse Matrix of class "dgCMatrix"
s0
(Intercept) 355
l -162
h 0
w -4
l2 294
h2 112
w2 99
lxh .
lxw .
hxw .
이렇게 파이썬과 R을 이용하여 릿지와 랏쏘 회귀분석을 서로 비교해보았다.
[다중회귀, 릿지, 랏쏘_파이썬 코드]
https://colab.research.google.com/drive/1Vd6z7l1bn2QctZT_rIBJS2IQ9GLwNqWb?usp=sharing
[다중회귀, 릿지, 랏쏘_R 코드]
https://colab.research.google.com/drive/1DY81g0BwHV9kp8vTVseg4-Qh2ER-VcGP?usp=sharing