Cours
Tutoriel sur le modèle de transformateur dans PyTorch : De la théorie au code
Le Transformer est l'un des modèles les plus puissants de l'apprentissage automatique moderne. Elle a 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 transformateurs ont remplacé les réseaux de mémoire à long terme (LSTM) dans ces tâches en raison de leur capacité à gérer les dépendances à long terme et les calculs parallèles.
PyTorch, une bibliothèque d'apprentissage automatique à code source ouvert réputée pour sa simplicité, sa polyvalence et son efficacité, est devenue une référence pour les chercheurs et les développeurs dans le domaine de l'apprentissage automatique et de l'intelligence artificielle. Ce tutoriel vise à fournir une compréhension complète de la construction d'un modèle de transformateur à l'aide de PyTorch.
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 des transformateurs
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. Ce mécanisme 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.
Le mécanisme d'auto-attention 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 construire un transformateur, il est essentiel d'installer correctement l'environnement de travail. Tout d'abord, PyTorch doit être installé. PyTorch peut être 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 -c pytorch
Pour d'autres options d'installation et d'exécution de PyTorch, consultez le site officiel.
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 à plusieurs 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 standard Python math
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
Avant de nous lancer dans la construction de nos composants, jetez un coup d'œil au tableau suivant, qui décrit les différents composants d'un transformateur et leur utilité :
Composant | Description | Objectif |
---|---|---|
Attention à plusieurs têtes | Mécanisme permettant de se concentrer sur différentes parties de l'entrée | Capture les dépendances entre les différentes positions dans la séquence |
Réseaux ascendants | Couches entièrement connectées en fonction de la position | Transforme les résultats de l'attention, ce qui ajoute à la complexité |
Codage positionnel | Ajoute des informations sur la position à l'encastrement | Fournit un contexte d'ordre séquentiel au modèle |
Normalisation des couches | Normalise les entrées de chaque sous-couche | Stabilisation de la formation, amélioration de la convergence |
Connexions résiduelles | Raccourcis entre les couches | Aide à la formation de réseaux plus profonds en minimisant les problèmes de gradient |
Abandon | Remise à zéro aléatoire de certaines connexions réseau | Empêche l'ajustement excessif en régularisant le modèle |
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 multi-têtes, consultez la section sur les mécanismes d' attention du cours sur les 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 la classe 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 si d_model
est divisible par num_heads
, puis définit les poids de transformation pour query
, key
, value
et output
.
Attention au produit point échelonné :
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 échelonnant en fonction de la racine carrée de la dimension de la clé (d_k
). - Masque d'application: Si un masque est fourni, il est appliqué aux scores d'attention pour masquer des valeurs spécifiques.
- Calcul des coefficients d'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 les têtes :
def split_heads(self, x):
Cette méthode redéfinit la forme de l'entrée x (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 des 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 dans l'initialisation. - Têtes fendues: Les fichiers transformés
Q
,K
,V
sont divisés en plusieurs têtes à l'aide de la méthodesplit_heads
. - Appliquer l'attention sur les produits à points échelonnés: La méthode
scaled_dot_product_attention
est appelée sur les têtes de fractionnement. - 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 à plusieurs 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 d'anticipation 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 PyTorch's nn.Module
, 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
etself.fc2
: Deux couches entièrement connectées (linéaires) avec des dimensions d'entrée et de sortie définies pard_model
etd_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 defc1
passe ensuite 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 feed-forward positionné 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 PyTorch's nn.Module
, 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é en tant que tampon, ce qui signifie qu'il fera partie de l'état du module mais ne sera pas considéré comme un paramètre pouvant faire l'objet d'une formation.
Méthode d'avancement :
def forward(self, x):
return x + self.pe[:, :x.size(1)]
La méthode directe 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 codages positionnels correspondent à la longueur de séquence réelle de x
.
Résumé
La classe PositionalEncoding
ajoute des informations sur la position des jetons 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 d'encodage
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 PyTorch's nn.Module
, ce qui signifie qu'elle peut être utilisée comme un 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.dropout
: 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
etself.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.mask
: 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 amont : 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 retour d'information): Comme à l'étape 2, la sortie feed-forward est ajoutée à l'entrée de cette étape (connexion résiduelle), suivie d'un dropout 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 du codeur du transformateur. Il comprend un mécanisme d'auto-attention à plusieurs têtes, suivi par le réseau neuronal à progression par position, avec des connexions résiduelles, une normalisation des couches et un abandon appliqués le cas échéant. Ensemble, ces composants permettent 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 du décodeur
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".dropout
: 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 du codeur: 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 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 amont : La sortie de l'étape précédente est transmise au réseau de type "feed-forward".
- Ajouter et normaliser (après retour d'information): 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 combinés pour construire le modèle complet du transformateur.
5. Combinaison des couches de codage et de décodage pour créer le réseau Transformer complet
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.dropout
: 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.
- Dernière couche linéaire: 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 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.dropout
: Taux d'abandon pour la régularisation, fixé à 0,1.
À titre de référence, le tableau suivant décrit les hyperparamètres les plus courants pour les modèles de transformateur et leurs valeurs :
Hyperparamètre | Valeurs typiques | Impact sur les performances |
---|---|---|
d_model | 256, 512, 1024 | Des valeurs plus élevées augmentent la capacité du modèle mais nécessitent davantage de calculs. |
num_heads | 8, 12, 16 | Un plus grand nombre de têtes peuvent capturer divers aspects des données, mais elles sont gourmandes en ressources informatiques. |
num_layers | 6, 12, 24 | Un plus grand nombre de couches améliore le pouvoir de représentation, mais peut conduire à un surajustement. |
d_ff | 2048, 4096 | Des réseaux plus larges augmentent la robustesse du modèle |
dropout | 0.1, 0.3 | Régularise le modèle pour éviter l'ajustement excessif |
taux d'apprentissage | 0.0001 - 0.001 | Impact sur la vitesse de convergence et la stabilité |
taille du lot | 32, 64, 128 | Des lots plus importants améliorent la stabilité de l'apprentissage mais nécessitent plus de mémoire. |
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 etsrc_vocab_size
, représentant un lot de séquences sources de forme (64,max_seq_length
).tgt_data
: Entiers aléatoires compris entre 1 ettgt_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 source et de cible qui peuvent être introduites dans le transformateur. 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 vaste 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'argumentignore_index
est fixé à 0, ce qui signifie que la perte ne prendra pas en compte les cibles dont l'indice est égal à 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 de formation 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.loss = criterion(...)
: 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.loss.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"Epoch: {epoch+1}, Loss: {loss.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 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 etsrc_vocab_size
, représentant un lot de séquences de sources de validation de forme (64,max_seq_length
).val_tgt_data
: Entiers aléatoires compris entre 1 ettgt_vocab_size
, représentant un lot de séquences cibles de validation de forme (64,max_seq_length
).
Boucle de validation :
with 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"Validation Loss: {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.
Obtenez une certification de haut niveau en matière d'IA
FAQ
En quoi l'auto-attention diffère-t-elle des mécanismes d'attention traditionnels ?
L'auto-attention diffère de l'attention traditionnelle en permettant à un modèle d'être attentif à toutes les positions d'une même séquence pour calculer sa représentation. Les mécanismes d'attention traditionnels se concentrent généralement sur l'alignement de deux séquences distinctes, comme dans les architectures codeur-décodeur, où le décodeur s'occupe des sorties du codeur.
Pourquoi les transformateurs sont-ils plus efficaces que les LSTM pour traiter de longues séquences ?
Les transformateurs utilisent des mécanismes d'auto-attention qui permettent la parallélisation et les connexions directes entre des éléments éloignés dans une séquence. Cela contraste avec les LSTM, qui traitent les séquences de manière séquentielle et peuvent avoir du mal à traiter les dépendances à longue distance en raison de problèmes de gradient de disparition.
Quel rôle joue la normalisation des couches dans l'architecture de Transformer ?
La normalisation des couches permet de stabiliser et d'accélérer la formation des réseaux profonds en normalisant les entrées de chaque couche. Cela garantit que la distribution des entrées de la couche reste cohérente, ce qui améliore la convergence et la performance du modèle.
Comment les encodages positionnels aident-ils le modèle Transformer ?
Les codages positionnels introduisent des informations sur la position de chaque jeton dans la séquence, ce qui permet au modèle de saisir l'ordre des mots, qui est crucial pour comprendre le contexte et les relations entre les mots.
L'architecture Transformer peut-elle être utilisée pour des tâches autres que le NLP, comme la vision par ordinateur ?
Oui, l'architecture Transformer a été adaptée à des tâches de vision par ordinateur, comme dans les Vision Transformers (ViTs), où elle traite les parcelles d'image comme des séquences, en tirant parti de ses puissants mécanismes d'attention pour capturer les relations spatiales.
Pourquoi utiliser l'abandon, et en quoi cela profite-t-il au processus de formation ?
L'exclusion est utilisée pour éviter l'ajustement excessif en mettant aléatoirement à zéro une fraction des unités d'entrée au cours de la formation. Cela encourage le modèle à apprendre des caractéristiques plus robustes qui ne dépendent pas de neurones spécifiques, améliorant ainsi la généralisation.
Comment le calcul du gradient est-il géré différemment dans les modes d'entraînement et d'évaluation ?
Pendant l'apprentissage, les gradients sont calculés pour mettre à jour les poids du modèle, tandis qu'en mode évaluation, le calcul des gradients est désactivé à l'aide de la touche torch.no_grad()
ce qui réduit l'utilisation de la mémoire et le temps de calcul puisque les mises à jour des poids sont inutiles.
Quelle est l'importance de l'utilisation de plusieurs têtes d'attention dans le cadre de l'attention multiple ?
Les têtes d'attention multiples permettent au modèle de se concentrer simultanément sur différentes parties de la séquence d'entrée, capturant ainsi diverses relations et modèles, ce qui améliore finalement la capacité du modèle à comprendre des structures de données complexes.
Comment le Transformateur gère-t-il les séquences de longueur variable ?
Le transformateur gère les séquences de longueur variable en utilisant des jetons de remplissage pour garantir une taille d'entrée cohérente entre les lots. Des masques sont appliqués pour ignorer ces jetons de remplissage lors des calculs d'attention, ce qui les empêche d'influencer les résultats du modèle.
Quels sont les défis potentiels liés à la formation d'un modèle de transformateur à partir de zéro ?
La formation d'un modèle de transformateur à partir de zéro peut s'avérer coûteuse en temps et en argent en raison de sa complexité et de la nécessité de disposer de vastes ensembles de données. En outre, la sélection d'hyperparamètres appropriés et la garantie d'une convergence sans surajustement peuvent s'avérer difficiles.

Chercheur doctorant - Deep Learning on Biomedical Images à l'Institut Leibniz-HKI, Allemagne. Enthousiasmé par la mise en œuvre de l'IA dans le domaine de la santé.
Apprenez-en plus sur PyTorch avec ces cours !
Cours
Apprentissage profond intermédiaire avec PyTorch
Cours