ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLOv5目标检测实战:从网络结构到RK3568树莓派部署全解析

YOLOv5目标检测实战:从网络结构到RK3568树莓派部署全解析 简介面向YOLOv5入门与进阶学习者的完整配套代码仓库内容来自B站「手把手带你实战YOLOv5」系列课程覆盖入门、拓展、进阶、部署四个篇章适合希望系统掌握目标检测框架从环境安装、数据集构建、模型训练到界面交互与推理部署全流程的读者。压缩包共340个文件约275.62MB以py脚本、yaml配置、ipynb文档、图片素材为主另含模型权重、TensorRT引擎、容器配置与训练日志便于对照视频复现实验。已有138人学习目录按入门、拓展、进阶、部署四个篇章组织检索方便。资源内含PySide6用户界面、Gradio Web GUI、MobileNet主干替换、SE注意力机制、TensorRT加速等关键实践代码并提供AutoDL服务器训练、C2f结构修改、TorchHub模型预测与Flask部署相关文件。覆盖环境安装、数据构建、模型训练、界面开发与云端部署等环节可帮助边看视频边动手打通模型训练到生产部署的完整链路。1. 一个压缩包装下整套目标检测实战YOLOv5 凭什么能行一个压缩包就能装下一整套目标检测实战这大概就是 YOLOv5 让我愿意反复折腾它的原因。标题里这个 YOLOv5.zip 并不是某个官方发布的成品而是一套你可以自己组装、自己训练、自己部署的目标检测代码集合里面既有网络结构、训练脚本也有数据组织和超参数配置。你拿到的不是“点开就能用”的黑盒而是一条把模型从 0 训练到边缘设备落地的完整路径。它适合两类人一类是刚入门目标检测、想用自己的数据集训出能跑模型的开发者另一类是正在为 RK3568、树莓派这类边缘设备选型的工程师。接下来的内容我会按一个常规的落地流程往下拆先跑通推理再训练自己的数据最后处理后处理和边缘部署。2. 跑通最小闭环YOLOv5 网络结构与本地推理的命令级拆解2.1 网络三段式Backbone、Neck、Head 各自的选型理由打开一张标准的 YOLOv5 网络结构图你会发现它不是你想象中那种一条直线的 CNN而是三个模块拼接出来的管道Backbone 负责提特征Neck 负责把不同尺度的特征融合Head 负责在融合后的特征上预测。Backbone 用的是 CSPDarknet里面的 C3 模块把特征图沿通道切成两份一份直接传递一份经过卷积再和传递的部分汇合。这样做的理由是让梯度回传时有更多“短路”路径避免网络太深之后梯度消失。Neck 用的是 PANet 结构也就是自顶向下和自底向上两条路径交替融合。自顶向下路径把小目标的语义信息传到大尺度特征上自底向上路径再把大目标的细节传回去两轮下来特征图同时有了语义和位置信息。Head 部分则输出三个尺度的预测分别对应 80×80、40×40、20×20 的特征图前两个负责检测小目标和中目标最后一个负责大目标。这个三段式结构决定了后面所有参数的调试方向Backbone 决定你提的特征够不够好Neck 决定多尺度目标能不能都照顾到Head 决定输出框的密度。看结构图时不要逐层去背先定位三处关键参数。stride 是 [8, 16, 32]确定了三个预测层的空间步长等于说一张 640×640 的图最后会被压成 80×80、40×40、20×20 三张特征图。nc 表示类别数如果训练自己的数据集只有 1 类这个值要改成 1。anchor 是预设的框尺寸近几个版本的 YOLOv5 会在训练时自动从数据里重新聚类所以不需要手工精调但要注意它是在 imgsz 上聚类的结果大改 imgsz 时最好让程序自己重新算。2.2 跑通 detect.py 的最小命令与权重加载逻辑先跑一个最小推理命令把 YOLOv5 跑起来再谈训练的事。假设你已经按官方仓库的 requirements.txt 装好了依赖并且手里有一个预训练权重下面这条命令就是最快看到检测结果的路径# 最小推理命令单张图片用官方预训练权重 python detect.py \ --weights yolov5s.pt \ --source data/images/bus.jpg \ --conf-thres 0.25 \ --iou-thres 0.45 \ --project runs/detect \ --name test_run这条命令里的 conf-thres 是置信度阈值低于 0.25 的框会被直接丢弃iou-thres 是 NMS 时的 IoU 阈值两个值直接影响最终框的数量和重叠情况。project 和 name 决定输出目录如果同样的 name 跑第二次会自动生成 test_run2不会覆盖旧结果。第一次跑的时候YOLOv5 会先检查模型结构然后加载权重并自动下载对应的 pt 文件之后才对图片进行推理。detect.py 加载的 .pt 文件不是单纯的权重而是把模型结构、训练超参数和类别名称一起打包的存档文件。所以换自己的 best.pt 进去就算类别数变了程序也能自动识别。推理完成后结果图保存在 runs/detect/test_run 下同时还会生成一个 labels 目录里面每个 txt 就是每张图检测到的物体信息。这个 txt 内容就是 YOLO 格式的标签解析时要注意它是归一化坐标import glob for txt in glob.glob(runs/detect/test_run/labels/*.txt): with open(txt, encodingutf-8) as f: for line in f: # 每行class_id, cx, cy, w, h全部是归一化坐标 cls, cx, cy, w, h map(float, line.split()) # 转像素坐标假设原图是 640x640 x1 int((cx - w / 2) * 640) y1 int((cy - h / 2) * 640) x2 int((cx w / 2) * 640) y2 int((cy h / 2) * 640) print(int(cls), x1, y1, x2, y2)这里需要特别说明detect.py 保存的 txt 已经是后处理完毕的结果不是网络原始输出。如果你要拿它做可视化或者转成 VOC 格式一定要记得乘回原图尺寸而不是直接用归一化值。很多人在这里图省事结果框的位置整体偏移尤其是图片不是正方形时偏移更明显这是最容易翻车的第一道暗坑。2.3 从 pt 文件里读出模型配置排查网络结构不一致的便捷手段当你怀疑“为什么我改的数据集没生效”或者“类别数怎么还是 80”时不要猜直接从 pt 文件里读配置。torch.load 可以把模型存档里的配置信息打出来import torch ckpt torch.load(yolov5s.pt, map_locationcpu) model ckpt[model].float() print(类别数:, model.yaml.get(nc)) print(anchors:, model.yaml.get(anchors)) print(stride:, model.stride)这段代码的核心价值在于训练和推理两个环节经常因为模型配置不一致导致结果对不上比如你用 1 类的 best.pt 去 detect.py 推理没问题但换到其他脚本里就报 “nc 不匹配”。通过读 model.yaml 能快速确认当前文件到底是什么配置而不是盲猜。如果你发现类别数不对十有八九是你训练时 data.yaml 写错或者加载了错误的权重文件。3. 训练自己的数据集从标注格式到超参数一次说清3.1 数据格式三件套images、labels、data.yamlYOLOv5 训练自己的数据集核心只有三样东西图片目录、标签目录、数据集配置文件。我见过的初学者踩坑大多数不是模型的问题而是这三样的组织方式不对。最标准的目录结构是这样dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamlimages 和 labels 下必须有同名的子目录而且标签文件要和图片文件同名、不同扩展名。比如 image 是img_001.jpg标签就是img_001.txt如果扩展名对不上训练时会被静默跳过。标签文件里每一行对应一个目标格式是五列class_id cx cy w h其中 cx、cy 是中心点的归一化坐标w、h 是宽高的归一化值。这和 COCO 的左上角坐标格式完全不一样转换时最容易错。data.yaml 则是最容易忽略的一环。它的内容很简单但路径一旦写错训练直接报错或者数据加载为空# 数据集配置路径写在当前 yaml 文件所在的目录下 train: ../dataset/images/train val: ../dataset/images/val nc: 1 # 类别总数从 0 开始编号 names: 0: cone # 类别名尽量用英文小写注意train 和 val 写的是 images 目录的路径YOLOv5 会自动把images替换成labels去读标签文件所以你只需要维护 images 里的图片与 labels 里的 txt 对应即可。names 这个 dict 的 key 必须是整数和标签文件里的 class_id 对应不然类别名会错乱。3.2 VOC 转 YOLO 格式的转换脚本与 4 个边界坑如果你手头的标注导出来是 VOC 的 XML或者从 Roboflow 等平台导出的格式是 COCO都要先转成 YOLO txt。VOC 转 YOLO 是最常见的场景脚本本身不复杂但有几个边界条件稍不注意就会让训练集白费。下面这段就是我常用的转换脚本import xml.etree.ElementTree as ET # VOC XML 的 object 节点里是 bndbox坐标为 x1 y1 x2 y2 像素值 for xml_file in sorted(p.glob(Annotations/*.xml)): tree ET.parse(xml_file) root tree.getroot() size root.find(size) w_img int(size.find(width).text) h_img int(size.find(height).text) lines [] for obj in root.iter(object): cls_id class_names.index(obj.find(name).text) # 从 0 开始 bndbox obj.find(bndbox) x1, y1 int(bndbox.find(xmin).text), int(bndbox.find(ymin).text) x2, y2 int(bndbox.find(xmax).text), int(bndbox.find(ymax).text) # 转成 YOLO 的归一化坐标 cx (x1 x2) / 2 / w_img cy (y1 y2) / 2 / h_img w (x2 - x1) / w_img h (y2 - y1) / h_img lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out.write(\n.join(lines))这里面有 4 个边界坑我一个个说清楚。第一个坑是类别 id 从 0 开始编号而不是 1。VOC 的标签名转成 id 时如果写成从 1 开始第一类会整体偏移成第二类训练时 loss 能收敛但输出全是错类别。第二个坑是归一化分母要用图片实际宽高而不是 640。有些人图省事直接除以 640小尺寸图片的坐标就会整体偏移训练结果看似正常但目标框位置总差一截。第三个坑是标签文件和图片同名、不同扩展名。jpg 对应 txtpng 也对应 txt但如果你用 xml 直接改成 txt 而不转内容训练时解析会直接报错。第四个坑是 train/val 的划分要按固定规则不要简单取前 80%。常见做法是先把所有文件名按哈希打乱再按 8:2 切分这样不会出现训练集全是白天、验证集全是晚上的尴尬局面。3.3 train.py 命令、超参数文件与 loss 曲线怎么读数据准备好之后训练命令本身很直接。关键是要先理解超参数文件很多模型效果不理想不是网络结构的问题而是超参数完全没动过。YOLOv5 的默认超参数在 hyp.scratch-low.yaml 里最重要的几个参数如下# hyp.scratch-low.yaml 的关键项按需修改 lr0: 0.01 # 初始学习率自己的小数据集建议调到 0.001 momentum: 0.937 weight_decay: 0.0005 warmup_epochs: 3.0 # 预热轮数前 3 轮 lr 从很小值爬升 mosaic: 1.0 # 马赛克增强概率小目标任务可以降到 0.5 close_mosaic: 10 # 最后 10 轮关闭 mosaic帮助模型收敛lr0 是初始学习率预训练权重继续训练时 0.01 的问题不大但如果是非常小的数据集比如只有几百张图调到 0.001 更稳。mosaic 是 YOLOv5 最有名的数据增强把四张图拼成一张对小目标任务是把双刃剑一方面增加了样本多样性另一方面也把目标缩得太小导致小目标更难学。close_mosaic 这个参数很多人不知道它的作用是训练最后 N 轮关闭 mosaic 增强让模型在接近真实数据分布上微调。如果你的验证集 mAP 总是比训练集差一截先检查它有没有设置。准备好之后启动训练python train.py \ --data dataset/data.yaml \ --weights yolov5s.pt \ --imgsz 640 \ --batch-size 16 \ --epochs 100 \ --hyp hyp.scratch-low.yaml \ --project runs/train \ --name my_datasettrain.py 的常用参数里--weights 用预训练权重做迁移学习只改 Head 部分收敛速度远快于从零训练。如果要从零开始把 --weights 换成空字符串。训练过程中重点关注 box_loss、cls_loss、obj_loss 三条曲线尤其是 val 曲线。如果 val loss 在最后 10 轮没有明显下降而训练 loss 还在降说明模型开始过拟合。best.pt 和 last.pt 保存在 runs/train/my_dataset/weights/ 下best.pt 是验证集上 mAP 最高的权重last.pt 是最后一轮的权重两者都要留着last.pt 经常能用来做进一步微调。4. 训练与推理最常见的 5 个坑现象、原因与止血方案4.1 Loss 一开始就是 nan 或 inf现象训练前 5 轮 loss 变成 nanval loss 也没法看终端里直接刷红。原因最常见的是学习率太大新初始化的 Head 权重对梯度敏感其次是输入图片中包含全黑、全白这类灰度方差接近 0 的图归一化时出现数值问题还有可能是在部分显卡上混合精度触发溢出。解决先用--adam切到 Adam 优化器同时把 lr0 降到 0.001 复训 5 轮这是最快速的中止方案。如果还 nan用 PIL 扫一遍所有训练图片把方差接近 0 的图删掉或替换。最后如果依然不行把--amp关掉先用 float32 跑通再回头调混合精度。这个坑的止血顺序就是降 lr、清异常图、关 amp。4.2 mAP 卡在 0.5 上不去训练集却接近 0.9现象训练集 loss 一直降val 的 mAP0.5 在 0.5 附近横盘两者差距越拉越大。原因过拟合加上 mosaic 数据增强没有在训练末期关闭。mosaic 拼接出来的图片分布和真实场景不一样模型在增强后的数据上练得很“嗨”一到验证集就露馅。解决在 hyp 文件里设置close_mosaic: 10即最后 10 轮关闭 mosaic。同时检查 random_perspective 的 degree 参数如果设得太大目标形态被严重扭曲也会导致验证集表现差。我一般会把 degree 控制在 5.0 以内scale 控制在 0.5 以内先保证增强不要过于激进。4.3 显存 OOM程序直接崩现象启动 train.py 后几秒终端报 CUDA out of memory进程直接退出。原因batch-size 和 imgsz 的乘积超过显存容量。YOLOv5 的默认 batch 是 16但如果你用 8GB 显存还坚持 640 的输入尺寸大概率爆显存。解决第一轮训练用--batch-size -1让 YOLOv5 根据当前显存自动搜索一个能跑通的 batch或者手动把 batch 减半、imgsz 从 640 降到 512。如果 batch 实在太小训练不稳定常见做法是调大 epochs 来弥补。另外一个隐藏技巧是开启梯度累积在 train.py 里用--accumulate参数手动控制累积步数。4.4 推理时大量漏检调低 conf_thres 又出现成片误检现象模型训练出来 val mAP 还行但实际视频里目标一闪而过就漏把 conf-thres 调低之后背景区域又冒出一堆框。原因任务场景和置信度阈值不匹配或者 NMS 的 iou_thres 太高小目标重叠框被全部抑制。很多人只调了 conf没调 iou结果漏检本质上是 NMS 把相邻的框合并掉了一个。解决先用--conf-thres 0.1 --iou-thres 0.3跑一遍看候选框到底有没有被检出来。如果候选框都在只是被 NMS 吞了把 iou_thres 调到 0.3 再对比。如果候选框本身就没检出那就要回训练阶段看小目标的增强策略了。4.5 验证集 mAP 高但实拍部署效果差现象val 集 mAP0.5 有 0.85但拿到实际场景拍照或视频里新环境几乎没用漏检误检明显。原因数据泄露或验证集目标尺寸单一。很多人从同一段视频里抽帧划分 train/val相邻帧几乎一模一样模型等于把验证集背下来了。另一种情况是验证集里所有目标都占图片很大面积模型的小目标能力完全没有被评测到。解决用时间上和训练集完全不同的另一段视频做验证集。检查 val 图片里目标高度占图片高度的比例如果全部高于 20%说明小目标评测缺失需要在 val 里补充小目标占比超过 30% 的图片。这条坑在锥桶、路障这类小目标场景里最常见mAP 看着高一上实拍就现原形。5. 从本机到边缘后处理参数、RK3568 量化和树莓派部署5.1 后处理链路从模型输出到最终检测框YOLOv5 的模型输出不是直接可用的检测框。前向推理后你拿到的是一个形状为1×(num_anchors×3)×(5nc)的张量以 640×640 输入、COCO 80 类为例这个张量是 1×25200×85。25200 等于三个尺度上的候选框数量之和80×80 40×40 20×20然后再乘以每个位置 3 个 anchor。你要做的就是把这三万多个候选框经过解码、阈值过滤、NMS 三步变成图像上几十个有效框。import torch import torchvision.ops as ops # pred: [1, 25200, 85] 的模型输出 pred model(img)[0].float() pred pred[0] # [25200, 85] # 按置信度过滤第 5 个维度是 obj 分数 conf pred[:, 4] mask conf 0.25 pred pred[mask] # 坐标已经是归一化值乘回 640 转成像素坐标 boxes pred[:, :4] * 640 scores pred[:, 4] * pred[:, 5:] # 目标分数 * 类别概率 cls_scores, cls_ids scores.max(dim1) keep ops.nms(boxes, cls_scores, iou_threshold0.45)这段代码里最关键的一步是scores pred[:, 4] * pred[:, 5:]。YOLOv5 的置信度分数不是单一值而是目标存在分数和类别概率的乘积。只取类别概率或者只取目标分数都会导致置信度失真。另一个细节是 NMS 要按类别分别做上面的代码直接用全局最大类别分数做 NMS 在单类别任务里没问题但多类别场景必须循环每个类别分别执行否则两个不同类的高度重叠框会被错误地合并掉一个。如果你的部署脚本是自己写的而不是用 YOLOv5 仓库自带的检测类后处理这块就是最容易出现“模型能跑但结果全乱”的地方基本每两位新手同事就会有一个在这里翻车。5.2 导出 ONNX 与 RK3568 量化两个最容易翻车的地方边缘设备上一般不会直接跑 PyTorch常见做法是先导出 ONNX再转到目标平台的推理格式。RK3568 这类带 NPU 的芯片流程是 ONNX 转 RKNN 再做 int8 量化。导出这一步的命令如下python export.py \ --weights runs/train/my_dataset/weights/best.pt \ --include onnx \ --opset 11 \ --simplify \ --batch-size 1注意--batch-size 1这个参数导出时固定成单 batch后续转 RKNN 时最稳。如果导出时开了动态维度虽然 ONNX 检查时看起来没问题但 RKNN 工具链对动态 shape 的兼容性很差量化阶段容易直接失败。导出的 ONNX 先用 Netron 打开看一眼输入输出 shape确认输入是 1×3×640×640、输出不是带动态轴的形状再进 RKNN 工具链。int8 量化最容易忽略的是校准集。RKNN 转 int8 需要你提供一批代表性图片做校准校准集的分布必须和真实场景一致。我见过有人随便拿 20 张网图做校准结果模型在真实场景里 mAP 掉了 5 个点。正确做法是从验证集里按场景均匀抽 100 张以上保证每种类别和大中小目标都有覆盖校准集图片尺寸要和训练尺寸一致。5.3 树莓派 4B/5 与 ROS 无人小车上的部署取舍树莓派 4B 是 ARMv8 CPU没有 NPU直接跑 fp32 的 YOLOv5s 大概只有 3-5 FPS基本只能做离线推理。树莓派 5 的 CPU 强不少但也不适合直接上大模型。两条路比较常见一是用 TorchScript 模型加 PyTorch CPU 跑不改代码适合快速验证但速度上限很低二是导出 ONNX 后用 NCNN 或 RKNN 跑NCNN 在树莓派上比 PyTorch CPU 快 1.5-2 倍但需要额外处理一些算子兼容问题。如果项目允许我一般会把输入尺寸从 640 降到 320速度能提升近一倍对锥桶这类单一目标场景来说精度损失完全可接受。如果你在 ROS 无人小车上部署核心不是推理引擎的选择而是图像数据的流转方式。常见做法是把推理封装成一个 ROS 节点订阅/camera/image_raw用 cv_bridge 把sensor_msgs/Image转成 cv::Mat推理后再把检测结果发布成自定义消息。这个架构里模型推理和总线解耦换模型只需要改节点内部。如果你用的是树莓派 5 上部署自己训练的 YOLOv5 模型建议先在本机用 NCNN 跑通再挪到 ROS 节点里不要一上来就把推理和 ROS 混着写否则以后排查问题非常痛苦。6. 从“能跑”到“能交付”批量验证、每类 mAP 与锥桶场景的最终复核大部分人的训练流程止步于跑完 train.py看到 best.pt 就以为任务结束了。实际上交付前还有一个环节比训练本身更重要批量验证和人工复核。YOLOv5 自带的 val.py 可以一次性输出整体 mAP 和每一个类别的 mAP这一步能让你看清模型到底在哪类目标上拖后腿。python val.py \ --data dataset/data.yaml \ --weights runs/train/my_dataset/weights/best.pt \ --conf-thres 0.25 \ --iou-thres 0.45 \ --batch-size 8 \ --save-jsonval.py 的终端输出里有一行 per-class AP 表每一行对应 data.yaml 里的一个类别。如果你的任务是多类别一定要逐类看而不是只看整体 mAP。整体 mAP 可能被样本量大的类别拉高样本量少的类别实际 AP 可能只有 0.4这种情况下直接部署一定会出问题。验证完指标后我还习惯做一步人工复核把多个检测结果图横向拼接成一张大图用肉眼扫一遍。这一步对锥桶这类单类别任务尤其重要因为锥桶形状相近、姿态多变、远处目标小模型很容易出现“训练指标好、实拍就飘”的情况。我的习惯是写一个简单的拼接脚本每 4 张结果图拼一行快速翻看。import cv2, glob # 把多张检测结果图横向拼接常用于锥桶这类单类别任务的复核 imgs sorted(glob.glob(runs/detect/review/*.jpg)) for i in range(0, len(imgs), 4): row [cv2.imread(p) for p in imgs[i:i4]] if len(row) 4: # 最后一批补一张黑图保持宽度一致 row.append(cv2.imread(imgs[0])) canvas cv2.hconcat(row) cv2.imwrite(freview_{i // 4}.jpg, canvas)这里的逻辑很简单把每 4 张检测结果图拼成一行然后批量生成 review 图。如果你做的是锥桶识别这类小目标任务建议专门准备一组遮挡、远距离、反光角度刁钻的图片作为复核集不要只挑好认的图自欺欺人。我自己最早在树莓派 5 上部署自己训练的模型时就是只看 val 指标结果实拍时被一堆远距离小目标打脸。后来固定了一套双保险先跑 val.py 看 per-class AP再抽一个与训练集时间不重叠的视频做批量拼接人工复核。这套流程虽然土但真的能在交付前拦住大多数问题希望帮到你。本文还有配套的精品资源点击获取
返回列表