Core ML Toolsでレイヤーを編集する方法

Core ML Toolsを使ってモデルを変換していると、レイヤーを追加して間に計算処理を入れたり、レイヤーの一部をカットしたくなることがありますが、本稿はその方法を説明します。

Core ML Toolsは、ドキュメントも少なくネット上にも情報が少ないため、ソースコードを読みながら調べるしかないのですが、レイヤーを編集する作業はCore MLへの変換ではよくあるユースケースだと思いますので、この記事が役に立てばと思います。

### モデルケース：出力の前にreshapeを入れて、出力のshapeを変更する

モデルケースとして、モデルの出力のshapeが(19)のものを、reshapeを追加して(1,1,19)にしてみます。

次元を追加しているだけなのでほとんど意味がないですが、単純なケースにしたいのでこうしています。

使用するモデルは、1桁の数字2つを与えると、それらを足した結果を予想（分類）するものを使います。分類結果は19個の配列になります。これは、1桁の数字を足すと0〜18で合計19種類の分類問題になるためです。

図にするとこんな感じです。

<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/0b8d9a6e-230d-f8cf-5b69-6750b9a15de0.png">

このモデルは以前の記事で使ったものと同じです。前回の記事は出力にラベル名まで与えていましたが、今回は単に19個の確率そのまま出力します。

[Kerasで作った簡単なモデルをCoreMLを使ってiOSで動作させる](https://qiita.com/TokyoYoshida/items/f6980d875bb3fce26814)


### 手順

それでは早速、Reshapeレイヤーを追加します。
Google Colaboratory上で作業をします。

なお、今回の作業の完全なコードはこちらにあります。
https://gist.github.com/TokyoYoshida/2fada34313385d63b666253490b5f3f4

**1.Kerasのモデルを読み込む**

Kerasで作ったモデルを使用します。モデルの作成部分は、[前回の記事](https://qiita.com/TokyoYoshida/items/f6980d875bb3fce26814)にあるので省略します。

```python:notebook
from keras.models import load_model
keras_model = load_model('my_model.h5')
```

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

CoreMLに変換します。今回はラベルを貼らないのでシンプルにそのまま変換しています。

```python:notebook
from coremltools.converters import keras as converter
mlmodel = converter.convert(keras_model)
```

**3.ビルダーで読み込む**

Core ML ToolsのNeuralNetworkBuilderに読み込みます。

```python:notebook
import coremltools
spec = coremltools.utils.load_spec(coreml_model_path)
builder = coremltools.models.neural_network.NeuralNetworkBuilder(spec=spec)
```

モデルの情報を表示してみます。
ここから、モデルの出力はoutput1という名前で、shapeは19であることがわかります。

```python:notebook
spec.description.output

# 出力結果
# [name: "output1"
# type {
#   multiArrayType {
#     shape: 19
#     dataType: DOUBLE
#   }
# }
# ]
```

レイヤーの情報を表示します。

```python:notebook
builder.layers
# 出力
# ['dense_4',
#  'dense_4__activation__',
#  'dense_5',
#  'dense_5__activation__',
#  'dense_6',
#  'activation_18']
```
今回の対象は、出力レイヤーであるactivation_18です。このレイヤーの出力に対してreshapeをかけます。

**4.Rehapeレイヤーを追加する**

NeuralNetworkBuilderにはadd_reshapeというメソッドがあり、reshapeレイヤーを追加できます。


~~ここで注意点なのですが、builderを使うと直感的にはもとのモデルを表すbuilderにそのままレイヤーを追加できるような気がしていまいますが、この方法だとうまくいきません。~~(2020/10/27追記、元のモデルを表すbuilderにレイヤーを追加できることがわかりましたので訂正致します）

例えば、先程builderに読み込んだモデルにそのままReshapeレイヤーを追加してみます。

```python:notebook
reshape = builder.add_reshape(name='Reshape', input_name='activation_18', output_name='output', target_shape=(1,1,19), mode=0)
```

これをXcodeで読み込むとこんなエラーがでます。

```:Xcodeのエラー
There was a problem decoding this CoreML document
validator error: Layer 'Reshape' consumes an input named 'activation_18' which is not present in this network.
```

Python上でCoreMLの推論を実行しようとするとこんなエラーになります。

```:Pythonのエラー
RuntimeError: Error compiling model: "Error reading protobuf spec. validator error: Layer 'Reshape' consumes an input named 'activation_18' which is not present in this network.".
```

ネットで探してもなかなかわからず苦労したのですが、きっと同じことで苦労する人がいるのではと思います。
このエラーが出る理由は、`input_name='activation_18'`としているところです。ここには元のモデルで`builder.spec.description`で表示したinputもしくはoutputの名前を指定する必要があります。

~~対処法ですが、NeuralNetworkBuilderを使って、レイヤーが１つだけの完結したモデルを作り、それを元のモデルに追加するようにします。~~
（2020/10/27修正）
add_reshapeの引数input_nameが元のモデルのoutput_nameと一致していれば、うまくレイヤーをつなぎこむことができます。

モデルにreshapeレイヤーを追加します。
inputは、元のモデルのoutput、つまり`output1`を指定します。これは新しいモデルの入力である`input2`のような気がしましたが、それだと元のモデルのoutputとreshapeレイヤーへの入力の紐付けがうまくいかないようです。
outputは、新しいモデルのoutputである`output2`を指定します。
target_shapeは今回変換したいshapeである(1,1,19)を指定します。

```python:notebook
builder.add_reshape(name='Reshape', input_name='output1', output_name='output2', target_shape=(1,1,19), mode=0)
```

これで、reshapeを追加したモデルができました。

レイヤー情報を確認します。

```python:notebook
builder.layers

# 出力
# ['dense_4',
#  'dense_4__activation__',
#  'dense_5',
#  'dense_5__activation__',
#  'dense_6',
#  'activation_18',
#  'Reshape']
```

**5.モデルの出力情報を変更する**

モデルの最後にreshapeレイヤーがついたわけですが、それだけだとモデルの最後のレイヤーが差し替わっただけであり、モデル全体としての出力情報は変わっていません。

試しに出力情報を確認すると、期待したshapeである(1,1,19)ではなく(19)のままであることがわかります。

```python:notebook
spec.description.output
# [name: "output1"
# type {
#   multiArrayType {
#     shape: 19
#     dataType: DOUBLE
#   }
# }
# ]
```

モデルの出力情報の変更は、これまたクセが強くて`builder.spec.description.output[0]`に対して代入しようとするとエラーになります。そのため、popしたりaddしたりしながら指定してきます。

```python:notebook
// 出力情報を１つ削除する。今回は出力情報は１つしかなかったので、出力情報が消える
builder.spec.description.output.pop()
// 出力情報を１つ追加する
builder.spec.description.output.add()
// 出力情報の属性を指定する
output = builder.spec.description.output[0]
output.name = "output2"
output.type.multiArrayType.dataType = coremltools.proto.FeatureTypes_pb2.ArrayFeatureType.ArrayDataType.Value('DOUBLE')
// shape情報として(1,1,19)を設定する
output.type.multiArrayType.shape.append(1)
output.type.multiArrayType.shape.append(1)
output.type.multiArrayType.shape.append(19)
```

（補足)
上の例では、pop()で消してadd()で追加することでクリーンな状態にしてから情報を追加していましたが、もともと存在するoutputの属性値だけを直接書き換えることはできます。

例えば、shapeを書き換えるのは次のようにします。

```python:shapeを書き換える例
builder.spec.description.output[0].type.multiArrayType.shape[0] = 100
```
（補足終わり）

出力情報を確認すると、うまく設定できていることがわかります。

```python:notebook
builder.spec.description.output
# [name: "output2"
# type {
#   multiArrayType {
#     shape: 1
#     shape: 1
#     shape: 19
#     dataType: DOUBLE
#   }
# }
# ]
```

**6.モデルの出力結果をJupyter Notebookで確認する**

モデルの出力結果をMac上で動作しているJupyter Notebookで確認します。
なぜMacでJupyter Notebook？という話なんですが、Google ColaboratoryはバックエンドがLinuxなのでCoreMLの推論はできず、モデルが正しく動作しているかの確認ができません。

Xcodeで推論してもよいのですが、Xcodeはおかしなモデルを読むとエラーを出してくれることもありますが、ビルド時に次のエラーを吐いていきなり落ちることもあります。


```:Xcodeのエラー
Command CoreMLModelCompile failed with a nonzero exit code
```

こうしたエラーが出たときは、Jupyter Notebookで実行することでエラーの詳細の情報を確認すると良いです。

ということで、Jupyter Notebookを起動します。

こちらに完全なコードがあります。
https://gist.github.com/TokyoYoshida/632b4c8070aa6c937539e4ae261a2740

モデルへの入力として[2,3]を与えて推論を実行してみます。

```python:JupyterNoteBook
coreml_model_path= "my_model_with_builder.mlmodel"

import coremltools
spec = coremltools.utils.load_spec(coreml_model_path)
builder = coremltools.models.neural_network.NeuralNetworkBuilder(spec=spec)

mlmodel = coremltools.models.MLModel(spec)

mlmodel.predict({'input1': np.array([2.0,3.0])})

# {'output2': array([[[2.56040733e-21, 7.25779588e-15, 1.99342376e-10, 1.11184195e-09,
#           5.92091055e-05, 9.99939799e-01, 9.72097268e-07, 1.13292452e-14,
#           4.43997455e-23, 5.00404492e-33, 0.00000000e+00, 0.00000000e+00,
#           0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
#           0.00000000e+00, 0.00000000e+00, 0.00000000e+00]]])}
```

今回のモデルは2つの要素を足し算してくれるモデルなので、5番目の要素の確率がほぼ1になっていることから正しく推論できていることがわかります。

また、出力されたarrayが`[[[確率の入った19個の配列]]]`というとなっていることから、shapeが(1,1,19)と意図通りになっていることが確認できます。


**7.モデルの出力結果をXcode & アプリで確認する**

最後に、モデルの出力結果をXcodeに載せてアプリで確認します。

完全なコードはこちらです。
https://github.com/TokyoYoshida/CoreMLSimpleTest

```swift:ViewController.swift
let model = my_model_with_builder()
let inputArray = try! MLMultiArray([2,3])
let inputToModel = my_model_with_builderInput(input1: inputArray)
if let prediction = try? model.prediction(input: inputToModel) {
print(prediction.output2)
try! print(prediction.output2.reshaped(to: [19]))
}

// # Double 1 x 1 x 19 array
// # Double 19 vector
// # [2.422891379885771e-21,7.01752566646674e-15,1.959301054732521e-10,1.089580203839091e-09,5.933549255132675e-05,0.9999396800994873,9.530076567898504e-07,1.087061586308846e-14,4.16250629238845e-23,4.617410135639087e-33,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,0,0,0]
// 
```

こちらも意図通りになっていることが確認できます。
確率の数字が微妙に違うかもしれませんが、これはモデルを訓練しなおしたためなので気にしないで下さい。

### 番外編　レイヤーを削除する方法

動作確認はしていませんが、レイヤーを削除する方法も書いておきます。

レイヤーを削除する。

```python:notebook
layers = builder.spec.neuralNetwork.layers
# レイヤーの後ろから２番めを取得(activation_18)
item = layers[-2]
# その要素を削除
layers.remove(item)
```

レイヤーの情報を表示すると、削除されていることがわかります。

```python:notebook
for layer in layers:
  print(layer.name)
# dense_4
# dense_4__activation__
# dense_5
# dense_5__activation__
# dense_6
# Reshape
```

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

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