케라스 창시자에게 배우는 딥러닝/ 생성 모델을 위한 딥러닝

  • 사람의 사고를 모방하려는 인공 지능의 잠재력은 이미지 인식 같은 소극적인 작업이나 자율 주행처럼 상당히 반응적인 작업을 넘어서고 있다. 인공 지능은 창작 활동으로 범위를 넓히고 있다.
  • (사례 생략)
  • 사실 지금까지 본 AI 예술 작품의 품질은 낮다. AI가 실제 작가나 화가, 작곡가와 견줄 수준은 못 된다. 사람을 대신한다는 것은 늘 핵심을 벗어난 생각이다.
    • 인공 지능이 사람의 지능을 어떤 다른 것으로 대체하는 것이 아니라 우리 생활과 일에 지능을 더한다. 이 지능은 다른 종류의 지능이다.
    • 여러 분야에서 특히 예술에서 AI는 사람의 능력을 증가시키는 도구로 사용될 것이다. 즉, 인공 지능이 아니라 확장된 지능(augmented intelligence)이다.
  • 예술 창작의 대부분은 간단한 패턴 인식과 기술로 만들어진다. 많은 사람은 이런 과정에 관심이 없거나 심지어 불필요하다고 생각하는데, 여기에 AI가 필요하다.
    • 사람의 지각, 언어, 예술 작품은 모두 통계적 구조를 가진다. 딥러닝 알고리즘은 이 구조를 학습하는데 뛰어나다.
    • 머신 러닝 모델은 이미지, 음악, 글의 통계적 잠재 공간(latent space)을 학습할 수 있다. 그 다음 이 공간에서 샘플을 뽑아 새로운 예술 작품을 만들 수 있다. 이 작품은 모델이 훈련 데이터에서 본 것과 비슷한 특징을 가질 것이다.
    • 당연히 이런 샘플링 자체가 예술 창작이라고 보기는 어렵다. 그냥 수학 연산에 불과하다.
  • 숙련된 예술가가 사용하면 알고리즘 창작은 의미 있고 아름다운 것으로 바뀔 수 있다. 잠재 공간 샘플링은 예술가의 능력을 높이는 붓이 될 수 있다.
  • (이하 생략)

LSTM으로 텍스트 생성하기

생성 RNN의 간단한 역사

  • 2014년 후반 머신 러닝 공동체에서도 소수의 사람들만이 LSTM 이란 용어를 알았다. 순환 네트워크를 사용하여 시퀀스 데이터를 성공적으로 생성한 애플리케이션은 2016년이 되어서야 주류가 되기 시작했다.
    • 하지만 이 기술은 1997년 LSTM 알고리즘이 개발된 이래 역사가 꽤 오래 되었다. 이 알고리즘은 초기에 글자를 하나씩 생성하는데 사용되었다.
  • 2002년 더글라스 에크(Douglas Eck)는 스위스의 슈미드후버(Schmidhuber)의 연구실에서 LSTM을 음악 생성에 처음 적용하여 가능성 있는 결과를 얻었다.
    • 에크는 현재 구글 브레인의 연구원으로 2016년에는 마젠타(Magenta)란 새로운 연구 그룹을 싲가해서 최신 딥러닝 기술을 사용하여 멋진 음악을 만드는 것에 집중하고 있다.
  • 2000년대 후반과 2010년대 초반에 알렉스 그레이브스(Alex Graves)는 순환 네트워크를 사용하여 시퀀스 데이터를 생성하는데 아주 중요한 선구적인 일을 했다.
    • 특히 2013년에 펜 위치를 기록한 시계열 데이터를 사용하여 순환 네트워크와 완전 연결 네트워크를 혼합한 네트워크로 사람이 쓴 것 같은 손글씨를 생성했으며, 이 작업이 전환점이 되었다.
  • (이하 생략)

시퀀스 데이터를 어떻게 생성할까?

  • 딥러닝에서 시퀀스 데이터를 생성하는 일반적인 방법은 이전 토큰을 입력으로 사용해서 시퀀스의 다음 1개 또는 몇 개의 토큰을 (RNN이나 컨브넷으로) 예측하는 것이다.
    • 예컨대 “the cat is on ma”란 입력이 주어지면 다음 글자인 타깃 “t”를 예측하도록 네트워크를 훈련한다.
    • 텍스트 데이터를 다룰 때 토큰은 보통 단어 또는 글자이다. 이전 토큰들이 주어졌을 때 다음 토큰의 확률을 모델링할 수 있는 네트워크를 언어 모델(language model)이라고 부른다. 언어 모델은 언어의 통계적 구조인 잠재 공간을 탐색한다.
  • 언어 모델을 훈련하고 나면 이 모델에서 샘플링을 할 수 있다(새로운 시퀀스를 생성한다).
    • 초기 텍스트 문자열을 주입하고 (조건 데이터(conditioning data)라고 부른다) 새로운 글자나 단어를 생성한다. (한 번에 여러 개의 토큰을 생성할 수도 있다).
    • 생성된 출력은 다시 입력 데이터로 추가된다. 이 과정을 여러 번 반복한다. 이런 반복을 통해 모델이 훈련한 데이터 구조가 반영된 임의의 길이를 가진 시퀀스를 생성할 수 있다. 사람이 쓴 문장과 거의 비슷한 시퀀스를 만든다.
    • 이 절의 예제는 LSTM 층을 사용한다. 텍스트 말뭉치(corpus)에서 N개의 글자로 이루어진 문자열을 추출하여 주입하고 N+1번째 글자를 예측하도록 훈련한다.
    • 모델의 출력은 출력 가능한 모든 글자에 해당하는 소프트맥스 값이다. 즉, 다음 글자의 확률 분포이다. 이 LSTM을 글자 수준의 신경망 언어 모델(character-elvel neural language model)이라고 부른다.

샘플링 전략의 중요성

  • 텍스트를 생성할 때 다음 글자를 선택하는 방법이 아주 중요하다.
    • 단순한 방법은 항상 가장 높은 확률을 가진 글자를 선택하는 탐욕적 샘플링(greedy sampling)이다. 이 방법은 반복적이고 예상 가능한 문자열을 만들기 때문에 논맂거인 언어처럼 보이지 않는다.
    • 좀 더 흥미로운 방법을 선택하면 놀라운 선택이 만들어지는데, 다음 글자의 확률 분포에서 샘플링하는 과정에 무작위성을 주입하는 방법이다. 이를 확률적 샘플링(stochastic sampling)이라고 부른다. (머신 러닝에서 확률적(stochastic)이란 뜻은 무작위(random)하다는 뜻이다.)
    • 이런 방식을 사용할 경우 ‘e’가 다음 글자가 될 확률이 0.3이라면 모델이 30% 정도는 이 글자를 선택한다.
    • 탐욕적 샘플링을 확률적 샘플링으로 설명할 수도 있다. 한 글자의 확률이 1이고 나머지 글자는 모두 0인 확률 분포를 보이는 경우다.
  • 모든 소프트맥스 출력은 확률적 샘플링에 사용하기 좋다. 이따금 샘플링 될 것 같지 않은 글자를 샘플링한다. 훈련 데이터에는 없지만 실제 같은 새로운 단어를 만들어 재미있고 창의적으로 보이는 문장을 생성한다.
    • 이 전략에는 한 가지 문제가 있는데, 샘플링 과정에서 무작위성의 양을 조절할 방법이 없다.
  • 왜 무작위성이 크거나 작아야 할까? 극단적인 경우를 생각해 보자.
    • 균등 확률 분포에서 다음 글자를 추출하는 완전한 무작위 샘플링이 있다. 모든 글자의 확률은 같다. 이 구조는 무작위성이 최대이다. 다른 말로 하면 이 확률 분포는 최대의 엔트로피를 가진다. 당연하게 흥미로운 것들을 생산하지 못한다.
    • 반대로 탐욕적 샘플링은 무작위성이 없기 때문에 흥미로운 것을 전혀 만들지 못한다. 탐욕적 샘플링의 확률 분포는 최소의 엔트로피를 가진다.
    • 모델의 소프트맥스 출력인 ‘실제’ 확률 분포에서 샘플링하는 것은 이 두 극단의 중간에 위치해 있다. 중간 지점에는 시도해 볼만한 더 높거나 낮은 엔트로피가 많다.
    • 작은 엔트로피는 예상 가능한 구조를 가진 시퀀스를 생성한다. (더 실제처럼 보인다) 반면 높은 엔트로피는 놀랍고 창의적인 시퀀스를 만든다.
    • 생성 모델에서 샘플링할 때 생성 과정에서 무작위성의 양을 바꾸어 시도해 보는 것이 좋다. 흥미는 매우 주관적이므로 최적의 엔트로피 값을 미리 알 수 없기 떄문이다. 얼마나 흥미로운 데이터를 생성할 것인지는 결국 사람이 판단해야 한다.
  • 샘플링 과정에서 확률의 양을 조절하기 위해 소프트맥스 온도(softmax temperature)라는 파라미터를 사용한다. 이 파라미터는 샘플링에 사용되는 확률 분포의 엔트로피를 나타낸다. 얼마나 놀라운 또는 예상되는 글자를 선택할지 결정한다.
    • temperature 값이 주어지면 다음과 같이 가중치를 적용하여 (모델의 소프트맥스 출력인) 원본 확률 분포에서 새로운 확률 분포를 계산한다.
import numpy as np

def reweight_distribution(original_distribution, temperature=0.5):
distribution = np.log(original_distribution) / temperature
distribution = np.exp(distribution)
return distribution / np.sum(distribution)
  • 높은 온도는 엔트로피가 높은 샘플링 분포를 만들어 더 놀랍고 생소한 데이터를 생성한다. 반면 낮은 온도는 무작위성이 낮기 때문에 예상할 수 있는 데이터를 생성한다.

글자 수준의 LSTM 텍스트 생성 모델 구현

  • 이런 아이디어를 케라스로 구현해 보자. 먼저 언어 모델을 학습하기 위해 많은 텍스트 데이터가 필요하다. ‘위키피디아’나 <반지의 제왕>처럼 아주 큰 텍스트 파일이나 텍스트 파일의 묶음을 사용할 수 있다.
    • 여기서는 19세기 후반 독일의 철학자 니체의 영어로 번역된 글을 사용하겠다.
    • 학습할 언어 모델은 일반적인 영어 모델이 아니라 니체의 문체와 특정 주제를 따르는 모델일 것이다.

데이터 전처리

  • 먼저 말뭉치를 내려받아 소문자로 바꾼다.
import keras
import numpy as np

path = keras.utils.get_file('nietzsche.txt', origin='https://s3.amazonaws.com/text-datasets/nietzsche.txt')
text = open(path).read().lower()
print('말뭉치 크기:', len(text))
  • 그 다음 maxlen 길이를 가진 시퀀스를 중복하여 추출한다. 추출된 시퀀스를 원-핫 인코딩으로 변환하고 크기가 (sequences, maxlen, unique_characters)인 3D 넘파이 배열 x로 합친다.
    • 동시에 훈련 샘플에 상응하는 타깃을 담은 배열 y를 준비한다. 타깃은 추출된 시퀀스 다음에 오는 원-핫 인코딩된 글자이다.
maxlen = 60
step = 3

sentences = []
next_chars = []

for i in range(0, len(text) - maxlen, step):
sentences.append(text[i: i+maxlen])
next_chars.append(text[i+maxlen])
print('시퀀스 개수:', len(sentences))

chars = sorted(list(set(text)))
print('고유한 글자:', len(chars))
char_indices = dict((char, chars.index(char)) for char in chars)

print('벡터화...')

x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)

for i, sentence in enumerate(sentences):
for t, char in enumerate(sentence):
x[i, t, char_indices[char]] = 1
y[i, char_indices[next_chars[i]]] = 1

네트워크 구성

  • 이 네트워크는 하나의 LSTM층과 그 뒤에 Dense 분류기가 뒤따른다. 분류기는 가능한 모든 글자에 대한 소프트맥스 출력을 만든다.
    • 순환 신경망이 시퀀스 데이터를 생성하는 유일한 방법은 아니다. 최근에는 1D 컨브넷도 이런 작업에 아주 잘 들어맞는다는 것이 밝혀졌다.
from keras import layers

model = keras.models.Sequential()
model.add(layers.LSTM(128, input_shape=(maxlen, len(chars))))
model.add(layers.Dense(len(chars), activation='softmax'))
  • 타깃이 원-핫 인코딩 되어 있기 때문에 모델을 훈련하기 위해 categorical_crossentropy 손실을 사용한다.
optimizer = keras.optimizers.RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)

언어 모델 훈련과 샘플링

  • 훈련된 모델과 시드(seed)로 쓰일 간단한 텍스트가 주어지면 다음과 같이 반복하여 새로운 텍스트를 생성할 수 있다.
    1. 지금까지 생성된 텍스트를 주입하여 모델에서 다음 글자에 대한 확률 분포를 뽑는다.
    2. 특정 온도로 이 확률 분포의 가중치를 조정한다.
    3. 가중치가 조정된 분포에서 무작위로 새로운 글자를 샘플링하낟.
    4. 새로운 글자를 생성된 텍스트의 끝에 추가한다.
  • 다음 코드는 모델에서 나온 원본 확률 분포의 가중치를 조정하고 새로운 글자의 인덱스를 추출한다. (샘플링 함수이다)
def sample(preds, temperature=1.0):
preds = np.asarray(preds).astype('float64')
preds = np.log(preds) / temperature
exp_preds = np.exp(preds)
preds = exp_preds / np.sum(exp_preds)
probas = np.random.multinomial(1, preds, 1)
return np.argmax(probas)
  • 마지막으로 다음 반복문은 반복적으로 훈련하고 텍스트를 생성한다. 에포크마다 학습이 끝난 후 여러 가지 온도를 사용하여 텍스트를 생성한다. 이렇게 하면 모델이 수렴하면서 생성된 텍스트가 어떻게 진화하는지 볼 수 있다.
    • 온도가 샘플링 전략에 미치는 영향도 보여준다.
import random
import sys

random.seed(42)
start_index = random.randint(0, len(text) - maxlen - 1)

for epoch in range(1, 60):
print('에포크', epoch)
model.fit(x, y, batch_size=128, epochs=1)

seed_text = text[start_index: start_index + maxlen]
print('--- 시드 텍스트: "' + seed_text + '"')

for temperature in [0.2, 0.5, 1.0, 1.2]:
print('------ 온도: ', temperature)
generated_text = seed_text
sys.stdout.write(generated_text)

for i in range(400):
sampled = np.zeros((1, maxlen, len(chars)))

for t, char in enumerate(generated_text):
sampled[0, t, char_indices[char]] = 1.

preds = model.predict(sampled, verbose=0)[0]
next_index = sample(preds, temperature)
next_char = chars[next_index]

generated_text += next_char
generated_text = generated_text[1:]

sys.stdout.write(next_char)
sys.stdout.flush()
print()
  • 시드 텍스트에 대한 온도별 결과는 다음과 같다.
--- 시드 텍스트: "the slowly ascending ranks and classes, in which,
through fo"
------ 온도: 0.2 the slowly ascending ranks and classes, in which, through for the stand of the stand and the spirit of the same that a soul the stand the stand of the stand of the same and decession and and the same and some soul the stand the stand of the standards and in the sensition of the most stand and all the stand the stand and the spirit of the stand in the possible of the same and power and in the stand of the same and the stand the stand and the same and the sa
------ 온도: 0.5 the slowly ascending ranks and classes, in which, through for the has so indigent to the prescious of the wasting to suffer of the communness, suffer the fact of the resurtion of the inger of she is a states and stand and stages of the man with lemmandent them and has and depromentarian to be the revering in bady and all will perhaps a sentiment of depress, our personal to be as he is a sense of all and opprenes the last thought of mind and same to becenti
------ 온도: 1.0 the slowly ascending ranks and classes, in which, through for the eastd have itherted athand reed, that he is wal a bork ummentation: on the metametistical, of disconvances, to the world will highert real strong comes things, fur converses, and prepaintary uman that he apan act to a be delighther ead and "only and will a good. the way parition of philosophing in single this istenate knowledge; peomphedinges nimchs, and the east and the the here es innecess
------ 온도: 1.2 the slowly ascending ranks and classes, in which, through for for our your partnog of imvelign--this can them, and realby" lubed there crimas consimerable dingmedsmalitatif powerom upon curwous, (any degr"bancenels of tle is enjoyede indoubble, ? a mourlour le oncus from stand whithrapting. envy; he tame world .fracking. an thoresises is sulngee, wno hights potheres to because perfecesamentated. in the unplly a lmee one high t sue brie oor noh dombine,
  • 여기서 볼 수 있듯이 낮은 온도는 아주 반복적이고 예상되는 텍스트를 만든다. 하지만 국부적인 구조는 실제와 매우 같다. 특히 모든 단어(단어는 글자의 지역 패턴으로 이루어진다)가 실제 영어 단어이다.
    • 높은 온도에서 생성된 텍스트는 아주 흥미롭고 놀라우며 창의적이기도 하다. 이따금 꽤 그럴싸해 보이는 완전히 새로운 단어를 창조한다.
    • 높은 온도에서는 국부적인 구조가 무너지기 싲가한다. 대부분의 단어가 어느 정도 무작위한 문자열로 보인다.
    • 확실히 이 네트워크에서는 텍스트 생성에 가장 좋은 온돈느 0.5이다.
    • 항상 다양한 샘플링 전략으로 실험해 보어야 한다. 학습된 구조와 무작위성 사이에 균형을 잘 맞추면 흥미로운 것을 만들 수 있다.
  • 더 많은 데이터에서 크고 깊은 모델을 훈련하면 이것보다 훨씬 논리적이고 실제와 같은 텍스트 샘플을 생성할 수 있다. 당연히 우연이 아닌 의미 있는 텍스트가 생성된다고 기대하지 말라.
    • 글자를 연속해서 나열하기 위한 통계 모델에서 데이터를 샘플링한 것뿐이다.
    • 언어는 의사소통의 수단이다. 의사소통이 의미하는 것과 의사소통이 인코딩된 메시지의 통계 구조 사이는 차이가 있다.
    • 이 차이를 검증하기 위해 다음과 같은 사고 실험을 해보자. 컴퓨터가 대부분의 디지털 통신에서 하는 것처럼 사람의 언어가 의사소통을 압축하는데 더 뛰어나다면 어떨까? 언어 의미가 줄지는 않겠지만 고유한 통계 구조가 사라질 것이다. 이는 방금과 같은 언어 모델을 학습하는 것을 불가능하게 만든다.

정리

  • 이전의 토큰이 주어지면 다음 토큰(들)을 예측하는 모델을 훈련하여 시퀀스 데이터를 생성할 수 있다.
  • 텍스트의 경우 이런 모델을 언어 모델이라고 한다. 단어 또는 글자 단위 모두 가능하다.
  • 다음 토큰을 샘플링할 때 모델이 만든 출력에 집중하는 것과 무작위성을 주입하는 것 사이에 균형을 맞추어야 한다.
  • 이를 위해 소프트맥스 온도 개념을 사용한다. 항상 다양한 온도에서 실험해서 적절한 값을 찾아야 한다.

딥드림

  • 딥드림(DeepDream)은 합성곱 신경망이 학습한 표현을 사용하여 예술적으로 이미지를 조작하는 기법이다. 2015년 여름 구글이 카페(Caffe) 딥러닝 라이브러리를 사용하여 구현한 것을 처음 공개했다. (텐서플로가 공개 되기 몇 달 전이다)
    • 딥드림이 생성한 몽환적인 사진은 순식간에 인터넷에 센세이션을 일으켰다. 알고리즘으로 변경된 환상적인 인공물, 새 깃털, 강아지 눈이 가득차 있다.
    • 이 딥드림은 다양한 종류의 강아지와 새가 있는 ImageNet 데이터셋에서 훈련된 컨브넷을 사용했다.

  • 딥드림 알고리즘은 5장에서 소개한 컨브넷을 거꾸로 실행하는 컨브넷 필터 시각화 기법과 거의 동일하다. 컨브넷 상위 층에 있는 특정 필터의 활성화를 극대화하기 위해 컨브넷의 입력에 경사 상승법을 적용했다. 몇 개의 사소한 차이를 빼면 딥드림도 동일한 아이디어를 사용한다.
    • 딥드림에서는 특정 필터가 아니라 전체 층의 활성화를 최대화한다. 한꺼번에 많은 특성을 섞어 시각화 한다.
    • 빈 이미지나 노이즈가 조금 있는 입력이 아니라 이미 가지고 있는 이미지를 사용한다. 그 결과 기존 시각 패턴을 바탕으로 이미지의 요소들을 다소 예술적인 스타일로 왜곡시킨다.
    • 입력 이미지는 시각 품질을 높이기 위해 여러 다른 스케일(옥타브(octave)라고 부른다)로 처리한다.

케라스 딥드림 구현

  • ImageNet에서 훈련한 컨브넷을 가지고 싲가하자. 케라스에는 이렇게 사용할 수 있는 컨브넷이 많다. VGG16, VGG19, Xception, ResNet50 등이다.
    • 이것 중 어느 것을 사용해도 딥드림을 구현할 수 있다. 당연히 어떤 컨브넷을 선택했느냐에 따라 시각화에 영향을 미친다. 각 컨브넷의 구조가 학습한 특성이 다르기 때문이다.
    • 원래 딥드림에서 사용한 컨브넷은 인셉션 모델이다. 실제로 인셉션이 멋진 딥드림 이미지를 잘 만든다. 여기서도 인셉션 V3 모델을 사용하겠다.
from keras.applications import inception_v3
from keras import backend as K

K.set_learning_phase(0)

model = inception_v3.InceptionV3(weights='imagenet', include_top=False)
  • 그다음 손실을 계산한다. 경사 상승법으로 최대화할 값이다.
    • 5장 필터 시각화에서 특정 층의 필터 값을 최대화했다. 여기서는 여러 층에 있는 모든 필터 활성화를 동시에 최대화한다.
    • 특별히 상위 층에 있는 활성화의 L2 노름에 대한 가중치 합을 최대화 하겠다.
    • 정확히 어떤 층들을 선택했는지에 따라 (당연히 최종 손실에 기여한 정도에 따라) 만들어 내는 시각 요소에 큰 영향을 미친다.
    • 어떤 층을 선택할지 파라미터로 손쉽게 바꿀 수 있어야 좋다. 하위 층은 기하학적인 패턴을 만들고 상위 층은 ImageNet에 있는 클래스로 보이는 시각 요소를 만든다. (예컨대 새나 강아지)
    • 먼저 임의로 4개의 층을 선택해 보겠다. 나중에 달느 설정을 다양하게 시도해 보는 것이 좋다.
layer_contributions = {
'mixed2': 0.2,
'mixed3': 3.,
'mixed4': 2.,
'mixed5': 1.5,
}
  • 이제 손실 텐서를 정의하겠다. 앞선 코드에서 선택한 츠으이 활성화에 대한 L2 노름의 가중치 합이다.
layer_dict = dict([(layer.name, layer) for layer in model.layers])

loss = K.variable(0.)

for layer_name in layer_contributions:
coeff = layer_contributions[layer_name]
activation = layer_dict[layer_name].output

scaling = K.prod(K.cast(K.shape(activation), 'float32'))
val = coeff * K.sum(K.square(activation[:, 2:-2, 2:-2, :])) / scaling
loss = loss + val
  • 그 다음 경사 상승법 과정을 준비한다.
dream = model.input

grads = K.gradients(loss, dream)[0]
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)

outputs = [loss, grads]
fetch_loss_and_grads = K.function([dream], outputs)

def eval_loss_and_grads(x):
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1]
return loss_value, grad_values

def gradient_ascent(x, iterations, step, max_loss=None):
for i in range(iterations):
loss_value, grad_values = eval_loss_and_grads(x)
if max_loss is not None and loss_value > max_loss:
break
print('...', i, '번째 손실:', loss_value)
x += step * grad_values
return x
  • 마지막으로 진짜 딥드림 알고리즘이다. 먼저 이미지를 처리하기 위한 스케일(옥타브라고도 부른다) 리스트를 정의한다. 스케일은 이전 스케일보다 1.4배 크다(40% 증가한다) 작은 이미지로 시작해서 점점 크기를 키운다. (아래 그림 참조)

  • 가장 작은 것에서 가장 큰 스케일까지 연속적인 각 단계에서 정의한 손실이 최대화되도록 경사 상승법을 수행한다. 경사 상승법이 실행된 후 이미지 크기를 40% 증가시킨다.
  • 스케일이 연속적으로 증가시키면서 (점점 뭉개지거나 픽셀 경계가 나타나므로) 이미지 상세를 많이 잃지 않도록 간단한 기교를 사용한다.
    • 스케일을 늘린 후 이미지에 손실된 디테일을 재주입한다. 원본 이미지가 크기를 늘렸을 때 어땠는지 알기 때문에 가능하다.
    • 작은 이미지 크기 S와 큰 이미지 크기 L이 주어지면 크기 L로 변경된 원본 이미지와 크기 S로 변경된 원본 이미지 사이의 차이를 계산한다.
    • 이 차이가 S에서 L로 변경되었을 때 잃어버린 디테일이다.
import scipy
from keras.preprocessing import image

def resize_img(img, size):
img = np.copy(img)
factors = (1, float(size[0]) / img.shape[1], float(size[1]) / img.shape[2], 1)
return scipy.ndimage.zoom(img, factors, order=1)

def save_img(img, fname):
pil_img = deprocess_img(np.copy(img))
image.save_img(fname, pil_img)

def preprocess_image(image_path):
img = image.load_img(image_path)
img = image.img_to_array(img)
img = np.expand_dims(img, axis=0)
img = inception_v3.preprocess_input(img)
return img

def deprocess_img(x):
if K.image_data_format() == 'channels_first':
x = x.reshape((3, x.shape[2], x.shape[3]))
x = x.transpose((1, 2, 0))
else:
x = x.reshape((x.shape[1], x.shape[2], 3))

x /= 2.
x += 0.5
x *= 255.
x = np.clip(x, 0, 255).astype('uint8')
return x

import numpy as np

step = 0.01
num_octave = 3
octave_scale = 1.4
iterations = 20
max_loss = 10.
base_image_path = './datasets/deepdream/original_photo_deep_dream.jpg'

img = preprocess_image(base_image_path)

original_shape = img.shape[1:3]
successive_shapes = [original_shape]

for i in range(1, num_octave):
shape = tuple([int(dim/(octave_scale ** i)) for dim in original_shape])
successive_shapes.append(shape)

successive_shapes = successive_shapes[::-1]

original_img = np.copy(img)
shrunk_original_img = resize_img(img, successive_shapes[0])

for shape in successive_shapes:
print('처리할 이미지 크기', shape)
img = resize_img(img, shape)
img = gradient_ascent(img, iterations=iterations, step=step, max_loss=max_loss)
upsacled_shrunk_original_img = resize_img(shrunk_original_img, shape)
same_size_original = resize_img(original_img, shape)
lost_detail = same_size_original - upsacled_shrunk_original_img

img += lost_detail
shrunk_original_img = resize_img(original_img, shape)
save_img(img, fname='dream_at_scale_' + str(shape) + '.png')

save_img(img, fname='./datasets/deepdream/final_dream.png')
  • 샌프란시스코 만과 구글 캠퍼스 사이 작은 언덕에서 찍은 사진을 이용해서 아래와 같은 이미지를 얻었다.

  • 손실로 사용할 층을 바꾸어 보면서 어떤 일이 발생하는지 꼭 시도해 보라. 네트워크의 하위 층은 지역적이고 비교적 덜 추상적인 표현을 가지고 있기 때문에 딥드림 이미지에 기하학적 패턴이 많이 생긴다.
    • 상위 층은 강아지 눈, 새의 깃털처럼 ImageNet에 많이 등장하는 물체를 기반으로 뚜렷이 구분되는 시각 패턴을 만든다.
    • layer_contributions 딕셔너리의 파라미터를 랜덤하게 생성하여 여러 가지 층 조합을 빠르게 탐색할 수 있다.
    • 아래 그림은 층 조합을 다르게 하여 얻은 이미지 결과이다.

정리

  • 딥드림은 네트워크가 학습한 표현을 기반으로 컨브넷을 거꾸로 실행하여 입력 이미지를 생성한다.
  • 재미있는 결과가 만들어지고 때로는 환각제 때문에 시야가 몽롱해진 사람이 만든 이미지 같기도 하다.
  • 이 과정은 이미지 모델이나 컨브넷에 국한되지 않는다. 음성이나 음악에도 적용될 수 있다.

뉴럴 스타일 트랜스퍼

  • 딥드림 이외에 딥러닝을 사용해서 이미지를 변경하는 또 다른 주요 분야는 뉴럴 스타일 트랜스퍼(neural style transfer)이다. 2015년 여름 리온 게티스(Leon Gatys) 등이 소개했다.
    • 뉴럴 스타일 트랜스퍼 알고리즘은 처음 소개된 이후 많이 개선되었고 여러 변종도 생겼다. 스마트폰 사진 앱에도 쓰인다.
  • 뉴럴 스타일 트랜스퍼는 타깃 이미지의 콘텐츠를 보존하면서 참조 이미지의 스탕리을 타깃 이미지에 적용한다. 아래 그림을 참고하라

  • 여기에서 스타일은 질감, 색깔, 이미지에 있는 다양한 크기의 시각 요소를 의미한다. 콘텐츠는 이미지에 있는 고수준의 대형 구조를 말한다.
    • 예컨대 위 그림의 <별이 빛나는 밤>에서 파란색과 노란색의 원을 그리는 듯한 붓질을 하나의 스타일로 생각할 수 있다.
  • 텍스쳐 생성과 밀접하게 연관된 스타일 트랜스퍼의 아이디어는 2015년 뉴럴 스타일 트랜스퍼가 개발되기 이전에 이미 이미지 처리 분야에서 오랜 역사를 가지고 있다.
    • 딥러닝을 기반으로 한 스타일 트랜스퍼 구현은 고전적인 컴퓨터 비전 기법으로 만든 것과는 비견할 수 없는 결과를 제공한다. 창조적인 컴퓨터 비전 애플리케이션 분야에 새로운 르네상스를 열었다.
  • 스타일 트랜스퍼 구현 이면에 있는 핵심 개념은 모든 딥러닝 알고리즘의 핵심과 동일하다. 목표를 표현한 손실 함수를 정의하고 이 손실을 최소화 한다. 
    • 여기서 원하는 것은 다음과 같다. 참조 이미지의 스타일을 적용하면서 원본 이미지의 콘텐츠를 보존하는 것이다.
    • 콘텐츠와 스타일을 수학적으로 정의할 수 있다면 최소화할 손실 함수는 다음과 같을 것이다.
loss = distance(style(reference_image) - style(generated_image)) + distance(content(original_image) - content(generated_image))
  • 여기에서 distance는 L2 노름 같은 노름 함수이다. content 함수는 이미지의 콘텐츠 표현을 계산한다. style 함수는 이미지의 스타일 표현을 계산한다.
    • 이 손실을 최소화하면 style(generated_image)는 style(reference_image)와 가까워지고, content(generated_image)는 content(original_image)와 가까워진다.
    • 앞서 정의한 스타일 트랜스퍼의 목적을 달성할 수 있다.
  • 게티스 등은 심층 합성곱 신경망을 사용하여 style과 content 함수를 수학적으로 정의할 수 있다는 것을 보였다.

콘텐츠 손실

  • 앞서 배웠듯이 네트워크에 있는 하위 층의 활성화는 이미지에 관한 국부적인 정보를 담고 있다. 반면 상위 층의 활성화일수록 점점 전역적이고 추상적인 정보를 담게 된다.
    • 다른 방식으로 생각하면 컨브넷 층의 활성화는 이미지를 달느 크기의 콘텐츠로 분해한다고 볼 수 있다. 컨브넷 상위 층의 표현을 사용하면 전역적이고 추상적인 이미지 콘텐츠를 찾을 것이다.
  • 타깃 이미지와 생성된 이미지를 사전 훈련된 컨브넷에 주입하여 상위 층의 활성화를 계산한다.
    • 이 두 값 사이의 L2 노름이 콘텐츠 손실로 사용하기에 좋다. 상위 층에서 보았을 때 생성된 이미지와 원본 타깃 이미지를 비슷하게 만들 것이다.
    • 컨브넷의 상위 층에서 보는 것이 입력 이미지의 콘텐츠라고 가정하면 이미지의 콘텐츠를 보존하는 방법으로 사용할 수 있다.

스타일 손실

  • 콘텐츠 손실은 하나의 상위 층만 사용한다. 게티스 등이 정의한 스타일 손실은 컨브넷의 여러 층을 사용한다. 하나의 스타일이 아니라 참조 이미지에서 컨브넷이 추출한 모든 크기의 스타일을 잡아야 한다.
    • 게티스 등은 층의 활성화 출력의 그람 행렬(Gram matrix)을 스타일 손실로 사용했다. 그람 행렬은 층의 특성 맵들의 내적(inner product)이다.
    • 내적은 층의 특성 사이에 있는 상관관계를 표현한다고 이해할 수 있다. 이런 특성의 상관관계는 특정 크기의 공간적인 패턴 통계를 잡아낸다.
    • 경험에 비추어 보았을 때 이 층에서 찾은 텍스쳐에 대응된다.
  • 스타일 참조 이미지와 생성된 이미지로 층의 활성화를 계산한다. 스타일 손실은 그 안에 내재된 상관관계를 비슷하게 보존하는 것이 목적이다.
    • 결국 스타일 참조 이미지와 생성된 이미지에서 여러 크기의 텍스쳐거 비슷하게 보이도록 만든다.
  • 요약하면 사전 훈련된 컨브넷을 사용하여 다음 손실들을 정의할 수 있다.
    • 콘텐츠를 보존하기 위해 타깃 콘텐츠 이미지와 생성되 ㄴ이미지 사이에서 상위 층의 활성화를 비슷하게 유지한다. 이 컨브넷은 타깃 이미지와 생성된 이미지에서 동일한 것을 보아야 한다.
    • 스타일을 보존하기 위해 저수준 층과 고수준 층에서 활성화 안에 상관관계를 비슷하게 유지한다. 특성의 상관관계는 텍스쳐를 잡아낸다. 생성된 이미지와 스타일 참조 이미지는 여러 크기의 텍스쳐를 공유할 것이다.
  • 2015년 뉴럴 스타일 트랜스퍼 원본 알고리즘을 케라스로 구현해 보자. 딥드림 구현과 공통점이 많다.

케라스에서 뉴럴 스타일 트랜스퍼 구현하기

  • 뉴럴 스타일 트랜스퍼는 사전 훈련된 컨브넷 중 어떤 것을 사용해서도 구현할 수 있다. 여기서는 게티스 등이 사용한 VGG19 네트워크를 사용하겠다. 
    • VGG19는 VGG16 네트워크의 변종으로 합성곱 층이 3개 더 추가되었다.
  • 일반적인 다음과 같다.
    1. 스타일 참조 이미지, 타깃 이미지, 생성된 이미지를 위해 VGG19의 층 활성화를 동시에 계산하는 네트워크를 설정한다.
    2. 세 이미지에서 계산한 층 활성화를 사용하여 앞서 설명한 손실 함수를 정의한다. 이 손실을 최소화하여 스타일 트랜스퍼를 구현할 것이다.
    3. 손실 함수를 최소화할 경사 하강법 과정을 설정한다.
  • 스타일 참조 이미지와 타깃 이미지의 경로를 정의하는 것부터 시작해 보자. 처리할 이미지는 크기가 비슷하다. 크기가 많이 다르면 스타일 트랜스퍼를 구현하는 것이 더 어렵다.)
from keras.preprocessing.image import load_img, img_to_array, save_img

target_image_path = './datasets/styletransfer/portrait.png'
style_reference_image_path = './datasets/styletransfer/popova.jpg'

width, height = load_img(target_image_path).size
img_height = 400
img_width = int(width * img_height / height)
  • VGG19 컨브넷에 입출력할 이미지의 로드, 전처리, 사후 처리를 위해 유틸리티 함수를 정의한다.
import numpy as np
from keras.applications import vgg19

def preprocess_image(image_path):
img = load_img(image_path, target_size=(img_height, img_width))
img = img_to_array(img)
img = np.expand_dims(img, axis=0)
img = vgg19.preprocess_input(img)
return img

def deprocess_image(x):
x[:, :, 0] += 103.939
x[:, :, 1] += 116.779
x[:, :, 2] += 123.68
x = x[:, :, ::-1]
x = np.clip(x, 0, 255).astype('uint8')
return x
  • VGG19 네트워크를 설정해 보자. 스타일 참조 이미지, 타깃 이미지 그리고 생성된 이미지가 담긴 플레이스홀더로 이루어진 배치를 입력으로 받는다.
    • 플레이스홀더는 심볼릭 텐서로 넘파이 배열로 밖에서 값을 제공해야 한다.
    • 스타일 참조 이미지와 타깃 이미지는 이미 준비된 데이터이므로 K.constant를 사용하여 정의한다. 
    • 반면 플레이스홀더에 담길 생성된 이미지는 계속 바뀐다.
from keras import backend as K

target_image = K.constant(preprocess_image(target_image_path))
style_reference_image = K.constant(preprocess_image(style_reference_image_path))
combination_image = K.placeholder((1, img_height, img_width, 3))

input_tensor = K.concatenate([target_image, style_reference_image, combination_image], axis=0)
model = vgg19.VGG19(input_tensor=input_tensor, weights='imagenet', include_top=False)
print('모델 로드 완료')
  • 콘텐츠 손신을 정의해보자. VGG19 컨브넷 상위 층은 타깃 이미지와 생성된 이미지를 동일하게 바라보아야 한다.
def content_loss(base, combination):
return K.sum(K.square(combination-base))
  • 다음은 스타일 손실이다. 유틸리티 함수를 사용하여 입력 행렬의 그람 행렬을 계산한다. 이 행렬은 원본 특성 행렬의 상관관계를 기록한 행렬이다.
def gram_matrix(x):
features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
gram = K.dot(features, K.transpose(features))
return gram

def style_loss(style, combination):
S = gram_matrix(style)
C = gram_matrix(combination)
channels = 3
size = img_height * img_width
return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size **2))
  • 두 손실에 하나를 더 추가한다. 생성된 이미지의 픽셀을 사용하여 계산하는 총 변위 손실(variation loss)이다.
    • 이는 생성된 이미지가 공간적인 연속성을 가지도록 도와주며 픽셀의 격자 무늬가 과도하게 나타나는 것을 막아준다. 이를 일종의 규제항으로 해석할 수 있다.
def total_variation_loss(x):
a = K.square(x[:, :img_height-1, :img_width-1, :] - x[:, 1:, :img_width - 1, :])
b = K.square(x[:, :img_height-1, :img_width-1, :] - x[:, :img_height - 1, 1:, :])
return K.sum(K.pow(a + b, 1.25))
  • 최소화할 손실은 이 세 손실의 가중치 평균이다. 콘텐츠 손실은 block5_conv2 층 하나만 사용해서 계산한다. 스타일 손실을 계산하기 위해 하위 층과 상위 층에 걸쳐 여러 층을 사용한다. 그리고 마지막에 총 변위 손실을 추가한다.
  • 사용하는 스타일 참조 이미지와 콘텐츠 이미지에 따라 content_weight 계수(전체 손실에 기여하는 콘텐츠 손실의 정도)를 조정하는 것이 좋다. content_weight가 높으면 생성된 이미지에 타깃 콘텐츠가 더 많이 나타나게 된다.
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])

content_layer = 'block5_conv2'
style_layers = [
'block1_conv1',
'block2_conv1',
'block3_conv1',
'block4_conv1',
'block5_conv1',
]

total_variation_weight = 1e-4
style_weight = 1.
content_weight = 0.025

loss = K.variable(0.)
layer_features = outputs_dict[content_layer]
target_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
val = content_weight * content_loss(target_image_features, combination_features)
loss = loss + val

for layer_name in style_layers:
layer_features = outputs_dict[layer_name]
style_reference_features = layer_features[1, :, :, :]
combination_features = layer_features[2, :, :, :]
sl = style_loss(style_reference_features, combination_features)
loss += (style_weight / len(style_layers)) * sl

loss += total_variation_weight * total_variation_loss(combination_image)
  • 마지막으로 경사 하강법 단계를 설정한다. 게티스의 원래 논문에서 L-BFGS 알고리즘을 사용하여 최적화를 수행했으므로 여기서도 이를 사용하겠다. 앞선 딥드림 에제와 가장 크게 차이나는 부분이다.
  • L-BFGS 알고리즘은 싸이파이에 구현되어 있는데 두 가지 제약 사항이 있다.
    • 손실 함수 값과 그래디언트 값을 별개의 함수로 전달해야 한다.
    • 이 하무는 3D 이미지 배열이 아니라 1차원 벡터만 처리할 수 있다.
  • 손실 함수 값과 그래디언트 값을 따로 계산하는 것은 비효율적이다. 두 계산 사이에 중복되는 계산이 많기 때문이다. 한번에 계산하는 것보다 거의 2배 가량 느리다.
    • 이를 피하기 위해 손실과 그래디언트 값을 동시에 계산하는 Evaluator란 이름의 파이썬 클래스를 만들겠다. 처음 호출할 때 손실 값을 반환하면서 다음 호출을 위해 그래디언트를 캐싱한다.
grads = K.gradients(loss, combination_image)[0]

fetch_loss_and_grads = K.function([combination_image], [loss, grads])

class Evaluator(object):
def __init__(self):
self.loss_value = None
self.grads_values = None

def loss(self, x):
assert self.loss_value is None
x = x.reshape((1, img_height, img_width, 3))
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1].flatten().astype('float64')
self.loss_value = loss_value
self.grad_values = grad_values
return self.loss_value

def grads(self, x):
assert self.loss_value is not None
grad_values = np.copy(self.grad_values)
self.loss_value = None
self.grad_values = None
return grad_values

evaluator = Evaluator()
  • 마지막으로 싸이파이 L-BFGS 알고리즘을 사용하여 경사 하강법 단계를 수행한다. 알고리즘마다 생성된 이미지를 저장한다. (여기서는 한 번 반복이 경사 하강법 단계 20번이다)
from scipy.optimize import fmin_l_bfgs_b
import time

result_prefix = 'style_transfer_results'
iterations = 20

x = preprocess_image(target_image_path)
x = x.flatten()

for i in range(iterations):
print('반복 횟수:', i)
start_time = time.time()
x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x, fprime=evaluator.grads, maxfun=20)
print('현재 손실 값:', min_val)
img = x.copy().reshape((img_height, img_width, 3))
img = deprocess_image(img)
fname = result_prefix + '_at_iteration_%d.png' % i
save_img(fname, img)
print('저장 이미지', fname)
end_time = time.time()
print('%d 번째 반복 완료: %ds' % (i, end_time - start_time))
  • 다음 그림이 만들어진 이미지이다. 이 기법은 이미지의 텍스쳐를 바꾸거나 텍스쳐를 전이한 것임을 기억하라.
    • 스타일 이미지의 텍스쳐가 두드러지고 비슷한 패턴이 많을 때 잘 작동한다. 또 콘텐츠 타깃을 알아보기 위해 수준 높은 이해가 필요하지 않을 때 잘 작동한다.
    • 일반적으로 인물 사진의 스타일을 다른 인물 사진으로 옮기는 것처럼 아주 추상적인 기교는 만들지 못한다. 이 알고리즘은 AI보다는 고전적인 시그널 처리에 가깝기 때문에 마술 같은 결과는 기대하지 말 것.

  • 스타일 트랜스퍼 알고리즘은 느리지만 간단한 변환을 수행하기 때문에 작고 빠른 컨브넷을 사용하여 학습할 수 있다. 물론 적절한 양의 훈련 데이터가 있어야 한다. 
    • 먼저 고정된 스타일 참조 이미지에 대해 여기서 소개한 방법으로 입력-출력 훈련 샘플을 많이 생성한다. 그 다음 이 스타일 변환을 학습하는 간단한 컨브넷을 훈련하면 스타일 트랜스퍼를 빠르게 수행할 수 있다.
    • 이런 모델을 만들면 이미지가 주어졌을 때 순식간에 스타일을 바꿀 수 있다. 그냥 이 작은 컨브넷을 통과시키면 된다.

정리

  • 스타일 트랜스퍼는 참조 이미지의 스타일을 적용하면서 타깃 이미지의 콘텐츠를 보존하여 새로운 이미지를 만드는 방법이다.
  • 콘텐츠는 컨브넷 상위 층의 활성화에서 얻을 수 있다.
  • 스타일은 여러 컨브넷 층의 활성화 안에 내재된 상관관계에서 얻을 수 있다.
  • 딥러닝에서는 사전 훈련된 컨브넷으로 손실을 정의하고 이 손실을 최적화 하는 과정으로 스타일 트랜스퍼를 구성할 수 있다.
  • 이런 기본 아이디어에서 출발하여 다양한 변종과 개선이 가능하다.

변이형 오토인코더를 사용한 이미지 생성

  • 이미지의 잠재 공간에서 샘플링해서 완전히 새로운 이미지나 기존 이미지를 변형하는 방식이 현재 가장 인기 있고 성공적으로 창조적 AI 애플리케이션을 만들 수 있는 방법이다.
  • 이 분야의 주요 기법인 변이형 오토인코더(Variational AutoEncoders, VAE)와 적대적 생성 네트워크(Generative Adversarial Networks, GAN)의 상세 구현을 다루어 보겠다.
    • 여기서 소개한 기법은 이미지에만 해당되는 것이 아니다. GAN과 VAE를 이용해서 소리, 음악 또는 텍스트의 잠재 공간을 만들 수 있다.
    • 실전에서는 사진에 사용했을 때 가장 재미있는 결과를 만들어내므로 여기서는 이 부분에 집중하겠다.

이미지의 잠재 공간에서 샘플링하기

  • 이미지 생성의 핵심 아이디어는 각 포인트가 실제와 같은 이미지로 매핑될 수 있는 저차원 잠재 공간(당연하게 이는 벡터 공간이다)의 표현을 만드는 것이다.
    • 잠재 공간의 한 포인트를 입력으로 받아 이미지(픽셀의 그리드)를 출력하는 모듈을 (GAN에서는) 생성자(generator) 또는 (VAE 에서는) 디코더(decoder)라고 부른다.
    • 잠재 공간이 만들어지면 여기서 포인트 하나를 특정하여 또는 무작위로 샘플링 할 수 있다.
    • 그 다음 이미지 공간으로 매핑하여 이전에 본 적 없는 이미지를 생성한다. (아래 그림 참조)

  • GAN과 VAE는 이미지의 잠재 공간 표현을 학습하는 2개의 전략이고 각각 나름의 특징을 가진다.
    • VAE는 구조적인 잠재 공간을 학습하는데 뛰어나다. 이 공간에서 특정 방향은 데이터에서 의미 있는 변화의 방향을 인코딩한다.
    • GAN은 매우 실제 같은 이미지를 만든다. 여기에서 만든 잠재 공간은 구조적이거나 연속성이 없을 수 있다.

이미지 변형을 위한 개념 벡터

  • 6장에서 단어 임베딩을 다룰 때 이미 개념 벡터(concept vector)에 대한 아이디어를 얻었다. 이 아이디어와 동일하다. 잠재 공간이나 임베딩 공간이 주어지면 이 공간의 어떤 방향은 원본 데이터의 흥미로운 변화를 인코딩한 축일 수 있다.
    • 예컨대 얼굴 이미지에 대한 잠재 공간에 웃음 벡터가 있을 수 있다. 잠재 공간의 z 포인트가 어떤 얼굴의 임베딩된 표현이라면 잠재 공간의 z + s 포인트는 같은 얼굴이 웃고 있는 표현을 임베딩한 것이다.
    • 이런 벡터를 찾아내면 이미지를 잠재 공간에 투영해서 의미 있는 방향으로 이 표현을 이동한 후 이미지 공간으로 디코딩하여 복원하면 변형된 이미지를 얻을 수 있다.
  • 기본적으로 이미지 공간에서 독립적으로 변화가 일어나는 모든 차원이 개념 벡터이다.
    • 얼굴이라면 안경을 쓰고 벗거나 남자 얼굴을 여자 얼굴로 바꾸는 등의 벡터를 발견할 수 있다.
    • 아래 그림은 뉴질랜드 빅토리아 대학 디자인 스쿨의 톰 화이트가 찾은 개념 벡터인 웃음 벡터의 예이다. 유명 인사의 얼굴 데이터셋(CelebA 데이터셋)에서 훈련한 VAE를 사용했다.

변이형 오토인코더

  • 2013년 12월 킹마(Kingma)와 웰링(Welling) 그리고 2014년 1월 르젠드(Rezende), 무함마드(Mhamed), 위스트라(Wierstra)가 동시에 발견한 변이형 오토인코더는 생성 모델의 한 종류로 개념 벡터를 사용하여 이미지를 변형하는데 아주 적절하다.
    • 오토인코더는 입력을 저차원 잠재 공간으로 인코딩한 후 디코딩하여 복원하는 네트워크이다.
    • 변이형 오토인코더는 딥러닝과 베이즈 추론(Bayesian inferrence)의 아이디어를 혼합한 오토인코더의 최신 버전이다.
  • 고전적인 오토인코더는 이미지를 입력 받아 인코더 모듈을 사용하여 잠재 벡터 공간으로 매핑한다. 그 다음 디코더 모듈을 사용해서 원본 이미지와 동일한 차원으로 복원하여 출력한다. (아래 그림 참조)
    • 오토인코더는 입력 이미지와 동일한 이미지를 타깃 데이터로 사용하여 훈련한다. 다시 말해 오토인코더는 원본 입력을 재구성하는 방법을 학습한다.
    • 코딩(coding)(인코더의 출력)에 여러 제약을 가하면 오토인코더가 더 흥미로운 또는 덜 흥미로운 잠재 공간의 표현을 학습한다.
    • 일반적으로 코딩이 저차원이고 희소(0이 많은) 하도록 제약을 가한다. 이런 경우 인코더는 입력 데이터의 정보를 적은 수의 비트에 압축하기 위해 노력한다.

  • 현실적으로 이런 전통ㅈ거인 오토인코더는 특별히 유용하거나 구조화가 잘 된 잠재 공간을 만들지 못한다. 압축도 아주 뛰어나지 않다. 이런 이유로 시대의 흐름에서 대부분 멀어졌다.
    • VAE는 오토인코더에 약간의 통계 기법을 추가하여 연속적이고 구조적인 잠재 공간을 학습하도록 만들어졌다. 결국 이미지 생성을 위한 강력한 도구로 탈바꿈 되었다.
  • 입력 이미지를 잠재 공간의 고정된 코딩으로 압축하는 대신 VAE는 이미지를 어떤 통계 분포의 파라미터로 변환한다.
    • 이는 입력 이미지가 통계적 과정을 통해 생성되었다고 가정하여 인코딩과 디코딩하는 동안 무작위성이 필요하다는 것을 의미한다.
    • VAE는 평균과 분산 파라미터를 사용하여 이 분포에서 무작위로 하나의 샘플을 추출한다. 이 샘플을 디코딩하여 원본 입력으로 복원한다. (아래 그림 참조)
    • 이런 무작위한 과정은 안정성을 향상하고 잠재 공간 어디서든 의미 있는 표현을 인코딩하도록 만든다. 즉 잠재 공간에서 샘플링한 모든 포인트는 유효한 출력으로 디코딩 된다.

  • 기술적으로 보면 VAE는 다음과 같이 작동한다.
    1. 인코더 모듈이 입력 샘플 input_img를 잠재 공간의 두 파라미터 z_mean과 z_log_var로 변환한다.
    2. 입력 이미지가 생성되었다고 가정한 잠재 공간의 정규 분포에서 포인트 z를 z = z_mean + exp(0.5 * z_log_var) * epsilon처럼 무작위로 샘플링한다. (epsilon은 작은 값을 가진 랜덤 텐서이다)
    3. 디코더 모듈은 잠재 공간의 이 포인트를 원본 입력 이미지로 매핑하여 복원한다.
  • epsilon이 무작위로 만들어지기 때문에 input_img를 인코딩한 잠재 공간의 위치(z_mean)에 가까운 포인트는 input_img와 비슷한 이미지로 디코딩 될 것이다. 이는 잠재 공간을 연속적이고 의미 있는 공간으로 만들어준다.
    • 잠재 공간의 이런 저차원 연속성은 잠재 공간에서 모든 방향이 의미 있는 데이터 변화의 축을 인코딩 하도록 만든다. 결국 잠재 공간은 매우 구조적이고 개념 벡터로 다루기에 적합해진다.
  • VAE의 파라미터는 2개의 손실 함수로 훈련한다. 디코딩된 샘플이 원본 입력과 동일하도록 만드는 재구성 손실(reconstruction loss)과 잠재 공간을 잘 형성하고 훈련 데이터에 과대적합을 줄이는 규제 손실(reularization loss)이다. 
  • 케라스의 VAE 구현을 간단히 살펴보자. 개략적으로 보면 다음과 같다.
z_mean, z_log_var = encoder(input_img)
z = z_mean + exp(0.5 * z_log_var) * epsilon
reconstructed_img = decoder(z)
model = Model(input_img, reconstructed_img)
  • 그 다음 이 모델을 재구성 손실과 규제 손실을 사용하여 훈련한다.
  • 다음 코드는 이미지를 잠재 공간상 확률 분포 파라미터로 매핑하는 인코더 네트워크이다. 입력 이미지 x를 두 벡터 z_mean과 z_log_var로 매핑하는 간단한 컨브넷이다.
import keras
from keras import layers
from keras import backend as K
from keras.models import Model
import numpy as np

img_shape = (28, 28, 1)
batch_size = 16
latent_dim = 2

input_img = keras.Input(shape=img_shape)

x = layers.Conv2D(32, 3, padding='same', activation='relu')(input_img)
x = layers.Conv2D(64, 3, padding='same', activation='relu', strides=(2,2))(x)
x = layers.Conv2D(64, 3, padding='same', activation='relu')(x)
x = layers.Conv2D(64, 3, padding='same', activation='relu')(x)

shape_before_flattening = K.int_shape(x)

x = layers.Flatten()(x)
x = layers.Dense(32, activation='relu')(x)

z_mean = layers.Dense(latent_dim)(x)
z_log_var = layers.Dense(latent_dim)(x)
  • 다음은 z_mean과 z_log_var를 사용하는 코드이다. 이 두 파라미터가 input_img를 생성한 통계 분포의 파라미터라 가정하고 잠재 공간 포인트 z를 생성한다.
    • 여기에서 (케라스의 백엔드 기능으로 만든) 일련의 코드를 Lambda 층으로 감싼다. 케라스에서는 모든 것이 층이므로 기본 층을 사용하지 않는 코드는 Lambda로 (또는 직접 만든 층으로) 감싸야 한다.
def sampling(args):
z_mean, z_log_var = args
epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim), mean=0., stddev=1.)
return z_mean + K.exp(0.5 * z_log_var) * epsilon

z = layers.Lambda(sampling)([z_mean, z_log_var])
  • 다음 코드는 디코더 구현이다. 벡터 z를 이전 특성 맵 차원으로 크기를 바꾸고 몇 개의 합성곱 층을 사용하여 최종 출력 이미지를 만든다. 최종 이미지는 원본 input_img와 차원이 같다.
decoder_input = layers.Input(K.int_shape(z)[1:])

x = layers.Dense(np.prod(shape_before_flattening[1:]), activation='relu')(decoder_input)
x = layers.Reshape(shape_before_flattening[1:])(x)
x = layers.Conv2DTranspose(32, 3, padding='same', activation='relu', strides=(2,2))(x)
x = layers.Conv2D(1, 3, padding='same', activation='sigmoid')(x)

decoder = Model(decoder_input, x)

z_decoded = decoder(z)
  • 일반적인 샘플 기준의 함수인 loss(y_true, y_pred) 형태는 VAE의 이중 손실에 맞지 않다. add_loss 내장 메서드를 사용하는 층을 직접 만들어 임의의 손실을 정의하겠다.
class CustomVariationalLayer(keras.layers.Layer):
def vae_loss(self, x, z_decoded):
x = K.flatten(x)
z_decoded = K.flatten(z_decoded)
xent_loss = keras.metrics.binary_crossentropy(x, z_decoded)
kl_loss = -5e-4 * K.mean(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
return K.mean(xent_loss + kl_loss)

def call(self, inputs):
x = inputs[0]
z_decoded = inputs[1]
loss = self.vae_loss(x, z_decoded)
self.add_loss(loss, inputs=inputs)
return x

y = CustomVariationalLayer()([input_img, z_decoded])
  • 이제 모델 객체를 만들고 훈련할 준비가 되었다. 층에서 손실을 직접 다루기 때문에 compile 메서드에서 손실을 지정하지 않는다. (loss=None) 그 결과 훈련하는 동안 타깃 데이터를 전달하지 않아도 된다. (다음 코드와 같이 모델의 fit 메서드에 x_train만 전달한다.)
from keras.datasets import mnist

vae = Model(input_img, y)
vae.compile(optimizer='rmsprop', loss=None)
vae.summary()

(x_train, _), (x_test, y_test) = mnist.load_data()

x_train = x_train.astype('float32') / 255.
x_train = x_train.reshape(x_train.shape + (1,))
x_test = x_test.astype('float32') / 255.

x_test = x_test.reshape(x_test.shape + (1,))

vae.fit(x=x_train, y=None, shuffle=True, epochs=10, batch_size=batch_size, validation_data=(x_test, None))
  • MNIST 데이터셋으로 모델의 훈련을 마치면 디코더 네트워크를 사용하여 잠재 공간의 임의의 벡터를 이미지로 변환할 수 있다.
import matplotlib.pyplot as plt
from scipy.stats import norm

n = 15
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
grid_x = norm.ppf(np.linspace(0.05, 0.95, n))
grid_y = norm.ppf(np.linspace(0.05, 0.95, n))

for i, yi in enumerate(grid_x):
for j, xi in enumerate(grid_y):
z_sample = np.array([[xi, yi]])
z_sample = np.tile(z_sample, batch_size).reshape(batch_size, 2)
x_decoded = decoder.predict(z_sample, batch_size=batch_size)
digit = x_decoded[0].reshape(digit_size, digit_size)
figure[i * digit_size: (i+1) * digit_size, j*digit_size: (j+1) * digit_size] = digit

plt.figure(figsize=(10,10))
plt.imshow(figure, cmap='Greys_r')
plt.show()
  • 샘플링된 숫자의 그리드(아래 그림 참고)는 다른 숫자 클래스 사이에서 완벽하게 연속된 분포를 보여준다. 잠재 공간의 한 경로를 따라서 한 숫자가 다른 숫자로 자연스럽게 바뀐다.
    • 이 공간의 특정 방향은 어떤 의미를 가진다. 예컨대 ‘6으로 가는 방향’, ‘9로 가는 방향’ 등이다.

정리

  • 딥러닝으로 이미지 데이터셋에 대한 통계 정보를 담은 잠재 공간을 학습하여 이미지를 생성할 수 있다. 잠재 공간에서 포인트를 샘플링하고 디코딩하면 이전에 본 적 없는 이미지를 생성한다. 이를 수행하는 주요 방법은 VAE와 GAN이다.
  • VAE는 매우 구조적이고 연속적인 잠재 공간의 표현을 만든다. 이런 이유로 잠재 공간 안에서 일어나는 모든 종류의 이미지 변형 작업에 잘 맞는다.
    • 다른 얼굴로 바꾸기, 찌푸린 얼굴에서 웃는 얼굴로 변형하기 등이다. 잠재 공간을 가로질러 이미지가 변환하는 잠재 공간 기반의 애니메이션에도 잘 맞는다.
    • 시작 이미지가 연속적으로 다른 이미지로 부드럽게 바뀌는 것을 볼 수 있다.
  • GAN은 실제 같은 단일 이미지를 생성할 수 있지만 구조적이고 연속적인 잠재 공간을 만들지 못한다.

적대적 생성 신경망 소개

  • 2014년 굿펠로우 드잉 소개한 적대적 생성 신경망(GAN)은 VAE와 다른 방법으로 이미지의 잠재 공간을 학습한다. GAN은 생성된 이미지가 실제 이미지와 통계적으로 거의 구분되지 않도록 강제하여 아주 실제 같은 합성 이미지를 생성한다.
  • GAN을 직관적으로 이해하는 방법은 가짜 피카소 그림을 만드는 위조범을 생각하는 것이다.
    • 위조범은 처음에 형편없이 위조하는데, 진짜 피카소 그림과 위조품을 섞어서 그림 판매상에게 보여주면 판매상은 각 그림이 진짜인지 평가하고 피드백을 준다.
    • 위조범은 새로운 위조품을 준비하고 시간이 지남에 따라 피카소의 스타일을 모방하는데 점점 더 능숙해지고, 판매상은 위조품을 구분하는데 점점 더 전문가가 된다. 결국 아주 훌륭한 피카소 위조품을 만들어 낼 것이다.
  • 위조범 네트워크와 전문가 네트워크가 바로 GAN이다. 두 네트워크는 상대를 이기기 위해 훈련한다. GAN 네트워크 2개는 다음과 같다.
    • 생성자 네트워크(generator network): 랜덤 벡터(잠재 공간의 무작위한 포인트)를 입력으로 받아 이를 합성된 이미지로 디코딩한다.
    • 판별자 네트워크(discriminator network)(또는 상대 네트워크): 이미지 (실제 또는 합성 이미지)를 입력으로 받아 훈련 세트에서 온 이미지인지, 생성자 네트워크가 만든 이미지인지 판별한다.
  • 생성자 네트워크는 판별자 네트워크를 속이도록 훈련한다. 훈련이 계속될수록 점점 더 실제와 같은 이미지를 생성하게 된다.
    • 실제 이미지와 구분할 수 없는 인공적인 이미지를 만들어 판별자 네트워크가 두 이미지를 동일하게 보도록 만든다.
    • 한편 판별자 네트워크는 생성된 이미지가 실제인지 판별하는 기준을 설정하면서 생성자의 능력 향상에 적응해 간다.
    • 훈련이 끝나면 생성자는 입력 공간에 있는 어떤 포인트를 그럴듯한 이미지로 변환한다.
    • VAE와 달리 이 잠재 공간은 의미 있는 구조를 보장하지 않는다. 특히 이 공간은 연속적이지 않다.

  • 놀랍게도 GAN은 최적화의 최솟값이 고정되지 않은 시스템이다. 이 책에서 다루는 어떤 훈련 설정과도 다르다. 보통 경사 하강법은 고정된 손실 공간에서 언덕을 내려오는 방법이다.
    • GAN에서는 언덕을 내려오는 매 단계가 조금씩 전체 공간을 바꾼다. 최적화 과정이 최솟값을 찾는게 아니라 두 힘 간의 평형점을 찾는 다이나믹 시스템이다.
  • 이런 이유로 GAN은 훈련하기 어렵기로 유명하다. GAN을 만들려면 모델 구조와 훈련 파라미터를 주의 깊게 많이 조정해야 한다.

GAN 구현 방법

  • 이 절에서는 케라스에서 가장 기본적인 형태의 GAN을 구현하는 방법을 설명하겠다. GAN은 수준 높은 기술이기 때문에 기술적인 내용을 깊이 설명하는 것은 이 책 범위를 벗어난다.
    • 구체적인 구현은 심층 합성곱 GAN(DCGAN)이다. 생성자와 판별자가 심층 컨브넷이다. 특히 생성자에서 이미지 업샘플링을 위해 Conv2DTranspose 층을 사용한다.
  • CIFAR10 데이터셋의 이미지로 GAN을 훈련하겠다. 이 데이터셋은 32×32 크기의 RGB 이미지 5만개로 이루어져 있고 10개의 클래스를 갖는다 (클래스마다 5,000개의 이미지가 있다) 문제를 간단하게 만들기 위해 “frog” 클래스의 이미지만 사용하겠다.
  • GAN 구조는 다음과 같다.
    1. generator 네트워크는(latent_dim,) 크기의 벡터를 (32, 32, 3) 크기의 이미지로 매핑한다.
    2. discriminator 네트워크는 (32, 32, 3) 크기의 이미지가 진짜일 확률을 추정하여 이진 값으로 매핑한다.
    3. 생성자와 판별자를 연결하는 gan 네트워크를 만든다. gen(x) = discriminator(generator(x))이다. 이 gan 네트워크는 잠재 공간의 벡터를 판별자의 평가로 매핑한다. 즉, 판별자는 생성자가 잠재 공간의 벡터를 디코딩하는 것이 얼마나 현실적인지 평가한다.
    4. 진짜/ 가짜 레이블과 함꼐 진짜 이미지와 가짜 이미지 샘플을 사용하여 판별자를 훈련한다. 일반적인 이미지 분류 모델을 훈련하는 것과 동일하다.
    5. 생성자를 훈련하려면 gan 모델의 손실에 대한 생성자 가중치의 그래디언트를 사용한다. 이 말은 매 단계마다 생성자에 의해 디코딩된 이미지를 판별자가 “진짜”로 분류하도록 만드는 방향으로 생성자의 가중치를 이동한다는 뜻이다. 다른 말로 하면 판별자를 속이도록 생성자를 훈련한다.

훈련 방법

  • GAN을 훈련하고 튜닝하는 과정은 어렵기로 유명하다. 알아 두어야 할 몇 가지 유용한 기법이 있다.
    • 딥러닝의 대부분이 그렇듯이 이는 과학보다 연금술에 가깝다. 이런 기법들은 이론에 바탕을 둔 지침이 아니고 경험을 통해 발견된 것이다.
    • 실제 일어난 현상을 직관적으로 이해하는 수준에서 검증되었다. 모든 문제에 반드시 적용해야 하는 것은 아니지만 경험상 잘 작동한다고 알려져 있다.
  • 다음은 이 절에서 GAN 생성자와 판별자를 구현하는데 사용할 몇 가지 기법이다. 이 목록이 GAN에 관련된 전체 팁은 아니다. GAN 논문들에서 더 많은 방법을 볼 수 있다.
    • 생성자의 마지막 활성화로 다른 종류의 모델에서 널리 사용하는 sigmoid 대신 tanh 함수를 사용한다.
    • 균등 분포가 아니고 정규 분포(가우시안 분포)를 사용하여 잠재 공간에서 포인트를 샘플링한다.
    • 무작위성은 모델을 견고하게 만든다. GAN 훈련은 동적 평형을 만들기 때문에 여러 방식으로 갇힐 가능성이 높다. 훈련하는 동안 무작위성을 주입하면 이를 방지하는데 도움이 된다. 무작위성은 두 가지 방법으로 주입한다. 판별자에 드롭아웃을 사용하거나 판별자를 위해 레이블에 랜덤 노이즈를 추가한다.
    • 희소한 그래디언트는 GAN 훈련을 방해할 수 있다. 딥러닝에서 희소는 종종 바람직한 현상이지만 GAN에서는 그렇지 않다. 그래디언트를 희소하게 만들 수 있는 것은 최대 풀링 연산과 ReLU 활성화 두 가지이다.
      • 최대 풀링 대신 스트라이드 합성곱을 사용하여 다운샘플링을 하는 것이 좋다.
      • 또 ReLU 활성화 대신 LeakyReLU 층을 사용하라. ReLU와 비슷하지만 음수의 활성화 값을 조금 허용하기 때문에 희소가 조금 완화된다.
    • 생성자에서 픽셀 공간을 균일하게 다루지 못하여 생성된 이미지에서 체스판 모양이 종종 나타난다. (아래 그림 참조) 이를 해결하기 위해 생성자와 판별자에서 스트라이드 Conv2DTranspose나 Conv2D를 사용할 때 스트라이드 크기로 나누어질 수 있는 커널 크기를 사용한다.

생성자

  • 먼저 벡터(훈련하는 동안 잠재 공간에서 무작위로 샘플링된다)를 후보 이미지로 변환하는 generator 모델을 만들어 보자.
    • GAN에서 발생하는 많은 문제 중 하나는 생성자가 노이즈 같은 이미지를 생성하는데서 멈추는 것이다.
    • 판별자와 생성자 양쪽에 모두 드롭아웃을 사용하는 것이 해결 방법이 될 수 있다.
import keras
from keras import layers
import numpy as np

latent_dim = 32
height = 32
width = 32
channels = 3

generator_input = keras.Input(shape=(latent_dim,))

x = layers.Dense(128 * 16 * 16)(generator_input)
x = layers.LeakyReLU()(x)
x = layers.Reshape((16, 16, 128))(x)

x = layers.Conv2D(256, 5, padding='same')(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2DTranspose(256, 4, strides=2, padding='same')(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2D(256, 5, padding='same')(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(256, 5, padding='same')(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2D(channels, 7, activation='tanh', padding='same')(x)

generator = keras.models.Model(generator_input, x)
generator.summary()

판별자

  • 다음은 후보 이미지(진짜 또는 가짜)를 입력으로 받고 2개의 클래스로 분류하는 discriminator 모델을 만들겠다. 이 클래슨느 ‘생성된 이미지’ 또는 ‘훈련 세트에서 온 진짜 이미지’이다.
discriminator_input = layers.Input(shape=(height, width, channels))

x = layers.Conv2D(128, 3)(discriminator_input)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Flatten()(x)

x = layers.Dropout(0.4)(x)

x = layers.Dense(1, activation='sigmoid')(x)

discriminator = keras.models.Model(discriminator_input, x)
discriminator.summary()

discriminator_optimizer = keras.optimizers.RMSprop(lr=0.0008, clipvalue=1.0, decay=1e-8)
discriminator.compile(optimizer=discriminator_optimizer, loss='binary_crossentropy')

적대적 네트워크

  • 마지막으로 생성자와 판별자를 연결하여 GAN을 설정한다. 훈련할 때 생성자가 판별자를 속이는 능력이 커지도록 학습한다.
    • 이 모델은 잠재 공간의 포인트를 “진짜” 또는 “가짜”의 분류 결정으로 변환한다. 훈련에 사용되는 타깃 레이블은 항상 ‘진짜 이미지’이다. 
    • gan을 훈련하는 것은 discriminator가 가짜 이미지를 보았을 때 진짜라고 예측하도록 만들기 위해 generator의 가중치를 업데이트 하는 것이다.
    • 훈련하는 동안 판별자를 동결(학습되지 않도록)하는 것이 아주 중요하다. gan을 훈련할 때 가중치가 업데이트 되지 않는다. 판별자의 가중치가 훈련하는 동안 업데이트 되면 판별자는 항상 “진짜”를 예측하도록 훈련된다. 이는 우리가 원하는 바가 아니다.
discriminator.tainable = False

gan_input = keras.Input(shape=(latent_dim,))
gan_output = discriminator(generator(gan_input))
gan = keras.models.Model(gan_input, gan_output)

gan_optimizer = keras.optimizers.RMSprop(lr=0.0004, clipvalue=1.0, decay=1e-8)
gan.compile(optimizer=gan_optimizer, loss='binary_crossentropy')

DCGAN 훈련 방법

  • 이제 훈련을 싲가한다. 훈련 반복의 내용을 요약 정리해 보자. 매 반복마다 다음을 수행한다.
    1. 잠재 공간에서 무작위로 포인트를 뽑는다(랜덤 노이즈)
    2. 이 랜덤 노이즈를 사용하여 generator에서 이미지를 생성한다.
    3. 생성된 이미지와 진짜 이미지를 섞는다.
    4. 진짜와 가짜가 섞인 이미지와 이에 대응하는 타깃을 사용하여 discriminator를 훈련한다. 타깃은 “진짜(실제 이미지일 경우)” 또는 “가짜(생성된 이미지일 경우)”이다.
    5. 잠재 공간에서 무작위로 새로운 포인트를 뽑는다.
    6. 이 랜덤 벡터를 사용하여 gan을 훈련한다. 모든 타깃은 “진짜”로 설정한다. 판별자가 생성된 이미지를 모두 “진짜 이미지”라고 예측하도록 생성자의 가중치를 업데이트 한다(gan 안에서 판별자는 동결되기 때문에 생성자만 업데이트한다) 결국 생성자는 판별자를 속이도록 훈련한다.
import os
from keras.preprocessing import image

(x_train, y_train), (_, _) = keras.datasets.cifar10.load_data()

x_train = x_train[y_train.flatten() == 6]

x_train = x_train.reshape((x_train.shape[0],) + (height, width, channels)).astype('float32') / 255.

iterations = 10000
batch_size = 20
save_dir = './datasets/gan_images/'

if not os.path.exists(save_dir):
os.mkdir(save_dir)

start = 0

for step in range(iterations):
random_latent_vectors = np.random.normal(size=(batch_size, latent_dim))
generated_images = generator.predict(random_latent_vectors)

stop = start + batch_size
real_images = x_train[start: stop]
combined_images = np.concatenate([generated_images, real_images])

labels = np.concatenate([np.ones((batch_size, 1)), np.zeros((batch_size, 1))])
labels += 0.05 * np.random.random(labels.shape)

d_loss = discriminator.train_on_batch(combined_images, labels)

random_latent_vectors = np.random.normal(size=(batch_size, latent_dim))
misleading_targets = np.zeros((batch_size, 1))
a_loss = gan.train_on_batch(random_latent_vectors, misleading_targets)

start += batch_size

if start > len(x_train) - batch_size:
start = 0

if step % 100 == 0:
gan.save_weights('gan.h5')

print('판별자 손실:', d_loss)
print('적대적 손실:', a_loss)

img = image.array_to_img(generated_images[0] * 255., scale=False)
img.save(os.path.join(save_dir, 'generated_frog' + str(step) + '.png'))

img = image.array_to_img(real_images[0] * 255., scale=False)
img.save(os.path.join(save_dir, 'real_frog' + str(step) + '.png'))
  • 훈련할 때 적대적 손실이 크게 증가하는 것을 볼 수 있을지 모른다. 반면 판별자의 손실은 0으로 향한다. 결국 판별자가 생성자를 압도할 수 있다. 이런 경우에는 판별자의 학습률을 낮추고 판별자의 드롭아웃 비율을 높여서 시도해 보라.

정리

  • GAN은 생성자 네트워크와 판별자 네트워크가 연결되어 구성된다. 판별자는 생성자의 출력과 훈련 데이터셋에서 가져온 진짜 이미지를 구분하도록 훈련된다. 생성자는 판별자를 속이도록 훈련된다. 놀랍게도 생성자는 훈련 세트의 이미지를 직접 보지 않는다. 데이터에 관한 정보는 판별자에서 얻는다.
  • GAN은 훈련하기 어렵다. GAN 훈련이 고정된 손실 공간에서 수행하는 단순한 경사 하강법 과정이 아니라 동적 과정이기 때문이다. GAN을 올바르게 훈련하려면 경험적으로 찾은 여러 기교를 사용하고 많은 튜닝을 해야 한다.
  • GAN은 매우 실제 같은 이미지를 만들 수 있다. 잠재 공간의 개념 벡터를 사용하여 이미지를 변형하는 등 실용적인 특정 애플리케이션에는 잘 맞지 않는다.

요약

  • 딥러닝을 사용한 창의적인 애플리케이션에서 심층 네트워크는 기존 콘텐츠에 설명을 다는 것을 넘어서 직접 콘텐츠를 생산하기 시작했다. 다음 내용들을 배웠다.
    • 한 번에 하나의 타임스텝씩 시퀀스 데이터를 생성하는 방법. 텍스트 생성이나 음표 하나씩 음악을 생성하거나 어떤 시계열 데이터를 생성하는 곳에 적용할 수 있다.
    • 딥드림의 작동 원리. 컨브넷 층 활성화를 최대화하기 위해 입력 공간에 경사 상승법을 적용한다.
    • 스타일 트랜스퍼 적용 방법. 콘텐츠 이미지와 스타일 이미지가 연결되어 재미있는 결과를 만든다.
    • GAN과 VAE가 무엇인지와 이를 사용하여 새로운 이미지를 만드는 방법. 잠재 공간의 개념 벡터를 사용하여 이미지를 변형하는 방법
[ssba]

The author

Player가 아니라 Creator가 되려는 자/ suyeongpark@abyne.com

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.