nlp-tutorial代码注释3-3,双向RNN简介

系列语:本系列是nlp-tutorial代码注释系列,github上原项目地址为:nlp-tutorial,本系列每一篇文章的大纲是相关知识点介绍 + 详细代码注释。

前言:3-1节介绍了普通的RNN:nlp-tutorial代码注释3-1,RNN简介。单向的RNN有一个缺点,即只使用了当前元素之前的信息来做处理,而没有用到之后的信息。而在nlp的一些任务中,常常要兼顾上下文。比如这个命名实体识别的这个例子:

“Teddy Roosevelt was a great President.”
“Teddy bears are on sale!”
这两个句子当只给定第一个单词时,无法判断 Teddy 是否是人名,而根据整个句子则很容易判断,第一句是人名,第二句是泰迪熊。双向循环神经网络(BRNN)则可以做到兼顾整个句子。

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()])
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值