Aprendizaje por Refuerzo
De los fundamentos de procesos de Markov a los agentes modernos: Q-Learning, Deep Q-Networks (DQN), Policy Gradients, Actor-Critic, PPO, SAC, RLHF para LLMs, y aplicaciones en robótica, conducción autónoma y más.
¿Qué es el Aprendizaje por Refuerzo?
El Aprendizaje por Refuerzo (Reinforcement Learning, RL) es el tercer gran paradigma del Machine Learning. A diferencia del aprendizaje supervisado —donde un modelo aprende de pares (entrada, etiqueta)— y del no supervisado —donde se buscan patrones sin etiquetas—, en RL un agente aprende a tomar decisiones secuenciales interactuando con un entorno, recibiendo recompensas (o penalizaciones) por sus acciones.
Los tres paradigmas del Machine Learning
El aprendizaje por refuerzo ocupa un lugar único entre los paradigmas del ML. Mientras que en supervisado se dispone de la "respuesta correcta" y en no supervisado no hay señal de retroalimentación, en RL el agente recibe una señal escalar retardada —la recompensa— que no le dice qué debería haber hecho, solo cuánto de bien lo hizo.
| Característica | Supervisado | No supervisado | Refuerzo |
|---|---|---|---|
| Datos | Pares (x, y) etiquetados | Solo x, sin etiquetas | Interacciones (s, a, r, s') |
| Señal | Error directo (label) | Ninguna | Recompensa escalar retardada |
| Objetivo | Minimizar error de predicción | Descubrir estructura | Maximizar recompensa acumulada |
| Feedback | Inmediato y completo | No hay | Retardado y parcial |
| Datos | Dataset estático (i.i.d.) | Dataset estático | Generados por el agente (no i.i.d.) |
| Exploración | No necesaria | No necesaria | Fundamental (explore vs exploit) |
| Ejemplo | Clasificar imágenes | Clustering, PCA | Jugar al ajedrez, control robótico |
Casos de uso del Aprendizaje por Refuerzo
El RL brilla en problemas donde se necesita tomar decisiones secuenciales y donde no existe un dataset etiquetado de "decisiones óptimas". Estos son los dominios más importantes:
Historia y hitos del Aprendizaje por Refuerzo
- Sutton & Barto (2018): "Reinforcement Learning: An Introduction" (2ª ed.) — Libro gratuito online. La biblia del RL.
- David Silver (2015): Curso de RL de DeepMind/UCL — Lectures
- Spinning Up (OpenAI): Guía educativa de RL profundo con implementaciones.
- Gymnasium (Farama): Entornos estándar de RL (antes OpenAI Gym).
- Stable Baselines3: Implementaciones fiables de algoritmos RL en PyTorch.
Taxonomía del Aprendizaje por Refuerzo
El campo del RL es amplio. Esta taxonomía te ayuda a situar cada algoritmo en su contexto:
🔍 ¿Model-Free o Model-Based?
- Model-Free: el agente no intenta aprender cómo funciona el entorno. Aprende directamente una política o función de valor por prueba y error. Más simple, más robusto, pero necesita muchas interacciones.
- Model-Based: el agente aprende un modelo del entorno (cómo transiciona entre estados) y lo usa para planificar. Más eficiente en datos, pero más complejo y propenso a errores del modelo.
- Value-Based: aprende el "valor" de cada estado o acción, y actúa de forma greedy (elige la acción con mayor valor).
- Policy-Based: aprende directamente la política \(\pi(a|s)\) sin calcular valores explícitamente.
- Actor-Critic: lo mejor de ambos mundos — un actor (política) y un critic (valor) que se entrenan juntos.
Procesos estocásticos y Cadenas de Markov
Para formalizar el RL necesitamos herramientas de la teoría de la probabilidad. Todo comienza con los procesos estocásticos: secuencias de variables aleatorias que evolucionan en el tiempo.
📐 Proceso estocástico
Un proceso estocástico es una colección de variables aleatorias \(\{X_t\}_{t \geq 0}\) indexadas por el tiempo \(t\). Cada \(X_t\) toma valores en un espacio de estados \(\mathcal{S}\). El proceso describe cómo el sistema evoluciona de forma probabilística a lo largo del tiempo.
Un caso especial extremadamente importante es cuando el futuro solo depende del presente, no del pasado. Esta es la propiedad de Markov:
Cadena de Markov
Una Cadena de Markov es un proceso estocástico con la propiedad de Markov, un espacio de estados discreto \(\mathcal{S} = \{s_1, s_2, \ldots, s_n\}\), y transiciones en tiempo discreto. Se define completamente por:
- Un conjunto de estados \(\mathcal{S}\)
- Una matriz de transición \(\mathbf{P}\) donde \(P_{ij} = P(X_{t+1} = s_j \mid X_t = s_i)\)
- Una distribución inicial \(\mu_0\) sobre los estados
Proceso de Decisión de Markov (MDP)
El MDP extiende la Cadena de Markov añadiendo acciones y recompensas. Es el marco matemático fundamental de todo el RL. Un MDP es una tupla \(\langle \mathcal{S}, \mathcal{A}, P, R, \gamma \rangle\):
| Símbolo | Nombre | Descripción |
|---|---|---|
| \(\mathcal{S}\) | Espacio de estados | Conjunto de todos los estados posibles del entorno. |
| \(\mathcal{A}\) | Espacio de acciones | Conjunto de todas las acciones que el agente puede tomar. |
| \(P(s'|s,a)\) | Función de transición | Probabilidad de llegar al estado \(s'\) al tomar la acción \(a\) en el estado \(s\). |
| \(R(s,a,s')\) | Función de recompensa | Recompensa inmediata al transicionar de \(s\) a \(s'\) con la acción \(a\). |
| \(\gamma \in [0,1)\) | Factor de descuento | Cuánto importan las recompensas futuras vs las inmediatas. |
La dinámica del MDP
En cada paso de tiempo \(t\):
Ejemplo: GridWorld
Un clásico ejemplo de MDP es el GridWorld: una cuadrícula donde un agente se mueve con acciones {↑, ↓, ←, →}, busca alcanzar la meta (+1) evitando trampas (−1).
Política, retorno y objetivo del RL
Política \(\pi\)
La política es la estrategia del agente. Define qué acción tomar en cada estado. Puede ser:
- Determinista: \(\pi(s) = a\) — mapea cada estado a exactamente una acción.
- Estocástica: \(\pi(a|s) = P(A_t = a \mid S_t = s)\) — da una distribución de probabilidad sobre acciones.
Retorno \(G_t\)
El retorno es la recompensa total acumulada (descontada) desde el paso \(t\) en adelante:
El factor de descuento \(\gamma\) asegura que la serie converge (para recompensas acotadas) y modela que las recompensas cercanas son más valiosas que las lejanas.
🎯 Objetivo del RL
Encontrar la política \(\pi^*\) que maximice el retorno esperado:
El agente no solo quiere recompensas altas ahora, sino a lo largo de toda su vida. Este es el principio de optimización a largo plazo que distingue al RL de la optimización greedy.
🎛️ Widget: Explorador del factor de descuento \(\gamma\)
Visualiza cómo \(\gamma\) afecta al peso de las recompensas futuras. Con recompensa constante \(r=1\) en cada paso:
Funciones de valor: \(V(s)\) y \(Q(s,a)\)
Las funciones de valor son el concepto central del RL. Responden a la pregunta: "¿cuánta recompensa acumulada puedo esperar desde aquí?"
State-Value Function \(V^\pi(s)\)
Action-Value Function \(Q^\pi(s,a)\)
🔗 Relación entre \(V\) y \(Q\)
Las funciones \(V\) y \(Q\) están íntimamente relacionadas:
Es decir, el valor de un estado es el promedio ponderado del valor de cada acción, ponderado por la probabilidad de tomar esa acción según la política.
Y la política óptima simplemente elige la acción con mayor Q-value:
Las ecuaciones de Bellman
Richard Bellman (1957) descubrió la relación recursiva fundamental de las funciones de valor. Las ecuaciones de Bellman expresan que el valor de un estado se puede descomponer en la recompensa inmediata más el valor descontado del siguiente estado.
Ecuación de Bellman para \(V^\pi\)
"El valor de un estado = recompensa esperada + γ × valor esperado del siguiente estado". Esta es una ecuación recursiva: \(V\) se define en términos de sí misma.
Ecuación de Bellman para \(Q^\pi\)
Ecuaciones de optimalidad de Bellman
Para la política óptima \(\pi^*\), las ecuaciones se simplifican porque la política greedy siempre elige la mejor acción:
La ecuación de optimalidad de Bellman para \(Q^*\) es la base del Q-Learning. El \(\max_{a'}\) reemplaza la media ponderada por la política porque la política óptima siempre elige la mejor acción.
📝 Resumen de ecuaciones clave
| Ecuación | Fórmula | Qué nos dice |
|---|---|---|
| Bellman \(V^\pi\) | \(V^\pi(s) = \mathbb{E}_\pi[r + \gamma V^\pi(s')]\) | Valor de un estado bajo política \(\pi\) |
| Bellman \(Q^\pi\) | \(Q^\pi(s,a) = \mathbb{E}[r + \gamma \mathbb{E}_{a'}[Q^\pi(s',a')]]\) | Valor de una acción bajo política \(\pi\) |
| Optimalidad \(V^*\) | \(V^*(s) = \max_a \mathbb{E}[r + \gamma V^*(s')]\) | Valor óptimo de un estado |
| Optimalidad \(Q^*\) | \(Q^*(s,a) = \mathbb{E}[r + \gamma \max_{a'} Q^*(s',a')]\) | Base del Q-Learning |
Episodios, trayectorias y horizonte
Consideremos un episodio con recompensas \(r_1 = +1, r_2 = -1, r_3 = +5, r_4 = 0\) y \(\gamma = 0.9\):
Observa cómo \(G_t\) cambia en cada paso porque las recompensas futuras son diferentes.
Q-Learning: el algoritmo fundamental
Q-Learning (Watkins, 1989) es el algoritmo más influyente de RL. Es un método off-policy y model-free que aprende la función de valor-acción óptima \(Q^*(s,a)\) directamente, sin necesidad de conocer el modelo del entorno.
Donde \(\alpha \in (0,1]\) es la tasa de aprendizaje y el término entre corchetes es el error de diferencia temporal (TD error): la diferencia entre lo que el agente observó y lo que esperaba.
📖 Interpretación intuitiva
La regla dice: "ajusta tu estimación de \(Q(s,a)\) hacia la recompensa inmediata + mejor valor futuro estimado". Es como corregir tus predicciones cada vez que tienes nueva información.
- \(\alpha\) grande: aprende rápido pero olvida rápido (inestable).
- \(\alpha\) pequeño: aprende lento pero estable.
- TD error = 0: la estimación es perfecta, no hay que ajustar.
- TD error > 0: la realidad fue mejor de lo esperado → aumentar Q.
- TD error < 0: la realidad fue peor de lo esperado → disminuir Q.
Propiedades del Q-Learning
Exploración: la estrategia ε-greedy
Si el agente siempre elige la acción con mayor Q-value (greedy), nunca descubrirá acciones potencialmente mejores. La estrategia ε-greedy resuelve el dilema exploración-explotación:
Con \(\varepsilon = 0.1\), el agente explora (acción aleatoria) el 10% del tiempo y explota (mejor acción conocida) el 90%. Normalmente se usa ε-decay: empezar con \(\varepsilon = 1.0\) (exploración pura) y decrecer gradualmente a \(\varepsilon_{\min} = 0.01\).
📉 Widget: Visualizador de ε-decay
Observa cómo \(\varepsilon\) decrece a lo largo de los episodios.
Q-Learning tabular
En la versión más simple, los valores \(Q(s,a)\) se almacenan en una tabla (Q-table) con una fila por cada estado y una columna por cada acción. El algoritmo completo es:
import numpy as np
import gymnasium as gym
# ═══════════════════════════════════════════════════
# Q-Learning tabular — FrozenLake
# ═══════════════════════════════════════════════════
env = gym.make("FrozenLake-v1", is_slippery=False)
# Hiperparámetros
alpha = 0.1 # Tasa de aprendizaje
gamma = 0.99 # Factor de descuento
epsilon = 1.0 # Exploración inicial
eps_min = 0.01 # Exploración mínima
eps_decay = 0.995 # Decay por episodio
n_episodes = 10000
# Inicializar Q-table: |S| × |A| = 16 × 4
n_states = env.observation_space.n # 16
n_actions = env.action_space.n # 4
Q = np.zeros((n_states, n_actions))
rewards_history = []
for episode in range(n_episodes):
state, _ = env.reset()
total_reward = 0
done = False
while not done:
# ε-greedy
if np.random.random() < epsilon:
action = env.action_space.sample() # Explorar
else:
action = np.argmax(Q[state]) # Explotar
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
# Actualización Q-Learning
td_target = reward + gamma * np.max(Q[next_state]) * (1 - terminated)
td_error = td_target - Q[state, action]
Q[state, action] += alpha * td_error
state = next_state
total_reward += reward
# Decay epsilon
epsilon = max(eps_min, epsilon * eps_decay)
rewards_history.append(total_reward)
# Mostrar Q-table final
print("Q-table aprendida:")
print(Q.round(2))
print(f"\nÉxito últimos 100 eps: {np.mean(rewards_history[-100:]):.0%}")
🗺️ Widget: Visualizador de Q-Table (GridWorld 4×4)
Simula Q-Learning en un GridWorld simple. Observa cómo la Q-table converge.
Limitaciones del Q-Learning tabular
El Q-Learning tabular funciona bien en entornos pequeños, pero se vuelve impracticable a medida que el espacio de estados crece:
| Entorno | Estados | Acciones | Entradas Q-table | ¿Tabular viable? |
|---|---|---|---|---|
| FrozenLake 4×4 | 16 | 4 | 64 | ✅ Trivial |
| Taxi-v3 | 500 | 6 | 3,000 | ✅ Fácil |
| Blackjack | ~700 | 2 | 1,400 | ✅ Fácil |
| Ajedrez | ~10⁴⁷ | ~35 | ~10⁴⁸ | ❌ Imposible |
| Atari (píxeles) | ~10⁶⁸⁰⁰⁰ | 18 | ~10⁶⁸⁰⁰¹ | ❌ Imposible |
| Robótica (continuo) | ∞ (ℝⁿ) | ∞ (ℝᵐ) | ∞ | ❌ Imposible |
🚫 Los tres problemas del Q-Learning tabular
- Maldición de la dimensionalidad: la Q-table crece exponencialmente con la dimensión del estado. Un juego de Atari con frames 210×160×3 tiene un espacio de estados astronómico.
- Sin generalización: aprender \(Q(s_1, a)\) no dice nada sobre \(Q(s_2, a)\), incluso si \(s_1\) y \(s_2\) son casi idénticos. Cada entrada es independiente.
- Estados continuos: con estados como posición \((x, y) \in \mathbb{R}^2\) o velocidades, no se puede indexar una tabla discreta.
SARSA: la alternativa on-policy
SARSA (State-Action-Reward-State-Action) es similar a Q-Learning, pero es on-policy: actualiza usando la acción que realmente tomará el agente (no la mejor posible).
La diferencia con Q-Learning: usa \(Q(s_{t+1}, a_{t+1})\) en lugar de \(\max_{a'} Q(s_{t+1}, a')\). El agente actualiza usando la acción que realmente tomó, no la mejor posible.
| Propiedad | Q-Learning | SARSA |
|---|---|---|
| Tipo | Off-policy | On-policy |
| Target | \(r + \gamma \max_{a'} Q(s', a')\) | \(r + \gamma Q(s', a')\) donde \(a'\) es la acción real |
| Política aprendida | Óptima \(Q^*\) (greedy) | Q-value de la política seguida (ε-greedy) |
| Exploración | Separada del aprendizaje | Integrada en el aprendizaje |
| Seguridad | Más arriesgado (asume acciones óptimas) | Más conservador (cuenta el riesgo de explorar) |
| Convergencia | A \(\pi^*\) (greedy óptima) | A la Q-value de la política ε-greedy |
En CliffWalking, el agente debe ir de un punto a otro bordeando un acantilado. Caer al acantilado da recompensa −100. El camino óptimo es bordear el acantilado (más rápido pero arriesgado con ε-greedy). El camino seguro es ir por arriba.
- Q-Learning aprende el camino óptimo (bordeando el acantilado) porque su target usa \(\max\) y asume que seguirá la política greedy.
- SARSA aprende el camino seguro (por arriba) porque sabe que con ε-greedy podría explorar y caer al acantilado.
SARSA es "más realista" porque tiene en cuenta que la política exploratoria puede causar errores.
De la Q-Table a la red neuronal (DQN)
La idea fundamental del Deep Q-Network (Mnih et al., 2013/2015) es reemplazar la Q-table por una red neuronal \(Q_\theta(s, a)\) que aproxima la función \(Q^*\). Esto permite manejar espacios de estados enormes (como píxeles) y generalizar a estados nunca vistos.
🏗️ Diseño de la red DQN
- Input: el estado \(s\), codificado como un vector numérico (features del entorno, píxeles aplanados, frame stacking…).
- Capas ocultas: Linear → ReLU (típicamente 2-3 capas de 64-512 neuronas). Para imágenes, CNN como extractor de features.
- Output: un valor \(Q(s, a_i)\) por cada acción posible. Capa linear sin activación (los Q-values pueden ser negativos).
- Dimensión output: \(|\mathcal{A}|\) — el número de acciones discretas posibles.
- Acción elegida: \(\arg\max_a Q_\theta(s, a)\) (en explotación).
Codificación del estado
La forma en que se codifica el estado es crítica para el rendimiento del DQN. El estado debe contener toda la información relevante para tomar una decisión.
| Entorno | Estado crudo | Codificación para DQN | Dimensión |
|---|---|---|---|
| CartPole | Posición, velocidad, ángulo, vel. angular | Vector \([x, \dot{x}, \theta, \dot{\theta}]\) | 4 |
| Snake | Posición cabeza, comida, dirección, cuerpo | Vector de distancias/booleanos: [peligro_frente, peligro_der, ...] | 11-15 |
| Atari Breakout | Frame RGB 210×160×3 | 4 frames grises 84×84 apilados | 4×84×84 |
| Pong DRL | Posiciones de palas y pelota | Vector \([y_{pad}, y_{ball}, x_{ball}, v_{x}, v_{y}]\) | 5-8 |
| Trading | Histórico de precios | Ventana de n precios normalizados + indicadores | ~50-200 |
Para juegos de Atari, el DQN original usa 4 frames consecutivos (en escala de grises, redimensionados a 84×84) como input. Esto permite a la red "ver" la velocidad y dirección de los objetos (un solo frame no tiene información de movimiento). La red usa capas convolucionales como feature extractor:
- Conv2d(4, 32, kernel=8, stride=4) → ReLU
- Conv2d(32, 64, kernel=4, stride=2) → ReLU
- Conv2d(64, 64, kernel=3, stride=1) → ReLU
- Flatten → Linear(3136, 512) → ReLU
- Linear(512, |A|) → Q-values
Diseño de recompensas (Reward Shaping)
El diseño de la función de recompensa es una de las partes más críticas (y artesanales) del RL. Una mala recompensa lleva a comportamientos inesperados o a que el agente no aprenda.
| Entorno | Recompensa simple | Recompensa mejorada (shaped) |
|---|---|---|
| Snake | +1 por comer, −1 por morir | +10 comer, −10 morir, −0.01/paso, +0.1 acercarse a comida, −0.1 alejarse |
| Pong | +1 punto, −1 punto rival | Suficiente con reward esparsa; la pelota da feedback natural |
| Robótica (reach) | +1 si toca el target | −distancia al target (reward densa), +10 si lo toca |
| CartPole | +1 por cada paso que sobrevive | Funciona bien con reward esparsa |
- Reward hacking: el agente encuentra una forma de obtener recompensa que no era la intención (e.g., dar vueltas en círculos para acumular "bonos por moverse").
- Reward esparsa: si la recompensa solo llega al final (e.g., ganar/perder), el agente tarda mucho en aprender porque la señal es muy débil.
- Reward demasiado densa: si se da demasiada señal intermedia, el agente puede optimizar los "bonos" en lugar del objetivo real.
- Escala incorrecta: si la recompensa por morir (−1) es mucho menor que la de comer (+10), el agente puede arriesgarse demasiado.
Experience Replay
En supervisado, los datos son i.i.d. (independientes e idénticamente distribuidos). En RL, las experiencias consecutivas \((s_t, a_t, r_{t+1}, s_{t+1})\) están altamente correlacionadas (el siguiente estado depende del anterior). Esto desestabiliza el entrenamiento de la red neuronal.
💾 Experience Replay Buffer
La solución es almacenar las experiencias en un buffer circular (replay buffer) y muestrear mini-batches aleatorios para entrenar. Esto rompe la correlación temporal y reutiliza experiencias pasadas.
Target Network y Soft Update
Un segundo problema de inestabilidad: al entrenar la red, el target \(r + \gamma \max_{a'} Q_\theta(s', a')\) también cambia porque depende de los mismos pesos \(\theta\) que estamos actualizando. Es como perseguir un objetivo que se mueve — inestable.
Se mantiene una segunda red (target network) con pesos \(\theta^-\) que se actualizan lentamente. El target se calcula con esta red "congelada":
Los pesos de la target network \(\theta^-\) se actualizan periódicamente mediante soft update o hard copy.
Soft Update (Polyak averaging)
Con \(\tau\) pequeño (típicamente \(\tau = 0.005\)), la target network se mueve muy lentamente hacia la red principal. Esto estabiliza el entrenamiento porque el target cambia gradualmente.
Hard Update
Alternativa: copiar \(\theta^- \leftarrow \theta\) cada \(C\) pasos (e.g., \(C=1000\)). El DQN original usaba hard update cada 10,000 frames.
🔬 Widget: Simulador de entrenamiento DQN
Visualiza cómo los componentes del DQN interactúan durante el entrenamiento.
La función de pérdida del DQN
El DQN se entrena como un problema de regresión: queremos que la predicción \(Q_\theta(s, a)\) se acerque al TD target.
Donde \(\ell\) es MSE o Huber loss, y \(d_i\) es 1 si \(s'_i\) es terminal. El TD target se calcula con la target network y se detiene el gradiente (no se propaga backprop al target).
⚙️ ¿Por qué Huber Loss en lugar de MSE?
La Huber Loss (smooth L1 loss) es más robusta a outliers que MSE. En RL, los TD errors pueden ser muy grandes al inicio del entrenamiento. Huber Loss se comporta como MSE para errores pequeños y como MAE para errores grandes, evitando gradientes explosivos.
El algoritmo DQN completo
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import gymnasium as gym
from collections import deque
import random
# ═══════════════════════════════════════════════════
# 1. Red neuronal Q-Network
# ═══════════════════════════════════════════════════
class QNetwork(nn.Module):
def __init__(self, state_dim, action_dim, hidden=128):
super().__init__()
self.net = nn.Sequential(
nn.Linear(state_dim, hidden),
nn.ReLU(),
nn.Linear(hidden, hidden),
nn.ReLU(),
nn.Linear(hidden, action_dim) # Sin activación
)
def forward(self, x):
return self.net(x)
# ═══════════════════════════════════════════════════
# 2. Replay Buffer
# ═══════════════════════════════════════════════════
class ReplayBuffer:
def __init__(self, capacity=100_000):
self.buffer = deque(maxlen=capacity)
def push(self, state, action, reward, next_state, done):
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size):
batch = random.sample(self.buffer, batch_size)
states, actions, rewards, next_states, dones = zip(*batch)
return (
torch.FloatTensor(np.array(states)),
torch.LongTensor(actions),
torch.FloatTensor(rewards),
torch.FloatTensor(np.array(next_states)),
torch.FloatTensor(dones),
)
def __len__(self):
return len(self.buffer)
# ═══════════════════════════════════════════════════
# 3. Agente DQN
# ═══════════════════════════════════════════════════
class DQNAgent:
def __init__(self, state_dim, action_dim):
self.action_dim = action_dim
self.gamma = 0.99
self.tau = 0.005
self.epsilon = 1.0
self.eps_min = 0.01
self.eps_decay = 0.995
self.batch_size = 64
# Q-network y target network
self.q_net = QNetwork(state_dim, action_dim)
self.target_net = QNetwork(state_dim, action_dim)
self.target_net.load_state_dict(self.q_net.state_dict())
self.optimizer = optim.Adam(self.q_net.parameters(), lr=1e-3)
self.buffer = ReplayBuffer()
def select_action(self, state):
if random.random() < self.epsilon:
return random.randrange(self.action_dim)
with torch.no_grad():
q_values = self.q_net(torch.FloatTensor(state).unsqueeze(0))
return q_values.argmax(dim=1).item()
def train_step(self):
if len(self.buffer) < self.batch_size:
return None
states, actions, rewards, next_states, dones = \
self.buffer.sample(self.batch_size)
# Q-values actuales para las acciones tomadas
q_values = self.q_net(states).gather(1, actions.unsqueeze(1)).squeeze()
# TD target con target network (sin gradiente)
with torch.no_grad():
next_q = self.target_net(next_states).max(dim=1)[0]
targets = rewards + self.gamma * next_q * (1 - dones)
# Loss y backprop
loss = nn.SmoothL1Loss()(q_values, targets) # Huber Loss
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
# Soft update de target network
for p, tp in zip(self.q_net.parameters(),
self.target_net.parameters()):
tp.data.copy_(self.tau * p.data + (1 - self.tau) * tp.data)
return loss.item()
def decay_epsilon(self):
self.epsilon = max(self.eps_min, self.epsilon * self.eps_decay)
# ═══════════════════════════════════════════════════
# 4. Training loop
# ═══════════════════════════════════════════════════
env = gym.make("CartPole-v1")
agent = DQNAgent(state_dim=4, action_dim=2)
for episode in range(500):
state, _ = env.reset()
total_reward = 0
while True:
action = agent.select_action(state)
next_state, reward, term, trunc, _ = env.step(action)
done = term or trunc
agent.buffer.push(state, action, reward, next_state, float(done))
loss = agent.train_step()
state = next_state
total_reward += reward
if done:
break
agent.decay_epsilon()
if (episode + 1) % 50 == 0:
print(f"Ep {episode+1} | Reward: {total_reward:.0f} | "
f"ε: {agent.epsilon:.3f} | Buffer: {len(agent.buffer)}")
import tensorflow as tf
import numpy as np
import gymnasium as gym
from collections import deque
import random
# ═══════════════════════════════════════════════════
# DQN en TensorFlow/Keras — CartPole
# ═══════════════════════════════════════════════════
def build_q_network(state_dim, action_dim, hidden=128):
return tf.keras.Sequential([
tf.keras.layers.Dense(hidden, activation='relu', input_shape=(state_dim,)),
tf.keras.layers.Dense(hidden, activation='relu'),
tf.keras.layers.Dense(action_dim) # Sin activación
])
# Crear redes
q_net = build_q_network(4, 2)
target_net = build_q_network(4, 2)
target_net.set_weights(q_net.get_weights())
optimizer = tf.keras.optimizers.Adam(1e-3)
buffer = deque(maxlen=100_000)
gamma, tau, batch_size = 0.99, 0.005, 64
epsilon, eps_min, eps_decay = 1.0, 0.01, 0.995
@tf.function
def train_step(states, actions, rewards, next_states, dones):
# TD target
next_q = tf.reduce_max(target_net(next_states), axis=1)
targets = rewards + gamma * next_q * (1.0 - dones)
with tf.GradientTape() as tape:
q_values = q_net(states)
indices = tf.stack([tf.range(batch_size), actions], axis=1)
q_selected = tf.gather_nd(q_values, indices)
loss = tf.keras.losses.Huber()(targets, q_selected)
grads = tape.gradient(loss, q_net.trainable_variables)
optimizer.apply_gradients(zip(grads, q_net.trainable_variables))
# Soft update
for w, tw in zip(q_net.weights, target_net.weights):
tw.assign(tau * w + (1 - tau) * tw)
return loss
env = gym.make("CartPole-v1")
for episode in range(500):
state, _ = env.reset()
total_reward = 0
while True:
if random.random() < epsilon:
action = env.action_space.sample()
else:
q = q_net(tf.expand_dims(state, 0))
action = int(tf.argmax(q, axis=1))
ns, r, term, trunc, _ = env.step(action)
buffer.append((state, action, r, ns, float(term or trunc)))
state = ns
total_reward += r
if len(buffer) >= batch_size:
batch = random.sample(buffer, batch_size)
s, a, rw, ns2, d = map(np.array, zip(*batch))
train_step(
tf.constant(s, dtype=tf.float32),
tf.constant(a, dtype=tf.int32),
tf.constant(rw, dtype=tf.float32),
tf.constant(ns2, dtype=tf.float32),
tf.constant(d, dtype=tf.float32),
)
if term or trunc:
break
epsilon = max(eps_min, epsilon * eps_decay)
Variantes y mejoras del DQN
| Variante | Problema que resuelve | Idea clave |
|---|---|---|
| Double DQN | Sobreestimación de Q-values | Selecciona acción con \(Q_\theta\), evalúa con \(Q_{\theta^-}\): \(y = r + \gamma Q_{\theta^-}(s', \arg\max_{a'} Q_\theta(s', a'))\) |
| Dueling DQN | No todos los estados necesitan evaluar acciones | Separa en \(V(s)\) + Advantage \(A(s,a)\): \(Q(s,a) = V(s) + A(s,a) - \bar{A}\) |
| Prioritized Replay | No todas las experiencias son igual de informativas | Muestrea experiencias con mayor TD error con más frecuencia. |
| Noisy Nets | ε-greedy es ineficiente para explorar | Añade ruido aprendible a los pesos de la red para explorar. |
| Rainbow | Combinar todas las mejoras | Double + Dueling + Prioritized + Noisy + Multi-step + Distributional. |
Rainbow (Hessel et al., 2018) combina 6 mejoras independientes y demuestra que juntas superan a cada una por separado. Es considerado el estado del arte de los métodos DQN.
- Double DQN: reduce sobreestimación.
- Prioritized Replay: muestrea experiencias útiles.
- Dueling architecture: separa valor de ventaja.
- Multi-step returns: usa n-step returns en lugar de 1-step.
- Distributional RL: predice la distribución del retorno, no solo la media.
- Noisy Nets: exploración parametrizada.
Hessel, M. et al. (2018). "Rainbow: Combining Improvements in Deep Reinforcement Learning". AAAI.
Policy Gradient Methods
Los métodos value-based (como DQN) aprenden \(Q(s,a)\) y derivan la política indirectamente (\(\arg\max\)). Los métodos policy-based aprenden la política \(\pi_\theta(a|s)\) directamente, parametrizada por una red neuronal con pesos \(\theta\).
🎯 ¿Por qué policy gradients?
- Acciones continuas: DQN solo funciona con acciones discretas (\(\arg\max\) no tiene sentido en \(\mathbb{R}^n\)). Policy gradients manejan acciones continuas naturalmente.
- Políticas estocásticas: a veces la política óptima es aleatoria (e.g., piedra-papel-tijeras). Policy gradients lo modelan directamente.
- Convergencia: cambios suaves en \(\theta\) → cambios suaves en la política (no saltos bruscos como en value-based).
El teorema del Policy Gradient
La dirección de mejora de la política es: aumentar la probabilidad de acciones que llevaron a retornos altos y disminuir la de acciones con retornos bajos. Es como decir: "haz más de lo que funcionó y menos de lo que no".
REINFORCE (Monte Carlo Policy Gradient)
El algoritmo más simple de policy gradient. Recoge un episodio completo, calcula los retornos \(G_t\) para cada paso, y actualiza:
Donde \(b\) es un baseline (típicamente \(V(s_t)\)) que reduce la varianza del estimador sin sesgar el gradiente.
import torch
import torch.nn as nn
import torch.optim as optim
import gymnasium as gym
class PolicyNetwork(nn.Module):
def __init__(self, state_dim, action_dim):
super().__init__()
self.net = nn.Sequential(
nn.Linear(state_dim, 128), nn.ReLU(),
nn.Linear(128, action_dim),
nn.Softmax(dim=-1) # Distribución sobre acciones
)
def forward(self, x):
return self.net(x)
env = gym.make("CartPole-v1")
policy = PolicyNetwork(4, 2)
optimizer = optim.Adam(policy.parameters(), lr=1e-3)
for episode in range(1000):
states, actions, rewards = [], [], []
state, _ = env.reset()
# 1. Recoger episodio completo
while True:
probs = policy(torch.FloatTensor(state))
dist = torch.distributions.Categorical(probs)
action = dist.sample()
next_state, reward, term, trunc, _ = env.step(action.item())
states.append(state)
actions.append(action)
rewards.append(reward)
state = next_state
if term or trunc:
break
# 2. Calcular retornos descontados
G, returns = 0, []
for r in reversed(rewards):
G = r + 0.99 * G
returns.insert(0, G)
returns = torch.FloatTensor(returns)
returns = (returns - returns.mean()) / (returns.std() + 1e-8) # Baseline
# 3. Policy gradient update
loss = 0
for s, a, G_t in zip(states, actions, returns):
probs = policy(torch.FloatTensor(s))
log_prob = torch.log(probs[a])
loss -= log_prob * G_t # Negativo porque optimizer minimiza
optimizer.zero_grad()
loss.backward()
optimizer.step()
Actor-Critic: lo mejor de ambos mundos
Los métodos Actor-Critic combinan value-based y policy-based: un actor (red de política \(\pi_\theta\)) decide qué hacer, y un critic (red de valor \(V_\phi\)) evalúa cuán buena fue esa decisión.
El advantage mide cuánto mejor fue la acción \(a_t\) comparada con el promedio. Si \(A > 0\): la acción fue mejor de lo esperado → aumentar su probabilidad. Si \(A < 0\): peor de lo esperado → disminuir su probabilidad.
| Variante | Año | Idea clave | Uso |
|---|---|---|---|
| A2C | 2016 | Advantage Actor-Critic síncrono. Múltiples workers en paralelo. | Entornos simples, educativo |
| A3C | 2016 | Asíncrono: cada worker actualiza gradientes independientemente. | Atari, históricamente importante |
| GAE | 2016 | Generalized Advantage Estimation: combina n-step returns con λ. | Reduce varianza del advantage |
PPO — Proximal Policy Optimization
PPO (Schulman et al., 2017) es el algoritmo de RL más usado en la práctica. Es un actor-critic que resuelve el problema de inestabilidad de los policy gradients limitando cuánto puede cambiar la política en cada paso.
Donde \(r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}\) es el ratio de probabilidades y \(\epsilon = 0.2\) es el clip range. El clipping limita los cambios bruscos en la política, asegurando estabilidad sin necesitar la complejidad de TRPO.
Schulman, J. et al. (2017). "Proximal Policy Optimization Algorithms". arXiv:1707.06347.
Otros algoritmos importantes
SAC (Haarnoja et al., 2018) maximiza no solo la recompensa sino también la entropía de la política, incentivando la exploración:
- Off-policy: usa replay buffer (más eficiente en datos que PPO).
- Acciones continuas: diseñado para espacios de acción continuos.
- Robusto: la entropía previene convergencia prematura.
- Uso: robótica, locomoción, control continuo.
Haarnoja, T. et al. (2018). "Soft Actor-Critic: Off-Policy Maximum Entropy Deep RL". ICML.
DDPG (Lillicrap et al., 2016) extiende DQN a acciones continuas usando una red actor que produce directamente la acción (sin softmax ni muestreo):
- Actor: \(\mu_\theta(s) \to a \in \mathbb{R}^n\) (acción determinista).
- Critic: \(Q_\phi(s, a) \to \mathbb{R}\) (evalúa par estado-acción).
- Off-policy con replay buffer y target networks para ambas redes.
- Exploración con ruido (Ornstein-Uhlenbeck o Gaussiano).
Lillicrap, T. et al. (2016). "Continuous control with deep reinforcement learning". ICLR.
AlphaGo (Silver et al., 2016) y AlphaZero (2017) combinan RL con Monte Carlo Tree Search:
- Red neuronal: recibe el tablero → predice política \(\pi(a|s)\) + valor \(V(s)\).
- MCTS: usa la red para guiar la búsqueda del árbol de jugadas.
- Self-play: el agente juega contra sí mismo y mejora iterativamente.
- AlphaZero: aprende desde cero (sin datos humanos) → superhuman en Go, ajedrez y shogi.
- MuZero: ni siquiera necesita conocer las reglas del juego — aprende el modelo del entorno.
Silver, D. et al. (2017). "Mastering the Game of Go without Human Knowledge". Nature.
Comparativa de algoritmos de RL
| Algoritmo | Tipo | On/Off policy | Acciones | Uso principal |
|---|---|---|---|---|
| Q-Learning | Value | Off | Discretas | Educativo, entornos tabulares |
| DQN | Value | Off | Discretas | Atari, juegos discretos |
| REINFORCE | Policy | On | Ambas | Educativo, simple |
| A2C/A3C | Actor-Critic | On | Ambas | Paralelizable, general |
| PPO | Actor-Critic | On | Ambas | LLMs (RLHF), robótica, general |
| SAC | Actor-Critic | Off | Continuas | Robótica, control |
| DDPG | Actor-Critic | Off | Continuas | Control continuo |
| TD3 | Actor-Critic | Off | Continuas | Mejora de DDPG |
| AlphaZero | Model-based + MCTS | On (self-play) | Discretas | Juegos de mesa |
RLHF — RL para alinear modelos de lenguaje
Reinforcement Learning from Human Feedback (RLHF) es la técnica que hizo posible ChatGPT. Usa RL (específicamente PPO) para alinear un LLM con las preferencias humanas.
💬 El RL en el contexto de LLMs
| Concepto RL | En RLHF para LLMs |
|---|---|
| Agente | El LLM (genera tokens) |
| Entorno | El prompt + contexto de conversación |
| Estado | Prompt + tokens generados hasta ahora |
| Acción | Siguiente token a generar |
| Política | \(\pi_\theta(token | prompt, tokens_{prev})\) = distribución sobre vocabulario |
| Recompensa | Score del Reward Model al final de la respuesta completa |
| Episodio | Generar una respuesta completa a un prompt |
Ouyang, L. et al. (2022). "Training language models to follow instructions with human feedback". NeurIPS.
RL en sistemas modernos
Retos abiertos y futuro del RL
| Reto | Descripción | Enfoques actuales |
|---|---|---|
| Sample efficiency | RL necesita millones de interacciones. Inviable en el mundo real. | Model-based RL, offline RL, sim-to-real. |
| Reward design | Diseñar la recompensa correcta es difícil y propenso a hacking. | RLHF, reward learning, inverse RL. |
| Generalización | El agente aprende para un entorno específico. No generaliza. | Meta-RL, foundation models para RL, procedural generation. |
| Seguridad | El agente puede descubrir comportamientos peligrosos. | Safe RL, constrained RL, alignment research. |
| Multi-agent | Múltiples agentes interactuando. Equilibrios, cooperación, competición. | MARL, game theory + RL, emergent communication. |
| Long-horizon | Tareas con miles de pasos y reward muy esparsa. | Hierarchical RL, intrinsic motivation, curriculum learning. |
🧩 Widget: Selector de algoritmo de RL
Recomienda el algoritmo más adecuado según tu problema.
Papers y recursos fundamentales
📚 Papers esenciales de RL
- Sutton & Barto (2018): "Reinforcement Learning: An Introduction" — Libro completo gratuito
- DQN: Mnih, V. et al. (2015). "Human-level control through deep RL". Nature
- Policy Gradient: Sutton, R. et al. (1999). "Policy Gradient Methods for RL with Function Approximation". NeurIPS
- PPO: Schulman, J. et al. (2017). arXiv:1707.06347
- SAC: Haarnoja, T. et al. (2018). arXiv:1801.01290
- AlphaGo: Silver, D. et al. (2016). Nature
- RLHF: Ouyang, L. et al. (2022). arXiv:2203.02155
- DPO: Rafailov, R. et al. (2023). arXiv:2305.18290
- Rainbow: Hessel, M. et al. (2018). arXiv:1710.02298
- Spinning Up: OpenAI educational resource