본문 바로가기

Euron/정리

[Week5] 05. 회귀(2)

05. 다항 회귀와 과(대)적합/과소적합 이해

 

다항 회귀 이해

 

이전 회귀는 독립변수(feature)와 종속변수(target)의 관계가 일차 방정식으로 표현된 회귀였다.

다항(Polynomial) 회귀란 독립 변수의 단항식이 아닌 2,3차 방정식과 같은 다항식으로 표현되는 것을 말한다.

여기서 다항 회귀는 선형 회귀이다.

 

사이킷런은 다항 회귀를 위한 클래스를 제공하지 않으므로, 비선형 함수를 선형 모델에 적용시키는 방법을 사용해 구현한다.

이를 위해 사이킷런은 PolynomialFeatures 클래스를 통해 피처를 다항식 피처로 변환한다.

degree 파라미터를 통해 입력받은 단항식 피처를 degree에 해당하는 다항식 피처로 변환한다.

from sklearn.preprocessing import PolynomialFeatures
import numpy as np

# 다항식으로 변환한 단항식 생성, [[0,1],[2,3]]의 2X2 행렬 생성
X = np.arange(4).reshape(2,2)
print('일차 단항식 계수 feature:\n',X )

# degree = 2 인 2차 다항식으로 변환하기 위해 PolynomialFeatures를 이용하여 변환
poly = PolynomialFeatures(degree=2)
poly.fit(X)
poly_ftr = poly.transform(X)
print('변환된 2차 다항식 계수 feature:\n', poly_ftr)
일차 단항식 계수 feature:
 [[0 1]
 [2 3]]
변환된 2차 다항식 계수 feature:
 [[1. 0. 1. 0. 0. 1.]
 [1. 2. 3. 4. 6. 9.]]

[x1, x2]를 2차 다항 계수 [1, x1, x2, x1^2, x1x2, x2^2]로 변경한다.

이렇게 변환된 다항식 피처에 선형 회귀를 적용해 다항 회귀를 구현할 수 있다.

 

사이킷런의 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)가 높아질수록 복잡한 피처 간의 관계를 모델링 할 수 있지만, 차수과 과하게 높은 경우 과적합의 문제가 크게 발생한다.

다음은 차수(1,4,15) 변화에 따른 회귀 예측 곡선이다.(코드는 생략)

실선이 다항 회귀 예측 곡선, 점선은 실제 데이터 세트의 코사인 곡선

Degree1의 경우 단순한 직선으로 단순 선형 회귀와 똑같다. 예측 곡선이 학습 데이터의 패턴을 제대로 반영하지 못하는 과소적합 모델이다.

Degree4의 경우 잡음까지는 예측하지 못하지만, 데이터 세트를 잘 반영한 유사한 모델이 되었다.

Degree15의 경우 예측 곡선이 학습 데이터 세트만 정확히 예측하고, 테스트 값의 실제 곡선과는 완전히 다른 예측곡선이 만들어졌다. 과적합이 심한 모델이 되며, 매우 높은 MSE 값이 나왔다.

 

편향-분산 트레이드오프(Bias-Variance Trade off)

 

고편향성은 단순화된 모델로 지나치게 한 방향성으로 치우친 경향(Degree1), 고분산은 매우 복잡하며 지나친 변동성을 가짐(Degree15)을 말한다.

편향과 분산은 한쪽이 높으면 한쪽이 낮아지는 경향이 있다.

따라서 편향과 분산이 서로 트레이드 오프를 이루면서 오류 Cost 값이 최대로 모델을 구축하는 것이 가장 효율적인 머신러닝 예측 모델을 만드는 방법이다.

 


06. 규제 선형 모델 - 릿지, 라쏘, 엘라스틱넷

 

규제 선형 모델의 개요

 

비용 함수는 학습 데이터의 잔차 오류 값을 최소로 하는 RSS 최소화 방법과 과적합을 방지하기 위해 회귀 계수 값이 커지지 않도록 하는 방법이 서로 균형을 이뤄야 한다.

최소화하기

alpha는 학습 데이터 적합 정도와 회귀 계수 값의 크기 제어를 수행하는 튜닝 파라미터이다.

alpha 값이 커지면 과적합을 개선하고, 작게 하면 학습 데이터 적합을 개선할 수 있다.

즉, alpha를 0부터 지속적으로 값을 등가 시키면 회귀 계수 값의 크기를 감소시킬 수 있다. 이와 같이 회귀 계수 값의 크기를 감소시켜 과적합을 개선하는 방식을 규제(Regularization)이라 한다.

 

릿지 회귀

 

w의 제곱에 대해 페널티를 부여하는 L2 규제를 적용한 회귀 모델.

Ridge 클래스의 주요 생성 파라미터 alpha는 릿지 회귀의 L2 규제 개수에 해당한다.

이전 Boston 데이터 세트를 Ridge 클래스를 이용해 예측 후 평가한다.

# 앞의 LinearRegression예제에서 분할한 feature 데이터 셋인 X_data과 Target 데이터 셋인 Y_target 데이터셋을 그대로 이용 
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score

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

규제가 없는 LinearRegression의 평균 RMSE 5.829보다 뛰어난 예측 성능을 보여주었다.

 

이번에는 alpha값을 변화하면서 이를 시각화 하였다.

릿지 회귀는 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()

alpha값(맨 위) 주목

alpha가 커질수록 회귀 계수값이 작아지는데, 특히 NOX 피처의 경우 회귀 계수 값이 크게 작아지고 있다.

그러나 릿지 회귀는 회귀 계수를 0으로 만들지는 않는다.

 

라쏘 회귀

 

W의 절댓값에 패널티를 부여하는 L1 규제를 선형 회귀에 적용한 것이 라쏘(Lasso) 회귀이다.

L1 규제는 불필요한 회귀 계수를 급격하게 감소시켜 0으로 만들고 제거한다. 즉, L1 규제는 피처 선택의 특성을 가진다.

Lasso 클래스도 마찬가지고 주요 파라미터 alpha를 가지며, L1 규제 계수에 해당한다.

 

RMSE과정은 릿지 회귀와 비슷하니 생략하고, alpha 값에 따른 피처별 회귀 계수에 주목하자.

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

0 주목

NOX 속성은 alpha가 0.07일 때부터 회귀 계수가 0이며, alpha를 증가시키면서 특정 회귀 계수가 0으로 바뀐다.

회귀 계수가 0인 피처는 회귀 식에서 제외되면서 피처 선택의 효과를 얻을 수 있다.

 

엘라스틱넷 회귀

 

엘라스틱넷(Elastic Net) 회귀는 L2 규제와 L1 규제를 결합한 회귀이다.

라쏘 회귀가 서로 상관관계가 높은 피처들의 경우에 이들 중에서 중요 피처만을 선택하고 다른 피처들은 회귀 계수를 0으로 만든다. 여기에 회귀 계수 값의 급격한 변동을 막기 위해 L2 규제를 추가한다.

엘라스틱넷 회귀의 단점으로 수행시간이 상대적으로 오래걸린다.

 

사이킷런은 ElasticNet 클래스를 이용해서 구현하며. 주요 생성 파라미터는 alpha와 l1_ratio이다.

엘라스틱넷의 규제는 a*L1 + b*L2 이며, alpha 파라미터 값은 a+b이다.

또한 l1_ratio 파라미터 값은 a/(a+b)이다. 즉, l1_ratio가 0이면 a가 0이므로 L2 규제와 동일하며, 1이면 b가 0이므로 L1 규제와 동일하다.

 

ElasticNet 클래스를 이용해 alpha값의 변화와 RMSE, 피처의 회귀 계수를 살펴본다.

alpha값의 변화만 보기 위해 l1_ratio 값을 0.7로 고정한다.

# 엘라스틱넷에 사용될 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)
#######  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)

0주목

alpha값의 변화에 따라 0이되는 피처가 있으나. 라쏘 회귀보다는 상대적으로 그 수가 적음을 알 수 있다.

 

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

 

선형 회귀 모델은 피처값과 타깃값의 분포가 정규 분포(평균을 중심으로 종 모양으로 데이터 값이 분포된 형태) 형태를 선호한다.

 

피처 데이터 세트 변환 방법

  1. StandardScaler 클래스를 이용해 평균이 0, 분산이 1인 표준 정규 분포를 가진 데이터 세트로 변환하거나 MinMaxScaler 클래스를 이용해 최솟값이 0이고 최댓값이 1인 값으로 정규화를 수행한다.
  2. 스케일링/정규화를 수행한 데이터 세트에 다시 다항 특성을 적용하여 변환한다. 보통 1번 방법을 통해 예측 성능 향상이 없을 경우 적용하는 방법.
  3. 원래 값에 log 함수를 적용하면 정규 분포에 가까운 형태로 값이 분포된다. 실제로 선형 회귀에서는 이러한 로그 변환(Log Transformation)이 많이 사용된다.

타깃값의 경우는 일반적으로 로그 변환을 적용한다.

 

보스턴 주택가격 피처 데이터 세트에 위 3가지 방법을 차례로 적용한 후 경우멸 RMSE를 측정한다.

이를 위한 get_scaled_data() 함수를 생성한다.

method 인자로 변환 방법을 결정하며, p_degree는 다항식 특성을 추가할 때 다항식 차수가 입력된다. 

로그 변환의 경우 언더 플로우 발생을 방지하기 위해 np.log1p()를 적용했다.

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 추출. 
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)

책에서 정리해준 값

로그 변환에서 좋은 성능 향상이 있음을 알 수 있다.

 


07. 로지스틱 회귀

 

로지스틱 회귀는 선형 회귀를 분류에 적용한 알고리즘이다.

로지스틱 회귀는 학습을 통해 선형 함수의 회귀 처적선이 아닌 시그모이드(Sigmoid) 함수 최적선을 찾고 이 시그모이드 함수의 반환값을 확률로 간주해 확률에 따라 분류를 결정한다.

차이점

시그모이드 함수는 항상 0과 1사이의 값으 반환한다.

 

사이킷런은 로지스틱 회귀를 위한 LogisticRegression 클래스를 제공한다.

 

solver 파라미터의 최적화 방안

  • lbfgs : default값. 메모리 공간을 절약할 수 있고 CPU코어 수가 많다면 최적화를 병렬로 수행할 수 있다.
  • liblinear : 다차원이고 작은 데이터세트에 효과적으로 동작하지만 국소 최적화 이슈가 있고, 병렬로 최적화할 수 없다.
  • newton-cg : 좀 더 정교한 최적화를 가능하게 하지만, 대용량의 데이터에서 속도가 많이 느리다.
  • sag : 경사 하강법 기반의 최적화 적용. 대용량의 데이터에서 빠르게 최적화한다.
  • saga : sag와 유사하나 L1 정규화를 가능하게 한다.

일반적으로 lbfgs나 liblinear를 선택한다.

서로 다른 slover 값으로 LogisticRegression을 학습하고 성능 평가를 진행한다.

특정 solver는 최적화에 상대적으로 많은 반복 횟수가 필요할 수 있으므로 max_iter값을 600으로 설정한다.(최대 반복 회수)

solvers = ['lbfgs', 'liblinear', 'newton-cg', 'sag', 'saga']

# 여러개의 solver 값별로 LogisticRegression 학습 후 성능 평가
for solver in solvers:
    lr_clf = LogisticRegression(solver=solver, max_iter=600)
    lr_clf.fit(X_train, y_train)
    lr_preds = lr_clf.predict(X_test)
    
    # accuracy와 roc_auc 측정
    print('solver:{0}, accuracy: {1:.3f}, roc_auc:{2:.3f}'.format(solver,
                                                                  accuracy_score(y_test, lr_preds),
                                                                  roc_auc_score(y_test , lr_preds)))
solver:lbfgs, accuracy: 0.977, roc_auc:0.972
solver:liblinear, accuracy: 0.982, roc_auc:0.979
solver:newton-cg, accuracy: 0.977, roc_auc:0.972
solver:sag, accuracy: 0.982, roc_auc:0.979
solver:saga, accuracy: 0.982, roc_auc:0.979

 

이외에 주요 파라미터로 penalty와 C가 있다.

penalty는 규제의 유형을 설정하며 l2 혹은 l1으로 설정할 수 있다. default는 l2.

C는 규제 강도를 조절하는 alpha 값의 역수이다. 즉, C = 1/alpha이므로 C 값이 작을수록 규제 강도가 크다.

liblinear, saga는 L1,L2 규제가 모두 가능하지만 lbfgs,newton-cg,sag의 경우는 L2 규제만 가능하다.

 

GridSearchCV를 이용해 solver,penalty,C를 최적화한다.

from sklearn.model_selection import GridSearchCV

params={'solver':['liblinear', 'lbfgs'],
        'penalty':['l2', 'l1'],
        'C':[0.01, 0.1, 1, 1, 5, 10]}

lr_clf = LogisticRegression()

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_))
# FitFailedWarning 메시지가 왜 안나오징?
최적 하이퍼 파라미터:{'C': 0.1, 'penalty': 'l2', 'solver': 'liblinear'}, 최적 평균 정확도:0.979

로지스틱 회귀는 가볍고 빠르지만 이진 분류 예측 성능도 뛰어나 기본 모델로 많이 사용한다.

 


08. 회귀 트리

 

트리 기반의 회귀는 회귀 트리를 이용한다. 즉, 회귀를 위한 트리를 생성하고 이를 기반으로 회귀 예측을 한다.

회귀 트리는 리프 노드에 속한 데이터 값의 평균값을 구해 회귀 예측값을 계산한다.

결정 트리, 랜덤 포래스트, GBM, XGBoost, LightGBM 등의 분류에서 사용한 모든 트리 기반의 알고리즘은 회귀도 가능하다.

 

사이킷런에서는 회귀 수행을 할 수 있는 Estimator 클래스를 제공한다.

get_model_cv_prediction()함수를 만들어 입력 모델과 데이터 세트를 입력 받고 교차 검증으로 평균 RMSE를 계산한다.

다양한 유형의 회귀 트리를 생성하고 보스턴 주택 가격을 예측한다.

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.251 
#####  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)

 

회귀 트리 Regressor가 선형 회귀와 비교해 어떻게 예측값을 판단하는지 확인하기 위해 시각화를 실시한다.

결정 트리 하이퍼 파라미터인 max_depth의 크기를 변화시키면서 회귀 트리 예측선의 변화를 살펴본다.

레이블값 Price와 가장 밀접한 양의 상관관계를 가지는 RM 칼럼만 이용해 PRICE 예측 회귀선을 표현한다.

선형 회귀는 직선으로 예측 회귀선을 표현하는 데 반해, 회귀 트리의 경우 분할되는 데이터 지점에 따라 브랜치를 만들면서 계단 형태로 회귀선을 만든다.

max_depth=7의 경우 이상치 데이터도 학습하며 복잡한 회귀선으로 과적합이 되기 쉬운 모델이 되었다.

 

'Euron > 정리' 카테고리의 다른 글

[Week9] 06. 차원 축소  (1) 2023.11.02
[Week8] 05. 회귀 - 캐글 실습  (1) 2023.10.19
[Week5] 05. 회귀(1)  (1) 2023.10.07
[Week4] 04. 분류 - 캐글 실습  (0) 2023.09.29
[Week3] 04. 분류(2)  (0) 2023.09.21