Saltar al contenido principal

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​

RangoCapacidadCaso de uso
4BajaTareas simples, cambios mínimos
8Baja-MediaFine-tuning estándar
16MediaRecomendación por defecto
32Media-AltaTareas complejas
64AltaMáxima expresividad
128+Muy altaRara 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):

ComponenteFull FTLoRA
Pesosdk × 2dk × 2 (congelados)
Gradientesdk × 22rk × 2
Optimizadordk × 82rk × 8
Total12dk2dk + 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​

ConceptoEcuación
Paso hacia adelanteh = Wx + (α/r)BAx
Conteo de parámetros(d + k) × r
Factor de escaladoα/r
Pesos fusionadosW' = W + (α/r)BA
En la inicializaciónBA = 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:

  1. ¿Cuál es el factor de escalado efectivo para r=16, alpha=32?
  2. ¿Cuántos parámetros añade LoRA para una matriz de 4096×4096 con r=8?
  3. ¿Por qué se inicializa B en cero y no A?