Normalizar recuentos de bigramas
Convierte los recuentos crudos de bigramas en distribuciones de probabilidad que suman 1.0 para cada palabra.
- Normalizar los recuentos crudos de bigramas en probabilidades dividiendo por el total de seguidores
- Comprender por qué se necesitan distribuciones de probabilidad para el muestreo ponderado
- Manejar casos límite: recuentos cero, palabras con un solo seguidor, claves faltantes
- Verificar que las probabilidades suman 1.0 para cada palabra
De recuentos a probabilidades
Los recuentos crudos te dicen que “the” → “cat” apareció 15 veces y “the” → “dog” apareció 5 veces. Pero para muestrear la siguiente palabra, necesitas probabilidades: “cat” debería elegirse el 75 % de las veces y “dog” el 25 %. Normalizar convierte los recuentos en una distribución donde todos los seguidores suman 1.0.
Las celdas siguientes reutilizan las funciones load_corpus, tokenize y build_bigrams de las lecciones 01 a 05. Cada página de lección inicia una sesión de Python nueva, así que ejecuta primero esta celda de configuración:
import csv
import string
from collections import defaultdict
with open("slm-corpus.csv", newline="") as f:
reader = csv.DictReader(f)
texts = [row["text"] for row in reader]
def load_corpus(path):
with open(path, newline="") as f:
reader = csv.DictReader(f)
return [row["text"] for row in reader]
def tokenize(text):
text = text.lower()
for char in string.punctuation:
text = text.replace(char, " ")
return text.split()
def build_bigrams(tokens):
bigrams = defaultdict(lambda: defaultdict(int))
for i in range(len(tokens) - 1):
bigrams[tokens[i]][tokens[i + 1]] += 1
return dict(bigrams)Conceptos clave
Normalizar con un bucle
Para cada palabra, suma los recuentos de sus seguidores y luego divide cada recuento por ese total:
def normalize_bigrams(bigrams):
normalized = {}
for word, followers in bigrams.items():
total = sum(followers.values())
normalized[word] = {w: c / total for w, c in followers.items()}
return normalizedAhora normalized["the"]["cat"] devuelve un float entre 0 y 1, la probabilidad de que “cat” siga a “the”.
Ejemplo
raw_bigrams = {"the": {"cat": 15, "dog": 5, "bird": 10}}
norm = normalize_bigrams(raw_bigrams)
print(norm["the"])
# {'cat': 0.5, 'dog': 0.1667, 'bird': 0.3333}Las probabilidades suman 1.0:
print(sum(norm["the"].values())) # 1.0Por qué importa la normalización para el muestreo
random.choices() necesita pesos que representen la probabilidad relativa. Si pasas recuentos crudos (15, 5, 10), funciona, pero tener las probabilidades correctas (0.5, 0.167, 0.333) hace que el modelo sea portable y comparable entre distintos tamaños de corpus.
import random
followers = list(norm["the"].keys())
weights = list(norm["the"].values())
next_word = random.choices(followers, weights=weights, k=1)[0]
print(f"Next word: {next_word}")Manejar casos límite
Algunas palabras no tienen seguidores (la última palabra del corpus, o palabras que solo aparecen al final de una oración). La tabla de bigramas no tendrá entradas para ellas:
def normalize_bigrams(bigrams):
normalized = {}
for word, followers in bigrams.items():
if not followers:
continue # skip words with no followers
total = sum(followers.values())
normalized[word] = {w: c / total for w, c in followers.items()}
return normalizedOmitir las entradas vacías evita errores de división por cero.
Un pipeline completo
Así encaja la normalización en el pipeline completo:
texts = load_corpus("slm-corpus.csv")
tokens = tokenize(" ".join(texts))
bigrams = build_bigrams(tokens)
model = normalize_bigrams(bigrams)
# Check a sample
print(f"Words in model: {len(model)}")
print(f"Followers of 'the': {list(model.get('the', {}).keys())[:5]}")Guardar el modelo
Es posible que quieras guardar la tabla de bigramas normalizada para reutilizarla. Como es un dict anidado de floats, json funciona bien:
import json
with open("bigram_model.json", "w") as f:
json.dump(model, f)
# Reload later
with open("bigram_model.json") as f:
model = json.load(f)Inténtalo
Construye y normaliza la tabla de bigramas, luego verifica:
- ¿Suman 1.0 las probabilidades de “the”?
- ¿Cuántas palabras tienen cero seguidores?
- ¿Cuál es la palabra más probable que siga a “the”?
model = normalize_bigrams(bigrams)
the_followers = model.get("the", {})
top_follower = max(the_followers, key=the_followers.get)
print(f"Most likely after 'the': '{top_follower}' ({the_followers[top_follower]:.3f})")Conclusiones clave
- La normalización convierte los recuentos crudos en probabilidades que suman 1.0 por palabra
random.choices()usa estas probabilidades como pesos para el muestreo ponderado- Omite las palabras sin seguidores para evitar la división por cero
- Guarda los modelos normalizados con
json.dump()para reutilizarlos entre scripts
Reto de práctica
Escribe una función bigram_stats(model) que imprima para cada palabra: la palabra, el número de seguidores y la siguiente palabra más probable. Limita la salida a las 10 palabras principales por recuento total de seguidores.
def bigram_stats(model, top_n=10):
words = sorted(model, key=lambda w: sum(model[w].values()), reverse=True)
for word in words[:top_n]:
followers = model[word]
total = sum(followers.values())
best = max(followers, key=followers.get)
print(f"'{word}': {len(followers)} followers, best=''{best}'' ({followers[best]:.3f})")1. ¿Por qué normalizar los recuentos de bigramas?
2. ¿Qué es P(palabra2 | palabra1) para un bigrama?
3. Si the aparece 1000 veces y (the, cat) aparece 50 veces, ¿qué es P(cat | the)?