深度学习作业十一 LSTM
习题6-4 推导LSTM网络中参数的梯度, 并分析其避免梯度消失的效果
LSTM的结构如上。在证明LSTM可以避免梯度消失时,大部分资料都是用证明的,其实从结构图中不难看出,不仅与遗忘门和输入门有关,同时还影响着外部状态,所以用证明再好不过了。学姐的博客(NNDL 作业十一 LSTM-CSDN博客)中也证明了为什么可以用证明,我也跟着证了一下。
以上图为例,在T=3时,对其中一个参数求偏导。
所以我们可以看到存在于各个路径之中。若的梯度可以趋于1,就可以避免梯度消失。
那么他的梯度是如何趋于1的呢?
神经网络在参数学习过程中可以通过自己控制这些参数相互配合来让趋近于1,当有多个进行连乘时,那么这条路径上的梯度可能不会消失(但是其他的路径依然可能会发生梯度消失)。
那是如何配合的呢?
LSTM的三个门(输入门、遗忘门、输出门)为每个时间步提供了控制信号,决定了哪些信息需要被保留,哪些信息需要被遗忘,这种机制使得信息可以有效地流动,而不会因为过多的层数或者时间步数而导致梯度消失。
- 遗忘门控制着历史信息的遗忘程度。通过控制的值,LSTM能够防止不必要的梯度消失,因为当 ftf_tft 接近1时,梯度可以较好地在时间上流动。
- 输入门允许新信息进入细胞状态,从而帮助模型捕捉新的有价值的信息,而不会因梯度衰减导致模型无法学习新特征。
- 输出门控制着输出的内容,通过控制信息流的输出,可以避免信息的过度消失。
所以,总结一下,LSTM避免梯度消失的原因:
- 细胞状态的线性传递:细胞状态几乎没有通过非线性激活函数,因此梯度在反向传播时可以稳定地传递,不会迅速衰减。
- 门控机制的控制:遗忘门、输入门和输出门通过选择性地遗忘和更新信息,避免了梯度因为不必要的更新而消失。
- 长程依赖的保持:细胞状态的“记忆”能力使得LSTM能够在较长的时间跨度内保持有效的梯度流动,从而解决了传统RNN中的梯度消失问题。
习题6-3P 编程实现下图LSTM运行过程
1. 使用Numpy实现LSTM算子
代码如下:
# ====使用Numpy实现LSTM算子=================================================
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))
# 权重
input_weight = np.array([1, 0, 0, 0])
inputgate_weight = np.array([0, 100, 0, -10])
forgetgate_weight = np.array([0, 100, 0, 10])
outputgate_weight = 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 = tanh(np.matmul(input_weight, x)) # 候选状态
i_t = np.round(sigmoid(np.matmul(inputgate_weight, x))) # 输入门
after_inputgate = g_t * i_t # 候选状态经过输入门
f_t = np.round(sigmoid(np.matmul(forgetgate_weight, x))) # 遗忘门
after_forgetgate = f_t * c_t # 内部状态经过遗忘门
c_t = np.add(after_inputgate, after_forgetgate) # 新的内部状态
o_t = np.round(sigmoid(np.matmul(outputgate_weight, x))) # 输出门
after_outputgate = o_t * tanh(c_t) # 激活后新的内部状态经过输出门
y.append(round(after_outputgate, 2)) # 输出
print('输出:', y)
运行结果:
2. 使用nn.LSTMCell实现
nn.LSTMCell
是一个 单时间步 的 LSTM 单元,通常用于手动循环计算单个时间步的输出。它接受当前时间步的输入以及前一时间步的隐藏状态和细胞状态作为输入,并返回当前时间步的隐藏状态和细胞状态。也就是说,nn.LSTMCell
不会自动处理序列的时间步,需要用户在循环中手动传递每个时间步的输入。
nn.LSTMCell(input_size, hidden_size)
-
input_size
(int
):- 输入张量的特征维度。即每个时间步输入数据的大小,形状为
(batch_size, input_size)
。 - 例如,如果输入是一个有 10 个特征的向量,则
input_size
为 10。
- 输入张量的特征维度。即每个时间步输入数据的大小,形状为
-
hidden_size
(int
):- LSTM 单元的隐藏状态和细胞状态的维度。即每个时间步的输出的维度。
- 这是 LSTM 的 "容量" 或 "记忆能力",通常也决定了网络中隐藏状态的维度。
- 例如,如果
hidden_size
为 20,那么每个时间步的隐藏状态和细胞状态的维度就是 20。
代码如下:
# ====使用nn.LSTMCell实现======================================
import torch
import torch.nn as nn
# 实例化
input_size = 4
hidden_size = 1
cell = nn.LSTMCell(input_size=input_size, hidden_size=hidden_size)
# 修改模型参数 weight_ih.shape=(4*hidden_size, input_size),weight_hh.shape=(4*hidden_size, hidden_size),
# weight_ih、weight_hh分别为输入x、隐层h分别与输入门、遗忘门、候选、输出门的权重
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)
print('cell.weight_ih.shape:', cell.weight_ih.shape)
print('cell.weight_hh.shape', cell.weight_hh.shape)
# 初始化h_0,c_0
h_t = torch.zeros(1, 1)
c_t = torch.zeros(1, 1)
# 模型输入input_0.shape=(batch,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.shape=(seq_len,batch,input_size)
input = torch.transpose(input_0, 1, 0)
print('input.shape:', input.shape)
output = []
# 调用
for x in input:
h_t, c_t = cell(x, (h_t, c_t))
output.append(np.around(h_t.item(), decimals=3)) # 保留3位小数
print('output:', output)
运行结果:
3. 使用nn.LSTM实现
nn.LSTM
是一个 多时间步 的 LSTM 层,能够自动处理输入序列并在序列中传播信息。它内部实现了时间步的循环,用户只需要一次性传入整个序列的输入,nn.LSTM
会自动执行所有时间步的计算,并返回整个序列的隐藏状态和细胞状态。适用于处理序列数据,尤其是在大多数深度学习任务中,直接使用 nn.LSTM
进行序列建模。
与nn.LSTMcell对比一下:
nn.LSTM(input_size, hidden_size, num_layers=1, bias=True, batch_first=False, dropout=0, bidirectional=False)
-
input_size
(int
):- 输入数据的特征维度,表示每个时间步输入数据的大小,形状为
(batch_size, seq_len, input_size)
或(seq_len, batch_size, input_size)
(具体取决于batch_first
参数)。 - 例如,如果输入是一个包含 10 个特征的向量,则
input_size
为 10。
- 输入数据的特征维度,表示每个时间步输入数据的大小,形状为
-
hidden_size
(int
):- 隐藏状态的维度,即每个 LSTM 单元中隐层的大小。决定了网络的记忆容量,也决定了每个时间步的输出大小。
- 例如,
hidden_size=20
时,每个时间步的输出大小是 20。
-
num_layers
(int
, 默认值=1):- LSTM 层的层数。通常,
num_layers=1
表示只有一层 LSTM 单元。如果num_layers > 1
,则 LSTM 将堆叠多个 LSTM 层(每层 LSTM 都会接收上一层的输出作为输入)。 - 例如,
num_layers=2
表示有 2 层 LSTM,第二层的输入是第一层的输出。
- LSTM 层的层数。通常,
-
bias
(bool
, 默认值=True):- 是否在 LSTM 层中使用偏置。大多数情况下,偏置是必要的,但也可以禁用它,具体取决于任务需求。
True
表示使用偏置,False
表示不使用偏置。
-
batch_first
(bool
, 默认值=False):- 控制输入和输出张量的形状。如果设置为
True
,则输入和输出张量的形状为(batch_size, seq_len, input_size)
和(batch_size, seq_len, hidden_size)
;如果设置为False
(默认值),则形状为(seq_len, batch_size, input_size)
和(seq_len, batch_size, hidden_size)
。 True
会使输入和输出张量的第一维是batch_size
,False
会使第一维是seq_len
。
- 控制输入和输出张量的形状。如果设置为
-
dropout
(float
, 默认值=0):- 在 LSTM 层之间应用的 dropout 概率。当
num_layers > 1
时,每一层之间都会应用这个 dropout。该值在 [0, 1] 之间。 dropout=0
表示不应用 dropout,dropout=0.5
表示有 50% 的概率丢弃连接。
- 在 LSTM 层之间应用的 dropout 概率。当
-
bidirectional
(bool
, 默认值=False):- 是否使用双向 LSTM。如果设置为
True
,LSTM 会同时在正向和反向两个方向上处理序列数据,从而能够捕捉序列的前后文信息。双向 LSTM 会将正向和反向的隐藏状态连接起来,输出的hidden_size
会是原来的一倍。 True
表示使用双向 LSTM,False
表示使用单向 LSTM。
- 是否使用双向 LSTM。如果设置为
代码如下:
# ====使用nn.LSTM实现=========================================
# LSTM
# 实例化
input_size = 4
hidden_size = 1
lstm = 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)
# 模型输入input.shape=(batch,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)
# 初始化h_0,c_0
h_t = torch.zeros(1, 1, 1)
c_t = torch.zeros(1, 1, 1)
# 调用
output, (h_t, c_t) = lstm(input, (h_t, c_t))
rounded_output = torch.round(output * 1000) / 1000 # 保留3位小数
print(rounded_output)
运行结果:
从三种实现的方式来看,运行出的结果基本一致。
参考:
LSTM参数梯度推导与实现:对抗梯度消失,
NNDL 作业十一 LSTM-CSDN博客
这次的分享就到这里,下次再见~