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で作ったモデルを使用します。モデルの作成部分は、前回の記事にあるので省略します。
from keras.models import load_model
keras_model = load_model('my_model.h5')
2.CoreMLに変換する
CoreMLに変換します。今回はラベルを貼らないのでシンプルにそのまま変換しています。
from coremltools.converters import keras as converter
mlmodel = converter.convert(keras_model)
3.ビルダーで読み込む
Core ML ToolsのNeuralNetworkBuilderに読み込みます。
import coremltools
spec = coremltools.utils.load_spec(coreml_model_path)
builder = coremltools.models.neural_network.NeuralNetworkBuilder(spec=spec)
モデルの情報を表示してみます。
ここから、モデルの出力はoutput1という名前で、shapeは19であることがわかります。
spec.description.output
# 出力結果
# [name: "output1"
# type {
# multiArrayType {
# shape: 19
# dataType: DOUBLE
# }
# }
# ]
レイヤーの情報を表示します。
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レイヤーを追加してみます。
reshape = builder.add_reshape(name='Reshape', input_name='activation_18', output_name='output', target_shape=(1,1,19), mode=0)
これを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の推論を実行しようとするとこんなエラーになります。
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)を指定します。
builder.add_reshape(name='Reshape', input_name='output1', output_name='output2', target_shape=(1,1,19), mode=0)
これで、reshapeを追加したモデルができました。
レイヤー情報を確認します。
builder.layers
# 出力
# ['dense_4',
# 'dense_4__activation__',
# 'dense_5',
# 'dense_5__activation__',
# 'dense_6',
# 'activation_18',
# 'Reshape']
5.モデルの出力情報を変更する
モデルの最後にreshapeレイヤーがついたわけですが、それだけだとモデルの最後のレイヤーが差し替わっただけであり、モデル全体としての出力情報は変わっていません。
試しに出力情報を確認すると、期待したshapeである(1,1,19)ではなく(19)のままであることがわかります。
spec.description.output
# [name: "output1"
# type {
# multiArrayType {
# shape: 19
# dataType: DOUBLE
# }
# }
# ]
モデルの出力情報の変更は、これまたクセが強くてbuilder.spec.description.output[0]
に対して代入しようとするとエラーになります。そのため、popしたりaddしたりしながら指定してきます。
// 出力情報を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を書き換えるのは次のようにします。
builder.spec.description.output[0].type.multiArrayType.shape[0] = 100
(補足終わり)
出力情報を確認すると、うまく設定できていることがわかります。
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はおかしなモデルを読むとエラーを出してくれることもありますが、ビルド時に次のエラーを吐いていきなり落ちることもあります。
Command CoreMLModelCompile failed with a nonzero exit code
こうしたエラーが出たときは、Jupyter Notebookで実行することでエラーの詳細の情報を確認すると良いです。
ということで、Jupyter Notebookを起動します。
こちらに完全なコードがあります。
https://gist.github.com/TokyoYoshida/632b4c8070aa6c937539e4ae261a2740
モデルへの入力として[2,3]を与えて推論を実行してみます。
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
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]
//
こちらも意図通りになっていることが確認できます。
確率の数字が微妙に違うかもしれませんが、これはモデルを訓練しなおしたためなので気にしないで下さい。
番外編 レイヤーを削除する方法
動作確認はしていませんが、レイヤーを削除する方法も書いておきます。
レイヤーを削除する。
layers = builder.spec.neuralNetwork.layers
# レイヤーの後ろから2番めを取得(activation_18)
item = layers[-2]
# その要素を削除
layers.remove(item)
レイヤーの情報を表示すると、削除されていることがわかります。
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