[Python] 앙상블 모형 이론 및 실습

거친코딩·2021년 5월 15일
0
post-thumbnail

기초부터 쌓아가는 머신러닝 #7

7주차 : 앙상블 모형 이론 및 실습


🚩 앙상블 모형과 랜덤포레스트가 무엇인가?

어떠한 한 현상에 대한 답을 얻는다고 가정해보자, 많은 경우에 한 명의 전문가보다 여려 명의 일반인들의 의견이 더 나은 경우가 있다.

  • 위 예제와 비슷하게, 하나의 좋은 모형(회귀,분류)으로부터 예측을 하는 것보다 여러 개의 모형으로부터 예측을 수집하는 것이 더 좋은 예측을 할 수 있다.
  • 이러한 여러 개의 모형을 앙상블이라고 부르고, 여러 개의 모형을 조화롭게 학습시키는 것을 앙상블 학습이라고 한다.
  • 그리고 6주차에서 배운 결정 트리 모형이 하나가 아니라, 훈련 세트를 무작위로 다른 서브셋으로 만들어서 결정 트리 분류기를 만들고, 많은 모형들 중에서 가장 많은 선택을 받은 클래스를 예측하는 앙상블 모형을 랜덤포레스트라고 한다.
  • 오늘날의 랜덤포레스트 모델은 가장 강력한 머신러닝 알고리즘 하나이다.
  • 그리고 머신러닝 대회에서 우승하는 솔루션들은 대부분 앙상블 방법을 사용하여서 최고 성능을 낸다.
  • 뒤에서 앙상블 방법들 중 배깅, 부스팅, 스태킹을 설명할 것이다.

🚩 투표 기반 분류기

  • 하나의 데이터셋을 여러종류의 분류기들로 훈련시켰다고 가정해보자.
  • 위에서 언급한대로 하나의 좋은 모델을 사용하는 것보다, 여러 종류의 분류기들이 가장 많이 예측한 클래스를 예측하는 것이 더 좋은 분류기를 만드는 매우 간단한 방법이다.
  • 이렇게 다수결의 투표로 정해지는 분류기를 hard voting(집접 투표) 분류기라고 한다.
  • 놀랍게도 위 모델 중 가장 성능이 좋은 모델의 정확도보다 다수결을 통해 예측한 앙상블 모델의 성능이 높은 경우가 많다.
  • 이렇게 랜덤 추측보다 조금 더 높은 성능을 내는 weak learner(약한 학습기) 가 충분히 많고 다양하다면 strong learner(강한 학습기)가 될 수 있다.


어떻게 약한 학습기가 강한 학습기가 되어서 더 좋은 성능을 낼 수 있을까?, 이 질문은 "큰 수의 법칙"으로 설명될 수 있다.

  • 먼저, 50:50의 동전이 아니라, 51:49의 불균형하게 앞면과 뒷면이 나오는 동전이 있다고 가정을 해보자.
  • 이 동전을 1,000번을 던진다면 거의 앞면 510번과 뒷면 490번이 나올 것이다.
  • 수학적으로 1,000번을 던졌을 때 앞면이 더 많게 나오는 확률은 거의 75% 정도 된다.
  • 수학적으로 10,000번을 던졌을 때 앞면이 더 많게 나오는 확률은 거의 97% 정도 된다.
  • 위 수학적 계산은 이항분포의 확률 질량 함수로 계산 가능하다. ex) 1-scipy.stats.binom.cdf(499,1000,0.51) = 0.747
  • 위의 내용을 기반으로 우리의 약한 분류기(51%) 1,000개로 앙상블 모형을 구축하고, 가장 많은 클래스를 예측으로 삼는다면 75%, 10,000개로 모형을 만들면 97% 정도의 성능을 낼 수 있다.
  • 하지만..! 위의 과정은 모든 분류기가 완벽하게 독립이고, 모델의 예측 오차에 대해서 상관관계가 없을때만 가능하다.
    🌞 TIP : 앙상블에서 예측기가 가능한 서로 독립일 때 최고 성능을 발휘한다. 그래서 가능한 다양한 알고리즘을 사용해서 학습을 하면 다양한 종류의 오차를 만들기 때문에 앙상블 모델의 성능을 높일 수 있다.
  • 여러 종류의 알고리즘을 사용해서 투표기반 분류기를 만드는 예제를 해보자.
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier,VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

# 데이터셋 로드
iris = load_iris()
X = iris.data[:,2:] # 꽃잎의 길이, 너비
Y = iris.target
x_train,x_test,y_train,y_test = train_test_split(X,Y,test_size=0.3,random_state=2021,shuffle=True)

# 약한 학습기 구축
log_model = LogisticRegression()
rnd_model = RandomForestClassifier()
svm_model = SVC()

# 앙상블 모델 구축
# 만약에 모든 모델이 predict_proba() 메서드가 있으면, 예측의 평균을 내어 soft voting(간접 투표)도 할수 있다.
# 간접 투표 방식은 확률이 높은 투표에 비중을 두기 때문에 성능이 더 높다. (voting='soft' 사용)
# svc는 기본적으로 predict_proba를 제공하지 않아, probability = True 지정 해야 사용 가능
# 대신 svc에서 probability = True를 지정하면 교차 검증을 사용해서 확률을 추정하기 때문에 훈련 속도 느려짐
# 대신 성능을 올라감
voting_model = VotingClassifier(
    estimators=[('lr',log_model),('rf',rnd_model),('svc',svm_model)], # 3개의 약한 학습기
    voting='hard' # 직접 투표(hard voting)
)

# 앙상블 모델 학습
voting_model.fit(x_train,y_train)

# 모델 비교
for model in (log_model,rnd_model,svm_model,voting_model):
  model.fit(x_train,y_train)
  y_pred = model.predict(x_test)
  print(model.__class__.__name__," : ",accuracy_score(y_test,y_pred))

> LogisticRegression  :  1.0
  RandomForestClassifier  :  0.9555555555555556
  SVC  :  1.0
  VotingClassifier  :  1.0

🚩 배깅과 페이스팅

  • 앙상블 모형의 좋은 성능을 내기 위해서는 다양한 종류의 오차를 만들어야 하고, 그러기 위해서는 다양한 알고리즘을 사용해야 한다고 배웠다.
  • 다양한 오차를 만들기위한 다른 하나의 방법으로는 훈련 세트의 서브셋을 무작위로 구성하여 모델을 학습시키는 것이 있다. 이를 배깅페이스팅이라고 부른다.
  • 배깅 : 훈련 세트의 중복을 허용하여 샘플링을 하는 방식 (통계학에서는 "부트스트래핑"이라고도 부름)
  • 페이스팅 : 훈련 세트의 중복을 허용하지 않고 샘플링 하는 방식
  • 배깅은 각 예측기가 학습하는 서브셋에 다양성을 증가시키므로 페이스팅보다 편향이 조금 더 높다.
  • 하지만 배깅은 예측기들의 상관관계를 줄이므로 앙상블의 분산을 감소 시킨다.
  • 전반적으로 배깅이 더 나은 모델을 만들지만, 시간과 장비가 좋다면 교차검증으로 배깅과 페이스팅을 둘다 해보면 좋다.

1. 사이킷런의 배깅과 페이스팅

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

# 모델 구축
# BaggingClassifier에서 사용한 분류기가 클래스 확률추정(predict_proba)이 가능하면 자동으로 간접 투표 사용 
bag_model = BaggingClassifier(
    DecisionTreeClassifier(), # 약한 학습기(결정 트리)
    n_estimators=500, # 약한 학습기(결정 트리) 500개 생성
    max_samples=0.05, # 0.0~1.0 사이 실수 선택(실수 x 샘플 수) 혹은 샘플수 지정
    bootstrap=True, # True : 배깅, False : 페이스팅
    n_jobs=-1 # 훈련과 예측에 사용할 CPU 코어 수 (-1 : 가용한 모든 코어 사용)
)

# 모델 학습
bag_model.fit(x_train,y_train)

# 모델 예측
y_pred = bag_model.predict(x_test)

# 모델 평가
print(bag_model.__class__.__name__," : ",accuracy_score(y_test,y_pred))
> BaggingClassifier  :  0.9777777777777777

  • 단일 결정 트리와 배깅을 사용한 결정트리 앙상블의 결정경계를 비교해보면 트리 앙상블이 더욱 일반화가 잘 된것을 확인할 수 있다.

2. oob 평가

  • 배깅(중복 허용 샘플링)을 하다보면 평균적으로 훈련 샘플의 약 63%정도만 추출되고 나머지 약 37%는 추출되지 않고, 이렇게 추출되지 않은 샘플들을 oob(out-of-bag)샘플이라고 부른다.
  • 예측기가 훈련되는 동안에는 oob샘플을 사용하지 않으므로, 검증 세트나 교차 검증을 사용하지 않고 oob샘플만을 가지고 모델 최적화를 위한 평가를 할 수 있다.
  • 앙상블의 평가는 각 예측기의 oob평가의 평균으로 확인한다.
# 모델 구축
bag_model = BaggingClassifier(
    base_estimator = DecisionTreeClassifier(),
    n_estimators = 500,
    bootstrap = True,
    n_jobs = -1,
    oob_score = True # oob평가를 위해 True를 지정한다.
)

# 모델 학습
bag_model.fit(x_train,y_train)

# 모델 평가(oob_score_)
print('oob_score : ',bag_model.oob_score_)

# 모델 평가
y_pred = bag_model.predict(x_test)
print('test_score : ',accuracy_score(y_test,y_pred))
>oob_score  :  0.9523809523809523
 test_score :  0.9333333333333333

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

  • 위에서는 훈련 샘플을 랜덤 샘플링하여 각 예측기의 오차 다양성을 주었지만, 이번에는 훈련 데이터들의 입력 특성들을 무작위로 샘플링하여 예측기를 만들어서 예측기에 대한 오차 다양성을 줄 수 있다.
  • 훈련 데이터들의 입력 특성들을 무작위로 샘플링하는 것을 랜덤 패치 방식랜덤 서브스페이스 방식이라고 한다.
  • 랜덤 패치 방식 : 훈련 특성과 샘플을 모두 샘플링하는 방식
    - ex) (bootstrap=True or False, max_samples<1.0, bootstrap_features=True, max_features<1.0)
  • 랜덤 서브스페이스 방식 : 훈련 샘플은 모두 사용하고, 특성만 샘플링 하는 것
    - ex) (bootstrap=False, max_samples=1.0, bootstrap_features=True, max_features<1.0)

🚩 랜덤 포레스트

  • 랜덤포레스트는 일반적으로 배깅방법을 사용한 결정트리 앙상블 모델이다.
  • 그래서 BaggingClassifier에 DecisionTreeClassifier를 넣는 대신, RandomForestClassifier를 사용할 수 있다.
  • 그래서 RandomForestClassifier는 DecisionTreeClassifier와 BaggingClassifier 매개변수 모두 가지고 있다.
  • 랜덤포레스트 모델은 트리의 노드를 분할할 때 전체 특성 중에서 최선의 특성을 찾는 것이 아니라, 무작위로 선택한 특성들 중에서 최선의 특성을 찾는 방식을 채택하여 무작위성을 더 가지게 된다.
  • 이를 통해 약간의 편향은 손해보지만, 더욱 다양한 트리를 만들므로 분산을 전체적으로 낮추어서 더 훌륭한 모델을 만들 수 있다.
from sklearn.ensemble import RandomForestClassifier

# 랜덤포레스트 모델 구축
rnd_model = RandomForestClassifier(
    n_estimators = 500, # 예측기 500개
    max_leaf_nodes = 16, # 자식노드의 최대 개수 
    n_jobs = -1 # CPU 코어 구동 개수
)

# 모델 학습
rnd_model.fit(x_train,y_train)

# 모델 예측
y_pred_rf = rnd_model.predict(x_test)

# 모델 평가
print("rnd_model : ",accuracy_score(y_pred_rf,y_test))
> rnd_model :  0.9333333333333333

1. 엑스트라 트리

  • 랜덤포레스트는 앞에서 말한 것 처럼 각 노드에서 무작위로 특성을 뽑은 다음 최적의 특성과 임계값을 선택한다.
  • 하지만 엑스트라 트리는 최적의 특성과 임계값을 찾는것 대신, 후보 특성을 사용해 무작위로 분할한 다음에 최상을 분할을 선택한다.
  • 이렇게되면 기본적으로 편향이 많은 랜던포레스트보다 더욱 편향이 심해지지만, 분산을 더욱 낮출 수 있게 된다.
  • 트리 알고리즘에서는 모든 노드에서 최적의 특성과 임계값을 고르는데 시간이 많이 들지만, 엑스트라 트리를 사용하면 훈련과 예측속도가 빨라진다.
  • 엑스트라 트리는 ExtraTreesClassifier를 이용하면 사용할 수 있다.
  • RandomForestClassifier와 ExtraTreesClassifier 중 어떤 것이 더 좋을지는 판단하기 어렵기 때문에, 교차검증을 통해서 서로 비교해보고, 더 나은 모델을 선택하여 그리드 탐색방법을 사용해 하이퍼파라미터 튜닝을 한다.

2. 특성 중요도

  • 랜덤포레스트는 성능이 좋다는 장점말고, 특성의 상대적 중요도를 측정하기 쉽다.(트리기반 모델은 특성 중요도 제공)
  • 사이킷런에서는 어떤 특성을 사용한 노드가 평균적으로 불순도를 감소시키는지 확인하여 특성 중요도를 측정하고, 훈련이 끝나고 난 뒤에 특성마다 자동으로 점수를 계산하고 저장한다.
  • 저장된 값은 featureimportances 변수에 저장되어 있다.
# 데이터셋 정의
x = iris.data[:,:]
y = iris.target

# 모델 구축
rnd_model = RandomForestClassifier(
    n_estimators = 500,
    n_jobs = -1
)

# 모델 학습
rnd_model.fit(x,y)

# 특성 중요도 확인 (전체 특성 중요도 합 : 1)
for feature_name,feature_imp in zip(iris['feature_names'],rnd_model.feature_importances_):
  print(feature_name,' : ',feature_imp)
> sepal length (cm)  :  0.09919561404019304
  sepal width (cm)  :  0.02374662557128492
  petal length (cm)  :  0.4544515575221269
  petal width (cm)  :  0.42260620286639516

🚩 부스팅

  • 부스팅이란, 약한 학습기를 여러 개들을 서로 연결하고 보완해가면서 더욱 강한 학습기를 만드는 앙상블 방법이다.
  • 다양한 부스팅 방법들이 있지만, 그중에서 가장 인기있는 아다부스트그래디언트 부스팅을 소개하겠다.
  1. 아다부스트

    - 아다부스트의 아이디어는 이전 예측기가 과소적합되었던 훈련 샘플의 가중치를 더 높이는 것이다.
    - 이 덕분에 새로운 예측기는 학습하기 어려운 샘플에 대해 더욱 잘 예측하게 된다.
    - 예를들어보면,아다부스트에서 첫 번째 예측기를 결정트리로 훈련시키고 예측을 했을 때, 잘못 분류된 훈련 샘플에 대해 가중치를 상대적으로 높이고, 두 번째에는 업데이트된 가중치를 통해서 예측의 오분류를 확인하고 가중치를 높일 것인지 낮출 것인지 결정되면서 반복된다.
    - 사이킷런에서 제공하는 아다부스트는 다음과 같이 수행해볼 수 있다.
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier

# 아다부스트 모델 구축
# 아다부스트의 학습기 : Decision Tree (max_depth =1) 사용
# 학습기 개수(n_estimators) : 200개
# SAMME(Stagewise Additive Modeling using a Multiclass Exponential loss function) 알고리즘 사용
# 기본 학습기가 확률 추정(predict_proba)이 가능하면 SAMME.R 사용 -> 일반적으로 성능이 더 좋음
ada_model = AdaBoostClassifier(
    DecisionTreeClassifier(max_depth=1),
    n_estimators = 200,
    algorithm = 'SAMME.R',
    learning_rate=0.5
)

# 모델 학습
ada_model.fit(x,y)
  1. 그래디언트 부스팅
    • 그래디언트 부스팅은 아다부스팅과 비슷하게 학습 샘플에 대해 오차를 보정하면서 순차적으로 예측기를 추가 한다.
    • 하지만 차이점은 아다부스트처럼 각 학습 샘플에 대한 가중치를 업데이트 하는 대신, 이전 예측기가 만든 잔차(residual error)에 새로운 예측기를 학습시키는 것이다.
    • 결정트리를 예측기로 활용하여 그래디언트 부스팅의 회귀 문제를 수행해 보겠고, 결정트리와 그래디언트 부스팅을 함께 적용한 이 알고리즘을 보통 그래디언트 부스티드 회귀 트리(Gradient Boosted Regression Tree = GBRT)라고 부른다.
from sklearn.tree import DecisionTreeRegressor

# 결정트리(max_depth=3) 모델 구축 및 학습
tree_reg_model_1 = DecisionTreeRegressor(max_depth=3)
tree_reg_model_1.fit(x,y)

# 첫 번째 학습기에서 발생한 잔차를 목적함수로 모델 학습
residual_1 = y - tree_reg_model_1.predict(x)
tree_reg_model_2 = DecisionTreeRegressor(max_depth=3)
tree_reg_model_2.fit(x,residual_1)

# 두 번째 학습기에서 발생한 잔차를 목적함수로 모델 학습
residual_2 = y - tree_reg_model_2.predict(x)
tree_reg_model_3 = DecisionTreeRegressor(max_depth=3)
tree_reg_model_3.fit(x,residual_2)

# 새로운 데이터를 세 개의 트리를 포함한 앙상블 모델로 예측
x_new = [[1.4,0.2]]
prediction = sum(tree.predict(x_new) for tree in [tree_reg_model_1,tree_reg_model_2,tree_reg_model_3])
prediction
> array([-5.20417043e-18])   

# 사이킷런에서 제공하는 GBRT 앙상블을 다음과 같이 간단하게 훈련시킬 수 있다.
from sklearn.ensemble import GradientBoostingRegressor

# GBRT 모형 구축
# GBRT 앙상블 모형도 마찬가지로 n_estimators, max_depth, min_samples_leaf 등을 통해 모델 규제가 가능하다.
# 추가적으로 learning_rate가 각 트리의 기여 정도를 조절한다.
# learning_rate가 0.1보다 낮게 설정되면 훈련을 위한 트리가 더 많이 필요하지만 성능은 좋아진다.
# 이러한 방식을 축소(shrinkage)라고 부르는 규제 방법이다.
gbrt = GradientBoostingRegressor(max_depth = 3,
                                 n_estimators = 3,
                                 learning_rate = 1)

# GBRT 모형 학습
gbrt.fit(x,y)

  • 왼쪽의 경우에는 훈련 세트를 학습하기 위한 불충분한 예측기 수로 인해 과소적합문제, 오른쪽은 훈련 세트에 대한 예측기 과다로 인해 발생한 과적합 문제를 보여준다.
  • 최적의 예측기 수를 찾는 간단한 방법은 다음과 같다.
# 최적의 estimators수를 찾기 위한 간단한 방법
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

# train set과 validation set을 8:2로 분리
x_train, x_val, y_train, y_val = train_test_split(x,y,test_size=0.2)

# GBRT(max_depth=3,예측기 수=120) 모형 구축
gbrt = GradientBoostingRegressor(max_depth=3, n_estimators=120)

# GBRT 모형 학습
gbrt.fit(x_train,y_train)

# staged_predict를 활용하여 훈련 각 단계에서 앙상블에 의해 만들어진 예측을 반복자로 반환
errors = [mean_squared_error(y_val,y_pred) for y_pred in gbrt.staged_predict(x_val)]

# validation 검증 결과에서 가장 좋은 성능을 보인 예측기 수를 추출 
bst_estimators_num = np.argmin(errors)

# 최고의 일반화 성능을 가진 하이퍼파라미터(n_estimators)를 가지고 재 모델 구축
print("best_est_num : ",bst_estimators_num)
gbrt_best = GradientBoostingRegressor(max_depth=3,n_estimators=bst_estimators_num)

# 데이터셋(train + valid)를 가지고 학습
gbrt_best.fit(x,y)


--------------------------------------------------------------------
조기종료(early stopping)를 활용한 
# warm_start : fit 메서드 호출될 때마다 기존 트리 유지 및 훈련 추가할 수 있게 해줌
# subsample : 각 트리는 무작위로 선택된 25% 훈련 샘플로 학습 => 편향 상승 => 분산 감소 => 훈련 속도 상승
# 위 subsample 방법을 "확률적 그래디언트 부스팅"이라 부른다.
gbrt = GradientBoostingRegressor(max_depth=3, warm_start=True,subsample=0.25)

min_val_error = float('inf')
error_going_up = 0
best_estimator = 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
    best_estimator = n_estimators
    error_going_up = 0
  else:
    error_going_up += 1 # 성능 향상이 되지 않을 때마다 +1 
    if error_going_up == 5:
      break # 성능 향상 연속 5회 : 조기 종료

지금까지 머신러닝을 배우기 위한 대부분의 기본 개념을 익혀보았다. 물론 모든 챕터의 개념을 세세하게 리딩한 것은 아니나, 지금까지의 내용을 토대로 각 모델에 대한 개념을 깊게 공부해본다면 지금보다 더 깊은 이해할 가질 수 있게 될 것이다. 다음 시간에는 머신러닝의 최신 알고리즘을 활용하여 kaggle 데이터 분석 및 예측을 해보는 시간을 가질 것이다.

profile
데이터 분석 유튜버 "거친코딩"입니다.

1개의 댓글

comment-user-thumbnail
2022년 3월 2일

감사합니다. 친절하고 깔끔한 안내로 많이 배웠습니다. 따라 배우다 아래와 같이 오류가 나서 문의 올립니다. ㅠㅠ 열심히 배우고 싶은데 이 부분에서 막혀 더 못나아가고 있습니다. ㅠㅠ

x_new = [[1.4,0.2]]
prediction = sum(tree.predict(x_new) for tree in [tree_reg_model_1,tree_reg_model_2,tree_reg_model_3])
prediction

ValueError Traceback (most recent call last)
in ()
1 x_new = [[1.4,0.2]]
----> 2 prediction = sum(tree.predict(x_new) for tree in [tree_reg_model_1,tree_reg_model_2,tree_reg_model_3])
3 prediction

4 frames
/usr/local/lib/python3.7/dist-packages/sklearn/base.py in check_n_features(self, X, reset)
399 if n_features != self.n_features_in
:
400 raise ValueError(
--> 401 f"X has {nfeatures} features, but {self.class.name} "
402 f"is expecting {self.n_features_in
} features as input."
403 )

ValueError: X has 2 features, but DecisionTreeRegressor is expecting 4 features as input.

답글 달기