Это 15-й день моего участия в августовском испытании обновлений. Узнайте подробности события:Испытание августовского обновления
Deep Learning with Python
Эта статья — одна из серии заметок, которые я написал, изучая Deep Learning with Python (второе издание, Франсуа Шолле). Содержимое статьи конвертировано из блокнотов Jupyter в Markdown, вы можете перейти наGitHubилиGiteeнайти оригинал.ipynb
ноутбук.
ты можешь идтиЧитайте оригинальный текст этой книги онлайн на этом сайте(Английский). Автор этой книги также дает соответствиеJupyter notebooks.
Эта статьяГлава 8 Генеративное глубокое обучение (Chapter 8. Generative deep learning) одной из записок.
передача нейронного стиля
8.3 Neural style transfer
Передача нейронного стиля, нейронная сеть, основанная на глубоком обучении, применяет стиль эталонного изображения к целевому изображению, сохраняя при этом содержимое целевого изображения для создания нового изображения.
Идея переноса нейронного стиля проста: определить функцию потерь, указывающую, чего нужно достичь, а затем минимизировать эти потери. Цель здесь состоит в том, чтобы сохранить содержимое исходного изображения, приняв стиль эталонного изображения.
Если предположить, что существуют функции content и style, которые могут вычислять содержание и стиль входного изображения соответственно, а также существует функция расстояния парадигмы, то потери нейронной передачи стиля можно выразить как:
loss = distance(content(original_image) - content(generated_image)) +
distance(style(reference_image) - style(generated_image))
На самом деле, используя глубокие сверточные нейронные сети, можно математически определить функции стиля и содержания.
определение потери
- Потеря контента
Активации сверточной нейронной сети в нижних (передних) слоях содержат локальную информацию об изображении, а в верхних (задних) слоях содержится более глобальная, абстрактная информация. Содержимое — это глобальная и абстрактная информация изображения, поэтому активация слоя в верхней части сверточной нейронной сети может использоваться для представления содержимого изображения.
Таким образом, для предварительно обученной сверточной нейронной сети с выбранным слоем наверху потеря контента может использовать норму L2 между «активацией этого слоя на целевом изображении» и «активацией этого слоя на сгенерированном изображении».
- потеря стиля
В отличие от содержимого, которое может быть выражено только в одном слое, для определения стиля требуется несколько слоев. Стиль многогранен, например, мазки, линии, текстуры, цвета и т. д., которые проявляются на разных уровнях абстракции. Таким образом, выражение стиля должно отражать извлеченный внешний вид во всех пространственных масштабах, а не только в одном масштабе.
Согласно этой идее, выражение потери стиля может быть выполнено с помощью матрицы активации слоя Грама. Эта матрица Грама является внутренним произведением каждой карты признаков определенного слоя, выражающим отображение корреляции между признаками слоя, которое соответствует внешнему виду найденной текстуры в этом масштабе. Сохранение подобных внутренних взаимосвязей внутри разных активаций слоя можно считать «стилем».
Затем мы можем определить потерю стиля с точки зрения текстуры, которую сгенерированное изображение и эталонное изображение стиля поддерживают на разных слоях.
Реализация Keras передачи нейронного стиля
Перенос нейронного стиля может быть реализован с помощью любой предварительно обученной сверточной нейронной сети, здесь выбрана VGG19.
Этапы переноса нейронного стиля следующие:
- Создайте сеть, которая одновременно вычисляет активации слоя VGG19 для эталонных изображений стиля, целевых изображений и сгенерированных изображений;
- Используйте активацию слоя, вычисленную на этих трех изображениях, чтобы определить функцию потерь, описанную ранее;
- Градиентный спуск для минимизации этой функции потерь.
В этом примере требуется, чтобы режим выполнения «точно в срок» для Tensorflow 2.x был отключен:
import tensorflow as tf
tf.compat.v1.disable_eager_execution()
Перед началом построения сети определите путь к эталонному изображению стиля и целевому изображению. Если размер изображения сильно отличается, передача стиля будет сложнее, поэтому здесь мы также определяем размер единообразно:
from tensorflow.keras.preprocessing.image import load_img, img_to_array
target_image_path = '/home/CDFMLR/img/portrait.jpg'
style_referencce_image_path = 'img/transfer_style_reference.jpg'
width, height = load_img(target_image_path).size
img_height = 400
img_width = width * img_height // height
Вот фото, которое я выбрал:
- transfer_style_reference: Винсент Ван Гог, Кипарис в пшеничном поле (A Wheatfield, with Cypresses), 1889 г., в Метрополитен-музее, Нью-Йорк.
- портрет: Поль Гоген, Бретонские пастухи (The Swineherd, Brittany), 1888 г., в Художественном музее округа Лос-Анджелес, Калифорния, США.
Далее нам понадобятся вспомогательные функции для загрузки и обработки изображений.
import numpy as np
from tensorflow.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):
# vgg19.preprocess_input 会减去ImageNet的平均像素值,使其中心为0。这里做逆操作:
x[:, :, 0] += 103.939
x[:, :, 1] += 116.779
x[:, :, 2] += 123.680
# BGR -> RGB
x = x[:, :, ::-1]
x = np.clip(x, 0, 255).astype('uint8')
return x
Сеть VGG19 построена следующим образом: в качестве входных данных принимается пакет из трех изображений, эти три изображения представляют собой эталонное изображение стиля, константу целевого изображения и заполнитель для сохранения сгенерированного изображения.
from tensorflow.keras import backend as K
target_image = K.constant(preprocess_image(target_image_path))
style_reference_image = K.constant(preprocess_image(style_referencce_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)
определениеПотеря контента, чтобы убедиться, что целевое изображение и сгенерированное изображение имеют одинаковые результаты на верхнем уровне сети:
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.0 * (channels ** 2) * (size ** 2))
Здесь мы определяем дополнительную «полную потерю вариации», чтобы сгенерированное изображение имело пространственную непрерывность и избегало чрезмерной пикселизации результата, что эквивалентно регуляризации.
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))
Теперь рассмотрим конкретный расчет потерь: при расчете потерь контента нам нужен верхний слой; для потери стиля нам нужно использовать ряд слоев, включая верхний и нижний слои; и, наконец, нам нужно добавить общее количество вариационная потеря. Окончательный убыток представляет собой средневзвешенное значение этих трех типов убытков.
Определим окончательные потери, которые необходимо минимизировать:
outputs_dict = {layer.name: layer.output for layer in model.layers}
content_layer = 'block5_conv2'
style_layers = [f'block{i}_conv1' for i in range(1, 6)]
total_variation_weight = 1e-4
style_weight = 1.0
content_weight = 0.025 # content_weight越大,目标内容更容易在生成图像中越容易识别
# 内容损失
loss = K.variable(0.)
layer_features = outputs_dict[content_layer]
target_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
loss = loss + content_weight * content_loss(target_image_features, combination_features)
# 风格损失
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 = loss + (style_weight / len(style_layers)) * sl
# 总变差损失
loss = loss + total_variation_weight * total_variation_loss(combination_image)
Наконец, есть процесс градиентного спуска. Это вызывает scipy, сL-BFGS
алгоритм оптимизирован.
Для быстрых вычислений мы создаем класс 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.grads_values = grad_values
return self.loss_value
def grads(self, x):
assert self.loss_value is not None
grad_values = np.copy(self.grads_values)
self.loss_value = None
self.grad_values = None
return grad_values
evaluator = Evaluator()
Наконец, вызовите алгоритм SciPy L-BFGS для запуска процесса градиентного подъема, сохраняя текущее сгенерированное изображение после каждой итерации (20 шагов градиентного подъема):
from scipy.optimize import fmin_l_bfgs_b
from imageio import imsave
import time
iterations = 20
def result_fname(iteration):
return f'results/result_at_iteration_{iteration}.png'
x = preprocess_image(target_image_path)
x = x.flatten()
for i in range(iterations):
print('Start of iteration', i)
start_time = time.time()
x, min_val, info = fmin_l_bfgs_b(evaluator.loss,
x,
fprime=evaluator.grads,
maxfun=20)
print(' Current loss value:', min_val)
img = x.copy().reshape((img_height, img_width, 3))
img = deprocess_image(img)
fname = result_fname(i)
imsave(fname, img)
print(' Image saved as', fname)
end_time = time.time()
print(f' Iteration {i} completed in {end_time - start_time} s')
Выходная информация:
Start of iteration 0
Current loss value: 442468450.0
Image saved as results/result_at_iteration_0.png
Iteration 0 completed in 177.57321500778198 s
...
Start of iteration 19
Current loss value: 44762796.0
Image saved as results/result_at_iteration_19.png
Iteration 19 completed in 177.95070385932922 s
После завершения мы сравниваем сгенерированный результат с исходным изображением:
Хм, все равно интересно!
играть сноваДавайте посмотрим на другой пример:
Стилистическим ориентиром по-прежнему является « Кипарис на пшеничном поле» Ван Гога и «Сборщики урожая» Миллера ( Des glaneuses , 1857, Музей Орсе, Париж) по содержанию.
Еще интереснее то, что сам Ван Гог нарисовал произведение «Цвай грабенде Бауэриннен ауф шнеебедектем Фельд» (Zwei грабенде бауериннен ауф шнеебедектем Фельд), которое отчасти имитирует «Сборщики урожая»:
Видно, что наша машинка — это всего лишь простая и грубая передача стиля, и мастер сам воссоздаст ее в подражание.
Наконец, еще одно добавление. Этот алгоритм передачи стиля медленный, но достаточно простой. Чтобы добиться быстрой передачи стиля, подумайте:
- Во-первых, используйте описанный здесь метод, чтобы исправить эталонное изображение стиля и сгенерировать большое количество обучающих примеров «ввод-вывод» для различных изображений контента.
- Возьмите эти «ввод-вывод» для обучения простой сверточной нейронной сети изучению этого конкретного стиля преобразования (ввод->вывод).
- Как только это будет сделано, перенос определенного стиля на изображение будет очень быстрым, и это делается с помощью одного прямого прохода.