머신러닝 실전 앙상블 (Ensemble)과 Hyperparameter 튜닝
Sep 28, 2020

머신러닝 알고리즘의 끝판왕인 앙상블(Ensemble) 알고리즘에 대하여 알아보도록 하겠습니다. 앙상블 알고리즘은 방법론 적인 측면에서 Voting, Bagging, Boosting 알고리즘등으로 나뉠 수 있겠고, 앙상블의 앙상블 알고리즘인 Stacking 그리고 Weighted Blending 등의 기법도 알아보도록 하겠습니다. 앙상블 알고리즘은 단일 알고리즘 대비 성능이 좋기 때문에, 캐글(Kaggle.com)과 같은 데이터 분석 대회에서도 상위권에 항상 꼽히는 알고리즘 들입니다.

데이터의 종류, 형태에 따라 어떤 알고리즘이 우세한지는 직접 적용해보고 성능 평가를 통해 알아볼 수는 있겠지만, 최근에는 LGBM 그리고 XGBoost와 같은 부스팅 계열의 알고리즘이 대회의 상위권을 휩쓸고 있습니다. 부스팅 계열의 앙상블 알고리즘이 정형 데이터에서 강세를 보이고 있습니다.

이 튜토리얼의 후반부에는 RandomSearchCVGridSearchCV라는 Hyperparameter 튜닝기도 다루고 있습니다. 물론, 시간과 리소스가 많이 투여되는 부부이기 때문에 최근에는 bayesian optimization을 기반으로 찾는 HyperOpt도 주목 받고 있습니다. 나중에 HyperOpt에 대한 내용도 다뤄보도록 하겠습니다.

튜토리얼은 보스톤 집 값 데이터를 토대로 집 값 예측을 해보는 튜토리얼이며, sklearn.dataset에 포함된 load_boston으로 진행합니다. 따라서, 별도의 파일 데이터는 다운로드 받을 필요 없습니다.

코드

Colab으로 열기 Colab으로 열기

GitHub GitHub에서 소스보기


앙상블(Ensemble)

머신러닝 앙상블이란 여러개의 머신러닝 모델을 이용해 최적의 답을 찾아내는 기법이다.

  • 여러 모델을 이용하여 데이터를 학습하고, 모든 모델의 예측결과를 평균하여 예측

앙상블 기법의 종류

  • 보팅 (Voting): 투표를 통해 결과 도출
  • 배깅 (Bagging): 샘플 중복 생성을 통해 결과 도출
  • 부스팅 (Boosting): 이전 오차를 보완하면서 가중치 부여
  • 스태킹 (Stacking): 여러 모델을 기반으로 예측된 결과를 통해 meta 모델이 다시 한번 예측

실습을 위한 데이터셋 로드

# 튜토리얼 진행을 위한 모듈 import
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from IPython.display import Image

np.set_printoptions(suppress=True, precision=3)
SEED = 30
from sklearn.datasets import load_boston

load_boston 데이터셋 로드

data = load_boston()

컬럼 소개

속성 수 : 13

  • CRIM: 범죄율
  • ZN: 25,000 평방 피트 당 주거용 토지의 비율
  • INDUS: 비소매(non-retail) 비즈니스 면적 비율
  • CHAS: 찰스 강 더미 변수 (통로가 하천을 향하면 1; 그렇지 않으면 0)
  • NOX: 산화 질소 농도 (천만 분의 1)
  • RM:주거 당 평균 객실 수
  • AGE: 1940 년 이전에 건축된 자가 소유 점유 비율
  • DIS: 5 개의 보스턴 고용 센터까지의 가중 거리
  • RAD: 고속도로 접근성 지수
  • TAX: 10,000 달러 당 전체 가치 재산 세율
  • PTRATIO 도시 별 학생-교사 비율
  • B: 1000 (Bk-0.63) ^ 2 여기서 Bk는 도시 별 검정 비율입니다.
  • LSTAT: 인구의 낮은 지위
  • MEDV: 자가 주택의 중앙값 (1,000 달러 단위)
df = pd.DataFrame(data['data'], columns=data['feature_names'])
df['target'] = data['target']
df.head()
CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT target
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

train / test 데이터를 분할 합니다.

from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(df.drop('target', 1), df['target'], random_state=SEED)
x_train.shape, x_test.shape
((379, 13), (127, 13))
x_train.head()
CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT
142 3.32105 0.0 19.58 1.0 0.871 5.403 100.0 1.3216 5.0 403.0 14.7 396.90 26.82
10 0.22489 12.5 7.87 0.0 0.524 6.377 94.3 6.3467 5.0 311.0 15.2 392.52 20.45
393 8.64476 0.0 18.10 0.0 0.693 6.193 92.6 1.7912 24.0 666.0 20.2 396.90 15.17
162 1.83377 0.0 19.58 1.0 0.605 7.802 98.2 2.0407 5.0 403.0 14.7 389.61 1.92
363 4.22239 0.0 18.10 1.0 0.770 5.803 89.0 1.9047 24.0 666.0 20.2 353.04 14.64
y_train.head()
142    13.4
10     15.0
393    13.8
162    50.0
363    16.8
Name: target, dtype: float64

모델별 성능 확인을 위한 함수

import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import mean_squared_error

my_predictions = {}
my_pred = None
my_actual = None
my_name = None

colors = ['r', 'c', 'm', 'y', 'k', 'khaki', 'teal', 'orchid', 'sandybrown',
          'greenyellow', 'dodgerblue', 'deepskyblue', 'rosybrown', 'firebrick',
          'deeppink', 'crimson', 'salmon', 'darkred', 'olivedrab', 'olive', 
          'forestgreen', 'royalblue', 'indigo', 'navy', 'mediumpurple', 'chocolate',
          'gold', 'darkorange', 'seagreen', 'turquoise', 'steelblue', 'slategray', 
          'peru', 'midnightblue', 'slateblue', 'dimgray', 'cadetblue', 'tomato'
         ]

def plot_predictions(name_, pred, actual):
    df = pd.DataFrame({'prediction': pred, 'actual': y_test})
    df = df.sort_values(by='actual').reset_index(drop=True)

    plt.figure(figsize=(11, 8))
    plt.scatter(df.index, df['prediction'], marker='x', color='r')
    plt.scatter(df.index, df['actual'], alpha=0.7, marker='o', color='black')
    plt.title(name_, fontsize=15)
    plt.legend(['prediction', 'actual'], fontsize=12)
    plt.show()

def mse_eval(name_, pred, actual):
    global my_predictions, colors, my_pred, my_actual, my_name
    
    my_name = name_
    my_pred = pred
    my_actual = actual

    plot_predictions(name_, pred, actual)

    mse = mean_squared_error(pred, actual)
    my_predictions[name_] = mse

    y_value = sorted(my_predictions.items(), key=lambda x: x[1], reverse=True)
    
    df = pd.DataFrame(y_value, columns=['model', 'mse'])
    print(df)
    min_ = df['mse'].min() - 10
    max_ = df['mse'].max() + 10
    
    length = len(df) / 2
    
    plt.figure(figsize=(9, length))
    ax = plt.subplot()
    ax.set_yticks(np.arange(len(df)))
    ax.set_yticklabels(df['model'], fontsize=12)
    bars = ax.barh(np.arange(len(df)), df['mse'], height=0.3)
    
    for i, v in enumerate(df['mse']):
        idx = np.random.choice(len(colors))
        bars[i].set_color(colors[idx])
        ax.text(v + 2, i, str(round(v, 3)), color='k', fontsize=12, fontweight='bold', verticalalignment='center')
        
    plt.title('MSE Error', fontsize=16)
    plt.xlim(min_, max_)
    
    plt.show()
    
def add_model(name_, pred, actual):
    global my_predictions, my_pred, my_actual, my_name
    my_name = name_
    my_pred = pred
    my_actual = actual
    
    mse = mean_squared_error(pred, actual)
    my_predictions[name_] = mse

def remove_model(name_):
    global my_predictions
    try:
        del my_predictions[name_]
    except KeyError:
        return False
    return True

def plot_all():
    global my_predictions, my_pred, my_actual, my_name
    
    plot_predictions(my_name, my_pred, my_actual)
    
    y_value = sorted(my_predictions.items(), key=lambda x: x[1], reverse=True)
    
    df = pd.DataFrame(y_value, columns=['model', 'mse'])
    print(df)
    min_ = df['mse'].min() - 10
    max_ = df['mse'].max() + 10
    
    length = len(df) / 2
    
    plt.figure(figsize=(9, length))
    ax = plt.subplot()
    ax.set_yticks(np.arange(len(df)))
    ax.set_yticklabels(df['model'], fontsize=12)
    bars = ax.barh(np.arange(len(df)), df['mse'], height=0.3)
    
    for i, v in enumerate(df['mse']):
        idx = np.random.choice(len(colors))
        bars[i].set_color(colors[idx])
        ax.text(v + 2, i, str(round(v, 3)), color='k', fontsize=12, fontweight='bold', verticalalignment='center')
        
    plt.title('MSE Error', fontsize=16)
    plt.xlim(min_, max_)
    
    plt.show()

단일 회귀예측 모델 (지난 시간)

from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Ridge
from sklearn.linear_model import Lasso
from sklearn.linear_model import ElasticNet
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures

LinearRegression

  • 기본 옵션 값을 사용하여 학습합니다.
linear_reg = LinearRegression()
linear_reg.fit(x_train, y_train)
pred = linear_reg.predict(x_test)
mse_eval('LinearRegression', pred, y_test)
              model        mse
0  LinearRegression  16.485165

Ridge

  • 규제 계수인 alpha=0.1을 적용합니다.
ridge = Ridge(alpha=0.1)
ridge.fit(x_train, y_train)
pred = ridge.predict(x_test)
mse_eval('Ridge(alpha=0.1)', pred, y_test)
              model        mse
0  LinearRegression  16.485165
1  Ridge(alpha=0.1)  16.479483

Lasso

  • 규제 계수인 alpha=0.01로 적용합니다.
lasso = Lasso(alpha=0.01)
lasso.fit(x_train, y_train)
pred = lasso.predict(x_test)
mse_eval('Lasso(alpha=0.01)', pred, y_test)
               model        mse
0   LinearRegression  16.485165
1   Ridge(alpha=0.1)  16.479483
2  Lasso(alpha=0.01)  16.441822

ElasticNet

  • 규제 계수인 alpha=0.01, l1_ratio=0.8을 적용합니다.
elasticnet = ElasticNet(alpha=0.01, l1_ratio=0.8)
elasticnet.fit(x_train, y_train)
pred = elasticnet.predict(x_test)
mse_eval('ElasticNet(alpha=0.1, l1_ratio=0.8)', pred, y_test)    
                                 model        mse
0  ElasticNet(alpha=0.1, l1_ratio=0.8)  16.638817
1                     LinearRegression  16.485165
2                     Ridge(alpha=0.1)  16.479483
3                    Lasso(alpha=0.01)  16.441822

Pipeline 학습

StandardScaler와 ElasticNet의 파이프라인 학습 합니다.

  • ElasticNet 모델은 규제 계수인 alpha=0.01, l1_ratio=0.8을 적용합니다.
elasticnet_pipeline = make_pipeline(
    StandardScaler(),
    ElasticNet(alpha=0.01, l1_ratio=0.8)
)
elasticnet_pipeline.fit(x_train, y_train)
elasticnet_pred = elasticnet_pipeline.predict(x_test)
mse_eval('Standard ElasticNet', elasticnet_pred, y_test)
                                 model        mse
0  ElasticNet(alpha=0.1, l1_ratio=0.8)  16.638817
1                     LinearRegression  16.485165
2                     Ridge(alpha=0.1)  16.479483
3                    Lasso(alpha=0.01)  16.441822
4                  Standard ElasticNet  16.423137

PolynomialFeatures

PolynomialFeatures와 ElasticNet의 파이프라인 학습을 진행합니다.

  • PolynomialFeatures는 degree=2, include_bias=False로 적용합니다.
  • ElasticNet 모델은 규제 계수인 alpha=0.1, l1_ratio=0.2을 적용합니다.
poly_pipeline = make_pipeline(
    PolynomialFeatures(degree=2, include_bias=False),
    ElasticNet(alpha=0.1, l1_ratio=0.2)
)
poly_pipeline.fit(x_train, y_train)
poly_pred = poly_pipeline.predict(x_test)
mse_eval('Poly ElasticNet', poly_pred, y_test)
/home/ubuntu/anaconda3/envs/tensorflow2_p36/lib/python3.6/site-packages/sklearn/linear_model/coordinate_descent.py:475: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 1748.27446730776, tolerance: 3.4587142691292874
  positive)
                                 model        mse
0  ElasticNet(alpha=0.1, l1_ratio=0.8)  16.638817
1                     LinearRegression  16.485165
2                     Ridge(alpha=0.1)  16.479483
3                    Lasso(alpha=0.01)  16.441822
4                  Standard ElasticNet  16.423137
5                      Poly ElasticNet  10.147479

앙상블 (Ensemble)

보팅 (Voting) - 회귀 (Regression)

Voting은 단어 뜻 그대로 투표를 통해 결정하는 방식입니다. Voting은 Bagging과 투표방식이라는 점에서 유사하지만, 다음과 같은 큰 차이점이 있습니다.

  • Voting은 다른 알고리즘 model을 조합해서 사용합니다.
  • Bagging은 같은 알고리즘 내에서 다른 sample 조합을 사용합니다.
from sklearn.ensemble import VotingRegressor, VotingClassifier

반드시, Tuple 형태로 모델을 정의해야 합니다.

single_models = [
    ('linear_reg', linear_reg), 
    ('ridge', ridge), 
    ('lasso', lasso), 
    ('elasticnet_pipeline', elasticnet_pipeline), 
    ('poly_pipeline', poly_pipeline)
]
voting_regressor = VotingRegressor(single_models, n_jobs=-1)
voting_regressor.fit(x_train, y_train)
VotingRegressor(estimators=[('linear_reg',
                             LinearRegression(copy_X=True, fit_intercept=True,
                                              n_jobs=None, normalize=False)),
                            ('ridge',
                             Ridge(alpha=0.1, copy_X=True, fit_intercept=True,
                                   max_iter=None, normalize=False,
                                   random_state=None, solver='auto',
                                   tol=0.001)),
                            ('lasso',
                             Lasso(alpha=0.01, copy_X=True, fit_intercept=True,
                                   max_iter=1000, normalize=False,
                                   positive=Fals...
                                      steps=[('polynomialfeatures',
                                              PolynomialFeatures(degree=2,
                                                                 include_bias=False,
                                                                 interaction_only=False,
                                                                 order='C')),
                                             ('elasticnet',
                                              ElasticNet(alpha=0.1, copy_X=True,
                                                         fit_intercept=True,
                                                         l1_ratio=0.2,
                                                         max_iter=1000,
                                                         normalize=False,
                                                         positive=False,
                                                         precompute=False,
                                                         random_state=None,
                                                         selection='cyclic',
                                                         tol=0.0001,
                                                         warm_start=False))],
                                      verbose=False))],
                n_jobs=-1, weights=None)
voting_pred = voting_regressor.predict(x_test)
mse_eval('Voting Ensemble', voting_pred, y_test)
                                 model        mse
0  ElasticNet(alpha=0.1, l1_ratio=0.8)  16.638817
1                     LinearRegression  16.485165
2                     Ridge(alpha=0.1)  16.479483
3                    Lasso(alpha=0.01)  16.441822
4                  Standard ElasticNet  16.423137
5                      Voting Ensemble  13.179718
6                      Poly ElasticNet  10.147479

보팅 (Voting) - 분류 (Classification)

분류기 모델을 만들때, Voting 앙상블은 1가지의 중요한 parameter가 있습니다.

voting = {'hard', 'soft'}

hard로 설정한 경우

class를 0, 1로 분류 예측을 하는 이진 분류를 예로 들어 보겠습니다.

Hard Voting 방식에서는 결과 값에 대한 다수 class를 차용합니다.

classification을 예로 들어 보자면, 분류를 예측한 값이 1, 0, 0, 1, 1 이었다고 가정한다면 1이 3표, 0이 2표를 받았기 때문에 Hard Voting 방식에서는 1이 최종 값으로 예측을 하게 됩니다.

soft

soft vote 방식은 각각의 확률의 평균 값을 계산한다음에 가장 확률이 높은 값으로 확정짓게 됩니다.

가령 class 0이 나올 확률이 (0.4, 0.9, 0.9, 0.4, 0.4)이었고, class 1이 나올 확률이 (0.6, 0.1, 0.1, 0.6, 0.6) 이었다면,

  • class 0이 나올 최종 확률은 (0.4+0.9+0.9+0.4+0.4) / 5 = 0.44,
  • class 1이 나올 최종 확률은 (0.6+0.1+0.1+0.6+0.6) / 5 = 0.4

가 되기 때문에 앞선 Hard Vote의 결과와는 다른 결과 값이 최종 으로 선출되게 됩니다.

from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression, RidgeClassifier
models = [
    ('Logi', LogisticRegression()), 
    ('ridge', RidgeClassifier())
]

voting 옵션에 대하여 지정합니다.

vc = VotingClassifier(models, voting='hard')

배깅(Bagging)

Bagging은 Bootstrap Aggregating의 줄임말입니다.

  • Bootstrap = Sample(샘플) + Aggregating = 합산

Bootstrap은 여러 개의 dataset을 중첩을 허용하게 하여 샘플링하여 분할하는 방식

데이터 셋의 구성이 [1, 2, 3, 4, 5 ]로 되어 있다면,

  1. group 1 = [1, 2, 3]
  2. group 2 = [1, 3, 4]
  3. group 3 = [2, 3, 5]
Image('https://teddylee777.github.io/images/2019-12-17/image-20191217015537872.png')

Voting VS Bagging

  • Voting은 여러 알고리즘의 조합에 대한 앙상블
  • Bagging은 하나의 단일 알고리즘에 대하여 여러 개의 샘플 조합으로 앙상블

대표적인 Bagging 앙상블

  1. RandomForest
  2. Bagging

RandomForest

  • DecisionTree(트리)기반 Bagging 앙상블
  • 굉장히 인기있는 앙상블 모델
  • 사용성이 쉽고, 성능도 우수함
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
rfr = RandomForestRegressor()
rfr.fit(x_train, y_train)
/home/ubuntu/anaconda3/envs/tensorflow2_p36/lib/python3.6/site-packages/sklearn/ensemble/forest.py:245: FutureWarning: The default value of n_estimators will change from 10 in version 0.20 to 100 in 0.22.
  "10 in version 0.20 to 100 in 0.22.", FutureWarning)
RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
                      max_features='auto', max_leaf_nodes=None,
                      min_impurity_decrease=0.0, min_impurity_split=None,
                      min_samples_leaf=1, min_samples_split=2,
                      min_weight_fraction_leaf=0.0, n_estimators=10,
                      n_jobs=None, oob_score=False, random_state=None,
                      verbose=0, warm_start=False)
rfr_pred = rfr.predict(x_test)
mse_eval('RandomForest Ensemble', rfr_pred, y_test)
                                 model        mse
0  ElasticNet(alpha=0.1, l1_ratio=0.8)  16.638817
1                     LinearRegression  16.485165
2                     Ridge(alpha=0.1)  16.479483
3                    Lasso(alpha=0.01)  16.441822
4                  Standard ElasticNet  16.423137
5                      Voting Ensemble  13.179718
6                      Poly ElasticNet  10.147479
7                RandomForest Ensemble   7.838730

주요 Hyperparameter

  • random_state: 랜덤 시드 고정 값. 고정해두고 튜닝할 것!
  • n_jobs: CPU 사용 갯수
  • max_depth: 깊어질 수 있는 최대 깊이. 과대적합 방지용
  • n_estimators: 앙상블하는 트리의 갯수
  • max_features: 최대로 사용할 feature의 갯수. 과대적합 방지용
  • min_samples_split: 트리가 분할할 때 최소 샘플의 갯수. default=2. 과대적합 방지용
Image('https://teddylee777.github.io/images/2020-01-09/decistion-tree.png', width=600)