第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的结构,我画了一张结构图。这张图展示了数据从输入到输出的完整流向:

YOLOv11n 模型结构概览 输入 (3×640×640) Backbone(主干网络) Conv (3→16) Conv (16→32) C2f (32→32) Conv (32→64) ...... 更多 C2f + Conv 层 ...... Neck(颈部) SPPF (512→512) Upsample + Concat C2f (512→256) Head(检测头) Detect (小尺度) Detect (中尺度) Detect (大尺度) 说明:C2f 是 YOLOv11 的核心模块,包含多个 Bottleneck 结构,参数量最大

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模块下手,效果最明显。

好了,模型加载和结构分析就到这里。你现在应该能回答这几个问题了:模型有多少层?每层是什么类型?参数分布在哪?这些信息是后续剪枝的基础,一定要做到心中有数。