[PYTHON] J'ai essayé d'implémenter la classification des phrases par Self Attention avec PyTorch

Cet article est l'article du 25ème jour du Calendrier de l'Avent Pytorch 2019!

introduction

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.

référence

Concernant la mise en œuvre, je me suis référé à l'article suivant Presque Marupakuri </ s>.

[attention personnelle] Implémentez un modèle de classification de document qui peut facilement visualiser la raison de la prédiction

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

Comment ça fonctionne

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.

  1. Convertissez une phrase de longueur $ n $ avec LSTM bidirectionnel (la dimension de la couche cachée est $ u $) (obtenez chaque $ h_i, (n \ fois 2u) $ dans (a) ci-dessous)
  2. Calculez l'attention avec le réseau neuronal en utilisant la valeur de chaque couche cachée du LSTM bidirectionnel en entrée ($ A = (A_ {ij}), 1 \ leq i \ leq r, 1 \ leq j \ leq dans (b) ci-dessous) Obtenez n $)
  3. Pondérer le vecteur de chaque couche cachée du LSTM bidirectionnel avec chaque Attention $ A_ {ij} $ pour obtenir l'incorporation de phrases dans Neural Network

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.

image.png

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.

la mise en oeuvre

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.

  • http://ai.stanford.edu/~amaas/data/sentiment/
  • L'exemple de mise en œuvre suivant est écrit en supposant qu'il fonctionnera sur Google Colab.

Importer la bibliothèque

--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

Préparation des données

--Téléchargez l'ensemble de données à partir de l'URL ci-dessus et préparez un fichier tsv au format suivant.

  • Le train et le test sont disponibles
  • L'étiquette est quantifiée avec 0 pour positif et 1 pour négatif.

Référence

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

image.png

Pré-traitement avec torchtext

  • Avec torchtext, vous pouvez prétraiter les données, obtenir rapidement des expressions distribuées de mots, des mini-lots, etc. ――Pour la représentation distribuée des mots, nous avons utilisé GloVe à 200 dimensions. Vous pouvez le télécharger avec torchtext, mais j'ai emprunté glove.6B.200d.txt à ici parce que je ne veux pas le télécharger à chaque fois. Attention car la taille est grande!
# 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)

Réglages des hyper paramètres, etc.

――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

Définition du modèle

Bidirectional LSTM

--Convertir des phrases avec LSTM bidirectionnel

  • Veuillez vous référer à ici pour les spécifications du LSTM bidirectionnel de PyTorch.
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

Couche d'auto-attention

--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)

Où classer en tenant compte de l'attention

  • Le poids d'attention pondère le vecteur de chaque couche cachée et Neural Network renvoie la prédiction pour la classification binaire. ――Je ne comprends honnêtement pas le traitement après pondération dans chaque couche Attenion, donc cette fois j'ai essayé les étapes suivantes pour le moment.
  1. Pondérer le vecteur de chaque couche cachée du LSTM bidirectionnel avec le poids de 3 couches Attention
  2. Ajoutez chaque vecteur pondéré pour obtenir m1, m2, m3
  3. Combinez les trois vecteurs m1, m2 et m3 tels quels (le nombre de dimensions devient 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

Modèle de déclaration

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)

Apprendre

«Pour le moment, j'ai essayé d'apprendre avec Epoch 10.

  • La perte diminue régulièrement, donc ça va pour le moment
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

Prédiction et précision

――Je ne pense pas que la précision soit meilleure que ce à quoi je m'attendais ...

  • La référence (1) dit que la précision était d'environ 90%, et il semble qu'il y ait divers retours de flamme dans certaines implémentations différentes ...
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

Visualisation de l'attention

  • Mettez en surbrillance et visualisez quels mots sont Attention. --Pour la fonction de mise en évidence, j'ai emprunté la source de Reference ① telle quelle.
  • Veuillez vous référer à ici lors de l'affichage du HTML sur le notebook jupyter. DoingJe fais un traitement étrange tel que la boucle for, mais je voulais juste en prendre un au hasard dans les données de test et le prédire. Désolé pour la mise en œuvre ridicule ...
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.

image.png

Supplément

Sans auto-attention ...

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

en conclusion

«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

J'ai essayé d'implémenter la classification des phrases par Self Attention avec PyTorch
J'ai essayé d'implémenter la classification des phrases et la visualisation de l'attention par le japonais BERT avec PyTorch
J'ai essayé d'implémenter CVAE avec PyTorch
J'ai essayé d'implémenter la lecture de Dataset avec PyTorch
J'ai essayé d'implémenter et d'apprendre DCGAN avec PyTorch
J'ai essayé d'implémenter SSD avec PyTorch maintenant (Dataset)
J'ai essayé de classer MNIST par GNN (avec PyTorch géométrique)
J'ai essayé d'implémenter SSD avec PyTorch maintenant (édition du modèle)
J'ai essayé d'implémenter Autoencoder avec TensorFlow
J'ai essayé de déplacer Faster R-CNN rapidement avec pytorch
J'ai essayé d'implémenter Mine Sweeper sur un terminal avec python
J'ai essayé d'implémenter le perceptron artificiel avec python
[Introduction à Pytorch] J'ai essayé de catégoriser Cifar10 avec VGG16 ♬
J'ai essayé d'implémenter Grad-CAM avec keras et tensorflow
J'ai essayé d'implémenter StarGAN (1)
J'ai essayé de comparer la précision de la classification des phrases BERT japonaises et japonaises Distil BERT avec PyTorch et introduction de la technique d'amélioration de la précision BERT
J'ai essayé d'implémenter une ligne moyenne mobile de volume avec Quantx
J'ai essayé d'implémenter la détection d'anomalies par apprentissage de structure clairsemée
J'ai essayé de mettre en œuvre une évasion (type d'évitement de tromperie) avec Quantx
[Django] J'ai essayé d'implémenter des restrictions d'accès par héritage de classe.
J'ai essayé d'implémenter ListNet d'apprentissage de rang avec Chainer
J'ai essayé de mettre en œuvre le chapeau de regroupement de Harry Potter avec CNN
J'ai essayé d'implémenter Deep VQE
J'ai essayé d'implémenter Attention Seq2Seq avec PyTorch
J'ai essayé de mettre en place une validation contradictoire
J'ai essayé d'expliquer l'ensemble de données de Pytorch
J'ai essayé d'implémenter DeepPose avec PyTorch
J'ai essayé d'implémenter Realness GAN
J'ai essayé la génération de phrases avec GPT-2
J'ai essayé d'implémenter PLSA en Python
J'ai essayé d'implémenter la permutation en Python
J'ai essayé de visualiser AutoEncoder avec TensorFlow
J'ai essayé de commencer avec Hy
J'ai essayé d'implémenter PLSA dans Python 2
[Introduction à Pytorch] J'ai joué avec sinGAN ♬
J'ai essayé d'implémenter DeepPose avec PyTorch PartⅡ
J'ai essayé d'implémenter PPO en Python
J'ai essayé de résoudre TSP avec QAOA
J'ai essayé de mettre en œuvre un apprentissage en profondeur qui n'est pas profond avec uniquement NumPy
J'ai essayé de communiquer avec un serveur distant par communication Socket avec Python.
J'ai essayé de mettre en œuvre une blockchain qui fonctionne réellement avec environ 170 lignes
765 J'ai essayé d'identifier les trois familles professionnelles par CNN (avec Chainer 2.0.0)
J'ai essayé d'implémenter la régression linéaire bayésienne par échantillonnage de Gibbs en python
J'ai essayé de prédire l'année prochaine avec l'IA
J'ai essayé de programmer la bulle de tri par langue
J'ai essayé d'utiliser lightGBM, xg boost avec Boruta
J'ai essayé d'apprendre le fonctionnement logique avec TF Learn
J'ai essayé de déplacer GAN (mnist) avec keras
J'ai essayé d'obtenir une image en grattant
J'ai essayé de sauvegarder les données avec discorde
J'ai essayé de détecter rapidement un mouvement avec OpenCV
J'ai essayé d'intégrer Keras dans TFv1.1
J'ai essayé de sortir LLVM IR avec Python
J'ai essayé d'implémenter TOPIC MODEL en Python
J'ai essayé de détecter un objet avec M2Det!
J'ai essayé d'automatiser la fabrication des sushis avec python
J'ai essayé de prédire la survie du Titanic avec PyCaret
J'ai essayé d'utiliser Linux avec Discord Bot