到底怎么用PyTorch的nn.RNN,这个问题看起来简单,但几乎每周都有读者来回踩同一个坑:维度对不上、batch_first搞混、初始化忘记传h_0、多层RNN堆上去反而不如单层……这类问题我从入门到给开源项目做贡献都遇过,所以干脆把这一整套东西拆开揉碎了写一篇实战向的完整笔记。这篇文章会围绕PyTorch的RNN API、多层RNN的堆叠机制、所有核心参数的含义与选择、以及输入输出的完整排布规则展开,最后给一个可以直接跑的实战案例(正弦波序列预测),再把人名分类这种文本场景的改动点单独讲清楚。无论你是刚接触RNN的新手,还是需要快速把模型跑起来的工程党,这篇都能当工具手册来用。
1. 从计算图到API:先把RNN的本质理清楚
1.1 RNN到底循环了什么
RNN最核心的公式只有两个。假设输入序列是 \(x_1, x_2, ..., x_T\),其中每个 \(x_t\) 是一个向量,RNN在时间步 t 做的事情是:
\[ h_t = \tanh(W_{ih} x_t + b_{ih} + W_{hh} h_{t-1} + b_{hh}) \]
这个公式并不需要背,但必须理解三个点。第一,\(W_{ih}\) 负责把当前时刻输入 \(x_t\) 映射到隐藏空间;第二,\(W_{hh}\) 负责把上一时刻的隐藏状态 \(h_{t-1}\) 映射到当前时刻,这是“循环”名字的来源;第三,激活函数用了 \(\tanh\) 而不是 ReLU,是因为 \(\tanh\) 输出范围在 -1 到 1 之间,在循环迭代中能让状态值保持稳定,避免重复乘权重之后数值快速爆炸。
举个例子帮助理解。你读一句话的时候,并不是把每个词当作孤立信息,而是带着“前面说了什么”的上下文来理解当前词。RNN的隐藏状态 \(h_t\) 就是这个上下文向量,它把从 \(x_1\) 到 \(x_t\) 的所有信息压缩成了一个固定维度的向量。这也是为什么RNN适合处理序列数据:它天然就有“记忆”能力,虽然这种记忆很简单、容易遗忘,但结构上确实是在逐时间步融合信息。
1.2 PyTorch怎么把公式封装成了API
PyTorch的 torch.nn.RNN 就是上面这个公式的封装。你不需要手动去定义 \(W_{ih}\)、\(W_{hh}\)、\(b_{ih}\)、\(b_{hh}\),也不需要自己写循环和 \(\tanh\),只需要告诉API两个数字:输入特征维度 input_size 和隐藏状态维度 hidden_size,其余权重矩阵都是模块内部自动创建并管理的。
python复制import torch
import torch.nn as nn
rnn = nn.RNN(input_size=8, hidden_size=16, batch_first=True)
print(rnn)
看输出你会注意到,nn.RNN 内部有 weight_ih_l0 和 weight_hh_l0 两个核心权重矩阵,还有对应的偏置项。weight_ih_l0 的形状是 (hidden_size, input_size),weight_hh_l0 的形状是 (hidden_size, hidden_size)。这两个矩阵正好对应公式里的 \(W_{ih}\) 和 \(W_{hh}\)。
这种封装的价值在于:你不用关心梯度怎么在时间维度上回传。PyTorch自动把循环展开成计算图,反向传播时对每个时间步求梯度再累加,这就是所谓的BPTT(Backpropagation Through Time)。你只需要把数据喂进去,把loss算出来,然后调用 loss.backward() 即可。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. nn.RNN参数逐项拆解:每个参数到底在控制什么
2.1 必填参数:input_size与hidden_size怎么定
input_size 和 hidden_size 是唯二必须手动指定的参数,也是最容易让初学者纠结的地方。
input_size 是单步输入向量的特征数。注意,它跟序列长度无关,跟batch大小也无关。比如你处理的是文本,每个词用一个50维的词向量表示,那 input_size=50;处理的是股票数据,每个时间步有开盘价、收盘价、成交量等5个特征,那 input_size=5。它是“切面”的概念:序列里每一个时间步取出来的那一个向量是什么维度。
hidden_size 是隐藏状态向量的维度,它决定了模型的记忆容量。隐藏状态越大,能承载的上下文信息就越多,模型表达能力越强,但参数量也会显著增加。参数量怎么算?单层RNN的参数量大约是 \(4 \times (hidden \times input + hidden \times hidden)\)(4来自t时刻的输入、上一时刻状态、两个偏置相关的矩阵运算,严格说还要加上偏置项)。我实测过一个例子:input_size=50, hidden_size=128 的单层RNN参数量是 4*(50*128 + 128*128) = 91136 左右,而 hidden_size=256 时直接翻了四倍多。所以不要盲目调大 hidden_size,当训练数据不够多、任务本身复杂度不高时,过大的隐藏状态只会带来过拟合和训练变慢。
2.2 结构参数:nonlinearity、num_layers与bidirectional
nonlinearity 默认是 'tanh',也可以改成 'relu'。实践中我基本不动这个参数,原因前面说过,RNN的循环结构天然需要输出有界的激活函数,relu 在时间维度展开后容易出现梯度爆炸。PyTorch官方的 nn.RNN 实现里 relu 选项更多是给研究用的,常规任务直接保留默认即可。
num_layers 指定堆叠几层RNN,默认是1。它是这篇笔记的重点,会在第4节单独展开讲。这里先给一个直观记忆:num_layers=2 意味着第一层RNN的输出序列,会作为第二层RNN的输入序列,第二层输出最终的隐藏序列。每一层有自己独立的一套权重,代码里你会看到 weight_ih_l0、weight_hh_l0、weight_ih_l1、weight_hh_l1 这样按层编号的参数。
bidirectional=True 则会让模型同时从两个方向处理序列。正向RNN从左往右读,反向RNN从右往左读,每个时间步最终输出是两个方向的隐藏状态拼接。它最常用于自然语言处理任务,因为一个词的语义往往依赖它的上下文两侧信息,而不仅仅依赖前文。需要注意两点:第一,输出维度会翻倍,变成 2 * hidden_size,因为拼接了;第二,最终隐藏状态 h_n 的第一维也会翻倍,变成 num_layers * 2,因为每一层都有正向和反向两个最终状态。
2.3 训练辅助参数:bias、batch_first与dropout
bias 参数控制是否使用偏置项,默认 True。在PyTorch的RNN实现里,有 bias_ih_l0 和 bias_hh_l0 两组偏置,分别对应输入映射和状态映射。绝大多数情况下偏置都该保留,关闭它能省下的参数不多,但模型的拟合能力会打折扣。只有当你在做某些严格的理论分析或模型蒸镏时才可能关掉它,实战里不用想
