この記事の趣旨
業務において機械学習をかじることがあるのだが、大体XGBoostを使ってポイ!でそのまま成果物としている。実務上は精度が大抵あれば問題ないのだが、より良い精度を追求すべきだという気持ちが前々からあったため、まとめる機会として整理してみる。
どのようなパラメータが存在するかという整理と、それらを変更した際の影響についてを試行してみる
実行環境
GoogleColaboratory
Python
パラメータの一覧について
パラメータの一覧についてはこちらでまとめられている。
当該記事の内容を踏まえるに調整できるパラメータはBooster ParametersとCommand Line Parametersのnroundsのみとのこと。
Learning Parametesrのobjectiveとeval_metricも自身で設定するものなのでパラメータなのではないかと思ったが、
objectiveは「何を目的に学習するか」、eval_metricは「何を基準に良し悪しを判断するか」なのでパラメータではなさそうという理解。
そのため二値分類ならばobjective="binary:logistic"が必須のようだ。
実装・検証
kaggleのTitanicデータ(Titanic - Machine Learning from Disaster)を利用する。
XGBoostをそのまま実行するパターンとパラメータを変えたパターン、最後にグリットサーチを実行した結果と比較
検証用の指標はAUCを用いる
実装
今回検証したいことはパラメータの効果なので、特殊な特徴量や欠損値処理は行わないで最低限のマート作成を行う
train.csvのデータの概要は下記のとおり
| カラム名 | データ概要 |
|---|---|
| PassengerId | 乗客の識別ID |
| Survived | 乗客の生存フラグ(1が生存・0が死亡) |
| Pclass | 乗客の社会的地位 |
| Name | 乗客の名前 |
| Sex | 乗客の性別 |
| Age | 乗客の年齢 |
| SibSp | 同乗している兄弟・配偶者の数 |
| Parch | 同乗している親族(親・子供)の数 |
| Ticket | チケット番号 |
| Fare | 料金 |
| Cabin | 部屋番号 |
| Embarked | 乗船した港(3箇所) |
検証
XGBoostに学習させるパラメータとしては
Pclass,Sex,Age,SibSp,Parch,Fare,Embarkedを採用する。
Sex,Embarkedについては元データではカテゴリ変数なのでフラグ値に変換を行う。
また欠損値については全てのカテゴリにおいて欠損のないレコードだけを対象とする。
前処理
import pandas as pd
# CSVファイルの読み込み
df = pd.read_csv('train.csv')
#欠損値を除いておく
use_cols = [
'Survived',
'Pclass',
'Sex',
'Age',
'SibSp',
'Parch',
'Fare',
'Embarked'
]
df = df.dropna(subset=use_cols).reset_index(drop=True)
#Embarked・Sexの前処理
train_oh = pd.get_dummies(
df[['Embarked', 'Sex']],
prefix=['Embarked', 'Sex'],
prefix_sep='_'
).astype(int)
df_merge = pd.concat([df, train_oh], axis=1)
#不要な項目は削除
drop_cols = [
'PassengerId',
'Name',
'Sex',
'Ticket',
'Cabin',
'Embarked'
]
#分析用のマート作成
mart = df_merge.drop(columns=drop_cols)
特に難しい処理を書いているわけではないので説明は割愛。
この辺の記述をもっとスマートにできればと思っているがずっと課題・・・
実装編
そのまま実行するパターン、パラメータを適当に変えてみたパターン・グリッドサーチを行ってみたパターンの3つを検証する。
①基本設定
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from xgboost import XGBClassifier
use_cols = [
'Survived',
'Pclass',
'Age',
'SibSp',
'Parch',
'Fare',
'Embarked_C',
'Embarked_Q',
'Embarked_S',
'Sex_female',
'Sex_male'
]
df_merge = df_merge[use_cols]
X = df_merge.drop(columns='Survived')
y = df_merge['Survived']
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.2,
random_state=42,
stratify=y
)
まずはモデル作成のための下準備。
検証用データの作成について知ったのだが、stratify=yを設定することでテストデータと
モデル用データの正解データの割合を揃えられるらしい。
私の実務上そこまでやったことはなかったが、必要な設定だと感じた。
②まずは何も考えずに入れてみる
#モデル1、パラメータをそのまま
model = XGBClassifier(
)
model.fit(
X_train,
y_train,
eval_set=[(X_test, y_test)],
verbose=True
)
y_pred_proba = model.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_pred_proba)
print(auc)
上記を実行した結果のAUCとしては
0.8545638945233265となる。
全然手をつけていない現時点でもかなり高いAUCとなっている。
なお初期時点ではパラメータは何も設定してないが、
print(model.get_params()) だとpython側で設定していないだけで全てNoneと出る。
model.get_booster().save_config()の方から確認すると初期値は下記の通りとなっている。
| パラメータ | 実際の値 | 意味 |
|---|---|---|
booster |
gbtree |
決定木ベースのモデルを使用 |
objective |
binary:logistic |
2値分類 |
n_estimators |
100 | 作成する木の本数 |
max_depth |
6 | 1本の木の最大深さ |
learning_rate |
0.3 | 各木による予測の修正量を調整する学習率 |
min_child_weight |
1 | 子ノードへの分割を制御するパラメータ |
gamma |
0 | ノードを分割するために必要な最小の損失減少量 |
subsample |
1 | 各木の学習に使用するデータの割合(100%) |
colsample_bytree |
1 | 各木の学習に使用する特徴量の割合(100%) |
colsample_bylevel |
1 | 各木の各レベルで使用する特徴量の割合(100%) |
colsample_bynode |
1 | 各ノードで使用する特徴量の割合(100%) |
reg_alpha |
0 | L1正則化の強さ(正則化なし) |
reg_lambda |
1 | L2正則化の強さ |
max_delta_step |
0 | 各木の更新量に対する制約(制約なし) |
max_leaves |
0 | 1本の木における葉の最大数(制限なし) |
grow_policy |
depthwise |
木を深さ方向に成長させる |
max_bin |
256 | 連続値を分割する際のビン数 |
tree_method |
auto |
木の構築方法を自動選択 |
たくさんあって手動で変更するのはちょっと大変なので、
わかりやすいn_estimators、max_depth、learning_rate、subsample、colsample_bytree、max_leaves あたりをいじってみる
③諸々手作業でいじってみる
##①n_estimatorsを100→1000に
model_n_estimators = XGBClassifier(
n_estimators=1000,
)
model_n_estimators.fit(
X_train,
y_train,
eval_set=[(X_test, y_test)],
verbose=True
)
y_pred_proba_n_estimators = model_n_estimators.predict_proba(X_test)[:, 1]
n_estimators_auc = roc_auc_score(y_test, y_pred_proba_n_estimators)
##②max_depthを6→60に
model_max_depth = XGBClassifier(
max_depth=60,
)
model_max_depth.fit(
X_train,
y_train,
eval_set=[(X_test, y_test)],
verbose=True
)
y_pred_proba_max_depth = model_max_depth.predict_proba(X_test)[:, 1]
max_depth_auc = roc_auc_score(y_test, y_pred_proba_max_depth)
##③learning_rateを0.3→0.7に
model_learning_rate = XGBClassifier(
learning_rate=0.7,
)
model_learning_rate.fit(
X_train,
y_train,
eval_set=[(X_test, y_test)],
verbose=True
)
y_pred_proba_learning_rate = model_learning_rate.predict_proba(X_test)[:, 1]
learning_rate_auc = roc_auc_score(y_test, y_pred_proba_learning_rate)
##④subsampleを1→0.5に
model_subsample = XGBClassifier(
subsample=0.5,
)
model_subsample.fit(
X_train,
y_train,
eval_set=[(X_test, y_test)],
verbose=True
)
y_pred_proba_subsample = model_subsample.predict_proba(X_test)[:, 1]
subsample_auc = roc_auc_score(y_test, y_pred_proba_subsample)
##⑤colsample_bytreeを1→0.5に
model_colsample_bytree = XGBClassifier(
colsample_bytree=0.5,
)
model_colsample_bytree.fit(
X_train,
y_train,
eval_set=[(X_test, y_test)],
verbose=True
)
y_pred_proba_colsample_bytree = model_colsample_bytree.predict_proba(X_test)[:, 1]
colsample_bytree_auc = roc_auc_score(y_test, y_pred_proba_colsample_bytree)
##⑥max_leavesを0→100に
model_max_leaves = XGBClassifier(
max_leaves=100,
)
model_max_leaves.fit(
X_train,
y_train,
eval_set=[(X_test, y_test)],
verbose=True
)
y_pred_proba_max_leaves = model_max_leaves.predict_proba(X_test)[:, 1]
max_leaves_auc = roc_auc_score(y_test, y_pred_proba_max_leaves)
print(n_estimators_auc)
print(max_depth_auc)
print(learning_rate_auc)
print(subsample_auc)
print(colsample_bytree_auc)
print(max_leaves_auc)
実行結果(順に)
0.8532454361054767
0.8599391480730223
0.850608519269777
0.8621703853955375
0.8587221095334685
0.8545638945233265
パラメータをいじった結果、元が0.854→最大0.862程度まで改善することができた。
subsample_aucを調整することが精度の向上につながることは想像通りという感じ。
またn_estimatorsを変えると木が細かくなって精度が下がってしまうという形か。
④パラメータの最適化を図る
パラメータの最適化には諸々手法が存在する
・GridSearch
・RandomSearch
・Optuna
GridSearchは一定候補を用意してその中から選んでもらう、
RandomSearchは指定した範囲の中からランダムに繰り返して最適化、Optunaは同様に指定した範囲の中からランダムに取得するが実行した内容をもとに学習を繰り返す。
Optunaの方が計算量は大きくなる。
直感Optunaが一番良さそうなのでGPTにOptunaを実装してもらう
import optuna
import xgboost as xgb
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import roc_auc_score
from xgboost import XGBClassifier
# =========================
# データ準備
# =========================
use_cols = [
'Survived',
'Pclass',
'Age',
'SibSp',
'Parch',
'Fare',
'Embarked_C',
'Embarked_Q',
'Embarked_S',
'Sex_female',
'Sex_male'
]
df_merge = df_merge[use_cols]
X = df_merge.drop(columns='Survived')
y = df_merge['Survived']
# =========================
# Train / Test 分割
# =========================
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.2,
random_state=42,
stratify=y
)
# =========================
# Optunaの目的関数
# =========================
def objective(trial):
params = {
'n_estimators': trial.suggest_int(
'n_estimators', 50, 1000
),
'max_depth': trial.suggest_int(
'max_depth', 2, 10
),
'learning_rate': trial.suggest_float(
'learning_rate', 0.01, 0.3, log=True
),
'subsample': trial.suggest_float(
'subsample', 0.5, 1.0
),
'colsample_bytree': trial.suggest_float(
'colsample_bytree', 0.5, 1.0
),
'max_leaves': trial.suggest_int(
'max_leaves', 4, 64
),
'objective': 'binary:logistic',
'eval_metric': 'auc',
'random_state': 42
}
model = XGBClassifier(**params)
scores = cross_val_score(
model,
X_train,
y_train,
cv=5,
scoring='roc_auc'
)
return scores.mean()
# =========================
# Optuna実行
# =========================
study = optuna.create_study(
direction='maximize'
)
study.optimize(
objective,
n_trials=100
)
# =========================
# 結果
# =========================
print("Best AUC:")
print(study.best_value)
print("Best Parameters:")
print(study.best_params)
実行結果
Best AUC:
0.8647893842806429
Best Parameters:
{'n_estimators': 362, 'max_depth': 3, 'learning_rate': 0.013867610669919995, 'subsample': 0.6697163675186409, 'colsample_bytree': 0.9120620332901009, 'max_leaves': 51}
先ほど適当にパラメータをいじった時に最もよかったAUCでも
0.862だったので、0.864が現段階で最良のパラメータと言える
結論
諸々工夫はしてみたもののoptunaを使って最適化を図ったとして
思いつきで実行した時のAUCよりも微々たる改善しかできていないようだった。
optunaはかなり便利かつ有用な思想だと感じたものの、実際計算時間もそこそこかかるもので
他のパラメータとの兼ね合いを考えるともっとデータの中身を精査する方が良いのではないかと考えた次第。
ただパラメータは高いに越したことがないので実務上発生したらチャレンジしてみたい。