3 minute read

最近在看李沐老师的《动手学深度学习》,这是一本很好的深度学习入门教材,对于基础的神经网络知识的讲解相当到位。尤其是其中介绍循环神经网络的地方,老师的讲解和代码都很详细了。但是作为学生,在学习的过程中还是难免有不懂的地方,所以我想写这一篇博客来对这一章的内容进行一次小小的总结。

mild days — 羊文学

循环神经网络原理

循环神经网络是一种处理时序数据的方法,在 8.1 节中提到过两种方式:一是将前面一段时刻的数据全部作为输入,送给模型得到输出;二是定义一个隐状态,该隐状态汇集了前一段时间的信息,然后结合第 t 时刻的数据作为输入得到输出。前者为自回归模型(AR),后者为隐变量自回归模型(LVAR),显然循环神经网络属于后者。

然而似乎 RNN 并不严格属于隐变量自回归模型()因为 RNN 的隐变量是确定的,一般的隐变量自回归模型都是需要从一个特定的分布中进行采样、积分或变分近似获得隐状态的。

  • AR vs LVAR
比较维度 AR(AutoRegressive)模型 LVAR(Latent Variable AutoRegressive)模型
模型类型 显变量模型(观测变量自回归) 隐变量模型(观测变量由隐变量驱动)
建模对象 直接建模观测变量的时间依赖关系 建模潜在(隐含)因子的时间依赖及其对观测变量的生成
结构表达 \(x_t = \sum_{i=1}^p \phi_i x_{t-i} + \epsilon_t\) \(z_t = \sum_{i=1}^p A_i z_{t-i} + \eta_t\),\(x_t = C z_t + \epsilon_t\)
是否引入隐变量 ❌ 否 ✅ 是(引入不可观测的潜变量 $z_t$)
可建模关系 只能建模观测变量自身的动态 可以建模观测变量间的共变结构与驱动因子动态
参数数量 少,主要为 AR 系数 较多,包含潜变量转移矩阵、投影矩阵等
表达能力 较弱,适合单变量时间序列建模 强大,适合高维时间序列、存在共因子的数据建模
估计方法 OLS、Yule-Walker EM 算法、变分推断、贝叶斯方法等
计算复杂度 高,需对隐变量进行估计或推断
应用场景 单变量或少量观测变量的序列预测 经济学、金融等高维数据建模(如因子模型、脑信号建模等)
可解释性 强,直接解释观测变量之间的因果关系 较弱,依赖隐变量建模假设

关于 RNN 详细的原理就不多说了,还是比较简单的,可以直接参见李沐的教程

代码

写代码的时候,踩了不少的坑,在这里就做个总结吧

由于 RNN 具有状态(state),这造成了其结构和一般的神经网络还是有挺大区别的。

torch.nn.RNN 是 PyTorch 提供的一个用于循环神经网络(Recurrent Neural Network, RNN)的模块。它实现了最基本的 RNN 单元(非 LSTM 或 GRU),用于处理序列数据。

下面我们详细说明 torch.nn.RNN 的前向传播(forward propagation)过程。


一、输入与输出格式

rnn = nn.RNN(input_size, hidden_size, num_layers=1, batch_first=False)
output, hn = rnn(input, h0)

参数解释:

  • input_size:每个时间步输入的特征维度。
  • hidden_size:隐藏状态的维度。
  • num_layers:堆叠的 RNN 层数。
  • batch_first:若为 True,输入输出的 shape 为 (batch, seq_len, input_size),否则为 (seq_len, batch, input_size)

输入张量:

  • input:形状为 (seq_len, batch, input_size)(batch, seq_len, input_size)
  • h0(可选):初始隐藏状态,形状为 (num_layers, batch, hidden_size)

输出张量:

  • output:包含每个时间步的输出,形状为 (seq_len, batch, hidden_size),其实是由状态 ht 组成的张量。
  • hn:最终时间步的隐藏状态,形状为 (num_layers, batch, hidden_size)

所以如果我们是一个 many to one 的模型,在输出时只需要取 output[-1] 传递给全连接层即可;而如果是 many to many 就看情况取相应数量的状态。


二、单层 RNN 的前向传播原理

对于一个序列 $(x_1, x_2, …, x_T)$,RNN 的每个时间步 $t$ 会执行如下操作:

数学表达:

\[h_t = \tanh(W_{ih} x_t + b_{ih} + W_{hh} h_{t-1} + b_{hh})\]

其中:

  • \(x_t\):当前时间步的输入,形状为 (input_size,)

  • \(h_{t-1}\):上一时间步的隐藏状态,形状为 (hidden_size,)

  • \(W_{ih}\):输入到隐藏层的权重矩阵,形状为 (hidden_size, input_size)

  • \(W_{hh}\):隐藏状态到隐藏层的权重矩阵,形状为 (hidden_size, hidden_size)

  • \(b_{ih}, b_{hh}\):偏置项,形状为 (hidden_size,)

  • \(\tanh\):激活函数

整个序列依次迭代执行该更新,产生每个时间步的隐藏状态

\[h_t\]


三、状态初始化

在定义 RNN 类的时候,需要手动写一个状态初始化的方法,该方法返回一个维度为 (num_layers, batch_size, hidden_size) 的张量。实例代码如下:

class RNNRegression(nn.Module):
    def __init__(self, input_nums, hidden_nums, output_nums):
        super().__init__()
        self.rnn = nn.RNN(1, hidden_nums)
        self.linear = nn.Linear(hidden_nums, output_nums)
        self.hidden_nums = hidden_nums
      
    def forward(self, x, state):
        x = x.transpose(0, 1).unsqueeze(-1)  # (seq_len, batch_size, input_size) 4,32,1
        output, hn = self.rnn(x, state)
        y = self.linear(output[-1].reshape(-1, output.shape[-1]))
        return y.squeeze(), state
  
    def begin_state(self, device, batch_size):
        return torch.zeros((self.rnn.num_layers, batch_size, self.hidden_nums), device=device)

四、梯度裁剪

由于 RNN 的结构,过多的层会导致梯度消失梯度爆炸,为了避免发生梯度爆炸,我们可以使用梯度裁剪的方法。

其具体操作也并不复杂,只需要加一行代码就可以了:torch.nn.utils.clip_grad_norm_(rnn.parameters(), max_norm=5.0, norm_type=2.0)norm_type 指定范数类型,默认是 L2 范数,其可以是任意的 p 范数。

当然除了裁剪之外,还有更加粗暴的方式——梯度截断,即直接把前面层的梯度舍弃掉。


五、预热

在 RNN 中,“预热(warm-up)” 是一个重要但容易被忽视的概念,尤其在处理长序列或状态传递模型时。它并不是 PyTorch 的内置功能,而是一种训练或推理时的策略。

RNN 的预热(warm-up) 指的是在开始对模型输出进行监督训练或评估之前,先输入一段“序列上下文”,仅用于初始化隐状态,但不参与损失计算或梯度反传

  • 为什么需要 warm-up?

RNN 模型的预测依赖于其内部的隐状态(hidden state)。在没有足够上下文时,隐状态往往是随机的或者全零的,这样会导致模型在序列初始阶段预测不准确,尤其在以下场景中尤为明显:

  1. 状态非常依赖历史输入(如轨迹预测、物理建模、金融时序);
  2. 训练使用截断反向传播(truncated BPTT),模型看不到完整历史;
  3. 使用非零初始状态会提升收敛速度或稳定性。

一些坑

这部分是我在写代码时遇到的一些问题。

最开始写好模型,进行训练时发现模型无法收敛,loss 很大,一直降不下去,后来检查了半天发现是损失函数输入的维度不匹配,两个输入的维度好像一个是 (32,1),一个是 (32),导致模型在计算损失的时候自动进行广播。

后来在一份文件中想要再写一个相似的模型处理不同的任务时,用了同一个优化器(optimizer),导致训练不收敛,也是 loss 下不去。后面检查了才发现问题所在。记住一个模型需要一个优化器,其和模型的参数是绑定的,不能用一个优化器优化多个模型的参数!(笨死了

Leave a comment