by museonghwang

규제 선형 모델 - Ridge, Lasso, Elastic Net

|

규제 선형 모델

선형 모델의 비용 함수를 최소화(RSS 최소화, 실제 값과 예측값의 차이를 최소화)하는 것만 고려한다면, 학습 데이터에 지나치게 맞추게 되고, 회귀 계수가 쉽게 커지는 현상이 발생하는데, 이럴경우 변동성이 오히려 심해져서 테스트 데이터 세트에서는 예측 성능이 저하되기 쉽습니다. 이를 반영해 비용 함수학습 데이터의 잔차 오류 값을 최소로 하는 RSS 최소화 방법과적합을 방지하기 위해 회귀 계수 크기를 제어하는 방법서로 균형 을 이뤄야 합니다.

image


위 사항을 고려하여 비용(Cost) 함수의 목표 를 다음과 같이 변경할 수 있습니다.


\[Cost\ Function\ Objective = Min(RSS(W) + alpha * ||W||^2_2)\]


비용 함수의 목표가 $RSS(W) + alpha * ||W||^2_2$ 를 최소화하는 $W$ 벡터를 찾는 것으로 변경 되며, 여기서 alpha학습 데이터 적합 정도와 회귀 계수 값의 크기 제어를 수행하는 튜닝 파라미터 입니다.

  • alpha = 0(또는 매우 작은 값)인 경우
    • 비용 함수 식은 기존과 동일한 $Min(RSS(W) + 0)$ 이 될 것입니다.
    • 즉, $W$ 가 커도 $alpha * \lVert W \rVert^2_2$ 가 0이 되어 비용 함수는 $Min(RSS(W))$
  • alpha = 무한대(또는 매우 큰 값)인 경우
    • 비용 함수 식은 $RSS(W)$ 에 비해 $alpha * \lVert W \rVert^2_2$ 값이 너무 커지게 되므로 $W$ 값을 0(또는 매우 작게)으로 만들어야 Cost가 최소화되는 비용 함수 목표를 달성할 수 있습니다.
    • 즉, $alpha * \lVert W \rVert^2_2$ 가 무한대가 되므로 비용 함수는 $W$ 를 0에 가깝게 최소화 해야 함.


즉, alpha 값을 크게 하면 비용 함수는 회귀 계수 $W$ 의 값을 작게 해 과적합을 개선 할 수 있으며, alpha 값을 작게 하면 회귀 계수 $W$ 의 값이 커져도 어느 정도 상쇄가 가능하므로 학습 데이터 적합을 더 개선 할 수 있습니다.

즉, alpha를 0에서부터 지속적으로 값을 증가시키면 회귀 계수 값의 크기를 감소시킬 수 있습니다.

image


이처럼 비용 함수에 alpha 값으로 페널티를 부여해 회귀 계수 값의 크기를 감소시켜 과적합을 개선하는 방식규제(Regularization) 라고 부릅니다.

  • L2 규제
    • 릿지(Ridge) 회귀
    • $alpha * \lVert W \rVert^2_2$ 와 같이 $W$ 의 제곱에 대해 페널티를 부여하는 방식
  • L1 규제
    • 라쏘(Lasso) 회귀
    • $alpha * \lVert W \rVert_1$ 와 같이 $W$ 의 절댓값에 대해 페널티를 부여
    • L1 규제를 적용하면 영향력이 크지 않은 회귀 계수 값을 0으로 변환
    • 적절한 피처만 회귀에 포함시키는 피처 선택의 특성을 가짐
  • L1, L2 결합 규제
    • 엘라스틱넷(Elastic Net) 회귀
    • L2 규제와 L1 규제를 결합한 회귀
    • 엘라스틱넷 회귀 비용함수의 목표는 $RSS(W) + alpha2 * \lVert W \rVert_2^2 + alpha1*\lVert W \rVert_1$ 식을 최소화하는 $W$ 를 찾는 것


릿지(Ridge) 회귀

사이킷런은 Ridge 클래스를 통해 릿지 회귀를 구현합니다. Ridge 클래스의 주요 생성 파라미터는 alpha 이며, 이는 릿지 회귀의 alpha L2 규제 계수 에 해당합니다. 보스턴 주택 가격을 Ridge 클래스를 이용해 예측하고, 예측 성능을 cross_val_score() 로 평가해 보겠습니다.

import numpy as np
import pandas as pd
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score
from sklearn.datasets import fetch_openml
import warnings
warnings.filterwarnings('ignore')

# boston 데이타셋 로드
boston = fetch_openml(name='boston')

# boston 데이타셋 DataFrame 변환 
bostonDF = pd.DataFrame(boston.data, columns = boston.feature_names)
bostonDF['RAD']=bostonDF['RAD'].astype(int)
bostonDF['CHAS']=bostonDF['CHAS'].astype(int)

# boston dataset의 target array는 주택 가격임. 이를 PRICE 컬럼으로 DataFrame에 추가함. 
bostonDF['PRICE'] = boston.target

y_target = bostonDF['PRICE']
X_data = bostonDF.drop(['PRICE'],axis=1, inplace=False)
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))
[output]
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 


릿지의 5개 폴드 세트의 평균 RMSE가 5.518으로, 규제가 없는 LinearRegression의 RMSE 평균인 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))
[output]
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가 100일 때 평균 RMSE가 5.330으로 가장 좋습니다. 이번에는 alpha 값의 변화에 따른 피처의 회귀 계수값을 가로 막대 그래프로 시각화해 보겠습니다.

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns

# 각 alpha에 따른 회귀 계수 값을 시각화하기 위해 5개의 열로 된 맷플롯립 축 생성  
fig, axs = plt.subplots(figsize=(20, 8), 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()

image


alpha 값을 계속 증가시킬수록 회귀 계수 값은 지속적으로 작아짐을 알 수 있습니다. 특히 NOX 피처의 경우 alpha 값을 계속 증가시킴에 따라 회귀 계수가 크게 작아지고 있습니다. 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)

image


alpha 값이 증가하면서 회귀 계수가 지속적으로 작아지고 있음 을 알 수 있습니다. 하지만 릿지 회귀의 경우에는 회귀 계수를 0으로 만들지는 않습니다.


라쏘(Lasso) 회귀

사이킷런은 Lasso 클래스를 통해 라쏘 회귀를 구현합니다. Lasso 클래스의 주요 생성 파라미터는 alpha 이며, 이는 라쏘 회귀의 alpha L1 규제 계수 에 해당합니다. Lasso 클래스를 이용해 바로 이전의 릿지 회귀 예제 코드와 유사하게 라쏘의 alpha 값을 변화시키면서 RMSE와 각 피처의 회귀 계수를 출력해 보겠습니다.

get_linear_reg_eval() 라는 별도의 함수를 만들어 회귀 모델의 이름, alpha값들의 리스트, 피처 데이터 세트와 타깃 데이터 세트를 입력받아서 alpha값에 따른 폴드 평균 RMSE를 출력하고 회귀 계수값들을 DataFrame으로 반환하겠습니다.

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,
    return_coeff=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는 evaluation metric만 반환하므로 모델을 다시 학습하여 회귀 계수 추출
        
        model.fit(X_data_n, y_target_n)
        if return_coeff:
            # 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


함수를 생성했으면 이를 이용해 alpha값의 변화에 따른 RMSE와 그때의 회귀계수들을 출력해 보겠습니다. get_linear_reg_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
)
[output]
#######  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일 때 5.612로 가장 좋은 평균 RMSE를 보여줍니다. 앞의 릿지 평균 5.518 보다는 약간 떨어지는 수치지만, LinearRegression 평균인 5.829 보다는 향상됐습니다. 다음은 alpha 값에 따른 피처별 회귀 계수입니다.

# 반환된 coeff_lasso_df를 첫번째 컬럼순으로 내림차순 정렬하여 회귀계수 DataFrame출력
sort_column = 'alpha:' + str(lasso_alphas[0])
coeff_lasso_df.sort_values(by=sort_column, ascending=False)

image


alpha의 크기가 증가함에 따라 일부 피처의 회귀 계수는 아예 0으로 바뀌고 있습니다. 회귀 계수가 0인 피처는 회귀 식에서 제외되면서 피처 선택의 효과 를 얻을수 있습니다.


엘라스틱(Elastic Net)넷 회귀

사이킷런은 ElasticNet 클래스를 통해서 엘라스틱넷 회귀를 구현합니다. ElasticNet 클래스의 주요생성 파라미터는 alphal1_ratio 입니다.

ElasticNet 클래스의 alphaRidgeLasso 클래스의 alpha 값과는 다릅니다. 엘라스틱넷의 규제는 a * L1 + b * L2 로 정의될 수 있습니다.

  • a : L1 규제의 alpha 값
  • b : L2 규제의 alpha 값
  • ElasticNet 클래스의 alpha 파라미터 값 : a + b
  • l1_ratio 파라미터 값 : a / (a + b)
    • l1_ratio 가 0 : a가 0이므로 L2 규제와 동일
    • l1_ratio 가 1 : b가 0이므로 L1 규제와 동일


ElasticNet 클래스를 이용해 바로 이전의 릿지, 라쏘 회귀 예제 코드와 유사하게 엘라스틱넷 alpha 값을 변화시키면서 RMSE와 각 피처의 회귀 계수를 출력해 보겠습니다.

# 엘라스틱넷에 사용될 alpha 파라미터의 값들을 정의하고 get_linear_reg_eval() 함수 호출
# l1_ratio는 0.7로 고정
elastic_alphas = [ 0.07, 0.1, 0.5, 1, 3]
coeff_elastic_df =get_linear_reg_eval(
    'ElasticNet',
    params=elastic_alphas,
    X_data_n=X_data,
    y_target_n=y_target
)
[output]
#######  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_elastic_df.sort_values(by=sort_column, ascending=False)

image


alpha 0.5일 때 RMSE가 5.467로 가장 좋은 예측 성능을 보이고 있습니다. alpha값에 따른 피처들의 회귀 계수들 값이 라쏘보다는 상대적으로 0이 되는 값이 적음 을 알 수 있습니다.

지금까지 규제 선형 회귀의 가장 대표적인 기법인 릿지, 라쏘, 엘라스틱넷 회귀를 살펴봤습니다. 이들중 어떤 것이 가장 좋은지는 상황에 따라 다르며, 각각의 알고리즘에서 하이퍼 파라미터를 변경해 가면서 최적의 예측 성능을 찾아내야 합니다. 하지만 선형 회귀의 경우 최적의 하이퍼 파라미터를 찾아내는 것 못지않게 먼저 데이터 분포도의 정규화와 인코딩 방법이 매우 중요합니다.


선형 회귀 모델을 위한 데이터 변환

선형 모델은 피처값과 타깃값의 분포가 정규 분포 형태를 매우 선호합니다. 따라서 선형 회귀 모델을 적용하기 전에 먼저 데이터에 대한 스케일링/정규화 작업을 수행하는 것이 일반적 입니다. 이러한 스케일링/정규화 작업을 선행한다고 해서 무조건 예측 성능이 향상되는 것은 아니지만, 일반적으로 중요 피처들이나 타깃값의 분포도가 심하게 왜곡됐을 경우에 이러한 변환 작업을 수행합니다.

  • 피처 데이터 세트에 적용하는 주요 변환 작업
    1. 스케일링/정규화를 수행
      • StandardScaler 클래스 : 평균이 0, 분산이 1인 표준 정규 분포를 가진 데이터 세트로 변환
      • MinMaxScaler 클래스 : 최솟값이 0이고 최댓값이 1인 값으로 정규화를 수행
    2. 스케일링/정규화를 수행한 데이터 세트에 다시 다항 특성을 적용하여 변환
      • 보통 1번 방법 후 예측 성능에 향상이 없을 경우 적용
    3. 로그 변환(Log Transformation)
  • 타킷값에 적용하는 주요 변환 작업
    • 로그 변환(Log Translormation) : 원래 값에 log 함수를 적용하면 보다 정규 분포에 가까운 형태로 값이 분포됨.
    • 앞 1, 2번 방법보다 훨씬 많이 사용


보스턴 주택가격 피처 데이터 세트에 위에서 언급한 표준 정규 분포 변환, 최댓값/최솟값 정규화, 로그 변환을 차례로 적용한 후에 RMSE로 각 경우별 예측 성능을 측정해 보겠습니다. 이를 위해 get_scaled_data() 함수를 생성합니다.

from sklearn.preprocessing import StandardScaler, MinMaxScaler, PolynomialFeatures

# 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]

# 변환 방법은 모두 6개, 원본 그대로, 표준정규분포, 표준정규분포+다항식 특성
# 최대/최소 정규화, 최대/최소 정규화+다항식 특성, 로그변환 
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(X_data_scaled.shape, X_data.shape)
    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,
        return_coeff=False
    )

image


일반적으로 선형 회귀를 적용하려는 데이터 세트에 데이터 값의 분포가 심하게 왜곡되어 있을 경우, 로그 변환을 적용하는 것이 좋은 결과를 기대할 수 있습니다.