LSTM 长短期记忆网络
由于长序列的长期依赖问题,当序列很长,RNN难以学习并保持序列早期时间步的信息。
LSTM(Long Short-Term Memory) 引入了细胞状态的概念,通过门控机制,控制信息保留的程度。
LSTM结构
LSTM的关键是细胞状态c,也就是图中最上方的一条贯穿整个链式结构的水平线,每个隐藏状态的输出,由细胞中的几个门共同控制。同时,这几个门也作用于细胞状态的更新。
门控机制
- 遗忘门
遗忘门(Forget Gate) 决定了要保留多少上一个细胞状态的信息
通过输入当前时间步的x和上一隐藏状态,对其进行拼接,乘上对应的权重加上偏置,最后使用sigmoid进行激活,得到遗忘门的值
- 输出门
**输入门(Input Gate)**决定要从当前时间步得到多少信息来更新细胞状态
同遗忘门得到输入门的 i t i_t it,相同的公式,但是权重不同,可以得到不同的值
同时,对输入的 x t , h t − 1 x_t,h_{t-1} xt,ht−1 计算得到细胞候选状态,通过输入门得到的保留程度,以此跟新细胞状态
- 细胞状态更新
C t = f t ∗ C t − 1 + i t ∗ C ~ t C_t = f_t * C_{t-1} + i_t * \tilde{C}_t Ct=ft∗Ct−1+it∗C~t
通过遗忘门和输入门得到对应的保存信息的权重$ f_t和i_t ,以及细胞候选状态 ,以及细胞候选状态 ,以及细胞候选状态\tilde{C}_t$,计算更新细胞状态
通过这样的细胞状态的更新传递,可以保留每个时间步的信息,也能记忆之前的时间步。每次的更新,得到不同的保留比例,从而实现长短期记忆
- 输出门
在特定的时间步的输出,同样的计算权重,而后使用当前细胞状态用tanh激活,乘上这个权重,得到当前时间步的输出,同时也是隐藏状态
总结
对比可知,所有的门控权重计算都相同,但是其输入的权重不同,所以可以保留不同的信息
注:这里最后的输出 h t h_t ht 应该是tanh而不是sigmoid
基于Numpy实现
import torch
import torch.nn as nn
import numpy as np
class LSTM(nn.Module):
def __init__(self,input_size,hidden_size,output_size):
super().__init__()
"""
:pram input_size 输入的词向量的维度
:param hidden_size 隐藏状态的维度
:pram output_size 输出的维度
"""
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
# 初始化权重
self.w_f = np.random.rand(hidden_size,input_size+hidden_size)
self.b_f = np.random.rand(hidden_size)
self.w_i = np.random.rand(hidden_size,input_size+hidden_size)
self.b_i = np.random.rand(hidden_size)
self.w_c = np.random.rand(hidden_size,input_size+hidden_size)
self.b_c = np.random.rand(hidden_size)
self.w_o = np.random.rand(hidden_size,input_size+hidden_size)
self.b_o = np.random.rand(hidden_size)
# 输出层
self.w_y = np.random.rand(output_size, hidden_size)
self.b_y = np.random.rand(output_size)
# 激活函数
def tanh(self,x):
return np.tanh(x)
def sigmoid(self,x):
return 1/(1+np.exp(-x))
def forward(self,x):
# 初始化隐藏状态
h_t = np.zeros((self.hidden_size,))
# cell
c_t = np.zeros((self.hidden_size,))
# 存储 时间步的隐藏和细胞状态
h_state = []
c_state = []
for t in range(x.shape[0]):
x_t = x[t] # 拿到当前时间步的输入
# 垂直方向凭借 列不变 增加行
x_t = np.concatenate([x_t,h_t]) # 拼接时间步和隐藏
# 遗忘门
f_t = self.sigmoid(np.dot(self.w_f,x_t)+self.b_f)
# 候选细胞状态
c_hat_t = self.tanh(np.dot(self.w_c,x_t)+self.b_c)
# 输入门
i_t = self.sigmoid(np.dot(self.w_i,x_t)+self.b_i)
# 细胞状态更新
c_t = f_t * c_t + i_t * c_hat_t
# 输出门
o_t = self.sigmoid(np.dot(self.w_o,x_t)+self.b_o)
# 更新隐藏状态
h_t = o_t * self.tanh(c_t)
# 保存隐藏状态和细胞状态
h_state.append(h_t)
c_state.append(c_t)
y_t = np.dot(self.w_y,h_t) + self.b_y
# 转化为张量 dim 行的维度
output = torch.softmax(torch.tensor(y_t),dim=0)
return np.array(h_state),np.array(c_state),output
# 数据输入
x = np.random.rand(3,2) # 三个词 词向量维度2
# 这里只是以输入一个数据为例,如果是多个批次,还需要对维度进行转换,以符合矩阵相乘
hidden_size = 5
# 实例化模型
mdoel = LSTM(2,hidden_size,6)
h_state,c_state,output = mdoel(x)
print('class:' ,output)
print('hidden:' ,h_state)
print('hidden size :' ,h_state.shape)
print('cell:' ,c_state)
print('cell size:' ,c_state.shape)
基于PyTorch API实现
import torch
import torch.nn as nn
class LSTM(nn.Module):
def __init__(self,input_size,hidden_size,output_size):
"""
:param input_size: 词向量
:param hidden_size: 隐藏层
:param output_size: 输出类别
"""
super().__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.lstm = nn.LSTM(input_size,hidden_size)
self.fc = nn.Linear(hidden_size,output_size)
def forward(self,x):
# 初始化隐藏状态和细胞状态
h0 = torch.zeros(1,x.size()[1],self.hidden_size)
c0 = torch.zeros(1,x.size()[1],self.hidden_size)
# 前向传播
out,state = self.lstm(x,(c0,h0))
# out所有时间步的输出 state最后一个时间步的状态 隐藏状态和细胞状态
out = self.fc(out[-1,:,:]) # 取最后一个时间步的输出
return out
# 定义参数
seq_size,batch_size,input_size = 5,4,2 # 5批次 4个词 向量维度2
hidden_size,output_size = 6,7
x = torch.randn(seq_size,batch_size,input_size )
model = LSTM(input_size,hidden_size,output_size)
out = model(x)
print(out)
print(out.shape)
序列池化
序列池化,把变长序列转换为固定长度的表示方法。如一个序列有多个词向量组成,每个向量的求和平均,合并为一个向量
主要有最大池化(突出序列特定的最大激活值)平均池化(保留序列总体的信息)注意力池化(注意力机制池化,对每个时间步分配权重)
import torch
import torch.nn as nn
input_data = torch.randn(2,3,4) # 批次 词数, 词向量维度
avg_pool = nn.AdaptiveAvgPool1d(1)
# max_pool = nn.AdaptiveMaxPool1d(1)
# 调整维度
input_data = input_data.permute(0,2,1) # b.s.d >> b.d.s #
output = avg_pool(input_data) # 转化为句向量
# output = max_pool(input_data) # 转化为句向量
print(output.shape) # torch.Size([2, 4, 1])