Machine Learning - Ensemble model
Random Forest¶
앙상블(ensemble)은 기존에 존재하는 기계학습 알고리즘으로 구축된 여러 모델의 예측을 결합하여 단일 모델에 비해 generalizability / robustness을 향상시키는 방법입니다.
앙상블 방법은 보통 두 가지 계열로 구분됩니다.
평균화(averaging) 방법: 여러 개의 모델을 독립적으로 생성 후 예측을 평균화합니다. 결합 추정기는 분산(variance)이 감소하기 때문에 일반적으로 단일 기본 모델보다 낫습니다.
부스팅(boosting) 방법: 기본 모델이 순차적으로 구축되고 결합된 모델의 편향(bias)을 줄이려고 합니다. 강력한 앙상블을 만들기 위해 몇 가지 약한 모델을 결합하는 방법입니다.
가장 대표적인 앙상블 모델은 랜덤 포레스트(random forest)이며, 이 모델은 기본 구성 요소로 결정 트리를 사용합니다.
결정 트리의 단점은 훈련 데이터에 과적합되는 경향이 있다는 것입니다. 랜덤 포레스트는 조금씩 다른 여러 결정 트리를 묶어 과적합 문제를 피할 수 있습니다. 랜덤 포레스트의 원리는, 각 트리는 비교적 예측을 잘 할 수 있지만 데이터의 일부에 과적합하는 경향을 가진다는 데 기초합니다. 잘 작동하되 서로 다른 방향으로 과적합된 트리를 많이 만들면 그 결과를 평균냄으로써 과적합된 양을 줄일 수 있습니다. 이렇게 하면 트리 모델의 예측 성능이 유지되면서 과적합이 줄어드는 것이 수학적으로 증명되었습니다.
이런 전략을 구현하려면 결정 트리를 많이 만들어야 합니다. 각각의 트리는 타깃 예측을 잘 해야 하고 다른 트리와는 구별되어야 합니다. 랜덤 포레스트는 이름에서 알 수 있듯이 트리들이 달라지도록 트리 생성 시 무작위성을 주입합니다.
랜덤 포레스트에서 트리를 랜덤하게 만드는 방법은 두 가지 입니다. 1) 트리를 만들 때 사용되는 데이터 포인트를 무작위로 선택하는 방법(Bagging = Bootstrap Aggregation)과 2) 분할 테스트에서 특성을 무작위로 선택(Feature Randomness)하는 방법입니다.
1-1. Random Forest: Bagging (=Bootstrap Aggregation)¶
Bootstrap sample은 더 큰 샘플에서 "bootstrap"된 더 작은 샘플입니다. Bootstraping은 동일한 크기의 작은 샘플을 하나의 원래 샘플에서 대량으로 반복적으로 추출하는 리샘플링의 한 유형입니다.
통계에 대한 표본 분포를 생성하기 위해 모집단(population)에서 반복되지 않는 큰 표본을 그리는 것이 이상적입니다. 그렇지 않은 작은 데이터에서 bootstrap sample은 모집단 모수(population parameter)에 대해 상당히 좋은 근사치가 될 수 있습니다.
Decision tree은 훈련 데이터에 매우 민감합니다. 훈련 데이터를 약간 변경하면 트리 구조가 크게 달라질 수 있습니다. 랜덤 포레스트는 각 개별 트리가 데이터 세트에서 무작위로 대체하여 샘플을 추출할 수 있도록 하여 서로 다른 트리를 생성함으로써 이를 활용합니다. 이 과정을 bagging이라고 합니다.
1-2. Random Forest: Feature Randomness¶
일반적인 의사 결정 트리에서 노드를 분할할 때, 모든 feature을 고려하여 왼쪽 노드와 오른쪽 노드 사이에 불순도가 가장 낮도록 분할합니다. 반면에 랜덤 포리스트의 각 트리는 feature의 random subset에서만 선택할 수 있습니다. 이로 인해 모델의 트리 간에 훨씬 더 많은 변동이 발생하고 궁극적으로 트리 간 상관 관계가 낮아지고 더 다양화됩니다.
결론적으로, 이러한 과정을 통해 구성된 랜덤 포레스트는 bagging을 통해 각기 다른 데이터셋에 훈련됨과 동시에 각기 다른 feature을 사용한 여러개의 결정 트리로 이루어지게 됩니다.
1-3. Scikit-learn Random Forest¶
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
# two_moon 데이터셋을 가지고 실습합니다.
X, y = make_moons(n_samples=200, noise=0.25, random_state=42)
Xm_train, Xm_test, ym_train, ym_test = train_test_split(X, y, stratify=y, random_state=42)
# two_moon 데이터를 시각화해봅니다.
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.show()
# 랜덤 포레스트의 결정 경계를 시각화하는 함수입니다.
from matplotlib.colors import ListedColormap
def plot_decision_boundary(forest):
figure = plt.figure(figsize=(36, 5))
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
xx, yy = np.meshgrid(np.arange(x_min, x_max, .02), np.arange(y_min, y_max, .02))
cm = plt.cm.RdBu
cm_bright = ListedColormap(['#FF0000', '#0000FF'])
for i in range(len(forest.estimators_)):
ax = plt.subplot(1, 6, i+1)
Z = forest.estimators_[i].predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, cmap=cm, alpha=.8)
ax.scatter(Xm_train[:, 0], Xm_train[:, 1], c=ym_train, cmap=cm_bright, edgecolors='k')
ax.set_xticks(())
ax.set_yticks(())
ax = plt.subplot(1, 6, 6)
Z = forest.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, cmap=cm, alpha=.8)
ax.scatter(Xm_train[:, 0], Xm_train[:, 1], c=ym_train, cmap=cm_bright, edgecolors='k')
ax.set_xticks(())
ax.set_yticks(())
scikit-learn RandomForestClassifier
다큐멘테이션
from sklearn.ensemble import RandomForestClassifier
# 트리 5개로 구성된 랜덤 포레스트 모델을 만들어보겠습니다.
forest_m = RandomForestClassifier(n_estimators=5, random_state=2)
forest_m.fit(Xm_train, ym_train)
RandomForestClassifier(n_estimators=5, random_state=2)
# 주어진 함수를 사용하여 다섯개의 트리와 랜덤 포레스트의 결정 경계를 시각화해봅니다.
plot_decision_boundary(forest_m)
다섯 개의 트리가 만든 결정 경계는 확연하게 다르다는 것을 알 수 있습니다. Bootstrap sampling 때문에 한쪽 트리에 나타나는 훈련 포인트가 다른 트리에는 포함되지 않을 수 있어 각 트리는 불완전합니다.
랜덤 포레스트는 개개의 트리보다는 덜 과적합되고 훨씬 좋은 결정 경계를 만들어줍니다. 실제 애플리케이션에서는 매우 많은 트리를 사용하기 때문에(수백, 수천 개) 더 부드러운 결정 경계가 만들어집니다.
scikit-learn에서 제공하는 유방암 데이터셋에 100개의 트리로 이뤄진 랜덤 포레스트를 적용해보겠습니다.
from sklearn.datasets import load_breast_cancer
# 유방암 데이터를 로드하고 train/test셋으로 나눕니다.
cancer = load_breast_cancer()
Xc_train, Xc_test, yc_train, yc_test = train_test_split(cancer.data, cancer.target, random_state=42)
# 데이터에 어떤 특성이 있는지 살펴봅니다.
cancer.feature_names
array(['mean radius', 'mean texture', 'mean perimeter', 'mean area', 'mean smoothness', 'mean compactness', 'mean concavity', 'mean concave points', 'mean symmetry', 'mean fractal dimension', 'radius error', 'texture error', 'perimeter error', 'area error', 'smoothness error', 'compactness error', 'concavity error', 'concave points error', 'symmetry error', 'fractal dimension error', 'worst radius', 'worst texture', 'worst perimeter', 'worst area', 'worst smoothness', 'worst compactness', 'worst concavity', 'worst concave points', 'worst symmetry', 'worst fractal dimension'], dtype='<U23')
# 랜덤 포레스트 분류기를 유방암 데이터에 훈련시킵니다. n_estimators=100, random_state=42
forest_c = RandomForestClassifier(n_estimators=100, random_state=42)
forest_c.fit(Xc_train, yc_train)
RandomForestClassifier(random_state=42)
print("Random Forest Training Set Accuracy: {:.3f}".format(forest_c.score(Xc_train, yc_train)))
print("Random Forest Test Set Accuracy: {:.3f}".format(forest_c.score(Xc_test, yc_test)))
Random Forest Training Set Accuracy: 1.000 Random Forest Test Set Accuracy: 0.965
랜덤 포레스트는 아무런 매개변수 튜닝 없이도 선형 모델이나 단일 결정 트리보다 높은 정확도를 내고 있습니다. 단일 결정 트리에서 한 것처럼 max_features
매개변수를 조정하거나 사전 가지치기를 할 수도 있습니다. 하지만 랜덤 포레스트는 기본 설정으로도 좋은 결과를 만들어줄 때가 많습니다.
결정 트리처럼 랜덤 포레스트도 특성 중요도를 제공하는데 각 트리의 특성 중요도를 취합하여 계산한 것입니다. 일반적으로 랜덤 포레스트에서 제공하는 특성 중요도가 하나의 트리에서 제공하는 것보다 더 신뢰할 만합니다.
def plot_imp(clf):
sorted_idx = clf.feature_importances_.argsort()
y_ticks = np.arange(0, len(cancer.feature_names))
fig, ax = plt.subplots()
ax.barh(y_ticks, clf.feature_importances_[sorted_idx])
ax.set_yticklabels(cancer.feature_names[sorted_idx])
ax.set_yticks(y_ticks)
fig.tight_layout()
plt.show()
from sklearn.tree import DecisionTreeClassifier
# 랜덤 포레스트와 비교하기 위해 결정트리 분류기를 생성하고 유방암 데이터에 훈련시킵니다.
dec_tree = DecisionTreeClassifier(random_state=42)
dec_tree.fit(Xc_train, yc_train)
DecisionTreeClassifier(random_state=42)
print("Decision Tree Training Set Accuracy: {:.3f}".format(dec_tree.score(Xc_train, yc_train)))
print("Decision Tree Test Set Accuracy: {:.3f}".format(dec_tree.score(Xc_test, yc_test)))
Decision Tree Training Set Accuracy: 1.000 Decision Tree Test Set Accuracy: 0.951
# 하나의 트리에서 제공하는 feature importance를 살펴보겠습니다.
dec_tree.feature_importances_
array([0. , 0.02601101, 0. , 0. , 0. , 0. , 0. , 0.69593688, 0. , 0. , 0. , 0. , 0. , 0.01277192, 0.00155458, 0. , 0.00670697, 0.01702539, 0. , 0. , 0.0877369 , 0.10787925, 0. , 0.03452044, 0.00985664, 0. , 0. , 0. , 0. , 0. ])
# 결정 트리의 특성 중요도를 시각화해봅니다.
plot_imp(dec_tree)
# 랜덤포레스트의 feature importance를 살펴봅니다.
forest_c.feature_importances_
array([0.03971058, 0.01460399, 0.05314639, 0.04277978, 0.00816485, 0.01140166, 0.08321459, 0.0902992 , 0.00443533, 0.00443395, 0.01951684, 0.00459978, 0.00868228, 0.04355077, 0.00464415, 0.0036549 , 0.00701442, 0.00504716, 0.00371411, 0.00658253, 0.08127686, 0.01649014, 0.07138828, 0.12319232, 0.01033481, 0.01580059, 0.03174022, 0.17229521, 0.01310266, 0.00518165])
# 랜덤 포레스트의 feature importance를 시각화해봅니다.
plot_imp(forest_c)
그림에서 알 수 있듯이 랜덤 포레스트에서는 단일 트리의 경우보다 훨신 많은 특성이 0 이상의 중요도 값을 갖습니다. 랜덤 포레스트를 만드는 무작위성은 알고리즘이 가능성 있는 많은 경우를 고려할 수 있도록 하므로, 그 결과 랜덤 포레스트가 단일 트리보다 더 넓은 시각으로 데이터를 바라볼 수 있습니다.
2. Ensemble Methods: Bagging and Boosting¶
2-1. Bagging (=Bootstrap Aggregation)¶
scikit-learn BaggingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import BaggingClassifier
# Bagging을 사용하여 cancer 데이터셋에 LogisticRegression 모델을 100개 훈련하여 앙상블해봅니다.
bagging = BaggingClassifier(LogisticRegression(), n_estimators=100, oob_score=True, n_jobs=-1, random_state=42)
bagging.fit(Xm_train, ym_train)
BaggingClassifier(base_estimator=LogisticRegression(), n_estimators=100, n_jobs=-1, oob_score=True, random_state=42)
BaggingClassifier
은 분류기가 predict_proba()
메서드를 지원하는 경우 확률값을 평균하여 예측을 수행합니다. 그렇지 않은 분류기를 사용할 때는 가장 빈도가 높은 클래스 레이블이 예측 결과가 됩니다.
2-1-1. Out-of-Bag error (OOB)¶
Out-of-Bag error은 기계학습에서 bagging을 사용한 모델의 성능을 측정합니다. OOB error은 모델 훈련에 사용되지 않은 데이터에 대해 계산됩니다. OOB error은 bootstrap 훈련 데이터셋에 OOB 샘플이 포함되지 않은 결정 트리의 하위 집합만 사용하여 계산됩니다.
OOB error은 validation score와 다른 성능 측정치입니다. 때때로 데이터 세트의 크기가 충분하지 않기 때문에 validation dataset을 구성하기 어려운 경우가 있습니다. 대규모 데이터가 아니며, 모든 데이터를 훈련 데이터셋으로 사용하려는 경우 OOB error으로 모델의 성능을 판단할 수 있습니다.
print("Train set accuracy: {:.3f}".format(bagging.score(Xm_train, ym_train)))
print("Test set accuracy: {:.3f}".format(bagging.score(Xm_test, ym_test)))
# OOB 오차를 출력합니다.
print("OOB sample score: {:.3f}".format(bagging.oob_score_))
Train set accuracy: 0.813 Test set accuracy: 0.880 OOB sample score: 0.807
from sklearn.tree import DecisionTreeClassifier
#결정 트리로 배깅을 수행하는 것보다 랜덤 포레스트를 사용하는 것이 편리하지만 여기서는 직접 결정 트리에 배깅을 적용해보겠습니다.
bagging = BaggingClassifier(DecisionTreeClassifier(), n_estimators=5, n_jobs=-1, random_state=42)
bagging.fit(Xm_train, ym_train)
BaggingClassifier(base_estimator=DecisionTreeClassifier(), n_estimators=5, n_jobs=-1, random_state=42)
# 랜덤 포레스트에서처럼 이 배깅 분류기에 있는 결정 트리의 결정 경계를 시각화해보겠습니다.
plot_decision_boundary(bagging)
결과 그래프는 랜덤 포레스트의 결정 경계와 매우 비슷합니다.
# `n_estimators=100`으로 늘려서 cancer 데이터셋에 훈련시켜보고 훈련 세트의 테스트 세트 성능을 확인해보겠습니다.
bagging = BaggingClassifier(DecisionTreeClassifier(), n_estimators=100, oob_score=True, n_jobs=-1, random_state=42)
bagging.fit(Xm_train, ym_train)
BaggingClassifier(base_estimator=DecisionTreeClassifier(), n_estimators=100, n_jobs=-1, oob_score=True, random_state=42)
print("Train set accuracy: {:.3f}".format(bagging.score(Xm_train, ym_train)))
print("Test set accuracy: {:.3f}".format(bagging.score(Xm_test, ym_test)))
print("OOB sample score: {:.3f}".format(bagging.oob_score_))
Train set accuracy: 1.000 Test set accuracy: 0.900 OOB sample score: 0.900
배깅은 랜덤 포레스트와 달리 max_samples
매개변수에서 부트스트랩 샘플의 크기를 지정할 수 있습니다. 또한 랜덤 포레스트는 DecisionTreeClassifier(splitter=“best”)
를 사용하도록 고정되어 있습니다. 결정 트리를 splitter=‘random’
으로 설정하면 무작위로 분할한 후보 노드 중에서 최선의 분할을 찾습니다.
2-2. Boosting¶
Adaboost(=Adaptive Boosting)은 ensemble method의 boosting method 중 가장 유명한 방법입니다. 다른 boosting 방법들 중에는 XGBoost, GradientBoost, 그리고 BrownBoost 등이 있습니다. Adaboost은 일련의 단일 모델을 반복적으로 가중치가 부여된 데이터에 학습시킵니다. 그 다음 모든 예측은 가중 다수결 (혹은 합계)를 통해 결합되어 최종 예측을 생성합니다.
처음에는 가중치가 모든 데이터 포인트에 $1/N$으로 동일하게 설정됩니다. 순차적으로 훈련을 반복하면서 가중치는 개별적으로 수정되고, 단일 모델은 재조정된 데이터에 다시 훈련됩니다. 잘못 예측된 데이터는 가중치가 증가하며, 올바르게 예측된 데이터는 가중치가 감소합니다. 훈련이 반복될수록 예측하기 어려운 데이터가 더욱 많은 가중치를 받게 됩니다. 이러한 원리 때문에 boosting이라고 불립니다.
scikit-learn의 AdaBoostClassifier
는 기본적으로 DecisionTreeClassifier(max_depth=1)
를 사용하고 AdaBoostRegressor
는 decisionTreeRegressor(max_depth=3)
를 사용하지만 base_estimator
매개변수에서 다른 모델을 지정할 수도 있습니다. 순차적으로 학습해야 하기 때문에 n_jobs
매개변수를 지원하지 않습니다.
- scikit-learn AdaBoostClassifier
- scikit-learn AdaBoostRegressor
from sklearn.ensemble import AdaBoostClassifier
# AdaBoost를 two_moons 데이터에 적용해봅니다.
ada = AdaBoostClassifier(n_estimators=5, random_state=42)
ada.fit(Xm_train, ym_train)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-1-d7b7492dffe3> in <module> 3 # AdaBoost를 two_moons 데이터에 적용해봅니다. 4 ada = AdaBoostClassifier(n_estimators=5, random_state=42) ----> 5 ada.fit(Xm_train, ym_train) NameError: name 'Xm_train' is not defined
# AdaBoost의 각 estimator의 결정 경계를 시각화해봅니다.
plot_decision_boundary(ada)
AdaBoostClassifier
는 깊이가 1인 결정 트리를 사용하기 때문에 각 트리의 결정 경계가 하나의 직선입니다. 앙상블된 결정 경계도 다른 앙상블 모델에 비해 더 단순합니다.
# 이번에는 AdaBoost를 breast cancer 데이터에 적용해봅니다.
ada = AdaBoostClassifier(n_estimators=100, random_state=42)
ada.fit(Xc_train, yc_train)
AdaBoostClassifier(n_estimators=100, random_state=42)
print("Train set accuracy: {:.3f}".format(ada.score(Xc_train, yc_train)))
print("Test set accuracy: {:.3f}".format(ada.score(Xc_test, yc_test)))
Train set accuracy: 1.000 Test set accuracy: 0.944
# AdaBoost의 특성 중요도를 시각화해봅니다.
plot_imp(ada)
AdaBoost의 특성 중요도를 확인해보면 다른 모델에서 부각되지 않았던 compactness error
특성을 크게 강조하고 있습니다.
2-2-1. Bagging vs Boosting¶
3. Feature Selection¶
새로운 특성을 만드는 방법이 많으므로 데이터의 차원이 원본 특성의 수 이상으로 증가하기 쉽습니다. 그러나 특성이 추가되면 모델은 더 복잡해지고 과적합될 가능성도 높아집니다. 보통 새로운 특성을 추가할 때나 고차원 데이터셋을 사용할 때, 가장 유용한 특성만 선택하고 나머지는 무시해서 특성의 수를 줄이는 것이 좋습니다. 이렇게 하면 모델이 간단해지고 일반화 성능이 올라갑니다.
이를 위한 전략으로 1) 일변량 통계 (univariate statistics), 2) 모델 기반 선택 (model-based selection), 3) 반복적 선택 (iterative selection)이 있습니다. 이 방법들은 모두 지도 학습 방법이므로 최적값을 찾으려면 타깃값이 필요합니다. 그리고 데이터를 훈련 세트와 테스트 세트로 나눈 다음 훈련 데이터만 특성 선택에 사용해야 합니다.
3-1. 일변량 통계¶
일변량 통계에서는 개개의 특성과 타깃 사이에 중요한 통계적 관계가 있는지를 계산합니다. 그런 다음 깊게 관련되어 있다고 판단되는 특성을 선택합니다. 분산분석(ANOVA)이라고도 합니다.
이 방식의 핵심 요소는 일변량, 즉 각 특성이 독립적으로 평가된다는 점입니다. 따라서 다른 특성과 깊게 연관된 특성은 선택되지 않을 것입니다. 일변량 분석은 계산이 매우 빠르고 평가를 위해 모델을 만들 필요가 없습니다. 한편으로 이 방식은 특성을 선택한 후 적용하려는 모델에 상관없이 사용할 수 있습니다.
scikit-learn에서 일변량 분석으로 특성을 선택하려면 분류에서는 f-classif
을, 회귀에서는 f_regression
을 보통 선택하여 테스트하고, 계산한 p-value에 기초하여 특성을 제외하는 방식을 선택합니다. 이런 방식들은 매우 높은 p-value를 가진 특성(즉, 타깃값과 연관성이 작은 특성)을 제외할 수 있도록 임계값을 조정하는 매개변수를 사용합니다. 임계값을 계산하는 방법은 각각 다르며, 가장 간단한 SelectKBest
는 고정된 k개의 특성을 선택하고, SelectPercentile
은 지정된 비율만큼 특성을 선택합니다. 그럼 cancer 데이터셋에 분류를 위한 특성 선택을 적용해보겠습니다. 문제를 조금 복잡하게 하기위해 의미 없는 노이즈 특성을 데이터에 추가하겠습니다. 특성 선택이 이 의미 없는 특성을 식별해서 제거하는지 보겠습니다.
- scikit-learn
SelectPercentile
- scikit-learn
f_classif
from sklearn.datasets import load_breast_cancer
from sklearn.feature_selection import SelectPercentile, f_classif
from sklearn.model_selection import train_test_split
cancer = load_breast_cancer()
# 고정된 난수를 발생시킵니다.
rng = np.random.RandomState(42)
noise = rng.normal(size=(len(cancer.data), 50))
# 데이터에 노이즈 특성을 추가합니다. 처음 30개는 원본 특성이고 다음 50개는 노이즈입니다.
X_w_noise = np.hstack([cancer.data, noise])
# 원 데이터와 노이즈가 추가된 데이터의 크기를 비교해봅니다.
print(cancer.data.shape)
print(X_w_noise.shape)
(569, 30) (569, 80)
# 데이터를 train/test셋으로 나눕니다. random_state=42, test_size=0.5
X_train, X_test, y_train, y_test = train_test_split(X_w_noise, cancer.target, random_state=42, test_size=0.5)
# f_classif(기본값)과 SelectPercentile을 사용하여 특성의 50%를 선택합니다.
select = SelectPercentile(score_func=f_classif, percentile=50)
select.fit(X_train, y_train)
# 훈련 세트에 적용합니다.
X_train_selected = select.transform(X_train)
print("X_train.shape", X_train.shape)
print("X_train_selected.shape", X_train_selected.shape)
X_train.shape (284, 80) X_train_selected.shape (284, 40)
결과에서 볼 수 있듯이 특성 개수가 80개에서 40개로 줄었습니다. (원본 특성의 50%)
# `get_support` 메서드는 선택된 특성을 boolean 값으로 표시해줍니다. 어떤 특성이 선택되었는지 확인해봅니다.
mask = select.get_support()
print(mask)
[ True True True True True True True True True True True False True True False True True True False False True True True True True True True True True True False False False True False True False False False False False False False True False False False True False False False False False False True False False False True False False False True False False False True True True False False False True True False False False False True True]
# 위 마스크를 시각화해봅니다.
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("feature number")
plt.yticks([0])
([<matplotlib.axis.YTick at 0x7f0abe733590>], <a list of 1 Text major ticklabel objects>)
마스킹된 그래프에서 볼 수 있듯이 선택된 특성은 대부분 원본 특성이고 노이즈 특성이 거의 모두 제거되었습니다. 그러나 원본 특성이 완벽하게 복원된 것은 아닙니다. 전체 특성을 이용했을 때와 선택된 특성만 사용했을 때 로지스틱 회귀의 성능을 비교해보겠습니다.
from sklearn.linear_model import LogisticRegression
# 테스트 데이터 변환
X_test_selected = select.transform(X_test)
# LogisticRegression모델을 생성하고 원본 데이터에 훈련시킨 성능과 중요한 특성들이 선택된 데이터에 훈련시킨 성능을 비교해봅니다.
lr = LogisticRegression(max_iter=10000)
lr.fit(X_train, y_train)
print("전체 특성을 사용한 정확도", lr.score(X_test, y_test))
lr.fit(X_train_selected, y_train)
print("선택된 일부 특성을 사용한 정확도", lr.score(X_test_selected, y_test))
전체 특성을 사용한 정확도 0.9614035087719298 선택된 일부 특성을 사용한 정확도 0.9649122807017544
이 경우에서는 일부 원본 특성이 없더라도 노이즈 특성을 제거한 쪽의 성능이 더 높습니다. 이 예는 인위적으로 간단하게 만든 예제이고 실제 데이터에서의 결과는 보통 엇갈리는 경우도 많습니다. 하지만 너무 많은 특성때문에 모델을 만들기가 현실적으로 어려울 때 일변량 분석으로 사용하여 특성을 선택하면 큰 도움이 될 수 있습니다. 또는 많은 특성들이 확실히 도움이 안 된다고 생각될 때 사용할 수 있습니다.
3-1-1. Selecting K best features¶
SelectPercentile 말고도 K 개의 중요한 특성을 선택하는 방법이 있습니다. SelectKBest
를 이용하면 가능합니다. SelectKBest
는 사용자가 지정한 k개의 중요한 특성을 반환해줍니다. 다만 위에서 사용한 데이터에는 음의 값이 있기 때문에 SelectKBest
를 사용할 수 없습니다. 따라서 이 예제는 Iris 데이터를 사용합니다.
scikit-learn
SelectKBest
scikit-learn
chi2
from sklearn.datasets import load_iris
iris_X, iris_y = load_iris(return_X_y=True)
iris_X.shape
(150, 4)
from sklearn.feature_selection import SelectKBest, chi2
# SelectKBest와 chi2 테스트를 사용하여 iris data에서 두개의 가장 중요한 특성을 선택합니다.
X_new = SelectKBest(chi2, k=2).fit_transform(iris_X, iris_y)
print(X_new.shape)
(150, 2)
3.2 모델 기반 특성 선택¶
모델 기반 특성 선택은 지도 학습 머신러닝 모델을 사용하여 특성의 중요도를 평가해서 가장 중요한 특성들만 선택합니다. 특성 선택에 사용되는 지도 학습 모델은 최종적으로 사용할 지도 학습 모델과 같을 필요는 없습니다. 특성 선택을 위한 모델은 각 특성의 중요도를 측정하여 순서를 매길 수 있어야 합니다. 결정 트리와 이를 기반으로 한 모델은 각 특성의 중요도가 담겨있는 feature_importance_
속성을 제공합니다. 일변량 분석과는 반대로 모델 기반 특성 선택은 한번에 모든 특성을 고려하므로 (사용된 모델이 상호작용을 잡아낼 수 있다면) 상호작용 부분을 반영할 수 있습니다. 모델 기반의 특성 선택은 SelectFromModel
에 구현되어 있습니다.
- scikit-learn
SelectFromModel
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier
# RandomForestClassifier와 SelectFromModel을 이용하여 중요한 특성을 선택합니다.
select = SelectFromModel(RandomForestClassifier(n_estimators=100, random_state=42), threshold='median')
SelectFromModel
은 지도 학습 모델로 계산된 중요도가 지정한 임계치보다 큰 모든 특성을 선택합니다. 일변량 분석으로 선택한 특성과 결과를 비교하기 위해 절반 가량의 특성이 선택될 수 있도록 중간값을 임계치로 사용하겠습니다. 트리 100개로 만든 랜덤포레스트 분류기를 사용해 특성 중요도를 계산합니다. 이는 매우 복잡한 모델이고 일변량 분석보다는 훨씬 강력한 방법입니다.
# 모델을 데이터에 훈련시키고 선택된 특성의 shape을 출력합니다.
select.fit(X_train, y_train)
X_train_l1 = select.transform(X_train)
print("X_train.shape:", X_train.shape)
print("X_train_l1.shape:", X_train_l1.shape)
X_train.shape: (284, 80) X_train_l1.shape: (284, 40)
# 선택된 특성을 같은 방식으로 그려보겠습니다.
mask = select.get_support()
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("featue number")
Text(0.5, 0, 'featue number')
이번에는 두 개를 제외한 모든 원본 특성이 선택되었습니다. 특성을 40개 선택하도록 지정했으므로 일부 노이즈 특성도 선택되었습니다.
# LogisticRegression 모델로 성능이 얼마나 향상되었는지 확인해봅니다.
X_test_l1 = select.transform(X_test)
score = LogisticRegression(max_iter=10000).fit(X_train_l1, y_train).score(X_test_l1, y_test)
print("test score", score)
test score 0.9543859649122807
3.3 반복적 특성 선택¶
반복적 특성 선택(iterative feature selection)에서는 특성의 수가 각기 다른 일련의 모델이 만들어집니다. 기본적으로 두가지 방법이 있습니다.
- 특성을 하나도 선택하지 않은 상태로 시작해서 어떤 종료 조건에 도달할 때 까지 하나씩 추가하는 방법
- 모든 특성을 가지고 시작해서 어떤 종료 조건이 될 때까지 특성을 하나씩 제거해가는 방법
일련의 모델이 만들어지기 때문에 이 방법은 앞서 소개한 방법들보다 계산 비용이 월씬 많이 듭니다. 재귀적 특성 제거 (Recursive Feature Elimination, RFE)가 이런 방법의 하나입니다. 이 방법은 모든 특성으로 시작해서 모델을 만들고 특성 중요도가 가장 낮은 특성을 제거합니다. 그런 다음 제거한 특성을 빼고 나머지 특성 전체로 새로운 모델을 만듭니다. 이런 식으로 미리 정의한 특성 개수가 남을 때까지 계속합니다. 이를 위해 모델 기반 선택에서처럼 특성 선택에 사용할 모델은 특성의 중요도를 결정하는 방법을 제공해야 합니다. 다음은 앞에서와 같은 랜덤 포레스트 모델을 사용합니다.
from sklearn.feature_selection import RFE
# RFE를 생성하고 데이터에 훈련시킵니다.
select = RFE(RandomForestClassifier(n_estimators=100, random_state=42), n_features_to_select=40)
select.fit(X_train, y_train)
# 선택된 특성을 시각화합니다.
mask = select.get_support()
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("feature number")
일변량 분석이나 모델 기반 선택보다 특성 선택이 나아졌지만, 여전히 특성 한개를 놓쳤습니다. 랜덤포레스트 모델은 특성이 누락될 때마다 다시 학습하므로 40번이나 실행됩니다. 그래서 이 코드를 실행하면 모델 기반 선택보다 훨씬 오래 걸립니다.
# RFE를 사용해서 특성을 선택했을 때 로지스틱 회귀의 정확도를 확인해보겠습니다.
X_train_rfe = select.transform(X_train)
X_test_rfe = select.transform(X_test)
score = LogisticRegression(max_iter=10000).fit(X_train_rfe, y_train).score(X_test_rfe, y_test)
print("test score", score)
# 또한 RFE에 사용된 모델을 이용해서도 예측을 할 수 있습니다. 이 경우 선택된 특성만 사용됩니다.
score = select.score(X_test, y_test)
print(score)
RFE안에 있는 랜덤 포레스트의 성능이 이 모델에서 선택한 특성으로 만든 로지스틱 회귀의 성능과 비슷합니다. 다른 말로 하면, 특성 선택이 제대로 되면 선형 모델의 성능은 랜덤 포레스트와 견줄만 합니다.
머신 러닝 알고리즘에 어떤 입력값을 넣을지 확신이 안선다면 특성 자동 선택이 도움이 될 수 있습니다. 또한 예측속도를 높이거나 해석하기 더 쉬운 모델을 만드는 데 필요한 만큼 특성의 수를 줄이는 데도 효과적입니다.
Comments