初めて Tensorflow Lite x Android!

# ACCESS Advent Calendar 24日 へようこそ

レザです。2018年の新卒ですが、８月間ぐらい ACCESS 正社員になって、本当に早かったと思います。
楽しいことや悔しいことがあったけど、良い経験だと思います。
今回の記事では私の最初のAdvent Calendarです (これも私の最初のブログポスト！:fireworks:)。
私の日本語が分かりづらいと思いますが、英語版も念の為書いてあります (皆さんにとってそれの方が分かりやすいかもしれません :sweat_smile:)。
今回の記事では Tensorflow-Lite と Android の話です。そして、試したことは [Fashion-MNIST](https://www.kaggle.com/zalando-research/fashionmnist/home) というデータセットを使って、端末で絵を描いて、Tensorflow-Lite で認識したいと思いま!

# Tensorflow and Tensorflow lite?

皆さんもうご存知だと思いますが、Tensorflow はグーグルさんが作った世界中で一番有名な機械学習ライブラリーです。
TensorflowーLite は Embedded Devicesやスマホなどを対応するために作られたものです。

## Tensorflow と Tensorflow Liteの違い?

(以下の画像は私が１２月７日に参加したミートアップから取ったものです。Laurence Moroney さん (グーグルさんの社員) の発表から取りました)

> TensorFlow Lite is the official solution for running machine learning models on mobile and embedded devices. It enables on‑device machine learning inference with low latency and a small binary size on Android, iOS, and other operating systems. - https://www.tensorflow.org/lite/

Tensorflow は機械学習世界になんでも使えます。モデルを作成、学習する、データ処理、分析などもできました。
問題なのは機械学習、特にディープラーニングが計算量が多いため、パワフルなコンピュータが必要です。なので、スマホとかでするなら時間がかかります。スマホやEmbedded Systemなどでディープラーニングができるように **学習の段階 (Training Phase)**は Tensorflow がやってくれて、**Inference Phase** は Tensorflow Lite 上でやります。

[//]: # (![Tensorflow](tensorflow.png))
<img width="1290" alt="tensorflow.png" src="https://qiita-image-store.s3.amazonaws.com/0/318479/8bbd8dd0-9d89-ca1c-3542-a849273ecf3b.png">

Tensorflow が学習をやってくれて、ルール（モデル）を作成し、できあがたルールを Android に入れ、認識や分析などのために使用できます。

[//]: # (![TFLite](tflite.png))
<img width="1291" alt="tflite.png" src="https://qiita-image-store.s3.amazonaws.com/0/318479/60b46100-c018-ac71-fbae-889057008483.png">

流れとしては以下のようなになります。

[//]: # (![TFLiteFlow](tflite_flow.png))
<img width="1076" alt="tflite_flow.png" src="https://qiita-image-store.s3.amazonaws.com/0/318479/6e7459a4-471a-033d-18c4-20b9ce66148a.png">

# Let's code!

この記事の目的は Tensorflow Lite 用いた、シンプルな機械学習アプリを Android で楽しめるように書きました。
ユーザーが絵を描いて、そして、その絵を認識されるようにしたいと思います。
インタネットでももっと良い記事があるので、ご興味があればぜひそれらも見てみてください。

## モデルを準備する

こちらは Google Colaboratory上でやった方がいいと思います。

https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja
> Colaboratory は、完全にクラウドで実行される Jupyter ノートブック環境です。設定不要で、無料でご利用になれます。

GPU と TPU も無料で使えるので (時間限定)!!! So, why not?

### データセットとの付き合い :heart: 

今回は [Fashion-MNIST](https://www.kaggle.com/zalando-research/fashionmnist/home) というデータセットを使おうと思います。MNIST のような均衡なデータセットで、MNIST では数字の画像ですが、Fashion-MNIST では、名前の通り、着るものです。ズボンとか、ドレスとか、コートとかが入っています。
このデータセットは [Zalando Research](https://research.zalando.com/) が作ったデータセットです。中身は 7万画像で、10クラスのファションアイテムが入っています。

クラスは以下のリストです。
- T-shirt/top
- Trouser
- Pullover
- Dress
- Coat
- Sandal
- Shirt
- Sneaker
- Bag
- Ankle boot

![fashion_mnist.jpg](https://qiita-image-store.s3.amazonaws.com/0/318479/48f2d866-d416-32b1-5ef0-09c69fa4b611.jpeg)

画像のサイズは 28 x 28 のグレイスケールの画像です。

### 学習のコードを準備しよう！

今回はシンプルな畳み込みを Tensorflow の Keras ラッパー を使用すると思います。

#### Imports!!!

```python
# TensorFlow and tf.keras
import tensorflow as tf
from tensorflow import keras

# Helper libraries
import numpy as np
import matplotlib.pyplot as plt
```

#### データの準備

Fashion-MNIST は Keras の中に入っているので、すぐ使えます！
`fashion_mnist.load_data()`から帰ってきた値はもう学習用のデータ (6万画像) とテスト用のデータ (1万画像)が別れています。

```python
fashion_mnist = keras.datasets.fashion_mnist

(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
```

ロードされた物をみてみよう!

<img width="311" alt="Screen Shot 2018-12-24 at 9.51.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/318479/72397852-73b3-3d47-ec4c-99b32f4bf198.png">

靴だ！

#### モデルを作成!

今回のモデルは複雑ではないので、チュートリアルページでもよくあります。
`data_format='channels_last'`は画像データに対して、color channels が一番後ろに置いてあるように追加します。
例えば画像のサイズは `(28, 28)`、このデータフォマットを使うと入力の値は `(28, 28, 1)` グレースケール, か `(28, 28, 3)`  RGB です。

```python
model = keras.Sequential([
    keras.layers.Conv2D(32, kernel_size=(3, 3), 
                        activation=tf.nn.relu, 
                        input_shape=input_shape,
                        data_format='channels_last'),
    keras.layers.Conv2D(64, (3, 3), activation=tf.nn.relu),
    keras.layers.MaxPool2D(pool_size=(2, 2)),
    keras.layers.Dropout(0.25),
    keras.layers.Flatten(),
    keras.layers.Dense(128, activation=tf.nn.relu),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(10, activation=tf.nn.softmax)
])
model.compile(optimizer="Adadelta", 
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
```

サマリーのモデルを見ると、以下のような仕組みが出てきます。
<img width="558" alt="Screen Shot 2018-12-24 at 10.01.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/318479/f3845266-4af4-590d-7f09-26b2fa363b50.png">

もっと詳しい見るなら Tensorboard で見てみよう.

![Model.PNG](https://qiita-image-store.s3.amazonaws.com/0/318479/c7d6eb2b-edb9-4af6-9344-25959fcfdbd0.png)

モデルは作成した後に、もちろん学習ですね。こんなモデルってどれぐらいできるの。。。Epoch 10 までを見ると

```
Epoch 1/100
60000/60000 [==============================] - 6s 102us/step - loss: 0.5628 - acc: 0.8017
Epoch 2/100
60000/60000 [==============================] - 5s 82us/step - loss: 0.3599 - acc: 0.8730
Epoch 3/100
60000/60000 [==============================] - 5s 82us/step - loss: 0.3118 - acc: 0.8885
Epoch 4/100
60000/60000 [==============================] - 5s 82us/step - loss: 0.2798 - acc: 0.9006
Epoch 5/100
60000/60000 [==============================] - 5s 82us/step - loss: 0.2553 - acc: 0.9084
Epoch 6/100
60000/60000 [==============================] - 5s 82us/step - loss: 0.2351 - acc: 0.9161
Epoch 7/100
60000/60000 [==============================] - 5s 82us/step - loss: 0.2186 - acc: 0.9210
Epoch 8/100
60000/60000 [==============================] - 5s 83us/step - loss: 0.2074 - acc: 0.9247
Epoch 9/100
60000/60000 [==============================] - 5s 82us/step - loss: 0.1980 - acc: 0.9293
Epoch 10/100
60000/60000 [==============================] - 5s 82us/step - loss: 0.1846 - acc: 0.9339
```

Epoch 10 で 0.9339 accuracy. 結構良いですね! 100 epoch なら......

```
Epoch 100/100
60000/60000 [==============================] - 5s 85us/step - loss: 0.0569 - acc: 0.9807
```

0.9807! そうすると学習用なデータで学習すると、98% 確率で認識できています。
テスト用のデータセットなら。。

```
10000/10000 [==============================] - 1s 68us/step
Test accuracy: 0.9324
```

93%. 悪くないですね! 次はそのモデルを保存しよう. 
これでも Keras で楽にできます。`model.save(file_name.h5)` を書けば、HDF5フォマットで学習モデルを保存され他の様にも使える様になりました!
Tensorflow-Lite と連携できるため、h5 のフォマットを tflite フォマットに変更することが必要です。
それをするために、いくつか方法があります。
There're several ways to do the conversion. 
- By Command Line
`tflite_convert --output_file=test.tflite --keras_model_file=fashion_mnist.h5`
- By Python code

```Python
import tensorflow as tf

# In Tensorflow 1.12, lite still under contrib package
converter = tf.contrib.lite.TFLiteConverter.from_keras_model_file("file.h5")
tflite_model = converter.convert()
open("file.tflite", "wb").write(tflite_model)
```

**注意**:
Mac ・ Linux でうまく動いているが、Windows でやった時にエラーが出てきました。Windows ではまだ対応されていなさそうです。Windows で作業するなら、Google Colaboratory を使用して上記のスクリプトを実行し、tflite ファイルを保存することにした方が楽かもしれません。
Colaboratory　を使うと、上記のため Google Colaboratory を使うと Google Drive の権限が必要です。

```python
# Load the Drive helper and mount
from google.colab import drive
drive.mount('/content/drive')
```

mount された後にファイルパスは `/content/drive/My Drive/path_to_file` のようになっています。
Conversion が終わったら、tflite をコンピュータに保存し、Android を始めよう！ここまでは Tensorflow の役割は終わりです。

## Preparing the Android

注意：
1. WARNING: The API is currently experimental and is not covered by TensorFlow API stability guarantees. 
2. Kotlin にはまだ公式対応されていないが、Moroney さんによると、今実装中ですが、いつリリースするのがまだ分かりません。

今回の記事では絵を書くことですが、その仕組みをあまり詳しく書かないので、以下のリンクに見てみると幸いです。
https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/graphics/FingerPaint.java

### Prepare Tensorflow lite

To make Tensorflow works, you need to add Tensorflow-lite at Gradle File like below.
Tensorflow lite が動けるように以下の設定が必要です。
```gradle
dependencies {
    ...
    implementation 'org.tensorflow:tensorflow-lite:+'
    ...
}
```

tflite ファイルのため "no compression" フラグも追加します.

```gradle
aaptOptions {
    noCompress "tflite"
    noCompress "lite"
}
```

先ほど作った tflite ファイルは assets フォルダに入れます。ラベルのために、.txt ファイルを作成し、1行は１クラスになります。並び順番としては、学習と同じ重ん版です。なので今回は以下の通りです。

```
T-shirt/top
Trouser
Pullover
Dress
Coat
Sandal
Shirt
Sneaker
Bag
Ankle boot
```

分析するため、org.tensorflow.lite.Interpreter を instantiate して、`interpreter.run(image, output)`を実行することです。

[Interpreter](https://www.tensorflow.org/api_docs/python/tf/contrib/lite/Interpreter) を初期化から始めよう!

Interpreter を初期化するために、先ほど `assets` の所に入れたモデルをアプリにロードすることが必要です。
Interpreter は以下の方法でモデルを渡し、[初期化](https://www.tensorflow.org/lite/apis#loading_a_model_2) することができます。 
`public Interpreter(@NotNull File modelFile);`
か MappedByteBuffer:
`public Interpreter(@NotNull MappedByteBuffer mappedByteBuffer);`

最初に`File`の方法をやってみましたが、失敗して、[例](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/examples/android/app/src/main/java/org/tensorflow/demo/TFLiteImageClassifier.java#L66) から取ってきたコードを使うことにしました。

```java
private static MappedByteBuffer loadModelFile(AssetManager assets, String modelFilename)
      throws IOException {
    AssetFileDescriptor fileDescriptor = assets.openFd(modelFilename);
    FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
    FileChannel fileChannel = inputStream.getChannel();
    long startOffset = fileDescriptor.getStartOffset();
    long declaredLength = fileDescriptor.getDeclaredLength();
    return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
}
```
上記で interpreter の初期化ができました。次は `run` の部分です。
`run` を実行するために 2変数が必要です。認識したい画像と出力のコンテナです。
まずは画像から準備しよう。以下のようなビューで画面上でドローができるようになりました。

```java
public class DrawingView extends View {

    private static final float TOUCH_TOLERANCE = 4;

    private Context mContext;

    private Bitmap mBitmap;
    private Canvas mCanvas;
    private Path mPath;
    private Paint mBitmapPaint;
    private Paint mPaint;
    private float mOldXPos;
    private float mOldYPos;

    public DrawingView(Context context) {
        super(context);
        initialize(context);
    }

    public DrawingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initialize(context);
    }

    public DrawingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initialize(context);
    }

    public DrawingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }


    private void initialize(Context context) {
        mContext = context;
        mPath = new Path();
        mBitmapPaint = new Paint(Paint.DITHER_FLAG);
        mPaint = new Paint();
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(0xFF000000);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(12);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldW, int oldH) {
        super.onSizeChanged(w, h, oldW, oldH);
        mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mBitmap);
        mCanvas.drawColor(Color.WHITE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
        canvas.drawPath(mPath, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float xPos = event.getX();
        float yPos = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                resetDrawPath(xPos, yPos);
                invalidate();
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                drawPath(xPos, yPos);
                invalidate();
                break;
            }
            case MotionEvent.ACTION_UP: {
                resetTouch();
                invalidate();
                break;
            }
        }
        return true;
    }

    private void resetDrawPath(float x, float y) {
        mPath.reset();
        mPath.moveTo(x, y);
        mOldXPos = x;
        mOldYPos = y;
    }

    private void drawPath(float x, float y) {
        float dx = Math.abs(x - mOldXPos);
        float dy = Math.abs(y - mOldYPos);
        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
            mPath.quadTo(mOldXPos, mOldYPos, (x + mOldXPos)/2, (y + mOldYPos)/2);
            mOldXPos = x;
            mOldYPos = y;
        }
    }

    private void resetTouch() {
        mPath.lineTo(mOldXPos, mOldYPos);
        mCanvas.drawPath(mPath, mPaint);
        mPath.reset();
    }

    public void resetCanvas() {
        mCanvas.drawColor(Color.WHITE);
        invalidate();
    }

    public Bitmap getBitmap() {
        return mBitmap;
    }
}
```
上記のビューで画面をタッチしたら、ドロー始まり、指を動かしたら、黒い線をドローし、指を画面から離れたら止まります。
例え、皆さんが同じ方法で作ると、取った Bitmap は背景が白で画像が黒です。そうしたら学習データと違って、反転しなければなりません。背景は黒いで絵は白いになります。

From 
<img width="338" alt="Screen Shot 2018-12-24 at 11.26.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/318479/350aa58f-1b6b-64e8-092c-561c925bc95a.png">
To
<img width="336" alt="Screen Shot 2018-12-24 at 11.26.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/318479/eb44503c-b210-3c93-8088-02f660367a84.png">

上記で書きましたが、fashion-MNIST の画像サイズは 28x28 ピックセルです。先ほど作られたモデルの入力サイズも 28x28 です。絵をそのまま取ったら、大きすぎるので、縮小しなければなりません。そうして、color channel も初期化しなければなりません。なので以下のような ByteBuffer を準備しましょう!

[reference](https://github.com/tensorflow/tensorflow/blob/25b4086bb5ba1788ceb6032eda58348f6e20a71d/tensorflow/contrib/lite/java/demo/app/src/main/java/com/example/android/tflitecamerademo/ImageClassifierFloatInception.java#L74
)

```java
// 32-bit float value requires 4 bytes
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4 * batchSize * imageWidth * imageHeight * colorChannel);
```

画像は白黒なので、color channel が　1 に設定し、認識したい画像も 1 つだけなので、1に設定します。
次は output のコンテナーです。
このコンテナーは floating point の 2 次元配になります。
```java
float[][] result = new float[1][mLabels.size()];
```
1 に設定する理由は 1 だけの画像を認識したいからです。`mLabels.size()` はクラスごとに、prediction の結果を見られます。
結果はラベルの順番と同じです。

### Result

#### Success Story and Failed Story

上手くできた所と上手くできなかった所があります。もちろん私が絵を描けないと言う影響もあると思います。
以下はスクリンショットです。

**Success**
<img src="https://qiita-image-store.s3.amazonaws.com/0/318479/3a7a485c-7df2-8f04-ac62-401cbc5f4b09.png" width=40%>

<img src="https://qiita-image-store.s3.amazonaws.com/0/318479/0fa48ce5-2cea-cded-f935-2b6fc1dc2855.png" width=40%>

**Failed**
<img src="https://qiita-image-store.s3.amazonaws.com/0/318479/5f39bd01-9722-907c-4e00-dcbeb9b8243a.png" width=40%>

これはカバンではなくドレスを描きたいですね :(


#### Performance

真面目に測っていませんが、全処理入っていませんが、inference の所のみで、24 ~ 128 ms で結果が出ました。
28 x 28 の小さな画像もありますが、結構早いと思います。

#### Challenge

この実装でチャレンジとしては画像のサイズです。実験のため、Nexus 6P を使って、Bitmap のサイズが 1440x2112 ぐらいで、context を無くさないで、28 x 28 に縮小すると結構難しいです。
例えば以下のような画像です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/318479/35be048f-e406-60d3-bdc2-893af3d2f2d8.png" width=30%>

28 x 28 に縮小すると以下のような画像になります。

<img width="128" src="https://qiita-image-store.s3.amazonaws.com/0/318479/ed605b65-43b7-4a29-42bd-014d25d2c4cd.png">

結構違いますね。。。

## End

* 端末で直接 inference をするのがメリットがあります。特にセキュリティーです。インタネットに繋がっていなくても処理できますし、安全性があります。
* 端末でinference をするともちろんトレード・オフがあります。例えば、精度が落ちるとか、画像がもっと大きいなら、遅くなるとか、ビデオで frame ごとの処理も遅いかもしれません。こちらの辺はまた次回に話したいと思います。 (実験しないと分かりません :sweat_smile:)
* Tensorflow Liteは Android だけ対応するではなく iOS でも 使えます。


この記事を読んでいただきありがとうございます。日本語が分かりづらかったら申し訳ございません。
こちらも日本語を書く勉強になると思います。
やっと機械学習を使う機会が見つかって楽しかったです。

私から以上となります。メリークリスマス・ハッピーホリデー :santa: 

外部リンク：
https://gist.github.com/rhezaharliman/c5bba018fed19c638306ada3c6d87f37 (英語版)
https://medium.com/tensorflow/using-tensorflow-lite-on-android-9bbc9cb7d69d
https://github.com/tensorflow/tensorflow/tree/master/tensorflow/lite
https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py
https://github.com/nex3z/tflite-mnist-android