博客算法工具链征程6上模型剪枝与量化联合优化:把YOLOv5x压缩到YOLOv5s的延迟,精度只掉1.5%

征程6上模型剪枝与量化联合优化:把YOLOv5x压缩到YOLOv5s的延迟,精度只掉1.5%

默认265282026-08-30
29
0

项目初期用YOLOv5x做检测,精度不错(mAP 84.2%),但延迟太高(38ms),帧率只有26fps,不满足产品要求的30fps。单独做量化优化(int8 PTQ)虽然延迟降到了24ms,但精度掉到81.5%。后来尝试把模型剪枝和量化结合起来,先剪枝减少模型参数量,再量化降低精度损失,最终延迟干到18ms(55fps),精度82.7%,只比原始浮点掉1.5%。这篇把联合优化的完整流程和踩坑经验记下来。

一、为什么单独量化不够?

YOLOv5x的参数量有86M,在征程6M上跑起来延迟38ms。我做过的优化尝试:

优化手段 延迟 mAP 问题

原始浮点 38ms 84.2% 基准

int8 PTQ 24ms 81.5% 精度掉2.7%,不够

int8 PTQ + mix 26ms 82.1% 精度还是不够

换YOLOv5s 12ms 77.2% 精度掉7%,差距太大

剪枝+int8 PTQ 18ms 82.7% 最优

单独量化只能解决"跑得慢"的问题,解决不了"精度掉太多"的问题。因为YOLOv5x本身参数量太大,量化误差被放大了。

思路:

先把模型剪枝变小(减少参数量),再量化。小模型量化后的精度损失比大模型更小,因为参数量少了,每个参数的重要性更高,量化时更"珍惜"每个参数。

二、剪枝策略选择

剪枝分两种:

1. 非结构化剪枝:随机剪掉单个权重,压缩率高但硬件支持差(BPU不支持稀疏矩阵)

2. 结构化剪枝:剪掉整个filter或channel,压缩率低但硬件友好

征程6的BPU只支持结构化剪枝,所以必须选方案2。

结构化剪枝的具体做法

以YOLOv5x的backbone为例,每层的channel数:

层 原始channel 剪枝后channel 剪枝比例

conv1 80 64 20%

conv2 160 128 20%

conv3 320 256 20%

conv4 640 512 20%

conv5 1280 1024 20%

统一剪掉20%的channel,模型参数量从86M降到55M,减少了36%。

剪枝方法: 基于L1 norm的filter importance排序,剪掉importance最低的filter。

```python

import torch

import torch.nn as nn

def prune_conv_layer(conv_layer, prune_ratio=0.2):

"""剪掉一个Conv2d层中importance最低的filter"""

# 计算每个filter的L1 norm(作为importance指标)

filters = conv_layer.weight.data # shape: [outC, inC, kH, kW]

importance = torch.sum(torch.abs(filters), dim=[1, 2, 3]) # 每个filter的L1 norm

# 排序,找出最不重要的filter

num_prune = int(conv_layer.out_channels * prune_ratio)

_, prune_indices = torch.topk(importance, num_prune, largest=False)

# 保留剩余的filter

keep_mask = torch.ones(conv_layer.out_channels, dtype=torch.bool)

keep_mask[prune_indices] = False

# 剪枝weight

conv_layer.weight.data = conv_layer.weight.data[keep_mask]

if conv_layer.bias is not None:

conv_layer.bias.data = conv_layer.bias.data[keep_mask]

# 更新out_channels

conv_layer.out_channels = conv_layer.out_channels - num_prune

return prune_indices

def prune_model(model, prune_ratio=0.2):

"""对整个模型做结构化剪枝"""

for name, module in model.named_modules():

if isinstance(module, nn.Conv2d):

# 剪掉当前层的output filter

prune_indices = prune_conv_layer(module, prune_ratio)

# 找到下一层,剪掉对应的input channel

next_module = find_next_conv(model, name)

if next_module is not None:

next_module.weight.data = next_module.weight.data[:, ~prune_indices]

next_module.in_channels = next_module.in_channels - len(prune_indices)

return model

```

注意: 剪枝不是一次性完成的,需要 iterative pruning + fine-tuning:

1. 剪掉10%的filter

2. fine-tune 5个epoch恢复精度

3. 再剪掉10%

4. fine-tune 5个epoch

5. ...直到达到目标压缩率

一次剪太多(比如直接剪50%),模型精度会崩,fine-tune也救不回来。iterative的方式可以让模型逐步适应新的结构。

三、剪枝后的精度恢复

剪枝20%后,模型精度从84.2%掉到了78.5%。需要通过fine-tune恢复:

```python

# 剪枝后的fine-tune

optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.937)

scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)

for epoch in range(100):

for batch in dataloader:

images, targets = batch

predictions = model(images)

loss = compute_loss(predictions, targets)

optimizer.zero_grad()

loss.backward()

optimizer.step()

scheduler.step()

# 每10个epoch评估一次

if epoch % 10 == 0:

mAP = evaluate(model, val_dataset)

print(f"Epoch {epoch}: mAP = {mAP:.1f}%")

```

fine-tune 100个epoch后,精度恢复到了83.8%,只比原始掉0.4%。这个精度对于后续量化来说已经足够好了。

一个小技巧: fine-tune的时候用knowledge distillation(知识蒸馏),用原始大模型(未剪枝的YOLOv5x)当teacher,剪枝后的模型当student。distillation loss可以帮助student学到teacher的"暗知识",精度恢复更快。

```python

# Knowledge distillation

teacher_model = YOLOv5x() # 原始大模型

teacher_model.load_state_dict(torch.load('yolov5x.pt'))

teacher_model.eval()

for batch in dataloader:

images, targets = batch

with torch.no_grad():

teacher_preds = teacher_model(images)

student_preds = model(images) # 剪枝后的模型

# 检测loss

detection_loss = compute_loss(student_preds, targets)

# distillation loss(teacher和student的分类分数要一致)

distillation_loss = F.kl_div(

F.log_softmax(student_preds[0], dim=1),

F.softmax(teacher_preds[0], dim=1),

reduction='batchmean'

)

loss = detection_loss + 0.5 * distillation_loss

optimizer.zero_grad()

loss.backward()

optimizer.step()

```

加了distillation后,fine-tune 50个epoch精度就恢复到了83.5%,省了50%的训练时间。

四、量化:剪枝后模型的int8 PTQ

剪枝后的模型参数量从86M降到了55M,但直接做int8 PTQ精度还是掉不少(83.8% -> 80.2%)。原因是剪枝后的模型更"脆弱",每个参数的重要性更高,量化误差更敏感。

解决: 用更精细的量化策略。

1. per-channel量化

默认的int8量化是per-tensor(整个tensor共用一个scale和zero_point),但对于剪枝后的模型,不同channel的权重分布差异很大,共用一个scale精度损失大。

per-channel量化给每个output channel单独的scale:

```yaml

calibration_parameters:

calibration_type: mix

quantization_strategy:

weight_quantization: per_channel # per_tensor | per_channel

activation_quantization: per_tensor

```

per-channel比per-tensor精度提升1.2%(80.2% -> 81.4%),但模型文件会稍微大一点(每个channel多存一个scale值)。

2. 对敏感层用int16

剪枝后的模型对某些层更敏感,这些层用int16:

```yaml

layer_config:

# 检测头(所有输出层)用int16

- layer_name: "model\\.24\\.m\\.0"

precision_mode: int16

- layer_name: "model\\.24\\.m\\.1"

precision_mode: int16

- layer_name: "model\\.24\\.m\\.2"

precision_mode: int16

# backbone的前两层用int16(剪枝后这几层更敏感)

- layer_name: "model\\.0"

precision_mode: int16

- layer_name: "model\\.1"

precision_mode: int16

```

敏感层升int16后,精度从81.4%回升到82.7%,延迟从16ms涨到18ms,帧率从62.5fps降到55.6fps。

五、性能对比

配置 参数量 延迟 FPS mAP 相比原始

YOLOv5x 浮点 86M 38ms 26.3 84.2% 基准

YOLOv5x int8 PTQ 86M 24ms 41.7 81.5% 精度掉2.7%

YOLOv5x mix 86M 26ms 38.5 82.1% 精度掉2.1%

剪枝20% + int8 55M 14ms 71.4 80.2% 延迟好但精度不够

剪枝20% + per-channel 55M 15ms 66.7 81.4% 精度好一些

剪枝20% + per-channel + mix 55M 18ms 55.6 82.7% 最优

YOLOv5s 浮点 7.2M 12ms 83.3 77.2% 精度太低

结论: 剪枝20% + per-channel量化 + 敏感层int16,延迟18ms(55fps),mAP 82.7%,只比原始浮点掉1.5%。这是延迟和精度的最佳平衡点。

如果精度要求更严(比如必须83%+),可以:

· 剪枝比例降到10%(延迟22ms,mAP 83.5%)

· 或者更多的层用int16(延迟20ms,mAP 83.1%)

六、剪枝+量化联合优化的坑

坑1:剪枝后模型的channel数不是8/16的倍数

BPU要求输入channel是8的倍数,输出channel是16的倍数。剪枝的时候如果随便剪掉20%的filter,channel数可能变成不被8/16整除的数,导致BPU padding浪费。

解决: 剪枝的时候确保channel数是8的倍数。

```python

def prune_to_multiple(conv_layer, multiple=8, prune_ratio=0.2):

"""剪掉filter,但确保剩余的channel数是multiple的倍数"""

original_out = conv_layer.out_channels

target_out = int(original_out * (1 - prune_ratio))

# 确保是8的倍数

target_out = (target_out // multiple) * multiple

num_prune = original_out - target_out

# 计算importance,剪掉最不重要的num_prune个filter

importance = torch.sum(torch.abs(conv_layer.weight.data), dim=[1, 2, 3])

_, prune_indices = torch.topk(importance, num_prune, largest=False)

keep_mask = torch.ones(original_out, dtype=torch.bool)

keep_mask[prune_indices] = False

conv_layer.weight.data = conv_layer.weight.data[keep_mask]

if conv_layer.bias is not None:

conv_layer.bias.data = conv_layer.bias.data[keep_mask]

conv_layer.out_channels = target_out

return prune_indices

```

坑2:剪枝后BN层的running_mean和running_var没有同步更新

剪枝filter的时候,如果BN层的running_mean和running_var没有同步剪掉对应的channel,后续inference时BN的计算会出错。

解决: 剪枝Conv的同时剪掉BN层对应的channel。

```python

def prune_conv_bn(conv_layer, bn_layer, prune_ratio=0.2):

prune_indices = prune_conv_layer(conv_layer, prune_ratio)

# 同步剪掉BN层

bn_layer.weight.data = bn_layer.weight.data[~prune_indices]

bn_layer.bias.data = bn_layer.bias.data[~prune_indices]

bn_layer.running_mean = bn_layer.running_mean[~prune_indices]

bn_layer.running_var = bn_layer.running_var[~prune_indices]

bn_layer.num_features = bn_layer.num_features - len(prune_indices)

return prune_indices

```

坑3:shortcut连接的channel数不匹配

YOLOv5里有大量的shortcut连接(残差连接),如果剪枝的时候两边的channel数剪得不一样,shortcut相加时会shape mismatch。

解决:

剪枝的时候记录每一层剪掉了哪些channel,shortcut连接的另一边要剪掉相同的channel。

```python

# 记录每一层的剪枝信息

prune_info = {}

for name, module in model.named_modules():

if isinstance(module, nn.Conv2d):

prune_indices = prune_conv_layer(module, prune_ratio)

prune_info[name] = prune_indices

# 处理shortcut连接

for shortcut in model.shortcuts:

# shortcut的两端剪掉相同的channel

source_name = shortcut.source

target_name = shortcut.target

if source_name in prune_info:

# target端也要剪掉相同的channel

target_conv = get_module(model, target_name)

target_conv.weight.data = target_conv.weight.data[~prune_info[source_name]]

target_conv.in_channels = target_conv.in_channels - len(prune_info[source_name])

```

坑4:fine-tune学习率设太高

剪枝后的模型结构变了,fine-tune的时候如果学习率设得太高(比如0.01),模型会震荡,精度恢复很慢。

建议:

fine-tune的学习率设成原始训练的1/10(比如0.001),训练50-100个epoch。

七、注意事项总结

1. 征程6只支持结构化剪枝:非结构化剪枝的稀疏矩阵BPU不支持。

2. iterative pruning + fine-tune:一次剪太多精度会崩,逐步剪逐步恢复。

3. knowledge distillation加速恢复:用原始大模型当teacher,省50%训练时间。

4. 剪枝后channel数是8/16的倍数:满足BPU对齐要求,减少padding浪费。

5. per-channel量化比per-tensor精度高1-2%:剪枝后的模型更敏感,需要更精细的量化。

6. 敏感层升int16:检测头和backbone前几层用int16,精度回升明显。

7. BN层参数同步剪:running_mean/running_var要一起剪掉对应channel。

8. shortcut连接两边剪掉相同的channel:避免shape mismatch。

9. fine-tune学习率要低:0.001左右,训练50-100个epoch。

10. 剪枝比例建议10-20%:超过30%精度恢复困难,性价比不高。

算法工具链
社区征文征程6
评论0
0/600