cs231n

cs231n Assignment 3: Q3 (GAN 구현)

츄츄츄츄츄츄츄 2023. 2. 27. 14:46

내 풀이 링크:https://github.com/lionkingchuchu/cs231n.git

 

GitHub - lionkingchuchu/cs231n: cs231n Spring 2022 Assignment

cs231n Spring 2022 Assignment. Contribute to lionkingchuchu/cs231n development by creating an account on GitHub.

github.com

이번 과제는 GAN 신경망 Genarative Adversarial Network 를 구현하는 것이다. GAN이란 지금 까지 우리가 다룬 classifier의 역할을 하는 모델이 아닌 새로운 데이터를 만드는 Generator를 만드는 네트워크를 만드는 것이다. 논문 [1406.2661] Generative Adversarial Networks (arxiv.org) 를 보면 GAN에 대한 더 자세한 설명을 볼 수 있다.

 

GAN의 간단한 작동 원리는 모델의 Generator과 Discriminator이 있는데, Generator는 새로운 데이터를 생성하고 Discriminator는 x (input data)와 Generator가 생성한 새로운 데이터를 같이 보면서 어떤것이 실제 데이터이고 어떤것이 Generator가 생성한 데이터인지 판별한다. 모델을 계속해서 train시키면 generaator는 discriminator를 속이기 위해 최대한 원본과 비슷한 새로운 데이터를 만들려고 노력할 것이고, discriminator는 생성된 데이터와 실제 데이터를 비교하며 최대한 어떤 것이 실제 데이터인지 판단하는 안목이 더 좋아질 것이다.

 

위 과정을 train하면서 계속해서 반복하면 숙달된 안목을 가진 discriminator를 속이기 위해 generator는 결국 실제와 정말로 비슷한 새로운 data를 생성할 능력이 생길 것이다. Generator가 충분히 train되었다면 이제 generator를 분리하여 새로운 데이터를 출력하는 모델을 만들 수 있게 될 것이다.

 

과제에서도 친절하게 GAN에 대한 설명을 해주고, 어떤 원리와 수식을 사용해서 Generator와 Discriminator를 최적화 시킬지에 대해 알려준다. GAN의 parameter 최적화와 hyperparmeter 최적화 하는것이 굉장히 어렵고, epoch 시간도 매우 오래걸리는 편이므로 이 과제에서는 비교적 분류가 쉬운 MNIST data를 사용한다. MNIST data를 사용한 GAN을 만들기 때문에 MNIST data (사진 데이터)를 로드하고 사진 데이터를 기준으로 generator와 discriminator를 구현한다.

G()함수는 generator를 통과하는 함수이고 D()함수는 discriminator를 통과하는 함수이다. x는 기존 MNIST사진 데이터들이다. 우리는 random noise 사진 (z) 를 통해서 MNIST의 숫자 사진을 만들어 내는 것이 목적이다.

 

먼저 D(x)는 MNIST 사진 데이터들을 discriminator가 판단한 결과이다. 해당 결과가 1일수록 실제 데이터에 가까운 점수이고, 0일수록 가짜 데이터에 가까운 점수이다. D(x)는 실제 사진이므로 log D(x)를 maximize 해주어야 실제 사진을 실제라고 더 판단을 잘하게 될 것이다. G(z)는 random noise로 만든 generator의 결과물을 뜻하고, D(G(z))는 이 결과물을 discriminator이 판단한 결과를 뜻한다. discriminator입장에서 D(G(z))는 가짜 데이터라고 판별해야 하므로 log(1 - D(G(z)) 를 maximize 해주어야 가짜 사진이라고 판단을 더 잘하게 될 것이다. 반대로 generator 입장에서는 log D(G(z))를 maximize 해주어야 실제 데이터에 가까운 데이터를 generate 할 수 있게 될 것이다.

 

먼저 z를 만들기 위한 random noise 구현이다.

torch.rand를 사용해 uniform한 (-1,1) 사이의 noise를 만들어야 하므로 2를 곱해주고 1을 빼준다.

 

다음은 discriminator layer 구현이다. 다음과 같은 구조의 layer를 구현하라고 한다. 

  • Fully connected layer with input size 784 and output size 256
  • LeakyReLU with alpha 0.01
  • Fully connected layer with input_size 256 and output size 256
  • LeakyReLU with alpha 0.01
  • Fully connected layer with input size 256 and output size 1

nn.Sequential을 통해 만든 layer들을 이어준다. 각 layer의 weights을 xavier initialization으로 구현하기 위해 initialize_weights 함수를 사용하라고 해서 각 layer의 weight initialize도 해주었다. LeakyReLU는 alpha 0.01 을 사용하라고 하였는데 pytorch 기본값 alpha가 0.01이라 굳이 안넣어도 됐다. 다음은 다음 구조를 가진 Generator 구현이다.

  • Fully connected layer from noise_dim to 1024
  • ReLU
  • Fully connected layer with size 1024
  • ReLU
  • Fully connected layer with size 784
  • TanH (to clip the image to be in the range of [-1,1])

discriminator과 같은 방법으로 nn.Sequential을 사용해 구현해주었다. 다음은 GAN에서 각 generator, discrimnator loss를 어떻게 구현할지 설명과 구현이다.

각각 generator loss, discriminator loss는 아까 minmax 설명하는 함수에 음수를 취해서 얻을 수 있다. 음수를 취하는 이유는 먼저 generator loss의 안의 logD(G(z)) 항은 generator로 만든 G(z)의 discriminator 가 판별한 (D(G(z))) 에 log를 취한 값이다. 만약 discriminator가 real image로 완벽히 착각하여 D(G(z)) = 1 이면 주면 log(1)이 되어 0, 즉 loss가 0이 된다. 이는 generator가 완벽하게 잘 속인 것이므로 loss가 0 인 최적 generator라고 생각할 수 있다. 반면에 discriminator가 fake image라고 완벽히 판별하여 D(G(z)) = 0 으로 log(0) 이되어 -∞ 이 된다. loss는 음수가 될 수 없으니 양수를 취해주기 위해 -를 붙여주면, loss가 +∞ 가 되는데, generator가 사진을 정말 못만든 최악의 generator 상태라고 볼 수 있다. 그러므로 generator가 real image와 가깝게 만들수록 loss는 0, fake image와 가깝게 만들수록 loss는 +∞로 발산하는 generator loss 식을 세울 수 있다. Discriminator loss도 같은 원리로 D(x)는 1에 가까워야 하고 D(G(z))는 0에 가까워야 하므로 두 식을 log취해주어 loss 식을 세울 수 있다.

 

그러면 이제 실제 loss를 구해보자. 위의 수식 bce(s,y)를 통해 s는 discriminator의 점수, y는 실제 값 (1: real image, 0: fake image) 일때의 binary cross entropy loss를 구할 수 있다. 우리가 직접 구현하면 아마도 log를 사용하다 보니 음의 무한대와 같은 경우에서 numerically unstable 하기 때문에 Pytorch에서 구현된 nn.BCEWithLogitsLoss를 사용해 binary cross entropy error를 구한다. 

discriminator loss 구현

discriminator loss의 경우 logits_real은 real image 부분 ( E[log(D(x))] ) 이므로 torch.ones를 target으로 주어야 하고, logits_fake는 fake image 부분 ( E[log(1-D(G(z)))] ) 이므로 torch.zeros를 target으로 주어야 한다. dtype을 float로 잘 맞추어 주어야 한다.

generator loss 구현

generator loss의 경우 real image가 target이므로 torch.ones로 target을 주어야 한다.

 

다음은 optimizer인데 Adam을 사용하고 lr, beta1, beta2 는 각각 1e-3, 0.5, 0.999를 사용하라 해서 시키는 대로 구현했다.

optimizer 구현

그러면 이제 run_a_gan으로 train을 실행해 보자.

run_a_gan 함수

run_a_gan()함수는 주요 입력으로 (D, G, D_solver, G_solver, discriminator loss, generator loss) 들을 받는다. D, G는 각각 우리가 위에서 생성한 discriminator, generator layer이고, D_solver, G_solve는 각각 D와 G를 train하기 위한 optimizer (아까의 경우 Adam) 이고, discriminator loss, generator loss는 위에서 구현한 loss를 구하는 함수이다. 

 

과정을 설명하면 loader_train에서 real_image data를 가져오고 Discriminator로 logits_real을 얻는다. g_fake_seed의 noise 이미지를 Generator를 통과시켜 fake_image를 생성한다. fake_image를 Discriminator를 통과시켜 logits_fake 점수를 얻고, 두개로 discriminator_loss를 얻어 discriminator를 업데이트 한다. 다음으로 generator로 다시 fake_image를 만들고, gen_logits_fake로 점수를 얻고 generator_loss로 loss를 얻어 업데이트 한다. D loss와 G loss가 잘 줄어드는 것을 볼 수 있다. 결과물도 꽤 숫자와 비슷한 것을 볼 수 있다.

.D loss, G loss
3750 iters 결과물

다음은 좀더 최신의 Least Squares GAN을 구현해보자. Generator loss, Discriminator loss가 조금 바뀐 것을 알 수 있다.

Least Squares GAN

아까는 log를 사용한 binary cross entropy error를 사용해 loss를 얻었지만, 이번에는 값에 1을 빼거나 제곱하고 0.5를 곱해서 구현한다. 아까처럼 0과 1을 대입했을때 결과는 똑같겠지만 gradient가 log함수와 달라져 업데이트 하는데 약간 달라질 것이다.

Discriminator Least Square Loss
Generator Least Square Loss

이제 바뀐 loss함수로 train 해보자. run_a_gan의 결과이다. 마찬가지로 loss가 D, G 모두 꽤 잘 줄어들었고, 결과물도 숫자의 형태를 잘 띄는 것 같다.

.D loss, G loss
3750 iters 결과물

다음은 Discriminator, Generator layer를 아래와 같은 형태의 좀 더 deep한 CNN을 사용해서 만든 것이다. 이전의 GAN은 FCNet을 사용해 spatial data를 포함하지 못했지만 CNN을 사용하여 spatial data를 포함할 수 있다. nn.Sequential로 이어주어 구현한다.

Discriminator:

  • Conv2D: 32 Filters, 5x5, Stride 1
  • Leaky ReLU(alpha=0.01)
  • Max Pool 2x2, Stride 2
  • Conv2D: 64 Filters, 5x5, Stride 1
  • Leaky ReLU(alpha=0.01)
  • Max Pool 2x2, Stride 2
  • Flatten
  • Fully Connected with output size 4 x 4 x 64
  • Leaky ReLU(alpha=0.01)
  • Fully Connected with output size 1

Deep CNN Discriminator

Generator:

  • Fully connected with output size 1024
  • ReLU
  • BatchNorm
  • Fully connected with output size 7 x 7 x 128
  • ReLU
  • BatchNorm
  • Use Unflatten() to reshape into Image Tensor of shape 7, 7, 128
  • ConvTranspose2d: 64 filters of 4x4, stride 2, 'same' padding (use padding=1)
  • ReLU
  • BatchNorm
  • ConvTranspose2d: 1 filter of 4x4, stride 2, 'same' padding (use padding=1)
  • TanH
  • Should have a 28x28x1 image, reshape back into 784 vector (using Flatten())

Deep CNN Generator

그럼 이제 구현한 Deep CNN을 이용한 Discriminator, Generator를 사용해 train 해보자. loss로는 맨 처음에 구현한 BCE를 사용한 loss를 사용했다. 최적화된 D, G layer를 사용해서 iter이 1750밖에 되지 않아도 아까보다 깔끔한 이미지를 만든 것을 볼 수 있다.

D loss, G loss
1750 iters 결과물

문제4.  f(x,y) = xy일때 minx maxy f(x,y)의 결과를 알아보는 문제이다.

Inline Question 4

We will look at an example to see why alternating minimization of the same objective (like in a GAN) can be tricky business.

Consider f(x,y)=xy. What does minxmaxyf(x,y) evaluate to? (Hint: minmax tries to minimize the maximum value achievable.)

Now try to evaluate this function numerically for 6 steps, starting at the point (1,1), by using alternating gradient (first updating y, then updating x using that updated y) with step size 1. Here step size is the learning_rate, and steps will be learning_rate * gradient. You'll find that writing out the update step in terms of xt,yt,xt+1,yt+1 will be useful.

Breifly explain what minxmaxyf(x,y) evaluates to and record the six pairs of explicit values for (xt,yt) in the table below.

Your answer:

y0 y1 y2 y3 y4 y5 y6
1 2 1 -1 -2 -1 1
x0 x1 x2 x3 x4 x5 x6
1 -1 -2 -1 1 2 1

먼저 f(x,y) = xy를 각각 x, y에대해 편미분하면

dfdy = x, dfdx = y 가 나온다.

여기서 minmax를 할때 y를 업데이트 하고, 업데이트 된 y를 사용해 x를 업데이트 한다고 한다.

먼저 처음에 y0일때에는 y의 gradient는 x0 = 1이 되므로 y1 = y0 + x0 = 2가 될 것이다. 다음으로 x0일때 x의 gradient는 y1 = 2 이므로 x1 = x0 - y1 = -1이 될 것이다. 위 과정을 계속 반복하면 y6 = 1, x6 = 1 로 결국 처음으로 돌아오는 결과가 생길 것이다.

 

문제5. 위 방법으로 최적의 값에 도달할수 있는가? 이다

Inline Question 5

Using this method, will we ever reach the optimal value? Why or why not?

Your answer:

it will never reach the optimal value since y6 = y0, and x6 = x0. It will go on a cycle.

 

당연히 y6 = y0 = 1, x6 = x0 = 1인 값으로 돌아왔으므로 여기서 계속 업데이트를 반복해도 사이클을 돌며 최적의 값에 도달하지 못할 것이다.

 

문제6. train하는데 초반에 만약 generator loss가 줄어드는 반면 discriminator loss가 큰값을 계속 유지한다면 이건 좋은 현상인지 묻는 문제이다.

Inline Question 6

If the generator loss decreases during training while the discriminator loss stays at a constant high value from the start, is this a good sign? Why or why not? A qualitative answer is sufficient.

Your answer:

No because high discriminator loss means it cannot distinguish between real image and fake image very well. It might be the case when although generator some strange image that overfits some class, discriminator cannot help generator make a image that looks like real image.

 

discriminator loss가 높다는 뜻은 discriminator가 실제 이미지인지 가짜 이미지인지 판별을 못한다는 뜻이다. 만약 generator가 실제와 가깝지 않은 이미지를 만들어도 discriminator는 실제라고 생각할 수 있다. 문제와 같은 상황은, generator가 특정 class에 overfit한 이미지를 만들어도, discriminator는 판단할 수 없다는 뜻이다. 저번에 fooling image나 class visualization을 하면서 특정 class에 overfit한 이미지를 만들면 얼마나 이상한 현실과 가깝지 않은 이미지가 나오는 것을 우리는 봤지 않은가? 만약 discriminator가 이를 현실 이미지가 아니라고 판단하지 못한다면 결과는 우리 눈에는 이상한 이미지를 generator가 계속해서 만들 것이다.