kerasでiOSアプリに使うモデルを作るためのTips ~ coreMLとの戦い 編 ~

[前回](https://qiita.com/Mco7777/items/158296ed7f66aed2ffc3)の続き

ちょうど良かったのでKerasのアドベントカレンダーに参加しました。

# 前置き

この話は2017年12月3日現在のお話です。各種オープンソースのバージョンアップによってどんどん解決されているかもしれません。

# Lambdaの便利さに取り憑かれたが故の悲劇
前回はLambdaの便利さについて書きましたが、実はある特定の目的でモデルを設計していると（現時点では）悲しみを背負うことになります。そうです、私は背負いました。

機械学習が色んな所で気軽に使えるようになってきた昨今、浮足立っているエンジニアの中で私も例に漏れず

「iOSアプリで学習したモデルを気軽に使えたら楽しそうだなぁ」

って思ってた所に発表されたのが coreML です。しかもなんと、公式でkerasのモデルをcoreMLで利用できるようにするためのコンバーター（[coremltools](https://github.com/apple/coremltools)）がオープンソースで公開されていると。

# Lambdaは使えません
そりゃもうワクワクしながら作ったモデルを変換してみましたね。チュートリアルのモデルを見たまんま変換してみて実際にxcodeで開いて感動し、いよいよ自分の作ったモデルでもやってみよう…！と思い変換した所に立ちふさがったエラーが

```
ValueError: Keras layer '<class 'keras.layers.core.Lambda'>' not supported.
```

＼(^o^)／

普通最新の技術ってエラーが良くわからなかったり、ソースコード追ってバグじゃないかって探してあわよくばプルリク投げたりとか色んな奮闘があると思うんですが、こんなはっきりとばっさり止められたのは逆に清々しいですね。

ちょっと調べてみると[このフォーラム](https://forums.developer.apple.com/thread/82116)には

> The problem with lambda layers is that they can contain *anything*, even things that Core ML does not support.
>> 問題はLambda「何でも」できすぎちゃう事だよね。coreMLで明らかにサポートできないものも含めて。（筆者意訳）

とありました。まぁそうだよね。でもそれならそれでkeras側でSplit的なレイヤーとBackendをLambda挟まずに使えるようなラッパーになるレイヤーほしいなと。 
`Concatenate` があるんだから `Split` くらいはできるんじゃないかなぁ。Backendも最低限の四則演算とか平方根、絶対値くらいが使えるだけでも良いんだよなぁ。Lambdaが便利すぎるが故にどんだけググっても

<strong> Lambda使ったらできるよ！（満面の笑み） </strong>

という情報しか出てこないし、多分それが現状の正解なんだと思います。coreMLなんて新参者に対する情報はまだストックされないよね。。

そう思ったら自分で実装してプルリクエスト送れって言う人もいると思います。自分もそう思いました。でも

- kerasのコード見る → kerasのコードって意外とシンプルなんだなぁ
- 該当箇所を見る → うーん、結構頑張ったらできなくもないのかなぁ
- ここでふと思う → あ、これkeras側でレイヤー作って仮に万が一マージされたとしてもそれを使ってモデルを変換できるようにcoremltoolsも修正しないとダメじゃん

今この辺でうだうだしている所です。誰か有能でここまでバババッと書けちゃう人いたら救いの手を。

【追記2018.05.09】
Lambdaレイヤーも使えるようになっているようです！
が、Lambda内部の実装をswiftかobjective-cで同一処理を行うクラスをCPU版,GPU版とそれぞれ実装する必要があるそうで、気軽に何でもとは行かなさそうです。

[http://machinethink.net/blog/coreml-custom-layers/](http://machinethink.net/blog/coreml-custom-layers/)

# 入出力のShapeの話
これは知っているか知っていないか、というだけの話ではありますがcoreMLでは入力/出力の形式として

- 単純な1次元配列
- 時系列x1次元配列 の2次元配列
- 高さx幅xチャンネル の画像を想定した3次元配列

これしか受け付けてくれません。（少なくとも私の認識では。）
kerasでは割と自由に入力のShapeなんかを変えて色んなデータを突っ込んだりするかと思いますが、そうしたい場合Inputや出力では低次元配列に全部入れ込んだ後、モデルの中でReshapeしてあげる必要があります。

```reshape.py
model_input = Input(shape=(30,)
reshaped = Reshape((3,5,2))(model_input)
output = Reshape((30,))(reshaped)
```

【追記】[ここ](https://github.com/apple/coremltools/issues/70)で議論されてました。

# Reshapeの話
実は上で書いた例であれば、3次元に収まっているので（画像じゃなくても）画像という体で入出力できなくはありません。実際問題になってくるのはどういう時かというと、私の例で言えば「時系列の画像データ」を扱いたい時でした。データのShapeとしては（時系列, 高さ, 幅, チャンネル）となる訳で、これは上記の通りcoreMLでは入力データにそのまま入れられません。

なのでこうしたいのですが、

```reshape2.py
model_input = Input(shape=(10,30,)
reshaped = Reshape((10,3,5,2))(model_input)
```

実はこれもできません。さすがになんでやねんと思いIssueをチェックしてたら数日前に似たような問題でIssueを立てている人を発見して色々議論しました。
[https://github.com/apple/coremltools/issues/69](https://github.com/apple/coremltools/issues/69)
結論、4次元以上（バッチ次元を含めると5次元）のTensorは現状受け入れられないようです。
これは入出力云々レベルではなく、知らないとモデルの構成の根本からばっさり切り捨てられることになるので注意しましょう。

ただし、coremltoolsのバージョンアップで解消される可能性もあるので、実際に小さいモデルでご自分で試してから判断されるのが良いと思います。

# TimeDistributedConv2Dをどうしても使いたい
結局4次元のTensorが使えないとなると時系列画像をそのまま扱うことはできません。でもここで諦めるのも癪なので次元の問題を回避してConv2Dを時系列に使うやり方を見つけました。それがこれです。

```timedistributed_conv2d.py
inputs = [Input(shape=(30,30,3)) for _ in range(10)]
conv = Conv2D(12, kernel_size=(2,2), padding='valid')
conved = [conv(_i) for _i in inputs]
flat_layer = [Flatten()(_i) for _i in conved]
concat = Concatenate()(flat_layer)
```

入力を複数受け入れられることを良いことに時系列データを複数の入力にしてしまっています。上の例では30pxの正方形3chの画像を長さ10の時系列で受け取っています。
あとTimeDistributedは基本重みを全ての時間で共有しているっぽいのでconvというインスタンスを用意して全部に適用しています。

ちなみにTimeDistributedについてはまた別の記事を書こうと思います。ドキュメントは[これ](timedistributed keras)です。

ここでのキモはその後のflat_layerで最終的にデータをフラットにすることでなんとか次元が4を超えないようにした後、Concatenateしている点です。とんでもなく無理やりです。coreMLを使う予定がないのであれば素直にTimeDistributedを使いましょう。

上の例は長さ10の時系列ですが、音楽データなどを扱いたいと思うとこの時系列は平気で100とか1000とかになってくると思います。そうすると入力数がとんでもないことになります。（一応入力数の制限はないっぽくて、300個くらいの入力でもちゃんとmlmodelができました。）

## しかしまだ駄目
このモデルでkeras側でもなんとか学習させたり予測させたりはできるモデルなのですが、実はこれもcoremltoolsからエラーを返されます。
これはまだ解決できていないので明確なことは書けないのですが、明らかにモデル側の問題ではなさそうだったのでIssueを立てました。
[https://github.com/apple/coremltools/issues/73](https://github.com/apple/coremltools/issues/73)

ここに書いたように、Conv2Dのレイヤーを使いまわすのではなく時系列内の1つずつに対して生成してやるとmlmodelの変換までうまく行きました。

```timedistributed_conv2d2.py
inputs = [Input(shape=(2,3,4)) for _ in range(3)]
conved = [Conv2D(12, kernel_size=(2,2), padding='valid')(_i) for _i in discriminator_in]
flat_layer = [Flatten()(c) for c in conved]
model = Model(inputs=discriminator_in, outputs=flat_layer)
```

しかし、察する方もいると思いますがConv2D層が時系列の長さ分できるのでパラメータ数が尋常じゃなく跳ね上がります。まぁそもそもモデルの形も変わっちゃうのであんまりしたくはないんですよね。。

こちらに関しては解決策が分かったら別記事にするか、上記のIssueの中で議論します。

【追記】レイヤーの使い回しについて完全解決ではないものの一部coremltools側の問題を修正してみました → https://qiita.com/Mco7777/items/1274c7a435f65c044466

ちなみにFlattenやReshape等の学習対象のパラメータを持たないレイヤーはパラメータも増えませんしモデルの構造が変わってしまうこともないので、このやり方で特に問題無いです。逆にFlatten等のインスタンスを使いまわすとこれもcoremltoolsに、複数の入力を持っている、とかで怒られます。

【追記】この問題も解決できたっぽいです → https://qiita.com/Mco7777/items/1274c7a435f65c044466#追記

# Concatenateの話
これもkerasで閉じた世界であれば一切意識しなくて良い内容なので辛いところですが、

```
ValueError: Only channel and sequence concatenation are supported.
```

らしいです。なので0次元目での結合、もしくはチャンネル次元（これはモードによって違いますが1か3でしょうか）で結合しかできないようです。
やるとしたらどうにかこうにか次元をひっくり返したりして繋いで戻して、といった操作が必要になってきそうです。

<font color="DarkGray">そう思って実装するとその操作が許可されてなかったりします。が、ちょっとそこまで試せていません…</font>

【追記】やってみました → [https://qiita.com/Mco7777/items/a0e2c3d75874a0052743](https://qiita.com/Mco7777/items/a0e2c3d75874a0052743)

# Pythonのバージョンの話
これは割りとシンプルな話なのですが、coremltoolsはpython3に対応していません。[今時の小学生はAppleでさえもディスっちゃうんすね。](https://togetter.com/li/1027888)
さらに面倒なことにpython3で保存したkerasのモデルはpython2のkerasからは読み込めません。

という事でpython2を使いましょう。

とは、いかないですね。今更2を使えと言われても色々面倒すぎる人もいると思います。私もそうです。
なので私は

- python3で保存したモデルを読み込み、 `save_weights` で重みだけ保存する。
- python2でモデルの定義を読み込み、 `load_weights` で重みを読み込む。（これは可能）
- python2でそのままモデルを構造ごと保存する。

こんな感じでやってます。一応重みだけならどうにかなるのと、モデルの定義ファイルを上手いことPython2,3互換で書いてしまえばなんとかなります。

ただ、早いところPython3に対応してほしいですね…

→ いつの間にか対応していました！
https://pypi.org/project/coremltools/

# Pythonのバンドルの話
これは他の環境の方は当てはまらないかもしれないのですが、当方MacのHigh Sierraで色々やっているわけですが、coremltoolsがモデルによってはこんなエラーを吐きます。

```
Fatal Python error: PyThreadState_Get: no current thread 
```

こうならずにモデルが普通にコンバートできる場合もあったので正直よくわからないのですが、[ここ](https://forums.developer.apple.com/thread/80529)で言われているように、Macに標準でインストールされているPythonを利用するようにしたら起こらなくなりました。

もし同じ問題を抱えている方は試してみてください。（ただしMac標準のPythonはpipすら入っていない不親切設計なので気持ちを穏やかにセットアップしてあげてください。）

# 結局

今現在作っているモデルは、結局LambdaやBackendの計算を頑張って排除したものを学習させたりしています。入力を工夫したりとかBackendで行う計算をなんとか外部に出してSwift側で計算させるようにするとか（モデルが分断されるので多分計算リソースを理想的に使えないんだと思いますが。。）しています。

その他諸々書いたような問題には全てぶち当たって来て、未だに自分の理想のモデルはmlmodelになっていません。（conv2Dを使いまわせない件）

まぁ自分はこのような問題に早めに気がつけたので良かったのですが、

```
上司「coreMLってkerasのモデル使えるらしいじゃん！こういうモデル作ってアプリ作ってよ！」

エンジニア「了解」

〜 モデル設計・学習・精度調整 にたくさんの時間と人的リソースと計算リソース（要するにお金）を使う 〜

エンジニア「できた！」

coremltools「ValueError:」「KeyError:」「RuntimeError:」
```

こうなってしまった後のエンジニアさんの悲痛な叫びは想像に難くありません。
少しでもこのような悲劇を減らすためにこの記事が役に立つことを祈ります。


<font color="DarkGray">coremltools含めcoreMLはかなり新しい技術で、それこそ開発がかなり盛んに行われているものですので、最終的にkerasとの互換性が100%近くになってLambdaの問題もShapeの問題も全て解決されればそれは素晴らしい未来です。なので筆者はこの記事が役に立たなくなる時が来ることを願っています。</font>


ちなみに今回の記事の内容はcoreMLを使うつもりがないのであれば<strong>全て読まなくて良いです。</strong>（今更）