В сегодняшнем руководстве мы изучим принципы нейронной передачи художественного стиля и покажем рабочий пример переноса стиля искусства Ван Гога на изображение.
Нейронная передача художественного стиля
Образ можно рассматривать как сочетание стиля и содержания. Техника передачи художественного стиля преобразует изображение, чтобы оно выглядело как картина с определенным стилем рисования. Мы увидим, как кодировать эту идею. Функция потери сравнивает сгенерированное изображение с содержимым фотографии и стилем картины. Следовательно, оптимизация выполняется для пикселя изображения, а не для весов сети. Два значения вычисляются путем сравнения содержимого фотографии со сгенерированным изображением, за которым следует стиль картины и созданное изображение.
Потеря контента (content loss)
Поскольку пиксели - не лучший выбор, мы будем использовать признаки CNN различных слоев, поскольку они лучше представляют контент. Начальные слои имеют высокую частоту, то есть это края, углы и текстуры, но более поздние слои представляют объекты и, следовательно, лучше подходят для содержимого. Последний слой может сравнивать объект с объектом лучше, чем пиксели. Но для этого нам нужно сначала импортировать необходимые библиотеки, используя следующий код:
import numpy as np from
PIL import Image
from scipy.optimize import fmin_l_bfgs_b
from scipy.misc import imsave
from vgg16_avg import VGG16_Avg
from keras import metrics
from keras.models import Model
from keras import backend as K
Теперь давайте загрузим необходимое изображение, используя следующую команду:
content_image = Image.open(work_dir + 'bird_orig.png')
Для этого случая мы будем использовать следующее изображение:
Поскольку мы используем архитектуру VGG для извлечения функций, среднее значение всех изображений ImageNet необходимо вычесть из всех изображений, как показано в следующем коде:
imagenet_mean = np.array([123.68, 116.779, 103.939], dtype=np.float32)
def subtract_imagenet_mean(image):
return (image - imagenet_mean)[:, :, :, ::-1]
Обратите внимание, что каналы разные. Функция предварительной обработки берет сгенерированное изображение и вычитает среднее значение, а затем меняет канал. Функция deprocess отменяет этот эффект из-за этапа предварительной обработки, как показано в следующем коде:
def add_imagenet_mean(image, s):
return np.clip(image.reshape(s)[:, :, :, ::-1] + imagenet_mean, 0, 255)
Сначала мы увидим, как создать изображение с содержимым из другого изображения. Это процесс создания изображения из случайного шума. Используемый здесь контент представляет собой сумму активации на некотором уровне. Мы минимизируем потерю содержимого между случайным шумом и изображением, что называется потерей содержимого (content loss). Эта потеря аналогична потере по пикселям, но применяется к активациям слоя, следовательно, захватывает контент без шума. Любая архитектура CNN может использоваться для прямого вывода изображения контента и случайного шума. Принимаются активации, и вычисляется среднеквадратическая ошибка, сравнивая активации этих двух выходов.
Пиксель случайного изображения обновляется, а веса CNN замораживаются. Мы заморозим сеть VGG на этот случай. Теперь можно загрузить модель VGG. Генеративные изображения очень чувствительны к методам подвыборки, таким как max pooling. Возвращение значений пикселей из max pooling невозможно. Следовательно, average pooling является более плавным методом, чем max pooling.
Функция преобразования модели VGG с average pooling используется для загрузки модели, как показано ниже:
vgg_model = VGG16_Avg(include_top=False)
Обратите внимание, что веса этой модели такие же, как и у исходной, даже несмотря на то, что тип объединения был изменен. Модели ResNet и Inception для этого не подходят из-за их неспособности предоставлять различные абстракции. Мы возьмем активации из последнего сверточного слоя модели VGG, а именно block_conv1, пока модель была заморожена. Это третий последний слой VGG с широким восприимчивым полем. Его код приведен здесь для справки:
content_layer = vgg_model.get_layer('block5_conv1').output
Теперь создается новая модель с VGG, усеченным до слоя, который давал хорошие характеристики. Следовательно, изображение может быть загружено сейчас и может использоваться для выполнения прямого вывода, чтобы получить фактически активированные слои. Для регистрации активации создается переменная TensorFlow с использованием следующего кода:
content_model = Model(vgg_model.input, content_layer)
content_image_array =
subtract_imagenet_mean(np.expand_dims(np.array(content_image), 0))
content_image_shape = content_image_array.shape
target = K.variable(content_model.predict(content_image_array))
Давайте определим класс оценщика для вычисления потерь и градиентов изображения. Следующий класс возвращает значения потерь и градиента в любой точке итерации:
class ConvexOptimiser(object):
def __init__(self, cost_function, tensor_shape):
self.cost_function = cost_function
self.tensor_shape = tensor_shape
self.gradient_values = None
def loss(self, point):
loss_value, self.gradient_values =
self.cost_function([point.reshape(self.tensor_shape)])
return loss_value.astype(np.float64)
def gradients(self, point):
return self.gradient_values.flatten().astype(np.float64)
Функцию потерь можно определить как среднеквадратическую ошибку между значениями активаций на определенных сверточных слоях. Потери будут вычисляться между слоями сгенерированного изображения и исходной фотографии содержимого, как показано ниже:
mse_loss = metrics.mean_squared_error(content_layer, target)
Градиенты потерь можно вычислить, рассматривая входные данные модели, как показано ниже:
grads = K.gradients(mse_loss, vgg_model.input)
Входные данные функции являются входными данными модели, а выходными данными будет массив значений потерь и градиентов, как показано ниже:
cost_function = K.function([vgg_model.input], [mse_loss]+grads)
Эта функция детерминирована для оптимизации, поэтому SGD не требуется:
optimiser = ConvexOptimiser(cost_function, content_image_shape)
Эту функцию можно оптимизировать с помощью простого оптимизатора, поскольку она выпуклая и, следовательно, детерминированная. Мы также можем сохранять изображение на каждом этапе итерации. Мы определим его таким образом, чтобы были доступны градиенты, поскольку мы используем оптимизатор scikit-learn для окончательной оптимизации. Обратите внимание, что эта функция потерь является выпуклой, поэтому для вычислений достаточно простого оптимизатора. Оптимизатор можно определить с помощью следующего кода:
def optimise(optimiser, iterations, point, tensor_shape, file_name):
for i in range(iterations):
point, min_val, info = fmin_l_bfgs_b(optimiser.loss,
point.flatten(),
fprime=optimiser.gradients, maxfun=20)
point = np.clip(point, -127, 127)
print('Loss:', min_val)
imsave(work_dir + 'gen_'+file_name+'_{i}.png',
add_imagenet_mean(point.copy(), tensor_shape)[0])
return point
Оптимизатор принимает функцию потерь, точку и градиенты и возвращает обновления. Необходимо сгенерировать случайное изображение, чтобы минимизировать потерю содержимого, используя следующий код:
def generate_rand_img(shape):
return np.random.uniform(-2.5, 2.5, shape)/1
generated_image = generate_rand_img(content_image_shape)
Вот случайное изображение, которое создается:
Оптимизацию можно запустить в течение 10 итераций, чтобы увидеть результаты:
iterations = 10
generated_image = optimise(optimiser, iterations, generated_image,
content_image_shape, 'content')
Если все пойдет хорошо, должны выводиться потери на итерациях, как показано здесь:
Current loss value: 73.2010421753
Current loss value: 22.7840042114
Current loss value: 12.6585302353
Current loss value: 8.53817081451
Current loss value: 6.64649534225
Current loss value: 5.56395864487
Current loss value: 4.83072710037
Current loss value: 4.32800722122
Current loss value: 3.94804215431
Current loss value: 3.66387653351
Вот изображение, которое сгенерировано, и теперь оно почти похоже на птицу. Можно запустить оптимизацию для дальнейших итераций, чтобы это было сделано:
Оптимизатор взял изображение и обновил пиксели, чтобы контент остался прежним. Хотя результаты хуже, он может в определенной степени воспроизводить изображение с контентом. Все изображения при итерациях дают хорошее представление о том, как создается изображение. В этом процессе нет пакетной обработки. В следующем разделе мы увидим, как создать изображение в стиле картины.
Потеря стиля с использованием матрицы Грама
После создания изображения, которое включает содержимое исходного изображения, мы увидим, как создать изображение, используя только стиль. Стиль можно представить как сочетание цвета и текстуры изображения. Для этого мы определим потерю стиля. Сначала мы загрузим изображение и преобразуем его в массив, как показано в следующем коде:
style_image = Image.open(work_dir + 'starry_night.png')
style_image = style_image.resize(np.divide(style_image.size,
3.5).astype('int32'))
Вот загруженное нами изображение стиля:
Теперь мы предварительно обработаем это изображение, изменив каналы, используя следующий код:
style_image_array = subtract_imagenet_mean(np.expand_dims(style_image,
0)[:, :, :, :3])
style_image_shape = style_image_array.shape
Для этого мы рассмотрим несколько слоев, как в следующем коде:
model = VGG16_Avg(include_top=False, input_shape=shp[1:])
outputs = {l.name: l.output for l in model.layers}
Теперь мы возьмем несколько слоев в качестве вывода массива первых четырех блоков, используя следующий код:
layers = [outputs['block{}_conv1'.format(o)] for o in range(1,3)]
Теперь создается новая модель, которая может выводить все эти слои и назначать целевые переменные, используя следующий код:
layers_model = Model(model.input, layers)
targs = [K.variable(o) for o in layers_model.predict(style_arr)]
Потеря стиля рассчитывается с использованием матрицы Грама. Матрица Грама - это произведение матрицы и ее транспонирования. Значения активации просто транспонируются и умножаются. Затем эта матрица используется для вычисления ошибки между стилем и случайными изображениями. Матрица Грама теряет информацию о местоположении, но сохраняет информацию о текстуре. Мы определим матрицу Грама, используя следующий код:
def grammian_matrix(matrix):
flattened_matrix = K.batch_flatten(K.permute_dimensions(matrix, (2, 0,
1)))
matrix_transpose_dot = K.dot(flattened_matrix,
K.transpose(flattened_matrix))
element_count = matrix.get_shape().num_elements()
return matrix_transpose_dot / element_count
Как вы, возможно, теперь знаете, это мера корреляции между парой столбцов. Размеры по высоте и ширине сглажены. Она не включает какие-либо локальные данные, так как информация о координатах не учитывается. Функция потери стиля вычисляет среднеквадратичную ошибку между матрицей Грама входного изображения и целевым изображением, как показано в следующем коде:
def style_mse_loss(x, y):
return metrics.mse(grammian_matrix(x), grammian_matrix(y))
Теперь давайте вычислим потери, суммируя все активации из различных слоев, используя следующий код:
style_loss = sum(style_mse_loss(l1[0], l2[0]) for l1, l2 in
zip(style_features, style_targets))
grads = K.gradients(style_loss, vgg_model.input)
style_fn = K.function([vgg_model.input], [style_loss]+grads)
optimiser = ConvexOptimiser(style_fn, style_image_shape)
Затем мы решаем его так же, как и раньше, создавая случайное изображение. Но на этот раз мы также применим фильтр Гаусса, как показано в следующем коде:
generated_image = generate_rand_img(style_image_shape)
Сгенерированное случайное изображение будет выглядеть так:
Оптимизацию можно запустить для 10 итераций, чтобы увидеть результаты, как показано ниже:
generated_image = optimise(optimiser, iterations, generated_image,
style_image_shape)
Если все пойдет хорошо, должны быть выведены значения потерь, подобные следующим:
Current loss value: 5462.45556641
Current loss value: 189.738555908
Current loss value: 82.4192581177
Current loss value: 55.6530838013
Current loss value: 37.215713501
Current loss value: 24.4533748627
Current loss value: 15.5914745331
Current loss value: 10.9425945282
Current loss value: 7.66888141632
Current loss value: 5.84042310715
Вот полученное изображение:
Здесь из случайного шума мы создали изображение с определенным стилем рисования без какой-либо информации о местоположении. В следующем разделе мы увидим, как совместить и то, и другое - потерю контента и стиля.
Перенос стиля
Теперь мы знаем, как реконструировать изображение, а также как создать изображение, которое отражает стиль исходного изображения. Очевидной идеей может быть просто объединить эти два подхода путем взвешивания и добавления двух функций потерь, как показано в следующем коде:
w,h = style.size
src = img_arr[:,:h,:w]
Как и раньше, мы собираемся получить последовательность выходных слоев, чтобы вычислить потерю стиля. Однако нам по-прежнему нужен только один выходной слой, чтобы вычислить потерю контента. Как мы узнаем, какой слой взять? Как мы обсуждали ранее, чем ниже уровень, тем точнее будет реконструкция содержимого. При объединении реконструкции контента со стилем мы можем ожидать, что более свободная реконструкция контента предоставит больше места для влияния стиля (re: inspiration). Более того, более поздний слой гарантирует, что изображение выглядит как тот же объект, даже если на нем нет тех же деталей. Для этого процесса используется следующий код:
style_layers = [outputs['block{}_conv2'.format(o)] for o in range(1,6)]
content_name = 'block4_conv2'
content_layer = outputs[content_name]
Теперь создается отдельная модель для стиля с необходимыми выходными слоями, используя следующий код:
style_model = Model(model.input, style_layers)
style_targs = [K.variable(o) for o in style_model.predict(style_arr)]
Мы также создадим другую модель для контента со слоем контента, используя следующий код:
content_model = Model(model.input, content_layer)
content_targ = K.variable(content_model.predict(src))
Теперь объединить два подхода так же просто, как объединить их соответствующие функции потерь. Обратите внимание, что в отличие от наших предыдущих функций, эта функция производит три отдельных типа выходных данных:
- Один для исходного изображения.
- Один для изображения, стилю которого мы подражаем.
- Один для случайного изображения, пиксели которого мы обучаем.
Один из способов настроить микширование реконструкций - это изменить коэффициент потери контента, который здесь равен 1/10. Если мы увеличим знаменатель, стиль будет иметь большее влияние на изображение, а если он будет слишком большим, исходное содержание изображения будет затемнено неструктурированным стилем. Точно так же, если он слишком маленький, изображение не будет иметь достаточного стиля. Мы будем использовать следующий код для этого процесса:
style_wgts = [0.05,0.2,0.2,0.25,0.3]
Функция потерь принимает слои стиля и содержимого, как показано здесь:
loss = sum(style_loss(l1[0], l2[0])*w
for l1,l2,w in zip(style_layers, style_targs, style_wgts))
loss += metrics.mse(content_layer, content_targ)/10
grads = K.gradients(loss, model.input)
transfer_fn = K.function([model.input], [loss]+grads)
evaluator = Evaluator(transfer_fn, shp)
Мы будем запускать процесс для 10 итераций, как и раньше, используя следующий код:
iterations=10
x = rand_img(shp)
x = solve_image(evaluator, iterations, x)
Значения потерь должны быть выведены, как показано здесь:
Current loss value: 2557.953125
Current loss value: 732.533630371
Current loss value: 488.321166992
Current loss value: 385.827178955
Current loss value: 330.915924072
Current loss value: 293.238189697
Current loss value: 262.066864014
Current loss value: 239.34185791
Current loss value: 218.086700439
Current loss value: 203.045211792
Это замечательные результаты. Каждый из них отлично справляется с воссозданием оригинального образа в стиле художника. Сгенерированное изображение будет выглядеть следующим образом:
На этом мы завершаем раздел о переносе стилей. Это очень медленная операция, но может работать с любыми изображениями. В следующем разделе мы увидим, как использовать аналогичную идею для создания сети сверхвысокого разрешения. Есть несколько способов сделать это лучше, например:
- Добавление фильтра Гаусса к случайному изображению
- Добавление разных весов к слоям
- Для контента могут использоваться разные слои и веса.
- Инициализация изображения, а не случайного изображения
- Цвет можно сохранять
- Для указания того, что нам требуется, можно использовать маски
- Любой эскиз можно преобразовать в картину
- Рисование эскиза и создание изображения
- Любое изображение можно преобразовать в художественный стиль, обучив CNN выводить такое изображение.
Подводя итог, мы научились реализовывать перенос стиля с одного изображения на другое, сохраняя контент как есть.
Вы читали отрывок из книги Rajalingappaa Shanmugamani под названием «Deep Learning for Computer Vision». Из этой книги вы узнаете, как моделировать и обучать продвинутые нейронные сети для реализации различных задач компьютерного зрения.
Комментариев нет:
Отправить комментарий