Влезте в духа на Хелоуин на GAN с този урок за генератор на тиква

(Генеративно състезателно обучение. Сините линии показват поток от входове, зелените линии изходи, а червените линии сигнали за грешка.)

Генеративните състезателни мрежи, или накратко GAN, са една от най-вълнуващите области на задълбочено обучение, възникващи през последните 10 години. Това е според, между другото, Yann LeCun от MNIST и славата на обратното разпространение. Бързият напредък след въвеждането на GAN през 2014 г. от Иън Гудфелоу и други маркира състезателното обучение като пробивна идея, пълна с потенциала да промени обществото по полезни, нечестиви и глупави начини. Обучението на GAN е използвано за всичко - от предсказуемите „котешки“ „генератори“ до измислени портретисти, „нарисувани“ от GAN, продавани за „шестцифрени суми“ на търгове за изобразително изкуство. Всички GAN се основават на простата предпоставка за дуелни мрежи: творческа мрежа, която генерира някакъв вид изходни данни (в нашия случай изображения) и скептична мрежа, която извежда вероятност, че данните са реални или генерирани. Те са известни като мрежите „Генератор“ и „Дискриминатор“ и като просто се опитват да си попречат, те могат да се научат да генерират реалистични данни. В този урок ще изградим GAN на базата на популярната напълно конволюционна DCGAN архитектура и ще го обучим да произвежда тикви за Хелоуин.

Ние ще използваме PyTorch, но можете също да използвате TensorFlow (ако това е, което ви харесва). Опитът от използването на двете големи библиотеки за дълбоко обучение стана поразително сходен, като се имат предвид промените в TensorFlow в тазгодишното 2.0 издание, което видяхме като част от по-широко сближаване на популярни рамки към динамично изпълняван, питоничен код с опция компилация на оптимизирана графика за ускоряване и внедряване.

За да настроите и активирате виртуална среда за основни експерименти с PyTorch:

virtualenv pytorch --python=python3 pytorch/bin/pip install numpy matplotlib torch torchvision source pytorch/bin/activate

И ако имате инсталирана conda и предпочитате да я използвате:

conda new -n pytorch numpy matplotlib torch torchvision conda activate pytorch

И за да ви спестим известно време в гадаене, ето импортираните файлове, от които се нуждаем:

import random import time import numpy as np import matplotlib.pyplot as plt import torch import torch.nn as nn import torch.nn.parallel import torch.optim as optim import torch.nn.functional as F import torch.utils.data import torchvision.datasets as dset import torchvision.transforms as transforms import torchvision.utils as vutils

Нашият GAN ще се основава на DCGAN архитектурата и ще заимства до голяма степен от официалното внедряване в примерите на PyTorch. „DC“ в „DCGAN“ означава „Deep Convolutional“, а архитектурата на DCGAN разшири протокола за неконтролирано състезателно обучение, описан в оригиналния „GAN документ“ на Ian Goodfellow. Това е сравнително проста и интерпретируема мрежова архитектура и може да формира отправна точка за тестване на по-сложни идеи.

Архитектурата DCGAN, подобно на всички GAN, всъщност се състои от две мрежи, дискриминатор и генератор. Важно е те да са еднакви по отношение на мощността им на прилягане, скоростта на обучение и т.н., за да се избегне несъответствието на мрежите. Обучението на GAN е известно нестабилно и може да отнеме доста малко настройка, за да може да работи върху дадена комбинация от архитектура на набор от данни. В този пример с DCGAN е лесно да останете с вашия генератор, извеждащ безсмислици в жълто/оранжево шахматно поле, но не се отказвайте! Като цяло изпитвам силно възхищение към авторите на нови пробиви като този, където би било лесно да се обезсърчите от ранни слаби резултати и може да се изисква героично ниво на търпение. От друга страна, понякога е просто въпрос на обстойна подготовка и добра идея, която се обединява, и нещата се получават само с „няколко допълнителни часа работа и изчисления“.

Генераторът е стек от транспонирани конволюционни слоеве, които трансформират дълго и слабо, многоканално тензорно латентно пространство в изображение в пълен размер. Това е илюстрирано в следната диаграма от документа на DCGAN:

Напълно конволюционен генератор от Radford et al. 2016.

Ще създадем екземпляр като подклас на класа torch.nn.Module. Това е гъвкав начин за внедряване и разработване на модели. Можете да заредите в forward клас функцията позволява включването на неща като прескачане на връзки, които не са възможни с прост екземпляр на torch.nn.Sequential модел.

class Generator(nn.Module): def __init__(self, ngpu, dim_z, gen_features, num_channels): super(Generator, self).__init__() self.ngpu = ngpu self.block0 = nn.Sequential(\ nn.ConvTranspose2d(dim_z, gen_features*32, 4, 1, 0, bias=False),\ nn.BatchNorm2d(gen_features*32),\ nn.ReLU(True)) self.block1 = nn.Sequential(\ nn.ConvTranspose2d(gen_features*32,gen_features*16, 4, 2, 1, bias=False),\ nn.BatchNorm2d(gen_features*16),\ nn.ReLU(True)) self.block2 = nn.Sequential(\ nn.ConvTranspose2d(gen_features*16,gen_features*8, 4, 2, 1, bias=False),\ nn.BatchNorm2d(gen_features*8),\ nn.ReLU(True)) self.block3 = nn.Sequential(\ nn.ConvTranspose2d(gen_features*8, gen_features*4, 4, 2, 1, bias=False),\ nn.BatchNorm2d(gen_features*4),\ nn.ReLU(True)) self.block5 = nn.Sequential(\ nn.ConvTranspose2d(gen_features*4, num_channels, 4, 2, 1, bias=False))\ def forward(self, z): x = self.block0(z) x = self.block1(x) x = self.block2(x) x = self.block3(x) x = F.tanh(self.block5(x)) return x

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

Напълно конволюционен двоичен класификатор, подходящ за използване като дискриминатор D(x).

И кодът:

class Discriminator(nn.Module): def __init__(self, ngpu, gen_features, num_channels): super(Discriminator, self).__init__() self.ngpu = ngpu self.block0 = nn.Sequential(\ nn.Conv2d(num_channels, gen_features, 4, 2, 1, bias=False),\ nn.LeakyReLU(0.2, True)) self.block1 = nn.Sequential(\ nn.Conv2d(gen_features, gen_features, 4, 2, 1, bias=False),\ nn.BatchNorm2d(gen_features),\ nn.LeakyReLU(0.2, True)) self.block2 = nn.Sequential(\ nn.Conv2d(gen_features, gen_features*2, 4, 2, 1, bias=False),\ nn.BatchNorm2d(gen_features*2),\ nn.LeakyReLU(0.2, True)) self.block3 = nn.Sequential(\ nn.Conv2d(gen_features*2, gen_features*4, 4, 2, 1, bias=False),\ nn.BatchNorm2d(gen_features*4),\ nn.LeakyReLU(0.2, True)) self.block_n = nn.Sequential( nn.Conv2d(gen_features*4, 1, 4, 1, 0, bias=False),\ nn.Sigmoid()) def forward(self, imgs): x = self.block0(imgs) x = self.block1(x) x = self.block2(x) x = self.block3(x) x = self.block_n(x) return x

Ще ни трябват и няколко помощни функции за създаване на устройството за зареждане на данни и инициализиране на теглата на модела според съветите в документа на DCGAN. Функцията по-долу връща програма за зареждане на данни на PyTorch с леко увеличение на изображението, просто го насочете към папката, съдържаща вашите изображения. Работя със сравнително малка партида безплатни изображения от Pixabay, така че увеличаването на изображението е важно за по-добър пробег от всяко изображение.

def get_dataloader(root_path): dataset = dset.ImageFolder(root=root_path,\ transform=transforms.Compose([\ transforms.RandomHorizontalFlip(),\ transforms.RandomAffine(degrees=5, translate=(0.05,0.025), scale=(0.95,1.05), shear=0.025),\ transforms.Resize(image_size),\ transforms.CenterCrop(image_size),\ transforms.ToTensor(),\ transforms.Normalize((0.5,0.5,0.5), (0.5,0.5,0.5)),\ ])) dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,\ shuffle=True, num_workers=num_workers) return dataloader

И за инициализиране на теглата:

def weights_init(my_model): classname = my_model.__class__.__name__ if classname.find("Conv") != -1: nn.init.normal_(my_model.weight.data, 0.0, 0.02) elif classname.find("BatchNorm") != -1: nn.init.normal_(my_model.weight.data, 1.0, 0.02) nn.init.constant_(my_model.bias.data, 0.0)

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

# ensure repeatability my_seed = 13 random.seed(my_seed) torch.manual_seed(my_seed) # parameters describing the input latent space and output images dataroot = "images/pumpkins/jacks" num_workers = 2 image_size = 64 num_channels = 3 dim_z = 64 # hyperparameters batch_size = 128 disc_features = 64 gen_features = 64 disc_lr = 1e-3 gen_lr = 2e-3 beta1 = 0.5 beta2 = 0.999 num_epochs = 5000 save_every = 100 disp_every = 100 # set this variable to 0 for cpu-only training. This model is lightweight enough to train on cpu in a few hours. ngpu = 2

След това инстанцираме моделите и програмата за зареждане на данни. Използвах настройка с двоен GPU, за да оценя бързо няколко различни итерации на хиперпараметъра. В PyTorch е тривиално да тренирате на няколко графични процесора, като опаковате вашите модели в класа torch.nn.DataParallel. Не се притеснявайте, ако всичките ви графични процесори са обвързани в преследването на изкуствен общ интелект, този модел е достатъчно лек за обучение на процесора за разумен период от време (няколко часа).

dataloader = get_dataloader(dataroot) device = torch.device("cuda:0" if ngpu > 0 and torch.cuda.is_available() else "cpu") gen_net = Generator(ngpu, dim_z, gen_features, \ num_channels).to(device) disc_net = Discriminator(ngpu, disc_features, num_channels).to(device) # add data parallel here for >= 2 gpus if (device.type == "cuda") and (ngpu > 1): disc_net = nn.DataParallel(disc_net, list(range(ngpu))) gen_net = nn.DataParallel(gen_net, list(range(ngpu))) gen_net.apply(weights_init) disc_net.apply(weights_init)

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

criterion = nn.BCELoss() # a set sample from latent space so we can unambiguously monitor training progress fixed_noise = torch.randn(64, dim_z, 1, 1, device=device) real_label = 1 fake_label = 0 disc_optimizer = optim.Adam(disc_net.parameters(), lr=disc_lr, betas=(beta1, beta2)) gen_optimizer = optim.Adam(gen_net.parameters(), lr=gen_lr, betas=(beta1, beta2)) img_list = [] gen_losses = [] disc_losses = [] iters = 0

Тренировъчната верига

Обучителният цикъл е концептуално ясен, но малко дълъг, за да се обхване в един фрагмент, така че ще го разделим на няколко части. Най-общо казано, първо актуализираме дискриминатора въз основа на прогнозите за набор от реални и генерирани изображения. След това подаваме генерирани изображения към наскоро актуализирания дискриминатор и използваме класификационния изход от D(G(z)) за тренировъчен сигнал за генератора, като използваме истинския етикет като целта.

Първо ще влезем в цикъла и ще извършим актуализация на дискриминатора:

t0 = time.time() for epoch in range(num_epochs): for ii, data in enumerate(dataloader,0): # update the discriminator disc_net.zero_grad() # discriminator pass with real images real_cpu = data[0].to(device) batch_size= real_cpu.size(0) label = torch.full((batch_size,), real_label, device=device) output = disc_net(real_cpu).view(-1) disc_real_loss = criterion(output,label) disc_real_loss.backward() disc_x = output.mean().item() # discriminator pass with fake images noise = torch.randn(batch_size, dim_z, 1, 1, device=device) fake = gen_net(noise) label.fill_(fake_label) output = disc_net(fake.detach()).view(-1) disc_fake_loss = criterion(output, label) disc_fake_loss.backward() disc_gen_z1 = output.mean().item() disc_loss = disc_real_loss + disc_fake_loss disc_optimizer.step()

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

След това ще актуализираме генератора въз основа на прогнозите на дискриминатора, използвайки реалния етикет и двоичната загуба на кръстосана ентропия. Обърнете внимание, че актуализираме генератора въз основа на погрешната класификация на фалшивите изображения като истински от дискриминатора. Този сигнал създава по-добри градиенти за обучение, отколкото минимизиране на способността на дискриминатора да открива директно фалшификати. Доста впечатляващо е, че GAN в крайна сметка могат да се научат да произвеждат „фотореалистично съдържание“ въз основа на този тип дуелиращи сигнали за загуба.

# update the generator gen_net.zero_grad() label.fill_(real_label) output = disc_net(fake).view(-1) gen_loss = criterion(output, label) gen_loss.backward() disc_gen_z2 = output.mean().item() gen_optimizer.step()

И накрая, има малко домакинска работа, за да следим нашето обучение. Балансирането на GAN обучение е нещо като изкуство и не винаги е очевидно само от числата дали вашите мрежи се учат ефективно, така че е добра идея да проверявате качеството на изображението от време на време. От друга страна, ако някоя от стойностите в оператора за печат достигне 0,0 или 1,0, има вероятност обучението ви да е пропаднало и е добра идея да повторите с нови хиперпараметри.

if ii % disp_every == 0: # discriminator pass with fake images, after updating G(z) noise = torch.randn(batch_size, dim_z, 1, 1, device=device) fake = gen_net(noise) output = disc_net(fake).view(-1) disc_gen_z3 = output.mean().item() print("{} {:.3f} s |Epoch {}/{}:\tdisc_loss: {:.3e}\tgen_loss: {:.3e}\tdisc(x): {:.3e}\tdisc(gen(z)): {:.3e}/{:.3e}/{:.3e}".format(iters,time.time()-t0, epoch, num_epochs, disc_loss.item(), gen_loss.item(), disc_x, disc_gen_z1, disc_gen_z2, disc_gen_z3)) disc_losses.append(disc_loss.item()) gen_losses.append(gen_loss.item()) if (iters % save_every == 0) or \ ((epoch == num_epochs-1) and (ii == len(dataloader)-1)): with torch.no_grad(): fake = gen_net(fixed_noise).detach().cpu() img_list.append(vutils.make_grid(fake, padding=2, normalize=True).numpy()) np.save("./gen_images.npy", img_list) np.save("./gen_losses.npy", gen_losses) np.save("./disc_losses.npy", disc_losses) torch.save(gen_net.state_dict(), "./generator.h5") torch.save(disc_net.state_dict(), "./discriminator.h5") iters += 1

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

Напредък на обучението след около 5000 епохи на актуализации.

Надяваме се, че горният урок е послужил за събуждане на апетита ви към GAN, занаяти за Хелоуин или и двете. След като усвоите основния DCGAN, който изградихме тук, експериментирайте с по-сложни архитектури и приложения. Обучението на GAN все още е изкусна наука, а балансирането на обучението е трудно. Използвайте подсказките от ganhacks и след като получите опростено доказателство за концепцията, работещо за вашия набор от данни/приложение/идея, добавяйте само малки части от сложността наведнъж. Успех и приятно обучение.

Първоначално публикувано в https://blog.exxactcorp.com на 28 октомври 2019 г.