Mécanisme d'attention (5) : principes et mise en œuvre de l'architecture de transformateur, traduction automatique réelle

Colonne : Répertoire de récurrence du réseau de neurones

mécanisme attentionnel

Le mécanisme d'attention est une technologie d'intelligence artificielle qui permet aux réseaux de neurones de se concentrer sur les informations clés tout en ignorant les parties sans importance lors du traitement des données de séquence. Le mécanisme d'attention a été largement utilisé dans le traitement du langage naturel, la vision par ordinateur, la reconnaissance vocale et d'autres domaines.

L'idée principale du mécanisme d'attention est de faire en sorte que le modèle accorde plus d'attention aux entrées importantes en attribuant différents poids aux signaux d'entrée à différentes positions lors du traitement des données de séquence. Par exemple, lors du traitement d'une phrase, le mécanisme d'attention peut ajuster l'attention du modèle à chaque mot en fonction de l'importance de chaque mot. Cette technique peut améliorer les performances du modèle, en particulier lorsqu'il s'agit de données à longue séquence.

Dans les modèles d'apprentissage en profondeur, les mécanismes d'attention sont souvent mis en œuvre en ajoutant des couches réseau supplémentaires qui apprennent à calculer des poids et à appliquer ces poids au signal d'entrée. Les mécanismes d'attention courants incluent l'auto-attention, l'attention multi-têtes, etc.

En conclusion, le mécanisme d'attention est une technique très utile qui peut aider les réseaux de neurones à mieux traiter les données séquentielles et à améliorer les performances du modèle.



attention à soi

L'auto-attention (Self-Attention) est un mécanisme d'attention dans l'apprentissage en profondeur. Il a été proposé pour la première fois dans l'article "Attention is All You Need" pour résoudre des problèmes de séquence à séquence dans le traitement du langage naturel, comme la traduction automatique.

Le mécanisme d'auto-attention permet au modèle d'apprendre automatiquement les dépendances entre différentes positions dans une séquence de texte, afin de mieux comprendre la relation entre les différentes parties de la séquence. Dans l'auto-attention, chaque élément de la séquence d'entrée est représenté par un vecteur, qui peut être considéré comme une collection de requête (Query), de clé (Key) et de valeur (Value).

Dans l'auto-attention, chaque requête est produite par points avec toutes les clés pour obtenir un poids d'attention qui représente la pertinence de la requête pour chaque clé. Ensuite, ces poids et valeurs d'attention sont pondérés et moyennés pour obtenir une représentation vectorielle pondérée, qui est la sortie de l'auto-attention.

L'avantage du mécanisme d'auto-attention est qu'il peut gérer de longues entrées de séquence car il n'a pas besoin de conserver toutes les informations historiques comme le réseau neuronal récurrent. L'auto-attention peut également apprendre des relations plus complexes, telles que les dépendances à longue portée, qui sont très importantes pour certaines tâches, telles que la génération de texte et la reconnaissance vocale.

Voici la formule du mécanisme d'auto-attention :

insérez la description de l'image ici

Supposons que la séquence d'entrée est X = [ x 1 , x 2 , . . . , xn ] X=[x_1, x_2, ..., x_n]X=[ x1,X2,... ,Xn] , dontxi x_iXjeIndique le iième dans la séquence d'entréeje éléments. Chaque vecteur d'entréexi x_iXjepeut être exprimé comme un ddvecteur de dimension d .

Tout d'abord, chaque vecteur d'entrée xi x_iXjeMappé sur trois vecteurs qi , ki , vi q_i, k_i, v_iqje,kje,vje, et leurs dimensions sont dd . Plus précisément, pour chaqueiimoi , avec :

qi = W qxi q_i = W_q x_iqje=OqXje

ki = W kxi k_i = W_k x_ikje=OkXje

vi = W vxi v_i = W_v x_ivje=OvXje

W q , W k , W v W_q, W_k, W_vOq,Ok,Ovsont respectivement trois matrices de poids apprenables.

Ensuite, calculez chaque vecteur de requête qi q_iqjeavec tous les vecteurs clés kj k_jkjeLe produit scalaire, puis normalisé par une fonction softmax pour obtenir le poids d'attention α i , j \alpha_{i,j}unje , je

α je , j = softmax ( qi T kj / d ) \alpha_{i,j}=\text{softmax}(q_i^T k_j/\sqrt{d})unje , je=softmax ( qjeTkje/d )

d \sqrt{d}d Il s'agit d'atténuer le problème d'une valeur numérique excessive qui peut être causée par l'opération de produit scalaire.

Enfin, utilisez le poids d'attention α i , j \alpha_{i,j}unje , jePour tous les vecteurs de valeur vj v_jvjeEffectuez une sommation pondérée pour obtenir chaque vecteur de requête qi q_iqjeLe vecteur de sortie correspondant oi o_ioje

oi = ∑ j = 1 n α je , jvj o_i = \sum_{j=1}^{n} \alpha_{i,j} v_joje=j = 1nunje , jevje

La sortie de l'auto-attention est tous les vecteurs de requête qi q_iqjeLe vecteur de sortie correspondant oi o_iojeLa collection de peut être exprimée comme une matrice O = [ o 1 , o 2 , . . . , on ] O=[o_1, o_2, ..., o_n]O=[ o1,o2,... ,on]

import torch
import torch.nn as nn

class SelfAttention(nn.Module):
    def __init__(self, input_dim, num_heads):
        super(SelfAttention, self).__init__()
        self.num_heads = num_heads
        self.q_linear = nn.Linear(input_dim, input_dim)
        self.k_linear = nn.Linear(input_dim, input_dim)
        self.v_linear = nn.Linear(input_dim, input_dim)
        self.output_linear = nn.Linear(input_dim, input_dim)

    def forward(self, x):
        # x shape: batch_size x seq_len x input_dim

        batch_size, seq_len, input_dim = x.size()

        # Project the input vectors to queries, keys, and values
        queries = self.q_linear(x).view(batch_size, seq_len, self.num_heads, input_dim // self.num_heads).transpose(1, 2)
        keys = self.k_linear(x).view(batch_size, seq_len, self.num_heads, input_dim // self.num_heads).transpose(1, 2)
        values = self.v_linear(x).view(batch_size, seq_len, self.num_heads, input_dim // self.num_heads).transpose(1, 2)

        # Compute the dot product of queries and keys
        dot_product = torch.matmul(queries, keys.transpose(-2, -1)) / (input_dim // self.num_heads) ** 0.5

        # Apply the softmax function to obtain attention weights
        attention_weights = torch.softmax(dot_product, dim=-1)

        # Compute the weighted sum of values
        weighted_sum = torch.matmul(attention_weights, values)

        # Reshape the output and apply a linear transformation
        weighted_sum = weighted_sum.transpose(1, 2).contiguous().view(batch_size, seq_len, input_dim)
        output = self.output_linear(weighted_sum)

        return output

Tout d'abord, nous définissons un modèle PyTorch appelé SelfAttention qui comprend quatre couches linéaires : q_linear, k_linear, v_linear et output_linear. Ces quatre couches linéaires entrent respectivement le vecteur xxx correspond àqqqkkkv vvecteur v et vecteur de sortie.

Dans la méthode directe, nous passons d'abord le vecteur d'entrée xxLa forme de x est interprétée comme (batch_size, seq_len, input_dim), où batch_size représente la taille du lot, seq_len représente la longueur de la séquence et input_dim représente la dimension du vecteur d'entrée.

Ensuite, nous saisirons le vecteur xxx est transmis respectivement aux couches linéaires q_linear, k_linear et v_linear, et leurs formes sont transformées en (batch_size, seq_len, num_heads, input_dim // num_heads). Ici, num_heads représente le nombre de têtes d'attention à utiliser et nous saisirons le vecteurxxx dansjjLa dimension d est divisée en num_heads sous-vecteurs, et un poids d'attention est calculé pour chaque sous-vecteur. De cette manière, la dimension de chaque sous-vecteur devient input_dim // num_heads.

Ensuite, après avoir transformé les formes des requêtes, des clés et des valeurs, nous devons transposer les requêtes, les clés et les valeurs dans la dimension num_heads, afin de pouvoir facilement changer leurs formes en (batch_size * num_heads, seq_len, input_dim // num_heads ), ce qui est pratique pour les calculs ultérieurs.

Ensuite, nous calculons le produit scalaire des requêtes et des clés et divisons le résultat par d modèle \sqrt{d_\text{modèle}}dmodèle . Ici, d modèle d_\text{modèle}dmodèleIndique le vecteur d'entrée xxLa dimension de x , c'est-à-dire input_dim. On divise pard modèle \sqrt{d_\text{modèle}}dmodèle C'est pour éviter le problème que le produit scalaire est trop grand ou trop petit.

Ensuite, nous effectuons l'opération softmax sur le résultat du produit scalaire dot_product pour obtenir le poids d'attention attention_weights. Ici, nous softmaxons la dernière dimension, c'est-à-dire calculons un poids d'attention pour chaque sous-vecteur.

Ensuite, nous avons pondéré et additionné les poids d'attention attention_weights et les valeurs pour obtenir le vecteur pondéré weighted_sum.

Enfin, nous changeons la forme du vecteur pondéré weighted_sum en (batch_size, seq_len, input_dim), puis le passons à la couche linéaire output_linear pour transformation afin d'obtenir la sortie vectorielle de sortie finale.

En résumé, ce code implémente une couche d'auto-attention avec un mécanisme d'auto-attention multi-tête qui prend en entrée un vecteur xxx correspond au vecteur de sortieyyy , et pour le vecteur d'entréexxChaque sous-vecteur dans x calcule un poids d'attention de sorte que différents sous-vecteurs soient pondérés différemment. De cette façon, nous pouvons mieux comprendre différentes informations dans le vecteur d'entrée et attribuer différentes informations à différents sous-vecteurs. Dans le même temps, le mécanisme d'auto-attention multi-têtes peut améliorer la capacité de représentation du modèle et faciliter la capture par le modèle des dépendances à longue distance.

code de localisation

Pourquoi utiliser le codage positionnel

Dans le modèle de transformateur, l'entrée est une rangée de phrases. Il est facile pour les humains de voir l'ordre de chaque mot dans la phrase, c'est-à-dire les informations de position, par exemple :

(1) Information de position absolue. a1 est le premier jeton, a2 est le deuxième jeton...
(2) Information de position relative. a2 est un chiffre derrière a1, a4 est à deux chiffres derrière a2...
(3) La distance entre différentes positions. a1 et a3 sont à deux places l'une de l'autre, a1 et a4 sont à trois places l'une de l'autre...

Mais c'est une chose très difficile pour la machine. L'auto-attention dans le transformateur peut apprendre la corrélation entre chaque mot de la phrase et prêter attention aux informations importantes, mais elle ne peut pas apprendre la signification de chaque mot. Les informations de localisation, donc nous devons ajouter des informations sur l'emplacement du jeton au modèle.

Le modèle Transformer abandonne RNN et CNN comme modèle de base de l'apprentissage des séquences. Nous savons que le réseau neuronal cyclique lui-même est une structure séquentielle, qui contient intrinsèquement les informations de position des mots dans la séquence. Lorsque la structure du réseau neuronal cyclique est supprimée et que Attention est complètement utilisé à la place, les informations sur l'ordre des mots seront perdues et le modèle n'aura aucun moyen de connaître les informations de position relative et absolue de chaque mot dans la phrase. Par conséquent, il est nécessaire d'ajouter le signal d'ordre des mots au vecteur de mots pour aider le modèle à apprendre cette information.Le codage positionnel est la méthode utilisée pour résoudre ce problème.

Le codage positionnel (Positional Encoding) est une méthode de re-représentation de chaque mot dans la séquence avec les informations de position du mot, de sorte que les données d'entrée portent les informations de position, afin que le modèle puisse découvrir les caractéristiques de position. Comme mentionné ci-dessus, le modèle Transformer lui-même n'a pas la capacité d'apprendre des informations sur l'ordre des mots comme RNN et doit fournir activement des informations sur l'ordre des mots au modèle. Ensuite, l'entrée d'origine du modèle est un vecteur de mots sans informations sur l'ordre des mots, et le codage de position doit combiner les informations sur l'ordre des mots et le vecteur de mots pour former une nouvelle entrée de représentation dans le modèle, de sorte que le modèle ait la capacité d'apprendre l'ordre des mots. information.

Calcul des codes de position

Supposons que l'entrée représente X ∈ R n × d X\in R^{n \times d}XRn × d contient une représentation d'intégration à d dimensions de n jetons dans une séquence. L'encodage de position utilise une matrice d'intégration de positionP ∈ R n × d P\in R^{n\times d}PRn × d sortieX + P X+PX+P , matriceiiLa ligne i représente le code de position d'un jeton :

Pos ( je , 2 j ) = sin ⁡ ( je 1000 0 2 j / modèle d ) , Pos ( je , 2 j + 1 ) = cos ⁡ ( je 1000 0 2 j / modèle d ) , \ begin {aligné} \ text{Pos}(i, 2j) &= \sin\left(\frac{i}{10000^{2j/d_{\text{model}}}}\right), \\ \text{Pos}(i , 2j+1) &= \cos\left(\frac{i}{10000^{2j/d_{\text{modèle}}}}\right), \end{aligned}Pos ( je ,2j ) _Pos ( je ,2 j+1 )=péché(1000 02 j / jmodèleje),=parce que(1000 02 j / jmodèleje),

Implémentation de l'encodage de position

#@save
class PositionalEncoding(nn.Block):
    """位置编码"""
    def __init__(self, num_hiddens, dropout, max_len=1000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(dropout)
        # 创建一个足够长的P
        self.P = np.zeros((1, max_len, num_hiddens))
        X = np.arange(max_len).reshape(-1, 1) / np.power(
            10000, np.arange(0, num_hiddens, 2) / num_hiddens)
        self.P[:, :, 0::2] = np.sin(X)
        self.P[:, :, 1::2] = np.cos(X)

    def forward(self, X):
        X = X + self.P[:, :X.shape[1], :].as_in_ctx(X.ctx)
        return self.dropout(X)

code de position absolue

Vous vous demandez peut-être comment une combinaison de sinus et de cosinus peut représenter une position/un ordre ?

C'est en fait très simple, en supposant que vous vouliez représenter un nombre au format binaire :
insérez la description de l'image ici
2 i données à la position i alternent une fois. Les données 2^i à la ième position sont alternées une fois.2 en position iLes données i sont alternées une fois.

La figure ci-dessous utilise le codage de la fonction sinusoïdale, la longueur de la phrase est de 50 (ordonnée) et la dimension du vecteur de codage est de 128 (abscisse). On peut voir que la fréquence alternative ralentit progressivement de gauche à droite. Comme on peut le voir sur la figure ci-dessous, chaque ligne est le code de position d'un élément lexical, on voit bien que les informations de position du premier mot et du dernier mot sont complètement différentes.
insérez la description de l'image ici

informations relatives à la position relative

En plus de capturer des informations de position absolue, le codage de position décrit ci-dessus permet également au modèle d'apprendre des informations de position relatives dans la séquence d'entrée. En effet, pour tout décalage de position déterminé aaun,经说i + un i+aje+Le codage de position à a peut projeter linéairement la position iiLe code de position en i est représenté.

L'interprétation mathématique de cette projection est, soit wi = 1 / 1000 0 2 j / d w_i=1/10000^{2j/d}wje=1/1000 02 j / j , pour tout décalage de position définiaaa , toute paire dans( pi , 2 j , pi , 2 j + 1 ) (p_{i,2j},p_{i,2j+1})( pje , 2 j,pje , 2 j + 1) peut être projeté linéairement sur( pi + a , 2 j , pi + a , 2 j + 1 ) (p_{i+a,2j},p_{i+a,2j+1})( pje + un , 2 j,pje + une , 2 j + 1)

insérez la description de l'image ici

Architecture du transformateur

Modèle

Transformer est un modèle de réseau neuronal basé sur un mécanisme d'auto-attention pour le traitement de tâches séquence à séquence (séquence à séquence), telles que la traduction automatique, la synthèse de texte, etc. Il a été proposé par Google et est considéré comme l'un des modèles les plus avancés dans le domaine du traitement du langage naturel.

Le composant le plus important du modèle Transformer est le mécanisme d'auto-attention, qui peut capturer la relation entre différentes positions dans la séquence d'entrée, améliorant ainsi les performances du modèle lors du traitement de longues séquences. En outre, Transformer utilise également des techniques telles que la connexion résiduelle et la normalisation des couches pour rendre la formation de modèle plus stable et efficace.

Le modèle Transformer comprend principalement les pièces suivantes :

Couche d'intégration d'entrée : mappe les mots de la séquence d'entrée sur un espace vectoriel continu pour faciliter le traitement ultérieur.

Couche de codage de position : afin de prendre en compte les informations de position relative entre différentes positions dans la séquence, chaque position dans la séquence d'entrée doit être codée pour obtenir un vecteur de codage de position.

Couche d'auto-attention : effectue des calculs d'auto-attention pour chaque position dans la séquence d'entrée, capturant ainsi les dépendances entre différentes positions.

Couche réseau d'anticipation : un simple processus de réseau d'anticipation est effectué sur le vecteur de sortie d'auto-attention de chaque position, améliorant ainsi la capacité non linéaire du modèle.

Couche de sortie : transformez linéairement le vecteur de sortie de la dernière couche et utilisez la fonction softmax pour obtenir la distribution de probabilité de chaque mot de sortie.

Pendant le processus de formation, le modèle Transformer utilise un mécanisme d'attention et un mécanisme de masque pour éviter la fuite d'informations futures, et utilise une fonction de perte d'entropie croisée pour évaluer les performances du modèle. Pendant l'inférence, le modèle Transformer utilise l'algorithme Beam Search pour générer la séquence de sortie optimale.

En bref, le modèle Transformer fonctionne bien dans les tâches séquence à séquence, a une bonne évolutivité et applicabilité, et est devenu l'une des directions de recherche importantes dans le domaine du traitement du langage naturel.

insérez la description de l'image ici
L'architecture de Transformer est décrite dans la figure. D'un point de vue macro, l'encodeur de Transformer est composé de plusieurs couches identiques, et chaque couche a deux sous-couches (les sous-couches sont représentées comme sous-couche sous-couchesous - couche ) . _ _ La première sous-couche est un pool d'auto-attention multi-tête ; la deuxième sous-couche est un réseau à réaction en fonction de la position. Plus précisément, lors du calcul de l'auto-attention de l'encodeur, la requête, la clé et la valeur proviennent toutes de la sortie de la couche d'encodeur précédente. Chaque sous-couche utilise une connexion résiduelle.

Le décodeur Transformer est également superposé par plusieurs couches identiques, et des connexions résiduelles et une normalisation de couche sont utilisées dans les couches. En plus des deux sous-couches décrites dans l'encodeur, le décodeur insère une troisième sous-couche entre ces deux, appelée couche d'attention de l'encodeur-décodeur. Dans l'attention de l'encodeur-décodeur, les requêtes proviennent de la sortie de la couche de décodeur précédente, tandis que les clés et les valeurs proviennent de la sortie de l'ensemble de l'encodeur. Dans l'auto-attention du décodeur, les requêtes, les clés et les valeurs sont toutes dérivées de la sortie de la couche de décodeur précédente. Cependant, chaque position dans le décodeur ne peut considérer que toutes les positions avant cette position. Cette attention masquée préserve les propriétés auto-régressives, garantissant que les prédictions ne dépendent que des jetons de sortie générés.

Réseaux d'anticipation basés sur la position

Dans un premier temps, nous implémentons la partie suivante :
insérez la description de l'image ici

Le réseau d'anticipation basé sur la position utilise le même perceptron multicouche (MLP) pour transformer la représentation de toutes les positions de la séquence, c'est pourquoi le réseau d'anticipation est appelé positionwise. Dans l'implémentation ci-dessous, une entrée X de forme (taille du lot, nombre de pas de temps ou longueur de la séquence, nombre d'unités cachées ou dimension de la caractéristique) sera transformée par un perceptron à deux couches en forme (taille du lot, nombre de pas de temps, ffn_num_outputs) Le tenseur de sortie, c'est-à-dire la dernière dimension, est modifié.

#@save
class PositionWiseFFN(nn.Module):
    """基于位置的前馈网络"""
    def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs,
                 **kwargs):
        super(PositionWiseFFN, self).__init__(**kwargs)
        self.dense1 = nn.Linear(ffn_num_input, ffn_num_hiddens)
        self.relu = nn.ReLU()
        self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs)

    def forward(self, X):
        return self.dense2(self.relu(self.dense1(X)))

L'exemple ci-dessous montre que la modification de la taille de la dimension la plus interne d'un tenseur modifie la taille de sortie d'un réseau d'anticipation basé sur la position. Parce que le même perceptron multicouche est utilisé pour transformer l'entrée à toutes les positions, lorsque l'entrée à toutes ces positions est la même, leur sortie est également la même.

ffn = PositionWiseFFN(4, 4, 8)
ffn.eval()
ffn(torch.ones((2, 3, 4)))[0]

insérez la description de l'image ici

Connexions résiduelles et normalisation des couches

Concentrons-nous maintenant sur les composants d'addition et de normalisation (addition et norme) dans le graphe. Comme mentionné au début de cette section, il s'agit de connexions résiduelles suivies d'une normalisation de couche. Les deux sont essentiels pour créer des architectures profondes efficaces.

La normalisation de couche (Layer Normalization) est une méthode de normalisation couramment utilisée dans les réseaux de neurones, et son but est d'améliorer l'efficacité de la formation et les performances des réseaux de neurones. Contrairement à la normalisation par lots (Batch Normalization), la normalisation des couches consiste à normaliser un seul échantillon, plutôt qu'à normaliser un lot d'échantillons.

Plus précisément, pour un avec ddEntrée x sur d entités = ( x 1 , x 2 , . . . , xd ) x=(x_1,x_2,...,x_d)X=( x1,X2,... ,X) , la normalisation de couche normalise chaque entité de sorte que la moyenne de chaque entité soit 0 et l'écart type soit 1, à savoir :
x ^ i = xi − μ σ 2 + ϵ \hat{x}_i = \frac {x_i - \mu }{\sqrt{\sigma^2+\epsilon}}X^je=p2+ϵ Xjem
Parmi eux, μ \muμ estxi x_iXjeLa moyenne de σ 2 \sigma^2p2 estxi x_iXjeLa variance de , ϵ \epsilonϵ est une petite constante (habituellement1 0 − 5 10^{-5}1 05 ), utilisé pour éviter la division par 0. La normalisation des couches met ensuite à l'échelle et traduit chaque entité :
yi = γ x ^ i + β y_i = \gamma \hat{x}_i + \betayje=cX^je+β
γ \gammacb \betaβ est un paramètre apprenable utilisé pour mettre à l'échelle et traduire chaque fonctionnalité. De cette manière, la normalisation des couches peut faire en sorte que chaque entité ait la même échelle et la même translation, améliorant ainsi les performances de généralisation du modèle.

Bien que la normalisation par lots soit largement utilisée dans la vision par ordinateur, dans les tâches de traitement du langage naturel (où l'entrée est généralement une séquence de longueur variable), la normalisation par lots n'est généralement pas aussi bonne que la normalisation des couches.

Le code suivant compare les effets de la normalisation des calques et de la normalisation des lots pour différentes dimensions.

ln = nn.LayerNorm(2)
bn = nn.BatchNorm1d(2)
X = torch.tensor([[1, 2], [2, 3]], dtype=torch.float32)
# 在训练模式下计算X的均值和方差
print('layer norm:', ln(X), '\nbatch norm:', bn(X))

insérez la description de l'image ici
La classe AddNorm peut désormais être implémentée à l'aide de connexions résiduelles et de la normalisation de couche. La retraite est également utilisée comme méthode de régularisation. C'est-à-dire que la partie suivante est réalisée :
insérez la description de l'image ici

#@save
class AddNorm(nn.Module):
    """残差连接后进行层规范化"""
    def __init__(self, normalized_shape, dropout, **kwargs):
        super(AddNorm, self).__init__(**kwargs)
        self.dropout = nn.Dropout(dropout)
        self.ln = nn.LayerNorm(normalized_shape)

    def forward(self, X, Y):
        return self.ln(self.dropout(Y) + X)

attention multi-têtes

Réalisez les parties suivantes :
insérez la description de l'image ici
Cette partie du code a été expliquée, voir pour plus de détails : https://blog.csdn.net/qq_51957239/article/details/129732592?spm=1001.2014.3001.5502

def transpose_qkv(X, num_heads):
    """Transposition for parallel computation of multiple attention heads.

    Defined in :numref:`sec_multihead-attention`"""
    # Shape of input `X`:
    # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`).
    # Shape of output `X`:
    # (`batch_size`, no. of queries or key-value pairs, `num_heads`,
    # `num_hiddens` / `num_heads`)
    X = X.reshape(X.shape[0], X.shape[1], num_heads, -1)

    # Shape of output `X`:
    # (`batch_size`, `num_heads`, no. of queries or key-value pairs,
    # `num_hiddens` / `num_heads`)
    X = X.permute(0, 2, 1, 3)

    # Shape of `output`:
    # (`batch_size` * `num_heads`, no. of queries or key-value pairs,
    # `num_hiddens` / `num_heads`)
    return X.reshape(-1, X.shape[2], X.shape[3])


def transpose_output(X, num_heads):
    """Reverse the operation of `transpose_qkv`.

    Defined in :numref:`sec_multihead-attention`"""
    X = X.reshape(-1, num_heads, X.shape[1], X.shape[2])
    X = X.permute(0, 2, 1, 3)
    return X.reshape(X.shape[0], X.shape[1], -1)
class MultiHeadAttention(nn.Module):
    """Multi-head attention.

    Defined in :numref:`sec_multihead-attention`"""
    def __init__(self, key_size, query_size, value_size, num_hiddens,
                 num_heads, dropout, bias=False, **kwargs):
        super(MultiHeadAttention, self).__init__(**kwargs)
        self.num_heads = num_heads
        self.attention = d2l.DotProductAttention(dropout)
        self.W_q = nn.Linear(query_size, num_hiddens, bias=bias)
        self.W_k = nn.Linear(key_size, num_hiddens, bias=bias)
        self.W_v = nn.Linear(value_size, num_hiddens, bias=bias)
        self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias)

    def forward(self, queries, keys, values, valid_lens):
        # Shape of `queries`, `keys`, or `values`:
        # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`)
        # Shape of `valid_lens`:
        # (`batch_size`,) or (`batch_size`, no. of queries)
        # After transposing, shape of output `queries`, `keys`, or `values`:
        # (`batch_size` * `num_heads`, no. of queries or key-value pairs,
        # `num_hiddens` / `num_heads`)
        queries = transpose_qkv(self.W_q(queries), self.num_heads)
        keys = transpose_qkv(self.W_k(keys), self.num_heads)
        values = transpose_qkv(self.W_v(values), self.num_heads)

        if valid_lens is not None:
            # On axis 0, copy the first item (scalar or vector) for
            # `num_heads` times, then copy the next item, and so on
            valid_lens = torch.repeat_interleave(
                valid_lens, repeats=self.num_heads, dim=0)

        # Shape of `output`: (`batch_size` * `num_heads`, no. of queries,
        # `num_hiddens` / `num_heads`)
        output = self.attention(queries, keys, values, valid_lens)

        # Shape of `output_concat`:
        # (`batch_size`, no. of queries, `num_hiddens`)
        output_concat = transpose_output(output, self.num_heads)
        return self.W_o(output_concat)

Encodeur

bloc codeur

Implémentez les parties suivantes :
insérez la description de l'image ici

#@save
class EncoderBlock(nn.Module):
    """Transformer编码器块"""
    def __init__(self, key_size, query_size, value_size, num_hiddens,
                 norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
                 dropout, use_bias=False, **kwargs):
        super(EncoderBlock, self).__init__(**kwargs)
        self.attention = d2l.MultiHeadAttention(
            key_size, query_size, value_size, num_hiddens, num_heads, dropout,
            use_bias)
        self.addnorm1 = AddNorm(norm_shape, dropout)
        self.ffn = PositionWiseFFN(
            ffn_num_input, ffn_num_hiddens, num_hiddens)
        self.addnorm2 = AddNorm(norm_shape, dropout)

    def forward(self, X, valid_lens):
        Y = self.addnorm1(X, self.attention(X, X, X, valid_lens))#自注意力
        return self.addnorm2(Y, self.ffn(Y))

Ce code définit une classe EncoderBlock pour un bloc d'encodeur Transformer. Dans la fonction __init__, il définit les sous-couches suivantes :

self.attention : une couche d'attention multi-tête qui utilise l'entrée comme requête, clé et valeur. Il transmet l'entrée sous forme de trois arguments à d2l.MultiHeadAttention et utilise valid_lens (un tenseur 1D où chaque élément représente la longueur valide de la séquence correspondante) pour masquer le remplissage non valide.
self.addnorm1 : une couche de normalisation de couche qui additionne l'entrée avec la sortie de l'attention multi-tête.
self.ffn : Un réseau Feed-Forward en fonction de la position (Réseau Feed-Forward en fonction de la position), utilisé pour traiter la sortie de la couche précédente.
self.addnorm2 : une autre couche de normalisation de couche qui additionne la sortie du réseau de neurones à réaction par élément positionnel avec la sortie de la couche précédente.

Dans la fonction directe, l'entrée X et la longueur effective valid_lens sont transmises à la couche d'attention multi-têtes pour traitement, et les résultats de sortie sont additionnés et traités via la couche de normalisation de couche et la couche de réseau neuronal à action directe par élément de position. En fin de compte, cette fonction renvoie la sortie d'anticipation positionnelle par élément de la couche de réseau neuronal.

Encodeur

Implémentez les parties suivantes :
insérez la description de l'image ici

#@save
class TransformerEncoder(d2l.Encoder):
    """Transformer编码器"""
    def __init__(self, vocab_size, key_size, query_size, value_size,
                 num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
                 num_heads, num_layers, dropout, use_bias=False, **kwargs):
        super(TransformerEncoder, self).__init__(**kwargs)
        self.num_hiddens = num_hiddens
        self.embedding = nn.Embedding(vocab_size, num_hiddens)
        self.pos_encoding = PositionalEncoding(num_hiddens, dropout)
        self.blks = nn.Sequential()
        for i in range(num_layers):
            self.blks.add_module("block"+str(i),
                EncoderBlock(key_size, query_size, value_size, num_hiddens,
                             norm_shape, ffn_num_input, ffn_num_hiddens,
                             num_heads, dropout, use_bias))

    def forward(self, X, valid_lens, *args):
        # 因为位置编码值在-1和1之间,
        # 因此嵌入值乘以嵌入维度的平方根进行缩放,
        # 然后再与位置编码相加。
        X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
        self.attention_weights = [None] * len(self.blks)
        for i, blk in enumerate(self.blks):
            X = blk(X, valid_lens)
            self.attention_weights[
                i] = blk.attention.attention.attention_weights
        return X

Plus précisément, la classe TransformerEncoder hérite de la classe d2l.Encoder, qui définit une couche d'incorporation de mots et une couche séquentielle composée de plusieurs EncoderBlocks. Chaque EncoderBlock se compose de deux parties : une couche d'attention multi-têtes et un réseau d'anticipation de position, qui sont utilisés pour traiter l'entrée.

Dans la fonction directe, l'entrée est d'abord passée à travers la couche d'intégration pour le codage de vecteur de mot, puis multipliée par la racine carrée de la dimension d'intégration pour la mise à l'échelle, et enfin le codage de position est ajouté. Le codage positionnel est implémenté par la classe PositionalEncoding, qui est utilisée pour coder chaque position dans la séquence afin d'aider le modèle à apprendre la relation de position relative des éléments dans la séquence.

Ensuite, l'entrée est traitée par plusieurs EncoderBlocks, et la sortie de chaque EncoderBlock est utilisée comme entrée du prochain EncoderBlock. Dans chaque EncoderBlock, l'entrée passe d'abord par la couche d'attention multi-têtes pour le calcul de l'auto-attention afin d'obtenir la matrice de pondération de l'attention, puis elle est traitée par connexion résiduelle et normalisation de couche avant d'être entrée dans le réseau d'anticipation de position pour traitement. En fin de compte, la fonction renvoie la sortie du dernier EncoderBlock comme sortie de l'encodeur entier.

Notez que dans chaque EncoderBlock, les poids d'attention de la couche d'attention multi-têtes seront enregistrés dans la liste self.attention_weights, qui peut être utilisée pour la visualisation et le débogage.

La forme de la sortie de l'encodeur du transformateur est (taille du lot, nombre de pas de temps, num_hiddens)

décodeur

bloc décodeur

Implémentez les parties suivantes :
insérez la description de l'image ici

class DecoderBlock(nn.Module):
    """解码器中第i个块"""
    def __init__(self, key_size, query_size, value_size, num_hiddens,
                 norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
                 dropout, i, **kwargs):
        super(DecoderBlock, self).__init__(**kwargs)
        self.i = i
        self.attention1 = d2l.MultiHeadAttention(
            key_size, query_size, value_size, num_hiddens, num_heads, dropout)
        self.addnorm1 = AddNorm(norm_shape, dropout)
        self.attention2 = d2l.MultiHeadAttention(
            key_size, query_size, value_size, num_hiddens, num_heads, dropout)
        self.addnorm2 = AddNorm(norm_shape, dropout)
        self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens,
                                   num_hiddens)
        self.addnorm3 = AddNorm(norm_shape, dropout)

    def forward(self, X, state):
        enc_outputs, enc_valid_lens = state[0], state[1]
        # 训练阶段,输出序列的所有词元都在同一时间处理,
        # 因此state[2][self.i]初始化为None。
        # 预测阶段,输出序列是通过词元一个接着一个解码的,
        # 因此state[2][self.i]包含着直到当前时间步第i个块解码的输出表示
        if state[2][self.i] is None:
            key_values = X
        else:
            key_values = torch.cat((state[2][self.i], X), axis=1)
        state[2][self.i] = key_values
        if self.training:
            batch_size, num_steps, _ = X.shape
            # dec_valid_lens的开头:(batch_size,num_steps),
            # 其中每一行是[1,2,...,num_steps]
            dec_valid_lens = torch.arange(
                1, num_steps + 1, device=X.device).repeat(batch_size, 1)
        else:
            dec_valid_lens = None

        # 自注意力
        X2 = self.attention1(X, key_values, key_values, dec_valid_lens)
        Y = self.addnorm1(X, X2)
        # 编码器-解码器注意力。
        # enc_outputs的开头:(batch_size,num_steps,num_hiddens)
        Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)
        Z = self.addnorm2(Y, Y2)
        return self.addnorm3(Z, self.ffn(Z)), state

Ceci est un bloc dans un décodeur. Chaque bloc du décodeur reçoit en entrée l'entrée du décodeur (la sortie de l'encodeur ou la sortie du bloc précédent) et l'état actuel (la sortie du bloc avant le décodeur ou la sortie de l'encodeur) et sort le bloc actuel Sortie et état mis à jour.

Le bloc est structuré comme suit :

Premièrement, l'entrée du bloc actuel est concaténée avec la sortie historique stockée dans l'état pour former les clés et les valeurs dont l'encodeur-décodeur se soucie. Pendant l'apprentissage, les jetons de toutes les séquences de sortie sont traités en même temps, de sorte que la sortie historique dans l'état est initialisée sur Aucun ; pendant l'inférence, la séquence de sortie est décodée jeton par jeton, de sorte que la sortie historique dans l'état contient le Tout sortie historique.

Ensuite, l'entrée est traitée à l'aide de l'auto-attention multi-tête.

Ajoutez la sortie de l'auto-attention à l'entrée et effectuez la normalisation de la couche.

Le résultat est traité en utilisant l'attention du codeur-décodeur. Ceci est calculé comme une "paire clé-valeur" composée de la sortie de l'encodeur et de la sortie d'auto-attention du bloc actuel.

La sortie de l'attention du codeur-décodeur est ajoutée à la sortie de l'auto-attention, et la normalisation de couche est effectuée.
Enfin, le résultat est traité à l'aide d'un réseau de neurones à anticipation, et la sommation et la normalisation sont à nouveau effectuées. La sortie et l'état mis à jour sont renvoyés.

décodeur

Implémentez les parties suivantes :
insérez la description de l'image ici

class TransformerDecoder(d2l.AttentionDecoder):
    def __init__(self, vocab_size, key_size, query_size, value_size,
                 num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
                 num_heads, num_layers, dropout, **kwargs):
        super(TransformerDecoder, self).__init__(**kwargs)
        self.num_hiddens = num_hiddens
        self.num_layers = num_layers
        self.embedding = nn.Embedding(vocab_size, num_hiddens)
        self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
        self.blks = nn.Sequential()
        for i in range(num_layers):
            self.blks.add_module("block"+str(i),
                DecoderBlock(key_size, query_size, value_size, num_hiddens,
                             norm_shape, ffn_num_input, ffn_num_hiddens,
                             num_heads, dropout, i))
        self.dense = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, enc_valid_lens, *args):
        return [enc_outputs, enc_valid_lens, [None] * self.num_layers]

    def forward(self, X, state):
        X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
        self._attention_weights = [[None] * len(self.blks) for _ in range (2)]
        for i, blk in enumerate(self.blks):
            X, state = blk(X, state)
            # 解码器自注意力权重
            self._attention_weights[0][
                i] = blk.attention1.attention.attention_weights
            # “编码器-解码器”自注意力权重
            self._attention_weights[1][
                i] = blk.attention2.attention.attention_weights
        return self.dense(X), state

    @property
    def attention_weights(self):
        return self._attention_weights

Voici une explication détaillée de chaque partie :

Constructeur init ()
vocab_size : Taille du vocabulaire, c'est-à-dire le nombre de mots distincts dans le vocabulaire.
key_size, query_size, value_size : tailles de dimension de la clé, de la requête et de la valeur dans le modèle Transformer.
num_hiddens : taille de la dimension des unités masquées.
norm_shape : la forme de la couche de normalisation.
ffn_num_input, ffn_num_hiddens : les dimensions de la couche d'entrée et masquée de la couche d'anticipation.
num_heads : nombre de têtes dans le mécanisme d'attention multi-têtes.
num_layers : le nombre de couches Transformer dans le décodeur.
abandon : la probabilité de la couche d'abandon.
**kwargs : paramètres supplémentaires.

Fonction init_state() : utilisée pour initialiser l'état du décodeur, renvoie une liste, le premier élément est la sortie de l'encodeur, le deuxième élément est la longueur effective de l'encodeur, et le troisième élément est une liste contenant chaque information d'état de la couche.

Fonction forward() : cette fonction prend l'entrée X et l'état et génère la sortie du décodeur et l'état du décodeur. Parmi eux, X est l'entrée du décodeur et l'état contient les informations d'état du décodeur. La fonction passe d'abord l'entrée X à travers la couche d'intégration et la couche de codage de position pour obtenir la représentation d'intégration de position X. Ensuite, pour chaque couche dans le décodeur, X et l'état sont entrés dans le bloc décodeur Transformer pour traitement, ce qui donne la sortie X et les nouvelles informations d'état du décodeur. Pendant le traitement, les poids d'auto-attention du décodeur et les poids d'auto-attention "encodeur-décodeur" sont enregistrés, stockés dans les première et deuxième sous-listes de la liste _attention_weights, respectivement. Enfin, entrez X dans la couche entièrement connectée pour obtenir la sortie du décodeur.

Fonction attention_weights() : Cette fonction renvoie une liste de _attention_weights. Cette liste contient les poids d'auto-attention de chaque couche de décodeur et les poids d'auto-attention "encodeur-décodeur".

Dans l'ensemble, ce code définit une classe de décodeur Transformer et fournit des fonctions pour initialiser l'état, la propagation vers l'avant et obtenir des poids d'attention.

Pratique : Traduction automatique

base de données

#@save
d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip',
                           '94646ad1522d915e7b0f9296181140edcf86a4f5')

#@save
def read_data_nmt():
    """载入“英语-法语”数据集"""
    data_dir = d2l.download_extract('fra-eng')
    with open(os.path.join(data_dir, 'fra.txt'), 'r',
             encoding='utf-8') as f:
        return f.read()

raw_text = read_data_nmt()
print(raw_text[:75])

prétraitement des données

#@save
def preprocess_nmt(text):
    """预处理“英语-法语”数据集"""
    def no_space(char, prev_char):
        return char in set(',.!?') and prev_char != ' '

    # 使用空格替换不间断空格
    # 使用小写字母替换大写字母
    text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()
    # 在单词和标点符号之间插入空格
    out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char
           for i, char in enumerate(text)]
    return ''.join(out)

text = preprocess_nmt(raw_text)
print(text[:80])
#@save
def tokenize_nmt(text, num_examples=None):
    """词元化“英语-法语”数据数据集"""
    source, target = [], []
    for i, line in enumerate(text.split('\n')):
        if num_examples and i > num_examples:
            break
        parts = line.split('\t')
        if len(parts) == 2:
            source.append(parts[0].split(' '))
            target.append(parts[1].split(' '))
    return source, target

source, target = tokenize_nmt(text)
source[:]
#@save
def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist):
    """绘制列表长度对的直方图"""
    d2l.set_figsize()
    _, _, patches = d2l.plt.hist(
        [[len(l) for l in xlist], [len(l) for l in ylist]])
    d2l.plt.xlabel(xlabel)
    d2l.plt.ylabel(ylabel)
    for patch in patches[1].patches:
        patch.set_hatch('/')
    d2l.plt.legend(legend)

show_list_len_pair_hist(['source', 'target'], '# tokens per sequence',
                        'count', source, target);
import collections
class Vocab:
    """Vocabulary for text."""
    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
        """Defined in :numref:`sec_text_preprocessing`"""
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []
        # Sort according to frequencies
        counter = count_corpus(tokens)
        self._token_freqs = sorted(counter.items(), key=lambda x: x[1],
                                   reverse=True)
        # The index for the unknown token is 0
        self.idx_to_token = ['<unk>'] + reserved_tokens
        self.token_to_idx = {
    
    token: idx
                             for idx, token in enumerate(self.idx_to_token)}
        for token, freq in self._token_freqs:
            if freq < min_freq:
                break
            if token not in self.token_to_idx:
                self.idx_to_token.append(token)
                self.token_to_idx[token] = len(self.idx_to_token) - 1

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self, indices):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]

    @property
    def unk(self):  # Index for the unknown token
        return 0

    @property
    def token_freqs(self):  # Index for the unknown token
        return self._token_freqs

def count_corpus(tokens):
    """Count token frequencies.

    Defined in :numref:`sec_text_preprocessing`"""
    # Here `tokens` is a 1D list or 2D list
    if len(tokens) == 0 or isinstance(tokens[0], list):
        # Flatten a list of token lists into a list of tokens
        tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)
#@save
def truncate_pad(line, num_steps, padding_token):
    """截断或填充文本序列"""
    if len(line) > num_steps:
        return line[:num_steps]  # 截断
    return line + [padding_token] * (num_steps - len(line))  # 填充

truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>'])
#@save
def build_array_nmt(lines, vocab, num_steps):
    """将机器翻译的文本序列转换成小批量"""
    lines = [vocab[l] for l in lines]#token to id
    lines = [l + [vocab['<eos>']] for l in lines]# 加上eos代表结束
    array = torch.tensor([truncate_pad(
        l, num_steps, vocab['<pad>']) for l in lines])# 转换为数组
    valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)#有效长度
    return array, valid_len
#@save
from torch.utils import data
def load_array(data_arrays, batch_size, is_train=True):
    """Construct a PyTorch data iterator.

    Defined in :numref:`sec_linear_concise`"""
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)

def load_data_nmt(batch_size, num_steps, num_examples=600):
    """返回翻译数据集的迭代器和词表"""
    text = preprocess_nmt(read_data_nmt())
    source, target = tokenize_nmt(text, num_examples)
    src_vocab = Vocab(source, min_freq=2,
                          reserved_tokens=['<pad>', '<bos>', '<eos>'])
    tgt_vocab = Vocab(target, min_freq=2,
                          reserved_tokens=['<pad>', '<bos>', '<eos>'])
    src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)
    tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps)
    data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)
    data_iter = load_array(data_arrays, batch_size)
    return data_iter, src_vocab, tgt_vocab

définition du modèle

class EncoderDecoder(nn.Module):
    """The base class for the encoder-decoder architecture.

    Defined in :numref:`sec_encoder-decoder`"""
    def __init__(self, encoder, decoder, **kwargs):
        super(EncoderDecoder, self).__init__(**kwargs)
        self.encoder = encoder
        self.decoder = decoder

    def forward(self, enc_X, dec_X, *args):
        enc_outputs = self.encoder(enc_X, *args)
        dec_state = self.decoder.init_state(enc_outputs, *args)
        return self.decoder(dec_X, dec_state)
encoder = TransformerEncoder(
    len(src_vocab), key_size, query_size, value_size, num_hiddens,
    norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
    num_layers, dropout)
decoder = TransformerDecoder(
    len(tgt_vocab), key_size, query_size, value_size, num_hiddens,
    norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
    num_layers, dropout)
net = EncoderDecoder(encoder, decoder)

réglage d'hyperparamètre

num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.1, 64, 10
lr, num_epochs, device = 0.005, 200, d2l.try_gpu()
ffn_num_input, ffn_num_hiddens, num_heads = 32, 64, 4
key_size, query_size, value_size = 32, 32, 32
norm_shape = [32]

train_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size, num_steps)

former

def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
    """Train a model for sequence to sequence.

    Defined in :numref:`sec_seq2seq_decoder`"""
    def xavier_init_weights(m):
        if type(m) == nn.Linear:
            nn.init.xavier_uniform_(m.weight)
        if type(m) == nn.GRU:
            for param in m._flat_weights_names:
                if "weight" in param:
                    nn.init.xavier_uniform_(m._parameters[param])
    net.apply(xavier_init_weights)
    net.to(device)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr)
    loss = MaskedSoftmaxCELoss()
    net.train()
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[10, num_epochs])
    for epoch in range(num_epochs):
        timer = d2l.Timer()
        metric = d2l.Accumulator(2)  # Sum of training loss, no. of tokens
        for batch in data_iter:
            optimizer.zero_grad()
            X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]
            bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],
                               device=device).reshape(-1, 1)
            dec_input = d2l.concat([bos, Y[:, :-1]], 1)  # Teacher forcing
            Y_hat, _ = net(X, dec_input, X_valid_len)
            l = loss(Y_hat, Y, Y_valid_len)
            l.sum().backward()  # Make the loss scalar for `backward`
            d2l.grad_clipping(net, 1)
            num_tokens = Y_valid_len.sum()
            optimizer.step()
            with torch.no_grad():
                metric.add(l.sum(), num_tokens)
        if (epoch + 1) % 10 == 0:
            animator.add(epoch + 1, (metric[0] / metric[1],))
    print(f'loss {
      
      metric[0] / metric[1]:.3f}, {
      
      metric[1] / timer.stop():.1f} '
          f'tokens/sec on {
      
      str(device)}')
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

insérez la description de l'image ici

prédire

def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
                    device, save_attention_weights=False):
    """Predict for sequence to sequence.

    Defined in :numref:`sec_seq2seq_training`"""
    # Set `net` to eval mode for inference
    net.eval()
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
        src_vocab['<eos>']]
    enc_valid_len = torch.tensor([len(src_tokens)], device=device)
    src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
    # Add the batch axis
    enc_X = torch.unsqueeze(
        torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
    enc_outputs = net.encoder(enc_X, enc_valid_len)
    dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
    # Add the batch axis
    dec_X = torch.unsqueeze(torch.tensor(
        [tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
    output_seq, attention_weight_seq = [], []
    for _ in range(num_steps):
        Y, dec_state = net.decoder(dec_X, dec_state)
        # We use the token with the highest prediction likelihood as the input
        # of the decoder at the next time step
        dec_X = Y.argmax(dim=2)
        pred = dec_X.squeeze(dim=0).type(torch.int32).item()
        # Save attention weights (to be covered later)
        if save_attention_weights:
            attention_weight_seq.append(net.decoder.attention_weights)
        # Once the end-of-sequence token is predicted, the generation of the
        # output sequence is complete
        if pred == tgt_vocab['<eos>']:
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq
engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation, dec_attention_weight_seq = d2l.predict_seq2seq(
        net, eng, src_vocab, tgt_vocab, num_steps, device, True)
    print(f'{
      
      eng} => {
      
      translation}, ',
          f'bleu {
      
      d2l.bleu(translation, fra, k=2):.3f}')

insérez la description de l'image ici
En conséquence, on peut voir que l'effet de la traduction automatique est très bon.

visualisation de l'attention

d2l.show_heatmaps(
    enc_attention_weights.cpu(), xlabel='Key positions',
    ylabel='Query positions', titles=['Head %d' % i for i in range(1, 5)],
    figsize=(7, 3.5))

insérez la description de l'image ici
On peut voir que le modèle accorde plus d'attention aux premiers mots d'une séquence

Bien que l'architecture Transformer soit proposée pour l'apprentissage séquence à séquence, les encodeurs Transformer ou les décodeurs Transformer sont généralement utilisés seuls dans différentes tâches d'apprentissage en profondeur.

Je suppose que tu aimes

Origine blog.csdn.net/qq_51957239/article/details/129732840
conseillé
Classement