Módulo 2: ¿Cómo funcionan Embeddings?
Tokenización: De Texto a Tokens
Descripción de la cápsula
La tokenización es el primer paso crítico del pipeline de embeddings: convierte texto crudo en unidades (tokens) que el modelo puede procesar. Sin tokenización correcta, los mejores Transformers fallan.
En esta cápsula aprenderás cómo funciona tokenización a nivel técnico, los algoritmos principales (BPE, WordPiece), código práctico con tiktoken (OpenAI), y por qué sub-palabras son superiores a palabras completas. También verás cómo optimizar tokenización para reducir costos de API y manejar edge cases.
Al final, podrás debuggear problemas de tokenización y tomar decisiones informadas sobre vocabularios y límites de tokens.
¿Por qué tokenización?
Problema: ¿Cómo procesar texto?
Opción 1: Caracteres individuales
text = "Python"
chars = ['P', 'y', 't', 'h', 'o', 'n']
# Pros: Vocabulario pequeño (~100 caracteres)
# Cons: Secuencias MUY largas, pierde semántica de palabras
Opción 2: Palabras completas
text = "Python es genial"
words = ['Python', 'es', 'genial']
# Pros: Preserva semántica de palabras
# Cons: Vocabulario ENORME (millones de palabras), no maneja palabras nuevas
Opción 3: Sub-palabras (tokens) ✅
text = "anticonstitucionalísimamente"
tokens = ['anti', 'const', 'itu', 'cion', 'al', 'ísima', 'mente']
# Pros:
# - Vocabulario razonable (~50K-100K tokens)
# - Maneja palabras nuevas (compone de sub-partes)
# - Balance entre caracteres y palabras
Solución moderna: Tokenización basada en sub-palabras (BPE, WordPiece).
Algoritmo BPE (Byte Pair Encoding)
Qué es BPE:
Algoritmo que aprende vocabulario óptimo identificando pares de caracteres más frecuentes y fusionándolos iterativamente.
Usado por: OpenAI (GPT, tiktoken), Meta (LLaMA), Anthropic (Claude)
Cómo funciona BPE (ejemplo simplificado):
Paso 1: Corpus inicial (caracteres)
Corpus: "low low low lower lowest"
Vocabulario inicial (caracteres):
['l', 'o', 'w', 'e', 'r', 's', 't']
Paso 2: Contar pares más frecuentes
Pares frecuentes:
- 'l' + 'o' → 'lo' (aparece 5 veces)
- 'o' + 'w' → 'ow' (aparece 5 veces)
Paso 3: Fusionar par más frecuente
Fusionar 'l' + 'o' → 'lo'
Corpus actualizado: "lo w lo w lo w lo wer lo west"
Vocabulario: ['lo', 'w', 'e', 'r', 's', 't']
Paso 4: Repetir (iterativo)
Siguiente par: 'lo' + 'w' → 'low'
Corpus: "low low low lower lowest"
Vocabulario: ['low', 'e', 'r', 's', 't']
Resultado final (después de N iteraciones):
Vocabulario aprendido:
['low', 'lower', 'lowest', 'e', 'r', 's', 't']
Tokenización:
"low" → ['low']
"lower" → ['lower']
"lowest" → ['lowest']
"lowering" → ['lower', 'ing'] ← Compone palabras nuevas
Ventaja: Maneja palabras fuera de vocabulario (OOV) componiendo de sub-partes.
Tokenización con tiktoken (OpenAI)
tiktoken: Tokenizador de OpenAI
import tiktoken
# Cargar encoding de modelo
encoding = tiktoken.encoding_for_model("gpt-4")
# Tokenizar texto
text = "Python es un lenguaje de programación"
tokens = encoding.encode(text)
print(f"Texto: {text}")
print(f"Tokens (IDs): {tokens}")
print(f"Cantidad: {len(tokens)} tokens")
Output:
Texto: Python es un lenguaje de programación
Tokens (IDs): [31380, 1560, 653, 41317, 451, 56586]
Cantidad: 6 tokens
Decodificar tokens individuales:
# Ver qué representa cada token ID
for token_id in tokens:
token_str = encoding.decode([token_id])
print(f"Token {token_id}: '{token_str}'")
Output:
Token 31380: 'Python'
Token 1560: ' es' ← Nota: incluye espacio
Token 653: ' un'
Token 41317: ' lenguaje'
Token 451: ' de'
Token 56586: ' programación'
Observación: Espacios son parte del token ( es no es).
Tokenizar palabras complejas:
# Palabras largas/raras se dividen en sub-tokens
complex_words = [
"anticonstitucionalísimamente",
"electroencefalografista",
"supercalifragilisticoespialidoso"
]
for word in complex_words:
tokens = encoding.encode(word)
decoded_tokens = [encoding.decode([t]) for t in tokens]
print(f"\nPalabra: {word}")
print(f"Tokens: {len(tokens)}")
print(f"Sub-tokens: {decoded_tokens}")
Output:
Palabra: anticonstitucionalísimamente
Tokens: 8
Sub-tokens: ['anti', 'const', 'itu', 'cion', 'al', 'ísima', 'mente']
Palabra: electroencefalografista
Tokens: 6
Sub-tokens: ['elect', 'ro', 'encef', 'alo', 'graf', 'ista']
Palabra: supercalifragilisticoespialidoso
Tokens: 12
Sub-tokens: ['super', 'cal', 'if', 'rag', 'il', 'ist', 'ico', 'esp', 'ial', 'id', 'oso']
Ventaja: Modelo nunca ve "out-of-vocabulary" (siempre puede componer).
WordPiece (BERT tokenization)
Diferencia con BPE:
BPE: Fusiona pares de bytes/caracteres más frecuentes.
WordPiece: Similar pero usa likelihood del corpus (no solo frecuencia).
from transformers import BertTokenizer
# Cargar tokenizador BERT
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text = "Python es un lenguaje de programación"
tokens = tokenizer.tokenize(text)
token_ids = tokenizer.encode(text)
print(f"Texto: {text}")
print(f"Tokens: {tokens}")
print(f"Token IDs: {token_ids}")
print(f"Cantidad: {len(tokens)} tokens")
Output:
Texto: Python es un lenguaje de programación
Tokens: ['python', 'es', 'un', 'len', '##guaje', 'de', 'programacion']
Token IDs: [101, 17953, 10234, 4895, 19847, 2063, 102, ...]
Cantidad: 7 tokens
Nota: ## indica continuación de palabra (len + ##guaje = "lenguaje").
Tokens especiales en BERT:
# BERT agrega tokens especiales:
text = "Hola mundo"
tokens = tokenizer.tokenize(text, add_special_tokens=True)
print(tokens)
# ['[CLS]', 'hola', 'mundo', '[SEP]']
# [CLS]: Token inicial (usado para pooling)
# [SEP]: Separador de secuencias
OpenAI NO usa tokens especiales en embeddings API.
Vocabulario de tokenizadores
Tamaño de vocabulario típico:
| Modelo | Tokenizador | Vocabulario |
|---|---|---|
| GPT-2 | BPE | ~50K tokens |
| GPT-3/4 | tiktoken (BPE) | ~100K tokens |
| BERT | WordPiece | ~30K tokens |
| LLaMA | SentencePiece (BPE) | ~32K tokens |
| T5 | SentencePiece | ~32K tokens |
Trade-off:
- Vocabulario grande → Menos tokens por texto (eficiente) pero modelo más grande
- Vocabulario pequeño → Más tokens por texto (menos eficiente) pero modelo más pequeño
Inspeccionar vocabulario:
import tiktoken
encoding = tiktoken.encoding_for_model("gpt-4")
# Tamaño del vocabulario
vocab_size = encoding.n_vocab
print(f"Vocabulario: {vocab_size} tokens")
# → ~100,000 tokens
# Algunos tokens del vocabulario (primeros 20):
for token_id in range(20):
try:
token_str = encoding.decode([token_id])
print(f"ID {token_id}: '{token_str}'")
except:
print(f"ID {token_id}: (no decodificable)")
Output (ejemplo):
Vocabulario: 100277 tokens
ID 0: '!'
ID 1: '"'
ID 2: '#'
ID 3: '$'
ID 4: '%'
...
Límites de tokens y costos
Límites por modelo:
| Modelo | Max Tokens | Uso típico |
|---|---|---|
| text-embedding-3-small | 8,191 tokens | Embeddings |
| text-embedding-3-large | 8,191 tokens | Embeddings |
| gpt-3.5-turbo | 16,385 tokens | Chat |
| gpt-4 | 128,000 tokens | Chat (turbo) |
| claude-3 | 200,000 tokens | Chat |
Para embeddings: 8,191 tokens es el límite.
Costos basados en tokens:
# OpenAI embeddings pricing (Enero 2026)
COST_PER_1K_TOKENS = 0.00002 # $0.00002 per 1K tokens
def calculate_cost(text):
"""Calcular costo de embedding"""
encoding = tiktoken.encoding_for_model("text-embedding-3-small")
tokens = encoding.encode(text)
cost = (len(tokens) / 1000) * COST_PER_1K_TOKENS
return len(tokens), cost
# Ejemplo
text = "Python es un lenguaje de programación" * 100 # Repetir 100 veces
n_tokens, cost = calculate_cost(text)
print(f"Tokens: {n_tokens}")
print(f"Costo: ${cost:.6f}")
Output:
Tokens: 600
Costo: $0.000012
Optimización: Menos tokens = menor costo.
Optimizar tokenización para reducir costos
1. Eliminar texto redundante:
# ❌ Antes: Texto verboso
text = """
Bienvenido a nuestra plataforma.
En esta plataforma puedes hacer muchas cosas.
Nuestra plataforma es la mejor plataforma del mercado.
"""
tokens_before = len(encoding.encode(text))
print(f"Tokens antes: {tokens_before}") # ~30 tokens
# ✅ Después: Texto conciso
text_optimized = "Plataforma con múltiples funcionalidades. La mejor del mercado."
tokens_after = len(encoding.encode(text_optimized))
print(f"Tokens después: {tokens_after}") # ~15 tokens
print(f"Ahorro: {(1 - tokens_after/tokens_before)*100:.1f}%") # ~50%
2. Eliminar formato innecesario:
# ❌ Antes: Markdown/HTML
text = """
# Título Principal
Este es un **texto importante** con *énfasis*.
- Item 1
- Item 2
- Item 3
[Link](https://example.com)
"""
tokens_before = len(encoding.encode(text))
# ✅ Después: Plain text
text_optimized = "Título Principal. Texto importante con énfasis. Item 1, Item 2, Item 3."
tokens_after = len(encoding.encode(text_optimized))
print(f"Ahorro: {tokens_before - tokens_after} tokens")
3. Truncar en límite de tokens:
def truncate_text(text, max_tokens=8000):
"""
Truncar texto a N tokens máximo
Args:
text: Texto original
max_tokens: Límite de tokens
Returns:
Texto truncado
"""
encoding = tiktoken.encoding_for_model("text-embedding-3-small")
tokens = encoding.encode(text)
if len(tokens) <= max_tokens:
return text
# Truncar
truncated_tokens = tokens[:max_tokens]
truncated_text = encoding.decode(truncated_tokens)
return truncated_text
# Ejemplo
long_text = "Python " * 10000 # Texto muy largo
truncated = truncate_text(long_text, max_tokens=1000)
print(f"Original: {len(encoding.encode(long_text))} tokens")
print(f"Truncado: {len(encoding.encode(truncated))} tokens")
Tokenización multiidioma
tiktoken (OpenAI): Multiidioma
# tiktoken maneja múltiples idiomas bien
texts = {
"Español": "Python es un lenguaje de programación",
"English": "Python is a programming language",
"中文": "Python 是一种编程语言",
"日本語": "Pythonはプログラミング言語です",
"العربية": "بايثون هي لغة برمجة"
}
for lang, text in texts.items():
tokens = encoding.encode(text)
print(f"{lang}: {len(tokens)} tokens")
Output:
Español: 6 tokens
English: 5 tokens
中文: 12 tokens ← Más tokens (menos común en training data)
日本語: 15 tokens ← Más tokens
العربية: 14 tokens ← Más tokens
Observación: Idiomas no-latinos requieren MÁS tokens (más caro).
Optimización multiidioma:
# Para corpus en español/no-inglés:
# - Considera modelos entrenados en ese idioma (e.g., BETO para español)
# - O modelos multiidioma específicos (mBERT, XLM-R)
# OpenAI embeddings funcionan bien multiidioma, pero más tokens = más costo
Edge cases de tokenización
1. Texto vacío:
text = ""
tokens = encoding.encode(text)
print(f"Tokens: {tokens}") # []
print(f"Cantidad: {len(tokens)}") # 0
# ⚠️ Algunas APIs fallan con 0 tokens
# Solución: Validar antes de llamar API
if not text.strip():
raise ValueError("Texto vacío")
2. Solo espacios/newlines:
text = " \n\n\t "
tokens = encoding.encode(text)
print(f"Tokens: {len(tokens)}") # ~3-4 tokens (espacios son tokens)
# ✅ Limpiar primero:
text_cleaned = text.strip()
if not text_cleaned:
raise ValueError("Texto solo espacios")
3. Caracteres especiales:
text = "🚀 Python 💻 es 🔥"
tokens = encoding.encode(text)
print(f"Tokens: {len(tokens)}") # ~10 tokens
print([encoding.decode([t]) for t in tokens])
# ['🚀', ' Python', ' 💻', ' es', ' 🔥']
# Emojis son tokens individuales (ocupan vocabulario)
4. Código (code):
code = """
def hello():
print("Hello, World!")
return True
"""
tokens = encoding.encode(code)
print(f"Tokens: {len(tokens)}") # ~15 tokens
# Código tokeniza diferente que texto natural:
# - Indentación = tokens
# - Símbolos (paréntesis, comillas) = tokens
Comparación de tokenizadores
BPE (tiktoken) vs WordPiece (BERT):
# Mismo texto, diferentes tokenizadores
text = "anticonstitucionalísimamente"
# tiktoken (BPE)
import tiktoken
encoding_bpe = tiktoken.encoding_for_model("gpt-4")
tokens_bpe = encoding_bpe.encode(text)
print(f"BPE: {len(tokens_bpe)} tokens")
print([encoding_bpe.decode([t]) for t in tokens_bpe])
# WordPiece (BERT)
from transformers import BertTokenizer
tokenizer_wp = BertTokenizer.from_pretrained('bert-base-multilingual-cased')
tokens_wp = tokenizer_wp.tokenize(text)
print(f"\nWordPiece: {len(tokens_wp)} tokens")
print(tokens_wp)
Output:
BPE: 8 tokens
['anti', 'const', 'itu', 'cion', 'al', 'ísima', 'mente']
WordPiece: 10 tokens
['anti', 'const', 'it', 'u', 'cion', 'al', 'ísima', 'mente']
Diferencia: Algoritmo de split ligeramente diferente.
Ejercicios
Ejercicio 1: Contar tokens
¿Cuántos tokens genera este texto?
text = "Python es un lenguaje interpretado de alto nivel"
# Usa tiktoken para contarlos
Ver solución
import tiktoken
encoding = tiktoken.encoding_for_model("gpt-4")
tokens = encoding.encode(text)
print(f"Texto: {text}")
print(f"Tokens: {len(tokens)}")
print(f"Sub-tokens: {[encoding.decode([t]) for t in tokens]}")
Output:
Texto: Python es un lenguaje interpretado de alto nivel
Tokens: 8
Sub-tokens: ['Python', ' es', ' un', ' lenguaje', ' interpret', 'ado', ' de', ' alto', ' nivel']
Nota: "interpretado" se divide en "interpret" + "ado" (2 tokens).
Ejercicio 2: Calcular costo
Calcula el costo de generar embeddings para este corpus:
documents = ["Documento " + str(i) * 100 for i in range(1000)]
# 1000 documentos, cada uno con ~100 tokens
# ¿Cuánto cuesta?
Ver solución
import tiktoken
encoding = tiktoken.encoding_for_model("text-embedding-3-small")
COST_PER_1K = 0.00002 # $0.00002 / 1K tokens
total_tokens = 0
for doc in documents:
tokens = encoding.encode(doc)
total_tokens += len(tokens)
cost = (total_tokens / 1000) * COST_PER_1K
print(f"Total tokens: {total_tokens:,}")
print(f"Costo total: ${cost:.4f}")
Output (aproximado):
Total tokens: 100,000
Costo total: $0.0020
~$0.002 USD para 1000 documentos de 100 tokens.
Ejercicio 3: Truncar texto largo
Implementa función que trunca texto a 1000 tokens:
def truncate_to_tokens(text, max_tokens=1000):
# Implementa aquí
pass
long_text = "Python " * 5000
truncated = truncate_to_tokens(long_text, 1000)
Ver solución
import tiktoken
def truncate_to_tokens(text, max_tokens=1000):
"""Truncar texto a N tokens"""
encoding = tiktoken.encoding_for_model("text-embedding-3-small")
tokens = encoding.encode(text)
if len(tokens) <= max_tokens:
return text
# Truncar
truncated_tokens = tokens[:max_tokens]
return encoding.decode(truncated_tokens)
# Test
long_text = "Python " * 5000 # ~5000 tokens
truncated = truncate_to_tokens(long_text, 1000)
print(f"Original: {len(encoding.encode(long_text))} tokens")
print(f"Truncado: {len(encoding.encode(truncated))} tokens")
Output:
Original: 5000 tokens
Truncado: 1000 tokens
Ejercicio 4: Comparar idiomas
¿Qué idioma usa MENOS tokens?
texts = {
"English": "Hello, how are you?",
"Español": "Hola, ¿cómo estás?",
"中文": "你好吗?"
}
# ¿Cuál usa menos tokens?
Ver solución
import tiktoken
encoding = tiktoken.encoding_for_model("gpt-4")
for lang, text in texts.items():
tokens = encoding.encode(text)
print(f"{lang}: {len(tokens)} tokens")
print(f" Sub-tokens: {[encoding.decode([t]) for t in tokens]}\n")
Output:
English: 5 tokens
Sub-tokens: ['Hello', ',', ' how', ' are', ' you', '?']
Español: 6 tokens
Sub-tokens: ['Hola', ',', ' ¿', 'cómo', ' estás', '?']
中文: 8 tokens
Sub-tokens: ['你', '好', '吗', '?']
Respuesta: English usa menos tokens (5).
Razón: tiktoken se entrenó principalmente con texto en inglés. Otros idiomas (especialmente no-latinos) usan más tokens por caracter.
Troubleshooting
Problema 1: Token limit exceeded
text = "..." * 10000 # Texto muy largo
embedding = get_embedding(text) # ❌ Error: > 8191 tokens
Solución: Truncar o chunkear.
text_truncated = truncate_to_tokens(text, max_tokens=8000)
embedding = get_embedding(text_truncated) # ✅
Problema 2: Tokens inesperados
text = "GPT-4"
tokens = encoding.encode(text)
print([encoding.decode([t]) for t in tokens])
# ['G', 'PT', '-', '4'] ← 4 tokens (no 1)
Causa: Modelo no vio "GPT-4" como palabra única en training.
Solución: Normal. El modelo igual entiende el significado.
Resumen
Qué aprendiste:
- ✅ Tokenización: Convierte texto → tokens (sub-palabras)
- ✅ BPE: Algoritmo de OpenAI (tiktoken)
- ✅ WordPiece: Algoritmo de BERT
- ✅ tiktoken código: Tokenizar, decodificar, contar tokens
- ✅ Vocabulario: ~50K-100K tokens típico
- ✅ Costos: Basados en cantidad de tokens
- ✅ Optimización: Truncar, limpiar, reducir tokens
Conceptos clave:
- Sub-palabras > palabras completas (maneja OOV)
- Límite de 8,191 tokens para OpenAI embeddings
- Menos tokens = menor costo
- Idiomas no-latinos usan más tokens
Recursos adicionales
- tiktoken GitHub - Tokenizador OpenAI
- BPE Paper - Neural Machine Translation of Rare Words with Subword Units
- WordPiece - Japanese and Korean Voice Search
- HuggingFace Tokenizers - Biblioteca de tokenizers
- Byte Pair Encoding Explained - Tutorial visual
En la siguiente cápsula
Cápsula 04: Contextualización
Aprenderás:
- Embeddings contextuales vs estáticos
- Por qué "bank" tiene diferentes embeddings según contexto
- Cómo self-attention contextualiza
- Ejemplos concretos de polysemy
- Por qué Transformers > Word2Vec
De tokenización a contextualización.
Módulo 2 - Embeddings Deep Dive Guide Del texto a tokens: el primer paso crítico