雑記 in hibernation

頭の整理と備忘録

optunaで脳筋ハイパラチューニング(こっからが本番:機械学習モデルのチューニング編)

Pythonでのoptuna実装を理論面にはノータッチの脳筋スタイルでお試ししてみます。記事は3回に分けて投稿予定で、前回の第1回では、まず1変数の関数に対する最適解探索を実装してみました。第2回の今回は、機械学習モデルのパラメータ最適化を実装します。次回、第3回でデフォルトパラメータやscikit-learnのランダムサーチとの精度比較をしようと思います。

第1回の記事はこちら

toeming.hatenablog.com

前回に引き続き、備忘録です。

1. optunaって?現在は? 年収は? 彼女は? 調べてみました!

略。前回の記事をご参照ください。

f:id:toeming:20201103020116p:plain
公式webサイトより引用。ロゴめっちゃカッコいい。


2. optunaによる機械学習モデルのハイパラ探索

早速本題です。機械学習モデルのパラメータ探索をoptuneにより実装します。例題として、おなじみのタイタニックの生存予測を取り扱います。つまり二値分類問題です。モデルは非線形SVM(scikit-learnのSVC())を採用し、探索するパラメータを "C", "kernel", "gamma"とします。SVMを対象とした理由は、チューニングによる精度差がわかりやすそうだったからです。

2.1. 実装

実装手順を説明していきます。

2.1.1. パッケージのインストール・インポート

例によって"optuna"をインポートするだけでOK。例によってめっちゃ楽。

他にインポートしているパッケージは学習や描画に使う物で、optunaとの依存関係はありません。

!pip install optuna
import optuna

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn import datasets, svm, model_selection, metrics, preprocessing
from sklearn.model_selection import train_test_split


2.1.2. データの前処理

詳細説明は略。とりあえずSVMがエラー吐かない程度の処理をやってます。処理内容は以下です。

  1. 読みこむ
  2. 欠損値補完
  3. 水準数の多い変数を削除(処理がめんどいので)
  4. カテゴリ変数をダミーエンコーディング
  5. 特徴量を0~1に正規化(SVMはスケールの影響を受けやすいため)
# データセットの読み込み
dataset_path = "/drive/train.csv"
data = pd.read_csv(dataset_path)

# 最頻値による欠損補完
data_fillna = data.copy()
data_fillna['Age'].fillna(data["Age"].mode()[0] , inplace=True) 
data_fillna['Cabin'].fillna(data["Cabin"].mode()[0] , inplace=True) 
data_fillna['Embarked'].fillna(data["Embarked"].mode()[0] , inplace=True) 

# 不要な変数の削除
data_dropped = data_fillna.drop(["Name", "Ticket", "Cabin"], axis=1)

# カテゴリ変数のダミー化
data_dummy = pd.get_dummies(data_dropped, drop_first=True)
print("dummy dataset shape : " , data_dummy.shape , "\n")

# 正規化
mm = preprocessing.MinMaxScaler()
data_dummy = pd.DataFrame(mm.fit_transform(data_dummy),index=data_dummy.index, columns=data_dummy.columns)

# IDと正解ラベルだけ処理前のデータに置換
data_dummy["PassengerId"] = data_dropped["PassengerId"]
data_dummy["Survived"] = data_dropped["Survived"]

data_dummy


前処理ののちに出来上がるテーブルは以下の通りです。

PassengerId Survived Pclass Age SibSp Parch Fare Sex_male Embarked_Q Embarked_S
0 1 0 1 0.271174 0.125 0 0.014151 1 0 1
1 2 1 0 0.472229 0.125 0 0.139136 0 0 0
2 3 1 1 0.321438 0 0 0.015469 0 0 1
3 4 1 0 0.434531 0.125 0 0.103644 0 0 1
4 5 0 1 0.434531 0 0 0.015713 1 0 1
... ... ... ... ... ... ... ... ... ... ...
886 887 0 0.5 0.334004 0 0 0.025374 1 0 1
887 888 1 0 0.233476 0 0 0.058556 0 0 1
888 889 0 1 0.296306 0.125 0.333333 0.045771 0 0 1
889 890 1 0 0.321438 0 0 0.058556 1 0 0
890 891 0 1 0.396833 0 0 0.015127 1 1 0


2.1.3. 最適化

前回の記事の関数の最適化と同様、optuna.studyインスタンスを"study"として作成し、study.optimize(objective, n_trials=100) でイテレーション100回の最適解探索を実行します。

objective関数はイテレーションごとに呼び出されます。引数のtrialを通して現在のイテレーションでのハイパーパラメータを受け取り、このパラメータにてモデルの学習・評価を実行します。

objective関数内での探索対象のパラメータ取得は" c = trial.suggest_loguniform("prm_C", 0.01, 1000)” のような形で行ます。trial.suggest_loguniform()は対数スケールで定義されるパラメータに利用します。カーネルなどのカテゴリ変数ではtrial.suggest_categorical()を利用するなど、変数の種類に応じて適した種類を選択して使用します。

また、objective関数は目的関数の値をリターンする必要があります。ここではモデルの正答率を返すことにより、正答率を最大化するハイパパラメータを探索する動作を実現します。

# 目的関数
def objective(trial):

  # ハイパーパラメータ
  c = trial.suggest_loguniform("prm_C", 0.01, 1000)
  kernel = trial.suggest_categorical("prm_kernel", ['linear', 'rbf', 'sigmoid'])
  gamma = trial.suggest_loguniform("prm_gamma", 0.01, 1000)

  # 学習
  model = svm.SVC(C=c, kernel=kernel, gamma=gamma, random_state=0)
  model.fit(data_fit.drop(["PassengerId","Survived"], axis=1), data_fit["Survived"])
   
  # モデル評価
  score_train = model.score(data_fit.drop(["PassengerId","Survived"], axis=1), data_fit["Survived"])
  score_eval = model.score(data_valid.drop(["PassengerId","Survived"], axis=1), data_valid["Survived"])

  # バリデーションセットに対する精度を返す
  return score_eval


# データ分割
data_train, data_eval = train_test_split(data_dummy, test_size=0.3)
data_fit, data_valid = train_test_split(data_train, test_size=0.3)
 
# 最適化の実行
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100)

print("\nbest : params / value =  " , study.best_params, " / ", study.best_value)
  


2.1.4. 評価セットによるスコア計算

最終的に最適解として得られたハイパーパラメータにより構築されたモデルの性能を評価するため、探索で得られた最適解を取得してモデルの再学習を行ったのち、予め元データを分割して用意しておいた評価用セットでスコアを算出します。

# 最優秀のパラメータで再学習
prm_C = study.best_params["prm_C"]
prm_kernel = study.best_params["prm_kernel"]
prm_gamma = study.best_params["prm_gamma"]
  
#モデルの構築
model = svm.SVC(C=prm_C, kernel=prm_kernel, gamma=prm_gamma, random_state=0)
model.fit(data_train.drop(["PassengerId","Survived"], axis=1), data_train["Survived"])
  
# モデル評価
score_train = model.score(data_train.drop(["PassengerId","Survived"], axis=1), data_train["Survived"])
score_eval = model.score(data_eval.drop(["PassengerId","Survived"], axis=1), data_eval["Survived"])
print("\nscore : train/eval = " , score_train, " / ", score_eval)


2.2. 実行結果

上記のコードを実行すると、以下のような出力を得ることができます。

[I 2020-10-31 20:43:45,917] A new study created in memory with name: no-name-23aad572-15c9-4bea-b877-12ca6ba35325
[I 2020-10-31 20:43:45,939] Trial 0 finished with value: 0.6631016042780749 and parameters: {'prm_C': 19.19949019758306, 'prm_kernel': 'sigmoid', 'prm_gamma': 0.7061649403304772}. Best is trial 0 with value: 0.6631016042780749.
[I 2020-10-31 20:43:45,968] Trial 1 finished with value: 0.786096256684492 and parameters: {'prm_C': 124.34293278951348, 'prm_kernel': 'linear', 'prm_gamma': 2.0364839058875153}. Best is trial 1 with value: 0.786096256684492.
[I 2020-10-31 20:43:45,985] Trial 2 finished with value: 0.786096256684492 and parameters: {'prm_C': 0.858568380669765, 'prm_kernel': 'linear', 'prm_gamma': 0.9307356442146649}. Best is trial 1 with value: 0.786096256684492.
[I 2020-10-31 20:43:46,009] Trial 3 finished with value: 0.786096256684492 and parameters: {'prm_C': 11.494096113406432, 'prm_kernel': 'sigmoid', 'prm_gamma': 0.06419088655895576}. Best is trial 1 with value: 0.786096256684492.

〜中略〜

[I 2020-10-31 20:43:49,314] Trial 96 finished with value: 0.8128342245989305 and parameters: {'prm_C': 0.2864419265237404, 'prm_kernel': 'rbf', 'prm_gamma': 2.2592385988034644}. Best is trial 57 with value: 0.8288770053475936.
[I 2020-10-31 20:43:49,347] Trial 97 finished with value: 0.8021390374331551 and parameters: {'prm_C': 0.16035445566328554, 'prm_kernel': 'rbf', 'prm_gamma': 8.032768721322736}. Best is trial 57 with value: 0.8288770053475936.
[I 2020-10-31 20:43:49,373] Trial 98 finished with value: 0.7967914438502673 and parameters: {'prm_C': 2.883930265251486, 'prm_kernel': 'rbf', 'prm_gamma': 46.52145469944467}. Best is trial 57 with value: 0.8288770053475936.
[I 2020-10-31 20:43:49,398] Trial 99 finished with value: 0.8181818181818182 and parameters: {'prm_C': 1.811608237076336, 'prm_kernel': 'rbf', 'prm_gamma': 7.165678247835968}. Best is trial 57 with value: 0.8288770053475936.

best : params / value =   {'prm_C': 0.702312966921039, 'prm_kernel': 'rbf', 'prm_gamma': 12.887678437484317}  /  0.8288770053475936

score : train/eval =  0.8041733547351525  /  0.8283582089552238


チューニングの結果得られたハイパーパラメータは以下のようになりました。

  • C : 0.702312966921039

  • kernel : 'rbf'

  • gamma : 12.887678437484317

精度は評価セットに対するスコアが0.828となりました。何度か試してみたところ、試行によって結構バラつきがあるみたいです。イテレーションの500回くらいまで増やしたりしてみましたが、ざっくりやってみた感覚的にはあんまり変わらんように感じました。

2.3. で、結局チューニングで精度は上がったん?

ってのが気になるところ。ということで、次回の記事ではデフォルトパラメータ及びscikit-learnのランダムサーチによるチューニングとoptunaとを戦わせてみて、最終的に得られる精度を比較したいと思います。先述の通り、肌感的にはチューニング後の精度が2,3%程度ばらつくので、何度かやって平均をとるなどする予定です。

3. おわりに

以上、optunaによるモデルチューニングでした。コーディングがイケてない感じがあるのは技術の限界です。ご容赦ください。

次回は精度比較を行っていきます。