Depuis que j'ai implémenté séquence à séquence avec chainer, son code et sa vérification
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
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 "<?>,
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.
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, 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 ...
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
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
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.
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 ...
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é.
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