上一回我拆完了《Python 深度学习第三版》的第一部分,把张量、梯度、反向传播这些概念用大白话捋了一遍,也留下了几个待办实验。这篇是第二篇,正好对应原书的第二部分。如果说第一部分是给神经网络“算总账”,那第二部分就是真正的“开门做生意”——从图像分类到文本情感分析,再到生成模型和多模态组合任务,每一章都在教你如何用最少的代码,把一个又一个看起来高大上的模型落地。我当时读完这部分最大的感受是:原来前面那些数学符号,最后都变成了几十行Keras代码,而真正决定模型上限的,往往不是网络结构本身,而是你对数据、训练过程和调试节奏的理解。这篇笔记就按照原书的顺序,把第二部分里我认为最值钱的知识点、最容易踩的坑,以及我实际复现代码时的一点心得,全部盘一遍。
1. 第二部分在全书中的定位:从“能跑通”到“跑得好”
1.1 为什么第二部分一上来就是卷积神经网络
我读完第二部分的章节安排后,第一反应是“这部分真的很懂初学者”。它没有一上来就抛Transformer,而是先用一个完整的图像分类任务,把卷积神经网络(CNN)从输入到输出的每个环节都拆开给你看。为什么偏偏是CNN?因为图像任务足够直观:每个像素点、每个卷积核、每张特征图,都能被可视化。你有问题可以立刻用代码验证:把某个卷积层的输出打印出来,看看它到底提取了什么特征。这种“所见即所得”的反馈,对建立模型直觉非常重要。
相比第一部分偏重数学推导,第二部分就是用大量可运行代码,让你对“模型内部到底在算什么”产生真实的体感。我在复现时也明显感觉到,只要踏踏实实跟着敲一遍,卷积、池化、填充、步长这些概念就不再是名词,而是一组可以直接控制的张量操作。很多人觉得CNN难,其实是缺了亲手看中间结果的这一步。书里这部分正好补上了。
1.2 三个核心任务:图像、文本、生成
第二部分实际上围绕着三类任务展开:图像分类、文本分类、生成式建模。图像部分用CNN解决“空间特征怎么提取”;文本部分用RNN和Transformer解决“序列特征怎么建模”;生成模型部分则把前面学到的所有组件重新组合,训练一个能从噪声中“创造”新样本的模型。
这三块看起来方向不同,骨子里共享一套方法论:先把输入转成张量,再设计一个神经网络去学习输入到输出的映射,最后通过损失函数和优化器让映射不断逼近目标。这一点非常重要。很多人学完图像分类之后,再去学文本处理会觉得是“全新的知识”,其实只是输入数据形态变了,训练流程几乎是一样的。理解了这套统一框架,再看后面的多模态例子就会非常顺。
1.3 读第二部分前需要具备的最小认知
如果你是从零开始,我个人建议先把第一部分的大致内容过一遍:至少要知道什么是张量、什么是自动微分、什么是Keras的Sequential或函数式API。不需要能手推BP公式,但得明白损失函数的值是怎么用来更新权重的。
否则看第二部分里的模型代码,可能会陷入“代码都看得懂,但不知道为什么要这么写”的迷茫。反过来,如果你已经用框架写过一些demo,这部分正好帮你补上系统性的设计思路。我自己在给一些新手做分享时,经常说:第一部分解决“模型为什么能学”,第二部分解决“模型该怎么用”,两者之间是递进关系,不能跳步。
2. 图像任务:卷积神经网络的实战打法
2.1 卷积层和池化层:信息压缩的艺术
书里对卷积的讲解不是上来就画Kernel滑动窗口的示意图,而是把“卷积”解释成一种局部模式提取器。它跟全连接层最大的区别在于参数共享:一个3x3的卷积核在整张图上滑动,提取的是同一种局部模式。这个设计非常聪明,比如人脸的边缘、眼睛、鼻子的纹理,可能在图片不同位置,但是用的是同一组权重去识别它们。
用生活化的类比就是:你在人群中找朋友,不需要看整张脸的所有细节,只要扫描眼睛和脸的轮廓特征,就能认出人。卷积网络正是把这种局部扫描和特征组合做成了可学习的操作。我复现书中代码时特别喜欢看一个点:池化层。不少人一开始会觉得池化只是“降分辨率”,其实它更大的作用是给网络提供平移不变性。比如一个物体稍微往左挪了两个像素,最大池化仍然能捕捉到同一个显著特征,这对于分类任务很有意义。
需要提醒的是,现在很多新网络已经用步长卷积替代池化,但书中保留池化讲解,本质上是为了让你理解下采样是怎么一步步把高分辨率图像抽象成类别概率的。我在实际项目中,对于128x128以下的输入,还是经常使用卷积层+池化层的组合,因为简单、稳定、显存友好。
2.2 数据增强:让一张图变成N张图的魔法
这部分最值得实操的应该就是数据增强。它不改变网络结构,而是通过对训练图片随机旋转、缩放、平移、水平翻转、对比度调整等操作,让模型每次看到的数据都有轻微差异。这样能显著抑制过拟合,特别是当你的数据集只有几千张时。
我在实际项目中经常把训练图按90°、180°、270°随机旋转,再配合宽度位移和缩放,效果立竿见影。一个几千张的图片数据集,配上数据增强之后,训练效果往往能赶上一两万张的干净数据集。需要注意一点:数据增强不是越猛越好。如果旋转角度太大,反而破坏了图像本身的语义,比如数字6旋转180度变成了9;如果是医学影像,水平翻转可能改变器官的左右位置,这种增强方式就该少用或不用。
书里给出的增强参数可以作为一个起点,但你需要基于自己的数据和任务做判断。我自己的习惯是先固定一个增强强度,然后观察训练集和验证集loss之间的间距,再把增强强度逐步上调,直到两者差距不再明显为止。这个操作听起来很基础,但很多人一上来就把所有增强手段全开,结果发现训练集loss都下不去,反而觉得模型能力不行。
2.3 迁移学习:小数据救星
第二部分图像章节的高潮是迁移学习。思路很简单:先在大规模公开图像数据集上训练好一个CNN模型,把前面所有层的权重原封不动搬过来,只替换最后的全连接分类层,然后用自己的小数据集微调。
我在做某个小项目时,用自己的5000张图片从零训练CNN,最多只能到82%准确率;换成迁移学习方法,用预训练模型的特征提取加上一个新的全连接层,直接到了94%。这个提升不是一星半点,而且训练时间还缩短了一半以上。
但迁移学习有几个坑。第一,预训练模型的全连接层要不要保留?我建议不要,直接用全局平均池化,或者去掉最后全连接层,拿特征向量接自己的分类器。第二,是否冻结底层?如果数据量很小且和原训练数据类别比较接近,建议冻结前面大部分层,只训练最后几层,否则很容易把已有的通用特征破坏掉。第三,图片输入尺寸一定要和模型预训练时一致,否则第一层卷积就会报错,或者输出的维度跟你预期完全不同。
2.4 一个完整的图像分类流水线
书里第二部分带有非常完整的例程:下载数据集、预处理、定义数据增强、搭建模型、训练、评估。照抄一遍就能跑通。我自己在总结时通常会把流程简化成五个步骤:
- 确认图片尺寸和通道数。
- 用数据加载器构造增强流水线。
- 定义模型:卷积层、池化层、展平、全连接层、Dropout。
- 选择优化器和损失函数。
- 训练并监控验证集准确率。
这里最重要的一条经验:不要一上来就堆大模型,先用一个小型CNN过一遍全流程,保证每一个环节都能输出中间结果,再逐步加层。我在复现时曾因为图片标签路径配错,导致训练时loss一直在高位波动,排查了半天才发现是数据集的顺序被打乱了。所以一定要尽早打印几批数据出来看,样本和标签是否对应。
3. 文本任务:从词袋到注意力机制
3.1 词嵌入:把文字变成向量
文本处理和图像不同,模型不能直接吃掉字符串,必须先转成数字。最简单的办法是one-hot编码,但词表一大就容易变成稀疏矩阵,计算效率极低。第二部分引入了词嵌入(Embedding)层:把每个词映射到一个低维稠密向量,在这个向量空间里,词与词的语义关系就可以用向量的加减法表达。
我第一次看到embedding可视化时,真的感觉“词的数学”是存在的。Embedding层的参数是和网络一起训练的,所以它学到的向量天然适合当前任务。这句话听起来很虚,实操中就是:你用影评数据训练出来的embedding,对“好看”“精彩”这些词的向量表示一定跟新词“无聊”离得很远。
实际操作里要注意词表大小的设置。如果词表设得太大,Embedding层参数会非常庞大,严重拖慢训练;设得太小,很多低频词就只能变成未知词。书里建议可以先统计训练集里的词频,取一个能覆盖95%以上词汇的阈值作为词表大小,这是非常实用的经验。另外别忘了设置屏蔽0的标记,对于变长序列补齐用的0,Embedding层可以把它屏蔽掉,避免它参与后续计算。
3.2 RNN家族:LSTM和GRU应该怎么选
文本序列建模部分,书中从简单RNN讲到LSTM,再讲到GRU。RNN的问题在于梯度很容易消失,所以实际场景中用得更多的是LSTM和GRU。
我个人经验是:如果任务依赖长距离上下文,比如情感分析中“虽然……但是……”这种结构,LSTM更稳;如果数据量不大、想在速度和效果之间平衡,GRU会是更好的选择。网络结构上用一层或两层就够了,不要盲目堆深,文本任务中层的堆叠收益往往没有图像任务那么明显。
之前我做过一个文本分类任务,把双向LSTM从一层加到三层,准确率几乎没有提升,训练时间倒是翻了一倍。后来加了注意力层和Dropout,效果才真正上去。所以文本模型的重点应该在表示学习和正则化,而不是单纯加深度。
3.3 注意力机制:为什么Transformer成了标配
书中在RNN之后顺理成章地引入了注意力机制。注意力解决的痛点是:当一句话很长时,无论LSTM还是GRU,最后一步的隐藏状态都可能丢失早期信息。注意力机制不再要求模型把整句信息压进一个向量,而是在每个解码时刻主动去“回顾”源序列的所有隐藏状态,按相关性加权求和。
这个思路简单却深刻。我在理解时把它类比成做阅读题:你不会只看最后一个字就写答案,而是遇到关键信息时,会回到原文对应的句子,圈出重点。注意力就是这样一种“动态圈重点”的机制。
Transformer则是把注意力机制用到极致:去掉了循环结构,用自注意力捕捉句子内部所有词两两之间的关系。书中对应章节虽然没有把Transformer的所有数学细节展开,但足够让你理解多头注意力和位置编码的来龙去脉。我自己在读完这部分后,再去看主流大模型的原理,明显感觉不再发怵,至少能看懂结构图里每一层在干什么了。
3.4 文本分类实操:一个情感分析小项目
这里我强烈建议按照书中给的代码,自己搭一个影评情感分类模型。整个代码量不大,却能把Embedding、LSTM/GRU、Dropout、全连接分类这些知识点全部串联起来。
我做的第一个模型用了Embedding层加双向LSTM加全局池化加全连接,在几千条测试样本上可以达到88%左右的准确率。后来改成Transformer结构的编码器,准确率提升到91%。这个实验值得做,因为它能直观地告诉你:同一份数据,不同模型结构的表达力差距是怎样的。做完之后,你可以试着把LSTM换成GRU,或者把双向结构改成单向,观察指标变化,这种小实验比单纯读十遍书都更有用。
4. 训练优化:让模型真正收敛起来
4.1 正则化全家桶:Dropout、BatchNorm、权重衰减
第二部分里有一个很容易被初学者跳过的章节,专门讲如何提高模型泛化能力。书中花了大量篇幅解释为什么验证集的loss会先降后升,答案就是过拟合。为了控制过拟合,它依次介绍了L1/L2权重正则、Dropout、BatchNormalization、数据增强。
很多同学会把Dropout和BatchNorm混为一谈,其实它们的作用完全不同:Dropout是随机关闭神经元,强迫网络不要依赖某个局部路径;BatchNorm是对每一层的输入做归一化,解决内部协变量偏移,让梯度传播更平稳。在项目实践中,我一般会同时使用这两者,但会注意Dropout不宜过大,超过0.5很容易欠拟合。尤其在全连接层之前加一个0.5的Dropout,在图像任务里几乎是惯例,但如果是小网络,0.3可能更合适。
权重衰减又是另一回事,它是在损失函数里加一个权重的平方和惩罚项,让模型权重不要长太大。这个手段在优化器里直接配置就行,但很多人会忽略它。我自己的习惯是:如果数据集比较小,默认把权重衰减设为1e-4或1e-5,能明显减少过拟合。
4.2 优化器怎么选:从SGD到Adam
书里对比了SGD、RMSprop、Adam。我的建议是:入门阶段直接用Adam,把学习率设为默认的0.001基本不会翻车;如果到后期发现模型无法收敛到更高精度,可以换用SGD并配合动量,再用余弦退火或者步进衰减。
Adam收敛快,但有时候会在最优解附近震荡;SGD虽然直愣愣,但配好学习率往往能冲到更好的泛化点。这就像打游戏,Adam是自动瞄准,SGD是手搓压枪,手搓练好了上限更高。训练图像模型时,我经常前几十个epoch用Adam快速找到一个差不多的区间,再切到SGD做最后的精调。这个方法在多个项目里都有效,但注意切换优化器之后学习率要重新调低,否则很容易跳飞。
4.3 学习率:最值得提前设置的参数
很多模型效果差,不是网络结构不对,而是学习率不合适。书里教你用对数刻度在几个数量级上做“学习率扫描”:从1e-5到1e-1,每个值训练几个epoch,看loss曲线下降最陡的那个点,就是适合当前模型的学习率起点。
我试过这个方法,比盲目搜索效率高太多。后续还可以结合回调函数,当验证集指标连续几个epoch不涨时自动把学习率减半。我自己见过太多同学在一个不合适的固定学习率上硬跑几十个epoch,最后只是反复看到loss波动,完全没进展。学习率就像调节水龙头,太小了水滴滴答答,太大了水花四溅,只有扫描一遍才知道合适的开度在哪里。
4.4 我的超参数调试四个阶段
把第二部分的调参内容总结成流程,我的习惯是分四个阶段:
第一阶段,用默认参数跑通流程,确认数据、模型、损失函数拼装无误。这个阶段别急着改参数,先让模型能跑起来,能输出指标。
第二阶段,调节学习率和batch size,目标是让loss稳定下降。batch size要注意和GPU显存匹配,通常16到32之间对大多数任务都够用。太小的batch会让梯度噪声偏大,太大的batch又容易收敛到尖锐极小值。
第三阶段,加入正则化和数据增强,解决验证集和训练集差距过大的问题。这个阶段的判断依据是训练集loss还在降,但验证集loss开始反弹。
第四阶段,做小规模网格搜索,微调最后的全连接层维度、Dropout率。这四个阶段听起来很简单,但每一步都需要用TensorBoard或者本地绘图把曲线画出来判断,而不是拍脑袋改参数。
5. 生成模型与多模态入门:像搭乐高一样组合模型
5.1 VAE和GAN的基本套路
第二部分的后半段开始进入“生成模型”。与其把VAE和GAN当作两种完全不同的技术,不如把它们统一理解为:训练一个从隐向量分布到样本分布的映射函数。
VAE用“编码器-解码器”结构,先学到隐向量的均值和方差,再采样回去重建图片,所以它生成的结果比较平滑、可解释;GAN用生成器与判别器的对抗,生成器想骗过判别器,判别器想识破假图,最终逼近真实分布。
我在复现时更喜欢VAE的稳定,因为GAN的训练曲线实在太难伺候了,稍不注意判别器就“打死”生成器,loss像个心电图。书里给的代码虽然能跑,但如果你想自己玩出新花样,需要非常小心地控制训练比例。一个常见做法是判别器每更新两次,生成器才更新一次,这样能稍微平衡两者的能力,但这个比例在不同数据集上效果差异很大。
5.2 文本生成:从CharRNN到自回归模型
文本生成部分会教你用RNN一个字符一个字符地预测下一个字符。本质上是一个多分类问题:给定前面的字符,预测词表里每个字符的概率。
这部分代码不难,难的是生成效果评估。书里用的温度采样策略很有意思:生成时把logits除以一个温度系数T,T越小越保守,T越大越随机。我在写文案素材辅助工具时经常用类似思路,把T设在0.8到1.2之间,能避免模型生成的话过于模板化。如果T超过1.5,生成结果就开始乱来了,经常会出现不连贯的词组。
另外还要注意,字符级模型生成速度很慢,因为每生成一个字符都要跑一次前向传播。如果做更长的文本,建议先用词级模型,或者直接使用基于注意力的大模型接口,不要自己从头训练一个超大字符级RNN,收益很低。
5.3 多模态组合任务的实现思路
这一版的第二部分还新增了多模态的内容,比如同时处理图像和文本。就我看到的代码思路,基本上就是把图像用CNN提取特征,文本用Embedding加Transformer提取特征,然后把两条特征拼接起来,再接入一个全连接层做分类或生成。
这种“各自特征提取+融合分类”的思路,比很多人想象的要简单。关键是两个分支的特征维度要匹配,必要时各加一个全连接层把它们对齐到相同维度。书里这部分的目的是让你破除“模型只会处理单一数据”的刻板印象。我在自己做多模态项目时,体会最深的是数据对齐比模型结构更麻烦:图片和文本的样本要一一对应,顺序不能乱,否则训练出来的模型等于在学随机映射。
6. 第二部分项目复盘:把CNN和RNN放进同一个系统
6.1 项目目标与数据集
为了巩固第二部分内容,我照着书里的思路,自己做了一个“自动判卷”小项目:输入一张包含手写文本的图片,先用手写数字识别模型定位每个区域的数字,再用一个文本分类模型判断答案选项。
这个项目正好用上了图像分类、文本分类、多模态拼接三个技能点。数据集我用了公开的手写数字库加上自己拍的一批选择题答题卡照片,总共只有8000张左右。数据量并不大,但因为有图像和文本两种模态,训练和调试的复杂度比单一任务高了不少。
6.2 整体架构与代码目录
目录结构大概是:
data/:原始图和标注models/:训练好的权重src/:数据预处理、特征提取、分类脚本
预处理是重头戏,要对图片做透视校正、灰度化、二值化,再用轮廓检测把每个数字区域切出来。模型部分,数字识别直接用书里的CNN结构;选项识别则用文本模型。两部分模型独立训练,推理时串行组装。最终在真实答题卡上的端到端识别准确率在92%左右,比起单独模型精度各掉了10多个点,问题主要出在候选区域切割不准上。
6.3 模型融合与结果分析
这个项目的最大教训是:模型再准,如果前一步预处理有瑕疵,整体分数照样会崩。后来我加了区域检测的后处理规则,比如根据答题卡上的固定标记对切割框做位置校正,准确率才回升到96%。
书里第二部分没有专门讲系统集成,但通过这个项目我体会到,深度学习项目最花时间的往往不是模型训练,而是“把模型放进真实的pipeline”过程。数据噪声、输入尺寸不一致、设备环境抖动,都要在工程上兜住。比如拍答题卡的时候角度稍微偏一点,透视校正做的不好,后面所有步骤都会跟着偏,这种问题在单个模型的训练阶段根本看不出来。
6.4 组合任务的三个通用建议
如果你也想做类似的多模型组合任务,我给三个建议。
第一,模型尽量分开训练、分开保存,哪个环节出了问题就单独排查,避免“背锅”不清楚。第二,每个子模型的输出建议保留概率本身,而不是只保留硬标签,这样后续融合可以按置信度加权。比如手写数字识别模型对数字4输出0.4的概率,对数字9输出0.6的概率,那就应该把0.6当作更可靠的判断。第三,一定要在每一层接口处打印shape,哪怕是资深老手也容易因为维度不匹配栽跟头。
7. 常见问题速查:你在训练时大概率会遇到的坑
7.1 显存不足(OOM)
OOM是新手遇到最多的错误。解决办法很简单:先把batch size从32降到16或者8,看是否通过;然后让显存按需增长,不要一次性占满整块卡;如果还不行,考虑用混合精度训练,把部分计算改成半精度。
千万不要一上来就换更大的显卡,先看数据流水线里有没有缓存了太多东西。比如我之前在一个图像任务里,因为加载数据集时没设prefetch,每次迭代都要重新读磁盘,显存倒是没爆,但GPU利用率一直在20%左右徘徊,排查了很久才发现是数据管线的问题。
7.2 Loss不降或者震荡
如果loss一直不降,先检查数据预处理:标签是否正确,归一化是否做了,输入是否和模型第一层匹配。很多时候不是模型的问题,而是喂进去的label和图片对应错了,这种情况loss会一直是某个常数。
如果loss是震荡型,多半是学习率太大,可以先降到原来的十分之一试试。我自己踩过的一个坑是损失函数用错了,把多分类的交叉熵写成了二分类的,导致训练过程一直很诡异。建议每次改完损失函数,都打印几轮loss看看数值范围是否合理,数值太大或太小都说明有问题。
7.3 过拟合信号出现得太早
如果验证集loss在第三个epoch就开始反弹,而训练集loss还在下降,说明模型容量过大或数据增强不足。应对方式按顺序尝试:加强数据增强、加Dropout、减小网络容量、提前停止训练。
不要同时改动多个变量,否则你永远不知道自己调整的哪个操作起作用了。我建议每次只改一个变量,跑几个epoch看效果,记录下来,再改下一个。这个工作看起来慢,实际上比“一顿乱改然后发现效果更差”要快得多。
7.4 效果调不上去的排查清单
我根据自己的经验总结了一个排查清单:
- 确认数据没有泄漏,验证集里没有混进训练样本。
- 确认评估指标和损失函数是否一致,别用准确率评估却用交叉熵做验证标准。
- 看预训练模型是否冻结正确,是否不小心把底层也一起训练了。
- 看batch size和学习率是否匹配,大batch配小学习率通常更稳。
- 看模型输出的概率分布是否太极端,如果一直是0.99或0.01,说明模型过于自信,可能过拟合。
这个清单每次都能帮我稳定解决80%的“效果差”问题。
我个人在实际复现第二部分时,最大的体会是:书里给的是“下限”,也就是照着敲一定能跑通;但真正拉开差距的,是你愿不愿意在默认代码上多做一步实验,比如换一种数据增强、调一个学习率、把单模型改成组合模型。这些额外尝试才是把知识变成能力的过程。如果你也在啃这本书,建议每读完一章,就用自己手边的数据把代码改一改,哪怕只是把猫狗分类换成垃圾分类,也能得到完全不同的实战收获。这个系列后面如果继续拆,我会重点聊聊怎么把第二部分的模型接到真实业务系统里,包括API化部署和模型压缩,到时候再和大家细聊。