使用PyTorch构建自然语言生成(NLG)系统

徐大白
1个月前 阅读 42 点赞 2

总览

  • 自然语言生成(NLG)和相关事物的简介-
  • 资料准备
  • 训练神经语言模型
  • 使用PyTorch构建自然语言生成系统

介绍

在过去的几年中,由于深度学习算法的进步和足够的计算能力,自然语言处理(NLP)有了显着增长。但是,前馈神经网络不被认为是对语言或文本建模的最佳选择。这是因为前馈网络未考虑文本中的单词顺序。

因此,为了捕获文本中存在的顺序信息,在NLP中使用了递归神经网络。在本文中,我们将看到如何使用PyTorch生成自然语言,使用递归神经网络(LSTM)。

目录

  1. 自然语言生成(NLG)概述
  2. 使用神经语言建模生成文本
  3. 了解神经语言模型的功能
  4. –数据准备
  5. –模型训练
  6. –文本生成
  7. 使用PyTorch生成自然语言


自然语言生成的简要概述

自然语言生成(NLG)是自然语言处理(NLP)的子字段,它与计算机自动生成人类可读文本有关。NLG可用于各种NLP任务,例如机器翻译语音转文本聊天机器人,文本自动更正或文本自动完成。

我们可以借助语言建模为NLG建模。让我解释一下语言模型的概念–语言模型学会预测单词序列的概率。例如,考虑以下句子:

我们可以看到,第一句话“the cat is small ”比第二句话“small the is cat ”更有可能,因为我们知道第二句话中的单词顺序是不正确的。这是语言建模背后的基本概念。语言模型应该能够区分较高可能性的单词(或标记)序列。

语言模型的类型

以下是两种类型的语言模型:

  1. 统计语言模型:  这些模型使用传统统计技术(例如N-gram,隐马尔可夫模型(HMM)和某些语言规则)来学习单词的概率分布。
  2. 神经语言模型: 这些模型的有效性已经超过统计语言模型。他们使用不同种类的神经网络对语言进行建模。

在本文中,我们将重点介绍基于RNN / LSTM的神经语言模型。

使用神经语言建模生成文本

使用统计语言模型生成文本

首先,让我们看看如何在统计模型(如N-Gram模型)的帮助下生成文本。要了解N-Gram语言模型的工作原理,请查看以下文章的前半部分:

假设我们必须为下面的句子生成下一个单词:

假设我们的N-Gram模型考虑了3个前一个词的上下文,只是预测了下一个词。因此,模型将尝试使概率P(w |“她建立了a”)最大化,其中“ w”代表文本数据集中的每个单词。将使该概率最大化的单词将作为句子“她在那里建一个……”的下一个单词生成。

然而,使用这样的统计模型存在某些缺点,该统计模型使用直接的前一个单词作为上下文来预测下一个单词。让我给您一些额外的背景信息。

现在,我们获得了有关正在发生的事情的更多信息。“sandcastle”一词很可能是下一个词,因为它对“海滩”一词有很强的依赖性,因为人们在大多数正确的海滩上建造沙堡。因此,问题在于,“sandcastle””与"beach"的依赖程度与其对当前环境(“she built”)的依赖程度不一样。

使用神经语言模型生成文本

为了捕获序列标记之间的这种无限制的依赖性,我们可以使用基于RNN / LSTM的语言模型。以下是我们将用于NLG的语言模型的简约表示:

  • x1,x2和x3分别是 timestep 1,timestep2timestep3 的输入单词嵌入
  • ŷ1,ŷ2和ŷ3是训练数据集中所有不同标记的概率分布
  • y1,y2和y3是基本真值
  • U,V和W是权重矩阵
  • H0,H1,H2和H3是隐藏状态

我们将在下一部分中介绍该神经语言模型的工作。

了解神经语言模型的功能

我们将分三个阶段尝试理解神经语言模型的功能:

  • 资料准备
  • 模型训练
  • 文字产生

1.数据准备

假设我们将下面的句子用作训练数据。

[ ‘alright that is perfect’,

  ‘that sounds great’,

  ‘what is the price difference’]

第一句话有4个记号,第二句话有3个记号,第三个句子有5个记号。因此,所有这些句子在记号方面都有不同的长度。LSTM模型只接受长度相同的序列作为输入。因此,我们必须使训练数据中的序列具有相同的长度。

有多种技术可以使序列长度相等。

一种技术是填充。我们可以在需要时使用填充令牌填充序列。但是,如果使用此技术,则在损失计算和文本生成期间将不得不处理填充令牌。

因此,我们将使用另一种技术,该技术涉及在不使用任何填充令牌的情况下将一个序列分成多个等长的序列。该技术还增加了训练数据的大小。让我将其应用于我们的训练数据。

假设我们希望序列具有三个标记。然后,第一个序列将分为以下序列:

[ ‘alright that is’,

‘that is perfect’ ]

第二个序列的长度仅为3,因此不会被拆分。但是,训练数据的第三个序列具有五个令牌,它将被分解为多个令牌序列:

[ ‘what is the’,

‘is the price’,

‘the price difference’ ]

现在,新的数据集将如下所示:

[ ‘alright that is’,

‘that is perfect’,

‘that sounds great’,

‘what is the’,

‘is the price’,

‘the price difference’ ]

2.模型训练

由于我们要解决下一个单词生成问题,因此目标应该是输入单词的下一个单词。例如,考虑第一个文本序列“alright that is”。

如您所见,对于我们训练数据的第一个序列,模型的输入为 “alright” “that” ,相应的目标标记为 “that” “is”。因此,在开始训练过程之前,我们将必须将数据集中的所有序列拆分为输入(Input)和目标(Target),如下所示:

因此,“Input” “Target” 下的这些序列对是将传递给模型的训练示例,训练示例的损失将是每个时间步的损失平均值。

让我们看看如何将此模型用于文本生成。

3.文字生成

一旦我们的语言模型得到训练,我们便可以将其用于NLG。这个想法是将文本字符串作为输入以及模型要在输入文本字符串之后生成的多个标记传递。例如,如果用户将 “ what is” 作为输入文本,并指定该模型应生成2个标记,则该模型可能会生成 “ what is going on” 或 “ what is your name” 或任何其他序列。

让我借助一些插图向您展示它是如何发生的:

input text = “what is”

n = 2

步骤1 –输入文本的第一个标记(“ what”)传递给训练后的LSTM模型。它产生一个输出ŷ1,我们将忽略它,因为我们已经知道第二个标记(“ is”)。该模型还生成隐藏状态H1,该状态将传递到下一个时间步。

步骤2 –然后,第二个标记(“ is”)在第2步与H1一起传递给模型。该时间步长的输出是概率分布,其中标记“ going”具有最大值。因此,我们将其视为模型首次生成或预测的标记。现在我们还有一个标记可以生成。

步骤3 –为了生成下一个标记,我们需要在时间步骤3将输入标记传递给模型。但是,我们用完了输入标记,“ is”是生成“ going”的最后一个标记。那么,接下来我们要传递什么作为输入呢?在这种情况下,我们将传递先前生成的标记作为输入。

该模型的最终输出将是“what is going on"。这就是我们将用于执行NLG的文本生成策略。接下来,我们将在电影情节摘要的数据集中训练自己的语言模型。

使用PyTorch生成自然语言

现在我们知道了神经语言模型是如何工作的以及需要什么样的数据预处理,让我们训练一个LSTM语言模型以使用PyTorch执行自然语言生成。我已经在Google Colab上实现了整个代码,所以我建议您也使用它。

让我们快速导入必要的库。

import re
import pickle
import random
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.functional as F

1.加载数据集

我们将使用CMU电影摘要语料库的样本。您可以从此链接下载样本数据的pickle文件。

您可以使用下面的代码输出五份摘要,这些摘要是随机抽样的。

# sample random summaries
random.sample(movie_plots, 5)


2.数据准备

首先,我们将稍微清除文本。我们将仅保留字母和撇号标点符号,并从文本中删除其余的其他元素。

# clean text
movie_plots = [re.sub("[^a-z' ]", "", i) for i in movie_plots]

不必执行此步骤。只是我希望我的模型仅关注字母,而不必担心标点符号或数字或其他符号。

接下来,我们将定义一个函数以从数据集中准备定长序列。我已指定序列的长度为五。它是一个超参数,您可以根据需要更改它。

# create sequences of length 5 tokens
def create_seq(text, seq_len = 5):
    sequences = []
    # if the number of tokens in 'text' is greater than 5
    if len(text.split()) > seq_len:
      for i in range(seq_len, len(text.split())):
        # select sequence of tokens
        seq = text.split()[i-seq_len:i+1]
        # add to the list
        sequences.append(" ".join(seq))
      return sequences
    # if the number of tokens in 'text' is less than or equal to 5
    else:
      return [text]

因此,我们会将电影情节摘要传递给此函数,并且它将为每个输入返回固定长度序列的列表。

seqs = [create_seq(i) for i in movie_plots]
# merge list-of-lists into a single list
seqs = sum(seqs, [])
# count of sequences
len(seqs)

一旦准备好相同长度的序列,就可以将它们进一步分为输入序列和目标序列。

现在我们必须将这些序列(x和y)转换为整数序列,但是在此之前,我们将必须将数据集中的每个不同的词映射为一个整数值。因此,我们将为标记字典创建一个标记,并为标记字典创建一个整数。

# create integer-to-token mapping
int2token = {}
cnt = 0
for w in set(" ".join(movie_plots).split()):
  int2token[cnt] = w
  cnt+= 1
# create token-to-integer mapping
token2int = {t: i for i, t in int2token.items()}
token2int["the"], int2token[14271]

输出: (14271, ‘the’)

# set vocabulary size
vocab_size = len(int2token)
vocab_size

输出: 16592

词汇量为16,592,即我们的数据集中有超过16,000个不同的标记。

一旦我们有了标记到整数的映射,就可以将文本序列转换为整数序列。

def get_integer_seq(seq):
  return [token2int[w] for w in seq.split()]
# convert text sequences to integer sequences
x_int = [get_integer_seq(i) for i in x]
y_int = [get_integer_seq(i) for i in y]
# convert lists to numpy arrays
x_int = np.array(x_int)
y_int = np.array(y_int)

3.模型制作

我们将批次的输入序列和目标序列传递给模型,因为最好是按批次进行训练,而不是一次将整个数据传递给模型。以下功能将从输入数据创建批次。

现在,我们将定义语言模型的体系结构。

class WordLSTM(nn.Module):
	    def __init__(self, n_hidden=256, n_layers=4, drop_prob=0.3, lr=0.001):
	        super().__init__()
	        self.drop_prob = drop_prob
	        self.n_layers = n_layers
	        self.n_hidden = n_hidden
	        self.lr = lr
	        self.emb_layer = nn.Embedding(vocab_size, 200)
	        ## define the LSTM
	        self.lstm = nn.LSTM(200, n_hidden, n_layers, 
	                            dropout=drop_prob, batch_first=True)
	        ## define a dropout layer
	        self.dropout = nn.Dropout(drop_prob)
	        ## define the fully-connected layer
	        self.fc = nn.Linear(n_hidden, vocab_size)      
	    def forward(self, x, hidden):
	        ''' Forward pass through the network. 
	            These inputs are x, and the hidden/cell state `hidden`. '''
	        ## pass input through embedding layer
	        embedded = self.emb_layer(x)     
	        ## Get the outputs and the new hidden state from the lstm
	        lstm_output, hidden = self.lstm(embedded, hidden)
	        ## pass through a dropout layer
	        out = self.dropout(lstm_output)
	        #out = out.contiguous().view(-1, self.n_hidden) 
	        out = out.reshape(-1, self.n_hidden) 
	        ## put "out" through the fully-connected layer
	        out = self.fc(out)
	        # return the final output and the hidden state
	        return out, hidden
	    def init_hidden(self, batch_size):
	        ''' initializes hidden state '''
	        # Create two new tensors with sizes n_layers x batch_size x n_hidden,
	        # initialized to zero, for hidden state and cell state of LSTM
	        weight = next(self.parameters()).data
	        # if GPU is available
	        if (torch.cuda.is_available()):
	          hidden = (weight.new(self.n_layers, batch_size,self.n_hidden).zero_().cuda(),
           weight.new(self.n_layers, batch_size, self.n_hidden).zero_().cuda())
	        # if GPU is not available
	        else:
	          hidden = (weight.new(self.n_layers, batch_size, self.n_hidden).zero_(),

	                    weight.new(self.n_layers, batch_size, self.n_hidden).zero_())
	        return hidden

输入序列将首先通过嵌入层,然后通过LSTM层。LSTM层将提供一组与序列长度相等的输出,并且这些输出中的每一个都将传递到线性(密集)层,在该层上将应用softmax。

# instantiate the model
	net = WordLSTM()
	# push the model to GPU (avoid it if you are not using the GPU)
	net.cuda()
	print(net)

现在让我们定义一个用于训练模型的函数。

class WordLSTM(nn.Module):
	    def __init__(self, n_hidden=256, n_layers=4, drop_prob=0.3, lr=0.001):
	        super().__init__()
	        self.drop_prob = drop_prob
	        self.n_layers = n_layers
	        self.n_hidden = n_hidden
	        self.lr = lr
	        self.emb_layer = nn.Embedding(vocab_size, 200)
	        ## define the LSTM
	        self.lstm = nn.LSTM(200, n_hidden, n_layers, 
	                            dropout=drop_prob, batch_first=True)
	        ## define a dropout layer
	        self.dropout = nn.Dropout(drop_prob)
	        ## define the fully-connected layer
	        self.fc = nn.Linear(n_hidden, vocab_size)      
	    def forward(self, x, hidden):
	        ''' Forward pass through the network. 
	            These inputs are x, and the hidden/cell state `hidden`. '''
	        ## pass input through embedding layer
	      def train(net, epochs=10, batch_size=32, lr=0.001, clip=1, print_every=32):
	    # optimizer
	    opt = torch.optim.Adam(net.parameters(), lr=lr)
	    # loss
	    criterion = nn.CrossEntropyLoss()  
	    # push model to GPU
	    net.cuda()
	    counter = 0
	    net.train()
	    for e in range(epochs):
	        # initialize hidden state
	        h = net.init_hidden(batch_size)
	        for x, y in get_batches(x_int, y_int, batch_size):
	            counter+= 1
	            # convert numpy arrays to PyTorch arrays
	            inputs, targets = torch.from_numpy(x), torch.from_numpy(y)
	            # push tensors to GPU
	            inputs, targets = inputs.cuda(), targets.cuda()
	            # detach hidden states
	            h = tuple([each.data for each in h])
	            # zero accumulated gradients
	            net.zero_grad()
	            # get the output from the model
	            output, h = net(inputs, h)
	            # calculate the loss and perform backprop
	            loss = criterion(output, targets.view(-1))
	            # back-propagate error
	            loss.backward()
	            # `clip_grad_norm` helps prevent the exploding gradient problem in RNNs / LSTMs.
	            nn.utils.clip_grad_norm_(net.parameters(), clip)
	            # update weigths
	            opt.step()            
	            if counter % print_every == 0:
	              print("Epoch: {}/{}...".format(e+1, epochs),
	                    "Step: {}...".format(counter))


# train the model
train(net, batch_size = 32, epochs=20, print_every=256)

我已将批次大小指定为32,并将训练模型20个时间段。训练可能需要一段时间。

4.文字生成

训练模型后,我们可以将其用于文本生成。请注意,此模型可以一次生成一个单词,并带有隐藏状态。因此,要生成下一个单词,我们将必须使用此生成的单词和隐藏状态。

# predict next token
def predict(net, tkn, h=None):        
  # tensor inputs
  x = np.array([[token2int[tkn]]])
  inputs = torch.from_numpy(x)
  # push to GPU
  inputs = inputs.cuda()
  # detach hidden state from history
  h = tuple([each.data for each in h])
  # get the output of the model
  out, h = net(inputs, h)
  # get the token probabilities
  p = F.softmax(out, dim=1).data
  p = p.cpu()
  p = p.numpy()
  p = p.reshape(p.shape[1],)
  # get indices of top 3 values
  top_n_idx = p.argsort()[-3:][::-1]
  # randomly select one of the three indices
  sampled_token_index = top_n_idx[random.sample([0,1,2],1)[0]]
  # return the encoded value of the predicted char and the hidden state
  return int2token[sampled_token_index], h
# function to generate text
def sample(net, size, prime='it is'):     
    # push to GPU
    net.cuda() 
    net.eval()
    # batch size is 1
    h = net.init_hidden(1)
    toks = prime.split()
    # predict next token
    for t in prime.split():
      token, h = predict(net, t, h)
    toks.append(token)
    # predict subsequent tokens
    for i in range(size-1):
        token, h = predict(net, toks[-1], h)
        toks.append(token)
    return ' '.join(toks)

函数 sample()从用户处输入一个输入文本字符串(“ prime”),并指定一个数字(“ size”),该数字指定要生成的标记数量。在给定输入词和隐藏状态的情况下,sample()使用predict()函数预测下一个词。下面给出了由模型生成的一些文本序列。

sample(net, 15)

输出:

``现在由他们拥有新福音并被关押的圣殿负责''

sample(net, 15, prime = "as soon as")

输出:

``其中一个团队正在等待他的拒绝,并把他扔进对sannokai的拒绝中''

sample(net, 15, prime = "they")

输出:

“一旦他被派去做不是他嘴上造成的普通战士”

样本(净值15,15,prime =“ they”)

输出:

“他们发现自己以扔进船的方式被扔进了船上”

 

尾注

自然语言生成是一个快速成熟的领域,并且是越来越活跃的研究领域。从N-Gram模型到RNN / LSTM模型,用于NLG的方法也已走了很长一段路,现在基于变压器的模型已成为该领域中最新的模型。

总而言之,在本教程中,我们涵盖了许多与NLG相关的内容,例如数据集准备,神经语言模型的训练方式以及PyTorch中的自然语言生成过程。我建议您尝试在更大的数据集上构建语言模型,并查看它生成什么样的文本。


原文:Build a Natural Language Generation (NLG) System using PyTorch

作者:PRATEEK JOSHIAUGUST 3, 2020

翻译:徐大白


| 2
登录后可评论,马上登录吧~
评论 ( 0 )

还没有人评论...

相关推荐