习题6-4 推导LSTM网络中参数的梯度, 并分析其避免梯度消失的效果.
LSTM遗忘门将输入映射到[0,1]之间,可以自主选择遗忘部分的信息的多少,来改善梯度消失的情况。既可以无限接近一,完全保留信息,长期信息的梯度不会消失;也可以无限接近于0,全部遗忘,不再往前传递之前的信息。
习题6-3P 编程实现下图LSTM运行过程
LSTM的真正运行过程中,g和h实际上使用tanh激活函数,而在这里,替换成了线性激活。你可以认为这里weight_Whh是全零,并且不带有偏置。或者认为把weight_Whh省略了。
1. 使用Numpy实现LSTM算子
程序:
import numpy as np
def sigmoid(x):
return 1/(1+np.exp(-x))
Wi=np.array([0,100,0,-10])
Wc=np.array([1,0,0,0])
Wf=np.array([0,100,0,10])
Wo=np.array([0,0,100,-10])
ct=0
y=0
def lstm_cell(xt,a_prev,c_prev,Wi,Wc,Wf,Wo):
ft=sigmoid(np.dot(xt,Wf.T))
it=sigmoid(np.dot(xt,Wi.T))
ct=np.dot(xt,Wc.T)
# ct=np.tanh(ct)
ot=sigmoid(np.dot(xt,Wo.T))
c_prev=c_prev*ft+it*ct
a_prev=ot*c_prev
# a_prev=ot*np.tanh(c_prev)
return a_prev,c_prev
x=[[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]]
xt=np.array(x)
a_prev=0
c_prev=0
memory=[]
y=[]
for i in range(xt.shape[0]):
# print(i, "memory is : " , round(c_prev))
# print(i, "memory is : " , c_prev)
memory.append(round(c_prev))
a,c=lstm_cell(xt[i],a_prev,c_prev,Wi,Wc,Wf,Wo)
a_prev=a
c_prev=c
# print(i,"y is : " , round(a_prev))
y.append(round(a_prev))
# print(i,"y is : " , a_prev)
print("memory: ",memory)
print("y: ",y)
运行结果:
memory: [0, 0, 3, 3, 7, 7, 7, 0, 6]
y: [0, 0, 0, 0, 0, 7, 0, 0, 6]
2. 使用nn.LSTMCell实现
torch.nn.LSTMCell(input_size, hidden_size, bias=True, device=None, dtype=None)
程序中需要自定义权重矩阵,使得其参数相同,另外,把参数bias=False,不带偏置。根据文档的说明和上述程序运行流程,我们可以得知,weight_ih形状是(4,4)的矩阵,第一行是遗忘门权重,第二行是输入门权重,第三行是候选状态权重,第四行是输出门权重。
weight_hh 是一个(4,1)的权重矩阵,根据之前的分析,它应该被设置为全0。
这个无法实现结果和第一问的结果一样,就是因为激活函数不一样。torch内置的是包装好的,激活函数无法更改。所以要想知道结果对不对,那就只有更改numpy实现的程序,将线性激活改成tanh.
运行之后,结果:
y: [0, 0, 0, 0, 0, 1, 0, 0, 1]
使用LSTMCell:
import torch
import torch.nn as nn
torch.manual_seed(0)
input_size = 4
hidden_size = 1
xt = 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)
lstm_cell = nn.LSTMCell(input_size, hidden_size,bias=False)
lstm_cell.weight_ih.data=torch.tensor([[0.,100.,0.,10.],[0.,100.,0.,-10.],[1.,0.,0.,0.],[0.,0.,100.,-10.]])
lstm_cell.weight_hh.data=torch.zeros([4,1])
hx = torch.zeros(1, hidden_size)
cx = torch.zeros(1, hidden_size)
cellmemory=[]
celly=[]
for i in range(xt.shape[0]):
# print(xt[i].unsqueeze(0))
hx, cx = lstm_cell(xt[i].unsqueeze(0), (hx, cx))
# print(i,"y is")
# print(i,"memory is :",cx.detach().numpy()[0][0])
# print(i,"y is : ",round(hx.detach().numpy()[0][0]))
cellmemory.append(round(cx.detach().numpy()[0][0]))
celly.append(round(hx.detach().numpy()[0][0]))
print(cellmemory)
print(celly)
结果:
[0, 0, 0, 0, 0, 1, 0, 0, 1]
3. 使用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)
同样,Bias=False.因为多了个num_layers,所以在参数初始化的时候稍微复杂一点。详情可见之后的程序中对比。
还有不同的就是,LSTM可以直接处理一整个序列,不需要管理时间步,所以不需要像LSTMCell那样进行循环。
程序:
import torch
import torch.nn as nn
input_size = 4
hidden_size = 1
num_layers = 1
xt = 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)
lstm = nn.LSTM(input_size, hidden_size, num_layers,bias=False)
# 手动设置参数权重
lstm.weight_ih_l0.data = torch.Tensor([[0.,100.,0.,10.],[0.,100.,0.,-10.],[1.,0.,0.,0.],[0.,0.,100.,-10.]])
lstm.weight_hh_l0.data = torch.zeros([4,1])
hx = torch.zeros(num_layers, 1, hidden_size)
cx = torch.zeros(num_layers, 1, hidden_size)
output,_= lstm(xt.unsqueeze(1), (hx, cx))
k=0
for i in output.squeeze(1).detach().numpy():
print(k," output y is : ",round(i[0]))
k=k+1
运行结果:
0 output y is : 0
1 output y is : 0
2 output y is : 0
3 output y is : 0
4 output y is : 0
5 output y is : 1
6 output y is : 0
7 output y is : 0
8 output y is : 1
结果是使用相同的参数和激活函数,三种方法得到的结果都是一致的。结果正确。
参考: