系列语:本系列是nlp-tutorial代码注释系列,github上原项目地址为:nlp-tutorial,本系列每一篇文章的大纲是相关知识点介绍 + 详细代码注释。
前言:3-1节介绍了普通的RNN:nlp-tutorial代码注释3-1,RNN简介。单向的RNN有一个缺点,即只使用了当前元素之前的信息来做处理,而没有用到之后的信息。而在nlp的一些任务中,常常要兼顾上下文。比如这个命名实体识别的这个例子:
BRNN模型
BRNN可以看做是将两个RNN结合在一起,一个从前往后,一个从后往前,分别做方向相反的前向传播。正向和反向的RNN有各自的权重矩阵。两个RNN在相同的时间步有不同的激活值,最终每个时间步总的激活值是将两个激活值连接在一起:
BRNN计算某个时间步的输出的公式为:
即BRNN时间步
t
t
t的激活值是将正向激活值
a
→
\stackrel{\rightarrow}{a}
a→和反向激活值堆叠
a
←
\stackrel{\leftarrow}{a}
a←得到的总激活值,那么显然BRNN权重矩阵的长度也是RNN的两倍。
代码实现
本次BRNN的代码实现使用的RNN单元是LSTM,单向LSTM的代码实现见上一节:LSTM
本次代码中的模型做的是一个语言模型做的事,就是根据输入去预测下一个词,但我觉得这个是有点问题的,因为语言模型是一个单向的RNN即可解决的任务,不需要BRNN,仅当作BRNN的实现示例吧。
pytorch代码及详细注释如下:(源代码为github中nlp-tutorial项目,项目地址:nlp-tutorial)
首先import一些需要的库,并设置元素默认的type为float:
import numpy as np #引入numpy库
import torch #引入torch
import torch.nn as nn #torch.nn是torch的神经网络库
import torch.optim as optim #torch.optim是优化库,包含很多优化函数
from torch.autograd import Variable #现在的pytorch版本variable已经回归tensor了,直接用tensor即可
dtype = torch.FloatTensor
接下来根据数据集建立词典,这里的数据集是一个非常长的句子:
sentence = ( # 语料库、数据集
'Lorem ipsum dolor sit amet consectetur adipisicing elit '
'sed do eiusmod tempor incididunt ut labore et dolore magna '
'aliqua Ut enim ad minim veniam quis nostrud exercitation'
)
word_dict = {w: i for i, w in enumerate(list(set(sentence.split())))} # 这两行分别建立单词到序号的和序号到单词的索引
number_dict = {i: w for i, w in enumerate(list(set(sentence.split())))}
n_class = len(word_dict) # 词典大小
max_len = len(sentence.split()) # 句子的最大长度
n_hidden = 5 # 隐藏单元个数
下面处理数据集,获得输入和对应的标记。样本中所有的输入长度都一样,都是max_len,但每个输入的句子中有效的单词数不一样,第一个输入中的有效单词是数据集中的第一个单词,其他的都用无效单词填充,标签是第二个单词;第二个输入中的有效单词是数据集中的前两个单词,其他的都用无效单词填充,标签是第三个单词;以此类推,获得输入集和对应的标签集:
def make_batch(seq_data):
input_batch = [] # 创建输入和对应的标签的空列表
target_batch = []
words = sentence.split() # 将句子split,把单词分离出来形成列表
for i, word in enumerate(words[:-1]):
input = [word_dict[n] for n in words[:(i + 1)]]
input = input + [0] * (max_len - len(input)) # 输入长度保持相同,剩下的用无效单词填充
target = word_dict[words[i + 1]] # 标签是从数据集中取的输入的下一个单词
input_batch.append(np.eye(n_class)[input])
target_batch.append(target)
return Variable(torch.Tensor(input_batch)), Variable(torch.LongTensor(target_batch))
模型部分、训练优化部分和单向的LSTM几乎完全相同,不重复介绍,可参见我的上一篇博客:LSTM。这里只介绍两个不同之处,然后将代码贴出来。一处是在模型里面的init函数中,LSTM层的构建,相比于单向LSTM,双向的只需要将nn.LSTM中的参数bidirectional设置为true即可,权重矩阵W的初始化,由于W要和正反两个前向传播的激活值堆叠形成的大激活值相乘,所以双向LSTM中W的长度应该是单向LSTM的二倍:
self.lstm = nn.LSTM(input_size=n_class, hidden_size=n_hidden, bidirectional=True)
self.W = nn.Parameter(torch.randn([n_hidden * 2, n_class]).type(dtype))
另一处是在模型类里面的forward函数中,和权重矩阵W的变化的道理相同,这里初始化第0个时间步的 h ( 0 ) h^{(0)} h(0)和 c ( 0 ) c^{(0)} c(0)时,第一个维度也要乘2:
hidden_state = Variable(torch.zeros(1*2, len(X), n_hidden)) # [num_layers(=1) * num_directions(=2), batch_size, n_hidden]
cell_state = Variable(torch.zeros(1*2, len(X), n_hidden)) # [num_layers(=1) * num_directions(=2), batch_size, n_hidden]
其余代码完全相同,如下:
class BiLSTM(nn.Module):
def __init__(self):
super(BiLSTM, self).__init__()
self.lstm = nn.LSTM(input_size=n_class, hidden_size=n_hidden, bidirectional=True)
self.W = nn.Parameter(torch.randn([n_hidden * 2, n_class]).type(dtype))
self.b = nn.Parameter(torch.randn([n_class]).type(dtype))
def forward(self, X):
input = X.transpose(0, 1) # 将X的形状变换为:[n_step, batch_size, n_class]
hidden_state = Variable(torch.zeros(1*2, len(X), n_hidden)) # [num_layers(=1) * num_directions(=2), batch_size, n_hidden]
cell_state = Variable(torch.zeros(1*2, len(X), n_hidden)) # [num_layers(=1) * num_directions(=2), batch_size, n_hidden]
outputs, (_, _) = self.lstm(input, (hidden_state, cell_state))
outputs = outputs[-1] # 取最后一步的输出,形状为:[batch_size, num_directions(=1) * n_hidden]
model = torch.mm(outputs, self.W) + self.b # model : [batch_size, n_class]
return model
input_batch, target_batch = make_batch(sentence)
model = BiLSTM()
criterion = nn.CrossEntropyLoss() # 损失函数为交叉熵损失函数
optimizer = optim.Adam(model.parameters(), lr=0.001) # 使用Adam算法进行优化
# Training
for epoch in range(10000):
optimizer.zero_grad() #每次训练前清除梯度缓存
output = model(input_batch) #模型计算output
loss = criterion(output, target_batch) #计算loss
if (epoch + 1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
loss.backward() #反向传播、自动求导
optimizer.step() #优化、更新参数
predict = model(input_batch).data.max(1, keepdim=True)[1]
print(sentence)
print([number_dict[n.item()] for n in predict.squeeze()])