Suite à l'implémentation de Seq2Seq dans Dernière fois, cette fois j'ai essayé d'implémenter Attention Seq2Seq avec Attention ajoutée à Seq2Seq avec PyTorch.
Même les débutants comme moi ne peuvent pas trouver beaucoup de code source qui implémente Attention dans PyTorch, et il y a aussi PyTorch Attention Tutorial. Il y en a, mais il semble que je n'ai pas appris le mini-batch (?), Et j'ai voulu implémenter une attention simple (?) Avec le sentiment qu'il semble être customisé pour cette tâche. J'ai essayé de mettre en œuvre Attention moi-même. Nous espérons pouvoir vous fournir des informations utiles pour ceux qui rencontrent des difficultés pour mettre en œuvre Attention.
Le mécanisme de l'attention est toujours [Deep Learning from scratch ❷ - Natural language processing](https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%] E3% 82% 89% E4% BD% 9C% E3% 82% 8B Apprentissage en profondeur-% E2% 80% 95% E8% 87% AA% E7% 84% B6% E8% A8% 80% E8% AA% 9E % E5% 87% A6% E7% 90% 86% E7% B7% A8-% E6% 96% 8E% E8% 97% A4-% E5% BA% B7% E6% AF% 85 / dp / 4873118360 / réf = sr_1_2? __ mk_ja_JP =% E3% 82% AB% E3% 82% BF% E3% 82% AB% E3% 83% 8A & mots-clés =% E3% 82% BC% E3% 83% AD% E3% 81% 8B% E3 % 82% 89% E4% BD% 9C% E3% 82% 8B & qid = 1568304570 & s = gateway & sr = 8-2) était extrêmement facile à comprendre.
L'exemple d'implémentation que je vais vous présenter est juste une implémentation scratch de Zero Work 2 (devrait être), donc si cet article est difficile à comprendre, je vous recommande fortement de lire Zero Work 2. ..
Je pense qu'il existe différents types d'attention, comme l'attention douce et l'attention dure, mais l'attention ici est Deep Learning from scratch ❷-Natural language processing. 82% BC% E3% 83% AD% E3% 81% 8B% E3% 82% 89% E4% BD% 9C% E3% 82% 8BApprentissage en profondeur-% E2% 80% 95% E8% 87% AA% E7 % 84% B6% E8% A8% 80% E8% AA% 9E% E5% 87% A6% E7% 90% 86% E7% B7% A8-% E6% 96% 8E% E8% 97% A4-% E5 % BA% B7% E6% AF% 85 / dp / 4873118360 / ref = sr_1_2? __Mk_ja_JP =% E3% 82% AB% E3% 82% BF% E3% 82% AB% E3% 83% 8A & mots-clés =% E3% 82 % BC% E3% 83% AD% E3% 81% 8B% E3% 82% 89% E4% BD% 9C% E3% 82% 8B & qid = 1568304570 & s = passerelle & sr = 8-2) (souple) Je ferai référence à Attention.
Seq2Seq a le problème que les caractéristiques des séries longues ne peuvent pas être capturées car Encoder le convertit en un vecteur de longueur fixe indépendamment de la longueur de la série d'entrée. Attention fournit un mécanisme pour prendre en compte la longueur de la séquence d'entrée du côté du codeur afin de résoudre ce problème.
Si vous expliquez Attention très grossièrement
Je vais faire l'opération. En 1., le nombre de vecteurs de calque cachés du côté de l'encodeur dépend de la longueur de la série qui est l'entrée du côté de l'encodeur, ainsi la forme prend en compte la longueur de la série. En 2., l'opération de sélection ne peut pas être différenciée, mais l'opération de sélection où prêter attention à chaque élément est pondérée de manière probabiliste avec $ softmax $.
Par souci de simplicité, la figure ci-dessous traite de deux cas où le côté codeur a trois séquences d'entrée w1, w2 et w3, et le côté décodeur a w'1, w'2.
① Lorsque la valeur de chaque couche cachée du côté de l'encodeur est $ h_1 $, $ h_2 $, $ \ cdots $, $ h_n $, $ hs = [h_1, h_2, \ cdots, h_n] $ est chaque couche du côté du décodeur. Passer au.
(2) Calculez le produit interne du vecteur de chaque couche cachée côté décodeur (ici, $ d_i $) et de chaque vecteur de $ hs $ $ h_1, h_2, \ cdots $. Cela signifie que nous calculons la similitude de chaque vecteur du côté du décodeur et de chaque vecteur de $ hs $. (Le produit interne est exprimé par $ (\ cdot, \ cdot) $.)
③ Convertissez le produit interne calculé en ② en une expression probabiliste avec $ softmax $ (cela s'appelle aussi poids d'attention)
④ Chaque élément de $ hs $ est pondéré par le poids d'attention et additionné pour former un vecteur (c'est aussi appelé un vecteur de contexte).
⑤ Combinez le vecteur de contexte et $ d_i $ en un seul vecteur
――Ajoutez les processus 1 à 5 expliqués ci-dessus du côté du décodeur et vous avez terminé. Il traite le problème de conversion du format de date ainsi que Zero Saku 2. (Parce qu'il est facile de confirmer la certitude lorsque le poids d'attention est visualisé)
Résolvons la tâche de conversion de divers styles d'écriture de date tels que les suivants au format AAAA-MM-JJ avec Attention seq 2seq.
Avant la conversion | Après la conversion |
---|---|
Nobenver, 30, 1995 | 1995-11-30 |
Monday, July 9, 2001 | 2001-07-09 |
1/23/01 | 2001-01-23 |
WEDNESDAY, AUGUST 1, 2001 | 2001-08-01 |
sep 7, 1981 | 1981-09-07 |
Nous empruntons les données du référentiel Github de Zero Work 2. https://github.com/oreilly-japan/deep-learning-from-scratch-2/tree/master/dataset
Placez ce fichier sur Google Drive et séparez-le avant et après la conversion comme suit.
from sklearn.model_selection import train_test_split
import random
from sklearn.utils import shuffle
#Montez Google Drive à l'avance et datez à l'emplacement suivant.Stocker txt
file_path = "drive/My Drive/Colab Notebooks/date.txt"
input_date = [] #Date des données avant la conversion
output_date = [] #Données de date après conversion
# date.Lire le txt ligne par ligne, diviser avant et après la conversion et séparer par entrée et sortie
with open(file_path, "r") as f:
date_list = f.readlines()
for date in date_list:
date = date[:-1]
input_date.append(date.split("_")[0])
output_date.append("_" + date.split("_")[1])
#Obtenir la longueur des séries d'entrée et de sortie
#Comme ils ont tous la même longueur, nous prenons len au 0ème élément
input_len = len(input_date[0]) # 29
output_len = len(output_date[0]) # 10
# date.Attribuez un identifiant à chaque caractère qui apparaît dans txt
char2id = {}
for input_chars, output_chars in zip(input_date, output_date):
for c in input_chars:
if not c in char2id:
char2id[c] = len(char2id)
for c in output_chars:
if not c in char2id:
char2id[c] = len(char2id)
input_data = [] #Données de date de pré-conversion identifiées
output_data = [] #Données de date converties identifiées
for input_chars, output_chars in zip(input_date, output_date):
input_data.append([char2id[c] for c in input_chars])
output_data.append([char2id[c] for c in output_chars])
# 7:Divisez en train et testez en 3
train_x, test_x, train_y, test_y = train_test_split(input_data, output_data, train_size= 0.7)
#Définir une fonction pour regrouper les données
def train2batch(input_data, output_data, batch_size=100):
input_batch = []
output_batch = []
input_shuffle, output_shuffle = shuffle(input_data, output_data)
for i in range(0, len(input_data), batch_size):
input_batch.append(input_shuffle[i:i+batch_size])
output_batch.append(output_shuffle[i:i+batch_size])
return input_batch, output_batch
Encoder
import torch
import torch.nn as nn
import torch.optim as optim
#Divers paramètres, etc.
embedding_dim = 200
hidden_dim = 128
BATCH_NUM = 100
vocab_size = len(char2id)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#Classe d'encodeur
class Encoder(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim):
super(Encoder, self).__init__()
self.hidden_dim = hidden_dim
self.word_embeddings = nn.Embedding(vocab_size, embedding_dim, padding_idx=char2id[" "])
self.gru = nn.GRU(embedding_dim, hidden_dim, batch_first=True)
def forward(self, sequence):
embedding = self.word_embeddings(sequence)
#hs est le vecteur de la couche cachée de GRU de chaque série
#Élément d'attention
hs, h = self.gru(embedding)
return hs, h
Decoder ――Similaire au côté encodeur, LSTM est changé en GRU par rapport à la fois précédente. ――Vous pouvez organiser votre esprit en écrivant sur un morceau de papier ce que signifie l'axe du tenseur de chaque couche. ――J'ai également répertorié la taille de chaque tenseur dans la couche Attention pour vous aider à la comprendre.
#Attention classe de décodeur
class AttentionDecoder(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, batch_size):
super(AttentionDecoder, self).__init__()
self.hidden_dim = hidden_dim
self.batch_size = batch_size
self.word_embeddings = nn.Embedding(vocab_size, embedding_dim, padding_idx=char2id[" "])
self.gru = nn.GRU(embedding_dim, hidden_dim, batch_first=True)
# hidden_dim*2 est torche le vecteur de contexte calculé par la couche cachée et la couche Attention de chaque série de GRU..Parce que la longueur est doublée en se connectant au chat
self.hidden2linear = nn.Linear(hidden_dim * 2, vocab_size)
#Je veux convertir la direction de la colonne avec probabilité, donc dim=1
self.softmax = nn.Softmax(dim=1)
def forward(self, sequence, hs, h):
embedding = self.word_embeddings(sequence)
output, state = self.gru(embedding, h)
#Couche d'attention
# hs.size() = ([100, 29, 128])
# output.size() = ([100, 10, 128])
#Sortie côté codeur en utilisant bmm(hs)Et la sortie côté décodeur(output)Afin de calculer la matrice pour chaque lot, fixez le lot comme sortie côté décodeur et prenez une matrice transposée.
t_output = torch.transpose(output, 1, 2) # t_output.size() = ([100, 128, 10])
#Calcul matriciel avec bmm en considérant le lot
s = torch.bmm(hs, t_output) # s.size() = ([100, 29, 10])
#Direction de la colonne(dim=1)Prenez softmax avec et convertissez-vous en expression probabiliste
#Cette valeur sera utilisée pour la visualisation de Attention plus tard, donc renvoyez-la avec retour.
attention_weight = self.softmax(s) # attention_weight.size() = ([100, 29, 10])
#Préparer un conteneur pour organiser le vecteur de contexte
c = torch.zeros(self.batch_size, 1, self.hidden_dim, device=device) # c.size() = ([100, 1, 128])
#Je ne savais pas comment calculer le vecteur de contexte pour la couche GRU de chaque décodeur à la fois.
#Extrayez le poids d'attention dans chaque couche (la couche GRU du côté du décodeur a 10 caractères car la chaîne de caractères générée est de 10 caractères) et créez un vecteur de contexte dans la boucle for.
#Étant donné que la direction du lot peut être calculée collectivement, le lot reste tel quel
for i in range(attention_weight.size()[2]): #10 boucles
# attention_weight[:,:,i].size() = ([100, 29])
#Prenez le poids d'attention pour la i-ème couche GRU, mais desserrez pour aligner la taille du tenseur avec hs
unsq_weight = attention_weight[:,:,i].unsqueeze(2) # unsq_weight.size() = ([100, 29, 1])
#Pondérer chaque vecteur de h avec le poids d'attention
weighted_hs = hs * unsq_weight # weighted_hs.size() = ([100, 29, 128])
#Créer un vecteur de contexte en ajoutant tous les vecteurs de chaque hs pondérés par le poids d'attention
weight_sum = torch.sum(weighted_hs, axis=1).unsqueeze(1) # weight_sum.size() = ([100, 1, 128])
c = torch.cat([c, weight_sum], dim=1) # c.size() = ([100, i, 128])
#Puisque l'élément zéro préparé comme une boîte reste, coupez-le et supprimez-le
c = c[:,1:,:]
output = torch.cat([output, c], dim=2) # output.size() = ([100, 10, 256])
output = self.hidden2linear(output)
return output, state, attention_weight
encoder = Encoder(vocab_size, embedding_dim, hidden_dim).to(device)
attn_decoder = AttentionDecoder(vocab_size, embedding_dim, hidden_dim, BATCH_NUM).to(device)
#Fonction de perte
criterion = nn.CrossEntropyLoss()
#optimisation
encoder_optimizer = optim.Adam(encoder.parameters(), lr=0.001)
attn_decoder_optimizer = optim.Adam(attn_decoder.parameters(), lr=0.001)
BATCH_NUM=100
EPOCH_NUM = 100
all_losses = []
print("training ...")
for epoch in range(1, EPOCH_NUM+1):
epoch_loss = 0
#Divisez les données en mini-lots
input_batch, output_batch = train2batch(train_x, train_y, batch_size=BATCH_NUM)
for i in range(len(input_batch)):
#Initialisation du gradient
encoder_optimizer.zero_grad()
attn_decoder_optimizer.zero_grad()
#Convertir les données en tenseur
input_tensor = torch.tensor(input_batch[i], device=device)
output_tensor = torch.tensor(output_batch[i], device=device)
#Propagation vers l'avant du codeur
hs, h = encoder(input_tensor)
#Attention Entrée décodeur
source = output_tensor[:, :-1]
#Attention Décodeur données de réponse correcte
target = output_tensor[:, 1:]
loss = 0
decoder_output, _, attention_weight= attn_decoder(source, hs, h)
for j in range(decoder_output.size()[1]):
loss += criterion(decoder_output[:, j, :], target[:, j])
epoch_loss += loss.item()
#Erreur de propagation de retour
loss.backward()
#Mise à jour des paramètres
encoder_optimizer.step()
attn_decoder_optimizer.step()
#Montrer la perte
print("Epoch %d: %.2f" % (epoch, epoch_loss))
all_losses.append(epoch_loss)
if epoch_loss < 0.1: break
print("Done")
# training ...
# Epoch 1: 1500.33
# Epoch 2: 77.53
# Epoch 3: 12.98
# Epoch 4: 3.40
# Epoch 5: 1.78
# Epoch 6: 1.13
# Epoch 7: 0.78
# Epoch 8: 0.56
# Epoch 9: 0.42
# Epoch 10: 0.32
# Epoch 11: 0.25
# Epoch 12: 0.20
# Epoch 13: 0.16
# Epoch 14: 0.13
# Epoch 15: 0.11
# Epoch 16: 0.09
# Done
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(all_losses)
――Il est prédit par presque la même méthode que la prédiction au moment de la précédente Seq2Seq.
import pandas as pd
#Renvoie l'index avec le plus grand élément du tenseur de sortie Decoder. Cela signifie le caractère généré
def get_max_index(decoder_output):
results = []
for h in decoder_output:
results.append(torch.argmax(h))
return torch.tensor(results, device=device).view(BATCH_NUM, 1)
#Données d'évaluation
test_input_batch, test_output_batch = train2batch(test_x, test_y)
input_tensor = torch.tensor(test_input_batch, device=device)
predicts = []
for i in range(len(test_input_batch)):
with torch.no_grad():
hs, encoder_state = encoder(input_tensor[i])
#Le décodeur indique d'abord le début de la génération de la chaîne de caractères"_"Parce que c'est une entrée"_"Créer un tenseur pour la taille du lot
start_char_batch = [[char2id["_"]] for _ in range(BATCH_NUM)]
decoder_input_tensor = torch.tensor(start_char_batch, device=device)
decoder_hidden = encoder_state
batch_tmp = torch.zeros(100,1, dtype=torch.long, device=device)
for _ in range(output_len - 1):
decoder_output, decoder_hidden, _ = attn_decoder(decoder_input_tensor, hs, decoder_hidden)
#Lors de l'acquisition du caractère prédit, il devient l'entrée du décodeur suivant tel quel
decoder_input_tensor = get_max_index(decoder_output.squeeze())
batch_tmp = torch.cat([batch_tmp, decoder_input_tensor], dim=1)
predicts.append(batch_tmp[:,1:])
#Lors de l'affichage du résultat de la prédiction, si l'ID reste tel quel, la lisibilité est médiocre, définissez donc un dictionnaire à convertir d'ID en chaîne de caractères pour restaurer la chaîne de caractères d'origine.
id2char = {}
for k, v in char2id.items():
id2char[v] = k
row = []
for i in range(len(test_input_batch)):
batch_input = test_input_batch[i]
batch_output = test_output_batch[i]
batch_predict = predicts[i]
for inp, output, predict in zip(batch_input, batch_output, batch_predict):
x = [id2char[idx] for idx in inp]
y = [id2char[idx] for idx in output[1:]]
p = [id2char[idx.item()] for idx in predict]
x_str = "".join(x)
y_str = "".join(y)
p_str = "".join(p)
judge = "O" if y_str == p_str else "X"
row.append([x_str, y_str, p_str, judge])
predict_df = pd.DataFrame(row, columns=["input", "answer", "predict", "judge"])
predict_df.head()
Il est arrivé que ce n'était pas 100% cette fois, mais je pense que ce sera environ 100% de taux de réponse correcte.
print(len(predict_df.query('judge == "O"')) / len(predict_df))
# 0.9999333333333333
predict_df.query('judge == "X"').head(10)
――J'ai fait une erreur dans le cas suivant ――Lorsque vous faites une erreur dans cette tâche, il semble qu'il existe de nombreux formats de date séparés par des barres obliques, comme indiqué ci-dessous.
―― Visualisons le poids de l'attention, qui est l'un des vrais plaisirs de l'attention. ―― Vous pouvez vérifier la certitude d'apprendre en regardant le poids d'attention.
import seaborn as sns
import pandas as pd
input_batch, output_batch = train2batch(test_x, test_y, batch_size=BATCH_NUM)
input_minibatch, output_minibatch = input_batch[0], output_batch[0]
with torch.no_grad():
#Convertir les données en tenseur
input_tensor = torch.tensor(input_minibatch, device=device)
output_tensor = torch.tensor(output_minibatch, device=device)
hs, h = encoder(input_tensor)
source = output_tensor[:, :-1]
decoder_output, _, attention_weight= attn_decoder(source, hs, h)
for i in range(3):
with torch.no_grad():
df = pd.DataFrame(data=torch.transpose(attention_weight[i], 0, 1).cpu().numpy(),
columns=[id2char[idx.item()] for idx in input_tensor[i]],
index=[id2char[idx.item()] for idx in output_tensor[i][1:]])
plt.figure(figsize=(12, 8))
sns.heatmap(df, xticklabels = 1, yticklabels = 1, square=True, linewidths=.3,cbar_kws = dict(use_gridspec=False,location="top"))
C'est un peu difficile à voir, mais les lettres «mardi 27 mars 2012» en bas de la figure ci-dessus sont les chaînes avant la conversion (entrée de l'encodeur), et «2012-03-27» disposé verticalement à gauche est généré. C'est un personnage. Voici comment lire la carte thermique, mais lorsque vous regardez les caractères générés de Decoder un par un, cela signifie que la couleur de la boîte de gauche est le caractère généré avec l'attention la plus brillante. Je pense que ce sera. (Veuillez préciser si c'est différent ...) (Bien sûr, si vous ajoutez toutes les valeurs dans la case de gauche, ce sera 1.)
Dans l'exemple ci-dessus, vous pouvez voir ce qui suit.
――On constate que vous faites attention à la partie année si vous générez AAAA dans son ensemble, et à la partie mois si vous générez MM. --Cette tâche est convertie en AAAA-MM-JJ, c'est-à-dire que le jour n'est pas converti, je ne fais donc pas attention aux caractères générés dans "Mardi"
D'ailleurs, il est attiré comme ça ↓
――Il semble qu'il existe différents modèles dans Attention comme décrit dans Zero work 2. ――Nous traiterons ensuite (?) De l'attention personnelle, qui est plus polyvalente que l'attention!
fin
Recommended Posts