手書き数式認識ソフト、tegashiki開発その4
前スレ
ここでやった事概要
- 簡単なケースのデータセットを人工的に合成する
- GRUのencoder-decoder-attentionでこの問題を解けるモデルを作る事に成功
- TensorFlow LiteとTPUの両方に都合の良いモデルを試行錯誤して、Convのencoder decoderを完成させる
- 手持ちの実機でハードコードしたjsonデータを整形してpredictする事に成功
実機で動いたのは大きい。やったね!
2019/06/11
簡単なデータセットを生成しよう。
とりあえず数字と文字のシンボルを集めて、それらを集めていろいろ作る、という事をやる。 まずは以下を作ってみよう。
- nonzero数字 数字
- nonzero数字 文字
- 文字 文字
- 数文字 _ 数文字
- 数文字 ^ 数文字
そのためには、nonzero数字、数字、文字の三つを集められれば良さそうか? 文字は微妙で、例えば大文字のデルタは文字なのか?というと文字の場合もあるがあんまり無い。 大文字のシグマやパイは違うだろう。でもアルファとかは文字。
この辺はまぁ適当にヒューリスティックで選ぶしか無かろう。 サポートしている語彙120くらいから、これらを人力で選ぶ。
次に各シンボルを適当に拡大縮小平行移動して並べる。 この時にaugumentationもついでに入れたい。
なんとなく雑にぶらしつつsubscriptを一つ作るコードを書いた。 全体にノイズ足すのはtensorflowのレイヤーでいいか。
明日はsuperscriptとただ並べるだけ、を作る予定。 他にもいろいろやりたいが、まずはこの三種類をトレーニングしてみるかなぁ。
2019/06/12
下付きと上付きのパターンを生成するコードは書けた。
あとは横に並べる奴も書いてトレーニングしてみたい所だが、ちょっと燃え尽きたので飯でも食い言ってあとで続きやる。
夕方に再開。 これまではpaddingとeosを区別していなかった気がしてきた。 eosは予想したいのだからこれは区別すべき。 という事で辞書ファイルを変更。 ただここまでの話にならないくらい悪いスコアからすれば小さな話である。
デバッグの為にデータセットを眺めているが、シンボルのデータは結構質が悪いなぁ。 人間が見ていても結構見分けつかないのがある。 見分けがつく方が多いのでトレーニングは可能だが、スコアが低めなのは仕方ない気がした。
逆に数式全体だとまぁまぁコンテキストで分かるのだよなぁ。
シンボルのデータは本来は手動で悪いのをはじく方が良いのだろうねぇ。やらないけど。
さて、データを生成する所まで来た。 シンボル一つも教師データに含めるかね? シンボル一つのTeX的にvalidなデータを全部集めるのは面倒だが、 とりあえず数字とsubscriptやsuperscript作るのに使っている文字だけに絞ると5万8000件くらいのデータがある。
数字と文字で63種類。どんぶり計算としては50として、superscriptとsubscriptは50x50で2500通りの組み合わせがある。 mnistは6万件と言っているから一文字6000件かぁ。 同じペースなら2500x6000なので1200万件くらい用意する事になるが、ちょっと多いよなぁ。
文字種がそろっていればいいか、と思えば50*6000件で30万件。そもそも元のデータセットが約6万件しかないのだから、ざっくり一文字1200件くらいしか無いんだよな。 だからこれ以上増やしても同じのが重複するだけなのか。
まぁいいや。同じ組み合わせが100個あれば学習してくれ、という気持ちなので、2500件x100で、25万件ずつ作るでどうだろう?
- subscript 25万件
- superscript 25万件
- 横に二項並ぶだけ 25万件
- 一文字 5万8千件
うーん、ちょっと多すぎる気がするなぁ。 全部同じ比率にしてみるか。
- subscript 6万件
- superscript 6万件
- 横に二項並ぶだけ 6万件
- 一文字 5万8千件
これで行こう。
一通りデータ作ってトレーニング流しているが、なんか凄い当たってるな。 単発のシンボルの予測よりむしろスコアが良い…
データ作る時に、なんとなくaugmentation入れてるからかなぁ。(ただ倍率と構成要素の左上の場所をちょっとずらしているだけ)。 acc 0.8くらいいってる。
augmentationめっちゃ重要だった可能性はあるな。元のデータでもちゃんとaugmentation入れたらオーバーフィットしない、という可能性はあるのかなぁ。
とりあえず一晩回して学習がどう進むか見てみよう。
2019/06/13
なんかlossは4万ステップくらいで底だが、その後lossが少し上がりつつもaccはじわじわ増加しているなぁ。 まだ上昇するのかもしれないが、既に十分なスコアだからこんなもんでいいか。
expgen_rnn_small_dropout05 acc 0.956
https://github.com/karino2/tegashiki/issues/1#issuecomment-501507465
さて、以上を踏まえてどう進めようか?
まず、やはり特定の構造に対して、シンボルを入れ替えた物がたくさん必要、という結論は堅いと思う。 augmentationを入れる事で治る可能性もあるが、やはりノイズを加えるだけじゃつらいだろう。 今回のデータはシンボル毎に拡大縮小とかちょっとずらす、みたいなのを入れられているので、全体にノイズ加えるよりはもうちょっとインテリジェントなaugmentationに出来ている。 これは既存データでは出来ない。 だから同じaugmentationを試す事は出来ない。 ノイズ足すだけは試す価値もあるが、まぁさすがにそれだけでここまで変わるとも思えない。
データの数も8000件から20万件くらいに増えている訳で、そちらの方がやはり主な理由、と考えるのが良かろう。
さて、現状は関数とか幾つかの良く使うケースをサポートしていないので実用にはならない。 だが、このくらいの限定された機能でも実機で試してみる事は出来る気がする。
よし、このモデルを実機に載せてみよう。そのために実機で手描き認識するテスト用アプリを書こう。 最終的にはライブラリにしてMeatPieDayに組み込みたいが、まずはスタンドアローンのデモアプリで開発を進めよう。
TensorFlow Liteのドキュメントを読んでたら、tanhが非対応だとか。ありゃ。attentionで使ってるな。 sigmoidはあるとか。あれ?この二つってそんな違ったっけ?
少しググったら
tanh x = 2 sigmoid(2x)-1
とからしい。じゃあなんで対応してないのかしら?まぁいいや。まだ動かないようならsigmoidで書き直すか。
(追記:普通にtanh動いた)
2019/06/14
TensorFlow Liteへのconversionで格闘。 まずTPUEstimatorからSavedModelにする為にexport_savedmodelする必要がある。 SavedModelを形式の名前にした奴は本当にひどいな。
なんかこれまでのコードが動かなくなっている。どうもtensorflowのバージョンが上がって1.14のrcバージョンになり、 そこでbatch_normalization_v1がbatch_normalizationに名前が変わってロードできなくなっているっぽい。 うげぇ。
とりあえず1.13.1に戻して作業を進める。
serving_input_receiver_fnに渡すplace_holderでは先頭にはバッチの次元が要るっぽい。 自分の用途では1なので1としておく。
build_raw_serving_input_receiver_fnに渡す辞書の名前はなんなのか良く分からない。feature_columnsのkeyと合わせておくのかな? この辺同じようなのがいっぱいあってどれが何に対応しているか全く分からん。
その他tensorflow._api.v1.liteにはOptimizeが無いぞ、とかいろいろ言われたり
https://github.com/tensorflow/tensorflow/issues/26413
こういうのをちまちま調べて対応していく。
そして現在以下のエラーが出た所で止まっている。
Check failed: other_op->type == OperatorType::kMerge Found Shape as non-selected output from Switch, but only Merge supported.
Aborted (core dumped)
ググるとissueに上がってるが、
https://github.com/tensorflow/tensorflow/issues/23772
しばらく放置して、環境教えろ、とか、まだ解決する?とかたまーに聞いて、反応が無くなると反応が無い、と閉じられている。 TensorFlow回りはこれ増えてきたなぁ。エンジニアリング力の弱さよ。 こういうのはタフなリーダーが必要だよな。
さて、TensorFlowのエンジニア力の話は置いといて、kMergeってどこで使っているんだろう?自分のモデルにはとりあえずそんな物は無い。 しいていえばGRUか。
良く分からないのでソースを見てみる。
https://github.com/rockchip-linux/tensorflow/blob/master/tensorflow/contrib/lite/toco/graph_transformations/resolve_tensorflow_switch.cc#L95
// Let us guard our assumption that only Merge nodes consume the outputs
// of Switch nodes:
CHECK(other_op->type == OperatorType::kTensorFlowMerge);
うが、逆にkTensorFlowMergeじゃない、と言っているのか。
じゃあother_op->type
が何か教えてよ!
と思って上のリンクを見直したら、これは誰かのforkか。 最新版を見るとちゃんと出力するようになっている。
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/toco/graph_transformations/resolve_tensorflow_switch.cc
うーむ、そうかぁ。新しいバージョンじゃないとダメかなぁ。 ロードの時のフックで新しいのにできそうだが、それならそもそもトレーニングしなおしたいな。
考えてみたらSavedModelはさすがにバージョン変えても動くだろうから、convertする所だけ新しい1.14.0-rc1試してみるか。
ValueError: Cannot find the variable that is an input to the ReadVariableOp.
なんか全然別のエラーになった…コードを見ると以下。
elif node.op in ["ReadVariableOp", "ResourceGather"]:
source_op_name = get_input_name(node)
... 中略 ...
if map_name_to_node[source_op_name].op != "VarHandleOp":
raise ValueError("Cannot find the variable that is an input "
"to the ReadVariableOp.")
だから期待と違う時はなんの型か表示しろって!酷いコードクオリティだなぁ、ほんと。
ダメ元でnightly buildも試したがダメだった。どうしよっかなぁ。
とりあえず1.13.1に戻して、TensorBoardでgraph見て、Switchとか使われている所を探す所からかな。
TensorFlow Liteでは幾つかの対応してないオペレータというのが書いてあるが、でかいモデルのどこに何のオペレータを使っているか、というのは、普通は分からない。 Kerasの対応するLayerなどで、Lite対応かどうかとか書いてくれないと、判定は出来ないよなぁ。 まぁ走らせてみておかしい所を直していく、というのはそれはそれで良いのだが、もうちょっとエラーメッセージ詳しく吐くようにしてくれないとつらいね。
LSTMは動くがconvertは出来てない、というissueを見つける。
https://github.com/tensorflow/tensorflow/issues/15805
モデルは結構シンプルなのだから、convertしないで最初からlite用に作るのは構わんのだよな。 ただトレーニングをTPUでしたいが。
当面の方針。1.14.0 rc1で、コードにモンキーパッチあててデバッグしていこう。
2019/06/15
やはり安定板で様子を見るか、と思い直し、1.13.1で原因究明していく方向で。
と思ったがいじりたい所がC++だ。うーん、とりあえず1.14.0-rc1でやってみるか。
やってみた。 どうもBatchNormalizationにSwitchが入っていて、これが未サポートらしい。 なんでそんなの要るのか?と思うと、どうもトレーニングモードの時だけ入るっぽい。 is_trainingをexport時に渡せてないのか。
コードを見直すと渡していなかったので修正。
今度はGRUの所でまたReadVariableOpがVarHandleOpになってない、というエラー。
gru_1/while/ReadVariableOp/Enter
になっている。ふむ。
少しググっていたら、このPRで解決しそうにも見える。
https://github.com/tensorflow/tensorflow/pull/27308
うーむ、もう一か月以上放置。酷いなぁ。
手動で当てて試した所、今度は
F tensorflow/lite/toco/graph_transformations/resolve_tensorflow_switch.cc:98] Check failed: other_op->type == OperatorType::kMerge Found Identity as non-selected output from Switch, but only Merge supported.
が。うーん。Identityかぁ。どこだろう?
やはりこの路線は非現実的な気がしてきた。
TFLiteには、experimentalの下にGRUCellとかtf.lite.experimental.nn.dynamic_rnnとかがある。これらを使うように書き直した方が早そう。 kerasを標準サポートとはなんだったのか。
2019/06/16
公式のレポジトリを眺めていたら、こんなの見つけた!
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/experimental/examples/lstm/g3doc/README.md
なるほど、GRUは動かないがLSTMは動くのか。そして謎だったOpHintの解説だ!
(追記: 一つ上のフォルダも分かりやすい解説多い。こちらとか。 https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/experimental/examples/lstm/TensorFlowLite_LSTM_Keras_Tutorial.ipynb)
大分理解したが、そもそもここまでしてRNNにする理由はあるのか?という疑問は湧くな。 TCNが動けばそれで良いのだが、こちらの方が簡単な可能性はある。
だが、これはこれでハマる可能性もあるので、まずはLSTMで動かす方がいいかもなぁ。
とりあえずGRUをLSTMにしてconvertしてみるか。
いろいろな落とし穴を切り抜けて無事モデルが出来たのでTPUEstimatorに掛けたら、
Compilation failure: Input 0 to node `gradients/tf_lite_lstm_cell/concat_1_grad/ConcatOffset` with op ConcatOffset must be a compile-time constant.
だって。fuseした結果が可変長しかサポート出来ないのだろうな。 ではTPUでTFLiteのRNNはトレーニング出来ないんじゃないか。最初に言ってよ!もぅ。
最終的なモデルを一回CPUでトレーニングするのは可能ではあるんだが、試行錯誤をデザインに組み込む方がきっと筋が良いよな。 少し頑張ってdilated CNN系列を動かすか。
2019/06/17
今日はRNNは無しにして、全部コンボリューションで行こう路線。 うーむ、それならTransformerでもいいのでは?という気もしてくるな。 まぁまずは既に作ってあるTCNベースで考えてみよう。
これまでencoderはいろいろ試したが、decoderはGRUしか試した事無かったんだよなぁ。 現状の問題としては、最大13個のストロークを表すフィーチャーがあって(256次元)、 これから最大5個のシンボルを生成する、という問題になっている。 最終的にはストロークは50個くらいになって、生成するシンボルも20くらいにしたい気はしているが、用途を考えればそんなオーダー。 しかもやはり長いのを当てるよりは短いのを確実に当てる方がまずは大切(単語ごとに変換するIMEみたいな物)。
さて、decoderもCNN系列にするなら、positional encodingとか入れる必要があるな。 ConvS2Sの論文をまずは読み直すか。
読みながら考えた事をメモる。 まずEncoderに関しては、既にfeature extractorした時点でこれでいいのでは?という気もする。 隣同士の関係が得られないが、ストロークの隣同士の関係がいるか?という気もする。
座標的な情報はそのままfeatureに保たれているはずなので、まずはそのままこのフィーチャーのリストにattentionを入れてみよう。 decoderはteacher forcingのinputにpositional embedding入れた物を入力にcausalなconv1dを、、、ってこれ、TCNでいい気がするな。
そもそも最大アウトプットが5なので、filter-size7なら一段で十分だな。というかfilter 5でいいな。
つまり、decoderとしては、positional embedding入れた、causalなconv1dを単に当てて、それにこれまで通りのattentionを入れてみるか。
よし、convdecという名前にしよう。 convdec
- エンコーダーは無しで、ストロークをEXTRACTED_FETURE_DIMのfeatureにした列をそのまま扱う(現状、最大で13ストローク)
- decoderはembeddingにpositional encodeとして位置を足す(一番目は1, 二番目は2, etc…)
- decoderは単にcausalなconv 1dとして、これの出す系列とstrokeを合わせてattentionを作り、これからpredictする
decoderのfilter sizeは3, kernel sizeは8。filter sizeが小さすぎる気がするな。 あとで増やした版も評価しよう(3になってたのはTCNでinput_type_dimと合わせるのをデフォルトにしていた為でこの場合は意味は無い)。
convdec: acc 0.20
全然ダメだな。ただ少しは学習が進んでいる。 そもそもtraining setのlossの下がり方も不十分なのでパラメータが足りない気はするな。 さて、どうしたもんか。
とりあえず単純にパラメータを増やすだけ、という事で、filter sizeを3から5に(maxのdecoderのseqlenが5)、 kernel sizeを3から128にしてみよう。
確かにスコアは改善したがいまいちだな。RNNのケースではもっとずばっと0.6くらいにはいっていたのに。
convdec_f5k128 0.237
ふと思ったが、attentionが位置も考慮に入れる為には、encoderの方にもpositional encoding要る気がしてきた。
convdec_f5k128_storkeposenc acc 0.275
改善はしたが大した事無いな。ただ学習のされ方は大分かわったので、変更が行われたのは間違いない。 strokeの方にもconvolution入れてみるかなぁ。
strokeの方に、filter sizeを64、32と減らす二段階のConv1Dを入れてみた。
2019/06/18
strokeの方にconvを入れたモデル、ロスは下がり続けているがaccが上がらない。 何が起きているのだろう? そしてaccは0.235あたりに天井があるように見える。 全てこの辺で止まっているので何かダメなんだろうな。
strokeを無視しているのかね。例えば全てをeosと予想するとどの位になるのだろう? 答えの長さの比は
l2:l3:l4 = 1:1:2
なので、スコア的には 0.51+0.331+0.25*2を4で割った物か。 0.3325。 そうか。全部eosを予想する予測器に負けているのか。全然ダメだな。
とりあえずロスが下げ止まるまでは流し続けるが次の方針を考えておこう。
(追記:結論はacc 0.233のまま)
まず、feature extractorを作り直そう。 つまりシンボル一つ予測する問題を解いてみる。positional encoding入れて。 これがたぶん解けてないんじゃないか。 で、それがなぜか、というのは良く分からないな。 convolutionの入れ方に問題があるのかもしれない。
シンプルなモデルから順番に試すのではなく、ConvS2Sとほとんど同じモデルから始めるかなぁ。 するとgluとかskip connectionとかが必要になる訳だが。
OpenSeq2SeqのConvS2Sのコードを読みつつ考える。
https://github.com/NVIDIA/OpenSeq2Seq/tree/master/open_seq2seq
OpenSeq2Seqをながめていると、Speech RecognitionでWave2Letterというのを見かける。
https://nvidia.github.io/OpenSeq2Seq/html/speech-recognition.html#speech-recognition
名前的に自分がやっている事と近いんじゃないか?少し論文を読んでみよう。
昼飯に行く前に、何か流しておきたい。 上のConvS2Sの実装を見てると、pos encodingは一回FCを挟んでいる気がするので真似してみよう、という気になる。
うぉ、pos encodingにFC挟んだら、それだけで一気に学習するようになった! うーむ、ここに挟む必要がある理屈をいまいち理解出来ていないが、このモデルならTensorFlow Liteでも動かせそうなのでとりあえずこいつをトレーニングしよう。
論文メモ:Wave2Letter
https://arxiv.org/abs/1609.03193
波形データからアルファベットの確率を推測するモデルと、 その確率の列から遷移確率などを計算して目的のalignをなんとなく学習して、それを使ってinference時はビームサーチでデコードする、というモデル。 長さの違うアルファベットの確率の列からアルファベットの系列を取り出す所が発明。
系列の長さが違うとencoder-decoderでモデル化したくなるが、こういう考え方もあるのか、とちょっと勉強にはなった。 例えば数式の言語モデルのような物を作れば、それを使ってデコーダーの所を改善する事は出来そう。
positional encodingはembedレイヤを挟む物だが、自分が論文を誤読していた模様。 この辺、どうなっていないといけないか、の感覚的な理解が無いので、どこが間違っているかを知るのに時間がかかる。 ただ今回は割といろいろ試した結果大分CNNで系列データを扱う感覚は鍛えられたと思う。 自分のモノにするには、こういう経験の積み重ねが大切なんだよねぇ。
で、positional encoding直したconvのencoder-decoderモデル。 スコア的にはaccは現在の所0.85。 もうちょっと上がりそうではあるが、GRUベースのモデルの0.956には大分届かなさそう。
ただとりあえず動かしてみてもそれなりには機能するレベルのスコアなので、まずはこれをAndroidにもっていく方向で作業したい。 うまく持っていければこのCNNベースのモデルを改善する事はいろいろ手はあるだろう。
convencdec_posfc acc 0.86
Check failed: stride * coords_shape.dims(0) == output_data.size() (32 vs. 160)
ファイルは以下
tensorflow/lite/toco/graph_transformations/resolve_constant_gather.cc:51
ふむ。ソースをgithubから読んでみると、outputの方が大きければ良さそうだが。 やっぱりpadding=causalが怪しいな。簡単なケースを見てみるか。 ただこの辺は明日だな。
2019/06/19
簡単なケースから順番に試していったら、エラーはpositional encodingのembeddingの所だ。
なんかexpand_dimsが足りないのが気に食わないらしいな。 だがこれはkerasのドキュメント的には自分が正しそうだが。うーん、どうなってるんだ?
まぁ別にembeddingくらい自分で作るか、という気もするので、自分で作る事に。
お、自分で作ってもダメだ。embedding_lookupがダメっぽい。 ただ、place_holderを直接食わせると平気。tf.rangeで作ってexpand_dimsしたものを食わせるとダメ。
これっぽいか?
https://github.com/tensorflow/tensorflow/issues/27781
いや、さらに調査していくと、どうも別件だが明らかにバグっぽい。 inputとoutputの間に関係ない所から作られるノードがあるとこのエラーが出る。
うーん、どうしよう。
条件が微妙過ぎて報告も難しいな。原因を追究してしまえるならしてしまいたい気もするがC++だしなぁ。
頑張ってone_hotとmatmulでembedding_lookupと同じ事をしてしのぐ。 reshape回りでTPUEstimatorのexportが文句言うが、今回は全部shapeが分かっているので直接値を渡すようにして無事動く。
コンバート出来たので次はトレーニングを流しておこう。最初の5000ステップ見る感じ順調に学習してそう。
convencdec_myembed2 acc: 0.73
おや?同じモデルのはずなのにaccが10%くらい低い。 違うのはembeddingの初期化くらいだが。
KerasのEmbeddingレイヤーの初期化を見るとrandom_uniformで、デフォルトでは-0.05から0.05で初期化されるらしい。 今は-1~1で初期化しているので大きすぎるのか。まぁありそうな話だ。
寝る前にこの初期化で流しなおすとして、とりあえず0.73なら動作しているのは確認出来るだろうから、実機に載せる作業を進めてみよう。
アプリのプロジェクトを作り、インタープリタとモデルのロード回りのコードを書く。 動作を確認するにあたり、Python側と比較出来る環境が欲しいな、と思う。
PythonからTFLiteのInterpreter使えないかな?と公式ドキュメントを見ていたら使えた。 allocate_tensorsがびっくりする程下の状態にべったりで使いにくいが、動作確認は出来る。 predictするとちゃんと動いてそう。
とりあえずこの入力に使ったデータをjsonにダンプしてみたが、どうせならkotlinのコードを吐いてしまえばいいか? ただあんまでかいデータはkotlinのコンパイラが文句言ってコンパイルできないんだよな。
多次元配列を作るのどうするか?とか少し考えたあたりで今日のやる気ゲージが尽きる。続きは明日やろう。
ギター弾きに行く前にお手製embedの初期値を-1~1から-0.05~0.05に変えて再トレーニングしたら普通にaccが0.81とかいっている。 やっぱ初期値重要らしい。へー。
2019/06/20
昨晩流したのはvalidation lossが上がり始めているのでここまででいいかなぁ。
convencdec_myembed_smallinit acc: 0.84
トレーニングロスはまだ下がってるので回し続けると何か変わるかもしれないが、そういう試行錯誤をする必要も無かろう。
Android側で、とりあえずハードコードしたデータをByteBufferとして食わせるコードを書く。 tfliteファイルはaaptOptionsでnoCompressにしておかないとmmap出来ないとかで失敗したりした。
今後の事を考えて、ByteBufferをラップするTensor的なクラスを作っておく。良さそう。
とりあえずpredictは出来たっぽい。logitが返ってくるのでまずはargmax作らないとな。
argmaxも出来てpredictしたクラスの結果が見えるようになった。 良さそう。
まだハードコードされたデータをpredictするだけだが、ここまで出来れば大分難しい部分は終了したと言って良かろう。
ここからはAndroid側の作業が続くので、その前にモデル関連の試行錯誤のissueを追記しておこう。 symbol一つ認識の試行錯誤は面倒なのでいいかなぁ…
元のdatasetでの試行は全てissue 1に書き終わる。 データ分析の実務とはなんぞや、という事にこれだけ明確に答えているissueも無いのではないか?
さて、人工的に生成しているデータセットの試行に移ろう。
generated simple dataset model history #3
こちらも長いので一気にやろうとするとくじけるから、ぼちぼち書いて行く。
次スレに続く。手書き数式認識ソフト、tegashiki開発その5