
Integrieren Sie Ihr eigenes LLM in EA (Teil 5): Handelsstrategie mit LLMs entwickeln und testen (III) – Adapter-Tuning
Inhaltsverzeichnis
- Inhaltsverzeichnis
- Einführung
- Umgebung einrichten
- Erstellen des Adaptermoduls
- Umschreiben der Klasse GPT2LMHeadModel
- Adapter-Abstimmung
- Leistungsvergleich verschiedener Feinabstimmungsmethoden
- Schlussfolgerung
Einführung
Im vorangegangenen Artikel haben wir die Feinabstimmung des vortrainierten GPT-2-Modells mit Hilfe der LoRA-Methode vorgestellt und es mit dem vollständig feinabgestimmten Modell unter verschiedenen Aspekten verglichen, die uns wichtig sind, einschließlich, aber nicht beschränkt auf den Trainings-Overhead, den Inferenz-Overhead und die Modellleistung.
In diesem Artikel werden wir die Adapter-Tuning-Methode zur Feinabstimmung des vortrainierten GPT-2-Modells verwenden und sie mit den bereits vorgestellten Feinabstimmungsmethoden vergleichen. Natürlich werden wir nicht fortfahren, verschiedene Methoden zur Feinabstimmung großer Sprachmodelle vorzustellen, da ständig neue Methoden zur Feinabstimmung entwickelt werden. Ich fürchte, dass Sie nicht die Geduld haben werden, alle Methoden einzeln zu lesen, daher werde ich nur einige der grundlegendsten Feinabstimmungsmethoden vorstellen (zum Beispiel haben wir bereits die LoRA-Abstimmung eingeführt und werden nicht viel Platz darauf verwenden, die QLoRA-Abstimmung vorzustellen, eine von LoRA erweiterte Methode).
Das bedeutet, dass dies der letzte Artikel über die Feinabstimmung großer Sprachmodelle sein wird. Wenn Sie andere Methoden ausprobieren möchten, können Sie sich auf die in dieser Artikelserie erwähnte Logik der Feinabstimmung beziehen und sie auf andere Feinabstimmungsmethoden anwenden, um weiter zu forschen. Ab dem nächsten Artikel werden wir uns darauf konzentrieren, das trainierte Modell mit der EA-Entwicklung zu kombinieren, um Handelsstrategien zu entwickeln und Backtests durchzuführen.
In unserem Beispiel verwenden wir einen relativ aggressiven Ansatz, d. h. wir geben 20 Datenpunkte ein, um die nächsten 40 Datenpunkte vorherzusagen. Wir haben dies gewählt, weil es schwierig ist, die Unterschiede zu vergleichen, wenn die vorhergesagten Werte zu kurz sind. Dies ist aggressiver als bei praktischen Anwendungen, bei denen Sie vielleicht eine konservativere Strategie anwenden, indem Sie 20 Werte eingeben, um die nächsten 5 Werte vorherzusagen. Dies muss bei der Anwendung dieser Techniken auf den Echtzeithandel beachtet werden. Eine praktischere Lösung besteht darin, diese beiden Werte (Eingabe- und Ausgabelänge) als Hyperparameter festzulegen und dann einen genetischen Algorithmus für Backtests mit verschiedenen Währungspaaren und unterschiedlichen Zeiträumen einzusetzen, um die optimalen Parameter zu finden. Wir werden in dieser Artikelserie nicht speziell darauf eingehen, und die Leserinnen und Leser können versuchen, dies selbst zu tun.
Jetzt wollen wir uns darauf konzentrieren, wie man mit Hilfe von Adapter-Tuning das vortrainierte GPT-2-Modell feinabstimmen kann.
Umgebung einrichten
Im Folgenden wird die Betriebsumgebung für die in diesem Artikel enthaltenen Codebeispiele beschrieben. Das bedeutet natürlich nicht, dass Ihre Code-Umgebung die gleiche sein muss wie meine, aber wenn Sie Probleme beim Ausführen des Codes haben, können Sie sich an meiner Umgebungskonfiguration orientieren.
Betriebssystem: Ubuntu 22.04.5 LTS (oder die entsprechende Version von WSL)
Python-Version: 3.10.14
Erforderliche Python-Bibliotheken:
- torch-2.4.1
- numpy-1.26.3
- pandas-2.2.3
- transformers-4.45.1
- peft-0.13.0
- matplotlib-3.9.2
Wenn Sie mit der Konfiguration der Code-Ausführungsumgebung nicht vertraut sind, habe ich sie in anderen Artikeln dieser Reihe ausführlich beschrieben:
- Nutzer von AMD-Grafikkarten können sich auf den vorherigen Artikel beziehen: Integrieren Sie Ihr eigenes LLM in EA (Teil 4): Trainieren Sie Ihr eigenes LLM mit GPU
- Nutzer von NVIDIA-Grafikkarten können sich auf den zweiten Artikel dieser Serie beziehen: Integrieren Sie Ihr eigenes LLM in EA (Teil 2): Beispiel für den Einsatz in einer Umgebung
In diesem Artikel wird dieser Teil nicht im Detail vorgestellt.
Erstellen des Adaptermoduls
Wir haben das Adapter-Tuning im ersten Artikel dieses Abschnitts kurz vorgestellt. Im Allgemeinen ist das Adapter-Tuning eine modulare Feinabstimmungsmethode, bei der die Feinabstimmung durch Einfügen spezialisierter Adaptermodule in verschiedene Schichten des vortrainierten Modells erreicht wird. Jedes Adaptermodul kann als ein kleines neuronales Netz betrachtet werden, das für die Erfassung der Datenverteilung einer bestimmten Aufgabe verantwortlich ist. Darüber hinaus kann das Adaptermodul unabhängig vom ursprünglichen Modell trainiert werden, was für die Verwaltung und Optimierung praktisch ist.
Gleichzeitig können Adapter für mehrere Aufgaben einfach zu demselben vortrainierten Modell hinzugefügt werden, um Multi-Task-Lernen zu ermöglichen. Vor allem bei komplexen Aufgaben und begrenzter Datenmenge kann das mit Adapter-Tuning fein abgestimmte Modell eine höhere Leistung erzielen.
Natürlich kann das Adaptermodul im Vergleich zu LoRA mehr Parameter einführen, was den Speicher- und Berechnungsaufwand erhöht, und es ist notwendig, das entsprechende Adaptermodul für jede Aufgabe zu entwerfen und anzupassen, und der Entwurfsprozess ist komplizierter. Das LoRA-Tuning konzentriert sich mehr auf die Verbesserung der Anpassungsfähigkeit des Modells mit einer minimalen Anzahl von Parametern, was für Szenarien mit begrenzten Ressourcen geeignet ist und eine effiziente Feinabstimmung erfordert. Das Adapter-Tuning hingegen erfasst aufgabenspezifische Informationen durch die Einführung unabhängiger Module, was sich für Szenarien eignet, die Multi-Task-Lernen oder flexible Anpassungen erfordern.
Sobald Sie das Ziel Ihrer Aufgabe festgelegt haben, ist die Wahl der richtigen Methode entscheidend. Wenn das trainierte Modell keine guten Ergebnisse erzielt, egal wie Sie die Parameter anpassen, sollten Sie in Erwägung ziehen, das Modell oder die Trainingsmethode zu ändern, anstatt Ihre eigenen Ideen zu verwerfen.
Als Nächstes werden wir das GPT-2-Modell mit Hilfe von Adapter-Tuning Schritt für Schritt feinabstimmen. Zunächst werden wir ein Adapter-Modul und ein GPT2LMHeadModel-Modul (nämlich die Klasse GPT2LMHeadModelWithAdapters) erstellen und dann das Adapter-Modul an die Klasse GPT2LMHeadModelWithAdapters anpassen.
Um das Adaptermodul in GPT-2 zu integrieren, wird eine modifizierte Version der Klasse GPT2LMHeadModel erstellt. Dieses Beispiel stellt nur eine vereinfachte Implementierung dar. Bitte beachten Sie die Schlüsseltechnologien der Adapterintegration. Die gesamte Implementierungslogik des Adaptermoduls ist nicht kompliziert. Zunächst definieren wir eine Klasse, die von nn.Module erbt und zwei Hauptoperationen enthält: Down-Sampling (down_project) und Up-Sampling (up_project). down_project ordnet die Eingabe-Merkmale der Engpass-Schicht zu, durchläuft die ReLU-Aktivierungsfunktion und fügt Dropout hinzu, um eine Überanpassung zu verhindern. up_project ordnet die Merkmale der Engpass-Schicht wieder der ursprünglichen Dimension zu und verwendet erneut Dropout, um eine Überanpassung zu verhindern.
Nun wollen wir den Code implementieren. Definieren Sie zunächst die Klasse Adapter, die vom nn.Module aus torch class Adapter(nn.Module) erbt:
Definieren der Initialisierungsmethode der Klasse, die zwei Parameter akzeptiert: in_features und bottleneck_features: def __init__(self, in_features, bottleneck_features=64):
- in_features: Dies ist die Dimension der Eingangsmerkmale. Für das Modell GPT-2 ist dies die Dimension der Einbettungsschicht.
- bottleneck_features: Dies ist die Dimension der Engpassschicht, d. h. die Merkmalsdimension nach der linearen Projektionsschicht. Der Standardwert ist auf 64 eingestellt.
- Aufruf der Initialisierungsmethode der Elternklasse (nn.Module): super(Adapter, self).__init__()
- Definieren einer linearen Schicht (nn.Linear), um die Dimension der Eingangsmerkmale auf die Dimension der Engpassschicht zu reduzieren: self.down_project = nn.Linear(in_features, bottleneck_features)
- Definieren einer weiteren linearen Schicht, um die Merkmalsdimension von der Engpassschicht wieder auf die Dimension der Eingangsmerkmale zu erhöhen: self.up_project = nn.Linear(bottleneck_features, in_features)
- Definieren der Dropout-Schicht, die dazu dient, einen Teil der Neuronen während des Trainings zufällig zu verwerfen, um eine Überanpassung zu verhindern. Die Verwerfungswahrscheinlichkeit wird auf 0,1 gesetzt: self.dropout = nn.Dropout(0,1)
- Aufruf der Methode zur Initialisierung der Gewichte: self.init_weights()
Definieren der Gewichtsinitialisierungsmethode init_weights():
- Initialisierung der Gewichtsparameter der Schicht down_project unter Verwendung einer Normalverteilung mit einem Mittelwert von 0,0 und einer Standardabweichung von 0,02: nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02)
- Initialisierung der Bias-Parameter der Ebene down_project mit einer Konstante von 0: nn.init.constant_(self.down_project.bias, 0)
- Analog dazu wird der Gewichtsparameter der Schicht up_project mit einer Normalverteilung mit einem Mittelwert von 0,0 und einer Standardabweichung von 0,02 initialisiert: nn.init.normal_(self.up_project.weight, mean=0,0, std=0,02)
- Initialisierung der Bias-Parameter der Schicht up_project mit einer Konstante von 0: nn.init.constant_(self.up_project.bias, 0)
Definieren der Vorwärtspropagationsmethode forward(): def forward(self, hidden_states), die einen Parameter hidden_states annimmt:
- Projektion der ausgeblendeten Zustände der Eingabedaten auf die Dimension der Engpassschicht durch die lineare Schicht down_project: hidden_states = self.down_project(hidden_states)
- Durchführen einer nichtlineare Transformation der ausgeblendeten Zustände der Engpass-Schicht mit der Aktivierungsfunktion ReLU durch: hidden_states = F.relu(hidden_states)
- Dropout auf die nichtlinear transformierten ausgeblendeten Zustände anwenden, wobei ein Teil der Neuronen zufällig verworfen wird: hidden_states = self.dropout(hidden_states)
- Erhöhen der ausgeblendeten Zustände von der Dimension der Engpassschicht zurück auf die Dimension der Eingangsmerkmale durch die lineare Schicht up_project: hidden_states = self.up_project(hidden_states)
- Erneutes Anwenden von Dropout auf die neu abgetasteten ausgeblendeten Zustände: hidden_states = self.dropout(hidden_states)
- Abschließend werden die vom Adaptermodul verarbeiteten ausgeblendeten Zustände zurückgegeben: return hidden_states
Die komplette Adapterklasse:
class Adapter(nn.Module): def __init__(self, in_features, bottleneck_features=64): super(Adapter, self).__init__() self.down_project = nn.Linear(in_features, bottleneck_features) self.up_project = nn.Linear(bottleneck_features, in_features) self.dropout = nn.Dropout(0.1) self.init_weights() def init_weights(self): nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.down_project.bias, 0) nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.up_project.bias, 0) def forward(self, hidden_states): hidden_states = self.down_project(hidden_states) hidden_states = F.relu(hidden_states) hidden_states = self.dropout(hidden_states) hidden_states = self.up_project(hidden_states) hidden_states = self.dropout(hidden_states) return hidden_states
Auf diese Weise haben wir einfach ein Adaptermodul erstellt. Der nächste Schritt ist die Anpassung dieses Moduls an unser GPT-2-Modell. Dazu müssen wir die Klasse GPT2LMHeadModel neu schreiben.
Umschreiben der Klasse GPT2LMHeadModel
Wenn Sie die Klasse GPT2LMHeadModel umfassend neu schreiben wollen, wird dies ein riesiges Projekt sein. Wir stellen hier nur eine vereinfachte Version zur Verfügung, um ein Beispiel zu geben, und implementieren nur die wichtigsten Teile. Unsere Aufgabe ist es hier, das Adaptermodul an das GPT-2-Netz anzupassen und verschiedene Eingangsbedingungen und Ausgangsanforderungen des Modells zu behandeln. Nach der Initialisierung müssen wir auch die Funktion für die Vorwärtsdurchgänge forward() neu schreiben, die Transformerschicht des ursprünglichen GPT-2-Modells aufrufen, um den ausgeblendeten Zustand hidden_states zu erhalten, und dann jedes Adaptermodul der Reihe nach anwenden, wobei die Ausgabe des Adaptermoduls zum ursprünglichen ausgeblendeten Zustand hinzugefügt wird. Schließlich werden die endgültigen „logits“ durch die lineare Schicht des Sprachmodells (lm_head) generiert, und der Verlust wird berechnet. Vervollständigen wir nun den Code.
Wir definieren unsere neu geschriebene Klasse als GPT2LMHeadModelWithAdapters, die von GPT2LMHeadModel erbt: class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel)
Definieren der Initialisierungsmethode __init__() der Klasse GPT2LMHeadModelWithAdapters und Aufruf der Initialisierungsmethode der Elternklasse in der Initialisierungsmethode, um Adapter hinzuzufügen:
- Definieren der Klassenmethode __init__(self, config), die einen Konfigurationsparameter config erhält: def __init__(self, config):
- Aufruf der Initialisierungsmethode der übergeordneten Klasse: super().__init__(config)
- Initialisierung von Adaptern, der Typ ist nn.ModuleList, die Adaptermodule enthält, die der Anzahl der Schichten des GPT-2-Modells entsprechen, wobei config.n_embd die Dimension der Einbettungsschicht und config.n_layer die Anzahl der Schichten ist: self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)])
Als Nächstes implementieren wir die Methode der Vorwärtsdurchgänge forward() in der Klasse GPT2LMHeadModelWithAdapters:
- Definieren der Methode der Vorwärtsdurchgänge durch Akzeptieren der benötigten Parameter, die zur Steuerung des Verhaltens und des Eingabeformats des Modells verwendet werden (wir werden diese Parameter hier nicht einzeln vorstellen, interessierte Leser können versuchen, diese Parameter zu optimieren): def forward(self, input_ids=None, past_key_values=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, encoder_hidden_states=None, encoder_attention_mask=None, labels=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None,):
- Dann wird die Transformer-Schicht im Modell für die Vorwärtsdurchgänge aufgerufen und die Ausgabe des Modells erhalten und der Variablen transformer_outputs zugewiesen: transformer_outputs = self.transformer(input_ids, past_key_values=past_key_values, attention_mask=attention_mask, token_type_ids=token_type_ids, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds, encoder_hidden_states=encoder_hidden_states, encoder_attention_mask=encoder_attention_mask, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict,)
- Ermitteln des ausgeblendeten Zustands hidden_states der Transformerschicht, der die Eingabe ist, die das Adaptermodul verarbeiten muss: hidden_states = transformer_outputs[0]
- Als Nächstes werden alle Adaptermodule in einer for-Schleife durchlaufen, um den nächsten Schritt der Anpassung vorzubereiten: for i, adapter in enumerate(self.adapters):
- Addieren der Ausgabe jeder Schicht des Adapter-Moduls zum ursprünglichen ausgeblendeten Zustand und Zuweisen von hidden_states als neuen ausgeblendeten Zustand, der an die nächste Schicht weitergegeben wird: hidden_states = hidden_states + adapter(hidden_states)
- Nach der Verarbeitung der ausgeblendeten Zustände (hidden_states) müssen wir die verarbeiteten ausgeblendeten Zustände (hidden_states) in die Logits-Ausgabe des Sprachmodells durch die lm_head-Schicht des Modells umwandeln. Jedes Logit entspricht der Wahrscheinlichkeit einer Vokabel: lm_logits = self.lm_head(hidden_states)
Nach der Umrechnung ist dies der Link zur Berechnung des Verlustes:
- Initialisierung des Verlusts mit leer: loss = None
- Prüfen, ob Kennzeichnungen vorhanden sind: wenn labels nicht None ist:
- Entfernen des letzten Tokens der Logits-Ausgabe, da wir das nächste Token vorhersagen müssen: shift_logits = lm_logits[..., :-1, :].contiguous()
- Entfernen des letzten Tokens der Kennzeichnung, da wir das nächste Token vorhersagen müssen: shift_labels = labels[..., 1:].contiguous()
- Definieren der Verlustfunktion als Kreuzentropie-Verlust (CrossEntropyLoss), eine häufig verwendete Verlustfunktion für Klassifizierungsaufgaben: loss_fct = nn.CrossEntropyLoss()
- Verflachen von shift_logits und shift_labels (view(-1, ...)) und dann die Verwendung der Kreuzentropie-Verlustfunktion: loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
Dabei ist besonders zu beachten, dass das Sprachmodell in der Regel für die Vorhersage des nächsten Wortes trainiert wird und nicht für die direkte Vorhersage des aktuellen Wortes. Daher müssen die Modellausgabe lm_logits und die Etiketten um eine Position im Zeitschritt verschoben werden, um den Verlust genau zu berechnen. Lautet ein Satz beispielsweise „I love programming“, dann kann die Modelleingabe „I love“ lauten, und die Modellausgabe lm_logits sollte die Wahrscheinlichkeitsverteilung sein, die „Iove programming“ entspricht. Um den Verlust zu berechnen, müssen wir die Wahrscheinlichkeitsverteilung von „love programming“ mit dem Kennzeichnung (label) „programming“ abgleichen.
- Überprüfen der Konfiguration von return_dict. Wenn es auf False gesetzt ist, wird die Ausgabe berechnet und zusammengeführt: if not return_dict:
- Zusammenführen der Logits-Ausgabe mit anderen Ausgaben der Transformerschicht (mit Ausnahme der Ausgabe des ersten ausgeblendeten Zustands) zur Ausgabe: output = (lm_logits,) + transformer_outputs[1:]
- Wenn Kennzeichen angegeben werden und der Verlust berechnet wird, wird der Verlust zusammen mit der Ausgabe zurückgegeben, andernfalls wird nur die Ausgabe zurückgegeben:return ((loss,) + output) if loss is not None else output
- Wenn return_dict auf True gesetzt ist, wird die kausale Ausgabe direkt zurückgegeben: return modeling_outputs.CausalLMOutputWithCrossAttentions( loss=loss, logits=lm_logits,past_key_values=transformer_outputs.past_key_values, hidden_states=transformer_outputs.hidden_states, attentions=transformer_outputs.attentions,cross_attentions=transformer_outputs.cross_attentions,)
class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel): def __init__(self, config): super().__init__(config) self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)]) def forward( self, input_ids=None, past_key_values=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, encoder_hidden_states=None, encoder_attention_mask=None, labels=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None, ): transformer_outputs = self.transformer( input_ids, past_key_values=past_key_values, attention_mask=attention_mask, token_type_ids=token_type_ids, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds, encoder_hidden_states=encoder_hidden_states, encoder_attention_mask=encoder_attention_mask, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict, ) hidden_states = transformer_outputs[0] # Apply adapters for i, adapter in enumerate(self.adapters): hidden_states = hidden_states + adapter(hidden_states) lm_logits = self.lm_head(hidden_states) loss = None if labels is not None: # Shift so that tokens < n predict the next token shift_logits = lm_logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = nn.CrossEntropyLoss() loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) if not return_dict: output = (lm_logits,) + transformer_outputs[1:] return ((loss,) + output) if loss is not None else output return modeling_outputs.CausalLMOutputWithCrossAttentions( loss=loss, logits=lm_logits, past_key_values=transformer_outputs.past_key_values, hidden_states=transformer_outputs.hidden_states, attentions=transformer_outputs.attentions, cross_attentions=transformer_outputs.cross_attentions, )
Auf diese Weise haben wir das Adaptermodul an unsere Klasse GPT2LMHeadModelWithAdapters angepasst. Aber bitte beachten Sie noch einmal, dass dies nur ein einfaches Beispiel ist. In realen Anwendungsszenarien sollten Sie die entsprechenden Module sorgfältig nach den Anforderungen der Aufgabe entwerfen.
Adapter-Abstimmung
Wir haben die Adapterklasse und die GPT-2-Modellklasse GPT2LMHeadModelWithAdapters erstellt und mit dem Adaptermodul angepasst. Als Nächstes laden wir das Modell und die Daten, um mit der Feinabstimmung zu beginnen. Einige Codes, die im Originalartikel interpretiert wurden, werden hier nicht im Detail interpretiert. Bitte beachten Sie die vorherigen Artikel.
1. Vorbereitung
Importieren der erforderlichen Bibliotheken, hier gibt es nichts Besonderes zu beachten.
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments, modeling_outputs import torch from torch import nn import torch.nn.functional as F
Wenn eine GPU im System verfügbar ist (überprüft durch torch.cuda.is_available()), wird die GPU verwendet, ansonsten die CPU. Definieren des geladenen Modells und des Namens des Feinabstimmungsmodells.
dvc = 'cuda' if torch.cuda.is_available() else 'cpu' print(dvc) model_name_or_path = 'gpt2' Tuned_model = "gpt2_Adapter-tuning"
2. Laden der Daten und des Tokenizer
Vergessen wir nicht, das von uns erstellte Adaptermodul und die umgeschriebene Klasse GPT2LMHeadModelWithAdapters hier einzufügen. Es können auch in andere Skripte einfügt werden, die dann in das Trainingsskript importiert werden.
Lesen wir die Daten aus der Datei llm_data.csv und erstellen ein DataFrame-Objekt, das zum Testen des Feinabstimmungsmodells verwendet wird.
df = pd.read_csv('llm_data.csv')
Laden des vortrainierten GPT-2 Tokenizer.
tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path)
Erstellen wir ein Trainingsdatensatz-Objekt, geben den verwendeten Tokenizer mit dem Parameter tokenizer an, geben den Pfad der Trainingsdatendatei mit dem Parameter file_path an, und geben die Blockgröße mit 60 mit block_size=60 an. Beachten Sie, dass dieser Wert nicht willkürlich festgelegt werden kann und mit den Daten im Datensatz übereinstimmen muss.
train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60)
Wir kombinieren mehrere Datenproben zu einem Stapel und verarbeiten gleichzeitig die Aufgabe der maskierten Sprachmodellierung (MLM). Verwenden wir den Parameter tokenizer, um den verwendeten Tokenizer anzugeben, und mlm=False, um anzugeben, dass keine maskierte Sprachmodellierung (MLM), sondern kausale Sprachmodellierung (CLM) verwendet wird.
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
3. Laden des Modells und Feinabstimmung des Modells
Wir verwenden zunächst die Klasse TrainingArguments, um das Trainingsparameter-Objekt zu instanziieren.
training_args = TrainingArguments(output_dir=Tuned_model, overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_strategy='no', )
- output_dir=Tuned_model: Gibt das Verzeichnis der Trainingsausgabe als gpt2_Adapter-tuning an.
- overwrite_output_dir=True: Ob überschrieben werden soll, wenn das Ausgabeverzeichnis bereits existiert.
- num_train_epochs=3: Gibt die Anzahl der Trainingsepochen mit 3 an.
- per_device_train_batch_size=32: Gibt die Größe der Trainingsserie für jedes Gerät mit 32 an.
- save_strategy='no': Legt fest, dass Checkpoints nicht gespeichert werden sollen.
Als Nächstes laden und instanziieren wir das vortrainierte GPT-2-Modellobjekt mit dem Adaptermodul:
model = GPT2LMHeadModelWithAdapters.from_pretrained(model_name_or_path) trainer = Trainer(model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset,)
- model=model: Gibt das zu trainierende Modell an.
- args=training_args: Gibt die Trainingsparameter an.
- data_collator=data_collator: Gibt den Datensammler an.
- train_dataset=train_dataset: Gibt den Trainingsdatensatz an.
Wir verwenden die Methode train() des Trainer-Objekts, um den Trainingsprozess zu starten: trainer.train()
trainer.train()
Wir speichern das feinabgestimmte Modell nach dem Training: trainer.save_model(Tuned_model)
trainer.save_model(Tuned_model)
Nach der Feinabstimmung wird das Modell im Ordner gpt2_Adapter-tuning unter der Datei gespeichert, in der sich das Trainingsskript befindet.
4. Testen des Feinabstimmungsmodell
Nach der Feinabstimmung müssen wir das feinabgestimmte Modell laden und eine Inferenz durchführen, um zu prüfen, ob das feinabgestimmte Modell normal funktionieren kann. Natürlich müssen wir beim Laden des fein abgestimmten Modells unsere neu geschriebene Klasse GPT2LMHeadModelWithAdapters verwenden, um es zu laden. Nach dem Laden des Modells müssen wir es auch auf GPU-Beschleunigung einstellen und das Modell in den Inferenzmodus versetzen.
model = GPT2LMHeadModelWithAdapters.from_pretrained(Tuned_model) model.to(dvc) model.eval()
Der nächste Schritt ist das Testen der Inferenz, um zu sehen, ob das Modell richtig funktioniert. Dieser Vorgang ist derselbe wie im vorherigen Artikel. Eine ausführliche Interpretation des Codes finden Sie in dem vorhergehenden Artikel. Dieser Artikel wird nicht darauf eingehen.
prompt = ' '.join(map(str, df.iloc[:, 1:20].values[-1])) generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt').to(dvc), do_sample=True, max_length=200)[0], skip_special_tokens=True) print(generated)
Das Ergebnis sieht wie folgt aus:
Das vollständige Skript für die Feinabstimmung ist lora-tuning.py:
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments,modeling_outputs import torch from torch import nn import torch.nn.functional as F dvc = 'cuda' if torch.cuda.is_available() else 'cpu' print(dvc) model_name_or_path = 'gpt2' Tuned_model="gpt2_Adapter-tuning" # Define the Adapter module class Adapter(nn.Module): def __init__(self, in_features, bottleneck_features=64): super(Adapter, self).__init__() self.down_project = nn.Linear(in_features, bottleneck_features) self.up_project = nn.Linear(bottleneck_features, in_features) self.dropout = nn.Dropout(0.1) self.init_weights() def init_weights(self): nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.down_project.bias, 0) nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.up_project.bias, 0) def forward(self, hidden_states): hidden_states = self.down_project(hidden_states) hidden_states = F.relu(hidden_states) hidden_states = self.dropout(hidden_states) hidden_states = self.up_project(hidden_states) hidden_states = self.dropout(hidden_states) return hidden_states # Integrate the Adapter into the model class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel): def __init__(self, config): super().__init__(config) self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)]) def forward( self, input_ids=None, past_key_values=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, encoder_hidden_states=None, encoder_attention_mask=None, labels=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None, ): transformer_outputs = self.transformer( input_ids, past_key_values=past_key_values, attention_mask=attention_mask, token_type_ids=token_type_ids, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds, encoder_hidden_states=encoder_hidden_states, encoder_attention_mask=encoder_attention_mask, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict, ) hidden_states = transformer_outputs[0] # Apply adapters for i, adapter in enumerate(self.adapters): hidden_states = hidden_states + adapter(hidden_states) lm_logits = self.lm_head(hidden_states) loss = None if labels is not None: # Shift so that tokens < n predict the next token shift_logits = lm_logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = nn.CrossEntropyLoss() loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) if not return_dict: output = (lm_logits,) + transformer_outputs[1:] return ((loss,) + output) if loss is not None else output return modeling_outputs.CausalLMOutputWithCrossAttentions( loss=loss, logits=lm_logits, past_key_values=transformer_outputs.past_key_values, hidden_states=transformer_outputs.hidden_states, attentions=transformer_outputs.attentions, cross_attentions=transformer_outputs.cross_attentions, ) if __name__=="__main__": # Load data df = pd.read_csv('llm_data.csv') tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path) train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60) data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False) training_args = TrainingArguments(output_dir=Tuned_model, overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_strategy= 'no', ) # Initialize model with adapters model = GPT2LMHeadModelWithAdapters.from_pretrained(model_name_or_path) trainer = Trainer(model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset,) trainer.train() trainer.save_model(Tuned_model) # Load the model for inference model = GPT2LMHeadModelWithAdapters.from_pretrained(Tuned_model) model.to(dvc) model.eval() prompt = ' '.join(map(str, df.iloc[:, 1:20].values[-1])) generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt').to(dvc), do_sample=True, max_length=200)[0], skip_special_tokens=True) print(f"test the model:{generated}")
Die Datendateien sind am Ende des Artikels beigefügt. Die ursprüngliche Datendatei ist llm_data.csv, und die vorverarbeitete Datendatei ist train.txt.
Leistungsvergleich verschiedener Feinabstimmungsmethoden
Als Nächstes werden wir die Effizienz und Leistung der verschiedenen Feinabstimmungsmethoden vergleichen. Bisher haben wir nur die Vollparameter-Feinabstimmung und die LoRA-Feinabstimmung eingeführt. Wenn man in diesem Artikel das Adapter-Tuning hinzufügt, sind es insgesamt drei. Als Nächstes werden wir sie nur noch vergleichen.
1. Effizienzvergleich
Trainingsprozess LoRA-Tuning:
- train_runtime: 69.5605s
- VRAM: 4.1G
- generate_runtime: 1.242877s
Trainingsprozess zur Feinabstimmung aller Parameter:
- train_runtime: 101.7946s
- VRAM: 5.67G
- generate_runtime: 0.876525s
Trainingsprozess Adapter-Tuning:
- train_runtime: 104.4355s
- VRAM: 5.52G
- generate_runtime: 0.882792s
Train_runtime(s) | VRAM(GB) | Generate_runtime(s) | |
---|---|---|---|
Volle Parameter-Feinabstimmung | 101.7946 | 5.67 | 0.876525 |
LoRA-tuning | 69.5605 | 4.1 | 1.242877 |
Adapter-tuning | 104.4355 | 5.52 | 0.882792 |
2. Genauigkeitsvergleich
Wie im vorigen Artikel laden wir die ersten 20 Spalten der Schlusskurse in der letzten Zeile der Originaldaten als Eingabe und die restlichen Daten als Ergebnis, um die mit den beiden Trainingsmethoden erhaltenen Modelle zu bewerten. An dieser Stelle sei darauf hingewiesen, dass wir, wie zu Beginn des Artikels erwähnt, eine aggressivere Vorhersagelänge gewählt haben, um den Vergleich der Ergebnisse aussagekräftiger zu machen.
Die ersten 20 Schlusskurse:
- input data:[0.61163 0.61162 0.61191 0.61195 0.61209 0.61231 0.61224 0.61207 0.61187 0.61184 0.6119 0.61169 0.61168 0.61162 0.61181 0.61184 0.61184 0.6118 0.61176]
Die übrigen Schlusskurse:
- true prices:[0.6119, 0.61197, 0.61201, 0.61242, 0.61237, 0.6123, 0.61229, 0.61242, 0.61212, 0.61197, 0.61201, 0.61213, 0.61212, 0.61206, 0.61203, 0.61206, 0.6119, 0.61193, 0.61191, 0.61202, 0.61197, 0.6121, 0.61211, 0.61214, 0.61203, 0.61203, 0.61213, 0.61218, 0.61227, 0.61226]
Als Nächstes laden wir die Modelle getrennt (die Modellparameter der Vollparameter-Feinabstimmung werden im Ordner gpt2_stock im aktuellen Verzeichnis gespeichert, das Modell der LoRA-Feinabstimmung wird im Ordner gpt2_LORA_None im aktuellen Verzeichnis gespeichert und das Modell der Adapterabstimmung wird im Ordner gpt2_Adapter-tuning im aktuellen Verzeichnis gespeichert), führen die Inferenz durch und berechnen ihren MSE, RMSE und NRMSE entsprechend den erhaltenen Ergebnissen. Diese Codes wurden bereits im vorangegangenen Artikel vorgestellt und werden in diesem Artikel nicht näher beschrieben. Das vollständige Testskript ist test.py:
import time import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config from sklearn.metrics import mean_squared_error import torch import numpy as np from peft import PeftModel import matplotlib.pyplot as plt from adapter_tuning import GPT2LMHeadModelWithAdapters df = pd.read_csv('llm_data.csv') dvc='cuda' if torch.cuda.is_available() else 'cpu' base_model='gpt2' fine_tuning_path='./gpt2_stock' lora_tuning_path ='./gpt2_LORA_None' adpter_tuning_path='./gpt2_Adapter-tuning' pre_length=40 tokenizer = GPT2Tokenizer.from_pretrained(base_model) model_fine_tuning = GPT2LMHeadModel.from_pretrained(fine_tuning_path).to(dvc) model_lora_tuning = GPT2LMHeadModel.from_pretrained(base_model) model_lora_tuning=PeftModel.from_pretrained(model_lora_tuning, lora_tuning_path).to(dvc) model_adapter_tuning = GPT2LMHeadModelWithAdapters.from_pretrained(adpter_tuning_path).to(dvc) input_data=df.iloc[:,1:20].values[-1] true_prices= df.iloc[-1:,21:].values.tolist()[0] prompt = ' '.join(map(str, input_data)) def generater(model): global true_prices model.eval() token=tokenizer.encode(prompt, return_tensors='pt').to(dvc) start_=time.time() generated = tokenizer.decode(model.generate(token, do_sample=True, max_length=200)[0], skip_special_tokens=True) end_=time.time() print(f'generate time:{end_-start_}') generated_prices=generated.split('\n')[0] generated_prices=list(map(float,generated_prices.split())) generated_prices=generated_prices[0:pre_length] # def trim_lists(a, b): # min_len = min(len(a), len(b)) # return a[:min_len], b[:min_len] # true_prices,generated_prices=trim_lists(true_prices,generated_prices) print(f"input data:{input_data}") print(f"true prices:{true_prices}") print(f"generated prices:{generated_prices}") mse = mean_squared_error(true_prices[:pre_length], generated_prices) print('MSE:', mse) rmse=np.sqrt(mse) nrmse=rmse/(np.max(true_prices)-np.min(generated_prices)) print(f"RMSE:{rmse},NRMSE:{nrmse}") return generated_prices, mse, rmse, nrmse def plot_(a,b,c,title): plt.figure(figsize=(7, 6)) if title=='predication': plt.plot(true_prices[:pre_length], label='True Values', marker='o') plt.plot(a, label='fine_tuning', marker='x') plt.plot(b, label='lora_tuning', marker='s') plt.plot(c,label='adapter_tuning',marker='d') plt.title(title) plt.xlabel('Index') plt.ylabel('Value') plt.legend() plt.savefig(f"{title}.png") def groups_chart(a,b,c,models): metrics = ['Train_time(s)', 'Infer_time(s)', 'Memory(GB)', 'MSE', 'RMSE', 'NRMSE'] plt.figure(figsize=(7, 6)) a=[101.7946,1.243,5.67,a[1],a[2],a[3]] b=[69.5605,0.877,4.10,b[1],b[2],b[3]] c=[104.4355,0.883,5.52,c[1],c[2],c[3]]# 104.4355s,VRAM:5.52G generate_runtime:0.882792s bar_width = 0.2 r1 = np.arange(len(metrics)) r2 = [x + bar_width for x in r1] r3 = [x + bar_width for x in r2] plt.bar(r1, a, color='r', width=bar_width, edgecolor='grey', label=models[0]) plt.bar(r2, b, color='b', width=bar_width, edgecolor='grey', label=models[1]) plt.bar(r3, c, color='g', width=bar_width, edgecolor='grey', label=models[2]) plt.yscale('log') plt.xlabel('Metrics', fontweight='bold') plt.xticks([r + bar_width for r in range(len(metrics))], metrics) plt.ylabel('Values (log scale)', fontweight='bold') plt.title('Model Comparison') plt.legend() # plt.show() plt.savefig('Comparison.png') fine_tuning_result = generater(model_fine_tuning) lora_tuning_result = generater(model_lora_tuning) adapter_tuning_result=generater(model_adapter_tuning) plot_(fine_tuning_result[0],lora_tuning_result[0],adapter_tuning_result[0],title='predication') groups_chart(fine_tuning_result,lora_tuning_result,adapter_tuning_result,models=['fine-tuning','lora-tuning','adapter-tuning'])
Anmerkung:
Es gibt hier das Problem, dass die Größenordnung der Indikatoren, die wir messen, nicht die gleiche ist. Deshalb habe ich hier eine logarithmische Skala verwendet: plt.yscale('log'), dies kann effektiv mit der Situation umgehen, in der die Datengröße sehr unterschiedlich ist.
Das Ergebnis der Inferenz des Vollparameter-Feinabstimmungsmodells:
- generated prices:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.61165, 0.61169, 0.61186, 0.61171, 0.61171, 0.6116, 0.61165, 0.61168, 0.61165, 0.61169, 0.61173, 0.61184, 0.61176, 0.61171, 0.61176, 0.61171, 0.61207, 0.61208, 0.61202, 0.6117, 0.61207]
- MSE: 1.257374999999991e-07
- RMSE:0.00035459483921794336
- NRMSE:0.43243273075362537
Ergebnisse der LoRA-Feinabstimmungsmodell-Inferenz:
- generated prices:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.61191, 0.61187, 0.6121, 0.61187, 0.61193, 0.61195, 0.61176, 0.61194, 0.61171, 0.61198, 0.61171, 0.61171, 0.61198, 0.61172, 0.61202, 0.6116, 0.61173, 0.61199, 0.61169, 0.61171, 0.61171]
- MSE: 1.0161999999999925e-07
- RMSE:0.0003187789202566557
- NRMSE:0.3887547808008319
Ergebnisse der Adapter-Tuning-Inferenz:
- generated prices:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.61173, 0.61168, 0.61165, 0.61178, 0.61173, 0.61164, 0.61174, 0.61163, 0.61174, 0.61163, 0.61174, 0.61162, 0.61162, 0.61167, 0.61168, 0.61165, 0.61167, 0.61168, 0.61162, 0.61167, 0.61174]
- MSE: 1.5644499999999023e-07
- RMSE:0.00039553128826932293
- NRMSE:0.4944141103367081
Chart-Visualisierung zum Vergleich:
Schlussfolgerung
In diesem Artikel haben wir erörtert, wie die Adapter-Tuning-Methode zur Feinabstimmung des vortrainierten GPT-2-Modells verwendet werden kann, und wir haben einen horizontalen Vergleich der vorgestellten Feinabstimmungsmethoden vorgenommen, der es uns ermöglicht, intuitiv eine Trainingsmethode und ein Modell zu wählen, die für unsere Handelsstrategie besser geeignet sind.
Wir haben festgestellt, dass das Adapter-Tuning zwar etwas längere Trainingszeiten und mehr VRAM als LoRA benötigt, aber einen anderen Ansatz zur Erfassung aufgabenspezifischer Informationen bietet. Die Wahl der besten Methode hängt von den spezifischen Projektanforderungen und den verfügbaren Ressourcen ab. Die Feinabstimmung über alle Parameter ist nach wie vor eine gute Grundlage, während LoRA Effizienz bietet und die Adapterabstimmung Modularität und potenzielle Vorteile für Multitasking-Szenarien bietet.
In den folgenden Artikeln werden wir nicht mehr länger verschiedene Methoden zur Feinabstimmung ausprobieren. Wir werden versuchen, unser fein abgestimmtes Modell zu nutzen, um Handelsstrategien zu formulieren und sie in den EA zu integrieren. Nachdem diese abgeschlossen sind, werden wir einen Backtest durchführen und den EA bewerten. Wenn Sie noch an der Feinabstimmung des Modells interessiert sind und bessere Ergebnisse erzielen wollen, können Sie versuchen, meinen Ideen zu folgen und es Schritt für Schritt anhand des Beispielcodes zu vervollständigen. Glauben Sie mir, das ist kein schwieriger Prozess.
Wir sehen uns im nächsten Artikel!
Anhang:
Dateien | Beschreibung |
---|---|
adapter_tuning.py | Code für Adapter-Tuning |
test.py | Code zum Vergleich der Effizienz und Leistung verschiedener Feinabstimmungsmethoden |
llm_data.csv | Rohdaten-Datei |
train.txt | Datei mit Trainingsdaten |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/13500





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.