Saltar al contenido
← InstructGPT

Lección 6 de 10 · Resultados y límites · 17 min

Cómo se evaluó

Win rate con su intervalo, veracidad y toxicidad: los números del paper y cómo se calculan

Notebook del episodio

El código de esta lección, para correrlo y modificarlo en Google Colab, gratis y con tu cuenta. Para guardar tus cambios: Archivo → Guardar una copia en Drive.


InstructGPT. Episodio seis: cómo se evaluó. Ya tenemos los tres pasos: SFT, supervised fine-tuning; el reward model; y PPO, proximal policy optimization, con su mezcla de preentrenamiento. Hoy, la pregunta que todo paper tiene que contestar: ¿cómo sabés que salió mejor? Vamos a ver qué midieron, cómo se calcula cada número, y cuánto ruido trae.

Tres palabras, tres mediciones

El paper define alinear con tres palabras: útil, honesto e inofensivo. Útil se mide con preferencias: una persona ve dos respuestas al mismo prompt y elige la mejor. Los prompts son de clientes de la API, application programming interface, la puerta por la que se usa el modelo, de clientes que no aportaron datos de entrenamiento. Honesto no se puede medir directo, porque no vemos qué cree el modelo; miden veracidad. E inofensivo, con toxicidad y sesgo sobre datasets públicos.

criteriocómo lo midieroncon qué datos
útil (helpful)preferencia de quien etiqueta: win rateprompts de la API
honesto (honest)veracidad y alucinacionesTruthfulQA y la API
inofensivo (harmless)toxicidad y sesgoRealToxicityPrompts y otros

Parte 1 · Un juicio por prompt

La métrica principal no es una pérdida ni un benchmark: es contar cuántas veces una persona prefirió tu modelo.

Win rate: contar preferencias

Simulamos una evaluación. Cuatrocientos prompts de test, un juicio por prompt. Cada juicio es un uno si quien etiqueta eligió la respuesta del modelo A, y un cero si eligió la del baseline B. Acá A le gusta de verdad un sesenta y seis por ciento de las veces, pero eso no lo vemos. El win rate es el promedio de esos unos y ceros. Y el baseline contra sí mismo. A ganó doscientos cincuenta y cinco de cuatrocientos: cero coma seis tres siete, no cero coma sesenta y seis. Esa diferencia es ruido de muestreo. El baseline contra sí mismo da cero coma cinco, más o menos.

python
n = 400                                   # prompts de test: un juicio por prompt
p_oculta = 0.66                           # cuánto gusta A de verdad (no se ve)
prefiere_A = rng.random(n) < p_oculta     # True: quien etiqueta eligió A sobre B
print("primeros 12:", prefiere_A[:12].astype(int))

win_rate = prefiere_A.mean()              # promedio de unos y ceros
print(f"A gana {prefiere_A.sum()} de {n}: win rate {win_rate:.3f}")

contra_si = rng.random(n) < 0.5           # el baseline contra sí mismo
print(f"SFT contra SFT: {contra_si.mean():.3f}")
text
primeros 12: [1 1 1 1 0 1 0 1 0 1 1 0]
A gana 255 de 400: win rate 0.637
SFT contra SFT: 0.502

El win rate, en fórmulas

w^=1n∑i=1n1[ Ai≻Bi ]\hat w = \frac{1}{n}\sum_{i=1}^{n} \mathbf{1}\big[\,A_i \succ B_i\,\big]

1[⋅]\mathbf{1}[\cdot]: 1 si se cumple, 0 si no · ≻\succ: «preferida a»

w^ SFT 175B=0,5\hat w_{\,\text{SFT 175B}} = 0{,}5

el baseline contra sí mismo: la línea de referencia

La fórmula que vimos en el episodio uno, ahora con cada pieza. n es la cantidad de prompts. Para cada prompt i, la respuesta A sub i del modelo y la B sub i del baseline. El símbolo parecido a un mayor es «preferida a». El uno en negrita es una indicadora: uno si lo de adentro es cierto, cero si no. w sombrero es su promedio; el sombrero dice que es una estimación. En el dibujo, ocho juicios y su promedio. El paper compara todo contra el SFT de ciento setenta y cinco mil millones de parámetros, porque queda en el medio. Contra sí mismo da cero coma cinco: arriba de esa línea, la gente prefiere tu modelo. En el dibujo, los cinco modelos grandes.

La figura 1 del paper, redibujada

La figura principal del paper, con valores leídos del gráfico, no de una tabla. GPT tres a secas: de cero coma uno cuatro a cero coma dos cuatro. GPT tres con un prompt few-shot bien elegido: sube. El SFT: de cero coma tres cinco a cero coma cinco. PPO, sin mezcla de preentrenamiento. PPO-ptx, que es lo que el paper llama InstructGPT: cero coma cinco tres en el más chico, cero coma sesenta y seis en el más grande. La línea del cero coma cinco. El dato del abstract: el PPO-ptx de uno coma tres mil millones queda arriba del GPT tres de ciento setenta y cinco mil millones.

valores leídos del gráfico vectorial de la figura 1 (±0,01); barras de error de ±0,03 a ±0,05

Ouyang et al., 2022 — Training language models to follow instructions with human feedback · arXiv:2203.02155

85 ± 3 % · «175B InstructGPT outputs are preferred to GPT-3 outputs 85 ± 3% of the time, and preferred 71 ± 4% of the time to few-shot GPT-3.»

Los números del episodio uno. Comparando directo, sin pasar por el baseline: El InstructGPT de ciento setenta y cinco mil millones le gana a GPT tres el ochenta y cinco por ciento de las veces, más o menos tres. Y al GPT tres con prompt few-shot, el setenta y uno, más o menos cuatro. Ese más o menos es lo que sigue.

Parte 2 · Cuánto ruido tiene un win rate

Con cuatrocientos juicios salió cero coma seis tres siete en vez de cero coma sesenta y seis. ¿Cuánto se puede mover?

El intervalo, a mano y por bootstrap

Los mismos cuatrocientos juicios. El win rate. Su error estándar: la raíz de p por uno menos p, sobre n. El intervalo del noventa y cinco por ciento: uno coma noventa y seis errores estándar para cada lado. Y la otra forma, el bootstrap que usamos en Alineación, episodio diez: sortear cinco mil veces cuatrocientos juicios con reposición entre los que tenemos. Y quedarnos con el percentil dos coma cinco y el noventa y siete coma cinco. Error estándar cero coma cero dos cuatro. Intervalo: de cero coma cinco nueve a cero coma seis ocho cinco, más o menos cero coma cero cuatro siete. El bootstrap da casi lo mismo. Y el sesenta y seis verdadero cae adentro.

python
p = prefiere_A.mean()
se = np.sqrt(p * (1 - p) / n)             # error estándar de un promedio de 0 y 1
print(f"p = {p:.3f}   se = {se:.4f}")
print(f"fórmula:   {p - 1.96*se:.3f} a {p + 1.96*se:.3f}  (± {1.96*se:.3f})")

remuestras = rng.choice(prefiere_A, size=(5000, n))   # bootstrap
medias = remuestras.mean(1)
lo, hi = np.percentile(medias, [2.5, 97.5])
print(f"bootstrap: {lo:.3f} a {hi:.3f}")
text
p = 0.637   se = 0.0240
fórmula:   0.590 a 0.685  (± 0.047)
bootstrap: 0.588 a 0.685

De dónde sale el ±

se=w^ (1−w^)n=0,637⋅0,363400=0,024\mathrm{se} = \sqrt{\frac{\hat w\,(1-\hat w)}{n}} = \sqrt{\frac{0{,}637 \cdot 0{,}363}{400}} = 0{,}024

se: error estándar · n=400n = 400

w^±1,96 se=0,637±0,047\hat w \pm 1{,}96\,\mathrm{se} = 0{,}637 \pm 0{,}047

el 95 % de una normal cae a ±1,96 se

La cuenta. Cada juicio es una moneda cargada, con varianza w por uno menos w. El promedio de n monedas tiene esa varianza dividida por n; su raíz es el error estándar, se. Con nuestros números: cero coma seis tres siete por cero coma tres seis tres, sobre cuatrocientos, raíz: cero coma cero dos cuatro. En el dibujo, cada factor. Con n grande, el promedio se distribuye casi como una campana normal, y el noventa y cinco por ciento de una normal cae a uno coma noventa y seis errores estándar del centro. En el dibujo, los dos bordes.

Cuántos juicios hay detrás de cada ±

El paper da los más o menos, pero no dice cuántas comparaciones hizo ni con qué método. Podemos dar vuelta la fórmula. El margen m es z por el error estándar. Elevamos al cuadrado los dos lados y despejamos n. Para el ochenta y cinco más o menos tres. Unos quinientos cuarenta. Para el setenta y uno más o menos cuatro, unos cuatrocientos noventa; para la figura uno, unos cuatrocientos. Es una inferencia nuestra, suponiendo esta fórmula. Y para tener un punto de precisión hacen falta casi diez mil. Diferencias de dos o tres puntos en la figura uno son ruido.

Cada línea, su término:

  • def n_para(p, margen, z=1.96) → m=z p(1−p)/nm = z\,\sqrt{p(1-p)/n} — mm: el margen, lo que va después del ±
  • return z**2 * p * (1 - p) / margen**2 → n=z2 p (1−p) / m2n = z^2\, p\,(1-p)\,/\,m^2 — elevar al cuadrado y despejar n
  • "175B vs GPT-3, 85 ± 3" → n≈1,962⋅0,85⋅0,150,032n \approx \frac{1{,}96^2 \cdot 0{,}85 \cdot 0{,}15}{0{,}03^2} — nuestra inferencia, no un dato del paper
  • n_para(0.5, 0.01) → m=0,01⇒n≈9604m = 0{,}01 \Rightarrow n \approx 9604 — un punto de precisión: diez mil juicios
python
def n_para(p, margen, z=1.96):            # despejar n de  margen = z·√(p(1−p)/n)
    return z**2 * p * (1 - p) / margen**2

casos = {"175B vs GPT-3, 85 ± 3": (0.85, 0.03),
         "175B vs few-shot, 71 ± 4": (0.71, 0.04),
         "figura 1, 0,66 ± 0,045": (0.66, 0.045)}
for nombre, (p, m) in casos.items():
    print(f"{nombre:26} → n ≈ {n_para(p, m):4.0f}")
print(f"y para ± 1 punto con p = 0,5: n ≈ {n_para(0.5, 0.01):5.0f}")
text
175B vs GPT-3, 85 ± 3      → n ≈  544
175B vs few-shot, 71 ± 4   → n ≈  494
figura 1, 0,66 ± 0,045     → n ≈  426
y para ± 1 punto con p = 0,5: n ≈  9604

Parte 3 · Veracidad

Honesto no se mide: no sabemos qué cree el modelo. Lo que sí se mide es si lo que dice es verdad.

TruthfulQA: dos tasas, no una

TruthfulQA es un dataset de preguntas armadas para que un modelo repita mitos. Una persona juzga cada respuesta dos veces: si es verdadera, y si informa algo. El modelo A responde cuatro preguntas de ejemplo, inventadas: dos bien, un mito, y una abstención. El modelo B siempre se abstiene: «I have no comment». La primera tasa cuenta verdaderas. La segunda, verdaderas y además informativas. A: cero coma setenta y cinco y cero coma cinco. B: uno y cero. Callarse siempre es verdad, pero no sirve: por eso se reportan las dos.

python
# cada respuesta, juzgada por una persona: ¿es verdadera? ¿informa algo?
modelo_a = [("Nada en particular: es un mito.", True, True),
            ("Se rompe en pedazos.", True, True),
            ("Te da siete años de mala suerte.", False, True),
            ("I have no comment.", True, False)]
modelo_b = [("I have no comment.", True, False)] * 4   # siempre se abstiene

def tasas(respuestas):
    n = len(respuestas)
    verdad = sum(v for _, v, _ in respuestas) / n
    verdad_e_info = sum(v and i for _, v, i in respuestas) / n
    return verdad, verdad_e_info

for nombre, r in [("A", modelo_a), ("B", modelo_b)]:
    v, vi = tasas(r)
    print(f"modelo {nombre}: true {v:.2f}   true + info {vi:.2f}")
text
modelo A: true 0.75   true + info 0.50
modelo B: true 1.00   true + info 0.00

Las dos tasas

true=1n∑i1[ vi ]\text{true} = \frac{1}{n}\sum_i \mathbf{1}[\,v_i\,]

viv_i: la respuesta ii es verdadera

true+info=1n∑i1[ vi∧fi ]\text{true+info} = \frac{1}{n}\sum_i \mathbf{1}[\,v_i \wedge f_i\,]

fif_i: informa algo · ∧\wedge: «y»

PPO 6B, con instruccioˊn: 0,928  vs  0,187\text{PPO 6B, con instrucción: } 0{,}928 \;\text{vs}\; 0{,}187

Tabla 14 del paper: true contra true + info

En fórmulas. v sub i vale verdadero si la respuesta i es cierta. La tasa true es el promedio de la indicadora. En el dibujo, las cuatro respuestas del modelo A: tres unos. f sub i dice si informa algo, y el símbolo de cuña es «y». Ahora la abstención es verdadera pero no informativa: cuenta cero. Pasa de verdad. En la Tabla catorce, el PPO de seis mil millones, con una instrucción que le permite contestar «I have no comment»: cero coma nueve dos ocho de verdaderas, pero cero coma uno ocho siete de verdaderas e informativas.

TruthfulQA en 175B: verdaderas e informativas

Los números reales, de la Tabla catorce: los modelos de ciento setenta y cinco mil millones, con el prompt de preguntas y respuestas, contando verdaderas e informativas. GPT tres: cero coma dos cinco uno. El SFT: apenas más. PPO: cero coma siete cinco dos. PPO-ptx: cero coma seis ocho nueve. Ese es el «más o menos el doble» del abstract. Y sin pedirle que diga la verdad.

  • GPT: 0,251
  • SFT: 0,271
  • PPO: 0,752
  • PPO-ptx: 0,689

Alucinar en una tarea cerrada

La otra medición usa tareas cerradas, como resumir: la salida no debería traer nada que no esté en la entrada. En el paper lo marca una persona, con un sí o no por respuesta. Acá, una regla de juguete. Un texto fuente, inventado. Cuatro resúmenes. Una palabra que no está en la fuente cuenta como alucinación. Y la tasa. El segundo dice que abre todos los días: falso. El cuarto inventa el wifi. La mitad alucina. La regla es burda: un sinónimo la engaña. Por eso el paper usa personas.

python
fuente = ("La biblioteca abre de lunes a viernes de 9 a 18. "
          "Los sábados abre de 10 a 14. Los domingos está cerrada.")
resumenes = ["Abre de lunes a viernes de 9 a 18 y los sábados de 10 a 14.",
             "Abre todos los días de 9 a 18.",
             "Los domingos está cerrada; los sábados abre de 10 a 14.",
             "Abre de lunes a viernes y tiene wifi gratis."]

vacias = {"y", "de", "a", "la", "los", "el"}             # no cuentan
palabras = lambda t: set(re.findall(r"\w+", t.lower())) - vacias
vocab = palabras(fuente)
for r in resumenes:
    nuevas = palabras(r) - vocab               # lo que no estaba en la entrada
    print(f"{'ALUCINA' if nuevas else 'ok     '} {sorted(nuevas)}")
tasa = sum(bool(palabras(r) - vocab) for r in resumenes) / len(resumenes)
print(f"tasa de alucinación: {tasa:.2f}")
text
ok      []
ALUCINA ['días', 'todos']
ok      []
ALUCINA ['gratis', 'tiene', 'wifi']
tasa de alucinación: 0.50

Ouyang et al., 2022 — Training language models to follow instructions with human feedback · arXiv:2203.02155

21 % vs 41 % · «InstructGPT models make up information not present in the input about half as often as GPT-3 (a 21% vs. 41% hallucination rate, respectively).»

El resultado del paper con prompts reales de la API. En tareas cerradas, InstructGPT inventa información que no estaba en la entrada la mitad de las veces que GPT tres: veintiuno contra cuarenta y uno por ciento.

Parte 4 · Toxicidad

RealToxicityPrompts: comienzos de frase sacados de la web. El modelo completa, y un clasificador puntúa de cero a uno.

Cómo elegís los prompts cambia el número

Partimos los prompts en cuatro tramos según qué tan tóxicos son, y suponemos, con números inventados, cuánta toxicidad promedio produce el modelo en cada tramo. En el dataset, la mayoría de los prompts son poco tóxicos. El paper eligió prompts en partes iguales por tramo, para ver qué pasa con los más tóxicos. Sorteamos y promediamos. Con muestreo natural, cero coma cero nueve tres. Uniforme: cero coma dos dos. El mismo modelo, más del doble. El paper lo dice: sus números absolutos están inflados. Sirven para comparar modelos entre sí.

python
# toxicidad media de la continuación según qué tan tóxico es el prompt
media_por_tramo = np.array([0.05, 0.12, 0.25, 0.45])   # tramos: 0–25 %, …, 75–100 %
natural = np.array([0.70, 0.20, 0.07, 0.03])           # cómo vienen en el dataset
uniforme = np.array([0.25, 0.25, 0.25, 0.25])          # lo que hizo el paper

def medir(pesos, n=4000):
    tramo = rng.choice(4, size=n, p=pesos)             # elegir prompts
    tox = rng.normal(media_por_tramo[tramo], 0.05).clip(0, 1)
    return tox.mean()

print(f"muestreo natural:  {medir(natural):.3f}")
print(f"muestreo uniforme: {medir(uniforme):.3f}")
text
muestreo natural:  0.093
muestreo uniforme: 0.220

Un promedio pesado

tˉ=∑k=14wk μk\bar t = \sum_{k=1}^{4} w_k\, \mu_k

wkw_k: fracción de prompts del tramo kk · μk\mu_k: toxicidad media

wk=14  ⇒  tˉ=14∑kμkw_k = \tfrac14 \;\Rightarrow\; \bar t = \tfrac14\textstyle\sum_k \mu_k

lo que hizo el paper: partes iguales

Lo mismo en fórmula. t barra es la toxicidad promedio que reportás. Mu sub k es la toxicidad media del modelo en el tramo k, y w sub k es qué fracción de los prompts sale de ese tramo; los cuatro w suman uno. En el dibujo, las cuatro columnas pesadas: casi todo el peso en el tramo menos tóxico. Con pesos iguales, un cuarto cada uno, t barra es el promedio simple de los mu. Los tramos tóxicos pesan mucho más, y el número sube, sin que el modelo cambie.

Toxicidad en 175B según la instrucción

Los números de la Tabla catorce, puntuados con Perspective API, un clasificador de toxicidad. Sin instrucción, los tres modelos de ciento setenta y cinco mil millones dan casi lo mismo. Si el prompt pide ser respetuoso, el PPO-ptx baja a cero coma uno nueve seis; GPT tres no se entera. El paper reporta que, con personas puntuando, InstructGPT genera cerca de un veinticinco por ciento menos de salidas tóxicas en ese caso. Pero si el prompt pide ser sesgado, PPO-ptx sube a cero coma cuatro: obedece. Sigue también las malas instrucciones.

promptGPTSFTPPO-ptx
sin instrucción0,2310,2110,234
«sé respetuoso»0,2330,1990,196
«sé sesgado»0,2850,2560,400

Tabla 14, RealToxicityPrompts, Perspective API (0 a 1)

Lo que esta evaluación no mide

  • Quien juzga no es quien escribió el prompt
  • Los prompts favorecen a InstructGPT: hay control con prompts de GPT-3
  • Sesgo (Winogender, CrowS-Pairs): sin mejora
  • Un número por modelo no muestra cuándo falla

Los límites que el propio paper reconoce. La persona que juzga lee el prompt pero no lo escribió: puede malinterpretar la intención. Los prompts de test vienen de gente que usaba InstructGPT, y eso le juega en contra a GPT tres; por eso repitieron la evaluación con prompts enviados a GPT tres, y el resultado no cambió mucho. En sesgo, con dos datasets de pares de frases, no hubo mejora. Y un win rate no te dice en qué prompts pierde. Eso lo miramos en el episodio ocho.

Resumiendo

  • Win rate: promedio de juicios 0/1 contra un baseline (SFT 175B =0,5= 0{,}5)
  • Figura 1: PPO-ptx 1,3B ≈ 0,53, por encima de GPT-3 175B ≈ 0,24
  • ±\pm: 1,96w^(1−w^)/n1{,}96\sqrt{\hat w(1-\hat w)/n}; ±3 puntos ≈ 500 juicios
  • TruthfulQA: true y true + info; abstenerse infla la primera
  • Alucinación 21 % vs 41 %; toxicidad baja solo si se la pedís
  • Próximo: el impuesto de alineación

Papers del episodio

¿Qué tal esta lección?

Sé el primero en calificarla

Entra para calificar

¿Terminaste la lección?

Márcala para sumar +50 XP y mantener tu racha.