コンテンツにスキップ

Top

iPhone で PyTorch で作成した深層学習モデルの推論を行う

PyTorchで作成・学習したネットワークモデルをiPhone上で推論したい場合どうすべきか。

いくつか方法はあるが、簡単なのはcoremltoolsを用いてPyTorchのモデルをCoreMLで動く形式に変換することである。

環境構築

Macでの作業を前提とする(だってiPhoneで動かすんだから、MacじゃないとXcodeうごかないでしょ)

あと、PyThonの(正確にはPyTrochなど)のバージョンは最新にするとエラーになることが多いので注意。
あくまで以下のバージョンで動作確認しているが、バージョンが変わると動かない(エラーになる)がしようがない。
PyTorchかcoremltoolsか知らんが下位互換性があんまないので。

pyenvは入っているものとする。

1
2
3
4
5
6
7
pyenv global 3.9.13
mkdir pytorch_ios_sample
cd pytorch_ios_sample/
python3 -m venv .venv
pyenv global system
source .venv/bin/activate
pip install pip==22.1.2

PyTorch関連のを入れる(このサンプルでは2022/06/27現在の最新で問題なかった)

1
2
3
pip install torch torchvision
pip install coremltools
export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python

以降、バージョンが異なるだけで不可解なエラーが発生しやすいので、何か問題が起きたらまずはバージョンを確認し、異なっている場合は合わせた上で再度確認すること。

1
2
3
4
5
6
7
$ python -c "import torch;print(torch.__version__)"
1.11.0
$ python -c "import torchvision;print(torchvision.__version__)"
0.12.0
$ python -c "import coremltools;print(coremltools.__version__)"
WARNING:root:Torch version 1.11.0 has not been tested with coremltools. You may run into unexpected errors. Torch 1.10.2 is the most recent version that has been tested.
5.2.0

coremltoolsでwarningが発生しているが、このサンプルを実行する上で問題が起きなかったので、torchを1.10.2にダウングレードはしていない。

なお、macOSは12.4で、Xcodeは13.4である。

サンプルコード実行

以下の公式のサンプルコードを動かす。

PyTorchセグメンテーションモデルを変換する
https://coremltools.readme.io/docs/pytorch-conversion-examples

ページの一番下の方にコードがあるのでコピペする。
(ここではdeeplabv3_resnet101.pyと命名する)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import urllib
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

import torch
import torch.nn as nn
import torchvision
import json

from torchvision import transforms
from PIL import Image

import coremltools as ct

# Load the model (deeplabv3)
model = torch.hub.load('pytorch/vision:v0.12.0', 'deeplabv3_resnet101', pretrained=True).eval()

# Load a sample image (cat_dog.jpg)
input_image = Image.open("cat_dog.jpg")
input_image.show()

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

input_tensor = preprocess(input_image)
input_batch = input_tensor.unsqueeze(0)

with torch.no_grad():
    output = model(input_batch)['out'][0]
torch_predictions = output.argmax(0)

def display_segmentation(input_image, output_predictions):
    # Create a color pallette, selecting a color for each class
    palette = torch.tensor([2 ** 25 - 1, 2 ** 15 - 1, 2 ** 21 - 1])
    colors = torch.as_tensor([i for i in range(21)])[:, None] * palette
    colors = (colors % 255).numpy().astype("uint8")

    # Plot the semantic segmentation predictions of 21 classes in each color
    r = Image.fromarray(
        output_predictions.byte().cpu().numpy()
    ).resize(input_image.size)
    r.putpalette(colors)

    # Overlay the segmentation mask on the original image
    alpha_image = input_image.copy()
    alpha_image.putalpha(255)
    r = r.convert("RGBA")
    r.putalpha(128)
    seg_image = Image.alpha_composite(alpha_image, r)
    # display(seg_image) -- doesn't work
    seg_image.show()

display_segmentation(input_image, torch_predictions)

# Wrap the Model to Allow Tracing*
class WrappedDeeplabv3Resnet101(nn.Module):

    def __init__(self):
        super(WrappedDeeplabv3Resnet101, self).__init__()
        self.model = torch.hub.load('pytorch/vision:v0.12.0', 'deeplabv3_resnet101', pretrained=True).eval()

    def forward(self, x):
        res = self.model(x)
        x = res["out"]
        return x

# Trace the Wrapped Model
traceable_model = WrappedDeeplabv3Resnet101().eval()
trace = torch.jit.trace(traceable_model, input_batch)

# Convert the model
mlmodel = ct.convert(
    trace,
    inputs=[ct.TensorType(name="input", shape=input_batch.shape)],
)

# Save the model without new metadata
mlmodel.save("SegmentationModel_no_metadata.mlmodel")

# Load the saved model
mlmodel = ct.models.MLModel("SegmentationModel_no_metadata.mlmodel")

# Add new metadata for preview in Xcode
labels_json = {"labels": ["background", "aeroplane", "bicycle", "bird", "board", "bottle", "bus", "car", "cat", "chair", "cow", "diningTable", "dog", "horse", "motorbike", "person", "pottedPlant", "sheep", "sofa", "train", "tvOrMonitor"]}

mlmodel.user_defined_metadata["com.apple.coreml.model.preview.type"] = "imageSegmenter"
mlmodel.user_defined_metadata['com.apple.coreml.model.preview.params'] = json.dumps(labels_json)

mlmodel.save("SegmentationModel_with_metadata.mlmodel")

実行するには cat_dog.jpg が必要なのでダウンロードしておく。

1
wget -O cat_dog.jpg https://files.readme.io/690356d-cat_dog.jpg

実行。結構待たされる。

1
2
3
4
5
6
$ python deeplabv3_resnet101.py
WARNING:root:Torch version 1.11.0 has not been tested with coremltools. You may run into unexpected errors. Torch 1.10.2 is the most recent version that has been tested.
Converting Frontend ==> MIL Ops: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊| 844/845 [00:00<00:00, 911.47 ops/s]
Running MIL Common passes: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 34/34 [00:01<00:00, 20.21 passes/s]
Running MIL Clean up passes: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 9/9 [00:00<00:00, 34.23 passes/s]
Translating MIL ==> NeuralNetwork Ops: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 944/944 [01:14<00:00, 12.67 ops/s]

で、SegmentationModel_no_metadata.mlmodelとSegmentationModel_with_metadata.mlmodelの2つのモデルができあがる。
SegmentationModel_no_metadata.mlmodelで問題ないが、これだとXcode上でプレビューができない。
(メタデータ付きモデルだとXcode上でプレビューができるようになる)
別にプレビューいらんがな、という人はSegmentationModel_no_metadata.mlmodelでなんら問題ない。

なぜクラスをラッピングしているのか?

WrappedDeeplabv3Resnet101(nn.Module): というクラスでラッピングしているがなぜか?
理由はPyTorchのTorchScriptがDictonaryに対応していないから。
よってわざわざラッピングして、outキーの値であるTensorだけを返却するようにしている。
いつか治るかも知らんが。

MIL形式

ほとんど意識しなくて良いが、Torch ScriptモデルからCoreMLモデルに変換する際、coremltools内ではMIL形式に変換している。
勝手にやっているので気にする必要がないが、変換においてサポートされていない演算子エラーが出た場合、このMILプログラムを書いて演算子を追加するなどする。
現状、MaskRCNNなどでエラーが生じるので、その辺りをMILで書き換えればうまく動かしたりできたりもする。
(MILを書き換えInstanceSegmentationを実現位しているサンプルコードは別途紹介するhttps://github.com/xta0/CoreML-MaskRCNN

torch.jit.traceとtorch.jit.script

どちらもPyTorchモデルをTorchScriptモデルに変換するツール。

torch.jit.traceが昔からあるけど、最近はtorch.jit.scriptを使った方が良いらしい。

ので torch.jit.script を使ったらエラーになった。

1
2
    assert str(node.output().type()) == "Tensor"
AssertionError

理不尽。

iPhoneで動かす

セグメンテーションモデルを動かすiOSサンプルアプリはAppleから提供されていないので、ネットから適当に探した以下を使う。