DPO: la derivación completa
38 min de lectura
Con el aprendizaje por refuerzo con retroalimentación humana (reinforcement learning from human feedback, RLHF), el bloque tiene montada la cadena con que se entrenó InstructGPT, y la cadena tiene tres eslabones. Las comparaciones de los anotadores entrenan un modelo de recompensa, . La política escribe respuestas, las puntúa, y la política sube la puntuación media pagando cada nat que se aleja de la referencia. En ningún punto una comparación toca la política: lo que dijeron los anotadores le llega a través de , y de las respuestas que salgan en cada sorteo.
En 2023, Rafailov y sus coautores mostraron que el eslabón del medio se puede quitar sin cambiar el objetivo, y pusieron la idea en el subtítulo de su artículo: «tu modelo de lenguaje es, en secreto, un modelo de recompensa». El método se llama optimización directa de preferencias (direct preference optimization, DPO), y al año siguiente Meta lo prefirió a PPO (proximal policy optimization), el algoritmo de RLHF de InstructGPT, para ajustar Llama 3 con preferencias. Esta lección lo deriva entero, y el paso del que depende todo es una cancelación que el bloque dejó preparada dos lecciones atrás.
El objetivo de RLHF, la recompensa media menos veces la distancia a la referencia, tiene un óptimo que se puede escribir. Es la referencia, con la probabilidad de cada respuesta multiplicada por un número que crece exponencialmente con su recompensa, y todo dividido por lo que haga falta para que vuelva a sumar 1. Si la referencia escribe dos respuestas por igual y una tiene un punto más de recompensa, con la política óptima escribe esa unas siete veces más.
Leída al revés, la fórmula dice algo más útil: cada política es la óptima de alguna recompensa. La que mide, en unidades de , cuánto multiplica la política la probabilidad de cada respuesta respecto de la referencia. Una política que escribe una respuesta el doble que la referencia le da de recompensa; una que la escribe la mitad, . Así que la política lleva dentro un modelo de recompensa, y se puede entrenar como se entrenaba aquél, con comparaciones. Al entrenarlo, se entrena ella.
La política óptima tiene fórmula
Fijemos una instrucción y una recompensa cualquiera, y escribamos el objetivo de RLHF como sumas sobre las respuestas, con en el lugar de . Las dos sumas llevan el mismo peso, , así que se pueden juntar, y entra en el logaritmo como :
El denominador de la última línea tiene la forma de una distribución sobre respuestas, pero no suma
- Llamemos a lo que suma, la función de partición (partition function), y a lo que queda al dividir por ella:
Con eso, el denominador es , y el logaritmo del cociente se parte en dos. no depende de , y las probabilidades de suman 1, así que sale de la suma tal cual:
El segundo término no depende de . El primero es por una divergencia de Kullback–Leibler (KL), que nunca es negativa y vale cero sólo si las dos distribuciones coinciden: la desigualdad de la lección sobre la perplejidad, otra vez. Así que nunca pasa de , y llega a ese valor exactamente cuando . Para un modelo capaz de dar a cada instrucción la distribución que quiera, como el de la lección sobre el ajuste supervisado, es el óptimo.
La política óptima es la referencia repesada por y renormalizada. Con grande casi no se separa de ; con cerca de cero, toda la masa se va a las respuestas de mayor recompensa, como la que repetía la segunda celda de la lección anterior. El ascenso de RLHF buscaba esto paso a paso, y aquí está escrito.
Escrito no quiere decir utilizable. es una suma sobre todas las respuestas posibles a , y para las de 16 tokens del mini-GPT eso son , unos términos. Sin ella, la fórmula no da la probabilidad de ninguna respuesta, y no hay forma de sortear de .
De dónde sale la fórmula, si no se adivina
La reescritura de arriba demuestra que es el máximo, pero hay que conocer la fórmula para hacerla. Así se encuentra. Tratemos cada probabilidad de la política como una variable libre, y fijémonos en dos respuestas cualesquiera, e , con recompensas y . Pasemos una cantidad pequeña de probabilidad de a , sin tocar las demás, de modo que todo sigue sumando 1. De todos los términos de , sólo cambian los de esas dos respuestas:
Derivando cada producto, la derivada del logaritmo deja un en los dos corchetes, que se cancelan en la resta:
En un máximo ningún traslado pequeño puede ganar nada, así que para cualquier par de respuestas con probabilidad positiva. Lo que hay entre corchetes, la recompensa de una respuesta menos por su logaritmo del cociente, vale lo mismo para todas: en el óptimo, cada respuesta rinde igual. Despejando, es por un mismo número para todas las respuestas, y el que hace que sumen 1 es . Es .
La segunda derivada, , es negativa: cada traslado recorre una curva cóncava, y su punto plano es un máximo. Y el valor común de los corchetes resulta ser , que es la ecuación de la sección siguiente vista desde aquí.
La recompensa, escrita con la política
La fórmula sirve leída al revés. Tomemos logaritmos en la definición de y despejemos la recompensa:
Una recompensa decide su política óptima, y la política óptima devuelve la recompensa salvo , un número que depende de la instrucción y no de la respuesta. Es un , la constante por instrucción que la lección sobre el modelo de recompensa encontró en Bradley–Terry. Recompensa y política óptima son, dadas la referencia y , la misma información escrita de dos maneras.
Así que cualquier política es la óptima de una recompensa, y de todas las que se diferencian de ella en una constante por instrucción. Llamemos recompensa implícita (implicit reward) de a
con el subíndice de los parámetros que la calculan, como : es un modelo de recompensa cuyos parámetros son los de la política. Vale cero en todas las respuestas cuando la política es la referencia, y es positiva en las que la política escribe más que la referencia.
Ahora, Bradley–Terry. Sustituyamos la recompensa despejada en la probabilidad de que un anotador prefiera a :
entra sumando con la preferida y restando con la rechazada, y se va. Lo que nadie podía calcular ha desaparecido, y no por suerte: las dos respuestas de una comparación contestan a la misma instrucción, y es la constante por instrucción que no cambia ninguna comparación. Aquella libertad, que en Bradley–Terry parecía un defecto del modelo, es lo que hace posible esta lección.
La probabilidad de cada comparación queda escrita sin ninguna recompensa. La pérdida de DPO es menos su logaritmo, en media, con en el lugar de : la pérdida del modelo de recompensa, , con en el lugar de :
Minimizarla es ajustar a las comparaciones un modelo de recompensa con la forma . Si la política puede ser cualquier distribución, esa forma alcanza cualquier recompensa salvo una constante por instrucción, que no ve; y la política óptima de la recompensa ajustada es, por construcción, . Las dos fases de RLHF quedan en una, y no hay nada que sortear: cada término lee el logaritmo de la probabilidad de la preferida y de la rechazada, en la política, con una ida sobre cada una, y en la referencia, que se calcula una vez antes de empezar.
El gradiente: cuánto se equivoca todavía la recompensa implícita
Del margen, sólo la política depende de , así que . Con la derivada de de la lección sobre el modelo de recompensa, la de cada comparación es
es, con el signo cambiado, el gradiente de la pérdida del ajuste supervisado para el par . Un paso de descenso es un paso de ajuste sobre la preferida y otro al revés sobre la rechazada: sube la una y baja la otra. Lo mismo hacía el estimador de la lección anterior, con respuestas que la política acababa de escribir; éstas estaban escritas antes de empezar.
El factor que lo multiplica, , es el mismo que en el modelo de recompensa: la probabilidad que la recompensa implícita da todavía al orden que el anotador no eligió. Mide cuánto se equivoca aún. Al empezar, la política es la referencia, las dos recompensas implícitas valen cero, y cada comparación cuesta con factor . Una comparación ya ordenada por un margen de 3 tiene un factor de : se apaga, y deja de mover la política. Una que está al revés empuja con un factor cercano a 1. Sin el factor, el paso subiría la preferida y bajaría la rechazada siempre, y el artículo de DPO muestra que así el modelo degenera.
Y está dentro del margen. Para un mismo alejamiento de la referencia, una grande da un margen grande, y la comparación se apaga cerca de la referencia; con pequeña, la política tiene que alejarse mucho más antes de que se apague. Es el freno de la KL, y la pérdida no tiene ningún término de KL: el freno está en cómo se escribe la recompensa.
Dos concesiones. La equivalencia con RLHF es entre óptimos, y supone una política que puede ser cualquier distribución. Y la pérdida sólo habla de las respuestas de las comparaciones: el freno actúa sobre ellas, mientras que la KL de RLHF se mide sobre las respuestas que escribe la propia política. Lo que DPO haga con las demás lo deciden los pesos, como en el ajuste supervisado.
DPO en el mini-GPT
Las dos celdas corren sobre el mini-GPT, tu mini-GPT del bloque 1 cargado desde el checkpoint, con la política de la lección anterior: el mini-GPT congelado más un vector de 512 números, uno por entrada, sumado a sus logits, y la misma instrucción, La Nela. La referencia es el mini-GPT sin . Lo que cambia es de dónde aprende.
Aquí las respuestas se sortean una vez, de la referencia, antes de entrenar: 64 continuaciones de 16 tokens, como las que un modelo ajustado escribe para que alguien las compare. El anotador es un programa que sigue a Bradley–Terry con la recompensa de la lección anterior, las letras a: entre dos respuestas, prefiere la de más letras con probabilidad de la diferencia. Compara cada respuesta con las otras 63, como los anotadores de InstructGPT ordenaban varias respuestas a una instrucción y de cada orden salían todas sus comparaciones.
La primera celda prepara eso, y guarda además la distribución de la referencia en cada token de cada respuesta. Con ella, sale para cualquier sin volver a pasar por la red: es esa distribución con cada entrada multiplicada por , y renormalizada.
from pyodide.http import open_url
exec(open_url("/courses/llm-agents/modelo.py").read()) # red, F, codificar, softmax, layer_norm
def avanzar_lote(ids, cache):
"""avanzar() de modelo.py con un eje más, el del batch: una fila nueva por respuesta."""
p, L, h, d = red.p, red.cfg["n_capas"], red.cfg["h"], red.cfg["d_model"]
B, d_k, t = len(ids), d // h, cache[0][0].shape[2] # t: posiciones ya guardadas
H = p["E"][ids] + p["P"][t] # (B, d)
for l in range(L):
Hn, _ = layer_norm(H, p[f"{l}.ln1_g"], p[f"{l}.ln1_b"])
q, k, v = (u.reshape(B, h, 1, d_k) for u in np.split(Hn @ p[f"{l}.Wqkv"], 3, axis=1))
cache[l] = K, V = np.concatenate([cache[l][0], k], 2), np.concatenate([cache[l][1], v], 2)
a = softmax(q @ K.transpose(0, 1, 3, 2) / np.sqrt(d_k)) # (B, h, 1, t + 1)
H = H + (a @ V).reshape(B, d) @ p[f"{l}.Wo"]
H = H + red.ffn(H[:, None], l)[0][:, 0]
return layer_norm(H, p["lnf_g"], p["lnf_b"])[0] @ p["E"].T
x = codificar("La Nela", F) # la instrucción: dos tokens
def responder(theta, rng, B=8, n=16):
"""B respuestas de la política, y la distribución de la referencia en cada uno de sus tokens."""
Z, (_, capas, _, _) = red.adelante(np.tile(x, (B, 1))) # el prefill, las B a la vez
cache = [(K, V) for (_, _, _, K, V, _, _), _ in capas]
z, Y, P = Z[:, -1], np.zeros((B, n), int), np.zeros((B, n, 512))
for j in range(n):
if j:
z = avanzar_lote(Y[:, j - 1], cache)
P[:, j] = softmax(z) # pi_ref en este token
q = P[:, j] * np.exp(theta) # pi_theta, sin normalizar
Y[:, j] = [rng.choice(512, p=fila / fila.sum()) for fila in q]
return Y, P
letras = lambda Y: np.array([decodificar(list(y), F).count("a") for y in Y])
# Los datos: 64 respuestas de la referencia, y un anotador que compara cada una con todas las demás.
rng = np.random.default_rng(0)
Y, P = responder(np.zeros(512), rng, B=64) # se sortean aquí, una vez
r = letras(Y)
i, j = np.triu_indices(64, 1) # los 2016 pares
gana_i = rng.random(len(i)) < 1 / (1 + np.exp(-(r[i] - r[j]))) # Bradley-Terry, con r = las letras a
w, l = np.where(gana_i, i, j), np.where(gana_i, j, i) # la preferida y la rechazada
print("%d respuestas, %.2f letras a de media; %d comparaciones" % (len(Y), r.mean(), len(w)))
print("el anotador prefirió la de más letras en el %.0f %%, la de menos en el %.0f %%; en el %.0f %% empataban"
% (100 * np.mean(r[w] > r[l]), 100 * np.mean(r[w] < r[l]), 100 * np.mean(r[w] == r[l])))
print("una comparación:", repr(decodificar(list(Y[w[0]]), F)), "≻", repr(decodificar(list(Y[l[0]]), F)))
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 64 respuestas tienen letras a de media. En las 2016 comparaciones, el anotador
prefirió la de más letras en el 69 % y la de menos en el 13 %; en el 18 % restante empataban, y echó
una moneda al aire. Es un anotador con ruido, como los de verdad. La segunda celda es DPO: cien pasos de descenso por gradiente sobre con la pérdida de la
lección, con y con . Mira que el bucle no llama a responder: sólo lee las
respuestas guardadas y las comparaciones. Después sí sortea 32 respuestas nuevas de cada política,
para medirla como en la lección anterior.
El paso depende de . La recompensa implícita es por el logaritmo del cociente, así que con pequeña tiene que llegar más lejos, y el gradiente, que lleva delante, empuja menos. Con un paso proporcional a , las dos ejecuciones recorren el mismo camino en recompensa implícita.
def log_cociente(theta, Y, P):
"""log pi_theta(y|x) - log pi_ref(y|x) de cada respuesta, y su gradiente respecto de theta."""
S = P @ np.exp(theta) # (B, n): lo que renormaliza cada token
k = (theta[Y] - np.log(S)).sum(axis=1)
cuenta = np.stack([np.bincount(y, minlength=512) for y in Y])
return k, cuenta - np.exp(theta) * np.einsum("bj,bjv->bv", 1 / S, P) # one-hot menos softmax
def dpo(beta, pasos=100):
"""Descenso por gradiente sobre theta con la pérdida de DPO, y nada más."""
theta, eta = np.zeros(512), 2.5 / beta**2 # beta pequeña: más lejos, menos empuje
for s in range(pasos + 1):
k, G = log_cociente(theta, Y, P)
margen = beta * (k[w] - k[l]) # r_theta(x, y_w) - r_theta(x, y_l)
factor = 1 / (1 + np.exp(margen)) # sigma(-margen)
if s % 25 == 0: # (+ 0: que el cero no salga -0.00)
print("paso %3d pérdida %.3f margen %.2f factor %.2f"
% (s, np.logaddexp(0, -margen).mean(), margen.mean().round(2) + 0, factor.mean()))
if s < pasos:
c = np.bincount(w, factor, 64) - np.bincount(l, factor, 64) # + preferidas, - rechazadas
theta += eta * beta * (c @ G) / len(w)
d = r[w] - r[l]
print("factor al final: %.2f si la preferida tenía tres letras más o más, %.2f si tenía menos"
% (factor[d >= 3].mean(), factor[d < 0].mean()))
return theta
for beta in (0.5, 0.1):
print("\nbeta = %g" % beta)
theta = dpo(beta)
rt = beta * log_cociente(theta, Y, P)[0] # la recompensa implícita de las 64
print("recompensa implícita media, según sus letras a:",
" ".join("%d: %.1f" % (n, rt[r == n].mean()) for n in range(1, 7)))
Yn, Pn = responder(theta, np.random.default_rng(1), B=32) # ahora sí, sólo para medir
print("32 respuestas nuevas: recompensa %.2f KL %.2f"
% (letras(Yn).mean(), log_cociente(theta, Yn, Pn)[0].mean()))
print(" ", repr(decodificar(list(Yn[0]), F)))
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 arranca en , como decía la sección del gradiente: con la política es la referencia y todo margen vale cero. Baja hasta y ahí se queda. No puede bajar más porque los datos tienen ruido, y el factor lo enseña: al final vale en las comparaciones que la preferida ganaba por tres letras o más, que se han apagado, y en las que el anotador eligió la de menos letras, que siguen empujando al revés. El entrenamiento se para donde los dos empujes se compensan.
Lo que ha aprendido la recompensa implícita está en la línea de las letras. Con , las respuestas con una a tienen de media , las de dos , y así hasta con seis: un punto por letra. Es la recompensa del anotador, en su unidad, y nadie se la ha dado a la política: la ha sacado de las comparaciones, como la habría sacado un modelo de recompensa. El nivel no significa nada, porque es el de siempre, una constante por instrucción. Con los valores son otros, de a , y el punto por letra es el mismo.
Las dos políticas, en cambio, no se parecen. Con , las respuestas nuevas tienen letras a de media, frente a las de las respuestas de la referencia, a nats de ella, y siguen siendo novela. Con tienen , a nats: a la De a la a la día a la a mano la. Es la fórmula despejada: la misma recompensa entre una cinco veces menor da un logaritmo del cociente cinco veces mayor. La pérdida no tiene ningún término de KL, y frena igual.
La lección anterior llegó, con y REINFORCE, a letras y nats. Los dos métodos persiguen el mismo óptimo y no llegan al mismo sitio, por las dos concesiones de arriba: 512 números no son cualquier política, y DPO sólo ha visto 64 respuestas de la referencia, mientras REINFORCE aprendía de las que escribía.
Comprueba tu intuición
Cinco preguntas y un desafío: la pérdida de DPO y su gradiente, para un batch de comparaciones.
La política óptima de RLHF tiene fórmula, . ¿Por qué no se calcula y se sortea de ella, sin entrenar nada?
Al sustituir la recompensa despejada en Bradley–Terry, el término desaparece. Marca las afirmaciones ciertas.
Marca todas las opciones correctas. Se corrige todo o nada: no hay puntuación parcial.
Una comparación con : la política hace la preferida veces más probable que la referencia, y la rechazada veces menos. ¿Cuánto vale el factor de esa comparación en el gradiente?
Se acepta un margen de ±0.002.
La pérdida de DPO no tiene ningún término de KL. ¿Qué frena a la política para que no se aleje de la referencia?
En la segunda celda, con , las 16 respuestas con cuatro letras a, más que la media, ganaron el 70 % de sus comparaciones. Su recompensa implícita media es : la política entrenada las escribe con menos probabilidad que la referencia. ¿Cómo puede ser?
Escribe dpo(logp_w, logp_l, ref_w, ref_l, beta): la pérdida de DPO media de un batch de
comparaciones y su gradiente. Los cuatro primeros argumentos son arrays con un número por
comparación: , y las dos de la
referencia. Devuelve (perdida, g_w, g_l), donde g_w[i] es la derivada de la pérdida media
respecto de logp_w[i], y g_l[i] respecto de logp_l[i]. Con márgenes de miles de nats no
puede desbordar ni dar nan.
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.
Con DPO, aprender de preferencias cabe en una pérdida: comparaciones, una referencia congelada y descenso por gradiente, como el ajuste supervisado. Todo lo que la política aprende está en esas comparaciones, y la celda las ha tenido fáciles. Su anotador era un programa que contaba letras, conocía la recompensa verdadera y comparó 2016 pares sin cansarse. Con anotadores de verdad, cada comparación cuesta tiempo y dinero, dos de ellos discrepan algo más de una vez de cada cuatro, y lo que prefieren es lo que el modelo aprende a hacer.
La lección siguiente, sobre de dónde salen las preferencias, mira esa fuente: anotadores humanos, una lista de principios escrita para que un modelo critique y corrija sus propias respuestas, y modelos que juzgan a otros. Y qué quiere decir, con cada una, que un modelo esté «alineado».
Para profundizar5 fuentes · 5 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.
- Direct Preference Optimization: Your Language Model is Secretly a Reward Model
El artículo de DPO. Sus apéndices A.1, A.2 y A.4 son las tres derivaciones de esta lección, y su sección 4 lee el gradiente igual. Muestra además que sin el factor el modelo degenera.
- RL with KL penalties is better viewed as Bayesian inference
La política óptima de esta lección leída como una posterior bayesiana: la referencia es la distribución a priori y la exponencial de la recompensa, la verosimilitud. La KL es lo que impide que todo acabe en una sola respuesta.
- A General Theoretical Paradigm to Understand Learning from Human Preferences
Dónde falla el freno de DPO: si las comparaciones de un par salen siempre iguales, el margen que las explica es infinito y la β deja de frenar. Su sección 4.2 lo deriva, y la 5 propone IPO, que fija un margen finito.
- The Llama 3 Herd of Models
DPO a escala: su sección 4.1.4 cuenta por qué Meta ajustó Llama 3 con DPO y no con PPO (menos cálculo y mejores resultados en obedecer instrucciones), y los dos cambios que hicieron a la pérdida.
- Is DPO Superior to PPO for LLM Alignment? A Comprehensive Study
El contrapunto: con PPO bien ajustado, RLHF gana a DPO en todas sus pruebas. Una de las razones es la de la celda: DPO sólo ve las respuestas de las comparaciones, y la política puede ir a parar a otras que nadie ha juzgado.