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:

ModeloTokenizadorVocabulario
GPT-2BPE~50K tokens
GPT-3/4tiktoken (BPE)~100K tokens
BERTWordPiece~30K tokens
LLaMASentencePiece (BPE)~32K tokens
T5SentencePiece~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:

ModeloMax TokensUso típico
text-embedding-3-small8,191 tokensEmbeddings
text-embedding-3-large8,191 tokensEmbeddings
gpt-3.5-turbo16,385 tokensChat
gpt-4128,000 tokensChat (turbo)
claude-3200,000 tokensChat

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:

  1. Sub-palabras > palabras completas (maneja OOV)
  2. Límite de 8,191 tokens para OpenAI embeddings
  3. Menos tokens = menor costo
  4. Idiomas no-latinos usan más tokens

Recursos adicionales

  1. tiktoken GitHub - Tokenizador OpenAI
  2. BPE Paper - Neural Machine Translation of Rare Words with Subword Units
  3. WordPiece - Japanese and Korean Voice Search
  4. HuggingFace Tokenizers - Biblioteca de tokenizers
  5. 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