YOLO11输出层解析,小白也能看懂的说明
你是不是也遇到过这样的困惑:
训练完一个YOLO11模型,用Netron打开ONNX或RKNN文件,看到输出节点标着“9个张量”,但完全不知道每个数字代表什么?
推理时拿到一堆浮点数,却不敢动、不敢改后处理代码,生怕一调就崩?
甚至在部署到RK3588板子上后,框画歪了、类别错了、置信度忽高忽低——问题到底出在哪儿?
别急。这篇文章不讲晦涩的数学推导,不堆砌论文公式,也不复制粘贴源码注释。
我们只做一件事:把YOLO11的输出层,掰开、揉碎、用生活里的例子讲清楚——哪怕你刚学Python三个月,也能看懂它到底在“说”什么。
1. 先搞清一个前提:YOLO11不是全新架构,而是YOLOv8的“升级版”
很多新手一看到“YOLO11”就以为是像YOLOv5→YOLOv8那样彻底重构的版本,其实不然。
YOLO11(官方命名仍为ultralytics==8.3.x系列)本质上是YOLOv8主干的能力增强版:它保留了YOLOv8的整体结构(Backbone + Neck + Head),但替换了关键模块——比如用C3k2替代C2f,用C2PSA替代部分SPPF,提升了小目标和密集场景的检测能力。
这意味着:
输入尺寸、预处理方式、Anchor设计逻辑,和YOLOv8完全一致
输出格式、解码规则、后处理流程,也和YOLOv8高度兼容
❌ 它没有像YOLOv10那样取消NMS,也没有像YOLO-NAS那样重写Head结构
所以,如果你已经会看YOLOv8的输出,那YOLO11的输出层,你几乎不用重新学——只需要理解它“多出来的那一点变化”。
2. YOLO11的输出到底长什么样?一张图说清本质
打开你训练好的yolo11_yaml_silu_best.onnx,用Netron查看输出节点,你会看到类似这样的结构:
output0: [1, 84, 80, 80] ← P3层(最小尺度,检测小目标) output1: [1, 84, 40, 40] ← P4层(中等尺度,检测中等目标) output2: [1, 84, 20, 20] ← P5层(最大尺度,检测大目标)注意:这里的84不是笔误,也不是乱码——它就是YOLO11输出层最核心的“密码”。
2.1 为什么是84?它拆开是啥?
我们来把它“剥洋葱”:
- YOLO11默认使用3个检测头(P3/P4/P5),每个头负责不同尺度的目标
- 每个检测头对每个网格(grid cell)预测3个anchor box(这是YOLOv8/YOLO11沿用的经典设定)
- 每个anchor box要输出4个坐标值(x, y, w, h)+ 1个置信度(objectness)+ C个类别概率
- 其中C是你数据集的类别数(比如垃圾检测是2类:纸巾、纸团 → C=2)
- 所以单个box输出维度 = 4 + 1 + C = 5 + C
- 因此,总通道数 = 3 × (5 + C)
代入你的实际训练配置:
如果你的yolo11.yaml里写的是nc: 2(2个类别),那么:
→ 单box输出 = 5 + 2 = 7
→ 每层3个anchor → 3 × 7 =21
→ 但Netron显示是84?等等——21 × 4 = 84?不对,没乘4。
真相是:YOLO11默认启用“解耦头”(Decoupled Head)结构,把定位(reg)、置信度(obj)、分类(cls)三部分分开输出,但最终在ONNX导出时被合并为一个张量。而84 = 3 × (4 + 1 + 2) × 1?还是不对。
再查官方源码与导出逻辑——原来:
YOLO11在ultralytics/nn/modules/head.py中,Head输出通道数 =num_anchors × (reg_dim + obj_dim + cls_dim)
其中:
reg_dim = 4(x,y,w,h)obj_dim = 1(objectness)cls_dim = nc(类别数)
所以标准计算就是:3 × (4 + 1 + nc)
那84怎么来的?
→ 解方程:3 × (5 + nc) = 84 → 5 + nc = 28 →nc = 23
这说明:你看到的84通道模型,极大概率是基于nc=23的通用配置导出的(比如COCO数据集有80类,但YOLO11官方release模型常用20+类精简版)。
而你自己的garbage.yaml里写的是nc: 2,那真实输出应为:3 × (4 + 1 + 2) =21通道。
关键结论来了:
你在Netron里看到的84,不代表你的模型真有84个输出通道;它只是导出时用了默认配置。真正决定输出维度的,是你训练时yaml文件里写的
nc值。
所以,请立刻打开你的garbage.yaml确认:
nc: 2 # ← 这一行,才是你模型输出的“宪法”只要这一行没错,你的实际输出就是21维,不是84维。
3. 输出张量怎么“读”?三步还原成真实框
假设你已确认nc=2,那么每个输出张量(如[1, 21, 80, 80])的真实含义如下:
3.1 第一步:把21拆成3组,每组7个数
| 索引范围 | 含义 | 举例说明 |
|---|---|---|
| 0~3 | 归一化坐标 | x, y, w, h(相对当前grid) |
| 4 | 是否有目标 | objectness(0~1之间) |
| 5~6 | 类别概率 | class0_prob, class1_prob |
小白提示:这里没有“softmax”!YOLO系列输出的类别概率是logits(未归一化的原始分),后处理时需用
sigmoid激活objectness,用softmax或squeeze + argmax取最高类。
3.2 第二步:把坐标从“网格相对”转成“图像绝对”
YOLO的坐标永远是“相对于当前特征图网格”的。比如P3层是80×80,意味着整张图被划成80行×80列的格子。
- 每个grid cell对应原图的区域大小 =
input_size / 80(若输入是640×640,则每个cell宽高=8像素) - 假设某输出在
(i=10, j=25)位置,其x=0.6, y=0.3, w=0.8, h=0.5 - 那它的真实图像坐标就是:
center_x = (j + x) × stride = (25 + 0.6) × 8 = 204.8center_y = (i + y) × stride = (10 + 0.3) × 8 = 82.4width = w × stride × anchor_w_ratio(anchor宽高比来自yaml)height = h × stride × anchor_h_ratio
小白提示:“stride”就是下采样倍数:P3层stride=8,P4层stride=16,P5层stride=32。这个数在YOLOv8/YOLO11中是固定的,不用猜。
3.3 第三步:合并3个尺度,过滤+去重,得到最终结果
这才是最常出错的环节:
- 把P3/P4/P5三层的所有预测框(共
80×80 + 40×40 + 20×20 = 8400个)全拉出来 - 对每个框:
- 计算最终置信度 =
objectness × max(class_prob)(比如objectness=0.9,class0_prob=0.8 → 置信度=0.72) - 设定阈值(如0.25),筛掉低分框
- 计算最终置信度 =
- 对剩余框做NMS(非极大值抑制):IOU > 0.45的重复框,只留分数最高的那个
小白提示:YOLO11没有取消NMS!它和YOLOv8一样,必须靠NMS去重。网上说“YOLO11去NMS”是误传——那是YOLOv10。
4. 为什么你在RK3588上看到的输出还是9个?它和21/84是什么关系?
你在博文里看到这句话:
“可以看到,YOLO11的输出还是和YOLOv8一样的,都是9个”
这其实是RKNN工具链的自动合并行为,不是模型本身的设计。
- ONNX模型导出时,YOLO11默认将3个检测头的输出分别作为3个独立输出节点(output0/output1/output2)
- 但RKNN Toolkit在转换时,为兼容旧有部署框架(尤其是早期RKNN SDK对多输出支持弱),会把这3个张量按通道拼接(concat)成一个大张量
- 拼接顺序是:
[P3, P4, P5]→ 总通道数 = 21 + 21 + 21 =63?但你说是9?
再深挖:
RKNN转换脚本(convert.py)中有一行关键配置:
outputs = ['output0', 'output1', 'output2'] # ← 原始3个输出 ... # RKNN Toolkit v2.3 默认将多输出合并为单输出,并重排为 [B, C, H, W] 格式 # 但某些版本会进一步“展平”为 [B, 9, ...] —— 这里的9,其实是“3个anchor × 3个坐标分量”的历史遗留标记真相是:“9”不是通道数,而是RKNN调试器对YOLO类模型的简化显示惯例。
它把每个anchor的(x,y,w,h,objectness,class0,classclass1)强行映射为3组“伪坐标”,凑成9个数,方便老工程师快速识别——但这完全不反映真实数据结构。
正确做法:
- 在RKNN推理代码中,不要依赖输出张量的shape显示为9
- 而是用
rknn.eval_perf()或rknn.get_inputs()明确获取各输出节点名和shape - 或直接打印
outputs[0].shape,outputs[1].shape,outputs[2].shape,你会看到真实的[1,21,80,80]等
血泪教训:曾有开发者因迷信“9个输出”,硬把21维数据截断成前9位,结果框全飘走、类别全错——根源就在这里。
5. 动手验证:用几行Python,亲眼看到输出在说什么
别光听我说,咱们现场跑通一小段代码,亲眼看看YOLO11输出的原始数据长啥样。
前提:你已成功运行过
python train.py,生成了runs/train/exp/weights/best.pt
环境:镜像中已预装ultralytics==8.3.9和torch
5.1 加载模型,获取原始输出
# test_output.py from ultralytics import YOLO import torch # 加载你自己的best.pt(不是yolo11n.pt!) model = YOLO("runs/train/exp/weights/best.pt") # 构造一个假输入(1张640x640的纯灰图) dummy_img = torch.ones(1, 3, 640, 640) * 128 # [B,C,H,W] # 关闭训练模式,只做前向推理 model.model.eval() with torch.no_grad(): outputs = model.model(dummy_img) # ← 这就是原始输出! print("YOLO11原始输出数量:", len(outputs)) # 应为3 for i, out in enumerate(outputs): print(f"output{i} shape: {out.shape}")运行后你会看到:
YOLO11原始输出数量: 3 output0 shape: torch.Size([1, 21, 80, 80]) output1 shape: torch.Size([1, 21, 40, 40]) output2 shape: torch.Size([1, 21, 20, 20])完美匹配我们前面的分析:nc=2 → 3×(4+1+2)=21
5.2 抽一个网格,看看它在“说”什么
继续加几行:
# 取P3层(output0)第0张图、第0个anchor、第10行第15列的预测 p3 = outputs[0][0] # [21, 80, 80] pred = p3[:, 10, 15] # [21] print("该位置21维输出:") print(f" x,y,w,h = {pred[0]:.3f}, {pred[1]:.3f}, {pred[2]:.3f}, {pred[3]:.3f}") print(f" objectness = {pred[4]:.3f}") print(f" class0_prob = {pred[5]:.3f}, class1_prob = {pred[6]:.3f}")输出类似:
该位置21维输出: x,y,w,h = 0.421, 0.617, 0.332, 0.289 objectness = 0.002 class0_prob = -1.824, class1_prob = -2.105注意:class0_prob是负数?因为它是logits,还没过sigmoid/softmax。
真实类别概率 =torch.softmax(pred[5:7], dim=0)→ 你会看到两个接近0的小数,加起来为1。
6. 部署时最容易踩的3个坑,现在就避开
结合你提供的RK3588部署全流程,我帮你把高频雷区标出来:
❌ 坑1:修改OBJ_CLASS_NUM但忘了同步改postprocess.h里的类别名数组长度
- 表现:程序编译通过,但运行时报segmentation fault
- 原因:C++后处理代码里用
char* names[] = {"paper", "tissue"},如果OBJ_CLASS_NUM=2但数组只写了1个,越界访问 - 正解:
OBJ_CLASS_NUM必须严格等于names数组元素个数,且names[i]顺序必须和yaml中names顺序完全一致
❌ 坑2:ONNX转RKNN时没指定input_size,导致输出shape错乱
- 表现:RKNN模型输出shape变成
[1, 21, 64, 64]等奇怪尺寸 - 原因:
exporter.py默认用640×640,但如果你训练时用的是其他尺寸(如416),必须显式传参 - 正解:导出ONNX时加参数
python ./ultralytics/engine/exporter.py --imgsz 416❌ 坑3:RKNN推理后没做objectness × class_prob,直接拿logits当置信度
- 表现:所有框置信度都在-3~+2之间,画出来全是“幽灵框”
- 原因:logits不能直接当置信度用
- 正解:在C++后处理中加入
float obj = sigmoid(output[4]); // objectness float cls0 = exp(output[5]) / (exp(output[5]) + exp(output[6])); // softmax float conf = obj * cls0; // 最终置信度7. 总结:YOLO11输出层,就记住这三句话
7.1 它的“形”由你定,不由名字定
YOLO11不是魔法,它的输出通道数 =
3 × (4 + 1 + nc)。nc写几,输出就是几。别被Netron里显示的84吓住。
7.2 它的“意”要分层读,不能一刀切
P3/P4/P5三层输出,各自独立。每层内每个网格输出7个数:4坐标+1置信+2类别。合并、解码、NMS,缺一不可。
7.3 它的“用”在端侧,要亲手验
RK3588上看到的“9个输出”是工具链的简化显示。真正在用的,是你
postprocess.cc里逐字节读取的21维张量。动手跑一次test_output.py,比读十篇博客都管用。
你现在可以打开自己的garbage.yaml,确认nc值;
可以进镜像终端,跑一遍那5行测试代码;
可以打开postprocess.h,核对OBJ_CLASS_NUM是否和names数组长度一致。
技术从不神秘,它只是需要有人,用你能听懂的话,带你走通第一遍。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。