Вступление

Neural Transfer Style - одно из самых удивительных применений искусственного интеллекта в творческом контексте. В этом проекте мы увидим, как перенести стиль художественной росписи на выбранное изображение и получить потрясающие результаты. Леон А. Гэтис и др. придумали концепцию стиля нейронной передачи в своей статье Нейронный алгоритм художественного стиля в 2015 году. После этого многие исследователи применили и улучшили методологию, добавляя элементы в потери, пробуя разные оптимизаторы и экспериментируя с разными нейронными сетями, используемыми для этой цели.
Тем не менее, исходная статья остается лучшим источником для понимания этой концепции, а сети VGG16 и VGG19 являются наиболее часто используемыми моделями в этом контексте. Этот выбор, который является необычным, учитывая, что обе сети уступили самым последним сетям, подтверждается высочайшей производительностью, достигнутой при передаче стилей.

Вы можете проверить этот Репозиторий GitHub для получения полного кода.

Как это работает?

Цель этого метода - применить стиль изображения, который мы будем называть «стилем изображения», к целевому изображению, сохраняя его содержимое. Давайте определим эти два термина:

  • Стиль - это текстуры и визуальные узоры в изображении. Примером могут служить мазки кисти художника.
  • Контент - это макроструктура изображения. Люди, здания, предметы - примеры содержания изображения.

Здесь показан потрясающий результирующий эффект:

Хотите увидеть больше эффектов? Ознакомьтесь с ними в конце статьи!

Давайте посмотрим на общие шаги:

  • Выберите изображение для стиля
  • Выберите эталонное изображение стиля. Обычно это картины в своеобразном и хорошо узнаваемом стиле.
  • Инициализируйте предварительно обученную глубокую нейронную сеть и получите функциональные представления промежуточных слоев. Этот шаг выполняется для достижения представлений как изображения содержимого, так и изображения стиля. В изображении контента наилучшим вариантом является получение представлений функций самых высоких слоев, поскольку они содержат информацию о макроструктуре изображения. Для эталонного изображения стиля представления элементов получаются из нескольких слоев в разных масштабах.
  • Определите функцию потерь для минимизации как сумму потери содержимого, потери стиля и потери вариации. На каждой итерации оптимизатор генерировал изображение. Потеря содержимого - это разница (нормализация l2) между сгенерированным изображением и изображением содержимого, в то время как потеря стиля между сгенерированным изображением и стилем. Позже мы увидим, как эти переменные определяются математически.
  • Повторите минимизацию потерь

Обработка и депроцессинг изображений

Прежде всего, нам нужно отформатировать наши изображения для использования в нашей сети. CNN, который мы собираемся использовать, - это предварительно обученная свёрточная сеть VGG19. Поскольку мы обрабатываем изображение в совместимый массив, нам также необходимо декомпрессировать получившееся изображение, переключившись с формата BGR на формат RGB. Для этого построим две вспомогательные функции:

# Preprocessing image to make it compatible with the VGG19 model
def preprocess_image(image_path):
    img = load_img(image_path, target_size=(resized_width, resized_height))
    img = img_to_array(img)
    img = np.expand_dims(img, axis=0)
    img = vgg19.preprocess_input(img)
    return img

# Function to convert a tensor into an image
def deprocess_image(x):
    x = x.reshape((resized_width, resized_height, 3))

    # Remove zero-center by mean pixel. Necessary when working with VGG model
    x[:, :, 0] += 103.939
    x[:, :, 1] += 116.779
    x[:, :, 2] += 123.68

    # Format BGR->RGB
    x = x[:, :, ::-1]
    x = np.clip(x, 0, 255).astype('uint8')
    return x

Потеря контента

При потере содержимого сохраняется стиль основного входного изображения. Поскольку более высокие уровни сверточной нейронной сети содержат информацию о макроструктуре изображения, мы вычисляем потерю контента как разницу (нормализация l2) между выходными данными самого высокого уровня входного изображения и того же слоя сгенерированного изображения.
Потеря контента определяется как:

В уравнении F - это функциональное представление изображения содержимого (то, что выводит сеть, когда мы пропускаем наше входное изображение), а P - одно из сгенерированного изображения, на определенном скрытом слое l.
Вот реализация:

# The content loss maintains the features of the content image in the generated image.
def content_loss(layer_features):
    base_image_features = layer_features[0, :, :, :]
    combination_features = layer_features[2, :, :, :]
    return K.sum(K.square(combination_features - base_image_features))

Потеря стиля

Понять потерю стиля не так просто, как потерю контента. Цель состоит в том, чтобы сохранить стиль изображения (то есть визуальные шаблоны в виде мазков кисти) во вновь созданном изображении. В предыдущем случае мы сравниваем необработанные выходные данные промежуточных слоев. Здесь мы сравниваем разницу между матрицами Грама конкретных слоев для эталонного изображения стиля и сгенерированного изображения. Матрица Грама определяется как внутренний продукт векторной карты признаков данного слоя. Значение матрицы состоит в том, чтобы фиксировать корреляции между элементами слоя. Вычисление потерь для нескольких слоев позволяет сохранить аналогичные функции, внутренне коррелированные в разных слоях, между изображением стиля и сгенерированным изображением.
Потеря стиля для одного слоя рассчитывается как:

В уравнении A - это матрица Грама для изображения стиля, а G - это матрица Грама для сгенерированного изображения, оба по отношению к данному слою. N и M - ширина и высота изображения стиля.
В уравнении A - это матрица Грама для стиля. image, а G - матрица Грама для сгенерированного изображения, оба по отношению к данному слою. N и M - ширина и высота изображения стиля.
Потеря стиля вычисляется сначала для каждого отдельного слоя, а затем применяется к каждому слою, рассматриваемому для модели. стиль. Давайте реализуем это:

# The gram matrix of an image tensor is the inner product between the vectorized feature map in a layer.
# It is used to compute the style loss, minimizing the mean squared distance between the feature correlation map of the style image
# and the input image
def gram_matrix(x):
    features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
    gram = K.dot(features, K.transpose(features))
    return gram


# The style_loss_per_layer represents the loss between the style of the style reference image and the generated image.
# It depends on the gram matrices of feature maps from the style reference image and from the generated image.
def style_loss_per_layer(style, combination):
    S = gram_matrix(style)
    C = gram_matrix(combination)
    channels = 3
    size = resized_width * resized_height
    return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2))

# The total_style_loss represents the total loss between the style of the style reference image and the generated image,
# taking into account all the layers considered for the style transfer, related to the style reference image.
def total_style_loss(feature_layers):
    loss = K.variable(0.)
    for layer_name in feature_layers:
        layer_features = outputs_dict[layer_name]
        style_reference_features = layer_features[1, :, :, :]
        combination_features = layer_features[2, :, :, :]
        sl = style_loss_per_layer(style_reference_features, combination_features)
        loss += (style_weight / len(feature_layers)) * sl
    return loss

Вариационная потеря

Наконец, последняя составляющая потерь - это вариационные потери. Этот элемент не включен в исходный документ, и он не является строго необходимым для успеха проекта. Тем не менее, эмпирически было доказано, что добавление этого элемента дает лучший результат, поскольку сглаживает изменение цвета между близкими пикселями.
Давайте включим это:

# The total variation loss mantains the generated image loclaly coherent,
# smoothing the pixel variations among neighbour pixels.
def total_variation_loss(x):
    a = K.square(x[:, :resized_width - 1, :resized_height - 1, :] - x[:, 1:, :resized_height - 1, :])
    b = K.square(x[:, :resized_width - 1, :resized_height - 1, :] - x[:, :resized_width - 1, 1:, :])
    return K.sum(K.pow(a + b, 1.25))

Полная потеря

Наконец, общие потери рассчитываются с учетом всех этих вкладов. Во-первых, нам нужно извлечь выходные данные определенных слоев, которые мы выбираем. Для этого мы определяем словарь как ‹имя слоя, вывод слоя›:

# Get the outputs of each key layer, through unique names.
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])

Затем мы вычисляем потери, вызывая ранее кодированные функции. Каждый компонент умножается на определенный вес, который мы можем настроить для получения более интенсивных или более легких эффектов:

def total_loss():
    loss = K.variable(0.)

    # contribution of content_loss
    feature_layers_content = outputs_dict['block5_conv2']
    loss += content_weight * content_loss(feature_layers_content)

    # contribution of style_loss
    feature_layers_style = ['block1_conv1', 'block2_conv1',
                            'block3_conv1', 'block4_conv1',
                            'block5_conv1']
    loss += total_style_loss(feature_layers_style) * style_weight

    # contribution of variation_loss
    loss += total_variation_weight * total_variation_loss(combination_image)
    return loss

Настройка нейронной сети

Сеть VGG19 принимает в качестве входных данных пакет из трех изображений: изображение входного контента, эталонное изображение стиля и символьный тензор, который содержит сгенерированное изображение. Первые две являются постоянными переменными и определяются как Variable с помощью пакета keras.backend. Третья переменная определяется как заполнитель, поскольку со временем она будет меняться, пока оптимизатор обновляет результаты.

После того, как переменные инициализированы, мы объединили их в тензор, который позже будет питать сеть.

# Get tensor representations of our images
base_image = K.variable(preprocess_image(base_image_path))
style_reference_image = K.variable(preprocess_image(style_reference_image_path))

# Placeholder for generated image
combination_image = K.placeholder((1, resized_width, resized_height, 3))

# Combine the 3 images into a single Keras tensor
input_tensor = K.concatenate([base_image,
                              style_reference_image,
                              combination_image], axis=0)

Как только это будет сделано, нам нужно определить потери, градиенты и результат. В исходной статье в качестве оптимизатора используется алгоритм L-BFGS. Одним из ограничений этого алгоритма является то, что он требует раздельной передачи потерь и градиентов. Поскольку их независимое вычисление было бы крайне неэффективным, мы реализуем класс Evaluator, который вычисляет значения потерь и градиентов одновременно, но возвращает их по отдельности. Давай сделаем это:

loss = total_loss()

# Get the gradients of the generated image
grads = K.gradients(loss, combination_image)
outputs = [loss]
outputs += grads

f_outputs = K.function([combination_image], outputs)

# Evaluate the loss and the gradients respect to the generated image. It is called in the Evaluator, necessary to
# compute the gradients and the loss as two different functions (limitation of the L-BFGS algorithm) without
# excessive losses in performance
def eval_loss_and_grads(x):
    x = x.reshape((1, resized_width, resized_height, 3))
    outs = f_outputs([x])
    loss_value = outs[0]
    if len(outs[1:]) == 1:
        grad_values = outs[1].flatten().astype('float64')
    else:
        grad_values = np.array(outs[1:]).flatten().astype('float64')
    return loss_value, grad_values

# Evaluator returns the loss and the gradient in two separate functions, but the calculation of the two variables
# are dependent. This reduces the computation time, since otherwise it would be calculated separately.
class Evaluator(object):

    def __init__(self):
        self.loss_value = None
        self.grads_values = None

    def loss(self, x):
        assert self.loss_value is None
        loss_value, grad_values = eval_loss_and_grads(x)
        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()

Последний шаг

Наконец-то все настроено! Последний шаг - выполнить несколько итераций оптимизатора, пока мы не достигнем желаемых потерь или желаемого результата. Мы будем сохранять результат по итерациям, чтобы убедиться, что алгоритм работает должным образом. Если результат не удовлетворительный, мы можем поиграть с весами, чтобы улучшить сгенерированное изображение.

# The oprimizer is fmin_l_bfgs
for i in range(iterations):
    print('Iteration: ', i)
    x, min_val, info = fmin_l_bfgs_b(evaluator.loss,
                                     x.flatten(),
                                     fprime=evaluator.grads,
                                     maxfun=15)

    print('Current loss value:', min_val)

    # Save current generated image
    img = deprocess_image(x.copy())
    fname = 'img/new' + np.str(i) + '.png'
    save(fname, img)

Чтобы увидеть весь код, перейдите по ссылке GitHub в начале страницы.

Потрясающие результаты

Если вы хотите поэкспериментировать с конкретными эффектами, картинами или у вас есть какие-либо предложения, просто оставьте комментарий!

Если вам понравилась эта статья, я буду рад, если вы нажмете кнопку хлопка 👏 , чтобы другие могли наткнуться на нее. Не стесняйтесь оставлять комментарии для любых комментариев или предложений!

Я специализируюсь в области науки о данных, и люблю машинное обучение и его бесконечные приложения. Вы можете найти больше обо мне и моих проектах на maurocomi.com. Вы также можете найти меня в Linkedin или написать мне по электронной почте. Я всегда готов поболтать или поработать над новыми замечательными проектами.