
1. 为什么先跑起来比跑得快更值得投入1.1 CPU推理不是退而求其次的选择很多人一提在 CPU 上跑 YOLOv5第一反应是没显卡才这么干。这个判断只对了一半。我接触过的实际场景里纯 CPU 跑检测的需求相当稳定工控机、边缘盒子、老旧的机架服务器、CI 流水线上的自动化验证、教学演示环境这些地方要么没有独立显卡要么装不了对应的驱动栈要么干脆不允许装。这时候 CPU 推理不是降级方案而是唯一能落地的方案。它的好处也很实在。没有驱动版本要匹配没有运行库要额外安装一套 pip 依赖装完就能跑换个机器复制过去照样能跑。做算法验证的时候这种确定性比峰值性能重要得多——你要的是每次跑出来的结果一致而不是今天快 30 毫秒明天崩一次。所以这一篇的定位很明确不谈加速框架不聊量化剪枝就是把 YOLOv5 的检测链路在 CPU 上完整跑通从加载权重到输出带框的图片中间每一步都拆开讲清楚。这是整个系列第三篇的核心任务前两篇把环境、依赖、权重这些前置事项处理完了现在该让模型真正吐出结果了。1.2 卡在第一步的人比想象中多我见过太多人在跑起来这一步反复打转。报错信息看不懂就去搜各种加速方案结果越搜越乱最后连基线都没建立起来。这是个典型的顺序错误。正确的节奏应该是先让流程闭环拿到一张带检测框的输出图记录下单张推理耗时然后才有资格谈优化。没有基线数据你后面做的任何调优都是盲目的——换个输入尺寸到底快了多少换后端到底有没有收益全靠猜。这也解释了为什么我把跑起来单独拎出来写一篇。跑通这件事本身不难难的是跑通的同时你要知道每一步在干什么出问题时能定位到具体环节。否则一旦报错只能靠搜关键词碰运气。1.3 本篇的前置条件与验收标准先把前置条件说清楚省得你中途卡住。Python 环境要有建议 3.8 到 3.10 之间PyTorch 装 CPU 版本安装命令里带上 CPU 专用的索引地址别装成带 CUDA 的大包白白多占几百兆YOLOv5 的仓库代码拉到本地权重文件提前下好放到weights/目录下别指望运行时自动下载网络一抖就前功尽弃。验收标准就一条执行一条推理命令能在输出目录里看到一张画好检测框的图片终端打印出每个目标的类别、置信度和坐标。做到了这一篇的目的就达到了。至于速度先别管后面单独一节算账。2. 三条跑通路径的选型与对比2.1 官方脚本路线验证环境的最短路径仓库里自带的detect.py是最省事的入口。它把权重加载、预处理、推理、后处理、画框、保存这一整条链路都封装好了你只需要传参数。对于第一次跑通的人来说这条路能帮你最快排除环境到底装没装对这个疑问。关键在于参数要传对。最容易被忽略的是设备参数不加这个参数脚本会去探测有没有可用的加速设备探测不到会回退但中间可能打印一堆警告干扰判断。所以干脆显式指定让它老老实实走 CPU。输入尺寸、置信度阈值、IoU 阈值这几个也建议显式写出来别依赖默认值——默认值不是不能用而是你写了才知道自己在控制什么。python detect.py \ --weights weights/yolov5s.pt \ --source data/images \ --img-size 640 \ --conf-thres 0.25 \ --iou-thres 0.45 \ --device cpu \ --project runs/detect \ --name cpu_baseline跑完以后去runs/detect/cpu_baseline/下面找结果原图会被复制一份并画上框。终端里会逐张打印检测到的目标数量最后给一个总耗时和平均每张的耗时。这个平均耗时就是你后面所有优化的参照系拿笔记下来。提示第一次运行会稍微慢一些因为权重加载和算子初始化都算在里面。测速的时候建议跑两次取第二次的数据更有参考价值。2.2 Python API 路线嵌入自有业务代码的正道脚本跑通以后绝大多数人的下一步是把它塞进自己的程序里。这时候再去调脚本就不合适了得用 Python 接口直接加载模型。常见做法是torch.hub.load本地模式下指定仓库路径和权重路径就能拿到一个可以直接调用的模型对象。import torch model torch.hub.load( ./yolov5, custom, pathweights/yolov5s.pt, sourcelocal ) model.conf 0.25 model.iou 0.45 model.to(cpu) results model(data/images/bus.jpg, size640) results.print() results.save(save_dirruns/api_demo)这条路的好处是返回的对象结构清晰.xyxy、.conf、.cls这些属性直接拿来做后续业务逻辑。比如你要统计画面里有几个人直接按类别筛一下计数要把坐标传给下游模块xyxy数组拿来就用。有一点要留意torch.hub.load在加载本地仓库时会执行仓库里的配置文件所以路径一定要指向你确认过的那份代码别随手写个相对路径导致加载了别的目录。另外模型对象第一次推理会带上初始化开销做批量处理时先把这一步预热掉。2.3 导出 ONNX 再推理跨运行时的验证通道如果你的目标环境里不方便装 PyTorch或者你打算后续换推理后端那导出 ONNX 是绕不开的一步。仓库里的export.py支持直接导出指定格式和输入尺寸即可。python export.py \ --weights weights/yolov5s.pt \ --include onnx \ --img-size 640 \ --opset 12 \ --device cpu导出成功后在权重同目录下会多出一个.onnx文件。这个文件可以用 ONNX Runtime 加载推理CPU 上的表现通常比原生 PyTorch 要好一些常见的量级是一到三倍的提升具体看你的 CPU 型号和算子覆盖情况。别把它当成必然结论你自己的机器上测出来的才算数。导出这一步有几个坑值得提前说。输入尺寸必须是 32 的整数倍因为网络里有多次下采样尺寸对不上会报形状不匹配opset 版本别选太高有些运行时的版本跟不上导出时的动态轴设置如果不开出来的模型就只能吃固定尺寸的输入后面想改尺寸得重新导。2.4 三条路径怎么选路径适用阶段上手成本灵活性建议官方脚本环境验证、批量处理成图最低低第一篇必跑Python API嵌入业务、二次开发中等高落地首选ONNX 导出换运行时、部署到精简环境中等中有性能诉求时再做我的建议是按顺序来先用脚本确认环境和权重都没问题再用 API 写自己的处理逻辑等这套跑顺了、有明确的性能诉求了再去折腾 ONNX。跳过前两步直接上 ONNX出问题时你分不清是模型问题、导出问题还是运行时问题。3. 一次完整 CPU 推理的逐段拆解3.1 权重加载优先用本地文件加载这一步看着简单其实决定了后面一堆事情。模型规模的选择直接影响耗时和精度YOLOv5 提供了从 n 到 x 五个量级参数量大致从两百万级到八千多万级不等。在 CPU 上我个人建议从 s 这个量级起步n 虽然快但精度损失在复杂场景下比较明显m 以上在普通 CPU 上单张就要接近一秒甚至更多做实时处理不现实。权重文件优先放本地。框架默认会尝试从网络拉取一旦网络不稳就是长时间的等待报错信息还不一定说得清。把文件下好放到位路径写死这是最稳的做法。加载完成后建议打印一下模型结构确认规模顺便看一眼输入输出形状。有些人拿到权重直接用结果发现是导出的中间格式跑起来形状对不上白折腾半天。3.2 预处理letterbox 到底在解决什么问题这一步是很多人忽略的关键环节。YOLOv5 要求输入是固定尺寸的正方形但现实中的图片长宽比五花八门。直接拉伸会把物体压扁变形检测精度掉得厉害。letterbox 的做法是等比缩放然后把空出来的部分用固定灰度值填满相当于给图片加了一圈边框。举个例子一张 1920×1080 的图要送进 640×640 的网络。等比缩放系数取 640/1920 和 640/1080 里较小的那个也就是 0.333缩放后是 640×360。然后在上下各补 140 像素的灰边凑成 640×640。这样物体比例没变只是画面里多了留白。理解这个机制很重要因为后处理要把坐标还原回去。网络输出的坐标是在 640×640 这个带边框的画布上的你得先减去填充量再除以缩放系数才能映射回原图。这一步算错了框就会整体偏移而且偏移量随图片尺寸变化看着像模型不准实际是坐标还原的数学错了。填充值也有讲究训练时用的是 114 这个灰度值推理时保持一致别自己改成 0 或者 255会引入分布偏移。3.3 前向推理与 NMS两个必须理解的环节前向推理本身没什么好说的把预处理好的张量送进网络拿回三个尺度的特征图输出。真正需要理解的是 NMS也就是非极大值抑制。模型输出的原始预测里同一个物体会有一堆重叠的框因为相邻的网格点都觉得自己检测到了。NMS 的作用就是把这些冗余框压掉只留最好的那个。流程是按类别分开处理先按置信度排序取最高的框然后把它和剩下所有框算 IoU也就是交并比超过阈值的认为是在描述同一个物体删掉再取下一个最高的重复这个过程。这里的关键参数是 IoU 阈值。设得高比如 0.7只有重叠度非常高的才被压掉结果是密集场景下框会重复设得低比如 0.3稍微挨着就被压掉人群或者堆叠物体就会漏检。0.45 是个常用的平衡点但如果你的场景里物体特别密集可以适当往上调。置信度阈值是另一道闸门在 NMS 之前就把低分框过滤掉。它决定了多确定才算数。0.25 是常用的宽松设置宁可多报也不错漏如果你的业务对误报敏感可以提到 0.4 甚至更高。这两道闸门配合使用先粗筛再精筛。我在实际项目里试过一个思路先用高置信度阈值拿到确定的目标再用低阈值拿一遍候选两次结果做合并去重。这个方法在漏检和误报之间找了个平衡代价是耗时翻倍要不要用看你自己的取舍。3.4 坐标还原与结果解析拿到 NMS 之后的框还差最后一步。框的坐标是归一化的数值在 0 到 1 之间是相对于网络输入的尺寸。要还原成原图像素坐标顺序是先乘回网络输入尺寸减去填充量除以缩放系数最后把超出原图边界的值裁掉。裁边这一步别省。填充导致的坐标有可能落在负值或者超出原图宽高不裁的话下游画框会报越界或者画出画面外。用简单的截断就行别搞复杂的插值。结果的类别索引也要注意模型输出的是索引数字需要对照类别名称表转成可读的标签。这个表通常跟权重配套别自己乱改顺序改了就对不上。如果你的应用只关心某几类可以在解析阶段就直接过滤省掉后面无谓的处理。4. 参数调优与耗时账本4.1 输入尺寸的取舍与耗时估算输入尺寸是性能影响最大的单一参数而且影响是非线性的。计算量大致跟边长的平方成正比也就是说从 640 降到 416理论计算量接近减半从 640 提到 1280计算量翻四倍。# 粗略估算相对计算量 base 640 for size in [320, 416, 512, 640, 800, 1024]: ratio (size / base) ** 2 print(f{size}: {ratio:.2f}x)这个估算只是量级参考实际耗时会受到内存访问模式、缓存命中率的影响不完全按平方走。但方向是对的。尺寸怎么选得看你的目标在画面里占多大。如果检测的是近处的人或者大件物品320 到 416 往往就够了如果是远处的小目标比如监控画面里的远处车辆那就得用 640 甚至更高否则目标在特征图上就剩几个像素网络根本认不出来。我的做法是拿一批标注好的样本用不同尺寸各跑一遍看精度和耗时的曲线拐点在哪。多数情况下你会发现在某个尺寸之后精度提升就变得很平缓而耗时还在往上涨那个拐点就是你的最佳取值。4.2 线程数与 CPU 亲和性PyTorch 在 CPU 上默认会用满所有核心但用满不等于更快。线程数超过物理核心数以后上下文切换的开销会吃掉一部分收益有时反而变慢。可以用torch.set_num_threads()手动控制。import torch import os import time for n in [1, 2, 4, os.cpu_count()]: torch.set_num_threads(n) # 这里放同一张图连续推理若干次取平均 print(fthreads{n}, avg{avg_ms:.1f}ms)实测下来有个规律小模型、小尺寸的情况下线程数收益到四核左右就基本饱和了大模型大尺寸时多线程收益更明显但也别超过物理核心数。另外如果你的程序本身是多进程架构每个进程都开满线程会互相抢资源这时候反而要限制单进程的线程数。还有一个容易忽略的点是推理时的梯度状态。推理阶段必须关掉梯度计算否则框架会保留中间结果用于反向传播内存占用和耗时都会明显上升。脚本里通常已经处理好了但你写自己的代码时得自己加。with torch.no_grad(): pred model(img)这一行几乎是零成本的高收益操作别漏。4.3 阈值组合的调优思路置信度和 IoU 这两个阈值不是独立调的得一起看。常见的做法是固定一个扫另一个画一条精度或者召回率的曲线出来找拐点。实际操作中我习惯这样先把 IoU 定在 0.45扫置信度从 0.1 到 0.6步长 0.05看误报和漏检的变化确定置信度以后再扫 IoU 从 0.3 到 0.7。两轮下来基本能找到适合你场景的组合。有个细节值得注意如果你的应用后面还要做跟踪或者计数阈值可以设得宽松一些让上游多给点候选后面靠逻辑去筛选如果是直接给人看的告警那就设严一点宁可少报也别天天误报误报几次用户就不信这个系统了。4.4 换后端的收益评估原生 PyTorch 跑通以后如果速度确实不够可以考虑换推理后端。ONNX Runtime 是比较通用的选择安装简单对 CPU 的优化做得不错。如果目标机器是常见的桌面或者服务器处理器还有一些针对特定指令集优化的后端可以试收益会更高一些但部署上多一层依赖。这里的原则是先测再换换了再测。别听别人说哪个快就直接上你的 CPU 型号、内存带宽、输入尺寸、模型量级都会影响结果。我见过同一个模型在两台配置接近的机器上不同后端的排名完全相反的情况所以自己的数据最可靠。测的时候注意控制变量同一批图片、同样的输入尺寸、同样的阈值、同样的线程数只换后端。跑之前先把第一次的预热推理排除掉不然测出来的数字不可比。5. 踩坑记录与排查速查5.1 环境类报错怎么定位最常见的是导入失败和版本冲突。装了一堆东西以后某些依赖互相不兼容报错信息往往指向一个看起来无关的模块。这时候别急着按报错去单独装某模块先看一眼已安装包的版本清单确认核心框架的版本和仓库要求是否匹配。另一个高频问题是权重文件路径。相对路径、绝对路径、当前工作目录三者的关系很容易搞混。我的习惯是一律用绝对路径或者用os.path拼出来别依赖执行时的工作目录。还有一类是权限和编码问题多见于路径里带中文或者空格。这类问题报错信息通常很含糊容易误判成模型问题。排查时先把路径换成纯英文短路径试一次能排除掉一大批干扰。5.2 结果异常怎么分类排查结果不对先分两类一类是压根没检测到东西一类是框位置明显偏了。没检测到先看置信度是不是设太高了临时降到 0.05 试试还不行就看输入图片本身是不是通道顺序搞错了比如 RGB 当 BGR 送进去颜色反了会严重影响浅色系的物体再不行就看归一化这一步均值方差有没有对上。框位置偏移八成是坐标还原的数学出问题。检查填充量有没有正确减掉缩放系数有没有用错方向。验证方法很简单拿一张正方形图片测如果正方形图准、长方形图偏那基本确定是 letterbox 相关的还原逻辑错了因为正方形图不需要填充绕过了那段逻辑。还有一种是框的数量对但类别全错这通常是类别名称表对不上或者模型的类别顺序跟你以为的不一样。把原始索引打印出来看看就知道了。5.3 速度慢的系统性排查慢是相对的先确认你的基线。一张 640×640 的图在普通四核笔记本 CPU 上跑小模型几百毫秒是正常范围如果到了一两秒那就有问题。排查顺序我一般这样走先确认梯度是不是关掉了再看线程数设置是不是只用了单线程然后看输入尺寸是不是被某个默认值顶到了很大接着看预处理是否有重复的拷贝操作比如在 Python 层做了一次缩放框架内部又做了一次最后才考虑换后端。一个常被忽略的耗时点是图片解码。如果你处理的是大尺寸的原图每次都要完整解码再缩放这部分开销可能比推理本身还大。做法是先用低开销的库做一次粗缩放把尺寸降下来再送进模型或者干脆预先处理好数据集。5.4 常见问题速查表现象可能原因处理办法导入模块报错依赖版本冲突查看版本清单重建干净环境权重加载失败路径不对或文件不完整改绝对路径校验文件完整性检测结果为空置信度阈值过高 / 通道顺序错临时降低阈值验证检查颜色通道框整体偏移letterbox 还原计算错误用正方形图对照测试类别标签错乱类别表与模型不匹配打印原始索引核对映射表单张耗时异常长未关闭梯度 / 线程配置不当加推理模式限制线程数首次运行很慢初始化开销跑两次取第二次数据换尺寸后报错不是 32 的整数倍改成 32 的倍数5.5 几个只有实际跑过才知道的细节说几个文档里通常不会写的。第一连续推理多张图片时第一张总是明显慢于后面这个差距在小图上能差出两三倍测速一定要跳过第一张。第二同一个模型在不同输入尺寸之间切换会触发重新编译或者内存重分配如果不做预热直接测那一张的数据会严重失真。做法是先用目标尺寸空跑一次再开始计时。第三CPU 推理的内存占用比想象中高尤其大尺寸输入的时候中间特征图占的空间不小。批量处理时不要一次把整批图都读进内存用生成器逐张喂进去更稳否则批次一大就容易吃满。第四输出目录的命名很容易重复导致覆盖。脚本默认会在同名目录后面加序号但如果你自己指定了固定路径要注意它会覆盖掉上次的结果。做对比实验时把实验参数写进目录名里比如带上尺寸和阈值事后翻记录能省很多事。我个人在这个环节上的体会是把每一步的输入输出都落盘存一份包括预处理后的图、原始预测、NMS 之后的结果。看起来占空间但每次出问题的时候这几张中间结果能让你在几分钟内定位到是哪一环出的岔子比对着日志猜快太多了。这套习惯我坚持用到现在几乎每次排查都能帮上忙。