Unity
GoogleCloudPlatform
TensorFlow
Unity #2Day 19

環境構築不要?今すぐ始めるTensorFlow on Unity

TL;DR

  • UnityがTensorFlowSharpを.unitypackageで配布しているから今すぐ使える!
  • TensorFlowはGCPのCloud Shellにインストール済みだから今すぐ使える!
  • Python側でモデルを作って、Unityで動かしてみよう。
  • 注:ml-agentsの話ではありません。ml-agentsのラッパーを介さずにTensorFlowを使う話なので、先にml-agentsについて(参考リンク)知ったほうが良いかもしれません。

想定読者

  • Unityエンジニアだけど今流行りの機械学習を始めたい。

UnityでTensorFlowを動かしてみる

とりあえず、UnityでTensorFlowが動くことを確かめます。

  1. Unity(2017.1以上)をインストール
  2. 空のプロジェクトを作る
  3. .NET 4.6 を有効にして再起動
    1. Edit > Project Settings > Player
    2. Other Settings / Configuration / Scripting Runtime Version
    3. 「Experimental (.NET 4.6 Equivalent)」 を選ぶ
  4. Unity Tensorflow Plugin をダウンロードして展開。
  5. 以下が実行できればOK。
using UnityEngine;
using TensorFlow;

public class HelloTensorFlow : MonoBehaviour
{
    void Start()
    {
        TFGraph graph = new TFGraph();
        TFOutput tensor = graph.Const(new TFTensor(12345));
        TFSession session = new TFSession(graph);
        TFTensor[] results =
            session.Run(new TFOutput[0], new TFTensor[0], new []{ tensor });
        print(results [0].GetValue());  // Consoleに12345が表示されるはず!
    }
}

GraphやSessionが何なのかはまだ気にしなくて良いです。

Cloud ShellでTensorFlowを動かしてみる

次にCloud ShellでTensorFlowを動かしてみます。iPythonもnumpyもTensorFlowもインストールされていて便利です。

  1. GCPに登録してCloud Shellを有効にする。
  2. Cloud Shellを開く。
  3. iPython(普通のPythonでも可)で以下を実行できればOK。
$ ipython
In [1]: import tensorflow as tf
In [2]: tensor = tf.constant(12345)
In [3]: session = tf.Session()
In [4]: session.run(tensor)
Out[4]: 12345

先ほどと同じことをしているのにPythonの方がややシンプルですね。

これ以降、iPythonは章ごとに閉じて開き直す前提で読んで下さい。

機械学習モデルを作って保存

簡単な機械学習モデルを作ってみましょう。簡単と言えば、中学数学でおなじみの1次元線形モデル $y=ax+b$です。$x$が入力、$y$が出力で、パラメーター$a$と$b$を学習データから決定する機械学習モデルだと言い張ることが出来ます。(実用性はさておき、ニューラルネットはこの延長にあるので、練習としては良いと思います!)

$y=ax+b$というモデルをTensorFlowで作ると以下のようになります。iPythonで実行しましょう。

import tensorflow as tf
a = tf.Variable(-1.0, name='a')
b = tf.Variable(2.0, name='b')
init = tf.variables_initializer([a, b], name='init')
x = tf.placeholder(tf.float32, name='x')
y = tf.add(a * x, b, name='y')

tf.Variableでパラメーター$a$と$b$を作り、tf.placeholderで入力用の変数を作ります。そして演算 tf.add+)や tf.multiply*)を使って $y=ax+b$ という式を組み立てました。

ax+b.png
組み立てた式はOperationの集合としてGraphというオブジェクトに追加されます。Graphは明示的に作らなくてもデフォルトで1つ用意されています。tf.add等を呼び出す度にGraphにOperationが追加されてしまうので、念のため、今の状態を./linear/model.pb.bytes (txt)に一旦保存しましょう。

tf.train.write_graph(tf.get_default_graph(), './linear', 'model.pb.bytes', as_text=False)
tf.train.write_graph(tf.get_default_graph(), './linear', 'model.pb.txt', as_text=True)

学習はまだですが、初期値 $a=-1,b=2$ を指定しているので、例えば$x=1$を渡したら$y=1$が返って来るはずです。それを試したのが以下です。

sess = tf.Session()
sess.run(init)
sess.run(y, {x: 1.0}) #1.0が返ってくるはず!

Graphはモデルの定義であるのに対し、Sessionはその状態であり、Variableの値を持っています。sess.runに評価(実行)したいものを渡すことで、Variableを初期化したり、入力を渡して式を評価したり、学習を進めたりすることが出来ます。

保存したモデルをUnityで使う

保存したモデルがUnityで動くことを確かめます。まず、Cloud Shellのファイルをダウンロードlinear/model.pb.bytesをダウンロードしてUnityのプロジェクトに置きます。

スクリーンショット 2017-12-17 21.46.31.png

で、それを以下のMonoBehaviourにセットして実行するとPython版と同様に$x=1$を渡して$y=1$が返って来ることが確かめられます。

using UnityEngine;
using TensorFlow;

public class ModelImportExample : MonoBehaviour
{
    // Inspector で model.pb.bytes をセットしてください
    public TextAsset model;

    void Start()
    {
        TFGraph graph = new TFGraph();
        graph.Import(model.bytes);
        TFSession sess = new TFSession(graph);

        // Python の sess.run(init) と同じ
        sess.Run(new TFOutput[0], new TFTensor[0],
            new TFOutput[0], new TFOperation[]{ graph ["init"] });

        // Python の sess.run(y, {x: 1.0}) と同じ
        TFTensor[] results = sess.GetRunner()
            .AddInput(graph ["x"] [0], new TFTensor(1f))
            .Fetch(graph ["y"] [0])
            .Run();
        print(results [0]); // 1が出力されるはず!
    }
}

sess.GetRunnersess.Runの引数をメソッドチェーンで構築するためのビルダーみたいなものです。

モデルを学習して保存

まず先程のモデルを作り直します。

import tensorflow as tf
a = tf.Variable(-1.0, name='a')
b = tf.Variable(2.0, name='b')
init = tf.variables_initializer([a, b], name='init')
x = tf.placeholder(tf.float32, name='x')
y = tf.add(a * x, b, name='y')

次に $y_{\mathit{goal}}$ という入力を用意して、$y$ が $y_{\mathit{goal}}$ に近くなるように、すなわち $\left|y_{\mathit{goal}}-y\right|$ が小さくなるように学習するOperatorminimizeを作ります。minimizesess.runする度に $a,b$ が変化して学習が進みます。最小二乗法なら解析的に求まるのでは?ということはここでは忘れてください。

y_goal = tf.placeholder(y.dtype, name='y_goal')
optimizer = tf.train.GradientDescentOptimizer(0.01)
minimize = optimizer.minimize(tf.abs(y_goal - y), name='minimize')

別の章で使うのでこの状態のGraphも一旦保存してください。

tf.train.write_graph(tf.get_default_graph(), './linear', 'training.pb.bytes', as_text=False)
tf.train.write_graph(tf.get_default_graph(), './linear', 'training.pb.txt', as_text=True)

次に学習データを用意します。今回は練習なので意味のない線形っぽいデータを手で作ります。

train_x = [0, 1, 2, 3, 4, 5]
train_y = [6, 4, 5, 3, 1, 0]

入力 $x=0$ に対し $y=6$ を出力、入力 $x=1$ に対し $y=4$ を出力、以下略、という意味です。試しに学習前のモデルに入力するとtrain_yとは全然違う値が返ることがわかります。

sess = tf.Session()
sess.run(init)
sess.run(y, feed_dict={x: train_x})
# [2., 1., 0., -1., -2., -3.]

train_xtrain_yを入力としてminimizeを何度も実行することで学習が出来ます。

for _ in range(10000):
    sess.run(minimize, feed_dict={x: train_x, y_goal: train_y})

学習後のモデルではtrain_yに少しは近づいたことがわかります。線形モデルに線形ではないデータを入れたのでもちろんぴったりにはなりません。

sess.run(y, feed_dict={x: train_x})
# [5.99999619, 4.7599988, 3.52000141, 2.28000402, 1.04000664, -0.19999075]

学習後のモデルの状態、つまりSessionが持っている $a,b$ の値を、以下のように.ckptファイルに保存します。ckptはcheckpointの意味で、別にこの拡張子じゃなくても良かったはず。

saver = tf.train.Saver()
saver.save(sess, 'linear/trained.ckpt')

最後に、TensorFlowSharpがckptに対応していないらしいので、以下のコマンドで、model.pb.txtのVariableにtrained.ckptを埋め込んで定数化したtrained.pb.bytesを出力します。

python $(python -c "import tensorflow as tf; print tf.__path__[0]")/python/tools/freeze_graph.py \
--input_grap linear/model.pb.txt \
--input_checkpoint linear/trained.ckpt \
--output_graph linear/trained.pb.bytes \
--output_node_names y

学習したモデルをUnityで使う

学習後のモデルlinear/trained.pb.bytesは、以下のコードで動かすことが出来ます。変数を定数化したのでinitを呼ぶ必要がなくなりました。

using UnityEngine;
using TensorFlow;

public class TrainedModelImportExample : MonoBehaviour
{
    // Inspector で trained.pb.bytes をセットしてください
    public TextAsset model;

    void Start()
    {
        TFGraph graph = new TFGraph();
        graph.Import(model.bytes);
        TFSession sess = new TFSession(graph);
        float[] x = new float[] { 0f, 1f, 2f, 3f, 4f, 5f };
        float[] y = sess.GetRunner()
            .AddInput(graph ["x"] [0], x)
            .Fetch(graph ["y"] [0])
            .Run() [0]
            .GetValue() as float[];
        foreach (float value in y) print(value);
    }
}

Python側で見た値と同じ値が見られるはずです。

学習をUnity側でやりたい場合

途中で保存したlinear/training.pb.bytesを読み込んでminimizeRunすれば出来ます。

using UnityEngine;
using TensorFlow;

public class TrainingModelImportExample : MonoBehaviour
{
    // Inspector で trainig.pb.bytes をセットしてください
    public TextAsset model;

    void Start()
    {
        TFGraph graph = new TFGraph();
        graph.Import(model.bytes);
        TFSession sess = new TFSession(graph);
        sess.Run(new TFOutput[0], new TFTensor[0],
            new TFOutput[0], new TFOperation[]{ graph ["init"] });

        // 学習
        float[] trainX = new float[] { 0f, 1f, 2f, 3f, 4f, 5f };
        float[] trainY = new float[] { 6f, 4f, 5f, 3f, 1f, 0f };
        for (int i = 0; i < 10000; ++i)
        {
            sess.Run(
                new TFOutput[]{ graph ["x"][0], graph ["y_goal"][0] },
                new TFTensor[]{ trainX, trainY },
                new TFOutput[0],
                new TFOperation[]{ graph ["minimize"] });
        }

        // 学習結果
        float[] y = sess.GetRunner()
            .AddInput(graph ["x"] [0], trainX)
            .Fetch(graph ["y"] [0])
            .Run() [0]
            .GetValue() as float[];
        foreach (float value in y) print(value);
    }
}

What's next?

学習済みモデルが https://github.com/tensorflow/models にたくさんあるので、必要な形式に出力して使ってみる、というのをやってみたいです。

おまけ:write_graphしたものをPythonで読む

import tensorflow as tf
with open('linear/model.pb.bytes', 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
    tf.import_graph_def(graph_def, name='')

graph = tf.get_default_graph()
init = graph.get_operation_by_name('init')
x = graph.get_tensor_by_name('x:0')
# あるいは x = graph.get_operation_by_name('x').outputs[0]

これだけだとVariableがVariable型として取り出せないようなので不便でした。ただ、Graphの保存に関するAPIはtf.saved_modelなど他にもあるようです。