☰
YOLOv5目标检测实战:从网络结构到RK3568树莓派部署全解析
2026/10/5 3:01:43 网站建设 项目流程

简介:面向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, encoding="utf-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_location="cpu") 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.yaml

YOLOv5 训练自己的数据集,核心只有三样东西:图片目录、标签目录、数据集配置文件。我见过的初学者踩坑,大多数不是模型的问题,而是这三样的组织方式不对。最标准的目录结构是这样:

dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

images 和 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 对应 txt,png 也对应 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_dataset

train.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 变成 nan,val 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 的 mAP@0.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 集 mAP@0.5 有 0.85,但拿到实际场景拍照或视频里,新环境几乎没用,漏检误检明显。

原因:数据泄露或验证集目标尺寸单一。很多人从同一段视频里抽帧划分 train/val,相邻帧几乎一模一样,模型等于把验证集背下来了。另一种情况是验证集里所有目标都占图片很大面积,模型的小目标能力完全没有被评测到。

解决:用时间上和训练集完全不同的另一段视频做验证集。检查 val 图片里目标高度占图片高度的比例,如果全部高于 20%,说明小目标评测缺失,需要在 val 里补充小目标占比超过 30% 的图片。这条坑在锥桶、路障这类小目标场景里最常见,mAP 看着高,一上实拍就现原形。

5. 从本机到边缘:后处理参数、RK3568 量化和树莓派部署

5.1 后处理链路:从模型输出到最终检测框

YOLOv5 的模型输出不是直接可用的检测框。前向推理后你拿到的是一个形状为1×(num_anchors×3)×(5+nc)的张量,以 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(dim=1) keep = ops.nms(boxes, cls_scores, iou_threshold=0.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-json

val.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:i+4]] if len(row) < 4: # 最后一批补一张黑图保持宽度一致 row.append(cv2.imread(imgs[0])) canvas = cv2.hconcat(row) cv2.imwrite(f"review_{i // 4}.jpg", canvas)

这里的逻辑很简单:把每 4 张检测结果图拼成一行,然后批量生成 review 图。如果你做的是锥桶识别这类小目标任务,建议专门准备一组遮挡、远距离、反光角度刁钻的图片作为复核集,不要只挑好认的图自欺欺人。我自己最早在树莓派 5 上部署自己训练的模型时,就是只看 val 指标,结果实拍时被一堆远距离小目标打脸。后来固定了一套双保险:先跑 val.py 看 per-class AP,再抽一个与训练集时间不重叠的视频做批量拼接人工复核。这套流程虽然土,但真的能在交付前拦住大多数问题,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询