雑記 in hibernation

頭の整理と備忘録

optunaで脳筋ハイパラチューニング(小手調べ:関数の最適解探索編)

「話題の最適化フレームワークがあるらしいやんけ。使ったろ。」の精神でoptunaを使った機械学習モデルのハイパーパラメータのチューニングをお試ししてみます。Pythonでのoptuna実装のチュートリアル的な内容です。理論面にはノータッチの脳筋スタイルで行きます。

記事は3回に分けて投稿しようと思っています。第1回の今回の記事では、まず1変数の関数に対する最適解探索を実装してみます。次回、第2回では機械学習モデルのパラメータ最適化を実装し、第3回でデフォルトパラメータやscikit-learnのパラメータチューニングとの精度比較をしようと思います(一回の記事にまとめようとしたら案外長くなりそうだったので、「関数の最適解探索編」「機械学習モデルのハイパラチューニング編」「精度比較編」で分割しました)。

例によって、備忘録です。

1. optunaとは

みんな大好きPreferred Networksが開発したオープンソースのパラメータ自動最適化フレームワークです。めっちゃざっくり言うと、パラメータ探索が楽々できちゃうパッケージです。

preferred.jp

機械学習モデルをチューニングする際、グリッドサーチや乱数ベースの手法を用いて最も良い性能の得られるハイパーパラメータを探索することがあります。探索を精緻にやろうとすればするほど、往々にして計算機資源や実行時間が問題になるわけですが、optunaを使うことにより試行回数に対して効率的なパラメータ探索ができるよ、というわけです。

optunaの特徴としては「Define-by-Run スタイルの API 」「Tree-structured Parzen Estimator(ベイズ最適化の一種)による効率的な探索」「非同期分散最適化をサポート」などがあるそうですが、ぶっちゃけ僕はよくわかってないです。「実装が楽で」「少ない試行回数でそこそこ結果が出て」「並列化もしやすい」程度に理解しています。


2. optunaによる関数の最適解探索

機械学習モデルの最適化を行う前に、まずは小手調べに関数の最適解探索をやってみます。

本日用意する関数はこちら。局所解がいっぱいあっていやらしいですね。果たしてoptunaは局所最適解に引っかからずに大域最適解に辿り着くことができるのでしょうか。

f:id:toeming:20201101122356p:plain


2.1. 実装

Pythonでの実装手順についても書いておきます。

なお、実装に当たっては以下の記事を参考にさせていただいてます。

qiita.com


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

"optuna"をインポートするだけでOK。めっちゃ楽。 (jupyter notebook上でインストールしてるので、マジックコマンドで書いてます)

numpyとmatplotlibはoptunaとは無関係ですが、最適化の結果を描画する処理で使うのでインポートしておきます。

# optunaのインストールとインポート
!pip install optuna
import optuna

# その他のパッケージ
import numpy as np
import matplotlib.pyplot as plt


2.1.2. 目的関数の定義

目的関数として、先に掲載した、いやらしい目的関数を以下のように実装して定義します。

def wave(x):
    return x ** 2 + np.sin(x/5)*2000 + 1939.7


2.1.3. 最適化

まず、optuna.studyインスタンスを"study"として作成しています。この時、スコアの最大化を行うのか、最小化を行うのかをパラメータで指定できます。今回は最小化したいので direction="minimize" です。

次に、optimizeメソッドで最適化を実行します。引数には、パラメータを受け取って目的関数のスコアを返す関数(ここでは def objective(trial)で定義)と、イテレーションの回数を指定します。今回は100回試行します。

最適化を行ったのち、optuna.studyインスタンスのメンバ変数から最適化の結果を参照することができます。

def objective(trial)で定義されている目的関数のスコアを返す関数では、引数"trial"から、最適化する変数をoptuna.trial.Trialで受け取ることができます。関数”objective”ではこの変数からスコアを計算し、返り値として出力します。

ここでの肝は”x = trial.suggest_uniform('x', -60, 60)”の部分で、ここでパラメータを変数として受け取るとともに、変数の種類(数値・カテゴリなど)を指定しています。今回はパラメータを"x"という名前で-60~60の数値変数として指定しています。最適化対象のパラメータが増えるに応じて、この記述も追加していくことになります(具体例として次回記事の機械学習モデルの最適化コードをみるとイメージしやすいと思います。)

# 最小化(最大化)したいスコアを返す関数
# ハイパーパラメータもここで定義
def objective(trial):
    x = trial.suggest_uniform('x', -60, 60) 
    return wave(x) 

# 最適化
study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=100)
print("best : params / value =  " , study.best_params, " / ", study.best_value)


一応、可視化もできるようにしておきます。

# 目的関数の描画
x = np.arange(-60,60,1)
y = wave(x)
plt.plot(x, y, color="skyblue")

# 探索過程の描画
trial_points_x = [each.params['x'] for each in study.trials]
trial_points_y = list(map(wave, trial_points_x))
#plt.scatter(trial_points_x, trial_points_y ,  color="blue", markersize=1, marker='.') # 線が不要の場合
plt.plot(trial_points_x, trial_points_y ,  color="gray", marker='.',markersize=1, linestyle='dashed',linewidth = 0.5)

# 最適解の描画
plt.plot(study.best_params["x"], study.best_value, color="red", markersize=10,marker='.')

plt.show()


2.2. 実行結果

上記のプログラムを実行すると、こんな感じの結果になります。出力が長いので一部中略しました。

結果として、最適解-7.577532942512523を得ることができました。

[I 2020-10-31 22:08:24,144] A new study created in memory with name: no-name-d4fdd6f3-ac08-4a64-9e89-af36acaa041f
[I 2020-10-31 22:08:24,148] Trial 0 finished with value: 3633.06124635331 and parameters: {'x': -29.255676447763705}. Best is trial 0 with value: 3633.06124635331.
[I 2020-10-31 22:08:24,151] Trial 1 finished with value: 3760.800360431499 and parameters: {'x': 33.246022559222794}. Best is trial 0 with value: 3633.06124635331.
[I 2020-10-31 22:08:24,153] Trial 2 finished with value: 3813.0478950777097 and parameters: {'x': -28.643902358081892}. Best is trial 0 with value: 3633.06124635331.
[I 2020-10-31 22:08:24,155] Trial 3 finished with value: 5290.722676896419 and parameters: {'x': 37.84205199211999}. Best is trial 0 with value: 3633.06124635331.
[I 2020-10-31 22:08:24,157] Trial 4 finished with value: 2920.4502572377323 and parameters: {'x': 52.707434052697465}. Best is trial 4 with value: 2920.4502572377323.

〜中略〜

[I 2020-10-31 22:08:24,591] Trial 97 finished with value: 686.6656131934335 and parameters: {'x': -11.853537873477123}. Best is trial 58 with value: 0.17518198674815721.
[I 2020-10-31 22:08:24,597] Trial 98 finished with value: 4509.100653321098 and parameters: {'x': -24.14638800267916}. Best is trial 58 with value: 0.17518198674815721.
[I 2020-10-31 22:08:24,608] Trial 99 finished with value: 3755.242259482945 and parameters: {'x': 5.514993534816217}. Best is trial 58 with value: 0.17518198674815721.
best : params / value =   {'x': -7.577532942512523}  /  0.17518198674815721


最適化の様子を出力したのが以下のグラフです。探索して得られた最適解を赤い点で示しています。局所最適解にハマらずに最適解にたどり着けていることがわかります。灰色の点線はイテレーションごとに計算を実行するポイントがどのように動いていったかを示しています。最適解付近での探索が密である一方、時々遠く離れたパラメータに飛ぶように移動するような動きが発生していることがわかります。このように「たまに余所見する」ことで局所最適解に陥ることを避けていると思われます(この辺りの余所見の塩梅がTree-structured Parzen Estimatorの効果、ということなのでしょうか)。

f:id:toeming:20201101072233p:plain


3. おわりに

optunaのチュートリアルとして、まず関数の最適化を実装して挙動を確認してみました。とりあえず実装の勝手がわかったので、次回の記事ではoptunaを用いた機械学習モデルのパラメータチューニングを実装してみます。

あと「この辺りの余所見の塩梅がTree-structured Parzen Estimatorの効果、ということなのでしょうか」などと漠然としたとこを吐かして理屈から逃げ回ってるのもちょっと恥ずいので、理論方面の話もちょっとくらいわかっておきたいなと思いました。小並感。