Chapitre 2 : Mathématiques de LoRA
Les Fondements Mathématiques de l'Adaptation de Rang Faible
Comprendre les mathématiques derrière LoRA vous aide à prendre de meilleures décisions concernant les hyperparamètres et à comprendre pourquoi cela fonctionne si efficacement.
Factorisation de Matrice
L'Équation Principale
Pour une matrice de poids pré-entraînée W ∈ ℝ^(d×k) :
W' = W + ΔW = W + BA
Où :
- W est le poids pré-entraîné figé (d × k)
- B ∈ ℝ^(d×r) est la matrice de projection vers le bas
- A ∈ ℝ^(r×k) est la matrice de projection vers le haut
- r est le rang (beaucoup plus petit que d et k)
Passage Avant
def forward(x, W, B, A, scaling):
# Chemin figé
h_base = W @ x # (d × k) @ (k × batch) = (d × batch)
# Chemin LoRA
h_lora = B @ A @ x # (d × r) @ (r × k) @ (k × batch) = (d × batch)
# Combiné
return h_base + scaling * h_lora
Rang et Capacité
Qu'est-ce que le Rang ?
Le rang r détermine la "capacité d'expression" maximale de la mise à jour LoRA.
La matrice ΔW = BA a un rang au plus égal à r
Si r est petit, ΔW ne peut représenter que des mises à jour dans un sous-espace de faible dimension.
Nombre de Paramètres
def lora_params(d, k, r):
"""Calculer les paramètres LoRA pour une matrice de poids."""
return r * d + r * k # Matrices B + A
# Exemple : matrice 4096 × 4096 avec r=16
params = lora_params(4096, 4096, 16)
print(f"Paramètres LoRA : {params:,}") # 131,072
# Comparer à la matrice complète
full = 4096 * 4096
print(f"Paramètres complets : {full:,}") # 16,777,216
print(f"Ratio : {params/full:.4%}") # 0.78%
Choix du Rang
| Rang | Capacité | Cas d'utilisation |
|---|---|---|
| 4 | Faible | Tâches simples, changements minimaux |
| 8 | Faible-Moyen | Ajustement fin standard |
| 16 | Moyen | Recommandation par défaut |
| 32 | Moyen-Élevé | Tâches complexes |
| 64 | Élevé | Capacité d'expression maximale |
| 128+ | Très Élevé | Rarement nécessaire |
Mise à l'Échelle Alpha
Le Facteur d'Échelle
LoRA inclut un facteur d'échelle α (alpha) :
ΔW = (α/r) × BA
Pourquoi Échelonner ?
Sans échelle, changer r modifie l'amplitude des mises à jour :
- r plus élevé → plus de paramètres → mises à jour plus grandes
- r plus bas → moins de paramètres → mises à jour plus petites
Avec l'échelle α/r, vous pouvez ajuster r sans changer l'amplitude.
Configurations Courantes
# Modèles courants
config1 = {"r": 16, "alpha": 16} # Échelle = 1
config2 = {"r": 16, "alpha": 32} # Échelle = 2
config3 = {"r": 32, "alpha": 64} # Échelle = 2
# Règle générale : alpha = 2 × r est courant
Effet de Alpha
def effective_scaling(alpha, r):
return alpha / r
# Exemples
print(effective_scaling(16, 16)) # 1.0
print(effective_scaling(32, 16)) # 2.0
print(effective_scaling(64, 32)) # 2.0
Plus α/r est élevé → plus grande contribution de LoRA → adaptation plus rapide mais risque accru d'instabilité.
Initialisation
Matrice A : Initialisation de Kaiming/He
# Petites valeurs aléatoires
A = torch.randn(r, k) * math.sqrt(2 / k)
# Ou plus simple :
A = torch.randn(r, k) * 0.01
Matrice B : Initialisation à Zéro
B = torch.zeros(d, r)
Raisonnement Mathématique
À l'initialisation :
ΔW = BA = B × A = 0 × A = 0
h_total = Wx + (α/r) × BAx = Wx + 0 = Wx
Le modèle commence exactement comme le modèle pré-entraîné—aucune perturbation initiale.
Flux de Gradient
Calcul des Gradients
Pour une perte L, nous avons besoin des gradients pour B et A :
∂L/∂B = (α/r) × (∂L/∂h) × x^T × A^T
∂L/∂A = (α/r) × B^T × (∂L/∂h) × x^T
Propriété Clé
Les gradients ne passent que par le chemin LoRA pendant l'entraînement :
- W est figé (pas de gradients)
- B et A reçoivent et mettent à jour les gradients
# En code
for name, param in model.named_parameters():
if "lora" in name.lower():
param.requires_grad = True
else:
param.requires_grad = False
Analyse de la Mémoire
Composants de Mémoire d'Entraînement
Pour une matrice de poids W ∈ ℝ^(d×k) :
| Composant | FT Complet | LoRA |
|---|---|---|
| Poids | dk × 2 | dk × 2 (figé) |
| Gradients | dk × 2 | 2rk × 2 |
| Optimiseur | dk × 8 | 2rk × 8 |
| Total | 12dk | 2dk + 20rk |
def memory_comparison(d, k, r, bytes_per_param=2):
"""Comparer l'utilisation de la mémoire."""
# FT Complet : poids + gradients + optimiseur (2 états)
full_ft = d * k * (2 + 2 + 8) # 12 octets par paramètre
# LoRA : poids figés + entraînement LoRA
frozen = d * k * 2 # Juste les poids, pas de gradients/optimiseur
lora = r * (d + k) * (2 + 2 + 8) # Entraînement complet
lora_total = frozen + lora
print(f"FT Complet : {full_ft / 1e9:.2f} GB")
print(f"LoRA : {lora_total / 1e9:.2f} GB")
print(f"Économies : {(1 - lora_total/full_ft) * 100:.1f}%")
memory_comparison(4096, 4096, 16)
Mathématiques de la Sélection du Rang
Vue Théorique de l'Information
Le rang détermine combien de "directions" indépendantes la mise à jour peut modifier :
- Rang r permet r modifications indépendantes
- Rang plus élevé = plus expressif mais plus de paramètres
Décomposition en Valeurs Singulières
Toute matrice peut être décomposée :
M = UΣV^T
Si M a un rang effectif faible (la plupart des valeurs singulières sont petites), une approximation de faible rang fonctionne bien.
Sélection Pratique
def estimate_rank(task_complexity, model_size):
"""Heuristique pour la sélection du rang."""
base_rank = 8 # Bon défaut
# Ajuster pour la complexité
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
# Exemples
print(estimate_rank("simple", "7B")) # 4
print(estimate_rank("moderate", "7B")) # 8
print(estimate_rank("complex", "7B")) # 16
Mathématiques de la Fusion
Opération de Fusion
Lors de l'inférence, nous pouvons fusionner LoRA dans les poids de base :
def merge_lora(W, B, A, alpha, r):
"""Fusionner LoRA dans les poids de base."""
scaling = alpha / r
W_merged = W + scaling * (B @ A)
return W_merged
Après Fusion
h = W_merged × x
Équivalent à :
h = (W + (α/r) × BA) × x
Mais calculé avec une seule multiplication de matrice—aucune surcharge.
Résumé des Équations Clés
| Concept | Équation |
|---|---|
| Passage avant | h = Wx + (α/r)BAx |
| Nombre de paramètres | (d + k) × r |
| Facteur d'échelle | α/r |
| Poids fusionnés | W' = W + (α/r)BA |
| À l'initialisation | BA = 0 (B est zéro) |
Points Clés à Retenir
✅ LoRA approxime les mises à jour de poids comme des produits de matrices de faible rang
✅ Le rang r contrôle la capacité ; α contrôle l'amplitude
✅ Échelle effective = α/r ; valeur courante est 2
✅ L'initialisation à zéro de B assure un démarrage propre
✅ Les économies de mémoire proviennent de l'absence de stockage des gradients/optimiseur pour les poids figés
✅ La fusion combine LoRA dans les poids de base sans surcharge
Prochaines Étapes
Continuez vers Chapitre 3 : Implémentation de LoRA avec PEFT pour une implémentation pratique.
Vérification Rapide
Avant de continuer, assurez-vous de pouvoir répondre :
- Quel est le facteur d'échelle effectif pour r=16, alpha=32 ?
- Combien de paramètres LoRA ajoute-t-il pour une matrice 4096×4096 avec r=8 ?
- Pourquoi B est-il initialisé à zéro au lieu de A ?