소스코드 원본 링크 : https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html
Implementation
입력 매개변수 집합과 데이터 집합이 준비되면 구현에 들어갈 수 있다. 먼저 가중치(weight) 초기화부터 생성자(Generator), 판별자(Discriminator), 손실 함수 및 학습 루프에 대해 살펴보자.
Weight Initialization
모든 모델은 가중치가 mean = 0, stdev = 0.2 인 정규 분포로부터 무작위로 초기화되도록 지정한다.
weight_init : 초기화 된 모델을 입력으로 받아 모든 컨볼루션, 컨볼루션-트랜스포스, 배치 정규화 레이어를 기준이 충족되도록 다시 초기화한다. 이 기능은 초기화 직후 모델에 적용된다.
# custom weights initialization called on netG and netD
def weights_init(m):
classname = m.__class__.__name__
if classname.find('Conv') != -1:
nn.init.normal_(m.weight.data, 0.0, 0.02)
elif classname.find('BatchNorm') != -1:
nn.init.normal_(m.weight.data, 1.0, 0.02)
nn.init.constant_(m.bias.data, 0)
Generator
우선 참고자료에 의하면 z는 uniform distribution 혹은 normal distribution 에서의 임의의 값이다.
생성기 G는 잠재 공간 벡터 z를 데이터 공간에 매핑하도록 설계되었다. 데이터가 이미지이므로 z를 데이터 공간으로 변환한다는 것은 궁극적으로 학습 이미지와 동일한 크기 (3x64x64) 의 RGB 이미지를 생성한다는 것을 의미한다.
이 과정은 strided 2d 컨볼루션-트랜스포스 레이어, 2d 배치 norm 레이어 그리고 relu activation 함수를 통해 이루어진다. 생성자의 출력은 tanh 함수를 통해 입력 데이터 범위 [-1,1]로 반환된다.
nz : z의 입력 벡터의 길이 100
ngf: 생성자를 통해 전달되는 feature map의 사이즈 64
nc : 출력 이미지의 채널 수 3
나중에 인스턴스를 print 해보면 ConvTranspose2d 에서 첫번째 인자는 입력 사이즈, 두번째는 출력 사이즈, 세번째는 커널 사이즈, 네번째는 스트라이드 사이즈, 다섯번째는 padding 값임을 확인할 수 있다.
# Generator Code
class Generator(nn.Module):
def __init__(self, ngpu):
super(Generator, self).__init__()
self.ngpu = ngpu
self.main = nn.Sequential(
# input is Z, going into a convolution
nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
# state size. (ngf*8) x 4 x 4
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
# state size. (ngf*4) x 8 x 8
nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
# state size. (ngf*2) x 16 x 16
nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(True),
# state size. (ngf) x 32 x 32
nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
nn.Tanh()
# state size. (nc) x 64 x 64
)
def forward(self, input):
return self.main(input)
이제 생성자를 인스턴스화하고 weights_init 함수를 적용할 수 있다.
# Create the generator
netG = Generator(ngpu).to(device)
# Handle multi-gpu if desired
if (device.type == 'cuda') and (ngpu > 1):
netG = nn.DataParallel(netG, list(range(ngpu)))
# Apply the weights_init function to randomly initialize all weights
# to mean=0, stdev=0.2.
netG.apply(weights_init)
# Print the model
print(netG)
Discriminator
판별자 D는 이미지를 입력으로 받고 이미지가 진짜였을 스칼라 확률을 출력하는 binary classification 이다. D는 3x64x64 입력 이미지 사용하여 Conv2d, BatchNorm2d, LeakyReLu 레이어를 처리하고 Sigmoid activation 함수를 사용하여 최종 확률을 출력한다. 이러한 아키텍처는 더 많은 레이어로 확장될 수 있지만 우리가 사용했던 레이어는 고수하는것이 좋다. 그 이유에 대해서는 이 링크를 확인해보는 것이 좋을것 같다.
class Discriminator(nn.Module):
def __init__(self, ngpu):
super(Discriminator, self).__init__()
self.ngpu = ngpu
self.main = nn.Sequential(
# input is (nc) x 64 x 64
nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# state size. (ndf) x 32 x 32
nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 2),
nn.LeakyReLU(0.2, inplace=True),
# state size. (ndf*2) x 16 x 16
nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 4),
nn.LeakyReLU(0.2, inplace=True),
# state size. (ndf*4) x 8 x 8
nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 8),
nn.LeakyReLU(0.2, inplace=True),
# state size. (ndf*8) x 4 x 4
nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, input):
return self.main(input)
판별자를 만들고 weights_init 함수를 적용하고 모델의 구조를 확인해보자.
# Create the Discriminator
netD = Discriminator(ngpu).to(device)
# Handle multi-gpu if desired
if (device.type == 'cuda') and (ngpu > 1):
netD = nn.DataParallel(netD, list(range(ngpu)))
# Apply the weights_init function to randomly initialize all weights
# to mean=0, stdev=0.2.
netD.apply(weights_init)
# Print the model
print(netD)
Loss Functions and Optimizers
우리는 Binary Cross Entropy loss function (BCELoss)를 사용할 것이고 다음과 같이 정의되어 있다.
‘진짜’ 레이블을 1로, ‘가짜’ 레이블을 0으로 정의한다. 이러한 레이블은 D와 G의 손실을 계산할 때 사용된다. 그리고 D와 G의 각각의 개별적인 optimizer를 설정한다. 둘 다 Adam optimizer를 사용하고 학습률 = 0.0002, Beta1 = 0.5로 설정한다. 학습 진행을 추적하기 위해 , 가우스 분포(i.e. fixed_noise) 로부터 도출된 잠재 벡터의 fixed batch를 생성할 것이다. 학습 루프에서 주기적으로 이 fixed_noise를 G에 입력할 것이고, 반복을 통해 노이즈로부터 이미지가 형성되는 것을 볼 수 있다.
# Initialize BCELoss function
criterion = nn.BCELoss()
# Create batch of latent vectors that we will use to visualize
# the progression of the generator
fixed_noise = torch.randn(64, nz, 1, 1, device=device)
# Establish convention for real and fake labels during training
real_label = 1
fake_label = 0
# Setup Adam optimizers for both G and D
optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999))