[ 핸즈 온 머신러닝 2 ] 앙상블 학습과 랜덤 포레스트란?
어제보다 나은 사람이 되기

걱정보단 실행을, 그러나 계획적으로

Box World 자세히보기

AI/Hands-On Machine Learning 2판

[ 핸즈 온 머신러닝 2 ] 앙상블 학습과 랜덤 포레스트란?

Box형 2021. 2. 4. 01:10
반응형

매일 아침 삶의 목표를 생각하며 일어나라

- 아이제이아 토마스 (NBA 아이닉스 사장 겸 감독) -


시작하며

 굉장히 복잡한 질문에 대해서 수천명의 사람들에게 대답을 모으는 것과 전문가에게 답을 듣는 것, 둘 중 어떤 것이 더 정확할까요? 대부분 전자가 더 정확하며 우리는 이를 대중의 지혜라고 합니다.

 마찬가지로 머신러닝에서도 적당한 성능의 모델 여러개들로부터 예측을 수집하는 것이 가장 좋은 하나의 모델의 예측보다 더 좋은 결과를 불러옵니다.

 우리는 이러한 학습 방법을 앙상블 학습(Ensemble Learning)이라고 합니다.

 이번 포스팅은 Decision Tree의 이해가 바탕이 되어야 합니다. 다음 포스팅을 참고해주세요!

[AI/Hands-On Machine Learning 2판] - [ 핸즈온 머신러닝 2 ] Decision Tree란?

 

[ 핸즈온 머신러닝 2 ] Decision Tree란?

성공하려면 이미 했던 일을 제대로 활용하라 - 블레이크 로스 (파이어폭스 공동 개발자) - 시작하며  저번 포스팅에서는 SVM에 대해 다뤄보았습니다. 이번 포스팅에서 다룰 Decision Tree은 SVM처럼 Cl

box-world.tistory.com


7.1 투표 기반 Classifier

 여기 Logistic Regression, SVM, Random Forest 등 정확도가 80%인 여러개의 Classifier가 있습니다. 

 그리고 앙상블 학습에서는 각 Classifier의 예측을 모아서 가장 많이 선택된 클래스를 예측합니다. 이렇게 다수결 투표로 정해지는 Classifier를 직접 투표(hard vote) 분류기라고 합니다.

 주목할 점은 이 다수결 투표 Classifier가 앙상블에 포함된 각각의 Classifier들 중 가장 뛰어난 Classifier보다도 정확도가 높은 경우가 많다는 것입니다. 즉 각 Classifier의 성능은 약할지언정, 개수가 충분히 많은 앙상블이라면 성능이 높을 수 있습니다.

 이것이 가능한 이유는 바로 큰수의 법칙 때문입니다. 예를 들어 앞면이 51%, 뒷면이 49%가 나오는 불균형한 동전이 있다고 가정하겠습니다.

 만약 동전을 1000번 던지면 510번은 앞면, 490번은 뒷면이 나올 것인데, 이를 수학적으로 계산하면 1000번을 던질 때 앞면이 다수일 확률은 75%에 가깝다는 것을 확인할 수 있습니다. 게다가 더 많이 던질 수록 앞면이 나올 확률은 증가합니다.(10000번일 경우 97% 이상으로 올라갑니다.)

 이와 비슷하게 51% 정확도를 가진 1000개의 분류기로 앙상블을 구축한고, 여기에서 가장 많이 나온 클래스를 예측으로 삼는다면 75%의 정확도를 기대할 수 있습니다. 물론 이러한 가정은 모든 Classifier가 완벽하게 독립적이고, 오차에 상관관계가 없어야 가능합니다.

더보기

앙상블 학습은 Classifier가 가능한 서로 독립적일 때 최고의 성능을 발휘합니다. 이를 위해 가장 좋은 방법은 각 Classifier를 서로 다른 알고리즘으로 학습하는 것입니다. 

 다음은 moons 데이터셋에 대해서 여러 Classifier를 조합한 투표 기반 분류기 VotinClassifier에 대한 코드입니다.

from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC

log_clf = LogisticRegression(random_state=42)
rnd_clf = RandomForestClassifier(random_state=42)
svm_clf = SVC(random_state=42)

voting_clf = VotingClassifier(
    estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)],
    voting='hard')
voting_clf.fit(X_train, y_train)

앙상블 내 들어있는 Classifier의 Testset 정확도를 확인해보겠습니다. 예상대로 VotingClassifier가 개별 Classifier보다 성능이 높은걸 확인할 수 있습니다.

from sklearn.metrics import accuracy_score

for clf in (log_clf, rnd_clf, svm_clf, voting_clf):
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)
    print(clf.__class__.__name__, accuracy_score(y_test, y_pred))
    
>>
LogisticRegression 0.864
RandomForestClassifier 0.872
SVC 0.888
VotingClassifier 0.896

 앙상블 내 모든 Classifier가 클래스의 확률을 예측할 수 있다면(predict_proba() 메서드가 있다면), 개별 Classifier의 예측을 평균 내어 확률이 가장 높은 클래스를 예측할 수 있는데, 이를 간접 투표(soft voting)이라고 합니다.

 이 방식은 확률 기반이므로 직접 투표(hard voting) 방식보다 성능이 높습니다. 간접 투표 방식을 사용하기 위해선 voting="hard"를  "soft"로 바꿔주면 됩니다.

 SVC(SVM Classifier)에서는 클래스 확률을 제공하지 않으므로 probability 매개변수를 True로 지정하면 됩니다.

 

 

 

 


7.2 Bagging과 Pasting

 다양한 Classifier를 만드는 한 가지 방법은 각기 다른 Training 알고리즘을 사용하는 것이고, 다른 하나는 같은 알고리즘을 사용하되 Training datset의 subset을 무작위로 구성하여 Classifier마다 다른 데이터로 학습 시키는 것입니다.

 이때 Training dataset에서 중복을 허용하여 subset을 나눠 학습하는 것을 배깅(Bagging), 중복을 허용하지 않는 것을 페이스팅(Pasting)이라고 합니다.

 다시 말해서 하나의 Training data가 하나의 Classifier를 위해 여러 번 학습에 사용될 수 있는건 Bagging뿐입니다.

 Classifier와 같은 모든 예측기가 학습을 마치면, 앙상블은 이들의 예측을 모아 새로운 data에 대한 예측을 만듭니다. 이때 최종 예측을 하는 수집함수는 Classification일 땐, Hard Voting Classifier처럼 가장 많은 예측 결과를 따르고, Regression에 대해선 평균을 계산합니다.

 원본 데이터 전체이 아닌, subset을 학습한 개별 Classifier는 크게 편향이 되어있지만(Underfit), 수집 함수를 통과하면 편향과 분산이 모두 감소합니다. 

 그리고 앙상블의 결과는 원본 데이터로 하나의 예측기를 훈련시킬 때보다, 편향은 비슷하지만 분산은 줄어듭니다(Overfitting이 덜 된다).


7.2.1 sklearn의 Bagging과 Pasting

 sklearn에서는 Bagging과 Pasting을 위해 BaggingClassifier(회귀의 경우 BaggingRegressor)를 제공합니다. 다음은 Decision Tree 500개로 구성로 구성된 앙상블을 훈련시키는 코드입니다. 각 Classifier는 Bagging으로 훈련됩니다.

  • Pasting을 사용하려면 bootstrap = False로 지정합니다.
  • n_jobs는 sklearn에서 훈련과 예측에 사용할 CPU 코어 수를 지정하는데, -1로 설정 시 가용한 모든 코어를 사용합니다.
  • BaggingClassifier는 앙상블에 사용되는 Classifier가 Decision Tree처럼 클래스 확률을 추정할 수 있다면, Soft Voting 방식을 사용합니다.
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier

bag_clf = BaggingClassifier(
    DecisionTreeClassifier(random_state=42), n_estimators=500,
    max_samples=100, bootstrap=True, n_jobs=-1, random_state=42)
bag_clf.fit(X_train, y_train)
y_pred = bag_clf.predict(X_test)

 다음은 하나의 Decision Tree의 결정 경계와 앞서 본 코드로 학습한 Bagging 앙상블의 결정 경계를 비교한 것입니다. Training set의 오차 수는 서로 비슷할지 모르나, 경계가 덜 불규칙한 앙상블의 예측이 훨씬 더 일반화를 잘할 것이라고 생각할 수 있습니다.

  Bootstrapping 즉 Bagging을 중복을 허용하는 특성 상 subset에 다양성을 증가시키므로 편향이 좀 더 높습니다.(More Underfitting) 그러나 다양성을 추가한다는 것은 예측기 간 상관관계를 줄이므로 앙상블의 분산을 감소시킵니다.(Less Overfitting)

 이러한 탓에 일반적으로는 Bagging을 더 많이 선호합니다. 편향, 분산 그리고 Overfit과 Underfit에 대한 개념이 궁금하신 분들은 다음 포스팅을 참고해주세요.

[AI/Coursera ( Machine Learning )] - [머신러닝] 모델의 과적합(Overfitting)을 피하기 위한 방법

 

[머신러닝] 모델의 과적합(Overfitting)을 피하기 위한 방법

시작하며 우리는 지금까지 Linear Regression 그리고 Logistic Regression 두가지에 중점을 두어 Supervised Learning을 공부하였습니다. 이번 포스팅에서는 모델 학습 과정에서 발생할 수 있는 'Overfitting' 이..

box-world.tistory.com


7.2.2 oob 평가

 Bagging을 사용하면 어떤 데이터는 여러 번 사용되고, 어떤 것은 전혀 선택되지 않을 수 있습니다. 앞서 사용한 BaggingClassifier는 평균적으로 각 에측기에 Training dataset의 63% 정도만 사용하는데, 이때 사용되지 않은 나머지 data를 oob(out of bag) 데이터라고 합니다.

 이렇게 남겨진 oob data들은 별도의 Validation set 없이 각 예측기를 평가하는데 사용됩니다. 앙상블 자체의 평가는 각 예측기의 oob 평가를 평균하여 얻습니다.

 sklearn에서는 oob_score=True로 지정하면, 자동으로 oob 평가를 수행합니다.

bag_clf = BaggingClassifier(
    DecisionTreeClassifier(random_state=42), n_estimators=500,
    bootstrap=True, n_jobs=-1, oob_score=True, random_state=40)
bag_clf.fit(X_train, y_train)

print('oob score :', bag_clf.oob_score_)

>> 
oob score : 0.9013333333333333

 oob 데이터 그리고 Test 데이터 이 둘은 모두 BaggingClassifier가 보지 못했던 데이터들이기 때문에 oob score와 Test  set의 accuracy의 정확도는 매우 유사할 것으로 보입니다.

 그리고 oob_decision_function을 이용하면, 각 Training data의 클래스 확률을 반환합니다.

bag_clf.oob_decision_function_[:5]

>>
array([[0.31746032, 0.68253968],
       [0.34117647, 0.65882353],
       [1.        , 0.        ],
       [0.        , 1.        ],
       [0.        , 1.        ]])

7.3 랜덤 패치와 랜덤 서브스페이스

 BaggingClassifier에서는 각 예측기를 학습하는데 일부 데이터만 쓰는 기능도 있지만, 일부 feature만 반영시키는 기능도 지원합니다. 예를 들어 데이터가 10개의 feature를 가지고 있는데, 5개의 feature만 학습에 사용하는 방식입니다.

 이러한 기법은 매우 고차원의 데이터셋을 다룰 때 유용합니다. feature와 데이터를 모두 일부만 사용하는 것을 랜덤 패치 방식(Random Patches Method)라고 합니다. 그리고 데이터는 모두 사용하고, feature만 일부를 사용하는 방식을 랜덤 서브스페이스 방식(Random Subspace Method)라고 합니다.

 

 

 

 


7.4 Random Forest

 랜덤 포레스트(Random Forest)는 Bagging 혹은 Pasting을 적용한 Decision Tree의 앙상블입니다. 일부만 사용할 Training set의 크기는 max_samples로 지정합니다.

from sklearn.ensemble import RandomForestClassifier

rnd_clf = RandomForestClassifier(n_estimators=500, max_leaf_nodes=16, n_jobs=-1, random_state=42)
rnd_clf.fit(X_train, y_train)
y_pred_rf = rnd_clf.predict(X_test)

 Random Forest 알고리즘은 트리의 노드를 분할할 때, 전체 feature 중 최선의 feature를 찾는 대신 무작위로 선택한 feature 후보들 중 최적의 feature를 찾음으로써 무작위성을 더 주입합니다.

 이러한 무작위성은 트리를 더욱 다양하게 만들고, 이러한 다양성은 편향을 감소시키지만(More Underfitting), 분산을 낮추어(Less Overfitting) 더 훌륭한 모델을 만들어냅니다. 다음은 BaggingClassifier로 RandomForestClassifier와 거의 유사하게 만든 것입니다.

bag_clf = BaggingClassifier(
    DecisionTreeClassifier(splitter="random", max_leaf_nodes=16, random_state=42),
    n_estimators=500, max_samples=1.0, bootstrap=True, n_jobs=-1, random_state=42)
bag_clf.fit(X_train, y_train)
y_pred = bag_clf.predict(X_test)

7.4.1 엑스트라 트리

 앞서 본 Random Forest는 각 노드의 분할을 위해 feature의 subset을 사용했습니다. 트리를 더욱 무작위하게 만들기 위해 최적의 임곗값 $t_k$를 찾는 대신 feature의 subset을 사용해 무작위로 분할 후 그중 최상의 분할을 선택합니다.

 이렇게 극단적으로 무작위한 Random Forest를 Extreme random Tree 앙상블 혹은 Extra tree라고 부릅니다. 이 역시 무작위성은 다양성을 늘려 편향을 늘리는 대신 분산을 줄이게 됩니다.

 기존엔 노드 분할 시 최적의 임곗값을 찾는 것이 트리 알고리즘에서 가장 많이 시간을 잡아먹는 부분이었는데 이것이 없어졌으므로 일반적인 Random Forest보다 Extra Tree가 훨씬 빠릅니다.

 sklearn에서는 ExtraTreesClassifier를 사용해 이를 구현할 수 있으며 기타 메서드나 파라미터는 동일합니다.


7.4.2 특성 중요도

 Random forest의 또 다른 장점은 어떤 feature가 예측에 중요한 비중을 차지하는지 상대적인 중요도를 측정하기 쉽다는 것입니다.

 sklearn에서는 어떤 feature를 사용한 노드가 gini(불순도)를 감소시키는지 확인하여 feature의 중요도를 측정합니다. 더 정확히는 가중치의 평균이며 각 노드의 가중치는 연관된 Training data 수와 같습니다.

 훈련이 끝난 뒤 feature마다 자동으로 이 점수를 계산하고 중요도의 전체 합이 1이 되도록 결과값을 정규화하여 이를 feature_importances_ 변수에 저장합니다.

 다음은 Iris 데이터셋을 이용한 코드 적용입니다. 수치를보면 petal length와 petal width가 가장 중요한 특성들이라는 걸 알 수 있습니다.

from sklearn.datasets import load_iris

iris = load_iris()
rnd_clf = RandomForestClassifier(n_estimators=500, n_jobs=-1, random_state=42)
rnd_clf.fit(iris["data"], iris["target"])
for name, score in zip(iris["feature_names"], rnd_clf.feature_importances_):
    print(name, score)
    
 >>
sepal length (cm) 0.11249225099876374
sepal width (cm) 0.023119288282510326
petal length (cm) 0.44103046436395765
petal width (cm) 0.4233579963547681

 이러한 특징은 이미지를 Classification하는데 모델이 어느 곳을 중점적으로 보는지 판단하는데 활용될 수 있습니다. 다음은 MNIST 데이터셋에서 Random Forest Classifier를 학습하고 각 픽셀의 중요도를 그래프로 나타낸 결과입니다.


7.5 Boosting

 부스팅(Boosting)은 약한 학습기를 여러 개 연결하여 강한 학습기를 만드는 앙상블 방법을 말합니다. 여기에는 AdaBoostGradient Boosting이 있습니다.


7.5.1 AdaBoost

AdaBoost이전 모델이 Underfitting했던 training data의 가중치를 더 높이며 새로운 모델을 만듭니다. 이렇게 하면 새로운 예측기는 학습하기 어려운 샘플에 점점 더 맞춰지게 됩니다.

 예를 들어 AdaBoost Classifier를 만들 때 먼저 Decision tree와 같은 첫 번째 Classifier를 Training set에서 훈련시키고 예측을 만듭니다. 그 다음 알고리즘이 잘못 분류했던 Training data의 가중치를 높입니다. 

 이것이 반영된 두번째 Classifier에서는 업데이트된 가중치로 Training set을 학습하고 예측하고, 나머지 과정은 반복되는 식입니다.

 다음은 AdaBoost방식으로 갱신되어 가는 다섯개의 연속된 예측기의 결정 경계입니다. moons 데이터셋을 사용하였고, 모델은 규제를 강하게 한 RBF 커널 SVM Classifier입니다. Classifier의 성능이 가면 갈 수록 좋아지는 것을 확인할 수 있습니다.


 AdaBoost 알고리즘을 좀 더 자세히 들여다보겠습니다. 각 데이터 가중치 $w^{(i)}$는 초기에 $\cfrac{1}{m}$으로 초기화 됩니다. 이후 첫 번째 예측기가 학습되고, 가중치가 적용된 에러율 $r_1$이 계산됩니다.

 두 번째 예측기의 가중치 $\alpha_j$는 다음 식으로 계산됩니다. 여기서 n처럼 생긴 저 기호는 learning rate입니다. 예측기가 정확할수록 에러율은 낮으므로 $\alpha_j$은 높아지게 되고, 성능이 매우 안좋다면 음수까지도 갈 수 있습니다.

 그 다음 알고리즘은 다음 식을 사용해 잘못 예측한 경우에만 해당 데이터의 가중치를 $\alpha_j$를 이용해 갱신합니다.

 그 후 모든 데이터의 가중치를 정규화하는 식으로 반복됩니다. 이 것은 정해진 예측기의 수에 도달하거나, 완벽한 예측기가 만들어지면 중지됩니다.

 AdaBoost는 단순히 모든 예측기의 예측을 계산하고, 예측기 가중치 $\alpha_j$를 더해 예측 결과를 만듭니다. 그리고 가중치 합이 가장 큰 클래스가 예측 결과가 됩니다.

 다음은 sklearn의 AdaBoostClassifier를 사용하여 200개의 얕은 Decision tree를 기반으로 AdaBoost Classifier를 훈련시킵니다. 이때 Decision tree의 max_depth = 1입니다.

from sklearn.ensemble import AdaBoostClassifier

ada_clf = AdaBoostClassifier(
    DecisionTreeClassifier(max_depth=1), n_estimators=200,
    algorithm="SAMME.R", learning_rate=0.5, random_state=42)
ada_clf.fit(X_train, y_train)

 

 

 

 

 


7.5.2 Gradient Boosting 

 Gradient Boosting도 이전까지의 오차를 보정한 예측기가 순차적으로 앙상블에 추가됩니다. 다만 AdaBoost처럼 데이터의 가중치를 갱신하는 대신 이전 예측기가 만든 잔여 오차(residual error)를 새로운 예측기에 학습시킵니다.

 간단한 회귀 문제를 풀어보겠습니다. 이때 사용되는 모델을 Gradient tree boosting 혹은 GBRT라고 합니다. 우선 DecisionTreeRegressor에 Training set을 학습시키겠습니다.

from sklearn.tree import DecisionTreeRegressor

tree_reg1 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg1.fit(X, y)

 이제 Residual Error를 두번째 DecisionTreeRegressor에 훈련시킵니다.

y2 = y - tree_reg1.predict(X)  # residual errors

tree_reg2 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg2.fit(X, y2)

 이번엔 세 번째입니다.

y3 = y2 - tree_reg2.predict(X)  # residual error

tree_reg3 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg3.fit(X, y3)

 새로운 데이터에 대한 예측은 모든 트리의 예측을 더하면 됩니다.

y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))

다음 그림에서 왼쪽 열은 앙상블 내 세 트리의 예측이고, 오른쪽 열은 앙상블의 예측입니다.

  • 첫번째 행은 앙상블에 하나의 트리만 있기 때문에 첫 번째 트리의 예측과 완전히 같습니다.
  • 두번째 행의 앙상블은 첫 번째 트리의 잔여 오차를 학습하였습니다. 직관적으로 보면 앙상블의 예측이 $h_2(x_1)$와 잔여 오차를 더한 값임을 볼 수 있습니다.
  • 트리가 앙상블에 추가될 수록 앙상블의 예측은 더욱 좋아집니다.

 다음은 sklearn에서 GBRT 앙상블을 간단하게 훈련시키는 코드입니다. learning_rate 매개변수는 각 트리의 기여 정도를 조절합니다. 이것이 작을 수록 앙상블은 더욱 많은 트리를 필요로 하지만, 성능은 좋아지는데 이러한 규제 방법을 축소(shrinkage)라고 합니다.

from sklearn.ensemble import GradientBoostingRegressor

gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1, random_state=42)
gbrt.fit(X, y)

 다음 그래프에서 왼쪽은 트리가 너무 적어 Underfitting된 앙상블이고, 오른쪽은 반대로 너무 많아서 Overfitting된 앙상블입니다.

 다음은 최적의 트리 수를 찾기 위해 조기 종료 기법을 적용한 코드입니다.

from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=49)

gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=120, random_state=42)
gbrt.fit(X_train, y_train)

# 최적의 트리 개수 찾기
errors = [mean_squared_error(y_val, y_pred)
          for y_pred in gbrt.staged_predict(X_val)]
bst_n_estimators = np.argmin(errors)

# 최적의 트리개수로 그래디언트 부스팅 학습
gbrt_best = GradientBoostingRegressor(max_depth=2, n_estimators=bst_n_estimators, random_state=42)
gbrt_best.fit(X_train, y_train)

 위와 같이 많은 수의 트리를 먼저 훈련시키고 최적의 수를 찾는 대신 최적의 수라고 판단 시 실제로 훈련을 중도에 중지하는 방법으로도 구현될 수 있습니다. 다음 코드는 다섯 번의 반복 동안 Validation Error가 향상되지 않으면 훈련을 종료합니다.

gbrt = GradientBoostingRegressor(max_depth=2, warm_start=True, random_state=42)

min_val_error = float("inf")
error_going_up = 0
for n_estimators in range(1, 120):
    gbrt.n_estimators = n_estimators
    gbrt.fit(X_train, y_train)
    y_pred = gbrt.predict(X_val)
    val_error = mean_squared_error(y_val, y_pred)
    if val_error < min_val_error:
        min_val_error = val_error
        error_going_up = 0
    else:
        error_going_up += 1
        if error_going_up == 5:
            break  # 조기 종료

 GradientBoostingRegressor는 트리 훈련 시 사용할 Training subset의 크기를 subsample 매개변수로 지정할 수 있습니다. 이를 통해 편향은 높이고 분산은 줄일 수 있는데 이러한 기법을 확률적 그레디언트 부스팅(Stochastic gradient boosting)이라고 합니다.

 최적화된 Gradient Boosting 구현으로 가장 유명한 것은 XGBoost입니다. 중요한 개념이니 꼭 한번 찾아보시길 바랍니다!!


 다음 포스팅에서는 고차원 데이터셋을 다룰 때 자주 사용하는 차원 축소에 대해서 다뤄보겠습니다. 긴 글 읽어주셔서 감사합니다. 행복한 하루 보내시길 바랍니다 :)

반응형