20.2 Gilieji konvoliuciniai generatyviniai priešiškieji tinklai

20.1 skyrelis pristatėme pagrindines GAN veikimo idėjas. Parodėme, kad jie gali atrinkti imtis iš kokio nors paprasto, lengvai atrenkamo skirstinio, pavyzdžiui, tolygiojo ar normaliojo, ir paversti jas imtimis, kurios atitinka tam tikro duomenų rinkinio skirstinį. Ir nors dvimačio Gauso skirstinio pritaikymo pavyzdys atskleidė esmę, jis nėra ypač įdomus.

Šiame skyrelyje parodysime, kaip GAN galima naudoti fotorealistiškiems vaizdams generuoti. Savo modelius grįsime giliaisiais konvoliuciniais GAN (DCGAN), pristatytais (Radford ir kt., 2015). Pasiskolinsime konvoliucinę architektūrą, kuri pasirodė tokia sėkminga diskriminatyviosiose kompiuterinės regos problemose, ir parodysime, kaip per GAN ją galima panaudoti fotorealistiškiems vaizdams generuoti.

from mxnet import gluon, init, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l

npx.set_np()
from d2l import torch as d2l
import torch
import torchvision
from torch import nn
import warnings
from d2l import tensorflow as d2l
import tensorflow as tf

20.2.1 Pokemon duomenų rinkinys

Naudosime Pokemon sprite'ų rinkinį, gautą iš pokemondb. Pirmiausia atsisiųskite, išskleiskite ir įkelkite šį duomenų rinkinį.

#@save
d2l.DATA_HUB['pokemon'] = (d2l.DATA_URL + 'pokemon.zip',
                           'c065c0e2593b8b161a2d7873e42418bf6a21106c')

data_dir = d2l.download_extract('pokemon')
pokemon = gluon.data.vision.datasets.ImageFolderDataset(data_dir)
#@save
d2l.DATA_HUB['pokemon'] = (d2l.DATA_URL + 'pokemon.zip',
                           'c065c0e2593b8b161a2d7873e42418bf6a21106c')

data_dir = d2l.download_extract('pokemon')
pokemon = torchvision.datasets.ImageFolder(data_dir)
#@save
d2l.DATA_HUB['pokemon'] = (d2l.DATA_URL + 'pokemon.zip',
                           'c065c0e2593b8b161a2d7873e42418bf6a21106c')

data_dir = d2l.download_extract('pokemon')
batch_size = 256
pokemon = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir, batch_size=batch_size, image_size=(64, 64))

Kiekvieną vaizdą pakeičiame į 64×6464\times 64 dydžio. Transformacija ToTensor pikselių reikšmes projektuos į [0,1][0, 1], o mūsų generatorius naudos tanh funkciją, kad gautų išvestis iš [−1,1][-1, 1]. Todėl duomenis normalizuojame su 0.50.5 vidurkiu ir 0.50.5 standartiniu nuokrypiu, kad atitiktų reikšmių intervalą.

batch_size = 256
transformer = gluon.data.vision.transforms.Compose([
    gluon.data.vision.transforms.Resize(64),
    gluon.data.vision.transforms.ToTensor(),
    gluon.data.vision.transforms.Normalize(0.5, 0.5)
])
data_iter = gluon.data.DataLoader(
    pokemon.transform_first(transformer), batch_size=batch_size,
    shuffle=True, num_workers=d2l.get_dataloader_workers())
batch_size = 256
transformer = torchvision.transforms.Compose([
    torchvision.transforms.Resize((64, 64)),
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize(0.5, 0.5)
])
pokemon.transform = transformer
data_iter = torch.utils.data.DataLoader(
    pokemon, batch_size=batch_size,
    shuffle=True, num_workers=d2l.get_dataloader_workers())
def transform_func(X):
    X = X / 255.
    X = (X - 0.5) / (0.5)
    return X

# For TF>=2.4 use `num_parallel_calls = tf.data.AUTOTUNE`
data_iter = pokemon.map(lambda x, y: (transform_func(x), y),
                        num_parallel_calls=tf.data.experimental.AUTOTUNE)
data_iter = data_iter.cache().shuffle(buffer_size=1000).prefetch(
    buffer_size=tf.data.experimental.AUTOTUNE)

Vizualizuokime pirmuosius 20 vaizdų.

d2l.set_figsize((4, 4))
for X, y in data_iter:
    imgs = X[:20,:,:,:].transpose(0, 2, 3, 1)/2+0.5
    d2l.show_images(imgs, num_rows=4, num_cols=5)
    break
warnings.filterwarnings('ignore')
d2l.set_figsize((4, 4))
for X, y in data_iter:
    imgs = X[:20,:,:,:].permute(0, 2, 3, 1)/2+0.5
    d2l.show_images(imgs, num_rows=4, num_cols=5)
    break
d2l.set_figsize(figsize=(4, 4))
for X, y in data_iter.take(1):
    imgs = X[:20, :, :, :] / 2 + 0.5
    d2l.show_images(imgs, num_rows=4, num_cols=5)

20.2.2 Generatorius

Generatorius turi atvaizduoti triukšmo kintamąjį z∈Rd\mathbf z\in\mathbb R^d, ilgio dd vektorių, į RGB vaizdą, kurio plotis ir aukštis yra 64×6464\times 64. 14.11 skyrelis pristatėme pilnai konvoliucinį tinklą, kuris įvesties dydžiui padidinti naudoja transponuotosios konvoliucijos sluoksnį (žr. 14.10 skyrelis). Pagrindinį generatoriaus bloką sudaro transponuotosios konvoliucijos sluoksnis, po kurio eina paketinė normalizacija ir ReLU aktyvacija.

class G_block(nn.Block):
    def __init__(self, channels, kernel_size=4,
                 strides=2, padding=1, **kwargs):
        super(G_block, self).__init__(**kwargs)
        self.conv2d_trans = nn.Conv2DTranspose(
            channels, kernel_size, strides, padding, use_bias=False)
        self.batch_norm = nn.BatchNorm()
        self.activation = nn.Activation('relu')

    def forward(self, X):
        return self.activation(self.batch_norm(self.conv2d_trans(X)))
class G_block(nn.Module):
    def __init__(self, out_channels, in_channels=3, kernel_size=4, strides=2,
                 padding=1, **kwargs):
        super(G_block, self).__init__(**kwargs)
        self.conv2d_trans = nn.ConvTranspose2d(in_channels, out_channels,
                                kernel_size, strides, padding, bias=False)
        self.batch_norm = nn.BatchNorm2d(out_channels)
        self.activation = nn.ReLU()

    def forward(self, X):
        return self.activation(self.batch_norm(self.conv2d_trans(X)))
class G_block(tf.keras.layers.Layer):
    def __init__(self, out_channels, kernel_size=4, strides=2, padding="same",
                 **kwargs):
        super().__init__(**kwargs)
        self.conv2d_trans = tf.keras.layers.Conv2DTranspose(
            out_channels, kernel_size, strides, padding, use_bias=False)
        self.batch_norm = tf.keras.layers.BatchNormalization()
        self.activation = tf.keras.layers.ReLU()

    def call(self, X):
        return self.activation(self.batch_norm(self.conv2d_trans(X)))

Pagal numatytuosius nustatymus transponuotosios konvoliucijos sluoksnis naudoja kh=kw=4k_h = k_w = 4 branduolį, sh=sw=2s_h = s_w = 2 žingsnius ir ph=pw=1p_h = p_w = 1 papildymą. Esant įvesties formai nh′×nw′=16×16n_h^{'} \times n_w^{'} = 16 \times 16, generatoriaus blokas padvigubins įvesties plotį ir aukštį.

nh′×nw′=[(nhkh−(nh−1)(kh−sh)−2ph]×[(nwkw−(nw−1)(kw−sw)−2pw]=[(kh+sh(nh−1)−2ph]×[(kw+sw(nw−1)−2pw]=[(4+2×(16−1)−2×1]×[(4+2×(16−1)−2×1]=32×32.\begin{aligned} n_h^{'} \times n_w^{'} &= [(n_h k_h - (n_h-1)(k_h-s_h)- 2p_h] \times [(n_w k_w - (n_w-1)(k_w-s_w)- 2p_w]\\ &= [(k_h + s_h (n_h-1)- 2p_h] \times [(k_w + s_w (n_w-1)- 2p_w]\\ &= [(4 + 2 \times (16-1)- 2 \times 1] \times [(4 + 2 \times (16-1)- 2 \times 1]\\ &= 32 \times 32 .\\ \end{aligned}
x = np.zeros((2, 3, 16, 16))
g_blk = G_block(20)
g_blk.initialize()
g_blk(x).shape
x = torch.zeros((2, 3, 16, 16))
g_blk = G_block(20)
g_blk(x).shape
x = tf.zeros((2, 16, 16, 3))  # Channel last convention
g_blk = G_block(20)
g_blk(x).shape

Jei transponuotosios konvoliucijos sluoksnį pakeisime į 4×44\times 4 branduolį, 1×11\times 1 žingsnius ir nulinį papildymą, esant 1×11 \times 1 dydžio įvesčiai, išvesties plotis ir aukštis padidės po 3.

x = np.zeros((2, 3, 1, 1))
g_blk = G_block(20, strides=1, padding=0)
g_blk.initialize()
g_blk(x).shape
x = torch.zeros((2, 3, 1, 1))
g_blk = G_block(20, strides=1, padding=0)
g_blk(x).shape
x = tf.zeros((2, 1, 1, 3))
# `padding="valid"` corresponds to no padding
g_blk = G_block(20, strides=1, padding="valid")
g_blk(x).shape

Generatorių sudaro keturi pagrindiniai blokai, didinantys įvesties plotį ir aukštį nuo 1 iki 32. Tuo pačiu metu jis pirmiausia latentinį kintamąjį projektuoja į 64×864\times 8 kanalų, o paskui kiekvieną kartą perpus sumažina kanalų skaičių. Galiausiai išvesčiai gauti naudojamas transponuotosios konvoliucijos sluoksnis. Jis dar kartą padvigubina plotį ir aukštį, kad atitiktų norimą 64×6464\times 64 formą, ir sumažina kanalų dydį iki 33. Tanh aktyvacijos funkcija pritaikoma išvesties reikšmėms projektuoti į (−1,1)(-1, 1) intervalą.

n_G = 64
net_G = nn.Sequential()
net_G.add(G_block(n_G*8, strides=1, padding=0),  # Output: (64 * 8, 4, 4)
          G_block(n_G*4),  # Output: (64 * 4, 8, 8)
          G_block(n_G*2),  # Output: (64 * 2, 16, 16)
          G_block(n_G),    # Output: (64, 32, 32)
          nn.Conv2DTranspose(
              3, kernel_size=4, strides=2, padding=1, use_bias=False,
              activation='tanh'))  # Output: (3, 64, 64)
n_G = 64
net_G = nn.Sequential(
    G_block(in_channels=100, out_channels=n_G*8,
            strides=1, padding=0),                  # Output: (64 * 8, 4, 4)
    G_block(in_channels=n_G*8, out_channels=n_G*4), # Output: (64 * 4, 8, 8)
    G_block(in_channels=n_G*4, out_channels=n_G*2), # Output: (64 * 2, 16, 16)
    G_block(in_channels=n_G*2, out_channels=n_G),   # Output: (64, 32, 32)
    nn.ConvTranspose2d(in_channels=n_G, out_channels=3,
                       kernel_size=4, stride=2, padding=1, bias=False),
    nn.Tanh())  # Output: (3, 64, 64)
n_G = 64
net_G = tf.keras.Sequential([
    # Output: (4, 4, 64 * 8)
    G_block(out_channels=n_G*8, strides=1, padding="valid"),
    G_block(out_channels=n_G*4), # Output: (8, 8, 64 * 4)
    G_block(out_channels=n_G*2), # Output: (16, 16, 64 * 2)
    G_block(out_channels=n_G), # Output: (32, 32, 64)
    # Output: (64, 64, 3)
    tf.keras.layers.Conv2DTranspose(
        3, kernel_size=4, strides=2, padding="same", use_bias=False,
        activation="tanh")
])

Sugeneruokite 100 matmenų latentinį kintamąjį, kad patikrintumėte generatoriaus išvesties formą.

x = np.zeros((1, 100, 1, 1))
net_G.initialize()
net_G(x).shape
x = torch.zeros((1, 100, 1, 1))
net_G(x).shape
x = tf.zeros((1, 1, 1, 100))
net_G(x).shape

20.2.3 Diskriminatorius

Diskriminatorius yra įprastas konvoliucinis tinklas, išskyrus tai, kad aktyvacijos funkcijai jis naudoja nutekėjusį ReLU (angl. leaky ReLU). Esant α∈[0,1]\alpha \in[0, 1], jos apibrėžimas yra

leaky ReLU(x)={xif x>0αxotherwise.\textrm{leaky ReLU}(x) = \begin{cases}x & \textrm{if}\ x > 0\\ \alpha x &\textrm{otherwise}\end{cases}.

Kaip matome, tai įprasta ReLU, jei α=0\alpha=0, ir tapatumo funkcija, jei α=1\alpha=1. Kai α∈(0,1)\alpha \in (0, 1), nutekėjęs ReLU yra netiesinė funkcija, kuri neigiamai įvesčiai grąžina nenulinę išvestį. Ja siekiama ištaisyti „mirštančios ReLU“ problemą, kai neuronas gali visada išvesti neigiamą reikšmę ir todėl negali daryti pažangos, nes ReLU gradientas yra 0.

alphas = [0, .2, .4, .6, .8, 1]
x = d2l.arange(-2, 1, 0.1)
Y = [d2l.numpy(nn.LeakyReLU(alpha)(x)) for alpha in alphas]
d2l.plot(d2l.numpy(x), Y, 'x', 'y', alphas)
alphas = [0, .2, .4, .6, .8, 1]
x = tf.range(-2, 1, 0.1)
Y = [tf.keras.layers.LeakyReLU(alpha)(x).numpy() for alpha in alphas]
d2l.plot(x.numpy(), Y, 'x', 'y', alphas)

Pagrindinį diskriminatoriaus bloką sudaro konvoliucinis sluoksnis, po kurio eina paketinės normalizacijos sluoksnis ir nutekėjusio ReLU aktyvacija. Konvoliucinio sluoksnio hiperparametrai panašūs į transponuotosios konvoliucijos sluoksnio generatoriaus bloke.

class D_block(nn.Block):
    def __init__(self, channels, kernel_size=4, strides=2,
                 padding=1, alpha=0.2, **kwargs):
        super(D_block, self).__init__(**kwargs)
        self.conv2d = nn.Conv2D(
            channels, kernel_size, strides, padding, use_bias=False)
        self.batch_norm = nn.BatchNorm()
        self.activation = nn.LeakyReLU(alpha)

    def forward(self, X):
        return self.activation(self.batch_norm(self.conv2d(X)))
class D_block(nn.Module):
    def __init__(self, out_channels, in_channels=3, kernel_size=4, strides=2,
                padding=1, alpha=0.2, **kwargs):
        super(D_block, self).__init__(**kwargs)
        self.conv2d = nn.Conv2d(in_channels, out_channels, kernel_size,
                                strides, padding, bias=False)
        self.batch_norm = nn.BatchNorm2d(out_channels)
        self.activation = nn.LeakyReLU(alpha, inplace=True)

    def forward(self, X):
        return self.activation(self.batch_norm(self.conv2d(X)))
class D_block(tf.keras.layers.Layer):
    def __init__(self, out_channels, kernel_size=4, strides=2, padding="same",
                 alpha=0.2, **kwargs):
        super().__init__(**kwargs)
        self.conv2d = tf.keras.layers.Conv2D(out_channels, kernel_size,
                                             strides, padding, use_bias=False)
        self.batch_norm = tf.keras.layers.BatchNormalization()
        self.activation = tf.keras.layers.LeakyReLU(alpha)

    def call(self, X):
        return self.activation(self.batch_norm(self.conv2d(X)))

Pagrindinis blokas su numatytaisiais nustatymais perpus sumažins įvesties plotį ir aukštį, kaip parodėme 7.3 skyrelis. Pavyzdžiui, esant įvesties formai nh=nw=16n_h = n_w = 16, branduolio formai kh=kw=4k_h = k_w = 4, žingsnio formai sh=sw=2s_h = s_w = 2 ir papildymo formai ph=pw=1p_h = p_w = 1, išvesties forma bus:

nh′×nw′=⌊(nh−kh+2ph+sh)/sh⌋×⌊(nw−kw+2pw+sw)/sw⌋=⌊(16−4+2×1+2)/2⌋×⌊(16−4+2×1+2)/2⌋=8×8.\begin{aligned} n_h^{'} \times n_w^{'} &= \lfloor(n_h-k_h+2p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+2p_w+s_w)/s_w\rfloor\\ &= \lfloor(16-4+2\times 1+2)/2\rfloor \times \lfloor(16-4+2\times 1+2)/2\rfloor\\ &= 8 \times 8 .\\ \end{aligned}
x = np.zeros((2, 3, 16, 16))
d_blk = D_block(20)
d_blk.initialize()
d_blk(x).shape
x = torch.zeros((2, 3, 16, 16))
d_blk = D_block(20)
d_blk(x).shape
x = tf.zeros((2, 16, 16, 3))
d_blk = D_block(20)
d_blk(x).shape

Diskriminatorius yra generatoriaus veidrodinis atvaizdas.

n_D = 64
net_D = nn.Sequential()
net_D.add(D_block(n_D),   # Output: (64, 32, 32)
          D_block(n_D*2),  # Output: (64 * 2, 16, 16)
          D_block(n_D*4),  # Output: (64 * 4, 8, 8)
          D_block(n_D*8),  # Output: (64 * 8, 4, 4)
          nn.Conv2D(1, kernel_size=4, use_bias=False))  # Output: (1, 1, 1)
n_D = 64
net_D = nn.Sequential(
    D_block(n_D),  # Output: (64, 32, 32)
    D_block(in_channels=n_D, out_channels=n_D*2),  # Output: (64 * 2, 16, 16)
    D_block(in_channels=n_D*2, out_channels=n_D*4),  # Output: (64 * 4, 8, 8)
    D_block(in_channels=n_D*4, out_channels=n_D*8),  # Output: (64 * 8, 4, 4)
    nn.Conv2d(in_channels=n_D*8, out_channels=1,
              kernel_size=4, bias=False))  # Output: (1, 1, 1)
n_D = 64
net_D = tf.keras.Sequential([
    D_block(n_D), # Output: (32, 32, 64)
    D_block(out_channels=n_D*2), # Output: (16, 16, 64 * 2)
    D_block(out_channels=n_D*4), # Output: (8, 8, 64 * 4)
    D_block(out_channels=n_D*8), # Outupt: (4, 4, 64 * 64)
    # Output: (1, 1, 1)
    tf.keras.layers.Conv2D(1, kernel_size=4, use_bias=False)
])

Paskutiniam sluoksniui naudojamas konvoliucinis sluoksnis su išvesties kanalu 11, kad gautume vieną prognozės reikšmę.

x = np.zeros((1, 3, 64, 64))
net_D.initialize()
net_D(x).shape
x = torch.zeros((1, 3, 64, 64))
net_D(x).shape
x = tf.zeros((1, 64, 64, 3))
net_D(x).shape

20.2.4 Apmokymas

Palyginti su pagrindiniu GAN iš 20.1 skyrelis, tiek generatoriui, tiek diskriminatoriui naudojame tą patį mokymosi greitį, nes jie panašūs vienas į kitą. Be to, Adam (12.10 skyrelis) β1\beta_1 keičiame iš 0.90.9 į 0.50.5. Tai sumažina momento – eksponentiškai pasvertojo praeities gradientų slenkamojo vidurkio – glotnumą, kad būtų atsižvelgta į sparčiai kintančius gradientus, nes generatorius ir diskriminatorius kovoja tarpusavyje. Be to, atsitiktinai sugeneruotas triukšmas Z yra 4 matmenų tenzorius, o skaičiavimams pagreitinti naudojame GPU.

def train(net_D, net_G, data_iter, num_epochs, lr, latent_dim,
          device=d2l.try_gpu()):
    loss = gluon.loss.SigmoidBCELoss()
    net_D.initialize(init=init.Normal(0.02), force_reinit=True, ctx=device)
    net_G.initialize(init=init.Normal(0.02), force_reinit=True, ctx=device)
    trainer_hp = {'learning_rate': lr, 'beta1': 0.5}
    trainer_D = gluon.Trainer(net_D.collect_params(), 'adam', trainer_hp)
    trainer_G = gluon.Trainer(net_G.collect_params(), 'adam', trainer_hp)
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[1, num_epochs], nrows=2, figsize=(5, 5),
                            legend=['discriminator', 'generator'])
    animator.fig.subplots_adjust(hspace=0.3)
    for epoch in range(1, num_epochs + 1):
        # Train one epoch
        timer = d2l.Timer()
        metric = d2l.Accumulator(3)  # loss_D, loss_G, num_examples
        for X, _ in data_iter:
            batch_size = X.shape[0]
            Z = np.random.normal(0, 1, size=(batch_size, latent_dim, 1, 1))
            X, Z = X.as_in_ctx(device), Z.as_in_ctx(device),
            metric.add(d2l.update_D(X, Z, net_D, net_G, loss, trainer_D),
                       d2l.update_G(Z, net_D, net_G, loss, trainer_G),
                       batch_size)
        # Show generated examples
        Z = np.random.normal(0, 1, size=(21, latent_dim, 1, 1), ctx=device)
        # Normalize the synthetic data to N(0, 1)
        fake_x = net_G(Z).transpose(0, 2, 3, 1) / 2 + 0.5
        imgs = np.concatenate(
            [np.concatenate([fake_x[i * 7 + j] for j in range(7)], axis=1)
             for i in range(len(fake_x)//7)], axis=0)
        animator.axes[1].cla()
        animator.axes[1].imshow(imgs.asnumpy())
        # Show the losses
        loss_D, loss_G = metric[0] / metric[2], metric[1] / metric[2]
        animator.add(epoch, (loss_D, loss_G))
    print(f'loss_D {loss_D:.3f}, loss_G {loss_G:.3f}, '
          f'{metric[2] / timer.stop():.1f} examples/sec on {str(device)}')
def train(net_D, net_G, data_iter, num_epochs, lr, latent_dim,
          device=d2l.try_gpu()):
    loss = nn.BCEWithLogitsLoss(reduction='sum')
    for w in net_D.parameters():
        nn.init.normal_(w, 0, 0.02)
    for w in net_G.parameters():
        nn.init.normal_(w, 0, 0.02)
    net_D, net_G = net_D.to(device), net_G.to(device)
    trainer_hp = {'lr': lr, 'betas': [0.5,0.999]}
    trainer_D = torch.optim.Adam(net_D.parameters(), **trainer_hp)
    trainer_G = torch.optim.Adam(net_G.parameters(), **trainer_hp)
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[1, num_epochs], nrows=2, figsize=(5, 5),
                            legend=['discriminator', 'generator'])
    animator.fig.subplots_adjust(hspace=0.3)
    for epoch in range(1, num_epochs + 1):
        # Train one epoch
        timer = d2l.Timer()
        metric = d2l.Accumulator(3)  # loss_D, loss_G, num_examples
        for X, _ in data_iter:
            batch_size = X.shape[0]
            Z = torch.normal(0, 1, size=(batch_size, latent_dim, 1, 1))
            X, Z = X.to(device), Z.to(device)
            metric.add(d2l.update_D(X, Z, net_D, net_G, loss, trainer_D),
                       d2l.update_G(Z, net_D, net_G, loss, trainer_G),
                       batch_size)
        # Show generated examples
        Z = torch.normal(0, 1, size=(21, latent_dim, 1, 1), device=device)
        # Normalize the synthetic data to N(0, 1)
        fake_x = net_G(Z).permute(0, 2, 3, 1) / 2 + 0.5
        imgs = torch.cat(
            [torch.cat([
                fake_x[i * 7 + j].cpu().detach() for j in range(7)], dim=1)
             for i in range(len(fake_x)//7)], dim=0)
        animator.axes[1].cla()
        animator.axes[1].imshow(imgs)
        # Show the losses
        loss_D, loss_G = metric[0] / metric[2], metric[1] / metric[2]
        animator.add(epoch, (loss_D, loss_G))
    print(f'loss_D {loss_D:.3f}, loss_G {loss_G:.3f}, '
          f'{metric[2] / timer.stop():.1f} examples/sec on {str(device)}')
def train(net_D, net_G, data_iter, num_epochs, lr, latent_dim,
          device=d2l.try_gpu()):
    loss = tf.keras.losses.BinaryCrossentropy(
        from_logits=True, reduction=tf.keras.losses.Reduction.SUM)

    for w in net_D.trainable_variables:
        w.assign(tf.random.normal(mean=0, stddev=0.02, shape=w.shape))
    for w in net_G.trainable_variables:
        w.assign(tf.random.normal(mean=0, stddev=0.02, shape=w.shape))

    optimizer_hp = {"lr": lr, "beta_1": 0.5, "beta_2": 0.999}
    optimizer_D = tf.keras.optimizers.Adam(**optimizer_hp)
    optimizer_G = tf.keras.optimizers.Adam(**optimizer_hp)

    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[1, num_epochs], nrows=2, figsize=(5, 5),
                            legend=['discriminator', 'generator'])
    animator.fig.subplots_adjust(hspace=0.3)

    for epoch in range(1, num_epochs + 1):
        # Train one epoch
        timer = d2l.Timer()
        metric = d2l.Accumulator(3) # loss_D, loss_G, num_examples
        for X, _ in data_iter:
            batch_size = X.shape[0]
            Z = tf.random.normal(mean=0, stddev=1,
                                 shape=(batch_size, 1, 1, latent_dim))
            metric.add(d2l.update_D(X, Z, net_D, net_G, loss, optimizer_D),
                       d2l.update_G(Z, net_D, net_G, loss, optimizer_G),
                       batch_size)

        # Show generated examples
        Z = tf.random.normal(mean=0, stddev=1, shape=(21, 1, 1, latent_dim))
        # Normalize the synthetic data to N(0, 1)
        fake_x = net_G(Z) / 2 + 0.5
        imgs = tf.concat([tf.concat([fake_x[i * 7 + j] for j in range(7)],
                                    axis=1)
                          for i in range(len(fake_x) // 7)], axis=0)
        animator.axes[1].cla()
        animator.axes[1].imshow(imgs)
        # Show the losses
        loss_D, loss_G = metric[0] / metric[2], metric[1] / metric[2]
        animator.add(epoch, (loss_D, loss_G))
    print(f'loss_D {loss_D:.3f}, loss_G {loss_G:.3f}, '
          f'{metric[2] / timer.stop():.1f} examples/sec on {str(device._device_name)}')

Modelį apmokome su nedideliu epochų skaičiumi tik demonstracijai. Siekiant geresnio našumo, kintamajam num_epochs galima nustatyti didesnę reikšmę.

latent_dim, lr, num_epochs = 100, 0.005, 20
train(net_D, net_G, data_iter, num_epochs, lr, latent_dim)
latent_dim, lr, num_epochs = 100, 0.0005, 40
train(net_D, net_G, data_iter, num_epochs, lr, latent_dim)

20.2.5 Santrauka

  • DCGAN architektūroje yra keturi konvoliuciniai diskriminatoriaus sluoksniai ir keturi „trupmeninio žingsnio“ konvoliuciniai generatoriaus sluoksniai.
  • Diskriminatorius yra 4 sluoksnių žingsninių konvoliucijų tinklas su paketine normalizacija (išskyrus jo įvesties sluoksnį) ir nutekėjusio ReLU aktyvacijomis.
  • Nutekėjęs ReLU yra netiesinė funkcija, kuri neigiamai įvesčiai grąžina nenulinę išvestį. Ja siekiama ištaisyti „mirštančios ReLU“ problemą ir padėti gradientams lengviau tekėti per architektūrą.

20.2.6 Pratimai

  1. Kas atsitiks, jei vietoj nutekėjusio ReLU naudosime standartinę ReLU aktyvaciją?
  2. Pritaikykite DCGAN Fashion-MNIST duomenims ir pažiūrėkite, kurios kategorijos veikia gerai, o kurios – ne.

Diskusijos

Diskusijos