计算机视觉深度学习这条路,走到目标检测这一步才算真正进入落地场景。前面如果你已经跟着这个系列把神经网络架构、图像分类这些基础过了一遍——用PyTorch搭过CNN、训过分器、理解了反向传播,那第四部分就是把这些能力拧成一股绳:从“这张图里有什么”进阶到“东西在哪儿、是什么”。目标检测算法、PyTorch训练管线、backbone架构选型,还有Python编码者最容易踩的那堆工程坑,都会在这一篇里集中出现。
如果你是Python出身、刚开始搞计算机视觉,这一篇尤其合适。它不纠结于数学推导,重点讲清楚检测任务的核心思路、架构演进逻辑,以及怎么用PyTorch把一套检测训练流程跑起来。读过之后你能直接对着代码动手,而不是站在理论门外干看。
1. 从“分类”到“检测”:思路先转过来,代码才好写
1.1 分类器回答不了“在哪里”
图像分类的输出是单个标签,整张图被归纳成一个概率分布。这个设定在很多真实场景里是不够用的:画面里有三个人、两只猫、一辆自行车,分类模型只能告诉你“这图大概率有猫”,却没法告诉你猫在哪个位置。这个问题从计算机视觉诞生那天就存在,一直到深度学习时代才被真正解决。
检测任务的标准输出是一组边界框(bounding box),每个框包含四个坐标值和一个类别。坐标通常有两种表示:[x_min, y_min, x_max, y_max],或者中心点加宽高[x_center, y_center, width, height]。无论哪种,核心都是回答“物体占据图像的哪块区域”。
初学者最容易犯的思维错误,是把检测理解为“先分类、再找框”两步走。传统视觉里确实有人这么干,用滑窗把图切成无数小块,每块送入分类器,再把得分高的小块合并。但滑窗的代价是灾难性的:假设输入是 1000×1000,窗口是 100×100,步长 50,那需要跑 361 个窗口,每个窗口都要过一次网络。实际中窗口数量会膨胀到几十万,算力根本扛不住。
深度学习时代换了个思路:既然网络本身能提取特征,那就让网络一次性输出所有候选区域,再做筛选和修正。这就是检测算法两条技术路线的共同起点。
1.2 检测的两条路线:两阶段与单阶段
理解整个目标检测领域,最省力的方式就是记住两条技术路线。
两阶段方法,以 Faster R-CNN 为代表,第一段先生成一堆可能包含物体的候选区域(region proposal),第二段再对每个候选区域做精细分类和坐标回归。这个“先粗筛、再精修”的过程很像招聘:先筛简历,选出能干活的候选人,再安排面试确定最终人选。优点是精度高,因为精修阶段有充足的计算资源;缺点是速度慢,两段网络串行,实时性通常不理想。
单阶段方法,以 YOLO、SSD 为代表,输入图像后直接一步输出所有框的位置和类别,中间没有显式的候选区域生成环节。它像是有经验的面试官,不筛简历,直接凭直觉现场判断。速度极快,视频实时检测基本靠它;缺点是对小物体、密集重叠场景容易漏检。
这两种路线在损失函数、训练策略、后处理细节上有很大差异,但底层都离不开一个共享的骨架网络——也就是第二部分要讲的 backbone。理解了 backbone 和它的特征提取逻辑,再看检测头(detection head)就轻松得多。
2. 神经网络架构这条暗线:backbone和特征金字塔
2.1 ResNet成为默认骨干是有原因的
几乎所有现代检测算法都把图像分类网络当作特征提取器,即骨干网络。早期检测研究用过 VGG,后来几乎全换成了 ResNet。VGG 结构简单、感受野规整,但层数加深后梯度衰减严重,训练 19 层以上的网络就需要额外技巧。ResNet 在 2015 年提出时做了一件看起来不起眼、影响却极其深远的事:给每一层加了一条“捷径”,让梯度可以跨层直接回传。
这个残差连接的物理意义,是说网络的每一层只需要学“变化量”,而不是重新学一个完整映射。类比到实际工作:如果你已经会写前端,那学一门新的后端框架,只需要补上新框架的差异部分,而不是把编程从头学一遍。ResNet 的每一层都承担“修正上一层的输出”这个任务,而不是从零开始构造特征,所以能稳定堆到 50 层、101 层甚至更深。
对你实际编码来说,选 backbone 时最直接的判断标准就是:项目对精度要求高就上 ResNet-50 或 ResNet-101,对速度要求高就考虑轻量网络。你不需要自己从零定义这些网络,torchvision 里直接有现成实现,两行代码就能加载预训练权重。
2.2 多尺度特征与特征金字塔
分类网络天然有一个“尺度悖论”。浅层特征图分辨率高、保留的空间信息多,但语义信息弱;深层特征图语义强,但分辨率低,小物体在深层图上可能已经缩成一个点甚至消失。检测任务偏偏什么尺度的物体都有,只用最后一层特征图做预测,小目标几乎必挂。
于是就有了特征金字塔网络(FPN)。FPN 做的事情通俗讲就是“高低层特征互相抄作业”:高层特征往下传语义,低层特征往上补细节,每一层都融合成一张同时具备语义和空间分辨率的特征图,然后在每一个层级上分别做预测。这个操作对检测精度的提升非常显著,尤其对小目标。
我见过不少同学自己动手搭检测网络时,直接拿分类网络的最后一层特征图接检测头,然后抱怨 mAP 上不去。这不是检测头的问题,而是缺少多尺度特征融合。现在主流检测库基本默认都带 FPN 结构,自定义网络时一定别把这个模块省掉。
2.3 选backbone的实际考量
选择 backbone 不只是看精度榜单。实际项目里有几个隐藏成本:显存占用、训练速度、部署难度。
举一个我实际遇到的例子:某项目需要在嵌入式设备上跑实时检测,设备显存只有 4G。如果用 ResNet-101 做 backbone,光图像编码一次就要吃掉大几百兆显存,根本跑不满分辨率。最后换成了 MobileNetV3 结构,参数量降了一个数量级,速度提上来了,精度掉的幅度在可接受范围内。
另外要注意预训练权重的来源。检测模型通常需要一个在 ImageNet 上预训练过的 backbone 作为初始化,直接随机初始化从头训检测任务,收敛极慢且效果差。PyTorch 里加载预训练权重很简单,但一定要确认载入时的键名匹配,否则会静默地随机初始化,等你发现的时候几个小时的训练时间已经浪费了。
3. PyTorch训练检测模型:一套能直接用的流程
3.1 Dataset与DataLoader的细节
检测模型的 Dataset 类,核心工作是三件事:读图像、解析标注、返回张量。听起来简单,但标注解析这一步最容易出问题。常见标注格式是 VOC 的 XML 和 COCO 的 JSON,两者坐标体系不同,一个是绝对像素坐标,一个是相对归一化坐标。Dataset 里必须统一成模型需要的格式,推荐统一转成[x_center, y_center, width, height]的归一化坐标,因为大多数检测模型内部都用这个格式。
DataLoader 里有一个容易忽略的参数:collate_fn。分类任务里每张图独立,直接堆成 batch 没有歧义;检测任务里每张图的物体数量不同,bounding box 列表长度不齐,默认的 collate 函数会把数据拼挂。这时需要自定义collate_fn,把图像和标注分开处理,图像堆成四维张量,标注保留为列表。
再提一个影响训练效率的细节:pin_memory=True和合适的num_workers。pin_memory 可以把数据放到锁页内存里,减少 CPU 到 GPU 的传输耗时;num_workers 决定数据预处理的并行进程数。很多人在小的 demo 数据集上感觉不到区别,但一旦数据量大、图像分辨率高,这两个参数能差出两三倍的训练吞吐。
3.2 训练循环、损失函数与调参
检测任务的损失函数通常是多任务加权求和:分类损失加边界框回归损失。分类损失一般用交叉熵,边界框回归损失推荐用 Smooth L1,它在误差小时退化为 L2、在误差大时退化为 L1,对离群点不那么敏感。多任务损失需要配权重,常见设置是分类权重 1、回归权重 1,但具体项目需要自己调。
训练检测模型时,学习率策略比网络结构更影响最终效果。我强烈建议用两段式的:前几个 epoch 用很小的学习率做 warmup,把模型从预训练状态平稳带到任务状态;之后再用余弦退火或阶梯下降逐步降低。没有 warmup 直接开大学习率,常见的后果是 loss 在前几百步剧烈震荡,甚至直接变成 NaN。
梯度裁剪也是训练稳定性的保障。检测模型的回归分支偶尔会产生极大的梯度,尤其是标注质量差、存在离群框时。PyTorch 里一行clip_grad_norm_(model.parameters(), max_norm=10)就能限制梯度范数,防止训练崩掉。这是个成本极低、收益极高的操作,值得养成习惯。
如果显存不足,不要急着换小模型。可以用梯度累积:每几步累加梯度再统一更新,等效于扩大了 batch size;或者直接开 PyTorch 的自动混合精度(AMP),显存占用能降三分之一左右,速度还能提升。AMP 的注意点是某些算子需要特殊处理,但检测模型的标准结构基本都兼容。
3.3 验证、可视化和模型存档
检测模型的验证是一套独立流程:模型推理出预测框,对预测框做 NMS 去重,然后和标注框计算 IoU,再按 IoU 阈值判定 true positive 和 false positive,最后算 mAP。这里的关键是 NMS 阈值和 IoU 阈值,通常 NMS 阈值取 0.45~0.5,mAP 的 IoU 阈值取 0.5,分别对应 mAP50 和 mAP50:95。
可视化比数值指标更能发现模型问题。每个验证阶段挑几张图,把预测框画到原图上保存。只盯着 mAP 数字,很难定位问题是漏检、误检还是定位不准;一看图就明白了,漏检通常是召回率不够,误检通常是置信度阈值太低,定位不准可能是回归分支权重偏小或 NMS 阈值不对。
模型存档不要只保存最后一个 epoch 的权重。建议在验证集上跟踪 mAP,每提升一次就覆盖保存一次 best 模型,同时保留最近几次 checkpoints。这个习惯在训练时间长的项目里能救命,中途训练崩溃也不至于从头再来。
4. 目标检测算法怎么选:Faster R-CNN、SSD、YOLO对比
4.1 两阶段:Faster R-CNN的结构和工作原理
Faster R-CNN 是两阶段检测的标准模板。它由三块组成:backbone 提特征,RPN 生成候选框,RoI Head 做精细预测。RPN 是整篇论文最核心的变动,它让“生成候选框”这件事也变成了可训练的网络模块,彻底告别了传统的选择性搜索。
RPN 的工作方式是在特征图每个位置铺一层 anchor,也就是预先定义好的一组不同尺度、不同长宽比的参考框。训练时这些 anchor 跟真实标注框算 IoU,正样本负责学“靠近真实框”的偏移量,负样本负责学“这是背景”。推理时 RPN 输出一堆带物体分数的框,经过 NMS 筛选后进入下一阶段。
RoI Head 部分有一个关键设计叫 RoI Align。早期版本用的是 RoI Pooling,因为两次量化操作会损失像素级精度,对小目标影响明显。RoI Align 用双线性插值解决对齐问题,几乎成了检测模型的标准配置。如果你自己实现 Faster R-CNN,直接上 RoI Align,别浪费时间重复走老弯路。
两阶段模型的精度优势,核心在于第二阶段有充分的上下文信息,可以把候选框内的特征放大重提,分类和回归的质量都更高。代价是结构复杂、训练资源占用大,两段之间的中间特征需要保存,显存开销明显高于单阶段方法。
4.2 单阶段:SSD与YOLO
SSD 的设计思路是把检测头直接接到 backbone 的多个不同层级上,每个层级的特征图负责检测对应尺度的物体。它没有显式的候选区域生成,而是直接用 anchor 匹配真实框并预测。SSD 在精度和速度之间取得了当时的最佳平衡,至今仍是入门单阶段检测的好教材。
YOLO 系列走得更远。第一代 YOLO 把图像划分成网格,每个网格直接预测若干个框。结构极简,推理极快,但早期版本对小目标和重叠物体表现很差,anchor 不匹配、网格粒度太粗都是原因。后续版本持续加入 anchor 机制、多尺度预测、跨层特征融合,才把精度追上来。
单阶段模型训练时有个通病:正负样本严重不平衡。一幅图上可能有几十个物体,但 anchor 或网格预测点有几千上万个,绝大部分是背景。如果你的模型总是预测不出东西,或训练 loss 下降很慢,问题往往在这里。解决方案包括在线难例挖掘,或者像后来的焦点损失函数(Focal Loss)那样,降低易分类样本的权重,让模型把注意力放到难样本上。
4.3 项目选型建议
怎么选检测算法,我的判断标准就三条:实时性要求、目标大小分布、开发周期。
实时性要求高,比如视频流、无人机、工业产线,直接选 YOLO 或其变体,轻量 backbone 加输入分辨率调低,一般能达到几十毫秒内完成一帧。精度要求优先、且目标尺寸差异大,比如卫星图像里的微小目标检测,优先考虑两阶段模型加 FPN 结构,RPN 阶段对小目标的召回能力通常更好。
开发周期短、团队刚入门,我建议直接基于成熟的检测工具箱起步,把精力放在数据和质量调优上,而不是从零写训练循环。检测模型的边界框回归很依赖良好初始化,预训练权重和标准数据增强流程比你自己折腾的版本靠谱得多。等整个流程通了,理解了各个模块的作用,再考虑自己改结构,风险会小很多。
5. 常见问题与排查技巧实录
5.1 标注数据和坐标转换的坑
检测项目里标注数据引发的 bug 最多。最常见的是坐标格式混用:标注是绝对坐标,代码里却按归一化坐标处理;或者图像是被 OpenCV 读进来的 BGR 顺序,可视化时却按 RGB 显示,导致框看起来没问题,但送入网络学习的颜色特征全部错位。
另外一类隐蔽问题是坐标越界和空标注。图像裁剪或缩放后,标注框可能有一部分落到图像外面;如果整张图的物体恰好被裁剪掉了,就出现完全没有真实框的样本。这类样本如果混在训练集里,回归分支会产生极大 loss。解决办法是在数据预处理阶段做一次清洗:过滤空标注样本,越界框做裁剪并检查剩余面积是否低于阈值。
5.2 显存与训练稳定性问题
显存溢出(OOM)的排查顺序要固定:先减小 batch size,这是最直接的杠杆;还没解决就降低输入图像分辨率,很多检测模型对分辨率的要求没那么高,512×512 和 1024×1024 的显存差距接近四倍;最后才考虑换小 backbone。要注意的是,通过梯度累积代替大 batch 时,应该同步调整 BN 层的行为,检测任务里 BN 统计量的稳定性会被小 batch 影响。
训练出现 NaN,先别怀疑网络结构,大概率是学习率过大或者标注里有无穷值。把学习率降到原来的十分之一重跑几个 step 试试,如果 NaN 消失,问题就在学习率;如果还在,就把标注数据里异常框、负值的坐标全部检查一遍。
5.3 mAP性能瓶颈的常见原因
模型 mAP 上不去,按我的经验,大概率不是网络结构不行。第一个要怀疑的是数据质量:标注框是否贴边、类别是否漏标。检测模型对标注的敏感度极高,一个漏标物体在训练时会被当负样本处理,相当于反向告诉模型“这里没东西”,特别伤召回率。
第二个要怀疑的是 anchor 配置。默认 anchor 的尺度和长宽比是根据 COCO 数据集得到的最优值,如果你的目标全是长条形或特别扁的物体,默认 anchor 可能匹配不上真实框。一个快速排查方式:统计训练集中所有标注框的宽高比和面积分布,如果和默认 anchor 明显不同,就重新聚类生成 anchor,效果立竿见影。
第三个问题是数据增强策略。很多人无脑套用分类任务的数据增强,比如水平翻转没有同步翻转标注框,导致框的坐标和图像内容对不上。使用检测专用数据增强时,记住一条铁律:任何对图像的几何变换,必须施加给标注框本身,剪裁时要处理越界部分,翻转时坐标也要同步翻转。
最后再分享一个我个人的工作习惯。每次开始检测项目之前,先写一个 30 行以内的脚本,把训练集里每个类别的样本数量、平均每张图物体数、标注框面积分布打印出来。这一步花不了 20 分钟,但能提前暴露类别不平衡、小目标过多、空标注等问题。很多团队花几周时间调模型,最后发现根因都在数据上,早点看清手里的牌,比盲目堆模型省时得多。