mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
Translated ['src/AI/AI-llm-architecture/0.-basic-llm-concepts.md', 'src/
This commit is contained in:
parent
d6634fbe4a
commit
64541e064d
@ -2,7 +2,7 @@
|
||||
|
||||
## Pretraining
|
||||
|
||||
Il pretraining è la fase fondamentale nello sviluppo di un modello di linguaggio di grandi dimensioni (LLM) in cui il modello è esposto a enormi e diversificati volumi di dati testuali. Durante questa fase, **l'LLM apprende le strutture fondamentali, i modelli e le sfumature del linguaggio**, inclusi grammatica, vocabolario, sintassi e relazioni contestuali. Elaborando questi dati estesi, il modello acquisisce una comprensione ampia del linguaggio e della conoscenza generale del mondo. Questa base completa consente all'LLM di generare testi coerenti e contestualmente rilevanti. Successivamente, questo modello pre-addestrato può subire un fine-tuning, in cui viene ulteriormente addestrato su dataset specializzati per adattare le sue capacità a compiti o domini specifici, migliorando le sue prestazioni e rilevanza in applicazioni mirate.
|
||||
Il pretraining è la fase fondamentale nello sviluppo di un modello di linguaggio di grandi dimensioni (LLM) in cui il modello è esposto a enormi e diversificati volumi di dati testuali. Durante questa fase, **l'LLM apprende le strutture fondamentali, i modelli e le sfumature del linguaggio**, inclusi grammatica, vocabolario, sintassi e relazioni contestuali. Elaborando questi dati estesi, il modello acquisisce una comprensione ampia del linguaggio e della conoscenza generale del mondo. Questa base completa consente all'LLM di generare testi coerenti e contestualmente rilevanti. Successivamente, questo modello pre-addestrato può subire un fine-tuning, in cui viene ulteriormente addestrato su set di dati specializzati per adattare le sue capacità a compiti o domini specifici, migliorando le sue prestazioni e rilevanza nelle applicazioni mirate.
|
||||
|
||||
## Componenti principali degli LLM
|
||||
|
||||
@ -32,25 +32,25 @@ GPT_CONFIG_124M = {
|
||||
|
||||
In PyTorch, un **tensor** è una struttura dati fondamentale che funge da array multidimensionale, generalizzando concetti come scalari, vettori e matrici a dimensioni potenzialmente superiori. I tensori sono il modo principale in cui i dati sono rappresentati e manipolati in PyTorch, specialmente nel contesto dell'apprendimento profondo e delle reti neurali.
|
||||
|
||||
### Mathematical Concept of Tensors
|
||||
### Concetto Matematico di Tensors
|
||||
|
||||
- **Scalari**: Tensors di rango 0, che rappresentano un singolo numero (zero-dimensionale). Come: 5
|
||||
- **Vettori**: Tensors di rango 1, che rappresentano un array unidimensionale di numeri. Come: \[5,1]
|
||||
- **Matrici**: Tensors di rango 2, che rappresentano array bidimensionali con righe e colonne. Come: \[\[1,3], \[5,2]]
|
||||
- **Tensors di Rango Superiore**: Tensors di rango 3 o superiore, che rappresentano dati in dimensioni superiori (ad esempio, tensori 3D per immagini a colori).
|
||||
|
||||
### Tensors as Data Containers
|
||||
### Tensors come Contenitori di Dati
|
||||
|
||||
Da una prospettiva computazionale, i tensori agiscono come contenitori per dati multidimensionali, dove ogni dimensione può rappresentare diverse caratteristiche o aspetti dei dati. Questo rende i tensori altamente adatti per gestire set di dati complessi in compiti di machine learning.
|
||||
|
||||
### PyTorch Tensors vs. NumPy Arrays
|
||||
### Tensors di PyTorch vs. Array NumPy
|
||||
|
||||
Sebbene i tensori di PyTorch siano simili agli array di NumPy nella loro capacità di memorizzare e manipolare dati numerici, offrono funzionalità aggiuntive cruciali per l'apprendimento profondo:
|
||||
Sebbene i tensori di PyTorch siano simili agli array NumPy nella loro capacità di memorizzare e manipolare dati numerici, offrono funzionalità aggiuntive cruciali per l'apprendimento profondo:
|
||||
|
||||
- **Differenziazione Automatica**: I tensori di PyTorch supportano il calcolo automatico dei gradienti (autograd), il che semplifica il processo di calcolo delle derivate necessarie per l'addestramento delle reti neurali.
|
||||
- **Accelerazione GPU**: I tensori in PyTorch possono essere spostati e calcolati su GPU, accelerando significativamente i calcoli su larga scala.
|
||||
|
||||
### Creating Tensors in PyTorch
|
||||
### Creazione di Tensors in PyTorch
|
||||
|
||||
Puoi creare tensori utilizzando la funzione `torch.tensor`:
|
||||
```python
|
||||
@ -125,7 +125,7 @@ I tensori sono essenziali in PyTorch per costruire e addestrare reti neurali:
|
||||
|
||||
## Differenziazione Automatica
|
||||
|
||||
La differenziazione automatica (AD) è una tecnica computazionale utilizzata per **valutare le derivate (gradienti)** delle funzioni in modo efficiente e accurato. Nel contesto delle reti neurali, l'AD consente il calcolo dei gradienti necessari per **algoritmi di ottimizzazione come il gradiente discendente**. PyTorch fornisce un motore di differenziazione automatica chiamato **autograd** che semplifica questo processo.
|
||||
La differenziazione automatica (AD) è una tecnica computazionale utilizzata per **valutare le derivate (gradienti)** delle funzioni in modo efficiente e accurato. Nel contesto delle reti neurali, l'AD consente il calcolo dei gradienti richiesti per **algoritmi di ottimizzazione come il gradiente discendente**. PyTorch fornisce un motore di differenziazione automatica chiamato **autograd** che semplifica questo processo.
|
||||
|
||||
### Spiegazione Matematica della Differenziazione Automatica
|
||||
|
||||
@ -199,7 +199,7 @@ Gradient w.r.t b: tensor([-0.0817])
|
||||
|
||||
### **1.Extending to Multilayer Networks**
|
||||
|
||||
In reti neurali più grandi con più strati, il processo di calcolo dei gradienti diventa più complesso a causa del numero maggiore di parametri e operazioni. Tuttavia, i principi fondamentali rimangono gli stessi:
|
||||
In reti neurali più grandi con più strati, il processo di calcolo dei gradienti diventa più complesso a causa dell'aumento del numero di parametri e operazioni. Tuttavia, i principi fondamentali rimangono gli stessi:
|
||||
|
||||
- **Forward Pass:** Calcola l'output della rete passando gli input attraverso ciascun strato.
|
||||
- **Compute Loss:** Valuta la funzione di perdita utilizzando l'output della rete e le etichette target.
|
||||
|
233
src/AI/AI-llm-architecture/2.-data-sampling.md
Normal file
233
src/AI/AI-llm-architecture/2.-data-sampling.md
Normal file
@ -0,0 +1,233 @@
|
||||
# 2. Campionamento Dati
|
||||
|
||||
## **Campionamento Dati**
|
||||
|
||||
Il **Campionamento Dati** è un processo cruciale nella preparazione dei dati per l'addestramento di modelli di linguaggio di grandi dimensioni (LLM) come GPT. Comporta l'organizzazione dei dati testuali in sequenze di input e target che il modello utilizza per imparare a prevedere la parola successiva (o token) basandosi sulle parole precedenti. Un corretto campionamento dei dati assicura che il modello catturi efficacemente i modelli linguistici e le dipendenze.
|
||||
|
||||
> [!TIP]
|
||||
> L'obiettivo di questa seconda fase è molto semplice: **Campionare i dati di input e prepararli per la fase di addestramento solitamente separando il dataset in frasi di una lunghezza specifica e generando anche la risposta attesa.**
|
||||
|
||||
### **Perché il Campionamento Dati è Importante**
|
||||
|
||||
I LLM come GPT sono addestrati a generare o prevedere testo comprendendo il contesto fornito dalle parole precedenti. Per raggiungere questo obiettivo, i dati di addestramento devono essere strutturati in modo che il modello possa apprendere la relazione tra sequenze di parole e le loro parole successive. Questo approccio strutturato consente al modello di generalizzare e generare testo coerente e contestualmente rilevante.
|
||||
|
||||
### **Concetti Chiave nel Campionamento Dati**
|
||||
|
||||
1. **Tokenizzazione:** Suddividere il testo in unità più piccole chiamate token (ad es., parole, sottoparole o caratteri).
|
||||
2. **Lunghezza della Sequenza (max_length):** Il numero di token in ciascuna sequenza di input.
|
||||
3. **Finestra Scorrevole:** Un metodo per creare sequenze di input sovrapposte spostando una finestra sul testo tokenizzato.
|
||||
4. **Passo:** Il numero di token che la finestra scorrevole si sposta in avanti per creare la sequenza successiva.
|
||||
|
||||
### **Esempio Passo-Passo**
|
||||
|
||||
Esploriamo un esempio per illustrare il campionamento dei dati.
|
||||
|
||||
**Testo di Esempio**
|
||||
```arduino
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
```
|
||||
**Tokenizzazione**
|
||||
|
||||
Assumiamo di utilizzare un **tokenizer di base** che suddivide il testo in parole e segni di punteggiatura:
|
||||
```vbnet
|
||||
Tokens: ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit."]
|
||||
```
|
||||
**Parametri**
|
||||
|
||||
- **Lunghezza Massima della Sequenza (max_length):** 4 token
|
||||
- **Passo della Finestra Scorrevole:** 1 token
|
||||
|
||||
**Creazione di Sequenze di Input e Target**
|
||||
|
||||
1. **Approccio della Finestra Scorrevole:**
|
||||
- **Sequenze di Input:** Ogni sequenza di input è composta da `max_length` token.
|
||||
- **Sequenze di Target:** Ogni sequenza di target è composta dai token che seguono immediatamente la corrispondente sequenza di input.
|
||||
2. **Generazione delle Sequenze:**
|
||||
|
||||
<table><thead><tr><th width="177">Posizione della Finestra</th><th>Sequenza di Input</th><th>Sequenza di Target</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. **Array di Input e Target Risultanti:**
|
||||
|
||||
- **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."],
|
||||
]
|
||||
```
|
||||
|
||||
**Rappresentazione Visiva**
|
||||
|
||||
<table><thead><tr><th width="222">Posizione del Token</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>
|
||||
|
||||
**Finestra Scorrevole con Passo 1:**
|
||||
|
||||
- **Prima Finestra (Posizioni 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Target:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Seconda Finestra (Posizioni 2-5):** \["ipsum", "dolor", "sit", "amet,"] → **Target:** \["dolor", "sit", "amet,", "consectetur"]
|
||||
- **Terza Finestra (Posizioni 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Target:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Quarta Finestra (Posizioni 4-7):** \["sit", "amet,", "consectetur", "adipiscing"] → **Target:** \["amet,", "consectetur", "adipiscing", "elit."]
|
||||
|
||||
**Comprendere il Passo**
|
||||
|
||||
- **Passo di 1:** La finestra si sposta in avanti di un token ogni volta, risultando in sequenze altamente sovrapposte. Questo può portare a un miglior apprendimento delle relazioni contestuali ma può aumentare il rischio di overfitting poiché punti dati simili vengono ripetuti.
|
||||
- **Passo di 2:** La finestra si sposta in avanti di due token ogni volta, riducendo la sovrapposizione. Questo diminuisce la ridondanza e il carico computazionale ma potrebbe perdere alcune sfumature contestuali.
|
||||
- **Passo Uguale a max_length:** La finestra si sposta in avanti per l'intera dimensione della finestra, risultando in sequenze non sovrapposte. Questo minimizza la ridondanza dei dati ma può limitare la capacità del modello di apprendere dipendenze tra le sequenze.
|
||||
|
||||
**Esempio con Passo di 2:**
|
||||
|
||||
Utilizzando lo stesso testo tokenizzato e `max_length` di 4:
|
||||
|
||||
- **Prima Finestra (Posizioni 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **Target:** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Seconda Finestra (Posizioni 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **Target:** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Terza Finestra (Posizioni 5-8):** \["amet,", "consectetur", "adipiscing", "elit."] → **Target:** \["consectetur", "adipiscing", "elit.", "sed"] _(Assumendo continuazione)_
|
||||
|
||||
## Esempio di Codice
|
||||
|
||||
Cerchiamo di capire meglio questo attraverso un esempio di codice da [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]])
|
||||
]
|
||||
```
|
||||
## Riferimenti
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -2,7 +2,7 @@
|
||||
|
||||
## Token Embeddings
|
||||
|
||||
Dopo la tokenizzazione dei dati testuali, il passo critico successivo nella preparazione dei dati per l'addestramento di modelli di linguaggio di grandi dimensioni (LLM) come GPT è la creazione di **token embeddings**. I token embeddings trasformano token discreti (come parole o sottoparole) in vettori numerici continui che il modello può elaborare e apprendere. Questa spiegazione analizza i token embeddings, la loro inizializzazione, utilizzo e il ruolo degli embeddings posizionali nel migliorare la comprensione del modello delle sequenze di token.
|
||||
Dopo la tokenizzazione dei dati testuali, il passo critico successivo nella preparazione dei dati per l'addestramento di modelli di linguaggio di grandi dimensioni (LLM) come GPT è la creazione di **token embeddings**. I token embeddings trasformano token discreti (come parole o sottoparole) in vettori numerici continui che il modello può elaborare e da cui può apprendere. Questa spiegazione analizza i token embeddings, la loro inizializzazione, utilizzo e il ruolo degli embeddings posizionali nel migliorare la comprensione del modello delle sequenze di token.
|
||||
|
||||
> [!TIP]
|
||||
> L'obiettivo di questa terza fase è molto semplice: **Assegnare a ciascuno dei token precedenti nel vocabolario un vettore delle dimensioni desiderate per addestrare il modello.** Ogni parola nel vocabolario avrà un punto in uno spazio di X dimensioni.\
|
||||
@ -72,7 +72,7 @@ tensor([[-0.4015, 0.9666, -1.1481]], grad_fn=<EmbeddingBackward0>)
|
||||
|
||||
### **Come Funzionano gli Embedding dei Token Durante l'Addestramento**
|
||||
|
||||
Durante l'addestramento, ogni token nei dati di input viene convertito nel suo corrispondente vettore di embedding. Questi vettori vengono poi utilizzati in vari calcoli all'interno del modello, come meccanismi di attenzione e strati di rete neurale.
|
||||
Durante l'addestramento, ogni token nei dati di input viene convertito nel suo corrispondente vettore di embedding. Questi vettori vengono poi utilizzati in vari calcoli all'interno del modello, come meccanismi di attenzione e strati di reti neurali.
|
||||
|
||||
**Scenario Esemplare:**
|
||||
|
||||
@ -118,7 +118,7 @@ cssCopy codeBatch
|
||||
```
|
||||
**Spiegazione:**
|
||||
|
||||
- Ogni token nella sequenza è rappresentato da un vettore di 256 dimensioni.
|
||||
- Ogni token nella sequenza è rappresentato da un vettore a 256 dimensioni.
|
||||
- Il modello elabora questi embedding per apprendere i modelli linguistici e generare previsioni.
|
||||
|
||||
## **Embedding Posizionali: Aggiungere Contesto agli Embedding dei Token**
|
||||
@ -141,7 +141,7 @@ Mentre gli embedding dei token catturano il significato dei singoli token, non c
|
||||
- **Esempio:** Indicano quanto sono distanti due token, indipendentemente dalle loro posizioni assolute nella sequenza.
|
||||
- **Usato Da:** Modelli come Transformer-XL e alcune varianti di BERT.
|
||||
|
||||
### **Come Vengono Integrati gli Embedding Posizionali:**
|
||||
### **Come Sono Integrati gli Embedding Posizionali:**
|
||||
|
||||
- **Stesse Dimensioni:** Gli embedding posizionali hanno la stessa dimensionalità degli embedding dei token.
|
||||
- **Addizione:** Vengono aggiunti agli embedding dei token, combinando l'identità del token con le informazioni posizionali senza aumentare la dimensionalità complessiva.
|
||||
@ -161,7 +161,7 @@ Combined Embedding = Token Embedding + Positional Embedding
|
||||
|
||||
## Esempio di Codice
|
||||
|
||||
Seguendo l'esempio di codice da [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):
|
||||
Seguendo con l'esempio di codice da [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...
|
||||
|
||||
|
@ -26,7 +26,7 @@ L'auto-attenzione, o intra-attention, è un meccanismo in cui l'attenzione viene
|
||||
- **Embeddings**: Rappresentazioni vettoriali dei token, che catturano informazioni semantiche.
|
||||
- **Pesi di Attenzione**: Valori che determinano l'importanza di ciascun token rispetto agli altri.
|
||||
|
||||
### Calcolo dei Pesi di Attenzione: Un Esempio Passo-Passo
|
||||
### Calcolo dei Pesi di Attenzione: Un Esempio Passo dopo Passo
|
||||
|
||||
Consideriamo la frase **"Hello shiny sun!"** e rappresentiamo ciascuna parola con un embedding a 3 dimensioni:
|
||||
|
||||
@ -68,7 +68,7 @@ Applica la **funzione softmax** ai punteggi di attenzione per convertirli in pes
|
||||
|
||||
Calcolando gli esponenziali:
|
||||
|
||||
<figure><img src="../../images/image (4) (1) (1) (1).png" alt="" width="249"><figcaption></figcaption></figure>
|
||||
<figure><img src="../../images/image (4) (1) (1).png" alt="" width="249"><figcaption></figcaption></figure>
|
||||
|
||||
Calcolando la somma:
|
||||
|
||||
@ -81,7 +81,7 @@ Calcolando i pesi di attenzione:
|
||||
#### Passo 3: Calcolare il Vettore di Contesto
|
||||
|
||||
> [!TIP]
|
||||
> Basta prendere ciascun peso di attenzione e moltiplicarlo per le dimensioni del token correlate e poi sommare tutte le dimensioni per ottenere solo 1 vettore (il vettore di contesto)
|
||||
> Basta prendere ciascun peso di attenzione e moltiplicarlo per le dimensioni del token correlato e poi sommare tutte le dimensioni per ottenere solo 1 vettore (il vettore di contesto)
|
||||
|
||||
Il **vettore di contesto** è calcolato come la somma pesata degli embeddings di tutte le parole, utilizzando i pesi di attenzione.
|
||||
|
||||
@ -119,7 +119,7 @@ In pratica, i meccanismi di auto-attenzione utilizzano **pesi addestrabili** per
|
||||
|
||||
<figure><img src="../../images/image (10) (1) (1).png" alt="" width="239"><figcaption></figcaption></figure>
|
||||
|
||||
La query è i dati da utilizzare come prima, mentre le matrici di chiavi e valori sono semplicemente matrici addestrabili casuali.
|
||||
La query è i dati da utilizzare come prima, mentre le matrici di chiavi e valori sono semplicemente matrici casuali addestrabili.
|
||||
|
||||
#### Passo 1: Calcolare Query, Chiavi e Valori
|
||||
|
||||
@ -226,7 +226,7 @@ print(sa_v2(inputs))
|
||||
|
||||
## Causal Attention: Nascondere le Parole Future
|
||||
|
||||
Per gli LLM vogliamo che il modello consideri solo i token che appaiono prima della posizione attuale per **prevedere il prossimo token**. **Causal attention**, nota anche come **masked attention**, raggiunge questo obiettivo modificando il meccanismo di attenzione per impedire l'accesso ai token futuri.
|
||||
Per i LLM vogliamo che il modello consideri solo i token che appaiono prima della posizione attuale per **prevedere il prossimo token**. **Causal attention**, nota anche come **masked attention**, raggiunge questo obiettivo modificando il meccanismo di attenzione per impedire l'accesso ai token futuri.
|
||||
|
||||
### Applicare una Maschera di Causal Attention
|
||||
|
||||
@ -248,7 +248,7 @@ masked_scores = attention_scores + mask
|
||||
attention_weights = torch.softmax(masked_scores, dim=-1)
|
||||
```
|
||||
|
||||
### Mascherare Pesi di Attenzione Aggiuntivi con Dropout
|
||||
### Mascherare Ulteriori Pesi di Attenzione con Dropout
|
||||
|
||||
Per **prevenire l'overfitting**, possiamo applicare **dropout** ai pesi di attenzione dopo l'operazione softmax. Il dropout **azzera casualmente alcuni dei pesi di attenzione** durante l'addestramento.
|
||||
```python
|
||||
@ -327,7 +327,7 @@ print("context_vecs.shape:", context_vecs.shape)
|
||||
|
||||
### Esempio di Codice
|
||||
|
||||
Potrebbe essere possibile riutilizzare il codice precedente e aggiungere semplicemente un wrapper che lo lancia più volte, ma questa è una versione più ottimizzata da [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) che elabora tutte le teste contemporaneamente (riducendo il numero di costose iterazioni for). Come puoi vedere nel codice, le dimensioni di ciascun token sono suddivise in diverse dimensioni in base al numero di teste. In questo modo, se un token ha 8 dimensioni e vogliamo utilizzare 3 teste, le dimensioni saranno suddivise in 2 array di 4 dimensioni e ciascuna testa utilizzerà uno di essi:
|
||||
Potrebbe essere possibile riutilizzare il codice precedente e aggiungere solo un wrapper che lo lancia più volte, ma questa è una versione più ottimizzata da [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) che elabora tutte le teste contemporaneamente (riducendo il numero di costose iterazioni for). Come puoi vedere nel codice, le dimensioni di ciascun token sono suddivise in diverse dimensioni in base al numero di teste. In questo modo, se un token ha 8 dimensioni e vogliamo utilizzare 3 teste, le dimensioni saranno suddivise in 2 array di 4 dimensioni e ciascuna testa utilizzerà uno di essi:
|
||||
```python
|
||||
class MultiHeadAttention(nn.Module):
|
||||
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
|
||||
@ -409,7 +409,7 @@ Per un'altra implementazione compatta ed efficiente, puoi utilizzare la [`torch.
|
||||
> [!TIP]
|
||||
> Risposta breve di ChatGPT su perché è meglio dividere le dimensioni dei token tra le teste invece di far controllare a ciascuna testa tutte le dimensioni di tutti i token:
|
||||
>
|
||||
> Sebbene consentire a ciascuna testa di elaborare tutte le dimensioni di embedding possa sembrare vantaggioso perché ogni testa avrebbe accesso a tutte le informazioni, la pratica standard è **dividere le dimensioni di embedding tra le teste**. Questo approccio bilancia l'efficienza computazionale con le prestazioni del modello e incoraggia ciascuna testa a imparare rappresentazioni diverse. Pertanto, dividere le dimensioni di embedding è generalmente preferito rispetto a far controllare a ciascuna testa tutte le dimensioni.
|
||||
> Sebbene consentire a ciascuna testa di elaborare tutte le dimensioni di embedding possa sembrare vantaggioso perché ogni testa avrebbe accesso a tutte le informazioni, la pratica standard è **dividere le dimensioni di embedding tra le teste**. Questo approccio bilancia l'efficienza computazionale con le prestazioni del modello e incoraggia ciascuna testa ad apprendere rappresentazioni diverse. Pertanto, dividere le dimensioni di embedding è generalmente preferito rispetto a far controllare a ciascuna testa tutte le dimensioni.
|
||||
|
||||
## References
|
||||
|
||||
|
@ -258,7 +258,7 @@ Questo è già stato spiegato in una sezione precedente.
|
||||
|
||||
#### **Scopo e Funzionalità**
|
||||
|
||||
- **Auto-Attenzione Multi-Testa:** Consente al modello di concentrarsi su diverse posizioni all'interno della sequenza di input durante la codifica di un token.
|
||||
- **Auto-Attenzione Multi-Testa:** Permette al modello di concentrarsi su diverse posizioni all'interno della sequenza di input quando codifica un token.
|
||||
- **Componenti Chiave:**
|
||||
- **Query, Chiavi, Valori:** Proiezioni lineari dell'input, utilizzate per calcolare i punteggi di attenzione.
|
||||
- **Teste:** Molteplici meccanismi di attenzione che funzionano in parallelo (`num_heads`), ciascuno con una dimensione ridotta (`head_dim`).
|
||||
@ -266,7 +266,7 @@ Questo è già stato spiegato in una sezione precedente.
|
||||
- **Mascheramento:** Una maschera causale è applicata per impedire al modello di prestare attenzione ai token futuri (importante per modelli autoregressivi come GPT).
|
||||
- **Pesi di Attenzione:** Softmax dei punteggi di attenzione mascherati e scalati.
|
||||
- **Vettore di Contesto:** Somma pesata dei valori, secondo i pesi di attenzione.
|
||||
- **Proiezione di Uscita:** Strato lineare per combinare le uscite di tutte le teste.
|
||||
- **Proiezione di Uscita:** Livello lineare per combinare le uscite di tutte le teste.
|
||||
|
||||
> [!TIP]
|
||||
> L'obiettivo di questa rete è trovare le relazioni tra i token nello stesso contesto. Inoltre, i token sono divisi in diverse teste per prevenire l'overfitting anche se le relazioni finali trovate per testa sono combinate alla fine di questa rete.
|
||||
@ -294,7 +294,7 @@ return self.scale * norm_x + self.shift
|
||||
- **Layer Normalization:** Una tecnica utilizzata per normalizzare gli input attraverso le caratteristiche (dimensioni di embedding) per ciascun esempio individuale in un batch.
|
||||
- **Componenti:**
|
||||
- **`eps`:** Una piccola costante (`1e-5`) aggiunta alla varianza per prevenire la divisione per zero durante la normalizzazione.
|
||||
- **`scale` e `shift`:** Parametri apprendibili (`nn.Parameter`) che consentono al modello di scalare e spostare l'output normalizzato. Sono inizializzati rispettivamente a uno e zero.
|
||||
- **`scale` e `shift`:** Parametri apprendibili (`nn.Parameter`) che consentono al modello di scalare e spostare l'output normalizzato. Sono inizializzati a uno e zero, rispettivamente.
|
||||
- **Processo di Normalizzazione:**
|
||||
- **Calcola Media (`mean`):** Calcola la media dell'input `x` attraverso la dimensione di embedding (`dim=-1`), mantenendo la dimensione per il broadcasting (`keepdim=True`).
|
||||
- **Calcola Varianza (`var`):** Calcola la varianza di `x` attraverso la dimensione di embedding, mantenendo anche la dimensione. Il parametro `unbiased=False` assicura che la varianza venga calcolata utilizzando l'estimatore biased (dividendo per `N` invece di `N-1`), che è appropriato quando si normalizza sulle caratteristiche piuttosto che sui campioni.
|
||||
@ -302,7 +302,7 @@ return self.scale * norm_x + self.shift
|
||||
- **Scala e Sposta:** Applica i parametri apprendibili `scale` e `shift` all'output normalizzato.
|
||||
|
||||
> [!TIP]
|
||||
> L'obiettivo è garantire una media di 0 con una varianza di 1 attraverso tutte le dimensioni dello stesso token. Lo scopo di questo è **stabilizzare l'addestramento delle reti neurali profonde** riducendo il cambiamento interno della covariazione, che si riferisce al cambiamento nella distribuzione delle attivazioni della rete a causa dell'aggiornamento dei parametri durante l'addestramento.
|
||||
> L'obiettivo è garantire una media di 0 con una varianza di 1 attraverso tutte le dimensioni dello stesso token. Lo scopo di questo è **stabilizzare l'addestramento delle reti neurali profonde** riducendo il cambiamento interno della covariata, che si riferisce al cambiamento nella distribuzione delle attivazioni della rete a causa dell'aggiornamento dei parametri durante l'addestramento.
|
||||
|
||||
### **Blocco Transformer**
|
||||
|
||||
@ -366,7 +366,7 @@ return x # Output shape: (batch_size, seq_len, emb_dim)
|
||||
- **Layer Norm (`norm2`):** Normalizza l'input.
|
||||
- **FeedForward Network (`ff`):** Applica la trasformazione feedforward.
|
||||
- **Dropout (`drop_shortcut`):** Applica dropout.
|
||||
- **Aggiungi Residuo (`x + shortcut`):** Combina con l'input dal primo percorso residuo.
|
||||
- **Aggiungi Residuo (`x + shortcut`):** Combina con l'input del primo percorso residuo.
|
||||
|
||||
> [!TIP]
|
||||
> Il blocco transformer raggruppa tutte le reti insieme e applica alcune **normalizzazioni** e **dropout** per migliorare la stabilità e i risultati dell'addestramento.\
|
||||
@ -437,9 +437,9 @@ return logits # Output shape: (batch_size, seq_len, vocab_size)
|
||||
- **Token Embeddings (`tok_emb`):** Converte gli indici dei token in embedding. Come promemoria, questi sono i pesi dati a ciascuna dimensione di ciascun token nel vocabolario.
|
||||
- **Positional Embeddings (`pos_emb`):** Aggiunge informazioni posizionali agli embedding per catturare l'ordine dei token. Come promemoria, questi sono i pesi dati ai token in base alla loro posizione nel testo.
|
||||
- **Dropout (`drop_emb`):** Applicato agli embedding per la regolarizzazione.
|
||||
- **Blocchi Transformer (`trf_blocks`):** Stack di `n_layers` blocchi transformer per elaborare gli embedding.
|
||||
- **Transformer Blocks (`trf_blocks`):** Stack di `n_layers` blocchi transformer per elaborare gli embedding.
|
||||
- **Normalizzazione Finale (`final_norm`):** Normalizzazione dello strato prima dello strato di output.
|
||||
- **Strato di Output (`out_head`):** Proietta gli stati nascosti finali alla dimensione del vocabolario per produrre logit per la previsione.
|
||||
- **Output Layer (`out_head`):** Proietta gli stati nascosti finali alla dimensione del vocabolario per produrre logits per la previsione.
|
||||
|
||||
> [!TIP]
|
||||
> L'obiettivo di questa classe è utilizzare tutte le altre reti menzionate per **prevedere il prossimo token in una sequenza**, fondamentale per compiti come la generazione di testo.
|
||||
@ -448,7 +448,7 @@ return logits # Output shape: (batch_size, seq_len, vocab_size)
|
||||
>
|
||||
> Inoltre, uno strato di **normalizzazione** è aggiunto **prima** dell'**output** e uno strato lineare finale è applicato alla fine per ottenere i risultati con le dimensioni appropriate. Nota come ogni vettore finale abbia la dimensione del vocabolario utilizzato. Questo perché sta cercando di ottenere una probabilità per ogni possibile token all'interno del vocabolario.
|
||||
|
||||
## Numero di Parametri da Addestrare
|
||||
## Numero di Parametri da addestrare
|
||||
|
||||
Avendo definita la struttura GPT, è possibile scoprire il numero di parametri da addestrare:
|
||||
```python
|
||||
@ -519,7 +519,7 @@ total_qkv_params = 3 * qkv_params = 3 * 589,824 = 1,769,472
|
||||
out_proj_params = (emb_dim * emb_dim) + emb_dim = (768 * 768) + 768 = 589,824 + 768 = 590,592
|
||||
```
|
||||
|
||||
- **Totale Parametri Attenzione Multi-Testa:**
|
||||
- **Parametri Totali di Attenzione Multi-Testa:**
|
||||
|
||||
```python
|
||||
mha_params = total_qkv_params + out_proj_params
|
||||
@ -547,7 +547,7 @@ 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
|
||||
```
|
||||
|
||||
- **Totale Parametri FeedForward:**
|
||||
- **Parametri Totali FeedForward:**
|
||||
|
||||
```python
|
||||
ff_params = ff_first_layer_params + ff_second_layer_params
|
||||
@ -565,7 +565,7 @@ ff_params = 2,362,368 + 2,360,064 = 4,722,432
|
||||
layer_norm_params_per_block = 2 * (2 * emb_dim) = 2 * 768 * 2 = 3,072
|
||||
```
|
||||
|
||||
**d. Totale Parametri per Blocco Transformer**
|
||||
**d. Parametri Totali per Blocco Transformer**
|
||||
```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
|
||||
|
941
src/AI/AI-llm-architecture/6.-pre-training-and-loading-models.md
Normal file
941
src/AI/AI-llm-architecture/6.-pre-training-and-loading-models.md
Normal file
@ -0,0 +1,941 @@
|
||||
# 6. Pre-training & Loading models
|
||||
|
||||
## Text Generation
|
||||
|
||||
Per addestrare un modello, è necessario che il modello sia in grado di generare nuovi token. Poi confronteremo i token generati con quelli attesi per addestrare il modello a **imparare i token che deve generare**.
|
||||
|
||||
Come negli esempi precedenti, abbiamo già previsto alcuni token, è possibile riutilizzare quella funzione per questo scopo.
|
||||
|
||||
> [!TIP]
|
||||
> L'obiettivo di questa sesta fase è molto semplice: **Addestrare il modello da zero**. Per questo verrà utilizzata l'architettura LLM precedente con alcuni cicli sui set di dati utilizzando le funzioni di perdita e l'ottimizzatore definiti per addestrare tutti i parametri del modello.
|
||||
|
||||
## Text Evaluation
|
||||
|
||||
Per eseguire un addestramento corretto è necessario misurare le previsioni ottenute per il token atteso. L'obiettivo dell'addestramento è massimizzare la probabilità del token corretto, il che implica aumentare la sua probabilità rispetto ad altri token.
|
||||
|
||||
Per massimizzare la probabilità del token corretto, i pesi del modello devono essere modificati affinché quella probabilità sia massimizzata. Gli aggiornamenti dei pesi avvengono tramite **backpropagation**. Questo richiede una **funzione di perdita da massimizzare**. In questo caso, la funzione sarà la **differenza tra la previsione effettuata e quella desiderata**.
|
||||
|
||||
Tuttavia, invece di lavorare con le previsioni grezze, si lavorerà con un logaritmo in base n. Quindi, se la previsione attuale del token atteso era 7.4541e-05, il logaritmo naturale (base *e*) di **7.4541e-05** è approssimativamente **-9.5042**.\
|
||||
Poi, per ogni voce con una lunghezza di contesto di 5 token, ad esempio, il modello dovrà prevedere 5 token, con i primi 4 token che sono gli ultimi dell'input e il quinto quello previsto. Pertanto, per ogni voce avremo 5 previsioni in quel caso (anche se i primi 4 erano nell'input, il modello non lo sa) con 5 token attesi e quindi 5 probabilità da massimizzare.
|
||||
|
||||
Pertanto, dopo aver eseguito il logaritmo naturale su ogni previsione, si calcola la **media**, si rimuove il **simbolo meno** (questo è chiamato _cross entropy loss_) e questo è il **numero da ridurre il più vicino possibile a 0** perché il logaritmo naturale di 1 è 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>
|
||||
|
||||
Un altro modo per misurare quanto sia buono il modello è chiamato perplexity. **Perplexity** è una metrica utilizzata per valutare quanto bene un modello di probabilità prevede un campione. Nella modellazione del linguaggio, rappresenta l'**incertezza del modello** quando prevede il prossimo token in una sequenza.\
|
||||
Ad esempio, un valore di perplexity di 48725 significa che quando deve prevedere un token non è sicuro su quale tra 48.725 token nel vocabolario sia quello giusto.
|
||||
|
||||
## Pre-Train Example
|
||||
|
||||
Questo è il codice iniziale proposto 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) alcune volte leggermente modificato
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Codice precedente utilizzato qui ma già spiegato nelle sezioni precedenti</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"
|
||||
)
|
||||
```
|
||||
### Funzioni per trasformare testo <--> ids
|
||||
|
||||
Queste sono alcune semplici funzioni che possono essere utilizzate per trasformare i testi dal vocabolario in ids e viceversa. Questo è necessario all'inizio della gestione del testo e alla fine delle previsioni:
|
||||
```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())
|
||||
```
|
||||
### Funzioni di generazione del testo
|
||||
|
||||
In una sezione precedente è stata presentata una funzione che otteneva solo il **token più probabile** dopo aver ottenuto i logit. Tuttavia, questo significa che per ogni input verrà sempre generato lo stesso output, il che lo rende molto deterministico.
|
||||
|
||||
La seguente funzione `generate_text` applicherà i concetti di `top-k`, `temperature` e `multinomial`.
|
||||
|
||||
- Il **`top-k`** significa che inizieremo a ridurre a `-inf` tutte le probabilità di tutti i token tranne i top k token. Quindi, se k=3, prima di prendere una decisione solo i 3 token più probabili avranno una probabilità diversa da `-inf`.
|
||||
- La **`temperature`** significa che ogni probabilità sarà divisa per il valore della temperatura. Un valore di `0.1` migliorerà la probabilità più alta rispetto a quella più bassa, mentre una temperatura di `5`, ad esempio, la renderà più piatta. Questo aiuta a migliorare la variazione nelle risposte che vorremmo che l'LLM avesse.
|
||||
- Dopo aver applicato la temperatura, una funzione **`softmax`** viene applicata nuovamente per fare in modo che tutti i token rimanenti abbiano una probabilità totale di 1.
|
||||
- Infine, invece di scegliere il token con la probabilità più alta, la funzione **`multinomial`** viene applicata per **prevedere il prossimo token in base alle probabilità finali**. Quindi, se il token 1 aveva il 70% di probabilità, il token 2 il 20% e il token 3 il 10%, il 70% delle volte verrà selezionato il token 1, il 20% delle volte sarà il token 2 e il 10% delle volte sarà il token 3.
|
||||
```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
|
||||
```
|
||||
> [!TIP]
|
||||
> Esiste un'alternativa comune a `top-k` chiamata [**`top-p`**](https://en.wikipedia.org/wiki/Top-p_sampling), nota anche come campionamento del nucleo, che invece di ottenere k campioni con la probabilità più alta, **organizza** tutto il **vocabolario** risultante per probabilità e **sommando** da quella con la probabilità più alta a quella più bassa fino a quando non si **raggiunge una soglia**.
|
||||
>
|
||||
> Poi, **solo quelle parole** del vocabolario saranno considerate in base alle loro probabilità relative.
|
||||
>
|
||||
> Questo consente di non dover selezionare un numero di campioni `k`, poiché il k ottimale potrebbe essere diverso in ogni caso, ma **solo una soglia**.
|
||||
>
|
||||
> _Nota che questo miglioramento non è incluso nel codice precedente._
|
||||
|
||||
> [!TIP]
|
||||
> Un altro modo per migliorare il testo generato è utilizzare **Beam search** invece della ricerca greedy utilizzata in questo esempio.\
|
||||
> A differenza della ricerca greedy, che seleziona la parola successiva più probabile a ogni passo e costruisce una singola sequenza, **beam search tiene traccia delle k sequenze parziali con il punteggio più alto** (chiamate "beams") a ogni passo. Esplorando più possibilità simultaneamente, bilancia efficienza e qualità, aumentando le possibilità di **trovare una sequenza complessiva migliore** che potrebbe essere persa dall'approccio greedy a causa di scelte subottimali precoci.
|
||||
>
|
||||
> _Nota che questo miglioramento non è incluso nel codice precedente._
|
||||
|
||||
### Funzioni di perdita
|
||||
|
||||
La funzione **`calc_loss_batch`** calcola l'entropia incrociata di una previsione di un singolo batch.\
|
||||
La **`calc_loss_loader`** ottiene l'entropia incrociata di tutti i batch e calcola l'**entropia incrociata media**.
|
||||
```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
|
||||
```
|
||||
> [!TIP]
|
||||
> **Gradient clipping** è una tecnica utilizzata per migliorare la **stabilità dell'addestramento** in grandi reti neurali impostando una **soglia massima** per le magnitudini dei gradienti. Quando i gradienti superano questo `max_norm` predefinito, vengono ridotti proporzionalmente per garantire che gli aggiornamenti ai parametri del modello rimangano all'interno di un intervallo gestibile, prevenendo problemi come i gradienti esplosivi e garantendo un addestramento più controllato e stabile.
|
||||
>
|
||||
> _Nota che questo miglioramento non è incluso nel codice precedente._
|
||||
>
|
||||
> Controlla il seguente esempio:
|
||||
|
||||
<figure><img src="../../images/image (6) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### Caricamento Dati
|
||||
|
||||
Le funzioni `create_dataloader_v1` e `create_dataloader_v1` sono già state discusse in una sezione precedente.
|
||||
|
||||
Da qui nota come sia definito che il 90% del testo sarà utilizzato per l'addestramento mentre il 10% sarà utilizzato per la validazione e entrambi i set sono memorizzati in 2 diversi data loader.\
|
||||
Nota che a volte parte del set di dati è anche lasciata per un set di test per valutare meglio le prestazioni del modello.
|
||||
|
||||
Entrambi i data loader utilizzano la stessa dimensione del batch, lunghezza massima, stride e num workers (0 in questo caso).\
|
||||
Le principali differenze sono i dati utilizzati da ciascuno, e il validatore non scarta l'ultimo né mescola i dati poiché non è necessario per scopi di validazione.
|
||||
|
||||
Inoltre, il fatto che **lo stride sia grande quanto la lunghezza del contesto** significa che non ci sarà sovrapposizione tra i contesti utilizzati per addestrare i dati (riduce l'overfitting ma anche il set di dati di addestramento).
|
||||
|
||||
Inoltre, nota che la dimensione del batch in questo caso è 2 per dividere i dati in 2 batch, l'obiettivo principale di questo è consentire l'elaborazione parallela e ridurre il consumo 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
|
||||
)
|
||||
```
|
||||
## Controlli di Sanità
|
||||
|
||||
L'obiettivo è verificare che ci siano abbastanza token per l'addestramento, che le forme siano quelle attese e ottenere alcune informazioni sul numero di token utilizzati per l'addestramento e per la validazione:
|
||||
```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)
|
||||
```
|
||||
### Seleziona dispositivo per addestramento e pre-calcoli
|
||||
|
||||
Il seguente codice seleziona semplicemente il dispositivo da utilizzare e calcola una perdita di addestramento e una perdita di validazione (senza aver ancora addestrato nulla) come punto di partenza.
|
||||
```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)
|
||||
```
|
||||
### Funzioni di addestramento
|
||||
|
||||
La funzione `generate_and_print_sample` prenderà semplicemente un contesto e genererà alcuni token per avere un'idea di quanto sia buona il modello a quel punto. Questa viene chiamata da `train_model_simple` ad ogni passo.
|
||||
|
||||
La funzione `evaluate_model` viene chiamata con la stessa frequenza indicata alla funzione di addestramento ed è utilizzata per misurare la perdita di addestramento e la perdita di validazione a quel punto nell'addestramento del modello.
|
||||
|
||||
Poi la grande funzione `train_model_simple` è quella che effettivamente addestra il modello. Si aspetta:
|
||||
|
||||
- Il caricatore di dati di addestramento (con i dati già separati e preparati per l'addestramento)
|
||||
- Il caricatore di validazione
|
||||
- L'**ottimizzatore** da utilizzare durante l'addestramento: Questa è la funzione che utilizzerà i gradienti e aggiornerà i parametri per ridurre la perdita. In questo caso, come vedrai, viene utilizzato `AdamW`, ma ce ne sono molti altri.
|
||||
- `optimizer.zero_grad()` viene chiamato per resettare i gradienti ad ogni round per non accumularli.
|
||||
- Il parametro **`lr`** è il **tasso di apprendimento** che determina la **dimensione dei passi** effettuati durante il processo di ottimizzazione quando si aggiornano i parametri del modello. Un tasso di apprendimento **più piccolo** significa che l'ottimizzatore **effettua aggiornamenti più piccoli** ai pesi, il che può portare a una convergenza più **precisa** ma potrebbe **rallentare** l'addestramento. Un tasso di apprendimento **più grande** può accelerare l'addestramento ma **rischia di superare** il minimo della funzione di perdita (**saltare oltre** il punto in cui la funzione di perdita è minimizzata).
|
||||
- **Weight Decay** modifica il passo di **Calcolo della Perdita** aggiungendo un termine extra che penalizza i pesi grandi. Questo incoraggia l'ottimizzatore a trovare soluzioni con pesi più piccoli, bilanciando tra l'adattamento ai dati e il mantenimento del modello semplice, prevenendo l'overfitting nei modelli di machine learning scoraggiando il modello dall'assegnare troppa importanza a qualsiasi singola caratteristica.
|
||||
- Gli ottimizzatori tradizionali come SGD con regolarizzazione L2 accoppiano il weight decay con il gradiente della funzione di perdita. Tuttavia, **AdamW** (una variante dell'ottimizzatore Adam) decouples il weight decay dall'aggiornamento del gradiente, portando a una regolarizzazione più efficace.
|
||||
- Il dispositivo da utilizzare per l'addestramento
|
||||
- Il numero di epoche: Numero di volte per passare sui dati di addestramento
|
||||
- La frequenza di valutazione: La frequenza per chiamare `evaluate_model`
|
||||
- L'iterazione di valutazione: Il numero di batch da utilizzare quando si valuta lo stato attuale del modello quando si chiama `generate_and_print_sample`
|
||||
- Il contesto di partenza: Quale frase iniziale utilizzare quando si chiama `generate_and_print_sample`
|
||||
- Il 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
|
||||
```
|
||||
> [!TIP]
|
||||
> Per migliorare il tasso di apprendimento ci sono un paio di tecniche rilevanti chiamate **linear warmup** e **cosine decay.**
|
||||
>
|
||||
> **Linear warmup** consiste nel definire un tasso di apprendimento iniziale e uno massimo e aggiornarlo costantemente dopo ogni epoca. Questo perché iniziare l'addestramento con aggiornamenti di peso più piccoli riduce il rischio che il modello incontri aggiornamenti grandi e destabilizzanti durante la fase di addestramento.\
|
||||
> **Cosine decay** è una tecnica che **riduce gradualmente il tasso di apprendimento** seguendo una curva a mezza coseno **dopo la fase di warmup**, rallentando gli aggiornamenti di peso per **minimizzare il rischio di superare** i minimi di perdita e garantire stabilità nell'addestramento nelle fasi successive.
|
||||
>
|
||||
> _Nota che questi miglioramenti non sono inclusi nel codice precedente._
|
||||
|
||||
### Inizia l'addestramento
|
||||
```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.")
|
||||
```
|
||||
### Stampa l'evoluzione dell'addestramento
|
||||
|
||||
Con la seguente funzione è possibile stampare l'evoluzione del modello mentre veniva addestrato.
|
||||
```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)
|
||||
```
|
||||
### Salva il modello
|
||||
|
||||
È possibile salvare il modello + ottimizzatore se si desidera continuare l'addestramento in seguito:
|
||||
```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
|
||||
```
|
||||
O solo il modello se hai intenzione di usarlo solo:
|
||||
```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
|
||||
```
|
||||
## Caricamento dei pesi di GPT2
|
||||
|
||||
Ci sono 2 script rapidi per caricare i pesi di GPT2 localmente. Per entrambi puoi clonare il repository [https://github.com/rasbt/LLMs-from-scratch](https://github.com/rasbt/LLMs-from-scratch) localmente, poi:
|
||||
|
||||
- Lo 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) scaricherà tutti i pesi e trasformerà i formati da OpenAI a quelli attesi dal nostro LLM. Lo script è anche preparato con la configurazione necessaria e con il prompt: "Every effort moves you"
|
||||
- Lo 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) ti consente di caricare qualsiasi dei pesi di GPT2 localmente (basta cambiare la variabile `CHOOSE_MODEL`) e prevedere testo da alcuni prompt.
|
||||
|
||||
## Riferimenti
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -17,7 +17,7 @@ LoRA rende possibile il fine-tuning di **grandi modelli** in modo efficiente cam
|
||||
3. **Fine-Tuning Efficiente Specifico per Compiti**: Quando vuoi adattare il modello a un **nuovo compito**, puoi semplicemente addestrare le **piccole matrici LoRA** (A e B) lasciando il resto del modello com'è. Questo è **molto più efficiente** rispetto a riaddestrare l'intero modello.
|
||||
4. **Efficienza di Archiviazione**: Dopo il fine-tuning, invece di salvare un **nuovo modello intero** per ogni compito, devi solo memorizzare le **matrici LoRA**, che sono molto piccole rispetto all'intero modello. Questo rende più facile adattare il modello a molti compiti senza utilizzare troppo spazio di archiviazione.
|
||||
|
||||
Per implementare LoraLayers invece di quelli Lineari durante un fine-tuning, questo codice è proposto qui [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):
|
||||
Per implementare LoraLayers invece di quelli Lineari durante un fine-tuning, qui è proposto questo codice [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
|
||||
|
||||
|
@ -0,0 +1,110 @@
|
||||
# 7.1. Fine-Tuning for Classification
|
||||
|
||||
## What is
|
||||
|
||||
Il fine-tuning è il processo di prendere un **modello pre-addestrato** che ha appreso **modelli linguistici generali** da enormi quantità di dati e **adattarlo** per eseguire un **compito specifico** o per comprendere il linguaggio specifico di un dominio. Questo si ottiene continuando l'addestramento del modello su un dataset più piccolo e specifico per il compito, permettendogli di regolare i suoi parametri per adattarsi meglio alle sfumature dei nuovi dati, sfruttando al contempo la vasta conoscenza che ha già acquisito. Il fine-tuning consente al modello di fornire risultati più accurati e pertinenti in applicazioni specializzate senza la necessità di addestrare un nuovo modello da zero.
|
||||
|
||||
> [!TIP]
|
||||
> Poiché il pre-addestramento di un LLM che "comprende" il testo è piuttosto costoso, di solito è più facile ed economico fare fine-tuning su modelli pre-addestrati open source per eseguire un compito specifico che vogliamo che esegua.
|
||||
|
||||
> [!TIP]
|
||||
> L'obiettivo di questa sezione è mostrare come fare fine-tuning su un modello già pre-addestrato, in modo che invece di generare nuovo testo, l'LLM selezioni e fornisca le **probabilità che il testo fornito venga categorizzato in ciascuna delle categorie date** (come se un testo fosse spam o meno).
|
||||
|
||||
## Preparing the data set
|
||||
|
||||
### Data set size
|
||||
|
||||
Naturalmente, per fare fine-tuning su un modello è necessario avere dei dati strutturati da utilizzare per specializzare il tuo LLM. Nell'esempio proposto 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 è fine-tuned per rilevare se un'email è spam o meno utilizzando i dati di [https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip](https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip)_._
|
||||
|
||||
Questo dataset contiene molti più esempi di "non spam" che di "spam", quindi il libro suggerisce di **utilizzare solo tanti esempi di "non spam" quanti di "spam"** (rimuovendo quindi dal set di addestramento tutti gli esempi extra). In questo caso, erano 747 esempi di ciascuno.
|
||||
|
||||
Poi, **il 70%** del dataset è utilizzato per **l'addestramento**, **il 10%** per **la validazione** e **il 20%** per **il test**.
|
||||
|
||||
- Il **set di validazione** è utilizzato durante la fase di addestramento per fare fine-tuning degli **iperparametri** del modello e prendere decisioni sull'architettura del modello, aiutando effettivamente a prevenire l'overfitting fornendo feedback su come il modello si comporta su dati non visti. Permette miglioramenti iterativi senza pregiudicare la valutazione finale.
|
||||
- Questo significa che, sebbene i dati inclusi in questo dataset non siano utilizzati direttamente per l'addestramento, vengono utilizzati per ottimizzare i migliori **iperparametri**, quindi questo set non può essere utilizzato per valutare le prestazioni del modello come quello di test.
|
||||
- Al contrario, il **set di test** è utilizzato **solo dopo** che il modello è stato completamente addestrato e tutti gli aggiustamenti sono stati completati; fornisce una valutazione imparziale della capacità del modello di generalizzare a nuovi dati non visti. Questa valutazione finale sul set di test fornisce un'indicazione realistica di come ci si aspetta che il modello si comporti nelle applicazioni del mondo reale.
|
||||
|
||||
### Entries length
|
||||
|
||||
Poiché l'esempio di addestramento si aspetta voci (testo delle email in questo caso) della stessa lunghezza, è stato deciso di rendere ogni voce grande quanto la più grande aggiungendo gli id di `<|endoftext|>` come padding.
|
||||
|
||||
### Initialize the model
|
||||
|
||||
Utilizzando i pesi pre-addestrati open-source, inizializza il modello per l'addestramento. Abbiamo già fatto questo prima e seguendo le istruzioni di [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) puoi farlo facilmente.
|
||||
|
||||
## Classification head
|
||||
|
||||
In questo esempio specifico (predire se un testo è spam o meno), non siamo interessati a fare fine-tuning secondo il vocabolario completo di GPT2, ma vogliamo solo che il nuovo modello dica se l'email è spam (1) o meno (0). Pertanto, andremo a **modificare l'ultimo strato che** fornisce le probabilità per token del vocabolario per uno che fornisce solo le probabilità di essere spam o meno (quindi come un vocabolario di 2 parole).
|
||||
```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
|
||||
)
|
||||
```
|
||||
## Parametri da ottimizzare
|
||||
|
||||
Per ottimizzare rapidamente, è più facile non ottimizzare tutti i parametri ma solo alcuni finali. Questo perché è noto che i livelli inferiori catturano generalmente strutture linguistiche di base e semantiche applicabili. Quindi, **ottimizzare solo gli ultimi livelli è di solito sufficiente e più veloce**.
|
||||
```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
|
||||
|
||||
Nelle sezioni precedenti, il LLM è stato addestrato riducendo la perdita di ogni token previsto, anche se quasi tutti i token previsti erano nella frase di input (solo 1 alla fine era realmente previsto) affinché il modello comprendesse meglio la lingua.
|
||||
|
||||
In questo caso ci interessa solo che il modello sia in grado di prevedere se il modello è spam o meno, quindi ci interessa solo l'ultimo token previsto. Pertanto, è necessario modificare le nostre precedenti funzioni di perdita di addestramento per tenere conto solo di quel token.
|
||||
|
||||
Questo è implementato 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) come:
|
||||
```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
|
||||
```
|
||||
Nota come per ogni batch siamo interessati solo ai **logits dell'ultimo token previsto**.
|
||||
|
||||
## Codice completo per la classificazione fine-tune di GPT2
|
||||
|
||||
Puoi trovare tutto il codice per fine-tunare GPT2 come classificatore di spam 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)
|
||||
|
||||
## Riferimenti
|
||||
|
||||
- [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,11 +1,11 @@
|
||||
# 7.2. Ottimizzazione per seguire istruzioni
|
||||
# 7.2. Fine-Tuning per seguire le istruzioni
|
||||
|
||||
> [!TIP]
|
||||
> L'obiettivo di questa sezione è mostrare come **ottimizzare un modello già pre-addestrato per seguire istruzioni** piuttosto che generare semplicemente testo, ad esempio, rispondendo a compiti come un chatbot.
|
||||
> L'obiettivo di questa sezione è mostrare come **affinare un modello già pre-addestrato per seguire le istruzioni** piuttosto che generare semplicemente testo, ad esempio, rispondendo a compiti come un chatbot.
|
||||
|
||||
## Dataset
|
||||
|
||||
Per ottimizzare un LLM per seguire istruzioni è necessario avere un dataset con istruzioni e risposte per ottimizzare il LLM. Ci sono diversi formati per addestrare un LLM a seguire istruzioni, ad esempio:
|
||||
Per affinare un LLM per seguire le istruzioni è necessario avere un dataset con istruzioni e risposte per affinare il LLM. Ci sono diversi formati per addestrare un LLM a seguire le istruzioni, ad esempio:
|
||||
|
||||
- L'esempio dello stile di prompt Apply Alpaca:
|
||||
```csharp
|
||||
@ -77,11 +77,11 @@ Nota che è anche possibile eseguire questa revisione passando le risposte gener
|
||||
|
||||
Altri test da eseguire per verificare la qualità delle risposte:
|
||||
|
||||
1. **Measuring Massive Multitask Language Understanding (**[**MMLU**](https://arxiv.org/abs/2009.03300)**):** MMLU valuta la conoscenza e le capacità di problem-solving di un modello in 57 soggetti, comprese le scienze umane, le scienze e altro. Utilizza domande a scelta multipla per valutare la comprensione a vari livelli di difficoltà, dall'elementare all'avanzato professionale.
|
||||
1. **Measuring Massive Multitask Language Understanding (**[**MMLU**](https://arxiv.org/abs/2009.03300)**):** MMLU valuta le conoscenze e le capacità di problem-solving di un modello in 57 soggetti, comprese le scienze umane, le scienze e altro. Utilizza domande a scelta multipla per valutare la comprensione a vari livelli di difficoltà, dall'elementare all'avanzato professionale.
|
||||
2. [**LMSYS Chatbot Arena**](https://arena.lmsys.org): Questa piattaforma consente agli utenti di confrontare le risposte di diversi chatbot fianco a fianco. Gli utenti inseriscono un prompt e più chatbot generano risposte che possono essere confrontate direttamente.
|
||||
3. [**AlpacaEval**](https://github.com/tatsu-lab/alpaca_eval)**:** AlpacaEval è un framework di valutazione automatizzato in cui un LLM avanzato come GPT-4 valuta le risposte di altri modelli a vari prompt.
|
||||
4. **General Language Understanding Evaluation (**[**GLUE**](https://gluebenchmark.com/)**):** GLUE è una raccolta di nove compiti di comprensione del linguaggio naturale, tra cui analisi del sentiment, implicazione testuale e risposta a domande.
|
||||
5. [**SuperGLUE**](https://super.gluebenchmark.com/)**:** Basandosi su GLUE, SuperGLUE include compiti più impegnativi progettati per essere difficili per i modelli attuali.
|
||||
5. [**SuperGLUE**](https://super.gluebenchmark.com/)**:** Basato su GLUE, SuperGLUE include compiti più impegnativi progettati per essere difficili per i modelli attuali.
|
||||
6. **Beyond the Imitation Game Benchmark (**[**BIG-bench**](https://github.com/google/BIG-bench)**):** BIG-bench è un benchmark su larga scala con oltre 200 compiti che testano le capacità di un modello in aree come ragionamento, traduzione e risposta a domande.
|
||||
7. **Holistic Evaluation of Language Models (**[**HELM**](https://crfm.stanford.edu/helm/lite/latest/)**):** HELM fornisce una valutazione completa attraverso vari metriche come accuratezza, robustezza e equità.
|
||||
8. [**OpenAI Evals**](https://github.com/openai/evals)**:** Un framework di valutazione open-source di OpenAI che consente di testare modelli AI su compiti personalizzati e standardizzati.
|
||||
|
@ -55,7 +55,7 @@ Dovresti iniziare leggendo questo post per alcuni concetti di base che dovresti
|
||||
> [!TIP]
|
||||
> L'obiettivo di questa quinta fase è molto semplice: **Sviluppare l'architettura del LLM completo**. Metti tutto insieme, applica tutti i layer e crea tutte le funzioni per generare testo o trasformare testo in ID e viceversa.
|
||||
>
|
||||
> Questa architettura sarà utilizzata sia per l'addestramento che per la previsione del testo dopo che è stato addestrato.
|
||||
> Questa architettura sarà utilizzata sia per l'addestramento che per la previsione del testo dopo che è stata addestrata.
|
||||
|
||||
{{#ref}}
|
||||
5.-llm-architecture.md
|
||||
@ -91,7 +91,7 @@ Dovresti iniziare leggendo questo post per alcuni concetti di base che dovresti
|
||||
## 7.2. Fine-Tuning to follow instructions
|
||||
|
||||
> [!TIP]
|
||||
> L'obiettivo di questa sezione è mostrare come **ottimizzare un modello già pre-addestrato per seguire istruzioni** piuttosto che semplicemente generare testo, ad esempio, rispondere a compiti come un chatbot.
|
||||
> L'obiettivo di questa sezione è mostrare come **ottimizzare un modello già pre-addestrato per seguire istruzioni** piuttosto che semplicemente generare testo, ad esempio, rispondendo a compiti come un chatbot.
|
||||
|
||||
{{#ref}}
|
||||
7.2.-fine-tuning-to-follow-instructions.md
|
||||
|
Loading…
x
Reference in New Issue
Block a user