Machine Learning Avanzado 20 min de lectura

Imitation learning: NPCs que aprenden viendo al jugador

Grabas demostraciones del jugador, entrenas un modelo a copiar su comportamiento, y en runtime el NPC actúa como un humano. Behavioral cloning paso a paso.

Publicado: · Por Juanjo "Banyo" López

0. Introducción

Diseñar comportamientos a mano tiene un techo. Un behavior tree con cincuenta nodos sigue sintiéndose programado: el jugador detecta el patrón en diez minutos. Una FSM que cubra “esquivar, cubrirse, flanquear, retroceder al estar herido” se infla rápido y termina llena de casos especiales que nadie quiere mantener.

La alternativa: en lugar de codificar cada decisión, grabas miles de pasos del jugador (qué ve, qué hace) y entrenas una red neuronal que aprenda ese mapping. En runtime, la red lee los sensores del NPC y predice la acción. El resultado se siente humano porque, literalmente, copia a un humano.

En este tutorial vas a ver:

  • Qué es behavioral cloning y en qué se diferencia del reinforcement learning.
  • Qué grabar, cuánto grabar, y cómo no envenenar el dataset.
  • El pipeline completo con Unity ML-Agents y Sentis para inferencia.

1. Demo

Imitation learning: grabar, entrenar, inferir Pipeline en tres fases: el jugador genera demos, el modelo aprende offline, el NPC ejecuta en runtime.

2. Concepto y arquitectura

Imitation Learning (también Behavioral Cloning) entrena una red que predice la acción del jugador a partir de la observación actual. No necesita reward function; necesita demostraciones. Es la forma más rápida de transferir “estilo humano” a un NPC.

El pipeline cabe en un diagrama:

Pipeline imitation learning
FASE 1: GRABARjugador juegasensores + inputs(obs, act) por frameFASE 2: ENTRENARred neuronalaprende mappingsupervised (offline)FASE 3: INFERIRNPC en runtimemodelo .onnxsensores → accióndataset.onnxUnity (juego)Python (PyTorch/ML-Agents)Unity (Sentis)

Tres fases: el jugador genera pares (obs, act) que entrenan una red offline; el modelo entrenado se despliega en el NPC.

Fase 1 es el jugador jugando con la cámara puesta. Fase 2 ocurre offline en Python (PyTorch, TensorFlow, ML-Agents trainer). Fase 3 es el .onnx corriendo en Unity con Sentis.

2.1 ¿Qué es behavioral cloning y cómo se diferencia del reinforcement learning?

Behavioral cloning (BC) es supervised learning puro. Dataset = lista de pares (observación, acción). La red aprende un mapping obs → act minimizando el error contra las acciones del jugador. Es exactamente el mismo problema matemático que clasificar imágenes: cambias “perro/gato” por “acelerar/frenar”.

Reinforcement learning (RL) aprende por trial and error. No hay dataset; hay un agente que actúa, recibe una recompensa (reward), y ajusta su política para maximizar reward acumulado. No necesita un humano que demuestre, pero sí necesita que tú diseñes una función de reward que capture “buen comportamiento”. Y diseñar reward es notoriamente difícil: si premias matar enemigos, el agente exploitará cualquier glitch que multiplique kills.

BC vs RL: comparativa rápida
BCRLNecesitaDatosTiempo trainEstiloRiesgodemos humanas10 min – 20 hminutos – horashumano (copia)distribution shiftreward functionmillones de pasoshoras – díasóptimo (explota)reward hacking

Dos enfoques para enseñar a un agente. BC copia, RL explora. Cada uno tiene su precio.

Para la mayoría de juegos comerciales, BC llega a “se siente humano” en una tarde. RL llega a “es imbatible” en una semana. No es lo mismo y rara vez quieres lo segundo.

2.2 ¿Qué grabar exactamente?

Una observación es el vector de sensores que ve el agente. Una acción es el input que produce el jugador. Tienes que decidir ambos antes de grabar un solo frame.

Para un NPC de carreras:

obs = [
  velocidad_actual,           // float
  raycast_frente,             // float (distancia)
  raycast_izq, raycast_der,   // float, float
  angulo_a_proximo_waypoint,  // float
  distancia_a_proximo_wp      // float
]                              // total: 6 floats

act = [
  throttle,    // float [-1, 1]
  steering     // float [-1, 1]
]              // total: 2 floats

Para un NPC de FPS:

obs = [
  hp_propia, ammo, hp_enemigo_visible,
  raycast_8_direcciones,        // 8 floats
  enemigo_en_fov (0/1),
  distancia_al_enemigo,
  cobertura_cercana (0/1)
]                                // ~14 floats

act = [
  mover_x, mover_y,    // continuo
  disparar (0/1),      // discreto
  recargar (0/1),
  saltar (0/1)
]                       // mix continuo/discreto

Frecuencia de muestreo típica: 10 a 30 Hz. Más de 30 genera datos redundantes (frames casi idénticos). Menos de 10 pierde transiciones rápidas. Si tu juego corre a 60 FPS, graba cada 2 o 3 frames.

2.3 Distribution shift: el problema principal de BC

Este es el motivo por el que BC tiene mala fama entre quienes lo prueban una vez y se rinden.

El agente entrenado solo conoce los estados que el jugador visitó. Si el jugador nunca chocó contra una pared, el modelo no sabe qué hacer cuando el NPC se aproxima a una pared en ángulo raro. El NPC comete un error pequeño, aterriza en un estado nuevo, comete un error mayor, cascade. En quince segundos el coche está atravesado en la pista.

Distribution shift cascade
trayectoria jugador (demos)trayectoria agente (deriva)estado inicialpaso 1 OKpaso 2 OKerror pequeñoestado off-distributionerror mayor → cascada

La trayectoria del jugador (verde) y la del agente (roja) coinciden al inicio; un error pequeño en el punto crítico saca al agente de la distribución y la divergencia crece.

La mitigación clásica se llama DAgger (Dataset Aggregation): entrenas un modelo inicial con tus demos, lo ejecutas, y cuando se desvía pides al experto que tome el control y “corrija” la trayectoria. Esas correcciones se suman al dataset. Repites. Cada iteración el modelo ve estados que antes lo descarrilaban.

GAIL (sección 2.8) ataca el mismo problema desde otro ángulo.

2.4 ¿Cuánto dato necesitas?

Depende de la dimensionalidad y de la variedad de situaciones, no de la longitud cronológica.

TareaDemos típicas
Mover hacia un waypoint con obstáculos simples10 – 30 min
Conducir un circuito completo con varios estilos2 – 8 h
Combate FPS contra enemigos variados5 – 20 h
Estrategia con planning a largo plazoBC solo no alcanza

Regla irrenunciable: calidad sobre cantidad. Diez minutos de un jugador competente que cubre situaciones diversas valen más que diez horas de un jugador errático. Si tu dataset contiene a alguien que se choca cada treinta segundos, el modelo aprende a chocarse cada treinta segundos.

2.5 Arquitectura de red típica

No necesitas un transformer. La red para BC en juegos suele ser modesta:

Arquitectura MLP para BC
Inputobs (16-128)Dense 128ReLUDense 128ReLUOutputtanh / softmaxvector sensores~16k params~16k paramsthrottle / steerTotal: 30k – 150k parámetros · inferencia < 1 ms

Una red modesta. Input vector de observaciones → 2 capas densas → output de acciones.

  • Acciones continuas (steering, throttle): output con tanh o lineal.
  • Acciones discretas (atacar/no atacar, qué item usar): output con softmax, loss cross-entropy.
  • Mezcla: dos cabezas en paralelo, una por tipo.

Total: 30k – 150k parámetros. Corre en CPU sin despeinarse. Inferencia: menos de 1 ms por agente en hardware moderno. Puedes correr cien NPCs simultáneos sin tocar el frame budget.

2.6 Unity ML-Agents para imitation

ML-Agents tiene el pipeline integrado, lo que ahorra mucho fontanería:

  1. En el Agent script, defines observaciones (CollectObservations) y acciones (OnActionReceived).
  2. Configuras behavior_type: HeuristicOnly y mapeas el input del jugador a las acciones en Heuristic(). Esto graba demos al jugar.
  3. ML-Agents guarda un .demo con todos los pares (obs, act).
  4. Lanzas el trainer desde Python: mlagents-learn config.yaml --run-id=bc_run. En el YAML pones behavioral_cloning con tu archivo .demo.
  5. El trainer escupe un .onnx. Lo arrastras al campo Model del Agent y cambias behavior_type: InferenceOnly. El NPC ya copia al jugador.

El mismo .onnx lo puedes cargar con Sentis si quieres más control sobre la inferencia o si no usas la abstracción Agent de ML-Agents.

2.7 ¿Cuándo BC NO funciona?

BC falla cuando el jugador toma decisiones basadas en información que el agente no ve. Ejemplos típicos:

  • Planning a largo plazo: el jugador “ahorra” munición porque sabe que el boss está en cinco minutos. El modelo ve solo el estado actual y no entiende por qué hay que ahorrar.
  • Memoria larga: el jugador recuerda dónde había una poción que dejó atrás. Sin recurrencia (LSTM, transformer), tu red no.
  • Información oculta: el jugador escuchó un audio cue. Si no incluyes audio en obs, el modelo nunca lo va a aprender.

En esos casos, BC sirve como inicialización (“warm start”) y encima haces fine-tuning con RL. El agente ya empieza con un comportamiento razonable y RL solo afina, en lugar de aprender de cero.

2.8 GAIL: Generative Adversarial Imitation Learning

GAIL toma la idea de las GANs y la aplica a comportamiento. Dos redes en juego:

  • Generador: el agente. Genera trayectorias.
  • Discriminador: una segunda red que recibe pares (obs, act) e intenta clasificar “esto vino de un humano” vs “esto vino del agente”.

El generador entrena para engañar al discriminador (que sus trayectorias se confundan con las humanas). El discriminador entrena para no dejarse engañar. En equilibrio, el agente produce comportamiento indistinguible del demostrador.

GAIL: discriminador adversario
demos humanasagente actúa(obs, act) real(obs, act) fakediscriminadorred clasificadora”real”(humano)“fake”(agente)reward implícito (engañar al discriminador)

El discriminador recibe pares (obs, act) de dos fuentes y clasifica real/fake. Su confianza alimenta el reward implícito que ajusta al agente.

Ventaja sobre BC puro: el discriminador empuja al agente a explorar y volver a la distribución del experto, lo que mitiga el distribution shift. Desventaja: entrenar dos redes adversarias es más inestable y más lento. ML-Agents incluye GAIL como reward signal; lo combinas con un trainer RL (PPO típicamente).

3. Pseudocódigo

function recordDemo(player: PlayerInput, agent: Sensors, dt: Float) -> Dataset
    samples = []
    while playing
        obs = agent.readSensors()
        act = player.currentAction()
        samples.append((obs, act))
        wait(dt)
    return samples

function trainBC(dataset: Dataset, epochs: Int) -> Model
    model = mlp(dataset.obsDim, 128, 128, dataset.actDim)
    for epoch in 0..epochs
        for (obs, act) in dataset.shuffle()
            pred = model(obs)
            loss = mse(pred, act)        # cross_entropy si discreto
            backprop(loss)
            optimizer.step()
    return model

function deploy(model: Model, agent: Agent)
    while alive
        obs = agent.readSensors()
        act = model(obs)
        agent.applyAction(act)

Tres ideas a notar: la grabación es síncrona con el dt del juego, el entrenamiento es offline y no toca Unity, y el deploy es una llamada al modelo por frame. El modelo no aprende en runtime; lee y produce.

4. Implementación en Unity / C#

Dos componentes: uno que graba demos en disco mientras el jugador juega, y otro que carga el modelo entrenado y lo ejecuta como NPC. El snippet asume sensores ya implementados como helpers.

using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Unity.Sentis;

// 1. GRABACIÓN ──────────────────────────────────────────
public class DemoRecorder : MonoBehaviour {
    public AgentSensors sensors;       // tu wrapper de raycasts + estado
    public PlayerInput  input;         // tu lectura de teclado/gamepad
    public float        sampleHz = 20f;
    StreamWriter writer;
    float nextSample;

    void Start() {
        var path = Path.Combine(Application.persistentDataPath, $"demo_{System.DateTime.Now:yyyyMMdd_HHmmss}.csv");
        writer = new StreamWriter(path);
    }

    void Update() {
        if (Time.time < nextSample) return;
        nextSample = Time.time + 1f / sampleHz;

        float[] obs = sensors.Read();              // vector de observación
        float[] act = input.CurrentAction();       // vector de acción
        writer.WriteLine(string.Join(",", obs) + "|" + string.Join(",", act));
    }

    void OnDestroy() { writer?.Close(); }
}

// 2. INFERENCIA ─────────────────────────────────────────
public class BCAgent : MonoBehaviour {
    public ModelAsset    onnxModel;    // arrastrado desde el editor
    public AgentSensors  sensors;
    public AgentActuator actuator;     // aplica throttle/steering/etc

    Worker worker;
    Tensor inputTensor;

    void Start() {
        var model = ModelLoader.Load(onnxModel);
        worker = new Worker(model, BackendType.CPU);
    }

    void Update() {
        float[] obs = sensors.Read();
        inputTensor?.Dispose();
        inputTensor = new Tensor<float>(new TensorShape(1, obs.Length), obs);

        worker.Schedule(inputTensor);
        var output = worker.PeekOutput() as Tensor<float>;
        float[] act = output.DownloadToArray();

        actuator.Apply(act);
    }

    void OnDestroy() {
        inputTensor?.Dispose();
        worker?.Dispose();
    }
}

Entre la grabación y la inferencia ocurre el paso que no está en Unity: pasas el CSV a Python, entrenas el modelo con ML-Agents o un script PyTorch corto, exportas a ONNX, y vuelves a Unity con el .onnx listo.

5. En otros engines

  • Godot: graba inputs y observaciones a JSON desde GDScript o C#. El entrenamiento se hace fuera (Python + PyTorch). Para inferencia, importa el .onnx con godot-onnx-runtime (plugin) o llamando a ONNX Runtime nativo.
  • Unreal: similar pipeline. La grabación va en C++ o Blueprints; la inferencia usa NNE (Neural Network Engine, integrado en Unreal 5.3+) que carga ONNX directamente. La parte de Python es idéntica.
  • JavaScript / web: TensorFlow.js te permite entrenar Y correr en el navegador. Útil para prototipos de ghost cars o para juegos web que quieren un NPC que aprenda del jugador sin servidor.

El núcleo no cambia entre engines: dataset de pares (obs, act), red modesta entrenada offline, modelo cargado en runtime. Lo que cambia es la API de inferencia.

6. Quiz

Pon a prueba lo que entendiste

Responde una por una. La explicación aparece al elegir, correcta o no.

  1. Tu agente BC se comporta como un jugador novato durante los primeros segundos y después acaba estampado contra la pared o dando vueltas en círculo. ¿Por qué probablemente?

  2. Quieres robustez extra ante distribution shift sin grabar mucho más dataset. ¿Qué eliges entre BC puro, RL de cero, o GAIL?

  3. Resulta que algunas de tus demos las grabó un jugador que usaba un exploit (saltar contra una pared para clipear). ¿Qué hace tu modelo BC con esos datos?

  4. Tu compañero propone subir la red de 128 a 2048 neuronas por capa para 'mejorar la calidad'. ¿Por qué rara vez ayuda en BC para juegos?