前置き
先回は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の方が十字のようなものが多いのは特徴です。それに対し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]
比較するモデル
ジェネレーターはこのように違うモデルを使います。
ConvTはUpsamplingとConvに置き換えられる。ただし一番目のConvTはそもそも使う目的が違うのでそのまま。
ConvTを使うモデルは先回のモデルと同じですが、今回は96pxの画像に使うものなのでちょっと数字が違います。
ディスクリミネータの方は同じモデルを使います。
後ろの数字は
ConvとConvT:入力サイズ,出力サイズ,フィルターサイズ,ストライド,パディング
Lin:入力サイズ,出力サイズ
BatchNorm:入力と出力のサイズ
Upsam:アップサンプリングする数
コードは先回と同じで、レイヤーを変えただけなので、省略します。
使ったデータ
今回使われた画像はsafebooruから取得した初音ミクの画像です。先回と同じくlbpcascade-animeface(https://github.com/nagadomi/lbpcascade_animeface )で顔を検索してその部分を取ったが、それだけではなく、バリエーションを増やすために、検索で得た部分の2倍と3倍の範囲も取っておいたのです。そして最後に右左が逆にされる物も作る。これはデータを増やす方法の一つです。
こうやって初音ミクの画像33198枚ができて、これを学習に使いました。
なぜミクを選んだというと、一番よく描かれたキャラであり、データが一番豊富のようです。
結果の比較
結果はこうなります。
1/3回
1回
2回
3回
5回
10回
15回
意外と、Upsam+ConvよりもConvTの方がうまくいけるってことは明らかです。
Upsam+Convの方がもうモードが崩れたので、続きはもう作りません。
ConvTの方が120回はこう
そして160回まで
最初はどっちも未熟で、どっちが上手かはあまり読み切れないのですが、どんどん違いがはっきり見えるようになっていくのです。
ConvTの方が十字のようなノイズが明らかですが、学習すればするほどどんどん少なくなっていくようです。
ちなみに、160回の結果を使ってこんなものくらいはできました。
近くから遠い距離に変わるところはかなり急。いつの間にかいきなり変わっちゃったって感じ。模範の中では3サイズしか準備されていないせいですね。
たくさん生成して背景としてMMDモデルと並べて使うとこんな風に。
BatchNormと活性化関数の順番について
DCGANの実装でもう一つよく見つかる違いはBatchNormと活性化関数の順番です。BatchNormは活性化関数の前に置くか、後ろに置くか、どっちでも使う人がいます。どっちの方がいいと疑問して検索してみたらこの記事を見つけました。
http://minibatch.net/2017/06/11/经典论文-Batch-Normalization
BatchNormを後ろの方に置いた方がいいという結果が教えられます。
それでも私の実験でできた結果は違います。BatchNormを前に置く方がうまくいけるようです。
これはConvTを使ってBatchNormを後ろに置いた時の60回訓練した後の結果です。
前に置いた時のと比べると、形があまり整えない。十字のようなノイズもあまり消えません。訓練し続けてももっとよくなりません。むしろモードは崩れていきがちです。
それにこれは偶然ではなく何回も試しましたが、学習は本当にあまり進まないようです。
Upsam+Convの場合も比べましたが、もともといい画像が作れていないため、違いはあまり見えにくいです。どっちらもConvTを使う場合みたいにいい画像が作れません。
おまけに
Upsam+Convの訓練段階の中でこんな怖そうなものが出てきました。
あまりにも刺激的なので、これを使ってこんな画像を作ってしまいました。
終わりに
ここで結果から見ると
- ConvTを使った方がUpsam+Convよりいい
- BatchNormは活性化関数の前に置いた方がいい
それでも場合によって結果は違うこともあるかもしれません。他の人の実験を見ることも必要だと思います。