【Core ML】YOLO v3をCoreMLに変換する方法

YOLO v3をCoreMLに変換してみたので手順を書きます。

YOLO v3は、物体を検出するアルゴリズムです。
YOLOをiOSで動作させるのに一番簡単な方法は、[Apple公式のCore MLモデル](https://developer.apple.com/jp/machine-learning/models/)をダウンロードして使用することですが、今回は手動で変換してみました。

[オリジナルのYOLOv3](https://pjreddie.com/darknet/yolo/)はDarknet構築されていますが、今回はこちらにあるKerasに変換したものを使用します。

qqwweee/keras-yolo3
https://github.com/qqwweee/keras-yolo3

### 手順

Google Colaboratoryでの手順を示します。

**1.必要なライブラリをインストール & インポートする**

```Python:Notebook
!pip install tensorflow-gpu==1.14.0
!pip install -U coremltools
!pip install keras==2.2.4
```

**2.keras-yolo3をcloneする**

まずは、リポジトリから落としてきます。

```Python:Notebook
!git clone https://github.com/qqwweee/keras-yolo3
```

**3.keras-yolo3を動かしてみる**

まずは、Pythonでそのままkeras-yolo3を動かしてみます。
このあたりはgithubのREADME.mdに書いてある手順そのままです。

最初に重み情報のファイルをダウンロードします。

```Python:Notebook
%cd keras-yolo3
#!wget https://pjreddie.com/media/files/yolov3.weights
```

keras版のyoloモデルに変換します。

```Python:Notebook
!python convert.py yolov3.cfg yolov3.weights model_data/yolo.h5
```

適当な画像をアップロードして推論させてみます。
今回はneco.jpgというファイルをアップロードしました。

```
! python yolo_video.py --image --input neco.jpg

# 出力結果
# (416, 416, 3)
# Found 2 boxes for img
# bed 0.66 (11, 481) (656, 660)
# cat 1.00 (98, 17) (624, 637)
# 6.3229040969999915
```

ちゃんと猫を検出できているようです。

**4.CoreMLに変換する**

Core ML Toolsを使って変換します。
入力画像は416(width)x416(height)x3(RGB)としています。
また、image_scaleに1/255.0を設定して正規化をします。

```Python:Notebook
from keras.models import load_model
from coremltools.converters import keras as converter
mlmodel = converter.convert(keras_model,
  output_names=['grid1','grid2','grid3'], 
  input_name_shape_dict = {'input1' : [None, 416, 416, 3]},
  image_input_names='input1', 
  image_scale=1/255.0,
)

# 出力
# 0 : input_1, <keras.engine.input_layer.InputLayer object at 0x7f7e4058fa58>
# 1 : conv2d_1, <keras.layers.convolutional.Conv2D object at 0x7f7e41cffb38>
# 2 : batch_normalization_1, <keras.layers.normalization.BatchNormalization object at 0x7f7e41cc6438>
# 〜略〜
# For large sized arrays, multiarrays of type float32 are more efficient.
# In future, float input/output multiarrays will be produced by default by the converter.
# Please use, either the flag 'use_float_arraytype' during the call to convert or
# the utility 'coremltools.utils.convert_double_to_float_multiarray_type(spec)', post-conversion.
```

変換したCore MLのモデルを保存します。

```Python:Notebook
coreml_model_path = 'yolo.mlmodel'
mlmodel.save(coreml_model_path)
```

**5.推論結果の表示を調べる**

CoreMLへの変換はうまくいきましたが、これをXcodeのプロジェクトにコピーして、Vision Frameworkで推論させようとすると失敗します。

というのは、YOLOv3の出力は３つあり、1x1x255x13x13, 1x1x255x26x26, 1x1x255x52x52というshapeをしていますが、このままではVision Frameworkでは解釈できないためです。出力結果をデコードする必要があります。

YOLOv3の出力についてはこちらのブログがわかりやすかったです。
[一般物体認識YOLOv3のモデル構造](https://note.com/seishin55/n/n141265f7fd5a)

デコードは自分で作ると大変そうなので、今回はこちらのプロジェクトを使います。

Ma-Dan/YOLOv3-CoreML
https://github.com/Ma-Dan/YOLOv3-CoreML

このプロジェクトでは、CoreMLの出力が255x13x13、255x26x26、255x52x52となることを想定しています。
この形になるように出力をReshapeする必要があります。

**6.出力をreshapeする**

モデルの出力を次のようにreshapeします。

1x1x255x13x13 → 255x13x13
1x1x255x26x26 → 255x13x26
1x1x255x52x52 → 255x13x52

そのためには、Core ML Toolsでreshapeするためのレイヤーを追加する必要があります。
なお、Core ML ToolsでCore MLモデルのレイヤーを編集する方法はこちらに詳しく書いてあります。

Core ML Toolsでレイヤーを編集する方法
https://qiita.com/TokyoYoshida/items/7aa67dcea059a767b4f2


reshapeするレイヤーですが、最初は次元を削減する[add_squeezeというのがあるので](https://apple.github.io/coremltools/generated/coremltools.models.neural_network.builder.html#coremltools.models.neural_network.builder.NeuralNetworkBuilder.add_squeeze)を試しましたが、なぜかうまくいきませんでした。

また、[add_reshape](https://apple.github.io/coremltools/generated/coremltools.models.neural_network.builder.html#coremltools.models.neural_network.builder.NeuralNetworkBuilder.add_reshape)というのもありましたが、これだと先頭の1x1の部分の次元が削減されないままでした。

いろいろ調べた結果、[add_reshape_static](https://apple.github.io/coremltools/generated/coremltools.models.neural_network.builder.html#coremltools.models.neural_network.builder.NeuralNetworkBuilder.add_reshape_static)というのがあり、これを使うとうまくreshapeできました。

次のようにして追加します。

```Python:Notebook
from coremltools.models.neural_network import datatypes

builder.add_reshape_static(name='Reshape1', input_name='grid1', output_name='output1', output_shape=(255,13,13))
builder.add_reshape_static(name='Reshape2', input_name='grid2', output_name='output2', output_shape=(255,26,26))
builder.add_reshape_static(name='Reshape3', input_name='grid3', output_name='output3', output_shape=(255,52,52))
```

次に、モデル全体の出力のshapeを指定します。

```Python:Notebook
builder.spec.description.output[0].name = "output1"
builder.spec.description.output[0].type.multiArrayType.shape[0] = 255
builder.spec.description.output[0].type.multiArrayType.shape.append(13)
builder.spec.description.output[0].type.multiArrayType.shape.append(13)

builder.spec.description.output[1].name = "output2"
builder.spec.description.output[1].type.multiArrayType.shape[0] = 255
builder.spec.description.output[1].type.multiArrayType.shape.append(26)
builder.spec.description.output[1].type.multiArrayType.shape.append(26)

builder.spec.description.output[2].name = "output3"
builder.spec.description.output[2].type.multiArrayType.shape[0] = 255
builder.spec.description.output[2].type.multiArrayType.shape.append(52)
builder.spec.description.output[2].type.multiArrayType.shape.append(52)
```

最後にモデルを保存します。

```Python:Notebook
mlmodel_modified = coremltools.models.MLModel(spec)
mlmodel_modified.save('Yolov3.mlmodel')
```

**7.アプリで表示させる**

あとは、Core MLのモデルをYOLOv3-CoreMLのプロジェクトにDrag & Dropして実行するだけです。

試しにXcode上からモデルを表示すると、正しく認識できていることがわかります。
<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/0b9eeb49-4aa5-7526-f025-ddc58d66700d.png">

実行結果です。

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/28ea8891-3eab-a60b-8353-d9200798188d.png">

ちゃんと認識できていますね。

### 最後に

NoteではiOS開発について定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshida

Twitterでも発信しています。
https://twitter.com/jugemjugemjugem