引言:赢了比赛,还要再进一步
上一章的"三足鼎立"比出了结果:GRU 用 73.06% 的测试准确率拿下了头名。但最后一节留了个尾巴——第 21 轮之后,训练损失还在往下掉,验证准确率却不涨反跌。这说明冠军模型已经开始"背答案",而不是"学方法"了,我们称之为过拟合。
这一章做三件事:先给模型穿上"防作弊装甲"(Dropout),让它不再死记硬背;再趁机把记忆容量调大、多训练几轮,看看冠军能不能更上一层楼;最后在真正的考场上(2.5 万条没见过的影评)做一次全面体检,用混淆矩阵看得清清楚楚。结尾,把整个系列的知识点串成一张地图,算是给这段学习之旅收个官。
🎯本章目标
- 理解过拟合,用Dropout 正则化给模型"防作弊";
- 调大隐藏维度、加长训练,把测试准确率从 73.06% 提升到79.80%;
- 在测试集上画混淆矩阵,搞懂精确率、召回率、F1 的关系;
- 把第 1-16 章知识串成一张认知树,完成收官。
一、过拟合:背答案而不是学方法
先想清楚冠军到底出了什么问题。模型训练时看的是一组影评,目标是让"见过"的这组影评判断得越来越准。可"越来越准"有两种实现方式:
- 学方法:从影评里提炼出真正的规律,比如"出现 great、amazing 多半是好评",这种能力能迁移到没见过的新影评上;
- 背答案:把每一句训练影评原封不动地记下来,见到一模一样的就答对。可新影评从没见过,背的答案用不上。
过拟合就是模型从"学方法"滑向了"背答案"。训练损失一路跌到接近 0(训练集全答对了),验证准确率却停在原地——因为它对没见过的数据束手无策。
怎么判断一个模型"过拟合"了?看训练损失和验证指标之间的缝隙。训练损失只管"训练集背得好不好",验证准确率才代表"新数据上真正厉不厉害"。两者差距越拉越大,就越说明模型在钻训练集的空子。
回到第 14 章那组真实数据就能看清这个"剪刀差":
| 训练轮 | 10 | 15 | 20 | 24 |
|---|---|---|---|---|
| 训练损失 | 0.396 | 0.185 | 0.093 | 0.054 |
| 验证准确率 | 59.9% | 68.6% | 73.5% | 73.05% |
从第 15 轮到第 24 轮,训练损失还在大幅下降(0.185 → 0.054),训练集几乎被"一字不差"背下来了;可验证准确率从峰值 73.85% 回落、停在 73% 一线不动。损失在降、验证不涨,这两个信号一撞,就是过拟合的典型特征。
把这个"剪刀差"画出来更直观:
训练损失(红)越走越低、验证准确率(蓝)先升后平,两条线之间张开一个大口子——缝得越开,模型"背答案"越严重。
怎么逼它"只能学方法、不许背答案"?思路是训练时故意给数据"加点干扰",让它没那么容易记住原文。这就是Dropout。
二、Dropout:随机"关掉"一部分神经元
Dropout 的做法非常朴素:每次训练时,随机让一部分神经元"罢工"(输出置 0),下次再换另一批。相当于每次都在练一个"残缺"的模型。
打个比方:老师出卷子时,每次都随机遮住几道题的答案,逼学生必须理解原理,而不是靠死记硬背某个标准答案。因为不知道哪部分会被遮住,学生只能把每个知识点都真正弄懂,这样学出来的本事,换个新卷子也用得上。
更妙的是,因为"每次关掉的是不同的一批",相当于同时训练了"很多个不同的残缺模型",最后大家一投票取平均——单个模型的奇思怪想(对训练集的死记)被平均掉了,保留下来的反而是大家一致认同的通用规律。这就是 Dropout 的"集成"直觉。
数学上,训练时对隐藏向量hhh逐元素乘一个随机掩码rrr,其中每个分量独立服从伯努利分布:
ri∼Bernoulli(1−p),h~i=ri⊙hi1−pr_i \sim \mathrm{Bernoulli}(1 - p), \qquad \tilde{h}_i = \frac{r_i \odot h_i}{1 - p}ri∼Bernoulli(1−p),h~i=1−pri⊙hi
这里ppp是"关掉比例"(Dropout 率),除以1−p1-p1−p是为了让h~\tilde hh~的期望值保持和hhh一样,不影响整体的"信号强度"。可以快速验证一下:因为E[ri]=1−p\mathbb{E}[r_i] = 1-pE[ri]=1−p,所以
E[h~i]=E[ri] hi1−p=(1−p) hi1−p=hi\mathbb{E}[\tilde h_i] = \frac{\mathbb{E}[r_i]\,h_i}{1-p} = \frac{(1-p)\,h_i}{1-p} = h_iE[h~i]=1−pE[ri]hi=1−p(1−p)hi=hi
期望不变,模型"平均下来"没被削弱,只是多了随机扰动。注意:只在训练时关,评估时全开——否则模型在考试时也会"掉链子"。
在 PyTorch 里就一行nn.Dropout(p),插入到该加的位置:
classSentimentGRU(nn.Module):def__init__(self,vocab=5002,embed=64,hidden=96,dropout=0.3):super().__init__()self.emb=nn.Embedding(vocab,embed,padding_idx=0)self.drop=nn.Dropout(dropout)# 防作弊装甲self.gru=nn.GRU(embed,hidden,batch_first=True)self.fc=nn.Linear(hidden,1)defforward(self,x):e=self.drop(self.emb(x))# 查表后先随机关一批_,h=self.gru(e)# 通读returnself.fc(self.drop(h[-1])).squeeze(-1)# 打分前再关一批Dropout 放在两处:Embedding 之后(打乱"词向量")、最后一个线性层之前(打乱"浓缩记忆")。关键提醒:评估时 PyTorch 会自动识别model.eval()并关闭 Dropout,所以训练/评估切换时别忘了model.eval()。
三、调参升级:73.06% → 79.80%
有了 Dropout 这把保险锁,模型不那么容易"背答案"了,就可以放心把手脚放开——把记忆容量从 64 加到 96,训练轮数从 12 加到 16。超参数对比如下:
| 超参数 | 第 15 章(基线) | 第 16 章(调参) |
|---|---|---|
| 模型 | GRU | GRU |
| 隐藏维度 | 64 | 96 |
| Dropout | 无 | 0.3 |
| 训练轮数 | 12 | 16 |
| 词向量维度 / 学习率 / Batch | 64 / 1e-3 / 64 | 不变 |
| 最大化序列长度 | 200 | 200 |
训练代码和上一章几乎一样,唯一区别是隐藏维度和 Dropout:
torch.manual_seed(42);np.random.seed(42)model=SentimentGRU(hidden=96,dropout=0.3)# 换了型号opt=torch.optim.Adam(model.parameters(),lr=1e-3)forepinrange(16):forstinrange(0,8000,64):x,y=pack(train_idx[st:st+64])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)# 每轮验证集打分真实跑出来的验证准确率曲线:
- 第 15 章基线(橙色虚线):峰值验证准确率 73.65%,训练 12 轮,测试集 73.06%;
- 调参后(蓝色实线):隐藏维度 96 + Dropout 0.3,训练 16 轮,验证峰值79.70%,测试集79.80%。
光是加一个 Dropout、调大一点隐藏维度,验证准确率就抬高了整整6 个百分点。更关键的是,曲线后半段依然稳稳上扬,没有出现上一章那根"涨到顶就回落"的尾巴——过拟合被明显压住了。最终在 2.5 万条从未见过的测试影评上拿到了79.80%。
把两代模型的成绩摆在一起:
| 模型 | 验证峰值 | 测试集准确率 | 参数量 | 说明 |
|---|---|---|---|---|
| 第15章 GRU(hidden=64) | 73.65% | 73.06% | 345,153 | 基线,后期过拟合 |
| 第16章 GRU(hidden=96 + Dropout) | 79.70% | 79.80% | 366,881 | 调参,稳定上扬 |
消融实验:确定 Dropout 到底贡献了多少
能涨 6 个点,是 hidden 变大还是 Dropout 的功劳?为了分清这笔账,做一次消融实验:把 hidden 固定成 96,其他不变,只去掉 Dropout 再跑一遍同样的 16 轮。真实结果:
| 配置 | 验证峰值 | 测试集准确率 | 结论 |
|---|---|---|---|
| hidden=96,无Dropout | 76.55% | 75.74% | Dropout 的作用被单独剥离 |
| hidden=96,带Dropout=0.3 | 79.70% | 79.80% | 完整配置 |
同样是 hidden=96、同样是 16 轮,光是把 Dropout 加上去,测试准确率就从 75.74% 涨到 79.80%,净增约 4 个百分点。这说明提升主要不是靠"记忆容量变大",而是靠 Dropout 压住了过拟合——模型从"背答案"真正转向了"学方法"。这也解释了为什么第 14 章末尾故意留了个"过拟合"的伏笔:那时候没有 Dropout,加容量只会背得更彻底,反而更糟。
四、混淆矩阵:看清模型到底错在哪
测试准确率 79.80% 是个好数字,但它只告诉我们"答对了八成",没说清楚"错在哪、偏向了谁"。情感分类是个二分类问题,答案只有"好评(1)"和"差评(0)"两种。把预测和真相交叉一摆,就得到一张混淆矩阵(Confusion Matrix),四格刚好对应四种情况:
- TP(真阳性):真实好评,预测也是好评;
- TN(真阴性):真实差评,预测也是差评;
- FP(假阳性):真实差评,却被判成好评;
- FN(假阴性):真实好评,却被判成差评。
在测试集 2.5 万条上统计出来的真实数字:
- 对角线(TP、TN):判对的部分,共 9339 + 10611 =19950条,占总数的79.8%,正好对上准确率;
- FP(差评误判成好评):只有 1889 条,最少。这类往往踩在"讽刺"上——比如"这电影烂得恰到好处",表面词是好的,模型就上当了;
- FN(好评漏判成差评):有 3161 条,最多。这类多是"欲扬先抑"的写法——开头抱怨一堆,结尾才转折夸好,读到最后一步时模型把"转折"给丢了。
从这两格各挑一条真实误判的影评,看看模型是怎么"被骗"的:
差评被判成好评(FP)│ 真实差评 预测好评 影评里反复出现 "great" "nice" "works" 等褒义词, 但整段其实在挖苦演员的表演与剧情——模型只抓了表面词。 好评被判成差评(FN)│ 真实好评 预测差评 影评先花大量篇幅抱怨节奏慢、人物扁平, 结尾才转折说自己"看得很享受"——模型没抓住最后的转折。第一类说明模型容易被单个褒义词误导(讽刺的坑);第二类说明模型只重视开头、丢了结尾的转折(长尾依赖的坑)。这两类错误,正是纯 GRU 这类"顺序读完最后一步"的模型最典型的短板。
从矩阵还能推出三个更细的指标:
Precision=TPTP+FP=93399339+1889≈0.832\text{Precision} = \frac{TP}{TP+FP} = \frac{9339}{9339+1889} \approx 0.832Precision=TP+FPTP=9339+18899339≈0.832
Recall=TPTP+FN=93399339+3161≈0.747\text{Recall} = \frac{TP}{TP+FN} = \frac{9339}{9339+3161} \approx 0.747Recall=TP+FNTP=9339+31619339≈0.747
F1=2⋅Precision⋅RecallPrecision+Recall≈0.787\text{F1} = \frac{2\cdot \text{Precision}\cdot \text{Recall}}{\text{Precision}+\text{Recall}} \approx 0.787F1=Precision+Recall2⋅Precision⋅Recall≈0.787
这三个指标各看一个侧面:
- 精确率 0.832:模型说"这是好评"时,有 83.2% 真的说对了。高,说明它很少"冤枉"好评;
- 召回率 0.747:所有真实好评里,它只捞回来了 74.7%,有近 1/4 漏掉了。相对偏低,正是那 3161 条"欲扬先抑"式好评被误杀;
- F1 0.787:把两者调和成一个数,兼顾"别冤枉"和"别漏掉",是更公允的单一排行指标。
用个生活场景区分精确率和召回率:精确率像"挑苹果",挑出来的苹果里坏果越少越好(精挑细选);召回率像"捞鱼",网兜越密,能捞到的鱼越多,但也会捞进一堆杂草(宁多勿漏)。两者天然此消彼长,F1 就是它们的"调和平均数",偏大者被惩罚、偏小者被拉高,最终给出一个平衡分。
三种模型最终的 F1 对比如下:
| 模型 | 精确率 | 召回率 | F1 |
|---|---|---|---|
| RNN(第15章) | 0.51 | 0.51 | 0.51 |
| LSTM(第15章) | 0.62 | 0.62 | 0.62 |
| GRU 调参后(第16章) | 0.832 | 0.747 | 0.787 |
五、常见坑与自查
- 评估时忘了
model.eval():不切回评估模式,Dropout 还在随机关神经元,验证/测试准确率会被"打折扣",测出来偏低。训练循环结束、开始打分前,务必model.eval()。 - Dropout 加错位置:加在输入层之前或输出层之后没有意义;正确位置是 Embedding 之后和线性层之前(即"信息流动的中段")。
- Dropout 率过大:设成 0.5 以上,模型"残废"得太狠,学不动,准确率反而下降。0.2~0.3 是常见稳妥区间。
- 只看准确率不看矩阵:79.8% 的准确率掩盖了"漏判好评偏多"这个倾向。要想知道模型是"爱冤枉"还是"爱漏掉",必须看混淆矩阵和 Precision/Recall。
- 测试集和调参混着用:如果在测试集上反复试参数、越试越高,测试集就"被污染"了,测出来的数字不再可信。调参只看验证集,测试集只碰一次。
- 调只加容量、不加 Dropout:第 14 章的教训——没有正则化时,把 hidden 从 64 加到 96,模型会"背得更彻底",很可能更早过拟合。加容量一定要和 Dropout 搭配。
六、收官:一串知识点,一张认知树
到这里,第 1-16 章的完整链路就算走通了。回头看一眼,其实整个系列就回答了一个问题:怎么让机器读懂一段文字的感情。从原理到实战,恰好是一根越走越坚实的主线:
这条主线的四个阶段,正好对应四个问题:
- 第 1-5 章 · 原理:为什么文字要用 RNN?循环到底是什么?手撕一遍前后向,摸清梯度消失的病根;
- 第 6-10 章 · 进化:直面病根,从 LSTM 的细胞状态到 GRU 的双门简化,理解"传送带 + 收费站"如何救场;
- 第 11-13 章 · 数据:把文字变成数字,把长短不一的影评装进统一模具,学会填充与掩码;
- 第 14-16 章 · 实战:搭模型、三兄弟对决、调参优化、混淆矩阵评估,最终拿到 79.80% 的测试准确率。
回头看:这套方法还能迁移到哪
这一路学到的,远不止一个 79.80% 的数字,而是一套可复用的方法链:
- 数据工程:把原始文本变成规整的张量(词表、填充、掩码),任何文本任务都用得上;
- 模型选型:任务规模中等、追求效率时选 GRU,长序列高精度要求时选 LSTM,先跑个 RNN 当基线;
- 训练技巧:固定种子保证可复现、梯度裁剪防爆炸、Dropout 防过拟合、只看验证集调参;
- 评估思维:准确率之外,用混淆矩阵 + 精确率/召回率/F1 看清模型的"性格"。
这套思路稍加改动,就能平移到机器翻译、文本生成、语音识别、时间序列预测等任务上——结构换成更大模型,管线依然成立。学会"从原理到实战"的完整闭环,比记住任何一个具体数字都更有价值。
小结
这一章既是优化,也是收官:
- 认识过拟合:训练损失狂降、验证走平,是模型在"背答案",用 Dropout 随机关掉部分神经元,逼它"学方法";
- 调参升级:隐藏维度 64→96、加 Dropout、练 16 轮,测试准确率从73.06%提升到79.80%,过拟合尾巴消失;
- 消融验证:固定 hidden=96 只去 Dropout,准确率掉到 75.74%——证明提升主要来自 Dropout 压过拟合(净增约 4 个点);
- 混淆矩阵评估:TP=9339 / TN=10611 / FP=1889 / FN=3161,精确率0.832、召回率0.747、F10.787,看清模型"判差评很准、漏好评偏多";
- 知识收官:把原理、进化、数据、实战串成一条主线,完成从零到 79.80% 的完整闭环。
至此,从"循环是什么"到"让机器读懂影评感情",一段完整的旅程画上了句号。三大循环网络、完整的数据管线、从手撕到实战的方法论,都可以迁移到机器翻译、文本生成、语音识别等其他序列任务上。理解原理、动手实现、用数据说话——这条路上学到的,远不止一个数字。
感谢一路读到这里的每一位读者。