СТАТИЯ

Интензивен курс на PyTorch, част 3

От Задълбочено обучение с PyTorch от Ели Стивънс и Лука Антига

__________________________________________________________________

Вземете 37% отстъпка Задълбочено обучение с PyTorch. Просто въведете код fccstevens в полето за промоционален код за отстъпка при плащане в manning.com.
__________________________________________________________________

В тази статия изследваме някои от възможностите на PyTorch, като играем генеративни състезателни мрежи.

Във втора част видяхме как да използваме предварително обучен модел за класифициране на изображения. Сега нека да разгледаме GAN.

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

Моделите, попадащи под името Generative Adversarial Networks (GANs), са един от най-оригиналните резултати от последните изследвания на дълбокото обучение. Ще ги разгледаме по-късно в нашето пътуване. Засега ще кажем, че докато в стандартните архитектури на невронни мрежи имаме една голяма мрежа, оптимизираща теглата си, за да минимизира функцията на загуба, свързана, да речем, със задача за класифициране, в GAN имаме няколко мрежи, назовава генератора и дискриминатора.

Генераторът има задачата да произведе реалистично изглеждащи изображения, започвайки от вход, докато дискриминаторът трябва да каже дали дадено изображение е произведено от генератора или принадлежи към набор от реални изображения. Крайната цел на генератора е да заблуди дискриминатора да смеси истински и фалшиви изображения. Крайната цел на дискриминатора е да разбере кога е измамен. Нарича се GAN игра.

Имайте предвид, че „Дискриминаторът печели“ или „Генераторът печели“ не трябва да се приема буквално, тъй като няма изрично съвпадение между двете. И двете мрежи са свързани с функции на разходите, които зависят от резултата на другата мрежа и които на свой ред се минимизират по време на обучението.

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

ЦИКЛЕГАН

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

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

Забавната част е, че в този момент не се нуждаем от двойки Моне/снимки като основни истини: достатъчно е да започнем от колекция от несвързани произведения на Моне и пейзажни снимки, за да могат генераторите да научат своята задача, надхвърляйки чисто контролирана настройка. Последствията от този модел отиват дори по-далеч от това: генераторът се научава как избирателно да променя външния вид на обектите в сцената, без да контролира какво има. Никакви сигнали не показват, че водата е вода и дървото е дърво, но те се превеждат в нещо, което е начинът, по който водата и дърветата са представени в домейна на Моне и обратно.

МРЕЖА, КОЯТО ПРЕВРЪЩА КОНЕТЕ В ЗЕБРИ

Още по-ясен пример е Horse2Zebra CycleGAN, с който ще играем в момента. В този случай мрежата CycleGAN беше обучена на набор от (несвързани) изображения на коне и изображения на зебра, извлечени от набора от данни на ImageNet. Мрежата се научава да прави изображение на един или повече коне и да ги превръща в зебри, оставяйки останалите възможно най-непроменени. Въпреки че човечеството не е затаило дъх през последните няколко милиона години за инструмент, който превръща конете в зебри, тази задача демонстрира способността на тези архитектури да моделират сложни процеси в реалния свят с дистанционно наблюдение. Въпреки че имат своите ограничения, има намеци, че в бъдеще няма да можем да различим истинско от фалшиво от видео емисия на живо, което отваря кутия с червеи, която надлежно ще затворим точно сега.

Време е да играете с предварително обучен CycleGAN. Това ни дава възможност да направим крачка по-близо и да разгледаме как се реализира мрежа, в този случай генератор. Нека го направим веднага: така изглежда една възможна генераторна архитектура за задачата кон към зебра. В нашия случай това е нашият стар приятел 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 може да бъде извикан като функция със същите аргументи, както са посочени във функцията forwardinput в този случай.

Без да навлизаме в детайлите, можем да разпознаем градивните елементи, наричани също модули в 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>

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

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

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

Дотук имахме възможност да играем с модел, който вижда в изображения, и модел, който генерира нови изображения. Ще завършим нашата обиколка с модел, който включва още една основна съставка: естествения език.

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

За да получим опит от първа ръка с модел, включващ естествен език, ще използваме предварително обучен модел за надписи на изображения, щедро предоставен от Ruotian Luo и внедрен след работата по NeuralTalk2 от Андрей Карпати. Ние поддържаме клонинг на кода на [REF]. Този вид модели генерират надпис на текущия английски език, описващ сцена, когато се представя с естествено изображение. Отново, интересната част е, че моделът се обучава върху голям набор от изображения с тяхното описание на изречение, напр. „Една котка тип Би се обляга на дървена маса, с една лапа върху лазерна мишка, а другата върху черен лаптоп“ [REF хартия].

Този модел за надписи има две свързани половини. Първата половина на мрежата се научава да генерира „описателни“ числени представяния на сцената (котка с ризи, лазерна мишка, лапа), които след това се приемат като входни данни за втората половина. Тази половина е повтаряща се невронна мрежа, която генерира съгласувано изречение чрез обединяване на тези описания. Цялата архитектура е обучена от край до край на двойки изображение-надпис.

Няколко други предложени модела за надписи, по-специално img2seq, от семейството seq2seq, които са многофункционални модели, специализирани в кодирането на входна последователност (в този случай последователност от пиксели) във вектор, който след това се декодира в друга последователност (последователност на знаци или думи).

NEURALTALK2

Обратно към модела 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.