1. 项目概述与需求拆解
1.1 这个系统到底解决什么问题
先说说为什么我会去做这么一套系统。在零售行业摸爬滚打的同行应该都有体会,商品标签识别这件事看起来简单,真正落地的时候全是坑。超市货架上的价签、仓储物流里的面单、电商仓库的分拣贴标,形态各样,背景复杂,光照条件一天二十四小时在变。传统的图像处理方案,比如基于边缘检测加模板匹配的老路子,碰上标签轻微倾斜、光线偏暗或者商品本身颜色跟标签颜色接近的时候,准确率会掉得很厉害。
我这次做的这套基于YOLO系列的商品标签识别系统,核心目标就两个:第一,把货架上不同种类商品的价签区域从复杂背景中准确框出来;第二,对框出来的标签区域做内容识别,提取出商品名、价格、促销信息这些关键字段。项目启动前我调研过市面上成熟的OCR方案,确实存在不少痛点,比如通用OCR引擎对特定场景识别精度不稳定,而且垂直领域的定制成本并不低。用YOLO自己训练一套,等于把识别的主动权握在自己手里,后续想扩展新品类也好办,标注一批数据重新训练就行。
适合来看这篇文章的人,我觉得有三类:一是刚接触目标检测,想找一个真实业务场景练手的学生或转行者;二是零售行业里做技术选型,想知道YOLO系列到底能干什么的从业者;三是已经在用YOLO,但想了解如何把模型从训练环境真正部署到业务场景中的开发者。这篇文章不会长篇大论复述原理,网上YOLO原理的文章已经够多了,我重点讲数据怎么准备、模型怎么选、坑在哪里,这些是我实际跑完一轮验证下来觉得最有价值的东西。
1.2 项目落地场景画像
为了把问题说清楚,我给自己定义了一个具体的模拟场景:某中大型连锁便利店,SKU数量在3000个左右,门店货架密集,价签规格有3到4种版式。系统需要部署在门店的监控摄像头后端,或者在手持设备端做辅助核验。目标是把价签检测的精确率做到90%以上,识别后的文字结果要和后台商品库对接,用于稽核价签与系统价格是否一致。
这个场景有很强的普遍性。现在很多同行在做的智慧零售项目,都绕不开商品识别这个环节。商品本身识别难做,因为同类商品包装相似度太高,可口可乐和百事可乐的瓶子放在一起,很多模型分不清楚。但价签就不一样,价签是人为设计的信息载体,上面有结构化的字段,识别起来路径更清晰。所以你会发现,很多零售科技公司在做商品识别的时候,都会把价签识别作为切入点和辅助验证手段,因为价签一旦识别出来,价格对不对、摆放对不对,一下就比对出来了。
这个项目还顺带带了另一个收益:价签数据是动态变化的,促销活动期间价签更换频率极高,人工巡检根本忙不过来。系统落地后,价签巡检从一天一次变成实时监听,异常价签能快速发现,这对连锁门店的合规运营是实打实的加分项。
2. 整体技术方案设计与模型选型
2.1 技术栈与任务链路规划
整个系统拆成四个环节:数据采集与标注、模型训练与验证、推理服务封装、业务逻辑对接。我不建议一上来就直接训练YOLO,先把链路想清楚,后面返工的几率会小很多。
第一环节是图像采集。门店摄像头是固定机位,拍出来的角度相对稳定,但不同门店的货架布局有差异,这点必须在数据采集阶段就考虑到。我当时的做法是分三个批次采集样本:第一批覆盖主货架正面视角,第二批覆盖端架和堆头(视角更偏,标签形变更大),第三批覆盖不同光线时段(早中晚各拍一部分)。这样采集下来的原始样本大约有6000多张,覆盖多个场景维度的变化。
第二环节是标注。标签框选要求是:只标当前售价标签区域,不标商品本身,不标促销爆炸贴,不标货架条。框选范围要贴着标签外沿,不能留太多背景边距。训练数据统一标注成YOLO格式的txt文件,类别只有一类,就是price_tag。
第三环节落到模型训练和验证。这里我做了两轮迭代。第一轮用YOLOv5s作为baseline,先把整个pipeline跑通;第二轮切到YOLOv8s,对比精度和速度的差异。关于模型选型的细节,我放在2.2节详细说。
第四环节是推理服务封装。模型训练好之后,用ONNX导出,放到一个FastAPI服务里跑。输入是一张图片,输出是标签框的坐标和置信度。服务接口设计成同步接口,单张图片延迟控制在200毫秒以内,这样下游不管是定时巡检还是人工抽查,都能直接复用。
2.2 YOLO系列选型对比:v5还是v8
YOLO系列现在版本很多,不少人一上来就纠结到底选哪个。我自己的判断标准很简单:稳定性和生态成熟度优先,性能其次。我第一轮用了YOLOv5s,第二轮用了YOLOv8s,两个模型用同一份数据集做对比,结论有一定参考价值。
先看模型结构层面。YOLOv5是相对经典的CSP架构,Anchor-Based检测头,训练和部署的资料非常多,社区里踩坑记录一搜一大堆,出了问题容易找到答案。YOLOv8把C3模块换成了C2f模块,检测头改成Anchor-Free,理论上对目标的尺寸变化更鲁棒。检测头的差异对价签这种宽高比相对固定的目标来说,影响其实没有想象中那么大,因为价签的规格相对统一,不是那种极端的长条形目标。
再说部署层面的对比。YOLOv5的ONNX导出链路非常成熟,PyTorch转ONNX再转TensorRT,每一步都有现成脚本可以抄。YOLOv8的导出也不差,Ultralytics官方出了统一的export命令,但在某些CUDA版本组合下,YOLOv8导出的ONNX做动态尺寸推理时偶尔会碰到算子兼容性问题,这个我在4.1节会详细说。
最后说训练收益。同一份数据,YOLOv5s训练120个epoch,mAP@0.5大约0.941,YOLOv8s训练120个epoch,mAP@0.5大约0.953。提升了1个百分点左右,但显存占用增加了约15%,推理时间也略有上浮。有一个需要强调的细节:mAP提升的这1个百分点,是在验证集上单张图片的表现,在连续视频帧的推理中,两者的实际表现差距几乎体会不到,因为视频相邻帧高度重叠,模型只要不连续漏检,效果差异就很小。
结论是:如果项目时间紧、部署环境复杂,直接选YOLOv5s就够了;如果项目长期迭代,想用新一代的特性,选YOLOv8s也不亏。不要迷信最新版本,YOLOv9、v10我都简单跑过测试,对于价签检测这个任务,模型结构复杂度的增加,并没有换来显著的精度回报,反而给部署引入了更多变量。
2.3 为什么不直接上现成的OCR方案
这里必须展开说一下。很多人问,标签识别不就是OCR吗,为什么还要专门训练YOLO。原因有两个层面。
第一个层面是检测和识别的分工不同。通用OCR方案(比如各种云服务厂商的OCR接口或者开源OCR模型)通常是端到端的文字识别,它能把图中所有文字都识别出来,但它的输出是文本框和文字内容,并没有告诉你哪个框是价签、哪个框是商品包装上的宣传语。在货架场景里,商品包装上到处都是文字,饮料瓶身的品牌名、零食包装的营养成分表、促销贴纸上的“惊爆价”,如果只靠OCR,噪音太多,下游接价格比对的时候根本无从下手。而YOLO做的是目标检测,它只需要回答一个问题:图像中哪个区域是价签?找到了区域,再对这个区域做精细的文字识别,整个信噪比完全不同。
第二个层面是成本可控。商业OCR按调用量计费,门店数量多、巡检频次高的时候,每天几十万次调用,一年下来是一笔不小的开支。自训练YOLO模型只要一次性投入GPU训练成本,推理阶段跑在自己的服务器上,边际成本极低。开源OCR方案虽然免费,但你要处理模型版本适配、Lite版本和各平台推理框架之间的差异,维护工作量也不小。现在YOLO检测干什么,OCR识别干什么,边界划得非常清楚,两边各自专注,效果和成本都最优。
3. 数据准备与标注实操
3.1 采集策略:怎么拍出“能打”的训练集
数据采集是整个项目里最容易偷懒但最不能偷懒的环节。模型效果好不好,七成看数据,三成才看模型。这句话说多少遍都不过分。
我第一轮采集的时候就吃过亏。当时图省事,只从门店监控里抽帧抽了两天,结果训练出来的模型在验证集上看着还行,一到新门店实测就露馅了——因为不同门店货架用的灯管类型不一样,色温不同,拍出来的图像整体偏暖或偏冷,模型对光照色温很敏感,稍微偏一点,检测置信度就断崖式下跌。
后来我把采集策略调整为四象限覆盖法,每个象限都有明确的样本要求。像距象限,覆盖远距离视角和近距离特写,价签在画面中的像素宽度至少要求100像素以上,这个尺寸是后续识别精度的安全线。朝向象限,覆盖正视、俯视、左右30度角,保证模型对透视形变有一定容忍度。光照象限,按门店营业时间分布,早、中、晚三个时段各取三分之一,同时专门挑了几家天花板灯光布局不同的门店做补采。背景象限,覆盖不同商品品类区域,食品区背景花花绿绿,日用品区背景相对素净,这些都要有。
最终采下来大约8000张,人工清洗去掉模糊帧、重复帧和标签遮挡超过30%的异常帧,剩7300张左右。这个数据规模对单类目标检测来说不算大,但胜在分布均匀,后期训练时发现,样本多样性比样本总数更重要。
3.2 标注规范与质量校验
标注直接决定了模型精度的上限,宁可慢一点,也要标得准。我用的标注工具是LabelImg的升级替代版,支持自动保存Pascal VOC格式,再通过脚本转成YOLO格式。
标注规范我列了三条硬性规则。第一,标签框必须贴住价签四边,允许有2到3像素的出血,但绝对不允许框到旁边的商品或货架边缘。第二,遮挡超过30%的价签不标,有些门店价签被促销插卡挡了一半,这种样本标进去只会教会模型去检测“被遮挡的价签”,实际场景里这类样本的用途有限。第三,同一个价签在图像中只标一次,如果一张图里同一个价签镜面反射出现了两次,只标实体那个,不标反射虚像。
标完不等于完结,质量校验这一步很多人会跳过去。我建议至少抽检30%的标注结果,重点看两类错误:一是漏标,尤其是角落位置或者阴影里的价签;二是错标,框选到了促销贴纸或者货架导轨上。把关方式可以拉一个经验丰富的同学一起做二次校验,两个人背靠背各标一遍,然后逐图比对差异,人工仲裁不满意的标注。
这里记录一个细节:我最终训练集的平均标注耗时是每张图19秒左右,7300张图总量大概38个小时。标注工作切给三个人并行,每天标6小时,用了三个工作日做完。你如果项目时间紧,也可以用半自动标注方案,先用一个初版模型做预标注,人工只修正错误的框,能省大概40%的时间。
3.3 数据增强策略与样本平衡
YOLO训练自带一系列在线数据增强,Ultralytics框架默认开启了马赛克增强、随机平移、缩放、翻转等。这些默认参数我用下来,对价签检测来说是够用的,但需要微调几个点。
mosaic增强默认概率是1.0,即每张训练图都是四张图拼接出来的。对价签检测来说,mosaic会制造很多尺度变化,有助于模型学会检测不同大小的价签,但也会引入一个问题:价签拼接时如果恰好在拼接缝附近,标注框会被裁剪掉一部分,模型学到的是一个“不完整价签”的特征。考虑到我们场景里价签本身是矩形小目标,我建议把mosaic概率调到0.6左右,留一部分原始图像直接训练,保住完整形状的特征。
还有一个很值得做的增强是hsv变换。门店光照色温变化大,默认hsv增强参数可以稍微加大饱和度变化范围和色相变化范围,让模型对日光灯管和暖黄灯光两种环境都更鲁棒。我实测把hsv_h从0.015调到0.03,hsv_s从0.7调到1.0之后,夜间样本的检测召回率提升了约3个百分点。
样本平衡上,价签单类目标不存在类别不平衡问题,但存在另一个形式的平衡问题:小目标数量占比过高。大量远距离视角的价签在640×640的输入尺寸下只占几十个像素,YOLO对这类小目标的召回效果天然不如大目标。解决思路不是去压小目标样本,而是在训练时叠加随机缩放增强,让同一张图的价签在训练过程中以不同尺度出现,迫使模型学会同一目标在不同尺度下的特征表达。实际效果验证下来,这一招比增加小目标样本数量更有效,因为样本数量加再多,模型也可能只是记住了那批样本的上下文背景,而不是真正学会小目标特征。
4. 模型训练与调优全过程
4.1 训练环境配置与参数初始化
我的训练机器是一张消费级显卡,显存12GB,跑YOLOv5s和YOLOv8s都足够。不需要多卡,单卡就能跑完整个项目,这也说明这个任务的算力门槛不高,一般团队都能承担。
训练之前有几个环境细节值得记录。PyTorch版本我用2.0.1,CUDA用11.7,cuDNN默认版本,这套组合跑YOLOv5和YOLOv8都没有遇到奇怪的兼容性问题。如果你是用Ultralytics框架,建议固定版本,因为新版本框架偶尔会改默认参数名,网上教程容易对不上。
参数初始化上,我沿用官方推荐的默认值做了一轮baseline训练。规模参数s,输入尺寸640×640,批次16,初始学习率0.01,动量为0.937,权重衰减5e-4。这些参数是Ultralytics针对COCO数据集调过的,迁移到价签场景时不需要大改,但有几个点要注意。第一,epoch数不要直接照搬官方默认的300,价签数据单类、样式统一,120个epoch左右就会收敛,300个epoch容易过拟合。第二,批次大小取决于显存,12GB显存下batch=16比较稳,batch再大有可能爆显存,爆的时候会自动缩小,但稳定性受影响。第三,预训练权重建议用COCO上训好的yolov5s.pt或yolov8s.pt做迁移学习,而不是从零开始,迁移学习能让模型在前十几个epoch就快速学会底层特征,收敛速度和最终精度都有明显优势。
4.2 训练过程中的指标巡检与判断方法
训练不是把命令丢进去就等结果,我习惯每10个epoch记录一次关键指标,包括train/loss、val/box_loss、mAP@0.5、mAP@0.5:0.95和Precision/Recall曲线。这些指标组合起来,能帮你判断模型是不是健康地收敛。
全局loss曲线正常的情况应该是前30个epoch快速下降,之后进入平台期,缓慢下降。如果你发现val loss先降后升,那就是过拟合信号,需要提前终止训练或者加大数据增强。mAP@0.5在120个epoch结束时达到0.94以上的话,说明模型效果已经可用,没必要为了零点几个百分点继续烧电费。
有一个细节很多人不注意:验证集的构建方式。如果你从整个数据集中随机抽20%做验证集,恰好把同一家门店拍的序列帧同时抽进了训练集和验证集,那验证指标会有虚高。正确做法是按门店维度划分,一家门店的所有图片要么全在训练集,要么全在验证集。这样才能模拟真实场景中“去一家新门店推理”的效果。
这个教训是我第二轮迭代时候发现的。第一轮按随机划分,验证集mAP有0.96,看起来非常漂亮,结果换了一家没参与训练的门店做实测,mAP直接掉到0.90。原因就是随机划分把同源图片泄露到了训练集,模型实际上“见过”验证集的门店背景。改成按门店划分后,验证集mAP回落到0.945,但实测表现反而更稳定。
4.3 精度提升三板斧:anchor优化、输入尺寸、难例挖掘
baseline训练完成后,如果mAP卡在某个水平上不去,我总结了三个最有效的调优手段。
第一板斧是anchor优化。YOLOv5和YOLOv8的训练脚本里都有自动聚类anchor的功能,但是默认的开启逻辑是基于COCO数据集的。价签的宽高比和COCO框的分布差别很大,价签通常比较扁平,很多标签纸是横版印刷,宽高比接近2:1甚至3:1。训练脚本会自动重算anchor,但你最好确认一下,如果训练日志里显示anchors是默认值,手动让脚本基于自己的数据重新聚类一遍,能提升小目标的定位精度。
第二板斧是输入尺寸。把输入分辨率从640×640提高到960×960,等于把价签在图像中的有效像素数扩大了2.25倍,小目标检测精度通常会有明显提升。代价是显存占用变大,推理速度变慢。我实测在960分辨率下,mAP@0.5提升约2.5个百分点,推理帧率从50FPS降到12FPS。对于门店巡检这种对实时性要求不高的场景,这个换算非常划算。
第三板斧是难例挖掘,也叫hard example mining。训练完一轮之后,把所有训练集的预测结果汇总一遍,把置信度低于0.5或者漏检的图片单独抽出来,人工看一下是什么原因。我那一轮做下来发现两类典型难例:一类是金黄色的包装袋配上金黄色价签背景,价签边缘完全融进背景里,模型很难区分边界;另一类是火烧价签边缘反光,框选区域泛白。针对这两类,我专门补采了200张左右的高难度样本,加入训练集重新训练,mAP提升最明显的就是这两个类别。
5. 部署与业务集成
5.1 ONNX导出与推理服务搭建
模型训练完,不能只停在PyTorch权重文件层面。实际部署我用的是ONNX Runtime推理,把PyTorch模型导出为ONNX,再放进服务里跑。
导出这一步有细节。YOLOv5的export.py脚本参数里有--dynamic,需要显式打开,否则导出的ONNX模型默认输入尺寸是固定的640×640,如果输入一张不同尺寸的图片,要么报错要么强制resize失真。打开动态尺寸后,ONNX模型的输入维度是[batch, 3, height, width],height和width都是动态轴,推理时任意尺寸的图片都能直接输入。
YOLOv8的导出命令是yolo export model=yolov8s.pt format=onnx dynamic=True,Ultralytics官方封装得比较干净。我在YOLOv8导出时遇到过一个问题:某个版本的ONNX导出会包含一个自定义的NMS节点,这个节点在ONNX Runtime下不完全兼容,推理时要么忽略它,要么报算子不存在的错误。解决办法是在导出时加参数nms=False,把NMS阶段留在推理代码里用纯Python或者NumPy实现。这样模型输出的是原始检测框、类别概率和置信度,灵活度更高。
推理服务我用FastAPI写的,代码如下,这是核心部分:
import cv2 import numpy as np import onnxruntime as ort from fastapi import FastAPI, UploadFile from fastapi.responses import JSONResponse app = FastAPI() session = ort.InferenceSession("best.onnx", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) def preprocess(image: np.ndarray) -> np.ndarray: img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (640, 640)) img = img.astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1)) return np.expand_dims(img, axis=0) def postprocess(outputs): boxes, scores = [], [] for pred in outputs[0]: x1, y1, x2, y2, conf = pred[:4], pred[4], pred[5] if conf > 0.45: boxes.append([x1, y1, x2, y2]) scores.append(conf) return boxes, scores @app.post("/detect") async def detect(file: UploadFile): data = await file.read() nparr = np.frombuffer(data, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) dets, _ = session.run(None, {"images": preprocess(img)}) boxes, scores = postprocess(dets) return JSONResponse({"boxes": boxes, "scores": scores})这是一个最精简的服务骨架。生产环境需要自己加上NMS部分,也就是非极大值抑制,因为模型在密集预测时会输出大量重叠框,NMS的作用是保留每个目标最可信的那个框。可以直接用cv2.dnn.NMSBoxes做,几行代码就搞定。
5.2 服务化部署的稳定性细节
推理服务上线之后,运维层面的几个问题会比训练更棘手。第一是显存泄漏,ONNX Runtime的CUDA Execution Provider在长时间运行后偶尔会有显存碎片化,表现为服务启动时显存占用400MB,跑了一周后涨到2GB。解决方法是定期重启服务,简单粗暴但有效,或者改用TensorRT做推理后端,显存管理会好一些。如果不追求极致性能,CPUExecutionProvider在640分辨率单张图片推理下大约300到400毫秒,对于巡检任务其实完全够用。
第二个问题是并发控制。FastAPI默认是异步模型,但ONNX Runtime的session对象不是线程安全的,如果直接用同一个session处理多请求,会出现crash或者结果错乱。解决办法是用锁保护推理调用,或者用线程池为每个线程创建独立的session。对于门店巡检这种低频调用场景,加一个asyncio.Lock就够了,实测没有性能瓶颈。
第三个问题是图片质量的鲁棒性。摄像头传上来的图不一定是干净清晰的,可能是夜间的噪点图、逆光剪影图或者运动模糊图。我在服务层加了一个前置校验:图片尺寸小于200×200的直接返回“图片质量过低”,不做推理;同时用拉普拉斯算子算了一下图像的方差,方差低于阈值说明图片模糊,也直接拦截掉。这样虽然可能误伤一部分依稀可见的图片,但整体服务的稳定性和输出可信度提升了,下游对接方不用整天处理“识别了个寂寞”的case。
5.3 下游业务逻辑怎么接
检测服务跑起来了,接下来的问题就是怎么让它产出的标签框真正用到业务流程中。我定义了一个简单的对接方案。
检测服务输出的是标签框坐标和置信度,再把框内的图像裁剪出来,发送给OCR识别。这里要有一个策略:什么时候触发OCR。如果每帧每个标签框都做OCR,资源消耗巨大,而且很多标签在视频帧中是重复的。我设计了一个去重和触发策略,基于目标跟踪的思路:用IOU判断前后帧检测框是否属于同一个标签,同一个标签只在“首次出现”和“内容变化疑似”时触发OCR,后者的判断是标签框内图像的像素变化幅度超过阈值。
OCR识别出文本内容之后,进入一个文本后处理模块。价签上的字段结构比较固定,我用正则表达式分层解析,先提取价格数字,再提取单位(元/斤还是元/件),最后提取商品名称关键词。解析结果写入数据库,和后台商品价格库做比对。价格不一致时,生成告警事件,推送到门店管理端。
这个链路的性能表现,我实测下来:一台普通GPU服务器可以支撑约20路摄像头同时接入,每轮全量巡检在5分钟内完成,识别延迟从图像输入到价格比对结果返回大约1.2秒。对于零售门店来说是完全可以接受的水平。
6. 实战中的常见问题与排查技巧
6.1 检测漏检和误检的定向修复
先记录一个非常高频的漏检场景:价签贴在透明玻璃柜上,背景穿透过来,模型经常把价签和背后商品当成一体。这个问题在技术指标上表现为precision正常但recall明显偏低。我排查后发现,问题根源不是模型“看不见”价签,而是模型学到的是“背景+价签”的组合特征,换了背景就失效。针对这种场景,我在数据增强里增加了一个随机背景替换:从训练集中随机裁剪一块非价签背景,粘贴到价签周围区域,模拟透明背景干扰。这招对玻璃柜场景非常有效,补强后该场景recall从65%升到了88%。
误检场景也踩过一次:货架导轨上的金属条在特定角度反光,形成一条高亮白条,形状和价签有点相似。模型一度把金属条误检成价签,导致检测结果出现大量假阳性。解决方式是去了现场拍了几百张金属条反光的负样本,单独建了一个reflection类加入训练集,让模型学会区分“高亮条状区域不等于价签”。加了负样本后误检率下降了约80%。
6.2 光照变化引发置信度抖动的处理策略
夜间的门店和白天完全不是一种视觉风格。模型白天表现很好,到了夜间整体置信度可以掉10个百分点。置信度阈值设得高,夜间大量漏检;阈值设得低,白天又出现误检。我的处理方案是做一个动态阈值策略:服务根据当前图像的亮度统计值(整图平均灰度),动态调整检测阈值。灰度值低于某个阈值(代表夜间),置信度阈值自动从0.45降到0.3;灰度值高于阈值,维持0.45。虽然这个策略听着不够“智能”,但实际效果极稳,一天24小时的检测召回率波动被控制在很小的范围内。
还有一个细节是摄像头白平衡。很多IPC摄像头在夜间会自动切换到红外模式,拍出来的图像是黑白的。黑白图像下价签的颜色属性完全丢失,模型只能靠形状和边缘特征判断。为了保住白天彩色特征的效果,我建议在摄像头端把图像的色彩模式固定为彩色,同时开启补光灯,避免摄像头自动切换红外。这个需要在设备选型阶段就和硬件供应商沟通好。
6.3 模型持续迭代的版本管理心得
模型不是训一次就结束了。新品进场、价签版式改版、门店装修变风格,都会影响模型的精度。持续迭代的核心是版本管理。
我建议在项目里建立一个数据回流的闭环。每周从实际推理日志中随机抽样一部分图片,人工打标后并入训练集。同时对改版后的价签保持敏感。价签版式改版之后,旧模型的置信度会明显下降,这个时候需要快速采集新版式样本,增量训练。训练集的数据比例要保持新版本样本占比不低于30%,否则模型很快会记住老版式。
训练的版本管理我用的方式比较朴素但有效:每个版本的训练权重、训练配置、训练集版本号打包存储,文件名编码里包含日期和数据规模信息。这样任何一次效果回退,都能快速定位到是数据变了还是配置变了,做好版本召回的准备。
7. 训练数据工程细节补充
7.1 标注格式与检查工具
YOLO格式的标注文件是txt文件,每一行代表一个目标,格式是class x_center y_center width height,坐标值均为归一化后的比例值。格式看着简单,但手写容易出错,最大的坑是坐标没有归一化或者归一化错误。我写了一个批量检查脚本,统计每个标注框是否超出图像边界、宽高是否为负数、类别是否超出已定义类别范围。这个脚本在正式训练前必须跑一遍,别觉得多此一举,我从第一轮标注中发现过不少坐标尾数差一位就偏出去的情况。
为了方便标注检查,我还做了一个小工具,把标注信息直接可视化画在图片上,随机抽样几百张逐张扫一眼。比起只看着txt数字靠谱太多。YOLO坐标本来就是给人眼复核准备的,不要跳过着这步。
7.2 训练集与验证集划分的代码实现
按门店划分数据集的逻辑,我用一段简单的Python脚本来实现。核心逻辑是先读取所有图片的路径,从路径中提取门店编号,基于门店编号做分组,然后按组分配训练集和验证集。
import os import random from collections import defaultdict image_root = "datasets/store_images" train_dir = "datasets/train" val_dir = "datasets/val" store_images = defaultdict(list) for fname in os.listdir(image_root): if not fname.endswith(".jpg"): continue store_id = fname.split("_")[0] store_images[store_id].append(fname) all_stores = list(store_images.keys()) random.seed(42) random.shuffle(all_stores) val_stores = set(all_stores[:int(len(all_stores) * 0.2)]) for store_id, files in store_images.items(): target_dir = val_dir if store_id in val_stores else train_dir for fname in files: os.rename(os.path.join(image_root, fname), os.path.join(target_dir, fname))门店ID在文件名里用前缀区分,这个设计一开始就要确定好,不然后期做门店维度划分会非常痛苦。我见过有人把所有文件堆在同一个目录里,文件名完全随机,后面要按来源分组只能靠人工回忆,浪费的时间非常不值得。
7.3 数据增强在训练配置中的落地方式
数据增强并不是训练参数里的一个开关,它有具体的位置和写法。以Ultralytics框架为例,增强参数在data.yaml旁边的训练配置里,或者直接在命令行传参。我常用的配置是:
python train.py --img 640 --batch 16 --epochs 120 --data price_tag.yaml --weights yolov5s.pt --mosaic 0.6 --hsv_h 0.03 --hsv_s 1.0 --hsv_v 0.6关键参数含义是:--mosaic表示马赛克增强的概率,0.6以上对复杂背景有效但容易破坏小目标形状,0.4以下效果不足;--hsv_h控制色相偏移,0.03不算大,但足以覆盖门店色温差异;--hsv_s控制饱和度缩放,1.0表示最大可以扩大到原来的两倍。这些参数值针对价签场景手工调过,如果你做的是其他目标检测任务,要重新试,不要直接照抄。
7.4 显存不足的常见应对
训练过程中最容易遇到的硬性问题是显存不足。我12GB的显卡跑YOLOv8s的batch=16偶尔也会报CUDA out of memory。遇到这种情况,有几个处理顺序可以尝试。
第一优先调整批次大小,从16降到8,显存占用直接减半,代价是训练速度变慢,但效果几乎不变。第二是降低输入尺寸,从640降到512,但代价是最终模型可能对细粒度特征识别有所下降,不推荐直接把这一步作为首选项。第三是开启混合精度训练,Ultralytics框架默认开启AMP,但如果你的环境里AMP没有生效,检查PyTorch的CUDA版本是否支持。
如果以上都试了还不行,就该考虑模型规模了——换YOLOv5s而不是YOLOv8s,参数量更小,显存占用更低。对于单类目标检测,YOLOv5s在价签场景的精度差距微乎其微,但显存友好度显著更高。
8. 业务应用扩展与后续演进方向
8.1 巡检任务编排与联动策略
检测模型稳定跑通之后,我做了第二层业务封装:巡检任务的编排。单张图片检测只是个技术能力,真正让业务跑起来的是一个定时任务调度器。
调度器的作用是定期调取门店摄像头抓拍当前画面,送入检测服务,把结果做时间序列聚合。这里关键的设计是抓拍频率不能太高。我之前做了个压力测试,每5分钟抓拍一次,发现很多帧内容完全没变化,白白消耗算力。后来改成动态间隔:如果最近三轮检测结果没有变化,把间隔拉长到30分钟;一旦检测到价签更新事件,立即把间隔缩短到5分钟并持续一段时间。这样既保证了对变更的快速响应,又控制了资源消耗。
联动策略上,检测结果和后台的告警系统打通。价格不一致的告警分为两级:单店单条价签异常,推送到门店店长;同品牌多门店同时出现同类异常,推送给区域运营。这个分级联动是实际业务中最有用的设计,避免告警轰炸导致运营团队麻木。
8.2 同源技术在相邻场景的快速迁移
这套基于YOLO的检测方案在迁移性上给了我很大惊喜。项目做完之后,有几个相邻场景的需求冒出来,我基本没怎么改模型骨架,只是换了数据集重新训练。
第一个相邻场景是促销插卡识别。门店经常在货架上放一些促销标语插卡,这种插卡和价签长得不一样,但检测逻辑完全一样,是“矩形信息载体检测”。我把价签数据和新采集的促销插卡数据合并,重新跑到新的二分类模型,迁移成本不到三天。第二个相邻场景是商品缺货检测。货架空位识别本质上也是目标检测,只不过目标不是“价签”而是“空位空槽”。数据采集方式稍微变了一下,从摄像头抓拍改为货架巡检机器人定点拍摄,训练链路完全复用。这个扩展让我意识到,YOLO这种通用目标检测框架的真正价值在于做一次pipeline冻结,后续新业务的启动成本被压得非常低。
8.3 从单店到多店的工程化思考
项目从单店POC到多店规模化落地,中间有几个工程化问题要提前想清楚。一是一键部署的问题,不可能派一个工程师跑二十家门店逐台部署。我当时的应对是将推理容器做成Docker镜像,配合一套独立部署脚本,在目标服务器上执行一个命令就能完成环境初始化和模型拉起。二是模型热更新,门店端的模型不能每次都重新拉镜像,需要设计一个模型文件版本管理接口,服务定时轮询模型仓库,发现新版本后自动加载并热切换。三是统一监控,在总部侧搭一个轻量的监控看板,汇总各门店的推理健康度、检测率、平均延迟等关键指标,异常门店自动标红,运维同学只需要盯这一块屏。
这些工程化打磨的工作量不亚于训练模型本身。很多项目从demo走到生产,死在架构细节上的不在少数。如果你打算把这个方向从个人项目扩展为真实业务,一开始就要把“部署10家门店”当作假设条件来做技术方案,而不是最后补课。
9. 成本分析与投入产出评估
9.1 人力成本、时间成本与硬件成本细分
做一个项目,不能光看技术,也得算算账。我按这个项目实际执行的情况拆解一下成本结构。
硬件成本:一张12GB显存的消费级显卡,按当时市场价算大约8000到10000元,这是最大的一笔硬件投入。如果项目有云GPU环境,也可以按小时租用,训练周期按两周算,大约需要100小时左右,云成本控制在几千元。CPU服务器一台普通配置就能跑推理,预算5000元以内。摄像头设备如果是复用的门店存量设备,不需要新购成本。
人力成本和时间成本:数据采集分散在两个周末,全天泡在门店,累计约16小时。标注集中三到五天完成,累计38小时。模型训练和调优大约一周,每天2到3小时,累计约15小时。部署和业务对接大约3天,每天4到5小时,累计约13小时。整体投入在80到100小时之间,如果是一个熟悉流程的开发者,还能再压缩30%左右。
9.2 从成本视角看技术选型的合理性
这个项目如果全用商业OCR接口,单门店每月调用量大约8万次,按照市场上常见的定价,每个月接口成本大约数百元起步,几十家门店年化成本就非常可观了。自训练方案的核心成本集中在项目初期,后期维护成本极低,每次迭代训练成本大约只在百元级以下水平。
从ROI的角度算下来,一次投入换取一年以上的持续使用,成本优势是非常明显的。要不要做自训练,有一个简单的判断标准:如果门店规模超过10家,且巡检频率达到每天一次以上,12个月内自训练的成本一定比纯商业接口低。门店少且巡检频率低的情况下,纯接口方案反而更划算,省了维护精力和人力。我见过不少团队在门店数量还很少的时候就铺了一套复杂的自训练系统,结果算力空转、人力耗在上面,反而拖累了业务推进。技术选型一定跟着业务规模走,不必一步到位。
9.3 项目收益的量化评估框架
项目做完之后,我给自己做了一个收益量化评估框架,用来衡量这个技术投资的真实价值。
第一个指标是巡检覆盖频率。人工巡检从每周三次提升到系统实时巡检,覆盖度提升是几十倍的量级。第二个指标是异常价签发现周期。系统落地前,价格不一致问题平均要等人工巡检发现,时长可能以天计;系统落地后,绝大部分异常在当天内就能被发现并推送。第三个指标是人力释放。门店店员平均每天花在价签巡检上的时间大约在半小时到一小时,几十家门店累计起来,每月释放的人力工时非常可观。
我给这个项目打完分之后,结论是:YOLO商品标签识别系统在技术成熟度、落地成本、业务价值三个维度上都表现均衡,是一个非常适合作为目标检测入门到实战中间态的经典项目。
10. 最后做一些经验复盘
10.1 那些用了才发现好用的工具链点
回看整个项目,有四个工具选择让我觉得特别顺。一是Ultralytics框架自带的训练日志可视化,用起来真的很省心,指标曲线、PR曲线、训练样本可视化全都有,不用额外搭一套可视化平台;二是Labellmg的自动保存彩蛋功能,改一个配置文件就能自动保存标注结果,手残党的福音;三是ONNX Runtime自带的模型优化工具,不需要额外加依赖,速度比裸的ONNX Runtime快一点点;四是FastAPI的自动API文档,调试接口根本不用写单独的客户端,打开浏览器就能看全部接口参数,开发效率提升非常明显。
10.2 踩过坑之后最想告诉你的几条建议
第一,数据不要省,尤其是负样本。大多数训练效果达不到预期,最终都要回到数据侧找原因,负样本杂讯非常影响模型表现。
第二,按业务视角划分数据集,不要按随机数划分。虚拟的“验证集指标好看”不代表真实场景好用,按门店、按灯光条件划分,指标更有参考意义。
第三,部署的时候别贪心一次上最复杂的推理框架。先用ONNX Runtime跑通整个链路,再去想TensorRT优化,每一步的复杂度都要有对应的业务价值支撑。
第四,一定要预留模型版本管理和回滚能力。模型迭代是持续过程,没有一个版本是最终版,没有回滚能力的时候,每次模型更新都像是在赌博。
第五,关注时间序列上的检测稳定性,不要只关注单帧mAP。视频巡检场景里,模型偶尔单帧漏检是可以接受的,但连续多帧漏检会导致业务告警的严重误判。需要根据时间维度的检测结果做平滑策略,比如连续三帧都检测不到价签才认为真的缺签,降低个别异常帧的影响。