循环神经网络学习
RNN训练方法–BPTT
BPTT (Backpropagation Through Time),这是一种用于训练循环神经网络(RNNs)的算法。由于 RNNs 能够处理序列数据,并且在每个时间步上都有内部状态,因此需要一种特殊的方法来计算梯度并更新权重。BPTT 就是这样一种方法,它扩展了标准的反向传播算法,以适应时间序列上的依赖关系。
BPTT 的基本原理
- 前向传播:首先,输入序列通过网络进行前向传播。对于每个时间步,RNN 会根据当前的输入和前一时间步的状态计算出新的状态,并可能产生输出。
- 损失计算:一旦整个序列被处理完毕,就可以计算总损失。这个损失通常是基于模型在所有时间步上的预测与实际目标之间的差异。
- 反向传播:然后,从最后一个时间步开始,计算损失关于每个时间步上的权重的梯度,并将这些梯度回传到前面的时间步。这个过程可以看作是在时间维度上展开 RNN,然后像普通前馈神经网络一样应用反向传播。
- 权重更新:最后,使用累积的梯度信息来更新网络中的权重,以便减少下一次迭代时的损失。
RNN存在的问题
- 梯度消失/爆炸:
- 在训练过程中,当通过时间反向传播误差时,由于链式法则的应用,梯度会随着时间步长的增加而被反复乘以权重矩阵中的值。如果这些值小于1,则梯度会趋向于0(梯度消失),导致远离当前时间步的信息无法对更新产生影响;如果这些值大于1,则梯度会趋向于无穷大(梯度爆炸),可能导致数值不稳定。
- 解决方案:使用梯度裁剪来防止梯度爆炸,以及引入更复杂的结构如LSTM或GRU来缓解梯度消失问题。
- 长期依赖问题:
- 由于梯度消失,RNN很难学习到远距离的时间依赖关系。也就是说,对于较早时间点的信息,RNN可能无法有效地将其与当前时间点的信息联系起来。
- 解决方案:LSTM和GRU等架构通过引入记忆单元和门控机制来更好地捕捉长期依赖性。
- 计算效率低
- 内存消耗
- 难以训练:
- 需要采用适当的初始化方法(如Xavier/Glorot初始化)、正则化技术(如Dropout)和优化算法(如Adam)可以帮助改善训练过程。
- 固定的上下文窗口
LSTM
长短期记忆网络(Long Short-Term Memory,LSTM)是一种特别设计来解决长期依赖问题的循环神经网络(RNN)架构。
LSTM的核心思想是引入了称为**“细胞状态”**(cell state)的概念,该状态可以在时间步长中被动态地添加或删除信息。能够有效地记住信息并控制何时让信息通过或忘记。
LSTM的结构:
门:
LSTM有通过精心设计的称作“门”的结构来去除或者增加信息到细胞状态的能力。
门是一种让信息选择式通过的方法。他们包含一个sigmoid神经网络层和一个pointwise乘法操作。
Sigmoid层输出0到1之间的数值,描述每个部分有多少量可以通过。
0代表“不许任何量通过”
1代表“允许任何量通过”
LSTM 拥有三个门,来保护和控制细胞状态。
门控机制
LSTM 单元由几个部分组成,主要包括:
- 输入门 (Input Gate):决定多少新输入的信息会被存储到单元状态中。
- 遗忘门 (Forget Gate):决定哪些信息应该从单元状态中被丢弃。
- 输出门 (Output Gate):基于当前单元状态和输入信息,决定输出什么值。
- 单元状态 (Cell State):这是LSTM的核心,它是一个贯穿整个序列的信息传递通道,允许信息在整个序列中流动而不受太多干扰。
工作原理
-
遗忘门:首先,LSTM会决定要忘记哪些信息。这一步通过一个sigmoid层完成,该层接收上一时间步的隐藏状态 ht−1 和当前输入 xt,然后输出一个介于0和1之间的数字向量。这个向量中的每个元素表示对应位置上的信息被保留的程度(1表示完全保留,0表示完全忘记)。
-
输入门:接下来,LSTM需要确定新的信息如何加入到单元状态中。这分为两个步骤:
- 一个新的候选值向量 C~t 通过tanh层生成。
- 一个sigmoid层(称为输入门)决定了这些候选值中的多少将被添加到单元状态中。
-
更新单元状态:旧的单元状态 Ct−1 乘以遗忘门的输出,然后加上输入门的结果(经过tanh激活的新候选值与输入门输出的逐元素相乘)。这样就得到了更新后的单元状态 Ct。
-
输出门:最后,LSTM需要确定要输出什么。这同样分为两步:
- 一个sigmoid层决定单元状态的哪一部分将被输出。
- 单元状态经过tanh层处理(将值缩放到-1到1之间),然后与输出门的结果进行逐元素相乘,得到最终的隐藏状态 ht。
W f , W i , W o , W c 是权重矩阵 , b f , b i , b 0 , b c 是偏置 σ 表示 s i g m o i d 激活函数, ∗ 表示逐元素乘法 遗忘门: f t = σ ( W f [ h t − 1 , x t ] + b f ) 输出门 : i t = σ ( W i [ h t − 1 , x t ] + b i ) 新候选值 : C ~ t = tanh ( W c [ h t − 1 , x t ] + b c ) 更新单元状态 : C t = f t ∗ C t − 1 + i t ∗ C ~ t 输出门 : o t = σ ( W o [ h t − 1 , x t ] + b o ) 最终隐藏状态 : h y = o t ∗ tanh ( C t ) \\W_f,W_i,W_o,W_c是权重矩阵,b_f,b_i,b_0,b_c是偏置 \\ \sigma表示sigmoid激活函数, *表示逐元素乘法 \\遗忘门:f_t = \sigma (W_f[h_{t-1},x_t]+b_f) \\输出门:i_t = \sigma(W_i[h_{t-1},x_t]+b_i) \\新候选值:\tilde{C}_t = \tanh(W_c[h_t-1,x_t]+b_c) \\更新单元状态:C_t = f_t*C_{t-1}+i_t*\tilde{C}_t \\输出门:o_t = \sigma(W_o[h_{t-1},x_t]+b_o) \\最终隐藏状态:h_y = o_t*\tanh(C_t) Wf,Wi,Wo,Wc是权重矩阵,bf,bi,b0,bc是偏置σ表示sigmoid激活函数,∗表示逐元素乘法遗忘门:ft=σ(Wf[ht−1,xt]+bf)输出门:it=σ(Wi[ht−1,xt]+bi)新候选值:C~t=tanh(Wc[ht−1,xt]+bc)更新单元状态:Ct=ft∗Ct−1+it∗C~t输出门:ot=σ(Wo[ht−1,xt]+bo)最终隐藏状态:hy=ot∗tanh(Ct)
代码实现
原生代码
import numpy as np
import torch
class LSTM:
def __init__(self, input_size, hidden_size, output_size):
# 参数:词向量大小,隐藏层大小, 输出类别
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
# 初始化权重,偏置,把结构的W,U拼接在一起
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,))
# 初始化细胞状态
c_t = np.zeros((self.hidden_size,))
h_states = [] # 存储每一个时间步的隐藏状态
c_states = [] # 存储每一个时间步的细胞状态
for t in range(x.shape[0]):
x_t = x[t] # 获取当前时间步的输入(一个词向量)
# 将x_t和h_t进行垂直方向拼接
x_t = np.concatenate([x_t, h_t])
# 遗忘门 "dot"迷茫中,这里是点积的效果,(5,7)点积(7,)得到的是(5,)
f_t = self.sigmoid(np.dot(self.W_f, x_t) + self.b_f)
# 输出门
i_t = self.sigmoid(np.dot(self.W_i, x_t) + self.b_i)
# 候选细胞状态
c_hat_t = self.tanh(np.dot(self.W_c, x_t) + self.b_c)
# 更新细胞状态, "*"对应位置直接相乘
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_states.append(h_t)
c_states.append(c_t)
# 输出层,分类类别
y_t = np.dot(self.W_y, h_t) + self.b_y
output = torch.softmax(torch.tensor(y_t), dim=0)
return np.array(h_states), np.array(c_states), output
# 数据输入
x = np.random.rand(3, 2)
hidden_size = 5
# 实例化模型
lstm = LSTM(2, hidden_size, 6)
h_states, c_states, output = lstm.forward(x)
print("h_states:", h_states)
print("h_states_shape:", h_states.shape)
print("c_states:", c_states)
print("c_states_shape:", c_states.shape)
print("output:", output)
print("output_shape:", output.shape)
基于Pytorch API的代码实现
import torch
import torch.nn as nn
# 定义模型
class LSTMModel(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(LSTMModel, self).__init__()
self.hidden_size = hidden_size
self.input_size = input_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最后时间步的隐藏状态和细胞状态
out, state = self.lstm(x, (h0, c0))
out = out[-1] # 取最后一个时间步的输出
output = self.fc(out)
return output
# 模型参数
seq_size, batch_size, input_size = 5, 4, 3
hidden_size, output_size = 6, 7
model = LSTMModel(input_size, hidden_size, output_size)
# 模拟数据
x = torch.randn(seq_size, batch_size, input_size)
output = model(x)
print(output)
print(output.shape)
序列池化(平均池化和最大池化)
import torch
import torch.nn as nn
# 平均池化
# 输入数据
input_data = torch.randn(2, 3, 4)
# 调用平均池化
avg_pool = nn.AdaptiveAvgPool1d(1)
# 调整形状去匹配池化的输入
input_data = input_data.permute(0, 2, 1) # (batch,seq,dim)->(batch,dim,seq)
output = avg_pool(input_data)
print(output)
print(output.shape)
# 最大池化
# 输入数据
input_data = torch.randn(2, 3, 4)
# 调用平均池化
max_pool = nn.AdaptiveMaxPool1d(1)
# 调整形状去匹配池化的输入
input_data = input_data.permute(0, 2, 1) # (batch,seq,dim)->(batch,dim,seq)
output = max_pool(input_data)
print(output)
print(output.shape)