[핸즈온 머신러닝] 7강 앙상블 학습과 랜덤 포레스트(ensemble, RandomForest)

2018. 7. 27. 13:57Python-이론/python-인공지능2

앙상블 학습과 랜덤 포레스트



앙상블 학습이란 하나로 이어진 여러개의 (SVM, 결정트리, 로지스틱 회귀, 경사하강법)예측기를 사용하여 분류, 회귀등을 하는 것을 앙상블학습이라고 부릅니다. 예를 들어 각기 다른 데이터 셋으로 여러개의 결정트리를 개별적으로 훈련시키고 예측을 구하면 됩니다. 이를 랜덤 포레스트라고 합니다. 

7.1 투표 기반 분류기 

만약에 80%의 분류기 여러 개를 훈련시켰다고하면 예측기들은 로지스틱회귀, SVM분류기, 경사하강법 등등으로 훈련시킬 수 있습니다. 투표 기반이라는 이름 그대로 각 분류기의 예측을 모아서 가장많이 예측된 클래스를 반환해주면 됩니다. 이렇게 다수결 투표로 이루어지는 것을 직접투표라고 합니다. 

위의 사진과 같이 4개의 분류기중 3개가 1번 클래스를 예측하고 있습니다. 다수결로 결정하고 결괄르 반환합니다. 



이 다수결 투표분류기는 개별 분류기 중 가장 뛰어난 것보다도 정확도가 높을 경우가 많습니다. 각분류기는 약한 분류기(weak learner) 즉 랜덤 추측보다 조금 더 높은 성능을 내는 분류기일지라도 다양하고 많은 앙상블은 강한 학습기가 될 수 있습니다. 


moons data를 이용합니다. 

from sklearn.metrics import accuracy_score

for clf in (log_clf, rnd_clf, svm_clf, voting_clf):
clf.fit(trainData, trainLabel)
predict = clf.predict(testData)
print(clf.__class__.__name__, accuracy_score(testLabel, predict)) Result LogisticRegression 0.864 RandomForestClassifier 0.872 SVC 0.888 VotingClassifier 0.896

역시 개별의 학습기보단 앙상블을 이용한 학습기가 더 좋은 졍확도를 나타냅니다.  그리고 앙상블 방법은 예측기가 가능한 서로 독립적일때 최고의 성능을 발휘합니다. 분류기를 각자 다른 알고리즘을 활용한다면 좋은 결과가 나올 것입니다. 

  


그리고 각자의 분류기가 어떤 클래스가 될 확률을 알 수 있다면(predict_proba함수를 사용할 수 있다면) 개별 분류기의 예측을 평균내어 가장 높은 확률의 클래스를 나타낼 수 있습니다. 이를 간접 투표라고 합니다. 이 방식은 확률이 높은 투표에 비중을 더 두기 때문에 직접투표 방식보다 성능이 높습니다. 이방식을 사용하려면 voting='soft'로 바꾸어 주고 모든 분류기가 클래스를 확률로 추정할 수 있으면 됩니다. svc에서는 옵션으로 probaility 매개변수를 True로 지정하면됩니다. 


7.2 배깅과 페이스팅  


앙상블 방법에서 좋은 효과를 내는 방법은 다양한 알고리즘을 사용하여 분류기를 나타내거나 같은 알고리즘을 사용하지만 훈련세트 서브셋을 무작위로 나누어서 분류기를 각기 다르게 학습시키는 것 입니다. 


훈련 세트에서 중복을 허용해서 샘플링하는 방법을 배깅 


훈련 세트에서 중복 허용하지 않고 샘플링 하는 방법을 페이스팅이라고 한다.


하나의 데이터를 bag라는 데이터를 무작위로 나누어서 훈련을 시켜준다. 서로 다른 데이터로 훈련을 시켜준 모델은 약한 학습기이고 과대적합 되어있다. 이런 모델들을 하나로 취합할여 강한 학습기로 바꾸고 과대적합을 줄인다.  


분류일 때는 가장 많이 선택된 클래스를 반환하고 회귀는 평균을 구해서 반환해준다. 



이사진에서 보는 것과 같이 같은 모델에 서로 다르게 샘플링된 데이터들을 이용하여 훈련시키고 있다. 


코드로 보면 아래와 같이 사용할 수 있다. 

from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier

bag_clf = BaggingClassifier(
DecisionTreeClassifier(), n_estimators=500,
bootstrap=True, max_samples=100, n_jobs=-1
)
bag_clf.fit(trainData, trainLabel)

max_samples=100 샘플수를 100개로 제한하겠다. 

n_estimators=500 결정트리 500개의 앙상블로 훈련시키겠다. 

n_jobs=-1 cpu의 코어수 제한하기 -1 컴퓨터의 코어 수 모두 사용하겠다라는 의미!!

bootstrap=True sampling할때 중복을 허용하여 하겠다. 즉 bagging기법을 사용하겠다는 의미 



 배깅과 페이스팅은 비슷한 편향을 갖지만 더 작은 분산을 만듭니다. 아래의 그림을 보면 왼쪽은 결정트리만 쓴 경우 오른쪽은 배깅과 함께 결정트리를 쓴경우입니다. 

oob 평가 


배깅을 사용하다보면 하나의 모델을 위해 어떤 데이터는 여러번 샘플링될 수도 있고 어떤 데이터는 이용되지 않을 수 있다. 일반적으로 훈련할 때 사용하는 데이터는 63% 정도이다. 나머지 37%는 oob(out of bag)라고 부릅니다. 훈련할 때 이 데이터를 사용하지 않으므로 마지막 테스트 데이터로 이용할 수 있습니다. oob_score=True로 해주면 훈련이 끝나는 후 자동으로 oob 평가를 수행합니다. 



bag_clf = BaggingClassifier(
DecisionTreeClassifier(), n_estimators=500,
bootstrap=True, n_jobs=-1, oob_score=True)
bag_clf.fit(trainData, trainLabel)
bag_clf.oob_score_ 0.896

  위와 같이 사용하면 oob의 점수를 알 수 있다. 

from sklearn.metrics import accuracy_score
y_pred = bag_clf.predict(testData)
accuracy_score(y_pred, testLabel) 0.896

oob로 확인한 값과 비슷하다.


그리고 기반이 되는 예측기가 확률을 반환할 수 있다면 결정함수의 값도 알 수 있다.

bag_clf.oob_decision_function array([[0.36756757, 0.63243243], [0.34825871, 0.65174129], [1. , 0. ], [0. , 1. ], [0. , 1. ], [0.08040201, 0.91959799], [0.39784946, 0.60215054], [0.00588235, 0.99411765], [1. , 0. ],

..... 오른쪽이 양성 왼쪽이 음성 클래스로 분류될 확률입니다.


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


데이터를 샘플링할 수 있지만 특성 또한 샘플링을 할 수 있다. 옵션은 max_features, bootstrap_features 두 매개변수로 조절됩니다. 

훈련 특성과 샘플 모두를 샘플링하는 것을 랜덤 패치 방식, 훈련샘플을 모두 사용하고( bootstrap=false max_samples=1.0) 특성을 샘플링하는 것을 랜덤 서브 스페이스 방식이라고 합니다.  특성 샘플링은 다양한 예측기를 만들며 편향을 늘리는 대신 분산을 낮춥니다. 



7.4 랜덤 포레스트 


랜덤 포레스트는 배깅방법을 결정트리에 활용한 앙상블 방법입니다. 사용할 때는 baggingClassifier에 DecisionTreeClassifier을 넣어 만드는 대신 결정트리에 최적화 되어 있는 RandomForestClassifier를 사용할 수 있습니다. 


from sklearn.ensemble import RandomForestClassifier

random_clf = RandomForestClassifier(n_estimators=500, max_leaf_nodes=16, n_jobs=-1)
random_clf.fit(trainData, trainLabel)

y_pred = random_clf.predict(testData)
accuracy_score(y_pred, testLabel) 0.92

 RandomForestClassifier는 결정트리를 조정하는 매개변수와 배깅을 조정하는 매개변수를 둘다 갖고 있습니다. 랜덤 포레스트 알고리즘은 트리의 노드를 분할할 때 전체 특성 중에서 최선의 특성을 찾는 대신 무작위로 선택한 특성 후보 중에서 최적의 특성을 찾는 식으로 무작위성을 더 주입합니다. 


이를 통해서 


편향을 손해 보는 대신 분산을 낮추어 전체적으로 더 훌륭한 모델을 만들어 냅니다. 


RandomForest가 아닌 Bagging에 결정트리를 넣고 해도 결과는 비슷하게 나옵니다.  



7.4.1 엑스트라 트리


랜덤 포레스트에서 트리를 만들 때 각 노드는 무작위로 특성의 서브셋을 만들어 분할에 사용합니다. 트리를 더 무작위하게 만들기 위해(보통의 결정트리와 같이) 최적의 임계값을 찾는 대신 후보 특성을 사용해 무작위로 분할한 다음 그중에서 최상의 분할을 선택합니다. 




이와 같이 극단적으로 무작위한 트리의 랜덤 포레스트를 익스트림 랜덤 트리라고 부릅니다. RandomForestClassifier와 같이 사용하면 됩니다. ExtraTreesClassifier


from sklearn.ensemble import ExtraTreesClassifier

extra_clf = ExtraTreesClassifier(n_estimators=500, max_leaf_nodes=16, n_jobs=-1)
extra_clf.fit(trainData, trainLabel)

y_pred = extra_clf.predict(testData)
accuracy_score(y_pred, testLabel) 0.912

일반 랜덤 포레스트 보단 정확도가 조금 더 낮게 나왔습니다.


편향이 늘어나는 대신 분산이 줄어들게 됩니다.  그리고 구분해주는 최적의 특성을 찾는 것이 랜덤포레스트보다 훨씬 빠릅니다. 



7.4.2 특성 중요도 


결정 트리를 기반으로 하는 모델은 모두 특성의 중요도를 제공해줍니다. 불순도를 얼마나 감소시키느냐에 따라 특성의 중요도를 측정합니다. 사이킷런은 훈련이 끝난 뒤 특성마다 자동으로 이 점수를 계산하고 중요도의 전체 합이 1이 되도록 결괏값을 정규화합니다.  이 값은 feature_importance_ 변수에 저장됩니다. 


from sklearn.datasets import load_iris
iris = load_iris()

rnd_clf = RandomForestClassifier(n_estimators=500, n_jobs=-1)
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.09475517538460963 sepal width (cm) 0.025029010963350945 petal length (cm) 0.4284133184610981 petal width (cm) 0.4518024951909411


꽃받침에 대한 데이터는 별로 중요하지 않고 꽃잎에 대한 데이터가 중요한 것을 알 수 있다. 



7.5 부스팅 


부스팅은 약한 학습기 여러개를 연결하여 강한학습기를 만드는 앙상블 방법을 말합니다. 부스팅 방법에는 아다부스트와 그래디언트 부스팅이 있습니다. 



7.5.1 아다 부스트 


이전 모델이 과소적합했던 훈련 샘플의 가중치를 더 높여서 다음단계에 다시 학습하는 방법입니다. 이 방법을 사용하면 예측기는 학습하기 어려운 샘플에 점점 더 맞춰지게 됩니다. 


예를 들어서 아다 부스트 분류기를 만들려면 첫번째 분류기를 훈련세트에서 훈련시키고 예측을 만듭니다. 다음에 잘못 분류된 훈련샘플의 가중치를 상대적으로 높입니다. 두번째 분류기는 업데이트된 가중치를 사용해 훈련세트에서 훈련하고 다시 예측을 만듭니다.  그다음 다시 가중치를 업데이트 해주는 식으로 반복합니다. 아래 사진과 같은 형태로 훈련합니다. 




import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import ListedColormap

def plot_decision_boundary(clf, X, y, axes=[-1.5, 2.5, -1, 1.5], alpha=0.5, contour=True):
x1s = np.linspace(axes[0], axes[1], 100)
x2s = np.linspace(axes[2], axes[3], 100)#100을
x1, x2 = np.meshgrid(x1s, x2s) #그리드 포인트를 만들게 도와준다.
X_new = np.c_[x1.ravel(), x2.ravel()]
y_pred = clf.predict(X_new).reshape(x1.shape)
custom_cmap = ListedColormap(['#fafab0','#9898ff','#a0faa0']) #색
plt.contourf(x1, x2, y_pred, alpha=0.3, cmap=custom_cmap)
if contour:
custom_cmap2 = ListedColormap(['#7d7d58','#4c4c7f','#507d50'])
plt.contour(x1, x2, y_pred, cmap=custom_cmap2, alpha=0.8)
plt.plot(X[:, 0][y==0], X[:, 1][y==0], "yo", alpha=alpha)#선을 긋는다.
plt.plot(X[:, 0][y==1], X[:, 1][y==1], "bs", alpha=alpha)
plt.axis(axes)
plt.xlabel(r"$x_1$", fontsize=18)
plt.ylabel(r"$x_2$", fontsize=18, rotation=0)

m = len(trainData)

plt.figure(figsize=(11, 4))
for subplot, learning_rate in ((121, 1), (122, 0.5)):
sample_weights = np.ones(m)
plt.subplot(subplot)
for i in range(5):
svm_clf = SVC(kernel="rbf", C=0.05, random_state=42)
svm_clf.fit(trainData, trainLabel, sample_weight=sample_weights)
y_pred = svm_clf.predict(trainData)
sample_weights[y_pred != trainLabel] *= (1 + learning_rate) #가중치 조절해준다.
plot_decision_boundary(svm_clf, X, Y, alpha=0.2)
plt.title("learning_rate = {}".format(learning_rate), fontsize=16)
if subplot == 121:
plt.text(-0.7, -0.65, "1", fontsize=14)
plt.text(-0.6, -0.10, "2", fontsize=14)
plt.text(-0.5, 0.10, "3", fontsize=14)
plt.text(-0.4, 0.55, "4", fontsize=14)
plt.text(-0.3, 0.90, "5", fontsize=14)

plt.show()


훈련 수가 늘어갈 수록 정확한 그래프가 만들어지는 것을 볼 수 있다. 




사이킷런은 SAMME라는 아다부스트의 다중 클래스 버전을 사용합니다. 클래스가 만약 두 개뿐일 때는 SAMME가 아다부스트와 동일합니다. 확률을 정확히 알고 싶다면  SAMME.R이라는 SAMME의 변종을 사용합니다. 이 알고리즘은 예측값 대신 클래스 확률에 기반하며 일반적으로 성능이 더 좋습니다. 


from sklearn.ensemble import AdaBoostClassifier

ada_clf = AdaBoostClassifier(
DecisionTreeClassifier(max_depth=1), n_estimators=200,
algorithm="SAMME.R", learning_rate=0.5)
ada_clf.fit(trainData, trainLabel)




7.5.2 그래디언트 부스팅 


아다부스트는 잘못 예측한 데이터에 대한 가중치를 수정하는 반면에 그래디언트 부스팅은 이전 예측기가 만든 잔여 오차에 새로운 예측기를 학습시킵니다. 


1 단계

tree_reg1 = DecisionTreeClassifier(max_depth=2)
tree_reg1.fit(X, y)

2단계

y2 = Y - tree_reg1.predict(X)
tree_reg2 = DecisionTreeClassifier(max_depth=2)
tree_reg2.fit(X, y2)

3단계

y3 = y2 - tree_reg2.predict(X)
tree_reg3 = DecisionTreeClassifier(max_depth=2)
tree_reg3.fit(X, y3)

하지만 이런식으로가 아닌 사이킷런에서 제공해주는 GradientBoostingRegressor를 사용하면 GBRT 앙상블을 간단하게 훈련시킬 수 있습니다. 

from sklearn.ensemble import GradientBoostingRegressor

gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1.0)
gbrt.fit(X, y)


내부적으로는 이런식으로 돌아갑니다.  첫 훈련 두번째 훈련 세번째 훈련을 모두 더해서 점점 더 좋은 결과를 냅니다. learning rate 매개변수가 각 트리 기여도를 의미합니다. 숫자가 작을 수록 많은 수의 트리가 필요하지만 예측의 성능은 더 좋아집니다. 이는 축소라는 규제입니다. 



그래디언트 부스팅을 더 좋은 결과를 내기위해선 최적의 트리수를 찾을 필요가 있습니다. 조기종료법을 사용하면 간단하게 최적의 트리수를 찾을 수 있습니다. 찾기 위해선 gradientBoostingRegressor 모델이 제공해주는 staged_predict라는 함수를 이용하면 됩니다. 


from sklearn.metrics import mean_squared_error
from sklearn.ensemble import GradientBoostingClassifier
gbrt = GradientBoostingClassifier(max_depth=2, n_estimators=120)
gbrt.fit(trainData, trainLabel)
predictA = gbrt.predict(testData)
print(accuracy_score(predictA, testLabel))
errors = [mean_squared_error(testLabel, y_pred) for y_pred in gbrt.staged_predict(trainLabel)]

bst_n_estimators = np.argmin(errors)

gbrt_best = GradientBoostingRegressor(max_depth=2, n_estimators=bst_n_estimators)
gbrt_best.fit(trainData, trainLabel)
predictB = gbrt.predict(testData)
print(accuracy_score(predictB, testLabel)) 0.892 0.914

밑에 최적의 트리 개수를 구한 뒤 더 좋은 결과 나옵니다. 하지만 위와 같은 방법은 먼저 훈련 시킨 뒤 다시 한번 구해서 훈련시키는 것 입니다. 따라서 실제로 훈련 도중에 중지하는 방법을 사용해보겠습니다. warm_start=True로 설정하면 사이킷런이 fit() 메서드가 호출될때 기존트리를 유지하고 훈련을 추가할 수 있게 해줍니다.  

gbrt = GradientBoostingRegressor(max_depth=2, warm_start=True)
min_val_error = float("inf")
error_going_up = 0
for n_estimator in range(1, 120):
gbrt.n_estimators = n_estimator
gbrt.fit(X, Y)
y_pred = gbrt.predict(testData)
val_error = mean_squared_error(y_pred, testLabel)
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

이 코드는 오차를 계속해서 줄이다가 5번 이상 줄어들지 않는다면 조기 종료를 시키는 코드이다. 



7.4 스태킹 


스태킹은 앙상블에 속한 모든 예측기의 예측을 취합하는 대신 취합하는 함수를 훈련시킬 수 없을까라는 아이디어에서 출발하게 되었습니다. 


stacking 각 분류기 마다 다른 결과를 예측하고 마지막 예측기(블렌더)가 예측을 입력으로 받아 최종 예측합니다. 


블렌더를 학습시키는 일반적인 방법은 홀드아웃 세트를 사용하는 것입니다. 먼저 훈련세트를 두개의 서브셋으로 나눈 뒤 첫번째 서브셋은 첫 번째 레이어의 예측을 훈련시키기 위해 사용합니다. 

그리고 훈련된 모델을 사용해서 두번째 셋을 예측하는데 사용합니다. 타겟값은 그대로 사용하고 예측한 값을 다시 입력 특성으로 만들어서 새로운 훈련세트를 만들 수 있습니다. 

마지막으로 블렌더가 새로운 훈련세트로 훈련됩니다. 즉 첫번째 레이어의 예측값으로 블렌더를 훈련시키고 결과값을 만드는 것 입니다. 


1차 레이어의 훈련셋은 선형회귀, 랜덤포레스트 회귀 등등으로 다양하게 이용할 수 있습니다.