Phase 04 - Lesson 09
Geração de Imagens — GANs
Uma GAN são duas redes neurais em um jogo fixo. Uma desenha, a outra critica. Elas melhoram juntas até que os desenhos enganem o crítico.
Tipo: Build Linguagens: Python Pré-requisitos: Fase 4 Lição 03 (CNNs), Fase 3 Lição 06 (Otimizadores), Fase 3 Lição 07 (Regularização) Tempo: ~75 minutos
Objetivos de Aprendizagem
- Explicar o jogo minimax entre gerador e discriminador e por que o equilíbrio corresponde a p_model = p_data
- Implementar uma DCGAN em PyTorch e fazê-la gerar imagens sintéticas coerentes de 32x32 em menos de 60 linhas
- Estabilizar o treinamento de GAN com os três truques padrão: perda não saturante, normalização espectral, TTUR (regra de atualização em duas escalas de tempo)
- Ler curvas de treinamento que distinguem convergência saudável de colapso de modo, oscilação e discriminador-vence-completamente
O Problema
A classificação ensina uma rede a mapear imagens para rótulos. A geração inverte o problema: amostrar novas imagens que parecem ter vindo da mesma distribuição. Não existe uma saída "correta" contra a qual você possa comparar; existe apenas uma distribuição que você quer imitar.
As funções de perda padrão (MSE, entropia cruzada) não conseguem medir "esta amostra veio da distribuição real?". Minimizar o erro por pixel produz médias borradas, não amostras realistas. O avanço foi aprender a perda: treinar uma segunda rede cujo trabalho é distinguir real de falso e usar seu julgamento para empurrar o gerador.
As GANs (Goodfellow et al., 2014) definiram essa estrutura. Em 2018, a StyleGAN já produzia rostos de 1024x1024 indistinguíveis de fotografias. Modelos de difusão desde então assumiram o trono em qualidade e controlabilidade, mas cada truque que torna a difusão prática — escolhas de normalização, espaços latentes, perdas de características — foi entendido pela primeira vez nas GANs.
O Conceito
As duas redes
flowchart LR
Z["z ~ N(0, I)<br/>ruído"] --> G["Gerador<br/>convoluções transpostas"]
G --> FAKE["Imagem falsa"]
REAL["Imagem real"] --> D["Discriminador<br/>classificador convolucional"]
FAKE --> D
D --> OUT["P(real)"]
style G fill:#dbeafe,stroke:#2563eb
style D fill:#fef3c7,stroke:#d97706
style OUT fill:#dcfce7,stroke:#16a34a
O gerador G recebe um vetor de ruído z e produz uma imagem. O discriminador D recebe uma imagem e produz um único escalar: a probabilidade de que a imagem seja real.
O jogo
G quer que D esteja errado. D quer estar certo. Formalmente:
min_G max_D E_x[log D(x)] + E_z[log(1 - D(G(z)))]
Leia da direita para a esquerda: D está maximizando a acurácia em imagens reais (log D(real)) e falsas (log (1 - D(fake))). G está minimizando a acurácia de D nas falsas — ele quer que D(G(z)) seja alto.
Goodfellow provou que esse minimax tem um equilíbrio global onde p_G = p_data, D produz 0.5 em todos os pontos, e a divergência de Jensen-Shannon entre as distribuições gerada e real é zero. A parte difícil é chegar lá.
Perda não saturante
A forma acima é numericamente instável. No início do treinamento, D(G(z)) está próximo de zero para cada falsa, então log(1 - D(G(z))) tem gradientes que se desvanecem em relação a G. A correção: inverter a perda de G.
L_D = -E_x[log D(x)] - E_z[log(1 - D(G(z)))]
L_G = -E_z[log D(G(z))] # não saturante
Agora, quando D(G(z)) está próximo de zero, a perda de G é grande e seu gradiente é informativo. Toda GAN moderna treina com essa variante.
Regras de arquitetura da DCGAN
Radford, Metz, Chintala (2015) destilaram anos de experimentos fracassados em cinco regras que tornam o treinamento de GAN estável:
- Substitua pooling por convoluções com stride (em ambas as redes).
- Use batch norm tanto no gerador quanto no discriminador, exceto na saída de G e na entrada de D.
- Remova camadas totalmente conectadas em arquiteturas mais profundas.
- G usa ReLU em todas as camadas exceto na saída (tanh na saída para [-1, 1]).
- D usa LeakyReLU (negative_slope=0.2) em todas as camadas.
Toda GAN moderna baseada em convoluções (StyleGAN, BigGAN, GigaGAN) ainda parte dessas regras e substitui as peças uma de cada vez.
Modos de falha e suas assinaturas
flowchart LR
M1["Colapso de modo<br/>G produz um conjunto<br/>estreito de saídas"] --> S1["Perda de D baixa,<br/>perda de G oscilando,<br/>variedade de amostras cai"]
M2["Gradientes desvanecentes<br/>D vence completamente"] --> S2["Acurácia de D ~100%,<br/>perda de G enorme e estática"]
M3["Oscilação<br/>G e D ficam trocando<br/>vitórias para sempre"] --> S3["Ambas as perdas oscilam<br/>descontroladamente sem tendência de queda"]
style M1 fill:#fecaca,stroke:#dc2626
style M2 fill:#fecaca,stroke:#dc2626
style M3 fill:#fecaca,stroke:#dc2626
- Colapso de modo: G encontra uma imagem que engana D e produz apenas aquela. Correção: adicione discriminação por minibatch, normalização espectral ou condicionamento por rótulo.
- Discriminador vence: D fica forte demais rápido demais, os gradientes de G se desvanecem. Correção: um D menor, uma taxa de aprendizado de D mais baixa, ou aplicar suavização de rótulos nos rótulos reais.
- Oscilação: as duas redes trocam vitórias sem nunca se aproximar do equilíbrio. Correção: TTUR (D aprende mais rápido que G por um fator de 2-4), ou troque para a perda de Wasserstein.
Avaliação
GANs não têm verdade fundamental (ground truth), então como saber se estão funcionando?
- Inspeção de amostras — simplesmente olhe para 64 amostras ao final de cada época. Inegociável.
- FID (Fréchet Inception Distance) — distância entre as distribuições de características da Inception-v3 dos conjuntos real e gerado. Quanto menor, melhor. Padrão da comunidade.
- Inception Score — mais antigo, mais frágil; prefira FID.
- Precisão/Recall para modelos generativos — mede qualidade (precisão) e cobertura (recall) separadamente. Mais informativo que o FID sozinho.
Para uma pequena execução com dados sintéticos, a inspeção de amostras é suficiente.
Construa
Passo 1: Gerador
Um pequeno gerador DCGAN que recebe ruído de 64 dimensões e produz uma imagem 32x32.
import torch
import torch.nn as nn
class Generator(nn.Module):
def __init__(self, z_dim=64, img_channels=3, feat=64):
super().__init__()
self.net = nn.Sequential(
nn.ConvTranspose2d(z_dim, feat * 4, kernel_size=4, stride=1, padding=0, bias=False),
nn.BatchNorm2d(feat * 4),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(feat * 4, feat * 2, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(feat * 2),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(feat * 2, feat, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(feat),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(feat, img_channels, kernel_size=4, stride=2, padding=1, bias=False),
nn.Tanh(),
)
def forward(self, z):
return self.net(z.view(z.size(0), -1, 1, 1))
Quatro convoluções transpostas, cada uma com kernel_size=4, stride=2, padding=1 para que dobrem o tamanho espacial de forma limpa. Ativações de saída em [-1, 1] via tanh.
Passo 2: Discriminador
Espelho do gerador. LeakyReLU, convoluções com stride, termina com um logit escalar.
class Discriminator(nn.Module):
def __init__(self, img_channels=3, feat=64):
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(img_channels, feat, kernel_size=4, stride=2, padding=1),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(feat, feat * 2, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(feat * 2),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(feat * 2, feat * 4, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(feat * 4),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(feat * 4, 1, kernel_size=4, stride=1, padding=0),
)
def forward(self, x):
return self.net(x).view(-1)
A última convolução reduz um mapa de características 4x4 para 1x1. A saída é um único escalar por imagem; aplique sigmoid apenas durante o cálculo da perda.
Passo 3: Passo de treinamento
Alterne: atualize D uma vez, depois G uma vez, a cada batch.
import torch.nn.functional as F
def train_step(G, D, real, z, opt_g, opt_d, device):
real = real.to(device)
bs = real.size(0)
# D step
opt_d.zero_grad()
d_real = D(real)
d_fake = D(G(z).detach())
loss_d = (F.binary_cross_entropy_with_logits(d_real, torch.ones_like(d_real))
+ F.binary_cross_entropy_with_logits(d_fake, torch.zeros_like(d_fake)))
loss_d.backward()
opt_d.step()
# G step
opt_g.zero_grad()
d_fake = D(G(z))
loss_g = F.binary_cross_entropy_with_logits(d_fake, torch.ones_like(d_fake))
loss_g.backward()
opt_g.step()
return loss_d.item(), loss_g.item()
O G(z).detach() no passo de D é crítico: não queremos que gradientes fluam para dentro de G durante a atualização dele. Esquecer disso é o bug clássico de iniciante.
Passo 4: Loop completo de treinamento em formas sintéticas
from torch.utils.data import DataLoader, TensorDataset
import numpy as np
def synthetic_images(num=2000, size=32, seed=0):
rng = np.random.default_rng(seed)
imgs = np.zeros((num, 3, size, size), dtype=np.float32) - 1.0
for i in range(num):
r = rng.uniform(6, 12)
cx, cy = rng.uniform(r, size - r, size=2)
yy, xx = np.meshgrid(np.arange(size), np.arange(size), indexing="ij")
mask = (xx - cx) ** 2 + (yy - cy) ** 2 < r ** 2
color = rng.uniform(-0.5, 1.0, size=3)
for c in range(3):
imgs[i, c][mask] = color[c]
return torch.from_numpy(imgs)
device = "cuda" if torch.cuda.is_available() else "cpu"
data = synthetic_images()
loader = DataLoader(TensorDataset(data), batch_size=64, shuffle=True)
G = Generator(z_dim=64, img_channels=3, feat=32).to(device)
D = Discriminator(img_channels=3, feat=32).to(device)
opt_g = torch.optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999))
opt_d = torch.optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999))
for epoch in range(10):
for (batch,) in loader:
z = torch.randn(batch.size(0), 64, device=device)
ld, lg = train_step(G, D, batch, z, opt_g, opt_d, device)
print(f"epoch {epoch} D {ld:.3f} G {lg:.3f}")
Adam(lr=2e-4, betas=(0.5, 0.999)) é o padrão da DCGAN — o beta1 baixo impede que o termo de momento estabilize demais o jogo adversarial.
Passo 5: Amostragem
@torch.no_grad()
def sample(G, n=16, z_dim=64, device="cpu"):
G.eval()
z = torch.randn(n, z_dim, device=device)
imgs = G(z)
imgs = (imgs + 1) / 2
return imgs.clamp(0, 1)
Sempre mude para o modo eval antes de amostrar. Para a DCGAN isso importa porque as estatísticas em execução (running stats) do batch norm são usadas em vez das estatísticas do batch.
Passo 6: Normalização espectral
Um substituto direto para o BN no discriminador que garante que a rede seja 1-Lipschitz. Corrige a maioria das falhas de "D vence forte demais".
from torch.nn.utils import spectral_norm
def build_sn_discriminator(img_channels=3, feat=64):
return nn.Sequential(
spectral_norm(nn.Conv2d(img_channels, feat, 4, 2, 1)),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(nn.Conv2d(feat, feat * 2, 4, 2, 1)),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(nn.Conv2d(feat * 2, feat * 4, 4, 2, 1)),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(nn.Conv2d(feat * 4, 1, 4, 1, 0)),
)
Troque Discriminator por build_sn_discriminator() e muitas vezes você não precisará do truque do TTUR. A normalização espectral é a melhoria de robustez única mais fácil que você pode aplicar.
Use
Para geração séria, use pesos pré-treinados ou troque para difusão. Duas bibliotecas padrão:
torch_fidelitycalcula FID / IS no seu gerador sem escrever código de avaliação personalizado.pytorch-gan-zoo(legado) eStudioGANtrazem implementações testadas de DCGAN, WGAN-GP, SN-GAN, StyleGAN e BigGAN.
Em 2026, as GANs ainda são a melhor escolha para: geração de imagens em tempo real (latência <10 ms), transferência de estilo, tradução imagem-para-imagem com controle preciso (Pix2Pix, CycleGAN). A difusão vence em fotorrealismo e condicionamento por texto.
Entregue
Esta lição produz:
outputs/prompt-gan-training-triage.md— um prompt que lê a descrição de uma curva de treinamento e escolhe o modo de falha (colapso de modo, D-vence, oscilação) mais a única correção recomendada.outputs/skill-dcgan-scaffold.md— uma skill que escreve um scaffold de DCGAN a partir dez_dim, doimage_sizealvo e donum_channels, incluindo o loop de treinamento e o salvador de amostras.
Exercícios
- (Fácil) Treine a DCGAN acima no conjunto de dados sintético de círculos e salve uma grade de 16 amostras ao final de cada época. Em qual época os círculos gerados se tornam claramente circulares?
- (Médio) Substitua o batch norm do discriminador por normalização espectral. Treine as duas versões lado a lado. Qual converge mais rápido? Qual tem menor variância entre três sementes (seeds)?
- (Difícil) Implemente uma DCGAN condicional: alimente o rótulo de classe tanto em G quanto em D (concatene o one-hot ao ruído em G, concatene um canal de embedding de classe em D). Treine no conjunto de dados sintético "círculos vs quadrados" da lição 7 e mostre que o condicionamento por classe funciona amostrando com rótulos específicos.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Gerador (G) | "A rede que desenha coisas" | Mapeia ruído para imagens; treinado para enganar o discriminador |
| Discriminador (D) | "O crítico" | Classificador binário; treinado para distinguir imagens reais de geradas |
| Minimax | "O jogo" | min sobre G, max sobre D de uma perda adversarial; o equilíbrio é p_G = p_data |
| Perda não saturante | "A versão numericamente sã" | A perda de G é -log(D(G(z))) em vez de log(1 - D(G(z))) para evitar gradientes desvanecentes no início do treinamento |
| Colapso de modo | "O gerador faz uma coisa só" | G produz apenas um pequeno subconjunto da distribuição de dados; corrija com SN, discriminação por minibatch ou batch maior |
| TTUR | "Duas taxas de aprendizado" | D aprende mais rápido que G, tipicamente por um fator de 2-4; estabiliza o treinamento |
| Normalização espectral | "Camada 1-Lipschitz" | Uma normalização de pesos que limita a constante de Lipschitz de cada camada; impede que D fique arbitrariamente íngreme |
| FID | "Fréchet Inception Distance" | Distância entre as distribuições de características da Inception-v3 dos conjuntos real e gerado; a métrica de avaliação padrão |
Leitura Adicional
- Generative Adversarial Networks (Goodfellow et al., 2014) — o artigo que começou tudo
- DCGAN (Radford, Metz, Chintala, 2015) — as regras de arquitetura que tornaram as GANs treináveis
- Spectral Normalization for GANs (Miyato et al., 2018) — o truque de estabilização mais útil de todos
- StyleGAN3 (Karras et al., 2021) — a GAN estado da arte; lê-se como um álbum de grandes sucessos de cada truque da última década