Implementación del Algoritmo Q-Learning en Python

Fundamentos Teóricos

El algoritmo Q-Learning es una técnica de aprendizaje por refuerzo que permite a un agente aprender la política óptima de acciones en diferentes estados. Su fórmula central es:

Q(s,a) ← (1 - α) * Q(s,a) + α * (r + γ * max Q(s',a'))

Donde Q(s,a) representa el valor esperado de tomar la acción a en el estado s, α es la tasa de aprendizaje, r es la recompensa inmdeiata, γ es el factor de descuento y s' es el nuevo estado después de la acción. El término max Q(s',a') indica el máximo valor posible en el siguiente estado.

La actualización combina el valor anterior con una estimación corregida basada en la recompensa y el valor futuro óptimo. Iterando este proceso, la tabla Q converge hacia valores que representan la mejor acción posible en cada situación.

Traducción a Código Python Simple

alpha = 0.1
gamma = 0.5

def update_q(state, action, reward):
    row, col = state
    if action == 0:    # arriba
        new_state = (row - 1, col)
    elif action == 1:  # abajo
        new_state = (row + 1, col)
    elif action == 2:  # izquierda
        new_state = (row, col - 1)
    elif action == 3:  # derecha
        new_state = (row, col + 1)
    else:
        raise ValueError("Acción inválida")
    
    Q[state][action] += alpha * (reward + gamma * max(Q[new_state]) - Q[state][action])

Lecciones Aprendidas

En los primeros intentos, se cometió el error de almacenar en la tabla Q solo los valores de cada estado, en lugar de los valores por cada par estado-acción. Es crucial recordar que la tabla Q debe registrar la evaluación de cada acción dentro de cada estado. Este malentendido inicial, aunque frustrante, ayudó a consolidar la comprensión del algoritmo.

Entorno Personalizado con Gym

A continuación se muestra un entorno creado con la API de Gym que simula un tablero 4×4 con celdas de inicio (S), meta (G), hielo (F) y peligro (H). El agente debe llegar desde S hasta G evitando las celdas peligrosas.

import gym
from gym import spaces
import numpy as np

class MiEntorno(gym.Env):
    metadata = {'render.modes': ['human']}
    
    def __init__(self):
        self.tablero = np.array([['S','F','F','F'],
                                 ['F','H','F','H'],
                                 ['F','F','F','H'],
                                 ['H','F','F','G']])
        self.alto, self.ancho = self.tablero.shape
        self.accion_espacio = spaces.Discrete(4)
        self.obs_espacio = spaces.Tuple((spaces.Discrete(self.alto),
                                         spaces.Discrete(self.ancho)))
        self.reset()
    
    def step(self, accion):
        fila, col = self.pos_actual
        if accion == 0:
            nueva_pos = (fila - 1, col)
        elif accion == 1:
            nueva_pos = (fila + 1, col)
        elif accion == 2:
            nueva_pos = (fila, col - 1)
        elif accion == 3:
            nueva_pos = (fila, col + 1)
        
        assert self._pos_valida(nueva_pos), "Movimiento inválido"
        self.pasos += 0.1
        
        if self.tablero[nueva_pos] == 'H':
            recompensa = -self.pasos - self.alto * self.ancho
            self.hecho = True
        elif self.tablero[nueva_pos] == 'G':
            recompensa = -self.pasos
            self.hecho = True
        else:
            recompensa = -self.pasos - abs(nueva_pos[0] - 3) - abs(nueva_pos[1] - 3)
            self.hecho = False
        
        self.pos_actual = nueva_pos
        return nueva_pos, recompensa, self.hecho, self.tablero[nueva_pos] == 'H'
    
    def reset(self):
        self.pos_actual = (0, 0)
        self.hecho = False
        self.pasos = 0
        return self.pos_actual
    
    def render(self, mode='human'):
        for i in range(self.alto):
            linea = ""
            for j in range(self.ancho):
                if (i, j) == self.pos_actual:
                    linea += "*"
                else:
                    linea += self.tablero[i][j]
            print(linea)
        print()
    
    def _pos_valida(self, pos):
        return 0 <= pos[0] < self.alto and 0 <= pos[1] < self.ancho

def softmax(x):
    e_x = np.exp(x)
    return e_x / np.sum(e_x)

def acciones_permitidas(fila, col):
    acciones = []
    if fila < 3: acciones.append(1)  # abajo
    if col < 3: acciones.append(3)   # derecha
    if fila > 0: acciones.append(0)  # arriba
    if col > 0: acciones.append(2)   # izquierda
    return acciones

Entrenamiento y Ejecución del Agente Q-Learning

entorno = MiEntorno()
Q = np.zeros((4, 4, 4))
for f in range(4):
    for c in range(4):
        permitidas = acciones_permitidas(f, c)
        for a in range(4):
            if a not in permitidas:
                Q[(f, c, a)] = -float("inf")
            else:
                Q[(f, c, a)] = 0.0

def elegir_accion(estado):
    # Usamos softmax para tener cierta exploración, pero en inferencia elegimos el argmax
    return np.argmax(softmax(Q[estado]))

def entrenar(episodios=200):
    alpha = 0.1
    gamma = 0.95
    tasa_exploracion = 0.9
    
    for _ in range(episodios):
        s = entorno.reset()
        while True:
            entorno.render()
            permitidas = acciones_permitidas(s[0], s[1])
            if np.random.uniform() < tasa_exploracion:
                accion = elegir_accion(s)
            else:
                accion = np.random.choice(permitidas)
            assert accion in permitidas, "Acción no permitida"
            sig_estado, recompensa, hecho, es_peligro = entorno.step(accion)
            if hecho:
                Q[s][accion] += alpha * (recompensa - Q[s][accion])
                break
            else:
                mejor_futuro = np.max(Q[sig_estado])
                Q[s][accion] += alpha * (recompensa + gamma * mejor_futuro - Q[s][accion])
            s = sig_estado

def jugar():
    s = entorno.reset()
    print("Estado inicial:")
    entorno.render()
    while True:
        accion = elegir_accion(s)
        acciones_str = ['arriba','abajo','izquierda','derecha']
        print(f"Tomando acción: {acciones_str[accion]}")
        sig_estado, recompensa, hecho, _ = entorno.step(accion)
        s = sig_estado
        entorno.render()
        if hecho:
            print(f"Recompensa final: {recompensa}")
            break

entrenar(episodios=150)
print("\nTabla Q final:")
for i in range(4):
    for j in range(4):
        print(f"({i},{j}): {Q[(i,j)]}")
jugar()

Conclusiones y Reflexiones

El desarrollo de este ejemplo tomó tiempo principalmente por dos razones: una definición incorrecta inicial de la tabla Q (valores por estado en lugar de pares estado-acción) y la tendencia a caer en bucles infinitos. Se debe cuidar la canitdad de episodios de entrenamiento y la configuración de la recompensa. Es importante permitir cierta aleatoriedad en la selección de acciones para evitar que el agente se estanque en caminos subóptimos. Si el entorno cambia, el agente debe reentrenarse. La función de recompensa utilizada combina el número de pasos y la distancia Manhatan, penalizando tanto la longitud de la trayectoria como la lejanía a la meta. Las acciones no permitidas se enmascaran con -inf y se convierten en probabilidad cero mediante softmax.

Etiquetas: q-learning aprendizaje por refuerzo Python OpenAI Gym Entorno Personalizado

Publicado el 7-11 21:19