D言語くんを召喚するちょっとディープな方法


はじめに

例年12月になると各方面からD言語くんの生態や歴史について多くの情報が集まります。
さらに今年はインスタ映え料理界やOffice界、VR界、コンシューマーゲーム界にまで進出するなど、その生息範囲の拡大はとどまるところを知りません。

しかし依然残る課題として、我々のD言語くんのイメージは例の公式絵から脱却することができていません。

誰の目にもショッキングな印象を与え、心を揺さぶるあの崇高なビジュアルを、我々は深層心理にすでに刻み込まれてしまっています。これから逃げることなど叶わないのです。

それでも!この世界には多種多様なD言語くんがあり、それらを一つでも多く見つけだし、広く世に知らせていくべきである!と私は考えました。

そこで今回は、世にあるD言語くんの生息域をまとめ、そこで見つかった新種の情報をもとに更なる新種のD言語くんを異次元から大量に召喚する技術を調査、実践してみた結果についてご報告していきます。

概要

と、冒頭の一節で力尽きそうです。一旦正気に戻ります。

今回の内容は、D言語くんの画像収集、およびPythonを使いGANという深層学習の一種でそれっぽいD言語くんを生成する、という目標に取り組んだ記録です。

ちょっとでも感性を刺激する画像が生成できれば良いなと思ってチャレンジしてみましたが、そういう意味では元にしたイラスト一覧見るのがベストかもしれません。
深層学習を使っても素人ではプロの人間には勝てない、まだそんな時代なのかもしれませんね。(完)

さて、私のローカル環境は機械学習界隈では少数派となるWindowsですが、それでもPython環境を作るAnaconda、機械学習フレームワークの1つであるTensorflowおよびそれ用のサンプルを流用することでちゃちゃっと実現することができました。

本記事も同じようにサクサク進めていきたいと思います。

ただし、日頃取り組んでいるのはクラス分類やセグメンテーションが主でGANは初挑戦なのと、細かい話をし始めると座りすぎで腰痛が悪化してしまうため色々浅いのはご容赦ください。

ちなみにStar数などから専門の方には有名かもしれませんが、お世話になったサンプルは下記になります。
同様の実装は色々ありましたが、手軽に使える度で比較すると頭一つ抜けていると感じました。

こちらMITライセンスとのことで、下記で一部引用させていただきます。誠にありがたいです。

今回の流れ

世にはすでに多くのGANの解説がありますので詳細はそちらにお任せしたいと思いますが、今回の方針、仕組みは以下のようになります。

  1. できるだけたくさんのD言語くん画像を集める
    ※ 必要なら拡大縮小回転や色味調整などを行いデータの水増しもする。前処理は大変だ。
  2. いくつかの乱数から一定の法則で画像を生成、学習して調整できるネットワークを用意する
    ※ Generator、略記はG、生成ネットワークと呼びます
  3. Generatorの生成画像か、集めた画像のどちらか区別、学習できるネットワークを用意する
    ※ Discriminator、略記はD、識別ネットワークと呼びます
  4. DはGの生成画像と本物と区別するように、GはDをだますように(本物と区別できないように)交互に学習、調整していく
  5. 学習した2のネットワークに適当な乱数を与えていかにも本物っぽいけどちょっと違う画像を生成させる(させたい)

この2~4(微調整が不要なら5)までが利用したサンプルを用いると簡単にできるようになっており、ツール類に慣れていれば1日もあればできるようになると思います。

また、最終的には独自に生成した画像を見つつ、申し訳程度に技術的な振り返りも行います。

と、本当は例の青いホリネズミをセグメンテーションしてマスク掛けたあとGANで上書きしてみよう(自動ですべて駆逐しよう)と思ったんですが、時間とマシンパワーと技術が足りませんでした。(全部だ)

召喚への道のり

いかんせんGANについては右も左もわからないので、安全を考え11月中盤から生息域の調査、終盤に召喚術式の構築を開始しました。

週末になるとその邪悪な魔力を放出しながら、RTを糧に少しずつ進捗していくことになります。

以下はここに至る道とその後の記録です。

D言語くんの生息地

彼を知り己を知れば百戦殆ふからず。(適当)

まずは元になる情報が必要です。ちゃちゃっと好みの子を見つけて連れて帰りましょう。
多ければ多いほど、多様であれば多様であるほど良いですね。D言語くん業界にもダイバーシティが訪れています。

外せないのは元祖ですが、こちらは公式コミュニティのGitHubリポジトリにいます。
他にも過去イラストがリスト化されているなど素晴らしい充実っぷりで大変助かりました。
READMEを読むと改善案募集中とのことで、ゲームへのリンクや自作絵などを追加するPullRequestを送ってみると良いかもしれません。

それ以外で多様な姿が見られるのはやはり日本が中心のようです。
手軽に幅広く知るにはやはりTwitterで検索するのが効率的でしょう。

キーワードとしては D言語くん またはハッシュタグの #dlangman などがメジャーです。
時期的なものかもしれませんが、下記リンクを見ていると1週間に何度か新種のD言語くんが見つかるようです。

https://twitter.com/search?f=images&q=D%E8%A8%80%E8%AA%9E%E3%81%8F%E3%82%93
https://twitter.com/hashtag/dlangman?f=images&vertical=default

また総数こそ少ないもののPixivなども期待できます。ここには希少種であるD言語ちゃんがいたりします。

https://www.pixiv.net/search.php?s_mode=s_tag_full&word=D%E8%A8%80%E8%AA%9E
https://www.pixiv.net/search.php?s_mode=s_tag_full&word=D%E8%A8%80%E8%AA%9E%E3%81%8F%E3%82%93

そしてここで重要となるのがニコニ立体にある2016年のアドベントカレンダーで召喚された元祖さんと瓜二つの3Dモデルです。ライセンスも、煮るなり焼くなり好きにしろ(NYSL)ということで好きにさせていただきましょう。

これは3Dモデルを前後左右あらゆる方向から撮ることで手軽に大量の標準データを得ようという考えです。
Windowsだと今はペイント3Dで読み込んで動かしながらPrintScreenを連打すればOKですね。

また最近はWorld Modelとか有望らしいので、平面だけでなく立体の概念を獲得につながりそうな情報はきっと有用だろうと思ってやっています。手抜きではありません。私は真剣です。

では、怒られなさそうな公式リストやTwitterを中心に少々拝借してきましょう。異次元から大量のD言語くんを召喚する生贄となってもらうために…

召喚術式

データを集めた後は、学習に使うためデータのトリミングやサイズ調整、その後最低限の水増しを行います。
ここでの手間の掛け方が後の召喚の品質を決定づける重要な要因となります。魔術でも化学でも触媒の品質は大変重要ですからね。知らんけど。

その後、実際にGANを用いて独自画像を生成、召喚していきましょう。

触媒の準備(データのトリミングと水増し)

まず、今回のGANには人の感性を刺激するマスコットらしい画像を生成してもらいたいため、世にある画像の中からそれらしき部分をトリミングしてやる必要があります。同時に水増しもしたいので、回転したり平行移動したり、まぁ色々手がかかる子となります。

当然、1匹?1人?ずつGIMPなどで加工していると年が明けてしまいますので、手早く作業するためには専用のツールが必要でした。
私が知らないだけで良いのがあるかもしれませんが、せっかく偉大な魔術に取り組むわけですから基礎からやっていきましょう。

個人的にはマイクロソフトのAzureにあるCognitive Servicesという認知・認識に関するサービス群から、Custom Visionというサービスのタグ付けツールの使い勝手が良かったので、それを目指したいと考えました。

というわけでこれをインスパイアしつつ、D言語でGUIを構築するGtk-Dというライブラリを用いて切り出しと微調整を行うツールを作りました。

はーかわいい(作業が進まない)

内部的にはD&Dで範囲を選んだら背景を適当に補いつつデータを切り出した後、ImageMagickという画像加工ツールを使いサイズ調整、また数度左右に回転させたりアンシャープマスクを掛けるなどして水増しまで行うようにしています。

ここで注意するポイントとしては、召喚後の姿がこれらに酷似することになるので、色味を変えたり極端に改変して見た目が悪くなるのは厳禁となります。心を鬼にして選抜総選挙です。

また基本原則ですが、ちゃんと水増し後のデータを見て変な画像は手で弾くのも大切です。
大抵(自分の作業ミスで)変な画像が混ざってるんですよ。本当に手間のかかる子ですね。

結果、収集画像は好みも考慮しつつフィルタリングして212枚、水増しを行い3259枚となりました。
懸念はあるものの、おおむね培養成功です。

魔法陣(ネットワーク)構成

触媒さえあれば、ぶっちゃけあとは呪文を唱えるだけ(スクリプトを実行するだけ)なのですが、召喚に使う魔法陣についてもちゃんと理解しておきます。

サイトから引用すると、Generator、生成ネットワークの構成は以下の通りです。

一番左が100次元の乱数ベクトル、それを4x4画像1024チャンネルのテンソルに変換、そこからサイズを倍でチャンネルが半分になるようアップサンプリングしていきます。
更に細かくはBatch Normalizationという補助術式も組み込まれていますがここでは割愛します。

結果、最初に乱数の層、乱数をテンソルに変換するProject and reshape層、アップサンプリングする畳み込み層が4つあり、全体で6層のディープ?な魔法陣となります。
(最近入力含めず5層と数えるケースもあると聞きますが、初歩の3層NNは入力もカウントするしどっちが普通かよくわかりません)

また識別側はこれを逆向きにして、最終層が乱数や画像ではなく1つの数値になった構造となります。結果が用意したデータだと思ったら1、生成画像だと思ったら0にすることで識別するネットワークとなります。

ちなみに結局チューニングの勘所がつかめずサンプルをほぼそのまま適用してしまうことになったのですが、画像の特性からなのかいくつか問題に突き当たったので、わからないなりにもちょっと弄って使いました。

詳細は重いので後述します。

召喚の儀

では、無事にデータは集まったので、GANに入力して実際に召喚していきましょう。

儀式に使ったPCはノートでグラフィックボードがGTX1060が1枚とあまり強いマシンではありませんが、64x64なら3~4時間くらいでそれなりの結果が出ました。イケてるクラウドのやつならもっと速いでしょう。

手順としては以下のようになります。

  1. GitHubからサンプルをclone
  2. Anacondaで1つ専用の環境を作る(依存関係のパッケージが揃えば2.7系でも3.0系でも大丈夫です)
  3. GitHubにあったRequirementのパッケージを追加する
  4. 用意したデータをclone先のdata/dman_64に配置
  5. Anacondaのコンソールでカレントディレクトリをclone先へ移動

そして、召喚の呪文を唱えます。

python main.py --dataset=dman_64 --input_fname_pattern=*.png --input_height=64 --input_width=64 --output_height=64 --output_width=64 --epoch=100 --train

これでsamplesディレクトリに学習途中のサンプルが出力されつつ、最後に色々な乱数を元にした画像が100枚ほど生成されます。

ネットワークの定義も不要で一発処理できてしまいますので、これくらいまとまったものがあると本当に入門に最適ですね。
成果を可視化する手間もないのがとても良かったです。

満足いかなかったらもう1回同じ呪文を唱えると追加で学習が行われます。
おそらくもうちょっと綺麗な画像が生成されるようになるでしょう。時間と電気代が許す限りやりたいですね。

そしてこのepochの部分が学習回数の指定になっているので、伸ばして待つこと数時間…

いでよD言語くん!

召喚結果

ババーン
レア度S 「元祖D言語くん」が召喚されました!ちょっとノイジーですが、無事に生成されてよかったです。

手足もちゃんとあり、うっすらと体の輪郭も見えるので及第点だと思います。データセットに占める割合も多く、これが生成されないと話になりませんからね。

はーバランスも絶妙で目線もバッチリで言うことないですね。かわいい。

なお64x64だとちょっと見づらかったので、waifu2xの力を借りてイラストモードで2倍に引き伸ばしてあります。JPGノイズを消す設定だとちょっと綺麗になるので最強ですね!(丸投げ)

さぁ、無限の力を手に入れたのでどんどん行きましょう。
聖杯は我が手にあるのだ。無料で10連回せるぞ。

ババーン
レア度A、「崖を飛び越えるD言語くん」が召喚されました。

ぶっちゃけ元の画像がわからないので技術者的には負けっぽい気がしてしまうんですが、まぁそういうこともありますね。
下に崖っぽい空間があり、上を飛び越えているように見えるのでこれは割と斬新な気がします。なんか創作意欲が刺激されてくる気がしませんか?

どんどん行きましょう。

レア度B、「腰パン(ずれ落ち)しているD言語くん」が召喚されました。

ふちの水色で元画像が浮かぶ人がいるかもしれません。アレがこうなったということです。
赤くないデータがあればちゃんとこういうのも生成されるということで、データセットには気を配る必要がありますね。

レア度C 「風洞実験されるD言語くん」が召喚されました。

大気圏に突入するD言語くんというタイトルを付けようと思ったのですが、宇宙っぽい要素がないので風の筋からタイトルつけました。
元画像の原型を残しつつほかの画像の色味を取り込んでいるので、こういう組み合わせの生成が簡単なのは良いですね。

レア度D 「時空の渦に飲み込まれるD言語くん」が召喚されました。

ぶっちゃけ単に生成がうまくいってない例なんですが、微妙に渦を巻いてるように見えたので取り上げました。
旅の扉や歪みのイメージがあり、こういうのでもよく見ていると刺激される部分があります。

レア度D、「右足側が異常発達したD言語くん」が召喚されました。

多分下半身が見切れてるイラストと混ざった結果だと思われるのですが、終盤まで生き残ったしぶとい子でした。
黒い帯のところに鬼火のようなものがあるなど、何か不思議なネットワークになってしまっているのかと思います。
もうちょっと綺麗に学習させたいですね。課題です。

レア度D 「三つ目の子が通る」が召喚されました。

これも生成失敗なんですが、目を出力する部分が重複してしまったんですかね。
異形の子でも受け入れられるケースはあると思うので、こういうのも受け入れていく姿勢が重要です。多分。

ババーン
レア度S 「記憶の確執」が召喚されました。

この「記憶の確執」っていうのはサルバドール・ダリの名画の題名です。時計が溶けてるやつって言うと伝わるでしょうか。

これ、背景が1枚目の元祖さんの類似画像から持ってきていて、元とは全然違う画像のD言語くんが上に乗ってるんですよね。
しかもちょっと不思議な角度で寝そべっていて、上半身が消え気味だったので名画のタイトルを付けてみました。
たった64px四方で人に名画を連想させるとは末恐ろしい子…!

まとめ・所感

ちょっと振り返りは少なめですが、結果の紹介は以上になります。上澄みだけでは10連回せなかったよ…

もうちょっと画像が増えると面白いと思うんですが、時間と技術が欲しいですね。

とまぁGANは初チャレンジでしたが、その可能性や雰囲気が少しでも伝われば幸いです。

同時にGANの感覚もつかめてきて、次はもう少し良い生成結果が得られそうな自信が持てました。
次は自分で線画を書いて自動着色とかやってみたいですね。赤バケツの塗りつぶしとスピード対決です。

では、以降重い話となりますので、興味のある方だけ覗いて行ってください。

お疲れさまでした!

反省点

以下、正気に戻って学習中に悩まされた点などまとめます。こういうのはちゃんと記憶があるうちにやりましょう。

画像サイズの調整

大きな画像が苦手なのは噂で聞いていたので、512x512で挫折したあとは128x128で様子を見ていましたが、この記事を書く必要もあったので学習が高速な64x64で落ち着きました。

今年の論文情報によると1024x1024程度の高精細な画像は十分いけるらしいので、良い手法を調べて取り入れていきたいですね。

多層にするか転移学習あたりが簡単かなーと思いつつ、WGAN-GPが良いかなーとか。
こういう方法模索するのが楽しいんですよねぇ。

ブロックノイズ(謎の縞模様)

あまりGANの画像生成で聞くことがなかったブロックノイズに定期的に悩まされました。

終盤に起きたつらい奴は画像が消えてしまったのですが、学習序盤から中盤で起きるとこんな感じになります。
全体的に学習不足なのはありますが、縦横に縞々が表れてあちこち原型がありません。

この要因は、D言語くん単体の画像を生成したかったので部分的にトリミングした際、切って足りない部分を埋めるべく、こんな画像ばかり集めていたためです。

注目してほしいのは左右の黒い帯です。(重要)

要するに、生成側がこの黒塗り部分や区切りをうまく出そうとする、そうすると畳み込みから復元してるので、ちょっと学習が暴走したときに同様のベタ塗りが色々なところに波及してしまうんですね。
またD言語くんが局所的に赤のチャンネルを1側に振り切るので、深い層で黒くしたい部分と食い合っていると赤が勝ってしまうとかの影響があるかもしれません。

対処としては十分学習させること、あとは最初から元画像を正方形に抜き出せ、という感じでしょうか。
そもそもツールにそういう機能が必要でしたね。最初から縦長の画像もあるんで難しいんですけど。

そしてこの問題、検出系の話で何度か聞いたことがありまして、実はネットワークを弄ると対処ができたりします。

たとえば複数のカメラが混在する環境では機種によるアスペクト比の違いなどから黒塗りでサイズ補正して揃えることがあるんですが、その場合大きい方は削り落とせないので、小さい方を大きくするしかないわけです。このとき大体同様の黒い帯を付けることになります。
平成初期の2時間サスペンスを16:9画面のTVで見ると横に黒いのが付くアレです。(おじさん…)

埋める領域を情報量ベースとかそれこそGANで埋めるとかできればいいんですけど、FPSとのトレードもあるのでそこまで強いことはしていられず、実際はちょっと小細工をしますが、結果黒塗り入力して後はネットワーク頼みです。

そして、そこからオートエンコーダーに食わせて内部表現取り出したり色々するんですが、そうするとオートエンコーダーの復元側で同じようなブロックノイズや線の歪み、縞模様が出るという現象が起きます。

これ、単一のカメラからやってるうちは起きないんですよね。不思議で調べたら帯のとこ塗ってる影響でした。

そして経験的には、Early stoppingを諦めて学習ぶん回す、最終層のStrideを小さくする、ブロックサイズを変える(大小どちらでも)、という対処が有効という結果が得られています。

案外中間層の構造はそんなに関係なく、学習回数を十分取ると解消されるので、できるだけ出力に近いところで帯を生成させるように頑張ってもらう、ということでしょうか。
実験不足もあると思うので誰か適切な対処を教えてほしいところではあります。

一応なぜこれで改善するのか解釈してみると、以下のようなロジックかと考えられます。

  • Strideの縮小はブロックノイズと似た出力が最終結果にたくさん反映されて誤差の増加を促進し、それがLoss関数によって捉えやすくなるため軽減しようとする力が多く働く
    • またはグラデーションになる段階で止まって一見ヤバイ見た目にまで発展しない
  • ブロックサイズの変更はほかの塗りつぶしに食われて相殺されたり他の出力と被るので、内部的には縞々だが出力としては問題ない見た目になっている

モード崩壊

モード崩壊というのは主にGeneratorの能力不足からくる現象だそうで、生成する画像が多様性を失って比較的Discriminatorを騙せるいくつかの画像(1つだったりする)しか生成してしなくなってしまう現象です。

識別側が強いというのは通説として知っていますが、今回は若干状況が違ったように思います。

というのも学習結果としては、いざ出力すると8~10種くらいしか画像が出てこない。しかしちょっと再学習すれば全然違う画像がまた10種類くらい出てくる、という状況だったからです。

生成側の気持ちになってみると、データセット全体をカバーする多様な出力は既に可能だが、1度学習時点で局所的に穴になるところを突いておき、塞がれたら一気に切り替えて次の穴を突くとLossは総じて低く保てる、といった感じでローテーションしてるようです。何とこしゃくな…

ひとまずモード崩壊であれば乱数の次元を増やしてしまうのが手っ取り早い解決になるそうで、少し増やしたところほぼ解消しましたが、釈然としません。
逆に余裕を与えないように削るか…?まぁこのあたりは今後の課題として頑張りましょう…

また、このとき基礎的な調整はあれこれやっておきました。

まずは似たような画像が多いとそこを穴として認識しやすくなるようなので、心を鬼にして削りました。(Lossに占める割合が増えるため)
更にミニバッチに類似画像が含まれたりしてもダメなのはよくあるので、画像の順序をバラバラで均等になるように見直しました。(ミニバッチの学習で局所的に同じ勾配が重なって増幅されてしまうため)

学習率落とすのも考えましたが、収束が遅いと飽きてしまうのでモチベーションがあるうちに気合でデータ調整しました。仕方ないね。

そんなこんなでデータセットを少し見直し、サンプルでは100次元だった乱数ベクトルを適当に二分探索して大丈夫になった138次元からちょっと多い144(128+16)次元を最終版として採用しています。

いくらD言語くんがかわいいとはいえ結構頑張ったぞこれ。

はい。今度こそ以上となります。論文読んで勉強します。みんながんばろうな!