アンサンブルモデル

前回のタイタニックデータを使い、多数決によるアンサンブルモデルを導入したいと思います。そもそも、アンサンブルモデルって何なのでしょうか?

アンサンブルモデルとは

アンサンブルは、モデルを組み合わせ、より良いモデルを構築することを指します。色々なモデルを試したものの精度が上がらない、そんな時、アンサンブルモデルが解決してくれるかもしれません。

今回は、多数決によるアンサンブルモデルを導入します。モデルを組み合わせ、各モデルが予測したクラスのうち、多数派のクラスをアンサンブルモデルの予測クラスとするといったロジックになっています。一部のモデルに重みをつける方法もありますが、導入方法はほとんど同じです。

どうして精度が上がるのか

アンサンブルモデルで精度が上がる理由は、二項分布から直感的に理解することができます。まず、各モデルが正しく分類する確率をεとおきます。その後、モデル全体から、正しく分類するモデルr個が、n個に達するまで繰り返し、独立事象である各試行の確率を合計したものが、アンサンブルモデルの精度Pとなります。

\[
\quad P=\sum^n_{r}{}_n C_r\epsilon^r(1-\epsilon)^{n-r}
\]

例えば、精度が70%の11個のモデルのうち、6個以上(半分以上)が正確に分類するという仮定を置いてみます。6個のモデルが正しく分類する確率(r=6の時の確率)、7個のモデルが正しく分類する確率(r=7の時の確率)……と計算し、各確率を合計すると、理論上92%もの精度に達します。そんなにオイシイ話があるのでしょうか。

\[
\quad P=\sum^{11}_{r=6}{}_{11} C_r0.7^r(1-0.3)^{11-r}=0.92
\]

※各モデルの精度は、それぞれ独立している必要があります。あるモデルAの学習に他のモデルB及びCの予測を利用している場合(スタッキング)、モデルA、B及びCで多数決のアンサンブルしてはいけません。それで精度が上がったらまあ良いのでしょうけど。

#In
from scipy.special import comb
import math

def ensemble_prob(n_classifier, p):
    k_start = int(math.ceil(n_classifier/2.))
    prob = [comb(n_classifier, k) * 
             p**k *
             (1-p)**(n_classifier-k)
             for k in range(k_start, n_classifier+1)]
    return sum(prob)

ensemble_prob(n_classifier=11, p=0.7)

#Out
0.92177520904

多数決によるアンサンブルモデルの実装

アンサンブルモデルに用いるモデルは、以下にしました。特にこだわりはないですが、同じようなモデルは避けました。

  • ロジスティック回帰
  • K近傍法
  • ランダムフォレスト
  • サポートベクタマシン
  • ナイーブベイズ

各モデルでK分割交差検証を行い、それぞれの精度を調べました。K近傍法とサポートベクタマシンの精度はゴミですが、ハイパーパラメータの最適化で改善されるかもしれないので、気にせず進めます。

#In
#層化抽出法を用いたK分割交差検証
kfold = StratifiedKFold(n_splits=5) 

models = []
cv_results = []
cv_means = []
cv_std = []
models.append(LogisticRegression())
models.append(KNeighborsClassifier(n_neighbors=2))
models.append(RandomForestClassifier())
models.append(SVC())
models.append(GaussianNB())

#各モデルでK分割交差検証を実施し、結果を'cv_results'へ格納
for model in models: 
    cv_results.append(cross_val_score(model,
                                      X_train, 
                                      y=y_train,
                                      scoring='accuracy',
                                      cv=kfold,
                                      n_jobs=-1))

for cv_result in cv_results: 
    cv_means.append(cv_result.mean())
    cv_std.append(cv_result.std())

#各モデルの精度
cv_results_df = pd.DataFrame({'Models':['Log', 'KNeighbors', 'RandomForestClassifier', 'SVC', 'GNB'],
                             'CrossValMeans':cv_means,
                             'CrossValerrors':cv_std,})

sns.barplot('CrossValMeans', 'Models', data=cv_results_df, orient='h')
plt.xlim([0.5, 0.85])
plt.xlabel('Mean Accuracy')
plt.title('Cross Validation Scores')

グリッドサーチを行い、ハイパーパラメータを最適化します。最適なパラメータは、”*_best”に格納しました。

#In
#ロジスティック回帰のグリッドサーチ
LOG = LogisticRegression()
log_param_grid = {'penalty':['l2','l1'],
                  'C':[0.001,0.01,0.1,1,10],
                  'class_weight':[None,'balanced']}
gsLOG = GridSearchCV(LOG,
                     param_grid=log_param_grid,
                     cv=kfold,
                     scoring='accuracy',
                     n_jobs=-1,
                     verbose=1)
gsLOG.fit(X_train, y_train)
LOG_best = gsLOG.best_estimator_

#K近傍法のグリッドサーチ
KNC = KNeighborsClassifier()
knc_pram_grid = {'n_neighbors':[1,3,5,7,9,11,13,15],
                'weights':['uniform','distance']}
gsKNC = GridSearchCV(KNC,param_grid=knc_pram_grid,cv=kfold,scoring='accuracy',n_jobs=-1,verbose=1)
gsKNC.fit(X_train, y_train)
KNC_best = gsKNC.best_estimator_

#ランダムフォレストのグリッドサーチ
RFC = RandomForestClassifier()
rfc_param_grid = {'max_depth':[None],
              'max_features':[1,3,5],
              'min_samples_split':[2,3,10],
              'min_samples_leaf':[1,3,10],
              'bootstrap':[False],
              'n_estimators':[100,300],
              'criterion':['gini']}
gsRFC = GridSearchCV(RFC,
                     param_grid=rfc_param_grid,
                     cv=kfold,
                     scoring='accuracy',
                     n_jobs=-1,
                     verbose=1)
gsRFC.fit(X_train, y_train)
RFC_best = gsRFC.best_estimator_

#サポートベクターマシンのグリッドサーチ
SVC = SVC(probability=True)
svc_param_grid = {'kernel':['rbf'], 
                  'gamma':[0.001, 0.01, 0.1, 1],
                  'C':[1, 10, 50, 100, 200, 300, 1000]}
gsSVC = GridSearchCV(SVC,
                     param_grid=svc_param_grid,
                     cv=kfold,
                     scoring='accuracy',
                     n_jobs=-1,
                     verbose=1)
gsSVC.fit(X_train, y_train)
SVC_best = gsSVC.best_estimator_

#ナイーブベイズ(グリッドサーチの必要なパラメータは存在しない)
GNB = GaussianNB()
GNB.fit(X_train, y_train)

ハイパーパラメータの最適化により、全体的に精度が上がりました。中でも、ランダムフォレストの精度は約82.5%をマークしています。

#In
#グリッドサーチ後の各モデルの精度
gs_best_score = [gsLOG.best_score_,
                 gsKNC.best_score_, 
                 gsRFC.best_score_,
                 gsSVC.best_score_, 
                 cv_means[-1]] #ナイーブベイズはグリッドサーチしていないため、K分割交差検証の精度を利用
gs_results_df = pd.DataFrame({'Models':['Log', 'KNeighbors', 'RandomForestClassifier', 'SVC', 'GNB'],
                             'Accuracy':gs_best_score})

sns.barplot('Accuracy', 'Models', data=gs_results_df, orient='h')
plt.xlim([0.5, 0.85])
plt.xlabel('Accuracy')
plt.title('Grid Search Best Scores')

アンサンブルモデルの作成は、上記で作成したモデルと最適なパラメータを渡すだけです。もっと重要なのが、パイプラインです。パイプラインとは、複数の処理をまとめることができる便利なツールです。通常、特徴量行列をスケーリングし、モデルで学習・評価する場合、”fit”や”transform”を繰り返す必要があり、煩雑なコードになってしまいます。

  1. スケーラーを作成し、トレーニングデータを学習。
  2. 学習したスケーラーで、トレーニングデータ及びテストデータを変換。
  3. スケール後のトレーニングデータで、モデルを学習。
  4. 学習したモデルで、テストデータを予測。

パイプラインを導入することで、特徴量行列のスケーリングとアンサンブルモデルをまとめ、1回の”fit”で済ませます。スケーリングだけでなく、主成分分析や次元削減等他の特徴量エンジニアリングも同じようにパイプラインへ追加すると良いですね。ただし、パイプラインの最後は、モデルである必要がある点は注意下さい。

#アンサンブルモデルを作成
voting_model = VotingClassifier(estimators=[('log', LOG_best), 
                                            ('knc', KNC_best),
                                            ('rfc', RFC_best), 
                                            ('svc', SVC_best),
                                            ('gnb', GNB)],
                                voting='hard', #単純な多数決
                                n_jobs=-1)

#スケーリングとアンサンブルモデルのパイプライン
voting_model_pipe = make_pipeline(StandardScaler(), voting_model)

#トレーニングデータでモデルを学習
voting_model_pipe.fit(X_train, y_train)

#学習したモデルでテストデータのターゲットベクトルのクラスを予測
y_hat = voting_model_pipe.predict(X_test)

モデルの評価

最後に、分類レポートを出力してみます。残念ながらモデルの精度は上がっていない、むしろランダムフォレストと比較すると下がっているようです。ランダムフォレストの精度が、タイタニックのデータセットで得られるほぼ限界に達している可能性があります。そのため、多数決によるアンサンブルモデルを導入すると、他のモデルがランダムフォレストの足を引っ張り、結果として精度が下がってしまうのではと考えています。

#In
#分類レポート print(classification_report(y_test, y_hat))

#Out
               precision    recall  f1-score   support

           0       0.78      0.89      0.83       127
           1       0.82      0.68      0.74        96

    accuracy                           0.80       223
   macro avg       0.80      0.78      0.79       223
weighted avg       0.80      0.80      0.79       223

残念ながら精度向上には至らなかったものの、理論上アンサンブルモデルを試す価値はあります。特に、全てのモデルが同等の精度である場合、アンサンブルモデルがその真価を発揮するのでしょう。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です