Search
Duplicate

머신 러닝 교과서/ 다양한 모델을 결합한 앙상블 학습

앙상블 학습

앙상블 학습(ensemble learning)의 목표는 여러 분류기를 하나의 메타 분류기로 연결하여 개별 분류기보다 더 좋은 일반화 성능을 달성하는 것이다.
전문가 10명의 예측을 묶어 전문가 한 명보다 더 정확하고 안정된 예측을 만드는 것
과반수 투표(majority voting)는 가장 인기 있는 앙상블 방법으로 분류기의 과반수가 예측한 클래스 레이블을 선택하는 방법이다.
엄밀히 말해 과반수 투표란 용어는 이진 클래스 분류에 해당하지만 다중 클래스 문제에도 쉽게 일반화 가능하다.
이를 다수결 투표(plurality voting)라고 한다.
이때는 가장 많은 투표(최빈값(mode))를 받은 클래스 레이블을 선택하면 된다.
아래 그림은 과반수 투표와 다수결 투표의 개념을 나타낸 것이다.
먼저 훈련 세트를 사용하여 mm개의 다른 분류기 (C1,C2,...,Cm)(C_{1}, C_{2}, ... , C_{m})를 훈련시킨다.
앙상블 방법에 따라 결정 트리, 서포트 벡터 머신, 로지스틱 회귀 분류기와 같은 여러 알고리즘들으 ㄹ사용하여 구축할 수 있다.
또는 같은 분류 알고리즘을 사용하고 훈련 세트의 부분 집합(subset)을 달리하여 학습할 수도 있다.
유명한 앙상블 방법 중 하나는 서로 다른 결정 트리를 연결한 랜덤 포레스트(random forest)이다.
아래 그림은 과반수 투표를 사용한 일반적인 앙상블 방법이다.
과반수 투표나 다수결 투표로 클래스 레이블을 예측하려면 개별 분류기 CjC_{j}의 예측 레이블을 모아 가장 많은 표를 받은 레이블 y^\hat{y}를 선택한다.
y^=mode{C1(x),C2(x),...,Cm(x)}\hat{y} = mode \{ C_{1}(x), C_{2}(x), ... , C_{m}(x) \}
예컨대 class1=1class 1 = -1이고 class2=1class2 = 1인 이진 분류 작업에서 과반수 투표 예측은 다음과 같이 쓸 수 있다.
C(x)=sign[jmCj(x)]={1iCj(x)01elseC(x) = sign [\sum_{j}^{m} C_{j}(x)] = \begin{cases} 1 & \sum_{i} C_{j}(x) \geq 0 \\ -1 & else \end{cases}
앙상블 방법이 개별 분류기보다 성능이 뛰어난 이유를 설명하기 위해 간단한 조합 이론을 적용해 보겠다.
다음 예에서 이진 분류 작업에 대해 동일한 에러율 ε\varepsilon를 가진 nn개의 분류기를 가정해 보자.
또 모든 분류기는 독립적이고 발생하는 오차는 서로 상관관계가 없다고 가정한다.
이런 가정하에 이 분류기의 앙상블이 만드는 오차 확률을 이항 분포(binomial distribution)의 확률 질량 함수(probability mass function)로 표현할 수 있다.
P(yk)=knεk(nk)(1ε)nk=εensembleP(y \geq k) = \sum_{k}^{n} \varepsilon^{k} \left( \begin{array}{rr} n \\ k \end{array} \right) (1 - \varepsilon)^{n-k} = \varepsilon_{ensemble}
여기서 (nk)\left( \begin{array}{rr} n \\ k \end{array} \right)는 이항 계수(binomial coefficient)로 nn개의 원소에서 kk개를 뽑는 조합의 가짓수이다.
이 식은 앙상블 예측의 특릴 확률을 계산한다.
좀더 구체적인 예를 들어서 에러율이 0.25 (ε=0.25)(\varepsilon = 0.25)인 분류기 11개(n=11)(n = 11)로 구성된 앙상블의 에러율은 다음과 같다.
P(yk)=k=6110.25k(10.25)11k=0.034P(y \geq k) = \sum_{k = 6}^{11} 0.25^{k} (1 - 0.25)^{11-k} = 0.034
모든 가정을 만족한다면 앙상블의 에러율 0.034는 개별 분류기의 에러율 0.25 보다 훨씬 낮다.
만일 에러율이 0.5인 분류기가 짝수 개일 때 예측이 반반으로 나뉘면 에러로 취급된다.
이상적인 앙상블 분류기와 다양한 범위의 분류기를 가진 경우와 비교하기 위해 확률 질량 함수를 구현해 보겠다.
from scipy.special import comb import math def ensemble_error(n_classifier, error): k_start = int(math.ceil(n_classifier / 2.)) probs = [comb(n_classifier, k) * error**k * (1-error)**(n_classifier - k) for k in range(k_start, n_classifier + 1)] return sum(probs)
Python
ensemble_error 함수를 이용하여 에러가 0.0에서 1.0까지 걸쳐있을 때 앙상블의 에러율을 계산해서 그래프로 시각화 하면 다음과 같다.
그래프에서 보이듯이 앙상블의 에러 확률은 개별 분류기보다 항상 좋다. 다만 개별 분류기가 무작위 추측 (ε<0.5)(\varepsilon < 0.5) 보다 성능이 좋아야 한다.

다수결 투표를 사용한 분류 앙상블

간단한 다수결 투표 분류기 구현

이 절에서 구현할 알고리즘은 여러 가지 분류 모델의 신뢰도에 가중치를 부여하여 연결할 수 있다.
수학적으로 표현하면 가중치가 적용된 다수결 투표는 다음과 같이 쓸 수 있다.
y^=argmaxij=1mwjχA(Cj(x)=i)\hat{y} = \arg \max_{i} \sum_{j = 1}^{m} w_{j} \chi_{A} (C_{j}(x) = i)
여기서 wjw_{j}는 y^=argmaxij=1mwjχA(Cj(x)=i)\hat{y} = \arg \max_{i} \sum_{j = 1}^{m} w_{j} \chi_{A} (C_{j}(x) = i)에 연관된 가중치이다.
y^\hat{y}는 앙상블이 예측한 클래스 레이블이다.
χA\chi_{A}는 특성 함수 (characteristic function) [Cj(x)=iA][C_{j}(x) = i \in A] 이다.
AA는 고유한 클래스 레이블 집합이다.
가중치가 동일하면 이 식을 다음과 같이 간단히 쓸 수 있다.
y^=mode{C1(x),C2(x),...,Cm(x)}\hat{y} = mode \{ C_{1}(x), C_{2}(x), ..., C_{m}(x) \}
가중치 개념을 더 잘 이해하기 위해 예제를 살펴보겠다. 세 개의 분류기 Cj(J{0,1})C_{j} (J \in \{ 0, 1 \})가 있고 샘플 xx의 클래스 레이블을 예측해야 한다고 가정하자.
세 개의 분류기 중 두 개가 클래스 0을 예측하고 C3C_{3} 하나가 샘플을 클래스 1로 분류했다.
세 개의 예측 가중치가 동일하다면 다수결 투표는 이 샘플이 클래스 0에 속한다고 예측할 것이다.
C1(x)0,C2(x)0,C3(x)1C_{1}(x) \to 0, C_{2}(x) \to 0, C_{3}(x) \to 1
y^=mode{0,0,1}=0\hat{y} = mode \{ 0, 0, 1 \} = 0
만일 C3C_{3}에 가중치 0.6을 할당하고 C1C_{1}과 C2C_{2}에 각각 0.2를 부여하면 다음과 같다.
y^=argmaxi[0.2×i0+0.2×i0+0.6×ii]=1\hat{y} = \arg \max_{i} [0.2 \times i_{0} + 0.2 \times i_{0} + 0.6 \times i_{i} ] = 1
직관적으로 생각해 봤을 때 C3C_{3}의 가중치가 C1,C2C_{1}, C_{2}보다 3배 더 크기 때문에 다음과 같이 쓸 수 있다.
y^=mode{0,0,1,1,1}=1\hat{y} = mode \{ 0, 0, 1, 1, 1 \} = 1
argmax와 bincount 함수를 사용하여 가중치가 적용된 다수결 투표를 파이썬 코드로 구현할 수 있다.
import numpy as np np.argmax(np.bincount([0,0,1], weights=[0.2, 0.2, 0.6]))
Python
사이킷런의 일부 분류기는 predict_proba 메서드에서 예측 클래스 레이블의 확률을 반환할 수 있다.
앙상블의 분류기가 잘 보정(calibration) 되어 있다면 다수결 투표에서 클래스 레이블 대신 예측 클래스 확률을 사용하는 것이 좋다.
확률을 사용하여 클래스 레이블을 예측하는 다수결 투표 버전은 다음과 같이 쓸 수 있다.
y^=argmaxij=1mwjpij\hat{y} = \arg \max_{i} \sum_{j=1}^{m} w_{j} p_{ij}
여기서 PijP_{ij}는 클래스 레이블 ii에 대한 jj번째 분류기의 예측 확률이다.
앞선 예제에 이어 클래스 레이블 i{0,1}i \in \{0, 1\}인 이진 분류 문제에서 세 개의 분류기로 구성된 앙상블 Cj(i{1,2,3})C_{j} (i \in \{ 1, 2, 3 \})을 가정해 보자.
어떤 샘플 xx에 대한 분류기 CjC_{j}는 다음과 같은 클래스 소속 확률을 반환한다.
C1(x)[0.9,0.1],C2(x)[0.8,0.2],C3(x)[0.4,0.6]C_{1}(x) \to [0.9, 0.1], C_{2}(x) \to [0.8, 0.2], C_{3}(x) \to [0.4, 0.6]
각 클래스 확률을 다음과 같이 계산할 수 있다.
p(i0x)=0.2×0.9+0.2×0.8+0.6×0.4=0.58p(i_{0}|x) = 0.2 \times 0.9 + 0.2 \times 0.8 + 0.6 \times 0.4 = 0.58
p(i1x)=0.2×0.1+0.2×0.2+0.6×0.6=0.42p(i_{1}|x) = 0.2 \times 0.1 + 0.2 \times 0.2 + 0.6 \times 0.6 = 0.42
y^=argmaxi[p(i0x),p(i1x)]\hat{y} = arg \max_{i} [p(i_{0}|x), p(i_{1}|x)]
넘파이의 average와 argmax 함수를 사용하여 클래스 확률 기반으로 가중치가 적용된 다수결 투표를 구현할 수 있다.
ex = np.array([0.9, 0.1], [0.8, 0.2], [0.4, 0.6]) p = np.average(ex, axis=0, weights=[0.2, 0.2, 0.6]) np.argmax(p)
Python
위 내용을 합하여 다수결 투표 앙상블 분류기를 구성하면 다음과 같다.
from sklearn.base import BaseEstimator from sklearn.base import ClassifierMixin from sklearn.preprocessing import LabelEncoder from sklearn.externals import six from sklearn.base import clone from sklearn.pipeline import _name_estimators import numpy as np import operator class MajorityVoteClassifier(BaseEstimator, ClassifierMixin): """다수결 투표 앙상블 분류기 매개변수 ---------- classifiers: 배열 타입, 크기 = [n_classifiers] 앙상블에 사용할 분류기 vote : str, {'classlabel', 'probability'} 기본값: 'classlabel' 'classlabel'이면 예측은 다수인 클래스 레이블의 인덱스가 된다. 'probability'면 확률 합이 가장 큰 인덱스로 클래스 레이블을 예측한다. (보정된 분류기에 추천한다) weights : 배열 타입, 크기 = [n_classifiers] 선택 사항, 기본값: None 'int' 또는 'float' 값의 리스트가 주어지면 분류기가 이 중요도로 가중치 되다. 'weights=None'이면 동일하게 취급한다. """ def __init__(self, classifiers, vote='classlabel', weights=None): self.classifiers = classifiers self.named_classifiers = {key:value for key, value in _name_estimators(classifiers)} self.vote = vote self.weights = weights def fit(self, X, y): """분류기를 학습한다 매개변수 ----------------- X : {배열 타입, 희소 행렬}, 크기 = [n_samples, n_features] 훈련 샘플 행렬 y : 배열 타입, 크기 = [n_samples] 타깃 클래스 레이블 벡터 반환값 ------------------ self : 객체 """ self.labelenc_ = LabelEncoder() self.labelenc_.fit(y) self.classes_ = self.labelenc_.classes_ self.classifiers_ = [] for clf in self.classifiers: fitted_clf = clone(clf).fit(X, self.labelenc_.transform(y)) self.classifiers_.append(fitted_clf) return self def predict(self, X): """X에 대한 클래스 레이블을 예측한다. 매개변수 ----------------- X : {배열 타입, 희소 행렬}, 크기 = [n_samples, n_features] 훈련 샘플 행렬 반환값 ------------------ maj_vote : 배열 타입, 크기 = [n_samples] 예측된 클래스 레이블 """ if self.vote == 'probability': maj_vote = np.argmax(self.predict_proba(X), axis=1) else: predictions = np.asarray([clf.predict(X) for clf in self.classifiers_]).T maj_vote = np.apply_along_axis(lambda x: np.argmax(np.bincount(x, weights=self.weights)), axis=1, arr=predictions) maj_vote = self.labelenc_.inverse_transform(maj_vote) return maj_vote def predict_proba(self, X): """X에 대한 클래스 확률을 예측한다. 매개변수 ----------------- X : {배열 타입, 희소 행렬}, 크기 = [n_samples, n_features] 훈련 샘플 행렬 반환값 ------------------ avg_proba : 배열 타입, 크기 = [n_samples, n_classes] 샘플마다 가중치가 적용된 클래스의 평균 확률 """ probas = np.asarray([clf.predict_proba(X) for clf in self.classifiers_]) avg_proba = np.average(probas, axis=0, weights=self.weights) return avg_proba def get_params(self, deep=True): """GridSearch를 위해 분류기의 매개변수 이름을 반환한다""" if not deep: return super(MajorityVoteClassifier, self).get_params(deep=False) else: out = self.named_classifiers.copy() for name, step in six.iteritems(self.named_classifiers): for key, value in six.iteritems(step.get_params(deep=True)): out['%s_%s' % (name, key)] = value return out
Python

다수결 투표 방식을 사용하여 예측 만들기

앞서 구현한 MajoritiVoteClassifier 클래스를 사용해 보겠다.
데이터는 붓꽃 데이터셋을 이용하였고
분류기는 로지스틱 회귀/ 결정트리/ k-최근접 이웃 분류기를 사용하였다.
from sklearn import datasets from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import cross_val_score from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.pipeline import Pipeline import numpy as np iris = datasets.load_iris() X, y = iris.data[50:, [1, 2]], iris.target[50:] le = LabelEncoder() y = le.fit_transform(y) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=1, stratify=y) clf1 = LogisticRegression(solver='liblinear', penalty='l2', C=0.001, random_state=1) clf2 = DecisionTreeClassifier(max_depth=1, criterion='entropy', random_state=0) clf3 = KNeighborsClassifier(n_neighbors=1, p=2, metric='minkowski') pipe1 = Pipeline([['sc', StandardScaler()], ['clf', clf1]]) pipe3 = Pipeline([['sc', StandardScaler()], ['clf', clf3]]) clf_labels = ['Logistic regression', 'Decision tree', 'KNN'] print('10-겹 교차 검증:\n') for clf, label in zip([pipe1, clf2, pipe3], clf_labels): scores = cross_val_score(estimator=clf, X=X_train, y=y_train, cv=10, scoring='roc_auc') print("ROC AUC: %0.2f (+/- %0.2f) [%s]" % (scores.mean(), scores.std(), label)) mv_clf = MajorityVoteClassifier(classifiers=[pipe1, clf2, pipe3]) clf_labels += ['Majority voting'] all_clf = [pipe1, clf2, pipe3, mv_clf] print('MajorityVoteClassifier:\n') for clf, label in zip(all_clf, clf_labels): scores = cross_val_score(estimator=clf, X=X_train, y=y_train, cv=10, scoring='roc_auc') print("ROC AUC: %0.2f (+/- %0.2f) [%s]" % (scores.mean(), scores.std(), label))
Python
위 코드를 실행해 보면 알 수 있지만, 10-겹 교차 검증으로 평가했을 때 보다 MajorityVoteClassifier의 성능이 뛰어나다.

앙상블 분류기의 평가와 튜닝

이번에는 본 적 없는 데이터에 대한 MajorityVoteClassifier의 일반화 성능을 확인하기 위해 ROC 곡선을 그려보겠다.
from sklearn.metrics import roc_curve from sklearn.metrics import auc import matplotlib.pyplot as plt colors = ['black', 'orange', 'blue', 'green'] linestyle = [':', '--', '-.', '-'] for clf, label, clr, ls in zip(all_clf, clf_labels, colors, linestyle): y_pred = clf.fit(X_train, y_train).predict_proba(X_test)[:, 1] fpr, tpr, thresholds = roc_curve(y_true=y_test, y_score=y_pred) roc_auc = auc(x=fpr, y=tpr) plt.plot(fpr, tpr, color=clr, linestyle=ls, label='%s (auc = %0.2f)' % (label, roc_auc)) plt.legend(loc='lower right') plt.plot([0, 1], [0, 1], linestyle='--', color='gray', linewidth=2) plt.xlim([-0.1, 1.1]) plt.ylim([-0.1, 1.1]) plt.grid(alpha=0.5) plt.xlabel('False positive rate (FPR)') plt.ylabel('True positive rate (TPR)') plt.show()
Python
ROC 곡선에서 보듯이 앙상블 분류기는 테스트 세트에서도 좋은 성능을 낸다.
앙상블의 결정 경계를 그려보면 다음과 같다.
from itertools import product sc = StandardScaler() X_train_std = sc.fit_transform(X_train) x_min = X_train_std[:, 0].min() - 1 x_max = X_train_std[:, 0].max() + 1 y_min = X_train_std[:, 1].min() - 1 y_max = X_train_std[:, 1].max() + 1 xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1), np.arange(y_min, y_max, 0.1)) f, axarr = plt.subplots(nrows=2, ncols=2, sharex='col', sharey='row', figsize=(7,5)) for idx, clf, tt in zip(product([0,1], [0,1]), all_clf, clf_labels): clf.fit(X_train_std, y_train) Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) axarr[idx[0], idx[1]].contourf(xx, yy, Z, alpha=0.3) axarr[idx[0], idx[1]].scatter(X_train_std[y_train==0, 0], X_train_std[y_train==0, 1], c='blue', marker='^', s=50) axarr[idx[0], idx[1]].scatter(X_train_std[y_train==1, 0], X_train_std[y_train==1, 1], c='green', marker='o', s=50) axarr[idx[0], idx[1]].set_title(tt) plt.text(-3.5, -4.5, s='Sepal width [standardized]', ha='center', va='center', fontsize=12) plt.text(-10.5, 4.5, s='Petal length [standardized]', ha='center', va='center', fontsize=12, rotation=90) plt.show()
Python

배깅: 부트스트랩 샘플링을 통한 분류 앙상블

배깅은 MajorityVoteClassfier와 매우 밀접한 앙상블 학습기법으로 앙상블에 있는 개별 분류기를 동일한 훈련 세트로 학습하는 것이 아니라 원본 훈련 세트에서 부트스트랩(bootstrap) 샘플(중복을 허용한 랜덤 샘플)을 뽑아서 사용한다.
배깅을 bootstrap aggregating이라고도 한다.

배깅 알고리즘의 작동 방식

배깅 분류기의 부트스트랩 샘플링의 작동 방식을 이해하기 위해 예를 들어보자.
아래 그림과 같이 7개의 훈련 샘플이 있을 떄, 배깅 단계 마다 중복을 허용하여 랜덤하게 샘플링된다.
각각의 부트스트랩 샘플을 사용하여 분류기 CjC_{j} 를 학습한다. 일반적으로 가지치기 하지 않는 결정 트리를 분류기로 사용한다.
아래 그림과 같이 각 분류기는 훈련 세트에서 추출한 랜덤한 부분 집합을 사용한다. 중복을 허용한 샘플링을 하기 때문에 각 부분 집합에는 일부가 중복되어 있고, 원본 샘플 중 일부는 포함되어 있지 않다.
개별 분류기가 부트스트랩 샘플에 학습되고 나면 다수결 투표를 사용하여 예측을 모은다.

배깅으로 Wine 데이터셋의 샘플 분류

배깅을 적용하기 위해 Wine 데이터셋으로 좀 더 복잡한 분류 문제를 만들어보자
import pandas as pd from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split from sklearn.ensemble import BaggingClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score df_wine = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data', header=None) df_wine.columns = ['Class label', 'Alcohol', 'Malic acid', 'Ash', 'Alcalinity of ash', 'Magnesium', 'Total phenols', 'Flavanoids', 'Nonflavanoid phenols', 'Proanthocyanins', 'Color intensity', 'Hue', 'ODD280/OD315 of diluted wines', 'Proline'] df_wine = df_wine[df_wine['Class label'] != 1] y = df_wine['Class label'].values X = df_wine[['Alcohol', 'ODD280/OD315 of diluted wines']].values le = LabelEncoder() y = le.fit_transform(y) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1, stratify=y) tree = DecisionTreeClassifier(criterion='entropy', random_state=1, max_depth=None) bag = BaggingClassifier(base_estimator=tree, n_estimators=500, max_samples=1.0, max_features=1.0, bootstrap=True, bootstrap_features=False, n_jobs=1, random_state=1) tree = tree.fit(X_train, y_train) y_train_pred = tree.predict(X_train) y_test_pred = tree.predict(X_test) tree_train = accuracy_score(y_train, y_train_pred) tree_test = accuracy_score(y_test, y_test_pred) print('결정 트리의 훈련 정확도/테스트 정확도 %.3f/%.3f' % (tree_train, tree_test)) bag = bag.fit(X_train, y_train) y_train_pred = bag.predict(X_train) y_test_pred = bag.predict(X_test) bag_train = accuracy_score(y_train, y_train_pred) bag_test = accuracy_score(y_test, y_test_pred) print('배깅의 훈련 정화도/테스트 정확도 %.3f/%.3f' % (bag_train, bag_test))
Python
위 코드를 실행해 보면 결정 트리와 배깅의 훈련 정확도가 훈련 세트에서 비슷하지만 테스트 세트의 정확도를 보면 배깅 분류기의 일반화 성능이 더 낫다는 것을 알 수 있다.
결정 트리와 배깅 분류기의 결정 경계를 비교해 보면 다음과 같다.
x_min = X_train[:, 0].min() - 1 x_max = X_train[:, 0].max() + 1 y_min = X_train[:, 1].min() - 1 y_max = X_train[:, 1].max() + 1 xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1), np.arange(y_min, y_max, 0.1)) f, axarr = plt.subplots(nrows=1, ncols=2, sharex='col', sharey='row', figsize=(8,3)) for idx, clf, tt in zip([0, 1], [tree, bag], ['Decision tree', 'Bagging']): clf.fit(X_train, y_train) Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) axarr[idx].contourf(xx, yy, Z, alpha=0.3) axarr[idx].scatter(X_train[y_train==0, 0], X_train[y_train==0, 1], c='blue', marker='^') axarr[idx].scatter(X_train[y_train==1, 0], X_train[y_train==1, 1], c='green', marker='o') axarr[idx].set_title(tt) axarr[0].set_ylabel('Alcohol', fontsize=12) plt.text(10.2, -1.2, s='ODD280/OD315 of diluted wines', ha='center', va='center', fontsize=12) plt.tight_layout() plt.show()
Python

약한 학습기를 이용한 에이다부스트

앙상블 메서드에 관한 마지막 절로 부스팅(boosting)을 설명해 보겠다. 특히 가장 유명한 부스팅 구현인 에이다부스트(AdaBoost, Adaptive Boosting)에 초점을 맞추겠다.
부스팅에서 앙상블은 약한 학습기(weak learner)라고도 하는 매우 간단한 분류기로 구성된다. 이 분류기는 랜덤 추측보다 조금 성능이 좋을 뿐이다.
약한 학습기의 전형적인 예는 깊이가 1인 결정트리이다.
부스팅의 핵심 아이디어는 분류하기 어려운 훈련 샘플에 초점을 맞추는 것이다. 즉 잘못 분류된 훈련 샘픙르 그다음 약한 학습기가 학습하여 앙상블 성능을 향상시킨다.

부스팅 작동 원리

배깅과 달리 부스팅의 초창기 방법은 중복을 허용하지 않고 훈련 세트에서 랜덤 샘플을 추출하여 부분집합을 구성한다. 원본 부스팅 과정은 다음 4개의 단계로 요약할 수 있다.
1.
훈련 세트 DD에서 중복을 허용하지 않고 랜덤한 부분집합 did_{i}을 뽑아 약한 학습기 C1C_{1}을 훈련한다.
2.
훈련 세트에서 중복을 허용하지 않고 두 번째 랜덤한 훈련 부분 집합 d2d_{2}를 뽑고 이전에 잘못 분류된 샘플의 50를 더해 약한 학습기 C2C_{2}를 훈련한다.
3.
훈련 세트 DD에서 C1C_{1}과 C2C_{2}에서 잘못 분류한 훈련 샘플 d3d_{3}를 찾아 세 번째 약한 학습기인 C3C_{3}를 훈련한다.
4.
약한 학습기 C1,C2,C3C_{1}, C_{2}, C_{3}를 다수결 투표로 연결한다.
레오 브레이만이 언급한 것처럼 부스팅은 배깅 모델에 비해 분산은 물론 편향도 감소시킬 수 있다.
실제로는 에이다부스트 같은 부스팅 알고리즘이 분산이 높다고 알려져 있다. 즉, 훈련 데이터에 과대적합되는 경향이 있다.
여기서 언급한 원본 부스팅 방법과는 다르게 에이다부스트는 약한 학습기를 훈련할 때 훈련 세트 전체를 사용한다.
훈련 샘플은 반복마다 가중치가 다시 부여되며 이 앙상블은 이전 학습기의 실수를 학습하는 강력한 분류기를 만든다.
에이다부스트 알고리즘을 구체적으로 깊게 알아보기 전에 아래 그림으로 에이다부스트의 기본 개념을 좀 더 이해해 보자.
위 그림의 (a)는 이진 분류를 위한 훈련 세트를 보여준다. 여기서 모든 샘플은 동일한 가중치를 가진다.
이 훈련 세트를 바탕으로 깊이가 1인 결정 트리(파선)을 훈련하여 샘플을 두 개의 클래스(삼각형과 원)로 나눈다. 물론 가능한 비용 함수(또는 결정 트리 앙상블일 경우 불순도 점수)를 최소화하는 트리를 훈련한다.
다음 단계 (b)에서 이전에 잘못 분류된 샘플 두 개에 큰 가중치를 부여하고 옳게 분류된 샘플의 가중치는 낮춘다.
다음 결정 트리는 가장 큰 가중치를 가진 훈련 샘플에 더 집중할 것이다. 아마도 이런 훈련 샘플은 분류하기 어려운 샘플일 것이다.
그림 (b)에 있는 약한 학습기는 세 개의 원 모양 샘플을 잘못 분류한다. (c)에서 이 샘플들에 큰 가중치가 부여된다.
이 에이다부스트 앙상블이 세 번의 부스팅 단계만을 가진다고 가정하면 (d)에서처럼 서로 다른 가중치가 부여된 훈련 세트에서 훈련된 세 개의 약한 학습기를 다수결 투표 방식으로 합친다.
의사 코드를 이용하여 이에 대한 알고리즘을 살펴보자.
1.
가중치 벡터 ww를 동일한 가중치로 설정한다. iwi=1\sum_{i} w_{i} = 1
2.
mm번 부스팅 반복의 jj번째에서 다음을 수행한다.
a.
가중치가 부여된 약한 학습기를 훈련한다. Cj=train(X,y,w)C_{j} = train (X, y, w)
b.
클래스 레이블을 예측한다. y^=predict(Cj,X)\hat{y} = predict(C_{j}, X)
c.
가중치가 적용된 에러율을 계산한다. εw(y^y)\varepsilon w \cdot (\hat{y} \neq y)
d.
학습기 가중치를 계산한다. αj=0.5log1εε\alpha_{j} = 0.5 \log {1 - \varepsilon \over \varepsilon}
e.
가중치를 업데이트 한다. w:=w×exp(αj×y^×y)w := w \times exp(-\alpha_{j} \times \hat{y} \times y)
f.
합이 1이 되도록 가중치를 정규화 한다. w:=wiwiw := {w \over \sum_{i} w_{i}}
3.
최종예측을 계산한다. y^=(j=1m(αj×predict(Cj,X))>0)\hat{y} = (\sum_{j=1}^{m}(\alpha_{j} \times predict(C_{j}, X)) > 0)
2-3 단계에서 y^y\hat{y} \neq y 표현은 1 또는 0으로 구성된 이진 벡터를 의미한다. 예측이 잘못되면 1이고 그렇지 않으면 0이다.
아래 그림과 같이 10개의 훈련 샘플로 구성된 훈련 세트를 사용하여 구체적인 예를 살펴보자.
위 표의 첫 번째 열은 1에서 10까지 훈련 샘플의 인덱스를 나타낸다.
두 번째 열은 각 샘플의 특성 값으로 1차원 데이터셋이다.
세 번째 열은 각 훈련 샘플 xix_{i}에 대한 진짜 클래스 레이블 yiy_{i}이다. 여기서 yi{1,1}y_{i} \in \{1, -1\}이다.
네 번째 열은 초기 가중치를 보여준다. 초기에 가중치를 동일하게 초기화한다. (같은 상수값을 할당한다) 그 다음 합이 1이 되도록 정규화한다. 샘플이 열 개인 훈련 세트에서는 가중치 벡터 ww에 있는 각 가중치 wiw_{i}를 0.1로 할당한다.
다섯 번째 열에는 예측 클래스 레이블(y^)(\hat{y})이 나와 있다. 분할 기준은 x3.0x \leq 3.0라고 가정한다.
표의 마지막 열은 의사코드에서 정의한 업데이트 규칙에 의해 업데이트된 가중치를 보여준다.
가중치 계산 공식이 복잡해 보이므로 단계별 계산을 수행해 보겠다. 단계 2-3에 나와 있는 가중치된 에러율 ε\varepsilon을 계산해 보자.
ε=0.1×0+0.1×0+0.1×0+0.1×0+0.1×0+0.1×0+0.1×1+0.1×1+0.1×1+0.1×0=310=0.3\varepsilon = 0.1 \times 0 + 0.1 \times 0 + 0.1 \times 0 + 0.1 \times 0 + 0.1 \times 0 + 0.1 \times 0 + 0.1 \times 1 + 0.1 \times 1 + 0.1 \times 1 + 0.1 \times 0 = {3 \over 10} = 0.3
다음으로 2-4에 나오는 학습기 가중치 $latex \alpha_{j} &s=2$를 계산한다. 나중에 3-5에서 가중치를 업데이트할 때와 단계 3에서 다수결 투표 예측을 위한 가중치에 사용된다.
αj=0.5log(1εε)0.424\alpha_{j} = 0.5 \log ({1 - \varepsilon \over \varepsilon}) \approx 0.424
학습기 가중치 αj\alpha_{j}를 계산한 후 다음 식을 사용하여 가중치 벡터를 업데이트 할 수 있다.
w:=w×exp(αj×y^×y)w := w \times \exp(-\alpha_{j} \times \hat{y} \times y)
여기서 y^×y\hat{y} \times y는 예측 클래스 레이블 벡터와 진짜 클레스 레이블 벡터 사이의 원소별 곱셈이다.
예측 y^i\hat{y}_{i}가 맞으면 y^i×yi\hat{y}_{i} \times y_{i}는 양의 값이 되고 αi\alpha_{i}도 양이기 때문에 ii번째 가중치가 감소한다.
0.1×exp(0.424×1×1)0.0650.1 \times \exp (-0.424 \times 1 \times 1) \approx 0.065
비슷하게 예측 레이블 y^i\hat{y}_{i}가 맞지 않으면 ii번째 가중치가 다음과 같이 증가한다.
0.1×exp(0.424×1×(1))0.1530.1 \times \exp (-0.424 \times 1 \times (-1)) \approx 0.153
또는 다음과 같다.
0.1×exp(0.424×(1)×1)0.1530.1 \times \exp (-0.424 \times (-1) \times 1) \approx 0.153
가중치 벡터의 각 가중치를 업데이트하고 난 후 가중치의 합이 1이 되도록 정규화 한다. (2-5 단계)
w:=wiwiw := {w \over \sum_{i} w_{i}}
여기서 iwi=7×0.065+3×0.153=0.914\sum_{i} w_{i} = 7 \times 0.065 + 3 \times 0.153 = 0.914이다.
옳게 분류된 샘플에 대응하는 가중치는 다음 부스팅 단계에서 초깃값 0.1보다 0.065/0.9140.0710.065 / 0.914 \approx 0.071로 감소된다.
비슷하게 잘못 분류된 샘플의 가중치는 0.1에서 0.153/0.9140.1670.153 / 0.914 \approx 0.167로 증가한다.

사이킷런에서 에이다부스트 사용

앞선 알고리즘을 바탕으로 에이다부스트 앙상블 분류기를 작성하고 훈련시켜 보자.
from sklearn.ensemble import AdaBoostClassifier tree = DecisionTreeClassifier(criterion='entropy', random_state=1, max_depth=1) ada = AdaBoostClassifier(base_estimator=tree, n_estimators=500, learning_rate=0.1, random_state=1) tree = tree.fit(X_train, y_train) y_train_pred = tree.predict(X_train) y_test_pred = tree.predict(X_test) tree_train = accuracy_score(y_train, y_train_pred) tree_test = accuracy_score(y_test, y_test_pred) print('결정 트리의 훈련 정확도/테스트 정확도 %.3f/%.3f' % (tree_train, tree_test)) ada = ada.fit(X_train, y_train) y_train_pred = ada.predict(X_train) y_test_pred = ada.predict(X_test) ada_train = accuracy_score(y_train, y_train_pred) ada_test = accuracy_score(y_test, y_test_pred) print('에이다부스트의 훈련 정확도/테스트 정확도 %.3f/%.3f' % (ada_train, ada_test))
Python
위 코드를 실행해 보면 에이다부스트 모델이 훈련 세트의 모든 클래스 레이블을 정확하게 예측하고 깊이가 1인 결정 트리에 비해 테스트 세트의 성능도 더 높다.
훈련 성능과 테스트 성능 사이에 간격이 크ㅡㅁ로 모델의 편향을 줄임으로써 분산이 발생했다.
결정 영역의 모습도 확인해 보자
x_min = X_train[:, 0].min() - 1 x_max = X_train[:, 0].max() + 1 y_min = X_train[:, 1].min() - 1 y_max = X_train[:, 1].max() + 1 xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1), np.arange(y_min, y_max, 0.1)) f, axarr = plt.subplots(nrows=1, ncols=2, sharex='col', sharey='row', figsize=(8,3)) for idx, clf, tt in zip([0, 1], [tree, ada], ['Decision tree', 'AdaBoost']): clf.fit(X_train, y_train) Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) axarr[idx].contourf(xx, yy, Z, alpha=0.3) axarr[idx].scatter(X_train[y_train==0, 0], X_train[y_train==0, 1], c='blue', marker='^') axarr[idx].scatter(X_train[y_train==1, 0], X_train[y_train==1, 1], c='red', marker='o') axarr[idx].set_title(tt) axarr[0].set_ylabel('Alcohol', fontsize=12) plt.text(10.2, -0.5, s='ODD280/OD315 of diluted wines', ha='center', va='center', fontsize=12) plt.tight_layout() plt.show()
Python
결정 영역을 그려보면 에이다부스트 모델의 결정 경계가 깊이가 1인 결정 트리의 결정 경계보다 확실히 더 복잡하다.
또 에이다부스트 모델이 이전 절에서 훈련한 배깅 분류기와 매우 비슷하게 특성 공간을 분할하고 있다.
앙상블 기법은 개별 분류기에 비해 계산 복잡도가 높기 때문에 실전에서는 비교적 많지 않은 예측 성능 향상을 위해 계산 비용에 더 투자할 것인지 주의 깊게 생각해야 한다.
이 트레이드오프(trade-off)의 예로 자주 인용되는 것은 앙상블 기법을 사용하여 100만 달러 상금이 걸린 넷플릭스 대회 (Netflix Prize)에서 우승한 팀이 상금은 받았지만 복잡도가 높아 실제 애플리케이션에 적용하기 어려워 넷플릭스에서는 이 모델을 구현하지 않았다는 것이다.
“새로운 방법들을 오프라인에서 평가했지만, 추가로 얻을 수 있는 정확도 향상이 운영 시스템에 적용하기 위해 필요한 노력만큼 가치가 있어 보이지 않았습니다”