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

ジョウト地方への第一歩【PokéAI】

4月の技術書典6以来多忙でプロジェクトを一時停止していたのですが、再開していきたいと思います。なお、ネタが用意できていないため9月の技術書典7には出展致しません。

今まで初代ポケモンのルールに準拠してAIの開発と実験を行なっていましたが、ポケモン間の連携を取る補助技がなく、複雑な手順を踏まずに使える攻撃技でとにかく殴るという結果になりがちでした。この状況を打開するため、ポケモン金銀のルールへと移行したいと考えています。ポケモン金銀ではポケモンや技の種類が増え、またタイプ相性としてもエスパータイプ一強ではなくなっています。ゲームバランスの調整だけではなく、天候をはじめとした場の効果が導入されたことで、補助技で場を整える専門のポケモンと、整った場で相手にとどめを刺すポケモンという連携が可能になります。今後の技術開発で見応えある戦略が登場することを期待しています。

ルールを変更するにあたり、ポケモンバトルのルールを再現するシミュレータが必要になります。 今まで初代ルールを実装した自作のシミュレータを利用していましたが、流石に金銀のシミュレータを作るのは大変ですし、AI開発に集中したいのでオープンソースのシミュレータの活用を試みることにし、使い方を調べ始めました。

シミュレータとして、Pokémon Showdownが使えそうでした。 github.com オンライン対戦サーバシステムがTypeScriptで実装されており、その中のシミュレータ部分を抜き出せばAI同士の対戦がさせられそうです。

かなり複雑なシステムになっており全容を把握できていないのですが、とりあえずこのファイルPokemon-Showdown/battle-stream-example.ts at master · Zarel/Pokemon-Showdown · GitHubがサンプルになっていて、ランダムに生成したパーティ同士をランダムな行動で対戦させることができるようです。これをベースに、ランダムなパーティを生成・対戦させ、ポケモンや技の強さを回帰するモデルを学習させてみることとしました。ソースコードはこちらですpokeai/random.js at e2a150eac4549c97b12fb57ce4db8170a87ebf01 · select766/pokeai · GitHub。TypeScriptで記述されているので型情報などを使いたいところなのですが、ビルドスクリプトを見るとコンパイル済みJavaScriptコードを文字列置換する処理などが入っており、型情報を含んだライブラリとして使うことが難しそうです。 強さの回帰アルゴリズム自体は以前の記事と同じ物を実装しましたselect766.hatenablog.com

ポイントを解説すると、

  • Dex.generateTeam('gen2customgame')で第2世代(金銀)準拠のパーティが生成されるようです。6体分の情報が配列に入っているので、.slice(0, 3)で3体分を取り出して従来通り3vs3の対戦にします。
  • バトル中の行動自体はRandomPlayerAIとして実装されているものをそのまま使います。
  • シミュレータからのメッセージで、|win|1のような勝ったプレイヤー名が返ってくるので、これを抽出してパーティとその勝敗を記録します。

これで、パーティの生成と対戦を繰り返して記録を生成しました。1回の対戦に対しこのような情報が出力されます。 ポケモン名、技、個体値などの情報が入ったパーティと勝敗情報です。

{'parties': [[{'name': 'Nidoking',
    'species': 'Nidoking',
    'moves': ['earthquake', 'thunder', 'rockslide', 'lovelykiss'],
    'ability': 'None',
    'evs': {'hp': 255,
     'atk': 255,
     'def': 255,
     'spa': 255,
     'spd': 255,
     'spe': 255},
    'ivs': {'hp': 30, 'atk': 30, 'def': 30, 'spa': 30, 'spd': 30, 'spe': 30},
    'item': 'leftovers',
    'level': 68,
    'shiny': False,
    'gender': 'M'},
   {'name': 'Mew',
    'species': 'Mew',
    'moves': ['swordsdance', 'earthquake', 'shadowball', 'explosion'],
    'ability': 'None',
    'evs': {'hp': 255,
     'atk': 255,
     'def': 255,
     'spa': 255,
     'spd': 255,
     'spe': 255},
    'ivs': {'hp': 30, 'atk': 30, 'def': 30, 'spa': 30, 'spd': 30, 'spe': 30},
    'item': 'leftovers',
    'level': 64,
    'shiny': False,
    'gender': 'N'},
   {'name': 'Raichu',
    'species': 'Raichu',
    'moves': ['thunderbolt', 'surf', 'thunderwave', 'reflect'],
    'ability': 'None',
    'evs': {'hp': 255,
     'atk': 255,
     'def': 255,
     'spa': 255,
     'spd': 255,
     'spe': 255},
    'ivs': {'hp': 30, 'atk': 30, 'def': 30, 'spa': 30, 'spd': 30, 'spe': 30},
    'item': 'leftovers',
    'level': 78,
    'shiny': False,
    'gender': 'M'}],
  [{'name': 'Nidoqueen',
    'species': 'Nidoqueen',
    'moves': ['earthquake', 'thunderbolt', 'fireblast', 'lovelykiss'],
    'ability': 'None',
    'evs': {'hp': 255,
     'atk': 255,
     'def': 255,
     'spa': 255,
     'spd': 255,
     'spe': 255},
    'ivs': {'hp': 30, 'atk': 30, 'def': 30, 'spa': 30, 'spd': 30, 'spe': 30},
    'item': 'leftovers',
    'level': 70,
    'shiny': False,
    'gender': 'F'},
   {'name': 'Rapidash',
    'species': 'Rapidash',
    'moves': ['fireblast', 'sunnyday', 'hiddenpowergrass', 'bodyslam'],
    'ability': 'None',
    'evs': {'hp': 255,
     'atk': 255,
     'def': 255,
     'spa': 255,
     'spd': 255,
     'spe': 255},
    'ivs': {'hp': 6, 'atk': 28, 'def': 28, 'spa': 30, 'spd': 30, 'spe': 30},
    'item': 'leftovers',
    'level': 78,
    'shiny': False,
    'gender': 'M'},
   {'name': 'Pidgeot',
    'species': 'Pidgeot',
    'moves': ['return', 'thief', 'hiddenpowerflying', 'reflect'],
    'ability': 'None',
    'evs': {'hp': 255,
     'atk': 255,
     'def': 255,
     'spa': 255,
     'spd': 255,
     'spe': 255},
    'ivs': {'hp': 14, 'atk': 24, 'def': 26, 'spa': 30, 'spd': 30, 'spe': 30},
    'item': '',
    'level': 78,
    'shiny': False,
    'gender': 'M'}]],
 'winner': 0}

いくつか気になる点があり、ポケモンによってレベルが違ったり、個体値を調整して都合がいい「めざめるパワー」のタイプが選ばれたりしています。どういう仕組みでランダムなパーティを生成しているのかまだ調べられていないのですが、何かしらのバランス調整のための情報が用いられていると考えられます。そもそも進化前のポケモンが入っていません。パーティ構成をAIが考えるというコンセプト上、できれば純粋なランダム生成を実装したいところです。

対戦記録を15755件生成し、強さの回帰モデルを学習してみました。10万件生成しようとしたのですが、メモリリークが生じてクラッシュしていました。これもブラックボックスの弊害かもしれません。線形SVMを学習したところ、学習データ自体に対する勝敗の識別率は61%となり、あまり精度が良くないようです。前述のバランス調整の影響があると思われます。学習された係数のtop10, worst10を以下に示します。係数が大きいものは勝ちに寄与、係数が小さいものは負けに寄与していると見なせます。パーティに含まれるポケモンおよび技の特徴(P, M特徴)を用いています。

ポケモンor技 係数
バタフリー -0.377
プレゼント -0.354
セレビィ -0.296
ヤンヤンマ -0.292
うずしお -0.265
カモネギ -0.244
ワタッコ -0.208
じばく -0.200
はかいこうせん -0.197
ムウマ -0.195
ポケモンor技 係数
サイコウェーブ 0.182
てんしのキッス 0.183
ジュゴン 0.185
レアコイル 0.193
やどりぎのタネ 0.195
デンリュウ 0.196
バンギラス 0.196
ソーナンス 0.207
アーボック 0.244
めざめるパワーエスパー 0.286

「プレゼント」や「じばく」が悪く、「バンギラス」が良いのはなんとなくわかりますが、今ひとつの結果です。 進化前のポケモンが入っていないなどの影響で、明確に弱い特徴がデータセットに含まれていない可能性が高いです。

今回はAIを金銀ルールに対応させる最初の第一歩を踏み出すということでオープンソースのシミュレータの導入とランダムな対戦のみを行いました。まずはパーティの生成方法をコントロールするなど、シミュレータの扱い方を習得する必要がありそうです。