** ML에 관한 자료들을 보면서 ML을 이용한 그림을 보고 도대체 어떻게 하는 것인지 궁금했었는데 이제서야 관련글을 보게 된다. 지금이라도 볼 수 있어서 기쁘다...
Artistic Style Transfer with Deep Learning
우리는 근래에 Loving Vincent에서 반고흐(Van Gogh)의 알려진 이야기를 보았다. 125명의 예술가가 함께 모여 그의 영화를 만들기 위해 수작업으로 캔버스에 65,000장의 반고흐 스타일 유화를 그렸다. 여기서 AI는 이들 125명의 예술가 만큼이나 재능이 있었다. 예술적 스카일이 불려지는 딥러닝 기술은 이런 종류의 그림을 만드는데 힘을 실어준다.
Loving Vincent (2017)
Artistic style transfer(일명 neural style transfer)는 보통의 이미지를 명작으로 바꿀 수 있다. 사실 이것은 CNN, 전이학습(transfer learning), 오코인코더(auto-encoder)같은 몇몇 딥러닝 기술의 조합니다. 비록 이것의 구현이 여기저기 있지만 이것은 정말 어려운 이론적 배경을 가지고 있는 것이 이것의 구현을 이해하기 어려운 이유이다. 이 글에서는 스타일 전이(style transfer)의 배경과 기본에서부터 이를 구현해 본다.
우선, 이 기술은 일반적인 신경망 연산이 아니다. 일반적인 신경망은 일벽과 출력쌍으로 가중치를 세부조정한다. 여기서 우리는 사전훈련된 네트워크를 사용하면서 가중치는 갱신하지 않는다. 가중치를 갱신하는 것 대신 입력을 갱신한다.
원래 연구는 사전훈련된 네트워크로 VGG를 사용한다. 이 글에서도 동일한 네트워크를 사용하지만 필수는 아니다. 여러분은 다른 어떠한 사전훈련된 신경망을 사용해도 된다. 기본적으로 VGG 네트워크는 아래 그림과 같은 구조이다.
VGG19 or OxfordNet
Images
여기서 우리는 이미지의 스타일을 또다른 스타일로 변환하려 한다. 변환하려는 이미지를 content이미지라고 부르는 반면 전달하려는 스타일의 이미지를 style이미지라고 부른다. 그러면 스타일 이미지의 붓 터치(brush stroke)가 컨텐트 이미지에 반영되고 새로운 이미지는 generated 이미지라고 부른다.
(Manual) Style Transfer in Loving Vincent. Content, style and generated ones respectively.
컨텐트와 스타일 이미지는 이미 존재한다. 여러분은 신경망에서 임의로 가중치를 초기화해야 한다는 것을 기억할 것이다. 여기서 생성된(generated) 이미지는 가중치 대신 임의로 초기화 될 것이다. 이 어플리케이션은 보통의 신경망이 아니라는 것을 기억하자. 아래 코드는 컨텐트와 스타일 이미지를 읽고 생성된 이미지(generated image)를 위해 임의로 이미지를 생성한다.
def preprocess_image(image_path):
img = load_img(image_path, target_size=(height, weight))
img = img_to_array(img)
img = np.expand_dims(img, axis=0)
img = preprocess_input(img)
return img
content_image = preprocess_image("content.jpeg")
style_image = preprocess_image("style.jpg")
height = 224; weight = 224 #original size of vgg
random_pixels = np.random.randint(256, size=(1, height, weight, 3))
generated_image = preprocess_input(random_pixels, axis=0)
보통, 파이썬은 3차원 numpy 배열로 이미지를 저장한다.(그중 1차원은 RGB코드) 그러나, VGG 네트워크는 4차원 입력으로 설게되었다. 3차원 넘파이 배열을 VGG의 입력으로 변환하면 “block1_conv1: expected ndim=4, found ndim=3“ layer exception이 발생할 것이다. 그렇기 때문에 전처리 단계에서 expand 차원 명령을 추가하였다.(위 소스코드의 맨 마지막 줄) 게다가 VGG 네트워크의 입력 특성은 224 X 224 X 3이다. 그렇기 때문에 스타일과 생성된 이미지는 224 X 224 크기이다.
Network
이제 이미지를 입력 입력 특성으로써 VGG 네트워크에 전달한다. 하지만 네트워크의 출력 대신 몇몇 레이어의 출력이 필요하다. 오토인코더가 데이터의 표현(representation)을 추출하기 위해 사용될 수 있다는 것을 기억하자. 사실, 우리는 이미지의 표현을 추출하기 위해 VGG를 사용한다.
운 좋게도 케라스는 바로 사용가능한 함수로 우승한 CNN 모델을 제공한다.
from keras.applications import vgg19
content_model = vgg19.VGG19(input_tensor=K.variable(content_image), weights='imagenet')
style_model = vgg19.VGG19(input_tensor=K.variable(content_image), weights='imagenet')
generated_model = vgg19.VGG19(input_tensor=K.variable(generated_image), weights='imagenet')
Loss
우리는 손실(loss)을 두번 저장한다. 하나는 컨텐트를 또 하나는 스타일을 위한 것이다. 보통의 신경망에서 손실값은 실제 출력과 모델의 출력(예측)을 비교하여 계산된다. 여기서는 오토인코딩된 이미지의 압축된 표현을 비교한다. 오토인코딩된 압축된 표현(representation)은 사실 몇몇 중간 레이어의 출력이라는 것을 기억하자. 네트워크를 실행한 후 각 레이어의 이름과 출력을 저장한다.
content_outputs = dict([(layer.name, layer.output) for layer in content_model.layers])
style_outputs = dict([(layer.name, layer.output) for layer in style_model.layers])
generated_outputs = dict([(layer.name, layer.output) for layer in generated_model.layers])
Content loss
임의로 생성된 이미지(generated image)와 컨텐트 이미지를 VGG 네트워크로 전달한다. 원래 작업은 content loss를 게산히기 위해 5번째 블록의 2번째 합성곱 레이어(block5_conv2)를 사용한다. 이것이 필수는 아니다. 여러분의 작업에서 이미지를 압축하기 위해 다른 레이어를 사용할 수도 있다.
Content loss
위에서 컨텐트 이미지와 생성된 이미지를 VGG 네트워크로 이미 전달했다. 이제 컨텐트 이미지와 생성된 이미지 모두에 대해 동일한 레이어의 출력에 대한 차이의 제곱으로 content loss를 계산할 수 있다.
def content_loss(content, generated):
return K.sum(K.square(content - generated))
loss = K.variable(0)
content_features = content_outputs['block5_conv2']
generated_features = generated_outputs['block5_conv2']
contentloss = content_loss(content_features, generated_features)
Style loss
이 손실 형태는 계산하기 좀 어렵다. 우선, 첫번째 5-레이어의 출력을 비교한다.
Style loss
여기서 gram 행렬(matrix)간 거리를 찾는다. Gram 행렬은 행렬과 행렬의 전치(transpose) 행렬을 곱하여 구할 수 있다.
gram = K.dot(features, K.transpose(features))
Gram 행렬을 구하려면 2차원 행렬에서 작업해야 한다. 기본적으로 batch flatten 명령은 n차원 행렬으리 2차원으로 변환한다. 예를 들어, 3번째 합성곱 레이어의 크기는 (56 X 56) X 256이다. 여기서 256은 해당 레이어의 필터수이다. 만약 레이어의 모양이 256 X 56 X 56으로 변한다면 56 X 56 크기인 행렬이 나란히 놓인다. Permute_dimentsions 함수는 flatten 전에 행렬을 구성하는데 도움이 된다.
def gram_matrix(x):
#put number of filters to 1st dimension first
features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
gram = K.dot(features, K.transpose(features))
return gram
Gram 행렬을 시각적으로 표현하면 아래와 같다. 여기서 nc를 필터의 수로 생각할 수도 있다.
Gram matrix
아래 코드는 style loss를 계산한다.
def style_loss(style, generated):
style_gram = gram_matrix(style)
content_gram = gram_matrix(generated)
channels = 3
size = height * weight
return K.sum(K.square(style_gram - content_gram)) / (4. * (pow(channels,2)) * (pow(size,2)))
#name of first 5 layers. you can check it by running content_model.summary()
feature_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', block5_conv1']
styleloss = K.variable(0)
for layer_name in feature_layers:
style_features = style_outputs[layer_name]
generated_features = generated_outputs[layer_name]
styleloss = styleloss + style_loss(style_features[0], generated_features[0])
Total loss
지금까지 content loss와 style loss를 모두 구했다. 이제는 total loss를 계산할 수 있다.
alpha = 0.025
beta = 0.2
total_loss = alpha * contentloss + beta * styleloss
Gradient Descent
Style transfer
Total loss는 역전파(back propagation) 알고리즘에서 거꾸로 모든 가중치에 반영된다는 것을 기억하자.
각 가중치에 대한 총 오류(total error)의 미분은 신경망 학습 절차에서 계산된다. 이 계산 또한 경사(gradient) 계산이라고 불린다. 스타일 전이(style transfer)에서는 가중치 대신 입력에 관한 기울기가 필요하다.
#gradients = K.gradients(total_loss, generated_model.trainable_weights)
gradients = K.gradients(total_loss, generated_model.input)
print(gradients)
이런 방법으로 (1, 224, 224, 3) 모양의 텐서의 기울기가 계산된다. 이제는 가중치 대신 생성된 이미지의 입력을 갱신한다.
learning_rate = np.array([0.1])
generated_image = generated_image - learning_rate * gradients[0]
사실 여기서는 임의로 생성된 이미지에 단순히 기본적인 경사하강(gradient descent)만을 적용했다. 여러분은 작품을 빠르게 만들기 위해 adam 옵티마이져 알고리즘을 적용할 수 있다. 원래 작업은 이미지를 갱신하기 위해 L-BFGS를 사용했다. 연구자는 L-BFGS가 Adam이나 다른 것보다 성능이 좋다고 했다. 마지막으로 실제학습을 위해 for 루프(epoch)에서 이 모든 연산을 수행한다.
Testing
반 고흐의 Starry Night 유화 style을 Galatasaray University에 적용했다. 결과는 250에포크 이후에 매우 인상적이다.
Galatasaray University as content
Van Gogh’s Starry Night as a style
Starry Night Style Galatasaray University
아래 비디오는 250 에코크 동안의 스타일 변화 애니메이션이다. 이미지의 변화가 놀랍다.
게다가 비디오에도 스타일 전이를 적용할 수 있다. 드론 영상에 스타일 전이를 적용한 것이 놀랍다.
비디오는 사실 24fps이다. 즉 실시간 솔루션은 아니다. 우선 비디오의 프레임을 추출하고 프레임에 스타일 변이를 적용하였다. 또한 각 프레임에 100 에포크 반복하였다. 이 작업은 성능좋은 GPU가 필요하다. GPU를 사용했지만 1분길이의 비디로르 변환하는데 하루이상 걸렸다. 여러분이 24fps를 다룰 수 있다면 실시간 스타일 변이를 적용할 수 있다. 이 글에서 비디오에 어떻게 스타일 전이를 적용하는지를 다룬다.
저자가 이미 작업한 비디오는 여기에서 찾아볼 수 있다.
저작권 및 지적재산권
최근 MIT의 Lex Fridman은 이러한 종류의 AI가 만든 작품에 대한 저작권을 포함하는 논쟁을 제기하였다. 잠저적인 소유자는 원본 이미지/비디오의 소유자, 구조 또는 알고리즘의 설계자(VGG 구조는 Oxford VGG 그룸에 의해 만들어 졌고 알고리즘 창시자는 스타일 전이의 원래 논문 저자이다.), 코드를 실행한 사람 또는 AI 시스템 자신이다. 아래 비디오는 이 주제에 대한 것이다.
To Sum up
이 글에서는 에술적 스타일 변이를 다뤘다. 이것은 CNN, 전이학습, 오토인코딩 같은 몇가지 고차원 딥러닝 기술의 조합이다. 여러분이 스타일 변이를 적용하기 전에 관련 주제를 이해하는 것을 강력하게 추천한다.
비록 이 동기의 배경이 신경망일지라도 표준 신경망 규칙을 적용할 수 없다. 가중치 대신 입력을 갱신하고 네트워크의 출력대신 몇몇 레이어의 출력을 사용한다.
이 글에서 사용된 코드는 여기에서 찾을 수 있다.