Capítulo 6: Modelos Generativos e Multimodais#

Este capítulo percorre a IA generativa em duas direções complementares. Primeiro, aprendemos a usar um modelo multimodal pronto (a família Gemini, do Google) através de sua API, para analisar imagens: descrever cenas, extrair informações em formato estruturado, ler textos e localizar objetos, tudo sem treinar nenhum modelo. Em seguida, passamos a construir modelos generativos a partir do zero, estudando a intuição dos principais paradigmas (autoencoders variacionais, redes adversariais e modelos de difusão), com foco prático em GANs sobre o MNIST e em Stable Diffusion para geração de imagens condicionada por texto.

A ordem é proposital. Começar consumindo um modelo multimodal por API é a porta de entrada mais rápida para a IA generativa. O contraste com o treinamento de GANs e de modelos de difusão deixa claro o que cada abordagem exige e o que cada uma oferece.

Análise de Imagens com a API do Gemini#

Os modelos multimodais (ou Vision-Language Models, VLMs) recebem imagens e texto na mesma requisição e respondem em linguagem natural. Eles são, na prática, modelos generativos: dado um prompt e uma imagem, geram um texto como resposta. Isso os torna o ponto de partida ideal para este capítulo, pois permitem analisar imagens com poucas linhas de código, sem coletar dados nem treinar redes.

No Capítulo 4 (seção LLMs de Visão e Uso de APIs) apresentamos a família Gemini e comparamos provedores via OpenRouter. Aqui o foco é prático e mais aprofundado: vamos configurar o acesso à API e aplicá-la a três tarefas de análise de imagens: saída estruturada em JSON, extração de texto (OCR) e detecção de objetos por prompt.

Criando e configurando a chave de API#

Para usar a API você precisa de uma chave de API (API key), gratuita dentro dos limites de uso para estudo. Acesse o Google AI Studio em aistudio.google.com/app/apikey, faça login e clique em Create API key. Copie a chave: ela é um segredo, então trate-a como uma senha e nunca a escreva no código nem a publique no GitHub.

O ponto importante é que a chave não vai no código. No Colab, você a cola uma única vez no painel de Secrets (ícone de uma chave na barra lateral): crie um segredo chamado GEMINI_API_KEY, cole o valor e habilite o acesso para o notebook. O código apenas lê a chave de lá. Feito isso, instale o SDK e crie o cliente:

!pip install -U -q google-genai
from google import genai
from google.genai import types
from google.colab import userdata

# A chave vem do painel "Secrets" do Colab (você a colou lá, não aqui)
client = genai.Client(api_key=userdata.get("GEMINI_API_KEY"))

from google.genai import errors

# Modelos (flash) em ordem de preferência; se um estiver sobrecarregado (503),
# cai para o próximo. Para tarefas mais exigentes, use um modelo "pro" (ex.: gemini-2.5-pro).
MODELOS = ["gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.5-flash-lite"]

def gerar_conteudo(contents, config=None):
    """Tenta cada modelo da lista e retorna a primeira resposta que funcionar."""
    for modelo in MODELOS:
        try:
            return client.models.generate_content(model=modelo, contents=contents, config=config)
        except errors.ServerError as e:   # 503/500: modelo ocupado, tenta o próximo
            print(f"{modelo} indisponível ({e.code}); tentando o próximo...")
        except errors.ClientError as e:
            if e.code == 429:                 # cota gratuita do dia esgotada neste modelo
                print(f"{modelo} sem cota hoje ({e.code}); tentando o próximo...")
            else:
                raise                         # outros erros (400, 403...) param na hora
    raise RuntimeError("Todos os modelos da lista falharam (indisponíveis ou sem cota). Tente novamente mais tarde.")

Além do cliente, definimos uma lista de modelos (MODELOS) e a função gerar_conteudo. Modelos populares às vezes ficam sobrecarregados (503) ou esgotam a cota gratuita do dia (429). Em vez de falhar, gerar_conteudo tenta cada modelo da lista em ordem (cada modelo tem cota separada) e devolve a primeira resposta que funcionar. Por isso, nos exemplos a seguir chamamos gerar_conteudo(...) em vez de client.models.generate_content(...) diretamente.

Descrição de imagem (verificando o acesso)#

Antes das análises mais elaboradas, vale confirmar que o acesso funciona com uma descrição simples. Para o exemplo ser autossuficiente, baixamos uma imagem da internet com wget na própria célula (uma foto de rua com um ônibus e pedestres), carregamos os bytes e os enviamos junto com uma instrução em texto:

# Baixa uma imagem de exemplo da internet
!wget -q https://raw.githubusercontent.com/ultralytics/ultralytics/main/ultralytics/assets/bus.jpg -O onibus.jpg

with open("onibus.jpg", "rb") as f:
    image_bytes = f.read()

# Exibe a imagem que será enviada ao modelo
import matplotlib.pyplot as plt
from PIL import Image

plt.imshow(Image.open("onibus.jpg"))
plt.axis("off")
plt.show()

response = gerar_conteudo(
    contents=[
        types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg"),
        "Descreva esta imagem em duas frases.",
    ],
    config=types.GenerateContentConfig(temperature=0.2),
)
print(response.text)

A célula exibe a imagem e imprime a descrição gerada pelo modelo:

../../_images/onibus.jpg
Dois homens de casaco e óculos de sol caminham numa calçada, com um ônibus elétrico azul da EMT Madrid ao fundo. O ônibus exibe mensagens como "100% elétrico" e "cero emisiones", e a cena se passa numa rua urbana ensolarada com um edifício amarelo e varandas no segundo plano.

Observação: usamos temperature=0.2 (valor baixo) porque, em tarefas de análise, queremos respostas mais determinísticas e fiéis à imagem, e não criatividade.

Saída estruturada em JSON#

Texto livre é difícil de usar dentro de um programa. Para integrar a resposta a um pipeline (salvar em banco, gerar um relatório, alimentar outro código), pedimos ao modelo uma saída estruturada: definimos um schema e o modelo responde em JSON válido seguindo exatamente esse formato. No SDK isso é feito com response_mime_type="application/json" e response_schema, usando classes do Pydantic para descrever os campos:

from pydantic import BaseModel, Field

# Schema da análise: o modelo é obrigado a preencher estes campos
class AnaliseImagem(BaseModel):
    descricao: str = Field(description="Descrição curta da cena")
    objetos: list[str] = Field(description="Lista dos objetos visíveis")
    cores_predominantes: list[str]
    contem_texto: bool
    n_pessoas: int = Field(description="Número de pessoas na imagem")

response = gerar_conteudo(
    contents=[
        types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg"),
        "Analise a imagem e preencha os campos solicitados.",
    ],
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=AnaliseImagem,
        temperature=0.1,
    ),
)

print(response.text)          # JSON em texto
analise = response.parsed     # objeto AnaliseImagem já validado
print(analise.objetos)
print("Tem texto?", analise.contem_texto)

A saída impressa pela célula:

{
    "descricao": "Uma cena de rua movimentada com um ônibus elétrico azul e branco, várias pessoas caminhando e edifícios em tons de bege ao fundo.",
    "objetos": ["ônibus", "pessoas", "edifício", "janelas", "sacadas", "sinal de trânsito", "postes de rua", "calçada"],
    "cores_predominantes": ["azul", "bege", "branco", "preto", "verde", "cinza"],
    "contem_texto": true,
    "n_pessoas": 4
}
['ônibus', 'pessoas', 'edifício', 'janelas', 'sacadas', 'sinal de trânsito', 'postes de rua', 'calçada']
Tem texto? True

A grande vantagem é que response.parsed devolve um objeto Python já validado pelo Pydantic. Em vez de interpretar um texto livre, acessamos analise.objetos, analise.n_pessoas e os demais campos com segurança de tipos.

Extração de texto (OCR)#

A mesma ideia resolve OCR (reconhecimento óptico de caracteres): em vez de uma biblioteca dedicada, pedimos ao modelo que transcreva o texto presente na imagem. VLMs lidam bem com fotos de documentos, placas e cartazes, inclusive com texto manuscrito e layout irregular. Para reproduzir, baixe a ficha de cadastro de exemplo abaixo (com campos preenchidos à mão) e faça o upload dela no seu Colab (painel de arquivos à esquerda, botão de upload). Depois é só ler o arquivo. Troque pela sua própria imagem com texto se quiser:

Ficha de cadastro de exemplo com campos preenchidos à mão

Baixar documento.jpg

with open("documento.jpg", "rb") as f:
    doc_bytes = f.read()

response = gerar_conteudo(
    contents=[
        types.Part.from_bytes(data=doc_bytes, mime_type="image/jpeg"),
        "Transcreva todo o texto visível nesta imagem, preservando a ordem de leitura.",
    ],
    config=types.GenerateContentConfig(temperature=0.0),
)
print(response.text)

A saída impressa pela célula:

AURORA COMÉRCIO

Ficha de Cadastro de Cliente

Nome: Mariana Costa Silva
CPF: 123.456.789-00
Telefone: (84) 99654-3210
E-mail: mariana.silva@email.com
Endereço: Rua das Acácias, 128, Natal/RN
Data: 24/06/2026
Assinatura: Mariana Silva

Dica: para campos específicos (nome, CPF, telefone, data), combine OCR com saída estruturada: defina um schema com esses campos e o modelo retorna os dados já organizados, prontos para uso.

Detecção de objetos por prompt: caixas delimitadoras#

VLMs também conseguem localizar objetos, retornando caixas delimitadoras (bounding boxes) a partir de uma instrução em linguagem natural. Diferente do YOLO (Capítulo 5), o vocabulário é aberto: podemos pedir “encontre o ônibus e os pedestres” sem que essas classes tenham sido pré-definidas.

O Gemini retorna coordenadas no formato [ymin, xmin, ymax, xmax], normalizadas no intervalo de 0 a 1000 (independente do tamanho real da imagem). Reutilizando a foto do ônibus já carregada (image_bytes), pedimos uma lista estruturada de caixas e depois convertemos para pixels:

from pydantic import BaseModel

class Caixa(BaseModel):
    label: str
    box_2d: list[int]   # [ymin, xmin, ymax, xmax], normalizado de 0 a 1000

response = gerar_conteudo(
    contents=[
        types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg"),
        "Detecte os objetos principais. Retorne box_2d como "
        "[ymin, xmin, ymax, xmax] normalizado de 0 a 1000.",
    ],
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=list[Caixa],
        temperature=0.0,
    ),
)

caixas = response.parsed
for c in caixas:
    print(c.label, c.box_2d)

Com as caixas em mãos, convertemos de 0-1000 para coordenadas em pixels e as desenhamos sobre a imagem com o Matplotlib:

import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image

img = Image.open("onibus.jpg")
largura, altura = img.size

fig, ax = plt.subplots(figsize=(10, 8))
ax.imshow(img)

for c in caixas:
    ymin, xmin, ymax, xmax = c.box_2d
    # Converte de 0-1000 para pixels
    x = xmin / 1000 * largura
    y = ymin / 1000 * altura
    w = (xmax - xmin) / 1000 * largura
    h = (ymax - ymin) / 1000 * altura

    ax.add_patch(patches.Rectangle((x, y), w, h, fill=False, edgecolor="red", linewidth=2))
    ax.text(x, y - 5, c.label, color="white", fontsize=10,
            bbox=dict(facecolor="red", alpha=0.7, pad=1))

ax.axis("off")
plt.show()

O resultado, com as caixas desenhadas sobre a imagem:

../../_images/onibus_caixas.jpg

Observação: a detecção por prompt é flexível e dispensa treinamento, mas costuma ser mais lenta e menos precisa na localização fina do que um detector dedicado como o YOLO. Para contagens exatas, objetos pequenos ou vídeo em tempo real, prefira um modelo treinado; para prototipagem e classes incomuns, o VLM resolve rápido.

Boas práticas e limitações#

  • Custo e cota: cada chamada consome tokens (de texto e de imagem) e pode gerar custo. O plano gratuito tem limites de requisições por minuto; para lotes grandes, controle a frequência das chamadas.

  • Alucinação: o modelo pode responder com confiança mesmo quando erra. Em domínios sensíveis (saúde, jurídico), trate a saída como sugestão e valide com um especialista.

  • Temperatura baixa: para análise factual, use temperature próxima de 0, reservando valores altos para tarefas criativas.

  • Privacidade: não envie imagens com dados pessoais ou sigilosos sem base legal (revisite a LGPD, discutida no Capítulo 4).

  • VLM ou modelo treinado: para classes fixas, alta precisão de localização ou tempo real, um detector treinado (YOLO) ainda é a melhor escolha. O VLM se destaca na flexibilidade e na rapidez de prototipagem.

Com esse panorama do uso de um modelo multimodal pronto, passamos agora a construir modelos generativos a partir do zero.

Introdução às Redes Generativas#

As redes generativas (ou modelos generativos) são algoritmos de aprendizado de máquina que tentam capturar a distribuição dos dados de treinamento para, a partir disso, gerar novas amostras semelhantes às originais. Enquanto modelos discriminativos aprendem a distinguir categorias (por exemplo, “é gato” ou “não é gato”), modelos generativos buscam entender “como” os dados foram criados, de modo a sintetizar exemplares que sigam o mesmo padrão estatístico.

Por que usar modelos generativos?#

  • Criação de Conteúdo: gerar imagens, texto ou áudio novos (arte, roteiros, trilhas sonoras).

  • Aumento de Dados: sintetizar exemplos para treinar outros modelos em cenários com poucos dados reais.

  • Compressão e Representação: aprender uma representação latente que capture as características principais dos dados (por exemplo, em autoencoders).

  • Análise de Distribuição: avaliar se certas regiões de espaço de dados são pouco exploradas ou gerar variações seguras para testes.

Principais Famílias de Modelos Generativos#

  • Modelos de Mistura Gaussiana (GMM)

    • Ideia principal: aproximam a distribuição dos dados por uma soma de distribuições normais multivariadas.

    • Semântica prática: cada componente gaussiano “modela” um cluster nos dados; a mistura (peso de cada componente) explica a probabilidade final.

    • Pontos-chave:

    • Fácil de entender e implementar em baixa dimensão.

    • Não escala bem para imagens de alta resolução.

    • Serve como introdução conceitual sobre “estimativa de densidade”.

  • Variational Autoencoders (VAEs)

    • Ideia principal: combinam um encoder que mapeia dados de entrada \(x\) para uma distribuição latente \(q_\phi(z \mid x)\) (normalmente \(\mathcal{N}(\mu, \sigma^2)\)) e um decoder que reconstrói \(x\) a partir de \(z\).

    • Objetivo de Treinamento: otimizar um limite inferior da evidência (ELBO), composto por:

    1. Automínimo erro de reconstrução (por exemplo, MSE).

    2. Divergência de Kullback–Leibler (\(\mathrm{KL}\)) entre \(q_\phi(z \mid x)\) e uma distribuição pré-definida \(p(z)\), tipicamente \(\mathcal{N}(0,I)\).

    • Pontos-chave:

    • Permitem amostrar diretamente do espaço latente (gera variações suaves).

    • Produzem amostras que podem ficar um pouco “borradas” em imagens.

    • Treinamento relativamente estável e eficiente em comparação com GANs.

  • Redes Generativas Adversariais (GANs)

    • Ideia principal: GANs consistem em duas redes neurais que competem entre si em um jogo de soma zero. A primeira, chamada Gerador (\(G\)), busca criar amostras que pareçam reais; a segunda, chamada Discriminador (\(D\)), tenta distinguir o que é real do que foi gerado. Essa competição força ambas as redes a melhorarem constantemente.

    • Gerador (\(G\)): a partir de um vetor aleatório \(z\sim p(z)\), tenta gerar uma amostra \(G(z)\) que pareça real.

    • Discriminador (\(D\)): recebe amostras reais ou geradas e tenta distinguir “real” de “falso”.

    • O jogo minimax:

    \[ \min_G \max_D \; \mathbb{E}_{x\sim p_\text{data}}[\log D(x)] \;+\; \mathbb{E}_{z\sim p(z)}[\log(1 - D(G(z)))]. \]
    • Pontos-chave:

    • Podem gerar imagens nítidas e de alta resolução.

    • Exigem cuidado para evitar instabilidades (colapso de modo, onde o gerador produz poucas variações).

    • Sucesso em domínios como geração de rostos (e.g., StyleGAN) e arte abstrata.

  • Modelos de Difusão (Diffusion Models)

    • Ideia principal: Modelos de Difusão simulam um processo de ruído progressivo em uma imagem real e treinam uma rede neural para reverter esse ruído passo a passo, recuperando a imagem original. Esse processo permite gerar novas amostras a partir de puro ruído, com alta qualidade e estabilidade.

    • Processo para frente (“noising”): parte-se de uma amostra real \(x_0\) e, em cada passo \(t\), adiciona-se ruído gaussiano para obter \(x_t\). Quando \(t\) for grande (por ex., \(T\approx1000\)), \(x_T\) é praticamente ruído puro.

    • Processo reverso (“denoising”): uma rede neural (\(\epsilon_\theta\)) é treinada para remover ruído de \(x_t\) e recuperar \(x_{t-1}\), até chegar a \(x_0\).

    • Pontos-chave:

    • Geralmente mais estáveis que GANs.

    • Produzem amostras de altíssima qualidade (sem artefatos típicos de GANs).

    • Inferir uma imagem exige dezenas ou centenas de etapas de denoising, tornando a geração mais lenta.

Tabela comparativa dos principais modelos apresentados:

Modelo

Abordagem

Vantagens Principais

Limitações Notáveis

Aplicações Típicas

GMM

Mistura de gaussianas

Simples, interpretável, útil para dados de baixa dimensão

Escala mal para imagens ou dados complexos

Clusterização, estimativa de densidade básica

VAE

Codificação latente probabilística

Treinamento estável, interpolações suaves no espaço latente

Geração de imagens pode ser borrada

Reconstrução e geração de dados; compressão

GAN

Competição Gerador vs Discriminador

Gera imagens realistas e de alta qualidade

Instabilidade no treinamento, colapso de modo

Geração de faces (StyleGAN), arte, deepfakes

Modelos de Difusão

Ruído + reversão progressiva

Amostras de altíssima qualidade, sem artefatos típicos de GANs

Lento na geração (múltiplas etapas de denoising)

Imagens hiper-realistas, arte, medicina, vídeo (e.g. SDXL)

Observação: a seguir, focaremos em GANs e Stable Diffusion (um diffusion model voltado para texto → imagem).

Redes Generativas Adversariais (GANs)#

Conceito e Arquitetura Básica

As Redes Generativas Adversariais (GANs) são uma classe de modelos generativos compostos por duas redes neurais que competem entre si em um jogo adversarial: o Gerador tenta produzir amostras realistas, enquanto o Discriminador tenta distinguir se uma amostra é real ou foi gerada.

Gerador \(G_\theta\)

  • Recebe como entrada um vetor aleatório \(z \sim p(z)\), normalmente amostrado de uma distribuição gaussiana \(\mathcal{N}(0, I)\) ou uniforme.

  • Gera uma amostra sintética \(G(z)\), com aparência similar à de dados reais.

  • É geralmente implementado com redes convolucionais transpostas (ou técnicas de upsampling).

Discriminador \(D_\phi\)

  • Recebe como entrada uma amostra \(x\) (real ou gerada).

  • Retorna uma probabilidade \(D(x) \in [0, 1]\), indicando o quão “real” é a entrada.

  • É implementado como uma rede convolucional tradicional, atuando como um classificador binário.

Funções de Custo (Loss) e Treinamento

Notação de Esperança Matemática \(\mathbb{E}\)

A notação \(\mathbb{E}_{x \sim p(x)}[f(x)]\) representa a esperança matemática (ou média esperada) da função \(f(x)\) quando a variável \(x\) é amostrada de uma distribuição \(p(x)\). Na prática, essa média é estimada por um minibatch de amostras durante o treinamento.

Objetivo do Discriminador

O discriminador busca maximizar a seguinte função de custo:

\[ \mathcal{L}_D = \mathbb{E}_{x \sim p_\text{data}(x)}[\log D(x)] + \mathbb{E}_{z \sim p(z)}[\log(1 - D(G(z)))] \]

Onde:

  • \(p_\text{data}(x)\): distribuição real dos dados.

  • \(p(z)\): distribuição aleatória de entrada do gerador.

  • \(D(x)\): probabilidade atribuída pelo discriminador de que \(x\) seja real.

Objetivo do Gerador

O gerador busca minimizar a capacidade do discriminador de identificar amostras falsas. Existem duas versões da loss:

  • Versão original:

\[ \mathcal{L}_G = \mathbb{E}_{z \sim p(z)}[\log(1 - D(G(z)))] \]
  • Versão modificada (mais estável):

\[ \mathcal{L}_G = \mathbb{E}_{z \sim p(z)}[-\log D(G(z))] \]

Esta versão é mais comum na prática, pois evita gradientes muito pequenos nas fases iniciais do treinamento.

Treinamento Adversarial

O processo de treinamento alterna entre dois passos:

  1. Atualização de \(D_\phi\):

  • Amostra um batch de dados reais \(x \sim p_\text{data}(x)\).

  • Amostra um batch de ruído \(z \sim p(z)\), gera \(G(z)\).

  • Otimiza os parâmetros \(\phi\) do discriminador para maximizar \(\mathcal{L}_D\).

  1. Atualização de \(G_\theta\):

  • Gera novas amostras \(G(z)\) com \(z \sim p(z)\).

  • Otimiza os parâmetros \(\theta\) do gerador para minimizar \(\mathcal{L}_G\).

Jogo Minimax

O treinamento das GANs é formulado como um jogo de soma zero entre duas redes com objetivos opostos:

\[ \min_G \max_D \; \mathbb{E}_{x \sim p_\text{data}(x)}[\log D(x)] + \mathbb{E}_{z \sim p(z)}[\log(1 - D(G(z)))] \]

Esse é o jogo minimax: o discriminador tenta maximizar a função, e o gerador tenta minimizá-la. No equilíbrio, \(G\) produz amostras indistinguíveis dos dados reais e \(D(x) \approx 0{,}5\).

Variações Populares de GANs

  • DCGAN (Deep Convolutional GAN): Estrutura convolucional profunda adaptada para imagens. Usa Strided Convolutions, Batch Normalization, e LeakyReLU.

  • WGAN (Wasserstein GAN): Substitui a função de perda original pela distância de Wasserstein, oferecendo estabilidade e interpretabilidade.

  • StyleGAN / StyleGAN2: Introduz camadas de controle de estilo e um mapeamento intermediário para o vetor \(z\), permitindo controle sobre características visuais como iluminação, expressão, e pose.

Exemplo Prático de GAN em PyTorch

Abaixo, um exemplo mínimo para treinar um GAN em um conjunto de imagens (por exemplo, MNIST ou CIFAR-10). Aqui usamos MNIST (imagens 28×28 em escala de cinza) apenas para fins ilustrativos.

import os
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms, utils
from torch.utils.data import DataLoader

# 1) CONFIGURAÇÕES GERAIS
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Usa GPU se disponível
latent_dim = 100   # Dimensão do vetor latente (entrada do gerador)
batch_size = 128   # Tamanho do lote
lr = 0.0002      # Taxa de aprendizado
epochs = 30      # Número de épocas de treinamento
sample_dir = "samples" # Pasta para salvar imagens geradas
os.makedirs(sample_dir, exist_ok=True)

# 2) CARREGAMENTO DO DATASET (MNIST)
transform = transforms.Compose([
  transforms.ToTensor(),           # Converte imagens para tensores
  transforms.Normalize([0.5], [0.5])     # Normaliza para o intervalo [-1, 1]
])
dataset = datasets.MNIST(root="./data", train=True, download=True, transform=transform)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

# 3) DEFINIÇÃO DO GERADOR
class Generator(nn.Module):
  def __init__(self):
    super().__init__()
    self.net = nn.Sequential(
      nn.Linear(latent_dim, 256),
      nn.LeakyReLU(0.2, inplace=True),
      nn.Linear(256, 512),
      nn.BatchNorm1d(512),
      nn.LeakyReLU(0.2, inplace=True),
      nn.Linear(512, 1024),
      nn.BatchNorm1d(1024),
      nn.LeakyReLU(0.2, inplace=True),
      nn.Linear(1024, 28*28), # Saída do gerador deve ter o tamanho da imagem do MNIST (784)
      nn.Tanh() # Saída entre [-1, 1] para combinar com a normalização do dataset
    )

  def forward(self, z):
    img = self.net(z) # Gera imagem a partir de vetor latente
    return img.view(-1, 1, 28, 28) # Reshape para imagem (1 canal, 28x28)

# 4) DEFINIÇÃO DO DISCRIMINADOR
class Discriminator(nn.Module):
  def __init__(self):
    super().__init__()
    self.net = nn.Sequential(
      nn.Linear(28*28, 512),
      nn.LeakyReLU(0.2, inplace=True),
      nn.Linear(512, 256),
      nn.LeakyReLU(0.2, inplace=True),
      nn.Linear(256, 1),
      nn.Sigmoid() # Produz probabilidade de ser imagem real
    )

  def forward(self, img):
    flat = img.view(-1, 28*28) # Achata a imagem para vetor
    return self.net(flat)

# 5) INSTANCIAÇÃO DE MODELOS, FUNÇÃO DE PERDA E OTIMIZADORES
generator = Generator().to(device)
discriminator = Discriminator().to(device)

criterion = nn.BCELoss() # Binary Cross Entropy para classificação binária (real ou falsa)
optim_G = optim.Adam(generator.parameters(), lr=lr, betas=(0.5, 0.999))
optim_D = optim.Adam(discriminator.parameters(), lr=lr, betas=(0.5, 0.999))

# 6) FUNÇÕES AUXILIARES PARA GERAR RÓTULOS
def real_labels(size):
  return torch.ones(size, 1, device=device) # Rótulo "1" para imagens reais

def fake_labels(size):
  return torch.zeros(size, 1, device=device) # Rótulo "0" para imagens falsas

# 7) LOOP DE TREINAMENTO DO GAN
for epoch in range(epochs):
  for i, (imgs, _) in enumerate(dataloader):
    batch_size_i = imgs.size(0)
    imgs = imgs.to(device)

    # ----- TREINAMENTO DO DISCRIMINADOR -----
    # Gera imagens falsas a partir de ruído
    z = torch.randn(batch_size_i, latent_dim, device=device)
    fake_imgs = generator(z)

    # Avalia imagens reais e falsas
    real_out = discriminator(imgs)
    fake_out = discriminator(fake_imgs.detach()) # Detach para não propagar gradientes para o gerador

    # Calcula a perda do discriminador
    loss_D_real = criterion(real_out, real_labels(batch_size_i))
    loss_D_fake = criterion(fake_out, fake_labels(batch_size_i))
    loss_D = (loss_D_real + loss_D_fake) / 2

    # Backpropagation e otimização do discriminador
    optim_D.zero_grad()
    loss_D.backward()
    optim_D.step()

    # ----- TREINAMENTO DO GERADOR -----
    # Tenta enganar o discriminador: quer que imagens falsas sejam classificadas como reais
    output = discriminator(fake_imgs)
    loss_G = criterion(output, real_labels(batch_size_i)) # Compara com rótulo real (1)

    # Backpropagation e otimização do gerador
    optim_G.zero_grad()
    loss_G.backward()
    optim_G.step()

    # Exibe progresso a cada 200 lotes
    if i % 200 == 0:
      print(f"Epoch [{epoch+1}/{epochs}] Batch [{i}/{len(dataloader)}] "
         f"Loss D: {loss_D.item():.4f}, Loss G: {loss_G.item():.4f}")

  # ----- GERA E SALVA AMOSTRAS -----
  with torch.no_grad():
    z = torch.randn(16, latent_dim, device=device)
    sample_imgs = generator(z).cpu()
    sample_grid = utils.make_grid(sample_imgs, nrow=4, normalize=True)
    utils.save_image(sample_grid, f"{sample_dir}/epoch_{epoch+1:03d}.png")

print("Treinamento concluído! Amostras salvas na pasta 'samples/'.")

Resumo do que o código faz:#

  • Treina um GAN (Generative Adversarial Network) usando o conjunto de dados MNIST.

  • O gerador aprende a criar imagens falsas de dígitos manuscritos.

  • O discriminador tenta distinguir imagens reais (do MNIST) de imagens falsas (do gerador).

  • Ambos são treinados de forma adversarial: um tenta enganar o outro.

  • Ao final de cada época, são geradas 16 imagens para avaliação visual do progresso do gerador.

Modelos de Difusão e Stable Diffusion

O processo de diffusion models consiste em dois momentos principais: inserir ruído progressivamente numa imagem real e, em seguida, aprender a reverter esse processo para gerar novas amostras. Primeiro, parte-se de uma imagem original \(x_0\) e adiciona-se ruído gaussiano em vários passos \(t=1,2,\dots,T\), de modo que, quando \(t\) é grande, a imagem se transforma em praticamente puro ruído. Formalmente, cada passo é descrito como:

\[ x_t = \sqrt{\alpha_t}\,x_{t-1} + \sqrt{1 - \alpha_t}\,\epsilon_t,\quad \epsilon_t \sim \mathcal{N}(0,I), \]

onde \(\alpha_t \in (0,1)\) controla o nível de ruído em \(t\). À medida que avança-se de \(t=1\) até \(T\), o modelo aplica mais ruído até que \(x_T\) seja quase aleatório.

O segundo momento é o processo inverso, chamado denoising. Treina-se uma rede \(\epsilon_\theta(x_t, t)\) para estimar o ruído \(\epsilon_t\) presente em \(x_t\). Com essa predição \(\hat{\epsilon} = \epsilon_\theta(x_t, t)\), obtém-se uma aproximação de \(x_{t-1}\) a partir de:

\[ x_{t-1} \approx \frac{1}{\sqrt{\alpha_t}}\Bigl(x_t - \frac{1 - \alpha_t}{\sqrt{1 - \bar\alpha_t}}\,\hat{\epsilon}\Bigr) + \sigma_t\,n,\quad n\sim \mathcal{N}(0,I), \]

onde \(\bar\alpha_t = \prod_{s=1}^{t} \alpha_s\) e \(\sigma_t\) é um termo de ruído adaptativo para garantir diversidade nas amostras. Ao repetir esse passo regressivamente de \(t=T\) até \(t=1\), chega-se a uma nova imagem \(x_0\) gerada pelo modelo.

Esse tipo de arquitetura costuma ser mais estável que GANs porque não envolve um jogo adversarial direto. Além disso, tende a produzir imagens de alta qualidade e com grande diversidade, podendo ser facilmente condicionado a textos, máscaras, esboços ou outros sinais auxiliares.

Visão Geral do Stable Diffusion

O Stable Diffusion é uma implementação eficiente de diffusion models para geração de imagens a partir de texto, baseada em duas ideias-chave: trabalhar no espaço latente e usar um mecanismo de atenção para incorporar informações textuais.

  • Espaço Latente (Latent Diffusion) Em vez de aplicar difusão diretamente em pixels de alta resolução (por exemplo, tensores \(512 \times 512 \times 3\)), o Stable Diffusion aproveita um autoencoder pré-treinado (VAE) para comprimir cada imagem para um espaço de dimensão muito menor (por exemplo, \(\mathbb{R}^{4\times 64\times 64}\)). No treinamento, adiciona-se ruído apenas nesse espaço latente. Na geração, amostra-se um vetor latente puro (\(z_T \sim \mathcal{N}(0, I)\)) e aplica-se o processo de denoising nesse espaço reduzido. Ao fim, o decoder do VAE reconstrói a imagem em pixels na resolução original.

  • Condicionamento em Texto Um encoder de texto (tipicamente o CLIP Text Encoder) converte o prompt em embeddings. Esses vetores de texto entram em uma U-Net de denoising via cross-attention: em cada passo de remoção de ruído, a U-Net recebe o latente atual \(z_t\), o índice do passo \(t\) e os embeddings do texto, e produz uma estimativa de ruído \(\hat{\epsilon}\) alinhada ao significado do prompt. Para reforçar a fidelidade ao texto, adota-se classifier-free guidance, que mistura a predição de ruído com e sem contexto textual usando um fator de peso \(w\).

  • Pipeline de Geração

  1. Converter o prompt em token IDs e embeddings.

  2. Amostrar ruído latente \(z_T \sim \mathcal{N}(0,I)\) no espaço comprimido (por exemplo, \(\mathbb{R}^{4\times64\times64}\)).

  3. Para \(t = T, T-1, \dots, 1\):

  • A U-Net recebe \(z_t\), o passo \(t\) e os embeddings do texto.

  • Prediz o ruído \(\hat{\epsilon}\).

  • Atualiza \(z_{t-1}\) pela fórmula de denoising, usando \(\hat{\epsilon}\) e os coeficientes \(\alpha_t\).

  1. Quando \(z_0\) é obtido, passa-se pelo decoder do VAE para gerar a imagem final em alta resolução (por exemplo, \(512\times512\times3\)).

  • Vantagens do Stable Diffusion

  • Processar no espaço latente reduz dramaticamente o custo computacional comparado a difundir em pixels.

  • A qualidade das imagens geradas costuma ser muito alta, com poucos artefatos.

  • Além de texto→imagem, suporta tarefas como inpainting, outpainting e outras modalidades condicionadas (máscaras, rascunhos, etc.).

Exemplo Prático Funcional de Stable Diffusion

A seguir, um exemplo simples que já funciona em um notebook (local ou Colab). Ele usa a biblioteca diffusers para carregar um pipeline do Stable Diffusion, gerar uma imagem a partir de um prompt e exibi-la.

# Instalação das dependências (execute apenas uma vez no notebook/Colab)
!pip install -q diffusers transformers accelerate safetensors

import torch
from diffusers import StableDiffusionPipeline
from PIL import Image
import matplotlib.pyplot as plt
from IPython.display import display

# Verifica se há GPU disponível
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Dispositivo em uso: {device}")

# Carrega o modelo Stable Diffusion 2.1
model_id = "stabilityai/stable-diffusion-2-1"
pipe = StableDiffusionPipeline.from_pretrained(
  model_id,
  torch_dtype=torch.float16 if device == "cuda" else torch.float32
).to(device)

# Define o prompt (descrição textual da imagem)
prompt = "Uma floresta encantada ao entardecer, estilo pintura digital"

# Parâmetros de geração
num_steps = 50     # Passos de denoising
guidance = 7.5     # Peso para aderência ao prompt

# Função para gerar imagem a partir do prompt
def gerar_imagem(prompt, steps=50, guidance_scale=7.5, height=512, width=512):
  with torch.autocast(device) if device == "cuda" else torch.no_grad():
    resultado = pipe(
      prompt=prompt,
      height=height,
      width=width,
      num_inference_steps=steps,
      guidance_scale=guidance_scale
    )
  return resultado.images[0]

# Função para exibir imagem (compatível com diferentes ambientes)
def exibir_imagem(imagem, titulo="Imagem Gerada"):
  try:
    # Método 1: Para Jupyter/Colab - usando matplotlib
    plt.figure(figsize=(10, 10))
    plt.imshow(imagem)
    plt.axis('off')
    plt.title(titulo)
    plt.show()
  except:
    try:
      # Método 2: Para Jupyter/Colab - usando IPython display
      display(imagem)
    except:
      try:
        # Método 3: Método padrão do PIL
        imagem.show()
      except:
        # Método 4: Salvar arquivo localmente
        nome_arquivo = "imagem_gerada.png"
        imagem.save(nome_arquivo)
        print(f"Imagem salva como: {nome_arquivo}")

# Geração e exibição da imagem
print("Gerando imagem... (pode levar alguns minutos)")
imagem = gerar_imagem(prompt, num_steps, guidance)

# Exibe a imagem usando múltiplos métodos
exibir_imagem(imagem, f"Prompt: {prompt}")

# Salva a imagem também
imagem.save("floresta_encantada.png")
print("Imagem salva como: floresta_encantada.png")

# Função adicional para gerar múltiplas imagens
def gerar_multiplas_imagens(prompt, quantidade=4, steps=50, guidance_scale=7.5):
  imagens = []
  for i in range(quantidade):
    print(f"Gerando imagem {i+1}/{quantidade}...")
    img = gerar_imagem(prompt, steps, guidance_scale)
    imagens.append(img)
    
    # Salva cada imagem
    nome_arquivo = f"imagem_{i+1}.png"
    img.save(nome_arquivo)
  
  # Exibe todas as imagens em uma grade
  fig, axes = plt.subplots(2, 2, figsize=(15, 15))
  fig.suptitle(f"Prompt: {prompt}", fontsize=16)
  
  for i, (img, ax) in enumerate(zip(imagens, axes.flat)):
    ax.imshow(img)
    ax.axis('off')
    ax.set_title(f"Variação {i+1}")
  
  plt.tight_layout()
  plt.show()
  
  return imagens

# Exemplo de uso da função para múltiplas imagens
# imagens_multiplas = gerar_multiplas_imagens(prompt, quantidade=4)

Outros Modelos de Diffusão Relevantes

  • DDPM (Denoising Diffusion Probabilistic Models): o design original, aplicando difusão diretamente em pixels.

  • DDIM (Denoising Diffusion Implicit Models): permite gerar amostras consistentes com muito menos passos via uma discretização alternativa.

  • Score-Based Models: estimam o gradiente do log-densidade \(\nabla_x \log p_t(x)\) em cada etapa, em vez de predizer explicitamente o ruído.

Essas variantes compartilham a mesma ideia central: adicionar ruído a partir de dados reais e então aprender a reverter esse processo, mas diferem na forma de parametrizar ou acelerar a sequência de passos. Cada uma pode ser adaptada a diferentes cenários de geração, seja por velocidade, qualidade ou requisitos de hardware.

Referências e Conteúdo Extra#