Q-Learning y Aprendizaje por Refuerzo con Gymnasium en Python
Hay dos formas de quedarte a medias con el Aprendizaje por Refuerzo (RL). Una es leer la ecuación de Bellman, entender el dilema exploración-explotación y cerrar la pestaña sin haber ejecutado una línea de código. La otra es copiar un bucle de entrenamiento que funciona, pero sin entender por qué el agente decide lo que decide, y quedarte sin herramientas cuando algo no converge. Este artículo va por las dos vías a la vez: primero las piezas que explican por qué funciona Q-Learning, después un agente completo entrenado con Gymnasium 1.3 que puedes copiar y ejecutar tal cual.
Qué resuelve el aprendizaje por refuerzo que la lógica explícita no puede
Imagina que quieres que un agente cruce un lago congelado sin caer en un agujero. No puedes programar cada movimiento para cada situación posible, porque el mapa cambia o el agente llega a estados que no anticipaste. Tampoco tienes un conjunto de datos de "movimientos correctos" para entrenar un modelo supervisado, porque no existe ese dataset: nadie ha etiquetado de antemano cuál es la mejor acción en cada una de las decenas de casillas del lago.
El Aprendizaje por Refuerzo cubre exactamente ese hueco. Un agente interactúa con un entorno, recibe una recompensa tras cada acción y ajusta su comportamiento para maximizar la recompensa acumulada a lo largo del tiempo, por ensayo y error. Es el paradigma detrás de sistemas que van desde robots que aprenden a caminar hasta agentes que juegan videojuegos o gestionan carteras de inversión: en todos los casos el problema es una secuencia de decisiones bajo incertidumbre, no una clasificación de una sola vez.
Las piezas del problema: estado, acción, recompensa y las dos funciones de valor
Para razonar sobre Q-Learning hace falta el vocabulario mínimo de RL:
- Estado (s): la descripción completa de la situación actual del entorno. En un lago congelado de 4x4, la casilla donde está el agente.
- Acción (a): una decisión disponible en ese estado. Moverse arriba, abajo, izquierda o derecha.
- Recompensa (r): la señal numérica que el entorno devuelve tras cada acción, positiva o negativa. El objetivo del agente es maximizar la suma de recompensas a largo plazo, no la recompensa inmediata.
- Política (π): la estrategia que mapea estados a acciones. Es lo que el agente está aprendiendo.
- Función de valor de estado V(s): la recompensa total esperada si empiezas en el estado
sy sigues la políticaπ. - Función de valor de acción Q(s, a): la recompensa total esperada si tomas la acción
aen el estadosy luego sigues la política óptima. Es la cantidad que Q-Learning aprende directamente, y la que le da nombre al algoritmo.
La diferencia entre V(s) y Q(s, a) importa en la práctica: con V(s) todavía necesitas un modelo del entorno para decidir qué acción tomar (tienes que simular a dónde te lleva cada una). Con Q(s, a) no: la mejor acción en el estado s es directamente la que tiene el Q-valor más alto, sin simular nada. Esa es la razón por la que Q-Learning es libre de modelo (no necesita conocer las probabilidades de transición del entorno) y off-policy: aprende la política óptima observando transiciones generadas por cualquier política de exploración, no solo por la política que está ejecutando en ese momento.
El dilema exploración-explotación y por qué epsilon decae con el tiempo
Un agente que solo repite la acción que mejor le funcionó la primera vez se queda atrapado en un óptimo local: nunca descubre que hay un camino más corto o una recompensa mayor por otro lado. Un agente que solo explora nunca aprovecha lo que ya sabe. Ese es el dilema exploración-explotación, y la solución más simple con la que trabaja este artículo es la estrategia epsilon-greedy:
- Con probabilidad
epsilon, el agente elige una acción aleatoria (explora). - Con probabilidad
1 - epsilon, elige la acción con el Q-valor más alto conocido (explota).
epsilon empieza alto (cerca de 1, casi toda exploración) y decae en cada episodio hasta un mínimo, para que el agente pase de "no sé nada, prueba todo" a "ya sé bastante, aprovéchalo" a medida que acumula experiencia. Decaer demasiado rápido atrapa al agente en una política subóptima antes de que termine de explorar; no decaer nunca le impide converger.
Q-Learning: la ecuación de Bellman, paso a paso
La actualización de Q-Learning se basa en la ecuación de Bellman:
Q(s, a) <- Q(s, a) + alpha * (r + gamma * max(Q(s', a')) - Q(s, a))Donde alpha (tasa de aprendizaje, entre 0 y 1) controla cuánto pesa la nueva información frente a lo que ya sabías: alto aprende rápido pero puede oscilar sin converger, bajo es estable pero lento. gamma (factor de descuento, entre 0 y 1) pondera las recompensas futuras frente a las inmediatas: cerca de 0 hace al agente cortoplacista, cerca de 1 le da casi el mismo peso a una recompensa lejana que a una inmediata. Y max(Q(s', a')) es el Q-valor más alto disponible en el siguiente estado s': la pieza que hace que la actualización de hoy ya tenga en cuenta la mejor jugada de mañana, sin necesidad de simular el futuro completo.
La memoria de todo este proceso es la Q-Table: una matriz de (número de estados) x (número de acciones), inicializada en ceros, que se actualiza con esa fórmula en cada paso hasta converger.
De la teoría al código: preparar el entorno con Gymnasium
Gymnasium es la librería activa que estandariza entornos de RL en Python; es el fork mantenido por la Farama Foundation de la antigua OpenAI Gym, que dejó de recibir actualizaciones. La API pública de gym.make(), env.reset() y env.step() es idéntica a la que documenta hoy el propio proyecto, y es la que vas a usar para casi cualquier algoritmo de RL, no solo Q-Learning:
pip install "gymnasium[toy-text]" numpyEl entorno FrozenLake-v1 plantea justo el problema del que hablábamos arriba: cruzar un lago 4x4 desde la casilla de inicio (S) hasta la meta (G) sin caer en un agujero (H), con recompensa +1 solo al llegar a la meta y 0 en cualquier otro caso. La firma actual, confirmada en la documentación oficial del entorno, es:
import gymnasium as gym
env = gym.make(
"FrozenLake-v1",
is_slippery=False, # False: movimiento determinista, ideal para aprender el algoritmo
render_mode="ansi",
)
observation, info = env.reset(seed=42) # reset() devuelve (observación, info), no solo la observación
observation, reward, terminated, truncated, info = env.step(env.action_space.sample())
# step() devuelve 5 valores desde la migración de Gym a Gymnasium:
# terminated (el episodio acabó por meta u hoyo) y truncated (se acabó el episodio por límite de pasos)
# son señales distintas y hay que comprobar las dos para saber si reiniciar.Mini proyecto: entrenar y evaluar un agente Q-Learning completo
El siguiente script entrena un agente Q-Learning tabular en FrozenLake-v1 y luego evalúa la política aprendida sin exploración. Usa render_mode="ansi" en las dos fases (en vez de "human") para que el ejemplo corra igual en un terminal SSH, un contenedor o tu portátil, sin depender de una ventana gráfica ni de pygame:
import gymnasium as gym
import numpy as np
# --- Entorno e hiperparámetros ---
env = gym.make("FrozenLake-v1", is_slippery=False, render_mode="ansi")
learning_rate = 0.9 # alpha
discount_factor = 0.95 # gamma
epsilon = 1.0 # probabilidad inicial de explorar
epsilon_decay_rate = 0.001
min_epsilon = 0.01
num_episodes = 2000
num_states = env.observation_space.n
num_actions = env.action_space.n
q_table = np.zeros((num_states, num_actions))
print(f"Estados: {num_states}, acciones: {num_actions}")
def choose_action(state, q_table, epsilon):
if np.random.uniform(0, 1) < epsilon:
return env.action_space.sample() # exploración
return int(np.argmax(q_table[state, :])) # explotación
# --- Entrenamiento ---
rewards_per_episode = []
for episode in range(num_episodes):
state, info = env.reset()
terminated = truncated = False
total_reward = 0.0
while not (terminated or truncated):
action = choose_action(state, q_table, epsilon)
next_state, reward, terminated, truncated, info = env.step(action)
old_value = q_table[state, action]
next_max = np.max(q_table[next_state, :])
q_table[state, action] = old_value + learning_rate * (
reward + discount_factor * next_max - old_value
)
state = next_state
total_reward += reward
epsilon = max(min_epsilon, epsilon - epsilon_decay_rate)
rewards_per_episode.append(total_reward)
if (episode + 1) % 200 == 0:
media = np.mean(rewards_per_episode[-200:])
print(f"Episodio {episode + 1}/{num_episodes} | epsilon={epsilon:.3f} | éxito últimos 200: {media:.2%}")
env.close()
# --- Evaluación de la política aprendida, sin exploración ---
env_eval = gym.make("FrozenLake-v1", is_slippery=False, render_mode="ansi")
num_eval_episodes = 10
successes = 0
for episode in range(num_eval_episodes):
state, info = env_eval.reset()
terminated = truncated = False
print(f"\n--- Episodio de evaluación {episode + 1} ---")
print(env_eval.render())
while not (terminated or truncated):
action = int(np.argmax(q_table[state, :]))
state, reward, terminated, truncated, info = env_eval.step(action)
print(env_eval.render())
if terminated and reward == 1:
successes += 1
print("Meta alcanzada.")
elif terminated:
print("Caída en un agujero.")
else:
print("Episodio truncado por límite de pasos.")
env_eval.close()
print(f"\nÉxitos: {successes}/{num_eval_episodes}")Con is_slippery=False y 2.000 episodios, la tasa de éxito en evaluación debería acercarse al 100%: el entorno es determinista, así que una Q-Table bien entrenada encuentra siempre el mismo camino óptimo. Si activas is_slippery=True (el comportamiento real del entorno, donde el agente resbala con cierta probabilidad hacia una dirección perpendicular), la tasa de éxito baja porque parte del resultado deja de depender de la política y empieza a depender del azar del propio entorno; ahí es donde se nota si tu agente realmente generalizó o memorizó una única trayectoria.
Errores comunes y cómo depurarlos
La documentación oficial de Gymnasium señala como error típico de principiante llamar a env.step() sin haber llamado antes a env.reset() en esa sesión: el entorno lo rechaza porque no tiene un estado inicial válido. Más allá de eso:
- El agente nunca converge: revisa primero
learning_rate(muy alto oscila, muy bajo tarda) y después la velocidad de decaimiento deepsilon(si decae demasiado rápido, el agente deja de explorar antes de haber visto suficientes estados). - Confundir
terminatedcontruncated: son señales distintas desde que Gym se convirtió en Gymnasium.terminatedsignifica que el episodio acabó por una razón del propio entorno (meta u hoyo);truncatedsignifica que se acabó por un límite externo de pasos. Tratarlas como lo mismo esconde episodios que en realidad no resolvieron nada, solo se quedaron sin tiempo. - Dimensiones de la Q-Table incorrectas: deben salir siempre de
env.observation_space.nyenv.action_space.n, nunca de un número fijo copiado de otro entorno. - Recompensas demasiado escasas: en
FrozenLake-v1solo hay señal al llegar a la meta; si cambias a un entorno con recompensas más raras todavía, la curva derewards_per_episodetarda mucho más en despegar y conviene graficarla para confirmar que sube, no asumirlo.
Cuándo la Q-Table deja de bastar
Q-Learning tabular funciona mientras el número de estados sea manejable: un lago 4x4 tiene 16 estados, un Taxi-v3 tiene unos cientos. En cuanto el espacio de estados es continuo o simplemente demasiado grande para una tabla (la posición y velocidad de un péndulo, los píxeles de una pantalla de juego), la Q-Table deja de ser viable y hace falta aproximar Q(s, a) con una red neuronal: eso es exactamente lo que hacen los Deep Q-Networks (DQN), que sustituyen la tabla por una función entrenada con descenso de gradiente pero mantienen la misma ecuación de Bellman en el fondo.
Fuera del family de Q-Learning, dos direcciones habituales para seguir: los métodos on-policy como SARSA, que actualizan el valor usando la acción que el agente realmente va a tomar (no la mejor posible, como hace Q-Learning), y los métodos de gradiente de política, que aprenden directamente una función que mapea estados a probabilidades de acción en lugar de pasar por una función de valor intermedia. Aplicaciones reales de esta familia van desde robots que aprenden a manipular objetos hasta sistemas de recomendación que ajustan su política con cada interacción del usuario; en todos los casos, el punto de partida conceptual sigue siendo el mismo que en un lago congelado de 16 casillas: agente, entorno, recompensa, y una política que mejora con cada episodio.