如果你不想使用 PyTorch 内置的 nn.RNN
模块,而是希望自己从头实现一个基础的 RNN 以更深入地理解其内部机制,你可以手动实现 RNN 的前向传播和反向传播。下面是一个详细的实现示例,包括中文注释说明。
手动实现基础 RNN
1. 基础 RNN 结构
在基础的 RNN 中,我们需要实现以下部分:
- 隐藏状态更新公式
- 输出计算公式
- 梯度计算和参数更新
2. RNN 的数学公式
-
隐藏状态更新 :
其中:
- ( h_t ):时间步 ( t ) 的隐藏状态
- ( x_t ):时间步 ( t ) 的输入
- ( W_{ih} ):输入到隐藏层的权重矩阵
- ( W_{hh} ):隐藏层到隐藏层的权重矩阵
- ( b_h ):隐藏层的偏置
- ( \tanh ):激活函数
2. 实现代码
以下是一个完整的手动实现 RNN 的代码示例:
python
import torch
import torch.nn.functional as F
class MyRNN(nn.Module):
def __init__(self, input_size, hidden_size, layers_size,first_batch=False):
super(MyRNN, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.layers_size = layers_size
self.first_batch = first_batch
self.w_in = []
self.w_hh = []
self.bh = []
for layers in range(self.layers_size):
self.w_in.append(torch.randn([hidden_size, input_size])*0.01)
self.w_hh.append(torch.randn([hidden_size, hidden_size])*0.01)
self.bh.append(torch.zeros(hidden_size))
input_size = hidden_size # 除了第一层,输入与隐层相等
self.w_in = nn.ParameterList([nn.Parameter(w) for w in self.w_in])
self.w_hh = nn.ParameterList([nn.Parameter(w) for w in self.w_hh])
self.bh = nn.ParameterList([nn.Parameter(b) for b in self.bh])
def forward(self, inputs, hidden=None):
if self.first_batch:
batch_size, seq_len, _ = inputs.size()
else:
seq_len, batch_size, _ = inputs.size()
inputs = inputs.transpose(0, 1)
if hidden is None:
hidden = [torch.zeros(batch_size, self.hidden_size) for _ in range(self.layers_size)]
outputs = []
print(inputs.shape)
for t in range(seq_len):
x = inputs[:, t, :]
for layer in range(self.layers_size):
h_pre = hidden[layer]
h_t = torch.tanh(torch.mm(x, self.w_in[layer].t()) + torch.mm(h_pre, self.w_hh[layer].t()) + self.bh[layer])
hidden[layer] = h_t
x = h_t
outputs.append(h_t.unsqueeze(1))
outputs = torch.cat(outputs, dim=1)
return outputs, hidden
4. 代码详解
- 初始化
python
def __init__(self, input_size, hidden_size, layers_size,first_batch=False):
super(MyRNN, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.layers_size = layers_size
self.first_batch = first_batch
self.w_in = []
self.w_hh = []
self.bh = []
for layers in range(self.layers_size):
self.w_in.append(torch.randn([hidden_size, input_size])*0.01)
self.w_hh.append(torch.randn([hidden_size, hidden_size])*0.01)
self.bh.append(torch.zeros(hidden_size))
input_size = hidden_size # 除了第一层,输入与隐层相等
self.w_in = nn.ParameterList([nn.Parameter(w) for w in self.w_in])
self.w_hh = nn.ParameterList([nn.Parameter(w) for w in self.w_hh])
self.bh = nn.ParameterList([nn.Parameter(b) for b in self.bh])
W_ih
:输入到隐藏层的权重矩阵W_hh
:隐藏层到隐藏层的权重矩阵bh
:隐藏层的偏置项
- 前向传播
python
def forward(self, inputs, hidden=None):
if self.first_batch:
batch_size, seq_len, _ = inputs.size()
else:
seq_len, batch_size, _ = inputs.size()
inputs = inputs.transpose(0, 1)
if hidden is None:
hidden = [torch.zeros(batch_size, self.hidden_size) for _ in range(self.layers_size)]
outputs = []
for t in range(seq_len):
x = inputs[:, t, :]
for layer in range(self.layers_size):
h_pre = hidden[layer]
h_t = torch.tanh(torch.mm(x, self.w_in[layer].t()) + torch.mm(h_pre, self.w_hh[layer].t()) + self.bh[layer])
hidden[layer] = h_t
x = h_t
outputs.append(h_t.unsqueeze(1))
outputs = torch.cat(outputs, dim=1)
if self.first_batch:
# 保证输入与输出一致
outputs = outputs.transpose(0, 1)
return outputs, hidden
x
:当前时间步的输入hidden
:更新后的隐藏状态torch.tanh
:激活函数
3. 示例运行
python
# 示例:定义参数并运行模型
input_size = 3
hidden_size = 8
num_layers = 1
batch_size = 5
seq_len = 4
# 初始化自定义RNN
model = MyRNN(input_size, hidden_size, num_layers, first_batch=False)
# 生成随机输入
# input_seq = torch.randn(batch_size, seq_len, input_size)
input_seq = torch.randn(seq_len, batch_size, input_size)
# 执行前向传播
output, hidden = model(input_seq)
print(f"输入的数据形状: {input_seq.shape}")
print("输出维度:", output.shape)
print("输出:", output.shape)
运行结果:
python
输入的数据形状: torch.Size([4, 5, 3])
输出维度: torch.Size([5, 4, 8])
输出: torch.Size([5, 4, 8])
4. 对RNN网络结构问题的思考
-
参数的初始化为什么要乘以 0.01?
乘以 0.01 是一种简化的初始化策略,主要目的是让权重矩阵的初始值较小,避免梯度爆炸或梯度消失等问题。一方面,在深度网络,随着层数的加深,梯度可能会在反向传播时不断增大,导致梯度爆炸,使得模型训练不稳定。将初始权重设置得较小(例如乘以 0.01)可以在一定程度上减缓这种现象。乘以 0.01 也是一种实现的简化方式,避免了复杂的权重初始化方法。
-
在前向传播中,外层循环是对输入序列长度,内层循环是对网络层数,那么为什么不能交换呢?
seq_len
表示输入序列的时间步长度。在每个时间步上,RNN
的计算是依赖于前一个时间步的隐藏状态。为了让时间步之间的数据流通,逐个时间步进行计算,就保证当前时间步的输入和前一个时间步的隐藏状态能够正常传递。这种设计是符合RNN
的时间序列计算机制的。RNN 的这种时间步间依赖性是其核心特点,必须按时间顺序处理。
-
2.1 时间依赖 :每个时间步的输出依赖于前一个时间步的隐藏状态。因此必须先按时间顺序处理每个时间步,这样才能确保前一个时间步的隐藏状态正确地传递给下一个时间步。如果先循环层数,再循环时间步,时间步之间的依赖被破坏(具体而言就是: 每一层的
h_pre
只是在当前层的时间步中起作用,但在跨层的时候,没有正确的隐藏状态传递给下一时间步。也就是说,在循环第一个时间步时,已经丢失了前一个时间步的隐藏状态。) -
2.2 层的串联 :
在每个时间步内,所有层应该是串联关系,但你是在整个时间序列结束后再计算下一个层,这样就会导致第二层无法获得第一层的输出,因为第一层的计算还没有完成。在每个时间步内,必须依次通过所有层的计算,将输出逐层传递下去。
时间步之间存在时序上的依赖关系 ,当前时间步的计算需要使用前一个时间步的隐藏状态。因此,时间步的顺序在计算中非常重要。而层数之间是空间上的依赖,每一层的输出作为下一层的输入,但它不涉及到前后时间步的状态传递。
-
3 代码中为什么要使用
unsqueeze
扩展outputs
的第一个维度?
RNN
模型的输出一般是形状为(seq_len, batch_size, hidden_size)
的三维张量:seq_len
表示序列的长度,即时间步数;batch_size
表示批处理的大小;hidden_size
表示每个时间步的隐藏状态的维度
注意:first_batch=True时候,输入输出是[Batch_size, seq_len,...]
若first_batch=False时候,输入输出是[seq_len, Batch_size, ...]
当我们循环遍历时间步时,每个时间步的隐藏状态 h_t
都是一个二维的张量 (batch_size, hidden_size)
,如果直接将它添加到 outputs
列表中,会造成最后合并时的维度不匹配。unsqueeze(1)
这个操作的作用就是在第 1 维(即时间步维度的位置)上插入一个新的维度,使 h_t
的形状从 (batch_size, hidden_size)
变成 (batch_size, 1, hidden_size)
。这样做之后,多个时间步的隐藏状态拼接在一起后,才能形成一个完整的三维张量,符合 RNN
的输出格式。
- 4 RNN中的数学公式可以是多样的,除了我实现的这种,还有别的
在同一个权重矩阵W
中,将输入𝑥
和前一隐藏状态ℎ
组合在一起进行处理。这种表达方式的背后逻辑是,将输入和前一隐藏状态拼接成一个向量,然后对它们应用同一个权重矩阵。
5. 关于一些参数的学习
num_layers
参数
num_layers
参数在 RNN 中的作用是控制模型的层数,也就是说,num_layers
决定了有多少个 RNN 层堆叠在一起。每一层 RNN 都会处理前一层的输出,形成更深层次的特征抽取。
具体说明
5-1. num_layers
的作用
- 控制层数 :
num_layers
设定了 RNN 堆叠的层数。例如,num_layers=1
表示模型只有一层 RNN,num_layers=2
表示模型有两层 RNN,依此类推。 - 多层 RNN 的优势:多层 RNN 可以更好地捕捉复杂的时间序列特征和长距离依赖。每一层 RNN 会从上一层的输出中提取更高级的特征。
5-2. 如何理解
-
单层 RNN :
如果
num_layers=1
,RNN 只包含一层处理时间步数据的神经网络。每个时间步的输入会直接传递到这一层,隐藏状态也直接从这一层输出。plaintext输入序列 -> RNN层 -> 输出
-
多层 RNN :
如果
num_layers=2
,RNN 包含两层堆叠的神经网络。第一层的输出会作为第二层的输入进行处理。这种层次结构允许网络捕捉更复杂的特征。plaintext输入序列 -> RNN层1 -> RNN层2 -> 输出
5. 总结
hidden_size
是隐藏层的维度,控制每个时间步的隐藏状态的大小。num_layers
代表 RNN 的层数,每层都可以捕捉不同层次的特征。num_layers
参数在 RNN 中用来指定模型的层数。- 更高的
num_layers
可以使模型更深,从而有潜力捕捉更复杂的特征,但也可能增加计算开销和训练难度。
- 更高的