Antes de empezar
Varios ejercicios usan lo que construiste en la teoría: modelo (el mini-GPT ya entrenado), generar, mostrar, codificar, decodificar, T, V, recortar, y para el LLM tok, llm y preguntar. Abre un cuaderno nuevo, pega y ejecuta en orden los bloques de la ficha Código (el entrenamiento tarda un par de minutos en una CPU y Qwen pesa casi 1 GB) y después trabaja debajo. Intenta cada ejercicio antes de abrir la respuesta.
Ejercicios
1. La regla de la cadena a mano
Un modelo de lenguaje por caracteres da estas probabilidades: el primer carácter es «s» con probabilidad 0,2; después de «s», viene «o» con 0,5; después de «so», viene «l» con 0,4.
- ¿Qué probabilidad le da el modelo a la palabra «sol»?
- ¿Cuál es la pérdida promedio por carácter (entropía cruzada, con logaritmo natural)?
- ¿Y la perplejidad? Interprétala.
Respuesta
- Por la regla de la cadena: 0,2 × 0,5 × 0,4 = 0,04.
- (−ln 0,2 − ln 0,5 − ln 0,4) / 3 = (1,609 + 0,693 + 0,916) / 3 ≈ 1,073.
- e¹·⁰⁷³ ≈ 2,92. Es lo mismo que (1 / 0,04)^(1/3): en promedio, es como si el modelo dudara entre unas 3 opciones igual de probables en cada carácter.
probs = torch.tensor([0.2, 0.5, 0.4])
perdida = -probs.log().mean()
print(f"P(sol) = {probs.prod():.2f}")
print(f"pérdida = {perdida:.3f} perplejidad = {perdida.exp():.2f}")
P(sol) = 0.04
pérdida = 1.073 perplejidad = 2.92
2. La temperatura a mano
Un modelo da los logits 2, 1 y 0 a tres tokens. Calcula las probabilidades con temperatura 1, 0,5 y 2 (con calculadora: e² ≈ 7,389 y e ≈ 2,718). ¿Qué pasa con la probabilidad del favorito?
Respuesta
- T = 1: e², e¹, e⁰ = 7,389, 2,718 y 1; suman 11,107. Probabilidades: 0,665, 0,245 y 0,090.
- T = 0,5: los logits se vuelven 4, 2 y 0: 54,60, 7,389 y 1, que suman 62,99. Probabilidades: 0,867, 0,117 y 0,016.
- T = 2: los logits se vuelven 1, 0,5 y 0: 2,718, 1,649 y 1, que suman 5,367. Probabilidades: 0,506, 0,307 y 0,186.
Con temperatura baja el favorito se lleva casi todo (de 0,67 a 0,87); con temperatura alta las tres opciones se acercan. El orden nunca cambia.
z = torch.tensor([2.0, 1.0, 0.0])
for temp in (1, 0.5, 2):
print(f"T = {temp}:", F.softmax(z / temp, dim=0).numpy().round(3))
T = 1: [0.665 0.245 0.09 ]
T = 0.5: [0.867 0.117 0.016]
T = 2: [0.506 0.307 0.186]
3. Top-k y top-p
Cuatro tokens tienen probabilidades 0,4, 0,3, 0,2 y 0,1. ¿Qué tokens sobreviven y con qué probabilidad, después de renormalizar, con top-k = 3? ¿Y con top-p = 0,6? ¿Y con top-p = 0,95? Compruébalo con recortar.
Respuesta
- top-k = 3: los tres primeros, que suman 0,9: 0,4/0,9, 0,3/0,9 y 0,2/0,9 = 0,444, 0,333 y 0,222.
- top-p = 0,6: el primero solo (0,4) no llega a 0,6, así que entra el segundo (0,7 ya pasa). Quedan 0,571 y 0,429.
- top-p = 0,95: los tres primeros suman 0,9, que no llega a 0,95: entran los cuatro, sin cambios.
probs = torch.tensor([0.4, 0.3, 0.2, 0.1])
print("top-k = 3: ", recortar(probs, k=3).numpy().round(3))
print("top-p = 0.6: ", recortar(probs, p=0.6).numpy().round(3))
print("top-p = 0.95:", recortar(probs, p=0.95).numpy().round(3))
top-k = 3: [0.444 0.333 0.222 0. ]
top-p = 0.6: [0.571 0.429 0. 0. ]
top-p = 0.95: [0.4 0.3 0.2 0.1]
4. Tres temperaturas en el mini-GPT
Genera 100 caracteres con el mini-GPT a partir de «Dijo Sancho» con temperatura 0,3, 1 y 1,6, con la misma semilla. Describe cada texto: ¿cuál repite más?, ¿cuál tiene más palabras que no existen?
Respuesta
for temp in (0.3, 1.0, 1.6):
print(f"--- temperatura {temp}")
mostrar(generar(modelo, "Dijo Sancho", n=100, temperatura=temp))
--- temperatura 0.3
Dijo Sancho—, que es lo que en el cual y le suele deseo de aquella venta
y la presententa de la cabeza de la ma
--- temperatura 1.0
Dijo Sancho— volviéndose al cabra que ayude sea la vala otra, que dicho:
no aun espavio de tus bienes, secretar
--- temperatura 1.6
Dijo Sanchombre Cautilo MañuchaC. Y uiey ques7ella Caspío, asido, donde:
briéndolo, vior Aprumente: Dígüecido e
- Con 0,3 el texto es casi codicioso: casi todas las palabras existen y son de las más comunes («que», «de la», «aquella venta», «la cabeza»), pero se acerca a las repeticiones que viste con la elección codiciosa («de la cabeza de la ma…»).
- Con 1 hay más variedad y más palabras inventadas («espavio», «vala»), aunque la mayoría existen.
- Con 1,6 casi nada es una palabra («Sanchombre», «uiey», «ques7ella»): aparecen mayúsculas fuera de lugar y hasta un número. Las opciones raras ganaron demasiada probabilidad.
5. Más allá de la ventana
Este código le pasa al mini-GPT un texto de 100 caracteres de una sola vez. Ejecútalo, lee el error y explica qué pasó. ¿Por qué generar no tiene este problema aunque escriba textos más largos?
largo = torch.tensor([codificar("En un lugar de la Mancha, " * 4)])
print(largo.shape)
logits = modelo(largo)
torch.Size([1, 104])
Traceback (most recent call last):
...
IndexError: index out of range in self
Respuesta
El modelo tiene una tabla posicion con un vector para cada una de las T = 64 posiciones. Con 104 caracteres, pide el vector de la posición 64, 65… que no existen, y nn.Embedding da IndexError. Esa es la ventana de contexto: el modelo, literalmente, no tiene cómo representar más de 64 posiciones. generar le pasa siempre ids[:, -T:], los últimos 64 caracteres, y olvida los anteriores. Los LLM hacen lo mismo con su ventana (32 768 tokens en Qwen2.5-0.5B).
logits = modelo(largo[:, -T:])
print(logits.shape)
torch.Size([1, 64, 92])
6. Contar los parámetros de un bloque
Con d = 96, cuenta a mano los parámetros de un Bloque del mini-GPT (pesos y sesgos de cada nn.Linear, y los dos de cada nn.LayerNorm). Después compruébalo y calcula qué parte del modelo está en los cuatro bloques.
Respuesta
qkv: 96 × 288 + 288 = 27 936.salida: 96 × 96 + 96 = 9312.- MLP: 96 × 384 + 384 = 37 248, y 384 × 96 + 96 = 36 960.
- Dos
LayerNorm: 2 × (96 + 96) = 384.
Total: 111 840 por bloque, unos 12 d². Los cuatro bloques son el 95 % del modelo; los embeddings y la cabeza de salida son poco, porque el vocabulario de caracteres es chico. En un LLM con 150 000 tokens, la tabla de embeddings ya pesa mucho más.
bloque = sum(p.numel() for p in modelo.bloques[0].parameters())
total = sum(p.numel() for p in modelo.parameters())
print(f"Un bloque: {bloque} cuatro: {4 * bloque / total:.0%}")
Un bloque: 111840 cuatro: 95%
7. Tres prompts, diez casos
Escribimos diez frases cortas de opinión (inventadas para este ejercicio, cinco positivas y cinco negativas, algunas difíciles). Compara tres prompts para clasificarlas con Qwen:
- A: solo la pregunta «¿Esta opinión es positiva o negativa?» y la frase.
- B: lo mismo, más «Responde solo con una palabra: positiva o negativa.»
- C: como B, pero con dos ejemplos resueltos antes (few-shot).
Cuenta una respuesta como acierto si menciona la etiqueta correcta y no la otra. ¿Qué prompt gana? ¿En qué frases fallan?
Respuesta
casos = [
("La comida llegó caliente y muy rica.", "positiva"),
("El profe explica tan bien que da gusto ir a clase.", "positiva"),
("No esperaba mucho, pero me encantó.", "positiva"),
("Barato y funciona, ¿qué más se puede pedir?", "positiva"),
("Lo volvería a comprar sin dudar.", "positiva"),
("Tardaron dos horas y la pizza llegó fría.", "negativa"),
("Se rompió a la semana de usarlo.", "negativa"),
("Qué maravilla: otra vez sin internet todo el día.", "negativa"),
("No lo recomiendo a nadie.", "negativa"),
("Bonito por fuera, pero no sirve para nada.", "negativa"),
]
pregunta = "¿Esta opinión es positiva o negativa?"
formato = "Responde solo con una palabra: positiva o negativa."
ejemplos = ("Opinión: Me encantó la película.\nRespuesta: positiva\n"
"Opinión: El hotel estaba sucio.\nRespuesta: negativa\n")
prompts = {
"A": lambda f: f"{pregunta}\n{f}",
"B": lambda f: f"{pregunta} {formato}\n{f}",
"C": lambda f: f"{pregunta} {formato}\n{ejemplos}"
f"Opinión: {f}\nRespuesta:",
}
def acierta(respuesta, etiqueta):
r = respuesta.lower()
otra = "negativa" if etiqueta == "positiva" else "positiva"
return etiqueta in r and otra not in r
for nombre, armar in prompts.items():
marcas = [acierta(preguntar(armar(f), max_tokens=10), e)
for f, e in casos]
fallos = [i for i, ok in enumerate(marcas) if not ok]
print(f"Prompt {nombre}: {sum(marcas)}/10 falla en {fallos}")
Prompt A: 5/10 falla en [1, 5, 6, 7, 9]
Prompt B: 9/10 falla en [7]
Prompt C: 9/10 falla en [7]
Pedir el formato ayuda mucho: el prompt A acierta solo 5 de 10 y los otros dos, 9. Sin esa instrucción, el modelo empieza a explicar en lugar de responder y, con 10 tokens, la etiqueta no alcanza a salir («La opinión que proporcionaste parece estar en un estado…»). La frase irónica (la 7, «Qué maravilla: otra vez sin internet») es la más difícil: tiene palabras positivas y un sentido negativo. Diez casos son muy pocos para decidir entre dos prompts que empatan; para eso harían falta muchos más.
8. Cazar una alucinación
Hazle a Qwen preguntas sobre datos concretos que puedas comprobar (autores, fechas, lugares) hasta encontrar una respuesta falsa. Explica por qué el modelo la dio y cómo la verificaste.
Respuesta
Un ejemplo con una novela boliviana clásica, Juan de la Rosa, de Nataniel Aguirre (publicada en 1885):
mostrar(preguntar("¿Quién escribió la novela boliviana «Juan de la "
"Rosa»? Responde en una frase."))
La novela boliviana "Juan de la Rosa" fue escrita por el autor boliviano
José María Vargas.
La respuesta es falsa y suena segura: la escribió Nataniel Aguirre. El modelo armó un nombre con forma de autor latinoamericano («José María Vargas»), que es un patrón muy probable después de «fue escrita por el autor boliviano», mientras que el dato correcto, poco frecuente en sus datos, no ganó. Se verifica en una fuente confiable (una enciclopedia, el catálogo de una biblioteca, un libro de literatura boliviana), no preguntándole otra vez al mismo modelo. Si tu respuesta salió correcta, prueba con datos más raros: un modelo tan chico se equivoca seguido.
Mini-proyecto: un asistente que lee un documento
Vas a armar un asistente que responde preguntas sobre un documento corto, poniendo el documento dentro del prompt, y a medir cuántas acierta.
El documento. Usa uno corto y tuyo: las reglas de tu colegio, la descripción de un club, las instrucciones de un juego. De referencia, aquí va uno con datos reales de esta ruta de aprendizaje:
documento = """La ruta de aprendizaje de la OBIA tiene 30 temas en seis fases.
Fase 1, Programar con datos: temas 1 a 5, desde Python hasta NumPy,
Pandas y matemáticas con código.
Fase 2, Aprender de ejemplos: temas 6 a 11. El tema 6 es regresión
lineal y descenso de gradiente; el 11, árboles de decisión y ensambles.
Fase 3, Más modelos, tiempo y estructura: temas 12 a 15 (máquinas de
vectores de soporte, series temporales, clustering y reducción de
dimensiones).
Fase 4, Redes neuronales: temas 16 a 20. El tema 18 presenta PyTorch y
el 20, las redes convolucionales.
Fase 5, Lenguaje y atención: temas 21 a 25. El tema 23 explica la
atención y los transformers; el 24, los modelos de lenguaje.
Fase 6, Ver, oír y crear: temas 26 a 30. El tema 28 trata de audio y el
30 es el proyecto final, junto con la IA responsable.
Cada tema tiene cuatro fichas: Resumen, Teoría, Código y Ejercicios."""
Pasos.
- Escribe 15 preguntas sobre tu documento, cada una con la palabra o el número que debe aparecer en una respuesta correcta (si vale escribirlo de dos formas, como «seis» o «6», acepta las dos). Incluye dos o tres cuya respuesta no esté en el documento.
- Arma un mensaje de sistema que diga «responde solo con la información del documento y, si no está, di exactamente: No está en el documento», y pon el documento y la pregunta en el mensaje del usuario.
- Genera las 15 respuestas de forma codiciosa y arma una tabla: pregunta, respuesta y acierto (sí o no). Calcula el porcentaje de aciertos.
- Haz las mismas preguntas sin el documento. ¿Cuántas acierta?
- Mira los fallos: ¿se inventó algo?, ¿confundió dos números parecidos?, ¿respondió algo que no estaba? Cambia el prompt para corregir uno y vuelve a medir.
Resultados de referencia, para comprobar los tuyos
Este código hace los pasos 1 a 4 con el documento de la ruta.
preguntas = [
("¿Cuántos temas tiene la ruta?", "30"),
("¿En cuántas fases se divide la ruta?", "seis|6"),
("¿Cómo se llama la fase 2?", "aprender de ejemplos"),
("¿Qué temas forman la fase 4?", "16"),
("¿De qué trata el tema 6?", "regresión lineal"),
("¿En qué tema se presenta PyTorch?", "18"),
("¿Qué tema explica la atención y los transformers?", "23"),
("¿En qué fase está el tema de clustering?", "3"),
("¿Cómo se llama la fase 6?", "ver, oír y crear"),
("¿Qué tema es el proyecto final?", "30"),
("¿De qué trata el tema 28?", "audio"),
("¿Cuántas fichas tiene cada tema?", "cuatro|4"),
("¿Qué tema trata de árboles de decisión?", "11"),
("¿Quién escribió el tema 12?", "no está en el documento"),
("¿En qué ciudad se dan las clases?", "no está en el documento"),
]
sistema = ("Responde en una frase corta, solo con la información del "
"documento. Si la respuesta no está en el documento, di "
"exactamente: No está en el documento.")
def evaluar(con_documento):
aciertos = 0
for i, (q, esperado) in enumerate(preguntas, 1):
texto = f"Documento:\n{documento}\n\nPregunta: {q}"
r = preguntar(texto if con_documento else q,
sistema=sistema if con_documento else None,
max_tokens=25)
# «seis|6»: vale cualquiera de las dos formas
ok = any(e in r.lower() for e in esperado.split("|"))
aciertos += ok
if con_documento:
corta = r.replace("\n", " ")[:70]
print(f"{i:2d} {'sí' if ok else 'no'} {corta}")
return aciertos
con = evaluar(True)
sin = evaluar(False)
print(f"Con el documento: {con}/15 ({con / 15:.0%})")
print(f"Sin el documento: {sin}/15")
1 sí 30
2 sí se divide la ruta en 6 fases.
3 sí Aprender de ejemplos
4 no Redes neuronales, series temporales, clustering y reducción de dimensi
5 sí regresión lineal y descenso de gradiente
6 no Red neuronal
7 no El tema explica la atención y los transformers.
8 sí Fase 3
9 no Proyecto final
10 no El proyecto final es el tema 28.
11 sí Audio
12 sí 4
13 sí El tema que trata de árboles de decisión es tema 11.
14 no El tema 12 fue escrito por el autor de "Más modelos, tiempo y estructu
15 no Madrid
Con el documento: 8/15 (53%)
Sin el documento: 0/15
- Con el documento acierta 8 de 15 (53 %); sin él, ninguna. El modelo no sabe nada de esta ruta: todo lo que acierta lo sacó del prompt. Esa es la idea de RAG.
- Confunde números cercanos: dice que el proyecto final es el tema 28, cuando el documento dice 30 (el 28 es el de audio, que está en la misma línea).
- Responde otra cosa: a «¿Qué tema explica la atención?» repite la pregunta sin el número, y a «¿Cómo se llama la fase 6?» contesta «Proyecto final».
- No respeta la instrucción de decir «No está en el documento»: en la pregunta 14 arma una respuesta con palabras del documento, y en la 15 inventa «Madrid». Las preguntas sin respuesta son las más importantes de medir, porque muestran si el asistente prefiere inventar antes que admitir que no sabe.
- Un modelo de 0,5 B parámetros es muy chico para esta tarea; los modelos más grandes suelen acertar mucho más con el mismo prompt. Para el paso 5, prueba pedir que copie la frase del documento que respalda su respuesta, poner la pregunta antes del documento o agregar un ejemplo resuelto, y vuelve a medir con las mismas 15 preguntas.
Errores comunes
- Olvidar la máscara causal. Sin ella, cada posición ve el carácter que tiene que predecir: la pérdida de entrenamiento baja casi a cero, pero el modelo no sirve para generar.
- Correr mal el objetivo.
yesxcorrida un lugar. Si usas la misma ventana como entrada y objetivo, el modelo aprende a copiar. - Pasarle al modelo más tokens que su ventana. En el mini-GPT da un
IndexError; en otros modelos, el principio del texto se pierde o la calidad cae. Recorta a los últimosT. - Confundir temperatura con calidad. Temperatura 0 (codicioso) no es «la respuesta correcta»: es la más probable paso a paso, y puede repetirse en bucle. Temperatura alta no es «más creativa» sin costo: también es más disparatada.
- No usar la plantilla de chat con un modelo instruct. Si le das el texto suelto, lo continúa como un modelo preentrenado en lugar de responder.
- Leer solo la respuesta y no la longitud. Si la respuesta termina a la mitad, se acabó
max_new_tokens, no es que el modelo no supiera. - Evaluar un prompt con un solo ejemplo. Un caso que salió bien no dice nada; mide con varios casos y una tabla.
- Creerle a un dato porque suena seguro. Un LLM produce texto probable, no texto verificado. Fechas, nombres, cifras y citas se comprueban en una fuente.
- Mandar datos personales a un servicio en la nube sin pensarlo. Lo que escribes en el prompt viaja a los servidores de otra empresa.
¿Lo lograste?
Al terminar este tema deberías poder hacer lo siguiente. Al lado, dónde lo practicaste.
- Explicar un modelo de lenguaje con la regla de la cadena y medirlo con la pérdida por token y la perplejidad (ejercicio 1).
- Entrenar en una CPU un modelo de lenguaje pequeño por caracteres, tipo GPT, y explicar su ventana de contexto (teoría, ejercicios 5 y 6).
- Generar texto con muestreo y controlar la temperatura, top-k y top-p (ejercicios 2, 3 y 4).
- Usar un LLM abierto con
transformersy su plantilla de chat (ejercicio 7 y mini-proyecto). - Escribir prompts con instrucciones y con ejemplos, y evaluarlos con varios casos (ejercicio 7 y pasos 3 a 5 del mini-proyecto).
- Explicar las alucinaciones y por qué un LLM puede equivocarse con seguridad, y verificar una respuesta (ejercicio 8 y paso 4 del mini-proyecto).