- 投稿日:2019-03-30T19:16:15+09:00
Kerasで作ったモデルをUnityに持っていくときのハマりどころ
はじめに
Unityでは、ゲーム内で強化学習させるならml-agentsとかKelpNetなどを使えますが、一方でゲーム中に得たデータを保存し、別の環境で機械学習させた後に学習結果をUnityにもっていく、という方法もあります。
そういう方法をDeep Neural Networkで行う場合はKerasが便利です。ネットワークを設計するのも簡単ですし、Google Colaboratoryにはデフォルトで入ってるので環境構築に悩むことなくすぐに作業できます。
ただ、Kerasで作ったモデルをUnity、特にOculus GoなどのAndroidデバイスに持っていくときにハマりどころがいくつかあります。しかも、ネットで見られる情報が不完全で試しても上手くいかないことが多いです。この記事では、そのハマりどころの紹介と回避方法を紹介します。ただし、使っている関数のいくつかがdeprecatedなので、そのうち修整が必要になるでしょう。問題が発生しましたら記事へのコメントでお知らせください。なお、記事で使用しているUnityのバージョンは2018.3.8f1です。
Kerasモデルの準備
こちらのチュートリアル の内容を実装します。開発環境はGoogle Colaboratoryです。
まず、こちらのデータをダウンロードして、pima-indians-diabetes.data.csvというファイル名でGoogle Colaboratoryのドライブにアップロードしてください。
次に、Keras Functional APIでモデルと学習プロセスを定義します。ここでinputは"input_x",outputは"output_y"と定義していることに注意してください。
from keras.models import Sequential from keras.layers import Dense import numpy numpy.random.seed(7) dataset = numpy.loadtxt("pima-indians-diabetes.data.csv", delimiter=",") X = dataset[:,0:8] Y = dataset[:,8] from keras.layers import Input from keras.models import Model inputs = Input(shape=(8,),name='input_x') x = Dense(12, activation='relu')(inputs) x = Dense(8, activation='relu')(x) predictions = Dense(1, activation='sigmoid',name='output_y')(x) model = Model(input=inputs, output=predictions) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(X, Y, epochs=150, batch_size=10) scores = model.evaluate(X, Y) print("\n%s: %.2f%%" % (model.metrics_names[1], scores[1]*100))学習が終了したら、
print(model.input) print(model.output)でinputとoutputの情報を見てください。
Tensor("input_x:0", shape=(?, 8), dtype=float32) Tensor("output_y/Sigmoid:0", shape=(?, 1), dtype=float32)上記のように、inputのほうは"input_x"と元の名前の通りですが、outputの方は"/Sigmoid"が追加されてます。この追加されてる方の名前をUnityで使うので注意してください。
KerasのモデルをTensorFlowグラフとして出力する
Kerasのモデルは直接Unityで読めないので、一旦TensorFlowの形式に変換します。ネットでググると変換の方法がいくつも出てきますが、ほとんどの方法はUnityに持っていったときに上手くいかなかったです。ここでは、こちらの記事の方法を紹介します。以下のコードはほぼ元記事の通りですが、そのままだとエラーが出るので一部修正しています。
import tensorflow as tf from tensorflow.python.framework import graph_util from tensorflow.python.framework import graph_io from keras import backend as K ksess = K.get_session() K.set_learning_phase(0) graph = ksess.graph num_output = 1 prefix = "output" pred = [None]*num_output outputName = [None]*num_output for i in range(num_output): outputName[i] = prefix + str(i) pred[i] = tf.identity(model.get_output_at(i), name=outputName[i]) constant_graph = graph_util.convert_variables_to_constants(ksess, ksess.graph.as_graph_def(), outputName)コードの内容を見るとわかりますが、Kerasのセッションをget_session()でTensorFlowのセッションとして呼び出し、学習したモデル内の変数をgraph_util.convert_variables_to_constantsで定数に変換しています。
output_dir = "./" output_graph_name = "keras2tf.pb" graph_io.write_graph(constant_graph, output_dir, output_graph_name, as_text=False)これで、keras2tf.pbというファイルにTensorflowグラフが保存されました。このファイルをローカルPCにダウンロードして、拡張子を.bytesに変更してください。
Unity内での作業
ここからはUnity内での作業になります。以下の作業はOculus Go向けのものですが、他のAndroidデバイスでも同様の流れで大丈夫かと思います。
Unityで新しいプロジェクトを作ったら、Androidをビルドターゲットにし、Player SettingでAPI Lebelを25に、Scripting Define SymbolをENABLE_TENSORFLOWにします。
つぎに、TensorFlowSharp Unityプラグインをインポートしてください。こちらからダウンロードできます。
ImportするとPlugins/Androidというフォルダが作られますが、その中の"System."と最初に名前のつくファイルは全て削除してください。そうしないとビルド時にエラーが出ます。どうやら、Unity2018.3で出るエラーのようです。
あと、上で作ったkeras2tf.bytesをAssetsフォルダの下にドラッグアンドドロップしてください。
次にModelImportExample.csというファイルを作り、以下のスクリプトを入力してください。スクリプト作成にはこちらの記事を参考にしました。
ModelImportExample.csusing UnityEngine; using TensorFlow; public class ModelImportExample : MonoBehaviour { public TextAsset model; private float[,] inputTensor = new float[1, 8]; private float[] testData = new float[] { 6f, 148f, 72f, 35f, 0f, 33.6f, 0.627f, 50f }; void Start() { #if UNITY_ANDROID && !UNITY_EDITOR TensorFlowSharp.Android.NativeBinding.Init(); #endif TFGraph graph = new TFGraph(); graph.Import(model.bytes); TFSession sess = new TFSession(graph); for (int i = 0; i < 8; i++) { inputTensor[0, i] = testData[i]; } TFTensor input = inputTensor; var runner = sess.GetRunner(); var test = runner.AddInput(graph["input_x"][0], input); test.Fetch(graph["output_y/Sigmoid"][0]); var output = runner.Run(); var result = output[0].GetValue() as float[,]; Debug.Log(result[0,0]); } }重要なポイントは、まず、Android向けにビルドするときは
#if UNITY_ANDROID && !UNITY_EDITOR TensorFlowSharp.Android.NativeBinding.Init(); #endifを追加してください。無いとビルドできません。
つぎに、インポートしたkeras2tf.bytesはTextAssetとしてエディタ上でmodelに割り当ててください。
あと、グラフへの入力と出力は
var test = runner.AddInput(graph["input_x"][0], input); test.Fetch(graph["output_y/Sigmoid"][0]);で定義しています。それぞれの名前は、Keras側で確認した通り"input_x", "output_y/Sigmoid"になっていることに注意してください。
これでエディタ上でスクリプトを実行すると、コンソールに"0.9049003"とアウトプットが出てくるはずです。確認したら、ビルドしてください。正常に終了するはずです。Oculus Goでも動作確認済みです。
- 投稿日:2019-03-30T14:30:31+09:00
C#8.0までに非同期ストリームのことを知っておく
やりたいこと
C#8.0で追加される予定の 非同期ストリーム でどんなことができるのか、どんな書き方をするのかを知っておこう、というのがこの記事の趣旨です。
気になる点があれば、遠慮なくコメントしてください。
ずばり、できるようになったことは?
非同期ストリーム は「複数の値を順に扱いたい」かつ「非同期な処理をシンプルに書きたい」というのを実現させるための機能だといえそうです。
「複数の値を順に扱う」というのは、イテレータの機能で、従来よりyieldやforeachなどで知られている機能のことです。
「非同期な処理をシンプルに書きたい」というのは、C#5.0の頃に追加された非同期機能で、async,awaitおよびTaskなどとして知られている機能のことです。
つまり、既存の2つの機能をうまく融合させた機能が「非同期ストリーム」なのです。できなかったこと
これまでは1つのメソッドで、
async/awaitキーワードと、yieldキーワードの共存ができませんでした。// [C#7.3まで]async/await と yield は共存できない // async IEnumerable<int> DoAsync(){C#8.0からはこれが書ける
asyncメソッド、かつ、IAsyncEnumerable<T>またはIAsyncEnumerator<T>型をreturnするメソッドを書いた場合、メソッドの内部でyieldキーワードを利用できるようになりました。 つまりasync/awaitキーワードと、yieldキーワードが共存できます。async_awaitとyieldの夢の競演async IAsyncEnumerable<int> DoAsync(){ await Task.Delay(1000); yield return 1; }同期イテレータとの対応
同期 非同期 IEnumerable<T>IAsyncEnumerable<T>┗ .GetEnumerator()┗ .GetAsyncEnumerator()IEnumerator<T>IAsyncEnumerator<T>┗ .Current┗ .Current┗ .MoveNext()┗ .MoveNextAsync()IDisposableIAsyncDisposable<T>┗ .Dispose()┗ .DisposeAsync()気づき:
MoveNextAsync()ではValueTaskが使用されている。非同期ストリームを消費する
IAsyncEnumerable<T>がreturnされても、これを使う側がないと片手落ちです。IEnumerable<T>を使うためにforeachを使っていたのと同様、IAsyncEnumerable<T>でも、await foreachが使えるようになりました。
これも違和感なく使える構文になっていると思います。await foreach(var item in asyncStream) { Console.WriteLine(item); }どんな動きになるのかな?
パターン1
このメソッドをasync IAsyncEnumerable<int> DoAsync() { foreach(var item in Enumerable.Range(1, 100).Select(x => x * 5)) { await Task.Delay(item); yield return item; } }こう呼び出すasync ValueTask DoSomething() { await foreach(var item in DoAsync()) { Console.WriteLine(item); } }これは動きが予想しやすいと思います。
5,10,15,...の数字が初めは勢いよく、徐々にスローダウンしながら表示されます。パターン2
拡張メソッドをこう定義してpublic static class Ex{ // 受けとったTaskの配列をIAsyncEnumerableとしてreturnする拡張メソッド public static IAsyncEnumerable<T> AsAsyncEnumerable<T>( this IEnumerable<Task<T>> tasks ) => tasks switch { null => throw new ArgumentNullException(nameof(tasks)), IEnumerable<Task<T>> ts => ts.AsAsyncEnumerableImpl(), }; static async IAsyncEnumerable<T> AsAsyncEnumerableImpl<T>( this IEnumerable<Task<T>> tasks ) { foreach(var task in tasks) { yield return await task; } } }こう呼び出すasync ValueTask DoSomething() { var tasks = new List<Task<int>>(); tasks.Add(Task.Delay(1000).ContinueWith(_ => 1)); tasks.Add(Task.Delay(3000).ContinueWith(_ => 2)); tasks.Add(Task.Delay(5000).ContinueWith(_ => 3)); tasks.Add(Task.Delay(2000).ContinueWith(_ => 4)); tasks.Add(Task.Delay(4000).ContinueWith(_ => 5)); await foreach(int item in AsyncEnumerableEx.AsAsyncEnumerable(tasks)) { Console.WriteLine(item); } }
Taskを使い慣れている人ならば、間違えないと思いますが、開始からおよそ1秒後に1が、そのおよそ2秒後に2が、そのおよそ2秒後に3,4,5が表示されます。
ListにAddした時点でTaskが動き始めていることが重要ですね。今回は
Task.Delay()のような無駄な処理を呼び出していますが、これがダウンロード処理と考えれば使い道は多そうな気がします。非同期処理とは言っていますが、結果は最初に指定した順に取り出されるので、処理も追いかけやすいはずです。まとめ
- 非同期ストリームは、既存の2機能「イテレータ」と「非同期処理」の力強い融合
IAsyncEnumerable<T>またはIAsyncEnumerator<T>
- 同期版のインタフェースやメソッドに
Asyncが付いただけなので、違和感なく扱えそう。- 列挙するときは、
await foreachを使う。- 非同期ストリーム機能の追加自体に難しいことはなさそう。難しいとすれば、それは非同期処理そのもの。
※C#8.0は現在Preview版です。Preview版で動作確認の上、当記事を書いていますが、記事の内容と異なる構文になる可能性もありますのでご了承ください。
今回書けなかったこと
- 非同期ストリームの例外処理や
CancellationTokenIAsyncEnumerable<T>とIAsyncEnumerator<T>の自力実装await foreachのダックタイピング的な挙動- 非同期LINQ的なこと
過去記事
- C#8.0ネタは、いずれどこかにリンクをまとめておきたい。

