Ajuste supervisado: máxima verosimilitud sobre un formato
35 min de lectura
Con la plantilla de chat, una conversación cabe ya en lo único que un modelo lee, una secuencia de tokens, y la respuesta tiene dentro de ella un principio y un final marcados por tokens que ningún texto puede falsificar. Que el modelo los reconozca es otra cosa. Para el mini-GPT son cuatro entradas más de su vocabulario, con el mismo vector las cuatro, que no ha leído nunca en ningún texto: el token que le cede la palabra no le anuncia nada, y eso sólo lo cambia entrenarlo.
En 2023, un modelo de 65 000 millones de parámetros aprendió a comportarse como un asistente con mil pares de instrucción y respuesta, sin ningún otro entrenamiento detrás. Sus autores lo leyeron así: casi todo lo que el modelo sabía venía del preentrenamiento, y los mil pares le enseñaron sobre todo la forma de decirlo. Esta lección escribe qué le pide a un modelo un entrenamiento sobre pares y, sobre todo, qué deja sin pedir, que es lo que explica aquella cifra. Después lo prueba en el mini-GPT.
Mira los pares con que vas a ajustarlo. Son diez preguntas por el color de algo, cada una con su respuesta de una frase: a ¿De qué color es la nieve? le toca La nieve es blanca., y así las diez. Comparten casi todo: la forma de la pregunta y la de la respuesta, el artículo de la una repetido en la otra, el punto y el token de fin. De un par a otro cambian dos palabras, el sustantivo y el color, y cada paso de entrenamiento empuja a la vez lo común y lo de cada uno.
Lo común se aprende diez veces, y es lo único que una pregunta nueva puede tomar prestado. Lo particular sólo vale para su par: el color de la nieve no dice nada del de la tierra, y la novela con que se preentrenó el mini-GPT tampoco. Diez pares son suficientes para cambiar cómo responde y no para decidir qué responde.
Ajustar es maximizar la verosimilitud de los pares
Fijemos el objeto. Hay un modelo preentrenado y un conjunto de pares , cada uno una conversación escrita con la plantilla: es la conversación hasta el token del asistente, incluido, e la respuesta con su , escrita por alguien y que obedece a . El ajuste supervisado (supervised fine-tuning, SFT) es seguir entrenando el modelo sobre ese conjunto, desde los pesos del preentrenamiento. Es supervisado porque cada instrucción llega con la respuesta que se quiere.
Lo que se maximiza es la verosimilitud del conjunto, con la palabra de la lección sobre funciones de pérdida del curso anterior: la probabilidad que el modelo le da entero, el producto de las de sus pares. Maximizarla es minimizar menos su logaritmo, una suma, que dividida entre el número de pares es la media de sobre un par sacado al azar del conjunto. Llamemos a la distribución de ese sorteo. Con el producto de la lección anterior dentro, la pérdida del ajuste es
Es la entropía cruzada del bloque anterior con una sola diferencia: la suma recorre los tokens de , que es lo que hace la máscara. Y tiene la forma exacta del preentrenamiento de la lección sobre predecir y obedecer, con en el lugar de . Aquella lección terminó proponiendo cambiar el texto con que se entrena. Esto es cambiarlo.
Su mínimo sale, por tanto, con el argumento de aquella lección. Agrupemos los pares por su instrucción: uno sacado al azar tiene la instrucción con probabilidad y, dada ésa, la respuesta con probabilidad , así que
donde la suma de fuera recorre las instrucciones del conjunto. Cada suma de dentro es la entropía cruzada entre dos distribuciones de respuestas a una misma instrucción, la del conjunto y la del modelo, y la desigualdad de la lección sobre la perplejidad la hace mínima en un solo sitio: cuando son iguales. Con un modelo capaz de dar a cada instrucción la distribución que quiera, el mínimo de la suma es el de cada sumando,
La igualdad dice tres cosas. Con una sola respuesta por instrucción, como en los diez pares de esta lección, vale 1 en ella, y el óptimo es aprenderse cada par de memoria. Si todas las respuestas del conjunto obedecen, el modelo obedece siempre en sus instrucciones: es el primer factor de la lección sobre predecir y obedecer, resuelto. Y el segundo factor, lo buena que es la respuesta cuando obedece, queda en el del conjunto. Un modelo ajustado contesta tan bien como quien escribió sus pares, con sus mismos errores y en la misma proporción.
El gradiente es el del preentrenamiento, con filas a cero
Del gradiente sólo cambia el principio. Cada factor de es un softmax, el de los logits de la posición que ha leído y los primeros tokens de . Escribámoslos , con lo que leyeron por argumento, como en la primera lección del bloque anterior. La pérdida de un par es
y cada término depende de una sola fila de logits. Su gradiente respecto de ella es el que dejó la lección sobre funciones de pérdida: el softmax menos el one-hot del token que de verdad vino,
Ésas son las filas que apuestan por un token de . La primera, , lee entera, hasta : el token del asistente no se cobra, pero la fila que lo lee sí. Las demás filas leen sólo un trozo de y apuestan por el token siguiente de . Se calculan en la misma ida y no aparecen en ningún término, así que su gradiente es cero.
Esas dos líneas son todo el cambio. Lo que queda es la vuelta de la lección sobre backpropagation, que es lineal en esos vectores y lleva cada uno a los pesos que calcularon su fila; un vector cero no lleva nada. En el preentrenamiento todas las filas tenían el suyo. En el ajuste, sólo las de la respuesta.
Que una fila no tenga gradiente no quiere decir que su posición no aprenda. Cada fila de lee, por la atención, las claves y los valores de todas las posiciones anteriores, las de incluidas, y la vuelta pasa por ellos hasta los pesos y las filas de de los tokens de la pregunta. El ajuste le enseña al modelo a leer la instrucción, en la medida en que eso le ayuda a escribir la respuesta, y nunca a escribirla. Así se separan las filas de rol, que empezaron siendo la misma media: cada una aprende de los sitios en que se lee.
Por el otro lado de la tabla atada, el de la salida, cada fila cobrada empuja hacia abajo el logit de toda entrada que no es , en proporción a lo que apostó por ella. sube en la fila que cierra cada respuesta y baja en las demás, y los tokens de rol, que no son en ninguna, sólo bajan: el modelo aprende a no escribirlos nunca, que es lo que la plantilla necesita.
Lo que la pérdida no dice
La suma de recorre las instrucciones del conjunto y ninguna más. Para una instrucción que no está en él no hay término, y la pérdida no dice nada de lo que haga ahí: ni que conteste bien, ni que conteste. Lo que pase lo deciden los pesos, que son los mismos para todas las instrucciones, y el sitio del que partieron.
Eso corta en dos direcciones. Hacia delante, una instrucción nueva recibe lo que los pares tienen en común y lo que el preentrenamiento dejó: con mil pares variados y un modelo que lo ha leído casi todo, una respuesta útil; con diez pares que sólo se distinguen en dos palabras y un modelo que ha leído una novela, el formato de los diez. Hacia atrás, el texto del preentrenamiento tampoco tiene término. Nada en le pide al modelo que lo siga prediciendo, y cada paso de Adam mueve casi todos los pesos casi , tengan o no que ver con los pares, como vio la lección sobre entrenar el mini-GPT.
Perder lo que un modelo sabía por ajustarlo a otra cosa se llama olvido catastrófico (catastrophic forgetting). En el ajuste supervisado se frena con muchos pares variados, un pequeño y pocas épocas. El bloque vuelve a él más adelante, cuando haga falta mejorar un modelo sin dejar que se aleje del de partida.
El mini-GPT, ajustado con diez pares
Las tres celdas corren sobre el mini-GPT, tu mini-GPT del bloque 1 cargado desde el checkpoint con las cuatro filas de la plantilla, y esta vez lo entrenan, en el navegador. La primera escribe los diez pares con la plantilla de la lección anterior, los apila en un batch y mide el modelo antes de tocarlo.
from pyodide.http import open_url
exec(open_url("/courses/llm-agents/modelo.py").read()) # red, F, codificar, Adam, quitar_cabecera
# La plantilla de la lección anterior: cuatro tokens especiales detrás de las 512 entradas.
ID = {e: 512 + i for i, e in enumerate(["<|sistema|>", "<|usuario|>", "<|asistente|>", "<|fin|>"])}
FIN = ID["<|fin|>"]
VOCAB = vocabulario(F) | {i: e.encode() for e, i in ID.items()}
ver = lambda ids: b"".join(VOCAB[i] for i in ids).decode("utf-8", errors="replace")
def plantilla(mensajes):
ids, mascara = [], []
for rol, contenido in mensajes:
trozo = codificar(contenido, F) + [FIN]
ids += [ID["<|%s|>" % rol]] + trozo
mascara += [0] + [int(rol == "asistente")] * len(trozo)
return ids, mascara
PARES = [("¿De qué color es el cielo?", "El cielo es azul."), ("¿De qué color es la nieve?", "La nieve es blanca."),
("¿De qué color es la hierba?", "La hierba es verde."), ("¿De qué color es el carbón?", "El carbón es negro."),
("¿De qué color es la sangre?", "La sangre es roja."), ("¿De qué color es el sol?", "El sol es amarillo."),
("¿De qué color es la noche?", "La noche es negra."), ("¿De qué color es el mar?", "El mar es azul."),
("¿De qué color es la leche?", "La leche es blanca."), ("¿De qué color es el oro?", "El oro es amarillo.")]
convs = [plantilla([("usuario", q), ("asistente", a)]) for q, a in PARES]
# El batch: una conversación por fila. La fila t apuesta por ids[t + 1]: su peso, la máscara corrida.
T = max(len(ids) for ids, _ in convs) - 1
X, Y, peso = np.zeros((10, T), dtype=int), np.zeros((10, T), dtype=int), np.zeros((10, T))
for b, (ids, m) in enumerate(convs):
X[b, :len(ids) - 1], Y[b, :len(ids) - 1], peso[b, :len(ids) - 1] = ids[:-1], ids[1:], m[1:]
red.p["E"] = np.vstack([red.p["E"], np.tile(red.p["E"].mean(axis=0), (4, 1))]) # las filas nuevas
red.cfg["n_v"] = 516
base = {k: v.copy() for k, v in red.p.items()} # el punto de partida
def responder(pregunta, n=16):
x, y = plantilla([("usuario", pregunta)])[0] + [ID["<|asistente|>"]], []
while len(y) < n and FIN not in y: # voraz, hasta que escribe <|fin|>
y.append(int(np.argmax(red.adelante(np.array([x + y]))[0][0, -1])))
return ver(y)
print("batch %s: %d posiciones, %d cobradas" % (X.shape, sum(len(i) - 1 for i, _ in convs), peso.sum()))
print("lo que cobra la primera fila:", repr(ver(Y[0][peso[0] == 1])))
print("pérdida en los pares: %.2f nats por token" % red.perdida(X, Y, peso)[0])
print("antes:", repr(responder("¿De qué color es la tierra?")))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador y se reutiliza en todas las lecciones.
Las diez conversaciones ocupan 277 posiciones de un batch de , y se cobran 116, las
de las respuestas con su <|fin|>, como enseña la primera fila. Esa fila es la ecuación del gradiente en código, porque peso es la máscara
corrida una posición: la fila apuesta por el token . Antes del ajuste, el mini-GPT paga
nats por cada token de las respuestas, un nat menos que los de repartir por
igual, y detrás de <|asistente|> escribe novela.
perdida divide entre la suma de peso, los 116 tokens cobrados, y no entre los diez pares. Con el
conjunto entero en cada paso, eso es una constante: no mueve el mínimo, y Adam, que divide cada
coordenada del gradiente por su propia escala, ni siquiera la nota.
La segunda da doce pasos de Adam sobre el batch entero. Reanuda un modelo ya entrenado con un Adam nuevo, así que empieza con cinco pasos de calentamiento hasta .
# repite el ajuste, no lo alarga.
red.p = {k: v.copy() for k, v in base.items()}
opt = Adam(red.p) # medias móviles a cero
for s in range(1, 13):
L, g = red.perdida(X, Y, peso) # la del bloque 1, con peso
opt.paso(red.p, g, eta=5e-3 * min(1, s / 5)) # cinco de calentamiento
if s % 3 == 0:
print("paso %2d pérdida %.3f" % (s, L))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador y se reutiliza en todas las lecciones.
La pérdida en los pares baja de a nats por token en doce pasos, y seguiría bajando hacia el cero del óptimo, que es aprenderse los diez. La tercera celda le hace seis preguntas que no están entre ellos, mide la novela antes y después del ajuste y cuenta los pesos que se han movido. Al final prueba, sin plantilla, el marco Pregunta: … Respuesta: de la lección sobre predecir y obedecer.
for q in ["¿De qué color es la tierra?", "¿De qué color es el vino?", "¿De qué color es la sal?",
"¿De qué color es el limón?", "¿Quién es la Nela?", "Dime un número."]:
print("%-28s %r" % (q, responder(q)))
texto = quitar_cabecera(open_url("/courses/llm-agents/corpus.txt").read())
R = np.array(codificar(texto[-6000:], F)[:513]) # 512 posiciones del texto reservado
novela = lambda: red.perdida(R[:-1].reshape(8, 64), R[1:].reshape(8, 64))[0]
ajustados, red.p = red.p, base # un momento, los pesos de antes
antes = novela()
red.p = ajustados
print("\nnovela: %.2f nats por token antes del ajuste, %.2f después (repartir por igual: %.2f)"
% (antes, novela(), np.log(516)))
movidos = sum((np.abs(red.p[k] - base[k]) > 1e-3).sum() for k in base)
print("pesos que se han movido más de 0.001: %d de %d" % (movidos, sum(v.size for v in base.values())))
x = codificar("Pregunta: ¿De qué color es la tierra?\nRespuesta:", F) # el marco, sin plantilla
y = red.generar(x, 16, temperatura=0)[len(x):]
print("\ncon el marco:", repr(ver(y[:y.index(FIN) + 1] if FIN in y else y)))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador y se reutiliza en todas las lecciones.
Las seis respuestas son frases con la forma de los pares, y las seis terminan en <|fin|>: el mini-GPT ha aprendido a acabar. En las cuatro preguntas de colores, el artículo de la respuesta es el de la pregunta: la tierra recibe La leche es blanca., y el vino, El cielo es azul.. Ninguna fila cobrada apuesta por un token de la pregunta, y aun así la respuesta depende de ella. Es el gradiente que entró por las filas de la respuesta y llegó a lo que esas filas leen.
El contenido, en cambio, sale de los pares tal cual, sin relación con lo que se pregunta. La Nela, la protagonista de la novela que el mini-GPT leyó entera, es La nieve es blanca., y un número es El cielo es azul.. Diez pares han enseñado el formato y nada más.
Tampoco ha salido gratis. La novela le cuesta ahora nats por token, frente a antes del ajuste: más que los de repartir por igual entre las 516 entradas. Doce pasos han movido más de una milésima de sus pesos. Y el marco de la lección sobre predecir y obedecer, al que el bloque prometió volver, recibe ahora una frase de colores con su <|fin|>. No es que el mini-GPT haya aprendido el marco: es que ya no sabe escribir otra cosa.
Comprueba tu intuición
Cuatro preguntas y un desafío: la pérdida del ajuste y su gradiente, para una conversación.
Un conjunto de ajuste tiene la instrucción ¿Cuánto es 7 por 8? cinco veces: cuatro con la respuesta 56 y una con 54, que se coló por error. Con un modelo capaz de dar a cada instrucción la distribución que quiera, ¿qué probabilidad le da a la respuesta 54 en el mínimo de ?
Se acepta un margen de ±0.001.
Das un paso de ajuste del mini-GPT sobre una sola conversación, un mensaje del usuario y la respuesta del asistente, con la máscara de la plantilla. Marca lo que recibe un gradiente distinto de cero.
Marca todas las opciones correctas. Se corrige todo o nada: no hay puntuación parcial.
Tras el ajuste de la lección, el mini-GPT contesta El cielo es azul. a ¿De qué color es el vino?, que no está entre los diez pares. ¿Qué lo explica?
Tras los doce pasos de la lección, la novela le cuesta al mini-GPT nats por token; antes le costaba . ¿Qué lo explica?
Escribe perdida_sft(Z, ids, mascara), la pérdida del ajuste de una sola conversación y su
gradiente. ids son los tokens de la conversación y mascara la de la plantilla, dos
listas del mismo tamaño. Z, de forma (len(ids) - 1, n_v), son los logits que da la red
leyendo ids[:-1], así que la fila t apuesta por ids[t + 1]. Devuelve
(perdida, dZ): la media de de lo que cada fila cobrada apostó por
su token, y su gradiente respecto de Z, con la forma de Z. Una fila se cobra cuando el
token por el que apuesta es de la respuesta.
La primera comprobación descarga el intérprete de Python (~15 MB); después queda en la caché del navegador. Este desafío se resuelve mejor con un teclado físico: en el móvil puedes leerlo y volver luego.
El ajuste supervisado resuelve el primer factor de la lección sobre predecir y obedecer: en las instrucciones de su conjunto, el modelo obedece. El segundo lo deja donde lo tenía el conjunto, porque su óptimo es copiar a quien escribió los pares, aciertos y errores en la misma proporción. Y escribir buenos pares es caro: cada uno es una respuesta entera, redactada por alguien que sepa hacerlo.
Juzgar es más barato que escribir. Entre dos respuestas a una misma instrucción, casi cualquiera sabe decir cuál es mejor sin saber escribir ninguna de las dos, y ése es un dato que la pérdida de esta lección no sabe usar. La lección siguiente, sobre preferencias y el modelo de recompensa, lo convierte en un número: entrena un modelo que puntúa respuestas a partir de comparaciones, y explica por qué comparaciones y no notas.
Para profundizar4 fuentes · 4 papers
De dónde sale lo de esta lección, y dónde seguir si quieres más. Nada de aquí hace falta para continuar el curso.
- LIMA: Less Is More for Alignment
Un modelo de 65 000 millones de parámetros ajustado con la pérdida de esta lección sobre sólo mil pares, sin nada más. Su conclusión es la de la lección a gran escala: el ajuste enseña el formato, y lo demás viene del preentrenamiento.
- The False Promise of Imitating Proprietary LLMs
Ajustan modelos abiertos con respuestas de ChatGPT y los evaluadores humanos los creen casi iguales. Copian el estilo y no los hechos: el «El vino es azul» de la lección, medido en modelos de verdad.
- Scaling Instruction-Finetuned Language Models
El otro extremo de los diez pares: 1 800 tareas escritas como instrucciones. Con tantas, lo que comparten deja de ser una frase y pasa a ser obedecer, y el modelo mejora en tareas que nunca vio.
- Overcoming catastrophic forgetting in neural networks
El olvido catastrófico de la lección, y un remedio: frenar los pesos que importaban para lo anterior. El bloque usará otro, que mide la distancia en las distribuciones y no en los pesos.