【Coremltools】PyTorchのモデルをCoreMLに変換する際にハマりまくった話

# 背景？？

CoreMLとは、iPhoneやMac上で動く機械学習ライブラリです。
このライブラリが優れているのは、端末内で機械学習を行えるだけではなく、
TensorFlowやPytorchのモデルを変換してスマホに搭載することもできるんですね。

モデルの変換にはCoremltoolsというものを使います。[公式のサイト](https://coremltools.readme.io/docs)のチュートリアルを見れば、
変換するところまでは簡単です。しかし、今回、変換させたCoreMLが正しい分類結果を出してくれずに
数日間悩んだので、解決策を載せておきます。

<br>
<br>

ハマったのは以下の点です。
- 分類における確率が「1100%」とかとんでもない値になる
- PyTorchのモデルで分類したときの結果と一致しない
- 変換できたけれどSwiftUIでエラーが出て動かない




# PyTorchのモデル → CoreMLのモデル

[公式のサイトのPyTorch conversion](https://coremltools.readme.io/docs/pytorch-conversion)の項目通りにいくとこんな感じ。

たとえば、MobileNetのpre-trained modelを変換する場合...

- モデルをダウンロードする
- 推論モードにする --> CoreMLではdrop outが無効化されるからとのことです
- モデルのトレースとinput sizeを記載してct.convertする
- 名前をつけて保存


```python
import torch
import torchvision
import coremltools as ct

# Load a pre-trained version of MobileNetV2
torch_model = torchvision.models.mobilenet_v2(pretrained=True)
# Set the model in evaluation mode.
torch_model.eval()

# Trace the model with random data.
example_input = torch.rand(1, 3, 224, 224) 
traced_model = torch.jit.trace(torch_model, example_input)
out = traced_model(example_input)

# Using image_input in the inputs parameter:
# Convert to Core ML neural network using the Unified Conversion API.
model = ct.convert(
    traced_model,
    inputs=[ct.TensorType(shape=example_input.shape)]
 )

# Save the converted model.
model.save("newmodel.mlmodel")
```

# 作成したCoreMLによる推論結果がPyTorchと一致しない！！

作成したmlmodelファイルをXcodeに突っ込むと、Previewの画面から推論を試すことができます。
今回はポメラニアンの画像を推論にかけてみることにしましょう。

<img width="800" alt="スクリーンショット 2022-10-03 19.31.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620139/846e3f4a-edac-6717-6aa7-7431fe42ddd7.png">

![pomerania.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620139/9614808a-7235-64a6-2a71-68d0590146f6.jpeg)

さて、推論結果は...
驚きの1611%!!!

![スクリーンショット 2022-10-03 14.16.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620139/fdf15db3-78ce-e8b1-4a87-72496201eaac.png)


# ソフトマックスの層を追加する

調べてみると、推論にソフトマックスが含まれていないため、モデルの方にソフトマックスの層を追加する必要があるようです。ラッパーモデルクラスを作成します。


```python
class TorchClassificationModel(nn.Module):
    def __init__(self):
        super(TorchClassificationModel, self).__init__()
        self.layers = nn.Sequential(
            base_model,
            nn.Softmax(dim=1)
        )
    def forward(self, x):
        return self.layers(x)
```

まともな数字が出てくるようになりましたね。Torchってなんぞや？？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620139/a71b1870-f16d-d16c-b4b8-0a392f32f186.png)


ちなみに、PyTorchモデルによる推論結果はこちら。
ポメラニアン98.85%としっかり推定できています。


```python
from PIL import Image
import torch
from torchvision import models, transforms
from torch.nn import functional as F

sample_img = Image.open("/content/pomeranian.jpg")

preprocess = transforms.Compose([
    transforms.Resize(224),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    )
])

img_tensor = preprocess(sample_img) #[3,224,224]
input = img_tensor.unsqueeze(0) #[1,3,224,224]
output = torch_model(input)

probs = F.softmax(output, dim=1)
probs, indices = probs.sort(dim=1, descending=True)

for probs, indices in zip(probs, indices):
    for k in range(3):
        print(f"Top-{k + 1} {class_labels[indices[k]]} {probs[k]:.2%}")

```

```
Top-1 Pomeranian 98.85%
Top-2 chow 0.71%
Top-3 keeshond 0.19%
```


# スケールとバイアスの設定

ちなみに、Pytorchでは入力画像は以下の処理を経ています。

1. 224px四方にリサイズする
2. ImageNetの平均と標準偏差により標準化

CoreMLではリサイズはされていますが標準化はされません。そこで、Coremltoolsでスケールとバイアスを設定することにより、標準化していない画像を投入しても正しく判定されるようにします。[AppleのQ&A](https://github.com/apple/coremltools/issues/1160)に従い、image_inputに下記を追加。


```python
#Set the image scale and bias for input image preprocessing.
scale = 1.0 / (255.0 * 0.226)
red_bias = -0.485 / 0.226
green_bias = -0.456 / 0.226
blue_bias = -0.406 / 0.226

image_input = ct.ImageType(name="input_1",
                           shape=example_input.shape,
                           scale=scale,
                           bias=[red_bias, green_bias, blue_bias],
                           color_layout='BGR')
```

さて、推論結果はどうでしょう？？
ポメラニアンが上位にきましたが、Pytorchの判定結果とは異なっていますね...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620139/d1e5d13c-9701-99c2-07cd-b5b6a340fe24.png)



そういえばBGRの変換って必要なのでしょうか？？抜いてみましょう。
結果、やっとPytorchの判定結果と一致するようになりました。

```python
#Set the image scale and bias for input image preprocessing.
scale = 1.0 / (255.0 * 0.226)
red_bias = -0.485 / 0.226
green_bias = -0.456 / 0.226
blue_bias = -0.406 / 0.226

image_input = ct.ImageType(name="input_1",
                           shape=example_input.shape,
                           scale=scale,
                           bias=[red_bias, green_bias, blue_bias])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620139/8b10c5e0-dfc7-ff7c-d471-be95f2474646.png)




# スマホの端末で推論が動かない！！

XcodeでSwiftUIのコードを書きます。
[こちら](https://github.com/create-with-swift/coreml-with-swiftui)を写経してみました。


```swift
    private func classifyImage() {
        let currentImageName = photos[currentIndex]
        
        guard let image = UIImage(named: currentImageName),
              let resizedImage = image.resizeImageTo(size:CGSize(width: 224, height: 224)),
              let buffer = resizedImage.convertToBuffer() else {
              return
        }
        
        let output = try? model.prediction(image: buffer)
        
        if let output = output {
            let results = output.classLabelProbs.sorted { $0.1 > $1.1 }
            let result = results.map { (key, value) in
                return "\(key) = \(String(format: "%.2f", value * 100))%"
            }.joined(separator: "\n")

            self.classificationLabel = result
        }
    }
```

すると2つのエラーメッセージが。

```
Incorrect argument label in call (have 'image:', expected 'input_1:')
```

```
Value of type 'MobileNetV2_pytorchOutput' has no member 'classLabelProbs'
```


これを直すためには、mlmodelの「Predictions」を開きます。すると、InputとOutputの項目にそれぞれ名前がついていることがわかりますね。指示に従い、Input画像の名前を「input_1」、output.classLabelProbsを「output.var_823」に変更します。※この部分は変換するたびに色んな値をとります。
これでエラーが出ずに動くようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620139/d7461e23-a058-6da5-f39f-61521f9b8954.png)


# まとめ

コードを書いておきます。

Python↓↓

```python
import torch.nn as nn

########################
# Convert Pytorch model     #
########################

import torch
import torchvision
import coremltools as ct
import numpy as np


# Load a pre-trained version of MobileNetV2
base_model = torchvision.models.mobilenet_v2(pretrained=True)

class TorchClassificationModel(nn.Module):
    def __init__(self):
        super(TorchClassificationModel, self).__init__()
        self.layers = nn.Sequential(
            base_model,
            nn.Softmax(dim=1)
        )
    def forward(self, x):
        return self.layers(x)

# Set the model in evaluation mode
torch_model = TorchClassificationModel().eval()
# torch_model = base_model.eval()

# Trace with random data
example_input = torch.rand(1, 3, 224, 224) # after test, will get 'size mismatch' error message with size 256x256
traced_model = torch.jit.trace(torch_model, example_input)


# Download class labels (from a separate file)
import urllib
label_url = 'https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt'
class_labels = urllib.request.urlopen(label_url).read().decode("utf-8").splitlines()
class_labels = class_labels[1:] # remove the first class which is background
assert len(class_labels) == 1000


#Set the image scale and bias for input image preprocessing.
scale = 1.0 / (255.0 * 0.226)
red_bias = -0.485 / 0.226
green_bias = -0.456 / 0.226
blue_bias = -0.406 / 0.226

image_input = ct.ImageType(name="input_1",
                           shape=example_input.shape,
                           scale=scale,
                           bias=[red_bias, green_bias, blue_bias])

# Convert to Core ML using the Unified Conversion API
mlmodel = ct.convert(
    traced_model,
    inputs=[image_input], 
    classifier_config = ct.ClassifierConfig(class_labels), 
    compute_units=ct.ComputeUnit.CPU_ONLY,
)

# Save model
mlmodel.save("MobileNetV2_pytorch.mlmodel")
```

SwiftUI↓↓ ※その他の部分は[こちら](https://github.com/create-with-swift/coreml-with-swiftui/blob/main/CoreMLwithSwiftUI/ContentView.swift)を写経をして下さい

```swift
    private func classifyImage() {
        let currentImageName = photos[currentIndex]
        
        guard let image = UIImage(named: currentImageName),
              let resizedImage = image.resize image to(size:CGSize(width: 224, height: 224)),
              let buffer = resizedImage.convertToBuffer() else {
              return
        }
        
        let output = try? model.prediction(input_1: buffer)
        
        if let output = output {
            let results = output.var_823.sorted { $0.1 > $1.1 }
            let result = results.map { (key, value) in
                return "\(key) = \(String(format: "%.2f", value * 100))%"
            }.joined(separator: "\n")

            self.classificationLabel = result
        }
    }
```



カメラで撮影したものを判定するUIを作ってみました。なかなかいい感じですね！！

<img width="400" alt="スクリーンショット 2022-10-03 19.31.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620139/26e35e30-f166-d690-860c-df3667105689.jpeg">





# 参考サイト：

https://coremltools.readme.io/docs/pytorch-conversion

https://qiita.com/john-rocky/items/99fed158990efb55e209

https://github.com/apple/coremltools/issues/1160