资讯动态

深度学习进阶必经之路:CNN、ResNet与目标检测实战拆解

发布时间:2026/9/17 6:48:31 来源:尧图企业网站定制
《动手学深度学习》这套公开课我前后完整刷过三遍如果只让我保留其中一段反复看那一定是第 51 到第 111 集。这个区间对应的正好是课程从现代卷积网络开始的进阶主线经典 CNN 架构的演进、批量归一化、残差网络、目标检测、语义分割以及循环神经网络的起点。对很多刚开始接触深度学习的人来说前 50 集看完会有一种“我都懂了”的错觉一旦打开真实项目却不知道从哪下手真正让你从“能看懂”变成“能实现”的恰恰是这一段。今天就把我做笔记、复现代码、跑实验时对这部分内容的拆解和踩坑记录整理出来给同样在这条路上爬坡的人一个参考。1. 为什么第 51111 集是全课程的分水岭1.1 前 50 集和后 60 集的差距在哪里前 50 集解决的是“零件问题”线性回归、softmax、多层感知机、反向传播、优化器、损失函数这些东西就像工具箱里的螺丝刀和扳手单独拿出来你都认识也知道怎么用。但深度学习的工程项目从来不是单个零件能撑起来的它更像装一台电脑你手里全是配件却没人告诉你主板应该怎么挑、电源瓦数怎么选、散热器装哪里。第 51 集往后课程开始真正教你“装配和选型”。第 51111 集这个区间讲的都是现代深度学习模型里真正在用的东西。比如你用任何开源仓库跑图像分类几乎绕不开 ResNet 的残差结构你做目标检测锚框、IoU、多尺度特征图这些概念看不懂连阅读检测模型源码的资格都没有你处理图像分割任务转置卷积和全卷积网络就是最基础的地基。这段内容和产线项目的距离比前面所有基础章节都近。1.2 这段内容的知识地图我自己把 51111 集按主题重新切了一遍大致可以分成四个块现代卷积网络结构、图像目标检测、语义分割、循环神经网络入门。切完之后学习目标一下就清晰了每看一集都知道它在整张地图里处在什么位置。主题块覆盖内容学完之后能做什么现代 CNN 架构AlexNet、VGG、NiN、GoogLeNet、批量归一化、ResNet、DenseNet理解主流图像分类模型的设计思路能自己改 backbone目标检测锚框、IoU、多尺度检测、SSD、YOLO、R-CNN 系列跑通检测训练流程看懂检测模型源码语义分割转置卷积、FCN、分割数据集处理像素级分类任务序列模型入门文本预处理、语言模型、RNN、GRU、LSTM为自然语言处理打底理解序列数据建模这张表是我第二次刷的时候整理出来的。第一次刷就是老老实实一集一集看看到后面忘了前面而且不同主题之间切换时没有心理准备会觉得“怎么突然就讲到这了”。如果你还没开始这段内容我建议先把这张地图存在手机里看一集对照一次方向感会好很多。2. 现代卷积架构的进化逻辑从 LeNet 到 ResNet2.1 AlexNet 与 VGG把“深度学习”里的“深”字做出来LeNet 在 MNIST 上表现很好但拿到复杂图像上就明显吃力核心原因是它的容量不够。AlexNet 做了三件很关键的事用 ReLU 替代 sigmoid解决了深层网络里的梯度衰减问题用 Dropout 做正则化减轻过拟合用最大池化和更大规模的卷积层堆叠把模型的表达能力提上去了。站在今天的视角看AlexNet 的结构并不复杂但它的意义在于证明了“更深的卷积网络是可行的”。VGG 把这件事推到了一个新的高度。它提出用多个 3x3 卷积核堆叠来替代大卷积核比如两个 3x3 卷积堆叠感受野等于一个 5x5 卷积三个堆叠等于 7x7。这样做的优势有两个一是参数量更少三个 3x3 的参数量是 27 个一个 7x7 是 49 个二是每个 3x3 卷积中间都跟着一个激活函数网络的非线性表达能力更强。这里有个很实用的计算细节刷课程时一定要自己推一遍。卷积输出尺寸公式是 H_out (H - k 2p) / s 1假设输入是 224x224卷积核是 3x3padding 为 1步长为 1那输出就是 (224 - 3 2) / 1 1 224尺寸不变。如果你把 padding 改成 0输出就变成 222特征图尺寸逐渐缩小后面接池化层时你才知道每一层的尺寸到底是多少。很多人看网络结构图觉得“差不多懂了”一到自己写代码就报维度不匹配的错原因就是没把这条公式真正手算过。2.2 GoogLeNet 与 NiN并行卷积的思考方式NiN 和 GoogLeNet 给我最大的启发是卷积网络不一定只能“一层一层往上叠”还可以“在同一个层上做多种变换再融合”。NiN 引入了 1x1 卷积。1x1 卷积不做空间维度的特征提取它的作用是在通道维度上做线性组合相当于一个跨通道的全连接层。这个操作可以灵活改变通道数而且计算量很小。你在后面看很多轻量级网络包括 MobileNet 里的深度可分离卷积本质上都是在卷积计算方式上做文章NiN 是这条思路的早期代表。GoogLeNet 的 Inception 模块更激进它在同一个网络层上并行跑多个不同尺寸的卷积核比如 1x1、3x3、5x5再加上一个 3x3 的最大池化最后把所有分支的输出在通道维度上拼接起来。这样做的好处是网络能在不同的感受野尺度上同时提取特征然后再自己决定哪些尺度更有用。在当时那个算力环境下Inception 通过大量 1x1 卷积先降维、再计算很好地控制了计算量。在笔记里我专门对比过 VGG 和 GoogLeNet 的参数量同样是 ImageNet 级别的分类任务VGG-16 的参数量超过 1 亿而 GoogLeNet 大概只有 VGG 的十分之一。这个对比非常直观地说明了“结构设计本身就是一种正则化”不是一味堆参数而是用巧妙的计算方式让同样多的表达能力花在刀刃上。2.3 批量归一化训练稳定性的关键批量归一化Batch NormalizationBN是这段课程里最容易被低估的内容之一。很多人把 BN 当成一个“加进去就能训得更快”的插件却不理解它到底解决了什么问题。简单说BN 做的事情是对每一个 mini-batch 的激活值做标准化让它变成均值为 0、方差为 1 的分布然后再通过两个可学习参数做缩放和平移。这个操作最直接的效果是缓解了“内部协变量偏移”也就是说前一层的参数更新会导致后一层输入的分布发生变化BN 把这种变化强行拉回来让每一层都在一个相对稳定的分布区间里工作。实操中有一个必须注意的细节训练模式和推理模式下的 BN 行为是完全不同的。训练时BN 用当前 batch 的均值和方差做归一化推理时它用的是训练过程中累计的全局均值和方差。如果你自己写模型忘了调用model.eval()推理结果可能会非常奇怪。我在第一次跑图像分类时训练集准确率很高一到测试集就崩查了半天发现是训练完没有切回 eval 模式。另外还有一个经验加了 BN 的网络初始化权重时不再那么敏感。没有 BN 之前权重初始化过大容易梯度爆炸过小容易梯度消失有了 BN即使初始化稍微随意一点网络也能在训练初期自动把分布拉回正常范围。这也是它能“加速收敛”的根本原因。2.4 ResNet 与 DenseNet残差连接为什么有效ResNet 是第 51111 集里分量最重的内容之一因为它是“深度学习”真正意义上能深下去的大功臣。理论上网络越深表达能力应该越强但实际训练时会发现网络深到一定程度后训练误差反而上升了这就是退化问题。这个现象不是过拟合因为训练误差都降不下去说明深层网络本身的优化难度变大了。ResNet 的解法非常优雅与其让网络直接学习期望的映射 H(x)不如让它学习残差 F(x) H(x) - x然后在输出端和输入做一次恒等映射相加也就是 y F(x) x。如果目标映射刚好接近恒等映射那么网络只需要把 F(x) 学到接近 0 就行这比从零开始拟合一个恒等映射要容易得多。残差连接提供了一个“梯度的高速公路”反向传播时梯度可以经过 shortcut 直接传到前面的层有效缓解了梯度消失。课程里还配套讲了 DenseNet。DenseNet 比 ResNet 更激进它让每一层都和前面所有层在通道维度上拼接相当于把“跳连”变成了“密集连接”。这样做的好处是特征复用更充分但显存消耗也更大因为中间特征要全部保留下来。实际项目中 ResNet 仍然是更通用的选择DenseNet 更常用于对参数量敏感的移动端场景。自己做笔记时我给残差块写了一个非常精简的 PyTorch 实现结构清晰方便随时回来复习import torch import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, in_ch, out_ch, stride1): super().__init__() self.conv1 nn.Conv2d(in_ch, out_ch, 3, stride, 1, biasFalse) self.bn1 nn.BatchNorm2d(out_ch) self.conv2 nn.Conv2d(out_ch, out_ch, 3, 1, 1, biasFalse) self.bn2 nn.BatchNorm2d(out_ch) self.shortcut nn.Sequential() if stride ! 1 or in_ch ! out_ch: self.shortcut nn.Sequential( nn.Conv2d(in_ch, out_ch, 1, stride, biasFalse), nn.BatchNorm2d(out_ch) ) def forward(self, x): out torch.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.shortcut(x) return torch.relu(out)这里有个小细节值得说卷积层和 BN 一起使用时卷积通常设置biasFalse。因为 BN 层会做标准化和可学习的平移卷积层的 bias 会被直接抵消掉留着只会白占参数量没有任何收益。3. 从“这是什么”到“它在哪”目标检测与分割入门3.1 锚框目标检测的第一步图像分类解决的是“这是什么”的问题目标检测还要回答“它在哪”。课程从锚框这个概念开始讲检测我认为是特别正确的切入点。锚框的本质是预先在图片上铺一批大小不同、宽高比不同的候选框然后让模型判断每个框里有没有目标、目标属于哪个类别、框的位置应该怎么调整。这个过程很像你在人群里找朋友你不可能同时遍历所有像素而是先快速扫过一张脸的大致位置再逐渐把注意力集中到那个区域最后精确框出来。理解锚框有一个关键指标叫 IoU也就是两个框的交集面积除以并集面积。IoU 用来衡量预测框和真实框的重合程度也用来决定一个锚框到底是正样本还是负样本。比如某个锚框和真实框的 IoU 大于 0.5就把它当成正样本去学习低于某个阈值就当背景不参与目标类别的训练。课程里反复强调的这个阈值直接影响了正负样本的平衡也决定了检测器精度的上限。我第一次做检测实验时一个很大的感受是检测的训练流程比分类复杂得多因为你不仅要训练网络还要处理 Anchor 的生成、正负样本匹配、边框回归的编码解码。如果只看损失函数的公式而不动手画一画锚框和真实框的位置关系很难真正理解。3.2 SSD 与 YOLO两种非常关键的检测思路SSD 的做法是在多个不同尺寸的特征图上分别做检测浅层特征图分辨率高适合检测小目标深层特征图语义信息丰富适合检测大目标。为了让每个位置能预测不同形状的物体SSD 在每个特征图位置上生成多个不同宽高比的锚框。这套“多尺度加锚框”的组合是当时速度和精度平衡得非常好的方案。YOLO 的思路更直接它把整张图片划分成一个网格比如 7x7每个网格负责预测固定数量的候选框以及类别概率。YOLO 不再需要单独的区域提议阶段一个网络端到端直接输出检测结果所以推理速度非常快适合实时性要求高的场景。第一代 YOLO 对小目标和密集目标的检测效果不好但它的思想启发了后面所有的单阶段检测器。对比这两种思路可以提炼出一个通用认知目标检测器的设计基本都在权衡“速度”和“精度”。两阶段检测器如 Faster R-CNN先产生候选区域再分类回归精度高但速度慢单阶段检测器如 SSD、YOLO一步到位速度快但早期精度相对逊色。现在很多新方法在尝试拉平这个差距你理解了这条主线之后看待各种论文时就不会觉得眼花缭乱。3.3 转置卷积与 FCN像素级预测的基石语义分割和分类检测不一样它的输出是一张和输入同尺寸的类别图每个像素都要被标记成对应的语义类别。怎么把卷积网络提取的特征图恢复到原图分辨率这就引出了转置卷积。转置卷积有时候会被误解成“卷积的逆运算”它其实不是数学意义上的逆而是让特征图从小变大的一种可学习的上采样方式。普通卷积做的是多对一的映射转置卷积做的是“一对多”的映射一个输入像素通过卷积核的转置传播到输出区域。实现上它会在输入像素之间插入空格填充再做一次普通卷积。所以转置卷积也有 stride、padding 这些参数但作用效果和普通卷积正好相反。FCN也就是全卷积网络是语义分割的基础模型。它把分类网络最后的全连接层全部替换成卷积层再用转置卷积把特征图上采样回原图大小。课程里这一步花了不少篇幅我也建议你亲手把特征图尺寸的变化过程算一遍比如输入是 224x224经过 VGG 的骨干网络后变成 7x7最后用转置卷积上采样回 224x224中间每一层的尺寸变化都要算清楚否则写代码时上采样倍数对不上输出就和输入对不齐。4. 复现实操用 ResNet 完成一次完整训练4.1 环境与数据准备学习这部分内容光看视频绝对不行必须动手把代码跑通。我自己复现的时候环境用的是 PyTorch CUDA数据选了 CIFAR-10 作为第一个验证基准因为它规模小、类别少在单张普通显卡上几分钟就能看到一个训练周期的结果非常适合用来验证模型写没写对。数据准备的几个关键步骤用torchvision.datasets.CIFAR10下载数据对训练集做随机裁剪、随机水平翻转和标准化对测试集只做标准化。这里要注意随机增强只作用于训练集如果对验证集也做随机翻转评测指标就会失真这也是一个新手非常容易犯的错误。标准化用的均值和方差是一组统计量。CIFAR-10 的均值大约是(0.4914, 0.4822, 0.4465)标准差大约是(0.2470, 0.2435, 0.2616)。把数据从 [0, 1] 转成近似标准正态分布可以让网络训练更稳定。如果你换数据集这一组数要重新统计不能无脑抄。4.2 模型定义与训练循环模型部分我直接用了前文定义的 ResidualBlock然后堆一个简化版的 ResNet-18网络主体就是“一次卷积加池化”后面接四个残差层最后做全局平均池化加全连接输出。全局平均池化这个操作很重要它取代了早期的全连接层可以大大减少参数也增强了模型对空间平移的鲁棒性。训练循环里有两个容易被忽略但特别影响结果的细节。第一个是优化器选择。课程里推荐了 Adam实际复现分类任务时我用 Adam 跑得也不错。但如果你做的是目标检测这类复杂任务很多开源库仍然偏爱 SGD with momentum因为它在充分调参后的泛化性能往往比 Adam 更稳。这个差异不值得争执实用主义的角度就是小规模实验用 Adam 快速验证正式项目就可以尝试 SGD。第二个是学习率调整策略。我会在训练过程中用torch.optim.lr_scheduler.CosineAnnealingLR或者 StepLR 做学习率衰减。前几次实验没有加学习率调度器训练到后半段 loss 就一直卡在 0.3 左右很难继续下降加了余弦退火之后最后几轮的 loss 还能稳定往下走测试准确率也涨了一个多点。学习率这种东西越到后期越决定模型能不能收敛到比较优的区域。4.3 训练策略与性能优化训练策略方面我的个人经验是先用较小的规模做一轮冒烟测试比如只跑几个 batch确认 loss 能下降、准确率不是随机水平再把整个训练流程跑完。这样能省下大量调试时间。很多人一上来就开全量训练结果代码里有维度错误跑到一半才报错浪费几个小时。关于 batch size需要记住一个经验规律batch size 越大梯度估计越稳定但 GPU 显存占用也越高。如果你从 64 改成 128最好把学习率也按比例调大一些比如从 0.01 调到 0.02这就是线性缩放规则。当然这个规则不是绝对的要配合学习率预热来用否则一开始学习率太大会让 loss 直接爆炸。CIFAR-10 这种小数据集上我常用的 ResNet-18 配置是这样的batch size 128、初始学习率 0.1配合预热、momentum 0.9、weight decay 5e-4、训练 100 epoch。在单张 RTX 3060 上大概二十分钟能跑完一轮完整的训练测试准确率能到 93% 到 94% 左右。如果你数据集不是 CIFAR 而是 ImageNet 子集需要按图片尺寸把学习率和数据增强策略重新调一遍。我每次训练都会顺手把 train loss、train acc、test acc 打点记录下来。这一步比很多人想象的重要只有曲线放在一张图里你才能直观地判断模型到底是欠拟合、过拟合、还是学习率设置不对。等到后面学目标检测时这种可视化习惯能帮你少走很多弯路。5. 训练不收敛、显存溢出常见问题与排查技巧5.1 模型不收敛怎么排查训练 loss 不下降是入门阶段最常遇到的问题原因五花八门。我的排查顺序通常是先看数据预处理有没有问题比如标签是否对齐、标准化是否做错再看模型输出层和损失函数是否匹配比如多分类用了 BCEWithLogitsLoss 却没改输出维度最后检查学习率学习率太大会发散太小会原地踏步。这里分享一个我踩过的坑有一次训练图像分类loss 始终在 1.5 左右下不去当时以为是模型结构写错了排查了一整天。最后发现是数据加载时shuffleFalse模型每个 epoch 看到的样本顺序完全一样梯度更新产生了某种周期性震荡。把shuffle打开之后loss 马上就开始下降了。这种问题在课程里只是一句话但实际遇到时真的会很折磨。梯度爆炸和梯度消失也是常见现象。典型表现是 loss 变成 NaN或者训练初期准确率就非常高、随后突然崩掉。排查时可以把每个 batch 的梯度范数打印出来观察。如果梯度范数非常大就在模型里加梯度裁剪clip_grad_norm_(model.parameters(), max_norm5.0)梯度裁剪不解决所有问题但能避免训练过程直接崩溃尤其是后面训练 RNN 时非常有用。5.2 显存溢出与训练速度慢显存溢出是最容易解决的“高级问题”。优先检查输入图片的分辨率是不是设置得太高batch size 能不能减少其次看模型里是不是有中间变量被保留了。PyTorch 中torch.no_grad()在推理时一定要用不然显存会被临时计算图占满。我自己的经验是相同效果下优先用小 batch size 配合梯度累积而不是直接买更大显存的显卡。梯度累积的思路是把多个 batch 的梯度加在一起再更新一次参数实际效果等同于增大了 batch size代码上只需要控制每隔几步才调用一次optimizer.step()。训练速度方面一个很容易被忽略的点是数据加载瓶颈。如果你的 GPU 使用率一直上不去但 CPU 已经满载那多半是 DataLoader 的num_workers设置太小。把它从默认值调成 4 或者 8并把pin_memoryTrue打开训练吞吐量往往会立刻提升。这些小细节课程里不一定展开讲但在实际项目中比调模型结构还要见效快。5.3 复现结果不一致的问题很多人做复现实验时会发现同样的代码、同样的数据跑两次结果不完全一样甚至和别人论文报告的数字有差距。这里的主要原因有三个。第一是随机性。深度学习训练涉及数据加载顺序、权重初始化、Dropout 和 BN 的随机行为甚至 CUDA 的卷积算法选择都会带来微小差异。如果你需要严格复现可以在代码开头设置多个随机种子并且把torch.backends.cudnn.deterministic True打开。但即使这样显卡型号不同、CUDA 版本不同仍然会有微小差异。第二是评价指标的计算方式。比如多分类的准确率是全局平均还是逐类别平均数据预处理是否用了同样的均值和标准差都会导致数字对不上。看论文的时候尤其要注意它的评测协议。第三是训练配置不一致。batch size 不同会影响 BN 的统计量学习率策略不同会影响最终收敛点。如果你发现复现结果和课程中展示的差距很大先对照超参数再怀疑代码正确性。这个排查习惯能帮你避免很多无意义的自我怀疑。把笔记变成自己的东西每次刷完这一段课程我最大的感受是真正重要的不是记住了 ResNet 有几层、YOLO 把图片分成几乘几的网格而是建立了一种“读模型”的能力——看到一个结构能本能地去想它解决了什么问题、代价是什么、如果换一个场景它还适不适用。我强烈建议你准备一个自己的代码仓库把这部分课程的示例代码按前面那张知识地图分目录组织每跑通一个实验就在目录里写一个几行字的 README记录数据和效果。这个过程会比“看视频”痛苦但它才是把“视频里的深度学习”变成“你自己的深度学习”的关键一步。别着急刷完第 51 到第 111 集值得你用一到两个月慢慢啃。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价