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 entrenó una red de Q-learning en píxeles brutos, superando a todos los agentes de RL clásicos en siete juegos de Atari. 2015: extendido a 49 juegos, publicado en Nature, lo que dio inicio a la era del deep-RL. DQN es Q-learning más tres trucos que hacen que la aproximación de funciones sea estable.

Tipo: Build Lenguajes: Python Prerrequisitos: Fase 3 · 03 (Backpropagation), Fase 9 · 04 (Q-learning, SARSA) Tiempo: ~75 minutos

El Problema

El Q-learning tabular necesita un valor Q separado para cada par (estado, acción). Un tablero de ajedrez tiene ~10⁴³ estados. Un fotograma de Atari tiene 210×160×3 = 100,800 características. El RL tabular falla con miles de estados, y mucho más con miles de millones.

La solución es obvia en retrospectiva: reemplazar la tabla Q con una red neuronal, Q(s, a; θ). Pero lo obvio en retrospectiva tardó décadas. La aproximación ingenua de funciones con Q-learning diverge bajo la "triada mortal" — aproximación de funciones + bootstrapping + aprendizaje off-policy. Mnih et al. (2013, 2015) identificaron tres trucos de ingeniería que estabilizaron el aprendizaje:

  1. Experience replay decorrelaciona las transiciones.
  2. Red objetivo (target network) congela el objetivo del bootstrap.
  3. Recorte de recompensa (reward clipping) normaliza las magnitudes de los gradientes.

DQN en Atari fue la primera vez que una única arquitectura con un único conjunto de hiperparámetros resolvió decenas de problemas de control a partir de píxeles brutos. Todo lo que el "deep-RL" ha construido desde entonces — DDQN, Rainbow, Dueling, Distributional, R2D2, Agent57 — está apilado sobre esta base de tres trucos.

El Concepto

Bucle de entrenamiento de DQN: env, replay buffer, red online, red objetivo, pérdida TD de Bellman

El objetivo. DQN minimiza la pérdida TD de un paso en una función Q neuronal:

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

θ = red online, actualizada a cada paso por descenso de gradiente. θ^- = red objetivo, copiada periódicamente de θ (cada ~10,000 pasos). D = replay buffer de transiciones pasadas.

Los tres trucos, en orden de importancia:

Experience replay. Un búfer circular de ~10⁶ transiciones. Cada paso de entrenamiento obtiene un minibatch de forma uniforme y aleatoria. Esto rompe la correlación temporal (los fotogramas sucesivos son casi idénticos), permite que la red aprenda de transiciones de recompensa poco comunes muchas veces y decorrelaciona las actualizaciones de gradiente consecutivas. Sin esto, el TD on-policy con una red neuronal diverge en Atari.

Red objetivo. Usar la misma red Q(·; θ) en ambos lados de la ecuación de Bellman hace que el objetivo se mueva con cada actualización — "perseguir su propia cola". La solución: mantener una segunda red Q(·; θ^-) con pesos congelados. Cada C pasos, se copia θ → θ^-. Esto estabiliza el objetivo de regresión durante miles de pasos de gradiente a la vez. Las actualizaciones suaves (soft updates) θ^- ← τ θ + (1-τ) θ^- (utilizadas en DDPG, SAC) son una variante más suave.

Recorte de recompensa. Las magnitudes de las recompensas en Atari varían de 1 a más de 1000. Recortar a {-1, 0, +1} evita que un solo juego domine el gradiente. Es incorrecto cuando la magnitud de la recompensa importa; es adecuado para Atari, donde solo importa el signo.

Double DQN. Hasselt (2016) corrige el sesgo de maximización: utiliza la red online para seleccionar la acción y la red objetivo para evaluarla.

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

Reemplazo directo, consistentemente mejor. Úselo por defecto.

Otras mejoras (Rainbow, 2017): replay priorizado (prioritized replay - muestrea más las transiciones con alto error TD), arquitectura dueling (cabezales separados para V(s) y ventaja), redes ruidosas (noisy networks - exploración aprendida), retornos de n-pasos (n-step returns), Q distribucional (distributional Q - C51/QR-DQN), bootstrapping de múltiples pasos. Cada una añade unos pocos puntos porcentuales; las ganancias son aproximadamente aditivas.

Build It

El código aquí utiliza únicamente la biblioteca estándar (stdlib) sin numpy — usamos un MLP con una sola capa oculta implementado a mano en un pequeño GridWorld continuo, por lo que cada paso de entrenamiento se ejecuta en microsegundos. El algoritmo es idéntico al DQN de Atari a escala.

Paso 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)

Capacidad de ~50,000 para Atari; 5,000 es suficiente para nuestro entorno de prueba (toy env).

Paso 2: una red Q diminuta (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

Paso hacia adelante (forward pass): lineal → ReLU → lineal. Esa es la red completa.

Paso 3: la actualización de 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))

La estructura es el Q-learning de la Lección 04 con dos diferencias: (a) hacemos retropropagación (backprop) a través de un Q(·; θ) diferenciable en lugar de indexar una tabla, (b) el objetivo utiliza Q(·; θ^-).

Paso 4: el bucle externo

Para cada episodio, actúe de forma ε-greedy con respecto a Q(·; θ), inserte las transiciones en el búfer, extraiga un minibatch, dé un paso de gradiente y sincronice periódicamente θ^- ← θ. El patrón:

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

En nuestro diminuto GridWorld con un estado one-hot de 16 dimensiones, el agente aprende una política casi óptima en ~500 episodios. En Atari, escale esto a 200 millones de fotogramas y agregue un extractor de características CNN.

Errores Comunes

  • Tríada mortal. La aproximación de funciones + off-policy + bootstrapping pueden divergir. DQN mitiga esto con red objetivo + replay; no elimine ninguno de los dos.
  • Exploración. ε debe decaer, típicamente de 1.0 a 0.01 durante el primer ~10% del entrenamiento. Sin suficiente exploración inicial, la red Q converge a un mínimo local.
  • Sobreevaluación. El max sobre una Q ruidosa tiene un sesgo hacia arriba. Siempre use Double DQN en producción.
  • Escala de recompensa. Recorte o normalice las recompensas; la magnitud del gradiente es proporcional a la magnitud de la recompensa.
  • Inicio en frío del replay buffer (coldstart). No entrene hasta que el búfer tenga unos pocos miles de transiciones. Los gradientes iniciales en ~20 muestras causan sobreajuste (overfitting).
  • Frecuencia de sincronización del objetivo. Muy frecuente ≈ sin red objetivo; muy poco frecuente ≈ objetivos obsoletos. El DQN de Atari utiliza 10,000 pasos de entorno. Regla general: sincronice cada ~1/100 del horizonte de entrenamiento.
  • Preprocesamiento de observaciones. El DQN de Atari apila 4 fotogramas para hacer que el estado sea de Markov. Cualquier entorno con información de velocidad necesita apilamiento de fotogramas o un estado recurrente.

Uso

En 2026, DQN rara vez es el estado del arte, pero sigue siendo el algoritmo de referencia off-policy:

Tarea Método de elección ¿Por qué no DQN?
Acción discreta similar a Atari Rainbow DQN o Muesli Mismo framework, más trucos.
Control continuo SAC / TD3 (Fase 9 · 07) DQN no tiene red de política.
On-policy / alto rendimiento (high-throughput) PPO (Fase 9 · 08) Sin replay buffer; más fácil de escalar.
RL offline CQL / IQL / Decision Transformer Objetivos Q conservadores, sin explosiones de bootstrapping.
Grandes espacios de acción discreta (recomendador) DQN con embedding de acción o IMPALA Funciona bien; los detalles adicionales importan.
RL de LLM PPO / GRPO A nivel de secuencia, no a nivel de paso; pérdida diferente.

Las lecciones aún se aplican. Las redes de replay y objetivo aparecen en SAC, TD3, DDPG, SAC-X, en el búfer de self-play de AlphaZero y en todos los métodos de RL offline. El recorte de recompensa sigue vivo como normalización de ventajas en PPO. La arquitectura es el modelo de referencia.

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.

Ejercicios

  1. Fácil. Ejecute code/main.py. Grafique la curva de retorno por episodio. ¿Cuántos episodios se necesitan hasta que la media móvil supere -10?
  2. Medio. Desactive la red objetivo (use la red online para ambos lados del objetivo de Bellman). Mida la inestabilidad del entrenamiento: ¿el retorno oscila o diverge?
  3. Difícil. Agregue Double DQN: use la red online para seleccionar argmax a' y la red objetivo para evaluar. Compare el sesgo de Q(s_0, best_a) frente al V*(s_0) real después de 1,000 episodios con frente a sin Double DQN en un GridWorld con recompensa ruidosa.

Términos Clave

Término Lo que la gente dice Lo que realmente significa
DQN "Deep Q-learning" Q-learning con una función Q neuronal, replay buffer y red objetivo.
Experience replay "Transiciones barajadas" Búfer circular muestreado de manera uniforme en cada paso de gradiente; decorrelaciona los datos.
Target network "Bootstrap congelado" Copia periódica de Q utilizada en el objetivo de Bellman; estabiliza el entrenamiento.
Tríada mortal "Por qué la RL diverge" Aproximación de funciones + bootstrapping + off-policy = sin garantía de convergencia.
Double DQN "Solución para el sesgo de maximización" La red online selecciona la acción, la red objetivo la evalúa.
Dueling DQN "Cabezales V y A" Descompone Q = V + A - media(A); misma salida, mejor flujo de gradiente.
Rainbow "Todos los trucos" DDQN + PER + dueling + n-pasos + noisy + distribucional en uno solo.
PER "Replay priorizado" Muestrea transiciones proporcionalmente a la magnitud del error TD.

Lectura Adicional

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