[DCGAN 모델] 코드 리뷰
딥러닝(Deep-Learning)

[DCGAN 모델] 코드 리뷰

반응형

[DCGAN 모델] 코드 리뷰






DCGAN을 통한 MNIST 과정을 실제 구현한 코드로, 어떤 진행과정을 거치는지 알아보려고 합니다.

DCGAN 모델에서 train.py와 model.py의 코드를 살펴보겠습니다.


DCGAN github 주소 : https://github.com/yihui-he/GAN-MNIST



기존에 구성된 train.py에 우리가 원하는 model을 만들어 학습을 시켜야 하는데, train이 어떻게 이루어지는지 진행과정을 알고 있는 것도 중요합니다.

또한 현재 github에 만들어진 model이 어떤 구성요소를 갖추고 있는지도 확인해보도록 합시다.


코드는 train.py와 model.py를 조금씩 쪼개서 설명이 필요할 때마다 번갈아가며 작성하도록 하겠습니다.


train.py


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import os
import numpy as np
from model import *
from util import *
from load import mnist_with_valid_set
 
n_epochs = 100
learning_rate = 0.0002
batch_size = 128
image_shape = [28,28,1]
dim_z = 100
dim_W1 = 1024
dim_W2 = 128
dim_W3 = 64
dim_channel = 1
 
visualize_dim=196
 
trX, vaX, teX, trY, vaY, teY = mnist_with_valid_set()
 
dcgan_model = DCGAN(
        batch_size=batch_size,
        image_shape=image_shape,
        dim_z=dim_z,
        dim_W1=dim_W1,
        dim_W2=dim_W2,
        dim_W3=dim_W3,
        )
 
Z_tf, Y_tf, image_tf, d_cost_tf, g_cost_tf, p_real, p_gen = dcgan_model.build_model()
sess = tf.InteractiveSession()
saver = tf.train.Saver(max_to_keep=10)
cs

train.py의 시작 부분입니다.
학습에 사용될 요소를 우선 import 해주는 모습을 볼 수 있구요.

import os
import numpy as np
from model import *
from util import *
from load import mnist_with_valid_set

아래 model, util, load는 같은 Mnist 폴더에 존재하는 파일들입니다.

n_epochs = 100
learning_rate = 0.0002
batch_size = 128
image_shape = [28,28,1]
dim_z = 100
dim_W1 = 1024
dim_W2 = 128
dim_W3 = 64
dim_channel = 1
 
visualize_dim=196

DCGAN 클래스에서 필요한 변수들을 설정하는 모습입니다. 이제 우리는 이 변수의 값을 이용해 생성자와 구분자를 만들어갈 것입니다.



trX, vaX, teX, trY, vaY, teY = mnist_with_valid_set()



mnist에 사용될 validation 변수를 생성하는 모습입니다. tr은 training set, va는 validation set, te는 test set라고 생각하면 됩니다.

(이 3가지의 차이점에 대해서도 잘 알고 있어야 함 - validation set은 모델 성능을 평가하기 위한 data set)


1
2
3
4
5
6
7
8
9
10
11
12
dcgan_model = DCGAN(
        batch_size=batch_size,
        image_shape=image_shape,
        dim_z=dim_z,
        dim_W1=dim_W1,
        dim_W2=dim_W2,
        dim_W3=dim_W3,
        )
 
Z_tf, Y_tf, image_tf, d_cost_tf, g_cost_tf, p_real, p_gen = dcgan_model.build_model()
sess = tf.InteractiveSession()
saver = tf.train.Saver(max_to_keep=10)
cs


앞서 만들었던 변수 값을 통해 DCGAN 클래스를 생성하는 모습입니다.


이를 더 제대로 이해하기 위해, model.py에 생성된 DCGAN 클래스를 확인해봅시다



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class DCGAN():
    def __init__(
            self,
            batch_size=100,
            image_shape=[28,28,1],
            dim_z=100,
            dim_y=10,
            dim_W1=1024,
            dim_W2=128,
            dim_W3=64,
            dim_channel=1,
            ):
 
        self.batch_size = batch_size
        self.image_shape = image_shape
        self.dim_z = dim_z
        self.dim_y = dim_y
 
        self.dim_W1 = dim_W1
        self.dim_W2 = dim_W2
        self.dim_W3 = dim_W3
        self.dim_channel = dim_channel
 
        self.gen_W1 = tf.Variable(tf.random_normal([dim_z+dim_y, dim_W1], stddev=0.02), name='gen_W1')
        self.gen_W2 = tf.Variable(tf.random_normal([dim_W1+dim_y, dim_W2*7*7], stddev=0.02), name='gen_W2')
        self.gen_W3 = tf.Variable(tf.random_normal([5,5,dim_W3,dim_W2+dim_y], stddev=0.02), name='gen_W3')
        self.gen_W4 = tf.Variable(tf.random_normal([5,5,dim_channel,dim_W3+dim_y], stddev=0.02), name='gen_W4')
 
        self.discrim_W1 = tf.Variable(tf.random_normal([5,5,dim_channel+dim_y,dim_W3], stddev=0.02), name='discrim_W1')
        self.discrim_W2 = tf.Variable(tf.random_normal([5,5,dim_W3+dim_y,dim_W2], stddev=0.02), name='discrim_W2')
        self.discrim_W3 = tf.Variable(tf.random_normal([dim_W2*7*7+dim_y,dim_W1], stddev=0.02), name='discrim_W3')
        self.discrim_W4 = tf.Variable(tf.random_normal([dim_W1+dim_y,1], stddev=0.02), name='discrim_W4')
 
    def build_model(self):
 
        Z = tf.placeholder(tf.float32, [self.batch_size, self.dim_z])
        Y = tf.placeholder(tf.float32, [self.batch_size, self.dim_y])
 
        image_real = tf.placeholder(tf.float32, [self.batch_size]+self.image_shape)
        h4 = self.generate(Z,Y)
        image_gen = tf.nn.sigmoid(h4)
        raw_real = self.discriminate(image_real, Y)
        p_real = tf.nn.sigmoid(raw_real)
        raw_gen = self.discriminate(image_gen, Y)
        p_gen = tf.nn.sigmoid(raw_gen)
        discrim_cost_real = bce(raw_real, tf.ones_like(raw_real))
        discrim_cost_gen = bce(raw_gen, tf.zeros_like(raw_gen))
        discrim_cost = discrim_cost_real + discrim_cost_gen
 
        gen_cost = bce( raw_gen, tf.ones_like(raw_gen) )
 
        return Z, Y, image_real, discrim_cost, gen_cost, p_real, p_gen
 
    def discriminate(self, image, Y):
        yb = tf.reshape(Y, tf.stack([self.batch_size, 11, self.dim_y]))
        X = tf.concat(axis=3, values=[image, yb*tf.ones([self.batch_size, 2828, self.dim_y])])
 
        h1 = lrelu( tf.nn.conv2d( X, self.discrim_W1, strides=[1,2,2,1], padding='SAME' ))
        h1 = tf.concat(axis=3, values=[h1, yb*tf.ones([self.batch_size, 1414, self.dim_y])])
 
        h2 = lrelu( batchnormalize( tf.nn.conv2d( h1, self.discrim_W2, strides=[1,2,2,1], padding='SAME')) )
        h2 = tf.reshape(h2, [self.batch_size, -1])
        h2 = tf.concat(axis=1, values=[h2, Y])
 
        h3 = lrelu( batchnormalize( tf.matmul(h2, self.discrim_W3 ) ))
        h3 = tf.concat(axis=1, values=[h3, Y])
        
        h4 = lrelu(batchnormalize(tf.matmul(h3,self.discrim_W4)))
        
        return h4
 
    def generate(self, Z, Y):
 
        yb = tf.reshape(Y, [self.batch_size, 11, self.dim_y])
        Z = tf.concat(axis=1, values=[Z,Y])
        h1 = tf.nn.relu(batchnormalize(tf.matmul(Z, self.gen_W1)))
        h1 = tf.concat(axis=1, values=[h1, Y])
        h2 = tf.nn.relu(batchnormalize(tf.matmul(h1, self.gen_W2)))
        h2 = tf.reshape(h2, [self.batch_size,7,7,self.dim_W2])
        h2 = tf.concat(axis=3, values=[h2, yb*tf.ones([self.batch_size, 77, self.dim_y])])
 
        output_shape_l3 = [self.batch_size,14,14,self.dim_W3]
        h3 = tf.nn.conv2d_transpose(h2, self.gen_W3, output_shape=output_shape_l3, strides=[1,2,2,1])
        h3 = tf.nn.relu( batchnormalize(h3) )
        h3 = tf.concat(axis=3, values=[h3, yb*tf.ones([self.batch_size, 14,14,self.dim_y])] )
 
        output_shape_l4 = [self.batch_size,28,28,self.dim_channel]
        h4 = tf.nn.conv2d_transpose(h3, self.gen_W4, output_shape=output_shape_l4, strides=[1,2,2,1])
        return h4
cs



상당히 복잡하고 긴 클래스입니다 ㅠㅠ 메소드 별로 나누어서 하나씩 차근차근 알아보겠습니다



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class DCGAN():
    def __init__(
            self,
            batch_size=100,
            image_shape=[28,28,1],
            dim_z=100,
            dim_y=10,
            dim_W1=1024,
            dim_W2=128,
            dim_W3=64,
            dim_channel=1,
            ):
 
        self.batch_size = batch_size
        self.image_shape = image_shape
        self.dim_z = dim_z
        self.dim_y = dim_y
 
        self.dim_W1 = dim_W1
        self.dim_W2 = dim_W2
        self.dim_W3 = dim_W3
        self.dim_channel = dim_channel
 
        self.gen_W1 = tf.Variable(tf.random_normal([dim_z+dim_y, dim_W1], stddev=0.02), name='gen_W1')
        self.gen_W2 = tf.Variable(tf.random_normal([dim_W1+dim_y, dim_W2*7*7], stddev=0.02), name='gen_W2')
        self.gen_W3 = tf.Variable(tf.random_normal([5,5,dim_W3,dim_W2+dim_y], stddev=0.02), name='gen_W3')
        self.gen_W4 = tf.Variable(tf.random_normal([5,5,dim_channel,dim_W3+dim_y], stddev=0.02), name='gen_W4')
 
        self.discrim_W1 = tf.Variable(tf.random_normal([5,5,dim_channel+dim_y,dim_W3], stddev=0.02), name='discrim_W1')
        self.discrim_W2 = tf.Variable(tf.random_normal([5,5,dim_W3+dim_y,dim_W2], stddev=0.02), name='discrim_W2')
        self.discrim_W3 = tf.Variable(tf.random_normal([dim_W2*7*7+dim_y,dim_W1], stddev=0.02), name='discrim_W3')
        self.discrim_W4 = tf.Variable(tf.random_normal([dim_W1+dim_y,1], stddev=0.02), name='discrim_W4')
cs


쉽게, 자바나 C++에서 클래스에서 생성자를 만드는 걸 상상하시면 됩니다. 현재 변수마다 값이 정해져 있지만,

우리가 앞서 봤던 train.py에서 지정한 변수들의 값으로 변경될 것 입니다. 다른 값은 똑같이 설정되어있고, batch_size만 100에서 128로 변경되겠죠?


이제 변수들을 통해 generator와 discriminate에서 사용할 W의 shape 값들을 알아봅시다.


gen_W1 = [dim_z + dim_y, dim_W1] = [110, 1024]

gen W2 = [dim_W1+dim_y, dim_W2*7*7] = [1034, 128*7*7]

gen_W3 = [5,5,dim_W3,dim_W2+dim_y] = [5, 5, 64, 138]

gen_W4 = [5,5,dim_channel,dim_W3+dim_y] = [5, 5, 1, 74]


discrim_W1 = [5,5,dim_channel+dim_y,dim_W3] = [5, 5, 11, 64]

discrim_W2 = [5,5,dim_W3+dim_y,dim_W2] = [5, 5, 74, 128]

discrim_W3 = [dim_W2*7*7+dim_y,dim_W1] = [128*7*7+10, 1024]

discrim_W4 = [dim_W1+dim_y,1] = [1034, 1]


각 shape마다 연산을 하면 위와 같은 모습으로 나타낼 수 있습니다!


이제 모델을 만드는 build_model 정의를 살펴봅시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    def build_model(self):
 
        Z = tf.placeholder(tf.float32, [self.batch_size, self.dim_z])
        Y = tf.placeholder(tf.float32, [self.batch_size, self.dim_y])
 
        image_real = tf.placeholder(tf.float32, [self.batch_size]+self.image_shape)
        h4 = self.generate(Z,Y)
        image_gen = tf.nn.sigmoid(h4)
        raw_real = self.discriminate(image_real, Y)
        p_real = tf.nn.sigmoid(raw_real)
        raw_gen = self.discriminate(image_gen, Y)
        p_gen = tf.nn.sigmoid(raw_gen)
        discrim_cost_real = bce(raw_real, tf.ones_like(raw_real))
        discrim_cost_gen = bce(raw_gen, tf.zeros_like(raw_gen))
        discrim_cost = discrim_cost_real + discrim_cost_gen
 
        gen_cost = bce( raw_gen, tf.ones_like(raw_gen) )
 
        return Z, Y, image_real, discrim_cost, gen_cost, p_real, p_gen
cs


Noise에 해당하는 Z와 Label에 해당하는 Y가 placeholder로 지정된 모습입니다. 각 shape은 아래와 같습니다.


Z = [self.batch_size, self.dim_z] = [128,100]

Y = [self.batch_size, self.dim_y] = [128,10]


다음 코드를 설명하기에 앞서, 나오는 변수명부터 알고 시작합시다!


image_real : 실제 이미지

image_gen : generator로 생성한 가짜 이미지

raw_real : 실제 이미지와 Label을 discriminate한 값

p_real : raw_real값에 sigmoid를 통해 확률로 만든 값

raw_gen : 생성 이미지와 Label을 discriminate한 값

p_gen : raw_gen값에 sigmoid를 통해 확률로 만든 값



image_real을 placeholder로 만든 이유는, 실제 이미지를 집어넣기 위한 것 입니다.

shape은 이미지 크기인 28x28x1에 batch_size 128을 더하면서 [128, 28, 28, 1]이 됩니다.


h4 = self.generate(Z,Y)


이제 generate가 시작되는 모습을 볼 수 있습니다. model.py에 있는 generate 선언 부분은 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def generate(self, Z, Y):
 
        yb = tf.reshape(Y, [self.batch_size, 11, self.dim_y])
        Z = tf.concat(axis=1, values=[Z,Y])
        h1 = tf.nn.relu(batchnormalize(tf.matmul(Z, self.gen_W1)))
        h1 = tf.concat(axis=1, values=[h1, Y])
        h2 = tf.nn.relu(batchnormalize(tf.matmul(h1, self.gen_W2)))
        h2 = tf.reshape(h2, [self.batch_size,7,7,self.dim_W2])
        h2 = tf.concat(axis=3, values=[h2, yb*tf.ones([self.batch_size, 77, self.dim_y])])
 
        output_shape_l3 = [self.batch_size,14,14,self.dim_W3]
        h3 = tf.nn.conv2d_transpose(h2, self.gen_W3, output_shape=output_shape_l3, strides=[1,2,2,1])
        h3 = tf.nn.relu( batchnormalize(h3) )
        h3 = tf.concat(axis=3, values=[h3, yb*tf.ones([self.batch_size, 14,14,self.dim_y])] )
 
        output_shape_l4 = [self.batch_size,28,28,self.dim_channel]
        h4 = tf.nn.conv2d_transpose(h3, self.gen_W4, output_shape=output_shape_l4, strides=[1,2,2,1])
        return h4
cs


차근차근 한줄씩 진행해보도록 할게요


yb = tf.reshape(Y, [self.batch_size, 11, self.dim_y])


Y가 가진 shape를 [self.batch_size, 1, 1, self.dim_y]로 변경해 yb 변수에 저장합니다.

따라서 yb = [128, 1, 1, 10]이 됩니다.



= tf.concat(axis=1, values=[Z,Y])


concat은 지정한 axis 위치의 shape 행렬을 더해주는 역할을 합니다. 현재 value 값은 Z와 Y이므로

concat(Z,Y) = [128, 100], [128, 10] = [128, 100+10] = [128, 110]

(axis는 행렬 첫번째가 0부터 시작, 보통 concat을 하면 행렬의 값이 다른 부분에 axis를 잡고 이루어집니다.)

이제 Z = [128, 110]이 되었습니다.



h1 = tf.nn.relu(batchnormalize(tf.matmul(Z, self.gen_W1)))


matmul은 행렬을 곱하는 것으로, Z와 gen_W1을 곱해봅시다.

[128, 110] * [110, 1024] = [128, 1024]

이제 이 값을 batchnormalize를 통해 정규화과정을 거치고, relu로 activation한 값을 h1에 저장합니다.

shape 형태는 유지되므로 h1 = [128, 1024]입니다.



h1 = tf.concat(axis=1, values=[h1, Y])


concat은 아까 설명한대로 진행하면 됩니다.

진행결과 h1은 [128, 1024+10] = [128, 1034]가 됩니다.



h2 = tf.nn.relu(batchnormalize(tf.matmul(h1, self.gen_W2)))


이것도 반복되는 코드죠? 위에서 구한 h1과 gen_W2를 행렬 곱을 합니다.

[128, 1034] * [1034, 128*7*7] = [128, 128*7*7]

h2 = [128, 128*7*7]



h2 = tf.reshape(h2, [self.batch_size,7,7,self.dim_W2])


h2를 reshape 해주는 과정입니다. shape을 [self.batch_size, 7, 7, self.dim_W2]로 바꿔주면 되네요

h2 = [128, 7, 7, 128]

(reshape을 할 때, 행렬의 크기는 유지되어야 합니다!)



h2 = tf.concat(axis=3, values=[h2, yb*tf.ones([self.batch_size, 77, self.dim_y])])


또 concat이 이루어집니다. 그 전에 values에 있는 값부터 정리합시다.

tf.ones란, 모든 원소 값이 1인 텐서를 생성하는 기능입니다.

yb = [128, 1, 1, 10]이므로 [128, 7, 7, 10]을 곱해주면 [128, 7, 7, 10]이 됩니다.

h2와 이 값을 concat하면 되겠죠? 현재 마지막 행렬 값만 다르고, 이 위치는 axis=3이기 때문에 제대로 작성되었음을 알 수 있습니다.

h2 = [128, 7, 7, 128+10] = [128, 7, 7, 138]



output_shape_l3 = [self.batch_size,14,14,self.dim_W3]

output_shape_l3의 변수에 [128, 14, 14, 64]의 shape을 저장합니다.



h3 = tf.nn.conv2d_transpose(h2, self.gen_W3, output_shape=output_shape_l3, strides=[1,2,2,1])


새로운 함수가 나왔습니다. conv2d_transpose는 데이터를 convolution 진행 시 upsampling 할 때 데이터의 피쳐를 살려 이미지화하는 함수입니다.

(upsampling : 샘플을 인위적으로 늘리는 일)


첫번째 인자인 h2는 value, 두번째 인자 self.gen_W3은 filter를 나타냅니다.

h2 = [128, 7, 7, 138]

gen_W3 = [5, 5, 64, 138]

output_shape_l3 = [128, 14, 14, 64]

strides = [1, 2, 2, 1]

(padding은 따로 설정하지 않으면 'SAME'입니다)


h2는 128개의 batch_size를 가진 7x7, depth가 138인 형태입니다.

gen_W3는 5x5, depth가 138인 filter가 64개로 구성되어 있습니다.

(★ value와 filter의 depth는 일치해야 합니다!)

filter가 64개가 있으므로, 결과로 나오는 output의 shape의 depth는 64가 되어야 합니다.

stride가 2고 padding은 SAME이기 때문에, output으로 나올 28x28에서 2로 나눈 14x14가 됩니다.

따라서 output_shape_l3 = [128, 14, 14, 64]로 나오는 것을 이해할 수 있습니다. 마지막으로 이 값을 h3에 저장한 코드입니다.



h3 = tf.nn.relu( batchnormalize(h3) )

h3 = tf.concat(axis=3, values=[h3, yb*tf.ones([self.batch_size, 14,14,self.dim_y])] )


h3의 값을 정규화와 relu를 적용했습니다. (shape은 유지)

yb*tf.ones([self.batch_size, 14, 14, self.dim_y]) = [128, 1, 1, 10] * [128, 14, 14, 10] = [128, 14, 14, 10]

따라서 h3과 이 값을 concat하면, [128, 14, 14, 64+10] = [128, 14, 14, 74]입니다.



output_shape_l4 = [self.batch_size,28,28,self.dim_channel]

h4 = tf.nn.conv2d_transpose(h3, self.gen_W4, output_shape=output_shape_l4, strides=[1,2,2,1])

return h4


output_shape_l4 = [128, 28, 28, 1]

conv2d_transpose는 위에서 설명한 것처럼 h3을 value, gen_W4를 filter로 놓고 strides를 적용하면 output_shape_l4로 잘 나오는 것을 알 수 있을 겁니다.

h3 = [128, 14, 14, 74] : batch_size가 128인 14x14, depth가 74

gen_W4 = [5, 5, 1, 74]  : 5x5, depth가 74인 filter가 1개 -> output의 depth는 1임을 알 수 있음


따라서 generate를 통해 리턴되는 h4는 [128, 28, 28, 1]입니다.

(이는 즉, 28x28x1인 이미지가 128개(batch_size)인 결과를 나타냄을 알 수 있습니다.)



이제 generate가 이루어지는 과정을 모두 알아봤습니다. 


h4 = self.generate(Z,Y)

이 한 줄의 코드로 위와 같은 진행 과정을 통해 h4에 [128, 28, 28, 1]의 shape을 가진 데이터가 저장되는 것입니다.


다시 build_model를 이어서 진행해보도록 합시다!


image_gen = tf.nn.sigmoid(h4)


generate를 통해 저장한 h4를 sigmoid로 activation하여 image_gen에 저장했습니다.



raw_real = self.discriminate(image_real, Y)


이제 image_gen과 Y를 discriminate하여 raw_real에 저장해야 합니다. discriminate를 보도록 합시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def discriminate(self, image, Y):
        yb = tf.reshape(Y, tf.stack([self.batch_size, 11, self.dim_y]))
        X = tf.concat(axis=3, values=[image, yb*tf.ones([self.batch_size, 2828, self.dim_y])])
 
        h1 = lrelu( tf.nn.conv2d( X, self.discrim_W1, strides=[1,2,2,1], padding='SAME' ))
        h1 = tf.concat(axis=3, values=[h1, yb*tf.ones([self.batch_size, 1414, self.dim_y])])
 
        h2 = lrelu( batchnormalize( tf.nn.conv2d( h1, self.discrim_W2, strides=[1,2,2,1], padding='SAME')) )
        h2 = tf.reshape(h2, [self.batch_size, -1])
        h2 = tf.concat(axis=1, values=[h2, Y])
 
        h3 = lrelu( batchnormalize( tf.matmul(h2, self.discrim_W3 ) ))
        h3 = tf.concat(axis=1, values=[h3, Y])
        
        h4 = lrelu(batchnormalize(tf.matmul(h3,self.discrim_W4)))
        
        return h4
cs


지금은 인자로 들어있는 image가 image_real이 되겠죠?


yb = tf.reshape(Y, tf.stack([self.batch_size, 11, self.dim_y]))


yb에 Y를 [self.batch_size, 1, 1, self.dim_y]로 reshape하여 저장합니다.

(stack이 여기서 어떤 역할을 하고 있는지는 잘 모르겠습니다. 알게되면 수정하여 올리겠습니다.)

yb = [128, 1, 1, 10]



= tf.concat(axis=3, values=[image, yb*tf.ones([self.batch_size, 2828, self.dim_y])])


image = [128, 28, 28, 1]

yb * [128, 28, 28, 10] = [128, 28, 28, 10]

따라서 X = [128, 28, 28, 1+10] = [128, 28, 28, 11]입니다.



h1 = lrelu( tf.nn.conv2d( X, self.discrim_W1, strides=[1,2,2,1], padding='SAME' ))


이전에 generate에서는 transpose로 output값이 인자로 지정되었지만, 이번엔 직접 output값을 구해야합니다.


X (value) : [128, 28, 28, 11]

discrim_W1 (filter) : [5, 5, 11, 64]


depth가 11로 일치하고, 64개의 filter가 생성되어 output의 depth는 64가 됨을 알 수 있습니다.

padding은 SAME이고 stride 2를 적용시키면 (28 / 2) , [128, 14, 14, 64]가 됩니다. 여기에 리키 렐루를 적용시키고 h1에 저장합니다.



h1 = tf.concat(axis=3, values=[h1, yb*tf.ones([self.batch_size, 1414, self.dim_y])])


yb * [128, 14, 14, 10] = [128, 14, 14, 10]

따라서 h1과 concat한 결과는 [128, 14, 14, 64+10] = [128, 14, 14, 74]가 됩니다.



h2 = lrelu( batchnormalize( tf.nn.conv2d( h1, self.discrim_W2, strides=[1,2,2,1], padding='SAME')) )


h1 (value) : [128, 14, 14, 74]

discrim_W2 (filter) : [5, 5, 74, 128]


depth가 74로 일치하고, 128개의 filter가 생성되어 output의 depth는 128이 됨을 알 수 있습니다.

padding은 SAME이고, stride 2를 적용시키면 (14 / 2), [128, 7, 7, 128]이 됩니다.



h2 = tf.reshape(h2, [self.batch_size, -1])


h2를 [self.batch_size, -1]로 reshape해야합니다. 여기서 -1은 이 행렬의 크기로 shape을 맞춰주라는 뜻입니다.

따라서 [128, 14, 14, 128] -> [128, 7*7*128]로 shape을 변경할 수 있습니다.



h2 = tf.concat(axis=1, values=[h2, Y])


[128, 7*7*128]와 Y([128, 10])을 concat하면, h2 = [128, 7*7*128 + 10]이 됩니다.



h3 = lrelu( batchnormalize( tf.matmul(h2, self.discrim_W3 ) ))


h2와 discrim_W3을 행렬 곱 합시다.

[128, 7*7*128 + 10] * [7*7*128 + 10, 1024] = [128, 1024]

딱 나누어 떨어져서 행렬 곱이 진행되는 것을 확인할 수 있습니다!!

정규화와 리키 렐루를 적용하여 나온 값을 h3에 저장하게 됩니다. ( 당연히 shape는 변함 없음! )



h3 = tf.concat(axis=1, values=[h3, Y])


이번엔 h3과 Y([128, 10])을 concat하면, h3 = [128, 1024+10] = [128, 1034]가 됩니다.



h4 = lrelu(batchnormalize(tf.matmul(h3,self.discrim_W4)))

return h4


드디어 discriminate의 마지막입니다. h3과 discrim_W4를 행렬 곱 합시다.

[128, 1034] * [1034, 1] = [128, 1]


이제 [128, 1]을 정규화와 리키 렐루를 적용하여 나온 값을 h4로 저장하고 리턴 값으로 출력되면서 discriminate가 끝납니다.

다시 build_model로 돌아가겠습니다!


raw_real = self.discriminate(image_real, Y)


결국 이 h4 데이터가 raw_real로 저장될 것입니다.



p_real = tf.nn.sigmoid(raw_real)


이 값을 확률로 만들어주기 위해 sigmoid로 activation해주고 p_real에 저장합니다.



raw_gen = self.discriminate(image_gen, Y)


이번에는 실제 이미지가 아닌, generator로 만든 가짜 이미지 image_gen을 Label인 Y와 discriminate를 하게 됩니다.


image_gen은 우리가 generate를 거치고 아래처럼 이미 만든 기억이 있습니다.


image_gen = tf.nn.sigmoid(h4)


따라서 [128, 28, 28, 1]의 shape을 가진 image_gen을 이용해서 discriminate를 똑같이 진행해봅시다!



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def discriminate(self, image, Y):
        yb = tf.reshape(Y, tf.stack([self.batch_size, 11, self.dim_y]))
        X = tf.concat(axis=3, values=[image, yb*tf.ones([self.batch_size, 2828, self.dim_y])])
 
        h1 = lrelu( tf.nn.conv2d( X, self.discrim_W1, strides=[1,2,2,1], padding='SAME' ))
        h1 = tf.concat(axis=3, values=[h1, yb*tf.ones([self.batch_size, 1414, self.dim_y])])
 
        h2 = lrelu( batchnormalize( tf.nn.conv2d( h1, self.discrim_W2, strides=[1,2,2,1], padding='SAME')) )
        h2 = tf.reshape(h2, [self.batch_size, -1])
        h2 = tf.concat(axis=1, values=[h2, Y])
 
        h3 = lrelu( batchnormalize( tf.matmul(h2, self.discrim_W3 ) ))
        h3 = tf.concat(axis=1, values=[h3, Y])
        
        h4 = lrelu(batchnormalize(tf.matmul(h3,self.discrim_W4)))
        
        return h4
cs


이번엔 image가 image_gen으로 되어 진행되겠습니다. 위에서 discriminate 과정을 다 설명했기 때문에, 이번에는 코드설명 없이 간단히 수식으로만 진행하겠습니다!


yb = [128, 1, 1, 10]

X = concat ([128, 28, 28, 1], [128, 28, 28, 10]) = [128, 28, 28, 11]

h1 = [128, 14, 14, 64] (value와 filter의 depth가 11로 일치, filter의 수가 64개이므로 output depth = 64, stride가 2이므로 28/2 = 14)

h1 = concat ([128, 14, 14, 64], [128, 14, 14, 10]) = [128, 14, 14, 74]


h2 = [128, 7, 7, 128] (value와 filter의 depth가 74로 일치, filter의 수가 128개이므로 output depth = 128, stride가 2이므로 14/2 = 7)

h2 = reshape -> [128, 7*7*128]

h2 = concat ( [128, 7*7*128] , [128, 10] ) = [128, 7*7*128+10]


h3 = [128, 7*7*128+10] * [128*7*7+10, 1024] = [128, 1024]

h3 = concat ( [128, 1024], [128, 10] ) = [128, 1034]


h4 = [128, 1034] * [1034, 1] = [128, 1]

return h4


결국 이 h4 데이터가 raw_gen로 저장될 것입니다.


p_gen = tf.nn.sigmoid(raw_gen)


이 값을 확률로 만들어주기 위해 sigmoid로 activation해주고 p_gen에 저장합니다.



1
2
3
discrim_cost_real = bce(raw_real, tf.ones_like(raw_real))
discrim_cost_gen = bce(raw_gen, tf.zeros_like(raw_gen))
discrim_cost = discrim_cost_real + discrim_cost_gen
cs


다음으로 진행되는 코드를 보면, cost가 나오는 걸 볼 수 있습니다. discriminate를 통해 실제 이미지와 가짜 이미지에서 나오는 cost 비용을 구하는 식이지 않을까 예상해봅니다.


bce라는 새로운 함수가 나오는데요. 


이는 model.py에 따로 함수가 만들어져 있습니다.


1
2
3
def bce(o, t):
    o = tf.clip_by_value(o, 1e-71. - 1e-7)
    return tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=o, labels=t))
cs


clip_by_value를 통해 o를 0.0000001과 1.0000001 사이로 만들어주고 있습니다. 상당히 큰 값들을 이 범위 안으로 줄여주면서 발산 등으로 원하는 값을 구하지 못할 수도 있어서 범위를 잡아주는 역할을 합니다.


그리고 현재 들어온 데이터들을 cross entropy를 통해 logit과 label의 확률분포 차이를 구해 평균을 리턴해주는 모습입니다.



discrim_cost_real = bce(raw_real, tf.ones_like(raw_real))


ones_like를 통해서 raw_real의 데이터를 모두 1인 텐서로 생성한 이후 bce를 통해 확률분포 차이의 평균을 discrim_cost_real에 저장하고 있습니다.



discrim_cost_gen = bce(raw_gen, tf.zeros_like(raw_gen))


zeros_like를 통해서 raw_gen의 데이터를 모두 0인 텐서로 생성한 이후 bce를 통해 확률분포 차이의 평균을 discrim_cost_gen에 저장하고 있습니다.



여기서 생각해볼 것은, discriminator는 실제 이미지는 0에 가깝고 가짜 이미지는 1에 가깝도록 출력해야 합니다. 

따라서 실제 이미지의 cost는 1에서 raw_real의 데이터 값 차이의 평균 값을 저장한 것이고, 가짜 이미지의 cost는 0에서 raw_gen의 데이터 값 차이의 평균 값을 저장한 것입니다.


그 이후 이 두 비용 값을 discrim_cost로 아래와 같이 더해 저장하는 과정이 이루어지고 있습니다.


discrim_cost = discrim_cost_real + discrim_cost_gen




gen_cost = bce( raw_gen, tf.ones_like(raw_gen) )


이번에는 가짜 이미지인 raw_gen의 데이터를 모두 1인 텐서로 생성한 이후 bce를 통해 확률분포 차이 평균을 gen_cost로 저장했습니다.

왜 가짜 이미지를 1인 텐서와 확률 분포 차이를 구할까요? 만약 generator가 학습을 제대로 행하면서 이미지를 진짜처럼 구현해낸다면 gen_cost가 실제 이미지와 유사하게 나오게 될 것입니다. 


GAN의 최종적인 목표는, 이 discriminator가 generator가 생성한 가짜 이미지를 구분할 수 없도록 만드는 것인데 이게 가능한 좋은 모델이 만들어진다면 실제 이미지의 cost와 가짜 이미지의 cost 비용 값이 비슷하게 나오지 않을까 생각하게 되었습니다.



return Z, Y, image_real, discrim_cost, gen_cost, p_real, p_gen


build_model 과정을 모두 진행했고, return으로 총 7가지 값을 받아왔습니다. 이제 이 값을 토대로 train.py에서 학습을 진행할 것입니다!




train.py 남은 과정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
discrim_vars = filter(lambda x: x.name.startswith('discrim'), tf.trainable_variables())
gen_vars = filter(lambda x: x.name.startswith('gen'), tf.trainable_variables())
discrim_vars = [i for i in discrim_vars]
gen_vars = [i for i in gen_vars]
 
train_op_discrim = tf.train.AdamOptimizer(learning_rate, beta1=0.5).minimize(d_cost_tf, var_list=discrim_vars)
train_op_gen = tf.train.AdamOptimizer(learning_rate, beta1=0.5).minimize(g_cost_tf, var_list=gen_vars)
 
Z_tf_sample, Y_tf_sample, image_tf_sample = dcgan_model.samples_generator(batch_size=visualize_dim)
 
tf.global_variables_initializer().run()
 
Z_np_sample = np.random.uniform(-11, size=(visualize_dim,dim_z))
Y_np_sample = OneHot( np.random.randint(10, size=[visualize_dim]))
iterations = 0
= 2
 
step = 200
 
for epoch in range(n_epochs):
    index = np.arange(len(trY))
    np.random.shuffle(index)
    trX = trX[index]
    trY = trY[index]
 
    for start, end in zip(
            range(0len(trY), batch_size),
            range(batch_size, len(trY), batch_size)
            ):
 
        Xs = trX[start:end].reshape( [-128281]) / 255.
        Ys = OneHot(trY[start:end])
        Zs = np.random.uniform(-11, size=[batch_size, dim_z]).astype(np.float32)
 
        if np.mod( iterations, k ) != 0:
            _, gen_loss_val = sess.run(
                    [train_op_gen, g_cost_tf],
                    feed_dict={
                        Z_tf:Zs,
                        Y_tf:Ys
                        })
            discrim_loss_val, p_real_val, p_gen_val = sess.run([d_cost_tf,p_real,p_gen], feed_dict={Z_tf:Zs, image_tf:Xs, Y_tf:Ys})
            print("=========== updating G ==========")
            print("iteration:", iterations)
            print("gen loss:", gen_loss_val)
            print("discrim loss:", discrim_loss_val)
 
        else:
            _, discrim_loss_val = sess.run(
                    [train_op_discrim, d_cost_tf],
                    feed_dict={
                        Z_tf:Zs,
                        Y_tf:Ys,
                        image_tf:Xs
                        })
            gen_loss_val, p_real_val, p_gen_val = sess.run([g_cost_tf, p_real, p_gen], feed_dict={Z_tf:Zs, image_tf:Xs, Y_tf:Ys})
            print("=========== updating D ==========")
            print("iteration:", iterations)
            print("gen loss:", gen_loss_val)
            print("discrim loss:", discrim_loss_val)
 
        print("Average P(real)=", p_real_val.mean())
        print("Average P(gen)=", p_gen_val.mean())
 
        if np.mod(iterations, step) == 0:
            generated_samples = sess.run(
                    image_tf_sample,
                    feed_dict={
                        Z_tf_sample:Z_np_sample,
                        Y_tf_sample:Y_np_sample
                        })
            generated_samples = (generated_samples + 1.)/2.
            save_visualization(generated_samples, (14,14), save_path='./vis/sample_%04d.jpg' % int(iterations/step))
 
        iterations += 1
cs



처음부터 알아보도록 하겠습니다. (sample 부분 제외)


discrim_vars = filter(lambda x: x.name.startswith('discrim'), tf.trainable_variables())

gen_vars = filter(lambda x: x.name.startswith('gen'), tf.trainable_variables())


name.startswith는 주어진 인자로 시작하는 변수를 찾아주는 기능을 합니다.


즉, discrim_vars에는 discrim으로 시작하는 변수만 filter를 이용해 묶어 저장되고, gen_vars에는 gen으로 시작하는 변수만 저장됩니다.


discrim_vars = [i for i in discrim_vars]

gen_vars = [i for i in gen_vars]


이 두 변수를 리스트 형식으로 만들어준 코드입니다. 리스트로 만든 이유는 다음 코드에서 알 수 있습니다!


train_op_discrim = tf.train.AdamOptimizer(learning_rate, beta1=0.5).minimize(d_cost_tf, var_list=discrim_vars)

train_op_gen = tf.train.AdamOptimizer(learning_rate, beta1=0.5).minimize(g_cost_tf, var_list=gen_vars)


Gradient Descent 알고리즘 중에서 Adam을 통해 discriminator와 generator의 cost function을 최소화하고 있습니다. minimize하는 과정에서 각 변수가 list형식으로 받아서 진행되고 있는 것을 확인할 수 있습니다.


tf.global_variables_initializer().run()


항상 모델을 training할 때 필요한 초기화 함수도 진행되구요.


for문에 들어가기에 앞서, 처음에 선언된 변수 값부터 확인하겠습니다.


iterations = 0

= 2

step = 200


이 변수 값들이 어떻게 쓰일지는, for문 안에서 동작 과정에 나오면 설명하겠습니다.


 

1
2
3
4
5
for epoch in range(n_epochs):
    index = np.arange(len(trY))
    np.random.shuffle(index)
    trX = trX[index]
    trY = trY[index]
cs


처음에 n_epochs는 200으로 값을 설정했었습니다. 즉, 전체 training data를 200번 학습시킨다는 뜻이 되겠습니다.


np.arange는 numpy에서 range 명령입니다. trY의 length를 range 값으로 index에 저장합니다.

trY는 load.py의 mnist와 minist_with_valid_set에서 정의하고 있습니다.


trY = trY[:50000] 이면, 50000개까지 저장하는 것으로 파악됩니다.

즉, index = array[[0,1,2, ... , 49999]]로 구성될 것입니다.


이 index를 random.shuffle를 통해 섞어주고, trX와 trY에 저장했습니다.
epoch이 한번씩 돌 때마다, shuffle로 index를 계속 랜덤으로 뒤죽박죽 만들어주며 다르게 학습되도록 만들고 있습니다.



1
2
3
4
for start, end in zip(
      range(0len(trY), batch_size),
      range(batch_size, len(trY), batch_size)
      ):
cs


epoch 안에서 또 for문이 진행되고 있는데요. zip을 통해 두 range를 start와 end로 각각 넣는 모습입니다.


range(0, len(trY), batch_size) = range(0, 50000, 128) -> 0부터 50000까지 128씩 증가

range(batch_size, len(trY), batch_size) = range(128, 50000, 128) -> 128부터 50000까지 128씩 증가


zip으로 위 두 개의 range가 묶이면 다음과 같이 반복됩니다.


(0, 128)

(128, 256)

(256, 384)

...



1
2
3
Xs = trX[start:end].reshape( [-128281]) / 255.
Ys = OneHot(trY[start:end])
Zs = np.random.uniform(-11, size=[batch_size, dim_z]).astype(np.float32)
cs


첫 for문은 start=0, end=128일 것입니다. 이는 0이상 128미만이라는 뜻입니다.


Xs = trX[0:128]를 [-1, 28, 28, 1]로 reshape하면

[[28, 28, 1], [28, 28, 1], [28, 28, 1], ... , [28, 28, 1], [28, 28, 1]]로 구성되며, [28, 28, 1]은 총 128개가 생성됩니다.


따라서 Xs = [128, 28, 28, 1]로 나타낼 수 있습니다. 255로 나누는 것은 픽셀 스케일을 나누는 것을 말합니다. (확실히 모르겠음. 한번 더 정리 필요)


Ys = trY[0:128]을 OneHot으로 저장한 데이터입니다. OneHot은 Softmax에서 배웠듯이 0 또는 1로 표시하는 것을 말합니다.

Mnist는 0~9까지의 lable을 가지므로, Ys의 Shape은 [128, 10]이 됩니다.


Zs의 사이즈는 [batch_size, dim_z] = [128, 100]입니다. random.uniform으로 -1부터 1까지 범위를 준 건, 이 데이터의 사이즈에 -1부터 1 사이의 값으로 type을 float으로 줬기 때문에 임의의 실수를 생성하는 것을 말합니다.


이 3가지 변수를 이용해서 loss 값을 구할 때 placeholder로 값을 가져오는 Z_tf, image_tf, Y_tf와 각각 shape이 일치하여 feed_dict로 활용합니다.


 

이제 변수를 다 만들었고, 조건문이 나오게 되는데요. 나머지 소스코드는 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
        if np.mod( iterations, k ) != 0:
            _, gen_loss_val = sess.run(
                    [train_op_gen, g_cost_tf],
                    feed_dict={
                        Z_tf:Zs,
                        Y_tf:Ys
                        })
            discrim_loss_val, p_real_val, p_gen_val = sess.run([d_cost_tf,p_real,p_gen], feed_dict={Z_tf:Zs, image_tf:Xs, Y_tf:Ys})
            print("=========== updating G ==========")
            print("iteration:", iterations)
            print("gen loss:", gen_loss_val)
            print("discrim loss:", discrim_loss_val)
 
        else:
            _, discrim_loss_val = sess.run(
                    [train_op_discrim, d_cost_tf],
                    feed_dict={
                        Z_tf:Zs,
                        Y_tf:Ys,
                        image_tf:Xs
                        })
            gen_loss_val, p_real_val, p_gen_val = sess.run([g_cost_tf, p_real, p_gen], feed_dict={Z_tf:Zs, image_tf:Xs, Y_tf:Ys})
            print("=========== updating D ==========")
            print("iteration:", iterations)
            print("gen loss:", gen_loss_val)
            print("discrim loss:", discrim_loss_val)
 
        print("Average P(real)=", p_real_val.mean())
        print("Average P(gen)=", p_gen_val.mean())
 
        if np.mod(iterations, step) == 0:
            generated_samples = sess.run(
                    image_tf_sample,
                    feed_dict={
                        Z_tf_sample:Z_np_sample,
                        Y_tf_sample:Y_np_sample
                        })
            generated_samples = (generated_samples + 1.)/2.
            save_visualization(generated_samples, (14,14), save_path='./vis/sample_%04d.jpg' % int(iterations/step))
 
        iterations += 1
 
cs


세부코드를 제외한 조건문만 먼저 살펴보겠습니다. 



if np.mod( iterations, k ) != 0:

generator loss를 train


else:

discriminator loss를 train



iterations+=1


>>>>> iterations를 k로 나눴을 때 나머지가 0일 때와 아닐 때로 나눈 상황




iterations의 초기값은 0이고, k는 2였습니다. 즉, 나머지가 1이면 generator loss를 training하는 것이고 

나머지가 0이면 else가 되어 discriminator loss를 training하는 조건문입니다.


이렇게 나눈 이유는, generator와 discriminator는 동시에 진행할 수 없어서 한번씩 번갈아가며 학습하기 위함입니다.

for문의 마지막에 iterations+=1을 통해 1씩 증가시키면서 번갈아 학습한다는 것을 알 수 있습니다. 



if np.mod(iterations, step) == 0:


>>>>> iterations과 step이 같을 때


step은 200으로, 이렇게 번갈아 진행하다가 iterations가 200이 되었을 때마다 실행되는 조건문입니다.

코드를 보면 model.py의 generated_sample의 값을 가져오는 부분 같습니다. (visualization으로 데이터를 시각화)




즉 다시 정리하면, 50000개의 데이터에서 batch_size = 128개씩 쪼개 학습하면서 generator와 discriminator의 loss를 구해 출력하는 반복문이 진행되는 것 입니다. 이는 epoch이 200번 모두 돌아갈 때까지 진행될 것이며, 반복문이 진행되는 동안 iterations가 200이 될 때 마다 generated_sample 함수를 통해 visualization으로 데이터를 시각화하는 과정이 이루어집니다.



지금까지 model이 train.py에서 학습이 이루어지는 과정에 대해서 알아봤습니다.


실습을 통한 구현을 할 때는, 사용자가 필요한 model을 직접 만들고 이 train.py를 적용한다면 DCGAN으로 학습한 결과를 얻어낼 수 있습니다. 

반응형