SwiftでTensorflowLiteを試してみる

# はじめに
私用でTensorflowLiteを使いそうなので、iOSで使えるか試してみました。

## 環境
Python3.7
Swift 5.2.2
Xcode 11.4.1
iOS 13.4

# TensorflowLiteとは
![tensorflowlite intro.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278251/de4baf63-13de-cf15-f972-31d221b7ebc5.png)

IoTデバイスやスマホなどで、Tensorflowで開発したモデルを利用することができるものらしいです。
詳細は良くわからないのですが、TensorflowLiteで学習してモデルを構築なんてことはできないそうですね。

## 公式のサンプル
一応、公式のサンプルプロジェクトがあります。
[iOSのクイックスタート](https://github.com/tensorflow/examples/tree/master/lite/examples/image_classification/ios)
### 動くは動くのだが....
<img width="200" alt="1B91A8E3-3516-4491-955F-34DADA84B407_1_102_o.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278251/a46d9556-673d-bf1c-3099-5eedde264e25.jpeg">
サンプルプロジェクトはビルドすれば動くのですが、自分でTensorflowLiteを動かした感覚が皆無だったため、自分で作ったモデルをスホマで動くかを試してみました。

# 本編
## 1. Tensorflowでモデルを作る
まずは、スマホで使う機械学習モデルを作らなければなりません。
全コードは載せておきますが、細かい説明は省略します。
そもそも機械学習初心者なので、モデルは適当です。

<details><summary>サンプルコード</summary><div>

```python
#%%

import tensorflow as tf
import tensorflow.keras as keras
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import math

#%%

pi = math.pi
t = 10 * np.sin(np.linspace(0, 2 * pi, 1000)) # sin波作成
data_set = []
for i in range(500):  # 1000個のsin波を作成
    x = np.linspace(0, 2 * pi, 1000)
    y = np.sin(x) + np.random.randn(1000) * 0.01  # 乱数かけて調整
    data_set.append(np.array([x, y]))
data_set = np.array(data_set)
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.scatter(data_set[0, 0, :], data_set[0, 1, :], marker=".")
plt.show()

#%%

model = keras.Sequential([
    keras.layers.Flatten(input_shape=(1000,)),
    keras.layers.Dense(1000, use_bias=True)
])

# loss=平均二乗誤差
# optimizer -> 最適化関数, loss -> 誤差関数, metrics -> モデル作成時、コンソールに表示するパラメータ(任意）
model.compile(optimizer='adam',
              loss='mean_squared_error')

#%%

# animation用のfig作成
ims = []
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.set_ylim(-1.5, 1.5)

# トレーニング実行
TRAIN_NUM = 10
for _ in range(TRAIN_NUM):
    fit = model.fit(
        data_set[:, 0], # training-data
        data_set[:, 1], # training-label
        epochs=1)  # 繰り返す回数

    test_data = np.linspace(0, 2*pi, 1000)
    predictions = model.predict(np.expand_dims(test_data, 0))

    im = ax.scatter(test_data, predictions[0], marker='.', c='b')
    ims.append([im])


#%%

interval = 100
ani = animation.ArtistAnimation(fig=fig, artists=ims, interval=interval)
ani.save('./output/ani_tensorflow2-turorial.gif', writer='imagemagick')
# ani.save('./output/ani_tensorflow2-turorial.mp4', writer='ffmpeg')

#%%

# tfliteモデルを作成
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
open("data/converted_model.tflite", "wb").write(tflite_model)

```

</div></details>

#### sin波を学習しただけです
X軸の0~2πまでを1000個の座標に分け、それに対するY軸の値を学習させています。
![ani_tensorflow2-turorial.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278251/d5c8253f-943b-e8bd-2aab-02402e5fc666.gif)


### `.tflite`ファイルを作成する。
重要なのはこここです。
IoTデバイスやスマホでモデルを使うには、 `.tflite`ファイルという形でモデルファイルを保存します。
コードは以下の通りです。

#### kerasの場合
```python
model = keras.Sequential([
    keras.layers.Flatten(input_shape=(1000,)),
    keras.layers.Dense(1000, use_bias=True)
])
# ~~~
# tfliteモデルを作成
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
open("data/converted_model.tflite", "wb").write(tflite_model)
```

`.tflite`ファイルの保存方法は使うモデルによってさまざまです。
[TensorFlow Lite コンバータ](https://www.tensorflow.org/lite/convert?hl=ja)

## 2. スマホ側で学習モデルを利用する
次に先ほど作成した`.tffile`をスマホ（iOS）で使っていきます。
今回作ったプロジェクトはGithubに載せておきます。
[TEST Project](https://github.com/namid11/TEST_Get-Model-on-TensorflowLite/tree/master/TEST_Get-Model-on-TensorflowLite)

### TnesorflowLiteをインストール
プロジェクトを作成したら、podを使って`TnesorflowLite`をインストールします。

```text:Podfile
pod 'TensorFlowLiteSwift'
```

### プロジェクトに`.tffile`を入れる
先ほど作成した`.tffile`をプロジェクトに入れてます。
<img width="271" alt="スクリーンショット 2020-04-27 9.50.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278251/e8116a4e-d09d-925b-2655-9b18d9c0d690.png">

## 3. 実行コード
最初に全体の流れを見せておきます。
### 全体コード

```swift
// モデルファイルのパスを取得
guard let modelPath = Bundle.main.path(
	  forResource: "converted_model",  // ファイル名
	  ofType: "tflite"  // 拡張子
) else {
	  print("Failed to load the model file with name: ~")
	  return
}

do {
  // Interpreterでモデルファイルを読み込む
  interpreter = try Interpreter(modelPath: modelPath)
  // メモリ確保。これはモデル読み込み直後に必須
  try interpreter?.allocateTensors()
} catch let error {
  print("Failed to create the interpreter with error: \(error.localizedDescription)")
  return
}

guard let intp = interpreter else {
    return
}

do {
    // X（入力）データを作成。 Data型（byte列）で作成する必要あり。（後ほど説明）
    let inputData = createXData()  
    // Interpreterにデータを入力
    try intp.copy(inputData, toInputAt: 0)　　
    // 実行
    try intp.invoke()
    // 出力データ取得
    let outputTensor = try intp.output(at: 0)  // Tensor型
    print(outputTensor)
    // 出力のData型のデータから、求める形にデコード
    let outputValues: [Float32] = decodeData(output: outputTensor.data)
    print(outputValues)
    // XとYを元にグラフ描画
    dispSinWave(X: createXLine(), Y: outputValues)
} catch let error {
    print("error:", error)
}
```

### 説明
#### `Interpreter`で学習モデルを読み取る
モデルへのパスを渡して、Interpreterインスタンスを作成します。

```swift
interpreter = try Interpreter(modelPath: modelPath)
// メモリ確保。これはモデル読み込み直後に必須
try interpreter?.allocateTensors()
```

#### 入力データ作成
`createXData`メソッドを作り、入力データを作成しています。
後ほど説明しますが、入力データはData型にする必要があります。（これで１日とられました...）

```swift
let inputData = createXData()  
```

#### `Interpreter`にデータを入力
`copy`メソッドで入力データを設定します。
`toInputAt`は、入力データが複数ある場合に指定します。
今回は入力は１つなので、０でOKです。

```swift
try intp.copy(inputData, toInputAt: 0)
```

#### 実行
```swift
try intp.invoke()
```

#### 出力データ取得
実行したら、`output`メソッドで出力を取得します。
`at`は出力が複数ある場合に指定します。

```swift
// 出力データ取得
let outputTensor = try intp.output(at: 0)  // Tensor型
```

また、出力はTensor型であり、以下のように出力の型などがわかります。
<img width="411" alt="スクリーンショット 2020-04-27 10.17.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278251/99252444-a0ad-6f1f-f1a5-708f75a2b959.png">

#### 出力をData型から変換
今回の出力は、Float32の配列であるため、`decodeData`メソッドで変換してます。
メソッドの中身は後ほど見せます。

```swift
// 出力のData型のデータから、求める形にデコード
let outputValues: [Float32] = decodeData(output: outputTensor.data)
```

### 入力、出力の作成
先ほど使っていた`createXData`と`decodeData`メソッドの説明です。

#### 入力 - `createXData`
入力、出力ともにData型で作成します。
今回の入力データはFloat32の配列であるため、まずFloat32の値をData型に変換し、その配列をもう一度Data型に変換しています。

```swift
private func createXData() -> Data {
    var bytes: [UInt8] = []
    let xLine = createXLine()
    for var value in xLine {
	  // FloatをData型に変換
        let valueBytes = Data(bytes: &value, count: (Float32.significandBitCount + Float32.exponentBitCount + 1) / 8)
	  // Data型をByte配列に
        let bytesArray = Array<UInt8>(valueBytes)
        // Byte配列を足していく
        bytes += bytesArray
    }
    return Data(bytes: bytes, count: bytes.count)
}

// 0 ~ 2PI までを1000個の座標に分けた配列
private func createXLine() -> [Float32] {
    var array: [Float32] = []
    let NUM = 1000
    let delta = 2*Float.pi / Float(NUM)
    for i in 0 ..< NUM {
        let value = Float32(i)*Float32(delta)
        array.append(value)
    }
    return array
}
```

#### 出力 - `decodeData`
出力もすべてData型なので、本来の[Float32]に直していきます。
Float32は4bytesなので、Data型の出力データから4bytesずつ取り出しFloat32型に変換しています。

```swift
private func decodeData(output: Data) -> [Float32] {
    var decodeValue: [Float32] = []
    let float32Bytes = (Float32.significandBitCount + Float32.exponentBitCount + 1) / 8   // Float32のバイト数
    for i in 0 ..< output.count / float32Bytes {
	// Float32のバイト数をとる
        let data = output[i*float32Bytes..<(i+1)*float32Bytes]  
	// バイト列からFloat32に変換
        let value = data.withUnsafeBytes { $0.load(as: Float32.self) }
        decodeValue.append(value)
    }
    
    return decodeValue
}
```

## 結果
私が作ったプロジェクトはボタンを押したらレイヤーが表示されます。

<img width="529" alt="スクリーンショット 2020-04-27 10.24.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278251/768f6abe-8621-e3f9-b5e4-0333b560679d.png">

# 参考記事
[初心者に優しくないTensorflow Lite の公式サンプル](https://qiita.com/yohachi/items/434f0da356161e82c242)
[iOS - quickstart](https://www.tensorflow.org/lite/guide/ios?hl=ja)
[TensorFlow Lite image classification iOS example application](https://github.com/tensorflow/examples/tree/master/lite/examples/image_classification/ios)