15分で作る機械学習Androidアプリ ! !

#今回やること

**15分で機械学習(画像分類)アプリを作ってみる!**
Android Studio 4.1でリリースされたML関連の機能を使用してサクっと作ってみます。
作るものはこんな感じ↓
<img width="280" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/2dce916a-4de0-5ce1-670e-a691293da4dd.jpeg"/> <img width="280" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/63350208-239f-f4e7-acc0-cc401de49dd8.jpeg"/>
<img width="280" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/1da06b54-2af3-d18f-afa6-4f1a40d44c46.jpeg"/> <img width="280" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/2a3c6aa6-f0bb-a412-cafe-ae16f72a21d9.jpeg"/>

↑見て分かる通り、精度的にまだまだ改善の余地ありですが、、、
githubとapkを今記事最下部に置いていますのでとりあえず試したいという方はそちらからどうぞ。

# Android Studio 4.1 ML-Binding
皆さんご存じの通り最近いろいろML関連のサポートが充実し始めてきました。
Android Studio 4.1 では `tensorflow liteモデル`を簡単に組み込めるようにする機能が追加されました。([公式リリースノート](https://developer.android.com/studio/preview/features?hl=ja#tensor-flow-lite-models))
内容としてはリリースノートの通りなんですがpick upすると

- **built-inで**tensorflow liteモデルを動かす
- gradleが**自動で推論パイプライン**を生成してくれる 
- tensorflow lite モデルの情報(input shape, output shape, label)をAndroid Studio上で確認できる
- ラベルのマッピングを自動でやってくれる
- tensorflow lite モデルの`metadata`が必要([tensoflow liteからエクスポートできる](https://www.tensorflow.org/lite/convert/metadata)、ただしまだ未対応のモデルも多数あるよう)
- Android Studio 4.1 で対応しているのは`画像分類`と`スタイル変換`のモデルのみ。`物体検知`とかはまだ未対応。([ただしAndroid Studio 4.2からは物体検知もサポートされ始める](https://tfhub.dev/android-studio/collections/ml-model-binding/1))
- コード量が格段に減る

こんな感じになります。
推論パイプライン自動生成を使えば今までbitmapをbufferにallocateしてサイズ合わせてとかの実装をやらなくてもよくなるのでカスタマイズ性は下がりますが、実装の手間は大幅に減ります。
あとは、IDEでモデルの情報を見れるのはそこそこ便利だと感じました。outputとかの把握が大事なのでそれをandroid studioで確認できるのはwikiの整理とかも最小限で済むのでアリですね。
ただし、**まだ対応しているモデルが少ない**のでそこは今後に期待といったところかと思いますが、そもそもtensorflow liteに対応しているモバイルアーキテクチャのモデルが圧倒的に少ないのでやっぱり、「このモデル対応してない」みたいなある程度の制約は覚悟しておく必要があります。

モバイルはリソースの制限があるのでモデルサイズ、パラメータを小さくする必要があるのですが、まぁ、でっかいモデルをモバイル向けに設計するのって簡単じゃないよね。。。
<img width="650" src="https://developer.android.com/studio/images/write/tf-lite-model-viewer.png?hl=ja"/>
[公式ドキュメントより](https://developer.android.com/studio/releases/#4.1-tensor-flow-lite-models)


# Let's 15 min 実装
では、さっそく実装していきましょう

## 使用するモデルを用意
まずは推論に使用したい`学習済みモデル`を[Tensorflow hubから探して](https://tfhub.dev/ml-kit/collections/image-classification/1)ダウンロードします。基本的にapache license 2.0です
ML-bindingは上述したように画像分類と画像変換しか対応していないので画像分類　Classificationのモバイルネットワークモデルを探しましょう。基本的にmetadataとセットになったモデルですがたまにmetadataがない場合があります。その時は[自分で作る](https://www.tensorflow.org/lite/convert/metadata)必要があります。

なお、今回は[このMobileNetV2 224x224](https://tfhub.dev/tensorflow/lite-model/mobilenet_v2_1.0_224/1/metadata/1)を使用します。
`MobileNetV2 224x224 tfliteモデル`をダウンロードして適当なフォルダにおいて用意します。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/ed540ba0-8cc8-37cd-c909-d164cc5d03f7.png)


## ML-binding
Android Studio 上でプロジェクトを右クリックして`  [New] > [Other] > [TensorFlow Lite Model] `を選択し先ほどインストールしたモデルをAndroid Studio にインポートします。
成功すると、`ml`リソースフォルダが新しく`res`と同じ階層に生成されそこからモデルの情報が見れます。metadataが情報不足、またはない場合はここで見れるモデル情報が最小限になるみたいです。
これで準備は終了です。

<img width="650" src="https://developer.android.com/studio/images/write/import-tf-lite-model.png"/>

##推論の実装
必要に応じてAACをgradleに追加しておきます。
あとは、gradle:4.1 以上が必要になります。

```build.gradle
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
```

推論コードを書く

```MainActivity.kt
/**
 * モデルをロードして画像分類を行う
 */
private suspend fun classifyImage(targetBitmap: Bitmap) = withContext(Dispatchers.IO) {
    val model = MobilenetV2.newInstance(this@MainActivity)  // モデルをロード
    val image = TensorImage.fromBitmap(targetBitmap)    // bitmapをテンソルに変換
    val output = model.process(image)  // 推論
    val categoryList : List<Category> = output.probabilityAsCategoryList
    model.close()
    return@withContext categoryList
}
```

この数行で終了です。。。
見たまんまなのですが、インポートしたファイル名でモデルをロードするためのクラスが生成されるのでそれを使って`process`関数を呼び推論し結果を取得するだけです。
当然かもですが、一般にパラメータたくさん持つモデルにデータ渡して推論するのは重い処理なのでコルーチン等使ってメインスレッドでは実行しないようにしましょう。


このoutputとして得られる`Categoryクラス`ですが、内容的には

```java
public final class Category {
    private final String label;
    private final float score;
}
```
こんな感じで`正解ラベルとマッピングされたラベル`と`スコア`が入っています。
スコアはいつも見たくソフトマックスにでも突っ込んで見やすい表示にするかって思ってましたが、おそらくすでに処理されていたのでそのまま使えました。

あとは、普通に関数をコールしてフォーマット整えてviewに反映させるだけです。

```MainActivity.kt
// RUN ボタンクリックで推論結果を表示
inferrenceBtn.setOnClickListener {
    if (bitmap == null) return@setOnClickListener
    lifecycleScope.launch {
        // 推論結果を取得
        val outputs = classifyImage(bitmap!!)
        // viewにフォーマットした推論結果を15個表示
        resultTextView.text = outputs.sortedByDescending { it.score }
            .take(15)
            .joinToString("\n") { category ->
                "Label: ${category.label}, Score: ${"%.2f".format(category.score * 100)}%"
            }
    }
}
```

後は、推論する画像の用意ですが今回は適当にintentでも発行してギャラリーから画像を取得しましょう

```MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // ギャラリーから画像を取得
    showGalleryBtn.setOnClickListener {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            setType("image/*")
        }
        startActivityForResult(intent, GALLERY_REQUEST_CODE)
    }
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    when (requestCode) {
        GALLERY_REQUEST_CODE -> {
            // ギャラリーから画像データの取得
            bitmap = MediaStore.Images.Media.getBitmap(contentResolver, data?.data ?: return)
            previewImg.setImageBitmap(bitmap)
        }
    }
}

```
以上で終わりです
問題なければ冒頭で紹介したものと同じ機能を持つものが作れているはずです。

#おわり
思った以上に簡単に実装できてしまって、mlサポートもここまで来たかぁって感じに思いました。
まだまだ、サポートが始まったばかりで対応している範囲は極々狭いので実際にこのml-bindingを使用してプロダクト作るとかはまだ先になりそうですが、お試しでやる分には手軽で楽しいですね。
2020/12時点でAndroid Studio 4.2-beta1が来ておりそちらを使えば、おそらく物体検知モデルもSSDモバイルアーキテクチャに限り使用できると思います。自分はwindowsでupdateがマニュアルインストールしかまだなかったのであきらめて今回は画像分類にしましたが。mac版1clickでアップデート可能っぽい？

今回のコード: https://github.com/SY-BETA/Simple-Image-Classification-Android-App
サンプルapk インストール(deploy gate): https://dply.me/9wprnq