Image for post Aprendizaje por Refuerzo desde Cero: Q-Learning con Gymnasium en Python

Aprendizaje por Refuerzo desde Cero: Q-Learning con Gymnasium en Python


Como desarrolladores, estamos acostumbrados a resolver problemas con lógica explícita o a entrenar modelos con datos etiquetados. Pero, ¿qué sucede cuando el problema es dinámico, las reglas no están claras o la mejor acción depende de una secuencia de decisiones? Aquí es donde el Aprendizaje por Refuerzo (RL) brilla. En este artículo, desglosaremos uno de los algoritmos fundamentales de RL, Q-Learning, y lo implementaremos paso a paso utilizando la popular biblioteca gymnasium en Python.

Contexto del Problema

Imagina que quieres enseñar a un robot a navegar por un laberinto. No puedes simplemente programar cada movimiento para cada posible situación, ya que el laberinto podría cambiar o el robot podría encontrarse en estados inesperados. Tampoco tienes un conjunto de datos masivo de “movimientos correctos” para cada situación (como en el aprendizaje supervisado). Lo que sí puedes hacer es permitir que el robot explore, reciba retroalimentación (recompensas o castigos) y aprenda por sí mismo a encontrar la salida.

El Aprendizaje por Refuerzo es una rama del Machine Learning donde un agente aprende a tomar decisiones interactuando con un entorno para maximizar una recompensa acumulada a lo largo del tiempo. Es un paradigma de aprendizaje por ensayo y error, ideal para problemas de toma de decisiones secuenciales, como juegos, robótica, sistemas de recomendación y optimización de recursos.

Conceptos Clave

Para entender Q-Learning, primero necesitamos familiarizarnos con algunos términos fundamentales del Aprendizaje por Refuerzo:

  • Agente: Es la entidad que toma decisiones y aprende. En nuestro ejemplo, sería el robot.
  • Entorno: Es el mundo con el que el agente interactúa. El laberinto en nuestro ejemplo.
  • Estado (State): Una descripción completa de la situación actual del entorno. Por ejemplo, la posición del robot en el laberinto.
  • Acción (Action): Una decisión que el agente puede tomar en un estado dado. Moverse hacia arriba, abajo, izquierda o derecha.
  • Recompensa (Reward): Un valor numérico que el entorno proporciona al agente después de realizar una acción. Puede ser positiva (llegar a la meta) o negativa (caer en un agujero). El objetivo del agente es maximizar la recompensa total a largo plazo.
  • Política (Policy): La estrategia del agente, que define cómo el agente elige una acción dado un estado. Es el “cerebro” del agente.
  • Función de Valor Q (Q-Value): Representa la “calidad” o el valor esperado de tomar una acción específica en un estado específico y luego seguir la política óptima a partir de ese momento. Se denota como Q(estado, acción).
  • Ecuación de Bellman (para Q-Learning): Es la regla de actualización central en Q-Learning. Permite al agente aprender y refinar sus Q-valores. La forma simplificada es:
    Q(s, a) = Q(s, a) + alpha * (recompensa + gamma * max(Q(s', a')) - Q(s, a))
    • alpha (tasa de aprendizaje): Determina cuánto de la nueva información se acepta. Un valor alto significa que el agente aprende rápido pero puede ser inestable; uno bajo, lento pero estable.
    • gamma (factor de descuento): Pondera la importancia de las recompensas futuras. Un valor cercano a 0 hace que el agente sea cortoplacista; uno cercano a 1, que valore más las recompensas futuras.
    • max(Q(s', a')): El Q-valor máximo para el siguiente estado s', representando la mejor acción posible desde ese nuevo estado.
  • Exploración vs. Explotación (Epsilon-Greedy): El agente necesita equilibrar la exploración (probar acciones nuevas para descubrir mejores recompensas) y la explotación (usar las acciones que ya sabe que dan buenas recompensas). La estrategia Epsilon-Greedy elige una acción aleatoria con probabilidad epsilon (exploración) y la mejor acción conocida con probabilidad 1 - epsilon (explotación). epsilon suele decrecer con el tiempo.
  • Tabla Q (Q-Table): Una tabla que almacena los Q-valores para cada par (estado, acción). Es la memoria del agente en Q-Learning tabular.
  • gymnasium: Una biblioteca de Python que proporciona una API estándar para definir entornos de Aprendizaje por Refuerzo y una colección diversa de entornos de referencia. Es un fork mantenido de la biblioteca original OpenAI Gym.

Implementación Paso a Paso

Vamos a implementar Q-Learning para resolver el entorno FrozenLake-v1 de gymnasium. En este entorno, un agente debe cruzar un lago congelado desde un punto de inicio (S) hasta una meta (G), evitando caer en agujeros (H).

1. Instalación de Dependencias

Necesitarás gymnasium y numpy. Abre tu terminal y ejecuta:


pip install gymnasium numpy

2. Configuración del Entorno y Parámetros

Primero, importamos las bibliotecas necesarias y definimos los hiperparámetros para nuestro algoritmo de Q-Learning. Es una buena práctica usar variables de entorno para claves sensibles, aunque en este ejemplo de Q-Learning tabular no las necesitaremos directamente.

3. Inicialización de la Q-Table

La Q-Table es el corazón de nuestro agente. Será una matriz donde las filas representan los estados y las columnas, las acciones. La inicializaremos con ceros.

4. La Estrategia Epsilon-Greedy

Esta función decidirá si el agente explora (elige una acción aleatoria) o explota (elige la mejor acción conocida de la Q-Table).

5. El Bucle de Entrenamiento

Aquí es donde ocurre la magia. El agente interactuará con el entorno durante un número determinado de episodios, actualizando la Q-Table en cada paso utilizando la Ecuación de Bellman.

Mini Proyecto / Aplicación Sencilla: FrozenLake-v1

A continuación, el código completo para entrenar un agente en el entorno FrozenLake-v1. Para simplificar el aprendizaje inicial, usaremos la versión no resbaladiza (is_slippery=False).


import gymnasium as gym
import numpy as np
import os # Buena práctica para variables de entorno, aunque no se usen directamente aquí.

# --- Configuración del Entorno y Hiperparámetros ---
# Entorno: FrozenLake-v1 (sin resbalones para simplificar el aprendizaje inicial)
# 'S' - Start, 'F' - Frozen, 'H' - Hole, 'G' - Goal
# La versión 'v1' es determinista si is_slippery=False
env = gym.make("FrozenLake-v1", is_slippery=False, render_mode="ansi") # render_mode="ansi" para ver la salida en texto

# Hiperparámetros de Q-Learning
learning_rate = 0.9  # alpha: Tasa de aprendizaje. Cuánto se actualiza el Q-valor.
discount_factor = 0.95 # gamma: Factor de descuento. Importancia de recompensas futuras.
epsilon = 1.0        # epsilon inicial para exploración. Probabilidad de explorar.
epsilon_decay_rate = 0.001 # Tasa de decaimiento de epsilon por episodio.
min_epsilon = 0.01   # epsilon mínimo. Asegura que siempre haya algo de exploración.
num_episodes = 2000  # Número de episodios de entrenamiento.

# --- Inicialización de la Q-Table ---
# La Q-Table tendrá dimensiones (num_estados, num_acciones)
num_states = env.observation_space.n
num_actions = env.action_space.n
q_table = np.zeros((num_states, num_actions))

print(f"Número de estados: {num_states}")
print(f"Número de acciones: {num_actions}")
print(f"Q-Table inicializada con forma: {q_table.shape}")

# --- Función para elegir acción (Epsilon-Greedy) ---
def choose_action(state, q_table, epsilon):
    if np.random.uniform(0, 1) < epsilon:
        # Exploración: elige una acción aleatoria
        return env.action_space.sample()
    else:
        # Explotación: elige la acción con el mayor Q-valor para el estado actual
        return np.argmax(q_table[state, :])

# --- Entrenamiento del Agente ---
rewards_per_episode = []

print("\nIniciando entrenamiento...")
for episode in range(num_episodes):
    state, info = env.reset() # Reiniciar el entorno para un nuevo episodio
    terminated = False
    truncated = False
    total_rewards = 0

    while not terminated and not truncated:
        action = choose_action(state, q_table, epsilon) # Elegir acción
        next_state, reward, terminated, truncated, info = env.step(action) # Ejecutar acción

        # Actualizar Q-Table (Ecuación de Bellman)
        # Q(s, a) = Q(s, a) + alpha * (reward + gamma * max(Q(s', a')) - Q(s, a))
        old_q_value = q_table[state, action]
        next_max_q = np.max(q_table[next_state, :])
        new_q_value = old_q_value + learning_rate * (reward + discount_factor * next_max_q - old_q_value)
        q_table[state, action] = new_q_value

        state = next_state
        total_rewards += reward

    # Decaimiento de Epsilon
    epsilon = max(min_epsilon, epsilon - epsilon_decay_rate)

    rewards_per_episode.append(total_rewards)

    if (episode + 1) % 100 == 0:
        print(f"Episodio {episode + 1}/{num_episodes}, Epsilon: {epsilon:.2f}, Recompensa promedio (últimos 100): {np.mean(rewards_per_episode[-100:]):.2f}")

print("\nEntrenamiento completado.")
env.close() # Cerrar el entorno de entrenamiento

# --- Evaluación del Agente Entrenado ---
print("\nEvaluando el agente entrenado (sin exploración)...")
num_eval_episodes = 10
successful_episodes = 0

# Usamos render_mode="human" para ver la simulación visualmente
env_eval = gym.make("FrozenLake-v1", is_slippery=False, render_mode="human")

for episode in range(num_eval_episodes):
    state, info = env_eval.reset()
    terminated = False
    truncated = False
    print(f"\n--- Episodio de Evaluación {episode + 1} ---")
    env_eval.render() # Mostrar el estado inicial

    while not terminated and not truncated:
        action = np.argmax(q_table[state, :]) # Elegir la mejor acción de la Q-Table (explotación pura)
        next_state, reward, terminated, truncated, info = env_eval.step(action)
        state = next_state
        env_eval.render() # Mostrar cada paso

        if terminated and reward == 1: # Si el objetivo es alcanzar la meta con recompensa 1
            successful_episodes += 1
            print("¡Meta alcanzada!")
        elif terminated and reward == 0: # Si cae en un agujero (en FrozenLake sin resbalones, esto no debería pasar con una política óptima)
            print("¡Caíste en un agujero!")

    if not terminated and not truncated:
        print("Episodio terminado por truncamiento (límite de pasos).")

env_eval.close()
print(f"\nAgente entrenado exitosamente en {successful_episodes}/{num_eval_episodes} episodios de evaluación.")

Este código primero entrena al agente durante num_episodes, permitiendo la exploración. Luego, evalúa el rendimiento del agente entrenado durante num_eval_episodes, donde el agente solo explota su conocimiento (no hay exploración aleatoria). Verás cómo el agente navega por el lago, intentando llegar a la meta.

Errores Comunes y Depuración

El Aprendizaje por Refuerzo puede ser sensible a la configuración. Aquí hay algunos errores comunes y consejos para depurar:

  • Hiperparámetros incorrectos:
    • learning_rate (alpha): Si es muy bajo, el aprendizaje será lento. Si es muy alto, el agente puede volverse inestable y no converger.
    • discount_factor (gamma): Un gamma bajo hace que el agente sea cortoplacista. Un gamma muy alto puede hacer que el agente valore demasiado las recompensas futuras, ignorando las inmediatas, o que el aprendizaje sea inestable.
    • epsilon y epsilon_decay_rate: Si epsilon no decae, el agente siempre explorará y nunca explotará su conocimiento. Si decae demasiado rápido, el agente podría quedarse atascado en óptimos locales sin explorar lo suficiente.
  • Entorno no reseteado: Cada nuevo episodio debe comenzar con env.reset() para asegurar que el agente empieza desde un estado inicial válido.
  • Dimensiones de la Q-Table: Asegúrate de que la Q-Table tenga las dimensiones correctas (número de estados x número de acciones) según env.observation_space.n y env.action_space.n.
  • Bucle infinito o truncamiento inesperado: Si el agente no aprende a terminar el episodio (alcanzar la meta o caer en un agujero), el bucle de entrenamiento podría continuar indefinidamente o ser truncado por un límite de pasos del entorno. Revisa las condiciones terminated y truncated.
  • Recompensas escasas: En entornos donde las recompensas son muy raras (sparse rewards), el agente puede tardar mucho en aprender. Considera técnicas de reward shaping (con precaución) o algoritmos más avanzados.
  • Depuración:
    • Imprime el state, action, reward y next_state en cada paso para entender el flujo.
    • Grafica la recompensa total por episodio (rewards_per_episode) para visualizar la curva de aprendizaje. Debería mostrar una tendencia ascendente.
    • Examina la Q-Table (o partes de ella) después del entrenamiento para ver si los valores tienen sentido.

Aprendizaje Futuro / Próximos Pasos

Has dado tus primeros pasos en el fascinante mundo del Aprendizaje por Refuerzo. Q-Learning es un algoritmo fundamental, pero es solo el comienzo:

  • Explora más entornos de gymnasium: Prueba con entornos más complejos como "Taxi-v3" o "CartPole-v1". Estos te presentarán nuevos desafíos, como espacios de observación más grandes o continuos.
  • Deep Q-Networks (DQNs): Para entornos con un número muy grande o continuo de estados, una Q-Table se vuelve inviable. Los DQNs utilizan redes neuronales para aproximar la función Q, permitiendo al agente manejar estos espacios complejos.
  • Otros algoritmos de RL: Investiga algoritmos como SARSA (un algoritmo on-policy), Policy Gradients (que aprenden la política directamente en lugar de una función de valor) o Actor-Critic.
  • Exploración avanzada: Más allá de Epsilon-Greedy, existen métodos de exploración más sofisticados como UCB (Upper Confidence Bound) o Thompson Sampling.
  • Aplicaciones reales: El RL se utiliza en robótica, juegos (AlphaGo, AlphaStar), vehículos autónomos, optimización de sistemas logísticos y financieros, y más.

El Aprendizaje por Refuerzo es un campo en constante evolución con un potencial inmenso. ¡Sigue explorando y construyendo agentes inteligentes!