Saltar al contenido
← LoRA y fine-tuning eficiente

Lección 10 de 10 · Más allá · 14 min

Qué aprende y qué olvida

El ΔW de LoRA contra el de un fine-tuning completo: rango, norma y una tarea vieja

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.


LoRA. Episodio diez, el último: qué aprende y qué olvida. LoRA es low-rank adaptation, adaptación de rango bajo. En nueve episodios armamos la fórmula, la implementamos, la abrimos en peft, la servimos, la cuantizamos y la variamos. Falta la pregunta de fondo: ¿el cambio que aprende LoRA se parece al de un fine-tuning completo? ¿Y qué le pasa a lo que el modelo ya sabía?

Dos preguntas, un experimento

  • Qué aprende: rango y norma de ΔW\Delta W
  • Qué olvida: el acierto en la tarea vieja
  • El paper: «learns less and forgets less»
  • Lo probamos en un juguete, y no coincide del todo

Qué aprende: comparamos la matriz del cambio, delta W, de LoRA y del fine-tuning completo, con la SVD, singular value decomposition, de la serie de álgebra. Qué olvida: medimos cuánto empeora una tarea que el modelo ya sabía. Un paper de dos mil veinticuatro lo resume en el título: LoRA aprende menos y olvida menos. Lo probamos en un juguete. Spoiler: la primera mitad se cumple; la segunda, acá, no.

Parte 1 · El experimento

La red de dígitos del episodio ocho. Tarea vieja: reconocer dígitos. Tarea nueva: reconocerlos en espejo.

Cómo se mide

Partimos de la misma base, entrenada en dígitos, y la copiamos. Una copia se ajusta entera; a la otra le ponemos LoRA de rango dieciséis en las tres capas. Las dos, en dígitos en espejo. Qué aprendieron: el acierto en espejo. Qué olvidaron: el acierto en los dígitos normales. Y qué cambió adentro: la SVD de delta W en la capa del medio, de doscientos cincuenta y seis por doscientos cincuenta y seis.

base: dígitos → copia → fine-tuning total → LoRA r = 16 → acierto en espejo → acierto en dígitos → ΔW: SVD

Completo contra LoRA

Copia de la base para el ajuste completo. Doscientos pasos de Adam, adaptive moment estimation, sobre todos los pesos. La otra copia, con la LoRALinear del episodio cinco en las tres capas. El mismo entrenamiento, solo A y B. Y medimos las dos tareas. La base: cuarenta y cinco coma nueve en espejo, noventa y siete coma siete en dígitos. El ajuste completo aprende el espejo, noventa y cuatro coma seis, y baja a setenta y siete coma seis en dígitos. LoRA aprende lo mismo, noventa y cuatro coma ocho, con un quinto de los parámetros. Pero baja a sesenta y uno: olvidó más.

python
ft = copy.deepcopy(base)                     # fine-tuning completo
entrenar(ft, espejo, lr=1e-3)                # todos los pesos

torch.manual_seed(1)                         # la misma semilla en todo el episodio
lo = copy.deepcopy(base)                     # LoRA de rango 16
for i in [0, 2, 4]:
    lo[i] = LoRALinear(lo[i], r=16, alpha=32)
entrenar(lo, espejo, lr=1e-3)                # solo A y B

print("             espejo  dígitos")
for nombre, m in [("base", base), ("completo", ft), ("LoRA r=16", lo)]:
    print(f"{nombre:10} {acierto(m, espejo):8.1%} {acierto(m, X):8.1%}")
for nombre, m in [("completo", ft), ("LoRA r=16", lo)]:
    n = sum(p.numel() for p in m.parameters() if p.requires_grad)
    print(f"entrenan en {nombre}: {n:,}")
text
             espejo  dígitos
base          45.9%    97.7%
completo      94.6%    77.6%
LoRA r=16     94.8%    61.0%
entrenan en completo: 85,002
entrenan en LoRA r=16: 17,568

La SVD de lo que cambió

Ahora adentro, en la capa del medio. Sumamos LoRA de rango cuatro, con un learning rate más alto para que aprenda lo mismo. D es delta W: lo que cambió. Sus valores singulares. El rango. El rango noventa: cuántos valores singulares juntan el noventa por ciento de la energía. Y la norma. El ajuste completo cambió la matriz en doscientas doce direcciones, pero con nueve ya junta el noventa por ciento; norma seis coma cinco cinco. LoRA de rango dieciséis: rango dieciséis, rango noventa tres, norma nueve coma seis siete. Rango cuatro: dos, y norma trece coma cinco nueve. Menos direcciones, cambios más grandes.

python
espectros = {}
for nombre, m in [("completo", completo(1e-3)), ("LoRA r=16", con_lora(16, 1e-3)),
                  ("LoRA r=4", con_lora(4, 3e-3))]:
    D = delta(m)                             # 256 × 256: lo que cambió
    S = torch.linalg.svdvals(D)
    espectros[nombre] = (S / S[0]).tolist()
    print(f"{nombre:9} rango {torch.linalg.matrix_rank(D):3},",
          f"rango90 {rango90(D):2}, norma {D.norm():5.2f}")
text
completo  rango 212, rango90  9, norma  6.55
LoRA r=16 rango  16, rango90  3, norma  9.67
LoRA r=4  rango   4, rango90  2, norma 13.59

Rango noventa y norma

ΔW=∑iσi uivi⊤\Delta W = \textstyle\sum_i \sigma_i\, u_i v_i^\top

SVD: capas de rango 1, ordenadas por σi\sigma_i

rango90=min⁡{k:∑i≤kσi2≥0,9∑iσi2}\text{rango}_{90} = \min\Big\{k : \textstyle\sum_{i \le k} \sigma_i^2 \ge 0{,}9 \sum_i \sigma_i^2\Big\}

cuántas capas juntan el 90 % de la energía

∥ΔW∥F=∑iσi2\lVert \Delta W \rVert_F = \sqrt{\textstyle\sum_i \sigma_i^2}

la norma de Frobenius: cuánto se movió en total

Como en la serie de álgebra, episodio seis: delta W es una suma de capas de rango uno, cada una con su valor singular sigma sub i. En el dibujo, los seis primeros del ajuste completo, relativos al mayor. El rango noventa es el menor k tal que los k primeros sigma al cuadrado suman el noventa por ciento del total. En el dibujo, la energía real del ajuste completo: repartida, y el resto todavía pesa. La norma de Frobenius, la raíz de todos los sigma al cuadrado, mide cuánto se movió la matriz. En el dibujo, LoRA de rango dieciséis: casi toda la energía en tres capas.

Valores singulares de ΔW, relativos al mayor

Los espectros. El ajuste completo baja despacio: muchas direcciones con algo de peso. LoRA de rango dieciséis cae rápido y se apaga en el dieciséis. Rango cuatro: cuatro valores y después, cero exacto. LoRA aprende un cambio de rango bajo por construcción; el ajuste completo, no: aprende en más direcciones aunque nadie se lo pida.

Biderman et al., 2024 — LoRA Learns Less and Forgets Less · arXiv:2405.09673

rango 10–100× mayor · «full finetuning learns perturbations with a rank that is 10-100X greater than typical LoRA configurations»

El paper compara LoRA y fine-tuning completo en Llama 2 de siete mil millones de parámetros, en programación y en matemática. Encuentra lo mismo que nuestro espectro, a escala: el ajuste completo aprende cambios con un rango de diez a cien veces mayor que las configuraciones típicas de LoRA. Y propone que eso explica parte de por qué, en tareas difíciles, LoRA queda por debajo.

Parte 2 · Aprender contra olvidar

Un solo punto no alcanza: según el learning rate, cada método aprende más y olvida más. Hay que mirar la curva entera.

Barrer el learning rate

Tres métodos: ajuste completo, LoRA de rango cuatro y de rango dieciséis. El completo usa un décimo del learning rate: el paper también recomienda para LoRA uno diez veces mayor. Tres learning rates para cada uno. Guardamos la norma de delta W. Y la correlación entre esa norma y lo que queda de la tarea vieja. Cada par es espejo barra dígitos. El completo: setenta y dos con noventa y tres, noventa con ochenta y nueve, noventa y cinco con setenta y ocho. LoRA de rango cuatro: noventa y uno con setenta y cuatro, hasta noventa y siete con cincuenta y uno. Rango dieciséis, peor todavía. Correlación entre norma y dígitos: menos cero coma noventa.

python
puntos = {}
for nombre, hacer in [("completo", lambda lr: completo(lr / 10)),
                      ("LoRA r=4", lambda lr: con_lora(4, lr)),
                      ("LoRA r=16", lambda lr: con_lora(16, lr))]:
    puntos[nombre] = []
    for lr in [1e-3, 3e-3, 1e-2]:            # completo: un décimo
        m = hacer(lr)
        norma = delta(m).norm().item()       # ΔW de la capa del medio
        puntos[nombre].append((acierto(m, espejo), acierto(m, X), norma))
    print(f"{nombre:9}", " ".join(f"{a:.0%}/{b:.0%}" for a, b, _ in puntos[nombre]))

todos = torch.tensor([p for v in puntos.values() for p in v])
c = torch.corrcoef(todos[:, 1:].T)[0, 1]
print(f"correlación norma ΔW vs dígitos: {c:.2f}")
text
completo  72%/93% 90%/89% 95%/78%
LoRA r=4  91%/74% 95%/59% 97%/51%
LoRA r=16 95%/61% 96%/52% 97%/44%
correlación norma ΔW vs dígitos: -0.90

Aprender (eje x) contra no olvidar (eje y)

Cada punto es una corrida; arriba a la derecha es lo ideal. El ajuste completo traza la curva más alta. LoRA de rango cuatro queda abajo. Y rango dieciséis, más abajo. En este juguete, para el mismo aprendizaje, el ajuste completo conserva más la tarea vieja. Lo contrario del título del paper.

Lo que olvida, según cuánto se movió

La pista está en la norma. En el eje horizontal, cuánto se movió la capa del medio. El completo. LoRA de rango cuatro. Rango dieciséis. Los tres métodos caen sobre una misma tendencia: cuanto más se mueve la matriz, más se olvida, sin importar quién la movió. Ojo: una correlación en nueve corridas de un juguete es una pista, no una ley. LoRA, para aprender lo mismo con pocas direcciones, tuvo que moverse más fuerte en ellas.

Por qué pocas direcciones pueden olvidar más

Δh=ΔW xviejo\Delta h = \Delta W\, x_{\text{viejo}}

lo que cambia la salida en un dígito normal

∥ΔWx∥≤σ1∥x∥\lVert \Delta W x \rVert \le \sigma_1 \lVert x \rVert

el peor caso lo pone el mayor σ\sigma

∥ΔW∥F2=∑i≤rσi2\lVert \Delta W \rVert_F^2 = \textstyle\sum_{i \le r} \sigma_i^2

con r chico, la energía se concentra: σ grandes

Delta h es cuánto cambia la salida de la capa ante una entrada vieja, x viejo: delta W por x. En el dibujo, una matriz por una columna. Si delta W apunta a direcciones que los dígitos normales usan, los rompe. Ese cambio está acotado por el mayor valor singular por el largo de x. En el dibujo, las normas reales: LoRA aprendió lo mismo con más norma. La norma al cuadrado es la suma de sigma al cuadrado, y con rango cuatro hay solo cuatro: la energía se concentra en pocas direcciones, con sigmas grandes. Pocas direcciones no quiere decir cambio chico: quiere decir un cambio concentrado, y si esas direcciones las usaba la tarea vieja, se nota.

¿Entonces el paper está mal?

  • Escala: 85 mil parámetros contra 7 mil millones
  • Tarea: el espejo usa los mismos píxeles y clases
  • Olvido medido en otros dominios, no en la misma tarea
  • El paper mismo: el balance depende del dataset

No. Mide otra cosa, en otra escala. Nuestra red tiene ochenta y cinco mil parámetros; Llama 2, siete mil millones, y el cambio de LoRA es una fracción mínima. Nuestro espejo pelea por los mismos píxeles y las mismas diez clases que la tarea vieja. El paper mide el olvido en benchmarks de sentido común, lejos de código y matemática. Y el propio paper, al comparar las curvas de aprender contra olvidar, dice que es difícil concluir que sean fundamentalmente distintas, y que el resultado cambia según el dataset. Olvida menos, en buena parte, porque aprende menos.

Biderman et al., 2024 — LoRA Learns Less and Forgets Less · arXiv:2405.09673

depende del dataset · «difficult to conclude whether LoRA and full finetuning offer fundamentally different learning-forgetting tradeoffs»

La frase textual, de la sección cuatro punto tres. Es difícil concluir que LoRA y el ajuste completo ofrezcan balances entre aprender y olvidar fundamentalmente distintos. El título es un buen resumen, pero el cuerpo del paper es más cuidadoso. Y nuestro juguete es un recordatorio: antes de citar un resultado, probalo en tu caso.

Un remedio barato: repasar la tarea vieja

Si olvidar depende de cuánto se mueve la matriz sobre las entradas viejas, hay un remedio directo: mostrarle entradas viejas mientras aprende. Se llama replay, repaso. Tomamos una fracción de los dígitos normales de entrenamiento. Y los mezclamos con el espejo, con sus etiquetas de siempre. Cero, cinco y veinte por ciento. LoRA de rango dieciséis, el mismo entrenamiento. Sin repaso: noventa y cuatro coma ocho en espejo, sesenta y uno en dígitos. Con cinco por ciento: noventa y cuatro coma uno, y setenta y seis coma nueve. Con veinte: noventa y cuatro coma tres, y ochenta y siete coma tres. Medio punto de aprendizaje a cambio de veintiséis de memoria. La misma idea se usa con el ajuste completo, y es de las pocas que funcionan en cualquier escala.

python
print("repaso   espejo  dígitos")
for frac in [0.0, 0.05, 0.2]:                # parte de la tarea vieja que vuelve
    viejos = tr[:int(frac * len(tr))]        # dígitos normales (tr ya está mezclado)
    Z = torch.cat([espejo[tr], X[viejos]])   # espejo + repaso
    Y = torch.cat([y[tr], y[viejos]])        # con sus etiquetas de siempre
    m = lora16()                             # LoRA r = 16, como antes
    entrenar_xy(m, Z, Y)
    print(f"{frac:5.0%} {acierto(m, espejo):8.1%} {acierto(m, X):8.1%}")
text
repaso   espejo  dígitos
   0%    94.8%    61.0%
   5%    94.1%    76.9%
  20%    94.3%    87.3%

Parte 3 · Cierre de la serie

Diez episodios, una fórmula. Qué te llevás, y por dónde seguir.

La serie, de punta a punta

El recorrido. Los primeros cinco: por qué no ajustar todo, la hipótesis del rango bajo, la fórmula W más alfa sobre r por B por A, dónde ponerla, y nuestra propia clase. Los otros cinco: peft por dentro, fusionar y servir, la base en cuatro bits, las variantes, y hoy, qué cambia de verdad en el modelo.

Por qué no todo → Rango intrínseco → La fórmula → Dónde ponerlo → LoRA desde cero → peft por dentro → Fusionar y servir → QLoRA → Variantes → Aprende y olvida

Lo que te llevás

  • W+αrBAW + \tfrac{\alpha}{r}BA: un cambio de rango ≤r\le r, con B=0B = 0 al empezar
  • peft hace exactamente esa cuenta (diferencia 0)
  • Fusionar es gratis; deshacer en bfloat16, no
  • Antes de creer un claim, medilo en tu caso

Cuatro ideas para quedarse. LoRA es una suma: la matriz original más un cambio de rango a lo sumo r, que arranca en cero. La librería no tiene magia: peft calcula exactamente lo mismo que nuestra clase. En producción, fusionar no cuesta nada, pero deshacer la fusión en bfloat16 deja errores. Y la lección de hoy: el ajuste completo y LoRA no se ordenan solos; medí aprender y olvidar en tu tarea.

Vaswani et al., 2017 — Attention Is All You Need · arXiv:1706.03762

la próxima serie · «We propose a new simple network architecture, the Transformer, based solely on attention mechanisms»

¿Por dónde seguir? Toda esta serie supuso una arquitectura: q proj, v proj, las proyecciones de atención donde pusimos A y B. Vienen de este paper de dos mil diecisiete, que propuso el Transformer, una arquitectura basada solo en mecanismos de atención. Es la lectura recomendada para seguir, y el tema de la próxima serie: entender de dónde salen las matrices que estuvimos adaptando, qué hace cada una, y por qué q y v fueron las primeras candidatas para LoRA.

Resumiendo

  • Completo: ΔW\Delta W en 212 direcciones; rango 90 = 9
  • LoRA: rango ≤r\le r por construcción, con σ\sigma más grandes
  • Juguete: igual aprendizaje, LoRA olvidó más (61 % vs 78 %)
  • Olvido ≈ cuánto se movió: correlación −0,90 con ∥ΔW∥\lVert\Delta W\rVert
  • Paper: rango 10–100× mayor; el balance depende del dataset
  • Próximo: Attention Is All You Need

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.