习题6-4 推导LSTM网络中参数的梯度, 并分析其避免梯度消失的效果
首先需要明确的是,RNN 中的梯度消失/梯度爆炸和普通的 MLP 或者深层 CNN 中梯度消失/梯度爆炸的含义不一样。MLP/CNN 中不同的层有不同的参数,各是各的梯度;而 RNN 中同样的权重在各个时间步共享,最终的梯度为各个时间步的梯之和。
因此,RNN 中总的梯度是不会消失的。即便梯度越传越弱,那也只是远距离的梯度消失,由于近距离的梯度不会消失,所有梯度之和便不会消失。RNN 所谓梯度消失的真正含义是,梯度被近距离梯度主导,导致模型难以学到远距离的依赖关系。
有多条求导路径,最后将这些求导路径相加得到最终的梯度,只要保证有一条远距离路径梯度不消失,总的远距离梯度就不会消失,这里我们只关注传到
的过程
下图箭头为为从传到
的过程的相反方向,即反向传播的方向,值得注意的是,从
传到
,包括从
传到
,再从
传到
。
可以求得公式
可以看到,这个梯度是由四个式子相加而成,其他姑且不用看,就看这个,控制好
就能让它梯度不消失。
也就是说,如果想让过去的影响现在的输出,让为1就可以了,之后在上图最上面那条路径中梯度会直接从
流到
,梯度不会消失,
的大小由训练学习得到的(它会学习到什么时候就不记前头的了),直到有输入x得到的
为0,则和前面的信息就没有关系了。
就是反向传到较远的地方,它的梯度是有多个式子的和(多条路到达),而不是仅仅是几个变量相乘的单一式子。
注意:梯度消失现象可以改善,但是梯度爆炸还是可能会出现的。
LSTM依然不能完全解决梯度消失这个问题,有文献表示序列长度一般到了三百多仍然会出现梯度消失现象。如果想彻底规避这个问题,还是transformer好用。(现在还没学 transformer,浅期待一下)
参考老师给的鱼书上的看法分析,避免梯度消失主要有两个原因,一个是使用对应元素的乘积,一个是遗忘门的使用。
习题6-3P 编程实现下图LSTM运行过程
同学提出,未发现输入。可以适当改动例题,增加该输入。
实现LSTM算子,可参考实验教材代码。
1. 使用Numpy实现LSTM算子
import numpy
import numpy as np
def sigmoid(x):#该题的输入不是贼正就是贼负,需要得到的结果结果不是0就是1,干脆自己定义一个简单的函数充当sigmoid
if x>0:
return 1
else:
return 0
Wi=np.array([0,100,0,-10])
Wf=np.array([0,100,0,10])
Wc=np.array([1,0,0,0])
Wo=np.array([0,0,100,-10])
x=np.array([[1,0,0],[3,1,0],[2,0,0],[4,1,0],[2,0,0],[1,0,1],[3,-1,0],[6,1,0],[1,0,1]])
rem=0#首先让初始的ft为0
r=[]
yy=[]
for j in x:
new_element = [1]
j=np.append(j,new_element)#把那个1放进去
i1=j*Wi
i2=sum(i1)
i3=sigmoid(i2)
f1 = j* Wf
f2 = sum(f1)
f3 = sigmoid(f2)
c1 = j * Wc
c2 = sum(c1)
o1 = j * Wo
o2 = sum(o1)
o3 = sigmoid(o2)
r.append(rem)
rem=i3*c2+f3*rem
y=rem*o3
yy.append(y)
print("当前的remember为:", r)
print("输出为:",yy)
可以看到,结果与题目中一致。
2. 使用nn.LSTMCell实现
这里的使用与np.rnncell啥的差不多,可以参考我之前写的文章,可以帮助怎样看文档,详细情况参考官方文档https://pytorch.org/docs/stable/generated/torch.nn.LSTMCell.html#torch.nn.LSTMCell
实例化
torch.nn.LSTMCell(input_size, hidden_size, bias=True, device=None, dtype=None)
输入:Inputs: input, (h_0, c_0)包含输入值和初始的内部状态和外部状态,具体形状:
输出:Outputs:(h_1, c_1)包含该时间步的输出的内部状态和外部状态,具体形状:
参数:,具体形状:
值得注意的是:
weight_ih :形状:4*hidden_size, input_size
其中为啥放的4*hidden_size,因为放的是“W_ii|W_if|W_ig|W_io”,即分别由到
、
、
和
的权重。
weight_hh:形状:4*hidden_size, hidden_size
放的是“W_hi|W_hf|W_hg|W_ho”,即分别由到
、
、
和
的权重。
import torch
import torch.nn as nn
torch.set_printoptions(precision=4,sci_mode=False)#设置不用科学计数法输出,这个个人喜好,设也行
#构建初始值
x=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]]])#[seq_len,batchsize,input_size]
h0=torch.zeros(1,1)#(batchsize,hidden_size)
c0=torch.zeros(1,1)#(batchsize,hidden_size)
#实例化
l=nn.LSTMCell(4,1)#[feature_len,hidden_len,num_layers]
#调整参数
l.weight_ih.data= torch.tensor([[0,100,0,-10],[0,100,0,10],[1,0,0,0],[0,0,100,-10]]).float()# (W_ii|W_if|W_ig|W_io), of shape (4*hidden_size, input_size)
l.weight_hh.data=torch.zeros(4,1)#(W_hi|W_hf|W_hg|W_ho), of shape (4*hidden_size, hidden_size)
h=[]
#forward()
for j in x:
(h0, c0) = l(j, (h0, c0))
h.append(torch.squeeze(h0).tolist())
h = [f'{num:.4f}' for num in h]
print("输出为:",h)
结果:
3. 使用nn.LSTM实现
这里的使用与np.rnn啥的差不多,可以参考我之前写的文章,可以帮助怎样看文档,详细情况参考官方文档https://pytorch.org/docs/stable/generated/torch.nn.LSTM.html#torch.nn.LSTM
实例化
torch.nn.LSTM(self, input_size, hidden_size, num_layers=1, bias=True, batch_first=False, dropout=0.0, bidirectional=False, proj_size=0, device=None, dtype=None)
输入:Inputs: input, (h_0, c_0)包含输入值和初始的内部状态和外部状态,具体形状:
输出:Outputs: output, (h_n, c_n)包含(每个时间步的隐藏层的外部状态)的输出值和初始的内部状态和外部状态,具体形状:
参数:
值得注意的是:
weight_ih_l[k] :形状:4*hidden_size, input_size
其中为啥放的4*hidden_size,因为放的是“W_ii|W_if|W_ig|W_io”,即分别由到
、
、
和
的权重。
weight_hh_l[k]:形状:4*hidden_size, hidden_size
放的是“W_hi|W_hf|W_hg|W_ho”,即分别由到
、
、
和
的权重。
首先访问权重名称方便改变它
weight = l.state_dict() # 使用`state_dict`属性来获取或设置模型的权重
print("权重名称:\n", weight.keys())
import torch
import torch.nn as nn
torch.set_printoptions(precision=4,sci_mode=False)#设置不用科学计数法输出,这个个人喜好,设也行
#构建初始值
x=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]]])#[seq_len,batchsize,input_size]
h0=torch.zeros(1,1,1)#(D∗num_layers,batchsize,hidden_size)
c0=torch.zeros(1,1,1)#(D∗num_layers,batchsize,hidden_size)
#实例化
l=nn.LSTM(4,1,1)#[feature_len,hidden_len,num_layers]
#调整参数
l.weight_ih_l0.data= torch.tensor([[0,100,0,-10],[0,100,0,10],[1,0,0,0],[0,0,100,-10]]).float()# (W_ii|W_if|W_ig|W_io), of shape (4*hidden_size, input_size)
l.weight_hh_l0.data=torch.zeros(4,1)#(W_hi|W_hf|W_hg|W_ho), of shape (4*hidden_size, hidden_size)
#forward()
out, (h, c)=l(x,(h0,c0))
out=torch.squeeze(out)
print(out)
【直接调用与自己实现numpy代码比较】
与numpy里的结果不一样,刚开始确实咋调都是这个值,后来查看了同学的博客,他发现了这是因为激活函数的原因,咱们这个nn.lstm有的是使用tanh的,那我也加上,主要改变就是激活函数的代码,直接拿公式写的函数,没有那些条件语句了,并且加上对激活。
在最后的输出上,直接调用的输出指的是,所以为了追求一致,就在自己写的numpy代码加上对
激活那步:
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def tanh(x):
return (np.exp(x)-np.exp(-x))/ (np.exp(x)+np.exp(-x))
Wi=np.array([0,100,0,-10])
Wf=np.array([0,100,0,10])
Wc=np.array([1,0,0,0])
Wo=np.array([0,0,100,-10])
x=np.array([[1,0,0],[3,1,0],[2,0,0],[4,1,0],[2,0,0],[1,0,1],[3,-1,0],[6,1,0],[1,0,1]])
rem=0#首先让ft为0
r=[]
yy=[]
for j in x:
new_element = [1]
j=np.append(j,new_element)#把那个1放进去
i1=j*Wi
i2=sum(i1)
i3=sigmoid(i2)
f1 = j* Wf
f2 = sum(f1)
f3 = sigmoid(f2)
c1 = j * Wc
c2 = sum(c1)
c2 = tanh(c2)
o1 = j * Wo
o2 = sum(o1)
o3 = sigmoid(o2)
r.append(rem)
rem=i3*c2+f3*rem#相当于ct
rem1=tanh(rem)#再激活一下
y=rem1*o3
yy.append(y)
yy = [f'{num:.4f}' for num in yy]
print("输出为:",yy)
关于:在自己写的numpy代码里,根本就没定义,在直接调用的代码里,里面自带了,那咱们就令其对应权重为0,也相当于没有
看到了Tanh()的梯度函数,粘过来了。
补一个,https://blog.youkuaiyun.com/weixin_41504611/article/details/135026799是写的向量内积和hadamard积(元素对位乘)
这次也是第一次代码完全由自己写的,确实感受不一样,即使代码水平有限,但是很有成就感!
https://www.cnblogs.com/sumwailiu/p/13623985.html
梯度消失和爆炸的原因和解决方案(这个可以看一下汇总)
https://blog.youkuaiyun.com/qq_38975453/article/details/134782034?spm=1001.2014.3001.5502