<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Trust, but Trace</title><description>Ensayos sobre fiabilidad de agentes, evaluación y el Reglamento europeo de IA, de un ingeniero de ML que trabaja en producción.</description><link>https://cchinchilla.dev/</link><language>es</language><copyright>© Carlos Chinchilla Corbacho — content licensed CC BY-NC 4.0</copyright><managingEditor>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</managingEditor><webMaster>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</webMaster><atom:link href="https://cchinchilla.dev/es/rss.xml" rel="self" type="application/rss+xml"/><item><title>El fine-tuning es mala herramienta para añadir conocimiento: tres fases de entrenamiento y el atajo de bajo rango</title><link>https://cchinchilla.dev/es/posts/fcw-pretraining-finetuning-lora-rlhf/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/fcw-pretraining-finetuning-lora-rlhf/</guid><description>El preentrenamiento y el fine-tuning comparten pérdida y se distinguen por una máscara. LoRA entrena dos matrices estrechas al lado de los pesos congelados. Los hechos que se enseñan con fine-tuning entran despacio y traen una confianza inmerecida: mi modelo dio Moscú como capital de un país que no existe.</description><pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;En la parte 9 entrenamos un modelo desde cero, pero lo normal en este trabajo es partir de unos pesos que ha entrenado otro y modificarlos. Preentrenamiento, fine-tuning, RLHF y LoRA son cuatro palabras que describen esa tarea como si fueran cuatro máquinas distintas.&lt;/p&gt;
&lt;p&gt;He leído la capa LoRA de peft y la pérdida DPO de trl, y después le he enseñado a SmolLM2-135M quince capitales, tres que ya sabía y doce de países que no existen. Se las aprendió todas, y ahí está el problema.&lt;/p&gt;
&lt;h2 id=&quot;tres-fases-y-un-modelo-en-entrenamiento&quot;&gt;Tres fases y un modelo en entrenamiento&lt;/h2&gt;
&lt;p&gt;El preentrenamiento consiste en predecir el siguiente token sobre todos los datos que tengas, con la entropía cruzada de la parte 8 aplicada a un corpus. Es en esta fase donde el modelo adquiere casi todo lo que sabe.&lt;/p&gt;
&lt;p&gt;El fine-tuning supervisado (SFT) usa la misma pérdida sobre pares de instrucción y respuesta. Lo habitual es cambiar qué posiciones cuentan y dejar fuera el prompt con una máscara, como hace trl por defecto con los datos de tipo prompt-completion. En un ejemplo con cinco tokens de prompt y siete de respuesta, la pérdida fue de 2,9883 sobre la secuencia entera y de 2,4845 sobre la respuesta sola, los mismos dos números que he sacado a mano de los logits. Lo único distinto es un array de etiquetas, igual que cuando se ejecuta el mismo binario con otro fichero de configuración.&lt;/p&gt;
&lt;p&gt;En la tercera fase, el ajuste por preferencias, no hay respuesta correcta. Cada ejemplo son dos respuestas y una nota que indica cuál prefirió una persona.&lt;/p&gt;
&lt;p&gt;Por defecto, las tres fases actualizan todos los pesos del modelo que se entrena, al que en el ajuste por preferencias se llama política, un término heredado del aprendizaje por refuerzo. Lo que distingue a la tercera es cuántos modelos más mantiene a su lado.&lt;/p&gt;
&lt;h2 id=&quot;el-ajuste-por-preferencias-optimiza-una-diferencia&quot;&gt;El ajuste por preferencias optimiza una diferencia&lt;/h2&gt;
&lt;p&gt;El aprendizaje por refuerzo a partir de retroalimentación humana (RLHF) entrena un modelo de recompensa para puntuar respuestas y luego optimiza la política según esas puntuaciones. Su algoritmo clásico, PPO (optimización proximal de políticas), añade un crítico que predice qué puntuación acabará teniendo una respuesta a medio escribir. También guarda una referencia congelada, el modelo tal como estaba antes de esta fase, para penalizar que la política se aleje de ella. Son cuatro modelos para una sola actualización. La optimización directa de preferencias (DPO) prescinde del modelo de recompensa y del crítico, pero conserva la referencia. Su núcleo, condensado de &lt;code&gt;dpo_trainer.py:1445-1483&lt;/code&gt; de trl, es este:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:104ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;chosen_logratios &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; chosen_logps &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; ref_chosen_logps        &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# how far the policy moved, on the good answer&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;rejected_logratios &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; rejected_logps &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; ref_rejected_logps  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# and on the bad one&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;chosen_scores, rejected_scores &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; chosen_logratios, rejected_logratios   &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# the default divergence&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;delta_score &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; chosen_scores &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; rejected_scores&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;per_sequence_loss &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;F.logsigmoid(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.beta &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; delta_score)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;La pérdida busca que la diferencia entre esos dos movimientos sea grande. &lt;code&gt;beta&lt;/code&gt;, que vale 0,1 por defecto, fija lo que cuesta alejarse de la referencia. Cuanto más alta, menos se mueve la política.&lt;/p&gt;
&lt;p&gt;La referencia parece una segunda copia de los pesos, pero con una política LoRA (la explico más abajo) no lo es. La parte entrenable es un pequeño desvío al lado de los pesos congelados, así que basta con apagarlo para tener la referencia. Con el desvío apagado, las log-probabilidades de la política coincidieron hasta el último bit con las de un modelo congelado cargado aparte, tanto antes de entrenar como tras veinte pasos. En el paso cero, la política y la referencia son el mismo modelo y la diferencia es cero. La pérdida vale entonces menos el logaritmo de un medio, es decir, log 2, que con seis decimales es 0,693147.&lt;/p&gt;
&lt;p&gt;Esas cuentas las he hecho en un bucle propio, fuera de &lt;code&gt;DPOTrainer&lt;/code&gt;. Con una tasa de aprendizaje brutal de 0,001 y veinte pasos sobre dos pares de preferencias, la diferencia pasó de 0 a 95,4. La respuesta buena se volvió 2,9 nats más probable y la mala, 92,5 nats menos.&lt;/p&gt;
&lt;p&gt;En parte, eso viene de serie. Como una log-probabilidad no puede pasar de cero y las respuestas buenas partían de −2,3 y −3,5, esos 2,9 nats eran casi todo el margen que tenían; las malas, en cambio, medían nueve y doce tokens y no tenían suelo. Por eso lo he relanzado con pares de la misma longitud, París contra Lyon y 4 contra 5, tres veces en cada sentido. Con la tasa brutal, el lado rechazado cayó entre 31 y 47 nats en todas las ejecuciones, mientras que el preferido se movió entre −1,2 y +4,2 y en una de ellas acabó siendo menos probable que al principio.&lt;/p&gt;
&lt;p&gt;Con una tasa realista de 0,00001, el reparto depende de dónde partía cada respuesta. Cuando las preferidas eran París y 4, ganaron entre 0,01 y 0,02 nats y las otras perdieron entre 0,16 y 0,17; cuando lo eran Lyon y 5, ganaron 0,15 y las otras perdieron 0,03. En estos pares, el movimiento mayor se lo llevó la respuesta que al principio era menos probable. La pérdida pide una diferencia y la saca del lado más barato. Nada en ella obliga a que la respuesta preferida se vuelva más probable.&lt;/p&gt;
&lt;h2 id=&quot;lora-entrena-otro-objeto&quot;&gt;LoRA entrena otro objeto&lt;/h2&gt;
&lt;p&gt;LoRA (adaptación de bajo rango) es una decisión sobre en qué consiste la actualización, válida para cualquiera de las tres fases. Se deja intacta una matriz de pesos y se le ponen al lado dos matrices pequeñas, que juntas forman el adaptador. Así lo hace peft, en una versión condensada de &lt;code&gt;lora/layer.py:263-1102&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:89ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.lora_A[adapter_name] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; nn.Linear(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.in_features, r, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;bias&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;False&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# d_in to r&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.lora_B[adapter_name] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; nn.Linear(r, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.out_features, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;bias&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;lora_bias)  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# r to d_out&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.scaling[adapter_name] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; lora_alpha &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; r&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;nn.init.zeros_(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.lora_B[adapter_name].weight)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; result &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; lora_B(lora_A(dropout(x))) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; scaling&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;El rango &lt;code&gt;r&lt;/code&gt; marca lo estrecho que es el tramo intermedio. Antes de sumarse al resultado, el desvío se multiplica por &lt;code&gt;lora_alpha / r&lt;/code&gt;, que con alpha 16 y rango 8 vale 2,0.&lt;/p&gt;
&lt;p&gt;El truco está en los ceros de la última línea de la inicialización. En el paso cero el producto de las dos matrices es cero, así que el modelo es idéntico, bit a bit, al que tenías al empezar. Después el entrenamiento mueve &lt;code&gt;A&lt;/code&gt; y &lt;code&gt;B&lt;/code&gt;. Lo que aprenden es una diferencia que, al multiplicarse, da un cambio de rango 8 en una de las matrices de 576 por 576 de SmolLM2 (rango en el sentido del álgebra lineal, es decir, el número de direcciones independientes que tiene el cambio). Fusionado con la matriz, el adaptador dio los mismos logits que cuando iba al lado, con una discrepancia máxima de 0,00008.&lt;/p&gt;
&lt;p&gt;De ese rango sale el ahorro. Si se adaptan las matrices query y value de Llama-3.1-8B con rango 8, se entrenan 3 407 872 números de 8 030 261 248, un 0,04 %.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Lo que cuesta entrenar Llama-3.1-8B con fine-tuning. Elige qué matrices llevan adaptador y arrastra el rango. La barra de arriba es LoRA y la de abajo, el fine-tuning completo. Ninguna de las dos incluye las activaciones.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-pretraining-finetuning-lora-rlhf/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Si subes el rango, el total apenas se mueve, porque casi todo es la base congelada. Ahora pasa la base a NF4 con doble cuantización, el formato de 4 bits de QLoRA (la receta para entrenar LoRA sobre una base de 4 bits), y desaparecen casi dos tercios de la barra de LoRA. Solo las capas de dentro de los bloques bajan a algo más de cuatro bits por peso; los embeddings y la capa de salida se quedan en dieciséis.&lt;/p&gt;
&lt;h2 id=&quot;los-hechos-entran-despacio-y-con-ellos-llega-la-confianza&quot;&gt;Los hechos entran despacio y con ellos llega la confianza&lt;/h2&gt;
&lt;p&gt;Hasta aquí la mecánica. Ahora viene la parte que se suele entender mal en las reuniones de planificación.&lt;/p&gt;
&lt;p&gt;El punto de partida son doce capitales de países inventados y doce reales, todas con la forma «The capital of Vantressa is Molduin». De las reales, el modelo completó tres palabra por palabra. En seis de las otras nueve contestó cosas como «the city of Rome», que no cuentan como coincidencia exacta. Las nueve quedaron fuera. Luego he ajustado todos los pesos sobre las quince a la vez, con una tasa de 0,00001.&lt;/p&gt;
&lt;p&gt;Las tres que ya sabía se afianzaron enseguida y en veinte pasos la confianza en el primer token de la respuesta correcta pasó del 22 % al 97 %. Para entonces las doce nuevas estaban en el 2 % y el modelo acertaba una. En el paso cuarenta ya respondía las doce. En parte, la lentitud se debe al punto de partida. En ese primer token, los hechos nuevos estaban a 8,6 nats de distancia, frente a los 1,5 de los conocidos, más de cinco veces más lejos. Además, cada uno necesitaba acertar dos o tres tokens cuando a los conocidos les bastaba uno. &lt;a href=&quot;https://arxiv.org/abs/2405.05904&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Gekhman y colaboradores&lt;/a&gt; describen el mismo patrón a gran escala: el conocimiento nuevo se ajusta significativamente más despacio y, a medida que entra, aumentan las alucinaciones.&lt;/p&gt;
&lt;p&gt;Eso sí, «respondía» quiere decir que completaba la frase con la que lo había entrenado. Con «What is the capital of Vantressa?» no acertó ninguna de las doce, ni al preguntarle de qué país es capital Molduin; con «Vantressa’s capital city is», en cambio, acertó seis. Lo que había aprendido era una frase.&lt;/p&gt;
&lt;p&gt;Algo parecido a ese aumento de las alucinaciones apareció también aquí. Otros seis nombres de país, inventados o sacados de lugares poco conocidos, se quedaron fuera del entrenamiento. Antes de entrenar, el modelo los completaba igual que las capitales reales de más arriba, con alrededor de un 24 % de confianza. Cinco respuestas empezaban por «the town of» o «the city of», cuatro de ellas seguidas del propio nombre del país. Tras cien pasos, cuatro de las seis acababan en una ciudad real seguida de un punto. Según el modelo, la capital de Volnaya es Moscú, con un 82 %. La de Quenby es Shillong, la de Dalmerie, Edimburgo, y la de Rhuddlan, Birmingham sin la B.&lt;/p&gt;
&lt;p&gt;En el entrenamiento no había nada sobre esos lugares. Moscú estaba en el batch como capital de Rusia, pero Volnaya también suena a ruso, y en el paso 10 la respuesta era Vorónezh. Otras respuestas se guiaban por el sonido, como Maribor para Marovska o Cardiff para Rhuddlan. Aprendió que una frase de esta forma acaba en una ciudad y a menudo escogió una que encajaba por cómo sonaba.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Fine-tuning de todos los pesos de un modelo de 135M con doce hechos que no podía haber visto, junto a los tres reales (de doce) que ya sabía completar. Arrastra el paso. La línea ámbar es su confianza en el primer token de la respuesta correcta para los hechos que ya conocía; la continua, lo mismo para los nuevos, y la punteada, la seguridad media con que responde (diga lo que diga) en seis lugares inventados que nadie le enseñó.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-pretraining-finetuning-lora-rlhf/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Arrastra hasta el paso 20 y verás que los hechos conocidos están en el 97,0 %, los nuevos en el 1,9 % y el modelo recuerda uno de los doce. Después recorre todo el entrenamiento fijándote en la línea punteada, la de los seis países que nadie le enseñó. Baja al 5,7 % en el paso 10, cuando el modelo deja de decir «the», y termina en el 41,1 %, frente al 23,7 % del principio. Esa subida se debe a dos respuestas: Moscú, con un 82 %, y «Th», el comienzo del propio nombre de Threndale, con un 89 % (el modelo lo completa como «Thwapth»). Las otras cuatro se quedan en un 19 % de media, cuando empezaron en un 24 %.&lt;/p&gt;
&lt;h2 id=&quot;el-parche-que-nadie-puede-leer&quot;&gt;El parche que nadie puede leer&lt;/h2&gt;
&lt;p&gt;Un fine-tuning completo es un diff, así que lo he contado. De 134 515 008 parámetros cambiaron 134 513 152, aunque ninguno lo bastante como para llamar la atención. La mediana del cambio fue 0,0002 y el máximo llegó a 0,0012, que es más o menos la tasa multiplicada por los cien pasos. Con el Adam de la parte 9, un peso con un gradiente estable se mueve en cada paso aproximadamente lo que vale la tasa. El cambio en una matriz de atención ocupó 572 de sus 576 direcciones posibles. Si se mide por la suma de los cuadrados de los cambios, las ocho primeras direcciones recogen el 71 % y hacen falta 27 para llegar al 90 %. Las ocho de LoRA lo recogen todo por construcción. Lo único que el diff comparte con un parche que se pueda revisar es que, al restarlo, se recuperan los pesos originales.&lt;/p&gt;
&lt;p&gt;Un parche ilegible solo es un problema si ha cambiado algo que te importaba, y por eso lo he medido. He hecho ocho versiones del mismo fine-tuning, con tres tasas de aprendizaje para la actualización completa y cinco para un adaptador LoRA de rango 8 sobre las matrices query y value. A cada una la he puntuado por cuánto subía su pérdida en seis entradas de este blog con las que no se entrenó ninguna.&lt;/p&gt;
&lt;p&gt;El fine-tuning completo aprendió los doce hechos con todas las tasas. LoRA no aprendió ninguno hasta la tasa de 0,0003, que es treinta veces la que ya le bastaba al completo. Si se toma para cada uno la tasa más baja de las probadas con la que aprendió, LoRA subió la pérdida en esas entradas más de veinte veces más que la actualización completa (0,111 nats frente a 0,005), una factura curiosa para la opción barata.&lt;/p&gt;
&lt;p&gt;A igualdad de tasa, con 0,001 la actualización completa subió la pérdida quince veces más que LoRA (4,419 frente a 0,287). Este adaptador sí limita el daño, solo que necesita una tasa a la que haya daño que limitar.&lt;/p&gt;
&lt;p&gt;Para ser justos, todo esto son ocho ejecuciones con una sola semilla sobre un modelo de 135M, con quince hechos, cien pasos, una única configuración de adaptador y una rejilla de tasas poco densa. Un adaptador en todas las capas lineales quizá aprendería con una tasa más baja, y con un modelo más grande o un dataset real el orden podría salir distinto. Lo que sí esperaría que se transfiriera es el método: comparar tanto a igualdad de resultado como de configuración y medir el daño colateral en algo ajeno al fine-tuning.&lt;/p&gt;
&lt;h2 id=&quot;qué-vigila-el-ingeniero-de-ml&quot;&gt;Qué vigila el ingeniero de ML&lt;/h2&gt;
&lt;p&gt;Son cuatro hábitos, y el primero es una decisión previa a todo lo demás.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pregúntate qué quieres cambiar.&lt;/strong&gt; Los hechos que faltan deberían llegar por recuperación a la ventana de contexto, donde puedes verlos y quitarlos. El comportamiento, el formato y las negativas a responder sí se le dan bien al fine-tuning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Puntúa el daño colateral además del objetivo.&lt;/strong&gt; Todas las ejecuciones que aprendieron los doce hechos acabaron por encima del 99 % en ellos, incluida la actualización completa a 0,001, que subió más de cuatro nats la pérdida en las seis entradas. La parte 8 pedía una sola puntuación sobre el conjunto de test, pero en esta fase hacen falta dos, una sobre texto general y otra sobre lo que de verdad vendes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trata la tasa como la palanca del daño.&lt;/strong&gt; En las ocho ejecuciones, la tasa movió el daño colateral tres órdenes de magnitud, frente al factor de quince que hubo entre la actualización completa y la de bajo rango en la única tasa con la que aprendieron las dos. Haz un barrido, como en la parte 9, y elige la tasa por la cifra de daño colateral.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mantén separados la base, el adaptador y el modelo fusionado.&lt;/strong&gt; El adaptador de Llama con rango 8 ocupa 6,8 MB al lado de los 16 GB de la base congelada y se apaga con una sola llamada. Por eso el modelo de referencia de antes salía gratis en memoria. El cómputo sí se paga, porque en cada paso hay que volver a ejecutar el modelo con el adaptador apagado. Fusiona cuando vayas a servir un solo adaptador sobre una base sin cuantizar. Mientras decides, o cuando una misma base sirva muchos adaptadores, mantén las piezas separadas.&lt;/p&gt;
&lt;p&gt;Esto tampoco es trabajo de modelado: todo se reduce a una máscara sobre las etiquetas, una sigmoide sobre una diferencia y, en la parte de bajo rango, dos matrices estrechas, una de ellas a cero. Las cuentas se leen en una tarde, y aun así acaban en un diff de cien millones de números que nadie puede revisar.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 10 de 12. El preentrenamiento y el fine-tuning supervisado comparten pérdida y se distinguen en qué posiciones ve esa pérdida. El ajuste por preferencias conserva una referencia congelada, a la que el PPO clásico de RLHF suma un modelo de recompensa y un crítico, mientras que DPO prescinde de los dos y optimiza una diferencia que saca sobre todo del lado que cuesta menos mover. LoRA entrena dos matrices estrechas junto a los pesos congelados. Entre las tasas que he probado, necesitó una treinta veces mayor que la de la actualización completa para aprender los hechos, y con ella causó más de veinte veces más daño colateral. Los hechos que se enseñan ajustando todos los pesos llegan despacio, como frases, y traen consigo una confianza inmerecida, hasta el punto de que el modelo dio Moscú como capital de un país que no existe. La siguiente parte se pregunta qué hace falta para servir el modelo. Si has llegado aquí directamente, puedes empezar por &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;el tokenizador&lt;/a&gt; y seguir con &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;la tabla a la que apunta el id de un token&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/&quot;&gt;las formas&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-attention-learned-weighted-average/&quot;&gt;la atención&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-transformer-six-shapes-residual-stream/&quot;&gt;el bloque&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-transformer-plus-five-decisions/&quot;&gt;GPT-2 y sus dos programas&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-to-llama-what-changed/&quot;&gt;lo que cambió Llama&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-loss-gradients-graph-not-formulas/&quot;&gt;el origen de los gradientes&lt;/a&gt; y &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-optimizers-schedules-lr-breaks-production/&quot;&gt;lo que hace el optimizador con ellos&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><category>Fundamentos de ML</category><category>Fundamentos de ML</category><category>From code to weights</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>La tasa de aprendizaje es el hiperparámetro que rompe producción</title><link>https://cchinchilla.dev/es/posts/fcw-optimizers-schedules-lr-breaks-production/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/fcw-optimizers-schedules-lr-breaks-production/</guid><description>Adam y el schedule son decisiones de ingeniería que las matemáticas no dictan. Nada valida la configuración: un valor malo diverge a las 30 horas y tumba la ejecución.</description><pubDate>Tue, 27 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 8 terminó con un gradiente, que indica hacia dónde mover cada peso. Cuánto moverlo lo decide el optimizador, y esa decisión se reduce a un puñado de números en un fichero de configuración que nada en todo el stack comprueba.&lt;/p&gt;
&lt;p&gt;He leído el paso de &lt;code&gt;Adam&lt;/code&gt; en torch y el schedule de nanoGPT, y después he entrenado de 21 maneras el modelo de caracteres de la parte 8 (cuatro bloques que leen el texto carácter a carácter): siete tasas de aprendizaje, cada una sin warmup, con warmup y con warmup más un decaimiento coseno. Ninguna de las 21 ejecuciones se cayó.&lt;/p&gt;
&lt;h2 id=&quot;tres-optimizadores-en-cinco-líneas&quot;&gt;Tres optimizadores en cinco líneas&lt;/h2&gt;
&lt;p&gt;El descenso de gradiente a secas resta el gradiente multiplicado por un tamaño de paso. El momentum va acumulando los gradientes pasados en una suma que se encoge un poco en cada paso, y es esa suma la que mueve el peso. Así, un peso empujado dos veces hacia el mismo lado coge velocidad. Adam añade una segunda media, la del gradiente al cuadrado, y divide por su raíz cuadrada. El paso de torch 2.14, condensado a partir de &lt;code&gt;adam.py:456-546&lt;/code&gt;, queda así.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:86ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;exp_avg.lerp_(grad, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; beta1)                    &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# average of the gradient&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;exp_avg_sq.mul_(beta2).addcmul_(grad, grad, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; beta2)   &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# average of its square&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;step_size &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; lr &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; beta1 &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; step)              &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# bias correction, early steps&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;denom &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; exp_avg_sq.sqrt() &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; beta2 &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; step) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0.5&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; eps&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;param.addcdiv_(exp_avg, denom, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;step_size)  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# one step per weight&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Toda la idea está en esa división. A un peso cuyo gradiente ha sido grande últimamente le toca un divisor grande, y a uno con gradientes pequeños, uno pequeño. Cada peso acaba así con su propio tamaño de paso, marcado por su historia reciente, y &lt;code&gt;lr&lt;/code&gt; solo pone la escala. Otros dos números de la configuración, &lt;code&gt;beta1&lt;/code&gt; y &lt;code&gt;beta2&lt;/code&gt;, fijan lo rápido que cada media olvida lo que ha visto (nanoGPT usa 0,9 y 0,95).&lt;/p&gt;
&lt;p&gt;AdamW añade una línea, &lt;code&gt;param.mul_(1 - lr * weight_decay)&lt;/code&gt; en &lt;code&gt;adam.py:419&lt;/code&gt;, que encoge un poco cada peso en cada paso. La última sección trata de dónde se coloca esa línea.&lt;/p&gt;
&lt;h2 id=&quot;la-escala-de-la-pérdida-se-cancela-y-lo-paga-la-memoria&quot;&gt;La escala de la pérdida se cancela y lo paga la memoria&lt;/h2&gt;
&lt;p&gt;En la parte 8, multiplicar una pérdida por 40 no cambió nada, y ahora se ve por qué. Si multiplicas cada gradiente por 40, &lt;code&gt;exp_avg&lt;/code&gt; se multiplica por 40 y &lt;code&gt;exp_avg_sq&lt;/code&gt; por 1600, así que su raíz cuadrada también se multiplica por 40 y el factor se cancela en la división. Se cancela en todas partes salvo en &lt;code&gt;eps&lt;/code&gt;, la constante que se suma al denominador para que no se acerque a cero. Escalar la pérdida por 40 equivale a dividir esa constante entre 40. Si alguna vez has cambiado las unidades de una métrica y las alertas han seguido igual porque todos los umbrales eran relativos, aquí pasa lo mismo.&lt;/p&gt;
&lt;p&gt;En tres pasos del modelo de caracteres, el desplazamiento mediano de los pesos con AdamW fue de 0,0022 tanto con la pérdida escalada como sin escalar, y los dos vectores de actualización apuntaban en la misma dirección. Con SGD a secas, el mismo cambio multiplicó por 17,5 el paso mediano y desvió 30 grados la actualización. Eso sí, la cancelación no es idéntica peso a peso: la mayor discrepancia fue de 0,00134, más de la mitad de un paso típico.&lt;/p&gt;
&lt;p&gt;Lo que Adam gana en robustez lo paga en memoria. Guarda dos números por peso, que torch llama &lt;code&gt;exp_avg&lt;/code&gt; y &lt;code&gt;exp_avg_sq&lt;/code&gt;, mientras que SGD con momentum guarda uno y SGD a secas, ninguno. Lo he comprobado en el estado del optimizador de torch con el modelo de caracteres, y salen justo esos números. Para GPT-2 small en fp32 son 996 MB de estado del optimizador, junto a 498 MB de pesos y otros 498 MB de gradientes. Pongamos 2 GB antes de contar una sola activación del grafo de la parte 8.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Cómo se reparte la memoria en un paso de entrenamiento de GPT-2 small en fp32. Elige un optimizador y después ajusta el batch y la longitud de secuencia. Pesos y gradientes no cambian; las otras dos barras, sí.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-optimizers-schedules-lr-breaks-production/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Con AdamW, las tres barras que no se mueven con el batch suman los 2 GB de antes. Cambia a SGD y verás desaparecer un tercio del total. Luego sube el batch y mira qué le hacen las activaciones a ese mismo presupuesto.&lt;/p&gt;
&lt;h2 id=&quot;el-primer-paso-mide-exactamente-la-tasa-de-aprendizaje&quot;&gt;El primer paso mide exactamente la tasa de aprendizaje&lt;/h2&gt;
&lt;p&gt;Vuelve a mirar &lt;code&gt;step_size = lr / (1 - beta1 ** step)&lt;/code&gt;. En el primer paso, &lt;code&gt;exp_avg&lt;/code&gt; es el gradiente multiplicado por &lt;code&gt;1 - beta1&lt;/code&gt;, justo lo que divide la corrección. Al cancelarse los dos, la primera actualización se queda en el gradiente dividido por su propio tamaño, multiplicado por &lt;code&gt;lr&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Con &lt;code&gt;lr&lt;/code&gt; a 0,001, el desplazamiento mediano de los pesos en el primer paso fue de 0,001, exactamente la tasa de aprendizaje, fuera cual fuera el gradiente. El tamaño del gradiente no llega nunca a la actualización, y de él solo sobrevive el signo, peso a peso. A partir del segundo paso, lo que sobrevive es cada gradiente comparado con su propio tamaño reciente.&lt;/p&gt;
&lt;p&gt;Para que salga exacto hacen falta dos condiciones, y las dos dependen de la configuración. Un gradiente cercano a &lt;code&gt;eps&lt;/code&gt; (10⁻⁸ por defecto) lo estropea: con 10⁻⁹ el primer paso se queda en una décima parte de la tasa, y con 10⁻¹², en una diezmilésima. Además, el &lt;code&gt;AdamW&lt;/code&gt; de torch aplica por defecto un weight decay de 0,01 si no le pasas otro valor. De ahí que el primer paso más grande que he medido fuera de 0,00101, apenas por encima de la tasa.&lt;/p&gt;
&lt;p&gt;Para eso está el warmup. Si la tasa arranca cerca de cero y sube de forma lineal durante los primeros cientos de pasos, esas primeras actualizaciones, que miden exactamente la tasa, salen pequeñas. El schedule de nanoGPT, en &lt;code&gt;train.py:231-242&lt;/code&gt;, cabe en ocho líneas.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:78ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; it &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; warmup_iters:                       &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# 1) linear warmup&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; learning_rate &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (it &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (warmup_iters &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; it &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; lr_decay_iters:                     &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# 2) then a floor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; min_lr&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;decay_ratio &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (it &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; warmup_iters) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (lr_decay_iters &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; warmup_iters)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;assert&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;&amp;lt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; decay_ratio &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;&amp;lt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;coeff &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0.5&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1.0&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; math.cos(math.pi &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; decay_ratio))   &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# 3) cosine in between&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; min_lr &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; coeff &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (learning_rate &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; min_lr)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Con 100 pasos de warmup, la primera actualización se quedó en 0,0000099 frente a 0,001 (cien veces menor) y a partir de ahí fue creciendo. Pasado el warmup, el resto del schedule va bajando la tasa hasta una décima parte del pico, que es lo que fija el &lt;code&gt;min_lr&lt;/code&gt; de nanoGPT en &lt;code&gt;train.py:68&lt;/code&gt;. El único &lt;code&gt;assert&lt;/code&gt; del schedule comprueba la aritmética del cociente, y el valor que has configurado ni lo mira.&lt;/p&gt;
&lt;h2 id=&quot;qué-dice-el-barrido&quot;&gt;Qué dice el barrido&lt;/h2&gt;
&lt;p&gt;He entrenado el modelo de caracteres durante 600 pasos con tres schedules (ninguno, 100 pasos de warmup y warmup más el decaimiento coseno) y siete tasas de aprendizaje para cada uno, de 0,0001 a 0,1. Los gradientes iban recortados a 1,0 en todo momento, como hace nanoGPT. El barrido no puede separar el warmup del recorte, y el recorte ya estaba haciendo su parte. Con 0,003 y sin warmup, quitar el recorte llevó la puntuación de 2,18 a 2,38 y dejó que la norma del gradiente llegara a 79. Como referencia, adivinar solo por frecuencia da 3,23 en el texto de test.&lt;/p&gt;
&lt;p&gt;Ninguna ejecución dio un NaN, ni siquiera con una tasa de aprendizaje diez veces mayor que la mejor.&lt;/p&gt;
&lt;p&gt;La mejor ejecución sacó 1,73 con warmup, coseno y una tasa de 0,01. La misma tasa dio 1,84 solo con warmup y 2,61 sin ninguno de los dos. Sin warmup, ninguna tasa bajó del 2,02 que sacó la de 0,001. El warmup, por tanto, no rescató ninguna ejecución rota. Hizo viable una tasa diez veces mayor, y en esa tasa estaba la buena puntuación.&lt;/p&gt;
&lt;p&gt;Con 0,1 constante, la pérdida se estancó cerca de 3,0, apenas mejor que adivinar por frecuencia de letras. Bajar, bajó, pero luego dejó de bajar. Ese valor solo te parecería malo si supieras lo que hace el mismo modelo con 0,01. Con el coseno, que baja la tasa hasta 0,01, la ejecución de 0,1 siguió mejorando hasta 2,71, aunque se quedó a casi un nat de la mejor.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Pérdida de entrenamiento del mismo modelo pequeño de caracteres durante 600 pasos (en escala logarítmica), con siete tasas de aprendizaje y tres schedules. Elige un schedule y después arrastra la tasa. Las dos líneas horizontales son puntuaciones en test: la de adivinar por frecuencia de letras y la mejor de las 21 ejecuciones. «Pérdida duplicada» es la regla para abortar, y marca el primer paso cuya pérdida superó el doble de la mejor hasta entonces.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-optimizers-schedules-lr-breaks-production/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Empieza en 0,01 y pulsa &lt;code&gt;sin warmup&lt;/code&gt;, que también quita el coseno, y verás que la puntuación en test sube de 1,73 a 2,61. Luego arrastra hasta 0,1 y mira cómo la curva se aplana sin llegar a explotar, con el indicador de NaN todavía en «nunca».&lt;/p&gt;
&lt;p&gt;Para ser justos con lo que da de sí un portátil, un modelo de cuatro bloques entrenado en él no es un preentrenamiento. Como cada cifra de aquí sale de una sola semilla, por debajo de unos 0,05 nats no hay nada que leer. A escala real, una tasa mala sí produce el pico de pérdida y el NaN. Lo que se transfiere es la forma: la misma tasa que va bien con warmup desperdicia el entrenamiento sin él, y el fallo que tienes que saber detectar no es que la ejecución se caiga.&lt;/p&gt;
&lt;h2 id=&quot;el-weight-decay-y-dónde-ponerlo&quot;&gt;El weight decay y dónde ponerlo&lt;/h2&gt;
&lt;p&gt;El weight decay encoge los pesos hacia cero. La forma original consiste en sumar &lt;code&gt;weight_decay * param&lt;/code&gt; al gradiente, con lo que ese término pasa por la división de Adam y se reescala peso a peso como todo lo demás. El &lt;code&gt;param.mul_(1 - lr * weight_decay)&lt;/code&gt; de AdamW lo saca de ese camino, y de ahí viene la W.&lt;/p&gt;
&lt;p&gt;La diferencia salta a la vista. Con el mismo entrenamiento de 400 pasos y un weight decay de 0,1 en todos los parámetros, la versión desacoplada sacó 2,22 en el texto de test y la acoplada 3,03, apenas mejor que adivinar por frecuencia. La acoplada terminó con una norma de los pesos (la longitud de todos ellos tomados como un solo vector) de 21, frente a 41. Reescalado por Adam, el weight decay fue lo bastante fuerte para vaciar el modelo.&lt;/p&gt;
&lt;p&gt;Hay dos cosas que decir de esa cifra. La primera es que la misma ejecución sin weight decay sacó 2,21. En 400 pasos, el weight decay desacoplado no aportó nada y toda la diferencia es el daño que hace el otro camino. La segunda es que aplicar weight decay a todos los parámetros probablemente empeora el caso acoplado, porque la parte del gradiente que encoge los pesos cae también sobre las escalas de normalización, que empiezan en 1,0 y en la inicialización concentran la mayor parte de la norma de los pesos.&lt;/p&gt;
&lt;p&gt;nanoGPT aplica el weight decay de forma selectiva en &lt;code&gt;model.py:269-274&lt;/code&gt;, donde lo llevan las matrices pero no los sesgos ni las escalas de LayerNorm. Son dos líneas de filtrado, y saltárselas no da ningún error.&lt;/p&gt;
&lt;h2 id=&quot;qué-vigila-el-ingeniero-de-ml&quot;&gt;Qué vigila el ingeniero de ML&lt;/h2&gt;
&lt;p&gt;Son cuatro hábitos, y los dos primeros consisten solo en guardar números.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Registra la tasa de aprendizaje efectiva: con la configurada no basta.&lt;/strong&gt; El número que importa es el que ha devuelto el schedule en este paso. Un schedule que se reinició al reanudar repitió el warmup, arrancó 70 veces por debajo de la tasa de una reanudación completa y acabó la ejecución más de tres veces por encima, mientras el fichero de configuración decía lo mismo en las dos ejecuciones. En los mismos 150 pasos sacó 2,275 frente a 2,230, dentro del ruido de una sola semilla. Solo la tasa registrada lo habría delatado.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Guarda en el checkpoint el estado del optimizador, además de los pesos.&lt;/strong&gt; En GPT-2 small, las dos medias de AdamW ocupan 996 MB, y si las tiras, el promediado vuelve a empezar sin avisar. Sobre el mismo batch, el primer paso tras reanudar dio 2,64 frente a 2,38 con una reanudación completa. Mi ejecución se recuperó en 150 pasos hasta quedar a 0,007 de la reanudación completa, de modo que a esta escala el estado no valía casi nada. La razón para guardarlo está en el transitorio, y no he probado una tasa lo bastante grande como para que resultara fatal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dimensiona la máquina con 16 bytes por parámetro, sin contar las activaciones.&lt;/strong&gt; En fp32, pesos, gradientes y las dos medias de AdamW ocupan cuatro bytes por parámetro cada uno, así que si dimensionas solo por los pesos te quedas corto en tres cuartas partes. La precisión mixta no rebaja la suma: pesos y gradientes de 2 bytes, más una copia maestra de 4 bytes y las dos medias, siguen sumando 16 bytes. En ese caso, si dimensionas solo por los pesos, te quedas corto en siete octavos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Haz el barrido de la tasa en un modelo pequeño y fija una regla para abortar.&lt;/strong&gt; El modelo pequeño te da la forma, pero el valor no se transfiere, porque la mejor tasa baja a medida que crece el modelo, salvo que uses algo como &lt;a href=&quot;https://arxiv.org/abs/2203.03466&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;μP&lt;/a&gt;. Como nada lanza una excepción, la regla tiene que ponerla tú. La mía saltaba cuando la pérdida llegaba al doble de su mejor valor, y lo hizo en cuatro de las 21 ejecuciones. Tres eran de 0,1; la cuarta siguió adelante y terminó bien. Una regla tan barata matará de vez en cuando una ejecución buena, y ese es el precio de pillar las malas en los diez primeros pasos.&lt;/p&gt;
&lt;p&gt;Esto tampoco es trabajo de modelado. Son cinco líneas de aritmética, un schedule de ocho líneas y un fichero de configuración que nada valida. Entre mi mejor ejecución y la peor hubo 1,3 nats con el mismo modelo, los mismos datos y la misma semilla, y lo único que cambiaba eran tres números de la configuración.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 9 de 12. Adam da a cada peso su propio tamaño de paso a partir de sus gradientes recientes, y por eso un factor constante en la pérdida se cancela y el primer paso mide exactamente la tasa de aprendizaje. El warmup y el decaimiento coseno caben en ocho líneas y el weight decay desacoplado en una. Nada de eso se comprueba al arrancar, y a escala pequeña el fallo es una ejecución que deja de mejorar sin hacer ruido. La siguiente parte trata de lo que pasa cuando partes de pesos que ha entrenado otro. Si has llegado aquí directamente, puedes empezar por &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;el tokenizador&lt;/a&gt; y seguir con &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;la tabla a la que apunta el id de un token&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/&quot;&gt;las formas&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-attention-learned-weighted-average/&quot;&gt;la atención&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-transformer-six-shapes-residual-stream/&quot;&gt;el bloque&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-transformer-plus-five-decisions/&quot;&gt;GPT-2 y sus dos programas&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-to-llama-what-changed/&quot;&gt;lo que cambió Llama&lt;/a&gt; y &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-loss-gradients-graph-not-formulas/&quot;&gt;el origen de los gradientes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><category>Fundamentos de ML</category><category>Fundamentos de ML</category><category>From code to weights</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>Pérdida y gradientes sin fórmulas: la retropropagación es recorrer un grafo</title><link>https://cchinchilla.dev/es/posts/fcw-loss-gradients-graph-not-formulas/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/fcw-loss-gradients-graph-not-formulas/</guid><description>La retropropagación recorre un grafo hacia atrás aplicando la regla de la cadena. Leída como código, explica por qué los gradientes se desvanecen o explotan y por qué un bug en el gradiente se queda en una curva algo torcida, sin stack trace.</description><pubDate>Tue, 20 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 7 acabó con cuatro cambios en un bloque, cada uno grabado en los pesos, y todos esos pesos se aprendieron. Esta parte cuenta cómo: hace falta un único número que diga cuánto se equivoca el modelo y, para cada peso, saber hacia dónde moverlo para que ese número baje. Esa dirección, peso a peso, es el gradiente.&lt;/p&gt;
&lt;p&gt;He leído el &lt;code&gt;engine.py&lt;/code&gt; de micrograd y lo he contrastado con torch. micrograd, que Andrej Karpathy escribió para enseñar diferenciación automática, hace todo el trabajo con números sueltos en 94 líneas. Después he entrenado un modelo pequeño de cinco maneras, cuatro de ellas mal, y ninguna de las malas ha dado error.&lt;/p&gt;
&lt;h2 id=&quot;la-pérdida-es-la-sorpresa-media&quot;&gt;La pérdida es la sorpresa media&lt;/h2&gt;
&lt;p&gt;Un modelo de lenguaje devuelve una probabilidad para cada token del vocabulario. La pérdida (la entropía cruzada) solo se fija en el token que venía a continuación en el texto y toma el logaritmo de su probabilidad cambiado de signo. Si el modelo le da probabilidad 1, cuesta 0; si le da un 1 %, cuesta 4,6. A ese coste lo llamo sorpresa, y se mide en nats porque el logaritmo es en base e, que vale unos 2,718.&lt;/p&gt;
&lt;p&gt;He pasado 1024 tokens de una entrada anterior por GPT-2 small y la sorpresa media ha salido 3,595 (el 3,60 que daba la parte 5). Si elevas e a esa potencia sale 36, es decir, de media el modelo dudaba tanto como si eligiera al azar entre 36 tokens. Una predicción uniforme entre 50 257 tokens da 10,8.&lt;/p&gt;
&lt;p&gt;La media esconde una distribución asimétrica, con una forma que conoce bien quien haya vigilado alguna vez una latencia p99. El token mediano costó 2,85 y la peor décima parte de los tokens se llevó el 28 % de la pérdida. Los más sorprendentes eran nombres: &lt;code&gt;&amp;quot; Claude&amp;quot;&lt;/code&gt; después de «renaming the assistants alone drops» costó 16,0. Entre los que menos sorprendieron estaban los trozos de una URL, como «org» después de «arxiv.», que costó una diezmilésima de nat.&lt;/p&gt;
&lt;p&gt;Al entrenar, esa sorpresa se promedia sobre todas las posiciones de todas las secuencias de un batch hasta quedar en un solo número. Todo lo que sigue trata de cómo volver desde ese número hasta los pesos.&lt;/p&gt;
&lt;h2 id=&quot;la-pasada-hacia-delante-construye-un-grafo&quot;&gt;La pasada hacia delante construye un grafo&lt;/h2&gt;
&lt;p&gt;micrograd envuelve cada número en un &lt;code&gt;Value&lt;/code&gt; que recuerda de dónde viene. Así es la multiplicación, condensada de &lt;code&gt;engine.py:24-33&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:71ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;__mul__&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(self, other):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;out &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Value(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.data &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; other.data, (&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, other), &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;#39;*&amp;#39;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#DCBDFB&quot;&gt;_backward&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;():&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.grad &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; other.data &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; out.grad    &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# d(out)/d(self) is other&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;other.grad &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.data &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; out.grad    &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# d(out)/d(other) is self&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;out._backward &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; _backward&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; out&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Cada operación hace dos cosas: calcula su resultado y deja tras de sí un nodo que conoce sus entradas y un closure que conoce su propia derivada local. La pasada hacia delante calcula una respuesta y, como efecto secundario, construye un grafo. Si alguna vez has escrito un tracer que registra cada llamada enlazada a las llamadas que le dieron sus datos, ya tienes hecha la mitad.&lt;/p&gt;
&lt;p&gt;Una expresión pequeña, &lt;code&gt;relu(a·b + c)·a&lt;/code&gt;, construye 7 nodos, y una red de 41 parámetros evaluada con cuatro ejemplos, 394. torch hace lo mismo con tensores. Cada nodo guarda lo que vaya a necesitar en la pasada hacia atrás. En GPT-2 small, una secuencia de 1024 tokens deja unos 820 MB de tensores guardados, más que los 498 MB que ocupan los pesos del modelo. Con un batch de ocho son 6,5 GB.&lt;/p&gt;
&lt;h2 id=&quot;la-pasada-hacia-atrás-desanda-el-grafo&quot;&gt;La pasada hacia atrás desanda el grafo&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Value.backward&lt;/code&gt;, condensado de &lt;code&gt;engine.py:54-70&lt;/code&gt;, es todo el algoritmo.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:77ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;topo, visited &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; [], &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#DCBDFB&quot;&gt;build_topo&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(v):                     &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# depth-first: children before parents&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; v &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;not&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; visited:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;visited.add(v)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; child &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; v._prev:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:12ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;build_topo(child)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;topo.append(v)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;build_topo(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.grad &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; v &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;reversed&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(topo):               &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# the output first, the inputs last&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;v._backward()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Hace una ordenación topológica y luego recorre la lista al revés. Ese orden garantiza que cada nodo haya reunido todas sus contribuciones antes de transmitir nada. El closure de cada nodo coge el gradiente que le ha llegado, lo multiplica por su derivada local y suma el resultado a sus entradas. Eso es la regla de la cadena hecha bucle, y no hay nada más. Si has escrito un sistema de build que recorre un grafo de dependencias, esto es el mismo recorrido hacia atrás: ningún paso se ejecuta hasta que todos los que dependen de él han entregado su parte.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: El grafo que construye micrograd para relu(a·b + c)·a con a = 2, b = −3 y c = 10. Recorre la pasada hacia atrás paso a paso: cada nodo muestra su valor y el gradiente que tiene hasta ese momento.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-loss-gradients-graph-not-formulas/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Recorre la pasada hacia atrás nodo a nodo y después cambia a &lt;code&gt;grad =&lt;/code&gt; para ver cómo &lt;code&gt;a&lt;/code&gt; pierde una de sus dos contribuciones.&lt;/p&gt;
&lt;p&gt;Todo depende de ese &lt;code&gt;+=&lt;/code&gt;. En la expresión de arriba &lt;code&gt;a&lt;/code&gt; se usa dos veces, así que le llega gradiente desde dos nodos. Con &lt;code&gt;+=&lt;/code&gt; recibe −2, que es lo que da torch; con &lt;code&gt;=&lt;/code&gt;, la segunda escritura machaca la primera y &lt;code&gt;a&lt;/code&gt; se queda con −6. No salta ningún error y el número, sencillamente, está mal. En la red de 41 parámetros, los gradientes de micrograd y los de torch se diferenciaron como mucho en 0,000007.&lt;/p&gt;
&lt;p&gt;torch acumula igual, en el &lt;code&gt;.grad&lt;/code&gt; de cada peso, hasta que algo lo pone a cero. nanoGPT, el GPT-2 mínimo que esta serie viene leyendo, usa esa acumulación a propósito. En &lt;code&gt;train.py:292-305&lt;/code&gt; procesa 40 batches pequeños en una GPU, divide cada pérdida entre 40 y llama 40 veces a &lt;code&gt;backward&lt;/code&gt; sobre los mismos buffers antes de hacer una sola actualización. Luego &lt;code&gt;optimizer.zero_grad&lt;/code&gt;, en &lt;code&gt;train.py:314&lt;/code&gt;, los vacía.&lt;/p&gt;
&lt;h2 id=&quot;con-cadenas-largas-el-gradiente-se-desvanece-o-explota&quot;&gt;Con cadenas largas, el gradiente se desvanece o explota&lt;/h2&gt;
&lt;p&gt;En cualquier camino de la pérdida a un peso, la regla de la cadena multiplica entre sí las derivadas locales, una por nodo. Las redes profundas tienen caminos largos. El producto de muchos números un poco por debajo de 1 es diminuto, y el de muchos un poco por encima, enorme. Eso es, en pura aritmética, el desvanecimiento y la explosión del gradiente, y para verlo no hace falta saber cálculo.&lt;/p&gt;
&lt;p&gt;He montado pilas de capas tanh de 256 de ancho y sin conexión residual; tanh aplasta cualquier valor al intervalo entre −1 y 1. Lo único que cambiaba de una pila a otra era cuánto escalaba cada capa su entrada. Con una escala de 0,5 y 30 capas, el gradiente que llegaba a la primera era 7 × 10⁻⁹ veces el de la última, y con 60 capas, unas 10⁻¹⁷ veces. Con una escala de 2 y 60 capas, la primera recibía unas 7700 veces más.&lt;/p&gt;
&lt;p&gt;El flujo residual de la parte 5, el bus del que lee cada bloque y al que suma su resultado, resuelve la mitad del problema, la del desvanecimiento. Si cada capa es residual, &lt;code&gt;x + layer(x)&lt;/code&gt;, todas tienen un camino directo de vuelta a la pérdida. En las mismas pilas, la primera capa no recibió nunca menos gradiente que la última, aunque con 60 capas y escala 2 seguía recibiendo 992 veces más. GPT-2 small está construido así, y al medirlo con una secuencia ningún bloque recibió un gradiente raquítico ni desbocado.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Cuánto gradiente recibe cada una de las capas tanh de la pila, en relación con la última y en escala logarítmica. Elige una profundidad y un tipo de pila y luego arrastra el control para cambiar cuánto escala cada capa su entrada.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-loss-gradients-graph-not-formulas/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Elige 60 capas, pon la escala en 0,5 y verás cómo se desvanece el gradiente de la pila simple. Si después pasas a la residual, con 0,5 no se desvanece nada y con 2 la primera capa sigue recibiendo unas mil veces más.&lt;/p&gt;
&lt;p&gt;Para eso está el recorte del gradiente. &lt;code&gt;clip_grad_norm_&lt;/code&gt; mide la longitud del gradiente entero, con todos los pesos a la vez, y si pasa de un límite lo reduce todo en la misma proporción. nanoGPT pone el límite en 1,0 (&lt;code&gt;train.py:63&lt;/code&gt;) y, en mi secuencia, el gradiente de GPT-2 small medía 9,76. Casi todo estaba en la tabla de tokens, que GPT-2 reutiliza como capa de salida, y el lado de la entrada se llevó 0,14. Una vez recortado medía 1,0. El recorte acorta el gradiente, pero nunca cambia su dirección.&lt;/p&gt;
&lt;p&gt;La precisión trae su propio desvanecimiento. fp16, el formato de coma flotante de 16 bits, no puede representar un número positivo por debajo de unos 6 × 10⁻⁸, y en ese formato el 9,3 % de los valores del gradiente de GPT-2 se redondeaba a cero. Con bf16, un formato de 16 bits que conserva el rango de fp32, no se perdió ninguno.&lt;/p&gt;
&lt;p&gt;Para fp16, el arreglo es multiplicar por un factor grande antes de la pasada hacia atrás. Cuando entrena en fp16, nanoGPT activa un &lt;code&gt;GradScaler&lt;/code&gt; (&lt;code&gt;train.py:196&lt;/code&gt;) cuyo factor empieza en 65 536. En mis gradientes, ese factor rescató todos menos 1743 de esos 11,6 millones de valores y empujó 11 de los más grandes por encima de 65 504, el techo de fp16. Por eso el &lt;code&gt;GradScaler&lt;/code&gt; vuelve a dividir por el factor antes de la actualización y, cuando algo desborda, se salta esa actualización y divide el factor entre dos.&lt;/p&gt;
&lt;h2 id=&quot;bugs-sin-stack-trace&quot;&gt;Bugs sin stack trace&lt;/h2&gt;
&lt;p&gt;En la mayoría de los programas, un bug detiene la ejecución en una línea concreta. Un gradiente equivocado no detiene nada y se limita a entrenar un modelo ligeramente distinto. He entrenado con el texto de este blog un modelo de caracteres de cuatro bloques, un GPT pequeño que lee un carácter por token. Han sido cinco ejecuciones de 1000 pasos, una correcta y cuatro con un bug distinto cada una, y todas las he puntuado con texto que el modelo no vio durante el entrenamiento. Adivinar letras solo por su frecuencia da 3,25 y la ejecución correcta ha llegado a 1,74.&lt;/p&gt;
&lt;p&gt;Dos de los bugs se vieron en la curva. Sin &lt;code&gt;zero_grad&lt;/code&gt;, los gradientes se fueron amontonando de un paso a otro. La pérdida bajó a 3,37 en el paso 50 y después subió hasta un 7,37 que ya era peor que una predicción uniforme entre 106 caracteres. Con los objetivos sin desplazar, al modelo se le pedía el carácter que acababa de recibir, con lo que la pérdida de entrenamiento bajó a 0,001 mientras la predicción real del carácter siguiente daba 10,7.&lt;/p&gt;
&lt;p&gt;Los otros dos no se vieron. Con la pérdida multiplicada por 40, el bug que aparece si olvidas la división de nanoGPT, el modelo quedó tan bien como el correcto, con 1,735. AdamW, el optimizador de nanoGPT, reescala el paso de cada peso, así que un factor constante se cancela (la parte 9 explica por qué). La tabla de posiciones desconectada del grafo, a la que por eso nunca llegó gradiente, no aprendió nada y se quedó en 1,95 (un 12 % peor) con una curva que parece normal.&lt;/p&gt;
&lt;p&gt;Para ser justos con lo que puede decir una curva, dos de los cuatro bugs los habría visto cualquiera que mirase la pérdida de entrenamiento, uno de ellos porque la pérdida salía demasiado buena. Los otros dos dejaron una curva que aceptarías, así que quien solo registra la pérdida caza la mitad.&lt;/p&gt;
&lt;h2 id=&quot;qué-vigila-el-ingeniero-de-ml&quot;&gt;Qué vigila el ingeniero de ML&lt;/h2&gt;
&lt;p&gt;Son cuatro hábitos, y ninguno exige saber cálculo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Registra la norma del gradiente por bloque.&lt;/strong&gt; Es lo más parecido a un stack trace que tiene un entrenamiento. Ahí se ve un pico, un bloque atascado cerca de cero o un total que sube poco a poco a lo largo de miles de pasos. La tabla de posiciones desconectada habría salido con gradiente cero desde el primer paso. Registra la norma antes del recorte (&lt;code&gt;clip_grad_norm_&lt;/code&gt; devuelve el total sin recortar), porque después el total nunca pasa del límite y el pico de cada bloque queda reducido con él.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evalúa con una tarea en la que la pérdida de entrenamiento no pueda hacer trampa.&lt;/strong&gt; La ejecución con los objetivos sin desplazar fue la que tuvo la mejor pérdida de entrenamiento de las cinco. Calcula una puntuación sobre texto de test, el que el modelo nunca ve al entrenar, con código que no comparta el camino de datos del bucle de entrenamiento, para que un mismo bug no pueda esconderse en ambos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ten claro qué bugs te esconde el optimizador.&lt;/strong&gt; Con AdamW, multiplicar la pérdida por 40 no cambió nada. Con SGD a secas, el mismo bug equivale a una tasa de aprendizaje 40 veces más alta de la cuenta. Comprueba el escalado de la pérdida directamente, con un batch y un gradiente conocido.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Calcula la memoria de las activaciones, además de la de los pesos.&lt;/strong&gt; GPT-2 small guarda unos 820 MB por cada secuencia de 1024 tokens para la pasada hacia atrás. Con el checkpointing de activaciones, que recalcula cada bloque durante esa pasada para no tener que guardar sus tensores, un batch de ocho pasó de 6,5 GB a 2,0 GB, a cambio de un 17 % más de tiempo en la CPU de mi portátil. La mayor parte de lo que queda son 1,65 GB de log-probabilidades, una por cada uno de los 50 257 tokens en cada posición.&lt;/p&gt;
&lt;p&gt;Esto tampoco es trabajo de modelado. La retropropagación es una ordenación y un bucle sobre closures, y cada uno de los bugs de arriba es un error de contabilidad dentro de ese bucle o a su alrededor. Como no puede fallar haciendo ruido, el ruido lo tiene que hacer el registro.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 8 de 12. La pérdida es la sorpresa media ante el token correcto. La pasada hacia delante construye un grafo de operaciones y guarda lo que va a necesitar cada una, y la pasada hacia atrás lo desanda multiplicando derivadas locales y sumándolas a las entradas. Los productos largos se desvanecen o explotan salvo que el bus dé a cada capa un camino directo, y los bugs de esa contabilidad entrenan en silencio sin que nada se caiga. La siguiente parte cuenta qué hace el optimizador con el gradiente. Si has llegado aquí directamente, puedes empezar por &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;el tokenizador&lt;/a&gt; y seguir con &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;la tabla a la que apunta el id de un token&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/&quot;&gt;las formas&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-attention-learned-weighted-average/&quot;&gt;la atención&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-transformer-six-shapes-residual-stream/&quot;&gt;el bloque&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-transformer-plus-five-decisions/&quot;&gt;GPT-2 y sus dos programas&lt;/a&gt; y &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-to-llama-what-changed/&quot;&gt;lo que cambió Llama&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><category>Fundamentos de ML</category><category>Fundamentos de ML</category><category>From code to weights</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>De GPT-2 a Llama: cuatro cambios y el motivo de cada uno</title><link>https://cchinchilla.dev/es/posts/fcw-gpt2-to-llama-what-changed/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/fcw-gpt2-to-llama-what-changed/</guid><description>Posiciones rotatorias, RMSNorm, feed-forward con puerta y grouped-query attention son cuatro retoques locales que se justifican más con mediciones que con teoría, y cada uno queda grabado en los pesos.</description><pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 6 acababa con las cinco decisiones de GPT-2 y el bucle que lo sirve. Si abres un modelo de esta década, el bloque sigue siendo el de la parte 5, con dos etapas que leen cada una su copia normalizada del bus y le suman el resultado. Por dentro, eso sí, han cambiado cuatro cosas.&lt;/p&gt;
&lt;p&gt;He leído &lt;code&gt;modeling_llama.py&lt;/code&gt; de transformers 5.17 comparándolo con el &lt;code&gt;model.py&lt;/code&gt; de nanoGPT y he cargado SmolLM2-135M, un modelo con la arquitectura de Llama y el tamaño de GPT-2 small. Después he ido deshaciendo cada cambio sobre sus pesos entrenados y midiendo qué se rompía.&lt;/p&gt;
&lt;h2 id=&quot;el-mismo-bloque-cuatro-retoques&quot;&gt;El mismo bloque, cuatro retoques&lt;/h2&gt;
&lt;p&gt;Resumido, &lt;code&gt;LlamaDecoderLayer.forward&lt;/code&gt; (&lt;code&gt;modeling_llama.py:305-323&lt;/code&gt;) queda así:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:87ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;residual &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; hidden_states&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;hidden_states &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.input_layernorm(hidden_states)            &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# RMSNorm, not LayerNorm&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;hidden_states, _ &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.self_attn(hidden_states, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;position_embeddings&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;hidden_states &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; residual &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; hidden_states                       &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# the bus, as in GPT-2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;residual &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; hidden_states&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;hidden_states &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.mlp(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.post_attention_layernorm(hidden_states))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;hidden_states &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; residual &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; hidden_states&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Son las mismas cuatro líneas de la parte 5, solo que con más nombres. Las sumas siguen donde estaban, y también el pre-LN, es decir, la norma que se aplica antes de cada etapa y no después. Lo que ha cambiado está dentro de las llamadas: cómo entran las posiciones en la atención, qué calcula la norma, qué forma tiene la red feed-forward y cuántas keys y values guarda la atención. (Llama, además, quita los vectores de sesgo de sus proyecciones lineales, aunque nada de lo que viene después depende de ellos.)&lt;/p&gt;
&lt;p&gt;La comparación entre los dos modelos es justa. GPT-2 small tiene 124 millones de parámetros repartidos en 12 bloques de anchura 768, y SmolLM2-135M, 135 millones en 30 bloques de anchura 576. Ninguno de los cuatro cambios tiene que ver con el tamaño, así que podrías construir cualquiera de los dos modelos con ellos o sin ellos.&lt;/p&gt;
&lt;h2 id=&quot;las-posiciones-pasan-a-ser-una-rotación&quot;&gt;Las posiciones pasan a ser una rotación&lt;/h2&gt;
&lt;p&gt;GPT-2 suma a cada token una fila aprendida, distinta para cada posición, y la tabla se acaba en 1024. Llama no tiene tabla de posiciones, y lo que hace es girar la query y la key (&lt;code&gt;modeling_llama.py:158-159&lt;/code&gt;).&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:44ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;q_embed &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (q &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; cos) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (rotate_half(q) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; sin)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;k_embed &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (k &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; cos) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (rotate_half(k) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; sin)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Es el embedding posicional rotatorio, RoPE. Como se vio en la parte 4, la atención puntúa un par de tokens comparando un vector query con un vector key. Cada query y cada key se divide en cabezas de 64 números, y el vector de cada cabeza se lee como 32 pares. Cada par es un punto sobre una circunferencia y gira, como la aguja de un reloj, un ángulo proporcional a la posición. &lt;code&gt;rotate_half&lt;/code&gt; es la manera que tiene el código de escribir ese giro sin una matriz. Las velocidades son distintas para cada par y todas salen de un solo número de la configuración, la base de rotación, que en SmolLM2 vale 100 000. Con esa base, el par más rápido da una vuelta cada 6,3 posiciones y el más lento, una cada 438 000.&lt;/p&gt;
&lt;p&gt;La rotación no está de adorno. Si giras una query según la posición &lt;code&gt;m&lt;/code&gt; y una key según la posición &lt;code&gt;n&lt;/code&gt;, su producto escalar (la puntuación de la parte 4) depende solo de &lt;code&gt;m − n&lt;/code&gt;. He pasado una query y una key aleatorias por la rotación de SmolLM2. La puntuación ha dado −13,4724 en las posiciones 10 y 3, y −13,4723 en 1010 y 1003. Con una posición menos de distancia, en 10 y 4, ha dado −10,786.&lt;/p&gt;
&lt;p&gt;El modelo entero hereda esa propiedad. He sumado 500 a todos los ids de posición de un texto de 512 tokens, y después 5000. La pérdida de SmolLM2 sobre ese texto (su sorpresa media ante el token siguiente) ha sido de 3,381 sin desplazar y de 3,381 tras cada desplazamiento. A GPT-2, el mismo desplazamiento de 500 lo ha llevado de 3,85 a 8,97 porque cada token pasa a leer otra fila de su tabla. Al quitar la rotación del todo, con todas las posiciones a 0, SmolLM2 ha pasado de 3,12 a 6,28 en el texto completo de 1024 tokens. Y bajar la base de rotación de los 100 000 de SmolLM2 a 10 000, el valor del artículo original de RoPE y de Llama 2, ha costado un 3 %.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Un par de números de una query y el mismo par de una key, cada uno girado según su posición. Elige dónde están la query y la key y luego desplaza las dos lo mismo. La puntuación es lo que aporta este par al producto escalar, con flechas de longitud 1.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-to-llama-what-changed/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Arrastra la posición de la key para ver cómo cambia la puntuación, y después desplaza las dos para comprobar que no se mueve. Si eliges el par 17, el más lento de los tres, las posiciones cercanas apenas se distinguen. Aquí la puntuación es solo lo que aporta un par, y va de −1 a 1.&lt;/p&gt;
&lt;p&gt;Y sin tabla tampoco hay última fila. La configuración de Llama 3.1 8B registra una longitud original de 8192 posiciones y un máximo de 131 072; el modelo llega hasta ahí estirando la rotación. Sus cabezas tienen 128 números, o sea, 64 pares. Los 29 más lentos giran 8 veces más despacio, los 29 más rápidos se quedan igual y los seis intermedios mezclan los dos tratamientos. Meta, además, &lt;a href=&quot;https://arxiv.org/abs/2407.21783&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;preentrenó la familia&lt;/a&gt; con longitudes crecientes hasta 128K, en seis etapas en el caso del modelo de 405B. No hay ningún assert que salte: pasada la longitud con la que se entrenó un modelo, la calidad hay que medirla, porque nada la garantiza.&lt;/p&gt;
&lt;h2 id=&quot;la-norma-deja-de-centrar&quot;&gt;La norma deja de centrar&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;LayerNorm&lt;/code&gt; resta a cada fila su media, la divide por su desviación típica y después la escala y la desplaza. &lt;code&gt;LlamaRMSNorm&lt;/code&gt;, en &lt;code&gt;modeling_llama.py:62-67&lt;/code&gt;, se salta la media y el desplazamiento y divide por la media cuadrática (RMS), que se calcula elevando al cuadrado cada número, promediando y sacando la raíz. Si alguna vez has estandarizado una columna de un dataframe, has restado y has dividido; RMSNorm se queda solo con la división.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:77ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;variance &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; hidden_states.pow(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;).mean(&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;keepdim&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;hidden_states &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; hidden_states &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; torch.rsqrt(variance &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.variance_epsilon)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.weight &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; hidden_states.to(input_dtype)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;RMSNorm apuesta a que la media es pequeña de por sí, y en SmolLM2 lo es. En una capa de norma típica, la media de una fila típica es el 2 % de su media cuadrática, y en la peor capa, el 4,5 %. Si vuelves a poner la LayerNorm completa con la misma escala aprendida, la pérdida sube menos de un 2 %, de 3,124 a 3,175.&lt;/p&gt;
&lt;p&gt;Para ser justos con el argumento a su favor, hay que ponerlo a prueba. &lt;a href=&quot;https://arxiv.org/abs/1910.07467&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;El artículo de RMSNorm&lt;/a&gt; la vende como más barata porque se ahorra el desplazamiento y calcula una estadística donde LayerNorm calcula dos. En la CPU de mi portátil, el &lt;code&gt;F.layer_norm&lt;/code&gt; de torch ha tardado entre 50 y 75 microsegundos sobre un tensor &lt;code&gt;[1, 1024, 576]&lt;/code&gt;, según la ejecución, y &lt;code&gt;F.rms_norm&lt;/code&gt;, entre 110 y 135. El profiler dice por qué: en CPU, &lt;code&gt;rms_norm&lt;/code&gt; sigue ejecutándose como una cadena de operaciones pequeñas (cuadrado, media, suma, raíz y dos multiplicaciones), mientras que &lt;code&gt;layer_norm&lt;/code&gt; es un único kernel optimizado, una sola rutina compilada que hace todo el trabajo.&lt;/p&gt;
&lt;p&gt;Con un solo hilo, en cambio, &lt;code&gt;layer_norm&lt;/code&gt; ha tardado unos 155 microsegundos y &lt;code&gt;rms_norm&lt;/code&gt;, unos 130, así que el orden se invierte. Menos aritmética no se traduce en menos tiempo hasta que alguien escriba el kernel, y en GPU no he medido.&lt;/p&gt;
&lt;h2 id=&quot;la-red-feed-forward-gana-una-puerta&quot;&gt;La red feed-forward gana una puerta&lt;/h2&gt;
&lt;p&gt;La red feed-forward de GPT-2 tiene dos matrices con una GELU en medio, y la de Llama, tres (&lt;code&gt;modeling_llama.py:175&lt;/code&gt;).&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:76ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;down_proj &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.down_proj(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.act_fn(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.gate_proj(x)) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.up_proj(x))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;La entrada pasa por dos proyecciones en paralelo. Una de ellas atraviesa SiLU, una activación suave parecida a GELU, y después multiplica a la otra elemento a elemento. Esa multiplicación hace de puerta. La combinación se llama SwiGLU y viene de un &lt;a href=&quot;https://arxiv.org/abs/2002.05202&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;artículo de 2020&lt;/a&gt; que la probó y publicó mejores resultados sin ofrecer ninguna teoría que los explicara.&lt;/p&gt;
&lt;p&gt;Con la misma anchura, tres matrices cuestan más que dos, así que la anchura se reduce. Si &lt;code&gt;d&lt;/code&gt; es la anchura del modelo, una red feed-forward convencional de anchura &lt;code&gt;4d&lt;/code&gt; tiene &lt;code&gt;8d²&lt;/code&gt; números sin contar los sesgos. Tres matrices de anchura &lt;code&gt;f&lt;/code&gt; tienen &lt;code&gt;3·d·f&lt;/code&gt;, y las dos cuentas coinciden con &lt;code&gt;f = 8d/3&lt;/code&gt;. SmolLM2 usa exactamente esa anchura (1536 son 8/3 de 576), y los 11 008 de Llama 2 7B salen de redondear 8/3 de 4096 al múltiplo de 256 superior.&lt;/p&gt;
&lt;p&gt;Con &lt;code&gt;8d/3&lt;/code&gt;, la puerta no altera el presupuesto: con cabezas de atención completas la red feed-forward sigue ocupando dos tercios del bloque. El 75 % de SmolLM2 se debe a que comparte keys y values entre cabezas de query, y de eso trata la sección siguiente. Llama 3.1 8B mueve el reparto por los dos lados, con una red feed-forward más ancha, &lt;code&gt;f = 3.5d&lt;/code&gt;, y una atención más pequeña porque comparte keys y values. Las dos cosas juntas dan el 81 % que citaba la parte 5 (176 de los 218 millones de parámetros de cada bloque). Solo con la red más ancha y cabezas de atención completas, sale el 72 %.&lt;/p&gt;
&lt;p&gt;La puerta importa. Si en SmolLM2 cambias SiLU por GELU, la pérdida pasa de 3,12 a 3,79. Si quitas del todo la multiplicación de la puerta, sube hasta 17,7 y supera con mucho los 10,8 de un modelo que reparte la probabilidad por igual entre los 49 152 tokens.&lt;/p&gt;
&lt;p&gt;Es también en la red feed-forward donde entra la mezcla de expertos, con muchas redes feed-forward por bloque y un router, una capa pequeña que elige unas pocas para cada token. Llama 3 siguió siendo denso, a diferencia de Llama 4 y de otras familias.&lt;/p&gt;
&lt;h2 id=&quot;se-comparten-keys-y-values&quot;&gt;Se comparten keys y values&lt;/h2&gt;
&lt;p&gt;GPT-2 tiene tantas cabezas de key y de value como de query. SmolLM2 tiene 9 cabezas de query y 3 cabezas key-value, y cada cabeza key-value sirve a un grupo de tres queries. Es como un índice que atiende tres consultas, con los mismos datos y preguntas distintas. En PyTorch puro, &lt;code&gt;repeat_kv&lt;/code&gt; (&lt;code&gt;modeling_llama.py:179-189&lt;/code&gt;) copia las tres cabezas de key y value hasta tener nueve antes de hacer las cuentas; el kernel de atención fusionado de torch puede ahorrarse la copia cuando no hay máscara de padding. A esto se le llama grouped-query attention, GQA. Llama 2 solo la usaba en sus dos tamaños más grandes, el de 70B y uno de 34B que no llegó a publicarse, y en Llama 3 la llevan todos los tamaños.&lt;/p&gt;
&lt;p&gt;En inferencia, compartir cabezas no es ninguna aproximación. He construido una copia de SmolLM2 con nueve cabezas completas de key y value, en la que cada cabeza repite la de su grupo, y he ejecutado los dos modelos. Los logits han coincidido hasta el último bit, y la copia tiene 147,8 millones de parámetros frente a 134,5. Lo que GQA sacrifica lo sacrifica en el entrenamiento, porque hay menos keys y values distintos que aprender.&lt;/p&gt;
&lt;p&gt;Donde se ahorra es en la caché de la parte 6. SmolLM2 guarda 11 520 números por token (la cifra la he sacado de la propia caché de transformers después de 256 tokens), y con cabezas completas guardaría 34 560. GPT-2 small, con 12 bloques frente a los 30 de SmolLM2, guarda 18 432.&lt;/p&gt;
&lt;p&gt;Con el tamaño de Llama 3.1 8B, esa proporción decide qué cabe. El modelo tiene 32 cabezas de query y 8 cabezas key-value, así que cada token cuesta 131 KB en bf16, el formato de 16 bits de la parte 3. Con el máximo de 131 072 posiciones, la caché de una sola secuencia ocupa 17,2 GB, frente a los 16,1 GB de los pesos, y con cabezas completas serían 68,7 GB.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Las dimensiones de GPT-2 small con los cambios de Llama activados de uno en uno. Activa o desactiva un cambio, o arrastra las cabezas key-value, y mira cómo quedan los parámetros, la parte del bloque que ocupa la red feed-forward, la caché y el límite de posiciones.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-to-llama-what-changed/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Empieza por GPT-2 small y activa los tres cambios de uno en uno. Verás que la parte que ocupa la red feed-forward apenas se mueve. Después baja las cabezas key-value hasta 1 y fíjate en la caché y en esa parte.&lt;/p&gt;
&lt;h2 id=&quot;qué-vigila-el-ingeniero-de-ml&quot;&gt;Qué vigila el ingeniero de ML&lt;/h2&gt;
&lt;p&gt;Son cuatro hábitos, y ninguno necesita una GPU.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recalcula la caché para cada modelo.&lt;/strong&gt; El número de parámetros no sirve para predecirla, porque la caché son &lt;code&gt;2 × bloques × cabezas key-value × anchura de cabeza&lt;/code&gt; números por token y las cabezas key-value son un campo de la configuración. SmolLM2 es más grande que GPT-2 small y aun así guarda tres octavos menos por token.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Porta la arquitectura antes que los pesos.&lt;/strong&gt; Cada uno de los cambios anteriores forma parte del contrato con el que se entrenaron los pesos. La activación, el esquema de posiciones con su base de rotación y su escalado, y el número de cabezas key-value son campos de la configuración. El tipo de norma, en cambio, viene con el nombre de la arquitectura. Si te equivocas en el número de cabezas, la carga falla porque las formas no encajan. Si aciertas el número pero te equivocas en la agrupación (repites la serie entera de cabezas compartidas cuando había que repetir cada una varias veces seguidas), no salta ninguna excepción, pero la pérdida ha subido por encima de 11. Lo demás falla con el mismo silencio. Una LayerNorm sin sesgo donde iba RMSNorm ha costado un 1,6 %, GELU donde iba SiLU, un 21 %, y la base de rotación equivocada, un 3 %. Con los pares de rotación formados por números contiguos, cuando el código los forma por mitades, la pérdida ha llegado a 7,24. Si portas un modelo a mano, compara tus logits con los de una implementación de referencia antes de fiarte de él.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prueba las longitudes a las que sirves el modelo.&lt;/strong&gt; La rotación quita el límite fijo, pero la pregunta sigue en pie. Si usas un modelo más allá de la longitud con la que se entrenó, te toca a ti medir cómo se comporta ahí, con tus propios datos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cuenta aparte los parámetros activos.&lt;/strong&gt; Con mezcla de expertos, Qwen3-30B-A3B tiene 30 500 millones de parámetros, usa 3350 millones por token y el 95 % de los pesos son expertos. La memoria depende de la primera cifra y la aritmética por token, de la segunda, y pagas las dos.&lt;/p&gt;
&lt;p&gt;Esto tampoco es trabajo de modelado. Un bloque de Llama es un bloque de GPT-2 con cuatro piezas cambiadas. Cada cambio es una línea de configuración o de código que puedes leer y cuyo coste puedes calcular, y cualquier desajuste se puede detectar con un test antes de servir la primera petición.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 7 de 12. Llama conserva el bloque y el bus de GPT-2 y cambia cuatro piezas: las posiciones pasan a ser una rotación de queries y keys, la norma prescinde de la media, la red feed-forward gana una puerta y las cabezas de query comparten keys y values. Si te equivocas con la norma, la activación o las posiciones, no salta ningún error, y lo que cambia es la pérdida. Las posiciones rotatorias eliminan una tabla y compartir keys y values reduce la caché, mientras que la puerta, con &lt;code&gt;8d/3&lt;/code&gt;, no cuesta nada. La siguiente parte se pregunta de dónde salen los pesos. Si has llegado aquí directamente, puedes empezar por &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;el tokenizador&lt;/a&gt; y seguir con &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;la tabla a la que apunta el id de un token&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/&quot;&gt;las formas&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-attention-learned-weighted-average/&quot;&gt;la atención&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-transformer-six-shapes-residual-stream/&quot;&gt;el bloque&lt;/a&gt; y &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-transformer-plus-five-decisions/&quot;&gt;GPT-2 y sus dos programas&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><category>Fundamentos de ML</category><category>Fundamentos de ML</category><category>From code to weights</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>GPT-2 es el transformer más cinco decisiones, y la inferencia es otro programa</title><link>https://cchinchilla.dev/es/posts/fcw-gpt2-transformer-plus-five-decisions/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/fcw-gpt2-transformer-plus-five-decisions/</guid><description>Un mismo artefacto ejecuta dos programas: el entrenamiento hace una sola pasada hacia delante, en paralelo, sobre la secuencia, y la generación es un bucle secuencial con caché. Los pesos son los mismos y los bugs, distintos.</description><pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 5 terminó con un bloque y una pila de doce. GPT-2 es esa pila con cinco decisiones alrededor, y el mismo checkpoint ejecuta dos programas distintos. El entrenamiento puntúa una secuencia entera en una sola llamada. La generación (lo que al servir el modelo se llama inferencia) escribe los tokens de uno en uno, en un bucle, y de ahí sale la mayoría de los fallos en producción.&lt;/p&gt;
&lt;p&gt;He vuelto a cargar los pesos de GPT-2 small de OpenAI en el &lt;code&gt;model.py&lt;/code&gt; de nanoGPT y he ejecutado con ellos los dos programas. Después he añadido una caché al bucle, he pasado los pesos a bf16 y he comparado lo que salía.&lt;/p&gt;
&lt;h2 id=&quot;cinco-decisiones-alrededor-de-la-pila&quot;&gt;Cinco decisiones alrededor de la pila&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;GPTConfig&lt;/code&gt;, en &lt;code&gt;model.py:108-116&lt;/code&gt;, cabe en una pantalla: 1024 posiciones, 12 bloques, 12 cabezas y anchura 768. El campo del vocabulario vale 50 304 porque, según el comentario de esa línea, se rellena hasta un múltiplo de 64 para ganar velocidad. El checkpoint de OpenAI tiene 50 257 filas, y al cargarlo la configuración se ajusta a él. Alrededor de esos números GPT-2 tomó cinco decisiones, y tres de ellas ya han salido en esta serie.&lt;/p&gt;
&lt;p&gt;El tokenizador es el BPE sobre bytes de la parte 1; como codifica cualquier cadena, no existe el token desconocido. La normalización es pre-LN, como en la parte 5. En &lt;code&gt;model.py:138&lt;/code&gt; la tabla de tokens se comparte con la capa de salida. Así, una sola matriz, la tabla que en el recuento de la parte 5 era el 31 % del modelo, sirve para los dos extremos. Con dos matrices separadas, GPT-2 small tendría 163 millones de parámetros, frente a los 124 millones que tiene.&lt;/p&gt;
&lt;p&gt;La cuarta decisión son las posiciones, que GPT-2 aprende. &lt;code&gt;wpe&lt;/code&gt; es una tabla de forma &lt;code&gt;[1024, 768]&lt;/code&gt;, con una fila por posición que se suma a la del token antes del primer bloque. Y como toda tabla tiene una última fila, si le pides a &lt;code&gt;forward&lt;/code&gt; 1100 posiciones, el assert de &lt;code&gt;model.py:173&lt;/code&gt; te para:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;text&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:79ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292e;--1:#adbac7&quot;&gt;AssertionError: Cannot forward sequence of length 1100, block size is only 1024&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;La quinta es la inicialización. Cada bloque escribe dos veces en el bus (el nombre que la parte 5 da al flujo residual), con lo que una pila de &lt;code&gt;L&lt;/code&gt; bloques acumula &lt;code&gt;2L&lt;/code&gt; escrituras. En nanoGPT todas las matrices, incluidas las tablas de tokens y de posiciones, arrancan con valores aleatorios de desviación típica 0,02 (el código de OpenAI usaba 0,01 para la de posiciones). Los sesgos empiezan en cero, y LayerNorm, en uno. &lt;code&gt;model.py:143-145&lt;/code&gt;, en cambio, inicializa con &lt;code&gt;0.02/√(2L)&lt;/code&gt; las dos capas que hacen esas escrituras, para que la suma no crezca con la profundidad. Lo he medido en modelos sin entrenar. Con la inicialización escalada, la norma del flujo se ha multiplicado más o menos por 11 tanto a lo largo de 12 bloques como de 48. Con la inicialización sin escalar, se ha multiplicado por 55 y por 112. A partir de ahí, el entrenamiento hace el resto.&lt;/p&gt;
&lt;h2 id=&quot;el-entrenamiento-es-una-sola-llamada&quot;&gt;El entrenamiento es una sola llamada&lt;/h2&gt;
&lt;p&gt;Si le das a &lt;code&gt;forward&lt;/code&gt; una secuencia y sus objetivos (la misma secuencia desplazada una posición), predice todas las posiciones a la vez. Le he pasado 92 posiciones de texto y ha devuelto logits de forma &lt;code&gt;[1, 92, 50257]&lt;/code&gt;, es decir, puntuaciones en bruto para cada token en cada posición, que la softmax convierte en probabilidades. También ha devuelto una sola pérdida, la entropía cruzada de &lt;code&gt;model.py:187&lt;/code&gt;, que es menos el logaritmo de la probabilidad asignada al siguiente token correcto, promediado sobre las 92. La máscara de la parte 4 es la que impide hacer trampa, porque la fila &lt;code&gt;t&lt;/code&gt; solo ve las filas &lt;code&gt;0..t&lt;/code&gt; y así ninguna predicción puede leer su propia respuesta.&lt;/p&gt;
&lt;p&gt;Ese programa se ejecuta en cada paso del entrenamiento, durante cientos de miles de pasos. Como todas las posiciones se conocen de antemano, atraviesan la pila juntas, en unas pocas multiplicaciones de matrices grandes. Si alguna vez has escrito un proceso por lotes sobre una tabla, ya sabes cómo es este programa. En la CPU de mi portátil, una pasada hacia delante sobre 1024 posiciones ha tardado 0,23 segundos.&lt;/p&gt;
&lt;p&gt;Sin objetivos, &lt;code&gt;forward&lt;/code&gt; toma un atajo en &lt;code&gt;model.py:190&lt;/code&gt; y calcula los logits solo de la última posición. En esa línea asoma por primera vez el segundo programa.&lt;/p&gt;
&lt;h2 id=&quot;la-generación-es-un-bucle&quot;&gt;La generación es un bucle&lt;/h2&gt;
&lt;p&gt;Para escribir texto, el modelo tiene que ver cada token antes de poder predecir el siguiente, y entre posiciones no queda nada que ejecutar en paralelo. Este es, resumido, el &lt;code&gt;generate&lt;/code&gt; de nanoGPT en &lt;code&gt;model.py:312-328&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:78ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; _ &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;range&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(max_new_tokens):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;idx_cond &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; idx[:, &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1024&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:]                 &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# crop to the position table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;logits, _ &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(idx_cond)                &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# the whole sequence, again&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;logits &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; logits[:, &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, :] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; temperature   &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, 50257], last position only&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;probs &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; F.softmax(logits, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;dim&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)         &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# top_k masking skipped here&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;idx_next &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; torch.multinomial(probs, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)    &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# draw one token id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;idx &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; torch.cat((idx, idx_next), &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;dim&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)   &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# append it and go round&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Mira otra vez la línea &lt;code&gt;self(idx_cond)&lt;/code&gt;. En cada paso, el bucle repite entera la pasada hacia delante sobre la secuencia acumulada hasta entonces y se queda con una sola fila. Si alguna vez has releído un fichero entero para añadirle una línea, ya has escrito este bucle. Con un prompt de 11 tokens, generar 256 tokens nuevos hace pasar 35 456 posiciones por la pila para sacar 256 filas útiles.&lt;/p&gt;
&lt;p&gt;La solución es una caché KV. En la parte 4, cada posición recibió un vector key, con el que se comparan las queries posteriores, y un vector value, que es lo que esas queries promedian. Con la máscara de la parte 4, los vectores key y value de la posición &lt;code&gt;t&lt;/code&gt; dependen solo de las posiciones &lt;code&gt;0..t&lt;/code&gt;, por lo que, una vez calculados, no vuelven a cambiar. Si los guardas (un par de tensores por bloque), en cada paso basta con pasar el token nuevo por la pila y calcular su atención sobre las filas guardadas. Para los mismos 256 tokens, eso son 266 posiciones. A cambio se paga en memoria, unos 74 kB por posición en GPT-2 small con fp32. Como nanoGPT no trae caché, he escrito una propia, de unas treinta líneas, que reutiliza sus módulos.&lt;/p&gt;
&lt;p&gt;El cronómetro lo confirma, aunque no en la misma proporción. Al generar 64, 256 y 512 tokens sin la caché, por la pila han pasado 37, 133 y 261 veces más posiciones, pero el tiempo se ha multiplicado por 2,9, por 5,0 y por 7,4. Con batch 1, cada paso sigue teniendo que leer los 124 millones de pesos para producir un token, y ese coste la caché no lo reduce. Escribir 1023 tokens con la caché ha llevado 9,3 segundos, unas cuarenta veces lo que tarda la pasada hacia delante del entrenamiento sobre la misma longitud.&lt;/p&gt;
&lt;p&gt;La caché se rompe además en el límite que impone la cuarta decisión. Pasados los 1024 tokens, &lt;code&gt;generate&lt;/code&gt; recorta la secuencia por la izquierda. A partir del segundo bloque, todos los vectores key y value guardados se calcularon cuando el token descartado aún estaba en la secuencia, y como las posiciones son aprendidas, cada token pasa también a otra fila de &lt;code&gt;wpe&lt;/code&gt;. La caché entera queda obsoleta. El bucle de nanoGPT se libra porque de todas formas lo recalcula todo, pero un bucle con caché tiene que resolverlo.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Un checkpoint de GPT-2 que genera texto, con caché KV y sin ella. Elige un tamaño y un dtype, y luego ajusta el prompt, los tokens nuevos y el batch. Los recuentos son posiciones que pasan por la pila, y las barras comparan la caché con los pesos.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-transformer-plus-five-decisions/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Mueve el deslizador de tokens nuevos y verás cómo se separan los dos recuentos. Después elige XL en bf16 y sube el batch hasta que la barra de la caché supere a la de los pesos.&lt;/p&gt;
&lt;h2 id=&quot;mismos-pesos-bugs-distintos&quot;&gt;Mismos pesos, bugs distintos&lt;/h2&gt;
&lt;p&gt;Dos programas que usan los mismos pesos deberían coincidir, y los míos han coincidido, o casi. En 200 pasos de decodificación voraz (greedy), en los que se toma siempre el token con la puntuación más alta, los dos bucles han escrito el mismo texto, aunque sus logits han llegado a diferir en 0,011. Casi toda esa diferencia era un desplazamiento común a todos los tokens, que la softmax ignora. La distancia entre los dos mejores candidatos ha variado como mucho 0,00005, y como en ningún paso ha bajado de 0,004, los tokens han aguantado. Una ejecución más larga o un modelo más grande pueden comerse ese margen, y cuando pase no saltará ningún error.&lt;/p&gt;
&lt;p&gt;La diferencia viene de la coma flotante. Pasado el prompt, cada multiplicación de matrices dentro de los bloques del bucle con caché trabaja con una sola fila, mientras que sin caché ve la secuencia entera. Por eso la biblioteca elige otros kernels (las rutinas que hacen las cuentas), que suman en otro orden. Como la suma en coma flotante no es asociativa, los resultados difieren en los últimos bits.&lt;/p&gt;
&lt;p&gt;Cambiemos ahora algo que en un despliegue se toca a menudo. He pasado los mismos pesos a bf16, el formato de 16 bits de la parte 3, y he vuelto a ejecutar el bucle de decodificación voraz. El modelo en fp32 ha continuado el prompt con &lt;code&gt;&amp;quot; were not working.&amp;quot;&lt;/code&gt; y el de bf16 con &lt;code&gt;&amp;quot; were not being run on the same system&amp;quot;&lt;/code&gt;. Se han separado en el tercer token y ya no han vuelto a juntarse. Al ejecutar el prompt ocho veces en un mismo batch, los tokens se han mantenido, pero los logits se han desplazado hasta 0,005 respecto a la ejecución con un solo prompt.&lt;/p&gt;
&lt;p&gt;El remedio al que suele recurrir la gente es poner la temperatura a 0. No lo arregla, y en nanoGPT el 0 ni siquiera es un valor válido. &lt;code&gt;generate&lt;/code&gt; divide los logits entre la temperatura. Con 0, los 50 257 logits se vuelven infinitos, la softmax los convierte en NaN (el valor de coma flotante para un resultado indefinido) y &lt;code&gt;torch.multinomial&lt;/code&gt; lanza una excepción. La mayoría de las API que aceptan el 0 lo usan como interruptor para pasar a argmax, que elige la puntuación más alta. Eso vuelve a ser decodificación voraz, y solo es tan determinista como lo sean los pesos, el dtype, la forma del batch y los kernels que tenga debajo.&lt;/p&gt;
&lt;p&gt;Para ser justos con la magnitud del problema, todas las ejecuciones que aparecen aquí han sido repetibles; con la misma semilla en la misma máquina, el muestreo ha sacado dos veces los mismos 40 tokens. La deriva aparece cuando cambia algo por debajo, y en la infraestructura que sirve el modelo esas cosas cambian sin consultarte.&lt;/p&gt;
&lt;h2 id=&quot;el-muestreo-es-una-política-por-encima-del-modelo&quot;&gt;El muestreo es una política por encima del modelo&lt;/h2&gt;
&lt;p&gt;La decodificación voraz es una elección, y GPT-2 small enseña por qué se suele evitar: antes de llegar a 200 tokens, el bucle de arriba ya repetía su propia frase. El muestreo, en cambio, sortea el token según la distribución, y tres ajustes dan forma a esa distribución antes del sorteo.&lt;/p&gt;
&lt;p&gt;La temperatura divide los logits antes de la softmax, de modo que por debajo de 1 concentra la distribución y por encima de 1 la aplana. Top-k se queda con los &lt;code&gt;k&lt;/code&gt; tokens de puntuación más alta y descarta el resto; top-p, también llamado muestreo de núcleo (nucleus sampling), conserva el conjunto más pequeño cuyas probabilidades suman al menos &lt;code&gt;p&lt;/code&gt;. El &lt;code&gt;sample.py&lt;/code&gt; de nanoGPT trae por defecto temperatura 0,8 y top-k 200, y no tiene top-p.&lt;/p&gt;
&lt;p&gt;El efecto de estos ajustes depende del prompt. Tras «The capital of France is», el candidato favorito de GPT-2 small es &lt;code&gt;&amp;quot; the&amp;quot;&lt;/code&gt;, con un 8,5 %, y &lt;code&gt;&amp;quot; Paris&amp;quot;&lt;/code&gt; queda quinto, con un 3,2 %. A temperatura 1, hacen falta 1504 tokens para cubrir el 90 % de la probabilidad. Tras «import numpy as», en cambio, &lt;code&gt;&amp;quot; np&amp;quot;&lt;/code&gt; se lleva él solo el 85 %, y para llegar al 90 % bastan tres. La entropía, medida en bits, le pone número a esa dispersión. Para el prompt de la capital a temperatura 1 vale 8,7 bits, tanta incertidumbre como elegir al azar entre unos 400 tokens igual de probables. Para el de numpy vale 1,5 bits, el equivalente a unos tres.&lt;/p&gt;
&lt;p&gt;Top-k no distingue un caso del otro, de ahí que cualquier &lt;code&gt;k&lt;/code&gt; que elijas falle en uno de los dos. Con los valores por defecto de nanoGPT, esos 200 tokens reúnen el 89 % de la masa tras el prompt de la capital, y tras el de numpy conservan 199 opciones que se reparten menos del 5 %. Top-p sí se adapta, y con la misma temperatura y &lt;code&gt;p = 0.9&lt;/code&gt; conserva 237 tokens en un caso y uno solo en el otro.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Probabilidades que GPT-2 small asigna al siguiente token en tres prompts, medidas una sola vez. Los prompts y los tokens van en inglés porque son literales: lo que ha leído el modelo y los candidatos que ha puntuado. Elige un prompt y mueve la temperatura y el umbral de top-p; las filas que quedan fuera del corte de top-p se ponen en gris.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-gpt2-transformer-plus-five-decisions/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Con los valores iniciales, temperatura 1 y &lt;code&gt;p = 0.9&lt;/code&gt;, el prompt de la capital conserva 1504 tokens y el de numpy, tres. Sube la temperatura a 2 y verás cómo crece el recuento de numpy. Los indicadores de top-40 y top-200 muestran lo que conserva un &lt;code&gt;k&lt;/code&gt; fijo.&lt;/p&gt;
&lt;h2 id=&quot;qué-vigila-el-ingeniero-de-ml&quot;&gt;Qué vigila el ingeniero de ML&lt;/h2&gt;
&lt;p&gt;Son cuatro hábitos, y el primero es un test.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compara en un test el bucle con caché y la pasada completa, logits incluidos.&lt;/strong&gt; Ejecuta los dos programas durante unos cientos de pasos y compara los tokens de la decodificación voraz y, con una tolerancia, los logits. Si solo compruebas que los tokens coinciden, el test puede pasar mientras los logits derivan, como en mi caso.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Incluye la caché en el presupuesto de memoria, igual que los pesos.&lt;/strong&gt; Por cada secuencia guarda &lt;code&gt;2·L·T·d&lt;/code&gt; números, con &lt;code&gt;L&lt;/code&gt; bloques, &lt;code&gt;T&lt;/code&gt; posiciones y anchura &lt;code&gt;d&lt;/code&gt;: un vector key y otro value por cada bloque y posición. En GPT-2 small, con 1024 posiciones en fp32, son 75 MB. En GPT-2 XL con bf16 son 315 MB por secuencia, así que unas diez secuencias de longitud completa pesan tanto como sus 3,1 GB de pesos. Subir el tamaño de batch es una decisión de throughput y también de memoria.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fija lo que comparas.&lt;/strong&gt; Cualquier cambio de dtype, de forma del batch, de versión de la biblioteca o de kernel altera los logits. Si tu test de regresión compara salidas del modelo, fija los cuatro, o compara con una tolerancia y registra lo que ha cambiado.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Registra los ajustes de muestreo con cada salida.&lt;/strong&gt; La temperatura, top-k, top-p y la semilla forman parte del programa. Dos ejecuciones que solo se diferencian en ellos se comportan de forma distinta, y una salida registrada sin ellos no se puede reproducir.&lt;/p&gt;
&lt;p&gt;Esto tampoco es trabajo de modelado. Los pesos son un único artefacto, y es el código que los rodea el que decide si una posición se puntúa o se escribe, qué sobrevive entre pasos y cómo una distribución se convierte en un token. Todo eso es código corriente que puedes probar.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 6 de 12. GPT-2 es la pila de la parte 5 más cinco decisiones: BPE sobre bytes, pre-LN, una tabla de tokens compartida con la capa de salida, posiciones aprendidas y una inicialización escalada para las escrituras en el bus. El entrenamiento ejecuta una sola pasada hacia delante, en paralelo, sobre todas las posiciones, y la generación, un bucle que necesita una caché, una política de muestreo y sus propios tests. La siguiente parte cuenta lo que cambió Llama. Si has llegado aquí directamente, puedes empezar por &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;el tokenizador&lt;/a&gt; y seguir con &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;la tabla a la que apunta el id de un token&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/&quot;&gt;las formas&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-attention-learned-weighted-average/&quot;&gt;la atención&lt;/a&gt; y &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-transformer-six-shapes-residual-stream/&quot;&gt;el bloque&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><category>Fundamentos de ML</category><category>Fundamentos de ML</category><category>From code to weights</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>Un bloque de transformer son seis tensores y un bus: léelo como un pipeline</title><link>https://cchinchilla.dev/es/posts/fcw-transformer-six-shapes-residual-stream/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/fcw-transformer-six-shapes-residual-stream/</guid><description>Leído línea a línea y con las formas al margen, un bloque de transformer son seis tensores sobre un flujo residual. Dos tercios de sus pesos están en la red feed-forward, y a medida que crece el contexto el coste se desplaza a la atención.</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 4 se quedó en una sola etapa, pero un decodificador es una pila de bloques (doce en GPT-2 small) y la atención es una de las dos etapas de cada uno. Aquí leo un bloque entero como se lee un pipeline: qué entra, qué escribe cada etapa y adónde van a parar los parámetros y el coste. Ya te adelanto que no van al mismo sitio.&lt;/p&gt;
&lt;p&gt;He leído &lt;code&gt;Block&lt;/code&gt; en el &lt;code&gt;model.py&lt;/code&gt; de nanoGPT con las formas anotadas al margen y le he cargado los pesos de GPT-2. Luego lo he roto a propósito, primero quitando los bloques de uno en uno y después haciendo que cada uno sobrescribiera su entrada sin sumarle nada.&lt;/p&gt;
&lt;h2 id=&quot;el-bloque-son-cuatro-líneas&quot;&gt;El bloque son cuatro líneas&lt;/h2&gt;
&lt;p&gt;nanoGPT es un GPT-2 mínimo repartido en dos ficheros. Su README remite ahora a un sucesor, pero &lt;code&gt;model.py&lt;/code&gt; sigue siendo la manera más corta de leer un bloque de GPT-2 sin trampas. &lt;code&gt;Block.forward&lt;/code&gt; está en &lt;code&gt;model.py:103-106&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:88ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#DCBDFB&quot;&gt;forward&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(self, x):                   &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# x: [B, T, 768], the stream&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;x &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; x &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.attn(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.ln_1(x))     &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# read a normalised copy, add attention&amp;#39;s output&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;x &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; x &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.mlp(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.ln_2(x))      &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# read again, add the feed-forward&amp;#39;s output&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; x                            &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, T, 768]: the shape it came in with&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Dentro de esas dos llamadas, en &lt;code&gt;model.py:52-92&lt;/code&gt;, hay seis tensores que merece la pena nombrar. El código de abajo está condensado, sin escalado, máscara ni dropout, y las formas del margen salen de ejecutar un modelo con las dimensiones de GPT-2 small, &lt;code&gt;d = 768&lt;/code&gt; y 12 cabezas.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:94ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;qkv &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.attn.c_attn(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.ln_1(x))         &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, T, 2304]    q, k, v side by side&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;q, k, v &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; qkv.split(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;768&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;dim&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)              &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# three views, [B, T, 768] each&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;q &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; q.view(B, T, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;12&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;64&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;).transpose(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)     &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, 12, T, 64]  12 heads of 64; k, v alike&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;att &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (q &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; k.transpose(&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)).softmax(&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, 12, T, T]   Part 4&amp;#39;s square&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;y &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (att &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; v).transpose(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;).reshape(B, T, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;768&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;y &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.attn.c_proj(y)                      &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, T, 768]     attention out&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;x &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; x &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; y                                    &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, T, 768]     the stream, attention added&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;h &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; F.gelu(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.mlp.c_fc(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.ln_2(x)))      &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, T, 3072]    the feed-forward&amp;#39;s hidden layer&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;out &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.mlp.c_proj(h)                     &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, T, 768]     feed-forward out&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Contando el propio flujo, el bloque son &lt;code&gt;x&lt;/code&gt;, las proyecciones, las puntuaciones, la salida de la atención, la capa oculta y la salida de la red feed-forward. Tres de esos tensores comparten la forma del flujo. Todo lo demás que aparece en el código es una vista, un paso elemento a elemento o la disposición por cabeza &lt;code&gt;[B, 12, T, 64]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Conviene detenerse en cómo se reparten el trabajo las etapas. La atención es la única que mezcla posiciones (la fila &lt;code&gt;t&lt;/code&gt; de su salida se construye con las filas &lt;code&gt;0..t&lt;/code&gt; del flujo), mientras que LayerNorm y la red feed-forward trabajan posición a posición.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LayerNorm&lt;/code&gt; resta a cada fila de 768 números su media, la divide por su desviación típica y después aplica una escala y un desplazamiento aprendidos. En &lt;code&gt;model.py:27&lt;/code&gt;, todo eso cabe en una sola llamada a &lt;code&gt;F.layer_norm&lt;/code&gt;. La red feed-forward ensancha cada fila a 3072, aplica GELU (una versión suave de &lt;code&gt;max(0, x)&lt;/code&gt;) y la vuelve a estrechar. Si has escrito un map sobre filas, ya has escrito el flujo de control de la red feed-forward.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Un bloque tal como lo escribe model.py, con las dimensiones de GPT-2 small. El flujo baja por la línea vertical. Cada rama lee una copia normalizada y suma su salida al flujo. El coste de las puntuaciones es el de la ruta eager, que las guarda en memoria; los kernels fusionados de sdpa no las guardan. Elige un tensor y después arrastra los tamaños.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-transformer-six-shapes-residual-stream/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Elige la capa oculta de la red feed-forward y mira sus parámetros. Luego elige las puntuaciones, que no tienen ninguno. Ahora arrastra &lt;code&gt;T&lt;/code&gt; y fíjate en el término de atención, las puntuaciones y la suma ponderada. Los parámetros no se mueven, pero la aritmética sí.&lt;/p&gt;
&lt;h2 id=&quot;la-arquitectura-está-en-el-signo-más&quot;&gt;La arquitectura está en el signo más&lt;/h2&gt;
&lt;p&gt;Si vuelves a leer las dos líneas, verás que ninguna etapa sustituye a &lt;code&gt;x&lt;/code&gt;. Cada una lee una copia normalizada y le suma al original lo que ha calculado, así que el flujo sale del bloque con dos retoques encima y el bloque siguiente lee el resultado.&lt;/p&gt;
&lt;p&gt;Ese flujo, &lt;code&gt;[B, T, 768]&lt;/code&gt; desde el embedding hasta el último bloque, es lo que la literatura llama flujo residual y lo que aquí llamo el bus. Todas las etapas lo leen y escriben en él, pero no es de ninguna. Si alguna vez has escrito &lt;code&gt;total += delta&lt;/code&gt; en un bucle, ya conoces el contrato: cada etapa suma al total acumulado sin poder sobrescribirlo, y para deshacer una escritura anterior hace falta que una etapa posterior la sume con el signo cambiado.&lt;/p&gt;
&lt;p&gt;Las cifras confirman que el contrato se cumple. He pasado 1024 tokens de &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/&quot;&gt;una entrada anterior&lt;/a&gt; por GPT-2 small y he medido longitudes (la norma de cada vector), tomando la mediana de todas las posiciones. En los bloques 1 a 10, cada etapa suma un vector de entre el 16 % y el 40 % de la longitud del flujo que lee. El flujo sale apuntando casi adonde apuntaba al entrar: la similitud coseno (1 significa la misma dirección) va de 0,93 a 0,97. Por el camino la norma también crece, de 49 tras el bloque 0 a 480 al final. El bus en sí no se normaliza nunca. Cada etapa normaliza su propia copia y &lt;code&gt;ln_f&lt;/code&gt; normaliza una vez más antes de que la capa de salida convierta la última fila en logits, las puntuaciones del token siguiente.&lt;/p&gt;
&lt;p&gt;Normalizar fuera del bus fue una decisión. Llamemos pre-LN al orden de GPT-2, que normaliza dentro de la rama, &lt;code&gt;x + attn(ln(x))&lt;/code&gt;, y deja el bus como está. El transformer original y GPT-1 eran post-LN y normalizaban después de la suma, &lt;code&gt;x = ln(x + attn(x))&lt;/code&gt;, con lo que reescalaban el propio bus en cada etapa. El artículo de GPT-2 deja constancia del cambio en una sola frase, la misma que añade &lt;code&gt;ln_f&lt;/code&gt;, y cita un precedente, pero no da ninguna razón propia.&lt;/p&gt;
&lt;p&gt;Un experimento pequeño muestra una. He entrenado dos modelos de seis bloques con el texto de este blog, carácter a carácter, con la misma configuración y sin warmup (el warmup consiste en arrancar con un tamaño de paso pequeño e ir subiéndolo). La pérdida mide la sorpresa media del modelo ante cada token siguiente. Cuanto más baja, mejor, y 0 significaría acertar siempre y con total seguridad.&lt;/p&gt;
&lt;p&gt;Tras 300 pasos, el modelo pre-LN llega a unos 2,4 y queda por debajo del 2,56 de un modelo que solo conoce la letra anterior. El post-LN se atasca en 3,2 (más o menos lo que dan por sí solas las frecuencias de las letras) y, en una semilla que he alargado hasta los 600 pasos, sigue ahí. Con un warmup de 100 pasos se desatasca entre los pasos 200 y 250. Es un experimento de juguete, con dos semillas y pérdidas leídas de los batches de entrenamiento, pero el resultado va en la línea del &lt;a href=&quot;https://arxiv.org/abs/2002.04745&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;análisis publicado&lt;/a&gt; sobre por qué post-LN necesita warmup.&lt;/p&gt;
&lt;p&gt;Vuelvo a GPT-2 small, esta vez para quitarle bloques. Sobre esos 1024 tokens, el modelo intacto da una pérdida de 3,59. Si quitas cualquiera de los bloques 1 a 10 y dejas que el flujo pase de largo, la pérdida queda entre 3,61 y 3,89; si quitas el bloque 0, salta a 9,15.&lt;/p&gt;
&lt;p&gt;Sobrescribir es peor que quitar. Si conservas el bloque pero haces que su salida sustituya al flujo sin sumarse a él, la pérdida queda por encima de 9,6 en los bloques 1 a 8, y en los bloques 2 a 7 supera el 10,8 de una predicción uniforme sobre todo el vocabulario. El modelo acaba peor que si adivinara a ciegas.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Cada barra es la pérdida de GPT-2 small con uno de sus doce bloques cambiado, medida sobre 1024 tokens de una entrada anterior de este blog (cuanto más baja, mejor). Alterna entre quitar el bloque y hacer que sobrescriba el flujo, y después elige uno.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-transformer-six-shapes-residual-stream/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Cambia a sobrescribir y verás cómo los bloques 2 a 7 cruzan la línea de puntos. Después elige el bloque 0.&lt;/p&gt;
&lt;p&gt;El bloque 0 es la excepción, y la norma de los vectores explica por qué. El embedding entra con una norma de 4,6 y la atención del bloque 0 escribe un vector siete veces más largo. Tras el bloque 0, el flujo es sobre todo la salida de ese bloque, así que se comporta más como un segundo embedding que como un retoque. Sobrescribir ahí no cuesta nada (3,59), porque sumar ya equivalía a sustituir.&lt;/p&gt;
&lt;h2 id=&quot;dos-tercios-de-los-pesos&quot;&gt;Dos tercios de los pesos&lt;/h2&gt;
&lt;p&gt;Los parámetros de un bloque se pueden contar a mano, con &lt;code&gt;d = 768&lt;/code&gt; y la anchura de la red feed-forward &lt;code&gt;f = 4d&lt;/code&gt;. Las dos proyecciones de la atención tienen &lt;code&gt;4d² + 4d&lt;/code&gt; (2 362 368 números); las dos de la red feed-forward, &lt;code&gt;2·d·f + f + d&lt;/code&gt;, que da &lt;code&gt;8d² + 5d&lt;/code&gt; (4 722 432), y las dos LayerNorm, 3072. En todos los tamaños de GPT-2 la red feed-forward se lleva dos tercios de cada bloque, porque esa proporción la fija el &lt;code&gt;4 * config.n_embd&lt;/code&gt; de &lt;code&gt;model.py:82&lt;/code&gt;, sea cual sea la anchura. Llama 3.1 8B cambia las dos etapas y su red feed-forward tiene el 81 % de un bloque; en la parte 7 veremos por qué.&lt;/p&gt;
&lt;p&gt;Los doce bloques suman 85 millones de parámetros, y GPT-2 small tiene 124 439 808. Casi todo el resto es la tabla de tokens, con 38,6 millones de números, el 31 % del modelo. Se usa dos veces, abajo del todo como la consulta de la parte 2 y arriba del todo como la proyección a logits. En &lt;code&gt;model.py:138&lt;/code&gt; queda compartida con la capa de salida (el mismo array, sin copia). En GPT-2 XL esa misma tabla se queda en el 5,2 % del modelo. Los modelos pequeños gastan casi un tercio de sus pesos en el vocabulario, y los grandes apenas lo notan.&lt;/p&gt;
&lt;h2 id=&quot;el-coste-está-en-otra-parte&quot;&gt;El coste está en otra parte&lt;/h2&gt;
&lt;p&gt;En un programa corriente, el tamaño de una función y su tiempo de ejecución salen del mismo código, pero aquí van por separado: los parámetros están sobre todo en la red feed-forward, y la aritmética crece con &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Una pasada hacia delante cuesta unas dos operaciones por token, una multiplicación y una suma, por cada número de una matriz de pesos (la parte 4 las contaba como FLOPs). Si llamamos &lt;code&gt;N&lt;/code&gt; a la cantidad de esos números, los parámetros cuestan &lt;code&gt;2N&lt;/code&gt; por token. A eso hay que sumarle el término de atención, &lt;code&gt;4·L·T·d&lt;/code&gt; (donde &lt;code&gt;L&lt;/code&gt; es el número de bloques), que cubre las puntuaciones y la suma ponderada sobre el cuadrado completo. En GPT-2 small con 1024 tokens salen 247 millones de operaciones por token de los parámetros y 38 millones de la atención. El &lt;code&gt;FlopCounterMode&lt;/code&gt; de torch coincide cuando la atención va escrita en sus cuatro líneas, por la ruta eager de la parte 4, y da 284,8 millones por token, justo la cifra de la fórmula (285,1 si &lt;code&gt;N&lt;/code&gt; es el &lt;code&gt;get_num_params()&lt;/code&gt; de nanoGPT, que cuenta también los sesgos y las LayerNorm).&lt;/p&gt;
&lt;p&gt;Con sdpa, la opción por defecto, el mismo contador daba 247 millones tanto con 256 tokens como con 1024. En torch 2.14, la versión que he usado, el contador no tiene entrada para el kernel de atención fusionado de CPU, la única rutina a la que llama sdpa para sustituir esas cuatro líneas. Por eso el término de atención no llega nunca al total, y nada te avisa. La rama principal de PyTorch ya registra ese kernel (&lt;a href=&quot;https://github.com/pytorch/pytorch/pull/195801&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;PR #195801&lt;/a&gt;), de modo que la próxima versión menor también debería contar el término de atención con sdpa.&lt;/p&gt;
&lt;p&gt;Dentro de un bloque, el término de atención es el 18 % de la aritmética con 1024 tokens y adelanta a las capas lineales a los 4608 tokens, seis veces &lt;code&gt;d&lt;/code&gt;. Los tiempos lo confirman. En la CPU de mi portátil, la etapa de atención de un bloque, proyecciones incluidas, se lleva el 38 % del tiempo con 256 tokens, el 47 % con 1024 y el 57 % con 4096, aunque tiene un tercio de los parámetros del bloque.&lt;/p&gt;
&lt;p&gt;Para ser justos con el reparto, en las longitudes para las que se diseñó GPT-2 son las capas lineales las que hacen la mayor parte de la aritmética, y dentro de ellas la red feed-forward se lleva más de la mitad. El término de atención solo toma el relevo pasada una longitud que GPT-2 no admite. Aun así, al elegir un modelo eliges dos presupuestos. Los parámetros fijan la memoria y no cambian desde que lo cargas; el contexto, que llega con cada petición, marca el coste y una parte creciente de la memoria.&lt;/p&gt;
&lt;h2 id=&quot;qué-vigila-el-ingeniero-de-ml&quot;&gt;Qué vigila el ingeniero de ML&lt;/h2&gt;
&lt;p&gt;Son cuatro hábitos, y los dos primeros son de aritmética.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cuenta los parámetros por componente, sin quedarte en el total.&lt;/strong&gt; El primer gráfico hace esa cuenta para un bloque, con &lt;code&gt;4d²&lt;/code&gt; para la atención y &lt;code&gt;2·d·f&lt;/code&gt; para la red feed-forward. Para la tabla, suma una sola vez el producto del vocabulario por &lt;code&gt;d&lt;/code&gt;. Si comparas un modelo de 124 millones de parámetros con uno de 1500 millones, pregúntate cuánto de cada uno es vocabulario.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Estima el coste en 2N por token y en 6N para entrenar.&lt;/strong&gt; Entrenar es una pasada hacia delante y otra hacia atrás, y la de atrás, que calcula los gradientes, cuesta lo que unas dos hacia delante. Eso da 6N por token y &lt;code&gt;6·N·D&lt;/code&gt; para una ejecución de &lt;code&gt;D&lt;/code&gt; tokens. La configuración de nanoGPT para GPT-2, &lt;code&gt;config/train_gpt2.py&lt;/code&gt;, procesa 491 520 tokens por iteración durante 600 000 iteraciones, 295 000 millones de tokens en total. Con eso, &lt;code&gt;6·N·D&lt;/code&gt; da 2,2 × 10²⁰ operaciones, que son 195 horas de una A100 a su pico en bf16, o un día de un nodo de ocho GPU. El fichero dice que cuentes con unos cinco días y el README habla de cuatro, lo que deja la ejecución entre una quinta y una cuarta parte del pico. El estimador del propio nanoGPT añade el término de atención (&lt;code&gt;model.py:296&lt;/code&gt;), que con 1024 tokens supone otro 15 %.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La profundidad es latencia.&lt;/strong&gt; Reparte de cuatro maneras los mismos 85 millones de parámetros de los bloques, en 6 bloques de anchura 1088, 12 de 768, 24 de 544 o 48 de 384. Con batch 1, un token tarda 3,2 ms, 4,0 ms, 4,7 ms y 6,7 ms en atravesar la pila en mi CPU. Cada bloque añade un sobrecoste fijo y los bloques se ejecutan uno detrás de otro. Si sirves las peticiones de una en una, lo que esperas lo marca la profundidad, aunque la aritmética sea la misma.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mide un bloque antes de quitarlo.&lt;/strong&gt; La poda de capas (desplegar el modelo con menos bloques) y la salida temprana (pararse en un bloque intermedio cuando la respuesta parece decidida) se apoyan en el bus, y el bus no es uniforme. Quitar un bloque intermedio de GPT-2 small sube la pérdida entre un 0,5 % y un 8 %, pero quitar el bloque 0 la sube un 155 %, y quitar el último, un 41 %. Las cifras de arriba me han llevado un minuto en un portátil, con texto de este blog; las tuyas, sácalas de tu propio tráfico.&lt;/p&gt;
&lt;p&gt;Esto tampoco es trabajo de modelado. Un bloque son cuatro líneas alrededor de una suma, y es la suma lo que permite que una pila de doce se comporte como un solo pipeline. Dónde están los pesos y adónde se va el tiempo son preguntas con respuestas distintas, y las dos se resuelven con cuentas que puedes hacer antes de cargar nada.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 5 de 12. Un bloque lee el flujo en dos etapas, la atención y una red feed-forward, y le vuelve a sumar los dos resultados. Gracias al signo más, quitar un bloque intermedio sube la pérdida en un pequeño porcentaje y el modelo sigue en pie. La red feed-forward tiene dos tercios de los pesos de cada bloque, y la parte de la aritmética que se lleva el término de atención crece con el contexto. La parte siguiente recorre las cinco decisiones que tomó GPT-2 alrededor de la pila. Si has llegado aquí directamente, puedes empezar por &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;el tokenizador&lt;/a&gt; y seguir con &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;la tabla a la que apunta el id de un token&lt;/a&gt;, &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/&quot;&gt;las formas&lt;/a&gt; y &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-attention-learned-weighted-average/&quot;&gt;la etapa en torno a la que se construye este bloque&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Fundamentos de ML</category><category>Fundamentos de ML</category><category>From code to weights</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>La atención es una media ponderada aprendida y su coste está en el cuadrado</title><link>https://cchinchilla.dev/es/posts/fcw-attention-learned-weighted-average/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/fcw-attention-learned-weighted-average/</guid><description>Un diccionario difuso cuyos pesos calcula el modelo a partir de su propia entrada. La idea es barata, pero la matriz de pesos es de T por T, así que el coste lo ponen los datos y en el código no se ve.</description><pubDate>Tue, 22 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 3 acababa en un cuadrado. En un bloque de decodificador hay dos tensores con &lt;code&gt;T&lt;/code&gt; dos veces en la forma, &lt;code&gt;[B, H, T, T]&lt;/code&gt;, es decir, un cuadrado &lt;code&gt;T × T&lt;/code&gt; por cabeza, con &lt;code&gt;H&lt;/code&gt; cabezas y &lt;code&gt;B&lt;/code&gt; secuencias. Con los valores por defecto del gráfico de aquella parte, suponían el 89 % de la memoria del bloque. En esta leo lo que los llena, que es una media ponderada. La media en sí no tiene nada de especial. Lo nuevo es que los pesos se calculan a partir de la entrada.&lt;/p&gt;
&lt;p&gt;He escrito la atención en cuatro líneas y la he contrastado con &lt;code&gt;F.scaled_dot_product_attention&lt;/code&gt;, la implementación del propio torch. Luego he leído &lt;code&gt;LlamaAttention.forward&lt;/code&gt; con las formas anotadas al margen y he calculado cuánto cuesta en Llama 3.1 8B con las longitudes de contexto que se prometen.&lt;/p&gt;
&lt;h2 id=&quot;un-diccionario-que-coincide-en-parte&quot;&gt;Un diccionario que coincide en parte&lt;/h2&gt;
&lt;p&gt;Buscar en un diccionario es una operación exacta: la clave coincide o no coincide, y obtienes un único valor. La atención hace lo mismo con una coincidencia difusa. Todas las claves coinciden un poco y obtienes la media de todos los valores, ponderada por lo bien que ha coincidido cada clave.&lt;/p&gt;
&lt;p&gt;Esas ponderaciones son los pesos de atención. No son parámetros ni se guardan en ningún sitio, porque el modelo los calcula a partir de la entrada en cada llamada. Hasta llegar a la sección del coste, cuando diga «pesos» me referiré a estos. Tres matrices aprendidas convierten cada fila de la entrada en una query, una key y un value. La query se compara con todas las keys y las puntuaciones resultantes se convierten en pesos:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:65ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;q, k, v &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; x &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Wq, x &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Wk, x &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Wv             &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, T, d] each&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;scores &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; q &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; k.transpose(&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; sqrt(d)  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, T, T]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;weights &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; scores.softmax(&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)                &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# every row sums to 1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;out &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; weights &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; v                           &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, T, d]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Fíjate en las formas. &lt;code&gt;x&lt;/code&gt; es el &lt;code&gt;[B, T, d]&lt;/code&gt; de la parte 3 y las tres proyecciones lo conservan. &lt;code&gt;scores&lt;/code&gt; es &lt;code&gt;[B, T, T]&lt;/code&gt;, un número por cada par de posiciones, y ahí está el cuadrado. La softmax convierte cada fila en pesos que suman uno, con los que la última línea calcula la media. Estas cuatro líneas dan lo mismo que &lt;code&gt;F.scaled_dot_product_attention&lt;/code&gt; con una tolerancia de &lt;code&gt;1e-5&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Que sea una media tiene dos consecuencias. La primera, que cada coordenada de la salida queda entre el menor y el mayor de los values en esa coordenada, así que la atención nunca se sale de ese rango. La segunda, que si cambias una fila de la softmax por una fila one-hot (un solo 1 y ceros en el resto), la salida es exactamente el value de esa posición. El diccionario difuso contiene el exacto como caso límite.&lt;/p&gt;
&lt;h2 id=&quot;por-qué-la-raíz-cuadrada&quot;&gt;Por qué la raíz cuadrada&lt;/h2&gt;
&lt;p&gt;La división por &lt;code&gt;sqrt(d)&lt;/code&gt; parece un detalle, pero de ella depende que el modelo aprenda desde el primer paso o arranque saturado. El motivo es una cuenta que puedes comprobar en una consola.&lt;/p&gt;
&lt;p&gt;El producto escalar de dos vectores aleatorios de anchura &lt;code&gt;d&lt;/code&gt; con componentes de varianza 1 tiene varianza &lt;code&gt;d&lt;/code&gt;. Lo he medido con &lt;code&gt;d = 4096&lt;/code&gt; y sale 4104. Si metes puntuaciones tan dispersas en una softmax, se satura: un peso se va casi a 1 y los demás, casi a 0. Con 1024 keys, el peso más grande de cada fila ha salido de media 0,976 y la fila se ha quedado en 0,09 bits de entropía de los 10 posibles. Eso es una búsqueda exacta disfrazada de difusa, y en plena inicialización. Un modelo entrenado también acaba afilando las filas, pero lo hace a propósito y desde un punto de partida del que puede aprender. Al dividir por &lt;code&gt;sqrt(d)&lt;/code&gt;, la varianza queda en 1,0 y la fila sube a 9,27 bits de entropía, con el peso más grande en 0,017. Con la pérdida calculada a partir de la media ponderada, el gradiente medio que llega a las puntuaciones es alrededor de un orden de magnitud mayor, entre 12 y 18 veces según la semilla. Si la pérdida se calcula a partir de un solo peso, el factor oscila entre unas pocas veces y muchos órdenes de magnitud, según las filas en las que le toque ganar a esa key. Una softmax saturada apenas tiene de qué aprender, porque su gradiente queda por debajo de &lt;code&gt;1e-6&lt;/code&gt; en el 99,8 % de las puntuaciones y lo poco que hay se amontona en los contados casi empates.&lt;/p&gt;
&lt;p&gt;Llama lo escribe una sola vez, &lt;code&gt;self.scaling = self.head_dim**-0.5&lt;/code&gt;, en &lt;code&gt;modeling_llama.py:226&lt;/code&gt;. Ahí la anchura que cuenta es la de la cabeza, sin que intervenga la del modelo.&lt;/p&gt;
&lt;h2 id=&quot;las-cabezas-son-una-vista-y-la-máscara-un-triángulo&quot;&gt;Las cabezas son una vista y la máscara, un triángulo&lt;/h2&gt;
&lt;p&gt;La atención multicabeza consiste en ejecutar las cuatro líneas de arriba una vez por cabeza, &lt;code&gt;H&lt;/code&gt; veces, sobre trozos de &lt;code&gt;d&lt;/code&gt;. Trocear no cuesta nada: &lt;code&gt;q.view(B, T, H, d // H).transpose(1, 2)&lt;/code&gt; da &lt;code&gt;[B, H, T, d/H]&lt;/code&gt; y comparte memoria con &lt;code&gt;q&lt;/code&gt;, porque el eje de las cabezas es solo un stride. Las puntuaciones pasan a ser &lt;code&gt;[B, H, T, T]&lt;/code&gt;, un cuadrado por cabeza. La cabeza 0 de la llamada en batch da lo mismo que la atención calculada solo sobre la cabeza 0. Lo que se gana con las cabezas son &lt;code&gt;H&lt;/code&gt; juegos de pesos distintos sobre las mismas posiciones, cada uno en un trozo de anchura &lt;code&gt;d/H&lt;/code&gt; (128 en Llama 3.1 8B, con 32 cabezas).&lt;/p&gt;
&lt;p&gt;Es la máscara la que hace que un decodificador deje de promediar sobre todas las posiciones. Para predecir el token que sigue a la posición &lt;code&gt;t&lt;/code&gt;, el modelo puede leer las posiciones &lt;code&gt;0..t&lt;/code&gt; y nada posterior, así que las puntuaciones por encima de la diagonal se ponen a &lt;code&gt;-inf&lt;/code&gt; antes de la softmax y sus pesos quedan a cero. Lo he comprobado de la forma más directa, calculando la atención sobre ocho posiciones y reescribiendo luego las keys y los values de las posiciones 5 a 7. Las posiciones 0 a 4 han devuelto la misma salida con una tolerancia de &lt;code&gt;1e-6&lt;/code&gt;, mientras que sin la máscara han cambiado las ocho.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: La matriz de puntuaciones de una cabeza con T = 12. Cada fila es una query y cada columna, una key; una celda encendida es una puntuación que llegará a la softmax. Elige una máscara y mira cuántas celdas quedan vivas.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-attention-learned-weighted-average/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Prueba las tres máscaras y mira el recuento. El tercer botón, el de la ventana, es la última palanca de la sección del coste. El triángulo se combina por suma con la máscara de padding de la parte 3. Si añades dos posiciones de padding a la izquierda de una secuencia, las dos primeras filas de query quedan enteras a &lt;code&gt;-inf&lt;/code&gt;. En la softmax, una fila toda de &lt;code&gt;-inf&lt;/code&gt; es &lt;code&gt;0/0&lt;/code&gt;, o sea, un NaN. Es la misma trampa de la parte anterior, ahora en la versión de cuatro líneas. La biblioteca transformers, en cambio, rellena su máscara eager con el mínimo del dtype (&lt;code&gt;masking_utils.py:602-604&lt;/code&gt;) y esas filas salen uniformes. Así no dan error, pero siguen sin significar nada.&lt;/p&gt;
&lt;p&gt;La ruta eager calcula las exponenciales y su suma en fp32 sea cual sea el dtype del modelo (&lt;code&gt;dtype=torch.float32&lt;/code&gt;, en &lt;code&gt;modeling_llama.py:208&lt;/code&gt;), y después vuelve a convertir la fila a ese dtype. Sumar el normalizador con los ocho bits significativos de bf16 sería ahorrar memoria donde no toca.&lt;/p&gt;
&lt;h2 id=&quot;lectura-de-llamaattentionforward&quot;&gt;Lectura de &lt;code&gt;LlamaAttention.forward&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;En transformers 5.17.0, &lt;code&gt;modeling_llama.py:243-281&lt;/code&gt; son esas cuatro líneas con las cabezas, la máscara y una caché, todo escrito por extenso. Abreviado y con las formas del modelo 8B, queda así:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:95ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;query_states &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.q_proj(hidden_states).view(hidden_shape).transpose(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, 32, T, 128]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;key_states   &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.k_proj(hidden_states).view(hidden_shape).transpose(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, 8, T, 128]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;value_states &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.v_proj(hidden_states).view(hidden_shape).transpose(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# [B, 8, T, 128]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;query_states, key_states &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; apply_rotary_pos_emb(query_states, key_states, cos, sin)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; past_key_values &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;not&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;key_states, value_states &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; past_key_values.update(key_states, value_states, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.layer_idx)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;attn_output, attn_weights &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; attention_interface(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, query_states, key_states, value_states,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:48ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;                                                &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;attention_mask, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;scaling&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.scaling)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;attn_output &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.o_proj(attn_output.reshape(&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;input_shape, &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Ahí hay tres cosas que no están en las cuatro líneas. La primera, &lt;code&gt;past_key_values.update&lt;/code&gt;, es la caché. Al generar se guardan las keys y los values de todos los tokens anteriores, así que cada token nuevo calcula una sola fila de query y lee &lt;code&gt;T&lt;/code&gt; keys guardadas.&lt;/p&gt;
&lt;p&gt;La segunda es que la key y el value tienen 8 cabezas y la query, 32, de ahí que &lt;code&gt;k_proj&lt;/code&gt; proyecte de 4096 a 1024. Es lo que se llama grouped-query attention (GQA), frente a una cabeza key-value por cada cabeza de query (MHA). Su razón de ser es la caché. La tercera, &lt;code&gt;attention_interface&lt;/code&gt;, elige qué implementación se ejecuta, con &lt;code&gt;eager_attention_forward&lt;/code&gt; como respaldo, en &lt;code&gt;modeling_llama.py:191-213&lt;/code&gt;. &lt;em&gt;Eager&lt;/em&gt; quiere decir que las cuatro líneas se ejecutan tal como están escritas, más &lt;code&gt;repeat_kv&lt;/code&gt;, que en tiempo de ejecución copia las 8 cabezas de key hasta tener 32 para que cuadre la multiplicación de matrices. Si pones &lt;code&gt;_attn_implementation&lt;/code&gt; a &lt;code&gt;sdpa&lt;/code&gt;, la &lt;code&gt;scaled_dot_product_attention&lt;/code&gt; de la primera sección, o a &lt;code&gt;flash_attention_2&lt;/code&gt;, las mismas operaciones las calcula otro kernel (la rutina que hace la aritmética). La línea que aplica la rotación queda para la parte 7.&lt;/p&gt;
&lt;h2 id=&quot;el-coste-lo-ponen-los-datos-y-el-código-no-lo-delata&quot;&gt;El coste lo ponen los datos y el código no lo delata&lt;/h2&gt;
&lt;p&gt;En software, el coste de una función se lee en su código: un bucle sobre la entrada es lineal, dos bucles anidados son cuadráticos, y lo sabes antes de ejecutarla. La atención es una sola función, su código son las cuatro líneas y en ellas no hay ningún bucle. El cuadrado está dentro de un único &lt;code&gt;@&lt;/code&gt; y nada en la forma del código delata que sea cuadrático. Su coste lo fija la &lt;code&gt;T&lt;/code&gt; de la petición y crece con el cuadrado de las posiciones, por cabeza y por capa.&lt;/p&gt;
&lt;p&gt;Con la forma de Llama 3.1 8B y en bf16, las puntuaciones de una secuencia en una capa ocupan 268 MB con 2048 tokens, 4,3 GB con 8192 y 1,1 TB con los 131 072 que declara la configuración. En el pico de memoria, la ruta eager mantiene tres cuadrados así (dos de ellos en fp32), que suman unas cinco veces esos bytes. Los pesos del modelo entero ocupan 16 GB. Si nadie llega a materializar esos 1,1 TB, es por el kernel.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Cuánto cuesta la atención de Llama 3.1 8B (32 capas, 32 cabezas de anchura 128, bf16) según la longitud de contexto y el batch. Las puntuaciones de una capa son [B, H, T, T], y la caché guarda las keys y los values de todos los tokens anteriores en todas las capas. Los pesos, 16 GB, aparecen dibujados como referencia de escala.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-attention-learned-weighted-average/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Si llevas &lt;code&gt;T&lt;/code&gt; a 16k con &lt;code&gt;eager&lt;/code&gt; seleccionado, las puntuaciones de una sola capa ya superan los pesos del modelo entero, y con 131k son unas setenta veces más grandes. Al cambiar a &lt;code&gt;sdpa&lt;/code&gt;, la cifra aparece tachada porque esa memoria nunca llega a reservarse.&lt;/p&gt;
&lt;p&gt;He cronometrado las tres últimas líneas, con 8 cabezas de anchura 64, en la CPU de mi portátil. De 512 a 8192 tokens (dieciséis veces más entrada), la versión eager ha pasado de 1,9 ms a 284 ms, 150 veces más. En una capa lineal sobre la misma entrada, el tiempo ha subido de 0,23 ms a 2,5 ms, once veces más. Al doblar &lt;code&gt;T&lt;/code&gt; de 4096 a 8192, el tiempo de la capa lineal se ha multiplicado por 1,95 y el de la atención por 4,3. Salvo por el ruido de la medición, es lo que predice el cuadrado.&lt;/p&gt;
&lt;p&gt;La primera de las tres palancas que mueven esa cifra es el kernel. Con su backend flash o con el memory-efficient, &lt;code&gt;F.scaled_dot_product_attention&lt;/code&gt; calcula los mismos números por bloques y nunca guarda entera la matriz &lt;code&gt;T × T&lt;/code&gt;, cosa que su backend de respaldo, math, sí hace. Con 4096 tokens y en fp32, el profiler ha imputado 545 MB a las multiplicaciones de matrices de la versión eager (el cuadrado de 537 MB más su salida) y 8,5 MB a sdpa, que además ha ido 2,4 veces más rápido.&lt;/p&gt;
&lt;p&gt;Flash attention lleva esa misma idea a la GPU, con una aritmética que sigue siendo cuadrática. Lo que cambia es lo que tiene que estar en memoria a la vez. La implementación se elige con un único campo de la configuración, &lt;code&gt;_attn_implementation&lt;/code&gt;, que en 5.17.0 vale &lt;code&gt;sdpa&lt;/code&gt; por defecto al cargar; &lt;code&gt;eager&lt;/code&gt; solo lo tienes si lo pides. Si el batch lleva padding, sdpa sigue guardando la propia máscara, booleanos &lt;code&gt;[B, 1, T, T]&lt;/code&gt; que ocupan 17 MB con 4096. Sin padding, transformers no pasa ninguna máscara y deja que el kernel aplique el triángulo (&lt;code&gt;is_causal=True&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Las otras dos cambian lo que se calcula. La grouped-query attention reduce la caché. Con 8 cabezas key-value, el modelo 8B guarda 131 kB por token entre sus 32 capas, 17 GB para un contexto de 128k, ya algo más que los pesos. Con 32 cabezas serían 524 kB por token y 69 GB, cuatro veces los pesos. Una ventana deslizante, por su parte, limita hasta dónde puede leer hacia atrás una query. Llama 3.1 no tiene ninguna, Mistral 7B v0.1 tenía una, y Gemma 2 y 3 intercalan capas con ventana y capas globales. En transformers se escribe &lt;code&gt;kv_idx &amp;gt; q_idx - sliding_window&lt;/code&gt; (&lt;code&gt;masking_utils.py:100&lt;/code&gt;). Con 32 768 tokens, una ventana de 4096 deja 126 millones de celdas vivas frente a los 537 millones del triángulo causal. Mientras la secuencia no supera la ventana, el coste es un triángulo; cuando la supera, es una banda de anchura &lt;code&gt;W&lt;/code&gt; que crece de forma lineal con &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Para ser justos con el cuadrado, si en la pasada del prompt cuentas operaciones y dejas los bytes a un lado, las capas lineales del modelo 8B hacen más operaciones que la atención hasta unos 30 000 tokens, y hasta más o menos el doble si solo tienes en cuenta el triángulo que calcula un kernel causal. Con 8192 tokens, la atención supone alrededor de una quinta parte de las operaciones en coma flotante del bloque (sus FLOPs) si cuentas el cuadrado entero, y alrededor de una octava parte si cuentas solo el triángulo.&lt;/p&gt;
&lt;p&gt;Como el cuadrado es un problema de memoria mucho antes de convertirse en uno de cómputo, la cuenta que hay que hacer antes de prometer una longitud de contexto es la de memoria.&lt;/p&gt;
&lt;h2 id=&quot;qué-vigila-el-ingeniero-de-ml&quot;&gt;Qué vigila el ingeniero de ML&lt;/h2&gt;
&lt;p&gt;Son cuatro cosas, la primera de ellas una multiplicación.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Calcula lo que ocupan &lt;code&gt;[B, H, T, T]&lt;/code&gt; y la caché antes de prometer un contexto.&lt;/strong&gt; El gráfico de arriba hace esa multiplicación, cabezas por &lt;code&gt;T&lt;/code&gt; al cuadrado por dos bytes en cada capa. Súmale la caché por token multiplicada por la secuencia más larga y por el batch con el que vas a servir el modelo. El &lt;code&gt;max_position_embeddings&lt;/code&gt; de la configuración es lo que el modelo admite tras estirar su rotación (aquí 131 072, una ventana de preentrenamiento de 8192 escalada y luego entrenada hasta 128k). Lo que cabe en tu GPU es otra cuenta.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ten claro qué kernel se ejecuta.&lt;/strong&gt; &lt;code&gt;eager&lt;/code&gt; devuelve los pesos y materializa el cuadrado; &lt;code&gt;sdpa&lt;/code&gt; y flash, no. Se elige en &lt;code&gt;_attn_implementation&lt;/code&gt; y de ese campo depende que existan o no los 545 MB de antes. Una herramienta de depuración que necesite los pesos no puede sacarlos de &lt;code&gt;sdpa&lt;/code&gt;, donde &lt;code&gt;output_attentions=True&lt;/code&gt; devuelve una tupla vacía. Si para tenerlos cargas el modelo con &lt;code&gt;attn_implementation=&amp;quot;eager&amp;quot;&lt;/code&gt;, el cuadrado se materializa en cada llamada.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Combina las máscaras a conciencia.&lt;/strong&gt; La causal, la de padding y la de ventana se unen con un AND, que en la forma &lt;code&gt;-inf&lt;/code&gt; es una suma. Una fila de query a la que ya no le queda nada que leer da NaN en esa forma y una fila uniforme en la de la biblioteca. Ninguno de los dos es un número que te sirva. La costumbre de la parte 3 vale ahora por partida doble: el índice del último token sale de la máscara y la máscara viaja con el batch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mide la latencia con el prompt más largo que vayas a aceptar.&lt;/strong&gt; El tiempo de las capas lineales crece con &lt;code&gt;T&lt;/code&gt;; el de la atención, con su cuadrado, y el de la lectura de la caché en cada token generado, con todo lo que esta ya contiene. Un p99 medido con 500 tokens no dice nada de lo que pasa con 8000. El salto de antes, de 1,9 ms a 284 ms, da una idea de la sorpresa que te espera.&lt;/p&gt;
&lt;p&gt;Esto tampoco es trabajo de modelado. Una media ponderada cabe en cuatro líneas y sus pesos salen de tres matrices aprendidas. El cuadrado en el que viven es el primer coste de la serie que depende de la entrada sin que el código tenga nada que decir.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 4 de 12. La atención es una media ponderada cuyos pesos calcula el modelo a partir de su propia entrada. La media cabe en cuatro líneas; lo que cuesta es la matriz de pesos &lt;code&gt;T × T&lt;/code&gt;, y el kernel decide si ese cuadrado llega a estar alguna vez en memoria. Como la atención es solo una de las etapas, la siguiente parte lee el bloque entero que la rodea, con seis tensores y un flujo residual al que cada etapa suma lo suyo. Si has llegado aquí directamente, puedes empezar por &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;el tokenizador&lt;/a&gt; y seguir con &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;la tabla a la que apunta el id de un token&lt;/a&gt; y &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/&quot;&gt;la forma que las dos le pasan a esta&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Fundamentos de ML</category><category>Fundamentos de ML</category><category>From code to weights</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>Las formas son el sistema de tipos del ML, y nadie las comprueba por ti</title><link>https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/</guid><description>[B, T, d] es una firma de función que el runtime nunca verifica, y por eso un eje equivocado solo da error cuando los tamaños difieren. Si coinciden, la operación se resuelve por broadcasting, devuelve números plausibles y el bug llega a producción.</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 2 se cerraba con una firma: un batch de secuencias de filas de embedding es un tensor de forma &lt;code&gt;[B, T, d]&lt;/code&gt;. En todo este stack no hay nada más parecido a un tipo que esa línea, así que aquí la leo como se lee la firma de una función y luego enseño qué hace el runtime cuando la firma está mal. Lo habitual es que no haga nada.&lt;/p&gt;
&lt;p&gt;He leído la regla de broadcasting tal como la escribe torch y después he escrito a propósito cuatro bugs de forma para ver cuáles detectaba el runtime. Solo ha detectado uno.&lt;/p&gt;
&lt;h2 id=&quot;una-forma-es-una-firma&quot;&gt;Una forma es una firma&lt;/h2&gt;
&lt;p&gt;Un tensor es un array de N dimensiones junto con una descripción de cómo está colocado en memoria. &lt;code&gt;[B, T, d]&lt;/code&gt; dice que hay &lt;code&gt;B&lt;/code&gt; secuencias en el batch, &lt;code&gt;T&lt;/code&gt; posiciones en cada una y &lt;code&gt;d&lt;/code&gt; números en cada posición. Léelo como si fuera &lt;code&gt;f(batch: List[List[Vec]])&lt;/code&gt;, con una diferencia que importará más adelante, y es que el runtime solo mira los tamaños y los nombres le dan igual.&lt;/p&gt;
&lt;p&gt;La descripción de la memoria es lo que un ingeniero de software ya conoce. Un tensor de forma &lt;code&gt;[4, 8, 16]&lt;/code&gt; tiene strides &lt;code&gt;(128, 16, 1)&lt;/code&gt;, así que dar un paso por el primer eje supone saltarse 128 números. Si transpones los dos primeros ejes, la forma pasa a &lt;code&gt;[8, 4, 16]&lt;/code&gt; y los strides a &lt;code&gt;(16, 128, 1)&lt;/code&gt;. Es la misma memoria leída en otro orden, y &lt;code&gt;data_ptr&lt;/code&gt; no cambia. Por eso &lt;code&gt;transpose&lt;/code&gt; sale gratis, un &lt;code&gt;view&lt;/code&gt; que aplane el resultado se niega y lo dice en el mensaje de error, y &lt;code&gt;reshape&lt;/code&gt; copia cuando no le queda otro remedio. Un tensor es un puntero, una forma y una tupla de strides. Las operaciones de vista que vas a usar son aritmética sobre las dos últimas.&lt;/p&gt;
&lt;p&gt;Cada operación tiene también su propia firma de formas, que dice qué entra, qué sale y qué eje desaparece. &lt;code&gt;mean(dim=1)&lt;/code&gt; sobre &lt;code&gt;[B, T, d]&lt;/code&gt; devuelve &lt;code&gt;[B, d]&lt;/code&gt;, ya sin la &lt;code&gt;T&lt;/code&gt;. Una capa lineal lleva &lt;code&gt;[B, T, d]&lt;/code&gt; a &lt;code&gt;[B, T, d_out]&lt;/code&gt; y solo cambia el último eje. Escribir la firma al lado de la llamada es la disciplina de la que trata esta entrada, y si no la escribes tú, no la va a escribir nadie.&lt;/p&gt;
&lt;h2 id=&quot;la-regla-cabe-en-cinco-líneas&quot;&gt;La regla cabe en cinco líneas&lt;/h2&gt;
&lt;p&gt;Dos tensores de formas distintas se pueden sumar igualmente, y cómo se suman lo decide una regla llamada broadcasting. torch tiene una copia legible en Python, &lt;code&gt;torch.broadcast_shapes&lt;/code&gt;, que llama a &lt;code&gt;_broadcast_shapes&lt;/code&gt; en &lt;code&gt;torch/_refs/__init__.py&lt;/code&gt; (líneas 388 a 470 en la v2.13.0). Quitando la maquinaria de formas simbólicas, la regla queda así:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:92ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;common_shape &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;reduce&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;max&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;len&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(shape) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; shape &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; shapes))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; arg_idx, shape &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;enumerate&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(shapes):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; idx &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;range&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;len&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(shape), &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;):          &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# align from the right&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; common_shape[idx] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:12ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;common_shape[idx] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; shape[idx]              &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# 1 stretches to anything&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; shape[idx] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:12ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;            &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;continue&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;                                    &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# ... in either direction&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;torch._check(common_shape[idx] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; shape[idx],   &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# otherwise sizes must match&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:12ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;            &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;lambda&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;Attempting to broadcast a dimension of length &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#F47067&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;shape[idx]&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#F47067&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt; at &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#F47067&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;idx&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#F47067&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;!&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; common_shape&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Las formas se alinean por la derecha, un tamaño 1 se estira hasta igualar al otro, los tamaños iguales pasan y cualquier otra combinación lanza un error. No hay más. En modo eager, la misma regla se ejecuta en C++ y falla con un mensaje más corto, que es el que muestra la demo de abajo. La regla existe para que puedas sumar un sesgo de forma &lt;code&gt;[d]&lt;/code&gt; a &lt;code&gt;[B, T, d]&lt;/code&gt; sin bucles ni copias.&lt;/p&gt;
&lt;p&gt;El resto de la entrada gira en torno a una propiedad de la regla: decide por el tamaño y es ciega al significado. &lt;code&gt;[8, 16] + [16]&lt;/code&gt; funciona porque 16 es igual a 16, y &lt;code&gt;[8, 16] + [8]&lt;/code&gt; lanza un error porque 8 no es 16 ni es 1. La regla no tiene ni idea de si el 16 de la derecha es una longitud de secuencia, una dimensión oculta o un batch, y cuando ese eje sea el equivocado, tú tampoco lo sabrás. Y si dos ejes coinciden en tamaño por casualidad, la regla no tiene manera de distinguirlos. El científico de datos lleva años apoyándose en ella con numpy, y el ingeniero de software se la encuentra como una regla de coerción sin tipos declarados, que es como deberían leerla los dos.&lt;/p&gt;
&lt;h2 id=&quot;el-bug-que-solo-aparece-con-un-batch-de-dieciséis&quot;&gt;El bug que solo aparece con un batch de dieciséis&lt;/h2&gt;
&lt;p&gt;En el primer bug intervienen las puntuaciones de atención, de forma &lt;code&gt;[B, T, T]&lt;/code&gt; (una matriz &lt;code&gt;T × T&lt;/code&gt; por secuencia), y una máscara de padding de forma &lt;code&gt;[B, T]&lt;/code&gt; que dice qué posiciones de cada secuencia son reales. En su versión aditiva (0 en las reales y &lt;code&gt;-inf&lt;/code&gt; en el padding), basta con sumarla a las puntuaciones para apagar el padding. Lo correcto es &lt;code&gt;scores + mask[:, None, :]&lt;/code&gt;. Con el &lt;code&gt;None&lt;/code&gt;, la máscara pasa a ser &lt;code&gt;[B, 1, T]&lt;/code&gt;, se estira por el eje de las queries y enmascara las keys.&lt;/p&gt;
&lt;p&gt;Ahora quita el &lt;code&gt;None&lt;/code&gt;. Con &lt;code&gt;B = 8&lt;/code&gt; y &lt;code&gt;T = 16&lt;/code&gt;, &lt;code&gt;scores + mask&lt;/code&gt; lanza un error porque 16 no es 8, pero con &lt;code&gt;B = 16&lt;/code&gt; y &lt;code&gt;T = 16&lt;/code&gt; se ejecuta. Con &lt;code&gt;B = 1&lt;/code&gt; también se ejecuta y además acierta por casualidad, porque 1 es justo lo que habría insertado el &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;La regla alinea &lt;code&gt;[16, 16]&lt;/code&gt; con &lt;code&gt;[16, 16, 16]&lt;/code&gt; por la derecha y lee la máscara como &lt;code&gt;[1, B, T]&lt;/code&gt;, de modo que la máscara pensada para la secuencia &lt;code&gt;b&lt;/code&gt; cae en la fila de query &lt;code&gt;b&lt;/code&gt; de todas las secuencias. Con longitudes aleatorias he medido un tercio de las posiciones mal enmascaradas y una quinta parte del peso de atención sobre el padding. La softmax que viene detrás da valores finitos, la pérdida también y la ejecución sigue adelante. No salta ningún aviso, porque no hay nada de qué avisar. La regla se ha cumplido.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Puntuaciones de atención [B, T, T] más una máscara de padding. Arrastra los dos tamaños y elige la forma de la máscara. El veredicto es el de la regla de broadcasting de torch, y lo único que mira es si los tamaños coinciden.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Mueve los dos tamaños hasta que coincidan y verás cómo el veredicto pasa de un error a un resultado silencioso. Después elige &lt;code&gt;[B, T, 1]&lt;/code&gt;, una máscara que se ejecuta con cualquier tamaño y siempre está mal, porque apaga las queries cuando debería apagar las keys. Esta sí se nota, ya que la fila de una query de padding es toda &lt;code&gt;-inf&lt;/code&gt; y su softmax da NaN. En los tres casos la regla es la misma.&lt;/p&gt;
&lt;p&gt;El segundo bug es el clásico: predicciones de forma &lt;code&gt;[256]&lt;/code&gt; y valores objetivo de forma &lt;code&gt;[256, 1]&lt;/code&gt; que se restan para calcular un error cuadrático. La regla estira los dos a &lt;code&gt;[256, 256]&lt;/code&gt; y enfrenta cada predicción con cada objetivo. La media de todo eso sigue siendo un número, y con él se entrena.&lt;/p&gt;
&lt;p&gt;Con este bug he entrenado una regresión lineal de una sola variable durante dos mil pasos. La pérdida ha bajado hasta estabilizarse en 9,7. La pendiente ajustada ha salido 0,000 y la ordenada en el origen igual a la media de los objetivos, con un R² de 0,0. El modelo ha aprendido a ignorar la entrada, que es lo mejor que puede hacer cuando la pérdida lo compara con los objetivos de todos los demás. La pérdida de la biblioteca, &lt;code&gt;F.mse_loss&lt;/code&gt;, avisa de este desajuste. La escrita a mano, &lt;code&gt;((pred - target) ** 2).mean()&lt;/code&gt;, no avisa, y es la que acaba en los notebooks.&lt;/p&gt;
&lt;p&gt;El tercer bug nunca da error en el sitio donde ocurre. Sobre un tensor de forma &lt;code&gt;[16, 16, 64]&lt;/code&gt;, tanto &lt;code&gt;mean(dim=0)&lt;/code&gt; como &lt;code&gt;mean(dim=1)&lt;/code&gt; devuelven &lt;code&gt;[16, 64]&lt;/code&gt;. Los números son distintos, pero con la misma forma nada de lo que venga después puede notarlo. Sobre &lt;code&gt;[3, 5, 7]&lt;/code&gt;, esas mismas dos llamadas devuelven &lt;code&gt;[5, 7]&lt;/code&gt; y &lt;code&gt;[3, 7]&lt;/code&gt;, y lo primero que intente combinarlas lanza un error. El bug estaba en los dos casos y solo los tamaños han decidido si se veía.&lt;/p&gt;
&lt;p&gt;El cuarto hace de control. &lt;code&gt;[3, 5, 7]&lt;/code&gt; más &lt;code&gt;[3, 5]&lt;/code&gt; falla porque 7 no es 5, y si se ha detectado es por lo único que les faltaba a los otros tres, que los tamaños fueran distintos.&lt;/p&gt;
&lt;p&gt;Para ser justos con la regla, hay que decir que hace lo que se le pide. Alternativa hay: un runtime que lleve los nombres de los ejes y se niegue a sumar &lt;code&gt;T&lt;/code&gt; con &lt;code&gt;B&lt;/code&gt;. torch publicó como prototipo los named tensors, que nunca pasaron de esa fase, y la 2.13 los ha eliminado para quitar sobrecarga y aligerar el código. Si no cuajaron fue en parte porque todas las bibliotecas del stack habrían tenido que ponerse de acuerdo en los nombres, y lo único que comparten todas son los tamaños. La regla es correcta, solo que no es una comprobación de tipos, y en todo el stack no hay nada que lo sea.&lt;/p&gt;
&lt;h2 id=&quot;el-padding-es-una-decisión-de-forma&quot;&gt;El padding es una decisión de forma&lt;/h2&gt;
&lt;p&gt;Las secuencias no vienen todas con la misma longitud. Tres secuencias de longitudes 5, 3 y 1 solo forman un tensor &lt;code&gt;[3, 5]&lt;/code&gt; si se rellenan, y con esas longitudes el 40 % del tensor es padding. A partir de ahí, toda operación sobre &lt;code&gt;T&lt;/code&gt; necesita saber qué celdas son reales, y ese dato es un segundo tensor, la máscara, que tiene que viajar con el primero.&lt;/p&gt;
&lt;p&gt;El lado por el que se rellena forma parte de la firma. Con padding por la derecha, el último token real está en el índice &lt;code&gt;length - 1&lt;/code&gt;, que para las tres de arriba es 4, 2 y 0. Con padding por la izquierda está en el 4 para las tres. Los decodificadores que generan token a token prefieren esta segunda opción, porque la posición que hay que leer es siempre la última. Las dos funcionan y el bug está en mezclarlas. El código pensado para padding por la derecha, si recibe un batch rellenado por la izquierda, lee padding, o el token equivocado, donde espera el último.&lt;/p&gt;
&lt;p&gt;La máscara no es opcional ni siquiera cuando el modelo no usa atención. Si haces pooling de media de ese batch &lt;code&gt;[3, 5]&lt;/code&gt; sin ella, el vector de la secuencia de longitud 1 promedia una fila real con cuatro de padding. Con &lt;code&gt;arange(1, 31).reshape(3, 5, 2)&lt;/code&gt;, la media de esa secuencia sin máscara es &lt;code&gt;[25, 26]&lt;/code&gt;, y con máscara es &lt;code&gt;[21, 22]&lt;/code&gt;, que es su única fila real. Por eso el código de pooling de la parte 2 divide por la suma de la máscara, y esa división es la firma puesta por escrito.&lt;/p&gt;
&lt;h2 id=&quot;la-memoria-es-forma-por-bytes&quot;&gt;La memoria es forma por bytes&lt;/h2&gt;
&lt;p&gt;Lo que cuesta un tensor es pura aritmética. Unas activaciones de forma &lt;code&gt;[32, 2048, 4096]&lt;/code&gt; son 268 millones de números, que ocupan 1,07 GB en fp32, 537 MB en bf16 y 268 MB en int8. Esa es la factura, y para calcularla no hace falta saber nada del modelo, basta con la forma y el ancho de un número. De esa cifra depende que un batch quepa o no en la tarjeta.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Los tensores más grandes de un bloque de decodificador para un batch, con las puntuaciones de atención guardadas como una matriz T × T completa. Cada cabeza mide 64 de ancho, así que hay d / 64 cabezas. Arrastra los tres tamaños y cambia el formato numérico: los bytes de cada fila son la forma por el ancho de un número, y los ids de token son int64 sea cual sea el formato. En int8 la cifra solo pone precio a la forma, porque en una ejecución real las puntuaciones y los pesos no se guardan en int8. Las dos últimas filas llevan T dos veces.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tensors-shapes-type-system/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Mueve los tres tamaños y cambia el formato. Con los valores iniciales, un bloque cuesta 77 GB en fp32 y las dos filas de &lt;code&gt;T × T&lt;/code&gt; suponen el 89 % del total, pero si bajas &lt;code&gt;T&lt;/code&gt; a 128 se quedan en un tercio. De esas dos filas trata la parte siguiente.&lt;/p&gt;
&lt;p&gt;Lo que sacrifica cada formato también es aritmética. Donde fp32 guarda 23 bits de mantisa, bf16 guarda 7 y conserva todo el exponente de fp32. Con esos 7 bits escribe π como 3,140625 y redondea &lt;code&gt;1 + 0.004&lt;/code&gt; a &lt;code&gt;1.0078125&lt;/code&gt;, pero su rango es el de fp32 salvo por el último paso de redondeo. fp16 guarda 10 bits y solo cinco de exponente, así que no pasa de 65 504 y los gradientes pequeños se quedan en cero por underflow si antes no se escala la pérdida. De ahí que el entrenamiento se haya pasado a bf16 y que para servir el modelo se recurra a int8. Cada formato es una decisión sobre qué precisión no le hace falta a un tensor.&lt;/p&gt;
&lt;p&gt;Al servir el modelo, el eje que controlas es el del batch, y mueve dos números en sentidos opuestos. He cronometrado en la CPU de mi portátil un matmul de &lt;code&gt;[B, 4096]&lt;/code&gt; por &lt;code&gt;[4096, 4096]&lt;/code&gt;, que cuesta 447 µs por fila con &lt;code&gt;B = 1&lt;/code&gt; y 22 µs por fila con &lt;code&gt;B = 512&lt;/code&gt;, es decir, veinte veces más throughput. La llamada en sí ha pasado de 0,45 ms a 11,4 ms, y cada petición ha esperado veinticinco veces más, sin contar lo que haya tardado en llenarse el batch. Con cinco repeticiones en un solo portátil, lo que vale es la proporción y los dígitos exactos importan poco. El batching dinámico es el ajuste que se mueve entre esos dos números, y dónde lo pongas es una decisión de producto disfrazada de forma de tensor.&lt;/p&gt;
&lt;h2 id=&quot;qué-vigila-el-ingeniero-de-ml&quot;&gt;Qué vigila el ingeniero de ML&lt;/h2&gt;
&lt;p&gt;Son cuatro hábitos, aunque los dos primeros son en realidad uno solo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anota las formas en los límites de las funciones.&lt;/strong&gt; &lt;code&gt;jaxtyping&lt;/code&gt; con &lt;code&gt;beartype&lt;/code&gt; convierte la firma en una comprobación que se ejecuta en cada llamada:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:87ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#6F42C1;--1:#DCBDFB&quot;&gt;@jaxtyped&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;typechecker&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;beartype)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#DCBDFB&quot;&gt;pool&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(x: Float[Tensor, &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;B T d&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;], mask: Bool[Tensor, &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;B T&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;]) -&amp;gt; Float[Tensor, &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;B d&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;]:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;m &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; mask[:, :, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;].float()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (x &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; m).sum(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; m.sum(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Si dentro de la función reduces por el eje equivocado, salta un error al devolver el resultado, porque &lt;code&gt;[T, d]&lt;/code&gt; no es &lt;code&gt;[B, d]&lt;/code&gt;. Y si pasas al error cuadrático el objetivo con forma de columna, la comprobación falla antes de que la regla pueda estirar nada. Así se detectan en el límite dos de los bugs de arriba (la reducción, solo cuando &lt;code&gt;B&lt;/code&gt; ≠ &lt;code&gt;T&lt;/code&gt;), con la forma escrita justo donde va a mirar el siguiente que lea el código. Para el bug del batch de dieciséis, y para la reducción por el eje equivocado cuando &lt;code&gt;B&lt;/code&gt; es igual a &lt;code&gt;T&lt;/code&gt;, hace falta el hábito siguiente.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prueba con tamaños que no puedan coincidir.&lt;/strong&gt; La anotación no ve el bug del batch de dieciséis. Cuando &lt;code&gt;B&lt;/code&gt; y &lt;code&gt;T&lt;/code&gt; valen 16, los dos nombres se ligan al mismo número y la comprobación pasa. Con la reducción por el eje equivocado ocurre lo mismo, porque &lt;code&gt;[T, d]&lt;/code&gt; y &lt;code&gt;[B, d]&lt;/code&gt; son el mismo &lt;code&gt;[16, 64]&lt;/code&gt;. El arreglo va en los tests, con &lt;code&gt;B = 3&lt;/code&gt;, &lt;code&gt;T = 5&lt;/code&gt; y &lt;code&gt;d = 7&lt;/code&gt;, y el código se queda como está. Cada &lt;code&gt;None&lt;/code&gt; olvidado y cada confusión entre &lt;code&gt;B&lt;/code&gt;, &lt;code&gt;T&lt;/code&gt; y &lt;code&gt;d&lt;/code&gt; dan entonces un tamaño que no coincide con nada, y el runtime lanza un error. Los ejes de tamaño 1 se siguen estirando con cualquier tamaño y las dos &lt;code&gt;T&lt;/code&gt; de las puntuaciones son siempre iguales; esos casos necesitan la anotación o un test que compruebe valores. Los números redondos en los tests son los que dejan que el bug del batch de dieciséis llegue a producción. Por eso los dos primeros hábitos son uno solo: la anotación nunca es más estricta que los tamaños que ve.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La máscara y el lado del padding son una sola decisión.&lt;/strong&gt; Apunta el lado junto a la configuración del tokenizador, calcula el índice del último token a partir de la máscara (nunca de &lt;code&gt;T&lt;/code&gt;) y rechaza cualquier batch cuya máscara no cuadre con su padding. El código de pooling de la parte 2 ya divide por la suma de la máscara; asegúrate de que el tuyo también lo haga.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Calcula lo que cuesta un tensor antes de reservarlo.&lt;/strong&gt; Multiplica la forma por los bytes de un número, para el tamaño de batch y el formato con los que vayas a servir el modelo. La demo de arriba hace esa multiplicación, y el resultado lo conoces antes de cargar un solo peso.&lt;/p&gt;
&lt;p&gt;Esto tampoco es trabajo de modelado. La forma es el único tipo que este stack no comprueba nunca. Si los dispositivos no coinciden, salta un error, pero un eje equivocado se resuelve por broadcasting, y esa comprobación te toca escribirla a ti.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 3 de 12. Una forma es una firma que nadie verifica. Como el broadcasting decide por tamaño, un eje equivocado solo provoca un error si da la casualidad de que los tamaños son distintos, y hay bugs que solo existen con un tamaño de batch concreto. Las puntuaciones de la primera demo y las dos filas ámbar de la segunda llevan &lt;code&gt;T&lt;/code&gt; dos veces, una matriz &lt;code&gt;T × T&lt;/code&gt; por secuencia (y por cabeza, en la segunda), y forman parte de la atención. La parte siguiente explica de dónde sale ese cuadrado y cuánto cuesta. Si has llegado aquí directamente, puedes empezar por &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;el tokenizador&lt;/a&gt; y seguir con &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;la tabla a la que apunta el id de un token&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Fundamentos de ML</category><category>Fundamentos de ML</category><category>From code to weights</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>Un embedding es una tabla de consulta, y todo lo demás es cómo se rellena</title><link>https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/</guid><description>Es la fila de una matriz indexada por id. La ciencia está en cómo se rellena; la ingeniería, en que no hay migración de esquema: otro modelo invalida tu índice.</description><pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 1 terminó en un índice. El tokenizador convierte el texto en ids, cada id es un número de fila y esa fila está en una matriz con la que nace el modelo. Ahora toca leer esa matriz, que es menos de lo que parece, apenas una tabla de consulta indexada por enteros y con contenido aprendido. La ciencia está en cómo llega ahí ese contenido, y la ingeniería, en lo que ya no puedes hacer con la tabla una vez rellena.&lt;/p&gt;
&lt;p&gt;Para esta entrada he leído dos ficheros. El primero es el &lt;code&gt;forward&lt;/code&gt; de &lt;code&gt;torch.nn.Embedding&lt;/code&gt;, que se reduce a una sola llamada, y el segundo, el módulo de pooling de sentence-transformers, donde se fabrica la otra clase de embedding, la que guardas en un índice. Después he montado un índice de 48 líneas y lo he roto adrede de tres maneras para medir qué falla.&lt;/p&gt;
&lt;h2 id=&quot;una-matriz-y-un-índice&quot;&gt;Una matriz y un índice&lt;/h2&gt;
&lt;p&gt;Si alguna vez has programado una tabla hash, llevas escrita casi toda una capa de embedding. &lt;code&gt;nn.Embedding(V, d)&lt;/code&gt; reserva una matriz de &lt;code&gt;V&lt;/code&gt; filas y &lt;code&gt;d&lt;/code&gt; columnas, y consultar el id &lt;code&gt;i&lt;/code&gt; devuelve la fila &lt;code&gt;i&lt;/code&gt;. En eso consiste toda la pasada hacia delante, como muestra el propio código de torch en &lt;code&gt;sparse.py:188-197&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:43ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#DCBDFB&quot;&gt;forward&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(self, input: Tensor) -&amp;gt; Tensor:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; F.embedding(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.weight,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.padding_idx,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.max_norm,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.norm_type,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.scale_grad_by_freq,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.sparse,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;F.embedding&lt;/code&gt; es un gather, y no hace falta fiarse porque se puede comprobar. Con un &lt;code&gt;Embedding(50257, 768)&lt;/code&gt; recién creado y tres ids, &lt;code&gt;torch.equal(emb(ids), emb.weight[ids])&lt;/code&gt; da &lt;code&gt;True&lt;/code&gt;. No hay transformación, ni hash, ni similitud de por medio. A cada id le corresponde su fila.&lt;/p&gt;
&lt;p&gt;Donde deja de parecerse a una tabla hash es en el tamaño. La tabla de entrada de GPT-2, &lt;code&gt;wte.weight&lt;/code&gt;, tiene 50 257 filas por 768 columnas, o sea, 38,6 millones de números que ocupan 154 MB en fp32, y cada uno de ellos es un parámetro entrenado. &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;, el modelo de frases que uso en el resto de la entrada, resuelve lo mismo con una tabla de 30 522 por 384. Si la fila 11 241 de la tabla de GPT-2 es el vector de &lt;code&gt;&amp;quot; token&amp;quot;&lt;/code&gt;, es porque ahí lo manda el tokenizador, como se vio en la parte 1. El contenido de la fila se aprendió, pero la dirección ya estaba fijada antes de que empezara el entrenamiento.&lt;/p&gt;
&lt;p&gt;Al ingeniero que viene del software le sorprende que las claves no signifiquen nada. La clave de una tabla hash significa algo por sí misma, mientras que la fila 11 241 solo tiene sentido dentro del modelo que la entrenó. Al científico de datos le sorprende justo lo contrario. Lo que con word2vec era un artefacto que se produce y luego se usa, aquí es un parámetro del propio modelo y se actualiza en el mismo paso de gradiente que todo lo que tiene encima. Nunca sale de ahí.&lt;/p&gt;
&lt;h2 id=&quot;dos-maneras-de-rellenar-la-tabla&quot;&gt;Dos maneras de rellenar la tabla&lt;/h2&gt;
&lt;p&gt;Hay dos maneras de darle números a una fila, y de cuál se use depende lo que puedas hacer con el resultado.&lt;/p&gt;
&lt;p&gt;La primera es la de antes, con la tabla como parámetro de un modelo de lenguaje. Las filas empiezan con valores aleatorios y les van llegando los gradientes a través de las capas de encima. Al acabar el entrenamiento, cada fila es lo que haya ayudado al modelo a predecir el siguiente token. Nadie diseña ese espacio, que surge como efecto secundario. Del gradiente que lo rellena se ocupa la parte 8.&lt;/p&gt;
&lt;p&gt;La segunda es la que despliega en la práctica la mayoría de los ingenieros. Coges un modelo entrenado, le pasas un texto y te quedas con la salida como vector de ese texto. Esa fila no se guarda en ningún sitio. Se calcula cada vez que hace falta y depende de la entrada entera, de todos sus ids a la vez.&lt;/p&gt;
&lt;p&gt;La palabra &lt;code&gt;bank&lt;/code&gt; tiene una fila en la tabla de entrada de ese modelo de frases, la 2924. Se le suma una tabla de posiciones, pero la fila de la palabra es la misma en «she sat on the river bank» y en «she opened a bank account». A la salida, en cambio, los dos vectores de ese mismo token tienen un coseno de 0,77 entre sí. Se consulta la misma fila y sale un resultado distinto, porque entre la tabla y la salida hay una posición y seis capas. Eso es lo que quiere decir «contextual», y ahí está toda la diferencia entre un embedding estático y el que metes en un índice de búsqueda.&lt;/p&gt;
&lt;p&gt;Lo que sale es un vector por token, pero una frase necesita uno solo, así que algo tiene que condensar en uno los &lt;code&gt;T&lt;/code&gt; vectores de los tokens. De eso se encarga el pooling. En sentence-transformers todas las variantes están en un mismo sitio. En la ruta por defecto, con padding, la rama por la que pasa un modelo con pooling de media es &lt;code&gt;pooling.py:200-213&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:112ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;elif&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; mode &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;mean&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;mean_sqrt_len_tokens&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; mean_sum &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;mask &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; attention_mask.unsqueeze(&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;).expand_as(token_embeddings).to(token_embeddings.dtype)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;mean_sum &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (token_embeddings &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; mask).sum(&lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;dim&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# ... mean_mask is the count of real tokens (or a supplied per-token weight sum), clamped away from zero&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; mode &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;mean&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;output_vectors.append(mean_sum &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; mean_mask)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Suma los tokens que la máscara de atención marca como reales y divide entre cuántos había. En esa función conviven seis modos, cada uno una manera distinta de reducir los mismos &lt;code&gt;T&lt;/code&gt; vectores. Por ejemplo, &lt;code&gt;cls&lt;/code&gt; se queda con el primero, &lt;code&gt;lasttoken&lt;/code&gt; con el último y &lt;code&gt;max&lt;/code&gt; con el mayor de cada dimensión. Cada modelo se entrena con uno de ellos, y esa elección forma parte del artefacto.&lt;/p&gt;
&lt;p&gt;Lo mismo vale para el paso siguiente. El &lt;code&gt;modules.json&lt;/code&gt; del propio modelo pequeño enumera lo que se ejecuta al llamar a &lt;code&gt;encode&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;json&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:98ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;[&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:2ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;idx&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;path&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;sentence_transformers.models.Transformer&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:2ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;idx&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;1&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;path&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;1_Pooling&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;sentence_transformers.models.Pooling&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:2ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;idx&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;2&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;path&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;2_Normalize&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#8DDB8C&quot;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;sentence_transformers.models.Normalize&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Primero va el Transformer, luego el pooling y al final la normalización a longitud unitaria. En el tercero no hay acuerdo ni siquiera dentro de la familia: &lt;code&gt;paraphrase-MiniLM-L6-v2&lt;/code&gt; viene sin él, y sus vectores en bruto tienen normas de alrededor de seis. De los tres módulos, los dos últimos son los que un cliente suele reimplementar, y si lo hace de otra manera, la respuesta cambia. Cuánto cambia se mide dos secciones más abajo.&lt;/p&gt;
&lt;h2 id=&quot;la-cercanía-es-un-producto-escalar&quot;&gt;La cercanía es un producto escalar&lt;/h2&gt;
&lt;p&gt;Cuando todos los vectores tienen longitud unitaria, la similitud coseno es un producto escalar, y buscar el vecino más cercano se reduce a multiplicar matrices y ordenar. GPT-2 lee su propia tabla del mismo modo a la salida, cuando calcula los logits del siguiente token como el producto escalar con cada fila de &lt;code&gt;wte&lt;/code&gt;. La parte de recuperación no tiene más, y por eso la factura del índice es pura aritmética. Diez millones de documentos con 384 dimensiones ocupan 15,4 GB en fp32 sin contar la estructura del índice; con 768 son 30,7 GB, y con 1536, la dimensión de un conocido modelo de embeddings que se sirve por API, 61,4 GB. La dimensión es un coste que pagas tú por cada documento mientras exista el índice, pero que fijó quien entrenó el modelo.&lt;/p&gt;
&lt;p&gt;El número que devuelve la búsqueda sirve para menos de lo que parece. En mi corpus de 48 líneas, la mejor coincidencia para «how do I undo a bad release» tiene un coseno de 0,30 con la consulta y es la línea correcta, el procedimiento de rollback. La mejor para «the migration is stuck waiting on a lock» saca 0,71 y también acierta. Un coseno es una posición en un espacio cuya escala ha elegido el modelo, y no sirve como medida de confianza. Compara el orden de los resultados dentro de un mismo modelo y nunca compares el número en bruto entre dos.&lt;/p&gt;
&lt;h2 id=&quot;no-hay-migración-de-esquema&quot;&gt;No hay migración de esquema&lt;/h2&gt;
&lt;p&gt;Lo que viene ahora no tiene equivalente en el software. Cuando cambias el esquema de una base de datos, escribes una migración, la ejecutas y las filas viejas quedan con la forma nueva. Un espacio de embeddings no tiene migración. Dos modelos producen dos espacios y ninguno trae un mapa hacia el otro, porque nadie los diseñó y cada uno es donde fue a parar el entrenamiento. Puedes ajustar un mapa a posteriori, y hay quien lo hace. El resultado es una aproximación aprendida, con su propio error, y eso no es una migración. Con un mapa lineal ajustado sobre cuatro mil frases ajenas al corpus, el índice desfasado que mido más abajo pasa del 60 % al 70 % y no sube más. Si recalculas los embeddings con un modelo nuevo, cada vector que tienes guardado queda en un sistema de coordenadas que no usa nadie más.&lt;/p&gt;
&lt;p&gt;Esperaba que eso se tradujera en resultados basura, y lo que sale es algo peor.&lt;/p&gt;
&lt;p&gt;La prueba usa 48 líneas de un runbook, ocho de cada tema: despliegues, autenticación, facturación, bases de datos, observabilidad y guardias. Las he indexado con el modelo L6 y he lanzado las consultas con &lt;code&gt;all-MiniLM-L12-v2&lt;/code&gt;, la actualización a la que recurriría un ingeniero (misma familia y mismas 384 dimensiones, así que el índice acepta los vectores sin rechistar).&lt;/p&gt;
&lt;p&gt;Frente a lo que devolvería el índice actualizado, el desfasado conserva el 60 % de los cinco primeros resultados. Al azar se conservaría el 10 %, y adivinando cinco líneas del tema correcto, el 35 %. El índice viejo consultado con el modelo viejo conserva el 77 %: el desajuste cuesta diecisiete puntos y los otros veintitrés se deben a que los dos modelos no coinciden en la respuesta. Con «user keeps getting logged out», el primer resultado sigue siendo la línea de la caducidad de sesión, pero el tercero es la política de retención de las copias de seguridad de la base de datos. Nada en la respuesta te dice cuál es cuál.&lt;/p&gt;
&lt;p&gt;Para ser justos con la cifra, un 60 % no es un índice revuelto, y para explicarlo no basta con que los dos modelos trabajen en inglés: la razón está en el linaje. Ambos se ajustaron a partir del mismo checkpoint padre, del que el L6 conserva solo una capa de cada dos, con lo que sus espacios empezaron en las mismas coordenadas. La misma línea codificada con uno y otro tiene, de media, un coseno de 0,54 consigo misma, muy por encima de lo que darían dos vectores ortogonales. Dos modelos que solo compartieran los datos de entrenamiento no compartirían coordenadas, y un índice desfasado entre ellos debería quedar cerca del azar. Además, seis temas bien delimitados de ocho líneas cada uno son el corpus más favorable que podría pedir un índice desfasado. No he medido un índice en producción, y esperaría que conservara menos. Lo que sí respalda la cifra es la forma del fallo, que es parcial, verosímil y silencioso.&lt;/p&gt;
&lt;p&gt;El módulo de pooling produce el mismo efecto sin cambiar de modelo. Con los mismos pesos, si cambias &lt;code&gt;mean&lt;/code&gt; por &lt;code&gt;cls&lt;/code&gt; al indexar y al consultar, los cinco primeros conservan el 50 % de la respuesta con pooling de media. Si los mezclas, con media en el índice y &lt;code&gt;cls&lt;/code&gt; en la ruta de consulta, conservan el 70 %. Sale mejor que con &lt;code&gt;cls&lt;/code&gt; en los dos lados porque uno de ellos sigue usando el pooling con el que se entrenó el modelo. Un cliente que reimplementa el pooling ha cambiado el modelo.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Un índice de 48 líneas de un runbook que se reindexa línea a línea con el modelo actualizado. Avanza la reindexación y elige qué modelo codifica las consultas. El recall se mide respecto a lo que devuelve la migración terminada, y las líneas que se cuelan son verosímiles. Las líneas y las consultas siguen en inglés porque son el texto que codificaron los modelos.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Avanza la reindexación y luego cambia el modelo que codifica las consultas. Con el modelo nuevo, el recall sube a medida que migra la tabla. Con el viejo empieza en 0,77 porque dos modelos usados de forma coherente ya discrepan en una cuarta parte de las respuestas. Después cae hasta 0,63 en la parte central de la migración, no vuelve al 0,77 hasta las últimas líneas y termina por debajo, en 0,73. Para entonces, cada línea del índice la ha escrito un modelo que la ruta de consulta no usa.&lt;/p&gt;
&lt;p&gt;Esa segunda curva es la que te encuentras en producción, donde el índice sale de un proceso por lotes, el codificador de consultas es un servicio desplegado y cada uno se actualiza un día distinto.&lt;/p&gt;
&lt;h2 id=&quot;qué-vigila-el-ingeniero-de-ml&quot;&gt;Qué vigila el ingeniero de ML&lt;/h2&gt;
&lt;p&gt;Son cuatro hábitos, todos baratos al lado de una reindexación.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;El modelo de embeddings forma parte del índice.&lt;/strong&gt; Guarda en los metadatos del índice el hash del modelo, el modo de pooling, el paso de normalización y la longitud máxima de secuencia, y rechaza cualquier consulta cuyo codificador no coincida. El &lt;code&gt;modules.json&lt;/code&gt; de antes es la mitad de ese contrato; el modo de pooling está a su lado, en &lt;code&gt;1_Pooling/config.json&lt;/code&gt;, y el hash lo añades tú. La longitud máxima importa. El modelo L6 lee 256 tokens y la versión L12 solo 128, de modo que el cambio que rompió el índice también redujo a la mitad, sin avisar, lo que ve cada vector. Todo eso tiene que ir con el índice, fuera de la configuración de cualquier cliente.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Corte limpio, sin reindexación gradual.&lt;/strong&gt; Construye el índice nuevo junto al viejo, cambia consultas e índice a la vez, detrás de un único flag, y borra el viejo al terminar. Una reindexación gradual con la ruta de consulta en marcha es justo la curva que cae en la demo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mide el solapamiento de vecinos y olvídate del coseno.&lt;/strong&gt; Mantén un conjunto fijo de consultas con su top-k esperado y calcula con él el recall@k cada vez que cambies de modelo. Un umbral de coseno ajustado con un modelo no significa nada con el siguiente, y el conjunto de vecinos es lo único observable que sobrevive al cambio. Eso sí, el solapamiento mide la deriva y deja fuera la relevancia: con el índice ya migrado, los cinco primeros de la consulta del cierre de sesión todavía incluyen una línea sobre almacenamiento en frío.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La dimensión es una factura.&lt;/strong&gt; Son N por d por cuatro bytes, sin contar la estructura del índice. La media precisión reduce la factura a la mitad, y algunos modelos recientes te dejan quedarte solo con las k primeras dimensiones. Las dos cosas se pueden aplicar a vectores ya guardados sin recalcular los embeddings, aunque los vectores truncados hay que renormalizarlos y las consultas hay que cortarlas a la misma k. Ambas decisiones van en los metadatos del índice, con todo lo demás. Calcula lo que te costará la dimensión de un modelo mayor con el corpus que tendrás dentro de dos años, porque el de hoy no sirve para esa cuenta.&lt;/p&gt;
&lt;p&gt;Esto tampoco es trabajo de modelado. Un índice de embeddings parece una base de datos con una columna de floats hasta el día en que alguien actualiza el modelo que escribió esa columna.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 2 de 12. Un embedding es una fila de una matriz que se rellena con el gradiente o con la salida de un modelo, y el espacio en el que vive no tiene migración: si cambias el modelo, todos los vectores guardados quedan desfasados sin avisar. Un batch de secuencias de esas filas es un tensor con la firma &lt;code&gt;[B, T, d]&lt;/code&gt;, y la siguiente parte lee esa firma como leerías la de una función y descubre que el runtime nunca la comprueba. Si has llegado aquí directamente, puedes empezar por &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;el tokenizador que decide en qué fila caes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Fundamentos de ML</category><category>Fundamentos de ML</category><category>From code to weights</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>Un modelo no lee texto: lo que el tokenizador decide por ti</title><link>https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/</guid><description>El tokenizador parece ir delante del modelo, pero va dentro. Leo en el BPE de tiktoken qué fija antes del primer peso: vocabulario, coste y límite de contexto.</description><pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div class=&quot;lede-intro&quot;&gt;&lt;p&gt;Un modelo de lenguaje no ha visto nunca una letra.&lt;/p&gt;&lt;p&gt;Solo ve enteros. En algún punto entre tu cadena y la primera multiplicación de matrices hay una función que convierte el texto en una lista de ids, y esa función quedó congelada el día en que se entrenó el modelo. Aunque parezca preprocesamiento, forma parte del artefacto. Cámbiala y los pesos seguirán ahí, pero el modelo que tenías habrá desaparecido.&lt;/p&gt;&lt;p&gt;Vengo de los compiladores, donde el parser es tuyo y puedes cambiarlo un martes cualquiera. Aquí solo es tuyo una vez, antes del entrenamiento, y ya nunca más.&lt;/p&gt;&lt;p&gt;Con este post empieza &lt;a href=&quot;https://cchinchilla.dev/es/series/&quot;&gt;una serie&lt;/a&gt; para ingenieros que dan el salto a la ingeniería de ML desde el software o desde la ciencia de datos. Cada parte se ocupa de una cosa que no se comporta como en el software. En esta le toca al parser, que va dentro del modelo.&lt;/p&gt;&lt;/div&gt;
&lt;h2 id=&quot;un-lexer-con-otro-objetivo&quot;&gt;Un lexer con otro objetivo&lt;/h2&gt;
&lt;p&gt;Si has escrito un compilador, ya has escrito un tokenizador. El lexer recibe caracteres y emite un flujo de símbolos (identificadores, palabras clave, literales). Su objetivo es reconocer la estructura para que el parser pueda construir un árbol.&lt;/p&gt;
&lt;p&gt;El tokenizador de un modelo es igual por fuera, pero persigue otra cosa. Aquí no hay estructura que reconocer. Se trata de comprimir, de convertir una cadena en el menor número posible de ids de un vocabulario fijo, para que el modelo vea menos posiciones y el vocabulario siga siendo lo bastante pequeño como para dar a cada id su fila en la matriz de embeddings.&lt;/p&gt;
&lt;p&gt;Ninguno de los dos extremos funciona. Con caracteres, el vocabulario es minúsculo y las secuencias salen demasiado largas; cada posición se paga en atención, cuyo coste crece con el cuadrado de la longitud. Con palabras enteras, las secuencias son cortas, pero el vocabulario queda sin acotar y, encima, deja un agujero por cada palabra que no esté en él. A ese agujero el científico de datos lo llama «fuera de vocabulario» y el ingeniero de software, «caso no contemplado».&lt;/p&gt;
&lt;p&gt;El término medio son las subpalabras. &lt;a href=&quot;https://arxiv.org/abs/1508.07909&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Sennrich, Haddow y Birch (2016)&lt;/a&gt; cogieron el byte-pair encoding, un algoritmo de compresión de 1994, y lo aplicaron a este otro problema para aprender las unidades de subpalabra a partir del corpus, sin tener que declararlas de antemano.&lt;/p&gt;
&lt;h2 id=&quot;bpe-es-un-solo-bucle&quot;&gt;BPE es un solo bucle&lt;/h2&gt;
&lt;p&gt;He leído el &lt;code&gt;_educational.py&lt;/code&gt; de &lt;a href=&quot;https://github.com/openai/tiktoken&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;tiktoken&lt;/code&gt;&lt;/a&gt;. Su codificador es una versión legible del núcleo en Rust, cosa que he comprobado ejecutando los dos sobre todas las muestras de más abajo y comparando los ids. El entrenador, en cambio, no imita nada. El núcleo en Rust no trae entrenador, así que &lt;code&gt;bpe_train&lt;/code&gt; (&lt;code&gt;_educational.py:119-185&lt;/code&gt;) es la única versión del bucle que hay en el paquete. Todo el entrenamiento es un solo bucle, y este es el cuerpo de &lt;code&gt;bpe_train&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:61ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;len&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(ranks) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; vocab_size:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;stats &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; collections.Counter()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; piece &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; words:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; pair &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;zip&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(piece[:&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;], piece[&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:]):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:12ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;stats[pair] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;most_common_pair &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;max&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(stats, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=lambda&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; x: stats[x])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;token_bytes &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; most_common_pair[&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; most_common_pair[&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;token &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;len&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(ranks)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# Add the new token!&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;ranks[token_bytes] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; token&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# ... then replace that pair through the corpus, and loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Empiezas con 256 tokens, uno por byte, y cuentas todos los pares adyacentes del corpus. Coges el más frecuente, le das el siguiente id y lo sustituyes en todo el corpus, de izquierda a derecha, y así hasta que el vocabulario tenga el tamaño que has pedido.&lt;/p&gt;
&lt;p&gt;El vocabulario es la lista ordenada de fusiones, y en ese orden está todo el modelo del idioma. Codificar consiste en reproducir ese orden: partes la entrada en bytes, buscas el par adyacente con el rango de fusión más bajo, lo fusionas y repites hasta que no quede nada que fusionar. &lt;code&gt;bpe_encode&lt;/code&gt; (&lt;code&gt;_educational.py:83-116&lt;/code&gt;) ocupa treinta y cuatro líneas y no recorre nunca la lista de fusiones. El entrenamiento se queda con el par más frecuente y la codificación con el de rango más bajo, que viene a ser lo mismo.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: El bucle de entrenamiento avanza un paso por clic: cuenta los pares adyacentes, fusiona el más frecuente y repite. El vocabulario es la lista de fusiones en orden. El símbolo ␣ es un espacio que va pegado a la palabra siguiente.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;La demo ejecuta ese bucle sobre un corpus de juguete, una fusión por clic. Como doce de sus dieciocho pasos son empates, la regla de desempate (gana el primer par que aparece) pesa mucho.&lt;/p&gt;
&lt;p&gt;Al ingeniero le importan dos detalles. El primero es que BPE trabaja con bytes y no con caracteres, de ahí que cualquier entrada se pueda codificar y que no exista el token desconocido. Un carácter que las fusiones no han visto nunca cuesta hasta un token por cada byte UTF-8, pero no da error. Con GPT-2, &lt;code&gt;ß&lt;/code&gt; es un token porque sus dos bytes se fusionaron durante el entrenamiento, &lt;code&gt;漢&lt;/code&gt; son tres, uno por byte, y una letra del alfabeto gótico son cuatro. Los vocabularios más recientes han recortado parte de ese sobrecoste (&lt;code&gt;o200k_base&lt;/code&gt; ya tiene &lt;code&gt;漢&lt;/code&gt; entero), aunque la letra del alfabeto gótico sigue costando cuatro tokens en todos ellos. El segundo detalle es que, antes de que entre en juego BPE, una expresión regular parte el texto en fragmentos, y las fusiones nunca pasan de un fragmento a otro. El patrón de GPT-2 deja el espacio inicial pegado a la palabra que lo sigue, de modo que &lt;code&gt;&amp;quot; token&amp;quot;&lt;/code&gt; y &lt;code&gt;&amp;quot;token&amp;quot;&lt;/code&gt; son tokens distintos. Por eso el modelo no trata igual el principio de una línea que el medio, y por eso un espacio de más en una plantilla de prompt cambia la salida.&lt;/p&gt;
&lt;h2 id=&quot;el-vocabulario-es-el-corpus-de-entrenamiento&quot;&gt;El vocabulario es el corpus de entrenamiento&lt;/h2&gt;
&lt;p&gt;El vocabulario de GPT-2 tiene 50 257 entradas. Si la cifra no es redonda es porque se trata de una suma: 256 tokens de byte, 50 000 fusiones aprendidas y un token especial, &lt;code&gt;&amp;lt;|endoftext|&amp;gt;&lt;/code&gt;, con el id 50256. &lt;a href=&quot;https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Radford et al. (2019)&lt;/a&gt; describen el diseño basado en bytes del que salen esas cifras. Las dos codificaciones más recientes de OpenAI que he comprobado, &lt;code&gt;cl100k_base&lt;/code&gt; y &lt;code&gt;o200k_base&lt;/code&gt;, tienen 100 261 y 200 000 tokens repartidos en rangos de 100 277 y 200 019 ids (el &lt;code&gt;n_vocab&lt;/code&gt; de tiktoken), y los huecos son ids sin usar.&lt;/p&gt;
&lt;p&gt;En las fusiones queda codificado el corpus de entrenamiento. Las palabras inglesas le salen baratas a GPT-2, que se entrenó sobre todo con texto web en inglés. &lt;code&gt;&amp;quot; tokenizer&amp;quot;&lt;/code&gt; son dos tokens, mientras que el mismo concepto en español, &lt;code&gt;&amp;quot; tokenizador&amp;quot;&lt;/code&gt;, son tres, y &lt;code&gt;&amp;quot; desvanecimiento&amp;quot;&lt;/code&gt;, seis. Una palabra alemana con diéresis, &lt;code&gt;&amp;quot; Wörter&amp;quot;&lt;/code&gt;, ocupa tres, y en uno de ellos los dos bytes de la &lt;code&gt;ö&lt;/code&gt; van soldados a la &lt;code&gt;r&lt;/code&gt; que la sigue. El tokenizador no tiene noción de carácter, solo de bytes y de frecuencias.&lt;/p&gt;
&lt;p&gt;Los números son el caso que pilla por sorpresa a los ingenieros. GPT-2 tokeniza &lt;code&gt;1234567&lt;/code&gt; como &lt;code&gt;123&lt;/code&gt;, &lt;code&gt;45&lt;/code&gt;, &lt;code&gt;67&lt;/code&gt; porque es lo que sale, sin más, de los rangos de fusión. &lt;code&gt;cl100k_base&lt;/code&gt; cambió la expresión regular para cortar las secuencias de dígitos en tramos de tres como máximo. Eso acota el daño sin arreglarlo. Los tramos se forman de izquierda a derecha, así que &lt;code&gt;1000&lt;/code&gt; acaba partido en &lt;code&gt;100&lt;/code&gt; y &lt;code&gt;0&lt;/code&gt;. Es una decisión del tokenizador con un efecto directo en lo que el modelo puede calcular, y se tomó en una expresión regular.&lt;/p&gt;
&lt;p&gt;Para ser justos con GPT-2, sus fusiones se aprendieron solo a partir de la frecuencia, y en el texto web &lt;code&gt;1000&lt;/code&gt; es tan común que &lt;code&gt;00&lt;/code&gt;, luego &lt;code&gt;000&lt;/code&gt; y luego &lt;code&gt;1000&lt;/code&gt; fueron ganando por turnos el recuento de pares. Una tabla de fusiones que respete la frecuencia se lo tragará entero. Nadie decidió romper la aritmética. El coste solo salió a la luz cuando la gente empezó a pedir a estos modelos que sumaran.&lt;/p&gt;
&lt;h2 id=&quot;el-mismo-párrafo-cuesta-más-en-español&quot;&gt;El mismo párrafo cuesta más en español&lt;/h2&gt;
&lt;p&gt;He escrito un párrafo de 184 bytes en inglés y en español, y he montado un payload JSON de más o menos el mismo tamaño, 182 bytes. Luego he codificado los tres con cada uno de los tres tokenizadores.&lt;/p&gt;
&lt;p&gt;Con GPT-2, el español cuesta en torno a 1,7× lo que el inglés, y con la codificación más reciente sigue en 1,2×. El JSON cuesta alrededor de 2,3× con las tres codificaciones, porque las llaves, las comillas y las claves no se comprimen. Ninguna de las 26 comillas de ese payload aparece sola como token. Cada una va soldada a la llave, el corchete, los dos puntos, la coma o el espacio que tiene al lado, en tokens como &lt;code&gt; &amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;:&lt;/code&gt;, &lt;code&gt;&amp;quot;,&lt;/code&gt;, &lt;code&gt;{&amp;quot;&lt;/code&gt; y &lt;code&gt;&amp;quot;},&lt;/code&gt;. Son veintiséis comillas repartidas en veintiséis tokens, una en cada uno.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Un párrafo escrito en dos idiomas y un payload JSON de igual tamaño, codificados con tres tokenizadores. Cambia la muestra y la codificación: los mismos bytes salen a otro precio. Cada bloque de color es un id que ve el modelo, y el espacio inicial forma parte del token.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-tokens-models-dont-read-text/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Si cambias la muestra y la codificación, verás que los vocabularios más recientes eliminan la mayor parte de la diferencia del español, mientras que la del JSON no se mueve.&lt;/p&gt;
&lt;p&gt;De ahí salen tres consecuencias, todas de facturación o de capacidad y ninguna de modelado. Una ventana de contexto expresada en tokens admite menos español que inglés, y mucho menos JSON. Un precio por token es un precio por idioma. Y una plantilla de prompt que envuelve cada petición en metadatos estructurados paga la tarifa del JSON en cada llamada.&lt;/p&gt;
&lt;h2 id=&quot;el-sistema-de-tipos-no-avisa&quot;&gt;El sistema de tipos no avisa&lt;/h2&gt;
&lt;p&gt;En el software, el parser es código y el modelo son datos. Puedes cambiar el parser, recompilar y leer los mismos datos. Aquí, en cambio, el parser está acoplado a los pesos. &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;La matriz de embeddings&lt;/a&gt; tiene una fila por id, y la fila 11 241 significa &lt;code&gt;&amp;quot; token&amp;quot;&lt;/code&gt; solo porque eso es lo que dijeron las fusiones de GPT-2 cuando se entrenó el modelo; con &lt;code&gt;cl100k_base&lt;/code&gt;, esa misma fila es &lt;code&gt;Seg&lt;/code&gt;. Si codificas el mismo texto con otro tokenizador, obtienes ids que indexan la misma matriz y significan otra cosa. Que el fallo se note o pase en silencio es cuestión de suerte. Con &lt;code&gt;o200k_base&lt;/code&gt;, el mismo párrafo en inglés llega al id 99 665, que una matriz de 50 257 filas rechaza. En sentido contrario, si metes ids de GPT-2 en un modelo de vocabulario más grande, todos caen dentro y no salta ninguna excepción. Y ni siquiera el rechazo es tan firme como parece. Los modelos en producción rellenan la matriz de embeddings hasta un múltiplo de 64 o de 128 para alinearla con los tensor cores, con lo que hay más filas en la matriz que ids en el vocabulario. Además, en una GPU un índice fuera de rango acaba apareciendo como un assert asíncrono en alguna línea que no tiene nada que ver con el sitio donde ocurrió.&lt;/p&gt;
&lt;p&gt;Por eso el tokenizador forma parte del artefacto. En GPT-2 esa misma matriz hace también de cabeza de salida, así que la consulta a la entrada y la distribución de muestreo a la salida dependen del mismo array de pesos y un cambio de tokenizador corrompe las dos. El fichero del modelo y el del tokenizador son dos mitades de una misma cosa, y cuando se desalinean, el fallo es silencioso. Un cambio completo cuyos ids siguen dentro de rango produce basura que ningún control detecta, y una deriva parcial (un token especial renumerado, un BOS que falta) produce texto fluido condicionado por una entrada equivocada.&lt;/p&gt;
&lt;h2 id=&quot;qué-vigila-el-ingeniero-de-ml&quot;&gt;Qué vigila el ingeniero de ML&lt;/h2&gt;
&lt;p&gt;Hay cuatro cosas que conviene comprobar antes de que nada de esto llegue a producción.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tokenizador y modelo, versionados juntos.&lt;/strong&gt; Guarda el hash del tokenizador junto a los pesos y compruébalo al cargar. No hay desajuste peor que el que no falla al cargar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Coste por idioma y por dominio.&lt;/strong&gt; Mídelo con tu tráfico real antes de estimar una factura o de prometer una longitud de contexto. La ventana de 8k son 8k tokens de inglés, pero con el payload JSON de arriba se acerca más a 3,5k del mismo contenido.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tokens especiales.&lt;/strong&gt; Por defecto, el &lt;code&gt;Encoding.encode&lt;/code&gt; de tiktoken lanza una excepción si el texto contiene algo parecido a &lt;code&gt;&amp;lt;|endoftext|&amp;gt;&lt;/code&gt;, precisamente porque un usuario podría escribirlo. Ese comportamiento por defecto es una decisión de seguridad. Comprueba qué hace con ello la capa que sirve tu modelo y en qué punto mete tu plantilla de chat sus propios tokens especiales.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Longitud en tokens, no en caracteres.&lt;/strong&gt; Todos los límites del modelo van en tokens. Para truncar, repartir el presupuesto de contexto o aplicar límites de consumo hay que pasar cada petición por el tokenizador, y contar caracteres no sirve.&lt;/p&gt;
&lt;p&gt;Nada de esto es trabajo de modelado. Es la parte del modelo que se comporta más como el software corriente, y precisamente por eso llega a producción sin que nadie la revise: una función que convierte cadenas en enteros parece preprocesamiento hasta el momento exacto en que deja de serlo.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 1 de 12. Esta parte empieza antes de que exista el modelo, con un tokenizador que va dentro del artefacto (no delante de él) sin que el sistema de tipos avise de ello. El id de un token es un índice, y &lt;a href=&quot;https://cchinchilla.dev/es/posts/fcw-embeddings-lookup-table/&quot;&gt;la siguiente parte&lt;/a&gt; lee lo que hay en ese índice: la matriz de embeddings, que es una tabla de consulta y poco más.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Fundamentos de ML</category><category>Fundamentos de ML</category><category>From code to weights</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>¿Quién calibra al juez? La brecha que nadie cierra</title><link>https://cchinchilla.dev/es/posts/llm-as-judge-the-calibration-gap/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/llm-as-judge-the-calibration-gap/</guid><description>En seis entregas he recorrido cinco estrategias y cinco herramientas. Dos de las herramientas miden la concordancia del juez con humanos, pero ninguna la exige en CI con una banda de confianza publicada. Ahí está la brecha.</description><pubDate>Tue, 25 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;En seis entregas he recorrido cinco estrategias y cinco de las siete herramientas cuyo código he leído para la investigación que hay detrás. Hay una pregunta a la que la serie ha apuntado una y otra vez sin llegar a responderla del todo: ¿hasta qué punto es realmente fiable el juez en la tarea que ejecuta y con la rúbrica que le han dado?&lt;/p&gt;
&lt;p&gt;Estoy construyendo AgentAnvil, un framework de pruebas guiado por contratos para agentes basados en LLM, y leer otros evaluadores me sirve para averiguar qué deberían restringir esos contratos. Las estrategias de esta serie limitan lo que el juez puede decir (partes 2 y 3), miden lo seguro que suena (parte 4), fijan lo que debe ignorar (parte 5) o prescinden de él donde bastan las comprobaciones deterministas (parte 6). Todas son honestas y funcionan, aunque cada una deja algo sin cubrir. Daba por hecho que ninguna de las cinco herramientas incluía la primitiva que mide con qué frecuencia el juez coincide con una persona en tu tarea. Al repasar el código por segunda vez he tenido que corregirme: dos de las cinco la traen. Lo que no ofrece ninguna es el flujo de trabajo que supedita el build de CI a esa concordancia, con una banda de confianza publicada que el build pueda leer.&lt;/p&gt;
&lt;p&gt;Con esta entrada cierro la serie y trazo el mapa de la brecha, que es más estrecha de lo que creía, pero real.&lt;/p&gt;
&lt;h2 id=&quot;la-intuición&quot;&gt;La intuición&lt;/h2&gt;
&lt;p&gt;Un juez es un instrumento de medida. Si mides dos veces el mismo líquido con un termómetro, obtendrás dos lecturas. La dispersión entre ellas es la varianza del instrumento. El termómetro sirve porque, en algún momento antes de llegar a tus manos, un fabricante caracterizó su error frente a un patrón de referencia (agua hirviendo, punto de congelación, celdas con trazabilidad al NIST). Si lleva implícito un margen de error, es porque alguien pagó ese paso de calibración.&lt;/p&gt;
&lt;p&gt;Un juez LLM no trae ese paso de serie. Escribes una rúbrica, eliges un modelo, lo lanzas sobre las trazas del agente y obtienes puntuaciones. El framework te devuelve la puntuación, pero rara vez le dice a la puerta de CI qué margen de error tiene en &lt;em&gt;tu&lt;/em&gt; tarea. Para saberlo, tendrías que pasar los mismos ítems por uno o varios anotadores humanos, calcular la concordancia con el juez y tratar el desacuerdo restante como el error del instrumento frente a la referencia (el sesgo más el ruido que arrastre una sola pasada). El ruido del juez entre ejecuciones, por sí solo, se mide aparte volviendo a juzgar los mismos ítems. La primitiva estándar para medir esa concordancia es la κ de Cohen (&lt;a href=&quot;https://doi.org/10.1177/001316446002000104&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Cohen, 1960&lt;/a&gt;, con los tramos de magnitud de &lt;a href=&quot;https://www.jstor.org/stable/2529310&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Landis y Koch, 1977&lt;/a&gt;). &lt;em&gt;Esa&lt;/em&gt; medida es el paso de calibración.&lt;/p&gt;
&lt;p&gt;Cuando empecé la serie, suponía que a las herramientas de código abierto les faltaba la primitiva en sí, pero no es del todo así. Al repasar con grep los cinco repositorios, esta vez buscando términos de calibración, he visto que dos de las herramientas incluyen la κ del juez frente a las etiquetas de referencia (Ragas con &lt;code&gt;validate_alignment()&lt;/code&gt; y TruLens con &lt;code&gt;GroundTruthAggregator(true_labels=...)&lt;/code&gt;), que ARES trae una versión más tosca y que Inspect AI ofrece un α de concordancia entre jueces. Lo que no incluye ninguna es la &lt;em&gt;puerta de calibración&lt;/em&gt;, el flujo de trabajo que rodea a esas primitivas: una banda de confianza publicada sobre la κ que el build de CI use como umbral para fallar, y un disparador que relance la calibración cuando cambien el modelo del juez o la rúbrica. Las primitivas existen, pero no la puerta que las envuelve. De esa brecha trata esta entrada.&lt;/p&gt;
&lt;h2 id=&quot;el-mapa-de-la-brecha&quot;&gt;El mapa de la brecha&lt;/h2&gt;
&lt;p&gt;Al final de la serie, la matriz de estrategias por herramienta queda así, ampliada con dos filas a las que no llegaron las entregas anteriores.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Cada celda llena marca una herramienta en la que el análisis de las partes 2 a 6 encontró la estrategia como decisión de diseño de primer orden, o en la que una segunda pasada con grep por el repositorio dio con una API de concordancia frente a la referencia. La tabla no es exhaustiva: Ragas y DeepEval también traen métricas de cadenas sin LLM, y las primitivas de concordancia parciales de ARES (exactitud bruta frente a la referencia) e Inspect AI (α entre jueces) quedan con la celda vacía. La última fila es la brecha y queda vacía, porque ninguna herramienta trae una puerta de CI ligada a una banda de confianza publicada.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-the-calibration-gap/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Las cinco primeras filas recogen lo que desgranó cada entrada. Ragas trae la restricción del alcance bien resuelta. DeepEval se encarga de la descomposición y, de pasada, roza la calibración con las probabilidades de los tokens. De ARES sale la cota de PPI, de TruLens la rúbrica negativa y de Inspect AI el reparto entre lo determinista y lo que puntúa un modelo. En total hay seis celdas llenas en cinco filas, una por estrategia salvo la calibración probabilística, que comparten DeepEval y ARES.&lt;/p&gt;
&lt;p&gt;La sorpresa vino con la sexta fila. En el &lt;code&gt;metrics/base.py&lt;/code&gt; de Ragas, &lt;code&gt;validate_alignment()&lt;/code&gt; recibe tu juez y un conjunto de test etiquetado por personas y devuelve la κ de Cohen. Para las métricas discretas usa &lt;code&gt;cohen_kappa_score&lt;/code&gt;, para las de ranking la κ ponderada y para las numéricas la r de Pearson. La guía práctica oficial (&lt;em&gt;«How to Align an LLM as a Judge»&lt;/em&gt;) desarrolla la misma idea con el porcentaje bruto de concordancia, pero donde está la κ es en &lt;code&gt;validate_alignment()&lt;/code&gt;. TruLens va más allá. En &lt;code&gt;feedback/groundtruth.py&lt;/code&gt;, &lt;code&gt;GroundTruthAggregator(true_labels=...)&lt;/code&gt; incluye &lt;code&gt;cohens_kappa&lt;/code&gt;, &lt;code&gt;ece&lt;/code&gt; (error de calibración esperado), &lt;code&gt;brier_score&lt;/code&gt;, &lt;code&gt;matthews_correlation&lt;/code&gt; y primitivas de correlación de rangos, todas accesibles directamente desde &lt;code&gt;trulens.feedback&lt;/code&gt; y con sus propios tests. ARES, por su parte, da la exactitud bruta del juez frente a la referencia junto a su cota de PPI. Las primitivas ni son exóticas ni faltan. Dos de las cinco herramientas incluyen la κ del juez frente a la referencia, ARES trae una versión más tosca e Inspect AI ofrece &lt;code&gt;krippendorff_alpha()&lt;/code&gt;, pensada para medir la concordancia entre jueces sin compararlos con la referencia.&lt;/p&gt;
&lt;p&gt;La séptima fila sigue vacía en todas las herramientas. Ninguna de las cinco envuelve esas primitivas en una puerta de CI ligada a una banda de confianza publicada, del estilo de &lt;em&gt;«haz que el build falle cuando la κ baje de 0,7 y la cota inferior de su IC del 95 % quede por debajo de 0,6»&lt;/em&gt;. Ragas devuelve una estimación puntual de la κ, y el bootstrap te toca a ti. TruLens devuelve lo mismo, aunque es la que más ha montado a su alrededor. Su paquete de benchmark tiene &lt;code&gt;AlignmentReport&lt;/code&gt;, &lt;code&gt;CriteriaABTest&lt;/code&gt; (para cambios de rúbrica) y &lt;code&gt;CrossModelAlignment&lt;/code&gt; (para cambios del modelo del juez), que comparan con un conjunto de referencia, y &lt;code&gt;Run.compare()&lt;/code&gt; pone un intervalo de confianza por bootstrap a las diferencias de puntuación entre ejecuciones. Pero todo eso se usa a mano. Ninguna de las cinco publica una banda de confianza sobre la κ ni la conecta a una puerta del build.&lt;/p&gt;
&lt;p&gt;ARES es la que más se acerca, y merece la pena deshacer el equívoco. Su implementación de PPI pone un intervalo de confianza alrededor de la puntuación agregada del agente, con lo que parece que debería llenar la fila de la puerta de calibración. Sin embargo, ese intervalo se calcula sobre el estadístico de la población, mientras que una puerta de calibración lo necesita sobre la concordancia con las etiquetas humanas del conjunto de test. PPI te dice que la puntuación agregada cae dentro de una banda (&lt;a href=&quot;https://arxiv.org/abs/2301.09633&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Angelopoulos et al., 2023&lt;/a&gt;), pero no le dice al build que falle cuando la concordancia entre el juez y las personas se desvía.&lt;/p&gt;
&lt;p&gt;Así que la brecha existe, aunque es más estrecha de lo que la planteé al empezar esta entrada.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-cuesta-la-brecha&quot;&gt;Lo que cuesta la brecha&lt;/h2&gt;
&lt;p&gt;Supongamos una puerta de CI. Una PR modifica el generador de planes de un agente, la CI lanza la batería de evaluación (veinte rúbricas de mil ítems cada una) y dos rúbricas bajan 3 puntos. ¿Es real esa bajada?&lt;/p&gt;
&lt;p&gt;En realidad son dos preguntas. Para saber si la bajada supera el ruido no hacen falta etiquetas; basta un bootstrap pareado o un test de permutaciones sobre las diferencias por ítem en esos 1000 ítems, con las dos ejecuciones juzgadas desde cero para que la varianza del juez entre ejecuciones quede dentro de las diferencias. En cambio, solo las etiquetas pueden decir si una bajada en la escala del juez es de las que notaría una persona. Un juez que se equivoca al azar acerca a cero las diferencias reales, y uno al que sesga el propio cambio (por ejemplo, porque altera el estilo de la salida) puede inventarlas u ocultarlas.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: El estadístico κ compara al juez con una etiqueta de referencia oculta en una tarea binaria equilibrada. Elige la exactitud real del juez y cuántos ítems etiquetados por personas tienes, y la banda que sale es lo que puedes publicar.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-the-calibration-gap/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;En esta demo fijas lo fiable que es tu juez frente a una referencia oculta y ves cómo queda la banda de confianza de la κ de Cohen con distintos tamaños de muestra. Sin ser una herramienta de calibración, enseña lo que cuesta no tener una. Aunque sepas que tu juez acierta el 80 % de las veces, la banda que puedes publicar es ancha mientras no hayas anotado cientos de ítems. Por debajo de unos 100 ítems etiquetados por personas, la banda es demasiado ancha para certificar la validez del juez.&lt;/p&gt;
&lt;p&gt;La mayoría de los equipos no llega a 100 ítems etiquetados por personas en cada rúbrica, y de hecho la mayoría no tiene ninguno. La serie ha ido documentando herramientas que funcionan bien en esa situación con estrategias que no necesitan ni una etiqueta. La brecha es lo que esas estrategias no pueden suplir.&lt;/p&gt;
&lt;h2 id=&quot;por-qué-nadie-ofrece-la-puerta&quot;&gt;Por qué nadie ofrece la puerta&lt;/h2&gt;
&lt;p&gt;Las primitivas son la parte fácil. Lo difícil es el flujo de la puerta de calibración que las rodea, y lo es por algo concreto que no encaja en un evaluador de código abierto de propósito general.&lt;/p&gt;
&lt;p&gt;Un framework puede incluir un juez, plantillas de rúbrica, grafos de descomposición, lógica de reintentos y parsers de salida estructurada, todo lo que has leído en las partes 2 a 6. También puede envolver &lt;code&gt;sklearn.metrics.cohen_kappa_score&lt;/code&gt; cuando ya hay etiquetas, que es justo lo que hace &lt;code&gt;GroundTruthAggregator.cohens_kappa&lt;/code&gt; de TruLens en &lt;code&gt;feedback/groundtruth.py&lt;/code&gt;. Lo que no puede traer un framework general es el &lt;em&gt;umbral&lt;/em&gt;. La puerta de CI que falla cuando κ &amp;lt; 0,7 necesita saber cuál es tu 0,7, en qué rúbrica y con cuánta tolerancia a las oscilaciones ruidosas que no son regresiones reales. Nada de eso vale igual para todas las tareas, y cualquier valor por defecto sería erróneo en algún caso importante.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Lo que cuesta calibrar un juez en una rúbrica. Elige el tamaño de la muestra, cuántos anotadores etiquetan cada ítem y cuánto se tarda por ítem. La factura es la que es.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-the-calibration-gap/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Esta otra demo enseña lo que cuesta en realidad producir las etiquetas. Eliges el número de ítems, el de anotadores y el tiempo de anotación por ítem, y la demo te devuelve la factura. Para una rúbrica es barata, pero se multiplica por cada rúbrica de la batería y vuelve con cada cambio de juez o de rúbrica. Por esa factura y porque, como decía arriba, cada equipo tiene que fijar su propio umbral, nadie ha empaquetado el hook de la puerta de calibración. La idea que guía el diseño de ARES (&lt;em&gt;«anota un conjunto pequeño y aprovéchalo bien»&lt;/em&gt;) es la respuesta de la literatura al coste de etiquetar, aunque ARES solo la aplica a la evaluación de RAG.&lt;/p&gt;
&lt;p&gt;Lo que queda por integrar está acotado y es concreto: un wrapper que ponga un intervalo de confianza por bootstrap a la κ que ya calculan Ragas y TruLens, una opción de configuración que diga &lt;em&gt;«estos son mis umbrales, úsalos como puerta»&lt;/em&gt; y un hook que lance la recalibración cuando cambien el modelo del juez o la versión de la rúbrica. Las piezas ya están hechas. La estimación puntual la da &lt;code&gt;sklearn.metrics.cohen_kappa_score&lt;/code&gt;, las bandas de confianza analíticas para la κ, el AC1 y el α de Krippendorff salen de la familia &lt;code&gt;irrCAC&lt;/code&gt; (el paquete de Gwet para la concordancia entre anotadores) y las extensiones con varios anotadores las cubre &lt;code&gt;statsmodels.stats.inter_rater&lt;/code&gt;. Ese wrapper es el trabajo que ninguna herramienta ha hecho de principio a fin.&lt;/p&gt;
&lt;h2 id=&quot;qué-puedes-hacer-hoy&quot;&gt;Qué puedes hacer hoy&lt;/h2&gt;
&lt;p&gt;Hay tres cosas que puedes hacer este trimestre sin cambiar de herramienta.&lt;/p&gt;
&lt;p&gt;La primera es reservar presupuesto para una muestra de calibración. Empieza con 50 ítems por rúbrica que etiquete una persona cuidadosa. El segundo juego de etiquetas lo pone tu juez, y con el par sacas una estimación puntual de la κ de Cohen. Cuando la rúbrica se estabilice, pasa a una muestra de entre 100 y 200 ítems, porque por debajo de unos 100 la banda de la κ es demasiado ancha para certificar la validez del juez (lo muestra la primera demo). Tanto si usas Ragas (&lt;code&gt;validate_alignment()&lt;/code&gt;) como TruLens (&lt;code&gt;GroundTruthAggregator&lt;/code&gt;) u otra herramienta, este es el único sitio donde no te libras de etiquetar. Repite la calibración una vez por trimestre, y también cada vez que cambien el modelo del juez o la rúbrica.&lt;/p&gt;
&lt;p&gt;La segunda, calcular tú mismo el intervalo de confianza por bootstrap y escribir el umbral en la puerta. Como ni Ragas ni TruLens devuelven una banda de confianza sobre la κ, tendrás que sacarla con un bucle de bootstrap de 1000 remuestreos o con las funciones de intervalo de confianza que trae &lt;code&gt;irrCAC&lt;/code&gt; (&lt;code&gt;kappa2.table&lt;/code&gt; en R, &lt;code&gt;irrCAC.table.CAC(table).cohen()&lt;/code&gt; en Python). Después, cuando la cota inferior caiga por debajo de tu umbral, haz que el build falle con un error de &lt;em&gt;calibración necesaria&lt;/em&gt;, sin marcarlo como regresión de calidad. Vistos desde fuera, los dos fallos se parecen, pero se arreglan de forma distinta (uno editando la rúbrica, el otro corrigiendo el agente), y distinguirlos es justo lo que no puede hacer por ti un juez sin ese wrapper.&lt;/p&gt;
&lt;p&gt;La tercera, ser honesto en el informe de evaluación sobre lo que el juez puede decirte y lo que no. No es lo mismo afirmar &lt;em&gt;«La fidelidad ha bajado 3 puntos»&lt;/em&gt; que afirmar &lt;em&gt;«En esta ejecución, el juez ha puntuado la fidelidad 3 puntos por debajo (IC del 95 % por bootstrap pareado, de 1,6 a 4,4). En un conjunto de calibración de 200 ítems, su concordancia con las personas es κ = 0,72 (IC del 95 %, de 0,62 a 0,82), así que la bajada es real en la escala del juez, y esa concordancia acota hasta qué punto refleja una regresión que notaría una persona»&lt;/em&gt;. Lo segundo es lo que tendría que publicar una puerta de calidad para controlar la calidad de verdad.&lt;/p&gt;
&lt;p&gt;Nada de esto exige que tu herramienta de código abierto haga más de lo que ya hace. Exige, eso sí, aceptar que el wrapper de la puerta de calibración te toca a ti y tratarlo como una tarea de ingeniería presupuestada y repetible, sin convertirlo en un proyecto de investigación.&lt;/p&gt;
&lt;h2 id=&quot;la-serie-de-principio-a-fin&quot;&gt;La serie, de principio a fin&lt;/h2&gt;
&lt;p&gt;Las estrategias de esta serie se complementan sin competir entre sí. Son cinco formas distintas de atacar el mismo problema, el de estrechar lo que un juez LLM puede estropear. Un harness de evaluación maduro las combina: determinista donde puede (parte 6), con el alcance restringido donde no puede (parte 2), con descomposición estructural donde el juicio es complejo (parte 3), anclado por rúbrica donde se solapa con métricas hermanas (parte 5) y con calibración probabilística donde la salida necesita un intervalo (parte 4). Es una arquitectura plural.&lt;/p&gt;
&lt;p&gt;Apiladas, las cinco quedan así. Si desactivas una estrategia, la barra deja al descubierto la parte que acotaba. El tramo de la derecha, el residuo que solo acotaría la puerta de calibración, no se mueve con ningún interruptor. (Los anchos de los tramos son ilustrativos.)&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: La barra horizontal representa la varianza total del juez. Activa o desactiva cada estrategia para ver su tramo acotado (tinta) o al descubierto (ámbar). El tramo de la derecha es la brecha de calibración, la varianza que dos herramientas (Ragas y TruLens) permiten medir con sus primitivas y que ninguna herramienta vigila con una puerta. Los anchos de los tramos son ilustrativos.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-the-calibration-gap/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Estoy trabajando en esta brecha con AgentAnvil. Que la acabe cerrando AgentAnvil u otra herramienta es lo de menos; la brecha existe, tiene nombre y las estrategias de esta serie no la cierran.&lt;/p&gt;
&lt;p&gt;Regla práctica: el juez es un instrumento de medida, y ningún build puede defender una medida que no lleve una banda de calibración publicada y vigilada por una puerta. Las cinco herramientas de esta serie estrechan la varianza, pero ninguna trae la puerta.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 7 de 7, la que cierra la serie. Dos de las cinco herramientas (Ragas y TruLens) incluyen la primitiva κ que expondría un juez calibrado, pero ninguna ofrece el flujo de la puerta de calibración, es decir, una banda de confianza publicada sobre la κ que haga fallar el build de CI cuando caiga. Las cinco estrategias estrechan lo que el juez puede estropear. La puerta en torno a la banda de calibración es el trabajo que ninguna herramienta ha hecho de principio a fin. Si has llegado aquí directamente, este es el recorrido completo: la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/&quot;&gt;parte 1&lt;/a&gt; pone nombre a la varianza, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/&quot;&gt;parte 2&lt;/a&gt; la acota con veredictos binarios, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-structural-decomposition/&quot;&gt;parte 3&lt;/a&gt; divide un juicio difícil en un grafo de juicios más pequeños, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-probabilistic-calibration/&quot;&gt;parte 4&lt;/a&gt; mide la certeza de la respuesta, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-rubric-anchoring/&quot;&gt;parte 5&lt;/a&gt; le dice al juez qué ignorar y la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-deterministic-hybridisation/&quot;&gt;parte 6&lt;/a&gt; prescinde del juez donde basta con una regex.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Forma parte de la misma serie y está relacionado con un artículo que presenté en DITTET 2026 (julio) sobre la varianza de los jueces en evaluadores de código abierto.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Metodología de evaluación</category><category>LLM como juez</category><category>Metodología de evaluación</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>Determinista donde puedas, juez donde haga falta</title><link>https://cchinchilla.dev/es/posts/llm-as-judge-deterministic-hybridisation/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/llm-as-judge-deterministic-hybridisation/</guid><description>Las llamadas a herramientas piden comparación determinista; saber si el agente cumple su objetivo exige un LLM. Unas herramientas trazan esa línea y otras no.</description><pubDate>Tue, 18 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;En la parte 5 vimos el anclaje por rúbrica, con una herramienta que le dice al juez lo que NO debe evaluar. La siguiente estrategia no afina más la pregunta del juez y plantea otra previa: si tiene sentido usar un juez y dónde.&lt;/p&gt;
&lt;p&gt;Estoy construyendo AgentAnvil, un framework de pruebas guiado por contratos para agentes basados en LLM, y leer otros evaluadores me sirve para averiguar qué deberían restringir esos contratos. La hibridación determinista es la más sencilla de las estrategias de esta serie y también la más fácil de aplicar medio bien. Algunas preguntas sobre un agente tienen una única respuesta correcta (si ha llamado a la herramienta adecuada, si el JSON se puede parsear, si ha elegido la opción B), y basta una comparación de cadenas, una regex o un parser para resolverlas. Otras no la tienen (si la respuesta ha sido útil de verdad), y ahí una regex no sirve. La estrategia consiste en usar cada método donde encaje y en dejar el reparto a la vista. Hay una herramienta, Inspect AI, que acierta con la arquitectura.&lt;/p&gt;
&lt;p&gt;Aquí la leo en su código fuente, en el commit &lt;a href=&quot;https://github.com/UKGovernmentBEIS/inspect_ai/tree/ff29dc921262dbe6869895f6d77dc7f4396f7f59&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;ff29dc9&lt;/code&gt;&lt;/a&gt; (abril de 2026), al que apuntan las referencias a ficheros y líneas y los fragmentos de código.&lt;/p&gt;
&lt;h2 id=&quot;la-intuición&quot;&gt;La intuición&lt;/h2&gt;
&lt;p&gt;Cuando tienes un LLM en el stack, lo cómodo es usarlo para todo y pedirle que puntúe esta salida, que compare estas dos, que valore esto de 0 a 3. Cada evaluación se convierte así en un prompt, y cada prompt cuesta entre una fracción de céntimo y unos pocos céntimos, además de unos segundos. Con mil muestras y cuatro evaluadores, la evaluación acaba costando más que la propia ejecución.&lt;/p&gt;
&lt;p&gt;En realidad, muchas de las preguntas que haces en una evaluación no necesitan un modelo de lenguaje. Para &lt;em&gt;«¿Ha llamado el agente a &lt;code&gt;get_weather&lt;/code&gt; con la ciudad correcta?»&lt;/em&gt; basta una comparación de cadenas; para &lt;em&gt;«¿Incluye la respuesta el nombre del usuario?»&lt;/em&gt;, una búsqueda de subcadena, y para &lt;em&gt;«¿Es la salida un objeto JSON válido que cumple este esquema?»&lt;/em&gt;, un parser. Son comprobaciones más baratas, más rápidas y deterministas (la misma entrada da siempre la misma respuesta), y puedes ejecutarlas en local, en bucle y gratis.&lt;/p&gt;
&lt;p&gt;El reflejo de meter un LLM en todo viene de que las herramientas actuales te empujan a ello. La mayoría de los frameworks de evaluación con LLM ponen en primer plano sus evaluadores con juez porque es lo que los distingue, la capacidad nueva que los convierte en frameworks de evaluación &lt;em&gt;con LLM&lt;/em&gt; y los separa de un ejecutor de tests corriente. Los evaluadores por coincidencia, regex o patrón están ahí, pero parecen un paso atrás. El usuario se pregunta por qué iba a usar otra cosa teniendo un juez potente.&lt;/p&gt;
&lt;p&gt;Porque el juez es la herramienta adecuada para unas preguntas y la equivocada para otras. Hay que usar cada método donde encaje. Lo más difícil es hacer explícito el reparto para que un revisor pueda auditarlo.&lt;/p&gt;
&lt;h2 id=&quot;inspect-ai-paso-a-paso&quot;&gt;Inspect AI, paso a paso&lt;/h2&gt;
&lt;p&gt;Inspect AI define &lt;code&gt;Scorer&lt;/code&gt; como un &lt;code&gt;Protocol&lt;/code&gt; estructural, sin clase base de la que heredar (&lt;code&gt;scorer/_scorer.py:33-61&lt;/code&gt;).&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:25ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#6F42C1;--1:#DCBDFB&quot;&gt;@runtime_checkable&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#F69D50&quot;&gt;Scorer&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#6CB6FF&quot;&gt;Protocol&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;__call__&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;self,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;state: TaskState,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;target: Target,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;) -&amp;gt; Score &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Ese es todo el contrato: un evaluador es un callable asíncrono que recibe el estado de la tarea (entrada, salida del modelo, llamadas a herramientas, metadatos) y el target (la respuesta que espera el dataset) y devuelve un &lt;code&gt;Score&lt;/code&gt; o &lt;code&gt;None&lt;/code&gt;. Tanto los evaluadores deterministas como los que usan juez &lt;strong&gt;cumplen este protocolo&lt;/strong&gt;, y el framework no los distingue en el sistema de tipos, donde todos son Scorers. El reparto lo decide el usuario, sin que el framework imponga ninguno.&lt;/p&gt;
&lt;p&gt;La familia determinista está repartida entre &lt;code&gt;_match.py&lt;/code&gt;, &lt;code&gt;_pattern.py&lt;/code&gt;, &lt;code&gt;_answer.py&lt;/code&gt;, &lt;code&gt;_classification.py&lt;/code&gt; y &lt;code&gt;_choice.py&lt;/code&gt; (más &lt;code&gt;_math.py&lt;/code&gt;). Los evaluadores que trae de serie son &lt;code&gt;match()&lt;/code&gt; (comparación de cadenas, con &lt;code&gt;location&lt;/code&gt; ∈ begin/end/any/exact), &lt;code&gt;includes()&lt;/code&gt; (prueba de subcadena), &lt;code&gt;pattern()&lt;/code&gt; (regex sobre la salida), &lt;code&gt;exact()&lt;/code&gt; (en ese commit, igualdad del conjunto de tokens normalizado; las versiones actuales comparan la secuencia de tokens normalizada, así que importan el orden y las repeticiones), &lt;code&gt;f1()&lt;/code&gt; (F1 al estilo SQuAD, que devuelve un valor continuo de 0 a 1), &lt;code&gt;answer()&lt;/code&gt; (un wrapper de regex para LETTER/WORD/LINE) y &lt;code&gt;choice()&lt;/code&gt; (opción múltiple mediante &lt;code&gt;state.choices[i].correct&lt;/code&gt;). Ninguno importa el tipo &lt;code&gt;Model&lt;/code&gt;; son funciones puras de la salida y el target, sin llamada al LLM, sin coste y sin varianza. Tampoco leen las llamadas a herramientas, y para comprobar una basta un evaluador propio de pocas líneas.&lt;/p&gt;
&lt;p&gt;La familia con juez está en &lt;code&gt;_model.py&lt;/code&gt; y tiene dos puntos de entrada, &lt;code&gt;model_graded_qa()&lt;/code&gt; para calificar respuestas abiertas y &lt;code&gt;model_graded_fact()&lt;/code&gt; para comparar la salida con una referencia. &lt;code&gt;model_graded_fact&lt;/code&gt; es, literalmente, un wrapper de una línea que llama a &lt;code&gt;model_graded_qa&lt;/code&gt; con otra plantilla (&lt;code&gt;_model.py:75-83&lt;/code&gt;). En los dos casos, la implementación real está en &lt;code&gt;_model_graded_qa_single()&lt;/code&gt; (&lt;code&gt;_model.py:154-237&lt;/code&gt;). La llamada que importa es esta:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:76ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; model.generate([scoring_prompt])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;match &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; re.search(grade_pattern &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;DEFAULT_GRADE_PATTERN&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, result.completion)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;En ese commit, el patrón por defecto para la nota es &lt;code&gt;r&amp;quot;(?i)GRADE\s*:\s*([CPI])(.*)$&amp;quot;&lt;/code&gt; (&lt;code&gt;_model.py:293&lt;/code&gt;), que falla en cuanto hay algo detrás de la línea de la nota. Si falta la nota, la puntuación es &lt;code&gt;INCORRECT&lt;/code&gt;. Las versiones actuales, en cambio, se quedan con el último &lt;code&gt;GRADE: X&lt;/code&gt; de la salida (&lt;code&gt;(?is).*(?&amp;lt;!\w)GRADE(?!\w)\s*:\s*([CPI])&lt;/code&gt;, simplificado) y, por defecto, dejan sin puntuar un veredicto que no se puede parsear o que no está entre las opciones. El juez tiene que escribir &lt;code&gt;GRADE: C&lt;/code&gt; o &lt;code&gt;GRADE: I&lt;/code&gt; (o &lt;code&gt;GRADE: P&lt;/code&gt; si está activada la puntuación parcial). Como la firma trae &lt;code&gt;partial_credit: bool = False&lt;/code&gt;, la calificación es binaria salvo que pidas la parcial.&lt;/p&gt;
&lt;p&gt;Conviene reconocer de dónde sale todo esto. Un comentario en &lt;code&gt;_model.py:240-241&lt;/code&gt; dice &lt;em&gt;«these templates are based on the openai closedqa templates»&lt;/em&gt; y enlaza a &lt;code&gt;github.com/openai/evals/blob/main/evals/registry/modelgraded/closedqa.yaml&lt;/code&gt;. Como las plantillas de OpenAI evals son de 2023, la forma de calificar de Inspect AI es anterior a la mayoría de los frameworks de evaluación con LLM que he leído. Funciona porque quienes la escribieron no tenían prisa por ser originales.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Seis tareas de evaluación: elige para cada una la familia de evaluador. La fila de abajo da el coste del reparto actual con 100 muestras (a unos 0,005 $ por llamada al LLM).&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-deterministic-hybridisation/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Hay dos maneras de componer evaluadores. La primera es &lt;code&gt;multi_scorer(scorers, reducer)&lt;/code&gt; (&lt;code&gt;_multi.py:13-30&lt;/code&gt;), que ejecuta N evaluadores por muestra y los reduce a un único Score con un reductor (máximo, media, moda o uno propio). La segunda es pasar una lista directamente al constructor de Task (&lt;code&gt;Task(scorer=[exact(), model_graded_qa()])&lt;/code&gt;), y entonces cada evaluador aporta al informe su propia columna de métrica, sin necesidad de reductor.&lt;/p&gt;
&lt;p&gt;Lo que Inspect AI &lt;strong&gt;no&lt;/strong&gt; trae es la elección de evaluador muestra a muestra. &lt;em&gt;«En esta fila usa exact, en esa usa el juez»&lt;/em&gt; no se puede declarar directamente, así que el usuario escribe una función propia decorada con &lt;code&gt;@scorer&lt;/code&gt; que decide según &lt;code&gt;state.metadata&lt;/code&gt; o &lt;code&gt;target&lt;/code&gt;, o divide el dataset en dos tareas. El framework te da las primitivas y deja en tus manos la decisión del reparto.&lt;/p&gt;
&lt;p&gt;El detalle más revelador está en &lt;code&gt;_model.py:341-354,372-414&lt;/code&gt;. Los marcadores &lt;code&gt;[BEGIN DATA] ... [END DATA]&lt;/code&gt;, que delimitan la salida del agente dentro del prompt de calificación, se sanean en todos los campos no fiables (el &lt;code&gt;answer&lt;/code&gt; del modelo y, del dataset, la &lt;code&gt;question&lt;/code&gt;, el &lt;code&gt;criterion&lt;/code&gt; y los metadatos). Si esos marcadores aparecen en un texto que controla el usuario, sus espacios se sustituyen por guiones. Un comentario lo deja claro: &lt;em&gt;«Literal space (not &lt;code&gt;\s&lt;/code&gt;) is intentional — &lt;code&gt;\s&lt;/code&gt; also matches U+00A0 (NBSP), which would let a model pre-neutralize its own output and bypass the mitigation»&lt;/em&gt;. Los mantenedores han tenido en cuenta la inyección de prompts dirigida al propio juez. En las demás herramientas de evaluación de código abierto es raro encontrar una mitigación comparable.&lt;/p&gt;
&lt;p&gt;Y hay que reconocerle el mérito. Entre las herramientas de evaluación de código abierto de esta serie, el protocolo Scorer de Inspect AI es el que mejor separa los evaluadores deterministas de los que usan juez. Hay una sola interfaz, las dos familias la cumplen y el usuario elige en cada tarea. Los mantenedores lo dicen sin rodeos (&lt;code&gt;docs/scorers.qmd:55&lt;/code&gt;) y describen &lt;code&gt;model_graded_fact()&lt;/code&gt; como el evaluador &lt;em&gt;«que se usa cuando la salida del modelo es demasiado compleja para valorarla con un simple evaluador &lt;code&gt;match()&lt;/code&gt; o &lt;code&gt;pattern()&lt;/code&gt;»&lt;/em&gt;. Calificar con un juez es el plan B.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-pierdes&quot;&gt;Lo que pierdes&lt;/h2&gt;
&lt;p&gt;Te toca pensar. La mayoría de las veces está claro si conviene &lt;code&gt;match()&lt;/code&gt; o &lt;code&gt;model_graded_qa()&lt;/code&gt;. Las respuestas exactas piden una coincidencia exacta y las abiertas, un evaluador con juez. Los casos difíciles están en medio, como una respuesta estructurada que el modelo puede formular de otra manera, un nombre de función que puede venir con otras mayúsculas o una respuesta numérica en la que el modelo puede poner la unidad o no. Ahí eliges tú, sin ayuda del framework. Eso es una ventaja cuando quien elige sabe lo que hace, y un coste cuando no lo sabe.&lt;/p&gt;
&lt;p&gt;El reparto se decide tarea a tarea. En Inspect AI, una Task asocia un evaluador (o una lista fija de evaluadores) a todo el dataset y no se puede cambiar de una muestra a otra. Si 800 de tus 1000 muestras pueden usar coincidencia exacta y 200 necesitan un juez, no hay forma de declararlo sin más. Tienes que escribir un evaluador propio que elija según los metadatos o partir el dataset en dos tareas. Las dos soluciones funcionan, pero ninguna cabe en una línea. Lo que falta es una primitiva para mezclar tipos de evaluador muestra a muestra, justo donde un framework más ambicioso automatizaría la decisión.&lt;/p&gt;
&lt;p&gt;Además, el prompt del juez es un valor por defecto que se puede cambiar. &lt;code&gt;model_graded_qa(template=..., instructions=..., grade_pattern=...)&lt;/code&gt; lo adapta a cada tarea, las claves de los metadatos de la muestra hacen de variables de la plantilla y el criterio sale del target de cada muestra. Para la mayoría de las preguntas abiertas basta con la plantilla por defecto, porque una calificación genérica sí generaliza. Cuando necesitas otro &lt;em&gt;protocolo&lt;/em&gt; de calificación (llamadas encadenadas al juez, salida estructurada, un veredicto que una sola regex no puede extraer), vuelves a tener que escribir un &lt;code&gt;@scorer&lt;/code&gt; propio y llamar tú mismo a &lt;code&gt;model.generate()&lt;/code&gt;. Es la misma vía de escape que te da cualquier otro framework y cumple su función, pero ya no cabe en una línea.&lt;/p&gt;
&lt;h2 id=&quot;cuándo-encaja-este-enfoque&quot;&gt;Cuándo encaja este enfoque&lt;/h2&gt;
&lt;p&gt;Para saber si te conviene esta estrategia basta una pregunta.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Alguna de tus preguntas de evaluación se puede responder con una regex?&lt;/strong&gt; La mayoría de las evaluaciones tienen al menos una: las llamadas a herramientas tienen una respuesta correcta, las salidas estructuradas siguen un esquema, las respuestas numéricas tienen un valor esperado y la respuesta de una tarea de opción múltiple es una letra. Si pagas una llamada al LLM para comprobar si el JSON se puede parsear, gastas dinero en lo que un parser hace gratis y le añades la varianza del juez a una pregunta que no tiene ninguna.&lt;/p&gt;
&lt;p&gt;Puntuar con un juez compensa cuando la respuesta es subjetiva de verdad, por ejemplo al decidir si la contestación ha sido útil, si el resumen es fiel o si el tono es el adecuado. Son preguntas cuya respuesta no sale de ninguna comparación de cadenas, y para ellas el juez es la herramienta adecuada. Eso sí, recurrir a él tiene que ser siempre una decisión explícita.&lt;/p&gt;
&lt;p&gt;Lo que la hibridación determinista no resuelve es el problema de calibración de los propios evaluadores con juez. Al repartir el trabajo, la varianza queda localizada: todo lo determinista sale impecable y todo lo que pasa por el juez arrastra el ruido que la serie viene documentando. El evaluador con juez es un LLM, y es justo en el LLM donde está la cuestión de la calibración. Algunas herramientas te dan, pese a ese ruido, un intervalo estadísticamente válido sobre la puntuación agregada (el ARES de la parte 4), pero la mayoría no. La última parte de la serie se ocupa de la brecha que nadie cierra.&lt;/p&gt;
&lt;p&gt;Como regla práctica, cada regex que puedas escribir es una llamada al LLM menos que depurar. Usa el juez donde haga falta y en ningún otro sitio.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 6 de 7. La hibridación determinista reparte la evaluación entre las preguntas que puede responder una regex y las que tiene que responder un juez. Entre las herramientas de evaluación de código abierto de esta serie, el protocolo &lt;code&gt;Scorer&lt;/code&gt; de Inspect AI es el que hace el reparto más limpio, con una sola interfaz que cumplen tanto los evaluadores deterministas como los que usan juez, y deja que el usuario elija en cada tarea. La última parte se ocupa de la brecha de calibración que nadie cierra. Si has llegado aquí directamente, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/&quot;&gt;parte 1&lt;/a&gt; pone nombre a la varianza, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/&quot;&gt;parte 2&lt;/a&gt; la acota con veredictos binarios, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-structural-decomposition/&quot;&gt;parte 3&lt;/a&gt; divide un juicio difícil en un grafo de juicios más pequeños, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-probabilistic-calibration/&quot;&gt;parte 4&lt;/a&gt; mide la certeza de la respuesta y la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-rubric-anchoring/&quot;&gt;parte 5&lt;/a&gt; le dice al juez qué ignorar.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Forma parte de la misma serie y está relacionado con un artículo que presenté en DITTET 2026 (julio) sobre la varianza de los jueces en evaluadores de código abierto.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Metodología de evaluación</category><category>LLM como juez</category><category>Metodología de evaluación</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>La rúbrica negativa: decirle al juez qué NO debe evaluar</title><link>https://cchinchilla.dev/es/posts/llm-as-judge-rubric-anchoring/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/llm-as-judge-rubric-anchoring/</guid><description>TruLens usa rúbricas que le dicen al juez qué queda fuera de su alcance, con una claridad poco habitual.</description><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 4 repasaba dos herramientas que intentaban medir hasta qué punto estaba seguro el juez, una dentro de una sola llamada y otra a lo largo de muchas. La siguiente estrategia deja de lado la certeza. Se plantea, de entrada, a qué pregunta debería responder el juez y le dice expresamente qué tiene que dejar fuera.&lt;/p&gt;
&lt;p&gt;Estoy construyendo AgentAnvil, un framework de pruebas guiado por contratos para agentes basados en LLM, y leer otros evaluadores me sirve para averiguar qué deberían restringir esos contratos. El anclaje por rúbrica es la estrategia que más fácil me ha resultado reconocer y la que más me ha costado precisar. La mayoría de las herramientas que usan un LLM como juez enuncian el criterio que hay que puntuar y ahí se quedan. Hay una, TruLens, que añade una segunda cláusula con &lt;em&gt;lo que queda fuera del alcance del juicio&lt;/em&gt;. Lo normal es que los prompts no la lleven, pero cuando aparece no está de adorno.&lt;/p&gt;
&lt;p&gt;Aquí la leo en el código fuente de TruLens 2.8.0, que es la versión a la que apuntan las referencias a ficheros y líneas.&lt;/p&gt;
&lt;h2 id=&quot;la-intuición&quot;&gt;La intuición&lt;/h2&gt;
&lt;p&gt;Una rúbrica es un contrato con el juez («puntúa esto en esta escala») y lo que el juez lee es su redacción. Lo que el contrato no dice, el juez lo rellena con lo que ha visto: su distribución de entrenamiento, su idea preconcebida de cómo es una buena respuesta, el solapamiento implícito entre esta métrica y todas las rúbricas que haya puntuado antes. Esos supuestos actúan en silencio y se notan en la puntuación, pero en el prompt no aparecen.&lt;/p&gt;
&lt;p&gt;La rúbrica negativa le da la vuelta a lo habitual. Escribes el criterio &lt;em&gt;y&lt;/em&gt; enumeras lo que el juez debe pasar por alto, sin confiar en que sepa interpretarlo bien por su cuenta. Si lo que puntúas es la calidad del plan, no menciones si el plan salió bien. Si puntúas las llamadas a herramientas, la elección de la herramienta es asunto de una métrica hermana, así que no la toques. La eficiencia de la ejecución se fija en el orden de los pasos y no le incumbe que el agente llegara o no al objetivo. Así se acota a propósito lo que el juez puede sopesar.&lt;/p&gt;
&lt;p&gt;Suena defensivo, y lo es. La cláusula negativa está ahí porque quien escribió la rúbrica ha visto al juez contar dos veces lo mismo, puntuar lo que no tocaba o contaminar una dimensión con otra. Es la vía por la que esa experiencia previa (lo que el modelo hace mal una y otra vez) entra en el prompt y se queda fijada en él.&lt;/p&gt;
&lt;p&gt;Las herramientas de la parte 4 medían a posteriori la incertidumbre del juez, mientras que esta estrategia toca una tecla muy distinta. El juez sigue igual de seguro, pero apunta a un blanco más estrecho.&lt;/p&gt;
&lt;h2 id=&quot;trulens-paso-a-paso&quot;&gt;TruLens, paso a paso&lt;/h2&gt;
&lt;p&gt;Casi todas las funciones de feedback de TruLens que dependen de un juez LLM, incluidas las siete métricas de agente, acaban en uno de los dos métodos de &lt;code&gt;LLMProvider&lt;/code&gt;, &lt;code&gt;generate_score&lt;/code&gt; o &lt;code&gt;generate_score_and_reasons&lt;/code&gt; (&lt;code&gt;feedback/llm_provider.py:180,283&lt;/code&gt;). Los dos montan un par de mensajes &lt;code&gt;[system, user]&lt;/code&gt;, piden una respuesta estructurada y delegan en &lt;code&gt;_create_chat_completion&lt;/code&gt;. El proveedor de OpenAI hace pasar la llamada por una cadena que va tanteando qué admite el modelo. Prueba primero con las salidas estructuradas de la Responses API y, si no funcionan, recurre a Chat Completions.&lt;/p&gt;
&lt;p&gt;En los kwargs de la llamada hay un detalle que pasa desapercibido. Con los modelos sin razonamiento la temperatura es 0,0 por defecto. Además, el módulo del proveedor de OpenAI fija en el código, al principio, un &lt;code&gt;SEED: int = 123&lt;/code&gt; y lo añade a la llamada salvo que quien la hace pase su propia semilla (&lt;code&gt;providers/openai/provider.py:23,439-443&lt;/code&gt;). Pero la Responses API no acepta &lt;code&gt;seed&lt;/code&gt;, así que TruLens la quita y reintenta (&lt;code&gt;provider.py:270-285&lt;/code&gt;). La semilla solo llega al modelo cuando se recurre a Chat Completions, donde OpenAI únicamente promete respetarla en la medida de lo posible. Por tanto, la repetibilidad descansa sobre todo en la temperatura 0 y no en la semilla. Para un harness de evaluación es un valor por defecto defendible, aunque no da el determinismo que sugiere esa constante fija.&lt;/p&gt;
&lt;p&gt;El prompt de sistema sale de plantillas, con un fichero por dominio: &lt;code&gt;templates/rag.py&lt;/code&gt; para las métricas de RAG, &lt;code&gt;templates/quality.py&lt;/code&gt; para la calidad general, &lt;code&gt;templates/safety.py&lt;/code&gt; para la seguridad y &lt;code&gt;templates/agent.py&lt;/code&gt; para las métricas propias de agentes.&lt;/p&gt;
&lt;p&gt;Si buscas con grep &lt;em&gt;«do not / don’t / only / regardless / out-of-scope»&lt;/em&gt; en todos los ficheros de plantillas, salen treinta y seis coincidencias. Quince están en &lt;code&gt;templates/agent.py&lt;/code&gt; y son las que tienen estructura, con bloques &lt;code&gt;In-scope:&lt;/code&gt;/&lt;code&gt;Out-of-scope:&lt;/code&gt;, remisiones a métricas hermanas y &lt;em&gt;«ignore execution»&lt;/em&gt;. Las demás, repartidas por el resto de ficheros, son sobre todo fórmulas hechas (&lt;em&gt;«respond only as a number»&lt;/em&gt;). Llevan el patrón las métricas de plan, de herramientas y de ejecución, pero casi ninguna de las de groundedness, harmfulness, correctness o sentiment.&lt;/p&gt;
&lt;p&gt;El caso más claro es el prompt de sistema de &lt;code&gt;PlanQuality&lt;/code&gt; (&lt;code&gt;templates/agent.py:212-248&lt;/code&gt;). Su primer párrafo, antes de cualquier criterio de puntuación, ya incluye una frase &lt;code&gt;CRITICAL:&lt;/code&gt; según la cual el juez fracasa en su tarea en cuanto alude a si el agente siguió el plan o a cualquier parte de la ejecución. El prompt insiste en la exclusión al menos tres veces más (&lt;em&gt;«Do NOT infer or fill gaps using execution steps.»&lt;/em&gt;, &lt;em&gt;«You are judging the strategy, not the outcome.»&lt;/em&gt;, &lt;em&gt;«Do NOT score on the execution quality.»&lt;/em&gt;). Es, además, el más largo de los siete prompts de sistema de agente, casi el doble que el siguiente.&lt;/p&gt;
&lt;p&gt;El ejemplo más estructurado está en el prompt de sistema de &lt;code&gt;ToolCalling&lt;/code&gt; (&lt;code&gt;templates/agent.py:361-363&lt;/code&gt;), donde TruLens abre una sección con nombre propio:&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;plaintext&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:75ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292e;--1:#adbac7&quot;&gt;Important scope boundaries:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292e;--1:#adbac7&quot;&gt;- In-scope: argument/schema correctness, semantic fit of query,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:2ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292e;--1:#adbac7&quot;&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292e;--1:#adbac7&quot;&gt;preconditions/postconditions, grounded interpretation of outputs,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:2ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292e;--1:#adbac7&quot;&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292e;--1:#adbac7&quot;&gt;explicit handling of tool-returned errors.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292e;--1:#adbac7&quot;&gt;- Out-of-scope: tool selection (Tool Selection), workflow efficiency&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:2ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292e;--1:#adbac7&quot;&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292e;--1:#adbac7&quot;&gt;(Execution Efficiency), external service/tool reliability (Tool Quality).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Fíjate en las referencias entre paréntesis. Las cláusulas de exclusión &lt;em&gt;nombran las métricas hermanas&lt;/em&gt; a las que el juez debe remitirse. &lt;code&gt;ToolSelection&lt;/code&gt; (&lt;code&gt;agent.py:305-308&lt;/code&gt;) y &lt;code&gt;ToolQuality&lt;/code&gt; (&lt;code&gt;agent.py:416-418&lt;/code&gt;) llevan también un bloque &lt;code&gt;Important scope boundaries:&lt;/code&gt;, así que las tres métricas de herramientas comparten ese encabezado, aunque cada una redacta el bloque a su manera. &lt;code&gt;ToolSelection&lt;/code&gt;, por ejemplo, usa viñetas &lt;em&gt;«Do NOT penalize …»&lt;/em&gt;. Solo ella y &lt;code&gt;ToolCalling&lt;/code&gt; nombran las métricas hermanas en las que delegan. El terreno que juzga cada una de las siete métricas de agente se ha repartido a mano (&lt;code&gt;ToolCalling&lt;/code&gt;, &lt;code&gt;ToolSelection&lt;/code&gt;, &lt;code&gt;ToolQuality&lt;/code&gt;, &lt;code&gt;ExecutionEfficiency&lt;/code&gt;, &lt;code&gt;PlanQuality&lt;/code&gt;, &lt;code&gt;PlanAdherence&lt;/code&gt;, &lt;code&gt;LogicalConsistency&lt;/code&gt;): la mayoría de las rúbricas le dicen al juez qué tiene que dejarles a las otras.&lt;/p&gt;
&lt;p&gt;Encima de todo eso se injerta la capa de CoT, igual en las siete métricas de agente. &lt;code&gt;COT_REASONS_TEMPLATE&lt;/code&gt; está en &lt;code&gt;templates/base.py:198-206&lt;/code&gt; y es un esqueleto fijo, &lt;em&gt;«Criteria: …, Supporting Evidence: …, Score: …»&lt;/em&gt;. Cada método &lt;code&gt;*_with_cot_reasons&lt;/code&gt; de las métricas de agente sustituye la etiqueta final del prompt de usuario por ese bloque, con &lt;code&gt;user_prompt.replace(&amp;quot;&amp;lt;METRIC&amp;gt; SCORE:&amp;quot;, COT_REASONS_TEMPLATE)&lt;/code&gt;. La clave exacta varía según la métrica (&lt;code&gt;TOOL CALLING SCORE:&lt;/code&gt;, &lt;code&gt;LOGICAL CONSISTENCY SCORE:&lt;/code&gt;, etc.). El prompt de sistema, que es donde están las cláusulas negativas, no se toca. (Las métricas de criterios al estilo de LangChain, en cambio, añaden la misma plantilla al prompt de sistema.)&lt;/p&gt;
&lt;p&gt;Después de tanto prompt, la puntuación en sí es poca cosa. El espacio de salida por defecto es una escala Likert de 0 a 3 (&lt;code&gt;templates/base.py:179&lt;/code&gt;) y el modelo devuelve un JSON estructurado. Si falla el parseo, la ruta de CoT le pide primero al LLM que reformatee la respuesta como JSON estricto, y solo entonces el código recurre a una expresión regular que captura los tokens numéricos y se queda con los que caen dentro de la escala (&lt;code&gt;generated.py:35,97&lt;/code&gt;). Si quedan varios números dentro del rango, elige el &lt;strong&gt;mínimo&lt;/strong&gt;. Según el comentario del código, es para que &lt;em&gt;«1 out of 3»&lt;/em&gt; se lea como 1, pero así también &lt;em&gt;«between 1 and 3»&lt;/em&gt; se convierte en 1, un comportamiento por defecto que sesga a la baja la puntuación de las respuestas que no se mojan. Y con la escala de 0 a 3 por defecto, &lt;em&gt;«5 out of 10»&lt;/em&gt; da error de parseo y no cuenta como un 5. Casi nunca importa, hasta el día en que importa. El número aceptado se normaliza después a un float entre 0 y 1 con &lt;code&gt;(raw - min) / (max - min)&lt;/code&gt;, y ese float es lo que ve el usuario.&lt;/p&gt;
&lt;p&gt;Conviene ver esto en su justa medida. En TruLens la rúbrica negativa aparece solo donde hacía falta; no es un principio de diseño general que recorra todos los prompts. Es un invento de la evaluación de agentes, añadido cuando TruLens 2.x incorporó siete métricas de agente que necesitaban distinguirse entre sí. Si lees las rúbricas más antiguas (RAG, calidad, seguridad), verás que las cláusulas negativas escasean o desaparecen.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-pierdes&quot;&gt;Lo que pierdes&lt;/h2&gt;
&lt;p&gt;Crecen las dos mitades del prompt. Las cláusulas negativas alargan la rúbrica y, con ello, encarecen en tokens cada llamada de puntuación. En una puerta de CI que lanza cientos de llamadas de feedback en cada evaluación, ese coste se nota, aunque pasa lo mismo con cualquier rúbrica estructurada.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Una rúbrica ilustrativa y simplificada al estilo de PlanQuality, montada capa a capa. Las líneas CRITICAL y «strategy, not outcome» son texto real de TruLens (agent.py:212, 225) y la capa de CoT es una versión abreviada de la plantilla de base.py. La rúbrica base y el bloque de alcance son inventados (PlanQuality no tiene bloque de alcance) y se dejan en inglés, como los prompts de TruLens. Como la rúbrica base es corta, el crecimiento sale exagerado: en el prompt de sistema real de PlanQuality, de unos 1400 tokens sin contar la traza, las frases de exclusión son más o menos una décima parte.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-rubric-anchoring/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;El coste más grave no se ve. Si una cláusula negativa está mal, le dice al juez que ignore justo la señal que debería sopesar. La creencia previa del autor queda codificada en el prompt y la métrica entera descansa sobre ella. Si esa creencia es errónea, la métrica se equivoca igual en todos los casos de test, sin hacer ruido y sin un parser que lo detecte.&lt;/p&gt;
&lt;p&gt;El patrón exige además que haya algo que desambiguar. TruLens lo necesitaba porque sus siete métricas de agente se solapan, mientras que un juez con un único propósito (una rúbrica, una puntuación) no tiene de qué desmarcarse. En ese caso, la cláusula negativa se queda en adorno (&lt;em&gt;«no penalices la verbosidad»&lt;/em&gt;), luego en letra muerta y en la siguiente revisión se borra. Por otro lado, el anclaje por rúbrica estrecha la pregunta que se le hace al juez, pero no su salida, que sigue siendo un Likert de 0 a 3 normalizado a un float entre 0 y 1. La varianza que haya al producir ese entero no se ha ido a ninguna parte. Has cambiado &lt;em&gt;qué&lt;/em&gt; se puntúa, pero la puntuación es igual de estable que antes.&lt;/p&gt;
&lt;h2 id=&quot;cuándo-encaja-este-enfoque&quot;&gt;Cuándo encaja este enfoque&lt;/h2&gt;
&lt;p&gt;Para saber si esta estrategia te sirve basta con una pregunta.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Tienes más de una métrica que razonablemente podría sopesar los mismos indicios?&lt;/strong&gt; Si es así, el anclaje por rúbrica es lo más barato que puedes hacer para que no cuenten dos veces lo mismo. Tanto la calidad del plan como la eficiencia de la ejecución tienen algo que decir sobre si el agente tuvo éxito. Si el prompt de cada una le dice al juez que ignore el terreno de la otra, las dos siguen midiendo cosas distintas. En cambio, si tu juez tiene una sola rúbrica y un solo veredicto, las cláusulas negativas no añaden nada que puedas verificar y el prompt crece en balde.&lt;/p&gt;
&lt;p&gt;Su sitio natural es la evaluación de agentes, porque en cuanto puntúas por separado el plan, la ejecución y las herramientas, es inevitable que los criterios se solapen. Con RAG quizá se gane un poco; con calidad y seguridad, normalmente nada.&lt;/p&gt;
&lt;p&gt;Lo que el anclaje por rúbrica no te dice es si la rúbrica es la adecuada. Las cláusulas negativas codifican la creencia previa del autor sobre dónde falla el juez, una creencia que no pasa de conjetura mientras nadie la mida. La siguiente parte repasa una herramienta que ataca el mismo problema desde el lado contrario y se pregunta en qué casos puedes prescindir del juez por completo y quedarte con comprobaciones deterministas.&lt;/p&gt;
&lt;p&gt;Como regla práctica, el anclaje por rúbrica es lo que escribes cuando has visto al juez hacerse un lío. Si aún no te ha pasado, no lo necesitas. Si ya te ha pasado, sabrás qué tiene que ignorar.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 5 de 7. El anclaje por rúbrica consiste en decirle al juez qué puntuar y, después, qué ignorar. Las cláusulas de rúbrica negativa que TruLens usa en sus métricas de agente son el punto por donde la experiencia previa de quien escribe la rúbrica (lo que el juez hace mal una y otra vez) entra en el prompt y se queda fijada en él. La siguiente parte repasa una herramienta que va más allá y se pregunta en qué casos puedes prescindir del juez por completo. Si has llegado aquí directamente, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/&quot;&gt;parte 1&lt;/a&gt; pone nombre a la varianza, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/&quot;&gt;parte 2&lt;/a&gt; la acota con veredictos binarios, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-structural-decomposition/&quot;&gt;parte 3&lt;/a&gt; divide un juicio difícil en un grafo de juicios más pequeños y la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-probabilistic-calibration/&quot;&gt;parte 4&lt;/a&gt; mide la certeza de la respuesta.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Forma parte de la misma serie y está relacionado con un artículo que presenté en DITTET 2026 (julio) sobre la varianza de los jueces en evaluadores de código abierto.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Metodología de evaluación</category><category>LLM como juez</category><category>Metodología de evaluación</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>Cuando los jueces muestran su incertidumbre: DeepEval y ARES</title><link>https://cchinchilla.dev/es/posts/llm-as-judge-probabilistic-calibration/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/llm-as-judge-probabilistic-calibration/</guid><description>DeepEval pondera las puntuaciones por las probabilidades de los tokens y ARES aplica Prediction-Powered Inference para dar intervalos de confianza formales. Llegan al mismo objetivo por caminos distintos.</description><pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 3 analizaba una herramienta que divide un juicio en otros más pequeños, un grafo de preguntas binarias en el que el camino recorrido decide la puntuación. La estrategia que toca ahora, la calibración probabilística, deja de dividir y se pregunta otra cosa. Ejecuta el juicio una sola vez e intenta medir cuánta certeza hay detrás de la respuesta.&lt;/p&gt;
&lt;p&gt;Estoy construyendo AgentAnvil, un framework de pruebas guiado por contratos para agentes basados en LLM, y leer otros evaluadores me sirve para averiguar qué deberían restringir esos contratos. La calibración probabilística es la estrategia sobre la que más me ha costado formarme una opinión, porque dos herramientas llegan a ella por caminos bien distintos. DeepEval lee la incertidumbre &lt;em&gt;dentro&lt;/em&gt; de una sola llamada al juez, en las probabilidades que el propio modelo asigna a los tokens. ARES coloca un intervalo de confianza formal &lt;em&gt;alrededor&lt;/em&gt; de la puntuación que da el juez sobre un conjunto de evaluación. La primera devuelve un número más fino y la segunda, un intervalo que puedes defender.&lt;/p&gt;
&lt;p&gt;En esta parte leo el código fuente de las dos.&lt;/p&gt;
&lt;h2 id=&quot;la-intuición&quot;&gt;La intuición&lt;/h2&gt;
&lt;p&gt;La incertidumbre de un juez LLM está en dos sitios, y cada herramienta va a por uno de ellos.&lt;/p&gt;
&lt;p&gt;El primero está dentro de una sola llamada. Aunque la respuesta JSON dé otra impresión, el juez nunca se queda con un único entero. Lo que produce es una distribución sobre los enteros que ha considerado, con la mayor parte de la probabilidad en «4», algo en «3» y «5» y una pizca en el resto. La &lt;em&gt;dispersión&lt;/em&gt; de esa distribución es la incertidumbre que expresa el juez, una señal de confianza aunque no esté calibrada. Un juez que pone el 99 % de su masa en «4» está seguro; uno que reparte un 35 %, un 30 % y un 25 % entre «3», «4» y «5» está dudando. El entero en bruto esconde los dos casos detrás del mismo número.&lt;/p&gt;
&lt;p&gt;El segundo está entre llamadas. Si ejecutas el juez sobre mil entradas y promedias las puntuaciones, tienes la cifra de un benchmark. Si lo ejecutas sobre una muestra más pequeña con etiquetas de referencia humanas, tienes una medida de su sesgo. Al combinar las dos cosas puedes pasar de un punto a un intervalo, el intervalo de confianza que escribiría un estadístico. El juez sigue igual de ruidoso que antes y lo único que haces es acotar cuánto podría equivocarse la cifra agregada.&lt;/p&gt;
&lt;p&gt;La función de DeepEval que consume el primer tipo de incertidumbre, &lt;code&gt;calculate_weighted_summed_score&lt;/code&gt;, ya salió en la parte 3. Allí lo que importaba era que la ponderación por probabilidad recupera un número continuo a partir de una escala de enteros. Aquí la función hace otra cosa: la dispersión de la distribución &lt;em&gt;es&lt;/em&gt; la confianza que expresa el juez, aunque el campo de la puntuación nunca la devuelva. ARES va a por el segundo tipo desde un ángulo que no tiene nada que ver, con la maquinaria estadística de un artículo de 2023 sobre prediction-powered inference. Son dos caminos para el mismo problema, con un código muy distinto en cada uno.&lt;/p&gt;
&lt;h2 id=&quot;deepeval-una-distribución-detrás-de-cada-puntuación&quot;&gt;DeepEval: una distribución detrás de cada puntuación&lt;/h2&gt;
&lt;p&gt;Para puntuar un caso de prueba, GEval hace una sola llamada y en ella pide también las log-probabilidades del token de la puntuación. La llamada al proveedor necesita un endpoint compatible con OpenAI Chat Completions (las ramas del código para OpenAI, Azure, LiteLLM y las pasarelas OpenRouter/Portkey la repiten cada una por su cuenta, y el resto de proveedores se quedan con el entero en bruto). Los parámetros se pasan explícitamente.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:44ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;completion &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; client.chat.completions.create(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;model&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.name,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;messages,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;temperature&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.temperature,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;logprobs&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;top_logprobs&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;top_logprobs,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.generation_kwargs,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;top_logprobs&lt;/code&gt; es un parámetro del constructor de &lt;code&gt;GEval&lt;/code&gt; que por defecto vale 20, el tope de la API de OpenAI, aunque DeepEval lo recorta según el modelo (a gpt-5.4, su juez por defecto, le deja 5). Para cada token generado, la respuesta trae hasta ese número de alternativas a las que el modelo daba alguna probabilidad. La distribución la pone el proveedor y GEval la consume.&lt;/p&gt;
&lt;p&gt;Luego está el suelo de ruido. En la parte 3 ya vimos el cuerpo del bucle de &lt;code&gt;calculate_weighted_summed_score&lt;/code&gt;, el que descarta los tokens con menos de un 1 % de probabilidad lineal y suma el resto según su peso. El umbral del 1 % es una variable local fija en el código.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:80ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# Filter out tokens with &amp;lt;1% linear probability, i.e., logprobs &amp;lt; math.log(0.01)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;min_logprob &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; math.log(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0.01&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;No se puede configurar ni se expone, y ni siquiera es una constante. Es un número mágico metido en el cuerpo de la función. El punto en el que el ruido del juez deja de contar lo ha fijado el autor de GEval.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: La distribución del juez sobre las puntuaciones enteras. Arrastra el control de concentración: la puntuación ponderada se queda en 4,00 y la distribución sigue siendo simétrica, pero la confianza del juez sí cambia. GEval.score no registra ni la distribución ni la confianza.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-probabilistic-calibration/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Mueve el control de concentración. La puntuación ponderada se queda en 4,00 porque la distribución es simétrica y la ponderación por probabilidad respeta la simetría. Lo que sí cambia con la dispersión es la etiqueta de al lado, que salta entre confianza «alta» y «baja». Esa etiqueta es lo que GEval no devuelve.&lt;/p&gt;
&lt;p&gt;A la salida de la métrica, todo se reduce a un número. La distribución pasa entera por &lt;code&gt;calculate_weighted_summed_score&lt;/code&gt; y acaba convertida en un único escalar normalizado.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:57ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.score &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;float&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(g_score) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.score_range[&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;])&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.score_range_span&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;not&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.strict_mode&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(g_score)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.success &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.is_successful()  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# score &amp;gt;= threshold&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A partir de ahí, quien llama a la métrica puede leer &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;reason&lt;/code&gt;, &lt;code&gt;verbose_logs&lt;/code&gt;, los pasos de evaluación, el coste y el número de tokens, y ningún otro dato que salga de la distribución. No hay probabilidades por token, ni recuento de tokens descartados, ni entropía, ni segundo momento. La media ponderada es el veredicto, y el aprobado o el suspenso sale de compararla con el umbral. La distribución del juez ha alimentado la puntuación y luego se ha esfumado.&lt;/p&gt;
&lt;p&gt;Para ser justos, GEval implementa fielmente la receta de &lt;a href=&quot;https://arxiv.org/abs/2303.16634&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Liu et al. (2023)&lt;/a&gt;. La ponderación por probabilidad es el recurso que recupera una puntuación continua a partir de una rúbrica de enteros, y DeepEval la incorpora de forma limpia, con un filtro del 1 % comentado (aunque fijo en el código) y un valor por defecto razonable. El mecanismo es correcto. Lo que falta está en la interfaz con quien llama, porque la información solo existe mientras dura una llamada a una función y nada en la API la devuelve.&lt;/p&gt;
&lt;h2 id=&quot;ares-un-intervalo-de-confianza-alrededor-de-la-puntuación&quot;&gt;ARES: un intervalo de confianza alrededor de la puntuación&lt;/h2&gt;
&lt;p&gt;ARES se hace otra pregunta. Cambia &lt;em&gt;«¿cómo de seguro está el juez de esta entrada concreta?»&lt;/em&gt; por &lt;em&gt;«¿cómo de seguro puedo estar de la puntuación agregada, sabiendo que una pequeña parte tiene etiquetas de referencia?»&lt;/em&gt;. El juez de ARES es, por defecto, un clasificador DeBERTa-v3-large ajustado, aunque se puede sustituir por un juez LLM. Sea cual sea, devuelve una predicción de 0 o 1 por cada ejemplo. Lo interesante del código es lo que ARES hace con esas predicciones.&lt;/p&gt;
&lt;p&gt;Todo parte de tres arrays. En &lt;code&gt;Y_labeled&lt;/code&gt; están las etiquetas de referencia humanas de un subconjunto pequeño anotado, normalmente unos pocos cientos de ejemplos; en &lt;code&gt;Yhat_labeled&lt;/code&gt;, las predicciones del juez sobre esos mismos ejemplos, y en &lt;code&gt;Yhat_unlabeled&lt;/code&gt;, sus predicciones sobre el conjunto de evaluación grande, el que no tiene etiquetas. Con eso, una sola función se encarga de toda la estadística.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:75ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#DCBDFB&quot;&gt;pp_mean_iid_asymptotic&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(Y_labeled, Yhat_labeled, Yhat_unlabeled, alpha):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;n &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Y_labeled.shape[&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;N &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Yhat_unlabeled.shape[&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;tildethetaf &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Yhat_unlabeled.mean()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;rechat &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (Yhat_labeled &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Y_labeled).mean()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;thetahatPP &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; tildethetaf &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; rechat&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;sigmaftilde &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; np.std(Yhat_unlabeled)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;sigmarec &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; np.std(Yhat_labeled &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Y_labeled)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;hw &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; norm.ppf(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; alpha &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; np.sqrt(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;sigmaftilde &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; N &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; sigmarec &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; n&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; [thetahatPP &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; hw, thetahatPP &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; hw]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Ese es el estimador entero, y tiene tres piezas.&lt;/p&gt;
&lt;p&gt;La primera es la estimación puntual. &lt;code&gt;tildethetaf&lt;/code&gt; es la media del juez sobre el conjunto sin etiquetar, un número que arrastra el sesgo del juez si lo tiene. &lt;code&gt;rechat&lt;/code&gt; es el &lt;em&gt;error&lt;/em&gt; medio del juez sobre el subconjunto etiquetado, &lt;code&gt;mean(Yhat - Y)&lt;/code&gt;. Al restar uno de otro sale &lt;code&gt;thetahatPP&lt;/code&gt;, la puntuación agregada del juez una vez descontado el sesgo que se le ha medido. Lo que hace posible la corrección es el subconjunto etiquetado, porque sin etiquetas de referencia no hay rechat ni rectificación.&lt;/p&gt;
&lt;p&gt;La segunda es el intervalo. &lt;code&gt;hw&lt;/code&gt; es la semiamplitud y suma dos términos: &lt;code&gt;sigma_f² / N&lt;/code&gt;, del juez sobre el conjunto grande sin etiquetar, y &lt;code&gt;sigma_rec² / n&lt;/code&gt;, del rectificador sobre el pequeño etiquetado. Como el conjunto sin etiquetar suele ser varios órdenes de magnitud mayor, su suelo es pequeño y el término que domina es el del rectificador. Con el &lt;code&gt;alpha=0.05&lt;/code&gt; por defecto, el multiplicador Z ronda 1,96, el del intervalo del 95 % de toda la vida.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: La semiamplitud del intervalo PPI tiene dos términos de varianza: uno se reduce al crecer el conjunto etiquetado por humanos n, y el otro, al crecer el conjunto sin etiquetar N. Al arrastrar los dos controles, el intervalo se estrecha y cambia el reparto de la varianza.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-probabilistic-calibration/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Lo que añade ARES es el &lt;em&gt;par&lt;/em&gt; &lt;code&gt;[thetahatPP - hw, thetahatPP + hw]&lt;/code&gt;, que se presenta junto a la estimación puntual (el punto medio del par). El ejemplo práctico del README imprime una predicción de 0,606 encima del intervalo &lt;code&gt;[0.547, 0.664]&lt;/code&gt;. Ese 0,606 ya viene con el sesgo corregido (no es la media en bruto del juez). El intervalo es lo que permite a quien use el resultado leer &lt;em&gt;«la puntuación agregada real está en este rango con un 95 % de confianza»&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Hay dos rarezas que conviene señalar. El conjunto anotado por humanos se limita por defecto a 300 ejemplos (&lt;code&gt;.head(300)&lt;/code&gt;), lo que pone un suelo fijo al término del rectificador. Y lo que parece una comparación con el intervalo clásico es código muerto: &lt;code&gt;binomial_iid&lt;/code&gt; se llama, pero nunca se importa, así que solo sobrevive el intervalo PPI.&lt;/p&gt;
&lt;p&gt;Para ser justos con ARES, la estadística que ha elegido es la primitiva adecuada para la pregunta que se hace. PPI es reciente (&lt;a href=&quot;https://arxiv.org/abs/2301.09633&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Angelopoulos et al. (2023)&lt;/a&gt;), y la mayoría de las herramientas de evaluación todavía no han asimilado que &lt;em&gt;«tenemos un conjunto de referencia pequeño y un conjunto grande sin etiquetar»&lt;/em&gt; es un caso con un estimador ya publicado. ARES lo ha recogido y lo ha llevado al código. Solo por eso ya merece la pena leerlo.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-pierdes&quot;&gt;Lo que pierdes&lt;/h2&gt;
&lt;p&gt;Los dos caminos tienen un precio, y cada uno lo paga a su manera. DeepEval lo paga en la interfaz. La distribución existe dentro de una llamada, la función la usa y después deja de existir para quien llama. Una puerta de CI que decide con &lt;code&gt;self.score &amp;gt;= self.threshold&lt;/code&gt; no puede preguntar &lt;em&gt;«¿ha sido un aprobado seguro o uno dudoso?»&lt;/em&gt;, porque el campo que lo respondería no existe. ARES lo paga en etiquetas humanas, unos pocos cientos de ejemplos anotados (usa 300 como máximo y el README pide al menos 50). Para un artículo o un benchmark de cara a un regulador es un coste que se paga una vez; para una puerta de CI que se ejecuta en cada PR, es prohibitivo. Cada herramienta paga el coste que encaja con el uso para el que está pensada, y en ninguna de las dos es un descuido.&lt;/p&gt;
&lt;p&gt;El prompt también las separa. La ponderación por probabilidad trata el prompt como algo fijo y no acota el eje de sensibilidad al prompt que señalaba la parte 1. Si reformulas la rúbrica, la distribución varía, pero la función sigue informando de la confianza dentro de ese prompt como si nada hubiera cambiado. PPI, en cambio, está anclado en otro sitio. Como son las etiquetas humanas las que definen el objetivo, reformular el prompt del juez cambia la amplitud del intervalo, pero no lo que el intervalo estima, siempre que el conjunto de referencia siga correspondiendo al sistema. La ponderación por probabilidad calibra &lt;em&gt;dado&lt;/em&gt; el prompt. Calibrar &lt;em&gt;entre&lt;/em&gt; prompts es otro problema que pide su propio artículo.&lt;/p&gt;
&lt;h2 id=&quot;cuándo-encaja-este-enfoque&quot;&gt;Cuándo encaja este enfoque&lt;/h2&gt;
&lt;p&gt;Qué camino tomar, o si conviene alguno, depende de una sola pregunta.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Lo que produces es una puntuación o una medida?&lt;/strong&gt; Una puntuación es lo que lee una puerta de CI, un número para hoy y para esta PR. Una medida es lo que lleva un artículo, una tabla de clasificación o un informe de auditoría, es decir, una afirmación sobre el agregado que tiene que sostenerse cuando la examinen a fondo. Como primitiva de puntuación, la ponderación por probabilidad de DeepEval es mejor: es barata, necesita una sola llamada, no exige anotar nada y devuelve un número continuo que respeta las dudas del juez. El PPI de ARES, en cambio, es una primitiva de medida. Devuelve un intervalo cuya amplitud se puede discutir y cuyos supuestos se pueden auditar, pero el término del rectificador necesita unos pocos cientos de etiquetas de referencia, presupuestadas una sola vez. Cuantas menos tengas, más ancho sale el intervalo. Si no puedes pagarlo, PPI queda descartado.&lt;/p&gt;
&lt;p&gt;Ninguna de las dos te da un juez cuyos veredictos se mantengan estables cuando reformulas el prompt. Si lo que ha cambiado entre despliegues es la rúbrica, DeepEval se adaptará sin avisar y dará cifras nuevas que no se pueden comparar con las anteriores. ARES sigue anclado a sus etiquetas de referencia, a costa de tener que mantenerlas al día. La siguiente parte analiza una herramienta que ataca ese problema sin medir la incertidumbre. Lo que hace es decirle al juez de antemano qué partes del juicio tiene que dejar fuera.&lt;/p&gt;
&lt;p&gt;Si hay que quedarse con una regla, la ponderación por probabilidad es un adorno sobre un único juicio y PPI es inferencia estadística sobre una población de juicios. Elige la que trabaje al mismo nivel de análisis que la pregunta que te piden responder.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 4 de 7. La calibración probabilística tiene dos caminos para decirte lo seguro que está el juez (las probabilidades de los tokens dentro de una llamada y un intervalo de confianza alrededor de un agregado). La siguiente entrega analiza una herramienta que ni siquiera intenta medir la incertidumbre del juez: le dice de antemano qué partes del juicio debe dejar fuera. Si has llegado aquí directamente, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/&quot;&gt;parte 1&lt;/a&gt; pone nombre a la varianza, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/&quot;&gt;parte 2&lt;/a&gt; la acota con veredictos binarios y la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-structural-decomposition/&quot;&gt;parte 3&lt;/a&gt; divide un juicio difícil en un grafo de juicios más pequeños.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Forma parte de la misma serie y está relacionado con un artículo que presenté en DITTET 2026 (julio) sobre la varianza de los jueces en evaluadores de código abierto.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Metodología de evaluación</category><category>LLM como juez</category><category>Metodología de evaluación</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>De la rúbrica al grafo: cómo divide DeepEval un juicio</title><link>https://cchinchilla.dev/es/posts/llm-as-judge-structural-decomposition/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/llm-as-judge-structural-decomposition/</guid><description>DeepEval plantea la evaluación como el recorrido de un grafo en el que cada nodo es una decisión más sencilla que el juicio completo.</description><pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 2 analizaba una herramienta que acota la varianza del juez limitando lo que puede decir. Ragas cambia la puntuación de 1 a 5 por un veredicto de sí o no, así que el juez ya no se mueve por una escala y solo puede caer a un lado u otro de una línea.&lt;/p&gt;
&lt;p&gt;Esta vez toca otra herramienta. Estoy construyendo AgentAnvil, un framework de pruebas guiado por contratos para agentes basados en LLM, y leer otros evaluadores me sirve para averiguar qué deberían restringir esos contratos. DeepEval acota la varianza cambiando la pregunta, sin tocar en absoluto la salida del juez. Divide el juicio, que planteado de golpe es una sola pregunta difícil, en una secuencia (o un grafo) de preguntas más pequeñas, cada una más fácil que el conjunto. En eso consiste la estrategia de descomposición estructural.&lt;/p&gt;
&lt;p&gt;DeepEval la ofrece de dos formas. G-Eval organiza el juicio como una lista de comprobación ordenada, y el DAG, como un grafo ramificado que el juez está obligado a recorrer. Aquí leo las dos directamente en el código fuente.&lt;/p&gt;
&lt;h2 id=&quot;la-intuición&quot;&gt;La intuición&lt;/h2&gt;
&lt;p&gt;«¿Es bueno este resumen?» es una sola pregunta, pero contestarla exige dar al menos cuatro respuestas: si es fiel a la fuente, si cubre los puntos principales, si es conciso y si es coherente. Un juez que recibe la pregunta entera tiene que sopesar las cuatro y reducirlas a un solo número. Todas esas subdecisiones se toman dentro del modelo (como mucho quedan parafraseadas en el motivo, nunca desglosadas), y el peso de cada una es el que el modelo haya decidido en esa ejecución.&lt;/p&gt;
&lt;p&gt;La herramienta de la parte 2 recortaba la salida del juez para dejarle menos cosas que decir y menos margen para oscilar. La descomposición deja la salida como está y recorta la pregunta haciendo las cuatro subpreguntas por separado. «¿Todas las frases tienen respaldo en la fuente, sí o no?» es una pregunta más concreta que «¿esto es bueno?», y una pregunta más concreta tiene menos varianza. El juez abarca menos cosas a la vez y tiene menos formas de contradecirse.&lt;/p&gt;
&lt;p&gt;También cambia de sitio la regla de combinación. En un juez monolítico esa regla está implícita en los pesos del modelo y es distinta en cada ejecución. Al descomponer, la regla pasa a código escrito por ti, ya sea una media, un umbral o una rama. Y el código no tiene varianza. No has hecho más fiable al juez (cada subllamada sigue siendo una llamada a un LLM), pero has sacado del modelo la tarea de juntar las partes. Ya no te puede sorprender cómo se han ponderado, porque las has ponderado tú.&lt;/p&gt;
&lt;p&gt;Las dos formas se diferencian en una sola cosa: una sigue pidiéndole al juez una puntuación, solo que le da mejores instrucciones, y la otra no se la pide nunca.&lt;/p&gt;
&lt;h2 id=&quot;g-eval-paso-a-paso&quot;&gt;G-Eval, paso a paso&lt;/h2&gt;
&lt;p&gt;G-Eval es la forma más suave. La métrica &lt;code&gt;GEval&lt;/code&gt; de DeepEval parte de una cadena &lt;code&gt;criteria&lt;/code&gt; en lenguaje natural o de una lista explícita &lt;code&gt;evaluation_steps&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:78ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#F69D50&quot;&gt;GEval&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#6CB6FF&quot;&gt;BaseMetric&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;__init__&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;self,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;name: &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;evaluation_params: Optional[List[SingleTurnParams]] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;criteria: Optional[&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;evaluation_steps: Optional[List[&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;]] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; criteria &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;not&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; evaluation_steps &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;not&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:12ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;validate_criteria_and_evaluation_steps(criteria, evaluation_steps)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;El validador exige al menos una de las dos, y si pasas las dos mandan los pasos explícitos. Si le das &lt;code&gt;criteria&lt;/code&gt;, la métrica convierte esa única frase en pasos antes de que el juez puntúe un caso de prueba.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:78ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#DCBDFB&quot;&gt;_generate_evaluation_steps&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(self, multimodal: &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;bool&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;) -&amp;gt; List[&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;]:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.evaluation_steps:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.evaluation_steps&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;g_eval_params_str &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; construct_g_eval_params_string(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.evaluation_params)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;prompt &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;._get_prompt(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;generate_evaluation_steps&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;criteria&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.criteria,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;parameters&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;g_eval_params_str,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;multimodal&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;multimodal,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; generate_with_schema_and_extract(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;metric&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;prompt&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;prompt, &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;schema_cls&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;gschema.Steps, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;El prompt es una sola instrucción: &lt;em&gt;«generate 3-4 concise evaluation steps based on the criteria»&lt;/em&gt;. El artículo de G-Eval, &lt;a href=&quot;https://arxiv.org/abs/2303.16634&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Liu et al. (2023)&lt;/a&gt;, llama a estos pasos cadena de razonamiento (chain of thought). DeepEval genera esa cadena a partir de tu criterio antes de puntuar, con lo que una instrucción vaga se convierte en una lista de comprobación explícita y numerada. Esa lista es la descomposición. La instancia de la métrica la guarda, pero &lt;code&gt;evaluate()&lt;/code&gt; (asíncrona por defecto) puntúa cada caso de prueba con una copia nueva de la métrica, de modo que en una ejecución por defecto cada caso recibe su propia lista recién generada. Si quieres la misma lista para todo un dataset, pasa &lt;code&gt;evaluation_steps&lt;/code&gt; de forma explícita.&lt;/p&gt;
&lt;p&gt;Luego viene la llamada de puntuación. El juez ve los pasos numerados y el caso de prueba y devuelve un objeto JSON con una puntuación y un motivo. La puntuación es un entero dentro de un rango (de 0 a 10 por defecto) que DeepEval normaliza a una métrica entre 0 y 1.&lt;/p&gt;
&lt;p&gt;Una puntuación entera es justo la salida de poca resolución que señalaba la parte 1, con los valores amontonados en un mismo entero. Si le pides un entero a un modelo, toda su distribución se reduce a uno solo. Cuando el juez da acceso a las log-probabilidades de los tokens (el juez de OpenAI que DeepEval usa por defecto lo hace), DeepEval no se queda con el entero. Lee las log-probabilidades del token de la puntuación (cada entero que ha considerado el modelo y cuánta probabilidad le ha dado a cada uno) y devuelve la media ponderada por probabilidad.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:77ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# inside calculate_weighted_summed_score(...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;min_logprob &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; math.log(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0.01&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# filter out tokens with &amp;lt;1% linear probability&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; token_logprob &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; score_logprobs.top_logprobs:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;logprob &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; token_logprob.logprob&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; logprob &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; min_logprob:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;not&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; token_logprob.token.isdecimal():&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;linear_prob &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; math.exp(logprob)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;token_score &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(token_logprob.token)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# ... accumulate token_score weighted by linear_prob ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;weighted_summed_score &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; sum_of_weighted_scores &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; sum_linear_probability&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Los tokens con menos del 1 % de probabilidad se descartan, y los que quedan se suman ponderados por su probabilidad. El juez dice «4», pero la puntuación ponderada se queda cerca de 3,8 porque el modelo ponía más probabilidad en el 3 que en el 5. Con eso se recupera una puntuación continua a partir de enteros amontonados, sin hacer una segunda llamada. Los jueces que no dan acceso a las logprobs (los wrappers de DeepEval para Anthropic y Gemini, la mayoría de los modelos personalizados) se quedan con el entero en bruto, y &lt;code&gt;strict_mode&lt;/code&gt; hace lo mismo.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Un juez LLM rara vez se decide por un solo entero: reparte la probabilidad entre los tokens de puntuación. Arrastra la opinión del juez y verás que la barra azul (el argmax, el entero que daría una puntuación en bruto) va a saltos, mientras que la línea ámbar, la puntuación de DeepEval ponderada por logprobs, se desliza.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-structural-decomposition/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Para ser justos con G-Eval, aquí la descomposición es orientativa. Los pasos guían al juez, pero no lo atan, y nada impide al modelo leer el paso tres por encima. A cambio de esa holgura, G-Eval gana alcance. Funciona con cualquier criterio que puedas escribir en una frase y, como la lista se genera sola, no necesitas conocer las subpreguntas de antemano. El juez sigue emitiendo el juicio entero, solo que con mejores instrucciones.&lt;/p&gt;
&lt;h2 id=&quot;el-paradigma-dag&quot;&gt;El paradigma DAG&lt;/h2&gt;
&lt;p&gt;En el DAG, la descomposición sí ata al juez.&lt;/p&gt;
&lt;p&gt;Mientras que G-Eval guía al juez y aun así le pide una puntuación, &lt;code&gt;DAGMetric&lt;/code&gt; no se la pide nunca. Le das un grafo, un &lt;code&gt;DeepAcyclicGraph&lt;/code&gt;, y el juez solo responde a un nodo cada vez. Hay cuatro tipos de nodo.&lt;/p&gt;
&lt;p&gt;Un &lt;code&gt;TaskNode&lt;/code&gt; es un paso de extracción que hace una llamada al LLM para sacar material intermedio («enumera las afirmaciones del resumen») y se lo pasa a los nodos siguientes, sin puntuar nada por sí mismo.&lt;/p&gt;
&lt;p&gt;Un &lt;code&gt;BinaryJudgementNode&lt;/code&gt; hace una única pregunta de sí o no y necesita exactamente dos hijos, uno por respuesta.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:99ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;__post_init__&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(self):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;len&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.children) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;raise&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;ValueError&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;BinaryJudgementNode must have exactly 2 children.&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;verdicts &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; [child.verdict &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; child &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.children]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; verdicts.count(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; verdicts.count(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;False&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;raise&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;ValueError&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;BinaryJudgementNode must have one True and one False VerdictNode child.&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Un &lt;code&gt;NonBinaryJudgementNode&lt;/code&gt; hace una única pregunta de opción múltiple, y como DeepEval construye el esquema del veredicto en tiempo de ejecución, el modelo no puede salirse de la lista.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:57ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;._verdict_schema &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; create_model(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;NonBinaryJudgementVerdict&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;verdict&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(Literal[&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;tuple&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;._verdict_options)], &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#AE4B07;--1:#F69D50&quot;&gt;reason&lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Por último, un &lt;code&gt;VerdictNode&lt;/code&gt; es el desenlace de una rama y lleva un &lt;code&gt;score&lt;/code&gt; o un &lt;code&gt;child&lt;/code&gt;, nunca las dos cosas.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:81ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;__post_init__&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(self):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.score &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;not&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.child &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;not&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;raise&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;ValueError&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:12ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;            &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;A VerdictNode can have either a &amp;#39;score&amp;#39; or a &amp;#39;child&amp;#39;, but not both.&amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.score &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.child &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;raise&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;ValueError&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:12ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;            &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;A VerdictNode must have either a &amp;#39;score&amp;#39; or a &amp;#39;child&amp;#39;.&amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Si lleva un &lt;code&gt;score&lt;/code&gt;, el nodo es una hoja, y si lleva un &lt;code&gt;child&lt;/code&gt;, el recorrido continúa.&lt;/p&gt;
&lt;p&gt;Ejecutar la métrica consiste en recorrer el grafo. Cada nodo de juicio hace su única llamada y obtiene un veredicto, que decide qué hijo se ejecuta. Los demás hijos terminan sin hacer nada.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:58ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# VerdictNode._execute — a branch runs only if its verdict&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# matches the parent judgement&amp;#39;s verdict&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;._parent._verdict.verdict &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;.verdict:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Al final llegas a una hoja, y su &lt;code&gt;score&lt;/code&gt; dividido entre diez es la métrica.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Una DAGMetric de DeepEval que mide la calidad de un resumen con tres juicios binarios y cuatro hojas con puntuación. Responde a cada pregunta: el camino se ilumina, las ramas descartadas se apagan y la hoja a la que llegas fija la puntuación.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-structural-decomposition/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Eso es lo que distingue al DAG de G-Eval. En G-Eval todos los pasos desembocan en una única llamada de puntuación y el juicio lo sigue emitiendo el juez, eso sí, bien informado. En el DAG, en cambio, el juez nunca llega a emitir el juicio. Solo se le pide un veredicto binario o n-ario por nodo, y la puntuación la decide el grafo, que es código escrito por ti. Con los mismos veredictos se recorre el mismo camino y sale el mismo número en todas las ejecuciones. DeepEval rechaza un grafo con algún ciclo antes de ejecutar nada, con un escueto &lt;em&gt;«Cycle detected in DAG graph»&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Además, las dos formas se pueden combinar, ya que el hijo de un &lt;code&gt;VerdictNode&lt;/code&gt; puede ser un &lt;code&gt;GEval&lt;/code&gt;. Cuando una rama ya no necesita bifurcarse más y pide una puntuación graduada, cuelgas un G-Eval de la hoja. El grafo decide el camino y G-Eval puntúa el último tramo. No son rivales; una puede contener a la otra.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-pierdes&quot;&gt;Lo que pierdes&lt;/h2&gt;
&lt;p&gt;La descomposición tiene dos costes.&lt;/p&gt;
&lt;p&gt;El primero es que hay más sitios donde equivocarse. Un juez monolítico hace una sola llamada que puede salir mal; un DAG con un nodo de tarea, dos nodos de juicio y cuatro hojas hace tres o cuatro, cada una con su propio riesgo de fallar. Además, esas llamadas se ejecutan en secuencia a lo largo del camino, así que en una puerta de CI ese DAG cuesta entre tres y cuatro veces la latencia y el gasto en API de un único juez global. La falta de fiabilidad del juez sigue ahí, repartida ahora entre más nodos. La apuesta, que se puede perder, es que cada llamada pequeña sea lo bastante fiable para que el producto siga ganando al monolito. El peor caso es un nodo interior, donde un veredicto erróneo manda el recorrido por una rama del todo equivocada y la puntuación cambia de golpe. Si el nodo se equivoca de forma sistemática, se equivoca igual en todas las ejecuciones; si su veredicto está en el límite, la puntuación salta de una rama a otra de una ejecución a la siguiente.&lt;/p&gt;
&lt;p&gt;El segundo es que ahora tienes que acertar con la estructura. El grafo es tu teoría del juicio: qué subpreguntas importan, en qué orden y cómo se combinan. Si la teoría está mal, la métrica se equivoca de forma determinista con todas las entradas. G-Eval tiene el mismo problema en versión más suave, porque sus pasos generados automáticamente también los escribe un LLM y la mayoría de la gente no los lee nunca.&lt;/p&gt;
&lt;p&gt;Conviene verlo con buenos ojos, como el precio justo de hacer legible un juicio. Un juez monolítico esconde su razonamiento y no puedes ver dónde se ha equivocado; uno descompuesto te señala exactamente de qué nodo desconfiar. Los dos tienen bugs, pero solo uno tiene stack trace.&lt;/p&gt;
&lt;h2 id=&quot;cuándo-encaja-este-enfoque&quot;&gt;Cuándo encaja este enfoque&lt;/h2&gt;
&lt;p&gt;La descomposición compensa cuando se dan tres condiciones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;El juicio tiene partes reales.&lt;/strong&gt; «Fiel, conciso y completo» se separa limpiamente en tres comprobaciones, pero «¿es elegante esta explicación?» no. Meter con calzador un grafo en un juicio sin costuras solo añade llamadas y una falsa sensación de rigor. Descompón donde el juicio tenga costuras.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conoces las subpreguntas mejor de lo que el modelo las adivinaría.&lt;/strong&gt; Si tu descomposición se limita a poner por escrito los pasos implícitos del propio modelo, eso ya lo hace gratis la lista que genera G-Eval. El DAG justifica su complejidad cuando sabes algo que el modelo no deduciría, como un orden, una dependencia o un cortocircuito. Por ejemplo, si falla la comprobación de fidelidad, el recorrido debería pararse antes de gastar una sola llamada en puntuar el estilo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Necesitas un resultado auditable o estable.&lt;/strong&gt; Un DAG te da un camino fijo que puedes señalar en una revisión. Cuando hay que explicarle el veredicto de una puerta a alguien que no la ha escrito, el recorrido ya es la explicación, cosa que no ocurre con una puntuación global.&lt;/p&gt;
&lt;p&gt;Como regla práctica, recurre a G-Eval cuando quieras una puntuación global mejor a bajo coste, y al DAG cuando el juicio tenga una estructura real que necesites imponer, sin conformarte con sugerirla. Ten en cuenta que entre las dos no hay una frontera nítida y que un G-Eval en la hoja de un DAG es un término medio con todas las de la ley. En el fondo, la herramienta es lo de menos: lo que eliges es cuánto del juicio puedes permitirte dejar dentro del modelo.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 3 de 7. La descomposición estructural divide un juicio difícil en un grafo de juicios más pequeños y le deja la aritmética al código. La siguiente parte repasa una herramienta que no toca el razonamiento del juez y se pregunta, en cambio, cuánto podría desviarse su puntuación. Si has llegado aquí directamente, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/&quot;&gt;parte 1&lt;/a&gt; pone nombre a la varianza y la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/&quot;&gt;parte 2&lt;/a&gt; la acota con veredictos binarios.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Forma parte de la misma serie y está relacionado con un artículo que presento en DITTET 2026 (julio) sobre la varianza de los jueces en evaluadores de código abierto.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Metodología de evaluación</category><category>LLM como juez</category><category>Metodología de evaluación</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>El artículo 72 define el bucle, pero tus paneles no lo cierran</title><link>https://cchinchilla.dev/es/posts/eu-ai-act-article-72-monitoring-gap/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/eu-ai-act-article-72-monitoring-gap/</guid><description>El artículo 72 exige que los datos de toda la vida útil alimenten la gestión de riesgos. Los paneles no cierran ese bucle y el modelo de plan quedaría en guía.</description><pubDate>Tue, 16 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div class=&quot;lede-intro&quot;&gt;&lt;p&gt;Creía que nuestros modelos ya estaban monitorizados. Teníamos paneles, alertas y un turno de guardia que despertaba a alguien cuando se disparaba la latencia.&lt;/p&gt;&lt;p&gt;Nada de eso era un sistema de vigilancia poscomercialización, al menos en el sentido que le da el artículo 72.&lt;/p&gt;&lt;p&gt;Un panel te dice que el sistema está en marcha. El artículo 72 pregunta otra cosa: si sigue siendo conforme y si puedes demostrarlo durante todo el tiempo que funcione. Al cambiar la pregunta cambian también lo que hay que entregar y quién lo lee. Esta entrada recorre la distancia entre el panel y lo que pide el artículo.&lt;/p&gt;&lt;/div&gt;
&lt;h2 id=&quot;qué-dice-exactamente-el-artículo-72&quot;&gt;Qué dice exactamente el artículo 72&lt;/h2&gt;
&lt;p&gt;El artículo 72 del &lt;a href=&quot;https://eur-lex.europa.eu/legal-content/ES/TXT/?uri=CELEX:32024R1689&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Reglamento (UE) 2024/1689&lt;/a&gt; está en el capítulo de vigilancia poscomercialización y regula qué obligaciones tienen los proveedores de sistemas de IA de alto riesgo desde que el sistema sale al mercado, pasadas ya la evaluación de la conformidad y el marcado CE, y mientras siga en él. Tiene cuatro apartados, y cada uno describe una pieza de un bucle.&lt;/p&gt;
&lt;p&gt;El apartado 1 impone la obligación. Los proveedores establecerán y documentarán un sistema de vigilancia poscomercialización proporcionado a la naturaleza de las tecnologías de IA y a los riesgos en juego. Fíjate en &lt;em&gt;documentarán&lt;/em&gt;. Lo que se entrega es un sistema documentado, y la simple costumbre de monitorizar no vale. Tiene que poder leerlo una autoridad de vigilancia del mercado, o un organismo notificado si interviene alguno.&lt;/p&gt;
&lt;p&gt;El apartado 2 es el motor. El sistema recopilará, documentará y analizará de manera activa y sistemática los datos pertinentes sobre el funcionamiento durante toda la vida útil del sistema, una vida útil que no se acaba en las pruebas de aceptación ni en la última auditoría. Y hay una frase sobre la que gira la mayor parte de esta entrada: &lt;em&gt;«Cuando proceda, la vigilancia poscomercialización incluirá un análisis de la interacción con otros sistemas de IA»&lt;/em&gt;. En un sistema multiagente procede por definición. La ley recoge el caso multiagente de forma expresa, sin que haga falta deducirlo.&lt;/p&gt;
&lt;p&gt;El apartado 3 establece que el sistema se basa en un plan, que a su vez forma parte de la documentación técnica del anexo IV. La Comisión tenía que dar a los proveedores un modelo para ese plan en un acto de ejecución, a más tardar el 2 de febrero de 2026. Quédate con esa fecha, porque más adelante cuento qué fue de ese acto.&lt;/p&gt;
&lt;p&gt;El apartado 4 es la cláusula de reutilización. Los proveedores cuyo producto está sujeto a la legislación del anexo I, sección A (productos sanitarios, máquinas) y que ya tienen un sistema y un plan de vigilancia poscomercialización con arreglo a ella pueden integrar en ellos los elementos que piden los apartados anteriores, siempre que la protección sea equivalente. Las entidades financieras pueden hacer lo mismo con sus sistemas del anexo III, punto 5, como los de calificación crediticia.&lt;/p&gt;
&lt;p&gt;Entre los cuatro apartados describen un bucle en el que se recopila, se analiza, se documenta en un plan y el resultado vuelve a alimentar el proceso.&lt;/p&gt;
&lt;h2 id=&quot;qué-se-monitoriza-hoy-en-la-práctica&quot;&gt;Qué se monitoriza hoy en la práctica&lt;/h2&gt;
&lt;p&gt;Ese es el bucle que describe la ley. Las herramientas de monitorización que un equipo usa de verdad abarcan menos, y el resto de esta entrada trata de esa distancia. La mayoría caben en cuatro grupos, y cada uno responde a una pregunta real.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Detección de deriva.&lt;/strong&gt; Herramientas como Evidently y NannyML observan las distribuciones de entrada y de salida y avisan cuando cambian. Como no necesita etiquetas, la detección de deriva es lo primero que montan los equipos. Capta justo aquello para lo que está pensada: que los datos de hoy ya no se parecen a los que se usaron para entrenar el modelo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Métricas por segmento.&lt;/strong&gt; Desglosan el rendimiento por cohorte, por región o por tipo de entrada y sacan a la luz degradaciones que una cifra agregada esconde. Son útiles y se usan poco.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plataformas de observabilidad.&lt;/strong&gt; Con Prometheus, Grafana u OpenTelemetry se miden la latencia, la tasa de errores, el throughput y el coste por petición. Son la columna vertebral de la operación, están maduras y responden a si el sistema está en marcha y a cuánto cuesta mantenerlo así.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trazas.&lt;/strong&gt; Registran lo que ha hecho el agente, con un span por cada paso y por cada llamada a una herramienta. Son el grupo más nuevo de los cuatro y el menos asentado.&lt;/p&gt;
&lt;p&gt;Las cuatro categorías son legítimas, pero ninguna responde a la pregunta que hace el artículo 72. El desajuste se ve en tres cosas.&lt;/p&gt;
&lt;p&gt;La primera es que las cuatro son operativas y ninguna mide el cumplimiento. Te dicen que el sistema está sano, pero no si en uso real se mantiene el nivel adecuado de precisión, solidez y ciberseguridad que los apartados 1 y 3 del artículo 15 te obligan a alcanzar y a declarar. &lt;em&gt;Sano&lt;/em&gt; y &lt;em&gt;conforme&lt;/em&gt; son afirmaciones distintas, y solo una de ellas está en la ley.&lt;/p&gt;
&lt;p&gt;La segunda, que nada relaciona una señal con los artículos. Cuando salta una alerta de deriva, ni una persona ni una herramienta la conectan con el nivel adecuado que el artículo 15, apartado 3, te obliga a declarar, ni con una línea del registro de riesgos del artículo 9. La señal existe, pero falta la correspondencia.&lt;/p&gt;
&lt;p&gt;La tercera, que la cláusula de interacción no tiene esquema. Un &lt;em&gt;análisis de la interacción con otros sistemas de IA&lt;/em&gt; da por hecho que sabes decir qué sistema influyó en cuál. OpenTelemetry está construyendo spans de agente y multiagente justo para eso, pero las convenciones GenAI siguen marcadas como Development: son un borrador y todavía no un estándar. La cláusula ya está en la ley, mientras que el esquema para cumplirla aún no está terminado.&lt;/p&gt;
&lt;h2 id=&quot;el-mapa-del-bucle&quot;&gt;El mapa del bucle&lt;/h2&gt;
&lt;p&gt;El gráfico pone seis requisitos, derivados del artículo 72 y de los artículos conexos, frente a lo que ofrecen hoy las herramientas de monitorización.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Lo que exigen el artículo 72 y los artículos conexos (9, 26 y 73) una vez que un sistema de IA de alto riesgo sale al mercado, y lo que de verdad ofrecen las herramientas de observabilidad actuales. Dos requisitos son huecos claros, y los demás se cubren en parte: de todos hay alguna pieza, pero ninguno está completo.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/eu-ai-act-article-72-monitoring-gap/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Hay dos huecos claros. Los demás requisitos se cubren en parte: de todos hay alguna pieza, pero ninguno está completo.&lt;/p&gt;
&lt;h2 id=&quot;dónde-se-quedan-cortas-las-herramientas&quot;&gt;Dónde se quedan cortas las herramientas&lt;/h2&gt;
&lt;p&gt;La distancia entre el artículo y las herramientas es mayor en cuatro puntos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Señales de cumplimiento frente a métricas de operaciones.&lt;/strong&gt; Una alerta de deriva es un evento de operaciones, y solo pasa a ser un evento de cumplimiento cuando alguien la relaciona con el nivel adecuado que el artículo 15, apartado 3, te obliga a declarar y deja constancia de ese juicio en el sistema de gestión de riesgos del artículo 9. Ese paso se hace a mano, o no se hace. Las herramientas hablan el idioma del SRE (latencia, saturación, tasa de errores) y el artículo habla otro, el de la precisión, la solidez y la ciberseguridad mantenidas durante todo el ciclo de vida. Nada traduce automáticamente de un idioma al otro.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;El plan es un documento, y un panel no lo sustituye.&lt;/strong&gt; El apartado 3 pide un plan de vigilancia poscomercialización versionado y fechado, que esté en la documentación técnica del anexo IV y que lea una autoridad de vigilancia del mercado. Un panel de Grafana no cumple nada de eso. Va en directo, es efímero y lo mira un ingeniero a las tres de la madrugada. Los dos son útiles, pero no son lo mismo. Tener el segundo no te exime de la obligación de elaborar el primero.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Causalidad entre sistemas.&lt;/strong&gt; Es la cláusula a la que dije que volvería. En &lt;a href=&quot;https://cchinchilla.dev/es/posts/eu-ai-act-article-15-testing-gap/&quot;&gt;la entrada sobre el artículo 15&lt;/a&gt; señalé la coordinación multiagente como el hueco al que las herramientas aún no habían llegado. El artículo 72, apartado 2, lo menciona sin rodeos al pedir que, cuando proceda, se analice la interacción con otros sistemas de IA. Para eso tienes que poder decir qué salida de un sistema condicionó la siguiente acción de otro, es decir, establecer causalidad a través de una frontera en la que no hay observabilidad. Pongamos que un agente de triaje rebaja la prioridad de un caso porque un agente de recuperación le ha pasado un documento desfasado. La salida parece normal y la métrica sigue en verde, pero nada deja constancia de que el agente de triaje decidió así por la respuesta del de recuperación. Multiplícalo por una flota de agentes que se llaman entre sí, y la &lt;em&gt;interacción&lt;/em&gt; deja de ser algo que puedas auditar a posteriori. Lo más parecido a un esquema son los spans de agente de OpenTelemetry, que siguen en desarrollo. Hasta que estén terminados, la &lt;em&gt;interacción con otros sistemas de IA&lt;/em&gt; es una obligación que puedes poner por escrito pero todavía no medir.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cerrar el bucle con el artículo 9.&lt;/strong&gt; Una monitorización que nunca actualiza el registro de riesgos deja el bucle abierto. El artículo 72 alimenta el artículo 9 (el sistema de gestión de riesgos, que abarca todo el ciclo de vida), y los dos forman una sola disciplina. Las herramientas llegan hasta el panel y ahí se paran, pero el artículo espera que la señal del panel cambie el registro de riesgos, el plan de pruebas y la próxima versión. Ese último salto, de la observación a una acción que queda registrada en la documentación, es el que ninguna herramienta da por ti.&lt;/p&gt;
&lt;h2 id=&quot;el-acto-de-ejecución-que-no-llegó&quot;&gt;El acto de ejecución que no llegó&lt;/h2&gt;
&lt;p&gt;El único apoyo oficial que el artículo daba a los proveedores para el plan era un modelo. El apartado 3 encargaba a la Comisión adoptar, a más tardar el 2 de febrero de 2026, un acto de ejecución con un modelo estándar de plan de vigilancia poscomercialización.&lt;/p&gt;
&lt;p&gt;No llegó en esos términos. El Reglamento ómnibus digital sobre IA, el paquete de simplificación que propone la propia Comisión, le quitaría la facultad de adoptar ese modelo obligatorio. La vigilancia poscomercialización sigue siendo obligatoria, pero el modelo pasaría a ser una guía voluntaria, prevista para el 2 de septiembre de 2027 a más tardar. El Parlamento y el Consejo alcanzaron un &lt;a href=&quot;https://www.consilium.europa.eu/en/press/press-releases/2026/05/07/artificial-intelligence-council-and-parliament-agree-to-simplify-and-streamline-rules/&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;acuerdo provisional sobre el paquete el 7 de mayo de 2026&lt;/a&gt;, aunque todavía no se ha adoptado formalmente. Su adopción se espera antes del 2 de agosto de 2026, y hasta entonces nada de esto es vinculante.&lt;/p&gt;
&lt;p&gt;Así que la fecha ha pasado, y el único documento normalizado que te habría dicho cómo es un plan conforme se quedaría en una guía. El plan lo escribes de todos modos, sin modelo, para cumplir una obligación que no ha desaparecido.&lt;/p&gt;
&lt;p&gt;También cambiaría el calendario. El mismo paquete aplazaría del 2 de agosto de 2026 al 2 de diciembre de 2027 las obligaciones de alto riesgo para los sistemas del anexo III. Sería un respiro de verdad, aunque no te libraría del trabajo. El sistema de vigilancia tiene que existir igualmente, y construirlo bien se come la mayor parte del tiempo que gana el aplazamiento. Como con todo lo que aún se mueve en este Reglamento, planifica con la ley tal como está hoy, sin contar con la versión que esperas que llegue.&lt;/p&gt;
&lt;p&gt;Hay un plazo que no se ha movido, el de dar la alarma. El aplazamiento cambiaría cuándo empieza a apretar el artículo 73, pero no la rapidez con que aprieta. Cuando las obligaciones de alto riesgo se apliquen a tu sistema, el artículo 73 exige notificar un incidente grave en un plazo de 15 días desde que se tiene conocimiento de él, que baja a 10 días si ha fallecido una persona y a 2 en caso de infracción generalizada o de alteración grave e irreversible de infraestructuras críticas. Esos plazos corren tanto si tu monitorización ha detectado el incidente como si no. Un sistema de vigilancia poscomercialización que no puede detectar y escalar dentro de esos plazos no pasa de ser un panel con una etiqueta de cumplimiento.&lt;/p&gt;
&lt;h2 id=&quot;qué-hacer-mientras-tanto&quot;&gt;Qué hacer mientras tanto&lt;/h2&gt;
&lt;p&gt;Puedes dar cuatro pasos desde ahora, sin esperar a ninguna guía, modelo de plan ni norma armonizada.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Escribe el plan ya y toma prestado un modelo que exista.&lt;/strong&gt; No va a llegar ningún modelo oficial en los términos originales, pero tampoco necesitas el de la Comisión. La normativa de productos sanitarios lleva años aplicando el seguimiento poscomercialización. Su estructura de plan (el plan de seguimiento poscomercialización del MDR y del IVDR) es el equivalente más maduro y encaja bien con el artículo 72. Parte de ahí, convierte el plan en un documento versionado dentro de la documentación técnica y revísalo cada vez que cambien el modelo de IA, el prompt o las dependencias.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Instrumenta con las convenciones GenAI de OpenTelemetry, aunque estén en borrador.&lt;/strong&gt; No están terminadas, pero ya definen spans para las operaciones de modelos y de agentes, que es lo más parecido a un esquema para la cláusula de interacción. Si las adoptas ahora, cuando las convenciones se estabilicen tus trazas entre sistemas ya existirán y no tendrás que empezar de cero ese día.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prepara el protocolo de respuesta a incidentes según los plazos del artículo 73.&lt;/strong&gt; Encadena detección, triaje y notificación para que los plazos de 15, 10 y 2 días se conviertan en procedimientos que se puedan ejecutar y dejen de ser párrafos de una política que nadie ha probado. Ensáyalo una vez antes de necesitarlo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Construye el canal de retroalimentación desde el responsable del despliegue.&lt;/strong&gt; Si eres el proveedor, la mitad de tus datos de vigilancia los tiene el responsable del despliegue, al que el artículo 26 obliga a vigilar el funcionamiento del sistema, conservar los archivos de registro durante al menos seis meses y avisarte cuando algo no cuadre. Esa señal es la que cierra tu bucle, pero solo si hay un canal por el que pueda llegar y un sitio en el registro de riesgos donde quede anotada. Hacen falta los dos.&lt;/p&gt;
&lt;p&gt;Nada de esto exige que exista el modelo de plan. La obligación consiste en mantener un bucle en marcha: observar el sistema en uso real, analizar lo que ves, escribirlo en un plan y dejar que eso cambie la próxima versión. Las herramientas te dan la observación, y el resto es ingeniería que te toca a ti.&lt;/p&gt;
&lt;p&gt;Una monitorización que nunca actualiza el registro de riesgos deja el bucle abierto. El artículo 72 ha definido el bucle y el ecosistema ha construido la instrumentación. Cerrarlo es el trabajo que queda.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Reglamento europeo de IA</category><category>Reglamento europeo de IA</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>Veredictos binarios: cómo mantiene Ragas a raya a los jueces</title><link>https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/</guid><description>Ragas descompone los juicios complejos en preguntas atómicas de sí o no. A cambio de menos varianza, pierdes información.</description><pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;La parte 1 puso nombre a cuatro ejes de varianza del LLM como juez. Lo más sencillo que puede hacer una herramienta para combatirla es restringir lo que el juez puede decir, y en eso consiste la estrategia de restricción del alcance. En esta parte recorro cómo la aplica Ragas en dos de sus métricas.&lt;/p&gt;
&lt;p&gt;Estoy construyendo AgentAnvil, un framework de pruebas guiado por contratos para agentes basados en LLM, y leo el código de otros evaluadores para averiguar qué forma deberían tener los contratos de AgentAnvil. El pipeline de fidelidad (faithfulness) de Ragas es de los ejemplos más claros de este patrón que he visto. Un año más tarde, sus autores aplicaron el mismo esquema a los agentes.&lt;/p&gt;
&lt;h2 id=&quot;la-intuición&quot;&gt;La intuición&lt;/h2&gt;
&lt;p&gt;Con un LLM como juez, la varianza contra la que peleas es mayor en el centro de la escala. Si le pides a un juez una puntuación de 1 a 5, el ruido se concentra en la frontera entre el 3 y el 4. Ahí es donde discrepan dos jueces entre sí y donde un mismo juez se contradice. Los extremos son más fáciles, porque un 1 y un 5 suelen ser casos claramente malos o claramente buenos, que no se deciden a cara o cruz.&lt;/p&gt;
&lt;p&gt;Si eliminas el centro, el ruido se queda sin sitio.&lt;/p&gt;
&lt;p&gt;La idea es sustituir una rúbrica continua por un veredicto binario, y así &lt;em&gt;«¿Es esta respuesta fiel al contexto?»&lt;/em&gt; pasa a ser &lt;em&gt;«¿Está respaldada la afirmación K, sí o no?»&lt;/em&gt;. El juez ya no recorre una escala y se limita a caer a un lado u otro de una línea. La varianza no desaparece de la frontera, pero ahora esa frontera es una decisión de implicación (entailment) y ha dejado de ser una nota de calidad.&lt;/p&gt;
&lt;p&gt;Al agregar los bits vuelves a tener un número de aspecto continuo, la fracción de afirmaciones respaldadas. Sale de átomos binarios, sin ningún juicio continuo de por medio. Cada veredicto que cambia mueve la puntuación 0,1 si hay diez afirmaciones y 0,05 si hay veinte. En la medida en que los errores de los veredictos sean independientes, el ruido del juez se promedia y no se acumula. Ragas juzga todas las afirmaciones juntas en una sola llamada y vuelve a descomponer la respuesta en cada ejecución, así que en la práctica la dispersión se reduce más despacio de lo que muestra la demo de abajo.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Simulación de una misma respuesta que un juez ruidoso puntúa una y otra vez. El histograma muestra dónde cae la puntuación en las distintas ejecuciones y la línea discontinua marca su media. Con más afirmaciones la dispersión se estrecha, suponiendo que cada veredicto se equivoque de forma independiente sobre un conjunto fijo de afirmaciones. La línea continua es el umbral de la puerta: si la llevas dentro de la dispersión, la puerta se vuelve inestable, y si la sacas de ella, se estabiliza.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;El pipeline de fidelidad de Ragas gira en torno a esta idea.&lt;/p&gt;
&lt;h2 id=&quot;la-fidelidad-en-ragas-paso-a-paso&quot;&gt;La fidelidad en Ragas, paso a paso&lt;/h2&gt;
&lt;p&gt;En Ragas, la fidelidad se calcula con dos llamadas al LLM y un paso aritmético. La primera llamada descompone, la segunda juzga y la puntuación es una fracción.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Fidelidad en Ragas: descomponer, juzgar, agregar.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Los fragmentos que siguen salen de los módulos originales &lt;code&gt;ragas.metrics._faithfulness&lt;/code&gt; y &lt;code&gt;_goal_accuracy&lt;/code&gt;. Desde la v0.4.2, Ragas recomienda importar estas métricas desde &lt;code&gt;ragas.metrics.collections&lt;/code&gt;, que conserva los mismos prompts (salvo la frase sobre el formato JSON), los tipos de veredicto y el cálculo de la puntuación.&lt;/p&gt;
&lt;p&gt;La descomposición está en &lt;code&gt;StatementGeneratorPrompt&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:76ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#F69D50&quot;&gt;StatementGeneratorPrompt&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;PydanticPrompt[StatementGeneratorInput, StatementGeneratorOutput]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;instruction &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;Given a question and an answer, analyze the complexity of each &amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;sentence in the answer. Break down each sentence into one or more &amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;fully understandable statements. Ensure that no pronouns are used &amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;in any statement. Format the outputs in JSON.&amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;La instrucción de no usar pronombres es la pieza que hace funcionar el resto del pipeline. Cada afirmación tiene que entenderse por sí sola, porque la siguiente llamada la juzga sin tener delante el resto de la respuesta. El propio prompt de descomposición de Ragas trae un ejemplo few-shot en el que una respuesta de dos frases sobre Einstein se descompone en cuatro afirmaciones independientes que pueden juzgarse por separado. Por los pronombres se cuela la dependencia, y una buena descomposición le cierra el paso.&lt;/p&gt;
&lt;p&gt;El veredicto lo emite &lt;code&gt;NLIStatementPrompt&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:80ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#F69D50&quot;&gt;NLIStatementPrompt&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(PydanticPrompt[NLIStatementInput, NLIStatementOutput]):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;instruction &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;Your task is to judge the faithfulness of a series of statements &amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;based on a given context. For each statement you must return &amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;verdict as 1 if the statement can be directly inferred based on &amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;the context or 0 if the statement can not be directly inferred &amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;based on the context.&amp;quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Una sola llamada juzga todas las afirmaciones a la vez y devuelve un veredicto por cada una. La instrucción acota explícitamente la salida a 1 o 0, sin término medio.&lt;/p&gt;
&lt;p&gt;Después viene la puntuación.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:71ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#DCBDFB&quot;&gt;_compute_score&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(self, answers: NLIStatementOutput):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#616972;--1:#87939E&quot;&gt;# check the verdicts and compute the score&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;faithful_statements &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;sum&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; answer.verdict &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; answer &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; answers.statements&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;num_statements &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;len&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(answers.statements)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; num_statements:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;score &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; faithful_statements &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; num_statements&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;logger.warning(&lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;No statements were generated from the answer.&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:8ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;score &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; np.nan&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; score&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Es la fracción de afirmaciones respaldadas, una salida de aspecto continuo obtenida al promediar juicios binarios.&lt;/p&gt;
&lt;p&gt;Quedan tres detalles de implementación.&lt;/p&gt;
&lt;p&gt;En primer lugar, lo que hace en realidad el juez cuando puntúa la «fidelidad» es inferencia en lenguaje natural, una tarea muy estudiada. Ragas reduce a dos las tres etiquetas habituales de esa tarea (implicación, neutral y contradicción) y solo distingue si hay implicación o no la hay. El planteamiento lo toma prestado de una tarea que ya existía.&lt;/p&gt;
&lt;p&gt;En segundo lugar, todas las afirmaciones cuentan lo mismo, sin ponderar por importancia, sin penalizar la longitud y sin tener en cuenta la confianza. Esa uniformidad hace que acotar la varianza salga barato, y también que la puntuación se pueda manipular. Basta con rellenar una respuesta con tres afirmaciones triviales que el contexto respalda, colocadas delante de la que de verdad importa, para que la fidelidad suba. La puntuación no sabe en qué átomo está el significado.&lt;/p&gt;
&lt;p&gt;Por último, el contrato de tipos es &lt;code&gt;verdict: int&lt;/code&gt; (cualquier entero). El prompt es el que acota el valor a 0 o 1, y &lt;code&gt;_compute_score&lt;/code&gt; recurre a la semántica truthy. La restricción está, por tanto, en la capa del prompt; la métrica de agentes la endurece en la capa de tipos.&lt;/p&gt;
&lt;h2 id=&quot;el-mismo-patrón-en-agentgoalaccuracy&quot;&gt;El mismo patrón en &lt;code&gt;AgentGoalAccuracy&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Más o menos un año después de su artículo de 2023, Ragas llevó el mismo patrón a los agentes.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AgentGoalAccuracy&lt;/code&gt; hace dos llamadas al LLM sobre la traza de una conversación de varios turnos, y el veredicto es directamente la métrica. La primera deduce qué buscaba la ejecución y cómo terminó.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:32ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#F69D50&quot;&gt;WorkflowOutput&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#6CB6FF&quot;&gt;BaseModel&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;user_goal: &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Field(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;end_state: &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Field(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;La segunda compara las dos cosas y responde a una pregunta binaria.&lt;/p&gt;
&lt;div class=&quot;expressive-code&quot;&gt;&lt;figure class=&quot;frame&quot;&gt;&lt;pre data-language=&quot;python&quot; class=&quot;wrap&quot; style=&quot;--ecMaxLine:45ch&quot;&gt;&lt;code&gt;&lt;div class=&quot;ec-line&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#F69D50&quot;&gt;CompareOutcomeOutput&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--0:#6F42C1;--1:#6CB6FF&quot;&gt;BaseModel&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;reason: &lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Field(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ec-line&quot; style=&quot;--ecIndent:4ch&quot;&gt;&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;indent&quot;&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;verdict: t.Literal[&lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--0:#032F62;--1:#96D0FF&quot;&gt;&amp;quot;1&amp;quot;&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;--0:#BF3441;--1:#F47067&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt; Field(&lt;/span&gt;&lt;span style=&quot;--0:#005CC5;--1:#6CB6FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;--0:#24292E;--1:#ADBAC7&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;copy&quot;&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Con &lt;code&gt;Literal[&amp;quot;0&amp;quot;, &amp;quot;1&amp;quot;]&lt;/code&gt;, el alcance queda fijado en el tipo, además de en el prompt. Pydantic no admite nada que no sea &lt;code&gt;&amp;quot;0&amp;quot;&lt;/code&gt; o &lt;code&gt;&amp;quot;1&amp;quot;&lt;/code&gt;, así que, si un LLM intenta devolver &lt;code&gt;&amp;quot;1.0&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;yes&amp;quot;&lt;/code&gt; o un objeto JSON, el parser lo rechaza. El paso de reintento de Ragas puede pedirle a un LLM que arregle el formato, pero a la puntuación nunca llega otra cosa que &lt;code&gt;&amp;quot;0&amp;quot;&lt;/code&gt; o &lt;code&gt;&amp;quot;1&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Es una vuelta de tuerca respecto a la fidelidad, donde &lt;code&gt;verdict: int&lt;/code&gt; acepta cualquier entero. La métrica tiene dos variantes. &lt;code&gt;WithReference&lt;/code&gt; compara con un objetivo que se le pasa y &lt;code&gt;WithoutReference&lt;/code&gt;, con el que acaba de deducir el propio modelo. El mecanismo (deducir y luego hacer una sola pregunta binaria) es idéntico, y se transfiere porque también se transfiere el juicio de fondo. En la mayoría de los planteamientos razonables, &lt;em&gt;«¿Ha conseguido la ejecución el objetivo del usuario?»&lt;/em&gt; se responde con un sí o un no.&lt;/p&gt;
&lt;p&gt;Un detalle para quien vaya a meter esto en la CI: la traza nunca llega en bruto al juez. Antes de que la vea el primer prompt, la métrica llama a &lt;code&gt;sample.pretty_repr()&lt;/code&gt; para aplanar la conversación de varios turnos en una sola cadena, y el segundo prompt solo ve lo que ha deducido el primero (más la referencia, cuando se pasa). Por eso dos pipelines con formateadores distintos podrían dar veredictos distintos a partir de la misma traza. El formateador forma parte de la métrica, aunque nunca aparezca en su nombre.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-pierdes&quot;&gt;Lo que pierdes&lt;/h2&gt;
&lt;p&gt;Los veredictos binarios no salen gratis.&lt;/p&gt;
&lt;p&gt;Un veredicto binario no puede decirte que una respuesta es correcta en parte y otra es excelente, ya que las dos acaban en «respaldada» o «no respaldada». Para una puerta de pasa o no pasa, esa granularidad es la adecuada, pero a la hora de orientar al desarrollador (&lt;em&gt;«¿cómo lo mejoro?»&lt;/em&gt;) los veredictos binarios se quedan cortos. Una puntuación de 0,6 no dice cuáles eran las tres afirmaciones respaldadas de las cinco, ni si las dos sin respaldo eran pequeñas licencias o alucinaciones en toda regla. La forma del fallo se pierde.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Ejemplo construido a mano de una respuesta errónea que se cuela por la puerta. Su única afirmación con contenido (la roja) es inventada y el contexto de abajo no la respalda; todo lo demás es relleno. Sube la puntuación con relleno, eleva después el umbral de la puerta para defenderte y verás lo poco que sirve.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Para ser justos con el diseño, que la forma sea binaria no quiere decir que Ragas se olvidara de la granularidad. Sus autores eligieron lo binario porque el caso de uso con el que creció la herramienta, la evaluación de la salida de sistemas RAG, tiene justo esa forma. En RAG, la fidelidad es implicación: o la afirmación está en el contexto recuperado o no está. El cumplimiento del objetivo del agente, la métrica que añadieron más tarde para agentes, también tiene forma de implicación. En los dos casos, la forma de la pregunta encaja con la del veredicto.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;«¿Es bueno lo binario?»&lt;/em&gt; es la pregunta equivocada. La que hay que hacerse es &lt;em&gt;«¿Tiene el juicio forma binaria?»&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;cuándo-encaja-este-enfoque&quot;&gt;Cuándo encaja este enfoque&lt;/h2&gt;
&lt;p&gt;Los veredictos binarios son el enfoque adecuado cuando se cumplen tres condiciones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La decisión es pasa o no pasa, sin grados intermedios.&lt;/strong&gt; En el fondo, una puerta de CI es un pasa o no pasa, aunque la puntuación sea un float de 0 a 1. En cambio, una métrica de cara al usuario que tenga que distinguir entre «bueno», «muy bueno» y «excelente» no lo es.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Los juicios intermedios se descomponen en comprobaciones independientes de sí o no.&lt;/strong&gt; La fidelidad se presta a ello de forma natural: dado el contexto, cada afirmación es independiente de las demás. Un juicio cuyas partes solo tienen sentido juntas no sobrevivirá al pipeline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La varianza de la zona media sale más cara que perder información.&lt;/strong&gt; Una puerta de CI inestable que falla 1 de cada 20 ejecuciones por los vaivenes del juez es peor que una puerta que no da ningún detalle de diagnóstico. De los dos costes, quédate con el que te salga más barato.&lt;/p&gt;
&lt;p&gt;En cuanto a los umbrales, elige a la vez el punto de corte y el N habitual. Un umbral de fidelidad de 0,8 con tres afirmaciones equivale a &lt;em&gt;«tres de tres»&lt;/em&gt;: basta una afirmación sin respaldo para que la puerta falle, de modo que ya es binaria en la propia métrica. Con veinte, equivale a &lt;em&gt;«dieciséis de veinte»&lt;/em&gt;, una puerta más cercana a lo continuo. Mientras no decidas cuántos átomos esperas, el umbral es un número sin significado operativo.&lt;/p&gt;
&lt;p&gt;Cuando se cumplen las tres condiciones, los veredictos binarios son la herramienta más limpia que tienes.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 2 de 7. Los veredictos binarios son el mecanismo más sencillo para acotar lo que puede decir un juez. En la siguiente parte leo el código de una herramienta que pone la cota en otro sitio. Si has llegado aquí directamente, la &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/&quot;&gt;parte 1&lt;/a&gt; pone nombre a la varianza que intentas acotar.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Forma parte de la misma serie y está relacionado con un artículo que presento en DITTET 2026 (julio) sobre la varianza de los jueces en evaluadores de código abierto.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Metodología de evaluación</category><category>LLM como juez</category><category>Metodología de evaluación</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>El problema del LLM como juez es la varianza, y las herramientas no se ponen de acuerdo en cómo arreglarla</title><link>https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/</guid><description>He leído el código fuente de siete herramientas de código abierto que usan un LLM como juez. La varianza aparece en cuatro ejes, y lo único que comparten los siete enfoques es el problema.</description><pubDate>Tue, 19 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div class=&quot;lede-intro&quot;&gt;&lt;p&gt;En lo único que coinciden es en la palabra «juez».&lt;/p&gt;&lt;p&gt;Para mi tesis doctoral he leído el código fuente de siete herramientas de código abierto que usan un LLM como juez. Además estoy desarrollando un framework de pruebas para agentes LLM, y las dos cosas influyen en cómo leo estas herramientas.&lt;/p&gt;&lt;p&gt;El problema es la varianza. Si ejecutas un juez LLM dos veces sobre la misma entrada, la puntuación varía; si cambias el modelo del juez, varía más, y si reformulas la rúbrica, más todavía. Ninguna de estas herramientas elimina esa varianza, pero cada una la restringe a su manera. Lo decisivo es qué restricción elige, y de eso tratan las siguientes entregas.&lt;/p&gt;&lt;/div&gt;
&lt;h2 id=&quot;por-qué-recurrir-a-un-juez&quot;&gt;Por qué recurrir a un juez&lt;/h2&gt;
&lt;p&gt;Si el paradigma existe es porque la mayor parte de lo que quieres evaluar de un agente, como la coherencia del plan, la elección de herramientas o si la respuesta fue útil, no tiene una respuesta de referencia. No hay un conjunto de test con el que comparar, así que recurres a un juez. Y el más barato que tienes a mano es un LLM potente.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/2306.05685&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Zheng et al. (2023)&lt;/a&gt; defendieron que ese LLM hiciera de juez. Bautizaron el paradigma «LLM-as-a-judge», construyeron MT-Bench en torno a él y mostraron que GPT-4 alcanzaba un 85 % de concordancia con expertos humanos en los votos sin empate del benchmark, algo por encima del 81 % que lograban los humanos entre sí en el mismo experimento. Fue esa cifra la que vendió el paradigma. Si un juez potente coincide con la preferencia humana más o menos tanto como los humanos entre sí, puedes meter un juez en una puerta de CI sin pagar a una persona para que haga ese trabajo.&lt;/p&gt;
&lt;p&gt;Unos meses antes, &lt;a href=&quot;https://arxiv.org/abs/2303.16634&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Liu et al. (2023)&lt;/a&gt; ya habían afinado la técnica. Su framework G-Eval añadía dos piezas: unos pasos de razonamiento (chain-of-thought) que el propio LLM genera antes de puntuar y una función que pondera la puntuación final por la probabilidad que el modelo asigna al token de cada opción (en GPT-4, que no exponía esas probabilidades, se estimaban con 20 muestras a temperatura 1). El resultado era una puntuación continua, capaz de recoger la incertidumbre del modelo sin reducirla a un único entero. En la tarea de resumen, la correlación de Spearman de G-Eval con las valoraciones humanas llegó a 0,514 y superó la de todos los evaluadores anteriores; el que más se acercó fue UniEval, con 0,474.&lt;/p&gt;
&lt;p&gt;Tres años después, el paradigma está en todas partes, hasta el punto de que todos los frameworks de evaluación de código abierto que he leído implementan alguna versión de él. Eso ya no se discute.&lt;/p&gt;
&lt;h2 id=&quot;cómo-aparece-la-varianza&quot;&gt;Cómo aparece la varianza&lt;/h2&gt;
&lt;p&gt;Si coges cualquier juez LLM de los que se usan de verdad, verás cuatro tipos de varianza en sus puntuaciones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;De una ejecución a otra, con el mismo modelo.&lt;/strong&gt; Aunque pongas la temperatura a cero, la puntuación sigue variando, porque al servir el modelo la inferencia no es determinista bit a bit. &lt;a href=&quot;https://arxiv.org/abs/2303.16634&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Liu et al. (2023)&lt;/a&gt; observaron un efecto relacionado: si pides a un LLM una puntuación entera en una escala de 1 a 5, las puntuaciones de entradas distintas se amontonan en un mismo entero y borran diferencias reales. Su arreglo (ponderar la puntuación por las probabilidades de los tokens, sin quedarse con el argmax) suaviza esa granularidad, pero la incertidumbre de fondo del juez sigue ahí. El juez no se decanta por una respuesta fija; en cada llamada reduce una distribución a un único valor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;De un juez a otro.&lt;/strong&gt; Si cambias el modelo que hay detrás del prompt del juez, los veredictos varían todavía más. Con los votos de Chatbot Arena, &lt;a href=&quot;https://arxiv.org/abs/2306.05685&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Zheng et al. (2023)&lt;/a&gt; vieron que GPT-4 y Claude-v1 coincidían el 66 % de las veces si se cuentan los empates y los veredictos que cambian con la posición, y el 95 % en los votos en que los dos eligen ganador. Con el mismo prompt y las mismas respuestas, otro modelo juzga distinto.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;De un prompt a otro.&lt;/strong&gt; Basta reformular la misma rúbrica para que cambie la puntuación. Zheng et al. prueban dos variantes del prompt para la misma tarea, y con solo cambiarles el nombre a los asistentes la tasa de «sesgo hacia el primero» de Claude-v1 baja del 75 % al 11,2 %. Al juez le afecta cómo están redactadas sus instrucciones, además de lo que dicen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sesgos sistemáticos que no se compensan al promediar.&lt;/strong&gt; Zheng et al. pusieron nombre a tres. El primero, el de posición, hace que los jueces favorezcan la respuesta que aparece antes. Con pares de respuestas deliberadamente parecidas, GPT-4 mantiene el veredicto el 65 % de las veces al intercambiar el orden y en el 30 % de esas pruebas lo cambia a favor de la primera, mientras que Claude-v1 lo mantiene el 23,8 % de las veces. En el segundo, el de verbosidad, un ataque de «lista repetitiva» (rellenar una respuesta con viñetas reformuladas, sin contenido nuevo) hace que Claude-v1 y GPT-3.5 elijan la respuesta más larga el 91,3 % de las veces. Y en el tercero, el de autopreferencia, GPT-4 favorece sus propias respuestas con una tasa de victorias un 10 % mayor y Claude-v1 las suyas con un 25 % más, aunque Zheng et al. advierten de que sus datos son demasiado limitados para confirmar este sesgo. &lt;a href=&quot;https://arxiv.org/abs/2411.15594&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Gu et al. (2024)&lt;/a&gt; documentan otros en su revisión posterior (el de concreción, el cultural y el de diversidad), pero los tres de Zheng son los primeros con los que se topa cualquier puerta de CI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Un matiz sobre el alcance.&lt;/strong&gt; Las cifras anteriores salen de MT-Bench y Chatbot Arena en 2023, con los GPT-4 y Claude-v1 de entonces. Con los modelos más recientes las magnitudes han cambiado, pero el patrón de fondo se mantiene y al juez le siguen afectando los detalles de forma. La deriva entre actualizaciones del modelo es un eje de varianza aparte y queda fuera de esta entrada.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Un juez real, Claude Haiku, puntúa dos comprobaciones concretas de fidelidad. Las dos rúbricas piden «faithful», pero cada una lo define a su manera: la estricta descarta la inferencia y la permisiva admite el conocimiento general. Cambia de una a otra y verás cómo se invierten los dos veredictos.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Son cuatro ejes y ninguno desaparece por sí solo. Ninguna de las siete herramientas de código abierto que he leído los elimina, aunque todas los restringen, cada una a su manera. En la sección siguiente explico por qué esa diferencia importa cuando el juez forma parte de un pipeline de producción.&lt;/p&gt;
&lt;h2 id=&quot;por-qué-importa-en-producción&quot;&gt;Por qué importa en producción&lt;/h2&gt;
&lt;p&gt;En el entorno académico, a los jueces LLM se les evalúa con benchmarks, mientras que en producción se les pone a decidir en puertas de calidad. Las dos situaciones parecen iguales, pero no lo son.&lt;/p&gt;
&lt;p&gt;Pongamos una puerta concreta: cada PR lanza la batería de tests, entre los que hay varias evaluaciones con juez LLM (la coherencia del plan sobre una entrada fija, la idoneidad de las llamadas a herramientas sobre otra y la utilidad de la respuesta sobre una tercera). Cada una puntúa entre 0 y 1, y la PR se fusiona si todas las puntuaciones superan un umbral.&lt;/p&gt;
&lt;p&gt;Ahora lleva a ese pipeline lo que describe la sección anterior. El mismo juez, con el mismo prompt y la misma entrada, da puntuaciones distintas en ejecuciones consecutivas. Si pasas el juez a otro modelo, muchas puertas cambian de veredicto, y si retocas la rúbrica en un docstring, el umbral deja de significar lo que significaba la semana pasada.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Simulación de 120 PR, una por cuadrado, que pasan por una puerta de calidad con un juez LLM ruidoso. Sube el ruido y la puerta empieza a fusionar bugs (rojo) y a bloquear trabajo válido (ámbar). Después activa el arreglo (relanzar las que fallan) y fíjate en cuántos bugs entran.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-variance-problem/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Los equipos reaccionan de dos maneras, y las dos empeoran la puerta.&lt;/p&gt;
&lt;p&gt;La primera es relanzar el test cuando falla y, si pasa al reintentarlo, darlo por bueno. Eso es enmascarar fallos reales con la excusa de gestionar tests inestables, porque cada reintento que convierte un fallo real en un aprobado es un bug fusionado. Cuando relanzar se vuelve costumbre en el equipo, la puerta pasa a ser, como mucho, informativa.&lt;/p&gt;
&lt;p&gt;La segunda es saltarse la puerta cuando conviene. Los ingenieros sénior aprenden qué puntuaciones son ruidosas y cuáles no. La puerta acaba siendo una convención social que nadie hace cumplir, y la gobernanza se va erosionando PR a PR.&lt;/p&gt;
&lt;p&gt;Las dos reacciones tienen el mismo origen. A una puerta de calidad no le basta con que la varianza sea &lt;em&gt;baja&lt;/em&gt;: necesita que esté &lt;em&gt;acotada&lt;/em&gt;, cosa que ninguno de los cuatro ejes anteriores garantiza por defecto. De ese hueco trata &lt;a href=&quot;https://cchinchilla.dev/es/series/&quot;&gt;esta serie&lt;/a&gt;, que recorre las distintas maneras en que cinco de las siete herramientas intentan poner una cota a lo que puede decir un juez.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Parte 1 de 7. En la serie leo una a una las herramientas de código abierto que usan un LLM como juez y miro cómo intenta cada una acotar la varianza. Esta primera entrega diagnostica la varianza en sí, con sus cuatro ejes y ninguna herramienta que la elimine. La siguiente analiza una herramienta que la acota con &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-scope-constraint-ragas/&quot;&gt;veredictos binarios&lt;/a&gt;, y la serie termina donde todas se quedan cortas, en &lt;a href=&quot;https://cchinchilla.dev/es/posts/llm-as-judge-the-calibration-gap/&quot;&gt;la brecha de calibración&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Pertenece a la misma serie y está relacionado con un artículo que presento en DITTET 2026 (julio) sobre la varianza de los jueces en evaluadores de código abierto.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Metodología de evaluación</category><category>LLM como juez</category><category>Metodología de evaluación</category><category>ML en producción</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item><item><title>El artículo 15 ya fija los requisitos, pero las herramientas van por detrás</title><link>https://cchinchilla.dev/es/posts/eu-ai-act-article-15-testing-gap/</link><guid isPermaLink="true">https://cchinchilla.dev/es/posts/eu-ai-act-article-15-testing-gap/</guid><description>El artículo 15 recoge cinco vulnerabilidades específicas de la IA y un requisito para todo el ciclo de vida. Las herramientas actuales cubren solo una parte, y aquí explico dónde está el hueco y qué hacer mientras tanto.</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div class=&quot;lede-intro&quot;&gt;&lt;p&gt;Daba por hecho que los requisitos de pruebas del Reglamento europeo de IA ya estaban cubiertos. Me equivocaba.&lt;/p&gt;&lt;p&gt;El artículo 15 es muy concreto: enumera cinco vulnerabilidades específicas de la IA, exige un funcionamiento uniforme durante todo el ciclo de vida del sistema y menciona los bucles de retroalimentación de los sistemas que siguen aprendiendo después del despliegue.&lt;/p&gt;&lt;p&gt;Las herramientas de evaluación que existen hoy cubren solo una parte de lo que recoge el artículo, y este texto trata de ese hueco.&lt;/p&gt;&lt;/div&gt;
&lt;h2 id=&quot;qué-dice-exactamente-el-artículo-15&quot;&gt;Qué dice exactamente el artículo 15&lt;/h2&gt;
&lt;p&gt;El artículo 15 del &lt;a href=&quot;https://eur-lex.europa.eu/legal-content/ES/TXT/?uri=CELEX:32024R1689&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Reglamento (UE) 2024/1689&lt;/a&gt; se titula &lt;em&gt;Precisión, solidez y ciberseguridad&lt;/em&gt; y regula lo que deben lograr los sistemas de IA de alto riesgo y lo que tienen que demostrar los proveedores. Tiene cinco apartados, y cada uno dice algo concreto.&lt;/p&gt;
&lt;p&gt;El apartado 1 da el marco general. Pide que los sistemas de IA de alto riesgo alcancen un &lt;em&gt;nivel adecuado&lt;/em&gt; de precisión, solidez y ciberseguridad y &lt;em&gt;«funcionen de manera uniforme en esos sentidos durante todo su ciclo de vida»&lt;/em&gt;. Merece la pena leer dos veces la cláusula del ciclo de vida. Para el artículo no basta con aprobar un benchmark el primer día: hace falta mantener el rendimiento mientras el sistema siga en servicio. Es una diferencia importante que la mayor parte de la infraestructura de evaluación pasa por alto.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;«Nivel adecuado»&lt;/em&gt; es un criterio cualitativo. Como el artículo no fija umbrales numéricos, es el proveedor quien justifica en la documentación técnica (anexo IV) qué es adecuado para la finalidad prevista del sistema. Lo que se entrega es precisamente esa justificación.&lt;/p&gt;
&lt;p&gt;El apartado 2 parte de que las metodologías para medir la precisión y la solidez aún no existen a gran escala, y encarga a la Comisión que fomente, en cooperación con las autoridades de metrología y de evaluación comparativa, el desarrollo de parámetros de referencia (benchmarks) y metodologías de medición. El propio artículo reconoce el hueco.&lt;/p&gt;
&lt;p&gt;El apartado 3 exige indicar en las instrucciones de uso los niveles reales de precisión y los parámetros pertinentes para medirla, es decir, las métricas. En eso se apoya la transparencia, en saber qué precisión has conseguido y con qué métricas. El artículo 13, apartado 3, letra b), inciso ii), extiende esa obligación a la solidez y la ciberseguridad.&lt;/p&gt;
&lt;p&gt;El apartado 4 se ocupa de la solidez y del comportamiento después del despliegue. Los sistemas de IA de alto riesgo deben ser lo más resistentes posible a los errores, fallos e incoherencias, y contar con medidas técnicas y organizativas para ello. Si un sistema sigue aprendiendo tras el despliegue, el artículo exige que los bucles de retroalimentación (resultados sesgados que influyen en las entradas futuras) se aborden con medidas de reducción de riesgos. Es la cláusula del aprendizaje en línea, recogida de forma explícita en el Reglamento. Entran en este apartado los sistemas de alto riesgo que ajustan el modelo en producción, y se puede defender que también los que usan las interacciones de los usuarios para moldear su comportamiento futuro, tanto si quienes los construyen los consideran «sistemas del Reglamento de IA» como si no.&lt;/p&gt;
&lt;p&gt;El apartado 5 trata de la ciberseguridad, y es ahí donde el artículo entra en detalles técnicos. Los sistemas de IA de alto riesgo deben resistir los intentos no autorizados de alterar su uso, sus resultados de salida o su funcionamiento. El artículo enumera cinco vulnerabilidades específicas de la IA (cuatro clases de ataque más los defectos en el modelo) que el proveedor debe, según corresponda, prevenir, detectar, combatir, resolver y controlar:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Envenenamiento de datos del conjunto de entrenamiento&lt;/li&gt;
&lt;li&gt;Envenenamiento de modelos en los componentes preentrenados&lt;/li&gt;
&lt;li&gt;Ejemplos adversarios / evasión de modelos en la inferencia&lt;/li&gt;
&lt;li&gt;Ataques a la confidencialidad&lt;/li&gt;
&lt;li&gt;Defectos en el modelo&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Son cinco categorías y figuran en el propio texto de la ley.&lt;/p&gt;
&lt;h2 id=&quot;qué-se-prueba-hoy-en-la-práctica&quot;&gt;Qué se prueba hoy en la práctica&lt;/h2&gt;
&lt;p&gt;Si pones los cinco apartados del artículo 15 al lado de las herramientas de evaluación actuales, enseguida ves que las categorías no encajan.&lt;/p&gt;
&lt;p&gt;La mayoría de esas herramientas caben en cuatro grupos, y cada uno cubre una parte distinta de lo que recoge el artículo.&lt;/p&gt;
&lt;p&gt;Los &lt;strong&gt;benchmarks de inyección de prompts y de jailbreak&lt;/strong&gt; comprueban si se puede conseguir que el modelo se salte sus instrucciones de seguridad o que anteponga las instrucciones de un atacante a las del desarrollador. Exploran lo que el artículo llama ejemplos adversarios y evasión de modelos, sobre todo en la frontera del modelo y de forma puntual. No cubren el requisito del ciclo de vida, y solo las baterías más recientes, pensadas para agentes, prueban qué pasa cuando al modelo se le añaden llamadas a herramientas y memoria.&lt;/p&gt;
&lt;p&gt;Los &lt;strong&gt;benchmarks de capacidades&lt;/strong&gt; miden lo que el modelo sabe y puede hacer en distintos dominios, normalmente con preguntas de opción múltiple o de respuesta abierta. Sirven como una pieza más para justificar la precisión que pide el apartado 1, pero por sí solos no bastan como prueba de solidez ni de ciberseguridad. Con saber que el modelo contesta a una pregunta no se llega a lo que exige el artículo.&lt;/p&gt;
&lt;p&gt;El &lt;strong&gt;red teaming de seguridad&lt;/strong&gt; consiste en generar escenarios adversarios, a menudo a mano o casi, y cubre en parte la vertiente de ciberseguridad del apartado 5. Lo que abarca depende de la imaginación del red team, sin un recorrido sistemático por las cinco vulnerabilidades que enumera la ley. Dos organizaciones que hacen red teaming del mismo sistema rara vez llegan a la misma cobertura.&lt;/p&gt;
&lt;p&gt;Los &lt;strong&gt;guardrails en tiempo de ejecución y el filtrado de salidas&lt;/strong&gt; actúan al atender cada petición, fuera de la fase de pruebas. Son una defensa de la fase de despliegue y ayudan a justificar el cumplimiento del apartado 4, pero no son pruebas. Limitan lo que hace el sistema en producción, y el rendimiento a lo largo del ciclo de vida es otra cuestión.&lt;/p&gt;
&lt;p&gt;De este mapa salen tres observaciones.&lt;/p&gt;
&lt;p&gt;La primera es que todas las categorías trabajan sobre todo en la capa del modelo. La cobertura para agentes está en pañales (desde mediados de 2024 han aparecido benchmarks como AgentDojo, AgentHarm y Agent Security Bench), y los pipelines de evaluación habituales todavía no la han incorporado. El &lt;em&gt;nivel adecuado&lt;/em&gt; de precisión, solidez y ciberseguridad del artículo se aplica al sistema de IA de alto riesgo en su conjunto, mientras que las herramientas miden solo un fragmento.&lt;/p&gt;
&lt;p&gt;La segunda, que todas son actividades puntuales. Pasas un benchmark de jailbreak antes de sacar una versión, la publicas y en la siguiente ya no lo relanzas porque las pruebas de regresión salen demasiado caras. Ninguna de estas categorías acredita un funcionamiento uniforme durante todo el ciclo de vida. Lo que dan es una instantánea, y una instantánea no es lo que pide el apartado 1.&lt;/p&gt;
&lt;p&gt;La tercera, que las vulnerabilidades del apartado 5 no están cubiertas por igual. De los ejemplos adversarios y la evasión de modelos se ocupan bien los benchmarks de inyección de prompts. El envenenamiento de datos y el de modelos tienen cada vez más cobertura durante el entrenamiento, aunque menos cuando el sistema ya está desplegado y entra en juego el fine-tuning incremental. Para los ataques a la confidencialidad existen herramientas en la capa del modelo (inferencia de pertenencia y extracción en el Adversarial Robustness Toolbox de IBM, sondas de reproducción de datos de entrenamiento en garak), pero hay poca cobertura en la del sistema, por ejemplo para la exfiltración a través de herramientas o las fugas entre agentes. Los defectos en el modelo, por último, quedan casi sin cubrir con las herramientas actuales.&lt;/p&gt;
&lt;p&gt;El artículo 15 dice qué hay que probar, y el ecosistema solo prueba una parte. Dónde se queda corto lo explico a continuación.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figura interactiva: Lo que el artículo 15 exige a un sistema de IA de alto riesgo y lo que prueban de verdad las herramientas de evaluación actuales. Ningún requisito de la lista está cubierto del todo: o lo está solo en parte o es un hueco que las herramientas no han cerrado.&lt;/em&gt; &lt;a href=&quot;https://cchinchilla.dev/es/posts/eu-ai-act-article-15-testing-gap/&quot;&gt;Ábrela en el artículo para usarla.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;dónde-se-quedan-cortas-las-herramientas&quot;&gt;Dónde se quedan cortas las herramientas&lt;/h2&gt;
&lt;p&gt;Hay otros huecos, pero en estos cuatro es donde más se separa lo que exige el artículo de lo que prueba hoy el ecosistema.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solidez frente a ataques, también en la capa del agente.&lt;/strong&gt; El apartado 5 enumera vectores de ataque que los benchmarks actuales abordan en la capa del modelo, como los ejemplos adversarios o la evasión de modelos en la inferencia y el envenenamiento de datos durante el entrenamiento. Con agentes, esa frontera se amplía. La selección de herramientas, la construcción del plan, la memoria y la comunicación entre sistemas son superficies por las que una entrada adversaria puede moldear el comportamiento sin que al modelo le llegue nunca nada que parezca un ataque. Una salida de herramienta manipulada que influye en el siguiente paso del plan es una entrada adversaria en la capa del agente, y lo mismo vale para una entrada de memoria envenenada que sesga las decisiones futuras. El artículo 15 está redactado pensando en el sistema, pero las herramientas trabajan sobre todo con el modelo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;De la prueba puntual al ciclo de vida.&lt;/strong&gt; El apartado 1 pide un funcionamiento uniforme durante todo el ciclo de vida del sistema. Las herramientas, en cambio, producen instantáneas, como los benchmarks antes de publicar una versión, los relanzamientos ocasionales o los ejercicios de red team ad hoc. Ninguna de ellas acredita el comportamiento a lo largo del ciclo de vida. Una instantánea de hace seis meses no demuestra nada si desde entonces se ha sustituido el modelo, se ha retocado la plantilla del prompt o un servicio aguas abajo ha cambiado la API. El ecosistema aún no ha construido la infraestructura de tests de regresión que permitiría comprobar el apartado 1 en cada versión, frente a cada cambio y mientras el sistema siga en marcha.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recuperación ante fallos.&lt;/strong&gt; El apartado 4 exige resistencia y admite como vía para lograrla la redundancia técnica y los planes de prevención contra fallos. Sin embargo, casi ningún harness de evaluación prueba hoy &lt;em&gt;qué hace el sistema cuando algo falla&lt;/em&gt;, como recuperarse de una llamada fallida a una herramienta, recurrir a un respaldo cuando la confianza del modelo es baja o degradarse de forma controlada cuando un servicio aguas arriba está caído. Son propiedades operativas del sistema y también las que menciona el apartado 4. Las herramientas tratan el éxito como el caso normal y pasan por alto los modos de fallo que le importan al artículo. De un sistema que nunca se ha probado en condiciones de fallo no se puede afirmar que sea resistente.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Coordinación multiagente.&lt;/strong&gt; El requisito de resistencia del apartado 4, que menciona los errores derivados de la interacción con otros sistemas, y el vector de ataques a la confidencialidad que recoge el apartado 5 pesan todavía más cuando el sistema se compone de varios agentes que se pasan salidas entre sí. Si además los agentes siguen aprendiendo, se suma la cláusula de los bucles de retroalimentación del apartado 4. Los fallos en cascada (cuando la salida de un agente se convierte en la entrada de otro sin observabilidad entre ellos) empiezan a preocupar en la literatura académica, y las herramientas actuales rara vez los prueban. Volveré sobre este hueco en otras entradas. Aquí me basta con señalar que las cláusulas del artículo ya cubren el caso multiagente y que las herramientas no se han puesto al día.&lt;/p&gt;
&lt;p&gt;Las normas armonizadas podrían cerrar parte de estos huecos, pero todavía no están listas.&lt;/p&gt;
&lt;h2 id=&quot;normas-armonizadas-aún-en-redacción&quot;&gt;Normas armonizadas aún en redacción&lt;/h2&gt;
&lt;p&gt;Las normas armonizadas desarrollan el detalle técnico de lo que pide el Reglamento. Cumplir una de ellas da presunción de conformidad con el requisito correspondiente. Sin una norma publicada, el proveedor tiene que demostrar el cumplimiento por su cuenta, y eso cuesta más, tarda más y se presta más a discusión.&lt;/p&gt;
&lt;p&gt;En mayo de 2026 el panorama es bastante peor de lo que da a entender el discurso en torno al Reglamento. &lt;strong&gt;Ni una sola norma armonizada del Reglamento de IA tiene publicada su referencia en el Diario Oficial de la Unión Europea (DOUE).&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;El borrador más avanzado de CEN-CENELEC JTC 21, prEN 18286 (Quality Management System), no superó en enero de 2026 la votación de la encuesta pública (Enquiry), y en abril, con los comentarios ya resueltos, estaba &lt;a href=&quot;https://www.etuc.org/sites/default/files/page/file/2026-04/AI%20standardisation%20Inclusiveness_Newsletter14.pdf&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;listo para el voto formal&lt;/a&gt;. Y esa es la norma &lt;em&gt;de procedimiento&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Las normas técnicas pertinentes para el artículo 15 (precisión y solidez, ciberseguridad) van más retrasadas, y ninguna de las dos ha llegado todavía a la encuesta pública.&lt;/p&gt;
&lt;p&gt;Aun con el procedimiento acelerado de JTC 21, la publicación está prevista para el cuarto trimestre de 2026. La referencia en el DOUE es un paso aparte, así que, siendo realistas, las normas que afectan al artículo 15 no llegarán antes de mediados o finales de 2027.&lt;/p&gt;
&lt;p&gt;La Comisión ya se ha dado cuenta. El 19 de noviembre de 2025 propuso el Reglamento ómnibus digital sobre IA, que liga la aplicación de las obligaciones de alto riesgo a que haya normas técnicas disponibles, con fechas tope en diciembre de 2027 y agosto de 2028. Mientras escribo esto, la propuesta está en fase de trílogos. &lt;strong&gt;Hasta que se adopte, la fecha legal de aplicación de las obligaciones de alto riesgo sigue siendo el 2 de agosto de 2026 para los sistemas del anexo III (el 2 de agosto de 2027 para los del anexo I).&lt;/strong&gt; Planifica con la fecha que fija hoy la ley, aunque esperes otra.&lt;/p&gt;
&lt;h2 id=&quot;qué-hacer-mientras-tanto&quot;&gt;Qué hacer mientras tanto&lt;/h2&gt;
&lt;p&gt;Hay cuatro cosas que puedes hacer sin esperar a las normas armonizadas. Lo que sigue da por hecho que eres el proveedor, es decir, el ingeniero que construye un sistema de IA de alto riesgo y no el equipo que lo usa. Los requisitos de la sección 2 (artículos 8 a 15) se aplican a los proveedores. Los responsables del despliegue, por su parte, tienen su propia lista de obligaciones en el artículo 26.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Documenta lo que pruebas, lo que dejas sin probar y por qué.&lt;/strong&gt; El artículo 15, apartado 1, exige un &lt;em&gt;nivel adecuado&lt;/em&gt; de precisión, solidez y ciberseguridad, y ese nivel es cualitativo, depende del contexto y se justifica sistema a sistema. Lo que se entrega es la justificación. El artículo 11, a través del anexo IV, punto 2, letra g), deja claro que los archivos de registro y los informes de las pruebas, fechados y firmados, son parte obligatoria de esa documentación técnica. Escribe qué mides, qué umbral has superado, qué has decidido no probar y con qué razonamiento técnico. Una autoridad de vigilancia del mercado puede exigir esa documentación, y en algunos casos (biometría, productos del anexo I) la evalúa un organismo notificado. Escríbela pensando en que alguien la va a leer línea a línea.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lleva un registro de riesgos continuo, porque una auditoría puntual no basta.&lt;/strong&gt; El artículo 9 exige un sistema de gestión de riesgos que funcione de forma iterativa durante todo el ciclo de vida del sistema, y el artículo 72, una &lt;a href=&quot;https://cchinchilla.dev/es/posts/eu-ai-act-article-72-monitoring-gap/&quot;&gt;vigilancia poscomercialización&lt;/a&gt;. Trátalos como una sola disciplina. El registro de riesgos se actualiza cada vez que cambia algo, sea el modelo, la plantilla del prompt, una herramienta o una dependencia. Una auditoría de riesgos que se hace una sola vez, en la aceptación, no es lo que pide el artículo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Empieza por las cinco vulnerabilidades que enumera el apartado 5.&lt;/strong&gt; Monta primero la cobertura de red team sobre esa lista: envenenamiento de datos, envenenamiento de modelos, ejemplos adversarios o evasión de modelos, ataques a la confidencialidad y defectos en el modelo. Asegura lo que fija la ley antes de pasar a amenazas afines. La lista del artículo marca por dónde empezar y no pone ningún techo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Diversifica las herramientas de pruebas.&lt;/strong&gt; Las normas aún se están redactando, la oferta comercial cambiará y los métodos madurarán. Un harness de pruebas atado a una sola batería de benchmarks o a una sola empresa es un riesgo en sí mismo, y el motivo principal tiene poco que ver con el cumplimiento normativo: en los próximos tres años los cambios van a reordenar el panorama. La infraestructura debería sobrevivir a esa reordenación.&lt;/p&gt;
&lt;p&gt;Ese es el trabajo de ingeniería que de verdad exige el artículo.&lt;/p&gt;
&lt;p&gt;Si ya has empezado con esto, me gustaría saber qué has encontrado. El hueco entre lo que exige el artículo 15 y lo que ofrece hoy el ecosistema es tan grande que ningún equipo le saca mucha ventaja a otro. Los métodos que acabarán inspirando las normas armonizadas se están construyendo ahora mismo en los equipos que no se quedan esperando.&lt;/p&gt;
&lt;div class=&quot;post-footer&quot;&gt;&lt;p&gt;&lt;em&gt;Escrito y traducido con ayuda de IA. Consulta la &lt;a href=&quot;https://cchinchilla.dev/es/ai-policy/&quot;&gt;política de IA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;</content:encoded><dc:creator>Carlos Chinchilla Corbacho</dc:creator><atom:updated>2026-10-06T00:00:00.000Z</atom:updated><category>Reglamento europeo de IA</category><category>Reglamento europeo de IA</category><author>hello@cchinchilla.dev (Carlos Chinchilla Corbacho)</author></item></channel></rss>