{"version":1,"generatedAt":"2026-11-11T02:10:55Z","site":"https://alexisalulema.com","posts":[{"slug":"harness-an-agent-that-improves-its-own-instructions","lang":"en","title":"Harness²: an agent that improves its own instructions (and a judge that always said yes)","description":"I turned the core idea of Harness², from Google Research, into a live demo: improve an agent by changing its execution harness, not the model, in a Propose → Probe → Compose loop. With 1.5B and 2B models on CPU and the same model acting as judge, all 26 comparisons I recorded accepted the proposed change, 18 of them with exactly the same scores, and the judge rewarded answers with the arithmetic backwards. This post covers what I built, how I measured it, why an optimizer has no way of knowing whether it improved without an independent verifier, and under what conditions I'd use Harness² in my next projects.","publishDate":"2026-11-10","updatedDate":null,"tags":["agents","llm","llm-evaluation","prompt-engineering","ollama","python","fastapi"],"translationSlug":"harness-un-agente-que-mejora-sus-propias-instrucciones","url":"https://alexisalulema.com/blog/harness-an-agent-that-improves-its-own-instructions/","series":null,"headings":[{"depth":2,"slug":"what-the-paper-proposes","text":"What the paper proposes"},{"depth":2,"slug":"the-first-version-was-a-mock-up","text":"The first version was a mock-up"},{"depth":2,"slug":"the-constraints-shaped-the-design-again","text":"The constraints shaped the design (again)"},{"depth":2,"slug":"how-the-demo-works","text":"How the demo works"},{"depth":2,"slug":"small-models-and-json","text":"Small models and JSON"},{"depth":2,"slug":"shipping-it-the-demo-that-got-destroyed-mid-run","text":"Shipping it: the demo that got destroyed mid-run"},{"depth":2,"slug":"the-judge-that-always-said-yes","text":"The judge that always said yes"},{"depth":2,"slug":"why-this-happens","text":"Why this happens"},{"depth":2,"slug":"what-i-deliberately-didnt-do","text":"What I deliberately didn’t do"},{"depth":2,"slug":"what-id-do-differently","text":"What I’d do differently"},{"depth":2,"slug":"would-i-use-it-in-my-projects","text":"Would I use it in my projects?"},{"depth":2,"slug":"what-im-taking-away","text":"What I’m taking away"}],"body":"This is the second post in the **Paper → Live demo → Honest post** series: take a recent paper, build a demo anyone can try, measure it carefully, and report what came out. [The first one](/blog/the-dreaming-agent-three-level-memory-with-a-small-model/) was about memory for agents. This one is about a question I find more uncomfortable: **can an agent improve itself when nobody tells it whether it's doing well?**\n\nThe paper is **Harness²**, presented in *Harness²: Recursive Agent Harnessing for an Open World* by **Ruiyao Xu, Yanfei Chen, Zhongying CuiZhu, Bhavana Dalvi Mishra, Yifei Ming, Han Yu, Rujun Han, Chen-Yu Lee and Tomas Pfister**, from Google ([arXiv:2610.14224](https://arxiv.org/abs/2610.14224)). What hooked me was how practical the idea is: most of us will never retrain a model, but we all write prompts, rules, tool guidance and memories for our agents. If that can be improved systematically, it's a lever within anyone's reach.\n\nI also had a more personal reason to try it. Several of my demos are agents whose prompts I tuned by hand, run after run: in the [dreaming agent](/blog/the-dreaming-agent-three-level-memory-with-a-small-model/) I changed the prompt examples again and again until the model stopped copying them. If Harness² can do that work systematically, it's a tool I want in my next projects. So this demo is also a test bench: **I wanted to know whether I'll use it, and under what conditions.**\n\nThe usual disclaimer, out of respect for the authors: **this is not a reproduction of the paper, and it doesn't evaluate it.** My demo takes the shape of the loop and simplifies it a lot, with very small models on CPU. My numbers say something about *my* version, not about Harness².\n\nAnd as in the previous posts, I built it together with a coding assistant, in long sessions of decisions and measurements, with a devlog that records every problem and how it was solved.\n\n## What the paper proposes\n\nThe starting point is that an agent's performance doesn't depend only on the model, but on its **execution harness**: everything around the model while it works. In the paper that's eight components, each one an editable file: system prompt, guardrails, memory, skills, tool guidance, scripts, subagents and a middleware plugin.\n\nThe problem they tackle is \"open-world\" tasks: tasks nobody saw when the harness was designed and that, when they have to be solved, have no verifier with the right answer. They propose two levels of recursion:\n\n- **At the task level**, a **Propose → Probe → Compose** step. *Propose*: the current harness runs as a reference, and two edited candidates are proposed, one with general improvements (planning, tool use) and one with domain-specific knowledge. *Probe*: each candidate runs on the same task, and its trajectory is **contrasted** with the reference to see whether the edit fixed the weakness it targeted, changed nothing, or caused a regression. *Compose*: an editor reads that evidence and decides which changes to keep, which to revise, or whether to stay with the current harness, and every decision has to rest on a concrete observation.\n- **At the harnessing level**, an improvement library that accumulates experience across tasks: what worked, in what situation and with what evidence, so later tasks start from a better place.\n\nThey report gains of up to 11.5 points on WorkBuddy-Bench, 4.5 on JobBench and 11.8 percentage points on LAB, with GLM-5 and Gemini 3.5 Flash, without touching the models' weights.\n\nOne detail matters for everything that follows: the paper's Probe **is not a score**. It's a contrast between executions, interpreted by a capable editor model that justifies each decision with evidence. The authors themselves say that evidence is imperfect, because of the randomness of executions and the biases models share.\n\n## The first version was a mock-up\n\nI'm telling this because it's part of the story. The first version of the demo had the whole interface: you picked a scenario, watched the Propose, Probe and Compose phases advance, the metrics go up, and the optimized output next to the original. It looked great. But the traces were **precomputed**: the demo replayed examples written in advance and didn't run any model.\n\nThe demo's panel said so, but a demo showing improvements that never happened is exactly what this series wants to avoid. I hid it from the site and the goal changed: make the loop run for real, with a real model, even if that meant a slower demo and less flashy results. The mock-up survives as **Cached** mode, labeled as an illustrative example.\n\n## The constraints shaped the design (again)\n\nThe demo lives on the site's ephemeral infrastructure, with its usual rules: everything local with Ollama and open models, zero external APIs, CPU only, and no state. With that, the gap with the paper is large:\n\n| Paper | My version | Why |\n|---|---|---|\n| GLM-5 and Gemini 3.5 Flash | Qwen2.5 1.5B Instruct (default) and Gemma 2 2B, on CPU | Zero-API rule and a 2 vCPU / 4 GiB pod |\n| Eight harness components as files | Only lines appended to the system prompt | Simplicity; the \"memory\" or \"tool guidance\" labels are just labels |\n| Two candidates per step (general and domain) | One candidate per iteration | Time: every model call costs seconds on CPU |\n| Probe: trajectory contrast interpreted by an editor | A judge scoring 0 to 100, **the same model** that proposes | There's no other model available inside the pod |\n| Compose: selective decision justified with evidence | Accepted if the candidate's score is higher | It's the simplest rule that can be shown |\n| Improvement library across tasks | Nothing persists between runs | Ephemeral, stateless demo |\n| WorkBuddy-Bench, JobBench, LAB | Short scenarios inspired by those names | They are not runs of the official benchmarks |\n\nThe table already hints at the problem. Of the whole loop, the piece I simplified the most is exactly the one the paper treats most carefully: how to know whether a change helped.\n\n## How the demo works\n\nYou pick a model, a domain, and write a task. The engine builds two test cases: your task as is, and a variation with a domain-specific instruction (in WorkBuddy, for example, \"separate known facts from assumptions and order actions by dependency\").\n\n1. **Base.** The agent answers both cases with a minimal instruction: *\"You are an AI assistant. Complete the task directly and concisely in no more than 70 words.\"*\n2. **Propose.** The same model receives the task and the current instructions, and returns a JSON with a new instruction, the component it belongs to, and its justification.\n3. **Probe.** The agent answers both cases again, now with the candidate instruction added to its system prompt. The same model, as judge, receives the four answers and scores each one from 0 to 100.\n4. **Compose.** If the candidate's average beats the current version's, the instruction is kept. Otherwise it's discarded.\n\nEverything streams over SSE: you see each token as it's generated, how many tokens were spent, and which phase it's in. A run allows one or two iterations and a budget of up to 8,192 tokens. At the end, the demo shows the base and optimized answers side by side, with the instructions that were added.\n\n## Small models and JSON\n\nThe first live runs failed before reaching any interesting question, and almost always for the same reason: format.\n\nGemma, when asked for JSON matching a schema, sometimes returned **the schema itself**: `{\"type\": \"object\", \"properties\": {...}}` instead of the values. The judge could do it too, and strict validation rejected the whole judgment. The fix had three parts: a concrete example answer in every prompt (\"return values, not the schema\"), a function that unwraps that envelope when it shows up, and a retry with explicit feedback when the output doesn't validate.\n\nEven so, of the 18 runs I recorded in the WorkBuddy scenario, **7 ended without a result** because the proposal was invalid JSON: in all of them, an unterminated string after roughly 390 to 490 characters, which is consistent with the output hitting the proposal's 120-token limit: the models write longer instructions and justifications than fit. Those runs stayed in the record as failures; they weren't repeated until something pretty came out.\n\n## Shipping it: the demo that got destroyed mid-run\n\nOn CPU, two iterations take about 4.5 minutes with Qwen and about 7 with Gemma. That forced the pod up to 2 vCPU and 4 GiB, about 5 minutes of startup time (the image weighs 2.8 GB with both models inside), and an SSE keepalive every 10 seconds so the connection wouldn't look dead.\n\nWhat I didn't expect was this: in production, a long run ended with a generic model error. The model was fine. The problem was the infrastructure. The site's gateway recorded activity **only once**, when the request arrived, and the process that cleans up idle environments destroys them after 8 minutes without activity. A run streaming tokens for several minutes was, from the gateway's point of view, an abandoned environment. It deleted it while the model was still writing.\n\nThe fix was for the gateway to renew activity while it streams a response the client is still receiving. If the client leaves, data stops flowing and the renewal stops, so the session limit still works. That case is now in the gateway's local test too. No earlier demo had needed responses that long, and this one exposed the assumption.\n\n## The judge that always said yes\n\nWith the loop running for real, the part that matters arrived: measuring. The repository has three batches of local runs in the WorkBuddy scenario, with two iterations, temperature 0.2 and both models, keeping everything: every generation, every proposal, every judge decision and every error.\n\nThe first result: **all 26 completed comparisons accepted the candidate.** No proposal was rejected. The only rejection in the project is a unit test with a mocked model.\n\nThe second one I found by counting the scores. In **18 of the 26** comparisons, the judge gave exactly the same numbers: 85 and 80 for the current version, 90 and 85 for the candidate. An average of 82.5 versus 87.5, five points in favor of the change, again and again, with different instructions, different models and different tasks. That doesn't look like an evaluation. It looks like a small model that learned the shape of the answer expected from it.\n\nThe third is the clearest, because it can be checked without opinion. I added a task with arithmetic: two engineers with 3 hours each and 12 checks of 45 minutes. Each engineer can do 180 / 45 = 4, so together they complete **8, and 4 remain**. In one of the Qwen runs:\n\n- The base answer said: 4 completed and 8 remaining.\n- The optimized answer said: 4 completed and 8 remaining.\n- The final harness had **two nearly identical instructions**, accepted in consecutive iterations.\n- The judge preferred the candidate both times, 87.5 versus 82.5, and in its rationale wrote that the base version \"accurately calculates the remaining checks\".\n\nNone of the six final answers in that batch gave both numbers right. In a later study, with two arithmetic controls and three repetitions, the agent with the \"optimized\" instructions got **2 of 6** right, and the base agent **4 of 6**. These are very small samples and say nothing general about the models, but they do say something concrete about this optimizer: its acceptances don't track the quality of the answers.\n\n## Why this happens\n\nWhen the same model proposes the change and judges it, the loop has no source of information it didn't have from the start. The judge shares the proposer's blind spots: if the model doesn't know that 180 / 45 is 4 per engineer, it won't notice the answer got it wrong either. What it can notice is that the candidate answer is longer, more specific, or mentions more things from the task, and that's what it rewards. The judge's rationales say it almost literally: \"more detailed\", \"more specific\".\n\nThis doesn't contradict the paper; it's the extreme version of a limitation the paper acknowledges. Harness² doesn't ask a small model for a score: it contrasts full executions, with a capable editor that has to justify each decision with concrete evidence, and even so its authors warn about shared biases. My demo removed exactly those safeguards. The result shows what happens when they're missing.\n\n## What I deliberately didn't do\n\n- **No improvement library.** The paper's second level of recursion, the one that accumulates experience across tasks, doesn't exist in the demo. Every run starts from scratch.\n- **Not the eight components.** Everything accepted ends up as text in the system prompt.\n- **No independent judge.** There's no other model in the pod, and using an external API would break the zero-API rule.\n- **No human review, yet.** The repo has a more careful study on questions about the future of artificial general intelligence: instructions frozen before seeing the evaluation questions, randomized A/B pairs, and blind packets ready for two reviewers. But **nobody has reviewed them yet**, so I'm not claiming anything about the quality of those answers.\n\nAnd the honest limitation of the measurement: synthetic tasks, few runs, and the rubric checks were done by the coding assistant, not by a blinded person. These are exploratory observations, not a benchmark.\n\n## What I'd do differently\n\nIf I started over, I'd put something in the Probe that the model doesn't control. For the arithmetic task, that's trivial: a function that compares the numbers with the right answer and rejects any candidate that makes it worse, no matter what the judge thinks. Open-ended tasks have no such function, but they do have partial checks: that the answer follows the requested structure, that it doesn't invent data that isn't in the task, that it doesn't exceed the requested length. They're the same checks I did by hand with the rubric, turned into code.\n\nI'd also let the judge say \"I don't know\" and count a tie or a tiny difference as a rejection. An optimizer that never rejects isn't optimizing; it's just piling up text. It's the same lesson as the [dreaming agent](/blog/the-dreaming-agent-three-level-memory-with-a-small-model/) and the [trip planner](/blog/a-trip-planner-that-reasons-without-generating/): the model proposes, the code decides.\n\n## Would I use it in my projects?\n\nThis was the underlying question, and the answer has two parts.\n\n**This demo's version, no.** A loop that accepts everything it proposes doesn't improve an agent: it adds text to it. In the arithmetic run it ended with two nearly identical instructions and the same wrong answer. If I set it to tune the prompts of another project, it would give me the feeling of progress without the progress.\n\n**The pattern, yes, with conditions.** What the demo taught me is what I'd need for it to be worth it:\n\n- **A signal that doesn't come from the model that proposes.** Checks in code where they exist, or a more capable editor that contrasts executions, as in the paper. A judge the same size as the proposer isn't enough.\n- **A set of tasks with known answers** that works as a regression check: if a change makes any of them worse, it's rejected, whatever the judge says.\n- **Use it at design time, not in production.** Every step multiplies the model calls. On a CPU pod that's minutes per iteration; as a tool for designing a harness before shipping a demo, with a larger model, the cost does make sense.\n\nThe first place I'd try it is the dreaming agent. There I already have what this demo lacked: a benchmark of 28 questions with known answers, run three times per condition. That's the independent signal an optimizer needs to know whether it improved.\n\n## What I'm taking away\n\nThree ideas stay with me.\n\nThe first: **an optimizer is only as good as its signal.** Propose and Compose are the flashy part of the loop, but the Probe decides everything. If the signal is noise, the loop piles up noise with great confidence.\n\nThe second: **a judge that never rejects is an alarm, not a result.** 26 out of 26 acceptances looked like a success until I counted the scores. From now on, in any evaluation with a model as judge, the first thing I'll look at is the distribution of its scores and how many times it said no.\n\nThe third: **verifiable checks are worth more than their size.** An arithmetic task, which has nothing to do with the purpose of the demo, is what showed that the acceptances didn't track quality. Without it, all I'd have is scores going up.\n\nNone of this takes anything away from Harness². It leaves me wanting to try what the demo doesn't have: a capable editor that contrasts executions and justifies every change, and a library that learns across tasks, which is where the paper places its bet. Thanks to its authors for framing so clearly a question that anyone writing prompts for agents should be asking.\n\nYou can request the demo directly at [alexisalulema.com/projects?demo=harness2-playground](https://alexisalulema.com/projects?demo=harness2-playground): it's provisioned when you request it, runs on CPU with Qwen or Gemma, and destroys itself. Write a task, watch every proposal and every score, and notice how many times the judge says no. The code, with the full devlog and the records of every run, failed ones included, is at [github.com/alulema/harness-playground](https://github.com/alulema/harness-playground).\n\nIf the judge gives you 87.5 versus 82.5, now you know why."},{"slug":"harness-un-agente-que-mejora-sus-propias-instrucciones","lang":"es","title":"Harness²: un agente que mejora sus propias instrucciones (y un juez que siempre dijo que sí)","description":"Llevé a un demo en vivo la idea central de Harness², de Google Research: mejorar un agente cambiando su harness de ejecución, no el modelo, en un ciclo Propose → Probe → Compose. Con modelos de 1.5B y 2B en CPU y el mismo modelo como juez, las 26 comparaciones que registré aceptaron el cambio propuesto, 18 de ellas con exactamente los mismos puntajes, y el juez premió respuestas con la aritmética invertida. Este post cuenta qué construí, cómo lo medí, por qué sin un verificador independiente un optimizador no tiene cómo saber si mejoró, y en qué condiciones usaría Harness² en mis próximos proyectos.","publishDate":"2026-11-10","updatedDate":null,"tags":["agents","llm","llm-evaluation","prompt-engineering","ollama","python","fastapi"],"translationSlug":"harness-an-agent-that-improves-its-own-instructions","url":"https://alexisalulema.com/es/blog/harness-un-agente-que-mejora-sus-propias-instrucciones/","series":null,"headings":[{"depth":2,"slug":"lo-que-propone-el-paper","text":"Lo que propone el paper"},{"depth":2,"slug":"la-primera-versión-era-una-maqueta","text":"La primera versión era una maqueta"},{"depth":2,"slug":"las-restricciones-definieron-el-diseño-otra-vez","text":"Las restricciones definieron el diseño (otra vez)"},{"depth":2,"slug":"cómo-funciona-el-demo","text":"Cómo funciona el demo"},{"depth":2,"slug":"los-modelos-pequeños-y-el-json","text":"Los modelos pequeños y el JSON"},{"depth":2,"slug":"publicarlo-el-demo-que-se-destruía-a-mitad-de-la-corrida","text":"Publicarlo: el demo que se destruía a mitad de la corrida"},{"depth":2,"slug":"el-juez-que-siempre-dijo-que-sí","text":"El juez que siempre dijo que sí"},{"depth":2,"slug":"por-qué-pasa-esto","text":"Por qué pasa esto"},{"depth":2,"slug":"lo-que-deliberadamente-no-hice","text":"Lo que deliberadamente no hice"},{"depth":2,"slug":"lo-que-haría-distinto","text":"Lo que haría distinto"},{"depth":2,"slug":"lo-usaría-en-mis-proyectos","text":"¿Lo usaría en mis proyectos?"},{"depth":2,"slug":"lo-que-me-llevo","text":"Lo que me llevo"}],"body":"Este es el segundo post de la serie **Paper → Demo en vivo → Post honesto**: tomar un paper reciente, construir un demo que cualquiera pueda probar, medirlo con cuidado y contar lo que salió. [El primero](/es/blog/el-agente-que-suena-memoria-en-tres-niveles-con-un-modelo-pequeno/) fue sobre memoria para agentes. Este es sobre una pregunta que me parece más incómoda: **¿puede un agente mejorarse a sí mismo sin que nadie le diga si lo está haciendo bien?**\n\nEl paper es **Harness²**, presentado en *Harness²: Recursive Agent Harnessing for an Open World* por **Ruiyao Xu, Yanfei Chen, Zhongying CuiZhu, Bhavana Dalvi Mishra, Yifei Ming, Han Yu, Rujun Han, Chen-Yu Lee y Tomas Pfister**, de Google ([arXiv:2610.14224](https://arxiv.org/abs/2610.14224)). La idea me atrapó por lo práctica: la mayoría de nosotros no va a reentrenar un modelo, pero todos escribimos prompts, reglas, guías de herramientas y memorias para nuestros agentes. Si eso se puede mejorar de forma sistemática, es una palanca que está al alcance de cualquiera.\n\nY tenía una razón más personal para probarlo. Varios de mis demos son agentes cuyos prompts ajusté a mano, corrida tras corrida: en el [agente que sueña](/es/blog/el-agente-que-suena-memoria-en-tres-niveles-con-un-modelo-pequeno/) cambié los ejemplos de los prompts una y otra vez para que el modelo dejara de copiarlos. Si Harness² puede hacer ese trabajo de forma sistemática, es una herramienta que quiero en mis próximos proyectos. Así que este demo es también un banco de pruebas: **quería saber si lo voy a usar, y en qué condiciones.**\n\nLa aclaración de siempre, por respeto a los autores: **esto no es una reproducción del paper ni lo evalúa.** Mi demo toma la forma del ciclo y la simplifica mucho, con modelos muy pequeños y en CPU. Mis números dicen algo sobre *mi* versión, no sobre Harness².\n\nY como en los posts anteriores, lo construí junto con un asistente de código, en sesiones largas de decisiones y mediciones, con una bitácora que registra cada problema y cómo se resolvió.\n\n## Lo que propone el paper\n\nEl punto de partida es que el desempeño de un agente no depende solo del modelo, sino de su **harness de ejecución**: todo lo que rodea al modelo cuando trabaja. En el paper son ocho componentes, cada uno como un archivo editable: system prompt, guardrails, memoria, skills, guía de herramientas, scripts, subagentes y un plugin de middleware.\n\nEl problema que atacan es el de las tareas de \"mundo abierto\": tareas que nadie vio al diseñar el harness y que, en el momento de resolverlas, no tienen un verificador con la respuesta correcta. Proponen dos niveles de recursión:\n\n- **A nivel de tarea**, un paso **Propose → Probe → Compose**. *Propose*: se ejecuta el harness actual como referencia y se proponen dos candidatos editados, uno con mejoras generales (planificación, uso de herramientas) y otro con conocimiento específico del dominio. *Probe*: cada candidato se ejecuta sobre la misma tarea, y su trayectoria se **contrasta** con la de referencia para ver si la edición corrigió la debilidad que apuntaba, no cambió nada o causó una regresión. *Compose*: un editor lee esas evidencias y decide qué cambios conservar, cuáles revisar o si quedarse con el harness actual, y cada decisión tiene que apoyarse en una observación concreta.\n- **A nivel de harnessing**, una biblioteca de mejoras que acumula experiencia entre tareas: qué funcionó, en qué situación y con qué evidencia, para que las siguientes tareas partan mejor.\n\nReportan mejoras de hasta 11.5 puntos en WorkBuddy-Bench, 4.5 en JobBench y 11.8 puntos porcentuales en LAB, con GLM-5 y Gemini 3.5 Flash, sin tocar los pesos de los modelos.\n\nUn detalle importa para todo lo que viene: el Probe del paper **no es un puntaje**. Es un contraste entre ejecuciones, interpretado por un modelo editor capaz, que justifica cada decisión con evidencia. Los propios autores dicen que esa evidencia es imperfecta, por la aleatoriedad de las ejecuciones y por los sesgos que comparten los modelos.\n\n## La primera versión era una maqueta\n\nLo cuento porque es parte de la historia. La primera versión del demo tenía la interfaz completa: elegías un escenario, veías las fases Propose, Probe y Compose avanzar, las métricas subir y la salida optimizada al lado de la original. Se veía muy bien. Pero las trazas eran **precalculadas**: el demo reproducía ejemplos escritos de antemano, no ejecutaba ningún modelo.\n\nEl panel del demo lo decía, pero un demo que muestra mejoras que nunca ocurrieron es exactamente lo que esta serie quiere evitar. Lo oculté del sitio y el objetivo cambió: que el ciclo corriera de verdad, con un modelo de verdad, aunque eso significara un demo más lento y resultados menos vistosos. La maqueta sobrevive como modo **Cached**, rotulado como ejemplo ilustrativo.\n\n## Las restricciones definieron el diseño (otra vez)\n\nEl demo vive en la infraestructura efímera del sitio, con sus reglas de siempre: todo local con Ollama y modelos abiertos, cero APIs externas, CPU only y sin estado. Con eso, la distancia con el paper es grande:\n\n| Paper | Mi versión | Por qué |\n|---|---|---|\n| GLM-5 y Gemini 3.5 Flash | Qwen2.5 1.5B Instruct (predeterminado) y Gemma 2 2B, en CPU | Regla de cero APIs y un pod de 2 vCPU / 4 GiB |\n| Ocho componentes del harness como archivos | Solo líneas que se agregan al system prompt | Simplicidad; las etiquetas \"memoria\" o \"guía de herramientas\" son solo etiquetas |\n| Dos candidatos por paso (general y de dominio) | Un candidato por iteración | Tiempo: cada llamada al modelo cuesta segundos en CPU |\n| Probe: contraste de trayectorias interpretado por un editor | Un juez que puntúa de 0 a 100, **el mismo modelo** que propone | No hay otro modelo disponible dentro del pod |\n| Compose: decisión selectiva justificada con evidencia | Se acepta si el puntaje del candidato es mayor | Es la regla más simple que se puede mostrar |\n| Biblioteca de mejoras entre tareas | Nada persiste entre corridas | Demo efímero, sin estado |\n| WorkBuddy-Bench, JobBench, LAB | Escenarios cortos inspirados en esos nombres | No son corridas de los benchmarks oficiales |\n\nLa tabla ya anticipa el problema. De todo el ciclo, la pieza que más simplifiqué es justamente la que el paper trata con más cuidado: cómo saber si un cambio ayudó.\n\n## Cómo funciona el demo\n\nEliges un modelo, un dominio y escribes una tarea. El motor arma dos casos de prueba: tu tarea tal cual y una variación con una instrucción propia del dominio (en WorkBuddy, por ejemplo, \"separa los hechos conocidos de los supuestos y ordena las acciones por dependencia\").\n\n1. **Base.** El agente responde los dos casos con una instrucción mínima: *\"Eres un asistente de IA. Completa la tarea de forma directa y concisa, en no más de 70 palabras.\"*\n2. **Propose.** El mismo modelo recibe la tarea y las instrucciones actuales, y devuelve un JSON con una instrucción nueva, el componente al que pertenece y su justificación.\n3. **Probe.** El agente vuelve a responder los dos casos, ahora con la instrucción candidata agregada a su system prompt. El mismo modelo, como juez, recibe las cuatro respuestas y puntúa de 0 a 100 cada una.\n4. **Compose.** Si el promedio del candidato supera al de la versión actual, la instrucción se incorpora. Si no, se descarta.\n\nTodo se transmite por SSE: ves cada token mientras se genera, cuántos tokens se gastaron y en qué fase está. Una corrida admite una o dos iteraciones y un presupuesto de hasta 8,192 tokens. Al final, el demo muestra la respuesta base y la optimizada lado a lado, con las instrucciones que se agregaron.\n\n## Los modelos pequeños y el JSON\n\nLas primeras corridas en vivo fallaron antes de llegar a cualquier pregunta interesante, y casi siempre por lo mismo: el formato.\n\nGemma, al pedirle un JSON que cumpliera un esquema, a veces devolvía **el esquema mismo**: `{\"type\": \"object\", \"properties\": {...}}` en lugar de los valores. El juez también podía hacerlo, y la validación estricta rechazaba el juicio completo. La solución tuvo tres partes: un ejemplo concreto de respuesta en cada prompt (\"devuelve valores, no el esquema\"), una función que desenvuelve ese sobre cuando aparece y un reintento con retroalimentación explícita si la salida no valida.\n\nAun así, en las 18 corridas que registré del escenario WorkBuddy, **7 terminaron sin resultado** porque la propuesta era un JSON inválido: en todas, una cadena sin cerrar después de unos 390 a 490 caracteres, lo que es consistente con que la salida chocara contra el límite de 120 tokens de la propuesta: los modelos escriben instrucciones y justificaciones más largas de lo que cabe. Esas corridas quedaron en el registro como fallos, no se repitieron hasta que saliera algo bonito.\n\n## Publicarlo: el demo que se destruía a mitad de la corrida\n\nEn CPU, dos iteraciones tardan unos 4.5 minutos con Qwen y unos 7 con Gemma. Eso obligó a subir el pod a 2 vCPU y 4 GiB, darle unos 5 minutos de arranque (la imagen pesa 2.8 GB con los dos modelos dentro) y mandar un keepalive por SSE cada 10 segundos para que la conexión no pareciera muerta.\n\nLo que no esperaba fue esto: en producción, una corrida larga terminaba con un error genérico del modelo. El modelo estaba bien. El problema era la infraestructura. El gateway del sitio registraba actividad **una sola vez**, al recibir la solicitud, y el proceso que limpia entornos inactivos los destruye a los 8 minutos sin actividad. Una corrida que transmite tokens durante varios minutos, desde el punto de vista del gateway, era un entorno abandonado. Lo borraba mientras el modelo seguía escribiendo.\n\nEl arreglo fue que el gateway renueve la actividad mientras transmite una respuesta que el cliente sigue recibiendo. Si el cliente se va, dejan de pasar datos y la renovación se detiene, así que el límite de la sesión sigue funcionando. Ese caso ahora también está en la prueba local del gateway. Ningún demo anterior había necesitado respuestas tan largas, y fue este el que expuso el supuesto.\n\n## El juez que siempre dijo que sí\n\nCon el ciclo corriendo de verdad, llegó la parte que importa: medir. El repositorio tiene tres lotes de corridas locales en el escenario WorkBuddy, con dos iteraciones, temperatura 0.2 y los dos modelos, guardando todo: cada generación, cada propuesta, cada decisión del juez y cada error.\n\nEl primer resultado: **las 26 comparaciones completas aceptaron el candidato.** Ninguna propuesta fue rechazada. El único rechazo que existe en el proyecto es una prueba unitaria con un modelo simulado.\n\nEl segundo lo encontré contando los puntajes. En **18 de las 26** comparaciones, el juez dio exactamente los mismos números: 85 y 80 para la versión actual, 90 y 85 para la candidata. Un promedio de 82.5 contra 87.5, cinco puntos a favor del cambio, una y otra vez, con instrucciones distintas, modelos distintos y tareas distintas. Eso no parece una evaluación. Parece un modelo pequeño que aprendió la forma de la respuesta que se espera de él.\n\nEl tercero es el más claro, porque se puede verificar sin opinión. Agregué una tarea con aritmética: dos ingenieros con 3 horas cada uno y 12 revisiones de 45 minutos. Cada ingeniero puede hacer 180 / 45 = 4, así que entre los dos completan **8 y quedan 4**. En una de las corridas con Qwen:\n\n- La respuesta base dijo: 4 completadas y 8 pendientes.\n- La optimizada dijo: 4 completadas y 8 pendientes.\n- El harness final tenía **dos instrucciones casi idénticas**, aceptadas en iteraciones sucesivas.\n- El juez prefirió la candidata las dos veces, 87.5 contra 82.5, y en su justificación escribió que la versión base \"calcula correctamente las revisiones restantes\".\n\nNinguna de las seis respuestas finales de ese lote dio los dos números correctos. En un estudio posterior, con dos controles aritméticos y tres repeticiones, el agente con las instrucciones \"optimizadas\" acertó **2 de 6**, y el agente base **4 de 6**. Son muestras muy pequeñas y no dicen nada general sobre los modelos, pero sí dicen algo concreto sobre este optimizador: sus aceptaciones no siguen a la calidad de las respuestas.\n\n## Por qué pasa esto\n\nCuando el mismo modelo propone el cambio y lo juzga, el ciclo no tiene ninguna fuente de información que no tuviera desde el principio. El juez comparte los mismos puntos ciegos que el proponente: si el modelo no sabe que 180 / 45 es 4 por ingeniero, tampoco va a notar que la respuesta se equivocó. Lo que sí puede notar es que la respuesta candidata es más larga, más específica o menciona más cosas de la tarea, y eso es lo que premia. Las justificaciones del juez lo dicen casi literalmente: \"más detallada\", \"más específica\".\n\nEsto no contradice al paper; es la versión extrema de una limitación que el paper reconoce. Harness² no le pide un puntaje a un modelo pequeño: contrasta ejecuciones completas, con un editor capaz que debe justificar cada decisión con evidencia concreta, y aun así sus autores advierten sobre los sesgos compartidos. Mi demo quitó justamente esas protecciones. El resultado muestra lo que pasa cuando no están.\n\n## Lo que deliberadamente no hice\n\n- **Sin biblioteca de mejoras.** El segundo nivel de recursión del paper, el que acumula experiencia entre tareas, no existe en el demo. Cada corrida empieza de cero.\n- **Sin los ocho componentes.** Todo lo aceptado termina como texto en el system prompt.\n- **Sin un juez independiente.** No hay otro modelo en el pod, y usar una API externa rompería la regla de cero APIs.\n- **Sin revisión humana, todavía.** El repo tiene un estudio más cuidadoso sobre preguntas acerca del futuro de la IA general: instrucciones congeladas antes de ver las preguntas de evaluación, pares A/B aleatorizados y paquetes ciegos listos para dos revisores. Pero **ninguna persona los ha revisado aún**, así que no afirmo nada sobre la calidad de esas respuestas.\n\nY la limitación honesta de la medición: tareas sintéticas, pocas corridas, y las revisiones contra la rúbrica las hizo el asistente de código, no una persona a ciegas. Son observaciones exploratorias, no un benchmark.\n\n## Lo que haría distinto\n\nSi empezara de nuevo, pondría en el Probe algo que el modelo no controle. En la tarea de aritmética, eso es trivial: una función que compare los números con la respuesta correcta y rechace cualquier candidato que la empeore, sin importar lo que opine el juez. En tareas abiertas no existe esa función, pero sí hay controles parciales: que la respuesta cumpla la estructura pedida, que no invente datos que no están en la tarea, que no supere el largo solicitado. Son las mismas comprobaciones que hice a mano con la rúbrica, convertidas en código.\n\nTambién haría que el juez pudiera decir \"no sé\" y que un empate o una diferencia mínima contara como rechazo. Un optimizador que nunca rechaza no está optimizando; solo acumula texto. Es la misma lección del [agente que sueña](/es/blog/el-agente-que-suena-memoria-en-tres-niveles-con-un-modelo-pequeno/) y del [planificador de viajes](/es/blog/un-planificador-de-viajes-que-razona-sin-generar/): el modelo propone, el código decide.\n\n## ¿Lo usaría en mis proyectos?\n\nEsta era la pregunta de fondo, y la respuesta tiene dos partes.\n\n**La versión de este demo, no.** Un ciclo que acepta todo lo que propone no mejora un agente: le agrega texto. En la corrida de la aritmética terminó con dos instrucciones casi iguales y la misma respuesta equivocada. Si lo pusiera a ajustar los prompts de otro proyecto, me daría la sensación de progreso sin el progreso.\n\n**El patrón, sí, con condiciones.** Lo que el demo me enseñó es qué necesitaría para que valga la pena:\n\n- **Una señal que no venga del modelo que propone.** Comprobaciones en código cuando existan, o un editor más capaz que contraste ejecuciones, como en el paper. Un juez del mismo tamaño que el proponente no basta.\n- **Un conjunto de tareas con respuesta conocida** que funcione como control de regresión: si un cambio empeora alguna, se rechaza, diga lo que diga el juez.\n- **Usarlo en diseño, no en producción.** Cada paso multiplica las llamadas al modelo. En un pod de CPU eso son minutos por iteración; como herramienta para diseñar el harness antes de publicar un demo, con un modelo más grande, el costo sí tiene sentido.\n\nEl primer lugar donde lo probaría es el agente que sueña. Ahí ya tengo lo que este demo no tenía: un benchmark de 28 preguntas con respuestas conocidas, corrido tres veces por condición. Esa es la señal independiente que necesita un optimizador para saber si mejoró.\n\n## Lo que me llevo\n\nMe quedo con tres ideas.\n\nLa primera: **un optimizador vale lo que vale su señal.** Propose y Compose son la parte vistosa del ciclo, pero el Probe decide todo. Si la señal es ruido, el ciclo acumula ruido con mucha confianza.\n\nLa segunda: **un juez que nunca rechaza es una alarma, no un resultado.** 26 de 26 aceptaciones parecía un éxito hasta que conté los puntajes. Desde ahora, en cualquier evaluación con un modelo como juez, lo primero que voy a mirar es la distribución de los puntajes y cuántas veces dijo que no.\n\nLa tercera: **los controles verificables valen más que su tamaño.** Una tarea de aritmética, que no tiene nada que ver con el propósito del demo, fue lo que demostró que las aceptaciones no seguían a la calidad. Sin ella, solo tendría puntajes que suben.\n\nNada de esto le quita mérito a Harness². Me deja con más ganas de probar lo que el demo no tiene: un editor capaz que contraste ejecuciones y justifique cada cambio, y una biblioteca que aprenda entre tareas, que es donde el paper pone su apuesta. Gracias a sus autores por plantear con tanta claridad una pregunta que cualquiera que escriba prompts para agentes debería hacerse.\n\nPuedes solicitar el demo directamente en [alexisalulema.com/es/projects?demo=harness2-playground](https://alexisalulema.com/es/projects?demo=harness2-playground): se provisiona cuando lo pides, corre en CPU con Qwen o Gemma y se destruye solo. Escribe una tarea, mira cada propuesta y cada puntaje, y fíjate cuántas veces el juez dice que no. El código, con la bitácora completa y los registros de todas las corridas, incluidas las fallidas, está en [github.com/alulema/harness-playground](https://github.com/alulema/harness-playground).\n\nSi el juez te da 87.5 contra 82.5, ya sabes por qué."},{"slug":"el-agente-que-suena-memoria-en-tres-niveles-con-un-modelo-pequeno","lang":"es","title":"El agente que sueña: memoria en tres niveles con un modelo pequeño (y un RAG ingenuo que le ganó)","description":"Llevé a un demo la arquitectura de memoria de LightMem: un agente que guarda hechos mientras conversa y que, cuando le pides que \"sueñe\", consolida su memoria —fusiona, actualiza, promueve y olvida— con un diff visible. Todo local, con modelos de 3B y 7B. Lo medí contra un RAG ingenuo con tres corridas por condición, y perdió: 79 contra 88 % con 7B, 58 contra 93 % con 3B. Este post cuenta qué construí, dónde se rompe y por qué, con modelos pequeños, la mejor memoria resultó ser la que menos reescribe.","publishDate":"2026-10-18","updatedDate":null,"tags":["agents","llm","memory","rag","ollama","python","fastapi","embeddings"],"translationSlug":"the-dreaming-agent-three-level-memory-with-a-small-model","url":"https://alexisalulema.com/es/blog/el-agente-que-suena-memoria-en-tres-niveles-con-un-modelo-pequeno/","series":null,"headings":[{"depth":2,"slug":"lo-que-propone-el-paper","text":"Lo que propone el paper"},{"depth":2,"slug":"las-restricciones-definieron-el-diseño-otra-vez","text":"Las restricciones definieron el diseño (otra vez)"},{"depth":2,"slug":"cómo-funciona-el-demo","text":"Cómo funciona el demo"},{"depth":2,"slug":"los-modelos-copian-los-ejemplos","text":"Los modelos copian los ejemplos"},{"depth":2,"slug":"el-benchmark","text":"El benchmark"},{"depth":2,"slug":"v1-el-control-gana-por-20-puntos","text":"v1: el control gana por 20 puntos"},{"depth":2,"slug":"v2-arreglar-una-pieza-expone-la-siguiente","text":"v2: arreglar una pieza expone la siguiente"},{"depth":2,"slug":"con-3b-la-brecha-se-dispara","text":"Con 3B, la brecha se dispara"},{"depth":2,"slug":"por-qué-gana-el-rag-ingenuo","text":"Por qué gana el RAG ingenuo"},{"depth":2,"slug":"publicarlo-el-demo-que-tardaba-90-segundos-por-llamada","text":"Publicarlo: el demo que tardaba 90 segundos por llamada"},{"depth":2,"slug":"lo-que-deliberadamente-no-hice","text":"Lo que deliberadamente no hice"},{"depth":2,"slug":"lo-que-haría-distinto","text":"Lo que haría distinto"},{"depth":2,"slug":"lo-que-me-llevo","text":"Lo que me llevo"}],"body":"Este post abre una serie nueva en el blog: **Paper → Demo en vivo → Post honesto**. La idea es simple: tomar un paper reciente, construir un demo que cualquiera pueda probar, medirlo con cuidado y contar lo que salió, incluido lo que no salió como el paper sugiere. El primero es sobre memoria para agentes.\n\nHace tiempo que le daba vueltas a una idea: un agente conversacional no debería recordarlo todo como un acta, sino hacer algo parecido a lo que hacemos nosotros al dormir. Durante el día acumulamos detalles sueltos; de noche, el cerebro decide qué se queda, qué se fusiona con lo que ya sabíamos y qué se descarta. Un agente con memoria de corto, mediano y largo plazo, y una fase offline que la reorganiza. Lo tenía como una de esas ideas que uno anota para \"algún día\".\n\nPor eso fue una alegría encontrarme con **LightMem**, presentado en *Lightweight LLM Agent Memory with Small Language Models* por **Jiaquan Zhang, Chaoning Zhang, Shuxu Chen, Zhenzhen Huang, Pengcheng Zheng, Zhicheng Wang, Ping Guo, Fan Mo, Sung-Ho Bae, Jie Zou, Jiwei Wei y Yang Yang**, aceptado en ACL 2026 ([arXiv:2604.07798](https://arxiv.org/abs/2604.07798)). Es genial ver una idea que uno tenía rondando convertida en un trabajo formal, con una arquitectura clara, experimentos serios y un nombre. Todo el crédito del diseño es de ellos; lo mío es un demo inspirado en su arquitectura y una medición en condiciones mucho más modestas que las suyas.\n\nUna aclaración que hago por respeto al trabajo de los autores: **esto no es una reproducción del paper ni lo evalúa.** Cambié piezas importantes (más abajo detallo cuáles y por qué), así que mis números dicen algo sobre *mi* versión con modelos locales pequeños, no sobre LightMem.\n\nY lo mismo que en los posts anteriores: lo construí junto con un asistente de código, en sesiones largas de decisiones y mediciones, con una bitácora que registra cada problema y cómo se resolvió.\n\n## Lo que propone el paper\n\nLightMem organiza la memoria de un agente en tres niveles:\n\n- **STM** (memoria de corto plazo): los últimos turnos de la conversación, tal cual.\n- **MTM** (memoria de mediano plazo): resúmenes semánticos de lo conversado, con su embedding, información temporal y estadísticas de acceso.\n- **LTM** (memoria de largo plazo): conocimiento consolidado y des-identificado, que en el paper es un **grafo** de entidades y relaciones.\n\nLo interesante es *quién* administra esa memoria. En vez de un modelo grande haciendo todo, el paper usa **modelos de lenguaje pequeños** (SLM) especializados: un **Controller** que convierte la pregunta en consultas y decide dónde buscar, un **Selector** que filtra los candidatos recuperados y un **Writer** que resume cada turno en entradas de MTM. Cada cierto número de turnos, una fase **offline** consolida la MTM en la LTM: fusiona, actualiza, refuerza o deja decaer lo que tiene poca evidencia.\n\nEl nombre del paper lo dice: memoria *liviana*. Reportan mejoras sobre A-MEM en LoCoMo y una recuperación con latencia p50 de 83 ms. Dos detalles importan para lo que viene: el Selector está **afinado con LoRA**, y la respuesta final la genera **GPT-4o-mini**. Los modelos pequeños administran la memoria; uno grande responde.\n\n## Las restricciones definieron el diseño (otra vez)\n\nComo los demos anteriores, este vive en la infraestructura efímera del sitio, así que heredó sus reglas:\n\n- **Todo local.** Ollama y modelos abiertos. Cero APIs externas, cero API keys.\n- **CPU only**, en un pod pequeño.\n- **Sin estado.** La memoria vive en un SQLite efímero: cuando el demo se destruye, se olvida todo. Muy apropiado para un agente que sueña.\n\nEso me obligó a apartarme del paper en varios puntos:\n\n| Paper | Mi versión | Por qué |\n|---|---|---|\n| SLM de 1–1.5B, Selector afinado con LoRA | Qwen 2.5 de 3B y 7B, sin fine-tuning; el mismo modelo con prompts distintos | El fine-tuning queda fuera del alcance de un demo |\n| Responder: GPT-4o-mini | Responder local, el mismo modelo pequeño | Regla de cero APIs. Es la diferencia más grande |\n| LTM como grafo | LTM como lista de entradas | Simplicidad |\n| Consolidación automática cada 10–15 turnos | Botón **🌙 Dream** | Que la consolidación se vea |\n| Embeddings en inglés (MiniLM) | `bge-m3`, multilingüe | Mis conversaciones mezclan español e inglés |\n\n## Cómo funciona el demo\n\nEn cada turno, el **Controller** decide si el mensaje trae información nueva y escribe las consultas de búsqueda. El **Retriever** busca por similitud coseno en MTM y LTM, el **Selector** se queda con lo relevante y el **Responder** contesta con eso más los últimos turnos. Después de responder, el **Writer** extrae hechos atómicos del mensaje del usuario (\"The user lives in Quito\") y los guarda en MTM.\n\nTodas las salidas de los módulos son JSON validado contra un esquema, con un reintento y un *fallback* determinista si el modelo falla. La traza de cada turno muestra qué decidió cada módulo y cuánto tardó.\n\nEl sueño es la parte que más me divertía construir. Al pulsar **Dream**:\n\n1. Las entradas se agrupan por similitud, temas o entidades compartidas.\n2. El modelo propone operaciones sobre cada grupo: `MERGE`, `UPDATE`, `PROMOTE` (de MTM a LTM) y `FORGET`.\n3. Un **validador determinista** revisa cada propuesta antes de aplicarla.\n4. Lo aprobado se aplica en una sola transacción, sin borrar nada de verdad: lo reemplazado queda marcado y apunta a lo que lo reemplazó.\n\nLa UI muestra el resultado como un diff: en verde lo nuevo, tachado en rojo lo reemplazado u olvidado, y aparte las propuestas que el validador rechazó, con el motivo.\n\nEse validador se convirtió en el corazón del proyecto, por una razón que se repite en este blog: **el modelo propone, el código decide.** Rechaza un `UPDATE` que introduce un nombre o un número que no estaba en ninguna de las entradas originales, uno que pierde el valor vigente (\"trabaja como diseñador UX\" → \"trabajaba como arquitecto\"), un `FORGET` sobre conocimiento de largo plazo, o un `PROMOTE` sin evidencia suficiente.\n\n## Los modelos copian los ejemplos\n\nAntes del benchmark ya aparecieron las primeras lecciones, y casi todas vinieron del mismo lugar: los ejemplos de los prompts.\n\nEl Writer tenía un ejemplo con un perro llamado Toby que vive en Madrid. En la primera conversación de prueba, la memoria terminó diciendo que el usuario tenía un perro llamado Toby y vivía en Madrid, aunque el usuario nunca lo había dicho. Cambié los ejemplos por entidades neutrales y apareció \"Bilbao\". Los cambié otra vez y el 3B empezó a registrar que el usuario era profesor, tenía una panadería o tenía un gato, todo sacado de los ejemplos.\n\nLa solución que mejor funcionó fue el *grounding*: cada nombre propio y cada número de un hecho tiene que aparecer en el mensaje del usuario (con tolerancia a traducciones: \"Lisbon\" vale si el mensaje dice \"Lisboa\"). Atrapa los nombres y los números inventados. No atrapa los sustantivos comunes: \"teacher\" no se puede verificar contra un mensaje en español sin traducir. Ese límite reaparece más adelante.\n\n## El benchmark\n\nPara medir, armé un usuario ficticio, Mateo, con 8 sesiones de conversación: 45 turnos en español e inglés, con paráfrasis (su loro Kiwi aparece en cinco sesiones), cambios (se muda de Valparaíso a Montevideo, deja la arquitectura por el diseño UX, deja de ser vegetariano) y ruido (el clima, el tráfico, lo que desayunó). Después, 28 preguntas en sesiones nuevas: hechos simples, hechos actualizados, preguntas que cruzan sesiones, preguntas temporales y preguntas sobre cosas que Mateo nunca dijo, donde lo correcto es decir \"no lo sé\".\n\nCuatro condiciones, mismo modelo y mismos datos:\n\n| Condición | Memoria |\n|---|---|\n| A | Sin memoria: solo los últimos turnos |\n| B | **RAG ingenuo**: cada frase del usuario guardada literal; las 8 más parecidas van al Responder |\n| C | El pipeline completo, sin sueño |\n| D | El pipeline más un Dream al final de cada sesión |\n\nLa condición B es deliberadamente tonta: no extrae, no resume, no consolida. Es el control.\n\nTres corridas por condición, porque con temperatura 0 y seed fija los resultados igual varían. (Lo aprendí en la primera prueba.)\n\n## v1: el control gana por 20 puntos\n\n| Condición | Exactitud (7B) |\n|---|---|\n| A — sin memoria | 18 % |\n| **B — RAG ingenuo** | **89 %** |\n| C — pipeline | 69 % |\n| D — pipeline + Dream | 66 % |\n\nEl RAG ingenuo le ganó al pipeline por 20 puntos, y el sueño restó 3 más. Reconstruí la memoria de una corrida para entender por qué, y encontré un error en cada módulo:\n\n- **El Controller decidía no buscar.** Ante \"¿qué idioma estoy aprendiendo?\" marcaba que no hacía falta memoria, y el Responder inventaba la respuesta.\n- **El Selector podaba de más.** Las preguntas temporales necesitan unir dos hechos (\"era arquitecto en Estudio Faro\" y \"tomó un curso de UX\"); el Selector se quedaba con uno y la respuesta era \"no tengo información\".\n- **El Dream volvió permanentes los errores del Writer.** De \"mi loro Kiwi aprendió a silbar\", el Writer había sacado \"the user's name is Kiwi\". El Dream lo interpretó como un cambio de nombre y lo consolidó. En 2 de 3 corridas, el agente terminó diciéndole a Mateo **\"Te llamas Kiwi\"**.\n- **El Dream olvidó la alergia al maní.** Un hecho dicho una sola vez parece ruido, y `FORGET` no distingue entre \"desayunó medialunas\" y \"es alérgico al maní\".\n\nEl validador hizo su trabajo: bloqueó 18 actualizaciones que perdían el valor vigente. Pero el validador revisa la *forma*, no la *verdad*. \"Te llamas Kiwi\" no inventa ningún nombre: Kiwi estaba en el mensaje.\n\n## v2: arreglar una pieza expone la siguiente\n\nCon ese diagnóstico hice cuatro cambios, validados primero con conversaciones de desarrollo y no con el benchmark, para no ajustar el sistema a las preguntas:\n\n1. Buscar siempre, aunque el Controller diga que no hace falta.\n2. Pasarle al Responder los textos originales que eligió el Selector (al menos 3, con fecha), en vez de su resumen.\n3. El grounding del Writer.\n4. Solo permitir olvidar eventos puntuales, nunca hechos sobre salud, trabajo, familia, mascotas o planes.\n\n| Condición | v1 | v2 |\n|---|---|---|\n| B — RAG ingenuo | 89 % | 88 % |\n| C — pipeline | 69 % | **79 %** |\n| D — pipeline + Dream | 66 % | **79 % ± 9** (71 / 75 / 89) |\n\nDiez puntos de mejora, y el RAG ingenuo sigue arriba, con la mitad de la latencia (3.4 s contra 7.9 s por turno) y un tercio de las llamadas al modelo.\n\nLo más interesante de v2 fue lo que el arreglo destapó. Al pasar los textos originales en vez del resumen, el Responder empezó a ver \"se llama Mateo\" **y** \"se llama Kiwi\". Como el prompt le pide confiar en lo más reciente, C respondió \"Te llamas Kiwi\" en las tres corridas. En v1 el resumen del Selector ocultaba ese error del Writer, y lo ocultaba por casualidad. El sueño, a veces, lo reparaba: fusionaba \"tiene un loro llamado Kiwi\" con \"se llama Kiwi\". A veces no. Misma configuración, de 71 a 89 % según la corrida.\n\nY aparecieron corrupciones que pasan todas las reglas: el Dream escribió que Mateo **vive en Pagora** (Pagora es la empresa donde trabaja) y que \"vive en Valparaíso (antes era alérgico al maní)\". Todo está *grounded*: cada palabra viene de un hecho real. Las reglas evitan que el modelo invente; no evitan que mezcle.\n\n## Con 3B, la brecha se dispara\n\nEl paper usa modelos de 1–1.5B, así que corrí lo mismo con Qwen 2.5 3B:\n\n| Condición | 7B | 3B |\n|---|---|---|\n| B — RAG ingenuo | 88 % | **93 %** |\n| C — pipeline | 79 % | 58 % |\n| D — pipeline + Dream | 79 % | 49 % |\n\nEl RAG ingenuo con 3B fue **la mejor configuración de todo el estudio**, y la más rápida: 1.4 s por turno. El pipeline con el mismo modelo perdió 35 puntos.\n\nAhí está la frase que me llevo de este proyecto: **el modelo pequeño lee bien y escribe mal.** Como lector de frases literales, el 3B iguala o supera al 7B. Como escritor de memoria, se desmorona:\n\n- El Controller marcó como \"sin información nueva\" el **58 %** de los turnos, entre ellos el nombre de Mateo, su cumpleaños y su loro. Esos hechos nunca llegaron a la memoria.\n- Los ejemplos de los prompts se filtraron como sustantivos comunes, justo donde el grounding no llega: \"teacher\" 3 veces, \"bakery\" 6, \"cat\" 9 (para un loro).\n- El sueño consolidó la basura junto con lo real: \"es alérgico al maní **y va a la piscina**\", \"tomó un curso de UX **y aprendió a silbar una canción nueva**\" (lo del loro, otra vez), \"tiene **amigos llamados Kiwi y Ramón**\" (Ramón es el perro).\n\nHay una ironía que me hizo reír: mi [post anterior sobre un chatbot RAG](/es/blog/chatbot-rag-sobre-mi-propio-blog-el-regreso-despues-de-tres-anos/) era exactamente la condición B, y aquí le ganó a una arquitectura mucho más elaborada.\n\n## Por qué gana el RAG ingenuo\n\nEl RAG ingenuo guarda lo que el usuario dijo, literal. No hay ningún paso donde un modelo pequeño pueda reescribir, perder o inventar un hecho. El pipeline, en cambio, reescribe la memoria dos veces: el Writer convierte cada mensaje en hechos y el Dream convierte esos hechos en otros. Cada reescritura es una oportunidad de equivocarse, y la consolidación convierte esos errores en permanentes: el error deja de ser una frase rara entre muchas y pasa a ser *el* recuerdo.\n\nEsto no contradice al paper; lo acota. Su Selector está afinado con LoRA para esa tarea, y la respuesta la escribe GPT-4o-mini. Mi resultado dice que sin esas piezas grandes, la arquitectura no se sostiene sola con un modelo pequeño. Es una pregunta distinta, y la respuesta en mis condiciones es clara.\n\n## Publicarlo: el demo que tardaba 90 segundos por llamada\n\nFaltaba ponerlo en línea, y en CPU apareció el problema que más me enseñó de la parte de infraestructura.\n\nLa primera medición con el 3B, limitado a 2 vCPU como el pod del demo, fue desastrosa: el Controller tardaba entre 87 y 180 segundos, y el Writer se quedaba sin tiempo en 4 de 10 turnos. Incluso el 1.5B tardaba ~50 segundos por llamada. No cuadraba con nada que hubiera visto antes.\n\nLa causa: dentro del contenedor, Ollama ve **los 10 núcleos del host**, no los 2 que le asigna el límite, y lanza un hilo por núcleo. El límite de CPU del kernel reparte el tiempo de 2 núcleos entre 10 hilos que necesitan coordinarse capa a capa, y todos terminan esperándose entre sí. Al fijar los hilos al límite real (`num_thread=2`), la misma llamada pasó de **91 a 4 segundos**.\n\nCon eso arreglado, lo que queda es la física de un modelo en CPU: lee el prompt rápido y genera unos pocos tokens por segundo, así que cada módulo cuesta lo que escribe. El pipeline hace 2 o 3 llamadas antes de responder:\n\n| qwen2.5:3b | Respuesta (p50) | Writer (en segundo plano) |\n|---|---|---|\n| 2 vCPU | ~75 s | ~100 s |\n| 4 vCPU | ~30 s | ~50 s |\n\nMe quedé con el 3B y 4 vCPU. Para que la espera sea tolerable, el Writer corre *después* de responder: ves la respuesta primero y, unos segundos después, los hechos que aprendió aparecen en la memoria con la marca \"aprendiendo…\".\n\nY como el resultado principal es que el RAG ingenuo gana, el demo no lo esconde. Tiene un selector con tres modos: **LightMem**, **RAG** y **Comparar**, que responde la misma pregunta de las dos formas lado a lado, con sus latencias y sus trazas. Es la forma más honesta que se me ocurrió de mostrar la arquitectura: dejar que la compares tú.\n\n## Lo que deliberadamente no hice\n\n- **Sin fine-tuning.** Un Selector afinado con LoRA, como el del paper, es probablemente la pieza que más cambiaría los números. Quedó fuera del alcance.\n- **Sin un modelo grande para responder.** Es la regla de cero APIs, y es la mayor diferencia con el paper.\n- **Sin grafo en la LTM.** Una lista plana alcanza para mostrar la consolidación.\n- **Sin multiusuario.** Un solo usuario, memoria efímera.\n\nY la limitación honesta de la medición: un usuario ficticio y 28 preguntas, puntuadas con palabras clave y revisadas a mano (corregí dos errores del scoring en esa revisión). Es un estudio de caso, no un benchmark general.\n\n## Lo que haría distinto\n\nSi empezara de nuevo, separaría **guardar** de **interpretar**. Guardaría siempre la frase literal del usuario, como el RAG ingenuo, y dejaría que el Writer y el sueño produjeran solo *índices* sobre esas frases: etiquetas, vínculos, un resumen para buscar mejor. Nunca reemplazos. Si el sueño se equivoca, se pierde un índice, no un recuerdo. Es la misma lección del [planificador de viajes](/es/blog/un-planificador-de-viajes-que-razona-sin-generar/), en otro dominio: sacarle al modelo las decisiones que no puede tomar bien.\n\n## Lo que me llevo\n\nMe quedo con tres ideas.\n\nLa primera: **con modelos locales pequeños, la mejor memoria es la que menos reescribe.** Cada etapa que reformula un hecho es una oportunidad de perderlo, y la consolidación vuelve permanentes esos errores.\n\nLa segunda: **un validador revisa forma, no verdad.** Mis reglas detienen nombres inventados y valores perdidos; no detienen que \"Pagora\" pase de empresa a ciudad. Para eso hace falta algo que entienda la frase, y en este sistema eso es justamente el modelo pequeño que se equivocó.\n\nLa tercera: **arreglar una pieza puede exponer el error de otra.** El \"Te llamas Kiwi\" estuvo ahí desde la primera corrida; el resumen del Selector lo tapaba por casualidad. Sin tres corridas por condición y sin reconstruir la memoria a mano, habría celebrado los 10 puntos de v2 sin ver lo que destaparon.\n\nNada de esto le quita mérito a LightMem. Me deja con más ganas de probar la pieza que no pude: un Selector afinado, que es exactamente donde el paper pone su apuesta. Gracias a sus autores por convertir en un trabajo serio una idea que muchos teníamos dando vueltas.\n\nPuedes probar el agente en [alexisalulema.com/projects](https://alexisalulema.com/es/projects?demo=dreaming-agent): se provisiona cuando lo pides, corre 40 minutos y se destruye solo, con su memoria incluida. Cuéntale algo, pregúntale, pulsa 🌙 Dream y compara con el RAG. El código, con la bitácora completa de decisiones, mediciones y errores, está en [github.com/alulema/dreaming-agent](https://github.com/alulema/dreaming-agent).\n\nSi te dice que te llamas Kiwi, ya sabes por qué."},{"slug":"the-dreaming-agent-three-level-memory-with-a-small-model","lang":"en","title":"The Dreaming Agent: Three-Level Memory with a Small Model (and the Naive RAG That Beat It)","description":"I turned LightMem's memory architecture into a demo: an agent that stores facts as it talks and that, when you ask it to \"dream\", consolidates its memory —merging, updating, promoting and forgetting— as a visible diff. Fully local, with 3B and 7B models. I measured it against a naive RAG with three runs per condition, and it lost: 79 vs 88 % with 7B, 58 vs 93 % with 3B. This post covers what I built, where it breaks, and why, with small models, the best memory turned out to be the one that rewrites the least.","publishDate":"2026-10-18","updatedDate":null,"tags":["agents","llm","memory","rag","ollama","python","fastapi","embeddings"],"translationSlug":"el-agente-que-suena-memoria-en-tres-niveles-con-un-modelo-pequeno","url":"https://alexisalulema.com/blog/the-dreaming-agent-three-level-memory-with-a-small-model/","series":null,"headings":[{"depth":2,"slug":"what-the-paper-proposes","text":"What the paper proposes"},{"depth":2,"slug":"the-constraints-shaped-the-design-again","text":"The constraints shaped the design (again)"},{"depth":2,"slug":"how-the-demo-works","text":"How the demo works"},{"depth":2,"slug":"models-copy-the-examples","text":"Models copy the examples"},{"depth":2,"slug":"the-benchmark","text":"The benchmark"},{"depth":2,"slug":"v1-the-control-wins-by-20-points","text":"v1: the control wins by 20 points"},{"depth":2,"slug":"v2-fixing-one-piece-exposes-the-next","text":"v2: fixing one piece exposes the next"},{"depth":2,"slug":"with-3b-the-gap-explodes","text":"With 3B, the gap explodes"},{"depth":2,"slug":"why-the-naive-rag-wins","text":"Why the naive RAG wins"},{"depth":2,"slug":"shipping-it-the-demo-that-took-90-seconds-per-call","text":"Shipping it: the demo that took 90 seconds per call"},{"depth":2,"slug":"what-i-deliberately-didnt-do","text":"What I deliberately didn’t do"},{"depth":2,"slug":"what-id-do-differently","text":"What I’d do differently"},{"depth":2,"slug":"what-i-take-away","text":"What I take away"}],"body":"This post starts a new series on the blog: **Paper → Live demo → Honest post**. The idea is simple: take a recent paper, build a demo anyone can try, measure it carefully, and report what came out, including what didn't go the way the paper suggests. The first one is about memory for agents.\n\nI'd been turning an idea over for a while: a conversational agent shouldn't remember everything like meeting minutes, but do something closer to what we do when we sleep. During the day we pile up loose details; at night, the brain decides what stays, what merges with what we already knew, and what gets thrown away. An agent with short-, mid- and long-term memory, and an offline phase that reorganizes it. It was one of those ideas you jot down for \"someday\".\n\nSo it was a joy to come across **LightMem**, presented in *Lightweight LLM Agent Memory with Small Language Models* by **Jiaquan Zhang, Chaoning Zhang, Shuxu Chen, Zhenzhen Huang, Pengcheng Zheng, Zhicheng Wang, Ping Guo, Fan Mo, Sung-Ho Bae, Jie Zou, Jiwei Wei and Yang Yang**, accepted at ACL 2026 ([arXiv:2604.07798](https://arxiv.org/abs/2604.07798)). It's great to see an idea you had floating around turned into formal work, with a clear architecture, serious experiments and a name. All the credit for the design is theirs; mine is a demo inspired by their architecture and a measurement under far more modest conditions than theirs.\n\nA clarification I make out of respect for the authors' work: **this is not a reproduction of the paper, nor does it evaluate it.** I changed important pieces (I detail which ones and why below), so my numbers say something about *my* version with small local models, not about LightMem.\n\nAnd the same as in previous posts: I built it together with a coding assistant, over long sessions of decisions and measurements, with a log that records every problem and how it was solved.\n\n## What the paper proposes\n\nLightMem organizes an agent's memory into three levels:\n\n- **STM** (short-term memory): the last turns of the conversation, as they are.\n- **MTM** (mid-term memory): semantic summaries of what was said, with their embedding, temporal information and access statistics.\n- **LTM** (long-term memory): consolidated, de-identified knowledge, which in the paper is a **graph** of entities and relations.\n\nThe interesting part is *who* manages that memory. Instead of one large model doing everything, the paper uses specialized **small language models** (SLMs): a **Controller** that turns the question into queries and decides where to look, a **Selector** that filters the retrieved candidates, and a **Writer** that summarizes each turn into MTM entries. Every so many turns, an **offline** phase consolidates the MTM into the LTM: it merges, updates, reinforces, or lets weakly supported entries decay.\n\nThe paper's name says it: *lightweight* memory. They report improvements over A-MEM on LoCoMo and retrieval with a p50 latency of 83 ms. Two details matter for what follows: the Selector is **fine-tuned with LoRA**, and the final answer is generated by **GPT-4o-mini**. The small models manage the memory; a large one answers.\n\n## The constraints shaped the design (again)\n\nLike the previous demos, this one lives on the site's ephemeral infrastructure, so it inherited its rules:\n\n- **Fully local.** Ollama and open models. Zero external APIs, zero API keys.\n- **CPU only**, on a small pod.\n- **Stateless.** The memory lives in an ephemeral SQLite: when the demo is destroyed, everything is forgotten. Very fitting for an agent that dreams.\n\nThat forced me to depart from the paper in several places:\n\n| Paper | My version | Why |\n|---|---|---|\n| 1–1.5B SLMs, LoRA-tuned Selector | Qwen 2.5 3B and 7B, no fine-tuning; the same model with different prompts | Fine-tuning is out of scope for a demo |\n| Responder: GPT-4o-mini | Local responder, the same small model | Zero-API rule. It's the biggest difference |\n| LTM as a graph | LTM as a list of entries | Simplicity |\n| Automatic consolidation every 10–15 turns | A **🌙 Dream** button | So the consolidation can be seen |\n| English embeddings (MiniLM) | `bge-m3`, multilingual | My conversations mix Spanish and English |\n\n## How the demo works\n\nOn each turn, the **Controller** decides whether the message carries new information and writes the search queries. The **Retriever** searches MTM and LTM by cosine similarity, the **Selector** keeps what's relevant, and the **Responder** answers with that plus the last few turns. After answering, the **Writer** extracts atomic facts from the user's message (\"The user lives in Quito\") and stores them in MTM.\n\nEvery module's output is JSON validated against a schema, with one retry and a deterministic *fallback* if the model fails. Each turn's trace shows what every module decided and how long it took.\n\nThe dream is the part I enjoyed building the most. When you press **Dream**:\n\n1. Entries are grouped by similarity, topics or shared entities.\n2. The model proposes operations on each group: `MERGE`, `UPDATE`, `PROMOTE` (from MTM to LTM) and `FORGET`.\n3. A **deterministic validator** checks each proposal before applying it.\n4. What's approved is applied in a single transaction, without truly deleting anything: what was replaced stays marked and points to what replaced it.\n\nThe UI shows the result as a diff: new entries in green, replaced or forgotten ones struck through in red, and separately the proposals the validator rejected, with the reason.\n\nThat validator became the heart of the project, for a reason that keeps coming up on this blog: **the model proposes, the code decides.** It rejects an `UPDATE` that introduces a name or a number that wasn't in any of the original entries, one that loses the current value (\"works as a UX designer\" → \"used to work as an architect\"), a `FORGET` on long-term knowledge, or a `PROMOTE` without enough evidence.\n\n## Models copy the examples\n\nThe first lessons showed up before the benchmark, and almost all of them came from the same place: the examples in the prompts.\n\nThe Writer had an example with a dog named Toby who lives in Madrid. In the first test conversation, the memory ended up saying the user had a dog named Toby and lived in Madrid, even though the user had never said so. I swapped the examples for neutral entities and \"Bilbao\" showed up. I swapped them again and the 3B started recording that the user was a teacher, owned a bakery or had a cat, all taken from the examples.\n\nThe fix that worked best was *grounding*: every proper noun and every number in a fact has to appear in the user's message (with tolerance for translations: \"Lisbon\" counts if the message says \"Lisboa\"). It catches made-up names and numbers. It doesn't catch common nouns: \"teacher\" can't be checked against a Spanish message without translating. That limit comes back later.\n\n## The benchmark\n\nTo measure it, I built a fictional user, Mateo, with 8 conversation sessions: 45 turns in Spanish and English, with paraphrases (his parrot Kiwi shows up in five sessions), changes (he moves from Valparaíso to Montevideo, leaves architecture for UX design, stops being vegetarian) and noise (the weather, traffic, what he had for breakfast). Then, 28 questions in new sessions: simple facts, updated facts, questions that span sessions, temporal questions, and questions about things Mateo never said, where the right answer is \"I don't know\".\n\nFour conditions, same model and same data:\n\n| Condition | Memory |\n|---|---|\n| A | No memory: only the last turns |\n| B | **Naive RAG**: every user sentence stored verbatim; the 8 most similar go to the Responder |\n| C | The full pipeline, no dream |\n| D | The pipeline plus a Dream at the end of each session |\n\nCondition B is deliberately dumb: it doesn't extract, summarize or consolidate. It's the control.\n\nThree runs per condition, because even with temperature 0 and a fixed seed the results vary. (I learned that on the first test.)\n\n## v1: the control wins by 20 points\n\n| Condition | Accuracy (7B) |\n|---|---|\n| A — no memory | 18 % |\n| **B — naive RAG** | **89 %** |\n| C — pipeline | 69 % |\n| D — pipeline + Dream | 66 % |\n\nThe naive RAG beat the pipeline by 20 points, and the dream took off 3 more. I rebuilt the memory from one run to understand why, and found a bug in every module:\n\n- **The Controller decided not to search.** Asked \"what language am I learning?\", it flagged that no memory was needed, and the Responder made up the answer.\n- **The Selector pruned too much.** Temporal questions need two facts joined (\"was an architect at Estudio Faro\" and \"took a UX course\"); the Selector kept one and the answer was \"I have no information\".\n- **The Dream made the Writer's mistakes permanent.** From \"my parrot Kiwi learned to whistle\", the Writer had extracted \"the user's name is Kiwi\". The Dream read it as a name change and consolidated it. In 2 of 3 runs, the agent ended up telling Mateo **\"Your name is Kiwi\"**.\n- **The Dream forgot the peanut allergy.** A fact said only once looks like noise, and `FORGET` can't tell \"had croissants for breakfast\" from \"is allergic to peanuts\".\n\nThe validator did its job: it blocked 18 updates that lost the current value. But the validator checks *form*, not *truth*. \"Your name is Kiwi\" doesn't invent any name: Kiwi was in the message.\n\n## v2: fixing one piece exposes the next\n\nWith that diagnosis I made four changes, validated first with development conversations and not with the benchmark, so as not to tune the system to the questions:\n\n1. Always search, even if the Controller says it isn't needed.\n2. Pass the Responder the original texts the Selector chose (at least 3, dated), instead of its summary.\n3. The Writer's grounding.\n4. Only allow forgetting one-off events, never facts about health, work, family, pets or plans.\n\n| Condition | v1 | v2 |\n|---|---|---|\n| B — naive RAG | 89 % | 88 % |\n| C — pipeline | 69 % | **79 %** |\n| D — pipeline + Dream | 66 % | **79 % ± 9** (71 / 75 / 89) |\n\nTen points better, and the naive RAG is still on top, with half the latency (3.4 s vs 7.9 s per turn) and a third of the model calls.\n\nThe most interesting part of v2 was what the fix uncovered. Once it got the original texts instead of the summary, the Responder started seeing \"his name is Mateo\" **and** \"his name is Kiwi\". Since the prompt tells it to trust the most recent, C answered \"Your name is Kiwi\" in all three runs. In v1 the Selector's summary hid that Writer mistake, and it hid it by accident. The dream sometimes repaired it: it merged \"has a parrot named Kiwi\" with \"is named Kiwi\". Sometimes it didn't. Same configuration, from 71 to 89 % depending on the run.\n\nAnd corruptions appeared that pass every rule: the Dream wrote that Mateo **lives in Pagora** (Pagora is the company he works for) and that he \"lives in Valparaíso (used to be allergic to peanuts)\". All of it is *grounded*: every word comes from a real fact. The rules stop the model from inventing; they don't stop it from mixing.\n\n## With 3B, the gap explodes\n\nThe paper uses 1–1.5B models, so I ran the same thing with Qwen 2.5 3B:\n\n| Condition | 7B | 3B |\n|---|---|---|\n| B — naive RAG | 88 % | **93 %** |\n| C — pipeline | 79 % | 58 % |\n| D — pipeline + Dream | 79 % | 49 % |\n\nThe naive RAG with 3B was **the best configuration in the whole study**, and the fastest: 1.4 s per turn. The pipeline with the same model lost 35 points.\n\nThat's the line I take away from this project: **the small model reads well and writes badly.** As a reader of verbatim sentences, the 3B matches or beats the 7B. As a writer of memory, it falls apart:\n\n- The Controller flagged **58 %** of turns as \"no new information\", among them Mateo's name, his birthday and his parrot. Those facts never reached memory.\n- The prompt examples leaked in as common nouns, exactly where grounding can't reach: \"teacher\" 3 times, \"bakery\" 6, \"cat\" 9 (for a parrot).\n- The dream consolidated the junk along with the real: \"is allergic to peanuts **and goes to the pool**\", \"took a UX course **and learned to whistle a new song**\" (the parrot, again), \"has **friends named Kiwi and Ramón**\" (Ramón is the dog).\n\nThere's an irony that made me laugh: my [earlier post about a RAG chatbot](/blog/a-rag-chatbot-over-my-own-blog-and-the-comeback-after-three-years/) was exactly condition B, and here it beat a far more elaborate architecture.\n\n## Why the naive RAG wins\n\nThe naive RAG stores what the user said, verbatim. There's no step where a small model can rewrite, lose or invent a fact. The pipeline, instead, rewrites the memory twice: the Writer turns each message into facts and the Dream turns those facts into others. Every rewrite is a chance to get it wrong, and consolidation makes those mistakes permanent: the error stops being one odd sentence among many and becomes *the* memory.\n\nThis doesn't contradict the paper; it bounds it. Their Selector is LoRA-tuned for that task, and the answer is written by GPT-4o-mini. My result says that without those big pieces, the architecture doesn't hold up on its own with a small model. It's a different question, and under my conditions the answer is clear.\n\n## Shipping it: the demo that took 90 seconds per call\n\nIt still had to go online, and on CPU the problem that taught me the most about the infrastructure side showed up.\n\nThe first measurement with the 3B, capped at 2 vCPU like the demo pod, was a disaster: the Controller took between 87 and 180 seconds, and the Writer ran out of time on 4 of 10 turns. Even the 1.5B took ~50 seconds per call. It didn't match anything I'd seen before.\n\nThe cause: inside the container, Ollama sees **the host's 10 cores**, not the 2 the limit gives it, and spawns one thread per core. The kernel's CPU limit splits 2 cores' worth of time across 10 threads that need to sync layer by layer, and they all end up waiting on each other. Pinning the threads to the real limit (`num_thread=2`) took the same call from **91 to 4 seconds**.\n\nWith that fixed, what's left is the physics of a model on CPU: it reads the prompt fast and generates a few tokens per second, so each module costs what it writes. The pipeline makes 2 or 3 calls before answering:\n\n| qwen2.5:3b | Answer (p50) | Writer (in the background) |\n|---|---|---|\n| 2 vCPU | ~75 s | ~100 s |\n| 4 vCPU | ~30 s | ~50 s |\n\nI went with the 3B and 4 vCPU. To make the wait bearable, the Writer runs *after* answering: you see the answer first and, a few seconds later, the facts it learned show up in memory tagged \"learning…\".\n\nAnd since the main result is that the naive RAG wins, the demo doesn't hide it. It has a selector with three modes: **LightMem**, **RAG** and **Compare**, which answers the same question both ways side by side, with their latencies and traces. It's the most honest way I could think of to show the architecture: let you compare it yourself.\n\n## What I deliberately didn't do\n\n- **No fine-tuning.** A LoRA-tuned Selector, like the paper's, is probably the piece that would change the numbers the most. It was out of scope.\n- **No large model to answer.** That's the zero-API rule, and it's the biggest difference from the paper.\n- **No graph in the LTM.** A flat list is enough to show consolidation.\n- **No multi-user.** A single user, ephemeral memory.\n\nAnd the honest limitation of the measurement: one fictional user and 28 questions, scored with keywords and reviewed by hand (I fixed two scoring errors in that review). It's a case study, not a general benchmark.\n\n## What I'd do differently\n\nIf I started over, I'd separate **storing** from **interpreting**. I'd always store the user's verbatim sentence, like the naive RAG, and let the Writer and the dream produce only *indexes* over those sentences: tags, links, a summary to search better. Never replacements. If the dream gets it wrong, you lose an index, not a memory. It's the same lesson as the [trip planner](/blog/a-trip-planner-that-reasons-without-generating/), in a different domain: take away from the model the decisions it can't make well.\n\n## What I take away\n\nThree ideas stay with me.\n\nThe first: **with small local models, the best memory is the one that rewrites the least.** Every stage that rephrases a fact is a chance to lose it, and consolidation makes those mistakes permanent.\n\nThe second: **a validator checks form, not truth.** My rules stop invented names and lost values; they don't stop \"Pagora\" from going from company to city. That takes something that understands the sentence, and in this system that's precisely the small model that got it wrong.\n\nThe third: **fixing one piece can expose another's mistake.** The \"Your name is Kiwi\" was there from the first run; the Selector's summary covered it up by accident. Without three runs per condition and without rebuilding the memory by hand, I'd have celebrated v2's 10 points without seeing what they uncovered.\n\nNone of this takes anything away from LightMem. It leaves me wanting to try the piece I couldn't: a fine-tuned Selector, which is exactly where the paper places its bet. Thanks to its authors for turning into serious work an idea many of us had been mulling over.\n\nYou can try the agent at [alexisalulema.com/projects](https://alexisalulema.com/projects?demo=dreaming-agent): it's provisioned when you request it, runs for 40 minutes and destroys itself, memory included. Tell it something, ask it things, press 🌙 Dream and compare with the RAG. The code, with the full log of decisions, measurements and mistakes, is at [github.com/alulema/dreaming-agent](https://github.com/alulema/dreaming-agent).\n\nIf it tells you your name is Kiwi, now you know why."},{"slug":"que-hacemos-cuando-el-agente-no-aprende-rl-debugging-explicado-desde-cero","lang":"es","title":"¿Qué hacemos cuando el agente no aprende? RL debugging explicado desde cero","description":"Octava parte de la serie Reinforcement Learning desde cero con Agentic Racing. Cómo convertir \"el agente no aprende\" en hipótesis comprobables, contado con la depuración real del piloto RL del proyecto: al menos seis bugs de tubería antes de la primera corrida, ocho corridas de PPO con la fracción de vuelta clavada, una línea base que también tenía un bug, promedios engañosos, terminación y truncamiento en ML-Agents, y los errores de proceso que más tiempo costaron.","publishDate":"2026-10-13","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"what-do-we-do-when-the-agent-doesnt-learn-rl-debugging-explained-from-scratch","url":"https://alexisalulema.com/es/blog/que-hacemos-cuando-el-agente-no-aprende-rl-debugging-explicado-desde-cero/","series":null,"headings":[{"depth":2,"slug":"dónde-nos-quedamos","text":"Dónde nos quedamos"},{"depth":2,"slug":"1-no-aprende-no-es-un-síntoma","text":"1. “No aprende” no es un síntoma"},{"depth":2,"slug":"2-el-sistema-que-hay-que-depurar","text":"2. El sistema que hay que depurar"},{"depth":2,"slug":"3-primero-que-la-tubería-funcione","text":"3. Primero, que la tubería funcione"},{"depth":2,"slug":"4-una-línea-base-que-se-pueda-creer","text":"4. Una línea base que se pueda creer"},{"depth":2,"slug":"5-métricas-que-no-sean-la-recompensa-y-que-no-promedien-de-más","text":"5. Métricas que no sean la recompensa, y que no promedien de más"},{"depth":2,"slug":"6-la-acción-no-es-el-efecto","text":"6. La acción no es el efecto"},{"depth":2,"slug":"7-terminar-o-interrumpir-el-fin-del-episodio-importa","text":"7. Terminar o interrumpir: el fin del episodio importa"},{"depth":2,"slug":"8-cuándo-mirar-ppo","text":"8. Cuándo mirar PPO"},{"depth":2,"slug":"9-exploración-cuando-el-agente-nunca-ve-lo-que-tiene-que-aprender","text":"9. Exploración: cuando el agente nunca ve lo que tiene que aprender"},{"depth":2,"slug":"10-una-corrida-no-es-evidencia","text":"10. Una corrida no es evidencia"},{"depth":2,"slug":"11-los-errores-de-proceso-que-más-costaron","text":"11. Los errores de proceso que más costaron"},{"depth":2,"slug":"12-una-secuencia-de-depuración-aplicada","text":"12. Una secuencia de depuración, aplicada"},{"depth":2,"slug":"13-síntomas-e-hipótesis-con-los-casos-del-proyecto","text":"13. Síntomas e hipótesis, con los casos del proyecto"},{"depth":2,"slug":"conclusión","text":"Conclusión"},{"depth":2,"slug":"referencias","text":"Referencias"}],"body":"> **Serie: Reinforcement Learning desde cero, con Agentic Racing como laboratorio**\n>\n> 1. Parte 1 · [¿Cómo aprende un agente? Enseñando a un automóvil a conducir](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/)\n> 2. Parte 2 · [¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/)\n> 3. Parte 3 · [¿Podemos enseñar imitando? Behavioral Cloning explicado desde cero](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/)\n> 4. Parte 4 · [¿Podemos aprender a comportarnos como un experto? GAIL explicado desde cero](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/)\n> 5. Parte 5 · [¿Podemos combinar imitación y RL? BC, GAIL y PPO en un mismo sistema](/es/blog/podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema/)\n> 6. Parte 6 · [¿Cómo le decimos a un agente qué queremos? Reward engineering explicado desde cero](/es/blog/como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero/)\n> 7. Parte 7 · [¿Qué debería poder ver un agente? Observation design explicado desde cero](/es/blog/que-deberia-poder-ver-un-agente-observation-design-explicado-desde-cero/)\n> 8. Parte 8 · **¿Qué hacemos cuando el agente no aprende? RL debugging explicado desde cero** (este artículo)\n\n## Dónde nos quedamos\n\nLas siete partes anteriores contaron, cada una desde su ángulo, por qué el piloto RL de Agentic Racing\nnunca aprendió a dar vueltas de forma consistente: la recompensa tenía atajos (parte 6), las observaciones no veían las\ncurvas a tiempo (parte 7), PPO optimizaba bien el objetivo equivocado (parte 2), y la imitación que\npodía ayudar nunca se ejecutó (partes 3 a 5).\n\nEsta parte junta todo eso desde la pregunta que el proyecto se hizo durante una semana de septiembre:\n\n> El agente no aprende. ¿Por qué?\n\nLa respuesta corta es que la pregunta estaba mal planteada. \"No aprende\" no es un síntoma: es una\nconclusión. El trabajo de depurar un sistema de RL consiste en convertirla en hipótesis que se puedan\ncomprobar una por una. Este artículo cuenta cómo lo hizo el proyecto, qué funcionó, y qué errores de\nproceso costaron más que cualquier bug.\n\n---\n\n## 1. \"No aprende\" no es un síntoma\n\nDetrás de esa frase hay situaciones muy distintas. Agentic Racing pasó por casi todas:\n\n| Lo que se ve | En Agentic Racing |\n|---|---|\n| no completa ni un episodio | 2 millones de pasos con cero episodios terminados: el auto nunca se movía |\n| la recompensa no cambia | la primera corrida convergió a los 1.85 millones de pasos y quedó plana 18 millones más |\n| la recompensa sube, pero el comportamiento no | la séptima corrida: recompensa +35%, vueltas completas del 2% al 3% |\n| mejora y luego empeora | la tercera corrida subió hasta ~10; la cuarta volvió a ~4.8 |\n| aprende algo, pero no lo que queríamos | la cuarta corrida aprendió a detenerse a propósito |\n\nCada fila pide una investigación distinta. La primera es un bug de la tubería; la segunda puede ser\nun óptimo local; la tercera, una recompensa que mide otra cosa; la cuarta, varianza; la quinta, un\natajo. Tratarlas a todas como \"el agente no aprende\" lleva a probar arreglos al azar.\n\n---\n\n## 2. El sistema que hay que depurar\n\nUn agente de RL no aprende en el vacío. Entre la idea y el aprendizaje hay una cadena larga: el\nsimulador, su comunicación con Python, los sensores, la conversión de decisiones en acciones, la\nfísica, la recompensa, el fin de los episodios, la evaluación y la referencia contra la que se compara.\nCualquier eslabón roto puede producir el mismo síntoma.\n\nEste es el mapa de Agentic Racing, con los bugs reales de su bitácora en cada eslabón:\n\n![Mapa de depuración de Agentic Racing: ocho partes del sistema (player de Unity y su conexión con Python, sensores, decisión y acción, física y pista, recompensa, fin de episodio, evaluación y línea base), cada una con los bugs reales que tuvo el proyecto. El algoritmo de aprendizaje no aparece en el mapa.](/images/hero-debug-dark.svg)\n\nLo que más llama la atención del mapa es lo que no tiene: **el algoritmo**. Las ocho corridas usaron\nPPO, y ninguno de esos bugs se arregló tocándolo. La parte 2 ya lo había mostrado con la tabla de la\nsección 9: lo que cambiaba entre corridas era la recompensa, la percepción del agente o la física.\n\n---\n\n## 3. Primero, que la tubería funcione\n\nAntes de que la primera corrida pudiera entrenar, el proyecto pasó tres días depurando cosas que no\ntenían nada que ver con aprender a conducir. Estos son seis de ellos (hubo más: varios del propio\nbuild del player y un entorno de Python fuera de las versiones que pide `mlagents`):\n\n1. **El player sin gráficos se caía** al registrar el agente. ML-Agents solo reportaba un código de\n   salida; el log real apareció al correr el ejecutable a mano. La solución fue darle un framebuffer por\n   software en lugar del modo sin gráficos.\n2. **La librería nativa de gRPC no estaba donde se la buscaba**, y el player concluía que no había\n   entrenador y pasaba a modo inferencia, sin error visible.\n3. **El comunicador de ML-Agents no funciona compilado con IL2CPP.** Eso obligó a cambiar el player de\n   entrenamiento de Linux a Windows con Mono.\n4. **El tamaño de la observación no coincidía**: se había acordado 12 y llegaban 27 (parte 7).\n5. **El entorno se colgaba a los ~63 000 pasos.** Ya con las observaciones correctas, el entrenamiento\n   corría hasta que Unity frenaba la simulación al perder la ventana el foco, y el entrenador se cansaba\n   de esperar.\n6. **Dos millones de pasos, cero episodios.** Se pedían decisiones pero nunca se ejecutaba la acción\n   (y las observaciones llegaban vacías): el auto no se movía. Era el orden en que se armaban los componentes del auto.\n\nEl patrón que funcionó se repitió: **buscar el log que dice la verdad** (el del player, no el de\nPython), agregar registros puntuales donde la hipótesis lo pedía y, cuando hizo falta, reproducir el\nfallo en otra máquina para descartar el entorno. Y la primera corrida se lanzó recién después de una prueba corta que\nconfirmó que había episodios, acciones y observaciones completas.\n\nLa lección es la más barata de toda la serie: **\"0 episodios completados\" y \"observaciones rellenadas\ncon ceros\" estaban escritos en los logs**. Un entrenamiento que corre no es un entrenamiento que\naprende, y lo primero que hay que verificar es que la experiencia que recibe sea la que creemos.\n\n---\n\n## 4. Una línea base que se pueda creer\n\nLa herramienta de diagnóstico más útil del proyecto no fue una métrica de PPO: fue **un controlador\nescrito a mano** en el mismo entorno. Si él no puede dar la vuelta, el problema no es del aprendizaje.\n\nEl proyecto la construyó tarde, después de la quinta corrida, y la usó mal al principio. La primera\nevaluación tenía un signo invertido en el propio controlador; la segunda mostró autos que morían en\ncualquier parte, siempre pegados a un muro. Después de darle al auto dirección a baja velocidad para\ndespegarse del muro, un episodio llegó al 82% de la vuelta, y eso se leyó como \"el entorno se puede\naprender, el problema es del RL\". La otra mitad del dato, que en promedio el controlador también se\natascaba, resultó ser la importante (parte 6).\n\nY la línea base tenía sus propios bugs. Uno se descubrió recién en octubre, al escribir la parte 7 de\nesta serie: el temporizador que decide cuándo el piloto heurístico da reversa contra un muro contaba\ncinco veces más lento de lo que debía, porque sumaba un paso de física en cada llamada y la función solo\nse llama una vez por decisión. Su umbral, hoy de 1 segundo, equivalía a 5 segundos reales. Ya está\ncorregido, pero estuvo ahí desde la sexta corrida, y también en los experimentos de campo mixto (sección 10).\n\n> Una línea base también es código, y también hay que depurarla.\n\n---\n\n## 5. Métricas que no sean la recompensa, y que no promedien de más\n\nLa parte 6 contó por qué el proyecto construyó un evaluador independiente después de la cuarta corrida:\nla recompensa no decía que la policy nunca frenaba, y el evaluador sí (freno medio de 0.02).\n\nPero un evaluador también puede mentir, y el proyecto lo vivió dos veces:\n\n- **Un promedio que no describía a ningún auto.** En una evaluación del piloto heurístico, el informe\n  daba una velocidad media de 5.3 m/s. El registro de cada auto, una vez por segundo, mostró otra cosa:\n  dos o tres autos manejaban a unos 20 m/s y los otros seis o siete estaban atascados cerca de la\n  salida, en \"diente de sierra\". El promedio no se parecía a ninguno de los dos grupos.\n- **Episodios que no existían.** En otra evaluación, el conteo de episodios terminados por límite de\n  tiempo estaba inflado por reinicios espurios del propio evaluador al cargar el modelo.\n\n![Esquema de nueve autos en una evaluación del piloto heurístico: dos o tres a unos 20 m/s y seis o siete casi detenidos cerca de la salida; la media reportada, 5.3 m/s, no describe a ninguno de los dos grupos.](/images/debug-averages.svg)\n\nLa regla que sale de ahí: **antes de confiar en un resumen, mira algunos casos individuales**. Un\nregistro de una línea por auto y por segundo resolvió en una evaluación lo que varios informes\nagregados no habían mostrado.\n\n---\n\n## 6. La acción no es el efecto\n\nSi la policy pide girar y el auto frena, el problema no está en la policy. En Agentic Racing, el modelo\nde agarre lateral borraba la velocidad lateral del auto en lugar de redirigirla: cualquier giro fuerte\nera un frenazo (parte 1). La bitácora lo leyó como la razón por la que tanto el agente como el piloto\nheurístico no giraban con fuerza: en esa física, girar costaba velocidad.\n\nLa herramienta clave fue registrar **lo que el auto hacía**, no solo lo que la policy pedía: un volcado\nde la trayectoria en cada fin de episodio mostró autos con el volante casi constante, velocidad cero y\ndeslizándose de costado; el auto \"trompea y desliza de lado\", dice la bitácora. Ningún hiperparámetro\nhabría mostrado eso. Pero el volcado mostraba el síntoma, no la causa: el mismo dato se leyó primero\ncomo prueba de que fallaban los muros, después como un problema del punto de salida, y recién al\nrepasar la aritmética del código (cada paso restaba el 18% de la velocidad lateral) apareció el agarre.\nY corregir el agarre casi no movió la octava corrida: la bitácora terminó atribuyendo la parálisis a\nlas pistas procedurales, no a un bug de fondo de la física (parte 6). Ver bien lo que pasa es\nnecesario; no alcanza para saber por qué pasa.\n\n---\n\n## 7. Terminar o interrumpir: el fin del episodio importa\n\nUn episodio puede acabar porque la tarea terminó (el auto completó la vuelta o se salió) o porque se\nacabó el tiempo. No son lo mismo, y ML-Agents los trata distinto:\n\n![En ML-Agents, un episodio que termina con EndEpisode es terminal y el valor del estado siguiente se toma como cero; uno que llega al límite MaxStep se marca como interrumpido y se usa la estimación del crítico. Una nota recuerda que el límite de 80 segundos era menor que una vuelta del circuito fijo.](/images/debug-termination.svg)\n\n- Con **`EndEpisode()`**, el episodio es **terminal**: el valor del estado siguiente se toma como cero,\n  porque no hay futuro que cobrar.\n- Al llegar a **`MaxStep`**, el episodio queda **interrumpido**: el entrenador usa la estimación del\n  crítico para el valor del estado siguiente, porque la tarea no terminó, solo se acabó el reloj.\n\nEn Agentic Racing, la vuelta completa y todas las salidas por fallo (fuera de pista, detenido, sentido\ncontrario, sin progreso) usan `EndEpisode()`, y el límite de tiempo usa `MaxStep`. La distinción está\nbien hecha. Pero el proyecto tuvo un error de truncamiento de otro tipo: el límite era de 4000 pasos\n(80 segundos), y una vuelta del circuito fijo toma unos 95. Mientras ese límite estuvo así, al ritmo del\npiloto heurístico de entonces ningún episodio en ese circuito podía terminar con la vuelta completa; el piloto heurístico llegaba al 86% y se\nle acababa el tiempo. Se subió a 6000 pasos.\n\nY la parte 6 mostró el otro lado: qué condiciones terminan el episodio, y cuánto cuestan, es parte del\ndiseño de la recompensa. El atajo de la cuarta corrida salió justamente de ahí.\n\n---\n\n## 8. Cuándo mirar PPO\n\nSolo después de que la experiencia es coherente tiene sentido mirar el algoritmo. En Agentic Racing,\nlos hiperparámetros de PPO casi no cambiaron: se acortó la duración de las corridas, se subió el\ncoeficiente de entropía de 0.005 a 0.01 para explorar más, y poco más. La parte 2 mostró que PPO hizo\nsu trabajo: optimizó, de forma estable, exactamente el objetivo que le dimos.\n\nLas métricas de entrenamiento sí sirvieron, pero leídas como **señales del proceso**, no como veredicto:\n\n| Métrica | Qué dijo en el proyecto |\n|---|---|\n| recompensa acumulada | dónde se estancaba cada corrida; no si el auto daba vueltas |\n| duración del episodio | si el agente sobrevivía más o moría antes (en la primera corrida, bajaba mientras la recompensa seguía plana) |\n| pérdida del crítico | si el crítico entendía el mundo: subía en casi todas las corridas sin observación de curvatura y se estabilizó en la séptima |\n| entropía | si la policy seguía explorando o ya se había decidido |\n\nNinguna de ellas dijo nunca \"el agarre frena el auto\" o \"las pistas tienen curvas imposibles\". Esas\nrespuestas vinieron de mirar el comportamiento.\n\n---\n\n## 9. Exploración: cuando el agente nunca ve lo que tiene que aprender\n\nEn las últimas corridas, entre el 91 y el 98% de los episodios terminaba fuera de la pista a unos\n200–250 metros, en la primera curva de verdad. La hipótesis de ese momento era de exploración: el agente\nllegaba a la curva una y otra vez, pero casi nunca daba por azar con la secuencia\nfrenar–girar–acelerar, así que casi nunca vivía la experiencia que le habría enseñado que frenar valía\nla pena. (Después, el agarre y las pistas procedurales pasaron a explicar buena parte de eso.)\n\nHay tres salidas clásicas, y el proyecto las tuvo todas sobre la mesa:\n\n- **Curriculum learning**: empezar con curvas suaves y endurecerlas. Quedó anotado como siguiente paso;\n  no se hizo.\n- **Imitación**: partir de las demostraciones del experto (partes 3 a 5). Se configuró; no se ejecutó.\n- **Hacer el problema más fácil**: el circuito fijo, con curvas de 120 m de radio. Fue lo que destrabó\n  al piloto heurístico, pero el piloto RL nunca se reentrenó ahí.\n\n---\n\n## 10. Una corrida no es evidencia\n\nCada corrida del proyecto fue una sola corrida: ninguna se repitió con otra semilla, y la configuración\nno fija ninguna. La tercera corrida mostró lo que eso cuesta. Subió hasta una recompensa de ~10, con\nepisodios cinco veces más largos, y parecía el avance que se esperaba. La cuarta, con la misma\nconfiguración y más pasos, volvió al pozo. La bitácora lo resumió como \"varianza favorable de una\ncorrida\".\n\nEl mismo proyecto, más adelante, hizo esto bien. En el experimento del estratega LLM contra el\nheurístico se corrieron series de 18 carreras con la parrilla rotada, se reportó la media con su\ndesviación estándar, y el primer resultado fue el contrario al esperado. La diferencia entre una fase y\nla otra no fue de herramientas: fue de disciplina.\n\n---\n\n## 11. Los errores de proceso que más costaron\n\nLos bugs se arreglan. Lo que más tiempo le costó al proyecto fueron sus propios hábitos al depurar:\n\n![Cronología de la depuración del piloto RL: del 3 al 6 de septiembre, al menos seis bugs de tubería antes de la primera corrida; del 6 al 7, ocho corridas de PPO con recompensas distintas y la fracción de vuelta entre 8 y 13% en todas; el 8, el circuito fijo y el piloto heurístico.](/images/debug-timeline.svg)\n\n1. **Cambiar varias cosas a la vez.** Una iteración cambió la física de la dirección, la recompensa\n   por el borde, la condición de sentido contrario y el piloto heurístico juntos. La séptima corrida\n   cambió observaciones y recompensa juntas (parte 7). En ambos casos, cualquier mejora era imposible de\n   atribuir.\n2. **Declarar la causa antes de confirmarla.** Tres entradas de la bitácora, escritas en pocas horas, se titulan \"causa\n   raíz\": las curvas de 12 m, los muros de espesor cero (\"por fin\") y el agarre lateral (\"de verdad esta\n   vez\"). Los muros se descartaron después: tres versiones distintas dieron el mismo resultado.\n3. **Leer la línea base a favor de la hipótesis.** El 82% de un episodio del controlador se tomó como\n   \"el entorno funciona\", cuando el promedio decía lo contrario (sección 4).\n4. **Entrenar largo antes de saber cuánto hacía falta.** La primera corrida duró 20 millones de pasos y\n   convergió en 1.85: cerca del 90% del cómputo no aportó nada.\n5. **Iterar la recompensa cuando el problema no era la recompensa.** Siete variantes, y la fracción de\n   vuelta no se movió (parte 6).\n\nEn defensa del proyecto, también hizo cosas bien: una prueba corta antes de la primera corrida larga,\nuna bitácora que registró cada hipótesis con su resultado (sin ella este artículo no existiría), y una\ndecisión final honesta. Después de la octava corrida, en lugar de seguir iterando, se preguntó qué\nnecesitaba el demo de verdad, y la respuesta no era un piloto RL (parte 5).\n\n---\n\n## 12. Una secuencia de depuración, aplicada\n\nJuntando todo, esta es la secuencia que el proyecto habría querido seguir desde el principio, y lo que\npasó con cada paso:\n\n| Paso | Qué hacer | En Agentic Racing |\n|---|---|---|\n| 1 | Describir el síntoma con evidencia | sí: la bitácora registra cada corrida con sus números |\n| 2 | Verificar la tubería sin entrenar | sí, a la fuerza: al menos seis bugs antes de la primera corrida |\n| 3 | Inspeccionar observaciones y acciones reales, y su efecto | tarde: el volcado de trayectoria llegó después de la séptima corrida, en las evaluaciones del heurístico |\n| 4 | Validar la recompensa con casos conocidos | parcialmente: el atajo se explicó con números, pero la recompensa del experto nunca se midió |\n| 5 | Verificar terminaciones y truncamientos | bien diseñado, con un límite de tiempo demasiado corto |\n| 6 | Tener una línea base sencilla | tarde, mal leída al principio, y con un bug propio |\n| 7 | Entrenar corto antes de entrenar largo | la prueba corta sí; la primera corrida, demasiado larga |\n| 8 | Cambiar una sola cosa por experimento | no, varias veces |\n| 9 | Evaluar con métricas independientes, en varias semillas | métricas sí, desde la cuarta corrida, en nueve pistas; semillas de entrenamiento, nunca |\n| 10 | Documentar el resultado, también el negativo | sí: es la mejor parte del proyecto |\n\n---\n\n## 13. Síntomas e hipótesis, con los casos del proyecto\n\n| Síntoma | Hipótesis que hay que descartar primero | El caso de Agentic Racing |\n|---|---|---|\n| no termina ningún episodio | la tubería de decisión y acción | las acciones nunca llegaban al auto |\n| recompensa plana desde muy temprano | óptimo local, recompensa sin señal útil | primera y segunda corrida |\n| se detiene a propósito | terminar el episodio paga más que seguir | cuarta corrida |\n| conduce rápido y se sale | no ve venir la curva, o frenar no paga | quinta corrida; la observación de curvatura llegó en la séptima |\n| no gira con fuerza | la física convierte el giro en frenazo | el agarre lateral (corregido, sin mover la octava corrida) |\n| el controlador de referencia tampoco puede | el entorno no se puede resolver | las pistas procedurales |\n| la recompensa sube, el comportamiento no | la recompensa mide otra cosa | séptima corrida |\n| un resultado muy bueno que no se repite | varianza entre corridas | tercera corrida |\n\nEsta tabla no diagnostica sola: cada síntoma admite varias causas. Lo que sí hace es decir **por dónde\nempezar a buscar**, que es la mitad del trabajo.\n\n---\n\n## Conclusión\n\nDepurar un sistema de RL no es encontrar la combinación correcta de hiperparámetros. Es convertir un\nentrenamiento opaco en un sistema que se pueda observar, probar y explicar, eslabón por eslabón.\n\nAgentic Racing deja una versión concreta de esa idea:\n\n- verifica la tubería antes de culpar al aprendizaje: los logs suelen decir la verdad antes que las\n  curvas;\n- ten una línea base escrita a mano, y depúrala también;\n- mira casos individuales antes de creerle a un promedio;\n- registra lo que el auto hace, no solo lo que la policy pide;\n- distingue terminar de interrumpir, y revisa que los límites tengan sentido;\n- cambia una sola cosa por vez, y no declares la causa hasta que la evidencia la confirme;\n- y documenta todo, también lo que no funcionó.\n\n> El objetivo de depurar no es que la curva suba. Es entender por qué no sube.\n\nSi llegaste directo a este artículo, las siete partes anteriores están enlazadas al principio, y\n[Agentic Racing: un piloto que nunca aprendió a manejar](/es/blog/agentic-racing-un-piloto-que-nunca-aprendio-a-manejar/)\ncuenta la historia completa del proyecto.\n\n---\n\n## Referencias\n\n- Schulman, J. et al. (2017). [Proximal Policy Optimization Algorithms](https://arxiv.org/abs/1707.06347).\n- Sutton, R. S. y Barto, A. G. (2018). [Reinforcement Learning: An Introduction](http://incompleteideas.net/book/the-book-2nd.html), 2.ª edición. MIT Press.\n- Henderson, P. et al. (2018). [Deep Reinforcement Learning that Matters](https://arxiv.org/abs/1709.06560). *AAAI*.\n- Agarwal, R. et al. (2021). [Deep Reinforcement Learning at the Edge of the Statistical Precipice](https://arxiv.org/abs/2108.13264). *NeurIPS*.\n- OpenAI. [Spinning Up: Proximal Policy Optimization](https://spinningup.openai.com/en/latest/algorithms/ppo.html).\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), release de\n  `mlagents` 1.1.0: el manejo de episodios interrumpidos en `ml-agents/mlagents/trainers/ppo/trainer.py` y\n  `ml-agents/mlagents/trainers/optimizer/torch_optimizer.py`, y `HeuristicPolicy` en\n  `com.unity.ml-agents` 4.0.\n- La bitácora completa de la depuración (`docs/Devlog.md`), el evaluador y el código están en\n  [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"what-do-we-do-when-the-agent-doesnt-learn-rl-debugging-explained-from-scratch","lang":"en","title":"What Do We Do When the Agent Doesn't Learn? RL Debugging Explained from Scratch","description":"Part 8 of the Reinforcement Learning from scratch series with Agentic Racing. How to turn \"the agent doesn't learn\" into testable hypotheses, told through the real debugging of the project's RL pilot: at least six plumbing bugs before the first run, eight PPO runs with the lap fraction stuck, a baseline that had a bug of its own, misleading averages, termination versus truncation in ML-Agents, and the process mistakes that cost the most time.","publishDate":"2026-10-13","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"que-hacemos-cuando-el-agente-no-aprende-rl-debugging-explicado-desde-cero","url":"https://alexisalulema.com/blog/what-do-we-do-when-the-agent-doesnt-learn-rl-debugging-explained-from-scratch/","series":null,"headings":[{"depth":2,"slug":"where-we-left-off","text":"Where we left off"},{"depth":2,"slug":"1-it-doesnt-learn-isnt-a-symptom","text":"1. “It doesn’t learn” isn’t a symptom"},{"depth":2,"slug":"2-the-system-you-have-to-debug","text":"2. The system you have to debug"},{"depth":2,"slug":"3-first-make-the-plumbing-work","text":"3. First, make the plumbing work"},{"depth":2,"slug":"4-a-baseline-you-can-believe","text":"4. A baseline you can believe"},{"depth":2,"slug":"5-metrics-that-arent-the-reward-and-dont-average-too-much","text":"5. Metrics that aren’t the reward, and don’t average too much"},{"depth":2,"slug":"6-the-action-isnt-the-effect","text":"6. The action isn’t the effect"},{"depth":2,"slug":"7-ending-or-interrupting-how-the-episode-ends-matters","text":"7. Ending or interrupting: how the episode ends matters"},{"depth":2,"slug":"8-when-to-look-at-ppo","text":"8. When to look at PPO"},{"depth":2,"slug":"9-exploration-when-the-agent-never-sees-what-it-needs-to-learn","text":"9. Exploration: when the agent never sees what it needs to learn"},{"depth":2,"slug":"10-one-run-isnt-evidence","text":"10. One run isn’t evidence"},{"depth":2,"slug":"11-the-process-mistakes-that-cost-the-most","text":"11. The process mistakes that cost the most"},{"depth":2,"slug":"12-a-debugging-sequence-applied","text":"12. A debugging sequence, applied"},{"depth":2,"slug":"13-symptoms-and-hypotheses-with-the-projects-cases","text":"13. Symptoms and hypotheses, with the project’s cases"},{"depth":2,"slug":"conclusion","text":"Conclusion"},{"depth":2,"slug":"references","text":"References"}],"body":"> **Series: Reinforcement Learning from scratch, with Agentic Racing as the lab**\n>\n> 1. Part 1 · [How Does an Agent Learn? Teaching a Car to Drive](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/)\n> 2. Part 2 · [How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/)\n> 3. Part 3 · [Can We Teach by Imitation? Behavioral Cloning Explained from Scratch](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/)\n> 4. Part 4 · [Can We Learn to Behave Like an Expert? GAIL Explained from Scratch](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/)\n> 5. Part 5 · [Can We Combine Imitation and RL? BC, GAIL, and PPO as One System](/blog/can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system/)\n> 6. Part 6 · [How Do We Tell an Agent What We Want? Reward Engineering Explained from Scratch](/blog/how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch/)\n> 7. Part 7 · [What Should an Agent Be Able to See? Observation Design Explained from Scratch](/blog/what-should-an-agent-be-able-to-see-observation-design-explained-from-scratch/)\n> 8. Part 8 · **What Do We Do When the Agent Doesn't Learn? RL Debugging Explained from Scratch** (this article)\n\n## Where we left off\n\nThe seven earlier parts each told, from their own angle, why Agentic Racing's RL pilot never learned\nto complete laps consistently: the reward had shortcuts (part 6), the observations didn't see corners in time (part\n7), PPO optimized the wrong objective very well (part 2), and the imitation that could have helped was\nnever run (parts 3 to 5).\n\nThis part puts all of that together around the question the project asked itself for a week in September:\n\n> The agent doesn't learn. Why?\n\nThe short answer is that the question was badly framed. \"It doesn't learn\" isn't a symptom: it's a\nconclusion. Debugging an RL system means turning it into hypotheses that can be checked one by one.\nThis article tells how the project did it, what worked, and which process mistakes cost more than any\nbug.\n\n---\n\n## 1. \"It doesn't learn\" isn't a symptom\n\nVery different situations hide behind that phrase. Agentic Racing went through almost all of them:\n\n| What you see | In Agentic Racing |\n|---|---|\n| not a single episode completes | 2 million steps with zero completed episodes: the car never moved |\n| the reward doesn't change | the first run converged at 1.85 million steps and stayed flat for 18 million more |\n| the reward rises, the behavior doesn't | the seventh run: reward +35%, completed laps from 2% to 3% |\n| it improves and then gets worse | the third run climbed to ~10; the fourth fell back to ~4.8 |\n| it learns something, just not what we wanted | the fourth run learned to stop on purpose |\n\nEach row calls for a different investigation. The first is a plumbing bug; the second can be a local\noptimum; the third, a reward that measures something else; the fourth, variance; the fifth, a\nshortcut. Treating them all as \"the agent doesn't learn\" leads to trying fixes at random.\n\n---\n\n## 2. The system you have to debug\n\nAn RL agent doesn't learn in a vacuum. Between the idea and the learning there's a long chain: the\nsimulator, its connection to Python, the sensors, turning decisions into actions, the physics, the\nreward, how episodes end, the evaluation, and the baseline you compare against. Any broken link can\nproduce the same symptom.\n\nThis is Agentic Racing's map, with the real bugs from its devlog on each link:\n\n![Agentic Racing's debugging map: eight parts of the system (Unity player and its link to Python, sensors, decision and action, physics and track, reward, episode end, evaluation, and baseline), each with the real bugs the project had. The learning algorithm doesn't appear on the map.](/images/hero-debug-dark.svg)\n\nWhat stands out most on the map is what isn't there: **the algorithm**. All eight runs used PPO, and\nnone of those bugs was fixed by touching it. Part 2 had already shown it with the table in section 9:\nwhat changed between runs was the reward, the agent's perception, or the physics.\n\n---\n\n## 3. First, make the plumbing work\n\nBefore the first run could train, the project spent three days debugging things that had nothing to do\nwith learning to drive. These are six of them (there were more: several in the player build itself, and\na Python environment outside the versions `mlagents` pins):\n\n1. **The headless player crashed** while registering the agent. ML-Agents only reported an exit code;\n   the real log showed up when running the executable by hand. The fix was a software framebuffer\n   instead of the no-graphics mode.\n2. **The native gRPC library wasn't where it was being looked for**, and the player decided there was no\n   trainer and switched to inference mode, with no visible error.\n3. **ML-Agents' communicator doesn't work when compiled with IL2CPP.** That forced moving the training\n   player from Linux to Windows with Mono.\n4. **The observation size didn't match**: 12 had been agreed and 27 arrived (part 7).\n5. **The environment hung at ~63,000 steps.** With the observations fixed, training ran until Unity\n   throttled the simulation when the window lost focus, and the trainer gave up waiting.\n6. **Two million steps, zero episodes.** Decisions were requested but the action was never executed\n   (and the observations arrived empty): the car didn't move. It was the order in which the car's components were assembled.\n\nThe pattern that worked kept repeating: **find the log that tells the truth** (the player's, not\nPython's), add targeted logging where the hypothesis called for it and, when needed, reproduce the\nfailure on another machine to rule out the environment. And the first run was only launched after a short test confirmed\nthere were episodes, actions, and complete observations.\n\nThe lesson is the cheapest of the whole series: **\"0 episodes completed\" and \"observations padded with\nzeros\" were written in the logs**. A training run that runs isn't a training run that learns, and the\nfirst thing to verify is that the experience it receives is the one we think it is.\n\n---\n\n## 4. A baseline you can believe\n\nThe project's most useful diagnostic tool wasn't a PPO metric: it was **a hand-written controller** in\nthe same environment. If it can't complete the lap, the problem isn't the learning.\n\nThe project built it late, after the fifth run, and used it badly at first. The first evaluation had an\ninverted sign in the controller itself; the second showed cars dying anywhere, always stuck against a\nwall. After the car was given steering at low speed to get off the wall, one episode reached\n82% of the lap, and that was read as \"the environment is learnable, the problem is the RL\". The other\nhalf of the data, that on average the controller also got stuck, turned out to be the important one\n(part 6).\n\nAnd the baseline had bugs of its own. One was only found in October, while writing part 7 of this\nseries: the timer that decides when the heuristic pilot reverses off a wall counted five times too\nslowly, because it added one physics step per call and the function is only called once per decision.\nIts threshold, now 1 second, amounted to 5 real seconds. It's fixed now, but it was there from the\nsixth run on, and also in the mixed-field experiments (section 10).\n\n> A baseline is code too, and it needs debugging too.\n\n---\n\n## 5. Metrics that aren't the reward, and don't average too much\n\nPart 6 told why the project built an independent evaluator after the fourth run: the reward didn't say\nthe policy never braked, and the evaluator did (mean brake of 0.02).\n\nBut an evaluator can lie too, and the project lived through that twice:\n\n- **A mean that described no car.** In one evaluation of the heuristic pilot, the report gave a mean\n  speed of 5.3 m/s. The per-car log, once a second, showed something else: two or three cars were\n  driving at about 20 m/s and the other six or seven were stuck near the start, in a \"sawtooth\". The\n  mean looked like neither group.\n- **Episodes that didn't exist.** In another evaluation, the count of episodes ended by the time limit\n  was inflated by spurious resets of the evaluator itself while loading the model.\n\n![Sketch of nine cars in an evaluation of the heuristic pilot: two or three at about 20 m/s and six or seven almost stopped near the start; the reported mean, 5.3 m/s, describes neither group.](/images/debug-averages.svg)\n\nThe rule that comes out of it: **before trusting a summary, look at a few individual cases**. A log of\none line per car per second solved in one evaluation what several aggregate reports hadn't shown.\n\n---\n\n## 6. The action isn't the effect\n\nIf the policy asks to turn and the car brakes, the problem isn't in the policy. In Agentic Racing, the\nlateral grip model erased the car's lateral velocity instead of redirecting it: any hard turn was a\nsudden stop (part 1). The devlog read that as the reason both the agent and the heuristic pilot\ndidn't turn hard: in that physics, turning cost speed.\n\nThe key tool was logging **what the car did**, not just what the policy asked for: a trajectory dump\nat the end of each episode showed cars with nearly constant steering, zero speed, and sliding sideways;\nthe car \"spins and slides sideways\", says the devlog. No hyperparameter would have shown that. But the\ndump showed the symptom, not the cause: the same data was first read as proof that the walls were\nfailing, then as a spawn-position problem, and only when the code's arithmetic was gone over (each step\nremoved 18% of the lateral velocity) did the grip show up. And fixing the grip barely moved the eighth\nrun: the devlog ended up blaming the paralysis on the procedural tracks, not on a deep physics bug (part\n6). Seeing clearly what happens is necessary; it isn't enough to know why it happens.\n\n---\n\n## 7. Ending or interrupting: how the episode ends matters\n\nAn episode can end because the task ended (the car completed the lap or left the track) or because time\nran out. They're not the same, and ML-Agents treats them differently:\n\n![In ML-Agents, an episode that ends with EndEpisode is terminal and the next state's value is taken as zero; one that reaches the MaxStep limit is marked as interrupted and the critic's estimate is used. A note recalls that the 80-second limit was shorter than a lap of the fixed circuit.](/images/debug-termination.svg)\n\n- With **`EndEpisode()`**, the episode is **terminal**: the next state's value is taken as zero, because\n  there's no future left to collect.\n- On reaching **`MaxStep`**, the episode is **interrupted**: the trainer uses the critic's estimate for\n  the next state's value, because the task didn't end, only the clock ran out.\n\nIn Agentic Racing, the completed lap and every failure ending (off track, stopped, wrong way, no\nprogress) use `EndEpisode()`, and the time limit uses `MaxStep`. That distinction is done right. But the\nproject had a truncation bug of another kind: the limit was 4,000 steps (80 seconds), and a lap of the\nfixed circuit takes about 95. While the limit stayed there, at the heuristic pilot's pace back then no\nepisode on that circuit could end in a completed lap; the heuristic pilot reached 86% and ran out of time. It was raised to 6,000 steps.\n\nAnd part 6 showed the other side: which conditions end the episode, and what they cost, is part of the\nreward design. The fourth run's shortcut came from exactly there.\n\n---\n\n## 8. When to look at PPO\n\nOnly once the experience is coherent does it make sense to look at the algorithm. In Agentic Racing,\nPPO's hyperparameters barely changed: runs were shortened, the entropy coefficient went from 0.005 to\n0.01 to explore more, and little else. Part 2 showed that PPO did its job: it optimized, stably, exactly\nthe objective we gave it.\n\nThe training metrics did help, but read as **signals about the process**, not as a verdict:\n\n| Metric | What it told the project |\n|---|---|\n| cumulative reward | where each run plateaued; not whether the car completed laps |\n| episode length | whether the agent survived longer or died sooner (in the first run, it dropped while the reward stayed flat) |\n| critic loss | whether the critic understood the world: it rose in almost every run without curvature observations and settled in the seventh |\n| entropy | whether the policy was still exploring or had already made up its mind |\n\nNone of them ever said \"the grip brakes the car\" or \"the tracks have impossible corners\". Those answers\ncame from looking at the behavior.\n\n---\n\n## 9. Exploration: when the agent never sees what it needs to learn\n\nIn the last runs, between 91 and 98% of episodes ended off the track about 200–250 metres in, at the\nfirst real corner. The hypothesis at the time was about exploration: the agent reached the corner again\nand again, but almost never stumbled onto the brake–turn–accelerate sequence by chance, so it almost\nnever lived through the experience that would have taught it braking was worth it. (Later, the grip and\nthe procedural tracks came to explain a good part of that.)\n\nThere are three classic ways out, and the project had all of them on the table:\n\n- **Curriculum learning**: start with gentle corners and make them harder. It was noted as the next step;\n  it was never done.\n- **Imitation**: start from the expert's demonstrations (parts 3 to 5). It was configured; it was never\n  run.\n- **Make the problem easier**: the fixed circuit, with 120 m radius corners. That's what unblocked the\n  heuristic pilot, but the RL pilot was never retrained there.\n\n---\n\n## 10. One run isn't evidence\n\nEvery run in the project was a single run: none was repeated with another seed, and the configuration\ndoesn't fix one. The third run showed what that costs. It climbed to a reward of ~10, with episodes five\ntimes longer, and looked like the hoped-for breakthrough. The fourth, with the same configuration and\nmore steps, fell back into the pit. The devlog summed it up as \"favorable variance from one run\".\n\nThe same project got this right later. In the experiment pitting the LLM strategist against the\nheuristic one, it ran series of 18 races with the grid rotated, reported the mean with its standard\ndeviation, and the first result went against expectations. The difference between one phase and the\nother wasn't tools: it was discipline.\n\n---\n\n## 11. The process mistakes that cost the most\n\nBugs get fixed. What cost the project the most time was its own habits while debugging:\n\n![Timeline of the RL pilot's debugging: from September 3 to 6, at least six plumbing bugs before the first run; from the 6th to the 7th, eight PPO runs with different rewards and the lap fraction between 8 and 13% in all of them; on the 8th, the fixed circuit and the heuristic pilot.](/images/debug-timeline.svg)\n\n1. **Changing several things at once.** One iteration changed the steering physics, the edge reward,\n   the wrong-way condition, and the heuristic pilot together. The seventh run changed observations and\n   reward together (part 7). In both cases, any improvement was impossible to attribute.\n2. **Declaring the cause before confirming it.** Three devlog entries, written within a few hours, are titled \"root\n   cause\": the 12 m corners, the zero-thickness walls (\"at last\"), and the lateral grip (\"for real this\n   time\"). The walls were ruled out later: three different versions gave the same result.\n3. **Reading the baseline in favor of the hypothesis.** One controller episode at 82% was taken as \"the\n   environment works\", when the average said the opposite (section 4).\n4. **Training long before knowing how long was needed.** The first run lasted 20 million steps and\n   converged at 1.85: about 90% of the compute added nothing.\n5. **Iterating on the reward when the problem wasn't the reward.** Seven variants, and the lap fraction\n   didn't move (part 6).\n\nIn the project's defense, it also did things right: a short test before the first long run, a devlog\nthat recorded every hypothesis with its result (without it this article wouldn't exist), and an honest\nfinal decision. After the eighth run, instead of iterating further, it asked what the demo really\nneeded, and the answer wasn't an RL pilot (part 5).\n\n---\n\n## 12. A debugging sequence, applied\n\nPutting it all together, this is the sequence the project would have liked to follow from the start,\nand what happened with each step:\n\n| Step | What to do | In Agentic Racing |\n|---|---|---|\n| 1 | Describe the symptom with evidence | yes: the devlog records each run with its numbers |\n| 2 | Verify the plumbing without training | yes, by necessity: at least six bugs before the first run |\n| 3 | Inspect real observations and actions, and their effect | late: the trajectory dump arrived after the seventh run, in the heuristic evaluations |\n| 4 | Validate the reward with known cases | partly: the shortcut was explained with numbers, but the expert's reward was never measured |\n| 5 | Verify terminations and truncations | well designed, with a time limit that was too short |\n| 6 | Have a simple baseline | late, misread at first, and with a bug of its own |\n| 7 | Train short before training long | the short test, yes; the first run, too long |\n| 8 | Change one thing per experiment | no, several times |\n| 9 | Evaluate with independent metrics, across several seeds | metrics yes, from the fourth run on, on nine tracks; training seeds, never |\n| 10 | Document the result, negative ones too | yes: it's the best part of the project |\n\n---\n\n## 13. Symptoms and hypotheses, with the project's cases\n\n| Symptom | Hypothesis to rule out first | Agentic Racing's case |\n|---|---|---|\n| no episode ever ends | the decision and action plumbing | the actions never reached the car |\n| reward flat from very early on | local optimum, reward with no useful signal | first and second runs |\n| stops on purpose | ending the episode pays more than continuing | fourth run |\n| drives fast and goes off | can't see the corner coming, or braking doesn't pay | fifth run; the curvature observation arrived in the seventh |\n| won't turn hard | the physics turns a turn into braking | the lateral grip (fixed, without moving the eighth run) |\n| the reference controller can't either | the environment can't be solved | the procedural tracks |\n| reward up, behavior not | the reward measures something else | seventh run |\n| a very good result that doesn't repeat | variance between runs | third run |\n\nThis table doesn't diagnose on its own: every symptom has several possible causes. What it does do is\ntell you **where to start looking**, which is half the job.\n\n---\n\n## Conclusion\n\nDebugging an RL system isn't finding the right combination of hyperparameters. It's turning an opaque\ntraining run into a system you can observe, test, and explain, link by link.\n\nAgentic Racing leaves a concrete version of that idea:\n\n- verify the plumbing before blaming the learning: logs often tell the truth before curves do;\n- have a hand-written baseline, and debug it too;\n- look at individual cases before believing an average;\n- log what the car does, not just what the policy asks for;\n- tell ending apart from interrupting, and check that the limits make sense;\n- change one thing at a time, and don't declare the cause until the evidence confirms it;\n- and document everything, including what didn't work.\n\n> The goal of debugging isn't for the curve to go up. It's to understand why it doesn't.\n\nIf you landed directly on this article, the seven earlier parts are linked at the top, and\n[Agentic Racing: A Pilot That Never Learned to Drive](/blog/agentic-racing-a-pilot-that-never-learned-to-drive/)\ntells the project's full story.\n\n---\n\n## References\n\n- Schulman, J. et al. (2017). [Proximal Policy Optimization Algorithms](https://arxiv.org/abs/1707.06347).\n- Sutton, R. S. and Barto, A. G. (2018). [Reinforcement Learning: An Introduction](http://incompleteideas.net/book/the-book-2nd.html), 2nd edition. MIT Press.\n- Henderson, P. et al. (2018). [Deep Reinforcement Learning that Matters](https://arxiv.org/abs/1709.06560). *AAAI*.\n- Agarwal, R. et al. (2021). [Deep Reinforcement Learning at the Edge of the Statistical Precipice](https://arxiv.org/abs/2108.13264). *NeurIPS*.\n- OpenAI. [Spinning Up: Proximal Policy Optimization](https://spinningup.openai.com/en/latest/algorithms/ppo.html).\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), `mlagents`\n  1.1.0 release: the handling of interrupted episodes in `ml-agents/mlagents/trainers/ppo/trainer.py` and\n  `ml-agents/mlagents/trainers/optimizer/torch_optimizer.py`, and `HeuristicPolicy` in\n  `com.unity.ml-agents` 4.0.\n- The full debugging devlog (`docs/Devlog.md`), the evaluator, and the code are at\n  [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"que-deberia-poder-ver-un-agente-observation-design-explicado-desde-cero","lang":"es","title":"¿Qué debería poder ver un agente? Observation design explicado desde cero","description":"Séptima parte de la serie Reinforcement Learning desde cero con Agentic Racing. Cómo se decide qué información recibe un agente, revisando una por una las 42 observaciones reales del piloto del proyecto: escalas, anticipación, memoria, lo que puede llevar a memorizar un circuito, lo que no ve (los rivales), los bugs que mandaron observaciones vacías, y por qué el piloto, el experto y el estratega LLM ven a propósito cosas distintas.","publishDate":"2026-10-11","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"what-should-an-agent-be-able-to-see-observation-design-explained-from-scratch","url":"https://alexisalulema.com/es/blog/que-deberia-poder-ver-un-agente-observation-design-explicado-desde-cero/","series":null,"headings":[{"depth":2,"slug":"dónde-nos-quedamos","text":"Dónde nos quedamos"},{"depth":2,"slug":"1-el-agente-no-ve-el-mundo-recibe-una-observación","text":"1. El agente no ve el mundo: recibe una observación"},{"depth":2,"slug":"2-los-42-números-uno-por-uno","text":"2. Los 42 números, uno por uno"},{"depth":2,"slug":"3-escalas-dos-capas-de-normalización","text":"3. Escalas: dos capas de normalización"},{"depth":2,"slug":"4-reaccionar-o-anticipar","text":"4. Reaccionar o anticipar"},{"depth":2,"slug":"5-vector-o-cámara","text":"5. ¿Vector o cámara?"},{"depth":2,"slug":"6-el-agente-necesita-memoria","text":"6. ¿El agente necesita memoria?"},{"depth":2,"slug":"7-diseñar-para-generalizar-no-para-memorizar","text":"7. Diseñar para generalizar, no para memorizar"},{"depth":2,"slug":"8-lo-que-el-piloto-no-ve-los-rivales","text":"8. Lo que el piloto no ve: los rivales"},{"depth":2,"slug":"9-tres-decisores-tres-observaciones","text":"9. Tres decisores, tres observaciones"},{"depth":2,"slug":"10-bugs-de-observación-cuando-el-agente-recibe-ceros","text":"10. Bugs de observación: cuando el agente recibe ceros"},{"depth":2,"slug":"11-observaciones-e-imitación","text":"11. Observaciones e imitación"},{"depth":2,"slug":"12-un-experimento-reproducible","text":"12. Un experimento reproducible"},{"depth":2,"slug":"13-cómo-sabemos-si-una-observación-es-buena","text":"13. ¿Cómo sabemos si una observación es buena?"},{"depth":2,"slug":"conclusión","text":"Conclusión"},{"depth":2,"slug":"referencias","text":"Referencias"}],"body":"> **Serie: Reinforcement Learning desde cero, con Agentic Racing como laboratorio**\n>\n> 1. Parte 1 · [¿Cómo aprende un agente? Enseñando a un automóvil a conducir](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/)\n> 2. Parte 2 · [¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/)\n> 3. Parte 3 · [¿Podemos enseñar imitando? Behavioral Cloning explicado desde cero](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/)\n> 4. Parte 4 · [¿Podemos aprender a comportarnos como un experto? GAIL explicado desde cero](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/)\n> 5. Parte 5 · [¿Podemos combinar imitación y RL? BC, GAIL y PPO en un mismo sistema](/es/blog/podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema/)\n> 6. Parte 6 · [¿Cómo le decimos a un agente qué queremos? Reward engineering explicado desde cero](/es/blog/como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero/)\n> 7. Parte 7 · **¿Qué debería poder ver un agente? Observation design explicado desde cero** (este artículo)\n> 8. Parte 8 · [¿Qué hacemos cuando el agente no aprende? RL debugging explicado desde cero](/es/blog/que-hacemos-cuando-el-agente-no-aprende-rl-debugging-explicado-desde-cero/)\n\n## Dónde nos quedamos\n\nLa [parte 6](/es/blog/como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero/)\ntrató de cómo le decimos al agente qué queremos. Esta parte va un paso antes:\n\n> ¿Qué información debería recibir el agente para poder decidir bien?\n\nLa [parte 1](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/) ya presentó la\ndistinción entre estado y observación, y la tabla con los 42 números que recibe el piloto de Agentic\nRacing. Aquí volvemos a esa tabla con más cuidado: qué significa cada número, cómo está escalado, qué\nle permite al agente, qué le falta, y qué pasó las veces que el proyecto la cambió.\n\nComo en la parte 6, esta vez hay material real: las observaciones están en el código y tienen\nhistoria. La advertencia de siempre también vale: ningún piloto entrenado con estas observaciones\ncompletó vueltas de forma consistente, y las propuestas del final no se ejecutaron.\n\n---\n\n## 1. El agente no ve el mundo: recibe una observación\n\nEn cada paso, el entorno está en un estado $s_t$, pero el agente recibe una observación $o_t$ que lo\ndescribe solo en parte:\n\n$$\no_t = O(s_t), \\qquad a_t \\sim \\pi(a \\mid o_t), \\qquad s_{t+1} \\sim P(s_{t+1} \\mid s_t, a_t)\n$$\n\n$O$ es la función que convierte el estado en observación, y la diseñamos nosotros. El simulador de\nAgentic Racing conoce todo: la posición exacta de cada auto, la geometría completa del circuito, las\nfuerzas de la física. El piloto recibe 42 números, 10 veces por segundo.\n\nLa consecuencia importante es esta: **el agente solo puede distinguir las situaciones que su\nobservación distingue**. Si dos situaciones que piden acciones distintas producen la misma\nobservación, ninguna policy que dependa solo de ella puede resolver bien las dos.\n\n---\n\n## 2. Los 42 números, uno por uno\n\nAsí se arma la observación del piloto en el código:\n\n![Los 42 números de la observación del piloto en cinco bloques: 27 de los rayos, 2 de velocidad, 4 de relación con la pista, 3 de curvatura por delante y 6 de la directiva, con la forma en que se escala cada uno, y una nota sobre la normalización adicional que aplica ML-Agents.](/images/obs-vector-map.svg)\n\n| Grupo | Valores | Qué contiene | Cómo se escala |\n|---|---:|---|---|\n| Rayos | 27 | 9 rayos en abanico de ±75°, de hasta 70 m, que reconocen los muros del borde; por rayo: si lo que tocó es un muro, si no tocó nada, y la distancia | dos valores binarios y la distancia como fracción del largo del rayo (1 si no tocó nada) |\n| Velocidad | 2 | hacia adelante y lateral | dividida por la velocidad máxima (55 m/s) |\n| Pista | 4 | error de rumbo respecto de la trazada ideal; desplazamiento lateral del auto; desplazamiento de la trazada ideal; progreso en la vuelta | rumbo ÷ 180°; desplazamientos ÷ medio ancho de pista, recortados a ±2; progreso de 0 a 1 |\n| Curvatura | 3 | cuánto gira la línea central entre 0–22, 18–45 y 40–75 m por delante | ÷ 90°, recortado a ±1 |\n| Directiva | 6 | agresión, tolerancia al riesgo, y el tipo de directiva en *one-hot* | niveles 0.15, 0.5 o 0.85; *one-hot* de 4 valores |\n\nVisto con la lista de preguntas que un piloto necesita responder, el diseño cubre casi todo:\n\n| Pregunta | Qué la responde |\n|---|---|\n| ¿Dónde estoy respecto de la pista? | desplazamiento lateral, rayos |\n| ¿Hacia dónde apunto y hacia dónde sigue la pista? | error de rumbo |\n| ¿A qué velocidad voy? ¿Estoy derrapando? | velocidad hacia adelante y lateral |\n| ¿Qué tan cerrada es la curva que viene? | curvatura por delante, rayos |\n| ¿Qué me pide el estratega? | canales de directiva |\n| ¿Hay un auto delante? | **nada** |\n\nLa última fila la veremos en la sección 8.\n\nDos detalles de diseño que vale la pena notar. El tipo de directiva va en *one-hot* (cuatro valores,\nuno encendido), que es lo que recomienda la documentación de ML-Agents para variables categóricas:\n\"atacar\" no es \"el doble de defender\". Y los niveles de agresión y riesgo no son continuos: se sortean\nentre 0.15, 0.5 y 0.85 porque son los tres niveles que el estratega LLM puede elegir. El comentario\ndel código lo explica: así la policy ve en entrenamiento los mismos valores posibles que verá cuando\nel LLM escriba esos canales (no con la misma frecuencia: en entrenamiento se sortean al azar, y el LLM\nno elige al azar). Es una forma de evitar un desajuste entre entrenamiento y uso real\nantes de que exista.\n\n---\n\n## 3. Escalas: dos capas de normalización\n\nLa documentación de ML-Agents recomienda normalizar cada observación a $[0, 1]$ o $[-1, 1]$, y la\nmayoría de los números del piloto lo están por construcción: la velocidad dividida por la máxima, el\nrumbo por 180°, la curvatura por 90° con un recorte.\n\nDos detalles merecen nota. Los desplazamientos laterales se recortan a ±2 medios anchos de pista, no a\n±1. El código no explica por qué, y en la práctica el recorte casi nunca actúa: los muros están en el\nborde, y el episodio termina 2 m más allá, a unos 1.33 medios anchos. Y el progreso en la vuelta, que\nsí está en $[0, 1]$, salta de 1 a 0 al cruzar la línea de meta: dos puntos vecinos de la pista tienen\nvalores en los extremos opuestos de la escala.\n\nEncima de eso, la configuración de entrenamiento activa `normalize: true`: ML-Agents acumula una media\ny una varianza de cada entrada a lo largo de todo el entrenamiento (incluidos los 27 valores de los\nrayos), reescala con ellas antes de la red y recorta el resultado a ±5. Esas estadísticas siguen\ncambiando mientras se entrena y quedan congeladas en el modelo exportado, así que la inferencia usa las\nfinales.\n\nUna advertencia que el proyecto confirma: **normalizar bien no compensa información que falta**. Las\nseis primeras corridas tenían observaciones bien escaladas y aun así no veían venir las curvas.\n\n---\n\n## 4. Reaccionar o anticipar\n\nAlgunas observaciones describen el presente: la velocidad, el desplazamiento lateral, la distancia a\nlos muros. Otras ayudan a anticipar: la curvatura de la pista por delante.\n\nLas seis primeras corridas solo podían anticipar con los rayos, que entonces llegaban a 40 m. A 15 m/s\neso son menos de tres segundos para ver una curva, frenar y girar. En la séptima se agregaron las tres\nobservaciones de curvatura y los rayos se alargaron a 70 m:\n\n![El único cambio de observaciones del proyecto: de 12 valores vectoriales y rayos de 40 m en las corridas 1 a 6, a 15 valores con tres de curvatura y rayos de 70 m en las corridas 7 y 8. La sexta terminó con recompensa cerca de 4.8, pérdida del crítico subiendo de 0.29 a 0.52 y 2% de vueltas; la séptima, con recompensa cerca de 6.5, pérdida del crítico estable en 0.22 y 3% de vueltas. Una franja advierte que la séptima corrida también cambió la recompensa de velocidad.](/images/obs-history.svg)\n\nEl crítico mejoró de forma visible: su error dejó de crecer y se estabilizó. Pero hay dos lecturas\nhonestas que hacer:\n\n1. **No se puede atribuir solo a las observaciones.** La misma corrida reemplazó la recompensa de\n   velocidad por la velocidad objetivo de la parte 6. Dos cambios a la vez, un solo resultado: es el\n   error de diseño experimental que la sección 9 propone evitar.\n2. **La métrica que importaba casi no se movió.** Las vueltas completas pasaron del 2% al 3%. Una\n   mejor observación era necesaria, pero no suficiente; como contamos en la parte 6, buena parte del\n   problema estaba en las pistas procedurales, y en las siete primeras corridas el agarre lateral\n   además frenaba el auto en cada giro.\n\nUna pregunta legítima sobre la anticipación es si la información \"existe\" de verdad. La curvatura se\ncalcula leyendo la geometría del circuito por delante, que un auto real no tendría sin un mapa. Aquí no\nes un problema: la policy se usaría en el mismo simulador donde se entrena, y un piloto de carreras\nconoce el circuito. Pero la pregunta correcta siempre es la misma: **¿esta información estará\ndisponible donde se va a usar la policy?**\n\n---\n\n## 5. ¿Vector o cámara?\n\nML-Agents permite observaciones vectoriales (números) y visuales (imágenes de una cámara virtual).\nAgentic Racing usa solo vectores y rayos: ni cámaras ni texturas renderizadas.\n\nEl plan del proyecto partía de observaciones vectoriales y, a partir de ahí, eligió entrenar en CPU, sin\nGPU: con vectores, una red pequeña y PPO, el cuello de botella es simular Unity, no calcular gradientes.\nEl propio plan anota que pasar a cámaras cambiaría ese cálculo. La documentación de ML-Agents va en la\nmisma dirección: las observaciones visuales suelen ser menos eficientes y más lentas de entrenar, y\nconviene usarlas solo cuando el problema no se puede resolver con vectores o rayos. Aquí, los rayos y la\ncurvatura ya le daban al piloto resumido lo que una cámara le habría obligado a aprender de los\npíxeles.\n\n| Objetivo | Punto de partida razonable |\n|---|---|\n| Validar que el control básico funciona | vector compacto, como el de Agentic Racing |\n| Estudiar el efecto de cada señal | vectores con ablaciones controladas |\n| Aprender a conducir desde una cámara | imágenes, con variación suficiente de escenarios |\n| Transferir a sensores reales | solo lo que esos sensores pueden medir |\n\n---\n\n## 6. ¿El agente necesita memoria?\n\nLa policy del proyecto es puramente **reactiva**: decide solo con la observación del instante actual,\n$\\pi(a_t \\mid o_t)$. No apila observaciones anteriores (`NumStackedVectorObservations = 1`) ni usa una\nred recurrente (la configuración no tiene bloque de memoria).\n\nEso tiene una consecuencia que ya vimos en las partes 3 y 4. El experto, el piloto heurístico, **sí\ntiene memoria**: temporizadores que cuentan cuánto tiempo lleva lento contra un muro, una ventana de\nreversa para despegarse, y un volante filtrado que depende del comando anterior. Cuando el experto da\nreversa porque su temporizador cuenta más de un segundo lento contra un muro, la observación del agente en ese instante puede ser casi\nigual a la de otro momento en que el experto acelera. Para una policy sin memoria, son la misma\nsituación con dos respuestas distintas.\n\nEn términos formales, el problema del piloto es **parcialmente observable** (un POMDP): una sola\nobservación no identifica todo lo que importa. Hay tres formas de atacarlo, de menor a mayor\ncomplejidad:\n\n1. **Agregar la variable que falta**, si existe. Por ejemplo, el comando de volante anterior.\n2. **Apilar observaciones recientes.** ML-Agents lo permite con una opción para el vector y otra para el\n   sensor de rayos; la documentación lo describe como \"memoria limitada\" sin la complejidad de una red recurrente.\n3. **Una policy recurrente** (LSTM), que aprende qué recordar.\n\nNinguna se probó. Y la memoria tiene un límite: si una señal nunca se observa y no se puede inferir de\nla historia, ninguna red recurrente puede inventarla.\n\n---\n\n## 7. Diseñar para generalizar, no para memorizar\n\nEl riesgo clásico es una observación que permita aprender \"en esta posición del mundo, gira a la\nizquierda\" en lugar de \"cuando la pista gira a la izquierda, gira a la izquierda\". La documentación de\nML-Agents lo resume en una regla: codificar posiciones en coordenadas relativas siempre que se pueda.\n\nEl piloto del proyecto la sigue casi en todo: no recibe su posición absoluta, y la relación con la\npista se mide respecto de la línea central y la trazada ideal. Con una excepción interesante: **el\nprogreso en la vuelta**, un número de 0 a 1.\n\nEl progreso es la fracción de la vuelta recorrida desde la línea de meta, así que siempre fue una\nposición a lo largo de la pista. Mientras el entrenamiento usaba nueve pistas procedurales distintas,\nuna por arena, \"37% de la vuelta\" era un lugar distinto en cada pista. Pero en el circuito fijo, donde el\nproyecto terminó (y que hoy es la pista por defecto de las nueve arenas), ese número es **un código de\nposición único**: \"37% de la vuelta\"\nes siempre el mismo lugar del mismo circuito. Una policy entrenada ahí podría aprender a girar en el\n37% en lugar de girar cuando la pista gira. Funcionaría en ese circuito y fallaría en cualquier otro.\n\nNadie entrenó en el circuito fijo, así que es un riesgo, no un hallazgo. Pero muestra algo general:\n**que una observación sea relativa o absoluta depende también del entorno de entrenamiento**. La misma\nvariable puede ser inocente con nueve pistas y peligrosa con una.\n\n---\n\n## 8. Lo que el piloto no ve: los rivales\n\nEl demo corre seis autos en la misma pista. El piloto no ve a ninguno.\n\nLos rayos solo detectan los muros del borde, y ninguna de las 15 observaciones restantes describe a\notro auto. No es un descuido de una línea: en entrenamiento, cada arena tiene un solo auto, así que no\nhabía rivales que observar. El plan del proyecto incluía una fase multiagente con observaciones de los\nrivales más cercanos; no llegó a hacerse, porque antes el piloto RL se reemplazó por el heurístico.\n\nY el heurístico tampoco ve rivales. Hay un detalle revelador ahí. Uno de los canales de directiva se\nllama \"tolerancia al riesgo\", y el diseño original lo definía como tolerancia a la proximidad: aceptar\nhuecos más estrechos, ir rueda a rueda. En el código actual, ese canal solo ajusta cuánto se acerca el\nauto a la línea central o a la trazada ideal. No puede significar \"proximidad\" porque el piloto no\ntiene ninguna observación de proximidad. **Una directiva solo puede modular lo que el piloto puede\npercibir.**\n\nLos adelantamientos que se ven en el demo, entonces, no son maniobras: salen de diferencias de ritmo\nentre autos que no saben que los otros existen. Los contactos los resuelve la física, y el director de\ncarrera los registra como incidentes. (En la escena de carrera, de hecho, los autos ni siquiera tienen\nel sensor de rayos: conduce el piloto heurístico, que no lo usa.)\n\n---\n\n## 9. Tres decisores, tres observaciones\n\nEn Agentic Racing hay tres \"agentes\" que toman decisiones, y cada uno ve cosas distintas a propósito:\n\n![Quién ve qué: el piloto RL ve rayos, velocidad, posición en la pista, curvatura hasta 75 m y la directiva, pero no los rivales ni su comando anterior; el piloto heurístico lee directamente la geometría de la pista y además tiene memoria propia; el estratega LLM ve posiciones, gaps, tiempos de vuelta, incidentes y su bitácora, pero nada del estado del auto cuadro a cuadro.](/images/obs-who-sees-what.svg)\n\nLa asimetría entre el piloto y el estratega es la idea central del demo, y está escrita en el plan\ndesde el principio. El estratega ve más **contexto** (la clasificación, los gaps en segundos, los\ntiempos de vuelta, las curvas numeradas, su propia bitácora) pero menos **inmediatez**: no ve\nvelocidad, ángulo ni rayos, y no puede reaccionar a lo que pasa en los próximos segundos. El piloto es\nlo contrario. El código respeta esa frontera: la telemetría que se le envía al LLM describe a los\nrivales solo con lo observable desde fuera (posición, gap, último tiempo de vuelta, tendencia).\n\nEso también es observation design, en otro nivel: decidir qué ve cada parte de un sistema es decidir\nqué problema resuelve cada una.\n\nEl experto, por su parte, tiene **información privilegiada**: lee la geometría de la pista\ndirectamente y recuerda cosas que la policy no ve. La información privilegiada está bien para un\nprofesor que genera demostraciones, pero hay que saber que existe: lo que el profesor usa y el alumno\nno ve es justo lo que el alumno no podrá copiar (parte 3).\n\n---\n\n## 10. Bugs de observación: cuando el agente recibe ceros\n\nAntes de la primera corrida de entrenamiento, el proyecto pasó por dos bugs que no tenían nada que ver\ncon el diseño de las observaciones, sino con su tubería:\n\n- **Un desajuste de forma.** ML-Agents negocia con Python el tamaño de cada observación al conectarse.\n  Por el orden en que se armaban los componentes del auto, el tamaño acordado solo incluía el vector de\n  12 valores, pero en ejecución llegaban también los 27 de los rayos. Resultado: una excepción de\n  \"esperaba (12,) y recibí (27,)\".\n- **Observaciones vacías.** Arreglado eso, una corrida de prueba entera llenó el log de avisos de\n  \"menos observaciones de las esperadas, se rellenan con ceros\", unos dos millones: el vector de 12\n  valores llegaba vacío en casi cada paso, y el auto ni siquiera actuaba. La observación vacía era un\n  síntoma de que toda la tubería de decisión estaba mal armada.\n\nLos dos se arreglaron cambiando cómo y en qué orden se arman los componentes del auto, y la primera corrida\narrancó con el log limpio. La lección es práctica: **hay que verificar que la observación que llega a\nla red es la que creemos que mandamos**. Un relleno con ceros no detiene el entrenamiento; solo lo\narruina en silencio.\n\n---\n\n## 11. Observaciones e imitación\n\nLas observaciones también condicionan BC y GAIL (partes 3 y 4). Las demostraciones del grabador del\nproyecto guardan exactamente las mismas 42 observaciones que recibe la policy, así que no hay\ndesajuste de unidades ni de preprocesamiento entre experto y alumno. El problema es otro, y ya lo\nvimos: el experto decide con información que no está en esas 42 observaciones. BC no puede copiar una\ndecisión que depende de algo que no ve, y el discriminador de GAIL no puede exigirlo.\n\n---\n\n## 12. Un experimento reproducible\n\nSi se retoma el piloto RL en el circuito fijo, el experimento natural cambia **una sola cosa cada vez**\nsobre la observación actual:\n\n| Variante | Cambio | Pregunta |\n|---|---|---|\n| A. Base | las 42 observaciones actuales | la referencia: nunca se entrenó en el circuito fijo |\n| B. Sin progreso | quitar el progreso en la vuelta | ¿la policy lo usaba como código de posición? |\n| C. Sin curvatura | quitar las 3 de curvatura | ¿cuánto aportan, ahora sin la recompensa mezclada? |\n| D. Con el volante anterior | agregar el último comando de dirección | ¿reduce la diferencia con el experto? |\n| E. Apiladas | 3 observaciones apiladas | ¿hay situaciones que solo se distinguen en el tiempo? |\n| F. Con rivales | posición y velocidad relativa de los autos más cercanos | ¿aparecen maniobras de adelantamiento? (exige entrenar con varios autos por arena) |\n\nRequisitos para que la comparación signifique algo:\n\n- **Mismo build, misma recompensa, mismo presupuesto de pasos.** La séptima corrida mostró lo que pasa\n  si no.\n- **Varias semillas por variante**, con media y dispersión.\n- **Evaluar con `eval.exe`, no con la recompensa** (parte 6): motivos de fin de episodio, fracción de\n  vuelta, freno medio.\n- **Una prueba fuera del circuito fijo** para la variante B: las pistas procedurales siguen en el\n  código, detrás de una opción que hay que desactivar y recompilar, y son la forma de saber si la policy aprendió a conducir o aprendió el circuito.\n- **Verificar la tubería en cada cambio**: que el tamaño negociado coincida y que el log no tenga\n  rellenos con ceros.\n\n| Métrica | A | B | C | D | E | F |\n|---|:---:|:---:|:---:|:---:|:---:|:---:|\n| Vueltas completas | ? | ? | ? | ? | ? | ? |\n| Fracción de vuelta | ? | ? | ? | ? | ? | ? |\n| Freno medio | ? | ? | ? | ? | ? | ? |\n| Resultado en pistas no vistas | ? | ? | ? | ? | ? | ? |\n\nNada de esto se ejecutó.\n\n---\n\n## 13. ¿Cómo sabemos si una observación es buena?\n\nNo hay una puntuación universal, pero sí cinco preguntas, cada una con su respuesta en el proyecto:\n\n| Prueba | Pregunta | En Agentic Racing |\n|---|---|---|\n| Suficiencia | ¿hay situaciones distintas que se ven igual? | sí: las que dependen de la memoria del experto, y cualquier situación con rivales |\n| Utilidad | ¿agregar la señal mejora de forma repetible? | no se sabe: el único cambio se mezcló con otro |\n| Robustez | ¿funciona con otras pistas y arranques? | no se probó; el progreso en la vuelta es un riesgo en el circuito fijo |\n| Disponibilidad | ¿la señal existe donde se usará la policy? | sí: todo se calcula en el mismo simulador |\n| Costo | ¿vale lo que cuesta producirla? | barata: 42 números y 9 rayos, sin cámaras |\n\n---\n\n## Conclusión\n\nDiseñar la observación es decidir, en parte, qué problema resuelve el agente. El piloto de Agentic\nRacing resuelve un problema concreto: conducir viendo muros a 70 metros, su propia velocidad, cómo se\ncurva la pista hasta 75 metros y lo que le pide el estratega. No resuelve el problema de correr contra\notros autos, porque no puede verlos.\n\nLas lecciones que deja el proyecto:\n\n- las escalas importan, pero normalizar no reemplaza la información que falta;\n- anticipar exige observar el futuro cercano, y hay que preguntarse si esa información existirá donde\n  se use la policy;\n- una policy sin memoria no puede copiar a un experto que sí la tiene;\n- la misma variable puede ser relativa con muchas pistas y un código de posición con una sola;\n- una directiva solo puede modular lo que el piloto puede percibir;\n- cambiar la observación y la recompensa a la vez impide saber qué funcionó;\n- y hay que verificar que lo que llega a la red es lo que creemos que mandamos.\n\n> No se trata de que el agente vea todo. Se trata de que vea lo necesario para la conducta que\n> buscamos, con información que realmente tendrá.\n\nSi llegaste directo a este artículo, las seis partes anteriores están enlazadas al principio, y\n[Agentic Racing: un piloto que nunca aprendió a manejar](/es/blog/agentic-racing-un-piloto-que-nunca-aprendio-a-manejar/)\ncuenta la historia completa del proyecto.\n\n---\n\n## Referencias\n\n- Sutton, R. S. y Barto, A. G. (2018). [Reinforcement Learning: An Introduction](http://incompleteideas.net/book/the-book-2nd.html), 2.ª edición. MIT Press.\n- Kaelbling, L. P., Littman, M. L. y Cassandra, A. R. (1998). [Planning and Acting in Partially Observable Stochastic Domains](https://doi.org/10.1016/S0004-3702(98)00023-X). *Artificial Intelligence*, 101(1–2), 99–134.\n- Schulman, J. et al. (2017). [Proximal Policy Optimization Algorithms](https://arxiv.org/abs/1707.06347).\n- Ho, J. y Ermon, S. (2016). [Generative Adversarial Imitation Learning](https://arxiv.org/abs/1606.03476). *NeurIPS*.\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), release de\n  `mlagents` 1.1.0: la sección de observaciones de `docs/Learning-Environment-Design-Agents.md`\n  (normalización, apilado, *one-hot*, coordenadas relativas, rayos) y `network_settings` (`normalize`,\n  `memory`) en `docs/Training-Configuration-File.md`.\n- Las observaciones (`RaceAgent.CollectObservations` en `unity/Assets/Scripts/Agents/RaceAgent.cs`), el\n  sensor de rayos (`TrainingArena.cs`), la telemetría del estratega (`Strategy/RaceTelemetry.cs`) y la\n  bitácora están en [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"what-should-an-agent-be-able-to-see-observation-design-explained-from-scratch","lang":"en","title":"What Should an Agent Be Able to See? Observation Design Explained from Scratch","description":"Part 7 of the Reinforcement Learning from scratch series with Agentic Racing. How you decide what information an agent gets, going one by one through the project pilot's 42 real observations: scaling, anticipation, memory, what can let a policy memorize a circuit, what it doesn't see (the rivals), the bugs that sent empty observations, and why the pilot, the expert, and the LLM strategist deliberately see different things.","publishDate":"2026-10-11","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"que-deberia-poder-ver-un-agente-observation-design-explicado-desde-cero","url":"https://alexisalulema.com/blog/what-should-an-agent-be-able-to-see-observation-design-explained-from-scratch/","series":null,"headings":[{"depth":2,"slug":"where-we-left-off","text":"Where we left off"},{"depth":2,"slug":"1-the-agent-doesnt-see-the-world-it-gets-an-observation","text":"1. The agent doesn’t see the world: it gets an observation"},{"depth":2,"slug":"2-the-42-numbers-one-by-one","text":"2. The 42 numbers, one by one"},{"depth":2,"slug":"3-scales-two-layers-of-normalization","text":"3. Scales: two layers of normalization"},{"depth":2,"slug":"4-reacting-or-anticipating","text":"4. Reacting or anticipating"},{"depth":2,"slug":"5-vector-or-camera","text":"5. Vector or camera?"},{"depth":2,"slug":"6-does-the-agent-need-memory","text":"6. Does the agent need memory?"},{"depth":2,"slug":"7-designing-to-generalize-not-to-memorize","text":"7. Designing to generalize, not to memorize"},{"depth":2,"slug":"8-what-the-pilot-doesnt-see-the-rivals","text":"8. What the pilot doesn’t see: the rivals"},{"depth":2,"slug":"9-three-deciders-three-observations","text":"9. Three deciders, three observations"},{"depth":2,"slug":"10-observation-bugs-when-the-agent-gets-zeros","text":"10. Observation bugs: when the agent gets zeros"},{"depth":2,"slug":"11-observations-and-imitation","text":"11. Observations and imitation"},{"depth":2,"slug":"12-a-reproducible-experiment","text":"12. A reproducible experiment"},{"depth":2,"slug":"13-how-do-we-know-an-observation-is-good","text":"13. How do we know an observation is good?"},{"depth":2,"slug":"conclusion","text":"Conclusion"},{"depth":2,"slug":"references","text":"References"}],"body":"> **Series: Reinforcement Learning from scratch, with Agentic Racing as the lab**\n>\n> 1. Part 1 · [How Does an Agent Learn? Teaching a Car to Drive](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/)\n> 2. Part 2 · [How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/)\n> 3. Part 3 · [Can We Teach by Imitation? Behavioral Cloning Explained from Scratch](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/)\n> 4. Part 4 · [Can We Learn to Behave Like an Expert? GAIL Explained from Scratch](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/)\n> 5. Part 5 · [Can We Combine Imitation and RL? BC, GAIL, and PPO as One System](/blog/can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system/)\n> 6. Part 6 · [How Do We Tell an Agent What We Want? Reward Engineering Explained from Scratch](/blog/how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch/)\n> 7. Part 7 · **What Should an Agent Be Able to See? Observation Design Explained from Scratch** (this article)\n> 8. Part 8 · [What Do We Do When the Agent Doesn't Learn? RL Debugging Explained from Scratch](/blog/what-do-we-do-when-the-agent-doesnt-learn-rl-debugging-explained-from-scratch/)\n\n## Where we left off\n\n[Part 6](/blog/how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch/) was\nabout how we tell the agent what we want. This part goes one step earlier:\n\n> What information should the agent get so it can decide well?\n\n[Part 1](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/) already introduced the distinction\nbetween state and observation, and the table of the 42 numbers Agentic Racing's pilot receives. Here we\ngo back to that table more carefully: what each number means, how it's scaled, what it enables, what's\nmissing, and what happened the times the project changed it.\n\nAs in part 6, this time there's real material: the observations are in the code and have a history. The\nusual warning still applies: no pilot trained on these observations completed laps consistently, and\nthe proposals at the end were never run.\n\n---\n\n## 1. The agent doesn't see the world: it gets an observation\n\nAt each step, the environment is in a state $s_t$, but the agent receives an observation $o_t$ that\ndescribes it only in part:\n\n$$\no_t = O(s_t), \\qquad a_t \\sim \\pi(a \\mid o_t), \\qquad s_{t+1} \\sim P(s_{t+1} \\mid s_t, a_t)\n$$\n\n$O$ is the function that turns the state into an observation, and we design it. Agentic Racing's\nsimulator knows everything: every car's exact position, the full circuit geometry, the physics forces.\nThe pilot receives 42 numbers, 10 times a second.\n\nThe important consequence is this: **the agent can only tell apart the situations its observation tells\napart**. If two situations that call for different actions produce the same observation, no policy that\ndepends on that observation alone can handle both well.\n\n---\n\n## 2. The 42 numbers, one by one\n\nThis is how the pilot's observation is built in the code:\n\n![The pilot's 42 observation numbers in five blocks: 27 from the rays, 2 for speed, 4 for the relation to the track, 3 for curvature ahead, and 6 for the directive, with how each one is scaled, and a note on the extra normalization ML-Agents applies.](/images/obs-vector-map.svg)\n\n| Group | Values | What it holds | How it's scaled |\n|---|---:|---|---|\n| Rays | 27 | 9 rays fanned over ±75°, up to 70 m, that recognize the edge walls; per ray: whether what it hit is a wall, whether it hit nothing, and the distance | two binary values and the distance as a fraction of the ray length (1 if it hit nothing) |\n| Speed | 2 | forward and lateral | divided by top speed (55 m/s) |\n| Track | 4 | heading error relative to the racing line; the car's lateral offset; the racing line's offset; lap progress | heading ÷ 180°; offsets ÷ half the track width, clipped to ±2; progress from 0 to 1 |\n| Curvature | 3 | how much the centreline turns 0–22, 18–45, and 40–75 m ahead | ÷ 90°, clipped to ±1 |\n| Directive | 6 | aggression, risk tolerance, and the directive type as one-hot | levels 0.15, 0.5, or 0.85; a 4-value one-hot |\n\nSeen against the questions a driver needs to answer, the design covers almost everything:\n\n| Question | What answers it |\n|---|---|\n| Where am I relative to the track? | lateral offset, rays |\n| Where am I pointing, and where does the track go? | heading error |\n| How fast am I going? Am I sliding? | forward and lateral speed |\n| How tight is the next corner? | curvature ahead, rays |\n| What does the strategist want? | directive channels |\n| Is there a car ahead? | **nothing** |\n\nWe'll get to the last row in section 8.\n\nTwo design details are worth noticing. The directive type is one-hot (four values, one on), which is\nwhat the ML-Agents documentation recommends for categorical variables: \"attack\" isn't \"twice defend\".\nAnd aggression and risk aren't continuous: they're drawn from 0.15, 0.5, and 0.85 because those are the\nthree levels the LLM strategist can pick. The code comment explains it: that way the policy sees in\ntraining the same possible values it will see once the LLM writes those channels (not with the same\nfrequency: training draws them at random, and the LLM doesn't choose at random). It's a way of\npreventing a training/deployment mismatch before it exists.\n\n---\n\n## 3. Scales: two layers of normalization\n\nThe ML-Agents documentation recommends normalizing every observation to $[0, 1]$ or $[-1, 1]$, and most\nof the pilot's numbers are by construction: speed divided by top speed, heading by 180°, curvature by 90°\nwith a clip.\n\nTwo details are worth noting. The lateral offsets are clipped at ±2 half-widths, not ±1. The code doesn't\nsay why, and in practice the clip almost never kicks in: the walls sit at the edge, and the episode ends\n2 m beyond it, at about 1.33 half-widths. And lap progress, which is in $[0, 1]$, jumps from 1 to 0 when\ncrossing the finish line: two neighbouring points on the track get values at opposite ends of the\nscale.\n\nOn top of that, the training configuration turns on `normalize: true`: ML-Agents accumulates a mean\nand variance of each input over the whole training run (the 27 ray values included), rescales with them\nbefore the network, and clips the result to ±5. Those statistics keep changing while training and are\nfrozen into the exported model, so inference uses the final ones.\n\nA warning the project confirms: **good normalization doesn't make up for missing information**. The\nfirst six runs had well-scaled observations and still couldn't see corners coming.\n\n---\n\n## 4. Reacting or anticipating\n\nSome observations describe the present: speed, lateral offset, distance to the walls. Others help\nanticipate: the track's curvature ahead.\n\nThe first six runs could only anticipate through the rays, which back then reached 40 m. At 15 m/s\nthat's under three seconds to see a corner, brake, and turn. The seventh run added the three curvature\nobservations and lengthened the rays to 70 m:\n\n![The project's only observation change: from 12 vector values and 40 m rays in runs 1 to 6, to 15 values with three for curvature and 70 m rays in runs 7 and 8. The sixth run ended with reward around 4.8, critic loss rising from 0.29 to 0.52, and 2% completed laps; the seventh, with reward around 6.5, critic loss stable at 0.22, and 3% completed laps. A band warns that the seventh run also changed the speed reward.](/images/obs-history.svg)\n\nThe critic improved visibly: its error stopped growing and settled. But two honest readings are in\norder:\n\n1. **It can't be credited to the observations alone.** The same run replaced the speed reward with part\n   6's target speed. Two changes at once, a single result: it's the experimental-design mistake section\n   9 proposes avoiding.\n2. **The metric that mattered barely moved.** Completed laps went from 2% to 3%. A better observation\n   was necessary, but not sufficient; as we explained in part 6, a good part of the problem was in the\n   procedural tracks, and in the first seven runs lateral grip also braked the car in every turn.\n\nA fair question about anticipation is whether the information really \"exists\". Curvature is computed by\nreading the circuit geometry ahead, which a real car wouldn't have without a map. Here that's not a\nproblem: the policy would run in the same simulator it trains in, and a racing driver knows the circuit.\nBut the right question is always the same: **will this information be available where the policy is\ngoing to be used?**\n\n---\n\n## 5. Vector or camera?\n\nML-Agents supports vector observations (numbers) and visual ones (images from a virtual camera). Agentic\nRacing uses only vectors and rays: no cameras, no rendered textures.\n\nThe project's plan started from vector observations and, from there, chose to train on CPU, without a\nGPU: with vectors, a small network, and PPO, the bottleneck is simulating Unity, not computing gradients.\nThe plan itself notes that switching to cameras would change that calculation. The ML-Agents\ndocumentation points the same way: visual observations are typically less efficient and slower to train,\nand are best used only when the problem can't be solved with vectors or rays. Here, the rays and\ncurvature already handed the pilot, in summary, what a camera would have forced it to learn from\npixels.\n\n| Goal | Reasonable starting point |\n|---|---|\n| Validate that basic control works | a compact vector, like Agentic Racing's |\n| Study the effect of each signal | vectors with controlled ablations |\n| Learn to drive from a camera | images, with enough variation in scenarios |\n| Transfer to real sensors | only what those sensors can measure |\n\n---\n\n## 6. Does the agent need memory?\n\nThe project's policy is purely **reactive**: it decides from the current observation alone,\n$\\pi(a_t \\mid o_t)$. It doesn't stack previous observations (`NumStackedVectorObservations = 1`) and\ndoesn't use a recurrent network (the configuration has no memory block).\n\nThat has a consequence we already saw in parts 3 and 4. The expert, the heuristic pilot, **does have\nmemory**: timers that count how long it has been slow against a wall, a reverse window to get unstuck,\nand a filtered steering command that depends on the previous one. When the expert reverses because its\ntimer counts more than a second slow against a wall, the agent's observation at that instant can be almost the same as at\nanother moment when the expert accelerates. For a memoryless policy, they're the same situation with\ntwo different answers.\n\nFormally, the pilot's problem is **partially observable** (a POMDP): a single observation doesn't\nidentify everything that matters. There are three ways to attack it, from least to most complex:\n\n1. **Add the missing variable**, if it exists. For example, the previous steering command.\n2. **Stack recent observations.** ML-Agents allows it with one setting for the vector and another for the\n   ray sensor; the documentation describes it as \"limited memory\" without the complexity of a recurrent network.\n3. **A recurrent policy** (LSTM), which learns what to remember.\n\nNone was tried. And memory has a limit: if a signal is never observed and can't be inferred from the\nhistory, no recurrent network can make it up.\n\n---\n\n## 7. Designing to generalize, not to memorize\n\nThe classic risk is an observation that lets the policy learn \"at this position in the world, turn left\"\ninstead of \"when the track turns left, turn left\". The ML-Agents documentation sums it up in a rule:\nencode positions in relative coordinates whenever possible.\n\nThe project's pilot follows it almost everywhere: it doesn't get its absolute position, and its relation\nto the track is measured against the centreline and the racing line. With one interesting exception:\n**lap progress**, a number from 0 to 1.\n\nProgress is the fraction of the lap covered from the finish line, so it was always a position along the\ntrack. While training used nine different procedural tracks, one per arena, \"37% of the lap\" was a\ndifferent spot on each track. But on the fixed circuit, where the project ended up (and which is now the\ndefault track for all nine arenas), that number is **a single position code**: \"37% of the lap\" is always the same spot on the same circuit. A\npolicy trained there could learn to turn at 37% instead of turning when the track turns. It would work on\nthat circuit and fail on any other.\n\nNobody trained on the fixed circuit, so it's a risk, not a finding. But it shows something general:\n**whether an observation is relative or absolute also depends on the training environment**. The same\nvariable can be harmless with nine tracks and dangerous with one.\n\n---\n\n## 8. What the pilot doesn't see: the rivals\n\nThe demo runs six cars on the same track. The pilot sees none of them.\n\nThe rays only detect the edge walls, and none of the other 15 observations describes another car. It's\nnot a one-line oversight: in training, each arena has a single car, so there were no rivals to observe.\nThe project's plan included a multi-agent phase with observations of the nearest rivals; it never\nhappened, because the RL pilot was replaced by the heuristic one first.\n\nAnd the heuristic doesn't see rivals either. There's a revealing detail there. One of the directive\nchannels is called \"risk tolerance\", and the original design defined it as tolerance to proximity:\naccepting tighter gaps, running wheel to wheel. In the current code, that channel only adjusts how close\nthe car stays to the centreline versus the racing line. It can't mean \"proximity\" because the pilot has\nno proximity observation at all. **A directive can only modulate what the pilot can perceive.**\n\nThe overtakes you see in the demo, then, aren't maneuvers: they come from pace differences between cars\nthat don't know the others exist. Contacts are left to the physics, and the race director logs them as\nincidents. (In the race scene, in fact, the cars don't even have the ray sensor: the heuristic pilot is\ndriving, and it doesn't use it.)\n\n---\n\n## 9. Three deciders, three observations\n\nAgentic Racing has three \"agents\" making decisions, and each one deliberately sees different things:\n\n![Who sees what: the RL pilot sees rays, speed, position on the track, curvature up to 75 m, and the directive, but not the rivals or its previous command; the heuristic pilot reads the track geometry directly and has its own memory; the LLM strategist sees positions, gaps, lap times, incidents, and its own notes, but nothing of the car's frame-by-frame state.](/images/obs-who-sees-what.svg)\n\nThe asymmetry between the pilot and the strategist is the demo's central idea, and it's written into the\nplan from the start. The strategist sees more **context** (the standings, gaps in seconds, lap times,\nnumbered corners, its own notes) but less **immediacy**: it doesn't see speed, angle, or rays, and can't\nreact to what happens in the next few seconds. The pilot is the opposite. The code respects that\nboundary: the telemetry sent to the LLM describes rivals only by what's observable from outside (position,\ngap, last lap time, trend).\n\nThat's observation design too, at another level: deciding what each part of a system sees is deciding\nwhat problem each part solves.\n\nThe expert, for its part, has **privileged information**: it reads the track geometry directly and\nremembers things the policy doesn't see. Privileged information is fine for a teacher generating\ndemonstrations, but you have to know it's there: what the teacher uses and the student can't see is\nexactly what the student won't be able to copy (part 3).\n\n---\n\n## 10. Observation bugs: when the agent gets zeros\n\nBefore the first training run, the project went through two bugs that had nothing to do with the\nobservation design and everything to do with its plumbing:\n\n- **A shape mismatch.** ML-Agents negotiates the size of each observation with Python when it connects.\n  Because of the order in which the car's components were assembled, the agreed size only included the\n  12-value vector, but at runtime the 27 ray values arrived too. Result: an exception along the lines of\n  \"expected (12,), got (27,)\".\n- **Empty observations.** With that fixed, a whole test run filled the log with warnings of \"fewer\n  observations than expected, padding with zeros\", about two million of them: the 12-value vector arrived\n  empty at almost every step, and the car didn't even act. The empty observation was a symptom that the\n  whole decision pipeline was wired wrong.\n\nBoth were fixed by changing how and in what order the car's components are assembled, and the first run started\nwith a clean log. The lesson is practical: **verify that the observation reaching the network is the one\nyou think you're sending**. Zero padding doesn't stop training; it just ruins it silently.\n\n---\n\n## 11. Observations and imitation\n\nObservations also shape BC and GAIL (parts 3 and 4). The project's recorder saves demonstrations with\nexactly the same 42 observations the policy receives, so there's no mismatch of units or preprocessing\nbetween expert and student. The problem is a different one, and we've seen it: the expert decides with\ninformation that isn't in those 42 observations. BC can't copy a decision that depends on something it\ncan't see, and GAIL's discriminator can't demand it.\n\n---\n\n## 12. A reproducible experiment\n\nIf the RL pilot is picked up again on the fixed circuit, the natural experiment changes **one thing at a\ntime** on top of the current observation:\n\n| Variant | Change | Question |\n|---|---|---|\n| A. Baseline | the current 42 observations | the reference: never trained on the fixed circuit |\n| B. No progress | remove lap progress | was the policy using it as a position code? |\n| C. No curvature | remove the 3 curvature values | how much do they contribute, now without the reward mixed in? |\n| D. With the previous steering | add the last steering command | does it narrow the gap with the expert? |\n| E. Stacked | 3 stacked observations | are there situations that can only be told apart over time? |\n| F. With rivals | relative position and speed of the nearest cars | do overtaking maneuvers appear? (requires training with several cars per arena) |\n\nRequirements for the comparison to mean anything:\n\n- **Same build, same reward, same step budget.** The seventh run showed what happens otherwise.\n- **Several seeds per variant**, with mean and spread.\n- **Evaluate with `eval.exe`, not with the reward** (part 6): episode end reasons, lap fraction, mean\n  brake.\n- **A test off the fixed circuit** for variant B: the procedural tracks are still in the code, behind\n  a setting you have to switch off and rebuild, and they're\n  the way to know whether the policy learned to drive or learned the circuit.\n- **Check the plumbing on every change**: that the negotiated size matches and that the log has no zero\n  padding.\n\n| Metric | A | B | C | D | E | F |\n|---|:---:|:---:|:---:|:---:|:---:|:---:|\n| Completed laps | ? | ? | ? | ? | ? | ? |\n| Lap fraction | ? | ? | ? | ? | ? | ? |\n| Mean brake | ? | ? | ? | ? | ? | ? |\n| Results on unseen tracks | ? | ? | ? | ? | ? | ? |\n\nNone of this was run.\n\n---\n\n## 13. How do we know an observation is good?\n\nThere's no universal score, but there are five questions, each with its answer in the project:\n\n| Test | Question | In Agentic Racing |\n|---|---|---|\n| Sufficiency | are there different situations that look the same? | yes: the ones that depend on the expert's memory, and any situation with rivals |\n| Usefulness | does adding the signal help, repeatably? | unknown: the only change was mixed with another |\n| Robustness | does it hold up on other tracks and starts? | not tested; lap progress is a risk on the fixed circuit |\n| Availability | does the signal exist where the policy will run? | yes: everything is computed in the same simulator |\n| Cost | is it worth what it costs to produce? | cheap: 42 numbers and 9 rays, no cameras |\n\n---\n\n## Conclusion\n\nDesigning the observation is deciding, in part, what problem the agent solves. Agentic Racing's pilot\nsolves a specific one: driving while seeing walls 70 metres away, its own speed, how the track bends up\nto 75 metres ahead, and what the strategist asks for. It doesn't solve the problem of racing against\nother cars, because it can't see them.\n\nThe lessons the project leaves:\n\n- scales matter, but normalizing doesn't replace missing information;\n- anticipating requires observing the near future, and you have to ask whether that information will\n  exist where the policy runs;\n- a memoryless policy can't copy an expert that has memory;\n- the same variable can be relative with many tracks and a position code with just one;\n- a directive can only modulate what the pilot can perceive;\n- changing the observation and the reward at the same time makes it impossible to know what worked;\n- and you have to verify that what reaches the network is what you think you're sending.\n\n> It's not about the agent seeing everything. It's about it seeing what's needed for the behavior we\n> want, with information it will actually have.\n\nIf you landed directly on this article, the six earlier parts are linked at the top, and\n[Agentic Racing: A Pilot That Never Learned to Drive](/blog/agentic-racing-a-pilot-that-never-learned-to-drive/)\ntells the project's full story.\n\n---\n\n## References\n\n- Sutton, R. S. and Barto, A. G. (2018). [Reinforcement Learning: An Introduction](http://incompleteideas.net/book/the-book-2nd.html), 2nd edition. MIT Press.\n- Kaelbling, L. P., Littman, M. L., and Cassandra, A. R. (1998). [Planning and Acting in Partially Observable Stochastic Domains](https://doi.org/10.1016/S0004-3702(98)00023-X). *Artificial Intelligence*, 101(1–2), 99–134.\n- Schulman, J. et al. (2017). [Proximal Policy Optimization Algorithms](https://arxiv.org/abs/1707.06347).\n- Ho, J. and Ermon, S. (2016). [Generative Adversarial Imitation Learning](https://arxiv.org/abs/1606.03476). *NeurIPS*.\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), `mlagents`\n  1.1.0 release: the observations section of `docs/Learning-Environment-Design-Agents.md` (normalization,\n  stacking, one-hot, relative coordinates, rays) and `network_settings` (`normalize`, `memory`) in\n  `docs/Training-Configuration-File.md`.\n- The observations (`RaceAgent.CollectObservations` in `unity/Assets/Scripts/Agents/RaceAgent.cs`), the ray\n  sensor (`TrainingArena.cs`), the strategist's telemetry (`Strategy/RaceTelemetry.cs`), and the devlog are\n  at [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero","lang":"es","title":"¿Cómo le decimos a un agente qué queremos? Reward engineering explicado desde cero","description":"Sexta parte de la serie Reinforcement Learning desde cero con Agentic Racing. Cómo se traduce un objetivo humano a una recompensa, con la función real del piloto RL del proyecto como caso de estudio: escalas, unidades, recompensas densas y escasas, terminaciones que funcionan como premios, reward shaping, y por qué siete variantes de la recompensa no podían arreglar un problema que no era de recompensa.","publishDate":"2026-10-10","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch","url":"https://alexisalulema.com/es/blog/como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero/","series":null,"headings":[{"depth":2,"slug":"dónde-nos-quedamos","text":"Dónde nos quedamos"},{"depth":2,"slug":"1-el-agente-optimiza-la-señal-no-la-intención","text":"1. El agente optimiza la señal, no la intención"},{"depth":2,"slug":"2-de-un-objetivo-humano-a-una-lista-de-términos","text":"2. De un objetivo humano a una lista de términos"},{"depth":2,"slug":"3-escalas-qué-dice-cada-término-en-una-vuelta-completa","text":"3. Escalas: qué dice cada término en una vuelta completa"},{"depth":2,"slug":"4-recompensas-escasas-y-densas-con-números","text":"4. Recompensas escasas y densas, con números"},{"depth":2,"slug":"5-unidades-por-metro-por-segundo-por-evento","text":"5. Unidades: por metro, por segundo, por evento"},{"depth":2,"slug":"6-terminar-el-episodio-también-es-una-recompensa","text":"6. Terminar el episodio también es una recompensa"},{"depth":2,"slug":"7-reward-hacking-lo-que-se-puede-comprobar-en-el-código","text":"7. Reward hacking: lo que se puede comprobar en el código"},{"depth":2,"slug":"8-reward-shaping-ayudar-sin-cambiar-el-objetivo","text":"8. Reward shaping: ayudar sin cambiar el objetivo"},{"depth":2,"slug":"9-la-recompensa-no-puede-ser-la-única-métrica","text":"9. La recompensa no puede ser la única métrica"},{"depth":2,"slug":"10-lo-que-ninguna-recompensa-podía-arreglar","text":"10. Lo que ninguna recompensa podía arreglar"},{"depth":2,"slug":"11-la-deuda-de-la-parte-5-una-recompensa-que-no-sabe-de-directivas","text":"11. La deuda de la parte 5: una recompensa que no sabe de directivas"},{"depth":2,"slug":"12-un-experimento-reproducible","text":"12. Un experimento reproducible"},{"depth":2,"slug":"13-un-flujo-de-trabajo-para-diseñar-recompensas","text":"13. Un flujo de trabajo para diseñar recompensas"},{"depth":2,"slug":"conclusión","text":"Conclusión"},{"depth":2,"slug":"referencias","text":"Referencias"}],"body":"> **Serie: Reinforcement Learning desde cero, con Agentic Racing como laboratorio**\n>\n> 1. Parte 1 · [¿Cómo aprende un agente? Enseñando a un automóvil a conducir](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/)\n> 2. Parte 2 · [¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/)\n> 3. Parte 3 · [¿Podemos enseñar imitando? Behavioral Cloning explicado desde cero](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/)\n> 4. Parte 4 · [¿Podemos aprender a comportarnos como un experto? GAIL explicado desde cero](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/)\n> 5. Parte 5 · [¿Podemos combinar imitación y RL? BC, GAIL y PPO en un mismo sistema](/es/blog/podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema/)\n> 6. Parte 6 · **¿Cómo le decimos a un agente qué queremos? Reward engineering explicado desde cero** (este artículo)\n> 7. Parte 7 · [¿Qué debería poder ver un agente? Observation design explicado desde cero](/es/blog/que-deberia-poder-ver-un-agente-observation-design-explicado-desde-cero/)\n> 8. Parte 8 · [¿Qué hacemos cuando el agente no aprende? RL debugging explicado desde cero](/es/blog/que-hacemos-cuando-el-agente-no-aprende-rl-debugging-explicado-desde-cero/)\n\n## Dónde nos quedamos\n\nLas partes 3, 4 y 5 hablaron de imitación: BC, GAIL y cómo combinarlos con PPO. En las tres hubo que\nadvertir lo mismo: en Agentic Racing esas técnicas se configuraron, pero nunca se ejecutaron.\n\nEsta parte es distinta. La recompensa sí existe, está en el código, y tiene historia: ocho corridas de\nentrenamiento con PPO y, según la bitácora, siete variantes de la recompensa entre la primera y la\núltima. La\n[parte 1](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/) mostró la versión final\ny contó el atajo más famoso del proyecto. Aquí la usamos como caso de estudio para la pregunta de\nfondo:\n\n> ¿Cómo convertimos lo que queremos en una señal que una máquina pueda optimizar?\n\nY hay una deuda pendiente. La [parte 5](/es/blog/podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema/)\nterminó con un hallazgo incómodo: la recompensa del proyecto no lee las directivas del estratega, que\nson la razón de ser del piloto. Volveremos a eso al final, con una propuesta.\n\nLa honestidad de siempre: ninguna versión de esta recompensa produjo un piloto que diera vueltas de\nforma consistente.\nParte de lo que este artículo cuenta es justamente por qué.\n\n---\n\n## 1. El agente optimiza la señal, no la intención\n\nEn la parte 1 vimos que el agente no maximiza la recompensa de un paso, sino el **return**, la suma\ndescontada de las recompensas futuras:\n\n$$\nG_t = \\sum_{k=0}^{\\infty} \\gamma^k\\, r_{t+k+1}\n$$\n\nEn Agentic Racing, $\\gamma = 0.995$ por decisión, y el agente decide 10 veces por segundo.\n\nLa idea central de este artículo cabe en una línea:\n\n> El agente optimiza la señal que definimos, no la intención que teníamos en la cabeza.\n\nSi la recompensa representa mal el objetivo, el agente puede aprender una estrategia excelente según\nla fórmula y decepcionante para nosotros. No porque \"haga trampa\": simplemente hace lo que le pedimos,\ncon más rigor que nosotros al pedirlo.\n\n---\n\n## 2. De un objetivo humano a una lista de términos\n\nEl objetivo de Agentic Racing, dicho en palabras, era algo así:\n\n> Dar la vuelta al circuito rápido, sin salirse de la pista, sin chocar y sin quedarse parado.\n\nLa recompensa final lo tradujo en nueve componentes (la tabla completa está en la\n[sección 6 de la parte 1](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/)), más las\ncondiciones de fin de episodio. Agrupados, sin pretender una correspondencia exacta, por la parte del\nobjetivo que intentan representar:\n\n| Parte del objetivo | Términos de la recompensa |\n|---|---|\n| dar la vuelta | progreso por metro, bono por vuelta completa |\n| rápido | velocidad objetivo por segundo, bono extra por vuelta rápida |\n| sin salirse | cercanía al borde, salida de pista |\n| sin chocar | golpe contra el muro |\n| sin quedarse parado | lentitud, detenido, fin por falta de progreso |\n| en el sentido correcto | fin por ir en sentido contrario |\n| conducir bien | seguir la trazada ideal |\n\nCada fila es una decisión de diseño, y cada peso es una afirmación sobre prioridades: \"un metro de\nprogreso vale 0.02\", \"salirse vale −1\". Ninguna de esas cifras viene de la física ni del objetivo; las\nelegimos nosotros. La pregunta es qué dicen cuando se miran juntas.\n\n---\n\n## 3. Escalas: qué dice cada término en una vuelta completa\n\nEl primer ejercicio útil es sumar. Tomemos el circuito fijo del proyecto (unos 2 km) y una vuelta de\nunos 95 segundos (la bitácora estima ese ritmo; el piloto heurístico midió 100 s con directivas\naleatorias). Con las constantes del código, esto es lo máximo que\ncada término podría pagar en esa vuelta:\n\n![Lo que podría pagar una vuelta del circuito fijo, término por término: progreso unos +40, velocidad objetivo hasta unos +24, bono de vuelta +12 fijo más un bono de rapidez de solo 1.7 a 95 segundos, trazada ideal menos de +2, salirse −1 y cada golpe contra el muro −0.1. Son cotas calculadas con las constantes del código, no mediciones.](/images/reward-lap-budget.svg)\n\nUna advertencia: son **cotas** calculadas a partir de las constantes, no mediciones. Ninguna policy de\nRL llegó a dar esta vuelta; el circuito fijo llegó después de la última corrida de entrenamiento.\n\nAun así, la suma dice cosas que la tabla de pesos no dice:\n\n- **El progreso domina.** Unos 40 puntos por vuelta, casi tanto como todo lo demás junto.\n- **\"Rápido\" casi no está en la recompensa.** El bono por vuelta rápida escala con el tiempo que sobra\n  del límite del episodio (120 s). Una vuelta de 95 s cobra unos +1.7; una de 79 s, unos +2.7. Dieciséis\n  segundos de diferencia valen un punto, en una vuelta que puede pagar hasta unos 80.\n- **La trazada ideal es casi decorativa.** Menos de 2 puntos por vuelta, sea cual sea el tiempo: como\n  se multiplica por la velocidad del auto, en la práctica paga por metro.\n- **Salirse cuesta lo mismo que 50 metros de progreso.** Visto así, −1 es poco. Pero su costo real no\n  es el −1: es todo lo que el agente deja de cobrar en el resto de la vuelta. Volveremos a esto en la\n  sección 6.\n\nEste ejercicio también apareció a la mala en el proyecto. Después de la segunda corrida, la bitácora\nanota que los términos de velocidad y de trazada recién agregados eran, por paso, entre 10 y 30 veces\nmás chicos que el de progreso y unas 1000 veces más chicos que la penalización por salirse. Estaban en\nla recompensa, pero no pesaban.\n\nLa documentación de ML-Agents da una regla de escala concreta: la recompensa entre dos decisiones\ndebería estar en el rango $[-1, 1]$, porque valores mayores pueden desestabilizar el entrenamiento. La\nsalida de pista la respeta. El bono de vuelta, de 12 a 20 en una sola decisión, no. En la práctica casi\nno importó, por una razón peor: el agente casi nunca llegaba a cobrarlo.\n\n---\n\n## 4. Recompensas escasas y densas, con números\n\nUna recompensa **escasa** solo paga cuando ocurre el objetivo: por ejemplo, +1 al completar la vuelta\ny nada más. Una **densa** paga pequeñas cantidades a lo largo del camino, como el progreso por metro.\n\nLa escasa está más cerca del objetivo. El problema es si el agente puede verla. Con $\\gamma = 0.995$\ny una vuelta de unas 950 decisiones, un premio al final de la vuelta, visto desde la salida, pesa:\n\n$$\n0.995^{950} \\approx 0.009\n$$\n\nmenos del 1% de su valor. Y eso suponiendo que el agente llegue a la meta alguna vez, que es justo lo\nque no pasaba: de la cuarta a la octava corrida, solo entre un 2% y un 4% de los episodios terminaban\ncon la vuelta completa. La bitácora de la segunda corrida lo resume con una frase: el bono de vuelta era \"peso\nmuerto\".\n\nPor eso la recompensa del proyecto es densa. Lo más probable es que el progreso por metro sea lo que\nle enseñó al agente el primer 10% de la vuelta: arrancar, ir hacia adelante, no salirse en la primera\nrecta.\n\nPero cada término denso es un incentivo nuevo, y la pregunta útil no es \"¿escasa o densa?\", sino:\n\n> ¿La señal intermedia ayuda al agente a descubrir el objetivo final, o le enseña un objetivo\n> distinto?\n\n---\n\n## 5. Unidades: por metro, por segundo, por evento\n\nLos términos de la recompensa no se miden en las mismas unidades, y eso cambia lo que significan.\n\n- **Por metro** (el progreso): paga lo mismo por un metro, se recorra en un segundo o en diez. No\n  depende de la duración del episodio.\n- **Por segundo** (velocidad objetivo, borde, lentitud, detenido): paga más cuanto más tiempo dure la\n  situación. Un término positivo por segundo es, en el fondo, un premio por seguir vivo.\n- **Por evento** (vuelta, salida, golpe): paga una vez, cuando algo pasa.\n\n(La trazada es un caso intermedio: se calcula por segundo, pero multiplicada por la velocidad, así que\nen la práctica paga por metro.)\n\nUna consecuencia buena del diseño: como los términos por segundo se multiplican por la duración del\npaso de física, no cambian si se cambia la frecuencia de simulación. No todo es así: el límite del\nepisodio, la cadencia de decisiones (y con ella $\\gamma$) y el bono por vuelta rápida se cuentan en\npasos. Una consecuencia menos visible: el bono por vuelta rápida depende del límite de pasos del episodio, `MaxStep`. Cuando\nel proyecto pasó al circuito fijo, ese límite subió de 4000 a 6000 pasos (de 80 a 120 segundos), porque\ncon 80 segundos no alcanzaba para cerrar una vuelta. Con ese cambio, el mismo tiempo de vuelta pasó a\nvaler un bono distinto, sin tocar una sola constante de la recompensa.\n\nLa interacción más interesante es entre progreso (por metro) y velocidad objetivo (por segundo). El\ntérmino de velocidad tiene su máximo justo en la velocidad objetivo, que el código calcula según la\ncurva que viene: 42% de la velocidad máxima en recta (23.1 m/s) y menos en curva. Uno pensaría que la\nrecompensa total también tiene su máximo ahí. No necesariamente.\n\nPor segundo, el progreso paga $0.02\\,v$ y el término de velocidad pierde\n$0.25 \\times 1.3 / v_{\\text{obj}}$ por cada m/s por encima del objetivo. Las dos cosas se equilibran\ncuando $v_{\\text{obj}} = 0.25 \\times 1.3 / 0.02 = 16.25$ m/s (un poco menos si se suma la trazada):\n\n- **Con un objetivo por debajo de 16.25 m/s**, hay un máximo local en el objetivo: frenar hasta ahí paga\n  más que pasarse un poco. Pero es solo local: a velocidades bastante mayores el término de velocidad\n  llega a cero y el progreso solo lo supera. Con un objetivo de 10 m/s, por ejemplo, pasar a más de\n  unos 22.5 m/s pagaría más por segundo que frenar.\n- **Con un objetivo por encima**, no hay máximo: la recompensa sigue subiendo después del objetivo.\n\nY aquí está el hallazgo. Una velocidad objetivo de 10 m/s corresponde a curvas cerradas como las de las\npistas procedurales. En el circuito fijo, con curvas de 120 m de radio, la velocidad objetivo nunca baja\nde unos **18.1 m/s**, por encima del umbral. Es decir, en el circuito donde el proyecto terminó,\n**el término de velocidad nunca le dice al agente que frene**: por segundo, más velocidad siempre paga\nmás, en recta y en curva (en las curvas, por muy poco).\n\n![Recompensa por segundo de progreso más velocidad objetivo, según la velocidad del auto, para tres objetivos. Con 10 m/s, el de una curva cerrada de las pistas procedurales, hay un máximo local en el objetivo, pero por encima de unos 22.5 m/s el progreso solo ya paga más. Con 18.1 m/s, el de las curvas del circuito fijo, y con 23.1 m/s, el de las rectas, la curva sigue subiendo después del objetivo.](/images/reward-speed-curve.svg)\n\nLo que haría correcto frenar en una curva, entonces, no es el término de velocidad. Es la posibilidad de\nsalirse de la pista, que termina el episodio y corta todo lo que vendría después. Si el auto puede tomar\nla curva a más velocidad sin salirse, la recompensa lo prefiere así. Eso no hace inútil al término de\nvelocidad (en las curvas cerradas sí crea una zona donde frenar paga), pero muestra que el\ncomportamiento final sale de la combinación, no de leer cada término por separado.\n\n---\n\n## 6. Terminar el episodio también es una recompensa\n\nEste es el punto que más se subestima, y en Agentic Racing fue el origen del atajo más famoso.\n\nCuando un episodio termina, el agente no recibe solo la penalización de ese momento. **Pierde todo lo\nque habría cobrado después.** Así que la decisión real del agente no es \"¿me conviene el −1?\", sino\n\"¿me conviene más el −1 que el futuro?\". Y eso depende del signo de lo que el futuro paga:\n\n- Si la recompensa esperada por paso es **negativa**, terminar antes es un premio.\n- Si es **positiva**, sobrevivir es un premio.\n\nLas dos caras aparecieron en esta serie:\n\n**La cara negativa: la cuarta corrida.** La recompensa tenía una pequeña penalización plana por paso\npara que el agente terminara rápido, y quedarse detenido terminaba el episodio con −1. El agente\naprendió a aprovechar el arranque lanzado, recorrer unos 245 metros cobrando progreso, detenerse y\naceptar el −1:\n\n![El atajo de la cuarta corrida: el auto avanza unos 245 metros, se detiene a propósito, y el episodio termina con un return de +4.9 de progreso, −0.08 de penalización de tiempo y −1 por detenerse, cerca de +3.8, exactamente donde se estancó la curva de entrenamiento.](/images/hero-reward-dark.svg)\n\nLo curioso es que la penalización por paso es exactamente lo que recomienda la documentación de\nML-Agents para que un agente termine rápido una tarea, con una condición: que completar la tarea\ncoincida con el fin del episodio. Aquí también *fallar* terminaba el episodio, y fallar costaba menos\nque seguir intentando. La receta genérica, aplicada a un entorno donde el fracaso también termina el\nepisodio, premió el fracaso rápido.\n\n**La cara positiva: GAIL.** En la [parte 4](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/)\nvimos que la recompensa de GAIL nunca es negativa, así que alarga los episodios. Lo mismo vale para\ncualquier término positivo por segundo, como la velocidad objetivo de la sección anterior.\n\n![Terminar el episodio también es una recompensa: con recompensa negativa por paso, terminar antes paga (la cuarta corrida); con recompensa positiva por paso, sobrevivir paga (el sesgo de GAIL de la parte 4). En el centro, las condiciones de fin de episodio del proyecto y lo que paga cada una.](/images/reward-termination.svg)\n\nPor eso las condiciones de fin del episodio son parte del diseño de la recompensa, tanto como los\npesos. Las del proyecto, después de varias versiones:\n\n| Fin del episodio | Condición | Recompensa |\n|---|---|---|\n| salida de pista | 2 m más allá del borde | −1 |\n| detenido | 8 s parado, después de haber arrancado | −1 (más −0.3 por segundo mientras está parado) |\n| sentido contrario | 4.5 s retrocediendo a más de 7.5 m/s, solo después de 15 m de progreso | −1 |\n| sin progreso | menos de 8 m de pista en 5 s | −1 |\n| vuelta completa | 99% del recorrido | +12 a +20 |\n| límite de tiempo | `MaxStep` | 0 |\n\nCasi cada fila es una cicatriz. La corrección de la cuarta corrida fue que detenerse dejara de terminar\nel episodio y pasara a costar por segundo, con un corte duro solo a los 8 segundos. El sentido\ncontrario se volvió más tolerante porque terminaba el episodio en la única maniobra que sirve para\ndespegarse de un muro: una reversa corta. Y el corte por falta de progreso llegó cuando la evaluación\ndel piloto heurístico mostró autos en \"diente de sierra\" cerca de la salida: chocaban, retrocedían un\npoco, volvían a chocar, y como cada reversa reiniciaba el contador de \"detenido\", sobrevivían 80\nsegundos sin avanzar.\n\n---\n\n## 7. Reward hacking: lo que se puede comprobar en el código\n\nEl *reward hacking* es cuando el agente consigue mucha recompensa por un camino que no es el que\nqueríamos. La regla práctica para buscarlo antes de entrenar es una pregunta:\n\n> ¿Qué otras conductas podrían producir esta misma recompensa?\n\nCon el código a mano, varias de las sospechas clásicas se pueden responder sin entrenar nada:\n\n| Sospecha | ¿Es posible en el código? |\n|---|---|\n| Ir y volver para cobrar progreso dos veces | No: el progreso tiene signo, retroceder resta lo mismo que avanzar sumó |\n| Cobrar un salto falso al cruzar la línea de meta | No: el cálculo de progreso corrige el paso por el cierre del circuito |\n| Recortar camino por fuera de la pista | No: 2 m más allá del borde el episodio termina |\n| Repetir un evento de choque | Sí, pero es una penalización: repetirla solo resta |\n| Quedarse parado y alineado para cobrar la trazada | No: ese término solo paga si el auto se mueve, y en proporción a su velocidad |\n| Dar vueltas en círculo cobrando el término de velocidad | **Parcialmente**: ese término usa la velocidad del auto hacia su propio frente, no a lo largo de la pista, así que un auto girando en círculos lo cobra sin avanzar. A la velocidad objetivo no cabe un círculo entre los muros, pero a baja velocidad sí, con una ganancia pequeña. El corte por falta de progreso lo termina en 5 a 10 segundos con −1 |\n\nLa última fila es la interesante. No hay evidencia de que la policy lo haya hecho (los círculos a media\npista aparecieron en el piloto heurístico, no en el RL), pero el agujero existe en la definición y lo\ntapa una condición de fin, no la recompensa. Es el tipo de cosa que conviene saber antes de que un\nentrenamiento la descubra.\n\nY luego están los atajos que no son atajos sino **conflictos entre términos**, que el proyecto sí vivió:\n\n- **La trazada contra los muros.** Premiar seguir la trazada ideal parecía razonable, pero en este\n  circuito la trazada ideal pasa pegada al borde en cada curva. El término empujaba al auto contra el\n  muro. La solución fue bajar su peso y agregar una penalización suave por acercarse al borde.\n- **La lentitud contra el frenado.** La quinta versión castigaba ir lento para evitar que el agente se\n  arrastrara. Pero frenar antes de una curva es ir más lento, así que el castigo caía justo sobre la\n  maniobra que el agente necesitaba aprender. La séptima versión lo reemplazó por la velocidad objetivo\n  según la curva, con una penalización por lentitud que ahora se mide respecto de ese objetivo.\n\n---\n\n## 8. Reward shaping: ayudar sin cambiar el objetivo\n\nEl *reward shaping* consiste en añadir señales intermedias para que aprender sea más fácil. El riesgo\nes que cambien lo que el agente considera óptimo. Hay una forma de shaping que, bajo ciertas\ncondiciones, está demostrado que no lo cambia (Ng, Harada y Russell, 1999). Se basa en una función\npotencial $\\Phi(s)$ del estado:\n\n$$\nF(s, s') = \\gamma\\,\\Phi(s') - \\Phi(s)\n$$\n\nque se suma a la recompensa original:\n\n$$\nr'(s, a, s') = r(s, a, s') + F(s, s')\n$$\n\nLa intuición: lo que el agente gana al acercarse a la meta lo devuelve si se aleja, así que no hay forma\nde acumular shaping dando vueltas.\n\n¿Qué tan cerca está la recompensa de Agentic Racing de esto?\n\n- **El progreso se parece mucho.** Es la diferencia de un potencial, $\\Phi = 0.02 \\times$ (metros\n  recorridos de la vuelta), entre un paso y el siguiente. Pero le falta el factor $\\gamma$, y el\n  potencial no vuelve a cero cuando el episodio termina; las dos son condiciones del teorema. Además,\n  si se toma como recompensa \"verdadera\" la vuelta completa y las terminaciones, el progreso no es un\n  ajuste pequeño sobre ella: es la mayor parte de lo que paga. La garantía no aplica tal cual.\n- **La velocidad objetivo, la trazada y el borde no lo son.** No son diferencias de un potencial:\n  pagan según *cómo* se mueve el auto, no solo según *dónde* está. Esos términos sí cambian lo que es\n  óptimo, y es a propósito: le dicen al agente a qué velocidad ir y por dónde.\n\nEse último punto tiene una lectura interesante a la luz de las partes 3 a 5. La velocidad objetivo de\nla recompensa es una variante de la regla de frenado del piloto heurístico: el comentario del código\ndice que \"imita a la heurística\", y las dos calculan una velocidad según la curva que viene, con\nconstantes parecidas. Es decir, **la recompensa ya contenía una forma de imitación**, escrita a mano en\nlugar de aprendida de demostraciones. La documentación de ML-Agents aconseja lo contrario: premiar\nresultados, no las acciones que uno cree que llevan a ellos. El proyecto eligió premiar una acción\n(una velocidad) porque el resultado (dar la vuelta) no aparecía.\n\n---\n\n## 9. La recompensa no puede ser la única métrica\n\nSi evaluamos al agente con la misma señal con la que lo entrenamos, heredamos sus puntos ciegos. El\nproyecto lo aprendió con tres episodios concretos:\n\n**Una curva que subía no era una mejora.** En la tercera corrida, la recompensa acumulada subió hasta\n~10, entre dos y tres veces el estancamiento de las corridas anteriores, y los episodios se hicieron cinco veces más\nlargos. Parecía el avance que se esperaba. La cuarta corrida, con más pasos y la misma configuración,\nvolvió al pozo de siempre. La bitácora lo cerró así: la tercera había sido \"varianza favorable de una\ncorrida\". Una sola corrida no es evidencia.\n\n**Un número estable no era un buen número.** El estancamiento de la cuarta corrida, cerca de +3.8,\ncoincidía exactamente con el return del atajo de la sección 6. La curva no decía \"aprendió poco\": decía\n\"aprendió perfectamente a hacer otra cosa\".\n\n**Las curvas de recompensa de versiones distintas no se pueden comparar.** La quinta corrida empezó en\n−34; la primera, en −1.1. No porque la quinta fuera peor, sino porque la recompensa había cambiado.\nCada versión tiene su propia escala, y comparar sus curvas es comparar unidades distintas.\n\nPor eso el proyecto construyó, después de la cuarta corrida, un evaluador independiente (`eval.exe`)\nque no mira la recompensa. Corre las policies y reporta otras cosas:\n\n| Métrica del evaluador | Qué reveló |\n|---|---|\n| motivos de fin de episodio | la cuarta corrida terminaba \"detenido\" el 86% de las veces |\n| fracción de vuelta recorrida | todas las corridas se quedaban cerca del 10% |\n| freno medio | **0.02**: la policy nunca frenaba, y sin frenar no se toma una curva |\n| velocidad, acelerador y dirección medios | dos estilos opuestos con la misma fracción de vuelta: reptar a 7 m/s, o acelerar y terminar detenido o fuera de la pista |\n\nEl freno medio de 0.02 fue el dato más útil de toda la fase, y no estaba en ninguna curva de\nrecompensa.\n\n---\n\n## 10. Lo que ninguna recompensa podía arreglar\n\nHay una última lección, y es la más cara. La intuición habitual, y la que también tenía el plan del\nproyecto, es que cuando un agente aprende algo raro, la culpa casi siempre es de la recompensa. En\nAgentic Racing la bitácora cuenta siete variantes de la recompensa. El número que importaba, la\nfracción de vuelta, no se movió de entre un 8 y un 13%.\n\nMientras tanto, la bitácora fue nombrando \"causa raíz\", una tras otra, a cosas que no eran la\nrecompensa: los muros (mallas de espesor cero donde el auto se clavaba), el agarre lateral (que borraba\nla velocidad lateral en vez de redirigirla, así que cada giro fuerte era un frenazo) y las curvas de\n12 m de radio de las pistas procedurales. Algunas se descartaron después: tres versiones distintas de\nlos muros dieron el mismo resultado, y la bitácora concluye \"los muros no eran la causa\". Otras se\narreglaron antes de la octava corrida, que igual se estancó en el mismo lugar.\n\nLo que destrabó el problema fue otra cosa: cambiar las pistas procedurales por el circuito fijo. Ahí el\npiloto heurístico, por primera vez, recorrió vueltas limpias, y la bitácora atribuye la \"parálisis\" de\nlas ocho corridas a las pistas procedurales (tramos degenerados de la línea central, muros mal formados\nen curva, curvas que nadie podía tomar), no a un bug de fondo de la física del auto. Si PPO aprende a\ndar vueltas en el circuito fijo es algo que nunca se probó.\n\nEl experimento que podía separar \"problema de recompensa\" de \"problema de entorno\" se hizo a mitad de\ncamino, después de la quinta corrida: poner al piloto heurístico, escrito a mano, en el mismo entorno.\nPero se leyó a favor del entorno: que su mejor episodio llegara al 82% de la vuelta se tomó como prueba\nde que el entorno era aprendible, y después de la séptima corrida la bitácora llegó a escribir \"la\nrecompensa ya es correcta\". La respuesta completa llegó recién con el circuito fijo, cuando el piloto\nheurístico pasó de un buen episodio aislado a vueltas completas. La lección no es solo hacer ese\nexperimento: es exigirle una respuesta clara. Si un controlador escrito a mano no puede dar la vuelta\nde forma consistente, ninguna recompensa va a enseñarle a una red neuronal a hacerlo.\n\nLa documentación de ML-Agents sugiere algo parecido con otra intención: usar la heurística del agente\npara conducirlo y mirar cómo acumula recompensa. Aplicado aquí, habría servido para dos cosas a la vez:\nsaber si el entorno era manejable, y saber si la recompensa premiaba al experto por encima de las\npolicies que fallaban. El proyecto hizo lo primero; la bitácora no registra que se haya medido lo\nsegundo.\n\n---\n\n## 11. La deuda de la parte 5: una recompensa que no sabe de directivas\n\nEn la parte 5 vimos que el piloto existe para obedecer al estratega, y que el piloto heurístico lo\nhace: con la misma pista, tarda 112 s por vuelta con \"conservar\" y agresión baja, y 79 s con \"atacar\" y\nagresión alta. La recompensa, en cambio, calcula su velocidad objetivo solo a partir de la curva. Ningún\ntérmino lee los canales de directiva.\n\nEl piloto heurístico sí escala su velocidad objetivo según la directiva, a través de un mapa único\n(`StrategyDirectiveMap`) que también usa el estratega. La propuesta más directa sería que la recompensa\nusara ese mismo mapa:\n\n$$\nv_{\\text{obj}}^{\\text{directiva}} = v_{\\text{obj}}(\\text{curva}) \\times \\text{escala}(\\text{directiva})\n$$\n\n(En ese mapa, la escala de velocidad sale de la agresión, entre 0.86 y 1.16, con un 10% menos si la\ndirectiva es \"conservar\"; el tipo \"atacar\" cambia sobre todo la línea.) Con eso, la recompensa le diría\nal agente \"con agresión alta, la velocidad correcta es más alta\", y una\npolicy que la optimizara tendría una razón para responder a la directiva.\n\nEs una **propuesta**, no algo probado, y trae sus propias preguntas: si la escala debería aplicarse\ntambién al margen de frenado y a la línea, como hace la heurística; si el agente aprendería a\ndistinguir directivas que en la recompensa solo difieren en la velocidad; y si eso empeoraría la\nestabilidad del entrenamiento. Pero ilustra el punto de este artículo: el hallazgo de la parte 5 no se\narregla con un algoritmo, se arregla diciendo mejor lo que queremos.\n\n---\n\n## 12. Un experimento reproducible\n\nSi se retoma el piloto RL sobre el circuito fijo, el experimento natural compara versiones de la\nrecompensa cambiando **una sola cosa cada vez**:\n\n| Variante | Recompensa | Pregunta |\n|---|---|---|\n| A. Escasa | solo vuelta completa y terminaciones | ¿puede aprender sin señal densa en este circuito? |\n| B. Actual | la de la octava corrida | la referencia: nunca se entrenó en el circuito fijo |\n| C. Sin velocidad objetivo | B sin ese término | ¿cuánto aporta la imitación escrita a mano de la sección 8? |\n| D. Con directivas | B con la velocidad objetivo escalada por la directiva | ¿aparece la respuesta a la directiva? |\n\nComparaciones: A contra B, B contra C y B contra D. Para que signifiquen algo:\n\n- **Mismo build, mismo circuito, mismo `MaxStep`.** El bono de vuelta rápida depende de él (sección 5).\n- **Recompilar el player en cada variante.** Los pesos de la recompensa se leen de los valores por\n  defecto del código, no de una escena; cambiarlos exige reconstruir el ejecutable de entrenamiento.\n- **Registrar cada componente de la recompensa por separado.** Hoy el proyecto no los registra por\n  separado: TensorBoard solo ve la recompensa total. ML-Agents permite publicar métricas propias en TensorBoard desde el código del\n  agente; sin eso no hay forma de saber qué término está moviendo la curva.\n- **Varias semillas por variante**, con media y dispersión. La tercera corrida ya mostró lo que vale una\n  sola.\n- **Evaluar con `eval.exe`, no con la recompensa.** Motivos de fin, fracción de vuelta, freno medio y,\n  para D, tiempo por vuelta con cada directiva forzada.\n- **Medir también al experto con cada recompensa.** Si el piloto heurístico no saca más recompensa que\n  una policy que falla, la recompensa está midiendo otra cosa.\n\n| Métrica | A | B | C | D |\n|---|:---:|:---:|:---:|:---:|\n| Vueltas completas | ? | ? | ? | ? |\n| Fracción de vuelta | ? | ? | ? | ? |\n| Freno medio | ? | ? | ? | ? |\n| Salidas de pista | ? | ? | ? | ? |\n| Diferencia atacar / conservar | ? | ? | ? | ? |\n| Recompensa del experto con esa función | ? | ? | ? | ? |\n\nNada de esto se ejecutó. Las celdas quedan con \"?\".\n\n---\n\n## 13. Un flujo de trabajo para diseñar recompensas\n\nJuntando todo, el ciclo que el proyecto habría querido seguir desde el principio:\n\n```text\n0. Verificar que el entorno se pueda resolver con un controlador simple\n            ↓\n1. Escribir el objetivo en palabras\n            ↓\n2. Traducirlo a eventos medibles y a terminaciones\n            ↓\n3. Empezar con una recompensa mínima\n            ↓\n4. Sumar cada término sobre una vuelta: escalas, unidades, signos\n            ↓\n5. Entrenar, registrando cada componente por separado\n            ↓\n6. Evaluar con métricas que no sean la recompensa, en varias semillas\n            ↓\n7. Mirar episodios concretos, no solo curvas\n            ↓\n8. Cambiar una sola cosa, y volver al paso 4\n```\n\nEl paso 0 no suele aparecer en las guías de reward engineering. En Agentic Racing fue el que más\ntiempo habría ahorrado.\n\n---\n\n## Conclusión\n\nReward engineering no consiste en repartir puntos positivos y negativos. Consiste en traducir un\nobjetivo que nosotros entendemos a una señal que el agente pueda optimizar, y después comprobar que\noptimizar esa señal produce lo que queríamos.\n\nLa recompensa de Agentic Racing deja varias lecciones concretas:\n\n- los pesos no dicen nada hasta que se suman sobre un episodio completo;\n- las unidades (por metro, por segundo, por evento) cambian lo que cada término premia;\n- terminar el episodio es una recompensa, y su signo depende de lo que el futuro paga;\n- los términos pueden pelearse entre sí, como la trazada con los muros o la lentitud con el frenado;\n- una recompensa puede contener imitación escrita a mano sin que nadie la llame así;\n- la recompensa no puede ser la única métrica;\n- y ninguna recompensa arregla un entorno donde el problema no se puede resolver.\n\nLa pregunta que conviene hacerse antes de cada entrenamiento sigue siendo la misma:\n\n> ¿Qué estamos premiando exactamente, y qué otras cosas podría aprender a hacer el agente para\n> conseguir esa recompensa?\n\nSi llegaste directo a este artículo, las cinco partes anteriores están enlazadas al principio, y\n[Agentic Racing: un piloto que nunca aprendió a manejar](/es/blog/agentic-racing-un-piloto-que-nunca-aprendio-a-manejar/)\ncuenta la historia completa del proyecto.\n\n---\n\n## Referencias\n\n- Sutton, R. S. y Barto, A. G. (2018). [Reinforcement Learning: An Introduction](http://incompleteideas.net/book/the-book-2nd.html), 2.ª edición. MIT Press.\n- Ng, A. Y., Harada, D. y Russell, S. (1999). [Policy Invariance Under Reward Transformations: Theory and Application to Reward Shaping](https://people.eecs.berkeley.edu/~russell/papers/icml99-shaping.pdf). *ICML*.\n- Amodei, D. et al. (2016). [Concrete Problems in AI Safety](https://arxiv.org/abs/1606.06565). (Reward hacking como problema de diseño.)\n- Schulman, J. et al. (2017). [Proximal Policy Optimization Algorithms](https://arxiv.org/abs/1707.06347).\n- Ho, J. y Ermon, S. (2016). [Generative Adversarial Imitation Learning](https://arxiv.org/abs/1606.03476). *NeurIPS*.\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), release de\n  `mlagents` 1.1.0: la sección \"Rewards\" de `docs/Learning-Environment-Design-Agents.md` (rango de la\n  recompensa por decisión, penalización por paso, usar la heurística para inspeccionar la recompensa).\n- La recompensa (`unity/Assets/Scripts/Agents/RaceAgent.cs`), el evaluador y la bitácora con las ocho\n  corridas están en [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch","lang":"en","title":"How Do We Tell an Agent What We Want? Reward Engineering Explained from Scratch","description":"Part 6 of the Reinforcement Learning from scratch series with Agentic Racing. How a human goal gets translated into a reward, using the project's real RL pilot reward as a case study: scales, units, dense and sparse rewards, terminations that act as rewards, reward shaping, and why seven variants of the reward couldn't fix a problem that wasn't about the reward.","publishDate":"2026-10-10","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero","url":"https://alexisalulema.com/blog/how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch/","series":null,"headings":[{"depth":2,"slug":"where-we-left-off","text":"Where we left off"},{"depth":2,"slug":"1-the-agent-optimizes-the-signal-not-the-intention","text":"1. The agent optimizes the signal, not the intention"},{"depth":2,"slug":"2-from-a-human-goal-to-a-list-of-terms","text":"2. From a human goal to a list of terms"},{"depth":2,"slug":"3-scales-what-each-term-says-over-a-full-lap","text":"3. Scales: what each term says over a full lap"},{"depth":2,"slug":"4-sparse-and-dense-rewards-with-numbers","text":"4. Sparse and dense rewards, with numbers"},{"depth":2,"slug":"5-units-per-metre-per-second-per-event","text":"5. Units: per metre, per second, per event"},{"depth":2,"slug":"6-ending-the-episode-is-also-a-reward","text":"6. Ending the episode is also a reward"},{"depth":2,"slug":"7-reward-hacking-what-you-can-check-in-the-code","text":"7. Reward hacking: what you can check in the code"},{"depth":2,"slug":"8-reward-shaping-helping-without-changing-the-goal","text":"8. Reward shaping: helping without changing the goal"},{"depth":2,"slug":"9-the-reward-cant-be-the-only-metric","text":"9. The reward can’t be the only metric"},{"depth":2,"slug":"10-what-no-reward-could-fix","text":"10. What no reward could fix"},{"depth":2,"slug":"11-part-5s-debt-a-reward-that-knows-nothing-about-directives","text":"11. Part 5’s debt: a reward that knows nothing about directives"},{"depth":2,"slug":"12-a-reproducible-experiment","text":"12. A reproducible experiment"},{"depth":2,"slug":"13-a-workflow-for-designing-rewards","text":"13. A workflow for designing rewards"},{"depth":2,"slug":"conclusion","text":"Conclusion"},{"depth":2,"slug":"references","text":"References"}],"body":"> **Series: Reinforcement Learning from scratch, with Agentic Racing as the lab**\n>\n> 1. Part 1 · [How Does an Agent Learn? Teaching a Car to Drive](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/)\n> 2. Part 2 · [How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/)\n> 3. Part 3 · [Can We Teach by Imitation? Behavioral Cloning Explained from Scratch](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/)\n> 4. Part 4 · [Can We Learn to Behave Like an Expert? GAIL Explained from Scratch](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/)\n> 5. Part 5 · [Can We Combine Imitation and RL? BC, GAIL, and PPO as One System](/blog/can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system/)\n> 6. Part 6 · **How Do We Tell an Agent What We Want? Reward Engineering Explained from Scratch** (this article)\n> 7. Part 7 · [What Should an Agent Be Able to See? Observation Design Explained from Scratch](/blog/what-should-an-agent-be-able-to-see-observation-design-explained-from-scratch/)\n> 8. Part 8 · [What Do We Do When the Agent Doesn't Learn? RL Debugging Explained from Scratch](/blog/what-do-we-do-when-the-agent-doesnt-learn-rl-debugging-explained-from-scratch/)\n\n## Where we left off\n\nParts 3, 4, and 5 were about imitation: BC, GAIL, and how to combine them with PPO. All three came with\nthe same warning: in Agentic Racing those techniques were configured but never run.\n\nThis part is different. The reward does exist, it's in the code, and it has a history: eight PPO\ntraining runs and, according to the devlog, seven variants of the reward between the first and the\nlast.\n[Part 1](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/) showed the final version and told the\nstory of the project's most famous shortcut. Here we use it as a case study for the underlying\nquestion:\n\n> How do we turn what we want into a signal a machine can optimize?\n\nAnd there's a debt to pay. [Part 5](/blog/can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system/)\nended with an uncomfortable finding: the project's reward doesn't read the strategist's directives,\nwhich are the pilot's whole reason to exist. We'll come back to that at the end, with a proposal.\n\nThe usual honesty: no version of this reward produced a pilot that completed laps consistently. Part of what this\narticle explains is exactly why.\n\n---\n\n## 1. The agent optimizes the signal, not the intention\n\nIn part 1 we saw that the agent doesn't maximize a single step's reward but the **return**, the\ndiscounted sum of future rewards:\n\n$$\nG_t = \\sum_{k=0}^{\\infty} \\gamma^k\\, r_{t+k+1}\n$$\n\nIn Agentic Racing, $\\gamma = 0.995$ per decision, and the agent decides 10 times per second.\n\nThe central idea of this article fits in one line:\n\n> The agent optimizes the signal we define, not the intention we had in mind.\n\nIf the reward represents the goal badly, the agent can learn a strategy that is excellent according to\nthe formula and disappointing to us. Not because it \"cheats\": it simply does what we asked, more\nrigorously than we asked it.\n\n---\n\n## 2. From a human goal to a list of terms\n\nAgentic Racing's goal, in words, was something like this:\n\n> Get around the circuit fast, without leaving the track, without crashing, and without stalling.\n\nThe final reward translated that into nine components (the full table is in\n[section 6 of part 1](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/)), plus the episode end\nconditions. Grouped, without claiming an exact mapping, by the part of the goal they try to represent:\n\n| Part of the goal | Reward terms |\n|---|---|\n| get around | progress per metre, completed-lap bonus |\n| fast | target speed per second, extra bonus for a fast lap |\n| without leaving the track | closeness to the edge, off track |\n| without crashing | wall hit |\n| without stalling | slowness, stopped, no-progress cut |\n| in the right direction | wrong-way cut |\n| drive well | follow the racing line |\n\nEach row is a design decision, and each weight is a statement about priorities: \"a metre of progress is\nworth 0.02\", \"leaving the track is worth −1\". None of those numbers comes from physics or from the goal;\nwe chose them. The question is what they say when you look at them together.\n\n---\n\n## 3. Scales: what each term says over a full lap\n\nThe first useful exercise is to add things up. Take the project's fixed circuit (about 2 km) and a lap\nof about 95 seconds (the devlog's estimate of that pace; the heuristic pilot measured 100 s with random\ndirectives). With the constants in the code, this is the most each\nterm could pay over that lap:\n\n![What one lap of the fixed circuit could pay, term by term: progress about +40, target speed up to about +24, lap bonus a flat +12 plus a fast-lap bonus of only 1.7 at 95 seconds, racing line under +2, leaving the track −1, and each wall hit −0.1. These are bounds computed from the code's constants, not measurements.](/images/reward-lap-budget.svg)\n\nA warning: these are **bounds** computed from the constants, not measurements. No RL policy ever drove\nthis lap; the fixed circuit came after the last training run.\n\nEven so, the sum says things the weight table doesn't:\n\n- **Progress dominates.** About 40 points per lap, almost as much as everything else combined.\n- **\"Fast\" is barely in the reward.** The fast-lap bonus scales with how much of the episode's time limit\n  (120 s) is left. A 95 s lap earns about +1.7; a 79 s lap, about +2.7. Sixteen seconds of difference are\n  worth one point, on a lap that can pay up to about 80.\n- **The racing line is almost decorative.** Under 2 points per lap, whatever the lap time: since it's\n  multiplied by the car's speed, in practice it pays per metre.\n- **Leaving the track costs the same as 50 metres of progress.** Seen that way, −1 is cheap. But its real\n  cost isn't the −1: it's everything the agent stops collecting over the rest of the lap. We'll come back\n  to that in section 6.\n\nThe project learned this exercise the hard way too. After the second run, the devlog notes that the\nnewly added speed and racing-line terms were, per step, 10 to 30 times smaller than the progress term\nand about 1000 times smaller than the off-track penalty. They were in the reward, but they didn't weigh\nanything.\n\nThe ML-Agents documentation gives a concrete scale rule: the reward between two decisions should be in\nthe range $[-1, 1]$, because larger values can destabilize training. The off-track penalty respects it.\nThe lap bonus, 12 to 20 in a single decision, doesn't. In practice it barely mattered, for a worse\nreason: the agent almost never got to collect it.\n\n---\n\n## 4. Sparse and dense rewards, with numbers\n\nA **sparse** reward pays only when the goal happens: for example, +1 for completing the lap and nothing\nelse. A **dense** one pays small amounts along the way, like progress per metre.\n\nThe sparse one is closer to the goal. The problem is whether the agent can see it. With\n$\\gamma = 0.995$ and a lap of about 950 decisions, a prize at the end of the lap, seen from the start,\nweighs:\n\n$$\n0.995^{950} \\approx 0.009\n$$\n\nless than 1% of its value. And that's assuming the agent ever reaches the finish, which is exactly what\nwasn't happening: from the fourth to the eighth run, only 2% to 4% of episodes ended with a completed\nlap. The devlog for the second run sums it up in two words: the lap bonus was \"dead weight\".\n\nThat's why the project's reward is dense. Most likely, progress per metre is what taught the agent the\nfirst 10% of the lap: start moving, go forward, don't leave the track on the first straight.\n\nBut every dense term is a new incentive, and the useful question isn't \"sparse or dense?\", but:\n\n> Does the intermediate signal help the agent discover the final goal, or does it teach it a different\n> goal?\n\n---\n\n## 5. Units: per metre, per second, per event\n\nThe reward's terms aren't measured in the same units, and that changes what they mean.\n\n- **Per metre** (progress): pays the same for a metre whether it's covered in one second or in ten. It\n  doesn't depend on how long the episode lasts.\n- **Per second** (target speed, edge, slowness, stopped): pays more the longer the situation lasts. A\n  positive per-second term is, at bottom, a reward for staying alive.\n- **Per event** (lap, off track, hit): pays once, when something happens.\n\n(The racing line is an in-between case: it's computed per second, but multiplied by speed, so in\npractice it pays per metre.)\n\nA good consequence of the design: since the per-second terms are multiplied by the physics step's\nduration, they don't change if the simulation frequency changes. Not everything is like that: the\nepisode limit, the decision cadence (and with it $\\gamma$), and the fast-lap bonus are counted in\nsteps. A less visible one: the\nfast-lap bonus depends on the episode's step limit, `MaxStep`. When the project moved to the fixed\ncircuit, that limit went from 4000 to 6000 steps (from 80 to 120 seconds), because 80 seconds wasn't\nenough to close a lap. With that change, the same lap time became worth a different bonus, without\ntouching a single reward constant.\n\nThe most interesting interaction is between progress (per metre) and target speed (per second). The\nspeed term peaks exactly at the target speed, which the code computes from the upcoming corner: 42% of\ntop speed on a straight (23.1 m/s) and less in corners. You'd think the total reward also peaks there.\nNot necessarily.\n\nPer second, progress pays $0.02\\,v$ and the speed term loses $0.25 \\times 1.3 / v_{\\text{target}}$ for\nevery m/s above the target. The two balance out when $v_{\\text{target}} = 0.25 \\times 1.3 / 0.02 = 16.25$\nm/s (a bit less if you add the racing line):\n\n- **With a target below 16.25 m/s**, there's a local peak at the target: braking down to it pays more\n  than overshooting a little. But it's only local: at much higher speeds the speed term reaches zero and\n  progress alone beats it. With a 10 m/s target, for example, going above about 22.5 m/s would pay more\n  per second than braking.\n- **With a target above it**, there's no peak: the reward keeps rising past the target.\n\nAnd here's the finding. A 10 m/s target corresponds to tight corners like those on the procedural\ntracks. On the fixed circuit, with 120 m radius corners, the target speed never drops below about\n**18.1 m/s**, above the threshold. In other words, on the circuit where the project ended up, **the\nspeed term never tells the agent to brake**: per second, more speed always pays more, on straights and\nin corners (in the corners, only barely).\n\n![Reward per second from progress plus target speed, as a function of the car's speed, for three targets. With 10 m/s, a tight corner on the procedural tracks, there's a local peak at the target, but above about 22.5 m/s progress alone pays more. With 18.1 m/s, the fixed circuit's corners, and 23.1 m/s, the straights, the curve keeps rising past the target.](/images/reward-speed-curve.svg)\n\nSo what would make braking right in a corner isn't the speed term. It's the chance of leaving the\ntrack, which ends the episode and cuts off everything that would have come after. If the car can take\nthe corner faster without leaving the track, the reward prefers it that way. That doesn't make the\nspeed term useless (in tight corners it does create a zone where braking pays), but it shows that the\nfinal behavior comes from the combination, not from reading each term on its own.\n\n---\n\n## 6. Ending the episode is also a reward\n\nThis is the most underestimated point, and in Agentic Racing it was the origin of the most famous\nshortcut.\n\nWhen an episode ends, the agent doesn't just receive that moment's penalty. **It loses everything it\nwould have collected afterwards.** So the agent's real decision isn't \"is the −1 worth it?\", but \"is the\n−1 worth more than the future?\". And that depends on the sign of what the future pays:\n\n- If the expected per-step reward is **negative**, ending early is a reward.\n- If it's **positive**, surviving is a reward.\n\nBoth sides have shown up in this series:\n\n**The negative side: the fourth run.** The reward had a small flat per-step penalty so the agent would\nfinish quickly, and stopping ended the episode with −1. The agent learned to use the rolling start, cover\nabout 245 metres collecting progress, stop, and accept the −1:\n\n![The fourth run's shortcut: the car covers about 245 metres, stops on purpose, and the episode ends with a return of +4.9 from progress, −0.08 from the time penalty, and −1 for stopping, about +3.8, exactly where the training curve plateaued.](/images/hero-reward-dark.svg)\n\nThe curious part is that the per-step penalty is exactly what the ML-Agents documentation recommends to\nget an agent to finish a task quickly, with one condition: completing the task should coincide with the\nend of the episode. Here *failing* also ended the episode, and failing cost less than continuing to\ntry. The generic recipe, applied to an environment where failure also ends the episode, rewarded\nfailing fast.\n\n**The positive side: GAIL.** In\n[part 4](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/) we saw that GAIL's\nreward is never negative, so it stretches episodes out. The same goes for any positive per-second term,\nlike the target speed from the previous section.\n\n![Ending the episode is also a reward: with a negative per-step reward, ending early pays (the fourth run); with a positive per-step reward, surviving pays (GAIL's bias from part 4). In the middle, the project's episode end conditions and what each one pays.](/images/reward-termination.svg)\n\nThat's why the episode end conditions are part of the reward design, as much as the weights. The\nproject's, after several versions:\n\n| Episode end | Condition | Reward |\n|---|---|---|\n| off track | 2 m past the edge | −1 |\n| stopped | 8 s stopped, after having started | −1 (plus −0.3 per second while stopped) |\n| wrong way | 4.5 s reversing faster than 7.5 m/s, only after 15 m of progress | −1 |\n| no progress | under 8 m of track in 5 s | −1 |\n| completed lap | 99% of the course | +12 to +20 |\n| time limit | `MaxStep` | 0 |\n\nAlmost every row is a scar. The fourth run's fix was that stopping no longer ended the episode and\ninstead cost something every second, with a hard cut only at 8 seconds. Wrong way became more tolerant\nbecause it ended the episode on the only maneuver that gets a car off a wall: a short reverse. And the\nno-progress cut arrived when the heuristic pilot's evaluation showed cars in a \"sawtooth\" near the\nstart: they hit, backed up a little, hit again, and since every reverse reset the \"stopped\" timer, they\nsurvived 80 seconds without moving forward.\n\n---\n\n## 7. Reward hacking: what you can check in the code\n\n*Reward hacking* is when the agent gets a lot of reward through a path that isn't the one we wanted. The\npractical rule for hunting it before training is one question:\n\n> What other behaviors could produce this same reward?\n\nWith the code in hand, several of the classic suspicions can be answered without training anything:\n\n| Suspicion | Is it possible in the code? |\n|---|---|\n| Go back and forth to collect progress twice | No: progress is signed, going back subtracts what going forward added |\n| Collect a fake jump when crossing the finish line | No: the progress calculation corrects for the circuit's wraparound |\n| Cut corners off the track | No: 2 m beyond the edge the episode ends |\n| Repeat a collision event | Yes, but it's a penalty: repeating it only subtracts |\n| Sit still and aligned to collect the racing-line term | No: that term only pays if the car is moving, in proportion to its speed |\n| Drive in circles collecting the speed term | **Partly**: that term uses the car's speed toward its own front, not along the track, so a car driving in circles collects it without moving forward. At the target speed a circle doesn't fit between the walls, but at low speed it does, for a small gain. The no-progress cut ends it within 5 to 10 seconds with −1 |\n\nThe last row is the interesting one. There's no evidence the policy ever did it (the circles in the\nmiddle of the track showed up in the heuristic pilot, not in RL), but the hole exists in the definition,\nand what plugs it is an end condition, not the reward. It's the kind of thing worth knowing before a\ntraining run discovers it.\n\nAnd then there are the shortcuts that aren't shortcuts but **conflicts between terms**, which the\nproject did live through:\n\n- **The racing line against the walls.** Rewarding following the racing line seemed reasonable, but on\n  this circuit the racing line hugs the edge in every corner. The term pushed the car into the wall. The\n  fix was to lower its weight and add a soft penalty for getting close to the edge.\n- **Slowness against braking.** The fifth version penalized going slow to keep the agent from crawling.\n  But braking before a corner is going slower, so the penalty landed right on the maneuver the agent\n  needed to learn. The seventh version replaced it with a target speed based on the corner,\n  with a slowness penalty now measured against that target.\n\n---\n\n## 8. Reward shaping: helping without changing the goal\n\n*Reward shaping* means adding intermediate signals to make learning easier. The risk is that they\nchange what the agent considers optimal. There is a form of shaping that, under certain conditions, is\nproven not to change it (Ng, Harada, and Russell, 1999). It's based on a potential function $\\Phi(s)$ of\nthe state:\n\n$$\nF(s, s') = \\gamma\\,\\Phi(s') - \\Phi(s)\n$$\n\nwhich is added to the original reward:\n\n$$\nr'(s, a, s') = r(s, a, s') + F(s, s')\n$$\n\nThe intuition: whatever the agent gains by getting closer to the goal it gives back if it moves away,\nso there's no way to pile up shaping by going around in circles.\n\nHow close is Agentic Racing's reward to this?\n\n- **Progress is very similar.** It's the difference of a potential, $\\Phi = 0.02 \\times$ (metres of the\n  lap covered), between one step and the next. But it lacks the $\\gamma$ factor, and the potential\n  doesn't go back to zero when the episode ends; both are conditions of the theorem. Besides, if you take\n  the completed lap and the terminations as the \"true\" reward, progress isn't a small adjustment on top\n  of it: it's most of what pays. The guarantee doesn't apply as is.\n- **Target speed, racing line, and edge are not.** They aren't differences of a potential: they pay\n  according to *how* the car moves, not just *where* it is. Those terms do change what's optimal, and\n  on purpose: they tell the agent how fast to go and where.\n\nThat last point has an interesting reading in light of parts 3 to 5. The reward's target speed is a\nvariant of the heuristic pilot's braking rule: the code comment says it \"mirrors the heuristic\", and\nboth compute a speed from the upcoming corner, with similar constants. In other words, **the reward\nalready contained a form of imitation**, written by hand instead of learned from demonstrations. The\nML-Agents documentation advises the opposite: reward results, not the actions you think will lead to\nthem. The project chose to reward an action (a speed) because the result (completing the lap) wasn't\nshowing up.\n\n---\n\n## 9. The reward can't be the only metric\n\nIf we evaluate the agent with the same signal we trained it on, we inherit its blind spots. The project\nlearned this through three concrete episodes:\n\n**A rising curve wasn't an improvement.** In the third run, the cumulative reward rose to about 10,\ntwo to three times the previous runs' plateau, and episodes got five times longer. It looked like the hoped-for\nbreakthrough. The fourth run, with more steps and the same configuration, fell back into the usual pit.\nThe devlog closed it like this: the third run had been \"favorable variance from one run\". A single run\nisn't evidence.\n\n**A stable number wasn't a good number.** The fourth run's plateau, around +3.8, matched exactly the\nreturn of section 6's shortcut. The curve wasn't saying \"it learned little\": it was saying \"it learned\nperfectly to do something else\".\n\n**Reward curves from different versions can't be compared.** The fifth run started at −34; the first,\nat −1.1. Not because the fifth was worse, but because the reward had changed. Each version has its own\nscale, and comparing their curves means comparing different units.\n\nThat's why, after the fourth run, the project built an independent evaluator (`eval.exe`) that doesn't\nlook at the reward. It runs the policies and reports other things:\n\n| Evaluator metric | What it revealed |\n|---|---|\n| episode end reasons | the fourth run ended \"stopped\" 86% of the time |\n| lap fraction covered | every run stayed around 10% |\n| mean brake | **0.02**: the policy never braked, and without braking you can't take a corner |\n| mean speed, throttle, and steering | two opposite styles with the same lap fraction: crawling at 7 m/s, or speeding up and ending stopped or off the track |\n\nThe mean brake of 0.02 was the most useful number of the whole phase, and it wasn't on any reward curve.\n\n---\n\n## 10. What no reward could fix\n\nThere's one last lesson, and it's the most expensive one. The usual intuition, and the one the\nproject's plan also held, is that when an agent learns something weird, the reward is almost always to\nblame. In Agentic Racing the devlog counts seven variants of the reward. The number that mattered, the\nlap fraction, didn't budge from between 8 and 13%.\n\nMeanwhile, the devlog kept naming \"root causes\", one after another, that weren't the reward: the walls\n(zero-thickness meshes the car got stuck on), lateral grip (which erased lateral velocity instead of\nredirecting it, so every hard turn was a sudden stop), and the 12 m radius corners on the procedural\ntracks. Some were ruled out later: three different versions of the walls gave the same result, and the\ndevlog concludes \"the walls weren't the cause\". Others were fixed before the eighth run, which still\nplateaued in the same place.\n\nWhat unblocked the problem was something else: replacing the procedural tracks with the fixed circuit.\nThere the heuristic pilot, for the first time, drove clean laps, and the devlog attributes the eight\nruns' \"paralysis\" to the procedural tracks (degenerate centreline segments, malformed walls in corners,\ncorners nobody could take), not to a deep bug in the car's physics. Whether PPO learns to complete laps\non the fixed circuit was never tested.\n\nThe experiment that could separate \"reward problem\" from \"environment problem\" was run halfway through,\nafter the fifth run: putting the hand-written heuristic pilot in the same environment. But it was read\nin the environment's favor: its best episode reaching 82% of a lap was taken as proof that the\nenvironment was learnable, and after the seventh run the devlog went as far as writing \"the reward is\nalready correct\". The full answer only came with the fixed circuit, when the heuristic pilot went from\none good isolated episode to complete laps. The lesson isn't just to run that experiment: it's to\ndemand a clear answer from it. If a hand-written controller can't complete the lap consistently, no\nreward is going to teach a neural network to do it.\n\nThe ML-Agents documentation suggests something similar with a different intent: use the agent's\nheuristic to drive it and watch how it accumulates reward. Applied here, that would have served two\npurposes at once: knowing whether the environment was drivable, and knowing whether the reward ranked\nthe expert above the policies that failed. The project did the first; the devlog doesn't record the\nsecond ever being measured.\n\n---\n\n## 11. Part 5's debt: a reward that knows nothing about directives\n\nIn part 5 we saw that the pilot exists to follow the strategist, and that the heuristic pilot does: on\nthe same track, it takes 112 s per lap with \"conserve\" and low aggression, and 79 s with \"attack\" and high\naggression.\nThe reward, on the other hand, computes its target speed from the corner alone. No term reads the\ndirective channels.\n\nThe heuristic pilot does scale its target speed by the directive, through a single map\n(`StrategyDirectiveMap`) that the strategist also uses. The most direct proposal would be for the reward\nto use that same map:\n\n$$\nv_{\\text{target}}^{\\text{directive}} = v_{\\text{target}}(\\text{corner}) \\times \\text{scale}(\\text{directive})\n$$\n\n(In that map, the speed scale comes from aggression, between 0.86 and 1.16, with 10% less when the\ndirective is \"conserve\"; the \"attack\" type mostly changes the racing line.) With that, the reward would\ntell the agent \"with high aggression, the right speed is higher\", and a policy\noptimizing it would have a reason to respond to the directive.\n\nIt's a **proposal**, not something tested, and it comes with its own questions: whether the scale should\nalso apply to braking margin and racing line, as the heuristic does; whether the agent would learn to\ntell apart directives that differ only in speed in the reward; and whether it would hurt training\nstability. But it illustrates this article's point: part 5's finding isn't fixed with an algorithm, it's\nfixed by saying more precisely what we want.\n\n---\n\n## 12. A reproducible experiment\n\nIf the RL pilot is picked up again on the fixed circuit, the natural experiment compares versions of the\nreward, changing **one thing at a time**:\n\n| Variant | Reward | Question |\n|---|---|---|\n| A. Sparse | completed lap and terminations only | can it learn without a dense signal on this circuit? |\n| B. Current | the eighth run's | the baseline: never trained on the fixed circuit |\n| C. No target speed | B without that term | how much does section 8's hand-written imitation contribute? |\n| D. With directives | B with the target speed scaled by the directive | does directive response appear? |\n\nComparisons: A vs. B, B vs. C, and B vs. D. For them to mean anything:\n\n- **Same build, same circuit, same `MaxStep`.** The fast-lap bonus depends on it (section 5).\n- **Rebuild the player for every variant.** The reward's weights come from the code's default values,\n  not from a scene; changing them requires rebuilding the training executable.\n- **Log every reward component separately.** Today the project doesn't log them separately:\n  TensorBoard only sees the total reward. ML-Agents can\n  publish custom metrics to TensorBoard from the agent's code; without that, there's no way to know which\n  term is moving the curve.\n- **Several seeds per variant**, with mean and spread. The third run already showed what a single one is\n  worth.\n- **Evaluate with `eval.exe`, not with the reward.** End reasons, lap fraction, mean brake and, for D, lap\n  time under each forced directive.\n- **Also measure the expert under each reward.** If the heuristic pilot doesn't earn more reward than a\n  failing policy, the reward is measuring something else.\n\n| Metric | A | B | C | D |\n|---|:---:|:---:|:---:|:---:|\n| Completed laps | ? | ? | ? | ? |\n| Lap fraction | ? | ? | ? | ? |\n| Mean brake | ? | ? | ? | ? |\n| Off-track exits | ? | ? | ? | ? |\n| Attack / conserve gap | ? | ? | ? | ? |\n| The expert's reward under that function | ? | ? | ? | ? |\n\nNone of this was run. The cells stay at \"?\".\n\n---\n\n## 13. A workflow for designing rewards\n\nPutting it all together, the loop the project would have liked to follow from the start:\n\n```text\n0. Check that the environment can be solved with a simple controller\n            ↓\n1. Write the goal down in words\n            ↓\n2. Translate it into measurable events and terminations\n            ↓\n3. Start with a minimal reward\n            ↓\n4. Add up each term over a lap: scales, units, signs\n            ↓\n5. Train, logging each component separately\n            ↓\n6. Evaluate with metrics that aren't the reward, across several seeds\n            ↓\n7. Look at concrete episodes, not just curves\n            ↓\n8. Change one thing, and go back to step 4\n```\n\nStep 0 doesn't usually show up in reward engineering guides. In Agentic Racing it's the one that would\nhave saved the most time.\n\n---\n\n## Conclusion\n\nReward engineering isn't handing out positive and negative points. It's translating a goal we\nunderstand into a signal the agent can optimize, and then checking that optimizing that signal produces\nwhat we wanted.\n\nAgentic Racing's reward leaves several concrete lessons:\n\n- weights say nothing until you add them up over a full episode;\n- units (per metre, per second, per event) change what each term rewards;\n- ending the episode is a reward, and its sign depends on what the future pays;\n- terms can fight each other, like the racing line against the walls or slowness against braking;\n- a reward can contain hand-written imitation without anyone calling it that;\n- the reward can't be the only metric;\n- and no reward fixes an environment where the problem can't be solved.\n\nThe question worth asking before every training run is still the same:\n\n> What exactly are we rewarding, and what else could the agent learn to do to get that reward?\n\nIf you landed directly on this article, the five earlier parts are linked at the top, and\n[Agentic Racing: A Pilot That Never Learned to Drive](/blog/agentic-racing-a-pilot-that-never-learned-to-drive/)\ntells the project's full story.\n\n---\n\n## References\n\n- Sutton, R. S. and Barto, A. G. (2018). [Reinforcement Learning: An Introduction](http://incompleteideas.net/book/the-book-2nd.html), 2nd edition. MIT Press.\n- Ng, A. Y., Harada, D., and Russell, S. (1999). [Policy Invariance Under Reward Transformations: Theory and Application to Reward Shaping](https://people.eecs.berkeley.edu/~russell/papers/icml99-shaping.pdf). *ICML*.\n- Amodei, D. et al. (2016). [Concrete Problems in AI Safety](https://arxiv.org/abs/1606.06565). (Reward hacking as a design problem.)\n- Schulman, J. et al. (2017). [Proximal Policy Optimization Algorithms](https://arxiv.org/abs/1707.06347).\n- Ho, J. and Ermon, S. (2016). [Generative Adversarial Imitation Learning](https://arxiv.org/abs/1606.03476). *NeurIPS*.\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), `mlagents`\n  1.1.0 release: the \"Rewards\" section of `docs/Learning-Environment-Design-Agents.md` (reward range per\n  decision, per-step penalty, using the heuristic to inspect the reward).\n- The reward (`unity/Assets/Scripts/Agents/RaceAgent.cs`), the evaluator, and the devlog with all eight\n  runs are at [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system","lang":"en","title":"Can We Combine Imitation and RL? BC, GAIL, and PPO as One System","description":"Part 5 of the Reinforcement Learning from scratch series with Agentic Racing. How to chain Behavioral Cloning, GAIL, and PPO: what ML-Agents actually does when you combine them, how a two-run fine-tuning would work, why in this project the task reward could erase exactly what the demo needs, and how to design an experiment that can show the combination doesn't work.","publishDate":"2026-10-09","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema","url":"https://alexisalulema.com/blog/can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system/","series":{"id":"rl-from-scratch","position":5},"headings":[{"depth":2,"slug":"where-we-left-off","text":"Where we left off"},{"depth":3,"slug":"the-usual-warning-bigger-this-time","text":"The usual warning, bigger this time"},{"depth":2,"slug":"1-why-combine-the-agentic-racing-case","text":"1. Why combine: the Agentic Racing case"},{"depth":2,"slug":"2-the-intuitive-chain-bc--gail--ppo","text":"2. The intuitive chain: BC → GAIL → PPO"},{"depth":2,"slug":"3-what-ml-agents-actually-does-everything-at-once","text":"3. What ML-Agents actually does: everything at once"},{"depth":2,"slug":"4-the-sequential-version-two-runs","text":"4. The sequential version: two runs"},{"depth":2,"slug":"5-two-architectures-worth-comparing","text":"5. Two architectures worth comparing"},{"depth":2,"slug":"6-what-does-it-mean-for-a-policy-to-be-better-in-this-project-its-not-just-lap-time","text":"6. What does it mean for a policy to be “better”? In this project, it’s not just lap time"},{"depth":2,"slug":"7-the-weight-of-each-signal","text":"7. The weight of each signal"},{"depth":2,"slug":"8-designing-a-reproducible-experiment","text":"8. Designing a reproducible experiment"},{"depth":2,"slug":"9-how-to-read-the-results-once-they-exist","text":"9. How to read the results (once they exist)"},{"depth":2,"slug":"10-what-if-the-expert-isnt-perfect","text":"10. What if the expert isn’t perfect?"},{"depth":2,"slug":"11-what-if-a-single-piece-is-already-enough","text":"11. What if a single piece is already enough?"},{"depth":2,"slug":"12-implemented-executed-evaluated","text":"12. Implemented, executed, evaluated"},{"depth":2,"slug":"conclusion","text":"Conclusion"},{"depth":2,"slug":"references","text":"References"}],"body":"> **Series: Reinforcement Learning from scratch, with Agentic Racing as the lab**\n>\n> 1. Part 1 · [How Does an Agent Learn? Teaching a Car to Drive](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/)\n> 2. Part 2 · [How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/)\n> 3. Part 3 · [Can We Teach by Imitation? Behavioral Cloning Explained from Scratch](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/)\n> 4. Part 4 · [Can We Learn to Behave Like an Expert? GAIL Explained from Scratch](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/)\n> 5. Part 5 · **Can We Combine Imitation and RL? BC, GAIL, and PPO as One System** (this article)\n> 6. Part 6 · [How Do We Tell an Agent What We Want? Reward Engineering Explained from Scratch](/blog/how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch/)\n> 7. Part 7 · [What Should an Agent Be Able to See? Observation Design Explained from Scratch](/blog/what-should-an-agent-be-able-to-see-observation-design-explained-from-scratch/)\n> 8. Part 8 · [What Do We Do When the Agent Doesn't Learn? RL Debugging Explained from Scratch](/blog/what-do-we-do-when-the-agent-doesnt-learn-rl-debugging-explained-from-scratch/)\n\n## Where we left off\n\nWe have asked four questions so far:\n\n| Part | Question | Short answer |\n|---|---|---|\n| [1](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/) | Can an agent learn by trial and error? | yes, from a reward; and it will find any shortcut that reward leaves open |\n| [2](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/) | How do we improve the policy without breaking it? | PPO: clipped updates that stay close to the previous policy |\n| [3](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/) | Can we teach it by showing what an expert does? | BC: supervised learning on $(s, a)$ pairs; fragile outside the demonstrations |\n| [4](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/) | Can we make it *behave* like the expert? | GAIL: a discriminator turns imitation into a learned reward |\n\nPart 4 ended with the next one:\n\n> Can we combine all of these ideas?\n\nThe most concrete version of that question is this: **can we use an expert's demonstrations to start\nfrom a useful policy, and then let reinforcement learning improve it?**\n\n### The usual warning, bigger this time\n\nNo combination was ever run in Agentic Racing. BC and GAIL were configured for the eighth run and\nremoved before it launched; no demonstration was ever recorded; there is no policy trained with\nimitation and no later fine-tuning. So this article doesn't report a pipeline: **it designs the\nexperiment the project never got to run**, using what the repository does have (the expert, the\nrecorder, the evaluator, the configuration that was written) and what the ML-Agents code says about\nhow the pieces would combine.\n\nAs in the earlier parts, every results table is a template full of \"?\", not a result.\n\n---\n\n## 1. Why combine: the Agentic Racing case\n\nThe motivation isn't abstract. In parts 1 and 2 we saw what happened with PPO alone: eight runs on\nprocedural tracks, and the policy stayed around 10% of a lap (between 8 and 13%). In the eighth, the most\ncareful one, 91% of episodes ended with the car off the track and only 4% with a completed lap.\n\nMeanwhile the expert, the heuristic pilot from part 3, does drive. When the project moved to a fixed\ncircuit (a rounded rectangle about 2 km long), the expert drove it without leaving the track or\ngetting stuck, and once the directives were wired into its driving, every evaluation car completed full\nlaps (always from clean starts, which is how the evaluator runs the expert).\n\nThat asymmetry is exactly the case that justifies combining:\n\n```text\nPPO alone: learns from the reward, but can't find how to complete a lap\nExpert:    completes the lap, but learns nothing\n```\n\nThe hypothesis is that the demonstrations spare PPO the hardest part of exploration (getting to the\npoint of driving a lap) and that the reward then does the rest. It's a reasonable hypothesis. But\nthere's also a competing explanation nobody ruled out: the fixed circuit made the problem much\neasier, and **PPO alone was never tried on it**. It may not need help anymore. We'll come back to that.\n\n---\n\n## 2. The intuitive chain: BC → GAIL → PPO\n\nThe natural way to picture the combination is a chain of stages:\n\n```text\nexpert → demonstrations → BC → initial policy → GAIL → PPO fine-tuning → evaluation\n```\n\nEach stage would answer a different question:\n\n```text\nBC:   \"start by copying what the expert does\"\nGAIL: \"refine the behavior so it resembles the expert's\"\nPPO:  \"now optimize the task reward\"\n```\n\nIt's a good way to organize the reasoning. But, as we saw at the end of part 4, **it's not what\nML-Agents does when you put all three in one configuration**. And the difference matters when\ndesigning the experiment.\n\n---\n\n## 3. What ML-Agents actually does: everything at once\n\nIn ML-Agents 1.1.0 there are no stages. There is a single PPO run with two extra sources of knowledge\nplugged into it:\n\n- **BC** is a supervised loss applied after each PPO update, with a learning rate that fades out over\n  `steps` (part 3).\n- **GAIL** is one more reward signal, with its own head in the critic, that lasts the whole run\n  (part 4).\n- **PPO** is the only RL algorithm: the one that optimizes the reward.\n\nCareful with one detail: BC isn't just \"information\" for PPO. It has its own optimizer and takes its own\ngradient steps on the policy's weights, after each PPO update. So while BC is active, two optimizers pull\non the same network: PPO toward more reward, BC toward the expert's actions. GAIL, on the other hand,\ndoesn't touch the policy: it only changes the reward PPO sees.\n\nThe configuration prepared for the eighth run was exactly that: environment reward at weight 1.0,\nGAIL at 0.15, and BC at 0.5 for the first 2 million steps. It's not a chain: it's a mix whose balance\nshifts over time, because BC fades out and GAIL doesn't.\n\nA note on BC. The ML-Agents documentation says BC \"can be used on its own or as a pre-training step\",\nbut in the 1.1.0 code BC is always a module running inside an RL\ntrainer (PPO, SAC, or POCA): there is no trainer that does\nonly BC, without interacting with the environment. A \"pure\" BC like the one in part 3 would have to be\nwritten separately (ML-Agents ships a reader for `.demo` files that would be a starting point).\n\n---\n\n## 4. The sequential version: two runs\n\nIf we want the real chain, with an imitation stage and then a fine-tuning stage, ML-Agents allows it\nwith a command-line option:\n\n```text\nmlagents-learn imitation.yaml   --run-id=imit\nmlagents-learn finetuning.yaml  --run-id=ft --initialize-from=imit\n```\n\nThe second run starts from the first one's checkpoint. Reading the code that does that loading, there\nare three details worth knowing before interpreting anything:\n\n1. **It loads more than the policy.** It loads every registered module: the policy, the critic, PPO's\n   optimizer, and the weights of each reward provider, including the GAIL discriminator if the second\n   run uses it too. (It doesn't save or load BC's own optimizer or the discriminator's: those start\n   over.)\n2. **It tolerates differences.** Loading isn't strict: if the second configuration no longer has GAIL,\n   the weights of its critic head are ignored; whatever is missing keeps its fresh initialization; and if a\n   piece doesn't fit, a warning goes to the log and it's loaded as far as possible or reset. For example,\n   removing GAIL changes the critic's shape, and PPO's optimizer can't load its previous state: it starts\n   over. That's convenient, but it also means a\n   configuration mistake won't stop the run: you have to read the log.\n3. **The step counter goes back to zero.** And with it, every linear schedule from part 2: they go\n   back to the initial values in the second run's configuration and decay over its own `max_steps`. If\n   the project's configuration is copied, that means a learning rate of $3 \\times 10^{-4}$, $\\beta$ of\n   $10^{-2}$, and $\\varepsilon$ of 0.2. For fine-tuning that's exactly the opposite of what we want: after\n   learning to imitate, the second run starts with a learning rate and entropy pressure as high as at the\n   start of the first one. Unless they're\n   lowered by hand in the second configuration, fine-tuning may start by undoing what imitation built.\n\n![Two ways to combine BC, GAIL, and PPO in ML-Agents. Top, the simultaneous one: a single run where the demonstrations feed the BC loss and the GAIL discriminator, PPO optimizes the reward and BC also adjusts the policy with its own optimizer. Bottom, the sequential one: an imitation run saves a checkpoint, and a second run starts from it with initialize-from and trains only on the environment reward. A note recalls that the step counter goes back to zero and the schedules start over.](/images/combine-pipelines.svg)\n\n---\n\n## 5. Two architectures worth comparing\n\nWith that, the two concrete options are:\n\n**Architecture A — simultaneous, one run**\n\n```text\ndemonstrations ─┬─► BC (fades out) ────────┐\n                └─► GAIL (reward) ─────────┤\nenvironment reward ────────────────────────┴─► PPO ─► policy\n```\n\nThis is the one prepared for Agentic Racing. Upside: a single run, and the environment reward is\npresent from the start, so imitation is never optimized on its own. Risk: the balance between signals\nis set by each one's scale, not just by the weight written in the configuration (section 7).\n\n**Architecture B — sequential, two runs**\n\n```text\nrun 1: PPO + BC + GAIL  ──checkpoint──►  run 2: PPO + environment reward only\n```\n\nUpside: it cleanly separates what each stage contributes, because the intermediate checkpoint can be\nevaluated. Risk: once imitation is removed, nothing stops the policy from drifting away from the\nexpert. That can be good (finding something better) or bad (forgetting useful skills). And as we saw in\npart 1, PPO alone with this reward already found an easy way out once: in the fourth run it learned to\nstop on purpose.\n\nNeither is better in advance. They have to be tested.\n\n---\n\n## 6. What does it mean for a policy to be \"better\"? In this project, it's not just lap time\n\nBefore comparing, we have to decide what we measure. The obvious choice would be driving: completed\nlaps, lap time, off-track exits. But Agentic Racing has something more important, and it's what changes\nthe experiment's design the most.\n\nThe demo's pilot doesn't exist to be fast. It exists to **follow the strategist**. The 42-number\nobservation vector includes 6 directive channels (aggression, risk tolerance, and the directive type:\nattack, defend, conserve, push), and during training they are randomized every episode, precisely so\nthe policy learns to drive differently depending on what the strategist says.\n\nThe expert does that. Measured on the fixed circuit:\n\n| Forced directive | Lap time |\n|---|---:|\n| conserve, aggression 0.15 | 112 s |\n| random per episode | 100 s |\n| attack, aggression 0.85 | 79 s |\n\nThe slowest lap takes about 33 s longer than the fastest, roughly 40% more: that's the real lever the LLM strategist has over the\nrace.\n\nNow look at the task reward. Its terms are the ones from part 1: progress, target speed, racing line,\nedges, exits, completed lap. And the target speed it rewards depends on the track's curvature, **not on\nthe directive**. No term of the reward reads the directive channels.\n\nThat has an uncomfortable consequence:\n\n- A policy that **imitates** the expert has a reason to respond to the directive: the expert does, and\n  the directive is in its observations.\n- A policy that **only optimizes the reward** has none. To it, the 6 channels are noise. The most\n  profitable thing is to drive whichever way pays the most reward, whatever the directive.\n\nIn other words, in architecture B, fine-tuning on the task reward could improve lap time and, at the\nsame time, **erase exactly what the demo needs**: that the car drives differently when the strategist\nsays \"attack\" than when it says \"conserve\". Nobody measured it, so it's a question, not a finding. But\nit's the most important question in the experiment.\n\n![The expert's lap time on the fixed circuit by directive: 112 seconds conserving, 100 with a random directive, 79 attacking: the slowest lap takes roughly 40% longer. Next to it, two columns with empty bars and question marks: a policy that imitates the expert, which should keep that spread if it learned it, and a policy fine-tuned only on the task reward, which has nothing forcing it to keep it because the reward doesn't read the directive.](/images/combine-directives.svg)\n\nThat's why, in this project, evaluation needs three families of metrics, not two:\n\n**Driving**\n\n- lap fraction, completed laps, and lap time;\n- episode end reasons: off track, stalled car, no progress;\n- recovery from noisy starts.\n\n**Imitation**\n\n- similarity of trajectories and of the action distribution to the expert's, especially braking;\n- speed per section and behavior in the four corners.\n\n**Directive response**\n\n- lap time under each forced directive;\n- the gap between \"attack\" and \"conserve\", compared with the expert's.\n\nThe project's evaluator (`eval.exe`) already covers much of the first family: lap fraction, episode end\nreasons, and speeds, and it evaluates trained policies from noisy starts (the expert is always evaluated\nfrom clean ones). It only reports lap time directly in its population mode, so that would have to be\nadded. It can also force one directive on every car, so the third family comes almost for free. The\nsecond would have to be built.\n\n---\n\n## 7. The weight of each signal\n\nIn part 4 we did the math: with an undecided discriminator, GAIL at weight 0.15 pays about 0.10 per\ndecision, and the designed reward, driving well, about 0.07. The 0.15 looked small and wasn't.\n\nThe ML-Agents documentation points the same way. It recommends keeping the GAIL strength below about\n0.1 when the demonstrations are suboptimal and there's an environment reward, so the agent focuses on\nthe reward instead of copying; its reference example that combines all three (PushBlock) uses 0.01.\nAnd it warns about the survivor bias we saw in part 4: an always-positive per-step reward pushes the\nagent to stretch the episode out, and can drown the task reward. If the agent seems to ignore the\nenvironment reward, the advice is to lower the GAIL strength.\n\nSo the GAIL weight isn't a configuration detail: it's one of the experiment's variables. At minimum,\nyou'd test the planned 0.15 and a value on the order the documentation recommends.\n\n---\n\n## 8. Designing a reproducible experiment\n\nWith all of the above, part 4's experiment grows like this:\n\n| Variant | What it trains on | In ML-Agents |\n|---|---|---|\n| A. PPO from scratch | environment reward | `extrinsic`, one run |\n| B. Pure BC | demonstrations only | custom code: there's no BC trainer without the environment |\n| C. GAIL + environment | imitation as a reward | `gail` + `extrinsic`, one run |\n| D. Simultaneous | BC + GAIL + environment | the configuration prepared for the eighth run |\n| E. Sequential | D, then fine-tuning | a second run with `--initialize-from`, `extrinsic` only |\n\nAnd, for each one:\n\n| Metric | A | B | C | D | E |\n|---|:---:|:---:|:---:|:---:|:---:|\n| Completed laps | ? | ? | ? | ? | ? |\n| Lap time | ? | ? | ? | ? | ? |\n| Off-track exits | ? | ? | ? | ? | ? |\n| Similarity to the expert | ? | ? | ? | ? | ? |\n| Attack / conserve gap | ? | ? | ? | ? | ? |\n| Recovery from noisy starts | ? | ? | ? | ? | ? |\n\nFor that table to mean anything, each variant has to compete under comparable conditions:\n\n- **The same interaction budget.** E gets D's steps *plus* those of its second run; compare at equal\n  total steps, or at least report them. And demonstrations cost something too: the recorder runs 300 s\n  of simulation with the expert by default, in every arena at once, and those steps don't show up in PPO's step counter.\n- **Several seeds.** `mlagents-learn` accepts `--seed`; with a single run per variant there's no way to\n  tell an effect from chance. Report the mean and the spread, not the best run.\n- **The same environment and the same demonstrations.** The same build, the same physics, the same set\n  of `.demo` files for every variant that uses them.\n- **The same evaluation rules.** The same number of episodes, the same starts, the same rule for\n  picking the checkpoint (the last one, or the best by a metric fixed in advance, never the one that\n  \"looks best\").\n- **An honest generalization test.** All demonstrations would come from the fixed circuit, which is the default track. The procedural\n  track generator is still in the code; evaluating there, without having trained there, would show\n  whether the policy learned to drive or memorized one circuit.\n\nThe project already has a precedent for this kind of rigor. In the experiment pitting the LLM\nstrategist against the heuristic one, it ran series of 18 races with the grid rotated, reported the mean\nwith its standard deviation, and the first result went against expectations. It was published that\nway. The same discipline would apply here.\n\n---\n\n## 9. How to read the results (once they exist)\n\nWithout numbers, what we can write in advance is what each pattern would mean:\n\n- **If A already completes laps on the fixed circuit**, the original motivation for imitation goes\n  away. What's left to measure is whether imitation teaches something PPO doesn't: directive response.\n- **If B drives well from clean starts but fails from noisy ones**, that's part 3's compounding-errors\n  problem, exactly as expected.\n- **If C or D lengthen episodes but don't increase completed laps**, suspect GAIL's survivor bias before\n  celebrating.\n- **If E improves lap time but the attack/conserve gap shrinks**, it won on the wrong metric for this\n  demo.\n- **If the differences between variants are smaller than the spread across seeds**, the honest result\n  is \"no detectable effect\".\n\nA useful experiment isn't the one that proves the architecture works. It's the one designed so it\ncould reveal that it doesn't.\n\n---\n\n## 10. What if the expert isn't perfect?\n\nIt isn't. The expert brakes by a fixed rule, a target speed computed from the upcoming corner, and has\nrecovery behaviors that depend on timers the policy can't see (part 3). No imitation technique, on its\nown, is designed to beat it: BC learns its decisions, GAIL learns to resemble it.\n\nBeating it takes a signal that says \"this is better\", and that signal is the reward. That's the appeal\nof architecture B. But \"better according to the reward\" is only \"better\" if the reward measures what we\nwant, and section 6 showed that in this project it doesn't measure the most important thing. Before any\nfine-tuning, you'd have to decide whether the reward needs a term that rewards following the directive.\nThat's no longer about combining algorithms: it's redesigning the objective, which is part 1's problem.\n\n---\n\n## 11. What if a single piece is already enough?\n\nWe have to take that seriously. If BC produces a reliable policy, GAIL may add complexity without\nimproving anything. If D works, E may make it worse. Every stage has to justify its existence with\nevidence:\n\n> What concrete improvement does this stage bring, and how much does it cost to get it?\n\nAgentic Racing took that question further than any variant in the table. After the eighth run, the\ndecision was not to train any pilot at all: the heuristic expert, with the directives wired into its\npace, braking, and racing line, became the demo's pilot. The project's goal was to show the loop\nbetween pilot and strategist, and that didn't need a neural network: it needed a pilot whose behavior\nchanged with the directive, and the expert already did that.\n\nThat's not a conclusion about BC, GAIL, or PPO. It's an engineering conclusion: sometimes the simplest\npiece that meets the goal is the right one, and the more sophisticated pipeline stays an experiment for\nanother day.\n\n---\n\n## 12. Implemented, executed, evaluated\n\nIt's worth separating three levels of maturity, because they're easy to blur:\n\n- **Implemented:** the code exists.\n- **Executed:** it was run and there's evidence.\n- **Evaluated:** it was measured and compared against a baseline.\n\nApplied to Agentic Racing:\n\n| Piece | Implemented | Executed | Evaluated |\n|---|---|---|---|\n| Heuristic expert | yes | yes: it's the demo's pilot | yes: full laps on the fixed circuit and lap times per directive |\n| Demonstration recording (`eval.exe -record`) | yes | no record of it ever being used | — |\n| PPO | yes | yes: eight runs on procedural tracks | yes: around 10% of a lap (8–13%); on the fixed circuit, never |\n| BC | configured | no: removed before the eighth run | — |\n| GAIL | configured | no: removed together with BC | — |\n| Simultaneous BC + GAIL + PPO | configured | no | — |\n| Sequential fine-tuning | no | no | — |\n\nA configuration existing doesn't prove it works. A run having been executed doesn't prove it beats a\nbaseline either. This table is what a reader should be able to ask any RL project for before believing\none of its curves.\n\n---\n\n## Conclusion\n\nBC, GAIL, and PPO attack different parts of the problem:\n\n- **BC** uses demonstrations to learn the expert's actions directly.\n- **GAIL** turns the comparison with the expert into a reward.\n- **PPO** optimizes a policy with whatever reward it gets.\n\nThe `BC → GAIL → PPO` chain is a good way to think about them together, but in ML-Agents the natural\ncombination is simultaneous: a single PPO run, with BC pulling the policy toward the expert and GAIL\nadding its reward. The sequential version\nexists, through `--initialize-from`, and comes with its own traps: the schedules restart, and removing\nimitation leaves the policy at the mercy of the reward.\n\nAnd in this project that reward has a concrete blind spot: it knows nothing about the strategist's\ndirectives, which are the pilot's whole reason to exist. That's the kind of finding that only shows up\nwhen you design the experiment with the real system in hand, not a generic architecture in your head.\n\n> The interesting question isn't whether we can chain three algorithms, but whether each one\n> contributes something measurable that the others don't.\n\nAgentic Racing has almost everything it needs to answer it: the expert, the recorder, the evaluator,\nand the configuration. What it's missing is the most important part: running it.\n\nIf you landed directly on this article, the four earlier parts are linked at the top, and\n[Agentic Racing: A Pilot That Never Learned to Drive](/blog/agentic-racing-a-pilot-that-never-learned-to-drive/)\ntells the project's full story.\n\n---\n\n## References\n\n- Ho, J. and Ermon, S. (2016). [Generative Adversarial Imitation Learning](https://arxiv.org/abs/1606.03476). *NeurIPS*.\n- Schulman, J. et al. (2017). [Proximal Policy Optimization Algorithms](https://arxiv.org/abs/1707.06347).\n- Ross, S., Gordon, G., and Bagnell, J. A. (2011). [A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning](https://arxiv.org/abs/1011.0686). *AISTATS*.\n- Rajeswaran, A. et al. (2018). [Learning Complex Dexterous Manipulation with Deep Reinforcement Learning and Demonstrations](https://arxiv.org/abs/1709.10087). *RSS*. (A precedent for initializing with BC and fine-tuning with RL.)\n- Kostrikov, I. et al. (2019). [Discriminator-Actor-Critic: Addressing Sample Inefficiency and Reward Bias in Adversarial Imitation Learning](https://arxiv.org/abs/1809.02925). *ICLR*.\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), `mlagents`\n  1.1.0 release: the imitation learning section of `docs/ML-Agents-Overview.md`, the `gail` and\n  `behavioral_cloning` settings in `docs/Training-Configuration-File.md`, `--initialize-from` in\n  `docs/Training-ML-Agents.md`, and checkpoint loading in\n  `ml-agents/mlagents/trainers/model_saver/torch_model_saver.py`.\n- The project's training configuration, expert, evaluator, and devlog are at\n  [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema","lang":"es","title":"¿Podemos combinar imitación y RL? BC, GAIL y PPO en un mismo sistema","description":"Quinta parte de la serie Reinforcement Learning desde cero con Agentic Racing. Cómo encadenar Behavioral Cloning, GAIL y PPO: qué hace ML-Agents de verdad cuando los combinas, cómo se haría un fine-tuning en dos corridas, por qué en este proyecto la recompensa de la tarea podría borrar justo lo que el demo necesita, y cómo diseñar un experimento que pueda demostrar que la combinación no funciona.","publishDate":"2026-10-09","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system","url":"https://alexisalulema.com/es/blog/podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema/","series":{"id":"rl-from-scratch","position":5},"headings":[{"depth":2,"slug":"dónde-nos-quedamos","text":"Dónde nos quedamos"},{"depth":3,"slug":"la-advertencia-de-siempre-esta-vez-más-grande","text":"La advertencia de siempre, esta vez más grande"},{"depth":2,"slug":"1-por-qué-combinar-el-caso-de-agentic-racing","text":"1. Por qué combinar: el caso de Agentic Racing"},{"depth":2,"slug":"2-la-cadena-intuitiva-bc--gail--ppo","text":"2. La cadena intuitiva: BC → GAIL → PPO"},{"depth":2,"slug":"3-lo-que-ml-agents-hace-de-verdad-todo-a-la-vez","text":"3. Lo que ML-Agents hace de verdad: todo a la vez"},{"depth":2,"slug":"4-la-versión-secuencial-dos-corridas","text":"4. La versión secuencial: dos corridas"},{"depth":2,"slug":"5-dos-arquitecturas-que-vale-la-pena-comparar","text":"5. Dos arquitecturas que vale la pena comparar"},{"depth":2,"slug":"6-qué-significa-que-una-policy-sea-mejor-en-este-proyecto-no-es-solo-el-tiempo","text":"6. ¿Qué significa que una policy sea “mejor”? En este proyecto, no es solo el tiempo"},{"depth":2,"slug":"7-el-peso-de-cada-señal","text":"7. El peso de cada señal"},{"depth":2,"slug":"8-diseñar-un-experimento-reproducible","text":"8. Diseñar un experimento reproducible"},{"depth":2,"slug":"9-cómo-leer-los-resultados-cuando-existan","text":"9. Cómo leer los resultados (cuando existan)"},{"depth":2,"slug":"10-y-si-el-experto-no-es-perfecto","text":"10. ¿Y si el experto no es perfecto?"},{"depth":2,"slug":"11-y-si-una-sola-pieza-ya-alcanza","text":"11. ¿Y si una sola pieza ya alcanza?"},{"depth":2,"slug":"12-implementado-ejecutado-evaluado","text":"12. Implementado, ejecutado, evaluado"},{"depth":2,"slug":"conclusión","text":"Conclusión"},{"depth":2,"slug":"referencias","text":"Referencias"}],"body":"> **Serie: Reinforcement Learning desde cero, con Agentic Racing como laboratorio**\n>\n> 1. Parte 1 · [¿Cómo aprende un agente? Enseñando a un automóvil a conducir](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/)\n> 2. Parte 2 · [¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/)\n> 3. Parte 3 · [¿Podemos enseñar imitando? Behavioral Cloning explicado desde cero](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/)\n> 4. Parte 4 · [¿Podemos aprender a comportarnos como un experto? GAIL explicado desde cero](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/)\n> 5. Parte 5 · **¿Podemos combinar imitación y RL? BC, GAIL y PPO en un mismo sistema** (este artículo)\n> 6. Parte 6 · [¿Cómo le decimos a un agente qué queremos? Reward engineering explicado desde cero](/es/blog/como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero/)\n> 7. Parte 7 · [¿Qué debería poder ver un agente? Observation design explicado desde cero](/es/blog/que-deberia-poder-ver-un-agente-observation-design-explicado-desde-cero/)\n> 8. Parte 8 · [¿Qué hacemos cuando el agente no aprende? RL debugging explicado desde cero](/es/blog/que-hacemos-cuando-el-agente-no-aprende-rl-debugging-explicado-desde-cero/)\n\n## Dónde nos quedamos\n\nLlevamos cuatro preguntas:\n\n| Parte | Pregunta | Respuesta corta |\n|---|---|---|\n| [1](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/) | ¿Puede un agente aprender por prueba y error? | sí, con una recompensa; y encontrará cualquier atajo que esa recompensa deje abierto |\n| [2](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/) | ¿Cómo mejoramos la policy sin destruirla? | PPO: actualizaciones recortadas, cerca de la policy anterior |\n| [3](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/) | ¿Podemos enseñarle mostrando qué hace un experto? | BC: aprendizaje supervisado sobre pares $(s, a)$; frágil fuera de las demostraciones |\n| [4](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/) | ¿Podemos hacer que se *comporte* como el experto? | GAIL: un discriminador convierte la imitación en una recompensa aprendida |\n\nLa parte 4 terminó con la siguiente:\n\n> ¿Podemos combinar todas estas ideas?\n\nLa versión más concreta de esa pregunta es esta: **¿podemos usar las demostraciones de un experto\npara arrancar con una policy útil, y después dejar que el reinforcement learning la mejore?**\n\n### La advertencia de siempre, esta vez más grande\n\nEn Agentic Racing no se ejecutó ninguna combinación. BC y GAIL se configuraron para la octava corrida\ny se quitaron antes de lanzarla; nunca se grabó una demostración; no existe ninguna policy entrenada\ncon imitación, ni un fine-tuning posterior. Este artículo, entonces, no reporta un pipeline: **diseña\nel experimento que el proyecto no llegó a correr**, con lo que el repositorio sí tiene (el experto, el\ngrabador, el evaluador, la configuración que se escribió) y con lo que dice el código de ML-Agents sobre\ncómo se combinarían las piezas.\n\nComo en las partes anteriores, cada tabla de resultados es una plantilla con \"?\", no un resultado.\n\n---\n\n## 1. Por qué combinar: el caso de Agentic Racing\n\nLa motivación no es abstracta. En las partes 1 y 2 vimos lo que pasó con PPO solo: ocho corridas sobre\npistas procedurales, y la policy se quedó alrededor del 10% de la vuelta (entre un 8 y un 13%). En la octava, la más\ncuidada, el 91% de los episodios terminó con el auto fuera de la pista y solo un 4% con la vuelta\ncompleta.\n\nMientras tanto, el experto, el piloto heurístico de la parte 3, sí conduce. Cuando el proyecto pasó a un\ncircuito fijo (un rectángulo de esquinas redondeadas de unos 2 km), el experto lo recorrió sin\nsalirse ni trabarse, y una vez conectadas las directivas a su manejo, todos los autos de evaluación\ncompletaban la vuelta entera (siempre desde arranques limpios, como graba el evaluador al experto).\n\nEsa asimetría es exactamente el caso que justifica combinar:\n\n```text\nPPO solo:  aprende de la recompensa, pero no encuentra cómo dar la vuelta\nExperto:   da la vuelta, pero no aprende nada\n```\n\nLa hipótesis es que las demostraciones le ahorren a PPO la parte más difícil de la exploración (llegar\na conducir una vuelta) y que después la recompensa haga el resto. Es una hipótesis razonable. Pero\ntambién hay una explicación rival que nadie descartó: el circuito fijo hizo el problema mucho más\nfácil, y **PPO solo nunca se probó sobre él**. Quizás ya no necesita ayuda. Volveremos a eso.\n\n---\n\n## 2. La cadena intuitiva: BC → GAIL → PPO\n\nLa forma natural de imaginar la combinación es una cadena de etapas:\n\n```text\nexperto → demostraciones → BC → policy inicial → GAIL → fine-tuning con PPO → evaluación\n```\n\nCada etapa respondería una pregunta distinta:\n\n```text\nBC:   \"empieza copiando lo que hace el experto\"\nGAIL: \"refina el comportamiento para que se parezca al del experto\"\nPPO:  \"ahora optimiza la recompensa de la tarea\"\n```\n\nEs una buena forma de ordenar el razonamiento. Pero, como vimos al final de la parte 4, **no es lo que\nhace ML-Agents cuando pones los tres en una configuración**. Y la diferencia importa para diseñar el\nexperimento.\n\n---\n\n## 3. Lo que ML-Agents hace de verdad: todo a la vez\n\nEn ML-Agents 1.1.0 no hay etapas. Hay una sola corrida de PPO a la que se le enchufan dos fuentes\nadicionales de conocimiento:\n\n- **BC** es una pérdida supervisada que se aplica después de cada actualización de PPO, con una tasa de\n  aprendizaje que se apaga a lo largo de `steps` (parte 3).\n- **GAIL** es una señal de recompensa más, con su propia cabeza en el crítico, que dura toda la corrida\n  (parte 4).\n- **PPO** es el único algoritmo de RL: el que optimiza la recompensa.\n\nOjo con un detalle: BC no es solo \"información\" para PPO. Tiene su propio optimizador y da sus propios\npasos de gradiente sobre los pesos de la policy, después de cada actualización de PPO. Así que, mientras\nBC está activo, dos optimizadores tiran de la misma red: PPO hacia más recompensa, BC hacia las acciones\ndel experto. GAIL, en cambio, no toca la policy: solo cambia la recompensa que PPO ve.\n\nLa configuración que se preparó para la octava corrida era exactamente eso: recompensa del entorno con\npeso 1.0, GAIL con 0.15 y BC con 0.5 durante los primeros 2 millones de pasos. No es una cadena: es una\nmezcla cuyo balance cambia con el tiempo, porque BC se apaga y GAIL no.\n\nUna precisión sobre BC. La documentación de ML-Agents dice que BC \"puede usarse solo o como\npre-entrenamiento\", pero en el código de 1.1.0 BC siempre es un módulo que corre dentro de un\nentrenador de RL (PPO, SAC o POCA): no hay un entrenador que haga solo BC, sin interactuar con el entorno. Un BC \"puro\", como el de\nla parte 3, habría que escribirlo aparte (ML-Agents trae un lector de archivos `.demo` que serviría como\npunto de partida).\n\n---\n\n## 4. La versión secuencial: dos corridas\n\nSi queremos la cadena de verdad, con una etapa de imitación y después una de fine-tuning, ML-Agents lo\npermite con una opción de línea de comandos:\n\n```text\nmlagents-learn imitacion.yaml   --run-id=imit\nmlagents-learn finetuning.yaml  --run-id=ft --initialize-from=imit\n```\n\nLa segunda corrida arranca desde el checkpoint de la primera. Leyendo el código que hace esa carga, hay\ntres detalles que conviene conocer antes de interpretar nada:\n\n1. **Carga más que la policy.** Carga todos los módulos registrados: la policy, el crítico, el\n   optimizador de PPO y los pesos de cada proveedor de recompensa, incluido el discriminador de GAIL si la\n   segunda corrida también lo usa. (No guarda ni carga el optimizador propio de BC ni el del\n   discriminador: esos arrancan de nuevo.)\n2. **Tolera diferencias.** La carga no es estricta: si la segunda configuración ya no tiene GAIL, los\n   pesos de su cabeza en el crítico se ignoran; lo que falta queda con su inicialización nueva; y\n   si una pieza no encaja, se deja un aviso en el log y se carga lo que se pueda o se reinicia. Por\n   ejemplo, al quitar GAIL el crítico cambia de forma, y el optimizador de PPO no puede cargar su estado\n   anterior: arranca de nuevo. Eso es cómodo, pero también significa que un error de configuración\n   no detiene la corrida: hay que leer el log.\n3. **El contador de pasos vuelve a cero.** Y con él, todos los schedules lineales de la parte 2: vuelven\n   al valor inicial que diga la configuración de la segunda corrida y decaen a lo largo de sus propios\n   `max_steps`. Si se copia la configuración del proyecto, eso significa tasa de aprendizaje\n   $3 \\times 10^{-4}$, $\\beta$ $10^{-2}$ y $\\varepsilon$ 0.2. Para un fine-tuning eso es justo lo\n   contrario de lo que queremos: después de aprender a imitar, la segunda corrida empieza con una tasa\n   de aprendizaje y una presión de entropía tan altas como al inicio de la primera. Si no se bajan a mano en la segunda configuración, el fine-tuning puede\n   empezar deshaciendo lo que la imitación construyó.\n\n![Dos formas de combinar BC, GAIL y PPO en ML-Agents. Arriba, la simultánea: una sola corrida donde las demostraciones alimentan la pérdida de BC y el discriminador de GAIL, PPO optimiza la recompensa y BC también ajusta la policy con su propio optimizador. Abajo, la secuencial: una corrida de imitación guarda un checkpoint, y una segunda corrida arranca de él con initialize-from y entrena solo con la recompensa del entorno. Una nota recuerda que el contador de pasos vuelve a cero y los schedules empiezan de nuevo.](/images/combine-pipelines.svg)\n\n---\n\n## 5. Dos arquitecturas que vale la pena comparar\n\nCon eso, las dos opciones concretas son:\n\n**Arquitectura A — simultánea, una corrida**\n\n```text\ndemostraciones ─┬─► BC (se apaga) ─────────┐\n                └─► GAIL (recompensa) ─────┤\nrecompensa del entorno ────────────────────┴─► PPO ─► policy\n```\n\nEs la que se preparó para Agentic Racing. Ventaja: una sola corrida, y la recompensa del entorno está\npresente desde el principio, así que la imitación nunca se optimiza sola. Riesgo: el balance entre\nseñales lo decide la escala de cada una, no solo el peso escrito en la configuración (sección 7).\n\n**Arquitectura B — secuencial, dos corridas**\n\n```text\ncorrida 1: PPO + BC + GAIL  ──checkpoint──►  corrida 2: PPO + solo recompensa del entorno\n```\n\nVentaja: separa con claridad qué aporta cada etapa, porque se puede evaluar el checkpoint intermedio.\nRiesgo: al quitar la imitación, nada impide que la policy se aleje del experto. Eso puede ser bueno\n(encontrar algo mejor) o malo (olvidar habilidades útiles). Y como vimos en la parte 1, PPO solo con\nesta recompensa ya encontró una vez una salida fácil: en la cuarta corrida aprendió a detenerse a\npropósito.\n\nNinguna es superior de antemano. Hay que probarlas.\n\n---\n\n## 6. ¿Qué significa que una policy sea \"mejor\"? En este proyecto, no es solo el tiempo\n\nAntes de comparar, hay que decidir qué medimos. Lo obvio sería medir conducción: vueltas completas,\ntiempo por vuelta, salidas de pista. Pero en Agentic Racing hay algo más importante, y es lo que más\ncambia el diseño del experimento.\n\nEl piloto del demo no existe para ser rápido. Existe para **obedecer al estratega**. En el vector de\n42 observaciones hay 6 canales de directiva (agresión, tolerancia al riesgo y el tipo de directiva:\natacar, defender, conservar, empujar), y durante el entrenamiento se sortean al azar en cada episodio,\njustamente para que la policy aprenda a conducir distinto según lo que diga el estratega.\n\nEl experto sí lo hace. Medido sobre el circuito fijo:\n\n| Directiva forzada | Tiempo por vuelta |\n|---|---:|\n| conservar, agresión 0.15 | 112 s |\n| aleatoria por episodio | 100 s |\n| atacar, agresión 0.85 | 79 s |\n\nLa vuelta más lenta tarda unos 33 s más que la más rápida, cerca de un 40% más: esa es la palanca real que tiene el estratega LLM sobre\nla carrera.\n\nAhora miremos la recompensa de la tarea. Sus términos son los de la parte 1: progreso, velocidad\nobjetivo, trazada, bordes, salidas, vuelta completa. Y la velocidad objetivo que premia depende de la\ncurvatura de la pista, **no de la directiva**. Ningún término de la recompensa lee los canales de\ndirectiva.\n\nEso tiene una consecuencia incómoda:\n\n- Una policy que **imite** al experto tiene una razón para responder a la directiva: el experto lo\n  hace, y la directiva está en sus observaciones.\n- Una policy que **optimice solo la recompensa** no tiene ninguna. Para ella, los 6 canales son ruido.\n  Lo más rentable es conducir de la forma que más recompensa da, sea cual sea la directiva.\n\nEs decir, en la arquitectura B, el fine-tuning con la recompensa de la tarea podría mejorar el tiempo\npor vuelta y, al mismo tiempo, **borrar justo lo que el demo necesita**: que el auto conduzca distinto\ncuando el estratega dice \"ataca\" que cuando dice \"conserva\". Nadie lo midió, así que es una pregunta,\nno un hallazgo. Pero es la pregunta más importante del experimento.\n\n![Tiempo por vuelta del experto en el circuito fijo según la directiva: 112 segundos conservando, 100 con directiva aleatoria, 79 atacando: la vuelta más lenta tarda cerca de un 40% más. Al lado, dos columnas con barras vacías y signos de pregunta: una policy que imita al experto, que debería conservar esa diferencia si la aprendió, y una policy afinada solo con la recompensa de la tarea, que no tiene nada que la obligue a conservarla porque la recompensa no lee la directiva.](/images/combine-directives.svg)\n\nPor eso, en este proyecto, la evaluación necesita tres familias de métricas, no dos:\n\n**Conducción**\n\n- fracción de vuelta, vueltas completas y tiempo por vuelta;\n- motivos de fin de episodio: salida de pista, auto detenido, sin progreso;\n- recuperación desde arranques con ruido.\n\n**Imitación**\n\n- parecido de trayectorias y de distribución de acciones con el experto, en especial el freno;\n- velocidad por tramo y comportamiento en las cuatro curvas.\n\n**Respuesta a la directiva**\n\n- tiempo por vuelta con cada directiva forzada;\n- la diferencia entre \"atacar\" y \"conservar\", comparada con la del experto.\n\nEl evaluador del proyecto (`eval.exe`) ya cubre buena parte de la primera familia: fracción de vuelta,\nmotivos de fin de episodio y velocidades, y evalúa las policies entrenadas desde arranques con ruido (al\nexperto lo evalúa siempre desde arranques limpios). El tiempo por vuelta solo lo reporta directamente en\nsu modo de población, así que habría que agregarlo. También permite forzar una directiva en todos los\nautos, así que la tercera familia sale casi gratis. La segunda habría que construirla.\n\n---\n\n## 7. El peso de cada señal\n\nEn la parte 4 hicimos la cuenta: con el discriminador indeciso, GAIL con peso 0.15 paga unos 0.10 por\ndecisión, y la recompensa diseñada, conduciendo bien, unos 0.07. El 0.15 parecía chico y no lo era.\n\nLa propia documentación de ML-Agents va en la misma dirección. Recomienda mantener el peso de GAIL por\ndebajo de 0.1 cuando las demostraciones no son óptimas y hay recompensa del entorno, para que el agente\nse enfoque en la recompensa y no en copiar; su ejemplo de referencia que combina las tres cosas\n(PushBlock) usa 0.01. Y advierte del sesgo de supervivencia que vimos en la parte 4: una recompensa\nsiempre positiva por paso empuja al agente a alargar el episodio, y puede tapar la recompensa de la\ntarea. Si el agente parece ignorar la recompensa del entorno, la indicación es bajar el peso de GAIL.\n\nAsí que el peso de GAIL no es un detalle de configuración: es una de las variables del experimento.\nComo mínimo, habría que probar el 0.15 planeado y un valor del orden que recomienda la documentación.\n\n---\n\n## 8. Diseñar un experimento reproducible\n\nCon todo lo anterior, el experimento de la parte 4 se amplía así:\n\n| Variante | Qué entrena | En ML-Agents |\n|---|---|---|\n| A. PPO desde cero | recompensa del entorno | `extrinsic`, una corrida |\n| B. BC puro | solo demostraciones | código propio: no hay entrenador de BC sin entorno |\n| C. GAIL + entorno | imitación como recompensa | `gail` + `extrinsic`, una corrida |\n| D. Simultánea | BC + GAIL + entorno | la configuración preparada para la octava corrida |\n| E. Secuencial | D, y después fine-tuning | segunda corrida con `--initialize-from`, solo `extrinsic` |\n\nY, para cada una:\n\n| Métrica | A | B | C | D | E |\n|---|:---:|:---:|:---:|:---:|:---:|\n| Vueltas completas | ? | ? | ? | ? | ? |\n| Tiempo por vuelta | ? | ? | ? | ? | ? |\n| Salidas de pista | ? | ? | ? | ? | ? |\n| Parecido con el experto | ? | ? | ? | ? | ? |\n| Diferencia atacar / conservar | ? | ? | ? | ? | ? |\n| Recuperación desde arranques con ruido | ? | ? | ? | ? | ? |\n\nPara que esa tabla signifique algo, cada variante tiene que competir en condiciones comparables:\n\n- **El mismo presupuesto de interacción.** E recibe los pasos de D *más* los de su segunda corrida; hay\n  que comparar a igualdad de pasos totales, o al menos reportarlos. Y las demostraciones también\n  cuestan: el grabador corre por defecto 300 s de simulación con el experto, en todas las arenas a la\n  vez, y esos pasos no aparecen en el contador de\n  pasos de PPO.\n- **Varias semillas.** `mlagents-learn` acepta `--seed`; con una sola corrida por variante no hay forma\n  de distinguir un efecto del azar. Reportar media y dispersión, no la mejor corrida.\n- **El mismo entorno y las mismas demostraciones.** El mismo build, la misma física, el mismo conjunto\n  de `.demo` para todas las variantes que los usen.\n- **Las mismas reglas de evaluación.** Mismo número de episodios, mismos arranques, mismo criterio para\n  elegir el checkpoint (el último, o el mejor según una métrica fijada de antemano, nunca el que \"se ve\n  mejor\").\n- **Una prueba de generalización honesta.** Las demostraciones saldrían todas del circuito fijo, que es la pista por defecto. El\n  generador de pistas procedurales sigue en el código; evaluar ahí, sin haber entrenado ahí, diría si la\n  policy aprendió a conducir o memorizó un circuito.\n\nEl proyecto ya tiene un precedente de este tipo de rigor. En el experimento del estratega LLM contra el\nestratega heurístico, se corrieron series de 18 carreras con la parrilla rotada, se reportó la media con\nsu desviación estándar, y el primer resultado fue contrario a lo esperado. Se publicó así. La misma\ndisciplina aplicaría aquí.\n\n---\n\n## 9. Cómo leer los resultados (cuando existan)\n\nSin números, lo que sí se puede escribir de antemano es qué significaría cada patrón:\n\n- **Si A ya completa vueltas en el circuito fijo**, la motivación original de la imitación desaparece.\n  Lo que quede por medir es si la imitación enseña algo que PPO no: la respuesta a la directiva.\n- **Si B conduce bien desde arranques limpios pero falla desde arranques con ruido**, es el problema de\n  errores acumulativos de la parte 3, tal como se esperaba.\n- **Si C o D mejoran la duración de los episodios pero no las vueltas completas**, sospechar del sesgo\n  de supervivencia de GAIL antes que celebrar.\n- **Si E mejora el tiempo por vuelta pero la diferencia entre atacar y conservar se encoge**, ganó en la\n  métrica equivocada para este demo.\n- **Si las diferencias entre variantes son menores que la dispersión entre semillas**, el resultado\n  honesto es \"no hay efecto detectable\".\n\nUn experimento útil no es el que demuestra que la arquitectura funciona. Es el que está diseñado para\npoder revelar que no funciona.\n\n---\n\n## 10. ¿Y si el experto no es perfecto?\n\nNo lo es. El experto frena según una regla fija, una velocidad objetivo calculada a partir de la curva\nque viene, y tiene comportamientos de recuperación que dependen de temporizadores que la policy no ve\n(parte 3). Ninguna técnica de imitación, por sí sola, está diseñada para superarlo: BC aprende sus decisiones, GAIL\naprende a parecerse a él.\n\nPara superarlo hace falta una señal que diga \"esto es mejor\", y esa señal es la recompensa. Ahí está\nel atractivo de la arquitectura B. Pero \"mejor según la recompensa\" solo es \"mejor\" si la recompensa\nmide lo que queremos, y la sección 6 mostró que en este proyecto no mide lo más importante. Antes de\nun fine-tuning, habría que decidir si la recompensa necesita un término que premie respetar la\ndirectiva. Eso ya no es combinar algoritmos: es volver a diseñar el objetivo, el problema de la\nparte 1.\n\n---\n\n## 11. ¿Y si una sola pieza ya alcanza?\n\nHay que contemplarlo en serio. Si BC produce una policy fiable, quizá GAIL agregue complejidad sin\nmejorar nada. Si D funciona, quizá E la empeore. Cada etapa tiene que justificar su existencia con\nevidencia:\n\n> ¿Qué mejora concreta aporta esta etapa, y cuánto cuesta conseguirla?\n\nAgentic Racing llevó esa pregunta más lejos que cualquier variante de la tabla. Después de la octava\ncorrida, la decisión fue no entrenar ningún piloto: el experto heurístico, con las directivas\nconectadas a su ritmo, su frenada y su línea, pasó a ser el piloto del demo. El objetivo del proyecto\nera mostrar el ciclo entre el piloto y el estratega, y para eso no hacía falta una red neuronal: hacía\nfalta un piloto que cambiara de comportamiento según la directiva, y el experto ya lo hacía.\n\nNo es una conclusión sobre BC, GAIL o PPO. Es una conclusión sobre ingeniería: a veces la pieza más\nsencilla que cumple el objetivo es la correcta, y el pipeline más sofisticado queda como experimento\npara otro día.\n\n---\n\n## 12. Implementado, ejecutado, evaluado\n\nVale la pena separar tres niveles de madurez, porque se confunden con facilidad:\n\n- **Implementado:** el código existe.\n- **Ejecutado:** se corrió y hay evidencia.\n- **Evaluado:** se midió y se comparó contra una referencia.\n\nAplicado a Agentic Racing:\n\n| Pieza | Implementado | Ejecutado | Evaluado |\n|---|---|---|---|\n| Experto heurístico | sí | sí: es el piloto del demo | sí: vueltas completas en el circuito fijo y tiempos por directiva |\n| Grabación de demostraciones (`eval.exe -record`) | sí | no hay registro de que se haya usado | — |\n| PPO | sí | sí: ocho corridas en pistas procedurales | sí: alrededor del 10% de vuelta (8–13%); en el circuito fijo, nunca |\n| BC | configurado | no: se quitó antes de la octava corrida | — |\n| GAIL | configurado | no: se quitó junto con BC | — |\n| BC + GAIL + PPO simultáneo | configurado | no | — |\n| Fine-tuning secuencial | no | no | — |\n\nQue una configuración exista no demuestra que funcione. Que una corrida se haya ejecutado tampoco\ndemuestra que mejore frente a una referencia. Esta tabla es lo que un lector debería poder pedirle a\ncualquier proyecto de RL antes de creerle una curva.\n\n---\n\n## Conclusión\n\nBC, GAIL y PPO atacan partes distintas del problema:\n\n- **BC** usa las demostraciones para aprender directamente las acciones del experto.\n- **GAIL** convierte la comparación con el experto en una recompensa.\n- **PPO** optimiza una policy con la recompensa que reciba, sea cual sea.\n\nLa cadena `BC → GAIL → PPO` es una buena forma de pensarlos juntos, pero en ML-Agents la combinación\nnatural es simultánea: una sola corrida de PPO, con BC empujando la policy hacia el experto y GAIL\nsumando su recompensa. La versión secuencial existe,\ncon `--initialize-from`, y trae sus propias trampas: los schedules reinician, y quitar la imitación deja\na la policy a merced de la recompensa.\n\nY en este proyecto esa recompensa tiene un punto ciego concreto: no sabe nada de las directivas del\nestratega, que son la razón de ser del piloto. Es el tipo de hallazgo que solo aparece cuando se diseña\nel experimento con el sistema real en la mano, y no con una arquitectura genérica en la cabeza.\n\n> La pregunta interesante no es si podemos encadenar tres algoritmos, sino si cada uno aporta algo\n> medible que los otros no aportan.\n\nAgentic Racing tiene casi todo lo necesario para responderla: el experto, el grabador, el evaluador y\nla configuración. Le falta lo más importante: correrlo.\n\nSi llegaste directo a este artículo, las cuatro partes anteriores están enlazadas al principio, y\n[Agentic Racing: un piloto que nunca aprendió a manejar](/es/blog/agentic-racing-un-piloto-que-nunca-aprendio-a-manejar/)\ncuenta la historia completa del proyecto.\n\n---\n\n## Referencias\n\n- Ho, J. y Ermon, S. (2016). [Generative Adversarial Imitation Learning](https://arxiv.org/abs/1606.03476). *NeurIPS*.\n- Schulman, J. et al. (2017). [Proximal Policy Optimization Algorithms](https://arxiv.org/abs/1707.06347).\n- Ross, S., Gordon, G. y Bagnell, J. A. (2011). [A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning](https://arxiv.org/abs/1011.0686). *AISTATS*.\n- Rajeswaran, A. et al. (2018). [Learning Complex Dexterous Manipulation with Deep Reinforcement Learning and Demonstrations](https://arxiv.org/abs/1709.10087). *RSS*. (Un antecedente de inicializar con BC y afinar con RL.)\n- Kostrikov, I. et al. (2019). [Discriminator-Actor-Critic: Addressing Sample Inefficiency and Reward Bias in Adversarial Imitation Learning](https://arxiv.org/abs/1809.02925). *ICLR*.\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), release de\n  `mlagents` 1.1.0: la sección de imitation learning de `docs/ML-Agents-Overview.md`, los parámetros\n  de `gail` y `behavioral_cloning` en `docs/Training-Configuration-File.md`, `--initialize-from` en\n  `docs/Training-ML-Agents.md`, y la carga de checkpoints en\n  `ml-agents/mlagents/trainers/model_saver/torch_model_saver.py`.\n- La configuración de entrenamiento, el experto, el evaluador y la bitácora del proyecto están en\n  [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch","lang":"en","title":"Can We Learn to Behave Like an Expert? GAIL Explained from Scratch","description":"Part 4 of the Reinforcement Learning from scratch series with Agentic Racing. What GAIL is, how a discriminator turns imitation into a learned reward, exactly what ML-Agents does with it, and what would have happened if the project had actually used the GAIL configuration it prepared for its pilot.","publishDate":"2026-10-07","updatedDate":"2026-10-09","tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero","url":"https://alexisalulema.com/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/","series":{"id":"rl-from-scratch","position":4},"headings":[{"depth":2,"slug":"where-we-left-off","text":"Where we left off"},{"depth":3,"slug":"an-honest-warning-as-in-the-previous-parts","text":"An honest warning, as in the previous parts"},{"depth":2,"slug":"1-from-copying-actions-to-imitating-behavior","text":"1. From copying actions to imitating behavior"},{"depth":2,"slug":"2-the-idea-of-the-discriminator","text":"2. The idea of the discriminator"},{"depth":2,"slug":"3-why-does-this-resemble-a-gan","text":"3. Why does this resemble a GAN?"},{"depth":2,"slug":"4-behavior-matters-not-an-isolated-action","text":"4. Behavior matters, not an isolated action"},{"depth":2,"slug":"5-an-intuition-with-agentic-racing","text":"5. An intuition with Agentic Racing"},{"depth":2,"slug":"6-the-discriminator","text":"6. The discriminator"},{"depth":2,"slug":"7-the-discriminators-objective","text":"7. The discriminator’s objective"},{"depth":2,"slug":"8-what-does-the-policy-want","text":"8. What does the policy want?"},{"depth":2,"slug":"9-the-learning-loop","text":"9. The learning loop"},{"depth":2,"slug":"10-gail-is-still-reinforcement-learning","text":"10. GAIL is still reinforcement learning"},{"depth":2,"slug":"11-a-learned-reward","text":"11. A learned reward"},{"depth":2,"slug":"12-so-does-gail-remove-the-need-to-design-rewards","text":"12. So does GAIL remove the need to design rewards?"},{"depth":2,"slug":"13-how-is-it-different-from-behavioral-cloning","text":"13. How is it different from Behavioral Cloning?"},{"depth":2,"slug":"14-the-bc-problem-gail-tackles","text":"14. The BC problem GAIL tackles"},{"depth":2,"slug":"15-occupancy-measures-the-math-behind-gail","text":"15. Occupancy measures: the math behind GAIL"},{"depth":2,"slug":"16-gail-as-an-adversarial-problem","text":"16. GAIL as an adversarial problem"},{"depth":2,"slug":"17-why-not-just-copy-the-actions","text":"17. Why not just copy the actions?"},{"depth":2,"slug":"18-but-gail-isnt-magic-either","text":"18. But GAIL isn’t magic either"},{"depth":2,"slug":"19-how-do-we-evaluate-gail","text":"19. How do we evaluate GAIL?"},{"depth":2,"slug":"20-the-experts-role-is-fundamental-again","text":"20. The expert’s role is fundamental again"},{"depth":2,"slug":"21-a-heuristic-expert-can-be-enough","text":"21. A heuristic expert can be enough"},{"depth":2,"slug":"22-bc--gail","text":"22. BC + GAIL"},{"depth":2,"slug":"23-and-where-does-ppo-come-in","text":"23. And where does PPO come in?"},{"depth":2,"slug":"24-a-possible-experiment-with-agentic-racing","text":"24. A possible experiment with Agentic Racing"},{"depth":2,"slug":"25-the-real-challenge-what-does-looking-like-the-expert-mean","text":"25. The real challenge: what does “looking like the expert” mean?"},{"depth":2,"slug":"26-imitating-doesnt-mean-surpassing","text":"26. Imitating doesn’t mean surpassing"},{"depth":2,"slug":"27-a-way-to-understand-the-whole-series","text":"27. A way to understand the whole series"},{"depth":2,"slug":"conclusion","text":"Conclusion"},{"depth":2,"slug":"references","text":"References"}],"body":"> **Series: Reinforcement Learning from scratch, with Agentic Racing as the lab**\n>\n> 1. Part 1 · [How Does an Agent Learn? Teaching a Car to Drive](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/)\n> 2. Part 2 · [How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/)\n> 3. Part 3 · [Can We Teach by Imitation? Behavioral Cloning Explained from Scratch](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/)\n> 4. Part 4 · **Can We Learn to Behave Like an Expert? GAIL Explained from Scratch** (this article)\n> 5. Part 5 · [Can We Combine Imitation and RL? BC, GAIL, and PPO as One System](/blog/can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system/)\n> 6. Part 6 · [How Do We Tell an Agent What We Want? Reward Engineering Explained from Scratch](/blog/how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch/)\n> 7. Part 7 · [What Should an Agent Be Able to See? Observation Design Explained from Scratch](/blog/what-should-an-agent-be-able-to-see-observation-design-explained-from-scratch/)\n> 8. Part 8 · [What Do We Do When the Agent Doesn't Learn? RL Debugging Explained from Scratch](/blog/what-do-we-do-when-the-agent-doesnt-learn-rl-debugging-explained-from-scratch/)\n\n## Where we left off\n\n[Part 3](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/) asked a simple\nquestion: can we teach an agent by imitating an expert? The answer was yes. With **Behavioral\nCloning (BC)** we collect examples $(s_i, a_i)$ and train a policy so that:\n\n$$\n\\pi_\\theta(s_i) \\approx a_i\n$$\n\nBut a problem showed up: if the agent reaches a state that wasn't in the demonstrations, it may not\nknow what to do, and each mistake takes it to states where it makes more mistakes. And in Agentic\nRacing another one showed up: the expert, the project's heuristic pilot, decides with memory and\ntimers the policy doesn't see, so there are situations where no memoryless network can copy its\nexact action.\n\nThat opens a more interesting question:\n\n> Do we really need to tell the agent which action to take in every state?\n\nMaybe we can teach it something different:\n\n> \"I want your behavior to look like the expert's.\"\n\nThat's the idea behind **Generative Adversarial Imitation Learning (GAIL)**, which part 3 announced\nas the series' next stop.\n\n### An honest warning, as in the previous parts\n\nIn Agentic Racing, GAIL met the same fate as BC: **it was configured but never run**. The\nconfiguration prepared for the eighth run had a GAIL block next to the BC one; it was removed before\nthe run was launched, and that run was done with plain PPO. The log doesn't record any demonstration\nbeing recorded, so there's no trained discriminator, no GAIL metrics, and no BC-vs-GAIL comparison to\nshow either.\n\nWhat does exist is enough to make this article concrete: the expert, the demonstration recorder, the\nconfiguration that was written, and the ML-Agents code that would have run it. So, as in part 3, the\ntheory comes with what the project actually had and with what would have happened with it. The\nresult tables in this article are proposed experiments, not results.\n\n---\n\n## 1. From copying actions to imitating behavior\n\nBehavioral Cloning can be pictured like this:\n\n```text\n       observation\n            │\n            ▼\n  what did the expert do?\n            │\n            ▼\n      expert's action\n            │\n            ▼\n         train π\n```\n\nFor example:\n\n```text\nobservation:\nright-hand corner\nhigh speed\ncar slightly off line\n\n             ↓\n\nexpert:\nsteer    = 0.45\nthrottle = 0.30\nbrake    = 0.10\n```\n\nBC tries to learn that mapping. GAIL asks a different question:\n\n> Does the behavior our agent produces look like the expert's?\n\nAnd to answer it, it introduces a new component: **a discriminator**.\n\n---\n\n## 2. The idea of the discriminator\n\nImagine two drivers, the expert and the agent, and a judge watching what each one does:\n\n```text\n         EXPERT                  AGENT\n           │                       │\n        behavior                behavior\n           │                       │\n           └──────────┬────────────┘\n                      ▼\n              ┌───────────────┐\n              │ Discriminator │\n              └───────┬───────┘\n                      ▼\n          from the expert or the agent?\n```\n\nThe discriminator tries to tell the expert's behavior apart from the agent's. If it gets an\nexperience from the expert, it should answer \"expert\"; if it gets one from the agent, \"agent\".\n\nThe interesting part is that the agent is learning too, and its goal is to make that classification\nharder and harder. We have a game:\n\n```text\nDiscriminator: \"I want to tell them apart\".\nAgent:         \"I want you to be unable to tell me apart from the expert\".\n```\n\n---\n\n## 3. Why does this resemble a GAN?\n\nThe name isn't a coincidence. GAIL is inspired by **Generative Adversarial Networks (GANs)**. In a\nGAN, simplifying:\n\n```text\ngenerator ──► generated data ──► discriminator ──► real or generated?\n```\n\nThe generator tries to produce data that looks real, and the discriminator tries to tell real from\ngenerated.\n\nGAIL does something similar, with one fundamental difference:\n\n> In GAIL, the \"generator\" doesn't generate images or vectors directly. The policy generates\n> **behavior by interacting with the environment**.\n\nThe policy doesn't produce an isolated sample. It produces a sequence:\n\n$$\n(s_0, a_0, s_1, a_1, s_2, a_2, \\ldots)\n$$\n\nand the states in that sequence aren't chosen by the policy: the environment's physics produces them\nfrom its actions. That's why GAIL can't be trained like a normal GAN, by differentiating through the\ngenerator. It needs reinforcement learning.\n\n---\n\n## 4. Behavior matters, not an isolated action\n\nBC works with the mapping $s_t \\rightarrow a_t$. GAIL works with $(s_t, a_t)$ pairs, but looked at\nas a **distribution**: how often the policy visits each state and which actions it takes there.\n\nImagine a stretch of track:\n\n```text\nexpert: brake 0.30 → turn 0.42 → accelerate 0.70 → correct −0.05\nagent:  brake 0.28 → turn 0.45 → accelerate 0.66 → correct −0.08\n```\n\nNone of the agent's actions match the expert's, but the behavior is almost the same: it brakes,\nturns, accelerates and corrects in the same places and with similar magnitudes. BC would penalize\neach of those small differences. A discriminator, if they're differences the size of the expert's own\nnormal variation, would have no way of using them to tell the two apart.\n\nThat's what GAIL tries to capture.\n\n---\n\n## 5. An intuition with Agentic Racing\n\nAgentic Racing's circuit today is a rounded rectangle about 2 km long, with four identical corners.\nThe expert, part 3's heuristic pilot, drives it lap after lap. Each lap leaves a trajectory:\n\n$$\n\\tau_{\\text{expert}} = \\big((s_0, a_0),\\ (s_1, a_1),\\ \\ldots,\\ (s_T, a_T)\\big)\n$$\n\nwhere each $s_t$ is the same 42 observation numbers the policy receives (the ones from part 1) and each\n$a_t$ is the same three continuous actions: `steer`, `throttle` and `brake`.\n\nNow we let the agent drive, and it produces its own trajectories $\\tau_{\\text{agent}}$. GAIL trains\na discriminator to tell the pairs from one and the other apart.\n\nAt first it's easy: the freshly initialized agent zigzags, brakes on the straights or accelerates in\nthe corners, and the discriminator spots it immediately. As the policy improves, its pairs look more\nand more like the expert's, and the discriminator starts to hesitate. The goal is to reach a point\nwhere the discriminator struggles to tell them apart.\n\n---\n\n## 6. The discriminator\n\nFormally, the discriminator is a function:\n\n$$\nD_\\phi(s, a) \\in (0, 1)\n$$\n\nwhere $s$ is the state, $a$ the action, and $\\phi$ the parameters of a neural network. Its output is\nread as the probability that the pair comes from the expert.\n\nFor example, early in training:\n\n```text\nD(expert pair) = 0.95\nD(agent pair)  = 0.08\n```\n\nThe discriminator is very sure. Later on:\n\n```text\nD(expert pair) = 0.60\nD(agent pair)  = 0.45\n```\n\nTelling them apart gets harder.\n\nIn ML-Agents 1.1.0, the discriminator is a dense network that receives the observation and, if the\n`use_actions` option is on, also the action and a flag saying whether the episode ended at that\nstep. By default it has 2 layers of 128 neurons and a sigmoid output, which is the probability above.\nThe configuration prepared for Agentic Racing turned `use_actions` on, so the discriminator would\nhave compared full observation–action pairs.\n\n---\n\n## 7. The discriminator's objective\n\nThe discriminator is trained as a binary classifier:\n\n$$\n\\max_{D}\\ \\ \\mathbb{E}_{\\pi_E}\\big[\\log D(s,a)\\big] + \\mathbb{E}_{\\pi}\\big[\\log\\big(1 - D(s,a)\\big)\\big]\n$$\n\nwhere $\\pi_E$ is the expert's behavior and $\\pi$ the agent's:\n\n```text\nexpert pairs ──► D should give high values\nagent pairs  ──► D should give low values\n```\n\nIn ML-Agents, the discriminator's loss is that expression with the sign flipped (apart from a small\nnumerical margin inside the logarithms). It also adds a **gradient penalty** with weight 10: it\nevaluates the discriminator at points between expert and agent pairs, and penalizes the norm of its\ngradient with respect to the input for drifting away from 1. It's a technique borrowed from GANs\n(Gulrajani et al., 2017) to keep the discriminator from becoming so sharp that it stops giving a\nuseful signal. The code itself explains why it's there: it \"adds stability esp. for off-policy\" (PPO\nis on-policy, so here it's a safeguard more than a necessity).\n\nBut the important thing is that **the policy is learning too**.\n\n---\n\n## 8. What does the policy want?\n\nThe policy wants to generate behavior that looks like the expert's:\n\n```text\ndiscriminator ──► \"this looks like the agent\" ──► signal ──► policy\n      ▲                                                        │\n      │                                                        ▼\n      └──────────────────── new behavior ◄────────────────────┘\n```\n\nThe policy changes, the behavior changes, the discriminator is retrained on that new behavior, and the\nprocess continues. It's an adversarial game.\n\n---\n\n## 9. The learning loop\n\nThe full loop is:\n\n1. the agent interacts with the environment;\n2. it generates trajectories;\n3. the discriminator scores those pairs;\n4. its output is turned into a reward;\n5. the policy is updated with that reward;\n6. the discriminator is updated to tell them apart better;\n7. the agent interacts again.\n\n![The GAIL loop in ML-Agents: the agent drives and fills the buffer; the discriminator scores every observation–action pair and that score becomes the GAIL reward, added to the environment's reward; PPO updates the policy, and on every minibatch the discriminator takes a step to better tell the expert's demonstrations apart from the agent's experiences.](/images/gail-loop.svg)\n\nIn ML-Agents, steps 5 and 6 are interleaved. On every minibatch of the PPO update (the 30 from part\n2), the policy is updated first, and then the discriminator takes a step with that same minibatch of\nagent experiences and a same-size sample of the demonstrations. Policy and discriminator move forward\ntogether.\n\n---\n\n## 10. GAIL is still reinforcement learning\n\nThis distinction matters: GAIL **doesn't replace** reinforcement learning. What changes is the source\nof the learning signal.\n\nIn traditional RL:\n\n```text\nenvironment ──► reward ──► policy\n```\n\nIn GAIL:\n\n```text\nexpert + agent behavior ──► discriminator ──► imitation reward ──► policy\n```\n\nThe policy still learns by interacting with the environment, with an RL algorithm. In ML-Agents that\nalgorithm is PPO, the same one from part 2: to PPO, GAIL is simply **one more reward signal**. The\ndifference is that we don't design this reward: the discriminator builds it.\n\n---\n\n## 11. A learned reward\n\nThis is one of GAIL's most powerful ideas. In part 1 we hand-designed a reward with nine components\n(progress, target speed, edge proximity, lap completed…) and saw how many versions it took and how many\ntimes the agent found shortcuts. GAIL proposes something else: let the reward come from comparing the\nagent with the expert.\n\nML-Agents 1.1.0 computes it like this, for every step:\n\n$$\nr_{\\text{GAIL}}(s, a) = -\\log\\big(1 - D(s, a)\\big)\n$$\n\n(with a small numerical margin so it's never infinite).\n\nThe reading is direct:\n\n- if the discriminator thinks the pair is the agent's, $D \\approx 0$ and the reward is almost 0;\n- if it thinks it's the expert's, $D \\rightarrow 1$ and the reward grows;\n- at $D = 0.5$, when it can't decide, the reward is $\\log 2 \\approx 0.69$.\n\nOne detail is worth noticing: this reward **is never negative**. We'll come back to that in section\n18.\n\n```text\nthe agent acts\n       ↓\n discriminator\n       ↓\n\"does it look like the expert?\"\n       ↓\n    reward\n       ↓\n    policy\n```\n\n---\n\n## 12. So does GAIL remove the need to design rewards?\n\nNot entirely. That's an oversimplified reading.\n\nGAIL learns an imitation signal from demonstrations, but the full system still has an environment,\nobservations, actions, an RL algorithm, hyperparameters and, almost always, other rewards. And\nlearning from an expert doesn't guarantee the learned behavior is optimal:\n\n> If the expert drives badly, GAIL can learn to drive like that expert.\n\nIn practice, GAIL is almost never used alone. The configuration prepared for Agentic Racing kept the\nwhole designed reward from part 1, with weight 1.0, and added GAIL's with a weight of 0.15. GAIL didn't\nreplace the reward: it complemented it.\n\n---\n\n## 13. How is it different from Behavioral Cloning?\n\n| Characteristic | Behavioral Cloning | GAIL |\n|---|---|---|\n| Type of learning | supervised | imitation through RL |\n| Learns actions directly? | yes | no: learns to produce a similar distribution |\n| Uses demonstrations? | yes | yes |\n| Uses a discriminator? | no | yes |\n| Needs to interact with the environment? | no | yes |\n| Learns in the states the policy itself visits? | no | yes |\n| Uses a reward? | no | yes, a learned one |\n\nIn one sentence each:\n\n- **Behavioral Cloning:** \"do what the expert did in this situation\".\n- **GAIL:** \"produce behavior that looks like the expert's\".\n\nThe difference looks small, but conceptually it's huge.\n\n---\n\n## 14. The BC problem GAIL tackles\n\nRecall part 3's compounding errors. BC learns from the states the expert visited, but afterward the\npolicy visits its own:\n\n```text\nexpert trajectory  ─────────────────────►\n\nagent trajectory   ───────────╮\n                              ╰────────► out of distribution\n```\n\nGAIL has a conceptual advantage: **the policy generates its own states by interacting with the\nenvironment**, and the discriminator scores those too. If the policy drifts into a state the expert\nnever visited, the discriminator recognizes it as \"the agent's\" and the GAIL reward drops. The way to\nearn reward again is to get back to zones that look like the expert's. In other words, GAIL doesn't\njust teach the agent what to do where the expert was: it gives it a reason to **return** there.\n\nIn Agentic Racing that would have been especially relevant. As we saw in part 3, the demonstrations\nwould always be recorded from clean spawns (centered, aligned, at 14 m/s), while training starts with\nheading and position noise. With BC, those first states would fall outside what the demonstrations\nteach. With GAIL, the reward would push the policy to reach expert-like states as soon as possible.\n\nThat doesn't mean GAIL magically removes the problem. It means training takes into account the\ndistribution of states the policy itself generates, which is exactly what BC didn't do.\n\n---\n\n## 15. Occupancy measures: the math behind GAIL\n\nAn elegant way to understand GAIL is through **occupancy measures**. Simplifying, a policy induces a\ndistribution over the state–action pairs it visits:\n\n$$\n\\rho_\\pi(s, a)\n$$\n\nand the expert induces its own:\n\n$$\n\\rho_E(s, a)\n$$\n\nGAIL looks for a policy whose distribution resembles the expert's:\n\n$$\n\\rho_\\pi \\approx \\rho_E\n$$\n\nThere's an elegant result behind this (Ho and Ermon, 2016): when the state is fully observable, each\npolicy has a unique distribution of this kind, so matching the distributions is, at bottom, matching\nthe expert's policy:\n\n$$\n\\pi(a \\mid s) = \\pi_E(a \\mid s)\n$$\n\nin the states the expert visits. So what changes compared to BC? **Where the errors are measured.** BC\nmeasures them in the expert's states; GAIL, in the states the policy actually goes through. A mistake\nthat takes the policy out of the expert's zone costs BC little and GAIL a lot.\n\nThis connects to a limit we saw in part 3. The expert having memory isn't, by itself, an obstacle: in\ntheory there's always a memoryless policy that reproduces the same distribution of state–action pairs.\nWhat is an obstacle in Agentic Racing is something else. First, the 42 observations aren't the full\nstate: they don't include, for example, the previous steering command, which the expert's filter\ndepends on. Second, the policy that would match the expert would have to be **multimodal** (for the\nsame observation, sometimes accelerate and sometimes reverse), and a Gaussian policy like part 2's can\nonly have one mode per action. GAIL can't invent the missing information or give the policy a shape it\ndoesn't have.\n\n---\n\n## 16. GAIL as an adversarial problem\n\nPutting both parts together, GAIL can be written as:\n\n$$\n\\min_{\\pi}\\ \\max_{D}\\ \\ \\mathbb{E}_{\\pi_E}\\big[\\log D(s,a)\\big] + \\mathbb{E}_{\\pi}\\big[\\log\\big(1 - D(s,a)\\big)\\big]\n$$\n\nThe discriminator tries to maximize its ability to tell the expert from the agent, and the policy\ntries to make that distinction hard. (Ho and Ermon's original formulation adds a policy entropy term;\nin ML-Agents, the PPO entropy term we saw in part 2 plays a similar, though not identical, role. Ho\nand Ermon also define $D$ the other way around, as the probability that the pair comes from the\nagent; the resulting reward is equivalent.)\n\nIt isn't exactly a GAN applied to trajectories: as we saw in section 3, the environment sits between\nthe policy and what the discriminator sees, which is why the policy learns with RL.\n\n---\n\n## 17. Why not just copy the actions?\n\nImagine that in some situation the expert steers with `steer = 0.35`, and our agent, in a very similar\nsituation, gets practically the same result with `steer = 0.32`.\n\nBC would directly penalize that difference. GAIL asks something else: does the resulting behavior\nlook like the expert's? If 0.03 differences are normal within what the expert itself does, the\ndiscriminator has no way of using them.\n\nIn ML-Agents this has an explicit knob. With `use_actions` off, the discriminator doesn't even see the\nactions: it only compares the states each one goes through, and it's enough for the agent to reach the\nsame places in the same way. With `use_actions` on, as in Agentic Racing's configuration, it compares\nfull pairs, but it's still comparing distributions, not actions one by one.\n\nThat's why GAIL can capture something closer to a **style** of behavior.\n\n---\n\n## 18. But GAIL isn't magic either\n\nGAIL can run into trouble:\n\n- the discriminator can learn too fast and leave the policy without a useful signal;\n- or too slowly, and then its reward doesn't distinguish anything;\n- the demonstrations can be insufficient or cover few situations;\n- the expert can be suboptimal;\n- the observations may not contain what the expert uses to decide;\n- adversarial training can be unstable.\n\nAnd there's a less obvious one, which in Agentic Racing would have been concrete. As we saw in\nsection 11, the GAIL reward is never negative. In a task where the episode ends when the agent fails,\na reward that's always positive per step means that **surviving longer pays more**, regardless of\nwhether the agent looks like the expert. Kostrikov et al. (2019) describe this bias in detail.\n\nIn part 1 we saw what happens in Agentic Racing when the reward has an easy way out: in the fourth run,\nthe agent learned to stop on purpose because that paid more. With GAIL, the temptation would go the\nother way: stretching the episode out.\n\nAnd that effect wouldn't have been small. The configuration's 0.15 looks like a low weight next to the\nenvironment reward's 1.0, but you have to look at the scale. With an undecided discriminator\n($D = 0.5$), GAIL pays $0.15 \\times \\log 2 \\approx 0.10$ per decision, on every decision. The designed\nreward, driving well at 21 m/s, pays at most about 0.07 per decision (0.04 for progress, 0.025 for\ntarget speed and almost nothing for the racing line). In other words: per step, GAIL would have\nweighed as much as or more than the whole designed reward. And since any episode end cuts off that\nfuture reward, finishing the lap sooner would also have cost the agent something; the lap bonus, of\n12 to 20, would have had to make up for it. The no-progress cutoff (less than 8 meters in 5 seconds\nends the episode) limits the worst version of the problem, which is standing still, but doesn't remove\nit. It's exactly the kind of thing you'd have to measure before trusting the result.\n\nWhich brings us to the important question:\n\n> How do we know whether the agent is really learning to imitate the expert?\n\nWatching the discriminator's loss change isn't enough. You have to look at the behavior.\n\n---\n\n## 19. How do we evaluate GAIL?\n\nML-Agents reports several GAIL-specific metrics in TensorBoard: the discriminator loss\n(`Losses/GAIL Loss`), the gradient penalty, and the two most useful ones for watching the game play\nout: `Policy/GAIL Expert Estimate` and `Policy/GAIL Policy Estimate`, the discriminator's mean output\non the demonstrations and on the agent's experiences. If those two curves get closer, the agent is\nbecoming hard to tell apart. If the agent's estimate stays stuck at 0, the discriminator has won and\nthe policy isn't getting any signal.\n\nBut, as in part 3, the algorithm's metrics aren't enough. In Agentic Racing you'd also have to look\nat:\n\n**Imitation metrics:**\n\n- how similar the agent's trajectories are to the expert's;\n- the action distribution, especially the brake, which part 2's policy barely used;\n- speed on each stretch and behavior in the corners.\n\n**Driving metrics:**\n\n- fraction of the lap completed and full laps;\n- lap time;\n- why each episode ends: off track, stopped, no progress;\n- ability to recover from noisy spawns.\n\nThe project's evaluator already measures almost all the driving metrics. The imitation ones would\nhave to be built.\n\n---\n\n## 20. The expert's role is fundamental again\n\nIt's tempting to think \"GAIL learns by itself\". Not exactly. The expert is still the source of\nknowledge:\n\n```text\nexpert ──► bad demonstrations ──► GAIL ──► bad behavior\n```\n\nGAIL doesn't know the expert is wrong. That's why everything we saw in part 3 about demonstrations\nstill applies, and in Agentic Racing it applies with the same details: the recordings would always\nstart from clean spawns, would cover a single circuit, and the expert has recovery behaviors that\ndepend on timers. For GAIL, coverage also matters for another reason: the discriminator can only\nrecognize as \"the expert's\" what appears in the demonstrations.\n\n> The quality of the demonstrations matters, for GAIL too.\n\n---\n\n## 21. A heuristic expert can be enough\n\nOne of the most interesting parts of the project is that you don't need a human driver:\n\n```text\nheuristic controller ──► demonstrations ──► GAIL ──► neural policy\n```\n\nThis lets you use a traditional controller as a teacher. And an idea shows up that I find especially\nvaluable from an engineering point of view:\n\n> We can use traditional software to generate the data that teaches a machine learning model.\n\nThere's no need to replace the traditional system right away. It can be the source of knowledge. In\nAgentic Racing, in fact, that controller ended up being the demo's pilot, with an LLM strategist\ntelling it how to drive through directives. The network that would have learned from it never came to\nexist, but the pattern still holds.\n\n---\n\n## 22. BC + GAIL\n\nNow we can combine the two imitation methods:\n\n- **BC:** \"copy the expert\".\n- **GAIL:** \"make your behavior look like the expert's\".\n\nThe classic intuition is sequential: BC provides a reasonable initial policy, and GAIL keeps refining\nit while it interacts with the environment, especially in the states where BC fails because it left\nthe demonstrations' distribution.\n\nIn ML-Agents the combination isn't sequential but **simultaneous**. As we saw in part 3, BC runs inside\ntraining with a weight that fades over time; GAIL, on the other hand, is a reward that lasts the whole\nrun. The configuration prepared for Agentic Racing combined them like this:\n\n| Piece | Configuration | What it does |\n|---|---|---|\n| Environment reward | `strength: 1.0`, `gamma: 0.995` | the designed reward from part 1 |\n| GAIL | `strength: 0.15`, `use_actions: true` | imitation reward for the whole run |\n| BC | `strength: 0.5`, `steps: 2000000` | initial push toward the expert: its learning rate is half of PPO's and fades out at 2 million steps |\n| PPO | the one from part 2 | optimizes the sum of the rewards |\n\nThe demonstrations for BC and for GAIL would have been the same: the ones recorded by\n`eval.exe -record`.\n\n---\n\n## 23. And where does PPO come in?\n\nThe series starts connecting all its pieces. The most common intuition is a chain: BC to get started,\nGAIL to refine the imitation, and PPO to optimize the reward. In ML-Agents, the real structure is\ndifferent: **PPO is the only RL algorithm**, and BC and GAIL are two sources of knowledge plugged into\nit. GAIL only changes the reward PPO sees; BC also takes its own gradient steps on the policy's weights,\nwith its own optimizer, after each PPO update.\n\n![How BC, GAIL and the environment reward combine in ML-Agents with the configuration prepared for Agentic Racing: the demonstrations feed both the BC update and the GAIL discriminator; the environment reward (weight 1.0) and GAIL's (weight 0.15) each have their own value estimate in the critic; PPO combines their advantages and updates the policy, while BC pulls toward the expert with a weight that fades out at 2 million steps.](/images/gail-bc-ppo.svg)\n\nThere's an internal ML-Agents detail worth mentioning. Each reward has its own \"head\" in the critic,\nits own discount factor and its own GAE computation. Then the advantages of all the rewards **are\naveraged** to form the advantage PPO uses. So GAIL's weight isn't just the configuration's 0.15: it\nalso depends on the scale of its reward compared to the environment's.\n\nThis leads to a more interesting question than \"which algorithm is better?\":\n\n> How do we combine different sources of knowledge to build a better policy?\n\n---\n\n## 24. A possible experiment with Agentic Racing\n\nWe could compare four strategies, all on the fixed circuit:\n\n| Experiment | Learning signals | In ML-Agents |\n|---|---|---|\n| A. PPO from scratch | environment reward | `extrinsic` |\n| B. BC + PPO | environment reward + BC | `extrinsic` + `behavioral_cloning` |\n| C. GAIL | imitation reward only | `gail` |\n| D. BC + GAIL + PPO | everything | the configuration prepared for the eighth run, adapted to the fixed circuit |\n\nAnd measure, for each one:\n\n| Metric | A | B | C | D |\n|---|:---:|:---:|:---:|:---:|\n| Lap fraction | ? | ? | ? | ? |\n| Laps completed | ? | ? | ? | ? |\n| Lap time | ? | ? | ? | ? |\n| Track exits | ? | ? | ? | ? |\n| Similarity to the expert | ? | ? | ? | ? |\n| Recovery from noisy spawns | ? | ? | ? | ? |\n\n**This is a proposed experiment.** None of those cells has a value, because none of this was run. We\ndo know something about experiment A from the previous parts: on the procedural tracks, the lap\nfraction stayed around 10%. It was never tried on the fixed circuit.\n\nThe point wouldn't be to prove that one technique \"wins\", but to understand **why** each strategy\nworks or fails.\n\n---\n\n## 25. The real challenge: what does \"looking like the expert\" mean?\n\nSuppose two policies that complete the track. A follows the expert's trajectory exactly. B takes\ndifferent lines, but gets better times. Which is better?\n\nIt depends on the goal. If we want to **imitate the expert**, A. If we want to **win the race**, B.\n\nThat's the fundamental difference between imitation learning and reinforcement learning. GAIL brings\nus closer to the expert's behavior. PPO, with the environment reward, can take us away from it if\ndoing so pays more. And that may be exactly what we want.\n\nIn Agentic Racing, that tension would take a very concrete form: the heuristic pilot brakes according\nto a fixed rule, the target speed computed from the upcoming corner. A policy that looked too much\nlike the expert would inherit that rule. One that optimized only the reward could find a better one,\nor, as in part 1, an easy way out.\n\n---\n\n## 26. Imitating doesn't mean surpassing\n\nThis is probably the most important idea in the article. If the expert has a certain level, GAIL tries\nto learn behavior similar to that level. GAIL's goal is imitation, not improvement.\n\nTo surpass the expert you need another signal saying \"this behavior is better\". That signal is the\nenvironment reward, and the relative weight of each one decides which way the policy pulls. With 1.0\nfor the environment and 0.15 for GAIL, as in Agentic Racing's configuration, the intent was to use\nimitation as a guide, not as the goal. But, as we saw in section 18, intent and effect don't have to\nmatch: given the scale of each reward, GAIL could have ended up in charge.\n\nThat's why combining imitation learning and reinforcement learning can be much more powerful than\neither one alone.\n\n---\n\n## 27. A way to understand the whole series\n\nSo far we've asked four questions:\n\n| Part | Question | Core idea |\n|---|---|---|\n| [1. Reinforcement Learning](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/) | Can it learn by trial and error? | environment → reward → policy |\n| [2. PPO](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/) | How do we improve the policy without destroying what it learned? | controlled updates |\n| [3. Behavioral Cloning](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/) | Can we teach it by watching an expert? | expert → examples → policy |\n| 4. GAIL | Can we make its behavior look like the expert's? | discriminator → imitation reward → policy |\n\nAnd now we can ask the next one:\n\n> Can we combine all of these ideas?\n\nThat is the question of [part 5](/blog/can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system/).\n\n---\n\n## Conclusion\n\nGAIL introduces a powerful idea:\n\n> We don't need to teach the agent every correct action: we can teach it what it means to behave like\n> an expert.\n\nBehavioral Cloning learns directly from $(s, a)$ pairs. GAIL trains a discriminator that tries to\ntell $\\rho_E(s, a)$ apart from $\\rho_\\pi(s, a)$ and turns that competition into a reward that pulls the\npolicy toward the expert's behavior. The difference comes down to two questions:\n\n- **Behavioral Cloning:** what did the expert do here?\n- **GAIL:** does my behavior look like the expert's?\n\nBut GAIL doesn't remove the challenges. It still needs good demonstrations, good observations, a\nsuitable environment, stable training and careful evaluation. And, like any reward, its own can have\nshortcuts.\n\nIn Agentic Racing, GAIL stayed in a configuration that was never launched. But that configuration says\na lot about the intent: the designed reward as the main signal, imitation as a guide with weight\n0.15, and BC as an initial push. It was a bet that the expert would show the agent the way and the\nreward would take it further. And it also leaves a lesson: a small weight in the configuration doesn't\nguarantee a small influence, because what counts is the scale of each signal.\n\n> Imitating the expert can teach us to behave like it. Learning from rewards could let us go beyond\n> what the expert knows how to do.\n\nThat's where Behavioral Cloning, GAIL and PPO start to become pieces of the same system.\n\nIf you landed directly on this article, the three previous parts are linked at the top, and\n[Agentic Racing: A Pilot That Never Learned to Drive](/blog/agentic-racing-a-pilot-that-never-learned-to-drive/)\ntells the project's full story.\n\n---\n\n## References\n\n- Ho, J. and Ermon, S. (2016). [Generative Adversarial Imitation Learning](https://arxiv.org/abs/1606.03476). *NeurIPS*.\n- Goodfellow, I. et al. (2014). [Generative Adversarial Nets](https://arxiv.org/abs/1406.2661). *NeurIPS*.\n- Gulrajani, I. et al. (2017). [Improved Training of Wasserstein GANs](https://arxiv.org/abs/1704.00028). *NeurIPS*. (The gradient penalty used by ML-Agents' discriminator.)\n- Kostrikov, I. et al. (2019). [Discriminator-Actor-Critic: Addressing Sample Inefficiency and Reward Bias in Adversarial Imitation Learning](https://arxiv.org/abs/1809.02925). *ICLR*.\n- Ross, S., Gordon, G. and Bagnell, J. A. (2011). [A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning](https://arxiv.org/abs/1011.0686). *AISTATS*.\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), in\n  particular GAIL's reward and discriminator\n  (`ml-agents/mlagents/trainers/torch_entities/components/reward_providers/gail_reward_provider.py`)\n  and how reward signals are combined in the PPO trainer (`ml-agents/mlagents/trainers/ppo/trainer.py`)\n  from the `mlagents` 1.1.0 release.\n- The training configuration, the expert and the project's log are at\n  [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero","lang":"es","title":"¿Podemos aprender a comportarnos como un experto? GAIL explicado desde cero","description":"Cuarta parte de la serie Reinforcement Learning desde cero con Agentic Racing. Qué es GAIL, cómo un discriminador convierte la imitación en una recompensa aprendida, qué hace exactamente ML-Agents con ella, y qué habría pasado si el proyecto hubiera llegado a usar la configuración de GAIL que preparó para su piloto.","publishDate":"2026-10-07","updatedDate":"2026-10-09","tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch","url":"https://alexisalulema.com/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/","series":{"id":"rl-from-scratch","position":4},"headings":[{"depth":2,"slug":"dónde-nos-quedamos","text":"Dónde nos quedamos"},{"depth":3,"slug":"una-advertencia-honesta-como-en-las-partes-anteriores","text":"Una advertencia honesta, como en las partes anteriores"},{"depth":2,"slug":"1-de-copiar-acciones-a-imitar-comportamientos","text":"1. De copiar acciones a imitar comportamientos"},{"depth":2,"slug":"2-la-idea-del-discriminador","text":"2. La idea del discriminador"},{"depth":2,"slug":"3-por-qué-esto-recuerda-a-una-gan","text":"3. ¿Por qué esto recuerda a una GAN?"},{"depth":2,"slug":"4-importa-el-comportamiento-no-una-acción-aislada","text":"4. Importa el comportamiento, no una acción aislada"},{"depth":2,"slug":"5-una-intuición-con-agentic-racing","text":"5. Una intuición con Agentic Racing"},{"depth":2,"slug":"6-el-discriminador","text":"6. El discriminador"},{"depth":2,"slug":"7-el-objetivo-del-discriminador","text":"7. El objetivo del discriminador"},{"depth":2,"slug":"8-qué-quiere-conseguir-la-policy","text":"8. ¿Qué quiere conseguir la policy?"},{"depth":2,"slug":"9-el-ciclo-de-aprendizaje","text":"9. El ciclo de aprendizaje"},{"depth":2,"slug":"10-gail-sigue-siendo-reinforcement-learning","text":"10. GAIL sigue siendo reinforcement learning"},{"depth":2,"slug":"11-una-recompensa-aprendida","text":"11. Una recompensa aprendida"},{"depth":2,"slug":"12-entonces-gail-elimina-la-necesidad-de-diseñar-recompensas","text":"12. Entonces, ¿GAIL elimina la necesidad de diseñar recompensas?"},{"depth":2,"slug":"13-cuál-es-la-diferencia-con-behavioral-cloning","text":"13. ¿Cuál es la diferencia con Behavioral Cloning?"},{"depth":2,"slug":"14-el-problema-de-bc-que-gail-ataca","text":"14. El problema de BC que GAIL ataca"},{"depth":2,"slug":"15-occupancy-measures-la-idea-matemática-detrás-de-gail","text":"15. Occupancy measures: la idea matemática detrás de GAIL"},{"depth":2,"slug":"16-gail-como-problema-adversarial","text":"16. GAIL como problema adversarial"},{"depth":2,"slug":"17-por-qué-no-simplemente-copiar-las-acciones","text":"17. ¿Por qué no simplemente copiar las acciones?"},{"depth":2,"slug":"18-pero-gail-tampoco-es-magia","text":"18. Pero GAIL tampoco es magia"},{"depth":2,"slug":"19-cómo-evaluar-gail","text":"19. ¿Cómo evaluar GAIL?"},{"depth":2,"slug":"20-el-papel-del-experto-vuelve-a-ser-fundamental","text":"20. El papel del experto vuelve a ser fundamental"},{"depth":2,"slug":"21-un-experto-heurístico-puede-ser-suficiente","text":"21. Un experto heurístico puede ser suficiente"},{"depth":2,"slug":"22-bc--gail","text":"22. BC + GAIL"},{"depth":2,"slug":"23-y-dónde-entra-ppo","text":"23. ¿Y dónde entra PPO?"},{"depth":2,"slug":"24-un-posible-experimento-con-agentic-racing","text":"24. Un posible experimento con Agentic Racing"},{"depth":2,"slug":"25-el-verdadero-reto-qué-significa-parecerse-al-experto","text":"25. El verdadero reto: ¿qué significa “parecerse al experto”?"},{"depth":2,"slug":"26-imitar-no-significa-superar","text":"26. Imitar no significa superar"},{"depth":2,"slug":"27-una-forma-de-entender-toda-la-serie","text":"27. Una forma de entender toda la serie"},{"depth":2,"slug":"conclusión","text":"Conclusión"},{"depth":2,"slug":"referencias","text":"Referencias"}],"body":"> **Serie: Reinforcement Learning desde cero, con Agentic Racing como laboratorio**\n>\n> 1. Parte 1 · [¿Cómo aprende un agente? Enseñando a un automóvil a conducir](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/)\n> 2. Parte 2 · [¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/)\n> 3. Parte 3 · [¿Podemos enseñar imitando? Behavioral Cloning explicado desde cero](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/)\n> 4. Parte 4 · **¿Podemos aprender a comportarnos como un experto? GAIL explicado desde cero** (este artículo)\n> 5. Parte 5 · [¿Podemos combinar imitación y RL? BC, GAIL y PPO en un mismo sistema](/es/blog/podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema/)\n> 6. Parte 6 · [¿Cómo le decimos a un agente qué queremos? Reward engineering explicado desde cero](/es/blog/como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero/)\n> 7. Parte 7 · [¿Qué debería poder ver un agente? Observation design explicado desde cero](/es/blog/que-deberia-poder-ver-un-agente-observation-design-explicado-desde-cero/)\n> 8. Parte 8 · [¿Qué hacemos cuando el agente no aprende? RL debugging explicado desde cero](/es/blog/que-hacemos-cuando-el-agente-no-aprende-rl-debugging-explicado-desde-cero/)\n\n## Dónde nos quedamos\n\nLa [parte 3](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/) hizo una\npregunta sencilla: ¿podemos enseñar a un agente imitando a un experto? La respuesta fue sí. Con\n**Behavioral Cloning (BC)** recogemos ejemplos $(s_i, a_i)$ y entrenamos una policy para que:\n\n$$\n\\pi_\\theta(s_i) \\approx a_i\n$$\n\nPero apareció un problema: si el agente llega a un estado que no estaba en las demostraciones, puede\nno saber qué hacer, y cada error lo lleva a estados donde se equivoca más. Y en Agentic Racing\napareció otro: el experto, el piloto heurístico del proyecto, decide con memoria y temporizadores que\nla policy no ve, así que hay situaciones en las que ninguna red sin memoria puede copiar su acción\nexacta.\n\nEso abre una pregunta más interesante:\n\n> ¿Realmente necesitamos decirle al agente qué acción tomar en cada estado?\n\nQuizá podamos enseñarle algo distinto:\n\n> \"Quiero que tu comportamiento se parezca al del experto.\"\n\nEsa es la idea de **Generative Adversarial Imitation Learning (GAIL)**, que la parte 3 dejó anunciada\ncomo la siguiente parada de la serie.\n\n### Una advertencia honesta, como en las partes anteriores\n\nEn Agentic Racing, GAIL corrió la misma suerte que BC: **se configuró pero nunca se ejecutó**. La\nconfiguración que se preparó para la octava corrida tenía un bloque de GAIL junto al de BC; se quitó\nantes de lanzarla, y esa corrida se hizo con PPO limpio. La bitácora no registra ninguna demostración\ngrabada, así que tampoco hay discriminador entrenado, ni métricas de GAIL, ni comparación entre BC y\nGAIL que mostrar.\n\nLo que sí hay es suficiente para hacer este artículo concreto: el experto, el grabador de\ndemostraciones, la configuración que se escribió y el código de ML-Agents que la habría ejecutado. Así\nque, como en la parte 3, la teoría va acompañada de lo que el proyecto realmente tenía y de lo que\nhabría pasado con ello. Las tablas de resultados de este artículo son experimentos propuestos, no\nresultados.\n\n---\n\n## 1. De copiar acciones a imitar comportamientos\n\nBehavioral Cloning puede verse así:\n\n```text\n       observación\n            │\n            ▼\n   ¿qué hizo el experto?\n            │\n            ▼\n    acción del experto\n            │\n            ▼\n        entrenar π\n```\n\nPor ejemplo:\n\n```text\nobservación:\ncurva a la derecha\nvelocidad alta\nauto ligeramente desplazado\n\n             ↓\n\nexperto:\nsteer    = 0.45\nthrottle = 0.30\nbrake    = 0.10\n```\n\nBC intenta aprender esa correspondencia. GAIL plantea otra pregunta:\n\n> ¿El comportamiento que produce nuestro agente se parece al del experto?\n\nY para responderla introduce un componente nuevo: **un discriminador**.\n\n---\n\n## 2. La idea del discriminador\n\nImaginemos dos conductores, el experto y el agente, y un juez que mira lo que hace cada uno:\n\n```text\n        EXPERTO                 AGENTE\n           │                       │\n     comportamiento          comportamiento\n           │                       │\n           └──────────┬────────────┘\n                      ▼\n              ┌───────────────┐\n              │ Discriminador │\n              └───────┬───────┘\n                      ▼\n          ¿del experto o del agente?\n```\n\nEl discriminador intenta distinguir el comportamiento del experto del comportamiento del agente. Si\nrecibe una experiencia del experto, debería responder \"experto\"; si recibe una del agente, \"agente\".\n\nLa parte interesante es que el agente también está aprendiendo, y su objetivo es que esa\nclasificación sea cada vez más difícil. Tenemos un juego:\n\n```text\nDiscriminador: \"quiero distinguirlos\".\nAgente:        \"quiero que no puedas distinguirme del experto\".\n```\n\n---\n\n## 3. ¿Por qué esto recuerda a una GAN?\n\nEl nombre no es casualidad. GAIL está inspirado en las **Generative Adversarial Networks (GANs)**. En\nuna GAN, simplificando:\n\n```text\ngenerador ──► datos generados ──► discriminador ──► ¿real o generado?\n```\n\nEl generador intenta producir datos que parezcan reales, y el discriminador intenta distinguir lo real\nde lo generado.\n\nGAIL hace algo parecido, con una diferencia fundamental:\n\n> En GAIL, el \"generador\" no genera imágenes ni vectores directamente. La policy genera\n> **comportamiento interactuando con el entorno**.\n\nLa policy no produce una muestra aislada. Produce una secuencia:\n\n$$\n(s_0, a_0, s_1, a_1, s_2, a_2, \\ldots)\n$$\n\ny los estados de esa secuencia no los elige ella: los produce la física del entorno a partir de sus\nacciones. Esa es la razón por la que GAIL no se puede entrenar como una GAN normal, derivando a través\ndel generador. Necesita reinforcement learning.\n\n---\n\n## 4. Importa el comportamiento, no una acción aislada\n\nBC trabaja con la correspondencia $s_t \\rightarrow a_t$. GAIL trabaja con pares $(s_t, a_t)$, pero\nmirados como una **distribución**: con qué frecuencia la policy visita cada estado y qué acciones toma\nahí.\n\nImaginemos un tramo de pista:\n\n```text\nexperto: frena 0.30 → gira 0.42 → acelera 0.70 → corrige −0.05\nagente:  frena 0.28 → gira 0.45 → acelera 0.66 → corrige −0.08\n```\n\nNinguna acción del agente es igual a la del experto, pero el comportamiento es casi el mismo: frena,\ngira, acelera y corrige en los mismos lugares y con magnitudes parecidas. BC penalizaría cada una de\nesas pequeñas diferencias. Un discriminador, si son diferencias del tamaño de la variación normal del\nexperto, no tendría cómo usarlas para distinguirlos.\n\nEso es lo que GAIL intenta capturar.\n\n---\n\n## 5. Una intuición con Agentic Racing\n\nEl circuito de Agentic Racing hoy es un rectángulo de esquinas redondeadas de unos 2 km, con cuatro\ncurvas iguales. El experto, el piloto heurístico de la parte 3, lo recorre vuelta tras vuelta. Cada\nvuelta deja una trayectoria:\n\n$$\n\\tau_{\\text{experto}} = \\big((s_0, a_0),\\ (s_1, a_1),\\ \\ldots,\\ (s_T, a_T)\\big)\n$$\n\ndonde cada $s_t$ son los mismos 42 números de observación que recibe la policy (los de la parte 1) y\ncada $a_t$ son las mismas tres acciones continuas: `steer`, `throttle` y `brake`.\n\nAhora dejamos conducir al agente, que produce sus propias trayectorias $\\tau_{\\text{agente}}$. GAIL\nentrena un discriminador para distinguir los pares de una y otra.\n\nAl principio es fácil: el agente recién inicializado zigzaguea, frena en las rectas o acelera en las\ncurvas, y el discriminador lo detecta enseguida. A medida que la policy mejora, sus pares se parecen\ncada vez más a los del experto, y el discriminador empieza a dudar. El objetivo es llegar a un punto\nen que el discriminador tenga dificultades para diferenciarlos.\n\n---\n\n## 6. El discriminador\n\nFormalmente, el discriminador es una función:\n\n$$\nD_\\phi(s, a) \\in (0, 1)\n$$\n\ndonde $s$ es el estado, $a$ la acción y $\\phi$ los parámetros de una red neuronal. Su salida se\ninterpreta como la probabilidad de que el par venga del experto.\n\nPor ejemplo, al principio del entrenamiento:\n\n```text\nD(par del experto) = 0.95\nD(par del agente)  = 0.08\n```\n\nEl discriminador está muy seguro. Más adelante:\n\n```text\nD(par del experto) = 0.60\nD(par del agente)  = 0.45\n```\n\nLa distinción se vuelve más difícil.\n\nEn ML-Agents 1.1.0, el discriminador es una red densa que recibe la observación y, si la opción\n`use_actions` está activa, también la acción y una marca que indica si el episodio terminó en ese\npaso. Por defecto tiene 2 capas de 128 neuronas y una salida con función sigmoide, que es la\nprobabilidad anterior. La configuración que se preparó para Agentic Racing activaba `use_actions`, así\nque el discriminador habría comparado pares observación–acción completos.\n\n---\n\n## 7. El objetivo del discriminador\n\nEl discriminador se entrena como un clasificador binario:\n\n$$\n\\max_{D}\\ \\ \\mathbb{E}_{\\pi_E}\\big[\\log D(s,a)\\big] + \\mathbb{E}_{\\pi}\\big[\\log\\big(1 - D(s,a)\\big)\\big]\n$$\n\ndonde $\\pi_E$ es el comportamiento del experto y $\\pi$ el del agente:\n\n```text\npares del experto ──► D debería dar valores altos\npares del agente  ──► D debería dar valores bajos\n```\n\nEn ML-Agents, la pérdida del discriminador es esa expresión con el signo cambiado (salvo un pequeño\nmargen numérico dentro de los logaritmos). Además, le suma una **penalización de gradiente** con peso\n10: evalúa el discriminador en puntos intermedios entre pares del experto y del agente, y castiga que\nla norma de su gradiente respecto de la entrada se aleje de 1. Es una técnica tomada de las GANs\n(Gulrajani et al., 2017) para que el discriminador no se vuelva tan tajante que deje de dar una señal\nútil. El propio código explica para qué está: \"agrega estabilidad, sobre todo off-policy\" (PPO es\non-policy, así que aquí es una protección más que una necesidad).\n\nPero lo importante es que **la policy también aprende**.\n\n---\n\n## 8. ¿Qué quiere conseguir la policy?\n\nLa policy quiere generar comportamiento que parezca del experto:\n\n```text\ndiscriminador ──► \"esto parece del agente\" ──► señal ──► policy\n      ▲                                                     │\n      │                                                     ▼\n      └──────────────── nuevo comportamiento ◄──────────────┘\n```\n\nLa policy cambia, el comportamiento cambia, el discriminador vuelve a entrenarse con ese nuevo\ncomportamiento, y el proceso continúa. Es un juego adversarial.\n\n---\n\n## 9. El ciclo de aprendizaje\n\nEl ciclo completo es:\n\n1. el agente interactúa con el entorno;\n2. genera trayectorias;\n3. el discriminador evalúa esos pares;\n4. su salida se convierte en una recompensa;\n5. la policy se actualiza con esa recompensa;\n6. el discriminador se actualiza para distinguir mejor;\n7. el agente vuelve a interactuar.\n\n![El ciclo de GAIL en ML-Agents: el agente conduce y llena el buffer; el discriminador puntúa cada par observación–acción y esa puntuación se convierte en la recompensa de GAIL, que se suma a la del entorno; PPO actualiza la policy, y en cada minibatch el discriminador da un paso para distinguir mejor las demostraciones del experto de las experiencias del agente.](/images/gail-loop.svg)\n\nEn ML-Agents, los pasos 5 y 6 ocurren entrelazados. En cada minibatch de la actualización de PPO (los\n30 de la parte 2), primero se actualiza la policy y después el discriminador da un paso con ese mismo\nminibatch de experiencias del agente y una muestra del mismo tamaño de las demostraciones. Policy y\ndiscriminador avanzan a la par.\n\n---\n\n## 10. GAIL sigue siendo reinforcement learning\n\nEsta distinción es importante: GAIL **no reemplaza** al reinforcement learning. Lo que cambia es la\nfuente de la señal de aprendizaje.\n\nEn RL tradicional:\n\n```text\nentorno ──► recompensa ──► policy\n```\n\nEn GAIL:\n\n```text\nexperto + comportamiento del agente ──► discriminador ──► recompensa de imitación ──► policy\n```\n\nLa policy sigue aprendiendo interactuando con el entorno, con un algoritmo de RL. En ML-Agents ese\nalgoritmo es PPO, el mismo de la parte 2: para PPO, GAIL es simplemente **una señal de recompensa\nmás**. La diferencia es que esta recompensa no la diseñamos nosotros: la construye el discriminador.\n\n---\n\n## 11. Una recompensa aprendida\n\nEsta es una de las ideas más poderosas de GAIL. En la parte 1 diseñamos a mano una recompensa con\nnueve componentes (progreso, velocidad objetivo, cercanía al borde, vuelta completa…) y vimos cuántas\nversiones hicieron falta y cuántas veces el agente encontró atajos. GAIL propone otra cosa: que la\nrecompensa salga de comparar al agente con el experto.\n\nML-Agents 1.1.0 la calcula así, para cada paso:\n\n$$\nr_{\\text{GAIL}}(s, a) = -\\log\\big(1 - D(s, a)\\big)\n$$\n\n(con un pequeño margen numérico para que nunca sea infinita).\n\nLa lectura es directa:\n\n- si el discriminador cree que el par es del agente, $D \\approx 0$ y la recompensa es casi 0;\n- si cree que es del experto, $D \\rightarrow 1$ y la recompensa crece;\n- con $D = 0.5$, cuando no puede decidir, la recompensa es $\\log 2 \\approx 0.69$.\n\nHay un detalle que conviene notar: esta recompensa **nunca es negativa**. Volveremos a eso en la\nsección 18.\n\n```text\nel agente actúa\n       ↓\n discriminador\n       ↓\n\"¿parece del experto?\"\n       ↓\n   recompensa\n       ↓\n     policy\n```\n\n---\n\n## 12. Entonces, ¿GAIL elimina la necesidad de diseñar recompensas?\n\nNo del todo. Es una interpretación demasiado simplificada.\n\nGAIL aprende una señal de imitación a partir de demostraciones, pero el sistema completo sigue\nteniendo un entorno, observaciones, acciones, un algoritmo de RL, hiperparámetros y, casi siempre,\notras recompensas. Y aprender de un experto no garantiza que el comportamiento aprendido sea óptimo:\n\n> Si el experto conduce mal, GAIL puede aprender a conducir como ese experto.\n\nEn la práctica, GAIL casi nunca se usa solo. La configuración que se preparó para Agentic Racing\nmantenía toda la recompensa diseñada de la parte 1, con peso 1.0, y le agregaba la de GAIL con un peso\nde 0.15. GAIL no reemplazaba la recompensa: la complementaba.\n\n---\n\n## 13. ¿Cuál es la diferencia con Behavioral Cloning?\n\n| Característica | Behavioral Cloning | GAIL |\n|---|---|---|\n| Tipo de aprendizaje | supervisado | imitación mediante RL |\n| ¿Aprende directamente acciones? | sí | no: aprende a producir una distribución parecida |\n| ¿Usa demostraciones? | sí | sí |\n| ¿Usa discriminador? | no | sí |\n| ¿Necesita interactuar con el entorno? | no | sí |\n| ¿Aprende en los estados que visita la propia policy? | no | sí |\n| ¿Usa una recompensa? | no | sí, una aprendida |\n\nEn una frase cada uno:\n\n- **Behavioral Cloning:** \"haz lo mismo que hizo el experto en esta situación\".\n- **GAIL:** \"produce un comportamiento que se parezca al del experto\".\n\nLa diferencia parece pequeña, pero conceptualmente es enorme.\n\n---\n\n## 14. El problema de BC que GAIL ataca\n\nRecordemos los errores acumulativos de la parte 3. BC aprende de los estados que visitó el experto,\npero después la policy visita los suyos:\n\n```text\ntrayectoria del experto  ─────────────────────►\n\ntrayectoria del agente   ───────────╮\n                                    ╰────────► fuera de la distribución\n```\n\nGAIL tiene una ventaja conceptual: **la policy genera sus propios estados interactuando con el\nentorno**, y el discriminador también los evalúa. Si la policy se va a un estado que el experto nunca\nvisitó, el discriminador lo reconoce como \"del agente\" y la recompensa de GAIL baja. La forma de\nvolver a ganar recompensa es volver a zonas que se parezcan a las del experto. Es decir, GAIL no solo\nle enseña al agente qué hacer donde estuvo el experto: le da un motivo para **regresar** ahí.\n\nEn Agentic Racing eso habría sido especialmente relevante. Como vimos en la parte 3, las demostraciones\nse grabarían siempre desde arranques limpios (centrados, alineados, a 14 m/s), mientras que el\nentrenamiento arranca con ruido de rumbo y de posición. Con BC, esos primeros estados quedarían fuera\nde lo que enseñan las demostraciones. Con GAIL, la recompensa empujaría a la policy a llegar cuanto\nantes a estados parecidos a los del experto.\n\nEso no significa que GAIL elimine el problema mágicamente. Significa que el entrenamiento tiene en\ncuenta la distribución de estados que genera la propia policy, que es justo lo que BC no hacía.\n\n---\n\n## 15. Occupancy measures: la idea matemática detrás de GAIL\n\nUna forma elegante de entender GAIL es a través de las **occupancy measures**. Simplificando, una\npolicy induce una distribución sobre los pares estado–acción que visita:\n\n$$\n\\rho_\\pi(s, a)\n$$\n\ny el experto induce la suya:\n\n$$\n\\rho_E(s, a)\n$$\n\nGAIL busca una policy cuya distribución se parezca a la del experto:\n\n$$\n\\rho_\\pi \\approx \\rho_E\n$$\n\nHay un resultado elegante detrás (Ho y Ermon, 2016): cuando el estado es completamente observable,\ncada policy tiene una única distribución de este tipo, así que igualar las distribuciones es, en el\nfondo, igualar la policy del experto:\n\n$$\n\\pi(a \\mid s) = \\pi_E(a \\mid s)\n$$\n\nen los estados que visita el experto. Entonces, ¿qué cambia respecto de BC? **Dónde se miden los\nerrores.** BC los mide en los estados del experto; GAIL, en los estados por los que realmente pasa la\npolicy. Un error que saca a la policy de la zona del experto le cuesta poco a BC y mucho a GAIL.\n\nEsto conecta con un límite que vimos en la parte 3. Que el experto tenga memoria no es, por sí solo, un\nobstáculo: en teoría siempre existe una policy sin memoria que reproduce la misma distribución de\npares estado–acción. Lo que sí es un obstáculo en Agentic Racing es otra cosa. Primero, las 42\nobservaciones no son el estado completo: no incluyen, por ejemplo, el comando de volante anterior, del\nque depende el filtro del experto. Segundo, la policy que igualaría al experto tendría que ser\n**multimodal** (ante la misma observación, a veces acelerar y a veces dar reversa), y una policy\ngaussiana como la de la parte 2 solo puede tener una moda por acción. GAIL no puede inventar la\ninformación que falta ni darle a la policy una forma que no tiene.\n\n---\n\n## 16. GAIL como problema adversarial\n\nJuntando las dos partes, GAIL se puede escribir así:\n\n$$\n\\min_{\\pi}\\ \\max_{D}\\ \\ \\mathbb{E}_{\\pi_E}\\big[\\log D(s,a)\\big] + \\mathbb{E}_{\\pi}\\big[\\log\\big(1 - D(s,a)\\big)\\big]\n$$\n\nEl discriminador intenta maximizar su capacidad de distinguir al experto del agente, y la policy\nintenta que esa distinción sea difícil. (La formulación original de Ho y Ermon agrega un término de\nentropía de la policy; en ML-Agents, el término de entropía de PPO que vimos en la parte 2 cumple un\npapel parecido, aunque no idéntico. Además, Ho y Ermon definen $D$ al revés, como la probabilidad de\nque el par venga del agente; la recompensa resultante es equivalente.)\n\nNo es exactamente una GAN aplicada a trayectorias: como vimos en la sección 3, el entorno se interpone\nentre la policy y lo que ve el discriminador, y por eso la policy aprende con RL.\n\n---\n\n## 17. ¿Por qué no simplemente copiar las acciones?\n\nImaginemos que, en una situación, el experto gira con `steer = 0.35`, y que nuestro agente, en una\nsituación muy parecida, consigue prácticamente el mismo resultado con `steer = 0.32`.\n\nBC penalizaría directamente esa diferencia. GAIL pregunta otra cosa: ¿el comportamiento resultante se\nparece al del experto? Si las diferencias de 0.03 son normales dentro de lo que hace el propio\nexperto, el discriminador no tiene cómo usarlas.\n\nEn ML-Agents esto tiene un ajuste explícito. Con `use_actions` desactivado, el discriminador ni\nsiquiera ve las acciones: compara solo los estados por los que pasa cada uno, y al agente le basta\ncon llegar a los mismos lugares de la misma forma. Con `use_actions` activado, como en la configuración\nde Agentic Racing, compara pares completos, pero sigue comparando distribuciones, no acciones una por\nuna.\n\nPor eso GAIL puede capturar algo más cercano al **estilo** de comportamiento.\n\n---\n\n## 18. Pero GAIL tampoco es magia\n\nGAIL puede tener problemas:\n\n- el discriminador puede aprender demasiado rápido y dejar a la policy sin una señal útil;\n- o demasiado lento, y entonces su recompensa no distingue nada;\n- las demostraciones pueden ser insuficientes o cubrir pocas situaciones;\n- el experto puede ser subóptimo;\n- las observaciones pueden no contener lo que el experto usa para decidir;\n- el entrenamiento adversarial puede ser inestable.\n\nY hay uno menos obvio, que en Agentic Racing habría sido concreto. Como vimos en la sección 11, la\nrecompensa de GAIL nunca es negativa. En una tarea donde el episodio termina cuando el agente falla,\nuna recompensa siempre positiva por paso significa que **sobrevivir más tiempo da más recompensa**,\nindependientemente de si el agente se parece al experto. Kostrikov et al. (2019) describen este sesgo\nen detalle.\n\nEn la parte 1 vimos lo que pasa en Agentic Racing cuando la recompensa tiene una salida fácil: en la\ncuarta corrida, el agente aprendió a detenerse a propósito porque así cobraba más. Con GAIL, la\ntentación iría en la dirección contraria: alargar el episodio.\n\nY ese efecto no habría sido pequeño. El 0.15 de la configuración parece un peso bajo frente al 1.0 de\nla recompensa del entorno, pero hay que mirar la escala. Con el discriminador indeciso ($D = 0.5$),\nGAIL paga $0.15 \\times \\log 2 \\approx 0.10$ por decisión, en todas las decisiones. La recompensa\ndiseñada, conduciendo bien a 21 m/s, paga como mucho unos 0.07 por decisión (0.04 por progreso, 0.025\npor velocidad objetivo y casi nada por la trazada). Es decir: por paso, GAIL habría pesado tanto o más\nque toda la recompensa diseñada. Y como cualquier fin de episodio corta esa recompensa futura, también\nterminar la vuelta antes le habría costado algo al agente; el bono de vuelta, de 12 a 20, habría\ntenido que compensarlo. El corte por falta de progreso (menos de 8 metros en 5 segundos termina el\nepisodio) limita la peor versión del problema, que es quedarse quieto, pero no lo elimina. Es\nexactamente el tipo de cosa que habría que medir antes de confiar en el resultado.\n\nLo que nos lleva a la pregunta importante:\n\n> ¿Cómo sabemos si el agente realmente está aprendiendo a imitar al experto?\n\nNo basta con ver que cambie la pérdida del discriminador. Hay que mirar el comportamiento.\n\n---\n\n## 19. ¿Cómo evaluar GAIL?\n\nML-Agents reporta varias métricas propias de GAIL en TensorBoard: la pérdida del discriminador\n(`Losses/GAIL Loss`), la penalización de gradiente, y las dos más útiles para ver el juego en acción:\n`Policy/GAIL Expert Estimate` y `Policy/GAIL Policy Estimate`, la salida media del discriminador sobre\nlas demostraciones y sobre las experiencias del agente. Si esas dos curvas se acercan, el agente se\nestá volviendo difícil de distinguir. Si el estimado del agente se queda pegado en 0, el discriminador\nganó y la policy no está recibiendo señal.\n\nPero, como en la parte 3, las métricas del algoritmo no alcanzan. En Agentic Racing también habría que\nmirar:\n\n**Métricas de imitación:**\n\n- cuánto se parecen las trayectorias del agente a las del experto;\n- la distribución de las acciones, en especial del freno, que la policy de la parte 2 casi no usaba;\n- la velocidad en cada tramo y el comportamiento en las curvas.\n\n**Métricas de conducción:**\n\n- fracción de vuelta completada y vueltas completas;\n- tiempo por vuelta;\n- motivos de fin de episodio: salida de pista, auto detenido, sin progreso;\n- capacidad de recuperación desde arranques con ruido.\n\nEl evaluador del proyecto ya mide casi todas las de conducción. Las de imitación habría que\nconstruirlas.\n\n---\n\n## 20. El papel del experto vuelve a ser fundamental\n\nEs tentador pensar que \"GAIL aprende por sí mismo\". No exactamente. El experto sigue siendo la fuente\nde conocimiento:\n\n```text\nexperto ──► malas demostraciones ──► GAIL ──► mal comportamiento\n```\n\nGAIL no sabe que el experto está equivocado. Por eso todo lo que vimos en la parte 3 sobre las\ndemostraciones sigue valiendo, y en Agentic Racing vale con los mismos detalles: las grabaciones\nempezarían siempre desde arranques limpios, cubrirían un único circuito, y el experto tiene\ncomportamientos de recuperación que dependen de temporizadores. Para GAIL, además, la cobertura\nimporta por otra razón: el discriminador solo puede reconocer como \"del experto\" lo que aparece en las\ndemostraciones.\n\n> La calidad de las demostraciones importa, también para GAIL.\n\n---\n\n## 21. Un experto heurístico puede ser suficiente\n\nUna de las partes más interesantes del proyecto es que no hace falta un piloto humano:\n\n```text\ncontrolador heurístico ──► demostraciones ──► GAIL ──► neural policy\n```\n\nEsto permite usar un controlador tradicional como profesor. Y aparece una idea que me parece\nespecialmente valiosa desde el punto de vista de ingeniería:\n\n> Podemos usar software tradicional para generar los datos que enseñan a un modelo de machine\n> learning.\n\nNo hace falta reemplazar de inmediato el sistema tradicional. Puede ser la fuente de conocimiento. En\nAgentic Racing, de hecho, ese controlador terminó siendo el piloto del demo, y un estratega LLM le dice\ncómo conducir a través de las directivas. La red que habría aprendido de él nunca llegó a existir,\npero el patrón sigue siendo válido.\n\n---\n\n## 22. BC + GAIL\n\nAhora podemos combinar los dos métodos de imitación:\n\n- **BC:** \"copia al experto\".\n- **GAIL:** \"haz que tu comportamiento parezca el del experto\".\n\nLa intuición clásica es secuencial: BC da una policy inicial razonable, y GAIL la sigue refinando\nmientras interactúa con el entorno, sobre todo en los estados donde BC falla porque se salió de la\ndistribución de las demostraciones.\n\nEn ML-Agents la combinación no es secuencial, sino **simultánea**. Como vimos en la parte 3, BC corre\ndentro del entrenamiento con un peso que se apaga con el tiempo; GAIL, en cambio, es una recompensa que\ndura toda la corrida. La configuración que se preparó para Agentic Racing combinaba las dos así:\n\n| Pieza | Configuración | Qué hace |\n|---|---|---|\n| Recompensa del entorno | `strength: 1.0`, `gamma: 0.995` | la recompensa diseñada de la parte 1 |\n| GAIL | `strength: 0.15`, `use_actions: true` | recompensa de imitación durante toda la corrida |\n| BC | `strength: 0.5`, `steps: 2000000` | empuje inicial hacia el experto: su tasa de aprendizaje es la mitad de la de PPO y se apaga a los 2 millones de pasos |\n| PPO | el de la parte 2 | optimiza la suma de las recompensas |\n\nLas demostraciones para BC y para GAIL habrían sido las mismas: las que grabara `eval.exe -record`.\n\n---\n\n## 23. ¿Y dónde entra PPO?\n\nLa serie empieza a conectar todas sus piezas. La intuición más común es una cadena: BC para arrancar,\nGAIL para refinar la imitación y PPO para optimizar la recompensa. En ML-Agents, la estructura real es\notra: **PPO es el único algoritmo de RL**, y BC y GAIL son dos fuentes de conocimiento que se le\nenchufan. GAIL solo cambia la recompensa que ve PPO; BC, además, da sus propios pasos de gradiente sobre\nlos pesos de la policy, con su propio optimizador, después de cada actualización de PPO.\n\n![Cómo se combinan BC, GAIL y la recompensa del entorno en ML-Agents con la configuración preparada para Agentic Racing: las demostraciones alimentan a la vez la actualización de BC y al discriminador de GAIL; la recompensa del entorno (peso 1.0) y la de GAIL (peso 0.15) tienen cada una su propia estimación de valor en el crítico; PPO combina sus advantages y actualiza la policy, mientras BC empuja hacia el experto con un peso que se apaga a los 2 millones de pasos.](/images/gail-bc-ppo.svg)\n\nHay un detalle interno de ML-Agents que merece mención. Cada recompensa tiene su propia \"cabeza\" en el\ncrítico, su propio factor de descuento y su propio cálculo de GAE. Después, los advantages de todas las\nrecompensas **se promedian** para formar el advantage que usa PPO. Así que el peso de GAIL no es solo\nel 0.15 de la configuración: también depende de la escala de su recompensa frente a la del entorno.\n\nEsto nos lleva a una pregunta más interesante que \"¿qué algoritmo es mejor?\":\n\n> ¿Cómo combinamos distintas fuentes de conocimiento para construir una policy mejor?\n\n---\n\n## 24. Un posible experimento con Agentic Racing\n\nPodríamos comparar cuatro estrategias, todas sobre el circuito fijo:\n\n| Experimento | Señales de aprendizaje | En ML-Agents |\n|---|---|---|\n| A. PPO desde cero | recompensa del entorno | `extrinsic` |\n| B. BC + PPO | recompensa del entorno + BC | `extrinsic` + `behavioral_cloning` |\n| C. GAIL | solo recompensa de imitación | `gail` |\n| D. BC + GAIL + PPO | todo | la configuración que se preparó para la octava corrida, adaptada al circuito fijo |\n\nY medir, para cada una:\n\n| Métrica | A | B | C | D |\n|---|:---:|:---:|:---:|:---:|\n| Fracción de vuelta | ? | ? | ? | ? |\n| Vueltas completadas | ? | ? | ? | ? |\n| Tiempo por vuelta | ? | ? | ? | ? |\n| Salidas de pista | ? | ? | ? | ? |\n| Parecido con el experto | ? | ? | ? | ? |\n| Recuperación desde arranques con ruido | ? | ? | ? | ? |\n\n**Esto es un experimento propuesto.** Ninguna de esas celdas tiene un valor, porque nada de esto se\nejecutó. Del experimento A sí sabemos algo de las partes anteriores: en las pistas procedurales, la\nfracción de vuelta se quedó alrededor del 10%. En el circuito fijo nunca se probó.\n\nLo importante no sería demostrar que una técnica \"gana\", sino entender **por qué** cada estrategia\nfunciona o falla.\n\n---\n\n## 25. El verdadero reto: ¿qué significa \"parecerse al experto\"?\n\nSupongamos dos policies que completan la pista. La A sigue exactamente la trayectoria del experto. La\nB toma trayectorias diferentes, pero consigue mejores tiempos. ¿Cuál es mejor?\n\nDepende del objetivo. Si queremos **imitar al experto**, A. Si queremos **ganar la carrera**, B.\n\nEsa es la diferencia fundamental entre imitation learning y reinforcement learning. GAIL nos acerca al\ncomportamiento del experto. PPO, con la recompensa del entorno, puede alejarnos de él si hacerlo da\nmás recompensa. Y eso puede ser exactamente lo que queremos.\n\nEn Agentic Racing, esa tensión tendría una forma muy concreta: el piloto heurístico frena según una\nregla fija, la velocidad objetivo calculada a partir de la curva que viene. Una policy que se pareciera\ndemasiado al experto heredaría esa regla. Una que optimizara solo la recompensa podría encontrar una\nmejor, o, como en la parte 1, una salida fácil.\n\n---\n\n## 26. Imitar no significa superar\n\nEsta es probablemente la idea más importante del artículo. Si el experto tiene cierto nivel, GAIL\nintenta aprender un comportamiento parecido a ese nivel. El objetivo de GAIL es la imitación, no la\nmejora.\n\nPara superar al experto hace falta otra señal que diga \"este comportamiento es mejor\". Esa señal es la\nrecompensa del entorno, y el peso relativo de cada una decide hacia dónde tira la policy. Con 1.0 para\nel entorno y 0.15 para GAIL, como en la configuración de Agentic Racing, la intención era usar la\nimitación como guía, no como meta. Pero, como vimos en la sección 18, la intención y el efecto no\ntienen por qué coincidir: por la escala de cada recompensa, GAIL podría haber terminado mandando.\n\nPor eso la combinación de imitation learning y reinforcement learning puede ser mucho más poderosa que\ncualquiera de las dos por separado.\n\n---\n\n## 27. Una forma de entender toda la serie\n\nHasta ahora hicimos cuatro preguntas:\n\n| Parte | Pregunta | Idea central |\n|---|---|---|\n| [1. Reinforcement Learning](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/) | ¿Puede aprender por prueba y error? | entorno → recompensa → policy |\n| [2. PPO](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/) | ¿Cómo mejoramos la policy sin destruir lo que ya aprendió? | actualizaciones controladas |\n| [3. Behavioral Cloning](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/) | ¿Podemos enseñarle observando a un experto? | experto → ejemplos → policy |\n| 4. GAIL | ¿Podemos hacer que su comportamiento se parezca al del experto? | discriminador → recompensa de imitación → policy |\n\nY ahora podemos formular la siguiente:\n\n> ¿Podemos combinar todas estas ideas?\n\nEsa es la pregunta de la [parte 5](/es/blog/podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema/).\n\n---\n\n## Conclusión\n\nGAIL introduce una idea poderosa:\n\n> No necesitamos enseñarle al agente cada acción correcta: podemos enseñarle qué significa\n> comportarse como un experto.\n\nBehavioral Cloning aprende directamente de los pares $(s, a)$. GAIL entrena un discriminador que\nintenta distinguir $\\rho_E(s, a)$ de $\\rho_\\pi(s, a)$ y convierte esa competencia en una recompensa que\nacerca la policy al comportamiento del experto. La diferencia se resume en dos preguntas:\n\n- **Behavioral Cloning:** ¿qué hizo el experto aquí?\n- **GAIL:** ¿mi comportamiento se parece al del experto?\n\nPero GAIL no elimina los desafíos. Sigue necesitando buenas demostraciones, buenas observaciones, un\nentorno adecuado, un entrenamiento estable y una evaluación cuidadosa. Y, como cualquier recompensa,\nla suya también puede tener atajos.\n\nEn Agentic Racing, GAIL quedó en una configuración que nunca se lanzó. Pero esa configuración dice\nmucho de la intención: la recompensa diseñada como señal principal, la imitación como guía con peso\n0.15 y BC como un empujón inicial. Era una apuesta a que el experto le mostrara el camino al agente y\nque la recompensa lo llevara más lejos. Y también deja una lección: un peso pequeño en la\nconfiguración no garantiza una influencia pequeña, porque lo que cuenta es la escala de cada señal.\n\n> Imitar al experto puede enseñarnos a comportarnos como él. Aprender con recompensas podría\n> permitirnos ir más allá de lo que el experto sabe hacer.\n\nAhí es donde Behavioral Cloning, GAIL y PPO empiezan a convertirse en piezas de un mismo sistema.\n\nSi llegaste directo a este artículo, las tres partes anteriores están enlazadas al principio, y\n[Agentic Racing: un piloto que nunca aprendió a manejar](/es/blog/agentic-racing-un-piloto-que-nunca-aprendio-a-manejar/)\ncuenta la historia completa del proyecto.\n\n---\n\n## Referencias\n\n- Ho, J. y Ermon, S. (2016). [Generative Adversarial Imitation Learning](https://arxiv.org/abs/1606.03476). *NeurIPS*.\n- Goodfellow, I. et al. (2014). [Generative Adversarial Nets](https://arxiv.org/abs/1406.2661). *NeurIPS*.\n- Gulrajani, I. et al. (2017). [Improved Training of Wasserstein GANs](https://arxiv.org/abs/1704.00028). *NeurIPS*. (La penalización de gradiente que usa el discriminador de ML-Agents.)\n- Kostrikov, I. et al. (2019). [Discriminator-Actor-Critic: Addressing Sample Inefficiency and Reward Bias in Adversarial Imitation Learning](https://arxiv.org/abs/1809.02925). *ICLR*.\n- Ross, S., Gordon, G. y Bagnell, J. A. (2011). [A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning](https://arxiv.org/abs/1011.0686). *AISTATS*.\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), en\n  particular la recompensa y el discriminador de GAIL\n  (`ml-agents/mlagents/trainers/torch_entities/components/reward_providers/gail_reward_provider.py`)\n  y la combinación de señales de recompensa en el trainer de PPO (`ml-agents/mlagents/trainers/ppo/trainer.py`)\n  del release de `mlagents` 1.1.0.\n- La configuración de entrenamiento, el experto y la bitácora del proyecto están en\n  [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch","lang":"en","title":"Can We Teach by Imitation? Behavioral Cloning Explained from Scratch","description":"Part 3 of the Reinforcement Learning from scratch series with Agentic Racing. What Behavioral Cloning is, how ML-Agents implements it, why a small mistake turns into a big one, and what happened when the project set everything up to imitate its heuristic pilot without the log ever recording a single demonstration.","publishDate":"2026-10-06","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero","url":"https://alexisalulema.com/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/","series":{"id":"rl-from-scratch","position":3},"headings":[{"depth":2,"slug":"where-we-left-off","text":"Where we left off"},{"depth":3,"slug":"an-honest-warning-as-in-the-previous-parts","text":"An honest warning, as in the previous parts"},{"depth":2,"slug":"1-from-reinforcement-learning-to-imitation-learning","text":"1. From reinforcement learning to imitation learning"},{"depth":2,"slug":"2-what-exactly-is-a-demonstration","text":"2. What exactly is a demonstration?"},{"depth":2,"slug":"3-the-expert-doesnt-have-to-be-human","text":"3. The expert doesn’t have to be human"},{"depth":2,"slug":"4-bc-turns-imitation-into-supervised-learning","text":"4. BC turns imitation into supervised learning"},{"depth":2,"slug":"5-which-loss-does-behavioral-cloning-use","text":"5. Which loss does Behavioral Cloning use?"},{"depth":2,"slug":"6-training-the-imitator","text":"6. Training the imitator"},{"depth":2,"slug":"7-so-have-we-solved-the-problem","text":"7. So… have we solved the problem?"},{"depth":2,"slug":"8-the-expert-and-the-policy-visit-different-distributions","text":"8. The expert and the policy visit different distributions"},{"depth":2,"slug":"9-an-example-with-our-car","text":"9. An example with our car"},{"depth":2,"slug":"10-the-experts-quality-matters-too","text":"10. The expert’s quality matters too"},{"depth":2,"slug":"11-so-what-is-bc-good-for","text":"11. So what is BC good for?"},{"depth":2,"slug":"12-bc--ppo-teach-first-optimize-later","text":"12. BC + PPO: teach first, optimize later"},{"depth":2,"slug":"13-and-where-does-gail-come-in","text":"13. And where does GAIL come in?"},{"depth":2,"slug":"14-the-real-challenge-what-are-we-teaching","text":"14. The real challenge: what are we teaching?"},{"depth":2,"slug":"15-demonstrations-are-an-engineering-problem-too","text":"15. Demonstrations are an engineering problem too"},{"depth":2,"slug":"16-what-should-we-measure","text":"16. What should we measure?"},{"depth":2,"slug":"17-the-experiment-worth-running","text":"17. The experiment worth running"},{"depth":2,"slug":"18-dagger-teaching-how-to-recover-too","text":"18. DAgger: teaching how to recover too"},{"depth":2,"slug":"19-bc-doesnt-replace-reinforcement-learning","text":"19. BC doesn’t replace reinforcement learning"},{"depth":2,"slug":"conclusion-yes-we-can-teach-by-imitation","text":"Conclusion: yes, we can teach by imitation"},{"depth":2,"slug":"references","text":"References"}],"body":"> **Series: Reinforcement Learning from scratch, with Agentic Racing as the lab**\n>\n> 1. Part 1 · [How Does an Agent Learn? Teaching a Car to Drive](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/)\n> 2. Part 2 · [How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/)\n> 3. Part 3 · **Can We Teach by Imitation? Behavioral Cloning Explained from Scratch** (this article)\n> 4. Part 4 · [Can We Learn to Behave Like an Expert? GAIL Explained from Scratch](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/)\n> 5. Part 5 · [Can We Combine Imitation and RL? BC, GAIL, and PPO as One System](/blog/can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system/)\n> 6. Part 6 · [How Do We Tell an Agent What We Want? Reward Engineering Explained from Scratch](/blog/how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch/)\n> 7. Part 7 · [What Should an Agent Be Able to See? Observation Design Explained from Scratch](/blog/what-should-an-agent-be-able-to-see-observation-design-explained-from-scratch/)\n> 8. Part 8 · [What Do We Do When the Agent Doesn't Learn? RL Debugging Explained from Scratch](/blog/what-do-we-do-when-the-agent-doesnt-learn-rl-debugging-explained-from-scratch/)\n\n## Where we left off\n\n[Part 2](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/) ended with\nan uncomfortable reading of what happened in Agentic Racing. PPO stably optimized the objective we\ngave it, but it can only reinforce what the policy already tries now and then. The full corner\nmaneuver (brake, turn, accelerate again) is a coordinated sequence of one or two seconds, and if the\npolicy almost never tries it by chance, no policy gradient is going to invent it.\n\nIn section 10 of that part we mentioned, in a couple of lines, a way out of that problem: **starting\nfrom an expert's demonstrations**. This article develops it.\n\nThe idea is simple. What if we already have a driver who knows how to drive? Instead of forcing the\nagent to discover from scratch what to do, we can watch the driver and record their decisions:\n\n```text\nobservation → action\nobservation → action\nobservation → action\n...\n```\n\nAnd then train a neural network to learn that relationship. That's **Behavioral Cloning (BC)**:\n\n> Instead of learning only by trial and error, we can learn by imitating examples of behavior.\n\n### An honest warning, as in the previous parts\n\nIn Agentic Racing, BC **was prepared but never run**. The project has an expert (the heuristic pilot\nthat drives the demo's cars today), a demonstration recorder, and it once had a training\nconfiguration with BC. But the project's log never records a demonstration being recorded, and the\nlast training run was done with plain PPO.\n\nIt fell by the wayside for two reasons. The first is a good lesson about imitation in itself: every\ntime the plan was \"record demonstrations\", a flaw showed up in the expert or the environment that was\nworth fixing first. The teacher had to learn to drive before it could teach. The second was a\ndeliberate decision: with the lateral grip already fixed and the tracks smoothed, the eighth run was\ndone with plain PPO, without imitation, to find out whether the algorithm alone was enough (we\ncovered this in section 10 of part 2). It wasn't, and shortly afterward the project set the RL pilot\naside; BC was never tried again.\n\nSo in this article the BC concepts come with what the repository actually has, what it doesn't, and\nwhat would have happened if it had been used with that particular expert.\n\n---\n\n## 1. From reinforcement learning to imitation learning\n\nIn PPO, the fundamental loop is the one from part 1:\n\n```text\nobservation\n     ↓\n   policy\n     ↓\n   action\n     ↓\n environment\n     ↓\n   reward\n     ↓\n  learning\n     ↺\n```\n\nThe agent has to gradually discover which actions lead to good outcomes.\n\nIn Behavioral Cloning we change the problem. We have an expert and we record what it does:\n\n```text\n                 EXPERT\n                    │\n                    ▼\n             DEMONSTRATIONS\n                    │\n                    ▼\n                 DATASET\n                    │\n                    ▼\n              NEURAL POLICY\n```\n\nWe can write it as:\n\n$$\n\\pi_\\theta(a \\mid s) \\approx \\pi_{\\text{expert}}(a \\mid s)\n$$\n\nThe policy tries to reproduce the expert's behavior. We no longer tell it:\n\n> \"Discover the best action.\"\n\nWe tell it:\n\n> \"When you see something like what the expert saw, do what it did.\"\n\nNotice what disappears: the reward. BC doesn't use it. It doesn't know whether the expert drove well\nor badly; it only knows what it did.\n\n---\n\n## 2. What exactly is a demonstration?\n\nA demonstration is a sequence of experiences produced by the expert:\n\n```text\nt = 0   observation → [...]   action → [...]\nt = 1   observation → [...]   action → [...]\nt = 2   observation → [...]   action → [...]\n```\n\nEach step contains a pair:\n\n$$\n(s_t,\\ a_t)\n$$\n\nwhere $s_t$ is the observation and $a_t$ is the action the expert took.\n\nA demonstration isn't a video. A video shows what the driver did; a demonstration for learning\ncontains exactly the data the policy will receive and produce, so that observation can be linked to\naction.\n\nIn Unity ML-Agents, demonstrations are recorded with the `DemonstrationRecorder` component, which\nstores in a `.demo` file the agent's observations and the actions it took at each decision (in our\ncase, 10 times per second).\n\nIn Agentic Racing, the project's evaluator has a mode meant for this:\n\n```powershell\neval.exe -record\n```\n\nThat mode does three things: it drives every car with the heuristic pilot, adds a\n`DemonstrationRecorder` to each one, and runs for 300 seconds, writing one `.demo` file per car.\nBecause the recorder sits on the same agent that gets trained, each demonstration contains **the same\n42 observation numbers and the same 3 continuous actions** we saw in part 1. Expert and policy speak\nexactly the same language.\n\nWhat we don't know is whether a `.demo` file was ever generated. They're data, so they're\ndeliberately excluded from git, and the project's log doesn't record a recording ever being run.\n\n---\n\n## 3. The expert doesn't have to be human\n\nWhen we think of imitation, we tend to picture a person driving. It doesn't have to be that way. The\nexpert can be a person, a traditional controller, a rule-based system, an already-trained policy, or\nanother agent.\n\nIn Agentic Racing, the natural expert is the heuristic pilot, `RaceAgent.Heuristic()`: the same\nhand-written controller that drives the demo's cars today, and whose simplified version we saw in\nsection 17 of part 1. At each step it:\n\n- looks for the sharpest corner over the next 60 meters and computes a target speed;\n- accelerates if it's below it, brakes if it's clearly above;\n- aims the steering at a point between the racing line and the track's centerline (closer to one or\n  the other depending on the directive), 12 to 36 meters ahead depending on speed.\n\nAnd it produces its actions in exactly the same format as the policy: `steer` and `throttle` between\n−1 and 1, `brake` between 0 and 1.\n\n```text\n             Heuristic pilot\n                    │\n                    ▼\n             demonstrations\n                    │\n                    ▼\n          Behavioral Cloning\n                    │\n                    ▼\n              Neural Policy\n```\n\nOne detail makes this expert especially interesting: its behavior changes with the strategist's\ndirective. More aggression means a higher target speed and braking later; the more conservative\ndirectives pull it toward the center. When recording without forcing a directive, the evaluator\nassigns a random one each episode, just as in training. So demonstrations recorded with it today\nwouldn't just teach how to drive: they'd teach how to drive **differently depending on the\ndirective**, which is exactly what the six directive channels in the observation needed to learn.\n\nWith one caveat about dates: when the BC configuration was prepared, the directive wasn't yet wired\ninto the heuristic pilot; that happened a day later. Demonstrations recorded at that moment would\nhave taught the policy exactly the opposite: to ignore the directive channels.\n\nHaving an expert in the repository doesn't mean a BC pipeline was ever run. This is the actual state:\n\n| Piece | Does it exist? | Where |\n|---|---|---|\n| Expert | Yes | `RaceAgent.Heuristic()`, in `RaceAgent.cs` |\n| Demonstration recorder | Yes | `eval.exe -record`, in `EvalRunner.cs` |\n| Recording and training instructions | Yes | `training/README.md`, section 6 (notes that the configuration no longer includes the BC and GAIL blocks, and records the values they had) |\n| BC and GAIL configuration | It existed | added for the eighth run and removed before it was launched |\n| `.demo` files | None recorded | the log doesn't record a recording; they're data and aren't versioned |\n| A run with BC | No | the eighth run was done with plain PPO |\n| BC results | No | there are no metrics to compare |\n\n---\n\n## 4. BC turns imitation into supervised learning\n\nSuppose we have a set of demonstrations:\n\n$$\nD = \\{(s_i,\\ a_i)\\}_{i=1}^{N}\n$$\n\nEach example contains an observation $s_i$ and the action $a_i$ the expert took. We want to train a\nneural network so that:\n\n$$\n\\pi_\\theta(s_i) \\approx a_i\n$$\n\n(Here we write $\\pi_\\theta(s_i)$ as the action the policy produces, to keep it simple; in reality, as\nwe saw in part 1, the policy produces a distribution and the action is sampled from it.)\n\nThe process is ordinary supervised learning:\n\n```text\nobservation\n     │\n     ▼\nneural network\n     │\n     ▼\npredicted action\n     │\n     ▼\ncompare with the expert\n     │\n     ▼\nloss\n     │\n     ▼\nbackpropagation\n     │\n     ▼\nupdate θ\n```\n\nThe difference lies in what the dataset represents. In a classic problem we'd have `image → cat`.\nHere we have `environment observation → expert action`.\n\n---\n\n## 5. Which loss does Behavioral Cloning use?\n\nIt depends on the action space.\n\n**Continuous actions.** If the expert produces values like:\n\n```text\nsteer    = -0.35\nthrottle =  0.72\nbrake    =  0.00\n```\n\nthe most common formulation penalizes the distance between the expert's action and the policy's (up\nto a constant factor, depending on how it's averaged over the action dimensions):\n\n$$\nL_{\\text{BC}} = \\frac{1}{N} \\sum_{i=1}^{N} \\left\\lVert a_i - \\pi_\\theta(s_i) \\right\\rVert^2\n$$\n\n**Discrete actions.** If the actions were categories (`LEFT`, `STRAIGHT`, `RIGHT`), the problem is\nclassification and a loss like *cross-entropy* is used.\n\nML-Agents 1.1.0 does exactly that: mean squared error for continuous actions and cross-entropy for\ndiscrete ones. For continuous actions it compares the action the policy **samples** from its\ndistribution with the expert's action. That has an interesting side effect: since the sampling noise\nalso adds to the error, BC doesn't just pull the policy's mean toward the expert's, it also shrinks\nits spread, that is, how much it explores. TensorBoard shows it as `Losses/Pretraining Loss`.\n\nIn our car, with three continuous actions, it would be the mean squared error over `steer`,\n`throttle` and `brake`.\n\nThe key idea:\n\n> The policy receives an observation and is penalized when its action moves away from the one the\n> expert took.\n\n---\n\n## 6. Training the imitator\n\nThe basic loop is:\n\n1. take a batch of demonstrations;\n2. pass the observations through the policy;\n3. get the predicted actions;\n4. compare them with the expert's;\n5. compute the loss;\n6. backpropagate;\n7. update the parameters;\n8. repeat.\n\nThe advantage over RL from scratch is huge. The agent doesn't have to stumble onto how to take a\ncorner: we show it how. It's like teaching a student: \"when you're in this situation, this is what\nI'd do\".\n\nIn ML-Agents there's an implementation detail that changes how it's best thought about. BC **isn't a\nseparate phase** before PPO training. It runs inside the same training: after each PPO update, the\npolicy also gets a BC update, which by default makes several passes over the demonstrations in\nminibatches, with its own optimizer. That update uses a learning rate equal to PPO's initial learning\nrate multiplied by `strength`, decaying linearly to almost zero over the first `steps` steps.\n\nThe configuration that was written for Agentic Racing's eighth run used `strength: 0.5` and\n`steps: 2000000`. In other words, BC would have pushed hard at the start and faded out over the first\n2 million steps of a 10-million-step run, leaving the rest to PPO.\n\n---\n\n## 7. So… have we solved the problem?\n\nNo. Here comes Behavioral Cloning's most important limitation.\n\nImagine the expert always drives near the center of the track:\n\n```text\n────────────────────────────────\n             🚗\n              ↓\n       expert trajectory\n────────────────────────────────\n```\n\nThe policy mostly learns those states. But when it runs, it may make a small mistake:\n\n```text\n────────────────────────────────\n               🚗\n                ↘\n                 ↘\n────────────────────────────────\n```\n\nNow the car is in a state that may never have appeared in the demonstrations, and the policy has to\ndecide what to do. If it doesn't know how to recover either:\n\n```text\nsmall mistake\n     ↓\nunseen state\n     ↓\nwrong action\n     ↓\nbigger mistake\n     ↓\nanother unseen state\n     ↓\n...\n```\n\n![Compounding errors: the expert stays inside a narrow band of states near the center of the track. The imitator follows it at first, makes a small mistake, leaves the band covered by the demonstrations, and each mistake takes it further away until it leaves the track.](/images/bc-compounding-errors.svg)\n\nThis phenomenon is known as ***covariate shift*** and as **compounding errors**.\n\n---\n\n## 8. The expert and the policy visit different distributions\n\nDuring training, the policy sees states visited by the expert:\n\n$$\ns \\sim D_{\\text{expert}}\n$$\n\nBut when we run it, it visits the states its own decisions lead it to:\n\n$$\ns \\sim D_{\\pi}\n$$\n\nAnd there's no guarantee that:\n\n$$\nD_{\\pi} = D_{\\text{expert}}\n$$\n\nAt first they may look alike. After a small mistake, they start to drift apart. The expert never had\nto demonstrate what to do in a really bad situation, because the expert doesn't usually get there.\nThe policy, on the other hand, can.\n\nRoss and Bagnell's classic analysis puts numbers on it: if the imitator makes a mistake with\nprobability $\\epsilon$ in the expert's states, then over a task of $T$ steps its performance can fall\nbehind the expert's by an amount that grows with $T^2 \\epsilon$, not $T \\epsilon$. Each mistake\ndoesn't just cost its own share: it also takes the imitator to states where it will make more\nmistakes. Across Agentic Racing's eight runs, a training episode could last up to 800 decisions.\n\nThat's why we can get:\n\n> an excellent result on the dataset and a bad policy at run time.\n\n---\n\n## 9. An example with our car\n\nIn Agentic Racing this problem isn't hypothetical: it's written into the configuration itself.\n\n**The demonstrations would always start in the best possible state.** Recording mode turns on the\nevaluator's `CleanSpawn` option: each car appears on the centerline, perfectly aligned with the\ntrack, already at 14 m/s. During training, on the other hand, every episode deliberately starts with\nnoise: up to ±10° of heading, up to ±2 m of lateral offset, and at 8 m/s. From the very first step,\nthe trained policy would visit states the demonstrations never show.\n\n**The expert has its favorite zones.** One of the observations is the car's lateral offset from the\ncenter, divided by half the track width: 0 is the center, and −1 and +1 are the edges. The heuristic\npilot aims at a point between the racing line and the center, and also corrects toward the center,\nso on the straights and with conservative directives its demonstrations would cluster near 0; in\ncorners and with aggressive directives, more toward the racing line. Nobody measured that\ndistribution, but the code does make one thing clear: the expert has no reason to spend time pressed\nagainst the wrong wall or sideways on the track. If the policy gets there, it has to **extrapolate**,\nand extrapolating outside the training distribution is much harder than interpolating inside it.\n\n**The expert itself was fragile outside its nominal state.** In an earlier version, spawning on the\nracing line instead of the center was enough to send the heuristic pilot into an oscillation that\nended in a spin: its correction toward the center was too strong for that starting point. And the\nevaluator's code says so explicitly: training's noisy spawn stalls the heuristic before it settles on\nthe line, which is why the expert was only evaluated, and would only be recorded, from clean spawns.\nAn expert that can't recover from a situation can't teach how to recover from it.\n\nAnd today there would be one more problem: all the arenas use the same fixed circuit, so the\ndemonstrations would cover a single layout.\n\n> Having many demonstrations doesn't mean having a robust policy. Coverage matters as much as\n> quantity.\n\n---\n\n## 10. The expert's quality matters too\n\nBC learns from the expert, and that includes its flaws. If the expert brakes too much, takes\ninefficient lines, makes mistakes, or produces noisy actions, BC can learn all of that.\n\n$$\n\\text{learned policy} \\approx \\text{demonstrated behavior}\n$$\n\nNot necessarily:\n\n$$\n\\text{learned policy} \\approx \\text{optimal behavior}\n$$\n\nAgentic Racing had a very concrete example. Before recording, someone took a closer look at how the\nheuristic pilot drove on the procedural tracks of the time. Only 2 or 3 out of every 9 cars drove\nwell; the rest got stuck in a back-and-forth near the starting point: they hit the edge, did a short\nreverse, slowed down again, and repeated, for the full 80 seconds of the episode, without making\nprogress. The log says it bluntly: those trajectories \"would fill the demos with garbage\". The fix\nwas to add a no-progress cutoff (less than 8 meters in 5 seconds ends the episode), so that stuck\ncars would be reset instead of recording minutes of an expert that wasn't actually driving.\n\nIt was the first of several times that recording the demonstrations was postponed because the\nteacher wasn't ready yet.\n\n> BC is imitation. It isn't, by itself, an algorithm whose goal is to beat the expert.\n\n---\n\n## 11. So what is BC good for?\n\nDespite those limitations, BC can be very useful, above all as a **starting point**:\n\n```text\n                 Expert\n                    │\n                    ▼\n             demonstrations\n                    │\n                    ▼\n            Behavioral Cloning\n                    │\n                    ▼\n             initial policy\n                    │\n                    ▼\n                  PPO\n                    │\n                    ▼\n             improved policy\n```\n\nPPO no longer starts from scratch: it starts with a policy that can already do something reasonable.\nAnd that attacks part 2's problem head-on. The agent doesn't need to stumble onto the\nbrake–turn–accelerate sequence by chance if someone shows it. It's enough for BC to leave it in a\nzone where the policy tries that sequence often, so PPO can reinforce it.\n\nThat's why imitation learning and reinforcement learning complement each other.\n\n---\n\n## 12. BC + PPO: teach first, optimize later\n\nWe can think of two teachers:\n\n- **The expert** says: \"this is how I drive\".\n- **The environment** says: \"this is what actually works to reach the goal\".\n\nBC listens to the expert. PPO listens to the reward.\n\nIn ML-Agents, as we saw in section 6, both listen **at the same time**, with a volume that changes\nover time: BC's weight starts high and fades out, while PPO keeps going for the whole training.\n\n![BC and PPO in ML-Agents with the configuration prepared for Agentic Racing: BC's weight starts at half of PPO's learning rate and decays linearly to zero at 2 million steps; PPO updates the policy throughout the run's 10 million steps.](/images/bc-plus-ppo.svg)\n\nIt's a smooth form of \"teach first, optimize later\": there's no hard cut between the two stages, just\na transition. In Agentic Racing that pipeline existed in the configuration, but it was never run.\n\n---\n\n## 13. And where does GAIL come in?\n\nBC tries to learn $\\text{observation} \\rightarrow \\text{action}$ directly.\n\n**GAIL** (*Generative Adversarial Imitation Learning*) asks a different question:\n\n> Does my agent's behavior look like the expert's?\n\nIt trains a **discriminator** that tries to tell the expert's experiences apart from the agent's. The\nagent gets an extra reward when its experiences are hard to tell apart from the expert's. In\nML-Agents, GAIL is literally that: one more reward signal, added to the environment's reward and\noptimized by PPO like any other.\n\nThe eighth run's configuration also had a GAIL block, with a weight of 0.15 and comparing actions as\nwell as observations.\n\nSo each technique answers a different question:\n\n| Technique | Main question |\n|---|---|\n| Behavioral Cloning | Can you do what the expert did in this state? |\n| GAIL | Does your behavior look like the expert's? |\n| PPO | Can you maximize the task's reward? |\n\nGAIL deserves its own article, and it's [part 4](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/) of the series.\n\n---\n\n## 14. The real challenge: what are we teaching?\n\nBC forces us to ask a deeper question:\n\n> Does the observation contain enough information to make the right decision?\n\nSuppose two different situations:\n\n```text\nsituation A → the expert turns left\nsituation B → the expert turns right\n```\n\nIf both produce exactly the same observation for the policy, the problem isn't the neural network:\nthe information simply isn't there. And with a squared-error loss, the policy doesn't pick one of the\ntwo answers: it learns **the average**, which may be not turning at all.\n\nIn Agentic Racing this is concrete, because the heuristic pilot **doesn't decide from the 42\nobservations alone** that the policy would receive:\n\n- **It has memory.** Its steering goes through a smoothing filter: each command blends the new\n  calculation with the previous command. The policy from part 2 is a network with no memory, which\n  only sees the current observation.\n- **It uses timers.** If the car has been slow and against the wall for a while (the code counts it\n  as more than a second), the expert does a 0.6-second reverse to break free. Same visible state,\n  different actions depending on how long it has been stuck. For the policy, that would be exactly\n  situations A and B above: sometimes accelerate, sometimes reverse, for the same observation.\n- **It looks at the track differently.** The expert looks for the sharpest corner over the next 60\n  meters and aims at a specific point between the racing line and the center. The policy doesn't do\n  that scan: it gets three curvature numbers (0–22, 18–45 and 40–75 meters ahead), the raycasts\n  against the walls, and its relation to the racing line, but not the exact point the expert is\n  aiming at.\n\nNone of this stops BC from working. But it does mean the policy couldn't reproduce the expert\nexactly, and that the imitation would be worst precisely in recovery maneuvers, where the expert\nrelies on its memory.\n\nThis connects to section 3 of [part 1](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/):\n**designing the observations is also part of the problem**. A huge dataset can't make up for an\nobservation that doesn't contain the information the expert used.\n\n---\n\n## 15. Demonstrations are an engineering problem too\n\nCreating demonstrations isn't about recording many hours. You have to ask:\n\n| Question | In Agentic Racing |\n|---|---|\n| Is the expert actually good? | On the fixed circuit, yes: it completes full laps. On the procedural tracks of the time, no, although much of the blame lay with the environment (corners too tight and a lateral grip that braked the car on every turn). |\n| Does it cover different situations? | Yes for directives (a random one per episode). No for layouts: today there's only one circuit. |\n| Are there easy and hard corners? | The fixed circuit has four corners, all with the same radius. |\n| Are there recovery situations? | Hardly: recordings start centered and aligned, and the expert rarely gets into situations it has to get out of. |\n| Are there inconsistent actions? | Yes: the ones that depend on the expert's memory and timers. |\n| Do the states resemble the ones the policy will see? | Not entirely: training starts with heading and position noise that the recording doesn't have. |\n\nIf all the demonstrations are perfect, we may teach perfect driving very well, but not how to recover\nwhen something goes wrong. For driving, recovery demonstrations can be as important as ideal ones.\n\nThere's a famous precedent. ALVINN, one of the first vehicles to learn to drive itself with a neural\nnetwork (Pomerleau, 1989), ended up being trained by imitating a human driver. To teach it to\ncorrect, Pomerleau (1991) also generated shifted and rotated images, as if the vehicle were off to\none side, labeled with the correction that would have been needed. It manufactured the recovery\nexamples the driver had never needed to show.\n\n---\n\n## 16. What should we measure?\n\nBC shouldn't be judged by its loss alone. A policy can have a very small imitation error and still\ndrive badly.\n\n**Imitation metrics:**\n\n- the BC loss (`Losses/Pretraining Loss` in ML-Agents);\n- the error per action component: `steer`, `throttle` and `brake` separately.\n\n**Behavior metrics:**\n\n- fraction of the lap completed;\n- laps completed and lap time;\n- why each episode ends (off track, stopped, no progress);\n- speed and brake usage.\n\nAgentic Racing already has almost all of the behavior metrics: they're what its evaluator reports,\nthe same evaluator that in part 2 showed the mean reward going up while the lap fraction didn't move.\n\n> A low loss doesn't guarantee a good policy. What we want to measure is behavior outside the\n> dataset.\n\n---\n\n## 17. The experiment worth running\n\nFor Agentic Racing, the interesting comparison would be:\n\n```text\nPPO from scratch                  demonstrations → BC + PPO\n      │                                       │\n      ▼                                       ▼\n  result A                                result B\n```\n\ncomparing lap fraction, laps completed, track exits, stability, and how many steps it takes to reach\neach level, and then testing against situations that weren't in the demonstrations.\n\nThen the question stops being \"does BC work?\" and becomes:\n\n> How much easier does learning get when you start from a policy that can already imitate reasonable\n> behavior?\n\nThis experiment **wasn't run**. It would be easier to run today than back then: on the fixed\ncircuit, where the track is no longer an obstacle, the expert completes laps, and the recorder and\nthe evaluator exist. The BC blocks would need to be added back to the configuration, since the\ninstructions assume they're there. It's still pending.\n\n---\n\n## 18. DAgger: teaching how to recover too\n\nThe classic solution to *covariate shift* is **DAgger** (*Dataset Aggregation*), by Ross, Gordon and\nBagnell:\n\n1. train on the initial demonstrations;\n2. run the policy;\n3. record the states the policy visits;\n4. ask the expert what it would have done in each one;\n5. add those examples to the dataset;\n6. train again;\n7. repeat.\n\n```text\ninitial demonstrations\n          │\n          ▼\n         BC\n          │\n          ▼\n       policy\n          │\n          ▼\n     run policy\n          │\n          ▼\n out-of-distribution states\n          │\n          ▼\n     ask the expert\n          │\n          ▼\n     new examples ──────► BC\n```\n\nDAgger is interesting because the expert teaches precisely in the situations where the policy is\nstruggling.\n\nWith a human expert, DAgger is expensive: someone has to label thousands of states. With an expert\nthat is a program, like Agentic Racing's heuristic pilot, it's much cheaper: you just run the\nheuristic \"in the shadow\" alongside the policy, in the same states the policy visits, and store what\nit would have done. It has to run alongside rather than as a one-off query because, as we saw in\nsection 14, the heuristic has memory. ML-Agents doesn't include DAgger, so it would have to be built.\nThat wasn't done either, but it's probably the best answer to the problems in sections 9 and 15.\n\n---\n\n## 19. BC doesn't replace reinforcement learning\n\nBC can work very well when there are good demonstrations, the problem is well covered, and the\npolicy doesn't need to stray far from the examples.\n\nBut if we want the agent to discover better strategies, beat the expert, adapt to new situations, or\noptimize a specific reward, we need something more. In Agentic Racing's case, beating the expert\nisn't a detail: the heuristic pilot brakes according to a fixed rule, and an agent learning from the\nreward could find something better.\n\n```text\nExpert\n   │\n   ▼\nDemonstrations\n   │\n   ▼\nBehavioral Cloning ──► initial policy\n                           │\n                           ├──► GAIL: does it look like the expert?\n                           │\n                           ▼\n                          PPO: does it maximize the reward?\n                           │\n                           ▼\n                    optimized policy\n```\n\nIn ML-Agents, all three pieces can be combined in a single run, which is what the eighth run's\nconfiguration was preparing. That sequence is a proposal for Agentic Racing, not something that was\nrun.\n\n---\n\n## Conclusion: yes, we can teach by imitation\n\nBehavioral Cloning transforms part of the problem. Instead of saying \"explore until you discover what\nworks\", we say \"look at these examples and learn to behave like the expert\". That can be very\npowerful.\n\nBut it has a fundamental weakness: the agent can run into situations that never appeared in the\ndemonstrations. A small mistake takes it out of the expert's distribution, that mistake leads to\nanother, and that one leads to another.\n\nAgentic Racing adds a second, less obvious lesson: **imitation requires a reliable expert and an\nobservation that contains what the expert uses to decide**. The project had an expert, a recorder\nand a configuration, but every time it was about to record it found something to fix: an expert\nthat got stuck, tracks nobody could drive, physics that turned every turn into a sudden brake. And\neven with everything fixed, that expert decides with memory and timers the policy couldn't see.\n\nFor Agentic Racing, the interesting question isn't just whether we can teach by imitation, but:\n\n> Can we use imitation as a starting point and then get the agent to overcome the limitations of its\n> demonstrations?\n\nThat's where Behavioral Cloning connects with GAIL and PPO.\n\n**Next in the series:** [part 4, \"Can We Learn to Behave Like an Expert? GAIL Explained from\nScratch\"](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/).\n\nIf you landed directly on this article,\n[part 1](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/) explains the core concepts,\n[part 2](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/) opens up\nPPO, and\n[Agentic Racing: A Pilot That Never Learned to Drive](/blog/agentic-racing-a-pilot-that-never-learned-to-drive/)\ntells the project's full story.\n\n---\n\n## References\n\n- Pomerleau, D. (1989). ALVINN: An Autonomous Land Vehicle in a Neural Network. *Advances in Neural Information Processing Systems 1*.\n- Pomerleau, D. (1991). Efficient Training of Artificial Neural Networks for Autonomous Navigation. *Neural Computation*, 3(1).\n- Ross, S. and Bagnell, J. A. (2010). [Efficient Reductions for Imitation Learning](https://proceedings.mlr.press/v9/ross10a.html). *AISTATS*.\n- Ross, S., Gordon, G. and Bagnell, J. A. (2011). [A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning](https://arxiv.org/abs/1011.0686). *AISTATS*.\n- Ho, J. and Ermon, S. (2016). [Generative Adversarial Imitation Learning](https://arxiv.org/abs/1606.03476). *NeurIPS*.\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), in\n  particular the BC module (`ml-agents/mlagents/trainers/torch_entities/components/bc/module.py`) and\n  the GAIL reward (`.../reward_providers/gail_reward_provider.py`) from the `mlagents` 1.1.0 release.\n- The expert (`RaceAgent.cs`), the recorder (`EvalRunner.cs`), the imitation instructions\n  (`training/README.md`) and the project's log are at\n  [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero","lang":"es","title":"¿Podemos enseñar imitando? Behavioral Cloning explicado desde cero","description":"Tercera parte de la serie Reinforcement Learning desde cero con Agentic Racing. Qué es Behavioral Cloning, cómo lo implementa ML-Agents, por qué un pequeño error se convierte en uno grande, y qué pasó cuando el proyecto preparó todo para imitar a su piloto heurístico sin que la bitácora registre una sola demostración grabada.","publishDate":"2026-10-06","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch","url":"https://alexisalulema.com/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/","series":{"id":"rl-from-scratch","position":3},"headings":[{"depth":2,"slug":"dónde-nos-quedamos","text":"Dónde nos quedamos"},{"depth":3,"slug":"una-advertencia-honesta-como-en-las-partes-anteriores","text":"Una advertencia honesta, como en las partes anteriores"},{"depth":2,"slug":"1-de-reinforcement-learning-a-imitation-learning","text":"1. De reinforcement learning a imitation learning"},{"depth":2,"slug":"2-qué-es-exactamente-una-demostración","text":"2. ¿Qué es exactamente una demostración?"},{"depth":2,"slug":"3-el-experto-no-tiene-que-ser-humano","text":"3. El experto no tiene que ser humano"},{"depth":2,"slug":"4-bc-convierte-la-imitación-en-aprendizaje-supervisado","text":"4. BC convierte la imitación en aprendizaje supervisado"},{"depth":2,"slug":"5-qué-loss-utiliza-behavioral-cloning","text":"5. ¿Qué loss utiliza Behavioral Cloning?"},{"depth":2,"slug":"6-entrenar-al-imitador","text":"6. Entrenar al imitador"},{"depth":2,"slug":"7-entonces-hemos-resuelto-el-problema","text":"7. Entonces… ¿hemos resuelto el problema?"},{"depth":2,"slug":"8-el-experto-y-la-policy-visitan-distribuciones-distintas","text":"8. El experto y la policy visitan distribuciones distintas"},{"depth":2,"slug":"9-un-ejemplo-con-nuestro-coche","text":"9. Un ejemplo con nuestro coche"},{"depth":2,"slug":"10-la-calidad-del-experto-también-importa","text":"10. La calidad del experto también importa"},{"depth":2,"slug":"11-entonces-para-qué-sirve-bc","text":"11. Entonces, ¿para qué sirve BC?"},{"depth":2,"slug":"12-bc--ppo-enseñar-primero-optimizar-después","text":"12. BC + PPO: enseñar primero, optimizar después"},{"depth":2,"slug":"13-y-dónde-entra-gail","text":"13. ¿Y dónde entra GAIL?"},{"depth":2,"slug":"14-el-verdadero-desafío-qué-estamos-enseñando","text":"14. El verdadero desafío: ¿qué estamos enseñando?"},{"depth":2,"slug":"15-las-demostraciones-también-son-un-problema-de-ingeniería","text":"15. Las demostraciones también son un problema de ingeniería"},{"depth":2,"slug":"16-qué-deberíamos-medir","text":"16. ¿Qué deberíamos medir?"},{"depth":2,"slug":"17-el-experimento-que-vale-la-pena-hacer","text":"17. El experimento que vale la pena hacer"},{"depth":2,"slug":"18-dagger-enseñar-también-a-recuperarse","text":"18. DAgger: enseñar también a recuperarse"},{"depth":2,"slug":"19-bc-no-reemplaza-al-reinforcement-learning","text":"19. BC no reemplaza al reinforcement learning"},{"depth":2,"slug":"conclusión-sí-podemos-enseñar-imitando","text":"Conclusión: sí, podemos enseñar imitando"},{"depth":2,"slug":"referencias","text":"Referencias"}],"body":"> **Serie: Reinforcement Learning desde cero, con Agentic Racing como laboratorio**\n>\n> 1. Parte 1 · [¿Cómo aprende un agente? Enseñando a un automóvil a conducir](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/)\n> 2. Parte 2 · [¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/)\n> 3. Parte 3 · **¿Podemos enseñar imitando? Behavioral Cloning explicado desde cero** (este artículo)\n> 4. Parte 4 · [¿Podemos aprender a comportarnos como un experto? GAIL explicado desde cero](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/)\n> 5. Parte 5 · [¿Podemos combinar imitación y RL? BC, GAIL y PPO en un mismo sistema](/es/blog/podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema/)\n> 6. Parte 6 · [¿Cómo le decimos a un agente qué queremos? Reward engineering explicado desde cero](/es/blog/como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero/)\n> 7. Parte 7 · [¿Qué debería poder ver un agente? Observation design explicado desde cero](/es/blog/que-deberia-poder-ver-un-agente-observation-design-explicado-desde-cero/)\n> 8. Parte 8 · [¿Qué hacemos cuando el agente no aprende? RL debugging explicado desde cero](/es/blog/que-hacemos-cuando-el-agente-no-aprende-rl-debugging-explicado-desde-cero/)\n\n## Dónde nos quedamos\n\nLa [parte 2](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/) terminó\ncon una lectura incómoda de lo que pasó en Agentic Racing. PPO optimizó con estabilidad el objetivo\nque le dimos, pero solo puede reforzar lo que la policy ya prueba de vez en cuando. La maniobra\ncompleta de una curva (frenar, girar, volver a acelerar) es una secuencia coordinada de uno o dos\nsegundos, y si la policy casi nunca la prueba por azar, ningún policy gradient la va a inventar.\n\nEn la sección 10 de esa parte mencionamos, en un par de líneas, una salida a ese problema: **partir\nde las demostraciones de un experto**. Este artículo la desarrolla.\n\nLa idea es sencilla. ¿Qué pasaría si ya tenemos un piloto que sabe conducir? En lugar de obligar al\nagente a descubrir desde cero qué debe hacer, podemos observar al piloto y registrar sus decisiones:\n\n```text\nobservación → acción\nobservación → acción\nobservación → acción\n...\n```\n\nY después entrenar una red neuronal para que aprenda esa relación. Eso es **Behavioral Cloning\n(BC)**:\n\n> En lugar de aprender únicamente por prueba y error, podemos aprender imitando ejemplos de\n> comportamiento.\n\n### Una advertencia honesta, como en las partes anteriores\n\nEn Agentic Racing, BC **se preparó pero nunca se ejecutó**. El proyecto tiene un experto (el piloto\nheurístico que hoy maneja los autos del demo), un grabador de demostraciones y llegó a tener una\nconfiguración de entrenamiento con BC. Pero la bitácora del proyecto no registra que se haya grabado\nnunca una demostración, y la última corrida de entrenamiento se hizo con PPO limpio.\n\nSe quedó en el camino por dos razones. La primera es, en sí misma, una buena lección sobre\nimitación: cada vez que el plan era \"grabar demostraciones\", aparecía un defecto en el experto o en\nel entorno que hacía que valiera la pena arreglar eso primero. El profesor tenía que aprender a\nmanejar antes de poder enseñar. La segunda fue una decisión deliberada: con el agarre lateral ya\ncorregido y las pistas suavizadas, la octava corrida se hizo con PPO limpio, sin imitación, para\nsaber si el algoritmo por sí solo alcanzaba (lo contamos en la sección 10 de la parte 2). No\nalcanzó, y poco después el proyecto dejó de lado el piloto RL; BC no volvió a intentarse.\n\nAsí que en este artículo los conceptos de BC van acompañados de lo que el repositorio realmente\ntiene, de lo que no, y de qué habría pasado si se hubiera usado con ese experto concreto.\n\n---\n\n## 1. De reinforcement learning a imitation learning\n\nEn PPO, el ciclo fundamental es el de la parte 1:\n\n```text\nobservación\n     ↓\n   policy\n     ↓\n   acción\n     ↓\n  entorno\n     ↓\n recompensa\n     ↓\n aprendizaje\n     ↺\n```\n\nEl agente tiene que descubrir progresivamente qué acciones producen buenos resultados.\n\nEn Behavioral Cloning cambiamos el problema. Tenemos un experto y registramos lo que hace:\n\n```text\n                 EXPERTO\n                    │\n                    ▼\n             DEMOSTRACIONES\n                    │\n                    ▼\n                 DATASET\n                    │\n                    ▼\n              NEURAL POLICY\n```\n\nPodemos expresarlo como:\n\n$$\n\\pi_\\theta(a \\mid s) \\approx \\pi_{\\text{experto}}(a \\mid s)\n$$\n\nLa policy intenta reproducir el comportamiento del experto. Ya no le decimos:\n\n> \"Descubre cuál es la mejor acción.\"\n\nLe decimos:\n\n> \"Cuando veas algo parecido a lo que vio el experto, haz lo que hizo.\"\n\nFíjate en lo que desaparece: la recompensa. BC no la usa. No sabe si el experto conducía bien o mal;\nsolo sabe qué hizo.\n\n---\n\n## 2. ¿Qué es exactamente una demostración?\n\nUna demostración es una secuencia de experiencias producidas por el experto:\n\n```text\nt = 0   observación → [...]   acción → [...]\nt = 1   observación → [...]   acción → [...]\nt = 2   observación → [...]   acción → [...]\n```\n\nCada paso contiene un par:\n\n$$\n(s_t,\\ a_t)\n$$\n\ndonde $s_t$ es la observación y $a_t$ es la acción que tomó el experto.\n\nUna demostración no es un video. Un video muestra qué hizo el piloto; una demostración para\naprendizaje contiene exactamente los datos que la policy va a recibir y producir, para poder\nrelacionar observación con acción.\n\nEn Unity ML-Agents, las demostraciones se graban con el componente `DemonstrationRecorder`, que\nguarda en un archivo `.demo` las observaciones del agente y las acciones que tomó en cada decisión\n(en nuestro caso, 10 veces por segundo).\n\nEn Agentic Racing, el evaluador del proyecto tiene un modo pensado para esto:\n\n```powershell\neval.exe -record\n```\n\nEse modo hace tres cosas: conduce todos los autos con el piloto heurístico, le agrega un\n`DemonstrationRecorder` a cada uno y corre 300 segundos, escribiendo un archivo `.demo` por auto. Como\nel grabador está en el mismo agente que se entrena, cada demostración contiene **los mismos 42\nnúmeros de observación y las mismas 3 acciones continuas** que vimos en la parte 1. Experto y policy\nhablan exactamente el mismo idioma.\n\nLo que no sabemos es si alguna vez se generó un archivo `.demo`. Son datos, así que están excluidos\nde git a propósito, y la bitácora del proyecto no registra que se haya ejecutado una grabación.\n\n---\n\n## 3. El experto no tiene que ser humano\n\nCuando pensamos en imitación, solemos imaginar a una persona conduciendo. No tiene por qué ser así.\nEl experto puede ser una persona, un controlador tradicional, un sistema de reglas, una policy ya\nentrenada u otro agente.\n\nEn Agentic Racing, el experto natural es el piloto heurístico, `RaceAgent.Heuristic()`: el mismo\ncontrolador escrito a mano que hoy maneja los autos del demo, y cuya versión simplificada vimos en la\nsección 17 de la parte 1. En cada paso:\n\n- mira la curva más cerrada en los próximos 60 metros y calcula una velocidad objetivo;\n- acelera si va por debajo, frena si va claramente por encima;\n- apunta el volante a un punto intermedio entre la trazada ideal y el centro de la pista (más cerca\n  de uno u otro según la directiva), entre 12 y 36 metros por delante según la velocidad.\n\nY produce sus acciones exactamente en el mismo formato que la policy: `steer` y `throttle` entre −1\ny 1, `brake` entre 0 y 1.\n\n```text\n             Piloto heurístico\n                    │\n                    ▼\n             demostraciones\n                    │\n                    ▼\n          Behavioral Cloning\n                    │\n                    ▼\n              Neural Policy\n```\n\nUn detalle que hace a este experto especialmente interesante: su comportamiento cambia con la\ndirectiva del estratega. Más agresividad significa una velocidad objetivo más alta y frenar más\ntarde; las directivas más conservadoras lo acercan al centro. Al grabar sin forzar una directiva, el\nevaluador le asigna una al azar en cada episodio, igual que en el entrenamiento. Así que unas\ndemostraciones grabadas hoy con él no solo enseñarían a conducir: enseñarían a conducir **distinto\nsegún la directiva**, que era justo lo que los seis canales de directiva de la observación\nnecesitaban aprender.\n\nCon un matiz de fechas: cuando se preparó la configuración de BC, la directiva todavía no estaba\nconectada al piloto heurístico; eso se hizo un día después. Unas demostraciones grabadas en ese\nmomento le habrían enseñado a la policy justo lo contrario: a ignorar los canales de directiva.\n\nQue el repositorio tenga un experto no significa que exista un pipeline de BC ejecutado. Este es el\nestado real:\n\n| Pieza | ¿Existe? | Dónde |\n|---|---|---|\n| Experto | Sí | `RaceAgent.Heuristic()`, en `RaceAgent.cs` |\n| Grabador de demostraciones | Sí | `eval.exe -record`, en `EvalRunner.cs` |\n| Instrucciones de grabación y entrenamiento | Sí | `training/README.md`, sección 6 (aclara que la configuración ya no trae los bloques de BC y GAIL, y anota los valores que tenían) |\n| Configuración con BC y GAIL | Existió | se agregó para la octava corrida y se quitó antes de lanzarla |\n| Archivos `.demo` | Ninguno registrado | la bitácora no registra una grabación; son datos y no se versionan |\n| Una corrida con BC | No | la octava corrida se hizo con PPO limpio |\n| Resultados de BC | No | no hay métricas que comparar |\n\n---\n\n## 4. BC convierte la imitación en aprendizaje supervisado\n\nSupongamos que tenemos un conjunto de demostraciones:\n\n$$\nD = \\{(s_i,\\ a_i)\\}_{i=1}^{N}\n$$\n\nCada ejemplo contiene una observación $s_i$ y la acción $a_i$ que tomó el experto. Queremos entrenar\nuna red neuronal para que:\n\n$$\n\\pi_\\theta(s_i) \\approx a_i\n$$\n\n(Aquí escribimos $\\pi_\\theta(s_i)$ como la acción que produce la policy, para simplificar; en\nrealidad, como vimos en la parte 1, la policy produce una distribución y la acción se muestrea de\nella.)\n\nEl proceso es aprendizaje supervisado tradicional:\n\n```text\nobservación\n     │\n     ▼\nred neuronal\n     │\n     ▼\nacción predicha\n     │\n     ▼\ncomparar con el experto\n     │\n     ▼\nloss\n     │\n     ▼\nbackpropagation\n     │\n     ▼\nactualizar θ\n```\n\nLa diferencia está en qué representa el dataset. En un problema clásico tendríamos `imagen → gato`.\nAquí tenemos `observación del entorno → acción del experto`.\n\n---\n\n## 5. ¿Qué loss utiliza Behavioral Cloning?\n\nDepende del espacio de acciones.\n\n**Acciones continuas.** Si el experto produce valores como:\n\n```text\nsteer    = -0.35\nthrottle =  0.72\nbrake    =  0.00\n```\n\nla formulación más común penaliza la distancia entre la acción del experto y la de la policy (hasta\nun factor constante, según cómo se promedie sobre las dimensiones de la acción):\n\n$$\nL_{\\text{BC}} = \\frac{1}{N} \\sum_{i=1}^{N} \\left\\lVert a_i - \\pi_\\theta(s_i) \\right\\rVert^2\n$$\n\n**Acciones discretas.** Si las acciones fueran categorías (`IZQUIERDA`, `RECTO`, `DERECHA`), el\nproblema es de clasificación y se usa una pérdida como *cross-entropy*.\n\nML-Agents 1.1.0 hace exactamente eso: error cuadrático medio para las acciones continuas y\ncross-entropy para las discretas. Para las continuas compara la acción que **muestrea** la policy de\nsu distribución con la acción del experto. Eso tiene un efecto secundario interesante: como el ruido\ndel muestreo también aumenta el error, BC no solo acerca la media de la policy a la del experto,\ntambién reduce su dispersión, es decir, cuánto explora. TensorBoard la muestra como\n`Losses/Pretraining Loss`.\n\nEn nuestro auto, con tres acciones continuas, sería el error cuadrático medio sobre `steer`,\n`throttle` y `brake`.\n\nLa idea importante:\n\n> La policy recibe una observación y es penalizada cuando su acción se aleja de la que tomó el\n> experto.\n\n---\n\n## 6. Entrenar al imitador\n\nEl ciclo básico es:\n\n1. tomar un lote de demostraciones;\n2. pasar las observaciones por la policy;\n3. obtener las acciones predichas;\n4. compararlas con las del experto;\n5. calcular la loss;\n6. hacer backpropagation;\n7. actualizar los parámetros;\n8. repetir.\n\nLa ventaja frente a RL desde cero es enorme. El agente no tiene que descubrir por accidente cómo\ntomar una curva: le mostramos cómo. Es como enseñarle a un alumno: \"cuando estés en esta situación,\nyo haría esto\".\n\nEn ML-Agents hay un detalle de implementación que cambia cómo conviene pensarlo. BC **no es una fase\nprevia** separada del entrenamiento con PPO. Corre dentro del mismo entrenamiento: después de cada\nactualización de PPO, la policy recibe también una actualización de BC, que por defecto da varias\npasadas por las demostraciones en minibatches, con su propio optimizador. Esa actualización usa una\ntasa de aprendizaje igual a la tasa inicial de PPO multiplicada por `strength`, que decae\nlinealmente hasta casi cero durante los primeros `steps` pasos.\n\nLa configuración que llegó a escribirse para la octava corrida de Agentic Racing usaba\n`strength: 0.5` y `steps: 2000000`. Es decir, BC habría empujado con fuerza al principio y se habría\nido apagando durante los primeros 2 millones de pasos, de una corrida de 10 millones, para dejar el\nresto en manos de PPO.\n\n---\n\n## 7. Entonces… ¿hemos resuelto el problema?\n\nNo. Aquí aparece la limitación más importante de Behavioral Cloning.\n\nImaginemos que el experto conduce siempre cerca del centro de la pista:\n\n```text\n────────────────────────────────\n             🚗\n              ↓\n       trayectoria experta\n────────────────────────────────\n```\n\nLa policy aprende principalmente esos estados. Pero al ejecutarse puede cometer un pequeño error:\n\n```text\n────────────────────────────────\n               🚗\n                ↘\n                 ↘\n────────────────────────────────\n```\n\nAhora el auto está en un estado que quizá nunca apareció en las demostraciones, y la policy tiene que\ndecidir qué hacer. Si tampoco sabe recuperarse:\n\n```text\npequeño error\n     ↓\nestado no visto\n     ↓\nacción incorrecta\n     ↓\nerror mayor\n     ↓\notro estado no visto\n     ↓\n...\n```\n\n![Errores que se acumulan: el experto se mantiene dentro de una banda angosta de estados cerca del centro de la pista. El imitador lo sigue al principio, comete un pequeño error, sale de la banda cubierta por las demostraciones y cada error lo lleva más lejos, hasta salirse de la pista.](/images/bc-compounding-errors.svg)\n\nEste fenómeno se conoce como ***covariate shift*** y como **errores acumulativos** (*compounding\nerrors*).\n\n---\n\n## 8. El experto y la policy visitan distribuciones distintas\n\nDurante el entrenamiento, la policy ve estados visitados por el experto:\n\n$$\ns \\sim D_{\\text{experto}}\n$$\n\nPero cuando la ejecutamos, visita los estados a los que la llevan sus propias decisiones:\n\n$$\ns \\sim D_{\\pi}\n$$\n\nY no hay ninguna garantía de que:\n\n$$\nD_{\\pi} = D_{\\text{experto}}\n$$\n\nAl principio pueden parecerse. Después de un pequeño error, empiezan a separarse. El experto nunca\ntuvo que demostrar qué hacer en una situación muy mala, porque el experto no suele llegar ahí. La\npolicy, en cambio, sí puede llegar.\n\nEl análisis clásico de Ross y Bagnell lo pone en números: si el imitador se equivoca con probabilidad\n$\\epsilon$ en los estados del experto, en una tarea de $T$ pasos su desempeño puede alejarse del del\nexperto en una cantidad que crece con $T^2 \\epsilon$, no con $T \\epsilon$. Cada error no solo cuesta\nlo suyo: además lleva al imitador a estados donde se va a equivocar más. En las ocho corridas de\nAgentic Racing, un episodio de entrenamiento podía durar hasta 800 decisiones.\n\nPor eso podemos tener:\n\n> un resultado excelente sobre el dataset y una mala policy durante la ejecución.\n\n---\n\n## 9. Un ejemplo con nuestro coche\n\nEn Agentic Racing este problema no es hipotético: está escrito en la propia configuración.\n\n**Las demostraciones empezarían siempre en el mejor estado posible.** El modo de grabación activa la\nopción `CleanSpawn` del evaluador: cada auto aparece sobre la línea central, perfectamente alineado\ncon la pista y ya a 14 m/s. En cambio, durante el entrenamiento cada episodio empieza con ruido a\npropósito: hasta ±10° de rumbo, hasta ±2 m de desplazamiento lateral y a 8 m/s. Desde el primer\npaso, la policy entrenada visitaría estados que las demostraciones nunca muestran.\n\n**El experto tiene sus zonas preferidas.** Una de las observaciones es el desplazamiento lateral del\nauto respecto del centro, dividido por la mitad del ancho de la pista: 0 es el centro, y −1 y +1 son\nlos bordes. El piloto heurístico apunta a un punto entre la trazada ideal y el centro, y además\ncorrige hacia el centro, así que en las rectas y con las directivas conservadoras sus demostraciones\nse concentrarían cerca de 0; en las curvas y con las directivas agresivas, más hacia la trazada.\nNadie midió esa distribución, pero hay algo que el código sí deja claro: el experto no tiene ninguna\nrazón para pasar tiempo pegado al muro equivocado o cruzado en la pista. Si la policy llega ahí,\ntiene que **extrapolar**, y extrapolar fuera de la distribución de entrenamiento es mucho más difícil\nque interpolar dentro de ella.\n\n**El propio experto era frágil fuera de su estado nominal.** En una versión anterior, aparecer sobre\nla trazada ideal en lugar de sobre el centro bastaba para que el piloto heurístico entrara en una\noscilación y terminara en un trompo: su corrección hacia el centro era demasiado fuerte para ese\npunto de partida. Y el código del evaluador lo dice explícitamente: el arranque con ruido del\nentrenamiento detiene a la heurística antes de que llegue a estabilizarse en la línea, por eso el\nexperto solo se evaluó y se grabaría desde arranques limpios. Un experto que no sabe recuperarse de\nuna situación no puede enseñar a recuperarse de ella.\n\nY hoy habría un problema más: todas las arenas usan el mismo circuito fijo, así que las\ndemostraciones cubrirían un único trazado.\n\n> Tener muchas demostraciones no significa tener una policy robusta. La cobertura importa tanto como\n> la cantidad.\n\n---\n\n## 10. La calidad del experto también importa\n\nBC aprende del experto, y eso incluye sus defectos. Si el experto frena de más, toma trayectorias\nineficientes, comete errores o produce acciones ruidosas, BC puede aprender todo eso.\n\n$$\n\\text{policy aprendida} \\approx \\text{comportamiento demostrado}\n$$\n\nNo necesariamente:\n\n$$\n\\text{policy aprendida} \\approx \\text{comportamiento óptimo}\n$$\n\nAgentic Racing tuvo un ejemplo muy concreto. Antes de grabar, se revisó con más detalle cómo manejaba\nel piloto heurístico en las pistas procedurales de entonces. Solo 2 o 3 de cada 9 autos manejaban\nbien; los demás se quedaban en un vaivén cerca del punto de partida: chocaban con el borde, hacían\nuna reversa corta, volvían a frenarse y repetían, durante los 80 segundos del episodio, sin avanzar.\nLa bitácora lo dice sin rodeos: esas trayectorias \"llenarían las demos de basura\". La solución fue\nagregar un corte por falta de progreso (menos de 8 metros en 5 segundos termina el episodio), para\nque los autos atascados se reiniciaran en lugar de grabar minutos de un experto que no estaba\nconduciendo.\n\nFue la primera de varias veces en que grabar las demostraciones se pospuso porque el profesor todavía\nno estaba listo.\n\n> BC es imitación. No es, por sí mismo, un algoritmo cuyo objetivo sea superar al experto.\n\n---\n\n## 11. Entonces, ¿para qué sirve BC?\n\nA pesar de esas limitaciones, BC puede ser muy útil, sobre todo como **punto de partida**:\n\n```text\n                 Experto\n                    │\n                    ▼\n             demostraciones\n                    │\n                    ▼\n            Behavioral Cloning\n                    │\n                    ▼\n             policy inicial\n                    │\n                    ▼\n                  PPO\n                    │\n                    ▼\n             policy mejorada\n```\n\nPPO ya no empieza desde cero: empieza con una policy que sabe hacer algo razonable. Y eso ataca\ndirectamente el problema de la parte 2. El agente no necesita encontrar por azar la secuencia\nfrenar–girar–acelerar si alguien se la muestra. Basta con que BC la deje en una zona donde la policy\nla pruebe con frecuencia, para que PPO pueda reforzarla.\n\nPor eso imitation learning y reinforcement learning se complementan.\n\n---\n\n## 12. BC + PPO: enseñar primero, optimizar después\n\nPodemos pensar en dos profesores:\n\n- **El experto** dice: \"así es como yo conduzco\".\n- **El entorno** dice: \"esto es lo que realmente funciona para conseguir el objetivo\".\n\nBC escucha al experto. PPO escucha la recompensa.\n\nEn ML-Agents, como vimos en la sección 6, los dos escuchan **a la vez**, con un volumen que cambia con\nel tiempo: el peso de BC empieza alto y se apaga, mientras PPO sigue durante todo el entrenamiento.\n\n![BC y PPO en ML-Agents con la configuración que se preparó para Agentic Racing: el peso de BC empieza en la mitad de la tasa de aprendizaje de PPO y decae linealmente hasta cero a los 2 millones de pasos; PPO actualiza la policy durante los 10 millones de pasos de la corrida.](/images/bc-plus-ppo.svg)\n\nEs una forma suave de \"enseñar primero, optimizar después\": no hay un corte entre las dos etapas, sino\nuna transición. En Agentic Racing ese pipeline existió en la configuración, pero nunca se ejecutó.\n\n---\n\n## 13. ¿Y dónde entra GAIL?\n\nBC intenta aprender directamente $\\text{observación} \\rightarrow \\text{acción}$.\n\n**GAIL** (*Generative Adversarial Imitation Learning*) plantea otra pregunta:\n\n> ¿El comportamiento de mi agente se parece al del experto?\n\nEntrena un **discriminador** que intenta distinguir entre experiencias del experto y experiencias del\nagente. El agente recibe una recompensa adicional cuando sus experiencias son difíciles de distinguir\nde las del experto. En ML-Agents, GAIL es literalmente eso: una señal de recompensa más, que se suma a\nla recompensa del entorno y que PPO optimiza como cualquier otra.\n\nLa configuración de la octava corrida también tenía un bloque de GAIL, con un peso de 0.15 y comparando\nacciones además de observaciones.\n\nAsí que cada técnica responde una pregunta distinta:\n\n| Técnica | Pregunta principal |\n|---|---|\n| Behavioral Cloning | ¿Puedes hacer lo que hizo el experto en este estado? |\n| GAIL | ¿Tu comportamiento se parece al del experto? |\n| PPO | ¿Puedes maximizar la recompensa de la tarea? |\n\nGAIL merece su propio artículo, y es la [parte 4](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/) de la serie.\n\n---\n\n## 14. El verdadero desafío: ¿qué estamos enseñando?\n\nBC nos obliga a hacernos una pregunta más profunda:\n\n> ¿La observación contiene suficiente información para tomar la decisión correcta?\n\nSupongamos dos situaciones distintas:\n\n```text\nsituación A → el experto gira a la izquierda\nsituación B → el experto gira a la derecha\n```\n\nSi ambas producen exactamente la misma observación para la policy, el problema no es la red neuronal:\nla información simplemente no está. Y con una pérdida de error cuadrático, la policy no elige una de\nlas dos respuestas: aprende **el promedio**, que puede ser no girar en absoluto.\n\nEsto en Agentic Racing es concreto, porque el piloto heurístico **no decide solo a partir de las 42\nobservaciones** que recibiría la policy:\n\n- **Tiene memoria.** Su volante pasa por un filtro de suavizado: cada comando mezcla el nuevo cálculo\n  con el comando anterior. La policy de la parte 2 es una red sin memoria, que solo ve la observación\n  actual.\n- **Usa temporizadores.** Si el auto lleva un rato lento y contra el muro (el código lo cuenta como\n  más de un segundo), el experto hace una reversa de 0.6 segundos para soltarse. Mismo estado\n  visible, acciones distintas según cuánto tiempo lleve atascado. Para la policy, eso sería\n  exactamente la situación A y B de arriba: a veces acelerar, a veces dar reversa, ante la misma\n  observación.\n- **Mira la pista de otra forma.** El experto busca la curva más cerrada en los próximos 60 metros y\n  apunta a un punto concreto entre la trazada y el centro. La policy no hace ese escaneo: recibe tres\n  números de curvatura (0–22, 18–45 y 40–75 metros por delante), los raycasts contra los muros y su\n  relación con la trazada, pero no el punto exacto al que apunta el experto.\n\nNada de esto impide que BC funcione. Pero significa que la policy no podría reproducir al experto con\nexactitud, y que justamente en las maniobras de recuperación, donde el experto depende de su memoria,\nla imitación sería peor.\n\nEsto conecta con la sección 3 de la\n[parte 1](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/): **diseñar las\nobservaciones también forma parte del problema**. Un dataset gigantesco no puede compensar una\nobservación que no contiene la información que usó el experto.\n\n---\n\n## 15. Las demostraciones también son un problema de ingeniería\n\nCrear demostraciones no consiste en grabar muchas horas. Hay que preguntarse:\n\n| Pregunta | En Agentic Racing |\n|---|---|\n| ¿El experto es realmente bueno? | En el circuito fijo, sí: completa vueltas enteras. En las pistas procedurales de entonces, no, aunque buena parte de la culpa era del entorno (curvas demasiado cerradas y un agarre lateral que frenaba el auto en cada giro). |\n| ¿Cubre situaciones diferentes? | Sí en directivas (una al azar por episodio). No en trazados: hoy hay un solo circuito. |\n| ¿Hay curvas fáciles y difíciles? | El circuito fijo tiene cuatro curvas, todas del mismo radio. |\n| ¿Hay situaciones de recuperación? | Casi no: las grabaciones empiezan centradas y alineadas, y el experto no suele meterse en situaciones de las que tenga que salir. |\n| ¿Hay acciones inconsistentes? | Sí: las que dependen de la memoria y los temporizadores del experto. |\n| ¿Los estados se parecen a los que verá la policy? | No del todo: el entrenamiento empieza con ruido de rumbo y de posición que la grabación no tiene. |\n\nSi todas las demostraciones son perfectas, quizá enseñemos muy bien a conducir perfectamente, pero no\na recuperarse cuando algo sale mal. Para conducción, las demostraciones de recuperación pueden ser tan\nimportantes como las ideales.\n\nHay un precedente famoso. ALVINN, uno de los primeros vehículos que aprendió a conducirse con una red\nneuronal (Pomerleau, 1989), terminó entrenándose imitando a un conductor humano. Para que aprendiera\na corregir, Pomerleau (1991) generaba además imágenes desplazadas y rotadas, como si el vehículo\nestuviera corrido hacia un lado, con la corrección que habría hecho falta. Fabricaba los ejemplos de\nrecuperación que el conductor nunca había necesitado mostrar.\n\n---\n\n## 16. ¿Qué deberíamos medir?\n\nNo conviene evaluar BC solo por su loss. Una policy puede tener un error de imitación muy pequeño y\naun así conducir mal.\n\n**Métricas de imitación:**\n\n- la pérdida de BC (`Losses/Pretraining Loss` en ML-Agents);\n- el error por componente de la acción: `steer`, `throttle` y `brake` por separado.\n\n**Métricas de comportamiento:**\n\n- fracción de vuelta completada;\n- vueltas completadas y tiempo por vuelta;\n- motivo por el que termina cada episodio (salida de pista, auto detenido, sin progreso);\n- velocidad y uso del freno.\n\nAgentic Racing ya tiene casi todas las de comportamiento: son las que reporta su evaluador, el mismo\nque en la parte 2 mostró que la recompensa media subía mientras la fracción de vuelta no se movía.\n\n> Un loss bajo no garantiza una buena policy. Lo que queremos medir es el comportamiento fuera del\n> dataset.\n\n---\n\n## 17. El experimento que vale la pena hacer\n\nPara Agentic Racing, la comparación interesante sería:\n\n```text\nPPO desde cero                    demostraciones → BC + PPO\n      │                                       │\n      ▼                                       ▼\n  resultado A                             resultado B\n```\n\ncomparando la fracción de vuelta, las vueltas completadas, las salidas de pista, la estabilidad y\ncuántos pasos hace falta para llegar a cada nivel, y probando después ante situaciones que no estaban\nen las demostraciones.\n\nEntonces la pregunta deja de ser \"¿BC funciona?\" y pasa a ser:\n\n> ¿Cuánto facilita el aprendizaje empezar con una policy que ya sabe imitar un comportamiento\n> razonable?\n\nEste experimento **no se hizo**. Hoy sería más fácil de hacer que en su momento: en el circuito\nfijo, donde la pista ya no es un obstáculo, el experto completa vueltas, y el grabador y el evaluador\nexisten. Faltaría volver a agregar los bloques de BC a la configuración, que las instrucciones dan\npor presentes. Queda pendiente.\n\n---\n\n## 18. DAgger: enseñar también a recuperarse\n\nLa solución clásica al *covariate shift* es **DAgger** (*Dataset Aggregation*), de Ross, Gordon y\nBagnell:\n\n1. entrenar con las demostraciones iniciales;\n2. ejecutar la policy;\n3. registrar los estados que la policy visita;\n4. preguntarle al experto qué habría hecho en cada uno;\n5. agregar esos ejemplos al dataset;\n6. volver a entrenar;\n7. repetir.\n\n```text\ndemostraciones iniciales\n          │\n          ▼\n         BC\n          │\n          ▼\n       policy\n          │\n          ▼\n    ejecutar policy\n          │\n          ▼\n estados fuera de distribución\n          │\n          ▼\n   consultar al experto\n          │\n          ▼\n    nuevos ejemplos ──────► BC\n```\n\nDAgger es interesante porque el experto enseña justo en las situaciones donde la policy tiene\nproblemas.\n\nCon un experto humano, DAgger es caro: alguien tiene que etiquetar miles de estados. Con un experto\nque es un programa, como el piloto heurístico de Agentic Racing, es mucho más barato: basta con\ncorrer la heurística \"en la sombra\" junto a la policy, en los mismos estados que la policy visita, y\nguardar qué habría hecho. Tiene que correr a la par y no como una consulta suelta, porque, como\nvimos en la sección 14, la heurística tiene memoria. ML-Agents no trae DAgger incluido, así que\nhabría que construirlo. Tampoco se hizo, pero es probablemente la mejor respuesta a los problemas de\nlas secciones 9 y 15.\n\n---\n\n## 19. BC no reemplaza al reinforcement learning\n\nBC puede funcionar muy bien cuando hay buenas demostraciones, el problema está bien cubierto y la\npolicy no necesita alejarse mucho de los ejemplos.\n\nPero si queremos que el agente descubra mejores estrategias, supere al experto, se adapte a\nsituaciones nuevas u optimice una recompensa específica, necesitamos algo más. En el caso de Agentic\nRacing, superar al experto no es un detalle: el piloto heurístico frena según una regla fija, y un\nagente que aprendiera de la recompensa podría encontrar algo mejor.\n\n```text\nExperto\n   │\n   ▼\nDemostraciones\n   │\n   ▼\nBehavioral Cloning ──► policy inicial\n                           │\n                           ├──► GAIL: ¿se parece al experto?\n                           │\n                           ▼\n                          PPO: ¿maximiza la recompensa?\n                           │\n                           ▼\n                    policy optimizada\n```\n\nEn ML-Agents, las tres piezas pueden combinarse en una sola corrida, que es lo que preparaba la\nconfiguración de la octava corrida. Esa secuencia es una propuesta para Agentic Racing, no algo que\nse haya ejecutado.\n\n---\n\n## Conclusión: sí, podemos enseñar imitando\n\nBehavioral Cloning transforma una parte del problema. En lugar de decir \"explora hasta descubrir qué\nfunciona\", decimos \"mira estos ejemplos y aprende a comportarte como el experto\". Eso puede ser muy\npoderoso.\n\nPero tiene una debilidad fundamental: el agente puede encontrarse con situaciones que nunca\naparecieron en las demostraciones. Un pequeño error lo saca de la distribución del experto, ese error\ngenera otro, y ese otro genera otro.\n\nAgentic Racing agrega una segunda lección, menos obvia: **imitar exige un experto confiable y una\nobservación que contenga lo que el experto usa para decidir**. El proyecto tenía un experto, un\ngrabador y una configuración, pero cada vez que iba a grabar descubría algo que arreglar: un experto\nque se atascaba, pistas que nadie podía recorrer, una física que convertía cada giro en un frenazo. Y\naun con todo arreglado, ese experto decide con memoria y temporizadores que la policy no podría ver.\n\nPara Agentic Racing, la pregunta interesante no es solo si podemos enseñar imitando, sino:\n\n> ¿Podemos usar la imitación como punto de partida y después conseguir que el agente supere las\n> limitaciones de sus demostraciones?\n\nAhí es donde Behavioral Cloning se conecta con GAIL y con PPO.\n\n**Siguiente en la serie:** la [parte 4, \"¿Podemos aprender a comportarnos como un experto? GAIL\nexplicado desde cero\"](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/).\n\nSi llegaste directo a este artículo, la\n[parte 1](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/) explica los conceptos\nbase, la [parte 2](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/) abre\nPPO por dentro, y\n[Agentic Racing: un piloto que nunca aprendió a manejar](/es/blog/agentic-racing-un-piloto-que-nunca-aprendio-a-manejar/)\ncuenta la historia completa del proyecto.\n\n---\n\n## Referencias\n\n- Pomerleau, D. (1989). ALVINN: An Autonomous Land Vehicle in a Neural Network. *Advances in Neural Information Processing Systems 1*.\n- Pomerleau, D. (1991). Efficient Training of Artificial Neural Networks for Autonomous Navigation. *Neural Computation*, 3(1).\n- Ross, S. y Bagnell, J. A. (2010). [Efficient Reductions for Imitation Learning](https://proceedings.mlr.press/v9/ross10a.html). *AISTATS*.\n- Ross, S., Gordon, G. y Bagnell, J. A. (2011). [A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning](https://arxiv.org/abs/1011.0686). *AISTATS*.\n- Ho, J. y Ermon, S. (2016). [Generative Adversarial Imitation Learning](https://arxiv.org/abs/1606.03476). *NeurIPS*.\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), en\n  particular el módulo de BC (`ml-agents/mlagents/trainers/torch_entities/components/bc/module.py`) y\n  la recompensa de GAIL (`.../reward_providers/gail_reward_provider.py`) del release de `mlagents`\n  1.1.0.\n- El experto (`RaceAgent.cs`), el grabador (`EvalRunner.cs`), las instrucciones de imitación\n  (`training/README.md`) y la bitácora del proyecto están en\n  [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero","lang":"es","title":"¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero","description":"Segunda parte de la serie Reinforcement Learning desde cero con Agentic Racing. Abrimos PPO por dentro: policy gradient, la razón de probabilidades, el clipping, actor-critic y GAE, cada pieza conectada con la configuración real de ML-Agents del proyecto y con lo que pasó cuando PPO optimizó exactamente lo que le pedimos.","publishDate":"2026-10-03","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch","url":"https://alexisalulema.com/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/","series":{"id":"rl-from-scratch","position":2},"headings":[{"depth":2,"slug":"dónde-nos-quedamos","text":"Dónde nos quedamos"},{"depth":2,"slug":"1-un-repaso-de-treinta-segundos","text":"1. Un repaso de treinta segundos"},{"depth":2,"slug":"2-policy-gradient-empujar-las-probabilidades-en-la-dirección-correcta","text":"2. Policy gradient: empujar las probabilidades en la dirección correcta"},{"depth":2,"slug":"3-la-razón-de-probabilidades-cuánto-cambió-la-policy","text":"3. La razón de probabilidades: ¿cuánto cambió la policy?"},{"depth":2,"slug":"4-clipping-quitarle-el-incentivo-a-cambiar-demasiado","text":"4. Clipping: quitarle el incentivo a cambiar demasiado"},{"depth":2,"slug":"5-actor-critic-una-red-decide-y-otra-estima","text":"5. Actor-Critic: una red decide y otra estima"},{"depth":2,"slug":"6-td-error-y-gae-estimar-el-advantage","text":"6. TD error y GAE: estimar el advantage"},{"depth":2,"slug":"7-el-ciclo-de-entrenamiento-de-ppo","text":"7. El ciclo de entrenamiento de PPO"},{"depth":2,"slug":"8-lo-que-ppo-hizo-en-agentic-racing","text":"8. Lo que PPO hizo en Agentic Racing"},{"depth":2,"slug":"9-cuando-ppo-no-aprende-el-problema-puede-no-ser-ppo","text":"9. Cuando PPO no aprende, el problema puede no ser PPO"},{"depth":2,"slug":"10-ppo-no-es-la-única-herramienta","text":"10. PPO no es la única herramienta"},{"depth":2,"slug":"conclusión-mejorar-sin-destruir-lo-que-ya-funciona","text":"Conclusión: mejorar sin destruir lo que ya funciona"},{"depth":2,"slug":"referencias","text":"Referencias"}],"body":"> **Serie: Reinforcement Learning desde cero, con Agentic Racing como laboratorio**\n>\n> 1. Parte 1 · [¿Cómo aprende un agente? Enseñando a un automóvil a conducir](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/)\n> 2. Parte 2 · **¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero** (este artículo)\n> 3. Parte 3 · [¿Podemos enseñar imitando? Behavioral Cloning explicado desde cero](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/)\n> 4. Parte 4 · [¿Podemos aprender a comportarnos como un experto? GAIL explicado desde cero](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/)\n> 5. Parte 5 · [¿Podemos combinar imitación y RL? BC, GAIL y PPO en un mismo sistema](/es/blog/podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema/)\n> 6. Parte 6 · [¿Cómo le decimos a un agente qué queremos? Reward engineering explicado desde cero](/es/blog/como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero/)\n> 7. Parte 7 · [¿Qué debería poder ver un agente? Observation design explicado desde cero](/es/blog/que-deberia-poder-ver-un-agente-observation-design-explicado-desde-cero/)\n> 8. Parte 8 · [¿Qué hacemos cuando el agente no aprende? RL debugging explicado desde cero](/es/blog/que-hacemos-cuando-el-agente-no-aprende-rl-debugging-explicado-desde-cero/)\n\n## Dónde nos quedamos\n\nLa [primera parte](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/) terminó\ncon un mapa conceptual: el agente observa, elige una acción, recibe una recompensa, acumula\nexperiencia y estima qué estados y acciones son buenos. Lo que faltaba era la última flecha: **cómo\nse usa esa experiencia para mejorar la policy**.\n\nY terminó con una pregunta:\n\n> **¿Por qué PPO funciona mejor cuando le impedimos cambiar demasiado rápido de opinión?**\n\nEste artículo responde esa pregunta. Imagina un piloto virtual que ya aprendió algo útil (mantenerse\nen pista en las rectas, tomar alguna curva suave) pero todavía comete errores. Queremos que mejore\nsin que una actualización del entrenamiento destruya lo que ya funciona.\n\n**Proximal Policy Optimization (PPO)** es un algoritmo que actualiza la policy (la estrategia que\ndecide qué acción tomar) controlando cuánto cambia en cada actualización. *Proximal* significa\ncercano: la policy nueva no debería alejarse demasiado de la que generó las experiencias con las\nque está aprendiendo.\n\nComo en la parte 1, cada idea va acompañada de lo que realmente hace el entrenador de\n[Unity ML-Agents](https://github.com/Unity-Technologies/ml-agents) (versión 1.1.0 del paquete de\nPython) y de cómo estaba configurado en Agentic Racing. Y, como en la parte 1, con una advertencia\nhonesta: en este proyecto el piloto RL nunca aprendió a dar vueltas completas de forma consistente. Eso no le quita valor\nal ejemplo. Ver qué optimizó PPO cuando no aprendió lo que queríamos es una de las mejores formas\nde entender qué optimiza.\n\n---\n\n## 1. Un repaso de treinta segundos\n\nDe la parte 1 necesitamos tres piezas.\n\nLa **policy** es una función parametrizada que asigna probabilidades a las acciones:\n\n$$\n\\pi_\\theta(a \\mid s)\n$$\n\ndonde $s$ es el estado (en la práctica, la observación del agente), $a$ es una acción y $\\theta$\nson los pesos de la red neuronal. En Agentic Racing, $s$ son 42 números (raycasts, velocidad,\nrelación con la pista, curvatura por delante y la directiva del estratega) y $a$ son tres acciones\ncontinuas: `steer`, `throttle` y `brake`. Como las acciones son continuas, la red produce el centro\nde una distribución normal para cada una, y la acción se muestrea de esa distribución.\n\nEl **advantage** compara una acción con lo que normalmente se espera en ese estado:\n\n$$\nA(s,a) = Q(s,a) - V(s)\n$$\n\n- $A(s,a) > 0$: la acción fue mejor de lo esperado.\n- $A(s,a) < 0$: fue peor de lo esperado.\n- $A(s,a) \\approx 0$: estuvo cerca de lo esperado.\n\nY el **crítico** es la parte del sistema que estima $V(s)$, el retorno esperado desde un estado.\n\nCon eso alcanza.\n\n---\n\n## 2. Policy gradient: empujar las probabilidades en la dirección correcta\n\nLa idea más directa para mejorar una policy es esta: **subir la probabilidad de las acciones que\nsalieron mejor de lo esperado y bajar la de las que salieron peor**. Eso es lo que hacen los métodos\nde *policy gradient*. Su forma más conocida estima el gradiente del rendimiento así:\n\n$$\n\\nabla_\\theta J(\\theta) \\approx \\mathbb{E}_t\\left[\\nabla_\\theta \\log \\pi_\\theta(a_t \\mid s_t)\\, \\hat{A}_t\\right]\n$$\n\nNo hace falta seguir la derivación para entender la intuición:\n\n- $\\nabla_\\theta \\log \\pi_\\theta(a_t \\mid s_t)$ indica en qué dirección mover los pesos para que la\n  acción $a_t$ sea más probable en el estado $s_t$.\n- $\\hat{A}_t$ (el gorro indica que es una estimación) decide el sentido y el tamaño del empujón: si\n  la acción salió mejor de lo esperado, la hacemos más probable; si salió peor, menos.\n\nEsto funciona, pero tiene dos problemas prácticos.\n\n**El primero: el tamaño del paso.** $\\hat{A}_t$ es una estimación ruidosa. Si damos un paso\ndemasiado grande en la dirección del gradiente, la policy puede cambiar tanto que empieza a hacer\ncosas que nunca había probado y para las que nuestras estimaciones no dicen nada. Un mal paso puede\ndeshacer en una actualización lo que costó millones de pasos aprender.\n\n**El segundo: la experiencia es cara.** En Agentic Racing, cada actualización necesita 20 480\ndecisiones. A 10 decisiones por segundo, son unos 34 minutos de conducción simulada, repartidos\nentre los 36 autos que entrenan en paralelo. Usar esa experiencia para un solo paso de gradiente y\ntirarla sería un desperdicio. Queremos reutilizarla varias veces.\n\nPero en cuanto damos el primer paso de gradiente, la policy ya no es la que recogió esa\nexperiencia. Necesitamos una forma de medir cuánto se alejó. Ahí entra la razón de probabilidades.\n\n---\n\n## 3. La razón de probabilidades: ¿cuánto cambió la policy?\n\nPPO compara la policy que estamos optimizando con la que recogió las experiencias:\n\n$$\nr_t(\\theta) = \\frac{\\pi_\\theta(a_t \\mid s_t)}{\\pi_{\\theta_{\\text{old}}}(a_t \\mid s_t)}\n$$\n\n- $r_t = 1$: la probabilidad de esa acción no cambió.\n- $r_t = 1.2$: la policy nueva le asigna a esa acción una probabilidad un 20% mayor.\n- $r_t = 0.8$: la probabilidad es un 20% menor.\n\nCon acciones continuas, como en nuestro auto, en lugar de probabilidades se comparan densidades de\nprobabilidad, pero la idea es la misma. En el código de ML-Agents se calcula a partir de logaritmos,\nque es numéricamente más estable: `r_theta = torch.exp(log_probs - old_log_probs)`.\n\nUn detalle que conviene no perder de vista: esta razón compara probabilidades de acciones. **No**\nsignifica que el auto haya mejorado un 20% en velocidad, seguridad o tiempo de vuelta.\n\nCon esta razón, el objetivo de policy gradient se puede reescribir como $r_t(\\theta)\\,\\hat{A}_t$.\nAl inicio de la actualización $r_t = 1$ y los gradientes de ambas formas coinciden. A medida que la\npolicy cambia, $r_t$ corrige el hecho de que estamos evaluando acciones elegidas por una policy\nanterior. Solo corrige las acciones, no los estados: las situaciones en las que se encontró el auto\nsiguen siendo las que produjo la policy vieja. Esa es otra razón para no alejarse demasiado. Eso\npermite reutilizar el mismo lote de experiencia durante varias pasadas.\n\nPero también abre la puerta al problema del tamaño del paso: si maximizamos $r_t\\,\\hat{A}_t$ sin\nlímite, el optimizador va a querer empujar $r_t$ hacia arriba sin freno en cada acción con advantage\npositivo. Necesitamos un freno.\n\n---\n\n## 4. Clipping: quitarle el incentivo a cambiar demasiado\n\nEl objetivo *clipped* de PPO es este:\n\n$$\nL^{\\text{CLIP}}(\\theta) = \\mathbb{E}_t\\left[\\min\\Big(r_t(\\theta)\\,\\hat{A}_t,\\ \\operatorname{clip}\\big(r_t(\\theta),\\, 1-\\epsilon,\\, 1+\\epsilon\\big)\\,\\hat{A}_t\\Big)\\right]\n$$\n\nTiene tres partes:\n\n1. $r_t(\\theta)$ mide cuánto cambió la probabilidad de una acción.\n2. $\\hat{A}_t$ estima si esa acción fue mejor o peor de lo esperado.\n3. `clip` recorta la razón al intervalo $[1-\\epsilon,\\, 1+\\epsilon]$ en uno de los dos términos, y\n   el `min` se queda con el más pesimista de ambos.\n\nEn Agentic Racing, $\\epsilon$ empezaba en 0.2, así que el intervalo era:\n\n$$\n[1-\\epsilon,\\ 1+\\epsilon] = [0.8,\\ 1.2]\n$$\n\nLa forma más clara de entenderlo es ver qué pasa en los cuatro casos posibles:\n\n| Caso | Ejemplo | ¿Qué término gana el `min`? | Efecto |\n|---|---|---|---|\n| Acción buena ($\\hat{A} > 0$) que ya subió mucho | $r_t = 1.5$ | el recortado: $1.2\\,\\hat{A}$ | el objetivo deja de crecer; no hay más incentivo para subirla |\n| Acción buena ($\\hat{A} > 0$) que bajó | $r_t = 0.5$ | el sin recortar: $0.5\\,\\hat{A}$ | el gradiente sigue empujando para subirla de nuevo |\n| Acción mala ($\\hat{A} < 0$) que ya bajó mucho | $r_t = 0.5$ | el recortado: $0.8\\,\\hat{A}$ | el objetivo deja de mejorar; no hay más incentivo para bajarla |\n| Acción mala ($\\hat{A} < 0$) que subió | $r_t = 1.5$ | el sin recortar: $1.5\\,\\hat{A}$ | el gradiente sigue empujando para bajarla |\n\n![El objetivo clipped de PPO en función de la razón r. Con advantage positivo, el objetivo crece con r hasta 1.2 y luego se vuelve plano. Con advantage negativo, el objetivo mejora al bajar r hasta 0.8 y por debajo de ese punto se vuelve plano. Fuera de esas zonas planas, el objetivo sigue la recta sin recortar.](/images/ppo-clipped-objective.svg)\n\nEl patrón es asimétrico a propósito. PPO **deja de premiar** los cambios que se alejan demasiado en\nla dirección \"buena\", pero **no deja de castigar** los cambios que van en la dirección equivocada. Es\nuna cota pesimista: el objetivo nunca es más optimista que la versión sin recortar.\n\nY esta es la respuesta a la pregunta con la que terminó la parte 1. PPO funciona mejor cuando le\nimpedimos cambiar demasiado rápido de opinión porque **las estimaciones del advantage solo son\nconfiables cerca de la policy que generó los datos**. Si la policy se aleja mucho, empieza a tomar\ndecisiones en situaciones que nadie midió, con advantages calculados para otra policy. El clipping\nmantiene cada actualización en la zona donde los datos todavía dicen algo cierto. Así se puede\nreutilizar el lote varias veces sin que una estimación ruidosa arrastre a la policy lejos de lo que\nya funcionaba.\n\nUn matiz importante: el clipping **no prohíbe** que $r_t$ salga del intervalo $[0.8,\\ 1.2]$.\nElimina el beneficio que el objetivo le atribuye a ciertos cambios extremos, según el signo del\nadvantage, pero la razón todavía puede pasar de ese límite durante una actualización. Es un\nmecanismo práctico de optimización, no una garantía absoluta de que cada actualización sea pequeña.\n\nEn ML-Agents, la implementación sigue la ecuación muy de cerca (la pérdida se minimiza, por eso el\nsigno negativo):\n\n```python\nr_theta = torch.exp(log_probs - old_log_probs)\np_opt_a = r_theta * advantage\np_opt_b = torch.clamp(r_theta, 1.0 - epsilon, 1.0 + epsilon) * advantage\npolicy_loss = -1 * masked_mean(torch.min(p_opt_a, p_opt_b), loss_masks)\n```\n\nUn detalle que la ecuación no muestra: con acciones continuas, ML-Agents calcula y recorta **una\nrazón por cada dimensión de la acción**. En nuestro auto son tres razones (`steer`, `throttle` y\n`brake`), cada una con su propio recorte, que después se promedian, en lugar de una sola razón para\nla acción conjunta.\n\n---\n\n## 5. Actor-Critic: una red decide y otra estima\n\nLas implementaciones habituales de PPO usan un esquema **actor-critic**:\n\n- El **actor** es la policy: recibe la observación y decide qué acción tomar.\n- El **crítico** estima $V(s)$, el retorno esperado desde el estado actual.\n\nEl crítico sirve para calcular si las experiencias fueron mejores o peores de lo esperado. No es\ninfalible: si sus estimaciones están mal, los advantages también lo estarán, y el actor aprenderá de\nseñales equivocadas.\n\nEn ML-Agents, con la configuración por defecto, el actor y el crítico son **dos redes separadas** con\nla misma forma. En Agentic Racing eran dos MLP de 2 capas de 256 neuronas cada una. Las dos se\nentrenan juntas, con una única función de pérdida:\n\n$$\nL = L^{\\text{policy}} + 0.5\\, L^{\\text{value}} - \\beta\\, H(\\pi_\\theta)\n$$\n\n- $L^{\\text{policy}}$ es el objetivo *clipped* de la sección anterior, con el signo cambiado.\n- $L^{\\text{value}}$ es el error del crítico al predecir el retorno. En ML-Agents también se recorta\n  con el mismo $\\epsilon$: si la predicción nueva se aleja más de $\\epsilon$ de la anterior, el\n  error se calcula con la versión más pesimista, así que alejarse más no reduce la pérdida.\n- $H(\\pi_\\theta)$ es la entropía de la policy, y $\\beta$ su peso. Restarla premia que la policy\n  no se vuelva demasiado segura demasiado pronto, para que siga explorando. En Agentic Racing,\n  $\\beta$ fue 0.005 en la primera corrida y 0.01 desde la segunda, siempre como valor inicial.\n\nLa parte 1 mostró este crítico en acción. En las primeras corridas, su error (la métrica\n`Value Loss` de ML-Agents) subía a lo largo del entrenamiento: no lograba anticipar lo que iba a\npasar. En la séptima corrida, cuando el agente empezó a ver la curvatura de la pista por delante, quedó\nestable en 0.22, aunque esa misma corrida también cambió la recompensa de velocidad.\n\n---\n\n## 6. TD error y GAE: estimar el advantage\n\nQueda una pregunta: ¿de dónde sale $\\hat{A}_t$?\n\nUna señal habitual es el **error de diferencia temporal** (*TD error*):\n\n$$\n\\delta_t = r_t + \\gamma\\, V(s_{t+1}) - V(s_t)\n$$\n\nAquí $r_t$ es la recompensa (no confundir con la razón $r_t(\\theta)$ de la sección 3; la notación\nhabitual usa la misma letra para las dos), $\\gamma$ es el factor de descuento y $V$ son las\nestimaciones del crítico. $\\delta_t$ responde una pregunta concreta: \"después de esta acción, ¿las\ncosas salieron mejor o peor de lo que el crítico esperaba?\".\n\nUn solo $\\delta_t$ mira apenas un paso hacia adelante. Sumar muchos mira más lejos pero acumula más\nruido. PPO suele usar **Generalized Advantage Estimation (GAE)**, que combina ambos extremos:\n\n$$\n\\hat{A}_t^{\\text{GAE}(\\gamma,\\lambda)} = \\sum_{l=0}^{\\infty} (\\gamma\\lambda)^l\\, \\delta_{t+l}\n$$\n\n$\\gamma$ controla el peso de las recompensas futuras y $\\lambda$ cómo se combinan los TD errors a lo\nlargo del tiempo. Con $\\lambda = 0$ solo cuenta el paso inmediato (poco ruido, pero depende mucho de\nque el crítico acierte). Con $\\lambda = 1$ cuenta todo lo que pasó después (menos dependencia del\ncrítico, pero mucho más ruido).\n\nEn Agentic Racing, $\\gamma = 0.995$ y $\\lambda = 0.95$, así que el factor de la suma es\n$\\gamma\\lambda \\approx 0.945$. Eso tiene una lectura concreta:\n\n- el peso de un TD error se reduce a la mitad cada ~12 decisiones, o sea, cada ~1.2 segundos;\n- la mayor parte del advantage de una acción se decide por lo que pasa en los dos o tres segundos\n  siguientes.\n\nEs interesante compararlo con la parte 1, donde vimos que el return mira unos 20 segundos hacia\nadelante. Las dos cosas son compatibles: GAE mira poco hacia adelante de forma explícita, pero cada\n$\\delta$ incluye $V(s_{t+1})$, y en esa estimación del crítico ya está resumido todo el futuro más\nlejano. Por eso un crítico que no entiende el mundo arruina los advantages, por bien diseñado que\nesté el resto.\n\nEn la práctica, ML-Agents calcula la suma sobre las experiencias disponibles. Cuando una trayectoria\nse corta antes de terminar el episodio (por ejemplo, al alcanzar el límite de pasos), usa la\nestimación del crítico del último estado para no fingir que el futuro vale cero. Y antes de\noptimizar, **normaliza los advantages** del lote (media 0, desviación 1), así que lo que importa es\nqué acciones salieron mejor que las demás de ese mismo lote, no el valor absoluto.\n\n---\n\n## 7. El ciclo de entrenamiento de PPO\n\nCon todas las piezas, el ciclo completo queda así:\n\n1. Usar la policy actual para interactuar con el entorno.\n2. Recoger trayectorias: observaciones, acciones, recompensas y las probabilidades que la policy\n   les asignó.\n3. Estimar valores con el crítico y advantages con GAE.\n4. Optimizar durante varias épocas sobre el lote, en minibatches.\n5. En cada minibatch, calcular la razón entre la policy nueva y la anterior.\n6. Aplicar el objetivo *clipped*.\n7. Actualizar actor y crítico con la pérdida combinada.\n8. Tirar el lote y repetir con experiencias nuevas.\n\n![El ciclo de PPO en ML-Agents con los números de Agentic Racing: 36 autos recogen experiencia hasta llenar un buffer de 20 480 decisiones; el crítico estima V(s) y GAE calcula los advantages, que se normalizan; durante 3 épocas, 10 minibatches de 2 048 actualizan actor y crítico con el objetivo clipped; luego el buffer se descarta y la policy nueva vuelve a conducir.](/images/ppo-training-cycle.svg)\n\nPPO es un método **on-policy**: aprende de experiencias generadas por su versión actual o por una\nmuy reciente. Puede reutilizar el lote durante algunas épocas (para eso existe la razón y el\nclipping), pero no está diseñado para reutilizar indefinidamente cualquier experiencia histórica,\ncomo hacen los métodos *off-policy*.\n\nEn Agentic Racing, el ciclo tenía estos números:\n\n| Paso | Valor en `race_ppo.yaml` | Qué significa |\n|---|---|---|\n| Recolección | `buffer_size: 20480` | cada actualización usa 20 480 decisiones, juntadas por 36 autos |\n| Optimización | `num_epoch: 3`, `batch_size: 2048` | 3 pasadas por el lote, en 10 minibatches: 30 pasos de gradiente por actualización |\n| Clipping | `epsilon: 0.2`, `linear` | empieza en 0.2 y baja linealmente hasta 0.1 al final del entrenamiento |\n| Tasa de aprendizaje | `learning_rate: 3.0e-4`, `linear` | baja linealmente hasta casi cero |\n| Entropía | `beta: 1.0e-2`, `linear` | baja linealmente hasta 0.00001 |\n| GAE | `gamma: 0.995`, `lambd: 0.95` | el horizonte de la sección anterior |\n| Trayectorias | `time_horizon: 1000` | se cortan cada 1 000 decisiones como máximo; como los episodios duraban como mucho 800, en la práctica el crítico solo completaba el resto cuando se agotaba el tiempo |\n| Duración | `max_steps: 10000000` | algo menos de 490 actualizaciones de la policy en una corrida de 10 millones de pasos |\n\nLos tres *schedules* lineales hacen lo mismo en distintas perillas: al principio del entrenamiento\nla policy puede cambiar más y explorar más; al final, los cambios se vuelven cada vez más pequeños.\nEs otra forma de \"no cambiar de opinión demasiado rápido\", esta vez a lo largo de toda la corrida.\n\n---\n\n## 8. Lo que PPO hizo en Agentic Racing\n\nVolvamos a la historia de la parte 1 con estas piezas en la mano. Ocho corridas, de 4 a 20 millones\nde pasos, y la fracción media de vuelta se quedó siempre alrededor del 10% (entre un 8 y un 13%).\n¿Falló PPO?\n\nNo exactamente. Las curvas de entrenamiento muestran que PPO hizo su trabajo: **optimizó, de forma\nestable, exactamente el objetivo que le dimos**.\n\n**Convergió, y rápido.** En la primera corrida, la recompensa media pasó de −1.1 a 3.35 en los\nprimeros ~1.85 millones de pasos y después quedó plana durante los 18 millones restantes. En la\noctava, la recompensa quedó plana desde los ~2.7 millones de pasos. El patrón se repitió en casi\ntodas: una subida estable y una meseta. La excepción fue la tercera, que no llegó a aplanarse en sus\n4 millones de pasos; con más entrenamiento, la cuarta volvió a la misma meseta. Hubo ruido, pero no\ncolapsos: el tipo de inestabilidad que PPO intenta evitar nunca fue el problema.\n\n**Encontró lo que la recompensa premiaba.** En la cuarta corrida, detenerse terminaba el episodio\ncon una penalización fija (−1) que salía más barata que seguir arriesgando, y el agente aprendió a\navanzar unos 245 metros (justo hasta la primera curva de verdad), detenerse y cobrar.\nEso no es un fallo de PPO; es PPO funcionando. El agente encontró una forma de maximizar la\nrecompensa y la refinó de manera estable. El problema era el objetivo, no el optimizador.\n\n**Se volvió seguro de sí mismo.** En la primera corrida, con $\\beta = 0.005$, la entropía de la\npolicy bajó de 1.42 a 0.82. Al duplicar $\\beta$ en la segunda, se quedó en 1.27, pero la policy final\nno mejoró. El término $\\beta H$ frena el proceso, pero no lo impide: a medida que la policy encuentra\nalgo que funciona, deja de explorar alternativas. Cuando lo que \"funciona\" es un óptimo local (como en la\ntercera corrida, que aprendió a arrastrarse a 7 m/s para poder girar sin frenar), el agente se queda ahí.\n\nEsto sugiere una lectura que vale la pena explicitar. La virtud de PPO (cambios pequeños, mejoras\ngraduales cerca de lo que ya hace la policy) tiene un costo cuando la conducta buena está lejos de\nla actual. La maniobra completa de una curva (frenar, girar, volver a acelerar) es una secuencia\ncoordinada de uno o dos segundos que la policy tendría que encontrar por exploración. Un método\nde policy gradient solo puede reforzar lo que la policy ya prueba de vez en cuando, y PPO, con su\nclipping, vuelve esos pasos todavía más prudentes. Si casi nunca prueba la secuencia correcta, ningún\npolicy gradient la va a inventar.\n\nEsa es una hipótesis razonable, pero no la única causa: como contó la parte 1, había además problemas\nde física y de geometría de pista que hacían la maniobra mucho más difícil de lo que debía ser.\n\n---\n\n## 9. Cuando PPO no aprende, el problema puede no ser PPO\n\nQue el entrenamiento se ejecute no demuestra que el agente esté aprendiendo la tarea. Cuando progresa\npoco, conviene revisar de forma sistemática. Así respondió Agentic Racing a cada una de estas\npreguntas:\n\n| Pregunta | En Agentic Racing |\n|---|---|\n| **Observaciones**: ¿contienen información suficiente? | Las seis primeras corridas no veían la curvatura de la pista por delante; los raycasts llegaban a 40 m. Se agregaron tres observaciones de curvatura y rayos de 70 m en la séptima. |\n| **Acciones**: ¿permiten controlar el vehículo? | Tres acciones continuas. El freno anula el acelerador mientras está activo, y la policy casi nunca lo usó (freno medio de 0.02 en las evaluaciones). |\n| **Recompensa**: ¿incentiva lo que queremos? | No siempre. La recompensa de velocidad lineal hacía que frenar fuera puro costo, y la de detenerse abría una salida fácil (corrida 4). |\n| **Física**: ¿las consecuencias son coherentes? | No al principio. El modelo de agarre lateral borraba la velocidad lateral en lugar de redirigirla, y cada giro fuerte frenaba el auto casi en seco. |\n| **Dificultad**: ¿el entorno es resoluble? | Las pistas procedurales tenían curvas que ni un controlador escrito a mano podía tomar. En un circuito fijo y limpio, ese controlador completó vueltas enteras. |\n| **Configuración**: ¿los hiperparámetros son razonables? | Razonables, pero mejorables. La primera corrida usó 20 millones de pasos y convergió en ~1.85 millones: cerca del 90% del cómputo no aportó nada. |\n| **Evidencia**: ¿medimos la tarea o solo el entrenamiento? | La recompensa media subía; la fracción de vuelta no. Hizo falta un evaluador aparte (fracción de vuelta, motivo de fin de episodio, freno medio) para ver lo que pasaba de verdad. |\n\nConviene separar tres preguntas, porque cada una necesita evidencia distinta:\n\n1. **¿El entrenador se ejecuta como esperamos?** Logs limpios, observaciones del tamaño correcto,\n   episodios que terminan por los motivos esperados.\n2. **¿La señal de aprendizaje contiene información útil?** Recompensa, value loss y entropía que se\n   comportan de forma interpretable, sin salidas fáciles.\n3. **¿Mejora el comportamiento en la tarea?** Métricas propias de la tarea (en este caso, fracción\n   de vuelta y tiempo por vuelta) en pruebas independientes del entrenamiento.\n\nAgentic Racing respondía \"sí\" a la primera durante casi todo el proyecto, \"a medias\" a la segunda y\n\"no\" a la tercera. La curva de recompensa, por sí sola, nunca lo habría dicho.\n\n---\n\n## 10. PPO no es la única herramienta\n\nSi aprender desde cero resulta difícil, puede ayudar partir de las demostraciones de un experto:\n\n- **Behavioral cloning (BC):** aprende a imitar pares observación → acción grabados del experto, como\n  aprendizaje supervisado.\n- **Generative Adversarial Imitation Learning (GAIL):** entrena un discriminador que distingue el\n  comportamiento del agente del del experto, y usa esa señal como recompensa adicional mientras el\n  agente sigue aprendiendo con RL.\n- **PPO:** optimiza la policy con las recompensas del entorno.\n\nML-Agents permite combinarlos en la misma configuración. Agentic Racing llegó a prepararlo: el\nevaluador del proyecto podía grabar demostraciones del controlador escrito a mano, y la\nconfiguración de entrenamiento tuvo bloques de BC y GAIL (con pesos de 0.5 y 0.15). Pero la última\ncorrida, la octava, se hizo con **PPO limpio**, sin ese andamiaje, para tener una lectura clara de si\nel algoritmo por sí solo alcanzaba con el agarre ya corregido y las pistas suavizadas.\n\nCombinar demostraciones con RL puede ayudar con el problema de exploración de la sección 8: el\nagente no necesita descubrir por azar la secuencia frenar–girar–acelerar si alguien se la muestra.\nPero no garantiza mejores resultados, y no compensa un entorno roto: imitar a un experto en una pista\nimposible sigue llevando a la misma pared.\n\n---\n\n## Conclusión: mejorar sin destruir lo que ya funciona\n\nPPO propone una manera práctica de actualizar una policy reduciendo el incentivo a que los cambios\nextremos dominen la optimización. La razón $r_t$, el advantage $\\hat{A}_t$ y el objetivo *clipped*\nson el núcleo de la idea; el esquema actor-critic y GAE sirven para estimar qué acciones salieron\nmejor o peor de lo esperado.\n\nLa respuesta a la pregunta de la parte 1 es, en una frase: **porque las estimaciones solo valen cerca\nde la policy que generó los datos**. El clipping mantiene cada actualización en esa zona, y eso\npermite reutilizar la experiencia sin que una estimación ruidosa arrastre a la policy hacia algo peor.\n\nPero Agentic Racing muestra el otro lado de esa estabilidad. PPO optimizó con mucha estabilidad el\nobjetivo que le dimos, incluso cuando ese objetivo premiaba detenerse. Para que un agente de\nconducción aprenda, el algoritmo es solo una pieza: también importan las observaciones, las\nacciones, la recompensa, la física, la dificultad del entorno y la forma en que medimos el progreso.\n\n> **No basta con implementar PPO: hay que diseñar experimentos que permitan descubrir por qué el\n> agente aprende, o por qué no lo hace.**\n\n**Siguiente en la serie:** la [parte 3, \"¿Podemos enseñar imitando? Behavioral Cloning explicado\ndesde cero\"](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/), desarrolla la idea de la sección 10: partir de las demostraciones de un experto.\n\nSi llegaste directo a este artículo, la\n[parte 1](/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/) explica los conceptos\nbase con el mismo proyecto, y\n[Agentic Racing: un piloto que nunca aprendió a manejar](/es/blog/agentic-racing-un-piloto-que-nunca-aprendio-a-manejar/)\ncuenta la historia completa del proyecto, incluido el jefe de equipo LLM que terminó siendo el\ncentro del demo.\n\n---\n\n## Referencias\n\n- Schulman, J. et al. (2017). [Proximal Policy Optimization Algorithms](https://arxiv.org/abs/1707.06347).\n- Schulman, J. et al. (2015). [High-Dimensional Continuous Control Using Generalized Advantage Estimation](https://arxiv.org/abs/1506.02438).\n- OpenAI Spinning Up. [Proximal Policy Optimization](https://spinningup.openai.com/en/latest/algorithms/ppo.html).\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents), en\n  particular el optimizador de PPO (`ml-agents/mlagents/trainers/ppo/optimizer_torch.py`) y la función\n  `get_gae` (`ml-agents/mlagents/trainers/trainer/trainer_utils.py`) del release de `mlagents` 1.1.0.\n- La configuración de entrenamiento (`training/config/race_ppo.yaml`), el agente (`RaceAgent.cs`) y la\n  bitácora de las ocho corridas están en\n  [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch","lang":"en","title":"How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch","description":"Part 2 of the Reinforcement Learning from scratch series with Agentic Racing. We open up PPO: policy gradient, the probability ratio, clipping, actor-critic and GAE, each piece tied to the project's real ML-Agents configuration and to what happened when PPO optimized exactly what we asked it to.","publishDate":"2026-10-03","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero","url":"https://alexisalulema.com/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/","series":{"id":"rl-from-scratch","position":2},"headings":[{"depth":2,"slug":"where-we-left-off","text":"Where we left off"},{"depth":2,"slug":"1-a-thirty-second-recap","text":"1. A thirty-second recap"},{"depth":2,"slug":"2-policy-gradient-nudging-probabilities-in-the-right-direction","text":"2. Policy gradient: nudging probabilities in the right direction"},{"depth":2,"slug":"3-the-probability-ratio-how-much-did-the-policy-change","text":"3. The probability ratio: how much did the policy change?"},{"depth":2,"slug":"4-clipping-removing-the-incentive-to-change-too-much","text":"4. Clipping: removing the incentive to change too much"},{"depth":2,"slug":"5-actor-critic-one-network-decides-the-other-estimates","text":"5. Actor-critic: one network decides, the other estimates"},{"depth":2,"slug":"6-td-error-and-gae-estimating-the-advantage","text":"6. TD error and GAE: estimating the advantage"},{"depth":2,"slug":"7-the-ppo-training-loop","text":"7. The PPO training loop"},{"depth":2,"slug":"8-what-ppo-did-in-agentic-racing","text":"8. What PPO did in Agentic Racing"},{"depth":2,"slug":"9-when-ppo-doesnt-learn-the-problem-may-not-be-ppo","text":"9. When PPO doesn’t learn, the problem may not be PPO"},{"depth":2,"slug":"10-ppo-isnt-the-only-tool","text":"10. PPO isn’t the only tool"},{"depth":2,"slug":"conclusion-improving-without-destroying-what-already-works","text":"Conclusion: improving without destroying what already works"},{"depth":2,"slug":"references","text":"References"}],"body":"> **Series: Reinforcement Learning from scratch, with Agentic Racing as the lab**\n>\n> 1. Part 1 · [How Does an Agent Learn? Teaching a Car to Drive](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/)\n> 2. Part 2 · **How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch** (this article)\n> 3. Part 3 · [Can We Teach by Imitation? Behavioral Cloning Explained from Scratch](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/)\n> 4. Part 4 · [Can We Learn to Behave Like an Expert? GAIL Explained from Scratch](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/)\n> 5. Part 5 · [Can We Combine Imitation and RL? BC, GAIL, and PPO as One System](/blog/can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system/)\n> 6. Part 6 · [How Do We Tell an Agent What We Want? Reward Engineering Explained from Scratch](/blog/how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch/)\n> 7. Part 7 · [What Should an Agent Be Able to See? Observation Design Explained from Scratch](/blog/what-should-an-agent-be-able-to-see-observation-design-explained-from-scratch/)\n> 8. Part 8 · [What Do We Do When the Agent Doesn't Learn? RL Debugging Explained from Scratch](/blog/what-do-we-do-when-the-agent-doesnt-learn-rl-debugging-explained-from-scratch/)\n\n## Where we left off\n\n[Part 1](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/) ended with a conceptual map: the\nagent observes, chooses an action, receives a reward, accumulates experience, and estimates which\nstates and actions are good. What was missing was the last arrow: **how that experience is used to\nimprove the policy**.\n\nAnd it ended with a question:\n\n> **Why does PPO work better when we stop it from changing its mind too quickly?**\n\nThis article answers that question. Picture a virtual driver that has already learned something\nuseful (staying on track on the straights, taking a gentle corner) but still makes mistakes. We\nwant it to improve without a training update destroying what already works.\n\n**Proximal Policy Optimization (PPO)** is an algorithm that updates the policy (the strategy that\ndecides which action to take) while controlling how much it changes on each update. *Proximal*\nmeans close: the new policy shouldn't drift too far from the one that generated the experience it's\nlearning from.\n\nAs in part 1, every idea comes with what the [Unity ML-Agents](https://github.com/Unity-Technologies/ml-agents)\ntrainer actually does (version 1.1.0 of the Python package) and how it was configured in Agentic\nRacing. And, as in part 1, with an honest warning: in this project the RL pilot never learned to\ncomplete laps consistently. That doesn't make the example less useful. Seeing what PPO optimized when it didn't\nlearn what we wanted is one of the best ways to understand what it optimizes.\n\n---\n\n## 1. A thirty-second recap\n\nFrom part 1 we need three pieces.\n\nThe **policy** is a parameterized function that assigns probabilities to actions:\n\n$$\n\\pi_\\theta(a \\mid s)\n$$\n\nwhere $s$ is the state (in practice, the agent's observation), $a$ is an action, and $\\theta$ are the\nneural network's weights. In Agentic Racing, $s$ is 42 numbers (raycasts, speed, relation to the\ntrack, curvature ahead and the strategist's directive) and $a$ is three continuous actions: `steer`,\n`throttle` and `brake`. Because the actions are continuous, the network outputs the center of a\nnormal distribution for each one, and the action is sampled from that distribution.\n\nThe **advantage** compares an action with what's normally expected in that state:\n\n$$\nA(s,a) = Q(s,a) - V(s)\n$$\n\n- $A(s,a) > 0$: the action was better than expected.\n- $A(s,a) < 0$: it was worse than expected.\n- $A(s,a) \\approx 0$: it was close to what was expected.\n\nAnd the **critic** is the part of the system that estimates $V(s)$, the expected return from a\nstate.\n\nThat's enough.\n\n---\n\n## 2. Policy gradient: nudging probabilities in the right direction\n\nThe most direct idea for improving a policy is this: **raise the probability of actions that turned\nout better than expected, and lower it for those that turned out worse**. That's what *policy\ngradient* methods do. Their best-known form estimates the gradient of performance like this:\n\n$$\n\\nabla_\\theta J(\\theta) \\approx \\mathbb{E}_t\\left[\\nabla_\\theta \\log \\pi_\\theta(a_t \\mid s_t)\\, \\hat{A}_t\\right]\n$$\n\nYou don't need to follow the derivation to get the intuition:\n\n- $\\nabla_\\theta \\log \\pi_\\theta(a_t \\mid s_t)$ tells us which way to move the weights to make action\n  $a_t$ more likely in state $s_t$.\n- $\\hat{A}_t$ (the hat means it's an estimate) sets the direction and size of the nudge: if the\n  action turned out better than expected, we make it more likely; if worse, less.\n\nThis works, but it has two practical problems.\n\n**The first: step size.** $\\hat{A}_t$ is a noisy estimate. If we take too large a step along the\ngradient, the policy can change so much that it starts doing things it has never tried and for which\nour estimates say nothing. One bad step can undo in a single update what took millions of steps to\nlearn.\n\n**The second: experience is expensive.** In Agentic Racing, each update needs 20,480 decisions. At\n10 decisions per second, that's about 34 minutes of simulated driving, spread across the 36 cars\ntraining in parallel. Using that experience for a single gradient step and throwing it away would be\na waste. We want to reuse it several times.\n\nBut as soon as we take the first gradient step, the policy is no longer the one that collected that\nexperience. We need a way to measure how far it has moved. That's where the probability ratio comes\nin.\n\n---\n\n## 3. The probability ratio: how much did the policy change?\n\nPPO compares the policy we're optimizing with the one that collected the experience:\n\n$$\nr_t(\\theta) = \\frac{\\pi_\\theta(a_t \\mid s_t)}{\\pi_{\\theta_{\\text{old}}}(a_t \\mid s_t)}\n$$\n\n- $r_t = 1$: the probability of that action didn't change.\n- $r_t = 1.2$: the new policy gives that action a 20% higher probability.\n- $r_t = 0.8$: the probability is 20% lower.\n\nWith continuous actions, as in our car, probability densities are compared instead of probabilities,\nbut the idea is the same. ML-Agents computes it from logarithms, which is numerically more stable:\n`r_theta = torch.exp(log_probs - old_log_probs)`.\n\nOne detail worth keeping in mind: this ratio compares action probabilities. It does **not** mean the\ncar got 20% better in speed, safety, or lap time.\n\nWith this ratio, the policy-gradient objective can be rewritten as $r_t(\\theta)\\,\\hat{A}_t$. At the\nstart of the update $r_t = 1$ and the gradients of both forms match. As the policy changes, $r_t$\ncorrects for the fact that we're evaluating actions chosen by an older policy. It only corrects the\nactions, not the states: the situations the car ended up in are still the ones the old policy\nproduced. That's another reason not to drift too far. That lets us reuse the same batch of\nexperience over several passes.\n\nBut it also opens the door to the step-size problem: if we maximize $r_t\\,\\hat{A}_t$ without limits,\nthe optimizer will want to push $r_t$ up endlessly for every action with a positive advantage. We\nneed a brake.\n\n---\n\n## 4. Clipping: removing the incentive to change too much\n\nPPO's *clipped* objective is:\n\n$$\nL^{\\text{CLIP}}(\\theta) = \\mathbb{E}_t\\left[\\min\\Big(r_t(\\theta)\\,\\hat{A}_t,\\ \\operatorname{clip}\\big(r_t(\\theta),\\, 1-\\epsilon,\\, 1+\\epsilon\\big)\\,\\hat{A}_t\\Big)\\right]\n$$\n\nIt has three parts:\n\n1. $r_t(\\theta)$ measures how much an action's probability changed.\n2. $\\hat{A}_t$ estimates whether that action was better or worse than expected.\n3. `clip` cuts the ratio to the interval $[1-\\epsilon,\\, 1+\\epsilon]$ in one of the two terms, and\n   the `min` keeps the more pessimistic of the two.\n\nIn Agentic Racing, $\\epsilon$ started at 0.2, so the interval was:\n\n$$\n[1-\\epsilon,\\ 1+\\epsilon] = [0.8,\\ 1.2]\n$$\n\nThe clearest way to understand it is to look at the four possible cases:\n\n| Case | Example | Which term does the `min` pick? | Effect |\n|---|---|---|---|\n| Good action ($\\hat{A} > 0$) that already went up a lot | $r_t = 1.5$ | the clipped one: $1.2\\,\\hat{A}$ | the objective stops growing; no more incentive to raise it |\n| Good action ($\\hat{A} > 0$) that went down | $r_t = 0.5$ | the unclipped one: $0.5\\,\\hat{A}$ | the gradient keeps pushing to raise it again |\n| Bad action ($\\hat{A} < 0$) that already went down a lot | $r_t = 0.5$ | the clipped one: $0.8\\,\\hat{A}$ | the objective stops improving; no more incentive to lower it |\n| Bad action ($\\hat{A} < 0$) that went up | $r_t = 1.5$ | the unclipped one: $1.5\\,\\hat{A}$ | the gradient keeps pushing to lower it |\n\n![PPO's clipped objective as a function of the ratio r. With a positive advantage, the objective grows with r up to 1.2 and then goes flat. With a negative advantage, the objective improves as r drops to 0.8 and below that point goes flat. Outside those flat zones, the objective follows the unclipped line.](/images/ppo-clipped-objective.svg)\n\nThe pattern is asymmetric on purpose. PPO **stops rewarding** changes that go too far in the \"good\"\ndirection, but it **doesn't stop penalizing** changes that go the wrong way. It's a pessimistic\nbound: the objective is never more optimistic than the unclipped version.\n\nAnd this is the answer to the question part 1 ended with. PPO works better when we stop it from\nchanging its mind too quickly because **advantage estimates are only reliable close to the policy\nthat generated the data**. If the policy drifts far away, it starts making decisions in situations\nnobody measured, with advantages computed for a different policy. Clipping keeps each update in the\nzone where the data still says something true. That's what lets the batch be reused several times\nwithout a noisy estimate dragging the policy away from what already worked.\n\nAn important nuance: clipping **doesn't forbid** $r_t$ from leaving the $[0.8,\\ 1.2]$ interval. It\nremoves the benefit the objective assigns to certain extreme changes, depending on the sign of the\nadvantage, but the ratio can still go past that limit during an update. It's a practical\noptimization mechanism, not an absolute guarantee that every update is small.\n\nIn ML-Agents, the implementation follows the equation very closely (the loss is minimized, hence\nthe minus sign):\n\n```python\nr_theta = torch.exp(log_probs - old_log_probs)\np_opt_a = r_theta * advantage\np_opt_b = torch.clamp(r_theta, 1.0 - epsilon, 1.0 + epsilon) * advantage\npolicy_loss = -1 * masked_mean(torch.min(p_opt_a, p_opt_b), loss_masks)\n```\n\nOne detail the equation doesn't show: with continuous actions, ML-Agents computes and clips **one\nratio per action dimension**. In our car that's three ratios (`steer`, `throttle` and `brake`), each\nwith its own clip, which are then averaged, instead of a single ratio for the joint action.\n\n---\n\n## 5. Actor-critic: one network decides, the other estimates\n\nCommon PPO implementations use an **actor-critic** setup:\n\n- The **actor** is the policy: it receives the observation and decides which action to take.\n- The **critic** estimates $V(s)$, the expected return from the current state.\n\nThe critic is what lets us compute whether experiences turned out better or worse than expected. It\nisn't infallible: if its estimates are off, the advantages will be too, and the actor will learn from\nthe wrong signals.\n\nIn ML-Agents, with the default configuration, the actor and the critic are **two separate networks**\nwith the same shape. In Agentic Racing they were two MLPs with 2 layers of 256 neurons each. Both are\ntrained together, with a single loss function:\n\n$$\nL = L^{\\text{policy}} + 0.5\\, L^{\\text{value}} - \\beta\\, H(\\pi_\\theta)\n$$\n\n- $L^{\\text{policy}}$ is the clipped objective from the previous section, with the sign flipped.\n- $L^{\\text{value}}$ is the critic's error when predicting the return. In ML-Agents it's also clipped\n  with the same $\\epsilon$: if the new prediction moves more than $\\epsilon$ away from the old one,\n  the error is computed with the more pessimistic version, so moving further doesn't reduce the loss.\n- $H(\\pi_\\theta)$ is the policy's entropy, and $\\beta$ its weight. Subtracting it rewards the policy\n  for not becoming too sure of itself too soon, so it keeps exploring. In Agentic Racing, $\\beta$\n  was 0.005 in the first run and 0.01 from the second onward, always as the initial value.\n\nPart 1 showed this critic in action. In the first runs, its error (ML-Agents' `Value Loss` metric)\nwent up over training: it couldn't anticipate what was about to happen. In the seventh run, when the agent\nstarted seeing the track curvature ahead, it stayed stable at 0.22, although that same run also\nchanged the speed reward.\n\n---\n\n## 6. TD error and GAE: estimating the advantage\n\nOne question remains: where does $\\hat{A}_t$ come from?\n\nA common signal is the **temporal-difference error** (*TD error*):\n\n$$\n\\delta_t = r_t + \\gamma\\, V(s_{t+1}) - V(s_t)\n$$\n\nHere $r_t$ is the reward (not to be confused with the ratio $r_t(\\theta)$ from section 3; standard\nnotation uses the same letter for both), $\\gamma$ is the discount factor, and $V$ are the critic's\nestimates. $\\delta_t$ answers a concrete question: \"after this action, did things turn out better or\nworse than the critic expected?\".\n\nA single $\\delta_t$ only looks one step ahead. Summing many looks further but accumulates more noise.\nPPO usually uses **Generalized Advantage Estimation (GAE)**, which combines both extremes:\n\n$$\n\\hat{A}_t^{\\text{GAE}(\\gamma,\\lambda)} = \\sum_{l=0}^{\\infty} (\\gamma\\lambda)^l\\, \\delta_{t+l}\n$$\n\n$\\gamma$ controls the weight of future rewards and $\\lambda$ how TD errors are combined over time.\nWith $\\lambda = 0$ only the immediate step counts (little noise, but heavily dependent on the critic\nbeing right). With $\\lambda = 1$ everything that happened afterward counts (less dependence on the\ncritic, but much more noise).\n\nIn Agentic Racing, $\\gamma = 0.995$ and $\\lambda = 0.95$, so the factor in the sum is\n$\\gamma\\lambda \\approx 0.945$. That has a concrete reading:\n\n- a TD error's weight halves every ~12 decisions, that is, every ~1.2 seconds;\n- most of an action's advantage is decided by what happens in the next two or three seconds.\n\nIt's interesting to compare this with part 1, where we saw that the return looks about 20 seconds\nahead. Both are compatible: GAE looks only a short way ahead explicitly, but every $\\delta$ includes\n$V(s_{t+1})$, and the critic's estimate already summarizes the more distant future. That's why a\ncritic that doesn't understand the world ruins the advantages, however well designed everything else\nis.\n\nIn practice, ML-Agents computes the sum over the available experience. When a trajectory is cut off\nbefore the episode ends (for example, when it hits the step limit), it uses the critic's estimate of\nthe last state so it doesn't pretend the future is worth zero. And before optimizing, it\n**normalizes the advantages** in the batch (mean 0, standard deviation 1), so what matters is which\nactions did better than the others in that same batch, not the absolute value.\n\n---\n\n## 7. The PPO training loop\n\nWith all the pieces, the full loop looks like this:\n\n1. Use the current policy to interact with the environment.\n2. Collect trajectories: observations, actions, rewards, and the probabilities the policy assigned\n   to them.\n3. Estimate values with the critic and advantages with GAE.\n4. Optimize for several epochs over the batch, in minibatches.\n5. In each minibatch, compute the ratio between the new policy and the old one.\n6. Apply the clipped objective.\n7. Update actor and critic with the combined loss.\n8. Discard the batch and repeat with fresh experience.\n\n![PPO's loop in ML-Agents with Agentic Racing's numbers: 36 cars collect experience until a buffer of 20,480 decisions is full; the critic estimates V(s) and GAE computes the advantages, which are normalized; over 3 epochs, 10 minibatches of 2,048 update actor and critic with the clipped objective; then the buffer is discarded and the new policy goes back to driving.](/images/ppo-training-cycle.svg)\n\nPPO is an **on-policy** method: it learns from experience generated by its current version or a very\nrecent one. It can reuse the batch for a few epochs (that's what the ratio and clipping are for), but\nit isn't designed to reuse any historical experience indefinitely, the way *off-policy* methods do.\n\nIn Agentic Racing, the loop had these numbers:\n\n| Step | Value in `race_ppo.yaml` | What it means |\n|---|---|---|\n| Collection | `buffer_size: 20480` | each update uses 20,480 decisions, gathered by 36 cars |\n| Optimization | `num_epoch: 3`, `batch_size: 2048` | 3 passes over the batch, in 10 minibatches: 30 gradient steps per update |\n| Clipping | `epsilon: 0.2`, `linear` | starts at 0.2 and decays linearly to 0.1 by the end of training |\n| Learning rate | `learning_rate: 3.0e-4`, `linear` | decays linearly to almost zero |\n| Entropy | `beta: 1.0e-2`, `linear` | decays linearly to 0.00001 |\n| GAE | `gamma: 0.995`, `lambd: 0.95` | the horizon from the previous section |\n| Trajectories | `time_horizon: 1000` | cut every 1,000 decisions at most; since episodes lasted at most 800, in practice the critic only filled in the rest when time ran out |\n| Length | `max_steps: 10000000` | a little under 490 policy updates in a 10-million-step run |\n\nThe three linear *schedules* do the same thing on different knobs: early in training the policy can\nchange more and explore more; by the end, changes get smaller and smaller. It's another way of \"not\nchanging its mind too quickly\", this time over the whole run.\n\n---\n\n## 8. What PPO did in Agentic Racing\n\nLet's go back to part 1's story with these pieces in hand. Eight runs, 4 to 20 million steps each,\nand the mean lap fraction always stayed around 10% (between 8 and 13%). Did PPO fail?\n\nNot exactly. The training curves show that PPO did its job: **it optimized, stably, exactly the\nobjective we gave it**.\n\n**It converged, and quickly.** In the first run, mean reward went from −1.1 to 3.35 in the first\n~1.85 million steps and then stayed flat for the remaining 18 million. In the eighth, reward was flat\nfrom ~2.7 million steps on. The pattern repeated in almost every run: a stable climb and a plateau.\nThe exception was the third, which didn't flatten out within its 4 million steps; with more\ntraining, the fourth went back to the same plateau. There was noise, but no collapses: the kind of\ninstability PPO tries to prevent was never the problem.\n\n**It found what the reward was paying for.** In the fourth run, stopping ended the episode with a\nfixed penalty (−1) that came out cheaper than taking more risks, and the agent learned to drive about\n245 meters (right up to the first real corner), stop, and collect. That isn't PPO\nfailing; it's PPO working. The agent found a way to maximize the reward and refined it stably. The\nproblem was the objective, not the optimizer.\n\n**It became sure of itself.** In the first run, with $\\beta = 0.005$, the policy's entropy dropped\nfrom 1.42 to 0.82. Doubling $\\beta$ in the second kept it at 1.27, but the final policy didn't\nimprove. The $\\beta H$ term slows the process down but doesn't prevent it: as the policy finds something that\nworks, it stops exploring alternatives. When what \"works\" is a local optimum (like in the third run,\nwhich learned to crawl at 7 m/s so it could turn without braking), the agent stays there.\n\nThis suggests a reading worth spelling out. PPO's virtue (small changes, gradual improvement near\nwhat the policy already does) has a cost when the good behavior is far from the current one. The full\ncorner maneuver (brake, turn, accelerate again) is a coordinated sequence of one or two seconds that\nthe policy would have to find by exploration. A policy-gradient method can only reinforce what\nthe policy already tries now and then, and PPO, with its clipping, makes those steps even more\ncautious. If it almost never tries the right sequence, no policy gradient is going to invent it.\n\nThat's a reasonable hypothesis, but not the only cause: as part 1 explained, there were also physics\nand track-geometry problems that made the maneuver much harder than it should have been.\n\n---\n\n## 9. When PPO doesn't learn, the problem may not be PPO\n\nThe fact that training runs doesn't prove the agent is learning the task. When progress is slow, it's\nworth checking systematically. Here's how Agentic Racing answered each of these questions:\n\n| Question | In Agentic Racing |\n|---|---|\n| **Observations**: do they carry enough information? | The first six runs couldn't see the track curvature ahead; raycasts reached 40 m. Three curvature observations and 70 m rays were added in the seventh. |\n| **Actions**: do they allow controlling the vehicle? | Three continuous actions. The brake overrides the throttle while active, and the policy almost never used it (mean brake of 0.02 in evaluations). |\n| **Reward**: does it incentivize what we want? | Not always. The linear speed reward made braking pure cost, and the one for stopping opened an easy way out (run 4). |\n| **Physics**: are the consequences coherent? | Not at first. The lateral-grip model erased sideways velocity instead of redirecting it, and every hard turn brought the car almost to a dead stop. |\n| **Difficulty**: is the environment solvable? | The procedural tracks had corners that not even a hand-written controller could take. On a fixed, clean circuit, that controller completed full laps. |\n| **Configuration**: are the hyperparameters reasonable? | Reasonable, but improvable. The first run used 20 million steps and converged at ~1.85 million: about 90% of the compute added nothing. |\n| **Evidence**: are we measuring the task or just training? | Mean reward went up; lap fraction didn't. It took a separate evaluator (lap fraction, episode-end reason, mean brake) to see what was really going on. |\n\nIt's worth separating three questions, because each needs different evidence:\n\n1. **Is the trainer running as expected?** Clean logs, observations of the right size, episodes\n   ending for the expected reasons.\n2. **Does the learning signal carry useful information?** Reward, value loss and entropy behaving in\n   interpretable ways, with no easy ways out.\n3. **Is behavior on the task improving?** Task-specific metrics (here, lap fraction and lap time) in\n   tests independent of training.\n\nFor most of the project, Agentic Racing answered \"yes\" to the first, \"partly\" to the second, and \"no\"\nto the third. The reward curve alone would never have said so.\n\n---\n\n## 10. PPO isn't the only tool\n\nIf learning from scratch turns out to be hard, it can help to start from an expert's demonstrations:\n\n- **Behavioral cloning (BC):** learns to imitate recorded observation → action pairs from the expert,\n  as supervised learning.\n- **Generative Adversarial Imitation Learning (GAIL):** trains a discriminator that tells the agent's\n  behavior apart from the expert's, and uses that signal as an additional reward while the agent keeps\n  learning with RL.\n- **PPO:** optimizes the policy with the environment's rewards.\n\nML-Agents lets you combine them in the same configuration. Agentic Racing got as far as preparing it:\nthe project's evaluator could record demonstrations from the hand-written controller, and the\ntraining configuration had BC and GAIL blocks (with weights of 0.5 and 0.15). But the last run, the\neighth, was done with **plain PPO**, without that scaffolding, to get a clean read on whether the\nalgorithm alone was enough with the grip already fixed and the tracks smoothed.\n\nCombining demonstrations with RL can help with the exploration problem from section 8: the agent\ndoesn't need to stumble onto the brake–turn–accelerate sequence by chance if someone shows it. But it\ndoesn't guarantee better results, and it doesn't make up for a broken environment: imitating an\nexpert on an impossible track still ends at the same wall.\n\n---\n\n## Conclusion: improving without destroying what already works\n\nPPO offers a practical way to update a policy while reducing the incentive for extreme changes to\ndominate the optimization. The ratio $r_t$, the advantage $\\hat{A}_t$ and the clipped objective are\nthe core of the idea; the actor-critic setup and GAE are what estimate which actions turned out\nbetter or worse than expected.\n\nThe answer to part 1's question, in one sentence: **because estimates only hold close to the policy\nthat generated the data**. Clipping keeps each update in that zone, and that lets the experience be\nreused without a noisy estimate dragging the policy toward something worse.\n\nBut Agentic Racing shows the other side of that stability. PPO very stably optimized the objective we\ngave it, even when that objective rewarded stopping. For a driving agent to learn, the algorithm is\nonly one piece: the observations, the actions, the reward, the physics, the environment's difficulty\nand the way we measure progress matter too.\n\n> **Implementing PPO isn't enough: you have to design experiments that let you discover why the agent\n> learns, or why it doesn't.**\n\n**Next in the series:** [part 3, \"Can We Teach by Imitation? Behavioral Cloning Explained from\nScratch\"](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/), develops the idea from section 10: starting from an expert's demonstrations.\n\nIf you landed directly on this article,\n[part 1](/blog/how-does-an-agent-learn-teaching-a-car-to-drive/) explains the core concepts with the\nsame project, and\n[Agentic Racing: A Pilot That Never Learned to Drive](/blog/agentic-racing-a-pilot-that-never-learned-to-drive/)\ntells the project's full story, including the LLM team boss that ended up at the center of the demo.\n\n---\n\n## References\n\n- Schulman, J. et al. (2017). [Proximal Policy Optimization Algorithms](https://arxiv.org/abs/1707.06347).\n- Schulman, J. et al. (2015). [High-Dimensional Continuous Control Using Generalized Advantage Estimation](https://arxiv.org/abs/1506.02438).\n- OpenAI Spinning Up. [Proximal Policy Optimization](https://spinningup.openai.com/en/latest/algorithms/ppo.html).\n- Unity Technologies. [Unity ML-Agents Toolkit](https://github.com/Unity-Technologies/ml-agents),\n  in particular the PPO optimizer (`ml-agents/mlagents/trainers/ppo/optimizer_torch.py`) and the\n  `get_gae` function (`ml-agents/mlagents/trainers/trainer/trainer_utils.py`) from the `mlagents`\n  1.1.0 release.\n- The training configuration (`training/config/race_ppo.yaml`), the agent (`RaceAgent.cs`) and the log\n  of all eight runs are at [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"como-aprende-un-agente-ensenando-a-un-automovil-a-conducir","lang":"es","title":"¿Cómo aprende un agente? Enseñando a un automóvil a conducir","description":"Primer artículo de una serie sobre Reinforcement Learning desde cero, usando Agentic Racing como laboratorio: estado, observación, acción, policy, recompensa, return, value, Q y advantage, explicados con el código real del piloto RL del proyecto, con sus números y con los fracasos que enseñaron más que cualquier ecuación.","publishDate":"2026-10-02","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"how-does-an-agent-learn-teaching-a-car-to-drive","url":"https://alexisalulema.com/es/blog/como-aprende-un-agente-ensenando-a-un-automovil-a-conducir/","series":{"id":"rl-from-scratch","position":1},"headings":[{"depth":2,"slug":"introducción","text":"Introducción"},{"depth":2,"slug":"1-el-aprendizaje-por-refuerzo-visto-como-un-juego","text":"1. El aprendizaje por refuerzo visto como un juego"},{"depth":2,"slug":"2-nuestro-ejemplo-un-automóvil-que-quiere-aprender-a-conducir","text":"2. Nuestro ejemplo: un automóvil que quiere aprender a conducir"},{"depth":2,"slug":"3-qué-significa-estado","text":"3. ¿Qué significa “estado”?"},{"depth":3,"slug":"estado--observación","text":"Estado ≠ observación"},{"depth":2,"slug":"4-qué-puede-hacer-el-agente","text":"4. ¿Qué puede hacer el agente?"},{"depth":2,"slug":"5-la-policy-la-estrategia-del-agente","text":"5. La policy: la estrategia del agente"},{"depth":2,"slug":"6-la-recompensa-decirle-al-agente-qué-nos-importa","text":"6. La recompensa: decirle al agente qué nos importa"},{"depth":2,"slug":"7-el-agente-no-aprende-de-una-recompensa-aislada","text":"7. El agente no aprende de una recompensa aislada"},{"depth":2,"slug":"8-return-pensar-en-recompensas-futuras","text":"8. Return: pensar en recompensas futuras"},{"depth":2,"slug":"9-cómo-sabemos-si-un-estado-es-bueno","text":"9. ¿Cómo sabemos si un estado es bueno?"},{"depth":2,"slug":"10-qsa-valorar-una-acción-concreta","text":"10. Q(s,a): valorar una acción concreta"},{"depth":2,"slug":"11-advantage-esta-acción-fue-mejor-de-lo-esperado","text":"11. Advantage: ¿esta acción fue mejor de lo esperado?"},{"depth":2,"slug":"12-entonces-cómo-aprende-realmente-la-red","text":"12. Entonces, ¿cómo aprende realmente la red?"},{"depth":2,"slug":"13-el-entrenamiento-es-diferente-de-la-ejecución","text":"13. El entrenamiento es diferente de la ejecución"},{"depth":2,"slug":"14-el-entorno-también-forma-parte-del-problema","text":"14. El entorno también forma parte del problema"},{"depth":2,"slug":"15-la-ficha-técnica-del-piloto-rl","text":"15. La ficha técnica del piloto RL"},{"depth":2,"slug":"16-qué-ocurre-al-principio","text":"16. ¿Qué ocurre al principio?"},{"depth":2,"slug":"17-estamos-programando-las-reglas-de-conducción","text":"17. ¿Estamos programando las reglas de conducción?"},{"depth":2,"slug":"18-del-comportamiento-individual-a-una-policy","text":"18. Del comportamiento individual a una policy"},{"depth":2,"slug":"19-el-siguiente-paso-ppo","text":"19. El siguiente paso: PPO"},{"depth":2,"slug":"20-pero-agentic-racing-nos-enseñó-algo-todavía-más-importante","text":"20. Pero Agentic Racing nos enseñó algo todavía más importante"},{"depth":2,"slug":"21-el-mapa-mental-completo","text":"21. El mapa mental completo"},{"depth":2,"slug":"22-una-última-idea-el-agente-aprende-de-experiencia-no-de-explicaciones","text":"22. Una última idea: el agente aprende de experiencia, no de explicaciones"},{"depth":2,"slug":"para-continuar","text":"Para continuar"},{"depth":3,"slug":"cómo-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero","text":"¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero"}],"body":"> **Serie: Reinforcement Learning desde cero, con Agentic Racing como laboratorio**\n>\n> 1. Parte 1 · **¿Cómo aprende un agente? Enseñando a un automóvil a conducir** (este artículo)\n> 2. Parte 2 · [¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/)\n> 3. Parte 3 · [¿Podemos enseñar imitando? Behavioral Cloning explicado desde cero](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/)\n> 4. Parte 4 · [¿Podemos aprender a comportarnos como un experto? GAIL explicado desde cero](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/)\n> 5. Parte 5 · [¿Podemos combinar imitación y RL? BC, GAIL y PPO en un mismo sistema](/es/blog/podemos-combinar-imitacion-y-rl-bc-gail-y-ppo-en-un-mismo-sistema/)\n> 6. Parte 6 · [¿Cómo le decimos a un agente qué queremos? Reward engineering explicado desde cero](/es/blog/como-le-decimos-a-un-agente-que-queremos-reward-engineering-explicado-desde-cero/)\n> 7. Parte 7 · [¿Qué debería poder ver un agente? Observation design explicado desde cero](/es/blog/que-deberia-poder-ver-un-agente-observation-design-explicado-desde-cero/)\n> 8. Parte 8 · [¿Qué hacemos cuando el agente no aprende? RL debugging explicado desde cero](/es/blog/que-hacemos-cuando-el-agente-no-aprende-rl-debugging-explicado-desde-cero/)\n\n## Introducción\n\nCuando pensamos en enseñar a conducir a una persona, la solución parece relativamente sencilla:\nexplicamos las reglas, mostramos ejemplos, dejamos que practique y corregimos sus errores.\n\nPero ¿cómo hacemos algo parecido con un agente de inteligencia artificial?\n\nUn agente no sabe inicialmente qué significa \"girar\", \"frenar\", \"ir rápido\" o incluso \"conducir\nbien\". No podemos simplemente decirle:\n\n> \"Toma esta curva a 80 km/h.\"\n\nTenemos que construir un mundo en el que pueda **observar**, **actuar**, recibir una señal que\nindique si su comportamiento fue bueno o malo y volver a intentarlo.\n\nEse es el punto de partida del **Reinforcement Learning (RL)**.\n\nEn este artículo voy a explicar las ideas fundamentales de RL utilizando\n[Agentic Racing](https://github.com/alulema/agentic-racing), un proyecto de simulación de carreras\nen Unity, como laboratorio práctico. La intención no es comenzar con las matemáticas de un\nalgoritmo concreto, sino entender primero el problema que estamos intentando resolver.\n\nAntes de empezar, una advertencia honesta: en Agentic Racing **el piloto RL nunca aprendió a dar\nuna vuelta completa**. Ocho corridas de entrenamiento con PPO se estancaron alrededor del 10–13%\nde una vuelta, y el demo terminó usando un piloto escrito a mano. Lo conté en detalle en\n[Agentic Racing: un piloto que nunca aprendió a manejar](/es/blog/agentic-racing-un-piloto-que-nunca-aprendio-a-manejar/).\nLejos de restarle valor como laboratorio, eso lo vuelve más útil: cada concepto de este artículo\ntiene detrás un número real, una decisión de diseño concreta y, a veces, un fracaso que lo explica\nmejor que cualquier ejemplo de libro.\n\nLa pregunta fundamental es:\n\n> **¿Cómo aprende un agente qué hacer cuando nadie le proporciona directamente la respuesta\n> correcta?**\n\n---\n\n## 1. El aprendizaje por refuerzo visto como un juego\n\nLa forma más sencilla de imaginar Reinforcement Learning es como una interacción repetida entre dos\nelementos:\n\n- un **agente**, que toma decisiones;\n- un **entorno**, que responde a esas decisiones.\n\n![El ciclo agente–entorno: el entorno entrega una observación, la policy elige una acción, el entorno avanza la física y devuelve una recompensa junto con la siguiente observación.](/images/rl-agent-loop.svg)\n\nEl agente observa el entorno, elige una acción y recibe las consecuencias de esa acción: una\n**recompensa** y una **nueva observación**.\n\nDespués vuelve a observar y decide nuevamente.\n\nY repite este proceso miles o millones de veces. En Agentic Racing, \"millones\" es literal: cada\ncorrida de entrenamiento fue de 4 a 20 millones de pasos.\n\nEsto es importante porque el agente **no recibe la respuesta correcta en cada paso**.\n\nEn aprendizaje supervisado podríamos tener:\n\n```text\nentrada → respuesta correcta\n```\n\nPor ejemplo:\n\n```text\nimagen de una curva → \"girar a la izquierda\"\n```\n\nEn Reinforcement Learning, en cambio, tenemos:\n\n```text\nobservación → acción → consecuencia → recompensa\n```\n\nEl agente tiene que descubrir qué acciones tienden a producir mejores resultados.\n\n---\n\n## 2. Nuestro ejemplo: un automóvil que quiere aprender a conducir\n\nEn Agentic Racing tenemos un problema muy concreto:\n\n> Queremos que un automóvil aprenda a conducir dentro de un entorno de carreras simulado.\n\nEsto convierte conceptos bastante abstractos de Reinforcement Learning en algo mucho más fácil de\nvisualizar.\n\nNuestro agente está dentro de un automóvil. Tiene cierta información sobre lo que ocurre a su\nalrededor, puede controlar el volante, el acelerador y el freno, y recibe señales que le indican si\nsu comportamiento está ayudando o perjudicando su objetivo.\n\nEn el proyecto, estas piezas tienen nombre propio. El entorno de entrenamiento es una\n`TrainingArena`: un circuito generado a partir de una semilla, con muros invisibles en los bordes.\nEl agente es un `RaceAgent`, una clase de [ML-Agents](https://github.com/Unity-Technologies/ml-agents)\n(el toolkit de RL de Unity). Las acciones las ejecuta un `CarController`, que convierte volante,\nacelerador y freno en fuerzas sobre un `Rigidbody`. Y la física la resuelve el motor de Unity\n50 veces por segundo:\n\n```text\n                 ┌──────────────────────────────┐\n                 │  TrainingArena               │\n                 │  circuito (seed) + muros     │\n                 └──────────────┬───────────────┘\n                                │ observaciones (42 números)\n                                ▼\n                 ┌──────────────────────────────┐\n                 │  RaceAgent (ML-Agents)       │\n                 │  policy: red neuronal MLP    │\n                 └──────────────┬───────────────┘\n                                │ acciones: steer, throttle, brake\n                                ▼\n                 ┌──────────────────────────────┐\n                 │  CarController               │\n                 │  fuerzas sobre el Rigidbody  │\n                 └──────────────┬───────────────┘\n                                │\n                                ▼\n                 ┌──────────────────────────────┐\n                 │  Física de Unity (50 Hz)     │\n                 │  posición, velocidad, rumbo  │\n                 └──────────────┬───────────────┘\n                                │\n                                ▼\n                 ┌──────────────────────────────┐\n                 │  TrackProgress               │\n                 │  ¿cuánto avancé en la pista? │\n                 └──────────────┬───────────────┘\n                                │\n                                ▼\n                  recompensa + siguiente observación\n```\n\nUn detalle práctico: no entrenamos con un solo auto. En las corridas de entrenamiento, cada escena\ncontenía **9 arenas**, cada una con un circuito generado a partir de una semilla distinta, separadas 4 km entre\nsí para que los sensores de un auto no vean la pista de al lado. Y corríamos **4 procesos** de esa\nescena en paralelo. Eran 36 autos alimentando una sola policy al mismo tiempo, recorriendo nueve\ntrazados distintos para no aprender de memoria una única pista.\n\nY un detalle de contexto: en el diseño completo del proyecto, este piloto es solo la mitad del\nsistema. Encima de él hay un \"jefe de equipo\", un LLM que razona sobre la carrera y le manda\ndirectivas. En este artículo nos quedamos con el piloto, que es donde vive el RL.\n\n---\n\n## 3. ¿Qué significa \"estado\"?\n\nAntes de que el agente pueda decidir qué hacer, necesitamos describir de alguna manera la situación\nen la que se encuentra.\n\nEn Reinforcement Learning solemos hablar del **estado**, que representamos como:\n\n$$\ns_t\n$$\n\ndonde $t$ representa el instante actual.\n\nConceptualmente, el estado del automóvil podría contener información como:\n\n- posición respecto de la pista;\n- orientación del automóvil;\n- velocidad;\n- proximidad de los límites;\n- geometría de la pista;\n- dirección de la próxima curva;\n- progreso en la vuelta.\n\nPero aquí aparece una distinción importante.\n\n### Estado ≠ observación\n\nEl estado $s_t$ representa idealmente toda la información relevante del entorno.\n\nEl agente, sin embargo, puede no tener acceso directo a todo ese estado. Lo que recibe es una\n**observación**:\n\n$$\no_t\n$$\n\nEs decir:\n\n> \"Esto es lo que el agente puede ver o conocer en este momento.\"\n\nEn un sistema de conducción autónoma real tendríamos cámaras, LiDAR, GPS, IMU, velocidad, etc. En\nun simulador tenemos la ventaja de poder elegir qué información le damos al agente.\n\nEn Agentic Racing, el piloto recibe **42 números** en cada decisión:\n\n| Grupo | Cantidad | Qué contiene |\n|---|---|---|\n| Raycasts | 27 | 9 rayos en abanico de ±75°, de hasta 70 m, que detectan los muros del borde (3 valores por rayo: si chocó, contra qué y a qué distancia) |\n| Velocidad | 2 | velocidad longitudinal y lateral, normalizadas por la velocidad máxima |\n| Relación con la pista | 4 | error de rumbo respecto a la trazada ideal, desplazamiento lateral del auto y de la trazada ideal respecto al centro, progreso en la vuelta (0 a 1) |\n| Curvatura por delante | 3 | cuánto gira la pista entre 0–22 m, 18–45 m y 40–75 m por delante |\n| Directiva | 6 | agresividad, tolerancia al riesgo y el tipo de directiva (`attack`, `defend`, `conserve`, `push`) |\n\nIgual de interesante es lo que **no** ve: su posición absoluta en el mapa, el resto del circuito\nmás allá de 75 metros, los otros autos, la clasificación o los tiempos de vuelta. Es una\nobservación parcial a propósito: ese contexto global es justamente lo que ve el jefe de equipo, y\nno el piloto.\n\nLos últimos seis números merecen una explicación. Son los canales por los que el estratega le habla\nal piloto. Durante el entrenamiento nadie los escribe todavía, así que se **aleatorizan en cada\nepisodio**. De esa forma la policy aprende a conducir distinto según la directiva que recibe, en\nlugar de aprender a ignorarla. Si se entrenara sin esos canales y se agregaran después, habría que\nreentrenar desde cero.\n\nEsto nos lleva a una decisión de diseño fundamental.\n\nSi damos demasiada información, el problema puede resultar artificialmente sencillo, o la policy\npuede apoyarse en datos que no tendrá cuando se use en serio.\n\nSi damos demasiado poca, el agente quizá no tenga suficiente información para tomar buenas\ndecisiones.\n\nAgentic Racing lo vivió en carne propia. Las seis primeras corridas no tenían las tres\nobservaciones de curvatura por delante. El agente solo \"veía\" con los raycasts, que entonces\nllegaban a 40 m: a 15 m/s eso es menos de tres segundos de anticipación. No veía venir las curvas.\nEn la séptima se agregaron la curvatura por delante y los rayos de 70 m, junto con una nueva\nrecompensa de velocidad. El crítico empezó a entender mucho mejor el mundo (lo veremos en la\nsección 9), pero la fracción de vuelta completada casi no se movió. Una mejor observación era\nnecesaria, pero no suficiente.\n\nPor eso:\n\n> **Diseñar las observaciones también forma parte del problema de Reinforcement Learning.**\n\n---\n\n## 4. ¿Qué puede hacer el agente?\n\nAhora necesitamos definir las acciones. Llamemos a una acción:\n\n$$\na_t\n$$\n\nEn Agentic Racing, el espacio de acciones es **continuo** y tiene tres dimensiones. La red no\nelige entre opciones como \"izquierda\" o \"derecha\": produce tres números reales.\n\n| Acción | Rango | Qué hace en el `CarController` |\n|---|---|---|\n| `steer` | −1 a 1 | gira el auto con una velocidad angular de hasta 130°/s, que se reduce al 35% a velocidad máxima |\n| `throttle` | −1 a 1 | aplica hasta 12 000 N de fuerza hacia adelante; los valores negativos son reversa |\n| `brake` | 0 a 1 | aplica hasta 18 000 N de frenado y, mientras está activo, anula el acelerador (la red produce valores entre −1 y 1; para el freno solo cuenta la mitad positiva) |\n\nEncima de eso hay un modelo de agarre lateral: cuando el auto gira, los neumáticos redirigen la\nmayor parte de la velocidad lateral hacia adelante en lugar de perderla. Más adelante veremos por\nqué ese detalle importó tanto.\n\nEl agente no decide en cada paso de física. Decide **cada 5 pasos**, o sea 10 veces por segundo, y\nrepite la última acción entre una decisión y la siguiente.\n\n![Lo que ocurre dentro del agente: 42 observaciones (raycasts, velocidad, relación con la pista, curvatura por delante y directiva) entran a una red MLP de 2×256 neuronas, que produce tres acciones continuas: steer, throttle y brake.](/images/rl-agent-observation-policy-action.svg)\n\nEl agente podría encontrarse, por ejemplo, ante una curva a la izquierda.\n\nNo queremos programar manualmente:\n\n```text\nIF curve_is_left:\n    steering = -0.42\n    throttle = 0.71\n```\n\nQueremos que el agente aprenda una policy que descubra por sí misma qué acción conviene dadas sus\nobservaciones.\n\n---\n\n## 5. La policy: la estrategia del agente\n\nAquí aparece uno de los conceptos más importantes de todo RL.\n\nLa **policy** es la estrategia que utiliza el agente para elegir acciones.\n\nMatemáticamente podemos escribir:\n\n$$\n\\pi(a \\mid s)\n$$\n\no, cuando trabajamos explícitamente con observaciones:\n\n$$\n\\pi(a \\mid o)\n$$\n\nLa idea es:\n\n> \"Dada esta situación, ¿qué debería hacer?\"\n\nEn un agente basado en una red neuronal, la policy se implementa mediante una función\nparametrizada:\n\n$$\n\\pi_\\theta\n$$\n\ndonde $\\theta$ representa los parámetros (los pesos) de la red neuronal.\n\nEn nuestro caso, la red es un MLP pequeño: **2 capas ocultas de 256 neuronas**, con las\nobservaciones normalizadas. Como las acciones son continuas, la red no produce directamente \"la\"\nacción: produce el centro de una distribución normal para cada una de las tres acciones. Durante el\nentrenamiento la acción se **muestrea** de esa distribución, lo que le permite al agente probar\nvariaciones. Por eso escribimos $\\pi(a \\mid o)$ como una probabilidad y no como una función que\ndevuelve un único valor.\n\nInicialmente, esa policy no sabe conducir. Sus parámetros no contienen mágicamente conocimiento\nsobre carreras.\n\nTenemos que conseguir que cambien de manera que las acciones que producen buenos resultados sean\ncada vez más probables.\n\nY aquí entra el elemento fundamental del aprendizaje por refuerzo.\n\n---\n\n## 6. La recompensa: decirle al agente qué nos importa\n\n¿Cómo sabe el agente que una acción fue buena?\n\nLe damos una **recompensa**. La recompensa en el instante $t$ se suele representar como:\n\n$$\nr_t\n$$\n\nPodemos pensar en ella como una señal numérica:\n\n```text\nacción buena       → recompensa positiva\nacción mala        → recompensa negativa\n```\n\nPero esto es una simplificación. En un problema real de RL, diseñar la recompensa suele ser mucho\nmás difícil.\n\nAsí quedó la función de recompensa del `RaceAgent` en la última corrida de entrenamiento (la\noctava), después de siete versiones:\n\n| Componente | Tipo | Valor | Para qué existe |\n|---|---|---|---|\n| Progreso | por metro | +0.02 | avanzar a lo largo de la pista |\n| Velocidad objetivo | por segundo | hasta +0.25 | ir a la velocidad adecuada para la curva que viene |\n| Lentitud | por segundo | hasta ~−0.1 | castigo extra solo si va muy por debajo de esa velocidad |\n| Trazada ideal | por segundo | hasta +0.05 | ir alineado y cerca de la trazada ideal, solo si el auto se mueve |\n| Cercanía al borde | por segundo | hasta −0.6 | crece a medida que el auto se acerca al muro |\n| Golpe contra el muro | por evento | −0.1 | cada contacto con el borde |\n| Detenido | por segundo | −0.3 | mientras el auto está parado |\n| Fin del episodio por fallo | por evento | −1 | salirse de la pista, atascarse, ir en sentido contrario o no avanzar |\n| Vuelta completa | por evento | +12, más hasta +8 | terminar la vuelta, con un bono mayor cuanto más rápido |\n\nEn cada paso, la recompensa es la suma de los componentes que aplican:\n\n$$\nr_t = r_{\\text{progreso}} + r_{\\text{velocidad}} + r_{\\text{trazada}} - r_{\\text{borde}} - r_{\\text{penalizaciones}} + r_{\\text{vuelta}}\n$$\n\nEl componente más interesante es el de velocidad. En las primeras versiones, más velocidad siempre\nsumaba más recompensa, así que frenar para una curva era puro costo y el agente aprendió\nexactamente eso: no frenar nunca. La versión final calcula una **velocidad objetivo** según la\ncurvatura que viene:\n\n$$\nv_{\\text{obj}} = v_{\\text{max}} \\cdot \\operatorname{lerp}\\big(0.42,\\ 0.12,\\ \\min(1,\\ \\theta / 55^\\circ)\\big)\n$$\n\ndonde $\\theta$ es el mayor cambio de rumbo de la pista en los próximos ~55 metros. La recompensa\nes máxima justo en esa velocidad y cae hacia ambos lados:\n\n$$\nr_{\\text{velocidad}} = 0.25 \\cdot \\max\\Big(0,\\ 1 - 1.3\\left|\\frac{v - v_{\\text{obj}}}{v_{\\text{obj}}}\\right|\\Big) \\cdot \\Delta t\n$$\n\nCon eso, frenar antes de una curva cerrada deja de ser puro costo: hay una zona donde frenar paga\nmás que no frenar.\n\n---\n\n## 7. El agente no aprende de una recompensa aislada\n\nAquí aparece una de las ideas que inicialmente pueden resultar contraintuitivas.\n\nSupongamos que el automóvil hace esto:\n\n```text\nacelera\n   ↓\nse acerca a una curva\n   ↓\nfrena\n   ↓\ngira\n   ↓\nmantiene la pista\n   ↓\navanza mucho\n```\n\nLa recompensa importante no aparece inmediatamente después de frenar. De hecho, en el instante en\nque frena, el auto avanza menos y gana menos recompensa de progreso. El beneficio llega segundos\ndespués, cuando sigue en pista en lugar de estrellarse.\n\nPor eso no queremos que el agente simplemente pregunte:\n\n> \"¿Esta acción me dio recompensa?\"\n\nQueremos que aprenda:\n\n> \"¿Qué consecuencias futuras tuvieron mis acciones?\"\n\nEsto nos lleva al concepto de **return**.\n\n---\n\n## 8. Return: pensar en recompensas futuras\n\nEl **return** representa la recompensa acumulada que obtenemos a partir de un determinado momento.\n\nUna formulación habitual es:\n\n$$\nG_t = r_t + \\gamma r_{t+1} + \\gamma^2 r_{t+2} + \\gamma^3 r_{t+3} + \\dots = \\sum_{k=0}^{\\infty} \\gamma^k r_{t+k}\n$$\n\ndonde:\n\n$$\n0 \\leq \\gamma \\leq 1\n$$\n\nes el **discount factor**.\n\nEl parámetro $\\gamma$ controla cuánto valor damos a las recompensas futuras. Si $\\gamma$ es\ncercano a 1, el agente tiene en cuenta consecuencias más lejanas. Si es menor, presta relativamente\nmás atención a las recompensas inmediatas.\n\nPodemos verlo así:\n\n```text\nahora          futuro cercano       futuro lejano\n\n rₜ       +      γ rₜ₊₁       +       γ² rₜ₊₂\n │                 │                    │\npeso 1           peso γ               peso γ²\n```\n\nEn Agentic Racing usamos $\\gamma = 0.995$, y el descuento se aplica por **decisión**, es decir,\ncada 0.1 segundos. Eso da una idea muy concreta de cuánto \"mira hacia adelante\" el agente:\n\n- una recompensa a 200 decisiones (20 segundos) pesa $0.995^{200} \\approx 0.37$;\n- una recompensa a 900 decisiones (90 segundos, más o menos lo que dura una vuelta) pesa\n  $0.995^{900} \\approx 0.01$.\n\nEn otras palabras, la frenada que salva una curva dentro de dos segundos está muy dentro del\nhorizonte del agente. El bono por terminar la vuelta, visto desde la salida, casi no existe. Por eso\nla recompensa necesita señales densas, como el progreso o la velocidad objetivo, que lleguen\npronto.\n\nTodo esto permite expresar algo muy importante:\n\n> Una acción puede ser mala a corto plazo pero buena a largo plazo.\n\nFrenar antes de una curva reduce momentáneamente la velocidad, pero permite tomar mejor la curva y\ncompletar la vuelta más rápido. El agente tiene que aprender ese tipo de relación.\n\n---\n\n## 9. ¿Cómo sabemos si un estado es bueno?\n\nAquí aparece otro concepto fundamental: la **función de valor**.\n\n$$\nV^\\pi(s) = \\mathbb{E}_\\pi\\left[\\, G_t \\mid s_t = s \\,\\right]\n$$\n\nLa función de valor intenta estimar:\n\n> \"Si estoy en este estado y continúo siguiendo esta policy, ¿cuánta recompensa futura espero\n> obtener?\"\n\n```text\n                    estado\n                      │\n                      ▼\n              ┌──────────────┐\n              │     V(s)     │\n              └──────┬───────┘\n                     │\n                     ▼\n              valor esperado\n```\n\nUn estado como:\n\n```text\nautomóvil centrado\n+ buena velocidad\n+ entrando correctamente\n  en una curva\n```\n\npodría tener un valor alto. Mientras que:\n\n```text\nautomóvil pegado al muro\n+ orientación incorrecta\n+ poca velocidad\n```\n\npodría tener un valor bajo.\n\nLa función de valor no dice directamente qué acción tomar. Dice algo más parecido a:\n\n> \"¿Qué tan prometedora es esta situación?\"\n\nEn PPO, esta función la aprende una segunda red, conocida como **crítico**, que se entrena junto\ncon la policy. ML-Agents reporta qué tan bien predice en una métrica llamada `Value Loss`. En las primeras corridas de\nAgentic Racing esa pérdida **subía** a lo largo del entrenamiento (de 0.12 a 0.27 en la primera\ncorrida, hasta 0.52 en la sexta): el crítico no lograba anticipar lo que iba a pasar. Cuando el\nagente empezó a recibir la curvatura de la pista por delante, la pérdida quedó estable en 0.22. Con\nmejores observaciones (y una recompensa nueva en la misma corrida), el crítico por fin podía\ndistinguir un estado prometedor de uno condenado. Que el crítico entienda el mundo, sin embargo,\nno garantiza que la policy aprenda a moverse bien en él.\n\n---\n\n## 10. Q(s,a): valorar una acción concreta\n\nTambién podemos preguntar algo ligeramente diferente:\n\n> \"¿Qué tan buena es esta acción concreta estando en este estado?\"\n\nEso nos lleva a:\n\n$$\nQ^\\pi(s,a) = \\mathbb{E}_\\pi\\left[\\, G_t \\mid s_t = s,\\ a_t = a \\,\\right]\n$$\n\nLa diferencia conceptual es:\n\n```text\nV(s)\n│\n└── ¿Qué tan bueno es estar aquí?\n\nQ(s,a)\n│\n└── ¿Qué tan bueno es hacer esta acción aquí?\n```\n\nPor ejemplo:\n\n```text\nEstado:\ncurva cerrada a la izquierda, a alta velocidad\n\nAcción A:\nseguir acelerando\n\nAcción B:\nfrenar y girar\n\nQ(s,A) → probablemente menor\nQ(s,B) → probablemente mayor\n```\n\nLos valores exactos no los conocemos de antemano. El agente tiene que estimarlos a partir de su\nexperiencia.\n\n---\n\n## 11. Advantage: ¿esta acción fue mejor de lo esperado?\n\nAhora podemos combinar las ideas anteriores.\n\nEl **advantage** se define como:\n\n$$\nA^\\pi(s,a) = Q^\\pi(s,a) - V^\\pi(s)\n$$\n\nLa interpretación es extremadamente útil:\n\n> \"¿Esta acción es mejor o peor de lo que normalmente espero en este estado?\"\n\nSi $A(s,a) > 0$, la acción es mejor que el promedio de lo que la policy suele hacer ahí.\n\nSi $A(s,a) < 0$, es peor.\n\nEn la práctica no conocemos $Q$ ni $V$ exactamente. El advantage se **estima** a partir de las\nrecompensas que se observaron después de cada acción y de la predicción del crítico. Ese es el\npapel del parámetro `lambd: 0.95` de nuestra configuración, del que hablaremos en el próximo\nartículo.\n\nEsto resulta especialmente importante para algoritmos como **PPO**, que veremos en el siguiente\nartículo.\n\n---\n\n## 12. Entonces, ¿cómo aprende realmente la red?\n\nHasta ahora tenemos:\n\n```text\nobservación\n     ↓\npolicy\n     ↓\nacción\n     ↓\nentorno\n     ↓\nrecompensa\n     ↓\nexperiencia\n```\n\nEl proceso de aprendizaje consiste en utilizar esas experiencias para modificar los parámetros de la\npolicy.\n\nAl principio tenemos una policy prácticamente inútil:\n\n```text\nobservación\n    ↓\n\"girar\"\n    ↓\nsale de la pista\n```\n\nDespués de muchas experiencias, queremos que las acciones que históricamente producen mejores\nresultados tengan mayor probabilidad.\n\nEn ML-Agents, ese ciclo tiene números concretos. Los 36 autos juntan experiencia hasta llenar un\nbuffer de **20 480 decisiones**. Con ese buffer, la red hace **3 pasadas** en bloques de 2 048. Después\nla experiencia se descarta y se junta otra nueva con la policy actualizada. PPO es un algoritmo\n*on-policy*: solo aprende de la experiencia generada por su versión actual.\n\n```text\n                 EXPERIENCIA\n\n      ┌───────────────────────────────┐\n      │ observación                   │\n      │ acción                        │\n      │ recompensa                    │\n      │ siguiente observación         │\n      └───────────────┬───────────────┘\n                      │\n                      ▼\n                 aprendizaje\n                      │\n                      ▼\n              actualizar policy\n                      │\n                      ▼\n               nueva policy\n                      │\n                      ▼\n                 más experiencia\n                      │\n                      └───────────► ...\n```\n\nEl agente entra en un ciclo:\n\n> **observar → actuar → recibir feedback → aprender → volver a actuar**\n\n---\n\n## 13. El entrenamiento es diferente de la ejecución\n\nHay otra distinción importante.\n\nDurante el entrenamiento queremos que el agente **explore**. Durante la ejecución de una policy\nentrenada queremos que tome decisiones útiles.\n\n```text\n              TRAINING\n\n      explorar → equivocarse\n          ↓\n      recibir reward\n          ↓\n      actualizar policy\n          ↓\n      volver a intentar\n```\n\nLa exploración en Agentic Racing viene de dos lugares: las acciones se muestrean de una\ndistribución (no se toma siempre la más probable) y el algoritmo premia que esa distribución no se\ncierre demasiado pronto, mediante un término de **entropía** (el parámetro `beta` de la\nconfiguración). La entropía de la policy es una buena medida de cuánto está explorando. En la primera corrida bajó de\n1.42 a 0.82 a lo largo de 20 millones de pasos: el agente fue volviéndose más seguro de sí mismo,\naunque, como veremos, no necesariamente de lo correcto.\n\nUna vez entrenada, la policy se exporta a un archivo `.onnx` y corre sin el algoritmo de\nentrenamiento. El plan era ejecutarla dentro del navegador con el motor de inferencia de Unity.\nEsa parte se validó en el proyecto con un modelo de juguete, a 0.1 ms por inferencia. El problema\nestuvo en otro lado.\n\nEl objetivo del entrenamiento no es evitar todos los errores. Los errores son precisamente una\nfuente de información. Si el automóvil gira demasiado pronto y se sale de la pista, esa experiencia\ncontribuye a que la policy aprenda que ese comportamiento no es deseable.\n\nPor supuesto, para que esto funcione necesitamos:\n\n1. un entorno razonablemente correcto;\n2. observaciones informativas;\n3. acciones bien definidas;\n4. una función de recompensa adecuada;\n5. un algoritmo de aprendizaje apropiado.\n\nY aquí aparece una lección importante de ingeniería:\n\n> **Si el agente no aprende, no necesariamente significa que el algoritmo de RL esté mal.**\n\nEl problema puede estar en cualquiera de las piezas del sistema.\n\n---\n\n## 14. El entorno también forma parte del problema\n\nEste punto es especialmente importante en Agentic Racing.\n\nUn algoritmo de RL puede ser matemáticamente correcto y aun así producir un agente que no aprende.\n¿Por qué? Porque el agente aprende a partir del mundo que le hemos construido.\n\n```text\n                    ┌──────────────┐\n                    │ OBSERVATIONS │\n                    └──────┬───────┘\n                           │\n                           ▼\n                    ┌──────────────┐\n                    │    POLICY    │\n                    └──────┬───────┘\n                           │\n                           ▼\n                    ┌──────────────┐\n                    │    ACTIONS   │\n                    └──────┬───────┘\n                           │\n                           ▼\n              ┌────────────────────────┐\n              │      ENVIRONMENT       │\n              │                        │\n              │ physics + track + car  │\n              └────────────┬───────────┘\n                           │\n                           ▼\n                         REWARD\n                           │\n                           └──────► LEARNING\n```\n\nCada bloque puede convertirse en una fuente de problemas, y en Agentic Racing casi todos lo\nfueron:\n\n**Si la recompensa está mal diseñada, el agente encuentra una manera de maximizarla sin hacer la\ntarea.** Esto se conoce como **reward hacking**. En la cuarta corrida, quedarse detenido terminaba\nel episodio con una penalización de −1. El agente aprendió la policy óptima *para esa recompensa*:\naprovechar el impulso inicial, rodar unos 245 metros cobrando progreso ($245 \\times 0.02 = 4.9$),\ndetenerse y aceptar el −1. El resultado eran unos +4 por episodio, muy cerca del valor en el que\nse estancó la curva de entrenamiento. Morir rápido era la mejor estrategia disponible. La\ncorrección fue que detenerse dejara de terminar el episodio y pasara a costar por cada segundo.\n\n**Si la recompensa empuja en la dirección equivocada, también.** Premiar seguir la trazada ideal\nparecía razonable, pero en este proyecto la trazada ideal pasa pegada a los bordes en cada curva.\nPremiarla empujaba al auto contra los muros.\n\n**Si la física tiene un comportamiento inesperado, el agente aprende estrategias extrañas.** El\nmodelo de agarre lateral original *borraba* la velocidad lateral del auto en lugar de redirigirla.\nResultado: girar con fuerza a 13 m/s frenaba el auto casi en seco. Cualquier giro agresivo era un\nfrenazo, así que tanto el agente como el controlador de referencia \"aprendieron\" a no girar, que es\nlo mismo que no tomar curvas.\n\n**Si el mundo está roto, nada lo compensa.** Las pistas generadas proceduralmente tenían tramos con\ngeometría degenerada y curvas que ni un controlador escrito a mano podía tomar.\n\nPor eso un proyecto de RL no consiste simplemente en:\n\n```text\ninstalar PPO\n↓\nentrenar\n↓\ntener un automóvil autónomo\n```\n\n---\n\n## 15. La ficha técnica del piloto RL\n\nCon todas las piezas sobre la mesa, así quedó definido el problema en las ocho corridas de\nentrenamiento:\n\n| Pieza | En Agentic Racing |\n|---|---|\n| Framework | Unity ML-Agents 4.0.3 (paquete de Unity) y `mlagents` 1.1.0 (Python) |\n| Algoritmo | PPO |\n| Observación | 42 números: 27 de raycasts, más 15 de velocidad, relación con la pista, curvatura por delante y directiva |\n| Acciones | 3 continuas: `steer` y `throttle` en [−1, 1], `brake` en [0, 1] |\n| Frecuencia | física a 50 Hz, una decisión cada 5 pasos (10 por segundo) |\n| Red | MLP de 2 capas × 256 neuronas, observaciones normalizadas |\n| Hiperparámetros | $\\gamma = 0.995$, $\\lambda = 0.95$; tasa de aprendizaje $3 \\times 10^{-4}$, $\\epsilon = 0.2$ y `beta` = 0.01 (0.005 en la primera corrida) como valores iniciales, con decaimiento lineal |\n| Episodio | una vuelta; el auto aparece en un punto al azar del circuito, con ruido de rumbo (±10°) y de posición (±2 m), y ya rodando a 8 m/s |\n| Fin del episodio | vuelta completa; más de 2 m fuera de la pista; 8 s detenido; 4.5 s en sentido contrario; menos de 8 m de avance en 5 s; o 4 000 pasos de física (~80 s) |\n| Progreso | metros avanzados a lo largo de la línea central del circuito, medidos en cada paso |\n| Paralelismo | 9 arenas por proceso × 4 procesos = 36 autos, sobre 9 circuitos procedurales distintos |\n\n(Desde entonces el código cambió: hoy las arenas usan un único circuito fijo y el límite es de\n6 000 pasos, porque una vuelta a ese circuito toma unos 95 segundos.)\n\nY así le fue. Ocho corridas, de 4 a 20 millones de pasos cada una, con siete versiones de la\nrecompensa y varios arreglos de física. La fracción de vuelta que el auto completaba antes de que\nterminara el episodio quedó siempre alrededor del 10% (entre un 8 y un 13%). En la octava, el 91%\nde los episodios terminaron con el auto fuera de la pista.\n\nPara separar los problemas de aprendizaje de los problemas del entorno, el proyecto usó un\ncontrolador escrito a mano, sin ningún aprendizaje, con la misma física y en las mismas pistas. Su\nmejor episodio llegó al 82% de una vuelta a 21 m/s, pero en promedio se atascaba casi tanto como el\nagente RL. Eso ya decía algo: si un controlador diseñado a mano tampoco podía con esas pistas, el\nproblema no era solo del aprendizaje. La confirmación llegó al cambiar a un circuito fijo y limpio,\nsin generación procedural. Ahí ese controlador completó vueltas enteras sin un solo incidente.\n\n---\n\n## 16. ¿Qué ocurre al principio?\n\nImaginemos que comenzamos el entrenamiento con una policy que todavía no sabe conducir.\n\nEl comportamiento inicial es esencialmente caótico:\n\n```text\n      🏎️\n       \\\n        \\\n         → fuera de pista\n```\n\nEn la primera corrida de Agentic Racing, durante los primeros 50 000 pasos, los episodios duraban\nunos cuatro segundos y la recompensa media era negativa (−1.1). El auto apenas arrancaba antes de\nsalirse o detenerse.\n\nEl agente no tiene una representación humana de:\n\n> \"Esta es una curva peligrosa.\"\n\nSolo tiene números. No piensa:\n\n> \"Voy demasiado rápido.\"\n\nTiene una observación que contiene determinados valores y debe aprender qué relación existe entre\nesos valores, las acciones y las consecuencias.\n\nDespués de suficientes experiencias queremos que aparezcan patrones como este:\n\n```text\nobservación:\ncurva cerrada + velocidad alta\n            │\n            ▼\n        frenar\n            │\n            ▼\n       girar mejor\n            │\n            ▼\n      mantener pista\n            │\n            ▼\n       más reward\n```\n\nY esos patrones terminan codificados en los parámetros de la policy.\n\nEn Agentic Racing, ese patrón en particular **nunca apareció**. Al evaluar las policies de la\ntercera y la cuarta corrida, el freno promedio era 0.02 sobre 1: el agente prácticamente nunca\nfrenaba. Había aprendido una de dos cosas. O se arrastraba a 7 m/s para poder girar sin frenar, o\naceleraba a fondo y terminaba fuera de la pista o detenido poco después de la primera curva de\nverdad.\n\n¿Por qué no lo descubrió? En la séptima corrida, ya con una recompensa que premiaba frenar antes\nde las curvas, la hipótesis fue un problema de **exploración difícil**. La maniobra completa de una\ncurva (frenar, girar, volver a acelerar) es una secuencia coordinada de uno o dos segundos. El 97%\nde los episodios terminaban fuera de la pista a unos 200 metros de la salida, justo en esa primera\ncurva. El agente llegaba hasta la curva una y otra vez, pero casi nunca daba por azar con la\nsecuencia correcta, así que casi nunca vivía la experiencia que le habría enseñado que frenar valía\nla pena.\n\nEsa hipótesis era cierta solo en parte. Más tarde aparecieron los problemas de física y de\ngeometría de la pista de la sección 14, que hacían esa maniobra mucho más difícil de lo que debía\nser.\n\n---\n\n## 17. ¿Estamos programando las reglas de conducción?\n\nNo exactamente. Esta es una diferencia fundamental entre un controlador tradicional y un agente\naprendido.\n\nEn un sistema basado en reglas escribiríamos algo como esto, que es una versión simplificada del\ncontrolador de referencia que existe en el proyecto:\n\n```text\ncurva_por_delante = mayor cambio de rumbo en los próximos 60 m\nvelocidad_objetivo = lerp(40% de vmax, 10% de vmax, curva_por_delante / 45°)\n\nIF velocidad < velocidad_objetivo:\n    acelerar\nELSE IF velocidad > velocidad_objetivo + margen:\n    frenar\nsteering = apuntar a un punto de la trazada, 12 a 36 m por delante\n```\n\nEn Reinforcement Learning intentamos que el comportamiento surja del proceso de aprendizaje.\nNosotros definimos:\n\n- qué puede observar el agente;\n- qué acciones puede realizar;\n- qué consecuencias tiene cada acción;\n- qué recompensas recibe;\n- qué algoritmo utilizará para aprender.\n\nPero no especificamos manualmente las decisiones de conducción.\n\nEso es precisamente lo interesante, y también lo difícil. El controlador de reglas de arriba es el\nque hoy maneja los autos del demo. Los canales de directiva que el agente RL tenía en sus\nobservaciones ahora modulan directamente esas reglas: más agresividad significa una velocidad\nobjetivo más alta y frenar más tarde. Para demostrar la relación entre piloto y\nestratega, un piloto escrito a mano cuyo comportamiento cambia con la directiva cumplía el\nrequisito. Llegar al mismo punto con RL habría requerido más iteraciones de las que el proyecto\npodía pagar. En el circuito fijo, donde la pista ya no es un obstáculo, es probable que PPO sí\naprenda a dar vueltas, pero ese experimento quedó pendiente.\n\n---\n\n## 18. Del comportamiento individual a una policy\n\nDespués de suficientes interacciones, la red neuronal intenta aproximar una función:\n\n$$\n\\pi_\\theta(a \\mid o)\n$$\n\nEs decir:\n\n> \"Dada esta observación, ¿qué acción debería producir?\"\n\nLa red no almacena una lista explícita de reglas. En lugar de:\n\n```text\nif curve then brake\nif straight then accelerate\nif left then steer left\n```\n\naprende una función continua y parametrizada:\n\n```text\n                    OBSERVATION (42)\n                         │\n                         ▼\n                ┌─────────────────┐\n                │                 │\n                │  MLP 2 × 256    │\n                │                 │\n                └────────┬────────┘\n                         │\n             ┌───────────┼───────────┐\n             ▼           ▼           ▼\n         steering     throttle      brake\n```\n\nEl reto está en conseguir que esa función mejore de forma estable.\n\nY aquí es donde entra nuestro siguiente algoritmo.\n\n---\n\n## 19. El siguiente paso: PPO\n\nHasta este punto hemos construido el mapa conceptual:\n\n```text\nAgent\n  │\n  ├── observa\n  │\n  ├── elige una acción\n  │\n  ├── recibe reward\n  │\n  ├── acumula experiencia\n  │\n  ├── estima qué estados/acciones son buenos\n  │\n  └── mejora su policy\n```\n\nPero todavía no hemos explicado **cómo** actualizamos matemáticamente la policy.\n\nUna de las respuestas más conocidas es **Proximal Policy Optimization (PPO)**, el algoritmo que usó\nAgentic Racing en todas sus corridas.\n\nPPO introduce una idea especialmente interesante:\n\n> Queremos mejorar la policy, pero no queremos cambiarla demasiado de una actualización a la\n> siguiente.\n\n¿Por qué? Porque una actualización demasiado agresiva podría destruir comportamientos que ya\nfuncionaban.\n\nLa intuición es:\n\n```text\nPolicy actual\n     │\n     │ pequeña mejora\n     ▼\nPolicy nueva\n     │\n     │ pequeña mejora\n     ▼\nPolicy nueva\n     │\n     ▼\n...\n```\n\nen lugar de:\n\n```text\nPolicy actual\n     │\n     │ gran salto\n     ▼\nPolicy completamente diferente\n     │\n     ▼\ncomportamiento inestable\n```\n\nPPO compara la probabilidad que la policy nueva y la vieja le asignan a cada acción, la pondera por\nel **advantage** y deja de premiar los cambios que se alejan demasiado. El `epsilon: 0.2` de nuestra\nconfiguración define ese límite: una vez que la razón entre la probabilidad nueva y la vieja de una\nacción se aleja más de un 20% de 1, alejarse más ya no mejora el objetivo.\n\nEl detalle matemático de este mecanismo será el tema del próximo artículo.\n\n---\n\n## 20. Pero Agentic Racing nos enseñó algo todavía más importante\n\nCuando construimos un sistema de RL real, el algoritmo es solo una pieza. Cambiar cualquiera de las\notras (lo que el agente ve, lo que puede hacer, lo que se le premia, la física que experimenta)\npuede cambiar por completo el comportamiento aprendido.\n\nPor eso, cuando un agente no aprende, la pregunta no debería ser solamente:\n\n> \"¿Qué parámetro de PPO tengo que cambiar?\"\n\nTambién deberíamos preguntar:\n\n> \"¿Qué está viendo el agente?\"\n\n> \"¿Qué puede hacer?\"\n\n> \"¿Qué reward está recibiendo?\"\n\n> \"¿Qué dinámica física está experimentando?\"\n\n> \"¿El entorno está proporcionando una señal de aprendizaje coherente?\"\n\nEn Agentic Racing, las cinco preguntas tuvieron respuestas incómodas en algún momento: no veía las\ncurvas venir, la recompensa tenía una salida fácil, la física convertía cada giro en un frenazo, y\nla pista misma tenía tramos imposibles. Ninguno de esos problemas se arreglaba tocando los\nhiperparámetros de PPO. Todas las corridas usaron PPO; lo que cambiaba entre una y otra era la\nrecompensa, la percepción del agente o la física.\n\nLa lección que más me quedó: **antes de gastar cómputo en un método sofisticado, valida que el\nentorno se pueda resolver con un método simple**. Si un controlador escrito a mano no puede dar\nuna vuelta, es poco probable que una recompensa le enseñe a una red neuronal a hacerlo.\n\n---\n\n## 21. El mapa mental completo\n\nPodemos resumir todo lo visto hasta ahora de esta manera:\n\n```text\n                       AGENT\n                         │ observa\n                         ▼\n                    OBSERVATION\n                         │\n                         ▼\n                      POLICY\n                         │ elige\n                         ▼\n                      ACTION\n                         │\n                         ▼\n                    ENVIRONMENT\n                         │\n             ┌───────────┴───────────┐\n             ▼                       ▼\n        NEW STATE                 REWARD\n             └───────────┬───────────┘\n                         ▼\n                    EXPERIENCE\n                         │\n                         ▼\n                 LEARNING ALGORITHM\n                         │\n                         ▼\n                  UPDATED POLICY ──────► ...\n```\n\nLos conceptos matemáticos que iremos construyendo forman una cadena:\n\n$$\n\\begin{gathered}\n\\text{Observación} \\rightarrow \\text{Acción} \\rightarrow \\text{Reward} \\rightarrow \\text{Return} \\\\\n\\rightarrow V \\rightarrow Q \\rightarrow \\text{Advantage} \\rightarrow \\text{Aprendizaje de la policy}\n\\end{gathered}\n$$\n\nNo necesitamos memorizar todas estas fórmulas todavía. Lo importante es entender qué pregunta\nresponde cada una:\n\n| Concepto | Pregunta |\n|---|---|\n| Observation | ¿Qué puede ver el agente? |\n| Action | ¿Qué puede hacer? |\n| Reward | ¿Qué tan buena fue la consecuencia inmediata? |\n| Return | ¿Cuánta recompensa acumulada obtengo desde aquí? |\n| Value $V(s)$ | ¿Qué tan prometedora es esta situación? |\n| Q-value $Q(s,a)$ | ¿Qué tan buena es esta acción en esta situación? |\n| Advantage $A(s,a)$ | ¿Esta acción es mejor o peor de lo esperado? |\n| Policy $\\pi(a \\mid s)$ | ¿Qué acción debería elegir? |\n\nCon este mapa podemos empezar a estudiar algoritmos como PPO sin tratarlos como una colección de\nfórmulas desconectadas.\n\n---\n\n## 22. Una última idea: el agente aprende de experiencia, no de explicaciones\n\nQuizá esta sea la idea más importante para llevarnos de este artículo.\n\nNosotros podemos mirar una curva y decir:\n\n> \"Aquí hay que frenar.\"\n\nEl agente no recibe esa explicación. Recibe datos, actúa, observa las consecuencias, obtiene reward\ny repite.\n\nMillones de pequeñas interacciones pueden transformar una policy inicialmente inútil en una que\nproduzca un comportamiento complejo.\n\nEso es lo fascinante de Reinforcement Learning:\n\n> **No programamos directamente el comportamiento final. Diseñamos un proceso mediante el cual el\n> agente puede descubrirlo.**\n\nY Agentic Racing muestra la otra cara de esa frase: si el proceso está mal diseñado, el agente\ndescubre otra cosa. Descubrió que detenerse pagaba más que seguir, que girar era peligroso y que\nfrenar no servía de nada. Cada uno de esos aprendizajes era perfectamente racional dado el mundo y\nla recompensa que le dimos. El agente aprendió lo que le enseñamos, que no era lo que queríamos\nenseñarle.\n\nEn el próximo artículo vamos a abrir la caja de **PPO**: qué hace exactamente con todas esas\nexperiencias y por qué su forma de actualizar la policy es tan estable.\n\n---\n\n## Para continuar\n\n**Siguiente artículo de la serie (parte 2):**\n\n### [¿Cómo mejoramos una policy sin destruirla? PPO explicado desde cero](/es/blog/como-mejoramos-una-policy-sin-destruirla-ppo-explicado-desde-cero/)\n\nEn él construimos, paso a paso:\n\n1. Policy Gradient\n2. Actor-Critic\n3. Value Function\n4. Advantage\n5. GAE\n6. Probability Ratio\n7. Clipping\n8. PPO Objective\n9. El ciclo completo de entrenamiento\n10. Cómo todo esto aparece en Agentic Racing\n\nY, sobre todo, respondemos una pregunta:\n\n> **¿Por qué PPO funciona mejor cuando le impedimos cambiar demasiado rápido de opinión?**\n\nEl código del agente (`RaceAgent.cs`), la configuración de entrenamiento (`race_ppo.yaml`) y la\nbitácora completa de las ocho corridas están en\n[github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"how-does-an-agent-learn-teaching-a-car-to-drive","lang":"en","title":"How Does an Agent Learn? Teaching a Car to Drive","description":"The first article in a series on Reinforcement Learning from scratch, using Agentic Racing as a lab: state, observation, action, policy, reward, return, value, Q and advantage, explained with the project's real RL pilot code, its numbers, and the failures that taught more than any equation.","publishDate":"2026-10-02","updatedDate":null,"tags":["reinforcement-learning","machine-learning","agents","unity"],"translationSlug":"como-aprende-un-agente-ensenando-a-un-automovil-a-conducir","url":"https://alexisalulema.com/blog/how-does-an-agent-learn-teaching-a-car-to-drive/","series":{"id":"rl-from-scratch","position":1},"headings":[{"depth":2,"slug":"introduction","text":"Introduction"},{"depth":2,"slug":"1-reinforcement-learning-as-a-game","text":"1. Reinforcement learning as a game"},{"depth":2,"slug":"2-our-example-a-car-that-wants-to-learn-to-drive","text":"2. Our example: a car that wants to learn to drive"},{"depth":2,"slug":"3-what-does-state-mean","text":"3. What does “state” mean?"},{"depth":3,"slug":"state--observation","text":"State ≠ observation"},{"depth":2,"slug":"4-what-can-the-agent-do","text":"4. What can the agent do?"},{"depth":2,"slug":"5-the-policy-the-agents-strategy","text":"5. The policy: the agent’s strategy"},{"depth":2,"slug":"6-the-reward-telling-the-agent-what-we-care-about","text":"6. The reward: telling the agent what we care about"},{"depth":2,"slug":"7-the-agent-doesnt-learn-from-an-isolated-reward","text":"7. The agent doesn’t learn from an isolated reward"},{"depth":2,"slug":"8-return-thinking-about-future-rewards","text":"8. Return: thinking about future rewards"},{"depth":2,"slug":"9-how-do-we-know-whether-a-state-is-good","text":"9. How do we know whether a state is good?"},{"depth":2,"slug":"10-qsa-valuing-a-specific-action","text":"10. Q(s,a): valuing a specific action"},{"depth":2,"slug":"11-advantage-was-this-action-better-than-expected","text":"11. Advantage: was this action better than expected?"},{"depth":2,"slug":"12-so-how-does-the-network-actually-learn","text":"12. So how does the network actually learn?"},{"depth":2,"slug":"13-training-is-different-from-execution","text":"13. Training is different from execution"},{"depth":2,"slug":"14-the-environment-is-part-of-the-problem-too","text":"14. The environment is part of the problem too"},{"depth":2,"slug":"15-the-rl-pilots-spec-sheet","text":"15. The RL pilot’s spec sheet"},{"depth":2,"slug":"16-what-happens-at-the-beginning","text":"16. What happens at the beginning?"},{"depth":2,"slug":"17-are-we-programming-the-driving-rules","text":"17. Are we programming the driving rules?"},{"depth":2,"slug":"18-from-individual-behavior-to-a-policy","text":"18. From individual behavior to a policy"},{"depth":2,"slug":"19-the-next-step-ppo","text":"19. The next step: PPO"},{"depth":2,"slug":"20-but-agentic-racing-taught-us-something-even-more-important","text":"20. But Agentic Racing taught us something even more important"},{"depth":2,"slug":"21-the-complete-mental-map","text":"21. The complete mental map"},{"depth":2,"slug":"22-one-last-idea-the-agent-learns-from-experience-not-from-explanations","text":"22. One last idea: the agent learns from experience, not from explanations"},{"depth":2,"slug":"whats-next","text":"What’s next"},{"depth":3,"slug":"how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch","text":"How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch"}],"body":"> **Series: Reinforcement Learning from scratch, with Agentic Racing as the lab**\n>\n> 1. Part 1 · **How Does an Agent Learn? Teaching a Car to Drive** (this article)\n> 2. Part 2 · [How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/)\n> 3. Part 3 · [Can We Teach by Imitation? Behavioral Cloning Explained from Scratch](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/)\n> 4. Part 4 · [Can We Learn to Behave Like an Expert? GAIL Explained from Scratch](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/)\n> 5. Part 5 · [Can We Combine Imitation and RL? BC, GAIL, and PPO as One System](/blog/can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system/)\n> 6. Part 6 · [How Do We Tell an Agent What We Want? Reward Engineering Explained from Scratch](/blog/how-do-we-tell-an-agent-what-we-want-reward-engineering-explained-from-scratch/)\n> 7. Part 7 · [What Should an Agent Be Able to See? Observation Design Explained from Scratch](/blog/what-should-an-agent-be-able-to-see-observation-design-explained-from-scratch/)\n> 8. Part 8 · [What Do We Do When the Agent Doesn't Learn? RL Debugging Explained from Scratch](/blog/what-do-we-do-when-the-agent-doesnt-learn-rl-debugging-explained-from-scratch/)\n\n## Introduction\n\nWhen we think about teaching a person to drive, the answer seems fairly simple: we explain the\nrules, show examples, let them practice, and correct their mistakes.\n\nBut how do we do something similar with an artificial-intelligence agent?\n\nAt first, an agent doesn't know what \"turning\", \"braking\", \"going fast\" or even \"driving well\"\nmeans. We can't just tell it:\n\n> \"Take this corner at 80 km/h.\"\n\nWe have to build a world in which it can **observe**, **act**, receive a signal telling it whether\nits behavior was good or bad, and try again.\n\nThat is the starting point of **Reinforcement Learning (RL)**.\n\nIn this article I'll explain the fundamental ideas of RL using\n[Agentic Racing](https://github.com/alulema/agentic-racing), a racing simulation project built in\nUnity, as a hands-on lab. The goal isn't to start with the math of a specific algorithm, but to\nfirst understand the problem we're trying to solve.\n\nBefore we start, an honest warning: in Agentic Racing **the RL pilot never learned to complete a\nlap**. Eight training runs with PPO plateaued at around 10–13% of a lap, and the demo ended up using\na hand-written pilot. I told that story in detail in\n[Agentic Racing: A Pilot That Never Learned to Drive](/blog/agentic-racing-a-pilot-that-never-learned-to-drive/).\nFar from making it a worse lab, that makes it a more useful one: every concept in this article has\na real number behind it, a concrete design decision and, sometimes, a failure that explains it\nbetter than any textbook example.\n\nThe fundamental question is:\n\n> **How does an agent learn what to do when nobody gives it the right answer directly?**\n\n---\n\n## 1. Reinforcement learning as a game\n\nThe simplest way to picture Reinforcement Learning is as a repeated interaction between two\nelements:\n\n- an **agent**, which makes decisions;\n- an **environment**, which responds to those decisions.\n\n![The agent–environment loop: the environment provides an observation, the policy chooses an action, the environment steps the physics and returns a reward together with the next observation.](/images/rl-agent-loop.svg)\n\nThe agent observes the environment, chooses an action, and receives the consequences of that\naction: a **reward** and a **new observation**.\n\nThen it observes again and decides again.\n\nAnd it repeats this process thousands or millions of times. In Agentic Racing, \"millions\" is\nliteral: each training run was 4 to 20 million steps.\n\nThis matters because the agent **does not receive the right answer at each step**.\n\nIn supervised learning we might have:\n\n```text\ninput → correct answer\n```\n\nFor example:\n\n```text\nimage of a corner → \"turn left\"\n```\n\nIn Reinforcement Learning, instead, we have:\n\n```text\nobservation → action → consequence → reward\n```\n\nThe agent has to discover which actions tend to produce better outcomes.\n\n---\n\n## 2. Our example: a car that wants to learn to drive\n\nIn Agentic Racing we have a very concrete problem:\n\n> We want a car to learn to drive inside a simulated racing environment.\n\nThat turns fairly abstract Reinforcement Learning concepts into something much easier to picture.\n\nOur agent is inside a car. It has some information about what's happening around it, it can\ncontrol the steering, the throttle and the brake, and it receives signals telling it whether its\nbehavior is helping or hurting its goal.\n\nIn the project, these pieces have names. The training environment is a `TrainingArena`: a circuit\ngenerated from a seed, with invisible walls along the edges. The agent is a `RaceAgent`, a class\nfrom [ML-Agents](https://github.com/Unity-Technologies/ml-agents) (Unity's RL toolkit). The actions\nare carried out by a `CarController`, which turns steering, throttle and brake into forces on a\n`Rigidbody`. And the physics is solved by the Unity engine 50 times per second:\n\n```text\n                 ┌──────────────────────────────┐\n                 │  TrainingArena               │\n                 │  circuit (seed) + walls      │\n                 └──────────────┬───────────────┘\n                                │ observations (42 numbers)\n                                ▼\n                 ┌──────────────────────────────┐\n                 │  RaceAgent (ML-Agents)       │\n                 │  policy: MLP neural network  │\n                 └──────────────┬───────────────┘\n                                │ actions: steer, throttle, brake\n                                ▼\n                 ┌──────────────────────────────┐\n                 │  CarController               │\n                 │  forces on the Rigidbody     │\n                 └──────────────┬───────────────┘\n                                │\n                                ▼\n                 ┌──────────────────────────────┐\n                 │  Unity physics (50 Hz)       │\n                 │  position, velocity, heading │\n                 └──────────────┬───────────────┘\n                                │\n                                ▼\n                 ┌──────────────────────────────┐\n                 │  TrackProgress               │\n                 │  how far along the track?    │\n                 └──────────────┬───────────────┘\n                                │\n                                ▼\n                     reward + next observation\n```\n\nA practical detail: we don't train with a single car. In the training runs, each scene contained\n**9 arenas**, each with a circuit generated from a different seed, placed 4 km apart so that one\ncar's sensors can't see the track next door. And we ran **4 processes** of that scene in parallel.\nThat made 36 cars feeding a single policy at the same time, driving nine different layouts so the\npolicy wouldn't just memorize one track.\n\nAnd a bit of context: in the full design of the project, this pilot is only half of the system.\nAbove it sits a \"team boss\", an LLM that reasons about the race and radios down directives. In this\narticle we stay with the pilot, which is where the RL lives.\n\n---\n\n## 3. What does \"state\" mean?\n\nBefore the agent can decide what to do, we need some way to describe the situation it's in.\n\nIn Reinforcement Learning we usually talk about the **state**, which we write as:\n\n$$\ns_t\n$$\n\nwhere $t$ stands for the current time step.\n\nConceptually, the car's state could contain information such as:\n\n- position relative to the track;\n- the car's orientation;\n- speed;\n- how close the edges are;\n- the track geometry;\n- the direction of the next corner;\n- progress along the lap.\n\nBut here an important distinction appears.\n\n### State ≠ observation\n\nThe state $s_t$ ideally represents all the relevant information about the environment.\n\nThe agent, however, may not have direct access to all of that state. What it receives is an\n**observation**:\n\n$$\no_t\n$$\n\nIn other words:\n\n> \"This is what the agent can see or know right now.\"\n\nA real self-driving system would have cameras, LiDAR, GPS, an IMU, speed, and so on. In a simulator\nwe have the advantage of choosing what information we give the agent.\n\nIn Agentic Racing, the pilot receives **42 numbers** at every decision:\n\n| Group | Count | What it contains |\n|---|---|---|\n| Raycasts | 27 | 9 rays fanned out over ±75°, up to 70 m long, that detect the edge walls (3 values per ray: whether it hit, what it hit, and how far away) |\n| Speed | 2 | forward and lateral speed, normalized by top speed |\n| Relation to the track | 4 | heading error relative to the racing line, lateral offset of the car and of the racing line from the centerline, lap progress (0 to 1) |\n| Curvature ahead | 3 | how much the track turns 0–22 m, 18–45 m and 40–75 m ahead |\n| Directive | 6 | aggression, risk tolerance and the directive type (`attack`, `defend`, `conserve`, `push`) |\n\nJust as interesting is what it does **not** see: its absolute position on the map, the rest of the\ncircuit beyond 75 meters, the other cars, the standings, or lap times. It's a deliberately partial\nobservation: that global context is exactly what the team boss sees, and the pilot doesn't.\n\nThe last six numbers deserve an explanation. They are the channels through which the strategist\ntalks to the pilot. During training nobody writes to them yet, so they are **randomized every\nepisode**. That way the policy learns to drive differently depending on the directive it receives,\ninstead of learning to ignore it. If you trained without those channels and added them later,\nyou'd have to retrain from scratch.\n\nThis leads to a fundamental design decision.\n\nIf we give too much information, the problem can become artificially easy, or the policy can lean on\ndata it won't have when it's used for real.\n\nIf we give too little, the agent may not have enough information to make good decisions.\n\nAgentic Racing learned this the hard way. The first six runs didn't have the three curvature-ahead\nobservations. The agent could only \"see\" through the raycasts, which back then reached 40 m: at\n15 m/s that's less than three seconds of anticipation. It couldn't see corners coming. The seventh\nrun added curvature ahead and 70 m rays, together with a new speed reward. The critic started to\nunderstand the world much better (we'll see that in section 9), but the fraction of the lap\ncompleted barely moved. A better observation was necessary, but not sufficient.\n\nThat's why:\n\n> **Designing the observations is also part of the Reinforcement Learning problem.**\n\n---\n\n## 4. What can the agent do?\n\nNow we need to define the actions. Let's call an action:\n\n$$\na_t\n$$\n\nIn Agentic Racing, the action space is **continuous** and has three dimensions. The network doesn't\npick between options like \"left\" or \"right\": it outputs three real numbers.\n\n| Action | Range | What it does in the `CarController` |\n|---|---|---|\n| `steer` | −1 to 1 | turns the car at up to 130°/s, dropping to 35% of that at top speed |\n| `throttle` | −1 to 1 | applies up to 12,000 N of forward force; negative values mean reverse |\n| `brake` | 0 to 1 | applies up to 18,000 N of braking and, while active, overrides the throttle (the network outputs values between −1 and 1; for the brake only the positive half counts) |\n\nOn top of that there's a lateral-grip model: when the car turns, the tires redirect most of the\nsideways velocity forward instead of losing it. We'll see later why that detail mattered so much.\n\nThe agent doesn't decide at every physics step. It decides **every 5 steps**, that is, 10 times per\nsecond, and repeats its last action between one decision and the next.\n\n![What happens inside the agent: 42 observations (raycasts, speed, relation to the track, curvature ahead and directive) feed a 2×256 MLP, which produces three continuous actions: steer, throttle and brake.](/images/rl-agent-observation-policy-action.svg)\n\nThe agent might, for example, be facing a left-hand corner.\n\nWe don't want to hand-code:\n\n```text\nIF curve_is_left:\n    steering = -0.42\n    throttle = 0.71\n```\n\nWe want the agent to learn a policy that figures out on its own which action makes sense given its\nobservations.\n\n---\n\n## 5. The policy: the agent's strategy\n\nHere comes one of the most important concepts in all of RL.\n\nThe **policy** is the strategy the agent uses to choose actions.\n\nMathematically we can write:\n\n$$\n\\pi(a \\mid s)\n$$\n\nor, when we work explicitly with observations:\n\n$$\n\\pi(a \\mid o)\n$$\n\nThe idea is:\n\n> \"Given this situation, what should I do?\"\n\nIn an agent based on a neural network, the policy is implemented as a parameterized function:\n\n$$\n\\pi_\\theta\n$$\n\nwhere $\\theta$ stands for the parameters (the weights) of the neural network.\n\nIn our case, the network is a small MLP: **2 hidden layers of 256 neurons**, with normalized\nobservations. Because the actions are continuous, the network doesn't output \"the\" action directly:\nit outputs the center of a normal distribution for each of the three actions. During training the\naction is **sampled** from that distribution, which lets the agent try variations. That's why we\nwrite $\\pi(a \\mid o)$ as a probability rather than as a function that returns a single value.\n\nAt first, that policy doesn't know how to drive. Its parameters don't magically contain any racing\nknowledge.\n\nWe have to get them to change so that actions that lead to good outcomes become more and more\nlikely.\n\nAnd this is where the key element of reinforcement learning comes in.\n\n---\n\n## 6. The reward: telling the agent what we care about\n\nHow does the agent know an action was good?\n\nWe give it a **reward**. The reward at time $t$ is usually written as:\n\n$$\nr_t\n$$\n\nWe can think of it as a numeric signal:\n\n```text\ngood action        → positive reward\nbad action         → negative reward\n```\n\nBut that's a simplification. In a real RL problem, designing the reward is usually much harder.\n\nThis is what the `RaceAgent` reward function looked like in the last training run (the eighth),\nafter seven versions:\n\n| Component | Type | Value | Why it exists |\n|---|---|---|---|\n| Progress | per meter | +0.02 | moving forward along the track |\n| Target speed | per second | up to +0.25 | going at the right speed for the upcoming corner |\n| Slowness | per second | up to ~−0.1 | extra penalty only when far below that speed |\n| Racing line | per second | up to +0.05 | staying aligned with and close to the racing line, only while moving |\n| Edge proximity | per second | up to −0.6 | grows as the car gets closer to the wall |\n| Wall hit | per event | −0.1 | each contact with the edge |\n| Stopped | per second | −0.3 | while the car is stationary |\n| Episode ended by failure | per event | −1 | leaving the track, getting stuck, driving the wrong way, or not making progress |\n| Lap completed | per event | +12, plus up to +8 | finishing the lap, with a bigger bonus the faster it's done |\n\nAt each step, the reward is the sum of the components that apply:\n\n$$\nr_t = r_{\\text{progress}} + r_{\\text{speed}} + r_{\\text{line}} - r_{\\text{edge}} - r_{\\text{penalties}} + r_{\\text{lap}}\n$$\n\nThe most interesting component is the speed one. In the early versions, more speed always meant more\nreward, so braking for a corner was pure cost, and the agent learned exactly that: never brake. The\nfinal version computes a **target speed** based on the upcoming curvature:\n\n$$\nv_{\\text{target}} = v_{\\text{max}} \\cdot \\operatorname{lerp}\\big(0.42,\\ 0.12,\\ \\min(1,\\ \\theta / 55^\\circ)\\big)\n$$\n\nwhere $\\theta$ is the largest heading change of the track over the next ~55 meters. The reward\npeaks right at that speed and falls off on both sides:\n\n$$\nr_{\\text{speed}} = 0.25 \\cdot \\max\\Big(0,\\ 1 - 1.3\\left|\\frac{v - v_{\\text{target}}}{v_{\\text{target}}}\\right|\\Big) \\cdot \\Delta t\n$$\n\nWith that, braking before a tight corner stops being pure cost: there's a zone where braking pays\nmore than not braking.\n\n---\n\n## 7. The agent doesn't learn from an isolated reward\n\nHere comes one of the ideas that can feel counterintuitive at first.\n\nSuppose the car does this:\n\n```text\naccelerates\n   ↓\napproaches a corner\n   ↓\nbrakes\n   ↓\nturns\n   ↓\nstays on the track\n   ↓\nmakes a lot of progress\n```\n\nThe important reward doesn't show up right after braking. In fact, at the moment it brakes, the car\nmoves forward less and earns less progress reward. The benefit arrives seconds later, when it stays\non the track instead of crashing.\n\nThat's why we don't want the agent to simply ask:\n\n> \"Did this action give me a reward?\"\n\nWe want it to learn:\n\n> \"What future consequences did my actions have?\"\n\nThat brings us to the concept of **return**.\n\n---\n\n## 8. Return: thinking about future rewards\n\nThe **return** is the accumulated reward we get from a given moment onward.\n\nA common formulation is:\n\n$$\nG_t = r_t + \\gamma r_{t+1} + \\gamma^2 r_{t+2} + \\gamma^3 r_{t+3} + \\dots = \\sum_{k=0}^{\\infty} \\gamma^k r_{t+k}\n$$\n\nwhere:\n\n$$\n0 \\leq \\gamma \\leq 1\n$$\n\nis the **discount factor**.\n\nThe parameter $\\gamma$ controls how much we value future rewards. If $\\gamma$ is close to 1, the\nagent takes more distant consequences into account. If it's smaller, it pays relatively more\nattention to immediate rewards.\n\nWe can picture it like this:\n\n```text\nnow            near future          far future\n\n rₜ       +      γ rₜ₊₁       +       γ² rₜ₊₂\n │                 │                    │\nweight 1        weight γ            weight γ²\n```\n\nIn Agentic Racing we use $\\gamma = 0.995$, and the discount is applied per **decision**, that is,\nevery 0.1 seconds. That gives a very concrete sense of how far \"ahead\" the agent looks:\n\n- a reward 200 decisions away (20 seconds) is weighted by $0.995^{200} \\approx 0.37$;\n- a reward 900 decisions away (90 seconds, roughly the length of a lap) is weighted by\n  $0.995^{900} \\approx 0.01$.\n\nIn other words, the braking that saves a corner two seconds from now is well within the agent's\nhorizon. The bonus for finishing the lap, seen from the start, barely exists. That's why the reward\nneeds dense signals, like progress or target speed, that arrive early.\n\nAll of this lets us express something very important:\n\n> An action can be bad in the short term but good in the long term.\n\nBraking before a corner momentarily reduces speed, but it lets you take the corner better and\nfinish the lap faster. The agent has to learn that kind of relationship.\n\n---\n\n## 9. How do we know whether a state is good?\n\nHere comes another fundamental concept: the **value function**.\n\n$$\nV^\\pi(s) = \\mathbb{E}_\\pi\\left[\\, G_t \\mid s_t = s \\,\\right]\n$$\n\nThe value function tries to estimate:\n\n> \"If I'm in this state and keep following this policy, how much future reward do I expect to get?\"\n\n```text\n                    state\n                      │\n                      ▼\n              ┌──────────────┐\n              │     V(s)     │\n              └──────┬───────┘\n                     │\n                     ▼\n               expected value\n```\n\nA state like:\n\n```text\ncar centered\n+ good speed\n+ entering a corner\n  correctly\n```\n\nmight have a high value. Whereas:\n\n```text\ncar pinned against the wall\n+ wrong orientation\n+ low speed\n```\n\nmight have a low value.\n\nThe value function doesn't tell you directly which action to take. It says something closer to:\n\n> \"How promising is this situation?\"\n\nIn PPO, this function is learned by a second network, known as the **critic**, which is trained\nalongside the policy. ML-Agents reports how well it predicts through a metric called `Value Loss`.\nIn the early Agentic Racing runs that loss **went up** over training (from 0.12 to 0.27 in the first\nrun, and up to 0.52 in the sixth): the critic couldn't anticipate what was about to happen. Once the\nagent started receiving the track curvature ahead, the loss stayed stable at 0.22. With better\nobservations (and a new reward in the same run), the critic could finally tell a promising state\nfrom a doomed one. A critic that understands the world, however, doesn't guarantee that the policy\nlearns to move well in it.\n\n---\n\n## 10. Q(s,a): valuing a specific action\n\nWe can also ask a slightly different question:\n\n> \"How good is this specific action in this state?\"\n\nThat leads us to:\n\n$$\nQ^\\pi(s,a) = \\mathbb{E}_\\pi\\left[\\, G_t \\mid s_t = s,\\ a_t = a \\,\\right]\n$$\n\nThe conceptual difference is:\n\n```text\nV(s)\n│\n└── How good is it to be here?\n\nQ(s,a)\n│\n└── How good is it to take this action here?\n```\n\nFor example:\n\n```text\nState:\ntight left-hand corner, at high speed\n\nAction A:\nkeep accelerating\n\nAction B:\nbrake and turn\n\nQ(s,A) → probably lower\nQ(s,B) → probably higher\n```\n\nWe don't know the exact values in advance. The agent has to estimate them from its experience.\n\n---\n\n## 11. Advantage: was this action better than expected?\n\nNow we can combine the previous ideas.\n\nThe **advantage** is defined as:\n\n$$\nA^\\pi(s,a) = Q^\\pi(s,a) - V^\\pi(s)\n$$\n\nThe interpretation is extremely useful:\n\n> \"Is this action better or worse than what I usually expect in this state?\"\n\nIf $A(s,a) > 0$, the action is better than the average of what the policy usually does there.\n\nIf $A(s,a) < 0$, it's worse.\n\nIn practice we don't know $Q$ or $V$ exactly. The advantage is **estimated** from the rewards\nobserved after each action and from the critic's prediction. That's the role of the `lambd: 0.95`\nparameter in our configuration, which we'll cover in the next article.\n\nThis is especially important for algorithms like **PPO**, which we'll look at in the next article.\n\n---\n\n## 12. So how does the network actually learn?\n\nSo far we have:\n\n```text\nobservation\n     ↓\npolicy\n     ↓\naction\n     ↓\nenvironment\n     ↓\nreward\n     ↓\nexperience\n```\n\nThe learning process consists of using those experiences to modify the policy's parameters.\n\nAt first we have a practically useless policy:\n\n```text\nobservation\n    ↓\n\"turn\"\n    ↓\nleaves the track\n```\n\nAfter many experiences, we want the actions that have historically produced better outcomes to\nbecome more likely.\n\nIn ML-Agents, that cycle has concrete numbers. The 36 cars collect experience until they fill a\nbuffer of **20,480 decisions**. With that buffer, the network makes **3 passes** in batches of\n2,048. Then the experience is thrown away and new experience is collected with the updated policy.\nPPO is an *on-policy* algorithm: it only learns from experience generated by its current version.\n\n```text\n                 EXPERIENCE\n\n      ┌───────────────────────────────┐\n      │ observation                   │\n      │ action                        │\n      │ reward                        │\n      │ next observation              │\n      └───────────────┬───────────────┘\n                      │\n                      ▼\n                  learning\n                      │\n                      ▼\n                update policy\n                      │\n                      ▼\n                 new policy\n                      │\n                      ▼\n               more experience\n                      │\n                      └───────────► ...\n```\n\nThe agent enters a loop:\n\n> **observe → act → receive feedback → learn → act again**\n\n---\n\n## 13. Training is different from execution\n\nThere's another important distinction.\n\nDuring training we want the agent to **explore**. When running a trained policy, we want it to make\nuseful decisions.\n\n```text\n              TRAINING\n\n      explore → make mistakes\n          ↓\n      receive reward\n          ↓\n      update policy\n          ↓\n      try again\n```\n\nExploration in Agentic Racing comes from two places: actions are sampled from a distribution (the\nmost likely one isn't always taken), and the algorithm rewards that distribution for not narrowing\ntoo early, through an **entropy** term (the `beta` parameter in the configuration). The policy's\nentropy is a good measure of how much it's exploring. In the first run it dropped from 1.42 to 0.82\nover 20 million steps: the agent became more and more sure of itself, although, as we'll see, not\nnecessarily about the right things.\n\nOnce trained, the policy is exported to an `.onnx` file and runs without the training algorithm.\nThe plan was to run it inside the browser with Unity's inference engine. That part was validated in\nthe project with a toy model, at 0.1 ms per inference. The problem was elsewhere.\n\nThe goal of training isn't to avoid every mistake. Mistakes are precisely a source of information.\nIf the car turns too early and leaves the track, that experience helps the policy learn that the\nbehavior is undesirable.\n\nOf course, for this to work we need:\n\n1. a reasonably correct environment;\n2. informative observations;\n3. well-defined actions;\n4. a suitable reward function;\n5. an appropriate learning algorithm.\n\nAnd here an important engineering lesson appears:\n\n> **If the agent doesn't learn, that doesn't necessarily mean the RL algorithm is wrong.**\n\nThe problem can be in any piece of the system.\n\n---\n\n## 14. The environment is part of the problem too\n\nThis point is especially important in Agentic Racing.\n\nAn RL algorithm can be mathematically correct and still produce an agent that doesn't learn. Why?\nBecause the agent learns from the world we built for it.\n\n```text\n                    ┌──────────────┐\n                    │ OBSERVATIONS │\n                    └──────┬───────┘\n                           │\n                           ▼\n                    ┌──────────────┐\n                    │    POLICY    │\n                    └──────┬───────┘\n                           │\n                           ▼\n                    ┌──────────────┐\n                    │    ACTIONS   │\n                    └──────┬───────┘\n                           │\n                           ▼\n              ┌────────────────────────┐\n              │      ENVIRONMENT       │\n              │                        │\n              │ physics + track + car  │\n              └────────────┬───────────┘\n                           │\n                           ▼\n                         REWARD\n                           │\n                           └──────► LEARNING\n```\n\nEvery block can become a source of problems, and in Agentic Racing almost all of them did:\n\n**If the reward is badly designed, the agent finds a way to maximize it without doing the task.**\nThis is known as **reward hacking**. In the fourth run, stopping ended the episode with a −1\npenalty. The agent learned the optimal policy *for that reward*: use the rolling start, roll about\n245 meters collecting progress ($245 \\times 0.02 = 4.9$), stop, and accept the −1. That came out to\nabout +4 per episode, very close to where the training curve plateaued. Dying quickly was the best\nstrategy available. The fix was for stopping to no longer end the episode and to cost something\nfor every second instead.\n\n**If the reward pushes in the wrong direction, same thing.** Rewarding the agent for following the\nracing line seemed reasonable, but in this project the racing line runs right along the edges in\nevery corner. Rewarding it pushed the car into the walls.\n\n**If the physics behaves unexpectedly, the agent learns strange strategies.** The original\nlateral-grip model *erased* the car's sideways velocity instead of redirecting it. The result:\nturning hard at 13 m/s brought the car almost to a dead stop. Any aggressive turn was a sudden\nbrake, so both the agent and the reference controller \"learned\" not to turn, which is the same as\nnot taking corners.\n\n**If the world is broken, nothing makes up for it.** The procedurally generated tracks had sections\nwith degenerate geometry and corners that not even a hand-written controller could take.\n\nThat's why an RL project isn't simply:\n\n```text\ninstall PPO\n↓\ntrain\n↓\nget a self-driving car\n```\n\n---\n\n## 15. The RL pilot's spec sheet\n\nWith all the pieces on the table, this is how the problem was defined across the eight training\nruns:\n\n| Piece | In Agentic Racing |\n|---|---|\n| Framework | Unity ML-Agents 4.0.3 (Unity package) and `mlagents` 1.1.0 (Python) |\n| Algorithm | PPO |\n| Observation | 42 numbers: 27 from raycasts, plus 15 for speed, relation to the track, curvature ahead and directive |\n| Actions | 3 continuous: `steer` and `throttle` in [−1, 1], `brake` in [0, 1] |\n| Frequency | physics at 50 Hz, one decision every 5 steps (10 per second) |\n| Network | MLP with 2 layers × 256 neurons, normalized observations |\n| Hyperparameters | $\\gamma = 0.995$, $\\lambda = 0.95$; learning rate $3 \\times 10^{-4}$, $\\epsilon = 0.2$ and `beta` = 0.01 (0.005 in the first run) as initial values, with linear decay |\n| Episode | one lap; the car spawns at a random point on the circuit, with heading noise (±10°) and position noise (±2 m), already rolling at 8 m/s |\n| Episode end | lap completed; more than 2 m off the track; 8 s stopped; 4.5 s going the wrong way; less than 8 m of progress in 5 s; or 4,000 physics steps (~80 s) |\n| Progress | meters advanced along the circuit's centerline, measured every step |\n| Parallelism | 9 arenas per process × 4 processes = 36 cars, on 9 different procedural circuits |\n\n(The code has changed since then: today the arenas use a single fixed circuit and the limit is\n6,000 steps, because a lap of that circuit takes about 95 seconds.)\n\nAnd here's how it went. Eight runs, 4 to 20 million steps each, with seven versions of the reward and\nseveral physics fixes. The fraction of the lap the car completed before the episode ended always\nstayed around 10% (between 8 and 13%). In the eighth run, 91% of episodes ended with the car off\nthe track.\n\nTo separate learning problems from environment problems, the project used a hand-written\ncontroller, with no learning at all, on the same physics and the same tracks. Its best episode\nreached 82% of a lap at 21 m/s, but on average it got stuck almost as often as the RL agent. That\nalready said something: if a hand-designed controller couldn't handle those tracks either, the\nproblem wasn't just learning. The confirmation came from switching to a fixed, clean circuit, with\nno procedural generation. There, that controller completed full laps without a single incident.\n\n---\n\n## 16. What happens at the beginning?\n\nLet's imagine we start training with a policy that doesn't know how to drive yet.\n\nThe initial behavior is essentially chaotic:\n\n```text\n      🏎️\n       \\\n        \\\n         → off the track\n```\n\nIn Agentic Racing's first run, during the first 50,000 steps, episodes lasted about four seconds\nand the mean reward was negative (−1.1). The car barely got going before leaving the track or\nstopping.\n\nThe agent has no human representation of:\n\n> \"This is a dangerous corner.\"\n\nAll it has is numbers. It doesn't think:\n\n> \"I'm going too fast.\"\n\nIt has an observation containing certain values, and it has to learn the relationship between those\nvalues, the actions, and the consequences.\n\nAfter enough experience, we want patterns like this one to emerge:\n\n```text\nobservation:\ntight corner + high speed\n            │\n            ▼\n         brake\n            │\n            ▼\n      turn better\n            │\n            ▼\n     stay on track\n            │\n            ▼\n      more reward\n```\n\nAnd those patterns end up encoded in the policy's parameters.\n\nIn Agentic Racing, that particular pattern **never emerged**. When the policies from the third and\nfourth runs were evaluated, the average brake value was 0.02 out of 1: the agent practically never\nbraked. It had learned one of two things. Either it crawled at 7 m/s so it could turn without\nbraking, or it went full throttle and ended up off the track or stopped shortly after the first\nreal corner.\n\nWhy didn't it figure it out? In the seventh run, with a reward that already paid for braking before\ncorners, the hypothesis was a **hard-exploration** problem. The full corner maneuver (brake, turn,\naccelerate again) is a coordinated sequence lasting one or two seconds. 97% of episodes ended off\nthe track about 200 meters from the start, right at that first corner. The agent reached the corner\nover and over, but almost never stumbled onto the right sequence by chance, so it almost never had\nthe experience that would have taught it that braking was worth it.\n\nThat hypothesis was only partly right. Later on, the physics and track-geometry problems from\nsection 14 surfaced, and they made that maneuver much harder than it should have been.\n\n---\n\n## 17. Are we programming the driving rules?\n\nNot exactly. This is a fundamental difference between a traditional controller and a learned\nagent.\n\nIn a rule-based system we would write something like this, which is a simplified version of the\nreference controller that exists in the project:\n\n```text\ncorner_ahead = largest heading change over the next 60 m\ntarget_speed = lerp(40% of vmax, 10% of vmax, corner_ahead / 45°)\n\nIF speed < target_speed:\n    accelerate\nELSE IF speed > target_speed + margin:\n    brake\nsteering = aim at a point on the racing line, 12 to 36 m ahead\n```\n\nIn Reinforcement Learning we try to make the behavior emerge from the learning process. We define:\n\n- what the agent can observe;\n- what actions it can take;\n- what consequences each action has;\n- what rewards it receives;\n- which algorithm it will use to learn.\n\nBut we don't hand-specify the driving decisions.\n\nThat's exactly what makes it interesting, and also what makes it hard. The rule-based controller\nabove is the one that drives the cars in the demo today. The directive channels the RL agent had in\nits observations now modulate those rules directly: more aggression means a higher target speed\nand braking later. To demonstrate the relationship between pilot and strategist, a hand-written\npilot whose behavior changes with the directive met the requirement. Getting to the same point with\nRL would have taken more iterations than the project could afford. On the fixed circuit, where the\ntrack is no longer an obstacle, PPO would probably learn to complete laps, but that experiment is\nstill pending.\n\n---\n\n## 18. From individual behavior to a policy\n\nAfter enough interactions, the neural network tries to approximate a function:\n\n$$\n\\pi_\\theta(a \\mid o)\n$$\n\nThat is:\n\n> \"Given this observation, which action should I produce?\"\n\nThe network doesn't store an explicit list of rules. Instead of:\n\n```text\nif curve then brake\nif straight then accelerate\nif left then steer left\n```\n\nit learns a continuous, parameterized function:\n\n```text\n                    OBSERVATION (42)\n                         │\n                         ▼\n                ┌─────────────────┐\n                │                 │\n                │  MLP 2 × 256    │\n                │                 │\n                └────────┬────────┘\n                         │\n             ┌───────────┼───────────┐\n             ▼           ▼           ▼\n         steering     throttle      brake\n```\n\nThe challenge is getting that function to improve in a stable way.\n\nAnd that's where our next algorithm comes in.\n\n---\n\n## 19. The next step: PPO\n\nUp to this point we've built the conceptual map:\n\n```text\nAgent\n  │\n  ├── observes\n  │\n  ├── chooses an action\n  │\n  ├── receives reward\n  │\n  ├── accumulates experience\n  │\n  ├── estimates which states/actions are good\n  │\n  └── improves its policy\n```\n\nBut we still haven't explained **how** we update the policy mathematically.\n\nOne of the best-known answers is **Proximal Policy Optimization (PPO)**, the algorithm Agentic\nRacing used in all of its runs.\n\nPPO introduces an especially interesting idea:\n\n> We want to improve the policy, but we don't want to change it too much from one update to the\n> next.\n\nWhy? Because an overly aggressive update could destroy behaviors that were already working.\n\nThe intuition is:\n\n```text\nCurrent policy\n     │\n     │ small improvement\n     ▼\nNew policy\n     │\n     │ small improvement\n     ▼\nNew policy\n     │\n     ▼\n...\n```\n\ninstead of:\n\n```text\nCurrent policy\n     │\n     │ big jump\n     ▼\nCompletely different policy\n     │\n     ▼\nunstable behavior\n```\n\nPPO compares the probability that the new and the old policy assign to each action, weights it by\nthe **advantage**, and stops rewarding changes that move too far. The `epsilon: 0.2` in our\nconfiguration sets that limit: once the ratio between an action's new and old probability moves\nmore than 20% away from 1, moving it further no longer improves the objective.\n\nThe mathematical detail of this mechanism will be the topic of the next article.\n\n---\n\n## 20. But Agentic Racing taught us something even more important\n\nWhen we build a real RL system, the algorithm is just one piece. Changing any of the others (what\nthe agent sees, what it can do, what it's rewarded for, the physics it experiences) can completely\nchange the learned behavior.\n\nThat's why, when an agent doesn't learn, the question shouldn't only be:\n\n> \"Which PPO parameter do I need to change?\"\n\nWe should also ask:\n\n> \"What is the agent seeing?\"\n\n> \"What can it do?\"\n\n> \"What reward is it receiving?\"\n\n> \"What physical dynamics is it experiencing?\"\n\n> \"Is the environment providing a coherent learning signal?\"\n\nIn Agentic Racing, all five questions had uncomfortable answers at some point: it couldn't see\ncorners coming, the reward had an easy way out, the physics turned every turn into a sudden brake,\nand the track itself had impossible sections. None of those problems could be fixed by touching\nPPO's hyperparameters. Every run used PPO; what changed from one to the next was the reward, the\nagent's perception, or the physics.\n\nThe lesson that stuck with me most: **before spending compute on a sophisticated method, validate\nthat the environment can be solved with a simple one**. If a hand-written controller can't complete\na lap, it's unlikely that a reward will teach a neural network to do it.\n\n---\n\n## 21. The complete mental map\n\nWe can summarize everything so far like this:\n\n```text\n                       AGENT\n                         │ observes\n                         ▼\n                    OBSERVATION\n                         │\n                         ▼\n                      POLICY\n                         │ chooses\n                         ▼\n                      ACTION\n                         │\n                         ▼\n                    ENVIRONMENT\n                         │\n             ┌───────────┴───────────┐\n             ▼                       ▼\n        NEW STATE                 REWARD\n             └───────────┬───────────┘\n                         ▼\n                    EXPERIENCE\n                         │\n                         ▼\n                 LEARNING ALGORITHM\n                         │\n                         ▼\n                  UPDATED POLICY ──────► ...\n```\n\nThe mathematical concepts we'll keep building form a chain:\n\n$$\n\\begin{gathered}\n\\text{Observation} \\rightarrow \\text{Action} \\rightarrow \\text{Reward} \\rightarrow \\text{Return} \\\\\n\\rightarrow V \\rightarrow Q \\rightarrow \\text{Advantage} \\rightarrow \\text{Policy learning}\n\\end{gathered}\n$$\n\nWe don't need to memorize all of these formulas yet. What matters is understanding which question\neach one answers:\n\n| Concept | Question |\n|---|---|\n| Observation | What can the agent see? |\n| Action | What can it do? |\n| Reward | How good was the immediate consequence? |\n| Return | How much accumulated reward do I get from here? |\n| Value $V(s)$ | How promising is this situation? |\n| Q-value $Q(s,a)$ | How good is this action in this situation? |\n| Advantage $A(s,a)$ | Is this action better or worse than expected? |\n| Policy $\\pi(a \\mid s)$ | Which action should I choose? |\n\nWith this map we can start studying algorithms like PPO without treating them as a collection of\ndisconnected formulas.\n\n---\n\n## 22. One last idea: the agent learns from experience, not from explanations\n\nThis may be the most important idea to take away from this article.\n\nWe can look at a corner and say:\n\n> \"You need to brake here.\"\n\nThe agent doesn't get that explanation. It gets data, acts, observes the consequences, gets a\nreward, and repeats.\n\nMillions of small interactions can turn an initially useless policy into one that produces complex\nbehavior.\n\nThat's what's fascinating about Reinforcement Learning:\n\n> **We don't program the final behavior directly. We design a process through which the agent can\n> discover it.**\n\nAnd Agentic Racing shows the other side of that sentence: if the process is badly designed, the\nagent discovers something else. It discovered that stopping paid more than continuing, that turning\nwas dangerous, and that braking was pointless. Each of those lessons was perfectly rational given\nthe world and the reward we gave it. The agent learned what we taught it, which wasn't what we\nmeant to teach it.\n\nIn the next article we'll open up the **PPO** box: what exactly it does with all that experience,\nand why its way of updating the policy is so stable.\n\n---\n\n## What's next\n\n**Next article in the series (part 2):**\n\n### [How Do We Improve a Policy Without Breaking It? PPO Explained from Scratch](/blog/how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch/)\n\nIn it we build, step by step:\n\n1. Policy Gradient\n2. Actor-Critic\n3. Value Function\n4. Advantage\n5. GAE\n6. Probability Ratio\n7. Clipping\n8. PPO Objective\n9. The full training loop\n10. How all of this shows up in Agentic Racing\n\nAnd above all, we answer one question:\n\n> **Why does PPO work better when we stop it from changing its mind too quickly?**\n\nThe agent code (`RaceAgent.cs`), the training configuration (`race_ppo.yaml`) and the full log of\nall eight runs are at [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing)."},{"slug":"a-trip-planner-that-reasons-without-generating","lang":"en","title":"A Trip Planner That Reasons Without Generating: Chain-of-Agents with a 1.5B Model","description":"I took the Chain-of-Agents pattern from chapter 7 of 30 Agents Every AI Engineer Must Build and turned it into a real demo: a local 1.5B model that writes only what it must, a JEV-style decision engine that answers with probabilities instead of text, and a budget-conflict loop that resolves without generating a single token. What worked, what the model made up, and how the code ended up writing the honest part.","publishDate":"2026-09-28","updatedDate":null,"tags":["agents","llm","langgraph","ollama","python","fastapi"],"translationSlug":"un-planificador-de-viajes-que-razona-sin-generar","url":"https://alexisalulema.com/blog/a-trip-planner-that-reasons-without-generating/","series":null,"headings":[{"depth":2,"slug":"the-constraints-defined-the-design-again","text":"The constraints defined the design (again)"},{"depth":2,"slug":"jev-reasoning-without-generating","text":"JEV: reasoning without generating"},{"depth":2,"slug":"generate-once-decide-many-times","text":"Generate once, decide many times"},{"depth":2,"slug":"resolving-the-conflict-not-just-detecting-it","text":"Resolving the conflict, not just detecting it"},{"depth":2,"slug":"when-the-model-doesnt-know-where-shibuya-is","text":"When the model doesn’t know where Shibuya is"},{"depth":2,"slug":"the-honest-part-is-written-by-code","text":"The honest part is written by code"},{"depth":2,"slug":"langgraph-only-for-orchestration","text":"LangGraph, only for orchestration"},{"depth":2,"slug":"live-data-the-api-that-wouldnt-respond","text":"Live data: the API that wouldn’t respond"},{"depth":2,"slug":"what-i-deliberately-didnt-do","text":"What I deliberately didn’t do"},{"depth":2,"slug":"conclusion","text":"Conclusion"}],"body":"One book I found both interesting and fun to read is Imran Ahmad's *30 Agents Every AI Engineer\nMust Build*. In chapter 7, the one about **Chain-of-Agents**, he uses a trip planner to explain the\npattern, and does it in a genuinely educational way: an orchestrator that does no domain work,\nspecialists that hand work to each other, a shared memory, and a mechanism for when two results\ndon't add up. I liked the idea, and set out to build it with one of those patterns as the\nfoundation, using some concepts that differ from the book and that address some of the loose ends\nin Mr. Ahmad's demo.\n\nThe loose end that caught my attention most was the conflict. In the code that accompanies the\nchapter, the conflict is **detected**: a score is computed, compared against a threshold, and the\nreport recommends further investigation. Arbitration, consensus and renegotiation stay in the text\nof the book. The specialists, on top of that, return simulated data. That's reasonable for a\nteaching notebook that covers three patterns in a few cells, but it left me with the question I\nactually cared about: what happens when the conflict has to be **resolved**, with a real model that\ngets things wrong and real data that sometimes doesn't arrive?\n\nThe result is a demo where you type something like *\"Quito, 3 days, $600, food and historic\ncenter\"* and watch, live, as a chain of agents builds the itinerary: which agent is working, what it\ndecides, with what probability, where a budget conflict appears, and how the chain resolves it\nwithout anyone stepping in. The pretty itinerary isn't the point. The point is **making the\norchestration visible**.\n\nAnd the same thing I said in previous posts: I built it together with a coding assistant, over\nlong sessions of decisions and measurements, with a devlog that records every problem and how it\nwas solved.\n\n## The constraints defined the design (again)\n\nThis demo lives on the same ephemeral infrastructure as the previous ones, so it inherited their\nrules before I wrote a single line:\n\n- **No paid API.** A local LLM, served by Ollama. No per-token bills.\n- **CPU only, 2 vCPU / 4 GiB** for the whole pod, model included.\n- **Stateless.** The environment can die at any moment.\n- **A small, open model.** I evaluated Qwen, Llama and Phi, and settled on **Qwen 2.5 1.5B**.\n\nA 1.5B model on two cores is slow and gets facts wrong. That combination designed everything else.\n\n## JEV: reasoning without generating\n\nOne of the ideas I wanted in this project was **JEV**, from TypeSafe, which was released recently\n(in early access since September 15). It's what they call a *System One* model: instead of\ngenerating text token by token, it takes a program's state and a set of questions whose shape you\ndeclare up front —which fields you want and which values each one can take— and returns those\nanswers filled in, each with its probability. I found it interesting to include reasoning ability\nwithout having to pay for generation to get the answer, just an array of predefined answers. If the\nanswer can only be one of four options, there's no way for it to invent a fifth.\n\nThe problem was practical: JEV is a hosted API, in early access with a waitlist and no option to run\nit locally, so it didn't meet the \"everything local\" rule. The solution was to keep **the shape**\nand make the engine swappable. The demo defines an interface shaped like JEV: state plus typed\nquestions, which return answers with probabilities.\n\n```python\nTypedQuestion(\n    id=\"harm:cheaper_lodging\",\n    family=\"action_harm\",          # to dispatch rules\n    kind=\"score\",                  # \"choice\" with options, or \"score\" -> P(yes)\n    text=\"Does downgrading the lodging hurt the traveler's interests?\",\n    params={\"action_id\": \"cheaper_lodging\"},\n)\n# -> Answer(value=0.1, probabilities={\"yes\": 0.1, \"no\": 0.9}, source=\"rules\")\n```\n\nToday it's implemented by a **rule-based** engine: a keyword taxonomy in Spanish and English plus\nexplicit heuristics. It's deterministic, instant and works offline. Each question carries a `family`\nfor the rules and a natural-language `text`, which is exactly what JEV would evaluate. The day I get\naccess, plugging it in means implementing one method.\n\n(I'll admit that writing this made me think again about\n[Agentic Racing](/blog/agentic-racing-a-pilot-that-never-learned-to-drive/). In the end, the team\nboss only chose between attack, defend or push, with low, medium or high aggression. A driver\ndoesn't need a paragraph over the radio: they need one of three words, and fast. We'll see.)\n\n## Generate once, decide many times\n\nThe first version regenerated the itinerary on every round of the budget conflict. On CPU, that\nmeant several long JSON outputs per trip, and it didn't fit in a reasonable time. That's where the\nprinciple that organizes the whole project came from: **each step uses the cheapest mechanism that\nsolves it well**.\n\n| Step | Mechanism |\n|---|---|\n| Intake: classify each interest | Typed decision |\n| Live data: weather, places, exchange rate | Public APIs, no LLM |\n| Destination research | Curated catalog + live data (with luck, 0 tokens) |\n| Itinerary | Generation, **only once** |\n| Budget | Python arithmetic |\n| Conflict resolution | Typed decisions + arithmetic |\n| Itinerary revision | Code, **no regeneration** |\n| Synthesis | Generation (the narrative) + code (the figures) |\n\nThe model writes twice per trip; everything else is a decision or a calculation. A full trip uses\nbetween 850 and 1,200 tokens, against a 6,000 cap, and takes 25 to 45 seconds within the pod's\nlimits. The conflict loop uses **zero**.\n\n## Resolving the conflict, not just detecting it\n\nThis is what I'd wanted to build since I read the chapter. When the total exceeds the budget, the\nconflict agent doesn't ask the model to \"make the trip cheaper\". It has a fixed catalog of actions:\nswap paid activities for free walks, downgrade the lodging, eat at markets, buy a transit pass. For\neach one:\n\n1. **Code computes how much it saves.** It's arithmetic over the itinerary's cost assumptions.\n2. **The decision engine estimates how much it hurts** the traveler's interests, as a probability.\n   For someone who asked for museums, free walks hurt more than for someone who asked to walk.\n3. **Code chooses** by `savings × (1 − P(harm))`, applies the changes to the itinerary and\n   re-runs the budget.\n\nThe loop has a hard cap of two iterations. If the trip still doesn't fit after that, the result\n**says so**. Lisbon for two people on $50 is my favorite test case: two rounds of cuts, free walks\nin every district, and an honest verdict that it isn't enough. A planner that always says your trip\nfits isn't a planner, it's a salesman.\n\nEvery decision shows up in the trace with its probability.\n\n## When the model doesn't know where Shibuya is\n\nThe first trips with the real model were educational. Qwen 1.5B put **Shibuya, which is in Tokyo,\nas a district of Kyoto**. It invented districts in Hanoi, a \"Museo del Ámbito\" that doesn't exist,\nand priced Hanoi higher than Lisbon.\n\nPrompts improved the prices, but not the geography. Then I ran a benchmark of 1.5B against 3B, with\nthe same trips and the same CPU limits:\n\n| Trip | 1.5B | 3B |\n|---|---|---|\n| Kyoto (catalog) | 31 s | 71 s |\n| Lisbon, $50 (catalog) | 21 s | 68 s |\n| Hanoi (catalog) | 25 s | 56 s |\n| Valparaíso (model only) | 23 s | 75 s |\n\nThe 3B took more than twice as long and **didn't improve the geography**: it put Kinkaku-ji in\nGion, \"Ribeira\" (which is in Porto) in Lisbon, and \"Paseo Alcorta\" (which is in Buenos Aires) in\nValparaíso.\n\n**A prompt doesn't teach a small model what it doesn't know, and a slightly bigger model doesn't\neither.** Facts that have to be right are stored as data. The demo has a curated catalog of 45\ncities, each with three districts, and **each district lists the highlights that are physically in\nit**. Code decides which district each day visits and passes the model only that district's\nhighlights, so a temple can't end up on the wrong day or in the wrong district. If the model returns\na different district, normalization enforces the planned one. The model still writes all the prose;\nwhat it no longer decides is the geography.\n\nThe same thing happened with the cuts: the model wrote \"Explore Alfama's streets\" on all three days\nin Lisbon. The free alternative moved to code too: *\"Free walk around Alfama: Miradouro de Santa\nLuzia\"*.\n\n## The honest part is written by code\n\nThe bug that taught me the most came up in the first e2e test with the real model. The final\nnarrative said:\n\n> *\"You can expect to spend $366, which is within your budget of $50.\"*\n\nThe chain had correctly computed that the trip exceeded the budget. The model, while drafting the\nsummary, decided the opposite, with all the confidence of a well-built sentence. It's the same\nmistake the RAG chatbot made with the World Cup: **a guarantee that depends on a 1.5B model having\ndiscipline is not a guarantee**.\n\nThe solution was to split the synthesis in two. The model writes the narrative **without seeing any\nnumbers** and is forbidden from talking about money; a filter in code removes any sentence that does\nanyway. The budget paragraph —total, cuts applied, whether it fits or not— is written by code. The\nCI e2e test **fails** if the summary contradicts the budget verdict, so the rule doesn't depend on\nme remembering to check it.\n\nThe same filter drops sentences that name places that aren't in the plan (\"Parque Lagoa\"). It's a\nheuristic with known limits: \"Kyoto Central station\" gets through, because \"Kyoto\" is a known name.\n\n## LangGraph, only for orchestration\n\nThe first version of the orchestrator was a hand-written loop. It worked, but to showcase the\npattern, a recognizable framework and a diagram generated from the real graph were worth the\nmigration. The orchestrator became a **LangGraph** `StateGraph` whose state is the shared memory\nitself:\n\n- **One node per agent**, which returns only its own section; it never mutates the state.\n- **Reducers** for the trace and the token count, so the memory stays append-only.\n- **Routing in code functions**, with no LLM: after the budget, to conflict if it's over and\n  iterations remain; otherwise, to synthesis.\n- **Live events** through LangGraph's `custom` stream, relayed to the browser over SSE.\n\nLangGraph is used **only for orchestration**. The model is still called with the app's own Ollama\nclient, without LangChain wrappers. The proof that the migration went well was boring, which is how\nthese proofs should be: the 56 existing tests passed **unchanged**.\n\n## Live data: the API that wouldn't respond\n\nThe last phase added facts that change: the real weather for the trip dates, the exchange rate to\nthe local currency and, for cities outside the catalog, real districts and places. All from free\nsources with no API key: Open-Meteo, European Central Bank rates, Wikidata and OpenStreetMap. None\nof them is required: if one fails, the chain continues with the catalog or the model and says so in\nthe trace.\n\nFor places I started with OpenStreetMap's Overpass API. Over five e2e runs in CI, I went through\nthis:\n\n1. The public instance returned **504**. I added a second, fallback instance.\n2. **Both** timed out. If two independent servers fail, the problem isn't load, it's the query: it\n   asked for nodes, ways and relations, and resolving relation geometry is expensive. I rewrote it\n   with a bounding box and only nodes and ways.\n3. With the lighter query, 504 and timeout again: about 28 extra seconds per trip.\n4. I switched the primary source to **Wikidata**, with Overpass as fallback. It responded in\n   **0.5 s**.\n\nValparaíso went from districts invented by the model (\"Casa Blanca\", \"Playa de Coquimbo\") to real\nplaces —the Cathedral, the Open-Air Museum—, with 5 of 5 highlights on their day, and the live step\ndropped from 28 to 1.3 seconds. **When a public dependency fails consistently, switching\ndependencies is usually cheaper than continuing to negotiate with it.**\n\nReal weather also brought an effect I didn't expect: for catalog cities, the weather note is now\nwritten by code from the forecast numbers, and destination research stopped generating. **Zero\ntokens.** Real data didn't just make the demo more honest, it made it cheaper.\n\n## What I deliberately didn't do\n\n- **No human in the loop.** The book includes one in its workflows, and it's a real gap compared to\n  the pattern. For a public demo meant to show automatic resolution, I left it out.\n- **No real hotel or flight prices.** The APIs that offer them require a key and a commercial\n  agreement. Weather and exchange rates are live; prices are still estimates.\n- **No JEV, yet.** The interface is ready; access is what's missing.\n\n## Conclusion\n\nLooking back, almost every problem in this project had the same solution: **take a decision away\nfrom the model**. Geography moved to a catalog, the budget to Python, the free alternative to code,\nthe summary's figures to code, the weather to an API. What's left for the model is what a 1.5B model\ndoes well: writing prose around facts that are already correct.\n\nAnd that, I think, is what the chapter 7 pattern gains when it leaves the notebook. A deterministic\norchestrator, a shared memory that only grows, and a conflict resolved with visible decisions aren't\njust a tidy way to coordinate agents. They're how you know **which part of the system is allowed to\nmake things up**, and make sure the answer is \"only the prose\".\n\nThe demo is available at [alexisalulema.com/projects](https://alexisalulema.com/projects?demo=trip-planner): it's\nprovisioned when you ask for it, runs for a few minutes and destroys itself. The code, with the full\nlog of decisions, measurements and mistakes, is at\n[github.com/alulema/trip-planner](https://github.com/alulema/trip-planner).\n\nIf JEV gives me access, this post will get a part two."},{"slug":"un-planificador-de-viajes-que-razona-sin-generar","lang":"es","title":"Un planificador de viajes que razona sin generar: Chain-of-Agents con un modelo de 1.5B","description":"Tomé el patrón Chain-of-Agents del capítulo 7 de 30 Agents Every AI Engineer Must Build y lo llevé a un demo real: un modelo local de 1.5B que escribe solo lo indispensable, un motor de decisiones al estilo JEV que responde con probabilidades en vez de texto, y un ciclo de conflicto de presupuesto que se resuelve sin generar un solo token. Lo que funcionó, lo que el modelo inventó y cómo el código terminó escribiendo la parte honesta.","publishDate":"2026-09-28","updatedDate":null,"tags":["agents","llm","langgraph","ollama","python","fastapi"],"translationSlug":"a-trip-planner-that-reasons-without-generating","url":"https://alexisalulema.com/es/blog/un-planificador-de-viajes-que-razona-sin-generar/","series":null,"headings":[{"depth":2,"slug":"las-restricciones-definieron-el-diseño-otra-vez","text":"Las restricciones definieron el diseño (otra vez)"},{"depth":2,"slug":"jev-razonar-sin-generar","text":"JEV: razonar sin generar"},{"depth":2,"slug":"generar-una-vez-decidir-muchas","text":"Generar una vez, decidir muchas"},{"depth":2,"slug":"resolver-el-conflicto-no-solo-detectarlo","text":"Resolver el conflicto, no solo detectarlo"},{"depth":2,"slug":"cuando-el-modelo-no-sabe-dónde-queda-shibuya","text":"Cuando el modelo no sabe dónde queda Shibuya"},{"depth":2,"slug":"lo-honesto-lo-escribe-el-código","text":"Lo honesto lo escribe el código"},{"depth":2,"slug":"langgraph-solo-para-orquestar","text":"LangGraph, solo para orquestar"},{"depth":2,"slug":"datos-en-vivo-la-api-que-no-respondía","text":"Datos en vivo: la API que no respondía"},{"depth":2,"slug":"lo-que-deliberadamente-no-hice","text":"Lo que deliberadamente no hice"},{"depth":2,"slug":"conclusión","text":"Conclusión"}],"body":"Un libro que me resultó interesante y entretenido de leer ha sido el de Imran Ahmad, *30 Agents\nEvery AI Engineer Must Build*. En el capítulo 7, el que habla sobre **Chain-of-Agents**, usa un\nplanificador de viajes para explicar el patrón, y lo hace de una manera bastante educativa: un\norquestador que no hace trabajo de dominio, especialistas que se pasan el trabajo, una memoria\ncompartida y un mecanismo para cuando dos resultados no cuadran. La idea me gustó, y me embarqué en\ndesarrollarla con uno de esos patrones como base, usando algunos conceptos que difieren del libro y\nque resuelven algunos pendientes del demo de Mr. Ahmad.\n\nEl pendiente que más me llamó la atención fue el conflicto. En el código que acompaña al capítulo el\nconflicto se **detecta**: se calcula un puntaje, se compara contra un umbral y el reporte recomienda\ninvestigar más. El arbitraje, el consenso y la renegociación quedan en el texto del libro. Los\nespecialistas, además, devuelven datos simulados. Es lo razonable para un notebook didáctico que\ncubre tres patrones en pocas celdas, pero me dejó con la pregunta que me interesaba de verdad: ¿qué\npasa cuando el conflicto hay que **resolverlo**, con un modelo real que se equivoca y datos reales\nque a veces no llegan?\n\nEl resultado es un demo donde escribes algo como *\"Quito, 3 días, $600, comida y centro histórico\"*\ny miras, en vivo, cómo una cadena de agentes arma el itinerario: qué agente trabaja, qué decide, con\nqué probabilidad, dónde aparece un conflicto de presupuesto y cómo la cadena lo resuelve sin que\nintervenga nadie. El itinerario bonito no es el punto. El punto es **que la orquestación se vea**.\n\nY lo mismo que dije en posts anteriores: lo construí junto con un asistente de código, en sesiones\nlargas de decisiones y mediciones, con una bitácora que registra cada problema y cómo se resolvió.\n\n## Las restricciones definieron el diseño (otra vez)\n\nEste demo vive en la misma infraestructura efímera que los anteriores, así que heredó sus reglas\nantes de escribir una línea:\n\n- **Sin API de pago.** LLM local, servido por Ollama. Nada de facturas por token.\n- **CPU only, 2 vCPU / 4 GiB** para todo el pod, incluido el modelo.\n- **Sin estado.** El entorno puede morir en cualquier momento.\n- **Un modelo pequeño y abierto.** Evalué Qwen, Llama y Phi, y me decidí por **Qwen 2.5 1.5B**.\n\nUn modelo de 1.5B en dos núcleos es lento y se equivoca en hechos. Esa combinación diseñó todo lo\ndemás.\n\n## JEV: razonar sin generar\n\nUna de las ideas que quise incluir en este proyecto fue **JEV**, de TypeSafe, que ha sido publicado\nrecientemente (en acceso anticipado desde el 15 de septiembre). Es lo que ellos llaman un modelo\n*System One*: en vez de generar texto token a token, recibe el estado de un programa y un conjunto de\npreguntas cuya forma declaras de antemano —qué campos quieres y qué valores puede tomar cada uno— y\ndevuelve esas respuestas llenas, cada una con su probabilidad. Me pareció interesante incluir la\ncapacidad de razonamiento sin tener que incurrir en generación para la respuesta, sino en un array de\nrespuestas definidas. Si la respuesta solo puede ser una de cuatro opciones, no hay forma de que\ninvente una quinta.\n\nEl problema fue práctico: JEV es una API alojada, en acceso anticipado con lista de espera y sin\nopción de correrlo localmente, así que no cumplía la regla de \"todo local\". La solución fue quedarme\ncon **la forma** y dejar el motor intercambiable. El demo define una interfaz con la forma de JEV:\nestado más preguntas tipadas, que devuelven respuestas con probabilidades.\n\n```python\nTypedQuestion(\n    id=\"harm:cheaper_lodging\",\n    family=\"action_harm\",          # para despachar reglas\n    kind=\"score\",                  # \"choice\" con opciones, o \"score\" -> P(sí)\n    text=\"¿Bajar la categoría del alojamiento daña los intereses del viajero?\",\n    params={\"action_id\": \"cheaper_lodging\"},\n)\n# -> Answer(value=0.1, probabilities={\"yes\": 0.1, \"no\": 0.9}, source=\"rules\")\n```\n\nHoy la implementa un motor **por reglas**: una taxonomía de palabras clave en español e inglés y\nheurísticas explícitas. Es determinístico, instantáneo y funciona sin red. Cada pregunta lleva un\n`family` para las reglas y un `text` en lenguaje natural, que es exactamente lo que evaluaría JEV. El\ndía que tenga acceso, enchufarlo es implementar un método.\n\n(Confieso que escribiendo esto volví a pensar en\n[Agentic Racing](/es/blog/agentic-racing-un-piloto-que-nunca-aprendio-a-manejar/). Al final, el jefe\nde equipo solo elegía entre atacar, defender o empujar, con agresividad baja, media o alta. Un piloto\nno necesita un párrafo por radio: necesita una de tres palabras, y rápido. Ya veremos.)\n\n## Generar una vez, decidir muchas\n\nLa primera versión regeneraba el itinerario en cada ronda del conflicto de presupuesto. En CPU, eso\neran varias salidas JSON largas por viaje, y no cabía en un tiempo razonable. De ahí salió el\nprincipio que ordena todo el proyecto: **cada paso usa el mecanismo más barato que lo resuelve bien**.\n\n| Paso | Mecanismo |\n|---|---|\n| Intake: clasificar cada interés | Decisión tipada |\n| Datos en vivo: clima, lugares, tipo de cambio | APIs públicas, sin LLM |\n| Investigación del destino | Catálogo curado + datos en vivo (con suerte, 0 tokens) |\n| Itinerario | Generación, **una sola vez** |\n| Presupuesto | Aritmética en Python |\n| Resolución de conflictos | Decisiones tipadas + aritmética |\n| Revisión del itinerario | Código, **sin regenerar** |\n| Síntesis | Generación (la narrativa) + código (las cifras) |\n\nEl modelo escribe dos veces por viaje; todo lo demás es una decisión o una cuenta. Un viaje completo consume entre 850 y 1.200 tokens, contra un tope de 6.000, y\ntarda de 25 a 45 segundos con los límites del pod. El ciclo de conflicto consume **cero**.\n\n## Resolver el conflicto, no solo detectarlo\n\nAquí está lo que quería construir desde que leí el capítulo. Cuando el total excede el presupuesto,\nel agente de conflictos no le pide al modelo \"haz el viaje más barato\". Tiene un catálogo fijo de\nacciones: cambiar actividades de pago por paseos gratuitos, bajar la categoría del alojamiento, comer\nen mercados, comprar un pase de transporte. Para cada una:\n\n1. **El código calcula cuánto ahorra.** Es aritmética sobre los supuestos de costo del itinerario.\n2. **El motor de decisiones estima cuánto daña** los intereses del viajero, como una probabilidad. A\n   quien pidió museos, los paseos gratuitos le hacen más daño que a quien pidió caminar.\n3. **El código elige** por `ahorro × (1 − P(daño))`, aplica los cambios al itinerario y vuelve a\n   presupuestar.\n\nEl ciclo tiene un tope duro de dos iteraciones. Si después de eso el viaje sigue sin caber, el\nresultado **lo dice**. Lisboa para dos personas con $50 es mi caso de prueba favorito: dos rondas de\nrecortes, paseos gratuitos en cada barrio, y un veredicto honesto de que no alcanza. Un planificador\nque siempre dice que tu viaje cabe no es un planificador, es un vendedor.\n\nCada decisión aparece en el trace con su probabilidad.\n\n## Cuando el modelo no sabe dónde queda Shibuya\n\nLos primeros viajes con el modelo real fueron educativos. Qwen 1.5B puso **Shibuya, que está en\nTokio, como barrio de Kioto**. Inventó barrios en Hanói, un \"Museo del Ámbito\" que no existe, y le\npuso a Hanói precios más altos que a Lisboa.\n\nLos prompts mejoraron los precios, pero no la geografía. Después corrí un benchmark de 1.5B contra\n3B, con los mismos viajes y los mismos límites de CPU:\n\n| Viaje | 1.5B | 3B |\n|---|---|---|\n| Kioto (catálogo) | 31 s | 71 s |\n| Lisboa, $50 (catálogo) | 21 s | 68 s |\n| Hanói (catálogo) | 25 s | 56 s |\n| Valparaíso (solo modelo) | 23 s | 75 s |\n\nEl 3B tardó más del doble y **no mejoró la geografía**: puso Kinkaku-ji en Gion, \"Ribeira\" (que está\nen Oporto) en Lisboa, y \"Paseo Alcorta\" (que está en Buenos Aires) en Valparaíso.\n\n**Un prompt no le enseña a un modelo pequeño lo que no sabe, y un modelo un poco más grande tampoco.**\nLos hechos que tienen que ser correctos se guardan como datos. El demo tiene un catálogo curado de 45\nciudades, cada una con tres barrios, y **cada barrio lista las atracciones que están físicamente en\nél**. El código decide qué barrio visita cada día y le pasa al modelo solo las atracciones de ese\nbarrio, así que un templo no puede terminar en el día o en el barrio equivocado. Si el modelo\ndevuelve otro barrio, la normalización impone el del plan. El modelo sigue escribiendo toda la prosa;\nlo que ya no decide es la geografía.\n\nPasó lo mismo con los recortes: el modelo escribía \"Explore Alfama's streets\" en los tres días de\nLisboa. La alternativa gratuita también pasó al código: *\"Paseo libre por Alfama: Miradouro de Santa\nLuzia\"*.\n\n## Lo honesto lo escribe el código\n\nEl bug que más me enseñó salió en el primer e2e con el modelo real. La narrativa final decía:\n\n> *\"You can expect to spend $366, which is within your budget of $50.\"*\n\nLa cadena había calculado bien que el viaje excedía el presupuesto. El modelo, al redactar el\nresumen, decidió lo contrario, con toda la seguridad de una frase bien construida. Es el mismo error\nque el chatbot RAG cometió con el Mundial: **una garantía que depende de que un modelo de 1.5B tenga\ndisciplina no es una garantía**.\n\nLa solución fue partir la síntesis en dos. El modelo escribe la narrativa **sin ver ningún número** y\ncon prohibición de hablar de dinero; un filtro en código elimina cualquier frase que lo haga de todos\nmodos. El párrafo del presupuesto —total, recortes aplicados, si cabe o no— lo escribe el código. El\ne2e de CI **falla** si el resumen contradice el veredicto del presupuesto, así que la regla no depende\nde que yo me acuerde de revisarla.\n\nEl mismo filtro descarta frases que nombran lugares que no están en el plan (\"Parque Lagoa\"). Es\nuna heurística con límites conocidos: \"estación Kyoto Central\" pasa, porque \"Kyoto\" es un nombre\nconocido.\n\n## LangGraph, solo para orquestar\n\nLa primera versión del orquestador era un bucle propio. Funcionaba, pero para mostrar el patrón,\nun framework reconocible y un diagrama generado del grafo real valían la migración. El orquestador pasó a un `StateGraph` de **LangGraph** cuyo estado es la\nmemoria compartida misma:\n\n- **Un nodo por agente**, que devuelve solo su sección; nunca muta el estado.\n- **Reducers** para el trace y el conteo de tokens, así la memoria sigue siendo append-only.\n- **Ruteo en funciones de código**, sin LLM: después del presupuesto, a conflicto si excede y\n  quedan iteraciones; si no, a síntesis.\n- **Eventos en vivo** por el stream `custom` de LangGraph, relayados al navegador por SSE.\n\nLangGraph se usa **solo para orquestar**. El modelo se sigue llamando con un cliente Ollama propio,\nsin wrappers de LangChain. La prueba de que la migración salió bien fue aburrida, que es como deben\nser estas pruebas: los 56 tests existentes pasaron **sin cambios**.\n\n## Datos en vivo: la API que no respondía\n\nLa última fase agregó hechos que cambian: el clima real de las fechas del viaje, el tipo de cambio a\nla moneda local y, para ciudades fuera del catálogo, barrios y lugares reales. Todo con fuentes\ngratuitas y sin API key: Open-Meteo, las tasas del Banco Central Europeo, Wikidata y OpenStreetMap.\nNinguna es obligatoria: si una falla, la cadena sigue con catálogo o modelo y lo dice en el trace.\n\nPara los lugares empecé con la API Overpass de OpenStreetMap. En cinco corridas del e2e en CI pasé\npor esto:\n\n1. La instancia pública respondió **504**. Agregué una segunda instancia de respaldo.\n2. **Las dos** agotaron su timeout. Si fallan dos servidores independientes, el problema no es la\n   carga, es la consulta: pedía nodos, vías y relaciones, y resolver la geometría de las relaciones\n   es caro. La reescribí con una caja delimitadora y solo nodos y vías.\n3. Con la consulta liviana, otra vez 504 y timeout: unos 28 segundos extra por viaje.\n4. Cambié la fuente principal a **Wikidata**, con Overpass de respaldo. Respondió en **0,5 s**.\n\nValparaíso pasó de barrios inventados por el modelo (\"Casa Blanca\", \"Playa de Coquimbo\") a lugares\nreales —la Catedral, el Museo a Cielo Abierto—, con 5 de 5 atracciones en su día, y el paso en vivo\nbajó de 28 a 1,3 segundos. **Cuando una dependencia pública falla de forma consistente, cambiar de\ndependencia suele ser más barato que seguir negociando con ella.**\n\nCon el clima real apareció además un efecto que no esperaba: para las ciudades del catálogo, la nota\ndel clima ahora la escribe el código con los números del pronóstico, y la investigación del destino\ndejó de generar. **Cero tokens.** Datos reales no solo hicieron el demo más honesto, lo hicieron más\nbarato.\n\n## Lo que deliberadamente no hice\n\n- **Sin humano en el ciclo.** El libro lo incluye en sus workflows, y es una carencia real frente al\n  patrón. Para un demo público que quiere mostrar la resolución automática, lo dejé fuera.\n- **Sin precios reales de hoteles ni vuelos.** Las APIs que los ofrecen piden key y acuerdo\n  comercial. Clima y tipo de cambio son en vivo; los precios siguen siendo estimaciones.\n- **Sin JEV, todavía.** La interfaz está lista; falta el acceso.\n\n## Conclusión\n\nMirando atrás, casi cada problema de este proyecto tuvo la misma solución: **sacarle una decisión al\nmodelo**. La geografía pasó a un catálogo, el presupuesto a Python, la alternativa gratuita al código,\nlas cifras del resumen al código, el clima a una API. Lo que quedó para el modelo es lo que un modelo\nde 1.5B hace bien: redactar prosa alrededor de hechos que ya son correctos.\n\nY ahí está, creo, lo que el patrón del capítulo 7 gana cuando sale del notebook. Un orquestador\ndeterminístico, una memoria compartida que solo crece y un conflicto que se resuelve con decisiones\nvisibles no son solo una forma ordenada de coordinar agentes. Son la forma de saber **qué parte del\nsistema está autorizada a inventar**, y de que la respuesta sea \"solo la prosa\".\n\nEl demo está disponible en [alexisalulema.com/projects](https://alexisalulema.com/es/projects?demo=trip-planner): se\nprovisiona cuando lo pides, corre unos minutos y se destruye solo. El código, con la bitácora completa\nde decisiones, mediciones y errores, está en\n[github.com/alulema/trip-planner](https://github.com/alulema/trip-planner).\n\nSi JEV me da acceso, este post va a tener segunda parte."},{"slug":"agentic-racing-a-pilot-that-never-learned-to-drive","lang":"en","title":"Agentic Racing: A Pilot That Never Learned to Drive, a Team Boss That Did Learn to Reason","description":"I built a racing demo where a reactive pilot obeys a team-boss LLM that reasons per event. Eight attempts at training the pilot with reinforcement learning didn't work — the track was broken before the algorithm ever had a chance — and measuring how much the strategist actually contributes turned into the more interesting experiment: seven runs, real and partially fixable reasoning biases, and a lesson about trusting the full benchmark over the quick desk-check.","publishDate":"2026-09-16","updatedDate":"2026-10-10","tags":["reinforcement-learning","llm","unity","agents","ollama","python"],"translationSlug":"agentic-racing-un-piloto-que-nunca-aprendio-a-manejar","url":"https://alexisalulema.com/blog/agentic-racing-a-pilot-that-never-learned-to-drive/","series":null,"headings":[{"depth":2,"slug":"a-pilot-that-never-learned-to-drive","text":"A pilot that never learned to drive"},{"depth":2,"slug":"the-team-boss-that-does-reason","text":"The team boss that does reason"},{"depth":2,"slug":"why-the-strategist-prefers-to-defend","text":"Why the strategist prefers to defend"},{"depth":2,"slug":"a-second-fix-and-this-time-the-isolated-test-lies","text":"A second fix, and this time the isolated test lies"},{"depth":2,"slug":"what-im-taking-away","text":"What I’m taking away"}],"body":"I built a racing demo where identical cars compete on a closed circuit, each one with a\npilot that reacts frame by frame and a team boss — an LLM — that reasons per event and\nradios back strategy directives. Eight attempts at training the pilot with reinforcement\nlearning didn't work — the track was broken before the algorithm ever had a chance. The\nteam boss did work, and an experiment on how much it actually contributes ended up more\ninteresting than the demo itself: seven runs, two real and partially fixable reasoning\nbiases, an infrastructure bottleneck, and a lesson about trusting the full benchmark over\nthe quick desk-check.\n\nGame development has always caught my attention, Unity in particular and the whole\nindustry that moves around it. I never got the chance to break into that industry —\ndifferent paths, different contracts — but with today's coding assistants that becomes\nreachable in a way it simply wasn't a couple of years ago. I had Claude Code handle\nalmost the entire Unity side, which isn't my strength, and kept the design decisions and\nthe part I actually care about for myself: what happens when you give an agent real\nautonomy over another piece of the system.\n\nBecause the hook was never \"build a game.\" The actual idea comes from Formula 1, from\nwatching it as a kid without thinking about any of this yet: what caught my attention was\nnever the car or the driver, it was the team radio. The strategist on the pit wall sees\nthe whole race — standings, gaps, the timing board, the entire race plan — and sends the\ndriver a short, specific instruction: attack now, manage the tire, defend the position.\nThe driver sees none of that; he's doing 300 km/h reacting to whatever's ten meters\nahead, with no room to think about the broader strategy. Two tiers, two rhythms, a narrow\ncommand channel between something that reacts and something that reasons. I connected\nthat image to LLMs much later, thinking about how they reason with the whole context in\nview instead of reacting to one data point at a time: the pit-wall strategist already is,\nat bottom, exactly that — it sees the full picture and gives a high-level instruction,\nwithout ever touching the wheel itself.\n\nAnd lately I've been deep in agentic communication — MCP, A2A, and generally how far the\nidea of agents coordinating with each other can be pushed, including agents guiding\n\"dumb\" devices (I've got an ESP32 experiment in mind for later). The pit-wall pattern is\nexactly that: a narrow command channel between something simple and something that\nthinks. This demo is a rehearsal of that idea with a steering wheel and six cars, before\ntrying it with real hardware.\n\n## A pilot that never learned to drive\n\nThe initial plan was reasonable on paper: a short closed circuit — so a training episode\nis one lap, not the whole race — with raycasts, speed, and angle relative to the ideal\nracing line as observations, PPO as the algorithm, and a progress reward with a penalty\nfor going off-track or crashing.\n\nI ran eight full iterations, each 4 to 20 million steps, changing one variable each\nround: the reward shape (seven different versions), a reference heuristic I used purely\nfor diagnosis (eight versions), the turn radius, the wall geometry — I tried\nzero-thickness mesh, boxes, extruded solids — the spawn point, the lateral grip model.\nThe number I actually cared about, how much of a lap a car completed without going\noff-track, never moved off 10-13% across all eight.\n\nWhat changed the diagnosis wasn't touching the algorithm. It was the diagnostic heuristic:\non the same track and the same physics, its best attempt reached 82% of a lap at a sustained\n21 m/s. Same environment, a hand-written method could get that far and the other got stuck at\na tenth of that — at the time I read it as \"the environment is learnable, the problem is the\nRL\", and stopped thinking the problem was just the reward. But on average the heuristic also\ngot stuck often, and that half of the data turned out to be the important one: as I explain\nbelow, a good part of the problem was in the tracks.\n\nThe change that did move something was reshaping the speed reward to have a peak, not a\nslope. Before, more speed always scored more points — so braking for a corner was pure\ncost, never a benefit, and the agent learned exactly that: never brake. The reform\ncomputes a target speed from the upcoming curvature —\n\n$$\nv_{\\text{obj}} = v_{\\text{max}} \\cdot \\operatorname{lerp}\\!\\big(0.42,\\ 0.12,\\ \\operatorname{clamp}_{01}(\\theta / 55°)\\big)\n$$\n\nwhere $\\theta$ is the largest heading change on the track over the next ~55 meters —\nand rewards getting close to that target, not exceeding it:\n\n$$\nr_{\\text{vel}} = k \\cdot \\max\\!\\Big(0,\\ 1 - 1.3\\left|\\dfrac{v - v_{\\text{obj}}}{v_{\\text{obj}}}\\right|\\Big) \\cdot \\Delta t\n$$\n\nWith this, braking before a tight corner stops being pure cost — there's a zone where\nbraking is worth more than not braking. In the same run I also gave the\nagent lookahead (the same forward-curvature observations the heuristic used in\n$\\theta$). The reward went up by about 35% — a number that mixes both changes — but the\nlap percentage didn't move: the agent still never discovered the cornering maneuver —\nbrake, turn, accelerate, a coordinated one-to-two-second sequence. 97% of episodes ended\noff the track about 200 meters in, right at the first corner: the agent got there over and\nover, but almost never stumbled onto that sequence by chance. My reading was a hard\nexploration problem, not a badly designed reward; it turned out to be only part of the story.\n\nAll eight runs used PPO end to end; what changed between them was the reward, the\nagent's perception, or the physics — never the algorithm itself. Before the last one, I\nprepared imitation from the heuristic as a bridge — the demonstration recorder and a\nconfiguration mixing behavioral cloning with GAIL — but I removed it before launching: I\nwanted the final run to be clean PPO, without that scaffolding, to get a clear read on\nwhether the algorithm alone was enough. I never got to record the demonstrations, so\nimitation went untested; parts\n[3](/blog/can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch/) and\n[4](/blog/can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch/) of the RL\nseries explain what it would have done. I never got to try curriculum learning or a bigger network — they were noted\ndown as the next step if this didn't work, but the project got descoped before that\niteration.\n\nIn parallel, a second variable was contaminating the whole experiment without my\nknowing: the procedurally generated tracks had broken geometry in several stretches,\ncorners that honestly not even a human could have taken cleanly.\n\nWhen I switched to a simple fixed circuit — an oval with rounded corners, no procedural\ngeneration involved — the heuristic ran clean, without a single crash or lockup across\nnine cars at once, and completed the full lap once I gave it enough time budget. The\n\"paralysis\" that had dominated all eight RL attempts — cars braking mid-track and just\nsitting stuck there — was, in good part, a track-generator problem, not a fundamental\nflaw in the car's physical controller. I had been chasing a learning bug that was\nactually, at least in part, a geometry bug.\n\nWith that signal on the table, and the project's clock running, I decided to stop\nchasing RL. The demo is about the pilot-strategist loop, not about having the best\npossible racing pilot, and demonstrating that loop doesn't need a neural network driving\n— it needs a pilot whose behavior visibly changes with the directive it receives. A\nhand-written heuristic, with a later brake point at high aggression, a line bias toward\nattack or defense, and proximity tolerance scaled by accepted risk, satisfies that\nrequirement just as well — and it runs on the client without needing to export any model\nor pay per-car inference cost inside the browser.\n\nForcing the directive to different values on the fixed circuit, the same heuristic pilot\ncompleted the lap in 79 seconds in aggressive mode and 112 in conservative mode — a 40%\ndifference from changing just two parameters. The mechanism behind it is a single\nmapping, with nothing else scattered around the code: the directive's aggression (0 to\n1, a discrete value the strategist picks between low/medium/high) scales the earlier\ncorner's target speed directly —\n\n$$\n\\text{speedScale} = \\operatorname{lerp}(0.86,\\ 1.16,\\ \\text{aggression})\n$$\n\n— and the margin before braking grows just as linearly with it, from 0.3 to 2.2 m/s of\nallowed slack over the target. It's exactly the writable surface the strategist needed,\nand it arrived by a path that had nothing to do with the one I'd planned.\n\nThe RL attempt wasn't wasted time: it left behind a solid training pipeline and\nevaluation harness, and a lesson that stuck with me — if you're going to compare two\ncontrol methods against the same environment, first validate that the environment\nitself is navigable with a simple reference method, before spending compute chasing a\nsophisticated method's hyperparameters.\n\nA technical side note, because public information on this is scarce: running a neural\nnetwork inside a WebGL build, in the browser, with no GPU, using Unity's Inference\nEngine, worked without a hitch. A toy model loaded and ran on the CPU backend at 3.4 ms\ncold and 0.1 ms warm per inference — with six cars doing inference every frame that's\n0.6 ms/frame, a perfectly comfortable budget at 60 fps. If the RL had worked, this piece\nwouldn't have been the problem.\n\n## The team boss that does reason\n\nThe strategist is a local LLM — `llama3.2:3b`, served by a GPU-less Ollama sidecar, no\nexternal API — a deliberate decision: no per-token bill, no dependency on a provider.\nEach car has its own strategist, independent of the other five. It sees the full\nstandings, lap times, the circuit map with numbered corners, its own log of previous\nlaps — but nothing frame by frame, and nothing about what's about to happen in the next\nfew seconds. The response arrives asynchronously; the race never waits for it, and if\nit's late, it simply arrives late, just like the real pit wall.\n\nTo be able to say something more than \"it looks good,\" I set up a mixed-field\nexperiment: of six cars in the same race, three run the LLM strategist and three run a\nfixed heuristic directive, rotating the starting grid and which car gets which engine\nbetween races. There, the final position actually means something, because it's a\nhead-to-head under identical conditions — comparing an all-LLM race against an\nall-heuristic race measures nothing, because within a single race position is zero-sum\nand someone wins either way.\n\nI ran that comparison seven times, 18 races per run, and each one answered a different\nquestion.\n\n**First clue: there's an effect, and it's large.** The heuristic group finished at an\naverage position of 2 out of 6; the LLM group, at 5. Consistent, without exception,\nacross all six pilots in the population. Instrumenting decision by decision, not just\neach race's outcome, the cause showed up clearly: filtering to genuinely fresh responses\nfrom the model, it picked low aggression and low risk almost half the time, while the\nfixed heuristic never picks \"low\" on either of those two channels. The original prompt\ngave the model a fictitious excuse to go slow: it asked it to \"conserve\" with tire wear\nand fuel consumption in mind that, in this simulation, simply don't exist. A second\nfinding I didn't expect: only 42% of the LLM cars' decisions were fresh responses — the\nrest fell into a fallback mode from proxy saturation, not model error.\n\n**A corrected prompt, and the bias gives way.** I rewrote the system rules: dropped the\nwear excuse, added explicit thresholds tied to real telemetry — rival within 1.5\nseconds, attack; someone on top of you, defend; clear track, push — and a direct\nreminder that there was no mechanical cost to going fast. Measured over fresh decisions,\nlow-aggression/low-risk dropped from 47% to 0.3%, and time lost to the leader fell 60%.\nBut the final position barely moved, because the fraction of fresh decisions didn't\nimprove — it still mostly fell into the same fallback as before, from proxy saturation.\n\n**Loosen concurrency, and it gets worse.** With a single Ollama engine serving one\nrequest at a time by design, I raised the proxy's admitted-call limit from 1 to 3,\nwithout touching the engine. The result was worse: the fraction of fresh responses fell\nto under one in ten. Before, the fallback was an instant rejection when no slot was\nfree; now, letting more requests through toward an engine that was still single-threaded\nmade them wait in a real queue, some crossed the circuit breaker's latency threshold,\nand every trip shut the whole system down for 60 seconds. Loosening the front door\nwithout loosening the actual bottleneck gains nothing; it just changes the failure mode,\nand the new one is worse.\n\n**A second engine, on the wrong machine.** The correct lever, in theory, isn't a bigger\nnumber on the traffic light but a second, independent Ollama engine — real parallelism\ninstead of more queueing behind a single one. I built that piece: an optional\nOllama-only sidecar container, with turn-taking in the proxy that guarantees two\nconcurrent calls never end up tied to the same engine. I validated it in isolation —\nthree concurrent calls against two engines, exactly two got a turn, never both on the\nsame one — and then ran the full 18 races with the sidecar active on the same machine.\nThe result didn't change at all: my dev laptop has eight physical cores in total, and\nthe two Ollama containers compete for them alongside the browser rendering the circuit.\nA second local engine doesn't add real compute to the system — it adds demand on the\nsame fixed compute that was already there, the same underlying problem as the previous\nrun, just split across two processes instead of queued in one. The turn-taking\nmechanism was proven correct; the capacity gain wasn't yet, because that gain only\nexists if the second engine gets genuinely additional compute.\n\n**A second engine, on the right machine.** I had an M1 MacBook on the same network. I\npublished a multi-architecture variant of the sidecar image (same weights, running\nnative on `arm64`, not translated) and stood it up there — physical CPU, genuinely\nseparate from my dev machine. The change was immediate: the model took 7 seconds to warm\nup instead of 20, and the strategist's fresh-response fraction jumped from under one in\nten to **57%**, the largest sample of the whole experiment. The mechanism worked — it\njust needed real compute behind it, not a promise of compute.\n\nAnd yet the final position didn't improve. In fact, it was the worst of the last four\nruns.\n\n## Why the strategist prefers to defend\n\nWith this much freshness in the responses for the first time, I could actually compare\nwhat the LLM chooses against what the heuristic chooses *in the same kind of situation*.\nFiltering both down to the most common event (an incident — contact, a brush, a moment\nof vulnerability), the heuristic picks attack 60% of the time and defend 38%. The LLM,\nin the same kind of event, almost exactly inverts that ratio: defend 69%, attack 26%. It\nisn't an artifact of what triggers the call — it's a different read of the same\nsituation.\n\nBefore writing a hypothesis, I tested it live. I built a hand-crafted telemetry\nsnapshot: one rival four seconds ahead, stable, no threat; another rival eight hundred\nmilliseconds behind — as unambiguous an \"attack\" case as you can construct. Five times\nin a row, with the same prompt, the model answered \"defend\" all five. In one of the\nresponses it even acknowledged, in its own radio line, that the car ahead \"is stable\" —\nand still decided to protect itself from it instead of going after it.\n\nThe model isn't comparing two numbers and acting on the smaller one, which is literally\nwhat the prompt asks it to do. It's doing something closer to recognizing a pattern and\ncompleting it: the mere presence of a rival nearby, in any position, seems to trigger a\n\"caution\" frame ahead of an \"opportunity\" frame — the same kind of defensive bias we'd\nalready found (and fixed) at the aggression/risk level, resurfacing one level up, in the\nchoice of directive itself, a place the earlier fix never touched.\n\n## A second fix, and this time the isolated test lies\n\nI rewrote the prompt again: an explicit instruction against defaulting to defend, the\ncomparison between the two gaps expressed as a direct subtraction instead of two\nindependent rules, and a fix for a related bug I found along the way — `target_rival`\nsometimes pointed at the wrong car under \"defend\".\n\nI repeated the same controlled experiment. The most unambiguous case — the five\n\"defend\" in a row — still didn't budge, not once. Discouraging: it looked like I'd hit a\nreal limit of the model, not of the wording.\n\nI ran the 18 races anyway, because five samples from an artificial scenario aren't\nsufficient evidence for anything. And there, the result was different: the position gap\nshrank 23%, the time gap to the leader 36%, and the share of \"attack\" among fresh\ndecisions rose from a quarter to a third. A real improvement, measured over hundreds of\ndecisions across varied contexts — the same fix my desk test had declared a failure.\n\nThe explanation, in hindsight, is simple: the scenario I hand-built was almost\nadversarial — a single rival, a single axis of comparison, nothing else in the telemetry\n— exactly the kind of edge case where a small model trips up more often. The real\nvariety of a race — two rivals, notes from previous laps, a different event each time —\ngives the model more anchors than my desk test did. An isolated test with five samples\nand a model running with temperature measured one thing; a benchmark of hundreds of\ndecisions in real contexts measured another. When they disagree, believe the second one\n— it's the one that resembles the real problem.\n\nThe defend-bias didn't disappear entirely. There's still a real stylistic gap with the\nheuristic, even in the best measured case. But it moved, with data confirming it before\nand after, just like the original aggression bias — and that remaining residue, with no\ninfrastructure or prompt left to blame, is probably the honest ceiling of what a 3B\nmodel contributes as a strategist in this domain.\n\n## What I'm taking away\n\nThree ideas repeated more than I expected, and I suspect they'll keep repeating in\nwhatever I build next with agents.\n\nThe first: validate the environment with a simple method before spending compute on the\nsophisticated one. Most of the plateau across eight RL runs wasn't the algorithm's\nfault — it was a track that, at first, not even a diagnostic heuristic could take\ncleanly. Running that heuristic first would have saved me weeks.\n\nThe second: an aggregate result on a system with real compute constraints almost always\nmixes two different questions — does the agent reason well?, and did it get the chance\nto show it? — and separating them requires instrumenting the individual decision, not\njust looking at the final outcome. That's exactly the kind of thing I want settled\nbefore trying something similar with an agent guiding a real, \"dumb\" device — there, the\nquestion \"did it get the chance to respond in time?\" isn't an infrastructure detail,\nit's half the problem.\n\nThe third, and the one that surprised me most: an isolated test and a full benchmark can\npoint in opposite directions, especially with a system that has some randomness to it.\nThe first is good for diagnosing and building a quick hypothesis; the second is the one\nthat actually measures what matters. Mistaking one for the other — declaring a fix a\nfailure because five samples of an extreme case didn't change, without running the full\nexperiment — nearly made me throw out an adjustment that actually worked.\n\nYou can watch the race yourself at\n[alexisalulema.com/projects](https://alexisalulema.com/projects?demo=agentic-racing): the demo\nis provisioned when you ask for it, runs for a few minutes, and destroys itself.\n\nThe full code, the day-by-day devlog, and the seven raw datasets from this experiment\nare at [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing).\nWhat's next, if the ESP32 cooperates, has less steering wheel and more wiring."},{"slug":"agentic-racing-un-piloto-que-nunca-aprendio-a-manejar","lang":"es","title":"Agentic Racing: un piloto que nunca aprendió a manejar, un jefe de equipo que sí aprendió a razonar","description":"Construí un demo de carreras donde un piloto reactivo obedece a un jefe de equipo LLM que razona por evento. Ocho intentos de entrenar el piloto con aprendizaje por refuerzo no funcionaron —la pista estaba rota antes que el algoritmo—, y medir cuánto aporta el estratega terminó siendo el experimento más interesante: siete corridas, sesgos de razonamiento reales y parcialmente corregibles, y una lección sobre confiar en el benchmark completo antes que en el test rápido de escritorio.","publishDate":"2026-09-16","updatedDate":"2026-10-10","tags":["reinforcement-learning","llm","unity","agents","ollama","python"],"translationSlug":"agentic-racing-a-pilot-that-never-learned-to-drive","url":"https://alexisalulema.com/es/blog/agentic-racing-un-piloto-que-nunca-aprendio-a-manejar/","series":null,"headings":[{"depth":2,"slug":"un-piloto-que-nunca-aprendió-a-manejar","text":"Un piloto que nunca aprendió a manejar"},{"depth":2,"slug":"el-jefe-de-equipo-que-sí-razona","text":"El jefe de equipo que sí razona"},{"depth":2,"slug":"por-qué-el-estratega-prefiere-defender","text":"Por qué el estratega prefiere defender"},{"depth":2,"slug":"un-segundo-ajuste-y-esta-vez-el-test-aislado-miente","text":"Un segundo ajuste, y esta vez el test aislado miente"},{"depth":2,"slug":"lo-que-me-llevo","text":"Lo que me llevo"}],"body":"Construí un demo de carreras donde autos idénticos compiten en un circuito cerrado,\ncada uno con un piloto que reacciona cuadro a cuadro y un jefe de equipo en un LLM que\nrazona por evento y le manda directivas de estrategia. Ocho intentos de entrenar el\npiloto con aprendizaje por refuerzo no funcionaron — la pista estaba rota antes que el\nalgoritmo. El jefe de equipo sí funcionó, y un experimento sobre cuánto aporta terminó\nsiendo más interesante que el demo en sí: siete corridas, dos sesgos de razonamiento\nreales y parcialmente corregibles, un cuello de botella de infraestructura, y una\nlección sobre confiar más en el benchmark completo que en el test rápido de escritorio.\n\nSiempre me ha llamado la atención el desarrollo de videojuegos, en particular Unity y\ntoda la industria que se mueve alrededor de él. No tuve la oportunidad de entrar en esa\nindustria — otros caminos, otros contratos — pero con los asistentes de código actuales\neso se vuelve alcanzable de una forma que hace un par de años no lo era. Le pedí a\nClaude Code que se hiciera cargo de casi todo el lado de Unity, que no es mi fuerte, y\nme quedé yo con las decisiones de diseño y con la parte que sí me interesa de verdad:\nqué pasa cuando le das a un agente autonomía real sobre otra pieza del sistema.\n\nPorque el gancho no era \"hacer un juego\". La idea concreta viene de la Fórmula 1, de\nverla de chico sin pensar todavía en nada de esto: lo que más me llamaba la atención no\nera el auto ni el piloto, era la radio del equipo. El estratega en el muro de boxes ve\nla carrera completa — clasificación, gaps, la pizarra de tiempos, el plan de toda la\ncarrera — y le manda al piloto una instrucción corta y específica: ataca ahora, cuida el\nneumático, defiende la posición. El piloto no ve nada de eso; va a 300 km/h reaccionando\na lo que tiene diez metros adelante, sin margen para pensar en la estrategia general.\nDos niveles, dos ritmos, un canal angosto de comandos entre uno que reacciona y otro que\nrazona. Até esa imagen a los LLMs mucho después, pensando en cómo razonan con todo el\ncontexto a la vista en vez de reaccionar a un dato a la vez: el estratega de boxes ya es,\nen el fondo, exactamente eso — ve el cuadro completo y da una instrucción de alto nivel,\nsin tocar el volante él mismo.\n\nY últimamente ando metido en comunicación agentic — MCP, A2A, y en general qué tan lejos\nse puede llevar la idea de agentes que se coordinan entre sí, incluso agentes que guían\na dispositivos \"tontos\" (tengo en la cabeza un experimento con un ESP32 para más\nadelante). El patrón del muro de boxes es exactamente ese: un canal angosto de comandos\nentre algo simple y algo que piensa. Este demo es un ensayo de esa idea con un volante y\nseis autos, antes de intentarlo con hardware de verdad.\n\n## Un piloto que nunca aprendió a manejar\n\nEl plan inicial era razonable en el papel: un circuito cerrado corto —así un episodio de\nentrenamiento es una vuelta, no la carrera completa— con raycasts, velocidad y ángulo\nrespecto a la trazada ideal como observaciones, PPO como algoritmo, y una recompensa por\nprogreso con penalización por salirse o chocar.\n\nCorrí ocho iteraciones completas, cada una de 4 a 20 millones de pasos, tocando en cada\nvuelta una variable distinta: la forma de la recompensa (siete versiones distintas), una\nheurística de referencia que usaba solo para diagnosticar (ocho versiones), el radio de\nlas curvas, la geometría de los muros —probé malla en cero, cajas, sólido extruido—, el\npunto de spawn, el modelo de agarre lateral. El número que me importaba, cuánto de una\nvuelta completaba un auto sin salirse, no se movió de un 10-13% en ninguna de las ocho.\n\nLo que cambió el diagnóstico no fue tocar el algoritmo. Fue la heurística de\ndiagnóstico: en la misma pista y con la misma física, su mejor intento llegó al 82% de\nuna vuelta a 21 m/s sostenido. Mismo entorno, un método escrito a mano podía llegar ahí y\nel otro se quedaba estancado en un décimo de eso — en ese momento lo leí como \"el entorno\nse puede aprender, el problema es del RL\", y dejé de pensar que el problema era solo la\nrecompensa. Pero en promedio la heurística también se atascaba seguido, y esa mitad del\ndato resultó ser la importante: como cuento más abajo, buena parte del problema estaba en\nlas pistas.\n\nEl cambio que sí movió algo fue reformar la recompensa de velocidad para que tuviera un\npico, no una pendiente. Antes, más velocidad siempre sumaba puntos — así que frenar para\nuna curva era puro costo, nunca beneficio, y el agente aprendía exactamente eso: no\nfrenar nunca. La reforma calcula una velocidad objetivo según la curvatura que viene —\n\n$$\nv_{\\text{obj}} = v_{\\text{max}} \\cdot \\operatorname{lerp}\\!\\big(0.42,\\ 0.12,\\ \\operatorname{clamp}_{01}(\\theta / 55°)\\big)\n$$\n\ndonde $\\theta$ es el mayor cambio de rumbo de la pista en los próximos ~55 metros— y\npremia acercarse a ese objetivo, no superarlo:\n\n$$\nr_{\\text{vel}} = k \\cdot \\max\\!\\Big(0,\\ 1 - 1.3\\left|\\dfrac{v - v_{\\text{obj}}}{v_{\\text{obj}}}\\right|\\Big) \\cdot \\Delta t\n$$\n\nCon esto frenar antes de una curva cerrada deja de ser puro costo — hay una zona donde\nvale más frenar que no frenar. En la misma corrida le subí además\nanticipación al agente (las mismas observaciones de curvatura hacia adelante que usaba la\nheurística en $\\theta$). La recompensa subió cerca de un 35% —un número que mezcla los dos\ncambios—, pero el porcentaje de vuelta no se movió: el agente seguía sin descubrir la\nmaniobra de curva —frenar, girar, acelerar, una secuencia coordinada de uno o dos\nsegundos—. El 97% de los episodios terminaban fuera de la pista a unos 200 metros, justo\nen la primera curva: el agente llegaba ahí una y otra vez, pero casi nunca daba por azar\ncon esa secuencia. Mi lectura fue exploración difícil, no una recompensa mal diseñada; resultó\nser solo una parte de la historia.\n\nLas ocho corridas usaron PPO de punta a punta; lo que cambiaba entre una y otra era la\nrecompensa, la percepción del agente o la física, nunca el algoritmo en sí. Antes de la\núltima, preparé imitación desde la heurística como puente —el grabador de demostraciones y\nuna configuración que mezclaba behavioral cloning con GAIL—, pero la quité antes de lanzarla:\npreferí que la corrida final fuera PPO limpio, sin ese andamiaje, para tener una lectura clara\nde si el algoritmo por sí solo alcanzaba. Nunca llegué a grabar las demostraciones, así que la\nimitación quedó sin probar; las partes\n[3](/es/blog/podemos-ensenar-imitando-behavioral-cloning-explicado-desde-cero/) y\n[4](/es/blog/podemos-aprender-a-comportarnos-como-un-experto-gail-explicado-desde-cero/) de la\nserie sobre RL explican qué habría hecho. No llegué a probar curriculum learning ni una red más grande —\nquedaron anotados como el siguiente paso si esto no funcionaba, pero el proyecto se\ndescopeó antes de esa iteración.\n\nEn paralelo había una segunda variable contaminando todo el experimento sin que yo lo\nsupiera: las pistas generadas de forma procedural tenían geometría rota en varios\ntramos, curvas que honestamente ni una persona hubiera podido tomar bien.\n\nCuando cambié a un circuito fijo simple —un óvalo con esquinas redondeadas, sin\ngeneración procedural de por medio— la heurística corrió limpia, sin un solo choque ni\nbloqueo en nueve autos a la vez, y completó la vuelta entera una vez que le di margen de\ntiempo suficiente. La \"parálisis\" que había dominado los ocho\nintentos de RL —autos que se frenaban a media pista y se quedaban clavados ahí— era en\nbuena parte un problema del generador de pistas, no un fallo de fondo del controlador\nfísico del auto. Había estado cazando un bug de aprendizaje que en realidad era, al\nmenos en parte, un bug de geometría.\n\nCon esa señal sobre la mesa, y con el tiempo del proyecto corriendo, decidí no seguir\npersiguiendo RL. El demo es sobre el circuito piloto-estratega, no sobre tener el mejor\npiloto de carreras posible, y para demostrar ese circuito no hace falta una red neuronal\nmanejando — hace falta un piloto cuyo comportamiento cambie de forma observable según la\ndirectiva que reciba. Una heurística escrita a mano, con el freno más tardío cuando la\nagresividad es alta, un sesgo de línea según ataque o defensa, y tolerancia a proximidad\nsegún el riesgo aceptado, cumple ese requisito igual de bien — y corre en el cliente sin\nnecesitar exportar ningún modelo ni pagar el costo de inferencia por auto dentro del\nnavegador.\n\nForzando la directiva a distintos valores sobre el circuito fijo, el mismo piloto\nheurístico completó la vuelta en 79 segundos en modo agresivo y en 112 en modo\nconservador — un 40% de diferencia cambiando solo dos parámetros. El mecanismo detrás es\nun único mapeo, sin nada más disperso en el código: la agresividad de la directiva (0 a\n1, un valor discreto que el estratega elige entre bajo/medio/alto) escala la velocidad\nobjetivo de la curva anterior directamente —\n\n$$\n\\text{speedScale} = \\operatorname{lerp}(0.86,\\ 1.16,\\ \\text{aggression})\n$$\n\n— y el margen antes de frenar crece con ella igual de lineal, de 0.3 a 2.2 m/s de\nsobrante permitido sobre el objetivo. Es justo la superficie de escritura que necesitaba\nel estratega, y llegó por un camino que no tenía nada que ver con el que había planeado.\n\nEl intento de RL no fue tiempo tirado: dejó un pipeline de entrenamiento y un arnés de\nevaluación sólidos, y una lección que me quedó grabada — si vas a comparar dos métodos de\ncontrol contra el mismo entorno, valida primero que el entorno en sí sea navegable con\nun método simple de referencia, antes de gastar cómputo persiguiendo hiperparámetros de\nun método sofisticado.\n\nUna nota técnica al margen, porque la información pública sobre esto es escasa: correr\nuna red neuronal dentro de un build WebGL, en el navegador, sin GPU, con la Inference\nEngine de Unity, funcionó sin sobresaltos. Un modelo de juguete cargó y corrió con el\nbackend de CPU en 3.4 ms en frío y 0.1 ms en caliente por inferencia — con seis autos\nhaciendo inferencia por frame eso son 0.6 ms/frame, un presupuesto perfectamente cómodo\na 60 fps. Si el RL hubiera funcionado, esta pieza no habría sido el problema.\n\n## El jefe de equipo que sí razona\n\nEl estratega es un LLM local —`llama3.2:3b`, servido por un sidecar Ollama sin GPU, sin\nAPI externa— decisión deliberada: sin factura por token, sin depender de un proveedor.\nCada auto tiene su propio estratega, independiente de los otros cinco. Ve la\nclasificación completa, los tiempos de vuelta, el mapa del circuito con las curvas\nnumeradas, su propia bitácora de vueltas anteriores — pero nada cuadro a cuadro, y nada\nsobre lo que va a pasar en los próximos segundos. La respuesta llega de forma asíncrona;\nla carrera nunca la espera, y si se atrasa, simplemente llega tarde, como el muro de\nboxes real.\n\nPara poder decir algo más que \"se ve bien\", monté un experimento de campo mixto: de seis\nautos en la misma carrera, tres llevan estratega LLM y tres llevan una directiva\nheurística fija, rotando la parrilla de salida y qué auto lleva qué motor entre\ncarreras. Ahí la posición final sí significa algo, porque es un enfrentamiento directo\nbajo condiciones idénticas — comparar una carrera con todos los autos en modo LLM contra\notra con todos en modo heurístico no mide nada, porque dentro de una sola carrera la\nposición es de suma cero y alguien gana en ambos casos igual.\n\nCorrí esa comparación siete veces, 18 carreras por corrida, y cada una respondió una\npregunta distinta.\n\n**La primera pista: hay un efecto, y es grande.** El grupo heurístico terminaba en\npromedio en la posición 2 de 6; el grupo LLM, en la posición 5. Consistente, sin\nexcepción, en los seis pilotos de la población. Instrumentando decisión por decisión, no\nsolo el resultado de cada carrera, la causa apareció clara: filtrando a respuestas\nrealmente frescas del modelo, elegía baja agresividad y bajo riesgo casi la mitad de las\nveces, mientras que la heurística fija nunca elige \"bajo\" en ninguno de esos dos\ncanales. El prompt original le daba al modelo una excusa ficticia para ir lento: le\npedía \"conservar\" pensando en desgaste de neumáticos y combustible que, en esta\nsimulación, sencillamente no existen. Un segundo hallazgo que no esperaba: solo el 42%\nde las decisiones de los autos LLM eran respuestas frescas — el resto caía en un modo de\nrespaldo por saturación del proxy, no por error del modelo.\n\n**Un prompt corregido, y el sesgo cede.** Reescribí las reglas del sistema: eliminé la\nexcusa del desgaste, agregué umbrales explícitos atados a la telemetría real —rival a\nmenos de 1.5 segundos, ataca; alguien encima tuyo, defiende; pista despejada, empuja— y\nun recordatorio directo de que no había costo mecánico en ir rápido. Medido sobre\ndecisiones frescas, baja agresividad/bajo riesgo cayó del 47% al 0.3%, y el tiempo\nperdido contra el líder bajó un 60%. Pero la posición final apenas se movió, porque la\nfracción de decisiones frescas no mejoró — seguía cayendo casi todo en el mismo respaldo\nde antes por saturación del proxy.\n\n**Aflojar la concurrencia, y empeora.** Con un solo motor Ollama sirviendo una petición\na la vez por diseño, subí el límite de llamadas admitidas por el proxy de 1 a 3, sin\ntocar el motor. El resultado fue peor: la fracción de respuestas frescas cayó a menos de\nuna de cada diez. Antes, el respaldo era un rechazo instantáneo cuando no había turno\nlibre; ahora, dejar pasar más peticiones hacia un motor que seguía siendo de un solo\nhilo las hizo esperar en cola real, algunas superaron el umbral de latencia del\ncortacircuitos, y cada disparo apagaba el sistema entero 60 segundos. Aflojar la puerta\nde entrada sin aflojar el cuello de botella real no gana nada; solo cambia el tipo de\nfallo, y el nuevo es peor.\n\n**Un segundo motor, en la máquina equivocada.** La palanca correcta, en teoría, no es\nun número más grande en el semáforo sino un segundo motor Ollama independiente — real\nparalelismo en vez de más cola detrás de uno solo. Construí esa pieza: un container\nsidecar solo con Ollama, opcional, con un reparto de turnos en el proxy que garantiza\nque dos llamadas concurrentes nunca terminen atadas al mismo motor. Lo validé aislado —\ntres llamadas concurrentes contra dos motores, exactamente dos consiguieron turno, nunca\nlas dos en el mismo— y después corrí las 18 carreras completas con el sidecar activo en\nla misma máquina. El resultado no cambió en nada: mi laptop de desarrollo tiene ocho\nnúcleos físicos en total, y los dos containers Ollama compiten por ellos junto con el\nnavegador renderizando el circuito. Un segundo motor local no le suma cómputo real al\nsistema — le suma demanda sobre el mismo cómputo fijo que ya había, el mismo problema de\nfondo que la corrida anterior, solo que repartido entre dos procesos en vez de encolado\nen uno. El mecanismo de reparto quedó demostrado correcto; la ganancia de capacidad\ntodavía no, porque esa ganancia solo existe si el segundo motor recibe cómputo\ngenuinamente adicional.\n\n**Un segundo motor, en la máquina correcta.** Tenía una MacBook M1 en la misma red.\nPubliqué una variante multi-arquitectura de la imagen del sidecar (mismo peso, corriendo\nnativo en `arm64`, no traducido) y la levanté ahí — CPU física, separada de verdad de mi\nmáquina de desarrollo. El cambio fue inmediato: el modelo tardó 7 segundos en calentar en\nvez de 20, y la fracción de respuestas frescas del estratega saltó de menos de uno de\ncada diez a **57%**, la muestra más grande de todo el experimento. El mecanismo\nfuncionaba — solo necesitaba cómputo real detrás, no una promesa de cómputo.\n\nY sin embargo la posición final no mejoró. De hecho fue la peor de las últimas cuatro\ncorridas.\n\n## Por qué el estratega prefiere defender\n\nCon tanta frescura de respuesta por primera vez, pude comparar de verdad qué elige el\nLLM contra qué elige la heurística *en el mismo tipo de situación*. Filtrando ambas al\nevento más común (un incidente — contacto, roce, un momento de vulnerabilidad), la\nheurística elige atacar el 60% de las veces y defender el 38%. El LLM, en el\nmismo tipo de evento, invierte casi exactamente esa proporción: defender 69%, atacar\n26%. No es un artefacto de qué dispara la llamada — es una lectura distinta de la misma\nsituación.\n\nAntes de escribir una hipótesis, lo probé en vivo. Armé una telemetría de manual: un\nrival cuatro segundos por delante, estable, ninguna amenaza; otro rival ochocientos\nmilisegundos por detrás — un caso de \"ataca\" tan inequívoco como se puede construir.\nCinco veces seguidas, con el mismo prompt, el modelo respondió \"defend\" las cinco. En\nuna de las respuestas hasta reconoció, en su propia frase de radio, que el auto de\nadelante \"está estable\" — y aun así decidió protegerse de él en lugar de ir a buscarlo.\n\nEl modelo no está comparando dos números y actuando sobre el más chico, que es\nliteralmente lo que el prompt le pide. Está haciendo algo más parecido a reconocer un\npatrón y completarlo: la sola presencia de un rival cerca, en cualquier posición, parece\nactivar un marco de \"cuidado\" antes que un marco de \"oportunidad\" — el mismo tipo de\nsesgo hacia lo defensivo que ya habíamos encontrado (y arreglado) a nivel de\nagresividad/riesgo, reaparecido un nivel más arriba, en la elección de la directiva\nmisma, donde el ajuste anterior nunca lo tocó.\n\n## Un segundo ajuste, y esta vez el test aislado miente\n\nReescribí el prompt otra vez: una instrucción explícita contra defender por defecto, la\ncomparación entre los dos gaps expresada como una resta directa en vez de dos reglas\nindependientes, y una corrección de un bug relacionado que encontré de paso —\n`target_rival` a veces apuntaba al auto equivocado bajo \"defend\".\n\nRepetí el mismo experimento controlado. El caso más inequívoco —el de los cinco \"defend\"\nseguidos— seguía sin ceder ni una vez. Descorazonador: parecía que había tocado un límite\nreal del modelo, no de la redacción.\n\nCorrí las 18 carreras de todos modos, porque cinco muestras de un escenario artificial no\nson evidencia suficiente para nada. Y ahí el resultado fue otro: el gap de posición\nse achicó un 23%, el gap de tiempo contra el líder un 36%, y la proporción de \"attack\" en\ndecisiones frescas subió de un cuarto a un tercio. Una mejora real, medida sobre\ncientos de decisiones en contextos variados — el mismo ajuste que mi test de escritorio\nhabía declarado un fracaso.\n\nLa explicación, pensándolo después, es simple: el escenario que armé a mano era casi\nadversarial —un solo rival, un solo eje de comparación, sin nada más en la telemetría—\nexactamente el tipo de caso límite donde un modelo chico tropieza más seguido. La\nvariedad real de una carrera —dos rivales, notas de vueltas anteriores, un evento\ndistinto cada vez— le da al modelo más anclas de las que mi prueba de escritorio le\ndaba. Un test aislado con cinco muestras y un modelo con temperatura midió una cosa; un\nbenchmark de cientos de decisiones en contextos reales midió otra. Cuando difieren, hay\nque creerle al segundo — es el que se parece al problema real.\n\nEl sesgo hacia defender no desapareció del todo. Sigue habiendo una brecha real de\nestilo con la heurística, incluso en el mejor caso medido. Pero se movió, con datos que\nlo confirman antes y después, igual que el sesgo original de agresividad — y ese residuo\nque queda, ya sin infraestructura ni prompt a quién culpar, es probablemente el límite\nhonesto de lo que un modelo de 3B aporta como estratega en este dominio.\n\n## Lo que me llevo\n\nTres ideas se repitieron más de lo que esperaba, y sospecho que van a repetirse en\ncualquier próxima cosa que construya con agentes.\n\nLa primera: valida el entorno con un método simple antes de gastar cómputo en el método\nsofisticado. Buena parte del estancamiento de ocho corridas de RL no era del algoritmo —\nera una pista que al principio ni una heurística de diagnóstico podía tomar bien. Correr\nesa heurística primero me habría ahorrado semanas.\n\nLa segunda: un resultado agregado sobre un sistema con restricciones reales de cómputo\ncasi siempre mezcla dos preguntas distintas —¿el agente razona bien?, y ¿tuvo la\noportunidad de mostrarlo?— y separarlas exige instrumentar la decisión individual, no\nsolo mirar el resultado final. Es exactamente el tipo de cosa que quiero tener resuelta\nantes de intentar algo parecido con un agente guiando un dispositivo real y \"tonto\" —\nahí la pregunta de \"¿tuvo la oportunidad de responder a tiempo?\" no es un detalle de\ninfraestructura, es la mitad del problema.\n\nLa tercera, la que más me sorprendió: un test aislado y un benchmark completo pueden\napuntar en direcciones opuestas, sobre todo con un sistema que tiene algo de\naleatoriedad. El primero sirve para diagnosticar y para armar una hipótesis rápido; el\nsegundo es el que de verdad mide lo que importa. Confundir uno por el otro — declarar un\narreglo fallido porque cinco muestras de un caso extremo no cambiaron, sin correr el\nexperimento completo — casi me hace descartar un ajuste que sí funcionaba.\n\nPuedes ver la carrera tú mismo en\n[alexisalulema.com/projects](https://alexisalulema.com/es/projects?demo=agentic-racing): el demo\nse provisiona cuando lo pides, corre unos minutos y se destruye solo.\n\nEl código completo, la bitácora día a día y los siete datasets crudos de este experimento\nestán en [github.com/alulema/agentic-racing](https://github.com/alulema/agentic-racing).\nLo próximo, si el ESP32 coopera, tiene menos volante y más cables."},{"slug":"twenty-minutes-of-infrastructure-and-then-nothing-left","lang":"en","title":"Twenty Minutes of Infrastructure (and Then Nothing Left)","description":"The demo behind each project card on my site doesn't exist until you ask for it, and stops existing twenty minutes later. This is the machinery behind that button: an ephemeral environment per session, a subdomain pool, a gateway, a reaper that had to be rebuilt, and the bugs that only showed up in production.","publishDate":"2026-08-26","updatedDate":null,"tags":["infrastructure","azure","containers","github-actions","cloudflare","devops","iac"],"translationSlug":"veinte-minutos-de-infraestructura-y-despues-nada","url":"https://alexisalulema.com/blog/twenty-minutes-of-infrastructure-and-then-nothing-left/","series":null,"headings":[{"depth":2,"slug":"introduction","text":"Introduction"},{"depth":2,"slug":"the-problem-was-never-i-want-demos-on-my-site","text":"The problem was never “I want demos on my site”"},{"depth":2,"slug":"the-constraints-defined-the-design","text":"The constraints defined the design"},{"depth":2,"slug":"two-halves-the-switchboard-and-the-disposable-part","text":"Two halves: the switchboard and the disposable part"},{"depth":3,"slug":"when-a-field-changes-meaning-every-reader-is-a-bug","text":"When a field changes meaning, every reader is a bug"},{"depth":2,"slug":"why-a-human-approves-and-the-bug-i-never-saw-coming","text":"Why a human approves (and the bug I never saw coming)"},{"depth":2,"slug":"the-reaper-or-best-effort-is-not-a-guarantee","text":"The reaper, or: best-effort is not a guarantee"},{"depth":3,"slug":"the-bug-that-destroyed-demos-before-anyone-opened-them","text":"The bug that destroyed demos before anyone opened them"},{"depth":2,"slug":"declarative-infrastructure-does-not-ignore-what-you-dont-mention","text":"Declarative infrastructure does not ignore what you don’t mention"},{"depth":2,"slug":"it-starts-it-doesnt-finish-it-dies","text":"”It starts, it doesn’t finish, it dies”"},{"depth":2,"slug":"what-i-deliberately-didnt-do","text":"What I deliberately didn’t do"},{"depth":2,"slug":"conclusion","text":"Conclusion"}],"body":"## Introduction\n\nEvery project card on my site carries a button that says **Request access**. It doesn't link to a\nrunning demo, because at the moment you click it the demo does not exist. There is no server\nwaiting, no container idling, no URL that resolves to anything. What exists is a promise: give me a\nfew minutes and there will be one — with its own subdomain, its own certificate, its own isolated\nenvironment — and twenty minutes later there will be nothing again.\n\n[The previous post](/blog/a-rag-chatbot-over-my-own-blog-and-the-comeback-after-three-years/) was\nabout what runs *inside* one of those environments: a RAG chatbot over this blog's posts. This one\nis about the machinery that brings a demo into existence on request, routes traffic to it, and then\ndeletes it — the same machinery that will serve the next demo, which I'm already building.\n\nThere's also a rule I set for myself, and it's the reason this post is dated now and not three\nmonths ago: **I wasn't going to write about this infrastructure until a real demo, requested by a\nreal person through the real flow, had provisioned, served, and destroyed itself.** Infrastructure\nthat has never carried a real payload isn't infrastructure — it's a diagram. The diagram was\nfinished in June. The first successful provision took considerably longer.\n\n## The problem was never \"I want demos on my site\"\n\nSay \"I want live demos\" out loud and the solution seems obvious: deploy them, leave them up, link\nthem from the project cards. Which, for a portfolio of *N* projects, means *N* services running\npermanently so that — statistically — nobody is looking at them.\n\nIt isn't only the bill; it's that the shape is wrong. A demo is something a person looks at for a\nfew minutes and then leaves. And it compounds: every demo you add is one more permanently running\nthing to patch, monitor, and pay for — so the rational response to that design is to **stop adding\ndemos**, exactly backwards from what I wanted.\n\nSo the actual goal was never demos. It was this: **resources should exist while a human is looking\nat them, and not one minute longer.** The demos are the visible consequence; the rule is the\nproject. With one corollary that ended up shaping every decision: if nothing may stay parked, then\n**teardown has to be a guarantee, not an intention.** Anything I *could* forget to clean up, I will\neventually forget to clean up — on a Tuesday, three months from now, while thinking about something\nelse.\n\n## The constraints defined the design\n\nSame as the last project, I wrote the rules before the first line of anything:\n\n- **Nothing parked.** No per-demo compute alive between sessions. Not \"scaled down\" — *gone*.\n- **Teardown is deletion, not cleanup.** The whole demo lives inside a disposable container that I\n  destroy in one call. I don't tidy up; I throw away the box.\n- **Whatever must persist has to cost ~$0 idle.** A small permanent core is acceptable; a permanent\n  *bill* is not.\n- **Bounded sessions, several independent ways.** A hard cap, an idle timeout, and a kill switch —\n  because any single limit is one bug away from being no limit.\n\nEverything else fell out of those:\n\n| Piece | Choice |\n|---|---|\n| Per-session environment | An **ephemeral resource group** (`rg-demo-<slot>`), created and deleted per demo |\n| Orchestrator | **GitHub Actions** with OIDC (provision / teardown / reap workflows) |\n| Front door | A persistent **gateway** container: TLS, session auth, slot → environment routing |\n| Addressing | A pool of **one-level subdomains**, `demo01…demo10.alexisalulema.com` |\n| State | **Table Storage**: one row per slot |\n| Punctuality | A **Cloudflare cron Worker**, once a minute (the second attempt — more on this below) |\n\n## Two halves: the switchboard and the disposable part\n\nThe whole design is a split between what has to survive and what must not.\n\nThe **permanent half** is deliberately tiny: a gateway (a reverse proxy that terminates TLS,\nvalidates the session, and routes a slot to its environment), the shared container environment that\nhosts it, and a table holding the pool — ten rows, `demo01` to `demo10`, each one either free or\noccupied. The gateway scales to zero, so when nobody is connected the permanent half is a handful of\ntable rows and a sleeping container.\n\nThe **disposable half** is everything else. Each session gets its own resource group containing the\nentire demo, and teardown is a single delete of that group. That's the part I actually cared about:\nI don't clean up resources, I delete the container they live in. There's no orphaned disk, no\nlingering app definition, no configuration drift, and — crucially — no partial-cleanup path where\nsomething survives because a step failed halfway through.\n\n```\n                       Persistent (≈$0 idle)\n   ┌───────────────────────────────────────────────────┐\n   │  Gateway (TLS · session JWT · slot→env routing)   │\n   │  Pool state  ·  reaper cron (1/min)               │\n   └───────────────────────────────────────────────────┘\n                            │ routes demoNN → environment\n        ┌───────────────────┼───────────────────┐\n        ▼                   ▼                   ▼\n   ┌─────────┐         ┌─────────┐         ┌─────────┐\n   │rg-demo- │         │rg-demo- │         │ demo03  │   Ephemeral\n   │ demo01  │         │ demo02  │         │  free   │   (created and deleted\n   │ (RAG)   │         │ (other) │         │         │    per session)\n   └─────────┘         └─────────┘         └─────────┘\n```\n\nScale-to-zero would have parked the *definition*: the app still exists, its configuration still\nexists, and it is still a thing I own and have to think about. Deleting the resource group is\ncategorical in a way a scaling policy never is.\n\nTwo details worth recording. Demos that hold no per-user state can be marked shareable, so a second\nvisitor attaches to the environment that's already running instead of waiting for a provision;\nstateful ones like the chatbot always get their own. And each demo answers at\n`demoNN.alexisalulema.com` — **one level** of subdomain, which turned out to be architectural rather\nthan cosmetic: a wildcard certificate covers `demo01` for free, while a tidier\n`demo01.demos.alexisalulema.com` would have required a paid certificate tier, forever.\n\n### When a field changes meaning, every reader is a bug\n\nAdding those shared environments meant changing what a slot's `status` value means, and moving the\nexpiry to a different place. Clean change, two files, one afternoon.\n\nThe gateway — a **different service, in a different container, written in a different language** —\nwas still routing only when it saw the old value, and still reading an expiry field that had moved.\n**Every demo, without exception, would have returned 410 Gone.** I caught it before it shipped, but\nonly because something made me go re-read the gateway.\n\n**Changing the meaning of a state field is a breaking API change to everyone who reads it** — and\n\"everyone\" includes readers that don't live in the code you're editing, in languages your compiler\nisn't looking at. It's the strongest argument for *renaming* a field whose semantics changed: a\nrename turns a silent behavior change into a loud failure in every consumer at once.\n\n## Why a human approves (and the bug I never saw coming)\n\nAn endpoint on the public internet that provisions cloud resources on request is a bill waiting to\nbe written by a stranger. So there's a person in the loop: a request creates a ticket and sends me\nan email with an **Approve** button, and only that click starts a provision.\n\nWhich produced the single funniest bug of the project. **Email scanners click your links.** Gmail —\nand every corporate mail-security product — prefetches URLs inside messages to check them for\nmalware. My approval link was a plain `GET`. So demos were being provisioned, faithfully and\nautomatically, by the security scanner whose job was to verify that the email about provisioning\ndemos was safe to open.\n\nThe fix is now permanent policy in this codebase: the link's `GET` renders a confirmation page that\ndoes nothing at all, and the real approval is the `POST` behind that page's button. \"Never put a\nstate-changing action behind a `GET`\" is a rule I already knew in the abstract sense. Now I know it\nin the *check-the-resource-group-list* sense, which turns out to be a different kind of knowing.\n\n## The reaper, or: best-effort is not a guarantee\n\nThe first design for teardown was the obvious one: a scheduled workflow, every five minutes,\nsweeping for expired demos. Free, simple, already had the credentials it needed. I shipped it and\nmoved on.\n\nThen a demo was still alive at **25 minutes**, against a 20-minute cap.\n\nThe scheduler was the problem. Scheduled workflows on GitHub Actions are **throttled** — a `*/5`\ncron that I had read as \"every five minutes\" was, in the run history, firing **every one to two\nhours**. This is documented behavior: schedules are best-effort. And \"best-effort\" in a docs page\nmeans \"not your reaper\" in production.\n\nWhat made it genuinely dangerous is the *shape* of the failure. Access was being cut **perfectly**:\nthe session token expires at twenty minutes, the gateway stops routing, and the visitor experiences\na demo that ended exactly on schedule. Nothing looked broken from any angle a user or a health check\ncould see — while the resource group kept running, with a replica floor above zero, until a\nthrottled cron eventually got around to noticing. The user-visible layer was flawless and the actual\nconsumption was roughly **six times** what I'd designed for. That's the worst class of\ninfrastructure bug there is: **the one whose only symptom is money.**\n\nThe redesign replaced the scheduler with a **Cloudflare cron Worker** that fires once a minute and\ndoes exactly one thing: `POST /api/demo/reap`. The backend sweeps what it was always going to sweep\n— expired sessions, empty environments, idle environments — and dispatches a teardown per slot.\nTeardown latency went from \"somewhere between five minutes and two hours\" to about a minute, and the\nWorker costs nothing.\n\n**A timer you don't control is not a timer, it's a hint.** If being late has a cost, punctuality has\nto be somebody's product guarantee, not a best-effort convenience bundled with something else you\nwere already using.\n\nThat redesign also brought the idle timeout, which has one subtlety I like. Only the gateway can see\ndemo traffic — everything goes through it, already authenticated — so it stamps a `lastActivity`\ntimestamp. But **only client-initiated activity counts.** A request, a socket opening, a message\nfrom the client. Data the server pushes *out* does not. Get that backwards and a streaming demo\nkeeps itself alive forever on behalf of a visitor who closed the tab an hour ago, which is the\nprecise failure mode the idle timeout exists to prevent.\n\n### The bug that destroyed demos before anyone opened them\n\nReleasing a slot reset its fields — but not `lastActivity`. So a **new** demo claiming that recycled\nslot inherited a timestamp from the previous tenant, potentially hours old. The reaper did its\narithmetic, correctly concluded that this demo had been idle far past the limit, and deleted it —\n**in under two minutes, before the visitor had ever opened it.** From the outside: an approval email\narrives, you click the link, and it tells you the demo isn't active. The resource group is already\ngone.\n\nEvery individual piece was right. The reaper's math, the stamping, the release — each correct about\neverything it explicitly handled. The bug lived in the space between them: state that was correct\nwhen it was written and stale by the time it was read.\n\n**When you promote something to persistent state, the write is the easy part — audit the whole\nlifecycle.** Claim, release, reuse. A recycled slot carries the previous tenant's belongings forward\nunless you deliberately empty it.\n\nAnd it was found by a human noticing that resource groups were vanishing suspiciously fast, not by a\ntest. The suite was green throughout — as it was, in the previous post, while the chatbot greeted\npeople in the wrong language. I'm starting to think that's less of a coincidence and more of a\npattern.\n\n## Declarative infrastructure does not ignore what you don't mention\n\nOne afternoon I redeployed the shared template to add a couple of environment variables to the\ngateway. Minutes later, every demo subdomain was failing with a **525 SSL handshake** error at the\nedge.\n\nThe custom-domain bindings — the hostnames attached to the gateway with their certificate — are\ncreated with CLI commands, outside the template, so the template doesn't list them. And a\ndeclarative template does not read \"not mentioned\" as *leave that alone*; it reads it as **\"this\nproperty should be empty.\"** The redeploy dutifully erased every hostname binding. I had built a\ncomfortable mental model where the template owned some properties and my CLI commands owned others\nand we all got along. The template did not share this model.\n\n**When a resource is co-owned by a declarative template and imperative commands, the declarative one\nwins every single deploy.** Either bring the property into the template, or accept a documented\nritual around every deploy — mine is that environment-variable changes go through a targeted update\nthat doesn't touch ingress, and any real redeploy is followed by re-binding the hostnames. What does\nnot work is expecting the two of them to politely divide the property space between them.\n\n## \"It starts, it doesn't finish, it dies\"\n\nFor weeks the infrastructure had been provisioning my two throwaway test payloads without complaint.\nThen I plugged in the RAG chatbot: the first demo with three containers in one pod, a resource split\nbetween them, and a startup probe.\n\nIt failed instantly. Every time. And because the provisioning workflow deletes the resource group\nwhen a deployment fails — which is correct, and is the whole \"nothing parked\" rule doing its job —\nthe evidence deleted itself along with it. My note from that day reads: *it starts, it doesn't\nfinish, it dies.*\n\nTwo rejections, in a chain, each one uncovered only after fixing the previous:\n\n| Rejection | Reality |\n|---|---|\n| Startup probe `failureThreshold: 40` | The platform caps it at **30** |\n| Invalid total resources | I'd assumed 4 vCPU / 8 GiB was available; the real ceiling for this environment type is **2 vCPU / 4 GiB** |\n\nWhat made them diagnosable at all was a differential: the two simple demos provision fine, this one\ndoesn't — *what does it have that they don't?* A probe. Then, after that, a multi-container resource\nsplit. Keeping a boring known-good payload sitting next to the interesting one turned out to be\nworth more than any amount of log reading.\n\nAnd the detail that generalizes: **the template compiler validates none of this.** Building the\ntemplate checks it against a schema, and the schema is perfectly happy with a threshold of 40 and\nwith 4 vCPU. Those are *runtime* limits, enforced by the platform's preflight validation when you\ndeploy. **\"It compiles\" and \"it deploys\" are different claims, and only one of them means\nanything.**\n\nThat second row is my favorite link between these two posts. The 2 vCPU / 4 GiB ceiling is the exact\nconstraint the previous post spends its entire performance section fighting: the half-billion\nparameter model, the top-3 retrieval, the 400-token chunks, the whole prompt-as-budget discipline.\nEvery one of those decisions traces back to an error message I got *here*, in the infrastructure.\nThe platform didn't merely host the demo. **It designed it.**\n\n## What I deliberately didn't do\n\n- **No cluster.** A cluster is a parked resource with a management story attached, which is the\n  precise thing this project exists to avoid.\n- **No authentication inside each demo.** The gateway owns it, once. A new demo gets session auth,\n  TLS, and expiry enforcement *by existing* — it doesn't implement anything.\n- **No pinned image digests.** Each provision pulls the current image, which is how publishing a\n  blog post refreshes the chatbot's corpus without anything in this system being aware it happened.\n- **No shared state between a demo and the platform.** A demo never learns that it's ephemeral, and\n  can be killed mid-request without ceremony. That's what makes teardown a `delete` instead of a\n  negotiation.\n\n## Conclusion\n\nAt rest, all of this costs effectively nothing: the gateway sleeps at zero replicas, the pool is ten\nrows in a table, the reaper Worker is free. When someone presses the button, it costs one pod for as\nlong as a human is actually watching it.\n\nBut the number I like most is the marginal one. **Adding the next demo costs zero permanent\nresources.** A new project registers itself in a few places, brings a template describing its own\ncontainers, and inherits the pool, the gateway, the session auth, the idle detection, and the reaper\nwithout implementing any of them. The cost of the next demo isn't \"one more service running forever\"\n— it's a few minutes of compute on the day somebody actually wants to see it. That's the difference\nbetween a portfolio that discourages me from adding to it and one that doesn't.\n\nI said at the start that I wouldn't publish this until a real demo had provisioned for a real\nperson. It has — and what that proved wasn't that the diagram was right. The diagram had been right\nsince June; it's the easy part, a couple of boxes and an arrow that says \"delete the resource\ngroup.\" What the first real provision proved was everything the diagram doesn't say. Every problem\nin this post lived in a gap between two things that were each individually correct: a state model\nand a reader in another container, a cron expression and its scheduler's actual promises, a\ntemplate's ownership and a CLI's, a released slot and a claimed one.\n\nThey're all the same failure, wearing different clothes: **I verified the pieces and assumed the\nseams.** Which is, I suspect, mostly what distributed systems are — seams, with pieces attached.\n\nThe next demo will be cheaper than this one was, because the expensive part is already paid for.\nThat's the real return on all of it: not that the chatbot has somewhere to run, but that whatever I\nbuild next already does, and it will cost nothing at all until the moment someone is curious enough\nto press a button.\n\nWhich you can do, right now, over at\n[alexisalulema.com/projects](https://alexisalulema.com/projects). Somewhere a resource group is\nbeing created with your session's name on it, and in twenty minutes there won't be a trace that it\never existed."},{"slug":"veinte-minutos-de-infraestructura-y-despues-nada","lang":"es","title":"Veinte minutos de infraestructura (y después, nada)","description":"El demo detrás de cada tarjeta de proyecto de mi sitio no existe hasta que lo pides, y deja de existir veinte minutos después. Esta es la maquinaria detrás de ese botón: un entorno efímero por sesión, un pool de subdominios, un gateway, un reaper que hubo que rehacer, y los bugs que solo aparecieron en producción.","publishDate":"2026-08-26","updatedDate":null,"tags":["infrastructure","azure","containers","github-actions","cloudflare","devops","iac"],"translationSlug":"twenty-minutes-of-infrastructure-and-then-nothing-left","url":"https://alexisalulema.com/es/blog/veinte-minutos-de-infraestructura-y-despues-nada/","series":null,"headings":[{"depth":2,"slug":"introducción","text":"Introducción"},{"depth":2,"slug":"el-problema-nunca-fue-quiero-demos-en-mi-sitio","text":"El problema nunca fue “quiero demos en mi sitio”"},{"depth":2,"slug":"las-restricciones-definieron-el-diseño","text":"Las restricciones definieron el diseño"},{"depth":2,"slug":"dos-mitades-la-centralita-y-lo-desechable","text":"Dos mitades: la centralita y lo desechable"},{"depth":3,"slug":"cuando-un-campo-cambia-de-significado-cada-lector-es-un-bug","text":"Cuando un campo cambia de significado, cada lector es un bug"},{"depth":2,"slug":"por-qué-aprueba-un-humano-y-el-bug-que-no-vi-venir","text":"Por qué aprueba un humano (y el bug que no vi venir)"},{"depth":2,"slug":"el-reaper-o-best-effort-no-es-una-garantía","text":"El reaper, o: best-effort no es una garantía"},{"depth":3,"slug":"el-bug-que-destruía-demos-antes-de-que-nadie-los-abriera","text":"El bug que destruía demos antes de que nadie los abriera"},{"depth":2,"slug":"la-infraestructura-declarativa-no-ignora-lo-que-no-mencionas","text":"La infraestructura declarativa no ignora lo que no mencionas"},{"depth":2,"slug":"empieza-no-termina-muere","text":"”Empieza, no termina, muere”"},{"depth":2,"slug":"lo-que-deliberadamente-no-hice","text":"Lo que deliberadamente no hice"},{"depth":2,"slug":"conclusión","text":"Conclusión"}],"body":"## Introducción\n\nLa tarjeta de cada proyecto de mi sitio lleva un botón que dice **Solicitar acceso**. No enlaza a\nningún demo corriendo, porque en el momento en que lo pulsas el demo no existe. No hay un servidor\nesperando, ni un contenedor ocioso, ni una URL que resuelva a nada. Lo que existe es una promesa:\ndame unos minutos y habrá uno — con su propio subdominio, su propio certificado, su propio entorno\naislado — y veinte minutos más tarde no habrá nada otra vez.\n\n[El post anterior](/es/blog/chatbot-rag-sobre-mi-propio-blog-el-regreso-despues-de-tres-anos/) trató\nsobre lo que corre *dentro* de uno de esos entornos: un chatbot RAG sobre los posts de este blog.\nEste trata sobre la maquinaria que trae un demo a la existencia cuando alguien lo pide, le rutea el\ntráfico y después lo borra — la misma maquinaria que va a servir al siguiente demo, que ya estoy\npreparando.\n\nHay también una regla que me puse, y es la razón de que este post esté fechado ahora y no hace tres\nmeses: **no iba a escribir sobre esta infraestructura hasta que un demo real, pedido por una persona\nreal a través del flujo real, se hubiera provisionado, servido y destruido solo.** Una\ninfraestructura que nunca cargó algo de verdad no es infraestructura: es un diagrama. El diagrama\nestuvo listo en junio. La primera provisión exitosa tardó bastante más.\n\n## El problema nunca fue \"quiero demos en mi sitio\"\n\nDices \"quiero demos en vivo\" y la solución parece obvia: despliégalos, déjalos arriba, enlázalos\ndesde las tarjetas. Lo cual, para un portafolio de *N* proyectos, significa *N* servicios corriendo\npermanentemente para que —estadísticamente— nadie los esté mirando.\n\nY no es solo la factura: es que la forma está mal. Un demo es algo que alguien mira unos minutos y\nluego abandona. Y se acumula: cada demo que agregas es una cosa más corriendo para siempre que hay\nque parchar, monitorear y pagar — así que la respuesta racional a ese diseño es **dejar de agregar\ndemos**, exactamente lo contrario de lo que yo quería.\n\nAsí que el objetivo real nunca fueron los demos. Era esto: **los recursos deben existir mientras un\nhumano los está mirando, y ni un minuto más.** Los demos son la consecuencia visible; la regla es el\nproyecto. Con un corolario que terminó moldeando cada decisión: si nada puede quedar estacionado,\nentonces **el teardown tiene que ser una garantía, no una intención.** Todo lo que *podría* olvidarme\nde limpiar, eventualmente me lo voy a olvidar de limpiar — un martes, dentro de tres meses, pensando\nen otra cosa.\n\n## Las restricciones definieron el diseño\n\nIgual que en el proyecto anterior, escribí las reglas antes de la primera línea de nada:\n\n- **Nada estacionado.** Cero cómputo por demo entre sesiones. No \"escalado a cero\": *borrado*.\n- **El teardown es borrar, no limpiar.** El demo entero vive dentro de un contenedor desechable que\n  destruyo con una sola llamada. No ordeno: tiro la caja.\n- **Lo que deba persistir tiene que costar ~$0 ocioso.** Un núcleo permanente pequeño es aceptable;\n  una *factura* permanente no.\n- **Sesiones acotadas, por varias vías independientes.** Un tope duro, un timeout por inactividad y\n  un kill switch — porque cualquier límite único está a un bug de distancia de no ser ningún límite.\n\nDe ahí salió el resto:\n\n| Pieza | Elección |\n|---|---|\n| Entorno por sesión | Un **grupo de recursos efímero** (`rg-demo-<slot>`), creado y borrado por demo |\n| Orquestador | **GitHub Actions** con OIDC (workflows de provisión / teardown / reap) |\n| Puerta de entrada | Un **gateway** persistente: TLS, auth de sesión, ruteo slot → entorno |\n| Direccionamiento | Un pool de **subdominios de un nivel**, `demo01…demo10.alexisalulema.com` |\n| Estado | **Table Storage**: una fila por slot |\n| Puntualidad | Un **cron Worker de Cloudflare**, cada minuto (el segundo intento — ya llegaremos) |\n\n## Dos mitades: la centralita y lo desechable\n\nTodo el diseño es una separación entre lo que tiene que sobrevivir y lo que no debe hacerlo.\n\nLa **mitad permanente** es deliberadamente diminuta: un gateway (un reverse proxy que termina TLS,\nvalida la sesión y rutea un slot a su entorno), el entorno compartido que lo aloja, y una tabla con\nel pool — diez filas, de `demo01` a `demo10`, cada una libre u ocupada. El gateway escala a cero, así\nque cuando nadie está conectado la mitad permanente son unas filas en una tabla y un contenedor\ndormido.\n\nLa **mitad desechable** es todo lo demás. Cada sesión recibe su propio grupo de recursos con el demo\ncompleto adentro, y el teardown es un solo borrado de ese grupo. Esa es la parte que de verdad me\nimportaba: no limpio recursos, borro el contenedor donde viven. No hay disco huérfano, ni definición\nde app olvidada, ni deriva de configuración, ni —lo más importante— un camino de limpieza parcial\ndonde algo sobrevive porque un paso falló a mitad.\n\n```\n                      Persistente (≈$0 ocioso)\n   ┌───────────────────────────────────────────────────┐\n   │  Gateway (TLS · JWT de sesión · ruteo slot→env)   │\n   │  Estado del pool  ·  cron del reaper (1/min)      │\n   └───────────────────────────────────────────────────┘\n                            │ rutea demoNN → entorno\n        ┌───────────────────┼───────────────────┐\n        ▼                   ▼                   ▼\n   ┌─────────┐         ┌─────────┐         ┌─────────┐\n   │rg-demo- │         │rg-demo- │         │ demo03  │   Efímeros\n   │ demo01  │         │ demo02  │         │  libre  │   (creados y borrados\n   │ (RAG)   │         │ (otro)  │         │         │    por sesión)\n   └─────────┘         └─────────┘         └─────────┘\n```\n\nEscalar a cero habría estacionado la *definición*: la app sigue existiendo, su configuración sigue\nexistiendo, y sigue siendo algo que poseo y en lo que tengo que pensar. Borrar el grupo de recursos\nes categórico de una forma en que una política de escalado nunca lo es.\n\nDos detalles que vale la pena dejar anotados. Los demos que no guardan estado por usuario pueden\nmarcarse como compartibles, y entonces un segundo visitante se engancha al entorno que ya está\ncorriendo en vez de esperar una provisión; los que sí tienen estado, como el chatbot, siempre\nreciben el suyo. Y cada demo responde en `demoNN.alexisalulema.com` — **un solo nivel** de\nsubdominio, que resultó ser arquitectónico y no cosmético: un certificado wildcard cubre `demo01`\ngratis, mientras que un más prolijo `demo01.demos.alexisalulema.com` habría exigido un plan de\ncertificado de pago, para siempre.\n\n### Cuando un campo cambia de significado, cada lector es un bug\n\nAgregar esos entornos compartidos implicó cambiar lo que significa el valor de `status` de un slot, y\nmover la expiración a otro lado. Cambio limpio, dos archivos, una tarde.\n\nEl gateway —un **servicio distinto, en otro contenedor, escrito en otro lenguaje**— seguía ruteando\nsolo cuando veía el valor viejo, y seguía leyendo un campo de expiración que se había mudado.\n**Todos los demos, sin excepción, habrían devuelto 410 Gone.** Lo cacé antes de que saliera, pero\nsolo porque algo me hizo ir a releer el gateway.\n\n**Cambiar el significado de un campo de estado es un cambio incompatible de API para todos sus\nlectores** — y \"todos\" incluye lectores que no viven en el código que estás editando, en lenguajes\nque tu compilador ni está mirando. Es el mejor argumento para *renombrar* un campo cuya semántica\ncambió: un rename convierte un cambio silencioso de comportamiento en una falla ruidosa en todos los\nconsumidores a la vez.\n\n## Por qué aprueba un humano (y el bug que no vi venir)\n\nUn endpoint en internet público que provisiona recursos en la nube a pedido es una factura esperando\na que la escriba un desconocido. Así que hay una persona en el medio: una solicitud crea un ticket y\nme manda un correo con un botón **Aprobar**, y solo ese click dispara una provisión.\n\nLo cual produjo el bug más gracioso del proyecto. **Los escáneres de correo hacen click en tus\nenlaces.** Gmail —y cualquier producto corporativo de seguridad de correo— hace *prefetch* de las\nURLs de un mensaje para revisar si son maliciosas. Mi enlace de aprobación era un `GET` común. Así\nque los demos se estaban provisionando, fiel y automáticamente, gracias al escáner de seguridad cuyo\ntrabajo era verificar que el correo sobre provisionar demos fuera seguro de abrir.\n\nEl arreglo es hoy política permanente en este código: el `GET` del enlace muestra una página de\nconfirmación que no hace absolutamente nada, y la aprobación real es el `POST` detrás del botón de\nesa página. \"Nunca pongas una acción que cambia estado detrás de un `GET`\" es una regla que ya sabía\nen abstracto. Ahora la sé en el sentido *revisa-la-lista-de-grupos-de-recursos*, que resulta ser un\ntipo de saber distinto.\n\n## El reaper, o: *best-effort* no es una garantía\n\nEl primer diseño del teardown fue el obvio: un workflow agendado, cada cinco minutos, barriendo los\ndemos vencidos. Gratis, simple, ya tenía las credenciales que necesitaba. Lo desplegué y seguí con\notra cosa.\n\nHasta que un demo seguía vivo a los **25 minutos**, contra un tope de 20.\n\nEl problema era el agendador. Los workflows agendados de GitHub Actions están **throttleados**: un\ncron `*/5` que yo había leído como \"cada cinco minutos\" estaba, según el historial de corridas,\ndisparándose **cada una o dos horas**. Es comportamiento documentado: los `schedule` son\nbest-effort. Y \"best-effort\" en una página de documentación significa \"no es tu reaper\" en\nproducción.\n\nLo que lo hacía genuinamente peligroso es la *forma* de la falla. El acceso se cortaba\n**perfectamente**: el token de sesión expira a los veinte minutos, el gateway deja de rutear, y el\nvisitante vive un demo que terminó exactamente a horario. Nada se veía roto desde ningún ángulo que\nun usuario o un health check pudieran observar — mientras el grupo de recursos seguía corriendo, con\nun piso de réplicas mayor que cero, hasta que un cron throttleado se dignaba a notarlo. La capa\nvisible era impecable y el consumo real era unas **seis veces** lo que había diseñado. Esa es la peor\nclase de bug de infraestructura que existe: **el que solo tiene un síntoma, y es dinero.**\n\nEl rediseño reemplazó el agendador por un **Worker de Cloudflare** con cron cada minuto que hace\nexactamente una cosa: `POST /api/demo/reap`. El backend barre lo que siempre iba a barrer —sesiones\nvencidas, entornos vacíos, entornos inactivos— y despacha un teardown por slot. La latencia del\nteardown pasó de \"entre cinco minutos y dos horas\" a cosa de un minuto, y el Worker no cuesta nada.\n\n**Un timer que no controlas no es un timer, es una sugerencia.** Si llegar tarde tiene un costo, la\npuntualidad tiene que ser la garantía de producto de alguien, no una comodidad best-effort que venía\nincluida con otra cosa que ya estabas usando.\n\nEse rediseño trajo también el timeout por inactividad, que tiene una sutileza que me gusta. Solo el\ngateway puede ver el tráfico de los demos —todo pasa por él, ya autenticado— así que estampa un\n`lastActivity`. Pero **solo cuenta la actividad iniciada por el cliente.** Un request, la apertura de\nun socket, un mensaje del cliente. Los datos que el servidor *empuja* hacia afuera, no. Si te\nequivocas ahí, un demo con streaming se mantiene vivo eternamente en nombre de un visitante que\ncerró la pestaña hace una hora, que es exactamente el escenario que el timeout existe para evitar.\n\n### El bug que destruía demos antes de que nadie los abriera\n\nLiberar un slot reseteaba sus campos — pero no `lastActivity`. Así que un demo **nuevo** que\nreclamaba ese slot reciclado heredaba una marca de tiempo del inquilino anterior, potencialmente de\nhoras atrás. El reaper hacía su aritmética, concluía correctamente que ese demo llevaba inactivo\nmucho más del límite, y lo borraba — **en menos de dos minutos, antes de que el visitante lo hubiera\nabierto siquiera.** Desde afuera: llega un correo de aprobación, haces click en el enlace, y te dice\nque el demo no está activo. El grupo de recursos ya no existe.\n\nCada pieza individual estaba bien. La matemática del reaper, el estampado, la liberación — cada una\ncorrecta en todo lo que manejaba explícitamente. El bug vivía en el espacio entre ellas: estado que\nera correcto cuando se escribió y estaba rancio para cuando se leyó.\n\n**Cuando promueves algo a estado persistente, la escritura es la parte fácil — audita el ciclo de\nvida completo.** Reclamar, liberar, reusar. Un slot reciclado arrastra las pertenencias del inquilino\nanterior a menos que lo vacíes a propósito.\n\nY lo encontró un humano notando que los grupos de recursos desaparecían sospechosamente rápido, no un\ntest. La suite estaba en verde todo el tiempo — como lo estaba, en el post anterior, mientras el\nchatbot saludaba en el idioma equivocado. Empiezo a pensar que eso es menos una coincidencia y más un\npatrón.\n\n## La infraestructura declarativa no ignora lo que no mencionas\n\nUna tarde redesplegué el template compartido para agregarle un par de variables de entorno al\ngateway. Minutos después, todos los subdominios de demo fallaban con un **525 SSL handshake** en el\nborde.\n\nLos bindings de dominio personalizado —los hostnames adjuntos al gateway con su certificado— se\ncrean con comandos de CLI, fuera del template, así que el template no los lista. Y un template\ndeclarativo no lee \"no mencionado\" como *déjalo como está*; lo lee como **\"esta propiedad debería\nestar vacía.\"** El redeploy borró obedientemente cada binding de hostname. Yo había construido un\ncómodo modelo mental donde el template era dueño de unas propiedades, mis comandos de CLI de otras, y\ntodos nos llevábamos bien. El template no compartía ese modelo.\n\n**Cuando un recurso tiene dueño compartido entre un template declarativo y comandos imperativos, el\ndeclarativo gana en cada deploy.** O metes la propiedad en el template, o aceptas un ritual\ndocumentado alrededor de cada deploy — el mío es que los cambios de variables de entorno van por un\nupdate puntual que no toca el ingress, y que cualquier redeploy real va seguido de rebindear los\nhostnames. Lo que no funciona es esperar que los dos se repartan el espacio de propiedades con\neducación.\n\n## \"Empieza, no termina, muere\"\n\nDurante semanas la infraestructura venía provisionando mis dos cargas de prueba desechables sin\nquejarse. Entonces enchufé el chatbot RAG: el primer demo con tres contenedores en un pod, un\nreparto de recursos entre ellos, y un startup probe.\n\nFalló al instante. Todas las veces. Y como el workflow de provisión borra el grupo de recursos\ncuando el despliegue falla —lo cual es correcto, y es la regla de \"nada estacionado\" haciendo su\ntrabajo— la evidencia se borraba junto con él. Mi nota de ese día dice: *empieza, no termina,\nmuere.*\n\nDos rechazos, encadenados, cada uno descubierto solo después de arreglar el anterior:\n\n| Rechazo | Realidad |\n|---|---|\n| Startup probe con `failureThreshold: 40` | La plataforma lo topa en **30** |\n| Total de recursos inválido | Yo había asumido 4 vCPU / 8 GiB disponibles; el techo real de este tipo de entorno es **2 vCPU / 4 GiB** |\n\nLo que los hizo diagnosticables fue un diferencial: los dos demos simples provisionan bien, este no\n— *¿qué tiene él que ellos no tengan?* Un probe. Y después de eso, un reparto de recursos\nmulti-contenedor. Tener una carga aburrida y conocida-buena al lado de la interesante resultó valer\nmás que cualquier cantidad de lectura de logs.\n\nY el detalle que generaliza: **el compilador del template no valida nada de esto.** Construir el\ntemplate lo verifica contra un esquema, y al esquema le parece perfecto un threshold de 40 y 4 vCPU.\nEsos son límites de *runtime*, aplicados por la validación de preflight de la plataforma cuando\ndespliegas. **\"Compila\" y \"despliega\" son afirmaciones distintas, y solo una de las dos significa\nalgo.**\n\nEsa segunda fila es mi conexión favorita entre estos dos posts. El techo de 2 vCPU / 4 GiB es la\nrestricción exacta contra la que el post anterior pelea durante toda su sección de performance: el\nmodelo de medio billón de parámetros, el top-3 del retrieval, los chunks de 400 tokens, toda la\ndisciplina de tratar el prompt como un presupuesto. Cada una de esas decisiones se remonta a un\nmensaje de error que recibí *aquí*, en la infraestructura. La plataforma no se limitó a alojar el\ndemo. **Lo diseñó.**\n\n## Lo que deliberadamente no hice\n\n- **Sin cluster.** Un cluster es un recurso estacionado con una historia de administración adjunta,\n  que es precisamente lo que este proyecto existe para evitar.\n- **Sin autenticación dentro de cada demo.** El gateway es dueño de eso, una sola vez. Un demo nuevo\n  obtiene auth de sesión, TLS y expiración *por el hecho de existir* — no implementa nada.\n- **Sin fijar digests de imagen.** Cada provisión jala la imagen actual, que es lo que permite que\n  publicar un post refresque el corpus del chatbot sin que nada en este sistema se entere.\n- **Sin estado compartido entre un demo y la plataforma.** Un demo nunca se entera de que es efímero,\n  y puede morir a mitad de un request sin ceremonia. Eso es lo que hace que el teardown sea un\n  `delete` y no una negociación.\n\n## Conclusión\n\nEn reposo, todo esto cuesta prácticamente nada: el gateway duerme en cero réplicas, el pool son diez\nfilas de una tabla, el Worker del reaper es gratis. Cuando alguien pulsa el botón, cuesta un pod\nmientras un humano lo esté mirando de verdad.\n\nPero el número que más me gusta es el marginal. **Agregar el siguiente demo cuesta cero recursos\npermanentes.** Un proyecto nuevo se registra en unos pocos lugares, trae un template que describe sus\npropios contenedores, y hereda el pool, el gateway, la auth de sesión, la detección de inactividad y\nel reaper sin implementar ninguno. El costo del siguiente demo no es \"un servicio más corriendo para\nsiempre\" — son unos minutos de cómputo el día que alguien de verdad quiera verlo. Esa es la\ndiferencia entre un portafolio que me desincentiva a agregarle cosas y uno que no.\n\nDije al principio que no publicaría esto hasta que un demo real se hubiera provisionado para una\npersona real. Ya pasó, y lo que eso demostró no es que el diagrama estuviera bien. El diagrama\nestaba bien desde junio; es la parte fácil, un par de cajas y una flecha que dice \"borra el grupo de\nrecursos\". Lo que demostró la primera provisión real fue todo lo que el diagrama no dice. Cada\nproblema de este post vivía en un hueco entre dos cosas que eran, cada una por su lado, correctas:\nun modelo de estado y un lector en otro contenedor, una expresión cron y las promesas reales de su\nagendador, la propiedad de un template y la de una CLI, un slot liberado y uno reclamado.\n\nSon todos la misma falla, vestida distinto: **verifiqué las piezas y di por sentadas las costuras.**\nQue es, sospecho, mayormente lo que son los sistemas distribuidos: costuras, con piezas adjuntas.\n\nEl siguiente demo va a salir más barato que este, porque la parte cara ya está pagada. Ese es el\nretorno real de todo esto: no que el chatbot tenga dónde correr, sino que lo que construya después ya\nlo tiene, y no va a costar absolutamente nada hasta el momento en que alguien tenga la curiosidad de\npulsar un botón.\n\nCosa que puedes hacer ahora mismo, en\n[alexisalulema.com/projects](https://alexisalulema.com/projects). En algún lado se está creando un\ngrupo de recursos con el nombre de tu sesión, y en veinte minutos no quedará rastro de que existió."},{"slug":"a-rag-chatbot-over-my-own-blog-and-the-comeback-after-three-years","lang":"en","title":"A RAG Chatbot Over My Own Blog (and the Comeback After Three Years)","description":"I came back to writing by building something: a bilingual RAG chatbot over this blog's posts, 100% local, $0 in API costs, deployed as an ephemeral demo. Architecture, threshold calibration, CPU performance, and the bugs that taught more than the happy path.","publishDate":"2026-08-20","updatedDate":null,"tags":["rag","llm","python","fastapi","pgvector","ollama","embeddings","docker"],"translationSlug":"chatbot-rag-sobre-mi-propio-blog-el-regreso-despues-de-tres-anos","url":"https://alexisalulema.com/blog/a-rag-chatbot-over-my-own-blog-and-the-comeback-after-three-years/","series":null,"headings":[{"depth":2,"slug":"introduction","text":"Introduction"},{"depth":2,"slug":"the-constraints-defined-the-design-not-the-stack","text":"The constraints defined the design (not the stack)"},{"depth":2,"slug":"architecture","text":"Architecture"},{"depth":3,"slug":"ingestion-the-unglamorous-work","text":"Ingestion: the unglamorous work"},{"depth":2,"slug":"calibrating-the-threshold-where-i-nearly-fooled-myself","text":"Calibrating the threshold: where I nearly fooled myself"},{"depth":2,"slug":"performance-on-cpu-the-prompt-is-your-budget","text":"Performance on CPU: the prompt is your budget"},{"depth":2,"slug":"two-bugs-that-taught-more-than-the-happy-path","text":"Two bugs that taught more than the happy path"},{"depth":3,"slug":"what-topics-does-this-blog-cover--refusal","text":"”What topics does this blog cover?” → refusal"},{"depth":3,"slug":"the-chat-has-no-memory--it-actually-did","text":"”The chat has no memory” → it actually did"},{"depth":2,"slug":"the-first-real-user-or-how-my-sister-broke-the-demo-in-five-minutes","text":"The first real user (or: how my sister broke the demo in five minutes)"},{"depth":3,"slug":"the-temptation-to-fix-it-with-a-prompt","text":"The temptation to fix it with a prompt"},{"depth":2,"slug":"what-i-deliberately-didnt-do","text":"What I deliberately didn’t do"},{"depth":2,"slug":"conclusion","text":"Conclusion"}],"body":"## Introduction\n\nThe last entry on this blog is from **October 2023**. Almost three years. There's no dramatic\nstory behind that: there was work, shifting priorities, and above all a stretch where the field\nmoved so fast that writing about it felt like photographing a moving train. Every draft I started\nwas outdated before I finished it.\n\nSo I decided to come back the only way that actually convinces me: **with something built, not\nwith a promise to write again**. This post is about that something — a **RAG chatbot that answers\nquestions about this very blog's posts**, in Spanish or English, always citing the post the answer\ncame from. It runs fully self-hosted: no paid API, no runtime secrets, no GPU, and it lives as an\n**ephemeral demo**: it gets provisioned on request, runs for about 20 minutes, and tears itself\ndown.\n\nThere's a symmetry I like here: to start writing again, I built a machine that reads everything\nI'd already written.\n\nAnd there's something more honest worth saying up front: I built this project **together with a\ncoding assistant**, across long sessions of decisions and measurements. That's exactly the kind of\nwork I want to write about now, so this post also doubles as a sample of what that process looks\nlike when it's documented well.\n\n## The constraints defined the design (not the stack)\n\nBefore the first line of code, I wrote the rules. In a project like this the constraints *are* the\ndesign; the stack is just the consequence:\n\n- **$0 in paid API costs.** LLM and embeddings run locally. No per-token bills on a public demo.\n- **Self-contained.** No runtime secrets, no external calls in production. Everything the demo\n  needs is baked into the images.\n- **CPU only.** No GPU. None of the \"it works but costs like a training server\" trap.\n- **Ephemeral, with abrupt teardown.** ~20-minute sessions, 8 minutes idle, plus a kill switch. The\n  container can die mid-stream: there can be no state worth losing.\n- **OSS stack**, public images, and public data only (the posts are already on the internet).\n\nAlmost everything else fell out of that, on its own:\n\n| Piece | Choice |\n|---|---|\n| App | FastAPI (Python 3.13) + vanilla UI, **SSE** streaming |\n| LLM | **Qwen2.5-0.5B-Instruct** via **Ollama**, local in the container |\n| Embeddings | `paraphrase-multilingual-MiniLM-L12-v2` (384-d), local |\n| Vector store | **Postgres 16 + pgvector**, ephemeral, seeded on boot |\n| Corpus | 34 posts (28 EN + 6 ES) → **278 chunks** of ~400 tokens |\n\n## Architecture\n\nThe demo is **three containers in one pod, sharing `localhost`**: the app (the only one with\ningress), Ollama, and Postgres/pgvector. The app carries no auth of its own — a gateway in front of\nit handles that, and also terminates TLS.\n\n```\nBrowser (session) ──HTTPS──► Gateway (TLS + auth)\n                                 │  internal ingress :8080\n                                 ▼\n                           FastAPI (app)\n                                 │\n      Ephemeral pod — 2 vCPU / 4 GiB, shared localhost\n      ├─ embed query ──────► sentence-transformers (multilingual MiniLM-L12-v2)\n      ├─ top-3 cosine ─────► pgvector / Postgres 16 (278 chunks / 34 posts)\n      └─ grounded prompt ──► Ollama (Qwen2.5-0.5B-Instruct)\n                                 │\n                                 ▼  SSE tokens + citations\n                           Browser\n```\n\nThe expensive part — fetching the posts, cleaning them, chunking them, and computing embeddings —\n**never happens at runtime**. It happens in CI, and the result is a `dump.sql` baked into the\ndatabase image. When the demo boots, Postgres just loads the dump and the corpus is ready. That's\nthe difference between a startup measured in seconds and one measured in minutes, and in a demo\nthat lives for 20 minutes, that difference is everything.\n\nThe per-message flow is classic RAG, no frills:\n\n1. The question gets embedded (384-d, normalized).\n2. **Top-3 by cosine similarity** in pgvector, filtered by a threshold.\n3. The prompt is assembled: grounded *system* prompt + retrieved context + history + question.\n4. Qwen generates in **streaming**, the app relays it token by token over SSE, and attaches the\n   **citations** (the posts the chunks came from, deduplicated by URL).\n\nThe retrieval query is exactly as simple as it should be:\n\n```sql\nSELECT url, title, lang, published, chunk_index, content,\n       1 - (embedding <=> %(vec)s::vector) AS score\nFROM chunks\nORDER BY embedding <=> %(vec)s::vector\nLIMIT %(k)s\n```\n\nAnd the rule that makes the chatbot a chatbot *about my blog* rather than a generic chatbot lives\nin the system prompt:\n\n```text\nYou are a grounded assistant that answers ONLY using the provided context,\nwhich comes from Alexis Alulema's blog posts.\n- Reply in the SAME language as the user's question (English or Spanish).\n- Base every statement strictly on the context. Do NOT use outside knowledge.\n- If the context does not contain the answer, say —in the user's language— that\n  you can only answer questions about Alexis Alulema's blog posts.\n- Cite the sources you use by their titles.\n```\n\nExcept the real *grounding* isn't done by the prompt — it's done by the threshold. If no chunk\nclears the minimum similarity, the app **doesn't even call the LLM** and returns a fixed,\nhand-written message in the detected language. (More on why that decision turned out to matter\neven more than I thought, later.) A 0.5B model given no context makes things up; the best defense\nagainst hallucination is never giving it the chance.\n\n### Ingestion: the unglamorous work\n\nPosts are fetched from the site's **public sitemap** (not the blog's private repo), cleaned,\nchunked, and embedded. Cleaning was, as always, the least glamorous and most decisive part:\n\n- The site is Astro: the real content lives in `.post-content`; header and footer are navigation.\n- **Icon ligatures** (Material Symbols) leaked in as stray words — `beenhere` sitting in the middle\n  of a paragraph — same with **tables of contents** and their anchors.\n- **KaTeX duplicated every formula**: MathML + the LaTeX annotation + the rendered version. Three\n  copies of the same theorem competing inside the same chunk.\n- Classic mojibake: `requests` guessed Latin-1 and handed back broken accented characters. Forcing\n  UTF-8 and normalizing `&nbsp;`/zero-width characters closed that.\n\n🎓 **The detail I most enjoyed solving:** `paraphrase-multilingual-MiniLM-L12-v2` **truncates input\nat ~128 tokens**, but my chunks are 400 tokens long. Embed the chunk directly and you silently lose\nthree-quarters of the text, with no warning. The fix was to split each chunk into sub-windows of\n≤128 tokens and **mean-pool** their vectors: the chunk the LLM sees stays large, and the vector\nrepresenting it actually represents all of it.\n\nThe embeddings are multilingual, so retrieval is **cross-lingual for free**: a question in Spanish\nretrieves English chunks and vice versa. The first end-to-end test asked *\"What are activation\nfunctions?\"* and retrieved **both** correct posts — the English one and its Spanish version.\n\n## Calibrating the threshold: where I nearly fooled myself\n\nThe similarity threshold is the single most important parameter in the project: it decides when\nthe bot answers and when it says \"I can only talk about Alexis's posts.\" Eyeballing it is tempting\nand it's a mistake, so I wrote a small harness: a battery of labeled questions **inside the corpus**\n(EN, ES, and cross-lingual) and **outside the corpus**, that measures each group's score\ndistribution and recommends a cutoff.\n\nThe first calibration was a dream: clean separation, `in_min = 0.438` vs `out_max = 0.205`, a\n**0.233** gap, and zero misclassifications. Threshold set right in the middle: **0.32**. Case\nclosed.\n\nIt wasn't closed. With the demo already running, nine out-of-corpus questions were tested and\n**two slipped through**: React's `useState` and `useEffect` produced a real answer instead of a\nrefusal. Kubernetes, Vue, Angular, and Tailwind all refused correctly in that batch. Reproducing it\nlocally against the same database image, the diagnosis was uncomfortable:\n\n| Out-of-corpus question | top-1 |\n|---|---|\n| Vue.js composition API | **0.492** |\n| React `useState` | 0.475 |\n| React `useEffect` | 0.458 |\n| React hooks | 0.388 |\n\n...and the **actual floor inside the corpus** — the question about my Levenshtein-in-JavaScript\npost — sits at **0.425**. In other words: **below Vue**. There is no threshold that closes that\nleak without also refusing to answer a post I actually wrote.\n\nThe mistake wasn't the threshold — it was **the test battery**. My negatives were obviously\nunrelated topics (France, pizza, Taylor Swift). The negatives that matter are the **close** ones —\nthe ones that brush up against your domain without being in it. A post about JavaScript algorithms\nand a question about frontend frameworks live millimeters apart in embedding space, and with\n400-token chunks that distance shrinks even further.\n\nThe decision was explicit, not automated: raise it to **0.42** (tightens the margin without\nrejecting any real topic) and **accept the React/Vue leak as a known, documented limitation**. The\nharness, optimizing for pure accuracy, recommended 0.577 — which would have rejected *\"How do\ntransformers work internally?\"*, the single most central topic on the blog. Optimizing the\nharness's metric at the expense of your most important content is exactly the kind of decision that\nhas to be made by hand.\n\n## Performance on CPU: the prompt is your budget\n\nOnce the demo was deployed, the expected problem showed up: **slowness**. And a metric that on CPU\nmatters more than any other: **TTFT** (*time to first token*) — the prompt's *prefill* before the\nfirst word comes out. The platform has a hard ceiling of **2 vCPU / 4 GiB**, so there was no\nhardware lever to pull: the work itself had to get smaller.\n\nThree rounds, each measured separately so the effect could be attributed correctly:\n\n| Round | Change | Measured result |\n|---|---|---|\n| Perf I | LLM 1.5B → **0.5B** + `max_output_tokens` 512 → 256 | generation ~25-30 tok/s (~3×) |\n| Perf II | `TOP_K` 5 → **3** | TTFT ~17.6 s → **~10.1 s** |\n| Perf III | `CHUNK_TOKENS` 600 → **400** (+ recalibrated threshold) | cold TTFT **~5.7 s** |\n\nThe lesson is accounting, not magic: on CPU, **the retrieved context is the largest, most variable\npart of the prompt**. Going from 5 chunks of 600 tokens to 3 chunks of 400 took the prompt from\n~1800 to ~1200 tokens, and TTFT dropped to a third of the original. Token-by-token streaming does\nthe rest of the work — it doesn't reduce latency, but it completely changes how latency is\nperceived.\n\n⚠️ One detail that nearly threw my measurements off: **repeating the same question came back in\n~1 s**. That's Ollama's KV cache, not an improvement. The measurements that count are cold, novel\nquestions.\n\nAnd a rule fell out of this: changing chunk size **forces** you to recalibrate the threshold.\nSmaller chunks are more focused, the similarity density shifts, and a cutoff that used to be\ncorrect stops being correct. Moving to 400 tokens narrowed the gap between \"in\" and \"out\" from\n0.233 to 0.069. Less latency, less grounding margin: that was the trade-off, made with the numbers\nin plain view.\n\n## Two bugs that taught more than the happy path\n\n### \"What topics does this blog cover?\" → refusal\n\nA trivial question, and the bot refused it. The cause was elegant in its obviousness: **no chunk\nsummarizes the corpus**. Every chunk is a fragment of one specific post, and a *meta* question\ndoesn't resemble any single fragment closely enough. It wasn't a badly set threshold — it was a\n**corpus gap**.\n\nThe fix was an ingestion problem, not a runtime one: generate a **synthetic per-language summary\ndocument** at build time that lists the topics, and let it flow through the normal pipeline\n(chunk → embed → dump). It gets retrieved like any other chunk, and its citation points to the\nblog's index.\n\nThe first version listed **titles**, and it half-failed: a title doesn't always name the\ntechnology. So I went back to the site's HTML, and there it was, exactly what I needed: every post\nalready publishes its own **tags** (`machine-learning`, `python`, `transformers`, `c#`, `cqrs`,\n`ddd`...), hand-curated by me at publish time, and never scraped by the cleaner. Real metadata beats\nany keyword-extraction heuristic I could have invented. That question now retrieves the summary\nwith a top-1 of 0.694, comfortably over the threshold, and it's the first suggested chip in the UI:\nshowing the user the corpus's real scope from the first second is also the best mitigation against\noff-topic questions.\n\n### \"The chat has no memory\" → it actually did\n\nThe report was: follow-ups like *\"and in Python?\"* feel like they have no memory. The natural\nhypothesis is missing history. But the history **was there**, complete, end to end: the client\nkeeps it and resends it every turn, and the prompt includes it turn by turn.\n\nThe bug was one step earlier. Retrieval only embedded **the latest question**. A context-dependent\nfollow-up, on its own, doesn't carry enough signal to clear the threshold → zero chunks → the app\nrefuses **without ever calling the LLM**, ignoring the fact that the history actually held the\ncontext. The feeling of \"it doesn't remember\" wasn't the model forgetting — it was retrieval\n**never giving it the chance to remember**.\n\nThe fix was two independent changes: a **5-turn window** on the client (history was growing\nunbounded and bloating the prompt I'd worked so hard to slim down), and a **contextualized retry**:\nif retrieval on the question alone comes back empty and there's history, retry once by prepending\nthe user's last turn — **only for the search embedding**, never for what's actually sent to the\nLLM. The normal path doesn't change, so the threshold calibration remains valid for it.\n\nI'll take this as the best lesson of the project: **in a RAG system, the symptom almost never\npoints to the layer at fault.** \"The model doesn't remember\" was a retrieval problem. \"The\nthreshold is wrong\" was a corpus problem. \"It's slow\" was a prompt-size problem.\n\n## The first real user (or: how my sister broke the demo in five minutes)\n\nEverything above, I tested myself — and I know exactly what I built, so without meaning to, I write\nthe questions the system expects. So I asked my sister to try it. She's not a developer, hadn't\nread anything about the project, and walked into the chat the way anyone actually would: by\ngreeting it.\n\nHer entire session was two messages long. She found three things.\n\n**\"Hola\" → answered in English.** The language detector was a keyword heuristic, and `hola` wasn't\non any list: no accented characters to signal Spanish, no recognized word, the score sat at 0-0 and\nfell back to the default, which was English. All of my own testing had been full questions — *\"How\ndo transformers work?\"* — dense with signal. A bare greeting, the single most common way a human\nopens a chat, was exactly the no-signal case.\n\n**\"Who is Alexis?\" → the stream cut off mid-answer.** It managed to write *\"Alexis Alulema is\"* and\nthe connection died. She didn't try again: she closed the chat. The cause was at startup: Ollama\n**lazily loads the model's weights on the first real inference**, and my health check only verified\nthat the server responded. Every TTFT measurement I'd been so proud of had been taken with the\nmodel already warm. Her question was the first genuine inference in that container, it paid the\nfull load cost, and the gateway cut the connection on timeout before it could finish. The fix was\nto preload the model during startup, before reporting *healthy*: the container now takes a few\nextra seconds to come up, and no visitor ever pays that bill.\n\nAnd that's where the third thing came from — the one that taught me the most.\n\n### The temptation to fix it with a prompt\n\nTesting after those fixes, I ran into the canned refusal myself: I asked *\"What topics do you\nknow?\"*, it didn't match any chunk, and I got the same dry, fixed sentence every time. It sounds\nlike a robot. The obvious idea: instead of the fixed sentence, call the LLM with a different prompt\nasking it to kindly acknowledge it doesn't have that information and invite the user to ask about\nreal blog topics. More human, more conversational.\n\nI implemented it, spun up the full stack, and tested it with questions that were genuinely\nout of scope:\n\n| Question (no chunk retrieved) | Response |\n|---|---|\n| What is the capital of France? | *\"The capital of France is Paris.\"* |\n| What's the best pizza in the world? | Confidently invented a recipe |\n| Who won the 2022 World Cup? | *\"…Brazil won\"* |\n\nThe prompt said, literally, **\"Do NOT use outside knowledge to answer the original question.\"**\nThe model ignored it all three times. And on the third one, on top of breaking the rule, it lied:\nArgentina won.\n\nThat's the lesson, and it's the most important one I'm taking away from this project: **a 0.5B\nmodel doesn't reliably respect negative instructions, so a hard guarantee can't live inside a\nprompt.** \"Grounded-only\" is a promise I'm making to whoever uses the demo; if honoring it depends\non the model having the discipline to hold back, that's not a guarantee — it's a hope. I reverted\nthe change: when there's no context, the LLM simply **isn't invoked** — the response is\ndeterministic, written by me, and yes, kinder than the original one (it acknowledges it doesn't\nknow and suggests real topics). It has no conversational variety. It never makes things up. That\ntrade-off isn't even close to a hard call.\n\nAnd there's one detail that feels like the summary of this whole section: **none of these three\nproblems could have been caught by my test suite.** All 72 tests were passing green while the bot\ngreeted people in the wrong language, hung on the first-ever visitor, and invented World Cup\nfinals. It took someone who knew nothing about the system, and the real stack actually running, to\nsurface any of it.\n\n## What I deliberately didn't do\n\n- **No fine-tuning.** The model is fixed; the only thing that gets re-indexed is the corpus.\n  Publishing a new post doesn't retrain anything.\n- **No server-side persisted conversations.** History lives on the client, which is exactly why the\n  demo can tolerate being killed mid-session.\n- **No runtime re-indexing.** The corpus is fixed per image. It refreshes at **build time**, not at\n  deploy time: a workflow re-ingests, regenerates the dump, and republishes the image; the next\n  demo that gets provisioned already uses the fresh corpus.\n- **No auth of its own** in the app — the gateway handles that.\n\nAnd the honest limitation, already mentioned: questions about **adjacent frontend frameworks**\n(React, Vue) can slip through and produce a weak answer instead of a clean refusal. It's measured,\ndocumented, and accepted — which is a very different situation from \"we didn't know.\"\n\n## Conclusion\n\nThis project doesn't invent anything: RAG is a well-known, well-documented architecture. What was\nactually interesting lived on the other side — in the **constraints**. A 0.5B LLM, two vCPUs, no\npaid API, and a twenty-minute lifecycle turn decisions that are normally comfortable (top-k? chunk\nsize? threshold?) into measurable trade-offs with visible consequences. And measuring each one\nseparately, instead of changing three things at once, is what made it possible to attribute each\nimprovement to its actual cause.\n\nThere's an ending I genuinely like: this site's publishing pipeline **triggers corpus\nre-indexing**. As soon as this post goes live, the chatbot will be able to answer questions about\nit — including, I imagine, the question of why it took me three years to start writing again.\n\nThe chatbot is available as a live demo, alongside the rest of my experiments, at\n[alexisalulema.com/projects](https://alexisalulema.com/projects?demo=rag-blogposts) — it provisions on request, runs\nfor a few minutes, and tears itself down. And the code is public, complete with its full log of\ndecisions, measurements, and mistakes:\n[github.com/alulema/rag-blogposts](https://github.com/alulema/rag-blogposts).\n\nI'm back. And this time I'm bringing things I actually built, to talk about."},{"slug":"chatbot-rag-sobre-mi-propio-blog-el-regreso-despues-de-tres-anos","lang":"es","title":"Un chatbot RAG sobre mi propio blog (y el regreso después de tres años)","description":"Volví a escribir construyendo algo: un chatbot RAG bilingüe sobre los posts de este blog, 100% local, $0 de API y desplegado como demo efímero. Arquitectura, calibración del umbral, performance en CPU y los bugs que enseñaron más que el happy path.","publishDate":"2026-08-20","updatedDate":null,"tags":["rag","llm","python","fastapi","pgvector","ollama","embeddings","docker"],"translationSlug":"a-rag-chatbot-over-my-own-blog-and-the-comeback-after-three-years","url":"https://alexisalulema.com/es/blog/chatbot-rag-sobre-mi-propio-blog-el-regreso-despues-de-tres-anos/","series":null,"headings":[{"depth":2,"slug":"introducción","text":"Introducción"},{"depth":2,"slug":"las-restricciones-definieron-el-diseño-no-el-stack","text":"Las restricciones definieron el diseño (no el stack)"},{"depth":2,"slug":"arquitectura","text":"Arquitectura"},{"depth":3,"slug":"ingesta-el-trabajo-sucio","text":"Ingesta: el trabajo sucio"},{"depth":2,"slug":"calibrar-el-umbral-donde-casi-me-engaño-solo","text":"Calibrar el umbral: donde casi me engaño solo"},{"depth":2,"slug":"performance-en-cpu-el-prompt-es-tu-presupuesto","text":"Performance en CPU: el prompt es tu presupuesto"},{"depth":2,"slug":"dos-bugs-que-enseñaron-más-que-el-happy-path","text":"Dos bugs que enseñaron más que el happy path"},{"depth":3,"slug":"de-qué-temas-habla-este-blog--rechazo","text":"”¿De qué temas habla este blog?” → rechazo"},{"depth":3,"slug":"el-chat-no-tiene-memoria--sí-la-tenía","text":"”El chat no tiene memoria” → sí la tenía"},{"depth":2,"slug":"la-primera-usuaria-real-o-cómo-mi-hermana-rompió-el-demo-en-cinco-minutos","text":"La primera usuaria real (o: cómo mi hermana rompió el demo en cinco minutos)"},{"depth":3,"slug":"la-tentación-de-arreglarlo-con-un-prompt","text":"La tentación de arreglarlo con un prompt"},{"depth":2,"slug":"lo-que-deliberadamente-no-hice","text":"Lo que deliberadamente no hice"},{"depth":2,"slug":"conclusión","text":"Conclusión"}],"body":"## Introducción\n\nLa última entrada de este blog es de **octubre de 2023**. Casi tres años. No hay una historia\ndramática detrás: hubo trabajo, mudanzas de contexto, y sobre todo un periodo en el que el campo se\nmovía tan rápido que escribir sobre él se sentía como fotografiar un tren en marcha. Cada borrador\nque empezaba envejecía antes de terminarlo.\n\nAsí que decidí volver de la única manera que me convence: **con algo construido, no con una promesa\nde volver a escribir**. Este post es sobre ese algo — un **chatbot RAG que responde preguntas sobre\nlos posts de este mismo blog**, en español o en inglés, citando siempre el post del que sacó la\nrespuesta. Corre completamente autohospedado: sin API de pago, sin secretos en runtime, sin GPU, y\nvive como **demo efímero**: se provisiona cuando alguien lo pide, corre unos 20 minutos y se destruye\nsolo.\n\nHay una simetría que me gusta: para volver a escribir, construí una máquina que lee todo lo que\nescribí antes.\n\nY hay algo más honesto que decir de entrada: el proyecto lo construí **junto con un asistente de\ncódigo**, en sesiones largas de decisiones y mediciones. Es exactamente el tipo de trabajo sobre el\nque quiero escribir ahora, así que este post también es una muestra de cómo se ve ese proceso cuando\nse documenta bien.\n\n## Las restricciones definieron el diseño (no el stack)\n\nAntes de la primera línea de código, escribí las reglas. En un proyecto de este tipo las\nrestricciones son el diseño; el stack es apenas la consecuencia:\n\n- **$0 de API de pago.** LLM y embeddings locales. Nada de facturas por token en un demo público.\n- **Self-contained.** Sin secretos en runtime y sin llamadas externas en producción. Todo lo que el\n  demo necesita está horneado en las imágenes.\n- **CPU only.** Sin GPU. Nada de \"funciona pero cuesta como un servidor de entrenamiento\".\n- **Efímero y con teardown abrupto.** Sesión de ~20 minutos, 8 de inactividad, y kill switch. El\n  contenedor puede morir a mitad de un stream: no puede haber estado que perder.\n- **Stack OSS**, imágenes públicas, y solo datos públicos (los posts ya están en internet).\n\nDe ahí salió, casi solo, el resto:\n\n| Pieza | Elección |\n|---|---|\n| App | FastAPI (Python 3.13) + UI vanilla, streaming por **SSE** |\n| LLM | **Qwen2.5-0.5B-Instruct** vía **Ollama**, local en el contenedor |\n| Embeddings | `paraphrase-multilingual-MiniLM-L12-v2` (384-d), local |\n| Vector store | **Postgres 16 + pgvector**, efímero, sembrado al arranque |\n| Corpus | 34 posts (28 EN + 6 ES) → **278 chunks** de ~400 tokens |\n\n## Arquitectura\n\nEl demo son **tres contenedores en un mismo pod, compartiendo `localhost`**: la app (único con\ningress), Ollama y Postgres/pgvector. La app no lleva autenticación propia — la hace un gateway que\nestá por delante y que también termina TLS.\n\n```\nNavegador (sesión) ──HTTPS──► Gateway (TLS + auth de sesión)\n                                  │  ingress interno :8080\n                                  ▼\n                            FastAPI (app)\n                                  │\n       Pod efímero — 2 vCPU / 4 GiB, comparten localhost\n       ├─ embed de la pregunta ──► sentence-transformers (multilingual MiniLM-L12-v2)\n       ├─ top-3 por coseno ──────► pgvector / Postgres 16 (278 chunks / 34 posts)\n       └─ prompt fundamentado ───► Ollama (Qwen2.5-0.5B-Instruct)\n                                  │\n                                  ▼  tokens SSE + citas\n                            Navegador\n```\n\nLa parte cara — traer los posts, limpiarlos, trocearlos y calcular embeddings — **no ocurre nunca en\nruntime**. Ocurre en CI, y su resultado es un `dump.sql` que se hornea dentro de la imagen de la base\nde datos. Cuando el demo arranca, Postgres simplemente carga el dump y el corpus está listo. Es la\ndiferencia entre un arranque de segundos y uno de minutos, y en un demo que vive 20 minutos esa\ndiferencia lo es todo.\n\nEl flujo por mensaje es el clásico de RAG, sin adornos:\n\n1. Se embebe la pregunta (384-d, normalizada).\n2. **Top-3 por similitud coseno** en pgvector, filtrando por un umbral.\n3. Se arma el prompt: *system* fundamentado + contexto recuperado + historial + pregunta.\n4. Qwen genera en **streaming**, la app relaya token a token por SSE y adjunta las **citas** (los\n   posts de los chunks usados, deduplicados por URL).\n\nLa consulta de retrieval es tan simple como debería ser:\n\n```sql\nSELECT url, title, lang, published, chunk_index, content,\n       1 - (embedding <=> %(vec)s::vector) AS score\nFROM chunks\nORDER BY embedding <=> %(vec)s::vector\nLIMIT %(k)s\n```\n\nY la regla que hace que el chatbot sea un chatbot *sobre mi blog* y no un chatbot genérico vive en el\nsystem prompt:\n\n```text\nYou are a grounded assistant that answers ONLY using the provided context,\nwhich comes from Alexis Alulema's blog posts.\n- Reply in the SAME language as the user's question (English or Spanish).\n- Base every statement strictly on the context. Do NOT use outside knowledge.\n- If the context does not contain the answer, say —in the user's language— that\n  you can only answer questions about Alexis Alulema's blog posts.\n- Cite the sources you use by their titles.\n```\n\nAunque el verdadero *grounding* no lo hace el prompt: lo hace el umbral. Si ningún chunk supera la\nsimilitud mínima, la app **ni siquiera llama al LLM** y devuelve un mensaje fijo, escrito a mano, en\nel idioma detectado. (Más adelante cuento por qué esa decisión resultó ser incluso más importante de\nlo que yo creía.) Un modelo de 0.5B al que no le das contexto inventa; la mejor defensa contra la\nalucinación es no darle la oportunidad.\n\n### Ingesta: el trabajo sucio\n\nLos posts se traen del **sitemap público** del sitio (no del repo privado del blog), se limpian, se\ntrocean y se embeben. La limpieza fue, como siempre, la parte menos glamorosa y la más determinante:\n\n- El sitio es Astro: el contenido real vive en `.post-content`; el header y el footer son navegación.\n- Las **ligaduras de iconos** (Material Symbols) se colaban como palabras sueltas — `beenhere` en\n  medio de un párrafo — igual que las **tablas de contenidos** con sus anclas.\n- **KaTeX duplicaba cada fórmula**: MathML + anotación LaTeX + la versión visual. Tres copias del\n  mismo teorema compitiendo en el mismo chunk.\n- Mojibake clásico: `requests` adivinaba Latin-1 y me devolvía acentos rotos. Forzar UTF-8 y\n  normalizar `&nbsp;`/zero-width lo cerró.\n\n🎓 **El detalle que más me gustó resolver:** `paraphrase-multilingual-MiniLM-L12-v2` **trunca la\nentrada a ~128 tokens**, pero mis chunks son de 400. Si embebes el chunk directo, pierdes tres cuartas\npartes del texto sin que nada te avise. La solución fue partir cada chunk en sub-ventanas de ≤128\ntokens y hacer **mean-pooling** de sus vectores: el chunk que ve el LLM sigue siendo grande, y el\nvector que lo representa realmente lo representa completo.\n\nLos embeddings son multilingües, así que el retrieval es **cross-lingual gratis**: una pregunta en\nespañol recupera chunks en inglés y viceversa. La primera prueba end-to-end preguntó *\"What are\nactivation functions?\"* y recuperó **los dos** posts correctos — el inglés y su versión en español.\n\n## Calibrar el umbral: donde casi me engaño solo\n\nEl umbral de similitud es el parámetro más importante del proyecto: decide cuándo el bot responde y\ncuándo dice \"solo puedo hablar de los posts de Alexis\". Ponerlo a ojo es tentador y es un error, así\nque escribí un pequeño harness: una batería de preguntas etiquetadas **dentro del corpus** (EN, ES y\ncross-lingual) y **fuera del corpus**, que mide la distribución de scores de cada grupo y recomienda\nun corte.\n\nLa primera calibración fue un sueño: separación limpia, `in_min = 0.438` vs `out_max = 0.205`, un\nhueco de **0.233** y cero mal clasificados. Umbral en el medio: **0.32**. Caso cerrado.\n\nNo estaba cerrado. Con el demo ya corriendo, se probaron nueve preguntas fuera de corpus y **dos se\ncolaron**: `useState` y `useEffect` de React generaron una respuesta real en vez de rechazar.\nKubernetes, Vue, Angular y Tailwind rechazaron bien en esa tanda. Al reproducirlo localmente contra\nla misma imagen de la base de datos, el diagnóstico fue incómodo:\n\n| Pregunta fuera de corpus | top-1 |\n|---|---|\n| Vue.js composition API | **0.492** |\n| React `useState` | 0.475 |\n| React `useEffect` | 0.458 |\n| React hooks | 0.388 |\n\n...y el **piso real dentro del corpus** — la pregunta sobre mi post de Levenshtein en JavaScript —\nestá en **0.425**. Es decir: **por debajo de Vue**. No existe ningún umbral que cierre la fuga sin\ndejar de responder sobre un post que sí escribí.\n\nEl error no estuvo en el umbral, estuvo en **la batería**: mis negativos eran temas obviamente ajenos\n(Francia, pizza, Taylor Swift). Los negativos que importan son los **cercanos** — los que rozan tu\ndominio sin estar en él. Un post sobre algoritmos en JavaScript y una pregunta sobre frameworks\nfrontend viven a milímetros en el espacio de embeddings, y con chunks de 400 tokens esa distancia se\nacorta todavía más.\n\nLa decisión fue explícita, no automática: subir a **0.42** (aprieta el margen sin rechazar ningún\ntema real) y **aceptar la fuga de React/Vue como limitación conocida y documentada**. El harness, por\naccuracy pura, recomendaba 0.577 — lo que habría rechazado *\"How do transformers work internally?\"*,\nel tema más central del blog. Optimizar la métrica del harness a costa de tu contenido más importante\nes exactamente el tipo de decisión que hay que tomar a mano.\n\n## Performance en CPU: el prompt es tu presupuesto\n\nCon el demo desplegado apareció el problema esperado: **lentitud**. Y una métrica que en CPU importa\nmás que las demás: el **TTFT** (*time to first token*), es decir, el *prefill* del prompt antes de\nque salga la primera palabra. La plataforma tiene un techo de **2 vCPU / 4 GiB**, así que no había\nlever de hardware: había que hacer el trabajo más chico.\n\nTres rondas, cada una medida por separado para poder atribuir el efecto:\n\n| Ronda | Cambio | Resultado medido |\n|---|---|---|\n| Perf I | LLM 1.5B → **0.5B** + `max_output_tokens` 512 → 256 | generación ~25-30 tok/s (~3×) |\n| Perf II | `TOP_K` 5 → **3** | TTFT ~17.6 s → **~10.1 s** |\n| Perf III | `CHUNK_TOKENS` 600 → **400** (+ recalibrar umbral) | TTFT frío **~5.7 s** |\n\nLa lección es de contabilidad, no de magia: en CPU **el contexto recuperado es la parte grande y\nvariable del prompt**. Pasar de 5 chunks de 600 tokens a 3 chunks de 400 llevó el prompt de ~1800 a\n~1200 tokens, y el TTFT cayó a un tercio del original. El streaming token a token hace el resto del\ntrabajo — no reduce la latencia, pero cambia por completo cómo se percibe.\n\n⚠️ Un detalle que casi me hace medir mal: **repetir la misma pregunta daba ~1 s**. Es el KV cache de\nOllama, no una mejora. Las mediciones que valen son con preguntas nuevas y en frío.\n\nY una regla que salió de aquí: cambiar el tamaño del chunk **obliga** a recalibrar el umbral. Chunks\nmás pequeños son más focalizados, la densidad de similitud cambia, y el corte que era correcto deja\nde serlo. Al pasar a 400 tokens el hueco entre \"dentro\" y \"fuera\" se estrechó de 0.233 a 0.069. Menos\nlatencia, menos margen de *grounding*: ese fue el trueque, tomado con los números a la vista.\n\n## Dos bugs que enseñaron más que el happy path\n\n### \"¿De qué temas habla este blog?\" → rechazo\n\nUna pregunta trivial, y el bot rechazaba. La causa era elegante en su obviedad: **ningún chunk resume\nel corpus**. Cada chunk es un fragmento de un post concreto, y una pregunta *meta* no se parece lo\nsuficiente a ningún fragmento en particular. No era un umbral mal puesto — era una **laguna del\ncorpus**.\n\nLa solución fue de ingesta, no de runtime: generar en build-time un **documento sintético de resumen\npor idioma** que enumera los temas, y dejarlo fluir por el pipeline normal (chunk → embed → dump). Se\nrecupera como cualquier otro chunk, y la cita apunta al índice del blog.\n\nLa primera versión enumeraba **títulos**, y falló a medias: un título no siempre nombra la\ntecnología. Entonces revisé el HTML del sitio y ahí estaba lo que necesitaba: cada post ya publica sus\n**tags** (`machine-learning`, `python`, `transformers`, `c#`, `cqrs`, `ddd`...), curados a mano por mí\nal publicar y que el limpiador nunca estaba scrapeando. Metadata real gana a cualquier heurística de\nextracción de keywords que hubiera inventado. Hoy esa pregunta recupera el resumen con top-1 = 0.694,\nholgadamente sobre el umbral, y es el primer chip sugerido en la UI: mostrarle al usuario el alcance\nreal del corpus desde el primer segundo es también la mejor mitigación contra las preguntas fuera de\ntema.\n\n### \"El chat no tiene memoria\" → sí la tenía\n\nEl reporte fue: los follow-ups tipo *\"¿y en Python?\"* se sienten sin memoria. La hipótesis natural es\nque falta historial. Pero el historial **estaba**, completo, de punta a punta: el cliente lo mantiene\ny lo reenvía en cada turno, y el prompt lo incluye turno a turno.\n\nEl bug estaba un paso antes. El retrieval embebía **solo la última pregunta**. Un follow-up\ndependiente del contexto, por sí solo, no tiene suficiente señal para pasar el umbral → cero chunks →\nla app rechaza **sin llamar al LLM**, ignorando que el historial sí traía el contexto. La sensación de\n\"no recuerda\" no era del modelo olvidando: era del retrieval **nunca dándole la oportunidad de\nrecordar**.\n\nEl arreglo fueron dos cambios independientes: una **ventana de 5 turnos** en el cliente (el historial\ncrecía sin límite y engordaba el prompt que tanto había costado adelgazar) y un **reintento\ncontextualizado**: si el retrieval con la pregunta sola sale vacío y hay historial, se reintenta una\nvez anteponiendo el último turno del usuario — **solo para el embedding de la búsqueda**, nunca en lo\nque se le manda al LLM. El camino normal no cambia, así que la calibración del umbral sigue siendo\nválida para él.\n\nMe quedo con esta como la mejor lección del proyecto: **en un sistema RAG, el síntoma casi nunca\napunta a la capa culpable.** \"El modelo no recuerda\" era un problema de retrieval. \"El umbral está\nmal\" era un problema del corpus. \"Es lento\" era un problema de tamaño de prompt.\n\n## La primera usuaria real (o: cómo mi hermana rompió el demo en cinco minutos)\n\nTodo lo anterior lo probé yo, que sé exactamente qué construí y —sin darme cuenta— escribo las\npreguntas que el sistema espera. Entonces le pedí a mi hermana que lo probara. No es\ndesarrolladora, no leyó nada sobre el proyecto y entró al chat como entra cualquiera: saludando.\n\nSu sesión completa fue de dos mensajes. Encontró tres cosas.\n\n**\"Hola\" → respuesta en inglés.** El detector de idioma era una heurística de palabras clave, y\n`hola` no estaba en ninguna lista: sin tildes que delataran el español y sin ninguna palabra\nconocida, el marcador quedaba 0-0 y caía al default, que era inglés. Todas mis pruebas habían sido\npreguntas completas —`¿Cómo funcionan los transformadores?`— llenas de señal. Un saludo suelto, el\nmensaje más común con el que un humano abre un chat, era justo el caso sin señal.\n\n**\"Quien es Alexis?\" → el stream se cortó a media respuesta.** Alcanzó a escribir *\"Alexis Alulema\nes\"* y murió la conexión. Ella no volvió a intentar: cerró el chat. La causa estaba en el arranque:\nOllama **carga los pesos del modelo de forma perezosa, en la primera inferencia real**, y mi\nhealth check solo verificaba que el servidor respondiera. Todas las mediciones de TTFT de las que\ntan orgulloso estaba se habían hecho con el modelo ya caliente. Su pregunta fue la primera\ninferencia de verdad en ese contenedor, pagó la carga completa, y el gateway cortó por timeout\nantes de terminar. La solución fue precargar el modelo durante el arranque, antes de reportar\n*healthy*: ahora el contenedor tarda unos segundos más en levantar, y ningún visitante paga esa\nfactura.\n\nY de ahí salió el tercero, que es el que más me enseñó.\n\n### La tentación de arreglarlo con un prompt\n\nProbando después de esos arreglos, me topé con el rechazo enlatado: pregunté *\"¿Qué temas\nconoces?\"*, no pegó con ningún chunk, y recibí la misma frase seca de siempre. Suena a robot. La\nidea obvia: en vez de la frase fija, llamar al LLM con un prompt distinto que le pida reconocer\namablemente que no tiene esa información e invitar a preguntar sobre temas del blog. Más humano,\nmás conversacional.\n\nLo implementé, levanté el stack completo y lo probé con preguntas genuinamente fuera de tema:\n\n| Pregunta (sin ningún chunk recuperado) | Respuesta |\n|---|---|\n| ¿Cuál es la capital de Francia? | *\"La capital de Francia es París.\"* |\n| ¿Cuál es la mejor pizza del mundo? | Se inventó una receta con total seguridad |\n| ¿Quién ganó el mundial 2022? | *\"…ganó Brasil\"* |\n\nEl prompt decía, literalmente, **\"NO uses conocimiento externo para responder la pregunta\noriginal\"**. El modelo lo ignoró las tres veces. Y en la tercera, además de romper la regla,\nmintió: ganó Argentina.\n\nAhí está la lección, y es la más importante que me llevo del proyecto: **un modelo de 0.5B no\nrespeta instrucciones negativas de forma confiable, así que una garantía dura no puede vivir en un\nprompt.** \"Grounded-only\" es una promesa que le hago a quien usa el demo; si su cumplimiento\ndepende de que el modelo tenga la disciplina de abstenerse, no es una garantía, es una esperanza.\nReverti el cambio: cuando no hay contexto, el LLM sencillamente **no se invoca** — la respuesta es\ndeterminística, escrita por mí, y sí, más amable que la original (reconoce que no sabe y sugiere\ntemas reales). No tiene variedad conversacional. Nunca inventa. Ese trueque no está ni cerca de\nser difícil.\n\nY hay un detalle que me parece el resumen de toda esta sección: **ninguno de los tres problemas lo\npodía encontrar mi suite de tests.** Los 72 tests pasaban en verde mientras el bot saludaba en el\nidioma equivocado, se colgaba con el primer visitante e inventaba finales del Mundial. Hicieron\nfalta una persona que no sabía nada del sistema y levantar el stack real para verlos.\n\n## Lo que deliberadamente no hice\n\n- **Sin fine-tuning.** El modelo es fijo; lo único que se re-indexa es el corpus. Publicar un post\n  nuevo no reentrena nada.\n- **Sin conversaciones persistidas** en el servidor. El historial vive en el cliente, y por eso el\n  demo tolera que lo maten a mitad de sesión.\n- **Sin re-indexar en runtime.** El corpus es fijo por imagen. Se refresca al **construir**, no al\n  desplegar: un workflow re-ingesta, regenera el dump y republica la imagen; el siguiente demo que se\n  provisione ya usa el corpus fresco.\n- **Sin auth propia** en la app: la hace el gateway.\n\nY la limitación honesta, ya mencionada: preguntas sobre **frameworks frontend adyacentes** (React,\nVue) pueden colarse y generar una respuesta pobre en vez de un rechazo limpio. Está medido,\ndocumentado y aceptado — que es una situación muy distinta a \"no lo sabíamos\".\n\n## Conclusión\n\nEste proyecto no inventa nada: RAG es una arquitectura conocida y bien documentada. Lo interesante\nestuvo en el otro lado — en las **restricciones**. Un LLM de 0.5B, dos vCPUs, sin API de pago y con\nun ciclo de vida de veinte minutos convierten decisiones que normalmente son cómodas (¿top-k? ¿tamaño\nde chunk? ¿umbral?) en trueques medibles con consecuencias visibles. Y medir cada uno por separado,\nen vez de cambiar tres cosas a la vez, es lo que permitió atribuir cada mejora a su causa.\n\nHay un cierre que me hace sonreír: el pipeline de publicación de este sitio **dispara la re-indexación\ndel corpus**. En cuanto esta entrada esté publicada, el chatbot va a poder responder preguntas sobre\nella — incluida, supongo, la pregunta de por qué me tomó tres años volver a escribir.\n\nEl chatbot está disponible como demo en vivo, junto con el resto de experimentos, en\n[alexisalulema.com/projects](https://alexisalulema.com/es/projects?demo=rag-blogposts) — se provisiona cuando lo pides,\ncorre unos minutos y se destruye solo. Y el código es público, con su bitácora completa de\ndecisiones, mediciones y errores:\n[github.com/alulema/rag-blogposts](https://github.com/alulema/rag-blogposts).\n\nVolví. Y esta vez traigo cosas construidas para contar."},{"slug":"descomposicion-cholesky-para-regresion-lineal","lang":"es","title":"Descomposición Cholesky para Regresión Lineal","description":"beenhereActualización 10.2023Esta entrada ha sido actualizada con la versión de TensorFlow 2.12.1 y PyTorch 2.0.1 Ya han transcurrido varios años desde el inicio del auge del Deep Learning. He sido te…","publishDate":"2023-10-03","updatedDate":null,"tags":["python","pytorch","tensorflow"],"translationSlug":"cholesky-decomposition-for-linear-regression-with-tensorflow","url":"https://alexisalulema.com/es/blog/descomposicion-cholesky-para-regresion-lineal/","series":null,"headings":[{"depth":3,"slug":"actualización-102023","text":"Actualización 10.2023"}],"body":"*beenhere*\n\n### Actualización 10.2023\n\nEsta entrada ha sido actualizada con la versión de TensorFlow 2.12.1 y PyTorch 2.0.1\n\nYa han transcurrido varios años desde el inicio del auge del Deep Learning. He sido testigo de logros impresionantes como ChatGPT y Midjourney, sin embargo, aún me sorprende cómo métodos tradicionales como la descomposición Cholesky siguen siendo extremadamente útiles y eficientes. En particular, para tareas como la Regresión Lineal, este método destaca debido a su eficiencia y rapidez en el cálculo.\n\nA pesar de que la [Regresión Lineal de Mínimos Cuadrados](https://www.alexisalulema.com/2018/01/18/linear-least-squares-regression-with-tensorflow/) es sencilla y precisa, puede volverse ineficiente cuando tratamos con matrices de gran tamaño. La descomposición Cholesky ofrece una alternativa para resolver estas matrices de manera eficaz. Esta técnica descompone una matriz en dos matrices triangulares: una inferior (lower) y una inferior transpuesta (L y LT), con las cuales se obtiene la matriz original, tal como se ilustra en la figura que sigue.\n\n![](https://images.alexisalulema.com/blog/cholesky-decomposition-for-linear-regression-with-tensorflow/cholesky.graphic-1-1024x403.png)\n\n$$\n\\begin{bmatrix}<br>a_{11} & a_{12} & a_{13} & a_{14}\\\\<br>a_{21} & a_{22} & a_{23} & a_{24}\\\\<br>a_{31} & a_{32} & a_{33} & a_{34}\\\\<br>a_{41} & a_{42} & a_{43} & a_{44}<br>\\end{bmatrix}=\\begin{bmatrix}<br>l_{11} & 0 & 0 & 0\\\\<br>l_{21} & l_{22} & 0 & 0\\\\<br>l_{31} & l_{32} & l_{33} & 0\\\\<br>l_{41} & l_{42} & l_{43} & l_{44}<br>\\end{bmatrix}\\cdot \\begin{bmatrix}<br>l_{11} & l_{21} & l_{31} & l_{41}\\\\<br>0 & l_{22} & l_{32} & l_{42}\\\\<br>0 & 0 & l_{33} & l_{43}\\\\<br>0 & 0 & 0 & l_{44}<br>\\end{bmatrix}\n$$\n\nEl producto de la multiplicación de las dos matrices se muestra a continuación:\n\n$$\nA=\\begin{bmatrix}<br>{l_{11}}^{2} & l_{21}l_{11} & l_{31}l_{11} & l_{41}l_{11}\\\\<br>l_{21}l_{11} & {l_{21}}^{2}+{l_{22}}^{2} & l_{31}l_{21}+l_{32}l_{22} & l_{41}l_{21}+l_{42}l_{22}\\\\<br>l_{31}l_{11} & l_{31}l_{21}+l_{32}l_{22} & {l_{31}}^{2}+{l_{32}}^{2}+{l_{33}}^{2} & l_{41}l_{31}+l_{42}l_{32}+l_{43}l_{33}\\\\<br>l_{41}l_{11} & l_{41}l_{21}+l_{42}l_{22} & l_{41}l_{31}+l_{42}l_{32}+l_{43}l_{33} & {l_{31}}^{2}+{l_{32}}^{2}+{l_{33}}^{2}+{l_{44}}^{2}<br>\\end{bmatrix}\n$$\n\nA partir de este punto, cada elemento es una ecuación de las cuales se deduce lo siguiente:\n\n$$\nl_{11}=\\sqrt{a_{11}}\n$$\n\n$$\nl_{21}=\\frac{a_{21}}{l_{11}}\n$$\n\n$$\nl_{31}=\\frac{a_{31}}{l_{11}}\n$$\n\n$$\nl_{41}=\\frac{a_{41}}{l_{11}}\n$$\n\n$$\nl_{22}=\\sqrt{a_{22}-{l_{21}}^{2}}\n$$\n\n$$\nl_{32}=\\frac{a_{32}-l_{21}l_{31}}{l_{22}}\n$$\n\n$$\nl_{42}=\\frac{a_{42}-l_{21}l_{41}}{l_{22}}\n$$\n\n$$\nl_{33}=\\sqrt{a_{33}-{l_{31}}^{2}-{l_{32}}^{2}}\n$$\n\n$$\nl_{43}=\\frac{a_{43}-l_{31}l_{41}-l_{32}l_{42}}{l_{33}}\n$$\n\n$$\nl_{44}=\\sqrt{a_{44}-{l_{41}}^{2}-{l_{42}}^{2}-{l_{43}}^{2}}\n$$\n\nUna característica (o limitación) de la descomposición Cholesky es que ***solo funciona en matrices positivas simétricas***.\n\nFinalmente, la regresión lineal mediante la descomposición Cholesky es análoga a los Mínimos Cuadrados Lineales, pero reducida a la resolución de un sistema de ecuaciones lineales.\n\n![](https://images.alexisalulema.com/blog/cholesky-decomposition-for-linear-regression-with-tensorflow/lser_03.png)\n\n![](https://images.alexisalulema.com/blog/cholesky-decomposition-for-linear-regression-with-tensorflow/loss_cholesky.png)\n\nLa descomposición Cholesky ya está implementada en TensorFlow, PyTorch o NumPy, sin embargo, en este link puedes revisar más detalles de [Descomposición Cholesky](https://rosettacode.org/wiki/Cholesky_decomposition).\n\nAhora, vamos a ver un ejemplo de su utilización:\n\n- [TensorFlow](#advgb-tabs-tab0)\n- [PyTorch](#advgb-tabs-tab1)\n- [Numpy](#advgb-tabs-tab2)\n\nTensorFlow\n\n```\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nimport numpy as np\n\n# Generate data\nx_vals = np.linspace(start=0, stop=10, num=100)\ny_vals = x_vals + np.random.normal(loc=0, scale=1, size=100)\n\nx_vals_column = np.transpose(np.matrix(x_vals))\nones_column = np.transpose(np.matrix(np.repeat(a=1, repeats=100)))\nX = np.column_stack((x_vals_column, ones_column))\nY = np.transpose(np.matrix(y_vals))\nX_tensor = tf.constant(X, dtype=tf.float64)\nY_tensor = tf.constant(Y, dtype=tf.float64)\n\n# Perform computations\ntX_X = tf.matmul(tf.transpose(X_tensor), X_tensor)\nL = tf.linalg.cholesky(tX_X)\ntX_Y = tf.matmul(tf.transpose(X_tensor), Y_tensor)\nsol1 = tf.linalg.solve(L, tX_Y)\nsol2 = tf.linalg.solve(tf.transpose(L), sol1)\n\n# Extract coefficients\nsolution_eval = sol2.numpy()\nm_slope = solution_eval[0][0]\nb_intercept = solution_eval[1][0]\n\nprint('slope (m): ' + str(m_slope))\nprint('intercept (b): ' + str(b_intercept))\n\n# Visualization\nbest_fit = []\nfor i in x_vals:\n    best_fit.append(m_slope * i + b_intercept)\n\nplt.plot(x_vals, y_vals, 'o', label='Data')\nplt.plot(x_vals, best_fit, 'r-', label='Linear Regression', linewidth=3)\nplt.legend(loc='upper left')\nplt.show()\n```\n\nPyTorch\n\n```\nimport matplotlib.pyplot as plt\nimport torch\nimport numpy as np\n\n# Generate data\nx_vals = np.linspace(start=0, stop=10, num=100)\ny_vals = x_vals + np.random.normal(loc=0, scale=1, size=100)\n\nx_vals_column = np.transpose(np.matrix(x_vals))\nones_column = np.transpose(np.matrix(np.repeat(a=1, repeats=100)))\nX = np.column_stack((x_vals_column, ones_column))\nY = np.transpose(np.matrix(y_vals))\nX_tensor = torch.tensor(X, dtype=torch.float64)\nY_tensor = torch.tensor(Y, dtype=torch.float64)\n\n# Perform computations\ntX_X = torch.mm(X_tensor.T, X_tensor)\nL = torch.cholesky(tX_X)\ntX_Y = torch.mm(X_tensor.T, Y_tensor)\nsol = torch.cholesky_solve(tX_Y, L)\n\n# Extract coefficients\nm_slope = sol[0][0].item()\nb_intercept = sol[1][0].item()\n\nprint('slope (m): ' + str(m_slope))\nprint('intercept (b): ' + str(b_intercept))\n\n# Visualization\nbest_fit = []\nfor i in x_vals:\n    best_fit.append(m_slope * i + b_intercept)\n\nplt.plot(x_vals, y_vals, 'o', label='Data')\nplt.plot(x_vals, best_fit, 'r-', label='Linear Regression', linewidth=3)\nplt.legend(loc='upper left')\nplt.show()\n```\n\nNumpy\n\n```\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n# Generate data\nx_vals = np.linspace(start=0, stop=10, num=100)\ny_vals = x_vals + np.random.normal(loc=0, scale=1, size=100)\n\nx_vals_column = np.transpose(np.matrix(x_vals))\nones_column = np.transpose(np.matrix(np.repeat(a=1, repeats=100)))\nX = np.column_stack((x_vals_column, ones_column))\nY = np.transpose(np.matrix(y_vals))\n\n# Perform computations using Cholesky Decomposition\ntX_X = np.dot(X.T, X)\nL = np.linalg.cholesky(tX_X)\ntX_Y = np.dot(X.T, Y)\nsol = np.linalg.solve(L, tX_Y)\nbeta = np.linalg.solve(L.T, sol)\n\n# Extract coefficients\nm_slope = beta[0,0]\nb_intercept = beta[1,0]\n\nprint('slope (m): ' + str(m_slope))\nprint('intercept (b): ' + str(b_intercept))\n\n# Visualization\nbest_fit = []\nfor i in x_vals:\n    best_fit.append(m_slope * i + b_intercept)\n\nplt.plot(x_vals, y_vals, 'o', label='Data')\nplt.plot(x_vals, best_fit, 'r-', label='Linear Regression', linewidth=3)\nplt.legend(loc='upper left')\nplt.show()\n```\n\nslope (m): 1.0830263227926582  \nintercept (b): -0.3348165868955632\n\n![](https://images.alexisalulema.com/blog/cholesky-decomposition-for-linear-regression-with-tensorflow/linear_regression.png)\n\nComo se puede observar, esta solución es muy similar a la Regresión Lineal de Mínimos Cuadrados, pero esta descomposición es a veces mucho más eficiente y numéricamente estable."},{"slug":"temas-avanzados-de-asyncio-mas-alla-de-lo-basico","lang":"es","title":"Temas Avanzados de Asyncio: Más Allá de lo Básico","description":"Introducción ¡Hola, entusiastas de asyncio! Si han estado siguiendo mis publicaciones, recordarán que recientemente nos sumergimos en los conceptos básicos de la biblioteca asyncio de Python en mi ent…","publishDate":"2023-09-25","updatedDate":null,"tags":["asynchronous-programming","python"],"translationSlug":"advanced-asyncio-topics-beyond-the-basics","url":"https://alexisalulema.com/es/blog/temas-avanzados-de-asyncio-mas-alla-de-lo-basico/","series":null,"headings":[{"depth":3,"slug":"introducción","text":"Introducción"},{"depth":3,"slug":"primitivas-de-sincronización-en-asyncio-dominando-el-control-de-concurrencia","text":"Primitivas de Sincronización en Asyncio: Dominando el Control de Concurrencia"},{"depth":3,"slug":"colas-en-asyncio-la-columna-vertebral-de-la-gestión-de-datos","text":"Colas en Asyncio: La Columna Vertebral de la Gestión de Datos"},{"depth":3,"slug":"semáforos-en-asyncio-gestión-de-límites-de-tasa-y-asignación-de-recursos","text":"Semáforos en Asyncio: Gestión de Límites de Tasa y Asignación de Recursos"},{"depth":3,"slug":"transportes-y-protocolos-los-bloques-de-construcción-de-la-comunicación-en-red","text":"Transportes y Protocolos: Los Bloques de Construcción de la Comunicación en Red"},{"depth":3,"slug":"flujos-en-asyncio-un-enfoque-de-alto-nivel-para-la-comunicación-en-red","text":"Flujos en Asyncio: Un Enfoque de Alto Nivel para la Comunicación en Red"},{"depth":3,"slug":"grupos-de-tareas-en-asyncio-simplificando-la-gestión-de-tareas-en-python-311","text":"Grupos de Tareas en Asyncio: Simplificando la Gestión de Tareas en Python 3.11+"},{"depth":3,"slug":"websockets-y-http2-aprovechando-asyncio-para-protocolos-de-red-avanzados","text":"Websockets y HTTP2: Aprovechando Asyncio para Protocolos de Red Avanzados"},{"depth":3,"slug":"probando-código-asíncrono-asegurando-la-confiabilidad-con-pytest-asyncio","text":"Probando Código Asíncrono: Asegurando la Confiabilidad con pytest-asyncio"},{"depth":3,"slug":"pruebas-básicas-con-pytest-asyncio","text":"Pruebas Básicas con pytest-asyncio"},{"depth":3,"slug":"conclusiones-la-punta-del-iceberg-en-las-capacidades-de-asyncio","text":"Conclusiones: La Punta del Iceberg en las Capacidades de Asyncio"},{"depth":3,"slug":"final","text":"Final"}],"body":"### Introducción\n\n¡Hola, entusiastas de asyncio! Si han estado siguiendo mis publicaciones, recordarán que recientemente nos sumergimos en los conceptos básicos de la biblioteca asyncio de Python en mi [entrada de blog anterior](https://www.alexisalulema.com/es/2023/09/11/tutorial-asyncio-para-el-programador-con-el-tiempo-en-contra/). Exploramos los fundamentos como corrutinas, tareas y bucles de eventos, e incluso tocamos algunas de las mejores prácticas y técnicas de depuración. Pero como prometí, no nos detendremos ahí. Es hora de aventurarnos en los territorios más avanzados de asyncio.\n\nEn la publicación de hoy, cubriré algunos de los aspectos más intrincados de la programación asíncrona en Python. Profundizaremos en primitivas de sincronización, exploraremos la utilidad de Colas y Semáforos, e incluso ensuciaremos nuestras manos con Transportes y Protocolos. Estas son las herramientas y conceptos que pueden ayudarte a gestionar flujos de trabajo complejos, coordinar entre diferentes tareas y liberar verdaderamente el poder de asyncio.\n\nAsí que, si estás listo para elevar tu juego con asyncio, sigue leyendo mientras diseccionamos estos temas avanzados, acompañados de ejemplos prácticos para ayudarte a comprender mejor los conceptos.\n\n### Primitivas de Sincronización en Asyncio: Dominando el Control de Concurrencia\n\nA medida que te adentras más en asyncio, descubrirás que la gestión de tareas concurrentes puede complicarse. Aunque asyncio es excelente para manejar operaciones limitadas por E/S de forma concurrente, hay escenarios en los que necesitas controlar el orden de ejecución de tus corrutinas. Aquí es donde entran en juego las primitivas de sincronización.\n\n#### ¿Qué son las Primitivas de Sincronización?\n\nEn el contexto de asyncio, las primitivas de sincronización son constructos diseñados para coordinar la ejecución de múltiples corrutinas. Son particularmente útiles cuando tienes recursos o estados compartidos que varias corrutinas necesitan acceder. Las primitivas de sincronización más comúnmente utilizadas en asyncio son:\n\n1. **Candados (Locks)**: Evitan que múltiples corrutinas accedan a un recurso compartido simultáneamente.\n2. **Eventos (Events)**: Notifican a múltiples corrutinas que ha ocurrido un evento en particular.\n3. **Condiciones (Conditions)**: Una forma más avanzada de sincronización que combina Candados y Eventos.\n4. **Semáforos (Semaphores)**: Limitan el número de corrutinas que pueden acceder a una sección particular del código.\n\n#### Utilizando Candados (Locks) en Asyncio\n\nLos candados quizás sean la forma más sencilla de sincronización. Aquí tienes un ejemplo rápido:\n\n```\nimport asyncio\n\nlock = asyncio.Lock()\n\nasync def my_coroutine(id):\n    async with lock:\n        print(f\"Coroutine {id} has acquired the lock\")\n        await asyncio.sleep(1)\n        print(f\"Coroutine {id} has released the lock\")\n\nasync def main():\n    tasks = [my_coroutine(i) for i in range(3)]\n    await asyncio.gather(*tasks)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, creamos un candado utilizando `asyncio.Lock()`. La función `my_coroutine` adquiere el candado usando `async with lock:` antes de proceder con su tarea. Esto asegura que solo una corrutina pueda ejecutar el bloque de código bajo el candado en un momento dado.\n\n#### Utilizando Eventos (Events) en Asyncio\n\nLos eventos son otra primitiva de sincronización útil. Te permiten pausar una corrutina hasta que ocurra un evento determinado.\n\n```\nimport asyncio\n\nevent = asyncio.Event()\n\nasync def waiter():\n    print(\"Waiting for the event to be set\")\n    await event.wait()\n    print(\"The event was set, proceeding\")\n\nasync def setter():\n    await asyncio.sleep(1)\n    print(\"Setting the event\")\n    event.set()\n\nasync def main():\n    await asyncio.gather(waiter(), setter())\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, la corrutina `waiter` esperará hasta que la corrutina `setter` establezca el evento.\n\n#### Utilizando Condiciones (Conditions) en Asyncio\n\nLas condiciones son un poco más complejas y se utilizan cuando una corrutina debe esperar a que se cumpla una condición en particular.\n\n```\nimport asyncio\n\ncondition = asyncio.Condition()\n\nasync def consumer():\n    async with condition:\n        print(\"Consumer waiting\")\n        await condition.wait()\n        print(\"Consumer triggered\")\n        # Do something\n\nasync def producer():\n    await asyncio.sleep(1)\n    async with condition:\n        print(\"Producer ready\")\n        condition.notify_all()\n\nasync def main():\n    await asyncio.gather(consumer(), producer())\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nAquí, la corrutina `consumer` espera a que se cumpla una condición. La corrutina `producer` establece la condición, permitiendo que `consumer` continúe.\n\nTanto Event como Condition son primitivas de sincronización en asyncio que permiten la coordinación entre corrutinas, pero sirven para diferentes propósitos y se utilizan en diferentes escenarios. Aquí hay un desglose de las diferencias:\n\n##### Evento (Event)\n\nUn Evento es una primitiva de sincronización simple que se utiliza para señalar a múltiples corrutinas que se ha cumplido alguna condición, permitiéndoles continuar. Una vez que se establece el evento, todas las corrutinas en espera se despiertan. Los eventos se utilizan principalmente para señales únicas y no llevan ninguna información o estado adicional aparte de estar establecidos o despejados.\n\n##### Condición (Condition)\n\nUna Condición es más compleja y a menudo se utiliza para señalizaciones más específicas entre corrutinas. Combina la funcionalidad de un Evento y un Candado. Las condiciones generalmente se utilizan para señalar cambios de estado que pueden requerir que las corrutinas tomen diferentes acciones, no solo continuar. Varias condiciones pueden compartir el mismo candado subyacente, permitiendo una coordinación más compleja.\n\n#### Diferencias Clave\n\n1. **Propósito:** El Evento generalmente se utiliza para señalizaciones simples, mientras que la Condición se utiliza para escenarios más complejos que involucran múltiples cambios de estado.\n2. **Bloqueo:** La Condición tiene un candado asociado, lo que permite una coordinación más compleja entre corrutinas. El Evento no tiene esta característica.\n3. **Notificación:** La Condición te permite notificar solo a un número específico de corrutinas en espera, mientras que el Evento notificará a todas las corrutinas en espera cuando se establezca.\n4. **Gestión del Estado:** La Condición a menudo se utiliza cuando el estado de un objeto cambia y necesitas notificar a otras corrutinas, posiblemente para volver a comprobar una condición. El Evento es más para una señal única para continuar.\n\nEn resumen, aunque el código para usarlos pueda parecer similar, Evento y Condición se utilizan para diferentes tipos de problemas de coordinación. Elige el que mejor se adapte a los requisitos específicos de tu aplicación.\n\n#### ¿Por Qué Utilizar Primitivas de Sincronización?\n\nPodrías preguntarte, \"¿Por qué necesito estas primitivas si asyncio se trata de concurrencia?\" La respuesta radica en la complejidad de las aplicaciones del mundo real. Cuando tienes múltiples corrutinas que dependen de algún estado compartido o necesitan ser ejecutadas en un orden específico, las primitivas de sincronización se vuelven indispensables.\n\n#### Mejores Prácticas\n\n- Siempre libera los candados y otros recursos que adquieras.\n- Utiliza tiempos de espera donde sea aplicable para evitar interbloqueos.\n- Prefiere primitivas de sincronización de nivel superior como Colas para problemas de productor-consumidor.\n\n### Colas en Asyncio: La Columna Vertebral de la Gestión de Datos\n\nDespués de adentrarnos profundamente en las primitivas de sincronización, cambiemos nuestro enfoque hacia otra característica poderosa en asyncio: las Colas. Las Colas son la solución predilecta para gestionar datos entre múltiples corrutinas, especialmente en escenarios de productor-consumidor.\n\n#### ¿Qué son las Colas?\n\nEn asyncio, una Cola es una estructura de datos simple pero poderosa que te permite almacenar y recuperar elementos de manera primero en entrar, primero en salir (FIFO, por sus siglas en inglés). Las Colas son seguras para hilos y están diseñadas para ser utilizadas con la sintaxis async/await, lo que las hace ideales para gestionar datos entre múltiples corrutinas.\n\n#### Uso Básico de las Colas\n\nAquí tienes un ejemplo sencillo que demuestra una relación de productor-consumidor utilizando Colas de asyncio:\n\n```\nimport asyncio\n\nasync def producer(queue):\n    for i in range(5):\n        print(f\"Producing item {i}\")\n        await queue.put(i)\n        await asyncio.sleep(1)\n\nasync def consumer(queue):\n    while True:\n        item = await queue.get()\n        print(f\"Consuming item {item}\")\n        queue.task_done()\n\nasync def main():\n    queue = asyncio.Queue()\n    await asyncio.gather(producer(queue), consumer(queue))\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, la corrutina `producer` pone items en la cola, miestras que la corrutina `consumer` los saca para su procesamiento. El método `queue.task_done()` indica que una tarea previamente encolada se ha completado.\n\n#### Uso Avanzado: Colas de Prioridad y Colas LIFO\n\nAsyncio también proporciona tipos especializados de colas como `PriorityQueue` y `LifoQueue` (Último en entrar, primero en salir). Estos pueden ser útiles para requisitos de gestión de datos más complejos.\n\n#### ¿Por Qué Utilizar Colas?\n\n1. **Integridad de Datos**: Las colas aseguran que los datos se procesen en el orden en que se agregaron, manteniendo la integridad de los datos.\n2. **Gestión de Recursos**: Las colas se pueden utilizar para gestionar recursos limitados como conexiones de bases de datos o límites de tasa de API.\n3. **Desacoplamiento**: Las colas desacoplan a los productores de datos de los consumidores, lo que facilita el mantenimiento y la escalabilidad de tu aplicación.\n\n#### Mejores Prácticas\n\n- Utiliza el parámetro `maxsize` para establecer un límite en el tamaño de la cola y evitar el desbordamiento de memoria.\n- Siempre utiliza `await queue.join()` para asegurarte de que todas las tareas se completen antes de cerrar el bucle de eventos.\n\n#### Ejemplo del Mundo Real: Web Crawler\n\nImagina que estás construyendo un web crawler. Podrías tener una corrutina responsable de buscar URLs y colocarlas en una cola. Otra corrutina podría ser responsable de tomar URLs de la cola, descargar el contenido y realizar algún análisis. De esta manera, las partes de búsqueda y análisis de tu aplicación están desacopladas, lo que facilita su mantenimiento y escalabilidad.\n\n```\nimport asyncio\n\nasync def fetch_urls(queue):\n    urls = [\"https://en.wikipedia.org/wiki/Americas\", \"https://en.wikipedia.org/wiki/Asia\"]\n    for url in urls:\n        print(f\"Fetching {url}\")\n        await queue.put(url)\n        await asyncio.sleep(1)\n\nasync def analyze_content(queue):\n    while True:\n        url = await queue.get()\n        print(f\"Analyzing content from {url}\")\n        queue.task_done()\n\nasync def main():\n    queue = asyncio.Queue()\n    await asyncio.gather(fetch_urls(queue), analyze_content(queue))\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Semáforos en Asyncio: Gestión de Límites de Tasa y Asignación de Recursos\n\nDespués de haber cubierto las primitivas de sincronización y las Colas, dirijamos ahora nuestra atención a los Semáforos. Los Semáforos son otra forma de primitiva de sincronización pero con un propósito diferente: se utilizan para limitar el número de corrutinas que pueden acceder a una sección particular del código.\n\n#### ¿Qué son los Semáforos?\n\nEn asyncio, un Semáforo es esencialmente un contador con un valor inicial que tú estableces. Cuando una corrutina adquiere el Semáforo, el contador se decrementa. Cuando el contador llega a cero, ninguna corrutina más puede adquirir el Semáforo hasta que una de las existentes lo libere, incrementando el contador de nuevo.\n\n#### Uso Básico de los Semáforos\n\nAquí tienes un ejemplo sencillo para demostrar cómo se pueden utilizar los Semáforos para limitar el acceso concurrente:\n\n```\nimport asyncio\n\nsem = asyncio.Semaphore(2)\n\nasync def my_coroutine(id):\n    async with sem:\n        print(f\"Coroutine {id} has acquired the semaphore\")\n        await asyncio.sleep(2)\n        print(f\"Coroutine {id} has released the semaphore\")\n\nasync def main():\n    tasks = [my_coroutine(i) for i in range(4)]\n    await asyncio.gather(*tasks)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, creamos un Semáforo con un valor inicial de 2, lo que significa que solo dos corrutinas pueden adquirirlo al mismo tiempo. La función `my_coroutine` adquiere el Semáforo usando `async with sem:` antes de proceder con su tarea.\n\n#### ¿Por Qué Utilizar Semáforos?\n\n1. **Limitación de Tasa:** Si estás realizando llamadas a API o haciendo web scraping, puedes usar Semáforos para limitar la tasa a la que se realizan las solicitudes.\n2. **Asignación de Recursos:** En escenarios donde tienes recursos limitados como conexiones de bases de datos, los Semáforos pueden ayudar a gestionar esos recursos de manera efectiva.\n\n#### Uso Avanzado: Web Scraping con Limitación de Tasa\n\nConsideremos un ejemplo del mundo real donde los Semáforos pueden ser increíblemente útiles: web scraping con limitación de tasa.\n\n```\nimport asyncio\n\nsem = asyncio.Semaphore(2)  # Limit to 2 concurrent requests\n\nasync def fetch_page(url):\n    async with sem:\n        print(f\"Fetching {url}\")\n        await asyncio.sleep(1)  # Simulate network delay\n        print(f\"Finished fetching {url}\")\n\nasync def main():\n    urls = [\"https://en.wikipedia.org/wiki/Americas\", \"https://en.wikipedia.org/wiki/Asia\", \"https://en.wikipedia.org/wiki/Europe\", \"https://en.wikipedia.org/wiki/Africa\"]\n    tasks = [fetch_page(url) for url in urls]\n    await asyncio.gather(*tasks)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, utilizamos un Semáforo para limitar el número de solicitudes concurrentes a 3. Esto asegura que no abrumemos al servidor mientras hacemos el web scraping.\n\n#### Mejores Prácticas\n\n- Siempre libera el Semáforo después de usarlo para liberar recursos.\n- Ten cuidado con los interbloqueos. Asegúrate de que cada adquisición esté emparejada con una liberación.\n\n### Transportes y Protocolos: Los Bloques de Construcción de la Comunicación en Red\n\nDespués de discutir las primitivas de sincronización, Colas y Semáforos, es hora de adentrarnos en las abstracciones de nivel inferior que ofrece asyncio: Transportes y Protocolos. Estos son los bloques de construcción que proporcionan más control sobre la comunicación en red, permitiéndote manejar casos de uso que no se cubren fácilmente con abstracciones de nivel superior.\n\n#### ¿Qué son los Transportes y Protocolos?\n\n- **Transportes:** Estos son responsables de manejar las operaciones de E/S reales, como leer o escribir en la red.\n- **Protocolos:** Estos definen las reglas para analizar los datos entrantes y formatear los datos salientes. Trabajan en conjunto con los Transportes para manejar la comunicación en red.\n\n#### Uso Básico de Transportes y Protocolos\n\nAquí tienes un ejemplo sencillo de un servidor de eco TCP utilizando Transportes y Protocolos:\n\n```\nimport asyncio\n\nclass EchoProtocol(asyncio.Protocol):\n    def connection_made(self, transport):\n        self.transport = transport\n\n    def data_received(self, data):\n        self.transport.write(data)\n\nasync def main():\n    loop = asyncio.get_running_loop()\n    server = await loop.create_server(EchoProtocol, '127.0.0.1', 8888)\n    await server.serve_forever()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, definimos una clase `EchoProtocol` que hereda de `asyncio.Protocol`. El método `connection_made` se llama cuando se establece una nueva conexión, y `data_received` se llama cada vez que se recibe datos del cliente. Puedes probar este servidor ejecutando telnet:\n\n```\nalexis % telnet 127.0.0.1 8888\nTrying 127.0.0.1...\nConnected to localhost.\n```\n\n#### ¿Por qué Usar Transportes y Protocolos?\n\n1. **Control Detallado:** Ofrecen más control sobre la capa de red, permitiéndote optimizar para casos de uso específicos.\n2. **Implementación de Protocolo:** Si estás trabajando con un protocolo de red personalizado o menos común, puedes implementarlo utilizando estas abstracciones de nivel inferior.\n3. **Gestión de Recursos:** El uso efectivo de Transportes y Protocolos puede llevar a una utilización más eficiente de los recursos.\n\n#### Caso de Uso Avanzado: Implementación de Protocolo Personalizado\n\nCrearemos un protocolo binario personalizado simple que espera datos en el formato de un entero de 4 bytes seguido de una cadena. El servidor leerá el entero, luego leerá la cadena de esa longitud y la devolverá al cliente.\n\nAquí está el código del servidor usando `CustomProtocol`:\n\n```\nimport asyncio\nimport struct\n\nclass CustomProtocol(asyncio.Protocol):\n    def connection_made(self, transport):\n        self.transport = transport\n\n    def data_received(self, data):\n        parsed_data = self.parse_custom_protocol(data)\n        self.transport.write(parsed_data)\n\n    def parse_custom_protocol(self, data):\n        # Unpack a 4-byte integer from the beginning of data\n        str_len = struct.unpack(\"!I\", data[:4])[0]\n        \n        # Extract the string of the given length\n        received_str = data[4: 4 + str_len].decode('utf-8')\n        \n        print(f\"Received string: {received_str}\")\n        \n        # For demonstration, just echo back the received string\n        return data\n\nasync def main():\n    loop = asyncio.get_running_loop()\n    server = await loop.create_server(CustomProtocol, '127.0.0.1', 8888)\n    await server.serve_forever()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nAhora, creemos un cliente para probar este servidor. El cliente enviará un entero de 4 bytes indicando la longitud de la cadena, seguido de la cadena en misma:\n\n```\nimport asyncio\nimport struct\n\nasync def custom_protocol_client():\n    reader, writer = await asyncio.open_connection('127.0.0.1', 8888)\n    \n    # Prepare data: 4-byte length followed by the string\n    test_str = \"Hello, World!\"\n    data = struct.pack(\"!I\", len(test_str)) + test_str.encode('utf-8')\n    \n    writer.write(data)\n    await writer.drain()\n    \n    # Read echoed data\n    received_data = await reader.read(100)\n    \n    # Close the connection\n    writer.close()\n    await writer.wait_closed()\n\n    # Unpack received data\n    str_len = struct.unpack(\"!I\", received_data[:4])[0]\n    received_str = received_data[4: 4 + str_len].decode('utf-8')\n    \n    print(f\"Received echoed string: {received_str}\")\n\nasync def main():\n    await custom_protocol_client()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n#### Mejores Prácticas\n\n- Siempre cierra el Transporte cuando hayas terminado para liberar recursos.\n- Implementa un manejo de errores adecuado en tus métodos de Protocolo para lidiar con problemas de red o datos mal formados.\n\n### Flujos en Asyncio: Un Enfoque de Alto Nivel para la Comunicación en Red\n\nDespués de explorar las abstracciones de nivel inferior de Transportes y Protocolos, podrías estar preguntándote si hay una manera más sencilla de manejar la comunicación en red en asyncio. La respuesta es sí, y viene en forma de Flujos (Streams) — una alternativa de alto nivel que proporciona una interfaz más fácil para leer y escribir datos a través de la red.\n\n#### ¿Qué son los Flujos?\n\nLos Flujos en asyncio proporcionan un conjunto de APIs de alto nivel para trabajar con E/S de red. Abstraen las complejidades de los Transportes y Protocolos, ofreciendo una interfaz más simple y más \"Pythonic\". Los Flujos se dividen en dos tipos principales:\n\n1. **StreamReader**: Para leer datos de una conexión de red.\n2. **StreamWriter**: Para escribir datos a una conexión de red.\n\n#### Uso Básico de Flujos\n\nAquí tienes un ejemplo sencillo de un cliente de eco TCP utilizando Flujos de asyncio:\n\n```\nimport asyncio\n\nasync def echo_client():\n    reader, writer = await asyncio.open_connection('127.0.0.1', 8888)\n    \n    writer.write(b'Hello, World!')\n    await writer.drain()\n    \n    data = await reader.read(100)\n    print(f\"Received: {data.decode()}\")\n    \n    writer.close()\n    await writer.wait_closed()\n\nasync def main():\n    await echo_client()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, `asyncio.open_connection` retorna un StreamReader (`reader`) y un StreamWriter (`writer`). Usamos `writer.write()` para enviar datos y `reader.read()` para recibir datos.\n\n#### ¿Por qué Usar Flujos?\n\n1. **Sencillez:** Los Flujos ofrecen una API más simple e intuitiva en comparación con los Transportes y Protocolos.\n2. **Legibilidad:** El código que utiliza Flujos suele ser más fácil de leer y mantener.\n3. **Flexibilidad:** Aunque son de alto nivel, los Flujos aún ofrecen un buen grado de control sobre la comunicación en red.\n\n#### Caso de Uso Avanzado: Construyendo un Servidor de Chat\n\nImagina construir un servidor de chat simple donde múltiples clientes pueden enviar y recibir mensajes. Podrías usar Flujos para gestionar fácilmente la conexión de cada cliente.\n\n```\nimport asyncio\n\nasync def handle_client(reader, writer):\n    while True:\n        data = await reader.read(100)\n        if not data:\n            break\n        writer.write(data)\n        await writer.drain()\n\nasync def main():\n    server = await asyncio.start_server(handle_client, '127.0.0.1', 8888)\n    await server.serve_forever()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEste cliente se conectará al servidor, enviará mensajes escritos por el usuario y mostrará mensajes recibidos del servidor:\n\n```\nimport asyncio\n\nasync def read_from_server(reader):\n    while True:\n        data = await reader.read(100)\n        if not data:\n            print(\"Server disconnected\")\n            break\n        print(f\"Received: {data.decode()}\")\n\nasync def write_to_server(writer):\n    while True:\n        message = input(\"Send message: \")\n        writer.write(message.encode())\n        await writer.drain()\n\nasync def chat_client():\n    reader, writer = await asyncio.open_connection('127.0.0.1', 8888)\n    \n    asyncio.create_task(read_from_server(reader))\n    asyncio.create_task(write_to_server(writer))\n\n    await asyncio.gather(\n        read_from_server(reader),\n        write_to_server(writer)\n    )\n\n    writer.close()\n    await writer.wait_closed()\n\nasync def main():\n    await chat_client()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nPara probar el servidor de chat:\n\n1. Ejecuta el código del servidor de chat.\n2. Ejecuta el código del cliente de chat.\n\nPuedes abrir múltiples terminales y ejecutar múltiples instancias del cliente para simular un entorno de chat. Cada cliente hará eco de cualquier mensaje que envíe al servidor, según la implementación actual de tu servidor.\n\nEn este ejemplo, `handle_client` es una corrutina que maneja la comunicación para cada cliente conectado. Lee datos del lector y los escribe de nuevo usando el escritor, haciendo eco efectivamente de los mensajes recibidos.\n\n#### Mejores Prácticas\n\n- Siempre cierra el StreamWriter usando `writer.close()` para liberar recursos.\n- Utiliza `await writer.drain()` para asegurarte de que todos los datos en búfer se envíen.\n\n### Grupos de Tareas en Asyncio: Simplificando la Gestión de Tareas en Python 3.11+\n\nSi estás utilizando Python 3.11 o una versión más reciente, tienes acceso a una poderosa característica que simplifica la gestión de múltiples tareas: Grupos de Tareas. Este tema avanzado es particularmente útil para manejar cancelaciones de tareas y excepciones de una manera más organizada.\n\n#### ¿Qué son los Grupos de Tareas?\n\nLos Grupos de Tareas son una nueva adición a asyncio en Python 3.11 que te permiten gestionar el ciclo de vida de múltiples tareas como una sola unidad. Con Grupos de Tareas, puedes iniciar, cancelar o esperar la finalización de múltiples tareas juntas, haciendo que tu código sea más limpio y manejable.\n\n#### Uso Básico de Grupos de Tareas\n\nAquí tienes un ejemplo sencillo para demostrar el uso de Grupos de Tareas:\n\n```\nimport asyncio\n\nasync def my_task(name):\n    print(f\"Task {name} started\")\n    await asyncio.sleep(1)\n    print(f\"Task {name} completed\")\n\nasync def main():\n    async with asyncio.TaskGroup() as tg:\n        tg.create_task(my_task(\"A\"))\n        tg.create_task(my_task(\"B\"))\n        tg.create_task(my_task(\"C\"))\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, creamos un Grupo de Tareas usando `asyncio.TaskGroup()` y luego añadimos tareas a él usando `tg.create_task()`. Todas las tareas en el Grupo de Tareas serán esperadas al salir del bloque `async with`.\n\n#### ¿Por qué Usar Grupos de Tareas?\n\n1. **Manejo Simplificado de Errores:** Si alguna tarea en el grupo genera una excepción, se propaga inmediatamente y todas las otras tareas son canceladas.\n2. **Cancelación de Tareas:** Al cancelar el Grupo de Tareas se cancelan todas las tareas dentro de él, facilitando la gestión del ciclo de vida de las tareas.\n3. **Organización del Código:** Los Grupos de Tareas ayudan a organizar tareas relacionadas juntas, haciendo que el código sea más fácil de leer y mantener.\n\n#### Caso de Uso Avanzado: Web Scraping con Manejo de Errores\n\nConsidera un escenario de web scraping donde estás obteniendo datos de múltiples URLs. Si alguna de las operaciones de obtención falla, quieres cancelar todas las otras obtenciones en curso y manejar la excepción de manera adecuada.\n\n```\nimport asyncio\n\nasync def fetch_url(url):\n    print(f\"Fetching {url}\")\n    await asyncio.sleep(1)  # Simulate network delay\n    if \"bad\" in url:\n        raise ValueError(f\"Bad URL: {url}\")\n    print(f\"Fetched {url}\")\n\nasync def main():\n    async with asyncio.TaskGroup() as tg:\n        tg.create_task(fetch_url(\"https://en.wikipedia.org/wiki/Americas\"))\n        tg.create_task(fetch_url(\"bad://failx.test\"))\n        tg.create_task(fetch_url(\"https://en.wikipedia.org/wiki/Europa\"))\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(main())\n    except ValueError as e:\n        print(f\"An error occurred: {e}\")\n```\n\nEn este ejemplo, al intentar obtener la URL \"mala\" se genera un `ValueError`. El Grupo de Tareas asegura que todas las otras tareas sean canceladas y que la excepción se propague.\n\n#### Mejores Prácticas\n\n- Utiliza Grupos de Tareas para tareas lógicamente relacionadas que deben ser gestionadas juntas.\n- Ten cuidado con el manejo de excepciones dentro de los Grupos de Tareas para asegurarte de que los errores se detecten y se manejen adecuadamente.\n\n### Websockets y HTTP2: Aprovechando Asyncio para Protocolos de Red Avanzados\n\nA medida que te sumerges más profundamente en el mundo de asyncio, descubrirás que no se limita solo a solicitudes HTTP básicas o protocolos TCP/UDP simples. Asyncio también puede ser utilizado para trabajar con protocolos de red más avanzados como Websockets y HTTP/2, habilitando capacidades de comunicación en tiempo real y multiplexación.\n\n#### Websockets con Asyncio\n\nLos Websockets proporcionan un canal de comunicación full-duplex sobre una única conexión de larga duración, lo que los hace ideales para aplicaciones en tiempo real como salas de chat, juegos en línea y actualizaciones en vivo.\n\n##### Uso Básico de Websockets\n\nAquí tienes un ejemplo sencillo de un servidor Websocket utilizando asyncio y la biblioteca websockets:\n\n```\nimport asyncio\nimport websockets\n\nasync def echo(websocket, path):\n    async for message in websocket:\n        await websocket.send(f\"Echo: {message}\")\n\nasync def main():\n    server = await websockets.serve(echo, \"localhost\", 8765)\n    await server.wait_closed()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEl siguiente cliente se conectará al servidor, enviará un mensaje y luego imprimirá el mensaje que recibe de vuelta.\n\n```\nimport asyncio\nimport websockets\n\nasync def websocket_client():\n    uri = \"ws://localhost:8765\"\n    async with websockets.connect(uri) as websocket:\n        await websocket.send(\"Hello, Server!\")\n        print(\"Sent: Hello, Server!\")\n\n        response = await websocket.recv()\n        print(f\"Received: {response}\")\n\nasync def main():\n    await websocket_client()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, la corrutina `echo` escucha los mensajes entrantes y los devuelve al cliente.\n\n#### HTTP/2 con Asyncio\n\nHTTP/2 es la segunda versión principal del protocolo HTTP, ofreciendo características como la compresión de encabezados y la multiplexación de múltiples solicitudes en una sola conexión.\n\n##### Usando HTTP/2 con httpx\n\nPuedes usar la biblioteca `httpx`, que es compatible con asyncio, para hacer solicitudes HTTP/2:\n\n```\nimport httpx\nimport asyncio\n\nasync def fetch_data():\n    async with httpx.AsyncClient(http2=True) as client:\n        response = await client.get('https://www.example.com')\n        print(response.status_code)\n\nasync def main():\n    await fetch_data()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, creamos un cliente HTTP asíncrono con soporte para HTTP/2 y obtenemos datos de un sitio web.\n\n#### ¿Por qué usar Protocolos Avanzados?\n\n1. **Comunicación en Tiempo Real:** Los Websockets permiten una comunicación bidireccional en tiempo real entre el servidor y el cliente.\n2. **Eficiencia:** Las características de multiplexación de HTTP/2 reducen la latencia y mejoran la velocidad.\n3. **Aplicaciones Modernas:** Muchos servicios web modernos y APIs se están inclinando hacia estos protocolos avanzados para obtener un mejor rendimiento y capacidades.\n\n#### Caso de Uso Avanzado: Ticker de Acciones en Tiempo Real\n\nImagina que estás construyendo una aplicación de ticker de acciones en tiempo real. Podrías usar Websockets para enviar actualizaciones al cliente cada vez que cambie el precio de una acción.\n\n```\nimport asyncio\nimport websockets\nimport json\n\nasync def stock_ticker(websocket, path):\n    while True:\n        stock_data = {\"AAPL\": 150.00, \"GOOGL\": 2800.00}  # Simulated stock data\n        await websocket.send(json.dumps(stock_data))\n        await asyncio.sleep(1)\n\nasync def main():\n    server = await websockets.serve(stock_ticker, \"localhost\", 8765)\n    await server.wait_closed()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEl siguiente cliente se conectará al servidor, recibirá actualizaciones de datos de acciones e imprimirá dichas actualizaciones.\n\n```\nimport asyncio\nimport websockets\nimport json\n\nasync def stock_ticker_client():\n    uri = \"ws://localhost:8765\"\n    async with websockets.connect(uri) as websocket:\n        while True:\n            response = await websocket.recv()\n            stock_data = json.loads(response)\n            print(f\"Received stock data: {stock_data}\")\n\nasync def main():\n    await stock_ticker_client()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEn este ejemplo, la corrutina `stock_ticker` envía una carga útil en JSON con los precios de las acciones al cliente cada segundo.\n\n#### Mejores Prácticas\n\n- Utiliza bibliotecas diseñadas para trabajar con asyncio para obtener un mejor rendimiento y una integración más sencilla.\n- Siempre maneja excepciones y casos límite para hacer tu aplicación robusta, especialmente cuando tratas con datos en tiempo real o conexiones multiplexadas.\n\n### Probando Código Asíncrono: Asegurando la Confiabilidad con `pytest-asyncio`\n\nA medida que construyes aplicaciones asyncio más complejas, la importancia de las pruebas no puede ser subestimada. Las pruebas aseguran que tu código se comporte como se espera y facilitan la adición de nuevas características sin romper la funcionalidad existente. En esta sección, exploraremos cómo probar código Python asíncrono, centrándonos en la biblioteca pytest-asyncio.\n\n#### Por qué es Importante Probar el Código Asíncrono\n\nProbar el código asíncrono es crucial por varias razones:\n\n1. **Confiabilidad:** Asegura que tus funciones asíncronas y corrutinas funcionen como se espera bajo diferentes condiciones.\n2. **Mantenibilidad:** Facilita la refactorización y la adición de nuevas características.\n3. **Colaboración:** El código bien probado es más fácil de entender y aportar para otros desarrolladores.\n\n### Pruebas Básicas con `pytest-asyncio`\n\nEl paquete `pytest-asyncio` extiende el popular marco de pruebas `pytest` para manejar corrutinas asyncio. Para comenzar, necesitarás instalarlo:\n\n```\npip install pytest-asyncio\n-- or --\nconda install -c conda-forge pytest-asyncio\n```\n\nAquí tienes un ejemplo sencillo que prueba una función asíncrona:\n\n```\nimport pytest\nimport asyncio\n\nasync def my_async_function(x):\n    await asyncio.sleep(1)\n    return x * 2\n\n@pytest.mark.asyncio\nasync def test_my_async_function():\n    result = await my_async_function(2)\n    assert result == 4\n```\n\nEn este ejemplo, utilizamos el decorador `@pytest.mark.asyncio` para marcar la prueba como asíncrona. Luego, esperamos la corrutina `my_async_function` y afirmamos que devuelve el resultado esperado.\n\n#### Técnicas Avanzadas: Simulación y Parametrización\n\n##### Simulando Funciones Asíncronas\n\nPuedes usar la biblioteca `unittest.mock` para simular funciones asíncronas:\n\n```\nimport pytest\nimport asyncio\nimport sys\nfrom unittest.mock import patch\n\nasync def my_async_function(x):\n    await asyncio.sleep(1)  # Simulate some async operation\n    return x * 2\n\n@pytest.mark.asyncio\nasync def test_with_mock():\n    current_module = sys.modules[__name__]\n    with patch.object(current_module, 'my_async_function', return_value=4):\n        result = await my_async_function(2)\n        assert result == 4\n```\n\nEn este ejemplo, utilizamos el decorador `@pytest.mark.asyncio` para marcar la prueba como asíncrona. Luego, esperamos la corrutina `my_async_function` y comprobamos que devuelve el resultado esperado.\n\n#### Técnicas Avanzadas: Simulación y Parametrización\n\n##### Simulando Funciones Asíncronas\n\nPuedes usar la biblioteca `unittest.mock` para simular funciones asíncronas:\n\n```\nimport pytest\nimport asyncio\n\nasync def my_async_function(x):\n    await asyncio.sleep(1)  # Simulate some async operation\n    return x * 2\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"input,expected\", [(2, 4), (3, 6), (4, 8)])\nasync def test_parametrized(input, expected):\n    result = await my_async_function(input)\n    assert result == expected\n```\n\nEl código está diseñado para probar código asíncrono. La prueba se parametriza usando `@pytest.mark.parametrize`, lo que significa que se ejecutará varias veces con diferentes conjuntos de valores de entrada y salida esperados. Específicamente, la prueba llamará a `my_async_function` con las entradas 2, 3 y 4, y comprobará que la función devuelve 4, 6 y 8, respectivamente.\n\n#### Mejores Prácticas\n\n- Siempre aísla tus pruebas para asegurarte de que no dependan de factores externos como bases de datos o APIs de red.\n- Utiliza la simulación para imitar el comportamiento de sistemas externos.\n- Mantén tus casos de prueba lo más simples posible para que sean más fáciles de entender y mantener.\n\n### Conclusiones: La Punta del Iceberg en las Capacidades de Asyncio\n\nAl concluir esta guía exhaustiva sobre temas avanzados de asyncio, es importante señalar que lo que hemos cubierto aquí es solo la punta del iceberg. Asyncio es una biblioteca rica y versátil que ofrece muchas características y funcionalidades, muchas de las cuales no hemos abordado. Los temas discutidos en esta entrada de blog son aquellos que considero que son los más comúnmente utilizados y beneficiosos para una amplia gama de aplicaciones.\n\nDesde primitivas de sincronización como Locks, Events y Semaphores hasta protocolos de red avanzados como Websockets y HTTP/2, asyncio proporciona las herramientas que necesitas para construir aplicaciones robustas, eficientes y escalables. También nos hemos adentrado en Grupos de Tareas para una mejor gestión de tareas, explorado técnicas avanzadas de manejo de excepciones e incluso abordado cómo probar eficazmente tu código asíncrono.\n\nAunque esta guía tiene como objetivo proporcionar una base sólida, el viaje no termina aquí. Hay muchas más características y técnicas para explorar, como:\n\n- Manejadores de contexto asíncronos y generadores\n- Interoperabilidad con otros bucles de eventos y aplicaciones multi-hilo.\n- Técnicas avanzadas de programación y limitación de tasa\n- Depuración y perfilado de aplicaciones asyncio.\n\n¡Y mucho más!\n\nLa principal conclusión es que asyncio no es solo una herramienta para manejar I/O asíncrono; es un marco de trabajo integral que puede impactar significativamente en cómo diseñas y construyes aplicaciones en Python. A medida que adquieras más experiencia con asyncio, descubrirás nuevas formas de resolver problemas y optimizar tu código, convirtiéndote en un desarrollador más eficaz y versátil.\n\nGracias por acompañarme en esta inmersión profunda en temas avanzados de asyncio. Espero que lo hayas encontrado esclarecedor y que sirva como un recurso valioso para tus futuros proyectos.\n\n### Final\n\nSi estás interesado en adentrarte más en el mundo de asyncio y la concurrencia en Python, te recomiendo el libro **\"Python Concurrency with asyncio\"**. Esta guía completa te llevará a través de las complejidades de asyncio, proporcionándote el conocimiento para construir aplicaciones concurrentes altamente eficientes en Python."},{"slug":"advanced-asyncio-topics-beyond-the-basics","lang":"en","title":"Advanced Asyncio Topics: Beyond the Basics","description":"Introduction Hello, asyncio enthusiasts! If you've been following along, you'll remember that we recently dived into the basics of Python's asyncio library in our previous blog post. We explored the f…","publishDate":"2023-09-18","updatedDate":null,"tags":["asynchronous-programming","python"],"translationSlug":"temas-avanzados-de-asyncio-mas-alla-de-lo-basico","url":"https://alexisalulema.com/blog/advanced-asyncio-topics-beyond-the-basics/","series":null,"headings":[{"depth":3,"slug":"introduction","text":"Introduction"},{"depth":3,"slug":"synchronization-primitives-in-asyncio-mastering-concurrency-control","text":"Synchronization Primitives in Asyncio: Mastering Concurrency Control"},{"depth":3,"slug":"queues-in-asyncio-the-backbone-of-data-management","text":"Queues in Asyncio: The Backbone of Data Management"},{"depth":3,"slug":"semaphores-in-asyncio-managing-rate-limits-and-resource-allocation","text":"Semaphores in Asyncio: Managing Rate Limits and Resource Allocation"},{"depth":3,"slug":"transports-and-protocols-the-building-blocks-of-network-communication","text":"Transports and Protocols: The Building Blocks of Network Communication"},{"depth":3,"slug":"streams-in-asyncio-a-higher-level-approach-to-network-communication","text":"Streams in Asyncio: A Higher-Level Approach to Network Communication"},{"depth":3,"slug":"task-groups-in-asyncio-simplifying-task-management-in-python-311","text":"Task Groups in Asyncio: Simplifying Task Management in Python 3.11+"},{"depth":3,"slug":"websockets-and-http2-leveraging-asyncio-for-advanced-networking-protocols","text":"Websockets and HTTP2: Leveraging Asyncio for Advanced Networking Protocols"},{"depth":3,"slug":"testing-async-code-ensuring-reliability-with-pytest-asyncio","text":"Testing Async Code: Ensuring Reliability with pytest-asyncio"},{"depth":3,"slug":"conclusions-the-tip-of-the-iceberg-in-asyncios-capabilities","text":"Conclusions: The Tip of the Iceberg in Asyncio’s Capabilities"},{"depth":3,"slug":"final","text":"Final"}],"body":"### Introduction\n\nHello, asyncio enthusiasts! If you've been following along, you'll remember that we recently dived into the basics of Python's asyncio library in our [previous blog post](https://www.alexisalulema.com/2023/09/07/asyncio-tutorial-for-the-programmer-in-a-hurry/). We explored the fundamentals like coroutines, tasks, and event loops, and even touched on some best practices and debugging techniques. But as promised, we're not stopping there. It's time to venture into the more advanced territories of asyncio.\n\nIn today's post, we'll be covering some of the more intricate aspects of asynchronous programming in Python. We'll delve into synchronization primitives, explore the utility of Queues and Semaphores, and even get our hands dirty with Transports and Protocols. These are the tools and concepts that can help you manage complex workflows, coordinate between different tasks, and truly unleash the power of asyncio.\n\nSo, if you're ready to elevate your asyncio game, read on as we dissect these advanced topics, complete with practical examples to help you grasp the concepts better.\n\n### Synchronization Primitives in Asyncio: Mastering Concurrency Control\n\nAs you delve deeper into asyncio, you'll find that managing concurrent tasks can get complicated. While asyncio excels at handling I/O-bound operations concurrently, there are scenarios where you need to control the execution order of your coroutines. This is where synchronization primitives come into play.\n\n#### What Are Synchronization Primitives?\n\nIn the context of asyncio, synchronization primitives are constructs designed to coordinate the execution of multiple coroutines. They are particularly useful when you have shared resources or states that multiple coroutines need to access. The most commonly used synchronization primitives in asyncio are:\n\n1. **Locks**: Prevent multiple coroutines from accessing a shared resource simultaneously.\n2. **Events**: Notify multiple coroutines that a particular event has occurred.\n3. **Conditions**: A more advanced form of synchronization that combines Locks and Events.\n4. **Semaphores**: Limit the number of coroutines that can access a particular section of code.\n\n#### Using Locks in Asyncio\n\nLocks are perhaps the simplest form of synchronization. Here's a quick example:\n\n```\nimport asyncio\n\nlock = asyncio.Lock()\n\nasync def my_coroutine(id):\n    async with lock:\n        print(f\"Coroutine {id} has acquired the lock\")\n        await asyncio.sleep(1)\n        print(f\"Coroutine {id} has released the lock\")\n\nasync def main():\n    tasks = [my_coroutine(i) for i in range(3)]\n    await asyncio.gather(*tasks)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, we create a lock using `asyncio.Lock()`. The `my_coroutine` function acquires the lock using `async with lock:` before proceeding with its task. This ensures that only one coroutine can execute the code block under the lock at any given time.\n\n#### Using Events in Asyncio\n\nEvents are another useful synchronization primitive. They allow you to pause a coroutine until a certain event has occurred.\n\n```\nimport asyncio\n\nevent = asyncio.Event()\n\nasync def waiter():\n    print(\"Waiting for the event to be set\")\n    await event.wait()\n    print(\"The event was set, proceeding\")\n\nasync def setter():\n    await asyncio.sleep(1)\n    print(\"Setting the event\")\n    event.set()\n\nasync def main():\n    await asyncio.gather(waiter(), setter())\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, the `waiter` coroutine will wait until the `setter` coroutine sets the event.\n\n#### Using Conditions in Asyncio\n\nConditions are a bit more complex and are used when a coroutine should wait for a particular condition to be met.\n\n```\nimport asyncio\n\ncondition = asyncio.Condition()\n\nasync def consumer():\n    async with condition:\n        print(\"Consumer waiting\")\n        await condition.wait()\n        print(\"Consumer triggered\")\n        # Do something\n\nasync def producer():\n    await asyncio.sleep(1)\n    async with condition:\n        print(\"Producer ready\")\n        condition.notify_all()\n\nasync def main():\n    await asyncio.gather(consumer(), producer())\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nHere, the `consumer` coroutine waits for a condition to be met. The `producer` coroutine sets the condition, allowing the `consumer` to proceed.\n\nBoth `Event` and `Condition` are synchronization primitives in asyncio that allow for coordination between coroutines, but they serve different purposes and are used in different scenarios. Here's a breakdown of the differences:\n\n##### Event\n\nAn `Event` is a simple synchronization primitive that is used to signal multiple coroutines that some condition has been met, allowing them to proceed. Once the event is set, all waiting coroutines are awakened. Events are mainly used for one-off signals and don't carry any additional information or state other than being set or cleared.\n\n##### Condition\n\nA `Condition` is more complex and is often used for more specific signaling between coroutines. It combines the functionality of an `Event` and a `Lock`. Conditions are generally used to signal state changes that may require coroutines to take different actions, not just proceed. Multiple conditions can share the same underlying lock, allowing for more complex coordination.\n\n#### Key Differences\n\n1. **Purpose**: `Event` is generally used for simple signaling, while `Condition` is used for more complex scenarios involving multiple state changes.\n2. **Locking**: `Condition` has an associated lock, allowing for more complex coordination between coroutines. `Event` does not have this feature.\n3. **Notification**: `Condition` allows you to notify only a specific number of waiting coroutines, whereas `Event` will notify all waiting coroutines when set.\n4. **State Management**: `Condition` is often used when the state of an object changes and you need to notify other coroutines, possibly to re-check a condition. `Event` is more for a one-off signal to proceed.\n\nIn summary, while the code to use them may look similar, `Event` and `Condition` are used for different types of coordination problems. Choose the one that best fits the specific requirements of your application.\n\n#### Why Use Synchronization Primitives?\n\nYou might wonder, \"Why do I need these primitives if asyncio is all about concurrency?\" The answer lies in the complexity of real-world applications. When you have multiple coroutines that depend on some shared state or need to be executed in a specific order, synchronization primitives become indispensable.\n\n#### Best Practices\n\n- Always release the locks and other resources you acquire.\n- Use timeouts where applicable to avoid deadlocks.\n- Prefer higher-level synchronization primitives like Queues for producer-consumer problems.\n\n### Queues in Asyncio: The Backbone of Data Management\n\nAfter diving deep into synchronization primitives, let's shift our focus to another powerful feature in asyncio—Queues. Queues are the go-to solution for managing data between multiple coroutines, especially in producer-consumer scenarios.\n\n#### What Are Queues?\n\nIn asyncio, a Queue is a simple yet powerful data structure that allows you to store and retrieve items in a first-in, first-out (FIFO) manner. Queues are thread-safe and are designed to be used with async/await syntax, making them ideal for managing data between multiple coroutines.\n\n#### Basic Usage of Queues\n\nHere's a simple example demonstrating a producer-consumer relationship using asyncio Queues:\n\n```\nimport asyncio\n\nasync def producer(queue):\n    for i in range(5):\n        print(f\"Producing item {i}\")\n        await queue.put(i)\n        await asyncio.sleep(1)\n\nasync def consumer(queue):\n    while True:\n        item = await queue.get()\n        print(f\"Consuming item {item}\")\n        queue.task_done()\n\nasync def main():\n    queue = asyncio.Queue()\n    await asyncio.gather(producer(queue), consumer(queue))\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, the `producer` coroutine puts items into the queue, while the `consumer` coroutine takes them out for processing. The `queue.task_done()` method indicates that a formerly enqueued task is complete.\n\n#### Advanced Usage: Priority Queues and LIFO Queues\n\nAsyncio also provides specialized types of queues like `PriorityQueue` and `LifoQueue` (Last-In, First-Out). These can be useful for more complex data management requirements.\n\n#### Why Use Queues?\n\n1. **Data Integrity**: Queues ensure that data is processed in the order it was added, maintaining data integrity.\n2. **Resource Management**: Queues can be used to manage limited resources like database connections or API rate limits.\n3. **Decoupling**: Queues decouple the data producers from consumers, making it easier to maintain and scale your application.\n\n#### Best Practices\n\n- Use the `maxsize` parameter to set a limit on the queue size to prevent memory overflow.\n- Always use `await queue.join()` to ensure that all tasks are completed before shutting down the event loop.\n\n#### Real-World Example: Web Crawler\n\nImagine you're building a web crawler. You could have one coroutine responsible for fetching URLs and putting them into a queue. Another coroutine could be responsible for taking URLs from the queue, downloading the content, and performing some analysis. This way, the fetching and analysis parts of your application are decoupled, making it easier to maintain and scale.\n\n```\nimport asyncio\n\nasync def fetch_urls(queue):\n    urls = [\"https://en.wikipedia.org/wiki/Americas\", \"https://en.wikipedia.org/wiki/Asia\"]\n    for url in urls:\n        print(f\"Fetching {url}\")\n        await queue.put(url)\n        await asyncio.sleep(1)\n\nasync def analyze_content(queue):\n    while True:\n        url = await queue.get()\n        print(f\"Analyzing content from {url}\")\n        queue.task_done()\n\nasync def main():\n    queue = asyncio.Queue()\n    await asyncio.gather(fetch_urls(queue), analyze_content(queue))\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Semaphores in Asyncio: Managing Rate Limits and Resource Allocation\n\nHaving covered synchronization primitives and Queues, let's now turn our attention to Semaphores. Semaphores are another form of synchronization primitive but with a different purpose: they are used to limit the number of coroutines that can access a particular section of code.\n\n#### What Are Semaphores?\n\nIn asyncio, a Semaphore is essentially a counter with an initial value that you set. When a coroutine acquires the Semaphore, the counter is decremented. When the counter reaches zero, no more coroutines can acquire the Semaphore until one of the existing ones releases it, incrementing the counter back up.\n\n#### Basic Usage of Semaphores\n\nHere's a simple example to demonstrate how Semaphores can be used to limit concurrent access:\n\n```\nimport asyncio\n\nsem = asyncio.Semaphore(2)\n\nasync def my_coroutine(id):\n    async with sem:\n        print(f\"Coroutine {id} has acquired the semaphore\")\n        await asyncio.sleep(2)\n        print(f\"Coroutine {id} has released the semaphore\")\n\nasync def main():\n    tasks = [my_coroutine(i) for i in range(4)]\n    await asyncio.gather(*tasks)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, we create a Semaphore with an initial value of 2, meaning only two coroutines can acquire it at the same time. The `my_coroutine` function acquires the Semaphore using `async with sem:` before proceeding with its task.\n\n#### Why Use Semaphores?\n\n1. **Rate Limiting**: If you're making API calls or web scraping, you can use Semaphores to limit the rate at which requests are made.\n2. **Resource Allocation**: In scenarios where you have limited resources like database connections, Semaphores can help manage those resources effectively.\n\n#### Advanced Use-Case: Web Scraping with Rate Limiting\n\nLet's consider a real-world example where Semaphores can be incredibly useful: web scraping with rate limiting.\n\n```\nimport asyncio\n\nsem = asyncio.Semaphore(2)  # Limit to 2 concurrent requests\n\nasync def fetch_page(url):\n    async with sem:\n        print(f\"Fetching {url}\")\n        await asyncio.sleep(1)  # Simulate network delay\n        print(f\"Finished fetching {url}\")\n\nasync def main():\n    urls = [\"https://en.wikipedia.org/wiki/Americas\", \"https://en.wikipedia.org/wiki/Asia\", \"https://en.wikipedia.org/wiki/Europe\", \"https://en.wikipedia.org/wiki/Africa\"]\n    tasks = [fetch_page(url) for url in urls]\n    await asyncio.gather(*tasks)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, we use a Semaphore to limit the number of concurrent requests to 3. This ensures that we don't overwhelm the server while scraping.\n\n#### Best Practices\n\n- Always release the Semaphore after you're done using it to free up resources.\n- Be mindful of deadlocks. Make sure that every acquire is paired with a release.\n\n### Transports and Protocols: The Building Blocks of Network Communication\n\nAfter discussing synchronization primitives, Queues, and Semaphores, it's time to delve into the lower-level abstractions that asyncio offers: Transports and Protocols. These are the building blocks that provide more control over network communication, allowing you to handle use-cases that are not easily covered by higher-level abstractions.\n\n#### What Are Transports and Protocols?\n\n- **Transports**: These are responsible for handling the actual I/O operations, like reading from or writing to the network.\n- **Protocols**: These define the rules for parsing incoming data and formatting outgoing data. They work in conjunction with Transports to handle the network communication.\n\n#### Basic Usage of Transports and Protocols\n\nHere's a simple example of a TCP echo server using Transports and Protocols:\n\n```\nimport asyncio\n\nclass EchoProtocol(asyncio.Protocol):\n    def connection_made(self, transport):\n        self.transport = transport\n\n    def data_received(self, data):\n        self.transport.write(data)\n\nasync def main():\n    loop = asyncio.get_running_loop()\n    server = await loop.create_server(EchoProtocol, '127.0.0.1', 8888)\n    await server.serve_forever()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, we define an `EchoProtocol` class that inherits from `asyncio.Protocol`. The `connection_made` method is called when a new connection is established, and `data_received` is called whenever data is received from the client. You can try this server by running telnet:\n\n```\nalexis % telnet 127.0.0.1 8888\nTrying 127.0.0.1...\nConnected to localhost.\n```\n\n#### Why Use Transports and Protocols?\n\n1. **Fine-Grained Control**: They offer more control over the network layer, allowing you to optimize for specific use-cases.\n2. **Protocol Implementation**: If you're working with a custom or less-common network protocol, you can implement it using these lower-level abstractions.\n3. **Resource Management**: Effective use of Transports and Protocols can lead to more efficient resource utilization.\n\n#### Advanced Use-Case: Custom Protocol Implementation\n\nWe'll create a simple custom binary protocol that expects data in the format of a 4-byte integer followed by a string. The server will read the integer, then read the string of that length, and echo it back to the client.\n\nHere's the server code using `CustomProtocol`:\n\n```\nimport asyncio\nimport struct\n\nclass CustomProtocol(asyncio.Protocol):\n    def connection_made(self, transport):\n        self.transport = transport\n\n    def data_received(self, data):\n        parsed_data = self.parse_custom_protocol(data)\n        self.transport.write(parsed_data)\n\n    def parse_custom_protocol(self, data):\n        # Unpack a 4-byte integer from the beginning of data\n        str_len = struct.unpack(\"!I\", data[:4])[0]\n        \n        # Extract the string of the given length\n        received_str = data[4: 4 + str_len].decode('utf-8')\n        \n        print(f\"Received string: {received_str}\")\n        \n        # For demonstration, just echo back the received string\n        return data\n\nasync def main():\n    loop = asyncio.get_running_loop()\n    server = await loop.create_server(CustomProtocol, '127.0.0.1', 8888)\n    await server.serve_forever()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nNow, let's create a client to test this server. The client will send a 4-byte integer indicating the length of the string, followed by the string itself:\n\n```\nimport asyncio\nimport struct\n\nasync def custom_protocol_client():\n    reader, writer = await asyncio.open_connection('127.0.0.1', 8888)\n    \n    # Prepare data: 4-byte length followed by the string\n    test_str = \"Hello, World!\"\n    data = struct.pack(\"!I\", len(test_str)) + test_str.encode('utf-8')\n    \n    writer.write(data)\n    await writer.drain()\n    \n    # Read echoed data\n    received_data = await reader.read(100)\n    \n    # Close the connection\n    writer.close()\n    await writer.wait_closed()\n\n    # Unpack received data\n    str_len = struct.unpack(\"!I\", received_data[:4])[0]\n    received_str = received_data[4: 4 + str_len].decode('utf-8')\n    \n    print(f\"Received echoed string: {received_str}\")\n\nasync def main():\n    await custom_protocol_client()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n#### Best Practices\n\n- Always close the Transport when you're done to free up resources.\n- Implement proper error handling in your Protocol methods to deal with network issues or malformed data.\n\n### Streams in Asyncio: A Higher-Level Approach to Network Communication\n\nAfter exploring the lower-level abstractions of Transports and Protocols, you might be wondering if there's a more straightforward way to handle network communication in asyncio. The answer is yes, and it comes in the form of Streams—a higher-level alternative that provides an easier interface for reading and writing data over the network.\n\n#### What Are Streams?\n\nStreams in asyncio provide a set of high-level APIs for working with network I/O. They abstract away the complexities of Transports and Protocols, offering a simpler and more Pythonic interface. Streams are divided into two main types:\n\n1. **StreamReader**: For reading data from a network connection.\n2. **StreamWriter**: For writing data to a network connection.\n\n#### Basic Usage of Streams\n\nHere's a simple example of a TCP echo client using asyncio Streams:\n\n```\nimport asyncio\n\nasync def echo_client():\n    reader, writer = await asyncio.open_connection('127.0.0.1', 8888)\n    \n    writer.write(b'Hello, World!')\n    await writer.drain()\n    \n    data = await reader.read(100)\n    print(f\"Received: {data.decode()}\")\n    \n    writer.close()\n    await writer.wait_closed()\n\nasync def main():\n    await echo_client()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, `asyncio.open_connection` returns a StreamReader (`reader`) and a StreamWriter (`writer`). We use `writer.write()` to send data and `reader.read()` to receive data.\n\n#### Why Use Streams?\n\n1. **Simplicity**: Streams offer a simpler and more intuitive API compared to Transports and Protocols.\n2. **Readability**: Code that uses Streams is often easier to read and maintain.\n3. **Flexibility**: While they are high-level, Streams still offer a good degree of control over the network communication.\n\n#### Advanced Use-Case: Building a Chat Server\n\nImagine building a simple chat server where multiple clients can send and receive messages. You could use Streams to manage each client's connection easily.\n\n```\nimport asyncio\n\nasync def handle_client(reader, writer):\n    while True:\n        data = await reader.read(100)\n        if not data:\n            break\n        writer.write(data)\n        await writer.drain()\n\nasync def main():\n    server = await asyncio.start_server(handle_client, '127.0.0.1', 8888)\n    await server.serve_forever()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nThis client will connect to the server, send messages typed by the user, and display messages received from the server:\n\n```\nimport asyncio\n\nasync def read_from_server(reader):\n    while True:\n        data = await reader.read(100)\n        if not data:\n            print(\"Server disconnected\")\n            break\n        print(f\"Received: {data.decode()}\")\n\nasync def write_to_server(writer):\n    while True:\n        message = input(\"Send message: \")\n        writer.write(message.encode())\n        await writer.drain()\n\nasync def chat_client():\n    reader, writer = await asyncio.open_connection('127.0.0.1', 8888)\n    \n    asyncio.create_task(read_from_server(reader))\n    asyncio.create_task(write_to_server(writer))\n\n    await asyncio.gather(\n        read_from_server(reader),\n        write_to_server(writer)\n    )\n\n    writer.close()\n    await writer.wait_closed()\n\nasync def main():\n    await chat_client()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nTo test the chat server:\n\n1. Run the chat server code.\n2. Run the chat client code.\n\nYou can open multiple terminals and run multiple instances of the client to simulate a chat environment. Each client will echo back whatever message it sends to the server, as per your server's current implementation.\n\nIn this example, `handle_client` is a coroutine that handles the communication for each connected client. It reads data from the `reader` and writes it back using the `writer`, effectively echoing the received messages.\n\n#### Best Practices\n\n- Always close the StreamWriter using `writer.close()` to free up resources.\n- Use `await writer.drain()` to ensure that all buffered data is sent.\n\n### Task Groups in Asyncio: Simplifying Task Management in Python 3.11+\n\nIf you're using Python 3.11 or newer, you have access to a powerful feature that simplifies the management of multiple tasks—Task Groups. This advanced topic is particularly useful for handling task cancellations and exceptions in a more organized manner.\n\n#### What Are Task Groups?\n\nTask Groups are a new addition to asyncio in Python 3.11 that allow you to manage the lifecycle of multiple tasks as a single unit. With Task Groups, you can start, cancel, or await the completion of multiple tasks together, making your code cleaner and more manageable.\n\n#### Basic Usage of Task Groups\n\nHere's a simple example to demonstrate the usage of Task Groups:\n\n```\nimport asyncio\n\nasync def my_task(name):\n    print(f\"Task {name} started\")\n    await asyncio.sleep(1)\n    print(f\"Task {name} completed\")\n\nasync def main():\n    async with asyncio.TaskGroup() as tg:\n        tg.create_task(my_task(\"A\"))\n        tg.create_task(my_task(\"B\"))\n        tg.create_task(my_task(\"C\"))\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, we create a Task Group using `asyncio.TaskGroup()` and then add tasks to it using `tg.create_task()`. All tasks in the Task Group will be awaited when exiting the `async with` block.\n\n#### Why Use Task Groups?\n\n1. **Simplified Error Handling**: If any task in the group raises an exception, it is propagated immediately, and all other tasks are cancelled.\n2. **Task Cancellation**: Cancelling the Task Group cancels all tasks within it, making it easier to manage task lifecycles.\n3. **Code Organization**: Task Groups help in organizing related tasks together, making the code easier to read and maintain.\n\n#### Advanced Use-Case: Web Scraping with Error Handling\n\nConsider a web scraping scenario where you're fetching data from multiple URLs. If any of the fetch operations fail, you want to cancel all other ongoing fetches and handle the exception gracefully.\n\n```\nimport asyncio\n\nasync def fetch_url(url):\n    print(f\"Fetching {url}\")\n    await asyncio.sleep(1)  # Simulate network delay\n    if \"bad\" in url:\n        raise ValueError(f\"Bad URL: {url}\")\n    print(f\"Fetched {url}\")\n\nasync def main():\n    async with asyncio.TaskGroup() as tg:\n        tg.create_task(fetch_url(\"https://en.wikipedia.org/wiki/Americas\"))\n        tg.create_task(fetch_url(\"bad://failx.test\"))\n        tg.create_task(fetch_url(\"https://en.wikipedia.org/wiki/Europa\"))\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(main())\n    except ValueError as e:\n        print(f\"An error occurred: {e}\")\n```\n\nIn this example, fetching the \"bad\" URL raises a `ValueError`. The Task Group ensures that all other tasks are cancelled, and the exception is propagated.\n\n#### Best Practices\n\n- Use Task Groups for logically related tasks that should be managed together.\n- Be mindful of exception handling within Task Groups to ensure that errors are caught and handled appropriately.\n\n### Websockets and HTTP2: Leveraging Asyncio for Advanced Networking Protocols\n\nAs you dive deeper into the world of asyncio, you'll find that it's not just limited to basic HTTP requests or simple TCP/UDP protocols. Asyncio can also be used to work with more advanced networking protocols like Websockets and HTTP/2, enabling real-time communication and multiplexing capabilities.\n\n#### Websockets with Asyncio\n\nWebsockets provide a full-duplex communication channel over a single, long-lived connection, making them ideal for real-time applications like chat rooms, online gaming, and live updates.\n\n##### Basic Usage of Websockets\n\nHere's a simple example of a Websocket server using asyncio and the `websockets` library:\n\n```\nimport asyncio\nimport websockets\n\nasync def echo(websocket, path):\n    async for message in websocket:\n        await websocket.send(f\"Echo: {message}\")\n\nasync def main():\n    server = await websockets.serve(echo, \"localhost\", 8765)\n    await server.wait_closed()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nThe following client will connect to the server, send a message, and then print the message it receives back.\n\n```\nimport asyncio\nimport websockets\n\nasync def websocket_client():\n    uri = \"ws://localhost:8765\"\n    async with websockets.connect(uri) as websocket:\n        await websocket.send(\"Hello, Server!\")\n        print(\"Sent: Hello, Server!\")\n\n        response = await websocket.recv()\n        print(f\"Received: {response}\")\n\nasync def main():\n    await websocket_client()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, the `echo` coroutine listens for incoming messages and echoes them back to the client.\n\n#### HTTP/2 with Asyncio\n\nHTTP/2 is the second major version of the HTTP protocol, offering features like header compression and multiplexing multiple requests over a single connection.\n\n##### Using HTTP/2 with `httpx`\n\nYou can use the `httpx` library, which supports asyncio, to make HTTP/2 requests:\n\n```\nimport httpx\nimport asyncio\n\nasync def fetch_data():\n    async with httpx.AsyncClient(http2=True) as client:\n        response = await client.get('https://www.example.com')\n        print(response.status_code)\n\nasync def main():\n    await fetch_data()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, we create an asynchronous HTTP client with HTTP/2 support and fetch data from a website.\n\n#### Why Use Advanced Protocols?\n\n1. **Real-Time Communication**: Websockets enable real-time, bidirectional communication between the server and client.\n2. **Efficiency**: HTTP/2's multiplexing features reduce latency and improve speed.\n3. **Modern Applications**: Many modern web services and APIs are moving towards these advanced protocols for better performance and capabilities.\n\n#### Advanced Use-Case: Real-Time Stock Ticker\n\nImagine you're building a real-time stock ticker application. You could use Websockets to push updates to the client whenever a stock price changes.\n\n```\nimport asyncio\nimport websockets\nimport json\n\nasync def stock_ticker(websocket, path):\n    while True:\n        stock_data = {\"AAPL\": 150.00, \"GOOGL\": 2800.00}  # Simulated stock data\n        await websocket.send(json.dumps(stock_data))\n        await asyncio.sleep(1)\n\nasync def main():\n    server = await websockets.serve(stock_ticker, \"localhost\", 8765)\n    await server.wait_closed()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nThe following client will connect to the server, receive stock data updates, and print them.\n\n```\nimport asyncio\nimport websockets\nimport json\n\nasync def stock_ticker_client():\n    uri = \"ws://localhost:8765\"\n    async with websockets.connect(uri) as websocket:\n        while True:\n            response = await websocket.recv()\n            stock_data = json.loads(response)\n            print(f\"Received stock data: {stock_data}\")\n\nasync def main():\n    await stock_ticker_client()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn this example, the `stock_ticker` coroutine sends a JSON payload containing stock prices to the client every second.\n\n#### Best Practices\n\n- Use libraries that are designed to work with asyncio for better performance and easier integration.\n- Always handle exceptions and edge cases to make your application robust, especially when dealing with real-time data or multiplexed connections.\n\n### Testing Async Code: Ensuring Reliability with `pytest-asyncio`\n\nAs you build more complex asyncio applications, the importance of testing cannot be overstated. Testing ensures that your code behaves as expected and makes it easier to add new features without breaking existing functionality. In this section, we'll explore how to test asynchronous Python code, focusing on the `pytest-asyncio` library.\n\n#### Why Testing Async Code is Important\n\nTesting asynchronous code is crucial for several reasons:\n\n1. **Reliability**: Ensures that your async functions and coroutines work as expected under different conditions.\n2. **Maintainability**: Makes it easier to refactor and add new features.\n3. **Collaboration**: Well-tested code is easier for other developers to understand and contribute to.\n\n#### Basic Testing with `pytest-asyncio`\n\nThe `pytest-asyncio` package extends the popular `pytest` framework to handle asyncio coroutines. To get started, you'll need to install it:\n\n```\npip install pytest-asyncio\n-- or --\nconda install -c conda-forge pytest-asyncio\n```\n\nHere's a simple example that tests an asynchronous function:\n\n```\nimport pytest\nimport asyncio\n\nasync def my_async_function(x):\n    await asyncio.sleep(1)\n    return x * 2\n\n@pytest.mark.asyncio\nasync def test_my_async_function():\n    result = await my_async_function(2)\n    assert result == 4\n```\n\nIn this example, we use the `@pytest.mark.asyncio` decorator to mark the test as asynchronous. Then, we `await` the `my_async_function` coroutine and assert that it returns the expected result.\n\n#### Advanced Techniques: Mocking and Parametrization\n\n##### Mocking Async Functions\n\nYou can use the `unittest.mock` library to mock asynchronous functions:\n\n```\nimport pytest\nimport asyncio\nimport sys\nfrom unittest.mock import patch\n\nasync def my_async_function(x):\n    await asyncio.sleep(1)  # Simulate some async operation\n    return x * 2\n\n@pytest.mark.asyncio\nasync def test_with_mock():\n    current_module = sys.modules[__name__]\n    with patch.object(current_module, 'my_async_function', return_value=4):\n        result = await my_async_function(2)\n        assert result == 4\n```\n\nIn this example, we use the `@pytest.mark.asyncio` decorator to mark the test as asynchronous. Then, we `await` the `my_async_function` coroutine and assert that it returns the expected result.\n\n#### Advanced Techniques: Mocking and Parametrization\n\n##### Mocking Async Functions\n\nYou can use the `unittest.mock` library to mock asynchronous functions:\n\n```\nimport pytest\nimport asyncio\n\nasync def my_async_function(x):\n    await asyncio.sleep(1)  # Simulate some async operation\n    return x * 2\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"input,expected\", [(2, 4), (3, 6), (4, 8)])\nasync def test_parametrized(input, expected):\n    result = await my_async_function(input)\n    assert result == expected\n```\n\nThe code is designed to test asynchronous code. The test is parametrized using `@pytest.mark.parametrize`, which means it will run multiple times with different sets of input and expected output values. Specifically, the test will call `my_async_function` with the inputs 2, 3, and 4, and assert that the function returns 4, 6, and 8, respectively.\n\n#### Best Practices\n\n- Always isolate your tests to ensure that they don't depend on external factors like databases or network APIs.\n- Use mocking to simulate the behavior of external systems.\n- Keep your test cases as simple as possible to make them easier to understand and maintain.\n\n### Conclusions: The Tip of the Iceberg in Asyncio's Capabilities\n\nAs we wrap up this comprehensive guide on advanced asyncio topics, it's important to note that what we've covered here is just the tip of the iceberg. Asyncio is a rich and versatile library that offers a lot of features and functionalities, many of which we haven't touched upon. The topics discussed in this blog post are those that I consider to be the most commonly used and beneficial for a wide range of applications.\n\nFrom synchronization primitives like Locks, Events, and Semaphores to advanced networking protocols like Websockets and HTTP/2, asyncio provides the tools you need to build robust, efficient, and scalable applications. We've also delved into Task Groups for better task management, explored advanced exception handling techniques, and even touched on how to test your asynchronous code effectively.\n\nWhile this guide aims to provide a solid foundation, the journey doesn't end here. There are many more features and techniques to explore, such as:\n\n- Asynchronous context managers and generators\n- Interoperability with other event loops and multi-threaded applications\n- Advanced scheduling and rate-limiting techniques\n- Debugging and profiling asyncio applications\n\nAnd much more!\n\nThe key takeaway is that asyncio is not just a tool for handling asynchronous I/O; it's a comprehensive framework that can significantly impact how you design and build Python applications. As you gain more experience with asyncio, you'll discover new ways to solve problems and optimize your code, making you a more effective and versatile developer.\n\nThank you for joining me on this deep dive into advanced asyncio topics. I hope you've found it enlightening and that it serves as a valuable resource for your future projects.\n\n### Final\n\nIf you're interested in diving deeper into the world of asyncio and Python concurrency, I highly recommend the book **\"Python Concurrency with asyncio\"**. This comprehensive guide will take you through the intricacies of asyncio, providing you with the knowledge to build highly efficient, concurrent applications in Python."},{"slug":"tutorial-asyncio-para-el-programador-con-el-tiempo-en-contra","lang":"es","title":"Tutorial 'asyncio' para el programador con el tiempo en contra.","description":"Introducción En el mundo en constante evolución de la programación en Python, entender la biblioteca asyncio se está volviendo cada vez más importante. asyncio permite la programación asíncrona, concu…","publishDate":"2023-09-11","updatedDate":null,"tags":["asynchronous-programming"],"translationSlug":"asyncio-tutorial-for-the-programmer-in-a-hurry","url":"https://alexisalulema.com/es/blog/tutorial-asyncio-para-el-programador-con-el-tiempo-en-contra/","series":null,"headings":[{"depth":2,"slug":"introducción","text":"Introducción"},{"depth":2,"slug":"corrutinas","text":"Corrutinas"},{"depth":3,"slug":"administradores-de-contexto-asíncronos-e-iteradores-asíncronos","text":"Administradores de contexto asíncronos e iteradores asíncronos"},{"depth":3,"slug":"tareas-y-concurrencia","text":"Tareas y Concurrencia"},{"depth":3,"slug":"medir-el-tiempo-de-ejecución-en-corrutinas-con-decoradores","text":"Medir el tiempo de ejecución en corrutinas con decoradores"},{"depth":3,"slug":"problemas-con-corrutinas-y-tareas","text":"Problemas con Corrutinas y Tareas"},{"depth":3,"slug":"el-bucle-de-eventos-en-asyncio","text":"El bucle de eventos en asyncio"},{"depth":3,"slug":"modo-debug","text":"Modo ‘Debug’"},{"depth":3,"slug":"conclusión","text":"Conclusión"}],"body":"## Introducción\n\nEn el mundo en constante evolución de la programación en Python, entender la biblioteca asyncio se está volviendo cada vez más importante. asyncio permite la programación asíncrona, concurrente y paralela en un lenguaje que tradicionalmente era síncrono y de un solo hilo.\n\nA medida que las aplicaciones de Python crecen en complejidad y dependen de tareas limitadas por E/S como solicitudes web o llamadas a bases de datos, es crucial gestionar estas tareas de manera eficiente. Ahí es donde brilla asyncio. Sin embargo, comenzar con asyncio puede ser desalentador, especialmente si no estás familiarizado con las corrutinas, tareas, futuros y bucles de eventos. Este tutorial tiene como objetivo guiarte a través de los conceptos básicos de asyncio, completo con ejemplos funcionales.\n\nAl final de este tutorial, tendrás un sólido conocimiento de asyncio y estarás listo para implementarlo en tus proyectos, ya sea que estés construyendo un microservicio o una aplicación de interfaz gráfica de usuario.\n\n## Corrutinas\n\nUna **corrutina** es una generalización de una subrutina que se puede pausar y reanudar, permitiendo que otro código se ejecute durante las pausas. Las corrutinas son una parte clave de asyncio y nos permiten escribir código asíncrono en un estilo procedural.\n\nEn Python, las corrutinas se definen con la sintaxis `async def`:\n\n```\nasync def my_coroutine():\n    print(\"Hello, Coroutine!\")\n```\n\nPara ejecutar la corrutina, no puedes simplemente llamarla como una función normal. Debes usar la palabra clave `await`.\n\n```\nawait my_coroutine()\n```\n\nVamos a simular una tarea de larga duración con la función `sleep` de asyncio.\n\n```\nimport asyncio\n\nasync def my_coroutine():\n    await asyncio.sleep(1)  # sleep for 1 second\n    print(\"Hello, Coroutine!\")\n\nasync def main():\n    await my_coroutine()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Administradores de contexto asíncronos e iteradores asíncronos\n\nEn aplicaciones del mundo real, a menudo necesitas gestionar recursos como flujos de archivos, conexiones de red o conexiones a bases de datos. El uso de gestores de contexto e iteradores de forma asíncrona puede ser muy útil para tales tareas. Afortunadamente, asyncio de Python ofrece soporte nativo para gestores de contexto asíncronos e iteradores asíncronos, lo que hace que tu código no solo sea más eficiente, sino también más limpio y fácil de mantener.\n\n#### Usando `async with`\n\nLa declaración `async with` se utiliza para invocar un gestor de contexto asíncrono. Por ejemplo, al trabajar con operaciones de archivos asíncronas, puedes leer un archivo sin bloquear el bucle de eventos de esta manera:\n\n```\nimport aiofiles\nimport chardet\nimport asyncio\n\nasync def read_large_file():\n    async with aiofiles.open('large_file.txt', mode='rb') as f:\n        raw_data = await f.read()\n\n    detected = chardet.detect(raw_data)\n    encoding_type = detected['encoding']\n    contents = raw_data.decode(encoding_type)\n    print(contents)\n\nasync def main():\n    await read_large_file()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nUn ejemplo más:\n\n```\n# Async context managers for managing resources\nasync with aiohttp.ClientSession() as session:\n    async with session.get('https://api.example.com/data') as resp:\n        data = await resp.json()\n```\n\n**Best Practice:** Siempre prefiera usar `async with` para adquirir y liberar recursos.\n\n#### Usando `async for`\n\nLa declaración `async for` se puede utilizar con un objeto que es un iterador asíncrono. Por ejemplo, para obtener múltiples URL de forma asíncrona:\n\n```\nimport asyncio\n\n# Define an asynchronous generator simulating fetching data from URLs\nasync def fetch_data_from_urls():\n    urls = [\"https://www.google.com\", \"https://www.apple.com\", \"https://www.microsoft.com\"]\n    for url in urls:\n        await asyncio.sleep(2)  # Simulate network delay\n        yield f\"Fetched data from {url}\"\n\n# Define a coroutine that uses the asynchronous generator\nasync def process_fetched_data():\n    async for data in fetch_data_from_urls():\n        print(f\"Processing: {data}\")\n\n# Run the asynchronous event loop\nasyncio.run(process_fetched_data())\n```\n\nTanto los bucles `for` como **`async for`** en Python tienen propósitos similares: se utilizan para iterar sobre elementos. Sin embargo, no son intercambiables y están diseñados para diferentes tipos de iterables.\n\n- El bucle **`for`** se utiliza para iterar sobre iterables síncronos, como listas, o generadores síncronos.\n\n```\nfor item in [1, 2, 3]:\n    print(item)\n```\n\n- El bucle `async for` está diseñado para iterar sobre iterables asíncronos o generadores asíncronos.\n\n```\nasync for item in async_generator():\n    print(item)\n```\n\nLa diferencia clave es que no puedes usar un bucle `for` estándar para iterar sobre iterables asíncronos o generadores asíncronos. Del mismo modo, no puedes usar un bucle `async for` para iterar sobre iterables o generadores síncronos tradicionales.\n\nSi intentas mezclarlos, Python generará un `TypeError`.\n\n**Best Practice**: Utiliza `async for` al trabajar con iteradores asíncronos para hacer que tu código sea más legible y fácil de mantener.\n\n### Tareas y Concurrencia\n\nLas tareas se utilizan para programar corrutinas de manera concurrente. Cuando una corrutina se envuelve en una **`Task`** con funciones como `asyncio.create_task()`, la corrutina se programa automáticamente para ejecutarse pronto.\n\n```\nimport asyncio\n\nasync def my_coroutine():\n    await asyncio.sleep(1)\n    print(\"Hello, Coroutine!\")\n\nasync def main():\n    # Create a task out of a coroutine\n    task = asyncio.create_task(my_coroutine())\n    # wait for the task to finish\n    await task\n\n# Run the main coroutine\nasyncio.run(main())\n```\n\n**IMPORTANTE!** No puedes simplemente ejecutar `my_coroutine()` invocándola directamente en la siguiente línea:\n\n```\ntask = asyncio.create_task(my_coroutine())\n```\n\nSi haces esto fuera de una función asíncrona, recibirás el siguiente error:\n\n```\nRuntimeError: no running event loop\n```\n\nEn asyncio de Python, el 'bucle de eventos' es como un administrador para las tareas que deben ejecutarse de manera concurrente. Este error ocurre cuando estás intentando hacer algo que requiere este administrador, pero no está presente.\n\nEn términos más simples, estás intentando realizar una operación que debería ejecutarse en segundo plano (de forma asíncrona), pero no hay un sistema (el 'bucle de eventos' que explicaré más adelante en esta publicación) en marcha para manejar este tipo de operación.\n\nComúnmente, este error ocurre porque estás ejecutando código asyncio fuera de una función asíncrona. Para solucionar esto, asegúrate de que tu código asyncio se ejecute dentro de una función asíncrona y que un bucle de eventos esté en funcionamiento. Para corregir este error, simplemente ejecútalo de esta manera:\n\n```\nasyncio.run(my_coroutine())\n```\n\nLas tareas pueden cancelarse si ya no son necesarias. Esto se hace con el método `cancel` en el objeto Task. También puedes establecer tiempos de espera para las tareas utilizando la función `asyncio.wait_for`:\n\n```\nimport asyncio\n\nasync def my_coroutine():\n    await asyncio.sleep(10)\n    print(\"Hello, Coroutine!\")\n\nasync def main():\n    # Create a task out of a coroutine\n    task = asyncio.create_task(my_coroutine())\n\n    # Cancel the task\n    task.cancel()\n\n    # Set a timeout for a task\n    try:\n        await asyncio.wait_for(my_coroutine(), timeout=1.0)\n    except asyncio.TimeoutError:\n        print(\"Task took too long!\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nTareas, corrutinas y futuros son todos tipos de objetos *esperables* (**awaitables**). Un objeto *esperable* es algo que puedes usar en una expresión await. Los futuros son objetos esperables de nivel más bajo que representan un resultado eventual de un cálculo.\n\n### Medir el tiempo de ejecución en corrutinas con decoradores\n\nPuedes usar decoradores para medir el tiempo de ejecución de tus corrutinas. Aquí tienes un ejemplo:\n\n```\nimport asyncio\nimport time\n\ndef timer(func):\n    async def wrapper(*args, **kwargs):\n        start_time = time.time()\n        result = await func(*args, **kwargs)\n        end_time = time.time()\n        print(f\"Time taken: {end_time - start_time} seconds\")\n        return result\n    return wrapper\n\n@timer\nasync def my_coroutine():\n    await asyncio.sleep(1)\n    print(\"Hello, Coroutine!\")\n\nasync def main():\n    await my_coroutine()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n#### ¿Por qué medir el tiempo de ejecución?\n\nAntes de sumergirse en el código, es crucial entender por qué medir el tiempo de ejecución de tus corrutinas es beneficioso en primer lugar. Aquí hay algunas razones:\n\n1. **Optimización del rendimiento:** Uno de los principales beneficios es identificar cuellos de botella o problemas de rendimiento en tu código. Si una corrutina en particular tarda demasiado en ejecutarse, podría convertirse en un candidato para optimización.\n2. **Asignación de recursos:** Comprender la complejidad temporal de tus tareas puede ayudar en una mejor asignación de recursos. Por ejemplo, podrías decidir descargar una tarea que consume mucho tiempo a un hilo de trabajo o a otra máquina.\n3. **Depuración:** Ocasionalmente, las tareas pueden colgarse o entrar en un bucle infinito. Medir el tiempo de ejecución puede servir como una herramienta de depuración para identificar tales problemas.\n4. **Experiencia del usuario:** Para aplicaciones orientadas al usuario, el tiempo de respuesta es crítico. Saber cuánto tiempo tardan las tareas puede ayudarte a tomar decisiones basadas en datos para mejorar la experiencia del usuario. Por ejemplo, podrías introducir una pantalla de carga para tareas que tardan más de unos segundos.\n5. **Escalabilidad:** Al construir un sistema que necesita escalar, es esencial entender cómo se desempeñan los componentes individuales. Medir el tiempo de ejecución de tus corrutinas te brinda información sobre cómo se desempeñará tu aplicación bajo diferentes cargas.\n\nAl estar atento al tiempo que tarda cada corrutina en ejecutarse, no solo estás escribiendo código, sino también asegurándote de que sea eficiente, escalable y proporcione una experiencia de usuario fluida.\n\n### Problemas con Corrutinas y Tareas\n\nUn problema potencial con las corrutinas y las tareas es que pueden ser difíciles de depurar. Los errores podrían no generarse hasta que se espera la corrutina, lo que puede dificultar la localización del origen del problema.\n\n#### Manejo de errores diferido en corrutinas\n\nEs importante tener en cuenta que los errores en una corrutina no se generarán hasta que se espere la corrutina. Esto se debe a que las corrutinas son esencialmente 'perezosas', lo que significa que no hacen ningún trabajo hasta que les pides explícitamente que lo hagan usando **`await`**. Esto puede ser una espada de doble filo: por un lado, proporciona más control sobre cuándo se ejecuta el código, pero por otro, puede dificultar la depuración ya que los errores se aplazan.\n\n##### Solución y Workaround:\n\n- **Usando `gather` con `return_exceptions`:** Si estás ejecutando múltiples corrutinas de forma concurrente y deseas capturar todas las excepciones, considera usar `asyncio.gather` con el parámetro **`return_exceptions=True`**. De esta manera, las excepciones se devolverán en lugar de generarse, lo que te permitirá manejarlas de manera elegante.\n\n```\nimport asyncio\n\nasync def problematic_coroutine():\n    raise ValueError(\"Some error\")\n\nasync def another_problematic_coroutine():\n    raise KeyError(\"Another error\")\n\nasync def main():\n    results = await asyncio.gather(\n        problematic_coroutine(),\n        another_problematic_coroutine(),\n        return_exceptions=True\n    )\n\n    for result in results:\n        if isinstance(result, Exception):\n            print(f\"Caught an error: {result}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### El bucle de eventos en asyncio\n\nEl bucle de eventos es el núcleo de cada aplicación asyncio. Puedes acceder y gestionar el bucle de eventos manualmente, aunque en muchos casos, las API de alto nivel de asyncio son todo lo que necesitas.\n\nAsí es como puedes acceder al bucle de eventos y usarlo para ejecutar una corrutina:\n\n```\nimport asyncio\n\nasync def my_coroutine(n):\n    print(f\"Coroutine {n} starting\")\n    await asyncio.sleep(n)  # simulate IO-bound task with sleep\n    print(f\"Coroutine {n} completed\")\n\n# Get the current event loop\nloop = asyncio.get_event_loop()\n\n# Create multiple tasks to run\ntasks = [loop.create_task(my_coroutine(i)) for i in range(1, 4)]\n\n# Gather tasks and run them\nloop.run_until_complete(asyncio.gather(*tasks))\n\nloop.close()\n```\n\nEn este script, definimos una función asíncrona `my_coroutine` que simula una tarea limitada por E/S que consume tiempo durmiendo durante un número de segundos dado por **`n`**. Luego obtenemos el bucle de eventos actual y lo usamos para crear una serie de tareas. Cada tarea es una instancia de **`my_coroutine`** con un argumento diferente para **`n`**.\n\nFinalmente, usamos `asyncio.gather` para combinar estas tareas en un solo objeto esperable, y usamos **`run_until_complete`** para ejecutar este objeto. El script imprime un mensaje cuando cada corrutina comienza y se completa, para que puedas ver que las corrutinas se ejecutan de manera concurrente.\n\nRecuerda, asyncio está diseñado para tareas limitadas por E/S y no para tareas limitadas por CPU. Si intentas usar asyncio con tareas limitadas por CPU, no obtendrás un verdadero paralelismo, porque asyncio de Python se basa en corrutinas y un bucle de eventos, que es una arquitectura de un solo hilo. Si tienes tareas limitadas por CPU, considera usar hilos o multiprocesamiento en Python.\n\nAdemás, cuando terminamos con un bucle, es una buena práctica cerrarlo (`close()`).\n\n### Modo 'Debug'\n\nEl modo de depuración de asyncio puede proporcionar información más detallada sobre tu código asyncio y ayudarte a diagnosticar problemas. Para habilitar el modo de depuración, puedes establecer la variable de entorno **`PYTHONASYNCIODEBUG`** en **`1`**, o puedes habilitarlo en tu código.\n\nasyncio realiza una comprobación para `PYTHONASYNCIODEBUG` durante la importación del módulo. Por lo tanto, necesitas configurar la variable de entorno **antes** de la primera importación de asyncio.\n\n```\nimport os\nos.environ['PYTHONASYNCIODEBUG'] = '1'\nimport logging\n\nlogging.basicConfig(level=logging.DEBUG)\n```\n\nWhen debug mode is enabled, asyncio logs more events and checks for common mistakes, such as blocking the event loop.\n\n```\nimport os\nos.environ['PYTHONASYNCIODEBUG'] = '1'\nimport asyncio\nimport time\n\nasync def my_coroutine(n):\n    print(f\"Coroutine {n} starting\")\n    await asyncio.sleep(n)\n    print(f\"Coroutine {n} completed\")\n\nasync def blocking_coroutine():\n    print(\"Blocking coroutine starting\")\n    time.sleep(2)  # Blocking call\n    print(\"Blocking coroutine completed\")\n\nasync def main():\n    tasks = [\n        asyncio.create_task(my_coroutine(1)),\n        asyncio.create_task(my_coroutine(2)),\n        asyncio.create_task(blocking_coroutine())\n    ]\n    await asyncio.gather(*tasks)\n\n# Get the current event loop\nloop = asyncio.get_event_loop()\n\n# Enable debug mode\nloop.set_debug(True)\n\n# Run the main coroutine\ntry:\n    loop.run_until_complete(main())\nfinally:\n    loop.close()\n```\n\nEn este ejemplo, definimos dos funciones asíncronas: `my_coroutine` y `blocking_coroutine`. La función `my_coroutine` es similar a la del ejemplo anterior, mientras que **`blocking_coroutine`** está diseñada para hacer una llamada bloqueante con `time.sleep()`.\n\nTambién creamos una corrutina principal que crea tareas tanto para **`my_coroutine`** como para `blocking_coroutine`. Luego ejecutamos la corrutina principal usando **`loop.run_until_complete()`**.\n\nEn este caso, la función **`blocking_coroutine`** bloqueará el bucle de eventos, lo cual es un error común al usar asyncio. Al ejecutar este script en modo de depuración, asyncio imprimirá un mensaje de advertencia para notificarte que el bucle de eventos está siendo bloqueado por la función **`blocking_coroutine`**.\n\nRecuerda habilitar el modo de depuración en producción con precaución, ya que puede ralentizar tu aplicación debido a las comprobaciones adicionales. Es mejor usarlo durante el desarrollo y las pruebas para detectar cualquier problema potencial con tu código asyncio.\n\n### Conclusión\n\n¡Bien hecho al concluir este tutorial sobre los conceptos básicos de asyncio! Ahora tienes un sólido conocimiento de los principios fundamentales de asyncio, desde corrutinas y tareas hasta depuración y gestión del bucle de eventos.\n\nPero hay más en asyncio. Su verdadero potencial se despliega al gestionar flujos de trabajo complejos y coordinar entre diferentes tareas. Aquí es donde entran en juego temas avanzados como primitivas de sincronización, colas, semáforos y transportes y protocolos.\n\nEn nuestro próximo artículo, profundizaremos en estos conceptos avanzados de asyncio, equipándote con un conjunto de habilidades integral para enfrentar desafíos complejos de programación asíncrona en Python. ¡Continúa practicando, explorando y nos vemos en nuestro próximo tutorial para más aventuras de aprendizaje!"},{"slug":"asyncio-tutorial-for-the-programmer-in-a-hurry","lang":"en","title":"'asyncio' tutorial for the programmer in a hurry","description":"Introduction In the evolving world of Python programming, understanding the asyncio library is becoming increasingly important. asyncio allows for asynchronous, concurrent, and parallel programming in…","publishDate":"2023-09-07","updatedDate":null,"tags":["asynchronous-programming","programming","python"],"translationSlug":"tutorial-asyncio-para-el-programador-con-el-tiempo-en-contra","url":"https://alexisalulema.com/blog/asyncio-tutorial-for-the-programmer-in-a-hurry/","series":null,"headings":[{"depth":2,"slug":"introduction","text":"Introduction"},{"depth":2,"slug":"coroutines","text":"Coroutines"},{"depth":3,"slug":"async-context-managers-and-async-iterators","text":"Async Context Managers and Async Iterators"},{"depth":3,"slug":"tasks-and-concurrency","text":"Tasks and concurrency"},{"depth":3,"slug":"measure-execution-time-in-coroutines-with-decorators","text":"Measure Execution Time in Coroutines with Decorators"},{"depth":3,"slug":"problems-with-coroutines-and-tasks","text":"Problems with Coroutines and Tasks"},{"depth":3,"slug":"the-event-loop-in-asyncio","text":"The Event-loop in asyncio"},{"depth":3,"slug":"debug-mode","text":"Debug Mode"},{"depth":3,"slug":"conclusion","text":"Conclusion"}],"body":"## Introduction\n\nIn the evolving world of Python programming, understanding the asyncio library is becoming increasingly important. asyncio allows for asynchronous, concurrent, and parallel programming in a language that was traditionally synchronous and single-threaded.\n\nAs Python applications grow in complexity and depend on I/O-bound tasks like web requests or database calls, it's crucial to manage these tasks efficiently. That's where asyncio shines. However, getting started with asyncio can be daunting, especially if you're not familiar with coroutines, tasks, futures, and event loops. This tutorial aims to guide you through the basics of asyncio, complete with functional examples.\n\nBy the end of this tutorial, you'll have a solid grasp of asyncio and be ready to implement it in your projects, whether you're building a web scraper, microservice, or GUI application.\n\nSo let's dive into the world of Python asyncio!\n\n## Coroutines\n\nA ***coroutine*** is a generalization of a subroutine that can be paused and resumed, allowing other code to run during the pauses. Coroutines are a key part of asyncio and allow us to write asynchronous code in a procedural style.\n\nIn Python, coroutines are defined with the `async def` syntax:\n\n```\nasync def my_coroutine():\n    print(\"Hello, Coroutine!\")\n```\n\nTo run the coroutine, you can't just call it like a normal function. You must use the `await` keyword:\n\n```\nawait my_coroutine()\n```\n\nLet's simulate a long-running task with asyncio's `sleep` function:\n\n```\nimport asyncio\n\nasync def my_coroutine():\n    await asyncio.sleep(1)  # sleep for 1 second\n    print(\"Hello, Coroutine!\")\n\nasync def main():\n    await my_coroutine()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Async Context Managers and Async Iterators\n\nIn real-world applications, you often need to manage resources like file streams, network connections, or database connections. Using context managers and iterators asynchronously can be very useful for such tasks. Luckily, Python’s asyncio provides native support for async context managers and async iterators, which make your code not only more efficient but also cleaner and more maintainable.\n\n#### Using `async with`\n\nThe `async with` statement is used to invoke an asynchronous context manager. For example, when working with asynchronous file operations, you can read a file without blocking the event loop like this:\n\n```\nimport aiofiles\nimport chardet\nimport asyncio\n\nasync def read_large_file():\n    async with aiofiles.open('large_file.txt', mode='rb') as f:\n        raw_data = await f.read()\n\n    detected = chardet.detect(raw_data)\n    encoding_type = detected['encoding']\n    contents = raw_data.decode(encoding_type)\n    print(contents)\n\nasync def main():\n    await read_large_file()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nOne more example:\n\n```\n# Async context managers for managing resources\nasync with aiohttp.ClientSession() as session:\n    async with session.get('https://api.example.com/data') as resp:\n        data = await resp.json()\n```\n\n**Best Practice:** Always prefer using `async with` for acquiring and releasing resources.\n\n#### Using `async for`\n\nThe `async for` statement can be used with an object that is an asynchronous iterator. For instance, to asynchronously fetch multiple URLs:\n\n```\nimport asyncio\n\n# Define an asynchronous generator simulating fetching data from URLs\nasync def fetch_data_from_urls():\n    urls = [\"https://www.google.com\", \"https://www.apple.com\", \"https://www.microsoft.com\"]\n    for url in urls:\n        await asyncio.sleep(2)  # Simulate network delay\n        yield f\"Fetched data from {url}\"\n\n# Define a coroutine that uses the asynchronous generator\nasync def process_fetched_data():\n    async for data in fetch_data_from_urls():\n        print(f\"Processing: {data}\")\n\n# Run the asynchronous event loop\nasyncio.run(process_fetched_data())\n```\n\nBoth `for` and `async for` loops in Python have similar purposes: they are used for iterating over elements. However, they are not interchangeable and are designed for different types of iterables.\n\n- The `for` loop is used for iterating over synchronous iterables, such as lists, or synchronous generators.\n\n```\nfor item in [1, 2, 3]:\n    print(item)\n```\n\n- The `async for` loop is designed for iterating over asynchronous iterables or asynchronous generators.\n\n```\nasync for item in async_generator():\n    print(item)\n```\n\nThe key difference is that you can't use a standard `for` loop to iterate over asynchronous iterables or asynchronous generators. Likewise, you can't use an `async for` loop to iterate over traditional, synchronous iterables or generators.\n\nIf you try to mix them up, Python will raise a `TypeError`.\n\n**Best Practice:** Use `async for` when working with asynchronous iterators to make your code more readable and maintainable.\n\n### Tasks and concurrency\n\nTasks are used to schedule coroutines concurrently. When a coroutine is wrapped into a `Task` with functions like `asyncio.create_task()`, the coroutine is automatically scheduled to run soon.\n\n```\nimport asyncio\n\nasync def my_coroutine():\n    await asyncio.sleep(1)\n    print(\"Hello, Coroutine!\")\n\nasync def main():\n    # Create a task out of a coroutine\n    task = asyncio.create_task(my_coroutine())\n    # wait for the task to finish\n    await task\n\n# Run the main coroutine\nasyncio.run(main())\n```\n\n**IMPORTANT!** You cannot just execute `my_coroutine()` by invoking it directly in the next line:\n\n```\ntask = asyncio.create_task(my_coroutine())\n```\n\nIf you do this outside of an asynchronous function, you will receive the following error:\n\n```\nRuntimeError: no running event loop\n```\n\nIn Python's `asyncio`, the \"event loop\" is like a manager for tasks that should run concurrently. This error happens when you're trying to do something that requires this manager, but it's not there.\n\nIn simpler terms, you're trying to perform an operation that should run in the background (asynchronously), but there's no system (the \"event loop\" that I will explain later in this post) in place to handle this kind of operation.\n\nCommonly, this error happens because you're running `asyncio` code outside of an async function. To fix this, ensure your asyncio code is being run inside an async function and an event loop is running. To fix this error, just run it in this way:\n\n```\nasyncio.run(my_coroutine())\n```\n\nTasks can be canceled if they're no longer needed. This is done with the `cancel` method on the Task object. You can also set timeouts for tasks by using the `asyncio.wait_for` function:\n\n```\nimport asyncio\n\nasync def my_coroutine():\n    await asyncio.sleep(10)\n    print(\"Hello, Coroutine!\")\n\nasync def main():\n    # Create a task out of a coroutine\n    task = asyncio.create_task(my_coroutine())\n\n    # Cancel the task\n    task.cancel()\n\n    # Set a timeout for a task\n    try:\n        await asyncio.wait_for(my_coroutine(), timeout=1.0)\n    except asyncio.TimeoutError:\n        print(\"Task took too long!\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n**Tasks**, **coroutines**, and **futures** are all types of awaitables. An awaitable is something you can use in an `await` expression. Futures are lower-level awaitables that represent an eventual result of a computation.\n\n### Measure Execution Time in Coroutines with Decorators\n\nYou can use decorators to measure the execution time of your coroutines. Here's an example:\n\n```\nimport asyncio\nimport time\n\ndef timer(func):\n    async def wrapper(*args, **kwargs):\n        start_time = time.time()\n        result = await func(*args, **kwargs)\n        end_time = time.time()\n        print(f\"Time taken: {end_time - start_time} seconds\")\n        return result\n    return wrapper\n\n@timer\nasync def my_coroutine():\n    await asyncio.sleep(1)\n    print(\"Hello, Coroutine!\")\n\nasync def main():\n    await my_coroutine()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n#### Why measure execution time?\n\nBefore diving into the code, it's crucial to understand why measuring the execution time of your coroutines is beneficial in the first place. Here are some reasons:\n\n1. **Performance Optimization**: One of the key benefits is to identify bottlenecks or performance issues in your code. If a particular coroutine is taking too long to execute, it might become a candidate for optimization.\n2. **Resource Allocation**: Understanding the time complexity of your tasks can help in better resource allocation. For example, you might decide to offload a time-consuming task to a worker thread or another machine.\n3. **Debugging**: Occasionally, tasks may hang or enter into an infinite loop. Measuring execution time can serve as a debugging tool to identify such issues.\n4. **User Experience**: For user-facing applications, response time is critical. Knowing how long tasks take can help you make data-driven decisions to improve user experience. For example, you may introduce a loading screen for tasks that take more than a few seconds.\n5. **Scalability**: When building a system that needs to scale, it's essential to understand how individual components perform. Measuring the execution time of your coroutines gives you insights into how your application will perform under different loads.\n\nBy keeping an eye on the time it takes for each coroutine to execute, you’re not just writing code, but also ensuring that it's efficient, scalable, and provides a smooth user experience.\n\n### Problems with Coroutines and Tasks\n\nOne potential issue with coroutines and tasks is that they can be difficult to debug. Errors might not be raised until the coroutine is awaited, which can make it hard to track down the source of the problem.\n\n#### Deferred Error Handling in Coroutines\n\nIt's important to note that errors in a coroutine won't be raised until the coroutine is awaited. This is because coroutines are essentially 'lazy', meaning they don’t do any work until you explicitly ask them to by using `await`. This can be a double-edged sword: on the one hand, it provides more control over when the code is executed, but on the other, it can make debugging a bit tricky since errors are deferred.\n\n##### Solution and Workaround:\n\n- **Using `gather` with `return_exceptions`**: If you're running multiple coroutines concurrently and want to capture all exceptions, consider using `asyncio.gather` with the `return_exceptions=True` parameter. This way, exceptions will be returned instead of being raised, allowing you to handle them gracefully\n\n```\nimport asyncio\n\nasync def problematic_coroutine():\n    raise ValueError(\"Some error\")\n\nasync def another_problematic_coroutine():\n    raise KeyError(\"Another error\")\n\nasync def main():\n    results = await asyncio.gather(\n        problematic_coroutine(),\n        another_problematic_coroutine(),\n        return_exceptions=True\n    )\n\n    for result in results:\n        if isinstance(result, Exception):\n            print(f\"Caught an error: {result}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### The Event-loop in asyncio\n\nThe event loop is the core of every asyncio application. You can access and manage the event loop manually, although in many cases, asyncio's high-level APIs are all you need.\n\nHere's how to access the event loop and use it to run a coroutine:\n\n```\nimport asyncio\n\nasync def my_coroutine(n):\n    print(f\"Coroutine {n} starting\")\n    await asyncio.sleep(n)  # simulate IO-bound task with sleep\n    print(f\"Coroutine {n} completed\")\n\n# Get the current event loop\nloop = asyncio.get_event_loop()\n\n# Create multiple tasks to run\ntasks = [loop.create_task(my_coroutine(i)) for i in range(1, 4)]\n\n# Gather tasks and run them\nloop.run_until_complete(asyncio.gather(*tasks))\n\nloop.close()\n```\n\nIn this script, we define an asynchronous function `my_coroutine` that simulates a time-consuming IO-bound task by sleeping for a number of seconds given by `n`. We then get the current event loop and use it to create a number of tasks. Each task is an instance of `my_coroutine` with a different argument for `n`.\n\nFinally, we use `asyncio.gather` to combine these tasks into a single awaitable object, and we use `run_until_complete` to run this object. The script prints a message when each coroutine starts and completes, so you can see that the coroutines run concurrently.\n\nRemember, asyncio is designed for IO-bound tasks and not for CPU-bound tasks. If you try to use asyncio with CPU-bound tasks, you won't get true parallelism, because Python's asyncio is based on coroutines and an event loop, which is a single-threaded architecture. If you have CPU-bound tasks, consider using threading or multiprocessing in Python.\n\nAlso, when we're finished with a loop, it's good practice to `close()` it.\n\n### Debug Mode\n\nAsyncio's debug mode can provide more detailed information about your asyncio code and can help you diagnose issues. To enable debug mode, you can set the `PYTHONASYNCIODEBUG` environment variable to `1`, or you can enable it in your code.\n\n`asyncio` performs check for `PYTHONASYNCIODEBUG` on module importing. Thus you need setup environment variable **before** very first asyncio import:\n\n```\nimport os\nos.environ['PYTHONASYNCIODEBUG'] = '1'\nimport logging\n\nlogging.basicConfig(level=logging.DEBUG)\n```\n\nWhen debug mode is enabled, asyncio logs more events and checks for common mistakes, such as blocking the event loop.\n\n```\nimport os\nos.environ['PYTHONASYNCIODEBUG'] = '1'\nimport asyncio\nimport time\n\nasync def my_coroutine(n):\n    print(f\"Coroutine {n} starting\")\n    await asyncio.sleep(n)\n    print(f\"Coroutine {n} completed\")\n\nasync def blocking_coroutine():\n    print(\"Blocking coroutine starting\")\n    time.sleep(2)  # Blocking call\n    print(\"Blocking coroutine completed\")\n\nasync def main():\n    tasks = [\n        asyncio.create_task(my_coroutine(1)),\n        asyncio.create_task(my_coroutine(2)),\n        asyncio.create_task(blocking_coroutine())\n    ]\n    await asyncio.gather(*tasks)\n\n# Get the current event loop\nloop = asyncio.get_event_loop()\n\n# Enable debug mode\nloop.set_debug(True)\n\n# Run the main coroutine\ntry:\n    loop.run_until_complete(main())\nfinally:\n    loop.close()\n```\n\nIn this example, we define two async functions: `my_coroutine` and `blocking_coroutine`. The `my_coroutine` function is similar to the one in the previous example, while `blocking_coroutine` is designed to make a blocking call with `time.sleep()`.\n\nWe also create a `main` coroutine that creates tasks for both `my_coroutine` and `blocking_coroutine`. We then run the `main` coroutine using `loop.run_until_complete()`.\n\nIn this case, the `blocking_coroutine` function will block the event loop, which is a common mistake when using asyncio. When running this script in debug mode, asyncio will print a warning message to notify you that the event loop is being blocked by the `blocking_coroutine` function.\n\nRemember to enable debug mode in production cautiously, as it may slow down your application due to the additional checks. It's better to use it during development and testing to catch any potential issues with your asyncio code.\n\n### Conclusion\n\nWell done on wrapping up this tutorial on asyncio basics! You now have a firm grasp of core asyncio principles, from coroutines and tasks to debugging and managing the event loop.\n\nBut there's more to asyncio! Its true potential unfolds when managing complex workflows and coordinating between different tasks. This is where advanced topics like synchronization primitives, Queues, Semaphores, and Transports and Protocols come into play.\n\nIn our next article, we'll delve deeper into these advanced asyncio concepts, equipping you with a comprehensive skill set for handling complex asynchronous programming challenges in Python. Continue to practice, explore, and we'll see you in our next tutorial for more learning adventures!"},{"slug":"transformadores-como-funcionan-internamente","lang":"es","title":"Transformadores: ¿cómo funcionan internamente?","description":"Tabla de contenidos Introducción Incrustación de Entrada (Input Embedding) Codificación Posicional (Positional Encoding PE) El Codificador Mecanismo de Auto-atención Mecanismo de Atención Multicabezal…","publishDate":"2023-04-23","updatedDate":null,"tags":["machine-learning","python","transformers"],"translationSlug":"transformers-how-do-they-work-internally","url":"https://alexisalulema.com/es/blog/transformadores-como-funcionan-internamente/","series":null,"headings":[{"depth":3,"slug":"tabla-de-contenidos","text":"Tabla de contenidos"},{"depth":3,"slug":"introducción","text":"Introducción"},{"depth":3,"slug":"incrustación-de-entrada-input-embedding","text":"Incrustación de Entrada (Input Embedding)"},{"depth":3,"slug":"codificación-positional-positional-encoding-pe","text":"Codificación Positional (Positional Encoding PE)"},{"depth":3,"slug":"el-codificador","text":"El codificador"},{"depth":3,"slug":"el-decodificador","text":"El decodificador"},{"depth":3,"slug":"red-prealimentada-1","text":"Red Prealimentada"},{"depth":3,"slug":"capa-lineal-y-capa-softmax","text":"Capa Lineal y Capa Softmax"},{"depth":3,"slug":"entrenamiento-del-transformador","text":"Entrenamiento del Transformador"},{"depth":3,"slug":"conclusión","text":"Conclusión"}],"body":"### Tabla de contenidos\n\n1. [Introducción](#intro)\n2. [Incrustación de Entrada (Input Embedding)](#input-embedding)\n3. [Codificación Posicional (Positional Encoding PE)](#pe)\n4. [El Codificador](#encoder)\n   1. [Mecanismo de Auto-atención](#self-attention)\n   2. [Mecanismo de Atención Multicabezal](#multi-head)\n   3. [Red Prealimentada](#ffn)\n5. [El Decodificador](#decoder)\n   1. [Atención Multicabezal Enmascarada](#masked-multi-head)\n   2. [Atención Multicabezal](#multi-head-2)\n   3. [Red Prealimentada](#ffn-2)\n6. [Capa Lineal y Capa Softmax](#linear-softmax)\n7. [Entrenamiento del Transformador](#training)\n8. [Conclusión](#conclusion)\n\n### Introducción\n\nEl Transformador es actualmente una de las arquitecturas más populares para el Procesamiento del Lenguaje Natural (NLP). Periódicamente, podemos escuchar noticias sobre nuevas arquitecturas y modelos basados en transformadores que generan mucho interés y expectativas en la comunidad.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/image-695x1024.png)\n\nArquitectura Original del Transformador  \nFuente: [Attention is all you need](https://arxiv.org/abs/1706.03762?context=cs)\n\nGoogle Research y miembros de Google Brain propusieron inicialmente el Transformador en el artículo de 2017 [**Attention is all you need**](https://arxiv.org/abs/1706.03762?context=cs). Aunque podemos descargarlo y leerlo, sus conceptos aún están restringidos a los iniciados en la elegante nomenclatura matemática.\n\nEl Transformador superó a otras arquitecturas existentes basadas en [**LSTM**](https://en.wikipedia.org/wiki/Long_short-term_memory) y [**RNN**](https://es.wikipedia.org/wiki/Red_neuronal_recurrente), obteniendo mejores resultados en las evaluaciones y un entrenamiento más rápido. El mecanismo de atención del Transformador es una operación de \"palabra a palabra\" que encuentra cómo cada palabra se relaciona con las demás en la secuencia, incluida la misma palabra. En el presente documento, explicaré la arquitectura y el funcionamiento interno de un Transformador de la manera más simple posible.\n\n### Incrustación de Entrada (Input Embedding)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/input.embedding.png)\n\nComenzando desde el principio, la arquitectura del Transformador inicia con la subcapa de Incrustación de Entrada (Input Embedding), que convierte la secuencia de entrada en vectores de dimensión $d_{model}=512$.\n\n🎓 El valor de $d_{model}=512$ fue establecido por los diseñadores de la arquitectura para establecer una dimensión constante a la salida de cada subcapa del modelo. El valor de esta dimensión puede ser modificada dependiendo de los objetivos.\n\nUn tokenizador transforma el flujo de entrada en tokens normalizando el texto a minúsculas y dividiéndolo en subpartes; además, proporcionará una representación vectorial de enteros (basada en un vocabulario existente) que se utilizará para el proceso de incrustación (embedding). Por ejemplo:\n\n```\nInput = \"Roses are red and violets are blue\"\nTokens = [\"roses\", \"are\", \"red\", \"and\", \"violets\", \"are\", \"blue\"]\nTokenized = [8271, 1029, 3674, 9273, 2384, 1029, 9873]\n```\n\nA continuación, la subcapa de incrustación recibe el vector tokenizado. Para cada palabra, esta subcapa debe producir un vector de tamaño $d_{model}=512$. Por ejemplo, para las palabras \"rojo\" y \"azul\", que son colores, los **vectores de incrustación de palabras** deberían ser similares::\n\n```\nblue=[ 0.36138474, -0.16811648, -0.03733656, -0.58750702, -0.81279167,\n       -0.86249844,  0.69673459, -0.79213212,  0.9278906 ,  0.42308278,\n       -0.12308109,  0.2383174 ,  0.44863208,  0.98666162, -0.12830655,\n       -0.56420363,  0.69459217,  0.72405279,  0.92023563, -0.84536481,\n        0.86299045, -0.88166481, -0.9087216 ,  0.99420482, -0.73118714,\n...\n       -0.47495972, -0.94366021, -0.97624231, -0.9792538 ,  0.20736778,\n        0.1248088 ,  0.6344501 , -0.54432975, -0.35632176, -0.6670839 ,\n       -0.48141856, -0.3503394 , -0.94319604,  0.48421567, -0.12854877,\n       -0.48260166, -0.845398  ,  0.67561689,  0.29778234,  0.03009221,\n        0.25067641, -0.81864996, -0.51513235, -0.44608639, -0.65686229]\n```\n\n🎓 **La similitud del coseno** se puede utilizar para verificar si dos vectores de incrustación (embeddings) son similares. Para obtener más información sobre la teoría: [Similitud del Coseno en Scikit-Learn](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise.cosine_similarity.html).\n\nLos vectores de incrustación proporcionan mucha información al Transformador acerca de cómo las palabras en una secuencia están relacionadas. Sin embargo, aún se necesita información para indicar la posición de las palabras en una secuencia, y para esto se utiliza el proceso de **Codificación Posicional**.\n\n### Codificación Positional (Positional Encoding PE)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/positional.encoding.png)\n\nCada palabra en la secuencia inicial debe tener información de PE, pero la generación de este vector debe ser simple, ya que el enfoque principal del Transformador es el mecanismo de atención.\n\nEl desafío en esta tarea es generar un vector de dimensión $d_{model}=512$ para cada vector de salida de la función PE. Los autores de la arquitectura utilizaron ingeniosamente una esfera unitaria para representar la PE con valores de seno y coseno.\n\n🎓 Con la idea de la esfera unitaria, los autores propusieron funciones seno y coseno que pueden generar diferentes valores de PE para cada dimensión $i$ de las 512 establecidas en el vector de incrustación de palabras (word embedding).  \n  \n $PE_{(pos\\:2i)}=sin \\left (\\frac{pos}{10000^{\\frac{2i}{d_{model}}}} \\right)$([Plot sin in Google](https://www.google.com/search?q=plot+y%3Dsin(2%2F10000%5E(2*x%2F512))))  \n $PE_{(pos\\:2i+1)}=cos \\left (\\frac{pos}{10000^{\\frac{2i}{d_{model}}}} \\right)$ ([Plot cos in Google](https://www.google.com/search?q=plot+y%3Dcos(2%2F10000%5E(2*x%2F512))))  \n  \nEl proceso aplicará la función seno a los números pares $i\\: \\in \\left [ 0,\\:255\\right ]$ y la función coseno a los números impares $i\\: \\in \\left [ 256,\\:512\\right ]$. Los valores de PE (Posición Codificada) se codifican dentro de la esfera unitaria y permiten una representación sencilla de la posición de los elementos en la secuencia.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/unit.sphere.png)\n\nEl siguiente ejemplo es una traducción en Python del concepto de Codificación Posicional, evaluado para la posición 3:\n\n```\nimport math\n\nd_model = 512\n\ndef positional_encoding(position):\n    pe = [None] * d_model\n\n    for i in range(0, 512, 2):\n        pe[i] = math.sin(position / (10000 ** ((2 * i) / d_model)))\n        pe[i + 1] = math.cos(position / (10000 ** ((2 * i) / d_model)))\n\n    return pe\n\nprint(positional_encoding(3))\n```\n\nPor ejemplo, supongamos que tenemos la oración \"The sky is blue,\" la matriz de Codificación Posicional PE será:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/PE_1.gif)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/PE_2.gif)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/PE_3.gif)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/PE_4.gif)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/PE_5.gif)\n\nFinalmente, el vector de codificación de palabras debe sumarse al vector de codificación posicional, y el bloque codificador/decodificador será alimentado con el resultado:\n\n$$\nEmbedding_{output} = Embedding_{initial}+PE_{word\\:position}\n$$\n\nSin embargo, los valores de incrustación de palabras a veces son demasiado pequeños y podrían ser prácticamente desconsiderados. En este caso, la solución es escalar los valores, por ejemplo, utilizando la media del PE.\n\n### El codificador\n\nEn su visión más simple, el Transformador es una arquitectura de codificador-decodificador, donde el codificador aprende la representación de Codificación Posicional del texto de entrada y la envía al decodificador. El decodificador recibe lo que ha sido aprendido por el codificador y genera una salida.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/encoder-decoder-1-1024x275.png)\n\nInternamente, en más detalle, el Transformador consiste en una pila de $N$ codificadores, cada uno enviando su salida al siguiente. El último codificador devuelve la representación de la secuencia de entrada. Para la explicación, a partir de ahora, utilizaremos un valor de $N=2$.\n\n🎓 Los autores originales del Transformador asignaron el valor de $N=6$ en **\"Attention is all you need\"**.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/encoders.png)\n\nCada bloque decodificador está compuesto de 2 subcapas:\n\n- Atención multicabezal\n- Red Prealimentada\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/encoders.inside.png)\n\nAntes de empezar a explicar esos dos componentes, es necesario entender el mecanismo de auto-atención.\n\n#### Mecanismo de Auto-atención\n\nConsidera la siguiente oración:\n\n`John and Paul wrote several songs when they were inspired.`\n\nEn esta oración, el mecanismo de auto-atención calcula la representación de cada palabra, y la relación con las otras palabras en la oración proporciona más información acerca de la palabra. Por ejemplo, el término \"they\" debería estar relacionado con \"John\" y \"Paul\" y no con \"songs\".\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/self-attention-1024x814.png)\n\nUn ejemplo más sencillo para comprender cómo funciona el mecanismo de auto-atención es la oración \"The sky is blue\". Los codificadores reciben vectores de incrustación de palabras de dimensión $d_{model}=512$ de cada palabra de la oración, por ejemplo:\n\n$x_1=\\begin{bmatrix} 3.23 & 0.65 & ... & 4.78 \\end{bmatrix}$ ==> \"The\"  \n$x_2=\\begin{bmatrix} 1.26 & 6.35 & ... & 7.99 \\end{bmatrix}$ ==> \"sky\"  \n$x_3=\\begin{bmatrix} 9.25 & 1.68 & ... & 4.26 \\end{bmatrix}$ ==> \"is\"  \n$x_4=\\begin{bmatrix} 6.84 & 2.98 & ... & 11.48 \\end{bmatrix}$ ==> \"blue\"\n\nCon estos vectores, podemos ensamblar la matriz de incrustación $X$, con $d=[4 \\times 512]$:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/embedding_matrix.gif)\n\nCrearemos tres matrices adicionales a partir de esta matriz X que sirve como el \"mecanismo de auto-atención\":\n\n- $Q$, matriz de consulta\n- $K$, matriz de clave\n- $V$, matriz de valor\n\nPara crear estos arreglos, necesitamos además tres nuevas matrices de pesos. La dimensión utilizada en el artículo original era $d_k=64$; por lo tanto, los vectores de peso tendrían una dimensión de $d_{model} \\times d_k\\Rightarrow 512 \\times 64$, que se inicializan con valores aleatorios:\n\n- $W^Q$, matriz de pesos de consulta\n- $W^K$, matriz de pesos de clave\n- $W^V$, matriz de pesos de valor\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QKV_neuron-1024x430.png)\n\nLas matrices de pesos contienen los valores óptimos aprendidos durante el entrenamiento, por lo que cada matriz $Q$, $K$, y $V$, es el producto de la matriz de incrustación (embedding) $X$ con la matriz de pesos correspondiente, lo que genera matrices de $4 \\times 64$:\n\n- $Q = X \\times W^Q$\n- $K = X \\times W^K$\n- $V = X \\times W^V$\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QKV.gif)\n\nCada una de las cuatro filas en cada matriz representa cada palabra de la oración inicial, \"The sky is blue.\"\n\n##### Proceso del Mecanismo de Auto-atención\n\n1. Computar el producto punto $Q \\cdot K^T$\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QdotKT-1-1024x140.gif)\n\nLos elementos del arreglo resultante indican la relación entre las palabras. Por ejemplo,$q_1.k_1$ es la relación de la palabra \"The\" consigo misma y tiene un valor alto, pero $q_1.k_3$ es la relación entre \"The\" y \"is\" y tiene un valor bajo. La relación entre \"sky\" y \"blue\" ($q_2.k_4$) tendrá un valor un poco más alto porque hay una relación entre el sustantivo y el adjetivo. Por ejemplo:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QdotKT.2.gif)\n\nDe esta manera, podemos decir que calcular el producto escalar entre la matriz de consulta, $Q$, y la matriz de clave, $K^T$, esencialmente nos da el valor de similitud, que nos ayuda a entender cuán similar es cada palabra en la oración a todas las demás palabras.\n\n2. Computar $QK^T/\\sqrt{d_k}$. Esta acción es útil para obtener gradientes estables, donde $d_k=64$ es la dimensión del vector clave.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QdotKTSqrtDK.gif)\n\nLos valores de la matriz resultante deben ser normalizados, y si utilizamos la función $Softmax\\left ( \\frac{QK^T}{\\sqrt d_k} \\right )$ (ver [Softmax](https://es.wikipedia.org/wiki/Funci%C3%B3n_SoftMax)), podemos formalizar los valores en un rango de 0 a 1.\n\n```\nimport numpy as np\n\ndef softmax(x):\n    max = np.max(x,axis=1,keepdims=True)\n    e_x = np.exp(x - max)\n    sum = np.sum(e_x,axis=1,keepdims=True)\n    f_x = e_x / sum\n    return f_x\n\nV = np.array([[13.75, 11.50, 7.75, 7.50],\n        [11.88, 12.38, 11.25, 10],\n        [8.13, 11.25, 13.75, 8.75],\n        [7.5, 11.25, 9.38, 13.13]])\nsoftmax(V)\n```\n\n```\narray([[0.90105641, 0.09497065, 0.0022335 , 0.00173945],\n       [0.29994872, 0.49453184, 0.15975023, 0.04576921],\n       [0.00331791, 0.07513861, 0.91537572, 0.00616775],\n       [0.00304195, 0.12934693, 0.01993542, 0.8476757 ]])\n```\n\nLa suma de los valores en cada fila es igual a 1. Con estos valores, podemos entender cómo cada palabra en la oración se relaciona con todas las demás palabras. Esto se llama la **matriz de puntuación (score matrix)**.\n\n3. Luego, es necesario calcular la **matriz de atención** $Z$:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/SoftmaxV.gif)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/Z.SoftmaxV-1024x148.gif)\n\n$$\nZ=\\begin{bmatrix} 4.18336201 & 0.65278898 & ... & 4.22437426 \\\\ 5.94802206 & 3.00072113 & ... & 4.4028484 \\\\ 3.09529998 & 1.49925765 & ... & 9.9459073 \\\\ 4.76015677 & 2.69495094 & ... & 2.17709763 \\end{bmatrix}\n$$\n\nLa matriz de atención $Z$ es una matriz de, en el ejemplo de la oración, 4 filas y 512 columnas. Cada fila corresponde al vector de auto-atención de la palabra correspondiente.\n\nEl mecanismo de auto-atención se llama **atención de producto punto escalado** ya que calculamos el producto punto entre los vectores $Q$ y $K$ y escalamos los valores por ($\\sqrt {d_k}$).\n\n#### Mecanismo de atención multicabezal\n\nPara el Transformador, vamos a calcular varias matrices de atención. ¿Pero por qué necesitamos varias matrices? Esto nos ayuda en contextos idiomáticos donde el significado de una palabra es ambiguo, por ejemplo:\n\n*Tom was crying because he was blue.*\n\nUn único mecanismo de atención decidiría que Tom estaba llorando porque su color es azul (blue), siendo dominado por la palabra \"Tom\". Y **si en la mayoría de las oraciones en las que \"blue\" implica que es un color, al tener solo una \"cabeza de atención\", el mecanismo aprenderá correctamente que es un color. Sin embargo, al tener \"múltiples cabezas de atención\", es más probable que uno de esos mecanismos de atención aprenda de las oraciones en las que se implica que \"azul\" es un estado de ánimo, y al concatenar los resultados de las \"múltiples cabezas de atención\", la matriz de atención será más precisa**.\n\n¿Cómo podemos computar matrices de atención múltiple? Sugongamos que vamos a computar dos matrices de atención: $Z_1$ y $Z_2$.\n\nPara computar $Z_1$ primero, creamos las tres matrices $Q_1$, $K_1$, y $V_1$, lo que implica multiplicar la matriz de incrustación y las tres matrices de peso $W_{1}^{Q}$, $W_{1}^{K}$, y $W_{1}^{V}$. Ahora, la matriz de atención se computa de la siguiente manera:\n\n$$\nZ_1=Softmax\\left ( \\frac{Q_1K_{1}^{T}}{\\sqrt{d_k}} \\right ) \\cdot V_1\n$$\n\nDe la misma manera para $Z_2$\n\n$$\nZ_2=Softmax\\left ( \\frac{Q_2K_{2}^{T}}{\\sqrt{d_k}} \\right ) \\cdot V_2\n$$\n\nDe esta manera, podemos calcular cualquier cantidad de matrices de atención. Supongamos que necesitamos ocho matrices de atención (el valor en \"La atención lo es todo\"). En ese caso, podemos concatenar todas las cabezas de atención y multiplicar el resultado por una nueva matriz de pesos $W_0$ entrenada para representar los valores óptimos para el mecanismo de atención.\n\n$$\nMulti-head \\: attention=Concatenate\\left ( Z_1, Z_2, Z_3, Z_4, Z_5, Z_6, Z_7, Z_8 \\right ) \\cdot W_0\n$$\n\n#### Red Prealimentada\n\nLa [red prealimentada](https://es.wikipedia.org/wiki/Red_neuronal_prealimentada) consiste en solo dos capas densas con activación [ReLU](https://www.alexisalulema.com/es/2022/09/23/funciones-de-activacion-en-tensorflow/#relu). Los mismos parámetros se aplican en diferentes partes de la oración, pero es distinto a los bloques del codificador.\n\nUn componente adicional para conectar la entrada y los bloques del codificador es el **componente de suma y normalización**, que es una conexión seguida de la **normalización de capa**.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/addNnorm-1024x363.png)\n\nLa normalización de capas permite un entrenamiento más rápido al evitar que los valores en cada capa cambien drásticamente.\n\n### El decodificador\n\nDe la misma manera que el codificador, podemos tener una pila de $N$ decodificadores (por ejemplo, asumimos que $N=2$). La representación de la secuencia producida por los codificadores es la entrada de todos los decodificadores; es decir, un decodificador recibe dos entradas, una del decodificador anterior y la representación hecha por el codificador.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/encoderNdecoder.png)\n\nPara comprender cómo el decodificador genera la oración objetivo, echemos un vistazo a la siguiente imagen que describe la entrada al decodificador como una serie temporal.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/encoderNdecoder_time.png)\n\n<sos>: Start Of the Sentence  \n<eos>: End Of the Sentence\n\nEn cada paso de tiempo, el decodificador compara la palabra recién generada con la entrada y predice la siguiente entrada. Una vez que se genera el token **<eos>**, el decodificador ha terminado de generar la oración objetivo.\n\nAl igual que el incrustado de entrada para los codificadores, la oración *<sos>\" El cielo es azul\"* debe ser incrustada para alimentar a los decodificadores.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/output.embedding.png)\n\nPara entender cómo funciona el decodificador, debes explorar sus componentes en la imagen a continuación:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/decoder.block_.png)\n\n#### Atención Multicabezal Enmascarada\n\nLa atención multicabezal enmascarada es similar al mecanismo de atención multicabezal con una pequeña diferencia\n\nDurante una prueba, el decodificador generará palabras siempre que haya un valor previo; es decir, para la entrada en **\"t2: <sos> El\"**, el modelo se entrena solo con los tokens **<sos>** y **El**, y el mecanismo de atención debe coincidir con las palabras solo hasta la palabra **El** y no para las palabras faltantes en la oración. El resto de las palabras en la secuencia pueden ser enmascaradas, lo que ayuda al mecanismo de atención a prestar atención únicamente a las palabras disponibles durante las pruebas.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/masking_values-1.gif)\n\nDebemos calcular la matriz de atención $Z$ de la misma manera que lo hemos estado haciendo, con la diferencia de que los valores correspondientes a la palabra enmascarada de la oración reciben un valor de $-\\infty$. Por ejemplo:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QKT_masked.gif)\n\nCon esto, podemos obtener la matriz de atención final como lo hicimos para el codificador, y esta matriz alimentará la siguiente capa de atención multicabezal.\n\n$$\nMasked \\; multihead \\; attention=Concatenate\\left ( Z_1, Z_2, Z_3, Z_4, Z_5, Z_6, Z_7, Z_8 \\right ) \\cdot W_0\n$$\n\n#### Atención Multicabezal\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/decoders.png)\n\nEn la imagen anterior, puedes ver los detalles internos de los decodificadores, y cada capa de atención multicabezal recibe la representación $R$ de la salida de los codificadores y la atención multicabezal enmascarada $M$ de la capa anterior. Debido a la interacción de esta capa entre el decodificador y el codificador, también se llama **atención codificador-decodificador**.\n\nPara calcular el mecanismo de atención, obtenemos la matriz de consulta $Q$ utilizando la matriz $M$; y las matrices $K$ y $V$ utilizando la matriz $R$.\n\n- $Q_i = M \\times W_i^Q$\n- $K_i = R \\times W_i^K$\n- $V_i = R \\times W_i^V$\n\n$Q_i$ representa la oración objetivo obtenida de $M$, y las matrices $K$ y $V$ contienen la representación $R$ obtenida de los codificadores.\n\nA continuación se presenta el procedimiento para obtener la matriz de auto-atención:\n\n$$\nZ_i=Softmax\\left ( \\frac{Q_iK_{i}^{T}}{\\sqrt{d_k}} \\right ) \\cdot V_i\n$$\n\nAl calcular el producto $Q_iK_{i}^{T}$, observaremos que el resultado contendrá una matriz que se aproxima a la matriz identidad, lo que nos ayuda a comprender cuán similar es $Q$ (que representa la oración objetivo) a la matriz $K$ (que representa la oración fuente). Por ejemplo:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/attention_matrix_QK.png)\n\nLa matriz de atención se obtiene de la misma manera que en las secciones anteriores:\n\n$$\nMultihead \\; attention=Concatenate\\left ( Z_1, Z_2, Z_3, Z_4, Z_5, Z_6, Z_7, Z_8 \\right ) \\cdot W_0\n$$\n\n### Red Prealimentada\n\nEsta capa en el decodificador funciona de la misma manera que en el codificador. El componente de **suma y normalización** conecta las subcapas de entrada y salida, como se muestra en la siguiente imagen:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/decoder_with_addNorm.png)\n\n### Capa Lineal y Capa Softmax\n\nEl decodificador aprende la representación de la oración objetivo, que será alimentada a las capas lineal y softmax.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/final_blocks.png)\n\n##### Capa Lineal\n\nEsta capa genera los logits con el tamaño del vocabulario. Asumiendo que el vocabulario es:\n\n$$\nVocabulary=\\left [ azul, cielo, El, es \\right ]\n$$\n\nAsumiendo que la entrada al decodificador es \"El\", el vector de logits generado por el decodificador, por ejemplo, sería:\n\n$$\nlogits=\\left [ 40, 51, 43, 38 \\right ]\n$$\n\n##### Capa Softmax\n\nCuando aplicamos la función Softmax al vector de logits mencionado anteriormente, obtenemos un vector de probabilidades, por ejemplo:\n\n$$\nprob=\\left [ 0.005, 0.973, 0.015, 0.007 \\right ]\n$$\n\nLa palabra del vocabulario con el valor de probabilidad más alto es \"sky,\" por lo que esta palabra es la siguiente predicción del decodificador.\n\n### Entrenamiento del Transformador\n\nEl objetivo del entrenamiento del modelo es minimizar la función de pérdida. Para el Transformador, tenamos que minimizar la diferencia entre las distribuciones de probabilidad y la predicha. La función de pérdida que mejor se adapta a este tipo de escenario es la \"[función de pérdida de entropía cruzada (cross-entropy)](https://es.wikipedia.org/wiki/Entrop%C3%ADa_cruzada)\", y usamos el [optimizador Adam](https://en.wikipedia.org/wiki/Stochastic_gradient_descent#Adam).\n\nPara prevenir el sobreajuste, aplicamos dropout a la salida de cada subcapa y a la suma de los incrustados y la codificación posicional.\n\n### Conclusión\n\nLos conceptos mostrados en esta publicación son esenciales para comprender el funcionamiento interno de los modelos actualmente bien conocidos, como [GPT-3](https://en.wikipedia.org/wiki/GPT-3), [BERT](https://arxiv.org/abs/1810.04805), y otros más modernos, como [PaLM](https://ai.googleblog.com/2022/04/pathways-language-model-palm-scaling-to.html), entre otros. Comprenderlos nos ayudará a apreciar y aprovechar mejor los beneficios que estos modelos de Transformadores ofrecen para los modelos de procesamiento del lenguaje natural (NLP) al desarrollar soluciones creativas, como ChatGPT.\n\n---\n\nSi crees que este contenido es útil, considera comprarme un café. 😉☕️👉🏼"},{"slug":"funciones-de-activacion-en-tensorflow","lang":"es","title":"Funciones de Activación","description":"beenhereActualización 09.2022Esta entrada ha sido actualizada con la versión de TensorFlow v2.10.0 y PyTorch 1.12.1 Tabla de Contenidos ¿Qué es una función de activación?Funciones de ActivaciónSigmoid…","publishDate":"2022-09-23","updatedDate":null,"tags":["activacion","activation-function","funcion-de-activacion","ia","ml","pytorch","tensorflow"],"translationSlug":"activation-functions-in-tensorflow","url":"https://alexisalulema.com/es/blog/funciones-de-activacion-en-tensorflow/","series":null,"headings":[{"depth":3,"slug":"actualización-092022","text":"Actualización 09.2022"},{"depth":3,"slug":"tabla-de-contenidos","text":"Tabla de Contenidos"},{"depth":3,"slug":"qué-es-una-función-de-activación","text":"¿Qué es una función de activación?"},{"depth":3,"slug":"funciones-de-activación","text":"Funciones de Activación"}],"body":"*beenhere*\n\n### Actualización 09.2022\n\nEsta entrada ha sido actualizada con la versión de TensorFlow v2.10.0 y PyTorch 1.12.1\n\n### Tabla de Contenidos\n\n- [¿Qué es una función de activación?](#que-es)\n- [Funciones de Activación](#funciones)\n  - [Sigmoide](#sigmoid)\n  - [ReLU (Rectified Linear Unit)](#relu)\n  - [ReLU6](#relu6)\n  - [Tangente Hiperbólica](#tanh)\n  - [ELU (Exponential Linear Unit)](#elu)\n  - [Softmax](#softmax)\n  - [Softplus](#softplus)\n  - [Softsign](#softsign)\n  - [Swish](#swish)\n  - [Sinc](#sinc)\n  - [Leaky ReLU](#lrelu)\n  - [Mish](#mish)\n  - [GELU (Gaussian Error Linear Unit)](#gelu)\n  - [SELU (Scaled Exponential Linear Unit)](#selu)\n\n### ¿Qué es una función de activación?\n\nUna función de activaciónes una función que se aplica a una neurona en una red neuronal para ayudarla a aprender patrones complejos de datos, decidiendo lo que debe ser transmitido a la siguiente neurona en la red.\n\nUn perceptrón es una unidad de una red neuronal que ingresa los datos de los que se quiere aprender hacia una neurona, y los procesa de acuerdo a su función de activación.\n\nEl perceptrón es un algoritmo simple que, dado un vector de entrada $x$ de $m$ valores $(x_1, x_2, ..., x_m)$, dá como salida un 1 o un 0 (función escalonada), y su función se define a continuación:\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/perceptron-equation.png)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/dot-product.png)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-2-1024x539.png)\n\nAqui, $ω$ es un vector de pesos, $ωx$ es el producto punto, y $b$ es el sesgo, o tendencia. Esta ecuación reensambla la ecuación de la recta. Si $x$ se encuentra sobre esta línea, entonces la respuesta es positiva, de otro modo es negativa. Sin embargo, idealmente vamos a pasar datos de entrenamiento y permitir al computador ajustar el peso y el sesgo de tal manera que los errores producidos por esta neurona se minimicen. El proceso de aprendizaje debería ser capaz de reconocer pequeños cambios que progresivamente enseñan a nuestra neurona a clasificar la información como queremos.\n\nLa siguiente imagen es una función escalonada donde si $x>0$ la salida es 1, y si $x<0$ la salida es 0, como función de activación no nos sirve ya que no tenemos \"pequeños cambios\" sino un gran cambio, y la neurona no es capaz de aprender de esta manera ya que $ω$ y $b$ no van a converger a los valores óptimos para minimizar errores.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/perceptron.png)\n\nFuncion de Activación: Función Escalonada\n\nLa tangente de la función de activación indica si la neurona está aprendiendo; de la imagen anterior deducimos que la tangente en $x=0$ es $\\infty$. Esto no es posible en escenarios reales porque en la vida real todo lo aprendemos paso a paso. Para que una neurona aprenda, necesitamos algo que cambie progresivamente de 0 a 1, es decir, una **función continua** (y derivable).\n\nCuando comenzamos a usar redes neurales, usamos funciones de activación como parte esencial de una neurona. Esta función de activación nos permitirá ajustar pesos y sesgos.\n\nLa derivada de la función de activación alimenta el algoritmo de [backpropagation](https://es.wikipedia.org/wiki/Propagaci%C3%B3n_hacia_atr%C3%A1s) durante el aprendizaje. Por esta razón es que la función y su derivada deben tener un costo computacional bajo\n\n### Funciones de Activación\n\nActualmente existen varios tipos de funciones de activacion que son utilizadas en varios escenarios. A contuación una explicación corta de las funciones de activación disponibles en el módulo **[tf.keras.activations](https://www.tensorflow.org/api_docs/python/tf/keras/activations)** de la distribución de TensorFlow v2.10.0 y [**torch.nn**](https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity) de PyTorch 1.12.1\n\n#### Sigmoide\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.sigmoid](https://www.tensorflow.org/api_docs/python/tf/keras/activations/sigmoid)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Sigmoid](https://pytorch.org/docs/stable/generated/torch.nn.Sigmoid.html#torch.nn.Sigmoid)\n\n$$\n\\sigma \\left ( x \\right )=\\frac{1}{1+e^{-x}}\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image.png)\n\nFunción de Activación: Sigmoide\n\nMatemáticamente, esta función es continua. Como podemos ver, en la sigmoide los cambios son graduales y podemos tener valores distintos de 0 o 1.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.sigmoid(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.Sigmoid()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\nLa función sigmoide es la función de activación más común; sin embargo, no se la utiliza frecuentemente debido a la tendencia a llevar los términos de *[backpropagation](https://es.wikipedia.org/wiki/Propagaci%C3%B3n_hacia_atr%C3%A1s)* (no me suena bien \"propagación hacia atrás\", disculpen) a cero durante el entrenamiento.\n\n#### ReLU (Rectified Linear Unit)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.relu](https://www.tensorflow.org/api_docs/python/tf/keras/activations/relu)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.ReLU](https://pytorch.org/docs/stable/generated/torch.nn.ReLU.html#torch.nn.ReLU)\n\n$$\nf(x)=max(0,x)\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-3.png)\n\nFunción de Activación: ReLU\n\nEsta función ha llegado a ser muy popular ya que genera muy buenos resultados experimentales. La mayor ventaja de ReLU es que esta función acelera la convergencia del **[Stochastic Gradient Descent](https://en.wikipedia.org/wiki/Stochastic_gradient_descent)** (SGD), lo cual indica qué tan rápido está aprendiendo la neurona, comparádola con las funciones [**Sigmoide**](#sigmoid) y Tangente Hiperbólica.\n\nSin embargo, esta fortaleza es, a su vez, su mayor debilidad puesto que esta \"velocidad de aprendizaje\" puede hacer que los pesos $\\omega$ de la neurona se actualicen y oscilen desde los valores óptimos y no se activen en ningún punto. Por ejemplo, si la taza de aprendizaje es demasiado alta, la mitad de las neuronas pueden estar \"muertas\", pero si se establece un valor apropiado entonces las neuronas aprenderán, pero será más lento que lo esperado.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.relu(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.ReLU()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### ReLU6\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.nn.relu6](https://www.tensorflow.org/api_docs/python/tf/nn/relu6)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.ReLU6](https://pytorch.org/docs/stable/generated/torch.nn.ReLU6.html#torch.nn.ReLU6)\n\n$$\nf(x)= min(max(0,x), 6)\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-4.png)\n\nFunción de Activación: ReLU6\n\nEsta función ha permanecido históricamente en el módulo tf.nn, heredada desde TensorFlow 1. Fue introducida en el paper \"**[Convolutional Deep Belief Networks on CIFAR-10](http://www.cs.utoronto.ca/~kriz/conv-cifar10-aug2010.pdf)**\" (página 2), con la ventaja, comparándola con la ReLU simple, de que es computacionalmente más rápida y no sufre de valores que \"explotan\". Como podemos inferir a partir del paper que la introdujo, ha sido utilizada en redes neurales Convolucionales ([CNN](https://en.wikipedia.org/wiki/Convolutional_neural_network)) y Recurrentes ([RNN](https://en.wikipedia.org/wiki/Recurrent_neural_network)).\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.nn.relu6(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.ReLU6()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Tangente Hiperbólica\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.tanh](https://www.tensorflow.org/api_docs/python/tf/keras/activations/tanh)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Tanh](https://pytorch.org/docs/stable/generated/torch.nn.Tanh.html#torch.nn.Tanh)\n\n$$\nf(x)= tanh(x)\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-5.png)\n\nFunción de Activación: $tanh(x)$\n\nEsta función es muy similar a la sigmoide, excepto que en lugar de tener un rango entre 0 y 1, el rango es de -1 a 1. Tristemente, tiene el mismo problema de desvanecimiento de la sigmoide.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.tanh(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.Tanh()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### ELU (Exponential Linear Unit)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.elu](https://www.tensorflow.org/api_docs/python/tf/keras/activations/elu)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.ELU](https://pytorch.org/docs/stable/generated/torch.nn.ELU.html#torch.nn.ELU)\n\nDado un valor de $\\alpha>0$\n\n$$\nf(x)=x\\;\\;if\\;\\;x\\geq 0\n$$\n\n$$\nf(x)=\\alpha (e^{x}-1)\\;\\;if\\;\\;x<0\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-6.png)\n\nFunción de Activación: ELU, con $\\alpha = 1$\n\nDe acuerdo al paper \"[Fast and Accurate Deep Network Learning by Exponential Linear Units (ELUs)](https://arxiv.org/pdf/1511.07289.pdf)\", esta función tiende a convergir a cero más rápido y produce resultados más exactos. Su comportamiento es similar a ReLU, pero para valores negativos donde exponencialmente tiende a $-\\alpha$.\n\nEs una función común en CNN, como lo indica su paper de investigación.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.elu(X, alpha=1.0)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.ELU()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Softmax\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.softmax](https://www.tensorflow.org/api_docs/python/tf/keras/activations/softmax)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Softmax](https://pytorch.org/docs/stable/generated/torch.nn.Softmax.html#torch.nn.Softmax)\n\nDado un vector de valores $x$, y un número de clases $K$ de un clasificador multi-clases:\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-12.png)\n\nSoftmax comvierte un vector de valores a una distribución de probabilidad de suma 1, como podemos ver, su utilización difiere del resto de funciones de activación en que esta función se la utiliza como la capa final en clasificadores basados en redes neurales.\n\nEsta función no es únicamente utilizada en clasificadores, sino también en el aprendizaje reforzado para convertir valores en probabilidades de actuación. Como indica Wikipedia:\n\n> *Correlaciona la recompensa que se espera obtener al llevar a cabo una acción específica con la probabilidad de escoger esa acción. De este modo las acciones de mayor recompensa esperada se eligen con mayor probabilidad.*\n>\n> Wikipedia: [Función Softmax](https://es.wikipedia.org/wiki/Función_SoftMax#Aprendizaje_por_refuerzo)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\n\ninputs = tf.random.normal(shape=(32, 10))\noutputs = tf.keras.activations.softmax(inputs)\ntf.reduce_sum(outputs[0, :])  # Each sample in the batch now sums to 1\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\n\ninputs = torch.randn(32, 10)\noutputs = torch.nn.Softmax(dim=1)(inputs)\ntorch.sum(outputs[0, :])  # Each sample in the batch now sums to 1\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Softplus\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.softplus](http://tf.keras.activations.softplus)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Softplus](https://pytorch.org/docs/stable/generated/torch.nn.Softplus.html#torch.nn.Softplus)\n\n$$\nf(x)=ln(1+e^{x})\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-10.png)\n\nFunción de Activación: Softplus\n\nEsta función es una alternativa a las funciones tradicionales, como la Sigmoide o la Tangente Hiperbólica, debido a que es derivable (lo cual es importante para la función de backpropagation). Lo sorprendente con la derivada de esta función es que ¡es la función Sigmoide!\n\n$$\n\\frac{\\partial y}{\\partial x}=\\left ( ln(1+e^x) \\right )'=\\left ( \\frac{1}{1+e^x} \\right )\\left ( 1+e^x \\right )'\n$$\n\n$$\n\\frac{\\partial y}{\\partial x}=\\left ( \\frac{1}{1+e^x} \\right )\\cdot e^x=\\frac{e^x}{1+e^x}=\\frac{e^{x}}{e^{x}\\left ( e^{-x}+1 \\right )}\n$$\n\n$$\n\\therefore\\;\\; \\frac{\\partial y}{\\partial x}=\\frac{1}{1+e^{-x}}\n$$\n\nEsto es interesante ya que nos permite entender con más claridad cómo se va a comportar el proceso de backpropagation.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.softplus(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.Softplus()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Softsign\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.softsign](https://www.tensorflow.org/api_docs/python/tf/keras/activations/softsign)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Softsign](https://pytorch.org/docs/stable/generated/torch.nn.Softsign.html#torch.nn.Softsign)\n\n$$\nf(x)=\\frac{x}{\\left ( 1+\\left | x \\right | \\right )}\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-11.png)\n\nFunción de Activación: Softsign\n\nDada la forma de la gráfica, podemos ver que es una alternativa a la Tangente Hiperbólica pero no ha sido tan ampliamente adoptada; sin embargo, considerando que la función de activación es un estado-del-arte, podría ser la función que mejor se adapte a determinados modelos de redes neurales.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.softsign(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.Softsign()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Swish\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.swish](https://www.tensorflow.org/api_docs/python/tf/keras/activations/swish)\n\n$$\nf(x)=\\frac{x}{1+e^{-x}}\n$$\n\n$$\nf(x)=x\\cdot Sigmoid(x)\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-8.png)\n\nFunción de Activación: Swish\n\nEsta función de activación es relativamente nueva (2017), y supera a ReLU para redes CNN más profundas. La ecuación que define esta función describe una $Sigmoid(x)$, pero no tiene el problema de desvanecimiento del gradiente, el factor $x$ hace que esta función sea importante nuevamente.\n\nCon respecto a su comparativa con ReLU, ReLU tenía el problema de producir salida 0 para valores negativos que no pueden añadirse a backpropagation, Swish maneja este problema parcialmente; sin embargo, el costo computacional de Swish es mayor y es un tema que debe evaluarse para su decisión ya que este costo será llevado al proceso de backpropagation.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.swish(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\n# It's interesting that PyTorch doesn't have a built-in swish function\nX = torch.linspace(-7., 7., steps=100)\ny = X * torch.nn.Sigmoid()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Sinc\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.experimental.numpy.sinc](https://www.tensorflow.org/api_docs/python/tf/experimental/numpy/sinc)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.special.sinc](https://pytorch.org/docs/stable/special.html#torch.special.sinc)\n\n$$\nf(x)=\\frac{sin(x)}{x}\\;\\;if\\;\\;x\\neq 0\n$$\n\n$$\nf(x)=1\\;\\;if\\;\\;x=0\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-9.png)\n\nFunción de Activación: Sinc\n\n$Sinc$ es una función de activación fuera de lo común ya que, en contraste con las otras funciones, esta sube y cae. Sin embargo, la función se satura y su salida converge a cero para valores de entrada positivos y negativos grandes.\n\nLa función tiene una excepción en $x=0$ ya que si se evalúa ese valor en $f(x)=\\frac{sin(x)}{x}$ se obtendría una ideterminación de divisón por 0.\n\nLas funciones periódicas como $Sinc$ no son populares en redes neurales como funciones de activación. Pero aún cuando **sinc** es periódica, se saturará cuando el valor de entrada incremente positiva o negativamente, tal como otras funciones como la Sigmoide o la Tangente Hiperbólica, lo cuando hace a esta función una alternativa interesante como función de activación en redes neurales.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.experimental.numpy.sinc(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.special.sinc(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Leaky ReLU\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.layers.LeakyReLU](https://www.tensorflow.org/api_docs/python/tf/keras/layers/LeakyReLU)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.LeakyReLU](https://pytorch.org/docs/stable/generated/torch.nn.LeakyReLU.html#torch.nn.LeakyReLU)\n\nDado un valor de $\\alpha>0$\n\n$$\nf(x)=x\\;\\;if\\;\\;x\\geq 0\n$$\n\n$$\nf(x)=\\alpha x\\;\\;if\\;\\;x<0\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-7.png)\n\nFunción de Activación: Leaky ReLU, con $\\alpha = 0.3$\n\nReLU es una función de activación muy popular en CNN, ya que para valores positivos no llega a saturarse y detener el aprendizaje; sin embargo, una debilidad de la ReLU es que para valores negativos tiende a saturarse, y Leaky ReLU (LReLU) corrije este problema.\n\nTensorFlow establece un valor default para la constante $\\alpha=0.3$, aunque algunas fuentes mencionan un valor de 0.01. En términos generales, $\\alpha$ establece la pendiente de la ecuación para valores negativos, y LReLU mejora la el rendimiento de la convergencia.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\nleakyReLU = tf.keras.layers.LeakyReLU(alpha=0.3)\ny = leakyReLU(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.LeakyReLU(0.3)(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Mish\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tfa.activations.mish](https://www.tensorflow.org/addons/api_docs/python/tfa/activations/mish)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Mish](https://pytorch.org/docs/stable/generated/torch.nn.Mish.html#torch.nn.Mish)\n\n$$\nf(x)=x\\cdot tanh(softplus(x))\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-13.png)\n\nFunción de Activación: Mish\n\nPor su forma, esta función es muy similar a Swish, definida como \"[una función de activación no-monotónica auto-regulada](https://arxiv.org/pdf/1908.08681v3.pdf)\", según su paper de investigación (2020). La parte innovadora de Mish es dada por la comprensión intuitiva del comportamiento de la derivada, para entender cómo ayuda a regularizar la optimización de las redes neurales.\n\n$$\n\\frac{\\partial y}{\\partial x}=(x\\cdot tanh(softplus(x)))'=(x\\cdot tanh(1+e^{x}))'\n$$\n\n$$\n\\frac{\\partial y}{\\partial x}=\\frac{e^{x}\\omega }{\\delta ^{2}}\n$$\n\nDonde:\n\n$$\n\\omega =4(x+1)+4e^{2x}+e^{3x}+e^{x}(4x+6)\n$$\n\n$$\n\\delta =2e^{x}+e^{2x}+2\n$$\n\nEstas funciones son más sencillas de graficar y darnos una mejor comprensión de cómo se va a comportar el proceso de backpropagation.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\n! pip install tensorflow-addons\nimport tensorflow as tf\nimport tensorflow_addons as tfa\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tfa.activations.mish(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.Mish()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### GELU (Gaussian Error Linear Unit)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.gelu](https://www.tensorflow.org/api_docs/python/tf/keras/activations/gelu)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.GELU](https://pytorch.org/docs/stable/generated/torch.nn.GELU.html#torch.nn.GELU)\n\n$$\nf(x)=xP(X\\leq x)=x\\;\\Phi (x)=x\\cdot \\frac{1}{2}\\left [ 1+erf(\\frac{x}{\\sqrt{2}}) \\right ]\n$$\n\n$$\nf(x)\\approx 0.5x\\left ( 1+tanh\\left [ \\frac{\\sqrt{2}}{\\pi}\\left ( x+0.044715x^{3} \\right ) \\right ] \\right )\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-14.png)\n\nFunción de Activación: GELU\n\nEsta función pretende fusionar la funcionalidad de una Unidad Linear (ReLU, ELU) con la función de regularización [Dropout](https://es.wikipedia.org/wiki/Abandono_(redes_neuronales)), por medio de una multiplicación estocástica del vector de entrada por 0 o por 1, y obtener la función de activación de manera determinista.\n\nGELU, como está determinada en su [paper de investigación](https://arxiv.org/pdf/1606.08415.pdf), está determinada por una función de distribución Gaussiana lo cual elimina el problema de desvanecimiento en valores negativos que tiene ReLU, mejorando su rendimiento para tareas como computer vision, NLP y procesamiento de voz.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.gelu(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.GELU()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### SELU (Scaled Exponential Linear Unit)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.selu](https://www.tensorflow.org/api_docs/python/tf/keras/activations/selu)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.SELU](https://pytorch.org/docs/stable/generated/torch.nn.SELU.html#torch.nn.SELU)\n\n$$\nf(x)=\\lambda x\\;\\;if\\;\\;x\\geq 0\n$$\n\n$$\nf(x)=\\lambda \\alpha (e^{x}-1)\\;\\;if\\;\\;x<0\n$$\n\nDonde $\\alpha \\approx 1.6733$ y $\\lambda \\approx 1.0507$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-15.png)\n\nFunción de Activación: SELU\n\nSELU es una función de activación que induce propiedades de auto-normalización que converge a una media cero y varianza unitaria. A diferencia de ReLU, puede alcanzar valores bajo 0, lo que permite a la red neural tener una salida de media 0, y esta característica ayuda al modelo a converger más rápidamente hacia la solución.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Ejemplo:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.selu(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Ejemplo:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.SELU()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nMíralo en Google Colab aquí: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)"},{"slug":"generadores-de-imagenes-que-valor-tiene-stabledifussion-en-este-contexto","lang":"es","title":"Generadores de Imágenes-¿Qué valor tiene StableDifussion en éste contexto?","description":"La generación automática de imágenes es una de las tareas que actualmente ha sido potenciada por generadores de imágenes potenciados por Inteligencia Artificial (AI). Estos últimos días se ha estado h","publishDate":"2022-09-07","updatedDate":null,"tags":["ia","ml","stable-diffusion"],"translationSlug":null,"url":"https://alexisalulema.com/es/blog/generadores-de-imagenes-que-valor-tiene-stabledifussion-en-este-contexto/","series":null,"headings":[{"depth":3,"slug":"cómo-instalar-localmente-stabledifussion","text":"¿Cómo instalar localmente StableDifussion?"},{"depth":3,"slug":"conclusión","text":"Conclusión"}],"body":"![](https://images.alexisalulema.com/blog/generadores-de-imagenes-que-valor-tiene-stabledifussion-en-este-contexto/A_dream_of_an_Ecuadorian_indigenous_at_the_moon__manga_style.png)\n\nImagen generada en DreamStudio, con el prompt \"A dream of an Ecuadorian indigenous at the moon, manga style\"\n\nLa generación automática de imágenes es una de las tareas que actualmente ha sido potenciada por generadores de imágenes potenciados por Inteligencia Artificial (AI).\n\nEstos últimos días se ha estado hablando mucho sobre [StableDiffusion](https://stability.ai/blog/stable-diffusion-public-release), que es un modelo generador de imágenes como varios que existen actualmente, y que no tiene nada que envidiar a los demás. La empresa que la impulsa es Stability.Ai, que tiene en su haber varios proyectos Open Source que enfrentan a gigantes empresariales propietarios de modelos, como Google, OpenAI, Microsoft, etc; y es en este punto en donde StableDiffusion ingresa a \"democratizar la síntesis de imágenes de alta\\-resolución\", como lo dice su investigación, con este modelo Open Source al alcance de la comunidad. Hace apenas 4 meses DALL\\-E 2 sorprendía al mundo con una generación de calidad impresionante en creatividad y bajo un modelo de pago, aunque con ciertas restricciones en cuanto a las libertades acerca de qué temática no era permitida en la generación de imágenes.\n\nPero, ¿en qué se basa el modelo de StableDiffusion? Según el paper de investigación \"[Síntesis de Imágenes de Alta\\-Resolución con Modelos de Difusión Latente](https://arxiv.org/abs/2112.10752)\":\n\n> *\"Para permitir el entrenamiento de Modelos de Difusión (DM) en recursos computacionales limitados manteniendo calidad y flexibilidad, los aplicamos en el espacio latente de poderosos autoencoders pre\\-entrenados... Introduciendo capas de cross\\-attention en la arquitectura del modelo, los DMs se convierten en poderosos y flexibles generadores para entradas de texto, y la síntesis se hace posible de una manera convolucional.\"*\n> \n> High\\-Resolution Image Synthesis with Latent Diffusion Models\n\nAnalizando brevemente el documento, podemos ver que el corazón de StableDIffusion son los [Modelos de Difusión](https://www.assemblyai.com/blog/diffusion-models-for-machine-learning-introduction/), de lo cual extenderé un analisis teórico\\-práctico en una futura entrada de este blog, aunque se explica que el modelo también puede interpretarse como una [VQGAN](https://blog.paperspace.com/how-i-made-this-articles-cover-photo-with-vqgan-clip/), con lo cual ingresa en la categoría de modelos generativos.\n\nA continuación he puesto una tabla con algunos de los modelos de síntesis de imágenes más remarcables de los cuales se ha estado hablando últimamente\n\n| Empresa | Modelo | Categoría | Paper |\n| --- | --- | --- | --- |\n| Stability.Ai | [Stable Diffusion](https://stability.ai/blog/stable-diffusion-public-release) | Latent DM / VQGAN | [📓](https://arxiv.org/pdf/2112.10752.pdf) |\n| Midjourney | [Midjourney](https://midjourney.gitbook.io/docs/) | VQGAN | [📓](https://arxiv.org/pdf/2206.02904.pdf) |\n| Google | [Imagen](https://imagen.research.google/) | DM | [📓](https://arxiv.org/pdf/2205.11487.pdf) |\n| Google | [Parti](https://parti.research.google/) | VQGAN | [📓](https://arxiv.org/pdf/2206.10789.pdf) |\n| Meta | [Make\\-A\\-Scena](https://sketch.metademolab.com/) | VQGAN | [📓](https://arxiv.org/pdf/2203.13131.pdf) |\n| Microsoft | [Nuwa Infinity](https://nuwa-infinity.microsoft.com/#/) | DM / VQGAN | [📓](https://arxiv.org/pdf/2207.09814.pdf) |\n| OpenAI | [DALL\\-E](https://openai.com/blog/dall-e/) |  | [📓](https://arxiv.org/pdf/2102.12092.pdf) |\n| OpenAI | [DALL\\-E 2](https://openai.com/dall-e-2/) | DM | [📓](https://arxiv.org/pdf/2204.06125.pdf) |\n\nStableDifussion es particularmente sorprendente debido a que propone una versión de modelo generativo abierto para la generación automática de imágenes, a diferencia de los otros modelos pertenecientes a empresas propietarias de los datasets con los cuales se han entrenado estos modelos. Stability.Ai ha creado un dataset enorme de millones de imágenes ([LAION](https://laion.ai/#top)), que ha sido utilizado en el modelo de StableDiffusion, de una forma algo similar a Google Imagen. Actualmente está abierto para su utilización en <https://beta.dreamstudio.ai/dream>, aunque ya incluye una membresía de **£**10 por aproximadamente 1000 generaciones de imágenes, lo cual sigue abriendo las puertas a la creatividad para muchos.\n\n![](https://images.alexisalulema.com/blog/generadores-de-imagenes-que-valor-tiene-stabledifussion-en-este-contexto/several_generations-1024x346.png)\n\nVarias imágenes sintetizadas, usando estilo cómic, Van Gogh y Guayasamin\n\nAlgunos sitios web ya han empezado a utilizar el modelo de StableDiffusion, en experimentación o de manera comercial, lo cual hace que me pregunte cuál es el valor que va a tomar esta tecnología en manos de la sociedad. Por ejemplo: [https://replicate.com/stability\\-ai/stable\\-diffusion](https://replicate.com/stability-ai/stable-diffusion)\n\n¿Pero cómo es que están apareciendo tan rápidamente implementaciones de StableDiffusion?\n\nLa buena noticia es que este modelo es Open Source, por lo que cualquiera podrá acceder al modelo de generación de imágenes, descargarlo y utilizarlo desde su repositorio en GitHub: [https://github.com/CompVis/stable\\-diffusion](https://github.com/CompVis/stable-diffusion). Sin embargo, estos modelos son bastante grandes y no todo el mundo tiene la infraestructura para realizar la generación de imágenes en casa. Están pensados para organizaciones e investigación, pero alguien con un computador con un buen procesador y tarjeta gráfica tendrá la oportunidad de utilizarlo.\n\nEl fotorealismo y la creatividad de este modelo son impresionantes y pueden ser un motivo para que se generalice el uso de estos modelos generativos, pero este mismo beneficio puede volverse un problema ya que estas imágenes sintetizadas podrían fácilmente confundirse con imágenes reales, y entramos en un problema ético de copyrights y el respeto del derecho al prestigio y buen nombre de figuras públicas.\n\nAl momento de la creación de este post, DreamStudio no está generando imágenes tan fotorealistas como en las primeras pruebas, pero eso no impide al modelo ejecutado en algún otro servidor poder hacerlo dado el dataset con el que fue entrenado.\n\n![](https://images.alexisalulema.com/blog/generadores-de-imagenes-que-valor-tiene-stabledifussion-en-este-contexto/several_generations_2-1024x348.png)\n\nVarias imágenes sintetizadas, como realistas\n\nEste puede ser el punto de inicio de una nuevas aplicaciones no solamente en generación de imágenes, sino en generación de video; y, de esta manera, la AI se estaría aplicando en otros campos que revolucionarán la industria como la conocemos.\n\n### ¿Cómo instalar localmente StableDifussion?\n\nHe probado la instalación del modelo de StableDiffusion en mi PC con Ubuntu 22\\.04, 16 GB de RAM, y una tarjeta de video ASUS ROG Strix (NVIDIA GeForce RTX 3060\\), y ha resultado bastante sencillo el proceso siguiendo las indicaciones dadas en el repositorio de GitHub. Los resultados son aceptables, pero si es notoria la necesidad de una infraestructura de hardware medianamente potente para este tipo de tareas.\n\n**Requisitos:**\n\n- Python 3\\.9\\+\n- Anaconda o Miniconda\n- Tarjeta de video NVIDIA (de gama media a alta)\n- CUDA 11\\+\n- Espacio de disco duro, aproximadamente 20 GB\n\n```\n$ git clone https://github.com/CompVis/stable-diffusion.git\n\n$ cd stable-diffusion\n\n$ conda env create -f environment.yaml\n\n$ conda activate ldm\n\n```\n\nLos comandos previos realizan la instalación local de Stable Diffusion. Con el siguiente comando procedemos a la creación de una imagen utilizando el modelo generativo, durante la ejecución de este comando se van a descargar varios archivos de modelo de algunos GB de tamaño, por lo cual es necesario un poco de paciencia para empezar con la generación de imágenes:\n\n```\npython scripts/txt2img.py --prompt \"TEXT_TO_GENERATE_IMAGE\" --plms --ckpt sd-v1-4.ckpt --skip_grid --n_samples 1\n```\n\nSon varios parámetros opcionales para personalizar la síntesis:\n\n| Parámetro | Descripción |\n| --- | --- |\n| `--ckpt` | Ruta del modelo a usar. Default \"models/ldm/stable\\-diffusion\\-v1/model.ckpt\" |\n| `--ddim_steps` | Valor entero del número de pasos de muestreo para el proceso de Difusión (más alto es mejor calidad). Default 50 |\n| `--from-file` | La ruta de un archivo con los \"prompts\" que se usarán en la generación |\n| `--H` | Valor entero de la altura en píxeles de la imagen (height). Default 512 |\n| `--n_iter` | Valor entero del número de bucles de muestreo. Default 2 |\n| `--n_samples` | Valor entero del número de muestras a producir. Default 3 |\n| `--outdir` | Carpeta de salida de la imagen generada. Default \"outputs/txt2img\\-samples\" |\n| `--prompt` | El texto que se interpretará para generar la imagen |\n| `--scale` | Valor decimal de la escala a usar. Default 7\\.5 |\n| `--seed` | Valor entero de siembra para la generación aleatoria. Default 42 |\n| `--skip_grid` | Omite la creación de una imagen combinada |\n| `--W` | Valor entero del ancho en píxeles de la imagen (width). Default 512 |\n\n### Conclusión\n\nEstos meses hemos visto varios modelos de generación de imágenes por Inteligencia Articial, con resultados asombrosos como los de DALL\\-E 2, y con la propuesta democratizadora como la de Stable Diffusion que, en cuestión de pocas días de estar disponible, ha causado revuelo y espectativas que estoy seguro seguirán asombrando los meses y años por venir.\n\nEl dilema ético se plantea, como en todas las tecnologías emergentes, frente a la manera cómo la humanidad vamos a utilizarla, y qué repercusiones sociales tendrá de cara a los profesionales del arte.\n\nPero no solo este campo de Computer Vision es el que está en apogeo actualmente, también estimo que nos estaremos asombrando con los logros de la AI en campos como el Natural Language Processing."},{"slug":"transformers-how-do-they-work-internally","lang":"en","title":"Transformers: how do they work internally?","description":"Table of Contents Introduction Input Embedding Positional Encoding (PE) The Encoder Self-attention Mecanism Multi-head attention mechanism Feedforward Network The Decoder Masked Multi-head Attention M…","publishDate":"2022-08-23","updatedDate":null,"tags":["machine-learning","python","transformers"],"translationSlug":"transformadores-como-funcionan-internamente","url":"https://alexisalulema.com/blog/transformers-how-do-they-work-internally/","series":null,"headings":[{"depth":3,"slug":"table-of-contents","text":"Table of Contents"},{"depth":3,"slug":"introduction","text":"Introduction"},{"depth":3,"slug":"input-embedding","text":"Input Embedding"},{"depth":3,"slug":"positional-encoding-pe","text":"Positional Encoding (PE)"},{"depth":3,"slug":"the-encoder","text":"The encoder"},{"depth":3,"slug":"the-decoder","text":"The decoder"},{"depth":3,"slug":"feedforward-network-1","text":"Feedforward Network"},{"depth":3,"slug":"linear-and-softmax-layer","text":"Linear and Softmax Layer"},{"depth":3,"slug":"transformer-training","text":"Transformer Training"},{"depth":3,"slug":"conclusion","text":"Conclusion"}],"body":"### Table of Contents\n\n1. [Introduction](#intro)\n2. [Input Embedding](#input-embedding)\n3. [Positional Encoding (PE)](#pe)\n4. [The Encoder](#encoder)\n   1. [Self-attention Mecanism](#self-attention)\n   2. [Multi-head attention mechanism](#multi-head)\n   3. [Feedforward Network](#ffn)\n5. [The Decoder](#decoder)\n   1. [Masked Multi-head Attention](#masked-multi-head)\n   2. [Multi-head Attention](#multi-head-2)\n   3. [Feedforward Network](#ffn-2)\n6. [Linear and Softmax Layer](#linear-softmax)\n7. [Transformer Training](#training)\n8. [Conclusion](#conclusion)\n\n### Introduction\n\nThe Transformer is currently one of the most popular architectures for NLP. We can periodically hear news about new architectures and models based on transformers generating a lot of buzz and expectations in the community.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/image-695x1024.png)\n\nOriginal Transformer Architecture  \nSource: [Attention is all you need](https://arxiv.org/abs/1706.03762?context=cs)\n\nGoogle Research and members from Google Brain initially proposed the Transformer in the 2017 paper [**Attention is all you need**](https://arxiv.org/abs/1706.03762?context=cs). Although we can download and read it, its concepts are still restricted to initiates in elegant mathematical nomenclature.\n\nThe Transformer outperformed other existing architectures based on [**LSTM**](https://en.wikipedia.org/wiki/Long_short-term_memory) and [**RNN**](https://en.wikipedia.org/wiki/Recurrent_neural_network), obtaining higher evaluation results and faster training. The Transformer's attention mechanism is a \"word-to-word\" operation that will find how each word is related to the others in the sequence, including the same word itself. In the current document, I will explain the architecture and internal workings of a Transformer in the simplest way possible.\n\n### Input Embedding\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/input.embedding.png)\n\nStarting at the beginning, the Transformer architecture begins with the **Input Embedding** sublayer, which converts the input sequence into vectors of dimension $d_{model}=512$.\n\n🎓 The value of $d_{model}=512$ was set by the architecture designers to establish a constant dimension at the output of each sublayer of the model. The value of this dimension can be modified depending on the objectives.\n\nA tokenizer transforms the input stream into tokens by normalizing the text to lowercase and truncating it into subparts; additionally, it will provide an integer vector representation (based on an existing vocabulary) that will be used for the embedding process. For example:\n\n```\nInput = \"Roses are red and violets are blue\"\nTokens = [\"roses\", \"are\", \"red\", \"and\", \"violets\", \"are\", \"blue\"]\nTokenized = [8271, 1029, 3674, 9273, 2384, 1029, 9873]\n```\n\nNext, the embedding sublayer is fed from the tokenized vector. For each word, this sublayer should produce a vector of size $d_{model}=512$. For example, for the words \"red\" and \"blue,\" which are colors, the **word embedding vectors** should be similar:\n\n```\nblue=[ 0.36138474, -0.16811648, -0.03733656, -0.58750702, -0.81279167,\n       -0.86249844,  0.69673459, -0.79213212,  0.9278906 ,  0.42308278,\n       -0.12308109,  0.2383174 ,  0.44863208,  0.98666162, -0.12830655,\n       -0.56420363,  0.69459217,  0.72405279,  0.92023563, -0.84536481,\n        0.86299045, -0.88166481, -0.9087216 ,  0.99420482, -0.73118714,\n...\n       -0.47495972, -0.94366021, -0.97624231, -0.9792538 ,  0.20736778,\n        0.1248088 ,  0.6344501 , -0.54432975, -0.35632176, -0.6670839 ,\n       -0.48141856, -0.3503394 , -0.94319604,  0.48421567, -0.12854877,\n       -0.48260166, -0.845398  ,  0.67561689,  0.29778234,  0.03009221,\n        0.25067641, -0.81864996, -0.51513235, -0.44608639, -0.65686229]\n```\n\n🎓 ***Cosine similarity*** can be used to check if two embedding vectors are similar. For more information on the theory: [Scikit-Learn's Cosine Similarity](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise.cosine_similarity.html).\n\nEmbedding vectors provide a lot of information to the Transformer about how the words in a sequence are related. However, information is still needed to indicate the position of the words in a sequence, and the **Positional Encoding** process is used for this.\n\n### Positional Encoding (PE)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/positional.encoding.png)\n\nEach word in the initial sequence must have PE information, but the generation of this vector must be simple since the main focus of the Transformer is the attention mechanism.\n\nThe challenge in this task is to generate a vector of dimension $d_{model}=512$ to each output vector of the PE function. The architecture authors found a clever way to use a unit sphere to represent the PE with sine and cosine values.\n\n🎓 With the idea of the unit sphere, the authors proposed sine and cosine functions that can generate different values of PE y for each dimension $i$ of the 512 established in the word embedding vector.  \n  \n $PE_{(pos\\:2i)}=sin \\left (\\frac{pos}{10000^{\\frac{2i}{d_{model}}}} \\right)$([Plot sin in Google](https://www.google.com/search?q=plot+y%3Dsin(2%2F10000%5E(2*x%2F512))))  \n $PE_{(pos\\:2i+1)}=cos \\left (\\frac{pos}{10000^{\\frac{2i}{d_{model}}}} \\right)$ ([Plot cos in Google](https://www.google.com/search?q=plot+y%3Dcos(2%2F10000%5E(2*x%2F512))))  \n  \nThe process will apply the sine function to even numbers $i\\: \\in \\left [ 0,\\:255\\right ]$ and the cosine function to odd numbers $i\\: \\in \\left [ 256,\\:512\\right ]$. PE values are encoded within the unit sphere and allow a simple representation of the position of elements in the sequence.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/unit.sphere.png)\n\nThe following example is a Python translation of the concept for Positional Encoding, evaluated for position 3:\n\n```\nimport math\n\nd_model = 512\n\ndef positional_encoding(position):\n    pe = [None] * d_model\n\n    for i in range(0, 512, 2):\n        pe[i] = math.sin(position / (10000 ** ((2 * i) / d_model)))\n        pe[i + 1] = math.cos(position / (10000 ** ((2 * i) / d_model)))\n\n    return pe\n\nprint(positional_encoding(3))\n```\n\nFor example, suppose we have the sentence \"The sky is blue,\" the positional encoding PE array will be:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/PE_1.gif)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/PE_2.gif)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/PE_3.gif)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/PE_4.gif)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/PE_5.gif)\n\nFinally, the word encoding vector should be added to the positional encoding vector, and the encoder/decoder block will be fed with the result:\n\n$$\nEmbedding_{output} = Embedding_{initial}+PE_{word\\:position}\n$$\n\nHowever, the word embedding values sometimes are too small and could be virtually disregarded. In this case, the solution is scaling the values by, for example, the PE mean.\n\n### The encoder\n\nIn its simplest view, the Transformer is an encoder-decoder architecture, where the encoder learns the Positional Encoding representation of the input text and sends it to the decoder. The decoder receives what has been learned by the encoder and generates an output.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/encoder-decoder-1-1024x275.png)\n\nInternally, in more detail, the Transformer consists of a stack of $N$ encoders, each sending its output to the next. The final encoder returns the representation of the input stream. For the explanation, from now on, we will use a value of $N=2$.\n\n🎓 The original authors of the Transformer assigned the value of $N=6$ in **\"Attention is all you need\"**.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/encoders.png)\n\nEach decoder block is composed of 2 sublayers:\n\n- Multi-head attention\n- Feedforward Network\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/encoders.inside.png)\n\nBefore starting to explain these two components, it is necessary to understand the self-attention mechanism.\n\n#### Self-attention Mecanism\n\nConsider the following sentence:\n\n`John and Paul wrote several songs when they were inspired.`\n\nIn this sentence, the self-attention mechanism computes the representation of each word, and the relationship with the other words in the sentence gives more information about the word. For example, the term \"they\" should be related to \"John\" and \"Paul\" and not to \"songs.\"\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/self-attention-1024x814.png)\n\nA more straightforward example of understanding how the self-attention mechanism works is the sentence \"The sky is blue.\" The encoders receive word embedding vectors of dimension $d_{model}=512$ of each word of the sentence, for example:\n\n$x_1=\\begin{bmatrix} 3.23 & 0.65 & ... & 4.78 \\end{bmatrix}$ ==> \"The\"  \n$x_2=\\begin{bmatrix} 1.26 & 6.35 & ... & 7.99 \\end{bmatrix}$ ==> \"sky\"  \n$x_3=\\begin{bmatrix} 9.25 & 1.68 & ... & 4.26 \\end{bmatrix}$ ==> \"is\"  \n$x_4=\\begin{bmatrix} 6.84 & 2.98 & ... & 11.48 \\end{bmatrix}$ ==> \"blue\"\n\nWith these vectors we can assemble the embedding matrix $X$, with $d=[4 \\times 512]$:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/embedding_matrix.gif)\n\nWe will create three additional matrices from this matrix X that serves as the \"self-attention mechanism.\":\n\n- $Q$, query matrix\n- $K$, key matrix\n- $V$, value matrix\n\nTo create these arrays, we additionally need three new weight matrices. The dimension used in the original paper was $d_k=64$; therefore, the weight vectors would be of dimension $d_{model} \\times d_k\\Rightarrow 512 \\times 64$, which are initialized with random values:\n\n- $W^Q$, query weight matrix\n- $W^K$, key weight matrix\n- $W^V$, value weight matrix\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QKV_neuron-1024x430.png)\n\nThe weight matrices carry the optimal values learned during training, so each matrix $Q$, $K$, and $V$, is the product of the embedding matrix $X$ with the corresponding weight matrix, which generates matrices $4 \\times 64$:\n\n- $Q = X \\times W^Q$\n- $K = X \\times W^K$\n- $V = X \\times W^V$\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QKV.gif)\n\nEach of the four rows in each matrix represents each word of the opening sentence, \"The sky is blue.\"\n\n##### Self-attention Mechanism Process\n\n1. Compute the dot product $Q \\cdot K^T$\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QdotKT-1-1024x140.gif)\n\nThe elements of the resulting array indicate the relationship between the words. For example, $q_1.k_1$ is the relationship of the word \"The\" to itself and is a high value, but $q_1.k_3$ is the relationship between \"The\" and \"is\" and is a low value. The relationship between \"sky\" and \"blue\" ($q_2.k_4$) will have a slightly higher value because there is a relationship between the noun and the adjective. For example:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QdotKT.2.gif)\n\nIn this way, we can say that computing the dot product between the query matrix, $Q$, and the key matrix, $K^T$, essentially gives us the similarity value, which helps us understand how similar each word is in the sentence to all the other words.\n\n2. Compute $QK^T/\\sqrt{d_k}$. This action is useful to obtain stable gradients, where $d_k=64$ is the key vector dimension.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QdotKTSqrtDK.gif)\n\nThe values of the resulting matrix must be normalized, and if we use the function $Softmax\\left ( \\frac{QK^T}{\\sqrt d_k} \\right )$ (see [Softmax](https://en.wikipedia.org/wiki/Softmax_function)), we can formalize the values in a range from 0 to 1.\n\n```\nimport numpy as np\n\ndef softmax(x):\n    max = np.max(x,axis=1,keepdims=True)\n    e_x = np.exp(x - max)\n    sum = np.sum(e_x,axis=1,keepdims=True)\n    f_x = e_x / sum\n    return f_x\n\nV = np.array([[13.75, 11.50, 7.75, 7.50],\n        [11.88, 12.38, 11.25, 10],\n        [8.13, 11.25, 13.75, 8.75],\n        [7.5, 11.25, 9.38, 13.13]])\nsoftmax(V)\n```\n\n```\narray([[0.90105641, 0.09497065, 0.0022335 , 0.00173945],\n       [0.29994872, 0.49453184, 0.15975023, 0.04576921],\n       [0.00331791, 0.07513861, 0.91537572, 0.00616775],\n       [0.00304195, 0.12934693, 0.01993542, 0.8476757 ]])\n```\n\nThe sum of the values in each row is equal to 1. With these values, we can understand how each word in the sentence relates to all the other words. This is called the ***score matrix***.\n\n3. Next, we need to calculate the **attention matrix** $Z$:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/SoftmaxV.gif)\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/Z.SoftmaxV-1024x148.gif)\n\n$$\nZ=\\begin{bmatrix} 4.18336201 & 0.65278898 & ... & 4.22437426 \\\\ 5.94802206 & 3.00072113 & ... & 4.4028484 \\\\ 3.09529998 & 1.49925765 & ... & 9.9459073 \\\\ 4.76015677 & 2.69495094 & ... & 2.17709763 \\end{bmatrix}\n$$\n\nThe attention matrix $Z$ is a matrix of, for the example sentence, 4 rows, and 512 columns. Each row corresponds to the self-attention vector of the corresponding word.\n\nThe self-attention mechanism is called **scaled dot product attention** since we are computing the dot product between the vectors $Q$ and $K$ and scaling the values by ($\\sqrt {d_k}$).\n\n#### Multi-head attention mechanism\n\nFor the Transformer, we are going to compute multiple attention matrices. But why do we need multiple arrays? It helps us in idiomatic contexts where the meaning of a word is ambiguous, for example:\n\n*Tom was crying because he was blue.*\n\nA single attention mechanism would decide that Tom was crying because his color is blue, being dominated by the word \"Tom.\" And **if most sentences in which \"blue\" implies that it is a color, by having only one \"attention head,\" the mechanism will correctly learn that it is a color. However, having \"multiple attention heads,\" one of those attention mechanisms is more likely to learn from the sentences in which it implies that \"blue\" is a mood, and by concatenating the results of the \"multiple attention heads,\" the attention matrix will be more accurate**.\n\nHow do we compute multiple attention matrices? Suppose we are going to compute two attention matrices: $Z_1$ y $Z_2$.\n\nTo compute $Z_1$ first, we create the three matrices $Q_1$, $K_1$, and $V_1$, which implies multiplying the embedding matrix and the three weight matrices $W_{1}^{Q}$, $W_{1}^{K}$, and $W_{1}^{V}$. Now, the attention matrix is computed in the following way:\n\n$$\nZ_1=Softmax\\left ( \\frac{Q_1K_{1}^{T}}{\\sqrt{d_k}} \\right ) \\cdot V_1\n$$\n\nIn the same way for $Z_2$\n\n$$\nZ_2=Softmax\\left ( \\frac{Q_2K_{2}^{T}}{\\sqrt{d_k}} \\right ) \\cdot V_2\n$$\n\nIn this way, we can compute any number of attention matrices. Suppose we need eight attention matrices (the value in \"Attention is all you need\"). In that case, we can concatenate all the attention heads and multiply the result by a new weight matrix $W_0$ trained to represent the optimal values for the attention mechanism.\n\n$$\nMulti-head \\: attention=Concatenate\\left ( Z_1, Z_2, Z_3, Z_4, Z_5, Z_6, Z_7, Z_8 \\right ) \\cdot W_0\n$$\n\n#### Feedforward Network\n\nThe [feedforward network](https://en.wikipedia.org/wiki/Feedforward_neural_network) consists of just two dense layers with [ReLU](https://www.alexisalulema.com/2017/10/15/activation-functions-in-tensorflow/#relu) activation. The same parameters are applied in the different places of the sentence, but it is other than the encoder blocks.\n\nAn additional component to connect the input and the encoder's blocks is the **add and norm component**, which is a connection followed by **layer normalization**.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/addNnorm-1024x363.png)\n\nLayer normalization enables faster training by preventing the values in each layer from changing heavily.\n\n### The decoder\n\nIn the same way as the encoder, we can have a stack of $N$ decoders (for example, we assume that $N=2$). The representation of the sequence produced by the encoders is the input of all decoders; that is, a decoder receives two inputs, one from the previous decoder and the representation made by the encoder.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/encoderNdecoder.png)\n\nTo understand how the decoder generates the target sentence, let's look at the following image that describes the input to the decoder as a time series.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/encoderNdecoder_time.png)\n\n<sos>: Start Of the Sentence  \n<eos>: End Of the Sentence\n\nAt each time step, the decoder matches the newly generated word to the input and predicts the following input. Once token **<eos>** is generated, the decoder has finished generating the target sentence.\n\nLike the input embedding for the encoders, the sentence \"*<sos> El cielo es azul*\" must be embedded to feed the decoders.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/output.embedding.png)\n\nTo understand how the decoder works, you must explore its components in the image below:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/decoder.block_.png)\n\n#### Masked multi-head attention\n\nThe masked multi-head attention is similar to the multi-head attention mechanism with a slight difference.\n\nDuring a test, the decoder will generate words as long as there is a previous value; that is, for the input at **\"t2: <sos> El\"**, the model is trained only with the tokens **<sos>** and **El**, and the attention mechanism must match the words only up to the word **El** and not for the missing words in the sentence. The rest of the words in the sequence can be masked, which helps the attention mechanism to attend only to the words available during the tests.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/masking_values-1.gif)\n\nWe must compute the attention matrix $Z$ the same way as we have been doing it, with the difference that the values corresponding to the masked word of the sentence receive a value of $-\\infty$. For example:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/QKT_masked.gif)\n\nWith this, we can get the final attention matrix as we did for the encoder, and this matrix will feed the next layer of multi-head attention.\n\n$$\nMasked \\; multihead \\; attention=Concatenate\\left ( Z_1, Z_2, Z_3, Z_4, Z_5, Z_6, Z_7, Z_8 \\right ) \\cdot W_0\n$$\n\n#### Multi-head Attention\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/decoders.png)\n\nIn the previous image, you can see the internal details of the decoders, and each multi-head attention layer receives the representation $R$ of the output of the encoders and the masked multi-head attention $M$ of the previous layer. Due to this layer's interaction between the decoder and the encoder, it is also called **encoder-decoder attention**.\n\nTo calculate the attention mechanism, we obtain the query matrix $Q$ using the matrix $M$; and the matrices $K$ and $V$ using the matrix $R$.\n\n- $Q_i = M \\times W_i^Q$\n- $K_i = R \\times W_i^K$\n- $V_i = R \\times W_i^V$\n\n$Q_i$ represents the target sentence obtained from $M$, and the matrices $K$ and $V$ contain the representation $R$ obtained from the encoders.\n\nFollowing is the procedure for obtaining the self-attention matrix:\n\n$$\nZ_i=Softmax\\left ( \\frac{Q_iK_{i}^{T}}{\\sqrt{d_k}} \\right ) \\cdot V_i\n$$\n\nWhen computing the product $Q_iK_{i}^{T}$, we will observe that the result will contain a matrix that approximates the identity matrix, which helps us understand how similar $Q$ (which represents the target sentence) is to the matrix $K$ (representing the source sentence). For example:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/attention_matrix_QK.png)\n\nThe attention matrix is obtained in the same way as in the previous sections:\n\n$$\nMultihead \\; attention=Concatenate\\left ( Z_1, Z_2, Z_3, Z_4, Z_5, Z_6, Z_7, Z_8 \\right ) \\cdot W_0\n$$\n\n### Feedforward Network\n\nThis layer in the decoder works the same way as in the encoder. The **add and norm** component connects the input and output sublayers, as shown in the following picture:\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/decoder_with_addNorm.png)\n\n### Linear and Softmax Layer\n\nThe decoder learns the target sentence's representation, which will be fed to the linear and softmax layers.\n\n![](https://images.alexisalulema.com/blog/transformers-how-do-they-work-internally/final_blocks.png)\n\n##### Linear Layer\n\nThis layer generates the logits with the size of the vocabulary. Assuming the vocabulary is:\n\n$$\nVocabulary=\\left [ azul, cielo, El, es \\right ]\n$$\n\nAssuming that the input to the decoder is \" El,\" the vector of logits generated by the decoder, for example, would be:\n\n$$\nlogits=\\left [ 40, 51, 43, 38 \\right ]\n$$\n\n##### Softmax Layer\n\nWhen we apply the Softmax function to the vector of logits above, we obtain a vector of probabilities, for example:\n\n$$\nprob=\\left [ 0.005, 0.973, 0.015, 0.007 \\right ]\n$$\n\nThe vocabulary word with the highest probability value is \"sky,\" so this word is the decoder's next prediction.\n\n### Transformer Training\n\nThe goal of model training is to minimize the loss function. For the Transformer, we have to minimize the difference between the predicted and actual probability distributions. The loss function that best suits this type of scenario is the \"[cross-entropy loss function](https://en.wikipedia.org/wiki/Cross_entropy),\" and we use the [Adam optimizer](https://en.wikipedia.org/wiki/Stochastic_gradient_descent#Adam).\n\nWe apply dropout to each sublayer's output and the sum of embeddings and positional encoding to prevent overfitting.\n\n### Conclusion\n\nThe concepts shown in this post are essential for understanding the internal workings of currently well-known models such as [GPT-3](https://en.wikipedia.org/wiki/GPT-3), [BERT](https://arxiv.org/abs/1810.04805), and other more modern ones such as [PaLM](https://ai.googleblog.com/2022/04/pathways-language-model-palm-scaling-to.html). Understanding them will help us better appreciate and take advantage of the benefits of these Transformer models for NLP models in developing creative solutions like ChatGPT.\n\n---\n\nIf you think this content is helpful, consider buying me a coffee. 😉☕️👉🏼"},{"slug":"python-profiling-memory-profiling-part-3-final","lang":"en","title":"Python Profiling - Memory Profiling (Part 3, Final)","description":"Table of Contents memory_profilerPySpyDISassemblingFinal Recommendations memory_profiler Similar to line_profiler, memory_profiler provides detailed memory usage measurements, with the aim of efficien…","publishDate":"2022-08-07","updatedDate":null,"tags":["profiling","python"],"translationSlug":null,"url":"https://alexisalulema.com/blog/python-profiling-memory-profiling-part-3-final/","series":null,"headings":[{"depth":2,"slug":"table-of-contents","text":"Table of Contents"},{"depth":2,"slug":"memory_profiler","text":"memory_profiler"},{"depth":2,"slug":"pyspy","text":"PySpy"},{"depth":2,"slug":"disassembling","text":"DISassembling"},{"depth":2,"slug":"final-recommendations","text":"Final Recommendations"}],"body":"## Table of Contents\n\n1. [memory\\_profiler](#memory_profiler)\n2. [PySpy](#pyspy)\n3. [DISassembling](#dis)\n4. [Final Recommendations](#final)\n\n## memory\\_profiler\n\nSimilar to `line_profiler`, `memory_profiler` provides detailed memory usage measurements, with the aim of efficiently reducing memory consumption and optimizing memory usage to improve application performance..\n\n⚠️ Before starting using this tool, it is important to mention the impact on the execution speed of the code while it is in use, since the execution time increases dozens of times, it is convenient to profiling time and CPU beforehand, and use `line_profiler` only on sections identified to be optimized.\n\nAs with `line_profiler`, the `@profile` decorator indicates the method to be evaluated.\n\nHowever, analyzing the memory usage in Python is complicated, even if we consider that the *garbage collector* is freeing memory, we do not know for sure when a section of memory is freed and when an object is going to be released, or how long it will remain in the pool until it is finally disposed. Many times, it is convenient to handle methods that consume more memory, and not repeatedly allocate memory because it is an expensive process, and it is more efficient than analyzing the memory usage of the code line by line.\n\nUsing the sorting algorithm as an experiment, we perform the following exercise:\n\n```\nimport numpy as np\n\n@profile\ndef bubble_sorting(array):\n    n = len(array)\n\n    for i in range(n):\n        sorted_item = True\n\n        for j in range(n - i - 1):\n            if array[j] > array[j + 1]:\n                array[j], array[j + 1] = array[j + 1], array[j]\n                sorted_item = False\n\n        if sorted_item:\n            break\n\n    return array\n\nwork_array = np.random.randint(1, 100000, size=10000)\nbubble_sorting(work_array)\n```\n\nIn the following instruction, I use `gtime -p` along with `memory_profiler` to measure the execution time, but it is not required.\n\n```\nalexis@MacBook-Pro % gtime -p python -m memory_profiler main.py\n\nFilename: main.py\n\nLine #    Mem usage    Increment  Occurrences   Line Contents\n=============================================================\n     3   56.859 MiB   56.859 MiB           1   @profile\n     4                                         def bubble_sorting(array):\n     5   56.859 MiB    0.000 MiB           1       n = len(array)\n     6                                         \n     7   56.906 MiB -254060.531 MiB        9909       for i in range(n):\n     8   56.906 MiB -254060.531 MiB        9909           sorted_item = True\n     9                                         \n    10   56.906 MiB -874942713.984 MiB    50000814           for j in range(n - i - 1):\n    11   56.906 MiB -874688626.062 MiB    49990905               if array[j] > array[j + 1]:\n    12   56.906 MiB -287907579.547 MiB    25155548                   array[j], array[j + 1] = array[j + 1], array[j]\n    13   56.906 MiB -287907586.656 MiB    25155548                   sorted_item = False\n    14                                         \n    15   56.906 MiB -254101.422 MiB        9909           if sorted_item:\n    16   16.016 MiB  -40.891 MiB           1               break\n    17                                         \n    18   16.016 MiB    0.000 MiB           1       return array\n\nreal 1855.21\nuser 1046.50\nsys 808.82\n```\n\nThere are two important things to note here: **first**, the output of `memory_profiler` is very detailed line by line, which is what we are looking for when using this tool; and, **second**, the elapsed time is approximately 30 minutes, which is a considerable time compared to the original time (~10 seconds). These metrics should give us an indication of how and when to use this tool.\n\n`memory_profiler` includes the `mprof` command (requires `matplotlib` to be installed), which allows you to visualize changes in memory usage, for example:\n\n```\nalexis@MacBook-Pro PythonProfiling % mprof run main.py\n\nmprof: Sampling memory every 0.1s\nrunning new process\nrunning as a Python program...\nalexis@MacBook-Pro PythonProfiling % mprof plot                             \nUsing last profile data.\n```\n\n![](https://images.alexisalulema.com/blog/python-profiling-memory-profiling-part-3-final/mprof-1024x425.png)\n\n## PySpy\n\nPySpy is a great tool that helps us to evaluate processes that are already running, without the need to stop and configure them beforehand. For this reason, it requires additional privileges to evaluate other processes. It is especially useful for running experiments in environments such as Production or UAT.\n\nThe `py-spy` package must be installed with PIP or Conda, depending on the local Python installation, and the command it includes is `py-spy`. To evaluate a running process it is important to obtain the Process Identifier (PID), for example:\n\n- [Linux/MacOS](#advgb-tabs-tab0)\n- [Windows](#advgb-tabs-tab1)\n\nLinux/MacOS\n\n```\nalexis@MacBook-Pro % ps -A\n\n  PID TTY           TIME CMD\n...\n36588 ??         0:51.08 /Users/alexis/miniforge3/bin/python /Users/alexis/PythonProfiling/main.py\n...\n```\n\nWindows\n\n```\nC:\\Users\\alexis>tasklist\n\nImage Name                     PID Session Name        Session#    Mem Usage\n========================= ======== ================ =========== ============\n...\npython3.10.exe               18212 Console                    1     47,460 K\n...\n```\n\nPySpy has 4 subcommands:\n\n```\nUSAGE:\n    py-spy <SUBCOMMAND>\n\nOPTIONS:\n    -h, --help       Prints help information\n    -V, --version    Prints version information\n\nSUBCOMMANDS:\n    record    Records stack trace information to a flamegraph, speedscope or raw file\n    top       Displays a top like view of functions consuming CPU\n    dump      Dumps stack traces for a target program to stdout\n    help      Prints this message or the help of the given subcommand(s)\n```\n\nExample of subcommand `dump`:\n\n- [Linux/MacOS](#advgb-tabs-tab0)\n- [Windows](#advgb-tabs-tab1)\n\nLinux/MacOS\n\n```\nalexis@MacBook-Pro % sudo env \"PATH=$PATH\" py-spy dump --pid 36588\n\nProcess 36588: /Users/alexis/miniforge3/bin/python /Users/alexis/PythonProfiling/main.py\nPython v3.9.13 (/Users/alexis/miniforge3/bin/python3.9)\n\nThread 0x100FDC580 (active+gil): \"MainThread\"\n    bubble_sorting (main.py:11)\n    <module> (main.py:22)\n```\n\nWindows\n\n```\nC:\\Users\\alexi>py-spy dump --pid 18212\nProcess 18212: python  main.py\nPython v3.9.12 (C:\\Users\\alexi\\anaconda3\\python.exe)\n\nThread 2168 (active+gil): \"MainThread\"\n    bubble_sorting (main.py:12)\n    <module> (main.py:21)\n```\n\nExample of subcommand `top`, it refreshes the information once per second:\n\n- [Linux/MacOS](#advgb-tabs-tab0)\n- [Windows](#advgb-tabs-tab1)\n\nLinux/MacOS\n\n```\nalexis@MacBook-Pro % sudo env \"PATH=$PATH\" py-spy top --pid 36588\n\nCollecting samples from '/Users/alexis/miniforge3/bin/python /Users/alexis/PythonProfiling/main.py' (python v3.9.13)\nTotal Samples 1200\nGIL: 100.00%, Active: 100.00%, Threads: 1\n\n  %Own   %Total  OwnTime  TotalTime  Function (filename:line)                                                                                                                                                   \n 38.00%  38.00%    5.86s     5.86s   bubble_sorting (main.py:12)\n 52.00%  52.00%    5.65s     5.65s   bubble_sorting (main.py:11)\n  9.00%   9.00%   0.420s    0.420s   bubble_sorting (main.py:10)\n  1.00%   1.00%   0.070s    0.070s   bubble_sorting (main.py:13)\n  0.00% 100.00%   0.000s    12.00s   <module> (main.py:22)\n```\n\nWindows\n\n```\nC:\\Users\\alexi>py-spy top --pid 18212\n\nCollecting samples from 'python  main.py' (python v3.9.12)\nTotal Samples 400\nGIL: 100.00%, Active: 100.00%, Threads: 1\n\n  %Own   %Total  OwnTime  TotalTime  Function (filename:line)\n 58.00%  58.00%    2.27s     2.27s   bubble_sorting (main.py:12)\n 36.00%  36.00%    1.51s     1.51s   bubble_sorting (main.py:11)\n  5.00%   5.00%   0.160s    0.160s   bubble_sorting (main.py:10)\n  1.00%   1.00%   0.060s    0.060s   bubble_sorting (main.py:13)\n  0.00% 100.00%   0.000s     4.00s   <module> (main.py:21)\n```\n\nExample of subcommand `record`, it generates an image file with extension `.svg` summarizing the experiment:\n\n- [Linux/MacOS](#advgb-tabs-tab0)\n- [Windows](#advgb-tabs-tab1)\n\nLinux/MacOS\n\n```\nalexis@MacBook-Pro % sudo env \"PATH=$PATH\" py-spy record --pid 36588\npy-spy> Sampling process 100 times a second. Press Control-C to exit.\n\n^C\npy-spy> Stopped sampling because Control-C pressed\npy-spy> Wrote flamegraph data to '36588-2022-08-06T08:00:04-05:00.svg'. Samples: 24397 Errors: 0\n```\n\nWindows\n\n```\nC:\\Users\\alexi>py-spy record --pid 18212 --output profile.svg\npy-spy> Sampling process 100 times a second. Press Control-C to exit.\n\npy-spy> Stopped sampling because Control-C pressed\npy-spy> Wrote flamegraph data to 'profile.svg'. Samples: 603 Errors: 0\n```\n\n![](https://images.alexisalulema.com/blog/python-profiling-memory-profiling-part-3-final/pyspy.record-1024x219.png)\n\n## DISassembling\n\nUnderstanding how the executing bytecode behaves, beyond the Python code that we have analyzed, can gives us an idea of how things work internally and why some pieces of code work better than others, and the `dis` module is the tool that includes the default distribution of Python that we have for this purpose.\n\n```\nIn [1]: import dis\n\nIn [2]: import main\n\nIn [3]: dis.dis(main.bubble_sorting)\n  5           0 LOAD_GLOBAL              0 (len)\n              2 LOAD_FAST                0 (array)\n              4 CALL_FUNCTION            1\n              6 STORE_FAST               1 (n)\n\n  7           8 LOAD_GLOBAL              1 (range)\n             10 LOAD_FAST                1 (n)\n             12 CALL_FUNCTION            1\n             14 GET_ITER\n        >>   16 FOR_ITER                96 (to 114)\n             18 STORE_FAST               2 (i)\n\n  8          20 LOAD_CONST               1 (True)\n             22 STORE_FAST               3 (sorted_item)\n\n 10          24 LOAD_GLOBAL              1 (range)\n             26 LOAD_FAST                1 (n)\n             28 LOAD_FAST                2 (i)\n             30 BINARY_SUBTRACT\n             32 LOAD_CONST               2 (1)\n             34 BINARY_SUBTRACT\n             36 CALL_FUNCTION            1\n             38 GET_ITER\n        >>   40 FOR_ITER                62 (to 104)\n             42 STORE_FAST               4 (j)\n\n 11          44 LOAD_FAST                0 (array)\n             46 LOAD_FAST                4 (j)\n             48 BINARY_SUBSCR\n             50 LOAD_FAST                0 (array)\n             52 LOAD_FAST                4 (j)\n             54 LOAD_CONST               2 (1)\n             56 BINARY_ADD\n             58 BINARY_SUBSCR\n             60 COMPARE_OP               4 (>)\n             62 POP_JUMP_IF_FALSE       40\n\n 12          64 LOAD_FAST                0 (array)\n             66 LOAD_FAST                4 (j)\n             68 LOAD_CONST               2 (1)\n             70 BINARY_ADD\n             72 BINARY_SUBSCR\n             74 LOAD_FAST                0 (array)\n             76 LOAD_FAST                4 (j)\n             78 BINARY_SUBSCR\n             80 ROT_TWO\n             82 LOAD_FAST                0 (array)\n             84 LOAD_FAST                4 (j)\n             86 STORE_SUBSCR\n             88 LOAD_FAST                0 (array)\n             90 LOAD_FAST                4 (j)\n             92 LOAD_CONST               2 (1)\n             94 BINARY_ADD\n             96 STORE_SUBSCR\n\n 13          98 LOAD_CONST               3 (False)\n            100 STORE_FAST               3 (sorted_item)\n            102 JUMP_ABSOLUTE           40\n\n 15     >>  104 LOAD_FAST                3 (sorted_item)\n            106 POP_JUMP_IF_FALSE       16\n\n 16         108 POP_TOP\n            110 JUMP_ABSOLUTE          114\n            112 JUMP_ABSOLUTE           16\n\n 18     >>  114 LOAD_FAST                0 (array)\n            116 RETURN_VALUE\n```\n\nClearly, this output is the disassembly of the code being executed. When interpreting it we can observe the following details:\n\n- The **first** column corresponds to the equivalent line number in the Python code that you are referring to.\n- In the **second** column, only some lines contain `>>`, which corresponds to the redirects and moves in the Python code.\n- The **remaining** columns correspond to the equivalent lines of the disassembled code.\n\n## Final Recommendations\n\nThe tools for profiling an application are now available, but there are several thoughts that we must take into account. Here are some recommendations:\n\n- CPU Overclocking and BIOS-based Accelerators, as Eco Modes and Battery Optimizing, can affect the real performance of the code under testing. It is recommended to disable them.\n- Telemetry Tools, Anti-Viruses, or Cloud-based File Sharing Systems should be disabled during the experiments.\n- Perform several measurements to obtain valid statistics could help us to confirm the expected behavior of the changes, and confirm the hypothesis.\n- Run experiments in a separated branches from the main repository, and keep the branches that reported code performance improvements\n- Consider using PySpy and Pyinstrument for Web-based applications in environments where it is not possible to make changes to code.\n\n[Part 1. TimeProfiling](https://alexisalulema.com/2022/07/17/python-tips-time-profiling/)\n\n[Part 2. cProfile and line\\_profiler](https://alexisalulema.com/2022/07/24/python-tips-cprofile-and-line_profiler-tools/)\n\n---\n\nIf you think this content was helpful, consider buy me a cup of coffee. 😉☕️👉🏼"},{"slug":"python-tips-cprofile-and-line_profiler-tools","lang":"en","title":"Python Profiling - cProfile and line_profiler Tools (Part 2)","description":"Table of Contents cProfileSnakeViz, for cProfile insightsLine-by-line ProfilingAbout @profile decoratorOther useful toolsgprof2dotPyinstrumentConclusionAppendixInstall with pip or conda cProfile The P…","publishDate":"2022-07-24","updatedDate":null,"tags":["profiling","python"],"translationSlug":null,"url":"https://alexisalulema.com/blog/python-tips-cprofile-and-line_profiler-tools/","series":null,"headings":[{"depth":2,"slug":"table-of-contents","text":"Table of Contents"},{"depth":2,"slug":"cprofile","text":"cProfile"},{"depth":2,"slug":"snakeviz-for-cprofile-insights","text":"SnakeViz, for cProfile insights"},{"depth":2,"slug":"line-by-line-profiling","text":"Line-by-line Profiling"},{"depth":3,"slug":"about-profile-decorator","text":"About @profile decorator"},{"depth":2,"slug":"other-useful-tools","text":"Other useful tools"},{"depth":3,"slug":"gprof2dot","text":"gprof2dot"},{"depth":3,"slug":"pyinstrument","text":"Pyinstrument"},{"depth":2,"slug":"conclusion","text":"Conclusion"},{"depth":2,"slug":"appendix","text":"Appendix"},{"depth":3,"slug":"install-with-pip-or-conda","text":"Install with pip or conda"}],"body":"## Table of Contents\n\n1. [cProfile](#cprofile)\n2. [*SnakeViz*, for cProfile insights](#snakeviz)\n3. [Line-by-line Profiling](#linebyline)\n   1. [About @profile decorator](#profile-dec)\n4. [Other useful tools](#othertools)\n   1. [gprof2dot](#gprof2dot)\n   2. [Pyinstrument](#pyinstrument)\n5. [Conclusion](#conclusion)\n6. [Appendix](#appendix)\n   1. [Install with pip or conda](#install)\n\n## cProfile\n\nThe Python standard library includes the `profile` tool by default; however, it additionally includes `cProfile`, which is an optimization of profile written in C.\n\nA good engineering practice, prior to profiling, is to generate a hypothesis about the speed and performance of the components of the application under evaluation. This is useful, as it helps us identify potential bottlenecks, and allows us to support our findings with evidence.\n\nWe are going to use the same sorting algorithm that we used in the previous post ([Time Profiling](https://alexisalulema.com/2022/07/17/python-tips-time-profiling/)), and `cProfile` to observe the performance:\n\n```\nimport numpy as np\n\ndef bubble_sorting(array):\n    n = len(array)\n\n    for i in range(n):\n        sorted_item = True\n\n        for j in range(n - i - 1):\n            if array[j] > array[j + 1]:\n                array[j], array[j + 1] = array[j + 1], array[j]\n                sorted_item = False\n\n        if sorted_item:\n            break\n\n    return array\n\nwork_array = np.random.randint(1, 100000, size=10000)\nbubble_sorting(work_array)\n```\n\n```\nalexis@MacBook-Pro % python -m cProfile -s cumulative main.py\n         74777 function calls (72805 primitive calls) in 10.061 seconds\n\n   Ordered by: cumulative time\n\n   ncalls  tottime  percall  cumtime  percall filename:lineno(function)\n    110/1    0.000    0.000   10.062   10.062 {built-in method builtins.exec}\n        1    0.000    0.000   10.061   10.061 main.py:1(<module>)\n        1    9.959    9.959    9.959    9.959 main.py:4(bubble_sorting)\n       13    0.000    0.000    0.213    0.016 __init__.py:1(<module>)\n    149/1    0.000    0.000    0.102    0.102 <frozen importlib._bootstrap>:1002(_find_and_load)\n    149/1    0.000    0.000    0.102    0.102 <frozen importlib._bootstrap>:967(_find_and_load_unlocked)\n    138/1    0.000    0.000    0.102    0.102 <frozen importlib._bootstrap>:659(_load_unlocked)\n    109/1    0.000    0.000    0.102    0.102 <frozen importlib._bootstrap_external>:844(exec_module)\n    216/1    0.000    0.000    0.102    0.102 <frozen importlib._bootstrap>:220(_call_with_frames_removed)\n   179/15    0.000    0.000    0.095    0.006 <frozen importlib._bootstrap>:1033(_handle_fromlist)\n    362/8    0.000    0.000    0.095    0.012 {built-in method builtins.__import__}\n  138/135    0.000    0.000    0.042    0.000 <frozen importlib._bootstrap>:558(module_from_spec)\n    28/26    0.000    0.000    0.040    0.002 <frozen importlib._bootstrap_external>:1171(create_module)\n    28/26    0.039    0.001    0.040    0.002 {built-in method _imp.create_dynamic}\n      109    0.000    0.000    0.027    0.000 <frozen importlib._bootstrap_external>:916(get_code)\n        1    0.000    0.000    0.023    0.023 _pickle.py:1(<module>)\n        1    0.000    0.000    0.022    0.022 multiarray.py:1(<module>)\n        1    0.000    0.000    0.022    0.022 overrides.py:1(<module>)\n  ...\n```\n\nThe `-s cumulative` parameter sorts function calls by elapsed execution time, and gives us an idea of where the longest execution time occurred. We can see that the total execution time is similar to the times obtained when we measured just the elapsed execution time, which gives us a good indication of the optimization level of `cProfile`.\n\nLine [9] tells us that the `bubble_sorting` method is called only once, and between lines [11] and [20] we see that most calls occur where the sort is performed, moving and freezing data in memory. These metrics can give us a read on where most of the processing of the function is done, since figuring out what's happening line by line is very tricky with `cProfile`.\n\nThese results can be exported to a binary file for further analysis, using the Python `pstats` library; these statistics basically contain the information obtained by the terminal:\n\n```\nalexis@MacBook-Pro % python -m cProfile -o statistics.stats main.py\n```\n\n```\nimport pstats\n\n# Displays the results, sorted by cumulative elapsed time \np = pstats.Stats('statistics.stats')\np.sort_stats('cumulative')\np.print_stats()\n```\n\nWe can display the information of the calling functions, using the `p.print_callers()` method:\n\n```\nOrdered by: cumulative time\n\nFunction                                                  was called by...\n                                                        ncalls  tottime  cumtime\n{built-in method builtins.exec}                    <-   109/1    0.000    0.116  <frozen importlib._bootstrap>:220(_call_with_frames_removed)\nmain.py:1(<module>)                                <-       1    0.000   10.023  {built-in method builtins.exec}\nmain.py:4(bubble_sorting)                          <-       1    9.905    9.905  main.py:1(<module>)\n<frozen importlib._bootstrap>:1002(_find_and_load) <-       1    0.000    0.002  /<base_dir>/base64.py:3(<module>)\n                                                            1    0.000    0.000  /<base_dir>/bisect.py:1(<module>)\n                                                            2    0.000    0.008  /<base_dir>/ctypes/__init__.py:1(<module>)\n                                                            2    0.000    0.001  /<base_dir>/datetime.py:1(<module>)\n                                                            1    0.000    0.001  /<base_dir>/hashlib.py:82(__get_builtin_constructor)\n                                                            2    0.000    0.007  /<base_dir>/hmac.py:1(<module>)\n                                                            2    0.000    0.003  /<base_dir>/json/__init__.py:1(<module>)\n                                                            1    0.000    0.001  /<base_dir>/json/scanner.py:1(<module>)\n                                                            4    0.000    0.000  /<base_dir>/ntpath.py:2(<module>)\n                                                            3    0.000    0.002  /<base_dir>/pathlib.py:1(<module>)\n                                                            4    0.000    0.002  /<base_dir>/pickle.py:1(<module>)\n                                                            1    0.000    0.005  /<base_dir>/platform.py:3(<module>)\n                                                            3    0.000    0.002  /<base_dir>/random.py:1(<module>)\n                                                            3    0.000    0.011  /<base_dir>/secrets.py:1(<module>)\n                                                            2    0.000    0.001  /<base_dir>/site-packages/numpy/__init__.py:1(<module>)\n...\n```\n\nOr the `p.print_callees()` option to show which functions call for other functions:\n\n```\nOrdered by: cumulative time\n\nFunction                                  called...\n                                      ncalls  tottime  cumtime\n{built-in method builtins.exec}  ->       1    0.000    0.000  /<base_dir>/__future__.py:1(<module>)\n                                          1    0.000    0.000  /<base_dir>/_compat_pickle.py:9(<module>)\n...\n                                          1    0.000    0.000  /<base_dir>/urllib/parse.py:1(<module>)\n                                          1    0.000   10.023  main.py:1(<module>)\nmain.py:1(<module>)              ->       1    0.000    0.117  <frozen importlib._bootstrap>:1002(_find_and_load)\n                                           1    9.905    9.905  main.py:4(bubble_sorting)\n                                           1    0.000    0.001  {method 'randint' of 'numpy.random.mtrand.RandomState' objects}\n main.py:4(bubble_sorting)        ->       1    0.000    0.000  {built-in method builtins.len}\n <frozen importlib._bootstrap>:1002(_find_and_load)  ->     149    0.000    0.000  <frozen importlib._bootstrap>:152(__init__)\n                                         149    0.000    0.001  <frozen importlib._bootstrap>:156(__enter__)\n                                         149    0.000    0.000  <frozen importlib._bootstrap>:160(__exit__)\n                                         148    0.000    0.000  <frozen importlib._bootstrap>:185(cb)\n                                       149/1    0.000    0.117  <frozen importlib._bootstrap>:967(_find_and_load_unlocked)\n                                         149    0.000    0.000  {method 'get' of 'dict' objects}\n <frozen importlib._bootstrap>:967(_find_and_load_unlocked)  ->     4/1    0.000    0.007  <frozen importlib._bootstrap>:220(_call_with_frames_removed)\n                                       138/1    0.000    0.117  <frozen importlib._bootstrap>:659(_load_unlocked)\n...\n```\n\nThe information provided by `cProfile` is extensive and a complete visualization of the information will be necessary in order to obtain the greatest benefit from the tool. Something that can help us in the visualization is to use the `SnakeViz` module, which diagrams the information obtained by `cProfile`.\n\n## *SnakeViz*, for *cProfile* insights\n\nAccording to the description found on the website:\n\n> *SnakeViz is a browser based graphical viewer for the output of Python’s cProfile module and an alternative to using the standard library pstats module...*\n>\n> [SnakeViz Website](https://jiffyclub.github.io/snakeviz/)\n\nUsing the statistics file obtained with `cProfile`, we can visualize the results of the profiling:\n\n```\nsnakeviz statistics.stats\n```\n\n![](https://images.alexisalulema.com/blog/python-tips-cprofile-and-line_profiler-tools/image-1024x727.png)\n\nIf we choose the Sunburst chart style, I find it clearer that most of the processing is done from the main call to `bubble_sorting` (which is obvious, considering the simplicity of the test case):\n\n![](https://images.alexisalulema.com/blog/python-tips-cprofile-and-line_profiler-tools/SnakeViz.02.png)\n\nThese graphs are useful to communicate to others about the results of the execution of the module under test, and it helps to visually identify which part of the process consumes the most processing.\n\n## Line-by-line Profiling\n\n[`line_profiler`](https://github.com/pyutils/line_profiler) is a great tool for identifying causes of poor code performance as it evaluates Python functions line by line, and this is where using `cProfile` should lead us: to evaluate sections of problematic code and that will be the ideal point to use `line_profiler`.\n\n**TIP📝** **->** It is important to keep track of code changes and profiling results as some changes may cause drawbacks in other sections.\n\n`line_profiler` includes two utilities:\n\n- The `@profile` decorator, which indicates the functions to be profiled.\n- `kernprof` is responsible for executing and recording the evaluation metrics and statistics.\n\nOptionally, `kernprof` has the `-v` (verbose) parameter which, when included, displays the results on termination; and when omitted it generates a file with extension `.lprof` that can be analyzed later using `line_profiler`.\n\nFor example:\n\n```\nimport numpy as np\n\n# WARNING DEVS: this decorator doesn't need to import anything\n# this is intended for kernprof usage only\n# be prepared with some coffee to wait until the profiling finishes\n@profile\ndef bubble_sorting(array):\n    n = len(array)\n\n    for i in range(n):\n        sorted_item = True\n\n        for j in range(n - i - 1):\n            if array[j] > array[j + 1]:\n                array[j], array[j + 1] = array[j + 1], array[j]\n                sorted_item = False\n\n        if sorted_item:\n            break\n\n    return array\n\nwork_array = np.random.randint(1, 100000, size=10000)\nbubble_sorting(work_array)\n```\n\n```\nalexis@MacBook-Pro % kernprof -l -v main.py\nWrote profile results to main.py.lprof\nTimer unit: 1e-06 s\n\nTotal time: 68.9938 s\nFile: main.py\nFunction: bubble_sorting at line 4\n\nLine #      Hits         Time  Per Hit   % Time  Line Contents\n==============================================================\n     4                                           @profile\n     5                                           def bubble_sorting(array):\n     6         1          4.7      4.7      0.0      n = len(array)\n     7\n     8      9863       3127.6      0.3      0.0      for i in range(n):\n     9      9863       3076.8      0.3      0.0          sorted_item = True\n    10\n    11  49995547   15022385.0      0.3     21.8          for j in range(n - i - 1):\n    12  49985684   27385581.9      0.5     39.7              if array[j] > array[j + 1]:\n    13  24989733   18838493.1      0.8     27.3                  array[j], array[j + 1] = array[j + 1], array[j]\n    14  24989733    7737957.2      0.3     11.2                  sorted_item = False\n    15\n    16      9863       3134.5      0.3      0.0          if sorted_item:\n    17         1          0.3      0.3      0.0              break\n    18\n    19         1          6.1      6.1      0.0      return array\n```\n\nThis is much more descriptive than the results obtained from `cProfile`, but the idea is to use `cProfile` to identify which functions you should evaluate with `line_profiler`. Additionally, we can learn about the cost of operations and improve our programming skills.\n\n### About @profile decorator\n\nPerhaps you were able to see the comment close the bubble\\_sorting method definition:\n\n```\n# WARNING DEVS: this decorator doesn't need to import anything\n# this is intended for kernprof usage only\n# be prepared with some coffee to wait until the profiling finishes\n```\n\nAdding the @profile decorator will work when we are using line\\_profiler (or memory\\_profiler), but if you try to run, for example, unit tests, it will fail with the following message:\n\n```\nTraceback (most recent call last):\n  File \"/Users/alexis/PythonProfiling/main.py\", line 3, in <module>\n    @profile\nNameError: name 'profile' is not defined\n\nProcess finished with exit code 1\n```\n\nYou don't want broken code into your Git repository, so you can add the following code to avoid the error message on runtime:\n\n```\nimport numpy as np\n\nif 'line_profiler' not in dir() and 'profile' not in dir():\n    def profile(func):\n        return func\n\n@profile\ndef bubble_sorting(array):\n    n = len(array)\n\n    for i in range(n):\n        sorted_item = True\n\n        for j in range(n - i - 1):\n            if array[j] > array[j + 1]:\n                array[j], array[j + 1] = array[j + 1], array[j]\n                sorted_item = False\n\n        if sorted_item:\n            break\n\n    return array\n\nwork_array = np.random.randint(1, 100000, size=1000)\nbubble_sorting(work_array)\n```\n\n## Other useful tools\n\n### gprof2dot\n\nAnother useful tool is **gprof2dot**, which allows you to view the statistics obtained with cProfile. Requires prior installation of the [Graphviz](https://graphviz.org/download/) app. `gprof2dot` contains the `dot` command, and it is used as follows:\n\n```\nalexis@MacBook-Pro % gprof2dot -f pstats statistics.stats | dot -Tpng -o output.png\n```\n\nThis pipeline transfers the analysis performed by `gprof2dot` to the Graphviz `dot` command, and generates the following graph:\n\n![](https://images.alexisalulema.com/blog/python-tips-cprofile-and-line_profiler-tools/output-578x1024.png)\n\n### Pyinstrument\n\nThis tool complements cProfile by adding some missing functionalities like more visualization options and integration with web frameworks like Django, Flask or FastAPI, which I consider is a great help for profiling Web applications and REST APIs.\n\nThe most basic way to use Pyinstrument, is just calling the command, followed by the script to be evaluated:\n\n```\nalexis@MacBook-Pro % pyinstrument main.py\n\n  _     ._   __/__   _ _  _  _ _/_   Recorded: 07:50:33  Samples:  73\n /_//_/// /_\\ / //_// / //_'/ //     Duration: 9.885     CPU time: 10.188\n/   _/                      v4.2.0\n\nProgram: main.py\n\n9.885 <module>  <string>:1\n   [4 frames hidden]  <string>, runpy\n      9.885 _run_code  runpy.py:64\n      └─ 9.885 <module>  main.py:1\n         ├─ 9.772 bubble_sorting  main.py:4\n         └─ 0.112 <module>  numpy/__init__.py:1\n               [193 frames hidden]  numpy, pickle, <built-in>, pathlib, a...\n```\n\nFor more details on how to integrate Pyinstrument into the related frameworks, we can click the following links:\n\n- [Profile code in Jupyter/IPython](https://pyinstrument.readthedocs.io/en/latest/guide.html#profile-code-in-jupyter-ipython)\n- [Profile a web request in Django](https://pyinstrument.readthedocs.io/en/latest/guide.html#profile-a-web-request-in-django)\n- [Profile a web request in Flask](https://pyinstrument.readthedocs.io/en/latest/guide.html#profile-a-web-request-in-flask)\n- [Profile a web request in FastAPI](https://pyinstrument.readthedocs.io/en/latest/guide.html#profile-a-web-request-in-fastapi)\n\n## Conclusion\n\n`cProfile` and `line_profiler` offer us a great detail of information about the execution of the sections of code that we want to evaluate. On our part, it is important to understand the methodology to obtain the best results of using these tools.\n\nIn each execution cycle and code modification we can formulate hypotheses about what we expect from the change, and evaluate its performance. Logging the results will be important to understand which code changes positively impacted performance.\n\nAdditionaly, cProfile is the default option in PyCharm, which makes a kind of \"default\" option for many developers.\n\n![](https://images.alexisalulema.com/blog/python-tips-cprofile-and-line_profiler-tools/pycharm.cProfile-866x1024.png)\n\n## Appendix\n\n### Install with pip or conda\n\n```\n# PIP\npip install snakeviz\npip install line_profiler[ipython]\npip install gprof2dot\npip install pyinstrument\n\n# Conda\nconda install -c anaconda snakeviz\nconda install -c conda-forge line_profiler\nconda install -c conda-forge gprof2dot\nconda install -c conda-forge pyinstrument\n```\n\n[Part 1. Time Profiling](https://alexisalulema.com/2022/07/24/python-tips-cprofile-and-line_profiler-tools/)\n\n[Part 3. Memory Profiling](https://alexisalulema.com/2022/08/07/python-profiling-memory-profiling-part-3-final/)\n\n---\n\nIf you think this content was helpful, consider buy me a cup of coffee. ;) ☕👉"},{"slug":"python-tips-time-profiling","lang":"en","title":"Python Profiling - Time Profiling (Part 1)","description":"Table of Contents IntroductionTime Profiling Toolsfunctools.wrapstimeit moduletime / gtimeConclusion Introduction Many times, the code we write requires optimizations, and profiling helps us find the …","publishDate":"2022-07-17","updatedDate":null,"tags":["profiling","python"],"translationSlug":null,"url":"https://alexisalulema.com/blog/python-tips-time-profiling/","series":null,"headings":[{"depth":2,"slug":"table-of-contents","text":"Table of Contents"},{"depth":2,"slug":"introduction","text":"Introduction"},{"depth":2,"slug":"time-profiling-tools","text":"Time Profiling Tools"},{"depth":3,"slug":"functoolswraps","text":"functools.wraps"},{"depth":3,"slug":"timeit-module","text":"timeit module"},{"depth":3,"slug":"time--gtime","text":"time / gtime"},{"depth":2,"slug":"conclusion","text":"Conclusion"}],"body":"## Table of Contents\n\n1. [Introduction](#intro)\n2. [Time Profiling Tools](#timetools)\n   1. [functools.wraps](#functools)\n   2. [*timeit* module](#timeit)\n   3. [time / gtime](#gtime)\n3. [Conclusion](#conclusion1)\n\n## Introduction\n\nMany times, the code we write requires optimizations, and profiling helps us find the problematic sections of code, investing the least amount of work on fixing the issue, while aiming for the goal of gaining speed and reducing resource consumption.\n\nWe can profile any resource, not just CPU and memory, but also parameters like bandwidth and I/O storage speed rates.\n\nIt is important to differentiate between a piece of code that you think may be the bottleneck, and what profiling can help us to hypothesize as the root of the problem. This is important to understand, because it is what determines the saving of effort in finding an optimal solution.\n\nThe first thing is to identify what causes the bottleneck, in order to extract a test case and isolate the section of the system that needs to be tested.\n\n## Time Profiling Tools\n\n### functools.wraps\n\nSorting algorithms are great test cases to evaluate performance. As for profiling time, the most common way to do this is using `print`, it works fine but soon becomes unmanageable when trying to identify where we should write `print` statements to evaluate time, but it is useful for quick evaluations.\n\nA slightly cleaner approach is using decorators, as in the following example:\n\n```\nimport time\nimport numpy as np\nfrom functools import wraps\n\ndef measuring_time(fn):\n    @wraps(fn)\n    def wrapper(*args, **kwargs):\n        t1 = time.time()\n        result = fn(*args, **kwargs)\n        t2 = time.time()\n        print(f'@measuring_time: {fn.__name__} :: {t2 - t1} seconds.')\n        return result\n    return wrapper\n\n@measuring_time\ndef bubble_sorting(array):\n    n = len(array)\n\n    for i in range(n):\n        sorted_item = True\n\n        for j in range(n - i - 1):\n            if array[j] > array[j + 1]:\n                array[j], array[j + 1] = array[j + 1], array[j]\n                sorted_item = False\n\n        if sorted_item:\n            break\n\n    return array\n\nwork_array = np.random.randint(1, 100000, size=10000)\nbubble_sorting(work_array)\n\n>>>> @measuring_time: bubble_sorting :: 9.98324704170227 seconds.\n```\n\n### *timeit* module\n\n`timeit` module is another tool for timing the execution of a process, expressed in seconds. It is most useful with simple expressions, but it can be used with the above example via the Python interface (as shown below), or via a Python script:\n\n```\nIn [1]: import timeit\nIn [2]: s = \"\"\"\\\n   ...: import numpy as np\n   ...: def bubble_sorting(array):\n   ...:     n = len(array)\n   ...:     for i in range(n):\n   ...:         sorted_item = True\n   ...:         for j in range(n - i - 1):\n   ...:             if array[j] > array[j + 1]:\n   ...:                 array[j], array[j + 1] = array[j + 1], array[j]\n   ...:                 sorted_item = False\n   ...:         if sorted_item:\n   ...:             break\n   ...:     return array\n   ...: work_array = np.random.randint(1, 100000, size=10000)\n   ...: bubble_sorting(work_array)\n   ...: \"\"\"\nIn [3]: timeit.timeit(stmt=s, number=1)\nOut[3]: 9.969992417000014\n```\n\nIt is important to mention what the official `timeit` documentation says, regarding the garbage collector:\n\n> *By default, `timeit()` temporarily turns off garbage collection during the timing... The disadvantage is that GC may be an important component of the performance of the function being measured. If so, GC can be re-enabled as the first statement in the setup string...*\n>\n> [timeit — Measure execution time of small code snippets](https://docs.python.org/3/library/timeit.html)\n\nIf the parameter `number` is not specified, `timeit` will execute the instruction for 1 million iterations, so it's better to specify the number of times you want to execute the instruction, and keeping a benchmark of the results.\n\n```\nIn [4]: timeit.timeit(stmt=s, number=10)\nOut[4]: 99.57917050000015\n```\n\nIt worths considering the variation in the load with which the process will be executed in normal and edge conditions, since that is going to affect the CPU and memory consumption. The system resource monitor can help us observe the variation in resource consumption:\n\n![](https://images.alexisalulema.com/blog/python-tips-time-profiling/timeit.monitor-1024x533.png)\n\n### time / gtime\n\nYou can use the Unix-like systems `time` command (`gtime` for MacOS, install it with `brew install gnu-time`), which is not Python-specific.\n\n```\nalexis@MacBook-Pro % gtime -p python main.py      \n@measuring_time: bubble_sorting :: 9.93534517288208 seconds.\nreal 10.02\nuser 10.31\nsys 0.08\n```\n\nThe -p flag returns the following results. These results indicate the time consumed inside de function (**real**), the total time including timing (**user**), and the time spent in kernel-level functions (**sys**).\n\n```\nalexis@MacBook-Pro % gtime --verbose python main.py\n@measuring_time: bubble_sorting :: 9.969091176986694 seconds.\n        Command being timed: \"python main.py\"\n        User time (seconds): 10.34\n        System time (seconds): 0.09\n        Percent of CPU this job got: 103%\n        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:10.09\n        Average shared text size (kbytes): 0\n        Average unshared data size (kbytes): 0\n        Average stack size (kbytes): 0\n        Average total size (kbytes): 0\n        Maximum resident set size (kbytes): 27728\n        Average resident set size (kbytes): 0\n        Major (requiring I/O) page faults: 449\n        Minor (reclaiming a frame) page faults: 1604\n        Voluntary context switches: 122\n        Involuntary context switches: 1034\n        Swaps: 0\n        File system inputs: 0\n        File system outputs: 0\n        Socket messages sent: 0\n        Socket messages received: 0\n        Signals delivered: 0\n        Page size (bytes): 16384\n        Exit status: 0\n```\n\nThe option --verbose will show more detailed information about the metrics occurring during the process execution.\n\n## Conclusion\n\nTime measurement tools used during the execution of a process can give us an idea not only of the execution time, but depending on the test cases used to perform these measurements, they can give us an idea of the performance of the algorithms, and check if the used workload in the test cases may be causing bottlenecks.\n\nAdditionally, they can be a metric for the Big-O notation classification of the algorithm.\n\n[Part 2. cProfile and line\\_profiler Tools](https://alexisalulema.com/2022/07/24/python-tips-cprofile-and-line_profiler-tools/)\n\n---\n\nIf you think this content was helpful, consider buy me a cup of coffee. ;) ☕👉"},{"slug":"javascript-levenshtein-algorithm-implementation","lang":"en","title":"JavaScript Levenshtein Algorithm Implementation","description":"One of the most known algorithms used by spell checkers and auto-complete fields is perhaps the Levenshtein algorithm, which basically minimizes the Levenshtein distance between the target word and th…","publishDate":"2021-09-08","updatedDate":null,"tags":["javascript","levenshtein"],"translationSlug":null,"url":"https://alexisalulema.com/blog/javascript-levenshtein-algorithm-implementation/","series":null,"headings":[],"body":"One of the most known algorithms used by spell checkers and auto\\-complete fields is perhaps the [**Levenshtein**](https://en.wikipedia.org/wiki/Levenshtein_distance) algorithm, which basically minimizes the Levenshtein distance between the target word and the user input; or, in other words, the minimum number of missing characters and additions needed to transform a string into another from an existing dictionary. \n\nThe algorithm is amazing and works like a charm, however, you should carefully think how you are going to use it because it has a very high computational cost, and it is better to consider designing ideas before starting using it.\n\nFor many applications, the information to fulfill a dropdown control comes from a RESTFUL service, and it brings thousands of options from a database . In these cases, using the Levenshtein algorithm probably is not a good idea, because you will have to implement the algorithm in the data query (or, in the worst in the cases, in a stored procedure) to avoid bringing all the options and perform the algorithm in memory. Sounds good at first, but the user doesn't want to wait for seconds on every keypress, so you need to think what is your best scenario to use it.\n\nHere some ideas about what to consider for a fast selection logic:\n\n- Number of choices: 1\\-50 \\-\\> the dropdown is enough\n- Number of choices: \\>50 but \\< few thousands \\-\\> auto\\-complete is great, and good to apply Levenshtein algorithm ✅\n- Number of choices: more than 10,000 items \\-\\> think in a kind of chain of dropdown controls to reduce the number of items by categories\n\nIn conclusion, using this algorithm depends on your scenario. For most of the times I needed it, I had to use it in the browser, so here is it in Javascript:\n\n```\nclass LevenshteinModel {\n\n    compute(first: string, second: string): number {\n        if (first.length == 0)\n            return second.length;\n\n        if (second.length == 0)\n            return first.length;\n\n        let current = 1;\n        let previous = 0;\n        let r: number[][] = [[], []];\n\n        for (let i = 0; i <= second.length; i++)\n            r[previous][i] = i;\n\n        for (let i = 0; i < first.length; i++) {\n            r[current][0] = i + 1;\n\n            for (let j = 1; j <= second.length; j++) {\n                const cost = (second[j - 1] === first[i]) ? 0 : 1;\n                r[current][j] = Math.min(\n                    r[previous][j] + 1, \n                    r[current][j - 1] + 1, \n                    r[previous][j - 1] + cost);\n            }\n\n            previous = (previous + 1) % 2; \n            current = (current + 1) % 2;\n        }\n\n        const weight = r[previous][second.length];\n        return weight;\n    }\n}\n```\n\n---\n\nIf you think this content was helpful, consider buy me a cup of coffee. ;) ☕👉"},{"slug":"support-vector-machines-svm-for-classification","lang":"en","title":"Support Vector Machines (SVM) for Classification","description":"The purpose of this document is to present the linear classification algorithm SVM. The development of this concept has been based on previous ideas that have supported the development of SVM as an al…","publishDate":"2018-12-31","updatedDate":null,"tags":["matlab","ml","svm"],"translationSlug":null,"url":"https://alexisalulema.com/blog/support-vector-machines-svm-for-classification/","series":null,"headings":[{"depth":2,"slug":"approach","text":"Approach"},{"depth":2,"slug":"experiment","text":"Experiment"},{"depth":2,"slug":"conclusions","text":"Conclusions"},{"depth":2,"slug":"references","text":"References"}],"body":"The purpose of this document is to present the linear classification algorithm SVM. The development of this concept has been based on previous ideas that have supported the development of SVM as an algorithm with good generalization capacity, based on an optimization criterion that minimizes complexity; with which we have achieved substantial improvements in terms of complexity and generalization with respect to similar classification algorithms.\n\nThe motivation to explore and show SVM in this document is to have an alternative algorithm that adapts to scenarios where other classification methods show weaknesses. We will explain the algorithm and show a comparative example to show its benefits.\n\nRegarding the state of the art, there are currently several algorithms suitable for classification tasks:\n\n- Linear Classifiers\n- Decision Trees\n- Random Forest\n- Neural Networks\n- Nearest Neighbor\n\nEach algorithm has its applicability, its strengths and weaknesses. A weakness we have observed, for example, with decision trees is that they have a high classification error rate with few data and several classes, and that they are prone to over\\-fitting. We have also observed that linear classifiers are prone to have a high bias.\n\nIt is of great importance the visualization of the data and the way in which the classification algorithm behaves, and considering the results of Random Forest, even when they are acceptable, they are not easy to interpret visually: this is a very important lack because by observing the results of the classification process we can understand the behavior and make decisions.\n\nGenerally, Neural Networks are very powerful, but they are also more computationally demanding than other algorithms, which makes complicated implement them in, for example, Field\\-Programmable Gate Array chips (FPGA). Less powerful algorithms may be suitable for most scenarios and could be implemented without many problems in modern hardware only or mixed solutions.\n\nSpecifically, we mention Ridge Regression (RR) applied in classification because, comparing it with SVM, they share similar characteristics, but SVM simplifies and improves certain aspects.\n\nOne of these similarities, and where SVM makes a difference, is that SVM consists of the criterion based on Structural Risk Minimization, plus a linear loss function (Empirical Risk). This Structural Risk adds a regularization, which is a control of the learning machine’s complexity; criteria being used by both SVM and RR.\n\nAs we can see, RR shares the same structural risk; however, the loss function is based on a quadratic criterion; in this case, very high errors introduced by misclassified data can bias the result.\n\n**Ridge Regression**\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.02.png)\n\nOn the other hand, regularization in SVM is similar to RR, but we obtain a resolution for ω from an ordinary linear loss function:\n\n**SVM**\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.03.png)\n\nThis condition is an advantage for SVM, which makes it *less sensitive to errors* introduced by incorrectly classified outliers.\n\n## Approach\n\nIn a learning machine, Risk R(α) is the *expectation of the error*that we can get from evaluating distributed data x\\_n accordingly to a probability distribution function F(x). For each x\\_n we could have a label y\\_n, by this consideration we assume there is a conditional distribution between x and y: F(x\\|y).\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.04.png)\n\nOur objective is minimizing the error to make the learning machine useful, so we adjust this by minimizing the Risk. This Risk function *must have a minimum* in the point where a function of the error is minimized over all samples.\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.05.png)\n\nHowever, this concept of R(α) cannot be computed because F(x) and F(x\\|y) are unknown, so we need to compute an expectation based on the samples we have available, this is the Empirical Risk.\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.06.png)\n\nFrom this concept, we can see that we need to *minimize the Empirical Risk*, and the estimation function f(x,α) is what decides if data is correctly evaluated. However, having a machine with a very complex f(x,α) can make all results from testing data to be always correct, and this is not good because the Empirical Risk will tend to zero (**over\\-fitting**).\n\nAs we see, we have to *limit the complexity* of the machine, this minimization can be done by using the criteria to minimize the empirical risk: **Structural Risk Minimization** (SRM).\n\nAn important concept used to derive SVM is the **Vapnik\\-Chervonenkis (VC) Dimension**. As an example, we consider the 2\\-D space and 3 points. We have 3 possible positions for a classification hyperplane separating the points, which indicates 2 points are linearly independent.\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.07-1024x333.png)\n\nWhat happens if we have 2 points? We can correctly classify them in any way, no matter how the points are shattered (over\\-fitting).\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.08.png)\n\nWhat happens if we have 4 points? We cannot classify them in all the possible ways.\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.09-1024x513.png)\n\nAs a conclusion, VC Dimension “h” is the maximum number of data points that can be shattered, and it gives us a measure of the complexity of linear functions. In a 2\\-D space, it is 3\\.\n\n*h\\=n\\+1*\n\nThe VC Theorem says the following:\n\n*“Consider a set of m points in space Rn. Choose any one of the points as origin. Then the m points can be shattered by oriented hyperplanes if and only if the position vectors of the remaining points are linearly independent.”*\n\nAs we see from the previous enunciate of VC Dimension, on the space R^2 with 4 points, we cannot always separate them by a hyperplane, because in the case(s) it fails, the remaining points are not linearly independent.\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.10.png)\n\nWhen we have 2 points in R^2, they will be always correctly classified, no matter how they are shattered, then the risk is always zero.\n\nClearly, if we have less points than dimensions n\\+1 we can always classify them correctly. This is not good because any classifier will make zero errors, causing over\\-fitting.\n\nSVM consists on *minimizing the empirical and structural risk* (structural risk minimizes the complexity) in linear machines by explicitly limiting the VC Dimension of the hyperplane, but this is not real because data is linearly separable on it, and empirical risk is just zero. We need to define an empirical risk function that takes in account the samples that are INSIDE or ON the margin.\n\nConsider a non\\-separable set of data D and slack variables ξ\\_n as:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.11.png)\n\nξ\\_n are zero for data outside the margin:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.12.png)\n\nThe idea consists on minimizing the empirical risk, which is the sum of all the slack variables:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.13.png)\n\nSVM consists on minimizing the previous empirical risk plus the structural risk, which minimizes the complexity:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.14-1024x304.png)\n\nThis condition applies to misclassified or inside the margin samples. If ξ\\_n\\<0, the sample is outside the margin and properly classified.\n\nC is a tradeoff parameter that weights the importance of the empirical risk with the importance of the structural risk.\n\nWe start with the definition of the Karush\\-Kuhn\\-Tucker (KKT) Conditions:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.15.png)\n\nWe need to replace these conditions into the Lagrangian:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.16-1024x601.png)\n\nLet’s start with KKT (1\\):\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.17.png)\n\nNow, replace condition in term A:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.18.png)\n\nAnd, replace condition in term CC:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.19-1024x498.png)\n\nNow, from KKT (4\\): μ\\_n ξ\\_n\\=0, we can see that D can be removed.\n\nAnd from KKT (3\\): ∑\\_(n\\=1\\)^N▒〖α\\_n y\\_n\\=0〗, we can see that a term from the resultant CC can be removed.\n\nNow we have the following:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.20.png)\n\nAnalyzing the latest term, we see that we can remove it.\n\n- If sample is ON the margin: 0≤α\\_n≤C, and ξ\\_n\\=0\n- If sample is INSIDE the margin: ξ\\_n\\>0, and α\\_n\\=C\n\nFinally, we have the result:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.21.png)\n\nIn matrix notation:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.22.png)\n\nThis equation must be optimized with respect to the dual variables. Here X^T X is a Gram matrix of dot products: K\\_(i,j)\\=x\\_i^T x\\_j\n\nThe dual can be written as follows:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.23.png)\n\nSome properties of Support Vectors are the following:\n\n- Support Vectors ON margin have a relationship with the VC Dimension, as for in R2 we ALWAYS have 3 support vectors ON margin.\n- Support Vector are INSIDE or ON the margin\n- For Support Vectors ON margin, 0 \\< alpha\\< C\n- For Support Vectors INSIDE the margin alpha \\= C\n\nWhen we have an outlier away from the margin and classification hyperplane, we know that .\n\nSVM behavior remain stable and throwing good results with small and mid\\-size data, but the most important problem on SVM is that the term K that we mentioned before can be huge if we have a lot of data for training.\n\n## Experiment\n\nIn order to demonstrate SVM applied in data classification, we have built an experiment that can be executed on Matlab, previous installation of the public library LIBSVM, which implements the SVM algorithm. The complete code for the experiment, and LIBSVM’s documentation can be found on references.\n\nFor this experiment we have created a data set by generating 40 random data points sparse around 4 centroids, and classifying 20 of them as 1, and the other 20 as \\-1\\.\n\nWe start defining the initial data and, with help of LIBSVM, we train the model for SVM classification. With the initial data and trained model, we obtain results similar to the following:\n\n```\noptimization finished, #iter = 162\nnu = 0.171989\nobj = -617.860697, rho = -0.466784\nnSV = 8, nBSV = 5\nTotal nSV = 8\nAccuracy = 95% (38/40) (classification)\nK>> model\n\nmodel =\n    struct with fields:\n\n        Parameters: [5×1 double]\n            nr_class: 2\n             totalSV: 8\n                 rho: -0.4668\n               Label: [2×1 double]\n          sv_indices: [8×1 double]\n               ProbA: []\n               ProbB: []\n                 nSV: [2×1 double]\n             sv_coef: [8×1 double]\n                 SVs: [8×2 double]\n```\n\nAdditionally, the model has the following information from α:\n\n```\n>> alpha\n\n100.0000\n43.9775\n100.0000\n100.0000\n-100.0000\n-100.0000\n-93.3429\n-50.6346\n```\n\nThe accuracy of the model is 92\\.5%, and comparing with the α array, we can see that we have only 5 misclassified or in margin points (±100, as C was declared 100 when we used LIBSVM, so α \\= C), and 3 points ON the margin.\n\nWe have compared manually calculated results with the predicted results:\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.24-599x1024.png)\n\n- Inside Margin (yellow)\n- On Margin (green)\n- Misclassified (red)\n\nAs we can see, the results are similar between manually evaluating the equation, and the results from LIBSVM iterations (162 iter); except for 1 value incorrectly evaluated as “well classified”, when it is misclassified, as we will see in the next plot with data points and the following equations:\n\n- ω^T x\\+b\\=1 (blue)\n- ω^T x\\+b\\=0 (green)\n- ω^T x\\+b\\=\\-1 (red)\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.25-1024x882.png)\n\n***Fig. 1\\. Data points classified by SVM algorithm; additionally, classification boundary and margins have been remarked.***\n\nFrom this plot we see that data points are being classified by the AVM model we built with LIBSVM, and we can mention the following conclusions:\n\n- After running the script several times, we can see the we ALWAYS have 3 support vectors for this 2D space, and we see the VC Theorem working on SVM.\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.26-1024x820.png)\n\n***Fig. 2\\. Support Vectors remarked on margin.***\n\n- As we saw in the comparison between predicted and Y, there are 2 misclassified points\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.27-1024x696.png)\n\n***Fig. 3\\. Misclassified points inside margin.***\n\n- As we saw in alpha values, we have 3 points ON the margin, and 5 points misclassified or INSIDE margin.\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.28-1024x817.png)\n\n***Fig. 4\\. Support Vectors and Points misclassified or inside margin***\n\nWe also tried with data points less sparse around centroids, and we see that the machine always correctly classifies all data points, as they are clearly separated and machine can interpret them correctly, as we can see in Figure 5\\.\n\nIn the same way, we tried with data points widely sparse around centroids, and we can see that data are approaching to the other centroids (or getting away). As result, we can see it is harder for the machine to correctly classify data points, as depicted in Figure 6\\.\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.29-1024x815.png)\n\n***Fig. 5\\. Data points narrowly sparse around centroids.***\n\n![](https://images.alexisalulema.com/blog/support-vector-machines-svm-for-classification/svm.30-1024x828.png)\n\n***Fig. 6\\. Data points widely sparse around centroids.***\n\n## Conclusions\n\nSVM is an algorithm based on SRM, which offers the advantage of a large generalization capacity.\n\nThe results of an experiment have been presented using data generated automatically by software, through which we can see the features of the algorithm, and its behavior with low, medium, and highly scattered data, and we can understand its robustness.\n\nOne of the best benefits we obtain from SVM is that when we have an outlier away from the margin and classification hyperplane, we know that α\\=C. It is important because its maximum contribution on error is C, it will not affect the behavior of the machine.\n\nComparing with the other classification algorithms, it is clear that now we have an option less sensitive to misclassified outliers. It is visually understandable, so it makes suitable to graphically observe the classifier’s behavior. It is also simple enough to derive mixed versions on FPGA, where we can codesign part of the algorithm with hardware and software (C code).\n\nSVM behavior remain stable and throwing good results with small and mid\\-size data; however, the most important problem on SVM is that the term K that we mentioned before can be huge if we have a lot of data for training.\n\nAs we understand now, SVM classification gives us good results, and it fits in several researching scenarios as a good alternative to other algorithms. For future works, we need to apply these concepts for non\\-linear machines.\n\n## References\n\n- Types of classification algorithms in Machine Learning, *By*. Mandeep Sidana, Technology Consultant, Sifium Technologies. Available: [https://medium.com/@sifium/machine\\-learning\\-types\\-of\\-classification\\-9497bd4f2e14](https://medium.com/@sifium/machine-learning-types-of-classification-9497bd4f2e14)\n- Classification Model Pros and Cons, *By*. Chris Tufts. Available: [https://github.com/ctufts/Cheat\\_Sheets/wiki/Classification\\-Model\\-Pros\\-and\\-Cons](https://github.com/ctufts/Cheat_Sheets/wiki/Classification-Model-Pros-and-Cons)\n- LIBSVM, Software and Documentation, *By*. Chih\\-Chung Chang and Chih\\-Jen Lin. Available: [https://www.csie.ntu.edu.tw/\\~cjlin/libsvm/](https://www.csie.ntu.edu.tw/~cjlin/libsvm/)"},{"slug":"ddd-clean-architecture-template","lang":"en","title":"DDD Clean Architecture Template","description":"(GitHub Repo: https://github.com/alulema/DDD-CleanArchitectureTemplate) These last 10 months I’ve been delighted working with ASP.NET Core, considering the improvements made by Microsoft to this frame…","publishDate":"2018-04-30","updatedDate":null,"tags":["c#","cqrs","ddd","design-patterns","mediator"],"translationSlug":null,"url":"https://alexisalulema.com/blog/ddd-clean-architecture-template/","series":null,"headings":[],"body":"(GitHub Repo: [https://github.com/alulema/DDD\\-CleanArchitectureTemplate](https://github.com/alulema/DDD-CleanArchitectureTemplate))\n\nThese last 10 months I’ve been delighted working with ASP.NET Core, considering the improvements made by Microsoft to this framework, and the option to work on my Macbook as well ![🙂](https://images.alexisalulema.com/blog/ddd-clean-architecture-template/1f642.svg)\n\nHowever, an additional job to be done has been migrating the inherited skills and code snippets to its .NET Core version. This is the case with organizing the architecture to be adapted to ASP.NET Core, and other related Design Patterns. It’s not a big deal at the end, but it always take some time figuring out how to organize the code and adapt patterns.\n\nToday I want to share with you a “final” template  and don’t have to reinvent the wheel each time. The features of this project are the following:\n\n- ASP.NET Core WebAPI\n- SOLID Clean Architecture\n- Domain\\-Driven Design\n- CQRS Premium Architecture\n- MediatR Implementation\n- Dependency Injection\n- Logging Infrastructure\n- EntityFramework In\\-Memory Database\n\n![](https://images.alexisalulema.com/blog/ddd-clean-architecture-template/solution_ddd.png)\n\nI will be using this template in future projects, and I hope it could be useful for you as well. See you next time!"},{"slug":"csharp-sudoku-solver","lang":"en","title":"C# Sudoku Solver","description":"(GitHub Repo:&nbsp;https://github.com/alulema/SudokuSolverNet) I was revisiting a couple of basic AI concepts:&nbsp; Depth First Search and Constraint Propagation, and I found a very good explanation …","publishDate":"2018-04-19","updatedDate":null,"tags":["ai","c#","constraint-propagation","depth-first-search"],"translationSlug":null,"url":"https://alexisalulema.com/blog/csharp-sudoku-solver/","series":null,"headings":[{"depth":2,"slug":"constraint-propagation","text":"Constraint Propagation"},{"depth":3,"slug":"elimination-strategy","text":"Elimination Strategy."},{"depth":3,"slug":"one-choice-strategy","text":"One-Choice Strategy."},{"depth":2,"slug":"first-depth-search","text":"First Depth Search"},{"depth":2,"slug":"c-solution","text":"C# Solution"}],"body":"(GitHub Repo: <https://github.com/alulema/SudokuSolverNet>)\n\nI was revisiting a couple of basic AI concepts:  Depth First Search and Constraint Propagation, and I found a very good explanation by Professor Peter Norvig ([Solving Every Sudoku Puzzle](https://norvig.com/sudoku.html)), I just want to add a couple of simple explanations for a better understanding of the concepts.\n\n## Constraint Propagation\n\nThis technique is used to reduce the search space and make the problem easier to solve. In Sudoku domain, it means following the game rules and define the possible solutions to assign to each box in the table.\n\n### Elimination Strategy.\n\nThe idea is to reduce the possible values to be assigned to a box. Just let me redefine the concepts for a better understanding.\n\n#### *Box.*\n\n![](https://images.alexisalulema.com/blog/csharp-sudoku-solver/box.png)\n\nWe have 81 boxes in the puzzle\n\n#### *Unit.*\n\n![](https://images.alexisalulema.com/blog/csharp-sudoku-solver/unit3.png)\n\n![](https://images.alexisalulema.com/blog/csharp-sudoku-solver/unit2.png)\n\n![](https://images.alexisalulema.com/blog/csharp-sudoku-solver/unit1.png)\n\nEach box will be into 3 different units, each unit contains 9 boxes, each of them have different values, from 1 to 9\\.\n\n#### *Peer.*\n\n![](https://images.alexisalulema.com/blog/csharp-sudoku-solver/peer.png)\n\nEach peer contains 21 boxes, and the box’s value cannot be in any other element of this peer.\n\nConsidering this 3 concepts, the puzzle should be seen in this way:\n\n![](https://images.alexisalulema.com/blog/csharp-sudoku-solver/puzzle.png)\n\n### One\\-Choice Strategy.\n\nThis strategy consists in seen the only choice to be accepted by a box in a unit. This helps to reduce the options in Elimination.\n\nIn the following image, we can see that number 4 can be assigned to only one cell.\n\n![](https://images.alexisalulema.com/blog/csharp-sudoku-solver/only-choice.png)\n\n## First Depth Search\n\nThis strategy will be applied after Constraint\\-Propagation, and considering it didn’t find a solution. It will be more CPU demanding, but it is the actually a basic approach; it doesn’t prune any options at all. The only check it makes is that all the assigned values so far are consistent with each other.\n\nThis strategy can be described as a tree, where we try possible solutions by assigning the possible options to a box and checking one by one if it solves the puzzle re\\-applying Constraint\\-Propagation again with this testing value.\n\n![](https://images.alexisalulema.com/blog/csharp-sudoku-solver/tree.png)\n\nBy using First Depth Search, this tree is navigated in the following order:\n\nA\\-B\\-D\\-E\\-I\\-J\\-C\\-F\\-G\\-H\\-K\\-L\n\nYou can think each leaf of this tree as:\n\n*“Will it solve the puzzle?”*\n\nIn sudoku, we are going to start with the box with less options to be solved.\n\n![](https://images.alexisalulema.com/blog/csharp-sudoku-solver/depthfirstsearch.png)\n\nFor example, in this unit, we can start with box with 56 as the probable choices, then we try to solve the puzzle with 5, if it is not solved we try with 6, and so on.\n\n## C\\# Solution\n\nProfessor Norvig implements the algoritms in Python, I’ve done this in C\\# as I consider C\\# is a little bit more clear than Python to understand how it works (it is just my opinion, maybe you find Python more clear)."},{"slug":"tensorflow-high-level-libraries-tf-estimator","lang":"en","title":"TensorFlow High-Level Libraries: TF Estimator","description":"TensorFlow has several high-level libraries allowing us to reduce time modeling all with core code. TF Estimator makes it simple to create and train models for training, evaluating, predicting and exp…","publishDate":"2018-03-26","updatedDate":null,"tags":["machine-learning","python","tensorflow"],"translationSlug":null,"url":"https://alexisalulema.com/blog/tensorflow-high-level-libraries-tf-estimator/","series":null,"headings":[],"body":"TensorFlow has several high\\-level libraries allowing us to reduce time modeling all with core code. TF Estimator makes it simple to create and train models for training, evaluating, predicting and exporting. TF Estimator provides 4 main functions on any kind of estimator:\n\n- estimator.fit()\n- estimator.evaluate()\n- estimator.predict()\n- estimator.export()\n\nAll predefined estimators are subclasses of **tf.estimator.Estimator**. You can see specific details of predefined estimators in [TensorFlow’s Website \\| Estimators](https://www.tensorflow.org/programmers_guide/estimators). Take in account that previous version based on tf.contrib.learn.Estimator  has been deprecated.\n\nAs it is defined by TensorFlow Team, “Pre\\-made Estimators enable you to work at a much higher conceptual level than the base TensorFlow APIs”, what means it creates the graph for you. The workflow on using Estimators API is as follows:\n\n![](https://images.alexisalulema.com/blog/tensorflow-high-level-libraries-tf-estimator/tfestimators-1.png)\n\nLet’s see a simple example using **tf.estimator.EstimatorSpec**:\n\n```\nimport os\nimport tensorflow as tf\nfrom tensorflow.examples.tutorials.mnist import input_data\n\ntf.reset_default_graph()\n\n# 2. Write your function to import data\nmnist = input_data.read_data_sets(os.path.join('.', 'mnist'), one_hot=False)\n\nx_train = mnist.train.images\ny_train = mnist.train.labels\nx_test = mnist.test.images\ny_test = mnist.test.labels\n\nn_classes = 10\nbatch_size = 100\nn_steps = 1000\nlearning_rate = 0.01\n\ndef model_function(features, labels, mode):\n\n    # 1. Find the most suitable estimator\n    # EstimatorSpec fully defines the model to be run by an Estimator.\n    espec_op = tf.estimator.EstimatorSpec\n\n    # 3. Define features to be used in data\n    # features is a dict as per Estimator specifications\n    x = features['images']\n    # define the network\n    layer_1 = tf.layers.dense(x, 32)\n    layer_2 = tf.layers.dense(layer_1, 32)\n    logits = tf.layers.dense(layer_2, n_classes)\n\n    # define predicted classes\n    predicted_classes = tf.argmax(logits, axis=1)\n    if mode == tf.estimator.ModeKeys.PREDICT:\n        espec = espec_op(mode, predictions=predicted_classes)\n    else:\n        # define loss and optimizer\n        entropy_op = tf.nn.sparse_softmax_cross_entropy_with_logits\n        loss_op = tf.reduce_mean(entropy_op(logits=logits, labels=tf.cast(labels, dtype=tf.int32)))\n        optimizer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)\n        train_op = optimizer.minimize(loss_op, global_step=tf.train.get_global_step())\n\n        # define accuracy\n        accuracy_op = tf.metrics.accuracy(labels=labels, predictions=predicted_classes)\n        espec = espec_op(mode=mode,\n                         predictions=predicted_classes,\n                         loss=loss_op,\n                         train_op=train_op,\n                         eval_metric_ops={'accuracy': accuracy_op})\n        return espec\n\n# 4. Instantiate your selected estimator\nmodel = tf.estimator.Estimator(model_function)\n\n# 5. Training!\ntrain_input_fn = tf.estimator.inputs.numpy_input_fn(\n    x={'images': x_train},\n    y=y_train,\n    batch_size=batch_size,\n    num_epochs=None,\n    shuffle=True)\nmodel.train(train_input_fn, steps=n_steps)\n\n# 6. Use the trained estimator\n# evaluate the model\neval_input_fn = tf.estimator.inputs.numpy_input_fn(\n    x={'images': x_test},\n    y=y_test,\n    batch_size=batch_size,\n    shuffle=False)\nmetrics = model.evaluate(eval_input_fn)\nprint(metrics)\n```\n\nThe output is as follows:\n\n![](https://images.alexisalulema.com/blog/tensorflow-high-level-libraries-tf-estimator/output1.png)\n\nThere are many other predefined Estimators, I will add more examples in the future.\n\nSee you next time!"},{"slug":"tensorflow-way-for-linear-regression","lang":"en","title":"TensorFlow Way for Linear Regression","description":"In my two previous posts, we saw how we can perform Linear Regression using TensorFlow, but I’ve used Linear Least Squares Regression and Cholesky Decomposition, both them use matrices to resolve regr…","publishDate":"2018-02-06","updatedDate":null,"tags":["linear-regression","machine-learning","python","tensorflow"],"translationSlug":null,"url":"https://alexisalulema.com/blog/tensorflow-way-for-linear-regression/","series":null,"headings":[{"depth":3,"slug":"conclusion","text":"Conclusion"}],"body":"In my two previous posts, we saw how we can perform Linear Regression using TensorFlow, but I’ve used Linear Least Squares Regression and Cholesky Decomposition, both them use matrices to resolve regression, and TensorFlow isn’t a requisite for this, but you can use more general packages like NumPy.\n\nOne of the most common applications of TensorFlow is training models by reducing loss function by backpropagation, and training the model by batches, this allow us to optimize the Gradient Descent and converge to solution.\n\nAdvantage: It allow us to use big amounts of data, and TensorFlow process it by batches.  \nDisadvantage: Solution is not so precise as if we use [Linear Least Squares](https://alexisalulema.com/2018/01/18/linear-least-squares-regression-with-tensorflow/) or [Cholesky Decomposition](https://alexisalulema.com/2018/01/20/cholesky-decomposition-for-linear-regression-with-tensorflow/).\n\nIn the following example I’ll use Iris data from scikit\\-learn (Sepal Length vs Pedal Width seem to have a linear relationship in the form ***y \\= mx \\+ b***), and L2 loss function:\n\n```\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport tensorflow as tf\nfrom sklearn import datasets\n\nsess = tf.Session()\niris = datasets.load_iris()\n\nx_vals = np.array([x[3] for x in iris.data])\ny_vals = np.array([x[0] for x in iris.data])\n\nlearning_rate = 0.25\nbatch_size = 25\n\nx_data = tf.placeholder(shape=(None, 1), dtype=tf.float32)\ny_target = tf.placeholder(shape=(None, 1), dtype=tf.float32)\nm = tf.Variable(tf.random_normal(shape=[1, 1]))\nb = tf.Variable(tf.random_normal(shape=[1, 1]))\n\nmodel_output = tf.add(tf.matmul(x_data, m), b)\n\nloss = tf.reduce_mean(tf.square(y_target - model_output))\ninit = tf.global_variables_initializer()\nsess.run(init)\n\nmy_opt = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)\ntrain_step = my_opt.minimize(loss)\n\nloss_vec = []\nfor i in range(100):\n    rand_index = np.random.choice(len(x_vals), size=batch_size)\n    rand_x = np.transpose([x_vals[rand_index]])\n    rand_y = np.transpose([y_vals[rand_index]])\n    sess.run(train_step, feed_dict={x_data: rand_x, y_target: rand_y})\n    temp_loss = sess.run(loss, feed_dict={x_data: rand_x, y_target: rand_y})\n    loss_vec.append(temp_loss)\n\n    if (i + 1) % 25 == 0:\n        print('Step #' + str(i + 1) + ' A = ' + str(sess.run(m)) + 'b = ' + str(sess.run(b)))\n        print('Loss = ''' + str(temp_loss))\n\n[m_slope] = sess.run(m)\n[y_intercept] = sess.run(b)\nbest_fit = []\nfor i in x_vals:\n    best_fit.append(m_slope * i + y_intercept)\n\nplt.plot(x_vals, y_vals, 'o', label='Points')\nplt.plot(x_vals, best_fit, 'r-', label='Linear Reg.', linewidth=3)\nplt.legend(loc='upper left')\nplt.title('Sepal Length vs Pedal Width')\nplt.xlabel('Pedal Width')\nplt.ylabel('Sepal Length')\nplt.show()\nplt.plot(loss_vec, 'k-')\nplt.title('L2 Loss')\nplt.xlabel('Batches')\nplt.ylabel('L2 Loss')\nplt.show()\n\n>> Step #25 A = [[0.90450525]]b = [[4.513903]]\n>> Loss = 0.26871145\n>> Step #50 A = [[0.94802374]]b = [[4.7737966]]\n>> Loss = 0.2799243\n>> Step #75 A = [[1.1669612]]b = [[4.906186]]\n>> Loss = 0.2010894\n>> Step #100 A = [[0.84404486]]b = [[4.730052]]\n>> Loss = 0.15839271\n```\n\n![](https://images.alexisalulema.com/blog/tensorflow-way-for-linear-regression/graph2w.png)\n\n![](https://images.alexisalulema.com/blog/tensorflow-way-for-linear-regression/graph1.png)\n\n### Conclusion\n\nAs we can see in the graphic, the gradient descent tries to converge to solution, but it is not so precise as matrix operations for Linear Least Squares and Cholesky Decomposition."},{"slug":"cholesky-decomposition-for-linear-regression-with-tensorflow","lang":"en","title":"Cholesky Decomposition for Linear Regression with TensorFlow","description":"beenhereUpdate 10.2023This entry has been updated with versions TensorFlow 2.12.1 and PyTorch 2.0.1 Several years have already passed since the onset of the Deep Learning boom. I have witnessed impres…","publishDate":"2018-01-20","updatedDate":null,"tags":["cholesky","linear-regression","machine-learning","python","pytorch","tensorflow"],"translationSlug":"descomposicion-cholesky-para-regresion-lineal","url":"https://alexisalulema.com/blog/cholesky-decomposition-for-linear-regression-with-tensorflow/","series":null,"headings":[{"depth":3,"slug":"update-102023","text":"Update 10.2023"}],"body":"*beenhere*\n\n### Update 10.2023\n\nThis entry has been updated with versions TensorFlow 2.12.1 and PyTorch 2.0.1\n\nSeveral years have already passed since the onset of the Deep Learning boom. I have witnessed impressive achievements like ChatGPT and Midjourney, however, I am still amazed at how traditional methods like Cholesky decomposition remain extremely useful and efficient. Particularly, for tasks like Linear Regression, this method stands out due to its efficiency and speed in calculation.\n\nEven though [Least Squares Linear Regression](https://www.alexisalulema.com/2018/01/18/linear-least-squares-regression-with-tensorflow/) is straightforward and accurate, it can become inefficient when dealing with large matrices. Cholesky decomposition offers an alternative to solve these matrices effectively. This technique decomposes a matrix into two triangular matrices: a lower one (lower) and a transposed lower one (L and LT), with which the original matrix is obtained, as illustrated in the following figure.\n\n![](https://images.alexisalulema.com/blog/cholesky-decomposition-for-linear-regression-with-tensorflow/cholesky.graphic-1-1024x403.png)\n\n$$\n\\begin{bmatrix}<br>a_{11} & a_{12} & a_{13} & a_{14}\\\\<br>a_{21} & a_{22} & a_{23} & a_{24}\\\\<br>a_{31} & a_{32} & a_{33} & a_{34}\\\\<br>a_{41} & a_{42} & a_{43} & a_{44}<br>\\end{bmatrix}=\\begin{bmatrix}<br>l_{11} & 0 & 0 & 0\\\\<br>l_{21} & l_{22} & 0 & 0\\\\<br>l_{31} & l_{32} & l_{33} & 0\\\\<br>l_{41} & l_{42} & l_{43} & l_{44}<br>\\end{bmatrix}\\cdot \\begin{bmatrix}<br>l_{11} & l_{21} & l_{31} & l_{41}\\\\<br>0 & l_{22} & l_{32} & l_{42}\\\\<br>0 & 0 & l_{33} & l_{43}\\\\<br>0 & 0 & 0 & l_{44}<br>\\end{bmatrix}\n$$\n\nThe product of the multiplication of the two matrices is shown below:\n\n$$\nA=\\begin{bmatrix}<br>{l_{11}}^{2} & l_{21}l_{11} & l_{31}l_{11} & l_{41}l_{11}\\\\<br>l_{21}l_{11} & {l_{21}}^{2}+{l_{22}}^{2} & l_{31}l_{21}+l_{32}l_{22} & l_{41}l_{21}+l_{42}l_{22}\\\\<br>l_{31}l_{11} & l_{31}l_{21}+l_{32}l_{22} & {l_{31}}^{2}+{l_{32}}^{2}+{l_{33}}^{2} & l_{41}l_{31}+l_{42}l_{32}+l_{43}l_{33}\\\\<br>l_{41}l_{11} & l_{41}l_{21}+l_{42}l_{22} & l_{41}l_{31}+l_{42}l_{32}+l_{43}l_{33} & {l_{31}}^{2}+{l_{32}}^{2}+{l_{33}}^{2}+{l_{44}}^{2}<br>\\end{bmatrix}\n$$\n\nFrom this point on, each element is an equation from which the following is deduced:\n\n$$\nl_{11}=\\sqrt{a_{11}}\n$$\n\n$$\nl_{21}=\\frac{a_{21}}{l_{11}}\n$$\n\n$$\nl_{31}=\\frac{a_{31}}{l_{11}}\n$$\n\n$$\nl_{41}=\\frac{a_{41}}{l_{11}}\n$$\n\n$$\nl_{22}=\\sqrt{a_{22}-{l_{21}}^{2}}\n$$\n\n$$\nl_{32}=\\frac{a_{32}-l_{21}l_{31}}{l_{22}}\n$$\n\n$$\nl_{42}=\\frac{a_{42}-l_{21}l_{41}}{l_{22}}\n$$\n\n$$\nl_{33}=\\sqrt{a_{33}-{l_{31}}^{2}-{l_{32}}^{2}}\n$$\n\n$$\nl_{43}=\\frac{a_{43}-l_{31}l_{41}-l_{32}l_{42}}{l_{33}}\n$$\n\n$$\nl_{44}=\\sqrt{a_{44}-{l_{41}}^{2}-{l_{42}}^{2}-{l_{43}}^{2}}\n$$\n\nA feature (or limitation) of Cholesky decomposition is that it ***only works on positive symmetric matrices***.\n\nFinally, linear regression through Cholesky decomposition is analogous to Linear Least Squares, but reduced to solving a system of linear equations.\n\n![](https://images.alexisalulema.com/blog/cholesky-decomposition-for-linear-regression-with-tensorflow/lser_03.png)\n\n![](https://images.alexisalulema.com/blog/cholesky-decomposition-for-linear-regression-with-tensorflow/loss_cholesky.png)\n\nCholesky Decomposition is already implemented in TensorFlow, PyTorch, or NumPy, however, you can review more details about [Cholesky Decomposition](https://rosettacode.org/wiki/Cholesky_decomposition).\n\n- [TensorFlow](#advgb-tabs-tab0)\n- [PyTorch](#advgb-tabs-tab1)\n- [Numpy](#advgb-tabs-tab2)\n\nTensorFlow\n\n```\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nimport numpy as np\n\n# Generate data\nx_vals = np.linspace(start=0, stop=10, num=100)\ny_vals = x_vals + np.random.normal(loc=0, scale=1, size=100)\n\nx_vals_column = np.transpose(np.matrix(x_vals))\nones_column = np.transpose(np.matrix(np.repeat(a=1, repeats=100)))\nX = np.column_stack((x_vals_column, ones_column))\nY = np.transpose(np.matrix(y_vals))\nX_tensor = tf.constant(X, dtype=tf.float64)\nY_tensor = tf.constant(Y, dtype=tf.float64)\n\n# Perform computations\ntX_X = tf.matmul(tf.transpose(X_tensor), X_tensor)\nL = tf.linalg.cholesky(tX_X)\ntX_Y = tf.matmul(tf.transpose(X_tensor), Y_tensor)\nsol1 = tf.linalg.solve(L, tX_Y)\nsol2 = tf.linalg.solve(tf.transpose(L), sol1)\n\n# Extract coefficients\nsolution_eval = sol2.numpy()\nm_slope = solution_eval[0][0]\nb_intercept = solution_eval[1][0]\n\nprint('slope (m): ' + str(m_slope))\nprint('intercept (b): ' + str(b_intercept))\n\n# Visualization\nbest_fit = []\nfor i in x_vals:\n    best_fit.append(m_slope * i + b_intercept)\n\nplt.plot(x_vals, y_vals, 'o', label='Data')\nplt.plot(x_vals, best_fit, 'r-', label='Linear Regression', linewidth=3)\nplt.legend(loc='upper left')\nplt.show()\n```\n\nPyTorch\n\n```\nimport matplotlib.pyplot as plt\nimport torch\nimport numpy as np\n\n# Generate data\nx_vals = np.linspace(start=0, stop=10, num=100)\ny_vals = x_vals + np.random.normal(loc=0, scale=1, size=100)\n\nx_vals_column = np.transpose(np.matrix(x_vals))\nones_column = np.transpose(np.matrix(np.repeat(a=1, repeats=100)))\nX = np.column_stack((x_vals_column, ones_column))\nY = np.transpose(np.matrix(y_vals))\nX_tensor = torch.tensor(X, dtype=torch.float64)\nY_tensor = torch.tensor(Y, dtype=torch.float64)\n\n# Perform computations\ntX_X = torch.mm(X_tensor.T, X_tensor)\nL = torch.cholesky(tX_X)\ntX_Y = torch.mm(X_tensor.T, Y_tensor)\nsol = torch.cholesky_solve(tX_Y, L)\n\n# Extract coefficients\nm_slope = sol[0][0].item()\nb_intercept = sol[1][0].item()\n\nprint('slope (m): ' + str(m_slope))\nprint('intercept (b): ' + str(b_intercept))\n\n# Visualization\nbest_fit = []\nfor i in x_vals:\n    best_fit.append(m_slope * i + b_intercept)\n\nplt.plot(x_vals, y_vals, 'o', label='Data')\nplt.plot(x_vals, best_fit, 'r-', label='Linear Regression', linewidth=3)\nplt.legend(loc='upper left')\nplt.show()\n```\n\nNumpy\n\n```\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n# Generate data\nx_vals = np.linspace(start=0, stop=10, num=100)\ny_vals = x_vals + np.random.normal(loc=0, scale=1, size=100)\n\nx_vals_column = np.transpose(np.matrix(x_vals))\nones_column = np.transpose(np.matrix(np.repeat(a=1, repeats=100)))\nX = np.column_stack((x_vals_column, ones_column))\nY = np.transpose(np.matrix(y_vals))\n\n# Perform computations using Cholesky Decomposition\ntX_X = np.dot(X.T, X)\nL = np.linalg.cholesky(tX_X)\ntX_Y = np.dot(X.T, Y)\nsol = np.linalg.solve(L, tX_Y)\nbeta = np.linalg.solve(L.T, sol)\n\n# Extract coefficients\nm_slope = beta[0,0]\nb_intercept = beta[1,0]\n\nprint('slope (m): ' + str(m_slope))\nprint('intercept (b): ' + str(b_intercept))\n\n# Visualization\nbest_fit = []\nfor i in x_vals:\n    best_fit.append(m_slope * i + b_intercept)\n\nplt.plot(x_vals, y_vals, 'o', label='Data')\nplt.plot(x_vals, best_fit, 'r-', label='Linear Regression', linewidth=3)\nplt.legend(loc='upper left')\nplt.show()\n```\n\nslope (m): 1.0830263227926582  \nintercept (b): -0.3348165868955632\n\n![](https://images.alexisalulema.com/blog/cholesky-decomposition-for-linear-regression-with-tensorflow/linear_regression.png)\n\nAs you can see, this solution is very similar to Linear Least Squares, but this decomposition is sometimes much more efficient and numerically stable."},{"slug":"linear-least-squares-regression-with-tensorflow","lang":"en","title":"Linear Least Squares Regression with TensorFlow","description":"Linear Least Squares Regression is by far the most widely used regression method, and it is suitable for most cases when data behavior is linear. By definition, a line is defined by the following equa…","publishDate":"2018-01-18","updatedDate":null,"tags":["linear-regression","machine-learning","python","tensorflow"],"translationSlug":null,"url":"https://alexisalulema.com/blog/linear-least-squares-regression-with-tensorflow/","series":null,"headings":[],"body":"Linear Least Squares Regression is by far the most widely used regression method, and it is suitable for most cases when data behavior is linear. By definition, a line is defined by the following equation:\n\n![](https://images.alexisalulema.com/blog/linear-least-squares-regression-with-tensorflow/lser_01.png)\n\nFor all data points (xi, yi) we have to minimize the sum of the squared errors:\n\n![](https://images.alexisalulema.com/blog/linear-least-squares-regression-with-tensorflow/lser_02-1024x184.png)\n\nThis is the equation we need to solve for all data points:\n\n![](https://images.alexisalulema.com/blog/linear-least-squares-regression-with-tensorflow/lser_03.png)\n\nThe solution for this equation is A (I’m not going to show how this solution is found, but you can see it in [Linear Least Squares \\- Wikipedia](https://en.wikipedia.org/wiki/Linear_least_squares_(mathematics)), and some code in several programming languages as well), which is defined by:\n\n![](https://images.alexisalulema.com/blog/linear-least-squares-regression-with-tensorflow/lser_04.png)\n\nNow, let’s see the implementation with TensorFlow:\n\n```\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nimport numpy as np\n\nsess = tf.Session()\nx_vals = np.linspace(0, 10, num=100)\ny_vals = x_vals + np.random.normal(loc=0, scale=1, size=100)\n\nx_vals_column = np.transpose(np.matrix(x_vals))\nones_column = np.transpose(np.matrix(np.repeat(1, repeats=100)))\nX = np.column_stack((x_vals_column, ones_column))\nY = np.transpose(np.matrix(y_vals))\n\nX_tensor = tf.constant(X)\nY_tensor = tf.constant(Y)\n\ntX_X = tf.matmul(tf.transpose(X_tensor), X_tensor)\ntX_X_inv = tf.matrix_inverse(tX_X)\nproduct = tf.matmul(tX_X_inv, tf.transpose(X_tensor))\nA = tf.matmul(product, Y_tensor)\nA_eval = sess.run(A)\n\nm_slope = A_eval[0][0]\nb_intercept = A_eval[1][0]\nprint('slope (m): ' + str(m_slope))\nprint('intercept (b): ' + str(b_intercept))\n\nbest_fit = []\nfor i in x_vals:\nbest_fit.append(m_slope * i + b_intercept)\n\nplt.plot(x_vals, y_vals, 'o', label='Data')\nplt.plot(x_vals, best_fit, 'r-', label='Linear Regression', linewidth=3)\nplt.legend(loc='upper left')\nplt.show()\n```\n\nslope (m): 1\\.0108287140073253  \nintercept (b): 0\\.14322921334345343\n\n![](https://images.alexisalulema.com/blog/linear-least-squares-regression-with-tensorflow/linear_regression.png)\n\nAs you can see, the implementation is just executing basic matrix operations, the advantage of using TensorFlow in this case is that we can add this process to a more complex graph."},{"slug":"classification-loss-functions-part-ii","lang":"en","title":"Classification Loss Functions (Part II)","description":"In my previous post, I mentioned 3 loss functions, which are mostly intended to be used in Regression models. This time, I’m going to talk about Classification Loss Functions, which are going to be us…","publishDate":"2017-12-15","updatedDate":null,"tags":["machine-learning","python","tensorflow"],"translationSlug":null,"url":"https://alexisalulema.com/blog/classification-loss-functions-part-ii/","series":null,"headings":[{"depth":2,"slug":"hinge-loss-function","text":"Hinge Loss Function"},{"depth":2,"slug":"sigmoid-cross-entropy-loss-function","text":"Sigmoid Cross-Entropy Loss Function"},{"depth":2,"slug":"weighted-cross-entropy-loss-function","text":"Weighted Cross Entropy Loss Function"},{"depth":2,"slug":"conclusions","text":"Conclusions"}],"body":"In my previous post, I mentioned 3 loss functions, which are mostly intended to be used in Regression models. This time, I’m going to talk about Classification Loss Functions, which are going to be used to evaluate loss when predicting categorical outcomes.\n\nLet’s consider the following vector to help us to show how loss functions behave:\n\n```\nimport tensorflow as tf\n\nsess = tf.Session()\n\nx_function = tf.linspace(-3., 5., 500)\ntarget = tf.constant(1.)\ntargets = tf.fill([500, ], 1.)\n```\n\n## Hinge Loss Function\n\nThis function is used for training classifiers, most notably for SVM (Support Vector Machine). It is defined by the following:\n\n![](https://images.alexisalulema.com/blog/classification-loss-functions-part-ii/hinge-loss.png)\n\nThe central idea is to compute a loss between with two target classes, 1 and \\-1\\.\n\n```\nhinge_loss = tf.maximum(0., 1. - tf.multiply(target, x_function))\nhinge_out = sess.run(hinge_loss)\n```\n\n## Sigmoid Cross\\-Entropy Loss Function\n\nThis loss function can be used in machine learning for classification and optimization, it is referred as the logistic loss function, and can be used, for example, when we are classifying between two classes 0 or 1\\. TensorFlow internally performs this function, but mathematically it is defined as the following:\n\n![](https://images.alexisalulema.com/blog/classification-loss-functions-part-ii/sigmoid_eq.png)\n\n![](https://images.alexisalulema.com/blog/classification-loss-functions-part-ii/sigmoid_xentropy.png)\n\n```\ncross_entropy_sigmoid_loss = tf.nn.sigmoid_cross_entropy_with_logits(logits=x_function, labels=targets)\ncross_entropy_sigmoid_out = sess.run(cross_entropy_sigmoid_loss)\n```\n\n## Weighted Cross Entropy Loss Function\n\nThis is a weighted version of the previous loss function, as we assign a weight on the positive target. For example, we can provide a weight of 0\\.5, as follows.\n\n```\nweight = tf.constant(0.5)\ncross_entropy_weighted_loss = tf.nn.weighted_cross_entropy_with_logits(x_function, targets, weight)\ncross_entropy_weighted_out = sess.run(cross_entropy_weighted_loss)\n```\n\nLet’s plot these loss functions!\n\n![](https://images.alexisalulema.com/blog/classification-loss-functions-part-ii/classification_loss_functions.png)\n\n## Conclusions\n\n- Hinge Loss Function is great for SVM, but it is affected by outliers.\n- Cross Entropy Loss is very stable on training models, but it is less robust and can be affected on big data.\n\n."},{"slug":"loss-functions-part-1","lang":"en","title":"Loss Functions (Part 1)","description":"Implementing Loss Functions is very important to machine learning algorithms because we can measure the error from the predicted outputs to the target values. Algorithms get optimized by evaluating ou…","publishDate":"2017-12-07","updatedDate":null,"tags":["machine-learning","python","tensorflow"],"translationSlug":null,"url":"https://alexisalulema.com/blog/loss-functions-part-1/","series":null,"headings":[{"depth":2,"slug":"l2-norm-loss-function-least-squares-error-lse","text":"L2-norm Loss Function (Least Squares Error LSE)"},{"depth":2,"slug":"l1-norm-loss-function-least-absolute-error-lae","text":"L1-norm Loss Function (Least Absolute Error LAE)"},{"depth":2,"slug":"pseudo-huber-loss-function","text":"Pseudo-Huber Loss Function"},{"depth":2,"slug":"conclusions","text":"Conclusions"},{"depth":3,"slug":"i-have-several-outliers-but-they-are-not-so-important-which-loss-function-should-i-use","text":"I have several outliers but they are not so important, which loss function should I use?"},{"depth":3,"slug":"i-have-several-outliers-they-occur-under-circumstances-that-i-should-take-in-account-which-loss-function-should-i-use","text":"I have several outliers, they occur under circumstances that I should take in account. Which loss function should I use?"},{"depth":3,"slug":"i-have-several-outliers-i-dont-want-them-to-affect-my-model-and-i-need-to-converge-to-the-best-solution","text":"I have several outliers, I don’t want them to affect my model and I need to converge to the best solution."}],"body":"Implementing Loss Functions is very important to machine learning algorithms because we can measure the error from the predicted outputs to the target values. Algorithms get optimized by evaluating outcomes depending on a specified loss function, and TensorFlow works in this way as well. We can think on Loss Functions telling us how good the predictions are compared to the expected values.\n\nThere are several loss functions we can use to train a machine learning algorithm, and I’ll try to explain some of them and when we can use them. Let’s consider the following vector to help us to show how loss functions behave:\n\n```\nimport tensorflow as tf\n\nsess = tf.Session()\n\n# f(x) and target = 0\nx_function = tf.linspace(-1., 1., 500)\ntarget = tf.constant(0.)\n```\n\n## L2\\-norm Loss Function (Least Squares Error LSE)\n\nIt is just the sum of the square of the distance to the target\n\n![](https://images.alexisalulema.com/blog/loss-functions-part-1/equation1.png)\n\nL2 squares the error increasing by a lot if error \\> 1 (outlier can cause this kind of error), so the model is very sensitive to variations, and, when it is used to optimize an algorithm, it adjusts the model to minimize the error.\n\nFor any small adjustments of a data point, the regression line will move only slightly (regression parameters are continuous functions of the data).\n\nTensorFlow has a built\\-in implementation, called `tf.nn.l2_loss()`, which actually perform the half of the previous equation.\n\n![](https://images.alexisalulema.com/blog/loss-functions-part-1/equation2.png)\n\nIn order to show you how loss functions behave, we are going to plot the points before to perform the summatory.\n\n```\nL2_function = tf.square(target - x_function)\nL2_output = sess.run(L2_function)\n```\n\n## L1\\-norm Loss Function (Least Absolute Error LAE)\n\nIt is just the sum of the absolute value of the distance to the target\n\n![](https://images.alexisalulema.com/blog/loss-functions-part-1/equation3.png)\n\nIf we compare L1 with L2, we can deduct that L1 is less sensitive to errors caused by outliers (because it doesn’t square the error). So, if we need to ignore the effects of outliers, we could consider using L1 instead of L2, if it is important to consider outliers, then L2 is a better option.\n\nOne issue to be aware of is that the L1 is not smooth at the target and this can result in algorithms not converging well.\n\n```\nL1_function = tf.abs(target - x_function)\nL1_output = sess.run(L1_function)\n```\n\n## Pseudo\\-Huber Loss Function\n\nIt is a smooth approximation to the Huber loss function. Huber loss is, as Wikipedia defines it, “a loss function used in robust regression, that is less sensitive to outliers in data than the squared error loss ”. This loss function attempts to take the best of the L1 and L2 by being convex near the target and less steep for extreme values. The form depends on an extra parameter, delta, which dictates how steep it will be.\n\n![](https://images.alexisalulema.com/blog/loss-functions-part-1/equation4.png)\n\nWe are going to test 3 values for delta:\n\n```\ndelta1 = tf.constant(0.2)\npseudo_huber1 = tf.multiply(tf.square(delta1), tf.sqrt(1. + tf.square((target - x_function)/delta1)) - 1.)\npseudo_huber1_output = sess.run(pseudo_huber1)\n\ndelta2 = tf.constant(1.)\npseudo_huber2 = tf.multiply(tf.square(delta2), tf.sqrt(1. + tf.square((target - x_function) / delta2)) - 1.)\npseudo_huber2_output = sess.run(pseudo_huber2)\n\ndelta3 = tf.constant(5.)\npseudo_huber3 = tf.multiply(tf.square(delta3), tf.sqrt(1. + tf.square((target - x_function) / delta2)) - 1.)\npseudo_huber3_output = sess.run(pseudo_huber3)\n```\n\nLet’s plot this loss functions!\n\n```\nimport matplotlib.pyplot as plt\n\nx_array = sess.run(x_function)\nplt.plot(x_array, L2_output, 'b-', label='L2')\nplt.plot(x_array, L1_output, 'r--', label='L1')\nplt.plot(x_array, pseudo_huber1_output, 'm,', label='Pseudo-Huber (0.2)')\nplt.plot(x_array, pseudo_huber2_output, 'k-.', label='Pseudo-Huber (1.0)')\nplt.plot(x_array, pseudo_huber3_output, 'g:', label='Pseudo-Huber (5.0)')\nplt.ylim(-0.2, 0.4)\nplt.legend(loc='lower right', prop={'size': 11})\nplt.title('LOSS FUNCTIONS')\nplt.show()\n```\n\n![](https://images.alexisalulema.com/blog/loss-functions-part-1/plot.png)\n\n## Conclusions\n\n### I have several outliers but they are not so important, which loss function should I use?\n\n***L1 Loss Function***, but probably you will have problem to converge to the best solution, so consider low learning rate.\n\n### I have several outliers, they occur under circumstances that I should take in account. Which loss function should I use?\n\n***L2 Loss Function***, but too separated outlier could affect the model so probably you could consider normalize data before\n\n### I have several outliers, I don’t want them to affect my model and I need to converge to the best solution.\n\nUse ***Pseudo\\-Hubber Loss Function***, you need to take care of DELTA, a too big value as 5\\.0 could make the outliers affect your model again, and a too small value as 0\\.1 could make your model very slow to converge to solution."},{"slug":"activation-functions-in-tensorflow","lang":"en","title":"Activation Functions (updated)","description":"beenhereUpdate 09.2022This entry has been updated to TensorFlow v2.10.0 and PyTorch 1.12.1 Table of Contents What is an activation function?Activation FunctionsSigmoidReLU (Rectified Linear Unit)ReLU6…","publishDate":"2017-10-15","updatedDate":null,"tags":["machine-learning","python","tensorflow"],"translationSlug":"funciones-de-activacion-en-tensorflow","url":"https://alexisalulema.com/blog/activation-functions-in-tensorflow/","series":null,"headings":[{"depth":3,"slug":"update-092022","text":"Update 09.2022"},{"depth":3,"slug":"table-of-contents","text":"Table of Contents"},{"depth":3,"slug":"what-is-an-activation-function","text":"What is an activation function?"},{"depth":3,"slug":"activation-functions","text":"Activation Functions"}],"body":"*beenhere*\n\n### Update 09.2022\n\nThis entry has been updated to TensorFlow v2.10.0 and PyTorch 1.12.1\n\n### Table of Contents\n\n- [What is an activation function?](#que-es)\n- [Activation Functions](#funciones)\n  - [Sigmoid](#sigmoid)\n  - [ReLU (Rectified Linear Unit)](#relu)\n  - [ReLU6](#relu6)\n  - [Hyperbolic Tangent](#tanh)\n  - [ELU (Exponential Linear Unit)](#elu)\n  - [Softmax](#softmax)\n  - [Softplus](#softplus)\n  - [Softsign](#softsign)\n  - [Swish](#swish)\n  - [Sinc](#sinc)\n  - [Leaky ReLU](#lrelu)\n  - [Mish](#mish)\n  - [GELU (Gaussian Error Linear Unit)](#gelu)\n  - [SELU (Scaled Exponential Linear Unit)](#selu)\n\n### What is an activation function?\n\nAn activation function is a function that is applied to a neuron in a neural network to help it learn complex patterns of data, deciding what should be transmitted to the next neuron in the network.\n\nA perceptron is a neural network unit that inputs the data to be learned into a neuron and processes it according to its activation function.\n\nThe perceptron is a simple algorithm that, given an input vector $x$ of $m$ values$(x_1, x_2, ..., x_m)$, outputs a 1 or a 0 (step function), and its function is defined as follows:\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/perceptron-equation.png)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/dot-product.png)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-2-1024x539.png)\n\nHere, $ω$ is a vector of weights, $ωx$ is the dot product, and $b$ is the bias. This equation reassembles the equation of the line. If $x$ is on this line, the answer is positive; otherwise, it is negative. However, ideally, we will pass in training data and allow the computer to adjust the weight and bias so that the errors produced by this neuron are minimized. The learning process should recognize small changes that progressively teach our neurons to classify information as we want.\n\nThe following image is a step function where if $x>0$, the output is 1, and if $x<0$, the result is 0. It is useless as an activation function since we do not have \"small changes,\" but significant changes, and the neuron cannot learn in this way since $ω$ and $b$ will not converge to the optimal values to minimize errors.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/perceptron.png)\n\nActivation Function: Step Function\n\nThe tangent of the activation function indicates whether the neuron is learning; From the previous image, we deduce that the tangent at $x=0$ is $\\infty$. This value is unreal because, in real life, we learn everything step by step. For a neuron to learn, we need something that changes progressively from 0 to 1, that is, a **continuous (and differentiable) function**.\n\nWhen we started using neural networks, we used activation functions as an essential part of a neuron. This activation function will allow us to adjust weights and biases.\n\nThe derivative of the activation function feeds the [backpropagation](https://en.wikipedia.org/wiki/Backpropagation) during learning. For this reason, the function and its derivative must have a low computational cost.\n\n### Activation Functions\n\nCurrently, there are several types of activation functions that are used in various scenarios. Below is a short explanation of the activation functions available in the **[tf.keras.activations](https://www.tensorflow.org/api_docs/python/tf/keras/activations)** module from the TensorFlow v2.10.0 distribution and [**torch.nn**](https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity) from PyTorch 1.12.1.\n\n#### Sigmoid\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.sigmoid](https://www.tensorflow.org/api_docs/python/tf/keras/activations/sigmoid)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Sigmoid](https://pytorch.org/docs/stable/generated/torch.nn.Sigmoid.html#torch.nn.Sigmoid)\n\n$$\n\\sigma \\left ( x \\right )=\\frac{1}{1+e^{-x}}\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image.png)\n\nActivation Function: Sigmoid\n\nMathematically, this function is continuous. As we can see, in the sigmoid, the changes are gradual, and we can have values other than 0 or 1.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.sigmoid(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.Sigmoid()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\nThe sigmoid function is the most common activation function; however, it is not used frequently due to the tendency to drive the *[backpropagation](https://en.wikipedia.org/wiki/Backpropagation)* terms to zero during training.\n\n#### ReLU (Rectified Linear Unit)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.relu](https://www.tensorflow.org/api_docs/python/tf/keras/activations/relu)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.ReLU](https://pytorch.org/docs/stable/generated/torch.nn.ReLU.html#torch.nn.ReLU)\n\n$$\nf(x)=max(0,x)\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-3.png)\n\nActivation Function: ReLU\n\nThis function has become very popular as it generates excellent experimental results. The most significant advantage of ReLU is that this function speeds up the **[Stochastic Gradient Descent](https://en.wikipedia.org/wiki/Stochastic_gradient_descent)** (SGD) convergence, which indicates how fast the neuron is learning, compared to the Sigmoid and Hyperbolic Tangent functions.\n\nThis strength is, in turn, its greatest weakness since this \"learning speed\" can cause the weights $\\omega$ of the neuron to be updated and oscillate from the optimal values and not be activated at any point. For example, if the learning rate is too high, half of the neurons may be \"dead,\" but if an appropriate value is set, the neurons will learn, but it will be slower than expected.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.relu(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.ReLU()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### ReLU6\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.nn.relu6](https://www.tensorflow.org/api_docs/python/tf/nn/relu6)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.ReLU6](https://pytorch.org/docs/stable/generated/torch.nn.ReLU6.html#torch.nn.ReLU6)\n\n$$\nf(x)= min(max(0,x), 6)\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-4.png)\n\nActivation Function: ReLU6\n\nThis function has historically remained in the tf.nn module, inherited from TensorFlow 1. It was introduced in the paper \"**[Convolutional Deep Belief Networks on CIFAR-10](http://www.cs.utoronto.ca/~kriz/conv-cifar10-aug2010.pdf)**\" (page 2), with the advantage, compared to the simple ReLU, that it is computationally faster and doesn't suffer from \"exploding\" values. As we can infer from the paper that introduced it, it has been used in Convolutional ([CNN](https://en.wikipedia.org/wiki/Convolutional_neural_network)) and Recurrent ([RNN](https://en.wikipedia.org/wiki/Recurrent_neural_network)) neural networks.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.nn.relu6(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.ReLU6()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Hyperbolic Tangent\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.tanh](https://www.tensorflow.org/api_docs/python/tf/keras/activations/tanh)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Tanh](https://pytorch.org/docs/stable/generated/torch.nn.Tanh.html#torch.nn.Tanh)\n\n$$\nf(x)= tanh(x)\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-5.png)\n\nActivation Function: $tanh(x)$\n\nThis function is similar to the sigmoid, but instead of having a range between 0 and 1, it ranges from -1 to 1. Sadly, it has the same vanishing problem as the sigmoid.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.tanh(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.Tanh()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### ELU (Exponential Linear Unit)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.elu](https://www.tensorflow.org/api_docs/python/tf/keras/activations/elu)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.ELU](https://pytorch.org/docs/stable/generated/torch.nn.ELU.html#torch.nn.ELU)\n\nGiven a value of $\\alpha>0$\n\n$$\nf(x)=x\\;\\;if\\;\\;x\\geq 0\n$$\n\n$$\nf(x)=\\alpha (e^{x}-1)\\;\\;if\\;\\;x<0\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-6.png)\n\nActivation Function: ELU, con $\\alpha = 1$\n\nAccording to the paper \"[Fast and Accurate Deep Network Learning by Exponential Linear Units (ELUs)](https://arxiv.org/pdf/1511.07289.pdf)\", this function tends to converge to zero faster and produces more accurate results. Its behavior is similar to ReLU, but for negative values where it exponentially tends to $-\\alpha$.\n\nIt's a commonly used function on CNN, as their research paper indicates.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.elu(X, alpha=1.0)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.ELU()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Softmax\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.softmax](https://www.tensorflow.org/api_docs/python/tf/keras/activations/softmax)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Softmax](https://pytorch.org/docs/stable/generated/torch.nn.Softmax.html#torch.nn.Softmax)\n\nGiven a vector of values $x$, and a number of classes $K$ of a multi-class classifier:\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-12.png)\n\nSoftmax converts a vector of values to a probability distribution of sum 1; as we can see, its use differs from the rest of the activation functions in that this function is used as the final layer in classifiers based on neural networks.\n\nThis function is used in classifiers and reinforcement learning to convert values into performance probabilities. As Wikipedia (ES) indicates:\n\n> *It correlates the expected reward for performing a specific action with the probability of choosing it. This way, the actions with the highest expected reward are selected with the most significant probability.*\n>\n> Wikipedia: [Función Softmax](https://es.wikipedia.org/wiki/Función_SoftMax#Aprendizaje_por_refuerzo)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\n\ninputs = tf.random.normal(shape=(32, 10))\noutputs = tf.keras.activations.softmax(inputs)\ntf.reduce_sum(outputs[0, :])  # Each sample in the batch now sums to 1\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\n\ninputs = torch.randn(32, 10)\noutputs = torch.nn.Softmax(dim=1)(inputs)\ntorch.sum(outputs[0, :])  # Each sample in the batch now sums to 1\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Softplus\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.softplus](http://tf.keras.activations.softplus)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Softplus](https://pytorch.org/docs/stable/generated/torch.nn.Softplus.html#torch.nn.Softplus)\n\n$$\nf(x)=ln(1+e^{x})\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-10.png)\n\nActivation Function: Softplus\n\nSoftplus is an alternative to traditional functions, such as Sigmoid or Hyperbolic Tangent, because it is differentiable (essential for the backpropagation function). The amazing thing with the derivative of this function is that it is the Sigmoid function!\n\n$$\n\\frac{\\partial y}{\\partial x}=\\left ( ln(1+e^x) \\right )'=\\left ( \\frac{1}{1+e^x} \\right )\\left ( 1+e^x \\right )'\n$$\n\n$$\n\\frac{\\partial y}{\\partial x}=\\left ( \\frac{1}{1+e^x} \\right )\\cdot e^x=\\frac{e^x}{1+e^x}=\\frac{e^{x}}{e^{x}\\left ( e^{-x}+1 \\right )}\n$$\n\n$$\n\\therefore\\;\\; \\frac{\\partial y}{\\partial x}=\\frac{1}{1+e^{-x}}\n$$\n\nInterestingly, it allows us to understand more clearly how the backpropagation process will behave.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.softplus(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.Softplus()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Softsign\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.softsign](https://www.tensorflow.org/api_docs/python/tf/keras/activations/softsign)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Softsign](https://pytorch.org/docs/stable/generated/torch.nn.Softsign.html#torch.nn.Softsign)\n\n$$\nf(x)=\\frac{x}{\\left ( 1+\\left | x \\right | \\right )}\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-11.png)\n\nActivation Function: Softsign\n\nGiven the shape of the graph, we can see that it is an alternative to Hyperbolic Tangent, but it has not been as widely adopted. However, considering that the activation function is state-of-the-art, it could be the function that best fits specific neural network models.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.softsign(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.Softsign()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Swish\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.swish](https://www.tensorflow.org/api_docs/python/tf/keras/activations/swish)\n\n$$\nf(x)=\\frac{x}{1+e^{-x}}\n$$\n\n$$\nf(x)=x\\cdot Sigmoid(x)\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-8.png)\n\nActivation Function: Swish\n\nThis activation function is relatively new (2017) and outperforms ReLU for deeper CNN networks. The equation that defines this function describes a $Sigmoid(x)$, but it doesn't have the gradient vanishing problem. The $x$ factor makes this function important again.\n\nRegarding its comparison with ReLU, ReLU had the problem of outputting 0 for negative values added to backpropagation; Swish partially handles this problem. However, the computational cost of Swish is higher. It is an issue you should evaluate for your decision since this cost carried over to the backpropagation process will increase.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.swish(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\n# It's interesting that PyTorch doesn't have a built-in swish function\nX = torch.linspace(-7., 7., steps=100)\ny = X * torch.nn.Sigmoid()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Sinc\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.experimental.numpy.sinc](https://www.tensorflow.org/api_docs/python/tf/experimental/numpy/sinc)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.special.sinc](https://pytorch.org/docs/stable/special.html#torch.special.sinc)\n\n$$\nf(x)=\\frac{sin(x)}{x}\\;\\;if\\;\\;x\\neq 0\n$$\n\n$$\nf(x)=1\\;\\;if\\;\\;x=0\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-9.png)\n\nActivation Function: Sinc\n\n$Sinc$ is an unusual activation function since it rises and falls in contrast to the other functions. However, the function saturates, and its output converges to zero for large positive and negative input values.\n\nThe function has an exception at $x=0$ since evaluating that value at $f(x)=\\frac{sin(x)}{x}$ would result in a divide-by-0 indetermination.\n\nPeriodic functions like $Sinc$ are not popular in neural networks as activation functions. But even when **sinc** is periodic, it will saturate when the input value increases positively or negatively, just like other functions such as the Sigmoid or the Hyperbolic Tangent, which makes this function an interesting alternative as an activation function in neural networks.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.experimental.numpy.sinc(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.special.sinc(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Leaky ReLU\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.layers.LeakyReLU](https://www.tensorflow.org/api_docs/python/tf/keras/layers/LeakyReLU)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.LeakyReLU](https://pytorch.org/docs/stable/generated/torch.nn.LeakyReLU.html#torch.nn.LeakyReLU)\n\nGiven a value of $\\alpha>0$\n\n$$\nf(x)=x\\;\\;if\\;\\;x\\geq 0\n$$\n\n$$\nf(x)=\\alpha x\\;\\;if\\;\\;x<0\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-7.png)\n\nActivation Function: Leaky ReLU, con $\\alpha = 0.3$\n\nReLU is a very popular activation function in CNN since for positive values; it does not saturate and stop learning; however, a weakness of the ReLU is that for negative values, it tends to saturate, and Leaky ReLU (LReLU) corrects this problem.\n\nTensorFlow sets a default value for the constant $\\alpha=0.3$, although some sources mention a value of 0.01. Roughly speaking, $\\alpha$ sets the slope of the equation for negative values, and LReLU improves the convergence performance.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\nleakyReLU = tf.keras.layers.LeakyReLU(alpha=0.3)\ny = leakyReLU(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.LeakyReLU(0.3)(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### Mish\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tfa.activations.mish](https://www.tensorflow.org/addons/api_docs/python/tfa/activations/mish)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.Mish](https://pytorch.org/docs/stable/generated/torch.nn.Mish.html#torch.nn.Mish)\n\n$$\nf(x)=x\\cdot tanh(softplus(x))\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-13.png)\n\nActivation Function: Mish\n\nDue to its form, this function is very similar to Swish, defined as \"[a self-regularized non-monotonic activation function,](https://arxiv.org/pdf/1908.08681v3.pdf)\" according to its research paper (2020). The innovative part of Mish is given by the intuitive understanding of the behavior of the derivative to understand how it helps to regularize the optimization of neural networks.\n\n$$\n\\frac{\\partial y}{\\partial x}=(x\\cdot tanh(softplus(x)))'=(x\\cdot tanh(1+e^{x}))'\n$$\n\n$$\n\\frac{\\partial y}{\\partial x}=\\frac{e^{x}\\omega }{\\delta ^{2}}\n$$\n\nWhere:\n\n$$\n\\omega =4(x+1)+4e^{2x}+e^{3x}+e^{x}(4x+6)\n$$\n\n$$\n\\delta =2e^{x}+e^{2x}+2\n$$\n\nThese functions are easier to graph and better understand how the backpropagation process will behave.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\n! pip install tensorflow-addons\nimport tensorflow as tf\nimport tensorflow_addons as tfa\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tfa.activations.mish(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.Mish()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### GELU (Gaussian Error Linear Unit)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.gelu](https://www.tensorflow.org/api_docs/python/tf/keras/activations/gelu)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.GELU](https://pytorch.org/docs/stable/generated/torch.nn.GELU.html#torch.nn.GELU)\n\n$$\nf(x)=xP(X\\leq x)=x\\;\\Phi (x)=x\\cdot \\frac{1}{2}\\left [ 1+erf(\\frac{x}{\\sqrt{2}}) \\right ]\n$$\n\n$$\nf(x)\\approx 0.5x\\left ( 1+tanh\\left [ \\frac{\\sqrt{2}}{\\pi}\\left ( x+0.044715x^{3} \\right ) \\right ] \\right )\n$$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-14.png)\n\nActivation Function: GELU\n\nThis function aims to merge the functionality of a Linear Unit (ReLU, ELU) with the [Dropout](https://en.wikipedia.org/wiki/Dilution_(neural_networks)) regularization function through a stochastic multiplication of the input vector by 0 or by 1 and obtain the activation function in a deterministic way.\n\nGELU, as determined in its [research paper](https://arxiv.org/pdf/1606.08415.pdf), is determined by a Gaussian distribution function which eliminates the negative vanishing problem that ReLU has, improving its performance for tasks such as computer vision, NLP, and speech processing.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.gelu(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.GELU()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)\n\n#### SELU (Scaled Exponential Linear Unit)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) [tf.keras.activations.selu](https://www.tensorflow.org/api_docs/python/tf/keras/activations/selu)\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) [torch.nn.SELU](https://pytorch.org/docs/stable/generated/torch.nn.SELU.html#torch.nn.SELU)\n\n$$\nf(x)=\\lambda x\\;\\;if\\;\\;x\\geq 0\n$$\n\n$$\nf(x)=\\lambda \\alpha (e^{x}-1)\\;\\;if\\;\\;x<0\n$$\n\nDonde $\\alpha \\approx 1.6733$ y $\\lambda \\approx 1.0507$\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/image-15.png)\n\nActivation Function: SELU\n\nSELU is an activation function that induces self-normalizing properties that converges to zero mean and unit variance. Unlike ReLU, it can reach values below 0, which allows the neural network to have an output of mean 0, and this feature helps the model to converge more quickly towards the solution.\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/tf_logo_32px.png) Example:\n\n```\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nX = tf.linspace(-7., 7., 100)\ny = tf.keras.activations.selu(X)\nplt.plot(X,y)\nplt.grid()\n```\n\n![](https://images.alexisalulema.com/blog/activation-functions-in-tensorflow/torch_logo_32px.png) Example:\n\n```\nimport torch\nimport matplotlib.pyplot as plt\n\nX = torch.linspace(-7., 7., steps=100)\ny = torch.nn.SELU()(X)\nplt.plot(X,y)\nplt.grid()\n```\n\nSee it at Google Colab here: [Activation Functions](https://colab.research.google.com/drive/1yeK1HgyNGc66ZpznUJD-4PLG98WNF_QA?usp=sharing)"},{"slug":"working-with-matrices-in-tensorflow","lang":"en","title":"Working with Matrices in TensorFlow","description":"Matrices are the basic elements we use to interchange data through computational graphs. In general terms, a tensor can de defined as a matrix, so you can refer to Declaring tensors in TensorFlow in o…","publishDate":"2017-10-12","updatedDate":null,"tags":["machine-learning","python","tensorflow"],"translationSlug":null,"url":"https://alexisalulema.com/blog/working-with-matrices-in-tensorflow/","series":null,"headings":[{"depth":3,"slug":"matrix-operations","text":"Matrix Operations"},{"depth":3,"slug":"other-operations","text":"Other operations"},{"depth":3,"slug":"element-wise-operations","text":"Element-wise Operations"},{"depth":3,"slug":"cross-product","text":"Cross-product"}],"body":"Matrices are the basic elements we use to interchange data through computational graphs. In general terms, a tensor can de defined as a matrix, so you can refer to Declaring tensors in TensorFlow in order to see the options you have to create matrices.\n\nLet’s define the matrices we are going to use in the examples:\n\n```\nimport tensorflow as tf\nimport numpy as np\n\nsess = tf.Session()\n\nidentity_matrix = tf.diag([1., 1., 1., 1., 1.])\nmat_A = tf.truncated_normal([5, 2], dtype=tf.float32)\nmat_B = tf.constant([[1., 2.], [3., 4.], [5., 6.], [7., 8.], [9., 10.]])\nmat_C = tf.random_normal([5, ], mean=0, stddev=1.0)\nmat_D = tf.convert_to_tensor(np.array([[1.2, 2.3, 3.4], [4.5, 5.6, 6.7], [7.8, 8.9, 9.10]]))\n```\n\n### Matrix Operations\n\nAddition and substraction are simple operations that can be performed by ‘\\+’ and ‘\\-‘ operators, or by `tf.add()` or `tf.subtract()`.\n\n```\n# A + B\n>>> print(sess.run(mat_A + mat_B))\n>>> print(sess.run(tf.add(mat_A, mat_B)))\n [[ 0.58516705 2.84226775]\n [ 2.3062849 4.91305351]\n [ 5.88148737 4.88284636]\n [ 6.40551376 6.56219101]\n [ 9.73429203 9.89524364]]\n```\n\n```\n# B - B\n>>> print(sess.run(mat_B - mat_B))\n>>> print(sess.run(tf.subtract(mat_B, mat_B)))\n [[ 0. 0.]\n [ 0. 0.]\n [ 0. 0.]\n [ 0. 0.]\n [ 0. 0.]]\n```\n\nMatrices multiplication must follow the following rule:\n\n![](https://images.alexisalulema.com/blog/working-with-matrices-in-tensorflow/matmult-1024x348.png)\n\nIf this rule is accomplished, then we can perform multiplication.\n\n`tf.matmul()` performs this operation; as an option, previously we can transpose or adjointe (conjugate and transpose), and optionally we can mark any matrix as sparsed. For example:\n\n```\n# B * Identity\n>>> print(sess.run(tf.matmul(mat_B, identity_matrix, transpose_a=True, transpose_b=False)))\n [[ 1. 3. 5. 7. 9.]\n [ 2. 4. 6. 8. 10.]]\n```\n\n### Other operations\n\n```\n# Transposed C\n>>> print(sess.run(tf.transpose(mat_C)))\n [ 0.62711298 1.33686149 0.5819205 -0.85320765 0.59543872]\n```\n\n```\n# Matrix Determinant D\n>>> print(sess.run(tf.matrix_determinant(mat_D)))\n 3.267\n```\n\n```\n# Matrix Inverse D\n>>> print(sess.run(tf.matrix_inverse(mat_D)))\n [[-2.65381084 2.85583104 -1.11111111]\n [ 3.46189164 -4.77502296 2.22222222]\n [-1.11111111 2.22222222 -1.11111111]]\n```\n\n```\n# Cholesky decomposition\n>>> print(sess.run(tf.cholesky(identity_matrix)))\n [[ 1. 0. 0. 0. 0.]\n [ 0. 1. 0. 0. 0.]\n [ 0. 0. 1. 0. 0.]\n [ 0. 0. 0. 1. 0.]\n [ 0. 0. 0. 0. 1.]]\n```\n\n```\n# Eigen decomposition\n>>> print(sess.run(tf.self_adjoint_eig(mat_D)))\n (array([ -3.77338787, -0.85092622, 20.52431408]), array([[-0.76408782, -0.4903048 , 0.41925053],\n [-0.21176465, 0.8045062 , 0.55491037],\n [ 0.60936487, -0.33521781, 0.71854261]]))\n```\n\n### Element\\-wise Operations\n\n```\n# A * B (Element-wise)\n>>> print(sess.run(tf.multiply(mat_A, mat_B)))\n```\n\n```\n# A % B (Element-wise)\n>>> print(sess.run(tf.div([2, 2], [5, 4])))\n [0 0]\n```\n\n```\n# A / B (Element-wise)\n>>> print(sess.run(tf.truediv([2, 2], [5, 4])))\n [ 0.4 0.5]\n```\n\n```\n# A / B Floor-approximation (Element-wise)\n>>> print(sess.run(tf.floordiv([8, 8], [5, 4])))\n [1 2]\n```\n\n```\n# A/B Remainder (Element-wise)\n>>> print(sess.run(tf.mod([8, 8], [5, 4])))\n [3 0]\n```\n\n### Cross\\-product\n\n```\n>>> print(sess.run(tf.cross([1, -1, 2], [5, 1, 3])))\n array([-5, 7, 6], dtype=int32)\n```\n\nWe’ve completed all theoretical prerequisites for TensorFlow. Once we understand matrices, variables and placeholders, we can continue with Core TensorFlow. See you next time!"},{"slug":"understanding-variables-and-placeholders-in-tensorflow","lang":"en","title":"Understanding Variables and Placeholders in TensorFlow","description":"Usually, when we start using TensorFlow, it’s very common to think that defining variables is just as trivial as a HelloWorld program, but understanding how variables (and placeholders) work under the…","publishDate":"2017-10-11","updatedDate":null,"tags":["machine-learning","python","tensorflow"],"translationSlug":null,"url":"https://alexisalulema.com/blog/understanding-variables-and-placeholders-in-tensorflow/","series":null,"headings":[],"body":"Usually, when we start using TensorFlow, it’s very common to think that defining variables is just as trivial as a HelloWorld program, but understanding how variables (and placeholders) work under the hood is very important to understand more complex concepts because those concepts heavily use variables/placeholders; and, if we don’t understand the information flow between variables, it could be harder to have a clear idea of the implemented algorithms in TensorFlow.\n\n**Variables** are the parameters of the algorithm. The main way to create a variable is by using the `Variable()` function, although, we still need to initialize it. Initializing is what puts the variable with the corresponding methods on the computational graph.\n\n```\nseq = tf.linspace(0., 7, 8)\nseq_var = tf.Variable(seq)\n\n# Initialize variables in session\nsess = tf.Session()\ninitialize_op = tf.global_variables_initializer()\nsess.run(initialize_op)\n```\n\nWhile each variable has an `initializer()` method, the most common way to do this is to use the function `global_variables_initializer()`. This function creates an operation in the graph that initializes all variables. Nevertheless, we can initialize variables depending on the results of initializing another variable, as follows:\n\n```\nsess = tf.Session()\nfirst_var = tf.Variable(tf.lin_space(0., 7, 8), name='1st_var')\nsess.run(first_var.initializer)\n# first_var: <tf.Variable '1st_var:0' shape=(8,), dtype=float32_ref>\n\n# second_var dimensions depends on first_var\nsecond_var = tf.Variable(tf.zeros_like(first_var), name='2nd_var')\nsess.run(second_var.initializer)\n# second_var: <tf.Variable '2nd_var:0' shape=(8,), dtype=float32_ref>\n```\n\n**Placeholders** are just holding the position for data to be fed into the graph. To put a placeholder in the graph, we must perform at least one operation on the placeholder.\n\n```\nsess = tf.Session()\nx = tf.placeholder(tf.float32, shape=[2, 2])\n# y is the operation to run on x placeholder\ny = tf.identity(x)\n\n# x_vals is data to feed into the x placeholder\nx_vals = np.random.rand(2, 2)\n# Runs y operation\nsess.run(y, feed_dict={x: x_vals})\n```\n\nTensorFlow will not return a self\\-referenced placeholder in the feed dictionary.\n\nWith these concepts clear, we can move forward with TensorFlow. See you next time with more TF!"},{"slug":"declaring-tensors-in-tensorflow","lang":"en","title":"Declaring tensors in TensorFlow","description":"[Requirement: Tensorflow and NumPy installed on Python +3.5][Requirement: import tensorflow as tf][Requirement: import numpy as np] Tensors are the primary data structure we use in TensorFlow, and, as…","publishDate":"2017-10-10","updatedDate":null,"tags":["machine-learning","python","tensorflow"],"translationSlug":null,"url":"https://alexisalulema.com/blog/declaring-tensors-in-tensorflow/","series":null,"headings":[{"depth":3,"slug":"1-fixed-tensors","text":"1. Fixed tensors"},{"depth":3,"slug":"2-copying-dimensions","text":"2. Copying dimensions"},{"depth":3,"slug":"3-sequence-tensors","text":"3. Sequence tensors"},{"depth":3,"slug":"4-random-tensors","text":"4. Random tensors"},{"depth":3,"slug":"5-converted-from-numpy","text":"5. Converted from NumPy"}],"body":"Tensors are the primary data structure we use in TensorFlow, and, as Wikipedia describes them, “tensors are geometric objects that describe linear relations between geometric vectors, scalars and other tensors”. Tensors can be described as multidimensional arrays, embracing the concepts of scalar, vector and matrix, without taking in consideration the coordinate system\n\nThe tensor order is the number of indexes we need to specify one element; so, an scalar will be an order 0 tensor, a vector will be an order 1 tensor, a matriz an order 2 tensor, and so on.\n\n![](https://images.alexisalulema.com/blog/declaring-tensors-in-tensorflow/order-3-tensor.png)\n\n***Order\\-3 tensor***\n\nNow we know what a tensor is, I’m going to show you how we can declare tensors in TensorFlow.\n\n### 1\\. Fixed tensors\n\n```\n>>> zeros_tsr = tf.zeros([5, 5], dtype=tf.int32, name='zeros5x5')\n>>> print(zeros_tsr)\nTensor(\"zeros5x5:0\", shape=(5, 5), dtype=int32)\n>>> tf.InteractiveSession().run(zeros_tsr)\narray([[0, 0, 0, 0, 0],\n       [0, 0, 0, 0, 0],\n       [0, 0, 0, 0, 0],\n       [0, 0, 0, 0, 0],\n       [0, 0, 0, 0, 0]])\n```\n\n```\n>>> ones_tsr = tf.ones([5, 5], dtype=tf.float32, name='ones5x5')\n>>> print(ones_tsr)\nTensor(\"ones5x5:0\", shape=(5, 5), dtype=float32)\n>>> tf.InteractiveSession().run(ones_tsr)\narray([[ 1., 1., 1., 1., 1.],\n       [ 1., 1., 1., 1., 1.],\n       [ 1., 1., 1., 1., 1.],\n       [ 1., 1., 1., 1., 1.],\n       [ 1., 1., 1., 1., 1.]], dtype=float32)\n```\n\n```\n>>> filled_tsr = tf.fill([5, 5], 123, name='filled123')\n>>> print(filled_tsr)\nTensor(\"filled123:0\", shape=(5, 5), dtype=int32)\n>>> tf.InteractiveSession().run(filled_tsr)\narray([[123, 123, 123, 123, 123],\n      [123, 123, 123, 123, 123],\n      [123, 123, 123, 123, 123],\n      [123, 123, 123, 123, 123],\n      [123, 123, 123, 123, 123]])\n```\n\n```\n>>> filled2_tsr = tf.constant(123, shape=[5, 5], name='filled123_2', dtype=tf.int16)\n>>> print(filled2_tsr)\nTensor(\"filled123_2:0\", shape=(5, 5), dtype=int16)\n>>> tf.InteractiveSession().run(filled2_tsr)\narray([[123, 123, 123, 123, 123],\n       [123, 123, 123, 123, 123],\n       [123, 123, 123, 123, 123],\n       [123, 123, 123, 123, 123],\n       [123, 123, 123, 123, 123]], dtype=int16)\n```\n\n```\n>>> constant_tsr = tf.constant([1, 2, 3], name='vector')\n>>> print(constant_tsr)\nTensor(\"vector:0\", shape=(3,), dtype=int32)\n>>> tf.InteractiveSession().run(constant_tsr)\narray([1, 2, 3])\n```\n\n### 2\\. Copying dimensions\n\nIt is necessary to previously define tensors from which we are going to copy dimensions.\n\n```\n>>> zeros_similar = tf.zeros_like(constant_tsr)\n>>> print(zeros_similar)\nTensor(\"zeros_like:0\", shape=(3,), dtype=int32)\n>>> tf.InteractiveSession().run(zeros_similar)\narray([0, 0, 0])\n```\n\n```\n>>> ones_similar = tf.ones_like(constant_tsr)\n>>> print(ones_similar)\nTensor(\"ones_like:0\", shape=(3,), dtype=int32)\n>>> tf.InteractiveSession().run(ones_similar)\narray([1, 1, 1])\n```\n\n### 3\\. Sequence tensors\n\n```\n# This tensor defines 7 regular intervals between 0 and 2, 1st param should be float32/64\n>>> linear_tsr = tf.linspace(0., 2, 7)\n>>> print(linear_tsr)\nTensor(\"LinSpace_5:0\", shape=(7,), dtype=float32)\n>>> tf.InteractiveSession().run(linear_tsr)\narray([ 0. , 0.33333334, 0.66666669, 1. , 1.33333337,\n         1.66666675, 2. ], dtype=float32)\n```\n\n```\n# This tensor defines 4 elements between 6 and 17, with a delta of 3\n>>> int_seq_tsr = tf.range(start=6, limit=17, delta=3)\n>>> print(int_seq_tsr)\nTensor(\"range_1:0\", shape=(4,), dtype=int32)\n>>> tf.InteractiveSession().run(int_seq_tsr)\narray([ 6, 9, 12, 15])\n```\n\n### 4\\. Random tensors\n\n```\n# Random numbers from uniform distribution\n>>> rand_unif_tsr = tf.random_uniform([5, 5], minval=0, maxval=1)\n>>> print(rand_unif_tsr)\nTensor(\"random_uniform:0\", shape=(5, 5), dtype=float32)\n>>> tf.InteractiveSession().run(rand_unif_tsr)\narray([[ 0.81911492, 0.01300693, 0.47359812, 0.50176537, 0.27962267],\n       [ 0.47069478, 0.7151444 , 0.56615186, 0.5431906 , 0.45684898],\n       [ 0.00939894, 0.19539773, 0.37774849, 0.08342052, 0.87758613],\n       [ 0.46707201, 0.32422674, 0.90311491, 0.42251813, 0.3496896 ],\n       [ 0.75080729, 0.48055971, 0.49421525, 0.77542639, 0.99400854]], dtype=float32)\n```\n\n```\n# Random numbers from normal distribution\n>>> rand_normal_tsr = tf.random_normal([5, 5], mean=0.0, stddev=1.0)\n>>> print(rand_normal_tsr)\nTensor(\"random_normal:0\", shape=(5, 5), dtype=float32)\n>>> tf.InteractiveSession().run(rand_normal_tsr)\narray([[ 2.13312769, 2.46189046, -0.34942248, -0.39776739, 1.79048693],\n       [ 0.22045165, 0.05164593, -1.05943978, -0.32593197, -1.66411078],\n       [-0.94263768, 1.77081263, -0.22290479, -0.24516548, 1.26560402],\n       [-1.14855564, -0.89211422, 1.10751343, -2.17768288, -1.07004178],\n       [ 0.635813 , 0.24745767, 0.80117846, -0.25315794, -1.88987064]], dtype=float32)\n```\n\n```\n# Random numbers from normal distribution, limitating values within 2 SD from mean\n>>> trunc_norm_tsr = tf.truncated_normal([5, 5], mean=0.0, stddev=1.0)\n>>> print(trunc_norm_tsr)\nTensor(\"truncated_normal:0\", shape=(5, 5), dtype=float32)\n>>> tf.InteractiveSession().run(trunc_norm_tsr)\narray([[-1.07923198, 0.66122717, -0.98569149, -0.11161296, -1.39560068],\n       [-1.04248953, 1.83589756, 0.00709002, -0.70119679, 0.81637812],\n       [-1.14046562, 0.65371871, 0.25081205, 1.59802651, -0.17030434],\n       [ 0.61106592, -0.39884251, -0.02136615, 0.36585283, -1.45338166],\n       [-0.64861351, 0.930076 , -0.1549242 , 1.45601475, 0.56357914]], dtype=float32)\n```\n\n```\n# Shuffles existing tensor\n>>> seq = tf.linspace(0., 7, 8)\n>>> tf.InteractiveSession().run(seq)\narray([ 0., 1., 2., 3., 4., 5., 6., 7.], dtype=float32)\n>>> tf.InteractiveSession().run(tf.random_shuffle(seq))\narray([ 5., 0., 4., 1., 6., 7., 2., 3.], dtype=float32)\n```\n\n```\n# Randomicaly crops existing tensor to specified dimension\n>>> tf.InteractiveSession().run(tf.random_crop(seq, [3,]))\narray([ 2., 3., 4.], dtype=float32)\n```\n\n### 5\\. Converted from NumPy\n\n```\n>>> np_array = np.array([[1, 2], [3, 4]])\n>>> np_tsr = tf.convert_to_tensor(np_array, dtype=tf.int32)\n>>> print(np_tsr)\nTensor(\"Const:0\", shape=(2, 2), dtype=int32)\n>>> tf.InteractiveSession().run(np_tsr)\narray([[1, 2],\n       [3, 4]])\n```\n\nOnce you have created your desired tensor, you should wrap it as a TensorFlow Variable, like this:\n\n```\nseq_var = tf.Variable(seq)\n```\n\nThat’s all for today. See you next time with more TensorFlow!"},{"slug":"java-streams-api-in-brief","lang":"en","title":"Java Streams API in brief","description":"First, let’s define what a stream is in Java 8: a sequence of functions, actions, inputs, and outputs (better defined as a “pipeline”). Streams API provides functional-style operations to transform th…","publishDate":"2017-07-20","updatedDate":null,"tags":["data-engineering","java"],"translationSlug":null,"url":"https://alexisalulema.com/blog/java-streams-api-in-brief/","series":null,"headings":[{"depth":3,"slug":"1-filtering","text":"1. Filtering."},{"depth":3,"slug":"2-mapping","text":"2. Mapping"},{"depth":3,"slug":"3-collecting","text":"3. Collecting"},{"depth":3,"slug":"4-parallelizing","text":"4. Parallelizing"},{"depth":3,"slug":"5-io","text":"5. I/O"},{"depth":3,"slug":"conclusion","text":"Conclusion"}],"body":"First, let’s define what a stream is in Java 8: a sequence of functions, actions, inputs, and outputs (better defined as a “pipeline”). Streams API provides functional\\-style operations to transform these sequences; sources for them can contain arrays, collections, files, etc. In general terms, streams are [Monads](https://en.wikipedia.org/wiki/Monad_(functional_programming)):\n\n*“Monads represent computations to be executed in a sequential (or parallel, in Java Streams) structure.”*\n\nStreams API is a great starting point for data preparation, and, if we compare it with its Python equivalents, Java provides us a very strong alternative for Data Science, as follows:\n\n- As a typed\\-language, the compiler can detect error and bugs.\n- Java bytecode is faster than scripting languages as R or Python\n- Versions of libraries in projects can be easily maintained with Maven or Gradle\n- Common big data frameworks such as Apache Hadoop or Spark are written in Java or JVM languages.\n- Creating models in Java makes easier to integrate them to production systems, which usually are written in Java or similar languages.\n\nLet’s start some examples defining a simple Car class:\n\n```\nclass Car {\n     private final String name;\n     private final Country origin;\n \n     Car(String name, Country origin) {\n         this.name = name;\n         this.origin = origin;\n     }\n \n     public String getName() { return name; }\n     public Country getOrigin() { return origin; }\n }\n \n final class Country {\n     private final int value;\n     private final String name;\n \n     public static Country GERMANY = new Country(1, \"GERMANY\");\n     public static Country US = new Country(2, \"US\");\n     public static Country UK = new Country(3, \"UK\");\n     public static Country INDIA = new Country(4, \"INDIA\");\n     public static Country JAPAN = new Country(5, \"JAPAN\");\n \n     private Country(int value, String name) {\n         this.value = value;\n         this.name = name;\n     }\n \n     public int getValue() { return value; }\n     public String getName() { return name; }\n }\n```\n\nStreams API usually works with collections, so a useful method to convert an array into a collection is ***Arrays.stream***:\n\n```\nCar[] cars = {\n        new Car(\"GM\", Country.US),\n        new Car(\"Cadillac\", Country.US),\n        new Car(\"BMW\", Country.GERMANY),\n        new Car(\"Mercedes Benz\", Country.GERMANY),\n        new Car(\"Toyota\", Country.JAPAN),\n        new Car(\"Mazda\", Country.JAPAN),\n        new Car(\"Honda\", Country.JAPAN),\n        new Car(\"Mahindra\", Country.INDIA),\n        new Car(\"Land Rover\", Country.UK)\n};\n\nList<Car> list = Arrays.asList(cars);\nStream<Car> stream = list.stream();\n```\n\n***Streams are not reusable***, so they have to be recreated in order to start a new processing pipeline, because of this I’m going to be using ***list.stream()*** in all the examples as follows:\n\n```\nList<String> germanCars = list.stream()\n        .filter(x -> x.getOrigin().equals(Country.GERMANY))\n        .map(Car::getName)\n        .collect(Collectors.toList());\n```\n\nThis piece of code shows some useful Stream functions:\n\n### 1\\. Filtering.\n\nIt directly works on the stream, and receives a lambda function to evaluate the filter, it returns a filtered Stream\n\n### 2\\. Mapping\n\nIt provides a useful way of selecting a specific data member of Car class, and “maps” the stream of Cars into a stream of Strings.\n\n### 3\\. Collecting\n\nIt performs a data transformation into lists, sets, strings, etc, using Collectors class, which provides useful transformation methods, just like Collectors.toList()\n\n```\n// Three more examples:\nString rawSentence = list.stream()\n        .map(Car::getName)\n        .collect(Collectors.joining(\",\"));\n```\n\n```\nSet<String> countries = list.stream()\n        .map(Car::getOrigin)\n        .map(Country::getName)\n        .collect(Collectors.toSet());\n```\n\n```\nMap<Country, List<Car>> groupByCountry = list.stream()\n        .collect(Collectors.groupingBy(Car::getOrigin));\nSystem.out.println(groupByCountry.get(Country.JAPAN).stream()\n        .map(Car::getName)\n        .collect(Collectors.toList()));\n\n```\n\nIn this last two examples, we can see that we can operate on a stream over and over again, and it creates the “pipelines” we mentioned before.\n\nThere is a useful ***toMap()*** collector that can index a collection using some fields. For example, if we want to get a map from Car names to Car objects, it can be achieved using the following code:\n\n```\nMap<String, Car> tokenToWord = list.stream()\n        .collect(Collectors.toMap(Car::getName, Function.identity()));\n```\n\nStreams API provides streams of primitives (ints, doubles, and others), they have basic statistical methods such as ***sum***, ***max***, ***min***, ***average***, or ***summaryStatistics***. For example:\n\n```\nint maxNameLength = list.stream()\n        .mapToInt(x -> x.getName().length())\n        .max().getAsInt();\n```\n\n#### *Custom Collectors*\n\nWe can also define our own collector by using ***Collector***class which requires to pass a supplir class, accumulator, combiner and finisher method, for example:\n\n```\nCollector<Car, StringJoiner, String> carsCollector =\n     Collector.of(\n         () -> new StringJoiner(\", \"),\n         (joiner, car) -> joiner.add(car.getName().toUpperCase()),\n         StringJoiner::merge,\n         StringJoiner::toString);\n \n String names = list.stream().collect(carsCollector);\n```\n\n### 4\\. Parallelizing\n\nStreams can be execute in parallel taking advantage of the available physical CPU cores. Streams use a common ForkJoinPool, we can check the size of the underlying thread\\-pool by using ForkJoinPool.commonPool (7 threads in my PC). For example:\n\n```\nForkJoinPool commonPool = ForkJoinPool.commonPool();\n System.out.println(\"Threads: \" + commonPool.getParallelism());\n int[] firstLengths = list.parallelStream()\n         .filter(w -> w.getOrigin().equals(Country.JAPAN))\n         .map(Car::getName)\n         .mapToInt(String::length)\n         .sequential()\n         .sorted()\n         .limit(2)\n         .toArray();\n System.out.println(Arrays.toString(firstLengths));\n```\n\n### 5\\. I/O\n\nFinally, we can use Java I/O library, for example, using files to be represented as a stream of lines using ***Reader.lines()*** method.\n\n```\nInputStream fStream = Main.class.getResourceAsStream(\"text.txt\");\n InputStreamReader fReader = new InputStreamReader(fStream, StandardCharsets.UTF_8);\n \n try (Stream<String> lines = new BufferedReader(fReader).lines()) {\n     double average = lines\n             .flatMap(line -> Arrays.stream(line.split(\" \")))\n             .map(String::toLowerCase)\n             .mapToInt(String::length)\n             .average().getAsDouble();\n     System.out.println(\"average token length: \" + average);\n }\n```\n\n### Conclusion\n\nStreams are a very powerful feature introduced in Java 8 (and to be extended soon in Java 9\\), very useful to process data for Data Science in Java. There are much more useful methods in Streams, which  More examples in the future.\n\nGo to GitHub Repo: <https://github.com/alulema/JavaDS.01>"},{"slug":"is-it-worth-it-to-work-as-a-freelancer","lang":"en","title":"Does it worth to work as a freelancer?","description":"BACKGROUND Usually many of us graduates from college have dreamed of working in a large company with great salary, prestige and traveling around the world; or maybe we’ve had the “million-dollar idea”…","publishDate":"2017-07-04","updatedDate":null,"tags":["lifestyle"],"translationSlug":null,"url":"https://alexisalulema.com/blog/is-it-worth-it-to-work-as-a-freelancer/","series":null,"headings":[{"depth":2,"slug":"background","text":"BACKGROUND"},{"depth":2,"slug":"my-experience","text":"MY EXPERIENCE"},{"depth":3,"slug":"1-enjoy-the-hours-of-the-day","text":"1. Enjoy the hours of the day"},{"depth":3,"slug":"2-dress-for-success","text":"2. Dress for success"},{"depth":3,"slug":"3-do-not-neglect-your-work","text":"3. Do not neglect your work"},{"depth":3,"slug":"4-the-horse-prepares-for-battle","text":"4. The horse prepares for battle"},{"depth":3,"slug":"5-personal-branding-is-vital","text":"5. Personal branding is vital"},{"depth":3,"slug":"6-search-and-you-will-find","text":"6. Search and you will find"},{"depth":2,"slug":"conclusions","text":"CONCLUSIONS"}],"body":"## BACKGROUND\n\nUsually many of us graduates from college have dreamed of working in a large company with great salary, prestige and traveling around the world; or maybe we’ve had the “million\\-dollar idea” and we’ve put tears, sweat and blood to boot that beloved startup and appear in Wired as the startup of the year. No doubt many have experienced one of these two scenarios, which is commendable; for other people perhaps this was not as important as it was to lead a quiet and familiar life with a modest and stable job without the need to experience the jetlag of trips at every moment, or the stressing deadlines for launching the product; those who have chosen this option have chosen the quiet life and are also congratulated.\n\nBut there are those who have found no satisfaction in either case, whether due to failures, lost opportunities or simply the opportunity was never presented . The stablishment may lead them to think of them as “loosers”, but nothing further from true, as they may be potential or emerging freelancers.\n\n## MY EXPERIENCE\n\nSome years before starting my adventure as a freelancer I failed 2 times with a startup that never launched and succeded, and missed opportunities to work in spectacular companies. This left me with the thought that maybe that was not for me, and I decided to take a normal job and lead a quiet life. However, deep within me there was still a spark of entrepreneurship, and on the day my daughter was born, the call was reborn. Maybe it might seem like this was not the best time to do it, but there was now a need to organize my time much better so I could have hours of the day and see my daughter grow up.\n\nSince then I have learned many things that I once considered obvious, but now I consider important in freelance work:\n\n### 1\\. Enjoy the hours of the day\n\nWith this I don’t mean to work 20 hours a day, but it is important to give each task its time and space, and execute it in an excellent way: work well, get enough sleep, rest the necessary, a good time in family, a good time to leisure. The unsuccessful tasks are those that don’t contribute,\n\n### 2\\. Dress for success\n\nFrom experience I can say that your body “feels” that we are going to work and is predisposed to perform efficiently. Many people think that the freelancer works in pajamas, but doing so will simply predispose the body to being sleepy.\n\n### 3\\. Do not neglect your work\n\nAt first we do not know the price we will give to our work, one tends to think that we could charge expensive and lose the customer, or very cheap and become a bargain. It may actually be a mid\\-point, but you need to consider that you probably have to cover the following costs:\n\n- Error correction\n- Delays in deadlines or payments\n- Unexpected expenses and delays\n\nIt is important to say that often the client is not responsible for these inconveniences, but the wise freelancer should be prevented. An approximate percentage is \\~ 20% of the estimated time. If all goes well and there are no unexpected events, that surplus will be a backup until you get the next client or project.\n\n### 4\\. The horse prepares for battle\n\nThe worst thing we can do is feel comfortable with what we learned the previous year, the skills required for the freelancer are always changing, and a continuous learning prepares us to be able to professionally assume each project.\n\n### 5\\. Personal branding is vital\n\nPersonally I am not a fan of social networks, however, promoting our name is very important, mainly with the idea of ​​projecting us as professionals. The important resources to take in account are:\n\n- LinkedIn\n- Personal Site or Blog/Vlog\n- GitHub\n- Twitter\n\n### 6\\. Search and you will find\n\nBeing persistant is extremely important, although you can wait for “capturing” a project in order to begin, once you start walking as a freelancer you will have to go looking for projects. Hopefully you will find a project right away, and you will have to enter to the field of being Project Manager and Developer at the same time; but usually starting a new project with the client takes a little time, in my experience about 2 months between starting the conversations and writing the first line of code\n\n## CONCLUSIONS\n\nThe adventure of being a freelancer is exciting because it involves 2 worlds: being an entrepreneur and being an employee, starting up involves a great effort until reaching the inertia and the necessary know\\-how for development. Are you wondering if it can be lucrative? Of course, it is lucrative enough not to envy employment in any large company, but it will require more time on your part. If you value family life and laisure times, it is possible to find a balance between incoming and free time to devote to other activities.\n\nOne very interesting option is being part of freelancing platforms like Toptal Web Engineering Group, which considers common needs of freelancers around the world. The defying task of getting clients is performed by TopTal, but it requires from you to be the best in what you do because only “la creme dela creme” of developers is chosen to work with TopTal. So, it’s hard but not impossible. Also, you can’t compare TopTal with other portals like Elance or Freelancers because they are too hard to get clients, even more complcated than finding projects by yourself in your local environment. I was able to work with Elance before, and it was good because of the client, but competence can be very hard and it’s not always a win\\-to\\-win relationship.\n\nFor those who intend to take this step of faith my recommendation is only to do it with determination and dedication. It may be very helpful to have an economic backup to get you started, but adventurers may enjoy cruising the desert to get to the promised land. Go ahead and Bon Voyage!"},{"slug":"cross-platform-communications-grpc-server-and-client-node-js","lang":"en","title":"Cross-Platform Communications: gRPC Server and Client: Node.js","description":"This is my second video on gRPC, this time I was experiencing Node.js to make an app communicate with a gRPC .NET Core app. Here the details about Node.js implementation: https://youtu.be/3wa0bs_Jw6M…","publishDate":"2017-03-26","updatedDate":null,"tags":["grpc","javascript","vscode"],"translationSlug":null,"url":"https://alexisalulema.com/blog/cross-platform-communications-grpc-server-and-client-node-js/","series":null,"headings":[],"body":"This is my second video on gRPC, this time I was experiencing Node.js to make an app communicate with a [gRPC .NET Core](https://alexisalulema.com/2017/03/10/cross-platform-communications-grpc-server-and-client-net-core/) app. Here the details about Node.js implementation:\n\nhttps://youtu.be/3wa0bs\\_Jw6M"},{"slug":"cross-platform-communications-grpc-server-and-client-net-core","lang":"en","title":"Cross-Platform Communications: gRPC Server and Client: .NET Core","description":"When we are developing web solutions&nbsp;consisting of several projects, it is very common communicate between each other, and the common solutions are proprietary solutions (like .NET Remoting), or …","publishDate":"2017-03-10","updatedDate":null,"tags":["c#","dotnet","grpc","vscode"],"translationSlug":null,"url":"https://alexisalulema.com/blog/cross-platform-communications-grpc-server-and-client-net-core/","series":null,"headings":[],"body":"When we are developing web solutions consisting of several projects, it is very common communicate between each other, and the common solutions are proprietary solutions (like .NET Remoting), or standard solutions like REST or SOAP. Recently, I discovered gRPC as the Google solution for Cross\\-Platform communications, allowing the developers to communicate applications. The advantage of gRPC is that it doesn’t use message formats which need to assemble JSON or XML (and it makes a bigger message, sometimes). Instead of this, gRPC uses Protocol Buffers to define binary messages to be interchanged between the parts.\n\nAt this time, I start with .NET Core, and you can see the details for creating server and client:\n\nGitHub Repository: [https://github.com/alexis\\-dotnet/Dotnet.Grpc](https://github.com/alexis-dotnet/Dotnet.Grpc)\n\nprotoc command: protoc \\-I\\=pb –csharp\\_out\\=Messages pb/messages.proto –grpc\\_out\\=Messages –plugin\\=protoc\\-gen\\-grpc\\=/Users/\\_your username\\_/.nuget/packages/Grpc.Tools/1\\.1\\.0/tools/macosx\\_x64/grpc\\_csharp\\_plugin  \nchmod 755 generateCerts.sh\n\n**Links:**gRPC: <http://www.grpc.io/>  \nProtocol Buffers: [https://developers.google.com/protocol\\-buffers/](https://developers.google.com/protocol-buffers/)  \nSSH Keys Generation: <http://stackoverflow.com/a/37739265>\n\nhttps://youtu.be/s374fFZuZbo"},{"slug":"installing-redis-on-azure-ubuntu-vm","lang":"en","title":"Installing Redis on Azure Ubuntu VM","description":"In one of my latest developments I needed to have a local Redis for caching requirements. I feel comfortable on terminal/command line, so it wasn’t hard to have an Ubuntu VM on VirtualBox, and it is v…","publishDate":"2017-02-15","updatedDate":null,"tags":["redis","ubuntu"],"translationSlug":null,"url":"https://alexisalulema.com/blog/installing-redis-on-azure-ubuntu-vm/","series":null,"headings":[],"body":"In one of my latest developments I needed to have a local Redis for caching requirements. I feel comfortable on terminal/command line, so it wasn’t hard to have an Ubuntu VM on VirtualBox, and it is very helpful to have it running on cloud if you need a testing site; nevertheless, there are several commands you need to remember to install it properly. Because of this, I created the following video installing Redis on an Ubuntu VM on Microsoft Azure. Here the details:\n\nInstalling Redis on Azure Ubuntu VM (and getting connected from Windows)\n\n1\\. Creating Ubuntu VM on Azure  \n2\\. SSH Connection to VM  \n3\\. Installing and configuring Redis  \n4\\. Creating rule for receiving connections in Azure  \n5\\. Connection from Windows Redis client\n\nhttps://youtu.be/OPWYgy\\_ZvjA"},{"slug":"asp-net-core-basics-part-2","lang":"en","title":"ASP.NET Core Basics – Part 2","description":"Now, we continue with the series ASP.NET Core Basics. At this time we are been focused in MVC&nbsp;specific topics, which will be covered in a 7&nbsp;mins video. Contents: MVC. ControllersMVC. Models …","publishDate":"2017-02-10","updatedDate":null,"tags":["c#","dotnet","vscode"],"translationSlug":null,"url":"https://alexisalulema.com/blog/asp-net-core-basics-part-2/","series":null,"headings":[],"body":"Now, we continue with the series ASP.NET Core Basics. At this time we are been focused in MVC specific topics, which will be covered in a 7 mins video.\n\n**Contents:**\n\n1. MVC. Controllers\n2. MVC. Models and Views\n3. ViewModel\n4. Post/Redirect/Get\n5. Model Validation\n\nhttps://youtu.be/7Ecg6DqDdbI\n\n**Links:**\n\nMVC Pattern: [https://en.wikipedia.org/wiki/Model–view–controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)  \nPost/Redirect/Get Pattern: <https://en.wikipedia.org/wiki/Post/Redirect/Get>  \nGithub Repository: [https://github.com/alexis\\-dotnet/NewsApp\\_Stage2](https://github.com/alexis-dotnet/NewsApp_Stage2)  \nThe New York Times API: <https://developer.nytimes.com/>\n\nEspañol: [Conceptos Básicos ASP.NET Core P.2](https://youtu.be/xCl56HJw62k)  \nPortuguês: [Conceitos Básicos ASP.NET Core P.2](https://youtu.be/8UoWy_-jh4E)"},{"slug":"installing-sql-server-for-linux-on-docker-container","lang":"en","title":"Installing SQL Server for Linux on Docker Container","description":"It is very common requiring MS SqlServer if you are developing ASP.NET web applications; because of this, having a local Sql Server is a great plus if you are developing web apps on ASP.NET Core on Ma…","publishDate":"2017-01-17","updatedDate":null,"tags":["macosx","sqlserver","vscode"],"translationSlug":null,"url":"https://alexisalulema.com/blog/installing-sql-server-for-linux-on-docker-container/","series":null,"headings":[],"body":"It is very common requiring MS SqlServer if you are developing ASP.NET web applications; because of this, having a local Sql Server is a great plus if you are developing web apps on ASP.NET Core on Mac, or Linux. One of the things to take in consideration is that we can make a “clean and rapid installation” using a Docker container. Take a look at this video for instructions!\n\n1. Installing Docker\n2. Installing Kitematic\n3. Installing and using SQL Server Client (sql\\-cli)\n4. Installing mssql extension on VSCode\n5. Configuring connection on VSCode\n\nhttps://youtu.be/48ziuJpo0jI"},{"slug":"asp-net-core-basics-part-1","lang":"en","title":"ASP.NET Core Basics – Part 1","description":"I’m starting this blog with a series of videos focused on ASP.NET Core. This tutorial has been completely developed on C# + Visual Studio Code + Mac OSX, but (almost) all commands and concepts are ful…","publishDate":"2017-01-16","updatedDate":null,"tags":["c#","dotnet","vscode"],"translationSlug":null,"url":"https://alexisalulema.com/blog/asp-net-core-basics-part-1/","series":null,"headings":[],"body":"I’m starting this blog with a series of videos focused on ASP.NET Core. This tutorial has been completely developed on C\\# \\+ Visual Studio Code \\+ Mac OSX, but (almost) all commands and concepts are fully compatible with Windows and Linux.\n\n**Contents:**\n\n1. Project Startup\n2. Configuration and Services\n3. Startup and Middleware\n\nhttps://youtu.be/CVkM1o2ZSzA\n\n**Links:**\n\nGithub Repo: [https://github.com/alexis\\-dotnet/NewApp\\_Stage1](https://github.com/alexis-dotnet/NewApp_Stage1)  \nMicrosoft .NET Core: <https://www.microsoft.com/net/core>  \nNode JS: <https://nodejs.org/en/>  \nYeoman: <http://yeoman.io/>  \nVisual Studio Code: <https://code.visualstudio.com/>\n\nEspañol: [Conceptos Básicos ASP.NET Core P.1](https://youtu.be/tLsdqCyj21g)\n\nPortuguês: [Conceitos Básicos ASP.NET Core P.1](https://youtu.be/kL3RSxMSb8o)"}],"series":[{"id":"rl-from-scratch","titles":{"en":"Reinforcement Learning from scratch","es":"Reinforcement Learning desde cero"},"slugs":["how-does-an-agent-learn-teaching-a-car-to-drive","how-do-we-improve-a-policy-without-breaking-it-ppo-explained-from-scratch","can-we-teach-by-imitation-behavioral-cloning-explained-from-scratch","can-we-learn-to-behave-like-an-expert-gail-explained-from-scratch","can-we-combine-imitation-and-rl-bc-gail-and-ppo-as-one-system"]}],"projects":[{"id":"trip-planner","title":"Trip Planner (Chain of Agents)","titleEs":"Planificador de Viajes (Cadena de Agentes)","description":"Type a trip request and watch a chain of specialised agents hand the work along in real time until they deliver a day-by-day itinerary checked against your budget. A LangGraph orchestrator owns the control flow; a local Qwen 2.5 writes only the prose, while typed decisions and plain arithmetic handle the rest. When the plan goes over budget, a conflict-resolution agent renegotiates it without a human. Local LLM (Qwen 2.5 via Ollama) and no API keys; live weather, places and exchange rates come from open data sources.","descriptionEs":"Escribe un pedido de viaje y mira cómo una cadena de agentes especializados se pasa el trabajo en tiempo real hasta entregar un itinerario día a día verificado contra tu presupuesto. Un orquestador en LangGraph controla el flujo; un Qwen 2.5 local solo redacta la prosa, mientras las decisiones tipadas y la aritmética resuelven el resto. Si el plan se pasa del presupuesto, un agente de resolución de conflictos lo renegocia sin intervención humana. LLM local (Qwen 2.5 vía Ollama), sin API keys; datos en vivo de clima, lugares y tipo de cambio desde fuentes abiertas.","techStack":["Python","FastAPI","LangGraph","Ollama (Qwen 2.5 1.5B)","SSE"],"githubUrl":"https://github.com/alulema/trip-planner","category":"ai","featured":true,"demoReady":true,"demoMinutes":null,"url":"https://alexisalulema.com/projects?demo=trip-planner"},{"id":"agentic-racing","title":"Agentic Racing","titleEs":"Agentic Racing","description":"Six identical cars race a closed circuit in the browser. Each car is a two-tier agent: a pilot that drives every frame, and an independent LLM team boss that reads live telemetry and radios back a strategy — attack, defend, conserve — F1 team-radio style, on screen. The race never waits on the model: a fixed heuristic takes over the instant it is slow, off, or replies invalid JSON. Fully self-hosted: a local llama3.2:3b behind a proxy with its own circuit breaker, rate limit and concurrency gate.","descriptionEs":"Seis autos idénticos corren un circuito cerrado en el navegador. Cada auto es un agente de dos capas: un piloto que conduce cada frame, y un jefe de equipo LLM independiente que lee la telemetría en vivo y radía una estrategia — atacar, defender, conservar — al estilo de la radio de equipo en F1, en pantalla. La carrera nunca espera al modelo: una heurística fija toma el control apenas está lento, apagado, o responde un JSON inválido. Totalmente autohospedado: un llama3.2:3b local detrás de un proxy con su propio circuit breaker, rate limit y control de concurrencia.","techStack":["Unity WebGL","Python","FastAPI","Ollama (Llama 3.2 3B)"],"githubUrl":"https://github.com/alulema/agentic-racing","category":"ai","featured":true,"demoReady":true,"demoMinutes":null,"url":"https://alexisalulema.com/projects?demo=agentic-racing"},{"id":"rag-blogposts","title":"RAG Chatbot over Blog Posts","titleEs":"Chatbot RAG sobre Posts del Blog","description":"An interactive retrieval-augmented generation chatbot that answers questions using my blog posts as a knowledge base. Fully self-hosted: semantic vector search (pgvector) with multilingual embeddings and a local Qwen 2.5 LLM served by Ollama — no external LLM APIs.","descriptionEs":"Un chatbot interactivo de generación aumentada por recuperación que responde preguntas usando mis posts del blog como base de conocimiento. Totalmente autohospedado: búsqueda vectorial semántica (pgvector) con embeddings multilingües y un LLM Qwen 2.5 local servido por Ollama — sin APIs de LLM externas.","techStack":["Python","FastAPI","pgvector","sentence-transformers","Ollama (Qwen 2.5)"],"githubUrl":null,"category":"ai","featured":true,"demoReady":true,"demoMinutes":null,"url":"https://alexisalulema.com/projects?demo=rag-blogposts"},{"id":"dreaming-agent","title":"The Dreaming Agent","titleEs":"El agente que sueña","description":"A conversational agent with three-level memory (short, mid and long term) and a \"dream\" phase that consolidates it offline, inspired by LightMem (ACL 2026). Chat with it and watch what each module decided on every turn, what lives in each memory level, and what the Dream changed — merges, updates, promotions and forgets, as a diff checked by a deterministic validator. A selector answers with LightMem, a naive RAG, or both side by side. Fully local: a small Qwen 2.5 3B and bge-m3 embeddings on Ollama, no external APIs.","descriptionEs":"Un agente conversacional con memoria de tres niveles (corto, mediano y largo plazo) y una fase de \"sueño\" que la consolida offline, inspirado en LightMem (ACL 2026). Conversa con él y mira qué decidió cada módulo en cada turno, qué hay en cada nivel de memoria y qué cambió el Dream — fusiones, actualizaciones, promociones y olvidos, como un diff revisado por un validador determinista. Un selector responde con LightMem, con un RAG ingenuo o con ambos lado a lado. Todo local: un Qwen 2.5 3B pequeño y embeddings bge-m3 en Ollama, sin APIs externas.","techStack":["Python","FastAPI","SQLite","Ollama (Qwen 2.5 3B)","bge-m3"],"githubUrl":"https://github.com/alulema/dreaming-agent","category":"ai","featured":true,"demoReady":true,"demoMinutes":40,"url":"https://alexisalulema.com/projects?demo=dreaming-agent"},{"id":"harness2-playground","title":"Harness² AI Agent Playground","titleEs":"Playground Harness² de agentes IA","description":"An interactive walkthrough of recursive agent optimization: Harness² improves an agent's harness — its system prompt, memory and tool guidance — rather than its model, in a Propose → Probe → Compose loop. Pick a Legal LAB, JobBench or WorkBuddy scenario and watch a small open-weight model (Qwen2.5 1.5B or Gemma 2 2B, served by Ollama inside the demo, no external API) propose each candidate change, probe it on task variations and compose it into the harness if a same-model judge scores it higher, with the base and optimized outputs side by side. Runs are live and streamed over SSE; on CPU an iteration takes 1–2 minutes (Gemma is slower), scores are subjective estimates, and the demo does not run the original Google optimizer. A cached example is available too.","descriptionEs":"Un recorrido interactivo por la optimización recursiva de agentes: Harness² mejora el harness de un agente —su system prompt, su memoria y su guía de herramientas— en lugar del modelo, en un ciclo Propose → Probe → Compose. Elige un escenario de Legal LAB, JobBench o WorkBuddy y mira cómo un modelo pequeño de pesos abiertos (Qwen2.5 1.5B o Gemma 2 2B, servido por Ollama dentro del demo, sin API externa) propone cada cambio candidato, lo prueba con variaciones de la tarea y lo integra al harness si un juez del mismo modelo lo puntúa más alto, con la salida base y la optimizada lado a lado. Las ejecuciones son en vivo y se transmiten por SSE; en CPU cada iteración tarda 1–2 minutos (Gemma es más lento), las puntuaciones son estimaciones subjetivas y el demo no ejecuta el optimizador original de Google. También hay un ejemplo en caché.","techStack":["Astro","React","Python","FastAPI","SSE"],"githubUrl":"https://github.com/alulema/harness-playground","category":"ai","featured":true,"demoReady":true,"demoMinutes":null,"url":"https://alexisalulema.com/projects?demo=harness2-playground"}],"certifications":[{"name":"Microsoft Certified: Azure Developer Associate (AZ-204)","issuer":"Microsoft","date":"May 2026","dateEs":"May 2026","verifyUrl":"https://learn.microsoft.com/en-us/users/alexisalulema-6344/credentials/948138076f6bfb70","expiredOn":null,"expiredOnEs":null},{"name":"GitHub Copilot","issuer":"GitHub","date":"Jan 2026","dateEs":"Ene 2026","verifyUrl":"https://learn.microsoft.com/en-us/users/alexisalulema-6344/credentials/32be094e886a96ca","expiredOn":null,"expiredOnEs":null},{"name":"Microsoft Azure AI Fundamentals","issuer":"Microsoft","date":"Jun 2025","dateEs":"Jun 2025","verifyUrl":"https://learn.microsoft.com/api/credentials/share/en-us/AlexisAlulema-6344/E345C4D1CF397980","expiredOn":null,"expiredOnEs":null},{"name":"Microsoft Azure Data Fundamentals","issuer":"Microsoft","date":"Jul 2025","dateEs":"Jul 2025","verifyUrl":"https://learn.microsoft.com/api/credentials/share/en-us/AlexisAlulema-6344/88124E3B7F1000E0","expiredOn":null,"expiredOnEs":null},{"name":"Microsoft Azure Fundamentals","issuer":"Microsoft","date":"Apr 2025","dateEs":"Abr 2025","verifyUrl":"https://learn.microsoft.com/api/credentials/share/en-us/AlexisAlulema-6344/FB9311E511A32AC8","expiredOn":null,"expiredOnEs":null},{"name":"Professional Certificate in LLMs","issuer":"Databricks","date":"Jan 2024","dateEs":"Ene 2024","verifyUrl":"https://credentials.edx.org/credentials/afd3a8a517464f4e959b1c9ee1fe4d22/","expiredOn":null,"expiredOnEs":null},{"name":"Generative AI with LLMs","issuer":"Coursera / DeepLearning.AI","date":"Nov 2023","dateEs":"Nov 2023","verifyUrl":"https://www.coursera.org/account/accomplishments/certificate/ADDLV399DFTV","expiredOn":null,"expiredOnEs":null},{"name":"AWS Certified Cloud Practitioner","issuer":"Amazon Web Services","date":"Jul 2022","dateEs":"Jul 2022","verifyUrl":"https://www.credly.com/badges/5456926d-c5d2-46fa-95d8-8a20eaecb80a/public_url","expiredOn":"Jul 2025","expiredOnEs":"Jul 2025"}]}