自作したモデルを iOS で使う際に input の型がどのような影響を与えるのか調べた

f:id:dealforest:20200117013235p:plain

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')

f:id:dealforest:20200117010515p:plain

さらにこのままでは 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")

f:id:dealforest:20200117010518p:plain

モデルの生成や 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 で入力した数字を、そのまま推論できたりしますので興味ある方はみてもらえればと思います。 また本記事の中のコードもこちらにあります。

dealforest/CoreMLEasyTryPlayground