過去数回の記事で使用した汎用行動選択モデルは強化学習で学習したものでしたが、教師あり学習のものより若干弱いものになっていました。 複雑なステップを経る教師あり学習を不要とし、強化学習だけで同等以上のモデルを学習できるようにするため、強化学習のハイパーパラメータのチューニングを行います。
強化学習のハイパーパラメータ
以前学習した際のハイパーパラメータは以下のようになっていました。
- 探索: epsilon-greedy
- ランダム行動する確率epsilon: 0.3 (定数)
- 報酬割引率: 0.95
- バッチサイズ: 32
- 最初に学習するまでのステップ数(replay bufferのサンプル数): 500
- Nサンプル収集するたびにoptimize: 1
- N optimizeごとにtarget networkのアップデート: 100
- replay bufferサイズ: 100,000
- optimizer (Adam)の学習率: 0.001
- バトル数: 10,000
予備実験で、バトル数10,000では不十分で、もっと多くのデータで学習すべきということが示唆されたため、バトル数は100,000に上げることにしました。 epsilon ()は最初は大きく、学習が進むにつれて減らしたほうが良いという話もあります。バトル数を増やすにあたり、ステップ数に応じてepsilonを減少させることも試みます。具体的には、epsilon decayパラメータを追加し、ステップ数に対してをとして用いることにしました。例えばに対しとなり、ちょうどよい範囲の減少になります。 ここに挙げたハイパーパラメータの数をすべて調整するには候補が多すぎるので、調整するハイパーパラメータの種類を絞ることとしました。バッチサイズなどは学習率の調整と近い効果を得られると考えられるので省略し、epsilon・epsilon decay・報酬割引率・学習率の4つを調整することとしました。
調整結果の評価は、教師あり学習で得たモデルとの比較により定量化します。教師あり学習と強化学習モデルに同一のパーティ群を操作させ、レーティングバトルでパーティ群の平均レートの差を計算します。 select766.hatenablog.com の評価方法と同じです。
もっとも単純なパラメータの調整方法はグリッドサーチで、例えばの候補が、の候補がであれば、これらすべての組み合わせとしての4通りを試し、もっとも良かった結果をハイパーパラメータチューニングの結果として得ます。ただ、各変数を細かく調整しようとすると、「一変数当たりの候補数」の「変数の数」乗の組み合わせすべてをしらみつぶしに調べるため、非常にコストが大きくなります。今回だと1つのハイパーパラメータに対して強化学習を行うのに5時間程度かかるため、何百もの組み合わせを試すことができません。そこで、試す候補を減らしつつ、有望な結果を得るための手法を用いる必要があります。直観的な手順としては、まずランダムなハイパーパラメータh1, h2で学習を行い、評価p1、p2(大きいほうが良いとする)を得ます。ならh1のほうが有望であると考え、次に試すハイパーパラメータh3はh1に近い値を選び、さらに評価が良くなるかを検証します。このように、過去の試行の結果を用いて有望なハイパーパラメータを選択するという手順を数学的に定式化した手法が提案されています。なお、このような手法では単に評価値最大が期待されるハイパーパラメータだけでなく、過去の試行が偶然悪かった可能性も考慮しています。
Optunaを用いたハイパーパラメータチューニング
効率的にハイパーパラメータをチューニングしてくれるライブラリとして、今回はPythonから扱いやすいOptunaを用いました。 Optunaには前述のようにハイパーパラメータをチューニングするアルゴリズムが複数実装されており、簡単なインターフェースで利用することが可能です。今回使用するアルゴリズムはデフォルトのTree-structured Parzen Estimatorです。
実際に動作するソースコードのOptuna利用部分をかいつまんで具体的な利用方法を説明します。Optunaのバージョンは2.0.0です。
まず、main関数で試行結果を保存するデータベースを開きます。Optunaは複数台のコンピュータを用いた並列探索に対応している点が特徴の1つで、データベースに試行結果(ハイパーパラメータとその評価結果)を保存します。今回はコンピュータ1台しか使いませんが、マルチコアを活かすため同時に複数の試行を走らせます。
import optuna study = optuna.load_study(study_name="study1", storage="sqlite://study1.db")
storage
引数にはデータベースのアドレスを指定しますが、コンピュータ1台だけであればsqliteを用いるとサーバソフトのインストールが不要で便利です。study_name
はデータベース内で独立したハイパーパラメータチューニングのセッションを区別する名前です。データベースを作成するにはoptuna
コマンドを用います。例えば
optuna create-study --study-name study1 --storage sqlite://study1.db
のように実行します。なお、optuna.create_study
でpythonコード内でデータベースを作成することもできます。
以下のコードで探索を開始します。
study.optimize(objective, n_trials=10)
objective
は最適化対象の関数で、後述します。n_trials
は試行回数です。今回は使用していませんが、n_jobs
を指定すると、その数のプロセスが立ち上がって並列探索されます。この機能を使わずに複数のプロセスで同時に同じコードを実行した場合でも、データベースを介して連携することにより並列探索が可能となります。複数台のコンピュータを連携させる場合にも共通のデータベースにさえ接続できればOSやネットワークの接続形態を問わないので非常にシンプルです。
objective
関数に、ハイパーパラメータを受け取って評価する機能を実装します。ちょっと長いのでコア部分だけ示します。
def objective(trial): trainer_id = ObjectId() # ハイパーパラメータを取得し、学習に必要な設定ファイルを作成 trainer_param = make_train_param(epsilon=trial.suggest_uniform("epsilon", 0.1, 0.5), epsilon_decay=trial.suggest_loguniform("epsilon_decay", 1e-7, 1e-5), gamma=trial.suggest_uniform("gamma", 0.8, 1.0), lr=trial.suggest_loguniform("lr", 1e-4, 1e-1)) # train_paramの値を用いて学習・評価する独自のコード subprocess.check_call( ["python", "-m", "pokeai.ai.generic_move_model.rl_train", trainer_param_file_path, "--trainer_id", ",".join(map(str, trainer_ids))]) rate_id = ObjectId() subprocess.check_call( ["python", "-m", "pokeai.ai.generic_move_model.rl_rating_battle", f"{evaluate_base_trainer_id},{trainer_id}", evaluate_party_tag, "--rate_id", str(rate_id), "--loglevel", "WARNING"]) rate_advantage = get_rate_advantage(rate_id, evaluate_base_trainer_id, trainer_id) trial.set_user_attr("trainer_id", str(trainer_id)) # JSON serializableである必要あり return -rate_advantage # rate_advantageを最大化=Optunaでは最小化
objective
関数はtrialという引数を受け取ります。この引数がOptunaが持っている試行の状態を表します。今回のobjective
の呼出しで試行すべきハイパーパラメータは、trial.suggest_uniform(name, low, high)
で得ます。
suggest_uniform
の引数で、パラメータの名称、最小値、最大値を指定します。これは一様分布ですが、logスケールに対して一様分布を生成するにはsuggest_loguniform
を用います。Optunaでは、探索前にハイパーパラメータの数や範囲を指定するのではなく、実行中に指定するという形態をとっています。この機能を使うと、あるハイパーパラメータに依存して別のハイパーパラメータを生成させるようなことが可能です。今回は使用しませんが、公式マニュアルから例を引用します。
def objective(trial): classifier_name = trial.suggest_categorical('classifier', ['SVC', 'RandomForest']) if classifier_name == 'SVC': svc_c = trial.suggest_loguniform('svc_c', 1e-10, 1e10) classifier_obj = sklearn.svm.SVC(C=svc_c) else: rf_max_depth = int(trial.suggest_loguniform('rf_max_depth', 2, 32)) classifier_obj = sklearn.ensemble.RandomForestClassifier(max_depth=rf_max_depth)
以下は、得られたハイパーパラメータを用いて強化学習と評価を行っています。objective
は通常のpythonコードですので、タスクごとに都合のいい手段で実装することができます。今回は、学習・評価コードを外部プロセス呼び出しで実行し、その結果を読み取ってrate_advantage
(実数値)に代入しています。Optunaはobjective
の戻り値を最小化するように動作するため、最大化したいrate_advantage
の符号を反転して返します。選ばれたハイパーパラメータと戻り値はデータベースに保存され、以後の探索に利用されます。今回は単に最適なハイパーパラメータを知るだけでなく、学習したモデルを後で使いたいので、モデルの保存のためにランダム生成したIDをtrial.set_user_attr("trainer_id", str(trainer_id))
でtrialに紐づけています。この値もデータベースに保存されるので、探索後に使うことができます。
なお、今回強化学習システムは独自に実装したものですので学習の実行や評価結果の計算は独自に行っていますが、Tensorflowなどの著名な機械学習ライブラリの典型的な利用であれば、optuna.integration
以下にライブラリ間の橋渡しを行ってくれる機能があり、より短いコードで実装が可能です。
実験結果
Optunaを用いた強化学習のハイパーパラメータチューニングの結果を示します。
チューニング対象の変数と範囲は以下の通りです。
- epsilon: 0.1~0.5
- epsilon decay: ~
- 報酬割引率: 0.8~1.0
- 学習率: ~
25回の試行を行った結果、各変数と評価(小さいほうが良い)の関係を示します。
これらのグラフから、学習率が評価に大きな影響を与えていることが見て取れます。一方、他の変数については明確な傾向が見られません。 複数の変数を同時に変化させながら試行しているため、横軸の値がほぼ同じでも別の変数の値が大きく異なり、縦軸の値には大きな変化が生じている場合があります。 Optunaのアルゴリズムにより有望なパラメータの範囲内で多くの探索がなされるため、学習率が付近の試行回数が多くなっています。 評価が最善だったのは、epsilon=0.28, epsilon decay=, 報酬割引率=0.96, 学習率=のときでした。そして評価結果は-35で、これは同じモデル構造で教師あり学習よりも強いモデルが強化学習によって学習できたことを示します。これで、強化学習単体で汎用行動選択モデルを用いた行動決定が可能になったと考えられます。次回は、強化学習結果のモデルを使ってパーティ生成を行い、さらにそのパーティ上で強化学習を行うサイクルを実装します。