Phase 09 - Lesson 05

Deep Q-Networks (DQN)

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

2013: Mnih treinou uma rede de Q-learning em pixels brutos, superando todos os agentes de RL clássicos em sete jogos de Atari. 2015: estendeu para 49 jogos, publicado na Nature, dando início à era do deep-RL. DQN é Q-learning mais três truques que tornam a aproximação de funções estável.

Tipo: Build Linguagens: Python Pré-requisitos: Fase 3 · 03 (Backpropagation), Fase 9 · 04 (Q-learning, SARSA) Tempo: ~75 minutos

O Problema

O Q-learning tabular precisa de um valor Q separado para cada par (estado, ação). Um tabuleiro de xadrez tem ~10⁴³ estados. Um frame do Atari tem 210×160×3 = 100.800 features. O RL tabular falha com milhares de estados, quanto mais com bilhões.

A solução é óbvia em retrospecto: substituir a tabela Q por uma rede neural, Q(s, a; θ). Mas o óbvio em retrospecto levou décadas. A aproximação ingênua de funções com Q-learning diverge sob a "tríade mortal" — aproximação de funções + bootstrapping + aprendizado off-policy. Mnih et al. (2013, 2015) identificaram três truques de engenharia que estabilizam o aprendizado:

  1. Experience replay decorrelaciona as transições.
  2. Rede alvo (target network) congela o alvo do bootstrap.
  3. Recorte de recompensa (reward clipping) normaliza as magnitudes dos gradientes.

O DQN no Atari foi a primeira vez que uma única arquitetura com um único conjunto de hiperparâmetros resolveu dezenas de problemas de controle a partir de pixels brutos. Tudo o que o "deep-RL" construiu desde então — DDQN, Rainbow, Dueling, Distributional, R2D2, Agent57 — está empilhado no topo desta base de três truques.

O Conceito

Loop de treinamento do DQN: env, replay buffer, rede online, rede alvo, perda TD de Bellman

O objetivo. O DQN minimiza a perda TD de um passo em uma função Q neural:

L(θ) = E_{(s,a,r,s')~D} [ (r + γ max_{a'} Q(s', a'; θ^-) - Q(s, a; θ))² ]

θ = rede online, atualizada a cada passo por gradiente descendente. θ^- = rede alvo, copiada periodicamente de θ (a cada ~10.000 passos). D = replay buffer de transições passadas.

Os três truques, em ordem de importância:

Experience replay. Um buffer circular de ~10⁶ transições. Cada passo de treinamento amostra um minibatch uniformemente de forma aleatória. Isso quebra a correlação temporal (frames sucessivos são quase idênticos), permite que a rede aprenda com transições raras de recompensa muitas vezes e decorrelaciona atualizações consecutivas de gradiente. Sem isso, o TD on-policy com uma rede neural diverge no Atari.

Rede alvo. Usar a mesma rede Q(·; θ) em ambos os lados da equação de Bellman faz com que o alvo se mova a cada atualização — "perseguindo o próprio rabo". A solução: manter uma segunda rede Q(·; θ^-) com pesos congelados. A cada C passos, copie θ → θ^-. Isso estabiliza o alvo da regressão por milhares de passos de gradiente de uma vez. Atualizações suaves (soft updates) θ^- ← τ θ + (1-τ) θ^- (usadas no DDPG, SAC) são uma variante mais suave.

Recorte de recompensa. As magnitudes de recompensa do Atari variam de 1 a mais de 1000. Recortar para {-1, 0, +1} impede que qualquer jogo individual domine o gradiente. Incorreto quando a magnitude da recompensa importa; adequado para o Atari, onde apenas o sinal importa.

Double DQN. Hasselt (2016) corrige o viés de maximização: usa a rede online para selecionar a ação e a rede alvo para avaliá-la.

target = r + γ Q(s', argmax_{a'} Q(s', a'; θ); θ^-)

Substituição direta, consistentemente melhor. Use por padrão.

Outras melhorias (Rainbow, 2017): replay priorizado (prioritized replay - amostra mais as transições com alto erro TD), arquitetura dueling (cabeças separadas para V(s) e vantagem), redes ruidosas (noisy networks - exploração aprendida), retornos de n-passos (n-step returns), Q distribucional (distributional Q - C51/QR-DQN), bootstrapping de múltiplos passos. Cada um adiciona alguns pontos percentuais; os ganhos são aproximadamente cumulativos.

Build It

O código aqui usa apenas a biblioteca padrão (stdlib) e não depende do numpy — usamos um MLP de camada oculta única implementado manualmente em um pequeno GridWorld contínuo, de modo que cada passo de treinamento é executado em microssegundos. O algoritmo é idêntico ao DQN do Atari em escala.

Passo 1: replay buffer

class ReplayBuffer:
    def __init__(self, capacity):
        self.buf = []
        self.capacity = capacity
    def push(self, s, a, r, s_next, done):
        if len(self.buf) == self.capacity:
            self.buf.pop(0)
        self.buf.append((s, a, r, s_next, done))
    def sample(self, batch, rng):
        return rng.sample(self.buf, batch)

~50.000 de capacidade para o Atari; 5.000 é suficiente para o nosso ambiente de teste (toy env).

Passo 2: uma rede Q minúscula (MLP manual)

class QNet:
    def __init__(self, n_in, n_hidden, n_actions, rng):
        self.W1 = [[rng.gauss(0, 0.3) for _ in range(n_in)] for _ in range(n_hidden)]
        self.b1 = [0.0] * n_hidden
        self.W2 = [[rng.gauss(0, 0.3) for _ in range(n_hidden)] for _ in range(n_actions)]
        self.b2 = [0.0] * n_actions
    def forward(self, x):
        h = [max(0.0, sum(w * xi for w, xi in zip(row, x)) + b) for row, b in zip(self.W1, self.b1)]
        q = [sum(w * hi for w, hi in zip(row, h)) + b for row, b in zip(self.W2, self.b2)]
        return q, h

Passagem direta (forward pass): linear → ReLU → linear. Essa é a rede inteira.

Passo 3: a atualização do DQN

def train_step(online, target, batch, gamma, lr):
    grads = zeros_like(online)
    for s, a, r, s_next, done in batch:
        q, h = online.forward(s)
        if done:
            y = r
        else:
            q_next, _ = target.forward(s_next)
            y = r + gamma * max(q_next)
        td_error = q[a] - y
        accumulate_grads(grads, online, s, h, a, td_error)
    apply_sgd(online, grads, lr / len(batch))

O formato é o Q-learning da Lição 04 com duas diferenças: (a) fazemos retropropagação (backprop) através de um Q(·; θ) diferenciável em vez de indexar uma tabela, (b) o alvo usa Q(·; θ^-).

Passo 4: o loop externo

Para cada episódio, aja de forma ε-greedy em relação a Q(·; θ), insira as transições no buffer, amostre um minibatch, dê um passo de gradiente e sincronize periodicamente θ^- ← θ. O padrão:

for episode in range(N):
    s = env.reset()
    while not done:
        a = epsilon_greedy(online, s, epsilon)
        s_next, r, done = env.step(s, a)
        buffer.push(s, a, r, s_next, done)
        if len(buffer) >= batch:
            train_step(online, target, buffer.sample(batch), gamma, lr)
        if steps % sync_every == 0:
            target = copy(online)
        s = s_next

Em nosso minúsculo GridWorld com um estado one-hot de 16 dimensões, o agente aprende uma política quase ideal em ~500 episódios. No Atari, dimensione isso para 200 milhões de frames e adicione um extrator de características CNN.

Armadilhas

  • Tríade mortal. A aproximação de funções + off-policy + bootstrapping podem divergir. O DQN atenua isso com rede alvo + replay buffer; não remova nenhum dos dois.
  • Exploração. ε deve decair, tipicamente de 1.0 para 0.01 ao longo dos primeiros ~10% do treinamento. Sem exploração inicial suficiente, a rede Q converge para um mínimo local.
  • Superestimação. O max sobre um Q ruidoso apresenta um viés para cima. Sempre use Double DQN em produção.
  • Escala de recompensa. Recorte ou normalize as recompensas; a magnitude do gradiente é proporcional à magnitude da recompensa.
  • Início frio do replay buffer (coldstart). Não treine até que o buffer tenha algumas milhares de transições. Gradientes iniciais em ~20 amostras causam overfitting.
  • Frequência de sincronização do alvo. Muito frequente ≈ nenhuma rede alvo; muito infrequente ≈ alvos obsoletos. O DQN do Atari usa 10.000 passos de ambiente. Regra prática: sincronize a cada ~1/100 do horizonte de treinamento.
  • Pré-processamento de observação. O DQN do Atari empilha 4 frames para tornar o estado Markoviano. Qualquer ambiente com informações de velocidade precisa de empilhamento de frames ou estado recorrente.

Uso

Em 2026, o DQN raramente é o estado da arte, mas continua sendo o algoritmo de referência off-policy:

Tarefa Método de escolha Por que não o DQN?
Ação discreta semelhante ao Atari Rainbow DQN ou Muesli Mesmo framework, mais truques.
Controle contínuo SAC / TD3 (Fase 9 · 07) O DQN não possui rede de política.
On-policy / alto throughput PPO (Fase 9 · 08) Sem replay buffer; mais fácil de escalar.
RL offline CQL / IQL / Decision Transformer Alvos Q conservadores, sem explosões de bootstrapping.
Grandes espaços de ações discretas (recomendador) DQN com embedding de ação ou IMPALA Funciona bem; os detalhes adicionais importam.
RL de LLM PPO / GRPO Nível de sequência, não nível de passo; perda diferente.

As lições ainda se aplicam. Redes de replay e alvo aparecem no SAC, TD3, DDPG, SAC-X, no buffer de self-play do AlphaZero e em todos os métodos de RL offline. O recorte de recompensa vive como normalização de vantagem no PPO. A arquitetura é o modelo padrão.

Ship It

Save as outputs/skill-dqn-trainer.md:

---
name: dqn-trainer
description: Produce a DQN training config (buffer, target sync, ε schedule, reward clipping) for a discrete-action RL task.
version: 1.0.0
phase: 9
lesson: 5
tags: [rl, dqn, deep-rl]
---

Given a discrete-action environment (observation shape, action count, horizon, reward scale), output:

1. Network. Architecture (MLP / CNN / Transformer), feature dim, depth.
2. Replay buffer. Capacity, minibatch size, warmup size.
3. Target network. Sync strategy (hard every C steps or soft τ).
4. Exploration. ε start / end / schedule length.
5. Loss. Huber vs MSE, gradient clip value, reward clipping rule.
6. Double DQN. On by default unless explicit reason to disable.

Refuse to ship a DQN with no target network, no replay buffer, or ε held at 1. Refuse continuous-action tasks (route to SAC / TD3). Flag any reward range > 10× per-step mean as needing clipping or scale normalization.

Exercícios

  1. Fácil. Execute code/main.py. Plote a curva de retorno por episódio. Quantos episódios são necessários até que a média móvel ultrapasse -10?
  2. Médio. Desative a rede alvo (use a rede online para ambos os lados do alvo de Bellman). Meça a instabilidade do treinamento — o retorno oscila ou diverge?
  3. Difícil. Adicione Double DQN: use a rede online para selecionar argmax a' e a rede alvo para avaliar. Compare o viés de Q(s_0, best_a) vs o V*(s_0) real após 1.000 episódios com vs sem Double DQN em um GridWorld com recompensa ruidosa.

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
DQN "Deep Q-learning" Q-learning com uma função Q neural, replay buffer e rede alvo.
Experience replay "Transições embaralhadas" Buffer circular amostrado uniformemente a cada passo de gradiente; decorrelaciona os dados.
Target network "Bootstrap congelado" Cópia periódica de Q usada no alvo de Bellman; estabiliza o treinamento.
Tríade mortal "Por que o RL diverge" Aproximação de funções + bootstrapping + off-policy = sem garantia de convergência.
Double DQN "Correção para o viés de maximização" A rede online seleciona a ação, a rede alvo a avalia.
Dueling DQN "Cabeças V e A" Decompõe Q = V + A - média(A); mesma saída, melhor fluxo de gradiente.
Rainbow "Todos os truques" DDQN + PER + dueling + n-passos + noisy + distribucional em um só.
PER "Replay priorizado" Amostra transições proporcionalmente à magnitude do erro TD.

Leitura Adicional

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