Modèle Seq2seq (séquence à séquence) avec PyTorch

Qu'est-ce que la PNL?

Le PNL ou traitement du langage naturel est l'une des branches populaires de l'intelligence artificielle qui aide les ordinateurs à comprendre, manipuler ou répondre à un humain dans son langage naturel. La PNL est le moteur derrière Google Translate cela nous aide à comprendre d’autres langues.

Qu’est-ce que Seq2Seq ?

Séq2Séq est une méthode de traduction automatique et de traitement du langage basée sur un codeur-décodeur qui mappe une entrée de séquence à une sortie de séquence avec une balise et une valeur d'attention. L'idée est d'utiliser 2 RNN qui fonctionneront ensemble avec un jeton spécial et tenteront de prédire la séquence d'état suivante à partir de la séquence précédente.

Comment prédire la séquence de la séquence précédente

Prédire la séquence à partir de la séquence précédente

Voici les étapes pour prédire la séquence de la séquence précédente avec PyTorch.

Étape 1) Chargement de nos données

Pour notre ensemble de données, vous utiliserez un ensemble de données de Paires de phrases bilingues délimitées par des tabulations. Ici, j'utiliserai l'ensemble de données anglais vers indonésien. Vous pouvez choisir ce que vous voulez, mais n'oubliez pas de modifier le nom du fichier et le répertoire dans le code.

from __future__ import unicode_literals, print_function, division
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

import numpy as np
import pandas as pd

import os
import re
import random

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

Étape 2) Préparation des données

Vous ne pouvez pas utiliser l'ensemble de données directement. Vous devez diviser les phrases en mots et les convertir en One-Hot Vector. Chaque mot sera indexé de manière unique dans la classe Lang pour créer un dictionnaire. La classe Lang stockera chaque phrase et la divisera mot par mot avec addSentence. Créez ensuite un dictionnaire en indexant chaque mot inconnu de la séquence vers les modèles de séquence.

SOS_token = 0
EOS_token = 1
MAX_LENGTH = 20

#initialize Lang Class
class Lang:
   def __init__(self):
       #initialize containers to hold the words and corresponding index
       self.word2index = {}
       self.word2count = {}
       self.index2word = {0: "SOS", 1: "EOS"}
       self.n_words = 2  # Count SOS and EOS

#split a sentence into words and add it to the container
   def addSentence(self, sentence):
       for word in sentence.split(' '):
           self.addWord(word)

#If the word is not in the container, the word will be added to it, 
#else, update the word counter
   def addWord(self, word):
       if word not in self.word2index:
           self.word2index[word] = self.n_words
           self.word2count[word] = 1
           self.index2word[self.n_words] = word
           self.n_words += 1
       else:
           self.word2count[word] += 1

La classe Lang est une classe qui nous aidera à créer un dictionnaire. Pour chaque langue, chaque phrase sera divisée en mots puis ajoutée au conteneur. Chaque conteneur stockera les mots dans l'index approprié, comptera le mot et ajoutera l'index du mot afin que nous puissions l'utiliser pour trouver l'index d'un mot ou trouver un mot à partir de son index.

Parce que nos données sont séparées par TAB, vous devez utiliser pandas comme notre chargeur de données. Les pandas liront nos données sous forme de dataFrame et les diviseront en phrases source et cible. Pour chaque phrase que vous avez,

  • vous le normaliserez en minuscules,
  • supprimer tous les non-caractères
  • convertir en ASCII à partir d'Unicode
  • divisez les phrases pour avoir chaque mot dedans.
#Normalize every sentence
def normalize_sentence(df, lang):
   sentence = df[lang].str.lower()
   sentence = sentence.str.replace('[^A-Za-z\s]+', '')
   sentence = sentence.str.normalize('NFD')
   sentence = sentence.str.encode('ascii', errors='ignore').str.decode('utf-8')
   return sentence

def read_sentence(df, lang1, lang2):
   sentence1 = normalize_sentence(df, lang1)
   sentence2 = normalize_sentence(df, lang2)
   return sentence1, sentence2

def read_file(loc, lang1, lang2):
   df = pd.read_csv(loc, delimiter='\t', header=None, names=[lang1, lang2])
   return df

def process_data(lang1,lang2):
   df = read_file('text/%s-%s.txt' % (lang1, lang2), lang1, lang2)
   print("Read %s sentence pairs" % len(df))
   sentence1, sentence2 = read_sentence(df, lang1, lang2)

   source = Lang()
   target = Lang()
   pairs = []
   for i in range(len(df)):
       if len(sentence1[i].split(' ')) < MAX_LENGTH and len(sentence2[i].split(' ')) < MAX_LENGTH:
           full = [sentence1[i], sentence2[i]]
           source.addSentence(sentence1[i])
           target.addSentence(sentence2[i])
           pairs.append(full)

   return source, target, pairs

Une autre fonction utile que vous utiliserez est la conversion des paires en Tensor. Ceci est très important car notre réseau ne lit que des données de type tensoriel. C'est également important car c'est dans cette partie qu'à chaque fin de phrase il y aura un jeton pour indiquer au réseau que la saisie est terminée. Pour chaque mot de la phrase, il obtiendra l'index du mot approprié dans le dictionnaire et ajoutera un jeton à la fin de la phrase.

def indexesFromSentence(lang, sentence):
   return [lang.word2index[word] for word in sentence.split(' ')]

def tensorFromSentence(lang, sentence):
   indexes = indexesFromSentence(lang, sentence)
   indexes.append(EOS_token)
   return torch.tensor(indexes, dtype=torch.long, device=device).view(-1, 1)

def tensorsFromPair(input_lang, output_lang, pair):
   input_tensor = tensorFromSentence(input_lang, pair[0])
   target_tensor = tensorFromSentence(output_lang, pair[1])
   return (input_tensor, target_tensor)

Modèle Seq2Seq

Modèle Seq2seq
Séq2Séq

Le modèle PyTorch Seq2seq est une sorte de modèle qui utilise le décodeur d'encodeur PyTorch au-dessus du modèle. L'encodeur encodera la phrase mot par mot dans un vocabulaire indexé ou des mots connus avec index, et le décodeur prédira la sortie de l'entrée codée en décodant l'entrée en séquence et essaiera d'utiliser la dernière entrée comme entrée suivante si c'est possible. Avec cette méthode, il est également possible de prédire la prochaine saisie pour créer une phrase. Chaque phrase se verra attribuer un jeton pour marquer la fin de la séquence. A la fin de la prédiction, il y aura également un jeton pour marquer la fin de la sortie. Ainsi, à partir de l’encodeur, il transmettra un état au décodeur pour prédire la sortie.

Modèle Seq2seq
Modèle Seq2Seq

L'encodeur encodera notre phrase d'entrée mot par mot dans l'ordre et à la fin il y aura un jeton pour marquer la fin d'une phrase. L'encodeur se compose d'une couche d'intégration et d'une couche GRU. La couche Embedding est une table de recherche qui stocke l'intégration de notre entrée dans un dictionnaire de mots de taille fixe. Il sera transmis à une couche GRU. La couche GRU est une unité récurrente fermée qui se compose de plusieurs types de couches de RNN qui calculera l'entrée séquencée. Cette couche calculera l'état caché par rapport à la précédente et mettra à jour les portes de réinitialisation, de mise à jour et de nouvelles.

Modèle Seq2seq

Séq2Séq

Le décodeur décodera l'entrée de la sortie de l'encodeur. Il essaiera de prédire la prochaine sortie et essaiera de l'utiliser comme prochaine entrée si cela est possible. Le décodeur se compose d’une couche d’intégration, d’une couche GRU et d’une couche linéaire. La couche d'intégration créera une table de recherche pour la sortie et la transmettra à une couche GRU pour calculer l'état de sortie prédit. Après cela, une couche linéaire aidera à calculer la fonction d'activation pour déterminer la vraie valeur de la sortie prédite.

class Encoder(nn.Module):
   def __init__(self, input_dim, hidden_dim, embbed_dim, num_layers):
       super(Encoder, self).__init__()
      
       #set the encoder input dimesion , embbed dimesion, hidden dimesion, and number of layers 
       self.input_dim = input_dim
       self.embbed_dim = embbed_dim
       self.hidden_dim = hidden_dim
       self.num_layers = num_layers

       #initialize the embedding layer with input and embbed dimention
       self.embedding = nn.Embedding(input_dim, self.embbed_dim)
       #intialize the GRU to take the input dimetion of embbed, and output dimention of hidden and
       #set the number of gru layers
       self.gru = nn.GRU(self.embbed_dim, self.hidden_dim, num_layers=self.num_layers)
              
   def forward(self, src):
      
       embedded = self.embedding(src).view(1,1,-1)
       outputs, hidden = self.gru(embedded)
       return outputs, hidden

class Decoder(nn.Module):
   def __init__(self, output_dim, hidden_dim, embbed_dim, num_layers):
       super(Decoder, self).__init__()

#set the encoder output dimension, embed dimension, hidden dimension, and number of layers 
       self.embbed_dim = embbed_dim
       self.hidden_dim = hidden_dim
       self.output_dim = output_dim
       self.num_layers = num_layers

# initialize every layer with the appropriate dimension. For the decoder layer, it will consist of an embedding, GRU, a Linear layer and a Log softmax activation function.
       self.embedding = nn.Embedding(output_dim, self.embbed_dim)
       self.gru = nn.GRU(self.embbed_dim, self.hidden_dim, num_layers=self.num_layers)
       self.out = nn.Linear(self.hidden_dim, output_dim)
       self.softmax = nn.LogSoftmax(dim=1)
      
   def forward(self, input, hidden):

# reshape the input to (1, batch_size)
       input = input.view(1, -1)
       embedded = F.relu(self.embedding(input))
       output, hidden = self.gru(embedded, hidden)       
       prediction = self.softmax(self.out(output[0]))
      
       return prediction, hidden

class Seq2Seq(nn.Module):
   def __init__(self, encoder, decoder, device, MAX_LENGTH=MAX_LENGTH):
       super().__init__()
      
#initialize the encoder and decoder
       self.encoder = encoder
       self.decoder = decoder
       self.device = device
     
   def forward(self, source, target, teacher_forcing_ratio=0.5):

       input_length = source.size(0) #get the input length (number of words in sentence)
       batch_size = target.shape[1] 
       target_length = target.shape[0]
       vocab_size = self.decoder.output_dim
      
#initialize a variable to hold the predicted outputs
       outputs = torch.zeros(target_length, batch_size, vocab_size).to(self.device)

#encode every word in a sentence
       for i in range(input_length):
           encoder_output, encoder_hidden = self.encoder(source[i])

#use the encoder’s hidden layer as the decoder hidden
       decoder_hidden = encoder_hidden.to(device)
  
#add a token before the first predicted word
       decoder_input = torch.tensor([SOS_token], device=device)  # SOS

#topk is used to get the top K value over a list
#predict the output word from the current target word. If we enable the teaching force,  then the #next decoder input is the next word, else, use the decoder output highest value. 

       for t in range(target_length):   
           decoder_output, decoder_hidden = self.decoder(decoder_input, decoder_hidden)
           outputs[t] = decoder_output
           teacher_force = random.random() < teacher_forcing_ratio
           topv, topi = decoder_output.topk(1)
           input = (target[t] if teacher_force else topi)
           if(teacher_force == False and input.item() == EOS_token):
               break

       return outputs

Étape 3) Formation du modèle

Le processus de formation dans les modèles Seq2seq commence par la conversion de chaque paire de phrases en Tensors à partir de leur index Lang. Notre modèle séquence à séquence utilisera SGD comme optimiseur et la fonction NLLLoss pour calculer les pertes. Le processus de formation commence par l'envoi d'une paire de phrases au modèle pour prédire le résultat correct. A chaque étape, la sortie du modèle sera calculée avec les mots vrais pour trouver les pertes et mettre à jour les paramètres. Ainsi, comme vous utiliserez 75000 75000 itérations, notre modèle séquence à séquence générera paires aléatoires à partir de notre ensemble de données.

teacher_forcing_ratio = 0.5

def clacModel(model, input_tensor, target_tensor, model_optimizer, criterion):
   model_optimizer.zero_grad()

   input_length = input_tensor.size(0)
   loss = 0
   epoch_loss = 0
   # print(input_tensor.shape)

   output = model(input_tensor, target_tensor)

   num_iter = output.size(0)
   print(num_iter)

#calculate the loss from a predicted sentence with the expected result
   for ot in range(num_iter):
       loss += criterion(output[ot], target_tensor[ot])

   loss.backward()
   model_optimizer.step()
   epoch_loss = loss.item() / num_iter

   return epoch_loss

def trainModel(model, source, target, pairs, num_iteration=20000):
   model.train()

   optimizer = optim.SGD(model.parameters(), lr=0.01)
   criterion = nn.NLLLoss()
   total_loss_iterations = 0

   training_pairs = [tensorsFromPair(source, target, random.choice(pairs))
                     for i in range(num_iteration)]
  
   for iter in range(1, num_iteration+1):
       training_pair = training_pairs[iter - 1]
       input_tensor = training_pair[0]
       target_tensor = training_pair[1]

       loss = clacModel(model, input_tensor, target_tensor, optimizer, criterion)

       total_loss_iterations += loss

       if iter % 5000 == 0:
           avarage_loss= total_loss_iterations / 5000
           total_loss_iterations = 0
           print('%d %.4f' % (iter, avarage_loss))
          
   torch.save(model.state_dict(), 'mytraining.pt')
   return model

Étape 4) Testez le modèle

Le processus d'évaluation de Seq2seq PyTorch consiste à vérifier la sortie du modèle. Chaque paire de modèles séquence à séquence sera introduite dans le modèle et générera les mots prédits. Après cela, vous rechercherez la valeur la plus élevée à chaque sortie pour trouver l'index correct. Et à la fin, vous comparerez pour voir notre prédiction de modèle avec la vraie phrase

def evaluate(model, input_lang, output_lang, sentences, max_length=MAX_LENGTH):
   with torch.no_grad():
       input_tensor = tensorFromSentence(input_lang, sentences[0])
       output_tensor = tensorFromSentence(output_lang, sentences[1])
  
       decoded_words = []
  
       output = model(input_tensor, output_tensor)
       # print(output_tensor)
  
       for ot in range(output.size(0)):
           topv, topi = output[ot].topk(1)
           # print(topi)

           if topi[0].item() == EOS_token:
               decoded_words.append('<EOS>')
               break
           else:
               decoded_words.append(output_lang.index2word[topi[0].item()])
   return decoded_words

def evaluateRandomly(model, source, target, pairs, n=10):
   for i in range(n):
       pair = random.choice(pairs)
       print(‘source {}’.format(pair[0]))
       print(‘target {}’.format(pair[1]))
       output_words = evaluate(model, source, target, pair)
       output_sentence = ' '.join(output_words)
       print(‘predicted {}’.format(output_sentence))

Maintenant, commençons notre formation avec Seq to Seq, avec un nombre d'itérations de 75000 1 et un nombre de couches RNN de 512 avec une taille cachée de .

lang1 = 'eng'
lang2 = 'ind'
source, target, pairs = process_data(lang1, lang2)

randomize = random.choice(pairs)
print('random sentence {}'.format(randomize))

#print number of words
input_size = source.n_words
output_size = target.n_words
print('Input : {} Output : {}'.format(input_size, output_size))

embed_size = 256
hidden_size = 512
num_layers = 1
num_iteration = 100000

#create encoder-decoder model
encoder = Encoder(input_size, hidden_size, embed_size, num_layers)
decoder = Decoder(output_size, hidden_size, embed_size, num_layers)

model = Seq2Seq(encoder, decoder, device).to(device)

#print model 
print(encoder)
print(decoder)

model = trainModel(model, source, target, pairs, num_iteration)
evaluateRandomly(model, source, target, pairs)

Comme vous pouvez le voir, notre phrase prédite ne correspond pas très bien, donc pour obtenir une plus grande précision, vous devez vous entraîner avec beaucoup plus de données et essayer d'ajouter plus d'itérations et de nombres de couches en utilisant Sequence pour séquencer l'apprentissage.

random sentence ['tom is finishing his work', 'tom sedang menyelesaikan pekerjaannya']
Input : 3551 Output : 4253
Encoder(
  (embedding): Embedding(3551, 256)
  (gru): GRU(256, 512)
)
Decoder(
  (embedding): Embedding(4253, 256)
  (gru): GRU(256, 512)
  (out): Linear(in_features=512, out_features=4253, bias=True)
  (softmax): LogSoftmax()
)
Seq2Seq(
  (encoder): Encoder(
    (embedding): Embedding(3551, 256)
    (gru): GRU(256, 512)
  )
  (decoder): Decoder(
    (embedding): Embedding(4253, 256)
    (gru): GRU(256, 512)
    (out): Linear(in_features=512, out_features=4253, bias=True)
    (softmax): LogSoftmax()
  )
)

5000 4.0906
10000 3.9129
15000 3.8171
20000 3.8369
25000 3.8199
30000 3.7957
35000 3.8037
40000 3.8098
45000 3.7530
50000 3.7119
55000 3.7263
60000 3.6933
65000 3.6840
70000 3.7058
75000 3.7044

> this is worth one million yen
= ini senilai satu juta yen
< tom sangat satu juta yen <EOS>

> she got good grades in english
= dia mendapatkan nilai bagus dalam bahasa inggris
< tom meminta nilai bagus dalam bahasa inggris <EOS>

> put in a little more sugar
= tambahkan sedikit gula
< tom tidak <EOS>

> are you a japanese student
= apakah kamu siswa dari jepang
< tom kamu memiliki yang jepang <EOS>

> i apologize for having to leave
= saya meminta maaf karena harus pergi
< tom tidak maaf karena harus pergi ke

> he isnt here is he
= dia tidak ada di sini kan
< tom tidak <EOS>

> speaking about trips have you ever been to kobe
= berbicara tentang wisata apa kau pernah ke kobe
< tom tidak <EOS>

> tom bought me roses
= tom membelikanku bunga mawar
< tom tidak bunga mawar <EOS>

> no one was more surprised than tom
= tidak ada seorangpun yang lebih terkejut dari tom
< tom ada orang yang lebih terkejut <EOS>

> i thought it was true
= aku kira itu benar adanya
< tom tidak <EOS>