Capítulo 2: Matemáticas de LoRA
Los fundamentos matemáticos de la adaptación de bajo rango
Comprender las matemáticas detrás de LoRA le ayuda a tomar mejores decisiones sobre los hiperparámetros y entender por qué funciona de manera tan efectiva.
Factorización de matrices
La ecuación principal
Para una matriz de pesos preentrenada W ∈ ℝ^(d×k):
W' = W + ΔW = W + BA
Donde:
- W es la matriz de pesos preentrenada y congelada (d × k)
- B ∈ ℝ^(d×r) es la matriz de proyección descendente
- A ∈ ℝ^(r×k) es la matriz de proyección ascendente
- r es el rango (mucho menor que d y k)
Paso hacia adelante (Forward Pass)
def forward(x, W, B, A, scaling):
# Ruta congelada
h_base = W @ x # (d × k) @ (k × batch) = (d × batch)
# Ruta LoRA
h_lora = B @ A @ x # (d × r) @ (r × k) @ (k × batch) = (d × batch)
# Combinado
return h_base + scaling * h_lora
Rango y capacidad
¿Qué es el rango?
El rango r determina la máxima "expresividad" de la actualización LoRA.
La matriz ΔW = BA tiene rango como máximo r
Si r es pequeño, ΔW solo puede representar actualizaciones en un subespacio de baja dimensión.
Conteo de parámetros
def lora_params(d, k, r):
"""Calcular los parámetros de LoRA para una matriz de pesos."""
return r * d + r * k # Matrices B + A
# Ejemplo: matriz 4096 × 4096 con r=16
params = lora_params(4096, 4096, 16)
print(f"LoRA params: {params:,}") # 131,072
# Comparado con la matriz completa
full = 4096 * 4096
print(f"Full params: {full:,}") # 16,777,216
print(f"Ratio: {params/full:.4%}") # 0.78%
Elección del rango
| Rango | Capacidad | Caso de uso |
|---|---|---|
| 4 | Baja | Tareas simples, cambios mínimos |
| 8 | Baja-Media | Fine-tuning estándar |
| 16 | Media | Recomendación por defecto |
| 32 | Media-Alta | Tareas complejas |
| 64 | Alta | Máxima expresividad |
| 128+ | Muy alta | Rara vez necesario |
Escalado alfa
El factor de escalado
LoRA incluye un factor de escalado α (alfa):
ΔW = (α/r) × BA
¿Por qué escalar?
Sin escalado, cambiar r cambia la magnitud de las actualizaciones:
- r más alto → más parámetros → actualizaciones más grandes
- r más bajo → menos parámetros → actualizaciones más pequeñas
Con el escalado α/r, puede ajustar r sin cambiar la magnitud.
Configuraciones comunes
# Patrones comunes
config1 = {"r": 16, "alpha": 16} # Escala = 1
config2 = {"r": 16, "alpha": 32} # Escala = 2
config3 = {"r": 32, "alpha": 64} # Escala = 2
# Regla general: alpha = 2 × r es común
Efecto de alfa
def effective_scaling(alpha, r):
return alpha / r
# Ejemplos
print(effective_scaling(16, 16)) # 1.0
print(effective_scaling(32, 16)) # 2.0
print(effective_scaling(64, 32)) # 2.0
Mayor α/r → mayor contribución de LoRA → adaptación más rápida pero mayor riesgo de inestabilidad.
Inicialización
Matriz A: Inicialización Kaiming/He
# Valores aleatorios pequeños
A = torch.randn(r, k) * math.sqrt(2 / k)
# O más simple:
A = torch.randn(r, k) * 0.01
Matriz B: Inicialización en cero
B = torch.zeros(d, r)
Razonamiento matemático
En la inicialización:
ΔW = BA = B × A = 0 × A = 0
h_total = Wx + (α/r) × BAx = Wx + 0 = Wx
El modelo comienza exactamente como el modelo preentrenado—sin alteración inicial.
Flujo del gradiente
Cálculo de gradientes
Para una pérdida L, necesitamos gradientes para B y A:
∂L/∂B = (α/r) × (∂L/∂h) × x^T × A^T
∂L/∂A = (α/r) × B^T × (∂L/∂h) × x^T
Propiedad clave
Los gradientes solo fluyen a través de la ruta LoRA durante el entrenamiento:
- W está congelada (sin gradientes)
- B y A reciben y actualizan gradientes
# En código
for name, param in model.named_parameters():
if "lora" in name.lower():
param.requires_grad = True
else:
param.requires_grad = False
Análisis de memoria
Componentes de memoria en entrenamiento
Para una matriz de pesos W ∈ ℝ^(d×k):
| Componente | Full FT | LoRA |
|---|---|---|
| Pesos | dk × 2 | dk × 2 (congelados) |
| Gradientes | dk × 2 | 2rk × 2 |
| Optimizador | dk × 8 | 2rk × 8 |
| Total | 12dk | 2dk + 20rk |
def memory_comparison(d, k, r, bytes_per_param=2):
"""Comparar uso de memoria."""
# Full FT: pesos + gradientes + optimizador (2 estados)
full_ft = d * k * (2 + 2 + 8) # 12 bytes por parámetro
# LoRA: pesos congelados + entrenamiento LoRA
frozen = d * k * 2 # Solo pesos, sin gradientes/optimizador
lora = r * (d + k) * (2 + 2 + 8) # Entrenamiento completo
lora_total = frozen + lora
print(f"Full FT: {full_ft / 1e9:.2f} GB")
print(f"LoRA: {lora_total / 1e9:.2f} GB")
print(f"Ahorro: {(1 - lora_total/full_ft) * 100:.1f}%")
memory_comparison(4096, 4096, 16)
Matemáticas de la selección de rango
Visión desde la teoría de la información
El rango determina cuántas "direcciones" independientes puede modificar la actualización:
- Rango r permite r modificaciones independientes
- Rango más alto = mayor expresividad pero más parámetros
Descomposición en valores singulares
Cualquier matriz puede descomponerse:
M = UΣV^T
Si M tiene rango efectivo bajo (la mayoría de los valores singulares son pequeños), una aproximación de bajo rango funciona bien.
Selección práctica
def estimate_rank(task_complexity, model_size):
"""Heurística para la selección de rango."""
base_rank = 8 # Buen valor por defecto
# Ajuste por complejidad
if task_complexity == "simple":
rank = max(4, base_rank // 2)
elif task_complexity == "moderate":
rank = base_rank
elif task_complexity == "complex":
rank = base_rank * 2
else:
rank = base_rank * 4
return rank
# Ejemplos
print(estimate_rank("simple", "7B")) # 4
print(estimate_rank("moderate", "7B")) # 8
print(estimate_rank("complex", "7B")) # 16
Matemáticas de la fusión
Operación de fusión
En inferencia, podemos fusionar LoRA en los pesos base:
def merge_lora(W, B, A, alpha, r):
"""Fusionar LoRA en los pesos base."""
scaling = alpha / r
W_merged = W + scaling * (B @ A)
return W_merged
Después de la fusión
h = W_merged × x
Equivalente a:
h = (W + (α/r) × BA) × x
Pero calculado con una sola multiplicación de matrices—sin sobrecarga.
Resumen de ecuaciones clave
| Concepto | Ecuación |
|---|---|
| Paso hacia adelante | h = Wx + (α/r)BAx |
| Conteo de parámetros | (d + k) × r |
| Factor de escalado | α/r |
| Pesos fusionados | W' = W + (α/r)BA |
| En la inicialización | BA = 0 (B es cero) |
Puntos clave
✅ LoRA aproxima las actualizaciones de pesos como productos de matrices de bajo rango
✅ El rango r controla la capacidad; α controla la magnitud
✅ Escalado efectivo = α/r; el valor común es 2
✅ La inicialización en cero de B asegura un inicio limpio
✅ El ahorro de memoria proviene de no almacenar gradientes/optimizador para los pesos congelados
✅ La fusión combina LoRA en los pesos base sin sobrecarga
Próximos pasos
Continúe con Capítulo 3: Implementando LoRA con PEFT para una implementación práctica.
Verificación rápida
Antes de continuar, asegúrese de poder responder:
- ¿Cuál es el factor de escalado efectivo para r=16, alpha=32?
- ¿Cuántos parámetros añade LoRA para una matriz de 4096×4096 con r=8?
- ¿Por qué se inicializa B en cero y no A?