LLM mit 2 Milliarden Parametern: Erstellen mit Python

vg

Schnelle Notiz (der Beitrag wurde aus dem englischen übersetzt): Wir werden ein LLM mit 2 Milliarden Parametern von Grund auf mit dem The Pile-Datensatz trainieren. Als Ergebnis erhalten wir ein LLM, das perfekte Grammatik und Zeichensetzung in den Antworten ausgibt, wobei kürzere Kontexte sinnvoll sind, aber nicht die gesamte Antwort.

Zuvor habe ich einen Artikel auf Medium über die Erstellung eines LLM mit 2,3+ Millionen Parametern unter Verwendung des Tiny Shakespeare-Datensatzes geschrieben, aber die Ausgabe ergab keinen Sinn.

# 2.3 Million Parameter LLM Output

ZELBETH:
Sey solmenter! tis tonguerered if
Vurint as steolated have loven OID the queend refore
Are been, good plmp:

Proforne, wiftes swleen, was no blunderesd a a quain beath!
Tybell is my gateer stalk smend as be matious dazest

Ich hatte einen Gedanken: Was wäre, wenn ich die Transformer-Architektur kleiner und weniger komplex und die Trainingsdaten vielfältiger mache? Wie groß ist dann das Modell, das eine einzelne Person mit ihrer fast veralteten GPU in Bezug auf Parameter erstellen könnte, die die richtige Grammatik sprechen und einen Text generieren können, der einen Sinn ergibt?

Dies ist die Ausgabe unseres trainierten Modells, das auf diesen Blog folgt:

13 Million Parameter LLM Output
------------------------------
In 1978, The park was returned to the factory-plate that the public share to the lower of the electronic fence that follow from the Station's cities. The Canal of ancient Western nations were confined to the city spot. The villages were directly linked to cities in China that revolt that the US budget and in Odambinais is uncertain and fortune established in rural areas.
2 Billion Parameter LLM Output
------------------------------
There are two miles east coast from 1037 and 73 million refugees (hypotetus) as the same men and defeated Harvard, and Croft. At right east and West Nile's Mediterranean Sea jets. It was found there a number of parties, blacksmith, musician and boutique hospitality and inspire the strain delivered Canadians have already ruled, rural branches with coalition railholder against Abyssy.

Ich habe festgestellt, dass 13+ Millionen Parameter ausreichen, um in Bezug auf die richtige Grammatik und Zeichensetzung einen Sinn zu ergeben, was ein positiver Punkt ist. Das bedeutet, dass wir einen sehr spezifischen Datensatz verwenden können, um unser zuvor trainiertes Modell für eine eingegrenzte Aufgabe weiter zu optimieren. Es kann sein, dass wir am Ende ein Modell mit weniger als 1 Milliarde oder sogar etwa 500 Millionen Parametern haben, das perfekt für unseren speziellen Anwendungsfall ist, insbesondere für die sichere Ausführung auf privaten Daten.

Ich empfehle, dass Sie zuerst ein Modell mit 13+ Millionen Parametern mit dem Skript trainieren, das in meinem GitHub-Repository verfügbar ist. Sie erhalten Ergebnisse innerhalb eines Tages, anstatt länger zu warten oder wenn Ihre lokale GPU möglicherweise nicht stark genug ist, um ein Modell mit einer Milliarde Parametern zu trainieren.

Die Codebasis ist wie folgt organisiert:

train-llm-from-scratch/
├── src/
│ ├── models/
│ │ ├── mlp.py # Definition des Multi-Layer Perceptron (MLP)-Moduls
│ │ ├── attention.py # Definitionen für Aufmerksamkeitsmechanismen (Single-Head, Multi-Head)
│ │ ├── transformer_block.py # Definition eines einzelnen Transformer-Blocks
│ │ ├── transformer.py # Definition des Haupt-Transformer-Modells
├── config/
│ └── config.py # Enthält Standardkonfigurationen (Modellparameter, Dateipfade, etc.)
├── data_loader/
│ └── data_loader.py # Enthält Funktionen zum Erstellen von Datenladern/-iteratoren
├── Skripte/
│ ├── train_transformer.py # Skript zum Trainieren des Transformer-Modells
│ ├── data_download.py # Skript zum Herunterladen des Datensatzes
│ ├── data_preprocess.py # Skript zum Vorverarbeiten der heruntergeladenen Daten
│ ├── generate_text.py # Skript zum Generieren von Text mit einem trainierten Modell
├── data/ # Verzeichnis zum Speichern des Datensatzes
│ ├── train/ # Enthält Trainingsdaten
│ └── val/ # Enthält Validierungsdaten
├── models/ # Verzeichnis, in dem trainierte Modelle gespeichert werden
  • Das Verzeichnis scripts enthält Skripts für Aufgaben wie das Herunterladen von Datasets, die Datenvorverarbeitung, das Modelltraining und die Textgenerierung mit dem trainierten Modell.
  • Das Verzeichnis src/models/ enthält die Implementierung von Schlüsselkomponenten, einschließlich des Transformatormodells, des Multi-Layer-Perzeptrons (MLP), der Aufmerksamkeitsmechanismen und der Transformatorblöcke.
  • Das Verzeichnis config/ enthält Konfigurationsdateien, die die Standardparameter für das Projekt angeben.
  • Das Verzeichnis data_loader/ stellt Funktionen zum Erstellen von Datenladern und Iteratoren bereit.

Inhaltsverzeichnis

  1. Voraussetzungen und Einarbeitungszeit
  2. Modul installieren
  3. Importieren von Bibliotheken
  4. Vorbereiten der Trainingsdaten
  5. Transformator Übersicht
  6. Mehrschichtiges Perzeptron (MLP)
  7. Einzelner Kopf Aufmerksamkeit
  8. Multi-Head-Aufmerksamkeit
  9. Transformator-Block
  10. Das endgültige Modell
  11. Stapelverarbeitung
  12. Trainingsparameter
  13. Trainieren des Modells
  14. Speichern des trainierten Modells
  15. Verlust der Ausbildung
  16. Generieren von Text
  17. Was kommt als nächstes

Voraussetzungen und Einarbeitungszeit

Stellen Sie sicher, dass Sie über ein grundlegendes Verständnis von objektorientierter Programmierung (OOP) und neuronalen Netzen (NN) verfügen. Die Vertrautheit mit PyTorch ist auch beim Programmieren hilfreich.

Sie benötigen eine GPU, um Ihr Modell zu trainieren. Colab oder Kaggle T4 funktionieren für das Training eines Modells mit 13+ Millionen Parametern, versagen jedoch für das Training mit Milliarden Parametern. Werfen Sie einen Blick auf den Vergleich:

LLM mit 2 Milliarden

Modul installieren

Stellen Sie sicher, dass Git in Ihrer Umgebung installiert ist. Zuerst müssen Sie das Repository klonen:

git clone https://github.com/FareedKhan-dev/train-llm-from-scratch.git
cd train-llm-from-scratch

Anschließend können Sie die erforderlichen Abhängigkeiten installieren:

pip install -r requirements.txt

Importieren von Bibliotheken

Importieren wir die erforderlichen Bibliotheken, die in diesem Blog verwendet werden:

# PyTorch für Deep Learning Funktionen und Tensoren
import torch
import torch.nn as nn
import torch.nn.functional as F

# Numerische Operationen und Arrays Umgang mit
import numpy als np

# Umgang mit HDF5-Dateien
import h5py

# Betriebssystem- und Dateiverwaltung
import os

# Parsing
von Befehlszeilenargumenten import argparse

# HTTP-Anfragen
und -Interaktionen
import requests

# Fortschrittsbalken für Schleifenaus tqdm import tqdm

# JSON-Behandlung
import json

# Zstandard Komprimierungsbibliothek
import zstandard als zstd

# Tokenisierungsbibliothek für große Sprachmodelle
import tiktoken

# Mathematische Operationen (wird für erweiterte mathematische Funktionen verwendet)
import mathematik

Vorbereiten der Trainingsdaten

Unser Trainingsdatensatz muss vielfältig sein und Informationen aus verschiedenen Bereichen enthalten, und The Pile ist die richtige Wahl dafür. Obwohl es 825 GB groß ist, werden wir uns nur an einen kleinen Teil davon halten, d. h. 5 % bis 10 %. Laden wir zunächst das Dataset herunter und sehen wir, wie es funktioniert. Ich werde die auf HuggingFace verfügbare Version herunterladen.

# Download validation dataset
!wget https://huggingface.co/datasets/monology/pile-uncopyrighted/resolve/main/val.jsonl.zst

# Download the first part of the training dataset
!wget https://huggingface.co/datasets/monology/pile-uncopyrighted/resolve/main/train/00.jsonl.zst

# Download the second part of the training dataset
!wget https://huggingface.co/datasets/monology/pile-uncopyrighted/resolve/main/train/01.jsonl.zst

# Download the third part of the training dataset
!wget https://huggingface.co/datasets/monology/pile-uncopyrighted/resolve/main/train/02.jsonl.zst

Das Herunterladen wird einige Zeit in Anspruch nehmen, aber Sie können das Trainingsdataset auch auf nur eine Datei beschränken, anstatt auf drei. Es ist bereits in train/val/test aufgeteilt. Sobald dies erledigt ist, stellen Sie sicher, dass Sie die Dateien korrekt in den jeweiligen Verzeichnissen ablegen.

import os
import shutil
import glob

# Verzeichnisstruktur
definieren train_dir = "data/train"
val_dir = "data/val"

# Verzeichnisse erstellen, wenn sie nicht existieren
os.makedirs(train_dir, exist_ok=True)
os.makedirs(val_dir, exist_ok=True)

# Verschieben Sie alle Zugdateien (z.B. 00.jsonl.zst, 01.jsonl.zst, ...)
train_files = glob.glob("*.jsonl.zst")
für Datei in train_files:
if file.startswith("val"):
# Validierungsdatei
verschieben dest = os.path.join(val_dir, Datei)
else:
# Trainingsdatei
verschieben dest = os.path.join(train_dir, Datei)
shutil.move(Datei, dest)

Unser Datensatz liegt in dem Format vor, einem komprimierten Dateiformat, das häufig zum Speichern großer Datensätze verwendet wird. Es kombiniert JSON-Zeilen (.jsonl), bei denen jede Zeile ein gültiges JSON-Objekt darstellt, mit Zstandard-Komprimierung (.zst). Lassen Sie uns ein Beispiel einer der heruntergeladenen Dateien lesen und sehen, wie sie aussieht.

in_file = "data/val/val.jsonl.zst" # Pfad zu unserer Validierungsdatei

mit zstd. open(in_file, 'r') wie in_f:
für i, Zeile in tqdm(enumerate(in_f)): # Die ersten 5 Zeilen
lesen data = json.loads(line)
print(f"Zeile {i}: {data}") # Die Rohdaten zur Inspektion
ausgeben if i == 2:
break


#### AUSGABE ####Zeile
: 0
{
"text": "Einfluss auf die Schlafqualität ... Epilepsie.",
"meta": {
"pile_set_name": "PubMed Abstracts"
}
}

Zeile: 1
{
"text": "LLMops ein neues GitHub-Repository ...",
"meta": {
"pile_set_name": "Github"
}
}

Jetzt müssen wir unseren Datensatz kodieren (tokenisieren). Unser Ziel ist es, ein LLM zu haben, das zumindest richtige Wörter ausgeben kann. Dazu müssen wir einen bereits verfügbaren Tokenizer verwenden. Wir werden den Open-Source-Tokenizer tiktoken von OpenAI verwenden. Das ist der Tokenizer, der für das ChatGPT-Modell (GPT-3) verwendet wird, um unseren Datensatz zu tokenisieren.

Wir müssen dafür eine Funktion erstellen, um Duplikate zu vermeiden, da wir sowohl die Trainings- als auch die Validierungsdatensätze tokenisieren werden.

def process_files(input_dir, output_file):
"""
Verarbeitet alle .zst-Dateien im angegebenen Eingabeverzeichnis und speichert codierte Token in einer HDF5-Datei.

Args:
input_dir (str): Verzeichnis mit .zst-Eingabedateien.
output_file (str): Pfad zur HDF5-Ausgabedatei.
"""
mit h5py. Datei(output_file, 'w') wie out_f:
# Erstellen Sie einen erweiterbaren Datensatz mit dem Namen 'tokens' in der HDF5-Datei
Datensatz = out_f.create_dataset('tokens', (0,), maxshape=(None,), dtype='i')
start_index = 0

# Iterieren Sie durch alle .zst-Dateien im Eingabeverzeichnis
für den Dateinamen in sortiert(os.listdir(input_dir)):
if filename.endswith(".jsonl.zst"):
in_file = os.path.join(input_dir, Dateiname)
print(f"Verarbeitung: {in_file}")

# Öffnet die .zst-Datei zum Lesen
mit zstd. open(in_file, 'r') as in_f:
# Iterieren Sie durch jede Zeile in der komprimierten Datei
für line in tqdm(in_f, desc=f"Processing {filename}"):
# Laden Sie die Zeile als JSON
data = json.loads(line)

# Hängen Sie das Textende-Token an den Text an und kodieren Sie es
text = data['text'] + "<|endoftext|>"
encoded = enc.encode(text, allowed_special={'<|endoftext|>'})
encoded_len = len(encoded)

# Berechnen Sie den Endindex für die neuen Token
end_index = start_index + encoded_len

# Erweitern Sie die Datensatzgröße und speichern Sie die codierten Token
dataset.resize(dataset.shape[0] + encoded_len, axis=0)
dataset[start_index:end_index] = encoded

# Aktualisieren Sie den Startindex für den nächsten Stapel von Token
start_index = end_index

Bei dieser Funktion gibt es zwei wichtige Punkte:

  1. Wir speichern die tokenisierten Daten in einer HDF5-Datei, was uns Flexibilität für einen schnelleren Datenzugriff beim Trainieren des Modells ermöglicht.
  2. Das Anfügen des Tokens <|endoftext|> markiert das Ende jeder Textsequenz und signalisiert dem Modell, dass es das Ende eines sinnvollen Kontexts erreicht hat, was zum Generieren kohärenter Ausgaben beiträgt.

Jetzt können wir unsere Trainings- und Validierungsdatensätze einfach wie folgt codieren:

# Tokenisierte Datenausgabeverzeichnisse definieren
out_train_file = "data/train/pile_train.h5"
out_val_file = "data/val/pile_dev.h5"

# Laden des Tokenizers von (GPT-3/GPT-2 Model)
enc = tiktoken.get_encoding('r50k_base')

# Trainingsdaten
verarbeiten process_files(train_dir, out_train_file)

# Prozess Validierungsdaten
process_files(val_dir, out_val_file)

Werfen wir einen Blick auf die Stichprobe unserer tokenisierten Daten:

Mit H5py. Datei(out_val_file, 'r') als Datei:
# Zugriff auf den 'tokens'-Datensatz
tokens_dataset = file['tokens']

# Drucken des dtype des Datensatzes
print(f"Dtype des 'tokens' Datensatzes: {tokens_dataset.dtype}")

# Laden und Drucken der ersten Elemente des Datensatzes
print("Erste Elemente des 'tokens'-Datensatzes:")
print(tokens_dataset[:10]) # Erste 10 token


#### AUSGABE ####
Dtype des 'tokens'-Datensatzes: int32

Die ersten paar Elemente des 'tokens'-Datensatzes:
[ 2725 6557 83 23105 157 119 Tel.: 229 77 5846 2429]

Wir haben unseren Datensatz für das Training vorbereitet. Nun werden wir die Transformer-Architektur codieren und uns entsprechend ihrer Theorie ansehen.

Transformator Übersicht

Werfen wir einen kurzen Blick darauf, wie eine Transformer-Architektur verwendet wird, um Text zu verarbeiten und zu verstehen. Es funktioniert, indem es den Text in kleinere Teile, sogenannte Token, aufteilt und das nächste Token in der Sequenz vorhersagt. Ein Transformator besteht aus vielen Schichten, sogenannten Transformatorblöcken, die übereinander gestapelt sind, mit einer letzten Schicht am Ende, um die Vorhersage zu treffen.

Jeder Transformatorblock besteht aus zwei Hauptkomponenten:

  • Self-Attention Heads: Diese finden heraus, auf welche Teile des Inputs das Modell am wichtigsten ist. Bei der Verarbeitung eines Satzes können die Aufmerksamkeitsköpfe beispielsweise Beziehungen zwischen Wörtern hervorheben, z. B. wie sich ein Pronomen auf das Substantiv bezieht, auf das es sich bezieht.
  • MLP (Multi-Layer Perceptron): Dies ist ein einfaches neuronales Feed-Forward-Netzwerk. Er nimmt die Informationen, die von den Aufmerksamkeitsköpfen hervorgehoben werden, und verarbeitet sie weiter. Das MLP verfügt über eine Eingabeschicht, die Daten von den Aufmerksamkeitsköpfen empfängt, eine verborgene Schicht, die die Verarbeitung komplexer macht, und eine Ausgabeschicht, die die Ergebnisse an den nächsten Transformatorblock weiterleitet.

Zusammen fungieren die Aufmerksamkeitsköpfe als der „Worüber man denken soll“-Teil, während der MLP der „Wie man darüber denkt“-Teil ist. Das Stapeln vieler Transformatorblöcke ermöglicht es dem Modell, komplexe Muster und Beziehungen im Text zu verstehen, aber dies ist nicht immer gewährleistet.

Anstatt uns das ursprüngliche Papierdiagramm anzusehen, visualisieren wir ein einfacheres und einfacheres Architekturdiagramm, das wir codieren werden.

Transformer-Architektur
Transformer-Architektur

Schauen wir uns den Ablauf unserer Architektur an, die wir programmieren werden:

  1. Eingabe-Token werden in Einbettungen umgewandelt und mit Positionsinformationen kombiniert.
  2. Das Modell verfügt über 64 identische Transformatorblöcke, die Daten sequenziell verarbeiten.
  3. Jeder Block führt zunächst eine Multi-Head-Aufmerksamkeit aus, um die Beziehungen zwischen den Token zu untersuchen.
  4. Jeder Block verarbeitet dann Daten über ein MLP, das die Daten erweitert und dann komprimiert.
  5. In jedem Schritt werden Restverbindungen (Verknüpfungen) verwendet, um den Informationsfluss zu erleichtern.
  6. Die Layer-Normalisierung wird durchgehend verwendet, um das Training zu stabilisieren.
  7. Der Aufmerksamkeitsmechanismus berechnet, welche Token aufeinander achten sollen.
  8. Der MLP erweitert die Daten auf die 4-fache Größe, wendet ReLU an und komprimiert sie dann wieder nach unten.
  9. Das Modell verwendet 16 Aufmerksamkeitsköpfe, um verschiedene Arten von Beziehungen zu erfassen.
  10. Die letzte Schicht wandelt die verarbeiteten Daten in Vorhersagen in Vokabulargröße um.
  11. Das Modell generiert Text, indem es wiederholt das nächstwahrscheinlichste Token vorhersagt.

Mehrschichtiges Perzeptron (MLP)

MLP ist ein grundlegender Baustein innerhalb des Feed-Forward-Netzwerks des Transformators. Seine Aufgabe ist es, Nichtlinearität einzuführen und komplexe Beziehungen innerhalb der eingebetteten Darstellungen zu lernen. Bei der Definition eines MLP-Moduls ist ein wichtiger Parameter n_embed, der die Dimensionalität der Eingangseinbettung definiert.

Das MLP besteht typischerweise aus einer versteckten linearen Schicht, die die Eingabedimension um einen Faktor erweitert (oft 4, den wir verwenden werden), gefolgt von einer nichtlinearen Aktivierungsfunktion, üblicherweise ReLU. Diese Struktur ermöglicht es unserem Netzwerk, komplexere Funktionen zu erlernen. Schließlich ordnet ein linearer Projektionslayer die erweiterte Darstellung wieder der ursprünglichen Einbettungsdimension zu. Diese Abfolge von Transformationen ermöglicht es dem MLP, die durch den Aufmerksamkeitsmechanismus erlernten Repräsentationen zu verfeinern.

MLP
MLP
# --- MLP (Multi-Layer Perceptron) Klasse ---

Klasse MLP(nn. Modul):
"""
Ein einfaches Multi-Layer-Perzeptron mit einer versteckten Schicht.

Dieses Modul wird innerhalb des Transformer-Blocks für die Feed-Forward-Verarbeitung verwendet.
Es erweitert die Einbettungsgröße der Eingabe, wendet eine ReLU-Aktivierung an und projiziert sie dann wieder
auf die ursprüngliche Einbettungsgröße.
"""
def __init__(selbst, n_embed):
super().__init__()
selbst.versteckt = nn. Linear(n_embed, 4 * n_embed) # Lineare Schicht zur Erweiterung der Einbettungsgröße
self.relu = nn. ReLU() # Aktivierungsfunktion
self.proj = nn. Linear(4 * n_embed, n_embed) # Lineare Schicht, die zurück in die Originalgröße

projiziert werden soll def forward(self, x):
"""
Vorwärts durch den MLP.

Argumente:
x (Taschenlampe. Tensor): Eingabetensor der Form (B, T, C), wobei B die Batchgröße,
T die Sequenzlänge und C die Einbettungsgröße ist.

Rückgabe:
Taschenlampe. Tensor: Ausgangstensor mit der gleichen Form wie die Eingabe.
"""
x = self.forward_embedding(x)
x = self.project_embedding(x)
return x

def forward_embedding(self, x):
"""
Wendet die versteckte lineare Schicht an, gefolgt von der ReLU-Aktivierung.

Argumente:
x (Taschenlampe. Tensor): Eingangstensor.

Rückgabe:
Taschenlampe. Tensor: Ausgabe nach der versteckten Schicht und ReLU.
"""
x = self.relu(self.hidden(x))
return x

def project_embedding(self, x):
"""
Wendet die lineare Projektionsebene an.

Argumente:
x (Taschenlampe. Tensor): Eingangstensor.

Rückgabe:
Taschenlampe. Tensor: Ausgabe nach der Projektionsebene.
"""
x = self.proj(x)
return x

Wir haben gerade unseren MLP-Teil codiert, in dem die Methode eine versteckte lineare Schicht initialisiert, die die Eingabeeinbettungsgröße () erweitert, und eine Projektionsschicht, die sie zurück reduziert. Die ReLU-Aktivierung wird nach der verborgenen Schicht angewendet. Die Methode definiert den Datenfluss durch diese Schichten.

Einzelner Kopf Aufmerksamkeit

Der Aufmerksamkeitskopf ist der Kern unseres Modells. Sein Zweck ist es, sich auf relevante Teile der Eingabesequenz zu konzentrieren. Bei der Definition eines Head-Moduls sind einige wichtige Parameter , und . Der Parameter bestimmt die Dimensionalität der Schlüssel-, Abfrage- und Wertprojektionen und beeinflusst die Darstellungskapazität des Aufmerksamkeitsmechanismus.

Die Eingabeeinbettungsdimension definiert die Größe der Eingabe in diese Projektionsschichten. wird verwendet, um eine Kausalmaske zu erstellen, die sicherstellt, dass das Modell nur die vorhergehenden Token berücksichtigt.

Innerhalb des Kopfes werden lineare Schichten für Schlüssel, Abfrage und Wert ohne Verzerrung initialisiert. Eine niedrigere dreieckige Matrix der Größe wird als Puffer registriert, um eine kausale Maskierung zu implementieren und zu verhindern, dass sich der Aufmerksamkeitsmechanismus um zukünftige Token kümmert.

Einzelkopf-Aufmerksamkeit
Einzelkopf-Aufmerksamkeit
# --- Achtung Kopf Klasse ---

Klasse Leiter(nn) Modul):
"""
Ein einzelner Aufmerksamkeitskopf.

Dieses Modul berechnet Aufmerksamkeitswerte und wendet sie auf die Werte an.
Es umfasst Schlüssel-, Abfrage- und Wertprojektionen und verwendet kausale Maskierung
, um zu verhindern, dass zukünftige Token berücksichtigt werden.
"""
def __init__(self, head_size, n_embed, context_length):
super().__init__()
self.key = nn. Linear(n_embed, head_size, bias=False) # Schlüsselprojektion
self.query = nn. Linear(n_embed, head_size, bias=False) # Abfrageprojektion
self.value = nn. Linear(n_embed, head_size, bias=False) # Wertprojektion
# Untere Dreiecksmatrix für kausale Maskierung
self.register_buffer('tril', torch.tril(torch.ones(context_length, context_length)))

def forward(self, x):
"""
Vorwärts durch den Aufmerksamkeitskopf.

Argumente:
x (Taschenlampe. Tensor): Eingangstensor der Form (B, T, C).

Rückgabe:
Taschenlampe. Tensor: Gibt den Tensor aus, nachdem die Aufmerksamkeit angewendet wurde.
"""
B, T, C = x.shape
k = self.key(x) # (B, T, head_size)
q = self.query(x) # (B, T, head_size)
scale_factor = 1 / math.sqrt(C)
# Aufmerksamkeitsgewichte berechnen: (B, T, head_size) @ (B, head_size, T) -> (B, T, T)
attn_weights = q @ k.transpose(-2, -1) * scale_factor
# Kausalmaskierung

attn_weights = attn_weights.masked_fill(self.tril[:T, :T] == 0, float('-inf'))
attn_weights = F.softmax(attn_weights, dim=-1)
v = self.value(x) # (B, T, head_size)
# Wenden Sie Aufmerksamkeitsgewichte auf Werte
an out = attn_weights @ v # (B, T, T) @ (B, T, head_size) -> (B, T, head_size)
return out

Unsere attention head-Klassenmethode initialisiert lineare Schichten für Schlüssel-, Abfrage- und Wertprojektionen. Eine untere dreieckige Matrix auf Basis wird für die kausale Maskierung verwendet. Die Methode berechnet Aufmerksamkeitsgewichtungen, indem sie das Punktprodukt der Abfrage und des Schlüssels skaliert, die Kausalmaske anwendet, die Gewichtungen mithilfe von Softmax normalisiert und die gewichtete Summe der Werte berechnet, um die Aufmerksamkeitsausgabe zu erzeugen.

Multi-Head-Attention

Um verschiedene Beziehungen innerhalb der Eingabesequenz zu erfassen, werden wir das Konzept der Mehrkopfaufmerksamkeit verwenden. Das Modul MultiHeadAttention verwaltet mehrere unabhängige Aufmerksamkeitsköpfe, die parallel arbeiten.

Der Schlüsselparameter ist hier n_head, der die Anzahl der parallelen Aufmerksamkeitsköpfe bestimmt. Die Eingabeeinbettungsdimension (n_embedcontext_length) und sind auch notwendig, um die einzelnen Aufmerksamkeitsköpfe zu instanziieren. Jeder Kopf verarbeitet die Eingabe unabhängig voneinander und projiziert sie in einen unterdimensionalen Unterraum der Größe . Durch die Verwendung mehrerer Köpfe kann das Modell verschiedene Aspekte der Eingabe gleichzeitig bearbeiten.

Multi Head Attention
Multi Head Attention
# --- Multi-Head Attention Class ---

Klasse MultiHeadAttention(nn. Modul):
"""
Multi-Head Attention Modul.

Dieses Modul kombiniert mehrere Aufmerksamkeitsköpfe parallel. Die Ausgänge der einzelnen Köpfe
werden verkettet, um die endgültige Ausgabe zu bilden.
"""
def __init__(selbst, n_head, n_embed, context_length):
super().__init__()
selbst.köpfe = nn. ModuleList([Head(n_embed // n_head, n_embed, context_length) for _ in range(n_head)])

def forward(self, x):
"""
Vorwärts durch die Aufmerksamkeit mehrerer Köpfe.

Argumente:
x (Taschenlampe. Tensor): Eingangstensor der Form (B, T, C).

Rückgabe:
Taschenlampe. Tensor: Ausgangstensor nach der Verkettung der Ausgänge aller Köpfe.
"""
# Verketten Sie die Ausgabe jedes Kopfes entlang der letzten Dimension (C)
x = torch.cat([h(x) für h in self.heads], dim=-1)
return x

Nachdem wir nun die Klasse definiert haben, die mehrere Aufmerksamkeitsköpfe kombiniert, initialisiert die Methode eine Liste von Instanzen. Die Methode wendet jeden Aufmerksamkeitskopf auf die Eingabe an und verkettet ihre Ausgaben entlang der letzten Dimension, wobei die von den einzelnen Köpfen gelernten Informationen zusammengeführt werden.

Transformator-Block

Um ein Modell mit einer Milliarde Parametern zu erstellen, brauchen wir definitiv eine tiefgreifende Architektur. Dazu müssen wir einen Transformatorblock codieren und ihn stapeln. Die Schlüsselparameter eines Blocks sind , und . Jeder Block besteht aus einer Mehrkopf-Aufmerksamkeitsschicht und einem Feed-Forward-Netzwerk (MLP), wobei vor jedem Block eine Schichtnormalisierung und nach jedem Restverbindungen angewendet wird.n_headn_embedcontext_length

Die Layer-Normalisierung, parametrisiert durch die Einbettungsdimension, trägt zur Stabilisierung des Trainings bei. Der Mehrkopf-Aufmerksamkeitsmechanismus nimmt, wie zuvor beschrieben, , und an. Das MLP nutzt auch die Einbettungsdimension . Diese Komponenten arbeiten zusammen, um die Eingabe zu verarbeiten und komplexe Muster zu lernen.

Transformer Block
Transformer Block
# --- Transformer Block Klasse ---

Klasse Block(nn. Modul):
"""
Ein einzelner Transformer-Block.

Dieser Block besteht aus einer mehrköpfigen Aufmerksamkeitsschicht, gefolgt von einer MLP,
mit Schichtnormalisierung und Restverbindungen.
"""
def __init__(selbst, n_head, n_embed, context_length):
super().__init__()
selbst.ln1 = nn. LayerNorm(n_embed)
selbst.attn = MultiKopfAufmerksamkeit(n_head, n_embed, context_length)
selbst.ln2 = nn. LayerNorm(n_embed)
self.mlp = MLP(n_embed)

def forward(self, x):
"""
Vorwärtsgang durch den Transformer-Block.

Argumente:
x (Taschenlampe. Tensor): Eingangstensor.

Rückgabe:
Taschenlampe. Tensor: Gibt den Tensor nach dem Block aus.
"""
# Mehrkopf-Aufmerksamkeit mit Restverbindung
x = x + self.attn(self.ln1(x))
# MLP mit Restverbindung
anwenden x = x + self.mlp(self.ln2(x))
return x

def forward_embedding(self, x):
"""
Vorwärtslauf mit Fokus auf die Einbettungs- und Aufmerksamkeitsteile.

Argumente:
x (Taschenlampe. Tensor): Eingangstensor.

Gibt Folgendes zurück:
Tupel: Ein Tupel, das die Ausgabe nach der MLP-Einbettung und den Rest enthält.
"""
res = x + self.attn(self.ln1(x))
x = self.mlp.forward_embedding(self.ln2(res))
return x, res

Unsere Klasse stellt einen einzelnen Transformatorblock dar. Die Methode implementiert den Vorwärtslauf des Blocks, indem sie die Schichtnormalisierung und die Multi-Head-Aufmerksamkeit mit einer Restverbindung anwendet, gefolgt von einer weiteren Schichtnormalisierung und dem MLP, wiederum mit einer Restverbindung. Die Methode bietet einen alternativen Vorwärtslauf, der sich auf die Aufmerksamkeits- und anfänglichen MLP-Einbettungsphasen konzentriert.

Das endgültige Modell

Bisher haben wir kleine Komponenten des Transformatorenmodells codiert. Als nächstes integrieren wir Token- und Positionseinbettungen mit einer Reihe von Transformatorblöcken, um Sequenz-zu-Sequenz-Aufgaben auszuführen. Um dies zu tun, müssen wir mehrere Schlüsselparameter codieren.

vocab_size Bestimmt die Größe der Token-Einbettungsschicht, wobei jedes Token einem dichten Größenvektor zugeordnet wird. Der Parameter ist wichtig für die Positionseinbettungsschicht, die die Position jedes Tokens in der Eingabesequenz kodiert, auch mit Dimension n_embedcontext_length. Die Anzahl der Aufmerksamkeitsköpfe (n_embedn_head) und die Anzahl der Blöcke (N_BLOCKS) bestimmen die Tiefe und Komplexität des Netzwerks.

Diese Parameter definieren zusammen die Architektur und Kapazität des Transformatormodells, also codieren wir es.

Transformer-Klasse
Transformer-Klasse
# --- Transformer Model Class ---

Klasse Transformer(nn. Modul):
"""
Das Hauptmodell des Transformers.

Diese Klasse kombiniert Token- und Positionseinbettungen mit einer Sequenz von Transformer-Blöcken
und einer abschließenden linearen Schicht für die Sprachmodellierung.
"""
def __init__(selbst, n_head, n_embed, context_length, vocab_size, N_BLOCKS):
super().__init__()
self.context_length =
context_length selbst. N_BLOCKS = N_BLOCKS
self.token_embed = nn. Einbettung(vocab_size, n_embed)
self.position_embed = nn. Einbettung(context_length, n_embed)
self.attn_blocks = nn. ModuleList([Block(n_head, n_embed, context_length) für _ im Bereich(N_BLOCKS)])
self.layer_norm = nn. LayerNorm(n_embed)
self.lm_head = nn. Linear(n_embed, vocab_size)
self.register_buffer('pos_idxs', torch.arange(context_length))

def _pre_attn_pass(self, idx):
"""
Kombiniert Token- und Positionseinbettungen.

Argumente:
idx (Taschenlampe. Tensor): Eingabe-Token-Indizes.

Rückgabe:
Taschenlampe. Tensor: Summe der Token- und Positionseinbettungen.
""
B, T = idx.shape
tok_embedding = self.token_embed(idx)
pos_embedding = self.position_embed(self.pos_idxs[:T])
return tok_embedding + pos_embedding

def forward(self, idx, targets=None):
"""
Vorwärtsgang durch den Transformer.

Argumente:
idx (Taschenlampe. Tensor): Eingabe-Token-Indizes.
Ziele (Fackel. Tensor, optional): Ziel-Token-Indizes für die Verlustberechnung. Der Standardwert ist Keine.

Rückgabe:
tupel: Logits und Verlust (wenn Ziele angegeben werden).
"""
x = self._pre_attn_pass(idx)
für Block in self.attn_blocks:
x = block(x)
x = self.layer_norm(x)
logits = self.lm_head(x)
Verlust = Keine
, wenn Ziele nicht ist Keine:
B, T, C = logits.shape
flat_logits = logits.view(B * T, C)
Ziele = targets.view(B * T). long()
loss = F.cross_entropy(flat_logits, targets)
return logits, loss

def forward_embedding(self, idx):
"""
Vorwärtspass mit Fokus auf die Einbettungs- und Aufmerksamkeitsblöcke.

Argumente:
idx (Taschenlampe. Tensor): Eingabe-Token-Indizes.

Returns:
tuple: Ausgabe nach Aufmerksamkeitsblöcken und dem Rest.
"""
x = self._pre_attn_pass(idx)
residual = x
für Block in self.attn_blocks:
x, residual = block.forward_embedding(x)
return x, residual

def generate(self, idx, max_new_tokens):
"""
Generiert neue Token mit einer gegebenen Startsequenz.

Argumente:
idx (Taschenlampe. Tensor): Anfängliche Sequenz von Token-Indizes.
max_new_tokens (int): Anzahl der zu generierenden Token.

Rückgabe:
Taschenlampe. Tensor: Die erweiterte Sequenz von Token.
"""
für _ im Bereich(max_new_tokens):
idx_cond = idx[:, -self.context_length:]
logits, _ = self(idx_cond)
logits = logits[:, -1, :]
probs = F.softmax(logits, dim=-1)
idx_next = torch.multinomial(probs, num_samples=1)
idx = torch.cat((idx, idx_next), dim=1)
idx zurückgeben

Unsere Klassenmethode initialisiert Token- und Positionseinbettungsschichten (, ), eine Sequenz von Modulen (), eine Normalisierungsschicht der letzten Schicht () und eine lineare Schicht für die Sprachmodellierung ().Transformer__init__token_embedposition_embedBlockattn_blockslayer_normlm_head

Die Methode _pre_attn_passforwardforward_embeddinggenerate kombiniert Token- und Positionseinbettungen. Das Verfahren verarbeitet die Eingabesequenz durch die Einbettungsschichten und die Reihe von Transformatorblöcken, wendet eine Normalisierung der Endschicht an und erzeugt Logits. Es berechnet auch den Verlust, wenn Ziele angegeben werden. Die Methode stellt einen Zwischendurchlauf bis zur Ausgabe der Aufmerksamkeitsblöcke bereit, und die Methode implementiert die Tokengenerierung.

Stapelverarbeitung

Wenn wir ein Deep-Learning-Modell mit Big Data trainieren, verarbeiten wir es aufgrund der GPU-Verfügbarkeit in Batches. Erstellen wir also eine Funktion get_batch_iteratordata_pathbatch_sizecontext_lengthdevice, die in eine HDF5-Datei übergeht, das gewünschte , das für jede Sequenz und das zum Laden der Daten.

Die bestimmt, wie viele Sequenzen während des Trainings parallel verarbeitet werden, während die die Länge der einzelnen Eingabesequenzen angibt. Der zeigt auf den Speicherort der Trainingsdaten.batch_sizecontext_lengthdata_path

# --- Data Loading Utility --- 

def get_batch_iterator(data_path, batch_size, context_length, device="gpu"):
"""
Erstellt einen Iterator zum Generieren von Datenstapeln aus einer HDF5-Datei.

Argumente:
data_path (str): Pfad zur HDF5-Datei mit tokenisierten Daten.
batch_size (int): Anzahl der Sequenzen in jedem Stapel.
context_length (int): Länge jeder Sequenz.
Gerät (str, optional): Gerät, auf das die Daten geladen werden sollen ('CPU' oder 'CUDA'). Der Standardwert ist "cpu".

Yields:
tuple: Ein Tupel, das Eingabesequenzen (xb) und Zielsequenzen (yb) enthält.
"""
# Öffnet die HDF5-Datei im Lesemodus
mit h5py. Datei(data_path, 'r') wie hdf5_file:

# Extrahieren Sie den Datensatz der tokenisierten Sequenzen
Datensatz = hdf5_file['tokens']

# Ermitteln Sie die Gesamtgröße des Datensatzes
dataset_size = dataset.shape[0]

# Berechnen Sie die Anzahl der Beispiele (Sequenzen), die aus den Daten
erstellt werden können n_examples = (dataset_size - 1) // context_length

# Erstelle ein Array von Indizes für Beispiele und mische sie nach Zufälligkeit
example_idxs = np.arange(n_examples)
np.random.shuffle(example_idxs)

# Initialisiere den Epochenzähler und den Beispielzähler
epochs = 0
Zähler = 0

, während True:
# Prüfen, ob der aktuelle Stapel die Anzahl der verfügbaren Beispiele
überschreitet if counter + batch_size > n_examples:
# Mischen Sie die Indizes erneut und setzen Sie den Zähler auf 0
zurück np.random.shuffle(example_idxs)
counter = 0
print(f"Finished epoch {epochs}") # Gibt die Epochennummer aus, wenn eine Epoche beendet ist
epochs += 1 # Inkrementieren des Epochenzählers

# Wählen Sie einen Stapel zufälliger Indizes aus, um Sequenzen
zu generieren random_indices = example_idxs[counter:counter+batch_size] * context_length

# Abrufen von Sequenzen aus dem Datensatz basierend auf den zufälligen Indizes
random_samples = torch.tensor(np.array([dataset[idx:idx+context_length+1] für idx in random_indices]))

# Trennung der Eingabesequenzen (xb) und Zielsequenzen (yb)
xb = random_samples[:, :context_length].to(Gerät) # Eingabesequenz (erste Hälfte der Zufallsstichprobe)
yb = random_samples[:, 1:context_length+1].to(Gerät) # Zielsequenz (zweite Hälfte der Zufallsstichprobe)

# Inkrementieren Sie den Zähler, um zum nächsten Batch-Zähler
zu gelangen += batch_size

# Geben Sie die Eingabe- und Zielsequenzen als Tupel für die aktuelle Batch-Rückgabe
xb, yb zurück

Unsere Funktion kümmert sich um das Laden und Batchen von Trainingsdaten. Es nimmt , , und als Eingabe an. Die Funktion öffnet die HDF5-Datei, mischt die Daten und tritt dann in eine Endlosschleife ein, um Batches zu generieren. In jeder Iteration wird eine zufällige Teilmenge der Daten ausgewählt, um einen Stapel von Eingabesequenzen () und den entsprechenden Zielsequenzen () zu bilden.get_batch_iteratordata_pathbatch_sizecontext_lengthdevicexbyb

Trainingsparameter

Nachdem wir unser Modell codiert haben, müssen wir die Trainingsparameter wie die Anzahl der Köpfe, Blöcke und mehr zusammen mit dem Datenpfad definieren.

# --- Konfiguration ---

# Vokabulargröße und Transformatorkonfiguration
definieren VOCAB_SIZE = 50304 # Anzahl der eindeutigen Token im Vokabular
CONTEXT_LENGTH = 512 # Maximale Sequenzlänge für das Modell
N_EMBED = 2048 # Dimension des Einbettungsraums
N_HEAD = 16 # Anzahl der Aufmerksamkeitsköpfe in jedem Transformatorblock
N_BLOCKS = 64 # Anzahl der Transformatorblöcke im Modell

# Pfade zu Trainings- und Entwicklungsdatensätzen
TRAIN_PATH = "data/train/pile_val.h5" # Dateipfad für den Trainingsdatensatz
DEV_PATH = "data/val/pile_val.h5" # Dateipfad für den Validierungsdatensatz

# Transformer-Trainingsparameter
T_BATCH_SIZE = 32 # Anzahl der Stichproben pro Trainingsbatch
T_CONTEXT_LENGTH = 16 # Kontextlänge für Trainingsbatches
T_TRAIN_STEPS = 200000 # Gesamtzahl der Trainingsschritte
T_EVAL_STEPS = 1000 # Häufigkeit (in Schritten) für die Durchführung der Auswertung
T_EVAL_ITERS = 250 # Anzahl der Iterationen für die Auswertung des Modells
T_LR_DECAY_STEP = 50000 # Schritt, bei dem die Lernrate
zerfallen soll T_LR = 5e-4 # Anfängliche Lernrate für das Training
T_LR_DECAYED = 5e-5 # Lernrate nach dem Zerfall
T_OUT_PATH = "models/transformer_B.pt" # Pfad zum Speichern des trainierten Modells

# Gerätekonfiguration
DEVICE = 'cuda'

# Speichern Sie alle Konfigurationen in einem Wörterbuch, um sie leicht zugänglich zu machen und zu ändern
default_config = {
'vocab_size': VOCAB_SIZE,
'context_length': CONTEXT_LENGTH,
'n_embed': N_EMBED,
'n_head': N_HEAD,
'n_blocks': N_BLOCKS,
'train_path': TRAIN_PATH,
' dev_path': DEV_PATH,
't_batch_size': T_BATCH_SIZE,
't_context_length': T_CONTEXT_LENGTH,
't_train_steps': T_TRAIN_STEPS,
't_eval_steps': T_EVAL_STEPS,
't_eval_iters': T_EVAL_ITERS,
't_lr_decay_step': T_LR_DECAY_STEP,
't_lr': T_LR,
't_lr_decayed': T_LR_ DECAYED,
't_out_path': T_OUT_PATH, 'device':
GERÄT,
}

Für die meisten Parameter habe ich die gebräuchlichsten Werte verwendet und sie auch in einem Wörterbuch gespeichert, um sie leicht zugänglich zu machen. Hier gelten die Parameter für ein Modell mit einer Milliarde Parametern. Wenn Sie ein Modell mit Millionen von Parametern trainieren möchten, können Sie die Hauptparameter reduzieren, zu denen , , und gehören. Sie können jedoch auch das Modellskript mit einer Million Parametern in meinem GitHub-Repository ausführen.CONTEXT_LENGTHN_EMBEDN_HEADN_BLOCKS

Trainieren des Modells

Lassen Sie uns unser Transformatormodell initialisieren und die Gesamtzahl der Parameter überprüfen.

# --- Initialisieren Sie die Modell- und Druckparameter --- 

model = Transformer(
n_head=config['n_head'],
n_embed=config['n_embed'],
context_length=config['context_length'],
vocab_size=config['vocab_size'],
N_BLOCKS=config['n_blocks']
).to(config['device'])


# Gibt die Gesamtzahl der Parameter
aus total_params = sum(p.numel() for p in model.parameters())print
(f"Gesamtzahl der Parameter im Modell: {total_params:,}")


#### AUSGABE ####2.141.346.251

Jetzt, da wir ein 2-Milliarden-Parameter-Modell haben, müssen wir unseren Adam-Optimierer und unsere Loss-Tracking-Funktion definieren, die uns hilft, den Fortschritt unseres Modells während des gesamten Trainings zu verfolgen.

# --- Einrichtung des Optimierers und der Verlustverfolgung --- 

# Richten Sie den AdamW-Optimierer mit der angegebenen Lernrate ein.
optimizer = torch.optim.AdamW(model.parameters(), lr=config['t_lr'])

# Liste zur Verfolgung von Verlustwerten während des Trainings.
losses = []

# Definieren Sie eine Fenstergröße für den Mittelwert der jüngsten Verluste in der Trainingsschleife.
AVG_WINDOW = 64

# Hilfsfunktion zur Schätzung des durchschnittlichen Verlusts für Trainings- und Entwicklungsdaten.
@torch.no_grad()
def estimate_loss(Schritte):
"""
Bewerten Sie das Modell anhand von Trainings- und Entwicklungsdatensätzen und berechnen Sie den durchschnittlichen Verlust.

Args:
steps (int): Anzahl der auszuwertenden Schritte.

Rückgabe:
dict: Wörterbuch mit durchschnittlichen Verlusten für 'train' und 'dev' Splits.
"""
out = {}
Modell. eval() # Setze das Modell in den Auswertungsmodus.

für die Aufteilung in ['train', 'dev']:
# Wählen Sie den entsprechenden Datenpfad für die aktuelle Aufteilung aus.
data_path = config['train_path'] if split == 'train' else config['dev_path']

# Erstellen Sie einen Batch-Iterator für die Auswertung.
batch_iterator_eval = get_batch_iterator(
data_path, config['t_batch_size'], config['t_context_length'], device=config['device']
)

# Initialisieren Sie einen Tensor, um die Verlustwerte für jeden Auswertungsschritt zu verfolgen.
losses_eval = torch.zeros(steps)
für k in range(steps):
try:
# Hol dir einen Stapel und berechne den Verlust.
xb, yb = next(batch_iterator_eval)
_, loss = model(xb, yb)
losses_eval[k] = loss.item()
außer StopIteration:
# Behandeln Sie den Fall, in dem der Dateniterator vorzeitig endet.
print(f"Warnung: Iterator für {split} endete vorzeitig.")
break

# Berechnet den mittleren Verlust für den aktuellen Split.
out[split] = losses_eval[:k + 1].mean()

model.train() # Stellt das Modell in den Trainingsmodus zurück.
Rückkehr nach draußen

Wir werden nun unsere Batch-Verarbeitungsfunktion und Trainingsschleife initialisieren, wodurch unser Training gestartet wird.

# --- Trainingsschleife ---

# Erstellen Sie einen Batch-Iterator für die Trainingsdaten.
batch_iterator = get_batch_iterator(
config['train_path'],
config['t_batch_size'],
config['t_context_length'],
device=config['device']
)

# Erstellen Sie einen Fortschrittsbalken, um den Trainingsfortschritt zu überwachen.
pbar = tqdm(range(config['t_train_steps']))für
Schritt in pbar:
try:
# Holen Sie sich einen Stapel von Eingabe- und Zieldaten.
xb, yb = next(batch_iterator)

# Führt einen Vorwärtsdurchgang durch und berechnet den Verlust.
_, loss = model(xb, yb)

# Zeichnet den Verlust für die Verfolgung auf.
losses.append(loss.item())
pbar.set_description(f"Train Belief: {np.mean(losses[-AVG_WINDOW:]):.4f}")

# Propagiert den Verlust zurück und aktualisiert die Modellparameter.
optimizer.zero_grad(set_to_none=Wahr)
loss.backward()
optimizer.step()

# Bewerten Sie das Modell regelmäßig anhand von Trainings- und Entwicklungsdaten.
if step % config['t_eval_steps'] == 0:
train_loss, dev_loss = estimate_loss(config['t_eval_iters']).values()
print(f"Schritt: {Schritt}, Zugverlust: {train_loss:.4f}, Entwicklungsverlust: {dev_loss:.4f}")

# Verringert die Lernrate im angegebenen Schritt.
if step == config['t_lr_decay_step']:
print('Decaying learning rate')
für g in optimizer.param_groups:
g['lr'] = config['t_lr_decayed']
except StopIteration:
# Behandeln Sie den Fall, in dem der Trainingsdateniterator vorzeitig endet.
print("Der Iterator für Trainingsdaten wurde vorzeitig beendet.")
brechen

Speichern des trainierten Modells

Da unsere Trainingsschleife in der Lage ist, Fehler zu verarbeiten, speichert sie unser teilweise trainiertes Modell, falls die Schleife einen Fehler auslöst, um Verluste zu vermeiden. Sobald das Training abgeschlossen ist, können wir unser trainiertes Modell speichern, um es später für die Inferenz zu verwenden.

# --- Speichern des Modells und der abschließenden Auswertung ---

# Führen Sie eine abschließende Bewertung des Modells anhand von Trainings- und Entwicklungsdatensätzen durch.
train_loss, dev_loss = estimate_loss(200).values()

# Stellen Sie sicher, dass der eindeutige Speicherpfad des Modells vorhanden ist, falls die Datei bereits vorhanden ist.
modified_model_out_path = config['t_out_path']
save_tries = 0
while os.path.exists(modified_model_out_path):
save_tries += 1
model_out_name = os.path.splitext(config['t_out_path'])[0]
modified_model_out_path = model_out_name + f"_{save_tries}" + ".pt"

# Speichern Sie das Zustandswörterbuch, den Optimiererstatus und die Trainingsmetadaten des Modells.
torch.save(
{
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'Verluste': Verluste,
'train_loss': train_loss,
'dev_loss': dev_loss,
'Schritte': len(Verluste),
},
modified_model_out_path
)
print(f"Modell in {modified_model_out_path}")
print(f"Training abgeschlossen. Zugverlust: {train_loss:.4f}, Entwicklerverlust: {dev_loss:.4f}")

Der endgültige Trainingsverlust für das Milliarden-Parameter-Modell beträgt 0,2314, und der Entwicklungsverlust beträgt 0,643.

Verlust des Trainings

Wenn ich den Verlust sowohl des Millionen- als auch des Milliarden-Parameter-Modells grafisch darstelle, sehen sie sehr unterschiedlich aus.

Vergleich von Trainingsverlusten
Vergleich von Trainingsverlusten

Das Milliarden-Parameter-Modell beginnt mit einem viel höheren Verlust und schwankt zu Beginn stark. Es geht zunächst schnell nach unten, wackelt dann aber, bevor es glatter wird. Dies zeigt, dass es dem größeren Modell am Anfang schwerer fällt, den richtigen Weg zum Lernen zu finden. Möglicherweise sind mehr Daten und sorgfältige Einstellungen erforderlich. Wenn die Lernrate gesenkt wird (die rote Linie), sinkt der Verlust stetiger, was zeigt, dass dies bei der Feinabstimmung hilft.

Der Verlust des Millionen-Parameter-Modells lässt sich von Anfang an leichter verringern. Es schwankt nicht so stark wie das größere Modell. Wenn die Lernrate gesenkt wird, ändert sich die Kurve nicht so sehr. Dies liegt wahrscheinlich daran, dass das kleinere Modell einfacher zu trainieren ist und schneller eine gute Lösung findet. Der große Unterschied zeigt, wie viel schwieriger es ist, sehr große Modelle zu trainieren. Sie brauchen andere Methoden und vielleicht mehr Zeit, um gut zu lernen.

Wir haben jetzt unser gespeichertes Modell. Wir können es endlich für Inferenz verwenden und sehen, wie es Text generiert. 😓

Generieren von Text

Erstellen Sie eine Funktion zum Generieren von Text aus unserem gespeicherten Modell, die den gespeicherten Modellpfad und den Encoder als Eingaben verwendet und den generierten Text zurückgibt.

def generate_text(model_path, input_text, max_length=512, device="gpu"):
""
Generiert Text mit einem vortrainierten Modell basierend auf dem angegebenen Eingabetext.

Argumente:
- model_path (str): Pfad zum Modellprüfpunkt.
- device (torch.device): Gerät, auf das das Modell geladen werden soll (z. B. 'cpu' oder 'cuda').
- input_text (str): Der Eingabetext für das Seeding der Generierung.
- max_length (int, optional): Maximale Länge des generierten Textes. Der Standardwert ist 512.

Gibt Folgendes zurück:
- str: Der generierte Text.
"""

# Laden Sie den Modell-Checkpoint
checkpoint = torch.load(model_path)

# Initialisieren Sie das Modell (Sie sollten sicherstellen, dass die Transformer-Klasse an anderer Stelle definiert ist)
model = Transformer().to(device)

# Laden Sie das Zustandsverzeichnis des Modells
model.load_state_dict(checkpoint['model_state_dict'])

# Laden Sie den Tokenizer für das GPT-Modell (wir verwenden 'r50k_base' für GPT-Modelle)
enc = tiktoken.get_encoding('r50k_base')

# Kodieren Sie den Eingabetext zusammen mit dem Textende-Token
input_ids = torch.tensor(
enc.encode(input_text, allowed_special={'<|endoftext|>'}),
dtype=torch.long
)[Keine, :].to(Gerät) # Fügen Sie eine Batch-Dimension hinzu und verschieben Sie sie auf das angegebene Gerät

# Generieren Sie Text mit dem Modell unter Verwendung der codierten Eingabe
mit torch.no_grad():
# Generieren Sie bis zu 'max_length' Texttoken
generated_output = model.generate(input_ids, max_length)

# Dekodieren Sie die generierten Token wieder in Text
generated_text = enc.decode(generated_output[0]].tolist())

return generated_text

Der Transformator, den wir zuvor definiert haben, muss hier aufgerufen werden, um die Architektur zu laden, und dann laden wir das gespeicherte Modell als Zustand in dieser Architektur.

Beobachten wir zunächst, was sowohl das Millionen- als auch das Milliarden-Parameter-Modell generieren, ohne eine Eingabe zu leisten, und sehen wir uns an, was sie zufällig generieren.

# Definieren der Dateipfade für die vortrainierten Modelle
Billion_model_path = 'models/transformer_B.pt' # Pfad zum Milliardenmodell
Million_model_path = 'models/transformer_M.pt' # Pfad zum Millionenmodell

# Verwendung von '<|endoftext|>' als Eingabe für die Modelle (fungiert als Eingabeaufforderung, die es den Modellen ermöglicht, frei Text zu generieren)
input_text = "<|endoftext|>"

# Rufen Sie die Funktion auf, um Text basierend auf dem Eingabetext unter Verwendung des Milliardenmodells
zu generieren B_output = generate_text(Billion_model_path, input_text)

# Rufen Sie die Funktion auf, um Text basierend auf dem Eingabetext mit dem Millionenmodell
zu generieren M_output = generate_text(Million_model_path, input_text)#

 Drucken Sie die von beiden Modellen
 generierte Ausgabe Drucken(B_output) # Ausgabe aus dem Milliarden-Modell
print(M_output) # Ausgabe aus dem Millionen-Modell

13 Million Parameter LLM Output
——————————
In 1978, The park was returned to the factory-plate that the public share to the lower of the electronic fence that follow from the Station’s cities. The Canal of ancient Western nations were confined to the city spot. The villages were directly linked to cities in China that revolt that the US budget and in Odambinais is uncertain and fortune established in rural areas.

2 Billion Parameter LLM Output
——————————
There are two miles east coast from 1037 and 73 million refugees (hypotetus) as the same men and defeated Harvard, and Croft. At right east and West Nile’s Mediterranean Sea jets. It was found there a number of parties, blacksmith, musician and boutique hospitality and inspire the strain delivered Canadians have already ruled, rural branches with coalition railholder against Abyssy.

Beide LLMs sind in der Lage, klare und genaue Wörter zu generieren, wenn der Kontext kurz und einfach ist. In der Ausgabe mit einer Million Parametern macht zum Beispiel der Satz „Die Dörfer waren direkt mit Städten in China verbunden“ Sinn und vermittelt eine klare Idee. Es ist leicht verständlich und verbindet logisch die Dörfer mit den Städten.

Wenn der Kontext jedoch länger und komplexer wird, beginnt die Klarheit zu verblassen. In der Milliarden-Parameter-Ausgabe werden Sätze wie „Es gibt zwei Meilen Ostküste von 1037 und 73 Millionen Flüchtlingen (Hypotetus)“ und „Schmied-, Musiker- und Boutique-Gastfreundschaft und inspirieren die gelieferten Kanadier“ immer schwieriger. Die Ideen scheinen unzusammenhängend zu sein, und die Satzstruktur fließt nicht natürlich. Während die verwendeten Wörter immer noch korrekt sein mögen, wird die Gesamtbedeutung verwirrend und unklar.

Der positive Punkt ist, dass das LLM mit 13+ Millionen Parametern auch damit beginnt, eine Art von sinnvollem Inhalt mit korrekter Wortschreibung zu generieren. Wenn ich zum Beispiel den Betreff-Eingabetext verwende, beginnt er, eine E-Mail für mich zu generieren. Auch wenn breiterer Text offensichtlich keine aussagekräftigen Ergebnisse liefert, werfen Sie einen Blick auf die Ausgabe:

# Eingabetext
input_text "Subject: "

# Aufrufen des Parameters Million Mod
m_output = generate_text(Million_model_path, input_text)

print(m_output) # Ausgabe des Million-Modells

Million Parameter LLM Output

Subject: ClickPaper-summary Study for Interview
Good morning, I hope this message finds you well, as the sun gently peeks through the clouds, ...

Unser Millionen-Parameter-Modell gibt uns die Motivation, dass wir ein sehr schmales, zielorientiertes LLM unter 1B Größe haben können, während unser 1B-trainiertes Modell uns zeigt, dass die Architektur in großer Tiefe und mit angemessener Berücksichtigung codiert werden muss. Andernfalls wird das Training oder die Leistung im Vergleich zum Modell mit mehreren Parametern nicht verbessert. Die Daten werden nur überangepasst, es sei denn, Sie haben eine umfassende Architektur für das Milliardenmodell.

Was kommt als nächstes

Ich empfehle, dass Sie das Modell mit 13+ Millionen Parametern erstellen und dann mit der Skalierung beginnen, indem Sie die nächsten 100 Parameter hinzufügen, um die Fähigkeit zu verbessern, kürzere Kontexte zu verarbeiten. Es liegt an Ihnen, wie viele Parameter Sie noch für bestimmte Aufgaben trainieren möchten. Versuchen Sie dann, das Modell für die verbleibenden Parameter unter 1B auf domänenspezifische Daten abzustimmen, z. B. das Schreiben von E-Mails oder Aufsätzen, und sehen Sie, wie der Text generiert wird.

Weiterer lesenswerter Beitrag: Clean Code: Der Mythos in der Frontend-Entwicklung

com

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)