ProGAN은 풀 HD 화질로 실제 사진같은 이미지를 생성하는 최신 GAN 기법이다.
ProGAN의 핵심 요소들은 다음과 같다.
- 고해상도 층으로 점진적 증가와 단계적 도입
- 미니배치 표준편차
- 균등 학습률
- 픽셀별 특성 정규화
이 장에서는 두 가지 예제에 대해 학습한다.
- ProGAN의 중요 혁신에 해당하는 코드 중 부드럽게 고해상도 층으로 연결되는 것과 3가지 혁신에 대한 예제
- TFHub에서 구글이 제공하는 사전 훈련된 구현을 다운로드하여 사용한다. TFHub는 머신러닝 모델을 위한 새로운 중앙 저장소이다. 도커 허브나 소프트웨어 패키지 분야의 Conda, PyPI 저장소와 비슷하다. 이 구현을 사용하면 잠재 공간을 보간하여 생성 샘플의 특징을 조절할 수 있다. 생성자의 잠재 공간에서 초기 벡터를 선택하기만 하면 원하는 이미지를 손쉽게 얻을 수 있다.
다른 장처럼 직접 구현하지 않고 TFHub로 ProGAN을 만드는 이유는 워크플로 속도를 높일 수 있는 엔지니어링 모범 사례를 제공하고 ProGAN의 원본 구현은 엔비디아 연구자들이 한두 달 걸려 실행한 것이다. 이를 우리가 시행하기에는 무리가 있기에 TFHub를 통해 만든다.
잠재 공간 보간
출력을 위한 초기값을 만드는 저해상도 공간을 잠재 공간이라고 불렀다. 만약 얼굴 이미지에 안경을 씌우는 벡터를 찾을 수 있다면 동일한 벡터로 새로운 이미지에 안경을 씌울 수 있다. 또한 랜덤한 벡터 두 개를 선택하고 둘 사이를 점진적으로 이동하여 부드럽게 조금씩두 번째 벡터로 변하는 이미지를 얻을 수 있다. 이를 보간(interpolation)이라고 한다. 한 벡터에서 다른 벡터로 의미 있는 변환은 GAN이 어떤 내재된 구조를 학습했다는 것을 보여준다.
최근 핀란드 엔비디아 팀이 발표한 논문에서 중요한 내용에 대해 알아보자.
고해상도 층으로 점진적 증대와 단계적 도입
고해상도 층으로의 점진적 증대란 우리가 구글 어스를 빠르게 스크롤할 때 주변 지형의 해상도가 낮았었는데 갑자기 증가해서 또렷하게 보이는 경험을 한 적이 있을 것이다. 이렇게 갑작스럽게 나타나는 대신에 점진적으로 부드럽게 천천히 복잡한 모습이 확대되어 나오는 것이 바람직하다고 볼 수 있다. 이것을 기술적으로 말하면 신경망은 저해상도 합성곱 층에서 출발하여 훈련하면서 여러 개의 고해상도 층으로 이동한다. 처음부터 고해상도 층을 사용하면 손실 함수의 공간을 탐색하기 어렵다. 따라서 간단한 것부터 시작해서 더 복잡하게 만든다.
이런 시나리오의 문제점은 한 번에 하나의 층을 추가할 때(4x4 에서 8x8로 이동할 때) 여전히 훈련에 큰 영향을 끼친다는 것이다. 대신 ProGAN 저자들은 다음과 같은 층을 추가하여 시스템이 고해상도에 적응할 시간을 주었다.

또한 이 해상도를 바로 도입하지 않고 0과 1 사이의 알파 파라미터를 사용해 고해상도에 새 층을 단계적으로 도입한다. 알파는 이전 층을 업스케일한 층과 전치 합성곱으로 커진 층을 얼마나 사용할지 결정한다. D에서는 0.5배로 감소시켜 판별자에게 훈련된 층에 부드럽게 주입되게 한다. 새로운 층에 확신이 생기면 (c) 처럼 32 x 32 를 남기고 적절하게 32 x 32 크기를 훈련한 후 대시 해상도를 늘린다.
구현
점진적 증가와 단계적 도입, 미니배치 표준 편차, 균등 학습률에 대해 각각 개별적인 코드로 구현하여 이해해보자. 먼저 필요한 라이브러리를 로드한다.
import tensorflow as tf
import tensorflow.keras as K
점진적 증가와 단계적 도입
def upscale_layer(layer, upscale_factor):
'''
upscale_factor 만큼 층을 업스케일 한다.
텐서 크기는 [group,height,width,channels]이다.
'''
height, width = layer.get_shape()[1:3]
size = (upscale_factor*height, upscale_factor*width)
upscale_layer = tf.image.resize_nearest_neighbor(layer, size)
return upscale_layer
def smoothly_merge_last_layer(list_of_layers, alpha):
'''
임계값 알파를 기반으로 층을 부드럽게 합친다.
이 함수는 모든 층이 이미 RGB로 바뀌었다고 가정한다.
생성자를 위한 함수이다.
: list_of_layers : 해상도 순서대로 정렬된 텐서 리스트
: alpha : (0,1) 사이의 실수
'''
last_fully_trained_layer = list_of_layers[-2] # 업스케일링을 위해 끝에서 두 번째 층을 선택한다.
last_layer_upscaled = upscale_layer(last_fully_trained_layer, 2) # 마지막으로 훈련된 층을 업스케일링한다.
larget_native_layer = list_of_layers[-1] # 새로 추가된 층은 아직 완전히 훈련되지 않았다.
assert larget_native_layer.get_shape() == last_layer_upscaled.get_shape() # 합치기 전에 층 크기가 같은지 확인한다.
new_layer = (1-alpha)*last_layer_upscaled + alpha*larget_native_layer # 곱셈은 브로드캐스팅되어 수행된다.
return new_layer
미니배치 표준편차
모드 붕괴는 GAN이 몇 개의 좋은 샘플만 만드는 방법이나 이들의 순서를 조금 바꾸기만 할 때 발생한다. 일반적으로 한 사람의 얼굴이 아니라 실제 데이터셋에 있는 모든 사람의 얼굴을 생성하길 원하낟. 따라서 케라스와 연구진은 판별자에게 샘플이 충분히 다양한지 알려주는 방법을 만들었다.
간단하게 요약하면 판별자를 위해 한 개의 스칼라 통계 값을 추가로 계산한다. 이 통계 값은 생성자가 만들거나 실제 데이터에서 온 미니배치에 있는 모든 픽셀의 표준편차이다. 판별자는 평가할 배치 이미지에서 표준편차가 낮다면 이 이미지는 아마 가짜라고 학습한다. 진짜 데이터는 분산이 크기 때문이다. 따라서 생성자는 판별자를 속이기 위해서는 생성 샘플의 분산을 늘릴 수밖에 없다.
def minibatch_std_layer(layer, group_size = 4):
'''
층의 미니배치 표준편차를 계산한다.
층의 데이터타입은 float32로 가정한다. 그렇지 않으면 타입 변환이 필요하다.
'''
group_size = K.backend.minimum(group_size, K.backend.shape(layer)[0]) # 미니배치는 group_size로 나눌 수 있거나 group_size 보다 같거나 작아야 한다.
shape = list(K.int_shape(input)) # 간단하게 쓰기 위해 크기 정보를 따로 저장한다. 그래피 실행 전에는 일반적으로 배치 차원이 None이기 때문에 tf.shape에서 이 크기를 얻는다.
shape[0] = tf.shape(input)[0]
minibatch = K.backend.reshape(layer,
(group_size, -1, shape[1], shape[2], shape[3])) # 미니배치 수준에서 연산하기 위해 크기를 바꾼다. 이 코드는 층이 [그룹(G), 미니배치(M), 너비(W), 높이(H), 채널(C)]라 가정한다.
minibatch -= tf.reduce_mean(minibatch, axis=0, keepdims=True) # [MWHC]의 평균을 계산한다.
minibatch = tf.reduce_mean(K.backend.square(minibatch), axis=0) # [MWHC]의 분산을 계산한다.
minibatch = K.backend.square(minibatch + 1e-8) # [MWHC]의 표준편차를 계산한다.
minibatch = tf.retuce_mean(minibatch, axis=[1,2,3], keepdims=True) # 특성 맵을 평균하여 [M,1,1,1] 픽셀을 얻는다.
minibatch = K.backend.tile(minibatch, (group_size, 1, shape[2], shape[3])) # 스칼라 값을 그룹과 픽셀에 맞게 변환한다.
return K.backend.concatenate([layer, minibatch], axis=1) # 새로운 특성 맵을 추가한다.
균등 학습률
케라스와 연구진은 간단한 표준정규분포 초기화를 사용하고 실행 시 층마다 가중치 스케일을 조정하였다. 이미 Adam이 이렇게 하고있지만 Adam은 파라미터마다 학습률이 다를 수 있다. 하지만 Adam은 파라미터의 표준편차를 추정하여 역전파된 그레디언트를 조정한다. 따라서 파라미터의 스케일은 이 업데이트와 독립적으로 유지된다. Adam은 방향에 따라 다른 학습률을 가지지만 한 차원이나 한 특성이 주어진 미니배치에서 얼마나 다양한지 나타내는 다이내믹 레인지를 항상 고려하지는 않는다.
def equalize_learning_rate(sahpe, gain, fan_in = None):
'''
He 초기화의 상수로 모든 층의 가중치를 조정하여
특성마다 각기 다른 다이내믹 레인지를 가지도록 분산을 맞춘다.
shape : 텐서(층)의 크기: 각 층의 차원
예를 들어, [4,4,48,3]. 이 경우 [커널 크기, 커널 크기, 필터 개수, 특성맵]이다.
하지만 구현에 따라 조금 다를 수 있다.
gain : 일반적으로 sqrt(2)
fan_in : 세이비어/He 초기화에서 입력 연결 개수
'''
if fan_in is None:
fan_in = np.prod(shape[:-1]) # 기본 값은 특성 맵 차원을 제외하고 shape의 모든 차원을 곱한다. 이를 통해 뉴런마다 입력 연결 개수를 얻는다.
std = gain / K.sqrt(fan_in)
wscale = K.constant(std, name='wscale', dtype = np.float32) # 조정을 위한 상수를 만든다.
adjusted_weights = K.get_value('layer', shape = shape, # 가중치 값을 얻어 브로드캐스팅으로 wscale을 적용한다.
initializer = tf.initializers.random_normal()) * wscale
return adjusted_weights
가중치 값을 -1과 1 사이로 제한하는 것이 여기에서는 대부분 좋은 결과를 만들지만 이 방법이 다른 곳에도 일반화할 수 있다는 의미는 아니다.
생성자의 픽셀별 특성 정규화
훈련의 안정성을 위해 특성을 정규화해야 하는 이유는 엔비디아 저자들이 경험적으로 특성 값이 갑자기 커지는 것이 훈련이 발산되는 초기 신호라는 것을 발견했기 때문이다.
대부분의 신경망은 어떤 형태의 정규화를 사용한다. 일반적으로 배치 정규화나 비슷한 다른 버전을 사용한다. 하지만 배치 정규화와 가상 배치 정규화가 작동하려면 개별 샘플을 평균할 수 있도록 미니배치 크기가 커야 한다. 그냥 표준 배치 정규화를 사용하면 배치 정규화는 높은 해상도를 출력하려면 너무 많은 메모리가 필요하다. 그럼 이제 픽셀별 특성 정규화에 대해 설명하겠다.
픽셀별 특성 정규화
각 특성 맵에 대해 다음을 반복한다.
- 위치 (x,y)에서 특성맵(fm)의 픽셀 값을 얻는다.
- 각 (x,y)에 대해 다음 같은 벡터를 만든다.
- v(0,0) = [fm(1)의 (0,0) 값, fm(2)의 (0,0) 값, ... , fm(n)의 (0,0) 값]
- v(0,1) = [fm(1)의 (0,1) 값, fm(2)의 (0,1) 값, ... , fm(n)의 (0,1) 값]
- ....
- v(n,n) = [fm(1)의 ( n,n ) 값, fm(2)의 ( n,n ) 값, ... , fm(n)의 ( n,n ) 값]
- 단위 노름을 가지도록 단계 2에서 정의한 각 벡터 v(i,i)를 정규화하고 이를 n(i,i)fkrh qnfmsek.
원본 텐서 크기로 다음 층에 전달한다.
이 항은 생성자에만 적용된다. 두 네트워크가 경쟁할 때만 활성화 크기가 폭주로 이어지기 때문이다.
def pixelwise_feat_norm(inputs, **kwargs):
'''
크리젭스키와 연구진이 2012년 논문에 제안한 픽셀별 특성 정규화
: inputs : 케라스 / TF 층
'''
normalization_constant = K.backend.sqrt(K.backend.mean(
inputs**2, axis = -1, keepdims = True) + 1.0e-8)
return inputs / normalization_constant
주요 혁신 요약
네 가지 방법으로 어떻게 GAN 훈련을 향상시킬 수 있는지 보았다. 하지만 훈련 효과를 기반으로 하지 않으면 각 효과를 따로 떼어서 설명하기가 힘들다. 고맙게도 논문의 저자들은 이해에 도움이 될만한 자료들을 제공해주었다.

ProGAN 논문 저자들은 SWD를 사용한다. 이 값은 작을수록 좋다. 5장에서 와서스테인거리는 두 분포가 비슷해지기 위해 이동해야 할 확률 질량의 양이기 때문에 작은 와서스테인 거리는 좋은 결과를 나타낸다. SWD는 이 거리를 최소화하기 위한 실제 데이터와 생성 샘플의 조각을 의미한다.
이 표에서 배울 수 있는 한 가지는 미니배치가 잘 작동하지 않는다는 것이다. 메가픽셀 크기의 해상도에서 GPU 메모리에 많은 이미지를 적재할 충분한 램이 없기 때문이다. 작은 미니배치를 사용해야 하지만 전반적으로 성능이 낮아진다. 미니배치 크기를 더 줄이면 훈련은 더욱 어려워진다.
텐서플로 허브를 사용한 실습
구글은 최근에 텐서플로 익스텐디드의 일부분으로 소프트웨어 엔지니어링의 모범 사례 구현을 머신러닝 분야로 확장하기 위해, 텐서플로 허브라는 모델과 코드 저장소를 만들었다.
허브 모듈을 임포트하고 적절한 URL로 호출하면 텐서플로는 스스로 모델을 다운로드하고 임포트하여 시작할 수 있다.
이제 실습해보자.
import matplotlib.pyplot as plt
import tensorflow as tf
import tensorflow_hub as hub
module = hub.KerasLayer("https://tfhub.dev/google/progan-128/1")# TFHub에서 ProGAN을 임포트한다.
latent_dim = 512
latent_vector = tf.random.normal([1, latent_dim], seed = 1337) # seed 값을 바꾸면 다른 얼굴을 생성한다.
interpolated_images = module(latent_vector) # 모듈을 사용해 잠재 공간에서 이미지를 생성한다.
plt.imshow(interpolated_images.numpy().reshape(128,128,3))
plt.show()

GAN은 많은 애플리케이션에 사용될 수 있다. 유방암 분야나 얼굴 이미지 생성뿐만이 아니다. 2018년 7우러 마지막에 공개된 의료 GAN 애플리케이션은 62개이다. 모두 ProGAN을 사용하진 않았다. 일반적으로 GAN은 많은 연구 분야에서 크게 발전하고 있지만 적용하기는 종종 어렵다.
이 장에서 소개한 모든 기술은 점진적으로 더 복잡한 모델을 만든느 일반적인 방법으로 GAN 문제를 해결한다. 앞으로는 GAN이 할 수 있는 일이 무엇인지에 대해 더 고민해보도록 하자.
'Book > GAN In Action' 카테고리의 다른 글
| 8장. CGAN (0) | 2024.08.10 |
|---|---|
| 7장. SGAN (0) | 2024.08.08 |
| 5장. GAN 평가의 어려움 (0) | 2024.08.06 |
| 4장. DCGAN (0) | 2024.08.01 |
| 3장. GAN 구현하기 (0) | 2024.07.30 |