作者 |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 到 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)
为了更好地了解,咱们将用一些定义的值来解释程序,这样咱们就能够了解每个张量是如何从一个层传递到另一个层的。所以假如咱们有:
batch_size = 64
hidden_size = 128
sequence_len = 100
num_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 行定义了forward 和backward 列表。在那里咱们将存储 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 层提供数据。为此,咱们应用forward 和backward 列表。在第 26 行中,咱们遍历与第 27 行级联的forward 和backward 对应的每个暗藏状态。须要留神的是,通过连贯两个暗藏状态,张量的维数将减少 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 : 100
epochs : 50
hidden_dim : 128
batch_size : 128
learning_rate : 0.001
咱们能够生成以下内容:
Seed:
one of the prairie swellswhich gave a little wider view than most of them jack saw quite close to the
Prediction:
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/