前言
YOLOv8 这套东西我用了一年多,从最早拿它跑路口车流量统计,到后面做小目标检测、实例分割、人体关节点估计,前后踩的坑不算少。很多人第一次接触 YOLOv8,脑子里只有一个模糊印象:一个"又快又准"的检测框架,装好环境、下个权重、跑一行yolo predict就完事。但真要做项目——尤其是本科毕业设计、工业质检、安防园区这类需要改结构、换数据集、上板子的场景——只会在命令行里敲命令是远远不够的。
问题的根子在于,YOLOv8 把三个任务塞进了同一套代码框架里:目标检测(detect)、实例分割(segment)、关节点估计(pose)。它们共享同一个骨干网络、同一个特征融合结构,只在检测头和解码方式上分叉。如果你不理解这个"共用骨架 + 分叉头"的设计逻辑,就会陷入一种很憋屈的状态:模型能跑,但一改就崩,一看输出张量就懵,一调参数就掉点。比如分割任务的输出为什么是4+nc+32个通道?那 32 是什么?关节点为什么每个点输出三个值而不是两个?这些数值背后都是有明确工程含义的。
这篇文章我想做的事情很直接:把 YOLOv8 在目标检测、实例分割、关节点估计三个任务上的原理掰开揉碎讲清楚,从骨干结构、颈部融合、解耦头设计,到正负样本分配、损失函数构成、输出张量的解码流程,再到环境配置、训练参数、导出部署这些实操环节。适合两类人看:一类是刚入门 YOLOv8、想知道"它到底怎么工作的"的同学;另一类是做了一段时间、想改进结构或者上嵌入式设备(比如 RK3588 这类带 NPU 的板子)、需要搞清楚每个张量含义的工程师。
我尽量不写成教科书,按自己实际调试的顺序来讲,中间会穿插不少"我试过""实测下来""这地方我踩过坑"之类的经验,也会给出能直接抄的参数和命令。有些细节(比如某些超参的默认值、某些层的具体实现)不同版本会有微调,我会在讲到的时候说明"以常见实现为准",你对着自己的版本核一下就行。
1. 三任务共用一套骨架:YOLOv8 的整体架构设计思路
1.1 从 YOLOv5 到 YOLOv8,结构上到底改了哪几处
要理解 YOLOv8,最快的路径是拿 YOLOv5 做对照。两者血缘关系很近,但 YOLOv8 在几个关键位置动了刀,这些改动恰恰决定了它后面能统一支撑三个任务。
第一处是骨干里的模块替换。YOLOv5 用的是 C3 模块,YOLOv8 换成了C2f。名字里的 f 是 "faster" 的意思,核心思路借了 ELAN 的设计:输入特征先做一次 1×1 卷积压缩通道,然后 split 成两路,一路直接往后传,另一路反复经过多个 bottleneck 再拼接回去。这样做的好处是梯度路径更多、跨层信息保留得更充分,同时参数量还压得住。实测下来,同样深度下 C2f 比 C3 的梯度流更顺,训练早期 loss 下降明显更稳,不太容易在前几个 epoch 出现那种"抖一下又弹回去"的情况。
第二处是把检测头从耦合改成解耦。YOLOv5 的检测头里,分类和回归是共用一部分中间特征的;YOLOv8 改成了两条完全独立的卷积分支,分类走分类的,回归走回归的。这个改动在检测任务上提升明显,因为分类关心的是"这是什么",回归关心的是"边界在哪",两者的特征偏好本来就不一样,硬绑在一起会互相牵制。
第三处是彻底转向Anchor-Free。YOLOv5 还是基于 anchor 的,你需要根据数据集统计出一组先验框尺寸,重新聚类;YOLOv8 直接去掉了 anchor,每个特征点直接预测它到目标四条边的距离。对我这种经常换数据集的人来说,这一步省了太多事——以前换个鸟类数据集、换个水果数据集,第一件事就是跑 k-means 重新聚 anchor,聚得不好 mAP 直接掉几个点。现在不用管了。
第四处是回归方式换成了DFL(Distribution Focal Loss)。传统做法是直接回归一个连续的距离值,YOLOv8 改成预测一个离散分布,再求期望。这个我放到后面检测损失那节详细说,先记住结论:它让边界回归在小目标和遮挡目标上更稳。
整体骨架可以概括成三段:Backbone(骨干,提特征)→ Neck(颈部,做多尺度融合)→ Head(头部,出结果)。Backbone 在 640 输入下依次输出 1/8、1/16、1/32 三个尺度的特征图,也就是 80×80、40×40、20×20。Neck 用的是 PAN-FPN 结构,自顶向下传一次语义、自底向上传一次定位,两轮下来让每个尺度都同时具备大目标的语义信息和小目标的细节信息。Head 就挂在这三个尺度上,各出一组预测。
提示:如果你看网络结构图看得头晕,一个简单的判断方法——数一下输出特征图的分辨率是 80、40、20 还是别的。只要记住 640 输入对应 80/40/20,其他尺寸按比例推就行,比如 1280 输入就是 160/80/40。
1.2 三个任务的分叉点:头部输出的通道数差异
三个任务共用同一套 Backbone 和 Neck,真正的差异全部集中在 Head 输出的通道数上。这一点非常关键,也是我见过最多人卡住的地方——明明加载的是同一个.pt权重,为什么有的输出 84 通道,有的输出 116 通道,有的输出 56 通道?
答案就在下面这张表里。以 640 输入、COCO 80 类、三个尺度共 8400 个特征点为例:
| 任务 | 输出张量形状 | 通道构成 | 额外输出 |
|---|---|---|---|
| 目标检测 | [B, 4+nc, 8400] | 4 个框偏移 + nc 个类别分数 | 无 |
| 实例分割 | [B, 4+nc+32, 8400] | 4 + nc + 32 个 mask 系数 | proto 原型 [B, 32, 160, 160] |
| 关节点估计 | [B, 4+1+3k, 8400] | 4 + 1 个人类别 + 3k 个关键点分量 | 无 |
8400 这个数字怎么来的?80×80 + 40×40 + 20×20 = 6400 + 1600 + 400 = 8400。Anchor-Free 之后,每个特征点就是一个预测位置,所以总数就是三个尺度特征图的像素数之和。
目标检测的 4 是(l, t, r, b),即该点到目标左、上、右、下四条边的距离,单位是特征图像素(解码时要乘回 stride)。nc 就是类别数,COCO 是 80,你自己的数据集是几类就是几。
实例分割多出来的 32 是mask 系数,它本身不是掩码,而是用来和 proto 原型做线性组合的权重。这也是为什么分割模型必须额外输出一个 proto——真正的高分辨率掩码信息存在 proto 里,32 个系数只是"怎么组合"的配方。
关节点估计里,4+1 中的 1 是"是不是人"的置信度,3k 是 k 个关键点,每个点的 3 个值分别是x、y和可见性置信度visibility。COCO 人体是 17 个点,所以是 51 个通道。很多人第一次看到3k会疑惑:为什么不是 2k?因为关键点存在"被遮挡但位置可推断"和"完全不在画面里"的区别,多出来的一维就是用来表达这种状态的。
注意:关节点估计的 YOLOv8 官方实现只做单类(person),也就是说
nc=1。如果你想做多类姿态(比如同时估人手和动物骨架),得自己改头部的类别分支和数据集组织方式,这不是改个 yaml 就能解决的。
2. 目标检测分支:从特征图到边界框的完整链路
2.1 解耦头与 Anchor-Free 的取舍逻辑
先说解耦头为什么值得单独拿出来讲。在耦合头里,分类和回归共享同一个卷积塔,最后再分叉出两个 1×1 卷积输出。问题在于,分类任务希望特征对"语义类别"敏感,回归任务希望特征对"空间边界"敏感,两者的最优特征子空间并不重合。共享卷积塔相当于让两个目标妥协,谁都拿不到最优解。
YOLOv8 的做法是让两条分支各走各的,每个尺度上分别有独立的卷积序列,中间不共享参数。代价是参数量和计算量略微上升,收益是精度稳定提升。我在自己的水果检测数据集上对比过:同样的数据、同样的训练轮数,解耦头比耦合头 mAP50 高大约 1.5 到 2 个点,小目标(单个果子在图中占比很小)上的提升更明显。
Anchor-Free 的取舍则更偏工程实用。基于 anchor 的方法有个天然优势:先验框提供了尺度上的归纳偏置,在网络容量有限时能加速收敛。但它要求你为每个数据集重新调 anchor,泛化到新场景时容易失配。Anchor-Free 直接预测距离,省掉了这部分调参成本,代价是训练初期收敛稍慢,需要更充分的 warmup。
这里有个实操细节:Anchor-Free 预测的四个距离是不加约束的实数,理论上可以是负数或超大值。所以训练时正样本分配的质量就直接决定了回归质量——这也是为什么 TaskAlignedAssigner 这套分配策略在 YOLOv8 里地位这么重。分配错了,网络就会去拟合一个根本不对应的目标,loss 再怎么降,mAP 也上不去。
2.2 TaskAlignedAssigner:正样本凭什么分给它
正负样本分配,是目标检测里最容易被忽视、但影响最大的一环。简单说,一幅图里 8400 个预测点,绝大多数是背景(负样本),只有少数几个应该负责预测真实目标(正样本)。分配策略决定了"哪些点算正样本、每个正样本对应哪个 GT"。
YOLOv8 用的是TaskAlignedAssigner,核心思想是:一个预测点够不够格当正样本,要看它"分类得分"和"定位精度"的综合表现,而不是像早期方法那样只看 IoU 或者只看中心点落在哪个格子。
具体流程分四步。第一步,先算每个预测点和每个 GT 的对齐度量:
align_metric = cls_score^alpha × iou^beta这里cls_score是该点对 GT 类别的预测分数,iou是预测框和 GT 框的 IoU。alpha默认 0.5,beta默认 6.0。beta 远大于 alpha,说明这套策略更看重定位质量——IoU 高的点优先级更高。这个数值选择是有讲究的:如果 alpha 太大,会让分类分高的点占便宜,但那些点可能定位很差,回归学出来就是歪的。
第二步,对每个 GT,找出中心点落在它内部的候选预测点(这叫中心先验筛选,能砍掉大量无关候选)。第三步,在这些候选里按 align_metric 排序,取topk=10个作为正样本。第四步,算这些正样本的软标签,用加权后的 IoU 作为回归目标的置信度权重。
我实测下来,topk 这个值调到 13 左右在某些密集小目标场景上会有一点提升,但稳定性下降,容易在目标密集区域出现重复检测。要动这个参数,建议先跑通 baseline,再单独做消融,别一上来就改。
提示:
TaskAlignedAssigner的 alpha、beta、topk 在 ultralytics 里可以通过自定义 loss 或者改源码调整。但改之前想清楚——你是在解决"漏检多"还是"误检多"?漏检多可以把 topk 提一点,误检多应该优先查数据集标注质量,那才是根因。
2.3 损失函数三件套:BCE + CIoU + DFL 怎么配合
YOLOv8 的总损失是三项加权求和:
total_loss = box_gain × box_loss + cls_gain × cls_loss + dfl_gain × dfl_loss默认权重是box=7.5、cls=0.5、dfl=1.5。这三个数不是拍脑袋来的,box 权重最高说明定位是主要矛盾,cls 权重低是因为分类相对好学。
分类损失用的是 BCE(二值交叉熵),每个类别独立判断。注意这里不是 softmax,而是对每个类做 sigmoid 后算 BCE。这样做是为了兼容"一个目标属于多个类别"的情形,在某些重叠类别场景(比如"人"和"骑手")下更有优势。
边界损失用的是 CIoU。相比普通 IoU,CIoU 额外考虑了中心点距离和长宽比一致性。公式大致是:
CIoU = IoU - ρ²(b, b_gt)/c² - αv其中ρ²是两个框中心的欧氏距离平方,c是最小外接矩形的对角线长度,v衡量长宽比的一致性。我理解它的方式很直白:光看重叠面积不够,还得看中心对不对得上、形状像不像。对于细长目标(比如电线杆、车道线)这种长宽比悬殊的物体,CIoU 比普通 IoU 收敛快很多。
DFL 损失是 YOLOv8 比较有特色的地方。传统回归直接输出一个数,DFL 把"到某条边的距离"离散成reg_max=16个区间,网络输出 16 个 logits,softmax 之后求期望:
距离 = Σ (i × softmax(logit_i))为什么要这么绕?因为直接回归一个连续值,对边界模糊、遮挡的目标容易学出一个"平均值",结果框不大不小、定位不准。改成分布之后,网络可以表达"我倾向于认为距离在 12 到 14 之间"这种不确定性,训练更稳定,对遮挡目标尤其友好。代价是回归分支的通道数变成4 × reg_max = 64,比直接 4 通道重不少,这也是为什么 YOLOv8 的 head 部分计算量不小。
三项损失的平衡我很在意。有次做泥石流、滑坡这类自然灾种检测,目标形状极不规则、边界模糊,我把 box 权重从 7.5 提到 9.0,同时把 dfl 从 1.5 降到 1.2,mAP50-95 提了不到一个点,但训练稳定性明显变好,波动小了。这种微调没有标准答案,要结合数据特点试。
2.4 输出解码:从 8400 个预测到最终结果
训练完或者加载权重做推理时,输出的原始张量是[B, 4+nc, 8400],必须经过解码才能变成人能看懂的框。解码大概是这么几步。
第一步,拆分。把通道维切成[B, 4, 8400]的框偏移和[B, nc, 8400]的类别分数。
第二步,反 sigmoid(如果需要)。类别分数本身经过 sigmoid,直接用就行,但有些导出格式(比如 ONNX)会把 sigmoid 融合进去,要确认一下。
第三步,找候选。对每个特征点,取类别分数最大值,如果超过conf_thres(默认 0.25)就进入候选。
第四步,解码框坐标。将预测点(特征图上的整数坐标)加上预测的偏移量,得到框的中心(x, y);再用偏移量的(l, t, r, b)算出左上右下。这一步要注意缩放:三个尺度的 stride 分别是 8、16、32,解码后要乘回去。
第五步,NMS。候选框之间做非极大值抑制,iou_thres默认 0.7,去掉重复框。
第六步,把框从 640×640 的输入尺度映射回原图尺度,同时做边界裁剪,防止超出图像范围。
注意:导出 ONNX 时,YOLOv8 默认会把一些后处理(解码 + NMS)也打包进去,形成"端到端"模型。好处是部署端省事,坏处是灵活性差,改个阈值都得重新导出。我一般导出两版:一版带后处理给嵌入式用,一版纯输出给需要自己写后处理的场景。
3. 实例分割分支:32 个系数 + proto 原型的组合游戏
3.1 Proto 模块与 mask 系数是怎么配合的
实例分割任务的输出多了一个 32 通道的 proto 张量,形状是[B, 32, 160, 160]。160 = 640/4,也就是说 proto 的分辨率是输入的 1/4。为什么不直接输出原图分辨率的掩码?因为那样计算量和显存占用都无法接受——如果每个实例都要输出一张 640×640 的掩码,几十个实例乘起来显存直接爆掉。
所以 YOLOv8 采用了原型 + 系数的分解思路,这是受 Mask R-CNN 之后的很多工作启发。proto 是"素材库",包含 32 个通道的通用掩码特征,这些特征对不同目标共享;每个检测实例只需要额外预测 32 个系数,用这 32 个系数对 proto 的 32 个通道做加权求和,就得到属于它的掩码。
具体计算流程是这样:
- 取某个实例的 32 个 mask 系数,形状
[32]。 - 与 proto 做矩阵乘法:
[32] × [32, 160×160] → [160×160]。 - 对结果做 sigmoid,得到 0 到 1 之间的掩码概率图。
- 用该实例的检测框裁剪掩码,框外的区域全部置 0。
- 上采样回原图分辨率,得到最终的二值掩码。
我第一次看这个设计的时候觉得挺妙:把"共性"和"个性"分开了。proto 学的是"这一片区域像不像目标轮廓"这种通用知识,系数学的是"这个具体实例对应哪些通道"。好处是共享计算、参数利用率高,坏处是掩码精度上限受 proto 分辨率限制。因为 proto 只有 160×160,对于原图里很小的目标,掩码边缘会比较糙。
提示:如果你做的是精细边缘分割(比如工业零件缺陷边界),160×160 的 proto 可能不够。我看到过的改进思路包括把 proto 分辨率提到 1/2、加边缘感知分支、或者把 proto 换成可变形卷积,但这些都涉及改 head 结构,改动量不小,而且要重新训练。
3.2 分割数据集的组织方式与标注要点
分割的数据集格式和检测差别很大。检测的标签是class cx cy w h(都是归一化到 0 到 1 的值,即class x_center y_center width height),分割则需要多边形点集:
class x1 y1 x2 y2 x3 y3 ... xn yn每一行是一个实例,后面跟着一串归一化的多边形顶点坐标,按顺序连起来构成轮廓。注意这里必须是闭合多边形,而且顶点的顺序要沿着边界顺序走,不能乱序。
实际标注中我总结了几条经验,都是踩过坑才明白的。
第一,顶点数量要够但不能太密。太少轮廓会失真(比如圆形标注成三角形),太多会让文件体积暴涨、训练变慢。一般 30 到 100 个顶点能覆盖大多数形状。如果是特别复杂的分叉轮廓,可以用多个多边形描述同一个实例,但要注意不同工具的支持情况。
第二,标注必须自交要避免。如果多边形自己缠绕,掩码生成的时候会出问题,可能出现部分区域被填充、部分区域空白的情况。标注时尽量顺着边界描,别来回穿插。
第三,孔洞是老大难。YOLO 格式原生不支持带孔的多边形(比如甜甜圈),一般的处理方式是拆分或者简化,把中间的孔当作另一个类别标注。这个局限要有心理准备。
第四,类别平衡对小目标分割尤其重要。我做过一个场景,画面里主体目标很大、远处小目标很小,结果模型几乎完全忽视小目标。解决方式是 Oversample 小目标样本,或者提高输入分辨率。
数据格式转换这块,一般是从标注工具(labelme、CVAT、Roboflow 等)导出的 json/xml 转成 YOLO 的 txt。转换脚本的核心就是把多边形坐标归一化、按类别分组。转换完一定要可视化验证——我见过太多"跑通了但标注全错位"的情况,原因是归一化用错了尺寸(用了缩放后的尺寸而不是原图尺寸)。
3.3 分割任务的损失与评估指标
分割任务的损失是在检测三项之外再加一项掩码损失。掩码损失用的是 BCE:把预测掩码和 GT 掩码逐像素比,算二值交叉熵。注意这里只对正样本(也就是被分配为某个实例的预测)计算掩码损失,背景不参与。
权重方面,默认是box=7.5, cls=0.5, dfl=1.5,掩码损失的权重通常是 1 左右,具体看版本。我实测过把掩码权重提到 2.0,边缘细节会好一些,但训练更容易过拟合小数据集。
评估指标上,检测用的是 mAP50 / mAP50-95,分割额外有mask mAP50。这两个要分开看。我遇到过检测 mAP 挺高、mask mAP 很低的情况,原因是框的位置大致对,但轮廓完全不对——通常是标注质量差导致的。这时候应该回去检查标注多边形,而不是调模型。
还有个容易忽略的点:mask mAP 的计算会用到检测框。也就是说,如果检测框不准,即使掩码本身没问题,mask mAP 也会被拖累。所以分割任务的调优要先把检测分支调好,再去看掩码。
4. 关节点估计分支:单类人体姿态的轻量实现
4.1 关键点表示方法与可见性建模
关节点估计在 YOLOv8 里的实现比很多人想象的简单——它本质上是在检测的基础上,让每个正样本额外回归一组关键点坐标。所以它的输出是[B, 4+1+3k, 8400],其中4+1是人体框和"是人"的置信度,3k是关键点。
为什么是3k而不是2k?这是姿态估计里的一个经典设计。每个关键点输出(x, y, v)三个值:x、y是归一化到 0 到 1 的坐标(相对于检测框,需要再映射回原图),v是可见性置信度。可见性这一维区分了三种情况:
- 关键点在画面内且清晰可见(v 接近 1)
- 关键点在画面内但被遮挡,位置靠推断(v 取中间值)
- 关键点完全不在画面里(v 接近 0,坐标无意义)
这个设计比只输出(x, y)强太多了。如果只有两维,遇到"人被门挡住一半"这种情况,网络会强行给一个坐标,导致骨架结构扭曲;有了可见性维度,网络可以表达"我不知道这个点在哪",损失函数也能选择性地忽略这些点。
坐标的归一化方式需要注意。YOLOv8 的关键点坐标通常是相对于该实例的检测框归一化的,即值域 0 到 1,表示在框内的相对位置。解码时要先乘以框的宽高,再加上框的左上角坐标,才是原图上的绝对坐标。我见过有人在可视化的时候直接用 0 到 1 的值画点,结果所有骨架都缩在左上角一小块区域——就是漏了这一步映射。
4.2 关键点损失:可见性 BCE 加坐标回归
关键点的损失由两部分组成,分别对应"可见性判断"和"坐标回归"。
可见性是分类问题,用 BCE。对每个关键点,GT 的可见性标签是 0 或 1(或者 0、0.5、1 三档),预测值经过 sigmoid 后算 BCE。
坐标回归是回归问题,YOLOv8 的实现思路和检测框回归类似,也是基于 CIoU 或者类似的几何度量,但作用对象变成了关键点组成的骨架结构。具体来说,它会把预测的关键点和 GT 关键点分别看成一组点,计算它们之间的相似度,相似度越高损失越小。
这里有个细节值得说:不可见的关键点不参与坐标损失。这符合直觉——你不可能要求网络去回归一个画面里根本不存在的位置。所以数据集里那些v=0的点,只贡献可见性损失,不贡献坐标损失。
我自己做过鸟类关键点的项目(标注喙、双翅、双脚、尾羽这些点),体会是:关键点的定义一致性比数量更重要。如果同一个姿态在不同样本里关键点的语义不一致(比如有的标在关节、有的标在骨骼中点),网络学出来的骨架会飘。标注前一定要写清楚每个点的定义,并且让所有标注员对齐标准。这个坑我栽过一次,重标了两千多张图。
注意:YOLOv8 官方 pose 实现是单人、单类。如果你要做的场景里有多个人,它其实是用检测框分别框出每个人,然后每个人单独估骨架。但如果两人重叠严重,检测框本身就会相互干扰,这时候可能需要在检测阶段就做得更细,或者考虑专门的多人姿态方案。
4.3 三任务的横向对比:复杂度与适用场景
把三个任务放一起看,能更清楚它们的成本和边界。下面这张表是我自己整理的,性能数据以 YOLOv8n 在 640 输入下的常见量级为参考,具体数值随版本和硬件会有浮动:
| 维度 | 目标检测 | 实例分割 | 关节点估计 |
|---|---|---|---|
| 输出通道(COCO) | 4+80=84 | 4+80+32=116 | 4+1+51=56 |
| 额外输出 | 无 | proto [32,160,160] | 无 |
| 计算量(相对) | 基准 | 约 1.3 倍 | 约 1.1 倍 |
| 显存占用(相对) | 基准 | 约 1.5 倍 | 约 1.2 倍 |
| 标注成本 | 中 | 高(多边形) | 高(逐点) |
| 典型场景 | 车流统计、质检 | 缺陷轮廓、遥感 | 动作识别、体育分析 |
从工程角度选型,我的建议是:能用检测解决的就别上分割。比如统计路口车流量,你只需要知道"有几辆车、在哪",检测就够了。分割的边际价值只在"需要精确轮廓"时体现,比如计算缺陷面积、区分粘连目标。关节点估计的适用面更窄,基本都是人体动作、运动分析这类场景。
还有个现实问题:标注成本。检测框标一张图可能几秒钟,多边形要几十秒,关键点更慢。如果你的项目预算有限,一定要在选型阶段想清楚,别一开始选了分割,标了两千张才发现其实检测够用。
5. 环境配置与训练实操:从零跑通自己的数据集
5.1 环境搭建与版本组合怎么选
环境配置这块,我踩过的坑比训练本身还多。核心原则只有一条:不要盲目装最新版。深度学习生态的版本兼容性很差,一个版本不匹配就是一堆莫名其妙的报错。
我的推荐做法是用 Conda 建独立环境,然后按这个顺序来:
conda create -n yolo python=3.10 -y conda activate yoloPython 选 3.10 是比较稳的,兼容性好,各种依赖都有轮子。
然后是 PyTorch。这一步必须去官网的安装页面选对应的 CUDA 版本,不要直接pip install torch,那样装到的是 CPU 版本,跑起来慢到怀疑人生。选择逻辑是:先看你的显卡驱动支持哪个 CUDA 版本,然后选对应的 PyTorch。比如显卡是 GTX 1660 Ti 这类较老的卡,CUDA 11.8 的组合兼容性最好;如果是较新的卡,可以考虑 CUDA 12.x 的组合。
# 以 CUDA 11.8 为例,具体命令以官网生成器为准 pip install torch torchvision --index-url <对应源>装完之后一定要验证:
import torch print(torch.__version__) print(torch.cuda.is_available()) # 必须是 True print(torch.cuda.get_device_name(0))cuda.is_available()返回 False 是最常见的问题。原因通常有三个:装的是 CPU 版 PyTorch、CUDA 版本和驱动不匹配、或者环境变量没配好。排查顺序是先重新装对版本的 PyTorch,再检查驱动版本(nvidia-smi能看到),最后看环境变量。
之后装 ultralytics:
pip install ultralytics如果是在 PyCharm 里做,记得把解释器切到刚才建的 conda 环境,别用系统默认的 Python,否则又是"我明明装了却 import 不到"。
提示:如果你用的是较老的显卡,int8 量化推理可能不被支持,或者性能提升不明显。这种卡上老老实实用 FP16 或者 FP32 推理就行,别折腾量化。
5.2 数据集组织与训练参数配置
YOLO 的数据集目录结构是固定的:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamlimages和labels下的文件名要一一对应,只是扩展名不同(图片是 .jpg/.png,标签是 .txt)。这一点很容易出错——我曾经因为一张图没有对应的 label 文件,训练时报了个很隐晦的错误,排查了半天。
data.yaml内容大致是:
path: /your/dataset/path train: images/train val: images/val nc: 3 names: ['cat', 'dog', 'bird']nc和names必须和标注里的 class id 严格对应。class id 从 0 开始。
训练命令的基本形态:
yolo task=detect mode=train model=yolov8n.pt data=data.yaml epochs=100 imgsz=640 batch=16如果是分割,把task=detect换成task=segment;姿态换成task=pose。模型可以选yolov8n/s/m/l/x五个尺寸,n 最小最快,x 最大最准。我的一般原则是:先跑 n 验证流程通不通,再换大模型冲精度。很多人一上来就上 l 或 x,结果显存爆了、训练一天没结果,浪费时间。
关键参数解释几个我觉得最重要的:
| 参数 | 默认值 | 作用与调法 |
|---|---|---|
epochs | 100 | 训练轮数。小数据集 50 到 100 够了,大数据集可以到 300 |
imgsz | 640 | 输入分辨率。小目标多可以提到 960 或 1280,但显存翻倍 |
batch | 16 | 批次大小。显存不够就减,配合accumulate补偿 |
lr0 | 0.01 | 初始学习率。小数据集可以降到 0.001 防止过拟合 |
lrf | 0.01 | 最终学习率系数,和 lr0 相乘得到末期学习率 |
freeze | None | 冻结前 N 层。迁移学习时冻结 backbone 能防过拟合 |
close_mosaic | 10 | 最后 N 轮关闭 mosaic 增强,让模型适应真实分布 |
freeze这个参数我用得比较多。如果你自己的数据集不大(几千张以内),直接全量微调很容易过拟合,表现就是训练 loss 一直降、验证 loss 先降后升。这时候冻结前 10 层(大概就是整个 backbone),只训练 neck 和 head,效果通常更稳。
freeze的写法:
yolo task=detect mode=train model=yolov8s.pt data=data.yaml freeze=10 epochs=100注意freeze是按层索引冻结的,具体冻多少层要看你选的是哪个尺寸的模型。n 和 s 的层数少,冻 10 层可能已经冻掉大半个网络了。
5.3 训练过程监控与损失曲线解读
训练启动后会生成runs/detect/train/目录,里面有results.csv、权重文件、以及各种曲线图。怎么看这些曲线,是判断训练是否健康的关键。
loss 曲线:box_loss、cls_loss、dfl_loss三条。正常情况下三条都应该单调下降然后趋于平缓。如果出现以下情况要警惕:
cls_loss一直不降:可能是类别标注有错,或者类别极不平衡box_loss降得很慢:可能是学习率太大,或者正样本分配有问题- 训练 loss 降但验证 loss 升:典型的过拟合,加正则或者减模型容量
mAP 曲线:metrics/mAP50和metrics/mAP50-95。前者宽松后者严格。一般 mAP50 会到 0.9 以上,mAP50-95 能有 0.6 到 0.7 就算不错。如果 mAP50 高但 mAP50-95 很低,说明框的大致位置对但精度不够,可能是 DFL 没学好或者数据标注框不够紧。
学习率曲线:可以看到 warmup 和余弦退火的过程。warmup 阶段 lr 从很小的值线性升到 lr0,然后稳定,最后余弦退火到lr0 × lrf。如果这条曲线异常(比如一直平的),检查一下配置。
画曲线的话,results.csv可以直接用 pandas 读出来画:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/detect/train/results.csv') df.columns = df.columns.str.strip() plt.plot(df['epoch'], df['train/box_loss'], label='box_loss') plt.plot(df['epoch'], df['val/box_loss'], label='val_box_loss') plt.legend() plt.show()注意results.csv的列名可能带前导空格,读进来后要处理一下,否则KeyError。这个小坑我中过。
提示:如果训练中途中断了想接着训,用
resume=True配合last.pt。但注意,如果你的代码或数据改了,resume 出来的结果可能不一致,最好还是重新训。我一般只在显存不足导致中断时才 resume。
6. 常见问题排查与部署踩坑实录
6.1 训练阶段的典型故障与速查
训练阶段的问题,八成集中在数据和环境上,真正是模型本身的问题反而少。我把遇到过的整理成一张表,方便你对照排查。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 训练一开始 loss 就是 nan | 学习率过大、数据里有非法值 | 降 lr0 到 0.001;检查标注是否越界或为空 |
| mAP 一直是 0 | 类别 id 不匹配、标签路径错 | 检查 data.yaml 的 nc/names;可视化一批标注 |
| 显存溢出 | batch 太大、imgsz 太大 | 降 batch 或用 batch 配合 accumulate |
| 只检测到部分类别 | 类别不平衡 | 补充少数类样本,或用 mosaic、copy-paste 增强 |
| 验证集表现远差于训练集 | 过拟合 | 加 freeze、加数据增强、减模型尺寸、加早停 |
| 小目标基本检不到 | 输入分辨率不够 | imgsz 提到 960/1280,或调小 anchor 相关设置 |
这里我想单独说小目标检测,因为这是提问频率最高的方向之一。小目标难检的根本原因是:在 640 输入下,一个 10×10 像素的目标经过 32 倍下采样后,在最小特征图上只剩不到一个像素,信息基本丢失了。解决办法有几个方向:
第一,提高输入分辨率。这是最直接有效的,640 提到 1280,小目标在特征图上的尺寸翻倍。代价是显存和计算量增加大约 4 倍。
第二,用更深的高分辨率分支。比如在 backbone 早期层(1/4 分辨率)加一个额外的检测头,专门负责小目标。这就是所谓的 "P2 头",代价是计算量增加不少。
第三,改进颈部融合。ASFF(Adaptive Spatial Feature Fusion)这类思路是让不同尺度的特征自适应加权融合,对小目标有一定帮助。我看到过把 ASFF 加进 YOLOv8 neck 的方案,在遥感数据集上有效,但要注意权重初始化和通道对齐的细节,加不好反而掉点。
第四,切图推理。大图切成小块分别检测,最后合并。这个方法简单粗暴但有效,缺点是推理时间线性增长,而且跨图目标会被切断。
6.2 导出与部署:ONNX、TensorRT 与边缘板子
训练完只是第一步,真正落地要做导出和部署。
ONNX 导出是最基础的一步:
yolo export model=best.pt format=onnx opset=12 imgsz=640opset版本建议 12 或 13,太低有些算子不支持,太高有些部署框架还没跟上。导出后用onnxruntime跑一遍验证,对比 PyTorch 的输出,确保数值一致。
TensorRT部署是性能瓶颈时的选择。核心流程是 ONNX 转 engine,然后 FP16 或 INT8 推理。FP16 一般能带来 1.5 到 2 倍加速,INT8 更多但要校准数据集,而且精度可能掉。
trtexec --onnx=best.onnx --saveEngine=best.engine --fp16这里有个大坑:输入输出的后处理要不要包含在模型里。YOLOv8 默认导出会带上解码和 NMS,好处是部署端简单;如果你想在 C++ 侧自己写后处理,导出时要加nms=False,拿到原始张量。两种方式我都用过,一般来说嵌入式场景带后处理更省事,服务器场景自己写更灵活。
边缘板子部署(比如带 NPU 的 RK3588 这类)走的是另一条路径,通常是 ONNX 转成板子专用的模型格式,再调用 NPU 推理。这条路的难点在算子支持——YOLOv8 里的 DFL、某些激活函数、还有后处理中的 topk,可能不被 NPU 直接支持,需要替换或者放到 CPU 上跑。实操体会是:先跑通官方提供的示例模型,再换自己的权重。很多人一上来就拿自己训练的模型去转,结果各种算子报错,根本分不清是模型问题还是流程问题。
量化方面,如果板子支持 int8 并且你追求速度,需要准备一批有代表性的校准图(几百张就够),覆盖各种光照和场景。校准集选取不好,量化后的精度会掉得很难看。
6.3 改进方向的取舍与经验总结
最后聊聊改进。现在网上关于 YOLOv8 改进的内容非常多——换注意力、换卷积、加 ASFF、加各种模块,看起来眼花缭乱。我的经验是:改进之前先确认 baseline 有没有做到位。
很多人的"改进"其实是补了基础工作的漏洞。比如你的 mAP 低,可能是因为:数据集标注质量差、类别不平衡没处理、学习率没调好、输入分辨率不匹配目标尺寸。这些问题不解决,加什么模块都是白费。我见过有人加了三个注意力模块,mAP 涨了 0.3,结果一检查发现验证集里有一半图片标注框偏移,修完标注直接涨了 8 个点。
真正值得做的改进,我按性价比排个序。
第一档是数据层面的改进:增加数据、修正标注、做针对性的增强(小目标用 copy-paste、遮挡用 mosaic)。投入产出比最高。
第二档是训练策略层面:调学习率策略、调损失权重、用 EMA(指数移动平均)、用更好的初始化。这些不改结构,风险低。
第三档是结构层面:换骨干、加模块、改颈部融合。这里要注意,改结构一定要做严格的消融实验,同数据、同参数、同随机种子,只改一个变量。不然你根本不知道涨点是来自改进还是来自随机波动。
第四档是后处理层面:改 NMS 策略、多尺度测试、TTA(测试时增强)。推理成本会增加,但精度稳定提升。
关于轻量化改进,如果你的目标是"macs 仅 5MB 级别"这种极小模型,思路无非是深度可分离卷积、通道剪枝、知识蒸馏。这些技术本身成熟,但要在 YOLOv8 上落地,得改源码、重训,工作量不小。如果只是想在小设备上跑,我建议直接用 YOLOv8n 或者找一个 n 级别以下的官方小模型,比自己去剪枝划算。
还有个方向是开放词汇检测和多模态微调,这类工作需要额外的文本或图像编码器,工程复杂度高,一般项目用不上,除非你的场景确实需要"零样本识别新类别"。
我在实际使用中最大的一个体会是:YOLOv8 的价值不只在精度,更在于它的工程完整性。从数据组织、训练、验证、导出到部署,整条链路都有现成的工具支持,而且三个任务的接口高度统一。这意味着你可以把精力放在数据质量和场景适配上,而不是重复造轮子。真要说有什么遗憾,大概是文档对内部实现细节的披露比较有限,很多机制(比如正样本分配的具体实现、关键点损失的确切形式)得去翻源码才能确认。但换个角度想,这也是个机会——能把源码读明白的人,改起结构来会顺手很多。
最后分享一个小技巧:调试阶段我会开一个小脚本,把模型的中间层输出 hook 出来,逐层看特征图的形状、数值范围和分布。这样改结构的时候,哪一层通道对不上、哪一步归一化出问题,一眼就能看出来。这个习惯帮我省了大量时间,比对着报错瞎猜强得多。