2. 精度诊断工具:定位量化精度损失的“病灶”

量化精度下降,说白了就是模型在 int8 下“变笨了”。但变笨的原因千奇百怪——有的层对数值特别敏感,有的层激活值分布太散,还有的层干脆就是量化参数没选对。

我刚开始做量化时,也踩过不少坑。有一次模型在 COCO 上掉了 3 个点,我对着整网调了一周,最后发现只是某一层 ReLU 后的激活值分布出了问题。嗯,从那以后我就养成了一个习惯:先诊断,再动手。

这一章,我们就来聊聊怎么用 Eval 工具做精度诊断。说白了,就是给模型做个“CT 扫描”,找到精度下降的病灶在哪。

2.1 诊断流程总览

我个人习惯把诊断分成三步:

  1. 全局扫描:跑一遍量化前后的精度对比,确认问题严重程度。
  2. 逐层定位:用 Eval 工具逐层分析,找到精度下降最严重的层。
  3. 微观分析:对问题层做激活值分布、权重分布、量化误差的统计。

下面这张图可以帮你快速理解整个流程:

量化精度诊断流程 全局精度对比 FP32 vs INT8 mAP 逐层精度扫描 定位 Top-K 问题层 微观分析 分布 + 误差统计 微观分析三件套 ① 激活值分布可视化:看数值范围是否被截断 ② 权重分布可视化:看权重是否集中在量化死区 ③ 量化误差层间分布:看哪一层误差最大 输出:问题层列表 + 修复优先级

2.2 全局精度对比:先看“病”有多重

别一上来就钻到细节里。先跑一遍量化前后的精度对比,确认问题到底有多大。

我一般用下面这个脚本:

# 评估 FP32 模型
python val.py --data coco.yaml --weights yolov11n.pt --batch 32 --img 640

# 评估 INT8 模型(假设已经量化好)
python val.py --data coco.yaml --weights yolov11n_int8.pt --batch 32 --img 640 --int8

对比结果通常长这样:

指标 FP32 INT8 下降幅度
mAP@0.5:0.95 37.3% 35.1% -2.2%
mAP@0.5 53.2% 51.0% -2.2%
mAP@0.75 40.1% 37.8% -2.3%
我的经验:如果 mAP 下降超过 1%,就需要认真诊断了。下降 0.5% 以内,通常可以接受。

2.3 逐层精度扫描:找到“病灶”

全局对比只能告诉你“病了”,但不知道“病在哪”。这时候就需要逐层扫描。

YOLOv11 的 Eval 工具支持逐层精度对比。我习惯用 --layer-wise 参数:

python val.py --data coco.yaml --weights yolov11n_int8.pt --layer-wise --save-json

输出会生成一个 JSON 文件,里面记录了每一层量化前后的精度差异。我写了个小脚本把它可视化:

import json
import matplotlib.pyplot as plt

with open('layer_wise_results.json', 'r') as f:
    data = json.load(f)

# 提取每层的精度下降
layers = [item['layer_name'] for item in data]
drops = [item['mAP_drop'] for item in data]

# 排序,找 Top-10 问题层
sorted_idx = sorted(range(len(drops)), key=lambda i: drops[i], reverse=True)
top10_layers = [layers[i] for i in sorted_idx[:10]]
top10_drops = [drops[i] for i in sorted_idx[:10]]

plt.figure(figsize=(12, 6))
plt.barh(top10_layers, top10_drops, color='#e74c3c')
plt.xlabel('mAP Drop (%)')
plt.title('Top-10 Layers with Largest Accuracy Drop')
plt.tight_layout()
plt.savefig('top10_problem_layers.png')
注意:逐层扫描会显著增加评估时间。我建议先用小数据集(比如 500 张图)快速定位,再用全量数据验证。

2.4 激活值与权重分布可视化

找到问题层后,下一步就是看它的激活值和权重分布。说白了,就是看量化后数值被“挤”成了什么样。

2.4.1 激活值分布

激活值分布能告诉你:量化时有没有发生严重的截断或舍入误差。

import torch
import matplotlib.pyplot as plt

# 假设 model 是量化后的模型,dataloader 是验证集
model.eval()
activations = []

with torch.no_grad():
    for images, _ in dataloader:
        # 注册 hook 来获取某层的输出
        def hook_fn(module, input, output):
            activations.append(output.cpu().numpy().flatten())
        
        handle = model.model[10].register_forward_hook(hook_fn)  # 第10层
        _ = model(images)
        handle.remove()

# 合并所有激活值
all_acts = np.concatenate(activations)

plt.figure(figsize=(10, 4))
plt.hist(all_acts, bins=100, color='#3498db', alpha=0.7)
plt.axvline(x=-128, color='red', linestyle='--', label='INT8 min')
plt.axvline(x=127, color='red', linestyle='--', label='INT8 max')
plt.xlabel('Activation Value')
plt.ylabel('Frequency')
plt.legend()
plt.title('Activation Distribution - Layer 10')
plt.show()
怎么看?如果激活值大量集中在 -128 或 127 附近,说明发生了严重的截断。如果分布太散(比如范围超过 [-128, 127]),说明量化参数没选好。

2.4.2 权重分布

权重分布能告诉你:量化后权重有没有“死掉”。

# 提取某层的权重
weight = model.model[10].conv.weight.data.cpu().numpy().flatten()

plt.figure(figsize=(10, 4))
plt.hist(weight, bins=100, color='#2ecc71', alpha=0.7)
plt.xlabel('Weight Value')
plt.ylabel('Frequency')
plt.title('Weight Distribution - Layer 10')
plt.show()
我的经验:如果权重分布集中在 0 附近,说明这层可能“学废了”。我曾经遇到过一个 case,某层 80% 的权重都在 [-1, 1] 之间,量化后直接变成 0,精度掉了 2 个点。

2.5 量化误差的层间分布

最后一步,统计每一层的量化误差。我一般用 均方误差(MSE) 和 余弦相似度 两个指标。

指标 含义 阈值(我的经验)
MSE 量化前后输出的平均平方误差 < 0.01 正常,> 0.05 需要关注
余弦相似度 量化前后输出方向的相似度 > 0.99 正常,< 0.95 需要关注

计算脚本:

def compute_layer_wise_error(model_fp32, model_int8, dataloader):
    errors = {}
    
    # 注册 hook 到每一层
    hooks_fp32 = []
    hooks_int8 = []
    
    for name, module in model_fp32.named_modules():
        if isinstance(module, torch.nn.Conv2d):
            def make_hook(name):
                def hook(module, input, output):
                    errors.setdefault(name, {'fp32': [], 'int8': []})
                    errors[name]['fp32'].append(output.detach().cpu())
                return hook
            hooks_fp32.append(module.register_forward_hook(make_hook(name)))
    
    # 类似地注册 int8 模型的 hook
    # ...(代码略,逻辑相同)
    
    # 计算每层的 MSE 和余弦相似度
    for name in errors:
        fp32_out = torch.cat(errors[name]['fp32'], dim=0)
        int8_out = torch.cat(errors[name]['int8'], dim=0)
        
        mse = torch.mean((fp32_out - int8_out) ** 2).item()
        cos_sim = torch.nn.functional.cosine_similarity(
            fp32_out.view(fp32_out.size(0), -1),
            int8_out.view(int8_out.size(0), -1)
        ).mean().item()
        
        print(f'{name}: MSE={mse:.6f}, CosSim={cos_sim:.4f}')
注意:MSE 和余弦相似度要结合着看。MSE 小但余弦相似度低,说明输出方向变了但幅度没变,这通常比 MSE 大更危险。

2.6 诊断报告模板

每次做完诊断,我都会生成一份报告。下面是我常用的模板:

层名 类型 mAP 下降 MSE 余弦相似度 优先级
model.10.conv Conv2d -0.8% 0.032 0.97 高
model.15.conv Conv2d -0.5% 0.018 0.98 中
model.22.conv Conv2d -0.2% 0.005 0.99 低
总结一下:诊断的核心就是三步——全局对比找问题、逐层扫描定位、微观分析确认。有了这份报告,你就能精准地知道该修哪一层,而不是盲目调参。