[PYTHON] Les débutants génèrent automatiquement des documents avec le LSTM de Pytorch

Motivation

-Été des étudiants de 3e année de premier cycle-

Je suis étudiant en 3e année, donc je recherche un stagiaire ...

ES "** Quel est le livrable? **"

Je "Eh ..."

ES "** Qiita? Github? **"

Je "Non ..."

"** Perdu **"

Master I "Je viens de terminer mes recherches de fin d'études et je veux faire un livrable ..."

Que faire cette fois

J'aimerais mettre en œuvre quelque chose qui m'intéresse et dont j'ai quelques connaissances.

Ma spécialité est le traitement du langage naturel, et dans mon mémoire de premier cycle, j'avais l'habitude de générer des documents grâce à l'apprentissage en profondeur. Après avoir pratiqué avec Pytorch, nous générerons automatiquement des documents.

Le générateur de document doit avoir une couche d'intégration, une couche LSTM et une couche linéaire. Créez des hyper-paramètres tels que le nombre de couches dans LSTM qui peuvent être spécifiées à partir de la ligne de commande.

Ensembles de données utilisés pour la formation, etc.

L'ensemble de données utilisé pour la formation sera "SNOW T15: Easy Japanese Corpus" du Laboratoire de traitement du langage naturel de l'Université de technologie de Nagaoka.

Laboratoire de traitement du langage naturel de l'Université de technologie de Nagaoka http://www.jnlp.org/SNOW/T15

Traduction bilingue japonais-anglais de 50 000 phrases + corpus parallèle japonais facile est super pratique. Comme il est fourni au format xlsx, convertissez-le à l'avance au format csv. De plus, puisqu'il s'agit d'une génération de phrases automatique, nous n'utilisons pas de japonais et d'anglais faciles.

Analyse morphologique

Le traitement du langage naturel est une analyse morphologique. L'analyse morphologique divise une phrase brute donnée en éléments morphologiques. OOV (Out Of Vocab) est souvent un problème dans l'analyse morphologique. La dimension de sortie de l'apprentissage profond dépend de la taille du corpus qui peut être produit, et plus elle est grande, plus elle met la pression sur la mémoire. Par conséquent, de nombreuses personnes enregistrent les mots à basse fréquence dans le corpus comme UNK (inconnu) et conçoivent diverses autres choses.

Ici, le morceau de phrase est utilisé.

https://github.com/google/sentencepiece

Phrasepiece est un outil super pratique qui effectue une analyse morphologique afin qu'il tienne dans le nombre spécifié de mots sans OOV en apprenant sans professeur. Veuillez vous référer à l'URL citée pour les spécifications détaillées. Il est utilisé pour analyser morphologiquement un ensemble de données dans la plage de 8000 mots sans OOV.

Définition du modèle

Eh bien, c'est un LSTM normal, donc je n'ai rien à dire. Je serais ravi si vous pouviez signaler tout problème.

LSTM.py


import torch
import torch.nn as nn
import torch.nn.functional as F

class LSTM(nn.Module):
    def __init__(self, source_size, hidden_size, batch_size, embedding_size, num_layers):
        super(LSTM, self).__init__()
        self.hidden_size = hidden_size
        self.source_size = source_size
        self.batch_size = batch_size
        self.num_layers = num_layers
        self.embed_source = nn.Embedding(source_size, embedding_size, padding_idx=0)
        self.embed_source.weight.data.normal_(0, 1 / embedding_size**0.5)
        self.lstm_source = nn.LSTM(self.hidden_size, self.hidden_size, num_layers=self.num_layers,
                                bidirectional=True, batch_first=True)
        self.linear = nn.Linear(self.hidden_size*2, self.source_size)

    def forward(self, sentence_words, hx, cx):
        source_k = self.embed_source(sentence_words)
        self.lstm_source.flatten_parameters()
        encoder_output, (hx, cx) = self.lstm_source(source_k, (hx, cx))
        prob = F.log_softmax(self.linear(encoder_output), dim=1)
        _, output = torch.max(prob, dim = -1)
        return prob, output, (hx, cx)

    def init_hidden(self, bc):
        hx = torch.zeros(self.num_layers*2, bc, self.hidden_size)
        cx = torch.zeros(self.num_layers*2, bc, self.hidden_size)
        return hx, cx

C'est normal.

Formation et chargeur

Ensuite, créez le code d'entraînement et le chargeur de l'ensemble de données. À propos, index.model est un modèle d'analyse morphologique créé par morceau de phrase. J'ai envie de tester soudainement après l'entraînement sans vérification. La formation apprend à saisir une certaine phrase japonaise et à produire exactement la même phrase. Au moment du test, entrez uniquement le premier mot de la phrase de test et affichez le reste dans la méthode cupidité dans l'ordre chronologique. Peut-être que cela devrait générer automatiquement le document ...

train.py


import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import argparse
from loader import Dataset
import torch.optim as optim
import sentencepiece as spm
from utils import seq_to_string, to_np, trim_seqs
import matplotlib.pyplot as plt
from torchviz import make_dot

from model.LSTM import LSTM

def make_model(source_size, hidden_size, batch_size, embedding_size=256, num_layers=1):
    model = LSTM(source_size, hidden_size, batch_size, embedding_size, num_layers)
    criterion = nn.NLLLoss(reduction="sum")
    model_opt = optim.Adam(model.parameters(), lr=0.0001)
    return model, criterion, model_opt

def data_load(maxlen, source_size, batch_size):
    data_set = Dataset(maxlen=maxlen)
    data_num = len(data_set)
    train_ratio = int(data_num*0.8)
    test_ratio = int(data_num*0.2)
    res = int(data_num - (train_ratio + test_ratio))
    train_ratio += res
    ratio=[train_ratio, test_ratio]
    train_dataset, test_dataset = torch.utils.data.random_split(data_set, ratio)
    dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
    test_dataloader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
    del(train_dataset)
    del(data_set)
    return dataloader, test_dataloader

def run_epoch(data_iter, model, criterion, model_opt, epoch):
    model, criterion = model.cuda(), criterion.cuda()
    model.train()
    total_loss = 0
    for i, data in enumerate(data_iter):
        model_opt.zero_grad()
        src = data[:,:-1]
        trg = data[:,1:]
        src, trg = src.cuda(), trg.cuda()
        hx, cx = model.init_hidden(src.size(0))
        hx, cx = hx.cuda(), cx.cuda()
        output_log_probs, output_seqs, _ = model(src, hx, cx)
        flattened_log_probs = output_log_probs.view(src.size(0) * src.size(1), -1)
        loss = criterion(flattened_log_probs, trg.contiguous().view(-1))
        loss /= (src.size(0) * src.size(1))
        loss.backward()
        model_opt.step()
        total_loss += loss
        if i % 50 == 1:
            print("Step: %d Loss: %f " %
                    (i, loss))
    mean_loss = total_loss / len(data_iter)
    torch.save({
                'model': model.state_dict()
                }, "./model_log/model.pt")
    dot = make_dot(output_log_probs ,params=dict(model.named_parameters()))
    dot.format = 'png'
    dot.render('image')
    return model, mean_loss

def depict_graph(mean_losses, epochs):
    epoch = [i+1 for i in range(epochs)]
    plt.xlim(0, epochs)
    plt.ylim(1, mean_losses[0])
    plt.plot(epoch, mean_losses)
    plt.title("loss")
    plt.xlabel("epoch")
    plt.ylabel("loss")
    plt.show()
    

def test(model, data_loader):
    model.eval()
    all_output_seqs = []
    all_target_seqs = []
    with torch.no_grad():
        for data in data_loader:
            src = Variable(data[:,:-1])
            src = src.cuda()
            del(data)
            input_data = src[:,:2]
            hx, cx = model.init_hidden(input_data.size(0))
            for i in range(18):
                hx, cx = hx.cuda(), cx.cuda()
                output_log_probs, output_seqs, hidden = model(input_data, hx, cx)
                hx, cx = hidden[0], hidden[1]
                input_data = torch.cat((input_data, output_seqs[:,-1:]), 1)
            all_output_seqs.extend(trim_seqs(input_data))
    out_set = (all_target_seqs, all_output_seqs)

    return out_set

if __name__ == "__main__":
    sp = spm.SentencePieceProcessor()
    sp.load("./index.model")
    source_size = sp.GetPieceSize()
    parser = argparse.ArgumentParser(description='Parse training parameters')
    parser.add_argument('--do_train', type=str, default='False')
    parser.add_argument('--batch_size', type=int, default=256)
    parser.add_argument('--maxlen', type=int, default=20)    
    parser.add_argument('--epochs', type=int, default=50)
    parser.add_argument('--hidden_size', type=int, default=128)
    parser.add_argument('--embedding_size', type=int, default=128)
    parser.add_argument('--num_layers', type=int, default=1)
    args = parser.parse_args()             
    model, criterion, model_opt = make_model(source_size, args.hidden_size, args.batch_size, args.embedding_size, args.num_layers)
    data_iter, test_data_iter = data_load(args.maxlen, source_size, args.batch_size)

    mean_losses = []
    if args.do_train == "True":
        for epoch in range(args.epochs):
            print(epoch+1)
            model, mean_loss = run_epoch(data_iter, model, criterion, model_opt, epoch)
            mean_losses.append(mean_loss.item())
        depict_graph(mean_losses, args.epochs)
    else:
        model.load_state_dict(torch.load("./model_log/model.pt")["model"])
    out_set = test(model, data_iter)

    true_txt = out_set[0]
    out_txt = out_set[1]

    with open("true.txt", "w", encoding="utf-8") as f:
        for i in true_txt:
            for j in i:
                f.write(sp.IdToPiece(int(j)))
            f.write("\n")
    
    with open("out.txt", "w", encoding="utf-8") as f:
        for i in out_txt:
            for j in i:
                f.write(sp.IdToPiece(int(j)))
            f.write("\n")

loader.py


import torch
import numpy as np
import csv
import sentencepiece as spm

class Dataset(torch.utils.data.Dataset):
    def __init__(self, maxlen):
        self.sp = spm.SentencePieceProcessor()
        self.sp.load("./index.model")
        self.maxlen = maxlen
        

        with open('./data/parallel_data.csv', mode='r', newline='', encoding='utf-8') as f:
            csv_file = csv.reader(f)
            read_data = [row for row in csv_file]
        self.data_num = len(read_data) - 1
        jp_data = []
        for i in range(1, self.data_num):    
            jp_data.append(read_data[i][1:2]) #Phrases japonaises difficiles

        self.en_data_idx = np.zeros((len(jp_data), maxlen+1))

        for i,sentence in enumerate(jp_data):
            self.en_data_idx[i][0] = self.sp.PieceToId("<s>")
            for j,idx in enumerate(self.sp.EncodeAsIds(sentence[0])[:]):
                self.en_data_idx[i][j+1] = idx
                if j+1 == maxlen-1: #Symbole de fin à la fin
                    self.en_data_idx[i][j+1] = self.sp.PieceToId("</s>")
                    break
            if j+2 <= maxlen-1:
                self.en_data_idx[i][j+2] = self.sp.PieceToId("</s>")
                if j+3 < maxlen-1:
                    self.en_data_idx[i][j+3:] = self.sp.PieceToId("<unk>") #Parce que c'est gênant, le unk généré lors de l'apprentissage du morceau de phrase est utilisé comme un pad.
            else:
                self.en_data_idx[i][j+1] = self.sp.PieceToId("</s>")
                if j+2 < maxlen-1:
                    self.en_data_idx[i][j+2:] = self.sp.PieceToId("<unk>")
        
    def __len__(self):
        return self.data_num

    def __getitem__(self, idx):
        en_data = torch.tensor(self.en_data_idx[idx-1][:], dtype=torch.long)
        return en_data

résultat

Pour le moment, essayez de tourner environ 100 époques. Je ne savais pas si les couches ou les hyper paramètres devaient être plus petits pour éviter le surentraînement, alors Il est réglé de manière à ne pas être trop grand.

C'est la perte pendant l'apprentissage. Figure_1.png

Il tombe régulièrement, mais c'est subtil du milieu. Peut-être que je devrais faire quelque chose au sujet des hyper paramètres.

Ainsi, ce qui suit est un exemple du texte de sortie réel.

(^ Ω ^) ... Ce n'est pas bien ...

Conclusions et ainsi de suite

Tout d'abord, la définition du modèle peut être mauvaise. Seq2seq est-il plus approprié? J'essaierai si j'ai une chance. Quoi qu'il en soit, je ne me suis jamais entraîné avec le modèle LSTM nu de manière cohérente (bien qu'il y ait Embedding), C'était assez amusant de réfléchir à la tâche à accomplir. Dans ma thèse de maîtrise, j'utilise essentiellement des Transformers, donc à l'avenir, j'aimerais parfois présenter des implémentations et des articles.

Recommended Posts

Les débutants génèrent automatiquement des documents avec le LSTM de Pytorch
Essayez de générer automatiquement des documents Python avec Sphinx
Générer automatiquement un diagramme de relation de modèle avec Django
Générez automatiquement des spécifications d'objets avec Blue Prism
Apprenez Zundokokiyoshi en utilisant LSTM
LSTM multivarié avec Keras
[Evangelion] Essayez de générer automatiquement des lignes de type Asuka avec Deep Learning
Générez automatiquement une table de distribution de fréquence en un seul coup avec Python
Mesurez et comparez les températures avec Raspberry Pi et générez automatiquement des graphiques