[PYTHON] Seq2Seq (3) ~ Edition CopyNet ~ avec chainer

Explication de CopyNet, la troisième version de seq2seq, et son implémentation

introduction

Synopsis jusqu'à la dernière fois http://qiita.com/kenchin110100/items/b34f5106d5a211f4c004 http://qiita.com/kenchin110100/items/eb70d69d1d65fb451b67

Normal seq2seq, Attention Model, cette fois j'ai implémenté CopyNet.

Nous expliquerons d'abord CopyNet, puis l'implémentation et ses résultats.

CopyNet

Qu'est-ce que CopyNet?

Pour expliquer CopyNet, commençons par un examen de Seq2Seq.

Sequence to Sequence
seq2seq.png

Seq2Seq est un type de modèle de décodeur d'encodeur, dans lequel l'encodeur convertit la phrase parlée ("Comment vous sentez-vous?") En vecteur, et le décodeur génère la phrase de réponse ("je me sens bien") à partir de ce vecteur.

Dans l'encodeur de Seq2Seq, seul le dernier vecteur intermédiaire de sortie a été pris en compte, mais le modèle d'attention devait prendre en compte des vecteurs intermédiaires plus divers.

Attention Model
attention.png

Maintenant, pensez à ce que vous faites avec CopyNet, lorsque l'énoncé est "Comment vous sentez-vous?" Et la réponse est "Je me sens bien".

Le mot est utilisé à la fois dans le discours et dans la réponse. L'idée de CopyNet est de permettre au côté du décodeur de générer plus facilement les mots utilisés dans le discours.

CopyNet
copynet.png

(La figure est juste une image)

La raison pour laquelle CopyNet est bon est qu'il peut gérer des mots inconnus. Par exemple, même si vous n'avez pas le mot lors de l'apprentissage, vous pouvez répondre en utilisant le mot en le copiant.

Dans ce qui suit, je présenterai deux articles sur CopyNet.

Jiatao Gu et al.

Ceci est le papier copynet original Gu, Jiatao, et al. "Incorporating copying mechanism in sequence-to-sequence learning." arXiv preprint arXiv:1603.06393 (2016).

Gu, Jiatao, et al
Capture d'écran 2017-03-11 17.49.04.png

La figure utilisée dans l'article est celle ci-dessus, mais si vous regardez de plus près, elle ressemblera à la figure ci-dessous.

Copy mode and StateUpdate
Gu.png

Dans la méthode proposée par Gu et al., Il existe deux mécanismes principaux, StateUpdate et CopyMode.

Dans StateUpdate, si le mot entré dans Decoder est un mot () inclus dans l'énoncé, le vecteur intermédiaire de ce mot (sorti par Encoder) est entré.

Dans CopyMode, si le mot que vous prévoyez de générer est inclus dans la phrase prononcée (), un vecteur intermédiaire est utilisé pour augmenter la probabilité d'occurrence de afin que le mot puisse être facilement sorti.

(L'explication est assez mauvaise, mais s'il vous plaît lire le papier pour plus de détails ...)

Ziqiang Cao et al.

Je voudrais présenter un autre article lié à CopyNet. À proprement parler, ce n'est pas CopyNet, mais il existe les articles suivants qui mettent en œuvre un mécanisme similaire.

Cao, Ziqiang, et al. "Joint Copying and Restricted Generation for Paraphrase." arXiv preprint arXiv:1611.09235 (2016).

Ziqiang Cao et al.
Capture d'écran 2017-03-11 18.16.25.png

(Figure utilisée dans l'article)

Celui-ci est un peu plus simple, et si vous l'expliquez brièvement, ce sera comme suit.

Restricted Generative Decoder
cao.png

La politique consiste à utiliser le poids calculé par le modèle d'attention tel quel.

S'il n'y a pas de mot dans l'entrée qui devrait être sorti, la probabilité du mot généré est utilisée telle quelle. S'il y a un mot dans l'entrée qui devrait être sorti (), utilisez la moyenne de la probabilité du mot généré et du poids calculé par le modèle d'attention par λ (λ est compris entre 0 et 1). Scalaire).

Le point est de savoir comment équilibrer ce λ, mais nous apprendrons également λ. (Veuillez lire l'article pour plus de détails ...)

la mise en oeuvre

Cette fois, Chainer a mis en œuvre la méthode de Ziqiang Cao et al. Il n'y a pas beaucoup d'implémentations de CopyNet sur le net, et je suis désolé si j'ai fait une erreur ...

L'encodeur et le décodeur utilisent le modèle utilisé au moment de Attention Model tel quel.

Attention

C'est fondamentalement le même que le modèle d'attention, mais le poids de chaque vecteur intermédiaire est également modifié pour être sorti.

attention.py


class Copy_Attention(Attention):

    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 att_f:Moyenne pondérée du vecteur intermédiaire du codeur direct
        :return att_b:Moyenne pondérée du vecteur intermédiaire du codeur inverse
        :return att:Poids de chaque vecteur intermédiaire
        """
        #Rappelez-vous la taille du mini lot
        batch_size = h.data.shape[0]
        #Initialisation de la liste pour enregistrer les poids
        ws = []
        att = []
        #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 = self.hw(functions.tanh(self.fh(f)+self.bh(b)+self.hh(h)))
            att.append(w)
            #Normaliser à l'aide de la fonction softmax
            w = functions.exp(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 i, (f, b, w) in enumerate(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(f, w), (batch_size, self.hidden_size))
        att = functions.concat(att, axis=1)
        return att_f, att_b, att

Seq2Seq with CopyNet

Le modèle qui combine Encoder, Decorder et Attention est le suivant.

copy_seq2seq.py


class Copy_Seq2Seq(Chain):
    def __init__(self, vocab_size, embed_size, hidden_size, batch_size, flag_gpu=True):
        super(Copy_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=Copy_Attention(hidden_size, flag_gpu),
            # Decoder
            decoder=Att_LSTM_Decoder(vocab_size, embed_size, hidden_size),
            #Réseau de calcul du poids de λ
            predictor=links.Linear(hidden_size, 1)
        )
        self.vocab_size = vocab_size
        self.embed_size = embed_size
        self.hidden_size = hidden_size
        self.batch_size = batch_size
        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 t:Mot prédictif
        :return att:Attention poids pour chaque mot
        :return lambda_:Poids pour déterminer si Copier est important ou Générer est important
        """
        #Calculer le vecteur d'entrée avec le modèle d'attention
        att_f, att_b, att = self.attention(self.fs, self.bs, self.h)
        #Vecteur d'entrée vers le décodeur
        t, self.c, self.h = self.decoder(w, self.c, self.h, att_f, att_b)
        #Calcul de λ à l'aide du vecteur intermédiaire calculé
        lambda_ = self.predictor(self.h)
        return t, att, lambda_

En fait, ce n'est pas très différent du modèle d'attention. Le changement est qu'il génère également le poids Attention, qui calcule λ pour équilibrer le mode copie et le mode générateur.

forward

Le grand changement concerne la fonction avancée. La fonction d'avance examine la phrase d'entrée et le mot que vous voulez sortir pour déterminer s'il faut calculer le mode de copie.

forward.py


def forward(enc_words, dec_words, model, ARR):
    """
Fonction pour calculer en avant
    :param enc_words:Déclaration d'entrée
    :param dec_words:Instruction de sortie
    :param model:modèle
    :param ARR:numpy ou cuda.Soit cupy
    :return loss:perte
    """
    #Enregistrer la taille du lot
    batch_size = len(enc_words[0])
    #Réinitialiser le dégradé enregistré dans le modèle
    model.reset()
    #Préparez une liste pour vérifier les mots utilisés dans la phrase d'entrée
    enc_key = enc_words.T
    #Modifiez l'instruction saisie dans Encoder en type Variable
    enc_words = [Variable(ARR.array(row, dtype='int32')) for row in enc_words]
    #Calcul du codeur
    model.encode(enc_words)
    #Initialisation de la perte
    loss = Variable(ARR.zeros((), dtype='float32'))
    # <eos>Vers le décodeur
    t = Variable(ARR.array([0 for _ in range(batch_size)], dtype='int32'))
    #Calcul du décodeur
    for w in dec_words:
        #Décoder mot par mot
        y, att, lambda_ = model.decode(t)
        #Convertir le mot correct en type variable
        t = Variable(ARR.array(w, dtype='int32'))

        #Journal de mots calculé en mode générateur_Prenez softmax
        s = functions.log_softmax(y)
        #Attention journal de poids_Prenez softmax
        att_s = functions.log_softmax(att)
        #En multipliant lambda par la fonction sigmoïde, 0~Changer pour une valeur de 1
        lambda_s = functions.reshape(functions.sigmoid(lambda_), (batch_size,))
        #Initialisation de la perte de mode génératif
        Pg = Variable(ARR.zeros((), dtype='float32'))
        #Initialisation de la perte du mode copie
        Pc = Variable(ARR.zeros((), dtype='float32'))
        #Initialisation de la perte pour apprendre l'équilibre lambda
        epsilon = Variable(ARR.zeros((), dtype='float32'))
        #À partir de là, la perte de chaque mot du lot est calculée et l'instruction for est inversée ...
        counter = 0
        for i, words in enumerate(w):
            # -1 est l'étiquette attachée au mot que vous n'apprenez pas. Ignorez cela.
            if words != -1:
                #Calcul de la perte de mode génératif
                Pg += functions.get_item(functions.get_item(s, i), words) * functions.reshape((1.0 - functions.get_item(lambda_s, i)), ())
                counter += 1
                #Si vous souhaitez sortir un mot dans la phrase d'entrée
                if words in enc_key[i]:
                    #Calculer le mode de copie
                    Pc += functions.get_item(functions.get_item(att_s, i), list(enc_key[i]).index(words)) * functions.reshape(functions.get_item(lambda_s, i), ())
                    #Apprenez à rendre lambda meilleur que le mode copie
                    epsilon += functions.log(functions.get_item(lambda_s, i))
                #S'il n'y a pas de mot que vous souhaitez afficher dans la phrase d'entrée
                else:
                    #Apprenez à rendre lambda meilleur que le mode génératif
                    epsilon += functions.log(1.0 - functions.get_item(lambda_s, i))
        #Divisez chaque perte par la taille du lot et additionnez
        Pg *= (-1.0 / np.max([1, counter]))
        Pc *= (-1.0 / np.max([1, counter]))
        epsilon *= (-1.0 / np.max([1, counter]))
        loss += Pg + Pc + epsilon
    return loss

Dans le code, trois pertes, Pg, Pc et epsilon, sont définies et calculées afin d'apprendre chacun des modes Génératif, Copie et λ.

Le but est d'utiliser functions.log_softmax. Si vous définissez log (softmax (x)), une erreur se produira lorsque le calcul de softmax devient 0, mais cette fonction le fait bien (comment cela fonctionne est un mystère ...).

Si vous utilisez la fonction functions.softmax_cross_entropy, vous n'avez pas besoin d'un calcul aussi gênant, mais cette fois je veux équilibrer la perte du mode copie et la perte du mode génératif avec λ, donc j'utilise les fonctions functions.get_items et functions.log_softmax pour faire la perte. Est en cours de calcul.

Si vous connaissez une meilleure implémentation, faites-le moi savoir ...

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

Expérience

Corpus

J'ai utilisé le corpus des échecs de dialogue comme avant. https://sites.google.com/site/dialoguebreakdowndetection/chat-dialogue-corpus

Résultat expérimental

Les 4 types d'énoncés suivants

Nous examinerons les résultats de réponse pour chaque Epoque.

Epoch 1

Parlant:Bonjour=>réponse:  ['Bonjour', '</s>'] ['copy', 'copy']
Parlant:Comment ça va?=>réponse:  ['État', 'Est', 'Est', 'est', 'est', '</s>'] ['copy', 'copy', 'copy', 'copy', 'copy', 'copy']
Parlant:j'ai faim=>réponse:  ['estomac', 'Mais', 'Mais', 'Mais', 'Ta', 'Ta', 'est', '</s>'] ['copy', 'copy', 'copy', 'copy', 'copy', 'copy', 'gen', 'copy']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['aujourd'hui', 'Est', 'Est', 'est', 'est', '</s>'] ['copy', 'copy', 'copy', 'copy', 'copy', 'copy']

C'est complètement cassé ...

Epoch 3

Parlant:Bonjour=>réponse:  ['Bonjour', '</s>'] ['copy', 'copy']
Parlant:Comment ça va?=>réponse:  ['État', 'Est', '</s>'] ['copy', 'gen', 'copy']
Parlant:j'ai faim=>réponse:  ['estomac', '</s>'] ['copy', 'copy']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['chaud', 'Est', 'Comme', 'est', 'Hey', '</s>'] ['copy', 'copy', 'gen', 'gen', 'gen', 'copy']

Epoch 5

Parlant:Bonjour=>réponse:  ['Bonjour', '</s>'] ['copy', 'copy']
Parlant:Comment ça va?=>réponse:  ['État', 'Est', 'Comme', 'est', 'Ou', '</s>'] ['copy', 'copy', 'gen', 'copy', 'gen', 'copy']
Parlant:j'ai faim=>réponse:  ['estomac', '</s>'] ['copy', 'copy']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['chaud', 'est', '</s>'] ['copy', 'gen', 'copy']

Même si vous êtes déclaré affamé ...

Epoch 7

Parlant:Bonjour=>réponse:  ['Bonjour', 'Je vous remercie', 'Masu', '</s>'] ['copy', 'gen', 'gen', 'copy']
Parlant:Comment ça va?=>réponse:  ['État', 'Est', '</s>'] ['copy', 'gen', 'copy']
Parlant:j'ai faim=>réponse:  ['estomac', 'Mais', 'Gratuit', 'Mieux', 'Ta', '</s>'] ['copy', 'gen', 'copy', 'copy', 'gen', 'gen']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['chaud', 'est', '</s>'] ['copy', 'gen', 'copy']

Le mot n'est pas inclus dans l'apprentissage, on peut donc dire qu'il est bien copié à cet égard.

Cependant, honnêtement, j'aimerais que vous répondiez un peu mieux.

Il apprend à la fois le mode copie et le mode génération, il semble donc que le décodeur n'ait pas complètement formé le modèle de langage.

Cela peut être lié au fait que le document a été évalué non pas par la tâche de dialogue mais par la tâche de synthèse. (Eh bien, la cause numéro un peut être la mise en œuvre ...)

Conclusion

J'ai implémenté CopyNet en utilisant chainer. J'ai fait le modèle de dialogue trois fois, donc je suis déjà plein lol La prochaine fois, je ferai autre chose.

Recommended Posts

Seq2Seq (3) ~ Edition CopyNet ~ avec chainer
Seq2Seq (1) avec chainer
Seq2Seq (2) ~ Attention Model edition ~ avec chainer
Utiliser tensorboard avec Chainer
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
Implémentation de SmoothGrad avec Chainer v2
Clustering embarqué profond avec Chainer 2.0
Un peu coincé dans le chainer
Perceptron multicouche avec chaînette: ajustement fonctionnel
Essayez de prédire les courses de chevaux avec Chainer
J'ai essayé d'implémenter Attention Seq2Seq avec PyTorch
[Chainer] Apprentissage de XOR avec perceptron multicouche
Première reconnaissance faciale d'anime avec Chainer
Exécuter l'inférence avec l'exemple de Chainer 2.0 MNIST
Utilisation de Chainer avec CentOS7 [Construction de l'environnement]
Essayez l'apprentissage de la représentation commune avec le chainer
Ingénierie de quantité de fonctionnalités voyageant avec Pokemon-Version numérique