En este proyecto implemento un modelo evolutivo, construido mediante algoritmos genéticos, que juega mi primer proyecto en Python. Si aún no lo has visto, puedes jugarlo aquí.
Utiliza un sistema que “evoluciona” su forma de jugar, inspirado en cómo la naturaleza mejora a las especies con el paso de las generaciones. Al principio, el programa crea un montón de jugadores sin ningún conocimiento del entorno, a los que se recompensa por conseguir una puntuación alta y se les indica que pueden moverse hacia arriba o hacia abajo.
Al comienzo, simplemente pulsan estas teclas de forma aleatoria, hasta que, dentro de ese caos, alguno consigue esquivar la primera pared. Eso le permite obtener una puntuación mayor que el resto, y ese pequeño “éxito” se transmite a la siguiente generación. El ciclo se repite una y otra vez. A medida que el proceso converge, empiezan a descubrir que esquivando paredes y recogiendo estrellas alcanzan puntuaciones más altas.
A continuación puedes ver en tiempo real cómo se entrena este algoritmo genético. Cada generación empieza con 20 individuos; cuando todos mueren, se crea una nueva generación combinando los mejores genes de la anterior junto con algunas mutaciones aleatorias, y el proceso vuelve a empezar. Puedes intentar jugar contra el bot, cuanto más lo dejes entrenar, mejor será.
Después del simulador dejo los detalles más técnicos para quien le interese.
Algoritmos Genéticos
Los algoritmos genéticos son técnicas de inteligencia artificial basadas en los principios de la evolución biológica. Funcionan mediante la selección iterativa de soluciones candidatas, favoreciendo aquellas con mejor rendimiento.
El proceso consta de las siguientes etapas:
- Población inicial: Se generan múltiples individuos con parámetros aleatorios (redes neuronales con pesos y conexiones inicializados aleatoriamente)
- Evaluación: Cada individuo ejecuta la tarea y acumula una puntuación (fitness) según su desempeño
- Selección: Los individuos con mayor fitness se seleccionan como progenitores
- Reproducción y mutación: Se crean nuevos individuos combinando características de los progenitores y aplicando modificaciones aleatorias
- Iteración: El ciclo se repite, mejorando progresivamente el rendimiento de la población
Este enfoque permite encontrar soluciones complejas sin necesidad de programar explícitamente el comportamiento deseado. A diferencia del aprendizaje supervisado tradicional, donde se requiere un conjunto de datos etiquetados, los algoritmos genéticos descubren soluciones mediante búsqueda heurística en el espacio de posibles configuraciones.
NEAT: Evolución de Topologías de Redes Neuronales
NEAT (NeuroEvolution of Augmenting Topologies) es un algoritmo genético especializado que evoluciona simultáneamente los pesos y la estructura de las redes neuronales. A diferencia de los métodos convencionales donde la arquitectura es fija, NEAT parte de redes mínimas que se complejizan según las necesidades del problema.
Características principales de NEAT:
- Complejización incremental: Comienza con redes sin capas ocultas (topología mínima), añadiendo neuronas y conexiones mediante mutaciones estructurales. Este enfoque resuelve el problema del bootstrapping evolutivo, donde estructuras complejas iniciales dificultan la convergencia
- Especiación: Agrupa individuos similares en especies mediante un coeficiente de compatibilidad que mide diferencias topológicas y de pesos. Esto protege innovaciones estructurales que inicialmente pueden reducir el fitness, permitiendo su refinamiento antes de competir con soluciones establecidas
- Marcadores de innovación histórica: Utiliza identificadores únicos (innovation numbers) para realizar cruces genéticos coherentes entre topologías diferentes, resolviendo el problema de alineamiento en crossover de redes con estructuras distintas
Esta aproximación permite que la complejidad de la red emerja naturalmente durante el entrenamiento, evitando tanto el overfitting de arquitecturas excesivamente complejas como el underfitting de estructuras demasiado simples.
Operadores Genéticos Implementados
El algoritmo aplica dos tipos principales de mutaciones:
Mutaciones de pesos (80% probabilidad):
- Perturbación gaussiana de conexiones existentes (±0.5 del valor actual)
- Reemplazo completo del peso (10% de las mutaciones)
- Rango permitido: [-30, 30] para evitar valores extremos que desestabilicen el aprendizaje
Mutaciones estructurales:
- Adición de conexiones (50% probabilidad): Crea enlaces entre neuronas previamente desconectadas
- Adición de nodos (20% probabilidad): Divide una conexión existente insertando una neurona intermedia
- Eliminación de componentes (50% y 20% respectivamente): Mantiene la población diversa sin complejidad innecesaria
La configuración emplea la función de activación tanh, que transforma los valores de entrada a un rango acotado entre -1 y 1. Esto introduce la no linealidad necesaria para que la red pueda ajustar su comportamiento de forma flexible.
Especiación y Diversidad
El algoritmo mantiene compatibilidad threshold = 3.0, donde la distancia genética entre dos individuos se calcula como:
δ = c₁·(E/N) + c₂·W̄
Donde:
- E: Genes disjuntos/excedentes (diferencias estructurales)
- W̄: Diferencia promedio de pesos en genes coincidentes
- c₁ = 1.0, c₂ = 0.5: Coeficientes que priorizan diferencias estructurales sobre variaciones de pesos
- N: Normalización por tamaño del genoma mayor
Las especies con stagnation > 20 generaciones (sin mejora de fitness) son eliminadas, excepto las 2 mejores especies (species elitism). Esto equilibra exploración de nuevas soluciones y explotación de estrategias prometedoras.
Implementación del Proyecto
El juego consiste en un endless runner donde una nave debe esquivar obstáculos procedurales (columnas superiores e inferiores) mientras recolecta power-ups. El objetivo es entrenar un agente mediante NEAT que aprenda a maximizar la supervivencia.
Arquitectura de Entrada/Salida
La red neuronal procesa 5 variables de entrada normalizadas:
- Posición vertical del cohete (0-300 píxeles): Estado actual del agente
- Distancia horizontal al próximo obstáculo (0-1024 píxeles): Información temporal crítica
- Posición vertical del obstáculo (-200 o 100 píxeles): Discrimina entre amenaza superior/inferior
- Distancia horizontal al próximo power-up (0-3072 píxeles): Oportunidad de recompensa
- Posición vertical del power-up (50-250 píxeles): Localización espacial del objetivo secundario
Genera 2 salidas que determinan la acción mediante activaciones umbral (>0.5):
- Salida 0: Desplazamiento vertical ascendente (10 píxeles/frame)
- Salida 1: Desplazamiento vertical descendente (10 píxeles/frame)
- Ambas <0.5: Mantener posición (actualiza temporizadores internos)
Esta representación enfrenta el problema de asignación de crédito temporal: ¿cómo atribuir el éxito/fracaso a acciones tomadas varios frames antes del evento? NEAT lo resuelve implícitamente mediante fitness acumulativo.
Función de Fitness Multi-Objetivo
La evaluación combina tres componentes que reflejan diferentes aspectos del comportamiento óptimo:
Supervivencia base (0.1 puntos/frame):
- Recompensa la exploración y evitación de riesgos
- Con 30 FPS, equivale a 3 puntos/segundo de supervivencia
- Penaliza implícitamente estrategias pasivas mediante terminación al colisionar
Recolección de power-ups (10 puntos/unidad):
- Incentivizes active searching behavior
- Requiere equilibrio entre supervivencia y toma de riesgos
- Los power-ups aparecen con 30% de probabilidad tras cada obstáculo
Superación de obstáculos (1 punto/unidad):
- Métrica de progreso temporal
- Obstáculos aparecen cada 400 píxeles aproximadamente
- Correlaciona directamente con tiempo de supervivencia
El trade-off entre estos objetivos emerge naturalmente: maximizar power-ups implica maniobras arriesgadas, mientras que evitar obstáculos favorece trayectorias conservadoras.
Escalado de Dificultad
El sistema implementa tres modos que modifican la velocidad de desplazamiento del escenario:
- Easy (15 px/frame): Ventana de reacción de ~40 frames ante obstáculo a distancia típica
- Normal (25 px/frame): Ventana de ~24 frames, requiere anticipación
- Hard (35 px/frame): Ventana de ~17 frames, exige respuestas casi instantáneas
Esta variación altera fundamentalmente el paisaje de fitness: velocidades superiores penalizan estrategias reactivas y favorecen políticas predictivas basadas en información de distancia. El incremento de dificultad no es lineal debido a la física del juego (límites de aceleración, tamaño de gaps, etc.).
Estrategias de Entrenamiento
Cada generación evalúa 20 genomas en paralelo durante máximo 50 generaciones, con:
- Elitismo = 2: Los dos mejores individuos de cada especie pasan directamente a la siguiente generación sin mutación
- Survival threshold = 20%: Solo el quintil superior de cada especie se reproduce
- Límite de fitness = 1000 puntos: Criterio de terminación anticipada (aprox. 300 seconds de supervivencia perfecta)
El entrenamiento típicamente converge en no más de 10 generaciones.