Phase 05 - Lesson 04
GloVe, FastText y Embeddings de Subpalabras
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Word2Vec entrenó un embedding por palabra. GloVe factorizó la matriz de coocurrencia. FastText embebió las piezas. BPE tendió el puente hacia los transformers.
Tipo: Build Lenguajes: Python Prerrequisitos: Fase 5 · 03 (Word2Vec desde Cero) Tiempo: ~45 minutos
El Problema
Word2Vec dejó dos preguntas abiertas.
Primero, había una línea paralela de investigación que factorizaba directamente la matriz de coocurrencia (LSA, HAL) en lugar de hacer actualizaciones skip-gram en línea. ¿El enfoque iterativo de Word2Vec era fundamentalmente mejor, o la diferencia era un artefacto de cómo los dos métodos manejaban los conteos? GloVe respondió eso: la factorización de matrices con una función de pérdida cuidadosamente elegida iguala o supera a Word2Vec, y cuesta menos entrenar.
Segundo, ninguno de los métodos tenía una solución para palabras que nunca había visto. Zoomer-approved, dogecoin, cualquier nombre propio acuñado la semana pasada, cada forma flexionada de una raíz poco común. FastText lo arregló embebiendo n-gramas de caracteres: una palabra es la suma de sus partes, incluyendo morfemas, así que incluso las palabras fuera de vocabulario obtienen un vector sensato.
Tercero, una vez que llegaron los transformers, la pregunta cambió de nuevo. Los vocabularios a nivel de palabra alcanzan su límite alrededor de un millón de entradas; el lenguaje real es más abierto que eso. La codificación por pares de bytes (BPE) y sus variantes resolvieron esto aprendiendo un vocabulario de unidades de subpalabras frecuentes que lo cubre todo. Cada tokenizador moderno de cada LLM moderno es un tokenizador de subpalabras.
Esta lección recorre las tres y luego explica cuál usar y cuándo.
El Concepto
GloVe (Global Vectors). Construye la matriz de coocurrencia palabra-palabra X donde X[i][j] es con qué frecuencia la palabra j aparece en el contexto de la palabra i. Entrena vectores tales que v_i · v_j + b_i + b_j ≈ log(X[i][j]). Pondera la pérdida para que los pares frecuentes no dominen. Listo.
FastText. Una palabra es la suma de sus n-gramas de caracteres más la propia palabra. where se convierte en <wh, whe, her, ere, re>, <where>. El vector de la palabra es la suma de esos vectores componentes. Entrena como Word2Vec. Beneficio: las palabras no vistas (whereupon) se componen a partir de n-gramas conocidos.
BPE (Byte-Pair Encoding). Empieza con un vocabulario de bytes (o caracteres) individuales. Cuenta cada par adyacente en el corpus. Fusiona el par más frecuente en un nuevo token. Repite por k iteraciones. Resultado: un vocabulario de k + 256 tokens donde las secuencias frecuentes (ing, tion, the) son tokens únicos y las palabras poco comunes se descomponen en piezas familiares. Cada oración se tokeniza en algo.
Constrúyelo
GloVe: factoriza la matriz de coocurrencia
import numpy as np
from collections import Counter
def build_cooccurrence(docs, window=5):
pair_counts = Counter()
vocab = {}
for doc in docs:
for token in doc:
if token not in vocab:
vocab[token] = len(vocab)
for doc in docs:
indexed = [vocab[t] for t in doc]
for i, center in enumerate(indexed):
for j in range(max(0, i - window), min(len(indexed), i + window + 1)):
if i != j:
distance = abs(i - j)
pair_counts[(center, indexed[j])] += 1.0 / distance
return vocab, pair_counts
def glove_train(vocab, pair_counts, dim=16, epochs=100, lr=0.05, x_max=100, alpha=0.75, seed=0):
n = len(vocab)
rng = np.random.default_rng(seed)
W = rng.normal(0, 0.1, size=(n, dim))
W_tilde = rng.normal(0, 0.1, size=(n, dim))
b = np.zeros(n)
b_tilde = np.zeros(n)
for epoch in range(epochs):
for (i, j), x_ij in pair_counts.items():
weight = (x_ij / x_max) ** alpha if x_ij < x_max else 1.0
diff = W[i] @ W_tilde[j] + b[i] + b_tilde[j] - np.log(x_ij)
coef = weight * diff
grad_W_i = coef * W_tilde[j]
grad_W_tilde_j = coef * W[i]
W[i] -= lr * grad_W_i
W_tilde[j] -= lr * grad_W_tilde_j
b[i] -= lr * coef
b_tilde[j] -= lr * coef
return W + W_tilde
Dos engranajes en movimiento que vale la pena nombrar. La función de ponderación f(x) = (x/x_max)^alpha reduce el peso de los pares muy frecuentes (como (the, and)) para que no dominen la pérdida. El embedding final es la suma de las tablas W (centro) y W_tilde (contexto). Sumar ambas es un truco publicado que tiende a superar el uso de una sola.
FastText: embeddings conscientes de subpalabras
def char_ngrams(word, n_min=3, n_max=6):
wrapped = f"<{word}>"
grams = {wrapped}
for n in range(n_min, n_max + 1):
for i in range(len(wrapped) - n + 1):
grams.add(wrapped[i:i + n])
return grams
>>> char_ngrams("where")
{'<where>', '<wh', 'whe', 'her', 'ere', 're>', '<whe', 'wher', 'here', 'ere>', '<wher', 'where', 'here>'}
Cada palabra está representada por su conjunto de n-gramas (típicamente de 3 a 6 caracteres). El embedding de la palabra es la suma de sus embeddings de n-gramas. Para el entrenamiento skip-gram, inserta esto donde Word2Vec usaba un solo vector.
def fasttext_vector(word, ngram_table):
grams = char_ngrams(word)
vecs = [ngram_table[g] for g in grams if g in ngram_table]
if not vecs:
return None
return np.sum(vecs, axis=0)
Para una palabra no vista, aún obtienes un vector siempre que algunos de sus n-gramas sean conocidos. whereupon comparte <wh, her, ere y <where con where, así que las dos quedan cerca una de la otra.
BPE: vocabulario de subpalabras aprendido
def learn_bpe(corpus, k_merges):
vocab = Counter()
for word, freq in corpus.items():
tokens = tuple(word) + ("</w>",)
vocab[tokens] = freq
merges = []
for _ in range(k_merges):
pair_freq = Counter()
for tokens, freq in vocab.items():
for a, b in zip(tokens, tokens[1:]):
pair_freq[(a, b)] += freq
if not pair_freq:
break
best = pair_freq.most_common(1)[0][0]
merges.append(best)
new_vocab = Counter()
for tokens, freq in vocab.items():
new_tokens = []
i = 0
while i < len(tokens):
if i + 1 < len(tokens) and (tokens[i], tokens[i + 1]) == best:
new_tokens.append(tokens[i] + tokens[i + 1])
i += 2
else:
new_tokens.append(tokens[i])
i += 1
new_vocab[tuple(new_tokens)] = freq
vocab = new_vocab
return merges
def apply_bpe(word, merges):
tokens = list(word) + ["</w>"]
for a, b in merges:
new_tokens = []
i = 0
while i < len(tokens):
if i + 1 < len(tokens) and tokens[i] == a and tokens[i + 1] == b:
new_tokens.append(a + b)
i += 2
else:
new_tokens.append(tokens[i])
i += 1
tokens = new_tokens
return tokens
>>> corpus = Counter({"low": 5, "lower": 2, "newest": 6, "widest": 3})
>>> merges = learn_bpe(corpus, k_merges=10)
>>> apply_bpe("lowest", merges)
['low', 'est</w>']
La primera iteración fusiona el par adyacente más común. Después de suficientes iteraciones, las subcadenas frecuentes (low, est, tion) se convierten en tokens únicos y las palabras poco comunes se descomponen de forma limpia.
Los tokenizadores reales de GPT / BERT / T5 aprenden de 30 mil a 100 mil merges. Resultado: cualquier texto se tokeniza en una secuencia de longitud acotada de IDs conocidos, nunca hay OOV.
Úsalo
En la práctica, rara vez entrenas alguno de estos por tu cuenta. Cargas checkpoints preentrenados.
import fasttext.util
fasttext.util.download_model("en", if_exists="ignore")
ft = fasttext.load_model("cc.en.300.bin")
print(ft.get_word_vector("whereupon").shape)
print(ft.get_word_vector("zoomerapproved").shape)
Para la tokenización de subpalabras al estilo BPE en la era de los transformers:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("gpt2")
print(tok.tokenize("unbelievably tokenized"))
['un', 'bel', 'iev', 'ably', 'Ġtoken', 'ized']
El prefijo Ġ marca los límites de palabra (una convención de GPT-2). Cada tokenizador moderno es una variante de BPE, WordPiece (BERT) o SentencePiece (T5, LLaMA).
Cuándo elegir cuál
| Situación | Elige |
|---|---|
| Vectores de palabra preentrenados de propósito general, sin necesidad de tolerancia a OOV | GloVe 300d |
| Vectores de palabra preentrenados de propósito general, debe manejar errores de tipeo / neologismos / lenguas morfológicamente ricas | FastText |
| Cualquier cosa que entre a un transformer (entrenamiento o inferencia) | El tokenizador que vino con el modelo. Nunca lo cambies. |
| Entrenar tu propio modelo de lenguaje desde cero | Entrena primero un tokenizador BPE o SentencePiece sobre tu corpus |
| Clasificación de texto en producción con un modelo lineal | Aún TF-IDF. Lección 02. |
Entrégalo
Guarda como outputs/skill-tokenizer-picker.md:
---
name: tokenizer-picker
description: Pick a tokenization approach for a new language model or text pipeline.
version: 1.0.0
phase: 5
lesson: 04
tags: [nlp, tokenization, embeddings]
---
Given a task and dataset description, you output:
1. Tokenization strategy (word-level, BPE, WordPiece, SentencePiece, byte-level). One-sentence reason.
2. Vocabulary size target (e.g., 32k for an English-only LM, 64k-100k for multilingual).
3. Library call with the exact training command. Name the library. Quote the arguments.
4. One reproducibility pitfall. Tokenizer-model mismatch is the single most common silent production bug; call out which pair must be used together.
Refuse to recommend training a custom tokenizer when the user is fine-tuning a pretrained LLM. Refuse to recommend word-level tokenization for any model targeting production inference. Flag non-English / multi-script corpora as needing SentencePiece with byte fallback.
Ejercicios
- Fácil. Ejecuta
char_ngrams("playing")ychar_ngrams("played"). Calcula la superposición de Jaccard de los dos conjuntos de n-gramas. Deberías ver piezas compartidas sustanciales (pla,lay,play), que es la razón por la que FastText transfiere bien entre variantes morfológicas. - Medio. Extiende
learn_bpepara rastrear el crecimiento del vocabulario. Grafica los tokens-por-carácter-de-corpus en función del número de merges. Deberías ver una compresión rápida al principio, con asíntota cerca de ~2-3 caracteres por token. - Difícil. Entrena un BPE de mil merges sobre las obras completas de Shakespeare. Compara la tokenización de palabras comunes con la de nombres propios poco comunes. Mide el promedio de tokens por palabra antes y después. Escribe lo que te sorprendió.
Términos Clave
| Término | Lo que dice la gente | Lo que realmente significa |
|---|---|---|
| Matriz de coocurrencia | Tabla de frecuencia palabra-palabra | X[i][j] = con qué frecuencia la palabra j aparece en una ventana alrededor de la palabra i. |
| Subpalabra | Pieza de una palabra | Un n-grama de caracteres (FastText) o token aprendido (BPE/WordPiece/SentencePiece). |
| BPE | Byte-pair encoding | Fusión iterativa de los pares adyacentes más frecuentes hasta que el vocabulario alcanza el tamaño objetivo. |
| OOV | Fuera de vocabulario | Palabra que el modelo nunca ha visto. Word2Vec/GloVe fallan. FastText y BPE la manejan. |
| BPE a nivel de byte | BPE sobre bytes crudos | El esquema de GPT-2. El vocabulario empieza con 256 bytes, así que nada es nunca OOV. |
Lectura Adicional
- Pennington, Socher, Manning (2014). GloVe: Global Vectors for Word Representation — el artículo de GloVe, siete páginas, todavía la mejor derivación de la función de pérdida.
- Bojanowski et al. (2017). Enriching Word Vectors with Subword Information — FastText.
- Sennrich, Haddow, Birch (2016). Neural Machine Translation of Rare Words with Subword Units — el artículo que introdujo BPE en el NLP moderno.
- Resumen de tokenizadores de Hugging Face — cómo BPE, WordPiece y SentencePiece realmente difieren en la práctica.