«El niño juega en el jardín.» Seis palabras y un punto. El tokenizador con el que se entrenó GPT-2 en 2019 necesitaba doce tokens para escribirla, y a su traducción inglesa, «The boy plays in the garden.», le bastaban siete. En 2022, con el tokenizador que usan GPT-3.5 y GPT-4, el español bajó a once. En 2024, con el de GPT-4o, bajó a siete: exactamente los mismos que el inglés, y esta vez con las palabras enteras.
La misma frase y su traducción, cortadas por tres generaciones del mismo tokenizador. El punto dentro de una caja marca el espacio que ese token lleva pegado delante. El inglés no se mueve: siete tokens y las mismas siete piezas las tres veces.
El inglés se quedó quieto. Siete tokens en las tres generaciones, y las tres veces cortado por los mismos sitios. Lo que se movió fue el español, y no porque nadie lo arreglara: el idioma no entra en la ecuación. Lo que cambió fue el tamaño de una lista y quién estaba dentro de ella.
Este artículo va sobre cómo se construye esa lista. Lo recorro en el orden en que ocurrió, porque en ese orden se entiende: cómo se cortaba el texto antes de que existiera nada de esto y por qué esa forma de cortar dejó de servir, de dónde salió BPE (que es un algoritmo de compresión de 1994 al que le cambiaron el trabajo), en qué se diferencian de verdad los tres métodos que se usan hoy, por qué debajo de todos ellos acabó habiendo un suelo hecho de bytes, y qué te sigue costando la elección cuando el texto que escribes no es inglés.
Ninguna de esas decisiones se tomó pensando en el español.
La era de la palabra, y por qué terminó
Un modelo de lenguaje no lee texto, lee números. Antes de que intervenga ninguna red, alguien tiene que partir la cadena de caracteres en unidades y darle a cada unidad un índice. A esas unidades se las llama tokens, a la lista cerrada de las que el modelo reconoce se la llama vocabulario, y al programa que decide dónde cortar, tokenizador.
El índice por sí solo no dice nada: que «casa» sea la entrada 412 y «perro» la 413 no las hace parecidas, igual que dos números de teléfono consecutivos no son de dos vecinos. Lo que el modelo aprende es un vector de números por cada entrada del vocabulario, su embedding, con el mismo para todas: unos cientos en los modelos pequeños, unos miles en los grandes. Apilados, esos vectores forman una matriz de que se entrena junto con el resto de la red, y al final del modelo hay otra matriz del mismo tamaño en la capa que decide qué token viene después.
Por eso el vocabulario se fija antes de entrenar y ya no se toca: cada entrada cuesta dos filas de números, y el número de filas hay que saberlo antes de empezar. Es finito, cerrado y caro. El idioma no es ninguna de las tres cosas, y todo lo que viene a continuación sale de ese choque.
Durante décadas, decidir dónde cortar fue escribir reglas. Un tokenizador de los de entonces era una lista de casos: corta por los espacios, separa la puntuación final de la palabra pero no el punto de «EE. UU.», deja juntas las comillas de apertura y cierre, decide qué hacer con los guiones. Los dos más usados en investigación, el de Penn Treebank y el de Moses, eran exactamente eso, y con ellos venía la letra pequeña: las reglas son por idioma. El que sabe que «don't» son dos palabras no sabe nada de «dámelo», y ninguno de los dos sirve para el chino, que no separa las palabras con espacios.
El problema de fondo no eran las reglas, que se pueden escribir. Era el tamaño de la lista que producían.
Para verlo con números en lugar de con adjetivos, junté tres libros en español de dominio público, todos de Project Gutenberg y todos descargables en texto plano: Don Quijote, de Cervantes; La Regenta, de Leopoldo Alas; y Amar es vencer, de Pauline Caro. Son 746 368 palabras en total, contando como palabra cualquier secuencia de símbolos separados por espacio y convirtiéndolo todo a minúsculas.
El vocabulario que sale de ahí tiene 38 792 palabras distintas, y su reparto es el de siempre en cualquier idioma: la frecuencia de una palabra es inversamente proporcional a su puesto en el ranking,
con un que en este corpus vale . Diez palabras («de», «que», «y», «la», «a»…) se llevan una porción enorme del texto, y detrás viene una cola que no se acaba nunca. Cuantifícala: de esas 38 792 palabras distintas, 17 536 aparecen exactamente una vez. Casi la mitad del vocabulario existe para ser usada una sola vez en 746 368 palabras.
Eso ya es incómodo, porque cada entrada del vocabulario cuesta una fila de números para representarla y otra en la capa de salida, y ahí estás pagando dos filas por una aparición. Pero lo grave es lo otro: la lista no deja de crecer. Leyendo la mitad del corpus habías visto 22 643 palabras distintas; leyendo la otra mitad aparecen 16 149 más. El texto se duplica y el vocabulario crece un 71 %.
Las mismas 746 368 palabras contadas de dos maneras, en ejes logarítmicos. Como vocabulario de palabras, la cuenta sube sin doblarse por ninguna parte. Como alfabeto, se planta en las primeras mil palabras y ya no se mueve: las once letras que aparecen después son tildes raras y caracteres de otros idiomas.
Esa curva tiene nombre y forma cerrada. Se la conoce como ley de Heaps, y dice que el vocabulario crece como una potencia del texto leído:
Aquí es cuántas palabras llevas leídas (746 368 al final del corpus), cuántas distintas han aparecido hasta ese punto, y y son dos constantes que se ajustan al corpus: fija la altura de la curva y , que es el que importa, su pendiente en los ejes logarítmicos de la figura. En cualquier idioma cae entre y ; en este corpus vale , con . Que sea menor que significa que cada palabra nueva es un poco más difícil de encontrar que la anterior; que sea mayor que significa que nunca dejarás de encontrarlas. No hay ningún tamaño de corpus a partir del cual el vocabulario esté completo, y no lo hay porque el español no es una lista: es una máquina de generar palabras, con su conjugación, sus diminutivos y su capacidad de pegarle dos clíticos a un imperativo.
Lo que hicieron los sistemas de traducción automática de la primera generación neuronal fue lo único que podían hacer: cortar la lista por donde se pudiera pagar. El corte no era estético, era aritmético. La capa que decide la siguiente palabra calcula una puntuación por cada entrada del vocabulario, lo que cuesta del orden de operaciones en cada paso de cada frase, con el mismo de los embeddings. Con en cientos de miles, esa capa sola se come el presupuesto. De ahí los vocabularios de 30 000 a 80 000 palabras de aquellos sistemas, y de ahí que hubiera trabajos dedicados solo a poder usar un vocabulario más grande.
¿Y las palabras que quedaban fuera? Se sustituían por un símbolo único, UNK, y se seguía adelante.
Mide lo que eso significa en el corpus de arriba: entrena el vocabulario con el 90 % del texto y
lee el 10 % restante. El 4,8 % de las palabras que lees no está en el vocabulario, y no son
rarezas, es una de cada veinte palabras corrientes. Todas ellas llegan al modelo como el mismo
símbolo, indistinguibles entre sí: el nombre propio, el tecnicismo y la errata comparten
representación. Y como en traducción hay que producir texto, UNK también salía por el otro lado,
así que hubo que
inventar parches que anotaran de dónde venía cada UNK para
copiar la palabra original o buscarla en un diccionario después.
Un tokenizador que necesita un sistema de parches para las palabras que no supo representar está diciendo, en voz baja, que la unidad elegida es la equivocada.
La fuga hacia el carácter, y su precio
Si el problema es que las palabras se acaban antes que el idioma, hay una salida obvia: no uses palabras. Usa letras. El vocabulario pasa a ser el alfabeto, unas decenas de símbolos, y en la figura de arriba es la línea plana: con mil palabras leídas ya has visto casi todas las letras que el español usa, y leer un millón más no añade prácticamente ninguna.
Todo lo que dolía se arregla de golpe. No hay UNK posible, porque cualquier palabra que llegue se
escribe con letras que ya están en el vocabulario. La capa de salida deja de ser el cuello de
botella, porque decidir entre cuarenta símbolos es gratis comparado con decidir entre cincuenta mil.
Y las palabras emparentadas dejan de ser desconocidas la una para la otra: «niño» y «niña»
comparten tres de sus cuatro letras, cosa que un vocabulario de palabras no puede ni enunciar. Hubo
modelos de carácter serios, y funcionaban.
El precio se ve en cuanto cuentas los pasos. Llama al número de caracteres de un texto, al número de tokens en que lo parte un tokenizador y a la longitud media de esos tokens, en caracteres. Los tokens son trozos del propio texto, así que sus longitudes suman el texto entero y las tres cantidades quedan atadas:
El texto fija , de modo que y no se mueven por separado: cuanto más cortas las piezas, más larga la secuencia, y no hay forma de tener las dos cosas. Cortando por caracteres, y por tanto . En el corpus de arriba eso significa que donde el tokenizador de palabras veía 746 368 unidades, el de caracteres ve 4 194 315: casi seis veces más pasos para el mismo texto.
Y el coste no se multiplica por seis, sino por bastante más, porque las partes que comparan cada posición con todas las demás (la atención, que es de lo que están hechos los modelos actuales) crecen como :
Treinta y dos veces el trabajo, por el mismo texto y para aprender de paso cómo se deletrea. Hay un segundo coste, menos visible y peor: la distancia entre dos palabras relacionadas de una frase se multiplica también por seis, así que el modelo tiene que sostener dependencias mucho más largas para descubrir lo mismo.
Con las dos salidas obvias descartadas, el compromiso se ve entero:
| corte | |||
|---|---|---|---|
| caracteres | decenas, y se satura | ||
| subpalabras | – | 30 000–200 000, elegido de antemano | |
| palabras | decenas de miles, y creciendo |
La fila del medio es la que se impuso, y su celda de la derecha dice en qué consiste: en lugar de dejar que el tamaño del vocabulario sea una consecuencia de cómo cortas, se fija por adelantado y se deja que un algoritmo decida qué piezas lo componen. Las palabras frecuentes acabarán siendo una pieza entera y las raras se escribirán con varias, sin que nadie tenga que decidir cuáles son unas y cuáles otras.
Así que la fuga hacia el carácter funcionó y nadie se quedó a vivir allí. La respuesta acabó siendo que la unidad no la eligiera nadie.
BPE: un algoritmo de compresión reutilizado
El algoritmo que acabó resolviendo esto se llama byte-pair encoding, BPE de aquí en adelante, y el nombre dice exactamente lo que hace: codificar pares de bytes. Se publicó en 1994 en una revista para programadores de C, y lo que quería era comprimir ficheros. Busca el par de bytes que más se repite, sustitúyelo en todo el fichero por un byte que no aparezca en él, y repite. Cada una de esas sustituciones hay que anotarla para poder deshacerla después, y donde se anotan es la tabla de sustituciones: una lista de líneas que dicen «el byte 0xFE significa el par AB».
Veintidós años después, un artículo sobre traducción automática sacó ese algoritmo de la compresión y lo puso a hacer otra cosa. No cambiaron el algoritmo. Cambiaron cuándo se para.
La diferencia está en qué haces con esa tabla. Al comprimir es un mal necesario que viaja con el fichero, porque sin ella nadie puede recuperar el original. Al tokenizar no se recupera nada: paras a las 30 000 sustituciones porque ese es el vocabulario que querías, y la tabla es el tokenizador. Todo lo que un tokenizador BPE sabe cabe en esa lista ordenada de pares.
El entrenamiento
El corpus se cuenta primero por palabras, con su frecuencia, y cada palabra se parte en caracteres
(en caracteres, sí, pese al nombre del algoritmo; los bytes de verdad vuelven más adelante, y no por
casualidad). Al final de cada palabra se pega una marca de fin, </w>, que impide que una pieza
cruce de una palabra a la siguiente y, sobre todo, deja escrito dónde había un espacio. Eso segundo
es un requisito, no un adorno: concatenar los tokens tiene que devolver el texto de partida, espacios
incluidos, porque un tokenizador que pierde información por el camino ya no divide el texto, lo
altera, y lo que descarta el modelo no volverá a verlo.
Sea la frecuencia de la palabra en el corpus y el número de veces que las piezas y aparecen adyacentes dentro de . Sumando sobre todas las palabras, y pesando cada una por las veces que sale, queda cuántas veces aparece ese par en el corpus entero:
En cada vuelta se fusiona el par con el recuento más alto,
y se repite tantas veces como piezas quieras añadir al vocabulario. Eso es todo. No hay gramática, no hay morfología y no hay ninguna noción de palabra más allá de la marca de fin.
from collections import Counter
CORPUS = """
el niño juega en el jardín la niña juega en el jardín
el niño aprende programación la niña aprende programación
la programación se aprende programando y programando se aprende
el profesor enseña programación la profesora enseña programación
enseñar no es lo mismo que aprender el niño enseña a la niña
la niña enseña al niño programación y el niño aprende
"""
def pares(vocab):
"""Cuenta cada par de piezas adyacentes, pesado por la frecuencia de la palabra."""
cuenta = Counter()
for piezas, f in vocab.items():
for par in zip(piezas, piezas[1:]):
cuenta[par] += f
return cuenta
def fusiona(vocab, par):
"""Sustituye todas las apariciones de `par` por la pieza que resulta de pegarlo."""
a, b = par
nuevo = {}
for piezas, f in vocab.items():
salida, i = [], 0
while i < len(piezas):
if i < len(piezas) - 1 and piezas[i] == a and piezas[i + 1] == b:
salida.append(a + b)
i += 2
else:
salida.append(piezas[i])
i += 1
nuevo[tuple(salida)] = f
return nuevo
def entrena(corpus, n_fusiones):
# Cada palabra empieza partida en caracteres. `</w>` marca dónde termina.
vocab = {tuple(p) + ("</w>",): f for p, f in Counter(corpus.split()).items()}
fusiones = []
for _ in range(n_fusiones):
cuenta = pares(vocab)
if not cuenta:
break
par = max(cuenta.items(), key=lambda kv: kv[1])[0] # empate: gana el primero visto
fusiones.append((par, cuenta[par]))
vocab = fusiona(vocab, par)
return fusiones
for i, (par, n) in enumerate(entrena(CORPUS, 15), 1):
print(f"{i:2}. {par[0]!r} + {par[1]!r} -> {(par[0] + par[1])!r} (x{n})")Sobre ese corpus de seis líneas, las quince primeras fusiones son estas. El número entre paréntesis de cada línea es , el recuento que ganó esa vuelta:
1. 'a' + '</w>' -> 'a</w>' (x18)
2. 'p' + 'r' -> 'pr' (x16)
3. 'e' + 'n' -> 'en' (x13)
4. 'o' + '</w>' -> 'o</w>' (x10)
5. 'pr' + 'o' -> 'pro' (x10)
6. 'n' + 'i' -> 'ni' (x9)
7. 'ni' + 'ñ' -> 'niñ' (x9)
8. 'l' + '</w>' -> 'l</w>' (x8)
9. 'n' + '</w>' -> 'n</w>' (x8)
10. 'e' + '</w>' -> 'e</w>' (x8)
11. 'pro' + 'g' -> 'prog' (x8)
12. 'prog' + 'r' -> 'progr' (x8)
13. 'progr' + 'a' -> 'progra' (x8)
14. 'progra' + 'm' -> 'program' (x8)
15. 'program' + 'a' -> 'programa' (x8)El corpus lo he generado con IA para que las fusiones se vean, y se nota: seis líneas de español con muchos niños y mucha programación. Un corpus real tiene miles de millones de palabras y las primeras fusiones son igual de aburridas que estas.
Merece la pena leer esa lista despacio, porque contiene tres cosas que no son obvias.
La primera fusión del corpus es una a pegada a un final de palabra. El algoritmo no sabe que en
español la a final marca el femenino en «niña» y «profesora», ni que el infinitivo termina en r,
ni qué es un morfema. Ha contado. Resulta que en español contar y saber gramática se parecen mucho,
porque las terminaciones son justamente lo que más se repite, y por eso las piezas de BPE se acaban
pareciendo a morfemas sin que nadie se lo pida.
La segunda es la escalera de la 11 a la 15. La pieza pro se come una letra por vuelta hasta llegar
a programa, y cada peldaño es una entrada más del vocabulario. BPE no reconoce la raíz «program»:
la construye, letra a letra, porque es lo que más se repite en un corpus donde todo el mundo
programa. Con una raíz frecuente eso es una ganga, porque después «programación», «programando» y
«programa» comparten la primera pieza. Con una rara es puro desperdicio.
La tercera es que el parecido con la morfología es una coincidencia estadística, y falla en cuanto
la estadística deja de acompañar. El tokenizador de GPT-4 parte «dámelo» en d, ám, elo, que no
es la segmentación de nadie: la morfológica sería «dá me lo», con el imperativo y sus dos clíticos.
Las piezas de BPE se parecen a morfemas cuando los morfemas son frecuentes, y a nada reconocible
cuando no lo son.
Codificar es repetir la lista
Con la lista aprendida, tokenizar una palabra nueva es aplicar las fusiones en el orden en que se aprendieron. Ni una más, ni en otro orden.
def codifica(palabra, fusiones):
"""Aplicar la lista, en orden. Esto es todo lo que hace un tokenizador BPE."""
vocab = {tuple(palabra) + ("</w>",): 1}
for par, _ in fusiones:
vocab = fusiona(vocab, par)
return next(iter(vocab))«programación» bajando por la lista de fusiones. A la izquierda, el número de la fusión que dispara cada fila y el par que pega; en verde, la pieza que acaba de formarse. Trece piezas al empezar, cinco al terminar.
Y el resultado, que es lo que el modelo acaba viendo:
>>> codifica("programación", fusiones)
('programa', 'c', 'i', 'ó', 'n</w>')Cinco piezas, y solo la primera significa algo por sí sola. Detrás quedan las letras sueltas de la
terminación, sin fusionar, porque quince fusiones dan para lo que dan: la lista se quedó sin
presupuesto antes de llegar a ellas. Sigue entrenando y eso cambia. Con nueve fusiones más, la
número 24 pega programació con n</w> y la palabra entera pasa a ser un solo token, de cinco
a uno.
Ahí está, en miniatura, el compromiso del que va todo lo demás. Cada fusión que añades acorta las secuencias y cuesta una entrada del vocabulario, con sus dos filas de números. Nadie escribe la regla de dónde parar: se elige un número de fusiones, y el algoritmo decide con él qué palabras del idioma caben enteras y cuáles llegan al modelo hechas trozos.
Que el orden mande tiene dos consecuencias que conviene ver juntas. La buena es el determinismo, que es la otra condición que un tokenizador tiene que cumplir: la misma palabra produce siempre el mismo corte, porque la lista no cambia y se recorre siempre igual. Si el corte cambiara de un día para otro, lo que el modelo aprendió dejaría de corresponderse con lo que recibe. La incómoda es que la lista es el tokenizador hasta en sus detalles más tontos. En mi código, cuando dos pares empatan a frecuencia gana el que se vio primero, y esa línea es tan parte del tokenizador como el corpus: dos implementaciones que resuelvan los empates de forma distinta, entrenadas sobre el mismo texto, producen vocabularios distintos y cortan distinto para siempre.
La palabra que el corpus no contenía
Queda la pregunta que el tokenizador de palabras no sabía contestar: qué hacer cuando llega una palabra que el corpus no contenía. Toma «El niño programa una criptomoneda», una frase corriente, y córtala con un tokenizador de palabras entrenado sobre el corpus de arriba: tres de sus cinco palabras no están en el vocabulario. «criptomoneda» es la peor de las tres, porque no es una rareza ni un nombre propio, es una palabra normal que ese corpus no llegó a contener, y sobre ella un tokenizador de palabras no tiene absolutamente nada que ofrecer.
Con la misma lista de quince fusiones, y al lado dos palabras que el corpus sí traía:
niño -> niñ o</w>
enseñar -> en s e ñ a r </w>
criptomoneda -> c r i p t o m o n e d a</w>No hay UNK por ninguna parte. «criptomoneda» no aparecía en el corpus, ninguna de sus fusiones se
aprendió, y aun así el tokenizador la escribe entera, letra a letra, porque las letras sueltas
siempre están en el vocabulario. Esa es la propiedad que hizo que BPE se comiera el campo: el
vocabulario es cerrado y finito, y sin embargo cualquier cadena tiene representación.
Y ahora el precio, que en 2016 no parecía grave y hoy es la mitad del problema. «criptomoneda» ha costado doce tokens en lugar de dos o tres. La palabra no se ha perdido, se ha encarecido, y encima llega al modelo troceada en piezas que no significan nada por separado. La pregunta ya no es si una palabra está en . Es cuánto te cobra el tokenizador por escribirla, y esa pregunta tiene una respuesta distinta para cada idioma.
Tres formas de elegir las piezas
BPE es el que todo el mundo nombra y no es el único que se usa. Hay tres algoritmos en producción, y la diferencia entre ellos cabe en una frase: cambia el criterio con el que se decide qué pieza entra en el vocabulario. Uno de los tres cambia además la dirección desde la que se llega.
WordPiece: fusionar lo que se busca, no lo que abunda
WordPiece corre el mismo bucle voraz que BPE, empezando por caracteres y fusionando de a un par por vuelta, y cambia solo la puntuación con la que elige el par. Donde BPE se queda con el más alto, WordPiece divide ese mismo recuento por lo mucho o poco que cada pieza aparezca por su cuenta:
donde es el número de veces que la pieza aparece en el corpus, sola o acompañada, y otro tanto para .
Tomando logaritmos, la puntuación es , que es la información mutua puntual del par salvo una constante que es la misma para todos los pares y por tanto no cambia cuál gana. Y esa cantidad mide otra cosa: no cuántas veces aparecen juntos, sino cuántas veces más de lo que cabría esperar si cada uno fuera por su lado.
La diferencia se ve en la traza de la sección anterior. La tercera fusión pegaba e con n porque
el par salía trece veces, pero e y n son dos de las letras más frecuentes del español: que
coincidan mucho no dice nada de ellas. WordPiece divide precisamente por eso, y prefiere pares cuyas
piezas, por separado, son más raras de lo que su unión sugiere. El vocabulario que sale tiene menos
relleno y más piezas que corresponden a algo.
Viene de un artículo de 2012 sobre búsqueda por voz en japonés y
coreano, dos idiomas donde ni
siquiera está claro dónde termina una palabra, y se hizo famoso seis años después por ser el
tokenizador de BERT. Marca las continuaciones con ##: en
c ##ript ##omo ##ned ##a, el ## dice que esa pieza no empieza palabra, y quitándolo se
reconstruye el texto.
Hay una segunda diferencia de la que casi nunca se habla. WordPiece no guarda lista de fusiones. BPE codifica repitiendo la suya en orden; WordPiece, que ya no la tiene, codifica de otra manera: en cada posición coge la pieza más larga del vocabulario que encaje ahí y sigue desde donde termine. Dos algoritmos de entrenamiento casi idénticos que producen dos codificadores distintos.
Unigram: empezar por arriba y podar
El tercero invierte la dirección entera. En lugar de crecer desde el alfabeto, arranca con un vocabulario enorme de candidatas (todas las subcadenas razonablemente frecuentes del corpus) y va quitando las que menos falta hacen, hasta bajar al tamaño fijado.
Decidir cuáles hacen menos falta obliga a medirlo, y medirlo obliga a tener un modelo. Ahí está la diferencia de fondo con los otros dos. Una segmentación de una frase tiene probabilidad
es decir, cada pieza tiene la suya y se supone que no dependen unas de otras: de ahí lo de unigram. Con eso, la mejor segmentación de una frase es la de mayor .
Encontrarla es más barato de lo que parece. Una cadena de caracteres se puede partir en trozos
contiguos de maneras, así que enumerarlas queda descartado para cualquier frase real, pero
no hace falta enumerar nada. Tomando logaritmos, es una suma de términos que no
se afectan entre sí, y entonces la mejor forma de llegar cortando hasta la posición depende solo
de la mejor forma de llegar a cada posición anterior , más la puntuación de la pieza que va de
a . Se recorre la frase de izquierda a derecha guardando un número por posición, la mejor
puntuación acumulada hasta ahí, y al terminar se deshace el camino hacia atrás. Ese recorrido
tiene nombre: es el algoritmo de Viterbi, el mismo que se usa para decodificar modelos ocultos
de Markov, y cambia el coste de exponencial a , con la longitud de la pieza más
larga del vocabulario. Es exactamente el código que aparece unos párrafos más abajo, donde se
llama maxlen.
El entrenamiento, por su parte, pasa a tener algo que optimizar: la verosimilitud del corpus, sumando sobre todas las segmentaciones posibles de cada frase ,
El algoritmo alterna dos pasos. El primero ajusta las , y tiene una dificultad de huevo y gallina: para saber cuánto se usa cada pieza habría que saber por dónde se corta cada frase, y para cortar las frases hacen falta las . Se rompe con esperanza-maximización (expectation-maximization, EM), que da vueltas entre las dos mitades: con las de la vuelta anterior calcula cuántas veces se espera que aparezca cada pieza, contando todas las segmentaciones posibles pesadas por su probabilidad, y fija las nuevas proporcionales a esos recuentos esperados. El segundo paso calcula, pieza a pieza, cuánto caería si esa pieza no existiera, y tira las que menos cuestan. Vuelta a empezar, hasta el tamaño fijado.
Sale de un artículo de 2018 cuyo tema principal es, de hecho, otro. Como el modelo da probabilidades, no hay una única segmentación buena sino un ranking de ellas, y se puede muestrear: la misma frase entra hoy cortada de una manera y mañana de otra, lo que funciona como aumento de datos y hace al modelo menos frágil ante un corte raro. Ni BPE ni WordPiece pueden hacer eso, porque para ellos solo existe una respuesta.
El fichero lo dice
Nada de esto hay que creérselo. El tokenizador de cualquier modelo abierto es un fichero que se descarga y se abre, y lo que hay dentro delata el algoritmo:
| algoritmo | qué trae el fichero | ejemplo |
|---|---|---|
| BPE | vocabulario y una lista ordenada de fusiones | GPT-2: 50 257 entradas, 50 000 fusiones |
| WordPiece | vocabulario suelto, sin fusiones, con el prefijo ## declarado | BERT: 30 522 entradas |
| Unigram | vocabulario donde cada pieza lleva su | XLM-R: 250 002 entradas puntuadas |
La aritmética de la primera fila cuadra sola y merece la pena mirarla: . Doscientas cincuenta y seis piezas de partida, cincuenta mil fusiones aprendidas y un token especial de fin de texto. De dónde salen esas 256 es justo el asunto de la sección siguiente.
Las puntuaciones de la tercera fila son literales: en el vocabulario de XLM-R, ▁de vale ,
ción vale y ▁niño vale , donde ▁ marca que la pieza empieza palabra. Con
esos números y quince líneas de Python se reproduce el tokenizador entero.
def segmenta(texto, piezas, maxlen=16):
# Viterbi: de todas las formas de partir el texto en piezas del vocabulario,
# la que maximiza la suma de log p(x). Coste O(n * maxlen), no exponencial.
s = "▁" + texto.replace(" ", "▁")
n = len(s)
mejor = [-math.inf] * (n + 1) # mejor log-probabilidad hasta la posición j
corte = [0] * (n + 1) # de dónde venía ese óptimo, para reconstruirlo
mejor[0] = 0.0
for j in range(1, n + 1):
for i in range(max(0, j - maxlen), j):
p = piezas.get(s[i:j]) # None si esa subcadena no es una pieza
if p is not None and mejor[i] + p > mejor[j]:
mejor[j], corte[j] = mejor[i] + p, i
salida, j = [], n
while j > 0:
salida.append(s[corte[j]:j])
j = corte[j]
return salida[::-1]Cargando en piezas el vocabulario publicado de XLM-R, esta función devuelve exactamente lo mismo
que el tokenizador oficial, token a token. No se parece al algoritmo: es el algoritmo.
Los tres sobre las mismas palabras
Con los tres tokenizadores reales delante, cortando las mismas seis palabras españolas:
| palabra | BPE (GPT-4o) | WordPiece (mBERT) | Unigram (XLM-R) |
|---|---|---|---|
| niño | ni ño | niño | ▁niño |
| jardín | j ard ín | jardín | ▁jardí n |
| enseñar | ense ñar | ens ##eña ##r | ▁enseñar |
| programación | program ación | programación | ▁programación |
| dámelo | d ám elo | dá ##mel ##o | ▁dám elo |
| criptomoneda | cript omon eda | c ##ript ##omo ##ned ##a | ▁cripto mone da |
La comparación no es limpia y conviene decirlo: los tres vocabularios tienen tamaños muy distintos (200 019, 119 547 y 250 002) y se entrenaron sobre corpus distintos, así que lo que se ve no es solo el algoritmo. Aun así, dos cosas se leen sin esfuerzo.
Ninguno coincide con otro en ninguna de las palabras que parte en trozos. Y ninguno de los tres da
con la segmentación que daría un hablante de «dámelo», que es «dá me lo», el imperativo con sus dos
clíticos: sale d ám elo, dá ##mel ##o y ▁dám elo. Tres algoritmos, tres cortes, y los tres a
la misma distancia de la morfología. Las piezas se parecen a morfemas cuando los morfemas son
frecuentes, y a nada reconocible cuando no lo son; la sección anterior ya lo avisaba con un solo
tokenizador, y aquí se ve que no era culpa de aquel.
SentencePiece no es un cuarto algoritmo
Es la confusión más repetida del tema, y viene de que el nombre aparece en la ficha técnica de media docena de modelos. SentencePiece es una implementación: una biblioteca que entrena y aplica BPE o Unigram, no una tercera manera de elegir piezas. Decir que un modelo «usa SentencePiece» informa tan poco como decir que un programa «usa gcc».
Lo que sí aportó, y es lo que hizo que se lo llevara todo, es de otro orden. Los tokenizadores
anteriores daban por hecho que el texto llegaba ya partido en palabras por un programa aparte, con
sus reglas por idioma, las mismas de la primera sección. SentencePiece se salta ese paso: trata la
frase como un flujo de caracteres, espacios incluidos, y convierte el espacio en un símbolo más, el
▁ de la tabla de arriba. Con eso no hay reglas por idioma que mantener, el japonés y el español
entran por la misma puerta, y la reconstrucción vuelve a ser exacta sin convenios que recordar: pega
las piezas, cambia ▁ por espacio y tienes el texto de partida.
Aun así, los tres algoritmos comparten un supuesto que no hemos tocado todavía. Los tres construyen sus piezas a partir de caracteres, y qué caracteres existen se decide mirando el corpus. Si mañana llega un texto con uno que aquel corpus no tenía (un emoji, un ideograma, una letra de un alfabeto que nadie contempló), el vocabulario se queda corto otra vez. Es el mismo agujero de antes, un piso más abajo.
El suelo de bytes
Unicode tiene asignados del orden de 150 000 caracteres, repartidos en más de 150 sistemas de escritura, y cada año se añaden más. Ningún corpus los contiene todos. Así que el alfabeto con el que arranca cualquiera de los tres algoritmos, por grande que sea el corpus, es siempre una lista incompleta, y el problema de la primera sección reaparece un piso más abajo: llega un ideograma, un emoji inventado el año pasado o una letra de un alfabeto que nadie miró, y el tokenizador no tiene con qué escribirlo.
La solución que se impuso, y que llegó con GPT-2 en 2019, es de una simplicidad incómoda: dejar de construir las piezas a partir de caracteres y construirlas a partir de bytes. Cualquier texto, en cualquier escritura, se guarda en el disco como una secuencia de bytes en UTF-8, y un byte solo puede tomar 256 valores. Si el alfabeto de partida son esos 256, no existe el texto que no se pueda escribir con el vocabulario. Ninguno. Nunca.
Ahí es donde encaja la aritmética de la sección anterior: . Los 256
bytes, las 50 000 fusiones aprendidas encima de ellos y un token de fin de texto. El vocabulario de
GPT-2 no tiene token de desconocido, y no por olvido: no hace falta, porque no hay nada que
pueda quedar fuera. El UNK que había obligado a inventar diccionarios de reparche desaparece del
problema por construcción.
Lo que cuesta bajar al byte
Que el suelo sean bytes tiene una consecuencia rara: un token puede ser un trozo de carácter. No una letra incompleta en sentido figurado, sino una secuencia de bytes que por sí sola no es nada.
'🙂' sus bytes UTF-8 son b'\xf0\x9f\x99\x82'
gpt2 2 tokens: [b'\xf0\x9f', b'\x99\x82']
cl100k_base 2 tokens: [b'\xf0\x9f', b'\x99\x82']
o200k_base 1 token: [b'\xf0\x9f\x99\x82']
'漢字' sus bytes UTF-8 son b'\xe6\xbc\xa2\xe5\xad\x97'
gpt2 5 tokens: [b'\xe6', b'\xbc', b'\xa2', b'\xe5\xad', b'\x97']
cl100k_base 3 tokens: [b'\xe6\xbc', b'\xa2', b'\xe5\xad\x97']
o200k_base 2 tokens: [b'\xe6\xbc\xa2', b'\xe5\xad\x97']Con GPT-2, la carita cuesta dos tokens y ninguno de los dos es un carácter: b'\xf0\x9f' es la
primera mitad de un emoji, y por separado no se puede ni imprimir. Los dos ideogramas cuestan cinco
tokens, tres de ellos bytes sueltos. El modelo no ve dos caracteres, ve cinco piezas cuya relación
con lo escrito solo existe si aprende a reconstruirla.
Esa es la moneda con la que se paga la garantía. Nada queda fuera del vocabulario, pero lo que está
lejos del corpus de entrenamiento entra troceado en pedazos sin significado. Compáralo con la ñ,
que en los tres tokenizadores es un token entero pese a ocupar dos bytes: el español está lo bastante
representado en el corpus como para que esa fusión se aprendiera. La diferencia entre un carácter
barato y uno caro no es Unicode, es cuánto salía en el corpus.
La regex que casi nadie mira
Antes de que BPE toque el texto, hay un paso del que casi nunca se habla y que decide más de lo que parece. El texto se parte primero con una expresión regular, y las fusiones nunca pueden cruzar esos cortes. La regex, por tanto, no acelera nada: delimita lo que el algoritmo tiene permitido aprender.
El trozo que gobierna los números cambió entre generaciones, y se ve a simple vista:
| tokenizador | los dígitos en la regex | 1234567 | 2026 |
|---|---|---|---|
| gpt2 | ' ?\p{N}++', rachas de cualquier longitud | 123 45 67 | 20 26 |
| cl100k, o200k | '\p{N}{1,3}', grupos de tres como mucho | 123 456 7 | 202 6 |
En GPT-2 la regex entregaba 1234567 entero y BPE fusionaba dentro según lo que hubiera visto, con
lo que los cortes salían donde cayera. Desde cl100k el corte va por delante y es fijo: grupos de
tres, de izquierda a derecha. Mira lo que le hace al año en que escribo esto. 2026 no se parte
en 20 y 26, ni se queda entero: sale 202 y 6. El mismo dígito acaba en un token distinto
según la longitud del número que lo rodea, y esa decisión, tomada en una expresión regular, es de
donde salen la mitad de los problemas aritméticos de los modelos de lenguaje.
Dos caminos hacia el mismo suelo
No todos bajaron al byte por la puerta de GPT-2. SentencePiece, que entrena sobre caracteres, añadió
una opción llamada byte_fallback: el vocabulario sigue siendo de caracteres, y solo cuando aparece
uno que no está se recurre a escribirlo con sus bytes. La garantía es la misma, la ruta no.
Los dos tokenizadores de Llama enseñan las dos rutas, una en cada versión. El de Llama 2, con 32 000
piezas, declara byte_fallback: true: es un BPE de caracteres con red de seguridad. El de Llama 3,
con 128 256, lo declara false, y no porque hayan quitado la red, sino porque ya no hace falta:
cambiaron a BPE de bytes, donde el suelo es el propio alfabeto. En dos versiones de la misma familia
se puede ver la migración entera.
La carrera del vocabulario
Con el suelo resuelto queda la pregunta de arriba: cuántas piezas. Y aquí los números se movieron mucho en pocos años, de las 32 000 de Llama 2 a las 200 019 de GPT-4o.
Lo que se compra subiendo es longitud de secuencia. Lo que se paga son parámetros: el modelo guarda la matriz de embeddings y la de salida, números en total, y la capa final hace del orden de operaciones por cada token que genera. Con y , esas dos matrices suman unos 1 050 millones de parámetros, que en un modelo de ocho mil millones es más de un octavo del total dedicado a entrar y salir del vocabulario. Subir divide el número de pasos y multiplica el coste de cada uno, así que hay un punto óptimo y depende del tamaño del modelo; hay trabajo reciente dedicado justo a localizarlo.
Puestos a mirar números, lo interesante es qué compra ese tamaño para el español. Aquí están siete tokenizadores reales sobre el mismo texto, 400 000 caracteres de literatura española, 72 786 palabras:
| tokenizador | algoritmo | tokens por palabra | |
|---|---|---|---|
| Llama 2 (2023) | BPE + byte_fallback | 32 000 | 1,750 |
| GPT-2 (2019) | BPE de bytes | 50 257 | 2,029 |
| GPT-3.5 y GPT-4 (2022) | BPE de bytes | 100 277 | 1,653 |
| BERT multilingüe (2018) | WordPiece | 119 547 | 1,542 |
| Llama 3 (2024) | BPE de bytes | 128 256 | 1,650 |
| GPT-4o (2024) | BPE de bytes | 200 019 | 1,460 |
| XLM-R (2019) | Unigram | 250 002 | 1,519 |
Dentro de una misma familia la tendencia es limpia: los tres tokenizadores de OpenAI bajan el español de 2,03 a 1,65 y a 1,46 tokens por palabra según duplican el vocabulario, con rendimientos decrecientes claros (la primera duplicación se lleva un 18 %, la segunda un 12 %).
Entre familias, la tabla dice algo distinto y más útil. El tamaño no manda. Llama 2, con 32 000 piezas, corta el español mejor que GPT-2 con 50 257. Un BERT multilingüe de 2018 lo corta mejor que Llama 3 de 2024, teniendo casi el mismo tamaño de vocabulario. Y XLM-R, con 250 002 piezas, el vocabulario más grande de la tabla, pierde contra GPT-4o, que tiene cincuenta mil menos.
Lo que decide no es cuántas piezas hay, sino cuántas de ellas se gastaron en tu idioma. XLM-R reparte un cuarto de millón de piezas entre cien idiomas; GPT-4o concentra doscientas mil en bastante menos. Un vocabulario es un presupuesto, y repartirlo es una decisión de producto que nadie te consulta y que tú acabas pagando por token.
Lo que te cuesta la elección
Todo lo anterior es maquinaria. Esta sección es la factura, y su gracia está en que ninguna de las cosas que vienen es un misterio: cada una se puede rastrear hasta una decisión concreta que has visto tomar en las secciones anteriores.
El impuesto por escribir en español
Toma el Quijote en español y la traducción inglesa de Ormsby, las dos en Project Gutenberg, y pásale a cada una los tres tokenizadores de OpenAI. El original español ocupa 2 105 831 caracteres; la traducción inglesa, 2 292 068. El texto español es más corto.
| tokenizador | tokens en español | tokens en inglés | español / inglés | caracteres por token, es / en |
|---|---|---|---|---|
| gpt2 (2019) | 778 102 | 565 416 | 1,38 | 2,71 / 4,05 |
| cl100k (2022) | 632 915 | 541 068 | 1,17 | 3,33 / 4,24 |
| o200k (2024) | 558 227 | 536 900 | 1,04 | 3,77 / 4,27 |
Con GPT-2, el libro más corto cuesta un 38 % más. La columna de la derecha dice por qué: el tokenizador saca 4,05 caracteres de cada token en inglés y 2,71 en español, de modo que el mismo esfuerzo de lectura rinde la mitad. Y esa diferencia no es solo estética, porque los tokens son la unidad en la que se cuenta todo lo demás: lo que cabe en la ventana de contexto, lo que tarda una respuesta y lo que cuesta la factura de la API. Escribir en español salía, literalmente, más caro.
La última fila es la buena noticia con la que empezaba este artículo: con el tokenizador de GPT-4o la razón baja a 1,04, casi paridad. No porque el español haya cambiado, sino porque doblar el vocabulario dejó sitio para meter dentro palabras españolas enteras. Y conviene no generalizar de aquí a todos los idiomas: el español comparte alfabeto con el inglés y aparece a espuertas en los corpus de entrenamiento. Para los idiomas que además cambian de escritura, la diferencia sigue siendo de varios factores, y la sección del suelo de bytes explica por qué: cuando un carácter cuesta tres tokens de bytes sueltos, no hay vocabulario que lo arregle sin dedicarle espacio.
Por qué no sabe cuántas erres tiene una palabra
Es el ejemplo que circula por todas partes, y casi siempre se explica mal. La pregunta es cuántas erres hay en «strawberry», y el modelo se equivoca. La causa no es que no sepa contar:
strawberry -> str · aw · berry
ferrocarril -> fer · roc · arr · ilEl modelo nunca ve la palabra. Ve tres piezas, y ninguna de las tres es una letra. Para responder
tendría que saber de memoria cómo se deletrea cada pieza, porque la información que necesita se
destruyó antes de que él existiera, en el momento del corte. En español pasa lo mismo y peor:
«ferrocarril» tiene cuatro erres repartidas entre cuatro piezas, y una de ellas, arr, lleva dos
dentro. Contar sobre eso es contar sobre algo que no está.
Es la condición de la primera sección vista desde el otro lado. El tokenizador puede conservar el texto entero y aun así destruir la estructura del texto, porque conservar y hacer accesible no son lo mismo.
La aritmética, cortada por donde no toca
La regex de la sección anterior parte los dígitos en grupos de tres de izquierda a derecha. Míralo sobre números que se parecen mucho:
1000 -> 100 · 0
10000 -> 100 · 00
100000 -> 100 · 000
1999 -> 199 · 9
19999 -> 199 · 99El primer dígito de 1000 está en el mismo token que el segundo y el tercero, y el último va suelto.
En 10000, ese mismo 100 inicial ahora va seguido de 00. Los tokens no se corresponden con
unidades, decenas y centenas, y no se corresponden de forma distinta según la longitud del número.
Un modelo que suma tiene que hacerlo sobre piezas cuya posición en el número cambia de un caso a
otro. Que la aritmética con números largos sea frágil deja de ser un misterio en cuanto ves los
cortes: no está sumando cifras, está prediciendo trozos.
El espacio del final del prompt
Este muerde a quien programa contra una API, y es completamente invisible:
'El niño juega en el' -> El · ni · ño · jue · ga · en · el
'El niño juega en el ' -> El · ni · ño · jue · ga · en · el · ␣Un espacio de más al final del prompt añade un token que existe por su cuenta. Y aquí está el
problema: en todo el texto con el que se entrenó el modelo, el espacio va pegado a la palabra
siguiente ( jardín, con su espacio delante, es un token). Un espacio suelto al final es una
situación que el modelo casi no ha visto nunca, así que en lugar de continuar con jardín tiene
que continuar con algo que empiece sin espacio, que es un reparto de probabilidad completamente
distinto. Una tecla de más y la calidad cae. Algunas bibliotecas lo arreglan por dentro con lo que
se llama token healing: quitan el último token del prompt y lo vuelven a fusionar con lo que el
modelo genere. Casi ninguna lo hace por defecto.
La sangría, y por qué el código se abarató
El mismo mecanismo explica un cambio que sí se notó en su momento. Una línea de Python indentada, con GPT-2 y con cl100k:
' return x' gpt2 5 tokens: ␣ · ␣ · ␣ · ' return' · ' x'
cl100k 3 tokens: ' ' · ' return' · ' x'
' return x' gpt2 9 tokens
cl100k 3 tokensGPT-2 gastaba un token por espacio, así que una función con ocho niveles de nada se comía la ventana de contexto en sangría. cl100k metió en el vocabulario piezas de varios espacios seguidos y el mismo código pasó a costar la mitad o menos. Nadie cambió el modelo: cambiaron qué había en la lista.
Los tokens fantasma
El más extraño de todos, y el que mejor enseña que el tokenizador es una pieza aparte. En el
vocabulario de GPT-2 hay entradas como SolidGoldMagikarp, petertodd, RandomRedditor o
externalToEVAOnly, cada una un solo token. Son nombres de usuario de Reddit y fragmentos de
código que aparecían tantísimo en el material con el que se entrenó el tokenizador que se ganaron su
fusión.
El detalle es que ese material no es el mismo con el que se entrenó el modelo. Cuando esos textos se
filtraron del corpus de entrenamiento, sus tokens se quedaron en el vocabulario con una fila de
embedding inicializada al azar y nunca ajustada. Pedirle a GPT-2 que repita uno de ellos
producía respuestas absurdas o evasivas,
porque estaba leyendo un vector que no significaba nada. La mayoría desaparecieron en los
vocabularios siguientes, aunque no todos: davidjl sigue siendo un token único en el de GPT-4.
Ninguna de estas seis cosas es un error de programación. Todas son la consecuencia de una decisión razonable: comprimir bien el corpus que había, cortar los números de forma fija, meter los espacios en la palabra siguiente, aprovechar el texto disponible para entrenar el tokenizador. Lo que tienen en común es que ninguna se tomó pensando en lo que tú ibas a hacer con el modelo.
Lo que viene después del tokenizador
Después de siete secciones de problemas causados por elegir una unidad, la pregunta se cae de madura: por qué no quitar el tokenizador de en medio y dejar que el modelo lea bytes.
Se ha probado, y funciona. ByT5 es un modelo de 2021 que lee bytes crudos y aguanta la comparación con su versión tokenizada, además de ser mucho más robusto frente a las erratas, que es justo lo que uno esperaría de algo que ve las letras. CANINE, del mismo año, hace algo parecido comprimiendo la secuencia por dentro. Los dos chocan contra la pared de la tercera sección: sin unidades largas la secuencia se multiplica por seis y el coste por treinta, y ninguna cantidad de elegancia paga eso.
Lo que ha cambiado en los últimos años es la forma de la pregunta. En lugar de elegir entre una unidad grande y una pequeña, la idea es no fijar la unidad: que el modelo lea bytes y decida sobre la marcha dónde agruparlos. MEGABYTE lo hizo con parches de tamaño fijo. El Byte Latent Transformer, de finales de 2024, lo hace con parches de tamaño variable, y su criterio es la mejor idea del asunto: un modelo pequeño estima la entropía del siguiente byte, y se abre un parche nuevo donde esa entropía se dispara,
Léelo despacio, porque invierte el planteamiento entero del artículo. BPE gasta piezas donde el corpus repetía cosas, hace años y de una vez para siempre. Esto gasta cómputo donde el texto que tienes delante es difícil de predecir, ahora. El final de «programación» es previsible y no merece un corte; el nombre propio que nunca has visto merece varios. Un tokenizador que se adapta al texto en lugar de a un corpus congelado.
Por el otro extremo hay quien empuja en la dirección contraria y le va bien: SuperBPE mantiene el vocabulario fijo pero deja que las piezas crucen los espacios, de modo que expresiones enteras se vuelven un token. Y H-Net aprende el troceado de punta a punta, dentro de la red, sin ningún módulo aparte.
A día de hoy nada de esto es lo normal. Los modelos que usas cuando abres una API llevan dentro un BPE de bytes con un vocabulario de entre cien y doscientas mil piezas, entrenado una vez y congelado. Lo que tendría que pasar para que eso cambiase es medible: que el sobrecoste de leer secuencias más largas baje por debajo de lo que cuesta hoy el vocabulario fijo, contando en ese coste los mil millones de parámetros de las dos matrices, el impuesto por idioma y la lista de defectos de la sección anterior. Está más cerca que hace tres años. Escribo esto en 2026, y es la parte del artículo que peor va a envejecer.
Qué te llevas
Si te llevas una sola cosa, que sea esta: el tokenizador es la única parte del sistema que se decide antes de que se aprenda nada y no se revisa nunca. Todo lo demás en un modelo de lenguaje se ajusta con los datos. El corte, no. Se fija al principio, con el corpus que hubiera entonces, y todo lo que descarta o desordena queda descartado y desordenado para siempre.
Y si te llevas tres cosas más, que sean prácticas.
Mide tu propio texto antes de elegir modelo. La tabla de la sección del suelo de bytes está hecha con literatura del siglo XVII; tu texto no se parece a eso. Son seis líneas:
import tiktoken
enc = tiktoken.get_encoding("o200k_base") # o "cl100k_base", para comparar
texto = open("lo_que_escribes.txt", encoding="utf8").read()
tokens, palabras = len(enc.encode(texto)), len(texto.split())
print(f"{tokens / palabras:.2f} tokens por palabra")Entre 1,46 y 2,03 tokens por palabra, que es el rango que sale en este artículo para el mismo texto español, hay un 40 % de diferencia en lo que cabe en el contexto, en lo que tarda y en lo que cuesta. Vale la pena saber en qué punto de ese rango estás antes de firmar nada.
Trata el prompt como lo que es, una secuencia de tokens. No dejes un espacio suelto al final. No esperes aritmética fiable sobre números largos. No le pidas que cuente letras, porque no las tiene delante. Ninguna de esas tres cosas es un fallo del modelo que vaya a arreglarse con una versión mejor: son consecuencias del corte, y solo se arreglan cambiando el corte.
Y si alguna vez entrenas algo tuyo, empieza por ahí. Un tokenizador entrenado sobre tu propio dominio, aunque sea pequeño, corta mejor que uno de propósito general con diez veces más piezas, por la misma razón por la que un BERT multilingüe de 2018 corta el español mejor que un Llama 3 de 2024: lo que decide no es cuántas piezas hay, sino cuántas se gastaron en lo que tú escribes.
Volvamos al principio. «El niño juega en el jardín.» Costaba doce tokens en 2019 y cuesta siete ahora, los mismos que en inglés. Nadie arregló el español. Lo que cambió fue quién entró en una lista.