引言:同一张桌子,比个高下
上一章把 GRU 单独训练了 25 轮,验证准确率爬到 73.85%。但单看一家,看不出好坏——GRU 到底比 RNN 强多少?LSTM 是不是更厉害?不放在一起跑,谁也说不清。
这一章做一件简单又公平的事:把 RNN、LSTM、GRU 三个模型,用完全一样的食材(同一份数据)、一样的火候(同一批超参数)、一样的起点(同一个随机种子)各炒一遍,然后看谁先熟、谁最香、谁糊了。
🎯本章目标
- 用"控制变量法"搭起三模型公平对比的框架;
- 同一张图上看清三者的收敛速度与最终精度;
- 用真实数据验证第 10 章的理论结论:RNN 梯度消失、GRU 性价比最高。
一、控制变量:公平对决的前提
要比得公平,就得让三个模型只差"循环单元"这一处,其余全部锁死。这就是理科里常说的控制变量法——其他条件不动,只改想比的那一个变量。
| 超参数 | 取值 | 为什么锁死 |
|---|---|---|
| Embedding 维度 | 64 | 三个模型查同一本"字典" |
| Hidden 维度 | 64 | 记忆容量一样大 |
| Epoch | 12 | 大家都跑同样的时长 |
| 学习率 | 1e-3(Adam) | 步子一样大 |
| Batch Size | 64 | 每批吃同样多的样本 |
| 梯度裁剪 | 5.0 | 防止个别模型当场爆炸 |
| 随机种子 | 42 | 起点完全一致,可复现 |
这样,最后谁高谁低,就只可能是"循环单元"本身造成的。
二、同一副骨架,换核心零件
三个模型只是中间那层不同,前后的Embedding和Linear(1)完全一样。代码上把循环单元做成一个能随时替换的"零件"即可:
defmake_model(cell):unit={'RNN':nn.RNN,'GRU':nn.GRU,'LSTM':nn.LSTM}[cell]classNet(nn.Module):def__init__(self):super().__init__()self.emb=nn.Embedding(VOCAB,64,padding_idx=0)self.rnn=unit(64,64,batch_first=True)# 只换这一层self.fc=nn.Linear(64,1)defforward(self,x):e=self.emb(x)out=self.rnn(e)h_n=out[1]ifisinstance(out,tuple)elseout[-1]h_n=h_n[0]ifisinstance(h_n,tuple)elseh_nreturnself.fc(h_n[-1]).squeeze(-1)# 最后一步记忆打分returnNet()有个小坑值得记住:nn.LSTM返回的是(输出, (h_n, c_n))的元组,而nn.RNN/nn.GRU返回(输出, h_n)。所以取最后一步记忆时,得先判断out是不是元组、h_n又是不是元组,统一取到"最后一层、最后一个时间步"的隐藏状态——上面那两行isinstance就是在处理这个差异。
训练三遍,唯一变的只有make_model('RNN')/make_model('GRU')/make_model('LSTM'):
forcellin['RNN','GRU','LSTM']:torch.manual_seed(42);np.random.seed(42)# 起点一致model=make_model(cell)opt=torch.optim.Adam(model.parameters(),lr=1e-3)forepinrange(12):forstinrange(0,8000,64):x,y=pack(train_idx[st:st+64])# 填充+掩码,同第14章lo=lossf(model(x),y)opt.zero_grad();lo.backward()nn.utils.clip_grad_norm_(model.parameters(),5.0)opt.step()vacc=evaluate(model,val_idx)# 每轮在验证集打分训练期间,每轮在 2000 条验证集上测一次准确率,12 轮下来就有三条完整的曲线。
三、三足鼎立,一张图见分晓
把三条验证准确率曲线画在同一张图上,谁在爬升、谁在打摆子,一目了然:
- RNN(绿线,贴底震荡):12 轮全程在 50% 上下抖动,峰值只有51.35%。这就是第 5 章讲的梯度消失——信息传不了几层就湮灭,训练损失虽然缓慢下降,验证准确率却始终在"瞎猜"水平,学不进去。
- GRU(蓝线,陡升最高):前 4 轮还在 50% 附近热身,第 5 轮后突然崛起,一路冲到峰值73.65%,最终测试集73.06%。既快又准,名不虚传。
- LSTM(红线,后段跳升):前 5 轮也压着 50%,第 6 轮跳升到 60.95%,随后又回落震荡,第 9 轮再爬到63.25%,测试集62.38%。能学,但"热得慢",节奏不稳定。
把关键数字收拢成一张结果表:
| 模型 | 验证峰值 | 测试集准确率 | 参数量 | 表现 |
|---|---|---|---|---|
| RNN | 51.35% | 50.65% | 328,513 | 梯度消失,学不动 |
| GRU | 73.65% | 73.06% | 345,153 | 收敛快、精度高 |
| LSTM | 63.25% | 62.38% | 353,473 | 慢热、震荡 |
GRU 用最少的门控(2 个门)、比 LSTM 小的参数量,却跑出了最高的精度——这正是第 10 章"IMDB 实战选 GRU 当主力"的实证。
四、为什么 RNN 会"打摆子"?
RNN 全程贴底,根源还是那条老路的梯度消失。把第 4 章的结论搬回来:隐藏状态更新是
ht=tanh(Whh ht−1+Wxh xt)h_t = \tanh\big(W_{hh}\,h_{t-1} + W_{xh}\,x_t\big)ht=tanh(Whhht−1+Wxhxt)
反向传播时,梯度要一路乘过∂ht/∂ht−1\partial h_t/\partial h_{t-1}∂ht/∂ht−1:
∂L∂h1=∏t=2T[Whh⊤ diag(1−ht2)] ∂L∂hT\frac{\partial L}{\partial h_1} = \prod_{t=2}^{T} \Big[ W_{hh}^\top \,\mathrm{diag}\big(1-h_t^2\big) \Big] \,\frac{\partial L}{\partial h_T}∂h1∂L=t=2∏T[Whh⊤diag(1−ht2)]∂hT∂L
影评动辄一两百个词,这一串连乘里只要谱半径小于 1,梯度就指数式缩到几乎为零,模型收不到来自远处的信号,自然学不动。GRU 和 LSTM 靠门控另开了"高速通道",梯度能顺着细胞状态近无损地传回去,才解得开这个结。
五、常见坑与自查
- 公平性被破坏:某个模型忘了设种子,或改了学习率,曲线就不具可比性。每次换模型前都要重新
manual_seed(42)。 - LSTM 取错状态:LSTM 返回的是元组
(h_n, c_n),直接当 RNN 那样取out[-1]会拿到c_n张量、维度对不上。先用isinstance判一下类型。 - 只看训练损失:RNN 的训练损失也在降(从 0.70 到 0.55),但验证准确率纹丝不动——评价必须看验证集准确率,不能只看训练损失。
- 种子固定了但数据没洗牌:忘了第 14 章的教训,切分前不
shuffle,验证集变单一种类,三条曲线统统失真。
小结
这一章用控制变量法,把三个模型放到同一张桌子上比了个高下:
- 公平对决:同一份数据、同一批超参数、同一个种子,只换循环单元;
- RNN 学不动:全程卡在 50% 震荡,峰值 51.35%,梯度消失的实证;
- GRU 又快又准:第 5 轮陡升,验证峰值 73.65%、测试 73.06%,性价比之王;
- LSTM 慢热:后段才爬到 63.25%,能学但不稳。
结论和第 10 章的理论预测完全对上了:IMDB 这类中等规模任务,GRU 是效率与精度兼得的主力。但这还只是 12 轮、64 维隐藏的"初赛"。下一章还会在最优模型上引入 Dropout、调大隐藏维度,挑战更高的精度上限。
下一篇(二十九):模型优化、测试评估与RNN知识体系收官