Një Tutorial GAN ​​për fillimin

Në këtë tutorial, ne do të ndërtojmë një DCGAN të thjeshtë në PyTorch dhe do ta trajnojmë atë për të gjeneruar shifra të shkruara me dorë. Si pjesë e këtij tutoriali, ne do të diskutojmë PyTorch DataLoader dhe si ta përdorim atë për të ushqyer të dhënat e imazhit real në një rrjet nervor PyTorch për trajnim. PyTorch është fokusi i këtij tutoriali, kështu që unë do të supozoj se jeni njohur me mënyrën se si funksionojnë GAN-et.

Kërkesat

  1. Python 3.7 ose më i lartë. Sa më i ulët dhe do të duhet të rifaktoroni vargjet f.
  2. PyTorch 1.5 Nuk jeni i sigurt se si ta instaloni atë? "Kjo mund të ndihmojë".
  3. Matplotlib 3.1 ose më i lartë
  4. Njëzet e dy minuta apo më shumë nga koha juaj.

Nuk kërkohet, por rekomandova të lexoni fillimisht "tutorialin tim të vaniljes GAN"; ai shpjegon disa gjëra që ky tutorial i merr si të mirëqenë. Unë gjithashtu rekomandoj që ta bëni këtë tutorial në një kompjuter me një GPU CUDA, ose të mbani gati një libër të madh të Sudokus.

Detyra në dorë

Krijoni një funksion G: Z → X ku Z~N₁(0, 1) dhe X~MNIST.

Kjo do të thotë, trajnoni një GAN që merr zhurmë të rastësishme 16-dimensionale dhe prodhon imazhe që duken si mostra reale nga grupi i të dhënave MNIST.

Por Para se të Fillojmë…

…le të bëjmë pak mirëmbajtjen e shtëpisë. Nëse nuk e keni bërë tashmë, instaloni versionet e kërkuara të Python dhe bibliotekat e mësipërme. Pastaj, krijoni drejtorinë e projektit tuaj. E thirra timen DCGAN. Në atë direktori, krijoni një drejtori të quajtur data. Më pas, lundroni te this repo GitHub dhe shkarkoni mnist_png.tar.gz. Ky skedar i ngjeshur përmban grupin e të dhënave MNIST si 70000 skedarë individualë png. Sigurisht, ne mund të përdorim grupin e integruar të të dhënave MNIST të PyTorch, por atëherë nuk do të mësoni se si të ngarkoni në të vërtetë të dhënat e imazhit për trajnim. Dekompresoni skedarin dhe vendosni drejtorinë mnist_png në drejtorinë tuaj data. Krijoni një skedar të quajtur dcgan_mnist.py dhe vendoseni në drejtorinë tuaj DCGAN. Drejtoritë e projekteve tuaja duhet të duken kështu:

Së fundi, shtoni sa vijon në skriptin tuaj dcgan_mnist.py:

import os
import torch
from torch import nn
from torch import optim
import torchvision as tv
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader

Në rregull, tani jemi gati të fillojmë.

Gjenerator

Shtoni sa vijon në skriptin tuaj dcgan_mnist.py:

Gjeneratori trashëgon nn.Module, e cila është klasa bazë për një rrjet nervor PyTorch. Gjeneratori ka tre mënyra:

Gjenerator.__init__

Konstruktori, i cili ruan variablat e instancës dhe thërret _init_layers. Nuk ka shumë për të thënë këtu.

Gjenerator._init_modules

Kjo metodë instancon modulet PyTorch (ose "shtresat", siç quhen në korniza të tjera). Kjo perfshin:

  • Një modul linear (“i lidhur plotësisht”), për hartimin e hapësirës latente në një hapësirë ​​7×7×256=12544-dimensionale. Siç do të shohim në metodën forward, ky tensor me gjatësi 12544 riformohet në një tensor "imazhi" (256, 7, 7) (kanalet×lartësia×gjerësia). Në PyTorch, ndryshe nga TensorFlow, kanalet vijnë para dimensioneve hapësinore.
  • Një modul 1-dimensional i normalizimit të grupit, nëse specifikohet.
  • Një modul ReLU që rrjedh.
  • Një shtresë konvolucionale 2-dimensionale.
  • Dy shtresa konvolucionare të transpozuara 2-dimensionale; këto përdoren për të përmirësuar shkallën e imazhit. Vini re se si kanalet jashtë të një shtrese konvolucionare janë kanalet brenda të tjetrës.
  • Dy shtresa 2-dimensionale të normalizimit të grupeve, nëse specifikohen.
  • Një modul Tanh si aktivizimi i daljes. Ne do t'i rishkallëzojmë imazhet tona në intervalin [-1, 1], kështu që aktivizimi i daljes së gjeneratorit tonë duhet ta pasqyrojë këtë.

Këto mund të ishin instancuar në metodën __init__, por më pëlqen ta mbaj instancimin e modulit të ndarë nga konstruktori. Është e parëndësishme për një model kaq të thjeshtë, por ndihmon në mbajtjen e kodit të thjeshtë ndërsa modeli rritet në kompleks.

Gjenerator.forward

Kjo është metoda që përdor Gjeneratori ynë për të gjeneruar mostra nga zhurma e rastësishme. Tensori i hyrjes i kalohet modulit të parë, dalja i kalohet modulit tjetër, dalja e atij kalohet në modulin tjetër, etj. Është mjaft e drejtpërdrejtë, por unë do të doja për të tërhequr vëmendjen tuaj në dy karakteristika interesante:

  1. Vini re rreshtin intermediate = intermediate.view((-1, 256, 7, 7)). Ndryshe nga Keras, PyTorch nuk përdor një modul të qartë të "riformësimit"; në vend të kësaj, ne riformësojmë manualisht tensorin duke përdorur operacionin PyTorch "view". Operacione të tjera të thjeshta PyTorch mund të aplikohen edhe gjatë kalimit përpara, si shumëzimi i tensorit me dy, dhe PyTorch nuk do të ngrejë syrin.
  2. Vini re se si ka deklarata if në metodën forward. PyTorch përdor një strategji define-nga-run, që do të thotë se grafiku llogaritës është ndërtuar në lëvizje gjatë kalimit përpara. Kjo e bën PyTorch jashtëzakonisht fleksibël; Nuk ka asgjë që ju ndalon të shtoni sythe në kalimin tuaj përpara, ose të zgjidhni rastësisht një nga disa module për t'u përdorur.

Diskriminues

Shtoni sa vijon në skriptin tuaj dcgan_mnist.py:

Nuk do të hyj në shumë detaje në lidhje me këtë pasi është shumë i ngjashëm me Gjeneratorin, por mbrapsht. Lexojeni mirë dhe sigurohuni që të kuptoni se çfarë po bën.

DCGAN

Shtoni sa vijon në skriptin tuaj dcgan_mnist.py:

DCGAN.__fillim__

Le të kalojmë ndërtuesin rresht pas rreshti:

self.generator = Generator(latent_dim).to(device)
self.discriminator = Discriminator().to(device)

Dy linjat e para (jo-docstring) të konstruktorit instantojnë Gjeneruesin dhe Diskriminuesin, i zhvendosin në pajisjen e specifikuar dhe i ruajnë si variabla të shembullit. Pajisja është zakonisht "cpu" ose "cuda" nëse dëshironi të përdorni një gpu.

self.noise_fn = noise_fn

Më pas, ne ruajmë noise_fn si një variabël shembulli; noise_fn është një funksion që merr një numër të plotë num si hyrje dhe kthen num vektorë latent si tensor PyTorch si dalje me formë (num, latent_dim). Ky tensor PyTorch duhet të jetë në pajisjen e specifikuar.

self.dataloader = dataloader

Ne ruajmë ngarkuesin e të dhënave, një objekt torch.utils.data.DataLoader, si një variabël shembulli; më shumë për këtë më vonë.

self.batch_size = batch_size
self.device = device

Ruani madhësinë e grupit dhe pajisjen si variabla të shembullit. E thjeshtë.

self.criterion = nn.BCELoss()
self.optim_d = optim.Adam(self.discriminator.parameters(), lr=lr_d, betas=(0.5, 0.999))
self.optim_g = optim.Adam(self.generator.parameters(), lr=lr_g, betas=(0.5, 0.999))

Vendosni funksionin e humbjes në "entropi binar të kryqëzuar" dhe vendosni optimizuesit e Adamit për gjeneratorin dhe diskriminuesin. Optimizuesit e PyTorch duhet të dinë se çfarë po optimizojnë. Për diskriminuesin, kjo nënkupton të gjithë parametrat e trajnueshëm brenda rrjetit Diskriminues. Për shkak se klasa jonë Discriminator trashëgon nga nn.Module, ajo ka metodën parameters() e cila kthen të gjithë parametrat e trajnueshëm në të gjitha variablat e instancës së saj që janë gjithashtu Module PyTorch. E njëjta gjë vlen edhe për Gjeneratorin.

self.target_ones = torch.ones((batch_size, 1), device=device)
self.target_zeros = torch.zeros((batch_size, 1), device=device)

Objektivat për stërvitje, të vendosura në pajisjen e specifikuar. Mos harroni, Diskriminuesi po përpiqet të klasifikojë mostrat reale si 1 dhe mostrat e gjeneruara si 0, ndërsa Gjeneruesi po përpiqet të bëjë që Diskriminuesi të keqklasifikojë mostrat e krijuara si 1. Ne i përcaktojmë dhe i ruajmë ato këtu në mënyrë që të mos na duhet t'i ribëjmë ato me çdo hap trajnimi.

DCGAN.gjenerojë_mostrat

Një metodë ndihmëse për gjenerimin e mostrave. Vini re se përdoret menaxheri i kontekstit no_grad, i cili i thotë PyTorch të mos mbajë gjurmët e gradientëve sepse kjo metodë nuk përdoret për trajnimin e rrjetit. Vini re gjithashtu se, pavarësisht nga pajisja e specifikuar, tensori i kthyer është vendosur në CPU, gjë që është e nevojshme për përdorim të mëtejshëm, si p.sh. shfaqja e mostrave ose ruajtja e tyre në disk.

DCGAN.train_step_generator

Kjo metodë kryen një hap stërvitor të gjeneratorit dhe e kthen humbjen si notim. Le ta kalojmë atë:

self.generator.zero_grad()

Pastro gradientin e gjeneratorit. Kjo është e nevojshme, pasi PyTorch automatikisht mban gjurmët e gradientëve dhe rrjetit llogaritës. Ne nuk duam që një hap trajnimi të ndikojë në hapin tjetër.

latent_vec = self.noise_fn(self.batch_size)
generated = self.generator(latent_vec)
classifications = self.discriminator(generated)
loss = self.criterion(classifications, self.target_ones)

Merrni një grup vektorësh latent, përdorni ato për të gjeneruar mostra, diskriminoni se sa realist është secili mostër, më pas llogaritni humbjen duke përdorur kriterin binar të ndër-entropisë. Vini re se duke i lidhur këto rrjete së bashku ne po krijojmë një grafik të vetëm llogaritës duke filluar nga vektori latent, duke përfshirë rrjetet Gjenerator dhe Diskriminues, dhe duke përfunduar me humbje.

loss.backward()
self.optim_g.step()

Një nga përfitimet kryesore të PyTorch është se ai automatikisht mban gjurmët e grafikut llogaritës dhe gradientëve të tij. Duke thirrur metodën backward në humbje, PyTorch aplikon përhapjen e pasme dhe llogarit gradientët e humbjes në lidhje me çdo parametër në grafikun e llogaritjes. Duke thirrur më pas metodën step të optimizuesit të Gjeneratorit, parametrat e Gjeneratorit (dhe vetëmparametrat e Gjeneratorit) shtyhen pak në drejtim negativ të gradientit të tyre.

return loss.item()

Më në fund e kthejmë humbjen. Është e rëndësishme të përdorim metodën item, në mënyrë që të kthejmë një float në vend të një tensor PyTorch. Nëse në vend të kësaj do të kishim kthyer tensorin, mbledhësi i mbeturinave Python nuk do të ishte në gjendje të pastronte grafikun themelor llogaritës dhe do të na mbaronte shpejt memoria.

DCGAN.train_step_discriminator

Kjo metodë është shumë e ngjashme me train_step_generator, por me dy dallime të dukshme. Së pari:

with torch.no_grad():
    fake_samples = self.generator(latent_vec)

Menaxheri i kontekstit no_grad përdoret këtu për t'i thënë PyTorch të mos shqetësohet për të mbajtur gjurmët e gradientëve. Nuk është e nevojshme, por shkurton llogaritjet e panevojshme. E dyta:

loss = (loss_real + loss_fake) / 2

Kjo linjë është vërtet e lezetshme. loss_real është humbja e diskriminuesit për mostrat reale (dhe bashkëngjitur me të është grafiku i tij llogaritës), dhe loss_fake është humbja (dhe grafiku) për mostrat e rreme. PyTorch është në gjendje t'i kombinojë këto në një grafik llogaritës duke përdorur operatorin +. Më pas ne aplikojmë ripërhapjen dhe përditësimet e parametrave në atë grafik të kombinuar llogaritës. Nëse nuk mendoni se është jashtëzakonisht e thjeshtë, provoni ta rishkruani këtë në një kornizë tjetër.

DCGAN.train_epoch

Ky funksion trajnon gjeneruesin dhe diskriminuesin për një epokë, që është një kalim mbi të gjithë grupin e të dhënave. Ne do të kthehemi te kjo pas një devijim të shkurtër.

kryesore

Shtoni kodin e mëposhtëm në skriptin tuaj:

Ky funksion ndërton, trajnon dhe shfaq GAN-in.

import matplotlib.pyplot as plt
from time import time
batch_size = 32
epochs = 100
latent_dim = 16

Importoni pyplot (për vizualizimin e shifrave të gjeneruara) dhe time (për kohën e trajnimit). Vendosni madhësinë e grupit të trajnimit në 32, numrin e epokave në 100 dhe dimensionin latent në 16.

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

Kjo linjë kontrollon nëse një pajisje cuda është e disponueshme. Nëse është, device i është caktuar ajo pajisje; përndryshe, device i caktohet procesori.

transform = tv.transforms.Compose([
            tv.transforms.Grayscale(num_output_channels=1),
            tv.transforms.ToTensor(),
            tv.transforms.Normalize((0.5,), (0.5,))
            ])

Ky transformim i përbërë përdoret nga ngarkuesi i të dhënave për të përpunuar paraprakisht imazhet. Të dhënat e MNIST që kemi shkarkuar më parë janë skedarë .png; kur PyTorch i ngarkon ato nga disku, ato duhet të përpunohen në mënyrë që rrjeti ynë nervor t'i përdorë ato siç duhet. Transformimet janë sipas renditjes:

  • Grayscale(num_output_channels=1): Konvertoni imazhin në shkallë gri. Kur ngarkohen, shifrat MNIST janë në format RGB me tre kanale. Greyscale i zvogëlon këto tre në një.
  • ToTensor(): Konvertoni imazhin në një Tensor PyTorch, me dimensione kanale × lartësi × gjerësi. Kjo gjithashtu rishkallëzon vlerat e pikselit, nga numrat e plotë midis 0 dhe 255 në notat midis 0.0 dhe 1.0.
  • Normalize((0.5,), (0.5,)): Shkalloni dhe përktheni vlerat e pikselit nga diapazoni [0.0, 1.0] në [-1.0, 1.0]. Argumenti i parë është μ dhe argumenti i dytë është σ, dhe funksioni i aplikuar për çdo piksel është:

Arsyeja që μ dhe σ janë një-tupe është se ky transformim zbatohet për kanal. Transformimi ekuivalent në një imazh RGB do të ishte Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))

dataset = ImageFolder(
            root=os.path.join("data", "mnist_png", "training"),
            transform=transform
            )

Këtu krijojmë grupin e të dhënave duke specifikuar rrënjën e tij dhe transformimet që do të aplikohen. Kjo përdoret për të krijuar DataLoader:

dataloader = DataLoader(dataset,
            batch_size=batch_size,
            shuffle=True,
            num_workers=2
            )

DataLoader është një objekt që ... mirë, ai ngarkon të dhëna nga një grup të dhënash. Këtu ne specifikojmë madhësinë e grupit tonë, i themi ngarkuesit të të dhënave të përziejë grupin e të dhënave midis epokave dhe përdor multiprocessing me dy procese punëtore (nëse jeni duke përdorur Windows dhe kjo shkakton probleme, vendosni num_workers në 0). Ju mund të përsërisni përmes këtij ngarkuesi të të dhënave dhe me çdo përsëritje ai do të kthejë një tuple që përmban:

  1. një tensor PyTorch i formës (32, 1, 28, 28) që korrespondon me një grup (32 mostra) të imazheve MNIST në shkallë gri (1 kanal) (28×28 piksele).
  2. një tensor PyTorch i formës (32,) me shifra nga 0 deri në 9, që korrespondon me etiketën (shifrën) e asaj imazhi. Këto etiketa të klasave janë marrë nga struktura e drejtorisë, pasi të gjitha zerot ishin në drejtorinë 0, të gjitha ato në 1, etj.
noise_fn = lambda x: torch.rand((x, latent_dim), device=device)

Një funksion për gjenerimin e zhurmës së rastësishme, të shpërndarë normalisht.

gan = DCGAN(latent_dim, noise_fn, dataloader, device=device)
start = time()
for i in range(10):
    print(f"Epoch {i+1}; Elapsed time = {int(time() - start)}s")
    gan.train_epoch()

Ndërtoni dhe trajnoni GAN-in.

DCGAN.train_epoch, përsëri:

Le ta rishikojmë këtë, tani që kemi diskutuar se çfarë është një DataLoader. Metoda është mjaft vetë-shpjeguese, edhe pse e folur, por unë do të doja të përqendrohesha në dy rreshta:

for batch, (real_samples, _) in enumerate(self.dataloader):
   real_samples = real_samples.to(self.device)

Këtu, ne përsërisim përmes ngarkuesit të të dhënave. Ne e mbështjellim ngarkuesin e të dhënave në një numërues në mënyrë që të mund të mbajmë gjurmët e numrit të grupit, por siç mund ta shihni, ngarkuesi i të dhënave me të vërtetë kthen një tufë siç premtoi. Ne e caktojmë tensorin e grupit të imazheve në real_samples dhe i shpërfillim etiketat pasi nuk na duhen. Pastaj, në lak, ne lëvizim real_samples në pajisjen e specifikuar. Është e rëndësishme që hyrja në model dhe vetë modeli të jenë në të njëjtën pajisje; mos u shqetësoni nëse harroni ta bëni këtë, PyTorch me siguri do t'ju njoftojë! Gjithashtu, mos u shqetësoni se ngarkuesi i të dhënave "ka mbaruar". Pasi të kemi përsëritur të gjithë grupin e të dhënave, cikli do të përfundojë, por nëse përpiqemi ta përsërisim përsëri, ai do të fillojë përsëri në fillim (duke ngatërruar imazhet fillimisht, sepse ne e kemi specifikuar këtë kur kemi bërë ngarkuesin e të dhënave).

Le të provojmë ta ekzekutojmë?

Nëse kopjoni dhe ngjitni saktë, ekzekutimi i skriptit duhet t'ju tregojë statistikat e trajnimit për disa minuta të ndjekura nga disa shifra të krijuara. Shpresojmë, duket diçka si kjo:

Nëse ato duken të tmerrshme dhe humbja juaj shpërtheu, provoni ta ekzekutoni përsëri (GAN-et janë jashtëzakonisht të paqëndrueshme). Nëse akoma nuk funksionon, hidhni një koment më poshtë dhe do të shohim nëse nuk mund ta korrigjojmë atë.

Vetëm për argëtim, modifikova skenarin për të parë se çfarë ishte në gjendje të bënte Gjeneratori pas çdo 10 hapash stërvitor. Këtu ishin rezultatet.

Unë mendoj se kjo është shumë e mirë për vetëm 1000 hapa. Këtu është humbja mbi ato hapa stërvitor, të ndarë në "epoka" me 10 hapa.

Mendime mbyllëse

  • DCGAN i përshkruar në këtë tutorial është padyshim shumë i thjeshtë, por duhet të jetë i mjaftueshëm për t'ju bërë të filloni të zbatoni GAN më komplekse në PyTorch.
  • A mund ta modifikoni këtë skript për të krijuar një "GAN të kushtëzuar" përpara se të bëj një tutorial për të?
  • Skenari i plotë është i disponueshëm "këtu".
  • Të gjitha imazhet e pacituara janë të miat. Mos ngurroni t'i përdorni ato, por ju lutemi citoni këtë artikull ❤