こんにちは。先日、多層パーセプトロンモデルを使ったGAN(敵対的生成ネットワーク)で画像生成を行いました。しかし機械学習で画像と言えば、畳み込みニューラルネットワーク(CNN)ですよね。とうわけで生成器・識別器にCNNと転置CNNを使って効率的に画像を学習できるようにしたDCGAN(Deep Convolutional GAN)による画像生成を検証してみました。これにはPyTorch公式でセレブの顔を生成する分かりやすいチュートリアルがありますが、本記事ではMNISTの数字画像生成という比較的簡単なタスクをやってみました。
【参考】DCGANのPyTorchのチュートリアルです。本記事の実装の9割はこちらに依っています。
DCGAN Tutorial — PyTorch Tutorials 1.5.1 documentation
【参考】GAN入門にはこちらの記事をご覧ください!
dajiro.hatenablog.com
DCGANの概念図
GANの基本的な概念については上の記事をご覧ください。DCGANの学習の流れは一般的なGANとまったく変わりありませんが、生成器・識別器に畳み込み・転置畳み込みニューラルネットワークを用いているのが最大の特徴です。前回は多層パーセプトロンで画像を生成しましたが、それでは画像1ピクセル周りの特徴などを効率的に取り込むことができません。それを可能にしたのが(転置)畳み込みニューラルネットワークです。
識別器~畳み込みニューラルネットワーク
画像の真偽を判定する識別器には、畳み込みニューラルネットワークが使われます。これは画像を入力として、フィルタ(カーネルとも呼ばれる)によって画像の細かい特徴を自動抽出します。CNNのイメージ図は以下の通りです。
GitHub - vdumoulin/conv_arithmetic: A technical report on convolution arithmetic in the context of deep learning
ここで、は高さ・幅を表すパラメータです。また
,
は出力画像サイズ、
,
は入力画像サイズ、
,
はカーネルサイズ、
はそれぞれストライドとパディングのパラメータです。実際にCNNを組む際には、設定したパラメータでどのようなサイズの画像が返ってくるのか計算しながら設計しましょう。
生成器~転置畳み込みニューラルネットワーク
画像を生成するためには、転置畳み込みニューラルネットワークが使われます。これはベクトルを入力として、それを拡大するようにして画像を生成する仕組みのことを指します。
転置畳み込みの様子をもう少し詳しく見てみましょう。元の小さな画像の外枠・間に空のピクセルを入れて、そこにカーネル処理を施すことによって画像を拡大します。パディング・ストライド・カーネルサイズといったパラメータはここでも登場します。
ちなみにこれは、畳み込み処理における画像サイズを求める公式を,
について解いたものと一致しています。また、他にdilationやout_paddingというパラメータがありそれらを組み込んだ完全な公式が存在しますが、私自身が理解できてないので(汗)、ここではいじらないことにしました。以下のリンクが参考になるかもしれません。
GitHub - vdumoulin/conv_arithmetic: A technical report on convolution arithmetic in the context of deep learning
ConvTranspose2d — PyTorch master documentation
PyTorch実装
PyTorchのDCGANチュートリアルの実装にのっとっているため、そちらをご覧ください。注意点としては、GANを構成する生成器と更新器の画像サイズの設計です。例えば画像を逆畳み込みで拡大する場合、元画像サイズ6, カーネルサイズ2, ストライド2, パディング1の場合出力される画像は10×10の画像です。このようにパラメータと画像サイズの関係に注意しながらGANを構成します。識別器と生成器以外はチュートリアルの実装とほぼ同じです。
生成器
# Generator Code class Generator(nn.Module): def __init__(self, ngpu): super(Generator, self).__init__() self.ngpu = ngpu self.main = nn.Sequential( # input is Z, going into a convolution nn.ConvTranspose2d( nz, ngf * 8, 3, 1, 0, bias=False), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), # state size. (ngf*8) x 3 x 3 nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), # state size. (ngf*4) x 6 x 6 nn.ConvTranspose2d( ngf * 4, ngf * 2, 2, 2, 1, bias=False), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), # state size. (ngf*2) x 10 x 10 nn.ConvTranspose2d( ngf * 2, ngf, 2, 2, 2, bias=False), nn.BatchNorm2d(ngf), nn.ReLU(True), # state size. (ngf) x 16 x 16 nn.ConvTranspose2d( ngf, nc, 2, 2, 2, bias=False), nn.Tanh() # state size. (nc) x 28 x 28 ) def forward(self, input): return self.main(input)
識別器
class Discriminator(nn.Module): def __init__(self, ngpu): super(Discriminator, self).__init__() self.ngpu = ngpu self.main = nn.Sequential( # input is (nc) x 28 x 28 nn.Conv2d(nc, ndf, 2, 2, 2, bias=False), nn.LeakyReLU(0.2, inplace=True), # state size. (ndf) x 16 x 16 nn.Conv2d(ndf, ndf * 2, 2, 2, 2, bias=False), nn.BatchNorm2d(ndf * 2), nn.LeakyReLU(0.2, inplace=True), # state size. (ndf*2) x 10 x 10 nn.Conv2d(ndf * 2, ndf * 4, 2, 2, 1, bias=False), nn.BatchNorm2d(ndf * 4), nn.LeakyReLU(0.2, inplace=True), # state size. (ndf*4) x 6 x 6 nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 8), nn.LeakyReLU(0.2, inplace=True), # state size. (ndf*8) x 3 x 3 nn.Conv2d(ndf * 8, 1, 3, 1, 0, bias=False), nn.Sigmoid() ) def forward(self, input): return self.main(input)
結果
ようやく結果です。損失関数の振る舞いを見ると、生成器の損失は下がるよりむしろ上がっていますが、学習できているのか不安になりますね。識別器の方は損失が減少しています。
そして実際に生成された画像がこちら。これは1エポックの学習を行って得られた画像ですが、ニョロニョロとした画像が生成されておりまだ数字には成長していません。なんだが、植物の萌芽のようで神秘的な印象を受けました。
それでは最後に、多層パーセプトロン(MLP)で得られた結果と見比べてみましょう。MLPのGANの方がややノイズが乗っているようには見え、数字が太い印象があります。DCGANの方がよりはっきりとした画像な気がしますが、当初予想していたよりもはっきりとした違いが見えません。もっと難しい画像の生成で比較すると、恐らくDCGANの方が良い精度になると思われますが、MNISTではあまり差がないことが分かりました。