UNSUPERVISED REPRESENTATION LEARNING WITH DEEP CONVOLUTIONAL
GENERATIVE ADVERSARIAL NETWORKS (2016)

개요

GAN의 불안정한 구조를 개선하고, 벡터 산술 연산을 가능케 함

기존 GAN의 한계

  • 불안정성 (그다지 좋지 않은 성능)
  • 블랙박스 (어떻게 이러한 결과가 나왔는지 알 수가 없음)
  • 생성모델 평가 (성능을 어떻게 평가할지 정량적 기준이 없음)
안정적인 DCGAN을 위한 아키텍처 가이드라인
- 식별기의 경우 pooling 레이어를 strided convolutions로 대체하고, 생성기의 경우 fractional-strided convolutions로 대체한다
- 식별기와 생성기 모두 batchnorm을 사용한다
- 더 깊은 아키텍처를 위해서 fully connected hidden layers를 없앤다
- 생성기의 경우, Tanh 을 사용하는 출력 레이어를 제외하고 모든 레이어에서 ReLU 활성화를 사용한다
- 식별기의 경우, 모든 레이어에서 LeakyReLU를 사용한다

UNSUPERVISED REPRESENTATION LEARNING WITH DEEP CONVOLUTIONAL
GENERATIVE ADVERSARIAL NETWORKS (2016)

초록

최근 몇년간 CNN을 활용한 지도학습 방식은 컴퓨터 비전 분야에서 매우 잘 적용됐다. 반면에 CNN을 활용한 비지도 학습은 주목 받지 못했다. 논문에서는 CNN의 지도학습과 비지도학습의 간극에 다리를 놓고자 한다. 우리가 소개하는 DCGAN은 특정 구조적 제한을 가지고 있으며, 비지도 학습의 강력한 후보가 될 것이라고 증명한다. 다양한 이미지 데이터셋으로 훈련시켜봄으로써 CNN 적대쌍은 대상의 부분부터 전경까지 특성들의 위계(a hierarchy of representations)를 학습한다. 나아가 우리는 학습된 특징을 활용해 일반적인 이미지의 특성으로서 적용가능성을 확인한다.

 

1. INTRO

  • GAN을 학습한 후, 생성기와 식별기의 일부분을 재활용해 지도학습에서 특성을 추출하는 데 사용하는 방법
- convolutional GAN의 구조적 위상에 제한을 걸어, 대부분의 환경에서 안정적으로 학습되도록 함.
- 이 구조를 Deep Convolutional GANs 라고 부르기로 함.
- 이미지 분류 과제에 훈련된 식별기를 사용하여 비지도 알고리즘과 경쟁하게게 함.
- GAN으로 학습한 필터를 시각화하고, 특정 필터가 특정 대상을 그리도록 학습됨을 실증적으로 보여줌.
- 생성기가 흥미로운 벡터 산술적 성질을 가지고 있어, 생성된 샘플을 가지고 다양한 의미적 특성을 간단하게 조작할 수 있음.
  • 라벨링되지 않은 데이터로부터 특징을 학습하기: K-means와 같은 클러스터링, 오토엔코더, …
  • natural image를 생성하기: non-parametric 모델, parametric 모델
  • CNN의 내부를 시각화하기: 블랙박스 메소드

3. 접근법 및 모델 구조

  • 핵심: CNN에서 세 가지 변경
    • 첫째, deterministic pooling 함수 대신에 strided convolutions을 사용 → 신경망이 스스로 공간적으로 다운샘플링 학습
    • *deterministic: 하나로 결정되는, 확률분포를 나타내는 stochastic 에 상반되는 표현 [Deep Learning] 헷갈리는 기본 용어 모음집
    • 둘째, convolutional features를 제외하고 fully connected 레이어들을 제거하는 트렌드 → global average pooling이 모델 안정성을 높이지만 수렴 속도를 해침. 가장 높은 convolutional feature들을 생성기와 식별기 각각의 인풋과 아웃풋에 바로 연결하는 절충안이 효과가 좋았음. 분포 Z에서 노이즈를 가져와 입력으로 사용하는 GAN의 첫번째 레이어를 fully connected로 볼 수는 있는데, 단순한 행렬 곱셈이기 때문이다. 하지만 4차원 텐서로 reshape되어 convolution의 시작으로 사용된다. 식별기의 경우 마지막 convolution 레이어는 flatten되어 시그모이드 단일 출력값으로 먹여진다.
    • 셋째, Batch Normalization

  • 생성기에서는 Tanh함수를 사용하는 출력 레이어를 제외하고 ReLU 활성화 함수를 사용한다. bound된 활성화 함수를 사용하면 더 금방 포화되고 학습 분포의 color space를 덮어버림. 식별기에서는 LeakyReLU가 잘 작동했고, 특히 고해상도 모델링에서 그랬음. maxout activation을 사용한 GAN과 다른 지점.

 

4. 적대 학습

LSUN, Imagenet-1k, Faces dataset 으로 학습

tanh 활성화 함수 범위 [-1, 1]으로 스케일링한 것 말고는 전처리 거친 것 없음

DCGAN generator 구조

→ A 100 dimensional uniform distribu- tion Z is projected to a small spatial extent convolutional representation with many feature maps. A series of four fractionally-strided convolutions (in some recent papers, these are wrongly called deconvolutions) then convert this high level representation into a 64 × 64 pixel image. Notably, no fully connected or pooling layers are used.

 

보충

1. Pooling vs. Stride (for downsampling)

Pooling vs. stride for downsampling

Pooling is a fixed operation and convolution can be learned. On the other hand, pooling is a cheaper operation than convolution, both in terms of the amount of computation that you need to do and number of parameters that you need to store (no parameters for pooling layer).

Pooling과 stride는 모두 downsampling에 쓰일 수 있지만 전자는 최소, 최대, 평균으로 골라 축소하는 방식이라면 후자는 필터를 통해 계산하는 방식이기 때문에, stride일 때 모델이 학습한다고 볼 수 있음

2. Global average pooling(GAP)

CNN + Fully Connected Layer 구조에서 Fully Connected Layer를 없애기 위한 방식으로 도입. Fully Connected Layer 는 앞단에서 출력한 feature 전체(=이미지 전체의 특징)를 행렬곱하여 결과로 출력. 반면 GAP은 feature 노드 값의 평균을 가져와 1차원의 벡터로 축소시킴. GAP을 거친 것을 Fully Connected Layer로 전달하기도 함.

3. Batch Normalization

요즘은 일반적으로 쓰는 것이지만 논문 발표 당시에는 비교적 새로운 것이었나 봄. 활성화 함수의 출력값을 정규화(평균 0, 분산 1)하는 레이어.

배치 정규화 알고리즘

4. fractionally strided convolutions

stride

filter가 이동하는 사이즈

transpose convolutions

stride가 1 미만. 기존 strided convolutions는 필터를 거친 결과가 입력보다 작아짐. 반면에 transpose convolutions은 필터를 거친 결과가 입력보다 커진다. 필터의 값과 모두 곱해 결과를 구하고, 필터가 겹치는 부분은 sum한다.

fractionally strided convolutions

transpose convolutions의 일종으로, 입력 셀 사이에 패딩(0)을 끼워넣는다.

5. walking in the latent space

latent space 잠재공간(=특징공간)

If I have to describe latent space in one sentence, it simply means a representation of compressed data.

 

walking in the latent space

단순 무작위값을 입력하는 것을 넘어, z공간을 활용한 다양한 조작이 가능함.

생성기의 input인 잠재공간에서 변수 z가 움직였을 때 부드러운 변화를 보여주는 것. latent space(Z) 에서 임의변수 z를 x개 추출한 후, z(x)와 z(x+1)에 Interpolation(보간) 을 수행했을 경우 부드러운 변화(transition)가 나타나야 한다. 

*부드러운 변화란 연속적인 변화라는 뜻이기도 함.

*보간: 우리가 알고 있는 정보가 참값이라고 믿고 그 주위를 짐작해보는 것(밥짓기 비유). 이와 비슷한 ‘회귀’는 알고 있는 정보에 오차가 포함되어 있다고 생각하고 메타모델을 구축하는 것.

word2vec 처럼 이미지 연산도 가능해짐.

How to Explore the GAN Latent Space

How to Explore the GAN Latent Space When Generating Faces - Machine Learning Mastery

<잠재 공간의 벡터 연산>

“잠재공간은 그 자체로 의미가 없다. 보통은 100차원의 초구(hypersphere)로 가우시안 분포(평균 0, 분산 1)를 띤다. 학습을 통해 생성기는 특정 출력 이미지를 가지고 point들을 잠재 공간으로 매핑하는 방법을 학습한다. 이 매핑은 모델이 학습될 때마다 매번 달라질 것이다.

생성기 입장에서 잠재 공간은 어떤 구조를 띤다. 이 구조는 모델에 따라서 query되고 navigate될 수 있다.

보통 새로운 이미지들은 잠재 공간의 랜덤한 point들로 생성된다. 이를 확장해 본다면, 잠재공간의 point들은 구축(construct)될 수 있으며 입력값이나 특정 이미지를 생성하는 query로도 쓰일 수 있다.

일련의 point들은 잠재 공간에서 두 point를 잇는 선형 경로에 만들어질 수 있다. 두 point를 생성된 이미지 두 장으로 볼 수 있다. 이 point들은 한 이미지에서 다른 이미지로 변화하는 일련의 이미지들을 생성하는 데 사용할 수 있다.

결국에 잠재 공간의 point들을 보존해서 간단한 벡터 연산을 통해 잠재 공간의 새로운 point, 즉 새로운 이미지를 생성하는 데 쓸 수 있다. 직관적이고 목적이 뚜렷한 이미지 생성이 가능해지는 것이다.”

 

코드 (vanila DCGAN)

 
inputs = keras.Input(shape=(100,))
x = inputs
x = layers.Dense(7*7*256, use_bias=False, input_shape=(100,))(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)
x = layers.Reshape((7, 7, 256))(x)
x = layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)
outputs = layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh')(x)

G = keras.Model(inputs, outputs)​

생성기: 100차원의 노이즈를 입력으로 받는 부분은 동일, Batch Normalization가 추가 되었음. LeakyReLU를 활성화 함수로 쓰고, strided convolutions로 쌓는다. (transpose convolutions라고도 한다고 함)

 

inputs = keras.Input(shape=(28,28,1))
x = inputs
x = layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same',
                                  input_shape=[28, 28, 1])(x)
x = layers.LeakyReLU()(x)
x = layers.Dropout(0.3)(x)

x = layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same')(x)
x = layers.LeakyReLU()(x)
x = layers.Dropout(0.3)(x)

x = layers.Flatten()(x)
x = layers.Dense(1)(x)
outputs = x

D = keras.Model(inputs, outputs)

식별기: 28*28 사이즈의 이미지를 받아 실제 샘플일 확률 0~1 값을 출력. LeakyReLU를 활성화 함수로 쓴다.

*이 코드에서는 GAP이 쓰이지 않은 듯

 

test_noise = tf.random.normal([1, 100])
fake_image_test = G(test_noise, training=False)

fake_image_test = layers.Reshape((28, 28))(fake_image_test) 
# 마지막 레이어에서 tahn 값으로 출력됐으므로

plt.imshow(fake_image_test[0], cmap='gray')

 

임의로 가짜 이미지를 생성한 결과

 

decision = D(fake_image_test, training=False)
print(decision)

tf.Tensor([[0.00125669]], shape=(1, 1), dtype=float32)

위 이미지를 식별기에 통과시킨 결과

 

EPOCHS = 50
noise_dim = 100

seed = tf.random.normal([BATCH_SIZE, noise_dim])

G_optimizer = tf.keras.optimizers.Adam(1e-4)
D_optimizer = tf.keras.optimizers.Adam(1e-4)

옵티마이저 설정

 

cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)

def D_loss(real_output, fake_output):
    real_loss = cross_entropy(tf.ones_like(real_output), real_output)
    fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
    total_loss = real_loss + fake_loss
    return total_loss

def G_loss(fake_output):
    return cross_entropy(tf.ones_like(fake_output), fake_output)

각 손실함수 정의

 

@tf.function
def train_step(real_images):  
  
  noises = tf.random.normal([BATCH_SIZE, noise_dim])
  
  with tf.GradientTape() as gen_tape, tf.GradientTape() as dsc_tape:
    fake_images = G(noises, training=True)
    
    real_output = D(real_images, training=True)
    fake_output = D(fake_images, training=True)
    
    gen_loss = G_loss(fake_output)
    dsc_loss = D_loss(real_output, fake_output)
    
  gen_gradients = gen_tape.gradient(gen_loss, G.trainable_variables)
  dsc_gradients = dsc_tape.gradient(dsc_loss, D.trainable_variables)
  
  G_optimizer.apply_gradients(zip(gen_gradients, G.trainable_variables)) 
  D_optimizer.apply_gradients(zip(dsc_gradients, D.trainable_variables))
  
  
def test_step(real_images):  
  noises = tf.random.normal([BATCH_SIZE, noise_dim])
  
  fake_images = G(noises, training=False)
  
  real_output = D(real_images, training=False)
  fake_output = D(fake_images, training=False)    
  
  gen_loss = G_loss(fake_output)
  dsc_loss = D_loss(real_output, fake_output)
  
  print("Generator loss:", gen_loss.numpy(), "Discriminator loss:", dsc_loss.numpy())
  return gen_loss.numpy(), dsc_loss.numpy()
  
# 학습 함수

def train(dataset, epochs):
    G_loss_history, D_loss_history = [], []
    for epoch in range(epochs):
        start = time.time()
    
        for i, image_batch in enumerate(dataset):
            train_step(image_batch)
            if i == 0:
                g, d = test_step(image_batch)
                G_loss_history.append(g)
                D_loss_history.append(d)    
      
        print ('Time for epoch {} is {} sec'.format(epoch + 1, time.time()-start))  
    return G_loss_history, D_loss_history
%%time
g_history, d_histroy = train(train_dataset, EPOCHS)

noises = tf.random.normal([50, 100])
generated_image = G(noises, training=False)

fig, axes = plt.subplots(nrows=3, ncols=2, figsize=(10,10))

for ax in axes.flat:
  ax.axis('off')

axes[0,0].imshow(generated_image[0][:,:,0], cmap='gray')
axes[0,1].imshow(generated_image[1][:,:,0], cmap='gray')
axes[1,0].imshow(generated_image[2][:,:,0], cmap='gray')
axes[1,1].imshow(generated_image[3][:,:,0], cmap='gray')
axes[2,0].imshow(generated_image[4][:,:,0], cmap='gray')
axes[2,1].imshow(generated_image[5][:,:,0], cmap='gray')

plt.show()

 

<잠재 공간에서 interpolate 하기>

def interpolate_points(p1, p2, n_steps=10):
	ratios = np.linspace(0, 1, num=n_steps) # linear interpolate vectors
	vectors = list()
	for ratio in ratios:
		v = (1.0 - ratio) * p1 + ratio * p2
		vectors.append(v)
		# v를 다시 쓰면 p1 + ratio*(p2-p1) 이므로 ratio만큼 p1에서 서서히 p2로 다가감
	return np.asarray(vectors)​

np.linspace: 0부터 1까지 (포함) num개수 만큼 있는 1차원 배열 생성해줌

p1와 p2 사이의 선형 보간(linear interpolate) → p1와 p2 사이 n_step 개 point를 반환

 

def plot_generated(examples, n):
    for i in range(n*n):
        plt.subplot(n, n, 1+i)
        plt.axis('off')
        plt.imshow(examples[i][:, :, 0], cmap='gray')
    plt.show()

n * n 으로 사진 펼쳐놓기

 

pts = tf.random.normal([20, 100])

results = None
for i in range(0, 20, 2): # 0부터 18까지 짝수만
    interpolated = interpolate_points(pts[i], pts[i+1])
    X = G.predict(interpolated)
    X = (X+1)/2.0 # [-1, 1]인 tahn값을 [0, 1]로 스케일링
    if results is None:
        results = X
    else:
        results = np.vstack((results, X))

plot_generated(results, 10)

pts : 100차원의 임의 노이즈 20개

interpolated: 노이즈 한 쌍 사이의 벡터 point 10개

np.vstack: 배열을 세로로 쌓음

 

 

<참조>

4. Unsupervised Representation learning with Deep Convolutional Generative Adversarial Networks(DCGAN) - paper review

Global Average Pooling 이란

'인공지능 > computer vision' 카테고리의 다른 글

styleGAN 이해하기  (0) 2022.05.31
PGGAN의 공식 코드 살펴보기  (0) 2022.05.31
PGGAN 이해하기  (0) 2022.05.31
GAN 이해하기  (0) 2022.05.15
distinctive image features from scale-invariant keypoints  (0) 2022.05.11
복사했습니다!