Seq2Seq (2) ~ Attention Model edition ~ avec chainer

In Sequence to Sequence (Seq2Seq), qui est une sorte de modèle EncoderDecoder, Un modèle d'attention sera présenté, et ses résultats de mise en œuvre et de vérification seront expliqués.

introduction

Dernière fois http://qiita.com/kenchin110100/items/b34f5106d5a211f4c004 J'ai implémenté le modèle Sequence to Sequence (Seq2Seq) avec Chainer, Cette fois, j'ai ajouté Attention Model à ce modèle.

Dans ce qui suit, nous expliquerons le modèle d'attention, sa méthode de mise en œuvre et les résultats de la vérification.

Attention Model

Qu'est-ce que le modèle d'attention?

En utilisant un réseau RNN tel que LSTM, les données de série telles que les instructions peuvent être converties en vecteurs de caractéristiques.

Cependant, les données initialement entrées sont moins susceptibles d'être reflétées dans le vecteur de caractéristiques de sortie final.

En d'autres termes, la phrase «Maman s'est maquillée, a mis une jupe et est sortie en ville» et la phrase «Papa s'est maquillé, a mis une jupe et est allé en ville» est devenue presque le même vecteur de caractéristiques. Cela signifie que cela finira.

Le modèle d'attention est un mécanisme qui considère correctement les données saisies au début.

Sequence to Sequence with Attention Model

La figure ci-dessous montre le flux de calcul du modèle Seq2Seq implémenté la dernière fois.

Sequence to Sequence
seq2seq.png

(Le chiffre est légèrement différent du précédent)

La partie bleue est le codeur qui vectorise la parole et la partie rouge est le décodeur qui émet la réponse du vecteur.

Si vous ajoutez le modèle Attention à cela, il ressemblera à la figure ci-dessous.

Sequence to Sequence with Attention model
attseq2seq.png

Ce sera un peu compliqué, mais l'endroit où [At] est écrit dans la figure est le modèle d'attention.

Du côté de l'encodeur, le vecteur intermédiaire qui est émis à chaque fois est stocké dans le modèle d'attention.

Du côté du décodeur, le vecteur intermédiaire précédent est entré dans le modèle d'attention. Sur la base du vecteur d'entrée, Attention Model prend la moyenne pondérée de l'entrée de vecteur intermédiaire côté encodeur et retourne.

En entrant la moyenne pondérée du vecteur intermédiaire de l'encodeur dans le décodeur, le modèle d'attention permet de prêter attention au mot avant, au mot après et n'importe où.

Il existe deux principaux types de modèles d'attention, appelés Attention globale et Attention locale.

Dans ce qui suit, nous expliquerons l'attention globale et l'attention locale.

Global Attention

Le document suivant propose une attention mondiale.

Bahdanau, Dzmitry, Kyunghyun Cho, and Yoshua Bengio. "Neural machine translation by jointly learning to align and translate." arXiv preprint arXiv:1409.0473 (2014).

À l'origine, il était utilisé dans la traduction automatique.

Le matériel qui explique Global Attention est https://www.slideshare.net/yutakikuchi927/deep-learning-nlp-attention Est facile à comprendre.

Ce sera en anglais, https://talbaumel.github.io/attention/ Est également facile à comprendre.

Le mécanisme de prise de la moyenne pondérée de l'entrée vectorielle intermédiaire du côté du codeur est illustré ci-dessous.

Global Attention
global_attention.png

La figure considère un état dans lequel trois vecteurs [vecteur intermédiaire 1], [vecteur intermédiaire 2] et [vecteur intermédiaire 3] sont entrés du côté du codeur.

Dans la figure, [eh] et [hh] sont des couches de couplage linéaire qui sortent le vecteur de la taille de la couche cachée à partir du vecteur de la taille de la couche cachée, [+] est l'addition des vecteurs et [×] est la multiplication de chaque élément du vecteur. Je suis.

[tanh] est une tangente hyperbolique qui transforme les éléments d'un vecteur de -1 en 1.

[hw] est une couche de couplage linéaire qui génère un scalaire de taille 1 à partir de la taille de la couche cachée.

[soft max] est une fonction SoftMax qui normalise les valeurs entrées afin que la somme soit 1.

La valeur calculée par [Soft max] est utilisée comme poids de la moyenne pondérée, et le résultat de la prise de la moyenne pondérée du vecteur intermédiaire est sorti.

C'est ainsi que fonctionne Global Attention.

Local Attention

Les articles suivants ont proposé une attention locale

Luong, Minh-Thang, Hieu Pham, and Christopher D. Manning. "Effective approaches to attention-based neural machine translation." arXiv preprint arXiv:1508.04025 (2015).

Il s'agit également d'un document de traduction automatique.

Le matériel de référence est https://www.slideshare.net/yutakikuchi927/deep-learning-nlp-attention est.

Voici un organigramme de calcul de Local Attention.

Local Attention
local_attention.png

Plus de réseaux ont été ajoutés à Global Attention.

La principale différence est le réseau de droite.

[ht] est une jointure linéaire qui génère le vecteur de taille de couche cachée à partir du vecteur de taille de couche cachée, et [tanh] fonctionne comme précédemment, en mettant à l'échelle les éléments du vecteur de -1 à 1.

[tw] est une couche couplée linéairement qui transforme un vecteur de taille de couche cachée en scalaire, et [sigmoïde] est une fonction sigmoïde qui met à l'échelle la valeur d'entrée de 0 à 1. Par conséquent, le vecteur entré jusqu'à présent sera un scalaire compris entre 0 et 1.

Ensuite, j'expliquerai ce que vous faites avec [ga] dans la figure. Le calcul de ga est le suivant.

output = \exp\bigl(-\frac{(s - input * Len(S))^2}{\sigma^2}\bigl)

Où $ input $ est un scalaire mis à l'échelle de 0 à 1, $ Len (S) $ est le nombre de vecteurs intermédiaires entrés par le codeur, et $ s $ est l'ordre des vecteurs de couche intermédiaire ([vecteur intermédiaire 1]]. Si c'est 1, cela représente 2) si c'est [vecteur intermédiaire 2].

Si la valeur sortie par la fonction sigmoïde est 0,1, [ga] sera une grande valeur lorsque le vecteur intermédiaire est 1, et une petite valeur lorsque le vecteur intermédiaire est 3.

En multipliant cette sortie par le poids calculé par Global Attention, il est possible de se concentrer davantage sur un vecteur intermédiaire spécifique.

la mise en oeuvre

Je l'ai implémenté avec le chainer comme avant. La partie Encoder est la même que pour Seq2Seq.

Le code auquel j'ai fait référence provient de l'ODA. Merci beaucoup. https://github.com/odashi/chainer_examples

Attention

J'ai implémenté Global Attention. Le code est comme suit

attention.py



class Attention(Chain):
    def __init__(self, hidden_size, flag_gpu):
        """
Instanciation de l'attention
        :param hidden_size:Taille du calque masqué
        :param flag_gpu:Utiliser ou non le GPU
        """
        super(Attention, self).__init__(
            #Une couche de couplage linéaire qui transforme le vecteur intermédiaire de l'encodeur avant en un vecteur de taille de couche caché
            fh=links.Linear(hidden_size, hidden_size),
            #Une couche de couplage linéaire qui transforme le vecteur intermédiaire de l'encodeur inverse en un vecteur de taille de couche caché
            bh=links.Linear(hidden_size, hidden_size),
            #Une couche de couplage linéaire qui transforme le vecteur intermédiaire du décodeur en un vecteur de taille de couche caché
            hh=links.Linear(hidden_size, hidden_size),
            #Couche de couplage linéaire pour convertir des vecteurs de taille de couche cachés en scalaires
            hw=links.Linear(hidden_size, 1),
        )
        #Souvenez-vous de la taille du calque caché
        self.hidden_size = hidden_size
        #Utilisez numpy lorsque vous n'utilisez pas cupy lors de l'utilisation de GPU
        if flag_gpu:
            self.ARR = cuda.cupy
        else:
            self.ARR = np

    def __call__(self, fs, bs, h):
        """
Calcul de l'attention
        :param fs:Une liste de vecteurs intermédiaires d'encodeur avant
        :param bs:Liste des vecteurs intermédiaires du codeur inverse
        :param h:Sortie vectorielle intermédiaire par le décodeur
        :return:Moyenne pondérée du vecteur intermédiaire du codeur direct et moyenne pondérée du vecteur intermédiaire du codeur inverse
        """
        #Rappelez-vous la taille du mini lot
        batch_size = h.data.shape[0]
        #Initialisation de la liste pour enregistrer les poids
        ws = []
        #Initialisez la valeur pour calculer la valeur totale du poids
        sum_w = Variable(self.ARR.zeros((batch_size, 1), dtype='float32'))
        #Calcul du poids à l'aide du vecteur intermédiaire de l'encodeur et du vecteur intermédiaire du décodeur
        for f, b in zip(fs, bs):
            #Calcul du poids à l'aide du vecteur intermédiaire du codeur direct, du vecteur intermédiaire du codeur inverse, du vecteur intermédiaire du décodeur
            w = functions.tanh(self.fh(f)+self.bh(b)+self.hh(h))
            #Normaliser à l'aide de la fonction softmax
            w = functions.exp(self.hw(w))
            #Enregistrez le poids calculé
            ws.append(w)
            sum_w += w
        #Initialisation du vecteur moyen pondéré en sortie
        att_f = Variable(self.ARR.zeros((batch_size, self.hidden_size), dtype='float32'))
        att_b = Variable(self.ARR.zeros((batch_size, self.hidden_size), dtype='float32'))
        for f, b, w in zip(fs, bs, ws):
            #Normalisé pour que la somme des poids soit 1.
            w /= sum_w
            #poids*Ajouter le vecteur intermédiaire de l'encodeur au vecteur de sortie
            att_f += functions.reshape(functions.batch_matmul(f, w), (batch_size, self.hidden_size))
            att_b += functions.reshape(functions.batch_matmul(b, w), (batch_size, self.hidden_size))
        return att_f, att_b

Dans l'explication, un seul encodeur a été utilisé, mais en fait, il est courant d'utiliser deux types d'encodeur, encodeur avant et encodeur inverse dans Attention Model.

Ainsi, lors du calcul de Attention, nous passons deux listes de vecteurs intermédiaires calculés par le codeur avant et une liste de vecteurs intermédiaires calculés par le codeur inverse.

Decoder

Contrairement au cas de Seq2Seq, les valeurs entrées par Decoder sont trois, le vecteur de mot, le vecteur intermédiaire calculé par Decoder, et la moyenne pondérée du vecteur intermédiaire de Encoder. Je réécris donc l'implémentation de Decoder.

att_decoder.py



class Att_LSTM_Decoder(Chain):
    def __init__(self, vocab_size, embed_size, hidden_size):
        """
Instanciation du décodeur pour le modèle Attention
        :param vocab_size:Nombre de vocabulaire
        :param embed_size:Taille du vecteur Word
        :param hidden_size:Taille du calque masqué
        """
        super(Att_LSTM_Decoder, self).__init__(
            #Couche pour convertir des mots en vecteurs de mots
            ye=links.EmbedID(vocab_size, embed_size, ignore_label=-1),
            #Un calque qui transforme un vecteur de mot en un vecteur quatre fois plus grand que le calque masqué
            eh=links.Linear(embed_size, 4 * hidden_size),
            #Un calque qui transforme le vecteur intermédiaire du décodeur en un vecteur quatre fois plus grand que le calque caché
            hh=links.Linear(hidden_size, 4 * hidden_size),
            #Un calque qui transforme la moyenne pondérée du vecteur intermédiaire de l'encodeur avant en un vecteur quatre fois la taille du calque caché
            fh=links.Linear(hidden_size, 4 * hidden_size),
            #Un calque qui transforme la moyenne pondérée du vecteur intermédiaire de l'encodeur avant en un vecteur quatre fois la taille du calque caché
            bh=links.Linear(hidden_size, 4 * hidden_size),
            #Un calque qui convertit un vecteur de taille de calque masqué en la taille d'un vecteur de mot
            he=links.Linear(hidden_size, embed_size),
            #Couche pour convertir le vecteur de mot en vecteur de taille de vocabulaire
            ey=links.Linear(embed_size, vocab_size)
        )

    def __call__(self, y, c, h, f, b):
        """
Calcul du décodeur
        :param y:Mots à entrer dans le décodeur
        :param c:Mémoire interne
        :param h:Vecteur intermédiaire décodeur
        :param f:Moyenne pondérée du codeur direct calculé par le modèle Attention
        :param b:Moyenne pondérée du codeur inversé calculée par Attention Model
        :return:Dictionnaire de la taille du vocabulaire, mémoire interne mise à jour, vecteur intermédiaire mis à jour
        """
        #Convertir des mots en vecteurs de mots
        e = functions.tanh(self.ye(y))
        #LSTM avec vecteur de mots, vecteur intermédiaire du décodeur, Attention du codeur avant, Attention du codeur inverse
        c, h = functions.lstm(c, self.eh(e) + self.hh(h) + self.fh(f) + self.bh(b))
        #Convertir la sortie vectorielle intermédiaire de LSTM en vecteur de taille lexicale
        t = self.ey(functions.tanh(self.he(h)))
        return t, c, h

L'utilisation d'un vecteur quatre fois la taille du calque caché est la même raison que j'ai expliquée la dernière fois.

Nous avons ajouté des couches [fh] et [bh] pour utiliser la moyenne pondérée des vecteurs intermédiaires de l'encodeur calculé par Attention, mais sinon, ce sont les mêmes.

Seq2Seq with Attention

Le modèle qui combine Encodeur, Décodeur et Attention est le suivant.

att_seq2seq.py



class Att_Seq2Seq(Chain):
    def __init__(self, vocab_size, embed_size, hidden_size, batch_size, flag_gpu=True):
        """
        Seq2Seq +Instanciation de l'attention
        :param vocab_size:Nombre de taille de vocabulaire
        :param embed_size:Taille du vecteur Word
        :param hidden_size:Taille du calque masqué
        :param batch_size:Mini taille de lot
        :param flag_gpu:Utiliser ou non le GPU
        """
        super(Att_Seq2Seq, self).__init__(
            #Encodeur avant
            f_encoder = LSTM_Encoder(vocab_size, embed_size, hidden_size),
            #Encodeur inversé
            b_encoder = LSTM_Encoder(vocab_size, embed_size, hidden_size),
            # Attention Model
            attention = Attention(hidden_size, flag_gpu),
            # Decoder
            decoder = Att_LSTM_Decoder(vocab_size, embed_size, hidden_size)
        )
        self.vocab_size = vocab_size
        self.embed_size = embed_size
        self.hidden_size = hidden_size
        self.batch_size = batch_size

        #Cupy lors de l'utilisation de GPU, numpy lorsque vous ne l'utilisez pas
        if flag_gpu:
            self.ARR = cuda.cupy
        else:
            self.ARR = np

        #Initialisez la liste pour stocker le vecteur intermédiaire de l'encodeur avant et le vecteur intermédiaire de l'encodeur inverse
        self.fs = []
        self.bs = []

    def encode(self, words):
        """
Calcul du codeur
        :param words:Une liste enregistrée de mots à utiliser dans votre saisie
        :return: 
        """
        #Mémoire interne, initialisation du vecteur intermédiaire
        c = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        h = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        #Tout d'abord, calculez l'encodeur avant
        for w in words:
            c, h = self.f_encoder(w, c, h)
            #Enregistrer le vecteur intermédiaire calculé
            self.fs.append(h)

        #Mémoire interne, initialisation du vecteur intermédiaire
        c = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        h = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        #Calcul du codeur inversé
        for w in reversed(words):
            c, h = self.b_encoder(w, c, h)
            #Enregistrer le vecteur intermédiaire calculé
            self.bs.insert(0, h)

        #Mémoire interne, initialisation du vecteur intermédiaire
        self.c = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        self.h = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))

    def decode(self, w):
        """
Calcul du décodeur
        :param w:Mots à saisir avec Decoder
        :return:Mot prédictif
        """
        #Calculez la moyenne pondérée de la couche intermédiaire de l'encodeur à l'aide du modèle Attention
        att_f, att_b = self.attention(self.fs, self.bs, self.h)
        #Utilisation du vecteur intermédiaire de Decoder, attention avant, attention inverse
        #Calcul du prochain vecteur intermédiaire, mémoire interne, mot prédit
        t, self.c, self.h = self.decoder(w, self.c, self.h, att_f, att_b)
        return t

    def reset(self):
        """
Initialiser les variables d'instance
        :return: 
        """
        #Mémoire interne, initialisation du vecteur intermédiaire
        self.c = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        self.h = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        #Initialisation de la liste qui enregistre le vecteur intermédiaire de l'encodeur
        self.fs = []
        self.bs = []
        #Initialisation du gradient
        self.zerograds()

Il utilise un total de trois LSTM: encodeur direct, encodeur inverse et décodeur.

Le calcul avant et le calcul du train sont les mêmes que pour Seq2Seq.

Le code créé est https://github.com/kenchin110100/machine_learning/blob/master/sampleAttSeq2Seq.py C'est dedans.

Expérience

Corpus

J'ai utilisé le corpus des échecs de dialogue comme je l'ai fait la dernière fois. https://sites.google.com/site/dialoguebreakdowndetection/chat-dialogue-corpus

Résultat expérimental

Il existe 4 types d'énoncés comme avant

Voyons la réponse pour chaque époque.

Première 1 époque

Parlant:Bonjour=>réponse:  ['alors', 'est', 'Hey', '</s>']
Parlant:Comment ça va?=>réponse:  ['Oui', '、', 'quoi', 'À', 'Vous voyez', 'main', 'Masu', 'Ou', '?', '</s>']
Parlant:j'ai faim=>réponse:  ['Oui', '</s>']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['Oui', '、', 'quoi', 'À', 'Vous voyez', 'main', 'Masu', 'Ou', '?', '</s>']

Avez-vous eu un regard si méchant ...

3Epoch

Parlant:Bonjour=>réponse:  ['Bonjour.', '</s>']
Parlant:Comment ça va?=>réponse:  ['alors', 'est', '</s>']
Parlant:j'ai faim=>réponse:  ['coup de chaleur', 'À', 'Qi', 'À', 'Attacher', 'main', 'Masu', 'Ou', '?', '</s>']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['coup de chaleur', 'À', 'Qi', 'À', 'Attacher', 'Absent', 'Hmm', 'est', 'Ou', '?', '</s>']

5Epoch

Parlant:Bonjour=>réponse:  ['Je vous remercie', '</s>']
Parlant:Comment ça va?=>réponse:  ['pastèque', 'Est', 'Comme', 'est', 'Hey', '</s>']
Parlant:j'ai faim=>réponse:  ['coup de chaleur', 'À', 'Qi', 'À', 'Attacher', 'main', 'Vouloir', 'est', 'Ou', '?', '</s>']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['coup de chaleur', 'À', 'Qi', 'À', 'Attacher', 'main', 'Vouloir', 'est', 'Ou', '?', '</s>']

Je connais déjà le coup de chaleur ...

7Epoch

Parlant:Bonjour=>réponse:  ['Bonsoir', '</s>']
Parlant:Comment ça va?=>réponse:  ['pastèque', 'Est', 'Je t'aime', 'est', 'Hey', '</s>']
Parlant:j'ai faim=>réponse:  ['Bai', 'Bai', '</s>']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['coup de chaleur', 'À', 'Qi', 'À', 'Attacher', 'Absent', 'Hmm', 'est', 'Ou', '?', '</s>']

Bonjour => Bonsoir, c'est terrible ...

J'ai l'impression que la précision est pire que celle de Seq2Seq ...

Comme pour Seq2Seq, je n'ai pas été en mesure de bien répondre à la question "Comment ça va?" Peut-être que le mot «ton» n'a pas été utilisé dans le corpus.

Puisque l'original est un corpus d'échec de dialogue, le résultat de l'échec est renvoyé, c'est-à-dire qu'il se peut que vous appreniez bien avec cela ...

La transition de la valeur de perte totale et la comparaison du temps de calcul sont résumées à la fin.

Conclusion

J'ai calculé le modèle Seq2Seq + Attention en utilisant le chainer.

J'ai senti que le temps de calcul était extrêmement long par rapport au cas de Seq2Seq seul. La comparaison de cette zone sera ...

La prochaine fois, j'implémenterai CopyNet ... Je veux le faire.

Recommended Posts

Seq2Seq (2) ~ Attention Model edition ~ avec chainer
Seq2Seq (3) ~ Edition CopyNet ~ avec chainer
Seq2Seq (1) avec chainer
J'ai essayé d'implémenter Attention Seq2Seq avec PyTorch
Attention Seq2 Exécutez le modèle de dialogue avec Seq
Chargez le modèle caffe avec Chainer et classez les images
Reconnaissance d'image avec le modèle Caffe Chainer Yo!
Utiliser tensorboard avec Chainer
Régression avec un modèle linéaire
Démarrage USB avec Raspberry Pi 4 modèle B (3) édition LVM
Chainer Tech Circle ML # 8 avec modèle récurrent de langage neuronal
MVC - Édition de modèle pour apprendre de 0 avec un biais uniquement
J'ai essayé d'implémenter SSD avec PyTorch maintenant (édition du modèle)
Essayez d'implémenter RBM avec chainer.
Apprenez les orbites elliptiques avec Chainer
Utilisation du chainer avec Jetson TK1
Réseau de neurones commençant par Chainer
Implémentation du GAN conditionnel avec chainer
Génération de légende d'image avec Chainer
Calibrer le modèle avec PyCaret
Implémentation de SmoothGrad avec Chainer v2
Clustering embarqué profond avec Chainer 2.0