习题6-4 推导LSTM网络中参数的梯度, 并分析其避免梯度消失的效果
长短期记忆网络(LSTM)是一种特殊的递归神经网络(RNN),它被广泛用于解决一些涉及到序列或时间序列预测的问题。LSTM的设计引入了一些特殊的结构,使得它能够更好地处理长期依赖问题,其中一个关键的结构就是“门”。
在LSTM中,门是用来控制信息流动的。具体来说,LSTM包含三个门:输入门、遗忘门和输出门。每个门都由一个或多个线性层和一个激活函数组成。这四个参数(三个门的权重和偏差)的梯度可以通过反向传播算法推导出来。
首先,我们假设LSTM的损失函数为L,那么参数的梯度可以通过对损失函数求偏导数得到。
对于输入门,我们有:
- i_t = sigmoid(W_xi * x_t + W_hi * h_{t-1} + b_i)
- g_t = tanh(W_xc * x_t + W_hc * h_{t-1} + b_c)
- c_t = i_t * g_t + c_{t-1} * f_t
- h_t = o_t * tanh(c_t)
其中,W_xi, W_hi, W_xc, W_hc, b_i, b_c 是参数,i_t, f_t, g_t, c_t, o_t, h_t 是中间变量。
那么对于损失函数L,对每个参数的梯度可以有:
- d(L)/d(W_xi) = (d(L)/d(i_t)) * (d(i_t)/d(W_xi))
- d(L)/d(W_hi) = (d(L)/d(i_t)) * (d(i_t)/d(W_hi)) + (d(L)/d(f_t)) * (d(f_t)/d(W_hi))
- d(L)/d(b_i) = (d(L)/d(i_t)) * (d(i_t)/d(b_i))
- ...类似地可以计算其他参数的梯度。
在训练过程中,使用梯度下降算法对参数进行更新,从而最小化损失函数。
至于LSTM避免梯度消失的效果,这主要是由于其特殊的结构设计。在普通的RNN中,随着时间的推移,信息的“足迹”会逐渐消失,导致难以捕捉到早期的信息。而LSTM通过引入门控机制和记忆单元,能够有效地避免这一问题。
在LSTM中,遗忘门(f_t)负责控制上一时刻的信息有多少应该保留到当前时刻,而输入门(i_t)和输出门(o_t)则负责控制当前时刻的新信息的流入和记忆单元的状态如何影响输出。这种机制使得LSTM能够更好地捕捉长期依赖的信息,从而避免了梯度消失的问题。
习题6-3P 编程实现LSTM运行过程
实现LSTM算子,可参考实验教材代码。
1. 使用Numpy实现LSTM算子
import numpy as np
# 输入数据
x = 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]])
# 输入门控权重
inputGata_W = np.array([0, 100, 0, -10])
# 输出门控权重
outputGata_W = np.array([0, 0, 100, -10])
# 遗忘门控权重
forgetGata_W = np.array([0, 100, 0, 10])
# 控制权重
c_W = np.array([1, 0, 0, 0])
# sigmoid函数
def sigmoid(x):
y = 1 / (1 + np.exp(-x)) # sigmoid函数计算
if y >= 0.5: # 如果y值大于等于0.5,返回1,否则返回0
return 1
else:
return 0
# 初始化变量
temp = 0
y = []
memory = []
# 对输入数据进行处理
for input in x:
memory.append(temp) # 将当前记忆值添加到memory列表中
temp_c = np.sum(np.multiply(input, c_W)) # 控制信号的计算
temp_input = sigmoid(np.sum(np.multiply(input, inputGata_W))) # 输入门控的sigmoid函数计算
temp_forget = sigmoid(np.sum(np.multiply(input, forgetGata_W))) # 遗忘门控的sigmoid函数计算
temp_output = sigmoid(np.sum(np.multiply(input, outputGata_W))) # 输出门控的sigmoid函数计算
temp = temp_c * temp_input + temp_forget * temp # 更新记忆值
y.append(temp_output * temp) # 将输出添加到y列表中
# 打印结果
print("memory:", memory)
print("y:", y)
2. 使用nn.LSTMCell实现
import torch
import torch.nn as nn
# 定义一个张量x,表示一系列输入数据,每个输入是一个4维向量
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]], dtype=torch.float)
# 在第二个维度上增加一个维度,以便与LSTM的输入匹配
x = x.unsqueeze(1)
# LSTM的输入大小和隐藏层大小
input_size = 4
hidden_size = 1
# 定义一个LSTM单元
lstm_cell = nn.LSTMCell(input_size, hidden_size, bias=False)
# 为LSTM单元设置权重值,注意这里使用的是硬编码的值,可能不适用于所有场景
lstm_cell.weight_ih.data = torch.tensor([[0, 100, 0, 10], # forget gate
[0, 100, 0, -10], # input gate
[1, 0, 0, 0], # output gate
[0, 0, 100, -10]], dtype=torch.float)
# 设置隐层到隐层的权重为全零矩阵
lstm_cell.weight_hh.data = torch.zeros([4 * hidden_size, hidden_size])
# 初始化隐层和细胞状态为全零向量
hx = torch.zeros(1, hidden_size)
cx = torch.zeros(1, hidden_size)
# 存储输出的列表
outputs = []
# 通过LSTM单元处理每个输入数据并收集输出
for i in range(len(x)):
hx, cx = lstm_cell(x[i], (hx, cx))
outputs.append(hx.detach().numpy()[0][0])
# 将输出列表中的值四舍五入并打印结果
outputs_rounded = [round(x) for x in outputs]
print(outputs_rounded)
3. 使用nn.LSTM实现
# 导入PyTorch库
import torch
# 导入PyTorch的nn模块,该模块包含了许多神经网络相关的类和函数
import torch.nn as nn
# 定义一个输入数据x,维度为(sequence_length, batch_size, input_size),这里的sequence_length=9, batch_size=1, input_size=4
# sequence_length表示序列长度,这里表示我们的输入数据有9个时间步
# batch_size表示批量大小,这里表示我们的输入数据是一个样本
# input_size表示输入的维度,这里表示每个时间步的输入特征维度为4
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]], dtype=torch.float)
# 使用unsqueeze函数给输入数据x增加一个时间步维度,使其维度变成(sequence_length, batch_size, input_size),这样才能被LSTM模型接受
x = x.unsqueeze(1)
# 定义一个LSTM模型,输入大小为input_size,隐藏层大小为hidden_size,bias设置为False表示不使用偏置项
# LSTM模型的参数:输入大小、隐藏层大小、bias(是否使用偏置项)等都可以根据实际需求进行调整
lstm = nn.LSTM(input_size=input_size, hidden_size=hidden_size, bias=False)
# 为LSTM模型的权重矩阵设置特定的值,这里的值是随意设置的,通常我们不会这样手动设置权重,而是让PyTorch自动为我们初始化权重
# weight_ih_l0表示第0个LSTM层中的输入到门(input gate)的权重矩阵,一共有4个门(forget gate、input gate、output gate、cell gate),所以权重矩阵的大小为4*hidden_size*input_size
# weight_hh_l0表示第0个LSTM层中的隐藏状态到隐藏状态的权重矩阵,大小为hidden_size*hidden_size
lstm.weight_ih_l0.data = torch.tensor([[0, 100, 0, 10], # forget gate
[0, 100, 0, -10], # input gate
[1, 0, 0, 0], # output gate
[0, 0, 100, -10]]).float() # cell gate
lstm.weight_hh_l0.data = torch.zeros([4 * hidden_size, hidden_size])
# 初始化隐藏状态和记忆状态,因为我们是第一次运行前向传播,所以这两个状态都初始化为零值
# hx表示隐藏状态,cx表示记忆状态,它们的维度都是(batch_size, hidden_size),因为我们的batch_size是1,所以这里直接用torch.zeros创建了大小为(1, hidden_size)的全零张量
hx = torch.zeros(1, 1, hidden_size)
cx = torch.zeros(1, 1, hidden_size)
# 使用LSTM模型进行前向传播,输入数据x和初始的隐藏状态和记忆状态(hx, cx),返回输出结果outputs和新的隐藏状态和记忆状态(hx_, cx_)
outputs, (hx_, cx_) = lstm(x, (hx, cx))
# 将输出结果outputs的维度从(sequence_length, batch_size, hidden_size)变为(sequence_length*batch_size, hidden_size)
# 然后使用tolist()函数将输出结果转换为列表类型输出结果,方便后续处理和展示输出结果
outputs = outputs.squeeze().tolist() # squeeze函数可以将维度为1的维度去掉,比如将形状为(9,)的张量变为形状为()的标量,因为我们的sequence length只有一个,所以使用squeeze可以将其去掉
# 对输出结果进行四舍五入处理,使其更接近真实值,方便展示输出结果
outputs_rounded = [round