问题一
推导LSTM网络中参数的梯度,并的分析其避免梯度消失的效果
LSTM网络是为了解简单RNN中存在的长程依赖问题而提出的一种新型网络结构,其主要思想是通过引入门控机制来控制数据的流通,门控机制包括输入门、遗忘门与输出门,同时在LSTM结构中,还存在一个内部记忆单元来存储每一个时间步的内部记忆,用于相关运算,具体的LSTM介绍与引入部分我们已经在上次作业中进行了相关叙述:
深度学习作业 - 作业十 - BPTT-CSDN博客
那么本次作业我们就不进行进一步的详细赘述了,直接进行推导。
首先,一个完整的LSTM网络如上图所示,各种状态与门值的计算已经写在图片里面了,我们要想推到反向传播参数,得先知道前向传播的计算过程,下面我们来总结一下这个过程。
前向传播
前向传播过程在每个时间步上的发生顺序为
(1)更新遗忘门输出
(2)更新输入门和其控制对象
(3)更新细胞状态,从到
(4)更新输出门和其控制对象,从到
(5)得到当前时间步的预测输出
反向传播
之前的反向传播中,我们都是仅仅定义了一个隐藏状态误差项,这是由于之前的网络结构只有一个隐藏状态,在LSTM中,隐层不止有还有一个,因此这里我们定义两个,即
为了方便找到梯度的递推模式,下面是根据前向传播公式给出数据在LSTM中数据的前向流动示意图
我们首先看最后一个时间步
我们可以发现,在时,误差只有这一条路径,因此可以很轻易的求出来,这里先假设损失函数是SSE,方便求解梯度(实际过程中这个损失函数可以改变,改变的话再对应求就可以了)
下面求,由于链式法则可以得到
又有
最终可求得
知道了最后一个时间步的梯度值,下一步就是求得每一步的梯度反向传播递推公式,即可由此推得前面每一个时刻的梯度公式。
下面求时的梯度
根据LSTM中数据的前向流动示意图可以得到,的误差来源如下:
(1)(注意这里的代表单元损失,而是整体损失,是单元损失的累加)。
(2)(来自输出门)
(3)(来自输入门)
(4)(代表输入门激活前的状态)
(5)(来自遗忘门)
由此,我们知道,误差主要来自于、与,下面是推导过程
由此我们求得了与和之间的递推关系。
下面继续求
根据LSTM中数据的前向流动示意图可以得到,的误差来源如下:
(1)(来自隐层状态)
(2)(来自隐层状态记忆单元)
由此,我们知道,误差主要来自于与,下面是推导过程
由此我们求得了与和之间的递推关系。
有了递推公式,现在计算梯度就比较容易了。
梯度计算
总结一下,对于所有门参数(遗忘门、输入门、候选状态、输出门),其统一的梯度表达为:
其中:
是各门的误差信号,具体为:
遗忘门:
输入门:
候选状态:
输出门:
其他参数的计算
其他参数的计算均与的计算类似。
对于输入权重矩阵,对应的梯度计算类似于,只需要将替换为
而偏置项的梯度是激活前状态的偏导的累加,即。
为什么LSTM能够避免梯度消失
这里要明确的一点是,RNN的梯度消失/爆炸并不是我们所说的传统意义上的梯度消失/爆炸。比如CNN中,各个层有各个层的不同参数,梯度各自不同,而RNN中权重在各个时间步是共享的,最终梯度是所有时间步的梯度之和。
因此,RNN 中总的梯度是不会消失的。即便梯度越传越弱,那也只是远距离的梯度消失,由于近距离的梯度不会消失,所有梯度之和便不会消失。RNN 所谓梯度消失的真正含义是,梯度被近距离梯度主导,导致模型难以学到远距离的依赖关系。
回到问题中,LSTM中有很多条传播的路径,但是有一条路径能够永远为梯度总和贡献远距离的梯度,因为这条路径上只涉及到了逐元素相乘与相加两个操作(之前SRN梯度消失就是因为涉及到了矩阵连乘,会导致梯度越乘越偏),梯度流是非常稳定的。同时对于LSTM中其他路径来说,由于梯度的计算还是矩阵连乘,照样会发生一些梯度消失或爆炸现象。
不过由于总的远距离梯度 = 各条路径的远距离梯度之和,即便其他远距离路径梯度消失了,只要保证有一条远距离路径(就是上面说的那条高速公路)梯度不消失,总的远距离梯度就不会消失(正常梯度 + 消失梯度 = 正常梯度)。因此 LSTM 通过改善一条路径上的梯度问题拯救了总体的远距离梯度。
问题二
编程实现LSTM的运行过程
这张图是老师用于让我们清楚看到LSTM内部运作的图,定义了每个序列由三个变量,,组成,网络内部存在一个记忆单元Memory。
当取1时,将输入进记忆单元Memory中,模拟了输入门的效果。
当取-1时,将记忆单元Memory清空为0,模拟了遗忘门的效果。
当取1时,将记忆单元Memory中的数据输出为。
按照如下图设置权重,来模拟这一过程,相应的在程序中也定义相同的权重,再编写一个算子即可。
实际上的LSTM网络结构比这个复杂,并且每个门控结构并不是全开或者全关,是以一定权重开启一部分的,这个例子还是比较形象的,下面分别使用Numpy与Pytorch实现。
1. 使用Numpy实现LSTM算子
代码
import numpy as np# 激活函数
def sigmoid(x):return 1 / (1 + np.exp(-x))# 权重参数
W_i = np.array([1, 0, 0, 0])
W_IGate = np.array([0, 100, 0, -10])
W_fGate = np.array([0, 100, 0, 10])
W_OGate = np.array([0, 0, 100, -10])# 输入数据
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:g_t = np.matmul(W_i, x) # 计算候选状态IGate = np.round(sigmoid(np.matmul(W_IGate, x))) # 计算输入门after_IGate = g_t * IGate # 候选状态经过输入门FGate = np.round(sigmoid(np.matmul(W_fGate, x))) # 计算遗忘门after_fGate = FGate * c_t # 内部状态经过遗忘门c_t = np.add(after_IGate, after_fGate) # 新的内部状态OGate = np.round(sigmoid(np.matmul(W_OGate, x))) # 计算输出门after_OGate = OGate * c_t # 新的内部状态经过输出门y.append(after_OGate) # 输出
print(f"输出:{y}")
运行结果
可见,输出结果与我们预期的相同,其实就是简单在循环里模拟了一下LSTM的计算过程,按照PPT上的权重与计算过程实现即可。
2. 使用nn.LSTMCell实现
代码
import numpy as np
import torch.nn# 设置参数
input_size = 4
hidden_size = 1
# 模型实例化
Cell = torch.nn.LSTMCell(input_size=input_size, hidden_size=hidden_size)
# 权重
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)
# 初始化内部状态
h_t = torch.zeros(1, 1)
c_t = torch.zeros(1, 1)
# 输入的数据[batch_size,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 = torch.transpose(input_0, 1, 0)
y = []
# 计算
for x in input:h_t, c_t = Cell(x, (h_t, c_t)) # 传入序列输入与各个状态y.append(np.round(h_t.item(), decimals=3))
print(f"输出:{y}")
输出结果
使用实例化的LSTMCell,每次循环分别传入输入,上一时间步的隐层状态与内部记忆单元值,即可自动计算,需要注意的是,由于LSTMCell内部有激活函数tanh,并且每一步计算都是数值计算,无法取整等,故输出不是严格的与示例相同,但是大小关系与0-1关系都是存在的。
3. 使用nn.LSTM实现
代码
import numpy as np
import torch.nn# 设置参数
input_size = 4
hidden_size = 1
# 模型实例化
Lstm = torch.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)
# 初始化内部状态
h_t = torch.zeros(1, 1, 1)
c_t = torch.zeros(1, 1, 1)
# 输入的数据[batch_size,seq_len,input_size]
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)
y, (h_t, c_t) = Lstm(input, (h_t, c_t))
y = torch.round(y * 1000) / 1000
print(f"输出:{y}")
运行结果
输出与使用LSTMCell相同,这里Numpy版本的程序与这两个API实现差在了tanh激活函数与内部取整操作。
LSTM就无需我们自己进行循环计算每一步骤的数据了,初始化数据后直接计算,输出结果即可。
总结
1.LSTM的推导主要是参照SumWaiLiu的博客园自己梳理复现了一遍,这里强烈推荐这个博客,写得又清楚又好,推导的过程再一次加深了对反向传播算法的认识,其实本质就是找到梯度的反向递归式,通过对求导链式法则的推导,一步一步由后面的损失函数计算前面的损失函数。得到递推公式后,想要计算任何一个参数的梯度直接使用已经计算好的损失函数代入求导式子即可,这也是本学期最后一次推导反向传播的作业了(大概),从开始到现在,接触并学会了推导式子,而不是像之前一样只会拿到一个特例来算了,这是一个很大的进步。
2.在大佬的博客园认识到,其实对于RNN来说,梯度消失不是对于整体来说的,而是对于时间间隔较长的两个时间步之前,反向传播计算梯度时,涉及到矩阵连乘,故会丢失这一部分的梯度信息,也就降低了学习这一部分数据的能力,而对于间隔较短的时间步来说,发生梯度消失或梯度爆炸的情况比较少,还是能够学得相邻时间步的信息的。也就是说对于RNN梯度消失与爆炸是说丢失了较远时间步的信息,以较近时间步的信息为主导。在LSTM中,引入了内部记忆状态这一概念,为梯度的计算提供了一条“高速公路”,其计算方式保证了不会发生矩阵连乘,也就不容易发生梯度消失或爆炸,所以在每一次求梯度都有这条路径作为保证,从而改善了梯度消失。
3. 在编程实现PPT上的例子时,从应用的角度再次认识了LSTMCell与LSTM,对于这两个函数,只需要实例化时传入参数,并且在调用之前做好初始化工作,即可成功运行。实际上由于这两个API内部存在tanh激活函数与精确的数值计算,无法与课上的模拟例子绝对相同,但是这就是我们正常应用的状态,故也不需要特别进行调整。
参考
【1】LSTM参数梯度推导与实现:对抗梯度消失,
【2】《神经网络的梯度推导与代码验证》之LSTM的前向传播和反向梯度推导 - SumwaiLiu - 博客园
【3】NNDL 作业十一 LSTM-CSDN博客