雑多なノート

プログラミング初心者がメモとかを書きます。基礎的なこともメモとして。偏食系のアニオタ。

TensorFlowの画像分類を利用したAndroidアプリを作ってみた

TensorFlowのAndroid実装を利用したアプリを作ってみたので流れをまとめる.
ちなみちディープラーニングのコードには一切触れてない入門書読んだだけのディープラーニングにわかなのでご了承を.
何か頓珍漢なこと言ってたら教えてください.

環境

モデルの準備

モデルが無ければ何も始まらないのでまずはモデルを用意する.
用意するといっても自分で1から作るのはしんどいといので既存のモデルを転移学習することにした.
自分でモデル設計するスキルも1から学習させられるようなスペックのパソコンも持ってないし.

既存のモデルと学習済みの重みを元に新しいモデルを作ることをFine-tuning(転移学習)というらしく,これにより少ない学習データと学習時間でそれなりの結果が得られる.
Fine-tuningは以下のチュートリアルに従うことによって簡単にできる.

How to Retrain Inception's Final Layer for New Categories  |  TensorFlow

まずはTensorflow本体をGitから持ってくる.

$ git clone https://github.com/tensorflow/tensorflow

次にビルドを行う.
初回のビルドはそれなりに時間がかかる.

$ cd tensorflow/
$ ./configure
$ bazel build tensorflow/examples/image_retraining:retrain

途中でライブラリが足りないだの何だの言われるので適宜持ってくる.
ノーパソで1時間ちょいかかった.

これでFine-tuningを行う準備は完了.
正直拍子抜けするほど簡単であるがこれだけでFine-tuningを行うことができるらしい.
任意のモデル使ったり細かいカスタマイズとかは出来ないけど.

データセットはラベル名をディレクトリ名として適当な場所に置く.
自分は大体以下の感じになった.

.
├── ajiazou
│   ├── 000000274001L.jpg
│   ├── 001(2).jpg
│   ├── 004.jpg
│   ├── 008.jpg
│   ├── ...
│ 
├── amerikabaku
│   ├── 000000274001L.jpg
│   ├── 001(2).jpg
│   ├── 004.jpg
│   ├── 008.jpg
│   ├── ...
│  

今回は画像検索からもってきた動物画像を学習データとして使った.
動物の種類は46種.
それぞれ100-200枚程度集められた.

そしてスクリプトを実行する.
image_dirに学習データを置いてあるディレクトリを指定する.

$ bazel-bin/tensorflow/examples/image_retraining/retrain --image_dir=~/dataset/ueno/

ここでもライブラリがないだの何だの言われるので適当にpipでインストールする.
後は待っているだけで自動的にモデルをダウンロードしてきてFine-tuningが行われる.
画像でないファイルが含まれると容赦なく落ちるので事前に確認しておく.
デフォルトでダウンロードされるモデルはGoogleのInception v3となってる.
オプションで指定することによりMobileNetという軽量モデルを使うことも可能である.
ただ試してみた感じ汎化性が微妙っぽい.
あまりに結果が悪すぎるので使い方が間違ってる可能性もあるけど.
モデルの指定の他にトレーニング回数(デフォルトで4000)や学習時の画像加工(反転,拡大,明るさ)の指定もできる.
ちなみにモデルのライセンスはApache2.0なのでいろいろと安心.
ちなみにTensoBordも使えるらしいので手軽にディープラーニングやってる感を味わえる.

途中経過はコンソールにも表示されるので見てて楽しい.
すぐに飽きたけど.

f:id:vayacico:20171021013959p:plain

学習されたモデルは特に指定してなければ/tmp/output_graph.pbとしてラベルが書かれたoutput_label.txtとペアで保存される.

PC上で使う分にはこれで終了なのだがAndroidで使うためにはもう一手間必要となる.
AndroidのTensorflowで扱うためには無駄のないモデルである必要があるらしく,つまりは学習のためのみに使われるドロップアウト層や結果に関与しない層を取り除く必要があるとのこと.
聞くと面倒そうだがこれもコマンド一発で行うことができる.

$ bazel build tensorflow/python/tools:optimize_for_inference 
$ bazel-bin/tensorflow/python/tools/optimize_for_inference --input="/tmp/output_graph.pb" --input_names="Mul" --output_names="final_result"

input_namesとoutput_namesはそれぞれ入力層と出力層の名前らしい.
正直よくわかってないけどinception v3とMobileNetは共通してこれでできるので問題ない.

これでAndroidで使うためのモデルの準備が完了した.
出力されたモデルファイルとラベルファイルをAndroidアプリのassetに持っていく.

Androidアプリの作成

続いてAndroidサイドの実装を行う.
tensorflow/tensorflow/examples/android at master · tensorflow/tensorflow · GitHub
ここに書いてある通り,gradleに書くだけで自動的にダウンロードしてきてくれて使える状態になる.

allprojects {
    repositories {
        jcenter()
    }
}

dependencies {
    compile 'org.tensorflow:tensorflow-android:+'
}

ただnightly-android [Jenkins]から最新のビルドを持ってきて使った方が早かったので自分はこっちを使った.
数時間おきに安定ビルドが行われているちょっとよくわからない世界である.

これでAndroidでTensorflowを使えるようになったわけだけど,自分で画像の形式変換や結果の解析のコードを書くのは面倒なのでTensorflowのデモアプリに含まれるクラスを持ってきて便利に使わせてもらう.
使わせてもらうクラスは以下の2つ.


簡単に言うとTensorflowを簡単に使うためのクラスと画像分類を簡単に使うためのクラス.
ここまでくると後は簡単.

まずは各変数の定義

int INPUT_SIZE = 299;
int IMAGE_MEAN = 128;
float IMAGE_STD = 128;
String INPUT_NAME = "Mul";
String OUTPUT_NAME = "final_result";
String MODEL_FILE = "file:///android_asset/optimized_graph.pb";
String LABEL_FILE = "file:///android_asset/output_labels.txt";

MODEL_FILEとLABEL_FILEでassetsに置いたモデルファイルとラベルファイルのパスを指定.
INPUT_SIZEは入力サイズでinception_v3を使う場合は299となる.
MobileNetを使う場合は指定したモデル名に書いてあるサイズを指定(MobileNet_v1_1.0_???)する.

次に初期化.

Classifier classifier = TensorFlowImageClassifier.create(getAssets(), MODEL_FILE, LABEL_FILE, INPUT_SIZE, IMAGE_MEAN, IMAGE_STD, INPUT_NAME, OUTPUT_NAME);

そしてこれで識別の実行.
それなりに時間がかかるので自分はAsyncTaskで実行させた.
スレッドで実行させると速度がガクッと下がるのが謎.
軽く調べた感じネイティブ実装でCPUに割り振るときのうんたらかんたらとか書いてあったがよく分からなかった.

List<Classifier.Recognition> results = classifier.recognizeImage(bitmap);

与えるBitmapはARGB形式(Bitmap.Config.ARGB_8888)でサイズがINPUT_SIZE x INPUT_SIZEである必要がある.
デモアプリに含まれるクラスでサイズの変換できる.
tensorflow/ImageUtils.java at master · tensorflow/tensorflow · GitHub

これでresultにラベル名と信頼度のリストが格納され無事分類が完了する.

実装したアプリ

以上の流れで実装してみたアプリを動物園で試してみた結果がこちら.
UI自体はWebViewで作った.


転移学習でラッキーさんに動物を分類してもらった


うまくいく動物相手だとわりと安定して正しく識別してくれる.
動画だとそれなりにいい感じに動いてるけど実際はダメダメ.
失敗動画はそんなに録画してないだけ.
撮影環境にかなりシビアである.
原因としては画像検索で得られる画像が良くも悪くも綺麗すぎるのが考えられる.
やはりプロが一眼で撮ったであろう野生生物とスマホのカメラで撮った動物園の動物とでは条件が違いすぎたのかもしれない.

ここでも述べられている通りデータの特徴の違いが大きく影響したのだと考えられる.
まぁ当たり前ではあるが.
https://qiita.com/nonbiri15/items/b29fe079d359d531bf85qiita.com

動物園でデータ集めればもうちょいいい結果になるのかもしれない.

まとめ

最終的なアプリとしてはちょっと悔しい結果ではあったが,専門知識を持ち合わせていない自分がここまで簡単にできるようになるとはディープラーニングもずいぶん裾野が広がってきたように感じる.
最先端っぽいことが簡単に実装できちゃうとやっぱり楽しい.
最先端の人たちにとっては画像分類なんかもう古いみたいになってるのかもしれないけど.
ググるとなんか凄いことばっかりやってるし.
まぁ順調に自分が扱えるところまで下りてきてると考える夢が広がる.

miniconda2のインストール

前にやったけどノーパソ買い替えでやり直しになったのでせっかくだしメモ

$ wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh
$ chmod +x Miniconda2-latest-Linux-x86_64.sh
$ ./Miniconda2-latest-Linux-x86_64.sh

指示通りにEnter押したりYES入力してると終わる.
簡易なPython環境を目的としてたので最後の質問でyesと答えてパスを追加しておいた.

再ログインすると有効になる.

$ which python
/home/vayacico/miniconda2/bin/python

AndroidのWebViewが死ぬほど重い

俺がAndroid標準のUIを使ってアプリを作るとどうしてもダサくなる
いかにも初心者がコピペで頑張って作りました感が出てしまう
合ってるんだけどね

真面目系クズの俺としては上っ面だけでも取り繕いたかったので今作ってるアプリはOnsen UIを使ってiPhoneチックな見た目にしている

でもデザインを大方作り終えて実機に持っていったときに問題が起きた
スクロールが死ぬほどカクカクなのだ
こんなん使い物にならんじゃんAndroid死ねとか思いつつググってたらハードウェアアクセラレーションなるものを有効にすれば高速化するらしい

android:hardwareAccelerated="true"

これをmanifestに書くと有効になる
あのカクカクがGPU支援程度で改善するわけないじゃんと思い追加してみた

めっちゃヌルヌルになった
GPUさんすごい
疑ってごめんよAndroid

というわけでOnsen UIはいいぞという話でした
実機に持っていかなくてもデザインできるのが利点であり穴でもあるけど

ネイティブプラグイン(aar)で外部ライブラリ(aar,jar)を使う

Android Studioでモジュールを作成して実行したらクラスが見つからないというエラーが出てしまった
gradeに書いてモジュールを作ったわけだけどaarファイルには含まれないらしい
Unityが見つけてくれないだけかもしれないけど

Assets->Plugins->Android->libsに置くことで認識してくれた

Failure to initialize, your hardware is not supported

Android上のUnityでネイティブプラグインを使ったアプリを作ってたときのこと.
外部ライブラリを追加した時点でこのエラーが表示されて起動しなくなった.

どうやら追加したライブラリのAARファイルにarmeabi-v7a以外の端末向けのネイティブコードが入ってたのが悪いらしい.
対応してないアーキテクチャ用のネイティブコードが入っていると混乱するんだとかなんとか.

消せばいいらしいが自分でビルドしたライブラリじゃないのでどうしようかと思ったらaarファイルを展開してディレクトリ消すだけで動いた.
aarファイルはapkファイルと同じく中身はZIPなので拡張子を.zipにするだけでエクスプローラーから見える.

jni以下にアーキテクチャ別にディレクトリがあるのでarmeabi-v7aを消す.
自分の場合はarm64-v8aとx86x86_64があったので消した.
消す必要があるのは一部だけっだったような気もするけど面倒だったので全消し.

これを再圧縮してUnityに突っ込んだところ無事動いた.


Error | Unity Community

tensorflowでfine-tuning

自前の画像で画像分類を行うfine-tuningを試してみる
まず必要なライブラリ群をそろえる

色々試したけど最終的に以下の内容で落ち着いた
かなり回り道をしたので色々間違ってるかもしれない

  • Bazel
  • Miniconda2
    • 最初は3.6でやったけどAndroidアプリのビルドでエラー出たので2.7に切り替え.設定ファイル弄れば回避できるらしかったけど不慣れなビルドツールで変なことしたくなかったので安定を選んだ
  • Numpy
    • Pip install numpyとapt-get install python-numpyの両方が必要だった


ソースコードはお馴染みのgithubから持ってくる

$ git clone https://github.com/tensorflow/tensorflow 
$ cd tensorflow
$ git checkout Branch


公式のチュートリアルに従って行う
まずはトレーニング用の画像を持ってくる
自分で用意しても良いんだろうけど今回は試しなので用意されているものを使う

cd ~
curl -O http://download.tensorflow.org/example_images/flower_photos.tgz
tar xzf flower_photos.tgz

中身は花の名前の付いたディレクトリが5つあり中には画像ファイルが600-800程度入っていた
このディレクトリ名がクラス名になるらしい
ディレクトリ名が自動的にクラス名になるのは簡単でいいかも

続いてビルド

bazel build tensorflow/examples/image_retraining:retrain

corei7のデスクトップパソコンで30分ほどかかった
初回のビルドが終わると次回からは早いらしい

そしてfine-tuningを行う

bazel-bin/tensorflow/examples/image_retraining/retrain --image_dir ~/flower_photos
    • image_dirには先ほどの花の画像群を展開した場所を指定する

私の環境ではpythonのモジュールが足らないだののエラーがでたのでpipでインストールを行った

このスクリプトを実行するとgoogleが作った高性能なモデルinception v3がダウンロードされそれに対してfine-tuningが行われる
正直中で何をやっているのかよくわからない

fine-tuningだからか思ったより学習には時間がかからなかった
学習されたモデルは/tmp/output_graph.pbに出力されラベルファイルが/tmp/output_label.txtに出力される


これで学習が完了したので実際にラベリングして確認してみる
先ほどと同じようにビルドと実行を行う

bazel build tensorflow/examples/image_retraining:label_image
bazel-bin/tensorflow/examples/image_retraining/label_image \
--graph=/tmp/output_graph.pb --labels=/tmp/output_labels.txt \
--output_layer=final_result:0 \
--image=$HOME/flower_photos/daisy/21652746_cc379e0eea_m.jpg

これで結果が出れば完了

bazelのインストール

なんとかアプリもリリースできたので次はディープラーニングだと思ってとりあえずデモ動かしてみることにした.
まずはビルド環境の導入.

Tensorflow導入のためにビルドツールBazelのインストールを行った
最初仮想環境のUbuntuでやろうとしたらメモリ不足で固まったのでBash on Windowsで行った
基本的に公式のチュートリアル(Ubuntu)の通りにやったらできた.

まずJDKのセットアップ.

sudo apt-get install openjdk-8-jdk

最新は9らしいが8を入れないと失敗する.
またUbuntu環境でやったときは先に2のリポジトリ追加を先にやったら失敗した

次にリポジトリの追加を行う

echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add -

そして普通にインストール

sudo apt-get update && sudo apt-get install bazel


これで成功
入れるJDKのバージョン間違えたり先にリポジトリ追加しちゃったりして無駄に苦労してしまった