СТАТЬЯ

Ускоренный курс PyTorch, часть 3

Из Глубокое обучение с PyTorch Эли Стивенса и Луки Антиги

__________________________________________________________________

Получите скидку 37 % на Углубленное обучение с PyTorch. Просто введите код fccstevens в поле с промокодом на скидку при оформлении заказа на manning.com.
________________________________________________________________________________

В этой статье мы исследуем некоторые возможности PyTorch, играя в генеративно-состязательные сети.

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

Предварительно обученная сеть, которая создает новые возможности

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

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

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

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

ЦИКЛЕГАН

Интересная эволюция CycleGAN этой концепции. CycleGAN может превращать изображения одного домена в изображения другого домена (и обратно) без необходимости явного предоставления совпадающих пар в обучающем наборе.

В CycleGAN генератор учится создавать изображение, соответствующее целевому распределению — например, картины Моне — начиная с изображения, принадлежащего другому распределению — например, фотографии пейзажа — чтобы гарантировать, что дискриминатор не сможет определить, является ли изображение созданный из пейзажной фотографии, является подлинной картиной Моне. В то же время, и здесь появляется префикс Цикл в аббревиатуре, получившаяся картина отправляется через другой генератор в обратном направлении, Моне к фотографии в нашем случае (!), чтобы быть оценивается другим дискриминатором на другой стороне. Создание такого цикла значительно стабилизирует тренировочный процесс, что является одной из первоначальных проблем с GAN.

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

СЕТЬ, ПРЕВРАЩАЮЩАЯ ЛОШАДЕЙ В ЗЕБРЫ

Еще более наглядный пример — Horse2Zebra CycleGAN, с которым мы сейчас поиграем. В этом случае сеть CycleGAN была обучена на наборе данных (несвязанных) изображений лошадей и изображений зебры, извлеченных из набора данных ImageNet. Сеть учится брать изображение одной или нескольких лошадей и превращать их всех в зебр, оставляя остальных без изменений, насколько это возможно. Хотя человечество за последние несколько миллионов лет не затаило дыхания для инструмента, превращающего лошадей в зебр, эта задача демонстрирует способность этих архитектур моделировать сложные процессы реального мира с дистанционным наблюдением. Хотя у них есть свои пределы, есть намеки на то, что в будущем мы не сможем отличить настоящее от подделки по видеотрансляции в прямом эфире, что открывает банку с червями, которую мы должным образом закроем прямо сейчас.

Пришло время поиграть с предварительно обученным CycleGAN. Это дает нам возможность подойти на шаг ближе и посмотреть, как реализована сеть, в данном случае генератор. Давайте сделаем это сразу: так выглядит возможная архитектура генератора для задачи horse to zebra. В нашем случае это наш старый друг ResNet. Мы покажем полный исходный код класса ResnetGenerator, чтобы продемонстрировать, что он сжат для того, что он делает. Он берет изображение, распознает в нем одну или несколько лошадей, глядя на пиксели, и индивидуально изменяет значения этих пикселей, в результате чего получается нечто похожее на правдоподобную зебру. Мы не узнаем ничего подобного в исходном коде, потому что его там нет; сеть — эшафот, сок — в весах.

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

# In[1]:
 import torch
 import torch.nn as nn
  
 class ResNetBlock(nn.Module):
  
     def __init__(self, dim):
         super(ResNetBlock, self).__init__()
         self.conv_block = self.build_conv_block(dim)
  
     def build_conv_block(self, dim):
         conv_block = []
  
         conv_block += [nn.ReflectionPad2d(1)]
  
         conv_block += [nn.Conv2d(dim, dim, kernel_size=3, padding=0, bias=True),
                        nn.InstanceNorm2d(dim),
                        nn.ReLU(True)]
  
         conv_block += [nn.ReflectionPad2d(1)]
  
         conv_block += [nn.Conv2d(dim, dim, kernel_size=3, padding=0, bias=True),
                        nn.InstanceNorm2d(dim)]
  
         return nn.Sequential(*conv_block)
  
     def forward(self, x):
         out = x + self.conv_block(x)
         return out
  
  
 class ResNetGenerator(nn.Module):
  
     def __init__(self, input_nc=3, output_nc=3, ngf=64, n_blocks=9):
  
         assert(n_blocks >= 0)
         super(ResNetGenerator, self).__init__()
  
         self.input_nc = input_nc
         self.output_nc = output_nc
         self.ngf = ngf
  
         model = [nn.ReflectionPad2d(3),
                  nn.Conv2d(input_nc, ngf, kernel_size=7, padding=0, bias=True),
                  nn.InstanceNorm2d(ngf),
                  nn.ReLU(True)]
  
         n_downsampling = 2
         for i in range(n_downsampling):
             mult = 2**i
             model += [nn.Conv2d(ngf * mult, ngf * mult * 2, kernel_size=3,
                                 stride=2, padding=1, bias=True),
                       nn.InstanceNorm2d(ngf * mult * 2),
                       nn.ReLU(True)]
  
         mult = 2**n_downsampling
         for i in range(n_blocks):
             model += [ResNetBlock(ngf * mult)]
  
         for i in range(n_downsampling):
             mult = 2**(n_downsampling - i)
             model += [nn.ConvTranspose2d(ngf * mult, int(ngf * mult / 2),
                                          kernel_size=3, stride=2,
                                          padding=1, output_padding=1,
                                          bias=True),
                       nn.InstanceNorm2d(int(ngf * mult / 2)),
                       nn.ReLU(True)]
  
         model += [nn.ReflectionPad2d(3)]
         model += [nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0)]
         model += [nn.Tanh()]
  
         self.model = nn.Sequential(*model)
  
     def forward(self, input):
         return self.model(input)

Здесь мы объявили два класса, ResNetGenerator и ResNetBlock. Последний используется из первого, и оба они происходят от nn.Module, который является способом PyTorch для указания части нейронной сети или, более элегантно, части дифференцируемых вычислений. Каждый экземпляр nn.Module можно вызывать как функцию с теми же аргументами, что и в функции forward — в данном случае input.

Не вдаваясь в подробности, мы можем распознать строительные блоки, также называемые модулями в PyTorch и обычно называемые слоями в других фреймворках, из которых состоят вычисления. Мы можем определить линейные функции, такие как Conv2d, посредством которых входное изображение сворачивается с изученными фильтрами для получения выходных данных, и нелинейные функции, такие как Tanh и ReLU. Все они создаются, накапливаются в списке model и передаются в контейнер nn.Sequential. При вызове с вводом последний вызывает каждый содержащийся модуль с выводом предыдущего модуля в качестве ввода. Это один из способов определения моделей в PyTorch.

На данный момент мы можем создать экземпляр класса ResNetGenerator с параметрами по умолчанию:

# In[2]:  
netG = ResNetGenerator()

На данный момент модель создана, но содержит мусор в виде весов. Ранее мы упоминали, что запускали модель генератора, предварительно обученную на наборе данных horse2zebra. Веса модели сохраняются в файле pth, который представляет собой не что иное, как файл pickle тензорных параметров модели. Мы можем загрузить их в наш ResNetGenerator, используя метод load_state_dict nn.Module:

# In[3]:
 model_path = 'horse2zebra_0.4.0.pth'
 model_data = torch.load(model_path)
 netG.load_state_dict(model_data)

В этот момент netG приобрел все знания, полученные во время обучения. Обратите внимание, что это полностью эквивалентно тому, что произошло, когда мы загрузили ResNet101 из torchvision, только функция torchvision.resnet101 скрыла это от нас.

Переведем сеть в режим eval, как мы это сделали для ResNet101:

# In[4]:
 netG.eval()
  
 # Out[4]:
 ResNetGenerator(
   (model): Sequential(
 ...
   )
 )

Мы готовы загрузить случайное изображение лошади и посмотреть, что выдает наш генератор. Прежде всего, нам нужно импортировать PIL и torchvision

# In[5]:
 from PIL import Image
 from torchvision import transforms

Затем мы определяем несколько входных преобразований, чтобы убедиться, что данные поступают в сеть с правильной формой и размером:

# In[6]:
 preprocess = transforms.Compose([transforms.Resize(256),
                                  transforms.ToTensor()])</programlisting>
 <simpara>Let&#8217;s open a horse file

Откроем файл с лошадью:

# In[7]:
  img = Image.open("horse.jpg") img

О, там чувак на лошади. Ненадолго, судя по картинке. В любом случае, давайте пропустим его через предварительную обработку и превратим в переменную правильной формы:

# In[8]:
img_t = preprocess(img)
batch_t = torch.unsqueeze(img_t, 0)

Мы не должны беспокоиться о деталях прямо сейчас. Важно то, что мы следим на расстоянии. На данный момент img_v можно отправить в нашу модель

# In[9]:
batch_out = netG(batch_t)

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

# In[10]:
 out_t = (batch_out.data.squeeze() + 1.0) / 2.0
 out_img = transforms.ToPILImage()(out_t)
 # out_img.save('zebra.jpg')
 out_img
  
 # Out[10]:
 <PIL.Image.Image image mode=RGB size=316x256 at 0x1C0C8E4C550>

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

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

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

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

Предварительно обученная сеть, описывающая сцены

Чтобы получить непосредственный опыт работы с моделью, включающей естественный язык, мы будем использовать предварительно обученную модель подписей к изображениям, щедро предоставленную Руотяном Луо и реализованную после работы над NeuralTalk2 Андреем Карпати. Мы поддерживаем клон кода по адресу [REF]. Этот тип моделей генерирует заголовок на современном английском языке, описывающий сцену, когда представлено естественное изображение. Опять же, интересная часть заключается в том, что модель обучается на большом наборе данных изображений с их описанием предложения, например. «Полосатый кот опирается на деревянный стол, одной лапой на лазерную мышь, а другой на черный ноутбук» [REF paper].

Эта модель субтитров состоит из двух соединенных половинок. Первая половина сети учится генерировать «описательные» числовые представления сцены (полосатый кот, лазерная мышь, лапа), которые затем принимаются в качестве входных данных для второй половины. Эта половина представляет собой рекуррентную нейронную сеть, которая генерирует связное предложение, объединяя эти описания. Вся архитектура полностью обучена на парах изображение-заголовок.

Несколько других предложенных моделей субтитров, в частности img2seq, из семейства seq2seq, которые представляют собой универсальные модели, специализирующиеся на кодировании входной последовательности (в данном случае последовательности пикселей) в вектор, который затем декодируется в другую последовательность (последовательность символов или слов).

НЕЙРАЛЬНЫЙ РАЗГОВОР2

Вернемся к модели NeuralTalk2, мы можем найти ее по адресу github.com/deep-learning-with-pytorch/ImageCaptioning.pytorch. Мы можем просто поместить набор изображений в каталог data и запустить следующий скрипт.

python eval.py --model ./data/FC/fc-model.pth --infos_path ./data/FC/fc-infos.pkl --image_folder ./data

Давайте попробуем с нашим изображением horse.jpg. На нем написано: «Человек, едущий на лошади по пляжу». Вполне уместно.

Теперь ради интереса посмотрим, сможет ли наш CycleGAN обмануть и эту модель NeuralTalk2. Давайте добавим изображение zebra.jpg в папку данных и перезапустим модель: «Группа зебр стоит в поле». Что ж, он правильно понял животное, но увидел на изображении больше одного из них. Наверняка это не та поза, которую сеть когда-либо видела у зебры, и она никогда не видела всадника на вершине зебры (с некоторыми ложными узорами зебры). Кроме того, вполне вероятно, что зебры изображены группами в обучающем наборе данных, и может быть некоторая предвзятость, которую можно исследовать. Сеть субтитров также не увидела гонщика. Опять же, вероятно, по той же причине: сеть никогда не видела всадника на зебре в обучающем наборе данных.

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

Мы хотели бы подчеркнуть, что что-то подобное, чего было чрезвычайно трудно достичь до появления глубокого обучения, можно получить с менее чем тысячей строк кода, с архитектурой общего назначения, которая ничего не знает о лошадях или зебрах, и корпус изображений и их описания (в данном случае набор данных MS COCO). Никаких жестко запрограммированных критериев или грамматики — все, включая предложение, возникает из закономерностей в данных.

Сетевая архитектура в этом последнем случае была более сложной, чем те, что мы видели ранее — в ней есть сверточная часть и рекуррентная часть.

На этом мы пока остановимся. И помните, это всего лишь пример того, на что способен PyTorch.

Чтобы узнать больше о книге, ознакомьтесь с ней на сайте liveBook бесплатно здесь.

Об авторе:
Эли Стивенс
последние 15 лет работал в Кремниевой долине инженером-программистом, а последние 7 лет – техническим директором стартапа, разрабатывающего программное обеспечение для медицинских устройств. Лука Антига — соучредитель и генеральный директор компании по разработке ИИ, расположенной в Бергамо, Италия, а также постоянный участник PyTorch.

Первоначально опубликовано на freecontent.manning.com.