工場裏のアーカイブス

素人によるiPhoneアプリ開発の学習記 あと機械学習とかM5Stackとか

Core MLを用いた、iPhone での機械学習あれこれ(2)

※(2020/09/21追記)本記事執筆時点ではPyTorchのモデルを直接Core MLモデルに変換する方法がありませんでしたが、現在はCore ML Toolsのバージョン4.0以降を用いることで可能となっています。それに関する記事も作成しましたので、併せてご覧ください。chemicalfactory.hatenablog.com
 
前回記事ではMNISTの手書き数字分類を題材として、PyTorchで作成したモデルを(ONNXを介して)Core MLモデルに変換して、iPhoneアプリに組み込むことを実践しました。

しかし実際の所、MNISTであれば前回記事で言及した、Apple社公式によるオープンソースのCore MLの中にそのものズバリのモデルがあります。本記事ではこちらを使用してみようと思います。
f:id:fleron:20200127000850p:plain
 

Apple社公式のCore MLモデルを使う

モデルのダウンロード

早速、Apple社公式のMNIST分類モデルをダウンロードしてみます(先程のページで「モデルを見る」→「ダウンロード」を選択)。”MNISTClassifier.mlmodel”という名前のファイルがダウンロード出来たら、早速ドラッグ&ドロップでXcodeのプロジェクトに追加して詳細を見てみます。

f:id:fleron:20200127002520p:plain

前回記事でPyTorchから変換したモデルと異なり、入力が「Image (Grayscale 28 x 28)」となっています。これは入力がCVPixelBuffer型であることを意味していいるようです。そしてこの型のCore MLモデルであれば「Vision」を用いることで、画像データをMLMultiArrayで必要だったリサイズや面倒な形式変換をすることなく、入力として容易に与えられるようです。
 

Visionを用いたCore MLモデルの利用

VisionはCore MLと同時期に公開された、機械学習による画像認識の機能を提供するフレームワークです。顔認識やバーコード認識、テキスト認識などの機能がデフォルトで用意されており、これ単体でもiPhoneカメラと組み合わせで色々面白いことが出来そうです(Visionの提供する機能の活用例は、以下の外部サイトがよく纏まっていて分かりやすかったです)
Core MLを利用した機械学習とVisionでの画像認識 - WonderPlanet Developers’ Blog

更にVisionでは、自分で用意したCore MLのモデルを用いて画像分類を行う機能も備えており、これを活用すれば、先述のように容易に画像データをモデルへと入力出来るようです。そこで実際に、前回記事で作成した手書き数字の予測アプリを、ダウンロードした”MNISTClassifier.mlmodel”とVisionを用いる方式に作り替えてみます。以下にコードを示します。

//前回記事で作成した手書き数字の予測アプリを作り替える.
//(プロジェクトには事前に、"MNISTClassifier.mlmodel"を追加しておく).

import UIKit
import CoreML

class ViewController: UIViewController {

    @IBOutlet weak var canvas: UIImageView!
    @IBOutlet weak var predictLabel: UILabel!
    
    //-------------------------------------------------------------------- 
    // 本来はここに、"canvas"に手書きで数字を書けるようにするための
    // コードが存在するが省略。
    //--------------------------------------------------------------------  
    
   @IBAction func predictButtonPushed(_ sender: Any) {
       //Visionでは入力としてUIImageは使用出来ず、CIImageやCGImageに
       //変換する必要あり。ここではCIImageに変換. 
       if let inputImage = self.canvas.image{
            if let ciInputImage = CIImage(image: inputImage){
                predict(image: ciInputImage)
            }
        }
    }
    
    func predict(image: CIImage) {
        //頭に”VN"が付くのはVisionが提供するオブジェクト.
        //Visionで利用出来る形で、Core MLモデルをロードする.
        //(入力が"Image"(CVPixelBuffer)形式であること.MLMultiArrayのものは不可.)
        guard let model = try? VNCoreMLModel(for: MNISTClassifier().model) else {
            fatalError("Loading CoreML Model Failed")
        }

        //Core MLモデルを用いた画像分類を、システムにリクエストするためのオブジェクト.
        //結果に対する処理もここで記述する.
        let request = VNCoreMLRequest(model: model) { (request, error) in
            guard let results = request.results as? [VNClassificationObservation] else {
                fatalError("Model failed to process image")
            }

            //結果は”VNClassificationObservation"オブジェクトの配列として得られる.
            //identifierプロパティで分類名(今回は"0"〜"9"の数字)が取得出来る.
            //confidenceプロパティで、その分類である確度が0.0〜1.0の数値として得られる.
            //配列はconfidence順にソートされているので、先頭要素のidentifierが予測結果となる.
            if let firstResult = results.first {
                self.predictLabel.text = "予測:\(firstResult.identifier)"
            }
        }

        //入力画像(CIImageやCGImageなど)に対する、リクエストを処理するためのハンドラ.
        let handler = VNImageRequestHandler(ciImage: image)

        //入力画像に対するリクエスト(画像分類)の処理を実行させる.
        //入力画像は事前にリサイズなどをしなくても、自動的にモデルに対して適切な形に変換してくれる模様.
        do {
            try handler.perform([request])
        } catch {
            print(error)
        }
    }
}

//前回記事のコードで用いた、UIImageの機能拡張(extension)は不要

 
本アプリでは予測ボタン(predictButton)をタップした後の処理を、Visionの機能を用いて大幅に書き換えています。

Visionでは画像認識の処理をリクエストするためのオブジェクトを作成し、それを入力画像に対して実行させる、という流れとなります。ここでも"canvas"の画像を入力として与えていますが、前処理はUIImage→CIImageの変換のみであり、リサイズすら行っていません。しかしコード中のコメントにも記載したように、Visionでは自動的にモデルに対して適切な形に整形してくれるようであり、非常に便利です。

そして画像分類の結果は”VNClassificationObservation”オブジェクトの配列として得られます。今回は0〜9の数字10種類の分類なので、配列の要素数も10となり、1つ1つのオブジェクトが分類の結果を保持しています。identifierプロパティでは分類名として数字の種類(”0”〜”9”)が、confidenceプロパティでは画像がその数字である確度が0.0〜1.0の数値として取得できます。そして配列はデフォルトでconfidence順にソートされているので、先頭オブジェクトのidentifierがそのまま予測された数字となります。

アプリを動かして見ると、流石に公式配布されているモデルだけあって体感的な精度は高いです。あえて雑に崩した数字を描いても結構正しく予測してくれます。
f:id:fleron:20200129002421p:plain
 

PyTorchのモデルを、Visionで利用可能なCore MLモデルに変換

Visionの便利さが分かりましたので、自作のPyTorchモデルをCore MLモデルに変換するときも、Visionで利用可能な形式、すなわち入力が"Image"(CVPixelBuffer)となるようにする方法が欲しい所です。

色々調べていた所、以下の外部サイトの記事で非常に貴重な情報が見つかりました。
Pytorch(skorch)で学習したモデルを使い、iOSで画像分類をする [2/3] - バイセル Tech Blog

2. image_input_namesをONNXに変換するときと同じものにする
Ccore MLモデルの入力が配列ではなく画像に設定され、iOSでの無駄な変換が不要になります


これだ!と思い早速試してみました。前回記事においてPyTorch→Core MLのモデル変換を行ったコードを少し修正してみます。

#前回記事におけるPyTorch→Core MLのモデル変換用コードを修正
#"MNIST.pth"ファイルは作成済みである前提

import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision.transforms as transforms
import onnx
from onnx_coreml import convert

class MNIST_Conv_MN(nn.Module):
    def __init__(self):
        super(MNIST_Conv_MN, self).__init__()
        self.conv1 = nn.Conv2d(1, 8, 3) 
        self.pooling = nn.MaxPool2d(2, 2) 
        self.fc1 = nn.Linear(13 * 13 * 8, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.pooling(x)
        x = x.view(-1, 13 * 13 * 8)
        x = self.fc1(x)
        return x
    
model = MNIST_Conv_MN()
model.load_state_dict(torch.load('MNIST.pth'))
input_names = ['input'] #追加
output_names = ['output'] #追加

dummy_input = torch.FloatTensor(1, 1, 28, 28)
torch.onnx.export(model, dummy_input, 'MNIST.proto', verbose=True,
                     input_names=input_names, output_names=output_names) #引数にinput_names, output_namesを追加

model_onnx = onnx.load('MNIST.proto')
onnx.checker.check_model(model_onnx)

coreml_model = convert(
    model_onnx,
    mode='classifier',
    image_input_names=['input'],  #前段で追加した"input_names = ['input']"と名前を揃える
    #image_output_namesは削除。残っているとエラーが発生する
    class_labels=[i for i in range(10)]
)

coreml_model.save('MNIST_image.mlmodel')  #Core MLモデル名を”MNIST_image.mlmodel”に変更


修正といっても箇所は少ないです。PyTorch→ONNXモデルの変換を行う部分で、新たにinput_names、output_namesを用意しています。一方でONNX→Core MLモデルの変換を行う部分では、参考記事の通りにimage_input_namesを先程のinput_namesと揃えています。そしてimage_output_namesは削除します(これが残っているとエラーが発生します。ただし、その背景についてはまだ理解出来ていませんが…)。

こちらを実行して上手く行けば、”MNIST_image.mlmodel”という名前でCore MLモデルが生成されます。早速ドラッグ&ドロップで、先程作成したVision版の手書き数字予測アプリのプロジェクトに追加して、詳細を見てみます。

f:id:fleron:20200130235518p:plain

入力の型がバッチリ「Image (Grayscale 28 x 28)」となりました。また地味に、入出力用の変数名も”input”、”output”とモデル変換時に変数として与えた通りの名前に変更出来ています。これならVisionで利用することが出来ますので、早速やってみましょう。手書き数字の認識アプリでCore MLモデルをロードする部分で、モデル名を以下のように書き換えるだけです。

 guard let model = try? VNCoreMLModel(for: MNIST_image().model) else {   //モデル名書き換え
       fatalError("Loading CoreML Model Failed")
 }


アプリを動かしてみると、これまでと同様に手書き数字を予測してくれます。ただし前回記事でMLMultiArrayを用いたときとモデルの中身自体は同じですが、「1」のつもりで縦線を1本引くと、何故か「8」と予測されやすくなっているなど、若干異なる感触の挙動が見られました。

おそらく、Visionで入力画像を自動的にモデルに合わせるプロセスの中身(特にリサイズの方式)が、自前による方法と異なっているからであると推測します。Visionは便利ではありますが、こうしたプロセスの中身は(今の自分にとっては)ブラックボックスであり、きちんと理解した上で使いこなすためには、もっと勉強が必要そうです。
 
まだまだ分からないことが山積みではありますが、ひとまずApple社公式のモデルを使ってみることや、PyTorchのモデルをVisionで利用可能な形式で変換することが出来ました。今後は「Create ML」についても触ってみたいと思います。

Core MLを用いた、iPhone での機械学習あれこれ(3) Create ML編に続きます。