その後のその後

iOSエンジニア 堤 修一のブログ github.com/shu223

TensorFlowをiOSで動かしてみる

TensorFlow に iOS サポートが追加された というニュースを見かけたので、ビルドして、iOSで動作させてみました。


たまたま目の前にあった扇風機もバッチリ認識してくれました)


本記事では最終的にうまくいった手順を書いています。この手順をなぞってみるにあたってTensorFlowや機械学習・ディープラーニングについての専門知識は不要ですのでぜひお試しください!

ビルド手順

(2017.4.15追記)v1.1.0 RC2 のビルド

現時点での最新Release(候補)である v1.1.0 RC2 も、tensorflow/contrib/makefile/build_all_ios.sh を実行するだけでビルドできました。

(2016.8.22追記)v0.10.0 RC0 のビルド

現時点での最新Releaseである v0.10.0 RC0 は、上記手順でビルドしようとすると compile_ios_tensorflow.sh を実行するところでコケてしまったのですが、今は各ビルドの手順がまるっと入ったスクリプトが用意されているようです。

tensorflow/contrib/makefile/build_all_ios.sh

依存ライブラリのダウンロードからiOSアーキテクチャ向けビルドまで、これ一発でokでした。

(※旧バージョンでの手順)v0.9.0 のビルド

iOSサポートバージョンであるv0.9.0をチェックアウトして、以下の手順でビルドしていきます。手順はこの通りになぞるだけですし、ほぼスクリプトを実行するだけで非常に簡単です(が、実行時間がかなりかかります)。

  • スクリプトを実行して Eigen や Protobuf 等の依存ライブラリをダウンロード
cd tensorflow
tensorflow/contrib/makefile/download_dependencies.sh
  • protobuf をビルド&インストール
cd tensorflow/contrib/makefile/downloads/protobuf/
./autogen.sh
./configure
make
sudo make install


ここで、僕の環境では `./autogen.sh` 実行時に

Can't exec "aclocal": No such file or directory at /usr/local/Cellar/autoconf/2.69/share/autoconf/Autom4te/FileUtils.pm line 326.
autoreconf: failed to run aclocal: No such file or directory
shuichi-MacBook-Pro-Retina-15inch:protobuf shuichi$ ./configure
-bash: ./configure: No such file or directory

というエラーが出ました。"aclocal"というのは、"automake"パッケージに含まれているらしいので、automakeをインストール。

brew instal automake


で、改めて

./autogen.sh
./configure
make
sudo make install
  • iOS native版のprotobufをビルド
cd ../../../../..
tensorflow/contrib/makefile/compile_ios_protobuf.sh
  • iOSアーキテクチャ向けにTensorFlowをビルド
tensorflow/contrib/makefile/compile_ios_tensorflow.sh

サンプルをビルドする

`tensorflow/tensorflow/contrib/ios_example` に2種類のサンプルがあります。


simple と名付けられたサンプルアプリは、今回ビルドした static library をアプリ内に組み込む際の最小実装としての参考にはなりそうですが、デモとして見て楽しい機能があるわけではないので、本記事ではもう一方の camera の方を紹介します。


まず、このファイルをダウンロードして、解凍すると入っている、

  • imagenet_comp_graph_label_strings.txt
  • tensorflow_inception_graph.pb

の2つのファイルを、`tensorflow/contrib/ios_examples/camera/data` に置きます。*1


この Inception というのはGoogleが提供する画像認識用の学習済みモデルのようです。

cameraサンプル実行例

cameraサンプルを実行すると、すぐにカメラが起動します。身近のものを色々と映してみました。(リアルタイムカメラ入力に対する認識結果を表示してくれます)

MacBook


"Notebook" "Laptop" どっちでも正解!

扇風機


"Electric Fan" 正解!

椅子


"Folding Chair"(折りたたみ椅子) 惜しい。。ですが、そもそも `imagenet_comp_graph_label_strings.txt` には chair が3種類しかなくて、その中では一番近いと言ってもいいかもしれません。

iPhone


"iPod"・・・惜しい!iPhoneなのでほぼ正解と言ってもいいかもしれません。

パフォーマンス

今回cameraサンプルを試したデバイスはiPhone 6だったのですが、認識はまだまだリアルタイムとは言い難く、数秒単位の遅延がある感じでした。まだiOS向けにビルドできるようになったというだけでiOSにおける GPU Accelerated はされてないですし、パフォーマンスの最適化はまだまだこれからのようです。


(2017.4.15追記)v1.1.0 rc2 / iPhone 7 で試したところ、それなりにリアルタイムで動きます。`contrib/ios_examples` 配下のコミットログを見たところ、Metal等による最適化はまだされてないようですが、いくつかパフォーマンスに関連しそうな改善はあったようです。

(追記ここまで)


ちなみにiOS 10 で Accelerate.framework に追加された BNNS (Basic neural network subroutines) や MetalPerformanceShaders.framework に追加されたCNNのAPIを使用する最適化もissueに挙がっています。


MPSCNNについてはいくつか記事を書いたのでよろしければ:

ビルド手順の情報ソース

iOSサポートバージョンである0.9.0トップ階層にあるREADMEを見ても、iOSについては書かれていません


上述したビルド手順は`contrib/makefile` 配下のREADMEに書かれています。

関連情報を以下にまとめておきます。

サンプルのソースコードを読んでみる

サンプルは全編Objective-C++で書かれています。読んでもわからないだろうと思いつつ、せっかくなので読んでみます。

tensorflow_utils

モデルとラベルデータを読み込むメソッドと、認識結果を返すメソッドを持つユーティリティクラスのようです。

tensorflow::Status LoadModel(NSString* file_name, NSString* file_type,
                             std::unique_ptr<tensorflow::Session>* session);
tensorflow::Status LoadLabels(NSString* file_name, NSString* file_type,
                              std::vector<std::string>* label_strings);
void GetTopN(const Eigen::TensorMap<Eigen::Tensor<float, 1, Eigen::RowMajor>,
             Eigen::Aligned>& prediction, const int num_results,
             const float threshold,
             std::vector<std::pair<float, int> >* top_results);

汎用的に使えそう。

ios_image_load

画像ファイルを読み込むクラス。

std::vector<tensorflow::uint8> LoadImageFromFile(const char* file_name,
             int* out_width,
             int* out_height,
             int* out_channels);

プロジェクトには追加されているけど使われてないっぽい。

CameraExampleViewController

サンプル本体。肝っぽいところだけを拾っていくと、


`std::unique_ptr` なメンバ変数を定義して、

std::unique_ptr<tensorflow::Session> tf_session;


viewDidLoadのタイミングでモデルを〜.pbファイルからロード

tensorflow::Status load_status =
  LoadModel(@"tensorflow_inception_graph", @"pb", &tf_session);


で、カメラからのリアルタイム入力が得られるたびに呼ばれるデリゲートメソッドを見てみると、

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {
  CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
  [self runCNNOnFrame:pixelBuffer];
}

CMSampleBufferRef から CVPixelBufferRef を取得して `runCNNOnFrame:` を呼んでいるだけのようです。


`runCNNOnFrame:` はコードが若干長いですが、このへんと、

tensorflow::Tensor image_tensor(
    tensorflow::DT_FLOAT,
    tensorflow::TensorShape(
        {1, wanted_height, wanted_width, wanted_channels}));
auto image_tensor_mapped = image_tensor.tensor<float, 4>();
tensorflow::uint8 *in = sourceStartAddr;
float *out = image_tensor_mapped.data();


このへんが肝っぽいです。

if (tf_session.get()) {
  std::string input_layer = "input";
  std::string output_layer = "output";
  std::vector<tensorflow::Tensor> outputs;
  tensorflow::Status run_status = tf_session->Run(
      {{input_layer, image_tensor}}, {output_layer}, {}, &outputs);
  if (!run_status.ok()) {
    LOG(ERROR) << "Running model failed:" << run_status;
  } else {
    tensorflow::Tensor *output = &outputs[0];
    auto predictions = output->flat<float>();

    // ラベル取得
  }
}


が、すみません、それぞれ何をやっているのかを解説するにはTensorFlowの処理をちゃんと理解する必要がありそうです。。(勉強します)

自分のアプリに組み込む

試せていませんが、こちら に手順が示されていました。

  • ユニバーサルなstatic library(libtensorflow-core.a)をビルドしてプロジェクトに追加する
    • [Library Search Paths] にも追加。

The `compile_ios_tensorflow.sh' script builds a universal static library in tensorflow/contrib/makefile/gen/lib/libtensorflow-core.a. You'll need to add this to your linking build stage, and in Search Paths add tensorflow/contrib/makefile/gen/lib to the Library Search Paths setting.

  • libprotobuf.a と libprotobuf-lite.a をプロジェクトに追加する。
    • [Library Search Paths]にも追加

You'll also need to add libprotobuf.a and libprotobuf-lite.a from tensorflow/contrib/makefile/gen/protobuf_ios/lib to your Build Stages and Library Search Paths.

  • [Header Search paths] を追加

The Header Search paths needs to contain the root folder of tensorflow, tensorflow/contrib/makefile/downloads/protobuf/src, tensorflow/contrib/makefile/downloads, tensorflow/contrib/makefile/downloads/eigen-eigen-, and tensorflow/contrib/makefile/gen/proto.

  • [Other Linker Flags] に `-force_load` を追加

In the Linking section, you need to add -force_load followed by the path to the TensorFlow static library in the Other Linker Flags section. This ensures that the global C++ objects that are used to register important classes inside the library are not stripped out. To the linker, they can appear unused because no other code references the variables, but in fact their constructors have the important side effect of registering the class.

  • bitcodeを無効にする

The library doesn't currently support bitcode, so you'll need to disable that in your project settings.

まとめ

TensorFlowをiOS向けにビルドし、付属のサンプルを動かしてみました。パフォーマンスの最適化等、まだまだこれから感はありますが、扇風機や椅子、ノートパソコンやiPhone等、大きさも形も全然違うものを認識してくれて未来を感じます。


試してみるだけなら機械学習やTensorFlowについての専門知識を必要としませんし、ぜひお手元で動かしてみてください!


次のステップとしては、下記記事で紹介されているテンソルの計算部分に特化してSwiftで書かれたライブラリと比較したりしつつ、ちゃんとTensorFlowでやっていることを理解したいと思います。


また、本記事同様に「とりあえず動かしてみる」シリーズとして、こちらもよろしければどうぞ:


*1:ちなみにこのとき、 https://github.com/miyosuda/TensorFlowAndroidDemo/find/master にある同名ファイルを使っても、ちゃんと動きません。参考