Cet article est l'article du 25ème jour du Calendrier de l'Avent Pytorch 2019!
Dans Dernière fois, nous avons implémenté Attention dans le modèle Encoder-Decoder, mais cette fois, nous implémenterons la classification des phrases dans Self Attention.
Les expressions intégrées de phrases dans Self Attention sont présentées dans les articles suivants, et sont également citées dans le célèbre article "Attention Is All You Need" de Transformer.
Cet article met en œuvre l'auto-attention introduite dans cet article.
Concernant la mise en œuvre, je me suis référé à l'article suivant Presque Marupakuri </ s>.
De plus, nous utiliserons torchtext, qui peut facilement effectuer un prétraitement, etc. pour la mise en œuvre, mais je me suis également référé à l'article suivant de la même personne pour torchtext.
② Traitement du langage naturel facile et profond avec torchtext
Le mécanisme de cet article est brièvement expliqué dans la référence (1), mais l'algorithme est grossièrement divisé en trois étapes.
Ici, $ d_a $ et $ r $ lors du calcul de Attention sont des hyper paramètres. $ d_a $ représente la taille de la matrice de poids lors de la prédiction de l'attention avec un réseau neuronal, et $ r $ est un paramètre correspondant au nombre de couches d'attention empilées.
L'idée est très simple, le but est de laisser le réseau neuronal apprendre quels mots doivent être mis en valeur (pondérés) lors de la classification des phrases.
Ensuite, nous implémenterons le mécanisme ci-dessus avec PyTorch. La tâche à résoudre est le jugement négatif / positif de la critique de film d'IMDb. Les données peuvent être téléchargées à partir de ce qui suit.
--Importer diverses bibliothèques utilisées dans l'implémentation ―― Puisque le jeu de données est en anglais, je pense que le moteur d'analyse morphologique est correct, mais pour le moment, j'ai préparé une fonction qui effectue un prétraitement avec nltk (bien qu'il y ait une partie qui souffre du prétraitement du texte torche, je m'en fiche une fois). Veuillez vous référer à ici pour nltk.
# torchtext
import torchtext
from torchtext import data
from torchtext import datasets
from torchtext.vocab import GloVe
from torchtext.vocab import Vectors
# pytorch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torch
#Autres
import os
import pickle
import numpy as np
import pandas as pd
from itertools import chain
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
#Enfin, il est utilisé pour visualiser l'attention.
import itertools
import random
from IPython.display import display, HTML
#Pour le prétraitement par nltk
import re
import nltk
from nltk import stem
nltk.download('punkt')
#Moteur morphologique préparé par nltk
def nltk_analyzer(text):
stemmer = stem.LancasterStemmer()
text = re.sub(re.compile(r'[!-\/:-@[-`{-~]'), ' ', text)
text = stemmer.stem(text)
text = text.replace('\n', '') #Supprimer les sauts de ligne
text = text.replace('\t', '') #Supprimer l'onglet
morph = nltk.word_tokenize(text)
return morph
--Téléchargez l'ensemble de données à partir de l'URL ci-dessus et préparez un fichier tsv au format suivant.
Par exemple, la préparation des données était la suivante.
train_pos_dir = 'aclImdb/train/pos/'
train_neg_dir = 'aclImdb/train/neg/'
test_pos_dir = 'aclImdb/test/pos/'
test_neg_dir = 'aclImdb/test/neg/'
header = ['text', 'label', 'label_id']
train_pos_files = os.listdir(train_pos_dir)
train_neg_files = os.listdir(train_neg_dir)
test_pos_files = os.listdir(test_pos_dir)
test_neg_files = os.listdir(test_neg_dir)
def make_row(root_dir, files, label, idx):
row = []
for file in files:
tmp = []
with open(root_dir + file, 'r') as f:
text = f.read()
tmp.append(text)
tmp.append(label)
tmp.append(idx)
row.append(tmp)
return row
row = make_row(train_pos_dir, train_pos_files, 'pos', 0)
row += make_row(train_neg_dir, train_neg_files, 'neg', 1)
train_df = pd.DataFrame(row, columns=header)
row = make_row(test_pos_dir, test_pos_files, 'pos', 0)
row += make_row(test_neg_dir, test_neg_files, 'neg', 1)
test_df = pd.DataFrame(row, columns=header)
Préparez les données comme ci-dessus et créez enfin le dataframe suivant (tout en supprimant la colonne d'étiquette car elle n'est pas nécessaire une fois).
train_df = pd.read_csv(imdb_dir + 'train.tsv', delimiter="\t", header=None)
train_df
# train.tsv, test.Mettez tsv ici
imdb_dir = "drive/My Drive/Colab Notebooks/imdb_datasets/"
# glove.6B.200d.Mettez txt ici
word_embedding_dir = "drive/My Drive/Colab Notebooks/word_embedding_models/"
TEXT = data.Field(sequential=True, tokenize=nltk_analyzer, lower=True, include_lengths=True, batch_first=True)
LABEL = data.Field(sequential=False, use_vocab=False, is_target=True)
train, test = data.TabularDataset.splits(
path=imdb_dir, train='train.tsv', test='test.tsv', format='tsv',
fields=[('Text', TEXT), ('Label', LABEL)])
glove_vectors = Vectors(name=word_embedding_dir + "glove.6B.200d.txt")
TEXT.build_vocab(train, test, vectors=glove_vectors, min_freq=1)
――Il n'y a pas de raison particulière, mais j'ai utilisé les paramètres suivants. ――Couche d'attention La figure ci-dessous montre 3 couches.
#Je veux utiliser le GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
BATCH_SIZE = 100 #Taille du lot
EMBEDDING_DIM = 200 #Nombre de dimensions intégrées des mots
LSTM_DIM = 128 #Nombre de dimensions de la couche cachée de LSTM
VOCAB_SIZE =TEXT.vocab.vectors.size()[0] #Nombre total de mots
TAG_SIZE = 2 #Cette fois, nous ferons un jugement négatif / positif, donc la taille finale du réseau est de 2.
DA = 64 #Taille de la matrice de poids lors du calcul de l'attention avec le réseau neuronal
R = 3 #Afficher l'attention en 3 couches
Bidirectional LSTM
--Convertir des phrases avec LSTM bidirectionnel
class BiLSTMEncoder(nn.Module):
def __init__(self, embedding_dim, lstm_dim, vocab_size):
super(BiLSTMEncoder, self).__init__()
self.lstm_dim = lstm_dim
self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)
#Définir le vecteur de mot appris comme intégration
self.word_embeddings.weight.data.copy_(TEXT.vocab.vectors)
#Nécessite d'éviter que le vecteur de mot ne soit mis à jour par rétro-propagation d'erreur_Définir grad sur False
self.word_embeddings.requires_grad_ = False
# bidirectional=Véritable et facile à faire du LSTM bidirectionnel
self.bilstm = nn.LSTM(embedding_dim, lstm_dim, batch_first=True, bidirectional=True)
def forward(self, text):
embeds = self.word_embeddings(text)
#Recevez la première valeur de retour car nous voulons le vecteur de chaque couche cachée
out, _ = self.bilstm(embeds)
#Renvoie le vecteur de chaque calque masqué dans les directions avant et arrière lors de sa connexion
return out
--Recevoir le vecteur de chaque couche cachée de LSTM bidirectionnel et calculer l'attention avec le réseau neuronal
--Selon l'article, Tanh ()
est utilisé pour la fonction d'activation, mais l'article introduit dans Reference ① utilise ReLU ()
, donc l'un ou l'autre semble aller bien.
class SelfAttention(nn.Module):
def __init__(self, lstm_dim, da, r):
super(SelfAttention, self).__init__()
self.lstm_dim = lstm_dim
self.da = da
self.r = r
self.main = nn.Sequential(
#Puisqu'il est bidirectionnel, la dimension du vecteur de chaque couche cachée est doublée en taille.
nn.Linear(lstm_dim * 2, da),
nn.Tanh(),
nn.Linear(da, r)
)
def forward(self, out):
return F.softmax(self.main(out), dim=1)
lstm_dim * 2 * 3
)
class SelfAttentionClassifier(nn.Module):
def __init__(self, lstm_dim, da, r, tagset_size):
super(SelfAttentionClassifier, self).__init__()
self.lstm_dim = lstm_dim
self.r = r
self.attn = SelfAttention(lstm_dim, da, r)
self.main = nn.Linear(lstm_dim * 6, tagset_size)
def forward(self, out):
attention_weight = self.attn(out)
m1 = (out * attention_weight[:,:,0].unsqueeze(2)).sum(dim=1)
m2 = (out * attention_weight[:,:,1].unsqueeze(2)).sum(dim=1)
m3 = (out * attention_weight[:,:,2].unsqueeze(2)).sum(dim=1)
feats = torch.cat([m1, m2, m3], dim=1)
return F.log_softmax(self.main(feats)), attention_weight
encoder = BiLSTMEncoder(EMBEDDING_DIM, LSTM_DIM, VOCAB_SIZE).to(device)
classifier = SelfAttentionClassifier(LSTM_DIM, DA, R, TAG_SIZE).to(device)
loss_function = nn.NLLLoss()
#Vous pouvez combiner plusieurs modèles dans un optimiseur en les intégrant à partir de la chaîne d'importation itertools.
optimizer = optim.Adam(chain(encoder.parameters(), classifier.parameters()), lr=0.001)
train_iter, test_iter = data.Iterator.splits((train, test), batch_sizes=(BATCH_SIZE, BATCH_SIZE), device=device, repeat=False, sort=False)
«Pour le moment, j'ai essayé d'apprendre avec Epoch 10.
losses = []
for epoch in range(10):
all_loss = 0
for idx, batch in enumerate(train_iter):
batch_loss = 0
encoder.zero_grad()
classifier.zero_grad()
text_tensor = batch.Text[0]
label_tensor = batch.Label
out = encoder(text_tensor)
score, attn = classifier(out)
batch_loss = loss_function(score, label_tensor)
batch_loss.backward()
optimizer.step()
all_loss += batch_loss.item()
print("epoch", epoch, "\t" , "loss", all_loss)
#epoch 0 loss 97.37978366017342
#epoch 1 loss 50.07680431008339
#epoch 2 loss 27.79373042844236
#epoch 3 loss 9.353876578621566
#epoch 4 loss 1.9509600398596376
#epoch 5 loss 0.22650832029466983
#epoch 6 loss 0.021685686125238135
#epoch 7 loss 0.011305359620109812
#epoch 8 loss 0.007448446772286843
#epoch 9 loss 0.005398457038154447
――Je ne pense pas que la précision soit meilleure que ce à quoi je m'attendais ...
answer = []
prediction = []
with torch.no_grad():
for batch in test_iter:
text_tensor = batch.Text[0]
label_tensor = batch.Label
out = encoder(text_tensor)
score, _ = classifier(out)
_, pred = torch.max(score, 1)
prediction += list(pred.cpu().numpy())
answer += list(label_tensor.cpu().numpy())
print(classification_report(prediction, answer, target_names=['positive', 'negative']))
# precision recall f1-score support
#
# positive 0.86 0.88 0.87 12103
# negative 0.89 0.86 0.87 12897
#
# accuracy 0.87 25000
# macro avg 0.87 0.87 0.87 25000
#weighted avg 0.87 0.87 0.87 25000
def highlight(word, attn):
html_color = '#%02X%02X%02X' % (255, int(255*(1 - attn)), int(255*(1 - attn)))
return '<span style="background-color: {}">{}</span>'.format(html_color, word)
def mk_html(sentence, attns):
html = ""
for word, attn in zip(sentence, attns):
html += ' ' + highlight(
TEXT.vocab.itos[word],
attn
)
return html
id2ans = {'0': 'positive', '1':'negative'}
_, test_iter = data.Iterator.splits((train, test), batch_sizes=(1, 1), device=device, repeat=False, sort=False)
n = random.randrange(len(test_iter))
for batch in itertools.islice(test_iter, n-1,n):
x = batch.Text[0]
y = batch.Label
encoder_outputs = encoder(x)
output, attn = classifier(encoder_outputs)
pred = output.data.max(1, keepdim=True)[1]
display(HTML('[Bonne réponse]' + id2ans[str(y.item())] + '\t [Prédiction]' + id2ans[str(pred.item())] + '<br><br>'))
for i in range(attn.size()[2]):
display(HTML(mk_html(x.data[0], attn.data[0,:,i]) + '<br><br>'))
Je suis désolé que ce soit minuscule, mais quand vous le visualisez, cela ressemble à ceci. Trois des mêmes phrases sont affichées, mais comme il y a trois couches Attention, chaque couche montre quel mot est attention. Le degré d'attention des mots est légèrement différent dans chaque couche d'attention, mais il semble qu'ils attirent presque de la même manière.
Au fait, lorsque j'ai résolu ce jugement négatif / positif uniquement avec le LSTM bidirectionnel sans auto-attention, la précision était d'environ 79,4%. --Lors de la résolution avec uniquement LSTM bidirectionnel, utilisez le réseau suivant et laissez les autres paramètres tels quels. ――L'attention de soi semble contribuer grandement à augmenter le niveau de précision.
class BiLSTMEncoder(nn.Module):
def __init__(self, embedding_dim, lstm_dim, vocab_size, tagset_size):
super(BiLSTMEncoder, self).__init__()
self.lstm_dim = lstm_dim
self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)
self.word_embeddings.weight.data.copy_(TEXT.vocab.vectors)
self.word_embeddings.requires_grad_ = False
self.bilstm = nn.LSTM(embedding_dim, lstm_dim, batch_first=True, bidirectional=True)
self.hidden2tag = nn.Linear(lstm_dim * 2, tagset_size)
self.softmax = nn.LogSoftmax()
def forward(self, text):
embeds = self.word_embeddings(text)
_, bilstm_hc = self.bilstm(embeds)
bilstm_out = torch.cat([bilstm_hc[0][0], bilstm_hc[0][1]], dim=1)
tag_space = self.hidden2tag(bilstm_out)
tag_scores = self.softmax(tag_space.squeeze())
return tag_scores
«Je suis un peu inquiet du modèle qui calcule l'attention lexicographiquement tel qu'implémenté dans Transformer etc. (celui qui divise l'incorporation des mots en requête, clé, valeur) et le réseau neuronal de cet article pour prédire l'attention. Je ne comprends pas vraiment la différence des modèles à faire. Avant de connaître cet article, je pensais que si j'allais à Attention, je ne pourrais pas obtenir le produit intérieur, alors je me demande s'il existe différentes méthodes de calcul pour Attention. ―― Ensuite, je veux écrire quelque chose sur Transformer!
fin
Recommended Posts