FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

危険!AIに奪われる遊び 〜本当にゲームはヤバいのか検証する〜

こんにちは。エンジニアの菅野です。

みなさんはAIと聞いてどういうものを思い浮かべますか?
あまり自分とは関わりが無いなと思う方もいるかも知れません。

でも写真をいい感じに補正する写真アプリや部屋をいい感じの状態にしてくれるエアコン等があり、知らず知らずの間に既にお世話になっていたりするものです。
仕事においてもExcelにはいい感じにグラフを作る機能があったりして、確実にAIは活用できる段階になっていると思います。

もしAIが人の仕事を奪ってやってくれるなら、遊ぶのもAIに任せたいと思いませんか?

思いますよね!
やってみましょう。

ゲームをAIにやらせて時短

ちょうどSteamでやらないまま積み上げてるゲームがいくつかあるので、そいつをAIにやらせればプレイする手間が省ける気がしたので早速AIにゲームをやらせようと思います。

ゲーム×機械学習だと強化学習でやるのが一般的です。
多数の試行錯誤を重ねてマリオをクリアできるようになったり、囲碁のAlphaGoも強化学習で人間を上回る強さになっています。

でも今回は雑にゲームのプレイ画面を見せて、それを模倣して同じようにプレイしてくれたらなと思うので、単純な教師あり学習で実験してみたいと思います。
強化学習だとスコアリングのために各種パラメータを取得しなきゃいけないし(市販のゲームのメモリ解析は無理)、多数の試行をするために並列化必須(たくさんゲーミングPC買わないと)なので…

積んであるゲームの中からAIにやってもらうゲームは…

F1 2018です。

早速プレイしてもらいましょう。

まずはお手本

教師あり学習なので、AIにやってもらうためにはお手本となるデータが必要です。
とりあえずプレイします(結局自分でやるのか…)

教師データの作成ですが、以前に私が作成した記事で使っていた表示用ScalaFXアプリを改造してデータ作成アプリを作成しました。 labs.septeni.co.jp

画面キャプチャとXboxコントローラの入力のキャプチャを行って「連番_コントローラ入力値.jpg」みたいなファイル名で0.05秒ごとに画像保存します。
あとでOne-Hot表現にしやすいようにラベル値(コントローラ入力値)はXboxコントローラの各ボタン入力のビットの合計を10進数にした値をラベル値としました。

早速データを取ります。
場所はモナコモンテカルロ市街地サーキットにしてみました。他のマシンに邪魔されないようにタイムアタックモードでやります。

データ作成アプリ上ではコントローラの入力とキャプチャした224×224の画像を表示しています。
最終的に行うAIからゲームへの入力はキーボード入力を使って行う関係上、Xboxコントローラもアナログ部分じゃなくて二値で表現できる十字キーとボタンで操作しています。
ただでさえ難しいゲームがより難しい…。アクセルがONかOFFなのでマシンが暴れる。

そんなこんなで、訓練用に1時間、検証用に30分、テスト用に30分走行したデータを採取しました。

AIに学習してもらう

次に作成したデータを使って学習していきます。
ディープラーニングで画像から推論できるようにしたいと思います。

使うフレームワークですが、前の記事だとDeeplearning4jを使ってましたが情報や資産が少なく辛かった記憶があるので、一番メジャーなTensorFlowで行きます。
最終的なアプリはまたScalaで作るのですが、TensorFlowには様々な言語のAPIがあるため推論部分はScalaアプリにも組み込むことができます!
でも訓練はできないのでPythonでやります。

Pythonの環境を構築して、TensorFlowインストールして、GPUを使えるようにして、そして嵌って…という苦行を行うのも良いですが(私は絶対やらない)、Colaboratoryという無料で使えてGPU機械学習ができるJupyterノートブック環境があるのでこれを使ってサクッと学習してみます。 colab.research.google.com

TensorFlowを使うと言いましたが、実際にはもっと抽象化したフレームワークのKerasを使います。
TensorFlowは低レイヤ過ぎて学習に必要な機能が少なくてとても苦労します。 keras.io

Kerasでは多数の画像分類用の完成されたモデルが読み込めるようになっていて、とても簡単に画像分類を始めることができます!

model = applications.MobileNetV2(input_shape=(224, 224, 3), weights='imagenet')

その中から今回はMobileNetを使います。MobileNetを選択した理由は、あとでもっと計算量が増えることをするのでなるべく軽量なモデルが良かったからです。

まずはF1の運転を行うモデルを作ります。
あとでネットワークの継ぎ接ぎをするのでやりやすいようにしています。
アクセル、ブレーキのON/OFFとステアリングの向きで12通りの組み合わせがあるので出力層のノード数は12個です。
Fine Tuning出来るかと思って一応ImageNetの重みを読み込んでいますが、今回のデータは学習済みデータと同じように前処理していないのでもしかしたら意味ないかも。

  class_num = 12
  model_input = Input(shape=(224, 224, 3), name='input', dtype='float32')
  base_model = applications.MobileNetV2(input_shape=(224, 224, 3), alpha=1.0, pooling='avg', include_top=False, weights='imagenet')
  net = base_model(model_input)
  logits = Dense(class_num, activation='softmax', name='logits')(net)
  model = Model(inputs=model_input, outputs=logits)

とりあえず少しだけ訓練を行って、試しに動かしてみます。
データ作成アプリを改造して推論したコントローラ操作を表示するようにしました。

運転操作は人が行っていて、アプリに表示されているコントロール表示がモデルの推論結果です。
リアルタイムに推論することは出来ていて、なんとなくうまく行ってる気がします。
あとは推論などに時間がかかって操作が遅れ気味なので、3フレーム先でどういう操作をすればよいかを予測するように学習の方針を改めます。
あと、この時点でやっとゲームとTensorFlowでGPUのリソースの取り合いが発生してどちらもまともに動かないということに気づいてしまいました…。
でもゲームのグラフィックオプションを出来る限り下げることでなんとか凌ぎました。

Epoch 10/500
1200/1200 [==============================] - 928s 773ms/step - loss: 0.9279 - acc: 0.8070 - val_loss: 0.7905 - val_acc: 0.6796
Epoch 11/500
1200/1200 [==============================] - 923s 769ms/step - loss: 0.8353 - acc: 0.8240 - val_loss: 0.7991 - val_acc: 0.6741
Epoch 12/500
1200/1200 [==============================] - 922s 769ms/step - loss: 0.7519 - acc: 0.8376 - val_loss: 0.7868 - val_acc: 0.6819
Epoch 13/500
1200/1200 [==============================] - 933s 778ms/step - loss: 0.6768 - acc: 0.8522 - val_loss: 0.7989 - val_acc: 0.6850
Epoch 14/500
1200/1200 [==============================] - 933s 777ms/step - loss: 0.6091 - acc: 0.8659 - val_loss: 0.8133 - val_acc: 0.6837
Epoch 15/500
1200/1200 [==============================] - 922s 768ms/step - loss: 0.5479 - acc: 0.8797 - val_loss: 0.8228 - val_acc: 0.6792
Epoch 16/500
1200/1200 [==============================] - 923s 769ms/step - loss: 0.4931 - acc: 0.8919 - val_loss: 0.8293 - val_acc: 0.6865
Epoch 17/500
1200/1200 [==============================] - 924s 770ms/step - loss: 0.4413 - acc: 0.9038 - val_loss: 0.8423 - val_acc: 0.6862

Epoch 00017: ReduceLROnPlateau reducing learning rate to 4.999999873689376e-06.
Epoch 18/500
1200/1200 [==============================] - 924s 770ms/step - loss: 0.3802 - acc: 0.9201 - val_loss: 0.8415 - val_acc: 0.6879
Epoch 19/500
1200/1200 [==============================] - 922s 768ms/step - loss: 0.3698 - acc: 0.9223 - val_loss: 0.8434 - val_acc: 0.6883
Epoch 20/500
1200/1200 [==============================] - 918s 765ms/step - loss: 0.3642 - acc: 0.9237 - val_loss: 0.8460 - val_acc: 0.6873

気づいた点を反映して本格的に学習していきます。
上記が訓練の結果で、実際に動かした感じで19エポック目のモデルが良さそうだったのでこれで学習完了とします。
正解率68%はとても危険な感じですが、これ以上上げられる気配はありませんでした。
原因はミスの多い人間がお手本データを作っているので、そもそも訓練データ自体が正解データでは無いところに根本的な問題を感じています。
この時点でゲームは強化学習1択かなと思い始めました…。AlphaGo Zeroなんて過去の対局データ使わずに自己対局だけで世界最強になる始末。

学習完了したモデルで実際に動かしてみました。操作は人間で、アプリ上に推論結果を出しています。

ステアリング操作はとても怪しいのですが、アクセル・ブレーキはかなりいい線いってるのではないでしょうか!

運転は点ではなく線だと思う

運転免許の危険予測問題で、「右カーブを走行しています。どのようなことに注意して運転しますか。」の答えが「オーバーステアでリアが流れ始めたので、軽くアクセルを入れながらカウンターを当てて脱出する。」だったらどうでしょう?
一枚絵でスピードやタイヤの限界なんてわからないですよね。
先程の学習モデルでも同じことが言えると思うのでどうにかしてみたいと思います。

  model_input = Input(shape=(None, 224, 224, 3), name='input', dtype='float32')
  
  net = TimeDistributed(mobilenet_features)(model_input)
  
  net = Conv1D(226, 3, padding='same', kernel_initializer='he_normal')(net)
  net = BatchNormalization(axis=2)(net)
  net = Activation('relu')(net)
  net = GlobalMaxPooling1D()(net)

  logits = Dense(class_num, activation='softmax', name='logits')(net)
  
  model = Model(inputs=model_input, outputs=logits)

どうにかしてみたモデルです。
画像を推論するモデルが計算した特徴量ベクトルを時系列で複数作って適当にまとめりゃいいのでは?時間方向で畳み込むことで動きを学習できるのでは?と思って作ってみました。
KerasにはTimeDistributedというラッパーがあるので、すごく簡単に時系列データの1フレーム分の処理が行なえます!
最終的に15枚の静止画(0.75秒分の動画)を学習するように作ってみました。

自分でネットワークを作るとハイパーパラメータの設定値に悩みますが、optunaというシンプルで使いやすい最適化ツールがあるので使ってみました。 optuna.org

  def objective(trial):
    model_input = Input(shape=(None, 1280), name='input', dtype='float32')

    net = TimeDistributed(Lambda(lambda x: x))(model_input)
    
    conv1_filters = trial.suggest_int('conv1_filters', 128, 384)
    conv1_size = trial.suggest_int('conv1_size', 1, 3)
    conv1_padding = trial.suggest_categorical('conv1_padding', ['same', 'causal'])

    net = Conv1D(conv1_filters, conv1_size, padding=conv1_padding, kernel_initializer='he_normal')(net)
    net = BatchNormalization(axis=2)(net)
    net = Activation('relu')(net)
    net = GlobalMaxPooling1D()(net)

    logits = Dense(class_num, activation='softmax', name='logits')(net)

    model = Model(inputs=model_input, outputs=logits)
    
    model.compile(loss='categorical_crossentropy',
              optimizer=Adam(lr=1e-3, beta_1=0.9, beta_2=0.999),
              metrics=['accuracy'])

    print(trial.params)
    epochs = 20
    early_stopping = EarlyStopping(monitor='val_loss', patience=4, verbose=1)
    
    hist = model.fit_generator(
        generator=train_feature,
        epochs=epochs,
        validation_data=val_feature,
        class_weight=class_weight,
        callbacks=[early_stopping],
        verbose=2
      )
    
    best_val = min(hist.history['val_loss'])
    return best_val

  study = optuna.create_study(direction='minimize')
  study.optimize(objective, n_trials=150)

  print('best_params: %s' % study.best_params)
  print('best_value: %s' % study.best_value)

最適な値を探索するのはoptunaに任せることが出来ます。
待ってる間に新しいゲームでも買いましょう。(やるとは言ってない)

best_params: {'conv1_filters': 226, 'conv1_size': 3, 'conv1_padding': 'same'}
best_value: 0.6584836531015238

最終的に上の値で行くことになりました。
その値で学習した結果が以下です。

Epoch 1/10
72/72 [==============================] - 11s 146ms/step - loss: 3.4031 - acc: 0.6117 - val_loss: 0.8609 - val_acc: 0.6480
Epoch 2/10
72/72 [==============================] - 10s 141ms/step - loss: 1.6484 - acc: 0.7235 - val_loss: 0.7666 - val_acc: 0.6954
Epoch 3/10
72/72 [==============================] - 9s 132ms/step - loss: 1.3463 - acc: 0.7546 - val_loss: 0.7316 - val_acc: 0.6902
Epoch 4/10
72/72 [==============================] - 9s 129ms/step - loss: 1.1618 - acc: 0.7792 - val_loss: 0.8332 - val_acc: 0.6584
Epoch 5/10
72/72 [==============================] - 9s 127ms/step - loss: 1.0175 - acc: 0.7927 - val_loss: 0.6604 - val_acc: 0.7293
Epoch 6/10
72/72 [==============================] - 10s 133ms/step - loss: 0.9307 - acc: 0.8035 - val_loss: 0.7070 - val_acc: 0.7190
Epoch 7/10
72/72 [==============================] - 9s 128ms/step - loss: 0.8766 - acc: 0.8126 - val_loss: 0.6942 - val_acc: 0.7176
Epoch 8/10
72/72 [==============================] - 9s 131ms/step - loss: 0.8658 - acc: 0.8214 - val_loss: 0.7376 - val_acc: 0.7172
Epoch 9/10
72/72 [==============================] - 10s 132ms/step - loss: 0.7491 - acc: 0.8372 - val_loss: 0.7044 - val_acc: 0.7245
Epoch 10/10
72/72 [==============================] - 10s 138ms/step - loss: 0.6961 - acc: 0.8474 - val_loss: 0.7734 - val_acc: 0.7060

正解率が70%を超えるようになりました!まだまだ低い値ですが。

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input (InputLayer)           (None, None, 224, 224, 3) 0         
_________________________________________________________________
time_distributed_1 (TimeDist (None, None, 1280)        2257984   
_________________________________________________________________
conv1d_1 (Conv1D)            (None, None, 226)         868066    
_________________________________________________________________
batch_normalization_1 (Batch (None, None, 226)         904       
_________________________________________________________________
activation_1 (Activation)    (None, None, 226)         0         
_________________________________________________________________
global_max_pooling1d_1 (Glob (None, 226)               0         
_________________________________________________________________
logits (Dense)               (None, 12)                2724      
=================================================================
Total params: 3,129,678
Trainable params: 3,095,114
Non-trainable params: 34,564
_________________________________________________________________
data_dir: train_data/data_3, data_total_count=24587, split_num=492
test_loss: 0.6521 - test_acc: 0.7279

最終的な精度をテストデータでチェックしました。テストデータでも72%なので確実に学習は出来ているっぽいです。

完成なのか?

では動かしてみましょう。

…?
なんか学習がおかしな方向へ行っている気がします。
精度の数値自体は上がってるのですが動かした感じの印象としては正直何だコレ。
もっとどうにか出来ると思って時系列データの処理の部分を層を増やしたりしてみたのですが、微妙に精度を向上出来たのみで人間が思うような推論が出来るようにはなりませんでした。

もっといろいろ試したくは思うのですが、ここまでで結構な時間が溶けているので一旦完成(?)としたいと思います。
嫌な予感しかしないのですが、念の為自動運転させてみました!
AIのヤンチャぶりをダイジェストでどうぞ!

  1. 開幕ドーナツターンでファンサービス
  2. ミニ四駆さながらの壁すり走行
  3. 掟破りの地元走りでシケインを大胆にカット
  4. 壁に激突からのどうして良いか分からなくなってFinish

おしまい

というわけで、今回は令和時代の新しいゲームの遊び方をご紹介しました。

万が一AIがゲームを上手くプレイできてしまうと、人間はゲームを買うだけで良くなりますし、あからさまなチート行為によりオンラインプレイで即BANされる等いろいろと捗るようになると思います!
ワクワクしますね!

ここまで読んでいただいてありがとうございます。
技術的詳細が知りたいという奇特な方のために今回得た知見を下にまとめておきますので良かったらどうぞ。

おまけ

この記事で作ったもののソースコードはこちら github.com

訓練データのキャプチャマシン
https://github.com/maji-KY/formula-AI/blob/master/src/main/scala/fai/LearningDataMaker.scala

学習を行うpythonコード
https://github.com/maji-KY/formula-AI/blob/master/Formula_AI_model_training.ipynb

学習済みモデルを読み込んでキーボードを操るAI
https://github.com/maji-KY/formula-AI/blob/master/src/main/scala/fai/FormulaAI.scala

Scala(Java)からTensorFlow使ってGPU上で推論できるようにする

Scalaならbuild.sbtに下記の3つの依存を追加すればGPU上でTensorFlowが使えるようになります。

libraryDependencies ++= Seq(
  "org.tensorflow" % "tensorflow" % tensorflowVersion,
  "org.tensorflow" % "libtensorflow" % tensorflowVersion, // enable gpu
  "org.tensorflow" % "libtensorflow_jni_gpu" % tensorflowVersion // enable gpu
)

簡単だと思いましたか?これとは別にちゃんと嵌りポイントが用意されているので安心してください!

まず、TensorFlowのバージョンと対応するCUDAのバージョンはきちんと確認しないといけません。
今回使ったTensorFlow1.13.1では対応するCUDAのバージョンは10.0です。10.1だと動きません。10.0が使える状態になってないといけないので、windowsの場合はnvidiaのサイトをくまなく探して過去バージョンを落としてこないといけません。

cuDNNも必要なので忘れずに。dllはパスが通っていればどこにおいてあっても問題なさそうです。

さらにwindowsでは嵌りポイントが追加されていて、Visual Studio 2015 Visual C++ 再頒布可能パッケージがインストールされていないとTensorFlowをJNIで動作させる部分が動きません。
もしVisual Studio 2017用がインストールされていてもそれでは動きません。更に困ったことに2015用と2017用は同時にインストールできない謎仕様のため2017用が入っている場合は削除する必要があります。

なんかよくわからないけど動かない場合は、-Xcheck:jni -XshowSettings:properties -Dorg.tensorflow.NativeLibrary.DEBUG=1のようなVMオプションやシステムプロパティを追加しましょう。
見てもよくわからない情報が出力されてデバッグが捗ります。

すべての罠をクリアするとやっとScalaからTensorFlowをGPU上で動作させることが出来るようになります。
ちなみにこの記事の動作環境はGeForce GTX1060です。以前はRadeonを使っていてCUDAが使えませんでしたが気づいたらグラボ交換していたのでGUDAで機械学習できるようになりました!

Scala(Java)で画面・コントローラキャプチャ、キーボード操作

JavaからXboxコントローラの入力を受け取るのにはjinputを使っています。windows用dllがクラスパス上に無いといけない以外は気をつけるべきポイントはありません。

キーボード操作にはjava.awt.Robotを使っています。標準で画面操作の自動化につかうためのクラスがあるので使ってみました。

画面キャプチャにはJNAを使っています。java.awt.Robotでもキャプチャ出来るのですが、Win32 APIを使ったほうが圧倒的に速いです。
あとウインドウ位置とサイズを取得するにはWin32 APIを使う以外ありません。
ネイティブAPIGPUの使用によって、画面キャプチャから15フレーム分の推論で80ミリ秒前後で完了するようになりました。これくらいだとリアルタイム感ありますね。CPUで推論すると15倍位の時間がかかります。

GPUリソースの取り合い

TensorFlowはデフォルトだとGPUのメモリを専有したがります。
なのでメモリ上限を指定するper_process_gpu_memory_fractionを設定しないと今回は不都合です。

Java APIでは
https://github.com/maji-KY/formula-AI/blob/master/src/main/scala/fai/Predictor.scala#L22
のように指定してSavedModelBundle.LoaderwithConfigProtoで突っ込めば設定できます。

紹介してなかったですが、TensorFlowのJava APIの使い方は以下のとおりです。
Kerasが作るモデルの入力には自動的にバッチサイズの次元が入るのでTensorFlow側で推論するときは明示的に値を指定してあげる必要があります。

private val model = SavedModelBundle.loader(modelPath.toFile.getAbsolutePath)
    .withTags("serve")
    .withConfigProto(config.toByteArray)
    .load()

private val session = model.session()
// ~略~
val input = Tensor.create(Array[Long](batchSize, frames.length, 224, 224, 3), videoFramesFloatBuf)
val result = session.runner().feed("input", input).fetch("predicted_label", 0).run()
val buf = LongBuffer.allocate(1)
result.get(0).writeTo(buf)
input.close()

Tensor.createしたものは明示的にcloseしないとメモリ解放されません。
sessionは作成するコストがかかるので使いまわします。

Kerasのドキュメント

Kerasのドキュメントは一見親切に見えますが、痒いところに手が届かないのでTensorFlowのkerasモジュールのドキュメントが役に立ちます。 www.tensorflow.org

TensorFlowのJava APIではモデルの読み込みはSavedModelである必要があります。
以下のようにすれば簡単にKerasのモデルをTensorFlowのSavedModelで保存する事ができます。

  model = load_model('model.h5', compile=False)

  session = backend.get_session()
  tf.compat.v1.saved_model.simple_save(
      session,
      './saved_model',
      inputs={'input': model.input},
      outputs={'output': model.output}
  )

注意点として、Kerasがゴミ(使い散らかした変数やグラフ)を放置している状態のときにSavedModelを保存するとそれらまで出力されてしまうので一度きれいな状態にしてから保存するのがおすすめです。

詰め込み教育の弊害

記事ではサラッと学習が完了しているように書いてますが、最初は全然学習が進まず途方に暮れていました。
原因は最初から静止画の特徴の抽出と時系列での特徴の抽出を組み合わせたモデルを一気に学習させようとしていたからでした。
どの部分が推論結果に影響するのかよくわからずに学習が進まない状態になっていたと考えられます。

解決策として、既に紹介した通りはじめに一枚ずつの静止画の分類として学習させます。
その後、静止画の推論モデルの出力層を取り外して、複数枚静止画の読み込み部分と一次元のCNN層と出力層をくっつけて学習すれば良いということになります。
当然学習済みの静止画の部分は重みを固定させなければいけません。Kerasでは対象のレイヤーをlayer.trainable = Falseとすれば重みが更新されません。

ただ、それだけだと学習時に大量の画像をGPUにロードすることになりColaboratoryで使えるメモリを使い果たしてクラッシュしてしまう、と言うかいくらメモリがあっても足りないのでこの記事では一工夫しています。
まず訓練データの静止画を学習した推論モデルの出力層の前の特徴量ベクトルの時点での値に変換して正解ラベル値とともにファイル保存します。
その特徴量データを使って一次元のCNN部分だけの学習をさせることによってメモリと実行時間を節約しています。
72000個のデータを1エポック10秒弱で学習することができました。

具体的な操作はソースコードを参照してください。
https://github.com/maji-KY/formula-AI/blob/master/Formula_AI_model_training.ipynb

鬼の直線番長

モデルを作り始めた頃、どんな画像を入力しても「ハンドル真っ直ぐでアクセル全開」の結果が返ってくるモデルしか出来上がらない状態でした。

こういう事が起こる理由は、訓練データに大きな偏りがあるからです。
その偏りというのは、正解ラベル毎のデータ数の偏りです。

サーキットというのは半分近くは直線なので「ハンドル真っ直ぐでアクセル全開」が正解のデータが多くを占めます。
そういうデータで学習を行うと直線のデータの学習回数が多くなってより多くの影響を受けるようになります。

今回の記事ではハンドルとアクセル・ブレーキの操作で回答の選択肢が12通りです。
仮に選択肢が12通りで正解に偏りがないマークシートの試験問題があったとして、一番左だけチェックを入れたときの正解率の期待値は約8.3%のはずです。
ところが正解の半分近くが一番右の選択肢だったらどうでしょう?適当に一番右だけチェックを入れれば半分近く正解できます!
そんな感じでとりあえず「ハンドル真っ直ぐでアクセル全開」と答えれば大体正解するため変な覚え方をしてしまったモデルが出来上がったのでした。

これを解決するには大きく、

  1. 訓練データを前処理する方法
  2. 少数派のデータの訓練時に不正解時のペナルティを大きくしてやる(損失関数にクラスごとに重みをつける)方法

があります。
今回は直線データが多くてもそれぞれに意味があると考え、間引いたりはせずに、損失関数にバイアスをかける2の方法で行きました。
Kerasだとfitメソッドのclass_weightで簡単に指定できます。

時系列データ=RNNではない

1フレーム毎の静止画の特徴量が時系列に並んでるデータを見たら「おっ、RNNだな」と思うかもしれません。
実際に初めに私が試したのはLSTMとGRUでした。

ただ、どちらも意味のあるものを学習する気配はありませんでした。
将来の気候変動などの予測のためにデータ変動のパターンを学習するのには活躍するのですが、今回のF1マシンの動作を認識するのには全く無意味のようです。

それ以外に3D CNNも試してみました。
画像は縦×横の二次元を畳み込んで特徴を抽出しますが、時系列で並べた画像を時間軸でも畳み込む3D CNNは正に動作を認識するためのモデルです。
Kerasには組み込みの3D CNNのモデルはないのですが、TensorFlow Hubにはi3d-kineticsという動画分類でトップクラスのモデルがあるので使ってみました。
https://tfhub.dev/deepmind/i3d-kinetics-600/1
しかし、思ったように精度が出ませんでした。詳しく調べてないですが、学習した重みが変なふうに壊れるので転移学習は出来てもFine Tuningは無理なのか…?
それにしても、TensorFlowが低レイヤー過ぎて学習させるのが辛い。

ちなみにそのままのi3d-kineticsにF1を見せたところ、

KINETICS_URL = "https://raw.githubusercontent.com/deepmind/kinetics-i3d/master/data/label_map.txt"
with request.urlopen(KINETICS_URL) as obj:
  labels = [line.decode("utf-8").strip() for line in obj.readlines()]
  print("Found %d labels." % len(labels))

with tf.Graph().as_default():
  frame_count = 25
  frames = [tf.reshape(tf.image.decode_jpeg(tf.read_file('f1/test_%s.jpg' % str(i)), channels=3), [1, 224, 224, 3]) for i in range(1, frame_count + 1)]
  concat = tf.concat(frames, 0)
  image_float = tf.cast(concat, tf.float32)
  i3d = hub.Module("https://tfhub.dev/deepmind/i3d-kinetics-400/1")
  logits = i3d(tf.reshape(image_float, [1, frame_count, 224, 224, 3]))
  probabilities = tf.nn.softmax(logits)
  with tf.train.MonitoredSession() as session:
    [ps] = session.run(probabilities)

print("Top 5 actions:")
for i in np.argsort(ps)[::-1][:5]:
  print("%-25s: %.4f%%" % (labels[i], ps[i] * 100))
shaking head: 99.9832%

という結果になりました。F1を知らないAIにはひたすら頭を振る謎の儀式に見えるようです。
というか、driving carというラベルが用意されているのだがそれは…
念の為、バイオリンを弾く動画も見せてみました。

playing ukulele: 98.683754%
playing violin: 1.3162433%

お、おう…。

BatchNormalizationは超強力
net = Conv1D(226, 3, padding='same', kernel_initializer='he_normal')(net)
net = BatchNormalization(axis=2)(net)
net = Activation('relu')(net)

1D CNNの部分でBatchNormalizationを入れているのですが、効果が凄いです。
BatchNormalizationを使う前はなかなか収束しなくて全く捗らないなと思っていたのですが、
使ってビックリ、訓練時の誤差があっという間に収束していきます!
今ではもう手放せません!
※効果には個人差があります。

データ読み込みの部分にバグが有り全く意味の無いデータを学習していた事があったのですが、それでもlossがぐんぐん減り続けてよくわからないデータにモデルがフィットしていく様は圧巻です。

危険!AIに奪われる時間

結局、AIに奪われるのは仕事や趣味などでは無く時間じゃないかと思い始めました。
考えただけで思い通りのものが勝手に出来上がる時代が来てほしい。