概述
该文件类似于施工队的特殊工具与拓展功能,这些工具主要用于优化和拓展建筑的特定部分,从而提高建设的效率
主要模块
- CrossConv 类(交叉卷积模块)
- 功能:实现高效的特征提取和下采样,支持快捷连接
- 建筑中的复合构件,增强结构的稳定性和承重能力
- Sum 类(加权和模块)
- 功能:对多个层的输出进行加权和,支持特征融合
- 建筑中的多层加固措施,提升整体结构的稳固性
- MixConv2d 类(混合深度卷积模块)
- 功能:通过不同大小的卷积核捕捉多尺度特征,提高特征表达能力
- 建筑中的多功能窗户,适应不同的光照和通风需求
- Ensemble 类(模型集成模块)
- 功能:集成多个模型的输出,提升检测性能和鲁棒性。
- 建筑中的多监控系统集成,提供全面和可靠的监控数据
- attempt_load 函数(模型加载函数)
- 功能:加载和集成模型权重,处理层融合和兼容性
- 施工队从仓库获取并处理建筑材料,确保施工材料的准备和适配
主要模块
CrossConv 类(交叉卷积模块)
该类主要实现了一个交叉卷积结构,通过先进行横向卷积,然后再进行纵向圈进,从而提高特征提取和下采样的效果,与此同时还支持快捷连接类似于残差连接,以此来提高信息流通和梯度传播
理解该模块的主要作用
横向卷积后纵向卷积,这个过程就像在制作一幅拼图,拼图中有许多小块。为了加速拼图的过程,你采用了一个高效的方式:首先,你将所有横向的拼图块(比如从左到右的块)先放好,然后再垂直地(从上到下)修补拼图。这样做是为了更高效地将拼图组合起来。你没有直接按纵向或横向一个方向做,而是结合了两个方向的拼图方式,这样可以更快地找到正确的块并减少空白区域
- 横向拼图(1xk 卷积):就像是你先铺好横向的拼图块,抓住了图像的某些基本特征
- 纵向拼图(kx1 卷积):然后你垂直地将拼图块修补好,这样能进一步细化和加强拼图的完整性,确保没有空隙
那么如果基于上述拼图的例子,快捷连接则就类似于拼图中加入了一些标记,这些标记的作用就是帮助在拼图过程中快速找到之前位置好的部分。这样的话,即使你不完全依赖拼图的每一个细节,你也能通过标记快速完成拼图任务
所以快捷连接的主要作用即使如果输入和输出的拼图块大小一致,就直接跳过某些步骤,快速将已有的拼图块与新拼图块结合起来,节省时间和精力
class CrossConv(nn.Module):
"""
CrossConv: 交叉卷积模块,主要用于特征提取和通道调整,支持快捷连接。
实现了两个卷积操作,分别使用不同形状的卷积核 `(1, k)` 和 `(k, 1)`,从而实现跨通道和空间方向的特征融合。
参数:
c1: 输入通道数
c2: 输出通道数
k: 卷积核大小 (默认值为 3)
s: 步幅 (stride, 默认值为 1)
g: 组卷积的分组数 (默认值为 1, 表示普通卷积)
e: 扩展因子,用于计算隐藏通道数 (默认值为 1.0)
shortcut: 是否启用快捷连接 (默认值为 False)
"""
def __init__(self, c1, c2, k=3, s=1, g=1, e=1.0, shortcut=False):
super().__init__()
# 计算隐藏通道数 c_,将输出通道数 c2 乘以扩展因子 e 进行缩放
c_ = int(c2 * e) # 隐藏通道数,用于中间的卷积操作
# 定义第一层卷积:使用卷积核大小为 (1, k),步幅为 (1, s)
# 将输入通道数 c1 转换为隐藏通道数 c_
self.cv1 = Conv(c1, c_, (1, k), (1, s))
# 定义第二层卷积:使用卷积核大小为 (k, 1),步幅为 (s, 1)
# 将隐藏通道数 c_ 转换为输出通道数 c2,支持组卷积(通过参数 g 控制分组数量)
self.cv2 = Conv(c_, c2, (k, 1), (s, 1), g=g)
# 判断是否使用快捷连接 (shortcut)
# 条件为 shortcut=True 且输入通道数 c1 等于输出通道数 c2
# 如果满足条件,则启用快捷连接;否则不使用
self.add = shortcut and c1 == c2
def forward(self, x):
"""
前向传播函数:
输入张量 x 经过两次卷积操作 (cv1 和 cv2) 生成输出。
如果启用快捷连接 (add=True),则将输入 x 与卷积结果相加。
否则,仅返回卷积结果。
参数:
x: 输入张量,形状为 (batch_size, c1, height, width)
返回:
输出张量,形状为 (batch_size, c2, new_height, new_width)
"""
# 判断是否使用快捷连接
if self.add:
# 启用快捷连接,将输入 x 和卷积结果相加
return x + self.cv2(self.cv1(x))
else:
# 不使用快捷连接,仅返回卷积结果
return self.cv2(self.cv1(x))
Sum 类(加权和模块)
主要功能是对多个输入层的输出进行加权和,支持是否应用权重的选项,这样有助于模型中融合来自不同的特征,从而提升模型的表达能力
分析:是否加入权重对其的影响
假设在一个团队项目中,有几个成员负责不同的任务。你希望根据每个成员的工作成果来决定他们在团队项目中贡献的权重
- 平均分配工作量(没有权重)
- 根据贡献的不同,对不同成员的工作量加权
该类主要应用哪个流程?
主要应用于预测头中,也就是多尺度输出的融合上,当输出三张不同特征图(其中分别捕捉小物体、中等物体、大物体)最后将这三张特征图进行融合
如果使用了加权和方式(也就是weight = True),则每个尺度的输出(每个层的特征图)会按照一定的权重进行加权,最后得出一个最终的检测结果。这种操作可以帮助模型更好地结合不同层次的信息,提高检测的准确性
class Sum(nn.Module):
"""
Sum 模块:
实现对多个输入层的加权求和或直接求和操作。适用于特征融合的场景,
可以选择是否为每个输入层分配权重。
参数:
n: 输入的层数。
weight: 布尔值,是否对输入层应用权重。
"""
def __init__(self, n, weight=False):
"""
初始化 Sum 模块。
参数:
n: 输入层的数量。
weight: 布尔值,表示是否对输入层加权求和。
"""
super().__init__()
self.weight = weight # 标志是否使用权重
self.iter = range(n - 1) # 迭代范围,用于遍历第 2 层到第 n 层的输入
if weight:
# 如果启用权重,则初始化权重参数
# 权重参数初始化为 -0.5, -1.0, ..., -(n-1)/2,允许梯度更新
self.w = nn.Parameter(-torch.arange(1.0, n) / 2, requires_grad=True)
def forward(self, x):
"""
前向传播: 对输入层进行加权求和或直接求和。
参数:
x: 输入的特征图列表,长度为 n。
每个元素为一个张量,形状为 (batch_size, channels, height, width)。
返回:
y: 输出特征图,形状与输入特征图一致。
"""
# 初始化输出为第一个输入层的特征图
y = x[0]
if self.weight: # 如果启用权重
# 使用 Sigmoid 激活函数对权重进行限制,并将权重范围缩放到 [0, 2]
w = torch.sigmoid(self.w) * 2
# 遍历后续层,将其按权重与初始输出进行加权求和
for i in self.iter:
y = y + x[i + 1] * w[i] # 对第 i + 1 个层的特征图进行加权后累加
else: # 如果未启用权重
# 遍历后续层,直接进行逐层累加
for i in self.iter:
y = y + x[i + 1]
# 返回融合后的特征图
return y
MixConv2d 类(混合深度卷积模块)
这个类主要实现混合深层卷积,通过使用不同大小的卷积核(如 1x1 和 3x3)并将它们的输出拼接在一起,以捕捉多尺度特征。借助该方式就可以捕获更多的图片细节
主要作用于特征采样阶段以及多尺度特征图融合阶段
实现流程总结
- 输入张量依次通过多个卷积层
- 卷积层的输出按通道维度拼接
- 拼接结果通过批归一化和激活函数,得到最终输出
class MixConv2d(nn.Module):
"""
MixConv2d 模块:
实现混合深度卷积 (Mixed Depthwise Convolution),
即同时使用多种大小的卷积核来进行特征提取,从而提高模型的特征表达能力。
参考论文: https://arxiv.org/abs/1907.09595
参数:
c1: 输入通道数。
c2: 输出通道数。
k: 卷积核大小的列表,例如 (1, 3, 5)。
s: 步幅 (stride),默认为 1。
equal_ch: 是否使每个卷积的通道数相等。如果为 False,则按卷积核大小调整通道分配。
"""
def __init__(self, c1, c2, k=(1, 3), s=1, equal_ch=True):
"""
初始化 MixConv2d 模块。
参数:
c1: 输入通道数。
c2: 输出通道数。
k: 卷积核大小的列表,例如 (1, 3, 5)。
s: 步幅 (stride),默认为 1。
equal_ch: 是否使每个卷积的通道数相等。默认值为 True。
"""
super().__init__()
n = len(k) # 获取卷积核的数量 (例如 k=(1, 3, 5) 时,n=3)
if equal_ch:
# 如果 equal_ch 为 True,确保每个卷积层分配的输出通道数相等
# 使用 torch.linspace 分配输出通道到不同卷积核组
i = torch.linspace(0, n - 1E-6, c2).floor() # 计算每个输出通道的索引
# 统计每个卷积核组的通道数
c_ = [(i == g).sum() for g in range(n)] # 每个卷积核组对应的通道数
else:
# 如果 equal_ch 为 False,按卷积核面积的平方比例分配通道数,使权重数量相等
b = [c2] + [0] * n # 设置右边的通道约束
a = np.eye(n + 1, n, k=-1) # 构造卷积核面积约束矩阵
a -= np.roll(a, 1, axis=1) # 计算差分,调整矩阵
a *= np.array(k) ** 2 # 调整矩阵以反映卷积核的大小
a[0] = 1 # 第一行为 1,表示约束总通道数
# 解线性方程组,计算每个卷积核的通道数,返回整数
c_ = np.linalg.lstsq(a, b, rcond=None)[0].round()
# 定义多个卷积层
# 每个卷积层的卷积核大小、通道数、步幅等由前面计算的参数决定
self.m = nn.ModuleList([
nn.Conv2d(c1, int(c_), k, s, k // 2, groups=math.gcd(c1, int(c_)), bias=False)
for k, c_ in zip(k, c_)
])
# 添加批归一化层和激活函数
self.bn = nn.BatchNorm2d(c2) # 批归一化
self.act = nn.SiLU() # SiLU 激活函数(也称为 Swish 激活函数)
def forward(self, x):
"""
前向传播:
1. 输入张量 x 依次通过所有卷积层。
2. 每个卷积层的输出按通道维度拼接。
3. 拼接结果通过批归一化层和激活函数。
参数:
x: 输入张量,形状为 (batch_size, c1, height, width)。
返回:
输出张量,形状为 (batch_size, c2, new_height, new_width)。
"""
# 依次通过每个卷积层,将所有输出按通道维度拼接
x = torch.cat([m(x) for m in self.m], 1) # 拼接多个卷积层的输出
# 将拼接结果通过批归一化和激活函数
return self.act(self.bn(x))
Ensemble 类(模型集成模块)
该类的主要作用即是通过将多个模型的输出进行组合(如拼接、取最大值或取平均值),以提升整体的检测性能和鲁棒性
这个就类似于停车场中的多个监控系统的集成,通过组合多个监控摄像头的画面和数据,从而提供更全面和更可靠的建筑监控,确保其安全性和功能性
主要过程分析
- 遍历所有模型,获取每个模型的输出
- 使用集成策略对输出进行融合
- 返回集成后的预测结果
class Ensemble(nn.ModuleList):
"""
Ensemble 模块:
实现模型集成,支持对多个模型的输出进行集成策略(如拼接、平均或最大值)。
适用于组合多个模型的预测结果,以提升整体性能。
继承自 `nn.ModuleList`,可将多个模型存储为列表,并对其进行统一的前向传播操作。
功能:
- 对输入数据 `x` 使用多个模型进行前向传播。
- 对每个模型的输出结果进行集成(默认拼接,也可改为平均或最大值策略)。
初始化:
- 初始化为空的 `nn.ModuleList`,后续可以通过 `append` 添加模型。
"""
def __init__(self):
"""
初始化 Ensemble 模块:
继承 `nn.ModuleList`,无需额外参数。
"""
super().__init__()
def forward(self, x, augment=False, profile=False, visualize=False):
"""
前向传播: 对集成的多个模型依次进行推理,并对结果进行集成。
参数:
x: 输入张量,形状根据模型需求决定,通常为 (batch_size, channels, height, width)。
augment: 是否使用增强推理(默认 False)。
profile: 是否打印性能分析信息(默认 False)。
visualize: 是否可视化特征(默认 False)。
返回:
y: 集成后的结果张量,形状根据集成策略决定。
None: 返回的第二个值为 None,占位符。
"""
y = [] # 存储每个模型的输出
# 遍历 Ensemble 中的每个模型,进行前向传播
for module in self:
# 调用每个模型的 forward 方法,将输入 `x` 传递给模型
# 获取每个模型的第一个输出结果(通常是预测结果),并添加到列表 `y`
y.append(module(x, augment, profile, visualize)[0])
# **集成策略部分**:
# 以下三种方法可用于不同的集成策略,可以根据实际需求选择一种:
# 最大值集成: 每个位置取所有模型预测结果的最大值
# y = torch.stack(y).max(0)[0]
# 平均值集成: 对每个位置的预测结果取平均值
# y = torch.stack(y).mean(0)
# 拼接集成: 将每个模型的预测结果在通道维度上拼接
y = torch.cat(y, 1)
# 返回集成后的结果和占位符 None(用于兼容推理的返回值结构)
return y, None
attempt_load 函数(模型加载函数)
该函数主要用于加载模型权重,支持单个模型权重文件或多个模型权重文件的集成,与此同时还处理模型的融合,并且确保模型兼容不同版本的 PyTorch
类似于施工队从仓库获取建筑材料,确保所有材料(模型权重)按需准备,并进行必要的处理(如预处理材料)以适应施工需求
def attempt_load(weights, map_location=None, inplace=True, fuse=True):
"""
尝试加载模型权重,支持加载单个模型或多个模型(集成模型)的权重。
参数:
weights: 单个权重文件路径或包含多个权重文件路径的列表。
map_location: 指定加载到的设备位置(如 'cpu', 'cuda')。
inplace: 是否使用原地操作,默认为 True,以节省内存(兼容 PyTorch 1.7.0+)。
fuse: 是否融合卷积层和批归一化层,默认为 True。
返回:
如果 `weights` 是单个权重文件路径,则返回加载的模型。
如果 `weights` 是包含多个权重文件路径的列表,则返回一个集成模型(Ensemble)。
"""
from models.yolo import Detect, Model # 导入 Detect 和 Model 模块
# 创建一个模型集成对象
model = Ensemble()
# 如果 weights 是列表,则逐个加载权重;如果是单个路径,则将其转换为列表处理
for w in weights if isinstance(weights, list) else [weights]:
# 下载并加载权重文件
ckpt = torch.load(attempt_download(w), map_location=map_location) # 下载并加载权重文件到指定设备
if fuse:
# 如果启用了 fuse(融合),将卷积层与批量归一化层融合,并设置模型为评估模式
model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().fuse().eval()) # FP32 模型
else:
# 如果未启用融合,仅加载模型权重,设置为评估模式
model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().eval())
# **兼容性更新**
# 遍历模型中的所有模块,确保它们的行为与当前 PyTorch 版本兼容
for m in model.modules():
# 如果模块类型是指定的激活函数或检测相关模块,则进行兼容性设置
if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU, Detect, Model]:
m.inplace = inplace # 设置 inplace 参数,用于兼容 PyTorch 1.7.0+
# 如果模块是 Detect,则检查并更新其 anchor_grid 属性
if type(m) is Detect:
if not isinstance(m.anchor_grid, list): # 如果 anchor_grid 不是列表类型
delattr(m, 'anchor_grid') # 删除旧的 anchor_grid 属性
setattr(m, 'anchor_grid', [torch.zeros(1)] * m.nl) # 设置新的 anchor_grid 为列表,大小为检测层数 (nl)
# 如果模块是 Conv 类型,则进行 PyTorch 1.6.0 的兼容性设置
elif type(m) is Conv:
m._non_persistent_buffers_set = set() # 兼容旧版本的 PyTorch
# **返回结果**
if len(model) == 1:
# 如果只有一个模型,直接返回该模型
return model[-1]
else:
# 如果是多个模型,打印集成模型的信息
print(f'Ensemble created with {weights}\n')
# 复制某些属性(如 'names')到集成模型
for k in ['names']:
setattr(model, k, getattr(model[-1], k)) # 使用最后一个模型的属性值
# 设置集成模型的步幅为所有模型中的最大步幅
model.stride = model[torch.argmax(torch.tensor([m.stride.max() for m in model])).int()].stride
# 返回集成模型
return model