Explication de CopyNet, la troisième version de seq2seq, et son implémentation
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
Pour expliquer CopyNet, commençons par un examen de Seq2Seq.
Sequence to Sequence |
---|
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 |
---|
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
CopyNet |
---|
(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
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 |
---|
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 |
---|
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 (
Dans CopyMode, si le mot que vous prévoyez de générer est inclus dans la phrase prononcée (
(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.
Ziqiang Cao et al. |
---|
(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 |
---|
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 (
Le point est de savoir comment équilibrer ce λ, mais nous apprendrons également λ. (Veuillez lire l'article pour plus de détails ...)
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.
J'ai utilisé le corpus des échecs de dialogue comme avant. https://sites.google.com/site/dialoguebreakdowndetection/chat-dialogue-corpus
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
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 ...)
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