Эксперименты с переносом нейронного стиля с предварительно обученными моделями VGG16, VGG19 и ResNet50. Код для этого проекта можно найти здесь.
Перенос нейронного стиля - это метод модификации изображения (впервые разработанный и представленный Леоном Гэтисом и др.), Который применяет стиль одного изображения к содержимому другого изображения. В этом сообщении блога мы рассмотрим, как это можно применить, используя предварительно обученную модель ConvNet от Keras.
Выше - конкретный пример передачи нейронного стиля. В этом сценарии стиль определяется как текстуры, цвета и общие узоры (например, мазки кисти) в изображении. Картина Ван Гога «Звездная ночь» - популярный выбор в качестве стилевого образа из-за своего очевидного стиля.
Ключ к успешной передаче стиля лежит в функции потерь. Мы хотим минимизировать потерю содержимого между изображением содержимого и сгенерированным изображением, а также минимизировать изображение стиля между изображением стиля и сгенерированным изображением. Давайте сначала посмотрим на потерю контента:
Потеря контента:
Более ранние уровни в наших сетях связаны с большим количеством локальной информации, в то время как более высокие уровни будут содержать активации с глобальной информацией. Поскольку содержимое определяется макроструктурой изображения (например, структурой здания в приведенном выше примере), самые верхние слои в нашей сети будут захватывать содержимое изображения. Таким образом, мы просто вычислим потерю контента как расстояние L2 (сумма квадратов разностей) между активациями верхнего слоя, вычисленными для изображения контента и сгенерированного изображения.
def content_loss(base, combination): return K.sum(K.square(combination - base))
Потеря стиля:
Чтобы зафиксировать потерю стиля, мы будем использовать метод матрицы Грама, описанный в оригинальной переводной бумаге в стиле 2015 года. Матрица Грама - это внутренний продукт карты функций с заданным слоем. В результате получается карта корреляций между элементами слоя. Эти корреляции функций - это то, что фиксирует узоры текстуры в определенном пространственном масштабе, который соответствует физическому виду текстур / цветов / узоров в этом масштабе. Таким образом, функция потери стиля направлена на минимизацию различий между корреляциями признаков при каждой активации слоя от изображения стиля к сгенерированному изображению.
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))
Мы также будем использовать полную потерю вариаций, которая используется для поощрения пространственной непрерывности и предотвращения чрезмерной пикселизации:
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))
Теперь мы готовы приступить к переносу стиля. Вот общие шаги, которые мы предпримем для каждой из следующих трех частей этого поста:
- Настройте сеть для одновременного вычисления активаций стиля, контента и сгенерированных изображений.
- Используйте эти активации для вычисления потерь. Общий убыток будет средневзвешенным:
style_loss
,content_loss
,and total_variation_loss
. - Используйте градиентный спуск, чтобы минимизировать функцию потерь для получения окончательного сгенерированного изображения.
Часть 1 с использованием VGG19
Начнем с настройки нашей модели. Мы объединяем наш стиль, контент и сгенерированное изображение (пока что заполнитель). Это позволяет нам вычислить активации для всех трех одновременно. Мы передадим наш объединенный input_tensor
в качестве аргумента нашей модели VGG19, чтобы соответствующим образом изменить форму слоев.
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)) # This placeholder will contain our generated image combination_image = K.placeholder((1, img_height, img_width, 3)) # We combine the 3 images into a single batch input_tensor = K.concatenate([target_image, style_reference_image, combination_image], axis=0) # We build the VGG19 network with our batch of 3 images as input. # The model will be loaded with pre-trained ImageNet weights. model = vgg19.VGG19(input_tensor=input_tensor, weights='imagenet', include_top=False)
Затем мы определим, какие слои использовать для определения содержания и стиля. 'block5_conv2'
- это слой верхнего уровня Conv2D
в нашей модели VGG19, поэтому мы будем использовать его для оценки контента. Что касается style_layers
, мы будем использовать различные Conv2D
слои, расположенные от верха до низа сети, чтобы вычислить ошибку стиля. Мы также присвоим веса стилям, содержанию и общему отклонению, которые будут использоваться для расчета взвешенных общих потерь.
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.0 content_weight = 0.025 # Define the loss by adding all components to a `loss` variable loss = K.variable(0.) layer_features = outputs_dict[content_layer] target_image_features = layer_features[0, :, :, :] combination_features = layer_features[2, :, :, :] 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 += (style_weight / len(style_layers)) * sl loss += total_variation_weight * total_variation_loss(combination_image)
Мы также будем использовать класс Python Evaluator
. Этот класс позволяет нам одновременно вычислять функцию потерь и градиенты. По сути, он удаляет избыточные вычисления при вычислении этих двух значений и ускоряет нашу программу в 2 раза.
# Get the gradients of the generated image grads = K.gradients(loss, combination_image)[0] # fetch the values of the current loss and the current gradients 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()
Теперь мы запустим наш процесс градиентного спуска, используя алгоритм SciPy L-BFGS. Мы выполним 10 итераций, каждая из которых состоит из 10 шагов градиентного спуска. Мы также сохраняем сгенерированное изображение на каждой итерации, чтобы мы могли отслеживать наш прогресс.
from scipy.optimize import fmin_l_bfgs_b from scipy.misc import imsave import time result_prefix = 'vgg19_try1' iterations = 10 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=10) print('Current loss value:', min_val) # Save current generated image img = x.copy().reshape((img_height, img_width, 3)) img = deprocess_image(img) fname = result_prefix + '_at_iteration_%d.png' % i imsave(fname, img) end_time = time.time() print('Image saved as', fname) print('Iteration %d completed in %ds' % (i, end_time - start_time))
Вот результаты:
Передача стиля сработала прилично. Однако стиль изображения, который мы выбрали выше, не очень хорош. В нем отсутствуют четко очерченные узоры и текстуры. Давайте попробуем еще раз с другой моделью и новым стилем изображения.
Часть 2 с использованием VGG16
На этот раз мы будем использовать модель VGG16 от Keras, показанную ниже:
model = vgg16.VGG16(input_tensor=input_tensor, weights='imagenet', include_top=False)
Нам также необходимо обновить нашу preprocess_image
функцию, чтобы отразить это изменение:
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 = vgg16.preprocess_input(img) return img
Мы также увеличили style_weight
и уменьшили content_weight
, чтобы получить лучшие результаты (также чтобы компенсировать переход на VGG16 с VGG19). Вот результаты:
Мы умеем добиваться отличных результатов. Содержимое изображения определенно сохраняется, а также цвета и текстуры, найденные в образе стиля.
Часть 3 с ResNet50
На этот раз мы попытаемся перенести стиль с помощью ResNet50 от Keras:
model = resnet50.ResNet50(input_tensor=input_tensor, weights='imagenet', include_top=False, pooling='max')
С первой попытки мы будем использовать следующие значения:
content_layer = 'res5b_branch2a' style_layers = ['res3a_branch2a','res4a_branch2a','res5a_branch2a'] total_variation_weight = 0 style_weight = 400000 content_weight = 0.0001
Вот результаты после 10 итераций:
К сожалению, нам не удалось полностью передать стиль изображения стиля. Мы видим зачатки текстуры, вдохновленные стилем изображения. Однако после изучения сгенерированного изображения я понял, что многие цвета, присутствующие в изображении стиля, не найдены в изображении содержания. Мы видим, что основная часть изображения контента - это некоторый оттенок синего. Поскольку изображение стиля не содержит много синего, не так много стиля, который можно применить из изображения стиля к изображению содержания. Таким образом, мы получаем сгенерированное изображение, которое выглядит наполовину законченным.
Совет. Убедитесь, что вы используете изображение стиля с такой же цветовой палитрой, что и изображение в содержании (иначе стиль не будет хорошо применен).
Возвращаясь к изображению, которое мы использовали в части 2, мы получаем следующие результаты:
У нас получилось немного лучше. Однако ясно, что ResNet50 намного хуже переносит стиль, чем сети VGG. Вот несколько причин, по которым это может быть:
- Сети VGG очень велики (обе имеют размер 500 МБ + по сравнению с 99 МБ ResNet50) - таким образом, они могут случайно захватывать и хранить больше информации, чем другие модели.
- VGG относительно неглубокие и модульные - нет остаточных соединений или ярлыков для пропуска необработанных данных через слои. В результате получается четкая иерархическая серия абстракций. ResNet, с другой стороны, слишком рассредоточен по слоям; отдельные функции существуют, но их можно смешивать на многих уровнях абстракции.
- VGG не так агрессивно понижает дискретизацию по сравнению с другими моделями (максимальное объединение слоев только после нескольких сверток).
Дальнейшая работа:
Вот несколько предлагаемых способов проверить причины, указанные выше:
- Обучите гораздо большие ResNet / гораздо меньшие группы VGG, чтобы увидеть, уменьшится ли разрыв в производительности. Если небольшая группа VGG не может выполнять передачу стилей лучше, чем сеть ResNet того же размера, это говорит о том, что преимущество VGG заключается в размере модели.
- Поэкспериментируйте с расчетом характеристик в ResNet. Поскольку объекты могут быть распределены тонко по многим слоям, создайте метод суммирования карт объектов по глубине перед вычислением матрицы Грама.
- Перебором множества комбинаций для слоев контента и стилей для ResNet50. Я уже пробовал множество комбинаций (это может быть связано с конкретным изображением, поскольку слои разных стилей лучше подходят для разных типов изображений).