Hugging Face · LLM Course · Capítulo 6
Tokenizers: lo que pasa antes de que exista un solo número
Usaste tokenizers en cada capítulo hasta acá sin mirar adentro. Este capítulo abre esa caja: cómo se entrena un tokenizer, qué lo hace "rápido" de verdad, y los tres algoritmos que compiten por decidir dónde cortar una palabra.
Antes de leercap. 2 §3, tokenizers en uso
🎯 Al terminar vas a poder
- Explicar por qué entrenar un tokenizer es determinístico, y cuándo vale la pena reentrenar uno sobre tu dominio.
- Hacer el ida y vuelta texto → ids → texto con
encode/decode, y leer los símbolosĠ,##y▁sin sorprenderte. - Seguir a mano las primeras fusiones de BPE sobre un corpus de juguete, y contrastar con lo que hacen WordPiece y Unigram.
- Explicar qué le hace el tamaño del vocabulario al costo por token de un texto en español, con números de modelos reales.
- Diagnosticar los errores de tokenización que no dan excepción: tokenizer equivocado, tokens especiales que faltan, contar caracteres en vez de tokens.
1. Entrenar un tokenizer nuevo a partir de uno viejo
Entrenar un tokenizer no es entrenar un modelo — no hay descenso de gradiente ni aleatoriedad. Es un proceso estadístico y determinístico: mirar un corpus entero y decidir qué subpalabras merecen ser un token propio. Mismo corpus, mismo algoritmo, mismo resultado siempre.
AutoTokenizer.train_new_from_iterator() reentrena el vocabulario de un tokenizer existente
sobre un corpus nuevo, conservando su algoritmo y tokens especiales. El ejemplo del original — reentrenar
el tokenizer de GPT-2 (pensado para inglés) sobre código Python — muestra por qué esto importa: el
tokenizer original parte _ y las palabras con guión bajo de forma rara porque nunca vio
código en su entrenamiento. Uno reentrenado sobre CodeSearchNet aprende tokens propios como uno
para "indentación de 4 espacios" o uno para las triples comillas de un docstring — 27 tokens contra 36
para la misma función, con una representación que refleja la estructura real del lenguaje.
🟢 Por qué esto importa hoy
Todo LLM de propósito general reentrena su tokenizer sobre un corpus masivo y multilingüe antes de arrancar el preentrenamiento del modelo en sí — es de los primeros pasos, no un detalle. Un vocabulario mal ajustado al dominio (código, otro idioma, notación matemática) infla la cantidad de tokens que necesitás para decir lo mismo, lo que encarece cada request y acorta la ventana de contexto útil.
2. Los poderes ocultos de un tokenizer rápido
La salida de un tokenizer no es un diccionario simple — es un BatchEncoding, y si el
tokenizer es "rápido" (respaldado por Rust, vía la librería tokenizers), trae consigo un
offset mapping: para cada token, recuerda exactamente qué tramo del texto original lo generó.
encoding = tokenizer("My name is Sylvain", return_offsets_mapping=True)
encoding.tokens() # ['[CLS]', 'My', 'name', 'is', 'S', '##yl', '##va', '##in', '[SEP]']
encoding.word_ids() # [None, 0, 1, 2, 3, 3, 3, 3, None] — a qué palabra pertenece cada token
encoding["offset_mapping"] # [(0,0), (0,2), (3,7), (8,10), (11,12), (12,14), ...]
Esto no es un detalle de performance — es lo que hace posible reconstruir, sin reglas hechas a mano
para cada tipo de tokenizer, qué tramo exacto del texto original corresponde a una entidad detectada
por un modelo de NER: agrupás los tokens consecutivos de la misma entidad y tomás el offset desde el
primero hasta el último. La misma técnica es la base de cómo un pipeline de question-answering
recupera la respuesta exacta dentro del contexto, carácter por carácter.
El ida y vuelta: encode → decode
Lo que usaste en todos los capítulos anteriores sin mirarlo: tokenizer(texto) hace
tokenizar + convertir a ids + agregar tokens especiales, y decode deshace los tres pasos.
Con GPT-2 se ven además los símbolos que aparecen en cada tokenizer BPE a nivel de byte:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("gpt2")
texto = "Hola mundo, tokenizers"
ids = tok(texto)["input_ids"] # [39, 5708, 27943, 11, 11241, 11341]
tok.convert_ids_to_tokens(ids) # ['H', 'ola', 'Ġmundo', ',', 'Ġtoken', 'izers']
tok.decode(ids) # 'Hola mundo, tokenizers' ← el texto original, exacto
tok.decode(ids[2:3]) # ' mundo' ← con el espacio adelante
Ese Ġ no es un carácter del texto: es cómo GPT-2 representa el espacio. Un
tokenizer a nivel de byte trabaja sobre los 256 bytes posibles, y varios de ellos (el espacio, los
saltos de línea, los bytes de control) no se pueden imprimir ni guardar cómodamente en un archivo JSON de
vocabulario — así que GPT-2 los mapea a caracteres Unicode visibles; al byte 32 (el espacio) le toca
Ġ (U+0120 = 256 + 32). Y el espacio se pega al token siguiente, no al anterior:
Ġmundo es "espacio + mundo". Por eso " mundo" y "mundo" son tokens
distintos con ids distintos, y por eso decode puede reconstruir el texto exacto sin adivinar
dónde iban los espacios — la reversibilidad de la que habla la sección siguiente. BERT hace lo inverso
con ## (marca "continúa la palabra anterior", sin espacio), y SentencePiece usa
▁ para lo mismo que Ġ.
Los ids son específicos de cada tokenizer: el 5708 de arriba es "ola" en GPT-2 y otra
cosa cualquiera en Llama. El modelo aprendió sus embeddings contra su tokenizer; pasarle ids de
otro no da error, da basura. Y decode reconstruye texto de ids, no de tokens: si
querés unir una lista de tokens a mano, convert_tokens_to_string es el que sabe qué hacer con
Ġ y ##.
3. El pipeline completo: normalización → pre-tokenización → modelo
Antes de partir en subpalabras, todo tokenizer pasa el texto por dos pasos previos:
Normalización
Limpieza general: minúsculas, sacar acentos, normalizar Unicode (NFC/NFKC), espacios repetidos.
Pre-tokenización
Cortar el texto en unidades chicas (típicamente palabras) — esos cortes son los límites donde el algoritmo de subpalabras puede operar.
Cada familia de tokenizer pre-tokeniza distinto: BERT corta en espacios y puntuación; GPT-2 corta igual
pero conserva los espacios como parte del token siguiente (el símbolo Ġ que ya viste en
capítulos anteriores); T5 (basado en SentencePiece) solo corta en espacios, reemplazándolos
por ▁, sin tocar la puntuación.
tokenizers cada
etapa es una pieza intercambiable.
SentencePiece merece mención aparte: trata el texto como una secuencia cruda de caracteres Unicode,
incluyendo los espacios en el conjunto de símbolos — por eso ni siquiera necesita un paso de
pre-tokenización separado, algo clave para idiomas como japonés o chino, donde el espacio no separa
palabras. Como bonus, su tokenización es reversible: decodificar es solo concatenar
tokens y cambiar ▁ por espacio, sin la pérdida de información que sufre un tokenizer que
colapsa espacios repetidos.
La Pre-tokenización con Expresiones Regulares en LLMs Modernos
En modelos modernos (GPT-4, Llama-3, Qwen), la pre-tokenización no es un simple split por espacios: se gobierna mediante una expresión regular compilada en Rust para aislar categorías sintácticas antes de que opere BPE.
La expresión regular canónica de GPT-4 y Llama-3 ilustra esta segmentación:
(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^
\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[
]*|\s*[
]+|\s+(?!\S)|\s+
¿Por qué esta complejidad?
- Aislamiento de Contracciones: Separa
's,'ve,'llpara que no se fundan con pronombres o sustantivos. - Preservación de Aritmética (): En tokenizers antiguos (GPT-2), números como
123456se partían arbitrariamente en123y456, mientras que1234se partía en12y34. Esta representación fracturaba el aprendizaje de operaciones aritméticas en el LLM. Las expresiones modernas obligan a particiones de dígitos consistentes (máximo 1 a 3 dígitos por token). - Espacios en Blanco y Sangrías: Captura bloques de espacios de código (indentaciones de 2 o 4 espacios) en tokens individuales, optimizando la longitud de contexto en programación.
4. Los tres algoritmos: BPE, WordPiece, Unigram
Los tres resuelven el mismo problema — optimizar el vocabulario de subpalabras — pero bajo principios algorítmicos y matemáticos radicalmente distintos:
1. Byte Pair Encoding (BPE): Construcción Frecuencial Greedy
BPE es un algoritmo constructivo ascendente (bottom-up). Dado un corpus tokenizado donde cada palabra tiene una frecuencia :
- Se inicializa el vocabulario con todos los bytes básicos ( símbolos) o caracteres individuales más los tokens de control.
- En cada iteración, se contabiliza la frecuencia agregada de todos los pares contiguos :
- Se selecciona el par con máxima frecuencia: .
- Se añade el nuevo símbolo compuesto al vocabulario: .
- Se reemplazan todas las apariciones de en y se registra la regla de fusión (merge rule) en la tabla ordenada.
Para tokenizar en inferencia, BPE toma una palabra nueva y aplica secuencialmente las reglas de fusión aprendidas en estricto orden cronológico.
2. WordPiece: Fusión por Información Mutua
Utilizado por BERT, WordPiece difiere de BPE en la función objetivo de selección de fusiones. En lugar de maximizar la frecuencia absoluta pura, WordPiece evalúa el incremento en la verosimilitud (likelihood) del corpus bajo un modelo de lenguaje unigram, lo cual equivale a maximizar la Información Mutua Puntual (PMI) entre los tokens candidatos:
Esta métrica penaliza la fusión de componentes individuales que ya son sumamente frecuentes de forma aislada (ej. partículas gramaticales o morfemas comunes), priorizando pares cuya correlación conjunta supera drásticamente su probabilidad marginal independiente.
3. Unigram Language Model (Kudo, 2018): Poda Probabilística Descendente
A diferencia de BPE y WordPiece, Unigram es un algoritmo top-down basado en optimización probabilística:
- Vocabulario Inicial Sobredimensionado: Comienza con un vocabulario enorme (ej. todos los caracteres más las 100.000 subcadenas más frecuentes del corpus).
- Estimación de Parámetros (EM): Mediante el algoritmo de Esperanza-Maximización (Expectation-Maximization), estima la probabilidad unigram de cada subpalabra sujeta a .
- Cálculo del Impacto de Poda: Para cada token , calcula cuánto se degradaría la pérdida de entropía cruzada del corpus si fuera eliminado del vocabulario ().
- Poda Iterativa: Elimina el 10%–20% de los tokens con menor (protegiendo siempre los caracteres base para evitar
[UNK]). Se repite hasta alcanzar el tamaño meta .
Durante la inferencia, una palabra puede segmentarse de múltiples maneras válidas. Unigram resuelve la ambigüedad aplicando el algoritmo de Viterbi para encontrar la segmentación que maximiza la probabilidad conjunta:
| BPE | WordPiece | Unigram | |
|---|---|---|---|
| Arranca de | Vocabulario chico (caracteres) | Vocabulario chico (caracteres) | Vocabulario grande |
| Cada paso | Fusiona el par más frecuente | Fusiona el par con mejor score (penaliza partes ya muy frecuentes) | Elimina los tokens que menos aumentan la pérdida al sacarlos |
| Aprende | Reglas de fusión + vocabulario | Solo el vocabulario final | Vocabulario con una probabilidad por token |
| Tokenizar | Aplica las fusiones aprendidas, en orden | Busca la subpalabra más larga que esté en el vocabulario, de izquierda a derecha | Busca la segmentación más probable (algoritmo de Viterbi) |
| Lo usan | GPT, GPT-2, RoBERTa, Llama | BERT, DistilBERT | T5, ALBERT, XLNet (vía SentencePiece) |
Vale la pena seguir un ejemplo concreto de BPE, porque el mecanismo es simple una vez que lo ves una
vez. Corpus de juguete: hug(×10), pug(×5), pun(×12),
bun(×4), hugs(×5). Se arranca con cada palabra partida en caracteres:
h u g (×10) p u g (×5) p u n (×12) b u n (×4) h u g s (×5)
El par más frecuente en todo el corpus es ("u","g") — aparece 20 veces (en hug,
pug y hugs). Esa es la primera fusión: "ug" entra al vocabulario,
y se reemplaza en todas partes. El siguiente par más frecuente pasa a ser ("u","n") (16
veces) → se funde en "un". Después ("h","ug") (15 veces) → "hug".
Y así, fusión por fusión, hasta llegar al tamaño de vocabulario que se pidió. Tokenizar una palabra
nueva es aplicar esas mismas reglas de fusión, en el mismo orden en que se aprendieron.
b + ug.
🟢 Actualización 2026
BPE ganó la carrera de forma aplastante para LLMs decoder-only — pero la variante que se usa hoy casi siempre es BPE a nivel de byte (la que introdujo GPT-2): en vez de partir en caracteres Unicode, parte en los 256 valores de byte posibles. Eso significa que el vocabulario base es minúsculo y nunca hay un carácter — emoji, símbolo raro, cualquier idioma — que caiga en el token desconocido, porque todo, sin excepción, se puede descomponer en bytes. Los vocabularios finales de los LLM modernos también crecieron mucho respecto a los ~50k de GPT-2 — hoy es común ver vocabularios de 100k a más de 250k tokens, para cubrir muchos idiomas sin que cada uno se fragmente en demasiados tokens.
Los números, para tenerlos como referencia:
| Tokenizer | Tamaño de vocabulario | Corpus de entrenamiento del tokenizer |
|---|---|---|
| GPT-2 (2019) | 50.257 | Inglés, web |
| Llama 2 (2023) | 32.000 | SentencePiece BPE, mayormente inglés |
| Llama 3 (2024) | ≈128k (128.256) | BPE a nivel de byte, multilingüe |
| Gemma 3 (2025) | ≈256k | SentencePiece, multilingüe (el mismo de Gemini) |
| Gemma 4 (2026) | 262.144 | SentencePiece, 140+ idiomas; el que usa tu Gemma 4 31B |
Por qué esto te importa directamente: el costo de una request y el espacio que ocupa un documento en la ventana de contexto se miden en tokens, y cuántos tokens necesita un texto depende de qué tan bien lo cubre el vocabulario. Un tokenizer de 32k entrenado sobre inglés tiene tokens enteros para "the" y "because", pero parte "interpretante" en cuatro o cinco pedazos; uno de 128k o 256k entrenado sobre un corpus multilingüe tiene lugar para subpalabras frecuentes del español, del japonés o del código. El mismo párrafo en español puede costar entre 1,5× y 2× más tokens con un tokenizer anglocéntrico que con uno multilingüe — es la diferencia entre que un documento entre o no en la ventana, y entre pagar el doble o no por la misma respuesta. Meta reportó, al pasar de Llama 2 a Llama 3, que el vocabulario nuevo necesitaba del orden de 15 % menos tokens para el mismo texto. El costo del vocabulario grande está en otro lado: la matriz de embeddings y el head de salida crecen linealmente con él (256k × hidden size es una fracción visible de un modelo chico), y el softmax final sobre todo el vocabulario en cada paso de decode también.
WordPiece cambia solo el criterio de qué fusionar: en vez del par más frecuente a secas, prioriza pares
donde cada parte individual es poco frecuente por sí sola — evita fusionar algo como
"un" + "##able" aunque el par sea común, porque ambos fragmentos ya son
comunes por separado y probablemente sirven mejor sueltos.
Unigram invierte el proceso: arranca de un vocabulario enorme (todas las subcadenas posibles, o el resultado de correr BPE hasta un vocabulario grande) y en cada paso elimina el porcentaje de tokens que, sacados, menos empeoran una función de pérdida calculada sobre todo el corpus. Nunca elimina caracteres sueltos, para poder tokenizar cualquier palabra sin caer en desconocido.
5. Armar un tokenizer, bloque por bloque
La librería tokenizers expone cada etapa del pipeline como una pieza intercambiable:
normalizers, pre_tokenizers, models (BPE/WordPiece/Unigram),
trainers, post_processors y decoders. Armar un tokenizer propio
es elegir una de cada una y entrenarlo:
from tokenizers import Tokenizer, models, normalizers, pre_tokenizers, trainers
tokenizer = Tokenizer(models.WordPiece(unk_token="[UNK]"))
tokenizer.normalizer = normalizers.Sequence([normalizers.NFD(), normalizers.Lowercase()])
tokenizer.pre_tokenizer = pre_tokenizers.Whitespace()
trainer = trainers.WordPieceTrainer(vocab_size=25000, special_tokens=["[UNK]", "[CLS]", "[SEP]"])
tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)
El último paso, post_processor, es el que agrega los tokens especiales en su lugar
([CLS] al inicio, [SEP] entre oraciones) y asigna los token_type_ids
del Capítulo 3. Un tokenizer armado así se guarda en un único JSON y se envuelve en
PreTrainedTokenizerFast para usarlo exactamente igual que cualquier tokenizer de
transformers — incluido subirlo al Hub con push_to_hub().
6. Errores comunes
Ninguno de estos lanza una excepción. Se ven en salidas raras, contextos truncados o facturas más altas de lo esperado.
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| El modelo genera texto sin sentido, o empieza bien y degenera | Tokenizer de otro modelo (o de otra versión del mismo): los ids no corresponden a los embeddings que el modelo aprendió | Cargar siempre tokenizer y modelo del mismo repo/carpeta; guardarlos juntos con save_pretrained (§2) |
Los input_ids no empiezan con BOS/[CLS] y el modelo responde peor de lo esperado | Se usó tokenizer.encode(..., add_special_tokens=False), o convert_tokens_to_ids(tokenize(...)), que nunca agrega especiales | Usar tokenizer(texto) (agrega por defecto); reservar add_special_tokens=False para texto que ya salió de un chat template (§2, §5) |
| Un documento "de 3.000 caracteres" se trunca aunque el contexto es de 4.096 | Se contaron caracteres en vez de tokens: en español, 3.000 caracteres son 800–1.000 tokens con un buen tokenizer y bastante más con uno anglocéntrico; y el chat template suma los suyos | Medir con len(tokenizer(texto)["input_ids"]) del tokenizer exacto del modelo, y dejar margen para la respuesta (§4) |
Después de un fine-tune de chat, el modelo escribe <|im_end|> literalmente en vez de parar | Los tokens especiales del template no estaban registrados como especiales: el tokenizer los partió en pedazos comunes y el modelo nunca aprendió el id de fin de turno | tokenizer.add_special_tokens({"additional_special_tokens": [...]}) + model.resize_token_embeddings(len(tokenizer)) antes de entrenar, y fijar eos_token al token de fin de turno (§5) |
| Dos BOS al principio de los ids | El chat template ya puso el BOS y tokenizer(texto) lo agregó otra vez | add_special_tokens=False al tokenizar la salida del template (Capítulo 2 §3) |
| Reentrenaste el tokenizer sobre tu dominio y el modelo preentrenado dejó de funcionar | Vocabulario nuevo = ids nuevos = embeddings del modelo que ya no corresponden a nada | Reentrenar el tokenizer solo si vas a preentrenar (o al menos continuar el preentrenamiento) del modelo con él; para fine-tuning, quedarse con el tokenizer original y a lo sumo agregar tokens (§1) |
decode devuelve el texto con espacios o acentos cambiados | El normalizer del tokenizer (minúsculas, NFD, colapsar espacios) es destructivo; el ida y vuelta no es exacto en BERT-like | Para reconstruir tramos exactos del original, usar offset_mapping sobre el texto crudo, no decode (§2, §3) |
7. Resumen
- Entrenar un tokenizer es estadístico y determinístico — nada que ver con entrenar un modelo.
tokenizer(texto)→ ids →decodees un ida y vuelta exacto en BPE a nivel de byte;Ġ,##y▁son las tres formas de marcar dónde iban los espacios. Los ids solo significan algo para el modelo que se entrenó con ese tokenizer.- Un tokenizer rápido (Rust) trae offset mapping — la pieza que hace posible NER y QA sin reglas hechas a mano por tipo de tokenizer.
- Normalización + pre-tokenización deciden los límites; BPE, WordPiece y Unigram deciden cómo llenar el vocabulario dentro de esos límites, desde direcciones opuestas (construir vs. podar).
- BPE a nivel de byte es el estándar de facto en LLMs decoder-only hoy — vocabulario base minúsculo, cero tokens desconocidos posibles. Los vocabularios crecieron de 32k–50k a 128k–256k, y eso se traduce directo en menos tokens (menos costo, más contexto útil) para todo lo que no sea inglés.
- Cada etapa del pipeline es una pieza intercambiable en
tokenizers— armar un tokenizer propio es elegir una de cada tipo y entrenarlo.
8. Ponete a prueba
Respondé antes de abrir cada respuesta. Si fallás una, volvé a la sección indicada.
1. ¿Por qué entrenar dos veces el mismo tokenizer sobre el mismo corpus da exactamente el mismo vocabulario, si entrenar dos veces un modelo no?
Porque no hay gradiente ni inicialización aleatoria: BPE cuenta pares y fusiona el más frecuente, WordPiece lo mismo con otro score, Unigram poda por pérdida. Son cuentas sobre el corpus, no optimización estocástica. (§1)
2. GPT-2 tokeniza "Hola mundo" como ['H', 'ola', 'Ġmundo']. ¿Qué es Ġ y por qué "mundo" al principio de una frase daría un token distinto?
Ġ es el byte del espacio (32) mapeado a un carácter visible (U+0120), pegado al token que le sigue. "mundo" sin espacio adelante es otra secuencia de bytes y por lo tanto otro token (o varios) con otros ids. Por eso el decode es exacto: cada token sabe si llevaba espacio adelante. (§2)
3. Un modelo de 4.096 tokens de contexto. Le mandás un documento de 3.500 caracteres en español más un system prompt. ¿Es seguro? ¿Cómo lo verificás?
Probablemente sí, pero no por los caracteres: hay que medir tokens con el tokenizer exacto del modelo, aplicar el chat template (que suma tokens propios), y dejar lugar para la respuesta (max_new_tokens). 3.500 caracteres en español son del orden de 900–1.200 tokens con un tokenizer multilingüe y más con uno anglocéntrico. (§4, §6)
4. En el corpus de juguete de §4, ¿cuál es la cuarta fusión de BPE, después de "ug", "un" y "hug"?
Después de tres fusiones el corpus es: hug (×10), p ug (×5), p un (×12), b un (×4), hug s (×5). Los pares: ("p","ug") 5, ("p","un") 12, ("b","un") 4, ("hug","s") 5. Gana ("p","un") con 12 → se crea el token "pun". (§4)
5. ¿Por qué un tokenizer BPE a nivel de byte nunca produce [UNK], y qué pasa en cambio con un emoji o un ideograma raro?
Porque el vocabulario base son los 256 bytes posibles, y cualquier texto UTF-8 es una secuencia de esos bytes: siempre hay una descomposición válida. Un carácter que el tokenizer nunca vio no falla — se parte en sus 2, 3 o 4 bytes, cada uno como un token, lo que cuesta más tokens pero nunca información. (§4)
6. Vas a fine-tunear un modelo para chat con un formato nuevo que usa <|fin|> como fin de turno. ¿Qué dos cosas tenés que hacer antes de entrenar, y qué ves si te olvidás?
Registrar <|fin|> como token especial (add_special_tokens) y redimensionar los embeddings del modelo (resize_token_embeddings); además, fijar ese token como eos_token para que generate pare ahí. Si no, el tokenizer lo parte en <, |, fin… y después del entrenamiento el modelo escribe esa secuencia literalmente y sigue generando. (§5, §6)
9. Para practicar
✏️ Ejercicio 1 — el mismo párrafo en español, tres tokenizers
Contá tokens con tres vocabularios de tamaño distinto y mirá cómo se parte una palabra larga. Solo descarga tokenizers (KB a pocos MB); sin modelo.
from transformers import AutoTokenizer
texto = ("El interpretante lacaniano no es una persona sino una función: "
"la que sostiene la cadena significante mientras el sujeto habla.")
palabra = "interpretante"
for nombre in ["gpt2", "HuggingFaceTB/SmolLM2-135M", "Qwen/Qwen2.5-0.5B", "google/gemma-4-E2B-it"]:
tok = AutoTokenizer.from_pretrained(nombre)
n = len(tok(texto, add_special_tokens=False)["input_ids"])
print(f"{nombre:32s} vocab={len(tok):>7d} tokens={n:3d} {len(texto)/n:.1f} chars/token {tok.tokenize(palabra)}")
Pista / qué esperar
Salida real: GPT-2 (vocab 50.257) usa 41 tokens, 3,1 caracteres por token; SmolLM2 (49.152) 38 y 3,3; Qwen2.5 (151.665) 30 y 4,2; Gemma 4 (262.144) 26 y 4,9. Los cuatro parten "interpretante" en ['interpret', 'ante']. Los números de la tabla de §4 son len(tok), y los caracteres por token son la métrica que resume todo: más alto = más barato. El mismo texto le cuesta a Gemma 4 un 37 % menos tokens que a GPT-2, que es lo que se paga por un vocabulario cinco veces más grande.
✏️ Ejercicio 2 — ida y vuelta, y dónde se rompe
Comprobá cuándo decode(encode(x)) == x y cuándo no, con un tokenizer byte-level y uno
WordPiece con normalización.
pruebas = ["Hola mundo", "Árbol y ñandú", "código\tcon tab", "emoji 🤗 raro"]
for nombre in ["gpt2", "bert-base-uncased"]:
tok = AutoTokenizer.from_pretrained(nombre)
for t in pruebas:
ids = tok(t, add_special_tokens=False)["input_ids"]
vuelta = tok.decode(ids)
print(f"{nombre:18s} {vuelta == t!s:5} {tok.convert_ids_to_tokens(ids)}")
Pista / qué esperar
GPT-2 devuelve True en los cuatro casos: el doble espacio, el tab y el emoji sobreviven (fijate cómo el emoji se convierte en varios tokens de bytes). BERT uncased devuelve False en casi todos: minúsculas, acentos fuera, espacios colapsados. Es la última fila de la tabla de §6 — y la razón por la que NER usa offset_mapping y no decode.
✏️ Ejercicio 3 — entrenar un BPE de juguete y leer sus fusiones
Reproducí el ejemplo de §4 con la librería tokenizers y comprobá que las primeras fusiones
son las que calculaste a mano. Corre en milisegundos.
from tokenizers import Tokenizer, models, pre_tokenizers, trainers
corpus = ["hug"] * 10 + ["pug"] * 5 + ["pun"] * 12 + ["bun"] * 4 + ["hugs"] * 5
tok = Tokenizer(models.BPE())
tok.pre_tokenizer = pre_tokenizers.Whitespace()
tok.train_from_iterator(corpus, trainer=trainers.BpeTrainer(vocab_size=12, show_progress=False))
tok.save("bpe-juguete.json")
import json
print(json.load(open("bpe-juguete.json"))["model"]["merges"])
print(tok.encode("bug").tokens, tok.encode("hugging").tokens)
Pista / qué esperar
Las fusiones salen en orden: u g, u n, h ug, p un… — exactamente la cuenta de §4 y de la pregunta 4 del quiz. "bug" se tokeniza como b + ug (nunca vio "bug", pero sí "ug"); "hugging" usa hug y después cae a caracteres sueltos para lo que no está en el vocabulario. Subí vocab_size y mirá qué fusiones aparecen después.
10. Fuentes y para seguir
- Hugging Face LLM Course — Chapter 6: The 🤗 Tokenizers library — el capítulo original, con los tres algoritmos trabajados paso a paso y el tokenizer armado bloque por bloque.
- Tokenizers — documentación — la referencia de la librería en Rust: normalizers, pre-tokenizers, modelos, trainers, post-processors y decoders.
- Neural Machine Translation of Rare Words with Subword Units (Sennrich et al., 2015) — el paper que trajo BPE al NLP; el algoritmo de §4 está en su sección 3.
- Subword Regularization (Kudo, 2018) — el paper de Unigram: por qué tener varias segmentaciones posibles con probabilidad es una ventaja, no un defecto.
- SentencePiece (Kudo & Richardson, 2018) — texto crudo sin pre-tokenización, el símbolo
▁, y la reversibilidad de §3. - The Llama 3 Herd of Models (Meta, 2024) — la sección de tokenizer explica el salto a 128k y su efecto en tokens por texto, el dato de §4.