序言:
我在用yolov11训练模型的时候发现一个情况,比如开始运行backbone,输入的参数为- [-1, 1, Conv, [64, 3, 2]],那由此得输入的参数为[64, 3, 2],则输出通道数应该为64,但很多情况并不是这样。经过循环debug我发现原来如下的参数控制着输出通道数。
参数部分
scales: # model compound scaling constants, i.e. 'model=yolo11n.yaml' will call yolo11.yaml with scale 'n'
# [depth, width, max_channels]
n: [0.50, 0.25, 1024] # summary: 319 layers, 2624080 parameters, 2624064 gradients, 6.6 GFLOPs
s: [0.50, 0.50, 1024] # summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPs
m: [0.50, 1.00, 512] # summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPs
l: [1.00, 1.00, 512] # summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPs
x: [1.00, 1.50, 512] # summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs
这就是yolov11.ymal文件中的scales参数。其中[深度,宽度,max_channels]:分别表示网络模型的深度因子、网络模型的宽度因子、最大通道数。
深度深度因子的作用:表示模型中重复模块的数量或层数的缩放比例。这里主要用来调整C3k2模块中的子模块Bottelneck重复次数。比如主干中第一个C3k2模块的个数系数是3,我们使用0.5x3并且向下取整就等于1了,这就代表第一个c3k2模块中bottelneck只重复一次;
宽度因子的作用:表示模型中通道数(即特征图的深度)的缩放比例,如果某个层原本有64个通道,而宽度设置为0.5,则该层的通道数变为32。比如使用yolov11n.yaml文件,参数为【0.50,0.25,1024】。第一个conv模块的输出通道数写的是64,但是实际上这个通道数并不是64,而是使用宽度因子0.25x64得到的最终结果16:同理,C3k2模块的输出通道虽然在yaml文件上写的是128,但是在实际使用时依然要乘上宽度因子0.25,那么第一个C3k2模块最终的到实际通道数就是0.25 x128=32。如下图所示,其他的依次类推。
max-channels: 表示每层最大通道数。每层的通道数会与这个参数进行一个对比,如果特征图通道数大于这个数,那就取 max channels的值。
主干部分
# YOLO11n backbone
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 2, C3k2, [256, False, 0.25]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2, [512, False, 0.25]]
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2, [512, True]]
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2, [1024, True]]
- [-1, 1, SPPF, [1024, 5]] # 9
- [-1, 2, C2PSA, [1024]] # 10
主干部分有四个参数[from,number,module,args] ,解释如下:
- from:这个参数代表从哪一层获得输入,-1就表示从上一层获得输入,[-1,6]就表示从上一层和第6层这两层获得输入。第一层比较特殊,这里第一层上一层没有输入,from默认-1就好了。
- number:这个参数表示模块重复的次数,如果为2则表示该模块重复3次,这里并不一定是这个模块的重复次数,块中的子模块重复的次数。对于C3k2模块来说,这个number就代表C3k2中Bottelneck或是C3k模块重复的次数。
- module:这个就代表你这层使用的模块的名称,比如你第一层使用了Conv模块,第三层使用了C3k2模块。
- args:表示这个模块需要传入的参数,第一个参数均表示该层的输出通道数。对于第一层conv参数【64,3,2】中64代表输出通道数,3代表卷积核大小k,2代表stride步长。每层输入通道数,默认是上一层的输出通道数。
- 其他说明:各层注释中的P1/2表示该层特征图缩放为输入图像尺寸的1/2,是第1特征层:P2/4表示该层特征图缩放为输入图像尺寸的1/4,是第2特征层;其他的依次类推。
头(head)部分
# YOLO11n head
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 6], 1, Concat, [1]] # cat backbone P4
- [-1, 2, C3k2, [512, False]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 4], 1, Concat, [1]] # cat backbone P3
- [-1, 2, C3k2, [256, False]] # 16 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 13], 1, Concat, [1]] # cat head P4
- [-1, 2, C3k2, [512, False]] # 19 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]]
- [[-1, 10], 1, Concat, [1]] # cat head P5
- [-1, 2, C3k2, [1024, True]] # 22 (P5/32-large)
- [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)
头部分有四个参数[from,number,module,args] ,解释如下:
- from:这个参数代表从哪一层获得输入,-1就表示从上一层获得输入,[-1,6]就表示从上一层和第6层这两层获得输入。第一层比较特殊,这里第一层上一层 没有输入,from默认-1就好了。
- number:这个参数表示模块重复的次数,如果为2则表示该模块重复3次,这里并不一定是这个模块的重复次数,也有可能是这个模块中的子模块重复的次数。对于C3k2模块来说,这个number就代表C3k2中Bottelneck或是C3k模块重复的次数。
- module:这个就代表你这层使用的模块的名称,比如你第一层使用了Conv模块,第三层使用了C3k2模块。
- args:表示这个模块需要传入的参数,第一个参数均表示该层的输出通道数。对于第一层conv参数【64,3,2】:64代表输出通道数,3代表卷积核大小k,2代表stride步长。每层输入通道数,默认是上一层的输出通道数。
- 这部分主要多出4个操作C2PSA、nn.Upsample、Concat、Detect,解释如下:
- C2PSA注意力机制:C2PSA块使用两个PSA(部分空间注意力)模块,它们在特征图的不同分支上操作,然后连接起来,类似于C2F块结构。这种设置确保模型专注于空间信息,同时保持计算成本和检测精度之间的平衡。C2PSA模块通过在提取的特征上应用空间注意力来细化模型选择性地关注感兴趣区域的能力。
- nn.upsample:表示上采样,将特征图大小进行翻倍操作。比如将大小为20X20的特征图,变为40X40的特征图大小。
- Concat:代表拼接操作,将相同大小的特征图,通道进行拼接,要求是特征图大小一致,通道数可以不相同。例如[-1,6]:-1代表上一层,6代表第六层(从第0层开始数),将上一层与第6层进行concat拼接操作。
- Detect的from有三个数: 15,18,21,这三个就是最终网络的输出特征图,分别对应P3,P4,P5。
1 类Conv
类Conv继承自nn.Module,所以应该是一个自定义的卷积层。首先看类的文档字符串,说是标准的卷积,带有一些参数,比如输入输出通道数、卷积核大小、步长、填充等等,还有分组卷积、膨胀率、激活函数。默认的激活函数是SiLU。SiLU(Sigmoid Linear Unit)是一个非线性激活函数,它结合了Sigmoid和线性变换的特点。那这个类应该是把卷积、BN层和激活函数封装在一起,形成一个模块,方便调用。接下来看初始化方法__init__。参数包括c1(输入通道数)、c2(输出通道数)、k(卷积核大小,默认为1)、s(步长,默认1)、p(填充,默认None)、g(groups,分组卷积的参数,默认1,即不分组)、d(dilation,膨胀率,默认1)、act(是否使用激活函数,默认为True)是否使用激活函数,如果是 True
则使用默认的激活函数 SiLU()
;如果传入自定义的激活函数(nn.Module
类型),则使用自定义激活函数;否则不使用激活函数,使用 nn.Identity()
,即不做任何操作。
1.1调用父类__init__()
super().__init__()调用父类的初始化方法。
self.conv
: 使用nn.Conv2d
定义了一个2D卷积层,接收c1
(输入通道数)、c2
(输出通道数)、k
(卷积核大小)、s
(步长)、autopad(k, p, d)
(根据卷积核大小、填充和扩张计算的填充)、groups=g
(分组卷积)以及d
(扩张因子)作为参数。self.bn
: 定义了一个BatchNorm2d
批归一化层,接收c2
作为输入通道数。self.act
: 激活函数。如果act
是True
,则使用默认的SiLU()
激活函数;如果act
是一个nn.Module
(例如 ReLU、SiLU 等),则使用该激活函数;如果act
是其他值,则不使用激活函数,使用nn.Identity()
。
class Conv(nn.Module):
"""Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""
default_act = nn.SiLU() # default activation
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
"""Initialize Conv layer with given arguments including activation."""
super().__init__()
self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
"""
self.conv = nn.Conv2d(
c1, # 输入通道数
c2, # 输出通道数
k, # 卷积核尺寸(如3表示3x3)
s, # 步长(stride)
autopad(k, p, d), # 自动计算padding(若未指定p)
groups=g, # 分组卷积参数(g=1为普通卷积)
dilation=d, # 膨胀系数(dilation)
bias=False # 禁用偏置(因为后续使用BN层)
"""
self.bn = nn.BatchNorm2d(c2)
self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()
1.2计算填充autopad函数
函数autopad
根据卷积核的大小、扩张因子和给定的填充值,自动计算合适的填充,以确保卷积操作后输出的张量尽可能保持与输入张量相同的尺寸。这通常用于实现“same”卷积,使得输入和输出的空间尺寸一致,或者接近一致,其最后返回的则是p(填充)系数。
k
: 卷积核的大小(可以是单个整数或列表)。p
: 填充大小。如果是None
,表示需要自动计算填充。d
: 扩张因子,默认为1。如果卷积使用扩张(dilation),则会影响输出的尺寸和填充。
def autopad(k, p=None, d=1): # kernel, padding, dilation
"""Pad to 'same' shape outputs."""
if d > 1:
k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
if p is None:
p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad
return p
如果 d
(扩张因子)大于1,则卷积核的实际大小会受到扩张的影响。扩张卷积会在卷积核元素之间插入空白,从而增大卷积核的“感受野”。对于单个整数 k
,计算扩张后的卷积核大小:d * (k - 1) + 1
。这里的 k - 1
是卷积核的大小减去1,因为扩张会使得卷积核的间距增大。如果 k
是一个列表(例如,二维卷积核的大小),则对每个维度的卷积核大小分别进行类似的扩张处理。
if d > 1:
k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
如果 p
为 None
,表示需要自动计算填充。在卷积中,为了保证输出的大小与输入的大小相同(或者尽量接近),填充的大小通常是卷积核的一半。
- 对于一维卷积(
k
是一个整数),自动填充大小为k // 2
,即卷积核的一半。 - 对于二维卷积(
k
是一个列表),则对每个维度分别计算填充大小,结果是每个维度的卷积核大小的一半。
if p is None:
p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad
1.3 向前传播过程方法
然后是forward方法,执行卷积、BN和激活函数。顺序是先卷积,然后BN,再激活函数。注意这里的顺序是conv -> bn -> act,这通常是标准的做法。返回的是self.act(self.bn(self.conv(x)))。
def forward(self, x):
"""Apply convolution, batch normalization and activation to input tensor."""
return self.act(self.bn(self.conv(x)))
还有一个forward_fuse方法,这个方法似乎跳过了BN层,直接执行卷积和激活函数。可能在某些情况下,比如模型融合或者导出时,不需要BN层,这时候可以用这个方法来前向传播。比如,在训练时使用forward,而在推理时将BN层合并到卷积中,提高速度,这时候forward_fuse。
def forward_fuse(self, x):
"""Apply convolution and activation without batch normalization."""
return self.act(self.conv(x))
1.4 卷积过程实例
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
参数项 | 值 | 说明 |
---|---|---|
from | -1 | 输入来源:取值为-1 表示使用前一层的输出作为输入 |
repeats | 1 | 重复次数:该模块(Conv )重复堆叠的次数(此处仅使用1次) |
module | Conv | 模块类型:使用自定义的Conv 类(包含卷积+BN+激活) |
args | [64, 3, 2] | 模块参数:传递给Conv 类的参数列表 |
每个卷积核对应一个输出通道,卷积核的数量直接决定输出通道数。
以提供的 Conv
层参数 [64, 3, 2]
为例:
-
输入通道数:继承自前一层的输出通道(假设输入是RGB图像,则为3通道)
-
输出通道数:由参数中的
64
指定 -
操作本质:
使用64个独立的3x3卷积核,每个核在所有输入通道上滑动计算,最终生成64个特征图。
分辨率变化公式:
以参数 k=3, s=2, p=1
(自动填充结果)为例:
-
输入尺寸:640x640
-
计算过程:
-
结果:输出分辨率缩小至
320x320
(即输入的1/2)
-
通道扩展
从3通道(RGB)到64通道,提取更丰富的语义特征(如边缘、纹理、颜色分布等) -
分辨率控制
通过步长=2快速下采样,实现:-
减少计算量(特征图尺寸缩小4倍)
-
扩大感受野(每个像素对应原始图像更大区域)
-
2.1 类Conv2
回顾父类Conv的结构。原来的Conv类有一个卷积层、BN层和激活函数,forward方法就是卷积后接BN和激活。而Conv2在初始化时,除了调用父类的初始化方法,还添加了一个1x1的卷积层cv2。这可能意味着Conv2是在并行使用两个卷积层,然后将它们的输出相加(3x3卷积 + 1x1卷积),推理时融合为单个3x3卷积。
class Conv2(Conv):
"""Simplified RepConv module with Conv fusing."""
def __init__(self, c1, c2, k=3, s=1, p=None, g=1, d=1, act=True):
"""Initialize Conv layer with given arguments including activation."""
super().__init__(c1, c2, k, s, p, g=g, d=d, act=act)
self.cv2 = nn.Conv2d(c1, c2, 1, s, autopad(1, p, d), groups=g, dilation=d, bias=False) # add 1x1 conv
def forward(self, x):
"""Apply convolution, batch normalization and activation to input tensor."""
return self.act(self.bn(self.conv(x) + self.cv2(x)))
def forward_fuse(self, x):
"""Apply fused convolution, batch normalization and activation to input tensor."""
return self.act(self.bn(self.conv(x)))
def fuse_convs(self):
"""Fuse parallel convolutions."""
w = torch.zeros_like(self.conv.weight.data)
i = [x // 2 for x in w.shape[2:]]
w[:, :, i[0] : i[0] + 1, i[1] : i[1] + 1] = self.cv2.weight.data.clone()
self.conv.weight.data += w
self.__delattr__("cv2")
self.forward = self.forward_fuse
2.1 调用父类__init__
()
-
功能:在父类
Conv
的3x3卷积基础上,新增并行1x1卷积分支。这个 1x1 卷积层的作用是处理通道之间的关系,通常用于降维或通道交互。 -
参数对齐:
groups
和dilation
参数与父类一致,确保分支结构兼容性
def __init__(self, c1, c2, k=3, s=1, p=None, g=1, d=1, act=True):
super().__init__(c1, c2, k, s, p, g=g, d=d, act=act) # 调用父类Conv初始化
self.cv2 = nn.Conv2d( # 新增1x1卷积分支
c1, c2, 1, s, # 1x1卷积核,步长s
autopad(1, p, d), # 自动填充(1x1卷积的padding通常为0)
groups=g, # 分组数同父类
dilation=d, # 膨胀系数同父类
bias=False # 与父类保持一致,禁用偏置
)
2.2 向前传播过程方法
forward方法是前向传播过程,它依次对输入 x
进行卷积、批归一化、激活函数的操作。重要的是,这里将两个卷积层的输出相加:self.conv(x)
(父类卷积的输出)和 self.cv2(x)
(新加入的 1x1 卷积的输出)。这意味着两个卷积层的输出是并行处理并合并的。最后,合并后的结果会经过批归一化和激活函数。
def forward(self, x):
return self.act(self.bn(self.conv(x) + self.cv2(x))) # 双分支结果相加后过BN和激活
forward_fuse
方法是优化版本的前向传播方法,它不再使用并行卷积(self.cv2(x)
),而是直接使用融合后的卷积(仅使用 self.conv(x)
)。这个方法一般在卷积融合之后使用。
def forward_fuse(self, x):
return self.act(self.bn(self.conv(x))) # 仅使用3x3卷积分支
fuse_convs
方法的作用是将两个卷积层(conv
和 cv2
)的权重融合为一个卷积层的权重。它通过将 cv2
(1x1 卷积)的权重添加到 conv
(原始卷积)的权重中,简化了模型结构。
def fuse_convs(self):
# 创建一个与3x3卷积核同形状的全零权重张量
w = torch.zeros_like(self.conv.weight.data)
# 计算1x1卷积核在3x3卷积核中的中心位置
i = [x // 2 for x in w.shape[2:]] # 对于3x3卷积核,i=[1,1]
# 将1x1卷积核的权重赋值到3x3卷积核的中心位置
w[:, :, i[0]:i[0]+1, i[1]:i[1]+1] = self.cv2.weight.data.clone()
# 融合权重:原3x3卷积核 + 中心点注入的1x1卷积核
self.conv.weight.data += w
# 删除1x1卷积分支(已融合到3x3卷积中)
self.__delattr__("cv2")
# 将前向传播方法切换为融合后的版本
self.forward = self.forward_fuse
执行顺序:
-
w = torch.zeros_like(self.conv.weight.data)
:首先创建一个与conv
权重相同形状的零张量w
。 -
i = [x // 2 for x in w.shape[2:]]
:计算i
,它是卷积权重的中心位置。这里假设权重的形状是四维的[out_channels, in_channels, height, width]
,并且i
是卷积核的中心位置。 -
w[:, :, i[0] : i[0] + 1, i[1] : i[1] + 1] = self.cv2.weight.data.clone()
:将cv2
卷积的权重复制到w
的中心位置。 -
self.conv.weight.data += w
:将cv2
的权重加到conv
的权重中。 -
self.__delattr__("cv2")
:删除cv2
卷积层,因为它已经被融合到conv
中,不再需要单独存在。 -
self.forward = self.forward_fuse
:将forward
方法指向forward_fuse
,这样前向传播时将只使用融合后的卷积层(即conv
)。
2.3 卷积过程实例
[-1, 1, Conv2, [64, 3, 2]]
-1
:输入来自前一层的输出;1
:该模块(Conv2
)重复1次;Conv2
:使用 Conv2
模块;[64, 3, 2]
:传递给 Conv2
的参数;64
:输出通道数;3
:卷积核尺寸(3x3);2
:步长(stride)
步骤1:输入假设
输入尺寸:[batch, 3, 640, 640]
(假设输入是3通道的RGB图像,分辨率为640x640)
参数解析:c1=3
:输入通道数为3(RGB图像);c2=64
:输出通道数为64;k=3
:卷积核尺寸为3x3;s=2
:步长为2(实现下采样);p=None
:自动填充(由autopad
计算);g=1
:普通卷积(非分组卷积);d=1
:无膨胀(dilation=1);act=True
:使用默认激活函数(SiLU)
步骤2:计算自动填充
膨胀系数 d=1
,实际卷积核尺寸仍为 3x3。
自动填充公式:p = k // 2 = 3 // 2 = 1。结果p=1,则自动填充值为1。
步骤3:初始化卷积层
# 父类Conv的3x3卷积
self.conv = nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1, bias=False)
# 新增1x1卷积分支
self.cv2 = nn.Conv2d(3, 64, kernel_size=1, stride=2, padding=0, bias=False)
参数解析:两个分支的输出通道数均为 64
;步长均为 2
(实现下采样);填充分别为 1
(3x3卷积)和 0
(1x1卷积)
步骤4: 前向传播(训练阶段)
def forward(self, x):
return self.act(self.bn(self.conv(x) + self.cv2(x)))
输入尺寸:x
的形状为 [batch, 3, 640, 640]
计算过程:
1. 3x3卷积 :
输入:[batch, 3, 640, 640]
输出:[batch, 64, 320, 320]
(计算公式:(640 + 2*1 - 3) // 2 + 1 = 320
)
2. 1x1卷积 :
输入:[batch, 3, 640, 640]
输出:[batch, 64, 320, 320]
(计算公式:(640 + 2*0 - 3) // 2 + 1 = 320
)
3. 逐元素相加:
将两个分支的输出相加,结果仍为 [batch, 64, 320, 320]
4. 批量归一化 (BN):
对相加结果进行归一化,输出形状不变
5. 激活函数 (SiLU):
对归一化结果应用SiLU激活,输出形状不变
-
输出:
[batch, 64, 320, 320]
步骤5: 前向传播(推理阶段)
def forward_fuse(self, x):
return self.act(self.bn(self.conv(x)))
输入尺寸:x
的形状为 [batch, 3, 640, 640]
计算过程:
1. 3x3卷积 :
输入:[batch, 3, 640, 640]
输出:[batch, 64, 320, 320]
(计算公式:(640 + 2*1 - 3) // 2 + 1 = 320
)
2. 批量归一化 (BN):
对相加结果进行归一化,输出形状不变
3. 激活函数 (SiLU):
对归一化结果应用SiLU激活,输出形状不变
-
输出:
[batch, 64, 320, 320]
步骤6: 卷积核融合(fuse_convs
)
def fuse_convs(self):
"""Fuse parallel convolutions."""
w = torch.zeros_like(self.conv.weight.data)
i = [x // 2 for x in w.shape[2:]]
w[:, :, i[0] : i[0] + 1, i[1] : i[1] + 1] = self.cv2.weight.data.clone()
self.conv.weight.data += w
self.__delattr__("cv2")
self.forward = self.forward_fuse
1. 在训练阶段,Conv2
模块的输出是 3x3卷积 和 1x1卷积 的结果相加:
2. 在推理阶段,我们希望将这两个卷积操作合并为一个等效的 3x3卷积,即:
3. 将 1x1卷积核 的权重赋值到 3x3卷积核 的中心位置,假设1x1卷积核的权重为 [[j]],
融合后的3x3卷积核权重为:
[[[a, b, c],
[d, e+j, f],
[g, h, i]]]
4. 将注入1x1权重的全零张量 w
加到原始的3x3卷积核上。原始的3x3卷积核的中心点权重被1x1卷积核的权重替换。
5. 删除 cv2
属性(即1x1卷积分支),在推理阶段不再需要1x1卷积,减少内存占用。
6. 将前向传播方法切换到 forward_fuse
,即仅使用融合后的3x3卷积,在推理阶段直接使用融合后的卷积核,避免额外的计算。
7. 融合后的3x3卷积核在数学上等价于原始的双分支卷积操作: