TL;DR
- UnityがTensorFlowSharpを.unitypackageで配布しているから今すぐ使える!
- TensorFlowはGCPのCloud Shellにインストール済みだから今すぐ使える!
- Python側でモデルを作って、Unityで動かしてみよう。
- 注:ml-agentsの話ではありません。ml-agentsのラッパーを介さずにTensorFlowを使う話なので、先にml-agentsについて(参考リンク)知ったほうが良いかもしれません。
想定読者
- Unityエンジニアだけど今流行りの機械学習を始めたい。
UnityでTensorFlowを動かしてみる
とりあえず、UnityでTensorFlowが動くことを確かめます。
- Unity(2017.1以上)をインストール
- 空のプロジェクトを作る
- .NET 4.6 を有効にして再起動
- Edit > Project Settings > Player
- Other Settings / Configuration / Scripting Runtime Version
- 「Experimental (.NET 4.6 Equivalent)」 を選ぶ
- Unity Tensorflow Plugin をダウンロードして展開。
- 以下が実行できれば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もインストールされていて便利です。
- GCPに登録してCloud Shellを有効にする。
- Cloud Shellを開く。
- 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$ という式を組み立てました。
組み立てた式は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のプロジェクトに置きます。
で、それを以下の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.GetRunner
はsess.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
を作ります。minimize
をsess.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_x
、train_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
を読み込んでminimize
をRun
すれば出来ます。
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など他にもあるようです。