2. 精度诊断工具:定位量化精度损失的“病灶”
量化精度下降,说白了就是模型在 int8 下“变笨了”。但变笨的原因千奇百怪——有的层对数值特别敏感,有的层激活值分布太散,还有的层干脆就是量化参数没选对。
我刚开始做量化时,也踩过不少坑。有一次模型在 COCO 上掉了 3 个点,我对着整网调了一周,最后发现只是某一层 ReLU 后的激活值分布出了问题。嗯,从那以后我就养成了一个习惯:先诊断,再动手。
这一章,我们就来聊聊怎么用 Eval 工具做精度诊断。说白了,就是给模型做个“CT 扫描”,找到精度下降的病灶在哪。
2.1 诊断流程总览
我个人习惯把诊断分成三步:
- 全局扫描:跑一遍量化前后的精度对比,确认问题严重程度。
- 逐层定位:用 Eval 工具逐层分析,找到精度下降最严重的层。
- 微观分析:对问题层做激活值分布、权重分布、量化误差的统计。
下面这张图可以帮你快速理解整个流程:
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 | 低 |
总结一下:诊断的核心就是三步——全局对比找问题、逐层扫描定位、微观分析确认。有了这份报告,你就能精准地知道该修哪一层,而不是盲目调参。