Neural Style Transfer — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Описание алгоритма)
(Функция потери контента)
 
(не показано 29 промежуточных версий этого же участника)
Строка 2: Строка 2:
  
 
[[Файл:Image1.jpeg|500px|thumb|right|[https://towardsdatascience.com/neural-style-transfer-tutorial-part-1-f5cd3315fa7f Рис. 1. Принцип работы алгоритма]]]
 
[[Файл:Image1.jpeg|500px|thumb|right|[https://towardsdatascience.com/neural-style-transfer-tutorial-part-1-f5cd3315fa7f Рис. 1. Принцип работы алгоритма]]]
Алгоритм '''нейронного переноса стиля''' (англ. ''Neural Style Transfer''), разработанный Леоном Гатисом, Александром Экером и Матиасом Бетге, позволяет получить изображение и воспроизводить его в новом художественном стиле. Алгоритм берет три изображения, входное изображение (англ. ''input image''), изображение контента (англ. ''content image'') и изображение стиля (англ. ''style image''), и изменяет входные данные так, чтобы они соответствовали содержанию изображения контента и художественному стилю изображения стиля. Авторами в качестве модели сверточной нейронной сети предлагается использовать сеть [http://neerc.ifmo.ru/wiki/index.php?title=%D0%A1%D0%B2%D0%B5%D1%80%D1%82%D0%BE%D1%87%D0%BD%D1%8B%D0%B5_%D0%BD%D0%B5%D0%B9%D1%80%D0%BE%D0%BD%D0%BD%D1%8B%D0%B5_%D1%81%D0%B5%D1%82%D0%B8#VGG VGG16].
+
Алгоритм '''нейронного переноса стиля'''<ref>[https://arxiv.org/pdf/1508.06576.pdf Gatys, L.A., Ecker, A.S., Bethge, M.: A neural algorithm of artistic style, 2015]</ref> (англ. ''Neural Style Transfer''), разработанный Леоном Гатисом, Александром Экером и Матиасом Бетге, преобразует полученное на вход изображение в соответствии с выбранным стилем. Алгоритм берет три изображения, входное изображение (англ. ''input image''), изображение контента (англ. ''content image'') и изображение стиля (англ. ''style image''), и изменяет входные данные так, чтобы они соответствовали содержанию изображения контента и художественному стилю изображения стиля. Авторами в качестве модели сверточной нейронной сети предлагается использовать сеть [[Сверточные_нейронные_сети#VGG | VGG16]].
  
 
== Принцип работы алгоритма ==
 
== Принцип работы алгоритма ==
  
[[Файл:Image2.png|500px|thumb|right|Рис. 2. Архитектура сверточной сети VGG16]]
+
[[Файл:Image2.png|500px|thumb|right|[https://towardsdatascience.com/neural-style-transfer-tutorial-part-1-f5cd3315fa7f Рис. 2. Архитектура сверточной сети VGG16]]]
  
Рассмотрим 1-й сверточный слой (англ. ''[http://neerc.ifmo.ru/wiki/index.php?title=%D0%A1%D0%B2%D0%B5%D1%80%D1%82%D0%BE%D1%87%D0%BD%D1%8B%D0%B5_%D0%BD%D0%B5%D0%B9%D1%80%D0%BE%D0%BD%D0%BD%D1%8B%D0%B5_%D1%81%D0%B5%D1%82%D0%B8#.D0.A1.D0.B2.D0.B5.D1.80.D1.82.D0.BE.D1.87.D0.BD.D1.8B.D0.B9_.D1.81.D0.BB.D0.BE.D0.B9 convolution layer]'') VGG16, который использует ядро 3x3 и обучает 64 карты признаков (англ. ''feature map'') для генерации представления изображения размерности 224x224x64, принимая 3-канальное изображение размером 224x224 в качестве входных данных (''Рисунок 2''). Во время обучения эти карты признаков научились обнаруживать простые шаблоны, например, такие как прямые линии, окружности или даже не имеющие никакого смысла для человеческого глаза шаблоны, которые тем не менее имеют огромное значение для этой модели. Такое "обнаружение" шаблонов называется обучением представления признаков. Теперь давайте рассмотрим 10-й сверточный слой VGG16, который использует ядро 3x3 с 512 картами признаков для обучения и в итоге генерирует вывод представления изображения размерности 28x28x512. Нейроны 10-го слоя уже могут обнаруживать более сложные шаблоны такие как, например, колесо автомобиля, окно или дерево и т.д.
+
Рассмотрим 1-й [[Сверточные_нейронные_сети#Сверточный слой | сверточный слой]]  (англ. ''convolution layer'') VGG16, который использует ядро 3x3 и обучает 64 карты признаков (англ. ''feature map'') для генерации представления изображения размерности 224x224x64, принимая 3-канальное изображение размером 224x224 в качестве входных данных (''Рисунок 2''). Во время обучения эти карты признаков научились обнаруживать простые шаблоны, например, такие как прямые линии, окружности или даже не имеющие никакого смысла для человеческого глаза шаблоны, которые тем не менее имеют огромное значение для этой модели. Такое "обнаружение" шаблонов называется обучением представления признаков. Теперь давайте рассмотрим 10-й сверточный слой VGG16, который использует ядро 3x3 с 512 картами признаков для обучения и в итоге генерирует вывод представления изображения размерности 28x28x512. Нейроны 10-го слоя уже могут обнаруживать более сложные шаблоны такие как, например, колесо автомобиля, окно или дерево и т.д.
  
Собственно вышеперечисленные свойства характерны для любой [http://neerc.ifmo.ru/wiki/index.php?title=%D0%A1%D0%B2%D0%B5%D1%80%D1%82%D0%BE%D1%87%D0%BD%D1%8B%D0%B5_%D0%BD%D0%B5%D0%B9%D1%80%D0%BE%D0%BD%D0%BD%D1%8B%D0%B5_%D1%81%D0%B5%D1%82%D0%B8 сверточной нейронной сети], работа которой обычно интерпретируется как переход от конкретных особенностей изображения к более абстрактным деталям, и далее к ещё более абстрактным деталям вплоть до выделения понятий высокого уровня. При этом сеть самонастраивается и вырабатывает необходимую иерархию абстрактных признаков (последовательности карт признаков), фильтруя маловажные детали и выделяя существенное.
+
Собственно вышеперечисленные свойства характерны для любой [[Сверточные_нейронные_сети | сверточной нейронной сети]], работа которой обычно интерпретируется как переход от конкретных особенностей изображения к более абстрактным деталям вплоть до выделения понятий высокого уровня. При этом сеть самонастраивается и вырабатывает необходимую иерархию абстрактных признаков (последовательности карт признаков), фильтруя маловажные детали и выделяя существенное.
  
 
Такая природа представления кодирования сама по себе является ключом к передаче стиля, который используется для вычисления функции потерь между сгенерированным изображением относительно изображения контента и изображения стиля. При обучении модели более десяти тысяч изображений на класс модель может генерировать аналогичное представление признаков для множества различных изображений, если они принадлежат к одному классу или имеют схожий контент или стиль.  
 
Такая природа представления кодирования сама по себе является ключом к передаче стиля, который используется для вычисления функции потерь между сгенерированным изображением относительно изображения контента и изображения стиля. При обучении модели более десяти тысяч изображений на класс модель может генерировать аналогичное представление признаков для множества различных изображений, если они принадлежат к одному классу или имеют схожий контент или стиль.  
  
Следовательно, имеет смысл использовать разницу в значении представления признаков сгенерированного изображения по содержанию и по стилю изображения, чтобы направлять итерации, через которые мы производим само сгенерированное изображение, но как убедиться, что изображение с содержанием '''C''' и сгенерированное изображение '''G''' похожи по своему содержанию, а не по стилю, в то время как сгенерированное изображение наследует только похожее представление стиля изображения стиля '''S''', а не само изображение стиля в целом. Это решается разделением функции потерь на две части: одна {{---}} потеря контента, а другая {{---}} потеря стиля.
+
Следовательно, имеет смысл использовать разницу в значении представления признаков сгенерированного изображения по содержанию и по стилю изображения, чтобы направлять итерации, через которые мы производим само сгенерированное изображение, но как убедиться, что изображение с контентом '''C''' и сгенерированное изображение '''G''' похожи по своему содержанию, а не по стилю, в то время как сгенерированное изображение наследует только похожее представление стиля изображения стиля '''S''', а не само изображение стиля в целом. Это решается разделением функции потерь на две части: одна {{---}} потеря контента, а другая {{---}} потеря стиля.
  
 
== Функция потерь ==
 
== Функция потерь ==
Строка 22: Строка 22:
 
В уравнении выше, чтобы получить общую потерю <math>L_{total}</math> нужно рассчитать потерю содержимого <math>L_{content}</math> и потерю стиля <math>L_{style}</math>, а также <math>\alpha</math> и <math>\beta</math> {{---}} гиперпараметры, которые используются для определения весов для каждого типа потерь, то есть эти параметры можно представить просто как "рычаги" для управления тем, сколько контента / стиля мы хотим наследовать в сгенерированном изображении.
 
В уравнении выше, чтобы получить общую потерю <math>L_{total}</math> нужно рассчитать потерю содержимого <math>L_{content}</math> и потерю стиля <math>L_{style}</math>, а также <math>\alpha</math> и <math>\beta</math> {{---}} гиперпараметры, которые используются для определения весов для каждого типа потерь, то есть эти параметры можно представить просто как "рычаги" для управления тем, сколько контента / стиля мы хотим наследовать в сгенерированном изображении.
  
[[Файл:Image6.jpeg|500px|thumb|right|Рис. 3. Различные представления, используемые для изображений контента, стиля и сгенерированного изображения]]
+
[[Файл:Image6.jpeg|500px|thumb|right|[https://towardsdatascience.com/neural-style-transfer-tutorial-part-1-f5cd3315fa7f Рис. 3. Различные представления, используемые для изображений контента, стиля и сгенерированного изображения]]]
  
 
Во время каждой итерации все три изображения, передаются через модель VGG16. Значения функции активации нейронов, которые кодируют представление признаков данного изображения на определенных слоях, принимаются как входные данные для этих двух функций потерь. Также стоит добавить: изначально мы случайным образом инициализируем сгенерированное изображение, например, матрицей случайного шума такого же разрешения, как и изображение контента. С каждой итерацией мы изменяем сгенерированное изображение, чтобы минимизировать общую потерю '''L'''.
 
Во время каждой итерации все три изображения, передаются через модель VGG16. Значения функции активации нейронов, которые кодируют представление признаков данного изображения на определенных слоях, принимаются как входные данные для этих двух функций потерь. Также стоит добавить: изначально мы случайным образом инициализируем сгенерированное изображение, например, матрицей случайного шума такого же разрешения, как и изображение контента. С каждой итерацией мы изменяем сгенерированное изображение, чтобы минимизировать общую потерю '''L'''.
Строка 28: Строка 28:
 
== Функция потери контента ==
 
== Функция потери контента ==
  
Возьмем функциональное представление 7-го сверточного слой VGG16. Чтобы вычислить потерю контента, пропускаем изображение контента и сгенерированное изображение через VGG16 и получаем значения функции активации (выходы) 7-го слоя для обоих этих изображений. После каждого сверточного слоя идет ReLU, поэтому мы будем обозначать выход этого слоя в целом как relu_3_3 (поскольку это выход третьего сверточного слоя третьего набора / блока сверток) (Рисунок 2). Наконец, мы находим L2-норму поэлементного вычитания между этими двумя матрицами значений функции активации следующим образом:  
+
Возьмем функциональное представление 7-го сверточного слоя VGG16. Чтобы вычислить потерю контента, пропускаем изображение контента и сгенерированное изображение через VGG16 и получаем значения функции активации (выходы) 7-го слоя для обоих этих изображений. После каждого сверточного слоя идет ReLU, поэтому мы будем обозначать выход этого слоя в целом как relu_3_3 (поскольку это выход третьего сверточного слоя третьего набора / блока сверток) (Рисунок 2). Наконец, мы находим L2-норму поэлементного вычитания между этими двумя матрицами значений функции активации следующим образом:  
  
  
<math>L_{content}(C, G, L) = \frac{1}{2} \sum\limits_{ij}(a[L](C)_{ij} - a[L](G)_{ij})^2</math>
+
<math>L_{content}(C, G, L) = \frac{1}{2} \sum\limits_{ij}(a[l](C)_{ij} - a[l](G)_{ij})^2</math>, где <math>a</math> {{---}} тензор выходов слоев сети, <math>l</math> {{---}} номер сверточного слоя
  
 
Это поможет сохранить исходный контент в сгенерированном изображении, а также минимизировать разницу в представлении признаков, которое логически фокусируется на разнице между содержимым обоих изображений.
 
Это поможет сохранить исходный контент в сгенерированном изображении, а также минимизировать разницу в представлении признаков, которое логически фокусируется на разнице между содержимым обоих изображений.
Строка 40: Строка 40:
  
 
=== Матрица Грама ===
 
=== Матрица Грама ===
 
[[Файл:Image9.jpeg|500px|thumb|right|Рис. 4. Принцип расчета матрицы Грама]]
 
  
 
Рассмотрим, как мы передаем наше изображение стиля через VGG16 и получаем значения функции активации из 7-го уровня, который генерирует матрицу представления объектов размером 56x56x256.
 
Рассмотрим, как мы передаем наше изображение стиля через VGG16 и получаем значения функции активации из 7-го уровня, который генерирует матрицу представления объектов размером 56x56x256.
В этом трехмерном массиве имеется 256 каналов размером 56x56 каждый. Теперь предположим, что есть канал ''A'', чьи блоки активации могут активироваться, когда они сталкиваются с разделом изображения, содержащим коричнево-черные полосы, а затем есть канал ''B'', чьи блоки активации могут активироваться, когда они сталкиваются с чем-то похожим на глазное яблоко. Если оба этих канала ''A'' и ''B'' активируются вместе для одного и того же входа, существует высокая вероятность того, что изображение может содержать лицо тигра (поскольку у него было два канала с высокими значениями, которые активируются для глазного яблока и коричнево-черных полос). Теперь, если оба эти канала будут запущены с высокими значениями активации, это означает, что они будут иметь высокую корреляцию по сравнению с корреляцией между каналом ''A'' и ''С'', где канал ''С'' может активироваться, когда он видит ромбовидный шаблон.
+
В этом трехмерном массиве имеется 256 каналов размером 56x56 каждый.  
  
Таким образом, чтобы получить корреляцию всех этих каналов друг с другом, нам нужно вычислить нечто называемое матрицей Грама, будем использовать ее для измерения степени корреляции между каналами, которая позже будет служить мерой самого стиля. Рисунок 4 помогает лучше понять как рассчитывается матрица Грама на примере.
+
Теперь предположим, что есть канал ''A'', чьи нейроны могут активироваться на изображении, содержащем коричнево-черные полосы, а нейроны канала ''B'' {{---}} на изображение, содержащее глазное яблоко. Если оба этих канала ''A'' и ''B'' активируются вместе для одного и того же изображения, то высока вероятность того, что изображение может содержать, например, лицо тигра (поскольку у него было два канала с большими абсолютными значениями, которые активируются для коричнево-черных полос и глазного яблока). Теперь, если оба эти канала будут с большими значениями функции активации, то они будут иметь более высокую корреляцию между каналами ''A'' и ''В'', чем между каналами ''A'' и ''С'', где канал ''С'' активируется  на изображении, содержащем ромбовидный шаблон. Чтобы получить корреляцию всех этих каналов друг с другом, нам нужно вычислить нечто называемое матрицей Грама, будем использовать ее для измерения степени корреляции между каналами. Таким образом, именно значение корреляции между каналами служит показателем того, насколько итоговое изображение наследует элементы изображения со стилем.
  
 
=== Функция потерь на основе корреляции матриц Грама ===
 
=== Функция потерь на основе корреляции матриц Грама ===
  
Теперь, как вы можете видеть, как каждый элемент матрицы Грама содержит меру корреляции всех каналов относительно друг друга. Обозначим матрицу Грама стилевого изображения слоя <math>l</math> как <math>GM[l](S)</math>, а матрицу Грама сгенерированного изображения того же слоя <math>GM[l](G)</math>. Обе матрицы были вычислены из одного и того же слоя, следовательно, с использованием одного и того же числа каналов, что привело к тому, что итоговая матрица размера <math>channels \times channels</math>. Теперь, если мы найдем сумму квадратов разности или L2-норму вычитания элементов этих двух матриц и попытаемся минимизировать ее, то в конечном итоге это приведет к минимизации разницы между изображением стиля и сгенерированным изображением.
+
Каждый элемент матрицы Грама содержит меру корреляции всех каналов относительно друг друга. Обозначим матрицу Грама стилевого изображения слоя <math>l</math> как <math>GM[l](S)</math>, а матрицу Грама сгенерированного изображения того же слоя <math>GM[l](G)</math>. Обе матрицы были вычислены из одного и того же слоя, следовательно, с использованием одного и того же числа каналов, что привело к тому, что итоговая матрица размера <math>channels \times channels</math>. Теперь, если мы найдем сумму квадратов разности или L2-норму вычитания элементов этих двух матриц и попытаемся минимизировать ее, то в конечном итоге это приведет к минимизации разницы между изображением стиля и сгенерированным изображением.
  
 
<math>L_{GM}(S, G, l) = \frac{1}{4N_l^2M_l^2} \sum\limits_{ij}(GM[l](S)_{ij} - GM[l](G)_{ij})^2</math>
 
<math>L_{GM}(S, G, l) = \frac{1}{4N_l^2M_l^2} \sum\limits_{ij}(GM[l](S)_{ij} - GM[l](G)_{ij})^2</math>
Строка 60: Строка 58:
 
<math>L_{style}(S, G) = \sum\limits_{l=0}^L w_l * L_{GM}(S, G, l)</math>
 
<math>L_{style}(S, G) = \sum\limits_{l=0}^L w_l * L_{GM}(S, G, l)</math>
  
== Пример кода на PyTorch ==
+
== Пример кода на Python ==
 +
 
 +
Данный пример реализован на основе [[Обзор_библиотек_для_машинного_обучения_на_Python#Библиотеки для глубокого обучения | открытой платформы глубокого обучения PyTorch]]
 +
 
 +
'''Функция потери контента'''
 +
  '''class''' ContentLoss(nn.Module):
 +
   
 +
    '''def''' __init__(self, target,):
 +
      super(ContentLoss, self).__init__()
 +
      <font color="green"># we 'detach' the target content from the tree used</font>
 +
      <font color="green"># to dynamically compute the gradient: this is a stated value,</font>
 +
      <font color="green"># not a variable. Otherwise the forward method of the criterion</font>
 +
      <font color="green"># will throw an error.</font>
 +
      self.target = target.detach()
 +
   
 +
    '''def''' forward(self, input):
 +
      self.loss = F.mse_loss(input, self.target)
 +
      return input
 +
 
 +
'''Функция потери стиля'''
 +
    '''def''' gram_matrix(input):
 +
      a, b, c, d = input.size()  <font color="green"># a=batch size(=1)</font>
 +
      <font color="green"># b=number of feature maps</font>
 +
      <font color="green"># (c,d)=dimensions of a f. map (N=c*d)</font>
 +
      features = input.view(a * b, c * d)  <font color="green"># resize feature maps</font>
 +
      G = torch.mm(features, features.t())  <font color="green"># compute the gram product</font>
 +
      <font color="green"># we 'normalize' the values of the gram matrix</font>
 +
      <font color="green"># by dividing by the number of element in each feature maps.</font>
 +
      return G.div(a * b * c * d)
 +
 
 +
  '''class''' StyleLoss(nn.Module):
 +
   
 +
    '''def''' __init__(self, target_feature):
 +
      super(StyleLoss, self).__init__()
 +
      self.target = gram_matrix(target_feature).detach()
 +
   
 +
    '''def''' forward(self, input):
 +
      G = gram_matrix(input)
 +
      self.loss = F.mse_loss(G, self.target)
 +
      return input
 +
 
 +
'''Инициализация модели'''
 +
  cnn = models.vgg19(pretrained=True).features.to(device).eval()
 +
 
 +
'''Нормализация'''
 +
  cnn_normalization_mean = torch.tensor([0.485, 0.456, 0.406]).to(device)
 +
  cnn_normalization_std = torch.tensor([0.229, 0.224, 0.225]).to(device)
 +
 
 +
  <font color="green"># create a module to normalize input image so we can easily put it in a</font>
 +
  <font color="green"># nn.Sequential</font>
 +
  '''class''' Normalization(nn.Module):
 +
    '''def''' __init__(self, mean, std):
 +
        super(Normalization, self).__init__()
 +
        <font color="green"># .view the mean and std to make them [C x 1 x 1] so that they can</font>
 +
        <font color="green"># directly work with image Tensor of shape [B x C x H x W].</font>
 +
        <font color="green"># B is batch size. C is number of channels. H is height and W is width.</font>
 +
        self.mean = torch.tensor(mean).view(-1, 1, 1)
 +
        self.std = torch.tensor(std).view(-1, 1, 1)
 +
   
 +
    '''def''' forward(self, img):
 +
        <font color="green"># normalize img</font>
 +
        return (img - self.mean) / self.std
 +
 
 +
'''Добавление собственных слоев'''
 +
  <font color="green"># desired depth layers to compute style/content losses :</font>
 +
  content_layers_default = ['conv_4']
 +
  style_layers_default = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']
 +
   
 +
  '''def''' get_style_model_and_losses(cnn, normalization_mean, normalization_std,
 +
                                style_img, content_img,
 +
                                content_layers=content_layers_default,
 +
                                style_layers=style_layers_default):
 +
    cnn = copy.deepcopy(cnn)
 +
   
 +
    <font color="green"># normalization module</font>
 +
    normalization = Normalization(normalization_mean, normalization_std).to(device)
 +
   
 +
    <font color="green"># just in order to have an iterable access to or list of content/style losses</font>
 +
    content_losses = []
 +
    style_losses = []
 +
   
 +
    <font color="green"># assuming that cnn is a nn.Sequential, so we make a new nn.Sequential</font>
 +
    <font color="green"># to put in modules that are supposed to be activated sequentially</font>
 +
    model = nn.Sequential(normalization)
 +
   
 +
    i = 0  <font color="green"># increment every time we see a conv</font>
 +
    for layer in cnn.children():
 +
        if isinstance(layer, nn.Conv2d):
 +
            i += 1
 +
            name = 'conv_{}'.format(i)
 +
        elif isinstance(layer, nn.ReLU):
 +
            name = 'relu_{}'.format(i)
 +
            <font color="green"># The in-place version doesn't play very nicely with the ContentLoss</font>
 +
            <font color="green"># and StyleLoss we insert below. So we replace with out-of-place</font>
 +
            <font color="green"># ones here.</font>
 +
            layer = nn.ReLU(inplace=False)
 +
        elif isinstance(layer, nn.MaxPool2d):
 +
            name = 'pool_{}'.format(i)
 +
        elif isinstance(layer, nn.BatchNorm2d):
 +
            name = 'bn_{}'.format(i)
 +
        else:
 +
            raise RuntimeError('Unrecognized layer: {}'.format(layer.__class__.__name__))
 +
   
 +
        model.add_module(name, layer)
 +
   
 +
        if name in content_layers:
 +
            <font color="green"># add content loss:</font>
 +
            target = model(content_img).detach()
 +
            content_loss = ContentLoss(target)
 +
            model.add_module("content_loss_{}".format(i), content_loss)
 +
            content_losses.append(content_loss)
 +
   
 +
        if name in style_layers:
 +
            <font color="green"># add style loss:</font>
 +
            target_feature = model(style_img).detach()
 +
            style_loss = StyleLoss(target_feature)
 +
            model.add_module("style_loss_{}".format(i), style_loss)
 +
            style_losses.append(style_loss)
 +
   
 +
    <font color="green"># now we trim off the layers after the last content and style losses</font>
 +
    for i in range(len(model) - 1, -1, -1):
 +
        if isinstance(model[i], ContentLoss) or isinstance(model[i], StyleLoss):
 +
            break
 +
   
 +
    model = model[:(i + 1)]
 +
   
 +
    return model, style_losses, content_losses
 +
 
 +
'''Градиентный спуск'''
 +
  '''def''' get_input_optimizer(input_img):
 +
    <font color="green"># this line to show that input is a parameter that requires a gradient</font>
 +
    optimizer = optim.LBFGS([input_img.requires_grad_()])
 +
    return optimizer
 +
 
 +
'''Запуск алгоритма'''
 +
  '''def''' run_style_transfer(cnn, normalization_mean, normalization_std,
 +
                        content_img, style_img, input_img, num_steps=300,
 +
                        style_weight=1000000, content_weight=1):
 +
    print('Building the style transfer model..')
 +
    model, style_losses, content_losses = get_style_model_and_losses(cnn,
 +
        normalization_mean, normalization_std, style_img, content_img)
 +
    optimizer = get_input_optimizer(input_img)
 +
   
 +
    run = [0]
 +
    while run[0] <= num_steps:
 +
   
 +
        def closure():
 +
            <font color="green"># correct the values of updated input image</font>
 +
            input_img.data.clamp_(0, 1)
 +
   
 +
            optimizer.zero_grad()
 +
            model(input_img)
 +
            style_score = 0
 +
            content_score = 0
 +
   
 +
            for sl in style_losses:
 +
                style_score += sl.loss
 +
            for cl in content_losses:
 +
                content_score += cl.loss
 +
   
 +
            style_score *= style_weight
 +
            content_score *= content_weight
 +
   
 +
            loss = style_score + content_score
 +
            loss.backward()
 +
   
 +
            run[0] += 1
 +
            if run[0] % 50 == 0:
 +
                print("run {}:".format(run))
 +
                print('Style Loss : {:4f} Content Loss: {:4f}'.format(
 +
                    style_score.item(), content_score.item()))
 +
                print()
 +
   
 +
            return style_score + content_score
 +
   
 +
        optimizer.step(closure)
 +
   
 +
    <font color="green"># a last correction...</font>
 +
    input_img.data.clamp_(0, 1)
 +
   
 +
    return input_img
 +
 
 +
  <font color="green"># run style transfer</font>
 +
  output = run_style_transfer(cnn, cnn_normalization_mean, cnn_normalization_std,
 +
                              content_img, style_img, input_img)
 +
 
 +
==См. также==
 +
* [[Сверточные_нейронные_сети | Свёрточная нейронная сеть]]
 +
* [https://ethereon.github.io/netscope/#/gist/3785162f95cd2d5fee77 Интерактивная архитектура сети VGG16]
  
class ContentLoss(nn.Module):
+
==Примечания==
 +
<references/>
  
    def __init__(self, target,):
+
== Источники информации ==
        super(ContentLoss, self).__init__()
+
* [https://towardsdatascience.com/neural-style-transfer-tutorial-part-1-f5cd3315fa7f Theory of Neural Style Transfer]
        # we 'detach' the target content from the tree used
+
* [https://towardsdatascience.com/neural-style-transfer-series-part-2-91baad306b24 TensorFlow and pyTorch Implementation of Neural Style Transfer]
        # to dynamically compute the gradient: this is a stated value,
+
* [https://pytorch.org/tutorials/advanced/neural_style_tutorial.html Neural Style Transfer using PyTorch]
        # not a variable. Otherwise the forward method of the criterion
 
        # will throw an error.
 
        self.target = target.detach()
 
  
    def forward(self, input):
+
[[Категория: Машинное обучение]]
        self.loss = F.mse_loss(input, self.target)
+
[[Категория: Нейронные сети]]
        return input
+
[[Категория: Сверточные нейронные сети]]

Текущая версия на 00:57, 19 апреля 2019

Описание алгоритма[править]

Алгоритм нейронного переноса стиля[1] (англ. Neural Style Transfer), разработанный Леоном Гатисом, Александром Экером и Матиасом Бетге, преобразует полученное на вход изображение в соответствии с выбранным стилем. Алгоритм берет три изображения, входное изображение (англ. input image), изображение контента (англ. content image) и изображение стиля (англ. style image), и изменяет входные данные так, чтобы они соответствовали содержанию изображения контента и художественному стилю изображения стиля. Авторами в качестве модели сверточной нейронной сети предлагается использовать сеть VGG16.

Принцип работы алгоритма[править]

Рассмотрим 1-й сверточный слой (англ. convolution layer) VGG16, который использует ядро 3x3 и обучает 64 карты признаков (англ. feature map) для генерации представления изображения размерности 224x224x64, принимая 3-канальное изображение размером 224x224 в качестве входных данных (Рисунок 2). Во время обучения эти карты признаков научились обнаруживать простые шаблоны, например, такие как прямые линии, окружности или даже не имеющие никакого смысла для человеческого глаза шаблоны, которые тем не менее имеют огромное значение для этой модели. Такое "обнаружение" шаблонов называется обучением представления признаков. Теперь давайте рассмотрим 10-й сверточный слой VGG16, который использует ядро 3x3 с 512 картами признаков для обучения и в итоге генерирует вывод представления изображения размерности 28x28x512. Нейроны 10-го слоя уже могут обнаруживать более сложные шаблоны такие как, например, колесо автомобиля, окно или дерево и т.д.

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

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

Следовательно, имеет смысл использовать разницу в значении представления признаков сгенерированного изображения по содержанию и по стилю изображения, чтобы направлять итерации, через которые мы производим само сгенерированное изображение, но как убедиться, что изображение с контентом C и сгенерированное изображение G похожи по своему содержанию, а не по стилю, в то время как сгенерированное изображение наследует только похожее представление стиля изображения стиля S, а не само изображение стиля в целом. Это решается разделением функции потерь на две части: одна — потеря контента, а другая — потеря стиля.

Функция потерь[править]

[math]L_{total}(S, C, G) = \alpha * L_{content}(C, G) + \beta * L_{style}(S, G)[/math]

В уравнении выше, чтобы получить общую потерю [math]L_{total}[/math] нужно рассчитать потерю содержимого [math]L_{content}[/math] и потерю стиля [math]L_{style}[/math], а также [math]\alpha[/math] и [math]\beta[/math] — гиперпараметры, которые используются для определения весов для каждого типа потерь, то есть эти параметры можно представить просто как "рычаги" для управления тем, сколько контента / стиля мы хотим наследовать в сгенерированном изображении.

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

Функция потери контента[править]

Возьмем функциональное представление 7-го сверточного слоя VGG16. Чтобы вычислить потерю контента, пропускаем изображение контента и сгенерированное изображение через VGG16 и получаем значения функции активации (выходы) 7-го слоя для обоих этих изображений. После каждого сверточного слоя идет ReLU, поэтому мы будем обозначать выход этого слоя в целом как relu_3_3 (поскольку это выход третьего сверточного слоя третьего набора / блока сверток) (Рисунок 2). Наконец, мы находим L2-норму поэлементного вычитания между этими двумя матрицами значений функции активации следующим образом:


[math]L_{content}(C, G, L) = \frac{1}{2} \sum\limits_{ij}(a[l](C)_{ij} - a[l](G)_{ij})^2[/math], где [math]a[/math] — тензор выходов слоев сети, [math]l[/math] — номер сверточного слоя

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

Функция потери стиля[править]

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

Матрица Грама[править]

Рассмотрим, как мы передаем наше изображение стиля через VGG16 и получаем значения функции активации из 7-го уровня, который генерирует матрицу представления объектов размером 56x56x256. В этом трехмерном массиве имеется 256 каналов размером 56x56 каждый.

Теперь предположим, что есть канал A, чьи нейроны могут активироваться на изображении, содержащем коричнево-черные полосы, а нейроны канала B — на изображение, содержащее глазное яблоко. Если оба этих канала A и B активируются вместе для одного и того же изображения, то высока вероятность того, что изображение может содержать, например, лицо тигра (поскольку у него было два канала с большими абсолютными значениями, которые активируются для коричнево-черных полос и глазного яблока). Теперь, если оба эти канала будут с большими значениями функции активации, то они будут иметь более высокую корреляцию между каналами A и В, чем между каналами A и С, где канал С активируется на изображении, содержащем ромбовидный шаблон. Чтобы получить корреляцию всех этих каналов друг с другом, нам нужно вычислить нечто называемое матрицей Грама, будем использовать ее для измерения степени корреляции между каналами. Таким образом, именно значение корреляции между каналами служит показателем того, насколько итоговое изображение наследует элементы изображения со стилем.

Функция потерь на основе корреляции матриц Грама[править]

Каждый элемент матрицы Грама содержит меру корреляции всех каналов относительно друг друга. Обозначим матрицу Грама стилевого изображения слоя [math]l[/math] как [math]GM[l](S)[/math], а матрицу Грама сгенерированного изображения того же слоя [math]GM[l](G)[/math]. Обе матрицы были вычислены из одного и того же слоя, следовательно, с использованием одного и того же числа каналов, что привело к тому, что итоговая матрица размера [math]channels \times channels[/math]. Теперь, если мы найдем сумму квадратов разности или L2-норму вычитания элементов этих двух матриц и попытаемся минимизировать ее, то в конечном итоге это приведет к минимизации разницы между изображением стиля и сгенерированным изображением.

[math]L_{GM}(S, G, l) = \frac{1}{4N_l^2M_l^2} \sum\limits_{ij}(GM[l](S)_{ij} - GM[l](G)_{ij})^2[/math]

В вышеприведенном уравнении [math]N_{l}[/math] представляет номер канала в карте признаков / выходных данных уровня [math]l[/math], а [math]M_{l}[/math] представляет [math]height*width[/math] карты признаков / выходных данных слоя [math]l[/math].

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

[math]L_{style}(S, G) = \sum\limits_{l=0}^L w_l * L_{GM}(S, G, l)[/math]

Пример кода на Python[править]

Данный пример реализован на основе открытой платформы глубокого обучения PyTorch

Функция потери контента

 class ContentLoss(nn.Module):
   
   def __init__(self, target,):
     super(ContentLoss, self).__init__()
     # we 'detach' the target content from the tree used
     # to dynamically compute the gradient: this is a stated value,
     # not a variable. Otherwise the forward method of the criterion
     # will throw an error.
     self.target = target.detach()
   
   def forward(self, input):
     self.loss = F.mse_loss(input, self.target)
     return input

Функция потери стиля

   def gram_matrix(input):
     a, b, c, d = input.size()  # a=batch size(=1)
     # b=number of feature maps
     # (c,d)=dimensions of a f. map (N=c*d)
     features = input.view(a * b, c * d)  # resize feature maps
     G = torch.mm(features, features.t())  # compute the gram product
     # we 'normalize' the values of the gram matrix
     # by dividing by the number of element in each feature maps.
     return G.div(a * b * c * d)
 class StyleLoss(nn.Module):
   
   def __init__(self, target_feature):
     super(StyleLoss, self).__init__()
     self.target = gram_matrix(target_feature).detach()
   
   def forward(self, input):
     G = gram_matrix(input)
     self.loss = F.mse_loss(G, self.target)
     return input

Инициализация модели

 cnn = models.vgg19(pretrained=True).features.to(device).eval()

Нормализация

 cnn_normalization_mean = torch.tensor([0.485, 0.456, 0.406]).to(device)
 cnn_normalization_std = torch.tensor([0.229, 0.224, 0.225]).to(device)
 
 # create a module to normalize input image so we can easily put it in a
 # nn.Sequential
 class Normalization(nn.Module):
   def __init__(self, mean, std):
       super(Normalization, self).__init__()
       # .view the mean and std to make them [C x 1 x 1] so that they can
       # directly work with image Tensor of shape [B x C x H x W].
       # B is batch size. C is number of channels. H is height and W is width.
       self.mean = torch.tensor(mean).view(-1, 1, 1)
       self.std = torch.tensor(std).view(-1, 1, 1)
   
   def forward(self, img):
       # normalize img
       return (img - self.mean) / self.std

Добавление собственных слоев

 # desired depth layers to compute style/content losses :
 content_layers_default = ['conv_4']
 style_layers_default = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']
   
 def get_style_model_and_losses(cnn, normalization_mean, normalization_std,
                                style_img, content_img,
                                content_layers=content_layers_default,
                                style_layers=style_layers_default):
   cnn = copy.deepcopy(cnn)
   
   # normalization module
   normalization = Normalization(normalization_mean, normalization_std).to(device)
   
   # just in order to have an iterable access to or list of content/style losses
   content_losses = []
   style_losses = []
   
   # assuming that cnn is a nn.Sequential, so we make a new nn.Sequential
   # to put in modules that are supposed to be activated sequentially
   model = nn.Sequential(normalization)
   
   i = 0  # increment every time we see a conv
   for layer in cnn.children():
       if isinstance(layer, nn.Conv2d):
           i += 1
           name = 'conv_{}'.format(i)
       elif isinstance(layer, nn.ReLU):
           name = 'relu_{}'.format(i)
           # The in-place version doesn't play very nicely with the ContentLoss
           # and StyleLoss we insert below. So we replace with out-of-place
           # ones here.
           layer = nn.ReLU(inplace=False)
       elif isinstance(layer, nn.MaxPool2d):
           name = 'pool_{}'.format(i)
       elif isinstance(layer, nn.BatchNorm2d):
           name = 'bn_{}'.format(i)
       else:
           raise RuntimeError('Unrecognized layer: {}'.format(layer.__class__.__name__))
   
       model.add_module(name, layer)
   
       if name in content_layers:
           # add content loss:
           target = model(content_img).detach()
           content_loss = ContentLoss(target)
           model.add_module("content_loss_{}".format(i), content_loss)
           content_losses.append(content_loss)
   
       if name in style_layers:
           # add style loss:
           target_feature = model(style_img).detach()
           style_loss = StyleLoss(target_feature)
           model.add_module("style_loss_{}".format(i), style_loss)
           style_losses.append(style_loss)
   
   # now we trim off the layers after the last content and style losses
   for i in range(len(model) - 1, -1, -1):
       if isinstance(model[i], ContentLoss) or isinstance(model[i], StyleLoss):
           break
   
   model = model[:(i + 1)]
   
   return model, style_losses, content_losses

Градиентный спуск

 def get_input_optimizer(input_img):
   # this line to show that input is a parameter that requires a gradient
   optimizer = optim.LBFGS([input_img.requires_grad_()])
   return optimizer

Запуск алгоритма

 def run_style_transfer(cnn, normalization_mean, normalization_std,
                        content_img, style_img, input_img, num_steps=300,
                        style_weight=1000000, content_weight=1):
   print('Building the style transfer model..')
   model, style_losses, content_losses = get_style_model_and_losses(cnn,
       normalization_mean, normalization_std, style_img, content_img)
   optimizer = get_input_optimizer(input_img)
   
   run = [0]
   while run[0] <= num_steps:
   
       def closure():
           # correct the values of updated input image
           input_img.data.clamp_(0, 1)
   
           optimizer.zero_grad()
           model(input_img)
           style_score = 0
           content_score = 0
   
           for sl in style_losses:
               style_score += sl.loss
           for cl in content_losses:
               content_score += cl.loss
    
           style_score *= style_weight
           content_score *= content_weight
    
           loss = style_score + content_score
           loss.backward()
   
           run[0] += 1
           if run[0] % 50 == 0:
               print("run {}:".format(run))
               print('Style Loss : {:4f} Content Loss: {:4f}'.format(
                   style_score.item(), content_score.item()))
               print()
   
           return style_score + content_score
   
       optimizer.step(closure)
   
   # a last correction...
   input_img.data.clamp_(0, 1)
   
   return input_img
 
 # run style transfer
 output = run_style_transfer(cnn, cnn_normalization_mean, cnn_normalization_std,
                             content_img, style_img, input_img)

См. также[править]

Примечания[править]

Источники информации[править]