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

Tres enfoques de embedding: coocurrencia GloVe, subpalabras FastText, merges BPE

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

  1. Fácil. Ejecuta char_ngrams("playing") y char_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.
  2. Medio. Extiende learn_bpe para 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.
  3. 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

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).