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

  • 48
    いいね
  • 0
    コメント

はじめに

一年前にTensorflowを使ってももクロ顔識別webアプリを開発しましたが、今回はWWDC2017で発表された iOS11の CoreML + Vision Framework を使ってももクロメンバーの顔画像をリアルタイムで識別するiOSアプリを作ってみました。

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/

訓練データは前回使ったものをそのまま使っています。ただし学習のプログラムは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に対応したようです。本コードも修正しておきました

Kerasで訓練

まずは訓練データの準備が必要です。インターネット上から適当にももクロの画像をかき集めてきてOpenCVで顔部分をくり抜いて、適当に目の位置やらを全体的に合わせておきます。各メンバーごとに250枚程度、合計1200枚ほどの画像を用意しました。それを3:1くらいで訓練+テスト用に分け、画像ファイルパスと正解ラベルをセットにしたCSVを生成し、以下のように配置しておきます。ここまでは前回記事の使い回し。

[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/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

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コードを内部的に生成してくれます。以下抜粋します。

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メソッドを以下のように修正してみます。

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での実行結果により近い感じになりました。

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

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

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

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


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が得られます。


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

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


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と組み合わせることでもっと面白いアプリが作れそうです。