はじめに
機械学習を一通り学び、実際にどう使用するのか学ぶためにKaggleコンペに参加している。
コンペ参加は4回目(タイタニック含む)である。
テーブルデータを扱いたいため、稼働中であるKaggle-PlayGroundをやってきた。
コンペにもだいぶ慣れたため、データ分析過程を共有したい。
何某かの参考になれば幸いである。
コンペ | KaggleScore | 順位/参加人数 | 上位% | 1位Score |
---|---|---|---|---|
アワビの年齢当て | RMSLE:0.14728 | 832/2608 | 31.9% | 0.14374 |
自動車保険の好意的反応 | ROC-AUC:0.89186 | 435/2236 | 19.4% | 0.89754 |
🆕キノコの食用・有毒当て | MCC:0.98488 | 472/2424 | 19.4% | 0.98514 |
PlayGroundは機械学習初学者向けのコンペである。賞金やメダルは付与されないが、シンプルで基本的な機械学習の実践の場であり、ここから学ぶことは多い。
基本的とはいえなかなか上位10%にたどり着けない難しさがある。
要約
Kaggleコンペにおける食べられるキノコと有毒なキノコの分類問題に取り組んだ。
MCC:0.982と上々の開始であったが、上位はすでに0.985をマークしていた。0.001の差で勝負していくこととなった。
欠損値80%以上の特徴量が見られたが、欠損値以外のユニークな値に重要な値が隠れていた。そのため欠損値は文字列nonで補完しskitlearnでラベルエンコーディングを行った。外れ値は量的変数は平均値で補完し、その他外れ値・異常値は処理せずそのままの状態で扱った。
学習のアプローチとしてLGBM,XGboost,RandomForest,NeuralNetworkを用いてモデルを構築し比較、手動でパラメーター範囲に当たりをつけ、optunaにてパラメータチューニングを行い性能を向上させた。バギング、スタッキングのアンサンブル手法を試したがPublicScoreの性能の向上は見られなかった。
最終的にLGBM、XGBoost単体の各モデルにてMCCPlibateScore:0.98502までkaggle提出スコアを同様に上げ、これら2つを最終提出とした。
コンペの最終結果であるPublicScoreはLGBMモデルで0.98488, XGboostで0.98485とLGBMモデルが最良であった。順位は472/2424位(上位19.4%)であった。
提出はしなかったダメ押しのLGBM・XGBoostアンサンブル学習のバギング・スタッキング各10回のモデルも同様にpablicScore0.98488という性能を示した。
1位ScoreはPublic0.98537, Plivate0.98514でその差は0.00265となった。
当コンペ3位のソリューションでは、大規模な計算リソースを用いてAutoGluonのモデルを効率的にトレーニングするために、分散処理環境とツール(RayやSLURMクラスター)を活用している。つまり処理環境を整えて時間を掛けたということらしい。
私個人のコンペの成績は振るわなかったものの、コードやディスカッションから多くの学びを得たためモデル構築のアプローチを共有したい。
コンペ概要
- Kaggle Binary Prediction of Poisonous Mushrooms
- 期間:2024.08.01-08.31
- 参加期間:2024.08.01-08.31
- 目的:キノコの物理的特性に基づいて、キノコが食べられるか有毒かを予測する。(分類)
- ターゲット:'class' (e=食用, p=有毒)
- 評価指標:MCC
評価指標:MCC
MCCについて
MCC(Matthews Correlation Coefficient)は、分類モデルの性能を評価する指標で、予測と実際のラベルの一致度を測ります。値は-1から1までで、1が完全一致、0がランダム、-1が完全不一致を示しす。
クラス不均衡がある場合や二値分類の性能を総合的に評価したいときに使われます。特に、陽性と陰性のサンプル数が大きく異なるデータセットで有効で、精度、再現率、F1スコアなどの他の指標と異なり、全体的なバランスを考慮して性能を評価する。
当コンペの目的変数であるe=食用, p=有毒はクラス不均衡が見られるが大きいとは言えない。
今回この評価指標が使われた背景は、キノコを分類してそれが毒キノコであった場合致命的となるため、全体的な予測精度を正確に評価したいということではないかと推測する。
毒キノコの誤検出を減らすことが重要になるためRecall(再現率)が適切なのでは?と思ったが、モデルの学習をすると一定の学習から0.99という数値になってくる。
コンペの性質上MCCによる評価で微細な評価を競うことが目的となるのかもしれない。
環境
kaggleノートブック、GoogleColabでのGPU使用を試したが、ページのクラッシュやカーネルのクラッシュが相次ぎ、なくなくローカル環境VSCodeCPUですべて実行した。
データの確認
データの概要
#データの読み込み
df_train = pd.read_csv('../dataset/train.csv')
df_test = pd.read_csv('../dataset/test.csv')
#予測用データに目的変数と疑似ラベル0を追加
df_test['class'] = 0
df = pd.concat([df_train, df_test], ignore_index=True)
#表示
display(df_train.head())
display(df_test.head())
以下に特徴量の概要を一覧にする。
連続変数 = Continuous(Con)
カテゴリカル変数 = Binary, Categorical(Cat)
欠損値(個):トレーニングデータとテストデータを合わせた数
データセットには、キノコの各種特徴量が含まれており、食べられるか有毒かのラベル('class' (e=食用, p=有毒))が付けられている。各種特徴量には多くの欠損値が含まれ、また異常値も確認された。
前処理・クリーニング
ベースモデルLGBM
df_trainに各段階の前処理・クリーニングを終えたデータを格納。
LGBMは処理が早く、文字列のラベルエンコーディングで欠損値まで対応できるということから最初のベースモデルとしてよく使用している。
ここでもLGBMでベースのモデルを構築していく。
単純に特徴量をsciktlearnでラベルエンコーディングをしたものをデフォルトのパラメータで学習してみる。
df_train = pd.read_csv('dataset/train.csv')
df_test = pd.read_csv('dataset/test.csv')
df_test['class'] = 0
df = pd.concat([df_train, df_test], ignore_index=True)
# LabelEncoderのインスタンスを作成
le = LabelEncoder()
# object型の列にラベルエンコーディングを適用する前に、全てを文字列に変換
for col in df.select_dtypes(include=['object']).columns:
df[col] = df[col].astype(str)
df[col] = le.fit_transform(df[col])
num_train_rows = len(df_train)
# データフレームを分割
df_train = df.iloc[:num_train_rows].reset_index(drop=True)
df_test = df.iloc[num_train_rows:].reset_index(drop=True)
df_test = df_test.drop('class',axis=1)
df = df_train#.sample(n=10000, random_state=42)
X = df.drop(['id', 'class'], axis=1)
y = df['class']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
lgbm_params = {}
lgbm_model = lgb.LGBMClassifier(**lgbm_params)
lgbm_model.fit(X_train, y_train)
y_pred = lgbm_model.predict(X_test)
mcc = matthews_corrcoef(y_test, y_pred)
print(f'MCC: {mcc:.5f}')
MCC: 0.97727
0.97727をベースラインとする。
以降の前処理・クリーニング、特徴量エンジニアリングのMCC値はLGBMで構築したモデルの評価である。
欠損値
欠損値は上記のマップの通りで、中には80%を超える特徴量も確認できる。
最初のアプローチとして、欠損値が80%を超える特徴量を削除して対応した。
結果はMCC:0.97220とベースラインから性能が悪化。
ここで、欠損値の扱いについてディスカッションから興味深い情報を得た。
このディスカッションでは、PPS(特徴量とターゲット変数との関係性を評価するための指標)を使用して85%以上の欠損値があったstem-rootが重要な特徴量であり、その他にもPPSが高い特徴量にはキノコが食用であることをほぼ保証する特定の値があることを示唆している。
このことから、以下の手順でエンコーディングを行った。
- 連続変数の欠損値は平均値で補完。
- カテゴリカル変数の欠損値を文字列nonに置き換える。
- データフレームすべてをラベルエンコーダーでラベルエンコーディング。
するとMCC:0.97655とベースラインからは若干減少したものの欠損値の多い特徴量を削除した場合より良い性能が示された。
特徴量重要度を通じて、モデルに対する各特徴量の影響を確認する。
# 特徴量重要度のプロット
feature_importances = pd.Series(lgbm_model.feature_importances_, index=X.columns)
feature_importances = feature_importances.sort_values(ascending=False)
plt.figure(figsize=(10, 6))
sns.barplot(x=feature_importances, y=feature_importances.index)
plt.title('Feature Importances')
plt.xlabel('Importance Score')
plt.ylabel('Features')
plt.show()
# 混同行列の作成とプロット
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.title('Confusion Matrix')
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.show()
確かに欠損値が64%であるstem-surfaceがモデルの特徴量重要度の上位に位置していることが分かる。
stem-rootも欠損値88%にも関わらず一定の重要度スコアを持っていることが示された。
異常値
例えばcap-shapeにはこのようなユニークな値がある。
df_train['cap-shape'].unique()
['f' 'x' 'p' 'b' 'o' 'c' 's' 'd' 'e' 'n' nan 'w' 'k' 'l' '19.29' '5 f' 't'
'g' 'z' 'a' '2.85' '7 x' 'r' 'u' '3.55' 'is s' 'y' '4.22' '3.6' '21.56'
'i' '6 x' '24.16' '8' 'm' 'ring-type' '10.13' 'is p' '7.43' 'h' '0.82'
'10.46' '2.77' '2.94' '12.62' '5.15' '19.04' '4.97' '49.21' 'b f' '9.13'
'1.66' '3.37' '7.21' '3.25' '11.12' '3 x' '4.3' '7.41' '6.21' '8.29'
'54.78' '20.25' '3.52' '3.04' '2.63' '3.91' '6.44' '8.3' '7.6' '17.44'
'4.33' '2.82' '6.53' '19.06']
カテゴリカル変数の中に数値や特徴量カラム名が紛れ込んでいる。
cap-shapeは傘の形状のため、データとしてはアルファベットで示されるはずである。
紛れ込んでいる数値やカラム名はデータの入力ミスとして扱い、欠損値扱いとしてエンコーディングを行った。
MCC:0.97850
比較のため欠損値とは別のグループとして扱ってみる。
MCC:0.97726
異常値をそのままエンコーディングしたらどうなるのか。
MCC:0.97862
MCCがより異常値をそのままエンコーディングを採用。
EDA・特徴量エンジニアリング
「特徴量棒グラフ」と「目的変数と各特徴量の割合棒グラフ」
すべてのユニークな値
見にくいのでユニークな値の頻度上位20のみを抽出
量的変数
相関係数ヒートマップ
特徴量重要度
混合行列ヒートマップ
仮説・検証
相関係数の高いもの、特徴量重要度の高いものを中心に組み合わせて新しいカラムを作ったら性能が良くなるのでは?
#df['stem-width*cap-diameter'] = pd.factorize(df['stem-width'].astype(str) + df['cap-diameter'].astype(str))[0]
#df['veil-type*veil-color'] = pd.factorize(df['veil-type'].astype(str) + df['veil-color'].astype(str))[0]
#df['has-ring*ring-type'] = pd.factorize(df['has-ring'].astype(str) + df['ring-type'].astype(str))[0]
#df['veil-color*has-ring'] = pd.factorize(df['veil-color'].astype(str) + df['has-ring'].astype(str))[0]
#df['stem-width*cap-surface'] = pd.factorize(df['stem-width'].astype(str) + df['cap-surface'].astype(str))[0]
#df['stem-width*gill-attachment'] = pd.factorize(df['stem-width'].astype(str) + df['gill-attachment'].astype(str))[0]
#df['stem-width*stem-color'] = pd.factorize(df['stem-width'].astype(str) + df['stem-color'].astype(str))[0]
結果:どの組み合わせも性能は悪化した。
出現頻度の多いラベルとそれ以外でラベルエンコーディング
ex.cap-shape
#4.cap-shape : 欠損値少数。異常値あり。出現頻度の多いラベルとそれ以外は最頻値x(0)でラベルエンコーディング。
cap_shape_label = {'x': 0, 'f': 1, 's': 2, 'b': 3, 'o': 4, 'p':5, 'c':6}
df['cap-shape'] = df['cap-shape'].map(cap_shape_label).fillna(0).astype('int8')
df['cap-shape']
結果:異常値をそのままでラベルエンコーディングしたほうが性能が良い。
異常値を異常値グループ、欠損値を欠損値グループに分けてワンホットエンコーディング
結果:ラベルエンコーディングのほうが性能が良い。
特徴量エンジニアリングコード全体
#1.df_testに疑似クラスラベル0を追加。
# 学習用データと予測用データを結合。
df_train = pd.read_csv('dataset/train.csv')
df_test = pd.read_csv('dataset/test.csv')
df_test['class'] = 0
df = pd.concat([df_train, df_test], ignore_index=True)
#2.クラスラベルの把握のためエンコード
class_label = {'e': 0, 'p': 1}
df['class'] = df['class'].map(class_label).fillna(3).astype('int8')
df['class']
#3.量的変数の欠損値に関しては平均で補完
cap_diameter_mean = df['cap-diameter'].mean()
df['cap-diameter'].fillna(cap_diameter_mean, inplace=True)
df['cap-diameter'] = df['cap-diameter'].astype('int8')
stem_height_mean = df['stem-height'].mean()
df['stem-height'].fillna(stem_height_mean, inplace=True)
df['stem-height'] = df['stem-height'].astype('int8')
df['stem-width'] = df['stem-width'].astype('int8')
#4.質的変数の欠損値はnonグループとして扱うため文字列にで補完。
df = df.fillna("non")
#5.異常値である数値を文字列に変換
object_columns = df.select_dtypes(include=['object']).columns
df[object_columns] = df[object_columns].astype(str)
#6.scikitlearnでラベルエンコーディング
categorical_columns = df.select_dtypes(include=['object']).columns
label_encoders = {}
for column in categorical_columns:
le = LabelEncoder()
df[column] = le.fit_transform(df[column])
label_encoders[column] = le
#7.結合したdfを再び学習用データと予測用データに分割。
num_train_rows = len(df_train)
df_train = df.iloc[:num_train_rows].reset_index(drop=True)
df_test = df.iloc[num_train_rows:].reset_index(drop=True)
#8.df_testから疑似クラスラベルを除去
df_test = df_test.drop('class',axis=1)
モデルとアプローチ
モデルを選定するために、pycaretを使用する。
exp = setup(data = df_train, target = 'class', session_id=42, ignore_features = 'id')
best = compare_models()
pycaretからRandomForestClassifierのMCC評価が0.9808と良好である。
Pycaretで上位に上がったRandomForest、CatBoost、XGBoost、LightGBMでシンプルなモデルを作成し実行した。
以下の記事を参考にニューラルネットワークの学習予測も行うこととした。
Don't neglect neural networks!(ニューラルネットワークを無視しないで!)
手動でパラメータの範囲に当たりをつけた後、optunaにて各モデルのパラメータ最適化を行い、それぞれ下記MCC値を記録した。
RondomForest
多数の決定木を用いて予測を行うバギング手法
X = df_train.drop(['id','class'], axis=1)
y = df_train['class']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
rf_model = RandomForestClassifier(n_estimators=100, random_state=42, verbose=1)
rf_model.fit(X_train, y_train)
y_pred = rf_model.predict(X_test)
mcc = matthews_corrcoef(y_test, y_pred)
print(f'MCC: {mcc:.6f}')
MCC: 0.982954
optuna,グリッドサーチでのパラメータチューニングでは性能が上がらず。最初のベースとして学習した上記が一番良い性能になった。
CatBoost
決定木に基づく勾配ブースティング
df_cat = df_train#.sample(n=100000, random_state=42)
# 特徴量とターゲットに分ける
X = df_cat.drop(['id', 'class'], axis=1)
y = df_cat['class']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Poolの作成
cat_features = [c for c in X_train.columns if X_train[c].dtype == 'object']
X_train_pool = Pool(X_train, y_train, cat_features=cat_features)
X_test_pool = Pool(X_test, y_test, cat_features=cat_features)
# CatBoostモデルの設定
cb_model = CatBoostClassifier(
loss_function='Logloss',
eval_metric='MCC',
learning_rate=0.08,
iterations=1412,
depth=12,
random_strength=0,
l2_leaf_reg=0.5,
random_seed=42,
)
# モデルのトレーニング
cb_model.fit(X=X_train_pool, eval_set=X_test_pool, verbose=100, early_stopping_rounds=100)
# テストデータでの予測
y_pred = cb_model.predict(X_test_pool)
# MCCの計算
mcc = matthews_corrcoef(y_test, y_pred)
print(f'MCC: {mcc:.5f}')
MCC: 0.98424
XGBoost
決定木に基づく勾配ブースティング
X = df.drop(['id', 'class'], axis=1)
y = df['class']
# データの分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# XGBoost用のDMatrixに変換
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)
# パラメータ設定
xgb_params = {
'objective': 'binary:logistic',
'eval_metric': 'logloss',
'verbosity': 0, # ログの表示レベル
'learning_rate': 0.03, # 学習率
'max_depth': 18, # ツリーの最大深さ
'min_child_weight': 2.5, # ノードに必要な最小重み
'subsample': 0.8, # サンプルの割合
'colsample_bytree': 0.5, # 特徴量の割合
'gamma': 0.25, # 最小損失削減
'scale_pos_weight': 1 # クラス重み
}
# 学習時のコールバック(EarlyStoppingを追加)
callbacks = [
xgb.callback.EarlyStopping(
rounds=100 # 100ラウンドの間改善がなければ早期停止
),
xgb.callback.EvaluationMonitor(period=100) # 評価モニタリング
]
# モデルの学習
evals = [(dtrain, 'train'), (dtest, 'test')]
xgb_model = xgb.train(xgb_params, dtrain, num_boost_round=500, evals=evals, callbacks=callbacks)
# 予測
y_pred_proba = xgb_model.predict(dtest)
y_pred = [1 if prob >= 0.5 else 0 for prob in y_pred_proba]
# MCCで評価
mcc = matthews_corrcoef(y_test, y_pred)
print(f'MCC: {mcc:.5f}')
MCC:0.9848
bagging5回、10回と試すが性能改善なし。
LightGBM
決定木に基づく勾配ブースティング
X = df_train.drop(['id', 'class'], axis=1)
y = df_train['class']
# データの分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# LightGBMのパラメータ
lgbm_params = {
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': 'binary_logloss',
'verbose': 1,
'learning_rate': 0.01,
'n_estimators': 1097,
'num_leaves': 956,
'min_child_samples': 112,
'reg_alpha': 0.19812645495932385,
'reg_lambda': 0.34036226127746694,
'colsample_bytree': 0.43199251589983806
}
# モデルの初期化
lgbm_model = lgb.LGBMClassifier(**lgbm_params)
# コールバックの設定
evaluations_result = {}
# モデルの学習
lgbm_model.fit(
X_train, y_train,
eval_set=[(X_test, y_test)],
eval_metric='mcc',
callbacks=[
lgb.record_evaluation(evaluations_result),
lgb.early_stopping(300),
lgb.callback.print_evaluation(period=100)
],
)
# テストデータでの予測
y_pred = lgbm_model.predict(X_test)
# MCCの計算
mcc = matthews_corrcoef(y_test, y_pred)
print(f'MCC: {mcc:.5f}')
MCC: 0.98486
bagging10回
X = df.drop(['id', 'class'], axis=1)
y = df['class']
# データの分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# LightGBMのパラメータ
lgbm_params = {
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': 'binary_logloss',
'verbose': 1,
'learning_rate': 0.01,
'n_estimators': 1097,
'num_leaves': 956,
'min_child_samples': 112,
'reg_alpha': 0.19812645495932385,
'reg_lambda': 0.34036226127746694,
'colsample_bytree': 0.43199251589983806
}
# LightGBMの初期化
base_lgbm_model = lgb.LGBMClassifier(**lgbm_params, random_state=13)
# バギングの設定
bagging_model = BaggingClassifier(estimator=base_lgbm_model, n_estimators=10, random_state=42, n_jobs=-1)
# モデルの学習
bagging_model.fit(X_train, y_train)
# テストデータでの予測
y_pred = bagging_model.predict(X_test)
# MCCの計算
mcc = matthews_corrcoef(y_test, y_pred)
print(f'Bagging MCC: {mcc:.6f}')
Bagging MCC: 0.984868
MLP(Multi-Layer Perceptron)
ニューラルネットワーク
X = df_train.drop(['id', 'class'], axis=1)
y = df_train['class']
# データの分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# KerasでシンプルなMLPモデルの作成
model = Sequential([
Dense(64, activation='relu', input_shape=(X_train.shape[1],)),
Dense(32, activation='relu'),
Dense(1, activation='sigmoid')
])
# モデルのコンパイル
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
# 早期終了のコールバック
early_stopping = EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True)
# モデルの訓練
history = model.fit(X_train, y_train, epochs=100, validation_split=0.2, callbacks=[early_stopping], batch_size=32)
# 確率予測
print("Making predictions with Keras MLP model...")
nn_probs = model.predict(X_test).flatten()
# 二値分類の予測
nn_preds = np.where(nn_probs > 0.5, 1, 0)
# MCCでの評価
nn_mcc = matthews_corrcoef(y_test, nn_preds)
print("Keras Neural Network MCC:", nn_mcc)
Keras Neural Network MCC: 0.9724279131944976
特徴量選択
from boruta import BorutaPy
import numpy as np
np.object = object
np.bool = bool
np.int = int
np.float = float
np.typeDict = {k: v for k, v in np.sctypeDict.items() if isinstance(v, type)}
X = df_train.drop(columns=['class', 'id'])
y = df_train['class']
# データのサブセットをランダムに抽出(例として2,000,000行)
sample_size = 3000000
sampled_data = df_train.sample(n=sample_size, random_state=42)
X_sampled = sampled_data.drop(columns=['class', 'id'])
y_sampled = sampled_data['class']
# データの分割
X_train, X_test, y_train, y_test = train_test_split(X_sampled, y_sampled, test_size=0.2, random_state=42)
# RandomForestClassifierの設定
rf_model = RandomForestClassifier(n_estimators=100, random_state=42, verbose=1)
# Borutaの設定
feat_selector = BorutaPy(estimator=rf_model, n_estimators=100, verbose=0, random_state=42)
# フィーチャー選択の実行
feat_selector.fit(X_train.values, y_train.values)
# 選択された特徴量のカラム名
selected_features = X_train.columns[feat_selector.support_].tolist()
print(f"選択された特徴量のカラム名: {selected_features}")
num_selected_features = len(selected_features)
print(f"選択された特徴量のカラム数: {num_selected_features}")
# 選択された特徴量だけを使ったデータセットの作成
X_train_selected = X_train[selected_features]
X_test_selected = X_test[selected_features]
# モデルの再訓練
rf_model.fit(X_train_selected, y_train)
# テストデータでの予測
y_pred = rf_model.predict(X_test_selected)
# MCCの計算
mcc_score = matthews_corrcoef(y_test, y_pred)
print(f"MCCスコア: {mcc_score}")
選択された特徴量のカラム名: ['cap-diameter', 'cap-shape', 'cap-surface', 'cap-color', 'does-bruise-or-bleed', 'gill-attachment', 'gill-spacing', 'gill-color', 'stem-height', 'stem-width', 'stem-root', 'stem-surface', 'stem-color', 'veil-type', 'veil-color', 'has-ring', 'ring-type', 'spore-print-color', 'habitat', 'season']
選択された特徴量のカラム数: 20
MCCスコア: 0.9832989624318514
Borutaを使用して特徴量選択をしたが、id以外の元のカラムをすべて使用することとなった。
アンサンブル学習
異なるアルゴリズムのモデルを用いてスタッキングでアンサンブル学習を行う。
いくつかのモデルの組み合わせを一通り試したが、どれも性能の改善には繋がらなかった。
評価方法
各モデルで性能が改善した場合に、k分割交差検証を実施し、モデルの安定性と一般化能力を確認。
それぞれのMCCの値が最高値を更新した場合Kaggleへ提出をした。
結果と考察
kaggleのコードやノートブックを確認。kaggleノートブックにて試してみるが、ページのクラッシュでデータが消えることが頻発。googlecolabでも同様にGPU環境での動作でカーネルクラッシュが続いた。
そのためすべてVScodeのローカル環境、CPUでの実行となった。
3日に1度当コンペに時間を掛け提出を繰り返した。欠損値・異常値の取り扱い方の変更や特徴量エンジニアリング、パラメータチューニング、バギングやスタッキングなど試しつつ0.98502で頭打ちとなる。
計11回の提出でPublisScoreが良好であったLGBM,XGBoostのモデルを最終提出とした。
結果はダメ押しのLGBM・XGBoostアンサンブルで各モデル10回バギング10回スタッキングで約1800min時間を掛けたものとLGBM10回バギングの成績がPrivateScoreで一番良好であった。
若干のスコア減少は合ったものの、順位は上昇し、順位は472/2424位(上位19.4%)であった。
1位ScoreはPublic0.98537, Plivate0.98514でその差は0.00265と非常に僅差の戦いである。それでも400位以上の差がついてしまった。
当コンペ3位のソリューションでは、大規模な計算リソースを用いてAutoGluonのモデルを効率的にトレーニングするために、分散処理環境とツール(RayやSLURMクラスター)を活用したという。
つまり処理環境を整えて時間を掛けたということか。
GPU環境をうまく利用できなかったことが敗因となっている。
次回はこちらもAutoGluonを試してみようと思う。
結論
欠損値の多い特徴量をそのまま使用。異常値に関しては処理せずまるごとラベルエンコーディング。
新たな特徴量生成はせず、特徴量は与えられたカラムのうちid以外を使用した。
結果、LGBMのパラメータチューニングした単体モデルと、LGBM・XGBoostアンサンブル学習のバギング・スタッキング各10回のモデルが評価指標MCCで0.98488という性能を示した。
コンペ参加についての感想
- 欠損値多い特徴量の扱いについて
- 単純に消去してしまおうという短絡的な思考から、有益なものがあることを分析し余すところなく使用するという思考を得ることができた。
- 初期から0.982という高精度を記録できた。しかし当コンペでは0.0001の精度を争うこととなった。実用でどれだけこの差が意識されるかは疑問であるが、よりよいモデルにするために試行錯誤した過程から欠損値に対する固定観念の払拭や各特徴量のモデルへの貢献度の見方など学びを深めることができた。
- 結局最終的に使い慣れたLGBMが個人的なScoreの最高値を記録した。他のモデルに関して、パラメータチューニングなどどこをどういじれば良いのか当たりをつけられるようになればまた結果は違ったかもしれない。特にニューラルネットワークに関して隠れ層やノード数についての学習が不足しており性能を高めるほどに至れなかった。学習の継続をしていきたい。
- 特徴量のなかでも各ユニークな値と目的変数との関係性の深堀も必要であった。今後は頻度の高いユニークな値からshap値のプラスとマイナスを一覧にし、モデルに貢献した値とモデルの足を引っ張った値を厳選していくことも検討していきたい。
- 9月のPlayGroundは中古車価格の予測である。今回の分類タスクとはちがい回帰タスクとなるため、やっていきたいと思う。似たようなコンペがあるため、アプローチはそこから進められるかと思う。だがそれは競合者も同様なのでそろそろ自分なりのアプローチもしっかり仮説を立てて検討していく時期にあるのかもしれない。
参考文献
- 使用データセット:Kaggle PlayGround Binary Prediction of Poisonous Mushrooms
- コンペデータ元:Mushroom1987/04/26
- Kaggle DiscussionFeatures with a significant number of missing rows still hold substantial value!(多くの欠損値がある特徴量でも価値があります!)
- Kaggle Discussion Don't neglect neural networks!(ニューラルネットワークを無視しないで!)