Tokenización y Embeddings: el primer paso para construir un LLM desde cero
Introducción
Antes de que un modelo de lenguaje pueda entender una palabra, tiene que convertirla en números. Ese proceso tiene dos pasos fundamentales: la tokenización (convertir texto en IDs numéricos) y los embeddings (convertir esos IDs en vectores densos que el modelo pueda procesar).
En este artículo vamos a implementar ambos desde cero, ejecutar el código en nuestra GTX 1070, y ver resultados reales. Todo el código funciona en hardware asequible — no necesitas una GPU de última generación para entender cómo funcionan los LLMs por dentro.
Este artículo abre la serie Construye tu LLM desde Cero en Español, que sigue paso a paso la serie Writing an LLM from scratch de Giles Thomas (licencia CC BY 4.0). Cada artículo replica los mismos experimentos del original, pero ejecutados con nuestro hardware (GTX 1070, 32 GB RAM) y explicados en español. No es una traducción: es el mismo viaje, con resultados reales y código que puedes ejecutar tú mismo.
¿Qué es la tokenización?
Los LLMs no procesan texto directamente. Procesan números. La tokenización es el puente entre ambos mundos: convierte una secuencia de caracteres en una secuencia de enteros (token IDs).
El sistema más usado hoy es Byte Pair Encoding (BPE). Funciona así:
1. Empieza con tokens para cada letra, número y signo de puntuación
2. Examina un corpus enorme de texto y encuentra pares de tokens que aparecen juntos con frecuencia
3. Crea nuevos tokens para esos pares
4. Repite hasta alcanzar el tamaño de vocabulario deseado
OpenAI usa BPE en todos sus modelos. La librería tiktoken nos da acceso al mismo tokenizador que usa GPT-2 —50.257 tokens— y que usaremos como referencia para comparar.
Vamos a probarlo con texto en español en nuestra máquina:
import tiktoken
tokenizer = tiktoken.get_encoding("gpt2")
texto = "Hola, ¿cómo estás? Bienvenido al mundo de los LLMs."
tokens = tokenizer.encode(texto)
print(f"Tokens decodificados: {[tokenizer.decode([t]) for t in tokens]}")
Resultado real:
'H', 'ola', ',', ' �', '�', 'c', 'ó', 'mo', ' est', 'ás', '?', ...
Observa algo interesante: el tokenizador de GPT-2 no está optimizado para español. La palabra «Hola» se parte en «H» + «ola», los caracteres acentuados se rompen en bytes (la ‘ó’ se parte en ‘ó’), y «cómo» necesita tres tokens. No es culpa del algoritmo BPE — es culpa de los datos de entrenamiento: GPT-2 se entrenó con texto casi exclusivamente en inglés.
La pregunta del millón: ¿podemos mejorar la tokenización en español?
Tuve la tentación de aplicar reglas de silabificación del español (consonantes, diptongos, hiatos…) para mejorar cómo se rompen las palabras. Pero resulta que BPE no funciona con reglas lingüísticas. Funciona con estadísticas. BPE no sabe qué es un diptongo ni le importa: lo único que hace es encontrar qué subcadenas aparecen juntas con más frecuencia y crear tokens para ellas.
Para demostrarlo, hice un experimento en tres fases:
Fase 1: Corpus literario clásico (fracaso didáctico)
Descargué 11 obras completas de Galdós, Unamuno, Baroja, Juan Ramón Jiménez, Echegaray y Clarín desde Project Gutenberg (~6.5 MB) y entrené un BPE. El resultado fue decepcionante: el corpus era demasiado pequeño (6.5 MB frente a los terabytes que necesita un tokenizador de verdad) y los libros tenían restos de cabeceras en inglés que contaminaron el vocabulario. Lección aprendida: la calidad y cantidad del corpus importan más que el algoritmo.
Fase 2: Wikipedia en español (donde la magia ocurre)
Descargué el dump completo de la Wikipedia en español desde Wikimedia (~4.8 GB comprimido) y extraje el texto a un archivo de 18 GB con 4.876.114 páginas. Usando ~500 MB de ese corpus, entrené un BPE con vocabulario de 50.000 tokens — el mismo orden de magnitud que GPT-2.
Fase 3: La comparativa definitiva
| Frase | tiktoken (inglés) | BPE Wikipedia ES | Mejora |
|---|---|---|---|
| «aprendizaje» | 5 tokens | 1 token | −80% |
| «procesamiento del lenguaje natural» | 11 tokens | 4 tokens | −64% |
| «inteligencia artificial» | 4 tokens | 2 tokens | −50% |
| «atención» | 3 tokens | 1 token | −67% |
| «construyamos» | 5 tokens | 3 tokens | −40% |
| «tokenización» | 4 tokens | 3 tokens | −25% |
| «Hola, ¿cómo estás?» | 11 tokens | 9 tokens | −18% |
| «fine tuning» | 2 tokens | 5 tokens | +150% (el inglés es mejor para inglés) |
El BPE entrenado con Wikipedia aprende la palabra «aprendizaje» como un solo token, «procesamiento» como uno solo, «atención» como uno solo. Con 500 MB de texto español, el BPE descubre naturalmente que las palabras largas del español merecen su propio token.
El contraejemplo perfecto: «fine tuning» sale mejor con tiktoken (2 tokens) que con el BPE español (5 tokens). ¿Por qué? Porque «fine» y «tuning» son palabras inglesas muy frecuentes en el corpus inglés de GPT-2, pero apenas aparecen en la Wikipedia española. Esto confirma que BPE aprende del idioma de su corpus, no de reglas universales.
La lección: si quisieras construir un LLM para español, entrenarías tu propio tokenizador BPE con cientos de GB de texto en español. El algoritmo descubre por sí solo que sufijos como «ción», «miento», «amiento», «mente», «ando» son unidades frecuentes que merecen su propio token. No necesita reglas — necesita datos.
Embeddings: de números a significado
Una vez que tenemos token IDs, necesitamos convertirlos en vectores que el modelo pueda procesar. Aquí entran los embeddings.
Un embedding es simplemente un vector de números reales que representa el significado de un token en un espacio de alta dimensionalidad. La capa de embedding no es más que una tabla lookup: para cada token ID, devuelve su vector correspondiente.
En GPT-2 small, cada token se convierte en un vector de 768 dimensiones. En GPT-3, son 12.288 dimensiones.
import torch
embedding_layer = torch.nn.Embedding(tokenizer.n_vocab, 768)
texto = "Hola mundo, construyamos un LLM desde cero"
tokens_tensor = torch.tensor(tokenizer.encode(texto))
embeddings = embedding_layer(tokens_tensor)
print(embeddings.shape) # (17 tokens, 768 dimensiones)
Resultado real:
Embeddings forma: torch.Size([17, 768])
Token 'H': [0.13, -0.40, 0.47, -0.45, ...] (28.23 de norma)
Cada uno de los 17 tokens de nuestra frase se convierte en un vector de 768 números aleatorios que, durante el entrenamiento, se irán refinando para capturar relaciones semánticas.
Codificación posicional: el orden importa
Los transformers (a diferencia de las RNNs) no tienen un sentido inherente del orden de las palabras. Necesitamos añadir explícitamente información sobre la posición de cada token. La implementación más simple suma la posición a una dimensión del embedding:
def positional_encoding(seq_len, d_model):
pos = torch.arange(seq_len).unsqueeze(1)
pe = torch.zeros(seq_len, d_model)
pe[:, 0] = pos.squeeze()
return pe
input_final = embeddings + positional_encoding(17, 768)
Resultado: el vector resultante tiene media ~0 y desviación estándar ~1 — exactamente lo que el transformer espera recibir.
Rendimiento en GTX 1070
Probemos qué tal se comporta nuestra GPU con una carga de trabajo realista de embeddings:
| Operación | Resultado |
|---|---|
| Batch | 1024 secuencias de 512 tokens |
| 50 iteraciones | 1.41 segundos |
| Tokens/segundo | 18.5 millones |
| VRAM usado | 1.688 MB (de 8.000) |
Conclusión: para la capa de embeddings, la GTX 1070 es más que suficiente. 18.5 millones de tokens por segundo y apenas 1.7 GB de VRAM. Esto significa que podemos trabajar con lotes grandes sin saturar la GPU — buena señal para cuando entrenemos nuestro propio modelo en artículos posteriores.
Resumen
| Concepto | Qué es | Dónde se usa |
|---|---|---|
| Tokenización | Texto → IDs numéricos | Entrada de cualquier LLM |
| BPE | Algoritmo de tokenización sub-palabra | GPT, LLaMA, Mistral |
| Embeddings | IDs → Vectores densos | Capa inicial del transformer |
| Codificación posicional | Añade información de orden | Se suma a los embeddings |
Punto clave: el tokenizador de GPT-2 no maneja bien el español — pero el problema son los datos de entrenamiento, no el algoritmo. Hemos demostrado que entrenando un BPE con 500 MB de Wikipedia en español, las palabras españolas se tokenizan mucho mejor (hasta un 80% menos de tokens).
En el próximo artículo implementaremos self-attention desde cero y veremos cómo el transformer empieza a «entender» las relaciones entre palabras ejecutando código real en nuestra GTX 1070.
📚 Fuentes y recursos
– Giles Thomas, Writing an LLM from scratch, parts 1–4 (CC BY 4.0) — https://www.gilesthomas.com/2024/12/llm-from-scratch-1
– Sebastian Raschka, Build a Large Language Model (from Scratch), Manning Publications
– OpenAI, tiktoken — https://github.com/openai/tiktoken
– Wikipedia en español, dump de Wikimedia — https://dumps.wikimedia.org/eswiki/
– Hugging Face, tokenizers — https://github.com/huggingface/tokenizers
– Código fuente de este artículo: /home/ia/projects/llm-from-scratch-lab/es01_tokenization_embeddings.py
– Tokenizador BPE español entrenado: /home/ia/projects/llm-from-scratch-lab/tokenizer_wiki_espanol.json
Deja una respuesta