強化学習で目指すF-ZERO王者（前編）


# できたもの（途中経過）
F-ZEROのコースをsegmentationする深層学習モデルをJetson Nano上で動かしています．480x288，30FPSで処理できています．
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Realtime &quot;F-ZERO&quot; course segmentation model is running on Jetson Nano. It is part of &quot;gaming AI making challenge&quot; as my personal work.<a href="https://twitter.com/hashtag/jetson?src=hash&amp;ref_src=twsrc%5Etfw">#jetson</a> <a href="https://twitter.com/hashtag/nvidia?src=hash&amp;ref_src=twsrc%5Etfw">#nvidia</a> <a href="https://t.co/AYCqE75JbG">pic.twitter.com/AYCqE75JbG</a></p>&mdash; nobu_e753 (@nobu_e753) <a href="https://twitter.com/nobu_e753/status/1177626936878628864?ref_src=twsrc%5Etfw">September 27, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

キャプチャしたものはこちら（キャプチャの過程で負荷がかかり，レートが落ちています）
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Realtime &quot;F-ZERO&quot; course segmentation model is running on Jetson Nano (captured movie)<a href="https://twitter.com/hashtag/jetson?src=hash&amp;ref_src=twsrc%5Etfw">#jetson</a> <a href="https://twitter.com/hashtag/nvidia?src=hash&amp;ref_src=twsrc%5Etfw">#nvidia</a> <a href="https://t.co/gnn79ys9Oa">pic.twitter.com/gnn79ys9Oa</a></p>&mdash; nobu_e753 (@nobu_e753) <a href="https://twitter.com/nobu_e753/status/1179014753139609600?ref_src=twsrc%5Etfw">October 1, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# はじめに
強化学習と聞くと真っ先にイメージするのがゲームの自動プレイです．ちょっと試してみたいな．．と思い論文や既存の実装を見るも，どうも馴染みのないゲームが題材にされていることが多いようでした．ROM入手にかかわることが大きいのでしょう．

そこで，**どうせ勉強を兼ねて試すなら"自分が遊んだ・好きなゲームで試してみたいよね"**ということで，そういった**システムをどう構築するか？から取り組んでみました**．

この記事はその前編で，システムの構築までを取り扱います．

<!-- [名古屋CV・PRML勉強会](https://www.slideshare.net/NobuhiroDOI1/20190928-cvpr-nagoya)にて発表した資料も公開していますので参考にどうぞ．-->

# 1. ゲームの選定

以前から格闘ゲームのハックはしてきたので今回もと思ったのですが，キャラクターが２人いたり動きに自由度があり，難易度高そうな感ぷんぷんでした．初トライで高難度は勘弁...ということで，

- レース or シューティング（一方方向にしか動かないゲームが解析しやすいと聞いたので）
- 背景がシンプルで，数値等の読み取りがしやすい
- 自分が遊んだことがある**（モチベーション維持のため最重要！）**

という条件をもとに考えた結果**，[F-ZERO](https://ja.wikipedia.org/wiki/F-ZERO)**（初代，1990年，スーパーファミコン）にしました．ロケットスタートとか，ジャンプ台でのショートカットとか，それなりに遊んでいた記憶があります．

なお，強化学習 x F-ZEROについてはいくつも先行事例があり，タイトルに"F-ZERO"とはいった論文（[Racing F-Zero with Imitation Learning, 2017](http://cs229.stanford.edu/proj2017/final-reports/5243706.pdf)）や，まんまなチュートリアル動画"[FZERO AI - PPO2 and A2C - OpenAI and Stable-baslines](https://www.youtube.com/playlist?list=PLTWFMbPFsvz2LIR7thpuU738FcRQbR_8I)"は見ているだけでも面白いです．

# 2. 全体構成
**"自分の好きなゲームで強化学習をしたい"**というのが今回のコンセプトなので，**ゲーム機実機を組み込んだ構成**としました．またどうせやるなら，ということで計算機にはJetson Nanoを充ててみました．学習はともかく推論ができれば，"Jetson Nano限定AIゲーム大会"なんてのも夢ではないかもしれません（是非やりたいです）．なお[ソニックは大会があったみたい](https://contest.openai.com/2018-1/)ですね．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156705/1e49b241-d006-7d3c-f203-ef2c352dd7ac.png)

## 使用した機材
- [Jetson Nano](https://www.nvidia.com/ja-jp/autonomous-machines/embedded-systems/jetson-nano/) (ML computer)
- [Retro freak](https://www.cybergadget.co.jp/retrofreak/) (Game console)
- [ezcap 261](http://www.ezcap.com/article_read_421.html) (Video capture)
- [GameControllerizer](https://sites.google.com/view/gamecontrollerizer) (Game controll)

ML computerについては，当初Edge TPUを使う予定でしたが，量子化手順がめんどくさすぎるのでパスしました．Video captureについてはezcap 261を選択．この機種はUVC(USB Video Class)＆Linuxに対応していることが明記されており，かつ安価であることから選びました．
キャプチャユニットをJetson Nanoに接続し`cv2.VideoCapture()`をたたくだけで，だらだらとゲーム画面がやってきます．

```python
import cv2

cap = cv2.VideoCapture(-1) # 0はカメラのデバイス番号
cap.set(cv2.CAP_PROP_FPS, 30)           # カメラFPSを30FPSに設定
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) # カメラ画像の横幅を1280に設定
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) # カメラ画像の縦幅を720に設定

ret, frame = cap.read() # 画像を取得
```

ゲーム機実機を使った場合「学習が一生終わらない」なんてことになるかもしれませんが，そうなったらその時に考えることにします．

# 3. 情報の抽出

とっかかりはゲーム画面からの情報の抽出です．ゲーム強化学習で一般的なフレームワークGym retroと異なり，システムをゼロから組んだ場合はこれが第一の壁になります．必要な情報としては

- Power
- Speed
- Time (option)
- Course (option)

位と考えました．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156705/af44316e-2574-93f2-ad7b-63452176d9aa.png)

まず初めに各情報の位置をメモしておき，それぞれ取り掛かることにしました．
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156705/64da5544-d9b9-21fd-91f0-c1256a5aad10.png" width=640> 

## 3.1 Power
最初は「赤色部分の長さ」で判別しようとしましたが，Powerゲージが25%を切ると点滅し赤⇔グレーを繰り返すのでうまくいきませんでした．そののち，ゲージ部分を観察し右側に黒線があることに気づいたので，**X方向で輝度最低のPixel位置**を求めるような判定ルーチンを組みました．

## 3.2 Speed
これも最初は**適当にテンプレートマッチング（SSD,SAD）すればいけるんじゃね？と思ってやったもののダメでした**．数字が微妙に透過しているせいで背景の影響を受け精度が散々でした．

仕方ないので小型のCNNで判別することにしました．28x24サイズで10クラスなのでMNISTとほぼ同等です．

```python:モデル構築部

DIGIT_SIZE = [24, 28, 1]

def __build_model(lr=1e-3):
    # functional
    i = k.layers.Input(DIGIT_SIZE, name="input_0")
    x = k.layers.Conv2D(8, (3,3), activation="relu", padding="valid")(i)
    x = k.layers.MaxPool2D()(x)
    x = k.layers.Flatten()(x)
    x = k.layers.Dense(10, activation="softmax", name="output")(x)
    model = k.models.Model(inputs=i, outputs=x)
    model.summary()
    model.compile(loss='binary_crossentropy',
                  optimizer=k.optimizers.Adam(lr=lr, decay=1e-5),
                  metrics=['accuracy'])
    return model
```

学習データはゲーム画面からクロップして仕訳けました．計685枚．
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156705/6abaa767-8a4f-c6e9-b157-73ef5a6e7110.png" width=480>)

学習部は`tf.keras`のお作法そのままです．1点だけ，収集した事例数が結構偏っていたので，この差を緩和するために`labels_weight`で事例重みを渡してやっています．問題が簡単なせいか，すぐに`Accuracy = 1.0`に．

```python:学習部
# Found 685 images belonging to 10 classes.
# Counter({0: 154, 3: 99, 1: 76, 2: 67, 6: 55, 5: 53, 7: 51, 4: 48, 8: 45, 9: 37})
model = __build_model()
model.fit_generator(
    batch_generator,
    epochs=10,
    class_weight=labels_weight,
    verbose=1)
```

両者の結果をOverlayで表示したものが以下です．Jetson Nano上でもリアルタイムで楽々動きます．
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Speed &amp; power value extraction from F-ZERO screen. The image processing program is running on JetsonNano. For speed value extraction, very small CNN model is used.<a href="https://twitter.com/hashtag/jetson?src=hash&amp;ref_src=twsrc%5Etfw">#jetson</a> <a href="https://twitter.com/hashtag/nvidia?src=hash&amp;ref_src=twsrc%5Etfw">#nvidia</a> <a href="https://t.co/wVJtNCy3th">pic.twitter.com/wVJtNCy3th</a></p>&mdash; nobu_e753 (@nobu_e753) <a href="https://twitter.com/nobu_e753/status/1179007135369162758?ref_src=twsrc%5Etfw">October 1, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 3.3 Time (option)
ひとしきり考えた末，スキップすることにしました．

[より短い`Time`] := [高い`Speed`] かつ [`Power`が減っていない]

と読み替えていいのではと．サボりですw

## 3.4 Course (option)
シンプルな強化学習であればコース情報は強化学習中に自動で獲得してゆくものでしょう．ただ，**あらかじめコースを抽出できるモデルを組んでおけば，学習時間を短縮できるのでは？**という思いからトライしてみました．また**Jetson Nano上でSegmentationタスクを動かしたとき，どのくらいのパフォーマンスを出せそうなのか？**ということにも興味がありました．

### モデル検討
とにかく早いモデルにしておこう，ということで，Segmentationデータセットとして有名な[Cityscape](https://www.cityscapes-dataset.com/benchmarks/)のベンチマークで最速のモデルを探してきました．

- Fast-SCNN: Fast Semantic Segmentation Network, 2019
- https://arxiv.org/abs/1902.04502

1024 x 512 処理時に285 FPSとのことなので期待が持てます（実際はさらにchannel数を半分に切りつめ，入力画像サイズ480 x 288で利用しました）．ありがたいことに[Fast-SCNNのTensorflowでの解説付き実装](https://medium.com/deep-learning-journals/fast-scnn-explained-and-implemented-using-tensorflow-2-0-6bd17c17a49e)も公開されていました．

### 学習データ
こればっかりはどうしよもないので気合で対応，１面（Mute city），２面（Big blue）からそれぞれ75 frameずつ，計150 frameに対して，アノテーション（コース，磁力帯）を作成しました．作成には，ブラウザ上で動くフリーの教師作成ツール[AnnoFab](https://annofab.com/)を使いました．作成時間は1時間強でした．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156705/c0afcacf-d053-56b2-3938-a4394718b958.png)


### 学習＆推論
磁力帯を無視し，ひとまずコースだけをsegmentationする学習フローを構築．学習は5000ステップ（適当），RTX 2080i上で18分かかりました．静止画に対する適用結果を確認すると，えいやで作ったモデルのわりに推論結果もまあまあです．
唯一，自車がコース端に激突しエフェクトが走るとsegmentation結果が甘くなりますが，これは学習データ追加でカバーできるでしょう．

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156705/57f745ff-32a1-f82a-6132-9bff3de9e60e.jpeg" width=480>

### 速度がでない！
モデルが出来あがりウキウキでJetson Nanoにマッピング，動画を処理してみるも速度が出ません！カクカクです！！ 調べてみると

**オーバーレイなどの事前事後処理が遅い**

でした．segmentation部分を視覚化するためにマスクを作ったりオーバーレイをしたりという事後処理を`OpenCV`+`numpy`で書いていたのですが，この処理だけで50-60msかかっていました．逆にCNNの部分は30msec以下でした．

**Edge deviceの場合，CNNについてはGPUというアクセラレータがついているのに対して，CPUは貧弱なので**こういう結果を生んだと考えられます．本件に限らずEdge deviceを利用する場合は注意が必要そうです．

### 高速化（並列化，**TF-TRT**）
このままでは学習に影響が出そうですし，なにより悔しいので高速化してみました．

#### 並列化
Pythonの`multiprocessing.Process`を使って，処理を

- 前処理（キャプチャ，縮小）
- Segmentation
- 後処理（オーバーレイ，表示）

の3つに分割，パイプラインで実行できるようにしました．
Jetson上でGPUの使用率を確認するツール[jtop](https://github.com/rbonghi/jetson_stats)で状況を確認してみると，使用率が如実に改善されていました．

#### TF-TRT（TensorFlow with TensorRT optimization）
割と使えそうなのがこちらです．Jetson Nano上でCNNモデルを動作させる場合，

1. Tensorflowモデルのまま，Tensorflow runtime上で動かす
2. TensorRTで最適化したモデルを，TensorRT runtime上で動かす

があります．もちろん2のほうが高効率ですが**モデル内にTensorRTサポート外オペレーションを含んでいると最適化ができません**．例えば今回のモデルには`ResizeBilinear`オペレーションが含まれており，これがTensorRT(5.X)非サポートでした．

しかしドキュメントをよく読むと，これ以外に第3の選択肢があり**「TensorRTで対応しているオペレーションだけ最適化を行いこれはTensorRT runtime上で動かす．その他はTensorflowモデルのままTensorflow runtime上で動かす」**ハイブリッドな方法が存在します（[公式ページ](https://docs.nvidia.com/deeplearning/frameworks/tf-trt-user-guide/index.html)）

導入方法も簡単で，Tensorflowの推論グラフ(.pb)を読み込んだ後に，数行付け加えるだけです．導入後に推論部のみの速度を計測したところ 24ms → 15ms と大きな改善がみられました．

```python:TF-TRT導入
# TF-TRT導入に必要
from tensorflow.python.compiler.tensorrt import trt_convert as trt
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction = 0.65)

...

graph = tf.Graph()
with graph.as_default():
    model_file = open(pb, 'rb')
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(model_file.read())

    #############################################
    # TF-TRTむけの追加部分
    converter = trt.TrtGraphConverter(
        input_graph_def=graph_def,
        # precision_mode="FP16",  # 演算モードも選べる
        nodes_blacklist=[OUTPUT_NODE])
    graph_def = converter.convert()
    #############################################

    tf.import_graph_def(graph_def, name="")
```

最適化前後のsegmentation速度です．なんとか30 fps達成です．これで冒頭のようなリアルタイムsegmentation画面を表示することができました．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156705/cd196d1b-1865-0a43-d425-912f9d818d53.png)

# まとめ
"自分の好きなゲームで強化学習をしたい"というコンセプトをもとに，これを可能にするシステムを構築してみました．あわせて，計算機部分にEdge device（Jetson Nano）を試用してみたことで，その特性を知ることができました．

特にJetson Nano上でリアルタイム処理を実現しようとする場合，

- GPUと比較してCPUが貧弱なので，CNN以外の処理負荷にも注意
- TensorRTが適用できない場合にも，TF-TRTがある

の２点は大変よい知見となりました．

足回りができたので，いよいよ本題の強化学習です．自動プレイでMute cityを１周出来るのがいつになるかはわかりませんが，本業の合間にのんびりやっていきます．

**挑戦者待ってます！**