
【機械学習】「スタッキング技術」を実装して予測精度を上げる
2021.12.20
目次
機械学習におけるスタッキング技術とは?
上位入賞者の公開コードに必ずと言っていいほど顔を出すのが「スタッキングされた学習モデル」です。
本記事では、スタッキング技術の内容から実際のコード実装までご紹介していきますが、
まず始めに、機械学習における「スタッキング技術」とは何かを見ていきたいと思います。
スタッキング(stacking)とは「積み重ねる」を意味し、複数の学習器を組み合わせて作った学習モデルのことを「スタッキングされた学習モデル」と呼びます。
なぜこんな複雑なことをするのかと言うと、各学習器の不得意な部分をフォローし合うことで隙のない学習モデルを作るためです。
人間がお互いの長所を活かし合って最大限の力を発揮するように、機械学習もお互いに助け合うことで予測精度を高めることができるのです。
スタッキングモデルの一例
では、スタッキングモデルの一例を見てみましょう。
下の図は、Kaggle と並んで世界的に人気のある大会 「KDD CUP」 において、2015年に優勝を果たした Jeong Yoon Lee 氏の学習モデルです。
スタッキング技術を用いて優勝したことにより、スタッキング技術に注目が集まるキッカケになりました。
出典:https://speakerdeck.com/smly/techniques-tricks-for-data-mining-competitions?slide=47
上記のモデルでは、3つのステージに分けてスタッキングを行い、64の個別モデルを組み合わせています。
決定係数の精度向上分はたったの「0.0015」かと思うかもしれませんが、大会ではこの差の中に10チーム以上が入ってくるほど「大きな差」となります。
この程度の精度向上であれば、スタッキング技術は不要と思う人もいるかもしれません。
しかし、精度向上分が小さいのは決定係数が既に高いのが原因であるため、決定係数が低い部分においては精度向上分も大きくなってしまいます。
言い換えれば、超プロ級の腕前でない場合は、スタッキング技術を使うことで精度向上が見込めるということです。
よって、単独モデルで行き詰った際には「スタッキング」を試すことを強くオススメします。
スタッキング技術のメリット・デメリット
メリット:予測精度が向上する
機械学習にとっては、「予測精度」こそが全てです。
単独モデルに及ばないこともありますが、基本的には精度が向上します。
デメリット①:結果の解釈・分析が難しくなる
学習モデルを複数回通すことで、リバースエンジニアリング(結果の解釈)が非常に難しくなります。
機械学習における大会等では予測精度のみが問われるため問題ありませんが、結果の解釈とセットで用いたいエンジニアにとっては注意が必要です。
対応策としては、データの考察等は単独モデルで行い、精度向上をスタッキングに求めるのが良いでしょう。
デメリット②:計算コストの増加
学習モデルが難解になる分、どうしても計算時間が長くなります。
ただし、数万行レベルであれば通常の PC でも実装が可能です。
対応策としては、下記が挙げられます。
- 計算コストの小さい学習モデルを採用する
- 各モデルの計算時間を短縮する(ハイパーパラメーターの調整)
- 学習環境の見直し(PC のスペック向上やサーバーのレンタルなど)
実際にスタッキングを実装してみよう!
使用するデータ
今回の検討では、コンクリートの強度に関するデータ「 Concrete Compressive Strength Data Set 」を使用します。
I-Cheng Yeh, “Modeling of strength of high performance concrete using artificial neural networks,” Cement and Concrete Research, Vol. 28, No. 12, pp. 1797-1808 (1998).
スタッキングモデル
今回は、2つのステージに分けてスタッキングを行います。
下の図を見ながら説明を読み進めてください。
1つ目のステージでは、「LightGBM」「RandomForest」の条件違いを、3つ + 重回帰 の計7つのモデルを作成します。
これ以外には、「ニューラルネット」や「k 近傍法」を組み込むケースを良く見かけます。
性質の異なる学習器を複数織り交ぜることにより、色んな長所を取り入れることができるためです。
自身に馴染みのある学習器を、たくさん入れてみることをオススメします!
その後、1つ目のステージにて予測された値が、2つ目のステージの入力値となります。
2つ目のステージでは、1つ目のステージにて出てきた7つの予測値を束ねます。
最後に束ねる際の学習器は、「Lasso」「Ridge」「ロジスティック」「重回帰」などが良く用いられます。
ただ今回は、説明変数の重みが完全に「0」にならないという点から「Ridge」を採用しました。
人間らしく言うと、「全ての人の意見を取り入れる」イメージです。
言葉での説明は少し難しいと思います。
でも、実際にコードを実装すると理解できてくると思いますので、安心してください!

必要なパッケージの読み込み
それでは、必要なパッケージを読み込んでいきたいと思います!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | %matplotlib inline # データ解析用ライブラリ import pandas as pd import numpy as np # データ可視化用ライブラリ import matplotlib.pyplot as plt # Scikit-learn from sklearn.metrics import mean_squared_error from sklearn.metrics import r2_score from sklearn.model_selection import KFold, StratifiedKFold from sklearn import linear_model from sklearn.ensemble import RandomForestRegressor # LightGBM import lightgbm as lgb |
LightGBM のパッケージをインストール
LightGBM のパッケージをインストールしていない方は、下記コードにてインストールを行ってください。
1 | pip install LightGBM |
データを読み込みます。
xxxの部分を各自変更して下さい。
1 2 3 4 | # データの読み込み df = pd.read_excel(r'C:xxx.xlsx', sheet_name='yyy', header=0) # 予測したい変数の設定 Objective_variable = 'Concrete compressive strength' |
目的変数のヒストグラムを確認する
目的変数のヒストグラムを確認します。
1 2 3 4 5 | # ヒストグラムの確認 data = np.array(df['Concrete compressive strength']) plt.hist(data, bins=10, histtype='barstacked', ec='black') plt.xlabel("Concrete compressive strength") plt.show() |
あまりに歪な分布をしていると、決定木系の精度は上がりません。
本記事ではデータ把握に関する部分を省いていますが、データの傾向把握は必ず実行してください。
精度が低い場合に要因を探すのは大変なので、当たり前のことを当たり前に順番に確認するクセを付けることが大事です。

スタック1段目
ランダムフォレストの予測値を作成
スタック1段目のランダムフォレストの予測値を作成します。
下記コードにより、5種類の木の深さ(2・4・6・8・10)における予測結果が格納されます。
木の深さにより学習の深さを変えることで、多様性を持たせています。
あくまで一例ですので、他のハイパーパラメーターを変えても構いません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | # スタッキング1段目(Random Forest) column_name = 'Random_Forest' # training dataの作成 train = df.drop(Objective_variable, axis=1) target = df[Objective_variable] # DataFrameの作成 Train_pred_df = pd.DataFrame(index=df.index, columns=[]) # 交差検証の回数 Fold = 10 # 木の深さが異なるモデルによる推定 for i in range(0, 5): max_depth=(i+1)*2 kf = KFold(n_splits=Fold, random_state=123, shuffle=True) pred_df_temp = pd.DataFrame({'index':0, column_name:0}, index=[1]) pred_df_temp_test = pd.DataFrame({'index':0, column_name:0}, index=[1]) # 交差検証 for train_index, val_index in kf.split(train, train.index): X_train = train.iloc[train_index] X_test = train.iloc[val_index] y_train = target.iloc[train_index] y_test = target.iloc[val_index] clf = RandomForestRegressor(n_estimators=100, criterion='mse', max_depth=max_depth) clf = clf.fit(X_train, y_train.values.ravel()) y_pred = clf.predict(X_test) y_pred = pd.DataFrame({'index':y_test.index, column_name:y_pred}) pred_df_temp = pd.concat([pred_df_temp, y_pred], axis=0) # データの整理 pred_df_temp = pred_df_temp.sort_values('index').reset_index(drop=True).drop(index=[0]).set_index('index') pred_df_temp = pd.concat([pred_df_temp, target], axis=1).rename(columns={str(Objective_variable) : 'true'}) if i == 0: Train_pred_df['true'] = pred_df_temp['true'] Train_pred_df[column_name + '_Maxdepth='+str(max_depth)] = pred_df_temp[column_name] else: Train_pred_df[column_name + '_Maxdepth='+str(max_depth)] = pred_df_temp[column_name] # 予測値の格納 Random_Forest_train_pred = Train_pred_df |
予測値が格納されていることを確認します。
1 2 | # 結果の確認 Random_Forest_train_pred |

LightGBMの予測値を作成
スタック1段目の LightGBM の予測値を作成します。
下記コードにより、3種類の学習反復回数(10・100・1000)における予測結果が格納されます。
学習反復回数を変えることで、多様性を持たせています。
こちらもあくまで一例ですので、他のハイパーパラメーターを変えても良いです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | # スタッキング1段目(LightGBM) column_name = 'LightGBM' # パラメータの設定 params = { 'objective': 'regression', 'metric': 'rmse'} # training dataの作成 train = df.drop(Objective_variable, axis=1) target = df[Objective_variable] # DataFrameの作成 Train_pred_df = pd.DataFrame(index=df.index, columns=[]) # 交差検証の回数 Fold = 10 # 反復学習回数の異なるモデルによる推定 for i in range(0, 3): num_boost_round=10**(i+1) kf = KFold(n_splits=Fold, shuffle=True, random_state=123) pred_df_temp = pd.DataFrame({'index':0, column_name:0}, index=[1]) pred_df_temp_test = pd.DataFrame({'index':0, column_name:0}, index=[1]) for train_index, val_index in kf.split(train, train.index): X_train = train.iloc[train_index] X_test = train.iloc[val_index] y_train = target.iloc[train_index] y_test = target.iloc[val_index] lgb_train = lgb.Dataset(X_train, y_train) lgb_eval = lgb.Dataset(X_test, y_test) clf = lgb.train(params, lgb_train, valid_sets=lgb_eval, num_boost_round=num_boost_round, verbose_eval=50) y_pred = clf.predict(X_test) y_pred = pd.DataFrame({'index':y_test.index, column_name:y_pred}) pred_df_temp = pd.concat([pred_df_temp, y_pred], axis=0) # データの整理 pred_df_temp = pred_df_temp.sort_values('index').reset_index(drop=True).drop(index=[0]).set_index('index') pred_df_temp = pd.concat([pred_df_temp, target], axis=1).rename(columns={str(Objective_variable) : 'true1'}) if i == 0: Train_pred_df['true1'] = pred_df_temp['true1'] Train_pred_df[column_name + '_num_boost_round='+str(num_boost_round)] = pred_df_temp[column_name] else: Train_pred_df[column_name + '_num_boost_round='+str(num_boost_round)] = pred_df_temp[column_name] # 予測値の格納 LightGBM_train_pred = Train_pred_df |
予測値が格納されていることを確認します。
1 2 | # 結果の確認 LightGBM_train_pred |

重回帰の予測値を作成
スタック1段目の重回帰の予測値を作成します。
下記コードにより、重回帰の予測結果が格納されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | # スタッキング1段目(重回帰) column_name = 'Multiple_regression' # 交差検証の回数 Fold = 10 # training dataの作成 train = df.drop(Objective_variable, axis=1) target = df[Objective_variable] # DataFrameの作成 Train_pred_df = pd.DataFrame(index=df.index, columns=[]) # モデルによる推定 kf = KFold(n_splits=Fold, shuffle=True, random_state=123) pred_df_temp = pd.DataFrame({'index':0, column_name:0}, index=[1]) pred_df_temp_test = pd.DataFrame({'index':0, column_name:0}, index=[1]) for train_index, val_index in kf.split(train, train.index): X_train = train.iloc[train_index] X_test = train.iloc[val_index] y_train = target.iloc[train_index] y_test = target.iloc[val_index] clf = linear_model.LinearRegression() clf = clf.fit(X_train, y_train.values.ravel()) y_pred = clf.predict(X_test) y_pred = pd.DataFrame({'index':y_test.index, column_name:y_pred}) pred_df_temp = pd.concat([pred_df_temp, y_pred], axis=0) # データの整理 pred_df_temp = pred_df_temp.sort_values('index').reset_index(drop=True).drop(index=[0]).set_index('index') pred_df_temp = pd.concat([pred_df_temp, target], axis=1).rename(columns={str(Objective_variable) : 'true1'}) Train_pred_df['true1'] = pred_df_temp['true1'] Train_pred_df[column_name] = pred_df_temp[column_name] # 予測値の格納 Multiple_regression_train_pred = Train_pred_df |
予測値が格納されていることを確認します。
1 2 | # 結果の確認 Multiple_regression_train_pred |

以上で、1段目の予測値作成は終わりです。
スタック2段目
では、2段目の組み合わせモデルを実装します。
今回は、「Ridge」を採用しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | # スタッキング2段目(Ridge回帰) column_name = 'Stacking' # training dataの作成 train = pred_temp.drop('true', axis=1).copy() target = pred_temp['true'].copy() # DataFrameの作成 Train_pred_df = pd.DataFrame(index=df.index, columns=[]) # 交差検証の回数 Fold = 10 # 回帰結果の格納 for i in range(0, 1): kf = KFold(n_splits=Fold, shuffle=True, random_state=123) pred_df_temp = pd.DataFrame({'index':0, column_name:0}, index=[1]) for train_index, val_index in kf.split(train, train.index): X_train = train.iloc[train_index] X_test = train.iloc[val_index] y_train = target.iloc[train_index] y_test = target.iloc[val_index] clf = linear_model.Ridge(alpha=0.1) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) y_pred = pd.DataFrame({'index':y_test.index, column_name:y_pred}) pred_df_temp = pd.concat([pred_df_temp, y_pred], axis=0) pred_df_temp = pred_df_temp.sort_values('index').reset_index(drop=True).drop(index=[0]).set_index('index') pred_df_temp = pd.concat([pred_df_temp, target], axis=1).rename(columns={str(Objective_variable) : 'true'}) if i == 0: Train_pred_df['true'] = pred_df_temp['true'] Train_pred_df[column_name] = pred_df_temp[column_name] else: Train_pred_df[column_name] = pred_df_temp[column_name] # 予測値の格納 Stacking_train_pred = Train_pred_df # 結果の確認 R2 = r2_score(Stacking_train_pred['true'], Stacking_train_pred['Stacking']) RMSE = np.sqrt(mean_squared_error(Stacking_train_pred['true'], Stacking_train_pred['Stacking'])) # 図の作成 plt.figure(figsize=(8,8)) ax = plt.subplot(111) ax.scatter('true', 'Stacking', data=Stacking_train_pred) ax.set_xlabel('True', fontsize=18) ax.set_ylabel('Pred', fontsize=18) ax.set_xlim(0,90) ax.set_ylim(0,90) x = np.linspace(0,90, 2) y = x ax.plot(x,y,'r-') plt.text(0.1, 0.8, 'R^2 = {}'.format(str(round(R2, 3))), transform=ax.transAxes, fontsize=15) plt.text(0.1, 0.9, 'RMSE = {}'.format(str(round(RMSE, 3))), transform=ax.transAxes, fontsize=15) plt.tick_params(labelsize=15) plt.title("Stacking results", fontsize=25) |

非常に高い予測精度を実現することができました!
予測結果をどの程度使ったのかを確認する
次に、どの予測結果をどの程度使ったのか、確認してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # パッケージのインポート import seaborn as sns # 偏回帰係数、切片の取得 a = clf.coef_ a = np.abs(a) b = clf.intercept_ # グラフの作成 sns.set() sns.set_style('whitegrid') sns.set_palette('gray') x = np.array(['RF_dep=2','RF_dep=4','RF_dep=6','RF_dep=8','RF_dep=10','LGBM_num=10','LGBM_num=100','LGBM_num=1000','MLT']) y = a x_position = np.arange(len(x)) fig = plt.figure() ax = fig.add_subplot(1, 1, 1) ax.barh(x_position, y, tick_label=x) ax.set_xlabel('Adoption rate') ax.set_ylabel('Method') fig.show() |

1つめのステージの予測値をバランスよく採用できています。
「 LigheGBM (勾配ブースティング)」の偏回帰係数( Partial regression coefficient )が高いのは、予測精度が最も高いためです。
一番頼れるモデルを頼りながらも、他のモデルも少し頼るというバランスの良いモデルができています。
また、全てのモデルを採用することで「過学習を防ぐ」といった重要な一面があります。
ただし、スタッキングでは「どの説明変数」が「どの程度寄与したか」の判断ができなくなる点には、注意が必要です。
一番信頼した「 LightGBM_num=1000」のモデルを確認することで予想は付きますが、定量性はどうしても欠けてしまいます。
scikit-learn
今回は、スタッキングの中身を正しく理解するため、スタッキング専用のパッケージを使いませんでした。
スタックに使用可能なパッケージは複数ありますが、皆さんに馴染みのある scikit-learn にもスタッキングのパッケージが追加されました。
※ Scikit-learn のバージョン0.22から追加されたパッケージとなりますので、アップグレードが必要です。
小回りは効きませんが、簡単にスタッキングを行うことができる便利なパッケージとなっています。
興味のある方は、ぜひ使ってみてください!
単独の学習モデルと予測精度を比較してみる
次に、単独モデルとの比較をします。
単独モデルの予測精度の方が高ければスタッキングは必要ありませんので、この確認は非常に重要です。
ランダムフォレスト単独の予測精度
ハイパーパラメータの最適化を行った後の決定係数は「0.915」となり、スタッキングの「0.941」には及びません。
余談ですが、非常にレベルの高い勝負となった要因は、質の良いデータを使っているためだと思います。
科学的な実験データは、ある程度、「説明しやすくなる傾向」にあります。
一方、人間が行動した結果のようなデータは、「説明しにくくなる傾向」にあります。

LightGBM 単独の予測精度
ハイパーパラメータの最適化を行った後の決定係数は「0.889」となり、スタッキングの「0.941」には及びません。

重回帰単独の予測精度
決定係数は「0.607」となり、スタッキングの「0.941」には及びません。
重回帰分析は結果の解釈が行いやすいという最大の長所がありますが、「精度が低い」のが短所となります。

単独モデルと比較した結果から言えること
「ランダムフォレスト」「 LighGBM」に関しては、ハイパーパラメーターの最適化を実行後に行った後の決定係数です。
にも関わらず、スタッキングとの差がでたということは、組み合わせによる良いとこ取りができたことを意味すると考察できます。
さいごに
今回は、スタッキングの背景理解から実装まで行いました。
スタッキングは複数の学習器を組み合わせてモデルを作るので、予測精度の向上が期待できます。
ただし、データによって精度が向上しない場合ももちろんあります。
そのため、注意して使うようにしてくださいね!
こちらの記事もオススメ!
書いた人はこんな人

- 「好きなことを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もりは大歓迎!
また、WEBエンジニアとモバイルエンジニアも積極採用中です!
ご応募をお待ちしております!
ITエンタメ2022.07.06高水準言語『FORTRAN』を開発したジョン・バッカス氏
ITエンタメ2022.06.22IntelliJ IDEAとkotlinを送り出したJetBrains創業物語
ITエンタメ2022.06.15【アタリ創業者】スティーブ・ジョブズを雇った男「ノーラン・ブッシュネル」
ITエンタメ2022.06.13プログラミングに飽きてPHPを開発したラスマス・ラードフ