做目标检测的人,多少都绕不开 YOLOv3 和它背后的 Darknet。就算是现在各种新模型层出不穷,很多做算法验证、嵌入式部署,甚至课程作业的同学,还是会选择在 Darknet 里把 YOLOv3 的测试流程完整跑一遍,再算清楚那堆评价指标。你可能会问,测试有什么好写的,不就是加载权重跑一下?但真正上手就会发现,从“测试”到“评价指标”之间隔着好几个容易踩坑的环节。权重要不要转?输出框坐标怎么理解?GT 标注怎么对齐类别?mAP 脚本到底按不按 Pascal VOC 的标准算?这些问题如果没理顺,最后算出来的指标,可能连自己都不信。
这篇文章我就把自己在 Darknet 下测试 YOLOv3、以及计算评价指标的全过程整理出来,会把环境准备、图片视频摄像头三类测试方式、IoU/精确率/召回率/mAP 的原理和踩坑、指标脚本的实战流水线,还有常见问题排查一次性讲清楚。内容按“能直接抄作业”的标准来写,适合刚开始接触 YOLOv3 目标检测、准备评估自己模型效果的开发者参考,也适合那些已经跑出结果但被 mAP 值卡住的人。
1. YOLOv3 测试前需要理清的三件事
1.1 Darknet 是哪种框架,为什么选它做测试
Darknet 是一个用 C 语言写出来的轻量级深度学习框架,设计目标就是简单、可移植、依赖少。它的优势不在于像大型框架那样什么功能都有,而在于“目标检测专用”这件事上做到了极致的直接。编译 Darknet 基本只需要一个支持 OpenMP 的编译器,如果要用 GPU 推理就加上 CUDA 和 cuDNN,不需要额外安装一堆运行时库。
我当时选它做 YOLOv3 测试,主要图三点。第一,YOLOv3 的官方权重和配置文件都是基于 Darknet 格式发布的,在 Darknet 里做测试不需要做权重格式转换,拿到就能用。第二,Darknet 的预测过程非常容易观察,你加不加-ext_output、-thresh这些参数,输出格式都会直接反应在终端里,非常适合调试。第三,Darknet 的代码量小,改起来方便,想输出中间层特征、想自定义 NMS 逻辑,都能直接改源码重新编译。
当然 Darknet 也有它的问题,比如对 Windows 的原生支持比较弱,在 Windows 下要借助 WSL 或者容器虚拟化才能跑得顺。所以如果你的主力环境是 Windows,我建议直接用一个 Linux 环境做测试,或者至少装好 WSL 之后再做,省得把时间花在环境治理上。
1.2 测试的本质:要验证模型的哪三个维度
很多人把“测试”简单理解成“让模型跑几张图看效果”,这其实只是最外层的动作。真正做模型测试,本质上是在回答三个问题:识别得对不对、识别得稳不稳、识别得快不快。
识别得对不对,靠的是对单张图片的定性和定量分析。定性就是看可视化检测结果,框的位置、类别标签、置信度值是否符合直觉;定量就是后面要说的 mAP、精确率、召回率。识别得稳不稳,要看模型在不同尺度、不同光照、不同遮挡情况下的表现,这一般需要准备一个覆盖面足够广的验证集。识别得快不快,则要看 FPS 和推理延迟,这在嵌入式场景里尤其关键,一个模型如果 mAP 很高但推理速度跑不动,在实际工程里也没有意义。
明白了这三个维度,你就知道测试不只是一个动作,而是一条完整的数据链路:从测试集准备,到模型前向推理,到输出解析,到指标计算,最后到性能分析。任何一个环节出了问题,你得到的结论都可能是错的。
1.3 测试与评价指标为什么是绑在一起的
在 Darknet 里,测试和评价指标是同一个流程的两端。Darknet 本身只是一个推理引擎,它负责把图片变成检测框输出,并不会告诉你这个模型到底好不好。好不好,需要靠另一个环节来判断,也就是评价指标。
所以你需要先把 Darknet 的输出处理成“能被评价脚本读取的格式”,然后再计算指标。这就是为什么网上会有一堆解释 mAP 计算的脚本,却不能直接塞进 Darknet 里一键完成的原因。Darknet 负责“生成预测”,评价脚本负责“对比真相”,两者通过中间文件衔接起来。这个衔接就是整个流程中最容易出错、也最值得花时间做规范的地方。
2. 完整测试的实操流程
2.1 环境准备与权重获取
先编译 Darknet。在 Linux 环境里,操作路径大致是:
git clone 你的Darknet源码地址 cd darknet make如果你有 NVIDIA GPU,建议打开 Makefile,把GPU=1、CUDNN=1设好再 make。编译完会生成一个darknet可执行文件,这就是后续所有测试操作的核心入口。
权重文件要去对应项目的官方 release 页面找,常见的是yolov3.weights。这里提醒一句,如果你用的是yolov3-tiny或者自己训练的权重,训练时的配置文件必须跟当前测试配置文件保持同一个网络结构,否则加载权重时会报维度不匹配。自己训练的模型还要注意类别数的问题,配置文件中filters的值有固定计算公式:
filters = 3 * (classes + 5)比如你的数据集有 10 个类别,那么最后一层卷积的滤波器数必须是3 * (10 + 5) = 45,少一个都加载不上。这个公式我强烈建议贴在电脑旁边,每次改类别数第一件事就是按它检查过滤器数。
测试前最好再确认一下配置文件里的输入尺寸。YOLOv3 默认width=416、height=416,如果你的测试图片不是正方形,Darknet 会直接拉伸到 416x416,这会直接影响小目标检测的表现。后面如果发现小目标框不准,可以把输入尺寸调到 544 或 608 再试试,代价是速度下降。
2.2 图片、视频、摄像头的三种测试方式
在 Darknet 里,最常见的单张图片测试命令是:
./darknet detect cfg/yolov3.cfg yolov3.weights data/dog.jpg执行完会在终端打印检测到的类别、置信度和坐标,同时在当前目录生成一张predictions.jpg,可以打开来看可视化效果。
如果一次想测多张图,就不要用detect,换成:
./darknet detector test cfg/coco.data cfg/yolov3.cfg yolov3.weights -ext_output -dont_show然后从标准输入逐行输入图片路径,每输入一条就得到一行预测输出。把一批图片路径先写进valid.txt,再用重定向就能完成批量测试:
./darknet detector test cfg/coco.data cfg/yolov3.cfg yolov3.weights -ext_output -dont_show < valid.txt > result.txt这里-ext_output的意思是输出带图片名的扩展格式,后面做指标计算时非常关键。-dont_show表示不弹窗口,适合服务器批量跑。
测试视频和摄像头则用 demo 模式:
./darknet detector demo cfg/coco.data cfg/yolov3.cfg yolov3.weights test.mp4 ./darknet detector demo cfg/coco.data cfg/yolov3.cfg yolov3.weights 0第二个命令中的0表示摄像头索引,如果只有一个摄像头,一般就是 0。这条命令我踩过一次坑,编译时如果没有把 OpenCV 加进去,demo 模式是跑不起来的。所以编译 Darknet 前先确认 Makefile 里的OPENCV=1已经打开。
2.3 读懂 YOLOv3 的输出结果
YOLOv3 的输出通常有两种形式。一种带-ext_output,会先列出图片路径,再列出检测信息;另一种不带,直接输出每个框的五个信息:类别编号 置信度 坐标或者left_x top_x right_x bottom_x 置信度 类别,具体格式取决于你编译的版本。
坐标信息尤其要注意。有些版本输出的是中心点坐标加宽高,也就是(x, y, w, h);有些版本输出的是左上角和右下角坐标,也就是(x1, y1, x2, y2)。前者适合直接画框,后者在计算 IoU 时反而更顺手。我建议你拿到一次输出后,用 OpenCV 或者 PIL 画出来对比一下,确认坐标格式再写解析脚本,否则后面指标计算会全部错乱。
另外,YOLOv3 的输出框是经过 NMS 处理之后的最终框,不是原始候选框。NMS 会抑制重叠度高的低置信度框,保留局部最优的检测框。你要理解,输出结果里每个框的置信度并不等同于分类概率,它是“这个框里有目标的概率”和“类别概率”的综合乘积,因此即使类别概率很高,如果框位置偏差大,置信度也会下降。
3. 评价指标从原理到计算
3.1 IoU:两个矩形框的重合程度怎么算
目标检测里最底层的评价指标是 IoU,全称是 Intersection over Union,也就是交并比。它计算公式是预测框与真实框的交集面积除以并集面积:
IoU = 两个框的交集面积 / 两个框的并集面积例如,真实框是一个 80x80 的正方形,预测框是一个 100x100 的正方形,两者重叠了一个 60x60 的区域。交集面积为 3600,并集面积为 6400 + 10000 - 3600 = 12800,所以 IoU 约为 0.28。这个数值太低,通常不会被判定为正样本。
在 Pascal VOC 的评估标准里,默认 IoU 阈值是 0.5,也就是说预测框和真实框的重合程度超过 50%,才认为这次检测命中。后续 MS COCO 赛制把标准提高到 0.75,并且还会额外计算一批跨 IoU 阈值的平均,比如 mAP@0.5:0.95,表达的是从 0.5 到 0.95 每隔 0.05 取一个阈值算 mAP,再取平均。
有个容易混淆的点:训练时的 positive match 阈值一般也是 0.5,但计算指标时如果对 IoU 阈值随意改动,得到的结果会有很大差异。我见过有人用同一个模型,把 IoU 阈值从 0.5 改到 0.75,mAP 直接从 70% 掉到 40%。所以报告指标时必须写明 IoU 阈值,否则别人都不知道你的模型到底什么水平。
3.2 精确率和召回率:漏检和误检的平衡
目标检测的评价不是一个非黑即白的问题,它需要同时回答两个问题:检测出来的框有多少是对的?真实存在的目标有多少被找到了?
对应的两个指标就是精确率 Precision 和召回率 Recall。
精确率的公式是:
Precision = TP / (TP + FP)召回率的公式是:
Recall = TP / (TP + FN)其中 TP 表示真正例,即预测框与某个真实框的 IoU 达到阈值;FP 表示假正例,即预测框没有与之匹配的真实框;FN 表示假负例,即真实框没有被任何预测框命中。
举个例子,某个验证集里一共有 10 个目标,模型输出了 8 个预测框,其中 6 个匹配上了真实目标,剩下 2 个框是误检。那么 TP=6,FP=2,FN=4,Precision = 6/(6+2) = 0.75,Recall = 6/(6+4) = 0.6。这说明它检测出的目标虽然多数是对的,但漏检了 40% 的真实目标。
这两个指标天然是冲突的。如果调高置信度阈值,预测框会变少,误检变少,但漏检会变多,于是精确率上升、召回率下降。相反,调低阈值,召回率上升、精确率下降。所以不能只看一个指标,要放在一起看。
3.3 mAP:最常用的综合指标
mAP 全称是 mean Average Precision,即所有类别平均精度。它把精确率和召回率合成一个数值来评价模型整体表现。
具体计算方法是:对每个类别,把所有预测框按置信度从高到低排序,依次计算 Precision 和 Recall,画出 PR 曲线,然后计算曲线下的面积,也就是该类的 AP。最后对所有类别的 AP 取平均,得到 mAP。
如果你用的是 COCO 数据集的评估方式,AP 实际上就是所有类别的 mAP,而 COCO 风格的 AP 还会区分 AP50、AP75 和 AP@0.5:0.95。普通 YOLOv3 爱好者最常看的还是 mAP@0.5,也就是 VOC 风格。它的好处是计算直观,阈值宽松,适合快速比较模型;坏处是它不能反映框的精细程度,一个框明明边缘不齐,只要 IoU 超过 0.5,就会被计算成 TP。
我采用一个模拟验证集给你做个示例:假设验证集有 5 类目标,某模型在 IoU=0.5 下各类 AP 分别为 76.3%、71.8%、68.2%、74.5%、69.7%,那么 mAP@0.5 就是这些值的平均,约 72.1%。这个值在通用目标检测里算中等偏上的表现,如果用 COCO 的 mAP@0.5:0.95 格式,通常会比这个低 20 到 30 个百分点。
3.4 F1、FPS、各类别和混淆信息:更完整的评价维度
只用 mAP 评价模型其实不够,我还会同时记录一组补充指标。
首先是 F1 分数,它是精确率和召回率的调和平均,适合在精确率和召回率同样重要的时候做参考。F1 的公式是:
F1 = 2 * Precision * Recall / (Precision + Recall)如果 F1 高,说明模型在漏检和误检之间找到了较好的平衡点。
其次是各类别的 AP 明细。只报平均 mAP 会掩盖某些类别的缺陷。我遇到过模型整体 mAP 有 68%,但其中“很小尺寸的物体”这个类别的 AP 只有 15% 的情况,这就要单独去看。统计时把每个类别的 AP、Precision、Recall 都存下来,比只看 mAP 有价值得多。
最后是 FPS 和单帧推理时间。FPS 表示每秒处理多少帧,计算方法是用 1000 除以单帧推理时间(毫秒)。例如单帧耗时 23ms,FPS 大约是 43.5。但在 GPU 上测试 FPS 时,要留意 batch size 的影响,用 batch=1 测的是真实部署延迟,用大的 batch size 测的是吞吐量,两者不能混着说。
4. 指标计算脚本的实战踩坑记录
4.1 我用的指标计算流程
我在 Darknet 下算 YOLOv3 指标,走的是下面这条流水线,你可以直接按它复现。
第一步,准备图片列表。把验证集里所有图片路径写入valid.txt,每行一个绝对路径:
find /data/dataset/val -name "*.jpg" > valid.txt这一步要注意图片后缀一致性,如果你的标注里全是.jpg,列表里就不能混入.png,否则 GT 和图片对不上。我因为后缀统一问题吃过一次亏,图片加载了 80%,其余全被跳过,导致最后 mAP 计算只有 0。
第二步,批量推理生成预测文件:
./darknet detector test cfg/my.data cfg/yolov3.cfg my.weights -ext_output -dont_show < valid.txt > det_result.txt这里的my.data文件里必须写清楚names = /path/to/classes.names,里面是所有类别的名字,顺序要和训练时一致。预测文件里通常混杂着图片序号提示符,因此解析时要过滤掉这些行,只保留检测信息。
第三步,准备 GT 文件。如果你是 YOLO 格式的标签,每个 txt 里内容是:
class_id center_x center_y width height坐标都是归一化的,需要先转成绝对像素坐标,转换公式是:
abs_x = center_x * img_width abs_y = center_y * img_height abs_w = width * img_width abs_h = height * img_height然后按 Pascal VOC 评估脚本需要的格式,把 GT 整理成{图片名: [框列表]}的结构。这里有一步我建议别省:用脚本把 GT 框画在对应图片上,跟原图叠加检查一遍,确认坐标转换正确。标注是 yolo 格式而评估脚本却以为是 voc 格式,是新手最常犯的错误之一。
第四步,运行评估脚本计算指标。你可以参考 Pascal VOC 的voc_eval逻辑写自己的评估脚本,也可以直接使用现成的适配 Darknet 输出的评估代码。基本逻辑是:对每个类别,先把预测按置信度降序排列,然后逐个预测匹配 GT 框,IoU 达到阈值的记为 TP,否则为 FP,最后按累计 TP/FP 计算 Precision 和 Recall,再积分得到 AP。
4.2 容易出错且最难发现的五类问题
第一类,类别名不匹配。你的classes.names顺序是person, dog, cat,但预测输出里类别编号是 0、1、2,评估脚本里却用了另一套顺序,结果全部对不上,mAP 就会变成 0。排查方法是打印一个类别的预测结果,人工检查类别编号对应的名字是否一致。
第二类,预测输出被污染。批量运行 Darknet 时,result.txt里除了检测结果还有像Enter Image Path:这样的提示行,以及因为模型崩溃或异常产生的错乱行。解析时如果没做行内容校验,这些脏数据会被当成预测框,导致 FP 暴涨。我建议每行至少校验一次“长度”和“数量”,不符合六项或七项字段的直接跳过。
第三类,坐标没有经过正确的归一化或反归一化。Darknet 换行输出有时会直接给出像素坐标,有时给出的是缩放后的坐标,视编译版本而定。你必须画图确认。这一步错了,IoU 怎么算都是错的,后边所有指标都没有参考价值。
第四类,GT 框维度为零。某些数据集手工标注时会有w=0或h=0的无效框,评估时这些框会让 IoU 计算报错,或者被跳过。我处理的办法是在加载 GT 时,直接丢弃长或宽小于 1 像素的框,并单独记到日志里,方便回头修复标注。
第五类,结果文件没有清空。每次重新推理前,要确保det_result.txt是空的,或者直接设置为新文件,否则上一次的残留结果会和本次的混在一起。这个问题非常隐蔽,指标可能只差零点几个点,但足以让你怀疑模型本身出了问题。
5. 常见问题与排查技巧实录
作为参考,我把实际测试中遇到的高频异常和排查方向整理成了速查表,遇到相似现象时可以直接按表操作:
| 现象 | 可能原因 | 排查建议 |
|---|---|---|
| 正常图片推理能出框,但 mAP 为 0 | 类别名不匹配、GT 路径为空、预测解析失败 | 打印一个类别的预测结果和 GT 样本,逐字段对比 |
| 检测框偏移明显 | 图片被拉伸但 GT 坐标没按同尺度变换 | 确认输入尺寸、标注是否归一化,再人工画框检查 |
| 小目标几乎检不到 | 输入尺寸过低、训练时没有小目标增强 | 把width和height调到 544 或 608 再测 |
| FPS 忽高忽低 | 显存或内存不足、后台有其他负载 | 用nvidia-smi监控显存,单独开进程跑测试 |
| demo 模式打开摄像头黑屏 | 摄像头索引错误或 OpenCV 未编译 | 先试0,再试1,确认 Makefile 打开了OPENCV=1 |
| 批量测试时程序崩溃 | 某张测试图损坏或格式异常 | 在图片列表里逐张检查,找到崩溃的那张删除或更换 |
| 不同批次测试 mAP 波动大 | 验证集太乱、阈值不统一、NMS 参数不一致 | 固定测试集和推理超参,并统一 IoU 阈值 |
还有一个我觉得特别值得说:如果你刚训练完模型,先别急着整套指标流程跑完。可以先挑五六张典型图片,可视化预测框和 GT 框,肉眼确认映射关系是对的,再全量跑指标。我习惯把预测框标成红色、GT 框标成绿色,叠加在同一张图里看。这一步看起来粗糙,但确实能提前发现 80% 的格式问题。
5.1 一个典型排查案例
有一次我用测试脚本在本地算 mAP,结果一个目标检测模型算出 mAP@0.5 = 2.3%,可单张图片可视化看起来效果不差,错误检测也不多。我排查了一下午,最后发现原因:我用的评估脚本里对预测框的坐标排序要求是(x1, y1, x2, y2)的左上右下格式,但脚本内部没有反转中心点格式的坐标,预测文件硬被解析成了像素格式。一番对日志后,我用 OpenCV 画框才发现预测框坐标其实是中心点加宽高,但评估脚本把中心的 x 值当成了左上角 x 值,导致 IoU 全部错算。
这个教训是:不要相信任何脚本“默认格式”,一定要自己画图核对,至少核对一遍边界框坐标和类别标签。否则算法模型没毛病,指标脚本却把所有结果带偏了。
5.2 关于置信度阈值的统一问题
计算 mAP 时,我强烈建议不要在推理阶段就过滤掉低置信度框,而是保留所有带置信度的检测结果,让评估脚本在累加过程中自己处理阈值。
为什么这么做?因为 mAP 的计算本身就是在“不同置信度阈值下”对 Precision 和 Recall 的积分。如果在 Darknet 推理时就把-thresh 0.25设成了过滤条件,那么低于 0.25 的框直接消失,PR 曲线就在开头少了一段,mAP 会被低估。更合理的做法是推理时把阈值设得很低,例如-thresh 0.001,让评估脚本去决定哪些框有效。
这一点在比较两个模型时尤其重要,模型 A 用 0.25 阈值跑,模型 B 用 0.001 阈值跑,两者的 mAP 根本没有可比性。统一超参,才敢比较结果。
6. 用更系统的方式沉淀测试结论
走到这里,基本流程已经通了。最后我想分享一些关于“怎么沉淀测试结论”的经验,这些通常不会写在框架文档里,但对实际项目很有帮助。
我会建议你每跑一轮测试,固定下来三份东西。第一份是测试环境信息,包括 Darknet 版本、GPU 型号、CUDA 版本、配置文件里的输入尺寸和 batch size。第二份是推理超参,特别是置信度阈值、NMS 的 IoU 阈值。第三份是测试集信息,包括图片数量、类别数量、GT 框数量。这三套信息都完整,别人才能复现你的测试结果,否则你只报一个 mAP 数字,别人复现出来的结果大概率不一样。
另外,测试过程中的日志比最终指标更值钱。我在实践中踩过很多次坑,最后养成一个习惯:推理阶段就输出足够详细的可解析日志,不要只给最终数字。例如每张图片的检测数、每张图片的平均置信度、每个类别命中的 GT 数,这些中间量能让你快速定位是模型中了还是数据中了。
如果你打算长期做目标检测,建议把测试和评估脚本都整理成一个固定的脚本仓库,以后每次训练完模型,一行命令就能得到完整报告。这个前期投入看起来有点麻烦,但在后续十几个模型迭代里,收益会非常显著。我就是靠这一套流程,每次迭代模型都能在三分钟内拿到全部指标,再也不用重复经历手工算指标的痛苦。