- LoRA (Low-Rank Adaptation) est une technique qui réduit le coût du fine-tuning en ne mettant à jour que de petites matrices de faible rang, sans réentraîner tout le LLM ; ce Studio implémente directement une couche LoRA pour en observer le fonctionnement
- L’idée centrale consiste à approximer la variation de poids ΔW du fine-tuning classique par le produit de deux petites matrices
A et B ; plus le rang r est faible, plus le nombre de paramètres entraînés et la capacité de représentation diminuent
- Une matrice de poids 5 000×10 000 compte 50 millions de paramètres, mais avec LoRA à
r=8, on ajoute seulement B 5 000×8 et A 8×10 000, soit 120 000 paramètres, 400 fois moins
- Sur une classification de sentiments IMDb basée sur DistilBERT, LoRA par défaut atteint 89,44 % de test acc, au-dessus des 86,22 % obtenus en entraînant seulement les deux dernières couches, mais en dessous des 92,31 % du fine-tuning complet
- Après recherche d’hyperparamètres, LoRA atteint 92,96 % de val acc et 92,39 % de test acc avec environ 500 000 paramètres entraînés, soit une précision légèrement supérieure au fine-tuning complet, qui entraîne 66 955 010 paramètres
Le coût du fine-tuning réduit par LoRA
- LoRA signifie Low-Rank Adaptation : c’est une technique destinée à rendre le fine-tuning des LLM plus efficace
- Le fine-tuning classique ajuste tous les paramètres d’un modèle de deep learning, tandis que LoRA ne met à jour qu’un petit ensemble de matrices de faible rang
- Un LLM préentraîné peut être utilisé pour de nombreuses tâches, mais le fine-tuning est utile pour l’adapter à un dataset ou à une tâche spécifique
- Plus le modèle est grand, plus la mise à jour de toutes les couches devient coûteuse en calcul
Approximer ΔW par le produit de petites matrices
- Dans le fine-tuning classique, la mise à jour d’une matrice de poids
W est calculée sous la forme ΔW
- LoRA approxime
ΔW par le produit de deux petites matrices A et B
- Si vous connaissez PCA ou SVD, on peut voir cela comme une décomposition de
ΔW en A et B
- Le rang
r est un hyperparamètre de LoRA
- Un
r plus petit réduit le nombre de paramètres entraînés, peut accélérer l’entraînement et abaisser les besoins en calcul
- En contrepartie, la capacité des matrices de faible rang à capturer les informations propres à la tâche diminue aussi
- Exemple avec une matrice de poids 5 000×10 000 :
- Mise à jour classique
ΔW : 50 millions de paramètres au total
- LoRA avec
r=8 : B 5 000×8, A 8×10 000
- Paramètres supplémentaires : 80 000 + 40 000 = 120 000
- 400 fois moins que le fine-tuning classique
- En pratique, il faut tester plusieurs valeurs de
r pour trouver le bon équilibre entre performances et coût
Implémenter une couche LoRA avec PyTorch
- La couche
LoRALayer de base reçoit la dimension d’entrée, la dimension de sortie, le rang et le coefficient de mise à l’échelle alpha
class LoRALayer(torch.nn.Module):
def __init__(self, in_dim, out_dim, rank, alpha):
super().__init__()
std_dev = 1 / torch.sqrt(torch.tensor(rank).float())
self.A = torch.nn.Parameter(torch.randn(in_dim, rank) * std_dev)
self.B = torch.nn.Parameter(torch.zeros(rank, out_dim))
self.alpha = alpha
def forward(self, x):
x = self.alpha * (x @ self.A @ self.B)
return x
in_dim est la dimension d’entrée de la couche à laquelle LoRA est appliqué, et out_dim sa dimension de sortie
rank contrôle la complexité des matrices A et B, ainsi que le nombre de paramètres ajoutés par LoRA
alpha détermine l’ampleur de la modification apportée par LoRA aux poids du modèle existant
- Un
alpha élevé ajuste davantage le comportement du modèle
- Un
alpha faible produit des modifications plus fines
A est initialisée avec de petits nombres aléatoires, avec un écart type défini par la racine carrée du rang
- Ce choix vise à éviter que les valeurs initiales de
A soient trop grandes
B est initialisée à zéro
- Avant le début de l’entraînement, comme
B=0, on a AB=0
- Tant que
A et B n’ont pas été mises à jour par rétropropagation, LoRALayer n’influence pas les poids d’origine
Remplacer les couches Linear par LinearWithLoRA
- LoRA s’applique généralement aux couches Linear / feed-forward d’un réseau de neurones
- Si le forward existant appelle deux couches Linear à la suite, après application de LoRA on ajoute la sortie LoRA à la sortie de chaque Linear
def forward(self, x):
x = self.linear_1(x) + self.lora_1(x)
x = F.relu(x)
x = self.linear_2(x) + self.lora_2(x)
return logits
- Pour modifier un modèle PyTorch existant, la méthode la plus simple consiste à remplacer chaque couche
Linear par LinearWithLoRA
class LinearWithLoRA(torch.nn.Module):
def __init__(self, linear, rank, alpha):
super().__init__()
self.linear = linear
self.lora = LoRALayer(
linear.in_features, linear.out_features, rank, alpha
)
def forward(self, x):
return self.linear(x) + self.lora(x)
LinearWithLoRA contient à la fois la couche Linear d’origine et la nouvelle LoRALayer
- En remplaçant les couches
Linear d’un modèle préentraîné par LinearWithLoRA, on peut lui ajouter LoRA puis le fine-tuner
Expérience de classification IMDb avec DistilBERT
- L’exemple pratique utilise de la classification de texte, dont l’évaluation de précision est plus simple que pour du texte généré
- Le modèle utilisé est DistilBERT préentraîné, via transformers de Hugging Face
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(
"distilbert-base-uncased", num_labels=2)
- Pour n’entraîner que les nouveaux poids LoRA, on définit
requires_grad à False pour tous les paramètres du modèle
for param in model.parameters():
param.requires_grad = False
- DistilBERT comporte 6 couches Transformer, chacune contenant des couches Linear
- Dans l’attention, on trouve
q_lin, k_lin, v_lin et out_lin
- Dans le FFN, on trouve
lin1 et lin2
- Côté sortie, on trouve deux couches Linear :
pre_classifier et classifier
Configuration pour appliquer LoRA de manière sélective
- La configuration LoRA par défaut applique LoRA uniquement aux matrices de poids query et value de l’attention
lora_r = 8
lora_alpha = 16
lora_dropout = 0.05
lora_query = True
lora_key = False
lora_value = True
lora_projection = False
lora_mlp = False
lora_head = False
- Une boucle remplace les couches Linear sélectionnées dans chaque couche Transformer de DistilBERT par
LinearWithLoRA
- Si
lora_query=True, q_lin est remplacée
- Si
lora_key=True, k_lin est remplacée
- Si
lora_value=True, v_lin est remplacée
- Si
lora_projection=True, out_lin est remplacée
- Si
lora_mlp=True, ffn.lin1 et ffn.lin2 sont remplacées
- Si
lora_head=True, pre_classifier et classifier sont remplacées
- Après remplacement, on peut vérifier dans la sortie du modèle que
q_lin, v_lin, etc. sont devenues des LinearWithLoRA
Comparaison entre LoRA par défaut et fine-tuning classique
- Résultats de l’entraînement de classification IMDb Movie Reviews avec la configuration LoRA par défaut :
- Train acc : 92,15 %
- Val acc : 89,98 %
- Test acc : 89,44 %
- Résultats du fine-tuning des deux dernières couches de sortie seulement :
- Train acc : 86,68 %
- Val acc : 87,26 %
- Test acc : 86,22 %
- Nombre de paramètres entraînés : 592 130
- LoRA par défaut obtient une test acc supérieure à la méthode qui n’entraîne que les deux dernières couches, avec moins de paramètres entraînés : 147 456
- Résultats d’un fine-tuning traditionnel de toutes les couches :
- Train acc : 96,41 %
- Val acc : 92,80 %
- Test acc : 92,31 %
- Nombre de paramètres entraînés : 66 955 010
- Le fine-tuning complet obtient une test acc environ 2 % supérieure à LoRA par défaut, mais met à jour environ 450 fois plus de paramètres que la configuration LoRA
Recherche d’hyperparamètres LoRA
- Les performances de LoRA peuvent varier selon
lora_r, lora_alpha et les couches auxquelles LoRA est appliqué
03_finetune-lora.py accepte les hyperparamètres comme arguments en ligne de commande
python 03_finetune-lora.py --lora_alpha 32 --lora_r 16
- On peut aussi activer simultanément d’autres cibles d’application de LoRA
python 03_finetune-lora.py \
--lora_alpha 32 \
--lora_r 16 \
--lora_query True \
--lora_key True \
--lora_value True \
--lora_projection True \
--lora_mlp True \
--lora_head True
03_gridsearch.py exécute la grille suivante sur tous les GPU disponibles
alpha_values = [1, 4, 8, 16, 32, 64]
rank_values = [1, 2, 4, 8, 16, 32]
lora_query = ["True"]
lora_key = ["False", "True"]
lora_value = ["True"]
lora_projection = ["False", "True"]
lora_mlp = ["False", "True"]
lora_head = ["False", "True"]
- Le script peut être exécuté dans Visual Studio Code, dans un terminal en ligne de commande ou sous forme de Job ; le Job se termine automatiquement une fois terminé
- Les résultats sont enregistrés dans
results.txt
Meilleure configuration issue de la recherche en grille
- Selon
results.txt, la meilleure configuration d’hyperparamètres est la suivante
lora_r: 8
lora_alpha: 1
lora_query: True
lora_key: False
lora_value: True
lora_projection: False
lora_mlp: True
lora_head: False
- Résultats de cette configuration :
- Val acc : 92,96 %
- Test acc : 92,39 %
- Cette configuration LoRA compte environ 500k paramètres entraînés, bien moins que les 66M paramètres du fine-tuning complet
- Sa précision est légèrement supérieure à celle du fine-tuning complet, qui atteint 92,80 % de val acc et 92,31 % de test acc
Environnement d’exécution et ressources supplémentaires
- Cliquer sur Run en haut du Studio permet de cloner l’environnement avec le code
- Après clonage du Studio, les fichiers de code peuvent être exécutés sans étape supplémentaire d’installation, de téléchargement ni de configuration
- Notebooks et scripts associés :
00_lora-layer.ipynb : implémentation d’une couche LoRA
01_finetune-last-layers.ipynb : fine-tuning des dernières couches
02_finetune-with-lora.ipynb : fine-tuning avec LoRA
03_finetune-lora.py : exécution avec arguments d’hyperparamètres LoRA
03_gridsearch.py : recherche en grille des hyperparamètres LoRA
04_finetune-all-layers.ipynb : fine-tuning de toutes les couches
- Ressources supplémentaires :
1 commentaires
Avis sur Hacker News
Le déroulé de la technique suit le cours LLMs 101 de Maxime Labonne : https://github.com/mlabonne/llm-course#4-supervised-fine-tun...
LoRA != LoRa, donc ça prête sans cesse à confusion. Je n’aime pas qu’on réutilise un acronyme qui existait déjà
C’est particulièrement vrai dans des endroits comme la une de HN, où les deux sens sont naturels
Dans l’informatique, je trouve encore étrange d’entendre des choses du genre : « on ne sait pas exactement comment ces nombres, c’est-à-dire ces hyperparamètres, influencent le résultat, donc essayons plusieurs valeurs et gardons celle qui marche le mieux »
On peut parfois tomber sur un maximum local au lieu de l’optimum/de la bonne réponse, mais ça fonctionne quand même. Comme on ne peut pas résoudre le problème avec une formule fermée, on tire au hasard quelques milliards d’échantillons pour trouver la valeur voulue ; je ne dis pas que les LLM sont identiques, mais ce genre d’approche est assez courant
Jusqu’ici, la majeure partie du secteur relevait de la conception d’ingénierie ; les LLM ressemblent davantage à quelque chose qui a été découvert
Idéalement, il faut une base théorique, mais pour extraire assez de données afin de construire ou de vérifier une théorie, il faut parfois passer par une exploration aléatoire
Nous avons plusieurs années de retard par rapport à là où nous devrions être. Quand je travaillais dans l’industrie du jeu vidéo dans les années 1990, il était de « notoriété commune » que les réseaux de neurones étaient, au mieux, une impasse et, au pire, une arnaque. C’est vraiment regrettable d’avoir perdu autant de temps parce que quelques figures d’autorité ont dissuadé tout le monde, et il faut éviter que cela se reproduise cette fois-ci
On ne sait toujours pas clairement quand faire du fine-tuning et quand utiliser RAG.
Avant, je pensais que le fine-tuning servait surtout à modifier le comportement d’un modèle, mais récemment certaines entreprises semblent aussi s’en servir pour ajouter des connaissances. Je me demande quels sont les principaux cas d’usage du fine-tuning
À mon avis, le principal cas d’usage reste le changement de comportement. C’est le cas du fine-tuning d’instructions, du fine-tuning pour la classification, etc.
Ajouter des connaissances dans les poids se fait de préférence par pré-entraînement. Ou bien, si l’on dispose d’une base de données ou de documents externes à interroger pendant la génération, on peut utiliser RAG comme pour des questions. À noter que, lors du NeurIPS 2023 LLM Efficiency Challenge, tous les gagnants qui ont fine-tuné le « meilleur » LLM en 24 heures avec un seul GPU ont utilisé LoRA ou QLoRA (LoRA quantifié)
Si les données supplémentaires ne sont pas concises ou nécessitent du contexte, le fine-tuning est meilleur que RAG.
S’il y a trop de contexte ou si le focus est dilué, le suivi du prompt peut s’affaiblir, et RAG ne permet pas au modèle d’apprendre des associations de tokens de plus haut niveau. Il faut donc avoir de la chance et extraire le bon contenu des documents d’appoint, ce qui n’est alors pas tellement mieux qu’un moteur de recherche avancé. C’est particulièrement problématique lorsqu’on traite des corpus spécialisés ayant leur propre micro-dialecte, qui apparaissent peu dans les datasets publics, comme des documents internes d’administrations ou de grandes entreprises
D’après ce que j’ai compris, le fine-tuning est anormalement efficace [0]. L’apprentissage en contexte dépend fortement de la puissance du modèle de base et de la manière dont RAG est configuré, c’est-à-dire du traitement des requêtes, de la recherche par embeddings, du classement des résultats, etc. [1]
Selon les articles que j’ai lus, le fine-tuning peut ajouter des connaissances sur un nouveau domaine ou renforcer des connaissances spécifiques, tandis que RAG se limite au renforcement. Cela dit, les deux techniques, avec des compromis différents, peuvent parfois atteindre des niveaux de capacité similaires [2]
[0] Fast.ai: Can Models learn from one sample, https://www.fast.ai/posts/2023-09-04-learning-jumps/ / https://archive.is/eJMPR
[1] LlamaIndex: Advanced RAG, https://blog.llamaindex.ai/a-cheat-sheet-and-some-recipes-fo... / https://archive.is/qtBXX
[2] Microsoft: RAG vs Fine-tuning: Pipelines, Tradeoffs, and a Case Study, https://arxiv.org/html/2401.08406v2#S6 / https://archive.is/UQ8Sa#S6
Ce sont des modèles autorégressifs. S’il existe un nouveau type de séquence où l’on peut prédire les éléments suivants à partir de ce qui précède, et si cette manière de faire diffère de ce que le modèle a déjà vu, le fine-tuning semble pertinent.
Comme critère pour décider quoi faire dans une situation de données précise, c’est assez flou, mais cela peut suffire comme heuristique générale. Quant à savoir si l’ajout de connaissances entre dans ce cadre, c’est peut-être une affaire de préférence tant qu’on n’a pas expérimenté
Très bon article. Je ne suis pas du domaine, mais quand j’ai lu l’article original, j’avais compris que LoRA ne s’appliquait qu’à la dernière couche dense, et pas indépendamment à toutes les couches. J’ai peut-être mal lu.
En creusant un peu pour comprendre pourquoi l’implémentation liée fait ça, j’ai vu que QLoRA utilisait cette approche et qu’elle semble avoir des effets intéressants. Ce serait bien d’ajouter une note sur la décision prise dans QLoRA. Cela dit, je ne comprends pas vraiment pourquoi ça fonctionne ; du point de vue d’un débutant, appliquer LoRA à la dernière couche a du sens, mais je ne vois pas bien la justification pour le répéter sur chaque couche linéaire. Quelqu’un peut expliquer l’intuition ?
Plus on remplace de couches par des couches LoRA, plus on augmente les degrés de liberté de l’optimisation. Certaines approches de fine-tuning recommandent de n’ajuster que la dernière couche, car elle est supposée contenir la représentation « de plus haut niveau » de l’entrée. D’autres ajustent toutes les couches. Dans la plupart des cas, cela dépend des données et du problème, et LoRA reflète simplement cette pratique.
Je préfère l’approche d’Axolotl, qui ne part pas « de zéro » mais d’une configuration. Axolotl prend en charge le fine-tuning de Mistral et Llama 2, ainsi que beaucoup de techniques récentes comme le sample packing, FlashAttention et xFormers.
Plutôt que d’apprendre LoRA à partir de zéro, je me concentre sur la collecte et la curation des données de fine-tuning, pour faire un fine-tuning centré sur les données.
https://github.com/Lightning-AI/lit-gpt
Le nommage est vraiment difficile. Au début, j’ai cru qu’il était question de LoRa pour « long range », ou de LoRaWAN pour la communication de capteurs IoT.
Quelles sont les bibliothèques les plus utilisées pour le fine-tuning ? Je parle des approches qui ne partent pas « de zéro ».
Waouh, moi aussi j’ai d’abord cru qu’il s’agissait évidemment de LoRa.
Quel est le coût en performances de LoRA ?
Pendant l’inférence, deux options sont possibles. Si l’on ajoute dynamiquement les valeurs LoRA pendant la passe avant, cela peut théoriquement être un peu plus lent, mais cela peut aussi être un avantage si l’on veut conserver de petits ensembles de poids distincts par client. En effet, on peut faire tourner un seul grand modèle de base et appliquer à la volée les poids LoRA propres à chaque client. Si l’on fusionne à nouveau les poids LoRA dans le modèle de base, on peut obtenir exactement les mêmes performances que le modèle de base.