- LoRA (Low-Rank Adaptation) ist eine Methode, die die Kosten des Fine-Tunings senkt, indem nicht das gesamte LLM neu trainiert wird, sondern nur kleine niedrig-rangige Matrizen aktualisiert werden; dieses Studio implementiert LoRA-Schichten direkt und zeigt ihre Funktionsweise
- Der Kern besteht darin, die Gewichtsänderung ΔW beim normalen Fine-Tuning als Produkt der beiden kleinen Matrizen
A und B zu approximieren; je kleiner der Rang r, desto geringer sind sowohl die Zahl der trainierbaren Parameter als auch die Darstellungskapazität
- Eine Gewichtsmatrix mit 5.000×10.000 hat 50 Millionen Parameter, aber LoRA mit
r=8 fügt nur B mit 5.000×8 und A mit 8×10.000 hinzu und kommt so auf 120.000 Parameter – 400-mal kleiner
- Bei der IMDb-Sentiment-Klassifikation auf Basis von DistilBERT erreichte das Standard-LoRA 89,44 % Test acc, höher als die 86,22 % beim Training nur der letzten zwei Schichten, aber niedriger als die 92,31 % beim vollständigen Fine-Tuning
- Nach Hyperparameter-Suche erreichte LoRA mit etwa 500.000 trainierbaren Parametern 92,96 % Val acc und 92,39 % Test acc und lag damit leicht über dem vollständigen Fine-Tuning, bei dem 66.955.010 Parameter trainiert wurden
Wie LoRA die Kosten des Fine-Tunings reduziert
- LoRA steht für Low-Rank Adaptation und ist eine Methode, um LLMs effizienter zu fine-tunen
- Beim normalen Fine-Tuning werden alle Parameter eines Deep-Learning-Modells angepasst, LoRA aktualisiert dagegen nur eine kleine Menge niedrig-rangiger Matrizen
- Vortrainierte LLMs lassen sich für viele Aufgaben nutzen, doch um sie an einen bestimmten Datensatz oder eine konkrete Aufgabe anzupassen, ist Fine-Tuning nützlich
- Je größer das Modell wird, desto höher werden die Rechenkosten, wenn alle Schichten aktualisiert werden
ΔW als Produkt kleiner Matrizen approximieren
- Beim normalen Fine-Tuning wird das Update der Gewichtsmatrix
W als ΔW berechnet
- LoRA approximiert
ΔW als Produkt der beiden kleinen Matrizen A und B
- Wer mit PCA oder SVD vertraut ist, kann sich das ähnlich wie eine Zerlegung von
ΔW in A und B vorstellen
- Der Rang
r ist ein Hyperparameter von LoRA
- Ein kleineres
r reduziert die Zahl der trainierbaren Parameter, beschleunigt das Training und senkt die Rechenanforderungen
- Gleichzeitig verringert sich aber auch die Kapazität der niedrig-rangigen Matrizen, aufgabenspezifische Informationen zu erfassen
- Beispiel einer Gewichtsmatrix mit 5.000×10.000:
- Normales Update
ΔW: insgesamt 50 Millionen Parameter
- LoRA mit
r=8: B 5.000×8, A 8×10.000
- Zusätzliche Parameter: 80.000 + 40.000 = 120.000
- 400-mal kleiner als klassisches Fine-Tuning
- In der Praxis sollte man verschiedene
r-Werte ausprobieren, um ein Gleichgewicht zwischen Leistung und Kosten zu finden
LoRA-Schicht mit PyTorch implementieren
- Die grundlegende
LoRALayer erhält Eingabedimension, Ausgabedimension, Rang und den Skalierungsfaktor 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 ist die Eingabedimension der Schicht, auf die LoRA angewendet wird, out_dim ist die Ausgabedimension
rank steuert die Komplexität der Matrizen A und B sowie die Anzahl der zusätzlichen Parameter durch LoRA
alpha bestimmt die Stärke der Änderung, die LoRA auf die bestehenden Modellgewichte ausübt
- Ein höheres
alpha passt das Modellverhalten stärker an
- Ein niedrigeres
alpha führt zu feineren Änderungen
A wird mit kleinen Zufallswerten initialisiert, wobei die Standardabweichung über die Quadratwurzel des Rangs festgelegt wird
- Das ist eine Wahl, um zu verhindern, dass die anfänglichen
A-Werte zu groß werden
B wird mit 0 initialisiert
- Vor Trainingsbeginn gilt daher bei
B=0 auch AB=0
- Bevor
A und B per Backpropagation aktualisiert werden, beeinflusst die LoRALayer die ursprünglichen Gewichte nicht
Linear-Schichten durch LinearWithLoRA ersetzen
- LoRA wird meist auf Linear-/Feedforward-Schichten in neuronalen Netzen angewendet
- Wenn der bestehende Forward-Pass zwei Linear-Schichten nacheinander aufruft, wird nach Anwendung von LoRA die LoRA-Ausgabe jeweils zur Ausgabe der Linear-Schicht addiert
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
- Beim Anpassen eines bestehenden PyTorch-Modells ist es am einfachsten, jede
Linear-Schicht durch LinearWithLoRA zu ersetzen
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 enthält sowohl die ursprüngliche Linear-Schicht als auch die neue LoRALayer
- Ersetzt man die
Linear-Schichten eines vortrainierten Modells durch LinearWithLoRA, kann man es mit LoRA fine-tunen
IMDb-Klassifikationsversuch mit DistilBERT
- Das praktische Beispiel verwendet Textklassifikation, weil sich Genauigkeit dort leichter bewerten lässt als bei generiertem Text
- Als Modell wird das vortrainierte DistilBERT aus Hugging Face transformers verwendet
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(
"distilbert-base-uncased", num_labels=2)
- Um nur die neuen LoRA-Gewichte zu trainieren, wird
requires_grad für alle Modellparameter auf False gesetzt
for param in model.parameters():
param.requires_grad = False
- DistilBERT hat 6 Transformer-Schichten, und in jeder davon gibt es Linear-Schichten
- Im Attention-Teil gibt es
q_lin, k_lin, v_lin, out_lin
- Im FFN gibt es
lin1, lin2
- Auf der Ausgabeseite gibt es zwei Linear-Schichten:
pre_classifier und classifier
Konfiguration für selektive LoRA-Anwendung
- Die Standard-LoRA-Konfiguration wendet LoRA nur auf die query- und value-Gewichtsmatrizen der Attention an
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
- In einer Schleife werden in jeder Transformer-Schicht von DistilBERT die ausgewählten Linear-Schichten durch
LinearWithLoRA ersetzt
lora_query=True ersetzt q_lin
lora_key=True ersetzt k_lin
lora_value=True ersetzt v_lin
lora_projection=True ersetzt out_lin
lora_mlp=True ersetzt ffn.lin1 und ffn.lin2
lora_head=True ersetzt pre_classifier und classifier
- Nach dem Ersetzen lässt sich in der Modellausgabe sehen, dass
q_lin, v_lin usw. zu LinearWithLoRA geworden sind
Vergleich von Standard-LoRA und klassischem Fine-Tuning
- Ergebnisse für die Klassifikation von IMDb Movie Reviews mit der Standard-LoRA-Konfiguration:
- Train acc: 92,15 %
- Val acc: 89,98 %
- Test acc: 89,44 %
- Ergebnisse beim Fine-Tuning nur der letzten zwei Ausgabeschichten:
- Train acc: 86,68 %
- Val acc: 87,26 %
- Test acc: 86,22 %
- Anzahl trainierbarer Parameter: 592.130
- Standard-LoRA erreichte eine höhere Test acc als das Training nur der letzten zwei Schichten und benötigte mit 147.456 trainierbaren Parametern weniger Parameter
- Ergebnisse beim klassischen Fine-Tuning aller Schichten:
- Train acc: 96,41 %
- Val acc: 92,80 %
- Test acc: 92,31 %
- Anzahl trainierbarer Parameter: 66.955.010
- Vollständiges Fine-Tuning erzielt eine um etwa 2 % höhere Test acc als Standard-LoRA, aktualisiert aber rund 450-mal mehr Parameter
Suche nach LoRA-Hyperparametern
- Die LoRA-Leistung kann je nach
lora_r, lora_alpha und Auswahl der Zielschichten variieren
03_finetune-lora.py nimmt Hyperparameter als Kommandozeilenargumente entgegen
python 03_finetune-lora.py --lora_alpha 32 --lora_r 16
- Es lassen sich auch weitere LoRA-Zielbereiche gleichzeitig aktivieren
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 führt auf allen verfügbaren GPUs das folgende Grid aus
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"]
- Das Skript kann in Visual Studio Code, im Kommandozeilen-Terminal oder als Job ausgeführt werden; ein Job wird nach Abschluss automatisch beendet
- Die Ergebnisse werden in
results.txt gespeichert
Beste Konfiguration aus der Grid-Suche
- Laut
results.txt sieht die beste Hyperparameter-Konfiguration wie folgt aus
lora_r: 8
lora_alpha: 1
lora_query: True
lora_key: False
lora_value: True
lora_projection: False
lora_mlp: True
lora_head: False
- Ergebnisse dieser Konfiguration:
- Val acc: 92,96 %
- Test acc: 92,39 %
- Diese LoRA-Konfiguration hat etwa 500k trainierbare Parameter und damit deutlich weniger als die 66M Parameter beim vollständigen Fine-Tuning
- Die Genauigkeit liegt leicht über den Werten des vollständigen Fine-Tunings mit Val acc 92,80 % und Test acc 92,31 %
Laufzeitumgebung und zusätzliche Materialien
- Mit einem Klick auf Run oben im Studio lässt sich die Umgebung inklusive Code klonen
- Nach dem Klonen des Studios können die Codedateien ohne zusätzliche Installation, Downloads oder Konfigurationsschritte ausgeführt werden
- Zugehörige Notebooks und Skripte:
00_lora-layer.ipynb: Implementierung der LoRA-Schicht
01_finetune-last-layers.ipynb: Fine-Tuning der letzten Schichten
02_finetune-with-lora.ipynb: LoRA-Fine-Tuning
03_finetune-lora.py: Ausführung mit LoRA-Hyperparameterargumenten
03_gridsearch.py: Grid-Suche für LoRA-Hyperparameter
04_finetune-all-layers.ipynb: Fine-Tuning aller Schichten
- Zusätzliche Materialien:
1 Kommentare
Meinungen auf Hacker News
Der Ablauf der Methode folgt Maxime Labonnes LLMs 101: https://github.com/mlabonne/llm-course#4-supervised-fine-tun...
LoRA != LoRa, daher ist das ständig verwirrend. Ich mag es nicht, dass ein bereits existierendes Akronym wiederverwendet wurde
Besonders an Orten wie der HN-Startseite, wo beide Bedeutungen naheliegend sind
Es fühlt sich immer noch seltsam an, dass man in der Informatik Dinge sagt wie: „Wir wissen nicht genau, wie diese Zahlen, also die Hyperparameter, das Ergebnis beeinflussen; probieren wir einfach mehrere Werte aus und nehmen den, der am besten funktioniert“
Manchmal landet man statt beim Optimum bzw. der richtigen Antwort in einem lokalen Maximum, aber es funktioniert trotzdem. Da es sich nicht mit einer geschlossenen Formel lösen lässt, zieht man eben Milliarden von Zufallsstichproben, um den gewünschten Wert zu finden. Das heißt nicht, dass LLMs dasselbe sind, aber solche Ansätze werden ziemlich häufig verwendet
Bisher war der Großteil der Branche ingenieurmäßig entworfen; LLMs sind eher etwas Entdecktes
Idealerweise bräuchte man eine theoretische Grundlage, aber um genug Daten herauszuziehen, um eine Theorie zu entwickeln oder zu überprüfen, ist manchmal zufällige Suche nötig
Wir liegen mehrere Jahre hinter dem, wo wir sein sollten. Als ich in den 1990ern in der Spielebranche arbeitete, galt es als „Allgemeinwissen“, dass neuronale Netze bestenfalls eine Sackgasse und schlimmstenfalls Betrug seien. Es ist wirklich bedauerlich, dass wir so viel Zeit verloren haben, weil ein paar Autoritäten alle davon abgehalten haben; diesmal sollten wir verhindern, dass sich das wiederholt
Es ist noch nicht klar, wann man Fine-Tuning machen und wann man RAG verwenden sollte.
Früher dachte ich, Fine-Tuning diene hauptsächlich dazu, das Verhalten eines Modells zu ändern, aber in letzter Zeit scheinen einige Unternehmen Fine-Tuning auch zum Hinzufügen von Wissen zu verwenden. Mich interessiert, wofür Fine-Tuning hauptsächlich eingesetzt wird
Der Haupteinsatzzweck ist meiner Ansicht nach weiterhin Verhaltensänderung. Dazu gehören Instruction-Fine-Tuning, Fine-Tuning für Klassifikation usw.
Wissen in die Gewichte einzubauen, macht man am besten über Pretraining. Oder wenn es eine externe Datenbank oder Dokumente gibt, die während der Generierung abgefragt werden sollen, kann man wie bei Fragen RAG verwenden. Nebenbei: Bei der NeurIPS 2023 LLM Efficiency Challenge haben alle Gewinner, die innerhalb von 24 Stunden mit einer einzigen GPU das „beste“ LLM feinabgestimmt haben, LoRA oder QLoRA (quantisiertes LoRA) verwendet
Wenn die zusätzlichen Daten nicht knapp sind oder Kontext benötigen, ist Fine-Tuning besser als RAG.
Wenn der Kontext zu umfangreich oder unscharf ist, kann die Prompt-Folgsamkeit verwässert werden, und RAG ermöglicht es dem Modell nicht, höherdimensionale Token-Assoziationen zu lernen. Deshalb muss man darauf hoffen, die benötigten Inhalte zufällig aus dem Zusatzmaterial herauszuziehen; dann ist es kaum besser als eine fortgeschrittene Suchmaschine. Das ist besonders problematisch bei spezialisierten Korpora mit eigenen Mikro-Dialekten, die in öffentlichen Datensätzen kaum vorkommen, etwa interne Dokumente von Behörden oder Großunternehmen
Nach meinem Verständnis ist Fine-Tuning ungewöhnlich effektiv [0]. Denn In-Context Learning hängt stark davon ab, wie leistungsfähig das Basismodell ist und wie RAG aufgebaut ist, also von Query-Verarbeitung, Embedding-Suche, Ergebnis-Ranking usw. [1]
Laut den Papers, die ich gelesen habe, kann Fine-Tuning neues Domänenwissen hinzufügen oder bestimmtes Wissen verstärken, während RAG auf Verstärkung beschränkt ist. Allerdings können beide Methoden trotz unterschiedlicher Trade-offs ein ähnliches Leistungsniveau zeigen [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
Das sind autoregressive Modelle. Wenn es einen neuen Typ von Sequenz gibt, bei dem sich spätere Elemente aus dem Vorhergehenden vorhersagen lassen, und diese Art anders ist als das, was das Modell zuvor gesehen hat, scheint Fine-Tuning sinnvoll.
Als Kriterium dafür, was man in einer konkreten Datensituation tun sollte, ist das ziemlich vage, aber als grobe Heuristik könnte es ausreichen. Ob das Hinzufügen von Wissen darunter fällt, ist ohne Experimente vielleicht Geschmackssache
Guter Artikel. Ich bin zwar nicht aus diesem Bereich, aber als ich das Original-Paper gelesen habe, hatte ich verstanden, dass LoRA nur auf die letzte Dense-Schicht angewendet wird und nicht unabhängig auf alle Schichten. Vielleicht habe ich es auch falsch gelesen.
Ich habe etwas nachgeforscht, warum die Implementierung im Link so ist: QLoRA hat diesen Ansatz verwendet, und er scheint interessante Effekte zu haben. Es wäre gut, eine Notiz zu dieser Entscheidung in QLoRA hinzuzufügen. Allerdings verstehe ich nicht wirklich, warum das funktioniert; aus Anfängerperspektive ergibt es Sinn, LoRA auf die letzte Schicht anzuwenden, aber die Begründung dafür, es für jede lineare Schicht zu wiederholen, erschließt sich mir nicht. Kann jemand die Intuition dahinter erklären?
Je mehr Schichten man durch LoRA-Schichten ersetzt, desto mehr Freiheitsgrade hat die Optimierung. Manche Fine-Tuning-Ansätze empfehlen, nur die letzte Schicht feinzujustieren, weil man annimmt, dass sie die „höchststufige“ Repräsentation der Eingabe enthält. Andere Ansätze fine-tunen alle Schichten. Meist hängt es von Daten und Aufgabe ab, und LoRA spiegelt diese Praxis einfach wider.
Ich bevorzuge bei Axolotl den Ansatz, nicht „von Grund auf“, sondern von der Konfiguration aus zu starten. Axolotl unterstützt Fine-Tuning von Mistral und Llama 2 und außerdem viele moderne Techniken wie Sample Packing, FlashAttention und xFormers.
Statt LoRA von Grund auf zu lernen, konzentriere ich mich darauf, Fine-Tuning-Daten zu sammeln und zu kuratieren, und mache datenzentriertes Fine-Tuning.
https://github.com/Lightning-AI/lit-gpt
Namensgebung ist wirklich schwierig. Anfangs dachte ich, hier ginge es um LoRa als „long range“ oder um LoRaWAN für die Kommunikation von IoT-Sensoren.
Welche Bibliotheken werden am häufigsten für Fine-Tuning verwendet? Gemeint sind Ansätze, die nicht „von Grund auf“ starten.
Wow, ich dachte am Anfang natürlich auch, es ginge um LoRa.
Wie hoch sind die Performance-Kosten von LoRA?
Bei der Inferenz gibt es zwei Möglichkeiten. Wenn die LoRA-Werte während des Forward Pass dynamisch hinzuaddiert werden, kann das theoretisch etwas langsamer sein; es kann aber auch ein Vorteil sein, wenn man pro Kunde kleine Gewichtssätze separat vorhalten möchte. Denn so kann man ein großes Basismodell betreiben und die kundenspezifischen LoRA-Gewichte bei Bedarf direkt anwenden. Wenn man die LoRA-Gewichte wieder in das Basismodell merged, erreicht man exakt dieselbe Performance wie mit dem Basismodell.