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
8759f27358
commit
b8f52bae75
@ -2,7 +2,7 @@
|
||||
|
||||
## Préentraînement
|
||||
|
||||
Le préentraînement est la phase fondamentale dans le développement d'un modèle de langage de grande taille (LLM) où le modèle est exposé à d'énormes et diverses quantités de données textuelles. Pendant cette étape, **le LLM apprend les structures, les motifs et les nuances fondamentaux de la langue**, y compris la grammaire, le vocabulaire, la syntaxe et les relations contextuelles. En traitant ces données étendues, le modèle acquiert une large compréhension de la langue et des connaissances générales sur le monde. Cette base complète permet au LLM de générer un texte cohérent et contextuellement pertinent. Par la suite, ce modèle préentraîné peut subir un ajustement fin, où il est formé davantage sur des ensembles de données spécialisés pour adapter ses capacités à des tâches ou domaines spécifiques, améliorant ainsi sa performance et sa pertinence dans des applications ciblées.
|
||||
Le préentraînement est la phase fondamentale dans le développement d'un modèle de langage de grande taille (LLM) où le modèle est exposé à d'énormes et diverses quantités de données textuelles. Pendant cette étape, **le LLM apprend les structures, les motifs et les nuances fondamentales de la langue**, y compris la grammaire, le vocabulaire, la syntaxe et les relations contextuelles. En traitant ces données étendues, le modèle acquiert une large compréhension de la langue et des connaissances générales sur le monde. Cette base complète permet au LLM de générer un texte cohérent et contextuellement pertinent. Par la suite, ce modèle préentraîné peut subir un ajustement fin, où il est formé davantage sur des ensembles de données spécialisés pour adapter ses capacités à des tâches ou domaines spécifiques, améliorant ainsi sa performance et sa pertinence dans des applications ciblées.
|
||||
|
||||
## Principaux composants du LLM
|
||||
|
||||
@ -10,7 +10,7 @@ Généralement, un LLM est caractérisé par la configuration utilisée pour l'e
|
||||
|
||||
- **Paramètres** : Les paramètres sont les **poids et biais apprenables** dans le réseau de neurones. Ce sont les nombres que le processus d'entraînement ajuste pour minimiser la fonction de perte et améliorer la performance du modèle sur la tâche. Les LLM utilisent généralement des millions de paramètres.
|
||||
- **Longueur de contexte** : C'est la longueur maximale de chaque phrase utilisée pour pré-entraîner le LLM.
|
||||
- **Dimension d'embedding** : La taille du vecteur utilisé pour représenter chaque token ou mot. Les LLM utilisent généralement des milliards de dimensions.
|
||||
- **Dimension d'embedding** : La taille du vecteur utilisé pour représenter chaque jeton ou mot. Les LLM utilisent généralement des milliards de dimensions.
|
||||
- **Dimension cachée** : La taille des couches cachées dans le réseau de neurones.
|
||||
- **Nombre de couches (profondeur)** : Combien de couches le modèle a. Les LLM utilisent généralement des dizaines de couches.
|
||||
- **Nombre de têtes d'attention** : Dans les modèles de transformateurs, c'est combien de mécanismes d'attention séparés sont utilisés dans chaque couche. Les LLM utilisent généralement des dizaines de têtes.
|
||||
@ -30,7 +30,7 @@ GPT_CONFIG_124M = {
|
||||
```
|
||||
## Tenseurs dans PyTorch
|
||||
|
||||
Dans PyTorch, un **tenseur** est une structure de données fondamentale qui sert d'un tableau multi-dimensionnel, généralisant des concepts comme les scalaires, les vecteurs et les matrices à des dimensions potentiellement supérieures. Les tenseurs sont la principale façon dont les données sont représentées et manipulées dans PyTorch, en particulier dans le contexte de l'apprentissage profond et des réseaux de neurones.
|
||||
Dans PyTorch, un **tenseur** est une structure de données fondamentale qui sert d'un tableau multidimensionnel, généralisant des concepts comme les scalaires, les vecteurs et les matrices à des dimensions potentiellement supérieures. Les tenseurs sont la principale façon dont les données sont représentées et manipulées dans PyTorch, en particulier dans le contexte de l'apprentissage profond et des réseaux de neurones.
|
||||
|
||||
### Concept Mathématique des Tenseurs
|
||||
|
||||
@ -41,13 +41,13 @@ Dans PyTorch, un **tenseur** est une structure de données fondamentale qui sert
|
||||
|
||||
### Tenseurs en tant que Conteneurs de Données
|
||||
|
||||
D'un point de vue computationnel, les tenseurs agissent comme des conteneurs pour des données multi-dimensionnelles, où chaque dimension peut représenter différentes caractéristiques ou aspects des données. Cela rend les tenseurs particulièrement adaptés pour gérer des ensembles de données complexes dans des tâches d'apprentissage automatique.
|
||||
D'un point de vue computationnel, les tenseurs agissent comme des conteneurs pour des données multidimensionnelles, où chaque dimension peut représenter différentes caractéristiques ou aspects des données. Cela rend les tenseurs particulièrement adaptés pour gérer des ensembles de données complexes dans des tâches d'apprentissage automatique.
|
||||
|
||||
### Tenseurs PyTorch vs. Tableaux NumPy
|
||||
|
||||
Bien que les tenseurs PyTorch soient similaires aux tableaux NumPy dans leur capacité à stocker et manipuler des données numériques, ils offrent des fonctionnalités supplémentaires cruciales pour l'apprentissage profond :
|
||||
|
||||
- **Différentiation Automatique** : Les tenseurs PyTorch prennent en charge le calcul automatique des gradients (autograd), ce qui simplifie le processus de calcul des dérivées nécessaires à l'entraînement des réseaux de neurones.
|
||||
- **Différentiation Automatique** : Les tenseurs PyTorch prennent en charge le calcul automatique des gradients (autograd), ce qui simplifie le processus de calcul des dérivées nécessaires pour l'entraînement des réseaux de neurones.
|
||||
- **Accélération GPU** : Les tenseurs dans PyTorch peuvent être déplacés et calculés sur des GPU, accélérant considérablement les calculs à grande échelle.
|
||||
|
||||
### Création de Tenseurs dans PyTorch
|
||||
|
@ -29,9 +29,9 @@ Si un mot comme `"Au revoir"` n'est pas dans le vocabulaire, il est remplacé pa
|
||||
`"Au revoir, le monde!"` → `["[UNK]", ",", "le", "monde", "!"]` → `[987, 455, 78, 467]`\
|
||||
_(En supposant que `[UNK]` a l'ID `987`)_
|
||||
|
||||
### **Méthodes Avancées de Tokenisation**
|
||||
### **Méthodes de Tokenisation Avancées**
|
||||
|
||||
Bien que le tokeniseur de base fonctionne bien pour des textes simples, il a des limitations, en particulier avec de grands vocabulaires et la gestion de nouveaux mots ou de mots rares. Les méthodes avancées de tokenisation abordent ces problèmes en décomposant le texte en sous-unités plus petites ou en optimisant le processus de tokenisation.
|
||||
Bien que le tokeniseur de base fonctionne bien pour des textes simples, il a des limitations, en particulier avec de grands vocabulaires et la gestion de nouveaux mots ou de mots rares. Les méthodes de tokenisation avancées abordent ces problèmes en décomposant le texte en sous-unités plus petites ou en optimisant le processus de tokenisation.
|
||||
|
||||
1. **Encodage par Paires de Bytes (BPE) :**
|
||||
- **Objectif :** Réduit la taille du vocabulaire et gère les mots rares ou inconnus en les décomposant en paires de bytes fréquemment rencontrées.
|
||||
@ -43,9 +43,9 @@ Bien que le tokeniseur de base fonctionne bien pour des textes simples, il a des
|
||||
- Élimine le besoin d'un token `[UNK]` puisque tous les mots peuvent être représentés en combinant des tokens de sous-mots existants.
|
||||
- Vocabulaire plus efficace et flexible.
|
||||
- _Exemple :_\
|
||||
`"jouant"` pourrait être tokenisé en `["jou", "ant"]` si `"jou"` et `"ant"` sont des sous-mots fréquents.
|
||||
`"jouant"` pourrait être tokenisé en `["joue", "ant"]` si `"joue"` et `"ant"` sont des sous-mots fréquents.
|
||||
2. **WordPiece :**
|
||||
- **Utilisé par :** Des modèles comme BERT.
|
||||
- **Utilisé par :** Modèles comme BERT.
|
||||
- **Objectif :** Semblable à BPE, il décompose les mots en unités de sous-mots pour gérer les mots inconnus et réduire la taille du vocabulaire.
|
||||
- **Comment ça fonctionne :**
|
||||
- Commence avec un vocabulaire de base de caractères individuels.
|
||||
@ -57,7 +57,7 @@ Bien que le tokeniseur de base fonctionne bien pour des textes simples, il a des
|
||||
- _Exemple :_\
|
||||
`"malheur"` pourrait être tokenisé en `["mal", "heur"]` ou `["mal", "heure"]` selon le vocabulaire.
|
||||
3. **Modèle de Langage Unigramme :**
|
||||
- **Utilisé par :** Des modèles comme SentencePiece.
|
||||
- **Utilisé par :** Modèles comme SentencePiece.
|
||||
- **Objectif :** Utilise un modèle probabiliste pour déterminer l'ensemble de tokens de sous-mots le plus probable.
|
||||
- **Comment ça fonctionne :**
|
||||
- Commence avec un grand ensemble de tokens potentiels.
|
||||
|
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. Échantillonnage des Données
|
||||
|
||||
## **Échantillonnage des Données**
|
||||
|
||||
**L'échantillonnage des données** est un processus crucial dans la préparation des données pour l'entraînement de modèles de langage de grande taille (LLMs) comme GPT. Il implique l'organisation des données textuelles en séquences d'entrée et de cible que le modèle utilise pour apprendre à prédire le mot suivant (ou le jeton) en fonction des mots précédents. Un échantillonnage des données approprié garantit que le modèle capture efficacement les motifs et les dépendances linguistiques.
|
||||
|
||||
> [!TIP]
|
||||
> L'objectif de cette deuxième phase est très simple : **Échantillonner les données d'entrée et les préparer pour la phase d'entraînement, généralement en séparant l'ensemble de données en phrases d'une longueur spécifique et en générant également la réponse attendue.**
|
||||
|
||||
### **Pourquoi l'Échantillonnage des Données est Important**
|
||||
|
||||
Les LLMs tels que GPT sont entraînés à générer ou prédire du texte en comprenant le contexte fourni par les mots précédents. Pour y parvenir, les données d'entraînement doivent être structurées de manière à ce que le modèle puisse apprendre la relation entre les séquences de mots et leurs mots suivants. Cette approche structurée permet au modèle de généraliser et de générer un texte cohérent et contextuellement pertinent.
|
||||
|
||||
### **Concepts Clés dans l'Échantillonnage des Données**
|
||||
|
||||
1. **Tokenisation :** Décomposer le texte en unités plus petites appelées jetons (par exemple, mots, sous-mots ou caractères).
|
||||
2. **Longueur de Séquence (max_length) :** Le nombre de jetons dans chaque séquence d'entrée.
|
||||
3. **Fenêtre Glissante :** Une méthode pour créer des séquences d'entrée qui se chevauchent en déplaçant une fenêtre sur le texte tokenisé.
|
||||
4. **Pas :** Le nombre de jetons que la fenêtre glissante avance pour créer la prochaine séquence.
|
||||
|
||||
### **Exemple Étape par Étape**
|
||||
|
||||
Passons en revue un exemple pour illustrer l'échantillonnage des données.
|
||||
|
||||
**Texte d'Exemple**
|
||||
```arduino
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
```
|
||||
**Tokenization**
|
||||
|
||||
Supposons que nous utilisions un **tokenizer de base** qui divise le texte en mots et en signes de ponctuation :
|
||||
```vbnet
|
||||
Tokens: ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit."]
|
||||
```
|
||||
**Paramètres**
|
||||
|
||||
- **Longueur maximale de séquence (max_length) :** 4 tokens
|
||||
- **Pas de fenêtre glissante :** 1 token
|
||||
|
||||
**Création de séquences d'entrée et de cible**
|
||||
|
||||
1. **Approche de fenêtre glissante :**
|
||||
- **Séquences d'entrée :** Chaque séquence d'entrée se compose de `max_length` tokens.
|
||||
- **Séquences cibles :** Chaque séquence cible se compose des tokens qui suivent immédiatement la séquence d'entrée correspondante.
|
||||
2. **Génération de séquences :**
|
||||
|
||||
<table><thead><tr><th width="177">Position de la fenêtre</th><th>Séquence d'entrée</th><th>Séquence cible</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. **Tableaux d'entrée et de cible résultants :**
|
||||
|
||||
- **Entrée :**
|
||||
|
||||
```python
|
||||
[
|
||||
["Lorem", "ipsum", "dolor", "sit"],
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
]
|
||||
```
|
||||
|
||||
- **Cible :**
|
||||
|
||||
```python
|
||||
[
|
||||
["ipsum", "dolor", "sit", "amet,"],
|
||||
["dolor", "sit", "amet,", "consectetur"],
|
||||
["sit", "amet,", "consectetur", "adipiscing"],
|
||||
["amet,", "consectetur", "adipiscing", "elit."],
|
||||
]
|
||||
```
|
||||
|
||||
**Représentation visuelle**
|
||||
|
||||
<table><thead><tr><th width="222">Position du 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>
|
||||
|
||||
**Fenêtre glissante avec un pas de 1 :**
|
||||
|
||||
- **Première fenêtre (positions 1-4) :** \["Lorem", "ipsum", "dolor", "sit"] → **Cible :** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Deuxième fenêtre (positions 2-5) :** \["ipsum", "dolor", "sit", "amet,"] → **Cible :** \["dolor", "sit", "amet,", "consectetur"]
|
||||
- **Troisième fenêtre (positions 3-6) :** \["dolor", "sit", "amet,", "consectetur"] → **Cible :** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Quatrième fenêtre (positions 4-7) :** \["sit", "amet,", "consectetur", "adipiscing"] → **Cible :** \["amet,", "consectetur", "adipiscing", "elit."]
|
||||
|
||||
**Comprendre le pas**
|
||||
|
||||
- **Pas de 1 :** La fenêtre avance d'un token à chaque fois, ce qui entraîne des séquences très chevauchantes. Cela peut conduire à un meilleur apprentissage des relations contextuelles mais peut augmenter le risque de surajustement puisque des points de données similaires sont répétés.
|
||||
- **Pas de 2 :** La fenêtre avance de deux tokens à chaque fois, réduisant le chevauchement. Cela diminue la redondance et la charge computationnelle mais pourrait manquer certaines nuances contextuelles.
|
||||
- **Pas égal à max_length :** La fenêtre avance de la taille entière de la fenêtre, entraînant des séquences non chevauchantes. Cela minimise la redondance des données mais peut limiter la capacité du modèle à apprendre des dépendances entre les séquences.
|
||||
|
||||
**Exemple avec un pas de 2 :**
|
||||
|
||||
En utilisant le même texte tokenisé et un `max_length` de 4 :
|
||||
|
||||
- **Première fenêtre (positions 1-4) :** \["Lorem", "ipsum", "dolor", "sit"] → **Cible :** \["ipsum", "dolor", "sit", "amet,"]
|
||||
- **Deuxième fenêtre (positions 3-6) :** \["dolor", "sit", "amet,", "consectetur"] → **Cible :** \["sit", "amet,", "consectetur", "adipiscing"]
|
||||
- **Troisième fenêtre (positions 5-8) :** \["amet,", "consectetur", "adipiscing", "elit."] → **Cible :** \["consectetur", "adipiscing", "elit.", "sed"] _(En supposant une continuation)_
|
||||
|
||||
## Exemple de code
|
||||
|
||||
Comprenons cela mieux à partir d'un exemple de code de [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]])
|
||||
]
|
||||
```
|
||||
## Références
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -127,7 +127,7 @@ Alors que les embeddings de tokens capturent le sens des tokens individuels, ils
|
||||
|
||||
### **Pourquoi les Embeddings Positionnels Sont Nécessaires :**
|
||||
|
||||
- **L'Ordre des Tokens Compte :** Dans les phrases, le sens dépend souvent de l'ordre des mots. Par exemple, "Le chat est assis sur le tapis" contre "Le tapis est assis sur le chat."
|
||||
- **L'Ordre des Tokens Est Important :** Dans les phrases, le sens dépend souvent de l'ordre des mots. Par exemple, "Le chat est assis sur le tapis" contre "Le tapis est assis sur le chat."
|
||||
- **Limitation des Embeddings :** Sans information positionnelle, le modèle traite les tokens comme un "sac de mots", ignorant leur séquence.
|
||||
|
||||
### **Types d'Embeddings Positionnels :**
|
||||
@ -161,7 +161,7 @@ Combined Embedding = Token Embedding + Positional Embedding
|
||||
|
||||
## Exemple de code
|
||||
|
||||
Suivant avec l'exemple de code de [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) :
|
||||
Suivant l'exemple de code de [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...
|
||||
|
||||
|
@ -10,7 +10,7 @@ Les mécanismes d'attention permettent aux réseaux de neurones de **se concentr
|
||||
|
||||
### Comprendre les Mécanismes d'Attention
|
||||
|
||||
Dans les modèles traditionnels de séquence à séquence utilisés pour la traduction linguistique, le modèle encode une séquence d'entrée en un vecteur de contexte de taille fixe. Cependant, cette approche a des difficultés avec les longues phrases car le vecteur de contexte de taille fixe peut ne pas capturer toutes les informations nécessaires. Les mécanismes d'attention abordent cette limitation en permettant au modèle de considérer tous les tokens d'entrée lors de la génération de chaque token de sortie.
|
||||
Dans les modèles traditionnels de séquence à séquence utilisés pour la traduction de langues, le modèle encode une séquence d'entrée en un vecteur de contexte de taille fixe. Cependant, cette approche a des difficultés avec les longues phrases car le vecteur de contexte de taille fixe peut ne pas capturer toutes les informations nécessaires. Les mécanismes d'attention abordent cette limitation en permettant au modèle de considérer tous les tokens d'entrée lors de la génération de chaque token de sortie.
|
||||
|
||||
#### Exemple : Traduction Automatique
|
||||
|
||||
@ -28,7 +28,7 @@ L'auto-attention, ou intra-attention, est un mécanisme où l'attention est appl
|
||||
|
||||
### Calcul des Poids d'Attention : Un Exemple Étape par Étape
|
||||
|
||||
Considérons la phrase **"Hello shiny sun!"** et représentons chaque mot par un embedding à 3 dimensions :
|
||||
Considérons la phrase **"Hello shiny sun!"** et représentons chaque mot avec un embedding de 3 dimensions :
|
||||
|
||||
- **Hello** : `[0.34, 0.22, 0.54]`
|
||||
- **shiny** : `[0.53, 0.34, 0.98]`
|
||||
@ -39,7 +39,7 @@ Notre objectif est de calculer le **vecteur de contexte** pour le mot **"shiny"*
|
||||
#### Étape 1 : Calculer les Scores d'Attention
|
||||
|
||||
> [!TIP]
|
||||
> Il suffit de multiplier chaque valeur de dimension de la requête par celle de chaque token et d'ajouter les résultats. Vous obtenez 1 valeur par paire de tokens.
|
||||
> Il suffit de multiplier chaque valeur de dimension de la requête par la valeur correspondante de chaque token et d'ajouter les résultats. Vous obtenez 1 valeur par paire de tokens.
|
||||
|
||||
Pour chaque mot de la phrase, calculez le **score d'attention** par rapport à "shiny" en calculant le produit scalaire de leurs embeddings.
|
||||
|
||||
@ -81,7 +81,7 @@ Calcul des poids d'attention :
|
||||
#### Étape 3 : Calculer le Vecteur de Contexte
|
||||
|
||||
> [!TIP]
|
||||
> Il suffit de prendre chaque poids d'attention et de le multiplier par les dimensions du token correspondant, puis d'additionner toutes les dimensions pour obtenir un seul vecteur (le vecteur de contexte)
|
||||
> Il suffit de prendre chaque poids d'attention et de le multiplier par les dimensions du token correspondant, puis de sommer toutes les dimensions pour obtenir un seul vecteur (le vecteur de contexte)
|
||||
|
||||
Le **vecteur de contexte** est calculé comme la somme pondérée des embeddings de tous les mots, en utilisant les poids d'attention.
|
||||
|
||||
@ -170,7 +170,7 @@ Pour éviter que les produits scalaires ne deviennent trop grands, mettez-les à
|
||||
> [!TIP]
|
||||
> Le score est divisé par la racine carrée des dimensions car les produits scalaires peuvent devenir très grands et cela aide à les réguler.
|
||||
|
||||
**Appliquer Softmax pour obtenir les poids d'attention :** Comme dans l'exemple initial, normalisez toutes les valeurs afin qu'elles s'additionnent à 1.
|
||||
**Appliquer Softmax pour obtenir les poids d'attention :** Comme dans l'exemple initial, normalisez toutes les valeurs pour qu'elles s'additionnent à 1.
|
||||
|
||||
<figure><img src="../../images/image (14).png" alt="" width="295"><figcaption></figcaption></figure>
|
||||
|
||||
@ -255,7 +255,7 @@ Pour **prévenir le surapprentissage**, nous pouvons appliquer **dropout** aux p
|
||||
dropout = nn.Dropout(p=0.5)
|
||||
attention_weights = dropout(attention_weights)
|
||||
```
|
||||
Un dropout régulier est d'environ 10-20%.
|
||||
Un abandon régulier est d'environ 10-20%.
|
||||
|
||||
### Code Example
|
||||
|
||||
@ -327,7 +327,7 @@ print("context_vecs.shape:", context_vecs.shape)
|
||||
|
||||
### Exemple de code
|
||||
|
||||
Il serait possible de réutiliser le code précédent et d'ajouter simplement un wrapper qui le lance plusieurs fois, mais voici une version plus optimisée de [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) qui traite toutes les têtes en même temps (réduisant le nombre de boucles for coûteuses). Comme vous pouvez le voir dans le code, les dimensions de chaque token sont divisées en différentes dimensions selon le nombre de têtes. De cette façon, si un token a 8 dimensions et que nous voulons utiliser 3 têtes, les dimensions seront divisées en 2 tableaux de 4 dimensions et chaque tête utilisera l'un d'eux :
|
||||
Il pourrait être possible de réutiliser le code précédent et d'ajouter simplement un wrapper qui le lance plusieurs fois, mais voici une version plus optimisée de [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) qui traite toutes les têtes en même temps (réduisant le nombre de boucles for coûteuses). Comme vous pouvez le voir dans le code, les dimensions de chaque token sont divisées en différentes dimensions selon le nombre de têtes. De cette façon, si un token a 8 dimensions et que nous voulons utiliser 3 têtes, les dimensions seront divisées en 2 tableaux de 4 dimensions et chaque tête utilisera l'un d'eux :
|
||||
```python
|
||||
class MultiHeadAttention(nn.Module):
|
||||
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
|
||||
|
@ -208,7 +208,7 @@ torch.sqrt(torch.tensor(2.0 / torch.pi)) *
|
||||
(x + 0.044715 * torch.pow(x, 3))
|
||||
))
|
||||
```
|
||||
#### **But et Fonctionnalité**
|
||||
#### **Objectif et Fonctionnalité**
|
||||
|
||||
- **GELU (Unité Linéaire d'Erreur Gaussienne) :** Une fonction d'activation qui introduit de la non-linéarité dans le modèle.
|
||||
- **Activation Douce :** Contrairement à ReLU, qui annule les entrées négatives, GELU mappe en douceur les entrées aux sorties, permettant des valeurs petites et non nulles pour les entrées négatives.
|
||||
@ -263,7 +263,7 @@ Cela a déjà été expliqué dans une section précédente.
|
||||
- **Requêtes, Clés, Valeurs :** Projections linéaires de l'entrée, utilisées pour calculer les scores d'attention.
|
||||
- **Têtes :** Plusieurs mécanismes d'attention fonctionnant en parallèle (`num_heads`), chacun avec une dimension réduite (`head_dim`).
|
||||
- **Scores d'Attention :** Calculés comme le produit scalaire des requêtes et des clés, mis à l'échelle et masqués.
|
||||
- **Masquage :** Un masque causal est appliqué pour empêcher le modèle de prêter attention aux tokens futurs (important pour les modèles autorégressifs comme GPT).
|
||||
- **Masquage :** Un masque causal est appliqué pour empêcher le modèle d'attendre des tokens futurs (important pour les modèles autorégressifs comme GPT).
|
||||
- **Poids d'Attention :** Softmax des scores d'attention masqués et mis à l'échelle.
|
||||
- **Vecteur de Contexte :** Somme pondérée des valeurs, selon les poids d'attention.
|
||||
- **Projection de Sortie :** Couche linéaire pour combiner les sorties de toutes les têtes.
|
||||
@ -289,17 +289,17 @@ 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
|
||||
```
|
||||
#### **Objectif et Fonctionnalité**
|
||||
#### **But et Fonctionnalité**
|
||||
|
||||
- **Layer Normalization :** Une technique utilisée pour normaliser les entrées à travers les caractéristiques (dimensions d'embedding) pour chaque exemple individuel dans un lot.
|
||||
- **Normalisation de Couche :** Une technique utilisée pour normaliser les entrées à travers les caractéristiques (dimensions d'embedding) pour chaque exemple individuel dans un lot.
|
||||
- **Composants :**
|
||||
- **`eps` :** Une petite constante (`1e-5`) ajoutée à la variance pour éviter la division par zéro lors de la normalisation.
|
||||
- **`scale` et `shift` :** Paramètres apprenables (`nn.Parameter`) qui permettent au modèle de mettre à l'échelle et de décaler la sortie normalisée. Ils sont initialisés respectivement à un et à zéro.
|
||||
- **`scale` et `shift` :** Paramètres apprenables (`nn.Parameter`) qui permettent au modèle de mettre à l'échelle et de décaler la sortie normalisée. Ils sont initialisés à un et zéro, respectivement.
|
||||
- **Processus de Normalisation :**
|
||||
- **Calculer la Moyenne (`mean`) :** Calcule la moyenne de l'entrée `x` à travers la dimension d'embedding (`dim=-1`), en gardant la dimension pour la diffusion (`keepdim=True`).
|
||||
- **Calculer la Variance (`var`) :** Calcule la variance de `x` à travers la dimension d'embedding, en gardant également la dimension. Le paramètre `unbiased=False` garantit que la variance est calculée en utilisant l'estimateur biaisé (division par `N` au lieu de `N-1`), ce qui est approprié lors de la normalisation sur les caractéristiques plutôt que sur les échantillons.
|
||||
- **Normaliser (`norm_x`) :** Soustrait la moyenne de `x` et divise par la racine carrée de la variance plus `eps`.
|
||||
- **Mettre à l'échelle et Décaler :** Applique les paramètres apprenables `scale` et `shift` à la sortie normalisée.
|
||||
- **Mettre à l'Échelle et Décaler :** Applique les paramètres apprenables `scale` et `shift` à la sortie normalisée.
|
||||
|
||||
> [!TIP]
|
||||
> L'objectif est d'assurer une moyenne de 0 avec une variance de 1 à travers toutes les dimensions du même token. Le but de cela est de **stabiliser l'entraînement des réseaux de neurones profonds** en réduisant le changement de covariables internes, qui fait référence au changement dans la distribution des activations du réseau en raison de la mise à jour des paramètres pendant l'entraînement.
|
||||
@ -469,7 +469,7 @@ print(f"Total number of parameters: {total_params:,}")
|
||||
```
|
||||
### **Calcul de l'étape par étape**
|
||||
|
||||
#### **1. Couches d'incorporation : Incorporation de jetons et incorporation de position**
|
||||
#### **1. Couches d'incorporation : Incorporation de jetons et Incorporation de position**
|
||||
|
||||
- **Couche :** `nn.Embedding(vocab_size, emb_dim)`
|
||||
- **Paramètres :** `vocab_size * emb_dim`
|
||||
@ -486,11 +486,11 @@ position_embedding_params = 1024 * 768 = 786,432
|
||||
embedding_params = token_embedding_params + position_embedding_params
|
||||
embedding_params = 38,597,376 + 786,432 = 39,383,808
|
||||
```
|
||||
#### **2. Blocs de Transformateur**
|
||||
#### **2. Blocs Transformer**
|
||||
|
||||
Il y a 12 blocs de transformateur, donc nous allons calculer les paramètres pour un bloc puis multiplier par 12.
|
||||
Il y a 12 blocs transformer, donc nous allons calculer les paramètres pour un bloc puis multiplier par 12.
|
||||
|
||||
**Paramètres par Bloc de Transformateur**
|
||||
**Paramètres par Bloc Transformer**
|
||||
|
||||
**a. Attention Multi-Tête**
|
||||
|
||||
@ -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. Total des Paramètres par Bloc de Transformateur**
|
||||
**d. Total des Paramètres par Bloc 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
|
||||
@ -583,7 +583,7 @@ total_transformer_blocks_params = 7,085,568 * 12 = 85,026,816
|
||||
```python
|
||||
pythonCopy codefinal_layer_norm_params = 2 * 768 = 1,536
|
||||
```
|
||||
**b. Couche de Projection de Sortie (`out_head`)**
|
||||
**b. Couche de projection de sortie (`out_head`)**
|
||||
|
||||
- **Couche :** `nn.Linear(emb_dim, vocab_size, bias=False)`
|
||||
- **Paramètres :** `emb_dim * vocab_size`
|
||||
|
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. Pré-entraînement et chargement des modèles
|
||||
|
||||
## Génération de texte
|
||||
|
||||
Pour entraîner un modèle, nous aurons besoin que ce modèle soit capable de générer de nouveaux tokens. Ensuite, nous comparerons les tokens générés avec ceux attendus afin d'entraîner le modèle à **apprendre les tokens qu'il doit générer**.
|
||||
|
||||
Comme dans les exemples précédents où nous avons déjà prédit certains tokens, il est possible de réutiliser cette fonction à cette fin.
|
||||
|
||||
> [!TIP]
|
||||
> L'objectif de cette sixième phase est très simple : **Entraîner le modèle depuis le début**. Pour cela, l'architecture LLM précédente sera utilisée avec quelques boucles parcourant les ensembles de données en utilisant les fonctions de perte et l'optimiseur définis pour entraîner tous les paramètres du modèle.
|
||||
|
||||
## Évaluation de texte
|
||||
|
||||
Pour effectuer un entraînement correct, il est nécessaire de mesurer les prédictions obtenues pour le token attendu. L'objectif de l'entraînement est de maximiser la probabilité du token correct, ce qui implique d'augmenter sa probabilité par rapport aux autres tokens.
|
||||
|
||||
Pour maximiser la probabilité du token correct, les poids du modèle doivent être modifiés afin que cette probabilité soit maximisée. Les mises à jour des poids se font via **backpropagation**. Cela nécessite une **fonction de perte à maximiser**. Dans ce cas, la fonction sera la **différence entre la prédiction effectuée et celle désirée**.
|
||||
|
||||
Cependant, au lieu de travailler avec les prédictions brutes, il travaillera avec un logarithme de base n. Ainsi, si la prédiction actuelle du token attendu était 7.4541e-05, le logarithme naturel (base *e*) de **7.4541e-05** est d'environ **-9.5042**.\
|
||||
Ensuite, pour chaque entrée avec une longueur de contexte de 5 tokens par exemple, le modèle devra prédire 5 tokens, les 4 premiers tokens étant les derniers de l'entrée et le cinquième étant celui prédit. Par conséquent, pour chaque entrée, nous aurons 5 prédictions dans ce cas (même si les 4 premiers étaient dans l'entrée, le modèle ne le sait pas) avec 5 tokens attendus et donc 5 probabilités à maximiser.
|
||||
|
||||
Par conséquent, après avoir effectué le logarithme naturel sur chaque prédiction, la **moyenne** est calculée, le **symbole moins retiré** (ceci est appelé _cross entropy loss_) et c'est le **nombre à réduire aussi près de 0 que possible** car le logarithme naturel de 1 est 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>
|
||||
|
||||
Une autre façon de mesurer la qualité du modèle est appelée perplexité. **Perplexité** est une métrique utilisée pour évaluer à quel point un modèle de probabilité prédit un échantillon. Dans la modélisation du langage, elle représente **l'incertitude du modèle** lors de la prédiction du prochain token dans une séquence.\
|
||||
Par exemple, une valeur de perplexité de 48725 signifie que lorsqu'il doit prédire un token, il n'est pas sûr de savoir lequel parmi 48 725 tokens dans le vocabulaire est le bon.
|
||||
|
||||
## Exemple de pré-entraînement
|
||||
|
||||
Voici le code initial proposé dans [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) parfois légèrement modifié
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Code précédent utilisé ici mais déjà expliqué dans les sections précédentes</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"
|
||||
)
|
||||
```
|
||||
### Fonctions pour transformer le texte <--> ids
|
||||
|
||||
Ce sont quelques fonctions simples qui peuvent être utilisées pour transformer des textes du vocabulaire en ids et vice versa. Cela est nécessaire au début du traitement du texte et à la fin des prédictions :
|
||||
```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())
|
||||
```
|
||||
### Générer des fonctions de texte
|
||||
|
||||
Dans une section précédente, une fonction qui obtenait simplement le **token le plus probable** après avoir obtenu les logits. Cependant, cela signifie que pour chaque entrée, la même sortie sera toujours générée, ce qui la rend très déterministe.
|
||||
|
||||
La fonction `generate_text` suivante appliquera les concepts de `top-k`, `temperature` et `multinomial`.
|
||||
|
||||
- Le **`top-k`** signifie que nous allons commencer par réduire à `-inf` toutes les probabilités de tous les tokens sauf pour les k tokens les plus élevés. Donc, si k=3, avant de prendre une décision, seuls les 3 tokens les plus probables auront une probabilité différente de `-inf`.
|
||||
- La **`temperature`** signifie que chaque probabilité sera divisée par la valeur de température. Une valeur de `0.1` améliorera la probabilité la plus élevée par rapport à la plus basse, tandis qu'une température de `5`, par exemple, la rendra plus plate. Cela aide à améliorer la variation des réponses que nous aimerions que le LLM ait.
|
||||
- Après avoir appliqué la température, une fonction **`softmax`** est appliquée à nouveau pour faire en sorte que tous les tokens restants aient une probabilité totale de 1.
|
||||
- Enfin, au lieu de choisir le token avec la plus grande probabilité, la fonction **`multinomial`** est appliquée pour **prédire le prochain token selon les probabilités finales**. Donc, si le token 1 avait 70% de probabilités, le token 2 20% et le token 3 10%, 70% du temps le token 1 sera sélectionné, 20% du temps ce sera le token 2 et 10% du temps ce sera le 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]
|
||||
> Il existe une alternative courante à `top-k` appelée [**`top-p`**](https://en.wikipedia.org/wiki/Top-p_sampling), également connue sous le nom d'échantillonnage par noyau, qui au lieu de prendre k échantillons avec la plus grande probabilité, **organise** tout le **vocabulaire** résultant par probabilités et **somme** celles-ci de la plus haute probabilité à la plus basse jusqu'à ce qu'un **seuil soit atteint**.
|
||||
>
|
||||
> Ensuite, **seules ces mots** du vocabulaire seront considérés en fonction de leurs probabilités relatives.
|
||||
>
|
||||
> Cela permet de ne pas avoir besoin de sélectionner un nombre d'échantillons `k`, car le k optimal peut être différent dans chaque cas, mais **seulement un seuil**.
|
||||
>
|
||||
> _Notez que cette amélioration n'est pas incluse dans le code précédent._
|
||||
|
||||
> [!TIP]
|
||||
> Une autre façon d'améliorer le texte généré est d'utiliser **Beam search** au lieu de la recherche gloutonne utilisée dans cet exemple.\
|
||||
> Contrairement à la recherche gloutonne, qui sélectionne le mot suivant le plus probable à chaque étape et construit une seule séquence, **la recherche par faisceau garde une trace des 𝑘 k séquences partielles les mieux notées** (appelées "faisceaux") à chaque étape. En explorant plusieurs possibilités simultanément, elle équilibre efficacité et qualité, augmentant les chances de **trouver une meilleure séquence globale** qui pourrait être manquée par l'approche gloutonne en raison de choix précoces et sous-optimaux.
|
||||
>
|
||||
> _Notez que cette amélioration n'est pas incluse dans le code précédent._
|
||||
|
||||
### Fonctions de perte
|
||||
|
||||
La fonction **`calc_loss_batch`** calcule l'entropie croisée d'une prédiction d'un seul lot.\
|
||||
La **`calc_loss_loader`** obtient l'entropie croisée de tous les lots et calcule l'**entropie croisée moyenne**.
|
||||
```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]
|
||||
> **Le clipping de gradient** est une technique utilisée pour améliorer **la stabilité de l'entraînement** dans de grands réseaux de neurones en fixant un **seuil maximum** pour les magnitudes des gradients. Lorsque les gradients dépassent ce `max_norm` prédéfini, ils sont réduits proportionnellement pour garantir que les mises à jour des paramètres du modèle restent dans une plage gérable, évitant des problèmes comme les gradients explosifs et assurant un entraînement plus contrôlé et stable.
|
||||
>
|
||||
> _Notez que cette amélioration n'est pas incluse dans le code précédent._
|
||||
>
|
||||
> Consultez l'exemple suivant :
|
||||
|
||||
<figure><img src="../../images/image (6) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### Chargement des données
|
||||
|
||||
Les fonctions `create_dataloader_v1` et `create_dataloader_v1` ont déjà été discutées dans une section précédente.
|
||||
|
||||
À partir de là, notez comment il est défini que 90 % du texte sera utilisé pour l'entraînement tandis que 10 % sera utilisé pour la validation et que les deux ensembles sont stockés dans 2 chargeurs de données différents.\
|
||||
Notez que parfois, une partie de l'ensemble de données est également laissée pour un ensemble de test afin d'évaluer mieux la performance du modèle.
|
||||
|
||||
Les deux chargeurs de données utilisent la même taille de lot, longueur maximale, stride et nombre de travailleurs (0 dans ce cas).\
|
||||
Les principales différences résident dans les données utilisées par chacun, et le validateur ne supprime pas le dernier ni ne mélange les données car cela n'est pas nécessaire à des fins de validation.
|
||||
|
||||
De plus, le fait que **le stride soit aussi grand que la longueur du contexte** signifie qu'il n'y aura pas de chevauchement entre les contextes utilisés pour entraîner les données (réduit le surapprentissage mais aussi l'ensemble de données d'entraînement).
|
||||
|
||||
En outre, notez que la taille du lot dans ce cas est de 2 pour diviser les données en 2 lots, l'objectif principal étant de permettre un traitement parallèle et de réduire la consommation par lot.
|
||||
```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
|
||||
)
|
||||
```
|
||||
## Vérifications de cohérence
|
||||
|
||||
L'objectif est de vérifier qu'il y a suffisamment de tokens pour l'entraînement, que les formes sont celles attendues et d'obtenir des informations sur le nombre de tokens utilisés pour l'entraînement et pour la validation :
|
||||
```python
|
||||
# Sanity checks
|
||||
if total_tokens * (train_ratio) < GPT_CONFIG_124M["context_length"]:
|
||||
print("Not enough tokens for the training loader. "
|
||||
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
|
||||
"increase the `training_ratio`")
|
||||
|
||||
if total_tokens * (1-train_ratio) < GPT_CONFIG_124M["context_length"]:
|
||||
print("Not enough tokens for the validation loader. "
|
||||
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
|
||||
"decrease the `training_ratio`")
|
||||
|
||||
print("Train loader:")
|
||||
for x, y in train_loader:
|
||||
print(x.shape, y.shape)
|
||||
|
||||
print("\nValidation loader:")
|
||||
for x, y in val_loader:
|
||||
print(x.shape, y.shape)
|
||||
|
||||
train_tokens = 0
|
||||
for input_batch, target_batch in train_loader:
|
||||
train_tokens += input_batch.numel()
|
||||
|
||||
val_tokens = 0
|
||||
for input_batch, target_batch in val_loader:
|
||||
val_tokens += input_batch.numel()
|
||||
|
||||
print("Training tokens:", train_tokens)
|
||||
print("Validation tokens:", val_tokens)
|
||||
print("All tokens:", train_tokens + val_tokens)
|
||||
```
|
||||
### Sélectionner un appareil pour l'entraînement et les pré-calculs
|
||||
|
||||
Le code suivant sélectionne simplement l'appareil à utiliser et calcule une perte d'entraînement et une perte de validation (sans avoir encore entraîné quoi que ce soit) comme point de départ.
|
||||
```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)
|
||||
```
|
||||
### Fonctions d'entraînement
|
||||
|
||||
La fonction `generate_and_print_sample` va simplement obtenir un contexte et générer quelques tokens afin d'avoir une idée de la performance du modèle à ce moment-là. Cela est appelé par `train_model_simple` à chaque étape.
|
||||
|
||||
La fonction `evaluate_model` est appelée aussi souvent que l'indique la fonction d'entraînement et elle est utilisée pour mesurer la perte d'entraînement et la perte de validation à ce moment dans l'entraînement du modèle.
|
||||
|
||||
Ensuite, la grande fonction `train_model_simple` est celle qui entraîne réellement le modèle. Elle attend :
|
||||
|
||||
- Le chargeur de données d'entraînement (avec les données déjà séparées et préparées pour l'entraînement)
|
||||
- Le chargeur de validation
|
||||
- L'**optimiseur** à utiliser pendant l'entraînement : C'est la fonction qui utilisera les gradients et mettra à jour les paramètres pour réduire la perte. Dans ce cas, comme vous le verrez, `AdamW` est utilisé, mais il en existe beaucoup d'autres.
|
||||
- `optimizer.zero_grad()` est appelé pour réinitialiser les gradients à chaque tour afin de ne pas les accumuler.
|
||||
- Le paramètre **`lr`** est le **taux d'apprentissage** qui détermine la **taille des étapes** prises pendant le processus d'optimisation lors de la mise à jour des paramètres du modèle. Un **taux d'apprentissage** **plus petit** signifie que l'optimiseur **effectue des mises à jour plus petites** des poids, ce qui peut conduire à une convergence plus **précise** mais peut **ralentir** l'entraînement. Un **taux d'apprentissage** **plus grand** peut accélérer l'entraînement mais **risque de dépasser** le minimum de la fonction de perte (**sauter par-dessus** le point où la fonction de perte est minimisée).
|
||||
- La **décroissance de poids** modifie l'étape de **calcul de la perte** en ajoutant un terme supplémentaire qui pénalise les poids importants. Cela encourage l'optimiseur à trouver des solutions avec des poids plus petits, équilibrant entre un bon ajustement des données et le maintien d'un modèle simple, prévenant le surapprentissage dans les modèles d'apprentissage automatique en décourageant le modèle d'accorder trop d'importance à une seule caractéristique.
|
||||
- Les optimisateurs traditionnels comme SGD avec régularisation L2 couplent la décroissance de poids avec le gradient de la fonction de perte. Cependant, **AdamW** (une variante de l'optimiseur Adam) découple la décroissance de poids de la mise à jour du gradient, conduisant à une régularisation plus efficace.
|
||||
- Le dispositif à utiliser pour l'entraînement
|
||||
- Le nombre d'époques : Nombre de fois à passer sur les données d'entraînement
|
||||
- La fréquence d'évaluation : La fréquence d'appel de `evaluate_model`
|
||||
- L'itération d'évaluation : Le nombre de lots à utiliser lors de l'évaluation de l'état actuel du modèle lors de l'appel de `generate_and_print_sample`
|
||||
- Le contexte de départ : Quelle phrase de départ utiliser lors de l'appel de `generate_and_print_sample`
|
||||
- Le 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]
|
||||
> Pour améliorer le taux d'apprentissage, il existe quelques techniques pertinentes appelées **linear warmup** et **cosine decay.**
|
||||
>
|
||||
> **Linear warmup** consiste à définir un taux d'apprentissage initial et un maximum, puis à le mettre à jour de manière cohérente après chaque époque. Cela est dû au fait que commencer l'entraînement avec des mises à jour de poids plus petites diminue le risque que le modèle rencontre de grandes mises à jour déstabilisantes pendant sa phase d'entraînement.\
|
||||
> **Cosine decay** est une technique qui **réduit progressivement le taux d'apprentissage** suivant une courbe demi-cosinus **après la phase de warmup**, ralentissant les mises à jour de poids pour **minimiser le risque de dépasser** les minima de perte et garantir la stabilité de l'entraînement dans les phases ultérieures.
|
||||
>
|
||||
> _Notez que ces améliorations ne sont pas incluses dans le code précédent._
|
||||
|
||||
### Start training
|
||||
```python
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
torch.manual_seed(123)
|
||||
model = GPTModel(GPT_CONFIG_124M)
|
||||
model.to(device)
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0004, weight_decay=0.1)
|
||||
|
||||
num_epochs = 10
|
||||
train_losses, val_losses, tokens_seen = train_model_simple(
|
||||
model, train_loader, val_loader, optimizer, device,
|
||||
num_epochs=num_epochs, eval_freq=5, eval_iter=5,
|
||||
start_context="Every effort moves you", tokenizer=tokenizer
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
execution_time_minutes = (end_time - start_time) / 60
|
||||
print(f"Training completed in {execution_time_minutes:.2f} minutes.")
|
||||
```
|
||||
### Imprimer l'évolution de l'entraînement
|
||||
|
||||
Avec la fonction suivante, il est possible d'imprimer l'évolution du modèle pendant qu'il était en cours d'entraînement.
|
||||
```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)
|
||||
```
|
||||
### Sauvegarder le modèle
|
||||
|
||||
Il est possible de sauvegarder le modèle + l'optimiseur si vous souhaitez continuer l'entraînement plus tard :
|
||||
```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
|
||||
```
|
||||
Ou juste le modèle si vous prévoyez de l'utiliser uniquement :
|
||||
```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
|
||||
```
|
||||
## Chargement des poids GPT2
|
||||
|
||||
Il y a 2 scripts rapides pour charger les poids GPT2 localement. Pour les deux, vous pouvez cloner le dépôt [https://github.com/rasbt/LLMs-from-scratch](https://github.com/rasbt/LLMs-from-scratch) localement, puis :
|
||||
|
||||
- Le 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) téléchargera tous les poids et transformera les formats d'OpenAI vers ceux attendus par notre LLM. Le script est également préparé avec la configuration nécessaire et avec le prompt : "Every effort moves you"
|
||||
- Le 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) vous permet de charger n'importe lequel des poids GPT2 localement (il suffit de changer la variable `CHOOSE_MODEL`) et de prédire du texte à partir de certains prompts.
|
||||
|
||||
## Références
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -0,0 +1,110 @@
|
||||
# 7.1. Ajustement fin pour la classification
|
||||
|
||||
## Qu'est-ce que c'est
|
||||
|
||||
L'ajustement fin est le processus qui consiste à prendre un **modèle pré-entraîné** ayant appris des **schémas linguistiques généraux** à partir de vastes quantités de données et à **l'adapter** pour effectuer une **tâche spécifique** ou comprendre un langage spécifique à un domaine. Cela se fait en poursuivant l'entraînement du modèle sur un ensemble de données plus petit et spécifique à la tâche, lui permettant d'ajuster ses paramètres pour mieux s'adapter aux nuances des nouvelles données tout en tirant parti des vastes connaissances qu'il a déjà acquises. L'ajustement fin permet au modèle de fournir des résultats plus précis et pertinents dans des applications spécialisées sans avoir besoin d'entraîner un nouveau modèle depuis le début.
|
||||
|
||||
> [!TIP]
|
||||
> Comme pré-entraîner un LLM qui "comprend" le texte est assez coûteux, il est généralement plus facile et moins cher d'ajuster des modèles pré-entraînés open source pour effectuer une tâche spécifique que nous souhaitons qu'il réalise.
|
||||
|
||||
> [!TIP]
|
||||
> L'objectif de cette section est de montrer comment ajuster un modèle déjà pré-entraîné afin qu'au lieu de générer un nouveau texte, le LLM sélectionne et donne les **probabilités que le texte donné soit catégorisé dans chacune des catégories données** (comme si un texte est un spam ou non).
|
||||
|
||||
## Préparation de l'ensemble de données
|
||||
|
||||
### Taille de l'ensemble de données
|
||||
|
||||
Bien sûr, pour ajuster un modèle, vous avez besoin de données structurées à utiliser pour spécialiser votre LLM. Dans l'exemple proposé dans [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 est ajusté pour détecter si un email est un spam ou non en utilisant les données de [https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip](https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip)_._
|
||||
|
||||
Cet ensemble de données contient beaucoup plus d'exemples de "non spam" que de "spam", donc le livre suggère de **n'utiliser que autant d'exemples de "non spam" que de "spam"** (en supprimant donc tous les exemples supplémentaires des données d'entraînement). Dans ce cas, il y avait 747 exemples de chaque.
|
||||
|
||||
Ensuite, **70%** de l'ensemble de données est utilisé pour **l'entraînement**, **10%** pour **la validation** et **20%** pour **les tests**.
|
||||
|
||||
- L'**ensemble de validation** est utilisé pendant la phase d'entraînement pour ajuster les **hyperparamètres** du modèle et prendre des décisions concernant l'architecture du modèle, aidant ainsi à prévenir le surajustement en fournissant des retours sur la performance du modèle sur des données non vues. Il permet des améliorations itératives sans biaiser l'évaluation finale.
|
||||
- Cela signifie que bien que les données incluses dans cet ensemble de données ne soient pas utilisées directement pour l'entraînement, elles sont utilisées pour ajuster les meilleurs **hyperparamètres**, donc cet ensemble ne peut pas être utilisé pour évaluer la performance du modèle comme le fait l'ensemble de test.
|
||||
- En revanche, l'**ensemble de test** est utilisé **uniquement après** que le modèle a été entièrement entraîné et que tous les ajustements sont terminés ; il fournit une évaluation impartiale de la capacité du modèle à généraliser sur de nouvelles données non vues. Cette évaluation finale sur l'ensemble de test donne une indication réaliste de la façon dont le modèle est censé performer dans des applications réelles.
|
||||
|
||||
### Longueur des entrées
|
||||
|
||||
Comme l'exemple d'entraînement attend des entrées (texte des emails dans ce cas) de la même longueur, il a été décidé de rendre chaque entrée aussi grande que la plus grande en ajoutant les ids de `<|endoftext|>` comme remplissage.
|
||||
|
||||
### Initialiser le modèle
|
||||
|
||||
En utilisant les poids pré-entraînés open source, initialisez le modèle pour l'entraînement. Nous avons déjà fait cela auparavant et en suivant les instructions de [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), vous pouvez facilement le faire.
|
||||
|
||||
## Tête de classification
|
||||
|
||||
Dans cet exemple spécifique (prédire si un texte est un spam ou non), nous ne sommes pas intéressés à ajuster selon le vocabulaire complet de GPT2 mais nous voulons seulement que le nouveau modèle indique si l'email est un spam (1) ou non (0). Par conséquent, nous allons **modifier la couche finale qui** donne les probabilités par token du vocabulaire pour une qui ne donne que les probabilités d'être un spam ou non (comme un vocabulaire de 2 mots).
|
||||
```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
|
||||
)
|
||||
```
|
||||
## Paramètres à ajuster
|
||||
|
||||
Afin d'ajuster rapidement, il est plus facile de ne pas ajuster tous les paramètres mais seulement certains derniers. Cela est dû au fait qu'il est connu que les couches inférieures capturent généralement des structures linguistiques de base et des sémantiques applicables. Donc, juste **ajuster les dernières couches est généralement suffisant et plus rapide**.
|
||||
```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
|
||||
```
|
||||
## Entrées à utiliser pour l'entraînement
|
||||
|
||||
Dans les sections précédentes, le LLM a été entraîné en réduisant la perte de chaque token prédit, même si presque tous les tokens prédits étaient dans la phrase d'entrée (seulement 1 à la fin était vraiment prédit) afin que le modèle comprenne mieux la langue.
|
||||
|
||||
Dans ce cas, nous ne nous soucions que de la capacité du modèle à prédire si le modèle est un spam ou non, donc nous ne nous soucions que du dernier token prédit. Par conséquent, il est nécessaire de modifier nos précédentes fonctions de perte d'entraînement pour ne prendre en compte que ce token.
|
||||
|
||||
Ceci est implémenté dans [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) comme :
|
||||
```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
|
||||
```
|
||||
Notez que pour chaque lot, nous ne sommes intéressés que par les **logits du dernier token prédit**.
|
||||
|
||||
## Code complet de classification fine-tune GPT2
|
||||
|
||||
Vous pouvez trouver tout le code pour fine-tuner GPT2 en tant que classificateur de spam dans [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)
|
||||
|
||||
## Références
|
||||
|
||||
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|
@ -5,7 +5,7 @@
|
||||
|
||||
## Ensemble de données
|
||||
|
||||
Pour ajuster un LLM afin de suivre des instructions, il est nécessaire d'avoir un ensemble de données avec des instructions et des réponses pour ajuster le LLM. Il existe différents formats pour entraîner un LLM à suivre des instructions, par exemple :
|
||||
Pour ajuster un LLM afin de suivre des instructions, il est nécessaire d'avoir un ensemble de données avec des instructions et des réponses pour affiner le LLM. Il existe différents formats pour entraîner un LLM à suivre des instructions, par exemple :
|
||||
|
||||
- L'exemple de style de prompt Apply Alpaca :
|
||||
```csharp
|
||||
@ -27,7 +27,7 @@ Can you explain what gravity is in simple terms?
|
||||
<|Assistant|>
|
||||
Absolutely! Gravity is a force that pulls objects toward each other.
|
||||
```
|
||||
Former un LLM avec ce genre de jeux de données au lieu de simplement du texte brut aide le LLM à comprendre qu'il doit donner des réponses spécifiques aux questions qu'il reçoit.
|
||||
Former un LLM avec ce type de jeux de données au lieu de simplement du texte brut aide le LLM à comprendre qu'il doit donner des réponses spécifiques aux questions qu'il reçoit.
|
||||
|
||||
Par conséquent, l'une des premières choses à faire avec un ensemble de données contenant des demandes et des réponses est de modéliser ces données dans le format de prompt souhaité, comme :
|
||||
```python
|
||||
@ -53,15 +53,15 @@ Puis, comme toujours, il est nécessaire de séparer le jeu de données en ensem
|
||||
|
||||
## Batching & Data Loaders
|
||||
|
||||
Ensuite, il est nécessaire de regrouper toutes les entrées et sorties attendues pour l'entraînement. Pour cela, il faut :
|
||||
Ensuite, il est nécessaire de regrouper toutes les entrées et les sorties attendues pour l'entraînement. Pour cela, il faut :
|
||||
|
||||
- Tokeniser les textes
|
||||
- Remplir tous les échantillons à la même longueur (généralement, la longueur sera aussi grande que la longueur de contexte utilisée pour pré-entraîner le LLM)
|
||||
- Créer les tokens attendus en décalant l'entrée de 1 dans une fonction de collate personnalisée
|
||||
- Créer les tokens attendus en décalant l'entrée de 1 dans une fonction de collage personnalisée
|
||||
- Remplacer certains tokens de remplissage par -100 pour les exclure de la perte d'entraînement : Après le premier token `endoftext`, substituer tous les autres tokens `endoftext` par -100 (car utiliser `cross_entropy(...,ignore_index=-100)` signifie qu'il ignorera les cibles avec -100)
|
||||
- \[Optionnel\] Masquer en utilisant -100 également tous les tokens appartenant à la question afin que le LLM apprenne uniquement à générer la réponse. Dans le style Apply Alpaca, cela signifiera masquer tout jusqu'à `### Response:`
|
||||
|
||||
Avec cela créé, il est temps de créer les chargeurs de données pour chaque jeu de données (entraînement, validation et test).
|
||||
Avec cela créé, il est temps de créer les chargeurs de données pour chaque ensemble de données (entraînement, validation et test).
|
||||
|
||||
## Load pre-trained LLM & Fine tune & Loss Checking
|
||||
|
||||
@ -72,18 +72,18 @@ Rappelez-vous que le surapprentissage se produit lorsque la perte d'entraînemen
|
||||
|
||||
## Response Quality
|
||||
|
||||
Comme il ne s'agit pas d'un fine-tune de classification où il est possible de faire davantage confiance aux variations de perte, il est également important de vérifier la qualité des réponses dans le jeu de test. Par conséquent, il est recommandé de rassembler les réponses générées de tous les jeux de test et **de vérifier leur qualité manuellement** pour voir s'il y a des réponses incorrectes (notez qu'il est possible pour le LLM de créer correctement le format et la syntaxe de la phrase de réponse mais de donner une réponse complètement incorrecte. La variation de perte ne reflétera pas ce comportement).\
|
||||
Comme il ne s'agit pas d'un peaufiner de classification où il est possible de faire davantage confiance aux variations de perte, il est également important de vérifier la qualité des réponses dans l'ensemble de test. Par conséquent, il est recommandé de rassembler les réponses générées de tous les ensembles de test et **de vérifier leur qualité manuellement** pour voir s'il y a des réponses incorrectes (notez qu'il est possible pour le LLM de créer correctement le format et la syntaxe de la phrase de réponse mais de donner une réponse complètement incorrecte. La variation de perte ne reflétera pas ce comportement).\
|
||||
Notez qu'il est également possible d'effectuer cette révision en passant les réponses générées et les réponses attendues à **d'autres LLM et leur demander d'évaluer les réponses**.
|
||||
|
||||
Autre test à effectuer pour vérifier la qualité des réponses :
|
||||
|
||||
1. **Measuring Massive Multitask Language Understanding (**[**MMLU**](https://arxiv.org/abs/2009.03300)**):** MMLU évalue les connaissances et les capacités de résolution de problèmes d'un modèle à travers 57 sujets, y compris les sciences humaines, les sciences, et plus encore. Il utilise des questions à choix multiples pour évaluer la compréhension à divers niveaux de difficulté, de l'élémentaire au professionnel avancé.
|
||||
2. [**LMSYS Chatbot Arena**](https://arena.lmsys.org): Cette plateforme permet aux utilisateurs de comparer les réponses de différents chatbots côte à côte. Les utilisateurs saisissent une invite, et plusieurs chatbots génèrent des réponses qui peuvent être directement comparées.
|
||||
3. [**AlpacaEval**](https://github.com/tatsu-lab/alpaca_eval)**:** AlpacaEval est un cadre d'évaluation automatisé où un LLM avancé comme GPT-4 évalue les réponses d'autres modèles à diverses invites.
|
||||
3. [**AlpacaEval**](https://github.com/tatsu-lab/alpaca_eval)**:** AlpacaEval est un cadre d'évaluation automatisé où un LLM avancé comme GPT-4 évalue les réponses d'autres modèles à divers prompts.
|
||||
4. **General Language Understanding Evaluation (**[**GLUE**](https://gluebenchmark.com/)**):** GLUE est une collection de neuf tâches de compréhension du langage naturel, y compris l'analyse des sentiments, l'implication textuelle et la réponse à des questions.
|
||||
5. [**SuperGLUE**](https://super.gluebenchmark.com/)**:** S'appuyant sur GLUE, SuperGLUE comprend des tâches plus difficiles conçues pour être difficiles pour les modèles actuels.
|
||||
6. **Beyond the Imitation Game Benchmark (**[**BIG-bench**](https://github.com/google/BIG-bench)**):** BIG-bench est un benchmark à grande échelle avec plus de 200 tâches qui testent les capacités d'un modèle dans des domaines tels que le raisonnement, la traduction et la réponse à des questions.
|
||||
7. **Holistic Evaluation of Language Models (**[**HELM**](https://crfm.stanford.edu/helm/lite/latest/)**):** HELM fournit une évaluation complète à travers divers indicateurs comme la précision, la robustesse et l'équité.
|
||||
7. **Holistic Evaluation of Language Models (**[**HELM**](https://crfm.stanford.edu/helm/lite/latest/)**):** HELM fournit une évaluation complète à travers divers métriques comme la précision, la robustesse et l'équité.
|
||||
8. [**OpenAI Evals**](https://github.com/openai/evals)**:** Un cadre d'évaluation open-source par OpenAI qui permet de tester des modèles d'IA sur des tâches personnalisées et standardisées.
|
||||
9. [**HumanEval**](https://github.com/openai/human-eval)**:** Une collection de problèmes de programmation utilisés pour évaluer les capacités de génération de code des modèles de langage.
|
||||
10. **Stanford Question Answering Dataset (**[**SQuAD**](https://rajpurkar.github.io/SQuAD-explorer/)**):** SQuAD se compose de questions sur des articles de Wikipédia, où les modèles doivent comprendre le texte pour répondre avec précision.
|
||||
@ -93,7 +93,7 @@ et beaucoup, beaucoup plus
|
||||
|
||||
## Follow instructions fine-tuning code
|
||||
|
||||
Vous pouvez trouver un exemple de code pour effectuer ce fine-tuning dans [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py)
|
||||
Vous pouvez trouver un exemple de code pour effectuer ce peaufiner à [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/01_main-chapter-code/gpt_instruction_finetuning.py)
|
||||
|
||||
## References
|
||||
|
||||
|
@ -82,7 +82,7 @@ Vous devriez commencer par lire ce post pour quelques concepts de base que vous
|
||||
## 7.1. Fine-Tuning for Classification
|
||||
|
||||
> [!TIP]
|
||||
> L'objectif de cette section est de montrer comment affiner un modèle déjà pré-entraîné afin qu'au lieu de générer un nouveau texte, le LLM donnera les **probabilités que le texte donné soit catégorisé dans chacune des catégories données** (comme si un texte est du spam ou non).
|
||||
> L'objectif de cette section est de montrer comment affiner un modèle déjà pré-entraîné afin qu'au lieu de générer un nouveau texte, le LLM donnera les **probabilités que le texte donné soit catégorisé dans chacune des catégories données** (comme si un texte est un spam ou non).
|
||||
|
||||
{{#ref}}
|
||||
7.1.-fine-tuning-for-classification.md
|
||||
|
Loading…
x
Reference in New Issue
Block a user