Chainerを使ってコンピュータにイラストを描かせる

# 概要
* つい一ヶ月前に提案された深層学習モデルであるDeep Convolutional Generative Adversarial Networks (以下DCGAN)をchainer上で実装した．
* 70万枚もの大量のイラストを使ってDCGANにイラストらしさを学習させた．
* 得られたモデルを利用して，コンピュータにイラストを描かせてみた．
* 結果としては結構上手く行った．本物と見分けがつかないというレベルではないものの，DCGANは正常にイラストらしい画像を生成できている．
* ネタが盛大に被って本当につらい

# はじめに

<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr">DCGANという画像を生成するニューラルネットをchainerで実装しました。恐るべきことに、今までにない美麗さで二次元イラストも自動生成できます…！<a href="https://t.co/C8P8GbzWN1">https://t.co/C8P8GbzWN1</a> <a href="https://t.co/DWwGhF9itw">pic.twitter.com/DWwGhF9itw</a></p>&mdash; おそるべし抹茶パワー (@mattya1089) <a href="https://twitter.com/mattya1089/status/676730414875054080">2015, 12月 15</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr">げ，ネタ被ってんぞ</p>&mdash; Masaki Saito (@rezoolab) <a href="https://twitter.com/rezoolab/status/676732030365888512">2015, 12月 15</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr">chainer AC止めて爆死アドベントカレンダーに執筆したい</p>&mdash; Masaki Saito (@rezoolab) <a href="https://twitter.com/rezoolab/status/677047909724983296">2015, 12月 16</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

まずはじめに，ご存知の方も多いと思いますがこのネタはつい4日前に投稿されたTwitterのツイートとネタ的に全く被っています．なんと！！ネタ的に，全く，被っている！！！！

一応言い訳しておくと今回のネタはこの記事を見てからコード拝借して実施した的なものじゃなく，ちゃんと自分でコード書いて実験していました．大体結果出たのが7日前位でああこれで一安心だとワクワク，温めて安心してる最中にこの仕打ちです．ｸﾞｴｴｴｴなんでや・・・1000RTとか・・・しかも[LeCun先生がFBに取り上げとる](https://www.facebook.com/yann.lecun/posts/10153269667222143)し・・・その後に発表するとか苦痛でしかないでしょ・・・Twitterは流石に想定外だった・・・

深層学習界隈の研究は最近競争がやたら激しいですが，まさか日本人オンリーのネタ枠で盛大に被るとは思っていなかった（だって[今回使った手法つい1ヶ月前にarXivにアップされた](http://arxiv.org/abs/1511.06434)んですよ？）まあしょうがないです．一番早く成果を発表した者が勝者で他は負け組なのが研の世界であり，敗因は結果が出た瞬間に発表せず，発表当日まで安心して温めておいた自分の側にあります．

これまでは結果見せて面白いとかいう感じの記事にする予定だったのですが，それはもうやられてしまったので今回はもう少し真面目にDCGANについての説明や，ごちゃごちゃパラメータ弄っている上で感じたいくつかの感想（実際には何を生成しているのか？DCGANを使って，究極的には人間が描くようなイラストを描けるのか？結局DCGANは何が凄いのか？なぜ他手法に比べてDCGANが上手くいくのか？など）について書いていくことにします．何かしらの参考になれば幸いです．

# 歴史的経緯
ご存知の通り，Convolutional Neural Network (CNN)をはじめとする，深層学習を利用した研究がコンピュータビジョンのあらゆる分野([物体認識](http://blogs.microsoft.com/next/2015/12/10/microsoft-researchers-win-imagenet-computer-vision-challenge/)，[説明文生成](http://www.cv-foundation.org/openaccess/content_cvpr_2015/ext/2A_044_ext.pdf)，etc.)でめざましい成果を挙げています．しかしながら，深層学習を利用した研究の多くは教師あり学習を対象としている一方で，教師なし学習に関する研究は教師あり学習のそれと比較してあまり多くありません．今後の深層学習に関する課題の一つとして，この教師なし学習の問題をいかに上手く解けるかが挙げられます ([LeCun談](http://www.pamitc.org/cvpr15/program.php#plenary))．

元々，第三次深層学習ブームは[Deep Belief Network](https://www.cs.toronto.edu/~hinton/absps/fastnc.pdf) (DBN)や[Deep Boltzmann Machine](http://www.cs.toronto.edu/~fritz/absps/dbm.pdf) (DBM)をはじめとする，教師なし学習のための生成モデルをその発端とするのですが，現在では学習が非常に大変ということもあり精力的にはあまり研究されていません．

（個人的な印象としては）近年，活発に研究されている教師なし学習のための深層学習のモデルは大きく2つが挙げられるかと思います．1つは対数尤度の変分下限を最大化する[Variational Auto Encoder](http://arxiv.org/abs/1312.6114) (VAE)と呼ばれるモデル，もう1つは今回実装した[Generative Adversarial Net](http://arxiv.org/abs/1406.2661) (GAN)と呼ばれるモデルです．これらのモデルを利用することで，大量の入力画像から，その入力画像の生成過程を表すモデル分布を求められます．一旦高品質な(真のモデル分布に近い)分布を求めることができれば，そのモデル分布を利用して高品質な画像を自動的に生成できると．

興味深いことに，GANに関してはほんの一ヶ月前に大きな進展が見られました．GANのネットワーク構造に工夫を加えた[Deep Convolutional Generative Adversarial Nets](http://arxiv.org/abs/1511.06434) (DCGAN)が，本物の画像と見分けがつかないレベルの質の画像を生成できたという論文が発表されたのです．比較的単純なネットワーク構造でも質の高い画像を生成できることを示したこの論文は大きなインパクトを学術界/産業界にもたらし，一週間もしない内にいろんなデータセットでこのモデルを試してみたというブログ記事が出てきました．もしDCGANによって本当にどんなデータセットのモデル分布でも高精度に求められるのであれば，これを使って，例えばイラストを描くといった，人間だけが行うある種の創造的な作業も機械学習で行えるようになるかもしれない．ということで，実験を行ってみたというのが今回の動機になります．

ただ，論文中ではDCGANの学習に数万から数百万もの画像を使っていたため，イラストの学習にも同程度のオーダーの画像が必要であると考えられます．しかし幸運なことに，自分の手元には前の研究で使用した約130万枚ものイラストが含まれるデータセットがあります．これを学習に利用することで，DCGANにデータセットの中には入っていないイラストを描いてもらいます．

# DCGANの大まかな説明
前述の通り，DCGANはGANのネットワーク構造を改良したモデルであるため，はじめにGANについて簡単に説明します（詳細は[元論文](http://arxiv.org/abs/1406.2661)を参照してください）．GANはGoodfellowらが2014年に提案した生成モデルであり，Generator, Discriminatorと呼ばれる2つのニューラルネットワークモデル(以下NN)を利用して大量の入力画像だけから，その画像の生成過程を表すモデル分布を生成します．Generatorは一様分布あるいは正規分布に従って適当に生成した乱数のベクトル(隠れ変数とも呼ばれる)から，本物そっくりの画像を生成する関数です．一方，Discriminatorは与えられた入力画像が，本物かGeneratorから生成された偽物かどうかを判別する関数です．

Generatorに本物そっくりの画像を生成してもらうため，著者らはGeneratorと Discriminatorに次のゲームをさせます: GeneratorはDiscriminatorを騙せるような画像を生成できれば勝ち．一方，Discriminatorは与えられた画像がきちんと本物か偽物かを識別できれば勝ち．このゲームの勝率をできるだけ上げるよう両者に競わせると，最終的にGeneratorはできるだけ(Discriminatorを騙す)本物そっくりの画像を生成し，一方でDiscriminatorはわずかな不自然さから偽物を識別します．

DCGANはGANで用いられていたネットワークの構成を変える(例えば，バッチ正規化([Batch Normalization](http://arxiv.org/abs/1502.03167))レイヤを加えたり，Discriminator側の活性関数を[LeakyReLU](https://web.stanford.edu/~awni/papers/relu_hybrid_icml2013_final.pdf)に変更したりするなど)ことで，より質の高い画像を生成できるよう変更したモデルです（詳細は[元論文](http://arxiv.org/abs/1511.06434)を参照してください）．今回はDCGANで提案されるネットワーク構成を利用して，イラストを描画するためのネットワークを学習しました．

# 実験設定
## データセット
今回は2種類のデータセット(一般的なイラストデータセットと，初音ミク画像だけが格納されているデータセット)を利用しました．一般的なイラストデータセットでは，クローリングで取得した約130万枚のイラストの内，ポルノ画像と4コマ漫画を除いた約70万枚のイラストを使用しました．データセットの画像を元論文と同じ解像度にするため，すべての画像をクロップ&リサイズして64x64pxの正方画像に変形しました．一応定量的評価に利用する目的で，対象のデータセットを分割し，8割を学習用，2割をテスト用に割り当てました．つまり約56万枚の画像を学習に使用しました．ただ結局テスト用データセットは今回使わなかったので，分割した意味は今のところありません．

初音ミクデータセットは一般的なイラストデータセットの多様性を落とす目的で作ったデータセットであり，初音ミクだけが写っているイラスト3万枚を集めました（初音ミクはアジアの中で一番多くファンアートが描かれているキャラクターなので彼女を採用しました）．これに関しては使用した画像枚数が少ないこともあり特にテストデータセットの作成は行っていません．

## ネットワーク構成
<img width="684" alt="architecture.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/61296/ff7e7553-3975-4c03-c4d1-6767757b3f28.jpeg">
(DCGANのネットワーク構成の概要図(元論文から引用))．

実装にはchainerを使用しました．一般的なNNの実装ですと特にchainerを使う利点はさほどないのですが，GANのようなちょっと複雑なネットワークの実装にchainerを使う利点は多いでしょう（スナップショットやリジューム機能など別途実装しなきゃいけないのがめんどいですが）．ネットワーク構成は元のDCGANで使用された構成に準じていますが，今回の実験に合わせて幾つか変えてあります:

* 論文元の実装であるtheanoで使用されている畳み込み層のパラメータ通りにChainer上で実装するとパラメータの解釈の違いによりエラーが発生する．そのため，今回は5x5のkernel sizeを4x4に変更した．paddingは論文中には明示的に書かれていないものの，今回は1に設定した．
* 元論文の畳み込み層で用いられるチャネル数を利用するとあまりにも学習が遅かったので，今回はすべて半分に設定した．
* 学習を安定させる目的で，Adamの学習率を2e-4から1e-4に変更した．

面白いことに，このパラメータはおそるべし抹茶パワーさん(抹茶好きな方なのかな？)が実験で用いたパラメータと，Adamの学習率とELU以外はすべて同じでした（特にパクったわけではないです）．
学習に用いたchainerのモデルコードを後ろに載せます．本当はコードを全部公開する予定でしたがやる気が無くなってしまいました．学習にはGTX Titan (無印)を利用して大体1日といったところです．

# 実験結果
![variables-iter250000.jpg](https://qiita-image-store.s3.amazonaws.com/0/61296/bcb96a38-259d-e877-688f-f492b54b5cdf.jpeg)

はじめに，一般的なイラストデータセットを使った場合におけるDCGANの生成結果を上図に示します．一部生成に失敗しているイラストがあるものの，おおむねイラストらしい画像が生成できていることが分かります．色情報が含まれていない線画は縮小によって情報が潰されているかなり難しい条件というものもあり，生成に成功している画像はありません．一方，一人の女性だけが写っている画像はうまく生成できているように思われます．これは構図が同じイラストがポートレートの場合多数あるため，比較的対応付けやすかったことがあるかもしれません．

微妙なところとしては，遠目で見るとなんとなくそれっぽいのですが，拡大してみると整合性が取れていないイラストが多い点です．これは抹茶さんの顔画像イラストの結果と比較するとあまり良くありません．今回の使用したデータセットは顔だけを切り出したというわけではないので，顔よりも比較的難しいデータセットになっているからと考えられます．

![variables-iter40000.jpg](https://qiita-image-store.s3.amazonaws.com/0/61296/7838e32d-1ca9-be96-ddd9-2e400be99ea1.jpeg)

次に，初音ミクデータセットを使った場合における同様の生成結果を上図に示します．傾向としては大体同じですが，こちらのほうが全体的な質が高い，すなわち，より自然なイラストを生成できていることが分かります．ただ拡大してみると，どうも緑髪や青髪出しとけばそれっぽく見えるだろとやってる印象があります．

# Discussion
## パラメータ調整についての感想
結果だけ見ると楽そうに見えますが実際にやると結構大変でした．特に，パラメータのチューニングには苦労しました．普通の教師あり学習のCNNだと調整するパラメータはそんなに多くないのですが，教師なし学習の場合パラメータのチューニングは大分シビアに行わなくてはいけません．これはGAN, VAE, RBM, DBMどれ使っても大体同じかと思います．

* Adamのbeta1をデフォルト設定の0.9に変えるとぼやけた画像しか出てこない．CNNを利用した教師あり学習だとモメンタムはあまりセンシティブに効くパラメータでないという印象があったが，これを入れるのと入れないのでは結構な違いがあった． 
* LeakyReLUをDiscriminator側に入れている理由は，明示的に論文に書いていないもののGeneratorを更新する際の勾配の情報をより多く伝えるためであると考えられる．LReLUの有無による違いを確認したところ，まああれば確かに綺麗になるかなという感じで劇的に効いてくるという感じではなかった．
* Batch normalization layerの有無は本当に重要である．MNISTレベルのデータセットだと無くてもいけるのだが，今回の実験ではこれを入れるのと入れないのでは著しい違いがあった．
* Discriminatorのパラメータ更新には，実画像のバッチと偽画像のバッチ2つを1つのバッチにまとめて更新する方法と，明示的に損失関数を2つに分けて更新する2通りの方法がある．Batch Normalization layerが無い場合だと最終的に得られる勾配はどっちも変わらないのだが，含めた場合は明確な違いが出てしまう（複雑ですね）．最初前者の方法で更新していたらG, Dどちらも勝率が100%になってしまう奇妙な結果が得られてしまった．最初chainer側のバグかと疑ってしまったのだが，最終的にBNの性質に着目し後者の実装にしたら綺麗に収束した（バグではなかった）．
* Kernel sizeは6x6と4x4の2通り試したが，4x4のほうが比較的綺麗な結果が出た． 

## 実際には何を生成しているのか？DCGANを使って，究極的には人間が描くようなイラストを描けるのか？

隠れ変数の値を少しずつ変えていくと，DCGANが生成する画像は少しずつ変化します．このことから，単純にDCGANはデータセット中の画像を覚えておいて，それをただ表示しているわけではないことが分かります．

ただこれをよく見るに，どうもデータセットに頻出するパーツ(髪の毛や衣装など)を記憶しておいて，その中から似たようなパーツ同士をモーフィングで繋ぎあわせている印象を受けます（それだけでも十分凄いのですが）．直感的には5層程度のNNが人体の三次元構造など，イラストの本質的な情報を画像から無教師学習できるとは思えないので，少なくとも人間が行っているような，創造的な作業は行っていないんじゃないかと．また，多層化でこの問題が本当に解決できるのかは甚だ疑問です．個人的には，きちんと機械にイラストを描いてもらうためには，単純なデータドリブンの方法論を使うのではなくイラストレーターに敬意を払い，その作業を注意深く観察した上で適切なモデルを提案する必要があると考えています．

## 結局DCGANは何が凄いのか？なぜ他手法に比べてDCGANが上手くいくのか？
一番効いてきているのがBatch NormalizationとAdamでした．これ入れないとそもそもノイズしか出てきません．個人的な印象だと，DCGANの一番の貢献はBNを入れたことだと思います．それ位違いがある．

![variables-iter80000.png](https://qiita-image-store.s3.amazonaws.com/0/61296/f663bc10-f719-96cd-265c-41471383ffaa.png)
(上図はBNを入れない場合の生成結果．ノイズだらけで使い物にならないことが分かる)

だとすれば，例えばDAEやVAE等の別のモデルでも，上手く行かなかったのは単なるBNの不在である可能性がある．だとすれば，BN入れれば今まで全然できなかった問題に対しても上手くいく可能性があるかと[^1]

[^1]: ちゃんと調べていませんがこれに関しては既にトップ大学の学生が確かめてるか，もう論文が出てるかと思います

## 実装に利用したchainerコードの一部

最後に，実際の実験に利用したchainerコードの一部を載せます．

```py

import chainer
import chainer.cuda
import chainer.functions as F
import chainer.links as L
import chainer.optimizers
import numpy as np


def init_normal(links, sigma):
    for link in links:
        shape = link.W.data.shape
        link.W.data[...] = np.random.normal(0, sigma, shape).astype(np.float32)


class Generator(chainer.Chain):

    n_hidden = 100
    sigma = 0.01

    def __init__(self):
        super(Generator, self).__init__(
            fc5=L.Linear(100, 512 * 4 * 4),
            norm5=L.BatchNormalization(512 * 4 * 4),
            conv4=L.Deconvolution2D(512, 256, ksize=4, stride=2, pad=1),
            norm4=L.BatchNormalization(256),
            conv3=L.Deconvolution2D(256, 128, ksize=4, stride=2, pad=1),
            norm3=L.BatchNormalization(128),
            conv2=L.Deconvolution2D(128, 64,  ksize=4, stride=2, pad=1),
            norm2=L.BatchNormalization(64),
            conv1=L.Deconvolution2D(64,  3,   ksize=4, stride=2, pad=1))
        init_normal(
            [self.conv1, self.conv2, self.conv3,
             self.conv4, self.fc5], self.sigma)


    def __call__(self, z, train=True):
        n_sample = z.data.shape[0]
        test = not train
        h = F.relu(self.norm5(self.fc5(z), test=test))
        h = F.reshape(h, (n_sample, 512, 4, 4))
        h = F.relu(self.norm4(self.conv4(h), test=test))
        h = F.relu(self.norm3(self.conv3(h), test=test))
        h = F.relu(self.norm2(self.conv2(h), test=test))
        x = F.tanh(self.conv1(h))
        return x

    def make_optimizer(self):
        return chainer.optimizers.Adam(alpha=1e-4, beta1=0.5)

    def generate_hidden_variables(self, n):
        return np.asarray(
            np.random.uniform(
                low=-1.0, high=1.0, size=(n, self.n_hidden)),
            dtype=np.float32)


class Discriminator(chainer.Chain):

    sigma = 0.01

    def __init__(self):
        super(Discriminator, self).__init__(
            conv1=L.Convolution2D(3,   64,  ksize=4, stride=2, pad=1),
            conv2=L.Convolution2D(64,  128, ksize=4, stride=2, pad=1),
            norm2=L.BatchNormalization(128),
            conv3=L.Convolution2D(128, 256, ksize=4, stride=2, pad=1),
            norm3=L.BatchNormalization(256),
            conv4=L.Convolution2D(256, 512, ksize=4, stride=2, pad=1),
            norm4=L.BatchNormalization(512),
            fc5=L.Linear(512 * 4 * 4, 1))
        init_normal(
            [self.conv1, self.conv2, self.conv3,
             self.conv4, self.fc5], self.sigma)

    def __call__(self, x, t, train=True):
        test = not train
        n_sample = x.data.shape[0]
        h = F.leaky_relu(self.conv1(x))
        h = F.leaky_relu(self.norm2(self.conv2(h), test=test))
        h = F.leaky_relu(self.norm3(self.conv3(h), test=test))
        h = F.leaky_relu(self.norm4(self.conv4(h), test=test))
        y = self.fc5(h)
        return F.sigmoid_cross_entropy(y, t)

    def make_optimizer(self):
        return chainer.optimizers.Adam(alpha=1e-4, beta1=0.5)
```

# 教訓
被りそうなネタはAC1日目に登録し速攻で発表を行い逃げ切りたい．