[VISION] 비전 시스템을 위한 딥러닝(10) - 딥드림과 신경 스타일 전이 (DeepDream and Neural Style Transfer)

Study/Vision & Deep Learning · 2023. 5. 31. 19:04

이 장은 신경망을 이용하여 회화 작품 이미지를 만들 수 있는 두 가지 기법인 딥드림과 신경 스타일 전이를 소개한다.

이를 위해 먼저 합성곱 신경망은 세상을 어떻게 보는지 탐구한다.

앞서 사물 탐지나 이미지 분류를 위한 특징을 추출하는 목적으로 CNN을 활용했다.

이번에는 이렇게 추출된 특징 맵을 시각화 하는 방법을 배운다.

이는 학습 과정에서 '실제로 학습되는 것'이 무엇인지 이해하는데 도움이 된다.

다음으로 설명할 딥드림 알고리즘은 신경망의 특징을 강조하여 이미지를 변형시키는 기법이고, 신경 스타일 전이는 두 이미지를 결합하여 새로운 이미지를 생성하는 기법이다.

이들은 예술적 창작에 활용될 수 있을 뿐만 아니라 신경망의 동작을 이해하고 성능 향상에도 도움이 된다. 또한, 이러한 기법들은 예술과 창조성을 알고리즘적으로 이해하는 데에도 중요한 역할을 할 수 있다.

 

9.1 합성곱 신경맘이 본 세계는 어떤 것일까

신경망의 동작 방식과 세계 인식에 대한 이해는 아직 부족한 상태이다. 신경망이 어떻게 작동하며, 세계를 어떻게 인식하는지는 아직 밝혀지지 않았다.

이를 이해한다면 신경망의 성능을 더욱 향상할 수 있을 뿐만 아니라, 설명 가능한 인공지능 문제를 해결하는 데에도 도움이 될 수 있다.

컴퓨터 비전 분야에서는 합성곱 신경망 내부의 특징 맵을 시각화 하여 어떤 특징을 통해 서로 다른 대상을 구분하는지 이해할 수 있다.

2009년에 제안된 합성곱층 시각화 아이디어를 케라스로 구현하여 이 개념을 이해해보려 한다.

이러한 노력을 통해 우리는 신경망이 세상을 어떻게 보고 예측 결과를 어떻게 이끌어내는지를 더 잘 이해할 수 있으며, 이는 신경망의 성능 향상과 설명 가능한 인공지능에 큰 도움이 될 것이다.

 

9.1.1 신경망의 동작 원리 다시 보기

신경망은 많은 양의 훈련 데이터를 활용하여 학습된다. 이 과정에서 신경망은 가중치를 조정하며 원하는 분류 성능을 달성한다.

보통 10개에서 30개 사이의 뉴런으로 구성된 여러 층으로 이루어진 신경망은 이미지를 입력층에 받아들이고, 각 층을 거쳐서 최종 예측 결과를 출력층에서 얻는다.

그러나, 이러한 신경망의 동작 원리를 정확히 이해하는 것은 어렵다.

학습이 완료된 신경망은 뒤에 위치한 층일수록 추상적인 특징을 추출하며, 마지막 몇 층에서는 이러한 특징들을 조합하여 최종적인 해석을 만든다. 이런 방식으로 복잡한 이미지에 대해 신경망의 다양한 뉴런이 반응하게 된다.

이와 같은 내부 동작을 살펴보기 위해 특징 맵을 시각화하는 방법이 있다.

이를 통해 신경망이 이미지에서 인식한 특징을 강조하여 표시할 수 있다.

예를 들어 '새'를 분류하는 경우, 무작위 노이즈 이미지를 입력하고 이 이미지에 신경망이 인식한 '새'의 중요한 특징을 강조해 나갈 수 있다.

이 장에서는 신경망의 필터를 시각화하여 신경망이 인식한 특징을 자세히 살펴보고 설명할 것이다.

신경망은 중요한 특징을 선택하여 다음 층으로 전달할 정도로 지능적이다. 중요하지 않은 특징은 출력층에 도달하기 전에 사라진다.

다시 말해, 신경망은 훈련 데이터에 포함된 대상의 특징을 학습한다. 신경망의 특징 맵을 시각화 하면 어떤 특징에 집중하고 예측 결과를 도출하는지 알 수 있을 것이다.

 

9.1.2 CNN의 특징 시각화 하기

합성곱 신경망에 의해 학습된 특징을 시각화 하는 가장 쉬운 방법 중 하나는 각 필터가 반응하는 시각적 패턴을 관찰하는 것이다.

이 과정은 입력 공간에서 경사 상승법을 통해 수행 할 수 있다.

 입력 이미지 값에 경사 상승법을 적용하면 빈 이미지에서 시작하여 특징 필터의 반응을 최대화할 수 있다.

결과 입력 이미지는 해당 필터가 가장 크게 반응하는 이미지가 된다.

가장 앞 쪽 층의 특징 맵

아직은 방향이나 색 같은 저수준의 일반적인 특징이 부호화 되어 있다.

중간 층의 특징 맵

이전에 방향이나 색 등의 특징이 해당 층에서 서로 결합해서 간단한 격자 모양이나 얼룩무늬 텍스처 등의 특징을 이룬다.

뒷쪽 부근의 특징 맵

현재 그림을 보면 패턴의 패턴을 보이기 시작했다. 신경망이 예측을 내리려면 여러 개의 특징 맵을 사용 하지만, 우리는 특징 맵을 보고 이 이미지의 내용이 무엇이었는지 짐작할 수 있다.

왼쪽 이미지를 보면 새의 부리와 눈을 볼 수 있다. 해당 특징이 꼭 새나 물고기가 아니더라도 대부분의 다른 클래스 (자동차, 보트, 건물 등)를 배제할 수 있다.

이렇게 특징 맵을 시각화 함으로써 해당 신경망이 의존하고 있는 특징을 파악할 수 있다. 특징을 파악한다면 입력 이미지를 강화하거나, 가중치를 조절하는 등 사용자의 판단이 필요한 부분에 신뢰도를 높일 수 있을 것이다.

 

 

9.1.3 특징 시각화 구현하기

위에서 보였던 예시를 실제 코드로 구현해볼 것이다.

전체 코드는 케라스 공식 코드 저장소(http://mng.bz/Md8n)에서 볼 수 있다.

코드는 케라스의 공식 문서에 포함된 CNN 시각화 구현 코드를 약간 수정한 것이다.

선택한 특징 맵의 평균 활성화도가 최대가 되는 패턴을 생성해 볼 것이다.

# build the VGG16 network with ImageNet weights
from keras.applications.vgg16 import VGG16 # 케라스에서 제공하는 VGG 모델을 임포트 한다.
 
model = VGG16(weights='imagenet', include_top=False) # 모델을 읽어 들인다.
print('Model loaded.')

model.summary()

VGG16 모델을 구성하는 각 층의 이름과 출력 모양을 확인하자.

이 중에 시각화 대상이 될 필터를 고를 것이다.

for layer in model.layer:
	if 'conv' not in layer.name:
    	continue
    filters, biases = layer.get_weights() # 필터의 가중치에 접근
    print(layer.name, layer.output.shape)

이번에는 특정 합성곱층의 특정 필터의 활성화도를 최대화하는 손실 함수를 정의하자.

경사 계산 기능을 제공하는 케라스의 백엔드 함수 gradients를 사용하고, 경사값이 지나치게 커지거나 작아지지 않고 매끄러운 경사를 타고 상승할 수 있도록 정규화한다.

for filter_index in range(0, 10):
    # we only scan through the first 200 filters,
    # but there are more (512)
    print('Processing filter %d' % filter_index)
    start_time = time.time()

    # we build a loss function that maximizes the activation
    # of the nth filter of the layer considered
    # 층의 문자열 이름으로 층에 접근한다.
    layer_output = layer_dict[layer_name].output
    
    # The img_size variable is computed this way
    # The dim_ordering can either be “tf” or “th”. It tells 
    # Keras whether to use Theano or TensorFlow dimension ordering for inputs/kernels/ouputs.
   	# 대상 합성곱층 n번째 필터의 활성화도가 최대가 되게 하는 손실 함수를 정의한다.
    loss = K.mean(layer_output[:, :, :, filter_index])

    # we compute the gradient of the input picture wrt this loss
    grads = K.gradients(loss, input_img)[0]

    # normalization trick: we normalize the gradient
    grads = normalize(grads)

    # this function returns the loss and grads given the input picture
    iterate = K.function([input_img], [loss, grads])

    # step size for gradient ascent
    step = 1.

############################################################################
    # 미리 정의했던 필터 활성화도에 대해 경사 상승법을 적용하는 함수를 사용한다.
    
    # we start from a gray image with some random noise
    input_img_data = np.random.random((1, img_width, img_height, 3))
    input_img_data = (input_img_data - 0.5) * 20 + 128

    # we run gradient ascent for 20 steps
    for i in range(20):
        loss_value, grads_value = iterate([input_img_data])
        input_img_data += grads_value * step

        print('Current loss value:', loss_value)
        if loss_value <= 0.:
            # some filters get stuck to 0, we can skip them
            break

    # decode the resulting input image
    if loss_value > 0:
        img = deprocess_image(input_img_data[0])
        kept_filters.append((img, loss_value))
    end_time = time.time()
    
    print('Filter %d processed in %ds' % (filter_index, end_time - start_time))

경사 상승법의 코드 구현이 끝났다. 하지만 아직 텐서를 이미지로 변환해 줄 함수가 필요하다.

해당 함수는 다음과 같다. 함수를 호출하면 텐서를 이미지로 변환해서 디스크에 저장한다.

# util function to convert a tensor into a valid image
def deprocess_image(x):
    # normalize tensor: center on 0., ensure std is 0.1
    # 텐서 정규화
    x -= x.mean()
    x /= (x.std() + 1e-5)
    x *= 0.1

	
    # clip to [0, 1]
    # 구간 클리핑
    x += 0.5
    x = np.clip(x, 0, 1)

    # convert to RGB array
    # RGB 배열로 변환
    x *= 255
    if K.common.image_dim_ordering() == 'th':
        x = x.transpose((1, 2, 0))
    x = np.clip(x, 0, 255).astype('uint8')
    return x

 

9.2 딥드림

딥드림은 구글 리서치의 알렉산더 모드빈체프가 2015년에 개발한 것으로, 합성곱 신경망을 이용해서 몽환적인 분위기의 이미지를 만들어내는 기법이다.

딥드림으로 생성한 이미지

생성된 이미지에서 나오는 물체는 딥드림 모델의 신경망이 이미지넷을 학습한 부산물로, 데이터셋의 영향을 받아 개와 새의 종류별 이미지가 특히 강조되어 있다.

딥드림 프로젝트는 CNN을 거꾸로 동작히키고 활성화 맵을 시각화 해보면 어떨까 하는 아이디어에서 재미 삼아 출발한 실험이었다.

활성화 맵을 시각화 하는 데는 9.1절에서 배운 시각화 기법이 그대로 사용되었지만 다음과 같은 부분에서 수정이 있었다.

  • 입력 이미지:
    필터 시각화에서는 입력 이미지가 필요 없었다. 빈 이미지 또는 노이즈 이미지로 시작해서 필터의 활성화도를 최대화시키는 방식으로 필터의 특징을 시각화했다. 그러나 딥드림은 시각화된 특징을 이미지로 끌어내는 것이 목적이므로 입력 이미지가 필요하다.
  • 최대화 대상이 층의 활성화도:
    필터 시각화는 이름 그대로 특정 필터에 대한 활성화도를 최대화시키는 것이다. 반면 딥드림은 층 전체의 활성화도를 최대화하는 방법으로 여러 특징을 한 번에 시각화할 수 있다.
  • 옥타브 도입:
    딥드림에서는 시각화된 특징의 품질을 향상하기 위해 입력 이미지를 서로 다른 배율로 처리한 다. 이를 옥타브라고 한다. 옥타브는 뒤에 더 자세히 설명하겠다.

 

9.2.1 딥드림 알고리즘 동작 원리

딥드림 알고리즘은 대규모 데이터셋으로 사전 학습된 신경망을 사용한다.

케라스에서는 VGG16, VGG19, 인셉션, ResNet 등 다양한 사전 학습된 합성곱 신경망을 한다. 어떤 신경망을 선택하든 딥드림을 구현할 수 있으며, 필요에 따라 별도의 데이터셋으로 학습한 신경망을 사용할 수도 있다.

사전 학습된 신경망의 구조와 데이터셋의 선택은 출력 이미지에 영향을 미치는데, 이러한 선택이 사전 학습된 특징을 결정하는 요소기 때문이다.

딥드림의 원래 개발자는 출력 이미지의 품질이 좋다는 이유로 인셉션 모델을 선택했다. 따라서 해당 장에서는 인셉션 v3 모델을 사용할 것이다.

실습 이후에는 다른 사전 학습된 신경망을 사용해 보고 결과를 비교해 볼 수도 있다.

딥드림의 전체 동작 과정은 다음과 같다.

사전 학습된 신경망(예: 인셉션 v3)에 이미지를 입력하고, 특정 층에서 경사를 계산하여 해당 층의 활성화를 최대화하는 방향으로 이미지를 수정한다. 이 과정을 10회, 20회, 40회와 같이 반복하면 해당 층의 패턴이 이미지에 된다.

만약 사전 학습 데이터셋의 이미지 크기가 작은 경우(예: ImageNet), 입력 이미지가 크면(예: 1000x1000) 딥드림 알고리즘은 예술적인 결과보다는 많은 수의 작은 패턴이 있는 노이즈와 유사한 이미지를 생성할 수 있다.

이러한 문제를 해결하기 위해 딥드림 알고리즘은 다양한 배율(옥타브)로 이미지를 처리한다.

옥타브는 일정한 간격으로 딥드림 알고리즘을 적용하는 것을 의미한다.

먼저 이미지를 몇 차례 축소하여 다양한 배율의 이미지를 만들고, 각 배율마다 다음 절차를 수행한다.

  1. 세부 묘사 주입: 합성된 이미지에서 잃어버린 세부 묘사를 확대하면서 다시 주입한다.
  2. 딥드림 알고리즘 수행: 주입된 이미지를 딥드림 알고리즘에 입력한다.
  3. 다음 배율까지 이미지를 확대한다.

첫 번째 배율에서는 이미지를 확대하지 않으므로 세부 묘사를 다시 주입할 필요가 없다.  바로 딥드림 알고리즘을 수행한 후 이미지를 확대한다.

이미지를 확대하면 세부 묘사가 손실되어 픽셀이 도드라져 보이거나 흐릿한 이미지가 될 수 있다. 이때 옥타브 2의 이미지에서 세부 묘사를 가져와 다시 주입한 후 딥드림 알고리즘을 다시 수행한다.

이러한 이미지를 확대하고 세부 묘사를 재주입하며 딥드림 알고리즘을 반복하면서 원하는 품질의 이미지를 얻을 때까지 진행할 수 있다. 이는 재귀적인 절차이므로 원하는 이미지 품질을 얻을 때까지 반복할 수 있다.

딥드림 처리 절차

num_octave = 3 # 배율수
octave_scale = 1.4 # 옥타브 간 이미지 배율 차이 큰 것이 작은 것의 1.4배
iteration = 20 # 반복횟수

 

9.2.2 케라스로 구현한 딥드립

여기서 보여주는 딥드림 구현 코드는 케라스 공식 문서(https://keras.io/examples/generative/deep_dream/)와 프랑소와 숄레의 책에서 제공하는 코드를 기반으로 한다.

from keras.preprocessing.image import load_img, save_img, img_to_array
import numpy as np
import scipy
import argparse

from keras.applications import inception_v3
from keras import backend as K

# disable all training operations since we won't be doing any training to the model.
# 모델을 학습하는 것이 아니므로 추론 모드로 설정한다.
K.set_learning_phase(0)

# download the pretrained Inception V3 model without its top part.
model = inception_v3.InceptionV3(weights='imagenet', include_top=False)

# print the model summary
model.summary()

사전 학습된 신경망의 각 층에 접근하기 위한 딕셔너리를 만든다. 먼저 모델의 개요를 출력해서 각 층의 이름을 확인한다.

어떤 층을 선택했는지와 그 층이 전체 손실값에 미치는 영향력에 따라 딥드림의 결과로 출력되는 이미지는 큰 영향을 받는다.

따라서 이를 쉽게 설정할 수 있도록 한다.

따라서 각 층의 영향력을 딕셔너리 형태로 설정한다. 층에 설정된 가중치가 클수록 해당 층의 영향력이 커진다.

layer_contributions = {
                        'mixed2': 0.2,
                        'mixed3': 3.,
                        'mixed4': 2.,
                        'mixed5': 1.5,
                        }

딕셔너리에 저장된 층의 이름과 가중치를 바꾸면 이미지의 결과도 변화한다.

이번에는 손실(각 층의 활성화도에 대한 L2 노름의 가중합)을 저장할 텐서를 정의한다.

# a dictionary that maps layer names to layer instances
# 층 이름으로 층 객체에 접근하게 해주는 딕셔너리
layer_dict = dict([(layer.name, layer) for layer in model.layers])

# define the loss by adding layer contributions to this scalar variable.
# 각 층의 영향력을 이 스칼라 변수에 더해 손실로 사용한다.
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'))
    
    # Adds the L2 norm of the features of a layer to the loss. 
    # You avoid border artifacts by only involving nonborder pixels in the loss.
    # 특징의 L2 노름을 손실에 더한다. 경계에 접하지 않은 픽셀만 손실에 포함하여 물체가 이미지 경계에 걸치는 일이 없도록 한다.
    loss = loss + coeff * K.sum(K.square(activation[:, 2: -2, 2: -2, :])) / scaling

그리고 손실을 계산한다. 이 손실은 경사 상승법에서 최대화의 대상이 되는 값이다.

필터 시각화에서는 특징 층에 포한된 특정 필터의 활성화도만 최대화시켰는데, 이번에는 여러 층의 모든 필터의 활성화도를 동시에 최대화시켜야 한다.

특히 뒷부분에 배치된 층의 활성화도에 대한 L2 노름의 가중합을 최대화시킨다.

# a tensor that holds the generated image
# 생성된 이미지를 저장하는 텐서
dream = model.input

# compute the gradients of the dream image with regard to the loss
# 손실에 대한 생성 이미지의 경사를 계산한다.
grads = K.gradients(loss, dream)[0]

# normalize the gradients
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)

# set up a keras function to retrieve the value of the loss and gradients given an input image
# 입력 이미지에 대한 손실 및 경사를 계산하는 케라스 함수를 정의한다.
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

# run the gradient ascent process for a number of iterations
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('...Loss value at', i, ':', loss_value)
        x += step * grad_values
    return x

이제 딥드림 알고리즘을 구현할 차례다. 처리 과정은 다음과 같다.

  1. 입력 이미지를 읽어 드린다.
  2. 배율 수를 정의한다
  3. 입력 이미지를 설정된 배율 중 가장 작은 크기로 축소한다.
  4. 가장 작은 배율로 시작해서 다음을 반복하며 한 단계씩 이미지를 확대한다.
    - 경사 상승법 수행
    - 다음 배율로 이미지 확대
    - 이미지 확대로 손실된 세부 묘사 재주입
  5. 이미지가 원래 크기로 돌아오면 처리 종료

알고리즘 파라미터는 다음과 같다.

step = 0.01 # 경사 상승법의 이미지 수정 크기
num_octave = 3
octave_scale = 1.4
iterations = 20
max_loss = 10

다음은 케라스 구현 코드이다.

import numpy as np


# Playing with these hyperparameters will also allow you to achieve new effects
step = 0.01
num_octave = 3
octave_scale = 1.4
iterations = 20
max_loss = 10.
base_image_path = './exmples/chapter_09/deepdream/GoldenGateBridge.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('Processing image shape', shape)
    
    img = resize_img(img, shape)
    img = gradient_ascent(img, iterations=iterations, step=step, max_loss=max_loss)
    upscaled_shrunk_original_img = resize_img(shrunk_original_img, shape)
    same_size_original = resize_img(original_img, shape)
    lost_detail = same_size_original - upscaled_shrunk_original_img
    img += lost_detail
    shrunk_original_img = resize_img(original_img, shape)
    
    phil_img = deprocess_image(np.copy(img))
    save_img('./exmples/chapter_09/deepdream/dream_at_scale_' + str(shape) + '.png', phil_img)

# save the result to disk
final_img = deprocess_image(np.copy(img))
save_img('final_dream.png', final_img)

자잘한 패키지 오류가 있는데 오류 내용을 구글에 검색하면 금방 해결책이 나온다.

포기하지 말고 구현해 보자.

 

 

원본 이미지
dream_at_scale_(271, 407)

 

dream_at_scale_(380, 570)
dream_at_scale_(533, 799)

 

9.3 신경 스타일 전이

이 절에서는 합성곱 신경망으로 이미지의 화풍을 다른 이미지로 옮기는 기술인 신경 스타일 전이를 이용해서 예술 이미지를 만들어 볼 것이다.

해당 기술의 목표는 어떤 이미지의 화풍을 추출해서 다른 이미지에 적용하는 것이다.

신경 스타일 전이의 예

신경 스타일 전이(Neural Style Transfer)는 리온 개티스 Leon A. Gatys가 2015년에 제안한 알고리즘이다. 스타일 전이의 개념은 이미지 처리 분야에서 오래전부터 연구되어 왔던 질감 생성과 관련이 깊다.

딥러닝을 기반으로 한 신경 스타일 전이는 기존의 컴퓨터 비전 기법과 비교할 수 없는 성과를 얻으며 컴퓨터 비전의 창조적인 응용 분야에 부흥을 가져왔다.

신경 스타일 전이는 예술적인 이미지를 생성하는 신경망 기법 중에서도 해당 책의 필자가 가장 선호하는 기법이라고 한다.

딥드림을 이용하여 원하는 이미지를 만들어내는 것은 딥러닝 엔지니어에게는 어려운 일이다. 딥드림은 몽환적인 이미지를 만들어내지만 때로는 혐오스러운 이미지를 만들어낼 수도 있다.  

하지만, 스타일 전이는 원하는 이미지의 대상에 특정한 그림의 화풍을 입혀 상상했던 그대로의 이미지를 만들어낼 수 있다. 예를 들어, 공학을 배운 예술가가 그린 그림에 맛봄을 얻을 수 있을 정도이다.

스타일 전이를 구현하는 방법은 일반적인 딥러닝 알고리즘의 구현 방법과 크게 다르지 않다. 먼저 우리가 원하는 목표를 가리키는 손실 함수를 정의하고, 이 손실 함수를 최적화함으로써 스타일 전이를 수행한다.

스타일 전이에서의 목표는 원본 이미지의 콘텐츠를 보존하면서 참조 이미지의 화풍을 적용하는 것이다. 나머지는 콘텐츠와 화풍을 수학적으로 표현하고, 손실 함수를 정의하여 이를 최적화하는 것이다.

손실 함수를 정의할 때 가장 중요한 점은 한쪽 이미지에는 콘텐츠를 보존하고 다른 이미지에는 화풍을 보존해야 하는 것이다.

  • 콘텐츠 손실:
    콘텐츠 이미지와 통합 이미지 사이에 계산되는 손실. 이 손실을 최소화하면 원본 이미지의 콘텐츠가 통합 이미지에 더 많이 보존된다.
  • 스타일 손실:
    스타일 이미지와 통합 이미지 사이에 계산되는 손실. 이 손실을 최소화 하면 통합 이미지의 화풍이 스타일 이미지와 비슷해진다.
  • 노이즈 손실:
    총 분산 손실이라고도 한다. 통합 이미지에 포함된 노이즈 양을 측정한 값이다. 이 손실을 최소화 하면 이미지가 매끄러워진다.

총 손실을 다음과 같이 계산된다.

총_손실 = [스타일(스타일_이미지) - 스타일(통합_이미지)] + [콘텐츠(원본_이미지) - 콘텐츠(통합_이미지)]  + 총 분산 손실

앞서 정의한 총 손실을 최소화하는 신경망 학습 방법을 이해해 보자.

 

9.3.1 콘텐츠 손실

콘텐츠 손실은 두 이미지가 포함된 대상과 그 위치가 서로 차이나는 정도를 측정한다.

이미지에 포함된 대상과 그 위치는 합성곱 신경망으로 추출한 고수준 표현을 사용해서 점수를 매긴다.

이러한 특징을 식별하는 것이 딥러닝과 신경망이 하는 일이다. 이 합성곱 신경망은 이미지의 내용을 추출하고 앞쪽 층에서 추출한 단순한 특징을 모아 고수준 특징을 학습한다.

콘텐츠 이미지와 통합 이미지의 평균제곱오차를 측정한 다음 이 오차가 최소화가 되도록 하면 통합 이미지에 대상을 더 추가해서 콘텐츠 이미지와 비슷한 이미지를 만들게 된다.

콘텐츠 손실을 계산하려면 먼저 콘텐츠 이미지와 스타일 이미지를 사전 학습된 신경망에 입력하고 고수준 특징을 추출할 수 있는 뒤쪽 층 중 하나를 선택한다.

그다음 두 이미지 사이의 평균 제곱오차를 계산하면 된다.

코드는 케라스 공식 코드 저장소(https://mng.bz/GVzv)를 참조한다.

먼저 콘텐츠 이미지와 스타일 이미지를 담을 2개의 변수를 선언한다.

그리고 새로 생성될 통합 이미지가 담길 텐서도 정의한다.

#콘텐츠 이미지 및 스타일
#이미지의 경로
content_ image_path ='/이미지_경로/ content_image.jpg' 
style_ image_path = '/이미지_경로/style_image.jpg'

#이미지를 담을 텐서
content_image = K.variable(preprocess_image (content_image _path))
style_image = K.variable(preprocess_image (style_image _path))
combined _image = K.placeholder((1, img_nrows, img_ncols, 3))

이제 3개의 이미지를 연접하고 한 텐서 안에 담아 VGG19 신경망에 입력한다

모델을 읽어드릴 때 include_top을 False로 설정해서 불필요한 신경망 분류기 부분은 제외한다.

특징 추출기 부분만 사용한다.

#3개의 이미지를 하나의 텐서에 담는다.
input tensor = K, concatenate([content_image, style_image, combined image], axis=0)

'''
3개의 이미지를 담은 텐서를 입력받는 VGG19 신경망을 구성한다. 
이 모델에 이미지넷 데이터셋에
사전 학습된 가중치를 읽어 들인다.
'''

model = vgg19.VG619(input_tensor=input_tensor, weights= 'imagenet', include_top=False)

콘텐츠 손실을 계산하는 데 사용할 신경망의 층을 선택한다.

이때 고수준 특징이 손실 계산에서 잘 반영되도록 뒤쪽 층을 선택한다.

# 손실 계산에 쓰일 층(각 층에는 중복되지 않는 이름이 붙어 있다)의 출력에 대한 심벌을 확보한다.
outputs_dict = dict([(layer name, layer output) for layer in model layers])
layer_features = outputs_dict['block5_conv2']

이제 손실 계산에 사용할 층의 특징을 입력 텐서에서 추출할 수 있다.

content_image_features = layer features[0, :, :, :]
combined _features = layer_features[2, :, :, :]

마지막으로 콘텐츠 이미지와 통합 이미지 사이의 평균제곱오차를 계산해 줄 content_loss 함수를 정의한다.

콘텐츠 이미지의 특징을 보존해서 통합 이미지에 전달하기 위한 보조 손실 함수도 함께 정의한다.

# 콘텐츠와 통합의 평균제곱오차 계산
def content_loss(content_image, combined_image):
	return K.sum(K. square (combined - base))

# 콘텐츠 손실에 가중치를 곱한다.
content loss = content weight * content_loss(content_image_features,
combined features)

 

9.3.2 스타일 손실

스타일(화풍)은 이미지의 질감, 색감, 시각적 패턴 등을 의미한다.

1) 여러 층을 이용한 스타일 특징 표현

스타일 손실은 콘텐츠 손실보다 정의하기 어렵다. 콘텐츠 손실에서는 뒤쪽 층에서 추출되는 고수준 특징만을 고려하면 되었지만, 스타일 손실은 저수준, 중수준, 고수준 특징까지 여러 다른 배율에서 화풍을 추출해야 하기 때문에 신경망의 여러 층을 선택해야 한다.

이 방법을 통해 콘텐츠 이미지의 물체 배치 정보는 제외하고, 스타일 이미지의 질감만을 추출할 수 있다.

2) 행렬을 이용한 두 특징 맵의 활성화 정도 계산

그람 행렬을 사용하면 두 특징 맵이 함께 활성화되는 정도를 수치적으로 측정할 수 있다.

우리의 목표는 합성곱 신경망의 여러 층에서 질감과 화풍을 잡아내는 손실 함수를 만드는 것이다.

이를 위해서는 합성곱 신경망의 여러 층의 활성화 정도 사이의 상관관계를 계산해야 한다. 이 상관관계는 행렬과 특징 맵의 활성화 정도의 특징별 외적을 계산하는 방법으로 측정할 수 있다.

특징 맵의 그람 행렬을 구하려면 특징 맵을 1차원으로 변환해서 점곱을 계산하면 된다.

def gram_matrix(input_tensor):
  # We make the image channels first 
  channels = int(input_tensor.shape[-1])
  a = tf.reshape(input_tensor, [-1, channels])
  n = tf.shape(a)[0]
  gram = tf.matmul(a, a, transpose_a=True)
  return gram / tf.cast(n, tf.float32)

이제 style_loss 함수를 정의한다. 이 함수는 스타일 이미지와 통합 이미지에 대해 지정된 여러 층의 그람 행렬을 계산사고 이들의 제곱 오차를 계산해서 화풍의 유사도를 비교한다.

def get_style_loss(base_style, gram_target):
  """Expects two images of dimension h, w, c"""
  # height, width, num filters of each layer
  # We scale the loss at a given layer by the size of the feature map and the number of filters
  height, width, channels = base_style.get_shape().as_list()
  gram_style = gram_matrix(base_style)
  
  return tf.reduce_mean(tf.square(gram_style - gram_target))

이 예제에서는 VGG19 신경망 각 블록의 첫 번째 합성곱층에 해당하는 5개 층을 대상으로 스타일 손실을 계산한다.

# Style layer we are interested in
style_layers = ['block1_conv1',
                'block2_conv1',
                'block3_conv1', 
                'block4_conv1', 
                'block5_conv1'
               ]

마지막으로 feature_layers에 지정된 각 층에 대한 스타일 손실을 계산하며 된다.

# Accumulate style losses from all layers
  # Here, we equally weight each contribution of each loss layer
  weight_per_style_layer = 1.0 / float(num_style_layers)
  for target_style, comb_style in zip(gram_style_features, style_output_features):
    style_score += weight_per_style_layer * get_style_loss(comb_style[0], target_style)

신경망의 학습은 통합 이미지와 스타일 이미지의 스타일 손실을 최소화하도록 이루어진다.

이런 방법으로 스타일 이미지의 화풍이 통합 이미지에 전이될 수 있다.

 

9.3.3 총 분산 손실

총 분산 손실은 통합 이미지에 포함되는 노이즈 양을 나타낸다. 신경망은 이 손실을 최소화해서 통합 이미지의 노이즈를 최소화시킨다.

  1. 이미지를 한 픽셀씩 오른쪽으로 시프트한 다음 원본 이미지와 시프트된 이미지의 제곱오차를 계산한다.
  2. 이번에는 이미지를 한 픽셀 아래로 시프트한 다음 원본 이미지와 시프트된 이미지의 제곱오차를 계산한다.

두 제곱오차(a와 b)를 합한 값이 총 분산 손실이다.

def total_variation_loss(X):
    a = K. square(
        x[:, :img_nrows - 1, :img_ncols - 1, :] - x[:, 1:, :img_ncols - 1, :])
    b = K. square(
        x[:, :img_nrows - 1, :img_ncols - 1, :] - x[:, :img_nrows - 1, 1:, :])
	return K.sum(K.pow(a + b, 1.25))

# 총 분산 손실에도 가중치를 적용한다.
tv_loss = total_variation_weight * total _variation_loss(combined image)

 

9.3.4 신경망의 학습

손실 함수의 정의가 끝났으니 이 손실 함숫값이 최소가 되도록 경사 하강법 알고리즘을 사용한다.

구현 코드가 복잡하기 때문에 전체 코드를 확인하고 이해할 필요가 있을 것 같다.

import IPython.display

def run_style_transfer(content_path, 
                       style_path,
                       num_iterations=1000,
                       content_weight=1e3, 
                       style_weight=1e-2): 
  # We don't need to (or want to) train any layers of our model, so we set their
  # trainable to false. 
  model = get_model() 
  for layer in model.layers:
    layer.trainable = False
  
  # Get the style and content feature representations (from our specified intermediate layers) 
  style_features, content_features = get_feature_representations(model, content_path, style_path)
  gram_style_features = [gram_matrix(style_feature) for style_feature in style_features]
  
  # Set initial image
  init_image = load_and_process_img(content_path)
  init_image = tf.Variable(init_image, dtype=tf.float32)
  # Create our optimizer
  opt = tf.optimizers.Adam(learning_rate=5, beta_1=0.99, epsilon=1e-1)

  # For displaying intermediate images 
  iter_count = 1
  
  # Store our best result
  best_loss, best_img = float('inf'), None
  
  # Create a nice config 
  loss_weights = (style_weight, content_weight)
  cfg = {
      'model': model,
      'loss_weights': loss_weights,
      'init_image': init_image,
      'gram_style_features': gram_style_features,
      'content_features': content_features
  }
    
  # For displaying
  num_rows = 2
  num_cols = 5
  display_interval = num_iterations/(num_rows*num_cols)
  start_time = time.time()
  global_start = time.time()
  
  norm_means = np.array([103.939, 116.779, 123.68])
  min_vals = -norm_means
  max_vals = 255 - norm_means   
  
  imgs = []
  for i in range(num_iterations):
    grads, all_loss = compute_grads(cfg)
    loss, style_score, content_score = all_loss
    opt.apply_gradients([(grads, init_image)])
    clipped = tf.clip_by_value(init_image, min_vals, max_vals)
    init_image.assign(clipped)
    end_time = time.time() 
    
    if loss < best_loss:
      # Update best loss and best image from total loss. 
      best_loss = loss
      best_img = deprocess_img(init_image.numpy())

    if i % display_interval== 0:
      start_time = time.time()
      
      # Use the .numpy() method to get the concrete numpy array
      plot_img = init_image.numpy()
      plot_img = deprocess_img(plot_img)
      imgs.append(plot_img)
      IPython.display.clear_output(wait=True)
      IPython.display.display_png(Image.fromarray(plot_img))
      print('Iteration: {}'.format(i))        
      print('Total loss: {:.4e}, ' 
            'style loss: {:.4e}, '
            'content loss: {:.4e}, '
            'time: {:.4f}s'.format(loss, style_score, content_score, time.time() - start_time))
  print('Total time: {:.4f}s'.format(time.time() - global_start))
  IPython.display.clear_output(wait=True)
  plt.figure(figsize=(14,4))
  for i,img in enumerate(imgs):
      plt.subplot(num_rows,num_cols,i+1)
      plt.imshow(img)
      plt.xticks([])
      plt.yticks([])
      
  return best_img, best_loss
반응형