본문 바로가기
Deep Learning/GAN

[GAN-②] DCGAN

by 룰루셩 2021. 12. 16.

DCGAN

지난번에 이어서 여러 GAN 중 DCGAN에 대해 정리해보려고 한다. ETRI 수업 때 파이토치 코드를 같이 뜯어보면서 설명해주셨었는데 그 부분도 같이 정리해두고자 한다.

 

↓↓↓GAN 기본 컨셉에 대한 내용은 이전 포스팅!


 

DCGAN?

Deep Convolutional Generative Adversarial Networks

고화질의 이미지를 생성한 최초의 GAN 모델이다. GAN이 처음 나왔을 때는 성능이 그리 좋지 않아서 이미지가 흐렸다.

Convolution을 사용한 GAN 모델이 제안되고 나서는 화질이 개선되었고, 간단하면서도 잘 작동해서 지금까지도 많이 쓰인다고 한다.

 

CNN을 사용해서 Discriminator를 구현하고, deconvolutional network를 통해 Generator를 만든 모델이다. 

(원래 CNN은 차원을 줄인다. deconvolution은 feature map의 크기를 증가시키는 방식으로 동작한다.)

 

- D(판별모델): convolution을 사용한다.

                        Leaky ReLU

                        Stride convolution으로 서브 샘플링

- G(생성모델): deconvolution(transpose convolution)

                        ReLU

                        pooling layer를 사용하지 않는다. (unpooling할 때 blocky한 이미지가 생성되기 때문에)

                                 unpooling은 pooling을 다시 되돌리는거라고 이해했다. → deconvolution 설명된 블로그!

                      대신 stride 2 이상을 사용해서 feature map 크기 늘려간다.

                        BatchNormalization 사용 (학습 안정화 시켜주기 위하여)

                        Adam 옵티마이져 사용

 

               * DCGAN tutorial 보충 설명

                 transpose convolution은 latent space vector로 하여금 이미지와 같은 차원을 갖도록 변환시켜주는 역할을 한다.

                 transpose convolution은 convolution의 반대적인 개념이라 이해하기.

                 입력된 작은 CHW 데이터를 가중치들을 이용해 더 큰 CHW로 업샘플링해주는 계층이다. 

 

 

잠재 공간(latent vector)에서의 산술연산 가능

잠재 공간에서 산술연산을 했을 때 그 결과가 이미지 공간에 반영된다.

많이들 예로 드는 것이 안경 낀 남자 - 안경 안 낀 남자 + 안경 안 낀 여자 = 안경 낀 여자 이렇게 안경 낀 남자 이미지에서 안경 안 낀 남자 이미지를 빼고, 안경 안 낀 여자 이미지를 적용해주면 안경 낀 여자만 나오겠지? 하는 실험을 진행했는데 이게 잘 된다는 것이다. 

출처: UNSUPERVISED REPRESENTATION LEARNING WITH DEEP CONVOLUTIONAL GENERATIVE ADVERSARIAL NETWORKS  

위의 예로 보면, 산술연산에서 사용한 latent vector라는 것은

모델에서 생성된 이미지 중 안경 낀 남자, 안경 안낀 남자, 안경 안낀 여자 그룹을 만들고

각 그룹에서 latent vector의 평균을 구한 값

을 의미한다.

그래서 안경 낀 남자의 z - 안경 안낀 남자의 z + 안경 안 낀 여자의 z 로 계산된 z를 Generator에 넣으면 안경 낀 여자 이미지가 나오는게 가능해졌다. 여기서 z는 latent vector를 의미한다.

 

 

pytorch 코드로 DCGAN 이해하기

(pytorch 튜토리얼에 있는 내용이다.)

 

생성자 G

latent vector z를 데이터 공간으로 변환시키도록 설계

= 학습 이미지와 같은 사이즈를 가진 RGB 이미지를 생성하는 것 (ex. 3x64x64)

 

stride 2를 가진 transpose convolution을 이어서 구성

각 transpose convolution 계층 하나당 {transpose convolution + BN + relu} 이렇게 한쌍으로 묶어서 사용한다.

생성자 마지막 출력 계층에서는 데이터를 tanh 함수에 통과시킨다 → 출력값을 [-1, 1] 사이의 범위로 조정하기 위해서

출처: DCGAN tutorial

ConvTranspose2d 할 때, 어떻게 convolution을 해야 전에꺼가 나올까 라고 생각하며 코드 짜면 쉽다고 한다.. 

# 잠재공간 벡터의 크기 (예. 생성자의 입력값 크기)
nz = 100

# 생성자를 통과하는 특징 데이터들의 채널 크기
ngf = 64

# 생성자 코드

class Generator(nn.Module):
    def __init__(self, ngpu):
        super(Generator, self).__init__()
        self.ngpu = ngpu
        self.main = nn.Sequential(
            # 입력데이터 Z가 가장 처음 통과하는 전치 합성곱 계층입니다.
            nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False),
             # kernel_size = 4, stride = 1, output = 0
            nn.BatchNorm2d(ngf * 8),
            nn.ReLU(True),
            # 위의 계층을 통과한 데이터의 크기. (ngf*8) x 4 x 4
            nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(True),
            # 위의 계층을 통과한 데이터의 크기. (ngf*4) x 8 x 8
            nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(True),
            # 위의 계층을 통과한 데이터의 크기. (ngf*2) x 16 x 16
            nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(True),
            # 위의 계층을 통과한 데이터의 크기. (ngf) x 32 x 32
            nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
            nn.Tanh()
            # 위의 계층을 통과한 데이터의 크기. (nc) x 64 x 64
        )

    def forward(self, input):
        return self.main(input)

위의 생성자 G의 구조는 아래와 같다.

Generator(
  (main): Sequential(
    (0): ConvTranspose2d(100, 512, kernel_size=(4, 4), stride=(1, 1), bias=False)
    (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU(inplace=True)
    (3): ConvTranspose2d(512, 256, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (4): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (5): ReLU(inplace=True)
    (6): ConvTranspose2d(256, 128, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (7): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (8): ReLU(inplace=True)
    (9): ConvTranspose2d(128, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (10): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (11): ReLU(inplace=True)
    (12): ConvTranspose2d(64, 3, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (13): Tanh()
  )
)

 

 

구분자 D

구분자 D 는 3x64x64 이미지를 입력받아, Conv2d, BatchNorm2d, 그리고 LeakyReLU 계층을 통과시켜 데이터를 가공시키고, 마지막 출력에서 Sigmoid 함수를 이용하여 0~1 사이의 확률값으로 조정한다.

Stride = 2 사용한 이유: 신경망 내에서 스스로 풀링 함수를 학습하기 때문에, 데이터를 처리하는 과정에서 직접적으로 풀링 계층 (MaxPool, AvgPooling)사용하는 것보다 유리하다고 한다.

 

# 구분자 코드

class Discriminator(nn.Module):
    def __init__(self, ngpu):
        super(Discriminator, self).__init__()
        self.ngpu = ngpu
        self.main = nn.Sequential(
            # 입력 데이터의 크기는 (nc) x 64 x 64 입니다
            nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # 위의 계층을 통과한 데이터의 크기. (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),
            # 위의 계층을 통과한 데이터의 크기. (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),
            # 위의 계층을 통과한 데이터의 크기. (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),
            # 위의 계층을 통과한 데이터의 크기. (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)
Discriminator(
  (main): Sequential(
    (0): Conv2d(3, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (1): LeakyReLU(negative_slope=0.2, inplace=True)
    (2): Conv2d(64, 128, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (3): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (4): LeakyReLU(negative_slope=0.2, inplace=True)
    (5): Conv2d(128, 256, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (6): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (7): LeakyReLU(negative_slope=0.2, inplace=True)
    (8): Conv2d(256, 512, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (9): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (10): LeakyReLU(negative_slope=0.2, inplace=True)
    (11): Conv2d(512, 1, kernel_size=(4, 4), stride=(1, 1), bias=False)
    (12): Sigmoid()
  )
)

 

손실함수와 옵티마이저

# BCELoss 함수의 인스턴스를 생성합니다
criterion = nn.BCELoss()
	# -ylogh(x) - (1-y)log(1-h(x))   여기서 y는 실제 y, h(x)는 predict한 값
    	# logD(x) + log(1-D(G(X)))

# 생성자의 학습상태를 확인할 잠재 공간 벡터를 생성합니다
fixed_noise = torch.randn(64, nz, 1, 1, device=device)

# 학습에 사용되는 참/거짓의 라벨을 정합니다
real_label = 1.
fake_label = 0.

# G와 D에서 사용할 Adam옵티마이저를 생성합니다
# G와 D가 서로 충돌되는 학습을 하기 때문에 optimizer 따로 설정해준다.
optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999))

BCEloss는 파이토치에서 $-ylogh(x)-(1-y)log(1-h(x))$ 와 같이 구현된다.

이때 y가 1일 때와 0일 때를 고려하여 우리가 원하는 요소들만 골라낼 수 있다. 

(이 표현이 정확한 표현인진 모르겠지만..) BCE loss를 사용하여 D loss와 G loss를 정의할 수 있다. 

 

 

학습

우리의 의도 = "진짜 혹은 가짜 이미지를 구성"하고,  logD(G(z))를 최대화하는 G의 목적함수를 최적화 시키는 것

학습 과정은 2가지로 나뉜다.

 

Part 1 - D의 학습

위의 로스에서도 정의했듯이, D는  $log(D(x))+log(1-D(G(z)))$를 최대화시키는 것과 같다.

1) 진짜 데이터들로만 이루어진 배치를 만들어 D에 통과시킨다.

       - 출력값으로 log(D(x))의 손실값을 계산

       - backpropagation 고정에서의 변화도를 계산

2) 가짜 데이터들로만 이루어진 배치를 만들어 D에 통과시킨다.

       - 그 출력값으로 log(1-D(G(z)))의 손실값 계산

       - backprobagation 변화도 계산

이때, 두 가지 스텝에서 나오는 변화도들을 축적시켜야한다. 

backpropagation 계산했으니, 옵티마이저 사용해서 backpropagation 적용

 

Part 2 - G의 학습

G는 $log(D(G(z)))$를 최대화 시켜주는 것

D를 이용해 G의 출력값을 판별해주고, 진짜 라벨값을 이용해 G의 손실값을 구한다. (즉, G로 생성한 데이터를 판별자에 넣어서 판별해서 나온 값을 진짜 라벨값을 이용해서 loss  계산해준다.)

구한 손실값으로 변화도를 구하고, 최종적으로는 옵티마이저를 이용해 G의 가중치들을 업데이트 시켜준다.

 

정리하면 다음과 같다. (pytorch tutorial에서 그대로 복사해옴)

  • Loss_D - 진짜 데이터와 가짜 데이터들 모두에서 구해진 손실값. (log(D(x)) + log(1 - D(G(z))).
  • Loss_G - 생성자의 손실값. log(D(G(z)))
  • D(x) - 구분자가 데이터를 판별한 확률값이다. 처음에는 1에 가까운 값이다가, G가 학습할수록 0.5값에 수렴하게 된다.
  • D(G(z)) - 가짜데이터들에 대한 구분자의 출력값이다. 처음에는 0에 가까운 값이다가, G가 학습할수록 0.5에 수렴하게 된다

 

# 학습 과정

# 학습상태를 체크하기 위해 손실값들을 저장합니다
img_list = []
G_losses = []
D_losses = []
iters = 0

print("Starting Training Loop...")
# 에폭(epoch) 반복
for epoch in range(num_epochs):
    # 한 에폭 내에서 배치 반복
    for i, data in enumerate(dataloader, 0):

        ############################
        # (1) D 신경망을 업데이트 합니다: log(D(x)) + log(1 - D(G(z)))를 최대화 합니다
        ###########################
        ## 진짜 데이터들로 학습을 합니다
        netD.zero_grad()
        # 배치들의 사이즈나 사용할 디바이스에 맞게 조정합니다
        real_cpu = data[0].to(device)
        b_size = real_cpu.size(0)
        label = torch.full((b_size,), real_label,
                           dtype=torch.float, device=device)  # batch 사이즈만큼 1을 만들어준다. 
                                                              # label = (b_size, 1)
        # 진짜 데이터들로 이루어진 배치를 D에 통과시킵니다
        output = netD(real_cpu).view(-1)
        # 손실값을 구합니다
        errD_real = criterion(output, label)   # log(D(x))
        # 역전파의 과정에서 변화도를 계산합니다
        errD_real.backward()
        D_x = output.mean().item()

        ## 가짜 데이터들로 학습을 합니다
        # 생성자에 사용할 잠재공간 벡터를 생성합니다
        noise = torch.randn(b_size, nz, 1, 1, device=device)
        # G를 이용해 가짜 이미지를 생성합니다
        fake = netG(noise)  # generator가 만들어낸 가짜 이미지
        label.fill_(fake_label)  # 0으로 채운다
        # D를 이용해 데이터의 진위를 판별합니다
        output = netD(fake.detach()).view(-1)
        # D의 손실값을 계산합니다
        errD_fake = criterion(output, label)
        # 역전파를 통해 변화도를 계산합니다. 이때 앞서 구한 변화도에 더합니다(accumulate)
        errD_fake.backward()
        D_G_z1 = output.mean().item()
        # 가짜 이미지와 진짜 이미지 모두에서 구한 손실값들을 더합니다
        # 이때 errD는 역전파에서 사용되지 않고, 이후 학습 상태를 리포팅(reporting)할 때 사용합니다
        errD = errD_real + errD_fake
        # D를 업데이트 합니다
        optimizerD.step()

        ############################
        # (2) G 신경망을 업데이트 합니다: log(D(G(z)))를 최대화 합니다
         가짜 이미지를 D에 넣었을 때 1이 되도록 학습
        ###########################
        netG.zero_grad()
        label.fill_(real_label)  # 생성자의 손실값을 구하기 위해 진짜 라벨을 이용할 겁니다
        # 우리는 방금 D를 업데이트했기 때문에, D에 다시 가짜 데이터를 통과시킵니다.
        # 이때 G는 업데이트되지 않았지만, D가 업데이트 되었기 때문에 앞선 손실값가 다른 값이 나오게 됩니다
        output = netD(fake).view(-1)
        # G의 손실값을 구합니다
        errG = criterion(output, label)
        # G의 변화도를 계산합니다
        errG.backward()
        D_G_z2 = output.mean().item()
        # G를 업데이트 합니다
        optimizerG.step()    # Discriminator는 학습 x, Generator만 학습!

'Deep Learning > GAN' 카테고리의 다른 글

[GAN-①] GAN 기본 컨셉  (0) 2021.11.25

댓글