coreMLでkerasのモデルが使える!って喜んでブログ書いている人々
に言いたいんですが、チュートリアルと言うかテンプレに近い画像処理やDense繋げただけのモデルやって満足している感じの人多すぎませんか。
もちろん元々画像処理系のアプリを作ってて、そこにcoreMLがバーン!と登場してきたって人は別に良いんです。いいタイミングで良いライブラリやフレームワークが登場しておめでとうって感じです。
たがしかし、別にTensorflowでNN作ったり学習させたりしている人らは画像処理しかやってはいけない訳ではないので、それ以外のモデル作ってる人もいるんじゃないでしょうか。正直そういう時にkerasはかなーり柔軟にモデル設計ができて、頭に思い描いたネットワークを構築、学習できるツールとしてとても優秀です。しかしkerasが優秀すぎるが故にcoreML側(正確に言うとコンバートするためのツールであるcoremltools)がついてこれていないようなのです。
kerasでちょっとでもチュートリアルから外れたモデルを作ろうとしたことのある人々
には、是非coremltoolsにぶち込んでみてほしいです。多分なんやかんや難癖をつけられてmlmodelファイルできません。
それについてはここ https://qiita.com/Mco7777/items/c5df09e8b68988cdf916 に色々書きました。
多分これ以外にもモデルの構造によって色んな問題があると思うので、是非ともQiitaでもブログでも良いので共有してください。きっと誰かが助かります。
どうしようもないものもある
上の記事に書いたのはkeras側でどうこうすれば回避できる回避策を模索したものになります。ただ、coremltoolsの実装次第でkeras側でどうしようもないものもあります。その中の一つがこのようなモデルでした。
inputs = []
for i in range(3):
inputs.append(Input(shape=(3,)))
dense = Dense(5)
densed = [dense(_i) for _i in inputs]
model = Model(inputs=inputs, outputs=densed)
kerasでfunctionalAPIにあんまり慣れていない人には分かりにくいかもしれませんが、3つのインプットを1つの重みを共有しているDenseを通して3つのアウトプットに出力している単純なモデルです。
確かにチュートリアル的なモデルではないですが、至ってシンプルなものです。実はこれですらcoremltools(現在のバージョンは0.7.0)ではコンバートできません。
原因として挙げられるのは重みを共有して複数の場所で同じレイヤーを使用しているからです。coremltoolsではこういうレイヤーをshared layerと呼んでいるっぽいです。
多分(coremltoolsのコードを軽く読んだだけなので間違っていたら指摘歓迎です)ですが、このようなshared layer自体、coreMLでは対応していないっぽく、converterはどうしているかというと同じ重みのレイヤーを複数コピーを作って無理やり前後のレイヤーを繋いで凌いでいるっぽいです。その証拠にshared layerを持つモデルをコンバートするとこのような出力になります。
0 : input_1, <keras.engine.topology.InputLayer object at 0x11947a190>
1 : input_2, <keras.engine.topology.InputLayer object at 0x11947a3d0>
2 : input_3, <keras.engine.topology.InputLayer object at 0x11947a250>
3 : dense_1_0, <keras.layers.core.Dense object at 0x11947a4d0>
4 : dense_1_1, <keras.layers.core.Dense object at 0x11947a4d0>
5 : dense_1_2, <keras.layers.core.Dense object at 0x11947a4d0>
object at の後ろをよく見てみてください。inputはそれぞれ違うオブジェクトなのでアドレス(?)が異なりますが、denseレイヤーは全て同様のオブジェクトを指しています。しかしcoreMLではdense_1_0〜1_2と3つのレイヤーとして作られてしまっていますね。
まぁそもそもcoreMLでは学習しようとはしていないので、同じ重みをコピーしたレイヤーを使うのであれば実質問題ありません。
しかし思い出してください。上で「コンバートできません」と書きました。実は上の出力の例はこちらのPR https://github.com/apple/coremltools/pull/92 での修正後の出力です。(まだマージされているわけではないのでもしかしたら致命的なミスをしている可能性はあります。)
何が問題かというと、アウトプットに近い位置のshared layerを含むモデルをコンバートしようとするとcoremltoolsの一部で存在しないキーをディクショナリから取得しようとしていたためKeyErrorとなってしまっていました。なので単純な話、coremltools 0.7.0ではコンバートできません。
※ ただshared layerの扱い方自体は実装されていたため、モデルの条件によってはshared layerを持つモデルでもコンバートできるものもあります。上に「アウトプットに近い位置」と書いたのがそれですが、詳しく書き始めると長くなるので気になる人はコード読んでみてください。Pythonですし結構読みやすいです。
これに関して修正してPR投げてやろうと思ってたらそれだけじゃまだ駄目でした。上記のモデルではshared layerをそのままアウトプットに繋いでいます。PRにも書いていますが、coreML用のoutputのレイヤーを作成する所で
self.output_layers = [self.get_coreml_layers(kl)[0] for kl in self.model.output_layers]
…
def get_coreml_layers(self, keras_layer):
coreml_layers = []
for key in self.keras_layer_map:
if self.keras_layer_map[key] == keras_layer:
coreml_layers.append(key)
return coreml_layers
このように、keras側のオブジェクトと一致するものを探してきた配列の[0]番目を常に使う実装がされていました。
こうなるとdenseは3つあるのに3つのアウトプットに全て1番最初のdenseが繋がれます。なので
RuntimeError: Error compiling model: "Error reading protobuf spec. validator error: Interface specifies output 'output1' which is not produced by any layer in the neural network.".
このようなエラーになりました。おっしゃる通りどこにもつながっていないレイヤーがおるぞと。
こちらの修正も先程のPRで投げている( https://github.com/apple/coremltools/pull/92/files#diff-76b8e1a1a481e41b38dbaabfbc4d10e4R173 )ので、もし同じ問題でお困りの方がいたら私のforkリポジトリからビルドしたらこのようなモデルも変換できるようになりますのでよろしければどうぞ。
ちなみに最初何を勘違いしたか同じオブジェクトだからマージして一本化しちゃえばいいじゃん!とか思いながらアホみたいなプルリク飛ばしてしまっています。が、気づいて修正しているので許してちょ。
#追記
ここ https://qiita.com/Mco7777/items/c5df09e8b68988cdf916#しかしまだ駄目 の最後の段落で書いていたFlattenやReshapeについてですが、なんとこのPRの修正で共通層を使ってもエラーにならなくなるという副作用がありました!
多分ですが、今までshared layerがそもそも少し問題がある状態で読み込まれていたり出力の選択の仕方に問題があったせいで副次的にこのような重みが一切関係ないものまで影響を受けていたっぽいです。なので同じ入力shapeから同じ出力shapeのFlattenやReshapeが何個も生成しないといけない状態は(問題ではないのかもしれませんが)気持ち悪かったのが、1個のFlatten層Reshape層を使うスッキリした形で使えるようになりました。
PRでも言及しておきました。ちゃんとレビューされて本当に正しいかどうか確認できるといいなぁ
https://github.com/apple/coremltools/pull/92#issuecomment-352234589