我最初接触目标检测的时候是一路踩坑走过来的。比如“为什么标注完数据还跑不起来”“loss怎么就是不下降”“明明训练完了图片上却一个框都没有”。说句实话这个领域里最不缺的就是碎片化教程但最缺的是一份能把数据集标注、模型训练、可视化结果串成一条线并且真的能让零基础的人从头到尾跑通的完整资料。这篇文章就是冲着这个缺口来的。它没有任何前置知识门槛不需要你懂神经网络原理也不需要你会写复杂的代码。我们只做一件事用YOLOv5把一个完全空白的自定义数据集从手工标注一路做到模型推理最后在图片和视频上看到实打实的检测框。整个过程我会按照一个项目的标准流程拆开来讲每一步都给命令、给参数、给文件内容也会告诉你那些没人写在文档里的坑。读完后你不仅能跑通一个完整项目还会理解每一步为什么这样做下次遇到问题自己知道往哪个方向查。整个项目过程中你的电脑配置哪怕只是普通办公机甚至只有CPU也完全能走完全流程只是训练时间会拉长而已不影响动手理解。1. 数据集标注是整个流程的地基为什么不能省以及用什么工具1.1 标注的本质教会模型“看到”目标很多人一上来就急着装环境、跑代码恨不得立刻看到检测框画在图片上。这是初学者最容易犯的方向性错误。先想清楚一件事YOLOv5本身只是一个会“读取图片、计算特征、输出坐标”的工具它并不知道你想检测什么。你想让它识别“施工人员的安全帽”它自己理解不了什么是安全帽你必须拿大量的、已经框好安全帽位置的图片喂给它它才能从这些带标签的例子中反推出规律。我们用一句话来理解标注工作——它就是把人类已经具备的视觉理解能力翻译成计算机能懂的坐标语言。对一张图片你告诉模型“这里从第130个像素到第220个像素是一个安全帽”模型才能在下一次见到类似区域时学会判断“这可能也是安全帽”。没有标注后续的模型训练就是无源之水不管配置多高、代码多新都白搭。而且标注质量直接决定模型上限。模型训练是在已有数据上找规律如果你的标注框位置歪了、类别标错了模型学到的一定是错误规律。“进去的是垃圾出来的自然也是垃圾”这是所有做数据的人都绕不开的一个基本原则。所以这篇我特意从0开始讲标注因为这是整个项目里最枯燥但最值得认真对待的环节。1.2 工具选型为什么我把LabelImg放一边改用X-anyLabeling提到目标检测标注工具很多人首先会想到LabelImg。这个工具本身没毛病但它有一个对纯小白非常不友好的点需要自己先配好Python虚拟环境还要安装一堆依赖光是GUI窗口弹不出来就能卡住很多人。我这里推荐一个更省心的替代方案X-anyLabeling。它最大的优势是直接提供了打包好的免安装版本下载解压后双击就能打开内置了Python运行环境不需要你自己折腾任何依赖。虽然它主要面向更灵活的标注需求但我们这里只用它的基础功能也完全够用输出标准VOC格式的XML文件。对于纯小白而言“开箱即用”永远比“功能最全”重要。当然如果你已经熟练掌握了LabelImg继续用完全没问题。工具只是手段核心在于产出的标注格式能和我们后面用到的转换脚本对接。我在这里把选型逻辑讲清楚是因为经常有人卡在第一步就放弃了。一点提醒无论用哪个工具标注出来的XML文件都要注意保存位置和文件命名。后面转换脚本会按照文件名去关联图片和标签文件命名混乱一点坑的是你自己。2. 从零开始打标签XML标注文件里的关键细节2.1 项目目录规划与模拟数据准备动手标注前先建好目录结构。我在实际项目中习惯这样组织清晰、不易乱yolov5_custom ├── data │ ├── images # 原始图片统一放这里 │ └── labels_xml # 标注生成的XML文件放这里 └── project_code # 后面放训练代码和脚本图片素材方面如果你是练习可以用自己的手机随便拍一批日常物体比如桌子上的杯子、文具盒、鼠标。想练得更接近真实项目可以下载公开数据集但需要注意版权和使用条款。我建议初学阶段用自己拍的照片因为目标场景只有你自己最清楚标注时也更方便不用去猜数据集里到底是什么物体。如果你使用的是自己拍摄的照片建议把手机横过来拍让画面里的物体尽量大、尽量居中一些这样后面对小目标检测的效果会好一些。这个细节是我在多个项目里踩坑后总结出来的——很多初学者数据集里目标又小又偏训练出来模型总是漏检其实第一步就决定了结局。2.2 标注工具操作路径与核心参数打开X-anyLabeling通过“打开文件夹”方式加载你的图片目录在左侧面板中设置好标签类别列表。以“检测安全帽”为例类别就填helmet一个如果想做一个多类别检测就逐个添加类别名比如person、car、helmet。具体的标注操作就是一个循环点击左侧的“创建矩形框”按钮或者按快捷键不同版本略有区别。在图片上按住鼠标左键从目标的左上角拖到右下角松开后完成一个框。在弹出的类别列表里选择对应类别确认后保存这个工具会自动同步保存XML标注文件。单张图片上如果存在多个目标就重复以上步骤直到所有目标都被框完。看起来很简单但实际标注中有几个点需要特别注意。一是框要紧贴目标边缘不能留太多背景也不能压到目标本身框得越准模型学得越准二是每个类别的样本数量要尽量均衡一些如果你要做两类检测一类标的800个目标另一类只有10个目标模型最后基本只会学到大类的特征小类形同虚设三是每张图片的目标数量不要极端不平衡个别图片空空如也、个别图片堆满目标都会给训练带来额外干扰。2.3 标注完成后必须检查的三件事标注全部做完不要急着进入下一步先花几分钟做三件事第一抽查XML内容是否正确。打开任意一个XML文件你会看到类似这样的核心内容annotation folderimages/folder filenameIMG_20210101_0001.jpg/filename size width640/width height480/height depth3/depth /size object namehelmet/name bndbox xmin112/xmin ymin98/ymin xmax321/xmax ymax364/ymax /bndbox /object /annotation你看这个结构并不复杂。重点是filename必须和图片文件名完全一致width和height必须和图片实际尺寸一致name必须是你的类别集合里的值bndbox里的四个值都在图片尺寸范围内。第二确认XML数量等于图片数量。一个文件夹里有多少张图片就应该有多少个对应的XML文件。如果出现漏标文件数量对不上后面转换脚本就会直接报错。这一步在标注阶段就能发现等训练跑起来才发现排查成本就高了。第三统计各类别目标数量。两种方式一是直观地看标注文件夹里的XML文件用文本编辑器全局搜索name差不多就能看出各类别出现的次数二是写一个小脚本统计。实际项目中建议写脚本因为数据量一大靠肉眼数完全不现实稍后我会给出一个统计脚本的参考。3. 从XML标注到YOLO训练格式数据转换与目录规范化3.1 为什么必须把XML转成TXTYOLO格式的坐标逻辑LabelImg和X-anyLabeling保存的都是Pascal VOC格式的XML文件坐标是绝对像素坐标表示“左上角点在哪里、右下角点在哪里”。而YOLOv5训练时默认读取的是YOLO格式的TXT文件坐标是归一化的比例值总共五个数字类别编号 中心点x比例 中心点y比例 框宽度比例 框高度比例。比如上面那个XML里的框如果图片宽640、高480中心点x就是(112321)/2 / 640 0.338中心点y就是(98364)/2 / 480 0.481宽就是(321-112) / 640 0.327高就是(364-98) / 480 0.554。TXT里对应的一行就是0 0.338 0.481 0.327 0.554有人会问为什么不直接用XML呢原因很简单归一化坐标和图片尺寸解耦了。模型训练时会对输入图片做缩放如果标注坐标是绝对像素值图片缩放后坐标就不准了而归一化坐标始终保持在0~1之间无论图片怎么缩放变形框和目标的相对位置关系都不变。这是YOLO系列一脉相承的格式逻辑理解这一点后你看所有YOLO的标注数据都会觉得顺眼。3.2 转换脚本怎么处理每个XML文件我一般用一个Python脚本批量完成XML到TXT的转换、类别编号映射、以及图片尺寸校验。下面这个脚本可以直接保存使用import xml.etree.ElementTree as ET import os # 类别映射表顺序决定了类别编号要和后面训练配置保持一致 class_names [helmet] def convert_xml_to_txt(xml_dir, txt_dir, image_dir): if not os.path.exists(txt_dir): os.makedirs(txt_dir) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_file) tree ET.parse(xml_path) root tree.getroot() # 从XML读取图片文件名并校验图片真实尺寸 filename root.find(filename).text image_w int(root.find(size/width).text) image_h int(root.find(size/height).text) # 图片文件名作为TXT文件名基础 txt_name os.path.splitext(xml_file)[0] .txt txt_path os.path.join(txt_dir, txt_name) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: print(f跳过未定义类别: {name} 文件: {xml_file}) continue class_id class_names.index(name) bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) # 防呆允许轻微越界但明显越界要触发警告 if xmin 0 or ymin 0 or xmax image_w or ymax image_h: print(f警告: {xml_file} 中框坐标越界: {xmin},{ymin},{xmax},{ymax}) x_center (xmin xmax) / 2 / image_w y_center (ymin ymax) / 2 / image_h box_w (xmax - xmin) / image_w box_h (ymax - ymin) / image_h # 把坐标钳位到0~1之间避免出现负数或大于1 x_center max(0, min(1, x_center)) y_center max(0, min(1, y_center)) box_w max(0, min(1, box_w)) box_h max(0, min(1, box_h)) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) print(f完成: {xml_file} - {txt_name}) if __name__ __main__: convert_xml_to_txt( xml_dirdata/labels_xml, txt_dirdata/labels_txt, image_dirdata/images )这个脚本里有几个细节都是我自己被问题教育过之后加进去的。坐标钳位那段代码防止图片边缘目标框稍微突出几个像素就导致训练直接报错警告日志则能在批量转换时快速定位有问题的标注文件。3.3 训练数据集的目录规范与train/val划分转换完成后需要把图片和TXT配对并按照YOLOv5的读取习惯组织目录。YOLOv5在训练时通过数据配置文件读取训练、验证图片路径而每张图片对应的TXT标注文件需要放在与图片同级的labels子目录下。推荐目录结构如下dataset ├── images │ ├── train │ │ ├── img_001.jpg │ │ └── ... │ └── val │ ├── img_002.jpg │ └── ... └── labels ├── train │ ├── img_001.txt │ └── ... └── val ├── img_002.txt └── ...注意images/train和labels/train里文件名必须一一对应只是后缀不同。这一点是初学者最容易出问题的地方——图片和标签目录对不上YOLOv5训练时会打印大量WARNING: no labels found的提示看似训练在跑其实模型根本没吃到任何标注。划分比例上我习惯用8:2或者9:1。数据量大几千张以上可以用9:1数据量小建议8:2多留一点验证图片能更可靠地判断模型有没有过拟合。下面是一个简单的随机划分脚本import os import random import shutil random.seed(42) image_dir data/images txt_dir data/labels_txt out_root dataset train_ratio 0.8 images [f for f in os.listdir(image_dir) if f.endswith((.jpg, .jpeg, .png))] random.shuffle(images) split_idx int(len(images) * train_ratio) train_images images[:split_idx] val_images images[split_idx:] for subset, img_list in [(train, train_images), (val, val_images)]: img_out os.path.join(out_root, images, subset) label_out os.path.join(out_root, labels, subset) os.makedirs(img_out, exist_okTrue) os.makedirs(label_out, exist_okTrue) for img_name in img_list: shutil.copy( os.path.join(image_dir, img_name), os.path.join(img_out, img_name) ) base_name os.path.splitext(img_name)[0] txt_name base_name .txt txt_src os.path.join(txt_dir, txt_name) if os.path.exists(txt_src): shutil.copy( txt_src, os.path.join(label_out, txt_name) ) else: print(f警告: {img_name} 没有对应的TXT文件已跳过)随机种子固定成42是为了方便复现和校对。如果发现某次划分方式不好还能在固定种子下重复对比。4. 搭建YOLOv5训练环境与配置文件80%的人卡在这一步4.1 环境准备与版本选择进入代码环节我们需要的大件有Python解释器、IDE我建议直接用VS Code或PyCharm社区版、以及YOLOv5源码。这部分如果严格按照步骤走通常十五分钟能全部就绪。关于YOLOv5源码直接在官方仓库拉取master分支或者指定版本的release分支即可。这里我强烈建议固定使用某个已发布的稳定版本不要频繁更新主分支代码因为依赖库版本会跟着变有些版本之间的兼容性问题会额外浪费大量时间。依赖方面YOLOv5官方提供了requirements.txt文件列出所有需要的Python库。在你建好的虚拟环境里执行安装命令例如pip install -r requirements.txt如果网络环境慢建议分步安装。核心依赖其实就这几个大件torch深度学习框架、torchvision与torch配套的视觉工具箱、opencv-python图像处理、numpy数值计算、matplotlib绘图展示。其他小工具一般都能顺带装上。这里有一个特别重要的版本匹配问题。PyTorch的CPU版本和GPU版本安装方式不统一而且CUDA版本、GPU驱动版本、PyTorch版本三者需要互相兼容。对纯小白我建议先装CPU版本跑通流程。即使你用CPU训练整个流程也能走通之后想上GPU再按照自己的显卡驱动版本重新安装对应PyTorch即可。千万别一上来就在GPU环境上折腾先跑通再加速是性价比最高的路。4.2 数据配置文件告诉训练脚本要去哪里找数据进入yolov5目录后先找到data目录创建一个自己的数据配置文件命名随意但要直观比如custom_data.yaml。写三个关键字段train: dataset/images/train val: dataset/images/val nc: 1 names: [helmet]注意这里的train和val路径有两种写法一种是绝对路径直接写全路径一种是相对路径路径相对于你执行训练命令时所在的目录。初学者最容易搞混的就是这个。我的建议是直接使用绝对路径写错了顶多报个路径不存在排查起来非常快相对路径绕来绕去反而难定位。nc是类别数量names是类别名称列表。这两个字段必须和转换脚本里的class_names顺序完全一致。我见过太多人转换脚本里顺序是[car, person]到配置文件名次又是[person, car]类别编号错位模型训练出来的检测框张冠李戴。4.3 模型配置文件与预训练权重选择YOLOv5的模型结构配置文件也放在models目录下常见的有yolov5s.yaml、yolov5m.yaml、yolov5l.yaml。这里的s、m、l代表模型的缩放系数。yolov5s最小、最快、精度相对最低yolov5l最大、最慢、精度相对最高。初学阶段、数据量又不大的情况直接用yolov5s.yaml就够了。如果数据量大、GPU显存充足再考虑大的模型。模型结构配置文件里有一项nc必须改成你的类别数这个一定不能忘。结构配置文件和数据配置文件是两个文件一个管模型结构一个管数据位置两者里的nc要一致。预训练权重方面官方会提供在公开数据集上预训练好的权重文件能显著加速我们的训练收敛。如果从头随机初始化权重开始训练不仅收敛慢而且极易过拟合。但需要注意权重文件的版本必须和仓库代码版本严格对应用新代码搭配旧权重加载时大概率直接报KeyError或者结构不匹配的错。5. 正式训练命令、日志解读与训练时长预期5.1 训练命令的完整含义一切就绪后进入yolov5目录执行训练命令python train.py --img 640 --batch 8 --epochs 50 --data custom_data.yaml --cfg models/yolov5s.yaml --weights yolov5s.pt --name helmet_exp参数看起来多但拆开讲就很清楚图片输入尺寸是640训练时会自动缩放图片到640×640。批次大小为8即每轮处理8张图片这个值受显存限制。训练轮数50轮每轮代表模型完整看一遍全部训练数据。data和cfg分别指向我们的数据配置文件和模型结构配置文件。weights指定使用官方预训练权重。name给本次实验命名训练产物会存到runs/train/helmet_exp目录。批次大小是个值得展开讲的参数。它能设多大主要取决于显存容量。显存不够时会报CUDA out of memory把--batch调小即可比如从8调到4甚至2。批次大小并不只影响显存占用还会影响梯度更新的稳定性通常批次大小越小训练损失曲线抖动越明显但初学阶段不用过度纠结这个。5.2 从训练日志中判断模型是否正常训练过程中终端里会滚动输出每一轮的训练信息。核心要看几个指标。train_loss下降是整个训练过程最主要的风向标。它代表模型在当前批次训练数据上的误差大小理论上随训练轮数上升而逐步下降。如果它一直在下降哪怕速度不快也是在正常推进。如果它上下震荡得很厉害通常说明批次太小或学习率偏大。val_loss是模型在验证集上的误差。它比训练损失更能反映模型的泛化能力。训练损失在降、验证损失也在降通常状态健康。如果训练损失持续降、验证损失却反弹上升说明模型在死记硬背训练集出现了过拟合这时可以考虑增加数据量或增加数据增强。mAP0.5是0.5这个IoU阈值下的平均精度一般认为在0.5以上就算可用的检测效果。它是结果里最直观的“及格线”指标。在训练过程中看到mAP稳步上升也说明一切正常。训练轮数方面我用一个自己的经验公式样本量几百张时50到100轮基本能看出效果样本量几千张时100到300轮比较常见。初学者不用盲目追求更多轮先看损失曲线和mAP趋势模型不再明显进步就停否则只是浪费时间。5.3 训练时间预期与产物清单最常被问的问题就是“大概要训练多久”。答案完全取决于你的硬件。用一个粗略的对比参考设备条件50轮训练耗时参考数据集规模为几百张图纯CPU可能数小时甚至更久入门级GPU显存有限1至2小时左右中高端GPU十几分钟到半小时这里要提前做好心理建设。如果你只有CPU训练会很漫长这不代表流程有问题只是硬件性能瓶颈。初学阶段可以把轮数先设小一点比如10轮验证全流程无误后再加长训练。训练正常结束后会在runs/train/helmet_exp目录下留下这些关键文件weights/best.pt验证集上效果最好的权重这是后续推理的首选。weights/last.pt最后一轮训练结束时的权重有时比best更接近最终状态。results.png整个训练过程的损失、精度等曲线汇总图。train_batch*.jpg、val_batch*.jpg训练和验证过程中的批次图片快照能看到标注框显示效果。6. 可视化推理把模型成果画到图片和视频上6.1 对图片进行单人检测推理训练结束后模型本身已经是一个保存下来的权重文件接下来要做的就是用这个权重去“看图”。YOLOv5官方代码里自带推理入口我们直接用官方脚本即可。一张图片的推理命令示例python detect.py --weights runs/train/helmet_exp/weights/best.pt --source data/images/img_001.jpg --conf-thres 0.25其中--source可以指向单张图片、一个图片文件夹、甚至一段视频文件。--conf-thres是置信度阈值含义是“模型认为某个框是目标的把握达到多少才显示出来”。阈值越低画出的框越多误检也越多阈值越高框越少漏检也越多。0.25是一个比较均衡的起点想看清漏检情况就调到0.1想去掉误检就调到0.4或0.5。出来的结果会存放在runs/detect/exp第二次推理则是exp2以此类推目录下。看到检测结果图上有人、有类别名、有置信度数字说明整套流程已经彻底走通了。如果框没画出来不要急着怀疑模型先确认权重文件路径是否对、推理命令里--source是否真的指向了包含你目标物的图片。6.2 视频与摄像头的在线检测检测入口不仅支持图片也支持视频。只需把--source改成视频文件路径即可python detect.py --weights runs/train/helmet_exp/weights/best.pt --source test_video.mp4 --conf-thres 0.25如果想用摄像头实时检测把--source指向摄像头设备编号例如0或1。这一步能让你更直观地感受到目标检测的实时反馈。画面上每一帧检测框的生成速度取决于模型的推理速度和硬件性能CPU环境下能做到每秒几帧就已经不错了想要流畅的实时检测基本上还是得靠GPU。初次体验建议先用小段视频测试跑通了再尝试摄像头。因为摄像头检测时如果有问题排查难度相对更高环境因素也会更复杂。6.3 模型效果评估的正确打开方式推荐使用官方验证脚本在验证集上做一次全面评估python val.py --data custom_data.yaml --weights runs/train/helmet_exp/weights/best.pt --img 640它会输出精确率、召回率、mAP等指标并生成带标注框的验证图片。评估不只是走个形式——如果精度低说明模型把很多非目标误判成目标可以尝试提高置信度阈值、增加负样本、或者增加训练轮数如果召回率低说明模型漏检过多通常需要补充正样本目标更多、更清晰的图片或者调低置信度阈值来找回一些漏检目标。7. 高频报错排查不用慌按这个思路自查7.1 报错位置决定问题方向小白遇到报错第一反应往往是去复制错误信息到处问人。我的经验是先不慌先看报错发生在哪个阶段。环境依赖问题通常发生在程序刚开始导入库的阶段出现ModuleNotFoundError时解决思路很直接找到缺失的库名装上对应版本即可。训练中期出现的报错通常集中在数据读取和标注格式上而训练中后期的报错则多与显存、硬件资源有关。按阶段排查比盯着错误信息末尾几行去猜要高效得多。我整理了高频报错的样板见下表报错现象最常见原因解决动作ModuleNotFoundError: No module named torch环境未装好或装错环境确认当前激活的是哪个虚拟环境再重新安装依赖WARNING: no labels found标签路径不对或标签目录为空检查数据和标签目录是否一一对应确认图片与TXT文件名一致CUDA out of memory显存不足调小batch大小或改用CPU训练assert能在图像中找到对象标签文件里坐标异常或空文件重新检查转换脚本输出打开TXT文件看是否有0开头的有效行KeyError: model.0.conv1.weight权重和模型结构版本不匹配下载与当前代码版本匹配的同系列权重重新加载7.2 一个典型的错误排查过程演示举个例子假设你训练时全部图片都提示no labels found但标签目录里明明有TXT文件。这时候不要急着改数据配置文件。先做一个最基础的检查运行一个Python命令打印标签目录和图片目录的实际路径确认程序看到的目录和你以为的目录是不是同一个。然后随机打开一个TXT文件看看里面是否有内容是否合法。如果TXT确实是空的回到转换脚本里检查XML里name是否写错特别是大小写是否和class_names列表完全一致。如果TXT有内容但训练仍报无标签大概率是路径没对上图片目录和标签目录的层级关系不符合要求。8. 训练完成后还能再优化什么三个立竿见影的调整方向8.1 扩展数据时不要只加图片数量初学者很容易觉得“效果差就是数据不够”然后拼命加图片。但数据数量不是唯一的变量平衡性和多样性同样关键。增加更多不同光线、不同背景、不同角度的目标图片往往比简单堆数量更有效。我这里就见过不少反例一个项目里所有图片都是同一时间、同一个角度拍的模型在这个场景表现很好换一个场景就完全失灵。8.2 数据增强的保守与激进YOLOv5默认开启了一部分数据增强策略。训练时如果你觉得模型在验证集上的效果越来越差可以检查数据配置里增强相关的参数。初学者不建议自己动手过多调增强参数但要懂得一个原则增强力度小模型容易对训练集过拟合增强力度大训练时间明显变长收敛也可能变慢。简而言之先用默认参数跑有明确问题再针对性调整。8.3 从单类到多类一次只改一个配置有些人在单类模型跑通之后立刻把类别数扩展到十几个然后训练出奇怪的结果——有的类全是误检有的类一个框都没有。多类扩展时我的建议是一次只增加一个类别并且新增类别时保持原有类别数据量不变这样出了问题能很快定位到是哪个新类别影响了模型。实操经验小结整个流程走下来你会发现目标检测并没有听上去那么神秘。它本质上就是“准备数据、配置环境、训练模型、推理验证”的循环。标注的耐心和配置细节的严谨才是决定成败的核心而不是代码有多高深。对纯新手来说我最大的建议是第一次做的时候不要追求一次搞定大而全的复杂场景就选一个很小的任务比如“桌面上的杯子”拍50到100张图片快速跑完整个流程把每个环节都亲手点一遍、敲一遍、看一遍。等这条链路完全跑通了你遇到真正的项目问题时才知道该往哪个环节去查。别怕报错报错本质上就是在教你每个环节背后的机制。我最早也是从一个个红色的错误信息中把这条链路摸清楚的。