【循环神经网络系列】二、LSTM


参考资料:

参考博客

  人人都能看懂的LSTM

  长短时记忆网络(LSTM)

  【机器学习】详解 LSTM

  详解LSTM

  如何从RNN起步,一步一步通俗理解LSTM

  Understanding LSTM Networks


1. 前言

 在使用深度学习处理时序问题时,RNN是最常使用的模型之一。RNN之所以在时序数据上有着优异的表现是因为RNN在 t t t 时间片时会将 t − 1 t−1 t1 时间片的隐节点作为当前时间片的输入,也就是RNN具有图1的结构。这样有效的原因是之前时间片的信息也用于计算当前时间片的内容,而传统模型的隐节点的输出只取决于当前时间片的输入特征。

在这里插入图片描述

 RNN的数学表达式可以表示为:
h t = σ ( x t × w x t + h t − 1 × w h t + b ) h_t=σ(x_t×w_{xt}+h_{t−1}×w_{ht}+b) ht=σ(xt×wxt+ht1×wht+b)
 而传统的DNN的隐节点表示为:
h t = σ ( x t × w x t + b ) h_t=σ(x_t×w_{xt}+b) ht=σ(xt×wxt+b)
对比RNN和DNN的隐节点的计算方式,我们发现唯一不同之处在于RNN将上个时间片的隐节点状态 h t − 1 h_t−1 ht1 也作为了神经网络单元的输入,这也是RNN擅长处理时序数据最重要的原因。

 所以,RNN的隐节点 h t − 1 h_t−1 ht1 有两个作用:

  • 计算在该时刻的预测值 y t : y t = σ ( h t ∗ w + b ) y^t: y^t=σ(h_t∗w+b) yt:yt=σ(htw+b)
  • 计算下个时间片的隐节点状态 h t h_t ht

 RNN的该特性也使RNN在很多学术和工业前景,例如OCR,语音识别,股票预测等领域上有了十足的进展。


(1)长期依赖(Long Term Dependencies)

 在深度学习领域中(尤其是RNN),“长期依赖“问题是普遍存在的。长期依赖产生的原因是当神经网络的节点经过许多阶段的计算后,之前比较长的时间片的特征已经被覆盖,例如下面例子:

eg1: The cat, which already ate a bunch of food, was full.
|   |     |      |     |  |   |   |   |     |   |
t0  t1    t2      t3    t4 t5  t6  t7  t8    t9 t10
eg2: The cats, which already ate a bunch of food, were full.
|   |      |      |     |  |   |   |   |     |    |
t0  t1     t2     t3    t4 t5  t6  t7  t8    t9   t10

 我们想预测’full’之前系动词的单复数情况,显然full是取决于第二个单词’cat‘的单复数情况,而非其前面的单词food。根据图1展示的RNN的结构,随着数据时间片的增加,RNN丧失了学习连接如此远的信息的能力(图2)。

在这里插入图片描述


(2)梯度消失/爆炸

 梯度消失和梯度爆炸是困扰RNN模型训练的关键原因之一,产生梯度消失和梯度爆炸是由于RNN的权值矩阵循环相乘导致的,相同函数的多次组合会导致极端的非线性行为。梯度消失和梯度爆炸主要存在RNN中,因为RNN中每个时间片使用相同的权值矩阵。对于一个DNN,虽然也涉及多个矩阵的相乘,但是通过精心设计权值的比例可以避免梯度消失和梯度爆炸的问题。

 处理梯度爆炸可以采用梯度截断的方法。所谓梯度截断是指将梯度值超过阈值 θ θ θ 的梯度手动降到 θ θ θ 。虽然梯度截断会一定程度上改变梯度的方向,但梯度截断的方向依旧是朝向损失函数减小的方向。

 对比梯度爆炸,梯度消失不能简单的通过类似梯度截断的阈值式方法来解决,因为长期依赖的现象也会产生很小的梯度。在上面例子中,我们希望 t 9 t9 t9 时刻能够读到 t 1 t1 t1 时刻的特征,在这期间内我们自然不希望隐层节点状态发生很大的变化,所以 [ t 2 , t 8 ] [t2,t8] [t2,t8] 时刻的梯度要尽可能的小才能保证梯度变化小。很明显,如果我们刻意提高小梯度的值将会使模型失去捕捉长期依赖的能力。


2. LSTM网络结构

 LSTM的全称是Long Short Term Memory,顾名思义,它具有记忆长短期信息的能力的神经网络。LSTM提出的动机是为了解决上面我们提到的长期依赖问题。传统的RNN节点输出仅由权值,偏置以及激活函数决定(图3)。RNN是一个链式结构,每个时间片使用的是相同的参数。

在这里插入图片描述

 而LSTM之所以能够解决RNN的长期依赖问题,是因为LSTM引入了门(gate)机制用于控制特征的流通和损失。对于上面的例子,LSTM可以做到在 t 9 t_9 t9 时刻将 t 2 t_2 t2 时刻的特征传过来,这样就可以非常有效的判断 t 9 t_9 t9 时刻使用单数还是复数了。LSTM是由一系列LSTM单元(LSTM Unit)组成,其链式结构如下图:

在这里插入图片描述

  • 每个黄色方框表示一个神经网络层,由权值,偏置以及激活函数组成;
  • 每个粉色圆圈表示元素级别操作;
  • 箭头表示向量流向;
  • 相交的箭头表示向量的拼接;
  • 分叉的箭头表示向量的复制。

在这里插入图片描述


2.1 LSTM的前向计算

  • 长期记忆单元 c t c_t ct
  • 短期记忆单元 h t h_t ht
  • 候选记忆单元 c t ~ \tilde{c_t} ct~
  • 遗忘门 f t f_t ft
  • 输入门 i t i_t it
  • 输出门 o t o_t ot

t t t 时刻,LSTM的输入有三个

  • 当前时刻网络的输入值 x t x_t xt
  • 上一时刻的短期记忆 h t − 1 h_{t-1} ht1
  • 上一时刻的长期记忆 c t − 1 c_{t-1} ct1

LSTM的输出有两个

  • 当前时刻LSTM的短期记忆单元 h t h_t ht
  • 当前时刻的长期记忆单元 c t c_t ct

在这里插入图片描述

LSTM的关键,就是怎样控制长期状态c。在这里,LSTM的思路是使用三个控制开关

  • 第一个开关,负责控制继续保存长期状态 c t c_t ct
  • 第二个开关,负责控制把即时状态输入到长期状态 c t c_t ct
  • 第三个开关,负责控制是否把长期状态 c t c_t ct 作为当前的LSTM的输出。

(1)门

 前文描述的开关在算法实现中使用(gate)。实际上是一层全连接层,它的输入是一个向量,输出是一个 [ 0 , 1 ] [0,1] [0,1] 的实数向量(一般使用sigmoid函数)。

 假设W是门的权重向量,b是偏置项,那么门可以表示为:

g ( x ) = σ ( W x + b ) g(x)=σ(Wx+b) g(x)=σ(Wx+b)

门的使用,就是用门的输出向量按元素乘以我们需要控制的那个向量。

 因为门的输出是0到1之间的实数向量,那么,当门输出为0时,任何向量与之相乘都会得到0向量,这就相当于啥都不能通过;输出为1时,任何向量与之相乘都不会有任何改变,这就相当于啥都可以通过。因为 σ \sigma σ 的值域是(0,1),所以门的状态都是半开半闭的。

LSTM用了三个门:

  • 遗忘门(forget gate),决定上一时刻的长期状态 c t − 1 c_{t-1} ct1 有多少保留到当前时刻长期状态 c t c_t ct
  • 输入门(input gate),决定了当前时刻网络的输入 x t x_t xt 有多少保存到长期状态 c t c_t ct
  • 输出门(output gate),控制长期状态 c t c_t ct 有多少输出到LSTM的当前输出值 h t h_t ht

(2)遗忘门

 在我们LSTM中的第一步是决定我们会从长期状态中丢弃什么信息。这个决定通过一个称为“遗忘门”的结构完成。该遗忘门会读取上一个输出 h t − 1 h_t-1 ht1和当前输入 x t x_t xt,做一个sigmoid的非线性映射,然后输出一个向量 f t f_t ft (sigmoid函数取值为(0,1)区间,所以该向量的每一个维度都在(0,1)之间,1表示完全保留,0表示完全舍弃,相当于记住了重要的,忘记了无关紧要的),最后与上一时刻的细胞状态 c t − 1 c_{t-1} ct1 相乘。

 事实上,权重矩阵都是两个矩阵拼接而成的:一个是 W f h W_{fh} Wfh ,它对应着输入项 h t − 1 h_{t-1} ht1,其维度为 d c × d h d_c \times d_h dc×dh;一个是 W f x W_{fx} Wfx ,它对应着输入项 x t x_t xt,其维度为 d c × d x d_c \times d_x dc×dx W f W_f Wf 可以写为:

在这里插入图片描述

 上式中, W f W_f Wf 是遗忘门的权重矩阵, [ h t − 1 , x t ] [h_{t-1},x_t] [ht1,xt] 表示把两个向量连接成一个更长的向量, b f b_f bf 是遗忘门的偏置项, σ \sigma σ 是sigmoid函数。如果输入的维度是 d x d_x dx ,隐藏层的维度是 d h d_h dh ,单元状态的维度是 d c d_c dc (通常 d h = d c d_h=d_c dh=dc ),则遗忘门的权重矩阵维度是 d c × ( d h + d x ) d_c \times (d_h+d_x) dc×(dh+dx)

在这里插入图片描述


(3)输入门

 下一步是确定什么样的新信息被存放在长期状态 c t c_{t} ct 中。这里包含两个部分:

  • sigmoid层称“输入门层”决定什么值我们将要更新;
  • tanh层创建一个新的候选值向量 c t ~ \tilde{c_t} ct~ ,会被加入到长期状态中;

在这里插入图片描述

在这里插入图片描述


(4)更新长期状态 c t c_t ct

  • 我们把上一时刻的长期状态 c t − 1 c_{t-1} ct1与遗忘门的输出 f t f_t ft 相乘,丢弃掉不需要的信息;
  • 将候选状态 c t ~ \tilde{c_t} ct~ 与输入门的输出 i t i_t it 相乘,用于保留当前重要的信息 ;
  • 最后把二者加和;

由于遗忘门的控制,它可以保存很久很久之前的信息,由于输入门的控制,它又可以避免当前无关紧要的内容进入记忆。

在这里插入图片描述


(5)输出门 o t o_t ot

 输出门 o t o_t ot 控制当前时刻的长期状态 c t c_t ct 有多少信息需要输出给短期状态 h t h_t ht
在这里插入图片描述

在这里插入图片描述


2.2 相关问题总结

(1)LSTM实现长短期记忆的原理是什么?

 与传统的循环神经网络相比,LSTM在 h t − 1 h_{t-1} ht1 x t x_t xt 的基础上加入了一个cell state (即长时记忆状态 c t c_{t} ct ) 来计算 h t h_t ht ,并且对网络模型内部进行了更加精心的设计。

  • 遗忘门神经元 f t f_t ft 控制前一步记忆单元中信息有多大程度上被遗忘掉;
  • 输入门神经元 i t i_t it 控制当前记忆中的信息以多大程度更新到记忆单元中;
  • 输出门神经元 o t o_t ot 控制当前的 h t h_t ht (即短时记忆单元)的输出有多大程度取决于当前的长时记忆单元。

 ①当输入的序列没有重要信息时,LSTM遗忘门的值接近于1,输入门的值接近于0,此时过去的信息将会保留,从而实现了长时记忆的功能;

 ②当输入的序列中出现了重要信息时,LSTM应当将其存入记忆中,此时其输入门的值会接近于1;

 ③当输入的序列中出现了重要信息,且该信息意味着之前的记忆不再重要时,则输入门的值会接近于1,而遗忘门的值会接近于0,这样旧的记忆就会遗忘,新的重要信息被记忆。

 经过这样的设计,整个网络更容易学习到序列之间的长期依赖。


(2)LSTM的原理公式是什么?

在这里插入图片描述


(3)LSTM为什么选择 s i g m o i d sigmoid sigmoid 作为遗忘门、输入门以及输出门神经元的激活函数,又为什么选择 tanh ⁡ \tanh tanh 作为记忆门神经元的激活函数?

 我们可以注意到,无论是 s i g m o i d sigmoid sigmoid 还是 tanh ⁡ \tanh tanh ,它们均属于饱和函数,也就是说当输入达到一定值的情况下,输出就不会明显变化了。比如,当输入小于一定值时, s i g m o i d sigmoid sigmoid 输出几乎接近于0, tanh ⁡ \tanh tanh 输出几乎接近于-1;当输入大于一定值时, s i g m o i d sigmoid sigmoid tanh ⁡ \tanh tanh 输出均接近于1。

  • (1)如果使用非饱和的激活函数,例如 R e L U ReLU ReLU ,我们很难实现门控效果。 s i g m o i d sigmoid sigmoid 的函数输出在0~1之间,且输入较大或较小时,其输出会非常接近1或0,从而保证该门是开或关。故选择 s i g m o i d sigmoid sigmoid 作为遗忘门、输入门以及输出门神经元的激活函数最合适不过。
  • (2) tanh ⁡ \tanh tanh 函数的输出在-1~1之间,并且中心为0,这与大多数场景下特征分布是0中心吻合,并且, tanh ⁡ \tanh tanh 在输入为0附近的时相比 s i g m o i d sigmoid sigmoid 函数有更大的梯度,通常使模型收敛更快

2.3 LSTM的参数学习

 LSTM的训练算法仍然是反向传播算法,具体参考:长短时记忆网络(LSTM)


3. Pytorch实现

参考资料

  Pytorch-LSTM输入输出参数详解

  Pytorch的LSTM的理解


 首先根据Pytorch中的LSTM实现,对其输入输出和参数进行分析:

class torch.nn.LSTM(*args, **kwargs)

(1)参数列表

  • input_size:x的特征维度;

  • hidden_size:隐藏层的特征维度;

  • num_layers:LSTM隐层的层数,默认为1;

  • bias:False则bih=0和bhh=0。默认为True;

  • batch_first:如果为True,则输入输出的数据格式为 (batch, seq, feature);

  • dropout:除最后一层,每一层的输出都进行dropout,默认为: 0;

  • bidirectional:True则为双向LSTM默认为False;

  • 输入:input,( h 0 h_0 h0, c 0 c_0 c0);

  • 输出:output,( h n , c n h_n,c_n hn,cn);

(2)输入数据格式

  • input(seq_len, batch, input_size)
  • h 0 h_0 h0(num_layers * num_directions, batch, hidden_size),初始化隐状态
  • c 0 c_0 c0(num_layers * num_directions, batch, hidden_size),初始化细胞状态

(3)输出数据格式

  • output(seq_len, batch, hidden_size * num_directions)

  • h n h_n hn(num_layers * num_directions, batch, hidden_size),最后一个时间步的隐状态

  • c n c_n cn(num_layers * num_directions, batch, hidden_size),最后一个时间步的细胞状态

在这里插入图片描述


3.1 单向LSTM

输入数据(以batch_first=True,单层单向为例)

  • 输入维度 = 28
  • time_steps= 3
  • batch_first = True
  • batch_size = 10
  • hidden_size =4
  • num_layers = 1
  • bidirectional = False

(1)h_init:维度形状为 (num_layers * num_directions, batch, hidden_size):

  • 第一个参数的含义num_layers * num_directions, 即LSTM的层数乘以方向数量。这个方向数量是由前面介绍的bidirectional决定,如果为False,则等于1;反之等于2(可以结合下图理解num_layers * num_directions的含义)。
  • batch:批数据量大小
  • hidden_size: 隐藏层节点数

(2)c_init:维度形状也为(num_layers * num_directions, batch,hidden_size),各参数含义与h_init相同。因为本质上,h_init与c_init只是在不同时刻的不同表达而已。

如果没有传入,h_init和c_init,根据源代码来看,这两个参数会默认为0。

import torch
from torch.autograd import Variable
from torch import nn

input_size = 28 # 输入维度
batch_size = 10 # 批量大小
seq_len = 3     # 序列长度[x1~xn]
hidden_size = 4 # 隐藏层维度

# 单向单层LSTM网络
lstm = nn.LSTM(input_size, hidden_size, num_layers=1,batch_first=True)  # 构建LSTM网络

# 构建输入(batch,seqlen,feature)->(10,3,28)
lstm_input = Variable(torch.randn(batch_size, seq_len, input_size))

h_init = Variable(torch.randn(1, lstm_input.size(0), hidden_size))  # 构建h输入参数   -- 每个batch对应一个隐层
c_init = Variable(torch.randn(1, lstm_input.size(0), hidden_size))  # 构建c输出参数   -- 每个batch对应一个隐层
out, (h, c) = lstm(lstm_input, (h_init, c_init))  # 将输入数据和初始化隐层、记忆单元信息传入
 
 
print(lstm.weight_ih_l0.shape) # 对应的输入学习参数
print(lstm.weight_hh_l0.shape) # 对应的隐层学习参数
print(out.shape, h.shape, c.shape)

输出结果如下:

在这里插入图片描述

 (1)lstm_seq.weight_ih_l0.shape的结果为:torch.Size([16, 28]),表示对应的输入到隐层的学习参数:(4*hidden_size, input_size)。

 (2)lstm_seq.weight_hh_l0.shape的结果为:torch.Size([16, 4]),表示对应的隐层到隐层的学习参数:(4*hidden_size, num_directions * hidden_size)

 (3)out.shape的输出结果:torch.Size([10,3, 4]),表示隐层到输出层学习参数,即(batch,time_steps, num_directions * hidden_size),维度和输入数据类似,会根据batch_first是否为True进行对应的输出结果,(如果代码中,batch_first=False,则out.shape的结果会变为:torch.Size([3, 10, 4])),

h.shape输出结果是: torch.Size([1, 10, 4]),表示隐层到输出层的参数,h_n:(num_layers * num_directions, batch, hidden_size),只会输出最后个time step的隐状态结果

c.shape的输出结果是: torch.Size([1, 10, 4]),表示隐层到输出层的参数,c_n :(num_layers * num_directions, batch, hidden_size),同样只会输出最后个time step的cell状态结果


3.2 双向LSTM

 RNN和LSTM都只能依据之前时刻的时序信息来预测下一时刻的输出,但在有些问题中,当前时刻的输出不仅和之前的状态有关,还可能和未来的状态有关系

 比如预测一句话中缺失的单词不仅需要根据前文来判断,还需要考虑它后面的内容,真正做到基于上下文判断。即:对于每个时刻t,输入会同时提供给两个方向相反的RNN,输出由这两个单向RNN共同决定

 因此提出了双向循环神经网络,网络结构如下图。可以看到Forward层和Backward层共同连接着输出层,其中包含了6个共享权值w1-w6。

在这里插入图片描述

 在Forward层从1时刻到t时刻正向计算一遍,得到并保存每个时刻向前隐含层的输出。在Backward层沿着时刻t到时刻1反向计算一遍,得到并保存每个时刻向后隐含层的输出。最后在每个时刻结合Forward层和Backward层的相应时刻输出的结果得到最终的输出,用数学表达式如下:

在这里插入图片描述


输入数据(以batch_first=True,双层双向)

在这里插入图片描述

'''
    batch_first = True :   输入形式:(batch, seq, feature)
    bidirectional = True
    num_layers = 2
'''
num_layers = 2  # 两层隐藏层
bidirectional_set  = True   # 使用双向LSTM
bidirectional = 2 if bidirectional_set else 1
 
input_size = 28 # 输入维度
hidden_size = 4 # 隐层维度
 
lstm_seq = nn.LSTM(input_size, hidden_size, num_layers=num_layers,
                   bidirectional=bidirectional_set,batch_first=True)  # 构建LSTM网络

lstm_input = Variable(torch.randn(10, 3, 28))  # 构建输入

h_init = Variable(torch.randn(num_layers*bidirectional, lstm_input.size(0), hidden_size))  # 构建h输入参数
c_init = Variable(torch.randn(num_layers*bidirectional, lstm_input.size(0), hidden_size))  # 构建c输出参数
out, (h, c) = lstm_seq(lstm_input, (h_init, c_init))  # 计算

print(lstm_seq.weight_ih_l0.shape)
print(lstm_seq.weight_hh_l0.shape)
print(out.shape, h.shape, c.shape)

输出结果如下

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

travellerss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值