출처 : 같이 gan 과기대 대회에 나가는 팀원 나라님의 친절한 설명 덕분에 잘 이해할 수 있었습니다. 🙇‍♀️

GAN 은 뭔가 2%가 부족해…

GAN 은 최대한 진짜 같은 가짜 이미지를 만들 수 있는 굉장히 깔쌈한 모델이라고 할 수 있다. 하지만 GAN이 진짜로 원하는 이미지를 생성한다고 말하기에는 뭔가 2% 가 부족한데 바로 condition 이 빠졌기 때문이다. condition 은 라벨이 될 수도 있고, ‘어떠한’ 이미지를 만들어라 ! 의 ‘어떠한’ 부분에 해당할 수도 있다. 가령 mnist 데이터를 활용해 gan 모델을 만들면 ‘숫자 7의’ 이미지를 만들어줘 ! 와 같은 condition 을 넣어주는 부분이 없기 때문에, mnist 숫자 이미지 중 아무거나 그럴싸하게 만들어줘 ! 라는 두루뭉실한 명령만 하는 셈이다.

이에 반해 condition 이 들어간 CGan 을 활용한다면 ‘mnists 중 숫자7 의 이미지를 만들어줘 !’ 이런 식으로 구체적 명령을 할 수 있다.

CGan 의 loss function

CGan 의 loss function 도 GAN 과 매우 유사하다. condition 이 붙었다는 차이만 가지고 있다.

선택 영역_182

CGan 학습 과정 정리

mnist 데이터를 넣었다고 가정하고 정리해보았다.

1) 생성자에게 “최대한 그럴싸한 숫자 c 의 이미지를 만들어줘 !” 라고 명령하고, 생성자는 x*|c 를 만든다.

2) 판별자에게 “숫자 c 의 진짜 오리지널 이미지” ; (x,c) 를 넣어주면 판별자는 1로 판별하도록 학습한다.

3) 판별자에게 “숫자 c 의 가짜 이미지 (=생성자가 만든 숫자 c 의 이미지)” ;(x*|c,c) 를 넣어주면 판별자는 0으로 판별하도록 학습한다.

(한편 1) 과정에서 생성자는 판별자를 최대한 속일 수 있을 정도의 가짜 이미지를 만들고자 노력한다.)

3) 을 보았을 때, 일반 Gan 하고 비교해보면 각 숫자 라벨별로 생성자가 만든 이미지가 미세하게 다른 점을 하나하나 구별해봐야하기 때문에 훨씬 어려운 task 를 수행하는 것 같다

선택 영역_183

ex. 생성자가 만든 숫자 2의 이미지

vs

선택 영역_184

ex. 오리지널 숫자 2의 이미지

CGan 코드

출처 : TF-GAN: 이활석님 TF-GAN repos.

Functional API 가 활용된 코드인데 Functional API 자체가 가물가물해서 상기시켜보았다

1 - With the “Functional API”, where you start from Input, you chain layer calls to specify the model’s forward pass, and finally you create your model from inputs and outputs:

import tensorflow as tf

inputs = tf.keras.Input(shape=(3,))
x = tf.keras.layers.Dense(4, activation=tf.nn.relu)(inputs)
outputs = tf.keras.layers.Dense(5, activation=tf.nn.softmax)(x)
model = tf.keras.Model(inputs=inputs, outputs=outputs)

**텐플, 모듈 import **

%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np

from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import (Activation, BatchNormalization, Concatenate, Dense,
                                     Embedding, Flatten, Input, Multiply, Reshape)
from tensorflow.keras.layers import LeakyReLU
from tensorflow.keras.layers import Conv2D, Conv2DTranspose
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.optimizers import Adam

이미지 shape, 채널, z 차원, 숫자 class 지정

img_rows = 28
img_cols = 28
channels = 1

# 입력 이미지 차원
img_shape = (img_rows, img_cols, channels)

# 생성자 입력으로 사용될 잡음 벡터 크기
z_dim = 100

# 데이터셋에 있는 클래스 개수
num_classes = 10

생성자

Transposed Convolution : Upscaling the input image to higher resolutions

Transpose Convolution 직관적인 feature upscaling 과정

선택 영역_189

  • generator

코드에서 사용된 Generator 모델 구성

선택 영역_190

def build_generator(z_dim):

    model = Sequential()

    # 완전 연결 층을 사용해 입력을 7×7×256 텐서로 변환합니다.
    model.add(Dense(256 * 7 * 7, input_dim=z_dim))
    model.add(Reshape((7, 7, 256)))

    # 7×7×256에서 14×14×128 텐서로 바꾸는 전치 합성곱 층
    model.add(Conv2DTranspose(128, kernel_size=3, strides=2, padding='same'))

    # 배치 정규화
    model.add(BatchNormalization())

    # LeakyReLU 활성화
    model.add(LeakyReLU(alpha=0.01))

    # 14×14×128에서 14×14×64 텐서로 바꾸는 전치 합성곱 층
    model.add(Conv2DTranspose(64, kernel_size=3, strides=1, padding='same'))

    # 배치 정규화
    model.add(BatchNormalization())

    # LeakyReLU 활성화
    model.add(LeakyReLU(alpha=0.01))

    # 14×14×64에서 28×28×1 텐서로 바꾸는 전치 합성곱 층
    model.add(Conv2DTranspose(1, kernel_size=3, strides=2, padding='same'))

    # tanh 활성화 함수
    model.add(Activation('tanh'))

    return model
  • cgan_generator

Embedding : 첫 번째 인자 = 단어 집합의 크기. 즉, 총 단어의 개수 , 두 번째 인자 = 임베딩 벡터의 출력 차원 , input_length = 입력 시퀀스의 길이

선택 영역_191

Multiply :

Arguments ) A list of input tensors (at least 2).

Returns ) A tensor, the element-wise product of the inputs

def build_cgan_generator(z_dim):

    # 랜덤 잡음 벡터 z
    z = Input(shape=(z_dim, ))

    # 조건 레이블: 정수 0-9까지 생성자가 만들 숫자
    label = Input(shape=(1, ), dtype='int32')

    # 레이블 임베딩: 
    # ----------------
    # 레이블을 z_dim 크기 밀집 벡터로 변환하고 
    # (batch_size, 1, z_dim) 크기 3D 텐서를 만듭니다.
    label_embedding = Embedding(num_classes, z_dim, input_length=1)(label)

    # 임베딩된 3D 텐서를 펼쳐서 (batch_size, z_dim) 크기 2D 텐서로 바꿉니다.
    label_embedding = Flatten()(label_embedding)

    # 벡터 z와 레이블 임베딩의 원소별 곱셈
    joined_representation = Multiply()([z, label_embedding])

    generator = build_generator(z_dim)

    # 주어진 레이블에 대한 이미지 생성
    conditioned_img = generator(joined_representation)

    return Model([z, label], conditioned_img)

판별자

  • discriminator

생성자 보다 비교적 간단하다. 단순히 LeakyReLU 와 Conv2D 를 차곡차곡 쌓았다.

def build_discriminator(img_shape):

    model = Sequential()

    # 28×28×2에서 14×14×64 텐서로 바꾸는 합성곱 층
    model.add(
        Conv2D(64,
               kernel_size=3,
               strides=2,
               input_shape=(img_shape[0], img_shape[1], img_shape[2] + 1),
               padding='same'))

    # LeakyReLU 활성화 함수
    model.add(LeakyReLU(alpha=0.01))

    # 14×14×64에서 7×7×64 텐서로 바꾸는 합성곱 층
    model.add(
        Conv2D(64,
               kernel_size=3,
               strides=2,
               padding='same'))

    # LeakyReLU 활성화 함수
    model.add(LeakyReLU(alpha=0.01))

    # 7×7×64에서 3×3×128 텐서로 바꾸는 합성곱 층
    model.add(
        Conv2D(128,
               kernel_size=3,
               strides=2,
               padding='same'))

    # LeakyReLU 활성화 함수
    model.add(LeakyReLU(alpha=0.01))

    # 시그모이드 활성화 함수를 사용한 출력층
    model.add(Flatten())
    model.add(Dense(1, activation='sigmoid'))

    return model
  • cgan_discriminator

np.prod :본래 axis 가 없다면 모든 array elements 를 곱하기 때문에 상수가 나오지만, axis 가 있다면 결과는 array 형태로 나올 수도 있음

np.prod((28 * 28 * 1)) = 784

선택 영역_192

Concatenate : concatenate 은 텐서를 쌓는데, 차원을 새로 만들지 않고, 지정한 축의 차원을 늘리는 방식으로 쌓는다.

def build_cgan_discriminator(img_shape):

    # 입력 이미지
    img = Input(shape=img_shape)

    # 입력 이미지의 레이블
    label = Input(shape=(1, ), dtype='int32')

    # 레이블 임베딩: 
    # ----------------
    # 레이블을 z_dim 크기의 밀집 벡터로 변환하고 
    # (batch_size, 1, 28×28×1) 크기의 3D 텐서를 만듭니다.
    label_embedding = Embedding(num_classes,
                                np.prod(img_shape),
                                input_length=1)(label)

    # 임베딩된 3D 텐서를 펼쳐서 (batch_size, 28×28×1) 크기의 2D 텐서를 만듭니다.
    label_embedding = Flatten()(label_embedding)

    # 레이블 임베딩 크기를 입력 이미지 차원과 동일하게 만듭니다. -> 라벨 하나당 3차원 (28,28,1) 
    label_embedding = Reshape(img_shape)(label_embedding)

    # 이미지와 레이블 임베딩을 연결합니다. #(batch_size,28,28,1) & (batch_size,28,28,1) -> (batch_size,28,28,2)
    concatenated = Concatenate(axis=-1)([img, label_embedding])

    discriminator = build_discriminator(img_shape)

    # 이미지-레이블 쌍을 분류합니다.
    classification = discriminator(concatenated)

    return Model([img, label], classification)

모델 만들기

def build_cgan(generator, discriminator):

    # 랜덤 잡음 벡터 z
    z = Input(shape=(z_dim, ))

    # 이미지 레이블
    label = Input(shape=(1, ))

    # 레이블에 맞는 이미지 생성하기
    img = generator([z, label])

    classification = discriminator([img, label]) #generator 이 생성한 img , 생성하라고 명령한 condition pair 을 discriminator 이 판별

    # 생성자 -> 판별자 연결 모델 
    # G([z, label]) = x* 
    # D(x*) = 분류
    model = Model([z, label], classification)

    return model
# 판별자 만들고 컴파일하기
discriminator = build_cgan_discriminator(img_shape)
discriminator.compile(loss='binary_crossentropy',
                      optimizer=Adam(learning_rate=0.00001),
                      metrics=['accuracy'])

# 생성자 만들기 -> generator ; conditioned image를 생성하는 모델 
generator = build_cgan_generator(z_dim)

# 생성자를 훈련하는 동안 판별자 모델 파라미터를 고정하기
discriminator.trainable = False

# 생성자를 훈련하기 위해 고정된 판별자로 CGAN 모델 만들고 컴파일하기
cgan = build_cgan(generator, discriminator)
cgan.compile(loss='binary_crossentropy', optimizer=Adam())

훈련

model.fit vs model.train_on_batch

model.fit will train 1 or more epochs. That means it will train multiple batches. model.train_on_batch, as the name implies, trains only one batch.

To give a concrete example, imagine you are training a model on 10 images. Let’s say your batch size is 2. model.fit will train on all 10 images, so it will update the gradients 5 times. (You can specify multiple epochs, so it iterates over your dataset.) model.train_on_batch will perform one update of the gradients, as you only give the model on batch. You would give model.train_on_batch two images if your batch size is 2.

/

np.random.randit : np.random.randint(0, X_train.shape[0], batch_size)

전체 mnist training data 중 랜덤으로 배치 사이즈 개수만큼을 가져옴

판별자 훈련 -> 판별자 parameters 고정 -> 생성자 훈련

accuracies = []
losses = []


def train(iterations, batch_size, sample_interval):

    # MNIST 데이터셋을 로드합니다.
    (X_train, y_train), (_, _) = mnist.load_data()

    # [0, 255] 사이 흑백 픽셀 값을 [–1, 1]로 스케일 변환합니다.
    X_train = X_train / 127.5 - 1.
    X_train = np.expand_dims(X_train, axis=3)

    # 진짜 이미지의 레이블: 모두 1
    real = np.ones((batch_size, 1))

    # 가짜 이미지의 레이블: 모두 0
    fake = np.zeros((batch_size, 1))

    for iteration in range(iterations):

        # -------------------------
        #  판별자를 훈련합니다.
        # -------------------------

        # 진짜 이미지와 레이블로 이루어진 랜덤한 배치를 얻습니다.
        idx = np.random.randint(0, X_train.shape[0], batch_size)
        imgs, labels = X_train[idx], y_train[idx]

        # 가짜 이미지 배치를 생성합니다.
        z = np.random.normal(0, 1, (batch_size, z_dim))
        gen_imgs = generator.predict([z, labels])

        # 판별자를 훈련합니다.
        d_loss_real = discriminator.train_on_batch([imgs, labels], real)
        d_loss_fake = discriminator.train_on_batch([gen_imgs, labels], fake)
        d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)

        # ---------------------
        #  생성자를 훈련합니다.
        # ---------------------

        # 잡음 벡터의 배치를 생성합니다.
        z = np.random.normal(0, 1, (batch_size, z_dim))

        # 랜덤한 레이블의 배치를 얻습니다.
        labels = np.random.randint(0, num_classes, batch_size).reshape(-1, 1)

        # 생성자를 훈련합니다.
        g_loss = cgan.train_on_batch([z, labels], real)

        if (iteration + 1) % sample_interval == 0:

            # 훈련 과정을 출력합니다.
            print("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" %
                  (iteration + 1, d_loss[0], 100 * d_loss[1], g_loss))

            # 훈련이 끝난 후 그래프를 그리기 위해 손실과 정확도를 저장합니다.
            losses.append((d_loss[0], g_loss))
            accuracies.append(100 * d_loss[1])