ディープラーニングでメイドインアビスの欠落文字を推定する

  • 22
    Like
  • 0
    Comment
More than 1 year has passed since last update.

注意

  • 漫画ネタです。
  • PythonもKerasも機械学習も経験浅いです。間違いや迷走が多々あると思われます。
  • ろくな結果が出ていません。代案、アイディア募集してます。

動機

  • メイドインアビス大好き
  • Kerasがめちゃくちゃ簡単なので何かやってみたかった
  • QiitaにKerasの記事が少なめなので紹介のつもりで

問題設定

奈落文字について

メイドインアビスはつくしあきひと先生(以下つくし卿)が連載されている穴もぐりまんがです。
作中ではひらがな、カタカナをベースにしたと考えられる文字、奈落文字が使われています。有志の方が作成された解読表がありますので以下に掲載します。ありがとう有志の方!
解読表
表を眺めると多くの文字がひらがなかカタカナの対応する文字に似ていることがわかると思います。
さて、表中に幾つか空白箇所がありますが、これらは未登場の文字です。こういう欠落があると困るわけです。ファンレターを書く際などに。
今回は最近ちょこちょこ勉強しているディープラーニングを用いて、これら欠落文字を推定することを目指します。

詳細な問題設定

具体的にはかな文字画像を入力とし、対応する奈落文字画像を出力として得るネットワークの構成を目指します。
最終的に推定を目指す文字は「せ」、「ぬ」、「ほ」、「を」です。
「せ」、「ぬ」、「を」はそのまま欠落文字で、「ほ」は未登場ですが「ぼ」からある程度推測できるため指標として用います。

環境

MacBook Pro (Retina, 13-inch, Early 2015)
OSX 10.12.1(16B2555)
CPU: 2.9 GHz Intel Core i5
メモリ: 8 GB 1867 MHz DDR3

Python 3.5.2 :: Anaconda 4.1.1 (x86_64), Keras(tensorflow backend, CPU-only)

ついでにリポジトリ
https://github.com/t-ae/abyss-letter

データセット

かな文字

ETL文字データベースよりETL6とETL7を使用させていただきました。ファイルフォーマットはこんなかんじです。
それぞれカタカナ、ひらがなが含まれていますが、画像サイズとファイルフォーマットが揃っているので展開スクリプトは共通にでき、サイズを揃える処理も不要になります。
ただし、濁点、半濁点付きの文字が含まれないので、それらの学習は諦めることになりました。奈落文字は濁点の特徴が顕著な文字なのでこれは痛い。

画像データは64x63の輝度4ビットとなっているので、32x32に縮小しつつnp.float32の画像にして文字種ごとにnpyファイルに保存します。numpyだとこういう繰り返しデータをいい感じに扱えて楽しいです。背景に結構ゴミが入っているので0.5未満のピクセルは全て0にしています。

バリデーション用とテスト用を除いて、学習に使う画像は45,000枚ほどです。

奈落文字

昔作った手書きフォントがあったのでそれをもとに画像データを作りました。
リポジトリに同梱しています。
使用する文字は元となったかな文字がほぼ明確なもののみとしています。
一文字種に画像一枚じゃ微妙かとも思ったので、学習に使う際は15度以内の左右回転を加えたものを(気休めに)使っています。おいおい見ていきますがこの回転はいらなかったかもです。『おいおい』だぜ。

作戦その1 - CNN

そもそもこの問題はAutoencoderをいじくり回しているときに思いついたものでした。なのでまずはシンプルにConvolutional Autoencoderの出力を奈落文字にかえただけのモデルを試します。
Kerasはモデルの画像出力が簡単にできます(追加のモジュールが必要です。参照)ので試しにやってみます。
今回作ったモデルはこちら。コードだとこのへん
abyss_model.h5.png
でかい(画像が)。フィルタ数等は手探りなので大きめにしました。Autoencoderにならって絞りきったところは入力の32x32x1より小さくなるようにしています。

画像中に出ないようですが、MaxPooling2D前のConvolution2Dは全て活性化関数にReLUを使っています。Kerasには活性化関数の指定方法が幾つかあり、Convolution2Dの引数に文字列で与えた場合は画像に出力されないようです。
対してUpSampling2Dの前のConvolution2DにはELUを使っています。ELUのほうが収束しやすいとかそんな感じらしいです。
MaxPoolingの前で使っていない理由ですが、最大値のみを取るのに0以下の小さい部分がどうなろうと大して関係ないだろぉーという想像のためです(無論Pool内全て0以下だったら効いてきますが)。

さて、こいつを訓練していきます。Kerasには素晴らしいタダ飯が用意されているのでありがたく頂きます。よ……要は味だぜ味……。エポック数を適当に大きくしてやれば収束判定して勝手に終わってくれます。1エポック20分くらいなのでメイドインアビスを読みなおしながら待ちましょう。


9時間以上経過してやっと終わりました。自分でもやってみようという方はなるべく耐えて下さいね。KerasのTensorBoardのログを吐いてくれる機能を設定していたので見てみましょう。
simple_cnn_tensorboard.png
ごみが入っていますがいいかんじに収束してそうです。
このときはEarlyStoppingpatienceは3にしていたのですが、2でも長いくらいだったので1で良かったと思います。逆に初期値の0は、今回の汚いデータセット(lossの減少に対しval_lossが上がりがちなデータセット)だとぱっと見にも不十分な段階で終わってしまっていました。

推定させてみる

早速推定させてみます。Kerasは一行でモデル丸ごと保存読み込みもできるので簡単です。まずは学習に使った文字たちから。
simple_cnn_NNAA-.png

再現はできてますね。ブレまくってるのは回転を加えたせいでしょう。ただのランダム回転で入力画像との相関がないため、こんなにブレているのだと考えられます。
気付く方は気付くと思いますが学習文字に「ン」が入り込んでいます。「ん」はひらがなベースなのですが……。全て終わったあとに気付いたので以降のサンプルでも「ン」が出続けます……。

さて、次はお待ちかねの欠落文字を見てみます。
simple_cnn_SENUHOWO.png

「せ」はなんとなく「も」っぽいのを作っていますね。「ヌ」も「ス」や「メ」っぽくなっています。「ホ」は「ネ」にひげを足したような見た目になっていて文字生成としては悪く無さそうです。「ぼ」から遠ざかりすぎですが(「ぼ」がひらがなベースなので本当は「ホ」は要らないです)。「を」は「も」と「ラ」に引きずられています。似ている文字があるとそれに寄っているようで、複数の文字の特徴をちゃんぽんにしてくれないかという期待は外れました。
それ以外はなんかもあもあができているだけです。

作戦その2 - Autoencoder + CNN

作戦1は上手く行きませんでしたが、そもそも適当にでっちあげたモデルなのであまり期待していませんでした。さらにマニュアル的な方法を全然使っていないのでどのあたりでだめになっているのかが全然分かりません。
というわけで次の作戦はAutoencoderで特徴抽出までやらせて、その上にCNNを乗っけて奈落文字を構成する、です。
先に特徴抽出だけ行い結果を確認することでその部分が確実にできていることを確認しつつ、以降上層をいろいろ試す際に特徴抽出部分をすっ飛ばすことができて総学習時間の短縮にもつながるだろうという狙いです。

モデル画像はでかいだけで大して役に立たないようだったのでコードを抜粋します。

encoder = Sequential([
    Convolution2D(16, 5, 5, border_mode='same', activation='relu', input_shape=[32, 32, 1]),
    MaxPooling2D(border_mode='same'), #16x16
    Convolution2D(32, 5, 5, border_mode='same', activation='relu'),
    MaxPooling2D(border_mode='same'), #8x8
    Convolution2D(16, 3, 3, border_mode='same', activation='relu'),
    MaxPooling2D(border_mode='same') #4x4
])

decoder = Sequential([
    Convolution2D(16, 3, 3, border_mode='same', activation='relu', input_shape=[4, 4, 16]),
    UpSampling2D(), # 8x8
    Convolution2D(16, 3, 3, border_mode='same', activation='relu'),
    UpSampling2D(), # 16x16
    Convolution2D(32, 5, 5, border_mode='same', activation='relu'),
    UpSampling2D(), # 32x32
    Convolution2D(1, 5, 5, border_mode='same', activation='sigmoid')
])

autoencoder = Sequential([encoder, decoder])

エンコーダとデコーダを分けつつまとめて読みやすいですね。
Kerasのドキュメントには書いておらず、ここのFine-tuningの項の例を読んで初めて知ったのですが、SequentialのレイヤーとしてSequentialをそのまま突っ込むことができます。
この状態でautoencoderを学習すればencoderのほうも重みが入っており、そのまま保存できます。こっちの記事ではやたらめんどくさげな書き方をしていますが、得られるものは同じはずです。
フィルタ数は記事のMNIST Autoencoderを参考にしつつ、画像サイズや文字種が大きくなっている分すこし増やしたほうが良いだろうと思って増やしていますが、そもそも文字のクラスを学んでいるわけではなく特徴を学んでいるわけなので、変えなくてもよかったかもしれません。

こいつの収束までは60エポックで5時間弱かかりました。

Autoencoderの性能を見てみる

Autoencoderの学習が済んだらとりあえずうまく再現できているか確認してみます。
まずは学習した文字種から。
ae_NNAA-.png

できてそうですね。次に欠落文字。

ae_SENUHOWO.png
学習文字と比べるとかすれが強いですがだいたいできてそうです。これらの文字種は学習データにもバリデーションデータにも一切出てきません。これはもう特徴を学習できていると考えていいのではないでしょうか。
一見外挿なのでここまで再現できることに感動したのですが、画像の特徴を学んでいると考えれば当然のことかもしれません。

さて、このAutoencoderからエンコーダのみを抜き出し、CNNにつなげます。モデルはこんなかんじになります(ソースコード)。

encoder = load_model(encoder_path)
encoder.trainable = False
for layer in encoder.layers:
    layer.trainable = False

generator = Sequential([
    Convolution2D(32, 3, 3, border_mode='same', input_shape=[4, 4, 16]),
    ELU(),
    UpSampling2D(), # 8x8
    Convolution2D(32, 3, 3, border_mode='same'),
    ELU(),
    UpSampling2D(), # 16x16
    Convolution2D(64, 5, 5, border_mode='same'),
    ELU(),
    UpSampling2D(), # 32x32
    Convolution2D(1, 5, 5, border_mode='same', activation='sigmoid')
])

model = Sequential([encoder, generator])

エンコーダを読み込んでtrainable=Falseにすることで重みの更新をしないようにしています。これで上層のgeneratorのみの学習ができます。
Sequentialtrainableを持っているのですが、全てのlayerにも設定しないと重みが更新されてしまうようでした。layerの方につけないで一回回したので数時間無駄になりました。他にもコピペのせいで最後のsigmoidを付け忘れたまま一回回していたりするのですが。

ではこいつも学習させます。

推定

patience=2でしたが95エポック13時間もかかりました。作戦1と比べてフィルタ数も少なめにしたし入力も少ないのになぜ……。tensorboardも貼っときます。
ae_cnn_tensorboard.png
まだ落ちそう……度し難し。

ではまず学習文字から。
ae_cnn_NNAA-.png

作戦1とそんなに変わらないですね。ブレがひどくなったような。

お次は欠落文字。
ae_cnn_SENUHOWO.png

やっぱり似た文字に引っ張られていますが、引っ張られ方は多少軽減されたような?(主観)
「せ」や「ほ」の一番左なんかは新しい文字感ないですかこれ?

作戦その3 - Autoencoder + Denoising Autoencoder

手短に……最後の作戦を伝えるぜ

  1. 作戦2で作ったエンコーダでかな文字特徴を抽出
  2. 奈落文字についてはDenoising Autoencoderを学習させてデコーダを作成
  3. かな文字特徴からアビス文字特徴への変換をDenseレイヤーに学習させる

Denoising Autoencoderにしたのは学習データが回転しただけの奈落文字しかなく、不十分な気がしたからです。あと使ったことなかったのでやってみたかったというのが大きいです。
Autoencoderのデコーダ側を活用する例は知らないのですがまぁものは試しということで。

Denoising Autoencoder

Denoising Autoencoderは作戦2で使ったAutoencoderからフィルタ数を削減したものにしました。作戦2の段階で多すぎた気がしていたので。
早速できたのを見てみます。

dae_NA.png
Noise:40%くらいまでは再現できるっぽいですね。ごま塩ノイズなのでもともと0のところに0が振られることが多く、40%は設定上の数値というだけで、実際は20%強というところだと思います。

Denseレイヤーでつなぐ

作戦2で作ったかな文字エンコーダと、先ほどできた奈落文字デコーダをDenseレイヤーで繋いでやります(ソースコード)。

# load encoder
encoder = load_model(encoder_path)
encoder.trainable = False
for layer in encoder.layers:
    layer.trainable = False

# load decoder
decoder = load_model(decoder_path)
decoder.trainable = False
for layer in decoder.layers:
    layer.trainable = False

dae_generator = Sequential([
    Flatten(input_shape=[4, 4, 16]),
    Dense(512),
    ELU(),
    Dense(512),
    ELU(),
    Dense(4*4*8),
    ELU(),
    Reshape([4, 4, 8])
])

model = Sequential([encoder, dae_generator, decoder])

エンコーダ、デコーダ部をtrainable=FalseにしているのでDenseレイヤーの部分だけ学習されます。
上手く行けばかな文字特徴から奈落文字特徴への変換ができるはずです。

推定

例によって学習文字から。
dae_ae_NNAA-.png

作戦2よりぼんやり感が強くなりましたが形状は残せてます。

お次は欠落文字。
dae_ae_SENUHOWO.png

似た文字から遠ざかってはいるようですが新しい字と言えるほどのものにはなっていないですね。
「ヌ」の「ス」っぽさはだいぶ薄まりました「メ」っぽかったのもほとんどわからなくなっています(もあもあしてよくわからなくなっていますが)。最後なのでついでに「ス」も貼っておきます。

dae_ae_SU.png

こちらのほうはより原型のスに近く、二画目が突き出しているかいないかを判断できているようです。

まとめ

三種類やってみましたがどれも似たり寄ったりな結果となってしまいました。
かな文字の特徴抽出はできていることまでは確認できましたが、それから奈落文字を書き出す部分がやはり難しいようです(かな文字特徴でなく一般画像特徴になってしまっていたんじゃないかと思ってます)。
そもそも字数が40種程度と少なめで、変換の規則性を導出しようとすること自体が難しかったのではと思います。例えば中国語フォントAと中国語・日本語を含むフォントBを用いてAっぽい字体の日本語フォントを作るという試行ならもうちょっとはそれらしい出力が得られるかもしれません。
奈落文字データの回転ですが、どうせかな文字との相関が無かったのでこれは無くても良かったと思います。むしろかな文字の特徴を奈落文字に変換するというプロセスでの余計なノイズとなり、もあもあが加速していた感があります。

他の生成モデルについて

DCGAN

画像生成では有名なやつですね。学習データが十分ならば奈落文字っぽい画像を生成することはできそうです。
ただ文字種が少ないため、それらの文字種と判断されないとDiscriminatorが否定するように学習される気がしており、その場合Generatorもそれらの文字種しか生成しなくなるので新規の文字を作るのは難しいそうに思います。
そもそもかな文字をベースに作りたいのでランダムに生成されるDCGANは向いていないです。Generatorを奈落文字デコーダとみなし、下層にかな文字エンコーダをつけて学習させるという方法なら特定の文字種に対応した出力が得られるかもしれませんがどのみちデータが足りないので試せません。

VAE

Keras Blogの記事で知りました。例ではMNISTを学習させてそれらの中間のような文字を生成しています。既知の文字から未知の文字を推定しようという今回の試みですが、未知の文字を既知の特徴から作るという点では合致しています。
こちらの記事では筆跡を入力として、似た筆跡の数字を生成するという例が挙げられています。逆に文字種を入力として筆跡(字体)を付加するものが作れれば今回の問題にも使えそうです。

このふたつくらいしか知らないです。使えそうなモデルをご存知でしたらぜひ教えてください。

感想

まずなによりデータセットに濁点文字が含まれていないのが残念でした。もしあれば濁点有無の文字と「ぼ」から「ほ」を推定するというのができていたと思います。ETL8には濁点ひらがなは含まれているようなので、濁点カタカナを含んだデータセットも是非欲しいところです。

Kerasは本当に素晴らしいライブラリでした。今回は触れていませんがRNNも簡単に扱えるのでTensorflowのCNNまでしか手を出していない人はやってみると良いと思います。MNISTの画像を時系列データ代わりに使うとかでも結構精度でて面白いです。
この記事内でKerasの機能やら得られた知見やら書きましたが別に一本記事書いても良かったかもしれません。

この試行は文化の日と有給を使った四連休で行ったのですが(オイラ勤勉だからな)、思っていたよりだいぶ学習に時間がかかって月曜までに終わらないんじゃないかと終始焦っていました。試行錯誤が必要なときはGPUを使える環境のほうが時間的にできることが多くなるので、次何かやるときはそうしたいと思います。費用がかさみそうですが……。

では、Kerasの製作者のFrançois Chollet氏とコミッターの方々、そしてドシガタ素晴らしい漫画を描いてくださっているつくし卿に感謝を捧げつつこの記事を終わります。

参考文献

付録

回転なしで作戦2

休日が余ったので、奈落文字の回転は要らなかったんじゃないかというのを作戦2で検証してみました。
norot_NNAA-.png
norot_SENUHOWO.png

ブレがなくなってますね。その分似た文字にしか見えないというのがちらほらありますが。
一応最初にこれでやって「せ」が「も」にしか見えないのを確認したので回転を入れようとなったのですが、こちらのほうがもあもあ感が薄いのでよかったですね。

作戦2でエンコードした特徴を混ぜ合わせる

二種類の文字についてエンコードして特徴をつくり、それらを混ぜ合わせたものを生成してみます。
mixed_features.png
中間の特徴を用いれば中間の文字ができるのではという考えがあったのですが、配合比率5:5だとどちらにも似つかないものになりがちなようです。これじゃ新しい文字を作るのが難しいのも分かります。最初にこの実験を行っていれば実現可能性が低いことを判断できていたと思います。
「ン」だけがやけにしっかり再現されてて、こいつを学習データに混入してしまった悪影響が見て取れますね。