尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CNN目标检测计算链路全解析:从输入图到输出框的每一步

CNN目标检测计算链路全解析:从输入图到输出框的每一步 搞深度学习这么久经常有人拿着“CNN目标检测”这个概念来问我这东西到底是怎么算出来的网上教程一大堆但大多数要么只讲YOLO怎么调参要么只给公式不解释过程真正把“一张图进去怎么一步步算出目标框和类别”这件事讲透的还真不多。我最早接触CNN目标检测的时候也有同样的困惑。看论文感觉自己懂了一到自己写代码调模型又觉得哪哪都对不上。后来自己去推了一遍前向传播的计算过程又动手跑了几个开源项目才把这块彻底捋顺。这篇东西我就想做一个实实在在的“计算示例”。从一张输入图片开始推算卷积层里特征图尺寸怎么变、参数量怎么算、锚框怎么匹配、损失怎么算、mAP怎么来以及训练的时候到底吃不吃GPU。整个过程我会拿真实数字走一遍尽可能让流程直观到可以对着算。适合谁看刚接触目标检测的算法新人准备面试想补基础的同学还有那些用YOLO训练过但没搞懂内部原理的朋友。阅读大概需要十五分钟建议拿纸笔跟着算一遍收获会比干看大得多。1. 先把“CNN做目标检测”这件事拆清楚1.1 目标检测到底是什么和单纯分类有什么区别目标检测的任务一句话概括在图中找到所有感兴趣的目标并告诉程序每个目标是什么、在哪里。这句话拆开看就三个问题图里有什么类别、图里哪儿有位置、有几个数量。这跟图像分类有本质区别。分类只回答“这张图是猫还是狗”目标检测要回答“图里那只猫在哪、那只狗又在哪”。很多初学者会把目标检测理解成“分类定位”的简单组合但从计算角度看难度是成倍增加的。分类网络最后一层输出一个概率向量就行了检测网络要同时在空间上输出大量候选区域的结果还要处理“一个位置可能没有目标”的情况。换句话说分类问题是一对一的映射检测问题是一对多的映射。也正因为如此目标检测模型的计算量通常远大于分类模型。同样输入一张224x224的图ResNet50分类前向一次大约40亿次浮点运算而一个轻量级检测模型动辄上百亿次。这个账后面我会详细算。1.2 CNN凭什么能接住目标检测这个活儿CNN在分类任务上的成功核心在于它用卷积核在图像上滑动自动学习从边缘到纹理再到部件和整体的层次化特征。这个“滑动查看局部”的机制恰好天然适配目标检测的“目标出现在图中任意位置”这个特性。想象一下一张图片里有一只猫猫可能出现在左上角也可能出现在右下角。全连接网络处理这种位置变化是很吃力的因为它的输入是展平的一整条向量猫在不同位置像素完全不同。但CNN的卷积核是共享权重的同一组卷积核在图上所有位置都滑一遍不管猫在哪儿都能被同样的“猫特征检测器”抓到。这个性质叫平移等变性是CNN能用来做检测的根本原因。实际检测模型就是在CNN提取的特征图上做文章。特征图可以理解为原图在不同语义层级的“压缩版地图”网络越深特征图上每个位置看到的原图范围就越大语义信息越强。目标检测框架也基本形成了共识结构先用CNN骨干网络提特征再接检测头做分类和回归区别只在于检测头怎么设计、候选框怎么产生。2. CNN目标检测的核心计算链路2.1 输入预处理从图片到张量目标检测的第一步不是直接进网络而是预处理。这一步经常被忽略但它直接决定了后面所有计算能不能对上。一张真实的图片比如1920x1080的JPEG在计算机里是三个通道的像素矩阵。模型接收的输入却往往要求固定尺寸比如640x640x3。这里面有两个计算动作缩放和归一化。缩放就是等比例把长边或短边resize到目标尺寸多余部分填充灰色。注意如果原图是横向的直接拉伸成正方形会改变目标的长宽比导致检测不准。YOLO系列的letterbox方式不会做纯拉伸而是保持比例缩放后用固定像素值填充这个细节虽然不影响前向计算但会影响标注框的坐标映射。归一化则是把像素值从[0, 255]映射到[0, 1]或[-1, 1]。别小看这一步它的作用是把数值范围收敛让模型训练的梯度更稳定。实际代码里经常看到img / 255.0这行背后的道理就在这。预处理完毕图片就从HWC形状变成了模型输入要求的BCHW形状B是batch size。假设我们用batch size为1跑推理输入张量形状就是1x3x640x640这个张量就是所有后续计算的起点。2.2 卷积层计算一次标准卷积的手算示例CNN最基础的计算单元是卷积。我拿一个具体的数字例子来走一遍大家就能明白特征图每一格是怎么来的。假设输入是3通道的特征图尺寸为32x32即形状为3x32x32。卷积核设置为3x3大小输出通道数为16步长为1padding为1。输出特征图的尺寸公式是$$H_{out} \frac{H_{in} 2 \times padding - kernel}{stride} 1$$带入数字$H_{out} \frac{32 2 \times 1 - 3}{1} 1 32$。也就是说这个卷积层输出特征图尺寸还是32x32但通道数从3变成了16。再看每个输出格子的具体计算。输出特征图上某一个位置的值比如第0行第0列是输入图上对应3x3局部区域、3个通道、共27个像素值分别与28个权重相乘再求和后加偏置。这里28个权重指的是27个卷积核权重加1个偏置项。所以一次卷积操作的计算量大约是输出特征图的每个格子要做27次乘加运算16个输出通道、32x32个空间位置总计 $32 \times 32 \times 16 \times 27 \approx 44.2$ 万次乘加。这还只是一个卷积层。这个数字放到整个模型里看会非常庞大这也是为什么训练目标检测模型几乎离不开GPU。CPU不是不能跑但同样的计算量一张中端显卡能比CPU快几十倍。稍后我会专门算一笔GPU显存的账。2.3 特征图尺寸与感受野决定“看多大范围”很多新手在配模型网络结构的时候最头疼的就是特征图尺寸怎么变。其实核心就是把握住两个东西每个卷积层或池化层怎么改尺寸以及下采样倍数。还是沿用上面的设置如果后面接一个stride为2的3x3卷积padding为1那么输出尺寸就是 $\frac{32 2 - 3}{2} 1 16$。这就是一次经典的下采样把特征图的宽高各减半。下采样倍数决定了最终检测特征图的尺度。假设输入640x640总共进行了5次stride为2的下采样特征图就变成20x20。这意味着最终特征图上每个点对应原图32x32的区域。配合3x3卷积每个点能看到的原图范围远大于32x32这就是感受野的概念。感受野影响的是“检测头能依据多广的上下文做判断”。小目标检测难就难在浅层特征的感受野太小无法结合周边上下文区分目标和噪声深层特征的感受野够大但空间分辨率太低小目标在特征图上可能已经退化成比一个像素还小的信息。多尺度特征融合比如YOLO的FPN结构就是来解决这个矛盾的。2.4 池化与多尺度小目标为什么难检测池化在过去的目标检测网络里经常出现作用是进一步下采样并提升平移不变性。最大池化就是取2x2窗口里的最大值计算简单但非常有效。现在的检测骨干网络更倾向于用stride大于1的卷积替代池化因为卷积的参数可以在训练中学习比固定操作的池化更能适配具体任务。多尺度是目标检测绕不开的话题。一张航拍图里地面车辆可能只有20x20像素而近处的车能到200x200。如果只用单一尺度的特征图做检测必然顾此失彼。FPN特征金字塔的思路是自顶向下把深层语义信息往浅层传同时保留下采样的连接。计算上相当于多了几次上采样和横向拼接的卷积操作但带来的是多个尺度的检测精度同步提升。小目标检测专项优化的模型比如YOLOv8的P2检测头本质上就是在FPN基础上再往浅层加了一层更大分辨率的特征图代价是显存和计算量的进一步上升。3. 从骨干网络到检测头完整前向流程3.1 骨干网络特征提取的流水线目标检测模型的第一步是让一张图通过骨干网络Backbone。骨干网络的作用就是把原始像素转换为语义特征丰富的特征图。以YOLOv8为例它的骨干网络基于CSPDarknet结构输入640x640x3的图会输出三个尺度的特征图分别是80x80、40x40、20x20通道数分别为64、128、256。这三个尺度分别负责检测小、中、大目标。骨干网络的参数有多大以YOLOv8n为例总参数量大约在310万左右。相比之下YOLOv8x总参数量超过6800万。参数量直接关联模型文件大小和计算量这也是为什么nano版本能在边缘设备上跑而x版本必须依赖强GPU。从计算链路的视角看骨干网络就是把原始图像逐步抽象成“在哪里有什么东西”的特征分布。越靠近输入端的层保留更多细节和位置信息越靠近输出端的层包含更多类别语义。这也是为什么分类头的输入取深层特征而定位头需要结合浅层特征才能把框画准。3.2 区域提议与锚框机制在特征图上“找地方”最早的两阶段检测网络Faster R-CNN使用RPN区域提议网络来生成候选框。一阶段检测器则走得更激进直接在特征图的每个位置预置一组锚框或直接回归省掉建议阶段换取速度。锚框机制的计算逻辑值得展开。在YOLOv3/YOLOv5这些基于锚框的模型里每个特征图格子会预设3个不同长宽比的锚框三个尺度加起来就是9个锚框。20x20的特征图每个格子3个锚框就要产生1200个预测框。加上40x40的4800个和80x80的19200个单张图共有25200个候选框。每个候选框在训练时需要和真实标注框计算IOU判断它是正样本还是负样本还要学习去修正自己的位置偏移量。拿到一张标注了100个目标的图25200个候选框里可能只有几百个能被认定为正样本其余全是负样本这个正负样本极度不平衡的问题是训练目标检测模型的一个核心难点。YOLOv8弃用了锚框机制改为anchor-free方式让模型直接预测目标中心点到四条边的距离。计算上少了一步“和预设锚框匹配”的逻辑但核心思想不变在特征图的每个位置去判断“这个当前位置是不是目标中心如果是离目标边界有多远”。3.3 分类分数与边界框回归检测头的两组输出检测头是CNN目标检测计算链路里的最后一个环节。它接收骨干网络输出的特征图通过几个卷积层输出两组必要信息分类分数和边界框回归参数。分类分数的形状是$B \times (类别数 1) \times H \times W$其中加1是背景类anchor-free模型也有类似设计。边界框回归参数通常是$B \times 4 \times H \times W$4对应中心点xy和宽高wh的偏移量。以80x80的特征图为例如果模型要检测COCO数据集80个类别每个位置输出84个值全部展平就是537600个输出值。输出层的计算量不大瓶颈全在骨干网络和特征融合部分。训练时这两组输出要和真实标注算损失。推理时则通过sigmoid把分类分数转为0到1的概率把回归偏移量解码为实际坐标再做后处理得到最终检测结果。3.4 后处理NMS去重合并的纯计算模型输出的25200个候选框里同一个目标往往被多个相近的框命中。这是CNN目标检测非常习惯的“过度预测”需要后处理把冗余框去除NMS就是干这件事的。NMS的完整计算流程分四步走第一步按置信度从高到低排序所有候选框。第二步取最高置信度的框与所有置信度更低的框计算IOU。第三步将IOU大于阈值的低置信度框抑制掉IOU阈值通常取0.45或0.5。第四步对剩余框重复这个过程直到处理完所有框。IOU指的是两个框的交集面积与并集面积的比值。假设预测框和真实框重叠了长方形区域交集面积除以两个框的合计面积减去交集面积就是IOU。IOU越高说明两个框重合度越高。NMS有一个常见变体叫Soft-NMS把被抑制的框置信度按IOU连续降低而不是直接丢弃适合目标密集重叠的场景。另外YOLOv5以上的版本支持跨类别NMS放在同一批次处理能减少类别之间的重复框冲突。4. 损失函数与训练计算模型怎么学会检测4.1 边界框回归损失和分类损失训练目标检测模型损失函数由两部分组成定位损失和分类损失。定位损失常用的有L1、smooth L1以及IOU系列的GIoU、DIoU、CIoU。CIoU是YOLO系列的标配它除了衡量两个框的重合度还惩罚中心点距离和长宽比不一致。计算时CIoU等于IOU减去归一化中心点距离和长宽比惩罚项。这类损失的好处是梯度方向更明确不会像纯L1那样在框完全不相交时梯度消失。分类损失在检测模型里更常用的是二元交叉熵而不是多分类softmax交叉熵。原因在于同一个位置可能同时属于多个类别比如“车”和“轿车”多标签分类用二元交叉熵更合理也便于配合Focal Loss处理正负样本不平衡问题。总损失就是把分类损失和回归损失按权重相加YOLO类模型通常还会加上目标有无的置信度损失。实际操作中常用参数是cls_loss_weight0.5、box_loss_weight7.5这个比例不是拍脑袋定的而是平衡两类任务的学习速度、防止某一项任务主导训练。4.2 正负样本匹配与困难样本挖掘训练中决定哪些候选框去监督模型的规则叫正负样本分配。早期Faster R-CNN的做法很简单和任意真实框IOU大于0.7的候选框为正样本IOU小于0.3的为负样本中间的直接忽略。YOLOv5的分配策略不同它根据GT框与锚框的最优宽高比筛选正样本宽高比小于4的才纳入。YOLOv8则采用TaskAlignedAssigner同时考虑分类分数和回归质量选择更符合任务目标的样本。困难样本挖掘是训练稳定性的一大保障。一阶段检测器天然有海量负样本Xavier论文里提出Focal Loss就是为了把训练焦点集中在难分样本上它通过调节因子降低易分样本的损失权重。这个模块具体参数化上alpha常取0.25gamma取2.0很多开源项目直接沿用这个默认配置效果稳定。4.3 训练目标检测到底需要GPU吗显存怎么估算这是群里被问爆的问题。我的回答是训练用CPU能跑但实用性极低推理用CPU还行但实时性要看模型大小。先算推理。YOLOv8n在640x640输入下单张图大约需要80亿次浮点运算。目前主流CPU单核浮点能力大概在每秒50亿次左右但是实际利用率非常低跑一张图可能要1到3秒甚至更慢。而一张主流GPU如RTX 3060单张图推理耗时约20毫秒差距是50倍以上。训练比推理更吃显存。显存消耗的大头不是参数而是中间特征图、梯度、优化器状态。以batch size为16训练YOLOv8s为例640分辨率理论估算显存消耗大概是模型参数和梯度占2GB优化器状态占4GB特征图和中间激活占6GB到8GB加起来大致需要12GB到14GB的显存。用8GB显存卡强行训练也不是不行把batch size降到4开启梯度累积一样能跑就是慢一些。经验习惯是能上GPU就上GPU显存不够优先调小batch size再用梯度累积补回等效batch size而不是盲目改小输入分辨率。改输入分辨率会影响锚框的尺度适配和目标的像素大小牵一发动全身。5. 目标检测训练过程中的评价标准5.1 评价指标的三个层级IOU、置信度、类别目标检测的评价指标不是一个数而是一整套围绕IOU和置信度展开的统计体系。先理清基础概念。预测框和真实框做IOU计算判断是否算作“匹配成功”。通常IOU阈值取0.5即IOU大于0.5即可判定这次的预测是正确命中。这个阈值取多少直接影响评价结果挑剔一点可以设0.75。置信度要达到多少才算一个“有效预测”评价的时候会遍历所有可能的置信度阈值观察性能随阈值的变化而不是固定看某个数值。这就是为什么目标检测的评价指标经常以曲线或若干统计量的形式出现。类别维度上每个类别的AP是分开计算的。比如COCO数据集80类就对80个类别分别算AP再取平均得到mAP。类别间样本不均衡的时候从各类别AP差异能一眼看出模型短板。5.2 mAP的计算流程与FPSmAP的全称是mean Average Precision计算流程分五步走第一步模型输出所有预测框每个框包含类别、置信度、坐标。第二步按置信度降序排列所有预测框。第三步依次遍历每个预测框与所有真实框计算IOU如果IOU大于阈值且类别一致且这个真实框没被匹配过记为TP否则记为FP。第四步累加TP和FP数量计算每个置信度截止点下的精确率和召回率。第五步画出P-R曲线并计算曲线下面积得到AP所有类别AP取平均就是mAP。很多人会混淆mAP和FPS。mAP衡量“准不准”FPS衡量“快不快”。FPS即每秒能处理多少帧图像它受硬件、输入分辨率、模型大小、batch size多重影响。工程上经常在mAP和FPS之间做取舍比如YOLO系列从v3到v8的演进本质上就是在不断优化这一组权衡曲线。实际项目里还有个容易被坑的地方同样一个模型在不同IOU阈值下mAP差异很大。COCO的官方评价是mAP0.5:0.95即从0.5到0.95按0.05步长取10个IOU阈值分别计算mAP再取平均整体数值通常比mAP0.5低十个百分点以上。如果你的项目对框的精准度要求很高比如料箱空满检测这种工业场景建议重点关注mAP0.75这个指标。5.3 小目标检测的专项评价AP_small小目标检测是现在目标检测的难点热点。COCO标准把小目标定义为像素面积小于32x32的目标中等目标为32x32到96x96之间大目标为大于96x96。在这个定义下小目标检测的评价维度就是AP_small。工业界有个通用体会模型在正常目标上能到50%的mAP一到小目标上可能只剩10%。原因我之前说过深层特征图分辨率不够小目标在深层已经丢失了空间信息。改进小目标检测实操层面有几个方向。一是提高输入分辨率代价是显存占用翻倍。二是增加浅层P2检测头让模型专门在更高分辨率的特征图上检测小目标。三是做数据增强把大图切成若干小图分块检测最后合并结果。每个方向都有代价实际项目中需要根据小目标的分布密度来选择组合方案。6. 实操环节手把手搭一个CNN目标检测流程6.1 环境准备与关键配置我自己常用的环境组合Python 3.10、PyTorch 2.0、CUDA 11.8、Ultralytics包。显卡从RTX 3060到4090都跑过显存低于8GB的卡建议用nano或s模型。搭建流程的第一步是准备数据。以鸟类目标检测数据集为例目录结构通常如下bird_dataset/ images/ train/ val/ labels/ train/ val/标签文件是TXT格式每一行代表一个目标包含类别id、归一化中心点x、中心点y、宽、高。拿一张1920x1080的图目标中心在(960, 540)宽300高200画个框对应TXT内容为0 0.5 0.5 0.15625 0.18518这个归一化坐标是模型能直接消费的数据格式编写标注脚本时最容易出错需要注意宽高要分别除以原图宽和原图高不能都除以最大边。数据准备好后需要写一个YAML配置文件描述路径、类别数和类别名。YOLO训练脚本基本都靠这个文件定位数据内容非常简单但容易踩坑train: bird_dataset/images/train val: bird_dataset/images/val nc: 1 names: [bird]6.2 训练参数选择与训练过程中的评价训练目标检测模型最常用的命令就一行但里面藏了挺多学问yolo detect train databird.yaml modelyolov8n.pt epochs100 imgsz640 batch16这行命令里的几个关键参数逻辑是epochs是训练轮数小数据集100轮足够batch受显存限制显存不够就先减半imgsz是输入分辨率640是通用基线显存充裕可以试960小目标提升明显。训练过程中要怎么看模型到底学得怎么样这里有一个新手容易犯的错误只看loss数值。loss下降说明模型在拟合训练集但不代表泛化能力就强。真正值得盯的是验证集上的mAP指标这两个是不同层次的信息。正确的习惯是训练时打开验证集评价开关每个epoch结束后自动跑一遍mAP如果训练loss还在降但验证mAP开始停滞或下降说明开始过拟合了该提前停止。优化器的选择上YOLO默认使用SGD很多人会换成AdamW。SGD在小数据集上泛化更好AdamW收敛更快但对学习率更敏感。如果训练的loss出现大幅震荡先检查学习率和batch size的匹配关系一般推荐batch越大学习率可以适当提高。6.3 推理部署与模型导出训练收敛后会得到best.pt权重文件。这一步很多初学者容易忽略后续的模型导出问题在GPU上验证好的模型要部署到端侧或服务器通常需要做格式转换yolo export modelbest.pt formatonnx imgsz640ONNX格式是目前跨平台部署最通用的中间格式。导出后可以用ONNX Runtime在CPU上跑也能继续转成TensorRT在NVIDIA显卡上推理加速。同一个模型从PyTorch切换到TensorRT推理速度往往能再翻一倍。部署到边缘设备时还需要考虑半精度量化。把FP32的权重压成FP16甚至INT8模型文件变小、推理加快但精度会有小幅下降。量化后一定要重新跑一遍测试集的mAP确认精度丢失在可接受范围内再上线。我见过不少项目在量化后精度掉到不可用原因是对负样本学习不到位所以量化前先用FP16的模型把置信度阈值调好再量化时影响会小很多。7. 常见问题与排查技巧实录7.1 训练loss正常但检测结果很差这类问题的典型特征是训练曲线漂亮loss一路下降但拿到实际图片上跑全是漏检和错检。别急着归咎于模型绝大多数情况下是数据问题或评价体系不对。排查的第一个方向是背景与目标的对比度。很多公开数据集的目标在画面里非常突出但实际业务场景的目标和背景融为一体模型学到的特征在部署环境里失效。解决方案是增加真实场景数据或者做亮度扰动、色彩抖动等增强。第二个方向是anchors的设置。用YOLOv5以上版本时如果你改过imgsz最好重新聚类一下anchors。默认anchors是在640分辨率下对COCO数据集聚类的换到自定义数据或不同分辨率匹配度可能很差。Ultralytics训练脚本会自动做anchor自适应调整但版本不同行为不同手动检查一下没坏处。7.2 显存不足的四步排查法训练时报CUDA out of memory是每个炼丹师都经历的痛。我处理这个问题有一套固定流程第一步batch size减半省下的是最立竿见影的显存。第二步关闭训练时不必要的中间量保存比如梯度检查点相关选项。第三步开启混精度训练FP16能省一半激活值显存。第四步降低输入分辨率这是最后一招因为会直接影响检测精度。显存不足还有一个隐藏触发点数据加载线程数过大。DataLoader的num_workers开太高CPU把数据往GPU搬运速度快于GPU处理速度时显存中堆积的未处理数据会持续积压表现为峰值显存异常。合理经验是num_workers设为CPU核心数的一半并且开启pin_memory。7.3 小目标漏检率高的常用补救手段小目标漏检通常是“特征丢失”和“样本稀疏”共同作用的结果。针对性补救手段按优先级排序是提高输入分辨率是第一优选择相当于给小目标分配更多像素其次是数据层面的切图策略对长宽比大的图像切成小块重叠推理并合并能保留小目标的高频信息最后才是调整网络结构比如增加P2检测头或用较小stride的骨干网络。如果条件允许多尺度训练是最稳的综合方案。训练时每隔若干个epoch随机切换输入尺寸比如640、800、960交替让模型见过不同尺度的目标推理时泛化能力会好很多。训练时间增加的代价也很明显基本是线性增长需要自己权衡。还有一个容易忽略的问题标注质量。小目标在标注时非常容易产生坐标偏移一个200x200目标标偏10像素可能只影响2%的面积但一个20x20的小目标标偏了3像素IOU就会急剧下降。训练前建议对所有小目标数据抽检一遍标注框这个工作不能省。近年来鸟检、无人机航拍检测这类场景都很火但大家在小目标检测上的拦路虎其实都很一致。如果你正在训练自己的检测模型结果不理想先把评测指标里的AP_small调出来看别只看总分这样才能定位问题到底出在哪个尺度上。说到底CNN目标检测的计算链路并不神秘就是一个在不同层次特征图上持续精化的过程。动手把一个示例的每个环节亲手算一遍踩过几个坑比看十篇教程都有用。希望这篇能把大家从调参工升级到理解原理的那一步。
返回列表