En la entrada anterior vimos cómo funciona el algoritmo BPE. En esta entrega veremos cómo los modelos LLM a gran escala tokenizan en la práctica, y luego tokenizaremos un corpus artesanalmente utilizando Python.
El tokenizador de OpenAI para el modelo gpt4o
Como referencia práctica, veremos cómo tokeniza GPT-4o con tiktokenizer; su tokenizador usa un vocabulario grande (~200K tokens), típico de sistemas comerciales. Por ejemplo, para el siguiente extracto de Altazor, de Vicente Huidobro:
Todas las lenguas están muertas
Muertas en manos del vecino trágico
Hay que resucitar las lenguas
Con sonoras risas
El número total de tokens viene dado por 32:

y cada token tiene un id único, que en este caso vienen dados por:

Un ejercicio interesante es ver qué ocurre cuando traducimos este texto al inglés, y calculamos sus tokens:

Con un número similar de palabras, la lengua inglesa suele generar menos tokens. A eso se le llama sobre-segmentación: entre otras razones, el tokenizador “rompe” más el texto cuando el idioma está menos favorecido por las frecuencias del corpus de entrenamiento (en este caso, el español). El resultado es más tokens para decir lo mismo, lo que encarece contexto y cómputo (Jurafsky y Martin, 2025). Moraleja pragmática: si quieres ahorrar dinero, escribe instrucciones en inglés!.
Creando nuestros propios tokens en Python!
Para construir nuestro tokenizador usaremos la librería tokenizers de Python, que implementa Byte-Level BPE. Hasta aquí ya entendemos qué es BPE; lo que falta es el componente byte-level. A continuación lo explicamos brevemente.
Byte-level BPE
En la entrada anterior describimos BPE partiendo desde caracteres. En byte-level BPE el punto de partida es distinto: antes de aplicar BPE, el texto se convierte a una secuencia de bytes (normalmente en UTF-8). Esto tiene una consecuencia clave: como cualquier texto en UTF-8 siempre es una secuencia de bytes entre 0x00 y 0xFF, el tokenizador dispone de un “alfabeto” base de 256 símbolos (uno por byte). A partir de ahí, BPE aprende a fusionar pares frecuentes de símbolos para formar tokens más largos. Para verlo, nota cómo se codifican algunos caracteres en UTF-8:
| Carácter | UTF-8 (Hex) | Unicode |
| a | 0x61 (1 byte) | U+0061 |
| ñ | 0xC3 0xB1 (2 bytes) | U+00F1 |
Si ahora tomamos el texto ñ😅, su representación UTF-8 es:
| Texto | UTF-8 (Hex) | Unicode |
| ñ😅 | 0xC3 0xB1 0xF0 0x9F 0x98 0x85 (6 bytes) | U+00F1 U+1F605 |
La pregunta natural es: ¿cuántos tokens produce?
En byte-level el “peor caso” (sin fusiones útiles) es directo: 1 token por byte, así que ñ😅 se representaría con 6 tokens. Sin embargo, en la práctica BPE aprende merges, y puede ocurrir que el tokenizador tenga un token que corresponda exactamente a la secuencia 0xC3 0xB1 (es decir, “ñ”), o incluso un token para el emoji completo. En ese caso, el mismo texto podría bajar a 5 tokens (si “ñ” es un token) o aún menos (si el emoji también se fusiona), es decir 2 tokens.
Esto explica por qué con byte-level BPE casi no existen “palabras desconocidas”: siempre se puede “caer” al nivel byte y representar cualquier texto. El problema ahora es la sobre-segmentación: si tu corpus no contiene suficientes ejemplos de ciertos patrones (jerga, nombres propios, emojis, mezclas de idiomas), esos fragmentos se partirán en más tokens, haciendo las secuencias más largas y aumentando el coste.
Dada esta breve introducción, ahora estamos en condiciones de crear tokens.
Generando tokens con Cervantes
Para este ejemplo, generaremos los tokens utilizando El Quijote de la Mancha de Miguel de Cervantes como input (lo puedes encontrar aquí en texto plano!). Para esto, emplearemos el siguiente código en Python:
from tokenizers import ByteLevelBPETokenizer
tokenizer = ByteLevelBPETokenizer()
tokenizer.train(
files=["quijote.txt"],
vocab_size=25000,
min_frequency=2,
special_tokens=["<s>", "</s>", "<pad>", "<unk>"]
)
vocab = tokenizer.get_vocab()
print("Tamaño del vocabulario:", len(vocab))
donde files, es una lista que contiene la ruta de los archivos que servirán como corpus, vocab_size es el tamaño objetivo del diccionario de tokens que va a tener el tokenizador, min_frequency es el umbral mínimo de apariciones que debe tener un patrón en nuestro corpus para ser fusionado, y por último, special_tokens, es una lista de tokens que se deben incluir sí o sí en el vocabulario.
La ejecución de este código da un tamaño total de vocabulario de 22.349 tokens. Para el mini-LLM que tenemos en mente, este número es más que aceptable. Sin embargo, en LLM’s grandes, necesitaríamos entre 50k-200k tokens para tener un modelo relativamente robusto.
Ahora que ya contamos con un tokenizador entrenado, el siguiente paso es evaluar su desempeño. Para ello, ejecutamos las siguientes líneas de código:
TEXT = "¡A ti lo agradezco, Dios, que cielo y tierra guías! ¡Válganme tus virtudes, gloriosa santa María! Desde aquí dejo a Castilla pues que al Rey he en ira"
enc = tokenizer.encode(TEXT)
print("Texto:", TEXT)
print("Nº tokens:", len(enc.ids))
print("IDs:", enc.ids)
print("Tokens:", enc.tokens)
Donde la variable TEXT contiene un texto que no está en el corpus original (es un fragmento del Cantar del Mío Cid modernizado). Habiendo dicho esto, el presente texto entrega un total de 38 tokens.
En este sentido, con la finalidad de ilustrar el problema de la sobre-segmentación, entrenaremos un tokenizador sobre un corpus deliberadamente pobre y observaremos cómo esto incrementa el número de tokens necesarios para codificar la misma frase de prueba. Para ello, utilizaremos el siguiente corpus:
En el 238 a. C. la tomaron los romanos, como consecuencia de la derrota cartaginesa en la Primera Guerra Púnica. Cerdeña se convirtió en provincia romana, y las ciudades costeras que existían fueron engrandecidas y embellecidas, mientras que se fundaron colonias como Turris Lybissonis y Feronia, que fueron pobladas por inmigrantes romanos.
Dejando todo lo demás constante, el número de tokens de la frase de prueba es ahora 127, considerablemente más que cuando teníamos un corpus más rico.
Por último, vale la pena recalcar que este proyecto consiste en construir nuestro propio LLM (pequeño) desde cero. Para esto, antes de entrenar el modelo, necesitamos dos piezas: un corpus (los textos con los que aprenderá) y un tokenizador entrenado sobre ese corpus. A escala industrial, lo caro no es solo entrenar: es reunir, limpiar, filtrar y deduplicar cantidades enormes de texto. Para ponerlo en perspectiva, en GPT-2 OpenAI entrenó con WebText: un dataset de algo más de 8 millones de documentos, que suman aproximadamente 40 GB de texto.
En esta serie no vamos a perseguir esa escala: trabajaremos con un corpus acotado, suficiente para entender el flujo completo.
Notas finales, y próxima entrada…
En esta entrada vimos un pequeño ejemplo de como funciona el algoritmo BPE, que es clave en la construcción de un LLM. En la próxima entrega introduciremos los modelos de lenguaje n-gram: modelos probabilísticos que asignan probabilidad a la próxima palabra y a oraciones completas. Stay tuned!
José Miguel Muñoz Urra – jmunozu@pulki.es