49
37

More than 5 years have passed since last update.

DCGANのジェネレータにおけるConvTとUpsample+Convとの違い、BatchNormと活性化関数の順番、初音ミクの生成で比較する

Last updated at Posted at 2018-04-04

前置き

先回はDCGANを実装してみました https://qiita.com/phyblas/items/bcab394e1387f15f66ee

最初から色んな人のモデルを比べて気になることがあります。一つはジェネレーターの中で、chainerで実装する人は逆畳み込み層(ConvT)を使うのに対し、kerasでDCGANを実装する人はアップサンプリング(Upsampling)+畳込み層(Conv)を使うことが多いです。

kerasで実装した例は
https://qiita.com/triwave33/items/35b4adc9f5b41c5e8141
https://qiita.com/t-ae/items/236457c29ba85a7579d5
https://qiita.com/taku-buntu/items/0093a68bfae0b0ff879d
https://qiita.com/knok/items/3f3c1d3eef4b435ed37e
みんなUpsampling+Convを使っていますね。

それに対しchainerで実装した例は
https://qiita.com/mattya/items/e5bfe5e04b9d2f0bbd47
https://qiita.com/rezoolab/items/5cc96b6d31153e0c86bc
https://github.com/chainer/chainer/tree/master/examples/dcgan
みんなConvTを使っています

そしてtensorflowの例もConvTを使っています
https://github.com/carpedm20/DCGAN-tensorflow

この2つの方法は同じ意味なのか?という質問が出ました。そして詳しく考えてみたら、計算は同じではないようです。それでもどっちでもDCGANで使われています。

実際には勿論フレームワークの違いは使うモデルに影響がないはずです。kerasを使ってもConvTを使うこともできますし。私は先回pytorchで実装した時はConvTを使ったが、今回はUpsampling+Convも試してみます。

同じような違うような

畳み込みや逆畳み込みのことはすでにたくさんの記事で書かれていますので、参考します。
https://qiita.com/search?q=逆畳み込み

要するに畳み込みを使うとサイズが小さくなるのに対し、逆畳み込みを使うと大きくなるのです。ただし、パディングがあったら畳み込みでサイズが小さくならいこともあります。

逆畳み込みでカーネルサイズは4と、ストライドは2と、パディングは1とすると、出力サイズは2倍となります。

例えば

  1    10
100  1000

で、全部の重みが1だったらこうなります

  1   11   11   10
101 1111 1111 1010
101 1111 1111 1010
100 1100 1100 1000

その一方、アップサンプリングはこんな風にデータを増えることで、サイズを2倍大きくします。

  1    1   10   10
  1    1   10   10
100  100 1000 1000
100  100 1000 1000

それに加えて畳み込みでカーネルサイズは3と、ストライドは1と、パディングは1とすると、こうなります。

  4   24   42   40
204 1224 2142 2040
402 2412 4221 4020
400 2400 4200 4000

見ての通りサイズはそのまま変わりません。結果もConvTと似ています。

つまり、ConvTは、Upsampling+Convのように見えるのはこういうことです。それでも中に行われる計算は相当違います。

コードを見る方がわかりやすい人もたくさんいると思うので、よく見えるように、まずはnumpyでUpsamplingとConvとConvTを実装します。

普通の2D畳み込み層なら4Dの入力は必要ですが、今回はわかりやすいように一対一の畳み込みだけにします。入力は単なる2Dの行列となります。入力と出力のチャンネル数は全部1。

import numpy as np

class Conv2D:
    def __init__(self,k,s=[1,1],p=[0,0]):
        self.k = k
        self.s = s
        self.p = p
        self.w = np.random.normal(0,0.02,k)
        self.b = 0

    def __call__(self,x):
        k0,k1 = self.k
        s0,s1 = self.s
        p0,p1 = self.p
        w = self.w
        b = self.b
        xs0,xs1 = x.shape
        ys0 = int(np.floor((xs0+2*p0-k0)/s0)+1)
        ys1 = int(np.floor((xs1+2*p1-k1)/s1)+1)
        xp = np.zeros([xs0+2*p0,xs1+2*p1])
        xp[p0:xs0+p0,p1:xs1+p1] = x
        y = np.array([[np.sum(xp[i*s0:i*s0+k0,
                                 j*s1:j*s1+k1]*w)
                       for j in range(ys1)]
                       for i in range(ys0)])+b
        return y

class ConvT2D:
    def __init__(self,k,s=[1,1],p=[0,0]):
        self.k = k
        self.s = s
        self.p = p
        self.w = np.random.normal(0,0.02,k)
        self.b = 0

    def __call__(self,x):
        k0,k1 = self.k
        s0,s1 = self.s
        p0,p1 = self.p
        w = self.w
        b = self.b
        xs0,xs1 = x.shape
        yp = np.zeros([(xs0-1)*s0+k0,
                       (xs1-1)*s1+k1])
        xw = x[:,:,None][:,:,None]*w
        for i in range(xs0):
            for j in range(xs1):
                yp[i*s0:i*s0+k0,
                   j*s1:j*s1+k1] += xw[i,j]
        y = yp[p0:(xs0-1)*s0+k0-p0,
               p1:(xs1-1)*s1+k1-p1]+b
        return y

class Upsam2D:
    def __init__(self,k=[2,2]):
        self.k = k
    def __call__(self,x):
        k = self.k
        return x.repeat(k[0],0).repeat(k[1],1)

そして試しにこうやって使って絵を作ってみます

import matplotlib.pyplot as plt

conv = Conv2D([3,3],[1,1],[1,1])
convt = ConvT2D([4,4],[2,2],[1,1])
upsam = Upsam2D()

x = np.random.normal(1,0.1,[100,100])
xconvt = convt(x)
xupsamconv = conv(upsam(x))

plt.figure(figsize=[6,6]).add_axes([0,0,1,1])
plt.imshow(xconvt,cmap='rainbow')
plt.savefig('convt.jpg')
plt.figure(figsize=[6,6]).add_axes([0,0,1,1])
plt.imshow(xupsamconv,cmap='rainbow')
plt.savefig('upsamconv.jpg')
plt.close()

結果は違うように見えますね。

ConvT
convt.jpg

Upsam+Conv
upsamconv.jpg

よく見えるのはConvTの方が十字のようなものが多いのは特徴です。それに対しUpsampling+Convは自然なノイズに見えます。

でもその違いはまだ十分に訓練が足りなく、パラメータがまだランダムのままの方が起こりやすい。よく訓練したらどっちも自然な画像が作れるようです。

pytorchでの使い方

さっきの計算はpytorchでは実装すればこうなります。

import torch
from torch.autograd import Variable as Var

conv = torch.nn.Conv2d(1,1,3,1,1)
conv.weight.data.normal_(0,0.02)
conv.bias.data.fill_(0)
convt = torch.nn.ConvTranspose2d(1,1,4,2,1)
convt.weight.data.normal_(0,0.02)
convt.bias.data.fill_(0)
upsam = torch.nn.Upsample(scale_factor=2,mode='nearest')

x = Var(torch.randn(1,1,100,100))*0.01+1
xconvt = convt(x).data.numpy()[0][0]
xupsamconv = conv(upsam(x)).data.numpy()[0][0]

比較するモデル

ジェネレーターはこのように違うモデルを使います。

gen.png gen2.png

ConvTはUpsamplingとConvに置き換えられる。ただし一番目のConvTはそもそも使う目的が違うのでそのまま。

ConvTを使うモデルは先回のモデルと同じですが、今回は96pxの画像に使うものなのでちょっと数字が違います。

ディスクリミネータの方は同じモデルを使います。

dis.png

後ろの数字は
ConvとConvT:入力サイズ,出力サイズ,フィルターサイズ,ストライド,パディング
Lin:入力サイズ,出力サイズ
BatchNorm:入力と出力のサイズ
Upsam:アップサンプリングする数

コードは先回と同じで、レイヤーを変えただけなので、省略します。

使ったデータ

今回使われた画像はsafebooruから取得した初音ミクの画像です。先回と同じくlbpcascade-animeface(https://github.com/nagadomi/lbpcascade_animeface )で顔を検索してその部分を取ったが、それだけではなく、バリエーションを増やすために、検索で得た部分の2倍と3倍の範囲も取っておいたのです。そして最後に右左が逆にされる物も作る。これはデータを増やす方法の一つです。

1000925_1_0.jpg
1000637_1_0.jpg

こうやって初音ミクの画像33198枚ができて、これを学習に使いました。

miku.jpg

なぜミクを選んだというと、一番よく描かれたキャラであり、データが一番豊富のようです。

結果の比較

結果はこうなります。

1/3回

ConvT pts000_012800-.jpg
Upsam+Conv pts000_012800.jpg

1回

ConvT pts001-.jpg
Upsam+Conv pts001.jpg

2回

ConvT pts002.jpg
Upsam+Conv pts002-.jpg

3回

ConvT pts003.jpg
Upsam+Conv pts003-.jpg

5回

ConvT pts005.jpg
Upsam+Conv pts005-.jpg

10回

ConvT pts010.jpg
Upsam+Conv pts010-.jpg

15回

ConvT pts015.jpg
Upsam+Conv pts015-.jpg

20回
ConvT pts020.jpg
Upsam+Conv pts020-.jpg

40回
ConvT pts040.jpg
Upsam+Conv pts040-.jpg

60回
ConvT pts060.jpg
Upsam+Conv pts060-.jpg

意外と、Upsam+ConvよりもConvTの方がうまくいけるってことは明らかです。

Upsam+Convの方がもうモードが崩れたので、続きはもう作りません。

ConvTの方が120回はこう

pts120.jpg

そして160回まで

pts160.jpg

最初はどっちも未熟で、どっちが上手かはあまり読み切れないのですが、どんどん違いがはっきり見えるようになっていくのです。

ConvTの方が十字のようなノイズが明らかですが、学習すればするほどどんどん少なくなっていくようです。

ちなみに、160回の結果を使ってこんなものくらいはできました。

ganimg.jpg

近くから遠い距離に変わるところはかなり急。いつの間にかいきなり変わっちゃったって感じ。模範の中では3サイズしか準備されていないせいですね。

たくさん生成して背景としてMMDモデルと並べて使うとこんな風に。

mikumiku.jpg

BatchNormと活性化関数の順番について

DCGANの実装でもう一つよく見つかる違いはBatchNormと活性化関数の順番です。BatchNormは活性化関数の前に置くか、後ろに置くか、どっちでも使う人がいます。どっちの方がいいと疑問して検索してみたらこの記事を見つけました。
http://minibatch.net/2017/06/11/经典论文-Batch-Normalization

BatchNormを後ろの方に置いた方がいいという結果が教えられます。

それでも私の実験でできた結果は違います。BatchNormを前に置く方がうまくいけるようです。

これはConvTを使ってBatchNormを後ろに置いた時の60回訓練した後の結果です。

pts060.jpg

前に置いた時のと比べると、形があまり整えない。十字のようなノイズもあまり消えません。訓練し続けてももっとよくなりません。むしろモードは崩れていきがちです。

それにこれは偶然ではなく何回も試しましたが、学習は本当にあまり進まないようです。

Upsam+Convの場合も比べましたが、もともといい画像が作れていないため、違いはあまり見えにくいです。どっちらもConvTを使う場合みたいにいい画像が作れません。

おまけに

Upsam+Convの訓練段階の中でこんな怖そうなものが出てきました。

pts004_012800.jpg

あまりにも刺激的なので、これを使ってこんな画像を作ってしまいました。

未标题-1.jpg

終わりに

ここで結果から見ると

  • ConvTを使った方がUpsam+Convよりいい
  • BatchNormは活性化関数の前に置いた方がいい

それでも場合によって結果は違うこともあるかもしれません。他の人の実験を見ることも必要だと思います。

49
37
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
37