#はじめに
一年前にTensorflowを使ってももクロ顔識別webアプリを開発しましたが、今回はWWDC2017で発表された iOS11の CoreML + Vision Framework を使ってももクロメンバーの顔画像をリアルタイムで識別するiOSアプリを作ってみました。
なお、本記事はiOSDC2017での発表の元となった記事です。
発表資料はこちら
#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入門)。
開発内容
今回のアプリの開発の流れは以下の通り
- 環境整備
- Keras(tensorflow backend)を使ってEC2上で学習し、モデルを.h5形式のファイルに出力
- CoreMLToolsを使って.h5ファイルを.mlmodelに変換
- iOSアプリに.mlmodelを組み込み、顔識別処理を実装
- AVFoundation と Vision Framework を使ってiOSデバイスのカメラ画像から顔画像を取得し、顔識別処理に流して結果を画面に表示
各ステップの手順とハマりどころなどを紹介します。
環境整備
以下のとおり整備しておきます。
- iOSまわり
- MacBook Pro / mac OS Sierra
- Xcode9 beta2
- iPhone7plus + iOS11 beta2
- pythonまわり
- EC2 p2xlargeインスタンス
- Python2.7 + Keras
1.2.22.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のサンプルをちょろっと改修した程度です。
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への対応は現在対応中らしいので、しばらく待ちましょう。
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,...)
この部分は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というわけです。以下がその実装です。
ここでは、カメラ画像から顔画像を切り出して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
}
いろいろデバッグしてみたところ、カメラ画像から取得した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と組み合わせることでもっと面白いアプリが作れそうです。