Les modèles basés sur Transformer sont célèbres pour leur capacité à analyser et interpréter des textes complexes. Ils s’appuient sur la compréhension de l’ordre et du contexte des mots – tâches pour lesquelles les méthodes traditionnelles de codage positionnel ont montré leurs limites. Pour combler cette lacune, le modèle ROFORMER, optimisé par le Rotary Position Embedding (RoPE), redéfinit notre approche du codage positionnel.
Encodage positionnel traditionnel
Les transformateurs traitent le texte comme une série de jetons et permettent le traitement parallèle des séquences pour une plus grande efficacité. Cependant, cette force a apporté son défi : l'agnosticisme inhérent du modèle à l'ordre symbolique. Le codage positionnel était la réponse, fournissant à chaque jeton une signature unique indiquant sa position dans la séquence.
Intégrations de position absolue
Initialement, des modèles comme BERT utilisaient des intégrations de positions absolues, attribuant un vecteur fixe à chaque position dans une séquence. Cette méthode, bien que simple, n'a pas intrinsèquement la capacité de s'adapter aux variations de longueur de séquence ou de mettre l'accent sur les distances relatives entre les jetons, essentielles à la compréhension de nombreuses constructions linguistiques.
Intégrations de position relative
Pour capturer la nature dynamique du langage, des intégrations de positions relatives ont été introduites, en se concentrant sur la distance entre les jetons plutôt que sur leurs positions absolues. Malgré leur avantage conceptuel, ces intégrations ont introduit une complexité informatique et n'ont pas réussi à s'intégrer de manière transparente dans le mécanisme d'auto-attention de Transformers, limitant leur efficacité.
ROFORMER et intégration de position rotative
Reconnaissant les limites des stratégies de codage de position existantes, ROFORMER introduit le Rotary Position Embedding (RoPE), une approche qui combine les avantages des informations de position absolue et relative sans leurs inconvénients respectifs.
Intégration de la position rotative
RoPE encode les informations de position à l'aide de matrices de rotation, permettant au modèle de comprendre non seulement où se trouve un jeton, mais aussi comment il se rapporte à tous les autres jetons d'une séquence.
Credit: Original Paper
Il fonctionne à travers une lentille géométrique, traitant les positions des jetons comme des points dans un espace multidimensionnel qui tournent pour marquer leurs relations séquentielles. Cette rotation permet au modèle de préserver et d'exploiter les indices de position absolus et relatifs au sein de son mécanisme d'auto-attention.
Implémentation de RoPE
La mise en œuvre de RoPE implique de coder la position de chaque jeton dans une matrice de rotation et d'appliquer cette matrice au sein du mécanisme d'auto-attention du Transformer. Ce processus permet une interprétation flexible et dynamique des informations de position, s'adaptant à différentes longueurs de séquence et capturant l'essence des interrelations des jetons sans surcharge de calcul significative.
Tout d’abord, vous aurez besoin d’une fonction pour générer les intégrations rotatives, puis vous intégrerez ces intégrations dans votre modèle. L'exemple ci-dessous suppose que vous êtes familiarisé avec la création de calques personnalisés dans Keras.
Étape 1 : Définir la fonction d'intégration rotative
Cette fonction génère les plongements rotatifs en fonction de la longueur maximale de la séquence et de la dimensionnalité des plongements.
from tensorflow.keras.layers import Layer
import numpy as np
def get_rotary_embedding(dim, max_seq_len):
inv_freq = 1.0 / (10000 ** (tf.range(0, dim, 2, dtype=tf.float32) / dim))
t = tf.range(max_seq_len, dtype=tf.float32)
freqs = tf.einsum('i,j->ij', t, inv_freq)
emb = tf.concat((tf.cos(freqs), tf.sin(freqs)), axis=-1)
return emb
inv_freq = 1.0 / (10000 ** (tf.range(0, dim, 2, dtype=tf.float32) / dim))
Cette ligne calcule l'inverse des fréquences mises à l'échelle exponentiellement en fonction des indices de position. Ces fréquences sont utilisées pour générer des modèles sinusoïdaux pour les intégrations rotatives, ce qui facilite le codage des informations de position relative dans des séquences. Ce mécanisme est particulièrement utile dans les tâches où la compréhension de l'ordre et du positionnement relatif des éléments est cruciale, comme dans le traitement du langage naturel ou l'analyse de séries chronologiques.
En détails:
-
tf.range(0, dim, 2, dtype=tf.float32)
crée une plage de valeurs allant de 0 jusqu'àdim
(exclusif), en incrémentant de 2. L'argumentdtype=tf.float32
spécifie que les éléments de ce tenseur sont des nombres à virgule flottante de 32 bits. Si « dim » vaut 8, par exemple, cela produirait « [0, 2, 4, 6] ». -
Le tenseur produit par
tf.range
est ensuite divisé par la dimensionnalité (dim
) des plongements. Cette opération réduit ces indices à une plage comprise entre 0 et 1 (exclusif si « dim » est pair, légèrement inclusif si « dim » est impair, car l'étape de plage saute toutes les autres valeurs). En continuant l'exemple avecdim
= 8, en divisant par 8, on obtient[0.0, 0.25, 0.5, 0.75]
. -
L'opération
10000 ** (...)
élève 10 000 à la puissance de chaque élément du tenseur précédemment mis à l'échelle. La base de 10 000 est quelque peu arbitraire, mais elle est choisie pour garantir que les fréquences varient sur une large plage, ce qui aide le modèle à différencier plus efficacement les différentes positions. Pour « [0,0, 0,25, 0,5, 0,75] », cela appliquerait l'opération de puissance à chacun, ce qui entraînerait des valeurs beaucoup plus grandes pour les éléments supérieurs. -
Enfin, la fréquence inverse est obtenue en prenant l'inverse (1/x) des valeurs de l'étape précédente. Les fréquences inverses sont plus petites pour les indices plus élevés, ce qui signifie que les éléments plus loin dans la séquence auront des fréquences plus petites, affectant la façon dont leurs positions sont codées dans le modèle. Cela permet aux intégrations d'évoluer de manière à ce que les positions relatives puissent être déduites via les mécanismes d'attention du modèle.
La ligne:
freqs = tf.einsum('i,j->ij', t, inv_freq)
utilise la fonction « tf.einsum » de TensorFlow, un outil qui permet une expression concise et efficace des opérations tensorielles à l'aide de la notation de sommation d'Einstein.
Cette opération calcule efficacement le produit externe des vecteurs « t » et « inv_freq », résultant en une matrice où chaque élément « (i, j) » est le produit du « i »-ième élément de « t » et du « j-ième élément de
inv_freq`. Cette matrice (« freqs ») représente les fréquences utilisées pour générer les modèles sinusoïdaux pour les plongements rotatifs.
Étape 2 : Couche Keras personnalisée pour les intégrations rotatives
Créons maintenant une couche Keras personnalisée qui applique des intégrations rotatives au tenseur d'entrée. Cette couche suppose que le tenseur d'entrée est de forme (batch_size, séquence_length, embedding_dim)
.
class RotaryEmbeddingLayer(Layer):
def __init__(self, dim, max_seq_len, **kwargs):
super().__init__(**kwargs)
self.dim = dim
self.max_seq_len = max_seq_len
self.rotary_embeddings = get_rotary_embedding(dim, max_seq_len)
def call(self, inputs):
seq_len = tf.shape(inputs)[1]
embeddings = self.rotary_embeddings[:seq_len]
cos_emb = embeddings[:, None, :self.dim // 2]
sin_emb = embeddings[:, None, self.dim // 2:]
# Decompose inputs into sine and cosine components
inputs_cos = inputs[..., :self.dim // 2]
inputs_sin = inputs[..., self.dim // 2:]
# Apply rotary embeddings
rotated_cos = inputs_cos * cos_emb - inputs_sin * sin_emb
rotated_sin = inputs_sin * cos_emb + inputs_cos * sin_emb
return tf.concat([rotated_cos, rotated_sin], axis=-1)
def get_config(self):
config = super().get_config()
config.update({
"dim": self.dim,
"max_seq_len": self.max_seq_len
})
return config
La ligne embeddings = self.rotary_embeddings[:seq_len]
sélectionne le sous-ensemble approprié d'intégrations rotatives pré-calculées en fonction de la longueur actuelle de la séquence d'entrée. La longueur des séquences pouvant varier d'un lot à l'autre, cette opération de découpage garantit que seuls les plongements correspondant à la longueur réelle de la séquence sont utilisés.
La variable embeddings
contient désormais un tenseur de forme (seq_len, embedding_dim)
, où seq_len
est la longueur des séquences du lot actuel, et embedding_dim
est la dimensionnalité des intégrations. Ce tenseur contient les plongements positionnels rotatifs pour chaque position de la séquence jusqu'à seq_len
.
emb = tf.concat((tf.cos(freqs), tf.sin(freqs)), axis=-1)
combine les transformations sinusoïdales et cosinusoïdales des fréquences de position en un seul tenseur :
-tf.cos(freqs)
et tf.sin(freqs)
appliquent respectivement les transformations cosinus et sinus au tenseur freqs
. Le tenseur « freqs » contient des valeurs de fréquence pour chaque position dans la séquence d'entrée et chaque dimension de l'espace d'intégration, calculées sur la base des positions de séquence et des fréquences inverses des dimensions d'intégration. Les fonctions sinus et cosinus sont appliquées élément par élément, ce qui donne deux tenseurs de la même forme que les « fréquences ». Ces transformations aident à coder la position d'une manière qui capture la nature cyclique des relations de position, facilitant ainsi la capacité du modèle à comprendre les positions relatives.
-tf.concat((tf.cos(freqs), tf.sin(freqs)), axis=-1)
concatène les tenseurs transformés en cosinus et en sinus le long du dernier axe (noté axis=-1
). La concaténation de ces tenseurs côte à côte double effectivement la dimensionnalité du tenseur « fréquences », la première moitié représentant les valeurs transformées en cosinus et la seconde moitié représentant les valeurs transformées en sinus pour chaque position. La concaténation garantit que chaque codage de position contient à la fois des informations sinusoïdales et cosinusoïdales, ce qui permet de conserver des informations sur l'amplitude et la phase des signaux de position.
- Le tenseur concaténé
emb
contient désormais les plongements rotatifs complets pour les positions d'entrée. La forme deemb
sera la même que celle defreqs
dans ses deux premières dimensions (correspondant aux positions de séquence et aux dimensions d'intégration), mais sa dernière dimension sera deux fois plus grande, représentant à la fois les valeurs sinus et cosinus. Ces intégrations sont utilisées pour moduler les intégrations d'entrée en ajoutant des informations de position de manière équivariante en rotation.
-cos_emb = incorporations[:, Aucun, :self.dim // 2]
:
-
Le premier deux-points
:
signifie « sélectionner tous les éléments de cette dimension », qui, dans ce cas, fait référence à toutes les positions de la séquence. -
« Aucun » est utilisé pour ajouter une dimension supplémentaire, rendant le tenseur tridimensionnel. Ceci est souvent fait pour garantir la compatibilité avec certaines opérations qui attendent des entrées d'un nombre spécifique de dimensions. Par exemple, lors d'une multiplication élément par élément avec un autre tenseur tridimensionnel, les formes doivent s'aligner selon les règles de diffusion.
-
:self.dim // 2
, sélectionne la première moitié des dimensions du dernier axe. Étant donné que laembedding_dimension
est doublée pour inclure à la fois les valeurs sinus et cosinus, la division par 2 sélectionne effectivement uniquement les composants cosinus des intégrations.
Étape 3 : Intégration avec un modèle Keras
Après avoir défini le RotaryEmbeddingLayer
, vous pouvez l'intégrer dans votre modèle Keras. Ce calque doit être appliqué à vos intégrations avant de les intégrer dans les calques d'attention ou dans tout calque de modèle ultérieur.
Voici un exemple simplifié de la façon d'intégrer les intégrations rotatives dans un modèle :
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Embedding, Dense
max_seq_len = 512
embedding_dim = 64
inp = Input(shape=(max_seq_len,))
x = Embedding(input_dim=10000, output_dim=embedding_dim)(inp)
x = RotaryEmbeddingLayer(dim=embedding_dim, max_seq_len=max_seq_len)(x)
# Add your model's layers here, e.g., Transformer blocks
x = Dense(1, activation='sigmoid')(x)
model = Model(inputs=inp, outputs=x)
model.summary()