
1. 深度学习笔记我的图像分类练手项目复盘如果你和我一样是那种白天写业务代码、晚上想搞点深度学习副业的程序员估计你手机里也存了一堆“深度学习笔记”收藏从未停止动手从未开始。我这次不一样真的把一个图像分类练手项目从零跑通了。这篇笔记不是概念堆砌而是从数据准备、模型搭建、训练调参到最后导出模型做推理的完整复盘中间踩过的坑比代码还多。这套笔记适合两类人一类是把神经网络当黑盒子、想弄懂基本逻辑的初学者另一类是已经跑通过一些简单模型、但总在训练阶段翻车、找不到排查思路的人。我会尽量用大白话讲清楚每一步为什么这么做以及遇到问题该按什么顺序查。1.1 为什么从图像分类开始图像分类是深度学习里最“短平快”的入门项目。原因很简单数据集容易整理评价指标直观训练过程能看到准确率一点点往上走这种正反馈能支撑你度过最初的上手阶段。图像分类问题涉及的卷积、池化、全连接、激活函数这些组件几乎是之后做目标检测、图像分割的基础把它吃透后面换任务不会太吃力。我当时选这个方向主要是想避开两个劝退点一个是环境配置一个是数学推导。环境问题可以靠现成的集成环境解决数学推导可以先放一放先用代码把完整链路跑通再回头补理论。事实上我在没有完全搞懂反向传播公式的情况下就先把一个简单模型训练起来了之后再回来看梯度下降理解速度反而快很多。所以我的建议是不要等“准备好了”再动手你永远准备不好先做一个小而完整的项目遇到问题再补课。1.2 这套笔记解决的三个痛点第一个痛点是“理论太多代码太少”。网上教程上来就讲反向传播公式但真到训练时遇到 loss 不降、准确率卡住没人告诉你先查哪里。这篇笔记把训练阶段最常见的故障排查顺序整理出来了照着查就行。第二个痛点是“数据集太干净”。教程里的公开数据集都是整理好的图片尺寸统一、标签基本正确、类别也均衡。但实际项目里图片尺寸不一、标签噪声、类别不平衡这三件事几乎一定会遇到。这篇笔记用了大量篇幅讲数据准备和预处理因为这里才是真正决定模型上限的地方。第三个痛点是“训练一次太慢”。没有自己的显卡或者用的是共享开发机一次实验跑几小时是很正常的事。如果每次失败都靠试错时间成本根本扛不住。所以我把常见失败原因整理成速查清单尽量让你一次实验就能定位问题而不是盲目调参数。2. 核心概念拆解神经网络到底在学什么2.1 线性层与非线性激活的配合很多人一开始会把神经网络想得很玄其实最基本的单元就是线性变换加非线性激活。线性变换就是 y Wx b把输入映射到输出但多个线性层堆叠起来本质上还是一个线性变换因为 W2(W1x b1) b2 这种形式始终是线性的。如果没有非线性激活函数再深的网络也只是一层线性变换根本学不了复杂关系。激活函数的作用就是给网络引入非线性。拿 ReLU 来说它的表达式是 f(x) max(0, x)一句话概括负数归零正数不变。它实现简单计算快还能缓解梯度消失问题所以现在大多数卷积网络都用它。Sigmoid 会把输出压到 0 到 1 之间适合二分类的输出层但如果用在隐藏层梯度很容易在饱和区域消失训练起来非常痛苦。我习惯这样理解如果没有激活函数网络就像一台只会做加减乘除的计算器永远画不出曲线加了 ReLU 之后它才具备拟合任意复杂函数的能力。你不需要背公式只需要记住“线性层负责特征组合激活函数负责引入非线性”就够了。2.2 损失函数、优化器与学习率的搭配逻辑分类任务里最常用的损失函数是交叉熵。为什么不直接用准确率当损失因为准确率是离散的梯度在大部分地方都是 0模型根本没法更新。交叉熵能给出连续可导的损失值告诉模型“你错得有多远”。训练时loss 是一个很小的数不代表模型差反而如果 loss 直接变成 0要怀疑是不是过拟合了。优化器负责根据损失计算梯度并更新参数。我最初用的是 Adam因为它对学习率不敏感调参压力小适合快速出基线。但后来发现在图像分类任务上Adam 收敛到的结果未必比带动量的 SGD 好尤其是后期微调阶段。所以我的做法是先用 Adam 快速跑通流程等确定模型和数据都没问题后再换成带动量的 SGD 配合余弦退火提高精度。学习率初始值一般设在 1e-3 到 3e-4 之间如果 loss 震荡剧烈就调小一点。还有一个细节不要一开始就开很大的学习率。很多人为了让模型快点收敛把学习率设成 0.1结果 loss 直接飞了。建议第一轮先观察 loss 的变化如果下降得很平稳再尝试加大如果抖动明显就减小。学习率不是越大越好它决定的是模型能不能走到正确的山谷底部。2.3 训练集、验证集和测试集的分工这部分看起来简单但实际项目里特别容易糊。训练集是给模型学习的验证集用来调整超参数和判断是否过拟合测试集只能最后用一次用来模拟模型在真实场景下的表现。如果反复用测试集调参测试集就变成了验证集最终评估结果会虚高。我见过不少人把全部数据扔给模型训练然后说准确率很高但一到新数据就翻车。这就是典型的没有预留验证集。划分数据的比例常规做法是 60% 训练、20% 验证、20% 测试如果数据量很大比如十万张以上验证和测试各留 5% 到 10% 就够。关键是要保证三个集合互不重叠并且按类别分层划分避免某一类全跑进训练集而测试集里只剩另外几类。还有一个容易忽略的问题数据标准化用的均值和方法只能从训练集算出来然后应用到验证集和测试集上。如果你拿全部数据算均值再划分数据集这就造成了数据泄露验证集和测试集不再是独立的。我一开始没注意这个导致模型评估结果虚高部署后效果差一大截。3. 数据准备与预处理最容易翻车的地方3.1 数据收集与标注的注意事项数据是深度学习的上限。我这次做的是宠物图片分类类别有猫、狗、鸟。理想情况下应该用公开数据集但为了模拟真实项目我选择自己收集图片于是马上体会到数据的残酷网上找的图大小不一有些图片还带水印甚至有几张猫狗同时出现在画面里直接成了标签噪声。我的处理办法是先写一个脚本把所有图片统一转成 RGB 三通道再按类别分文件夹存放。接下来最花时间的步骤是人工清洗。不要完全相信下载时的文件名和元信息必须肉眼扫一遍。我大约收集了 1500 张图花了两个晚上筛选掉了 200 多张不干净或重复的。这个环节不能省因为标签噪声对模型的影响远大于你的想象一张被标成“猫”的狗图片会让 loss 莫名其妙跳动。如果你要做的项目数据量特别大可以把人工清洗改成置信度过滤先用一个预训练模型对图片做预测把置信度低于阈值的图片挑出来人工复核。这样可以省不少力气但前提是预训练模型的任务和你的任务比较接近。通用经验是数据质量宁可少而精不要多而脏。3.2 归一化、增强与批次大小的选择图像数据最基础的处理是归一化。大多数框架默认输入图片的像素值范围是 0 到 1 或者 -1 到 1。直接拿 0 到 255 的原始像素喂给网络会让梯度更新不稳定训练很难收敛。最省事的做法是除以 255把范围压到 0 到 1更标准的做法是减均值再除标准差这能让数据分布更接近标准正态训练加速。数据增强则是用随机裁剪、水平翻转、颜色抖动等方式人为制造更多的训练样本。它解决的核心问题是模型泛化能力不足让模型对物体的位置、角度、光线变化更鲁棒。但增强不能太狠比如随机旋转 45 度以上图像里的猫可能看起来像抽象画反而破坏了语义。我一般用随机水平翻转、随机裁剪到固定尺寸、小幅颜色抖动这几招在大多数图像任务里都很稳。批次大小这个超参数经常被忽视。它受限于显存常用值是 32、64、128。批次太小梯度估计噪声大训练不稳批次太大一个 batch 的样本数多更新次数少可能收敛到平坦的极小值。我实验下来在中小数据量下用 32 或 64 都比较稳。如果你只有一个 batch 能放下 16 张图也不要强求更大可以把学习率调小一点作为补偿。3.3 数据集划分的实操账本我按类别分层划分最终的数据集数量大概是这样类别训练集验证集测试集猫48080100狗45080100鸟4208080总共有 1870 张图片。划分的时候用了固定的随机种子确保每次实验拿到的数据集都一样这样才能公平对比不同模型和超参数的效果。用随机种子划分后我把每个样本的路径和标签存成了一个索引文件训练时直接读取这个文件避免每次运行重新打乱导致训练集和验证集发生重叠。这里有一个值得单独说的细节划分数据集一定要基于文件索引而不是直接复制图片文件。因为图片很多复制不仅慢而且一旦你想改划分比例又得重新复制。我后来是写了一个脚本把 train.txt、val.txt、test.txt 三个文件生成出来里面每一行是图片路径和标签。这样训练代码只依赖这几个文件临时调整比例只需要重新生成 txt不用动原始图片非常省事。4. 模型搭建与训练从零配置到收敛4.1 选择基础模型的判断依据很多人开始写模型骨架时总想从零发明一个网络结构。我的建议是不要。图像分类领域已经有大量被验证过的经典结构它们的设计原理、训练技巧、甚至预训练权重都是公开的。站在巨人肩膀上做迁移学习效果远好于自己搭一个十来层的卷积网络然后从随机初始化开始训练。我当时选择了带残差连接的卷积网络作为骨干。残差连接的核心思想是让网络可以跨层传递信息即使层数加深梯度也能顺畅回传。这个结构不是玄学它实际解决了深度网络退化的问题。但我不是直接从头训练而是加载了在大型公开数据集上预训练过的权重然后冻结前面的卷积层只训练最后的全连接层。为什么这样有效因为卷积网络靠前的层学到的是边缘、纹理、颜色这些通用特征这些特征在不同任务间可以迁移只有后面几层才和具体类别强相关。如果你的数据量和我的差不多不到两千张完全不必追求特别深的模型。模型参数量太大数据量不够必然过拟合。宁可先用小模型跑通流程再逐步增加深度。我的经验是参数尽量选能加载到预训练权重的经典结构然后用验证集表现来决定模型大小而不是直觉认为“越大越好”。4.2 训练超参数配置与迭代观察点训练一个模型需要提前定好一堆超参数。我把常用的配置记录如下轮数30 到 40批次大小32初始学习率1e-3优化器Adam后面换带动量的 SGD权重衰减1e-4学习率调度每 10 轮衰减为原来的 0.1权重衰减系数很容易被忽略它本质上是对大的权重值施加惩罚防止模型过于依赖某些特征。我一开始没设训练到后期验证准确率总比训练低不少加上权重衰减之后过拟合明显缓解。不过系数也不能太大否则模型会被限制得太死损失降不下去。训练过程中每个 epoch 记录四样东西训练损失、验证损失、训练准确率、验证准确率。不要只盯着训练损失训练损失降得好不一定代表模型好。我习惯每两三个 epoch 打印一次验证准确率如果验证准确率连续几个 epoch 不涨就检查是不是学习率太大导致震荡或者模型容量不够。保存模型时一定要以验证集表现最好的一轮为准而不是最后一轮。因为深度学习训练在后期可能出现短暂的验证 loss 升高但后面又降回来的情况。如果只保存最后一轮可能错过最好的模型。我的习惯是每次验证准确率超过历史最佳就保存一份模型副本同时记录当时 epoch 数。4.3 过拟合监控与早停策略过拟合的典型特征就是训练损失不断下降验证损失却开始回升验证准确率停滞甚至下降。这说明模型开始死记训练样本的特征而没有学到真正通用的规律。遇到这种情况第一反应不是增加数据而是先看现有数据是不是已经被模型“背下来”了。解决过拟合有几个常用手段数据增强、Dropout、权重衰减、早停。数据增强在前面说过它扩大训练分布最自然Dropout 是在训练时随机让一部分神经元失活强制网络多个通路协作但用的时候别设太大一般 0.3 到 0.5 之间权重衰减是对大权重施加惩罚平滑模型决策边界。早停是训练过程中的一个保险机制。我设置的最大轮数是 40同时设置 patience 8意思是如果验证 loss 连续 8 轮没有改善就停止训练并恢复历史最佳模型。这样可以避免在过拟合阶段浪费大量计算时间。实现起来也很简单训练循环里维护一个最佳验证损失变量如果当前验证损失更低就更新并保存模型否则计数器加一计数达到 patience 就 break。这里有一个容易踩的坑早停的标准应该是验证损失而不是验证准确率因为准确率在类别不平衡的时候可能会骗人。验证损失在过拟合初期往往比准确率更早暴露问题所以我的训练代码里同时监控两者以验证损失为主要早停依据。5. 常见问题与排查技巧实录5.1 损失不降的排查顺序这是我在训练阶段遇过最多的情况明明网络搭好了数据也喂进去了loss 就是纹丝不动。很多人的第一反应是加大学习率结果越加越乱。正确的做法是按下面的顺序排查这基本能覆盖 90% 的问题。先检查数据预处理。索引和标签对不对图片有没有被错误地归一化标签是不是从 0 开始的整数类别数有没有和模型输出维度一致。我翻车过一次标签从 1 开始编号但损失函数默认从 0 开始导致所有预测全都错位loss 一直高位震荡。再检查模型输出层。全连接层输出节点数必须和类别数相等如果多一个少一个损失要么报错要么不降。接着检查优化器参数。学习率设成 1e-2 以上时模型很容易发散loss 变成 nan。遇到 nan先降学习率再看有没有除零。最后检查损失函数是否和任务匹配。回归任务用均方误差分类任务用交叉熵这两者别混。如果你发现 loss 一直稳定在某个常数附近比如分类任务类别数是 5loss 一直大约等于 1.6ln5说明模型输出基本均匀分布根本没学到东西主要查数据加载和标签有没有对上。5.2 GPU显存溢出的处理显存溢出大概是训练过程中最常见的技术性错误。直观原因是模型权重、中间特征图和梯度加起来超出了显存上限。但我遇到过一种更迷惑的情况模型明明不大可运行到第二个 epoch 就溢出原因是测试时没有设置不计算梯度或者把训练和验证都放在同一个自动求导上下文里导致中间变量一直没释放。处理方法优先级大致是先减小批次大小这是最直接的如果还不行降低图片分辨率再不行用混合精度训练把部分计算改成半精度显存占用能降一半左右。还有一个方法是梯度累积也就是用多个小批次模拟一个大批次先算几个小批次的梯度加起来做一个更新但注意要适当缩放梯度否则学习率等效变大。排查的时候要分清是“真溢出”还是“逻辑错误”。真正显存不足时错误信息通常包含 CUDA out of memory 字样如果是不小心打开了自动求导但没有关闭通常表现为显存占用不断攀升。我的习惯是训练模式函数里显式区分训练和验证验证阶段用no_grad上下文包裹这样显存占用基本稳定。5.3 类别不平衡的应对思路真实数据里类别数量经常差好几倍。我这套数据里三个类别相对均衡但我另一个朋友做的场景只有 2% 的正样本模型最后几乎把所有样本都判成负类准确率还挺高因为负类占比 98%。这说明只看准确率在类别不平衡时完全没有意义得看精准率和召回率。应对不平衡首先考虑数据层面最简单的做法是对少类别做重复采样也就是把少类样本多复制几遍再参与训练。这样做会改变数据分布模型会略微偏向少类。但注意重复太多容易过拟合一般把比例控制在 1:2 以内比较合适。另一个思路是在损失函数里给不同类别加权重让模型在少类上犯错的惩罚更大。加权交叉熵写起来简单效果也直接。如果数据极其不平衡光靠权重可能还不够可以试试 Focal Loss 这类专门设计的损失函数它能让模型把注意力集中到难分类的样本上天然对容易类别的损失进行衰减。但这类损失会引入额外超参数需要另行调优建议先跑通基线的加权交叉熵再考虑升级。6. 实测过程中的一些碎碎念6.1 一个让我折腾最久的bug整个项目里让我最痛苦的问题不是模型不收敛而是验证结果“太好”了。测试集准确率一度到了 99%我当时还很开心但拿到几张没见过的图片去推理效果差得离谱。排查了两天终于发现数据预处理器里有一个缓存开关训练前第一次读取图片时会把处理后的数组存进临时缓存验证阶段读取图片时直接用了这份缓存。问题在于缓存生成是在划分数据集之前做的等于验证集里的部分图片已经被训练阶段写入过缓存模型在验证时“见过”了这些图片的信息。这个 bug 让我理解了一个原则数据预处理必须放在数据集划分之后并且每一步都要用纯函数处理不允许在整理图片时有任何全局状态。后来我把整条数据管线重写了一遍先读图片路径再按索引文件划分最后在训练循环里实时做预处理和增强不用缓存。验证集准确率掉到了 88% 左右但拿到真实图片推理时效果反而更正常了。所以看到虚高的指标先不要高兴想想是不是在数据划分上出了问题。6.2 后续可以怎么扩展这套笔记覆盖的流程虽然以图像分类为例但大部分方法论可以直接平移。如果你想继续往下走可以做三件事。第一把分类模型换成目标检测模型在图片里同时定位出物体位置和类别这需要补充边界框回归的知识。第二尝试模型剪枝和量化在保准率基本不变的前提下压缩模型体积把训练好的模型迁移到手机或边缘设备上推理。第三换一个更复杂的数据集把解决类别不平衡的权重策略、数据增强策略全部重新调一遍巩固这套排查思路。我个人在实际操作中体会最深的就是训练深度模型其实是一个系统性工程模型结构只占一部分数据清洗、超参数管理、训练监测缺一环都会翻车。这套笔记最大的价值不是模型最终跑到了多少准确率而是让我知道遇到问题该按什么顺序查。如果你也在跑第一个深度学习项目建议先把这份笔记里的故障排查顺序存下来遇到问题不用慌照着清单一步步排除很多坑其实都是共通的。