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.