工場裏のアーカイブス

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

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

iPhoneでの機械学習あれこれ(1)を執筆した時点では、PyTorchモデルを直接Core MLモデルに変換するツールはありませんでした。当時から「Core ML tools」という機械学習モデルの変換ツール(Pythonパッケージ)はあったのですが、PyTorchには対応していませんでした。

しかし、今年の7月末にリリースされたCore ML tools 4.0では、遂にPyTorchモデルを直接Core MLモデルに変換可能となったようです。

coremltools.readme.io

そこで、早速導入してトライしてみました。まだまだ情報も少なく色々手探りでしたが、何とかある程度方法が理解出来てきましたので、どこかで誰かの役に立つかもしれないと信じてメモしてみます。
 

Core ML tools 4.0の導入

Core ML toolsのインストールは通常、以下のコマンドで行うことが出来るようです。

$ pip install -U coremltools

ただし本記事執筆時点では、Core ML tools 4.0はまだbeta版としてのリリースなので、以下のように --pre をつける必要があるようです。

$ pip install -U --pre coremltools

インストールが完了したら、以下のようにpythonコードを実行すればバージョン確認が出来ます。

#本記事執筆時点では ”4.0b3” と表示
import coremltools as ct
print(ct.__version__)

 

PyTorchモデルのCore MLモデルへの直接変換

入力がMLMultiArray型となるモデル

手始めにiPhoneでの機械学習あれこれ(1)で用いたMNISTの手書き数字分類モデルを、ONNXを介さず、直接Core MLモデル(入力はMLMultiArray型)に変換してみます。

#Pythonコード
#過去記事(iPhoneでの機械学習あれこれ(1))で作成したMNISTの
#手書き数字分類モデルを,直接Core MLモデルに変換する
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 coremltools as ct

#モデルおよびパラメータについては過去記事参照
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')) 


#モデルへの入力データを用意するが,形状さえ合っていれば
#rand関数などで生成したダミーデータで問題がない模様
#本モデルではMNIST画像(28 * 28 のグレースケール)をミニバッチの形に
#したものが入力となるため,ダミーの形状は(1, 1, 28, 28) となる
dummy_input = torch.rand(1, 1, 28, 28)

#torch.jit.trace関数を用いて,モデルをTorchScriptという
#中間形式に変換する必要がある.先程のダミーデータを用いればOK
trace = torch.jit.trace(model, dummy_input)

#本モデルは0〜9の数字の分類問題用なので, ラベルを用意する
classifier_config=ct.ClassifierConfig([i for i in range(10)])

#Core MLモデルに変換する(先程変換した中間形式を与える必要がある)
#inputsとしてTensorTypeを与えると、データの入力形状がMLMultiArrayとなる
#TensorTypeのnameを”input”にすると、Xcode上でエラーが出るので(名前の衝突?)避ける
mlmodel = ct.convert(
    trace,
    inputs=[ct.TensorType(name="input_1", shape=dummy_input.shape)],
    classifier_config=classifier_config
)

mlmodel.save("MNIST.mlmodel")

 
※TorchScriptの詳しいことついては正直な所、まだあまり理解出来ておりません。ひとまず公式リファレンスを載せておきます。PyTorchで学習したモデルをC++などで利用したりすることも可能となるようです。
pytorch.org

ひとまず、これだけでCore MLモデルに変換出来ます。試しにXcodeに追加して確認してみると、入力の型が「MultiArray (Float32 1 x 1 x 28 x 28)」となっています。
f:id:fleron:20200920152006p:plain

このモデルを用いて、iPhoneでの機械学習あれこれ(1)で作成した手書き数字を予測するアプリを作り直してみます。と言っても、coreMLRequestの中身のみを微修正するだけです。

//swiftコード
//過去記事(iPhoneでの機械学習あれこれ(1))で作成した手書き数字予測アプリの
//coreMLRequestのみ,今回変換したモデルに合わせて一部修正

func coreMLRequest(image: UIImage){
        let imgSize: Int = 28
        let imageShape: CGSize = CGSize(width: imgSize, height: imgSize)

        let imagePixel = image.resize(to: imageShape).getPixelBuffer()
        
        //過去記事ではMLMultiArrayの形状は(1, 28, 28)であったが, 本モデルは(1, 1, 28, 28)なので修正する.
        let mlarray = try! MLMultiArray(shape: [1, 1, NSNumber(value: imgSize), NSNumber(value: imgSize)], dataType: MLMultiArrayDataType.float32 )
        for i in 0..<imgSize*imgSize {
            mlarray[i] = imagePixel[i] as NSNumber
        }
        
        //本記事で直接変換したCore MLモデル("MNIST.mlmodel")をロード
        let MNISTModel = MNIST()
        
        //本モデルでは入力用の変数名は"input_1", 出力用の変数名は”_45"
        if let prediction = try? MNISTModel.prediction(input_1: mlarray) {
            print(prediction._45)
            if let first = (prediction._45.sorted{ $0.value > $1.value}).first{
                self.predictLabel.text = "予測:\(Int(first.key))"
            }
        }
    }


試しに動かしてみると、このモデルでもきちんと手書き数字を予測することが出来ました。

f:id:fleron:20200920160534p:plain
 

入力がCVPixelBuffer型となるモデル(Visionで利用可能)

入力がMLMultiArray型だと扱いが不便なので、iPhone での機械学習あれこれ(2)で触れたように、入力をCVPixelBuffer型にしてVisionで利用可能なモデルに直接変換したい所ですが、それも簡単に出来てしまいます。

先程の変換用コードで、ct.convertのinputsにTensorTypeを与えましたが、以下のようにImageTypeに変更するだけです。

#pythonコード
#先程の変換コードうち、ct.convertを以下のように書き換える
mlmodel = ct.convert(
    trace,
    inputs=[ct.ImageType(name="input", shape=dummy_input.shape, scale=1/255)],
    classifier_config=classifier_config
)


ただしImageTypeでは入力画像の前処理を行わせることが出来ます。例えばモデルにVisionから画像を入力させる場合、その画素値は通常0〜255となります。一方で本モデルはiPhoneでの機械学習あれこれ(1)でも触れた通り、画素値が0〜1に正規化されたMNIST画像で学習しています。そこで、scale=1/255とスケーリングの設定をしておくことで、入力画像の画素値も0〜1にすることが出来ます。

厄介なのは必要なスケーリングを忘れて、入力の画素値がモデルの想定とズレていたとしても、後々Xcodeでの利用時にはエラー無しでビルドが通ってしまいます。その場合、ただただ出力が全然想定通りにならない(本モデルの場合、手書き数字の予測が全然当たらない)…というバグを抱える可能性があります
(実際、iPhone での機械学習あれこれ(2)の自作モデルで「1」が「8」に予測されたりしたのは、そのせいかもしれません…)。

変換出来たCore MLモデルを先程同様にXcode上で確認してみると、確かに入力の型が「Image (Grayscale 28 x 28)」とVisionで利用可能な形式になっています。
f:id:fleron:20200920221047p:plain

あとはiPhone での機械学習あれこれ(2)で作成した、Visionを用いた手書き数字予測アプリで、モデル名さえ書き換えればそのまま用いることが出来ます。

//Swiftコード
//過去記事(iPhone での機械学習あれこれ(2))のVisionを用いた
//手書き数字予測アプリで,モデル名を適宜書き換えればOK
guard let model = try? VNCoreMLModel(for: MNIST().model) else {   //モデル名書き換え。ここでは"MNIST.mlmodel"からとする
       fatalError("Loading CoreML Model Failed")
 }


きちんとスケーリングしたことで、「1」が「8」と認識されやすくなるような挙動も解消しました。
f:id:fleron:20200921001651p:plain
 
なお補足ですが、ImageTypeには他にもbiasを設定することが出来ます。これはスケーリング後の画素値に、更に設定した値を加えるものです。例えば入力画像の画素値を-1〜1にしたい場合は、以下の例のようにまず画素値を0〜2にスケーリングした後、biasでそこから1を引けば(-1を加えれば)良いようです。

#pythonコード
#カラー入力画像のRGB画素値(0〜255)を、-1〜1にしたい場合
ct.ImageType(shape=(1, 3, 28, 28), scale=2/255, bias=[-1,-1,-1])


以上、色々手探りでしたが、Core ML Tool 4.0を用いて基礎的なPyTorchモデル→Core MLモデルの直接変換をやってみることが出来ました。

なお、今回は既にPyTorchで学習済みのモデルを変換しましたが、Core MLでは未学習のモデルをiPhoneiPad上でオンデバイス学習することも出来たりするようですので、今後も色々調べてトライして行きたいと思います。