Curso / HF LLM Course / Capítulo 6
HF LLM Course · 6 de 12

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.

El pipeline de un tokenizer, etapa por etapa, sobre una frase El texto crudo pasa por normalización, que lo pone en minúsculas y colapsa espacios; por pre-tokenización, que lo corta en palabras; por el modelo de subpalabras, que parte transformer en transform y er; y por el post-procesado, que agrega CLS y SEP. Recién ahí cada token se convierte en un id del vocabulario. texto crudo Using a Transformer doble espacio, mayúsculas normalización using a transformer minúsculas, NFD, espacios · BERT uncased es destructivo pre-tokenización using a transformer corta en espacios y puntuación (BERT) · GPT-2 guarda el espacio como Ġ SentencePiece usa ▁ y no necesita esta etapa modelo · WordPiece using a transform ##er BPE, WordPiece o Unigram deciden dónde cortar dentro de cada palabra post-procesado [CLS] using a transform ##er [SEP] → ids: uno por token, del vocabulario de este tokenizer
Una frase atravesando las cuatro etapas de un tokenizer tipo BERT. Las dos primeras deciden los límites; el modelo de subpalabras decide cómo llenar el vocabulario dentro de esos límites; el post-procesado agrega lo que el modelo espera ver. En la librería 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, 'll para que no se fundan con pronombres o sustantivos.
  • Preservación de Aritmética (p{N}{1,3}): En tokenizers antiguos (GPT-2), números como 123456 se partían arbitrariamente en 123 y 456, mientras que 1234 se partía en 12 y 34. 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 V — 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 D={(w,fw)} donde cada palabra w tiene una frecuencia fw:

  1. Se inicializa el vocabulario V0 con todos los bytes básicos (256 símbolos) o caracteres individuales más los tokens de control.
  2. En cada iteración, se contabiliza la frecuencia agregada de todos los pares contiguos (u,v):
    freq(u,v)=∑w∈Dfw⋅count(u,v)(w)
  3. Se selecciona el par con máxima frecuencia: (u*,v*)=argmax(u,v)freq(u,v).
  4. Se añade el nuevo símbolo compuesto u*v* al vocabulario: V←V∪{u*v*}.
  5. Se reemplazan todas las apariciones de (u*,v*) en D 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:

score(u,v)=freq(u,v)freq(u)⋅freq(v)

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:

  1. Vocabulario Inicial Sobredimensionado: Comienza con un vocabulario enorme V0 (ej. todos los caracteres más las 100.000 subcadenas más frecuentes del corpus).
  2. Estimación de Parámetros (EM): Mediante el algoritmo de Esperanza-Maximización (Expectation-Maximization), estima la probabilidad unigram de cada subpalabra P(t) sujeta a ∑t∈VP(t)=1.
  3. Cálculo del Impacto de Poda: Para cada token t∈V, calcula cuánto se degradaría la pérdida de entropía cruzada del corpus si t fuera eliminado del vocabulario (ΔLt).
  4. Poda Iterativa: Elimina el 10%–20% de los tokens con menor ΔLt (protegiendo siempre los caracteres base para evitar [UNK]). Se repite hasta alcanzar el tamaño meta |V|.

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 T*=(t1,…,tm) que maximiza la probabilidad conjunta:

T*=argmaxT∏j=1mP(tj)
BPEWordPieceUnigram
Arranca deVocabulario chico (caracteres)Vocabulario chico (caracteres)Vocabulario grande
Cada pasoFusiona el par más frecuenteFusiona el par con mejor score (penaliza partes ya muy frecuentes)Elimina los tokens que menos aumentan la pérdida al sacarlos
AprendeReglas de fusión + vocabularioSolo el vocabulario finalVocabulario con una probabilidad por token
TokenizarAplica las fusiones aprendidas, en ordenBusca la subpalabra más larga que esté en el vocabulario, de izquierda a derechaBusca la segmentación más probable (algoritmo de Viterbi)
Lo usanGPT, GPT-2, RoBERTa, LlamaBERT, DistilBERTT5, 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.

Las primeras cuatro fusiones de BPE sobre el corpus de juguete El corpus hug por diez, pug por cinco, pun por doce, bun por cuatro y hugs por cinco arranca partido en caracteres. El par más frecuente es u g, veinte veces, y se fusiona en ug. Después u n, dieciséis veces; después h ug, quince; después p un, doce. Cada fusión se aplica en todo el corpus y entra al vocabulario en ese orden. 0 · empieza en caracteres par más frecuente: (u, g) ×20 h u g ×10 p u g ×5 p u n ×12 b u n ×4 h u g s ×5 1 · fusión «ug» siguiente: (u, n) ×16 h ug ×10 p ug ×5 p u n ×12 b u n ×4 h ug s ×5 2 · fusión «un» siguiente: (h, ug) ×15 h ug ×10 p ug ×5 p un ×12 b un ×4 h ug s ×5 3 · fusión «hug» siguiente: (p, un) ×12 hug ×10 p ug ×5 p un ×12 b un ×4 hug s ×5 4 · fusión «pun» y así hasta vocab_size hug ×10 p ug ×5 pun ×12 b un ×4 hug s ×5 tokenizar una palabra nueva es aplicar las mismas fusiones en el mismo orden: «bug» → b + ug · «hugging» → hug + g + i + n + g
BPE fusiona siempre el par más frecuente del corpus y aplica la fusión en todas partes. El vocabulario crece una entrada por paso, y las reglas quedan guardadas en orden: por eso «bug», que el corpus nunca vio, sale como 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:

TokenizerTamaño de vocabularioCorpus de entrenamiento del tokenizer
GPT-2 (2019)50.257Inglés, web
Llama 2 (2023)32.000SentencePiece BPE, mayormente inglés
Llama 3 (2024)≈128k (128.256)BPE a nivel de byte, multilingüe
Gemma 3 (2025)≈256kSentencePiece, multilingüe (el mismo de Gemini)
Gemma 4 (2026)262.144SentencePiece, 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íntomaCausa probableQué hacer
El modelo genera texto sin sentido, o empieza bien y degeneraTokenizer 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 esperadoSe usó tokenizer.encode(..., add_special_tokens=False), o convert_tokens_to_ids(tokenize(...)), que nunca agrega especialesUsar 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.096Se 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 suyosMedir 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 pararLos 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 turnotokenizer.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 idsEl chat template ya puso el BOS y tokenizer(texto) lo agregó otra vezadd_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 funcionarVocabulario nuevo = ids nuevos = embeddings del modelo que ya no corresponden a nadaReentrenar 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 cambiadosEl normalizer del tokenizer (minúsculas, NFD, colapsar espacios) es destructivo; el ida y vuelta no es exacto en BERT-likePara reconstruir tramos exactos del original, usar offset_mapping sobre el texto crudo, no decode (§2, §3)

7. Resumen

  1. Entrenar un tokenizer es estadístico y determinístico — nada que ver con entrenar un modelo.
  2. tokenizer(texto) → ids → decode es 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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