自作したモデルを iOS で使う際に input の型がどのような影響を与えるのか調べた
1桁の手書きの数字(0〜9の数字に対応)を分類するモデルを Vision, CoreML から利用してみたいと思います。
Apple が提供しているモデルもありますが、今回は自作したモデルを使っていきます。
input が画像のため今回のモデルでは input の型が Image
, MLMultiArray
で作成することができます。
それらを Vision, CoreML から利用するのにどれくらい変わってくるのかを調べてみました。
mlmodel を用意
keras のサンプルをおこないモデルを作成しました。 今回利用するモデルは手書き数字の画像をもとに推論を行い0-9のどの数字かを判断するものです。
keras でモデルを作成したままでは CoreML から使えないため coremltools で変換します。
import coremltools mlmodel = coremltools.converters.keras.convert( model, # 自作したモデル input_names=['image'], output_names=['symbol'], class_labels=['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] ) mlmodel.save('Mnist.mlmodel')
さらにこのままでは input の型が MLMuitiArray
で扱いずらいので Image
に変換したモデルも用意します。
import coremltools import coremltools.proto.FeatureTypes_pb2 as ft spec = coremltools.utils.load_spec("Mnist.mlmodel") input = spec.description.input[0] input.type.imageType.width = 28 input.type.imageType.height = 28 input.type.imageType.colorSpace = ft.ImageFeatureType.GrayScale coremltools.utils.save_spec(spec, "MnistImageType.mlmodel")
モデルの生成や coremltools につきましては別記事でおいおいまとめます。 気になるかたは「Core MLモデルの入力の型をMLMultiArrayから画像(CVPixelBuffer)に変更する」をどうぞ。
mlmodel を使って推論を実行するパターン
- input の型が
Image
(MnistImageType.mlmodel) の場合に- Vision.framework を使って推論を行う
- CoreML.framework を使って推論を行う
- input の型が
MLMultiArray
(Mnist.mlmodel) の場合に- Vision.framework を使って推論を行う
- CoreML.framework を使って推論を行う
(これから先、冗長なため framework という単語は省略します)
mlmodel を Xcode プロジェクトに追加することでクラスが自動生成され利用しやすいようになっています。自動生成されたクラスはこちら。 多くの場合このクラスを利用するかと思います。 便利ではありますが残念ながら Swift Playgrounds では生成されませんので Playgrounds から利用する際は注意が必要ですね。
自動生成される内容をみていくと input, output の型定義、input, output の型を用いて分類を実行できるようにしているだけです。
最低限利用する際には input だけ MLFeatureProvider
に準拠して作成すると利用できます。
作成するのがめんどくさい場合は、一度 Xcode プロジェクトに追加し自動生成されたクラスを持ってくればいいかなと思います。
今回は最低限の推論が実行できているところを確認できればいいので、自動生成されたクラスは利用せず実行していきましょう。
input の型が Image
の場合に Vision を使う
下記のコードは Vision を利用した際に、よく見ますね。
mlmodel の output の定義によって request.results
の型はかわってきます。
import UIKit import Vision let image = UIImage(named: "4.png")! let modelURL = Bundle.main.url(forResource: "MnistImageType", withExtension: "mlmodelc")! let model = try! VNCoreMLModel(for: MLModel(contentsOf: modelURL)) let handler = VNImageRequestHandler(cgImage: image.cgImage!, options: [:]) let request = VNCoreMLRequest(model: model, completionHandler: { (request, error) in guard let results = request.results as? [VNClassificationObservation] else { fatalError() } if let result = results.first { print("☀️ idntifier: \(result.identifier) (\(result.confidence))") } else { print("🌧not found") } }) try! handler.perform([request]) /* [predict result] ☀️ idntifier: 4 (1.0) [verbose] identifier: 4 (1.0) identifier: 0 (1e-45) identifier: 1 (1e-45) identifier: 2 (1e-45) identifier: 3 (1e-45) identifier: 5 (1e-45) identifier: 6 (1e-45) identifier: 7 (1e-45) identifier: 8 (0.0) identifier: 9 (0.0) */
ここで見て分かるように Vision での推論は非同期処理でおこなわれます。
また Vision でも自動生成したクラスを利用することはできます。この場合 output の型推論がきくようになります。
let model = try! VNCoreMLModel(for: MnistImageType().model)
input の型が Image
の場合に CoreML を使う
input の型が Image
の場合は CVPixelBuffer
をわたします。
Vision を利用した際はチャンネル数や画像サイズの調整はよしなに行われますが、CoreML を利用した際は自分で行う必要があります。
その際に便利なのが CoreMLHelpers です。
今回は UIImage.pixelBufferGray(width:height:)
を利用し CVPixelBuffer
に変換します。
import UIKit import CoreML // input の型を定義 class MnistImageTyepInput: MLFeatureProvider { var image: CVPixelBuffer var featureNames: Set<String> = ["image"] // mlmodel の input とずれているとエラーになる func featureValue(for featureName: String) -> MLFeatureValue? { if (featureName == "image") { return MLFeatureValue(pixelBuffer: image) } return nil } init(image: CVPixelBuffer) { self.image = image } } let image = UIImage(named: "4.png")! let pixelBuffer = image.pixelBufferGray(width: 28, height: 28)! let modelURL = Bundle.main.url(forResource: "MnistImageType", withExtension: "mlmodelc")! let model = try! MLModel(contentsOf: modelURL) let input = MnistImageTyepInput(image: pixelBuffer) let output = try! model.prediction(from: input) print(output.featureValue(for: "symbol")) print(output.featureValue(for: "classLabel")) /* [symbol] Dictionary : { 0 = "1.401298464324817e-45"; 1 = "1.401298464324817e-45"; 2 = "1.401298464324817e-45"; 3 = "1.401298464324817e-45"; 4 = 1; 5 = "1.401298464324817e-45"; 6 = "1.401298464324817e-45"; 7 = "1.401298464324817e-45"; 8 = 0; 9 = 0; } [classLabel] String : 4 */
ここで見て分かるように CoreML での推論は同期処理でおこなわれます。
input の型が MLMultiArray
の場合に Vision を使う
残念ながらこのパターンは実行することができません。
Vision は input の型が Image
のときしか利用できません。
ただしコンパイルは通ってしまい下記のような実行時エラーになります。
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) * frame #0: 0x000000010a38460d libswiftCore.dylib`function signature specialization <Arg[0] = Exploded, Arg[1] = Exploded> of Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never + 509 frame #1: 0x000000010a1bc2da libswiftCore.dylib`swift_unexpectedError + 314 frame #2: 0x000000010cabca07 $__lldb_expr4`main at Vision.xcplaygroundpage:7:18 frame #3: 0x000000010a09e560 predictNotUseAutoGenerateModel`linkResources + 304 frame #4: 0x00007fff23bd429c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12 frame #5: 0x00007fff23bd3a08 CoreFoundation`__CFRunLoopDoBlocks + 312 frame #6: 0x00007fff23bce894 CoreFoundation`__CFRunLoopRun + 1284 frame #7: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438 frame #8: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65 frame #9: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621 frame #10: 0x000000010a09e62d predictNotUseAutoGenerateModel`main + 205 frame #11: 0x00007fff5227ec25 libdyld.dylib`start + 1
input の型が MLMultiArray
の場合に CoreML を使う
⚠️このパターンの利用方法は推奨しませんが、推論を行うと大変なことを示したかったので実装しました
Image
のときと同様に CoreMLHelpers を利用して MLMultiArray
を作成したいところですが、そのような機能はありません。
理由は明確で MLMultiArray
に変換するよりも input の型を Image
に変換した mlmodel を作成した方がいいためです。
「How to convert images to MLMultiArray」に詳細が書かれていますので興味がある方はご覧ください。
そのため他に比べてコードがどうしても長くなってしまっています。
import UIKit import CoreML // input の型を定義 class MnistInput : MLFeatureProvider { var image: MLMultiArray var featureNames: Set<String> = ["image"] func featureValue(for featureName: String) -> MLFeatureValue? { if (featureName == "image") { return MLFeatureValue(multiArray: image) } return nil } init(image: MLMultiArray) { self.image = image } } extension UIImage { func pixelDataForGrayscale() -> [UInt8]? { guard let cgImage = self.cgImage else { return nil } let size = self.size let dataSize = size.width * size.height var pixelData = [UInt8](repeating: 0, count: Int(dataSize)) let context = CGContext( data: &pixelData, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: Int(size.width), space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue) context?.draw(cgImage, in: CGRect(origin: CGPoint(x: 0, y: 0), size: size)) return pixelData } } func preprocess(image: UIImage) -> MLMultiArray? { guard let pixels = image.pixelDataForGrayscale() else { return nil } // [channel, width, height] guard let array = try? MLMultiArray(shape: [1, 28, 28], dataType: .double) else { return nil } for (index, element) in pixels.enumerated() { let value = Double(element) / 255.0 array[index] = NSNumber(value: value) } return array } let image = UIImage(named: "samples/4.png")! let modelURL = Bundle.main.url(forResource: "Mnist", withExtension: "mlmodelc")! let model = try! MLModel(contentsOf: modelURL) let multiArray = preprocess(image: image)! let input = MnistInput(image: multiArray) let output = try! model.prediction(from: input) print(output.featureValue(for: "symbol") ?? "none") print(output.featureValue(for: "classLabel") ?? "none") /* [symbol] Dictionary : { 0 = "0.006920351181179285"; 1 = "0.0009511407115496695"; 2 = "0.005366008728742599"; 3 = "8.467519364785403e-06"; 4 = "0.9653815627098083"; 5 = "0.003310209838673472"; 6 = "0.00117537344340235"; 7 = "0.004033106379210949"; 8 = "0.0103130042552948"; 9 = "0.002540647285059094"; } [classLabel] String : 4 */
まとめ
Vision | CoreML | |
---|---|---|
Image |
UIImage のまま利用可能画像情報は Vision が調整 |
UIImage から CVPixelBuffer に変換。画像情報は自分で調整 |
MLMultiArray |
利用不可 | 非推奨 |
こうやってみると input で画像を扱う場合は Vision か CoreML かは関係なく coremltools
で型を Image
にしておくべきですね。
今回は入力が画像でしたためどうしても MLMultiArray
の心理的な壁を感じたかもしれません。
入力が文字や数字のモデルを扱う場合ですとそうでもありませんので、また別途まとめれればなと思います。
また画像を推論するにあたり簡単に試せる方がいいので playground を作成しました。 live view で入力した数字を、そのまま推論できたりしますので興味ある方はみてもらえればと思います。 また本記事の中のコードもこちらにあります。