[PYTHON] Seq2Seq (1) avec chainer

Depuis que j'ai implémenté séquence à séquence avec chainer, son code et sa vérification

introduction

Un modèle bien connu pour générer des instructions à l'aide d'un réseau neuronal basé sur RNN est séquence à séquence (Seq2Seq).

Cette fois, je vais résumer les résultats dans la méthode et la vérification lors de la mise en œuvre de ce Seq2Seq à l'aide de chainer.

Sequence to Sequence(Seq2Seq)

Seq2Seq est un type de modèle de décodeur d'encodeur qui utilise RNN et peut être utilisé comme modèle pour le dialogue automatique et la traduction automatique.

Ceci est le papier original Sutskever, Ilya, Oriol Vinyals, and Quoc V. Le. "Sequence to sequence learning with neural networks." Advances in neural information processing systems. 2014.

Le contour du flux de Seq2Seq ressemble à ce qui suit

seq2seq.png

Par exemple, s'il y a un énoncé et une réponse tels que "Comment vous sentez-vous?" Ou "C'est plutôt bien", le côté Encodeur (bleu sur la figure) vectorise l'énoncé et le côté Décodeur (rouge sur la figure). , Former le RNN pour générer une réponse.

"<'EOS'>" est une abréviation pour End Of Statement, qui indique que la phrase se termine ici.

Le but de Seq2Seq est de saisir l'énoncé de la direction opposée, et si l'énoncé est "Comment vous sentez-vous?", Entrez "<?>, , , " dans l'encodeur dans cet ordre.

Des incorporations séparées sont utilisées du côté de l'encodeur et du côté du décodeur, et seule la couche intermédiaire générée (ligne rouge sur la figure) est partagée.

J'ai écrit Seq2Seq comme un réseau neuronal basé sur RNN, mais cette fois je l'ai implémenté en utilisant la mémoire à long terme (LSTM).

Pour une explication détaillée de LSTM, http://qiita.com/t_Signull/items/21b82be280b46f467d1b http://qiita.com/KojiOhki/items/89cd7b69a8a6239d67ca La zone est facile à comprendre.

Le point de LSTM est que LSTM lui-même a une cellule mémoire (comme une collection de mémoires), et quand une nouvelle entrée est faite, la cellule mémoire est oubliée (Forget Gate), mémorisée (Input Gate), sortie (Output Gate). C'est un point à opérer.

la mise en oeuvre

Cette fois, j'ai implémenté Seq2Seq en utilisant chainer.

Il y a beaucoup d'exemples de code qui implémentent Seq2Seq avec chainer, mais cette fois j'ai essayé de l'écrire aussi simple que possible (j'ai l'intention).

Le code de référence est https://github.com/odashi/chainer_examples est. Merci, oda.

Dans chainer, le modèle de NN est décrit comme une classe.

Encoder

Premièrement, un encodeur pour convertir des énoncés en vecteurs

encoder.py



class LSTM_Encoder(Chain):
    def __init__(self, vocab_size, embed_size, hidden_size):
        """
Initialisation de classe
        :param vocab_size:Nombre de types de mots utilisés (nombre de vocabulaire)
        :param embed_size:Taille des mots dans la représentation vectorielle
        :param hidden_size:Taille de couche intermédiaire
        """
        super(LSTM_Encoder, self).__init__(
            #Couche pour convertir des mots en vecteurs de mots
            xe = 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),
            #Couche pour convertir la couche intermédiaire de sortie à 4 fois la taille
            hh = links.Linear(hidden_size, 4 * hidden_size)
        )

    def __call__(self, x, c, h):
        """
Fonctionnement du codeur
        :param x: one-vecteur chaud
        :param c:Mémoire interne
        :param h:Couche cachée
        :return:Mémoire interne suivante, couche cachée suivante
        """
        #Convertir en vecteur de mot avec xe et multiplier ce vecteur par tanh
        e = functions.tanh(self.xe(x))
        #Saisie en ajoutant la valeur de la mémoire interne précédente, 4 fois la taille du vecteur de mot et 4 fois la taille de la couche intermédiaire.
        return functions.lstm(c, self.eh(e) + self.hh(h))

Le point d'encodeur est la raison pour laquelle le vecteur est converti à 4 fois la taille du calque caché spécifié.

Dans le document officiel du chainer, スクリーンショット 2017-02-23 18.06.40.png une.

En d'autres termes, "Parce que le vecteur d'entrée est divisé en oubli, entrée, sortie et cellule, faites-en quatre fois la taille."

Le functions.lstm de Chainer ne calcule que les fonctions, pas l'apprentissage du réseau. Donc hein et hh dans le code font cela à la place.

En fait, il existe une classe pratique appelée links.LSTM dans chainer qui ne sort que la sortie et apprend même si vous la saisissez, mais je ne l'ai pas utilisée cette fois. Parce que je veux partager la valeur de la couche cachée entre Encoder et Decoder (je pense que links.LSTM peut toujours être utilisé, mais cette fois c'est pour le futur ...).

Donc l'image du calcul ressemble à ceci, les lignes se chevauchent et c'est difficile à voir ...

encoder.png

Decoder

Ensuite, à propos du décodeur

decoder.py



class LSTM_Decoder(Chain):
    def __init__(self, vocab_size, embed_size, hidden_size):
        """
Initialisation de classe
        :param vocab_size:Nombre de types de mots utilisés (nombre de vocabulaire)
        :param embed_size:Taille des mots dans la représentation vectorielle
        :param hidden_size:Taille de vecteur intermédiaire
        """
        super(LSTM_Decoder, self).__init__(
            #Couche pour convertir les mots d'entrée en vecteurs de mots
            ye = links.EmbedID(vocab_size, embed_size, ignore_label=-1),
            #Une couche qui transforme un vecteur de mot en un vecteur quatre fois la taille d'un vecteur intermédiaire
            eh = links.Linear(embed_size, 4 * hidden_size),
            #Une couche qui transforme un vecteur intermédiaire en un vecteur quatre fois la taille du vecteur intermédiaire
            hh = links.Linear(hidden_size, 4 * hidden_size),
            #Calque pour convertir le vecteur de sortie à la taille du vecteur de mot
            he = links.Linear(hidden_size, embed_size),
            #Vecteur de mot au vecteur de taille de vocabulaire (un-Calque à convertir en vecteur chaud)
            ey = links.Linear(embed_size, vocab_size)
        )

    def __call__(self, y, c, h):
        """

        :param y: one-vecteur chaud
        :param c:Mémoire interne
        :param h:Vecteur intermédiaire
        :return:Mot prédit, prochaine mémoire interne, prochain vecteur intermédiaire
        """
        #Convertissez le mot entré en vecteur de mot et appliquez-le à tanh
        e = functions.tanh(self.ye(y))
        #Mémoire interne, 4 fois le mot vecteur+Multiplier LSTM par 4 fois le vecteur intermédiaire
        c, h = functions.lstm(c, self.eh(e) + self.hh(h))
        #Convertissez le vecteur intermédiaire de sortie en vecteur de mot et convertissez le vecteur de mot en un vecteur de sortie de la taille d'un vocabulaire
        t = self.ey(functions.tanh(self.he(h)))
        return t, c, h

Le décodeur rend également le vecteur quatre fois plus grand. La différence est que le vecteur intermédiaire de sortie est converti en un vecteur de la taille du nombre de vocabulaire.

Par conséquent, nous avons besoin de couches he et ey que Encoder n'avait pas.

L'image de ce calcul est la suivante

decoder.png

Decorder utilise le vecteur de sortie pour la rétro-propagation.

Seq2Seq

Le code ci-dessous est Seq2Seq créé en combinant ces encodeurs et décodeurs.

seq2seq.py



class Seq2Seq(Chain):
    def __init__(self, vocab_size, embed_size, hidden_size, batch_size, flag_gpu=True):
        """
Initialisation de Seq2Seq
        :param vocab_size:Taille de mot
        :param embed_size:Taille du vecteur Word
        :param hidden_size:Taille de vecteur intermédiaire
        :param batch_size:Mini taille de lot
        :param flag_gpu:Utiliser ou non le GPU
        """
        super(Seq2Seq, self).__init__(
            #Instanciation du codeur
            encoder = LSTM_Encoder(vocab_size, embed_size, hidden_size),
            #Instanciation du décodeur
            decoder = 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
        #Utilisez cupy lors du calcul avec GPU et numpy lors du calcul avec CPU
        if flag_gpu:
            self.ARR = cuda.cupy
        else:
            self.ARR = np

    def encode(self, words):
        """
La partie qui calcule l'encodeur
        :param words:Liste des mots enregistrés
        :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'))

        #Demandez à l'encodeur de lire les mots dans l'ordre
        for w in words:
            c, h = self.encoder(w, c, h)

        #Faire du vecteur intermédiaire calculé une variable d'instance à hériter du décodeur
        self.h = h
        #La mémoire interne n'est pas héritée, donc initialisez
        self.c = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))

    def decode(self, w):
        """
La partie qui calcule le décodeur
        :param w:mot
        :return:Sortie d'un vecteur de taille de nombre de mots
        """
        t, self.c, self.h = self.decoder(w, self.c, self.h)
        return t

    def reset(self):
        """
Vecteur intermédiaire, mémoire interne, initialisation du gradient
        :return:
        """
        self.h = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        self.c = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))

        self.zerograds()

Le calcul de la propagation vers l'avant à l'aide de la classe Seq2Seq est effectué comme suit.

forward.py



def forward(enc_words, dec_words, model, ARR):
    """
Une fonction qui calcule la propagation vers l'avant
    :param enc_words:Une liste de mots prononcés
    :param dec_words:Une liste de mots dans la phrase de réponse
    :param model:Instance Seq2 Seq
    :param ARR: cuda.cupy ou numpy
    :return:Perte totale calculée
    """
    #Enregistrer la taille du lot
    batch_size = len(enc_words[0])
    #Réinitialiser le dégradé stocké dans le modèle
    model.reset()
    #Remplacez le mot de la liste d'énoncés par le type Variable, qui est le type de chaînage.
    enc_words = [Variable(ARR.array(row, dtype='int32')) for row in enc_words]
    #Calcul d'encodage ⑴
    model.encode(enc_words)
    #Initialisation de la perte
    loss = Variable(ARR.zeros((), dtype='float32'))
    # <eos>Vers le décodeur(2)
    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(3)
        y = model.decode(t)
        #Convertir le mot correct en type variable
        t = Variable(ARR.array(w, dtype='int32'))
        #Calculez la perte en comparant le mot correct avec le mot prédit(4)
        loss += functions.softmax_cross_entropy(y, t)
    return loss

Le déroulement de ce calcul est illustré comme ceci

forward.png

Les mots dans enc_words et dec_words à apprendre doivent être identifiés à l'avance (convertis en nombres).

La fonction softmax est utilisée pour calculer la perte.

Tout ce que vous avez à faire est de laisser le chainer apprendre la perte calculée par forward et de mettre à jour le réseau.

Le code principal pour l'apprentissage est le suivant.

train.py



def train():
    #Vérifiez le nombre de vocabulaire
    vocab_size = len(word_to_id)
    #Instanciation du modèle
    model = Seq2Seq(vocab_size=vocab_size,
                    embed_size=EMBED_SIZE,
                    hidden_size=HIDDEN_SIZE,
                    batch_size=BATCH_SIZE,
                    flag_gpu=FLAG_GPU)
    #Initialisation du modèle
    model.reset()
    #Décidez si vous souhaitez utiliser le GPU
    if FLAG_GPU:
        ARR = cuda.cupy
        #Mettre le modèle dans la mémoire du GPU
        cuda.get_device(0).use()
        model.to_gpu(0)
    else:
        ARR = np

    #Commencer à apprendre
    for epoch in range(EPOCH_NUM):
        #Initialiser l'optimiseur pour chaque époque
        #Utilisez Adam en toute sécurité
        opt = optimizers.Adam()
        #Définir le modèle sur l'optimiseur
        opt.setup(model)
        #Ajuster si le dégradé est trop grand
        opt.add_hook(optimizer.GradientClipping(5))
        
        #Lecture des données d'entraînement créées à l'avance
        data = Filer.read_pkl(path)
        #Mélanger les données
        random.shuffle(data)
        #Début de l'apprentissage par lots
        for num in range(len(data)//BATCH_SIZE):
            #Créez un mini-lot de toute taille
            minibatch = data[num*BATCH_SIZE: (num+1)*BATCH_SIZE]
            #Création de données pour la lecture
            enc_words, dec_words = make_minibatch(minibatch)
            #Calcul de la perte par propagation directe
            total_loss = forward(enc_words=enc_words,
                                 dec_words=dec_words,
                                 model=model,
                                 ARR=ARR)
            #Calcul du gradient avec propagation de l'erreur en retour
            total_loss.backward()
            #Mettre à jour le réseau avec un gradient calculé
            opt.update()
            #Initialiser le dégradé enregistré
            opt.zero_grads()
        #Enregistrer le modèle pour chaque époque
        serializers.save_hdf5(outputpath, model)

C'était assez long, mais le code est terminé. Au fait, le code créé est https://github.com/kenchin110100/machine_learning/blob/master/sampleSeq2Sep.py C'est dedans.

Expérience

Corpus

Corpus des échecs de dialogue https://sites.google.com/site/dialoguebreakdowndetection/chat-dialogue-corpus A été utilisé.

Je voulais vraiment étudier avec un corpus plus long, mais j'ai abandonné car cela prenait trop de temps à étudier ...

Résultat expérimental

Les quatre énoncés suivants

Regardons la précision du modèle pour chaque Epoque

Première 1 époque

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

Êtes-vous philosophe?

Puis 3 Epoch

Parlant:Bonjour=>réponse:  ['Bonjour', 'Je vous remercie', 'Masu', '</s>']
Parlant:Comment ça va?=>réponse:  ['pastèque', 'Est', 'Je t'aime', 'est', 'Hey', '</s>']
Parlant:j'ai faim=>réponse:  ['alors', 'Nana', 'Hmm', 'est', 'Ou', '?', '</s>']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['quoi', 'Ou', 'À', 'Aller', 'main', 'Masu', 'Ou', '?', '</s>']

Je ne suis allé nulle part ...

5 Epoch

Parlant:Bonjour=>réponse:  ['Bonjour', 'Je vous remercie', 'Masu', '</s>']
Parlant:Comment ça va?=>réponse:  ['Mer', 'Est', 'un', 'Homme', 'alors', 'Aller', 'main', 'Masu', 'Ou', '?', '</s>']
Parlant:j'ai faim=>réponse:  ['Ouaip', '</s>']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['quoi', 'À', 'manger', 'Mieux', 'Ta', 'Ou', '?', '</s>']

Je ne suis même pas allé à la mer ...

Avec 8 Epoch ...

Parlant:Bonjour=>réponse:  ['Bonjour', 'Je vous remercie', 'Masu', '</s>']
Parlant:Comment ça va?=>réponse:  ['méduse', 'Est', 'Bien', 'est', 'Hey', '</s>']
Parlant:j'ai faim=>réponse:  ['Aussi', '</s>']
Parlant:Il fait chaud aujourd'hui=>réponse:  ['coup de chaleur', 'À', 'Qi', 'À', 'Attacher', 'Absent', 'Hmm', 'est', 'Ou', '?', '</s>']

Cela devient regrettable, mais est-ce la limite? J'ai aussi essayé plus d'Epoch, mais la précision n'a pas beaucoup changé.

Conclusion

Je viens d'implémenter Seq2Seq en utilisant chainer. Il semble que l'utilisation d'un corpus plus grand améliorera la précision, mais si la quantité de calcul devient trop importante, elle ne convergera pas facilement ...

Au fait, j'ai ajouté (1) au titre car je pense aux 2e et 3e puces! !! Ensuite, je voudrais ajouter une attention à ce Seq2Seq.

Recommended Posts

Seq2Seq (1) avec chainer
Seq2Seq (3) ~ Edition CopyNet ~ 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
Implémentation de SmoothGrad avec Chainer v2
Clustering embarqué profond avec Chainer 2.0
Un peu coincé dans le chainer
[Chainer] Apprentissage de XOR avec perceptron multicouche
Première reconnaissance faciale d'anime avec Chainer
Utilisation de Chainer avec CentOS7 [Construction de l'environnement]
Essayez l'apprentissage de la représentation commune avec le chainer
Apprenez à coloriser les images monochromes avec Chainer
Voir à travers la conversion de pétrissage de tarte avec Chainer
Classez les visages d'anime avec l'apprentissage en profondeur avec Chainer
Défiez DQN (Modoki) avec Chainer ✕ Open AI Gym!
Échantillons de chaîne
Essayez avec Chainer Deep Q Learning - Lancement
Meilleures pratiques personnelles lors de la mise au point avec Chainer
Chargez le modèle caffe avec Chainer et classez les images
Catégoriser les images de visage de personnages d'anime avec Chainer
J'ai essayé la séparation linéaire super facile avec Chainer
Reconnaissance d'image avec le modèle Caffe Chainer Yo!