第一次拿到一份带中文注释的 YOLOv5 工程包,很多人的第一反应是"这下省事了",结果真正跑起来才发现,省下的只是读注释的时间,该踩的坑一个没少。所谓YOLOv5中文版,在社区里并没有一个统一的标准定义,它通常指的是上游开源工程经过中文注释、中文文档补充、中文标签可视化改造之后的衍生版本,也有人把整合了标注工具、训练脚本、推理 Demo 的一键包统称为中文版。不管拿到的是哪一种,它解决的核心问题是一样的:把原本需要边查文档边猜的工程,变成能看懂、能改、能落地的东西。这篇内容我打算按"拆包—搭环境—准备数据—调参—改可视化—排故障—导出部署"这条真实的工作流走一遍,把每一个环节里那些文档上不会写、但实际会卡住人的地方摊开讲清楚。适合刚接触目标检测、准备用 YOLOv5 做第一个落地小项目的人,也适合已经能跑通 Demo、但在自己数据集上反复翻车的朋友。
1. 拆开"YOLOv5中文版"这个包:汉化究竟动了哪几层
1.1 汉化包通常由三块拼起来
很多人以为中文版就是把英文注释翻译一遍,实际上绝大多数社区版本改动集中在三个层面,理解这三层能帮你在出问题时快速定位该看哪里。第一层是代码注释层,包括train.py、detect.py、val.py、models/yolo.py、utils/*.py这些核心文件里的行内注释和函数说明被翻成中文,这一层只影响阅读体验,不影响运行结果,出问题也不需要怀疑它。第二层是文档与教程层,通常是仓库根目录多了几个中文 Markdown,或者配套的操作手册,把安装、训练、预测的流程重新组织了一遍,这一层的价值在于降低了入门门槛,但版本滞后是常态,文档里写的参数在代码里可能已经改了名字。第三层是功能性改造层,也是真正会让中文版和原版产生行为差异的部分,包括检测框上渲染中文类别名、日志输出中文化、标注工具汉化、数据配置文件支持中文类名等。
我个人的判断是:前两层随便用,第三层要格外小心。因为一旦有人动过utils/plots.py里的绘图函数或者utils/general.py里的日志函数,就可能在升级、迁移或者多人协作时埋下不兼容的种子。拿到包以后,先在仓库里找有没有改动记录,哪怕只是一个简单的README对比表,也比两眼一抹黑强。没有的话,就去看看detect.py和utils/plots.py的修改时间,这两个文件被动过,基本可以确定做过中文渲染改造。
1.2 注释版、文档版、可视化版的实际差别
为了把差别说清楚,我按自己用过的几种类型做个对照。需要强调的是,下面的评价只针对"改动范围"和"使用风险",不针对具体哪个仓库,因为这类工程包更新频率高,今天的情况和三个月后可能完全不同。
| 版本类型 | 典型改动 | 上手难度 | 主要风险 | 适合谁 |
|---|---|---|---|---|
| 纯注释版 | 核心 py 文件行内注释翻译 | 低 | 与上游版本脱节,注释描述的行为已变 | 想看源码但英文吃力的人 |
| 注释+文档版 | 增加中文 README、参数说明表 | 低 | 文档滞后于代码,参数名对不上 | 第一次跑通全流程的人 |
| 可视化增强版 | 改 plots.py 支持中文标签渲染 | 中 | 改动侵入绘图链路,升级易冲突 | 要出中文检测图做汇报的人 |
| 全流程整合包 | 内置标注脚本、数据划分、一键训练 | 中 | 封装过深,出问题难定位 | 只想快速出结果的小项目 |
从我的使用体验看,注释+文档版的性价比最高,因为它让你看懂工程但不动工程;而全流程整合包在第一次演示时体验极佳,一旦你要换成自己的数据、自己的场景,封装的壳子就会变成障碍,你得先搞明白它到底偷偷改了什么。我一般建议:整合包只用来做通识性了解,真正开始做自己的项目时,回到结构清晰的原版工程 + 自己写的中文笔记,这个组合最稳。
1.3 拿到手先做的一件事:对文件做差异比对
这一步很多人跳过,但我觉得它是整个流程里性价比最高的动作。具体做法很简单:把这个中文版和你选定的上游版本放到两个目录里,用任意文件比对工具(命令行 diff、编辑器的目录对比、版本管理工具的差异视图都行)跑一遍,重点看train.py、detect.py、utils/plots.py、utils/general.py、utils/dataloaders.py这五个文件。
如果你发现utils/dataloaders.py被改过,那就要特别留意数据加载链路,因为很多"训练能跑但结果不对"的问题都出在这里,比如有人为了支持某种特殊格式把图像读取从 OpenCV 换成 PIL,颜色通道从 BGR 变成 RGB 却没有同步调整预处理,模型照样训练、照样出框,只是精度会莫名其妙掉几个点,这种问题极难通过看日志发现。如果你发现utils/general.py里的check_img_size或increment_path被动过,那就留意输出目录的生成逻辑,避免训练结果被写到你意想不到的位置,白跑一轮。
提示:差异比对不只是找茬,更是给自己建立一份心理地图。知道哪些文件被动过,后面出问题时排查范围立刻缩小一半。
2. 环境与目录:中文版最容易在第一公里翻车
2.1 版本锁定的必要性不能省
YOLOv5 这类工程对依赖版本的敏感度比一般项目高得多,原因在于它同时依赖深度学习框架、图像处理库、可视化库和处理标注格式的解析库,四者之间任何一个版本漂移都可能引发连锁反应。我见过最常见的一类报错是张量形状相关的运行时错误,追到根子上其实是某次无意中升级了框架版本,导致某些算子的默认行为变了。
具体做法是:先把 CUDA 驱动对应关系确认清楚,再去装匹配的框架版本。这里不需要记复杂的对应表,只需要记住一条原则——先定框架版本,再让框架告诉你该配哪个 CUDA,不要反过来。安装命令里那个带版本号的索引地址,就是把框架版本钉死的关键,很多人图省事直接用默认源装,装出来的是最新版,和你代码里用的写法对不上,就开始怀疑人生。
# 示例:显式指定版本,避免"装到最新版"的默认行为 pip install torch==1.13.1 torchvision==0.14.1 --index-url <对应框架官方提供的镜像索引> pip install -r requirements.txt装完之后不要急着跑训练,先做一次最小验证:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "无可用设备")第二行输出为真、第三行能打印出你的卡型号,才算地基打平。如果第二行是假,别急着去改代码,先确认驱动和框架版本是否匹配,九成问题在这一步就能解决。
2.2 目录速查与各文件职责
中文版工程包的目录结构和原版基本一致,但因为你可能拿到的是经过整理的版本,花五分钟把每个目录的职责过一遍很有必要。我把实际排查中最常打开的几个列出来:
data/:数据集配置的 yaml 文件、类别名文件、示例图片都在这里。很多人把自己的 yaml 也扔这儿,其实放外面更清晰。models/:网络结构定义和不同规模的配置文件(n/s/m/l/x)。改结构、改检测头都在这。utils/:工具函数大本营,绘图、日志、数据加载、指标计算全在这。排查问题时打开频率最高的目录。weights/:预训练权重的默认落点。runs/:训练和推理的输出默认写在这里,每次训练生成一个递增编号的目录。runs/train/exp*/weights/:best.pt和last.pt就在这里,前者是按验证集指标挑出来的,后者是最后一轮。
一个很实际的提醒:best.pt和last.pt不一定谁更好。验证集指标最高的那一轮,在测试集上未必最优,尤其是小数据集上过拟合明显的场景。我一般两个都留着,用测试集分别跑一遍再决定上线用哪个,多花几分钟,能避免不少"线上效果不如训练日志"的困惑。
2.3 中文路径、空格路径与编码链路
这是中文环境下特有的问题,而且极其隐蔽。典型表现是:脚本跑起来没报错,但读到的图片数量是零,训练直接卡在数据集扫描阶段;或者标注文件读取失败,日志里提示某个文件不存在,你去目录里一找,文件明明在那儿。
根本原因是路径里的非 ASCII 字符在某个环节被用错误的编码解释了一次。工程里读文件的函数如果没显式指定编码,就会使用操作系统默认编码,中文环境下的某些系统默认编码并不支持 UTF-8 字符集,于是"读到了"但"读错了"。规避办法有两层:第一层是从根上不给自己找麻烦,工程根目录、数据集目录、权重路径全部用纯英文加下划线,哪怕多打几个字。第二层是补齐编码声明,把所有读 yaml、读 txt、读 json 的地方检查一遍。
# 容易出问题的写法 with open(cfg_path) as f: cfg = yaml.safe_load(f) # 稳妥写法 with open(cfg_path, 'r', encoding='utf-8') as f: cfg = yaml.safe_load(f)如果你拿到的是中文版工程项目,第二段写法大概率已经改好了;如果是原版,就得自己动手补。补的时候注意,不只是你写的脚本要补,工程内部读类别名文件的函数也要补,那个地方往往被忽略,结果就是训练日志里类别名显示成一堆问号。
注意:路径里带空格和带中文是两种不同的问题。空格问题通常表现为命令解析失败、参数被截断;中文问题表现为静默读错。前者报错明显,后者才是真正难缠的。
3. 数据侧:从标注到 data.yaml 的中文场景处理
3.1 YOLO 标签格式的坐标换算
这一块必须自己手算一遍,否则永远理解不了模型为什么学不会。YOLO 使用的标签格式是每行一个目标,包含五列:类别索引、中心点横坐标、中心点纵坐标、框宽、框高,后四个值全部除以图像宽高做了归一化,落在一到零之间。
假设你有一张 1280×720 的图,人工标注框的左上角是 (400, 200),右下角是 (800, 500),类别是索引 2。换算过程是:框宽 = 800-400 = 400,框高 = 500-200 = 300,中心横坐标 = (400+800)/2 = 600,中心纵坐标 = (200+500)/2 = 350。归一化之后,中心横坐标 = 600/1280 = 0.46875,中心纵坐标 = 350/720 = 0.48611,框宽 = 400/1280 = 0.3125,框高 = 300/720 = 0.41667。最终那一行就是2 0.46875 0.48611 0.3125 0.41667。
理解了这套换算,很多问题就迎刃而解了。比如"框位置偏移",多半是标注工具导出时用了左上右下坐标而没做归一化;比如"框大小全错",多半是宽高算成了右下角坐标本身而不是差值;比如"训练集正常验证集偏移",多半是训练集和验证集的标注用了不同工具导出,格式不一致。我一般会在拿到第一批标注后,写十行脚本随机抽几张图,把标签反算回像素坐标,画到原图上肉眼确认,这个动作花五分钟,能省几小时的排查。
3.2 中文类名在四个环节的编码一致性
用中文类名做检测是可以的,但要求从标注到训练到推理,四个环节的编码链路完全一致。这四个环节分别是:标注工具导出的类别名文件、数据配置 yaml 里的 names 列表、训练时日志里的显示、推理时的渲染。任何一环用错编码,都会在那一环出问题,而且症状各不相同。
| 环节 | 典型症状 | 处理方式 |
|---|---|---|
| 标注导出 | 类别名变成乱码或问号 | 导出时选 UTF-8,别选本地编码 |
| yaml 读取 | 训练启动报解析错误 | 读文件时显式声明 UTF-8 |
| 日志打印 | 终端里中文显示成方块 | 终端编码设为 UTF-8 |
| 图上渲染 | 检测框标签全是问号 | 需要专用绘制方案,见第 5 章 |
一个务实的建议:如果你的类别不多,且只是自己用,类名用英文或拼音,只在最终出图和汇报时映射成中文。这样做的好处是把编码问题压缩到最外层一个映射字典里,训练链路完全干净,未来换环境、换机器、换部署端都不会带包袱。我做过几个项目都是这个思路,一个简单的字典{0: '裂纹', 1: '缺口', 2: '污渍'}放在推理脚本里,改起来比改数据集配置方便得多。
3.3 数据集划分与标签缓存
数据集目录的推荐结构是images/train、images/val、labels/train、labels/val四件套,图片和标签同名不同后缀,工程会按路径替换后缀自动找标签,所以文件名必须严格对应,img_001.jpg对应的标签必须是img_001.txt,不能有img_001_v2.txt这种。
配置文件的写法我建议把根路径和相对路径分开,迁移时只改一行:
# 数据配置示例 path: /data/defect_det # 数据集根目录 train: images/train val: images/val nc: 3 names: ['crack', 'gap', 'stain']这里有个很实用的坑要提一下:工程在第一次加载数据时会生成一个缓存文件,把标签路径、形状、尺寸都写进去,路径通常和标签目录同级。这个缓存的存在是为了加速第二次加载,但它不会自动感知标签变化。也就是说,你改了标注、重新导出了标签,缓存还在,训练读到的还是旧标签,症状就是"我明明改了标注,怎么结果一点没变"。解决办法很简单:改了标注就把缓存文件删掉,或者启动时带上重新扫描的参数。这个坑我踩过不止一次,最离谱的一次是花了半天时间怀疑模型结构,最后发现是缓存没清。
另外,划分比例上,小数据集不要机械地用八比二。如果你的数据只有几百张,验证集太小会让指标波动剧烈,看不出真实水平,我一般会划到七比三,甚至做多折交叉验证来看稳定性,光看单次划分的曲线很容易被噪声误导。
4. train.py 里真正影响结果的那几个参数
4.1 权重选择与 imgsz、batch 的三角关系
这三个参数是绑在一起的,任何一个单独调都是在赌博。先说权重,从零训练和加载预训练权重是两条完全不同的路。小数据集(几千张以内)几乎一定要加载预训练权重,否则模型收敛极慢,而且容易在早期就陷进局部最优。加载的方式是给训练脚本传权重路径,工程会自动区分"加载骨干权重"和"加载完整权重"两种情况,前者适合类别数变化大的场景,后者适合同类任务微调。
再说 imgsz 和 batch。图像尺寸决定了单张图占用的显存和计算量,batch 决定了并行度。显存估算有个粗略经验:在 640 尺寸、最小规模模型下,训练阶段单张图大约占用几百兆显存量级的空间,具体数值受框架版本、是否开启半精度、是否开启梯度检查点影响很大,不要照抄网上的数字,用你自己的卡试。我建议的调试顺序是:先把 imgsz 定在 640,然后把 batch 从 4 往上加,加到显存占用接近但不超过上限的八成,再固定 batch 去微调 imgsz。
这里有个容易忽略的细节:imgsz 必须是网络最大下采样倍数的整数倍。YOLOv5 的骨干最大下采样 32 倍,所以尺寸最好是 32 的整数倍,你传个 650 进去,工程会自动向上取整到 672,你以为在用 650,其实不是。如果你确实需要非标准尺寸,要么接受自动取整,要么在网络配置里改下采样策略,后者代价大得多。
4.2 学习率、warmup 与 cos_lr
学习率是第二个重灾区。工程自带的超参数配置文件里,初始学习率是按一个标准批大小设定的,如果你把 batch 调小了却没有同比调学习率,训练就会变得很不稳定,表现为损失曲线剧烈震荡或者直接变成无效值。经验法则是按批大小等比例缩放:批大小减到四分之一,初始学习率也减到四分之一左右。
# 超参数片段示例(注意 lr0 要和 batch 匹配) lr0: 0.0025 # 初始学习率,需随 batch 缩放 lrf: 0.01 # 最终学习率系数,最终 lr = lr0 * lrf momentum: 0.937 weight_decay: 0.0005 warmup_epochs: 3.0 warmup_momentum: 0.8 warmup_bias_lr: 0.1warmup 的作用是在训练最初几轮把学习率从很小慢慢升上去,避免一开始就用大学习率把权重打乱。轮数设为三左右是比较通用的选择,如果你的数据集特别小,可以把 warmup 轮数再降低,否则很可能前几轮都在空转。学习率衰减策略方面,余弦退火在长周期训练上通常比线性衰减表现更平滑,但在极短周期(比如只训几十轮)时差异不明显,不必纠结。
一个实测心得:如果你发现损失在前十几轮掉得很快然后长时间横盘,先别急着加轮数,去看看数据增强是不是配得太重。工程默认的增强配置是为大规模数据准备的,小数据集上过度增强会把有效的样本信息冲淡,表现为训练集损失降不下去。这时候把 mosaic 和混合增强的概率调低,往往比调学习率更有效。
4.3 中文日志乱码与训练可观测性
训练过程中的日志是你唯一的眼睛,如果这双眼睛瞎了,调参就变成了盲猜。中文环境下最常见的问题是终端里中文显示成方块或者问号,根源是终端编码和输出编码不一致。处理方式因操作系统而异,但通用的思路是把环境变量里的编码相关设置显式指向 UTF-8,并且确认终端本身用的是支持中文的字体。Windows 下还有一层是控制台代码页,需要手动切一下。
比乱码更麻烦的是日志里没有你想要的信息。工程默认打印的是每轮的损失分量和验证指标,这些够用但不够细。我习惯在训练脚本里加两处输出:一是每个类别在验证集上的表现,因为总体指标可能被大类掩盖,小类全错也看不出来;二是学习率随轮次的实际变化,用来确认调度策略真的生效了。这两处加完之后,很多"指标不涨"的问题能从曲线上直接读出原因,省去大量试错。
# 验证阶段按类别输出,便于定位小类失效 from utils.metrics import ap_per_class # 拿到 p, r, ap, f1 和类别索引后,逐类打印 for i, c in enumerate(class_names): print(f"类别 {c}: precision={p[i]:.3f}, recall={r[i]:.3f}, ap={ap[i]:.3f}")这段代码的具体调用方式随版本变化,思路是通用的:把聚合指标拆开看。我遇到过好几次总体平均精度看起来还不错、但某个关键类别实际完全检不出来的情况,如果只看总体数字,上线之后才会发现问题。
5. 检测框上写中文:可视化环节的改造
5.1 cv2.putText 的中文困境
这是中文版最核心的改造点,也是很多人第一次接触这个标题的原因。OpenCV 的图像绘制函数在渲染文字时,底层用的是它自带的字体渲染模块,这个模块只支持有限的字符集,对中文字符没有字形数据,所以你传进去一个中文字符串,它不会报错,也不会崩溃,而是静默地画出一串问号或者什么都不画。这个行为设计让无数人以为是编码问题,去改文件编码、改字符串类型,折腾半天才发现方向错了。
正确的思路是绕开这个渲染模块,改用能处理中文字形的绘制方案。主流的做法是引入图像处理库中的文字绘制能力,它能加载系统字体文件并正确渲染任意字符。代价是绘制过程要在另一种图像格式上完成,需要做颜色通道的转换,这个转换如果做反了,画出来的图会颜色错乱,红蓝互换。
5.2 PIL 方案与字体加载细节
具体实现上,核心是三件事:格式转换、字体加载、坐标对齐。格式转换要把 OpenCV 的 BGR 数组转成 RGB 的图像对象,绘制完再转回来。字体加载要指定一个支持中文的字体文件路径,系统自带的微软雅黑、思源黑体之类都可以,注意字体文件格式,有的字体是多字体集合,加载时需要指定索引。坐标对齐是因为文字绘制通常以左上角为锚点,而你手里的框坐标是中心加宽高或者左上角加右下角,需要先换算成文字该放置的像素位置。
import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont FONT_PATH = "/usr/share/fonts/truetype/custom/source-han-sans.ttf" # 换成实际可用的中文字体 def draw_zh_box(img_bgr, box, label, color=(0, 255, 0), font_size=20): """box 为 (x1, y1, x2, y2) 像素坐标""" x1, y1, x2, y2 = [int(v) for v in box] cv2.rectangle(img_bgr, (x1, y1), (x2, y2), color, 2) # BGR -> RGB -> PIL img_pil = Image.fromarray(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(img_pil) font = ImageFont.truetype(FONT_PATH, font_size) # 文字底框,避免浅色背景上看不清 tw, th = draw.textbbox((0, 0), label, font=font)[2:] ty = max(0, y1 - th - 4) draw.rectangle([x1, ty, x1 + tw + 4, ty + th + 4], fill=color) draw.text((x1 + 2, ty + 2), label, font=font, fill=(0, 0, 0)) # PIL -> RGB -> BGR return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)这段代码有几个真正影响体验的细节值得说。第一,字体文件必须先确认存在,直接写路径很容易在换机器后失效,稳妥做法是写一个候选列表,逐个尝试直到加载成功,全部失败就退回英文类名,保证程序不崩。第二,文字底框一定要画,否则深色文字压在深色背景上几乎不可见,尤其是做汇报图的时候,这一点差别非常明显。第三,中文字体的字号比英文字体的视觉尺寸偏小,同样的字号下中文看起来更挤,通常要比英文调大一两个像素。第四,每张图都重新加载一次字体是有开销的,在批量推理场景下应该把字体对象提到循环外面,我见过因为这个问题让整个推理流程慢了将近一倍的情况。
5.3 批量推理脚本的落地改造
单张图改造完,实际用起来还是要批量处理。批量脚本要注意三件事:目录遍历时的排序稳定性、结果保存的命名规则、以及异常图片的跳过。第一件是因为如果输出视频帧序列,顺序错乱会直接毁掉结果;第二件是为了能和原始文件对应上,方便回溯;第三件是现实场景里总会有几张损坏图或者零字节文件,不加保护就会中断整个批次。
import os, glob, cv2 def batch_infer(model, img_dir, out_dir, font_path): os.makedirs(out_dir, exist_ok=True) paths = sorted(glob.glob(os.path.join(img_dir, "*.*"))) # 排序保证稳定 for p in paths: img = cv2.imread(p) if img is None: print(f"跳过无法读取的文件: {os.path.basename(p)}") continue results = model(img) # 逐框绘制中文标签(draw_zh_box 见上) for box, label in parse_results(results): img = draw_zh_box(img, box, label) cv2.imwrite(os.path.join(out_dir, os.path.basename(p)), img)parse_results是按你的推理接口写的适配函数,不同版本返回结构差异不小,有的是对象属性,有的是张量列表,别照抄,先打印一次看看结构再写。这一步花十分钟,能省下后面反复调试的时间。
6. 三个高频故障的完整排查链路
6.1 损失不降、指标横盘
这是最让人焦虑的一类问题,因为可能性太多。我自己的排查顺序是固定的一套链路,按这个顺序走,八成能定位到。
第一步,确认标签真的被读到了。看训练启动时的日志,会打印扫描到的图片数和标签数,如果标签数是零或者明显偏少,问题在数据侧,回到第 3 章。第二步,确认类别索引没越界。标签里的类别索引从零开始,如果你标了三个类却写了索引 3,这个标签会被静默丢弃,症状就是某个类完全学不会。第三步,确认图像和标签一一对应。抽十张图,把标签反画回图上肉眼确认框的位置,这一步能抓到绝大多数标注格式错误。第四步,看损失分量的趋势。检测框回归损失和分类损失是分开的,如果回归损失在降、分类损失不动,说明定位在学但分类学不动,往往是类别不平衡或者类别名映射错了;反过来则是框的位置有问题。第五步,才去怀疑学习率和增强强度,把增强全部关掉用最小配置跑十几轮,如果损失能降,说明是增强的问题,逐项加回来定位。
我在实际项目里还遇到过一个更边缘的情况:损失曲线看起来一切正常,验证指标也还行,但测试集上完全失效。最后发现是训练集和测试集的采集条件差异太大,训练数据都是白天光照,测试数据是夜间补光,模型学到的是光照特征而不是目标特征。这种情况调参救不回来,只能补数据。所以当你反复调参无效时,想想是不是数据分布本身的问题。
6.2 显存溢出与批大小的实际取舍
显存问题的典型表现是训练几轮之后突然报溢出,或者一开始就报。前者通常是某个批次里出现了特别大或者数量特别多的样本,导致显存峰值突增;后者纯粹是配置超了。
针对前者,有几个降低峰值的手段:开启梯度累积,用小批大小模拟大批大小的效果;开启混合精度训练,能明显降低显存占用,同时速度提升也很可观;限制每张图的最大目标数,避免个别图片上目标过多。针对后者,按第 4 章说的顺序从 batch=4 开始往上试。
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| 启动即溢出 | 批大小或尺寸超限 | 降 batch,降 imgsz |
| 训练中途溢出 | 个别样本目标过多 | 开梯度累积、开混合精度 |
| 验证阶段溢出 | 验证批大小独立设置 | 单独调小验证批大小 |
| 多卡训练溢出 | 每卡批大小设置过大 | 按卡数均分,别按总数填 |
有一点必须说明:显存占用和批大小不是完全线性关系,因为还有模型参数、优化器状态、缓存等固定开销。所以小批大小阶段加上去很快,到后面就加不动了,别按线性去推算能加到多少。
6.3 训练正常但推理结果全错
训练日志漂亮、验证指标正常、一跑推理全是错框,这类问题的排查链路和训练侧完全不同。第一,确认推理时的预处理和训练时一致。图像尺寸的缩放方式、是否保持长宽比、填充值是多少,这几个细节不一致都会导致结果偏移。训练时是把图缩放并填充到方形,推理时如果直接拉伸,几何关系就变了。第二,确认通道顺序。训练时读图用的是 BGR,如果你在推理脚本里用另一个库读成了 RGB,颜色就反了,模型对颜色敏感的任务会直接崩掉。第三,确认置信度阈值和重叠抑制阈值。阈值设太高会漏检,设太低会框满屏,看起来像"全错"其实只是阈值不合适。第四,确认类别索引到名称的映射没搞错,这类问题特别隐蔽,框的位置完全正确,只是标签名张冠李戴。
排查这类问题最快的方法是拿训练集里的图去推理。训练集上的结果如果也不对,那问题一定在推理链路;训练集上完美而测试集不行,那问题在数据分布或者过拟合。这个二分法用起来很省时间,我基本是条件反射式地先做这一步。
7. 导出与跨端部署的收尾动作
7.1 导出中间格式后的后处理重写
训练完只是完成一半,真正落地往往要导出成通用格式,交给别的运行时去跑。导出本身一条命令的事,麻烦的是后处理必须自己重新实现一遍。训练框架内部做的那些事——把网络输出解码成框、按阈值筛选、做非极大值抑制、把归一化坐标还原成像素坐标、把输入尺寸映射回原图尺寸——在部署端一个都不会自动帮你做。
非极大值抑制是最容易写错的一环。它的逻辑是按置信度排序,取最高的框,然后把与它重叠度超过阈值的框全部抑制,重复这个过程。手写的时候常见的错误是忘了在抑制后重新排序,或者重叠度计算时坐标格式搞混(左上右下和中心宽高混用)。我建议先用少量图片,把自己的后处理结果和训练框架官方的推理结果做逐框对比,误差应该在很小的范围内,确认一致之后再上批量。
7.2 精度对齐与验收方法
导出之后必须做精度对齐,方法是:同一批图片,分别用原框架和部署端各跑一遍,逐图对比检测框的数量、位置和类别。对比不要只看最终指标,要看单图级的差异,因为聚合指标会掩盖个别样本上的严重偏差。
我的验收清单大概是四项:一是检测框数量的整体一致性,允许少量边界样本有差异;二是重叠框的平均交并比,正常情况应该在很高的水平;三是类别判定的一致性,如果有系统性的类别错位,说明映射表或者后处理的分数顺序有问题;四是端到端耗时的分布,不只看平均值,要看长尾,因为部署环境里偶发的慢请求往往比平均耗时更影响体验。
如果是中文标签渲染,还要在部署端再做一次字体验证。部署环境往往是最小化的镜像,系统字体可能一个中文的都没有,代码跑起来不报错,出图全是方块。稳妥做法是把字体文件随包一起分发,用相对路径加载,并且在启动时做一次可用性自检,加载失败就打印明确提示,而不是静默降级。
这套流程走下来,一个从中文注释版起步的 YOLOv5 工程,就变成了你自己能掌控、能改、能交付的东西。我个人最深的体会是,中文版最大的价值不是省了读英文的时间,而是给了你一个可以对照的起点——有对照才能发现差异,有差异才知道该往哪里深挖。真正让人成长的不是跑通的那一次,而是后面那些怎么调都不对、最后靠一步步缩小范围找到原因的过程。如果非要说一个建议,那就是别怕回到原版,注释可以自己写,工程越干净,走得越远。