Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Let's use TensorFlow on Android!

Let's use TensorFlow on Android!

2016/11/19 ABC 2016 Autumnにて発表した「TensorFlowをAndroidで使おう!」の資料です。

Arata Furukawa

November 19, 2016
Tweet

More Decks by Arata Furukawa

Other Decks in Technology

Transcript

  1. Let's use TensorFlow on Android! November 19, 2016 Arata Furukawa

  2. 古川 新 東海大学 理学部1年 MaruLabo管理者 日本Androidの会 コミュニティ運営委員 日本Androidの会 学生部 部長

  3. Overview 1. 実行フローの説明 2. TensorFlow C++ APIの解説 3. ビルド方法 →理論の話はしません。

    理論については私のブログか、 DevFestでの発表資料を参考にしてください。 古川のブログ:http://ornew.net/ DevFestでの資料:https://speakerdeck.com/ornew/tensorflow-on-android
  4. How to run on Android? TensorFlowをAndroidでどうやって実行する? 1

  5. Workflow TensorFlowはC++とPythonで作られており、 Javaをサポートしていない つまり、Javaからネイティブコードを 実行する仕掛けが必要 → JNIを使います Java Native Interface

  6. About JNI JNIの指定するインターフェイスに準じたABIを持つネイティブコードを Javaから実行することができる仕組み。 例)以下のようなコードをC++で書きます。 JNIEXPORT jbyteArray JNICALL Java_com_example_app_Class_method (JNIEnv*

    env, jobject instance, jstring param){ // native code } →バイト配列を戻り値とし、引数として文字列を受け取る  Classクラスのメソッドmethodとして、Javaで使用できる ※C++のABIは処理系依存であるため、宣言はextern “C”内で行い、Cのマングルを使用する。
  7. JNI - TensorFlow JNIを使えばJavaからC++コードを実行できるので、 このC++コード内部でTensorFlowを実行すればいい。 TensorFlow C++ API 公式リファレンス(r0.11) https://www.tensorflow.org/versions/r0.11/api_docs/cc/index.html

  8. TensorFlow C++ API TensorFlow C++ API について基本を解説します 2

  9. Why C++? 多くの人はTensorFlowをPythonで使うと思います。 Python APIならよく知っているという方も多いでしょう。 Pythonは高速ですが、それでもC++ネイティヴには及びません。 僅かな差ではありますが、機械学習ほどになると、 その差は致命的な問題になる事もあります。 TensorFlowでは、演算グラフをシリアライズすることで、 グラフ構築はPythonで行い、速度を要求される場面では

    C++を利用するという選択肢があります。 Androidでの利用もまた、C++を使うことになります。
  10. C++ API 基本的に流れはPython APIと全く同じです。 ただし、デザインがC++流で、Pythonとは違うため、 使い方に癖があります。Pythonのものと同じ感覚で使うと ひどいバグになります(Signal 11と仲良くなれます) 今回はハマりがちな以下の説明をします。 1.

    Session作成と管理 2. Tensor作成と代入 3. Operation実行 尚、ソースコード中のtensorflow名前空間は長いので 省略します。
  11. About Session このSessionの作成自体には何も難しいことはありませんが、 Sessionはシングルスレッド前提であることに注意し、厳重に管理する 必要があります。 Androidアプリではあらゆるところで非同期処理が使われており、意 図しない形で別スレッドから操作が走る可能性があります。なるべく、 Sessionを作成するために使ったJavaのスレッドからSessionへの操 作を行う必要があります。 また、ネイティブコードの実行であるため、間違えて別スレッドからアク

    セスしたりして何かが起きてしまった場合、セグメンテーション違反で 問答無用でアプリが落ちます。しかも、エラーハンドリングをきちんとし ていないと落ちた理由すらわからない状況になる可能性もあります。 Pythonとは異なりC++は全て自分で管理する必要がありますので、 このSessionの管理にはとりわけ注意してください。
  12. Session - Create Session作成は以下のようになります。今回は簡単のため、作成する Sessionは1つとします。 SessionOptions options; std::unique_ptr<Session> session(NewSession(options)); //

    使用後 session->Close(); SessionOptionsをNewSessionに渡すことで新たなSessionを作 成できます。ここで、スマートポインタを用いていることに気をつけてく ださい。 Sessionはあらゆるリソースを持つことになりますので、破棄するとき は一度だけClose()を実行する必要があります。 ここで、C++に中途半端に知識がある方は、スマートポインタのカスタ ムデリータでCloseすればいいと考えるでしょう。 これは、ほぼ間違いなくやっては駄目です。
  13. Session - Lifecycle 今回のJNIコードは動的ライブラリとしてJavaからメモリにロードされ、 実行されることになります。ここで、Sessionはほとんどの場合で、そ の関数の内部で使用が完結することはないでしょう。実際にはグロー バル変数で保管して再利用することになります。このSessionのデスト ラクタは果たして呼び出されるでしょうか?答えは謎です。呼び出され るときもあれば、呼び出されない時もあります。 共有ライブラリの初期化と破棄の正確なタイミングはJava側からは一

    切関知できないという点を抑える必要があります。これにはいくつか理 由があります。
  14. Session - Close C++の共有ライブラリがロードされる時、C++はグローバル変数など の初期化を行うことが仕様で明記されています。しかし、アプリを実行 したとき、「必ずロードが発生するとは限らない」のです。既に共有ライ ブラリがプロセスにマップされていた場合は、C++の初期化は発生し ません。 既に共有ライブラリがマップされている可能性があるのは、共有ライブ ラリが破棄されるタイミングが処理系に依存するからです。つまり、ア

    プリの実行中にカスタムデリータが実行されない可能性があるので す。Sessionを確保したメモリ自体は問題ありませんが、Sessionが 確保した外部リソースなどがロックされたままになってしまう可能性な どがあります。 ですから、Closeは必ず明示的に呼び出してください。
  15. Tensor 次に、Tensorを説明します。 Tensorは以下のようにコンストラクトします。 Tensor tensor( DT_FLOAT, TensorShape({12,5,2})); Python APIと大して変わりませんね。 第一引数は内部表現の型、第二引数にTensorの形状を指定します。

    上の例であれば、12x5x2の三次元Tensorです。 難しいことはありませんが、これの使い方に少し癖があります。
  16. Tensor - View このTensorに値を代入するにはどうすればよいのでしょうか。 実はこのままでは何もできないので、Viewを取得する必要がありま す。これは、AndroidのViewとは無関係で、デザインとしてのView に近いニュアンスです。C++で言うと、ポインタや参照に近く、 string_viewと同じ発想のデザインです。NumPyを理解されている 方は、想像しやすいかと思います。 先程の例のTensorを、フラットなViewとして取得し、順番に値を代入

    すると以下のようになります。 auto view = tensor.flat<float>(); for(int i = 0; i < 12 * 5 * 2; ++i){ view(i) = i; }
  17. Tensor - Flat auto view = tensor.flat<float>(); ここでTensorのViewを取得しています。このViewに対し操作を加え ると、元となったTensorに反映されます。このような間接的にオブジェ クトを操作する事ができるようなデザインをViewと呼ぶことがありま

    す。 このViewはフラットとして取得したので、一次元配列のようにアクセス できます。 view(i) = i; 更に詳しく言えばViewの関数オブジェクトとしてデザインされており、 関数呼び出し演算子にインデクスを指定することで要素にアクセスで きます。この戻り値はmutableなため、左辺値として機能します。
  18. Tensor - Matrix 先ほどはフラットとしてViewを取得しましたが、今度は元のまま 12x5x2の行列として取得してみましょう。 auto view = tensor.matrix<float>(); view(0,0,0)

    = 0; matrixは、内部で保持している構造をそのまま返します。
  19. Tensor - Shaped 形状を変形して取得したい場合は以下のようになります。 auto view = tensor.shaped<float,2>({12, 10}); view(0,0)

    = 0; これは、12x5x2だったTensorを12x10の2次元Tensorとして取得し ています。 こんなこともできちゃいます。 auto view = tensor.shaped<float,5>({2,2,2,3,5}); view(0,0,0,0,0) = 0; より複雑な操作についてはAPIリファレンスを読んで下さい。 https://www.tensorflow.org/versions/r0.11/api_docs/cc/ClassTensor.html
  20. Run operations オペレーションの実行と言っていますが、厳密に言えばグラフの実行で す。オペレーションの実行と私が呼ぶのには訳があります。 SessionのRunメソッドにより、グラフを実行することができます。Python APIでは、最初に実行するオペレーションを引数として渡します。Python APIにおいては、直感的にはオペレーションを実行するように見えます。し かし、実はTensorFlowの本質的な話をするならば、これは厳密には少 し違います。Runが行うのは、あくまでもグラフの実行でしかないためで す。そして、Runに渡しているものはオペレーションとしてではなく、エンド

    ポイントとして指定されています。 TensorFlowの本当の動作は、グラフを実行し、指定されたエンドポイン トが満たされるまで動き続けるようになっているのです。結果的に、目的 のオペレーションを指定することで、オペレーションが実行されているよう に見えるのです。
  21. Run graph Python APIでは、直感的に実行できるようにデザインされています が、ローレベルAPIであるC++ APIは少し違います。最初、どうやっ てオペレーションを実行したら良いのか悩んだ記憶があります。 SessionのRunは以下のように定義されます。 StatusSession::Run( const

    vector<pair<string,Tensor>> &inputs, const vector<string> &output_tensor_names, const vector<string> &target_node_names, vector<Tensor> *outputs) それぞれの引数について解説します。
  22. Run graph - inputs 第1引数 inputs 型 vector<pair<string,Tensor>> const& これは簡単ですね。入力となるTensorです。

    Python APIのfeed_dictと等価です。使い方もそっくりです。 例) session->Run({ {"x", x}, {"labels", labels}, }, ...);
  23. Run graph - output_tensor_names 第2引数 output_tensor_names 型 vector<string> const& これが一番厄介です。名前から察するに、出力となるTensorの名前

    ですよね? 実は、これが先程言った、オペレーションの指定なのです。 TensorFlowはこの出力(=エンドポイント)が満たされるまで実行され ます。よって、ここにオペレーションの名前を指定することで、オペレー ションを実行できます。 例) session->Run({}, {"train_op:0"}, ...
  24. Run graph - target_node_names 第3引数 target_node_names 型 vector<string> const& これもちょっと厄介です。これは、「実行されるが出力に含まれないノー

    ド」を指定します。繰り返し言っているように、TensorFlowは出力が 満たされた時点で実行を終了します。しかし、これでは出力はないが 変数を変更するなどの副作用を持つ動作をするオペレーションが実行 できません。そういったノードを指定します。 私は使ったことがありませんが、サンプルによっては使い分けている ので、意味を理解しておかないと混乱します。
  25. Run graph - outputs 第4引数 outputs 型 vector<Tensor>* これも簡単ですね。出力が入るTensorの配列を指定します。

  26. Run graph - node name ちなみに、output_tensor_namesに指定する名前ですが、 これはグラフ定義の中に定義されている名前である必要が あります。この名前は、Pythonでグラフを作る時にちゃんと 名前をつけることをおすすめします。 y

    = tf.nn.softmax(tf.matmul(x, W) + b, name='name') ほとんどのオペレーション、関数にname引数が存在しており、 省略すると自動的に付きます。これはTensorBoardで使われる だけでなく、C++から使うときにも必要になりますので、 重要なオペレーションには必ず名前をつけましょう。
  27. How to build AndroidのためのTensorFlowを使ったJNIコードのビルド方法 3

  28. Bazel TensorFlowはGoogleの開発するBazelと呼ばれるビルドツールを 使っています。Bazelそのものの解説はしません。 尚、ビルド方法については私のブログにまとめてありますので、詳しい 手順はそちらを参照してください。 TensorFlowをAndroidで実行するためのビルド方法(v0.11.0rc0版) http://ornew.net/build-tensorflow-for-android-v0-11-0rc0 TensorFlowをAndroidで実行するためのビルド方法(〜v0.10.0版) http://ornew.net/build-tensorflow-for-android-v0-10-0 ここでは、基本的なことを説明します。

  29. Bazel - Dependency BazelでJNIに準じた共有ライブラリをビルドをするのですが、 TensorFlowとAndroidのために幾つか気をつけることがあります。 まず、共有ライブラリに @org_tensorflow//tensorflow/core:android_tensorflow_lib を静的リンクしてください。 Bazelクエリの説明をすると長いので省略しますが、端的にいうと TensorFlowのコアライブラリの内、Androidでの実行に必要なモ

    ジュール片をまとめたライブラリです。これがないと実行できません。 ただし、結構無駄なものも入っていたりするので、ライブラリサイズを削 りたい場合は必要なものだけコアライブラリからリンクすると良いでしょ う。
  30. Bazel - Options もう一つ気をつけないといけないのが、ビルドオプションです。 クロスコンパイルツールとターゲットABIの明示的な指定が 必要になります。 bazel build libexample.so --crosstool_top=//external:android/crosstool

    --cpu=armeabi-v7a [email protected]_tools//tools/cpp:toolchain これを指定しないとAndroidでは実行できません。 尚、これらの依存ツール群は、TensorFlowのbzlファイルを 読み込むことでBazelにより自動解決されます。
  31. Thanks! ご清聴ありがとうございました 左のアイコンでFacebookやっております。 質問等ありましたらお気軽にどうぞ。 古川新

  32. CREDITS Special thanks to all the people who made and

    released these awesome resources for free: ◦ Presentation template by SlidesCarnival ◦ Photographs by Unsplash