Ensemble Learning - Boosting, Stacking

Boosting

  • 앞에서 언급했던 Bagging이나 Random Forests는 부트스트랩 방식으로 데이터를 뽑긴해도 각 모델에 대해 독립적이라고 가정하지만, Boosting은 resampling을 할 때 오분류된 데이터에 더 가중치를 주어서 오분류된 데이터가 뽑힐 확률이 높도록 하여 복원 추출을 하고 다시 학습하기 때문에 모델들이 Sequential한 것이다.

Boosting 개념

  • 부스트(boost) 방법은 미리 정해진 갯수의 모형 집합을 사용하는 것이 아니라 하나의 모형에서 시작하여 모형 집합에 포함할 개별 모형을 하나씩 추가한다. 모형의 집합은 위원회(commit) $C$라고 하고 $m$개의 모형을 포함하는 위원회를 $C_{m}$으로 표시한다. 위원회에 들어가는 개별 모형을 약 분류기(weak classifier)라고 하며 $k$로 표시한다.
  • 부스트 방법의 특징은 한번에 하나씩 모형을 추가한다는 것이다.
  • 그리고 m번째로 위원회에 추가할 개별 모형 $k_{m}$의 선택 기준은 그 전단계의 위원회 $C_{m-1}$의 성능을 보완하는 것이다. 위원회 $C_{m}$의 최종 결정은 다수결 방법을 사용하지 않고 각각의 개별 모형의 출력을 가중치 $\alpha$로 가중 선형조합한 값을 판별함수로 사용한다. 또한 부스트 방법은 이진 분류에만 사용할 수 있으며 $y$값은 1 또는 -1의 값을 가진다.

AdaBoost(에이다부스트)

AdaBoost 개념

  • 에이다 부스트(adaboost)라는 이름은 적응 부스트(adaptive boost)라는 용어에서 나왔다. 에이다부스트는 위원회에 넣을 개별 모형 $k_{m}$을 선별하는 방법으로학습데이터 집합의 $i$번째 데이터에 가중치 $w_{i}$를 주고 분류 모형이 틀리게 예측한 데이터의 가중치를 합한 값을 손실함수 $L$로 사용한다. 이 손실함수를 최소화하는 모형이 k_{m}으로 선택된다.
  • 위 식에서 $I$는 $k(x_{i}) \neq y_{i}$라는 조건이 만족되면 1, 아니면 0을 갖는 indicator function이다. 즉 예측을 틀리게한 데이터들에 대한 가중치의 합이다. 위원회 $C_{m}$에 포함될 개별 모형 $k_{m}$이 선택된 후에는 가중치 $\alpha_{m}$을 결정해야 한다. 이 값은 다음처럼 계산한다.
  • 데이터에 대한 가중치 $w_{m, i}$는 최초에는$(m=1)$ 모든 데이터에 대해 동일한 값을 갖지만, 위원회가 증가하면서 값이 바뀐다. 가중치의 값은 지시함수를 사용하여 위원회 $C_{m-1}$이 맞춘 문제는 작게, 틀린 문제는 크게 확대(boosting)된다.
  • $m$번째 멤버의 모든 후보에 대해 위 손실함수를 적용하여 가장 값이 작은 후보를 $m$번째 멤버로 선정한다.
  • 에이다 부스팅은 사실 다음과 같은 손실함수를 최소화하는 $C_{m}$을 찾아가는 방법이라는 것을 증명할 수 있다.
  • 개별 멤버 $k_{m}$과 위원회 관계는
  • 이고 이 식을 대입하면
  • $y_{i}$와 $k_{M}(x_{i})$ 1 또는 -1값만 가질 수 있다는 점을 이용하면,
  • $L_{m}$을 최소화하려면 $\sum_{i=1}^N w_{m,i} I\left(k_m(x_i) \neq y_i\right)$을 최소화하는 $k_{m}$ 함수를 찾은 다음 $L_{m}$을 최소화하는 $\alpha_{m}$을 찾아야 한다.
  • 이 조건으로부터 $\alpha_{m}$ 공식을 유도할 수 있다.

Adaboost 비용함수

  • 다음은 Scikit-Learn의 ensemble 서브패키지가 제공하는 AdaBoostClassifier 클래스를 사용하여 분류 예측을 하는 예이다. 약분류기로는 깊이가 1인 단순한 의사결정나무를 채택하였다. 여기에서는 각 표본 데이터의 가중치 값을 알아보기 위해 기존의 AdaBoostClassifier 클래스를 서브 클래싱하여 가중치를 속성으로 저장하도록 수정한 모형을 사용하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from sklearn.datasets import make_gaussian_quantiles
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier

X1, y1 = make_gaussian_quantiles(cov=2.,
n_samples=100, n_features=2,
n_classes=2, random_state=1)
X2, y2 = make_gaussian_quantiles(mean=(3, 3), cov=1.5,
n_samples=200, n_features=2,
n_classes=2, random_state=1)
X = np.concatenate((X1, X2))
y = np.concatenate((y1, - y2 + 1))

class MyAdaBoostClassifier(AdaBoostClassifier):

def __init__(self,
base_estimator=None,
n_estimators=50,
learning_rate=1.,
algorithm='SAMME.R',
random_state=None):

super(MyAdaBoostClassifier, self).__init__(
base_estimator=base_estimator,
n_estimators=n_estimators,
learning_rate=learning_rate,
random_state=random_state)
self.sample_weight = [None] * n_estimators

def _boost(self, iboost, X, y, sample_weight, random_state):
sample_weight, estimator_weight, estimator_error = \
super(MyAdaBoostClassifier, self)._boost(iboost, X, y, sample_weight, random_state)
self.sample_weight[iboost] = sample_weight.copy()
return sample_weight, estimator_weight, estimator_error

model_ada = MyAdaBoostClassifier(DecisionTreeClassifier(max_depth=1, random_state=0), n_estimators=20)
model_ada.fit(X, y)

def plot_result(model, title="분류결과", legend=False, s=50):
x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, 0.02), np.arange(x2_min, x2_max, 0.02))
if isinstance(model, list):
Y = model[0].predict(np.c_[xx1.ravel(), xx2.ravel()]).reshape(xx1.shape)
for i in range(len(model) - 1):
Y += model[i + 1].predict(np.c_[xx1.ravel(), xx2.ravel()]).reshape(xx1.shape)
else:
Y = model.predict(np.c_[xx1.ravel(), xx2.ravel()]).reshape(xx1.shape)
cs = plt.contourf(xx1, xx2, Y, cmap=plt.cm.Paired, alpha=0.5)
for i, n, c in zip(range(2), "01", "br"):
idx = np.where(y == i)
plt.scatter(X[idx, 0], X[idx, 1], c=c, s=s, alpha=0.5, label="Class %s" % n)
plt.xlim(x1_min, x1_max)
plt.ylim(x2_min, x2_max)
plt.xlabel('x1')
plt.ylabel('x2')
plt.title(title)
plt.colorbar(cs)
if legend:
plt.legend()
plt.grid(False)

plot_result(model_ada, "에이다부스트(m=20) 분류 결과")

Adaboost 20번째 모델까지의 결과

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
plt.figure(figsize=(10, 15))
plt.subplot(421);
plot_result(model_ada.estimators_[0], "1번 분류모형의 분류 결과", s=10)
plt.subplot(422);
plot_result(model_ada.estimators_[1], "2번 분류모형의 분류 결과", s=(4000*model_ada.sample_weight[0]).astype(int))
plt.subplot(423);
plot_result(model_ada.estimators_[2], "3번 분류모형의 분류 결과", s=(4000*model_ada.sample_weight[1]).astype(int))
plt.subplot(424);
plot_result(model_ada.estimators_[3], "4번 분류모형의 분류 결과", s=(4000*model_ada.sample_weight[2]).astype(int))
plt.subplot(425);
plot_result(model_ada.estimators_[4], "5번 분류모형의 분류 결과", s=(4000*model_ada.sample_weight[3]).astype(int))
plt.subplot(426);
plot_result(model_ada.estimators_[5], "6번 분류모형의 분류 결과", s=(4000*model_ada.sample_weight[4]).astype(int))
plt.subplot(427);
plot_result(model_ada.estimators_[6], "7번 분류모형의 분류 결과", s=(4000*model_ada.sample_weight[5]).astype(int))
plt.subplot(428);
plot_result(model_ada.estimators_[7], "8번 분류모형의 분류 결과", s=(4000*model_ada.sample_weight[6]).astype(int))
plt.tight_layout()

Adaboost 모델의 가중치의 변화에 따른 decision boundary의 변화 - 01

Adaboost 모델의 가중치의 변화에 따른 decision boundary의 변화 - 02

Adaboost 모형의 정규화

  • Adaboost 모형이 과최적화(overfitting)가 되는 경우에는 학습 속도(learning rate)를 조정하여 정규화를 할 수 있다. 이는 필요한 멤버의 수를 강제로 증가시켜서 과최적화를 막는 역할을 한다. 즉, 새롭게 적용되는 모형에 대한 가중치를 줄여서 동일한 모형의 횟수를 거치더라도 가중치가 크게 영향을 받지 않도록 하여 과최적화를 없애는 방법이다.
  • AdaBoostClassifier 클래스에서는 learning_rate인수를 1보다 적게 주면 새로운 멤버의 가중치를 강제로 낮춘다.

그레디언트 부스팅 (Gradient boosting)

  • 기본적으로 부스팅은 다음 Round에서 이전에 잘못 예측한 데이터들에 대한 처리를 어떻게 하느냐에 따라 종류별로 차이가 존재한다. Gradient Boosting은 이전 Round의 분류기로 예측한 error를 다음 Round의 분류기가 예측할 수 있도록 학습하면서 진행한다.

그레디언트 부스팅의 개념

  • 이전 모델의 error를 다음 모델이 예측할 수 있게끔 학습시켜 해당 분류기들의 학습된 결과를 계속해서 합해 나가면 마지막에는 최소한의 error만 남으므로, error를 최대한 줄일 수 있게 된다.

그레디언트 부스팅의 원리

  • 위에서 언급했던 것과 같이 error를 예측하게 하므로 이해하기 쉽게 regression을 통한 예시로 설명하겠다. 처음 모델의 error를 다음 모델은 예측하도록 학습하므로 이전 모델보다 오차가 더 줄어들 것이다. 그 다음 모델도 이전 모델의 오차를 학습하게 되므로 더 오차가 줄어들 것이다. 이렇게 최종적으로는 error가 최대한 0에 가까워질 때 까지 학습하여 train set에 대해서는 과최적화가 이루어 질 것이다.

그레디언트 부스팅의 이해

  • 최종적으로는 학습 데이터에 대한 error를 작게 하는 것이므로 아래 그림에서와 같이 negative gradient를 최소화시키면서 학습 될 것이다.

그레디언트 부스팅의 cost function

  • 위의 그림에서 볼 수 있듯이 그레디언트 부스트 모형은 변분법(calculus of variations)을 사용한 모형이다. 학습 $f(x)$를 최소화하는 $x$는 다음과 같이 gradient descent 방법으로 찾을 수 있다.
  • 그레디언트 부스트 모형에서는 손실 범함수(loss functional) $L(y, C_{m-1})$을 최소화하는 개별 분류함수 $k_{m}$를 찾는다. 이론적으로 가장 최적의 함수는 범함수의 미분이다.
  • 따라서 그레디언트 부스트 모형은 분류/회귀 문제에 상관없이 개별 멤버 모형으로 회귀분석 모형을 사용한다. 가장 많이 사용되는 회귀분석 모형은 의사결정 회귀나무(decision tree regression model)모형이다.
  • 그레디언트 부스트 모형에서는 다음과 같은 과정을 반복하여 멤버와 그 가중치를 계산한다.

      1. $-\tfrac{\delta L(y, C_m)}{\delta C_m}$를 목표값으로 개별 멤버 모형 $k_{m}$을 찾는다.
      1. $ \left( y - (C_{m-1} + \alpha_m k_m) \right)^2 $ 를 최소화하는 스텝사이즈 $\alpha_{m}$을 찾는다.
      1. $ C_m = C_{m-1} + \alpha_m k_m $ 최종 모형을 갱신한다.
  • 만약 손실 범함수가 오차 제곱 형태라면

  • 범함수의 미분은 실제 목표값 $y$와 $C_{m-1}$과의 차이 즉, 잔차(residual)가 된다.
  • Scikit-Learn의 GradientBoostingClassifier는 약한 학습기의 순차적인 예측 오류 보정을 통해 학습을 수행하므로 멀티 CPU 코어 시스템을 사용하더라도 병렬처리가 지원되지 않아서 대용량 데이터의 경우 학습에 매우 많은 시간이 필요하다. 또한 일반적으로 GBM이 랜덤 포레스트보다는 예측 성능이 조금 뛰어난 경우가 많다. 그러나 수행시간이 오래 걸리고, 하이퍼 파라미터 튜닝 노력도 더 필요하다.

  • loss: 경사 하강법에서 사용할 비용 함수를 저장한다. 특별한 이유가 없으면 default인 ‘deviance’를 그대로 적용한다.

  • learning_rate: GBM이 학습을 진행할 때마다 적용하는 학습률이다. Weak learner가 순차적으로 오류 값을 보정해 나가는 데 적용하는 계수이다. 0~1 사이의 값을 지정할 수 있으며 default=0.1이다. 너무 작은 값을 적용하면 업데이트 되는 값이 작아져서 최소 오류 값을 찾아 예측 성능이 높아질 가능성이 높다. 하지만 많은 weak learner는 순차적인 반복이 필요해서 수행 시간이 오래 걸리고, 또 너무 작게 설정하면 모든 weak learner의 반복이 완료돼도 최소 오류 값을 찾지 못할 수 있다. 반대로 큰 값을 적용하면 최소 오류 값을 찾지 못하고 그냥 지나챠 버려 예측 성능이 떨어질 가능성이 높아지지만, 빠른 수행이 가능하다. 이러한 특성 때문에 learning_rate는 n_estimators와 상호 보완적으로 조합해 사용한다. learning_rate를 작게하고 n_estimators를 크게 하면 더 이상 성능이 좋아지지 않는 한계점까지는 예측 성능이 조금씩 좋아질 수 있다.
  • subsample: weak learner가 학습에 사용하는 데이터의 샘플링 비율이다. default=1이며, 이는 전체 학습 데이터를 기반으로 학습한다는 의미이다.(0.5이면 학습데이터의 50%를 의미) 과적합이 염려되는 경우 subsample을 1보다 작은 값으로 설정한다.
1
2
3
from sklearn.ensemble import GradientBoostingClassifier

model_grad = GradientBoostingClassifier(n_estimators=100, max_depth=2, random_state=0)
1
2
%%time
model_grad.fit(X, y)
결과
1
2
3
4
5
6
7
8
9
10
11
12
13
CPU times: user 50 ms, sys: 0 ns, total: 50 ms
Wall time: 50.4 ms

GradientBoostingClassifier(criterion='friedman_mse', init=None,
learning_rate=0.1, loss='deviance', max_depth=2,
max_features=None, 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=100,
n_iter_no_change=None, presort='auto',
random_state=0, subsample=1.0, tol=0.0001,
validation_fraction=0.1, verbose=0,
warm_start=False)
1
plot_result(model_grad)

그레디언트 부스트의 decision boundary

1
2
3
4
5
6
7
plt.subplot(121)
plot_result(model_grad.estimators_[3][0])
plt.subplot(122)
plot_result([model_grad.estimators_[0][0],
model_grad.estimators_[1][0],
model_grad.estimators_[2][0],
model_grad.estimators_[3][0]])

그레디언트 부스트에 사용된 모형들의 4번째 까지의 각각의 decision decision boundary

XGBoost

XGBoost 개념

  • XGboost는 GBM에 기반하고 있지만, GBM의 단점인 느린 수행 시간 및 과적합 규제(Regularization) 부재 등의 문제를 해결해서 매우 각광을 받고 있다. 특히 XGBoost는 병렬 CPU 환경에서 병렬 학습이 가능해 기존 GBM보다 빠르게 학습을 완료할 수 있다. 다음은 XGboost의 장점이다.
항목 설명
뛰어난 예측성능 일반적으로 분류와 회귀 영역에서 뛰어난 예측 성능을 발휘한다.
GBM 대비 빠른 수행 시간 일반적인 GBM은 순차적으로 Weak Learner가 가중치를 증감하는 방법으로 학습하기 때문에 전반적으로 속도가 느리다. 하지만 XGBoost는 병렬 수행 및 다양한 기능으로 GBM에 비해 빠른 수행 성능을 보장한다. 아쉽게도 XGBoost가 일반적인 GBM에 비해 수행 시간이 빠르다는 것이지, 다른 머신러닝 알고리즘 (예를 들어 랜덤 포레스트)에 비해서 빠르다는 의미는 아니다.
과적합 규제 (Regularization) 표준 GBM의 경우 과적합 규제 기능이 없으나 XGBoost는 자체에 과적합 규제 기능으로 과적합에 좀 더 강한 내구성을 가질 수 있다.
Tree pruning (나무 가지치기) 일반적으로 GBM은 분할 시 부정 손실이 발생하면 분할을 더 이상 수행하지 않지만, 이러한 방식도 자칫 지나치게 많은 분할을 발생할 수 있다. 다른 GBM과 마찬가지로 XGBoost도 max_depth 파라미터로 분할 깊이를 조정하기도 하지만, tree pruning으로 더 이상 긍정 이득이 없는 분할을 가지치기 해서 분할 수를 더 줄이는 추가적인 장점을 가지고 있다.
자체 내장된 교차 검증 XGBoost는 반복 수행 시마다 내부적으로 학습 데이터 세트와 평가 데이터 세트에 대한 교차 검증을 수행해 최적화된 반복 수행 횟수를 가질 수 있다. 지정된 반복 횟수가 아니라 교차 검증을 통해 평가 데이터 세트의 평가값이 최적화 되면 반복을 중간에 멈출 수 있는 조기 중단 기능이 있다.
결손값 자체 처리 XGBoost는 결손값을 자체 처리할 수 있는 기능을 가지고 있다.

XGBoost의 장점

  • XGBoost의 핵심 라이브러리는 C/C++로 작성돼 있다. XGBoost 개발 그룹은 파이썬에서도 XGBoost를 구동할 수 있도록 파이썬 패키지를 제공한다. 이 파이썬 패키지의 역할은 대부분 C/C++ 핵심 라이브러리를 호출하는 것이다.
1
2
3
4
5
# window용
# conda install -c anaconda py-xgboost

# Linux용
conda install -c conda-forge xgboost
  • python 래퍼 모듈과 Scikit-Learn 래퍼 XGBoost 모듈의 일부 hyper-parameter는 약간 다르므로 이에 대한 주의가 필요하다.

  • python 래퍼 XGBoost 모듈

    • XGboost 고유의 프레임워크를 python 언어 기반에서 구현한 것으로 별도의 API기반을 갖고 있어 Scikit-Learn 프레임워크를 기반으로 한 것이 아니기에 Scikit-Learn의 fit(), predict() 메서드 같은 Scikit-Learn 고유의 아키텍처와 다른 다양한 유틸리티(cross_val_score, GridSearchCV, Pipeline 등)와 함께 사용될 수 없다.

    • 일반 parameter

      • 일반적으로 실행 시 thread의 개수나 silent 모드 등의 선택을 위한 parameter로서 default parameter 값을 바꾸는 경우는 거의 없다.
      • booster : gbtree(tree based model) 또는 gblinear(linear model)선택 default=gbtree
      • silent : default=0이며, 출력 메시지를 나타내고 싶지 않을 경우 1로 설정한다.
      • nthread : CPU의 실행 thread 개수를 조정하며, default는 CPU의 전체 thread를 다 사용하는 것이다. Multi Core/thread CPU 시스템에서 전체 CPU를 사용하지 않고 일부 CPU만 사용해 ML 애플리케이션을 구동하는 경우에 변경한다.
    • Booster parameter

      • tree 최적화, Boosting, Regularization 등과 관련 parameter 등을 지칭한다.

      • eta [default=0.3, alias:learning_rate] : GBM의 학습률(learning rate)과 같은 parameter이다. 0~1 사이의 값을 지정하며 Boosting step을 반복적으로 수행할 때 업데이트되는 학습률 값. python 래퍼 기반의 xgboost를 이용할 경우 default=0.3 scikit-learn 래퍼를 이용할 경우 eta는 learning_rate로 대체되며, default=0.1이다. 보통은 0.01~0.2 사이의 값을 선호한다.

      • num_boost_rounds : GBM의 n_estimators와 같은 parameter이다.

      • min_child_weight[default=1] : GBM의 min_child_leaf와 유사함(똑같지는 않음). 과적합을 조절하기 위해 사용된다.
      • gamma [default=0, alias: min_split_loss] : tree의 leaf 노드를 추가적으로 나눌지를 결정할 최소 손실 감소 값이다. 해당 값보다 큰 손실(loss)이 감소된 경우에 leaf 노드를 분리한다. 값이 클수록 과적합 감소 효과가 있다.
      • max_depth [default=6] : tree 기반 알고리즘의 max_depth와 같다. 0을 지정하면 깊이에 제한이 없다. Max_depth가 높으면 특정 feature 조건에 특화되어 룰 조건이 만들어지므로 과적합 가능성이 높아지며 보통은 3~10사이의 값을 적용한다.
      • sub_sample [default=1] : GBM의 subsample과 동일하다. tree가 커져서 과적합되는 것을 제어하기 위해 데이터를 샘플링하는 비율을 지정한다. sub_sample=0.5로 지정하면 전체 데이터의 절반을 tree를 생성하는 데 사용한다. 0에서 1사이의 값이 가능하나 일반적으로 0.5~1사이의 값을 사용한다.
      • colsample_bytree [default=1] : GBM의 max_features와 유사하다. tree 생성에 필요한 feature(column)를 임의로 샘플링 하는 데 사용된다. 매우 많은 feature가 있는 경우 과적합을 조정하는 데 적용한다.
      • lambda [default=1, alias:reg_lambda] : L2 Regularization 적용 값이다. feature 개수가 많을 경우 적용을 검토하며 값이 클수록 과적합 감소 효과가 있다.
      • alpha : L1 Regularization 적용값이다. feature 개수가 많을 경우 적용을 검토하며 값이 클수록 감소 효과가 있다.
      • scale_pos_weight [default=1] : 특정 값으로 치우친 비대칭한 클래스로 구성된 데이터 세트의 균형을 유지하기 위한 paramter이다.
    • 학습 task parameter

      • 학습 수행 시의 객체 함수, 평가를 위한 지표 등을 설정하는 parameter이다.
      • objective : 최솟값을 가져야할 손실 함수(loss function)을 정의한다. XGBoost는 많은 유형의 손실함수를 사용할 수 있다. 주로 사용되는 손실함수는 이진 분류인지 다중 분류인지에 따라 달라진다.
        • binary:logistic : 이진 분류일 때 적용한다.
        • multi:softmax : 다중 분류일 때 적용한다. 손실 함수가 multi:softmax 일 경우에는 label class의 개수인 num_class parameter를 지정해야 한다.
        • multi:softprob : multi:softmax와 유사하나 개별 label class의 해당되는 예측 확률을 반환한다.
      • eval_metric : 검증에 사용되는 함수를 정의한다. default는 회귀인 경우 rmse, 분류인 경우 error이다. 다음은 eval_metric의 값 유형이다.
        • rmse : Root Mean Square Error
        • mae : Mean Absolute Error
        • logloss : Negative log-likelihood
        • error : Binary classification error rate (0.5 threshold)
        • merror : Multiclass classification error rate
        • mlogloss : Multiclass logloss
        • auc : Area under the curve
    • 대부분의 hyper parameter는 Booster paramter에 속한다.

  • Scikit-Learn 래퍼 XGBoost 모듈

    • XGboost 패키지의 Scikit-Learn 래퍼 클래스는 XGBClassifier, XGBRegressor이다. 이를 이용하면 Scikit-Learn estimator가 학습을 위해 사용하는 fit(), predict() 와 같은 표준 Scikit-Learn 개발 프로세스 및 다양한 유틸리티를 활용할 수 있다.
  • 과적합(overfitting) 문제가 심각하다면 다음과 같이 적용할 것을 고려할 수 있다.

    • eta 값을 낮춘다.(0.01~0.1)
      • eta 값을 낮출 경우 num_round(또는 n_estimators)는 반대로 높여줘야 한다.
    • max_depth 값을 낮춘다.
    • min_child_weight 값을 높인다.
    • gamma 값을 높인다.
    • 또한 subsample과 colsample_bytree를 조정하는 것도 tree가 너무 복잡하게 생성되는 것을 막아 과적합 문제에 도움이 될 수 있다.
  • XGBoost 자체적으로 교차 검증, 성능 평가, feature 중요도 등의 시각화 기능을 가지고 있다. 또한 XGBoost는 기본 GBM에서 부족한 다른 여러 가지 성능 향상 기능이 있다. 그 중에 수행 속도를 향상시키기 위한 대표적인 기능으로 Early Stopping 기능이 있다. 기본 GBM의 경우 지정된 횟수를 다 완료해야 한다. 허나, XGBoost와 LightGBM은 모두 early Stopping 기능이 있어서 n_estimators에 지정한 Boosting 반복 횟수에 도달하지 않더라도 예측 오류가 더 이상 개선되지 않으면 반복을 끝까지 수행하지 않고 중지해 수행 시간을 개선 할 수 있다.

  • 예를 들어 n_estimators=200, early Stopping 파라미터 값을 50으로 설정하면, 1부터 200회까지 Boosting을 반복하다가 50회를 반복하는 동안 학습 오류가 감소하지 않으면 더 이상 Boosting을 진행하지 않고 종료한다.(가령 100회에서 학습 오류 값이 0.8인데, 101회~150회 반복하는 동안 예측 오류가 0.8보다 작은 값이 하나도 없으면 Boosting을 종료한다.)

  • 아래는 python 래퍼의 Xgboost 사용법을 간단히 정리해 놓은 것이다. 일반적으로 XGBoost는 GBM과는 다르게 병렬처리와 early Stopping 등으로 빠른 수행시간 처리가 가능하지만, CPU 코어가 많지 않은 개인용 PC에서는 수행시간 향상을 경험하기 어려울 수도 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import xgboost as xgb
from xgboost import plot_importance
import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings('ignore')

dataset = load_breast_cancer()
X_features = dataset.data
y_label = dataset.target

cancer_df = pd.DataFrame(data=X_features, columns=dataset.feature_names)
cancer_df['target'] = y_label

print(dataset.target_names)
print(cancer_df['target'].value_counts())
결과
1
2
3
['malignant', 'benign']
1 357
0 212


1
2
X_train, X_test, y_train, y_test=train_test_split(X_features, y_label, test_size=0.2, random_state=156)
print(X_train.shape, X_test.shape)
결과
1
(455, 30) (114, 30)

  • python 래퍼 XGboost가 Scikit-Learn 래퍼 XGboost와 차이점은 여러가지가 있지만, 가장 큰 차이는 학습용 데이터와 테스트용 데이터 세트를 위해 별도의 객체인 DMatrix를 생성한다는 점이다.

    • Dmatrix는 주로 numpy 입력 parameter를 받아서 만들어지는 XGBoost만의 전용 데이터 세트이지만 numpy이외에 libsvm txt 포맷 파일, xgboost 이진 버퍼 파일을 parameter로 입력받아 변환할 수 있다.
  • data는 피처 데이터 세트이며, label은 classification의 경우에는 label 데이터 세트, regression의 경우에는 숫자형인 종속값 데이터 세트이다.


1
2
dtrain = xgb.DMatrix(data=X_train, label=y_train)
dtest = xgb.DMatrix(data=X_test, label=y_test)

  • early_stopping_rounds 파라미터를 설정해 조기 중단을 수행하기 위해서는 반드시 eval_set과 eval_metric이 함께 설정되야 한다. XGboost는 반복마다 eval_set으로 지정된 데이터 세트에서 eval_metric의 지정된 평가 지표로 예측 오류를 측정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
params = {'max_depth':3,
'eta':0.1,
'objective':'binary:logistic',
'eval_metric':'logloss',
'early_stoppings':100
}
num_rounds = 400

# train 데이터 세트는 'train', evaluation(test) 데이터 세트는 'eval'로 명시한다.
wlist = [(dtrain, 'train'), (dtest, 'eval')]

# 하이퍼 파라미터와 early stopping 파라미터를 train() 함수의 파라미터로 전달
xgb_model = xgb.train(params=params, dtrain=dtrain, num_boost_round=num_rounds, evals=wlist)

  • python 래퍼 xgboost는 predict() 메서드가 예측 결과값이 아닌 예측 결과를 추정할 수 있는 확률 값을 반환한다는 것이다.
    • 예측 확률이 0.5보다 크면 1, 그렇지 않으면 0으로 예측하는 로직을 추가

1
2
3
4
5
6
pred_probs = xgb_model.predict(dtest)
print('predict() 수행 결과값을 10개만 표시, 예측 화귤값으로 표시된')
print(np.round(pred_probs[:10], 3))

preds = [1 if prob > 0.5 else 0 for prob in pred_probs]
print('예측값 10개만 표시:', preds[:10])
결과
1
2
3
predict() 수행 결과값을 10개만 표시, 예측 화귤값으로 표시된
[0.95 0.003 0.9 0.086 0.993 1. 1. 0.999 0.998 0. ]
예측값 10개만 표시: [1, 0, 1, 0, 1, 1, 1, 1, 1, 0]

  • xgboost 패키지에 내장된 시각화 기능 중 plot_importance() API는 feature의 중요도를 막대그래프 형식으로 나타낸다. 기본 평가 지표로 f1-score를 기반으로 해 각 feature의 중요도를 나타낸다. Scikit-Learn은 Estimator 객체의 feature_importances_ 속성을 이용해 직접 시각화 코드를 작성해야 하지만, xgboost 패키지는 plot_importance()를 이용해 바로 피처 중요도를 시각화할 수 있다. plot_importance() 호출 시 파라미터로 앞에서 학습이 완료된 모델 객체 및 Matplotlib의 ax 객체를 입력하기만 하면 된다.
  • 내장된 plot_importance() 이용 시 유의할 점은 xgboost numpy 기반의 feature 데이터터로 학습시에 피처명을 제대로 알 수 가 없으므로 f0, f1와 같이 feature 순서별로 f자 뒤에 순서를 붙여서 X 축에 feature들로 나열한다.
1
2
3
4
5
6
from xgboost import plot_importance
import matplotlib.pyplot as plt
%matplotlib inline

fig, ax = plt.subplots(figsize=(10, 12))
plot_importance(xgb_model, ax=ax)

python 래퍼의 xgboost feature 중요도

  • 또한, Decision Tree에서 보여준 tree 기반 규칙 구조도 xgboost에서 시각화할 수 있다. xgboost 모듈의 to_graphviz() API를 이용하면 jupyter notebook에 바로 규칙 tree 구조를 그릴 수 있다. xgboost.cv() API를 통해 GridSearchCV와 유사한 기증을 수행할 수 있다.

  • 아래는 Scikit-Learn 래퍼의 xgboost의 사용법을 정리해 놓은 것이다.

    • 앞의 python 래퍼와 동일한 결과를 보여준다.

1
2
3
4
5
6
7
from xgboost import XGBClassifier
from sklearn.metrics import confusion_matrix

xgb_wrapper = XGBClassifier(n_estimators=400, learning_rate=0.1, max_depth=3, random_state=156)
xgb_wrapper.fit(X_train, y_train)
w_preds = xgb_wrapper.predict(X_test)
print(confusion_matrix(y_test, w_preds))
결과
1
2
[[35  2]
[ 1 76]]

  • early stopping 기능을 사용하는 방법은 아래와 같다. 성능 평가를 수행할 데이터 세트는 학습 데이터가 아니라 별도의 데이터 세트이어야 한다. 허나, 아래 데이터 자체의 크기가 작기 때문에 평가용으로 사용해 보았다. 허나, 절대 아래와 같이 evals에 test 데이터를 사용하면 안된다. 만일 test data를 사용했다면 predict하는 경우에는 학습에 사용되지 않은 또 다른 데이터를 사용해야한다.

  • 또한, early stopping을 너무 적게 잡는다면 전역 최적화가 이루어지지 않을 수도 있으므로 주의하자

    • GridSearchCV와 같이 hyper parameter를 tuning할 경우에는 XGBoost가 GBM보다는 빠르지만 아무래도 GBM을 기반으로 하고 있기 때문에 수행 시간이 상당히 더 많이 요구된다. 앙상블 계열 알고리즘은 overfitting이나 noise에 기본적으로 뛰어난 알고리즘이므로 hyper parameter tuning으로 성능 수치 개선이 급격하게 좋아지는 경우는 그리 많지 않다. 일반 PC가 아닌 적어도 8-Core이상의 병렬 CPU Core 시스템을 가진 컴퓨터가 있다면 더 다양하게 hyper parameter 변경해 가면서 성능 향상을 적극저으로 시도해 보면 좋을 것이다.

1
2
3
4
5
xgb_wrapper = XGBClassifier(n_estimators=400, learning_rate=0.1, max_depth=3, random_state=156)
evals = [(X_test, y_test)]
xgb_wrapper.fit(X_train, y_train, early_stopping_rounds=100, eval_metric='logloss', eval_set=evals, verbose=True)
w_preds = xgb_wrapper.predict(X_test)
print(confusion_matrix(y_test, w_preds))
결과
1
2
[[34  3]
[ 1 76]]

LightGBM

  • LightGBM은 XGBoost와 함께 부스팅 계열 알고리즘에서 가장 각광 받고 있다. XGBoost는 매우 뛰어난 부스팅 알고리즘이지만, XGBoost에서 GridSearchCV로 hyper parameter 튜닝을 수행하다 보면 여전히 학습시간이 오래 걸린다. 물론 GBM 보다는 빠르지만, 대용량 데이터의 경우 만족할 만한 학습 성능을 기대하려면 많은 CPU Core를 가진 시스템에서 높은 병렬도로 학습을 진행해야 한다.
  • LightGBM의 가장 큰 장점은 XGBoost보다 학습에 걸리는 시간이 훨씬 적다는 점이다. 또한 메모리 사용량도 상대적으로 적다. LightGBM이 XGBoost보다 2년 후에 만들어지다보니 XGBoost의 장점은 계승하고 단점은 보완하는 방식으로 개발되었기 때문에 예측 성능에서의 차이는 거의 없지만, 기능상의 다양성이 더 높다.
  • LightGBM의 한 가지 단점으로 알려진 것은 적은 데이터 세트에 적용할 경우 과적합이 발생하기 쉽다는 것이다. 적은 데이터 세트의 기준은 애매하지만, 일반적으로 10,000건 이하의 데이터 세트 정도라고 LightGBM 공식 문서에서 기술하고 있다.
  • LightGBM은 일반 GBM 계열의 트리 분할 방법과 다르게 leaf 중심 트리 분할(Leaf Wise) 방식을 사용한다. 기존의 대부분 트리 기반 알고리즘은 트리의 깊이를 효과적으로 줄이기 위한 균형 트리 분할(Level wise)방식을 사용한다. 즉, 최대한 균형 잡힌 트리를 유지하면서 분할하기 때문에 트리의 깊이가 최소화될 수 있다. 이렇게 균형잡힌 트리를 생성하는 이유는 과적합(overfitting)에 보다 더 강한 구조를 가질 수 있다고 알려져 있기 때문이다. 반대로 균형을 맞추기 위한 시간이 필요하다는 상대적인 단점이 있다. 하지만, LightGBM의 leaf 중심 트리 분할 방식은 트리의 균형을 맞추지 않고, 최대 손실 값(max delta loss)을 가지는 leaf 노드를 지속적으로 분할하면서 트리의 깊이가 깊어지고 비대칭적인 규칙 트리가 생성된다. 하지만 이렇게 최대 손실값을 가지는 leaf 노드를 지속적으로 분할해 생성된 규칙 트리는 학습을 반복할수록 결국은 균형 트리 분할 방식보다 예측 오류 손실을 최소화 할 수 있다는 것이 LightGBM의 구현 사상이다.

LightGBM의 개념

  • LightGBM 설치 방법
    • Window에 설치할 경우에는 Visual Studio Build tool 2015 이상이 설치돼있어야 한다.
    • 아나콘다 프롬프트를 관리자 권한으로 실행한 다음 아래 명려어 실행
1
conda install -c conda-forge lightgbm
  • LightGBM 하이퍼 파라미터는 XGBoost와 많은 부분이 유사하지만 트리의 분할 방식이 다르므로 예를 들어 max_depth를 매우 크게 가져가는 것과 같이 트리 특성에 맞게 설정해 주어야 할 것이다.

주요 파라미터

  • num_iterations [default = 100] : 반복 수행하려는 트리의 개수를 지정한다. 크게 지정할수록 예측 성능이 높아질수 있으나, 너무 크게 지정하면 오히려 과적합으로 성능이 저하 될 수 있다. Scikit-Learn GBM과 XGBoost의 Scikit-Learn 호환 클래스의 n_estimators와 같은 파라미터이므로 LightGBM의 Scikit-Learn 호환 클래스에서는 n_estimators로 이름이 변경된다.

  • learning_rate [default = 0.1] : 0에서 1사이의 값을 지정하며 Boosting 스텝을 반복적으로 수행할 때 업데이트되는 학습롤값이다. 일반적으로 n_estimators를 크게하고 learning_rate를 작게해서 예측 성능을 향상시킬 수 있으나, 마찬가지로 과적합(overfitting) 이슈와 학습 시간이 길어지는 부정적인 영향도 고려해야한다. GBM, XGBoost의 learning_rate와 같은 파라미터이다.

  • max_depth [default=1] : 트리 기반 알고리즘의 max_depth와 같다. 0보다 작은 값을 지정하면 깊이에 제한이 없다. 지금까지 소개한 Depth Wise 방식의 트리와 다르게 LightGBM은 Leaf wise 기반이므로 깊이가 상대적으로 더 깊다.

  • min_data_in_leaf [default=20] : Decision Tree의 min_samples_leaf와 같은 파라미터이다. 하지만 Scikit-Learn 래퍼 LightGBM 클래스인 LightGBMClassifier에서는 min_child_samples 파라미터로 이름이 변경된다. 최종 결정 클래스인 Leaf 노드가 되기 위해서 최소한으로 필요한 레코드(데이터) 수이며, 과적합을 제어하기 위한 파라미터이다.

  • num_leaves [default=31] : 하나의 트리가 가질 수 있는 최대 Leaf 개수이다.

  • boosting [default=gbdt] : Boosting 트리를 생성하는 알고리즘을 기술한다.

    • gbdt : 일반적인 그레디언트 부스팅 결정트리
    • rf : 랜덤포레스트
  • bagging_fraction [default=1.0] : 트리가 커져서 과적합되는 것을 제어하기 위해서 데이터 샘플링하는 비율을 지정한다. Scikit-Learn의 GBM과 XGBoost의 sub_sample 파라미터와 동일하기에 Scikit-Learn 래퍼 LightGBM인 LightGBMClassifier에서는 sub_sample로 동일하게 파라미터 이름이 변경된다.

  • feature_fraction [default=1.0] : 개별 트리를 학습할 때마다 무작위로 선택하는 feature의 비율이다. 과적합을 막기 위해 사용된다. GBM의 max_features와 유사하며, XGBClassifier의 colsample_bytree와 똑같으므로 LightGBMClassifier에서는 동일하게 colsample_bytree로 변경된다.

  • lambda_l2 [default=0.0] : L2 Regulation 제어를 위한 값이다. feature 개수가 많을 경우 적용을 검토하며 값이 클수록 과적합 감소 효과가 있다. XGBClassifier의 reg_lambda와 동일하므로 LightGBMClassifier에서는 reg_lambda로 변경된다.

  • lambda_l1 [default=0.0] : L1 Regulation 제어를 위한 갑이다. L2와 마찬가지로 과적합 제어를 위한 것이며, XGBClassifier의 reg_alpha와 동일하므로 LightGBMClassifier에서는 reg_alpha로 변경된다.

Learning Task 파라미터

  • objective : 최솟값을 가져야 할 손실함수(loss function)을 정의한다. XGBoost의 objective 파라미터와 동일하다. 애플리케이션 유형, 즉 regression, multiclass classification, binary classificationdl인지에 따라 objective인 손실함수가 지정된다.

하이퍼 파라미터 튜닝 방안

  • num_leaves의 개수를 중심으로 min_child_samples(min_data_in_leaf), max_depth를 함께 조정하면서 모델의 복잡도를 줄이는 것이 기본 튜닝 방안이다.

    • num_leaves는 개별 트리가 가질 수 있는 최대 Leaf의 개수이고 LightGBM 모델의 복잡도를 제어하는 주요 파라미터이다. 일반적으로 num_leaves의 개수를 높이면 정확도가 높아지지만, 반대로 트리의 깊이가 깊어지고 모델의 복잡도가 커져서 과적합 영향도가 커진다.

    • min_data_in_leaf는 Scikit-Learn 래퍼 클래스에서는 min_child_samples로 이름이 바뀐다. 과적합을 개선하기 위한 중요한 파라미터이다. num_leaves와 학습 데이터의 크기에 따라 달라지지만, 보통 큰 값으로 설정하면 트리가 깊어지는 것을 방지한다.

    • max_depth는 명시적으로 깊이의 크기를 제한한다. num_leaves, min_data_in_leaf와 결합해 과적합을 개선하는데 사용한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from lightgbm import LGBMClassifier

import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

dataset = load_breast_cancer()
X_data = dataset.data
y = dataset.target

X_train, X_test, y_train, y_test = train_test_split(X_data, y, test_size=0.2 ,random_state=1234)

lgbm_wrapper = LGBMClassifier(n_estimators=400)

evals = [(X_test, y_test)]
lgbm_wrapper.fit(X_train, y_train, early_stopping_rounds=100, eval_metric="logloss", eval_set=evals, verbose=True)
preds = lgbm_wrapper.predict(X_test)
1
2
from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, preds)

결과

1
2
array([[36,  9],
[ 2, 67]])
1
2
3
4
5
6
7
8
9
def confusion_matrix_mine(y_test, pred):
class_types=list(set(y_test))
confusion_matrix=np.zeros((len(class_types),len(class_types)))
for y, pred in zip(y_test, preds):
if y==pred:
confusion_matrix[y,pred]=confusion_matrix[y,pred]+1
else:
confusion_matrix[y,pred]=confusion_matrix[y,pred]+1
return confusion_matrix


1
confusion_matrix_mine(y_test, pred)
결과
1
2
array([[36.,  9.],
[ 2., 67.]])


1
2
from sklearn.metrics import classification_report
print(classification_report(y_test, preds))
결과
1
2
3
4
5
6
7
8
              precision    recall  f1-score   support

0 0.95 0.80 0.87 45
1 0.88 0.97 0.92 69

accuracy 0.90 114
macro avg 0.91 0.89 0.90 114
weighted avg 0.91 0.90 0.90 114


1
2
3
4
5
6
from lightgbm import plot_importance
import matplotlib.pyplot as plt
%matplotlib inline

fig, ax = plt.subplots(figsize=(10,12))
plot_importance(lgbm_wrapper, ax=ax)

lightgbm 피처 중요도 그래프

CatBoost

  • 최근 machine learning 알고리즘은 아래 그림에서 볼 수 있듯이 lightgbm과 catboost를 사용하는 유저들이 많아졌다는 것을 확인할 수 있다. 그렇다면 과연 CatBoost과 무엇이길래 많은 사람들이 사용하는지 한번 알아보자.

CatBoost의 성장

  • categorical feature에 잘 맞는다고 알려져있다. 일반적으로 머신러닝의 모델은 분산(Variance)과 편향(bias)의 trade-off관계를 조절하며 어떤 것에 더 중점을 둘지를 결정하여 만들게 된다. 그러나 이 CatBoost는 잔차 추정의 분산을 최소로 하면서 bias를 피하는 boosting기법이다. 즉, validation set을 제외한 data set에서 train에 사용되지 않은 data들을 통해 잔차의 분산을 최소화시키도록 학습을 시키는 방법이다.

Catboost 개념

CatBoost 참조

기존의 부스팅 방법

  • 기존의 부스팅 기법을 간략히 설명하면 다음과 같다.

    • 1) 실제 값들의 평균과 차이인 잔차(Residual)를 구한다.
    • 2) 데이터로 이 잔차들을 학습하는 모델을 만든다.
    • 3) 모델을 통해 학습한 파라미터 (평균 + 잔차예측 값 * learning_rate)를 업데이트한다.
    • 4) 위의 과정을 loss값이 일정 round 동안 수렴할때까지 반복한다.

기존의 부스팅의 문제점

  • 1) 느린 학습 속도

    • 부스팅 모델이 아닌 배깅과 비교했을 때, 훨씬 느린 속도를 보인다. 배깅의 경우 여러 트리들이 병렬적으로 모델 학습을 수행하고 부스팅의 경우 순차적으로 모델학습을 수행하기 때문에 느린 속도를 갖을 수 밖에 없다. 이런 문제점을 보완한 것이 XGBoost, LightGBM, CatBoost들이다.
  • 2) overfitting

    • 속도문제를 샘플링이나 알고리즘 최적화로 어느정도 개선이 되었다면, 남아있는 문제는 overfitting이다. 이는 부스팅이라는 개념 자체가 가지고 있는 문제인데, 부스팅 자체가 오차(error)를 줄여나가기 위해 학습하는 모델이기 때문에 굉장히 High Variance한 모델이기 때문이다.

CatBoost의 특징

Level-wise Tree

  • LightGBM은 Leaf-wise 방식으로 트리를 만들었지만, XGBoost와 동일하게 Level-wise 방식으로 트리를 만들어 나간다. 직관적으로 표현하자면 Level-wise는 BFS같이 트리를 만들어나가는 방식이고, Leaf-wise는 DFS 같이 트리를 만들어나가는 형태인 것이다.

Ordered Boosting

  • CatBoost는 기존의 부스팅 과정과 전체적인 양상은 비슷하되, 조금 다르다. 기존의 부스팅 모델이 일괄적으로 모든 훈련 데이터를 대상으로 잔차를 계산 했다면, CatBoost는 일부 즉, 훈련에 사용되지 않은 나머지 데이터에 대해 error를 추정한 뒤, 이것을 통해 모델을 만들고, 그 뒤에 데이터의 잔차는 만들어진 모델을 통해 예측한 값을 사용한다.

  • 예를 들면, 아래와 같은 데이터가 있다고 가정해보자.

time datapoint class label
12:00 $x_1$ 10
12:01 $x_2$ 12
12:02 $x_3$ 9
12:03 $x_4$ 4
12:04 $x_5$ 52
12:05 $x_6$ 22
  • 기존의 부스팅 기법은 모든 학습 데이터(x1~x10)까지의 잔차를 일괄 계산한다. 반면, CatBoost의 과정은 다음과 같다.

    • 1) 먼저 $x_{1}$의 잔차만 계산하고, 이를 기반으로 모델을 만든다. 그리고 $x_{2}$의 잔차를 이 모델로 예측한다.
    • 2) $x_{1}, x_{2}$의 잔차를 가지고 모델을 만든다. 이를 기반으로 $x_{3}, x_{4}$의 잔차를 모델로 예측한다.
    • 3) $x_{1}, x_{2}, x_{3}, x_{4}$를 가지고 모델을 만든다. 이를 기반으로 $x_{5}, x_{6}, x_{7}, x_{8}$의 잔차를 모델로 예측한다.
    • 4) 반복
  • 위와 같이 순서에 따라 모델을 만들고 예측하는 방식을 Ordered Boosting이라고 부른다.

Random Permutation

  • 위에서 Ordered Boosting을 할 때, 데이터 순서를 섞어주지 않으면 매번 같은 순서대로 잔차를 예측하는 모델을 만들 가능성이 있다. 그러므로 이 순서를 위사 임의로 랜덤하게 섞어주어야 한다. CatBoost는 이러한 점을 감안해서 데이터를 셔플링하여 뽑아낸다. 뽑아낼 때도 역시 모든 데이터를 뽑는게 아니라, 그 중 일부만 가져오게 할 수 있다. 이 모든 기법이 overffiting을 방지하기 위해 tree를 다각적으로 만들려는 시도인 것이다.

Ordered Target Encoding

  • Target Encoding, Mean Encoding, Response Encoding이라고 불리는 데 모두 다 같은 개념을 지칭하는 용어이다.
  • 범주형 변수를 수로 인코딩 시키는 방법 중, 비교적 가장 최근에 나온 기법인데, 간단한 설명을 하면 다음과 같다.
time feature class_labels(max_temperature on that day)
sunday sunny 35
monday sunny 32
tuesday cloudy 15
wednesday cloudy 14
thursday mostly_cloudy 10
friday cloudy 20
saturday cloudy 25
  • 위 데이터에서 time, feature로 class_label을 예측한다고 가정해보자. feature의 cloudy는 다음과 같이 인코딩 할 수 있다.
    • 즉, cloudy를 cloudy를 가진 데이터들의 class_label의 값의 평균으로 인코딩하는 것이다. 이러한 이유로 Mean encoding이라 불리기도 한다.
  • 그런데, 위에서 우리가 예측하는 값이 Train set feature에 들어가버리는 문제, 즉 Data Leakage 문제를 일으킨다. 이는 overfitting을 발생시키는 주 원인이자, Mean encoding 방법 자체의 문제이기도 하다. 그래서 CatBoost는 이에 대한 해결책으로, 현재 데이터의 인코딩을 위해 이전 데이터들의 인코딩된 값을 사용한다.

  • 즉, 현재 데이터의 Target값을 사용하지 않고, 이전 데이터들의 Target값 만을 사용하니, Data Leakage가 일어나지 않는 것이다. 물론 data 중 이미 평균값과 동일한 값이 존재 한다면 사용할 수 없을 것이다.

Categorical Feature Combinations

  • 아래 데이터의 경우에는 country만 보아도 hair_color feature를 알 수 있기 때문에, class_label을 예측하는데 있어, 두 feature 다 필요 없이 이 중 하나의 feature만 있으면 된다. CatBoost는 이렇게 information gain이 동일한 두 feature를 하나의 feature로 묶어버린다. 결과적으로, 데이터 전처리에 있어 feature selection에 대해 부담이 줄어들 수 있다고 볼 수 있다.
country hair clolor class_label
India Black 1
India Black 1
India Black 1
India Black 1
russia white 0
russia white 0
russia white 0

One-hot Encoding

  • 범주형 변수를 항상 Target Encoding하는 것은 아니다. Catboost는 낮은 Cardinality를 가지는 범주형 변수에 한해서, 기본적으로 One-hot encoding을 시행한다. Cardinality 기준은 one_hot_max_size 파라미터로 설정할 수 있다.
  • 예를 들어, one_hot_max_size = 3으로 설정한 경우, Cardinality가 3이하인 범주형 변수들은 Target Encoding이 아니라 One-hot Encoding으로 변환한다. 낮은 개수를 갖는 범주형 변수의 경우 One-hot Encoding이 더 효율적이라 그런 것이라 생각든다.

Optimized Parameter tuning

  • CatBoost는 파라미터들의 default 값이 기본적으로 최적화가 잘 되어서, 파라미터 튜닝에 크게 신경쓰지 않아도 된다고한다. 물론 데이터 마다 다르기 때문에 튜닝을 해보는 것이 좋긴하다.

CatBoost의 한계

  • Sparse한 Matrix는 처리하지 못한다.
  • 데이터 대부분이 수치형 변수인 경우, LightGBM보다 학습 속도가 느리다.그러므로 대부분이 범주형 변수인 경우에만 사용하는 것을 추천한다.

Ensemble 실습


1
2
3
4
5
import os
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

데이터 info

  • id: 고유 아이디
  • feat_1 ~ feat_93: 설명변수
  • target: 타겟변수 (1~9)

1
2
3
# 데이터 불러오기
data = pd.read_csv("../data/otto_train.csv") # Product Category
data.head() # 데이터 확인


1
2
3
nCar = data.shape[0] # 데이터 개수
nVar = data.shape[1] # 변수 개수
print('nCar: %d' % nCar, 'nVar: %d' % nVar )
결과
1
nCar: 61878 nVar: 95

의미가 없다고 판단되는 변수 제거


1
data = data.drop(['id'], axis = 1) # id 제거

타겟 변수의 문자열을 숫자로 변환


1
2
3
4
5
6
7
8
9
10
mapping_dict = {"Class_1": 1,
"Class_2": 2,
"Class_3": 3,
"Class_4": 4,
"Class_5": 5,
"Class_6": 6,
"Class_7": 7,
"Class_8": 8,
"Class_9": 9}
after_mapping_target = data['target'].apply(lambda x: mapping_dict[x])

설명변수와 타겟변수를 분리, 학습데이터와 평가데이터 분리


1
2
3
4
5
feature_columns = list(data.columns.difference(['target'])) # target을 제외한 모든 행
X = data[feature_columns] # 설명변수
y = after_mapping_target # 타겟변수
train_x, test_x, train_y, test_y = train_test_split(X, y, test_size = 0.2, random_state = 42) # 학습데이터와 평가데이터의 비율을 8:2 로 분할|
print(train_x.shape, test_x.shape, train_y.shape, test_y.shape) # 데이터 개수 확인
결과
1
(49502, 93) (12376, 93) (49502,) (12376,)

1. XGBoost


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# !pip install xgboost
import xgboost as xgb
import time
start = time.time() # 시작 시간 지정
xgb_dtrain = xgb.DMatrix(data = train_x, label = train_y) # 학습 데이터를 XGBoost 모델에 맞게 변환
xgb_dtest = xgb.DMatrix(data = test_x) # 평가 데이터를 XGBoost 모델에 맞게 변환
xgb_param = {'max_depth': 10, # 트리 깊이
'learning_rate': 0.01, # Step Size
'n_estimators': 100, # Number of trees, 트리 생성 개수
'objective': 'multi:softmax', # 목적 함수
'num_class': len(set(train_y)) + 1} # 파라미터 추가, Label must be in [0, num_class) -> num_class보다 1 커야한다.
xgb_model = xgb.train(params = xgb_param, dtrain = xgb_dtrain) # 학습 진행
xgb_model_predict = xgb_model.predict(xgb_dtest) # 평가 데이터 예측
print("Accuracy: %.2f" % (accuracy_score(test_y, xgb_model_predict) * 100), "%") # 정확도 % 계산
print("Time: %.2f" % (time.time() - start), "seconds") # 코드 실행 시간 계산
결과
1
2
Accuracy: 76.67 %
Time: 20.48 seconds

2. LightGBM


1
2
3
4
5
6
7
8
9
10
11
12
13
# !pip install lightgbm
import lightgbm as lgb
start = time.time() # 시작 시간 지정
lgb_dtrain = lgb.Dataset(data = train_x, label = train_y) # 학습 데이터를 LightGBM 모델에 맞게 변환
lgb_param = {'max_depth': 10, # 트리 깊이
'learning_rate': 0.01, # Step Size
'n_estimators': 100, # Number of trees, 트리 생성 개수
'objective': 'multiclass', # 목적 함수
'num_class': len(set(train_y)) + 1} # 파라미터 추가, Label must be in [0, num_class) -> num_class보다 1 커야한다.
lgb_model = lgb.train(params = lgb_param, train_set = lgb_dtrain) # 학습 진행
lgb_model_predict = np.argmax(lgb_model.predict(test_x), axis = 1) # 평가 데이터 예측, Softmax의 결과값 중 가장 큰 값의 Label로 예측
print("Accuracy: %.2f" % (accuracy_score(test_y, lgb_model_predict) * 100), "%") # 정확도 % 계산
print("Time: %.2f" % (time.time() - start), "seconds") # 코드 실행 시간 계산
결과
1
2
Accuracy: 73.57 %
Time: 8.46 seconds

3. Catboost


1
2
3
4
5
6
7
8
9
10
11
12
13
# !pip install catboost
import catboost as cb
start = time.time() # 시작 시간 지정
cb_dtrain = cb.Pool(data = train_x, label = train_y) # 학습 데이터를 Catboost 모델에 맞게 변환
cb_param = {'max_depth': 10, # 트리 깊이
'learning_rate': 0.01, # Step Size
'n_estimators': 100, # Number of trees, 트리 생성 개수
'eval_metric': 'Accuracy', # 평가 척도
'loss_function': 'MultiClass'} # 손실 함수, 목적 함수
cb_model = cb.train(pool = cb_dtrain, params = cb_param) # 학습 진행
cb_model_predict = np.argmax(cb_model.predict(test_x), axis = 1) + 1 # 평가 데이터 예측, Softmax의 결과값 중 가장 큰 값의 Label로 예측, 인덱스의 순서를 맞추기 위해 +1
print("Accuracy: %.2f" % (accuracy_score(test_y, cb_model_predict) * 100), "%") # 정확도 % 계산
print("Time: %.2f" % (time.time() - start), "seconds") # 코드 실행 시간 계산
결과
1
2
3
4
5
6
7
8
9
10
11
12
90:	learn: 0.6928407	total: 53s	remaining: 5.24s
91: learn: 0.6930427 total: 53.5s remaining: 4.66s
92: learn: 0.6935073 total: 54.1s remaining: 4.07s
93: learn: 0.6940932 total: 54.6s remaining: 3.49s
94: learn: 0.6944972 total: 55.2s remaining: 2.9s
95: learn: 0.6948810 total: 55.7s remaining: 2.32s
96: learn: 0.6951840 total: 56.3s remaining: 1.74s
97: learn: 0.6954264 total: 56.8s remaining: 1.16s
98: learn: 0.6955881 total: 57.4s remaining: 580ms
99: learn: 0.6956285 total: 57.9s remaining: 0us
Accuracy: 69.64 %
Time: 58.31 seconds

Stacking

  • 스태킹(Stacking)은 개별적인 여러 알고리즘을 서로 결합해 예측 결과를 도출한다는 점에서 앞서 소개한 Bagging 및 Boosting과 공통정을 가지고 있다. 하지만 가장 큰 차이점은 개별 알고리즘으로 예측한 데이터를 기반으로 다시 예측을 수행한다는 점이다. 즉, 개별 알고리즘의 예측 결과 데이터 세트를 최종적인 메타 데이터 세트로 만들어 별도의 ML 알고리즘으로 최종 학습을 수행하고 테스트 데이터를 기반으로 다시 최종 예측을 수행하는 방식이다. 이렇게 개별 모델의 예측된 데이터 세트를 기반으로 학습하고 예측하는 방식을 Meta Model이라고 한다.

  • 스태킹을 현실 모델에 적용하는 경우는 그리 많지 않다. 그러나 캐글과 같은 대회에서 높은 순위를 차지하기 위해 조금이라도 성능 수치를 높여야 할 경우 자주 사용된다. 스태킹을 적용할 때는 많은 개별 모델이 필요하다. 2~3개의 개별 모델만을 결합해서는 쉽게 예측 성능을 향상 시킬 수 없으며, 스태킹을 적용한다고 해서 반드시 성능이 향상 되리라는 보장도 없다.

  • 위의 Ensemble 방법들과 다르게 python의 내장 모듈로 되어있지는 않다. 그러므로 직접 코딩을 해서 사용해야 한다. 학습 데이터에 대해 단일 모델만 사용하는 것이 아니라 여러 모델을 사용한다. 이렇게 추정한 학습 모델을 통해 학습데이터를 예측한다. 이렇게 얻게 된 각 모델의 예측결과를 다시 독립 변수로 사용한다. 해당 독립 변수들을 통해 검증 데이터를 예측하여 성능을 측정한다.

Stacking이란?

  • Stacking을 할 때 k-fold(보통 k=5 or 10)를 통해 학습 데이터와 검증데이터를 나누어 주어야 한다. 또한 학습하는데 굉장히 오랜 시간이 소요되므로 비효율적인 모델이긴 하지만 성능은 좋다. 그러므로 캐글 같이 성능을 올려야만 성적이 좋아지는 상황이 아닌 현업에서 사용하기에는 굉장히 무리가 있다. 그러므로 성능이 중요한 상황에서 최후의 보루로 생각하고 있는 것이 좋다.
  • 아래의 그림에서 예를 들어 설명하자면 다음과 같다. 먼저 train과 test 데이터를 각각 5-fold로 나누어 준다. 모델은 총 4가지로 SVM, KNN, RF, GBM을 사용할 것이다. 4가지 모델에 대해 5번 학습을 하므로 총 20번을 학습해야 한다. 그러므로 그에 따른 소요시간은 엄청날 것이다. 이렇게 학습한 모델을 통해 train 데이터 중 학습에 사용되지 않은 데이터를 예측하여 새로운 feature들을 얻는다. 본래 데이터의 feature는 2개 였지만 각 모델의 예측값을 통해 새로운 feature 4개를 얻게 된다. Test 데이터에 대해서도 학습한 모델을 통해 예측한 결과를 새로운 feature로 사용한다.

Stacking이란? - 01

Stacking이란? - 02

Stacking이란? - 03

  • 아래와 같이 학습한 모델들의 예측값들을 새로운 feature로 사용하여 기존의 train 데이터와 test 데이터에 merge 시켜준다.

Stacking이란? - 04

Stacking이란? - 05

  • 새롭게 얻은 train 데이터를 모델에 적합시키고 test 데이터를 예측하여 성능을 계산한다.

Stacking이란? - 06

  • 다음과 같이 각 모델별 prediction을 통해 얻은 feature들만을 사용하기도 한다.

Stacking이란? - 07

기본 스태킹 모델 실습


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np

from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

cancer_data = load_breast_cancer()
X_data = cancer_data.data
y_label = cancer_data.target

X_train, X_test, y_train, y_test = train_test_split(X_data, y_label, test_size=0.2, random_state=0)

개별 ML 모델 생성 및 메타 모델 생성

1
2
3
4
5
6
7
8
# weak_learners
knn_clf = KNeighborsClassifier(n_neighbors=4)
rf_clf = RandomForestClassifier(n_estimators=100, random_state=0)
dt_clf = DecisionTreeClassifier()
ada_clf = AdaBoostClassifier(n_estimators=100)

# meta Model
lr_final = LogisticRegression(C=10)

개별 모델 학습

1
2
3
4
knn_clf.fit(X_train, y_train)
rf_clf.fit(X_train, y_train)
dt_clf.fit(X_train, y_train)
ada_clf.fit(X_train, y_train)

개별 모델들이 반환하는 예측 데이터와 각 모델 별 정확도 측정

1
2
3
4
5
6
7
8
9
knn_pred = knn_clf.predict(X_test)
rf_pred = rf_clf.predict(X_test)
dt_pred = dt_clf.predict(X_test)
ada_pred = ada_clf.predict(X_test)

print('KNN 정확도: {0:.4f}'.format(accuracy_score(y_test, knn_pred)))
print('Random Forest 정확도: {0:.4f}'.format(accuracy_score(y_test, rf_pred)))
print('Decision Tree 정확도: {0:.4f}'.format(accuracy_score(y_test, dt_pred)))
print('AdaBoost 정확도: {0:.4f}'.format(accuracy_score(y_test, ada_pred)))
결과
1
2
3
4
KNN 정확도: 0.9211
Random Forest 정확도: 0.9649
Decision Tree 정확도: 0.9123
AdaBoost 정확도: 0.9561

  • 개별 알고리즘으로부터 예측된 예측값을 column level로 옆으로 붙여서 피처 값으로 만들어, 최종 메타 모델인 로지스틱 회귀에서 학습 데이터로 사용할 것이다. 반환된 예측 데이터 세트는 1차원 형태의 ndarray이므로 먼저 반환된 예측 결과를 행 형태로 붙인 뒤, numpy의 transpose()를 이용해 행과 열 위치를 바꾼 ndarray로 변환하면 된다.

1
2
3
4
pred=np.array([knn_pred, rf_pred, dt_pred, ada_pred]).T
lr_final.fit(pred, y_test)
final=lr_final.predict(pred)
print("최종 메타 모델의 예측 정확도 : {:.4f}".format(accuracy_score(y_test, final)))
결과
1
최종 메타 모델의 예측 정확도 : 0.9737

CV 기반의 스태킹

  • 과적합(overfitting) 방지를 위한 CV세트 기반의 스태킹모델을 살펴보겠다. 앞의 마지막 메타 모델인 로지스틱 회귀 모델을 학습할 때 label 데이터 세트로 학습데이터가 아닌 테스트용 label 데이터 세트를 기반으로 학습했기 때문에 과적합 문제가 발생할 수 있다.

  • 이는 다음과 같이 2단계의 step으로 구분될 수 있다.

    • step 1) 각 모델별로 원본 train/test 데이터를 예측한 결과 값을 기반으로 메타 모델을 위한 train/test용 데이터를 생성한다.
    • step 2) step 1에서 개별 모델들이 예측한 train용 데이터를 모두 스태킹 형태로 합쳐서 메타 모델이 학습할 최종 train 데이터 세트를 생성한다. 마찬가지로 각 모델들이 예측한 test용 데이터를 모두 스태킹 형태로 합쳐서 메타 모델이 예측할 최종 테스트 데이터 세트를 생성한다. 메타 모델은 최종적으로 생성된 train 데이터 세트와 원본 train 데이터의 label 데이터를 기반으로 학습한 뒤, 최종적으로 생성된 test용 데이터 세트를 예측하고, 원본 test 데이터의 label 데이터를 기반으로 평가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from sklearn.model_selection import KFold
from sklearn.model_selection import StratifiedKFold

def get_stacking_base_datasets(model, X_train_n, y_train_n, X_test_n, n_folds):
# 지정된 n_folds값으로 KFold 생성
kf=KFold(n_splits=n_folds, shuffle=False, random_state=0)
# 추후에 meta model이 사용할 학습 데이터 반환을 위한 numpy array 초기화
train_fold_pred = np.zeros((X_train_n.shape[0], 1))
test_pred = np.zeros((X_test_n.shape[0], n_folds))
print(model.__class__.__name__, 'model 시작')

for folder_counter, (train_index, valid_index) in enumerate(kf.split(X_train_n)):
# 입력된 학습 데이터에서 기반 모델이 학습/예측할 폴드 데이터 세트 추출
print('\t 폴드 세트:', folder_counter, '시작')
X_tr = X_train_n[train_index]
y_tr = y_train_n[train_index]
X_te = X_train_n[valid_index]

# 폴드 세트 내부에서 다시 만들어진 학습 데이터로 기반 모델의 학습 수행
model.fit(X_tr, y_tr)

# 폴드 세트 내부에서 다시 만들어진 검증 데이터로 기반 모델 예측 후 데이터 저장
train_fold_pred[valid_index, :]=model.predict(X_te).reshape(-1,1)

# 입력된 원본 테스트 데이터를 폴드 세트내 학습된 기반 모델에서 예측 후 데이터 저장
test_pred[:, folder_counter] = model.predict(X_test_n)

# 폴드 세트 내에서 원본 테스트 데이터를 예측한 데이터를 평균하여 테스트 데이터 생성
test_pred_mean = np.mean(test_pred, axis=1).reshape(-1, 1)

# train_fold_pred는 최종 메타 모델이 사용하는 학습 데이터, test_pred_mean은 테스트 데이터
return train_fold_pred, test_pred_mean


1
2
3
4
knn_train, knn_test = get_stacking_base_datasets(knn_clf, X_train, y_train, X_test, 7)
rf_train, rf_test = get_stacking_base_datasets(rf_clf, X_train, y_train, X_test, 7)
dt_train, dt_test = get_stacking_base_datasets(dt_clf, X_train, y_train, X_test, 7)
ada_train, ada_test = get_stacking_base_datasets(ada_clf, X_train, y_train, X_test, 7)
결과
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
KNeighborsClassifier model 시작
폴드 세트: 0 시작
폴드 세트: 1 시작
폴드 세트: 2 시작
폴드 세트: 3 시작
폴드 세트: 4 시작
폴드 세트: 5 시작
폴드 세트: 6 시작
RandomForestClassifier model 시작
폴드 세트: 0 시작
폴드 세트: 1 시작
폴드 세트: 2 시작
폴드 세트: 3 시작
폴드 세트: 4 시작
폴드 세트: 5 시작
폴드 세트: 6 시작
DecisionTreeClassifier model 시작
폴드 세트: 0 시작
폴드 세트: 1 시작
폴드 세트: 2 시작
폴드 세트: 3 시작
폴드 세트: 4 시작
폴드 세트: 5 시작
폴드 세트: 6 시작
AdaBoostClassifier model 시작
폴드 세트: 0 시작
폴드 세트: 1 시작
폴드 세트: 2 시작
폴드 세트: 3 시작
폴드 세트: 4 시작
폴드 세트: 5 시작
폴드 세트: 6 시작


1
2
3
4
Stack_final_X_train = np.concatenate((knn_train, rf_train, dt_train, ada_train), axis=1)
Stack_final_X_test = np.concatenate((knn_test, rf_test, dt_test, ada_test), axis=1)
print('원본 학습 피처 데이터 Shape:', X_train.shape, '원본 테스트 피처 Shape:', X_test.shape)
print('스태킹 학습 피처 데이터 Shape:', Stack_final_X_train.shape, '스태킹 테스트 피처 데이터 Shape:', Stack_final_X_test.shape)
결과
1
2
원본 학습 피처 데이터 Shape: (455, 30) 원본 테스트 피처 Shape: (114, 30)
스태킹 학습 피처 데이터 Shape: (455, 4) 스태킹 테스트 피처 데이터 Shape: (114, 4)


1
2
3
lr_final.fit(Stack_final_X_train, y_train)
stack_final = lr_final.predict(Stack_final_X_test)
print('최종 메타 모델의 예측 정확도:{0:.4f}'.format(accuracy_score(y_test, stack_final)))
결과
1
최종 메타 모델의 예측 정확도:0.9737