ml-agents 0.8.0 で自分のプロジェクトで機械学習させる (実践編)


解決したい課題

今、私が取り組んでいることに「頭部を3Dスキャンしたキャプチャ3Dモデルで、いかに安価に手軽に実写っぽい映像を作れるか」ということがある。
動画を見てもらうと、どういうことかは理解できると思う。

そこの一つの課題点として、実際の表情と、ARKit Face Tracking でセンシングした表情とで微妙に差異があるということだ。
例えば、実際の口の開きが半開きなのに、3Dモデルの方が全開開きになってしまう、ということだ。
いい具合に開け具合をパラメーター調整すればいいのだが、それを手動でやるのは大変面倒臭い。これでは「手軽に実写っぽい映像を作る」という本来の趣旨と異なってしまう。

というわけで、この「3Dモデルと実写の微妙に違う表情の差異」を機械学習でできるだけ近づけるというのが、今回の課題となる。

採用した手法

機械学習を使う際、大事となるのが「何を評価すればいいか」ということだ。それも定量的に。
今回は「表情の差異」ということなので、表情はOpenCVによる画像認識で定量的に算出可能だ。
OpenCV で定量的に評価し、それを機械学習の評価として使うということにした。
つまり、

3Dモデルによる表情変化 → OpenCVで表情を数値化
参考となった実写の表情の変化 → 同様にOpenCVで数値化

というように、3Dと実写を同時に評価して、その差異を見比べるということだ。

ML-Agents で実装するために必要な三つのこと

では実際にML-Agents で機械学習を実装していくわけだが、はて何から手をつけていいのやら?という気持ちになる。
まあ、Academy, Agent, Brain の役割はなんとなくはわかる、が、実際自分の課題を解決する際に、何をどうすればいいのか、決めなくてはいけないことが多い。
決めなくてはいけない三つのことを、かなり主観的に洗い出してみた。

何を観察すればいいのか

何を観察対象にすればいいのかが最初さっぱりわからなかった。いろいろ文献やサンプルを見たところ、どうやら Agent が行動する際の判断基準にする何か、が観察対象だということがわかった。
つまり、鬼ごっこであれば周りの隠れる位置と鬼の場所だし、ブロック崩しであれば球の位置と自機の位置だ。自分の情報とか相手との距離とかを入れるのもポイントだ。
自分の位置を知ってこそ、行動ができるということだ。

何を評価すればいいのか

良かった、悪かった、というのをどうやって判定すればいいのだろうか、というのもまた難しい。
サンプルプロジェクトは何が成功、失敗というのが明確なのがほとんど。ボールが落ちたら失敗、点を取ったら成功。大変わかりやすい。
しかし画像認識の場合、何をもって類似している、というのはなかなかに難しい。

とりあえずOpenCVを評価基準に使うことにした。

何をコントロールすればいいのか

ゲームの場合、何をコントロールすればいいのかは簡単だ。プレイヤーがする行動がそのままAgentがコントロールできるようにすればいい。
インベーダーなら左右の移動と攻撃だし、パックマンなら上下左右移動だ。

しかし今回の場合、パラメーター操作で実写と3Dモデルの表情を似させる、ということをしたいわけだ。
何をコントロールすることで似せられることができるのかは大変悩むポイントだった。

さて、最終的には最適な3つのポイントを見つかったのだが、実際はそれまで紆余曲折があった。
というわけでその実装の遍歴を順を追って行きたいと思う。

実装

実装例1

観察対象(特徴点の絶対座標)

OpenCVでは顔の特徴点を検出してくれる。その特徴点を観察させることにした。
特徴点は顔の中心からの相対位置の方がいいのか、絶対位置でもいいのか、いまいちわからないが、とりあえず絶対位置でやってみることにした。

評価基準(特徴点の差異)

今回は「類似度」ということなので、特徴点の差分の合計が類似度、ということにした。差分がゼロであれば、全く同一となり、類似度がマックスになるようにする。
類似度が高ければ、評価が高くなるようにした。

コントロール(AnimationCurveの始点終点を直値で)

考えた末に、表情変化をAnimationCurveのパラメーターでコントロールできるようにした。
つまり、ARKit によるパラメーターが 0 → 0.5 → 0.8 となるとしたら、そのパラメーターをAnimationCurveで変化させて、0 → 0.2 → 0.5 にするようにカーブを作るということだ。
AnimationCurve をコントロールする際、いろんなコントロール方法があるのだが、ここで使ったのが、始点(0,0の位置)のTangent(立ち上がり率)と、終点(1,1の位置)のTangentを
直値で設定させるようにした。

コードで書くと、

key[0].outTangent = vectorAction[0];
key[1].inTangent = vectorAction[1];

という感じになる。

結果→失敗

まず、収束しない。1フレームごとに曲線が全く違うものになってしまう。一回一回を曲線でコントロールしているようだった。そりゃ無理だろう。

改善案1 コントロール(AnimationCurveの始点終点を変異で)

1フレームごとにパッパッと変わってしまうのであれば、そこに至るまでの変異を与えるようにすればいいのでは、ということで、変異をコントロールすることにした。
今までは

key[0].outTangent = vectorAction[0];

となっていたのを

key[0].outTangent += vectorAction[1] * 0.01f;

というような感じ修正した。

結果→失敗

すると、カーブが変異し続ける、という全くもって意図しない動作になってしまった。またもや収束しない感じだ。

改善案2 評価基準(アゴの特徴点のみ)

それまでは唇の特徴点の開閉度合いにしていたが、唇の空き具合なのか、アゴの開閉具合なのかが明確ではなかった。
より問題をシンプルにするために、アゴのだけに絞ることにした。アゴの下限のポイントだけに注目した。

結果→多少改善

この改善により、評価基準がより明快になった。ML-Agents 実装は、最初シンプルなものからやった方がいい、という教訓を得た。

改善案3 コントロール(カーブを辞めてそのままの値に)

カーブがどうやらうまくコントロールできないようなので、もういっそのこと直接パラメーターを取得できるようにした。
つまり、なんかよくわからない関数 f(x) にARKit で得られた値を与えてやれば、いい具合のBlendshapeが得られる、ということだ。

結果→劇的によくなる

最初、「さすがにこの方法はうまくいかないだろう」と思った。0から1までの値が放り込まれて、いい具合に0から1までの値が返ってきて、それがちょうど似ている具合になるなんて、さすがに都合よすぎるんじゃない?と思ったからだ。
ところが、これがバッチリうまくいった。
なんだカーブじゃなくてよかったのか、もっと大雑把に訳も分からず「適当な関数ちょうだい」って言えば良かったんだなと、改めて機械学習の凄さを思い知らされた。

改善案4 コントロール(値を直値から変異に)

改善案3で直値でうまくいったが、収束するのが多少時間がかかった。それであれば、正解までにたどり着くように変異を与えるようにすれば、もっと柔軟に対応できるのでは?ということで変異にした。
これはゲームで例えれば、今まで移動が行きたい場所にワープだったが、ちゃんとスムーズに移動できるようにした、という変更に似ているかもしれない。

改善案5 観察対象(よりシンプルに)

観察すべきものは本当に必要なものだけに絞る方がいいかもしれない、ということで、以下の三つだけを観察することにした。

  • ARKit Face Trackingで算出されたアゴの開閉度合い
  • 現在のアゴのパラメータ
  • アゴの特徴点の差異

結果→改善

収束しやすくなった。よりシンプルになったからだと思われる。


↑右のアゴの開閉度合いから左のブレンドシェイプのアゴの値を自動的に生成している様子

まとめ

結果としてはうまく言ったが、途中うまくいかないことが多かった。
というわけでいくつか教訓的なものをまとめたいと思う

教訓1 最初は問題をシンプルに

いきなり難しい課題に挑戦してしまうと、どこが問題なのかが解明するのに時間がかかってしまう。徐々に問題を複雑にした方がいいので、最初はシンプルな課題に落とし込むべきである。

教訓2 ゲーム空間に落とし込む

サンプルでは単純なゲームが多かったが、複雑な問題もゲーム的な空間に落とし込めば解決可能になる。つまり今回の問題は、単純に考えれば、類似というゴールに向かって進むプレイヤーの問題である。そこに進むには類似度という測定法が必要だし、どう動けばいいのかというコントロールも明快になる、という寸法だ。

教訓3 意外と丸投げでイケる

機械学習は極論を言ってしまえば、「入力に対して、いい具合の戻り値が返ってくる関数」を作ることだ。そうやって関数の形の外形さえしっかりしてしまえば、関数の中身は意外と何とかなる。「なんでその値が返ってくるかは分からないが、とにかくそれでOK」と割り切りが必要だ。