🔷

【14日目】Stackingで精度を高める【2021アドベントカレンダー】

2021/12/14に公開2

2021年1人アドベントカレンダー(機械学習)、14日目の記事になります。

https://qiita.com/advent-calendar/2021/solo_advent_calendar

テーマは Stacking になります。

スタッキング(stacking)とは「積み重ねる」を意味し、複数の学習器を組み合わせて作った学習モデル を指します。

https://rightcode.co.jp/blog/information-technology/kaggler-stacking-prediction-accuracy

Colab のコードはこちら Open In Colab

1層目 各モデルの予測結果を集計

取り回しが簡単なので、代表的な回帰モデルのうち、.fit() .predict() が使えるものをピックアップしました。

from sklearn.linear_model import Ridge
from sklearn.linear_model import Lasso
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import ElasticNet
from sklearn.svm import SVR
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.ensemble import RandomForestRegressor
import xgboost as xgb
from catboost import CatBoostRegressor

複数のモデルのインスタンスを辞書型に設定します。

見た目の問題なので必須ではないですが、CatBoost はログを非表示にしておきます。

https://catboost.ai/en/docs/concepts/python-reference_catboost_fit#silent

https://stackoverflow.com/questions/51111323/how-to-suppress-catboost-iteration-results

また、XGBoost のエラーログも出ないように設定しておきます。

https://qiita.com/tommarute/items/8702b16cc8d964502356

models = {
    "ridge":Ridge(random_state=SEED),
    "lasso":Lasso(random_state=SEED),
    "linear":LinearRegression(),
    "elastic_net":ElasticNet(random_state=SEED),
    "svm":SVR(),
    "random_forest":RandomForestRegressor(random_state=SEED),
    "gradient":GradientBoostingRegressor(random_state=SEED),
    "catboost":CatBoostRegressor(random_state=SEED, 
                                 silent=True, # ログを非表示
                                 ),
    "xgboost":xgb.XGBRegressor(
        random_state=SEED,
        objective='reg:squarederror'
        ),
    "lightgbm":lgb.LGBMRegressor(random_state=SEED),
}

GroupKFold を使っているため、必ずしも 各Fold 毎のデータは index がキレイにそろうわけではありません。
そのため、単純に処理したデータを結合すると、indexがバラバラになってしまい、カラム(各モデル)ごとの整合性がなくなってしまい、Stacking の効果が発揮されません。
そこで、sort_index() によってすべてのカラムの index を揃えてやります。

gkf = GroupKFold(n_splits=5)
groups = X_train_dropna["Genre"]

cv_result_stck = {}
pred_df = pd.DataFrame()

for i, (model_name, model) in enumerate(models.items()):

    print(i, model)

    each_model_df = pd.DataFrame()
    each_model_result = []

    for train_index, test_index in gkf.split(X_train_dropna, y_train_dropna, groups):

        X_train_gkf, X_test_gkf = X_train_dropna.iloc[train_index], X_train_dropna.iloc[test_index]
        y_train_gkf, y_test_gkf = y_train_dropna.iloc[train_index], y_train_dropna.iloc[test_index]

        model.fit(X_train_gkf, y_train_gkf)
        y_pred = model.predict(X_test_gkf)

        tmp_df = pd.DataFrame(
                        [y_pred],
                        columns=test_index
                    )

        rmse = mean_squared_error(y_test_gkf, y_pred, squared=False)
        each_model_result.append(rmse)
        each_model_df = pd.concat([each_model_df , tmp_df.T]) # 各KFold ごとの予測結果をDataFrameに縦に並べる

    cv_result_stck[model_name] = each_model_result # 各モデルのRMSEを集計
    each_model_df.columns = [model_name] # カラム名をモデル名に変更
    pred_df = pd.concat([pred_df, each_model_df.sort_index()], axis=1) # 予測結果集計用DataFrameに各モデルの予測結果をくっつける
pred_df.head()

最も精度 (今回はRMSE) のよいモデルの名前を取得してやります。

2層目の 学習モデルに使うためと、一番精度のよいカラムを Group に設定するためです。

best_model_name = ""
best_rmse = 9999.9999

for model_name, rmse in cv_result_stck.items():
    print(model_name, np.mean(rmse))
    if best_rmse > np.mean(rmse):
        best_rmse = np.mean(rmse)
        best_model_name = model_name

print()
print(f"Best model is {best_model_name}. Score is {best_rmse}")

XGBoost が一番よい精度が出たようです。

出力:
Best model is xgboost. Score is 0.03133802749741507

2階層目 各モデルの予測結果をもとに予測

最も良い精度の出たモデルを使って2層目のデータを学習・推論してみます。

# 学習・推論
gkf = GroupKFold(n_splits=5)

groups = pred_df[best_model_name]

cv_result_stck_best_model = []

for train_index, test_index in gkf.split(pred_df, y_train_dropna, groups):

    X_train_gkf, X_test_gkf = pred_df.iloc[train_index], pred_df.iloc[test_index]
    y_train_gkf, y_test_gkf = y_train_dropna.iloc[train_index], y_train_dropna.iloc[test_index]

    # 学習、推論

    best_model = models[best_model_name]

    best_model.fit(X_train_gkf, y_train_gkf)
  
    y_pred = models[best_model_name].predict(X_test_gkf)

    rmse = mean_squared_error(y_test_gkf, y_pred, squared=False)
    cv_result_stck_best_model.append(rmse)

次に、Optuna + LightGBM を使ってみます。

# 学習・推論
gkf = GroupKFold(n_splits=5)

params = {
          'task': 'train',              # タスクを訓練に設定
          'boosting_type': 'gbdt',      # GBDTを指定
          'objective': 'regression',    # 回帰を指定
          'metric': 'rmse',             # 回帰の損失(誤差)
          'learning_rate': 0.1,         # 学習率
          'deterministic':True,         # 再現性確保用のパラメータ
          'force_row_wise':True,        # 再現性確保用のパラメータ
          'seed': SEED                  # シード値
          }

groups = pred_df[best_model_name]

cv_result_stck_lgbm = []

for train_index, test_index in gkf.split(pred_df, y_train_dropna, groups):

    X_train_gkf, X_test_gkf = pred_df.iloc[train_index], pred_df.iloc[test_index]
    y_train_gkf, y_test_gkf = y_train_dropna.iloc[train_index], y_train_dropna.iloc[test_index]

    # 学習、推論
    lgb_train = opt_lgb.Dataset(X_train_gkf, y_train_gkf)
    lgb_test = opt_lgb.Dataset(X_test_gkf, y_test_gkf, reference=lgb_train)

    model = opt_lgb.LightGBMTuner(
                    params,                    # ハイパーパラメータをセット
                    lgb_train,              # 訓練データを訓練用にセット
                    valid_sets=[lgb_train, lgb_test], # 訓練データとテストデータをセット
                    valid_names=['Train', 'Test'],    # データセットの名前をそれぞれ設定
                    num_boost_round=100,              # 計算回数
                    early_stopping_rounds=50,         # アーリーストッピング設定
                    evals_result=lgb_results,
                    verbose_eval=-1,                  # ログを最後の1つだけ表示
                    show_progress_bar = False,        # ログの非表示
                    optuna_seed=SEED,
                    )
    
    # 訓練の実施
    model.run()
    
    best_params = model.best_params
    

   # 損失推移を表示
    loss_train = lgb_results['Train']['rmse']
    loss_test = lgb_results['Test']['rmse']   
    
    fig = plt.figure()
    
    plt.xlabel('Iteration')
    plt.ylabel('logloss')

    plt.title(f"fold:{fold}")
    plt.plot(loss_train, label='train loss')
    plt.plot(loss_test, label='test loss')
    
    plt.legend()
    plt.show()

    # 推論
    best_model = model.get_best_booster()

    y_pred = best_model.predict(X_test_gkf)

    # 評価
    rmse = mean_squared_error(y_test_gkf, y_pred, squared=False)
    cv_result_stck_lgbm.append(rmse)
print("RMSE:", round(np.mean(cv_result), 4))
print(f"Best Model {best_model_name} Stacking RMSE:", round(np.mean(cv_result_stck_best_model), 4))
print("Optuna LightGBM Stacking RMSE:", round(np.mean(cv_result_stck_lgbm), 4))
項目 RMSE
通常のOptuna + LightGBM 0.1799
ベストモデルで Stacking 0.0501
Optuna + LightGBM で Stacking 0.0501

14日目は以上になります、最後までお読みいただきありがとうございました。

その他参考サイト

https://qiita.com/roki18d/items/03ced79f87769b071eec

Discussion

pk_16pk_16

Optuna + LightGBMの交差検証なのですが、
(>for train_index, test_index in gkf.split(pred_df, y_train_dropna, groups):)
各モデル(ridgeやlassoなど)を格納したpred_dfを訓練データにするとtestデータで予測すると(predict = best_model.predict(test_df))
「testデータの特徴量数が訓練データと異なっています」、と怒られます。

この場合、交差検証のpred_dfではなく、train_dfを訓練データに入れるべ気だと考えていますが、如何でしょうか?

pk_16pk_16

すいません、追記です。
最も良い精度の出たモデルを使って2層目のデータを学習・推論するという事で、pred_dfを使用するという事は理解できていて、RMSEまでは実行できるのですが、いざテストデータで予測してみようとなるとpred_dfは今回10のモデルが特徴量として入っているので、test_dfで実施した際は「testデータの特徴量数が訓練データと異なっています」、と怒られますとなります。

自身として悩んでいる為、意見交換して頂けると幸甚です。