В августе 2015 года вышла статья под названием «Нейронный алгоритм художественного стиля». Тогда я только начинал заниматься глубоким обучением и попытался прочитать статью. Я не мог понять этого. Так что я сдался. Несколько месяцев спустя было выпущено приложение под названием Prisma, и люди просто сходили с ума от него. Если вы не знаете, что такое Prisma, это, по сути, приложение, которое позволяет вам применять стили рисования известных художников к вашим собственным фотографиям, и результаты получаются весьма визуально приятными. Это не фильтры Instagram, где к изображению применяется какое-то преобразование только в цветовом пространстве. Это намного сложнее, а результаты еще интереснее. Вот интересное фото, которое я нашла в Интернете.

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

По сути, в Neural Style Transfer у нас есть два изображения - стиль и контент. Нам нужно скопировать стиль из изображения стиля и применить его к изображению содержимого. Под стилем мы в основном подразумеваем узоры, мазки и т. Д.

Для этого мы используем предварительно обученную сеть VGG-16. В исходной статье используется сеть VGG-19, но я не смог найти веса VGG-19 для TensorFlow, поэтому выбрал VGG-16. Собственно, это (использование VGG-16 вместо VGG-19) не сильно влияет на конечные результаты. Полученные изображения идентичны.

Как это работает интуитивно?

Чтобы понять это, я дам вам базовую информацию о том, как работают ConvNets.

ConvNets работают по основному принципу свертки. Скажем, например, у нас есть изображение и фильтр. Мы перемещаем фильтр по изображению и берем на выходе взвешенную сумму входов, охваченных фильтром, преобразованных с помощью нелинейности, такой как сигмоид, ReLU или tanh. У каждого фильтра есть собственный набор весов, которые не меняются во время операции свертки. Это красиво изображено на GIF-изображении ниже.

Здесь синяя сетка - это вход. Вы можете увидеть область 3x3, покрытую ползунком фильтра на входе (темно-синяя область). Результат этой свертки называется картой характеристик, которая изображается зеленой сеткой.

Вот графики функций активации ReLU и tanh:

Итак, в ConvNet входное изображение сворачивается с помощью нескольких фильтров и генерируются карты фильтров. Эти карты фильтров затем сворачиваются с еще несколькими фильтрами, и создается еще несколько карт функций. Это прекрасно проиллюстрировано на диаграмме ниже.

Вы также можете увидеть термин maxpool на изображении выше. Maxpoollayer в основном используется с целью уменьшения размерности. В операции maxpool мы просто перемещаем окно, скажем, размером 2x2, по изображению и принимаем в качестве вывода максимальное значение, охватываемое окном. Вот пример-

А теперь кое-что действительно крутое. Внимательно посмотрите на изображение ниже и изучите карты функций на разных слоях.

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

Таким образом, мы можем сделать вывод, что ConvNets развивают иерархическое представление функций.

Это свойство - основа стилевого переноса.

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

Для этого важно, чтобы наша функция стоимости правильно представляла проблему.
Функция стоимости состоит из двух терминов - члена потери стиля и члена потери контента, оба из которых объясняются ниже.

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

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

P ^ l - это представление исходного изображения, а F ^ l - представление сгенерированного изображения на картах характеристик слоя l.

Утрата стиля

Здесь A ^ l - это представление исходного изображения, а G ^ l - представление сгенерированного изображения в слое l. Nl - это количество карт объектов, а Ml - размер плоской карты объектов в слое l. wl - вес, присвоенный потере стиля слоя l.

Под стилем мы в основном подразумеваем захват мазков и узоров кисти. Поэтому мы в основном используем нижние уровни, которые фиксируют низкоуровневые функции. Также обратите внимание на использование здесь граммовой матрицы. Матрица Грама вектора X равна X.X_transpose. Интуиция, лежащая в основе использования матрицы грамма, заключается в том, что мы пытаемся собрать статистику нижних слоев.
Однако вам не обязательно использовать матрицу Грама. Были опробованы некоторые другие статистические данные (например, среднее значение), и они тоже неплохо работают.

Общий убыток

где альфа и бета - веса содержания и стиля соответственно. Их можно настроить, чтобы изменить окончательный результат.

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

Теперь все, что нам нужно сделать, это минимизировать эту потерю. Мы минимизируем эту потерю, изменяя вход в саму сеть. В основном мы начинаем с пустого серого холста и начинаем изменять значения пикселей, чтобы минимизировать потери. Для минимизации этой потери можно использовать любой оптимизатор. Здесь для простоты я использовал простой градиентный спуск. Но люди использовали Adam и L-BFGS и получили неплохие результаты в этой задаче.

Этот процесс хорошо видно на следующем GIF-изображении.

Вот результаты некоторых моих экспериментов. Я запускал эти изображения (512x512 пикселей) для 1000 итераций, и этот процесс занимает около 25 минут на моем ноутбуке с 2 ГБ Nvidia GTX 940MX. Это займет гораздо больше времени, если вы используете его на ЦП, но и намного меньше, если у вас лучший графический процессор. Я слышал, что 1000 итераций на GTX Titan занимают всего 2,5 минуты.

Если вы хотите получить более глубокие знания о сверточных нейронных сетях, ознакомьтесь с курсом Stanford CS231n.

Итак, у вас есть собственная передача нейронного стиля. Вперед, реализуйте это и получайте удовольствие !!
До следующего раза!

Если вы хотите узнать больше о Neural Style Transfer и вариантах его использования, ознакомьтесь с отличной записью в блоге Fritz AI о Neural Style Transfer. Блог также содержит дополнительные ресурсы и руководства, которые помогут вам начать работу с вашим первым проектом Neural Style Transfer.