Kerasで作った簡単なモデルをCoreMLを使ってiOSで動作させる

CoreML Toolsを理解するために、自分で作ったシンプルなモデルを使えば、自分で好きなようにモデルを変更して試せるので良いのではないかと思い試してみました。その手順を説明します。

### 作るもの

数字2組を与えたら、それらを足した結果を予測するモデルを作ります。

<図1>
<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/64eafd9b-0277-6627-5f15-ff4d0eafebfe.png">


### 手順

今回のコードの完全版はこちらに保管してあります。
https://gist.github.com/TokyoYoshida/bab3d0396c05afce445852d2ae224cf4

**1.Google Colabを起動する**

Google Colaboratoryのサイトにアクセスします。
[Google Colaboratory](https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja)

**2. 必要なものをインストール & インポートする**

coremltoolsのバージョンにあわせてtensorflow, kerasもインストールしています。

```python:notebook
!pip install tensorflow==1.14.0
!pip install -U coremltools
!pip install keras==2.2.4
```

そして必要なモジュールをインポートします。

```python:notebook
from keras.models import Sequential
from keras.layers import Dense, Activation
import numpy as np
import math
from keras.utils import np_utils
import keras
```

**3. Tensorboardの準備をする**

CoreML Toolsで変換をするとき、モデルの情報が必要になることがあります。
そのためにはある程度モデルを理解する必要がありますが、Tensorboardで可視化しておくと、モデルの情報を理解する助けになるため、その準備をしておきます。
（今回は自分でモデルを作っているので、モデルの情報は要りませんが）

参考：
[[TF]KerasからTensorboardを使用する方法](https://qiita.com/supersaiakujin/items/568605f999ef5cc741be)

```python:notebook
!mkdir logs
tb_cb = keras.callbacks.TensorBoard(log_dir='./logs', histogram_freq=0, batch_size=32, write_graph=True, write_grads=False, write_images=False, embeddings_freq=0, embeddings_layer_names=None, embeddings_metadata=None)
cbks = [tb_cb]
```

**4. モデルを作り学習させる**

入力データして、2つの整数の組み合わせを1万組作成します。
教師データは今回は分類問題として扱うため、[0,1,2,3,4・・17,18]という18個の配列になります。（答えの組み合わせとして9 + 9 = 18が最大値のため）
この配列には、答えとなる部分だけに1が入り、それ以外は0が入ります。このような表現方法を[One-hot表現](https://ja.wikipedia.org/wiki/One-hot)と呼びます。

<図2>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/8c7a1ea6-7063-74e6-8c74-f94ef0469137.png)


モデルは、シンプルな全結合レイヤーを３つ、活性化関数はsoftmaxを使用します。

参考：
[Kerasで1桁の足し算](https://qiita.com/TakenoriHirao/items/d889e11dbec4d3d7423e)

```python:notebook
x = np.random.randint(0, 10, (10000,2))
y = np_utils.to_categorical(np.sum(x, axis=1))

model = Sequential()
model.add(Dense(512, activation='relu', input_dim=2))
model.add(Dense(256, activation='relu'))
model.add(Dense(y.shape[1]))
model.add(Activation("softmax"))
model.compile('rmsprop',
              'categorical_crossentropy',
              metrics=['accuracy'])


train_rate = 0.7
train_len = math.floor(len(x) * train_rate)
trainx = x[0:train_len]
trainy = y[0:train_len]
testx = x[train_len:]
testy = y[train_len:]

history = model.fit(trainx, trainy,
                    batch_size=128,
                    epochs=100,
                    verbose=1,
                    callbacks=cbks,
                    validation_data=(testx, testy))
```

上のコードを実行すると学習が始まります。

```python:notebook
7000/7000 [==============================] - 0s 54us/step - loss: 0.1705 - acc: 0.9677 - val_loss: 0.0172 - val_acc: 1.0000
Epoch 99/100
7000/7000 [==============================] - 0s 53us/step - loss: 0.0804 - acc: 0.9804 - val_loss: 0.0069 - val_acc: 1.0000
Epoch 100/100
7000/7000 [==============================] - 0s 57us/step - loss: 0.0745 - acc: 0.9806 - val_loss: 0.0062 - val_acc: 1.0000
```

今回はトレーニングデータとテストデータは分割しているものの、実際にはかぶりがあるので、テストデータによる検証結果(val_acc)はあまり当てにならないです。

**5. Tensorboardで可視化してみる**

Tensroboardを読み込んで、実行します。
なぜか`tensorboard-plugin-wit`をアンインストールしないとエラーがでるので、アンインストールしておきます。

```python:notebook
%load_ext tensorboard
!pip uninstall tensorboard-plugin-wit
%tensorboard --logdir ./logs
```

学習の状況

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/d51ca4b2-e357-f939-4de6-ea345fb499c2.png">

グラフの情報

レイヤーは下に入力(dense_1)、上に出力（activation_1)というようにレイヤーが下から上に向かっています。

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/d99b8c33-0752-8faa-73af-662c7a35552e.png">

入力側（dense_1）を見てみます。

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/a94111ef-917a-d015-1c71-b7ba3a6070d3.png">

OperationがPlaceholderとなっており、ここにデータが入ることがわかります。
dtypeはDT_FLOATとなっています。今回のデータは整数で作っていますが、小数以下のデータも扱えます。
shapeは、{"dim":{"size":-1},"size":2]}となっています。つまり(-1,2)のshapeということですね。
-1は任意の値という意味になります。2は、2つの文字の組み合わせを入力しているためです。
<図.2>の入力データのところで、2つの数字を組み合わせxテストデータ数（任意）ということと一致します。

出力側（activation_1）を見てみます。
activation_1はsoftmax関数なので、１つ前のdense_3が出力した19個の数字に対してsoftmax関数の計算結果を出し、loss、metrics、trainingに出力しています。

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/a45e18c2-55a5-82e6-da87-60cf1c286d40.png">

モデルの情報は、kerasのsummaryメソッドでも出力できます。
Tensorboardではレイヤーが下から上に向かっていましたが、こちらは上から下に向かっていますね。

```python:notebook
model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 512)               1536      
_________________________________________________________________
dense_2 (Dense)              (None, 256)               131328    
_________________________________________________________________
dense_3 (Dense)              (None, 19)                4883      
_________________________________________________________________
activation_1 (Activation)    (None, 19)                0         
=================================================================
Total params: 137,747
Trainable params: 137,747
Non-trainable params: 0
```

**6. Notebook上で予想してみる**

iOSで動作させる前に、Notebook上でうまく動作しているか確認します。
適当に2つの数字の組み合わせを与えると、正しく計算できていることがわかります。

```python:notebook
np.argmax(model.predict(np.array([[7,6]])),axis=1)
// array([13])
np.argmax(model.predict(np.array([[1,3]])),axis=1)
// array([4])
```

**7. Core MLに変換する**

上で作ったモデルを保存しておきます。
(これをしなくても既にモデルは得られているのでそのままCore MLに変換することもできます。)


```python:notebook
model.save('my_model.h5')
```

読み込んで変換します。

```python:notebook
from keras.models import load_model
keras_model = load_model('my_model.h5')
from coremltools.converters import keras as converter

# 予想結果の数字の分類ラベルを作る["0","1","2"..."18"]
class_labels = np.arange(0, 19).astype('unicode').tolist()

# 変換
mlmodel = converter.convert(keras_model, # 変換対象のモデル
  output_names=['digitProbabilities'], # 予想の出力に名前をつける。swiftから変数名としてアクセスできるようになる
  class_labels=class_labels, # 予想結果の分類ラベル
  predicted_feature_name='digit' # 分類の出力に名前をつける。swiftから変数名としてアクセスできるようになる
)
```


保存します。

```python:notebook
coreml_model_path = 'my_model.mlmodel'
mlmodel.save(coreml_model_path)
```

このあたりのコードは、こちらの本に書いてあるものをそのまま使っています。

[Core ML Tools実践入門 - iOS × DEEP LEARNING](https://booth.pm/ja/items/1723495)
<img width="200px" src="https://booth.pximg.net/74c0c65b-cd71-4b0d-9263-f3095f3e17a0/i/1723495/b9d1423c-b98d-4a91-bfa4-589e3ec06c16_base_resized.jpg">


**8. Core MLモデル（.mlmodelファイル）をダウンロードする**

Notebookから下のように選択して「ダウンロード」を選択してダウンロードします。

<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/7a952aa0-cea1-a2c6-cb10-d5c5bb30cb09.png">


**9. XcodeのプロジェクトにDrag & Dropする**

Xcodeを起動して、プロジェクトの作成から「Single View App」を作成します。

今回作ったプロジェクトはgithubにあるので、こちらを使っても良いです。
[TokyoYoshida/CoreMLSimpleTest](https://github.com/TokyoYoshida/CoreMLSimpleTest)

プロジェクトの、任意の位置に.mlmodelファイルをDrag & Dropします。
<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/ceaed4f9-e615-d44c-438b-7a2334208c43.png">

Xcode内でモデルを選択すると、プレビューを見ることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/2783f82a-da88-6479-6876-b06eab98b7c0.png)

入力がMultiArray型で2つのDoubeを取ります。これは例えば、2+3を予想させたいなら、[2,3]を与えます。
出力は、digitProbabilitiesは、Dictionaryで文字列がKey、DoubleがValueになっています。この項目は予想結果で、数字のラベルごとの確率が出力されます。
digitは、予想結果を数字のラベルに当てはめた結果です。

なお、MultiArray型は、Core ML内に定義されているモデルの入力や出力として使用する多次元配列です。

**10. Core MLを使った推論のコードを書く**

今回のモデルは、画像認識ではないのでVisionFrameworkなどは用いず、Core MLを直接操作します。
ViewControllerのviewDidLoadにコードを書いていきます。

```swift:ViewController.swift
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let model = my_model()
        // 予想したい数字のペアをつくる
        let inputArray = try! MLMultiArray([2,3])
        let inputToModel: my_modelInput = my_modelInput(input1: inputArray)
        // 推論する
        if let prediction = try? model.prediction(input: inputToModel) {
            // 結果の出力
            print(prediction.digit)
            print(prediction.digitProbabilities)
        }
    }
}
```

**11. 実行する**

アプリを実行すると無駄にアプリの画面が出ますが、気にせずXcodeのOutput欄を見ます。

```swift:結果出力
5
["13": 1.401298464324817e-45, "7": 4.403268860642129e-08, "16": 0.0, "12": 1.401298464324817e-45, "10": 1.401298464324817e-45, "4": 2.876720373024e-06, "11": 1.401298464324817e-45, "1": 1.2956196287086532e-23, "6": 6.624156412726734e-06, "8": 6.452452973902557e-18, "15": 1.401298464324817e-45, "2": 7.265933324842114e-14, "0": 1.0373160919090815e-33, "18": 0.0, "9": 1.7125880512063084e-34, "17": 0.0, "3": 1.129986526746086e-15, "14": 1.401298464324817e-45, "5": 0.9999904632568359]
```

2+3を与えているので、5が推論されていますね。
digitProbabilitiesの出力では、各ラベルをキーとして確率が出力されています。
5である確率は0.9999904632568359なので、ほぼ1になっています。
それ以外の数字、例えば13である確率は、1.401298464324817e-45となっていますがこれは浮動小数の表現で1.401298464324817✕10の-45乗なのでほぼゼロという結果になっています。

### 最後に

NoteではiOS開発について定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshida

Twitterでは簡単なtipsを発信しています。
https://twitter.com/jugemjugemjugem