Keras + iOS11 CoreML + Vision Framework による、ももクロ顔識別アプリの開発

#はじめに

[一年前](http://qiita.com/kenmaz/items/4b60ea00b159b3e00100)にTensorflowを使って[ももクロ顔識別webアプリ](http://momomind.kenmaz.net)を開発しましたが、今回はWWDC2017で発表された iOS11の CoreML + Vision Framework を使ってももクロメンバーの顔画像をリアルタイムで識別するiOSアプリを作ってみました。

なお、本記事は[iOSDC2017での発表](https://iosdc.jp/2017/node/1292)の元となった記事です。
発表資料は[こちら](https://speakerdeck.com/kenmaz/coremldeaidoruyan-ren-shi-apuriwozuo-rou)

#CoreML / Vision Frameworkとは？

CoreMLはiOS11で導入された機械学習フレームワークです。Kerasなどメジャーな機械学習ライブラリを使ってデスクトップやクラウド上で訓練し、その学習結果をCoreML用に変換しiOSアプリに組み込むことで、デバイス単体で予測や識別処理をさせることができます。

Vision Frameworkは顔検出や矩形検出など高度な画像認識処理を行うAPIを提供してくれるフレームワークです。こちらもCoreML上に構築されているようです。

iOS11のCoreMLやVision Frameworkについては以下を参照。
https://developer.apple.com/videos/play/wwdc2017/506/
https://developer.apple.com/videos/play/wwdc2017/703/
https://developer.apple.com/videos/play/wwdc2017/710/

訓練データは[前回](http://qiita.com/kenmaz/items/4b60ea00b159b3e00100)使ったものをそのまま使っています。ただし学習のプログラムはTensorflowで書いていたのでそのままではiOSに組み込めず、Kerasで学習部分のコードは書き直しました（祝Keras入門）。

# 開発内容

今回のアプリの開発の流れは以下の通り

1. 環境整備
2. Keras(tensorflow backend)を使ってEC2上で学習し、モデルを.h5形式のファイルに出力
3. CoreMLToolsを使って.h5ファイルを.mlmodelに変換
4. iOSアプリに.mlmodelを組み込み、顔識別処理を実装
5. AVFoundation と Vision Framework を使ってiOSデバイスのカメラ画像から顔画像を取得し、顔識別処理に流して結果を画面に表示

各ステップの手順とハマりどころなどを紹介します。

# 環境整備
以下のとおり整備しておきます。

- iOSまわり
    - MacBook Pro / mac OS Sierra
    - Xcode9 beta2
    - iPhone7plus + iOS11 beta2 
- pythonまわり
    - EC2 p2xlargeインスタンス
    - Python2.7 + Keras ~~1.2.2~~ 2.0.5

EC2インスタンスについては、Keras/GPUまわり設定済みのAMI(ubuntu)が転がってたのでそれを利用しました。

~~またCoreMLToolsが現状 python2.7 + Keras1.2.2の組み合わせにしか対応していないためこのような組み合わせになりました。最初Python3とKerasの最新版を使って開発してましたがモデルの変換に失敗してしまいました。注意が必要です。~~
【2017/07/01 追記】6/28にKeras2に[対応したようです](https://forums.developer.apple.com/thread/79505#240767)。本コードも[修正しておきました](https://github.com/kenmaz/momo_mind/commit/a47859b7559bf0a8ad1c024626b0e25ab808f5df)。
# Kerasで訓練
まずは訓練データの準備が必要です。インターネット上から適当にももクロの画像をかき集めてきてOpenCVで顔部分をくり抜いて、適当に目の位置やらを全体的に合わせておきます。各メンバーごとに250枚程度、合計1200枚ほどの画像を用意しました。それを3:1くらいで訓練＋テスト用に分け、画像ファイルパスと正解ラベルをセットにしたCSVを生成し、以下のように配置しておきます。ここまでは[前回記事](http://qiita.com/kenmaz/items/4b60ea00b159b3e00100)の使い回し。

```
[momo_mind]$  wc -l deeplearning/train.txt 
     931 deeplearning/train.txt
[momo_mind]$  wc -l deeplearning/test.txt 
     333 deeplearning/test.txt
```

次にKerasで訓練のコードを書くわけですが、単純なCNNで問題ないので、[cifar10のサンプル](https://github.com/fchollet/keras/blob/master/examples/cifar10_cnn.py)をちょろっと改修した程度です。

https://github.com/kenmaz/momo_mind/blob/master/keras/mcz_main.py

```
Epoch 195/200
931/931 [==============================] - 3s - loss: 0.3469 - acc: 0.8851 - val_loss: 1.3892 - val_acc: 0.7027
Epoch 196/200
931/931 [==============================] - 3s - loss: 0.2677 - acc: 0.9098 - val_loss: 1.3042 - val_acc: 0.7508
Epoch 197/200
931/931 [==============================] - 3s - loss: 0.2714 - acc: 0.9087 - val_loss: 1.7264 - val_acc: 0.7417
Epoch 198/200
931/931 [==============================] - 3s - loss: 0.3208 - acc: 0.9012 - val_loss: 1.1210 - val_acc: 0.7778
Epoch 199/200
931/931 [==============================] - 3s - loss: 0.3604 - acc: 0.9012 - val_loss: 1.0405 - val_acc: 0.7808
Epoch 200/200
931/931 [==============================] - 3s - loss: 0.2743 - acc: 0.9184 - val_loss: 1.6675 - val_acc: 0.6456
```
200エポック訓練して、訓練データでの精度90%、テストデータでの精度70%程度、といったところです。今回はiOSアプリに組み込んでアプリとして一通り完成させるのが主目的なので、とりあえずこれでOK。訓練が完了したら以下のコードでネットワークの構造＋重みデータを.h5形式で保存できます。手軽！

```
model.save('model.h5')
```

# mlmodelへの変換
こっからが本番です。まずはCoreMLToolsのインストール。
https://pypi.python.org/pypi/coremltools

```
pip install coremltools
```

次に、以下のようなテキストファイルを作ります。
https://github.com/kenmaz/momo_mind/blob/master/coreml/labels.txt

```
reni
kanako
shiori
arin
momoka
```

CNNの出力値と正解ラベル（0 = れに、1 = かなこ...) を改行区切りにしたテキストです。アプリに組み込んだときにCoreMLが生成するクラスは、予測結果として↑で定義したStringを返してくれます。

.h5->.mlmodelへの変換コードを書きます。
https://github.com/kenmaz/momo_mind/blob/master/coreml/convert.py

```py
path = '../keras/model.h5'

import coremltools
coreml_model = coremltools.converters.keras.convert(path,
        input_names = 'image',
        image_input_names = 'image',
        class_labels = 'labels.txt')

coreml_model.save('Momomind.mlmodel')
```
.h5とlabels.txtをconvertメソッドに食わせてsaveを呼ぶだけです（と言いたいところですが↑のコードには実は少し問題があります。詳細は後述）。
ひとまずこのコードを実行することで、以下のような出力とともに`Momomind.mlmodel`ファイルが生成されます。

```
[coreml] $ python convert.py
0 : convolution2d_input_1, <keras.engine.topology.InputLayer object at 0x1144a1510>
1 : convolution2d_1, <keras.layers.convolutional.Convolution2D object at 0x1144a1410>
2 : activation_1, <keras.layers.core.Activation object at 0x1144eb290>
3 : convolution2d_2, <keras.layers.convolutional.Convolution2D object at 0x1144eb890>
4 : activation_2, <keras.layers.core.Activation object at 0x114513f50>
5 : maxpooling2d_1, <keras.layers.pooling.MaxPooling2D object at 0x1144a1e90>
6 : convolution2d_3, <keras.layers.convolutional.Convolution2D object at 0x11452ff10>
7 : activation_3, <keras.layers.core.Activation object at 0x114583610>
8 : convolution2d_4, <keras.layers.convolutional.Convolution2D object at 0x114572d90>
9 : activation_4, <keras.layers.core.Activation object at 0x1145b4cd0>
10 : maxpooling2d_2, <keras.layers.pooling.MaxPooling2D object at 0x1145dc350>
11 : flatten_1, <keras.layers.core.Flatten object at 0x114603a10>
12 : dense_1, <keras.layers.core.Dense object at 0x1146126d0>
13 : activation_5, <keras.layers.core.Activation object at 0x11462df90>
14 : dense_2, <keras.layers.core.Dense object at 0x114654950>
15 : activation_6, <keras.layers.core.Activation object at 0x1146b9f10>
[coreml] $ 
```

Kerasで構築したCNNの構造が正しく解析されている様子がわかります。

__注意__
2017/08/22時点ではcoremltoolsはpython2にしか対応してない点に注意です。python3への対応は現在対応中らしいので、しばらく待ちましょう。

https://forums.developer.apple.com/thread/79505#thread-message-240706

# iOSアプリへの組み込みと検証

生成した`Momomind.mlmodel`をiOSのプロジェクトにインポートすると、Xcodeが自動的に`Momomind`という名前のクラスのSwiftコードを内部的に生成してくれます。以下抜粋します。

```swift
class MomomindInput : MLFeatureProvider {
    var image: CVPixelBuffer
    ...
}

class MomomindOutput : MLFeatureProvider {
    let classLabel: String
    ...
}

@objc class Momomind:NSObject {
    ...
    convenience override init() {
        let bundle = Bundle(for: Momomind.self)
        let assetPath = bundle.url(forResource: "Momomind", withExtension:"mlmodelc")
        try! self.init(contentsOf: assetPath!)
    }
    ...
    func prediction(image: CVPixelBuffer) throws -> MomomindOutput {
        let input_ = MomomindInput(image: image)
        return try self.prediction(input: input_)
    }
}

```

`Momomind.predication`メソッドに`CVPixelBuffer`を与えて、返ってきた`MomomindOutput.classLabel`で識別結果が文字列として得られる、というシンプルなクラスです。

ちなみに今回の`Momomind.mlmodel`は90MB近くあるので、アプリのバイナリサイズに結構影響しそうですが、上のコードを見る限り、`bundle.url(forResource: "Momomind", withExtension:"mlmodelc")`で読み込んでるようなので、ネットワーク経由で`mlmodelc`をダウンロードして読み込む、といったことも可能なのかもしれません（未検証）。

ではさっそくこのクラスを使ってメンバー画像を識別してみます。kerasでの訓練時に使った1000枚の訓練・テスト画像をiOSプロジェクトに組み込んで、それぞれの画像を`Momomind`クラスに食わせて、kerasで訓練したときと同等の精度がでるかどうかを確認してみます。今回は以下のようなテスト用のViewControllerを作って試してみました。
https://github.com/kenmaz/momo_mind/blob/master/momomind-ios11/momomind_ios11/TestViewController.swift

これを実行してみると・・

```
processing..train
success: 251 / 1137, accu:0.220756
processing..test
success: 82 / 333, accu:0.246246
```

訓練・テストデータいずれも精度22~24%程度と、だいぶ悪い結果が出てしまいました。

問題の原因は、kerasの.h5ファイルを、CoreMLの.mlmodelファイルへ変換する部分にありました。convert.pyのconvertメソッドを以下のように修正してみます。

```py
coreml_model = coremltools.converters.keras.convert(path,
        input_names = 'image',
        image_input_names = 'image',
+       is_bgr = True, #(1)
+       image_scale = 0.00392156863, #(2)
        class_labels = 'labels.txt')
```

これで精度を確認してみると・・

```
processing..train
success: 876 / 1137, accu:0.770449

processing..test
success: 214 / 333, accu:0.642643
```

訓練データでの精度77%、テストデータでの精度64%程度、kerasでの実行結果により近い感じになりました。

（１）のパラメータは入力画像のピクセル順序がRGB or BGRかを指定するものです。指定無しの場合RGBとして扱われます。

`CIImage(cgImage:)`で作った`CIImage`から、`CVPixelBuffer`経由でピクセルデータを取ってみたところ、RGBではなく、BGRAの順で保持しているようでした。ということで`is_bgr=True`を指定してやると精度が30%くらい向上しました。

（２）は入力画像を指定のスケールで変換するためのパラメータです。デフォルトは1なので、このパラメータの指定なしの場合、各ピクセルごとに 0-255 の範囲の値が入力されることになります。

しかし、今回のKerasのコードでは以下のように画像の各ピクセルを1/255したものをCNNの入力として訓練を行っていました。

```py

X_train /= 255
X_test /= 255
...
model.fit(X_train, Y_train,...)
```
https://github.com/kenmaz/momo_mind/blob/master/keras/mcz_main.py#L59

この部分はcifar10のサンプルからそのまま拝借したものなのですが、学習率に対して0-255の入力値は大きすぎてうまく学習がすすまないため、1/255にスケーリングさせるというテクニックによるもののようです。
https://github.com/Arsey/keras-transfer-learning-for-oxford102/issues/1

※) ちなみに書きながら気づきましたが、kerasの場合、自分で入力値を1/255せずとも、`ImageDataGenerator(rescale=1./255,..`と指定すればOKのようでした。

ということで 1/255 = 0.0039.. をimage_scaleとして指定してやることにしました。これにより精度が更に10%ほど向上しました。

*2017/08/10追記*
convert時のオプションでもっとエレガントに対応できるようです。
http://machinethink.net/blog/help-core-ml-gives-wrong-output/
*追記終了*

(1)(2)の修正により精度はましな感じになりましたが、kerasで実行したときよりまだ劣っているようなので、何かまだ足りてない部分があるのかもしれません。分かり次第追記します。

`convert`メソッドのその他のパラメータの説明は下記ページを参照してください。
http://pythonhosted.org/coremltools/generated/coremltools.converters.keras.convert.html


ともかく、これでkerasで学習させたコードをiOSアプリに組み込み、それなりに正しく動作していることを確認できました。あとはVision Frameworkと連携させて、よりアプリっぽく仕上げていきます。

# Vision Frameworkでカメラ画像から顔認識してCoreMLに投げる

`AVCaptureSession`を使ってカメラからの映像を随時`CIImage`として受け取り、それをVision Frameworkの`VNDetectFaceRectanglesRequest`に流し込むことで、カメラ画像から顔の矩形をリアルタイムかつ高精度に取得できます。あとは矩形の範囲を`CIImage`として切り取り、上述の要領で`Momomind`クラスに渡してやればOKというわけです。以下がその実装です。

https://github.com/kenmaz/momo_mind/blob/master/momomind-ios11/momomind_ios11/ViewController.swift

ここでは、カメラ画像から顔画像を切り出してCoreMLに渡す部分でややハマりました。

カメラ画像の`CIImage`に対して`VNDetectFaceRectanglesRequest `で得られた顔位置の矩形`box`を使って`cropping`すると、顔の部分のみの`CIImage`が得られます。

```swift

let faceImage:CIImage = image.cropping(to: box).applying(transform)
```

しかし、この`faceImage`をそのままCoreMLに食わせるとどうもおかしなピクセル値が渡ってしまうようでした。以下のように、一度`CGImage`を生成してから、再度`CIImage`に変換しなおしてCoreMLに渡す、という手順を踏む必要がありました。

```swift
        
let ctx = CIContext()
guard let cgImage = ctx.createCGImage(faceImage, from: faceImage.extent) else {
   assertionFailure()
   return nil
}
```

https://github.com/kenmaz/momo_mind/blob/master/momomind-ios11/momomind_ios11/ViewController.swift#L249

いろいろデバッグしてみたところ、カメラ画像から取得した`CIImage`に対してcropping/applyしても、その背後にいる`CVPixelBuffer`にはなにも影響せず、一旦`CGImage`として書き出してやることで、顔画像分のサイズのピクセル値のみを保持する`CVPixelBuffer`が別途確保される、といったことかなと思います。このあたりは自分が`AVFoundation`をよく理解していない可能性があるので、今後追記します。

なお、Vision FrameworkについてはAppleのサンプルコードが公開されているので、そちらを見ればなんとなく使い方はわかると思います。
https://developer.apple.com/sample-code/wwdc/2017/ImageClassificationwithVisionandCoreML.zip


#まとめ
以上、CoreMLを用いて、Kerasで書いた機械学習のコードをiOSアプリに組み込んでアプリとしてまとめる、という一連の流れを説明しました。convertしてアプリに組み込むだけで、IFとなるクラスまで生成してくれるあたり、使い勝手の良さを感じました。

またVision Frameworkもかなり遊べるFrameworkなので、CoreMLと組み合わせることでもっと面白いアプリが作れそうです。