作者|Fernando López
编译|VK
起源|Towards Data Science

“写作没有规定。有时它来得容易而且完满;有时就像在岩石上钻孔,而后用炸药把它炸开一样。”—欧内斯特·海明威

本博客的目标是解释如何通过实现基于LSTMs的弱小体系结构来构建文本生成的端到端模型。

博客分为以下几个局部:

  • 介绍
  • 文本预处理
  • 序列生成
  • 模型体系结构
  • 训练阶段
  • 文本生成

残缺代码请拜访:https://github.com/FernandoLp...

介绍

多年来,人们提出了各种各样的倡议来建模自然语言,但这是怎么回事呢?“建模自然语言”指的是什么?咱们能够认为“建模自然语言”是指对形成语言的语义和语法进行推理,实质上是这样,但它更进一步。

目前,自然语言解决(NLP)畛域通过不同的办法和技术解决不同的工作,即对语言进行推理、了解和建模。

自然语言解决(NLP)畛域在过来的十年里倒退十分迅速。许多模型都从不同的角度提出了解决不同NLP工作的办法。同样,最受欢迎的模型中的共同点是施行基于深度学习的模型。

如前所述,NLP畛域解决了大量的问题,特地是在本博客中,咱们将通过应用基于深度学习的模型来解决文本生成问题,例如循环神经网络LSTM和Bi-LSTM。同样,咱们将应用当今最简单的框架之一来开发深度学习模型,特地是咱们将应用PyTorch的LSTMCell类来开发。

问题陈说

给定一个文本,神经网络将通过字符序列来学习给定文本的语义和句法。随后,将随机抽取一系列字符,并预测下一个字符。

文本预处理

首先,咱们须要一个咱们要解决的文本。有不同的资源能够在纯文本中找到不同的文本,我倡议你看看Gutenberg我的项目(https://www.gutenberg.org/).。

在这个例子中,我将应用George Bird Grinnell的《Jack Among the Indians》这本书,你能够在这里找到:https://www.gutenberg.org/cac...。所以,第一章的第一行是:

The train rushed down the hill, with a long shrieking whistle, and then began to go more and more slowly. Thomas had brushed Jack off and thanked him for the coin that he put in his hand, and with the bag in one hand and the stool in the other now went out onto the platform and down the steps, Jack closely following.

如你所见,文本蕴含大写、小写、换行符、标点符号等。倡议你将文本调整为一种模式,使咱们可能以更好的形式解决它,这次要升高咱们将要开发的模型的复杂性。

咱们要把每个字符转换成它的小写模式。另外,倡议将文本作为一个字符列表来解决,也就是说,咱们将应用一个字符列表,而不是应用“字符串”。将文本作为字符序列的目标是为了更好地解决生成的序列,这些序列将提供给模型(咱们将在下一节中具体介绍)。

代码段1-预处理

def read_dataset(file):    letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m',               'n','o','p','q','r','s','t','u','v','w','x','y','z',' ']        # 关上原始文件    with open(file, 'r') as f:        raw_text = f.readlines()            # 将每一行转换为小写    raw_text = [line.lower() for line in raw_text]        # 创立一个蕴含整个文本的字符串    text_string = ''    for line in raw_text:        text_string += line.strip()             # 。创立一个字符数组    text = list()    for char in text_string:        text.append(char)             # 去掉所有的符号,只保留字母    text = [char for char in text if char in letters]        return text

如咱们所见,在第2行咱们定义了要应用的字符,所有其余符号都将被抛弃,咱们只保留“空白”符号。

在第6行和第10行中,咱们读取原始文件并将其转换为小写模式。

在第14行和第19行的循环中,咱们创立了一个代表整本书的字符串,并生成了一个字符列表。在第23行中,咱们通过只保留第2行定义的字母来过滤文本列表。

因而,一旦文本被加载和预处理,例如:

text = "The train rushed down the hill."

能够失去这样的字符列表:

text = ['t','h','e',' ','t','r','a','i','n',' ','r','u','s','h','e','d',' ','d','o','w','n',' ','t','h','e',' ','h','i','l','l']

咱们曾经有了全文作为字符列表。家喻户晓,咱们不能将原始字符间接引入神经网络,咱们须要一个数值示意,因而,咱们须要将每个字符转换成一个数值示意。为此,咱们将创立一个字典来帮忙咱们保留等价的“字符索引”和“索引字符”。

代码段2-字典创立

def create_dictionary(text):   char_to_idx = dict()  idx_to_char = dict()    idx = 0  for char in text:    if char not in char_to_idx.keys():            # 构建字典      char_to_idx[char] = idx      idx_to_char[idx] = char      idx += 1                    return char_to_idx, idx_to_char

咱们能够留神到,在第11行和第12行创立了“char-index”和index-char”字典。

到目前为止,咱们曾经演示了如何加载文本并以字符列表的模式保留它,咱们还创立了两个字典来帮忙咱们对每个字符进行编码和解码。

序列生成

序列生成的形式齐全取决于咱们要实现的模型类型。如前所述,咱们将应用LSTM类型的循环神经网络,它按程序接收数据(工夫步长)。

对于咱们的模型,咱们须要造成一个给定长度的序列,咱们称之为“窗口”,其中要预测的字符(指标)将是窗口旁边的字符。每个序列将由窗口中蕴含的字符组成。要造成一个序列,窗口一次向右失去一个字符。要预测的字符始终是窗口前面的字符。咱们能够在图中分明地看到这个过程。

在本例中,窗口的大小为4,这意味着它将蕴含4个字符。指标是作者在窗口图像左边的第一个字符

到目前为止,咱们曾经看到了如何以一种简略的形式生成字符序列。当初咱们须要将每个字符转换为其各自的数字格局,为此,咱们将应用预处理阶段生成的字典。这个过程能够在下图可视化。

很好,当初咱们晓得了如何应用一个一次滑动一个字符的窗口来生成字符序列,以及如何将字符转换为数字格局,上面的代码片段显示了所形容的过程。

代码段3-序列生成

def build_sequences(text, char_to_idx, window):    x = list()    y = list()        for i in range(len(text)):        try:            # 从文本中获取字符窗口            # 将其转换为其idx示意            sequence = text[i:i+window]            sequence = [char_to_idx[char] for char in sequence]                        #失去target            # 转换到它的idx示意            target = text[i+window]            target = char_to_idx[target]                        # 保留sequence和target            x.append(sequence)            y.append(target)                     except:            pass            x = np.array(x)    y = np.array(y)        return x, y

太棒了,当初咱们晓得如何预处理原始文本,如何将其转换为字符列表,以及如何以数字格局生成序列。当初咱们来看看最乏味的局部,模型架构。

模型架构

正如你曾经在这篇博客的题目中读到的,咱们将应用Bi-LSTM循环神经网络和规范LSTM。实质上,咱们应用这种类型的神经网络,因为它在解决程序数据时具备微小的后劲,例如文本类型的数据。同样,也有大量的文章提到应用基于循环神经网络的体系结构(例如RNN、LSTM、GRU、Bi-LSTM等)进行文本建模,特地是文本生成[1,2]。

所提出的神经网络构造由一个嵌入层、一个双LSTM层和一个LSTM层组成。紧接着,后一个LSTM连贯到一个线性层。

办法

该办法包含将每个字符序列传递到嵌入层,这将为形成序列的每个元素生成向量模式的示意,因而咱们将造成一个嵌入字符序列。随后,嵌入字符序列的每个元素将被传递到Bi-LSTM层。随后,将生成形成双LSTM(前向LSTM和后向LSTM)的LSTM的每个输入的串联。紧接着,每个前向+后向串联的向量将被传递到LSTM层,最初一个暗藏状态将从该层传递给线性层。最初一个线性层将有一个Softmax函数作为激活函数,以示意每个字符的概率。下图显示了所形容的办法。

到目前为止,咱们曾经解释了文本生成模型的体系结构以及实现的办法。当初咱们须要晓得如何应用PyTorch框架来实现所有这些,然而首先,我想简略地解释一下bilstm和LSTM是如何协同工作的,以便稍后理解如何在代码中实现这一点,那么让咱们看看bilstm网络是如何工作的。

Bi-LSTM和LSTM

规范LSTM和Bi-LSTM的要害区别在于Bi-LSTM由2个LSTM组成,通常称为“正向LSTM”和“反向LSTM”。基本上,正向LSTM以原始程序接管序列,而反向LSTM接管序列。随后,依据要执行的操作,两个LSTMs的每个工夫步的每个暗藏状态都能够连接起来,或者只对两个LSTMs的最初一个状态进行操作。在所提出的模型中,咱们倡议在每个工夫步退出两个暗藏状态。

很好,当初咱们理解了Bi-LSTM和LSTM之间的要害区别。回到咱们正在开发的示例中,下图示意每个字符序列在通过模型时的演变。

太好了,一旦Bi-LSTM和LSTM之间的交互都很分明,让咱们看看咱们是如何在代码中仅应用PyTorch框架中的LSTMcell来实现的。

那么,首先让咱们理解一下如何结构TextGenerator类的构造函数,让咱们看看上面的代码片段:

代码段4-文本生成器类的构造函数

class TextGenerator(nn.ModuleList):        def __init__(self, args, vocab_size):        super(TextGenerator, self).__init__()            self.batch_size = args.batch_size        self.hidden_dim = args.hidden_dim        self.input_size = vocab_size        self.num_classes = vocab_size    self.sequence_len = args.window            # Dropout        self.dropout = nn.Dropout(0.25)            # Embedding 层        self.embedding = nn.Embedding(self.input_size, self.hidden_dim, padding_idx=0)            # Bi-LSTM        # 正向和反向        self.lstm_cell_forward = nn.LSTMCell(self.hidden_dim, self.hidden_dim)        self.lstm_cell_backward = nn.LSTMCell(self.hidden_dim, self.hidden_dim)            # LSTM 层        self.lstm_cell = nn.LSTMCell(self.hidden_dim * 2, self.hidden_dim * 2)            # Linear 层        self.linear = nn.Linear(self.hidden_dim * 2, self.num_classes)

如咱们所见,从第6行到第10行,咱们定义了用于初始化神经网络每一层的参数。须要指出的是,input_size等于词汇表的大小(也就是说,咱们的字典在预处理过程中生成的元素的数量)。同样,要预测的类的数量也与词汇表的大小雷同,序列长度示意窗口的大小。

另一方面,在第20行和第21行中,咱们定义了组成Bi-LSTM的两个LSTMCells (向前和向后)。在第24行中,咱们定义了LSTMCell,它将与Bi-LSTM的输入一起馈送。值得一提的是,暗藏状态的大小是Bi-LSTM的两倍,这是因为Bi-LSTM的输入是串联的。稍后在第27行定义线性层,稍后将由softmax函数过滤。

一旦定义了构造函数,咱们须要为每个LSTM创立蕴含单元状态和暗藏状态的张量。因而,咱们按如下形式进行:

代码片段5-权重初始化

# Bi-LSTM# hs = [batch_size x hidden_size]# cs = [batch_size x hidden_size]hs_forward = torch.zeros(x.size(0), self.hidden_dim)cs_forward = torch.zeros(x.size(0), self.hidden_dim)hs_backward = torch.zeros(x.size(0), self.hidden_dim)cs_backward = torch.zeros(x.size(0), self.hidden_dim)# LSTM# hs = [batch_size x (hidden_size * 2)]# cs = [batch_size x (hidden_size * 2)]hs_lstm = torch.zeros(x.size(0), self.hidden_dim * 2)cs_lstm = torch.zeros(x.size(0), self.hidden_dim * 2)# 权重初始化torch.nn.init.kaiming_normal_(hs_forward)torch.nn.init.kaiming_normal_(cs_forward)torch.nn.init.kaiming_normal_(hs_backward)torch.nn.init.kaiming_normal_(cs_backward)torch.nn.init.kaiming_normal_(hs_lstm)torch.nn.init.kaiming_normal_(cs_lstm)

一旦定义了蕴含暗藏状态和单元状态的张量,是时候展现整个体系结构的组装是如何实现的.

首先,让咱们看一下上面的代码片段:

代码片段6-BiLSTM+LSTM+线性层

# 从 idx 到 embeddingout = self.embedding(x)# 为LSTM筹备shapeout = out.view(self.sequence_len, x.size(0), -1)forward = []backward = []# 解开Bi-LSTM# 正向for i in range(self.sequence_len):  hs_forward, cs_forward = self.lstm_cell_forward(out[i], (hs_forward, cs_forward))  hs_forward = self.dropout(hs_forward)  cs_forward = self.dropout(cs_forward)  forward.append(hs_forward)   # 反向for i in reversed(range(self.sequence_len)):  hs_backward, cs_backward = self.lstm_cell_backward(out[i], (hs_backward, cs_backward))  hs_backward = self.dropout(hs_backward)  cs_backward = self.dropout(cs_backward)  backward.append(hs_backward)   # LSTMfor fwd, bwd in zip(forward, backward):  input_tensor = torch.cat((fwd, bwd), 1)  hs_lstm, cs_lstm = self.lstm_cell(input_tensor, (hs_lstm, cs_lstm))# 最初一个暗藏状态通过线性层out = self.linear(hs_lstm)

为了更好地了解,咱们将用一些定义的值来解释程序,这样咱们就能够了解每个张量是如何从一个层传递到另一个层的。所以假如咱们有:

batch_size = 64hidden_size = 128sequence_len = 100num_classes = 27

所以x输出张量将有一个形态:

# torch.Size([batch_size, sequence_len])x : torch.Size([64, 100])

而后,在第2行中,x张量通过嵌入层传递,因而输入将具备一个大小:

# torch.Size([batch_size, sequence_len, hidden_size])x_embedded : torch.Size([64, 100, 128])

须要留神的是,在第5行中,咱们正在reshape x_embedded 张量。这是因为咱们须要将序列长度作为第一维,实质上是因为在Bi-LSTM中,咱们将迭代每个序列,因而重塑后的张量将具备一个形态:

# torch.Size([sequence_len, batch_size, hidden_size])x_embedded_reshaped : torch.Size([100, 64, 128])

紧接着,在第7行和第8行定义了forwardbackward 列表。在那里咱们将存储Bi-LSTM的暗藏状态。

所以是时候给Bi-LSTM输出数据了。首先,在第12行中,咱们在向前LSTM上迭代,咱们还保留每个工夫步的暗藏状态(hs_forward)。在第19行中,咱们迭代向后的LSTM,同时保留每个工夫步的暗藏状态(hs_backward)。你能够留神到循环是以雷同的程序执行的,不同之处在于它是以相同的模式读取的。每个暗藏状态将具备以下形态:

# hs_forward : torch.Size([batch_size, hidden_size])hs_forward : torch.Size([64, 128])# hs_backward : torch.Size([batch_size, hidden_size])hs_backward: torch.Size([64, 128])

很好,当初让咱们看看如何为最新的LSTM层提供数据。为此,咱们应用forwardbackward 列表。在第26行中,咱们遍历与第27行级联的forwardbackward 对应的每个暗藏状态。须要留神的是,通过连贯两个暗藏状态,张量的维数将减少2倍,即张量将具备以下形态:

# input_tesor : torch.Size([bathc_size, hidden_size * 2])input_tensor : torch.Size([64, 256])

最初,LSTM将返回大小为的暗藏状态:

# last_hidden_state: torch.Size([batch_size, num_classes])last_hidden_state: torch.Size([64, 27])

最初,LSTM的最初一个暗藏状态将通过一个线性层,如第31行所示。因而,残缺的forward函数显示在上面的代码片段中:

代码片段7-正向函数

def forward(self, x):        # Bi-LSTM    # hs = [batch_size x hidden_size]    # cs = [batch_size x hidden_size]    hs_forward = torch.zeros(x.size(0), self.hidden_dim)    cs_forward = torch.zeros(x.size(0), self.hidden_dim)    hs_backward = torch.zeros(x.size(0), self.hidden_dim)    cs_backward = torch.zeros(x.size(0), self.hidden_dim)        # LSTM    # hs = [batch_size x (hidden_size * 2)]    # cs = [batch_size x (hidden_size * 2)]    hs_lstm = torch.zeros(x.size(0), self.hidden_dim * 2)    cs_lstm = torch.zeros(x.size(0), self.hidden_dim * 2)        # 权重初始化    torch.nn.init.kaiming_normal_(hs_forward)    torch.nn.init.kaiming_normal_(cs_forward)    torch.nn.init.kaiming_normal_(hs_backward)    torch.nn.init.kaiming_normal_(cs_backward)    torch.nn.init.kaiming_normal_(hs_lstm)    torch.nn.init.kaiming_normal_(cs_lstm)        # 从 idx 到 embedding    out = self.embedding(x)        # 为LSTM筹备shape    out = out.view(self.sequence_len, x.size(0), -1)        forward = []    backward = []        # 解开Bi-LSTM    # 正向    for i in range(self.sequence_len):        hs_forward, cs_forward = self.lstm_cell_forward(out[i], (hs_forward, cs_forward))        hs_forward = self.dropout(hs_forward)        cs_forward = self.dropout(cs_forward)        forward.append(hs_forward)             # 反向    for i in reversed(range(self.sequence_len)):        hs_backward, cs_backward = self.lstm_cell_backward(out[i], (hs_backward, cs_backward))        hs_backward = self.dropout(hs_backward)        cs_backward = self.dropout(cs_backward)        backward.append(hs_backward)             # LSTM    for fwd, bwd in zip(forward, backward):        input_tensor = torch.cat((fwd, bwd), 1)        hs_lstm, cs_lstm = self.lstm_cell(input_tensor, (hs_lstm, cs_lstm))             # 最初一个暗藏状态通过线性层    out = self.linear(hs_lstm)        return out

到目前为止,咱们曾经晓得如何应用PyTorch中的LSTMCell来组装神经网络。当初是时候看看咱们如何进行训练阶段了,所以让咱们持续下一节。

训练阶段

太好了,咱们来训练了。为了执行训练,咱们须要初始化模型和优化器,稍后咱们须要为每个epoch 和每个mini-batch,所以让咱们开始吧!

代码片段8-训练阶段

def train(self, args):    # 模型初始化  model = TextGenerator(args, self.vocab_size)    # 优化器初始化  optimizer = optim.RMSprop(model.parameters(), lr=self.learning_rate)    # 定义batch数  num_batches = int(len(self.sequences) / self.batch_size)    # 训练模型  model.train()    # 训练阶段  for epoch in range(self.num_epochs):        # Mini batches    for i in range(num_batches):            # Batch 定义      try:        x_batch = self.sequences[i * self.batch_size : (i + 1) * self.batch_size]        y_batch = self.targets[i * self.batch_size : (i + 1) * self.batch_size]      except:        x_batch = self.sequences[i * self.batch_size :]        y_batch = self.targets[i * self.batch_size :]              # 转换 numpy array 为 torch tensors      x = torch.from_numpy(x_batch).type(torch.LongTensor)      y = torch.from_numpy(y_batch).type(torch.LongTensor)            # 输出数据      y_pred = model(x)            # loss计算      loss = F.cross_entropy(y_pred, y.squeeze())            # 革除梯度      optimizer.zero_grad()            # 反向流传      loss.backward()            # 更新参数      optimizer.step()            print("Epoch: %d ,  loss: %.5f " % (epoch, loss.item()))

一旦模型被训练,咱们将须要保留神经网络的权重,以便当前应用它们来生成文本。为此咱们有两种抉择,第一种是定义一个固定的时间段,而后保留权重,第二个是确定一个进行函数,以取得模型的最佳版本。在这个非凡状况下,咱们将抉择第一个选项。在对模型进行肯定次数的训练后,咱们将权重保留如下:

代码段9-权重保留

# 保留权重torch.save(model.state_dict(), 'weights/textGenerator_model.pt')

到目前为止,咱们曾经看到了如何训练文本生成器和如何保留权重,当初咱们将进入这个博客的最初一部分,文本生成!

文本生成

咱们曾经到了博客的最初一部分,文本生成。为此,咱们须要做两件事:第一件事是加载训练好的权重,第二件事是从序列汇合中随机抽取一个样本作为模式,开始生成下一个字符。上面咱们来看看上面的代码片段:

代码片段10-文本生成器

def generator(model, sequences, idx_to_char, n_chars):    # 评估模式  model.eval()    # 定义softmax函数  softmax = nn.Softmax(dim=1)    # 从序列汇合中随机选取索引  start = np.random.randint(0, len(sequences)-1)    # 给定随机的idx来定义模式  pattern = sequences[start]    # 利用字典,它输入了Pattern  print("\nPattern: \n")  print(''.join([idx_to_char[value] for value in pattern]), "\"")    # 在full_prediction中,咱们将保留残缺的预测  full_prediction = pattern.copy()    # 预测开始,它将被预测为一个给定的字符长度  for i in range(n_chars):        # 转换为tensor    pattern = torch.from_numpy(pattern).type(torch.LongTensor)    pattern = pattern.view(1,-1)        # 预测    prediction = model(pattern)    # 将softmax函数利用于预测张量    prediction = softmax(prediction)        # 预测张量被转换成一个numpy数组    prediction = prediction.squeeze().detach().numpy()    # 取概率最大的idx    arg_max = np.argmax(prediction)        # 将以后张量转换为numpy数组    pattern = pattern.squeeze().detach().numpy()    # 窗口向右1个字符    pattern = pattern[1:]    # 新pattern是由“旧”pattern+预测的字符组成的    pattern = np.append(pattern, arg_max)        # 保留残缺的预测    full_prediction = np.append(full_prediction, arg_max)      print("Prediction: \n")  print(''.join([idx_to_char[value] for value in full_prediction]), "\"")

因而,通过在以下特色下训练模型:

window : 100epochs : 50hidden_dim : 128batch_size : 128learning_rate : 0.001

咱们能够生成以下内容:

Seed:one of the prairie swellswhich gave a little wider view than most of them jack saw quite close to thePrediction:one of the prairie swellswhich gave a little wider view than most of them jack saw quite close to the wnd banngessejang boffff we outheaedd we band r hes tller a reacarof t t alethe ngothered uhe th wengaco ack fof ace ca  e s alee bin  cacotee tharss th band fofoutod we we ins sange trre anca y w farer we sewigalfetwher d e  we n s shed pack wngaingh tthe we the we javes t supun f the har man bllle s ng ou   y anghe ond we nd ba a  she t t anthendwe wn me anom ly tceaig t i isesw arawns t d ks wao thalac tharr jad  d anongive where the awe w we he is ma mie cack seat sesant sns t imes hethof riges we he d ooushe he hang out f t thu inong bll llveco we see s the he haa is s igg merin ishe d t san wack owhe o or th we sbe se we we inange t ts wan br seyomanthe harntho thengn  th me ny we ke in acor offff  of wan  s arghe we t angorro the wand be thing a sth t tha alelllll willllsse of s wed w brstougof bage orore he anthesww were ofawe ce qur the he sbaing tthe bytondece nd t llllifsffo acke o t in ir me hedlff scewant pi t bri pi owasem the awh thorathas th we hed ofainginictoplid we me

正如咱们看到的,生成的文本可能没有任何意义,然而有一些单词和短语仿佛造成了一个想法,例如:

we, band, pack, the, man, where, he, hang, out, be, thing, me, were

祝贺,咱们曾经到了博客的结尾!

论断

在本博客中,咱们展现了如何应用PyTorch的LSTMCell建设一个用于文本生成的端到端模型,并实现了基于循环神经网络LSTM和Bi-LSTM的体系结构。

值得注意的是,倡议的文本生成模型能够通过不同的形式进行改良。一些倡议的想法是减少要训练的文本语料库的大小,减少epoch以及每个LSTM的暗藏层大小。另一方面,咱们能够思考一个基于卷积LSTM的乏味的架构。

参考援用

[1] LSTM vs. GRU vs. Bidirectional RNN for script generation(https://arxiv.org/pdf/1908.04...

[2] The survey: Text generation models in deep learning(https://www.sciencedirect.com...

原文链接:https://towardsdatascience.co...

欢送关注磐创AI博客站:
http://panchuang.net/

sklearn机器学习中文官网文档:
http://sklearn123.com/

欢送关注磐创博客资源汇总站:
http://docs.panchuang.net/