Человек отлично справляется с созданием уникального стиля для произведений искусства, но очень трудно заставить компьютер изучать чей-то стиль. Это потому, что «стиль» является настолько абстрактным термином, что нет четкого определения «стиля». Ссылаясь на статью Gatys et al, он предложил алгоритм для применения стиля изображения к заданному изображению контента. Мы реализуем его в этой статье.

Передача нейронного стиля. Гатис и др. (2015) (https://arxiv.org/abs/1508.06576)

Прежде всего, нам нужно будет выбрать устройство для выполнения модели, гораздо быстрее запустить сеть на GPU, чем на CPU. Здесь мы проверяем, доступен ли GPU для обучения.

Вспомогательные функции

Теперь мы создаем вспомогательную функцию для загрузки/отображения изображения. Сначала мы изменяем размер изображения до желаемого разрешения и преобразуем его в тензор PyTorch. Исходное изображение PIL имеет значение от 0 до 255 для всех каналов RGB, и тензор PyTorch преобразует его в значение от 0 до 1. Важно понимать, что все модели PyTorch обучаются с диапазоном значений от 0 до 1.

Изображение содержимого и изображение стиля

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

Нормализация изображения

Еще одно важное скрытое правило заключается в том, что модель VGG19, предоставляемая PyTorch, обучается с нормализованным изображением. Поэтому нам также нужно нормализовать наш контент и стиль изображения, прежде чем отправлять его в сеть, иначе вы получите неожиданный результат, и будет очень сложно обнаружить проблему.

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

Для начала нам нужно определить, что такое «стиль». Автор статьи предложил разделить проблему на две части: потеря контента и потеря стиля. Содержимое измеряет расстояние между сгенерированным изображением и изображением содержимого, а потеря стиля измеряет расстояние между сгенерированным изображением и изображением стиля. Сбалансировав обе потери, мы надеемся, что сможем создать изображение где-то посередине. В отличие от обычной модели CNN, мы не тренируемся с метками, а вместо этого фиксируем вес модели и оптимизируем выходное изображение.

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

Потеря контента очевидна, это просто потеря квадратной ошибки между двумя представлениями функций. Потеря умножается на 1/2 только для простоты вычисления производной. Но это не обязательно, поэтому в нашем примере мы его опускаем.

Потеря стиля

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

Грамматическая матрица

Дана матрица F, которая является представлением признаков слоя «l» модели. Сделав внутренний продукт между F и F транспонированными, вы получите матрицу граммов. Он представляет собой корреляцию между признаками.

Потеря стиля определяется среднеквадратичным расстоянием между элементами матрицы Грама исходного изображения и матрицей Грама изображения, которое необходимо сгенерировать. Пусть «a» и «g» представляют собой представление признаков слоя «l». Затем мы вычисляем соответствующие грамм-матрицы «A» и «G». Затем квадратичная ошибка будет нормализована. Опять же, 1/4 просто для простоты получения производной.

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

Извлечение признаков

Ссылаясь на статью, автор предложил извлекать признаки из первых пяти слоев свертки. Мы называем его conv_1, conv_2, conv_3, conv_4 и conv_5, которые соответствуют слоям 0, 5, 10, 19, 28 в модели PyTorch VGG19, которую мы увидим позже.

Обучите модель (LBFGS)

Вот основной поток обучения:

  1. Получение функции среднего слоя изображения стиля
  2. Получение функции среднего слоя изображения контента

LOOP (шаг обучения)

5. Получение функции среднего слоя выходного изображения

6. Найдите потерю контента

7. Найдите потерю стиля

8. Рассчитайте общую потерю: потеря альфа-контента + потеря бета-стиля

9. Выполните одношаговый градиентный спуск на «Выходном изображении».

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

Обучите модель (АДАМ)

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

Загрузите предварительно обученную модель VGG19.

Загрузите предварительно обученную модель VGG19. Мы заменяем этот слой ReLU на неместный, потому что встроенная версия не очень хорошо работает, когда мы пытаемся получить функции среднего слоя.

Sequential(
  (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (1): ReLU()
  (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (3): ReLU()
  (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (6): ReLU()
  (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (8): ReLU()
  (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (11): ReLU()
  (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (13): ReLU()
  (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (15): ReLU()
  (16): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (17): ReLU()
  (18): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (19): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (20): ReLU()
  (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (22): ReLU()
  (23): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (24): ReLU()
  (25): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (26): ReLU()
  (27): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)

Результат переноса стиля с использованием LBFGS

Наконец, мы готовы сгенерировать изображение, начиная с изображения содержимого, используя оптимизатор LBFGS. Каждый слой стиля взвешивается на 1/5, как это было предложено автором. Вес слоя по сути является изменяемым гиперпараметром.

Starting transfer...
Iteration[100] Total loss: 1929.011719 - (content: 450.000916, style 1479.010864)
Iteration[200] Total loss: 1141.951538 - (content: 463.555481, style 678.396057)
Iteration[300] Total loss: 791.199158 - (content: 459.424805, style 331.774353)
Iteration[400] Total loss: 749.459595 - (content: 461.496948, style 287.962646)
Iteration[500] Total loss: 1082.324951 - (content: 444.428894, style 637.896118)
Finished transfer...

Результат переноса стиля с помощью ADAM

И мы также можем сгенерировать вывод с помощью ADAM, и на этот раз мы попытаемся начать со случайного белого шума. Посмотрим, что у нас получится.

Starting transfer...
Iteration[100] Total loss: 489.444641 - (content: 232.080826, style 257.363831)
Iteration[200] Total loss: 388.175659 - (content: 217.801422, style 170.374222)
Iteration[300] Total loss: 349.264343 - (content: 209.968674, style 139.295654)
Iteration[400] Total loss: 327.177948 - (content: 204.975128, style 122.202827)
Iteration[500] Total loss: 312.928192 - (content: 201.571472, style 111.356712)
Iteration[600] Total loss: 302.883362 - (content: 198.948883, style 103.934494)
Iteration[700] Total loss: 295.332245 - (content: 196.794342, style 98.537895)
Iteration[800] Total loss: 289.500610 - (content: 195.026703, style 94.473900)
Iteration[900] Total loss: 284.735077 - (content: 193.422012, style 91.313065)
Iteration[1000] Total loss: 280.884064 - (content: 192.166962, style 88.717110)
Iteration[1100] Total loss: 277.672119 - (content: 191.090607, style 86.581512)
Iteration[1200] Total loss: 274.882874 - (content: 190.064362, style 84.818512)
Iteration[1300] Total loss: 272.421234 - (content: 189.194229, style 83.227005)
Iteration[1400] Total loss: 270.255920 - (content: 188.385971, style 81.869942)
Iteration[1500] Total loss: 268.334473 - (content: 187.646240, style 80.688232)
Iteration[1600] Total loss: 266.598938 - (content: 186.970596, style 79.628349)
Iteration[1700] Total loss: 265.058807 - (content: 186.353195, style 78.705612)
Iteration[1800] Total loss: 263.676880 - (content: 185.819397, style 77.857498)
Iteration[1900] Total loss: 262.423462 - (content: 185.330231, style 77.093231)
Iteration[2000] Total loss: 261.278259 - (content: 184.863541, style 76.414734)
Iteration[2100] Total loss: 260.220673 - (content: 184.496017, style 75.724663)
Iteration[2200] Total loss: 259.233124 - (content: 184.007675, style 75.225456)
Iteration[2300] Total loss: 258.319275 - (content: 183.757706, style 74.561577)
Iteration[2400] Total loss: 257.470093 - (content: 183.619034, style 73.851074)
Iteration[2500] Total loss: 256.676697 - (content: 183.516342, style 73.160355)
Iteration[2600] Total loss: 255.935364 - (content: 183.571335, style 72.364037)
Iteration[2700] Total loss: 255.206375 - (content: 183.285263, style 71.921112)
Iteration[2800] Total loss: 254.505219 - (content: 182.278564, style 72.226662)
Iteration[2900] Total loss: 253.956146 - (content: 183.996185, style 69.959953)
Iteration[3000] Total loss: 253.355484 - (content: 184.029602, style 69.325882)
Iteration[3100] Total loss: 252.644409 - (content: 179.877167, style 72.767235)
Iteration[3200] Total loss: 252.278503 - (content: 183.824402, style 68.454094)
Iteration[3300] Total loss: 251.668427 - (content: 178.669907, style 72.998528)
Iteration[3400] Total loss: 251.086090 - (content: 181.005371, style 70.080727)
Iteration[3500] Total loss: 250.745728 - (content: 182.713974, style 68.031746)
Iteration[3600] Total loss: 250.370544 - (content: 176.382141, style 73.988411)
Iteration[3700] Total loss: 249.884674 - (content: 183.262207, style 66.622475)
Iteration[3800] Total loss: 249.469055 - (content: 176.172562, style 73.296486)
Iteration[3900] Total loss: 248.990631 - (content: 182.117065, style 66.873566)
Iteration[4000] Total loss: 248.512497 - (content: 178.094940, style 70.417557)
Iteration[4100] Total loss: 248.176697 - (content: 180.735245, style 67.441452)
Iteration[4200] Total loss: 247.894470 - (content: 176.769608, style 71.124855)
Iteration[4300] Total loss: 247.552078 - (content: 180.386200, style 67.165878)
Iteration[4400] Total loss: 247.346893 - (content: 175.786301, style 71.560585)
Iteration[4500] Total loss: 247.139633 - (content: 181.971237, style 65.168396)
Iteration[4600] Total loss: 246.928680 - (content: 175.374069, style 71.554611)
Iteration[4700] Total loss: 246.467590 - (content: 178.710480, style 67.757118)
Iteration[4800] Total loss: 246.204773 - (content: 179.035278, style 67.169487)
Iteration[4900] Total loss: 246.128571 - (content: 174.557266, style 71.571304)
Iteration[5000] Total loss: 245.969849 - (content: 181.300140, style 64.669701)
Finished transfer...

Ключевые вынос

  • Мы реализуем другую функцию потерь и используем обратное распространение для переноса стиля на наше изображение контента.
  • Для этого мы используем предварительно обученную модель VGG19 и извлекаем представление функции из среднего слоя.
  • Пробуем разные оптимизаторы (LBFGS и ADAM)
  • Продемонстрируйте возможность обучения на случайном белом шуме и изображении контента.

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

Спасибо за чтение!