LoginSignup
3
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-25

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

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

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

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

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

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

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

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

Kerasで作った簡単なモデルをCoreMLを使ってiOSで動作させる

手順

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

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

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

Kerasで作ったモデルを使用します。モデルの作成部分は、前回の記事にあるので省略します。

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

2.CoreMLに変換する

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

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

3.ビルダーで読み込む

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

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

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

notebook
spec.description.output

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

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

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レイヤーを追加してみます。

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を使って、レイヤーが1つだけの完結したモデルを作り、それを元のモデルに追加するようにします。
(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)を指定します。

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

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

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

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)のままであることがわかります。

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

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

notebook
// 出力情報を1つ削除する今回は出力情報は1つしかなかったので出力情報が消える
builder.spec.description.output.pop()
// 出力情報を1つ追加する
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を書き換えるのは次のようにします。

shapeを書き換える例
builder.spec.description.output[0].type.multiArrayType.shape[0] = 100

(補足終わり)

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

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]を与えて推論を実行してみます。

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

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]
// 

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

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

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

レイヤーを削除する。

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

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

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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3