LSTM,长短期记忆(Long short-term memory是一种特殊的RNN。需要上下文的依赖信息,但不希望这些依赖信息过长(过大 计算图会越来越复杂),所以叫长的短时记忆。相比普通的RNN,LSTM在长的序列中有更好的表现。
在学习LSTM之前我们需要了解什么是RNN循环神经网络?
RNN
循环神经网络和传统的卷积神经网络不同,CNN只能单独的取处理一个个的输入,前一个输入和后一个输入是完全没有关系的。但是,有时候我们需要能处理连续的序列数据。即前面的输入和后面的输入是有关系的。比如常见的NLP自然语言处理(上下文是有语义联系的)、视频处理(需要把每一帧连接起来)等等。
RNN结构如下:
一个简单的循环神经网络由一个输入层、一个隐藏层和一个输出层组成:
将序列展开来 就是下面的结构:
O
t
=
g
(
V
∗
S
t
)
O_t=g(V * S_t)
Ot=g(V∗St) 输出层是一个全连接层(输出的每个节点都和隐藏层的每个节点相连)
S
t
=
f
(
U
∗
X
t
+
W
∗
S
t
−
1
)
S_t=f(U * X_t + W * S_{t-1})
St=f(U∗Xt+W∗St−1) 隐藏层是一个循环层(隐藏层S会一直循环传播更新下去)
V是隐藏层到输出层的权重矩阵。
U是输入层到隐藏层的权重矩阵
o表示输出层的值;
g和f都是激活函数;
隐藏层的值S不仅取决于当前输入x,还取决于上一时刻隐藏层的值 S t − 1 S_{t-1} St−1。W是上一时刻隐藏层的值作为当前输入的权重矩阵。
循环神经网络的输出值O是受当前输入 x t x_t xt和隐藏层的值 S t S_t St 影响;而隐藏层又是受前面前面每个时刻输入值 x 1 x_1 x1, x 2 x_2 x2 … x t − 1 x_{t-1} xt−1影响的,所以网络输出结果受多个连续的输入值影响。
但是对于长序列,这样循环下去就会造成RNN在结构上很深,会产生梯度消失和梯度爆炸问题。LSTM是升级后的RNN,在梯度消失问题上表现好。
LSTM
LSTM主要是为了解决长序列训练过程中的梯度消失和梯度爆炸问题。
注意!!!这里的梯度消失/梯度爆炸和前面我们间的深层 CNN网络堆叠导致的梯度消失/梯度爆炸的含义不一样。
CNN 中不同的层,有不同的参数w,各是各的梯度;而 RNN 中同样的权重在各个时刻共享,最终的梯度 G= 各个时间段的梯度
g
t
g_t
gt 的和。RNN 中总的梯度是不会消失的。只是随着循环的不断进行,梯度越传越弱,远距离的梯度消失,但近距离的梯度不会消失,所以梯度之和不会消失。
RNN 所谓梯度消失的真正含义是,梯度被近距离梯度主导,导致模型难以学到远距离的依赖关系。(可以看作随着时间序列的推进,模型会逐渐遗忘之前学到的依赖)所以不适合处理长序列。
所以在LSTM中再增加一个单元状态,即c我叫它长时记忆单元,让它来保存长期的状态,那么结构就变成:
在某时刻t算出来的梯度信息存储在c里后,然后让它把梯度一路带下去而无任何损耗,这样就能解决远距离依赖的问题了。
但是往长时记忆单元C添加每一个时刻的信息,这样效率频率肯定很低,因此我们让模型只记忆该记的信息。所以增加控制开关来选择对新信息选择记还是不记,以此来控制长时记忆单元c.
三个控制开关
LSTM通过设计门限结构解决长期依赖问题。三个开关分别用三个门实现。门实际上就是一层全连接层,它的输入是一个向量,门上使用
s
i
g
m
o
d
sigmod
sigmod激活函数输出是一个0-1之间的实数向量。这样就可以定义开关的打开程度,从而有了去除或添加信息的能力,让模型只记忆该记的信息。
g
(
x
)
=
σ
(
W
x
+
b
)
g(x)=\sigma(Wx+b)
g(x)=σ(Wx+b)
W是门的权重向量,b是偏置项,
σ
\sigma
σ是sigmoid函数
门的使用:用门的输出向量按元素乘
∘
\circ
∘以要控制的那个向量。当门输出为0时,任何向量与之相乘都会得到0向量,不能通过;输出为1时,可以通过。sigmoid函数的值域是(0,1),所以门的状态都是半开半闭的。
三个门的输入都是当前时刻的输入 x t x_t xt和上一时刻的输出 h t − 1 h_{t-1} ht−1。输出都是0-1的向量。
输入门(input gate)/更新门:
i
t
=
σ
(
W
i
⋅
[
h
t
−
1
,
x
t
]
+
b
i
)
i_t = \sigma(W_i \cdot [h_{t-1},x_t] + b_i)
it=σ(Wi⋅[ht−1,xt]+bi)
它决定了当前时刻网络的输入有多少保存到单元状态,即实际要输入到神经元状态的信息
遗忘门(forget gate):
f
t
=
σ
(
W
i
⋅
[
h
t
−
1
,
x
t
]
+
b
f
)
f_t = \sigma(W_i\cdot [h_{t-1},x_t] + b_f)
ft=σ(Wi⋅[ht−1,xt]+bf)
决定了上一时刻的单元状态有多少保留到当前时刻,也就是哪些信息被舍去(遗忘)
由于遗忘门的控制,它可以记忆很久之前的信息,由于输入门的控制,又可以避免保存太多无关紧要的内容进入记忆,而LSTM最终的输出,是由输出门和单元状态共同决定。
输出门(output gate): 控制单元状态c有多少输出到LSTM的当前输出值。
o
t
=
σ
(
W
o
⋅
[
h
t
−
1
,
x
t
]
+
b
o
)
o_t = \sigma(W_o\cdot [h_{t-1},x_t] + b_o)
ot=σ(Wo⋅[ht−1,xt]+bo)
h
t
h_t
ht=
o
t
o_t
ot
∗
t
a
n
h
(
c
t
)
* tanh(c_t)
∗tanh(ct)
网络隐含层状态
c
t
c_t
ct再通过一个
t
a
n
h
tanh
tanh层,范围为(-1,1),对记忆单元中的信息产生候选输出,然后与输出们
o
t
o_t
ot相乘。
最终输出
下面用LSTM模型简单实现mnist手写数据集分类
import torch
from torch import nn
import torchvision.datasets as dsets
import torchvision.transforms as transforms
import torch.optim
import warnings
# GPU or CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 利用过滤器来忽略warnings
warnings.filterwarnings('ignore')
# 超参
EPOCH = 1
BATCH_SIZE = 64
TIME_STEP = 28 # image height
INPUT_SIZE = 28 # 输入的大小 mnist数据集是28*28 piex
LR = 0.01
DOWNLOAD_MNIST = False # set to True if haven't download the data
# Mnist 数据集
train_data = dsets.MNIST(
root='./mnist/',
train=True,
transform=transforms.ToTensor(),
download=DOWNLOAD_MNIST,
)
# 批处理
train_loader = torch.utils.data.DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
test_data = dsets.MNIST(root='./mnist/', train=False, transform=transforms.ToTensor())
test_x = test_data.test_data.type(torch.FloatTensor)[:2000] / 255. # shape (2000, 28, 28) value in range(0,1)
test_y = test_data.test_labels.numpy()[:2000] # 转化成 numpy array
class Lstm(nn.Module):
# 初始化 LSTM model in torch.nn模块中的lstm的参数
def __init__(self, input_size=28, hidden_size=64, output_size=10, num_layers=1):
super().__init__()
self.lstm = nn.LSTM(
input_size=input_size,
hidden_size=hidden_size, # 隐藏层节点数量
num_layers=num_layers, # recurrent layer的数量,默认等于1
batch_first=True # 官方不推荐把batch放在第一维,此时输入输出的各个维度含义为 (seq_length,batch,feature)
) #
self.out = nn.Linear(64, output_size)
def forward(self, _x):
x, (h_n, h_c) = self.lstm(_x, None) # _x is input, size (seq_len, batch, input_size)
# print(x.shape)
# print(x[:, -1, :])
out = self.out(x[:, -1, :])
return out
net = Lstm()
optimizer = torch.optim.Adam(net.parameters(), lr=LR)
loss_func = nn.CrossEntropyLoss() # 交叉熵
# training and testing
for epoch in range(EPOCH):
for step, (train_x, train_y) in enumerate(train_loader): # gives batch data
train_x = train_x.view(-1, 28, 28) # reshape x to (batch, time_step, input_size)
output = net(train_x) # rnn output
loss = loss_func(output, train_y)
optimizer.zero_grad()
loss.backward()
optimizer.step() # 更新参数
if step % 50 == 0:
test_output = net(test_x) # (samples, time_step, input_size)
pred_y = torch.max(test_output, 1)[1].data.numpy()
accuracy = float((pred_y == test_y).astype(int).sum()) / float(test_y.size)
print('Epoch: ', epoch, '| train loss: %.4f' % loss.data.numpy(), '| test accuracy: %.2f' % accuracy)
# 打印预测结果
test_output = Lstm(test_x[:10].view(-1, 28, 28))
pred_y = torch.max(test_output, 1)[1].data.numpy()
print(pred_y, 'pred_label')
print(test_y[:10], 'real_label'))
打印精度如下:
Pytorch里面有关于LSTM的封装
torch.nn.LSTM(*args, **kargs)
参数:
input_size: 输入数据的特征数
hidden_size:隐藏层状态的特征数
num_layers: 循环层数
bias :是否使用偏置项,默认True
batch_first :如果为true,输入和输出的tensor应该为(batch,seq,feature)的形状
dropout: 非零 就在除输出层外的其他层添加Dropout层
bidirectional: True表示双向的LSTM,默认False
#torch.nn.LSTM(*args, **kargs)
net = nn.LSTM(10,20,2) #输入的特征数为10,隐含层20 2个循环层
input = Variable(torch.randn(5,3,10)) #输入
h0 = Variable(torch.randn(2,3,20)) #隐含层状态
c0 = Variable(torch.randn(2,3,20)) #记忆单元状态
output, ht = net(input, (h0,c0)) #输出是最后一层的输出特征的tensor ,还有隐含层状态
LSTM的变体
- GRU :
将遗忘,门和输入门整合成一个更新门;将单元状态 c t c_t ct和隐藏状态 h t h_t ht合并.
GRU比LSTM少了一个门,使得其训练速度更快,更方便构建更复杂的网络