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

汎用行動選択モデルの学習 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利用将棋エンジンの起動高速化に高速スナップショット復元が利用できることがわかりました。次の機会があれば設定したいと思います。

TensorRTのバッチサイズチューニング【コンピュータ将棋】

前回の記事ではNVIDIA GPU上でDNNモデルを高速実行できるライブラリであるTensorRTの使い方について紹介しました。

select766.hatenablog.com

その中で、最適化プロファイルの設定というチューニング項目の効果について紹介します。

TensorRTはモデルとGPUの組み合わせに対して実行計画を最適化しますが、バッチサイズによって適切な実行計画は異なる可能性があります。そのため、複数の実行計画(プロファイル)を使い分ける機能が搭載されています。 プロファイルは、(kMIN=最小バッチサイズ, kOPT=最適バッチサイズ, kMAX=最大バッチサイズ)という3つの数値の組を与えて生成します。最適バッチサイズの入力が与えられたときの実行速度が最大となるように実行計画が最適化されます。

実際にこのパラメータにより実行速度がどう変化するのか実測値を掲載します。実験環境はAWSのp3.2xlargeインスタンスで、Tesla V100 GPUが1基搭載されています。ベンチマークには自作のプログラムを用いました。 https://github.com/select766/tensorrt-shogi

今回使用したモデルは将棋用の9x9の画像を入力とするCNNで、チャンネル数256、161層あります。詳しい定義へのリンクを載せておきます。

https://github.com/select766/neneshogi_v3/blob/b9cf80249baa819673ecba0a0720a3aebaccf7fb/neneshogi/neneshogi/models/resnet.py#L27

ch: 256
depth: 79
block_depth: 2
move_hidden: 2048

最適化プロファイルの前に、32bit浮動小数点数での演算と16bit浮動小数点数での演算での比較を掲載します。最適化プロファイルは、(1, 256, 256)の1つだけを使用します。なお最適化プロファイルにはバッチサイズのほかにメモリ容量という項目もあるのですがこちらは設定していません。

まずは1バッチあたりの計算時間を比較します。

f:id:select766:20200502093159p:plain
バッチサイズと計算時間

理論的な計算量はバッチサイズに比例しますが、実際の計算時間は階段状の変化を見せています。バッチサイズが一定の幅に入っている場合に計算時間はほぼ一定になっているとみなすことができます。また浮動小数点数の精度により大きな速度差があり、バッチサイズ250の時には16bitは32bitの6.5倍速にもなっています。計算誤差が許容できるかどうかには拠りますが、16bitで計算することによる速度向上はかなり大きいです。

同じ測定結果を1秒あたりに処理できるサンプル数に換算して示します。

f:id:select766:20200502093247p:plain
バッチサイズと1秒当たりのサンプル数

16bitの場合で述べますと、極大値がバッチサイズ63、126、252の時に発生しており、そのサイズを超えると一気に性能が悪くなります。2の累乗より少し手前が極大になるようですが、2の累乗-1というわけでもなく実測してみるしかなさそうです。バッチサイズ126のときと252のときでは速度があまり変わらないことを考えると、レイテンシが小さいバッチサイズ126を最大サイズとして用いるのがよさそうです。

追記(2020-05-10): GPUのSM (Streaming Multiprocessor)の数から最適値がある程度予想できるそうです。

これ以降は16bitに固定し、最大バッチサイズ126として最適化プロファイルの効果を確認します。

最適化プロファイルを(1,126,126)の1つだけとした場合と、[(1,1,1),(2,15,15),(16,126,126)]の3つを用いた場合、それと従来の(1,256,256)を比較しました。

f:id:select766:20200504164606p:plain
バッチサイズと計算時間(16bit、最適化プロファイル設定比較)

バッチサイズ126に対して最適化を行うことで、256に対する最適化を行うよりバッチサイズ126時点での速度を高められています。一方、バッチサイズ63付近では劣っています。

f:id:select766:20200504164655p:plain
バッチサイズ20までを拡大

バッチサイズ1,15,126に対して最適化を行うと、まずバッチサイズ1における処理時間を半分ほどに下げることができています。バッチサイズ15までについても、バッチサイズ256に最適化したときと同等の性能となっています。ルート局面付近などの評価ではバッチサイズ1やそれに近い少量のサンプルだけを評価したいため、その遅延を下げられることのメリットがある可能性があります。

TensorRTの最適化はブラックボックスのため試行錯誤するほかないですが、チューニングはかなり影響があるという結果に注意が必要といえます。

*1:n*(1<<14)*SM)/(H*W*C

USIプロトコルのコンピュータ将棋エンジンのフェイルオーバーツール【コンピュータ将棋】

コンピュータ将棋エンジンの強さを最大化するにはAWS等のクラウド上の強力なマシンの利用が不可欠ですが、大会で本番対局中に回線が切れるという可能性があります。また普段より性能の高いマシンで十分に動作テストが行えず、クラッシュするリスクもあります。このような問題が生じた際に時間切れ負けとして諦めるのではなく、ローカルのマシン上で別のエンジンに切り替えて終局まで指し続けられるようにするツールを作成しました。

github.com

今年は世界コンピュータ将棋選手権が中止となり代替としてオンライン開催になります。オンライン開催だとローカルマシンと対局サーバ間の通信が切れるリスクと、クラウドとローカルマシン間の通信が切れるリスクは同じなのであまりこのツールの出番はないかもしれません。とはいえ作りかけで放置するとソースコードの構造を忘れてしまうので、完成させて公開しました。

TensorRTを用いて将棋AI向けDeep Neural Networkの推論を高速化する【コンピュータ将棋】

TensorRTは、NVIDIA社が提供している、Deep Neural Networkの推論を高速に行うライブラリです。NVIDIA社のGPU上での推論(学習済みモデルの実行)に特化しており、CaffeやPyTorchで学習したモデルを読み込んで実行計画を最適化したうえで推論してくれます。もちろんPyTorch等の学習ができるフレームワークでも推論に使えるのですが、推論専用の最適化がなされるところが特徴的で、実験してみたところPyTorchより高速な結果を得ることができました。

私は今回TensorRTを将棋AI向けのDNNの推論に用いました。もともとの動機としてはPythonを用いてPyTorchで学習したモデルをC++で書かれたゲーム木探索部から使いたいという目標があり、C++から使えて推論が高速なライブラリであるとみなすことができるので使いました。PyTorchのモデルをC++から使うためにはLibTorchを使うという手段もあるかと思いますが、推論速度の最適化がデフォルトでなされるので簡単というのがTensorRTのメリットになります。ここでは将棋のDNNを対象にしていますが、出力が2つある点以外は画像分類タスクと同じです。

ソースコードはこちらで公開しています。TensorRTについてはほとんど日本語の記事が見当たらず、サンプルを呼んだものの試行錯誤が必要だった点がいくつかあるためポイントなどを解説します。

github.com

モデル形式

TensorRTはいくつかの形式のモデルを読み込むことができますが、PyTorchで学習したモデルを用いる場合はONNX形式が使えます。

DNNはResNetをベースとした畳み込み層主体のもので、画像分類と似ていますが出力が2つあるものを扱います。入力サイズ: (batchsize, 119, 9, 9)、出力サイズ: policy=(batchsize, 2187)、 value=(batchsize, 2)。

ここでのポイントは、バッチサイズを実行時に動的に変えられるようにすることです。そのためにはdynamic_axesというオプションが必要でした。

torch.onnx.export(model, torch.randn(1, 119, 9, 9), "/path/to/output/file", export_params=True, opset_version=10,
                  verbose=True, do_constant_folding=True, input_names=["input"],
                  output_names=["output_policy", "output_value"],
                  # TensorRTでバッチサイズを可変にする際に必要
                  dynamic_axes={'input': {0: 'batch_size'},  # variable length axes
                                'output_policy': {0: 'batch_size'},
                                'output_value': {0: 'batch_size'}})

ここから先はPyTorchはお役御免で、出力されたONNXモデルファイルだけを使います。

環境構築

OS: Ubuntu 18.04を想定します。GPUのドライバ及びCUDA 10.2をインストールしておきます。

cuDNN v7.6.5 (November 18th, 2019), for CUDA 10.2 (cuDNN Library for Linux)およびTensorRT 7.0.0.11 for Ubuntu 18.04 and CUDA 10.2 tar packageを探してダウンロードします。Ubuntu向けのパッケージファイルもあるのですが使い方がよくわかりませんでしたので、tar.gzのほうを使いました。

必要に応じて次のように(雑なやり方ですが)パスを通します。TensorRT自体は実行バイナリ状態で配布されているのでビルド作業はありません。

sudo bash
mkdir /usr/local/mycudnn
cd /usr/local/mycudnn
tar zxvf cudnn-10.2-linux-x64-v7.6.5.32.tgz
tar zxvf TensorRT-7.0.0.11.Ubuntu-18.04.x86_64-gnu.cuda-10.2.cudnn7.6.tar.gz
echo /usr/local/mycudnn/cuda/lib64 >> /etc/ld.so.conf.d/00cudnn.conf
echo /usr/local/mycudnn/TensorRT-7.0.0.11/targets/x86_64-linux-gnu/lib >> /etc/ld.so.conf.d/00cudnn.conf
ldconfig

TensorRTを使うアプリケーションのコンパイル、リンクについてはいくつかオプションが必要なのでMakefileを参照ください。

大まかな流れ

サンプルコードを抜粋して解説していきます。これは、モデルの推論速度を様々なバッチサイズでテストするベンチマークです。GPUごとにスレッドを立てて同時実行することによりマシン全体でのパフォーマンス測定もできます。

includeしているcommon.hなど、リポジトリに同梱されているものはTensorRT本体ではなく、サンプルコードで使われているユーティリティです。典型的な使い方をする限りはこれらを活用すると楽になります。

TensorRTを使用する際の手順は、

  • 使用するGPU番号を指定する
  • ONNXモデルからエンジンをビルドするか、シリアライズされたエンジンをロードする
  • エンジンにデータを与えて実行

となります。

使用するGPU番号を指定する

https://github.com/select766/tensorrt-shogi/blob/aa9799d6a8f99c17656e3b5294f8450204c4e3fc/cpp/multi_gpu_bench.cpp#L502

if (cudaSetDevice(device) != cudaSuccess)
{
    gLogError << "cudaSetDevice failed" << std::endl;
    return;
}

cudaSetDeviceで使用するGPUを選択します。0~GPU数-1の整数です。このAPIはTensorRTのものではなくてCUDAを直接叩くことになります。これを呼び出したスレッドで使うGPU番号の指定となります。

ONNXモデルからエンジンをビルドする

https://github.com/select766/tensorrt-shogi/blob/aa9799d6a8f99c17656e3b5294f8450204c4e3fc/cpp/multi_gpu_bench.cpp#L327

auto parsed = parser->parseFromFile(
    "data/trt/model.onnx", static_cast<int>(gLogger.getReportableSeverity()));

onnxモデルファイルを読み込みます。

builder->setMaxBatchSize(batchSizeMax);

使用する最大バッチサイズを指定しておく必要があります。

config->setFlag(BuilderFlag::kINT8);

計算を8bitで量子化された状態で行う場合はこのオプションを指定します。ただ、量子化するためのスケール値を適切に指定しないと誤差が大きすぎて使用できません。私が今回使う予定のV100(GPUの型番)では8bit演算コアがないようなので、このオプションは使用していません。

config->setFlag(BuilderFlag::kFP16);

計算を16bit浮動小数点数で行うオプションです。V100ではこれを指定すると(デフォルトの32bitと比べて)倍速以上の速度が出る場合があります。ただし計算誤差が出ます。

最適化プロファイルの作成

https://github.com/select766/tensorrt-shogi/blob/aa9799d6a8f99c17656e3b5294f8450204c4e3fc/cpp/multi_gpu_bench.cpp#L194

TensorRTはモデルとGPUの組み合わせに対して実行計画を最適化しますが、バッチサイズによって適切な実行計画は異なる可能性があります。そのため、複数の実行計画(プロファイル)を使い分ける機能が搭載されています。 プロファイルは、(最小バッチサイズ, 最適バッチサイズ, 最大バッチサイズ)という3つの数値の組を与えて生成します。最適バッチサイズの入力が与えられたときの実行速度が最大となるように実行計画が最適化されます。 小さいバッチサイズに対して、大きいサイズとは別のプロファイルを作成するほうが小さいバッチサイズでの性能が高くなることが期待できます。実験結果については別の記事で書きたいと思いますが、指定方法は以下のようになります。

auto profile = builder->createOptimizationProfile();
profile->setDimensions(inputTensorNames[0].c_str(), OptProfileSelector::kMIN, Dims4{lastbs + 1, 119, 9, 9});
profile->setDimensions(inputTensorNames[0].c_str(), OptProfileSelector::kOPT, Dims4{bs_opt, 119, 9, 9});
profile->setDimensions(inputTensorNames[0].c_str(), OptProfileSelector::kMAX, Dims4{bs_max, 119, 9, 9});
int profileIdx = config->addOptimizationProfile(profile);
for (int b = lastbs + 1; b <= bs_max; b++)
{
        profileForBatchSize[b] = profileIdx;
}

OptProfileSelector::kMINでプロファイルが対応する最小バッチサイズ(厳密には、バッチサイズ以外の次元も含めたテンソルの最小サイズ)、OptProfileSelector::kOPTが最適バッチサイズ、OptProfileSelector::kMAXが最大バッチサイズの指定となります。これらを指定したのちconfig->addOptimizationProfileを呼び出すことでプロファイルが登録され、プロファイル番号が得られます。この番号は推論時に必要になりますので、バッチサイズごとにどのプロファイル番号を使用するかを配列に保存しています。

このソースコードでは何やら文字列のパースと絡めていますが、次のような指定ができるようにしています。

profileBatchSizeRange: opt1-max1-opt2-max2...

profileBatchSizeRange=="10-20-100-200"のとき、

  • バッチサイズ1~20について、バッチサイズ10に最適化したプロファイルを作成
  • バッチサイズ21~200について、バッチサイズ100に最適化したプロファイルを作成

なお、プロファイルは必ずしも複数作る必要はなく、最小バッチサイズ~最大バッチサイズをカバーする1つのプロファイルだけでも十分動作します。

エンジンをビルドした後、推論に必要な「コンテキスト」を作成する必要があります。コンテキストはプロファイルごとに作成する必要があります。1つのコンテキストに対してsetOptimizationProfileを毎回呼び出して対象プロファイルを切り替えるという操作はエラーとなるようです。

for (int i = 0; i < mEngine->getNbOptimizationProfiles(); i++)
{
    auto ctx = std::shared_ptr<nvinfer1::IExecutionContext>(mEngine->createExecutionContext(), samplesCommon::InferDeleter());
    if (!ctx)
    {
        return false;
    }
    ctx->setOptimizationProfile(i);
    mContextForProfile[i] = ctx;
}

エンジンのシリアライズ

実行してみると分かりますが、エンジンのビルドには数十秒~数分かかります。ビルド中に様々な実行計画の候補を比較検討しているのだと予想されます。アプリケーション起動時に毎回待たされるのは困るので、ビルド済みのエンジンをファイルに保存して再利用することができます。

https://github.com/select766/tensorrt-shogi/blob/aa9799d6a8f99c17656e3b5294f8450204c4e3fc/cpp/multi_gpu_bench.cpp#L251

IHostMemory *serializedModel = mEngine->serialize();

ofstream serializedModelFile(serializePath, ios::binary);
serializedModelFile.write((const char *)serializedModel->data(), serializedModel->size());

シリアライズされたエンジンのロードは以下のように行います。ちょっと煩雑に見えますが、ファイルサイズをチェックした後、単にファイル全体をfdataに読み込んで、runtime->deserializeCudaEngineに渡しているだけです。

https://github.com/select766/tensorrt-shogi/blob/aa9799d6a8f99c17656e3b5294f8450204c4e3fc/cpp/multi_gpu_bench.cpp#L261

ifstream serializedModelFile(serializePath, ios::in | ios::binary);
serializedModelFile.seekg(0, ios_base::end);
size_t fsize = serializedModelFile.tellg();
serializedModelFile.seekg(0, ios_base::beg);
std::vector<char> fdata(fsize);
serializedModelFile.read((char *)fdata.data(), fsize);

auto runtime = createInferRuntime(gLogger);
mEngine = std::shared_ptr<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(fdata.data(), fsize, nullptr), samplesCommon::InferDeleter());

なお、エンジンはGPUの機種に固有のものなので、シリアライズしたものを別のマシンに持っていっても動くとは限りません。

推論

CPUとGPUでデータをやり取りするための入出力バッファを作成します。

https://github.com/select766/tensorrt-shogi/blob/aa9799d6a8f99c17656e3b5294f8450204c4e3fc/cpp/multi_gpu_bench.cpp#L371

auto mContext = mContextForProfile.at(profileForBatchSize[batchSize]);
std::string inputBindingName = addProfileSuffix(inputTensorNames[0], profileForBatchSize[batchSize]);
int bidx = mEngine->getBindingIndex(inputBindingName.c_str());
mContext->setBindingDimensions(bidx, Dims4{batchSize, 119, 9, 9});

まずバッチサイズに対応するコンテキストを取り出します。次に、コンテキストにバッチサイズを教えるための手順があります。プロファイルが複数ある場合にこれがトリッキーで、プロファイル0の場合は入力テンソル名(ONNXのエクスポート時に指定した'input')をgetBindingIndexに与えればいいのですが、プロファイル1以降では'input [profile 1]'のようなプロファイル番号を組み合わせた入力テンソル名を与える必要があります。かなり不可解な仕様ですが、これをやると動きます。参考

ここから先はデータのやり取りと実行ですが、特に変なことはありません。samplesCommon::BufferManagerを使えば容易です。

標準出力の抑制

TensorRTは何かとデバッグメッセージが標準出力・標準エラー出力に吐き出されるのですが、将棋AIでは標準入出力を指し手のやり取りに使うので邪魔になりますのでこれを抑制します。

https://github.com/select766/tensorrt-shogi/blob/aa9799d6a8f99c17656e3b5294f8450204c4e3fc/cpp/multi_gpu_bench.cpp#L615

setReportableSeverity(Logger::Severity::kINTERNAL_ERROR);

以上のようなテクニックを駆使することで、将棋AIにTensorRTを組み込むことができます。