.. _sec_dcgan:
Redes Adversariais Gerativas Convolucionais Profundas
=====================================================
Em :numref:`sec_basic_gan`, apresentamos as idéias básicas por trás de
como funcionam os GANs. Mostramos que eles podem extrair amostras de
alguma distribuição simples e fácil de amostrar, como uma distribuição
uniforme ou normal, e transformá-los em amostras que parecem
corresponder à distribuição de algum conjunto de dados. E embora nosso
exemplo de correspondência de uma distribuição gaussiana 2D tenha
chegado ao ponto, não é especialmente empolgante.
Nesta seção, demonstraremos como você pode usar GANs para gerar imagens
fotorrealísticas. Estaremos baseando nossos modelos nos GANs
convolucionais profundos (DCGAN) introduzidos em
:cite:`Radford.Metz.Chintala.2015`. Vamos tomar emprestada a
arquitetura convolucional que se mostrou tão bem-sucedida para problemas
discriminativos de visão de computador e mostrar como, por meio de GANs,
eles podem ser aproveitados para gerar imagens fotorrealistas.
.. raw:: html
`__. Primeiro baixe, extraia e
carregue este conjunto de dados.
.. raw:: html
.. raw:: html
.. code:: python
#@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)
.. parsed-literal::
:class: output
Downloading ../data/pokemon.zip from http://d2l-data.s3-accelerate.amazonaws.com/pokemon.zip...
.. raw:: html
.. raw:: html
.. code:: python
#@save
d2l.DATA_HUB['pokemon'] = (d2l.DATA_URL + 'pokemon.zip',
'c065c0e2593b8b161a2d7873e42418bf6a21106c')
data_dir = d2l.download_extract('pokemon')
pokemon = torchvision.datasets.ImageFolder(data_dir)
.. parsed-literal::
:class: output
Downloading ../data/pokemon.zip from http://d2l-data.s3-accelerate.amazonaws.com/pokemon.zip...
.. raw:: html
.. raw:: html
Redimensionamos cada imagem em :math:`64\times 64`. A transformação
``ToTensor`` projetará o valor do pixel em :math:`[0, 1]`, enquanto
nosso gerador usará a função tanh para obter saídas em :math:`[-1, 1]`.
Portanto, normalizamos os dados com :math:`0,5` de média e :math:`0,5`
de desvio padrão para corresponder ao intervalo de valores.
.. raw:: html
.. raw:: html
.. code:: python
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())
.. raw:: html
.. raw:: html
.. code:: python
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())
.. raw:: html
.. raw:: html
Vamos visualizar as primeiras 20 imagens.
.. raw:: html
.. raw:: html
.. code:: python
d2l.set_figsize((4, 4))
for X, y in data_iter:
imgs = X[0:20,:,:,:].transpose(0, 2, 3, 1)/2+0.5
d2l.show_images(imgs, num_rows=4, num_cols=5)
break
.. figure:: output_dcgan_2541de_30_0.svg
.. raw:: html
.. raw:: html
.. code:: python
warnings.filterwarnings('ignore')
d2l.set_figsize((4, 4))
for X, y in data_iter:
imgs = X[0:20,:,:,:].permute(0, 2, 3, 1)/2+0.5
d2l.show_images(imgs, num_rows=4, num_cols=5)
break
.. figure:: output_dcgan_2541de_33_0.svg
.. raw:: html
.. raw:: html
O Gerador
---------
O gerador precisa mapear a variável de
ruído\ :math:`\mathbf z\in\mathbb R^d`, um vetor comprimento :math:`d`,
para uma imagem RGB com largura e altura :math:`64\times 64`. Em
:numref:`sec_fcn` introduzimos a rede totalmente convolucional que usa
a camada de convolução transposta (consulte
:numref:`sec_transposed_conv`) para aumentar o tamanho da entrada. O
bloco básico do gerador contém uma camada de convolução transposta
seguida pela normalização do lote e ativação ReLU.
.. raw:: html
.. raw:: html
.. code:: python
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)))
.. raw:: html
.. raw:: html
.. code:: python
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)))
.. raw:: html
.. raw:: html
Por padrão, a camada de convolução transposta usa um kernel
:math:`k_h = k_w = 4`, passos :math:`s_h = s_w = 2` e um preenchimento
:math:`p_h = p_w = 1`. Com uma forma de entrada de
:math:`n_h^{'} \times n_w^{'} = 16 \times 16`, o bloco gerador dobrará a
largura e a altura da entrada.
.. math::
\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}
.. raw:: html
.. raw:: html
.. code:: python
x = np.zeros((2, 3, 16, 16))
g_blk = G_block(20)
g_blk.initialize()
g_blk(x).shape
.. parsed-literal::
:class: output
(2, 20, 32, 32)
.. raw:: html
.. raw:: html
.. code:: python
x = torch.zeros((2, 3, 16, 16))
g_blk = G_block(20)
g_blk(x).shape
.. parsed-literal::
:class: output
torch.Size([2, 20, 32, 32])
.. raw:: html
.. raw:: html
Se alterar a camada de convolução transposta para um kernel
:math:`4\times 4` kernel, passos :math:`1\times 1` e preenchimento zero.
Com um tamanho de entrada de :math:`1 \times 1`, a saída terá sua
largura e altura aumentadas em 3, respectivamente.
.. raw:: html
.. raw:: html
.. code:: python
x = np.zeros((2, 3, 1, 1))
g_blk = G_block(20, strides=1, padding=0)
g_blk.initialize()
g_blk(x).shape
.. parsed-literal::
:class: output
(2, 20, 4, 4)
.. raw:: html
.. raw:: html
.. code:: python
x = torch.zeros((2, 3, 1, 1))
g_blk = G_block(20, strides=1, padding=0)
g_blk(x).shape
.. parsed-literal::
:class: output
torch.Size([2, 20, 4, 4])
.. raw:: html
.. raw:: html
O gerador consiste em quatro blocos básicos que aumentam a largura e a
altura da entrada de 1 para 32. Ao mesmo tempo, ele primeiro projeta a
variável latente em canais :math:`64\times 8` e, em seguida, divide os
canais a cada vez. Por fim, uma camada de convolução transposta é usada
para gerar a saída. Ele ainda dobra a largura e a altura para
corresponder à forma desejada de :math:`64\times 64` e reduz o tamanho
do canal para :math:`3`. A função de ativação tanh é aplicada aos
valores de saída do projeto na faixa :math:`(-1, 1)`.
.. raw:: html
.. raw:: html
.. code:: python
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)
.. raw:: html
.. raw:: html
.. code:: python
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)
.. raw:: html
.. raw:: html
Gere uma variável latente de 100 dimensões para verificar a forma de
saída do gerador.
.. raw:: html
.. raw:: html
.. code:: python
x = np.zeros((1, 100, 1, 1))
net_G.initialize()
net_G(x).shape
.. parsed-literal::
:class: output
(1, 3, 64, 64)
.. raw:: html
.. raw:: html
.. code:: python
x = torch.zeros((1, 100, 1, 1))
net_G(x).shape
.. parsed-literal::
:class: output
torch.Size([1, 3, 64, 64])
.. raw:: html
.. raw:: html
Discriminador
-------------
O discriminador é uma rede de rede convolucional normal, exceto que usa
um ReLU com vazamento como sua função de ativação. Dado
:math:`\alpha \in[0, 1]`, sua definição é
.. math:: \textrm{leaky ReLU}(x) = \begin{cases}x & \text{if}\ x > 0\\ \alpha x &\text{otherwise}\end{cases}.
Como pode ser visto, é um ReLU normal se :math:`\alpha=0`, e uma função
de identidade se :math:`\alpha=1`. Para :math:`\alpha \in (0, 1)`, o
ReLU com vazamento é uma função não linear que fornece uma saída
diferente de zero para uma entrada negativa. Seu objetivo é corrigir o
problema de “ReLU agonizante” de que um neurônio pode sempre produzir um
valor negativo e, portanto, não pode fazer nenhum progresso, pois o
gradiente de ReLU é 0.
.. raw:: html
.. raw:: html
.. code:: python
alphas = [0, .2, .4, .6, .8, 1]
x = np.arange(-2, 1, 0.1)
Y = [nn.LeakyReLU(alpha)(x).asnumpy() for alpha in alphas]
d2l.plot(x.asnumpy(), Y, 'x', 'y', alphas)
.. figure:: output_dcgan_2541de_84_0.svg
.. raw:: html
.. raw:: html
.. code:: python
alphas = [0, .2, .4, .6, .8, 1]
x = torch.arange(-2, 1, 0.1)
Y = [nn.LeakyReLU(alpha)(x).detach().numpy() for alpha in alphas]
d2l.plot(x.detach().numpy(), Y, 'x', 'y', alphas)
.. figure:: output_dcgan_2541de_87_0.svg
.. raw:: html
.. raw:: html
O bloco básico do discriminador é uma camada de convolução seguida por
uma camada de normalização em lote e uma ativação ReLU com vazamento. Os
hiperparâmetros da camada de convolução são semelhantes à camada de
convolução transposta no bloco gerador.
.. raw:: html
.. raw:: html
.. code:: python
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)))
.. raw:: html
.. raw:: html
.. code:: python
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)))
.. raw:: html
.. raw:: html
Um bloco básico com configurações padrão reduzirá pela metade a largura
e a altura das entradas, como demonstramos em :numref:`sec_padding`.
Por exemplo, dada uma forma de entrada :math:`n_h = n_w = 16`, com uma
forma de kernel :math:`k_h = k_w = 4`, uma forma de passo
:math:`s_h = s_w = 2` e uma forma de preenchimento
:math:`p_h = p_w = 1`, a forma de saída será:
.. math::
\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}
.. raw:: html
.. raw:: html
.. code:: python
x = np.zeros((2, 3, 16, 16))
d_blk = D_block(20)
d_blk.initialize()
d_blk(x).shape
.. parsed-literal::
:class: output
(2, 20, 8, 8)
.. raw:: html
.. raw:: html
.. code:: python
x = torch.zeros((2, 3, 16, 16))
d_blk = D_block(20)
d_blk(x).shape
.. parsed-literal::
:class: output
torch.Size([2, 20, 8, 8])
.. raw:: html
.. raw:: html
O discriminador é um espelho do gerador.
.. raw:: html
.. raw:: html
.. code:: python
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)
.. raw:: html
.. raw:: html
.. code:: python
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)
.. raw:: html
.. raw:: html
Ele usa uma camada de convolução com canal de saída :math:`1` como a
última camada para obter um único valor de predição.
.. raw:: html
.. raw:: html
.. code:: python
x = np.zeros((1, 3, 64, 64))
net_D.initialize()
net_D(x).shape
.. parsed-literal::
:class: output
(1, 1, 1, 1)
.. raw:: html
.. raw:: html
.. code:: python
x = torch.zeros((1, 3, 64, 64))
net_D(x).shape
.. parsed-literal::
:class: output
torch.Size([1, 1, 1, 1])
.. raw:: html
.. raw:: html
Treinamento
-----------
Comparado ao GAN básico em :numref:`sec_basic_gan`, usamos a mesma
taxa de aprendizado para o gerador e o discriminador, uma vez que são
semelhantes entre si. Além disso, alteramos :math:`\beta_1` em Adam
(:numref:`sec_adam`) de :math:`0,9` para :math:`0,5`. Ele diminui a
suavidade do momentum, a média móvel exponencialmente ponderada dos
gradientes anteriores, para cuidar dos gradientes que mudam rapidamente
porque o gerador e o discriminador lutam um com o outro. Além disso, o
ruído gerado aleatoriamente ``Z``, é um tensor 4-D e estamos usando GPU
para acelerar o cálculo.
.. raw:: html
.. raw:: html
.. code:: python
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)}')
.. raw:: html
.. raw:: html
.. code:: python
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)}')
.. raw:: html
.. raw:: html
Treinamos o modelo com um pequeno número de épocas apenas para
demonstração. Para melhor desempenho, a variável ``num_epochs`` pode ser
definida para um número maior.
.. raw:: html
.. raw:: html
.. code:: python
latent_dim, lr, num_epochs = 100, 0.005, 20
train(net_D, net_G, data_iter, num_epochs, lr, latent_dim)
.. parsed-literal::
:class: output
loss_D 0.066, loss_G 7.035, 2563.8 examples/sec on gpu(0)
.. figure:: output_dcgan_2541de_138_1.svg
.. raw:: html
.. raw:: html
.. code:: python
latent_dim, lr, num_epochs = 100, 0.005, 20
train(net_D, net_G, data_iter, num_epochs, lr, latent_dim)
.. parsed-literal::
:class: output
loss_D 0.281, loss_G 6.224, 1057.2 examples/sec on cuda:0
.. figure:: output_dcgan_2541de_141_1.svg
.. raw:: html
.. raw:: html
Resumo
------
- A arquitetura DCGAN tem quatro camadas convolucionais para o
Discriminador e quatro camadas convolucionais “com passos
fracionários” para o Gerador.
- O Discriminador é um conjunto de convoluções de 4 camadas com
normalização em lote (exceto sua camada de entrada) e ativações ReLU
com vazamento.
- ReLU com vazamento é uma função não linear que fornece uma saída
diferente de zero para uma entrada negativa. Ele tem como objetivo
corrigir o problema do “ReLU agonizante” e ajudar os gradientes a
fluírem mais facilmente pela arquitetura.
Exercícios
----------
1. O que acontecerá se usarmos a ativação ReLU padrão em vez de ReLU com
vazamento?
2. Aplique DCGAN no Fashion-MNIST e veja qual categoria funciona bem e
qual não funciona.
.. raw:: html
.. raw:: html
`Discussões `__
.. raw:: html
.. raw:: html
`Discussões `__
.. raw:: html
.. raw:: html
.. raw:: html