* (Mise à jour du 29/11/2019 : Ce mois-ci la famille de BERT s'est agrandie avec son cousin français, tout juste sorti des labos de l'INRIA/Facebook AI Research/Sorbonne Université, et rien que le nom - CamemBERT - valait bien quelques applaudissements :).
Il s'agit d'un Transformer spécialement pré-entraîné en français qui, pour cette langue, obtient de meilleurs résultats que les modèles classiques multilingues de BERT - qui fonctionnaient déjà très bien.
Les complices à l'origine de ce Transformer AOP certifié "qualité terroir" ont publié leur papier ici : CamemBERT: a Tasty French Language Model.
Pour les utilisateurs de PyTorch, et plus récemment TensorFlow 2.0, la startup HuggingFace l'a déjà intégré au catalogue de sa librairie "Transformers", sous license Apache version 2.0, parmi pas mal d'autre Transformers pré-entraînés - y'a plus qu'à.)
* (Mise à jour du 28/06/2019 : En retravaillant, entre-autres, la phase de pré-entrainement non supervisé, XLNet superforme aujourd'hui BERT sur quantité de tâches. Le papier a été publié ici le 19 juin pour ceux que ça intéresse.
Il vous en coûtera quand même dans les 60.000 $ pour le pré-entraîner - les estimations varient à cause de l'ambiguïté cœur/TPU/appareil dans le papier. BERT quand à lui reste open source, livré pré-entraîné gratuitement, et se pré-entraîne au besoin pour 9 fois moins cher).
BERT c'est pour Bidirectional Encoder Representations from Transformers. Il est sorti des labos Google AI fin 2018, et s'il est ce jour l'objet de notre attention c'est que son modèle est à la fois :
Il a l'avantage par rapport à ses concurrents Open AI GTP (GPT-2 est ici pour ceux que ça intéresse) et ELMo d'être bidirectionnel, il n'est pas obligé de ne regarder qu'en arrière comme OpenAI GPT ou de concaténer la vue "arrière" et la vue "avant" entraînées indépendamment comme pour ELMo.
Source ici.
Exemples de ce qu'il peut faire :
. BERT peut faire de la traduction. Il peut même une fois pré-entraîné pour traduire [français/anglais-anglais/français] puis [anglais/allemand-allemand/anglais], traduire du français vers l'allemand sans entrainement.
. BERT peut comparer le sens de deux phrases pour voir si elles sont équivalentes.
. BERT peut générer du texte. Si on lui demande de générer spontanément une page Wikipedia en partant simplement de la chaîne de caractères "The transformer", il peut produire :
<pre class="ql-syntax ql-indent-3" spellcheck="false">"The transformer" are a Japanese [[hardcore punk]] band <- Notez qu'ici il génère aussi les markups avec les crochets, comme pour une vraie page Wikipedia. ==Early years== The band was formed in 1968 during the height of Japanese music history. Among the legendary [...] ==History== ===Born from the heavy metal revolution=== In 1977 the self-proclaimed King [...] On 1 January 1981 bassist Micho Kono [...] <- Notez que Micho Kono est bien a consonance Japonaise, notez aussi qu'il est une pure invention, il n'a pas de page Wikipedia. Or tout ceci est un échantillon tiré d'une distribution. Si on ré-échantillonne (i.e. qu'on lui redemande de produire une page Wikipedia à partir de "The transformer"), on peut obtenir : "The transformer" is a [[book]] by British [[illuminatis!]] [[Herman Muirhead]], set in a post-apocalyptic world that border on a mysterious alien known as the &quot;Transformer Planet&quot; which is his trademark to save Earth. [...] <- Notez qu'il sait qu'un livre sur Wikipedia a souvent des citations. ==Summary== <- Notez qu'il sait qu'un livre sur Wikipedia a souvent une section "résumé". </pre>
Et tout ça sans se contredire entre le début et la fin du texte, formant un tout cohérent sur un texte pourtant très long. (Tiré de cette vidéo)
. BERT peut décrire et catégoriser une image
. BERT sait faire de l'analyse logique de phrase, c'est à dire déterminer si tel élément est un sujet, un verbe, un complément d'objet direct etc... mieux que les meilleurs modèles jusqu'ici.
Note : Pour aller plus loin sur ces notions, chercher "NLP tagging" ou "POS tagging" pour part-of-speech tagging en anglais.
La même chose, dans un esprit plus "structurel", vous pouvez aussi Googler : "NLP parsing" - "Shallow Parsing" aka "Chunking", "Constituency Parsing" aka "Deep Parsing", "Dependency Parsing" le plus populaire en matière de parsing pour le NLP.
Enfin vous trouverez souvent ça en français sous le terme "étiquetage morpho-syntaxique".
. BERT peut répondre à des questions
Etc...
BERT est par ailleurs décrit dans ce papier et ses sources sont disponibles ici.
Dans ce chapitre on ne parle pas de BERT. On présente les RNNs et les mécanismes d'attention, afin de pouvoir revenir à BERT plus confortablement.
Pour faire du "sequence to sequence", i.e. de la traduction simultanée, ou du text-to-speech, ou encore du speech-to-text, l'état de l'art jusque ces dernières années, c'était les RNNs, nourris avec des séquences de word-embeddings *, et parfois quelques couches de convolution sur les séquences d'entrée pour en extraire des caractéristiques (features) plus ou moins fines (fonction du nombre de couches), afin d'accélérer les calculs, avant de passer les infos au RNN.
* Vous trouverez matière ici pour comprendre les word-embeddings.
Notez que BERT utilise des embeddings sur des morceaux de mots. Donc ni des embeddings niveau caractère, ni des embeddings au niveau de chaque mot, mais un intermédiaire.
Contrairement aux réseaux de neurones "classiques", que nous appellerons FFN pour feed-forward neural networks, qui connectent des couches de neurones formels les unes à la suite des autres, avec pour chaque couche sa propre matrice de poids "entraînable" - c'est à dire dont on peut modifier les poids petit à petit lors de l'apprentissage - et qui prennent en entrée des fournées (batchs) de données, les RNNs traitent des séquences, qu'ils parcourent pas à pas, avec une même matrice de poids. Pour cette raison (le pas-à-pas), dans le cadre des RNNs, on ne peut pas paralléliser les calculs.
Les séquences sont des objets dont chaque élément possède un ordre, une position, une inscription dans le temps. Par exemple dans une phrase, chaque mot vient dans un certain ordre, est prononcé sur un intervalle de temps distinct de celui des autres.
Nous allons les représenter de façon déroulée pour des raisons de lisibilité, mais en réalité il s'agit d'itérer dans l'ordre sur chaque élément d'une séquence. Il n'y a qu'une seule matrice de poids, ou "les poids sont partagés dans le temps" si vous préférez. Ci-dessous la représentation réelle et la représentation déroulée.
Les x-i sont les entrées. Par exemple les mots encodés d'une phrase. Les hi les états cachés "hidden states", et les o-i les sorties (o pour "outputs") mais généralement notés 'y'
Il en existe de différents types, quelques exemples illustrés ci-dessous :
Source : Cette vidéo issue d'un cours d'Andrew Ng sur Coursera (ses MOOCs sont excellents soit dit en passant)
Une seule matrice de poids aide à traiter de séquences de longueurs différentes (par exemple si l'apprentissage s'opère sur des séquences de longueurs 8 max., avec 8 matrices de poids, à la première séquence de longueur 12 le modèle va être en difficulté, il lui manquera 4 matrices). Par ailleurs, deux séquences différentes peuvent avoir un sens très similaire, par exemple : "combien de lignes fait cet article ?" et "cet article fait combien de lignes ?". Dans ce cas une seule matrice de poids permet de partager les même paramètres à chaque étape de traitement, et par conséquence de donner un résultat global assez similaire en sortie pour deux phrase ayant le même sens, bien qu'ayant une composition différente.
Les FFNs sont soumis à l'explosion ou à l'évanouissement du gradient (exploding/vanishing gradient descent), et ce phénomène est d'autant plus prononcé qu'on traite de longue séquences ou qu'on les approfondis. De la même manière les RNNs y sont soumis quand on augmente le nombre d'étapes de traitement, mais de façon aggravée par rapport aux FFNs. En effet il peut y avoir des dizaines/centaines de mots dans une phrase, et rarement autant de couches dans un FFN. De plus, dans un FFN, chaque couche possède sa propre matrice de poids et ses propres fonctions d'activation. Les matrices peuvent parfois se contre-balancer les unes les autres et "compenser" ce phénomène. Dans un RNN avec une seule matrice les problèmes de gradient sont plus prononcés.
Note : Il peut y avoir plusieurs couches dans un RNN, auquel cas chaque couche aura sa propre matrice de poids. Entendez par là qu'en sus du nombre d'étapes de traitement, un RNN peut aussi être un réseau "profond". On peut empiler plusieurs couches de RNN connectées entre elles.
Afin de conserver la mémoire du contexte, et d'atténuer les problèmes de descente de gradient, les RNNs sont généralement composés d'unités un peu particulières, les LSTMs (long short term memory), ou d'une de leur variantes les GRUs (gated recurrent units). Il existe en sus d'autres techniques pour les soucis de gradient dans les RNNs - gradient clipping quand ça explose, sauts de connexion, i.e. additive/concatenative skip/residual connexion quand ça "vanishe" etc... Voici un article pour plus de détails. Cependant nous ne rentreront pas dans les détails, ni de ces techniques, ni de ce que sont les LSTM/GRU.
Sachez simplement que les LSTMs sont construites pour mémoriser une partie des autres éléments d'une séquence, et sont donc bien adaptées à des tâches traitant d'objets dont les éléments ont des dépendances entre eux à plus ou moins long terme, comme la traduction simultanée, qui ne peut pas se faire mot à mot, sans le contexte de ce qui a été prononcé avant.
Simplement pour en "montrer une", ci-dessous l'illustration d'une LSTM (il en existe de plusieurs sortes), avec sa "forget gate" (f), "input gate" (i), "output gate" (o) et vecteur candidat (C). En sortie d'une cellule on a h et C, et ces deux sorties vont nourrir la cellule suivante. Pour schématiser, C va transporter l'information de ce que l'on conserve ou que l'on oublie des C et h de la cellule précédente.
Source ici : Cellule "Long short term memory" (LSTM) illustrée
Dans notre cas, pour faire du "sequence to sequence", la topologie utilisée classiquement est illustrée ci-dessous. Habituellement on a un encodeur, suivi d'un décodeur.
La sortie de l'encodeur est une suite de chiffres qui représente le sens de la phrase d'entrée. Passée au décodeur, celui-ci va générer un mot après l'autre, jusqu'à tomber sur un élément qui signifie "fin de la phrase".
Notez qu'ici h-4 est un vecteur qui contient tout le sens de la phrase en sortie de l'encodeur. Il est souvent appelé "context vector", vecteur contexte.
Les mécanismes d'attention sont les moyens de faire passer au décodeur l'information de quelles étapes de l'encodeur (i.e. quels mots de la séquence d'entrée) sont les plus importantes au moment de générer un mot de sortie. Quels sont les mots de la séquence d'entrée qui se rapportent le plus au mot qu'il est en train de générer en sortie, soit qu'ils s'y rapportent comme contexte pour lui donner un sens, soit qu'ils s'y rapportent comme mots "cousins" de signification proche.
Les mécanismes d'auto-attention (self-attention) sont similaires, sauf qu'au lieu de s'opérer entre les éléments de l'encodeur et du décodeur, ils s'opèrent sur les éléments de l'input entre eux (le présent regarde le passé et le futur) et de l'output entre eux aussi (le présent regarde le passé, vu que le futur est encore à générer).
Par exemple au moment de générer "feel", c'est en fait l'ensemble "avoir la pêche" qui a du sens, il doit faire attention à tous les mots, et idem quand il génère "great", car il s'agit de la traduction d'une expression idiomatique.
Pour introduire l'attention, parlons un peu de la convolution car elle est souvent intégrée aux RNNs et répond - en partie - à un objectif similaire, en ce qu'elle fournit un "contexte" à une suite de mots. On peut en effet passer la séquence d'entrée dans une ou plusieurs couches de convolution, avant de la passer dans l'encodeur. Les produits de convolution vont alors extraire des caractéristiques contextuelles entre mots se trouvant à proximité les uns des autres, exacerber le poids de certains mots, et atténuer le poids de certains autres mots, et ceci de façon très positionnelle. La convolution permet couche après couche d'extraire des caractéristiques (features) spatiales (dans diverses zones de la phrase), de plus en plus fines au fur et à mesure que l'on plonge plus profond dans le réseau.
Par analogie avec la convolution sur les images, pour appliquer la convolution à une phrase on peut par exemple à mettre un mot par ligne, chaque ligne étant le vecteur d'embeddings du mot correspondant. Chaque case du tableau de chiffres que cela produit étant alors l'équivalent de l'intensité lumineuse d'un pixel pour une image.
Les CNN (convolutional neural networks) ont aussi l'avantage que les calculs peuvent se faire en parallèle (O(n/k) vs. O(n) pour un RNN), qu'on peut les utiliser pour concentrer l'information, et par conséquent diminuer les problèmes de d'explosion/évanouissement du gradient, utile s'il s'agit, par exemple, de générer une texte de plus de 1000 mots. Je les mentionne parce-que c'est ce qu'on fait WaveNet et ByteNet avec d'assez bon résultats.
Ce qui sortira du CNN sera une info du type : "les mots 8, 11 et 23 sont très importants pour donner le sens exact de cette phrase, de plus il faudra combiner ou corréler 8 et 11, retiens ça au moment de décoder la phrase". À chaque étape on décide de conserver tel ou tel mot (concentrant ainsi l'information) qui est censé avoir de l'importance au moment de générer le mot suivant. Ceci-dit, c'est très positionnel. Ça dépend beaucoup de la position des mots dans la phrase et de leur position les uns par rapport aux autres pour créer un contexte, plus que de leur similarité de contexte sémantique.
J'insiste sur ce point car c'est une différence fondamentale avec l'attention, qui ne va pas regarder dans quelle position se trouvent les mots, mais plutôt quels sont les mots les plus "similaires" entre eux. Comme la notion de "position" d'un élément dans une séquence reste très importante, les mécanismes d'attention nous obligeront à passer cette notion par d'autres moyens, mais ils ont l'avantage de pouvoir faire des liens entre des éléments très distants d'une séquence, de façon plus légère qu'avec un produit de convolution, et d'une façon plus naturelle (sans se baser sur la position mais plutôt en comparant les similarités entre les éléments).
Les RNNs avec LSTMs ont en réalité déjà leurs mécanismes d'attention, il existe plusieurs façons de faire, et nous allons en illustrer une.
Maintenant zoomons sur la partie "Attention", le carré en rouge dans le schéma précédent, et le détail de comment peuvent être calculés les aij. Il en existe pas mal, nous mentionnerons trois grandes familles et en détaillerons une dans une des familles.
Ce qui donne en vue globale :
Lors d'une traduction simultanée, l'attention permet d'obtenir de bons résultats même lorsque les séquences en entrée son longues. On est plus obligé de travailler avec des phrases de longueur fixe. Extrait du papier "Neural machine translation by jointly learning to align and translate", la qualité du résultat (BLUE score) de la traduction d'un jeu de test, en fonction de la longeur de la phrase à traduire. On observe que le modèle avec attention entraîné sur des phrases comptant jusqu'à 50 mots (RNNsearch-50) ne voit pas la qualité de sa traduction se dégrader au delà de 50 mots.
Bonus : Résolution de co-références - Schémas de Winograd
Nous en parlons ici parce que c'est lié aux mécanismes d'attention, et c'est remarquable parce-que qu'aucun autre modèle que BERT ne donne de bonnes performances sur ce point.
La co-référence c'est lorsque élément en référence un autre mais de façon suffisamment ambiguë pour qu'il faille une compréhension fine de la phrase pour comprendre ce qu'il référence.
Exemple : Je ne peux pas garer ma voiture sur cette place parce-qu’elle est trop petite. <- Ici le pronom personnel "elle" renvoie à la place de parking, pas à la voiture. Il faut une compréhension assez fine de la phrase pour le comprendre. BERT y arrive très bien.
Illustration de l'attention Multi-têtes (que nous détaillerons plus tard), on voit bien que chaque tête "voit" des choses que les autres ne voient pas et ce "collège" se complète bien.
Niveaux d'attention (intensité de couleur) apportée aux mots de la séquence, par chaque tête, au moment par exemple de traduire le mot "ils".
-------------------------------------------------------------------------------------------------------------------------------
Note d'attention :
Comme le fait justement remarquer Florian Arthur de chez Quantmetry en commentaire, et que je cite tel quel parce que c'est limpide : "BERT n'utilise qu'une partie de l'architecture Transformer. Comme son nom l'indique (Bidirectional Encoder Representations from Transformers) Bert n'est composé que d'un empilement de blocs type "Encodeur" sans "Décodeur". Il y a aussi des modèles comme GPT-2 composés de couches "Décodeur" seulement et plus spécialisés pour de la génération de texte. Référence avec toutes les architectures en début d'article : https://jalammar.github.io/illustrated-gpt2/ PS : A l'image de CamemBert et FlauBERT il y a aussi une version FR de GPT qui vient d'être mise à disposition sur Hugging Face : https://github.com/AntoineSimoulin/gpt-fr"
Je vous invite a prendre en compte cette remarque lors de la poursuite de votre lecture, car l'article ne distingue pas toujours nettement le cas général du cas particulier - Transformer/BERT.
-------------------------------------------------------------------------------------------------------------------------------
Si on devait résumer ce qu'est cette architecture "Transformer", décrite dans "Attention is all you need" (2017), à la lumière de ce que l'on a vu précédemment, on pourrait dire que c'est :
Vue d'ensemble :
Source ici. Architecture globale de "The Transformer"
BERT apprend de façon non supervisée, l'entrée se suffit à elle même, pas besoin de labelliser, qualifier quoi que ce soit, on se servira uniquement de l'entrée, et de plusieurs manières. Nous appellerons ça le MLM pour "Masked language model". Nous allons décortiquer ce que le modèle tente d'apprendre.
Notations :
[CLS] indique un début de séquence
[SEP] une séparation, en général entre deux phrases dans notre cas.
[MASK] un mot masqué
Ici la séquence d'entrée a été volontairement oblitérée d'un mot, le mot masqué, et le modèle va apprendre à prédire ce mot masqué.
Entrée = [CLS] the man went to [MASK] store [SEP]
Entrée = [CLS] the man [MASK] to the store [SEP]
Ici le modèle doit déterminer si la séquence suivante (suivant la séparation[SEP]) est bien la séquence suivante. Si oui, IsNext sera vrai, le label sera IsNext, si non, le label sera NotNext.
Entrée = [CLS] the man went to [MASK] store [SEP]
he bought a gallon [MASK] milk [SEP]
Label = IsNext
Entrée = [CLS] the man [MASK] to the store [SEP]
penguin [MASK] are flight ##less birds [SEP]
Label = NotNext
C'est ici en terme de résultats que BERT va se montrer surpuissant. En effet, une fois son apprentissage non supervisé terminé, il est :
. Non seulement capable de se spécialiser sur beaucoup de tâches différentes (traduction, réponse à des questions etc.., le papier en recense une dizaine, ce qui est beaucoup, et il en existe sûrement davantage qui n'ont pas encore été testées).
. Mais surtout il surclasse dans la plus part de ses spécialisations les modèles spécialisés existants !
Ci-dessous une illustration de quatre de ses spécialisations.
Source ici.
Comme nous l'avons déjà vu, BERT peut être utilisé pour diverses tâches. Ci-dessous nous détaillerons son fonctionnement dans le cadre de la génération de texte, ça nous permettra de voir en détails le fonctionnement de chaque module.
Les embeddings : chaque mot est représenté par un vecteur (colonne ou ligne de réels), ici de dimension 512, qu'on notera dmodel comme dans le papier "Attention is all you need".
On ajoute éventuellement à ces embeddings, pour chaque mot, les embeddings d'un "segment" quand cela a du sens (par exemple chaque phrase est un segment et on veut passer plusieurs phrases à la fois, on va alors dire dans quel segment se trouve chaque mot).
On ajoute ensuite le "positional encoding", qui est une façon d'encoder la place de chaque élément dans la séquence. Comme la longueur des phrases n'est pas prédéterminée, on va utiliser des fonctions sinusoïdales donnant de petites valeurs entre 0 et 1, pour modifier légèrement les embeddings de chaque mot. La dimension de l'embedding de position (à sommer avec l'embedding sémantique du mot) est la même que celle de l'embedding sémantique, soit 512, pour pouvoir sommer terme à terme.
Notez qu'il existe beaucoup de façons d'encoder la position d'un élément dans une séquence.
Pour clarifier, une illustration ci-dessous :
Encoding positionnel. Matrice du bas inspirée par cet article.
Ce qui donne en entrée une matrice de taille [longueur de la séquence] x [dimension des embeddings - 512] :
Représentation de l'entrée de BERT. Les + sont des sommes termes à termes. Source ici.
Comme vu précédemment nous allons utiliser un mécanisme d'attention (auto-attention ou pas) de type clé-valeur.
Chaque mot, décrit comme la somme de ses embeddings sémantiques et positionnels va être décomposé en trois abstractions, :
. Q : Une requête (query)
. K : Une clé (key)
. V : Une valeur (value)
Chacune de ces abstractions est ici un vecteur de dimension 64. Le pourquoi du 64 vous le comprendrez mieux plus loin, il n'est pas obligatoire. Disons que dans notre cas, comme on veut des sorties de dimension [longueur de la séquence] x [dimension des embeddings - 512] tout au long du parcours, et que dans l'attention on fera du multi-têtes (voir plus loin) avec 8 têtes, qu'on concaténera la sortie de chaque tête, on aura alors 8 x 64 = 512 en sortie de l'attention et c'est bien ce qu'on veut.
Chacune de ces abstractions est calculée et apprise (mise à jour des matrices de poids) lors du processus d'apprentissage, grâce à une matrice de poids. Chaque matrice est distincte, une pour la requête que nous noterons Wq, une pour la clé que nous noterons Wk, une pour la valeur, que nous noterons Wv.
Les dimensions de ces matrices sont [64 (dimension requête ou clé ou valeur)] x [longueur de la séquence].
Note sur le Multi-têtes :
Chaque tête de l'attention a ses propres matrices Wqi, Wki, Wvi.
On concatène la sortie de chaque tête pour retrouver une matrice de dimension [longueur de la séquence] x [dimension des embeddings, i.e. 512].
Notons X notre matrice en entrée. Pour chaque tête en parallèle, on calcule X.Wqi = Qi pour la tête i, X.Wki = Ki pour la tête i, X.Wvi = Vi pour la tête i.
Ensuite on calcule l'attention pour la tête i Attention(Qi, Ki, Vi) avec la formule suivante :
Source ici.
Avec ici dk = 64, la dimension pour chaque tête.
Explication de la formule :
Tout d'abord revoyons rapidement ce qu'est un produit scalaire avec l'illustration ci-dessous.
[Q x Transposée de K] est un produit scalaire entre les vecteurs requête et les vecteurs clé. Comme nous venons de le voir, plus la clé "ressemblera" à la requête, plus le score produit par [Q x Transposée de K] sera grand pur cette clé.
La partie dk (=64) est simplement là pour normaliser, ce n'est pas toujours utilisé dans les mécanismes d'attention.
La softmax va donner une distribution de probabilités qui va encore augmenter la valeur pour les clés similaires aux requêtes, et diminuer celles des clés dissemblables aux requêtes.
Enfin, les clés correspondent à des valeurs, quand on multiplie le résultat précédent par V, les valeurs correspondant aux clés qui ont été "élues" à l'étape précédente sont sur-pondérées par rapport aux autres valeurs.
Enfin on concatène la sortie de chaque tête, et on multiplie par une matrice W0, de dimensions 512 x 512 ([(nombre de têtes) x (dimension requête ou clé ou valeur, i.e. 64)]x[dimension des embeddings]), qui apprend à projeter le résultat sur un espace de sortie aux dimensions attendues.
Source ici.
Ce qui donne en schéma global :
On vient ajouter la représentation initiale à celle calculée dans les couches d'attention ou dans le FFN.
Cela revient à dire : Apprend les relations entre les éléments de la séquence, mais n'oublie pas ce que tu sais déjà à propos de toi.
Appelons "Sortie(x)" la sortie de chaque couche qui prend une entrée "x". On applique un dropout de 10%, donc en sortie de chaque couche il nous reste finalement Dropout(Sortie(x)), auquel on ajoute x (l'entrée). On obtient [x + dropout(sortie(x))] qu'on normalise ligne à ligne. Au final on aura LayerNorm[x + Dropout(sortie(x))].
Couches de neurones formels avec une ReLU comme fonction d'activation, formalisé par :
Ici W1 a pour dimensions [dimension des embeddings]x[dimmension d'entrée du FFN - au choix] et W2 [dimmension d'entrée du FFN - au choix] x [dimension des embeddings]. Source ici.
C'est le réseau de neurones "standard" tel que vous le connaissez.
L'auto-attention "masquée" :
Rappelons le principe d'auto-régression du Transformer.
Par exemple pour traduire "Avoir la pèche", nous avons vu que :
. Pour le premier passage, on envoie dans l'encodeur une version "embeddée" de la séquence [<CLS> Avoir la pèche <fin de phrase>], et en même temps l'amorce de séquence [<CLS>] dans le décodeur.
. Le Transformer doit alors nous prédire le premier mot de la traduction : "feel" (i.e. nous sortir la séquence [<CLS> feel])
. Au deuxième passage, on envoie à nouveau dans l'encodeur tout la version "embeddée" de la séquence [<CLS> Avoir la pèche <fin de phrase>], et une version "embeddée" de ce que le Transformer a prédit dans le décodeur.
. Etc... Jusqu'à ce que la séquence prédite en sortie du Transformer finisse par <fin de phrase>.
Pourquoi et comment le masque.
- Pourquoi :
Dans notre cas, la phrase a prédire en sortie est déjà connue lorsque nous entraînons le modèle. Le jeu d'entraînement possède déjà la correspondance entre "avoir la pèche" et "feel great"
Or il faut faire apprendre au modèle que :
. Au premier passage quand l'encodeur reçoit [<CLS> Avoir la pèche <fin de phrase>] et le décodeur reçoit [<CLS>], le modèle doit prédire [<CLS> feel].
. Puis "feel" dans ce contexte doit prédire "great" au passage suivant.
. Etc...
Il faut donc lors de l'entraînement "masquer" à l'encodeur le reste des mots à traduire. Quand "feel" sort du modèle, le décodeur ne doit pas voir "great", il doit apprendre seul que "feel" dans ce contexte doit ensuite donner "great".
- Comment :
Eh bien on va simplement faire en sorte que dans la matrice softmax([Q x Transposée de K]) de notre formule d'attention, la ligne correspondant a chaque mot soit a 0 pour les colonnes représentant les mots suivants "chronologiquement" le dernier mot généré par le modèle.
Attention masquée. Matrices inspirées par cet article.
L'attention encodeur-décodeur :
Le décodeur possède une couche d'attention qui prend en entrée le séquence de sortie du FFN de l'encodeur, qu'il multiplie à ses matrices clés et valeurs (Wki et Wvi pour la tête "i"), tandis que la séquence sortant de la couche d'auto-attention "masquée" de l'encodeur va se multiplier à la matrice des requêtes (Wqi pour la tête "i").
L'encodeur découvre des choses intéressantes (features) à propos de la séquence d'entrée. Ce sont les valeurs. À ces valeurs attribue un label, un index, une façon d'adresser ces "choses"; c'est la clé. Puis le décodeur avec sa requête va décider du type de valeur à aller chercher. La requête demande la valeur pour une clé en particulier.
Si on devait en faire un dialogue ça donnerait :
Encodeur : Tiens c'est intéressant, j'ai trouvé un prénom (clé), qui a pour valeur "Doris" (valeur). J'ai trouvé un lieu (clé) qui a pour valeur "San-Salvador".
Décodeur : Je pense avoir besoin d'un prénom.
Encodeur : Voici "Doris".
La couche linéaire
Enfin nous y voila. Appelons S la matrice en sortie du décodeur. On la multiplie par une matrice de poids (qui peuvent apprendre) W1. C'est une couche totalement connectée qui projette simplement la sortie précédente dans un espace de la taille de notre vocable.
W1 est la matrice qui va permettre d'extraire un mot dans notre dictionnaire de vocabulaire. Elle aura donc pour dimensions [dimension des embeddings, i.e. dmodel] x [nombre de mots dans notre vocable].
La softmax
Alors ce n'est pas très clair dans la doc (et je ne suis pas allé voir dans le code), mais comme la softmax n'a de sens que sur chaque ligne du produit S.W1 si on veut l'utiliser pour choisir un mot du vocable, et vu que je n'ai pas lu dans la doc ou ailleurs qu'on faisait la moyenne, la somme, ou autre opération entre les lignes de S ou celles de S.W1, ou celles qui adviendraient si on empilait les résultats d'une softmax appliquée à chaque ligne de S.W1, amha ils considèrent que c'est le mot généré précédemment qui porte ici toute l'information pour générer mot suivant. Du coup je suppose que c'est la dernière ligne de S.W1 (correspondant au dernier mot généré, de dimension [1] x [taille du vocable]) qui est passée par la softmax.
La softmax nous donne alors l'élément le plus probable à prédire (on prend le mot de la colonne qui donne la probabilité la plus haute).
Mécanique générale du Transfomer, exemple du processus de traduction. Deux des schémas sont tirés de cet article très didactique.
Pour cette démonstration, nous allons utiliser BERT pour faire de l'analyse de sentiments. Il s'agit du modèle en haut à gauche dans le schéma du paragraphe "Les customisations possibles".
Prévoyez quand même une bonne conf. (au moins 16 Go de RAM).
Note : Si vous beaucoup de labels, allez dans les sources, dans "bert_run_classifier.py", la classe "ColaProcessor", dans le return get_labels (return ["0", "1", "2"]) vous pouvez adapter les classes à vos besoins.
Cola c'est pour "Corpus of Linguistic Acceptability", c'est une des classes utilisable pour faire de la classification avec BERT. Il en existe d'autres, comme Mrpc (Microsoft Research Paraphrase Corpus) etc...
Addendum du 04/03/2020 sur les compatibilités : Ce tuto datant de la publication de l'article (avril 2019), assurez-vous que vous êtes en :
- Python 3.6
- Tensorflow 1.13
- Version de BERT : https://github.com/google-research/bert/tree/bee6030e31e42a9394ac567da170a89a98d2062f
- Version du modèle de langue pré-entraîné : https://storage.googleapis.com/bert_models/2018_11_23/multi_cased_L-12_H-768_A-12.zip
Ce n'est pas error-proof, mais c'est un bon début pour reproduire les manips.
Aussi, pour utiliser facilement des Transformers, dont BERT, vous pouvez regarder du côté de la librairie "Transformers" de HuggingFace (licence Apache 2.0).
1/ Téléchargez BERT ici.
(Ou alors dans votre environnement tensorflow, depuis le terminal, clonez BERT avec la ligne de commande : git clone https://github.com/google-research/bert.git)
Puis dézippez le où vous voulez, dans l'exemple on le dézippe directement dans le dossier de téléchargement.
2/ Il y a plusieurs modèles pré-entraînés de BERT, je vous propose de télécharger celui-ci car il gère plusieurs langues et que nous allons l'utiliser avec de français : BERT-Base, Multilingual Cased.
3/ Dans "bert-master" (le dossier des sources de BERT dézippé) à la racine :
. Créez un dossier "data"
. Dézippez BERT-Base, Cased. Le dossier doit s'appeler "multi_cased_L-12_H-768_A-12" et mettez le à la racine de "bert-master".
. Créez un fichier texte "load_data.py"
. Créez un dossier "output"
BERT prend en entrée des fichiers '.tsv', les colonnes séparées par des tabulations.
1/ Préparez votre fichier d'entrainement, nommez le "train.csv" (oui .csv et non .tsv, le code ci-dessous se chargera de la conversion), avec le point-virgule ';' pour séparateur, et une ligne d'en-tête : ID,Label,Lettre,texte, les champs sont assez explicite pour que je ne détaille pas ce que vous devez mettre dans ces colonnes.
Exemple :
ID;Label;Lettre;texte
1;1;a;"l'endroit était très bruyant"
2;0;a;nous avons très bien mangé dans ce restaurant
Faites la même chose avec un fichier "test.csv" qui contient seulement les IDs et le texte à qualifier.
Exemple :
ID;texte
1;très bonne ambiance
2;trop cher pour ce que c'est
3;"nous reviendrons"
Ensuite placez ces deux fichier dans "bert-master\data"
2/ Ouvrez "load_data.py", copiez / collez le code ci-dessous et jouez le. Le script créera train.tsv, dev.tsv et test.tsv aux bon formats.
Notez qu'après avoir joué le script, "dev.tsv" et "train.tsv" ne doivent pas avoir de header, alors que "test.tsv" doit en avoir un.
import pandas as pd
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
le = LabelEncoder()
df = pd.read_csv("data/train.csv", sep=';', encoding='latin-1') #En cas d'erreur essayez avec d'autres encodings
# Crée les DataFrames train et dev dont BERT aura besoin, en ventillant 1 % des données dans test
df_bert = pd.DataFrame({'user_id':df['ID'], 'label':le.fit_transform(df['Label']), 'alpha':['a']*df.shape[0], 'text':df['texte'].replace(r'\n',' ',regex=True)})
df_bert_train, df_bert_dev = train_test_split(df_bert, test_size=0.01)
# Crée la DataFrame test dont BERT aura besoin
df_test = pd.read_csv("data/test.csv", sep=';', encoding='latin-1') #En cas d'erreur essayez avec d'autres encodings
df_bert_test = pd.DataFrame({'user_id':df_test['ID'], 'text':df_test['texte'].replace(r'\n',' ',regex=True)})
# Enregistre les DataFrames au format .tsv (tab separated values) comme BERT en a besoin
df_bert_train.to_csv('data/train.tsv', sep='\t', index=False, header=False)
df_bert_dev.to_csv('data/dev.tsv', sep='\t', index=False, header=False)
df_bert_test.to_csv('data/test.tsv', sep='\t', index=False, header=True)
3/ Ouvrez la fenêtre de commande de votre environnement tensorflow si vous utilisez Anaconda
Pour vous mettre dans le dossier de BERT tapez la ligne de commande suivante : cd C:\cheminVersBert\bert-master\
Enfin, tapez la commande suivante : python run_classifier.py --task_name=cola --do_train=true --do_eval=true --do_predict=true --data_dir=./data/ --vocab_file=./multi_cased_L-12_H-768_A-12/vocab.txt --bert_config_file=./multi_cased_L-12_H-768_A-12/bert_config.json --init_checkpoint=./multi_cased_L-12_H-768_A-12/bert_model.ckpt --max_seq_length=400 --train_batch_size=8 --learning_rate=2e-5 --num_train_epochs=3.0 --output_dir=./output/ --do_lower_case=False
Les prédictions sur le jeu de test seront dans le dossier "output" dans le fichier "test_results.tsv".
Les poids suite à cet entrainement sont sont aussi dans le dossier "output", c'est le "model.ckpt" avec le plus grand indice. Il vous sera utile pour utiliser votre modèle customisé.
Note : Si vous n'avez pas 64 Go de RAM, vous pouvez diminuer la paramètre "train_batch_size". L'entrainement prendra plus de temps mais vous aurez moins de problèmes mémoire.
1/ Dans le dossier "data", remplacez test.tsv par les données que vous souhaitez qualifier (test.tsv a le même format que précédemment - n'oubliez pas le header)
2/ Récupérez le nom du modèle customisé généré lors de l'étape précédente (qui doit se trouver dans le dossier "output" et s’appeler "model.ckpt-[avec un index qui s'incrémente à chaque customisation]").
3/ Jouez la commande suivante (sans dépasser le max_seq_length utilisé lors de la customisation, et en considérant vous êtes toujours dans le dossier "bert-master", sinon ressaisissez la commande cd C:\cheminVersBert\bert-master\ avant) :
python run_classifier.py --task_name=cola --do_predict=true --data_dir=./data --vocab_file=./multi_cased_L-12_H-768_A-12/vocab.txt --bert_config_file=./multi_cased_L-12_H-768_A-12/bert_config.json --init_checkpoint=./output/model.ckpt-3000 --max_seq_length=400 --output_dir=./output/
Idem, le résultat de vos prédictions sera dans le dossier "output".
Références (en dehors des papiers Arxiv déjà mentionnés dans l'article) :
Commentaires :
Aucun commentaires pour le moment
Laissez un commentaire :