三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

python神经网络编程入门(二十九)——模型优化、测试评估与知识体系收官

python神经网络编程入门(二十九)——模型优化、测试评估与知识体系收官

引言:赢了比赛,还要再进一步

上一章的"三足鼎立"比出了结果:GRU 用 73.06% 的测试准确率拿下了头名。但最后一节留了个尾巴——第 21 轮之后,训练损失还在往下掉,验证准确率却不涨反跌。这说明冠军模型已经开始"背答案",而不是"学方法"了,我们称之为过拟合

这一章做三件事:先给模型穿上"防作弊装甲"(Dropout),让它不再死记硬背;再趁机把记忆容量调大、多训练几轮,看看冠军能不能更上一层楼;最后在真正的考场上(2.5 万条没见过的影评)做一次全面体检,用混淆矩阵看得清清楚楚。结尾,把整个系列的知识点串成一张地图,算是给这段学习之旅收个官。

🎯本章目标

  1. 理解过拟合,用Dropout 正则化给模型"防作弊";
  2. 调大隐藏维度、加长训练,把测试准确率从 73.06% 提升到79.80%
  3. 在测试集上画混淆矩阵,搞懂精确率、召回率、F1 的关系;
  4. 把第 1-16 章知识串成一张认知树,完成收官。

一、过拟合:背答案而不是学方法

先想清楚冠军到底出了什么问题。模型训练时看的是一组影评,目标是让"见过"的这组影评判断得越来越准。可"越来越准"有两种实现方式:

  • 学方法:从影评里提炼出真正的规律,比如"出现 great、amazing 多半是好评",这种能力能迁移到没见过的新影评上;
  • 背答案:把每一句训练影评原封不动地记下来,见到一模一样的就答对。可新影评从没见过,背的答案用不上。

过拟合就是模型从"学方法"滑向了"背答案"。训练损失一路跌到接近 0(训练集全答对了),验证准确率却停在原地——因为它对没见过的数据束手无策。

怎么判断一个模型"过拟合"了?看训练损失和验证指标之间的缝隙。训练损失只管"训练集背得好不好",验证准确率才代表"新数据上真正厉不厉害"。两者差距越拉越大,就越说明模型在钻训练集的空子。

回到第 14 章那组真实数据就能看清这个"剪刀差":

训练轮10152024
训练损失0.3960.1850.0930.054
验证准确率59.9%68.6%73.5%73.05%

从第 15 轮到第 24 轮,训练损失还在大幅下降(0.185 → 0.054),训练集几乎被"一字不差"背下来了;可验证准确率从峰值 73.85% 回落、停在 73% 一线不动。损失在降、验证不涨,这两个信号一撞,就是过拟合的典型特征。

把这个"剪刀差"画出来更直观:

过拟合的"剪刀差":训练损失狂降,验证却走平回落0.00.20.40.60.81.005101520Epoch训练损失(一路跌向 0)验证准确率(先升后走平)缝隙越拉越大 = 过拟合

训练损失(红)越走越低、验证准确率(蓝)先升后平,两条线之间张开一个大口子——缝得越开,模型"背答案"越严重。

怎么逼它"只能学方法、不许背答案"?思路是训练时故意给数据"加点干扰",让它没那么容易记住原文。这就是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}riBernoulli(1p),h~i=1prihi

这里ppp是"关掉比例"(Dropout 率),除以1−p1-p1p是为了让h~\tilde hh~的期望值保持和hhh一样,不影响整体的"信号强度"。可以快速验证一下:因为E[ri]=1−p\mathbb{E}[r_i] = 1-pE[ri]=1p,所以

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]=1pE[ri]hi=1p(1p)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 章(调参)
模型GRUGRU
隐藏维度6496
Dropout0.3
训练轮数1216
词向量维度 / 学习率 / Batch64 / 1e-3 / 64不变
最大化序列长度200200

训练代码和上一章几乎一样,唯一区别是隐藏维度和 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)# 每轮验证集打分

真实跑出来的验证准确率曲线:

GRU 调参前后验证准确率(IMDB)0.450.500.550.600.650.700.750246810121416Epoch调参后 hidden=96 + Dropout79.70%第15章基线 hidden=64 无Dropout73.65%验证峰值 79.70%
  • 第 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,Dropout76.55%75.74%Dropout 的作用被单独剥离
hidden=96,Dropout=0.379.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 万条上统计出来的真实数字:

测试集混淆矩阵(2.5万条全新影评)预测好评(1)预测差评(0)真实好评(1)真实差评(0)TP = 9339(答对好评)FN = 3161(好评漏判)FP = 1889(差评误判)TN = 10611(答对差评)对角线 TP + TN = 19950,占总数 25000 的 79.8%FP 只占 7.6%,FN 占 12.6% —— 漏判好评明显偏多
  • 对角线(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+188993390.832

Recall=TPTP+FN=93399339+3161≈0.747\text{Recall} = \frac{TP}{TP+FN} = \frac{9339}{9339+3161} \approx 0.747Recall=TP+FNTP=9339+316193390.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+Recall2PrecisionRecall0.787

这三个指标各看一个侧面:

  • 精确率 0.832:模型说"这是好评"时,有 83.2% 真的说对了。高,说明它很少"冤枉"好评;
  • 召回率 0.747:所有真实好评里,它只捞回来了 74.7%,有近 1/4 漏掉了。相对偏低,正是那 3161 条"欲扬先抑"式好评被误杀;
  • F1 0.787:把两者调和成一个数,兼顾"别冤枉"和"别漏掉",是更公允的单一排行指标。

用个生活场景区分精确率和召回率:精确率像"挑苹果",挑出来的苹果里坏果越少越好(精挑细选);召回率像"捞鱼",网兜越密,能捞到的鱼越多,但也会捞进一堆杂草(宁多勿漏)。两者天然此消彼长,F1 就是它们的"调和平均数",偏大者被惩罚、偏小者被拉高,最终给出一个平衡分。

三种模型最终的 F1 对比如下:

模型精确率召回率F1
RNN(第15章)0.510.510.51
LSTM(第15章)0.620.620.62
GRU 调参后(第16章)0.8320.7470.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章)循环思想 · BPTT · 梯度消失三兄弟进化(第6-10章)RNN → LSTM → GRU数据处理(第11-13章)词嵌入 · 词表 · 填充掩码实战对比(第14-15章)三模型同台 · GRU 胜出优化评估(第16章)Dropout · 调参 · 混淆矩阵最终成果测试准确率 79.80%RNN 家族:原理 → 手撕 → 数据 → 实战 → 优化一条主线读懂文本序列建模

这条主线的四个阶段,正好对应四个问题:

  • 第 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% 的完整闭环。

至此,从"循环是什么"到"让机器读懂影评感情",一段完整的旅程画上了句号。三大循环网络、完整的数据管线、从手撕到实战的方法论,都可以迁移到机器翻译、文本生成、语音识别等其他序列任务上。理解原理、动手实现、用数据说话——这条路上学到的,远不止一个数字。

感谢一路读到这里的每一位读者。

← 返回列表