強化学習の題材としてファミコンのスーパーマリオをクリアさせるというのは見た目的におもしろいっちゃーおもしろいけど、正直右Bダッシュにジャンプを組み合わせればクリアするピンポイントを発見してそれっぽく学習したかのように見えてしまうのではないかという疑念があった(自分的には)。 本当にまともに学習してるのかというのを試したくて、別の題材としてパックマンを動かしてみることにした。

とはいえパックマンは固定画面、わかりやすい色使い、死亡・クリア条件が明確、可能な行動が「上下左右または入力なし」と少ない、 と直感的にはスーパーマリオに比べても単純だし余裕でステージクリアくらいするっしょと思っていた。
Gym(nasium)で任意のファミコンゲームを動かす方法
スーパーマリオのGym環境がどのような構成かというとnes-pyでファミコンをエミュレーションして、その出力画像を強化学習の観測できる環境として入力に渡す。
強化学習の報酬はエミュレーションのRAMの内容からプレイヤーの位置や生死状態を判定して、強化学習フレームワークGymで動くように状態や報酬を返すということをしている。
これをパックマンに置き換えるにはエミュレーションの部分はそのまま使えるが、報酬を決めるためのRAMのどのアドレスになんの情報を格納しているのかを調べる必要がある。
あとリセット状態からタイトル画面を飛ばしてゲーム画面から学習を始めるようにするためにスタートボタンを押すという処理も書く必要がある。
ファミコン版パックマンのRAM内容
ではということでマシン語の解析をやっていく。 ファミコンはMOS 6502というCPUが使われているので、そのマシンコードとしてバイナリファイルを逆アセンブルしてやる。
NMIベクトル($fffa)からの毎フレーム行われる処理を追ったり、パッドの入力(メモリ$4016の読み出し)から自機の動作を特定したり、BGの書き換え($2000〜)からスコア情報を調べたり地道に追っていく。
調べた結果RAMの内容をある程度特定した:
| RAM | 内容 |
|---|---|
| $001a | プレイヤーX座標(スプライト座標は-12) $001bが小数部 |
| $001c | プレイヤーY座標(スプライト座標は+3) $001dが小数部 |
| $001e~$002d | モンスター座標(各4バイト:0,1=X、2,3=Y) |
| $003f | ゲーム全体の状態(0,2=タイトル、4=ゲーム、6=敵を食べてる、8=プレイヤー死亡、10=ゲームオーバー、12=ステージクリア) |
| $0050 | 移動方向(0=上、1=左、2=下、3=右) |
| $0067 | 残機数 |
| $0068 | ステージ番号 |
| $006a | 残りクッキーの数(初期値:192) |
| $0070~$0075 | スコア(各桁0~9、下位から) |
RAMの内容がわかればあとはフレームごとに内容を読み出して報酬判定に利用するコードを書いてやればよい。
強化学習
パックマン報酬関数
エージェントを希望の行動に導くためには強化学習の報酬をどうするか。 スーパーマリオでは右移動量+タイムペナルティ+死亡ペナルティとなっていた (ただしカスタマイズで置き換えている)。 それを元に以下のようにしてみた:
- スコア報酬:スコア増分(ただしそのままでは問題があったので多少調整)
- エサを取り尽くして欲しいので
50に上げ、逆にパワーエサはピンチに追い込まれてから食べて欲しいので-300のペナルティに
- エサを取り尽くして欲しいので
- タイムペナルティ:毎フレーム
-1 - 死亡ペナルティ:
-500 - ステージクリアボーナス:
+1000
パワーエサを食べて無敵状態になると敵を連続して捕獲するごとにスコアが200点から倍々で最大1600点になるが、nes-pyではreward_rangeによって報酬が制限されるらしく、-1000〜+1000の範囲としてみた。
- ステージクリア優先というよりかは敵捕獲優先になりそうではあるが、パワーエサは4つしかなく何度も行えないので問題ないだろうと考えた
ネットワーク構成
nes-pyでエミュレーションした結果の画像情報256x240x3を強化学習に食わせる際に、どのような前処理を施すかや、ネットワーク構成にするかには自由度がある。 元のスーパーマリオでは前処理で84x84に縮小、モノクロ化して、CNNで畳み込んでいた。
パックマンでは色を保持した方が認識しやすいんじゃなかろうかという推測からモノクロ化は適用しないようにした。 また画面の右部分はスコア表示などで直接ゲームには関係ないのでクロップしてみた。

- CNNは3段で特徴数は全て
64- プーリング層はなくて、畳み込みでだいたい縦横1/4→1/2→1/2
- これはStable Baselines 3の
CnnPolicyのデフォルトっぽい
- 活性化関数の前にバッチ正規化を入れてみた
- 活性化関数:
ReLU - 全結合の特徴数は
512 - スキップフレーム4、フレームスタック4段
学習結果:❌
強化学習のコードは前回と同様にStable Baselines3で、アルゴリズムは一番性能がよさげだったPPOを使ってみた。 が結果としては300万ステップ学習させてもステージクリアには至らず、エサを半分ほど食べるくらいにしかならなかった。
- 不可解なのは敵から逃げるという行動をまったく学んでないように見える
- 報酬からスコアを除いたり割引率ガンマを減らしたりしてみたが効果はなかった
- そもそもエージェントが観測できるのは画面の映像情報だけなので、自分が操作してるのはどれとか取った行動がどう影響したかとか認識できてるのかも不明だし…
- そのくせパワーエサを食べることだけは一丁前に学ぶのはなぜなのか
- パワーエサにペナルティを与えたところ一応すぐには食べなくなる
- 学習が進むとステージ開始後の行動がほとんど固定されてしまうのがいまいち面白味がない…
試したこと
- 隠れ層のニューロン数を増やしたり、段数を増やしたり
- 入力画像をカラーじゃなくモノクロにしてみたり
- ネットワークの最後、全結合の後に
ReLUを適用しているのが怪しい?か疑った、が別にこれでいいっぽい? - 単にスコア増加を報酬にすると敵を食べることを優先してしまうのかも?と思ってエサのみにしても変わらず
- 死亡ペナルティで
-500とか与えたのが極端で悪影響か?と減らしたり0にしたりしてみたがダメ - タイムペナルティじゃなくて、長く生き延びる方がマシなんじゃないかと小さな報酬にしてみてもダメ
- スキップフレーム数が多いと繊細な判断ができないのかもと思って4から2に減らしてみたり
- スーパーマリオのような横スクロールとなにが違うかと考えたときに画面中でのプレイヤー位置が固定してたら認識しやすいんじゃないかと思いプレイヤーを中心に画像を加工して与えてみたが改善せず
- 活性化関数:
ReLUをLeakyReLUやSiLU(Swish)、またFReLUに変えてみたが結果は悪化 - バッチ正規化:あまり効果感じず
- Pointwise convolution、Global Average Pooling
グラディウスも試してみた
パックマンさえクリアできないようでは歯がたたないだろうとは思うが、煮詰まってたこともありヒントになることもあるかもしれないと思いグラディウスも動かしてみることにした。

グラディウスはいわゆる横スクロールシューティングゲームで、可能な行動は移動が上下左右斜めありの8方向と、ショットやパワーアップのボタン操作も組み合わせるとかなりの数になるのが強化学習としてはネックだろう。
グラディウスのRAM内容
解析結果から以下の通り:
| RAM | 内容 |
|---|---|
| $0000 | ゲーム全体の状態(0,1=タイトル、2=デモ、3=ゲーム開始、5=ゲーム中) |
| $0019 | ステージ番号 |
| $0020 | 残機数 |
| $0040 | 自機スピード |
| $0041 | ミサイル有無 |
| $0042 | 所持カプセル数 |
| $0044 | ショット種別(0=ノーマル、1=レーザー、2=ダブル) |
| $0045 | オプションの数 |
| $0046 | バリア耐久度 |
| $0100 | プレイヤーの状態(1=通常、2=死亡) |
| $0320 | プレイヤーY座標 |
| $0360 | プレイヤーX座標 |
| $07a0~$07b7 | オプションX座標(リングバッファ:サイズ=24、($0160のオプションカウンタ-オプション番号✖️11) % 24) |
| $07c0~$07d7 | オプションY座標(同上) |
| $07e4~$07e6 | 1Pスコア(BCD表現、下位から、値は1/10) |
グラディウスの強化学習での課題点
グラディウスはスーパーマリオやパックマンと違う課題があるように思う。 それは観測できる状態として得られる画面のピクセル情報からは自分の現在のパワーアップ状態が(直接は)把握できない。 ということからして、「スピードは2個までに抑えておこう」とか「オプションやレーザーやミサイルをつけた方が有利」なんてことを学べると思えない。
また行動としてショット撃つ・撃たないや、パワーアップする・しないを掛け合わせると行動数が膨れ上がって大変なことになる。 行動の種類が多いと学習が難しそうなので、ショットの連射とパワーアップの選択は自動で行うようにして、移動方向のみを学習させるようにしてみた。
結果
言わずもがな、ステージ1の地形の最初のあたりで敵の弾にやられる。 長く学習させても敵や弾を避ける方向に動いてるようには見えず、ランダム動作と大差ない…。
今後のステップ
強化学習のアルゴリズムの動作の理解も自作もできてない現状、手詰まり…。
- まずは敵から逃げることを学ばないことには始まらない
- CNNがちゃんと自分や敵を認識してるのか、注目する箇所を可視化してみる
- 認識しやすいように敵を1色にしてみる?
- プレイヤーとエサとパワーエサが同じ色なのを変えてみる?
- DQNでAtariのMs.Pacmanを学習してるはずなのでどうやってるのか見てみる
リンク
- リポジトリ:
- パックマン:pacman-rl-sb3
- グラディウス:gradius-rl-sb3
- 自作ファミコンエミュレータ:実行中のRAM内容表示機能付き
- TypeScriptで書いた6502逆アセンブラ アドレスにラベル名を定義できる
- ファミコン版のパックマン取扱説明書
- 過去記事:
- スーパーマリオの強化学習を動かす(Stable Baselines 3) 強化学習、Stable Baselines 3について
- 強化学習に再挑戦(二重倒立振子) Pythonの環境整備について