Semana 4: Generando Texto por Muestreo
🎯 Objetivos de aprendizaje
Al final de esta semana podrás:
- Explicar la diferencia entre siempre elegir la siguiente palabra más probable y muestrear de la distribución.
- Usar
random.choicespara muestrear una siguiente palabra ponderada por probabilidades de bigrama. - Escribir una función
generate_text()que produzca una oración nueva, una palabra a la vez, a partir de la tabla de bigramas. - Hacer que la generación aleatoria sea reproducible con
random.seed, cuando eso sea útil.
Lección
Dos formas de elegir la "siguiente" palabra
Dado probs_table["the"] = {"cat": 0.35, "dog": 0.3, "mouse": 0.1, "mat": 0.15, ...}, hay dos estrategias distintas para elegir qué viene a continuación:
- Voraz (greedy): siempre toma la única palabra más probable (
"cat", con 35%). Esto es determinista — la misma entrada siempre produce la misma salida— y rápidamente se vuelve repetitivo, ya que la palabra de mayor probabilidad después de "cat" probablemente también sea siempre la misma. - Muestreo (sampling): elige una palabra al azar, pero ponderada por su probabilidad — de modo que
"cat"se elige alrededor del 35% de las veces,"dog"alrededor del 30%, y así sucesivamente, igual que lanzar un dado ponderado donde cada cara tiene un área de distinto tamaño, exactamente como muestrear de cualquier distribución discreta .
Esta semana usa el muestreo, porque es lo que hace que el texto generado varíe de una ejecución a otra — la misma razón por la que un modelo de lenguaje no te da la respuesta idéntica cada vez que le haces la misma pregunta.
Muestreo con random.choices
random.choices(population, weights) de Python implementa exactamente esto: dada una lista de resultados posibles y una lista de pesos correspondiente, devuelve un resultado elegido con probabilidad proporcional a su peso.
import random
def sample_next(word, probs_table):
if word not in probs_table:
return None # sin continuación conocida — callejón sin salida
next_words = list(probs_table[word].keys())
weights = list(probs_table[word].values())
return random.choices(next_words, weights=weights, k=1)[0]
sample_next("the", probs_table) # p. ej. "cat" — pero no siempre
weights no necesita sumar exactamente 1 para que random.choices funcione correctamente (normaliza internamente) — pero los nuestros ya lo hacen, ya que vinieron directamente del bigram_probabilities de la semana pasada.
Reproducibilidad con random.seed
La aleatoriedad es exactamente lo que queremos para tener variedad, pero hace que depurar y compartir resultados sea incómodo — "generó una oración rara" es difícil de investigar si no puedes reproducir exactamente la misma ejecución. random.seed(n) fija el generador de números aleatorios de Python en un punto de partida específico, de modo que cada llamada a random.choices(...) después de eso se vuelve determinista para esa ejecución:
random.seed(42)
print(generate_text(probs_table, "the")) # siempre el mismo resultado, cada vez que ejecutas esto
random.seed() # vuelve a aleatorizar, de regreso al comportamiento impredecible normal
Esta es una técnica general, no específica de modelos de lenguaje — cada vez que necesitas "aleatorio, pero reproducible para pruebas", random.seed es la herramienta.
Generar una oración completa
Empezando desde una palabra semilla, muestrea repetidamente la siguiente palabra y vuelve a introducirla como la nueva "palabra anterior" — un bucle, que se detiene ya sea al alcanzar una longitud fija o cuando sample_next llega a un callejón sin salida:
def generate_text(probs_table, start_word, max_words=10):
words = [start_word]
current = start_word
for _ in range(max_words - 1):
next_word = sample_next(current, probs_table)
if next_word is None:
break
words.append(next_word)
current = next_word
return " ".join(words)
generate_text(probs_table, "the")
# p. ej. "the cat sat on the rug" — variará entre ejecuciones
Este es el núcleo generativo de todo el modelo de lenguaje: nada más que muestreo ponderado repetido de una tabla construida a partir de conteo. Es un modelo de lenguaje real, funcional (aunque muy débil) — genuinamente la misma idea que el bucle de muestreo de siguiente-token dentro de modelos mucho más grandes, solo que estimada a partir de 20 oraciones de conteo en lugar de miles de millones de parámetros entrenados en enormes conjuntos de datos.
Generar varios candidatos a la vez
Como cada llamada a generate_text es aleatoria, generar varias oraciones y revisarlas es una forma natural de explorar lo que el modelo puede producir — el mismo patrón de "muestrea unos cuantos, elige el mejor" usado al trabajar con cualquier sistema generativo:
def generate_many(probs_table, start_word, n=5, max_words=10):
return [generate_text(probs_table, start_word, max_words) for _ in range(n)]
for sentence in generate_many(probs_table, "the", n=5):
print(sentence)
⚠️ Errores comunes
- Olvidar comprobar
Nonedesample_next. Sigenerate_textno comprobaraif next_word is None: break, fallaría la siguiente vez que intentara buscarNonecomo clave enprobs_table. - Asumir que
random.choicesnecesita pesos que sumen exactamente 1. No es así — normaliza cualquier peso que le des. Esto importa si alguna vez pasas conteos crudos en lugar de probabilidades ya normalizadas. - Olvidar que
random.seed()(sin argumento) vuelve a aleatorizar. Después de depurar con una semilla fija, olvidar volver a llamar arandom.seed()significa que cada llamada "aleatoria" posterior en esa sesión sigue siendo determinista, lo que puede parecer un error en código no relacionado. - Esperar una salida gramaticalmente perfecta. Un modelo bigrama no tiene ningún concepto de concordancia sujeto-verbo, significado a nivel de oración, ni nada más allá de "qué suele seguir a esta única palabra" — esto es una limitación genuina, no un error que arreglar, y es exactamente lo que las Preguntas Socráticas de abajo te piden notar directamente.
🧩 Retos
Llama a generate_text cinco veces con el mismo start_word. ¿Son idénticas las salidas? ¿Por qué sí o por qué no?
Llama a generate_text con un start_word que nunca aparezca como la primera palabra de un bigrama en ninguna parte del corpus (encontraste candidatos para esto en el Reto 3 de la semana pasada). ¿Qué ocurre, y por qué?
Modifica generate_text para que se detenga antes si la misma palabra se elige 3 veces seguidas (una protección simple contra bucles repetitivos). ¿Bajo qué condiciones del corpus crees que esto realmente podría ocurrir?
Escribe un sample_next_greedy(word, probs_table) que siempre elija la única siguiente palabra más probable en lugar de muestrear al azar. Ejecuta generate_text (usando esta versión voraz) cinco veces con la misma palabra inicial — ¿qué notas?
Llama a random.seed(1) antes de generar una oración, anota el resultado, y luego llama a random.seed(1) de nuevo antes de generar otra oración con los mismos argumentos. ¿Son idénticos los dos resultados? ¿Por qué?
Usando generate_many, genera 20 oraciones candidatas a partir de la misma palabra inicial, y luego imprime la que sea más larga (tenga más palabras).
🤔 Preguntas socráticas
- El muestreo introduce aleatoriedad a propósito. ¿Se te ocurre un caso de uso real para un modelo de lenguaje donde realmente quisieras el comportamiento voraz, siempre-la-misma-respuesta, en su lugar?
sample_nextdevuelveNonecuando la palabra actual no tiene continuación conocida. ¿Qué analogía del mundo real tiene este "callejón sin salida" — se parece más a que el modelo esté incierto, o a que el modelo literalmente nunca haya visto antes esta situación?- Este modelo bigrama solo puede generar secuencias de palabras que son estadísticamente plausibles dadas transiciones de una sola palabra — no tiene ningún concepto de que la oración "tenga sentido" en su conjunto. Genera varias oraciones y busca alguna que sea gramaticalmente extraña o sin sentido a pesar de que cada par individual de palabras sea localmente plausible. ¿Qué sugiere esto sobre los límites de mirar solo una palabra hacia atrás?
random.seedhace que un proceso aleatorio sea reproducible para depuración, pero las aplicaciones reales de modelos de lenguaje (como un chatbot) generalmente no fijan la semilla. ¿Por qué podría ser la elección correcta no fijarla ahí, aunque eso haga que los errores sean más difíciles de reproducir?
✅ Cuestionario semanal
✅ Cuestionario semanal
🎁 Bono: manejo de errores para una palabra inicial inválida
Disponible al aprobar el cuestionario de esta semana.