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:
- Experience replay decorrelaciona as transições.
- Rede alvo (target network) congela o alvo do bootstrap.
- 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
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
maxsobre 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
- 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? - 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?
- Difícil. Adicione Double DQN: use a rede online para selecionar
argmax a'e a rede alvo para avaliar. Compare o viés deQ(s_0, best_a)vs oV*(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
- Mnih et al. (2013). Playing Atari with Deep Reinforcement Learning — o artigo do workshop do NeurIPS de 2013 que deu início ao deep RL.
- Mnih et al. (2015). Human-level control through deep reinforcement learning — o artigo da Nature, DQN de 49 jogos.
- Hasselt, Guez, Silver (2016). Deep Reinforcement Learning with Double Q-learning — DDQN.
- Wang et al. (2016). Dueling Network Architectures — dueling DQN.
- Hessel et al. (2018). Rainbow: Combining Improvements in Deep RL — o artigo de combinação de truques.
- OpenAI Spinning Up — DQN — exposição moderna clara.
- Sutton & Barto (2018). Ch. 9 — On-policy Prediction with Approximation — o tratamento clássico do livro-texto sobre a "tríade mortal" (aproximação de funções + bootstrapping + off-policy) que a rede alvo e o replay buffer do DQN foram projetados para conter.
- CleanRL DQN implementation — implementação de referência do DQN em arquivo único usada em estudos de ablação; boa para ler junto com a versão do zero desta lição.