习题6-4 推导LSTM网络中参数的梯度, 并分析其避免梯度消失的效果
LSTM(长短期记忆网络)是一种特殊的循环神经网络(RNN),旨在解决普通 RNN 在处理长序列时遇到的梯度消失和梯度爆炸问题。它通过设计多个门控机制来实现更好地学习和记忆序列中的长期依赖关系。
按照上图LSTM循环单元的结构来进行前向传播的过程
对于每个时间步t,LSTM的输入包括:
当前时间步的输入
上一时刻的隐藏状态
上一时刻的记忆单元状态
(一)LSTM的前向传播
1.LSTM的遗忘门(forget gate)
决定了上一时刻的记忆单元状态有多少比例要“遗忘”,如果遗忘门算出来的结果是0.8,是上一时刻的记忆乘以0.8,有80%的要记住,而不是80%要遗忘。
遗忘门的值:
2.LSTM的输入门(input gate)
决定了当前时刻的输入有多少比例要“更新”记忆单元
输入门的值:
3.LSTM的候选记忆单元(cell state )
生成当前时刻的新候选记忆单元
新的候选记忆单元:
4.更新记忆单元
记忆单元状态是通过遗忘门、输入门和候选记忆单元来更新的:
5.LSTM的输出门(output gate)
决定了记忆单元有多少信息可以影响到输出
输出门:
6.计算隐藏状态
隐藏状态 是通过输出门和当前时刻的记忆单元 来计算的:
7.计算输出
8.总结前向传播过程
Ok,我们已经完成了前向传播的过程,计算顺序是计算遗忘门、输入门、候选记忆单元,然后根据前一个时间步的记忆单元、遗忘门、候选记忆单元、输入门更新记忆单元。计算输出门,计算隐层输出,计算预测输出。
(二) LSTM的反向梯度推导
LSTM 的反向传播主要依赖链式法则,并且要计算每个门控的梯度。由于 LSTM 结构复杂,反向传播过程的推导也会比普通 RNN 更加复杂。
对于每个时间步 t,我们需要通过链式法则计算损失函数对 LSTM 各个参数的梯度
首先定义两种隐藏状态的梯度:
为了方便推导,给出数据在LSTM中的前向流动:
下面是自己画的:
对于t=T,即时间序列截止的那个时间步,我们可以得到:
解释:
对于t<T,我们要利用和递推得到和
先来推导的递推公式:
根据上图我们可以知道的误差来源为3类:
根据链式法则和全微分方程,有:
上面这个递推公式需要解决三个问题:
,,的求解
①对于,我们在上面已经进行了推导,
②接下来求:
基于逐层展开,得到:
由于,所以:
整理得:
③ 接下来求
所以:
于是我们现在得到了从和推得的递推公式
接下来我们利用和来推得
现在,我们能计算和了,有了它们,计算变量的梯度就比较容易了,这里只以计算Wf的梯度计算为例:
其他变量的梯度按照上述类似的方式可依次求得
(三)LSTM防止梯度消失
首先需要明确的是,RNN 中的梯度消失/梯度爆炸和普通的 MLP 或者深层 CNN 中梯度消失/梯度爆炸的含义不一样。MLP/CNN 中不同的层有不同的参数,各是各的梯度;而 RNN 中同样的权重在各个时间步共享,最终的梯度 g= 各个时间步的梯度g(t)之和。
因此,RNN 中总的梯度是不会消失的。即便梯度越传越弱,那也只是远距离的梯度消失,由于近距离的梯度不会消失,所有梯度之和便不会消失。RNN 所谓梯度消失的真正含义是,梯度被近距离梯度主导,导致模型难以学到远距离的依赖关系。
LSTM防止梯度消失归功于记忆单元。
LSTM 中梯度的传播有很多条路径,但这条路径上只有逐元素相乘和相加的操作,它可以直接将信息传递到很远的时间步,使得梯度可以直接流过时间步,无需经过多次非线性变换,梯度流最稳定;但是其他路径上梯度流与普通 RNN 类似,照样会发生相同的权重矩阵反复连乘。
由于总的远距离梯度 = 各条路径的远距离梯度之和,即便其他远距离路径梯度消失了,只要保证有一条远距离路径(就是上面说的那条高速公路)梯度不消失,总的远距离梯度就不会消失(正常梯度 + 消失梯度 = 正常梯度)。因此 LSTM 通过改善一条路径上的梯度问题拯救了总体的远距离梯度。
习题6-3P 编程实现下图LSTM运行过程
使用Numpy实现LSTM算子
代码:
import numpy as np#定义激活函数,计算输入输出遗忘门都需要激活
def sigmoid(x):return 1/(1+np.exp(-x))#定义4个权重
input_weight=np.array([1,0,0,0])
inputgate_weight=np.array([0,100,0,-10])
forgetgate_weight=np.array([0,100,0,10])
outputgate_weight=np.array([0,0,100,-10])#定义输入sequence,大小为batch_size*seq_len*hidden_size
#本例中,batch_size=1,seq_len=9,hidden_size=3
input=np.array([[1,0,0,1],[3,1,0,1],[2,0,0,1],[4,1,0,1],[2,0,0,1],[1,0,1,1],[3,-1,0,1],[6,1,0,1],[1,0,1,1]])y=[] #输出
c_t=0 #记忆单元for x in input:cc_t=np.matmul(input_weight,x) #候选状态i_t=np.round(sigmoid(np.matmul(inputgate_weight,x))) #输入门,激活函数是sigmoidafter_inputgate=cc_t*i_t #候选状态经过输入门f_t=np.round(sigmoid(np.matmul(forgetgate_weight,x))) #遗忘门after_forgetgate=f_t*c_t #内部状态经过遗忘门c_t=np.add(after_inputgate,after_forgetgate) #新的内部状态o_t=np.round(sigmoid(np.matmul(outputgate_weight,x))) #输出门after_outputgate=o_t*c_t #新的内部状态经过输出门y.append(after_outputgate) #输出print('输出:',y)
结果:
使用nn.LSTMCell实现
(一)调用LSTMCell的简单示例:
import torch
import torch.nn as nn# LSTMCell 参数
input_size = 10 # 输入特征维度
hidden_size = 20 # 隐层状态维度
batch_size = 5 # 批大小# 创建 LSTMCell 模型
lstm_cell = nn.LSTMCell(input_size, hidden_size)# 创建输入数据
x = torch.randn(batch_size, input_size) # (batch_size, input_size)# 初始隐状态和细胞状态
hx = torch.zeros(batch_size, hidden_size) # 隐状态 (batch_size, hidden_size)
cx = torch.zeros(batch_size, hidden_size) # 细胞状态 (batch_size, hidden_size)# 调用 LSTMCell
h_t, c_t = lstm_cell(x, (hx, cx))print("h_t.shape:", h_t.shape) # (batch_size, hidden_size)
print("c_t.shape:", c_t.shape) # (batch_size, hidden_size)
函数创建时需要的参数:
nn.LSTMCell
是一个单步的 LSTM 计算单元,用于逐步处理每个时间步。创建时需要以下参数:
- input_size (
int
): 输入特征的维度,每个时间步的输入的特征数量。 - hidden_size (
int
): 隐藏状态的维度。
函数调用时的传入参数:
接受两个主要的输入:当前时间步的输入和前一个时间步的隐藏状态。
- input (
Tensor
): 当前时间步的输入数据,形状为(batch_size, input_size)
。 - (hx, cx) (
tuple of Tensor
): 上一时间步的隐状态和细胞状态,hx
和cx
的形状为(batch_size, hidden_size)
。
函数的返回值:
LSTMCell
返回一个元组 (h_t, c_t)
:
- h_t: 当前时间步的隐状态,形状为
(batch_size, hidden_size)
。 - c_t: 当前时间步的细胞状态,形状为
(batch_size, hidden_size)
(二)调用nn.LSTMCell函数解决本例
经过上面的例子总结,我知道了创建函数时需要传入的参数是input_size和hidden_size,所以在函数创建时要初始化input_size和hidden_size,在这个例子中,input_size是4,hidden_size为1,因为最后输出就是1个数。
函数调用时需要传入的参数是输入input、前一时刻的隐藏状态h_t、前一时刻的细胞状态c_t。所以在函数创建之前,我们需要按照标准的大小准备这些变量。首先按照例子准备好输入,大小是batch_size*seq_len*input_size。初始的隐层状态初始化为全0,大小是batch_size*hidden_size,初始的细胞状态初始化为全0,大小是batch_size*hidden_size.
因为LSTMCell函数只是返回一个时间步的输出,所以我们要遍历每个时间步,这样把输入的前两个维度进行交换顺序,变为seq_len*batch_size*input_size。
这样通过for循环,每次获得每个时间步的输入x,把x和初始的隐状态和细胞状态(h_t,c_t)传入模型,得到返回值当前时刻的隐状态和细胞状态h_t,c_t,那当前时刻获得的h_t,c_t又作为下一时刻的上一个时间步的隐层状态和细胞状态。每次把h_t放入列表output中,最后可视化输出。
代码:
import numpy as np
import torch
import torch.nn as nn#实例化
input_size=4
hidden_size=1
# 创建模型
cell=nn.LSTMCell(input_size=input_size,hidden_size=hidden_size)#修改模型参数 weight_ih.shape=(4*hidden_size, input_size),weight_hh.shape=(4*hidden_size, hidden_size),
#weight_ih、weight_hh分别为输入x、隐层h分别与输入门、遗忘门、候选、输出门的权重
cell.weight_ih.data=torch.tensor([[0,100,0,-10],[0,100,0,10],[1,0,0,0],[0,0,100,-10]],dtype=torch.float32)
cell.weight_hh.data=torch.zeros(4,1)
print('cell.weight_ih.shape:',cell.weight_ih.shape)
print('cell.weight_hh.shape',cell.weight_hh.shape)
#初始化h_0,c_0
h_t=torch.zeros(1,1)
c_t=torch.zeros(1,1)
#模型输入input_0.shape=(batch,seq_len,input_size)
input_0=torch.tensor([[[1,0,0,1],[3,1,0,1],[2,0,0,1],[4,1,0,1],[2,0,0,1],[1,0,1,1],[3,-1,0,1],[6,1,0,1],[1,0,1,1]]],dtype=torch.float32)
#交换前两维顺序,方便遍历input.shape=(seq_len,batch,input_size)
input=torch.transpose(input_0,1,0)
print('input.shape:',input.shape)
output=[]
#调用
for x in input:h_t,c_t=cell(x,(h_t,c_t))output.append(np.around(h_t.item(), decimals=3))#保留3位小数
print('output:',output)
结果:
使用nn.LSTM实现
(一)调用LSTM的简单示例:
import torch
import torch.nn as nn# LSTM 参数
input_size = 10 # 输入特征维度
hidden_size = 20 # 隐层状态维度
num_layers = 2 # LSTM 层数
batch_size = 5 # 批大小
seq_len = 7 # 序列长度# 创建 LSTM 模型
lstm = nn.LSTM(input_size, hidden_size, num_layers)# 创建输入数据
x = torch.randn(seq_len, batch_size, input_size) # (seq_len, batch_size, input_size)# 初始隐状态和细胞状态
h0 = torch.zeros(num_layers, batch_size, hidden_size)
c0 = torch.zeros(num_layers, batch_size, hidden_size)# 调用 LSTM
output, (hn, cn) = lstm(x, (h0, c0))print("output.shape:", output.shape) # (seq_len, batch_size, hidden_size)
print("hn.shape:", hn.shape) # (num_layers, batch_size, hidden_size)
print("cn.shape:", cn.shape) # (num_layers, batch_size, hidden_size)
函数创建时需要传入的参数:
nn.LSTM
是用于处理序列数据的多时间步 LSTM 层,通常在创建时需要以下参数:
- input_size (
int
): 输入特征的维度。也就是每个时间步输入的特征数量。 - hidden_size (
int
): 隐藏状态的维度。LSTM 的输出和隐状态的大小。 - num_layers (
int
, optional): LSTM 层数,默认为 1。设为多个层可以堆叠多个 LSTM 层。 - bias (
bool
, optional): 是否使用偏置项,默认为True
。 - batch_first (
bool
, optional): 如果为True
,则输入和输出的张量形状为(batch, seq_len, input_size)
,默认为False
,则为(seq_len, batch, input_size)
。 - dropout (
float
, optional): 如果num_layers > 1
,则在各个 LSTM 层之间应用 dropout,默认为0
。 - bidirectional (
bool
, optional): 是否使用双向 LSTM,默认为False
。 - proj_size (
int
, optional): 投影层的维度,默认为None
。
调用函数时的传入参数:
- input (
Tensor
): 输入数据,形状为(seq_len, batch_size, input_size)
(如果batch_first=False
)或(batch_size, seq_len, input_size)
(如果batch_first=True
)。 - h0 (
Tensor
, optional): 初始隐状态,形状为(num_layers * num_directions, batch_size, hidden_size)
。如果未提供,默认为零。 - c0 (
Tensor
, optional): 初始细胞状态,形状为(num_layers * num_directions, batch_size, hidden_size)
。如果未提供,默认为零。
返回值:
LSTM
返回一个元组 (output, (h_n, c_n))
:
- output: 每个时间步的隐状态,形状为
(seq_len, batch_size, hidden_size)
(或根据batch_first
的设置调整形状)。 - (h_n, c_n): 最后一个时间步的隐状态和细胞状态,形状为
(num_layers * num_directions, batch_size, hidden_size)
(二)调用LSTM函数解决本例
通过上面的分析,我们知道,再利用torch.nn.LSTM创建函数的时候,需要传入的参数是input_size,hidden_size和num_layer,所以在函数创建是需要定义这些变量。
函数调用时需要传入的参数是input,h_t、c_t,其中input的大小是seq_len*batch_size*hidden_size,h_t的大小是num_layers*batch_size*hidden_size,c_t的大小是num_layers*batch_size*hidden_size,所以我们需要按照题目给的例子和指定大小取初始化这些变量。
将input,(h_t,c_t)传入模型,得到返回值output,(h_t,c_t),其中output是每个时间步的输出,h_t是最后一个时间步的隐层输出,c_t是最后一个时间步的细胞状态。最后将output输出观察输出。
代码:
#LSTM
import torch
from torch import nn
#定义输出输出维度
input_size=4
hidden_size=1
#定义LSTM模型
lstm=nn.LSTM(input_size=input_size,hidden_size=hidden_size,batch_first=True)#修改模型参数
lstm.weight_ih_l0.data=torch.tensor([[0,100,0,-10],[0,100,0,10],[1,0,0,0],[0,0,100,-10]],dtype=torch.float32)
lstm.weight_hh_l0.data=torch.zeros(4,1)#定义输入sequence,大小为batch_size*seq_len*input_size
#本例中,batch_size=1,seq_len=9,input_size=3
input=torch.tensor([[[1,0,0,1],[3,1,0,1],[2,0,0,1],[4,1,0,1],[2,0,0,1],[1,0,1,1],[3,-1,0,1],[6,1,0,1],[1,0,1,1]]],dtype=torch.float32)
#初始化h_0,c_0,大小为num_layer*batch_size*hidden_size
h_t=torch.zeros(1,1,1)
c_t=torch.zeros(1,1,1)
#调用函数
output,(h_t,c_t)=lstm(input,(h_t,c_t)) #output是每个时间步的输出,ht是最后一个时间步的隐状态,ct是最有一个时间步的细胞状态rounded_output = torch.round(output * 1000) / 1000 # 保留3位小数
print(rounded_output)
结果:
总结和心得体会
调用numpy实现得到的结果和题目给的答案一样,但是直接调用nn.LSTM和nn.LSTMCell得到的结果和答案不一样,是因为在这个函数里面,候选记忆单元的激活函数和记忆单元经过输出门之前的激活函数都是tanh,但是在例子中为了简单,就直接使用了线性函数作为激活函数。
本次作业,我分析了LSTM的前向传播过程,分析前向传播中的公式,更加理解了前向传播的过程,此外我也进行了反向梯度推导,然后分析了LSTM防止梯度消失的原因。我也分析了直接调用nn.LSTM和nn.LSTMCell在函数创建时的传入参数,函数调用时的传入参数和返回值是什么,学会了在实际中怎么使用这两个函数。
参考:
《神经网络的梯度推导与代码验证》之LSTM的前向传播和反向梯度推导 - SumwaiLiu - 博客园