Kaggleで学ぶ、k-fold交差検証と、特徴量エンジニアリング:僕たちのKaggle挑戦記
Kaggle公式「機械学習」入門/中級講座の次は、本稿で紹介する動画シリーズで学ぶのがオススメ。記事を前後編に分け、前編では交差検証や、特徴量エンジニアリング(標準化/正規化/対数変換/多項式と交互作用の特徴量/ターゲットエンコーディングなど)を試した体験を共有します。
こんにちは、初心者Kagglerの一色です。また、この連載の記事を開いてくれてありがとうございます!
前回は、「筆者はどうやって『Titanicの次』という壁を突き破れたのか」というテーマで、「『無料のKaggle公式講座×コンペ初参戦』で機械学習を始めよう」という記事を公開し、ページ参照数などで大きな反響がありました。やはりこの壁でつまずいている人は少なくないのだなと認識を新たにしました。
同じテーマの続編として今回と次回と次々回は、より応用的な手法に取り組んだ体験、具体的には、
した体験を共有します。この記事を読めば、機械学習で精度を高めていくための、より現実的なステップについて知ることができると思います。少なくとも次に何を学ぶべきかの参考程度にはなるのではないかと思います。
壁「Titanicの次」を突き破る! オススメの方法【第二弾】
筆者はどうやって上記のような応用的な手法を次々と試せたのか。そのきっかけになったのが、
- 前回の記事でも書いた、Abhishek Thakur氏によるYouTube動画の再生リスト「Kaggle's 30 Days of ML」内の:
- Competition Part-1: 交差検証とKaggleでの最初のサブミッション(Cross Validation & First Submission on Kaggle)
- Competition Part-2: 特徴量エンコーディング[カテゴリー変数&数値変数](Feature Engineering [Categorical & Numerical Variables])
- Competition Part-3: ターゲットエンコーディングの概要と仕組み(What is Target Encoding and how does it work?)
- Competition Part-4: Optunaを使ったハイパーパラメーターのチューニング(Hyperparameter tuning using Optuna)
- Competition Part-5: モデルブレンディング(Model Blending)
- Competition Part-6: モデルスタッキング(Model Stacking)
という6本の動画です。なお、「Part-7」もありますが、こちらは「パブリック/プライベートのリーダーボード(Public and Private Leaderboard)」が解説されています。
英語ですが自動翻訳による日本語字幕などを駆使しつつ、何とか全部視聴し、全て試しました。その実体験から、「Kaggle機械学習入門〜中級講座」の次に学ぶ教材としてオススメします。
上記のYouTube動画で描ける成長曲線と、1位の解法との比較
前回の記事は「“30 Days of ML”公式プログラム」の前半の実体験を共有する記事となっていました。今回はその後半の実体験を共有する記事となっており、そこでは、
- 回帰問題のコンペ「30 Days of ML」
というプログラム参加者限定の特別なコンペティションに参加しました。
上記の各YouTube動画に1つずつ取り組むことで、そのコンペで次のように成績が変化しました。※図と下記の箇条書きでは、トップ何%に入ったかを示しています。【数値】はモデルのスコアで、数値が小さいほど精度が高いことになります。
- 0日目(チュートリアル): Top 90%【0.73845】(ベースライン)
- 番外編(Azure ML AutoML): Top 45%【0.72252】
- 1日目(YouTube動画の1回目): Top 42%【0.72138】
- 2日目(YouTube動画の2回目): ランクアップせず【0.72241】
- 2日目(YouTube動画の3回目): ランクアップせず【0.72256】
- 3日目(YouTube動画の4回目): Top 23%【0.71903】
- 4日目(YouTube動画の5回目): Top 20%【0.71871】
- 4日目(YouTube動画の6回目): ランクアップせず【0.71872】
- 最終結果(Private Leaderboard): Top 18.5%【0.71682】
残念ながら、トップ10へのランクイン(景品獲得)にはほど遠い状況ですが、個人的には今回はなるべくしてなった結果だと思っています。根本の問題として時間不足でした。プログラム前半の内容を2日遅れで終えて、その後、Kaggle上だけでなく、ローカル環境のVisual Studio Codeで実行できる環境の整備(※いずれ記事化したいと考えています)などに1週間ほど費やしてしまったからです。
実質的に残りの4日間で上記の動画を見て進めましたが(※1日1動画ペースぐらいがちょうどよいと思います)、動画の内容を一通り行うだけで手一杯で、細かな作業まで手が回りませんでした。例えばEDA(探索的データ解析)や特徴量選択などに手が回りませんでした。コンペのDiscussionとCodeも一切見ていません。
「Kaggleはじめの一歩:Kaggle入門」で書かれていることができていないですね……(泣)。[一色]
トップ20%に入るって十分スゴイと思ったんですががが……(目指しているところが違い過ぎる)[かわさき]
とはいえ、
が最終的にモデルのブレンディングやスタッキングを行っている点などを見て、筆者が実施した上記の方向性やステップは大きく間違っているわけではないとも思いました。上記のノートブックを読むと、恐らく1位のHungNT氏も同じ動画も視聴していたと思います。よって、筆者が実施した内容を共有することは、多くの機械学習/Kaggleの初心者にとっても有用だと考えています。
また、「1位の解法と、筆者がやったことは、何が違うか」を考えることが、実力を付けるヒントになるかもしれません。上記の反省点以外で1位の解法を見て感じたのは、引き出しの多さ、使える手法の多さに違いがあるということです。前回は線形回帰/決定木/ランダムフォレスト/XGBoostを使ったので、今回もこれらを活用しました。もちろんLightGBMなども使おうと思いましたが、時間がないので断念しました。1位の解法では、XGBoost/CatBoost/HistGradientBoostingRegressorの他、コンペ「Tabular Playground Series - Feb 2021」の1位の解法を参考に「Ridge with features from Denoise Transformer AutoEncoder」というのも活用していました。こういった多種多様な手法を試すことももっと必要だったと思います。
さてそれでは、前置きが長くなりましたが、今回も、筆者がYouTube動画を視聴しならがら試した体験内容を、読者の皆さんが追体験するイメージで書いていきます。ターゲットとなる読者は、Kaggle未経験者〜私と同じような初心者Kagglerを想定しています。コンペでの実践内容を1本の記事で公開しようと考えていましたが、長くなってしまったので、YouTube動画のPart-1〜3を今回、Part-4を次回、Part-5〜6を次々回、という前中後編に分けて公開することにします。
0日目(Top 90%): ベースライン
今回は、公式のチュートリアルとして、
というシンプルなノートブックが公開されたので、これをベースラインとなる最初のノートブックとし、この状態のまま最初のサブミッション(提出)を行いました。
これだけだとつまらないと思うので、筆者が試したバージョン管理について説明してみようと思います。
バージョン管理の必要性
コンペに参加すると、データの準備〜機械学習〜モデルの評価という一連の「実験(Experiment)」のたびに、たくさんのノートブック(コード)が出来上がります。場合によっては、使うデータセットにも複数のバージョンが出来上がります。こういった実験を管理するためのツールやサービスに、MLflowやWeights & Biasesなどがあります。ちなみに、そこまで大がかりではなくてもHydraなどのツールを使ってハイパーパラメーターなどの構成をYAML形式で管理したり、もっと原始的にPythonファイルへのコマンドライン引数や、定数化/関数化/メソッド化して管理したりすることなどが考えられます。
コードのバージョン管理
Kaggleノートブック環境には、MLflowほどの実験管理機能はありません。よって運用方法によって工夫する必要があると思います。Kaggleノートブックでは、[Save Version]を実行することで、ローカル環境のGitと同じような形でノートブック(コード)にバージョン名を付けて保存できます(図3)。
モデルのバージョン管理
しかし、このコードのバージョン管理機能だけでは、さまざまな手法、例えばランダムフォレストやXGBoostなど複数の手法ごとにモデルを管理することができません。この、いわば「モデルのバージョン管理」は、ノートブックを分けて管理するのが適切ではないかと筆者は考えました。ノートブックにどういったファイル名を付けて管理するかは、読者自身が自由に決める方がよいと思います。
筆者の場合は次のようなファイル名の命名規則で管理しましたので、参考にしてもらえるとうれしいです。
- ファイルの命名規則:「<コンペ名> -v<モデルバージョン>- <モデルの内容>」
- 例「30 Days -v8- Model Stacking」
なお、今回のハイパーパラメーターの管理は、各モデルのノートブックにハードコーディングしたり定数化/関数化したりしました。Weights & Biasesなどを使うと、より柔軟な形で管理できるのだと思いますが、筆者自身が試していないし、詳しくないので今回は説明を省略します。
データのバージョン管理
今回は後述の「1日目:交差検証」で、新たなデータセットを作成しました。よってデータのバージョン管理も必要になりました。
Kaggleノートブック環境には、プライベート/パブリックなデータセットを作る機能が搭載されているので(図4)、この機能を使って適切な名前を付けることで、「データのバージョン管理」も可能です。
以上で、コード/モデル/データという最低限のバージョン管理は行えるのではないかと思います。
番外編(Top 45%):AutoML
前回、Azure Machine Learnigの自動機械学習(AutoML)を試した結果が良かったので、今回も試してみました(※詳細な内容説明は割愛します)。Stacking法のEnsemble(アンサンブル学習、前回説明)が最良の結果だったので、それをサブミッションした結果、今回もまずまずの結果でした。
評価値は0.72252で、最終結果の1位が0.71533なので、その差は0.00719。100%換算すると0.7%の差しかないことを考えると、Kaggleのような精度競争ではない実際の活用を行う上では、手軽なAutoMLだけで済ませても十分なのではないかとも思いました。そうなると、手動による高度なデータサイエンスの出番もないですよね……。どうなんでしょう。
1日目(Top 42%):交差検証
いよいよここからが、YouTube動画の内容になります。YouTube動画の1回目では、前半で交差検証を行うための準備を行い、後半でXGBoostによるモデルを作成しました。
前半:交差検証を行うための準備
まず、新たなデータセット(CSVファイル)を作成しました。元々のデータセットは、図5に示すように、各カテゴリー変数(cat0〜9)/数値変数(cont0〜13)、目的変数(target)があり、それぞれの説明変数(特徴量)が何を意味するかは分からない状態になっていました。そのため、列(column:カラム)名やその意味から特徴量を取捨選択するという手段が取れませんでした。
このデータセットをランダムに5分割して、分かれた各フォールド(fold)に0〜4というインデックス番号を付けます。
図6を見ると分かるように、[kfold]という列を1つ追加した新たなデータセットを作成し、その[kfold]列にそのフォールドインデックスを格納しました。あとはこれをCSVファイルに保存して、使い回しやすいようにします。一連のコードはリスト1のようになります。
import pandas as pd
from sklearn import model_selection
# 元々のデータセットを読み込んで、[kfold]列を作成
df_train = pd.read_csv('../input/30-days-of-ml/train.csv', index_col=None)
df_train['kfold'] = -1
# 交差検証用にデータセットを5分割(5-fold)してフォールドインデックスを格納
kf = model_selection.KFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_indices, valid_indices) in enumerate(kf.split(X=df_train)):
df_train.loc[valid_indices, 'kfold'] = fold
# 新しいデータセットをCSVファイルに保存
df_train.to_csv('train_folds.csv', index=False)
出来上がったCSVファイルは、前掲の図4の手順で、新しいデータセットとしてKaggle環境にアップロードします(※筆者の場合は、ローカル環境で作業したので、ローカル環境にデータセット用のフォルダーを作成して管理しました)。
random_state引数に42を指定している! それはともかく、これはシンプルでよい方法ですね。下のコードを見てそう思いました。
後半:XGBoostによるモデルの作成
次に、XGBoostでモデルを作成しました。そのコード自体は、前回のリスト6でも説明したので説明不要だと思います。前回と違うのは、データセットを5分割した新しいデータセットを使う点です。これに焦点を絞って説明します。
とはいえ難しくありません。リスト2はコード全体を示しており長いですが、太字部分だけに着目すると、forループで0〜4のフォールドインデックスごとに、1つの対象フォールドを検証データとし、それ以外を訓練データとして使っているだけです。
import numpy as np
import pandas as pd
from sklearn.preprocessing import OrdinalEncoder
from sklearn.metrics import mean_squared_error
from xgboost import XGBRegressor
# 5分割済みの訓練&検証データとテストデータのロード
df_train = pd.read_csv('../input/30days-folds/train_folds.csv')
df_test = pd.read_csv('../input/30-days-of-ml/test.csv')
# 利用する特徴量の選択
useful_features = [c for c in df_train.columns if c not in ('id', 'target', 'kfold')]
# カテゴリ変数の選択
categorical_cols = [c for c in useful_features if df_train[c].dtype == 'object']
valid_scores = [] # 検証データの評価スコアを保存する変数
test_predictions = [] # テストデータに対する予測結果を保存する変数
# 5分割(fold)したデータセットで交差検証
for fold in range(5):
# フォールドインデックスに一致するものを検証データとして使う
X_valid = df_train[df_train.kfold == fold].reset_index(drop=True)
# フォールドインデックスに一致しないものを訓練データとして使う
X_train = df_train[df_train.kfold != fold].reset_index(drop=True)
# テストデータのコピー
X_test = df_test[useful_features]
# 正解値の教師データ
y_train = X_train.target
y_valid = X_valid.target
# 特徴量の選択
X_train = X_train[useful_features]
X_valid = X_valid[useful_features]
X_test = X_test[useful_features]
# カテゴリー変数の序数エンコーディング
ordinal_encoder = OrdinalEncoder()
X_train[categorical_cols] = ordinal_encoder.fit_transform(X_train[categorical_cols])
X_valid[categorical_cols] = ordinal_encoder.transform(X_valid[categorical_cols])
X_test[categorical_cols] = ordinal_encoder.transform(X_test[categorical_cols])
# XGBoostのモデルを訓練(fit)する
model = XGBRegressor(
#n_jobs=-1, # CPUを使う場合
tree_method='gpu_hist', gpu_id=-1, predictor='gpu_predictor', # GPUを使う場合
random_state=fold)
model.fit(X_train, y_train)
# 検証データをモデルに入力して予測する
preds_valid = model.predict(X_valid)
# 検証データの評価スコアを取得し、ループ外の変数に保存して、スコアを出力
score_valid = mean_squared_error(y_valid, preds_valid, squared=False)
valid_scores.append(score_valid)
print(fold, score_valid)
# 同様に、テストデータをモデルに入力して予測する
preds_test = model.predict(X_test)
# 予測結果をループ外の変数に保存
test_predictions.append(preds_test)
# 出力例:
# 0 0.7245705518241878
# 1 0.7242510349403168
# 2 0.7263048437982703
# 3 0.7268360366892102
# 4 0.7257268137232141
# 5回分の検証データによる評価スコアを平均する
score_validation = np.mean(valid_scores)
print(score_validation)
# 出力例: 0.7255378561950397
# 5回分のテストデータに対する予測結果を平均する
preds_submission = np.mean(np.column_stack(test_predictions), axis=1)
print(preds_submission)
# 出力例: [8.015874 8.346141 8.373257 ... 8.317327 8.129509 8.017377]
リスト2のコードにより、図7のような流れで訓練データと検証データを変えながら、機械学習モデルの作成を5回繰り返すことになります。
交差検証を行う方法はさまざまです。前回はscikit-learnライブラリのsklearn.model_selection.cross_val_score()関数を使った交差検証を説明しました。今回のようにCSVファイルに保存しておけば、一貫したデータセットで同じ結果が出せるので、複数のノートブック間で試行錯誤する際にも比較しやすいなどのメリットがあります。
交差検証用のデータセットを利用するコード(太字部分)はたった3行なので、今回の交差検証の方法はとても簡単ですね。
2日目(ランクアップせず):特徴量エンジニアリング
[カテゴリー変数]序数エンコーディング
先ほどのリスト2では、最低限の特徴量の加工処理(エンジニアリング)として、全てのカテゴリー変数(cat0〜9)を、
- 序数エンコーディング(Ordinal Encoding)
しました。scikit-learnのOrdinalEncoderクラスを使用しています。
[カテゴリー変数]ワンホットエンコーディング
YouTube動画の2回目では、カテゴリー変数のエンコーディング手法として、
- ワンホット・エンコーディング(One-Hot Encoding)
を紹介していました。とはいえ、scikit-learnを使う場合、基本的なコードはOrdinalEncoderと変わりません。リスト2でOrdinalEncoderクラスを使っている部分を、リスト3のようにOneHotEncoderクラスを使ったコードに書き換えるだけです。ただしワンホット・エンコーディングでは、元のカテゴリー変数の列は削除して、新しいワンホット・エンコーディングの列を追加する必要があるので、少しコード量が増えています。
from sklearn.preprocessing import OneHotEncoder
# ……省略……
for fold in range(5):
# ……省略……
# カテゴリー変数のワンホット・エンコーディング
onehot_encoder = OneHotEncoder(sparse=False, handle_unknown='ignore')
X_train_ohe = onehot_encoder.fit_transform(X_train[categorical_cols])
X_valid_ohe = onehot_encoder.transform(X_valid[categorical_cols])
X_test_ohe = onehot_encoder.transform(X_test[categorical_cols])
# ワンホット・エンコーディングの列を準備
df_train_ohe = pd.DataFrame(X_train_ohe, columns=[f'ohe_{i}' for i in range(X_train_ohe.shape[1])])
df_valid_ohe = pd.DataFrame(X_valid_ohe, columns=[f'ohe_{i}' for i in range(X_valid_ohe.shape[1])])
df_test_ohe = pd.DataFrame(X_test_ohe, columns=[f'ohe_{i}' for i in range(X_test_ohe.shape[1])])
# 元のカテゴリー変数の列は削除
X_train = X_train.drop(categorical_cols, axis=1)
X_valid = X_valid.drop(categorical_cols, axis=1)
X_test = X_test.drop(categorical_cols, axis=1)
# 新しいワンホット・エンコーディングの列を追加
X_train = pd.concat([X_train, df_train_ohe], axis=1)
X_valid = pd.concat([X_valid, df_valid_ohe], axis=1)
X_test = pd.concat([X_test, df_test_ohe], axis=1)
# ……省略……
数値変数のエンコーディングについて
続いて、数値変数(cont0〜13)のエンコーディング手法として、
- 標準化(Standardization)
- 正規化(Min-Max Normarization)<筆者が独自に追加>
- 対数変換(Log transformation)
- 多項式と交互作用の特徴量(Polynomial and interaction features)
を紹介していました。これらのエンコーディング機能も、scikit-learnのsklearn.preprocessingモジュールに含まれているので、公式APIドキュメントを調べることをオススメします。
scikit-learnは強いですねぇ。ちまちまコードを書かなくても、欲しいものがちゃんとある感じがします。
[数値変数]標準化
Copyright© Digital Advantage Corp. All Rights Reserved.