English · Español
01 — Modelos de lenguaje de n-grama¶
🇪🇸 Un n-grama es un modelo de lenguaje primitivo: la probabilidad de la siguiente palabra depende solo de las
n-1anteriores. Es estúpido. Para corpora pequeños y locales —como el nuestro de conjugaciones verbales inglesas— es sorprendentemente bueno, y por eso es la baseline correcta para el transformer.
Este archivo deriva el modelo de lenguaje de n-grama, la estrategia de suavizado, la métrica de perplejidad, y los mandos empíricos que afectan al número baseline que Borja compromete.
El planteamiento¶
Un modelo de lenguaje es una distribución de probabilidad sobre secuencias de tokens:
Para nuestro corpus de gramática de verbos, las secuencias son filas cortas como I worked / yo trabajé . (con un separador y un token de fin-de-fila). \(T\) es típicamente 6–12.
Por la regla de la cadena de probabilidad, cualquier distribución conjunta factoriza como:
Un modelo de lenguaje ideal computaría cada \(P(w_t \mid w_1, \ldots, w_{t-1})\) usando el prefijo completo. Eso es inviable: el número de prefijos distintos es exponencial en \(t\), y tenemos un corpus finito.
La asunción de n-grama trunca la dependencia condicional a los últimos \(n - 1\) tokens:
Ahora solo hay \(|V|^{n-1}\) contextos de condicionamiento distintos (con vocabulario \(V\)). Para \(|V| = 64\) (una estimación razonable para nuestro corpus de gramática de verbos tokenizado con BPE tras la Fase 11) y \(n = 5\), eso son \(64^4 = 16{,}7\) millones de contextos. La mayoría nunca aparece en el corpus — véase suavizado abajo.
Estimación por máxima verosimilitud¶
Dado un corpus de secuencias de tokens, la estimación por máxima verosimilitud (MLE) para un n-grama es el ratio empírico de cuentas:
donde \(c(\cdot)\) cuenta ocurrencias de un n-grama (numerador) o (n-1)-grama (denominador) en el corpus de entrenamiento.
Ejemplo trabajado sobre el corpus de gramática de verbos. Supongamos que para \(n = 3\) queremos \(P(\text{works} \mid \text{he, } \emptyset)\) donde \(\emptyset\) es el token separador previo (así la ventana es (<sep>, he, ?)).
count((<sep>, he, works)) = 12 ← visto 12 veces en entrenamiento
count((<sep>, he, work)) = 2 ← visto 2 veces (probablemente en construcciones subjuntivas)
count((<sep>, he, worked))= 8
count((<sep>, he, ...)) = total = 50 ← todas las continuaciones observadas
Entonces \(P_\text{MLE}(\text{works} \mid \langle\text{sep}\rangle, \text{he}) = 12 / 50 = 0{,}24\).
La MLE es una distribución categórica discreta sobre \(V\), parametrizada por cuentas. Implementarla = construir un dict[(context_tokens), Counter[next_token]]. Implementarla eficientemente = un trie o un hash map indexado por una tupla. No necesitamos eficiencia en la Fase 14.
El problema del cero-conteo¶
La MLE asigna probabilidad cero a cualquier n-grama que no apareció en el entrenamiento. Eso es un desastre:
- La perplejidad de una secuencia que contenga cualquier n-grama no visto es \(\infty\). El número baseline carece de sentido.
- El modelo no puede generalizar en absoluto. "I will play" podría aparecer; "I will play tomorrow" podría no (porque tomorrow es raro en nuestro corpus) — el modelo asignaría cero a la fila entera.
Por eso existe el suavizado.
Suavizado add-α (Laplace, \(\alpha = 1\); nosotros usamos \(\alpha = 0{,}01\))¶
Añade un pseudo-conteo \(\alpha\) a cada conteo de n-grama, incluidos los no vistos:
Esto redistribuye masa de probabilidad de eventos vistos a no vistos. El hiperparámetro \(\alpha\) controla cuánto:
- \(\alpha = 0\) → MLE, probabilidades cero, perplejidad infinita en test.
- \(\alpha = 1\) → suavizado de Laplace. Suaviza mucho. A menudo sobre-suaviza para datos pequeños.
- \(\alpha = 0{,}01\) → suavizado modesto. Nuestro default. Funciona bien para nuestro corpus.
Nótese que con \(\alpha = 0{,}01\) y \(|V| = 64\), el desplazamiento del denominador es \(0{,}01 \times 64 = 0{,}64\). Para un contexto con cuenta \(c(\text{context}) = 50\), el denominador pasa a ser \(50{,}64\) en lugar de \(50\) — un desplazamiento apenas perceptible para contextos frecuentes, y un desplazamiento grande para los raros (que es lo que quieres).
Mejor suavizado: un repaso¶
Por completitud — usamos add-\(\alpha\), pero deberías saber que estos existen:
- Suavizado Kneser-Ney (KN). Considera cuán diversos son los contextos de un token. Un token que aparece en muchos contextos (
theen inglés) es más probable que aparezca en un contexto nuevo que un token que siempre aparece en el mismo contexto (FranciscotrasSan). KN es el estado del arte entre los modelos de lenguaje no neurales. No lo implementamos. Mención de un párrafo. - Stupid backoff (Brants et al. 2007). Si el n-grama no se ha visto, recae al (n-1)-grama con una penalización fija. Usado en sistemas prácticos a gran escala antes de los LMs neurales. Implementación de dos líneas. No lo implementamos.
- Good-Turing. Estima la probabilidad de eventos no vistos a partir de la tasa de eventos vistos exactamente una vez. Teóricamente atractivo, prácticamente reemplazado por KN.
Ninguno de estos cerrará la brecha frente a un modelo de lenguaje neural en tareas con estructura semántica. Son sofisticados solo relativos a MLE; son juguetes relativos a un transformer.
Perplejidad: la métrica de evaluación¶
Un modelo de lenguaje asigna una probabilidad a una secuencia. Para comparar modelos, necesitamos un único número. La perplejidad (PPL) es ese número.
Definición: \(\text{PPL}\) es la exponencial del log-verosimilitud negativo medio por token:
donde \(N\) es el número total de tokens en el conjunto de evaluación. (Véase el archivo de teoría de la Fase 5 para la derivación; la glosa es: PPL es el tamaño efectivo de vocabulario sobre el que el modelo está incierto por token.)
Interpretaciones:
- \(\text{PPL} = 1\) significa que el modelo está perfectamente seguro en cada paso.
- \(\text{PPL} = |V|\) significa que el modelo está uniformemente incierto — equivalente a asignar igual probabilidad a cada token del vocabulario.
- \(\text{PPL} = e^{H(w)}\) donde \(H(w)\) es la entropía cruzada por token en nats.
Para nuestro corpus de gramática de verbos con \(|V| \approx 64\):
- Un modelo aleatorio tiene \(\text{PPL} \approx 64\).
- Un 1-grama (unigrama) sobre nuestro corpus tiene \(\text{PPL} \approx 30\) (algunos tokens son mucho más frecuentes: I, you, he, the, separadores).
- Un 3-grama debería aterrizar alrededor de 4–8 — el modelo es esencialmente correcto cuando el contexto es suficiente.
- Un 5-grama debería acercarse a 2–4.
- Un Mini-GPT perfectamente entrenado (Fase 17/18) debería alcanzar < 2 sobre el conjunto held-out, porque la tarea es mayoritariamente determinista.
Estas son predicciones para el laboratorio de la Fase 14; no son garantías. El punto es saber aproximadamente qué números esperar, así que un resultado de \(\text{PPL} = 30\) para el 3-grama es señal de que algo va mal (bug, problema de suavizado, mismatch de evaluación), no progreso.
Leer la relación perplejidad ↔ NLL¶
Algunos equipos reportan log-verosimilitud negativo (NLL) en nats por token, otros reportan perplejidad. Están trivialmente relacionados:
- \(\text{NLL} = -\frac{1}{N} \sum \log P\) (en nats si se usa \(\ln\), en bits si se usa \(\log_2\)).
- \(\text{PPL} = \exp(\text{NLL})\) (con misma base; \(\exp\) si NLL en nats, \(2^{(\cdot)}\) si NLL en bits).
Los dos llevan la misma información. La perplejidad es más interpretable como "tamaño efectivo de vocabulario"; la NLL es más útil cuando quieres sumar pérdidas a nivel de token (es aditiva).
Manejo de los límites de secuencia¶
Concretamente: nuestro corpus tiene filas como I work / yo trabajo separadas por un salto de línea / token de fin-de-fila. El n-grama necesita saber dónde empiezan y acaban las filas.
Dos convenciones:
- Padding con tokens de inicio. Anteponer \(n - 1\) copias de un token
<bos>a cada fila. Un 3-grama ve(<bos>, <bos>, I), (<bos>, I, work), (I, work, /), .... La primera predicción está condicionada a un contexto que existe. - Token de fin de secuencia. Añadir
<eos>a cada fila. El modelo aprende cuándo parar. Cuenta como parte de la pérdida por token.
Para la Fase 14, usa ambos: <bos> antepuesto y <eos> añadido. El laboratorio 01 lo especifica.
Cómputo y almacenamiento¶
Para nuestro corpus (~600 filas, ~6000 tokens en total):
- Un modelo de 3-grama tiene como mucho \(|V|^3 = 64^3 \approx 260\text{k}\) entradas. En la práctica, solo se almacena el subconjunto visto — unas pocas miles de entradas.
- Un modelo de 5-grama tiene como mucho \(|V|^5 \approx 10^9\) entradas. Almacenamos solo el subconjunto visto — siguen siendo unas miles de entradas (la mayoría de contextos nunca aparece).
- El entrenamiento es una pasada por el corpus, tiempo \(O(N \cdot n)\), memoria \(O(\text{n-gramas únicos})\).
- La inferencia (computar perplejidad sobre un conjunto held-out) es tiempo \(O(N)\), una búsqueda por token.
Esto cabe en milisegundos y kilobytes. No hay preocupación de "tiempo de entrenamiento" o "GPU" en la Fase 14.
Lo que un n-grama no puede aprender¶
Tres fallos concretos, demostrados en el laboratorio 01:
- Generalización entre paradigmas. Supongamos que el set de entrenamiento tiene muchos ejemplos de
he works, he played, he listenedpero ninguno dehe walked. Un 3-grama no aprende nada sobrehe walkeda partir de los otros patrones — el trigrama(<sep>, he, walked)tiene cuenta 0 y solo obtiene la masa de suavizado \(\alpha\). Por contraste, un modelo basado en attention con embeddings compartidos podría enrutarhehaciawalkedcorrectamente porquewalkedestá embebido cerca deworked, played, listened. - Cadenas de tiempo-auxiliar. La construcción del futuro
he is going to workes una dependencia de 4 tokens:is → going → to → work. Un 3-grama con ventana 3 ve(is, going, to)pero no(he, is, going). Predecirátocorrectamente trasgoingpero no puede restringir la forma verbal tres tokens después. Un 5-grama arregla esto para la construcción específica pero no generaliza entregoing to / will / 'll. - Alineamiento bilingüe. Dado
I work / yo, el n-grama debería predecirtrabajo. Lo hará — si vio exactamente ese bigrama antes. No aprende la función "traducir el verbo inglés al presente 1ª-singular español". Solo memoriza coocurrencias. Un corpus mayor cerraría la brecha; el nuestro no.
Estos fallos son la semilla de attention.
En qué es un n-grama genial¶
Para nuestro corpus, un n-grama es casi óptimo en:
- Pronombre → forma de present-simple.
I work, you work, he ___→worksporque(<sep>, he, works)es el trigrama más frecuente tras(<sep>, he, *). - Pronombre → forma de pasado.
I worked, you worked, he ___→workedanálogamente. - Colocación del artículo en español.
yo ___→trabajo(o cualquier forma verbal alineada con pronombre que siga ayo).
Si tu tarea es exactamente pronombre → completado de forma verbal, un 5-grama con \(\alpha = 0{,}01\) sobre nuestro corpus estará dentro del 10% del modelo óptimo. La victoria del transformer tiene que venir de generalización entre paradigmas, no de batir al n-grama en la tarea central.
Esta realización es el puente a la Fase 15: necesitamos un modelo cuyos internos hagan de la generalización el modo natural de operación, no la memorización.
Un ejercicio antes del laboratorio¶
Computa a mano: dadas las cuentas de entrenamiento
Para \(n = 3\), \(\alpha = 0{,}01\), \(|V| = 64\), computa \(P_\alpha(\text{work} \mid \langle\text{bos}\rangle, I)\).
Si puedes computar esto mentalmente, entiendes el modelo. El resto de la Fase 14 es plomería de datos.
Lo que esta fase NO cubre¶
- Implementar el suavizado Kneser-Ney. Un párrafo arriba es el tratamiento entero.
- Modelos de lenguaje bayesianos jerárquicos, modelos de lenguaje neurales previos a GPT, RNN-LMs como categoría aparte. Son interesantes pero no están en el camino crítico.
- El imperio de los n-gramas de los 90 (traducción, IR, retrieval). Tocamos RAG en la Fase 29; los usos no-LM de los n-gramas están fuera del alcance.
- N-gramas de orden variable ("KN interpolado"). Usamos \(n\) fijo por modelo.
- Ajuste de hiperparámetros del suavizado. Fija \(\alpha = 0{,}01\).
Siguiente: theory/02-rnn-recurrence.md.