experimental.py
models\experimental.py
目录
experimental.py
1.所需的库和模块
2.class Sum(nn.Module):
3.class MixConv2d(nn.Module):
4.class Ensemble(nn.ModuleList):
5.def attempt_load(weights, device=None, inplace=True, fuse=True):
1.所需的库和模块
import math
import numpy as np
import torch
import torch.nn as nn
from utils.downloads import attempt_download
2.class Sum(nn.Module):
# 这段代码定义了一个名为 Sum 的类,它是一个继承自 nn.Module 的 PyTorch 神经网络模块。这个模块的目的是将多个输入张量( x )相加,并且可以选择性地应用权重。
# 这个模块的设计灵感来源于论文 "EfficientDet: Scalable and Efficient Object Detection" 中提到的加权双向特征金字塔网络(Weighted Bi-directional Feature Pyramid Network, BiFPN)。
# 定义了一个继承自 nn.Module 的新类 Sum ,这是一个 PyTorch 神经网络模块的基类。
class Sum(nn.Module):
# Weighted sum of 2 or more layers https://arxiv.org/abs/1911.09070 2 层或更多层的加权和 https://arxiv.org/abs/1911.09070。
# Sum 类的构造函数,它接受两个参数。
# 1.n :输入的数量。
# 2.weight :一个布尔值,表示是否应用权重,默认为 False 。
def __init__(self, n, weight=False): # n: number of inputs
# 调用基类的构造函数,这是 Python 中继承机制的一部分,用于确保基类被正确初始化。
super().__init__()
# 将传入的 weight 参数保存为类的成员变量。
self.weight = weight # apply weights boolean
# 创建一个迭代器对象,用于在 forward 方法中迭代输入张量。
self.iter = range(n - 1) # iter object
# 如果 weight 参数为 True ,则执行以下操作。
if weight:
# 创建一个可学习的参数 w ,它是一个 PyTorch 参数( nn.Parameter ),这意味着它的值可以在训练过程中被优化。参数的初始值是通过 torch.arange(1.0, n) / 2 生成的,然后取负值。 requires_grad=True 表示需要计算这些权重的梯度。
self.w = nn.Parameter(-torch.arange(1.0, n) / 2, requires_grad=True) # layer weights
# 定义了 forward 方法,这是神经网络模块的前向传播方法,它接受输入张量 1.x 。
def forward(self, x):
# 初始化输出 y 为第一个输入张量 x[0] 。
y = x[0] # no weight
# 如果 weight 为 True ,则执行以下操作。
if self.weight:
# 权重参数 self.w 通过 sigmoid 函数激活,然后乘以 2,以确保权重在 0 到 2 之间。
w = torch.sigmoid(self.w) * 2
# 迭代剩余的输入张量,并将它们与对应的权重相乘后累加到 y 。
for i in self.iter:
y = y + x[i + 1] * w[i]
# 如果 weight 为 False ,则执行以下操作。
else:
# 迭代剩余的输入张量,并将它们直接累加到 y 。
for i in self.iter:
y = y + x[i + 1]
# 返回最终的输出 y 。
return y
# 这个模块可以用于将多个输入张量相加,并且可以选择是否对每个输入张量应用不同的权重。如果启用权重,权重将通过训练过程进行优化。
3.class MixConv2d(nn.Module):
# 这段代码定义了一个名为 MixConv2d 的类,它是一个继承自 nn.Module 的 PyTorch 神经网络模块。这个模块实现了混合深度卷积(Mixed Depth-wise Convolution),这是一种在单个卷积操作中混合使用不同核大小的技术。
# 定义了一个名为 MixConv2d 的新类,它继承自 PyTorch 的 nn.Module 类。
class MixConv2d(nn.Module):
# Mixed Depth-wise Conv https://arxiv.org/abs/1907.09595
# MixConv2d 类的构造函数,它接受以下参数。
# 1.c1 :输入通道数。
# 2.c2 :输出通道数。
# 3.k :一个元组,包含不同的核大小,默认为 (1, 3) 。
# 4.s :步长,默认为 1 。
# 5.equal_ch :一个布尔值,表示是否每个分组的通道数相等,默认为 True 。
def __init__(self, c1, c2, k=(1, 3), s=1, equal_ch=True): # ch_in, ch_out, kernel, stride, ch_strategy
# 调用基类的构造函数。
super().__init__()
# 获取核大小的数量。
n = len(k) # number of convolutions
# 如果 equal_ch 为 True ,则每个分组的通道数相等。
if equal_ch: # equal c_ per group
# 生成一个从 0 到 n-1 的等间隔序列,长度为 c2 。
i = torch.linspace(0, n - 1E-6, c2).floor() # c2 indices
# 计算每个分组的通道数。
c_ = [(i == g).sum() for g in range(n)] # intermediate channels
# 如果 equal_ch 为 False ,则使用线性方程组求解每个分组的通道数,使得权重数量相等。
else: # equal weight.numel() per group
# 创建一个列表 b ,其中包含 c2 (总的输出通道数)后面跟着 n 个零。这个列表将作为线性方程组的 b 向量。
b = [c2] + [0] * n
# 创建一个 n+1 行 n 列的单位矩阵 a ,然后通过 k=-1 参数将矩阵向下移动一个位置,这样可以得到一个除了第一列以外都是零的矩阵。
a = np.eye(n + 1, n, k=-1)
# 通过 np.roll 函数将矩阵 a 沿列方向向右移动一个位置,然后从原矩阵 a 中减去这个移动后的矩阵。这样做的结果是,除了第一列和最后一列以外,其他列的对角线上的元素会变成 -1 。
a -= np.roll(a, 1, axis=1)
# 将矩阵 a 中的每个元素乘以对应的核大小的平方( k 是一个包含核大小的元组)。
a *= np.array(k) ** 2
# 将矩阵 a 的第一行设置为全1,这是因为我们想要在第一组卷积中使用所有的输出通道。
a[0] = 1
# 使用 numpy 的 linalg.lstsq 函数解决线性方程组 ax = b 。 rcond=None 参数表示不对矩阵进行奇异值截断。函数返回的是方程组的解,我们取第一个返回值(即解向量),并使用 round 函数将解向量中的值四舍五入到最近的整数。这个解向量 c_ 表示每个卷积核大小对应的输出通道数。
c_ = np.linalg.lstsq(a, b, rcond=None)[0].round() # solve for equal weight indices, ax = b
# 通过这种方式,我们可以确保每个卷积核大小对应的权重数量大致相等,从而在不同的卷积核大小之间平衡模型的复杂度。这种方法在设计深度学习模型时非常有用,因为它可以帮助我们更有效地利用模型的参数。
# 创建一个模块列表,包含不同核大小的卷积层。
self.m = nn.ModuleList([
# groups=math.gcd(c1, int(c_)) :分组数,使用 math.gcd 函数计算输入通道数和输出通道数的最大公约数,这样可以确保每个分组的通道数是整数。
# bias=False :不使用偏置项,因为后面会使用批量归一化层,它会包含偏置项。
nn.Conv2d(c1, int(c_), k, s, k // 2, groups=math.gcd(c1, int(c_)), bias=False) for k, c_ in zip(k, c_)])
# 通过这行代码,为 MixConv2d 类创建了一个包含多个具有不同核大小的卷积层的 ModuleList 。在前向传播时,每个卷积层都会对输入特征图进行卷积操作,然后将结果拼接起来,形成最终的输出特征图。这种方法可以提高模型对不同尺度特征的捕捉能力,同时保持计算效率。
# 创建一个批量归一化层。
self.bn = nn.BatchNorm2d(c2)
# 创建一个激活层,SiLU(也称为Sigmoid-Weighted Linear Unit)。
self.act = nn.SiLU()
# 定义了前向传播的方法,它接受一个输入。
# 1.x :这是一个张量(tensor),代表输入的特征图(feature maps)。
def forward(self, x):
# [self.m] :这是一个包含多个卷积层的 ModuleList ,每个卷积层都有不同的核大小。
# for m in self.m :这是一个循环,遍历 ModuleList 中的每个卷积层。
# m(x) :对每个卷积层应用输入 x ,得到每个卷积层的输出。
# torch.cat([m(x) for m in self.m], 1) :使用 torch.cat 函数将所有卷积层的输出在通道维度(维度1)上拼接起来。这样,如果每个卷积层的输出通道数不同,它们仍然可以被合并成一个单一的张量。
# self.bn(...) :将拼接后的张量传递给批量归一化层( BatchNorm2d ),这有助于规范化数据,使其具有相同的均值和方差,从而提高模型的训练效率和性能。
# self.act(...) :最后,将归一化后的张量传递给激活函数层(在这里是 SiLU 即 Sigmoid-Weighted Linear Unit ),这为模型引入非线性,使其能够学习更复杂的特征。
return self.act(self.bn(torch.cat([m(x) for m in self.m], 1)))
# forward 方法将输入 x 通过不同核大小的卷积层,然后将这些卷积层的输出在通道维度上拼接,接着进行批量归一化和激活,最终输出的结果就是模型的前向传播结果。这种方法允许模型在单个操作中利用不同尺度的局部感受野,从而提高其对不同尺度特征的捕捉能力。
# 这个 MixConv2d 模块可以根据核大小的不同,将输入通道分割成不同的组,并在每个组上应用不同大小的卷积核。这种方法可以提高模型的表达能力,同时保持计算效率。
4.class Ensemble(nn.ModuleList):
# 这段代码定义了一个名为 Ensemble 的类,它继承自 PyTorch 的 nn.ModuleList 。这个类用于实现模型集成(ensemble),即将多个模型的预测结果结合起来以提高性能。
# 定义了一个名为 Ensemble 的新类,它继承自 PyTorch 的 nn.ModuleList 类。
class Ensemble(nn.ModuleList):
# Ensemble of models
# Ensemble 类的构造函数。
def __init__(self):
# 调用基类的构造函数,初始化 ModuleList 。
super().__init__()
# 定义了 forward 方法,这是神经网络模块的前向传播方法。它接受以下参数。
# 1.x :输入数据。
# 2.augment :一个布尔值,表示是否应用数据增强。
# 3.profile :一个布尔值,表示是否进行性能分析。
# 4.visualize :一个布尔值,表示是否进行可视化。
def forward(self, x, augment=False, profile=False, visualize=False):
# 列表推导式,它遍历 Ensemble 中的每个模块(即每个模型),将输入 x 和其他参数传递给每个模型的 forward 方法,并收集每个模型的输出。这里每个模型的 forward 方法返回一个元组,并且只取元组的第一个元素(即预测结果)。
y = [module(x, augment, profile, visualize)[0] for module in self]
# y = torch.stack(y).max(0)[0] # max ensemble
# y = torch.stack(y).mean(0) # mean ensemble
# 使用 torch.cat 函数将所有模型的输出在通道维度(维度1)上拼接起来。这种集成方法通常用于非最大抑制(NMS)集成,其中不同模型的预测结果被合并,然后应用 NMS 来去除重叠的检测框。
y = torch.cat(y, 1) # nms ensemble
# 返回拼接后的输出 y 和一个 None 值,表示没有额外的输出。在 PyTorch 中, None 通常用于表示没有额外的输出或信息。
return y, None # inference, train output
# 这个 Ensemble 类提供了一种简单的方式来集成多个模型的预测结果。通过这种方式,可以提高模型的鲁棒性和准确性,特别是在目标检测等任务中。集成方法可以根据具体任务的需求进行调整,例如使用最大值集成( max ensemble )或平均值集成( mean ensemble )。
5.def attempt_load(weights, device=None, inplace=True, fuse=True):
# 这段代码定义了一个名为 attempt_load 的函数,它用于加载一个或多个预训练的模型权重,并创建一个模型集合( Ensemble )。这个函数处理了模型权重的加载、兼容性更新和模型融合。
# 定义了 attempt_load 函数,它接受以下参数。
# 1.weights :模型权重的路径或路径列表。
# 2.device :模型运行的设备,默认为 None 。
# 3.inplace :是否在原地修改模型参数,默认为 True 。
# 4.fuse :是否融合模型中的某些层以提高效率,默认为 True 。
def attempt_load(weights, device=None, inplace=True, fuse=True):
# Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a
# 从 models.yolo 模块导入 Detect 和 Model 类。
from models.yolo import Detect, Model
# 这段代码是 attempt_load 函数中的一部分,它负责加载一个或多个预训练模型的权重,并对每个模型进行兼容性更新,最后将它们添加到一个 Ensemble 实例中。
# 创建一个 Ensemble 实例,这个实例将用来存储多个模型。
model = Ensemble()
# 遍历 weights 参数。如果 weights 是一个列表,直接遍历它;如果不是列表,将其转换为一个只包含该权重的列表,这样就可以统一处理单个权重和权重列表的情况。
for w in weights if isinstance(weights, list) else [weights]:
# 对于每个权重文件,首先尝试下载(如果文件不存在),然后使用 torch.load 加载权重文件。 map_location='cpu' 确保权重文件被加载到 CPU 上。
# def attempt_download(file, repo='ultralytics/yolov5', release='v7.0'): -> 尝试从 GitHub 仓库的发布资产中下载文件,如果本地找不到该文件。返回处理后的文件路径。返回文件的路径,以字符串形式。 -> return file / return str(file)
ckpt = torch.load(attempt_download(w), map_location='cpu') # load
# 从加载的权重中,尝试获取 ema (指数移动平均)键对应的模型状态,如果没有,则获取 model 键对应的状态。然后将模型状态转移到指定的 device 上,并转换为浮点数(FP32)。
ckpt = (ckpt.get('ema') or ckpt['model']).to(device).float() # FP32 model
# Model compatibility updates
# 检查加载的模型是否有 stride 属性,如果没有,则为其设置一个默认值。
if not hasattr(ckpt, 'stride'):
ckpt.stride = torch.tensor([32.])
# 检查模型是否有 names 属性,并且该属性是否是列表或元组。如果是,将其转换为字典,其中索引作为键,原始列表或元组中的元素作为值。
if hasattr(ckpt, 'names') and isinstance(ckpt.names, (list, tuple)):
ckpt.names = dict(enumerate(ckpt.names)) # convert to dict
# 如果 fuse 参数为 True 且模型有 fuse 方法,则调用该方法来融合模型中的某些层,然后设置模型为评估模式( eval )。如果没有 fuse 方法或 fuse 参数为 False ,则直接将模型设置为评估模式。最后,将模型添加到 Ensemble 实例中。
model.append(ckpt.fuse().eval() if fuse and hasattr(ckpt, 'fuse') else ckpt.eval()) # model in eval mode
# 通过这段代码,可以将多个预训练模型的权重加载并集成到一个 Ensemble 实例中,每个模型都被设置为评估模式,并且进行了必要的兼容性更新。这样的集成模型可以用于提高预测的准确性和鲁棒性。
# 这段代码是 attempt_load 函数的另一部分,它负责对加载的模型进行模块兼容性更新,并在模型集合中只有一个模型时返回该单个模型。
# Module compatibility updates
# 模块兼容性更新。
# 遍历 Ensemble 实例 model 中的所有模块。 modules() 方法返回模型中所有模块的迭代器。
for m in model.modules():
# 获取当前模块 m 的类型。
t = type(m)
# 检查模块类型是否为特定的激活函数类或自定义的 Detect 和 Model 类。如果是,执行以下操作。
if t in (nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU, Detect, Model):
# 设置模块的 inplace 属性。这是为了兼容 PyTorch 1.7.0 版本,因为在新版本中,某些操作默认是原地(in-place)执行的,这可以通过设置 inplace=True 来启用。
m.inplace = inplace # torch 1.7.0 compatibility
# if t is Detect and not isinstance(m.anchor_grid, list):
# delattr(m, 'anchor_grid')
# setattr(m, 'anchor_grid', [torch.zeros(1)] * m.nl)
# 检查模块类型是否为 nn.Upsample 并且没有 recompute_scale_factor 属性。如果是,执行以下操作。
elif t is nn.Upsample and not hasattr(m, 'recompute_scale_factor'):
# 为模块添加 recompute_scale_factor 属性并设置为 None 。这是为了兼容 PyTorch 1.11.0 版本,因为在新版本中, Upsample 模块的行为有所变化。
# recompute_scale_factor 属性在 PyTorch 的 nn.Upsample 模块中用于控制插值过程中缩放因子( scale_factor )的计算方式。具体来说,这个属性影响如何根据输入和输出张量的大小来计算插值时使用的缩放因子。
m.recompute_scale_factor = None # torch 1.11.0 compatibility
# Return model
# 返回模型。
# 检查 Ensemble 实例 model 中是否只有一个模型。
if len(model) == 1:
# 如果只有一个模型,直接返回该模型。 model[-1] 表示 Ensemble 实例中的最后一个模型,也就是唯一的模型。
return model[-1]
# 这段代码确保了加载的模型与当前 PyTorch 版本的兼容性,并且提供了一种简洁的方式来处理只包含单个模型的模型集合。通过这种方式, attempt_load 函数可以灵活地处理单个模型和模型集合,同时确保模型在不同版本的 PyTorch 中都能正常工作。
# 这段代码是 attempt_load 函数的最后一部分,它负责完成模型集合( Ensemble )的设置,并返回这个集合。
# Return detection ensemble
# 打印一条消息,表明已经使用提供的权重创建了一个模型集合,并显示这些权重。
print(f'Ensemble created with {weights}\n') # 使用 {weights} 创建的集成。
# 遍历一个包含属性名称的元组。
for k in 'names', 'nc', 'yaml':
# 对于每个属性名称,使用 setattr 函数将 Ensemble 实例的对应属性设置为第一个模型( model[0] )的同名属性的值。这样做是为了确保模型集合具有与单个模型相同的属性,例如类别名称( names )、类别数量( nc )和模型配置( yaml )。
# setattr(model, k, getattr(model[0], k)) :对于每个属性名称 k ,这行代码执行以下操作 :
# getattr(model[0], k) :获取模型集合中第一个模型( model[0] )的属性 k 的值。
# setattr(model, k, ...) :将这个值设置为整个模型集合对象( model )的属性 k 。
setattr(model, k, getattr(model[0], k))
# 计算模型集合中所有模型的最大步长( stride ),并将其设置为模型集合的 stride 属性。 torch.argmax 找到最大步长值的索引,然后通过索引访问对应的模型,并获取其 stride 属性。
model.stride = model[torch.argmax(torch.tensor([m.stride.max() for m in model])).int()].stride # max stride
# 断言检查模型集合中的所有模型是否具有相同数量的类别( nc )。如果不一致,则抛出异常,并显示每个模型的类别数量。
assert all(model[0].nc == m.nc for m in model), f'Models have different class counts: {[m.nc for m in model]}' # 模型有不同的类别数:{[m.nc for m in model]。
# 返回最终构建的模型集合( Ensemble )。
return model
# 这段代码确保了模型集合具有一致的属性,并且所有模型都具有相同数量的类别。通过设置最大步长,它还确保了模型集合可以正确地处理不同分辨率的输入。最后,它返回构建好的模型集合,这个集合可以用于推理或进一步的训练。
# 这个函数提供了一个灵活的方式来加载和集成多个模型,同时处理了模型的兼容性问题。通过这种方式,可以轻松地将多个预训练模型组合起来,以提高模型的性能和鲁棒性。
# 模型集合( Ensemble )中包含的每个模型都使用各自的 yaml 属性。当你创建一个模型集合并将多个模型添加到其中时,每个模型都保留自己的配置信息,包括 yaml 属性。
# 在 attempt_load 函数中,代码段:
# for k in 'names', 'nc', 'yaml':
# setattr(model, k, getattr(model[0], k))
# 将模型集合中第一个模型的 yaml 属性复制到 模型集合对象 上,使得你可以通过模型集合对象访问这个属性。但这并不意味着集合中的其他模型的 yaml 属性被修改或覆盖。每个模型仍然保持自己的独立 yaml 配置。
# 如果你需要访问集合中某个特定模型的 yaml 属性,你应该直接访问那个模型实例的属性。例如:
# yaml_config_for_model = model[0].yaml # 获取模型集合中第一个模型的 yaml 配置
# 在这里, model[0] 表示模型集合中的第一个模型,你可以通过索引来访问集合中的其他模型,并获取它们的 yaml 属性。每个模型的 yaml 属性都是独立的,反映了它们各自的配置信息。