Nach der Implementierung von Seq2Seq in Letztes Mal habe ich diesmal versucht, Attention Seq2Seq mit Attention zu Seq2Seq mit PyTorch zu implementieren.
Selbst Anfänger wie ich können nicht viel Quellcode finden, der Attention in PyTorch implementiert, und es gibt auch PyTorch Attention Tutorial. Es gibt, aber es scheint, dass ich Mini-Batch (?) Nicht gelernt habe, und ich wollte eine einfachere (?) Aufmerksamkeit implementieren, mit dem Gefühl, dass es für diese Aufgabe angepasst zu sein scheint. Ich habe versucht, Attention selbst zu implementieren. Wir würden uns freuen, wenn Sie denjenigen, die Probleme bei der Implementierung von Attention haben, nützliche Informationen zur Verfügung stellen könnten.
Der Mechanismus der Aufmerksamkeit ist immer noch [Deep Learning von Grund auf ❷ - Verarbeitung natürlicher Sprache](https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%] E3% 82% 89% E4% BD% 9C% E3% 82% 8BTiefenlernen-% 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 & Schlüsselwörter =% 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) war überwiegend leicht zu verstehen.
Das Implementierungsbeispiel, das ich vorstellen werde, ist nur eine Scratch-Implementierung von Zero Work 2 (sollte es sein). Wenn dieser Artikel schwer zu verstehen ist, empfehle ich dringend, Zero Work 2 zu lesen. ..
Ich denke, es gibt verschiedene Arten von Aufmerksamkeit, wie z. B. weiche und harte Aufmerksamkeit, aber die Aufmerksamkeit hier ist Deep Learning von Grund auf ❷-Verarbeitung natürlicher Sprache. 82% BC% E3% 83% AD% E3% 81% 8B% E3% 82% 89% E4% BD% 9C% E3% 82% 8BDeep-Learning-% 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 & Schlüsselwörter =% 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) (weich) Ich werde auf Aufmerksamkeit verweisen.
Seq2Seq hat das Problem, dass die Eigenschaften langer Serien nicht erfasst werden können, da der Encoder sie unabhängig von der Länge der Eingabeserien in einen Vektor fester Länge konvertiert. Achtung bietet einen Mechanismus, um die Länge der Eingabesequenz auf der Encoderseite zu berücksichtigen, um dieses Problem zu lösen.
Wenn Sie Aufmerksamkeit sehr grob erklären
Ich werde die Operation machen. In 1. hängt die Anzahl der verborgenen Schichtvektoren auf der Encoderseite von der Länge der Reihe ab, die auf der Encoderseite eingegeben wird, sodass die Form die Länge der Reihe berücksichtigt. In 2. kann die Auswahloperation nicht unterschieden werden, aber die Auswahloperation, wo auf jedes Element geachtet werden soll, wird wahrscheinlich mit $ softmax $ gewichtet.
Der Einfachheit halber werden in der folgenden Abbildung zwei Fälle behandelt, in denen die Encoderseite drei Eingangssequenzen w1, w2 und w3 und die Decoderseite drei w'1, w'2 aufweist.
① Wenn der Wert jeder verborgenen Ebene auf der Encoderseite $ h_1 $, $ h_2 $, $ \ cdots $, $ h_n $, $ hs = [h_1, h_2, \ cdots, h_n] $ ist, ist jede Ebene auf der Decoderseite. Übergeben an.
(2) Berechnen Sie das innere Produkt des Vektors jeder verborgenen Schicht auf der Decoderseite (hier $ d_i $) und jedes Vektors von $ hs $ $ h_1, h_2, \ cdots $. Dies bedeutet, dass wir berechnen, wie ähnlich jeder Vektor auf der Decoderseite und jeder Vektor auf $ hs $ sind. (Das innere Produkt wird als $ (\ cdot, \ cdot) $ ausgedrückt.)
③ Konvertieren Sie das in ② berechnete innere Produkt mit $ softmax $ in einen probabilistischen Ausdruck (dies wird auch als Aufmerksamkeitsgewicht bezeichnet).
④ Jedes Element von $ hs $ wird mit dem Aufmerksamkeitsgewicht gewichtet und zu einem Vektor addiert (dies wird auch als Kontextvektor bezeichnet).
⑤ Kombinieren Sie den Kontextvektor und $ d_i $ zu einem einzigen Vektor
――Fügen Sie die oben erläuterten Prozesse 1 bis 5 zur Decoder-Seite hinzu, und Sie sind fertig. Es befasst sich mit dem Problem der Datumsformatkonvertierung sowie mit Zero Saku 2. (Weil es einfach ist, die Sicherheit zu bestätigen, wenn das Aufmerksamkeitsgewicht visualisiert wird)
Lösen wir die Aufgabe, verschiedene Datumsschreibstile wie den folgenden in das Format JJJJ-MM-TT mit Attention seq 2seq zu konvertieren.
Vor der Konvertierung | Nach der Konvertierung |
---|---|
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 |
Wir leihen Daten aus dem Github-Repository von Zero Work 2 aus. https://github.com/oreilly-japan/deep-learning-from-scratch-2/tree/master/dataset
Stellen Sie diese Datei auf Google Drive und trennen Sie sie vor und nach der Konvertierung wie folgt.
from sklearn.model_selection import train_test_split
import random
from sklearn.utils import shuffle
#Mounten Sie Google Drive im Voraus und datieren Sie es an den folgenden Ort.Speichern Sie txt
file_path = "drive/My Drive/Colab Notebooks/date.txt"
input_date = [] #Datumsdaten vor der Konvertierung
output_date = [] #Datumsdaten nach Konvertierung
# date.Lesen Sie txt Zeile für Zeile, teilen Sie es vor und nach der Konvertierung und trennen Sie es durch Eingabe und Ausgabe
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])
#Ermitteln Sie die Länge der Eingabe- und Ausgabeserie
#Da sie alle gleich lang sind, nehmen wir len beim 0. Element
input_len = len(input_date[0]) # 29
output_len = len(output_date[0]) # 10
# date.Weisen Sie jedem Zeichen, das in txt erscheint, eine ID zu
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 = [] #Identifizierte Datumsdaten vor der Konvertierung
output_data = [] #ID-konvertierte Datumsdaten
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:In Zug teilen und in 3 testen
train_x, test_x, train_y, test_y = train_test_split(input_data, output_data, train_size= 0.7)
#Definieren Sie eine Funktion zum Stapeln von Daten
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
#Verschiedene Parameter usw.
embedding_dim = 200
hidden_dim = 128
BATCH_NUM = 100
vocab_size = len(char2id)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#Encoder-Klasse
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 ist der Vektor der verborgenen GRU-Schicht jeder Reihe
#Aufmerksamkeitselement
hs, h = self.gru(embedding)
return hs, h
Decoder ―― Ähnlich wie bei der Encoder-Seite wird LSTM im Vergleich zur vorherigen Zeit in GRU geändert. ――Wenn Sie es implementieren, während Sie auf ein Blatt Papier schreiben, was die Achse des Tensors jeder Schicht bedeutet, können Sie Ihren Geist organisieren. ――Ich habe auch die Größe jedes Tensors in der Aufmerksamkeitsebene aufgelistet, um Ihnen das Verständnis zu erleichtern.
#Achtung Decoder Klasse
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 ist der Kontextvektor, der durch die verborgene Schicht und die Aufmerksamkeitsschicht jeder GRU-Reihe berechnet wird..Weil sich die Länge durch die Verbindung mit der Katze verdoppelt
self.hidden2linear = nn.Linear(hidden_dim * 2, vocab_size)
#Ich möchte die Spaltenrichtung mit Wahrscheinlichkeit konvertieren, also dimmen=1
self.softmax = nn.Softmax(dim=1)
def forward(self, sequence, hs, h):
embedding = self.word_embeddings(sequence)
output, state = self.gru(embedding, h)
#Aufmerksamkeitsschicht
# hs.size() = ([100, 29, 128])
# output.size() = ([100, 10, 128])
#Ausgabe auf der Encoderseite mit bmm(hs)Und der Ausgang auf der Decoderseite(output)Um die Matrix für jede Charge zu berechnen, legen Sie die Charge als Ausgabe auf der Decoderseite fest und nehmen Sie eine transponierte Matrix.
t_output = torch.transpose(output, 1, 2) # t_output.size() = ([100, 128, 10])
#Matrixberechnung mit bmm unter Berücksichtigung der Charge
s = torch.bmm(hs, t_output) # s.size() = ([100, 29, 10])
#Spaltenrichtung(dim=1)Nehmen Sie softmax mit und konvertieren Sie in probabilistischen Ausdruck
#Dieser Wert wird später zur Visualisierung von Attention verwendet. Geben Sie ihn daher mit return zurück.
attention_weight = self.softmax(s) # attention_weight.size() = ([100, 29, 10])
#Bereiten Sie einen Container vor, um den Kontextvektor zu organisieren
c = torch.zeros(self.batch_size, 1, self.hidden_dim, device=device) # c.size() = ([100, 1, 128])
#Ich wusste nicht, wie ich den Kontextvektor für die GRU-Schicht jedes Decoders gleichzeitig berechnen sollte.
#Extrahieren Sie das Aufmerksamkeitsgewicht in jeder Ebene (die GRU-Ebene auf der Decoderseite hat 10 Zeichen, da die generierte Zeichenfolge 10 Zeichen enthält) und erstellen Sie einen Kontextvektor in der for-Schleife.
#Da die Chargenrichtung gemeinsam berechnet werden kann, bleibt die Charge unverändert
for i in range(attention_weight.size()[2]): #10 Schleifen
# attention_weight[:,:,i].size() = ([100, 29])
#Nehmen Sie das Aufmerksamkeitsgewicht für die i-te GRU-Schicht, aber drücken Sie nicht, um die Tensorgröße mit hs auszurichten
unsq_weight = attention_weight[:,:,i].unsqueeze(2) # unsq_weight.size() = ([100, 29, 1])
#Gewichten Sie jeden Vektor von hs mit Aufmerksamkeitsgewicht
weighted_hs = hs * unsq_weight # weighted_hs.size() = ([100, 29, 128])
#Erstellen Sie einen Kontextvektor, indem Sie alle Vektoren jedes hs addieren, gewichtet mit dem Aufmerksamkeitsgewicht
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])
#Da das als Box vorbereitete Nullelement erhalten bleibt, schneiden Sie es in Scheiben und löschen Sie es
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)
#Verlustfunktion
criterion = nn.CrossEntropyLoss()
#Optimierung
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
#Teilen Sie die Daten in Mini-Batch
input_batch, output_batch = train2batch(train_x, train_y, batch_size=BATCH_NUM)
for i in range(len(input_batch)):
#Gradienteninitialisierung
encoder_optimizer.zero_grad()
attn_decoder_optimizer.zero_grad()
#Daten in Tensor konvertieren
input_tensor = torch.tensor(input_batch[i], device=device)
output_tensor = torch.tensor(output_batch[i], device=device)
#Vorwärtsausbreitung des Encoders
hs, h = encoder(input_tensor)
#Achtung Decoder-Eingang
source = output_tensor[:, :-1]
#Richtige Antwortdaten des Attention Decoders
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()
#Fehler bei der Weitergabe
loss.backward()
#Parameteraktualisierung
encoder_optimizer.step()
attn_decoder_optimizer.step()
#Verlust zeigen
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)
――Es wird mit fast der gleichen Methode vorhergesagt wie die Vorhersage zum Zeitpunkt der vorherigen Seq2Seq.
import pandas as pd
#Gibt den Index mit dem größten Element vom Decoder-Ausgangstensor zurück. Das heißt das generierte Zeichen
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)
#Bewertungsdaten
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])
#Der Decoder zeigt zuerst den Beginn der Zeichenfolgengenerierung an"_"Weil es eine Eingabe ist"_"Erstellen Sie einen Tensor für die Stapelgröße
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)
#Beim Erfassen des vorhergesagten Zeichens wird es so wie es ist die Eingabe des nächsten Decoders
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:])
#Wenn beim Anzeigen des Vorhersageergebnisses die ID unverändert bleibt, ist die Lesbarkeit schlecht. Definieren Sie daher ein Wörterbuch, das von der ID in eine Zeichenfolge konvertiert werden soll, um die ursprüngliche Zeichenfolge wiederherzustellen.
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()
――Es ist passiert, dass es diesmal nicht 100% war, aber ich denke, es wird ungefähr 100% richtige Antwortrate sein.
print(len(predict_df.query('judge == "O"')) / len(predict_df))
# 0.9999333333333333
predict_df.query('judge == "X"').head(10)
――Ich habe im folgenden Fall einen Fehler gemacht ――Wenn Sie bei dieser Aufgabe einen Fehler machen, gibt es anscheinend viele Datumsformate, die durch Schrägstriche getrennt sind (siehe unten).
――Lass uns das Aufmerksamkeitsgewicht visualisieren, was eine der wahren Freuden der Aufmerksamkeit ist. ――Sie können die Sicherheit des Lernens überprüfen, indem Sie das Aufmerksamkeitsgewicht betrachten.
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():
#Daten in Tensor konvertieren
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"))
Es ist etwas schwer zu erkennen, aber die Zeichen "Dienstag, 27. März 2012" am unteren Rand der obigen Abbildung sind die Zeichen vor der Konvertierung (Encoder-Eingabe), und "2012-03-27", das vertikal links angeordnet ist, wird generiert. Es ist ein Charakter. So lesen Sie die Heatmap. Wenn Sie jedoch die generierten Zeichen des Decoders einzeln betrachten, bedeutet dies, dass die Farbe des Felds links das Zeichen ist, das mit der größten Aufmerksamkeit generiert wurde. Ich denke, es wird. (Bitte darauf hinweisen, wenn es anders ist ...) (Wenn Sie alle Werte im Feld links hinzufügen, ist dies natürlich 1.)
Im obigen Beispiel sehen Sie Folgendes.
――Es ist ersichtlich, dass Sie beim Generieren von JJJJ als Ganzes auf den Jahresteil und beim Generieren von MM auf den Monatsteil achten. --Diese Aufgabe wird in JJJJ-MM-TT konvertiert, dh der Tag wird nicht konvertiert, daher achte ich nicht auf generierte Zeichen in "Dienstag".
Außerdem wird es so angezogen ↓
――Es scheint, dass es in Attention verschiedene Muster gibt, wie in Zero Work 2 beschrieben. ――Nächste werden wir uns mit (?) Selbstaufmerksamkeit befassen, die vielseitiger ist als Aufmerksamkeit!
Ende
Recommended Posts