1 Punkte von GN⁺ 2024-01-23 | 1 Kommentare | Auf WhatsApp teilen
  • 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

 
GN⁺ 2024-01-23
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

    • Geht mir genauso. Mein Hauptberuf ist Machine Learning, und trotzdem – oder vielleicht gerade deshalb – halte ich jedes Mal kurz inne, wenn ich dieses Akronym an Stellen mit kaum Kontext sehe.
      Besonders an Orten wie der HN-Startseite, wo beide Bedeutungen naheliegend sind
    • Was ist denn die andere Bedeutung außer „Low-Rank Adaptation“? Der Unterschied ist auch schwer zu googeln
    • So etwas passiert, wenn Leute zu stark spezialisiert sind und sich nicht darum kümmern, was außerhalb ihrer eigenen Bubble passiert
    • Ich mag den Trend nicht, dass Software-Leute Namen aus dem Hardware-Bereich übernehmen
    • Es ist schade, dass inzwischen zwei voneinander unabhängige Technologien dasselbe Akronym verwenden
  • 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“

    • Ist „mehrere Werte ausprobieren und den nehmen, der am besten funktioniert“ nicht ähnlich wie der Einsatz einer Monte-Carlo-Simulation zur Wertsuche?
      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
    • Es fühlt sich an wie der Unterschied zwischen etwas, das ingenieurmäßig konstruiert wurde, und etwas, das entdeckt wurde.
      Bisher war der Großteil der Branche ingenieurmäßig entworfen; LLMs sind eher etwas Entdecktes
    • Dieses Bottom-up-Gefrickel ähnelt auch der Art, wie die Informatik in den USA entstanden ist, wie Dijkstra selbst beobachtet hat: https://www.cs.utexas.edu/users/EWD/transcriptions/EWD06xx/E...
      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
    • Es liegt zu einem großen Teil auch daran, dass Minsky und andere Perzeptronen herabsetzten, weil sie keine nichtlinearen Funktionen modellieren könnten. LLMs wären ohne moderne CPUs und GPUs ohnehin kaum entstanden, aber das heißt nicht, dass wir nicht schon früher eine bessere theoretische Grundlage hätten haben können.
      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
    • Das Gefühl ist ähnlich, wenn man die Einstellungen von Stable Diffusion untersucht. Was man schnell merkt: Es steckt viel Spekulation darin
  • 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?

    • Wie bei vielem im maschinellen Lernen hängt die Wahl der Schichten stärker von empirischen Belegen ab als von Theorie. In einer typischen LoRA-Trainingspipeline wird das Basismodell eingefroren und nur die LoRA-Schichten werden angepasst.
      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.

  • 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?

    • Während des Trainings ist es effizienter als vollständiges Fine-Tuning, weil per Backpropagation nur ein Teil der Parameter aktualisiert wird.
      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.