DCGANをChainerで実装
DCGANとは
DCGANはランダムなベクトルから画像を生成するGeneratorと、画像が与えられた時にその画像がGeneratorによって作られたものなのか、本当の画像なのかを判定する関数Discriminatorでなりたっています。
DCGANのアイディア
この2つの関数を相互に競わせるように更新することで、画像を生成するGeneratorをもっともらしいものへと更新します。
Generatorは、できるだけDiscriminatorに偽物だと判定されないように、重みを更新します。
反対に、Discriminatorは、できるだけGeneratorの画像を本物だと判定しないように、重みを更新します。
しくみとしてはこれだけで構成されていて、DCGANではGeneratorに逆Convolution(Deconvolution)を、DiscriminatorにConvolutionを用います。またConvolutionを行う際、BatchNormalizationを行うことで、高速な学習を可能にします。
実装する
今回はChainerを用いて、DCGANを実装しました。コードはgithubに置いています。https://github.com/Nyker510/hobby
僕のローカル環境ではGPUを使うことができないので、cpuを用いたコードになっていますので、そのまま使うととても遅いのでそれだけは注意してください。
構成としてはGenerator,Discriminatorを定義しているdcgan.py
とデータを与えて最適化していくtrainer.py
で構成されています。
dcgan.py
まずはGeneratorから
class Generator(Chain): '''ランダムなベクトルから画像を生成する画像作成機 ''' def __init__(self, z_dim): super(Generator, self).__init__( l1=L.Linear(z_dim, 3 * 3 * 512), dc1=L.Deconvolution2D(512, 256, 2, stride=2, pad=1,), dc2=L.Deconvolution2D(256, 128, 2, stride=2, pad=1,), dc3=L.Deconvolution2D(128, 64, 2, stride=2, pad=1,), dc4=L.Deconvolution2D(64, 1, 3, stride=3, pad=1), # Convolution, Deconvolutionともに値は画像の大きさに合わせて変化させる # 必要がある。 # 今回はMNISTをターゲットにするので、元の大きさは28×28 # 元の512チャンネルから1チャンネル(MNISTは白黒なためチャンネルが無い)に変換するまでに # 3→4→5→10→28となるようにstride,pad,windowの大きさを選んでいる # bn0 = L.BatchNormalization(6*6*512), bn1=L.BatchNormalization(512), bn2=L.BatchNormalization(256), bn3=L.BatchNormalization(128), bn4=L.BatchNormalization(64), ) self.z_dim = z_dim def __call__(self, z, test=False): h = self.l1(z) # 512チャンネルをもつ、3*3のベクトルに変換する h = F.reshape(h, (z.data.shape[0], 512, 3, 3)) h = F.relu(self.bn1(h, test=test)) h = F.relu(self.bn2(self.dc1(h), test=test)) h = F.relu(self.bn3(self.dc2(h), test=test)) h = F.relu(self.bn4(self.dc3(h), test=test)) x = self.dc4(h) return x
Generatorはランダムなz_dim
次元ベクトルを受け取って画像として返す関数です。今回実験対称としてMNISTの白黒画像を用いることを想定しているので、最後画像として返すDeconvolution関数を(28,28)次元のチャンネル数1と鳴るように、stride,pad,windowの大きさを選んでいます。
なので、違う大きさの画像でやりたいという場合や、RGBのある画像(チャンネル数が3)でやりたいというときは、それに合わせて変化させる必要があります。
(例えばRGBでやりたいのであれば、 dc4=L.Deconvolution2D(64, 3, 3, stride=3, pad=1)
です)
これと反対方向に、即ち画像を受け取って2値に写像する関数として、Discriminatorも定義します。
trainer.py
trainer.pyでは実際に最適化を行っていきます。
大切なところは、lossをどのように与えるか、の部分です。
z = np.random.uniform(-1, 1, (batchsize, self.z_dim)) z = z.astype(dtype=np.float32) z = Variable(z) x = self.gen(z) y1 = self.dis(x) # 答え合わせ # ジェネレーターとしては0と判別させたい(騙すことが目的) loss_gen = F.softmax_cross_entropy( y1, Variable(np.zeros(batchsize, dtype=np.int32))) # 判別機としては1(偽物)と判別したい loss_dis = F.softmax_cross_entropy( y1, Variable(np.ones(batchsize, dtype=np.int32))) # load true data form dataset idx = perm[i * batchsize:(i + 1) * batchsize] x_data = self.X[idx] x_data = Variable(x_data) y2 = self.dis(x_data) # 今度は正しい画像なので、0(正しい画像)と判別したい loss_dis += F.softmax_cross_entropy( y2, Variable(np.zeros(batchsize, dtype=np.int32)))
仮定として、Discriminatorの二次元の出力の内、0次元目を正しい画像である確率、1次元目を間違った画像(Generatorによって作成された偽画像)である確率であるとします。
まずzを適当なランダムベクトルとして設定した後、それをGeneratorに渡すことで画像xを作成します。その後、xをDiscriminatorに渡して二次元のベクトルy1を得ます。
Generatorとしては、Discriminatorを騙すことが目的です。
言い換えると、この画像xをDiscriminatorに渡した時に「本物だ!」と思わせたいのです。したがって全てのy1が0次元目の値が大きくなって欲しいので、ロスとしてy1と0とのSOFTMAXを与えます。
反対にDiscriminatorとしては、この画像xをちゃんと偽物であると判定しなくてはなりません。
すなわち1次元目が大きい値を取るようになって欲しいので、Discriminatorのロストしてはy1と1とのSOFTMAXを与えます。
そしてつぎに,Discriminatorに正しい画像も与えてあげて、これをちゃんと正しい画像(つまり0)と判定してほしいので、y2と0とのSOFTMAXをロスに加えます。
実験結果
MNISTの画像を使って実験を行います。本当は0~9でやれば一番良いのですが、GPUが使えない環境なので無限に時間がかかって終わりません。ですので泣く泣く、0の画像だけを用いて実験を行いました。
training中、epochが終わるごとにランダムなベクトルから画像をGeneratorによって作成してプロットさせていて、その画像を示します。同時にDiscriminatorが出力した「この画像が本物である確率」も画像の上に表示しています。
1 epoch目
ただのノイズです。
100 epoch目
だいぶ画像っぽくなってきました。がしかしDiscriminatorには正しい画像だとは思われていないようです。
500 epoch目
かなりしっかりと0を作るようになっています。正しい画像である確率が0.3程度になるなど、Discriminatorも判定に苦労している様子が伺えます。
1000 epoch目
500epoch目に比べて白黒の部分がはっきりとしてきました。
まとめ
今回はMNISTの0の画像だけというシンプルなタスクですが、学習が進むに連れて明確な輪郭をもって画像を出力する様子が確認できました。GPU環境であれば、更に高次の特徴をもった画像(例えばキャラクターのイラストなど)でも作成できるようなので、画像の自動生成技術として様々な用途で使えるかもしれません。