Loading [MathJax]/jax/output/CommonHTML/jax.js

릿지 회귀(Ridge Regression)

릿지 회귀가 뭐지?

Ridge regression is a method of estimating the coefficients of multiple-regression models in scenarios where independent variables are highly correlated.
- Wikipedia(Ridge regression)

기존의 다중 선형 회귀선을 팽팽한 고무줄이라고 하면, 릿지 회귀는 이 고무줄을 느슨하게 만들어준 것이다.

다중 선형 회귀 모델은 특성이 많아질수록 훈련 데이터에 과적합되기 쉽다. 이는 마치 "나는 훈련에서 무조건 만점을 받겠어!"라고 하여 열심히 훈련을 해서 만점을 받았는데, 정작 실전에서는 뭐 하나 제대로 못하는 병사같다.
이에 반해 릿지 회귀는 훈련은 좀 덜 열심히 했지만, 실전에서는 꽤 쓸만한 병사라고 할 수 있겠다.


릿지 회귀는 어떻게 생겼지?

과적합된 다중 선형 회귀 모델은 단 하나의 특이값에도 회귀선의 기울기가 크게 변할 수 있다. 릿지 회귀는 어떤 값을 통해 이 기울기가 덜 민감하게 반응하게끔 만드는데, 이 값을 람다(lambda, λ)라고 한다.
릿지 회귀의 식은 아래와 같다.

βridge:argmin[ni=1(yiβ0β1xi1βpxip)2+λpj=1β2j]

(n: 샘플 수, p: 특성 수, λ: 튜닝 파라미터(패널티))
(참고 - 람다 : lambda, alpha, regularization parameter, penalty term 모두 같은 뜻)

식의 앞 부분은 다중 선형 회귀에서의 최소제곱법(OLS, Ordinary Least Square)과 동일하다.
뒤쪽의 람다가 붙어 있는 부분이 기울기를 제어하는 패널티 부분이다.
뒷부분을 자세히 보면 회귀계수 제곱의 합으로 표현되어 있는데, 이는 L2 Loss1와 같다. 이런 이유로 릿지 회귀를 L2 정규화(L2 Regularization)라고도 한다.

만약 람다가 0이면 위 식은 다중 선형 회귀와 동일하다.
반대로 람다가 커지면 커질수록 다중 회귀선의 기울기를 떨어뜨려 0으로 수렴하게 만든다. 이는 덜 중요한 특성의 개수를 줄이는 효과로도 볼 수 있다.

적합한 람다의 값을 구하는 방법은 아래에서 scikit-learn을 통해 예시를 들겠다.


scikit-learn을 이용하여 릿지 회귀 수행하기

scikit-learn을 통해 릿지 회귀를 사용하는 방법으로 Ridge와 RidgeCV가 있다.
둘 모두 같은 릿지 회귀이나, RidgeCV는 여러 alpha값(=람다)을 모델 학습 시에 한꺼번에 받아서 자기 스스로 각각의 alpha값에 대한 성능을 비교 후 가장 좋은 alpha를 선택한다. (CV : Cross Validation2)

아래 예시에서는 먼저 다중 선형 회귀와 릿지 회귀의 차이를 간단히 살펴본 후, 보다 구체적인 Ridge 그리고 RidgeCV의 사용을 다루겠다.

  1. 다중 선형 회귀와 릿지 회귀 간단한 비교
    (사용 데이터 : Anscombe's quartet)
# 데이터 불러오기
import seaborn as sns

ans = sns.load_dataset('anscombe').query('dataset=="III"')
baseline = ans.y.mean() # 기준 모델
sns.lineplot(x='x', y=baseline, data=ans, color='red'); # 기준 모델 시각화
sns.scatterplot(x='x', y='y', data=ans);

 

# 다중 선형 회귀(OLS)
%matplotlib inline

ax = ans.plot.scatter('x', 'y')

# OLS 
ols = LinearRegression()
ols.fit(ans[['x']], ans['y'])

# 회귀계수와 intercept 확인
m = ols.coef_[0].round(2)
b = ols.intercept_.round(2)
title = f'Linear Regression \n y = {m}x + {b}'

# 훈련 데이터로 예측
ans['y_pred'] = ols.predict(ans[['x']])

ans.plot('x', 'y_pred', ax=ax, title=title);

 

# 릿지 회귀
import matplotlib.pyplot as plt
from sklearn.linear_model import Ridge

def ridge_anscombe(alpha):
    """
    alpha : lambda, penalty term
    """
    ans = sns.load_dataset('anscombe').query('dataset=="III"')

    ax = ans.plot.scatter('x', 'y')

    ridge = Ridge(alpha=alpha, normalize=True)
    ridge.fit(ans[['x']], ans['y'])

    # 회귀계수와 intercept 가져오기
    m = ridge.coef_[0].round(2)
    b = ridge.intercept_.round(2)
    title = f'Ridge Regression, alpha={alpha} \n y = {m}x + {b}'

    # 예측
    ans['y_pred'] = ridge.predict(ans[['x']])

    ans.plot('x', 'y_pred', ax=ax, title=title)
    plt.show()

# 여러 알파값을 넣어서 기울기의 변화 확인하기
alphas = np.arange(0, 2, 0.4)
for alpha in alphas:
    ridge_anscombe(alpha=alpha)

 

∴ 그래프를 보면 alpha = 0인 경우에는 OLS와 같은 그래프 형태로 같은 모델임을 확인할 수 있고. alpha 값이 커질수록 직선의 기울기가 0에 가까워 지면서 평균 기준모델(baseline)과 비슷해진다.


  1. Ridge & RidgeCV 구체적인 활용 방법

1) 데이터 불러오기 및 전처리
(사용할 데이터 : Melbourne Housing Market)

import pandas as pd
from sklearn.model_selection import train_test_split

# 데이터 불러오기
df = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/melbourne_house_prices/MELBOURNE_HOUSE_PRICES_LESS.csv')

# 범주형 특성 중 값의 종류가 너무 많은 특성은 제외
df.drop(columns=['Suburb','Address','SellerG','Date'], inplace=True)

# 결측치인 타겟 값 제거
df.dropna(subset=['Price'], inplace=True)

# 중복된 행 제거
df.drop_duplicates(inplace=True)
from category_encoders import OneHotEncoder

# 사용할 특성들과 타겟을 별도로 분리
target = 'Price'

data = df.drop(target, axis=1)
target = df[target]

# 훈련 / 테스트 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(data, target, train_size=0.8, test_size=0.2, random_state=2)

# 범주형 특성을 수치형으로 변환하는 인코딩 수행
# 자세한 내용은 아래 참고 자료 링크(One-hot Encoder)에 있습니다
encoder = OneHotEncoder(use_cat_names = True)
X_train = encoder.fit_transform(X_train)
X_test = encoder.transform(X_test)

2) Ridge

from sklearn.linear_model import Ridge

alphas = [0, 0.001, 0.01, 0.1, 1]

# Ridge의 경우 alpha값을 이와 같이 따로 따로 넣어주어야 함
for alpha in alphas:
  ridge = Ridge(alpha=alpha, normalize=True)
  ridge.fit(X_train, y_train)
  y_pred = ridge.predict(X_test)

  mae = mean_absolute_error(y_test, y_pred)
  r2 = r2_score(y_test, y_pred)
  print(f'Test MAE: ${mae:,.0f}')
  print(f'R2 Score: {r2:,.4f}\n')

Test MAE: $255,214
R2 Score: 0.5877

Test MAE: $255,264
R2 Score: 0.5878

Test MAE: $254,701
R2 Score: 0.5874

Test MAE: $252,997
R2 Score: 0.5794

Test MAE: $279,498
R2 Score: 0.4742


3) RidgeCV

from sklearn.linear_model import RidgeCV
from sklearn.metrics import mean_absolute_error, r2_score

alphas = [0, 0.001, 0.01, 0.1, 1]

# RidgeCV는 alpha로 넣고자 하는 값들을 리스트로 전달하면 내부적으로 최적의 alpha값을 찾아냄
ridgecv = RidgeCV(alphas=alphas, normalize=True, cv=5)
# cv : cross-validation -> 데이터를 k등분한 후 각각에 대하여 검증 진행
# 검증 결과 가장 점수가 높은 모델을 채택
ridgecv.fit(X_train, y_train)
y_pred = ridgecv.predict(X_test)

mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f'Test MAE: ${mae:,.0f}')
print(f'R2 Score: {r2:,.4f}\n')

print(f'alpha: {ridgecv.alpha_}') # 최종 결정된 alpha값
print(f'cv best score: {ridgecv.best_score_}') # 최종 alpha에서의 점수(R^2 of self.predict(X) wrt. y.)

Test MAE: $255,264
R2 Score: 0.5878

alpha: 0.001
cv best score: 0.5705823371670962


*1 L2 Loss란?
L2 Loss는 L2 Norm을 기준으로 만들어진 손실 함수이다.
다만 L2 Norm이 각 원소 제곱의 합에 루트를 씌워준 것이라면 L2 Loss는 루트를 씌우지 않는다는 차이가 있다.
L2 Norm=ni=1x2i
L2 Loss=ni=1(yˆy)2

*2 CV : Cross Validation(교차 검증)?
교차 검증은 주어진 데이터를 동일한 크기로 (고등어 자르듯이) 여러 등분한 후, 각각에 대하여 검증을 진행하는 것이다.
이는 모델이 한 가지 경우에만 잘 맞는, 즉 과적합을 줄이고자 행하는 작업이다.


<참고 자료>

+ Recent posts