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:

  1. Substitua pooling por convoluções com stride (em ambas as redes).
  2. Use batch norm tanto no gerador quanto no discriminador, exceto na saída de G e na entrada de D.
  3. Remova camadas totalmente conectadas em arquiteturas mais profundas.
  4. G usa ReLU em todas as camadas exceto na saída (tanh na saída para [-1, 1]).
  5. 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_fidelity calcula FID / IS no seu gerador sem escrever código de avaliação personalizado.
  • pytorch-gan-zoo (legado) e StudioGAN trazem 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 de z_dim, do image_size alvo e do num_channels, incluindo o loop de treinamento e o salvador de amostras.

Exercícios

  1. (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?
  2. (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)?
  3. (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

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).