1La idea: el cambio de pesos vive en pocas dimensiones
Un fine-tuning completo aprende una actualización $\Delta W$ para cada matriz de pesos y la suma al valor entrenado previamente: $W = W_0 + \Delta W$. LoRA parte de una observación empírica: aunque $W_0$ sea enorme, la actualización que hace falta para especializar el modelo tiene un "rango intrínseco" bajo. Es decir, $\Delta W$ se puede aproximar bien con el producto de dos matrices delgadas. En lugar de aprender los $d\times k$ números de $\Delta W$, aprendes solo los de esas dos matrices.
El truco es puramente algebraico. Para un peso congelado $W_0 \in \mathbb{R}^{d\times k}$, LoRA aprende:
$W_0$ nunca cambia: sus gradientes están apagados. Los únicos parámetros que reciben gradiente son los de $B$ y $A$. Como $r$ (el rank) es pequeño — típicamente 8, 16 o 32 frente a dimensiones de miles — el número de parámetros entrenables se desploma.
2El forward con factor de escala
En cada capa adaptada, la salida se calcula como el peso base más el aporte de bajo rango, ponderado por un factor de escala $\alpha/r$:
El escalar $\alpha$ es lora_alpha. Dividir por $r$ mantiene la magnitud del
aporte estable cuando cambias el rank: si duplicas $r$, el factor $\alpha/r$ se reduce a la
mitad, de modo que subir el rank no infla automáticamente la contribución del adaptador.
Una convención común es fijar $\alpha = 2r$ (o $\alpha = r$) y ajustar desde ahí.
$A$ se inicializa aleatoria y $B$ en cero: $A \sim \mathcal{N}(0,\sigma^2)$, $B = 0 \Rightarrow \Delta W = 0$ al inicio. Esto es deliberado: el entrenamiento arranca exactamente en el modelo base, sin perturbarlo, y a partir de ahí $B$ se despega del cero. Nada de "romper" el modelo en el paso 0.
3Cuántos parámetros ahorras
Un fine-tuning completo de una matriz entrena $d\cdot k$ parámetros. LoRA entrena solo los de $B$ y $A$: $r\,d + r\,k = r\,(d+k)$. El ratio de compresión por matriz adaptada es:
Con $d=k=4096$ y $r=8$, ese ratio es $\approx 0{,}39\%$: entrenas menos de cuatro de cada mil parámetros de esa capa. Por eso el adaptador cabe en unos pocos megabytes y el entrenamiento entra en GPUs modestas. Esa economía es la razón de ser de PEFT (parameter-efficient fine-tuning): mover el mínimo de pesos posible.
4Qué capas adaptar: target_modules
LoRA no se aplica a todo el modelo, sino a un conjunto elegido de capas lineales. En un
Transformer los candidatos naturales son las proyecciones de la atención
(q_proj, k_proj, v_proj, o_proj) y las
del bloque MLP (gate_proj, up_proj, down_proj).
| Elección | Qué adapta | Cuándo |
|---|---|---|
| Solo atención | q_proj, k_proj, v_proj, o_proj | Menos parámetros, buen punto de partida; suele bastar para estilo/formato. |
| Atención + MLP | Lo anterior + gate_proj, up_proj, down_proj | Más capacidad; tareas que exigen "conocimiento" nuevo. |
all-linear | Todas las capas lineales del modelo | Cómodo y frecuente, pero el que más infla parámetros. |
Adaptar más módulos no es adaptar mejor. Cada matriz nueva agrega parámetros entrenables, riesgo de sobreajuste y peso al adaptador. Empieza por la atención, mide, y sube a MLP solo si la métrica de eval lo justifica. La regla de oro: el codo de la curva, no el máximo de módulos.
lora_dropout aplica dropout a la entrada del aporte de bajo rango; es una
regularización barata que ayuda cuando el conjunto de datos es pequeño y el adaptador
tiende a memorizar.
5Adaptador separado vs. modelo fusionado
Durante el entrenamiento y también en despliegue puedes mantener el adaptador aparte: cargas el modelo base una vez y le enchufas los pesos de $B$ y $A$. Esto habilita algo potente: entrenar varios adaptadores sobre un mismo base (uno por tarea, cliente o idioma) y cambiar entre ellos sin duplicar los gigabytes del modelo original.
Cuando ya no necesitas esa flexibilidad y quieres cero overhead en inferencia,
fusionas el adaptador dentro de los pesos base con merge_and_unload().
La operación es la misma suma del forward, resuelta una sola vez:
El resultado es una sola matriz: desaparece la ruta paralela de LoRA y la inferencia corre exactamente como el modelo original, sin la multiplicación extra por $B$ y $A$. A cambio pierdes la modularidad — ya no puedes desenchufar el adaptador. Fusiona cuando el adaptador esté congelado y elegido; mantenlo separado mientras experimentes.
6Configurarlo en PEFT
Con la librería peft de HuggingFace, todo lo anterior es una LoraConfig.
Fíjate en los tres controles del módulo: r, lora_alpha y
target_modules.
from peft import LoraConfig, get_peft_model config = LoraConfig( r=16, # rank de la descomposición lora_alpha=32, # escala α (aquí α = 2r) lora_dropout=0.05, # regularización del aporte target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], bias="none", task_type="CAUSAL_LM", ) model = get_peft_model(base_model, config) model.print_trainable_parameters() # p.ej. 0.4% del total
Tras entrenar y validar, fusionas para servir el modelo como uno solo:
# fusiona los adaptadores en los pesos base y descarga la envoltura PEFT merged = model.merge_and_unload() merged.save_pretrained("qwen3-0.6b-especializado")
merge_and_unload, la inferencia no paga overhead de LoRA.Para on-device se fusiona el adaptador LoRA en los pesos base (merge_and_unload) antes de exportar. Así el teléfono corre una sola matriz por capa, con cero overhead de inferencia: el adaptador no viaja aparte. LoRA es una herramienta de entrenamiento eficiente; en producción móvil se aplana.
- Instala
pip install peft transformers. - Configura
LoraConfigconr,lora_alphaytarget_modules. - Entrena barriendo
r ∈ {4,8,16,32,64}. - Grafica la métrica de eval vs el número de parámetros entrenables e identifica el codo.
- Fusiona el mejor adaptador con
merge_and_unloady verifica quegenerate()coincide con el adaptador sin fusionar.
El entrenamiento de LoRA es Python (PEFT). Pero para inferencia, candle sabe cargar y fusionar adaptadores LoRA en Rust.
Lo habitual para móvil es exportar el modelo ya fusionado a GGUF (Módulo 9) y cargarlo desde el runtime del teléfono.
Lecturas y recursos
✦Ejercicios
De menor a mayor complejidad. El último es el que hace un practicante de verdad.
Cuenta los parámetros entrenables
Envuelve Qwen3-0.6B con una LoraConfig de r=8 sobre solo la atención y llama a print_trainable_parameters(). Anota el porcentaje frente al total.
Entrega: el número de parámetros entrenables y su ratio. Pista: compáralo con la fórmula $\dfrac{r(d+k)}{dk}$ por matriz.
Verifica la inicialización en cero
Justo después de get_peft_model, extrae los pesos de $B$ y $A$ de una capa adaptada. Comprueba que $B = 0$ y que, por tanto, $\Delta W = BA = 0$ antes de entrenar.
Entrega: assert de que la norma de $BA$ es cero al inicio. Pista: recorre named_parameters() buscando lora_A / lora_B.
Barrido de lora_alpha
Con r fijo, entrena adaptadores idénticos variando lora_alpha $\in \{r, 2r, 4r\}$ sobre un conjunto pequeño. Grafica la pérdida de eval frente a $\alpha$ y explica el efecto del factor $\alpha/r$.
Entrega: curva eval-vs-α + una frase sobre por qué escala el aporte. Pista: mantén todo lo demás constante (semilla, datos, pasos).
Solo atención vs. atención + MLP
Entrena dos adaptadores con el mismo r: uno con target_modules solo de atención y otro añadiendo gate_proj, up_proj, down_proj. Compara métrica de eval, parámetros entrenables y tamaño del adaptador.
Entrega: tabla de 2 filas (eval, nº params, MB) + cuál gana por parámetro. Pista: mira si el MLP paga su costo o solo agrega sobreajuste.
Frontera de Pareto y test de fusión
Barre $r \in \{4,8,16,32,64\}$ y target_modules ∈ {solo-atención, atención+MLP}
sobre Qwen3-0.6B, fijando todo lo demás; grafica la métrica de eval vs. el número de
parámetros entrenables (la frontera de Pareto) e identifica el codo; luego fusiona el mejor
adaptador y asegura que generate() coincide con el adaptador sin fusionar dentro
de tolerancia numérica.
Entrega: scatter eval-vs-params-entrenables con el $(r, \alpha, \text{target\_modules})$ elegido + un test de equivalencia de la fusión. Pista: compara generate() antes y después de merge_and_unload() con torch.allclose sobre los logits.
- LoRA congela $W_0$ y aprende $\Delta W = BA$ con $r \ll \min(d,k)$: dos matrices delgadas en vez de una densa.
- El forward suma $\dfrac{\alpha}{r}BAx$;
lora_alpha/rmantiene estable el aporte al cambiar el rank. - Init $B=0$ ⇒ el entrenamiento arranca exactamente en el modelo base, sin perturbarlo.
- El adaptador es portátil: varios sobre un mismo base;
merge_and_unloadlo fusiona para cero overhead. - Más
target_modules≠ mejor: busca el codo de la curva, no el máximo de módulos adaptados.