4. per-tensor与per-channel量化对比

任务目标

本案例以YOLOv5s backbone中首个3×3卷积层权重为对象,分别实施per-tensor(整个张量共享一个scale/zero_point)与per-channel(每个输出通道独立scale/zero_point)对称量化,对比两种粒度下反量化权重与原始权重的最大误差(Max Abs Error)及余弦相似度,从而量化分析量化粒度对权重精度的实际影响。

环境与依赖

需要 torch、torchvision(用于加载YOLOv5s预训练权重)。若网络受限,可离线下载权重文件后通过本地路径加载。

核心实现

import torch
import torchvision

# 1. 加载YOLOv5s backbone第一层卷积权重
model = torchvision.models.detection.yolov5s(weights=None)  # 仅取结构
# 实际使用官方yolov5s权重(此处用随机初始化演示流程,真实评估请加载官方权重)
state = torch.load('yolov5s.pt', map_location='cpu')['model'].float().state_dict()
conv_weight = state['model.0.conv.weight']  # shape: [32, 3, 3, 3]

def symmetric_quantize(tensor, bits=8):
    """对称量化,返回量化值、scale"""
    amax = tensor.abs().max()
    scale = amax / (2**(bits-1) - 1)
    q = torch.round(tensor / scale).clamp(-(2**(bits-1)), 2**(bits-1)-1)
    return q, scale

def per_tensor_dequant(w, bits=8):
    q, scale = symmetric_quantize(w, bits)
    return q * scale

def per_channel_dequant(w, bits=8):
    # w shape: [out_c, in_c, kh, kw]
    out_c = w.shape[0]
    w_reshaped = w.view(out_c, -1)
    scales = w_reshaped.abs().max(dim=1).values / (2**(bits-1) - 1)
    q_list = []
    for i in range(out_c):
        q_i = torch.round(w_reshaped[i] / scales[i]).clamp(-(2**(bits-1)), 2**(bits-1)-1)
        q_list.append(q_i * scales[i])
    return torch.stack(q_list).view_as(w)

# 2. 执行两种量化
w_pt = per_tensor_dequant(conv_weight)
w_pc = per_channel_dequant(conv_weight)

# 3. 误差与相似度评估
def max_abs_err(a, b):
    return (a - b).abs().max().item()

def cos_sim(a, b):
    return torch.nn.functional.cosine_similarity(a.flatten(), b.flatten(), dim=0).item()

print(f"per-tensor  MaxAbsErr: {max_abs_err(conv_weight, w_pt):.6f}")
print(f"per-tensor  CosSim:    {cos_sim(conv_weight, w_pt):.6f}")
print(f"per-channel MaxAbsErr: {max_abs_err(conv_weight, w_pc):.6f}")
print(f"per-channel CosSim:    {cos_sim(conv_weight, w_pc):.6f}")

# 4. 通道级误差分布(观察per-channel是否均匀)
channel_err_pt = (conv_weight - w_pt).view(conv_weight.shape[0], -1).abs().max(dim=1).values
channel_err_pc = (conv_weight - w_pc).view(conv_weight.shape[0], -1).abs().max(dim=1).values
print(f"per-tensor  通道误差范围: [{channel_err_pt.min():.6f}, {channel_err_pt.max():.6f}]")
print(f"per-channel 通道误差范围: [{channel_err_pc.min():.6f}, {channel_err_pc.max():.6f}]")

运行方式

  1. 将上述代码保存为 compare_quant.py。
  2. 确保当前目录存在官方 yolov5s.pt 权重文件(从YOLOv5官方仓库release下载)。
  3. 执行 python compare_quant.py。

验证方法与判定标准

观察输出的四项指标:

  • 通过条件:per-channel的MaxAbsErr应显著小于per-tensor(通常小一个数量级以上),且CosSim更接近1.0。
  • 通道误差范围:per-tensor的通道误差范围跨度大(某些通道误差极大),而per-channel各通道误差均匀且整体偏小。这直接说明per-channel通过为每个通道独立分配scale,避免了动态范围差异大的通道被“拖累”。

若使用随机初始化权重(如上代码注释所示),由于各通道分布相似,两者差异可能不明显;因此务必加载真实预训练权重进行对比,才能观察到per-tensor因通道间范围差异导致的精度损失。

per-tensor vs per-channel 量化误差对比示意 per-tensor(共享scale) 权重张量 [32, 3, 3, 3] 通道0 通道1 通道2 通道3 通道4 通道5 ... 单一 scale = max(|W|)/127 通道间范围差异大 → 小范围通道误差大 MaxAbsErr: 0.0231(示例) per-channel(每通道独立scale) 权重张量 [32, 3, 3, 3] 通道0 通道1 通道2 通道3 通道4 通道5 ... 每通道独立 scale_i = max(|W_i|)/127 各通道按自身范围缩放 → 误差均匀且小 MaxAbsErr: 0.0018(示例) 通道权重 per-tensor scale per-channel scales

关键说明

  • 本案例聚焦权重静态量化的粒度对比,不涉及激活量化与反向传播,因此误差分析仅反映权重本身的量化损失。
  • 实际部署中,per-channel量化通常需要硬件支持(如NVIDIA TensorRT、Intel QDQ格式),若目标平台不支持,则需退化为per-tensor并接受相应精度损失。
  • 若加载官方权重后运行发现per-tensor误差远大于per-channel,即验证了通道间权重范围差异是per-tensor精度下降的主因;若两者接近,则说明该层通道分布均匀,此时可结合后续层(如检测头)的敏感性决定是否值得采用per-channel。