第3章:模型加载——解剖YOLOv11的每一层
好,咱们进入实战环节。上一章聊了剪枝的理论基础,这一章得先把模型请出来,看看它到底长什么样。我个人习惯,拿到一个预训练模型,第一件事不是跑推理,而是先把它拆开,看看每一层有多少参数。
你想想看,剪枝说白了就是删参数。你连参数分布在哪都不知道,怎么剪?所以这一章的核心就三件事:加载权重、打印结构、统计参数。
3.1 加载YOLOv11预训练权重
YOLOv11官方提供了在COCO上训好的权重。我建议直接用ultralytics库来加载,省事。
from ultralytics import YOLO
# 加载官方预训练模型
model = YOLO('yolo11n.pt') # 或者 yolo11s.pt, yolo11m.pt 等
# 查看模型基本信息
print(type(model.model))
print(model.model) # 打印完整结构
嗯,这里要注意:model.model才是底层的torch.nn.Module对象。如果你直接print(model),看到的是Ultralytics封装后的接口,不是我们想要的网络结构。
我的习惯:加载完权重后,先跑一次前向传播,确认模型能正常推理。别等到剪完才发现权重没加载对。
import torch
# 构造一个虚拟输入
dummy_input = torch.randn(1, 3, 640, 640)
# 前向传播测试
with torch.no_grad():
output = model.model(dummy_input)
print(f"输出类型: {type(output)}")
if isinstance(output, (list, tuple)):
print(f"输出数量: {len(output)}")
for i, o in enumerate(output):
print(f" 输出[{i}] shape: {o.shape}")
YOLOv11的输出是一个列表,包含三个尺度的检测头输出。每个尺度对应不同大小的特征图,用来检测不同尺寸的目标。
3.2 解剖模型结构
加载完模型,咱们来拆开看看。YOLOv11的结构可以分成三大部分:Backbone(主干网络)、Neck(颈部)、Head(检测头)。
我写了个小脚本,可以逐层打印模型信息:
def print_model_structure(model, detail=False):
"""打印YOLOv11模型结构"""
for i, (name, module) in enumerate(model.model.named_modules()):
# 跳过最顶层模块,只打印子模块
if name == '':
continue
# 统计该模块的参数数量
params = sum(p.numel() for p in module.parameters())
if detail or params > 0:
print(f"[{i:3d}] {name:40s} | 参数: {params:>10,} | {module.__class__.__name__}")
# 打印所有含参数的模块
print_model_structure(model, detail=True)
运行后你会看到类似这样的输出(以yolo11n为例):
| 序号 | 模块名称 | 参数数量 | 模块类型 | 所属部分 |
|---|---|---|---|---|
| 0 | model.0 | 928 | Conv | Backbone |
| 1 | model.1 | 3,680 | Conv | Backbone |
| 2 | model.2 | 7,168 | C2f | Backbone |
| 3 | model.3 | 14,720 | Conv | Backbone |
| 4 | model.4 | 28,672 | C2f | Backbone |
| ... | ... | ... | ... | ... |
| 21 | model.21 | 0 | Detect | Head |
关键发现:YOLOv11n总参数量约260万,其中Backbone占了约60%,Neck约25%,Head约15%。剪枝的重点应该放在Backbone和Neck的卷积层上。
3.3 统计各层参数分布
光看逐层参数还不够,咱们得从宏观上把握参数分布。我习惯按模块类型做聚合统计:
from collections import defaultdict
def analyze_parameter_distribution(model):
"""分析模型参数分布"""
type_params = defaultdict(int)
type_counts = defaultdict(int)
for name, module in model.model.named_modules():
if name == '':
continue
params = sum(p.numel() for p in module.parameters())
if params > 0:
module_type = module.__class__.__name__
type_params[module_type] += params
type_counts[module_type] += 1
print(f"{'模块类型':20s} {'数量':>6s} {'参数量':>12s} {'占比':>8s}")
print("-" * 50)
total = sum(type_params.values())
for mtype in sorted(type_params.keys(), key=lambda k: type_params[k], reverse=True):
count = type_counts[mtype]
params = type_params[mtype]
ratio = params / total * 100
print(f"{mtype:20s} {count:>6d} {params:>12,} {ratio:>7.2f}%")
analyze_parameter_distribution(model)
输出结果大致如下:
| 模块类型 | 数量 | 参数量 | 占比 |
|---|---|---|---|
| C2f | 8 | 1,204,736 | 46.2% |
| Conv | 12 | 1,024,512 | 39.3% |
| SPPF | 1 | 147,456 | 5.7% |
| Upsample | 1 | 0 | 0.0% |
| Concat | 4 | 0 | 0.0% |
| Detect | 1 | 229,440 | 8.8% |
我曾经踩过的坑:一开始我直接对Detect模块的卷积层做剪枝,结果发现检测头对剪枝特别敏感,稍微剪一点精度就崩了。后来我学乖了——检测头尽量不动,或者只做极低比例的剪枝。
3.4 可视化模型结构
为了更直观地理解YOLOv11的结构,我画了一张结构图。这张图展示了数据从输入到输出的完整流向:
3.5 保存模型结构信息
为了方便后续剪枝时参考,我建议把模型结构保存下来:
import json
def save_model_info(model, save_path='model_info.json'):
"""保存模型结构信息到JSON文件"""
model_info = {
'total_params': sum(p.numel() for p in model.model.parameters()),
'trainable_params': sum(p.numel() for p in model.model.parameters() if p.requires_grad),
'layers': []
}
for i, (name, module) in enumerate(model.model.named_modules()):
if name == '':
continue
params = sum(p.numel() for p in module.parameters())
if params > 0:
layer_info = {
'index': i,
'name': name,
'type': module.__class__.__name__,
'params': params
}
model_info['layers'].append(layer_info)
with open(save_path, 'w') as f:
json.dump(model_info, f, indent=2)
print(f"模型信息已保存到 {save_path}")
print(f"总参数量: {model_info['total_params']:,}")
print(f"可训练参数量: {model_info['trainable_params']:,}")
save_model_info(model)
小技巧:保存的JSON文件后续可以用来做剪枝策略的参考。比如你可以根据每层的参数量排序,优先剪掉参数量大的层。
3.6 不同版本YOLOv11的参数对比
YOLOv11有多个版本,从n到x,参数量差异很大。我整理了一个对比表:
| 模型版本 | 总参数量 | Backbone参数 | Neck参数 | Head参数 | 模型大小(MB) |
|---|---|---|---|---|---|
| YOLOv11n | 2.6M | 1.5M (58%) | 0.7M (27%) | 0.4M (15%) | 5.4 |
| YOLOv11s | 9.4M | 5.5M (59%) | 2.5M (27%) | 1.4M (15%) | 18.5 |
| YOLOv11m | 20.1M | 11.8M (59%) | 5.4M (27%) | 2.9M (14%) | 39.5 |
| YOLOv11l | 25.3M | 14.9M (59%) | 6.8M (27%) | 3.6M (14%) | 49.7 |
| YOLOv11x | 56.9M | 33.6M (59%) | 15.3M (27%) | 8.0M (14%) | 111.5 |
核心结论:不管哪个版本,Backbone都占了近60%的参数。这意味着——剪枝的主战场在Backbone。我个人的经验是,先对Backbone的C2f模块下手,效果最明显。
好了,模型加载和结构分析就到这里。你现在应该能回答这几个问题了:模型有多少层?每层是什么类型?参数分布在哪?这些信息是后续剪枝的基础,一定要做到心中有数。