Joonas' Note
[딥러닝 일지] PyTorch로 DCGAN 훈련해보기 본문
- DCGAN
- Tutorial
- 미리보는 결과
- 튜토리얼 코드 보기
- 판별자(Discriminator)
- 생성자(Generator)
- 학습 코드 (판별자)
- 학습 코드 (생성자)
- 학습 결과
- Vector Arithmetic
- Walking in the latent space
- 노트북
이전 글 - [딥러닝 일지] VAE; Variational Auto Encoder
DCGAN
Deep Convolutional Generative Adversarial Network에 대한 내용은 인터넷에 충분하게 많이 있으니 생략한다.
아래 링크가 가장 읽기 좋았다.
- https://jaejunyoo.blogspot.com/2017/02/deep-convolutional-gan-dcgan-2.html
- https://memesoo99.tistory.com/32
논문 링크: https://arxiv.org/pdf/1511.06434.pdf
Tutorial
파이토치 공식 문서의 튜토리얼에 DCGAN에 대한 코드와 설명이 이미 잘 되어있다.
아래는 한국어 문서 링크
미리보는 결과
튜토리얼에서는 5 epoch만 돌지만, 배치 크기를 키우고 10 epoch 까지 학습해보았다.
10 epoch 를 학습하는데에 40분 정도 걸렸다.
학습한 시간에 비해 몇몇 얼굴 이미지들은 아주 퀄리티가 높게 생성되었다. 놀라운 결과다.
튜토리얼 코드 보기
판별자(Discriminator)
(3, 64, 64)로 주어진 이미지가 진짜인지 가짜인지를 판별한다.
진짜일수록 1에 가깝고, 가짜일수록 0에 가까운 정도를 나타내기 때문에 1차원 벡터의 출력을 갖는다.
__________________________________________________________________________________________
Layer Type Output Shape Param #
==========================================================================================
discriminator Discriminator (-1, 1, 1, 1) 0
├─main Sequential (-1, 1, 1, 1) 0
| └─0 Conv2d (-1, 64, 32, 32) 3,072
| └─1 LeakyReLU (-1, 64, 32, 32) 0
| └─2 Conv2d (-1, 128, 16, 16) 131,072
| └─3 BatchNorm2d (-1, 128, 16, 16) 513
| └─4 LeakyReLU (-1, 128, 16, 16) 0
| └─5 Conv2d (-1, 256, 8, 8) 524,288
| └─6 BatchNorm2d (-1, 256, 8, 8) 1,025
| └─7 LeakyReLU (-1, 256, 8, 8) 0
| └─8 Conv2d (-1, 512, 4, 4) 2,097,152
| └─9 BatchNorm2d (-1, 512, 4, 4) 2,049
| └─10 LeakyReLU (-1, 512, 4, 4) 0
| └─11 Conv2d (-1, 1, 1, 1) 8,192
| └─12 Sigmoid (-1, 1, 1, 1) 0
==========================================================================================
Trainable params: 2,765,568
Non-trainable params: 0
Total params: 2,765,568
__________________________________________________________________________________________
DCGAN에서는 안정적인 학습을 위해서 중간에 배치 정규화층(BatchNorm2d)을 쌓았다.
논문에서는, 모든 층에 배치 정규화를 적용하면 학습이 불안정해진다고 하면서(sample oscillation and model instability), 생성자의 출력 레이어와 판별자의 입력 레이어에는 적용하지 않았다.
생성자(Generator)
잠재 벡터로부터 이미지를 만든다. VAE(Variational AutoEncoder)에서 Decoder와 구조적으로는 다른 점이 없다.
잠재 벡터 z는 노이즈와 용어가 섞여서 사용되는 것으로 보인다.
생성자 네트워크의 출력은 입력 이미지와 동일하게, 3채널의 (64, 64) 이미지이다.
학습 코드 (판별자)
판별자의 목표는 주어진 이미지가 진짜인지 가짜인지 정확하게 맞추는 것이고, 생성자의 목표는 판별자를 혼란스럽게 하는 것이다.
다시 말하면, 생성자는 판별자가 0인지 1인지 완전히 헷갈려서 50%로 맞추게 되는 상황, 즉, 출력이 0.5 에 수렴하는 것이 최적이다.
판별자와 생성자 두 모델이 경쟁하는 구조이다보니, 학습이 무척 불안정하기로 유명하다.
(이후에 안정적인 학습과 관련한 많은 논문들이 쏟아졌다.)
criterion = nn.BCELoss()
real_label = 1.
fake_label = 0.
판별자가 진짜/가짜 이미지를 분류하는 작업만 하므로, 비교 손실 함수로 Binary Cross Entropy를 사용한다.
netD.zero_grad()
real_imgs = data[0].to(device) # data = 1 batch = (images, labels)
b_size = real_imgs.size(0)
label = torch.full((b_size,), real_label, dtype=torch.float, device=device)
output = netD(real_imgs).view(-1)
errD_real = criterion(output, label)
errD_real.backward()
noise = torch.randn(b_size, nz, 1, 1, device=device)
fake_imgs = netG(noise)
label.fill_(fake_label)
output = netD(fake_imgs.detach()).view(-1)
errD_fake = criterion(output, label)
errD_fake.backward()
optimizerD.step()
각 네트워크를 학습하기 전에 가중치가 업데이트 되지 않도록 초기화를 잘 해주어야한다.
판별자는 logD(x)+log(1−D(G(z))) 가 최대가 되도록 업데이트 된다. x 는 진짜 이미지고, z 는 가짜 이미지다.
코드에서 x 는 real_imgs, z 는 noise이다.
위 수식을 분리해서, (1) 진짜 이미지를 넣고 레이블을 모두 1로 주어서 손실을 계산하게 만들어서 업데이트 하고, (2) 가짜 이미지를 넣고 레이블을 모두 0으로 주어서 손실을 계산하면 된다.
학습 코드 (생성자)
생성자는 log(D(G(z))) 가 최대가 되도록 업데이트한다. 즉, 생성자가 만든 가짜 이미지 G(z) 를 판별자에게 주었을 때, 1에 가까워야 한다는 의미이다.
netG.zero_grad()
label.fill_(real_label)
output = netD(fake_imgs).view(-1)
errG = criterion(output, label)
errG.backward()
optimizerG.step()
판별자를 통해 계산한 손실만큼, 생성자는 가중치를 업데이트 한다.
학습 결과
생성자(G)는 손실이 점점 줄어들고 있고, 잘 안 보이지만 확대해보면 판별자(D)는 0에서 0.5로 가고 있다.
결과는 글의 상단에서 확인한 GIF 이미지들.
Vector Arithmetic
논문에서는 DCGAN에서도 벡터 산술연산이 가능하다는 것을 보여주고 있다.
smiling woman - neutral woman + neutral man = smiling man
아래는 직접 실행해 본 결과이다.
결과 이미지를 보고 노이즈들의 평균 벡터를 얻을 수 있고, 이것들이 어떤 특징을 가지고 있다는 것을 잘 보여준다.
Walking in the latent space
가장 해보고 싶었던 것이 있다. 두 이미지 사이를 서서히 변화시키는 것이다.
퀄리티가 그렇게 좋지는 않지만 성공했다.
G를 Generator 모델, D를 Discriminator 모델이라고 하자. DCGAN의 구조는 랜덤한 노이즈로부터 이미지를 만들 수 있다.
랜덤하게 만든 노이즈 Na와 노이즈 Nb가 있다면, 두 노이즈 사이를 선형 보간(linear interpolation)해서 서서히 변화하는 노이즈를 여러 개 얻을 수 있다.
코드는 간단하다.
def interpolate_points(p1, p2, n_steps=10):
ratios = np.linspace(0, 1, num=n_steps)
vs = [(1.0 - r) * p1 + r * p2 for r in ratios]
return torch.stack(vs)
n1 = torch.randn(5, nz, 1, 1, device=device)
n2 = torch.randn(5, nz, 1, 1, device=device)
fake = []
for i in range(5):
point = interpolate_points(n1[i], n2[i]).to(device)
fake.append(netG(point))
fake = torch.vstack(fake).cpu()
그럼 이제 내가 원하는 특징을 없애거나 추가하는 이미지를 만들고 싶고, 그것을 위와 동일한 방식으로 출력하고 싶다.
"웃는 얼굴"을 만드는 노이즈를 어떻게 알 수 있을까?
반대로 생각해보자. 웃는 얼굴을 만들었던 노이즈를 전부 모아보면 하나의 점으로 모일 것이다.
VAE에서처럼 어떤 확률분포를 추정하는 모델이기 때문에.
물론 웃는 얼굴을 만드는 노이즈를 전부 모았다고 웃는 얼굴의 특징만 있지는 않을 것이다.
모은 노이즈에 갈색 머리가 많다면 갈색 머리의 특징도 함께 포함하고 있을 것이다.
# Average points
avg_smiling = average_points(noise, f_smiling)
avg_neutral = average_points(noise, f_neutral)
avg_blonde = average_points(noise, f_blonde)
avg_dark = average_points(noise, f_dark)
avg_imgs = torch.from_numpy(np.stack([avg_smiling, avg_neutral, avg_blonde, avg_dark])).to(device)
웃는 얼굴을 만드는 노이즈와, 아무 표정이 없는 얼굴을 만드는 노이즈를 보간해서 이미지를 만들고 출력해보자.
smile_to_neutral = interpolate_points(torch.from_numpy(avg_smiling), torch.from_numpy(avg_neutral)).to(device)
generated_images = netG(smile_to_neutral)
plt.figure(figsize=(12, 2))
plt.axis('off')
plt.title('Smiling to Neutral')
plt.imshow(np.transpose(vutils.make_grid(generated_images, nrow=10, padding=2, normalize=True).cpu(), (1,2,0)))
plt.show()
코드를 실행한 결과는 이미 위에 첨부된 이미지이다.
같은 방법으로 금발 이미지에서 흑발 이미지로 interpolate한 결과도 확인할 수 있다.
노트북
https://github.com/joonas-yoon/practice-on-dl/blob/main/03_DCGAN/DCGAN.ipynb
'AI > 딥러닝' 카테고리의 다른 글
[딥러닝 일지] WGAN-GP (Gradient Penalty) (0) | 2022.06.12 |
---|---|
[딥러닝 일지] WGAN (Wasserstein GAN) (0) | 2022.06.11 |
[딥러닝 일지] VAE; Variational Auto Encoder (0) | 2022.06.05 |
[딥러닝 일지] Auto Encoder (with MNIST) (0) | 2022.06.03 |
[PyTorch] RuntimeError: DataLoader worker is killed by signal: Bus error. (0) | 2022.05.30 |