機械学習周りのプログラミング中心。 イベント情報
ポケモンバトルAI本電子書籍通販中

汎用行動選択モデルの学習 part11 Optunaを用いた強化学習のハイパーパラメータチューニング【PokéAI】

過去数回の記事で使用した汎用行動選択モデルは強化学習で学習したものでしたが、教師あり学習のものより若干弱いものになっていました。 複雑なステップを経る教師あり学習を不要とし、強化学習だけで同等以上のモデルを学習できるようにするため、強化学習のハイパーパラメータのチューニングを行います。

強化学習のハイパーパラメータ

以前学習した際のハイパーパラメータは以下のようになっていました。

  • 探索: 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を減少させることも試みます。具体的には、epsilon decayパラメータdを追加し、ステップ数sに対して \epsilon (1-d)^{s} \epsilonとして用いることにしました。例えば d=10^{-5}, s={100000}に対し(1-d)^{s}=0.368となり、ちょうどよい範囲の減少になります。 ここに挙げたハイパーパラメータの数をすべて調整するには候補が多すぎるので、調整するハイパーパラメータの種類を絞ることとしました。バッチサイズなどは学習率の調整と近い効果を得られると考えられるので省略し、epsilon・epsilon decay・報酬割引率・学習率の4つを調整することとしました。

調整結果の評価は、教師あり学習で得たモデルとの比較により定量化します。教師あり学習強化学習モデルに同一のパーティ群を操作させ、レーティングバトルでパーティ群の平均レートの差を計算します。 select766.hatenablog.com の評価方法と同じです。

もっとも単純なパラメータの調整方法はグリッドサーチで、例えば\epsilonの候補が\{0.1,0.5\}dの候補が\{10^{-4}, 10^{-6}\}であれば、これらすべての組み合わせとして(\epsilon, d) = \{(0.1, 10^{-4}), (0.1, 10^{-6}), (0.5, 10^{-4}), (0.5, 10^{-6})\}の4通りを試し、もっとも良かった結果をハイパーパラメータチューニングの結果として得ます。ただ、各変数を細かく調整しようとすると、「一変数当たりの候補数」の「変数の数」乗の組み合わせすべてをしらみつぶしに調べるため、非常にコストが大きくなります。今回だと1つのハイパーパラメータに対して強化学習を行うのに5時間程度かかるため、何百もの組み合わせを試すことができません。そこで、試す候補を減らしつつ、有望な結果を得るための手法を用いる必要があります。直観的な手順としては、まずランダムなハイパーパラメータh1, h2で学習を行い、評価p1、p2(大きいほうが良いとする)を得ます。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_studypythonコード内でデータベースを作成することもできます。

以下のコードで探索を開始します。

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: 10^{-7}10^{-5}
  • 報酬割引率: 0.8~1.0
  • 学習率: 10^{-4}10^{-1}

25回の試行を行った結果、各変数と評価(小さいほうが良い)の関係を示します。

f:id:select766:20200830173319p:plain
epsilonと評価の関係
f:id:select766:20200830173426p:plain
epsilon decayと評価の関係
f:id:select766:20200830173447p:plain
報酬割引率(gamma)と評価の関係
f:id:select766:20200830173510p:plain
学習率(lr)と評価の関係

これらのグラフから、学習率が評価に大きな影響を与えていることが見て取れます。一方、他の変数については明確な傾向が見られません。 複数の変数を同時に変化させながら試行しているため、横軸の値がほぼ同じでも別の変数の値が大きく異なり、縦軸の値には大きな変化が生じている場合があります。 Optunaのアルゴリズムにより有望なパラメータの範囲内で多くの探索がなされるため、学習率が4 \times 10^{-4}付近の試行回数が多くなっています。 評価が最善だったのは、epsilon=0.28, epsilon decay= 2.5 \times 10^{-6}, 報酬割引率=0.96, 学習率= 4.1 \times 10^{-4}のときでした。そして評価結果は-35で、これは同じモデル構造で教師あり学習よりも強いモデルが強化学習によって学習できたことを示します。これで、強化学習単体で汎用行動選択モデルを用いた行動決定が可能になったと考えられます。次回は、強化学習結果のモデルを使ってパーティ生成を行い、さらにそのパーティ上で強化学習を行うサイクルを実装します。

汎用行動選択モデルの学習 part10 Q関数を用いたパーティ生成【PokéAI】

前回強化学習によって得たQ関数を用いてパーティの強さを定量化する指標(パーティ評価関数)を定義しました。 今回はこれを用いて強力なパーティを生成する手法を説明します。

ここでの目的は、パーティ評価関数R(X)の値が比較的大きいパーティXを多数求めることです。上位の目的は強化学習中に対戦させるパーティを生成することであり、強化学習エージェントは様々なパーティを受け持ち、様々な相手への立ち回り方を学習する必要があります。そのためR(X)が厳密な最大値をとる唯一のパーティを生成するのではなく、R(X)をできるだけ大きくしつつ、パーティごとにポケモン・技が異なるような多様性のあるパーティ群X_G = \{{X}^1, {X}^2, {X}^3,...\}を生成します。

ペナルティ項つき山登り法による多様なパーティ生成

手法は以前提案した山登り法ベースですが、それだけだと同一のパーティが大量にできてしまうため、今回新たにペナルティ項を加えることとしました。 select766.hatenablog.com

山登り法を用いたパーティ生成手法について、第3巻から必要な部分を変更して再掲します。

パーティ評価関数R(X)の値が最大となるようなパーティXを生成するために、単純な離散最適化手法である山登り法を用います。山登り法の概要を下図に示します。最初にランダムなパーティX_0を生成し、これを少しだけ変更した近傍パーティ候補N(X_0) = \{{C_1}^1, {C_1}^2, ...\}を生成します。そして、これをRで評価し、最も強いパーティを選択します。この処理を1世代とします。選択されたパーティをX_1とし、次の世代では再度これを少しだけ変更したパーティ候補N(X_1)=\{{C_2}^1, {C_2}^2, ...\}を生成します。そして、これをRで評価し、最も強いパーティを選択します。これを繰り返すことで徐々に強いパーティへと変化させていきます。ここで、パーティを少しだけ変更する手段は2通り用意しています。(1)ポケモンを1匹完全に別のものにし、技もランダムに設定する、(2)1匹のポケモンを選び、その技を1つ選び、別のものにする。(1)が選ばれる確率はそれぞれ10%、それ以外では(2)が選ばれ、パーティが変更されます。

f:id:select766:20200821082434p:plain
山登り法の概要

山登り法は以前から使用しています。第1巻では、パーティ構成1匹の条件で用いており、パーティ変更の手段(1)を使いませんでした。ポケモンによって覚える技が違うため、ポケモンを別のものに変更すると技はランダムに設定しなおすこととなり、1匹しかいないポケモンを変更すると近傍ではなくなってしまうという考え方で変更を避けました。しかしその場合、初期値としてランダムに選んだポケモンが弱いと改善の余地がないという状態になります。現在の条件で強いとされているフォレトスサイドンと、極端に弱いレディアンヤンヤンマが同じ頻度で現れるというのは強いパーティの生成として不十分と考えられます。第3巻では、パーティ構成3匹の条件で用いて、ポケモン変更ありで強いパーティを得ました。今回はパーティ構成1匹で、単純に同様の手法をとると同一のパーティが大量にできてしまうという結果になりました。山登り法は局所最適解が出る手法なので、初期値によって違う結果がある程度得られますが、探索範囲が狭いと同じ解に収束する可能性が高くなります。

この問題に対して、すでに生成されたパーティと類似のパーティが生成されにくくするための明示的なペナルティ項S(X)を設けることにしました。パーティ群に含まれるパーティは、{X}^1, {X}^2, ...の順に1つずつ山登り法で生成します。{X}^iを生成する際は、それまでに生成したパーティと類似するパーティのパーティ評価関数の値を小さくし、今までに生成されていないパーティの生成を促進します。ペナルティ項は

S(X_i)=\frac{1}{i-1}\sum_{j=1}^{i-1} K(X_j, X_i)

と定義します。ここで、K(X_j, X_i)はパーティ間の類似度を表す関数です。過去に提案したパーティを表す特徴量(P,M,PM,MM)の内積として定義しました。パーティ間でポケモン1匹、技1つ、ポケモンと技の組み合わせ、技と技の組み合わせがいくつ一致するかで算出されます。パーティ間で一切一致がない場合は0、ポケモンも技もすべて一致する場合は1+4+4+6(4つの技から2つを選ぶ組み合わせ)=15になります。 select766.hatenablog.com

このペナルティ項を用いて、山登り法でパーティを評価する際の計算式を R(X_i) - \lambda S(X_i)とします。ペナルティの強さは負でないスカラー定数\lambdaで制御します。

実験結果

ペナルティの強さを変えつつパーティを生成して比較します。

山登り法のパラメータは、近傍パーティ生成数10、世代数100とし、パーティ群の大きさは871としました。871は、1000-129=871で算出しています。129は今回対象にする全ポケモンの種族数で、山登り法で生成したパーティ群に、各種族のポケモンが含まれるパーティを1つずつ足して合計で1000パーティとし、今後の強化学習の対象とするためです。この追加の129パーティは本稿の実験結果には含まれません。

\lambda=0で生成した871パーティからランダムに10パーティ抽出した結果を示します。

ヤドラン,LV55,じしん,すてみタックル,だいもんじ,ふみつけ
ガルーラ,LV55,かえんほうしゃ,ちきゅうなげ,かみなり,だいもんじ
ヤドラン,LV55,じしん,だいもんじ,のしかかり,でんじほう
サイドン,LV55,だいもんじ,じしん,でんじほう,10まんボルト
フォレトス,LV55,ころがる,すてみタックル,とっしん,ギガドレイン
ガルーラ,LV55,だいもんじ,かえんほうしゃ,かみなり,ちきゅうなげ
サイドン,LV55,じしん,10まんボルト,だいもんじ,でんじほう
サイドン,LV55,だいもんじ,10まんボルト,じしん,でんじほう
ガルーラ,LV55,かみなり,かえんほうしゃ,だいもんじ,ちきゅうなげ
フォレトス,LV55,とっしん,ころがる,ギガドレイン,すてみタックル

\lambda=0.1の場合を下に示します。

スイクン,LV55,れいとうビーム,なみのり,いわくだき,たきのぼり
ガルーラ,LV55,れいとうパンチ,だいもんじ,いわくだき,かみなり
プテラ,LV55,だいもんじ,はがねのつばさ,げんしのちから,じしん
エアームド,LV55,そらをとぶ,ゴッドバード,すなあらし,どろかけ
フォレトス,LV55,とっしん,ころがる,ギガドレイン,ソーラービーム
フォレトス,LV55,かいりき,ころがる,おんがえし,ギガドレイン
サイドン,LV55,ふぶき,じしん,でんじほう,ほのおのパンチ
ケンタロス,LV55,かみなり,ふみつけ,つのドリル,すてみタックル
フォレトス,LV55,ソーラービーム,かいりき,すてみタックル,ギガドレイン
ガルーラ,LV55,なみのり,かえんほうしゃ,かみなり,ちきゅうなげ

\lambda=1の場合を下に示します。

ドードリオ,LV55,のしかかり,どくどく,ゴッドバード,はがねのつばさ
ミルタンク,LV55,かみなりパンチ,ふみつけ,ちきゅうなげ,ずつき
カイリキー,LV55,かいりき,ほのおのパンチ,すてみタックル,ちきゅうなげ
ツボツボ,LV55,じしん,ヘドロばくだん,いわくだき,ころがる
カメックス,LV55,ころがる,たきのぼり,ハイドロポンプ,れいとうパンチ
フーディン,LV55,ほのおのパンチ,れいとうパンチ,サイケこうせん,かみなりパンチ
エアームド,LV55,はがねのつばさ,そらをとぶ,スピードスター,すなあらし
ラプラス,LV55,サイコキネシス,はかいこうせん,つのドリル,いわくだき
サイドン,LV55,つのドリル,ころがる,どろかけ,かみなり
エアームド,LV55,おんがえし,はがねのつばさ,ゴッドバード,どくどく

\lambda=10の場合を下に示します。

マリルリ,LV55,ふぶき,ハイドロポンプ,ばくれつパンチ,ずつき
プテラ,LV55,げんしのちから,ゴッドバード,はがねのつばさ,とっしん
ライコウ,LV55,すなあらし,いあいぎり,かみなり,スピードスター
ハガネール,LV55,いあいぎり,じしん,はかいこうせん,ロケットずつき
スイクン,LV55,バブルこうせん,いわくだき,スピードスター,なみのり
スイクン,LV55,たきのぼり,いあいぎり,なみのり,かげぶんしん
ピクシー,LV55,はなびらのまい,すてみタックル,ふぶき,ほのおのパンチ
ゴルダック,LV55,ハイドロポンプ,スピードスター,はなびらのまい,れいとうパンチ
デンリュウ,LV55,かみなりパンチ,とっしん,かみなり,いわくだき
ガルーラ,LV55,すなあらし,かえんほうしゃ,ほのおのパンチ,はかいこうせん

各々のパーティの良さは直観的にはどの条件でもあまり変わらない一方、\lambda=0ではポケモンの種類が偏っています。実際、「フォレトス,LV55,ころがる,すてみタックル,とっしん,ギガドレイン」のパーティは219回も生成されてしまいました。別の側面では、ランダムな初期値から開始して同じ解に収束しているので、山登り法の世代数などのパラメータは十分であると考えられます。\lambdaが大きくなるにつれてパーティの多様性が増していますが、直観的に異常な(極端に弱そうな)内容にはなっていません。

定量的に、全パーティでのポケモンや技の出現回数を算出した結果を示します。各要素出現回数上位20件を表示しています。

\lambda=0

要素 出現回数
フォレトス 223
ガルーラ 162
ケンタロス 91
サイドン 86
ヤドラン 85
ミルタンク 68
エアームド 50
スイクン 38
ラプラス 29
プテラ 29
マンタイン 2
カイリキー 2
ハッサム 2
レアコイル 2
ツボツボ 1
カビゴン 1
要素 出現回数
だいもんじ 360
かみなり 287
じしん 271
すてみタックル 260
かえんほうしゃ 252
ギガドレイン 223
とっしん 222
ころがる 221
ちきゅうなげ 220
のしかかり 148
れいとうビーム 140
でんじほう 139
ふみつけ 119
10まんボルト 85
すなあらし 82
かみなりパンチ 71
おんがえし 61
そらをとぶ 50
ドリルくちばし 50
はかいこうせん 41

\lambda=0.1

要素 出現回数
フォレトス 159
ガルーラ 150
ヤドラン 99
サイドン 91
ミルタンク 81
ケンタロス 73
エアームド 67
スイクン 53
ラプラス 51
プテラ 31
カビゴン 6
ハッサム 4
レアコイル 3
ランターン 1
カイリキー 1
ドードリオ 1
要素 出現回数
じしん 223
かみなり 179
ギガドレイン 144
れいとうビーム 138
だいもんじ 126
おんがえし 112
すなあらし 106
かえんほうしゃ 103
すてみタックル 98
ちきゅうなげ 96
なみのり 89
のしかかり 88
10まんボルト 87
とっしん 83
かみなりパンチ 83
はかいこうせん 82
かいりき 80
でんじほう 79
れいとうパンチ 79
ずつき 79

\lambda=1

要素 出現回数
ガルーラ 72
フォレトス 63
サイドン 62
ケンタロス 62
ミルタンク 58
ヤドラン 51
エアームド 48
スイクン 43
ラプラス 42
プテラ 38
ハッサム 29
ドードリオ 26
カビゴン 23
マンタイン 18
カイリキー 18
フーディン 17
レアコイル 17
メガニウム 12
ツボツボ 11
ゴルダック 10
要素 出現回数
じしん 114
かみなり 97
だいもんじ 93
おんがえし 89
れいとうビーム 89
のしかかり 83
サイコキネシス 83
すなあらし 81
どくどく 81
すてみタックル 79
はかいこうせん 79
かげぶんしん 77
ずつき 77
でんじほう 76
かえんほうしゃ 75
とっしん 75
かいりき 75
なみのり 75
いわくだき 75
ハイドロポンプ 74

\lambda=10

要素 出現回数
サイドン 21
ガルーラ 19
ケンタロス 19
ミルタンク 17
プテラ 17
フォレトス 16
ヤドラン 16
エアームド 16
ラプラス 16
メガニウム 16
ドードリオ 15
ゲンガー 15
フシギバナ 14
カビゴン 14
カイリュー 14
ハッサム 14
スイクン 12
ヤミカラス 12
マンタイン 12
ネイティオ 12
要素 出現回数
じしん 79
おんがえし 76
れいとうビーム 75
どくどく 75
だいもんじ 74
かげぶんしん 74
かみなり 73
のしかかり 73
サイコキネシス 73
スピードスター 73
すてみタックル 72
とっしん 72
はかいこうせん 72
ずつき 72
どろかけ 72
いわくだき 72
すなあらし 71
ロケットずつき 71
ハイドロポンプ 71
ギガドレイン 70

ペナルティ項が大きくなるほど出現回数の偏りが小さくなっていることが確認できます。 \lambda=0では最大出現回数のポケモンフォレトスでしたが\lambda=1ではガルーラに変わっています。これは、フォレトスよりガルーラのほうが覚える技の種類が多いため、フォレトスを含むパーティ同士よりガルーラを含むパーティ同士のほうが類似度を抑えつつ多く生成できたものと考えられます。\lambda=10まで大きくすると、分布が平準化されすぎて強さの評価が失われていると考えられます。 分布の広がりの必要性は、生成されたパーティ群を用いた行動の強化学習フェーズの要請から来たものであり単体でその有効性を定量評価することは困難です。\lambda=1の場合に生成されたパーティが定性的に悪くないので、今後はこのパラメータでパーティ生成をすることとします。

今回までで、バトル中の行動選択のために学習した強化学習エージェントのQ関数を用いることで、強いパーティを生成する手法を提案しました。次回は強化学習のハイパーパラメータ調整について書く予定です。

汎用行動選択モデルの学習 part09 Q関数を用いたパーティ評価【PokéAI】

前回、バトル中の行動選択を行うQ関数について、バトル開始直後のQ値は0ではなく、自分のパーティ構成および相手に応じて変動することがわかりました。 今回から、この性質を用いて強いパーティを生成することを試みます。

Q関数を用いたパーティの強さの定式化

強化学習を通じて学習されるQ関数Q(s, a)のsにはバトルの状態、aには選択する行動が代入されます。現在は交代なしの1vs1バトルを扱っており、バトル開始直後のsには相手のポケモンのタイプ(17次元のうち該当する次元が1)が含まれます。残りHPなどの要素もありますが、定数になります。また、aには自分のポケモンの種族(251次元のうち該当する次元が1)および技(251次元のうち該当する次元が1)が含まれます。

前回掲載したように、Q値(Q関数の出力)は次のようになります。1つの状況に対して選択肢となる技ごとに異なるaが与えられるため、異なるQ値が得られます。

相手 パラセクト HP182/182  
自分 ギャラドス HP220/220  
技       Q値
いわくだき   0.31
ハイドロポンプ 0.24
なみのり    0.36
のしかかり   0.68

相手ポケモンが変化すればsが変化し、Q値が変化します。

相手 ニドクイン HP215/215  
自分 ギャラドス HP220/220  
技       Q値
いわくだき   -0.15
ハイドロポンプ 0.18
なみのり    0.37
のしかかり   0.31

良いパーティは、どんな相手に対しても高いQ値が得られるパーティであると考えられます。 これを定式化すると、パーティXに対してパーティの良さを表す関数R(X)は次のようになります。


R(X) = \frac{1}{|E|} \sum_E \max_a Q(s(E), a)

ここで、Eは相手として想定するポケモンの集合、s(E)は相手ポケモンを代入した状態ベクトルです。今回、Eにはポケモン全種類(最終進化系129種類)を用いることにします。 言葉で説明すれば、Rは相手ポケモンに対して最善の技を選択したときのQ値を、想定されるすべての相手ポケモンに対して平均したものとなります。平均ではなくてmaxなのは、相手ごとに有効な技を1つ覚えていれば十分であるためです。

具体例として、上に示したギャラドスのパーティに対するRは、E={パラセクト, ニドクイン}としたときに


R(X) = \frac{1}{2} (\max \{0.31, 0.24, 0.36, 0.68\} + \max \{-0.15, 0.18, 0.37, 0.31\}) = \frac{1}{2} (0.68 + 0.37) = 0.525

となります。

実験結果

実際に上記の定式化を使って、ランダムに生成したパーティ1,000個の評価を行いました。エージェントは前回と同じid:7cfです。

トップ10のパーティは以下のようになりました。

R=0.675 フォレトス,LV55,かげぶんしん,すてみタックル,ソーラービーム,ギガドレイン
R=0.665 サイドン,LV55,かいりき,だいもんじ,じしん,ふみつけ
R=0.664 フォレトス,LV55,かげぶんしん,いわくだき,ギガドレイン,スピードスター
R=0.656 ヤドラン,LV55,じしん,かげぶんしん,ちきゅうなげ,とっしん
R=0.651 ガルーラ,LV55,ほのおのパンチ,のしかかり,かえんほうしゃ,いわくだき
R=0.646 サイドン,LV55,じしん,ばくれつパンチ,かえんほうしゃ,バブルこうせん
R=0.646 フォレトス,LV55,ころがる,おんがえし,とっしん,スピードスター
R=0.636 フォレトス,LV55,ずつき,すてみタックル,スピードスター,すなあらし
R=0.634 フォレトス,LV55,いわくだき,ソーラービーム,ころがる,すてみタックル
R=0.634 フォレトス,LV55,かげぶんしん,おんがえし,どくどく,いわくだき

ワースト10のパーティは以下のようになりました。

R=-0.601 レディアン,LV55,スピードスター,ばくれつパンチ,かげぶんしん,ずつき
R=-0.596 レディアン,LV55,ソーラービーム,すてみタックル,ずつき,ころがる
R=-0.584 レディアン,LV55,おんがえし,かげぶんしん,すてみタックル,ソーラービーム
R=-0.501 レディアン,LV55,どくどく,おんがえし,スピードスター,はかいこうせん
R=-0.501 レディアン,LV55,はかいこうせん,どくどく,おんがえし,すてみタックル
R=-0.496 レディアン,LV55,ばくれつパンチ,ずつき,れいとうパンチ,ソーラービーム
R=-0.383 ヤンヤンマ,LV55,かげぶんしん,ソーラービーム,ずつき,はがねのつばさ
R=-0.379 ヤンヤンマ,LV55,つばさでうつ,ずつき,ソーラービーム,はがねのつばさ
R=-0.378 ヤンヤンマ,LV55,スピードスター,つばさでうつ,ずつき,ソーラービーム
R=-0.366 ヤンヤンマ,LV55,はがねのつばさ,つばさでうつ,スピードスター,おんがえし

上位はフォレトスが占めています。あまり強いというイメージはないですが、物理技で突破するのは困難なので交代なしのルールでは強いのかもしれません。サイドンなども強力な技を覚えた個体が上位に来ていることが確認できます。下位はレディアンヤンヤンマでした。この時代、タイプ一致の虫タイプも飛行タイプも技に恵まれていないので妥当な結果と思われます。

このように、Q関数でパーティの強さを評価することができそうだと分かりました。今後、既存のパーティを評価するだけでなく、強いパーティの生成を試みたいと思います。

汎用行動選択モデルの学習 part08 Q関数の観察【PokéAI】

前回、強化学習で汎用行動選択モデルの学習が可能だということを確認しました。今回はパラメータチューニングは置いておいて、学習結果のモデルの出力を観察してみたいと思います。DQNで学習されるQ関数Q(s, a)は、状態sのときに行動aをとったときの割引報酬和の期待値に対応します。ここで、報酬は勝ちが1、負けが-1に設定して学習してあります。うまく学習ができていれば、残りHPであったり相手との相性によってQ値が変動する様子が観察できるはずです。

学習結果のモデル(id:7cf)同士で対戦させ、各局面でのQ値を記録しました。

まず、100パーティ中レート8位のギャラドスを使った場合の結果を示します。1位のカビゴンは相性の影響がある場面が少なかったためです。

むし・くさタイプのパラセクトを相手にしたバトルの全ターンを示します。

相手 パラセクト HP182/182  
自分 ギャラドス HP220/220  
技       Q値
いわくだき   0.31
ハイドロポンプ 0.24
なみのり    0.36
のしかかり   0.68
選択=> のしかかり

相手 パラセクト HP127/182  
自分 ギャラドス HP174/220  
技       Q値
いわくだき   0.48
ハイドロポンプ 0.50
なみのり    0.55
のしかかり   0.75
選択=> のしかかり

相手 パラセクト HP80/182 par 
自分 ギャラドス HP174/220  
技       Q値
いわくだき   0.85
ハイドロポンプ 0.85
なみのり    0.89
のしかかり   0.91
選択=> のしかかり

相手 パラセクト HP32/182 par 
自分 ギャラドス HP174/220  
技       Q値
いわくだき   0.94
ハイドロポンプ 0.93
なみのり    0.97
のしかかり   1.00
選択=> のしかかり

ハイドロポンプ等の水技が半減なので、のしかかりのQ値が相対的に高くそれが実際の行動として選ばれています。そして、相手のHPが減り、自分のHPがあまり減少していないという勝利に近い状況になるとQ値が大きくなっており、期待通りの結果になっているといえます。

次は天敵である電気タイプのサンダースとの対戦です。

相手 サンダース HP187/187  
自分 ギャラドス HP220/220  
技       Q値
いわくだき   0.02
ハイドロポンプ 0.15
なみのり    0.15
のしかかり   0.22
選択=> のしかかり

相手 サンダース HP130/187  
自分 ギャラドス HP220/220  
技       Q値
いわくだき   0.33
ハイドロポンプ 0.52
なみのり    0.46
のしかかり   0.51
選択=> ハイドロポンプ

相手 サンダース HP64/187  
自分 ギャラドス HP220/220  
技       Q値
いわくだき   0.65
ハイドロポンプ 0.70
なみのり    0.72
のしかかり   0.71
選択=> なみのり

最初のターンの時点で、パラセクト戦のときよりQ値が低いことがわかります。なお、自分のポケモンは種族を特徴量と入れており、相手のポケモンについては種族は入れずにタイプを入れています。タイプ相性による有利不利を判断できていると考えられます。

水技が抜群となるニドクインとの対面を示します。

相手 ニドクイン HP215/215  
自分 ギャラドス HP220/220  
技       Q値
いわくだき   -0.15
ハイドロポンプ 0.18
なみのり    0.37
のしかかり   0.31
選択=> なみのり

水技のQ値が高くなっており、技と相手の相性についても認識ができています。

相手 ギャラドス HP220/220  
自分 ニドクイン HP215/215  
技       Q値
10まんボルト -0.25
だいもんじ   0.46
バブルこうせん -0.37
ほのおのパンチ 0.34
選択=> だいもんじ

逆にニドクイン側は最適な10まんボルトのQ値がかなり低くなっており、間違いもまだまだ多いです。このような間違いにより強化学習モデルは教師あり学習モデルより弱くなっていると考えられます。

次に、レート最下位のエイパムについての結果を示します。

電気技が有効なフリーザーとの対戦です。

相手 フリーザー HP215/215  
自分 エイパム HP176/176  
技       Q値
でんじほう   0.24
10まんボルト 0.17
いわくだき   -0.07
どくどく    0.31
選択=> どくどく

相手 フリーザー HP197/215 tox 
自分 エイパム HP102/176  
技       Q値
でんじほう   0.10
10まんボルト 0.10
いわくだき   -0.03
どくどく    0.04
選択=> 10まんボルト

相手 フリーザー HP123/215 tox 
自分 エイパム HP31/176  
技       Q値
でんじほう   -0.20
10まんボルト -0.29
いわくだき   -0.42
どくどく    -0.36
選択=> でんじほう

まずどくどくで状態異常にしてから、電気技で攻めていくという戦略です。とはいえフリーザーの能力値が高いので勝てそうにないですが。ほかのバトルも観察すると、相手が状態異常でないときにどくどくを使うのではなく、自分のHPが減っていないときに使うという判断に見えました。

この技構成では手も足も出ないニドクインとの対面を示します。

相手 ニドクイン HP215/215  
自分 エイパム HP176/176  
技       Q値
でんじほう   -0.35
10まんボルト -0.78
いわくだき   -0.69
どくどく    -0.33
選択=> どくどく

初手の時点ですでにすべてのQ値がマイナスです。

この対面の最終ターンです。

相手 ニドクイン HP215/215  
自分 エイパム HP5/176  
技       Q値
でんじほう   -1.06
10まんボルト -1.16
いわくだき   -1.15
どくどく    -1.12
選択=> でんじほう

負けが確定的な場面で、Q値が-1より低くなっています。

ここまでで、タイプ相性やHPの減り方によってQ値が期待通り変動していることを確認できました。

さらに、相性があまり関係ない相手との最初のターンにおける出力を比較してみます。

相手 リングマ HP215/215  
自分 ギャラドス HP220/220  
技       Q値
いわくだき   -0.07
ハイドロポンプ 0.30
なみのり    -0.11
のしかかり   0.02
選択=> ハイドロポンプ
相手 リングマ HP215/215  
自分 エイパム HP176/176  
技       Q値
でんじほう   -0.27
10まんボルト -0.58
いわくだき   -0.58
どくどく    -0.39
選択=> でんじほう

自分のポケモンによってQ値に大きな違いがあることがわかります。特にいわくだきに対するQ値は、入力特徴としては自分のポケモンの種族だけが異なっている状態であり、ポケモンの強さを表現しているものと考えられます。またいわくだきは比較的弱い技なので、他の技より低いQ値となることが多いです。 ここから、「バトルの最初のターンにおけるQ値を、パーティの良さを表す指標として使えるのではないか」という仮説を提唱します。 パーティの良さを表す指標=パーティ評価関数を用いて強いパーティを構築する手法は過去に提案しました。このときはバトル中の行動の強化学習とは別個にパーティ評価関数を学習していましたが、Q関数の利用によりパーティの生成も実現できることが期待されます。

今回は、強化学習によって得たQ関数を用いて、バトル中の様々な状況におけるQ値を観察しました。タイプ相性やHPの減り方によってQ値が期待通り変動していることを確認できました。さらに、バトルの最初のターンにおけるQ値はパーティの良さを表しているという仮説を立てました。今後、Q関数をパーティ生成に活用する手法を検討します。

汎用行動選択モデルの学習 part07 DQNの学習結果【PokéAI】

前回、汎用行動選択モデルを強化学習させるシステムを実装しました。今回はその結果を評価します。

学習条件

強化学習関係のデフォルトの学習条件は以下のように設定しました。

  • アルゴリズム: DQN (double DQN)
  • 探索: epsilon-greedy
    • ランダム行動する確率epsilon: 0.3 (定数)
  • 報酬割引率: 0.95
  • バッチサイズ: 32
  • 最初に学習するまでのステップ数(replay bufferのサンプル数): 500
  • Nサンプル収集するたびにoptimize: 1
  • N optimizeごとにtarget networkのアップデート: 100
  • replay bufferサイズ: 100,000

2種類の条件をで強化学習を行いました。各種IDにはわかりやすいように1,2,3のような番号を振りたいところなのですが、後続の記事との一貫性をとるのが難しいためデータベース上のIDを用いることにします。

  • エージェント 7cf
    • モデル: 3層16チャンネル
    • パーティ群 good_200614_1 (パーティ数100)
      • バトルごとに、100パーティから2パーティをランダムに選択し自己対戦
    • 対戦数 10,000
  • エージェント 93d
    • モデル: 3層64チャンネル
    • パーティ群 good_200614_2 (パーティ数 1,000)
    • 対戦数 100,000

教師あり学習したモデルを同じフォーマットに変換したものはエージェント 316として表します。3層16チャンネルモデルで、900パーティ分の行動記録から学習しています。

定性評価

強化学習モデル、教師あり学習モデルおよびランダムに行動するプレイヤーを混合してレーティングバトルを行いました。

ランダムに生成したパーティ群を用意し、全エージェント・パーティの組み合わせをそれぞれプレイヤーと呼ぶことにします。例えばエージェント1,2,3とパーティA,Bがあるとき、プレイヤー1はモデル1がパーティAを操作、プレイヤー2はモデル1がパーティBを操作、プレイヤー3はモデル2がパーティAを操作、というように6人のプレイヤーがバトルに参加します。各プレイヤーは暫定レート(イロレーティング)を持ち、レートが近いもの同士を選択して対戦させることで各プレイヤーのレートを収束させます。1プレイヤーあたり100回対戦を行います。

まずは教師あり学習と同じ構造のモデルを評価します。エージェントに操作させるパーティ群はgood_200614_1で、強化学習の際に用いたのと同じパーティ群です。3エージェント、100パーティの全組み合わせで合計300プレイヤーがバトルに参加することになります。

各エージェントが操作したプレイヤーの平均レートを示します。

エージェント 平均レート
ランダム 1403
316(教師あり) 1553
7cf(強化学習) 1544

残念ながら、教師あり学習のほうが強いという結果になりました。

パーティ群を同じ条件で別途ランダムに生成したgood_200614_3を用いて試しました。

エージェント 平均レート
ランダム 1422
316(教師あり) 1565
7cf(強化学習) 1513

さらに教師ありと強化学習の差が開きました。任意のパーティを操作できるようにモデルを学習しようとしているとはいえ、学習に使ったパーティのほうが他のパーティよりうまく操作できる傾向がみられます。

よりパラメータ数・バトル数を増やしたエージェントも評価しました。パーティ群はgood_200614_3です。

エージェント 平均レート
ランダム 1413
316(教師あり) 1555
93d(強化学習) 1532

教師あり学習との差は縮まりましたが、まだ同等性能を達成できたとは言えません。 モデル構造は同じなので、学習方法の改善で解決するのが理想です。

定性評価

Trainer 7cfだけで対戦させてレーティングおよびバトルログを観察します。

上位下位パーティ10個を示します。レートの下にパーティ構成(1匹のみ)が続きます。

1882
カビゴン,55,,じしん,のしかかり,どろかけ,ソーラービーム
1843
ミルタンク,55,,かいりき,じしん,でんじほう,のしかかり
1828
ファイヤー,55,,おんがえし,かげぶんしん,だいもんじ,はかいこうせん
1744
マタドガス,55,,かえんほうしゃ,はかいこうせん,かげぶんしん,ヘドロばくだん
1733
フリーザー,55,,どろかけ,れいとうビーム,すなあらし,とっしん
1725
ハッサム,55,,ロケットずつき,いわくだき,かげぶんしん,おんがえし
1704
ムウマ,55,,おんがえし,スピードスター,サイコキネシス,かげぶんしん
1701
ギャラドス,55,,いわくだき,ハイドロポンプ,なみのり,のしかかり
1699
フリーザー,55,,ふぶき,かげぶんしん,はがねのつばさ,すてみタックル
1691
ケンタロス,55,,すてみタックル,かえんほうしゃ,いわくだき,かいりき
931
エイパム,55,,でんじほう,10まんボルト,いわくだき,どくどく
1168
ヤンヤンマ,55,,スピードスター,はがねのつばさ,ソーラービーム,つばさでうつ
1217
スピアー,55,,ロケットずつき,ギガドレイン,スピードスター,はかいこうせん
1229
ダグトリオ,55,,すてみタックル,とっしん,ヘドロばくだん,はかいこうせん
1249
バリヤード,55,,どろかけ,ずつき,ロケットずつき,10まんボルト
1266
ヤミカラス,55,,かげぶんしん,つばさでうつ,スピードスター,ゴッドバード
1271
ヘルガー,55,,かげぶんしん,どろかけ,いわくだき,スピードスター
1303
スピアー,55,,どくどく,とっしん,ロケットずつき,ギガドレイン
1305
フーディン,55,,ばくれつパンチ,とっしん,すてみタックル,おんがえし
1306
ルージュラ,55,,すてみタックル,はなびらのまい,はかいこうせん,どろかけ

上位にはカビゴンをはじめとした有力なポケモンが来ています。あくまでランダム生成なので技構成が完璧というわけではありませんが、各ポケモンにそれなりにあった技を所持しています。最下位はエイパムでした。エイパムの特攻種族値は40で、特殊電気技は全くマッチしません。いわくだきも威力20で話になりません。ポケモンと技のミスマッチが生じているパーティが下位に来ていることが見て取れます。

最上位だったカビゴンのバトル中の行動を定性的に確認します。

相手 サンダース HP187/187
自分 カビゴン HP292/292
じしん のしかかり どろかけ ソーラービーム
選択=> じしん

相手 サンダース HP52/187
自分 カビゴン HP207/292
じしん のしかかり どろかけ ソーラービーム
選択=> じしん
相手 ニョロトノ HP215/215
自分 カビゴン HP292/292
じしん のしかかり どろかけ ソーラービーム
選択=> じしん

相手 ニョロトノ HP160/215
自分 カビゴン HP255/292
じしん のしかかり どろかけ ソーラービーム
選択=> じしん

相手 ニョロトノ HP100/215
自分 カビゴン HP177/292
じしん のしかかり どろかけ ソーラービーム
選択=> じしん

相手 ニョロトノ HP43/215
自分 カビゴン HP137/292
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

原則的にじしんを使い、相手のHPが減っているとのしかかりを使うのでしょうか?タイプ一致を踏まえるとのしかかりのほうが威力が大きいため、本来はのしかかりをメインに使うのが正解と思われます。

相手 モンジャラ HP187/187
自分 カビゴン HP292/292
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

相手 モンジャラ HP135/187
自分 カビゴン HP262/292
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

相手 モンジャラ HP77/187 par
自分 カビゴン HP235/292
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

相手 モンジャラ HP24/187 par
自分 カビゴン HP235/292
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

じしんがいまひとつの相手(草タイプ)にはのしかかりを使っています。

相手 クロバット HP209/209
自分 カビゴン HP292/292
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

相手 クロバット HP135/209 par
自分 カビゴン HP239/292
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

相手 クロバット HP67/209 par
自分 カビゴン HP239/292
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

相手 クロバット HP67/209 par
自分 カビゴン HP239/292
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

じしんが無効の相手(飛行タイプ)にものしかかりを使えています。

相手 ガラガラ HP182/182
自分 カビゴン HP292/292
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

相手 ガラガラ HP152/182
自分 カビゴン HP233/292 brn
じしん のしかかり どろかけ ソーラービーム
選択=> どろかけ

相手 ガラガラ HP147/182 accuracy-1
自分 カビゴン HP176/292 brn
じしん のしかかり どろかけ ソーラービーム
選択=> じしん

相手 ガラガラ HP125/182 accuracy-1
自分 カビゴン HP118/292 brn
じしん のしかかり どろかけ ソーラービーム
選択=> どろかけ

相手 ガラガラ HP120/182 accuracy-2
自分 カビゴン HP82/292 brn
じしん のしかかり どろかけ ソーラービーム
選択=> どろかけ

相手 ガラガラ HP115/182 accuracy-3
自分 カビゴン HP26/292 brn
じしん のしかかり どろかけ ソーラービーム
選択=> のしかかり

brnはやけど状態を表します。やけどしたところでなぜかどろかけを使うように変化します。間違った学習をしているようです。

技選択の正しさの簡易評価として、技の選択回数に対する効果抜群となった回数の比率を確認してみましたが、ランダムに行動するよりは高い値になっていることがわかりました。対戦カードによってそもそも効果抜群の選択肢が存在するか否かが変動しますし、あまり安定する指標とは思えませんが。持続的に成果を確認できる定量評価指標が望まれます。

結論として、ある程度行動は正しいものの人が見て間違っている行動も混じっていました。定量的には教師あり学習より弱いという結果となっており、強化学習のパラメータの改善が必要と考えられます。

汎用行動選択モデルの学習 part06 DQNの自前実装【PokéAI】

前回まで、教師あり学習であらゆるパーティの行動選択を行えるモデルを学習させ、3層16チャンネル程度の全結合DNNである程度適切な行動がとれることがわかりました。 今回からはバトルの勝敗を報酬とした強化学習に取り組みます。

select766.hatenablog.com

実装面の課題として、まず強化学習フレームワークを選定する必要があります。もともとChainerベースのChainerRLを使っていたのですが、Chainerの開発終了に伴いPyTorchに移行を進めているという背景があり、強化学習フレームワークもPyTorchに対応したものが必要です。フレームワークの候補はいくつかありRLlibを少し触ってみたのですが、基本的に一人用ゲームの環境を想定した作りとなっており、対戦ゲームでモデル同士を自己対戦させるにはそれなりの追加実装を必要とします。また、状態によって選択可能な行動が制約される(技のPP切れ、交代先が瀕死かどうかなど)ことに対応する実装も必要です。そこで今回は、既存の強化学習フレームワークを使わず自前でDQN (Deep Q-Network)を実装することにしました。メリットは、ポケモンバトル用に特化した実装ができるため自由度が高く、コードの見通しが良くなります。デメリットはDQN以外の強化学習アルゴリズム(A3C, ACER, PPO等)を使いたくなった場合には各アルゴリズムを自前で実装しなければならない点です。本当は確率的な行動選択をモデリングするACER等の方策ベースの強化学習アルゴリズムのほうが読み合いを要するゲームにふさわしいように思えますが、その域に至るのは当分先と考え、実装が容易な価値ベースのDQNを実装することにしました。より具体的には、DQNを少し改良したDouble DQNを実装しました。

実装は、

を参考にDQNのコアアルゴリズムを実装しつつ、ポケモンバトルのシミュレータの呼出し方法などに即してデータの取り回しを独自に実装しました。

システムの構成を下図に示します。

f:id:select766:20200611214915p:plain
強化学習システム構成

ポイントは、モデルの更新を行うTrainerと、バトル中の行動を決定するAgentを分離している点です。DQNの学習では、(state(ターンNの状態), action(ターンNで選択した行動), next_state(ターンN+1の状態), reward(ターンNで得た報酬))という組の情報をreplay bufferに集積してモデルの更新に使うわけですが、バトルには2人のプレイヤーが必要で、それぞれ同じターンに異なる状態を観測することになります(自分のポケモンが覚えている技の情報がstateの一部に含まれるため)。Agentはプレイヤー1人分の視点で時系列を記録することにより見通しを良くしたうえで、両方のプレイヤーが得たreplay bufferをTrainerが持つ単一のreplay bufferに集積することで1つのモデルを更新するという仕組みとしました。

学習結果を軽く説明します。バトルごとに100パーティから2パーティをランダムにピックアップし、3層16チャンネルのモデルを10000エピソード(バトル数)学習させました。 学習後のモデルとランダムに行動するエージェントを、同じ100パーティから2パーティランダムにピックアップした条件で1000回対戦させたところ学習後のモデルの勝率が66.6%となり、学習の効果があることがわかりました。 前回教師あり学習で得たモデルはランダムに対する勝率が66.7%となり、今回の強化学習結果と近い強さである可能性が高いことがわかりました。モデル同士の対戦は未実装のため、今後評価していければと思います。 ランダムに行動する相手に対してあまり高くない勝率なのは、バトルの条件が1vs1なので、ポケモン同士の相性がかなり効いている可能性があります。

次回、学習したモデルがとった行動の分析を提示できればと思います。

AWSスポットインスタンスの起動高速化【コンピュータ将棋】

先日の世界コンピュータ将棋オンライン大会では、ねね将棋はAWSのスポットインスタンス上で将棋エンジンを動作させていました。スポットインスタンスAWS上の余剰計算資源を安価に貸し出すような形態で、余剰が少なくなると強制的に終了されてしまう仮想マシンです。メリットは価格がオンデマンドインスタンス(強制終了されない通常の仮想マシン)と比べて1/3程度になることです。今回使用した8GPUのインスタンスはオンデマンドですと1時間当たり3000円を超えるため、金銭負担を軽減するためリスクを受け入れてスポットインスタンスを利用することにしました。幸い今回のイベントでは一度も強制終了されることはなくすべての対局を実施できました。一応強制終了されても対局を放棄しないようにローカルマシンに切り替える仕掛けとして使えるのがフェイルオーバーツールで、以前の記事で紹介しました。

select766.hatenablog.com

この記事ではスポットインスタンスを利用したことによる別の問題について事後検証結果を報告します。その問題は、起動に時間がかかるという点です。スポットインスタンスはAMIと呼ばれる仮想マシンテンプレートのようなものを指定して起動します。AMIにはスナップショットと呼ばれる読み取り専用のディスクイメージが関連付けられていて、ここにOSやソフトウェアがインストールされた状態になっています。スポットインスタンス起動時にはスナップショットをコピーして読み書き可能なディスク(EBSボリューム)が生成されるという挙動になります。

今回の運用では各対局直前にスポットインスタンスを起動するという運用をとってコストを最小化する方針としました。これを実施するにあたり生じた問題は、スポットインスタンスでは起動直後のランダムアクセスのパフォーマンスが著しく悪い(と思われる挙動をする)という問題でした。具体的には将棋エンジン上でCUDA、TensorRT、DNNモデルを読み込む部分で6分ほどもかかるという点でした。一度読み込みができると、将棋エンジンを終了して再度読み込みを行っても1分以内で完了するということも分かりました。スポットインスタンスの起動の指示を出してからsshでログイン可能となるまで2分程度かかるため、対局前にトータルで10分程度時間が必要でした。

今後のためにこの読み込み速度を改善できないか調べたところ、Amazon EBS ボリュームの初期化という記事を見つけました。

スナップショットから復元されたボリュームへのアクセスは、ストレージブロックがAmazon S3からプルダウンされてボリュームに書き込こまれると可能になります。この事前処理には一定の時間がかかるため、各ブロックへの初回アクセス時には、I/O 操作のレイテンシーが著しく増加する可能性があります。ボリュームのパフォーマンスは、すべてのブロックがダウンロードされてボリュームに書き込まれると正常値に達します。

オンデマンドインスタンスにはあらかじめ対応するEBSボリュームが存在しており、起動直後からランダムアクセスのパフォーマンスが十分高いのですが、スポットインスタンスでは上記の挙動が発生することでCUDA等の読み込み速度が低下している可能性があります。

この問題への対処策として、Amazon EBS 高速スナップショット復元が使えそうでした。

Amazon EBS 高速スナップショット復元を使用するとスナップショットからボリュームを作成でき、このボリュームは作成時に完全に初期化された状態になります。これにより、ブロックの初回アクセス時における I/O オペレーションのレイテンシーがなくなります。高速スナップショット復元を使用して作成されたボリュームでは、プロビジョンドパフォーマンスをすべて即座に提供できます。

これを実際に試してみました。この機能は1時間当たり$0.75×アベイラビリティゾーン数だけコストがかかるため、本番利用の日にだけ有効化すべきです。

f:id:select766:20200505173144p:plain
スナップショットを右クリックし、Manage Fast Snapshot Restoreをクリック

f:id:select766:20200505173238p:plain
Availability Zoneをすべて選択、Saveをクリック

f:id:select766:20200505173413p:plain
Save後、再度この画面を開くとenabling, optimizingという状態に。これは作業途中。マニュアルによれば「スナップショットの最適化には TiB あたり 60 分を要します。」

f:id:select766:20200505173443p:plain
数分待つとenabledになっていた

以上の設定をしたうえで、従来通りスポットインスタンスを起動し、そこから対局開始までの所要時間を計測しました。

f:id:select766:20200505173621p:plain
「テンプレートからインスタンスを起動」をクリックする時点で計測開始

結果は、1分23秒でssh接続が可能となり、直後に将棋エンジンを起動したところ計測開始から2分3秒で対局が開始しました。従来ですと8分以上かかっていたため大幅に改善したことになります。なお高速スナップショット復元には利用頻度の制限があるようですが、今回のように90GBのイメージを1時間あたり最大2回程度起動する用途であれば十分のようです。

AWSのスポットインスタンス上のGPU利用将棋エンジンの起動高速化に高速スナップショット復元が利用できることがわかりました。次の機会があれば設定したいと思います。