Construire un transformateur avec PyTorch
L'objectif de ce tutoriel est de fournir une compréhension complète de la construction d'un modèle de transformateur à l'aide de PyTorch. Le Transformer est l'un des modèles les plus puissants de l'apprentissage automatique moderne. Ils ont révolutionné le domaine, en particulier dans les tâches de traitement du langage naturel (NLP) telles que la traduction et le résumé de texte. Les réseaux de mémoire à long terme (LSTM) ont été remplacés par les transformateurs dans ces tâches en raison de leur capacité à gérer les dépendances à long terme et les calculs parallèles.
L'outil utilisé dans ce guide pour construire le Transformer est PyTorch, une bibliothèque d'apprentissage automatique open-source populaire connue pour sa simplicité, sa polyvalence et son efficacité. Grâce à son graphe de calcul dynamique et à ses nombreuses bibliothèques, PyTorch est devenu une référence pour les chercheurs et les développeurs dans le domaine de l'apprentissage automatique et de l'intelligence artificielle.
Pour ceux qui ne sont pas familiers avec PyTorch, une visite au cours de DataCamp, Introduction à l'apprentissage profond avec PyTorch est recommandée pour une base solide.
Développer des applications d'IA
Contexte et théorie
Présentés pour la première fois dans l'article Attention is All You Need de Vaswani et al, les transformateurs sont devenus la pierre angulaire de nombreuses tâches de la PNL en raison de leur conception unique et de leur efficacité.
Au cœur de Transformers se trouve le mécanisme d'attention, et plus précisément le concept d'"auto-attention", qui permet au modèle d'évaluer et de hiérarchiser les différentes parties des données d'entrée. C'est ce mécanisme qui permet aux transformateurs de gérer les dépendances à long terme dans les données. Il s'agit fondamentalement d'un système de pondération qui permet à un modèle de se concentrer sur différentes parties de l'entrée lors de la production d'un résultat.
Ce mécanisme permet au modèle de prendre en compte différents mots ou caractéristiques dans la séquence d'entrée, en attribuant à chacun un "poids" qui indique son importance pour la production d'un résultat donné.
Par exemple, dans une tâche de traduction de phrases, lors de la traduction d'un mot particulier, le modèle peut accorder une plus grande attention aux mots qui sont grammaticalement ou sémantiquement liés au mot cible. Ce processus permet au transformateur de capturer les dépendances entre les mots ou les caractéristiques, quelle que soit la distance qui les sépare dans la séquence.
L'impact de Transformers dans le domaine de la PNL ne peut être surestimé. Ils ont surpassé les modèles traditionnels dans de nombreuses tâches, démontrant une capacité supérieure à comprendre et à générer du langage humain de manière plus nuancée.
Pour une compréhension plus approfondie du NLP, le cours Introduction au traitement du langage naturel en Python de DataCamp est une ressource recommandée.
Configuration de PyTorch
Avant de se lancer dans la construction d'un transformateur, il est essentiel d'installer correctement l'environnement de travail. Tout d'abord, PyTorch doit être installé. PyTorch (version stable actuelle - 2.0.1) peut être facilement installé à l'aide des gestionnaires de paquets pip ou conda.
Pour pip, utilisez la commande :
pip3 install torch torchvision torchaudio
Pour conda, utilisez la commande :
conda install pytorch torchvision torchaudio pytorch-cuda=11.7 -c pytorch -c nvidia
Pour utiliser pytorch avec un processeur, veuillez consulter la documentation de pytorch.
En outre, il est utile d'avoir une compréhension de base des concepts d'apprentissage profond, car ceux-ci seront fondamentaux pour comprendre le fonctionnement des transformateurs. Pour ceux qui ont besoin d'une remise à niveau, le cours DataCamp Deep Learning in Python est une ressource précieuse qui couvre les concepts clés de l'apprentissage profond.
Construire le modèle de transformateur avec PyTorch
Pour construire le modèle de transformateur, les étapes suivantes sont nécessaires :
- Importer les bibliothèques et les modules
- Définir les éléments de base - Attention multi-têtes, réseaux de rétroaction en fonction de la position, codage positionnel
- Construction du bloc Encoder
- Construction du bloc décodeur
- Combinaison des couches Encoder et Decoder pour créer le réseau complet du transformateur
1. Importer les bibliothèques et modules nécessaires
Nous commencerons par importer la bibliothèque PyTorch pour les fonctionnalités de base, le module de réseau neuronal pour la création de réseaux neuronaux, le module d'optimisation pour l'entraînement des réseaux et les fonctions utilitaires de données pour la manipulation des données. En outre, nous importerons le module mathématique standard de Python pour les opérations mathématiques et le module copy pour la création de copies d'objets complexes.
Ces outils jettent les bases de la définition de l'architecture du modèle, de la gestion des données et de la mise en place du processus de formation.
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import math
import copy
2. Définir les éléments de base : Attention à plusieurs têtes, réseaux de rétroaction en fonction de la position, codage positionnel
Attention à plusieurs têtes
Le mécanisme d'attention multi-têtes calcule l'attention entre chaque paire de positions dans une séquence. Il se compose de plusieurs "têtes d'attention" qui saisissent différents aspects de la séquence d'entrée.
Pour en savoir plus sur l'attention à plusieurs têtes, consultez la section Mécanismes d'attention du cours Concepts des grands modèles de langage (LLM).
Figure 1. Attention à plusieurs têtes (source : image créée par l'auteur)
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
# Ensure that the model dimension (d_model) is divisible by the number of heads
assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
# Initialize dimensions
self.d_model = d_model # Model's dimension
self.num_heads = num_heads # Number of attention heads
self.d_k = d_model // num_heads # Dimension of each head's key, query, and value
# Linear layers for transforming inputs
self.W_q = nn.Linear(d_model, d_model) # Query transformation
self.W_k = nn.Linear(d_model, d_model) # Key transformation
self.W_v = nn.Linear(d_model, d_model) # Value transformation
self.W_o = nn.Linear(d_model, d_model) # Output transformation
def scaled_dot_product_attention(self, Q, K, V, mask=None):
# Calculate attention scores
attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
# Apply mask if provided (useful for preventing attention to certain parts like padding)
if mask is not None:
attn_scores = attn_scores.masked_fill(mask == 0, -1e9)
# Softmax is applied to obtain attention probabilities
attn_probs = torch.softmax(attn_scores, dim=-1)
# Multiply by values to obtain the final output
output = torch.matmul(attn_probs, V)
return output
def split_heads(self, x):
# Reshape the input to have num_heads for multi-head attention
batch_size, seq_length, d_model = x.size()
return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)
def combine_heads(self, x):
# Combine the multiple heads back to original shape
batch_size, _, seq_length, d_k = x.size()
return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)
def forward(self, Q, K, V, mask=None):
# Apply linear transformations and split heads
Q = self.split_heads(self.W_q(Q))
K = self.split_heads(self.W_k(K))
V = self.split_heads(self.W_v(V))
# Perform scaled dot-product attention
attn_output = self.scaled_dot_product_attention(Q, K, V, mask)
# Combine heads and apply output transformation
output = self.W_o(self.combine_heads(attn_output))
return output
Définition et initialisation de la classe :
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
La classe est définie comme une sous-classe de nn.Module de PyTorch.
- d_model : Dimensionnalité de l'entrée.
- num_heads : Le nombre de têtes d'attention à diviser l'entrée.
L'initialisation vérifie que d_model est divisible par num_heads, puis définit les poids de transformation pour la requête, la clé, la valeur et la sortie.
Attention aux produits à points échelonnés :
def scaled_dot_product_attention(self, Q, K, V, mask=None):
- Calcul des scores d'attention : attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k). Ici, les scores d'attention sont calculés en prenant le produit en points des requêtes (Q) et des clés (K), puis en les mettant à l'échelle par la racine carrée de la dimension de la clé (d_k).
- Application du masque : Si un masque est fourni, il est appliqué aux scores d'attention pour masquer des valeurs spécifiques.
- Calcul de la pondération de l'attention : Les scores d'attention sont passés par une fonction softmax pour les convertir en probabilités dont la somme est égale à 1.
- Calcul de la production : Le résultat final de l'attention est calculé en multipliant les poids de l'attention par les valeurs (V).
Fendre la tête :
def split_heads(self, x):
Cette méthode remodèle l'entrée x selon la forme (batch_size, num_heads, seq_length, d_k). Il permet au modèle de traiter simultanément plusieurs têtes d'attention, ce qui permet un calcul parallèle.
Combiner les têtes :
def combine_heads(self, x):
Après avoir appliqué l'attention à chaque tête séparément, cette méthode combine les résultats en un seul tenseur de forme (batch_size, seq_length, d_model). Le résultat est ainsi préparé pour un traitement ultérieur.
Méthode d'avancement :
def forward(self, Q, K, V, mask=None):
C'est dans la méthode directe que se fait le calcul proprement dit :
- Appliquer les transformations linéaires : Les requêtes (Q), les clés (K) et les valeurs (V) sont d'abord soumises à des transformations linéaires à l'aide des poids définis lors de l'initialisation.
- Têtes fendues : Les transformés Q, K, V sont divisés en plusieurs têtes à l'aide de la méthode split_heads.
- Appliquer le produit de points échelonné Attention : La méthode scaled_dot_product_attention est appelée sur les têtes divisées.
- Combinez les têtes : Les résultats de chaque tête sont combinés en un seul tenseur à l'aide de la méthode combine_heads.
- Appliquer la transformation de sortie : Enfin, le tenseur combiné est soumis à une transformation linéaire en sortie.
En résumé, la classe MultiHeadAttention encapsule le mécanisme d'attention multi-têtes couramment utilisé dans les modèles de transformateurs. Il se charge de diviser l'entrée en plusieurs têtes d'attention, d'appliquer l'attention à chaque tête, puis de combiner les résultats. Ce faisant, le modèle peut saisir diverses relations dans les données d'entrée à différentes échelles, améliorant ainsi la capacité d'expression du modèle.
Réseaux en aval en fonction de la position
class PositionWiseFeedForward(nn.Module):
def __init__(self, d_model, d_ff):
super(PositionWiseFeedForward, self).__init__()
self.fc1 = nn.Linear(d_model, d_ff)
self.fc2 = nn.Linear(d_ff, d_model)
self.relu = nn.ReLU()
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
Définition de la classe :
class PositionWiseFeedForward(nn.Module):
La classe est une sous-classe de nn.Module de PyTorch, ce qui signifie qu'elle hérite de toutes les fonctionnalités requises pour travailler avec des couches de réseaux neuronaux.
Initialisation :
def __init__(self, d_model, d_ff):
super(PositionWiseFeedForward, self).__init__()
self.fc1 = nn.Linear(d_model, d_ff)
self.fc2 = nn.Linear(d_ff, d_model)
self.relu = nn.ReLU()
- d_model : Dimensionnalité de l'entrée et de la sortie du modèle.
- d_ff : Dimensionnalité de la couche interne du réseau de type "feed-forward".
- self.fc1 et self.fc2 : Deux couches entièrement connectées (linéaires) avec des dimensions d'entrée et de sortie définies par d_model et d_ff.
- self.relu : Fonction d'activation ReLU (Rectified Linear Unit), qui introduit une non-linéarité entre les deux couches linéaires.
Méthode d'avancement :
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
- x : L'entrée du réseau de type "feed-forward".
- self.fc1(x) : L'entrée passe d'abord par la première couche linéaire (fc1).
- self.relu(...) : La sortie de fc1 est ensuite passée par une fonction d'activation ReLU. ReLU remplace toutes les valeurs négatives par des zéros, ce qui introduit une non-linéarité dans le modèle.
- self.fc2(...) : La sortie activée passe ensuite par la deuxième couche linéaire (fc2), qui produit la sortie finale.
En résumé, la classe PositionWiseFeedForward définit un réseau neuronal de type feed-forward qui consiste en deux couches linéaires avec une fonction d'activation ReLU entre les deux. Dans le contexte des modèles de transformateurs, ce réseau de rétroaction est appliqué à chaque position séparément et de manière identique. Il aide à transformer les caractéristiques apprises par les mécanismes d'attention au sein du transformateur, agissant comme une étape de traitement supplémentaire pour les résultats de l'attention.
Codage positionnel
Le codage positionnel est utilisé pour injecter les informations relatives à la position de chaque jeton dans la séquence d'entrée. Il utilise des fonctions sinusoïdales et cosinusoïdales de différentes fréquences pour générer le codage positionnel.
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_seq_length):
super(PositionalEncoding, self).__init__()
pe = torch.zeros(max_seq_length, d_model)
position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer('pe', pe.unsqueeze(0))
def forward(self, x):
return x + self.pe[:, :x.size(1)]
Définition de la classe :
class PositionalEncoding(nn.Module):
La classe est définie comme une sous-classe de nn.Module de PyTorch, ce qui lui permet d'être utilisée comme une couche standard de PyTorch.
Initialisation :
def __init__(self, d_model, max_seq_length):
super(PositionalEncoding, self).__init__()
pe = torch.zeros(max_seq_length, d_model)
position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer('pe', pe.unsqueeze(0))
- d_model : La dimension de l'entrée du modèle.
- max_seq_length : La longueur maximale de la séquence pour laquelle les encodages positionnels sont précalculés.
- pe : Un tenseur rempli de zéros, qui sera alimenté par des encodages positionnels.
- position : Un tenseur contenant les indices de position pour chaque position dans la séquence.
- div_term : Terme utilisé pour mettre à l'échelle les indices de position d'une manière spécifique.
- La fonction sinus est appliquée aux indices pairs et la fonction cosinus aux indices impairs de pe.
- Enfin, pe est enregistré comme tampon, ce qui signifie qu'il fera partie de l'état du module mais ne sera pas considéré comme un paramètre entraînable.
Méthode d'avancement :
def forward(self, x):
return x + self.pe[:, :x.size(1)]
La méthode forward ajoute simplement les codages positionnels à l'entrée x.
Il utilise les premiers éléments x.size(1) de pe pour s'assurer que les encodages positionnels correspondent à la longueur de séquence réelle de x.
Résumé
La classe PositionalEncoding ajoute des informations sur la position des tokens dans la séquence. Étant donné que le modèle de transformateur n'a pas de connaissance inhérente de l'ordre des jetons (en raison de son mécanisme d'auto-attention), cette classe aide le modèle à prendre en compte la position des jetons dans la séquence. Les fonctions sinusoïdales utilisées sont choisies pour permettre au modèle d'apprendre facilement à s'intéresser aux positions relatives, car elles produisent un codage unique et régulier pour chaque position de la séquence.
3. Construction des blocs de l'encodeur
Figure 2. La partie encodeur du réseau de transformateurs (Source : image de l'article original)
class EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
super(EncoderLayer, self).__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask):
attn_output = self.self_attn(x, x, x, mask)
x = self.norm1(x + self.dropout(attn_output))
ff_output = self.feed_forward(x)
x = self.norm2(x + self.dropout(ff_output))
return x
Définition de la classe :
class EncoderLayer(nn.Module):
La classe est définie comme une sous-classe de nn.Module de PyTorch, ce qui signifie qu'elle peut être utilisée comme bloc de construction pour les réseaux neuronaux dans PyTorch.
Initialisation :
def __init__(self, d_model, num_heads, d_ff, dropout):
super(EncoderLayer, self).__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
Paramètres :
- d_model : La dimensionnalité de l'entrée.
- num_heads : Le nombre de têtes d'attention dans l'attention multi-têtes.
- d_ff : La dimensionnalité de la couche interne dans le réseau à progression par position.
- décrochage : Le taux d'abandon utilisé pour la régularisation.
Composants :
- self.self_attn : Mécanisme d'attention à plusieurs têtes.
- self.feed_forward : Réseau neuronal feed-forward en fonction de la position.
- self.norm1 et self.norm2 : Normalisation de la couche, appliquée pour lisser l'entrée de la couche.
- self.dropout : Couche d'exclusion, utilisée pour éviter l'ajustement excessif en mettant aléatoirement certaines activations à zéro pendant l'apprentissage.
Méthode d'avancement :
def forward(self, x, mask):
attn_output = self.self_attn(x, x, x, mask)
x = self.norm1(x + self.dropout(attn_output))
ff_output = self.feed_forward(x)
x = self.norm2(x + self.dropout(ff_output))
return x
Entrée :
- x : L'entrée de la couche d'encodage.
- masque : Masque facultatif permettant d'ignorer certaines parties de l'entrée.
Étapes de traitement :
- Attention à soi : L'entrée x passe par le mécanisme d'auto-attention à têtes multiples.
- Ajouter et normaliser (après attention) : La sortie de l'attention est ajoutée à l'entrée originale (connexion résiduelle), suivie d'un abandon et d'une normalisation à l'aide de norm1.
- Réseau en aval (Feed-Forward Network) : La sortie de l'étape précédente est transmise au réseau d'anticipation en fonction de la position.
- Ajouter et normaliser (après Feed-Forward) : Comme à l'étape 2, la sortie feed-forward est ajoutée à l'entrée de cette étape (connexion résiduelle), suivie d'un abandon et d'une normalisation à l'aide de norm2.
- Sortie : Le tenseur traité est renvoyé en tant que sortie de la couche de codage.
Résumé :
La classe EncoderLayer définit une seule couche de l'encodeur du transformateur. Il comprend un mécanisme d'auto-attention à plusieurs têtes, suivi d'un réseau neuronal à progression par position, avec des connexions résiduelles, une normalisation des couches et un abandon appliqués le cas échéant. L'ensemble de ces composants permet au codeur de capturer des relations complexes dans les données d'entrée et de les transformer en une représentation utile pour les tâches en aval. En règle générale, plusieurs couches de codeurs de ce type sont empilées pour former la partie codeur complète d'un modèle de transformateur.
4. Construction des blocs de décodage
class DecoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
super(DecoderLayer, self).__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.cross_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, enc_output, src_mask, tgt_mask):
attn_output = self.self_attn(x, x, x, tgt_mask)
x = self.norm1(x + self.dropout(attn_output))
attn_output = self.cross_attn(x, enc_output, enc_output, src_mask)
x = self.norm2(x + self.dropout(attn_output))
ff_output = self.feed_forward(x)
x = self.norm3(x + self.dropout(ff_output))
return x
Définition de la classe :
class DecoderLayer(nn.Module):
Initialisation :
def __init__(self, d_model, num_heads, d_ff, dropout):
super(DecoderLayer, self).__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.cross_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
Paramètres:
- d_model : La dimensionnalité de l'entrée.
- num_heads : Le nombre de têtes d'attention dans l'attention multi-têtes.
- d_ff : La dimensionnalité de la couche interne du réseau de type "feed-forward".
- décrochage : Le taux d'abandon pour la régularisation.
Composants:
- self.self_attn : Mécanisme d'auto-attention à têtes multiples pour la séquence cible.
- self.cross_attn : Mécanisme d'attention à têtes multiples qui s'occupe de la sortie du codeur.
- self.feed_forward : Réseau neuronal feed-forward en fonction de la position.
- self.norm1, self.norm2, self.norm3 : Composants de normalisation des couches.
- self.dropout : Couche d'exclusion pour la régularisation.
Méthode d'avancement:
ef forward(self, x, enc_output, src_mask, tgt_mask):
attn_output = self.self_attn(x, x, x, tgt_mask)
x = self.norm1(x + self.dropout(attn_output))
attn_output = self.cross_attn(x, enc_output, enc_output, src_mask)
x = self.norm2(x + self.dropout(attn_output))
ff_output = self.feed_forward(x)
x = self.norm3(x + self.dropout(ff_output))
return x
Entrée:
- x : L'entrée de la couche décodeur.
- enc_output : La sortie du codeur correspondant (utilisé dans l'étape d'attention croisée).
- src_mask : Masque de source permettant d'ignorer certaines parties de la sortie de l'encodeur.
- tgt_mask : Masque cible permettant d'ignorer certaines parties de l'entrée du décodeur.
Étapes de traitement :
- Auto-attention sur la séquence cible : L'entrée x est traitée par un mécanisme d'auto-attention.
- Ajouter et normaliser (après l'auto-attention) : Le résultat de l'auto-attention est ajouté à l'original x, suivi d'un abandon et d'une normalisation à l'aide de norm1.
- Attention croisée avec la sortie de l'encodeur : La sortie normalisée de l'étape précédente est traitée par un mécanisme d'attention croisée qui s'occupe de la sortie du codeur enc_output.
- Ajouter et normaliser (après l'attention croisée) : La sortie de l'attention croisée est ajoutée à l'entrée de cette étape, suivie d'une élimination et d'une normalisation à l'aide de norm2.
- Réseau en aval (Feed-Forward Network) : La sortie de l'étape précédente est transmise au réseau de type "feed-forward".
- Ajouter et normaliser (après Feed-Forward) : La sortie feed-forward est ajoutée à l'entrée de cette étape, suivie d'un dropout et d'une normalisation à l'aide de norm3.
- Sortie : Le tenseur traité est renvoyé en tant que sortie de la couche décodeur.
Résumé :
La classe DecoderLayer définit une seule couche du décodeur du transformateur. Il se compose d'un mécanisme d'auto-attention à têtes multiples, d'un mécanisme d'attention croisée à têtes multiples (qui s'occupe de la sortie du codeur), d'un réseau neuronal à progression par position et des connexions résiduelles correspondantes, de la normalisation des couches et des couches d'exclusion. Cette combinaison permet au décodeur de générer des sorties significatives sur la base des représentations du codeur, en tenant compte à la fois de la séquence cible et de la séquence source. Comme pour le codeur, plusieurs couches de décodeur sont généralement empilées pour former la partie décodeur complète d'un modèle de transformateur.
Ensuite, les blocs Encoder et Decoder sont réunis pour construire le modèle complet du transformateur.
5. Combinaison des couches Encoder et Decoder pour créer le réseau complet du transformateur
Figure 4. Le réseau des transformateurs (Source : Image de l'article original)
class Transformer(nn.Module):
def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout):
super(Transformer, self).__init__()
self.encoder_embedding = nn.Embedding(src_vocab_size, d_model)
self.decoder_embedding = nn.Embedding(tgt_vocab_size, d_model)
self.positional_encoding = PositionalEncoding(d_model, max_seq_length)
self.encoder_layers = nn.ModuleList([EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
self.decoder_layers = nn.ModuleList([DecoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
self.fc = nn.Linear(d_model, tgt_vocab_size)
self.dropout = nn.Dropout(dropout)
def generate_mask(self, src, tgt):
src_mask = (src != 0).unsqueeze(1).unsqueeze(2)
tgt_mask = (tgt != 0).unsqueeze(1).unsqueeze(3)
seq_length = tgt.size(1)
nopeak_mask = (1 - torch.triu(torch.ones(1, seq_length, seq_length), diagonal=1)).bool()
tgt_mask = tgt_mask & nopeak_mask
return src_mask, tgt_mask
def forward(self, src, tgt):
src_mask, tgt_mask = self.generate_mask(src, tgt)
src_embedded = self.dropout(self.positional_encoding(self.encoder_embedding(src)))
tgt_embedded = self.dropout(self.positional_encoding(self.decoder_embedding(tgt)))
enc_output = src_embedded
for enc_layer in self.encoder_layers:
enc_output = enc_layer(enc_output, src_mask)
dec_output = tgt_embedded
for dec_layer in self.decoder_layers:
dec_output = dec_layer(dec_output, enc_output, src_mask, tgt_mask)
output = self.fc(dec_output)
return output
Définition de la classe :
class Transformer(nn.Module):
Initialisation :
def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout):
Le constructeur prend les paramètres suivants :
- src_vocab_size : Taille du vocabulaire source.
- tgt_vocab_size : Taille du vocabulaire cible.
- d_model : La dimensionnalité des encastrements du modèle.
- num_heads : Nombre de têtes d'attention dans le mécanisme d'attention multi-têtes.
- num_layers : Nombre de couches pour le codeur et le décodeur.
- d_ff : Dimensionnalité de la couche interne du réseau de type "feed-forward".
- max_seq_length : Longueur maximale de la séquence pour le codage positionnel.
- décrochage : Taux d'abandon pour la régularisation.
Il définit les éléments suivants :
- self.encoder_embedding : Couche d'intégration pour la séquence source.
- self.decoder_embedding : Couche d'intégration pour la séquence cible.
- self.positional_encoding : Composant d'encodage positionnel.
- self.encoder_layers : Liste des couches de l'encodeur.
- self.decoder_layers : Une liste de couches de décodeurs.
- self.fc : Couche finale entièrement connectée (linéaire) correspondant à la taille du vocabulaire cible.
- self.dropout : Couche de décrochage.
Méthode de génération de masque :
def generate_mask(self, src, tgt):
Cette méthode est utilisée pour créer des masques pour les séquences source et cible, en veillant à ce que les jetons de remplissage soient ignorés et que les futurs jetons ne soient pas visibles lors de l'apprentissage de la séquence cible.
Méthode d'avancement :
def forward(self, src, tgt):
Cette méthode définit la passe avant du transformateur, qui prend les séquences source et cible et produit les prédictions de sortie.
- Encodage d'entrée et encodage positionnel : Les séquences source et cible sont d'abord intégrées à l'aide de leurs couches d'intégration respectives, puis ajoutées à leurs codages positionnels.
- Couches de l'encodeur : La séquence source passe par les couches du codeur, la sortie finale du codeur représentant la séquence source traitée.
- Couches de décodage : La séquence cible et la sortie du codeur passent par les couches du décodeur, ce qui donne la sortie du décodeur.
- Couche linéaire finale : La sortie du décodeur est mise en correspondance avec la taille du vocabulaire cible à l'aide d'une couche entièrement connectée (linéaire).
Sortie :
La sortie finale est un tenseur représentant les prédictions du modèle pour la séquence cible.
Résumé :
La classe Transformer rassemble les différents composants d'un modèle Transformer, y compris les embeddings, le codage positionnel, les couches de codage et les couches de décodage. Il fournit une interface pratique pour l'entraînement et l'inférence, en encapsulant les complexités de l'attention multi-têtes, des réseaux de type feed-forward et de la normalisation des couches.
Cette implémentation suit l'architecture standard de Transformer, ce qui la rend adaptée aux tâches de séquence à séquence telles que la traduction automatique, le résumé de texte, etc. L'inclusion du masquage garantit que le modèle respecte les dépendances causales au sein des séquences, en ignorant les jetons de remplissage et en empêchant les fuites d'informations provenant des jetons futurs.
Ces étapes séquentielles permettent au modèle Transformer de traiter efficacement les séquences d'entrée et de produire les séquences de sortie correspondantes.
Formation du modèle de transformateur PyTorch
Préparation des données d'échantillonnage
À des fins d'illustration, un ensemble de données fictives sera créé dans cet exemple. Toutefois, dans un scénario pratique, un ensemble de données plus important serait utilisé et le processus impliquerait le prétraitement du texte ainsi que la création de correspondances de vocabulaire pour les langues source et cible.
src_vocab_size = 5000
tgt_vocab_size = 5000
d_model = 512
num_heads = 8
num_layers = 6
d_ff = 2048
max_seq_length = 100
dropout = 0.1
transformer = Transformer(src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout)
# Generate random sample data
src_data = torch.randint(1, src_vocab_size, (64, max_seq_length)) # (batch_size, seq_length)
tgt_data = torch.randint(1, tgt_vocab_size, (64, max_seq_length)) # (batch_size, seq_length)
Hyperparamètres :
Ces valeurs définissent l'architecture et le comportement du modèle de transformateur :
- src_vocab_size, tgt_vocab_size : Taille du vocabulaire pour les séquences source et cible, toutes deux fixées à 5000.
- d_model : Dimensionnalité des encastrements du modèle, fixée à 512.
- num_heads : Nombre de têtes d'attention dans le mécanisme d'attention multi-têtes, fixé à 8.
- num_layers : Nombre de couches pour le codeur et le décodeur, fixé à 6.
- d_ff : Dimensionnalité de la couche interne du réseau de type feed-forward, fixée à 2048.
- max_seq_length : Longueur maximale de la séquence pour le codage positionnel, fixée à 100.
- décrochage : Taux d'abandon pour la régularisation, fixé à 0,1.
Création d'une instance de transformateur :
transformer = Transformer(src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout)
Cette ligne crée une instance de la classe Transformer, en l'initialisant avec les hyperparamètres donnés. L'instance aura l'architecture et le comportement définis par ces hyperparamètres.
Générer des données d'échantillons aléatoires :
Les lignes suivantes génèrent des séquences source et cible aléatoires :
- src_data : Entiers aléatoires compris entre 1 et src_vocab_size, représentant un lot de séquences sources de forme (64, max_seq_length).
- tgt_data : Entiers aléatoires compris entre 1 et tgt_vocab_size, représentant un lot de séquences cibles de forme (64, max_seq_length).
- Ces séquences aléatoires peuvent être utilisées comme entrées dans le modèle de transformateur, simulant un lot de données avec 64 exemples et des séquences de longueur 100.
Résumé :
L'extrait de code montre comment initialiser un modèle de transformateur et générer des séquences aléatoires de sources et de cibles qui peuvent être introduites dans le modèle. Les hyperparamètres choisis déterminent la structure et les propriétés spécifiques du transformateur. Cette configuration pourrait faire partie d'un script plus large dans lequel le modèle est entraîné et évalué sur des tâches réelles de séquence à séquence, telles que la traduction automatique ou le résumé de texte.
Formation du modèle
Ensuite, le modèle sera entraîné à l'aide de l'échantillon de données susmentionné. Toutefois, dans un scénario réel, un ensemble de données beaucoup plus important serait utilisé, qui serait généralement divisé en ensembles distincts à des fins de formation et de validation.
criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.Adam(transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)
transformer.train()
for epoch in range(100):
optimizer.zero_grad()
output = transformer(src_data, tgt_data[:, :-1])
loss = criterion(output.contiguous().view(-1, tgt_vocab_size), tgt_data[:, 1:].contiguous().view(-1))
loss.backward()
optimizer.step()
print(f"Epoch: {epoch+1}, Loss: {loss.item()}")
Fonction de perte et optimiseur :
- criterion = nn.CrossEntropyLoss(ignore_index=0): Définit la fonction de perte comme une perte d'entropie croisée. L'argument ignore_index est fixé à 0, ce qui signifie que la perte ne prendra pas en compte les cibles ayant un index de 0 (généralement réservé aux jetons de remplissage).
- optimizer = optim.Adam(...) : Définit l'optimiseur comme Adam avec un taux d'apprentissage de 0,0001 et des valeurs bêta spécifiques.
Mode d'entraînement au modèle :
- transformer.train() : Met le modèle de transformateur en mode d'apprentissage, ce qui permet d'activer des comportements tels que l'abandon qui ne s'appliquent que pendant l'apprentissage.
Boucle de formation :
L'extrait de code entraîne le modèle pendant 100 époques à l'aide d'une boucle d'entraînement typique :
- for epoch in range(100) : Itère sur 100 époques d'apprentissage.
- optimizer.zero_grad() : Efface les gradients de l'itération précédente.
- output = transformer(src_data, tgt_data[ :, :-1]) : Fait passer les données sources et les données cibles (à l'exception du dernier élément de chaque séquence) à travers le transformateur. Cette situation est fréquente dans les tâches de séquence à séquence où la cible est décalée d'un token.
- perte = critère(...) : Calcule la perte entre les prédictions du modèle et les données cibles (à l'exclusion du premier jeton de chaque séquence). La perte est calculée en transformant les données en tenseurs unidimensionnels et en utilisant la fonction de perte d'entropie croisée.
- perte.backward() : Calcule les gradients de la perte en fonction des paramètres du modèle.
- optimizer.step() : Met à jour les paramètres du modèle en utilisant les gradients calculés.
- print(f "Époque : {époque+1}, Perte : {perte.item()}") : Imprime le numéro de l'époque actuelle et la valeur de la perte pour cette époque.
Résumé :
Cet extrait de code entraîne le modèle de transformateur sur des séquences source et cible générées de manière aléatoire pendant 100 époques. Il utilise l'optimiseur d'Adam et la fonction de perte d'entropie croisée. La perte est imprimée pour chaque époque, ce qui vous permet de suivre la progression de l'entraînement. Dans un scénario réel, vous remplaceriez les séquences source et cible aléatoires par des données réelles issues de votre tâche, telle que la traduction automatique.
Évaluation des performances du modèle de transformateur
Après l'apprentissage du modèle, ses performances peuvent être évaluées sur un ensemble de données de validation ou de test. Voici un exemple de la manière dont cela pourrait se faire :
transformer.eval()
# Generate random sample validation data
val_src_data = torch.randint(1, src_vocab_size, (64, max_seq_length)) # (batch_size, seq_length)
val_tgt_data = torch.randint(1, tgt_vocab_size, (64, max_seq_length)) # (batch_size, seq_length)
with torch.no_grad():
val_output = transformer(val_src_data, val_tgt_data[:, :-1])
val_loss = criterion(val_output.contiguous().view(-1, tgt_vocab_size), val_tgt_data[:, 1:].contiguous().view(-1))
print(f"Validation Loss: {val_loss.item()}")
Mode d'évaluation :
- transformer.eval() : Place le modèle de transformateur en mode évaluation. Ceci est important car cela désactive certains comportements comme l'abandon qui ne sont utilisés que pendant la formation.
Générer des données de validation aléatoires :
- val_src_data : Entiers aléatoires compris entre 1 et src_vocab_size, représentant un lot de séquences sources de validation de forme (64, max_seq_length).
- val_tgt_data : Entiers aléatoires compris entre 1 et tgt_vocab_size, représentant un lot de séquences cibles de validation de forme (64, max_seq_length).
Boucle de validation :
- avec torch.no_grad() : Désactive le calcul des gradients, car nous n'avons pas besoin de calculer les gradients pendant la validation. Cela permet de réduire la consommation de mémoire et d'accélérer les calculs.
- val_output = transformer(val_src_data, val_tgt_data[:, :-1]): Fait passer les données source de validation et les données cible de validation (à l'exception du dernier élément de chaque séquence) par le transformateur.
- val_loss = criterion(...) : Calcule la perte entre les prédictions du modèle et les données cibles de validation (à l'exclusion du premier token de chaque séquence). La perte est calculée en transformant les données en tenseurs unidimensionnels et en utilisant la fonction de perte d'entropie croisée définie précédemment.
- print(f "Perte de validation : {val_loss.item()}") : Imprime la valeur de la perte de validation.
Résumé :
Cet extrait de code évalue le modèle de transformateur sur un ensemble de données de validation généré de manière aléatoire, calcule la perte de validation et l'imprime. Dans un scénario réel, les données de validation aléatoires doivent être remplacées par des données de validation réelles provenant de la tâche sur laquelle vous travaillez. La perte de validation peut vous donner une indication de la performance de votre modèle sur des données inédites, ce qui constitue une mesure essentielle de la capacité de généralisation du modèle.
Pour plus de détails sur les transformateurs et Hugging Face, notre tutoriel, Une introduction à l'utilisation des transformateurs et Hugging Face, est utile.
Conclusion et autres ressources
En conclusion, ce tutoriel a montré comment construire un modèle Transformer en utilisant PyTorch, l'un des outils les plus polyvalents pour l'apprentissage profond. Grâce à leur capacité de parallélisation et à leur aptitude à saisir les dépendances à long terme dans les données, les transformateurs présentent un immense potentiel dans divers domaines, en particulier dans les tâches de traitement automatique des langues telles que la traduction, le résumé et l'analyse des sentiments.
Pour ceux qui sont désireux d'approfondir leur compréhension des concepts et techniques d'apprentissage profond avancés, envisagez d'explorer le cours Apprentissage profond avancé avec Keras sur DataCamp. Vous pouvez également lire un autre tutoriel sur la construction d'un réseau neuronal simple avec PyTorch.