08 YOLOv8检测头对INT8敏感的深度拆解——从直觉到数学证明

YOLOv8检测头对INT8敏感的深度拆解------从直觉到数学证明

一句话总结 :检测头对INT8敏感的真实原因不是方差小 ,而是长尾分布 (90%背景小值+10%目标大值)。σ=0.05的纯高斯分布下INT8也能保持0.9999的余弦相似度,但长尾分布下小值区域的相对误差高达4.1%(Backbone仅1.05%)。量化不怕整体缩小,怕的是有的地方缩得多、有的地方缩得少。


目录


开篇:从一次代码实验的认知颠覆开始

这个笔记的起源是一篇网上的文章,标题是《为什么检测头对INT8量化特别敏感?代码实验验证》。原文的结论是:方差越小→余弦相似度越低→对INT8越敏感。然后用不同 σ 跑了一遍,σ=0.05 时余弦相似度掉到 0.75,看起来证据确凿。

但跑了一下发现不对劲------同样 σ=0.05 的纯高斯分布,余弦相似度明明有 0.9999。为什么差这么多?

原来是随机种子问题。原实验用的随机数样本太少(1000个),σ=0.05 时偶然抽到了几个异常大的值,导致scale被拉大,看似"方差小就敏感"。但用足够多样本(比如10000个)重复跑,纯高斯分布下不同 σ 的表现几乎完全一样。

那检测头敏感到底是因为什么?

带着这个问题深入查了一下,发现真相是长尾分布------检测头的输出 ≈ 90% 背景小值 + 10% 目标大值。这种分布不是高斯分布,它的 scale 被 10% 的大值决定,让 90% 的小值失去了精度。


第一部分:前置知识------看懂量化代码需要知道的几个概念

1.1 σ (sigma) 是什么?

代码里 torch.randn(N) * 5.0 中的 5.0 就是 σ(标准差)

python 复制代码
torch.randn(N)       # 生成N个标准正态分布随机数, 默认 σ=1
torch.randn(N) * 5.0 # σ=5.0,数值散布范围宽
torch.randn(N) * 0.1 # σ=0.1,数值散布范围窄
σ 95%数据范围 直观比喻
σ=5.0 ±10 左右 撒了一把黄豆,满地都是
σ=3.0 ±6 左右 中等
σ=0.1 ±0.2 左右 撒了一把芝麻,全堆在一起
σ=0.05 ±0.1 左右 极度集中

1.2 scale 里的 127 从哪来?

量化公式:scale = max_abs / 127

记忆方法三个层次:

① 一句话死记:INT8 的正半轴还剩 127 个格子

复制代码
INT8 范围: [-128, -127, ..., -1, 0, 1, ..., 126, 127]
对称量化时 0 必须占中间 →
  负半轴: 128 个(-128 ~ -1)
  正半轴: 127 个(1 ~ 127)
最大正数 = 127 → 所以除以 127

② 通用公式:N位有符号量化 → scale = max_abs / (2^(N-1) - 1)

位数 公式 数值
INT8 2^(8-1)-1 127
INT4 2^(4-1)-1 7
INT16 2^(16-1)-1 32767

③ 8字口诀:最大绝对值,分给127格

128 去哪了? 正半轴有 127 个正数,负半轴有 128 个负数(包括 -128)。对称量化时把 -128 留给一个特殊值(通常表示饱和或 NaN),所以正负对称除以 127。

1.3 步长决定了什么?精度 vs 范围

步长 = scale,就是 INT8 尺子上一格的真实数值宽度。

步长决定两件事:

属性 公式 步长大的影响 步长小的影响
精度 能分辨的最小变化 粗,0.12 以内的变化分辨不出 细,能分辨 0.003 的变化
范围 scale × 127 范围大(±15) 范围小(±0.3)

精度和范围不可兼得:

复制代码
步长小 → 精度高 → 但范围小 (Head场景)
步长大 → 范围大 → 但精度低 (Backbone场景)

把 scale 看作尺子的刻度间距:

  • Backbone 的尺子 每格 0.122,量程 ±15.5 --- 量大楼
  • Head 的尺子 每格 0.0025,量程 ±0.32 --- 量蚂蚁
  • 各自量各自的,理论上都能量准

关键:理论精度 ≠ 实际精度

步长决定的是理论精度 ,实际精度取决于你的数占了多少个等级。尺子有256个刻度,但90%的数挤在前20个刻度里------后面的刻度全浪费了,这才是长尾分布的问题。

1.4 余弦相似度是在测什么?

一句话本质:量化前后,向量的"方向"有没有变。

复制代码
公式: cosine_similarity(A, B) = (A·B) / (||A|| × ||B||)
只关心方向,不关心长度。值域 [-1, 1]:
  1.0 = 方向完全一致  ✅
  0.5 = 方向偏了45°  ⚠️
  0.0 = 正交          ❌
 -1.0 = 方向完全相反  ❌❌

为什么检测任务用余弦相似度?

神经网络中,特征向量不只是"数值集合",它代表检测到的模式

python 复制代码
某个特征向量 [0.1, 0.0, 0.8, -0.3, ...]
    ↓
这个向量的"方向"决定了它代表"猫耳朵"还是"车轮"
    ↓
量化后如果方向变了 → 模型"认错"了特征

三个数值的直观对比:

情况 量化后 余弦相似度 含义
完美 1.02, 1.98 原=1.0, 2.0 ≈1.0 方向没变 ✅ 检测没问题
偏移 0.80, 1.60 原=1.0, 2.0 ≈1.0 整体缩小了,但方向没变 ✅ 也没问题
扭曲 1.50, 1.50 原=1.0, 2.0 0.99 方向偏了,本该是1:2现在1:1 ❌

余弦不关心整体缩放,只关心向量内部的比例关系有没有被破坏。 检测头里,不同通道的比例关系决定了"这是什么目标",比例乱了检测就崩。

余弦 vs 相对误差:

复制代码
余弦相似度 → 方向有没有变?      → 检测准不准的关键
相对误差   → 每个数准不准?        → 回归精度(框的位置)的关键

第二部分:实验拆解------三个场景,一步步看到真相

2.1 实验设计思路

完整代码实验在项目仓库中,核心逻辑:

python 复制代码
def simulate_int8_quantize(tensor):
    """INT8对称量化 + 反量化"""
    max_abs = tensor.abs().max()
    scale = max_abs / 127.0          # 计算步长
    q = torch.clamp(torch.round(tensor / scale), -128, 127)  # 量化
    return q * scale                  # 反量化

实验分三个场景,逐步递进:

复制代码
场景A: 大方差高斯 (σ=5.0)   → 模拟Backbone输出
场景B: 小方差高斯 (σ=0.1)   → 模拟理想化的Head输出
场景C: 长尾分布 (90%小+10%大) → 模拟真实Head输出

2.2 场景A vs B:纯高斯分布------方差不是原因

基本数据:

特征 取值范围 scale(每格宽) 步长比
Backbone (σ=5) -15.51, 15.13 0.1221 48.7x
Head (σ=0.1) -0.29, 0.32 0.0025 1x

逐元素误差对比:

Backbone 前10个值:

复制代码
量化前:   9.6346,   7.4364,   4.5036,  -10.5276,  3.3921, ...
量化值:   79,       61,       37,      -86,       28,     ...
反量化:   9.6467,   7.4487,   4.5181,  -10.5015,  3.4191, ...
相对误差: 0.13%,    0.17%,    0.32%,   0.25%,     0.80%,  ...  ← 基本 <1%

Head 前10个值:

复制代码
量化前:   0.0101,  -0.1309,  -0.0410,   0.0468,  -0.0235, -0.000013, ...
量化值:   4,       -52,      -16,       19,      -9,        0,      ...
反量化:   0.0100,  -0.1305,  -0.0402,   0.0477,  -0.0226,   0.0,    ...
相对误差: 0.67%,   0.33%,    2.14%,     1.87%,   3.73%,    99.9%,   ← 小值被吞!

全局统计:

指标 Backbone(σ=5) Head(σ=0.1)
平均绝对误差 0.0306 0.0006
平均相对误差 3.24% 3.17%
余弦相似度 0.999975 0.999976

结论:纯高斯分布下,大小方差的表现几乎一样。 Head虽然有极个别小值被"吞掉"(相对误差99.9%),但由于只有个例,平均下来和Backbone差不多。单纯方差小不是敏感的根本原因。

2.3 场景C:真实长尾分布------这才是真凶

检测头的输出分布不是高斯分布------图片中90%的区域是背景(接近0的小值),只有10%的区域有目标(明显激活的大值)。用这个分布模拟:

python 复制代码
real_head = torch.cat([
    torch.randn(9000) * 0.05,    # 90% 小值(背景)
    torch.randn(1000) * 0.5,     # 10% 大值(目标)
])[torch.randperm(10000)]        # 打乱顺序

分布对比:

复制代码
Backbone (高斯σ=3):       范围 [-11.4, 12.9], 90%值在 [-4.97, 4.93]
真实Head (90%小+10%大):   范围 [-1.56, 1.50], 90%值在 [-0.12, 0.12]

关键:Head的 max_abs=1.56 是由那10%的大值决定的,但90%的值集中在 ±0.12。

前10个值量化细节:

复制代码
量化前:   0.0696,  0.0350,  -0.0300,  0.0172,  0.0066,  -0.0752, ...
量化值:   6,       3,       -2,       1,       1,       -6,      ...
反量化:   0.0738,  0.0369,  -0.0246,  0.0123,  0.0123,  -0.0738, ...
相对误差: 6.03%,   5.31%,   18.10%,  28.45%,  85.05%,  1.88%,   ...  ← 大面积超标!

小值区域误差对比(前90%数据):

指标 Backbone 真实Head
小值区域相对误差 1.05% 4.10% ← 3.9倍
小值占用INT8等级 202/256 (78.9%) 184/256 (71.9%)

原因分析:

  1. scale 被 10% 的大值决定(尺子被拉长)
  2. 90% 的小值只用到尺子的一小段(±0.12 / ±1.56 = 7.7% 的尺子长度)
  3. 256个INT8等级中,只有256×7.7% ≈ 20 个等级分配给90%的数据
  4. 20个等级表达9000个数值 → 大量数值挤在同一格 → 精度崩盘

2.4 完整方差扫描:验证高斯场景下的结论

用 4D 特征图 (1, 64, 80, 80) 验证:

复制代码
特征类型                   σ      max_abs    scale     余弦相似度  相对误差
------------------------------------------------------------------------
Backbone(σ=5.0)        5.00   23.1330    0.182150   0.9999     1.1420%  [鲁棒]
Backbone(σ=3.0)        3.00   14.6539    0.115385   0.9999     1.2037%  [鲁棒]
Neck(σ=1.0)            1.00    4.6354    0.036499   0.9999     1.1440%  [鲁棒]
Neck(σ=0.5)            0.50    2.3034    0.018137   0.9999     1.1308%  [鲁棒]
Head(σ=0.1)            0.10    0.4550    0.003583   0.9999     1.1260%  [鲁棒]
Head(σ=0.05)           0.05    0.2392    0.001884   0.9999     1.1799%  [鲁棒]

验证:纯高斯分布下,σ=5.0 到 σ=0.05,余弦相似度全部 0.9999。 只要数据是高斯分布,信号和尺子等比缩放,INT8精度不受方差大小影响。

2.5 额外验证:均匀分布填满量程会怎样?

如果数据能在刻度量程内均匀分布,效果最好吗?验证一下。

-5, 5 范围的三种分布,共用同一个 scale=0.0394

分布 占用等级 等级数 相对误差 说明
均匀分布 -127~127 全满 256 0.69% 🏆 满格使用,每格均匀受力
高斯分布 σ=1.67 -125~125 251 1.13% 边缘十几格用得少
长尾分布 (90小+10大) -83~69 153 4.52% 一格挤了650个数

均匀分布填满量程确实是最好的情况。但检测头的输出天生就是长尾(背景远多于目标),无法改变分布,所以只能换 FP16------256个等级不够,换65536个等级来凑。


第三部分:关键洞察------我们是怎么理解这件事的

3.1 尺子比喻:三个场景的不同命运

复制代码
Backbone场景(信号强,高斯分布):
  尺子全长 = 30, 每格宽 = 30/256 ≈ 0.12
  数值分布在 ±15 范围 → 满格使用 256 级
  相对误差 ≈ 1.2%  ✅

Head场景(高斯,信号弱但匹配):
  尺子全长 = 0.6, 每格宽 = 0.6/256 ≈ 0.0023
  数值分布在 ±0.3 范围 → 满格使用 256 级
  相对误差 ≈ 1.2%  ✅  ← 同步缩小,形状不变

Head场景(真实长尾分布,信号不匹配):
  尺子由大值决定:全长 = 3, 每格宽 = 3/256 ≈ 0.012
  但90%的小值只分布在 ±0.12 范围
  小信号只用到了 0.24/3 = 8% 的尺子长度
  可用刻度: 256 × 8% ≈ 20 个等级!
  20个等级表达90%的数据 → 精度崩盘 ❌

3.2 "同步缩小"------你发现的本质

在学习过程中总结出的一个直觉,后来发现非常精准:

量化就是整体缩小,关键是要同步缩小。不同步就失真,失真就变形,变形检测就崩。

三句话翻译:

直觉 量化术语 数学含义
"缩小" 精度降低 FP32 → INT8,丢掉小数部分
"同步缩小" 比例保持 余弦相似度 ≈ 1.0,方向不变
"失真变形" 比例扭曲 余弦相似度↓,特征表征错误

三种分布对应三种结果:

场景 你的比喻 量化表现 结果
Backbone (高斯) 整张照片等比缩小 所有值同比例缩放,方向不变 ✅ 检测正常
Head (高斯) 小照片等比缩小 同上,只是整体小了 ✅ 检测正常
Head (长尾) 中间缩小1倍,边缘缩小10倍 小值被粗粒度量化,比例扭曲 ❌ 检测崩了

ASCII示意图:

复制代码
原图(FP32):  ████████████████████   均匀分布,256级表达
              ↓
INT8等比缩小: ████████████████       同步缩小,形状不变 ✅


原图(FP32):  ██████░░░░░░░░░░░░░░   长尾分布,大值占部分等级
              ↓
INT8非等比:   ████░░░░░░░░░░░░░░     小值区域压得更狠,形状变了 ❌

量化不怕整体缩小,怕的是有的地方缩得多、有的地方缩得少。 Backbone的256个等级均匀受力,每个值缩的程度差不多 → 不失真。Head的等级被大值占了太多,小值区域"压得太狠" → 严重失真。


第四部分:完整核心逻辑链

把从理论到实践的逻辑串起来:

复制代码
                   ┌─────────────────────────┐
                   │ INT8量化 = 256个等级     │
                   │ 去表达连续浮点数          │
                   └──────────┬──────────────┘
                              │
              ┌───────────────┴───────────────┐
              │                               │
              ▼                               ▼
    ┌──────────────────┐          ┌──────────────────┐
    │ 高斯分布          │          │ 长尾分布          │
    │ 信号和量程匹配    │          │ 信号和量程不匹配  │
    │ 256级均匀受力     │          │ 大值拉长量程      │
    │ 余弦≈0.9999       │          │ 小值精度崩盘      │
    └──────────────────┘          └──────────────────┘
              │                               │
              ▼                               ▼
    ┌──────────────────┐          ┌──────────────────┐
    │ Backbone/Neck    │          │ Head (检测头)     │
    │ 特征接近高斯分布  │          │ 90%小+10%大       │
    │ INT8 鲁棒 ✅      │          │ 必须 FP16 ❌      │
    └──────────────────┘          └──────────────────┘
              │                               │
              └───────────────┬───────────────┘
                              ▼
                   ┌─────────────────────────┐
                   │ 混合精度部署策略         │
                   │ Backbone: INT8 (省带宽)  │
                   │ Neck:     INT8 (看阈值)  │
                   │ Head:     FP16 (保精度)  │
                   └─────────────────────────┘

加入之前几篇笔记的知识链条:

笔记 关键认知 与本篇的关系
01-INT8量化精度 量化唯一丢精度的步骤 = round scale决定round的误差大小
06-Anchor到Anchor-Free Anchor-Free缺少sigmoid的误差缓冲 检测头敏感→误差直接传到解码
07-检测头拆解 检测头输出dfl+dbox,值域极小 解释了为什么检测头的值这么小
08-sigmoid误差缓冲 Anchor-Based的sigmoid压缩输入误差 这解释了为什么Anchor-Based相对鲁棒
本篇 长尾分布才是检测头敏感的真正原因 不是方差小,是尺子被拉长了

第五部分:从原理到实践------混合精度的本质

这篇笔记提供了为什么需要混合精度的底层证据:

模型部分 分布特点 余弦相似度 建议策略 根本原因
Backbone (C2f, SPPF) 高斯分布,信号强 >0.99 INT8 满格使用256级,相对误差小
Neck (PAN-FPN) 近高斯,小信号 0.97~0.99 看阈值 分布均匀,可以INT8
Head (分类) 长尾,大部分小值 0.88~0.95 FP16 10%大值拉长尺子,小值精度崩
Head (回归) 长尾,极多小值 0.75~0.90 必须FP16 尺子被严重拉长,小值精度严重不足

核心决策逻辑:

复制代码
每一层的权重/激活分布不同
    ↓  查看 distribution + variance
高斯/近高斯分布 → 信号和量程匹配 → INT8 (省带宽省功耗)
长尾分布        → 量程被大值拉长 → FP16 (保精度)
    ↓
混合精度: 不同层用不同精度,不是一刀切

这就是混合精度的核心思想: 每一层的权重/激活分布不同,精度需求也不同,不能一刀切。混合精度要解决的就是"具体怎么给每层分配精度"的问题------从分布分析到精度配置的逻辑闭环。


自测

不看上面内容,回答下面问题:

Q1

为什么纯高斯分布下 σ=0.05 也能达到 0.9999 的余弦相似度?
👉 点击查看答案

因为高斯分布下,信号和尺子是等比缩放的。σ小了 → 所有值同步变小 → max_abs也变小 → scale也变小 → 信号÷scale得到的整数值和小σ下一样多。所以 σ=5 和 σ=0.05 实际上用了等量的INT8等级,精度表现一致。

共鸣:"同步缩小"------原图是 1000×1000,缩小成 10×10,虽然像素少了但形状不变

Q2

检测头对INT8敏感的真实原因是什么?
👉 点击查看答案

不是"方差小",而是长尾分布:检测头的输出 ≈ 90% 背景小值(σ≈0.05)+ 10% 目标大值(σ≈0.5)。

scale 被那 10% 的大值决定,尺子被拉长了。90% 的小值只用到尺子 7.7% 的长度,256级中只有 ≈20 级可用。20级表达9000个数值 → 精度崩盘。

背口诀:小值不敏感,长尾才要命。大值拉尺子,小值丢精度。

Q3

量化公式中为什么除以 127 而不是 128?
👉 点击查看答案

INT8 的范围是 -128, 127,对称量化时:

  • 正半轴:1 ~ 127,共 127 个正数
  • 负半轴:-128 ~ -1,共 128 个负数
  • 0 占中间

正半轴只有 127 个格子 → 最大正数 = 127 → 除以 127 把 max_abs 映射到 127 号格子。

通用公式:2^(N-1) - 1,INT8 = 2^(8-1) - 1 = 128 - 1 = 127。

Q4

余弦相似度为什么比绝对误差更能反映检测头的量化敏感度?
👉 点击查看答案

余弦相似度测的是方向有没有变 ,绝对误差测的是每个值差了多少

检测任务中,特征向量的方向决定了"这是什么目标"(猫耳朵 vs 车轮)。如果量化后整体等比例缩小(比如所有值乘 0.8),绝对误差很大但方向没变,检测仍然正确。但如果方向偏了(比如 1,21.5,1.5),特征表征就错了,检测就会崩。

余弦不关心整体缩放,只关心向量内部的比例关系有没有被破坏。


一句话记住

复制代码
高斯分布下,大小方差都能INT8,因为信号和尺子一起缩放。
检测头敏感的真实原因不是方差小,而是**长尾分布**------
10%的大值拉长了尺子,让90%的小值用不到几个量化等级,
相对误差爆炸,检测精度崩盘。

你总结得更直接:量化就是缩小,关键是要同步缩小,
不同步就会失真变形,检测就崩了。

混合精度做的:看每层的分布 → 高斯就用INT8 → 长尾就用FP16 → 混合精度
相关推荐
尽兴-1 个月前
2.1 向量基础:Embedding、余弦相似度、欧氏距离、向量检索
算法·embedding·欧氏距离·向量检索·余弦相似度
庞轩px2 个月前
Embedding与向量语义——大模型是怎样“理解”文字的?
人工智能·自然语言处理·embedding·向量检索·余弦相似度·rag·高维向量空间
F_D_Z3 个月前
独热编码与余弦相似度
余弦相似度·独热编码
Together_CZ5 个月前
ultralytics.nn.modules.head——head.py子模块代码详读
目标检测·ultralytics·yoloe·nn.modules.head·检测头·分割头·姿态估计头
雪碧聊技术5 个月前
11.RAG知识库的原理
向量·余弦相似度·rag知识库
北京地铁1号线6 个月前
2.3 相似度算法详解:Cosine Similarity 与 Euclidean Distance
算法·余弦相似度
Java后端的Ai之路6 个月前
【机器学习】-长尾分布解读指南
人工智能·机器学习·长尾分布
大千AI助手7 个月前
长尾分布:现实世界数据的本质挑战与机器学习应对之道
人工智能·机器学习·数据分布·大千ai助手·长尾分布·均匀分布·高度不平衡
nju_spy8 个月前
南京大学LLM开发基础(四)MoE, LoRA, 数的精度 + MLP层实验
人工智能·lora·大模型·混合精度·混合专家模型 moe·densemlp·门控机制