Core MLを用いた、iPhone での機械学習あれこれ(4)
iPhoneでの機械学習あれこれ(1)を執筆した時点では、PyTorchモデルを直接Core MLモデルに変換するツールはありませんでした。当時から「Core ML tools」という機械学習モデルの変換ツール(Pythonパッケージ)はあったのですが、PyTorchには対応していませんでした。
しかし、今年の7月末にリリースされたCore ML tools 4.0では、遂にPyTorchモデルを直接Core MLモデルに変換可能となったようです。
そこで、早速導入してトライしてみました。まだまだ情報も少なく色々手探りでしたが、何とかある程度方法が理解出来てきましたので、どこかで誰かの役に立つかもしれないと信じてメモしてみます。
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)」となっています。
このモデルを用いて、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))" } } }
試しに動かしてみると、このモデルでもきちんと手書き数字を予測することが出来ました。
入力が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で利用可能な形式になっています。
あとは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」と認識されやすくなるような挙動も解消しました。
なお補足ですが、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では未学習のモデルをiPhoneやiPad上でオンデバイス学習することも出来たりするようですので、今後も色々調べてトライして行きたいと思います。