系列语:本系列是nlp-tutorial代码注释系列,github上原项目地址为:nlp-tutorial,本系列每一篇文章的大纲是相关知识点介绍 + 详细代码注释。
前言:上一节笔记是RNN相关,链接如下:nlp-tutorial代码注释3-1,RNN简介,普通的RNN有一个问题,就是梯度消失,本节介绍解决此问题的一个方法:使用LSTM单元。
梯度消失问题:
考虑第i个时间步的损失对第j个时间步的激活值的导数,通过链式法则可以得到下图的求导公式:
当i、j距离较远时,若Wh较小,整个式子会指数级的变小,这是梯度消失;若Wh较大,整个式子会指数级的变大,这是梯度爆炸。
由于梯度消失,较远处的计算产生的影响也会消失,最终更新梯度时就只会收到较近的计算的影响,而很难受到长期的影响,亦即RNN记性很差。
举例:当RNN处理下图这样的语言模型预测问题时,根据上下文,很显然空格处是tickets,而由于上一个tickets距离太远,RNN很难预测出这个词是tickets。
主要的问题是:RNN很难去保存很多个时间步之前的信息,即不具有记忆性。我们需要一个具有记忆的RNN!这就是LSTM的主要想法。
LSTM
在第t个时间步,有一个隐藏状态
h
(
t
)
h^{(t)}
h(t)和一个单元状态
c
(
t
)
c^{(t)}
c(t),他们都是长度为n的向量,单元状态c可以存储长期信息,LSTM可以对单元状态c进行删除、写入、读取信息的操作。
单元状态c的信息被删除、写入、读取分别由三个对应的门控制。在每个时间步,门的每个元素可以是1(打开)、0(关闭),也可以是介于两者之间的值。具体公式如下,在时间步t计算
h
(
t
)
h^{(t)}
h(t)和
c
(
t
)
c^{(t)}
c(t):
遗忘门
f
(
t
)
f^{(t)}
f(t):控制对上一个时间步的单元状态
c
(
t
−
1
)
c^{(t-1)}
c(t−1)是保持还是遗忘;
输入门
i
(
t
)
i^{(t)}
i(t):控制写入新单元状态的哪些内容;
输出门
o
(
t
)
o^{(t)}
o(t):控制单元内容的哪些部分输出到
h
(
t
)
h^{(t)}
h(t);
c
~
\tilde{c}
c~
(
t
)
^{(t)}
(t):新单元状态;
c
(
t
)
c^{(t)}
c(t):通过遗忘一些上个时间步的单元状态
c
(
t
−
1
)
c^{(t-1)}
c(t−1)并写入一部分
c
~
\tilde{c}
c~
(
t
)
^{(t)}
(t)而在本时间步产生的新单元状态;
h
(
t
)
h^{(t)}
h(t):从单元状态
c
(
t
)
c^{(t)}
c(t)中读取一部分作为本时间步的隐藏状态。
LSTM架构让RNN更容易保存很多个时间步之前的信息,例如如果遗忘门一直被设置为0,那么信息就能够得到永久的保存。LSTM并不能保证没有梯度消失,但他确实让模型更容易学期长期的依赖关系。
代码实现
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
接下来是建立字典,本次代码的目的是根据前三个字母预测单词的第四个字母,字典中是26个字母:
char_arr = [c for c in 'abcdefghijklmnopqrstuvwxyz']# 建立字母列表
word_dict = {n: i for i, n in enumerate(char_arr)} # 这两行分别建立字母到序号的和序号到字母的索引
number_dict = {i: w for i, w in enumerate(char_arr)}
n_class = len(word_dict) # 字典大小
seq_data = ['make', 'need', 'coal', 'word', 'love', 'hate', 'live', 'home', 'hash', 'star'] # 数据集
接着设置一些参数:步长为3,即根据3个字母预测下一个,n_hidden是隐藏层单元个数:
n_step = 3
n_hidden = 128
处理数据集,获得输入和对应的标记:
def make_batch(seq_data):
input_batch, target_batch = [], [] #空列表
for seq in seq_data:
input = [word_dict[n] for n in seq[:-1]] # 'm', 'a' , 'k' is input
target = word_dict[seq[-1]] # 'e' is target
input_batch.append(np.eye(n_class)[input])
target_batch.append(target)
return Variable(torch.Tensor(input_batch)), Variable(torch.LongTensor(target_batch))
接着定义模型:
class TextLSTM(nn.Module):
首先是_init_,先继承父类,再使用nn.LSTM搭建LSTM层,再初始化隐藏层的参数W和b:
def __init__(self):
super(TextLSTM, self).__init__()
self.lstm = nn.LSTM(input_size=n_class, hidden_size=n_hidden)
self.W = nn.Parameter(torch.randn([n_hidden, n_class]).type(dtype))
self.b = nn.Parameter(torch.randn([n_class]).type(dtype))
再是forward,这里首先要初始化第0个时间步的 h ( 0 ) h^{(0)} h(0)和 c ( 0 ) c^{(0)} c(0),这里output是所有时间步的输出,这里是RNN语言模型,只需要最后一步的输出即可:
def forward(self, X):
input = X.transpose(0, 1) # 将X的形状变换为:[n_step, batch_size, n_class]
#初始化第0个时间步的ht和ct
hidden_state = Variable(torch.zeros(1, len(X), n_hidden)) # [num_layers(=1) * num_directions(=1), batch_size, n_hidden]
cell_state = Variable(torch.zeros(1, len(X), n_hidden)) # [num_layers(=1) * num_directions(=1), 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
接着是训练前的准备工作,调用make_batch函数获得输入和输出,接着选择损失函数和优化方法:
input_batch, target_batch = make_batch(seq_data)
model = TextLSTM()
criterion = nn.CrossEntropyLoss() #损失函数为交叉熵损失函数
optimizer = optim.Adam(model.parameters(), lr=0.001) #使用Adam算法进行优化
接下来训练模型:
for epoch in range(1000):
optimizer.zero_grad() #每次训练前清除梯度缓存
output = model(input_batch) #模型计算output
loss = criterion(output, target_batch) #计算loss
if (epoch + 1) % 100 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
loss.backward() #反向传播、自动求导
optimizer.step() #优化、更新参数
最后对训练好的模型进行测试:
inputs = [sen[:3] for sen in seq_data]
predict = model(input_batch).data.max(1, keepdim=True)[1]
print(inputs, '->', [number_dict[n.item()] for n in predict.squeeze()])