torch_utils.py
utils\torch_utils.py
目录
torch_utils.py
1.所需的库和模块
2.def smart_inference_mode(torch_1_9=check_version(torch.__version__, '1.9.0')):
3.def smartCrossEntropyLoss(label_smoothing=0.0):
4.def smart_DDP(model):
5.def reshape_classifier_output(model, n=1000):
6.def torch_distributed_zero_first(local_rank: int):
7.def device_count():
8.def select_device(device='', batch_size=0, newline=True):
9.def time_sync():
10.def profile(input, ops, n=10, device=None):
11.def is_parallel(model):
12.def de_parallel(model):
13.def initialize_weights(model):
14.def find_modules(model, mclass=nn.Conv2d):
15.def sparsity(model):
16.def prune(model, amount=0.3):
17.def fuse_conv_and_bn(conv, bn):
18.def model_info(model, verbose=False, imgsz=640):
19.def scale_img(img, ratio=1.0, same_shape=False, gs=32):
20.def copy_attr(a, b, include=(), exclude=()):
21.def smart_optimizer(model, name='Adam', lr=0.001, momentum=0.9, decay=1e-5):
22.def smart_hub_load(repo='ultralytics/yolov5', model='yolov5s', **kwargs):
23.def smart_resume(ckpt, optimizer, ema=None, weights='yolov5s.pt', epochs=300, resume=True):
24.class EarlyStopping:
25.class ModelEMA:
1.所需的库和模块
import math
import os
import platform
import subprocess
import time
import warnings
from contextlib import contextmanager
from copy import deepcopy
from pathlib import Pathimport torch
import torch.distributed as dist
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.parallel import DistributedDataParallel as DDPfrom utils.general import LOGGER, check_version, colorstr, file_date, git_describe
from utils.lion import Lion# 这段代码执行了几个操作,主要用于配置和环境设置,特别是在分布式训练和模型性能分析的上下文中。
# 从环境变量中获取名为 LOCAL_RANK 的值,并将其转换为整数。如果环境变量 LOCAL_RANK 不存在,则默认值为 -1 。 LOCAL_RANK 通常用于分布式训练中标识当前进程在本地机器上的排名。
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html
# 获取名为 RANK 的环境变量值,并将其转换为整数,用于标识当前进程在所有分布式进程中的全局排名。如果 RANK 环境变量不存在,则默认值为 -1 。
RANK = int(os.getenv('RANK', -1))
# 获取名为 WORLD_SIZE 的环境变量值,并将其转换为整数,表示分布式训练中的总进程数。如果 WORLD_SIZE 环境变量不存在,则默认值为 1 ,意味着非分布式设置。
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1))# 开始一个 try 块,用于尝试导入模块或执行代码。
try:# 尝试导入 thop 模块,这是一个用于计算模型的浮点运算次数(FLOPs)的库。import thop # for FLOPs computation
# 如果 thop 模块无法导入(例如,因为没有安装),则捕获 ImportError 。
except ImportError:# 如果 thop 模块无法导入,则将 thop 设置为 None 。thop = None# Suppress PyTorch warnings# warnings.filterwarnings(action, category=None, message='', module='', lineno=0)
# warnings.filterwarnings() 是 Python 标准库 warnings 中的一个函数,用于控制警告消息的显示。这个函数允许开发者根据警告的类别、消息内容或其他属性来过滤警告,决定哪些警告应该显示,哪些应该被忽略。
# 参数说明 :
# action :一个字符串,指定对警告执行的操作。常用的值包括 'ignore' (忽略警告)、 'default' (显示警告,除非被过滤)、 'always' (总是显示警告,即使已经被过滤)、 'module' (仅当警告来自模块顶层时显示)、 'once' (只显示一次警告)。
# category :一个警告类别,指定要过滤的警告类型。如果为 None ,则匹配所有类别。
# message :一个字符串或正则表达式,用于匹配警告消息的内容。如果为 None 或空字符串,则不基于消息内容过滤。
# module :一个字符串或正则表达式,用于匹配引发警告的模块名称。如果为 None ,则不基于模块名称过滤。
# lineno :一个整数,指定引发警告的代码行号。如果为 0 ,则不基于行号过滤。
# 返回值 :warnings.filterwarnings() 没有返回值,它直接修改 Python 的警告过滤列表。
# warnings.filterwarnings() 函数是一个强大的工具,用于控制 Python 程序运行时的警告信息。通过这个函数,开发者可以根据需要过滤掉不希望看到的警告,从而使得程序的输出更加清晰。# 使用 warnings 模块忽略特定消息的警告。这里忽略了关于 CUDA 不可用的警告,这在系统不支持 CUDA 但代码尝试使用 CUDA 设备时会出现。
warnings.filterwarnings('ignore', message='User provided device_type of \'cuda\', but CUDA is not available. Disabling') # 用户提供的 device_type 为 \'cuda\',但 CUDA 不可用。正在禁用。
# 忽略所有 UserWarning 类别的警告。 UserWarning 是 Python 标准库 warnings 中的一个类别,用于警告用户可能的错误。
warnings.filterwarnings('ignore', category=UserWarning)
# 这段代码主要用于设置分布式训练环境变量、尝试导入性能分析库,并忽略一些特定的警告。这些设置有助于在分布式环境中运行模型训练和分析,同时避免不必要的警告信息干扰输出。
2.def smart_inference_mode(torch_1_9=check_version(torch.__version__, '1.9.0')):
# 这段代码定义了一个名为 smart_inference_mode 的函数,它是一个装饰器工厂,用于根据 PyTorch 版本应用不同的装饰器。
# 函数定义。
# 1.torch_1_9 : 一个布尔值,指示当前 PyTorch 版本是否大于或等于 1.9.0。默认值由 check_version 函数确定,该函数检查 PyTorch 版本是否满足指定的最小版本。
# def check_version(current='0.0.0', minimum='0.0.0', name='version ', pinned=False, hard=False, verbose=False):
# -> 它用于检查当前安装的软件包版本是否满足指定的最低版本要求。函数返回 result ,即版本检查的结果。
# -> return result
def smart_inference_mode(torch_1_9=check_version(torch.__version__, '1.9.0')):# Applies torch.inference_mode() decorator if torch>=1.9.0 else torch.no_grad() decorator 如果 torch>=1.9.0,则应用 torch.inference_mode() 装饰器,否则应用 torch.no_grad() 装饰器。# 定义一个内部函数 decorate ,它接受一个函数。# 1.fn :作为参数。def decorate(fn):# torch.inference_mode(mode=True)# torch.inference_mode() 是 PyTorch 中的一个上下文管理器,用于在推理(inference)时禁用梯度计算和某些其他功能,以提高性能。这个功能在 PyTorch 1.9 版本中引入,作为 torch.no_grad() 的一个替代方案,特别是在推理模式下运行代码时,能够获得更好的性能。# 参数说明 :# mode :一个布尔值,指示是否启用推理模式。默认为 True 。# 功能描述 :# 当 mode=True 时, torch.inference_mode() 禁用梯度计算和视图跟踪,这可以减少内存消耗并提高计算速度。# 该上下文管理器是线程局部的,不会影响其他线程的计算。# 注意事项 :# torch.inference_mode() 与 torch.no_grad() 类似,但提供了更好的性能优化,特别是在推理模式下。# 在使用 torch.inference_mode() 时,不能在中途设置 requires_grad=True ,这与 torch.no_grad() 不同,后者允许在上下文内部更改 requires_grad 属性。# 如果已经在推理模式下,再次使用 torch.inference_mode() 作为装饰器或上下文管理器不会改变当前状态,它只会在退出时恢复到先前的状态。# torch.inference_mode() 提供了一种有效的方式来优化模型推理时的性能,特别是在需要禁用梯度计算的场景中。# 根据 torch_1_9 的值,选择 torch.inference_mode 或 torch.no_grad 装饰器,并将其应用于函数 fn 。# torch.inference_mode 是 PyTorch 1.9.0 版本中引入的上下文管理器,用于设置模型为推理模式,优化推理性能。# torch.no_grad 是一个上下文管理器,用于暂时禁用梯度计算,通常用于推理过程中,以减少内存消耗和加速计算。return (torch.inference_mode if torch_1_9 else torch.no_grad)()(fn)# 返回值。函数返回 decorate 函数,它是一个装饰器工厂,可以根据 PyTorch 版本选择适当的装饰器。return decorate
# 在模型推理时,使用 smart_inference_mode 装饰器可以确保代码与不同版本的 PyTorch 兼容。如果 PyTorch 版本大于或等于 1.9.0,它将使用 torch.inference_mode 装饰器;否则,使用 torch.no_grad 装饰器。
3.def smartCrossEntropyLoss(label_smoothing=0.0):
# 这段代码定义了一个名为 smartCrossEntropyLoss 的函数,它用于创建一个带有标签平滑(label smoothing)功能的交叉熵损失函数(CrossEntropyLoss)。这个函数的目的是根据不同版本的PyTorch自动选择是否启用标签平滑。
# 定义了一个函数 smartCrossEntropyLoss ,它接受一个参数。
# 1.label_smoothing :默认值为 0.0 。这个参数用于控制标签平滑的程度。
def smartCrossEntropyLoss(label_smoothing=0.0):# Returns nn.CrossEntropyLoss with label smoothing enabled for torch>=1.10.0 返回启用了标签平滑的 nn.CrossEntropyLoss,适用于 torch>=1.10.0 。# 检查当前PyTorch的版本是否大于或等于1.10.0。 check_version 接受两个参数,当前PyTorch的版本和目标版本号。如果当前版本满足条件,它将返回 True 。# def check_version(current='0.0.0', minimum='0.0.0', name='version ', pinned=False, hard=False, verbose=False):# -> 用于检查当前安装的软件包版本是否满足指定的最低版本要求。函数返回 result ,即版本检查的结果。# -> return resultif check_version(torch.__version__, '1.10.0'):# torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')# torch.nn.CrossEntropyLoss() 是 PyTorch 中的一个损失函数类,用于多分类问题中的交叉熵损失计算。这个类在训练分类模型时非常有用,尤其是在神经网络的输出层。# 参数说明 :# weight (Tensor, optional) :一个手动的权重值,与类别的数量相同,用于重新调整类别的权重。默认为 None ,即不使用权重。# size_average (bool, optional) :这个参数已经被弃用(在PyTorch 0.4之后),现在使用 reduction 参数来控制。如果设置为 True ,则输出的平均损失;如果设置为 False ,则输出总损失。默认为 None ,即根据 reduction 参数决定。# ignore_index (int, optional) :指定一个类别的索引,该索引的类别在计算损失时会被忽略。这在处理某些类别的数据时非常有用,例如在某些图像中,某些像素点的类别是未知的。默认为 -100 。# reduce (bool, optional) :这个参数已经被弃用(在PyTorch 1.1.0之后),现在使用 reduction 参数来控制。如果设置为 True ,则输出平均损失;如果设置为 False ,则输出总损失。默认为 None ,即根据 reduction 参数决定。# reduction (string, optional) :指定如何减少损失值。 'none' 不减少; 'mean' 输出平均损失; 'sum' 输出总损失。默认为 'mean' 。# 工作原理 :# CrossEntropyLoss 计算预测和目标之间的交叉熵损失。输入的预测值需要是未经归一化的分数(logits),而目标值需要是类别的索引。# 如果PyTorch版本满足条件,函数将返回一个 nn.CrossEntropyLoss 对象,并启用标签平滑功能,标签平滑的值由参数 label_smoothing 指定。return nn.CrossEntropyLoss(label_smoothing=label_smoothing)# 检查 label_smoothing 参数是否大于0。如果大于0,说明用户希望启用标签平滑功能。if label_smoothing > 0:# 如果 label_smoothing 大于0,但是PyTorch版本不满足条件,函数将通过 LOGGER 发出警告,告知用户标签平滑功能需要PyTorch版本至少为1.10.0。LOGGER.warning(f'WARNING ⚠️ label smoothing {label_smoothing} requires torch>=1.10.0') # 警告⚠️标签平滑{label_smoothing}需要torch> = 1.10.0 。# 如果PyTorch版本不满足条件,或者 label_smoothing 参数为0或负值,函数将返回一个没有启用标签平滑功能的 nn.CrossEntropyLoss 对象。return nn.CrossEntropyLoss()
# 这个 smartCrossEntropyLoss 函数根据PyTorch的版本和用户指定的 label_smoothing 参数来决定是否启用标签平滑功能。如果PyTorch版本是1.10.0或以上,并且 label_smoothing 大于0,它将返回一个带有标签平滑功能的交叉熵损失函数。否则,它将返回一个标准的交叉熵损失函数。如果用户希望使用标签平滑但PyTorch版本不满足要求,函数会发出警告。
4.def smart_DDP(model):
# 这段代码定义了一个名为 smart_DDP 的函数,用于创建一个分布式数据并行(Distributed Data Parallel,简称 DDP)模型,同时包含了对 PyTorch 版本的检查。
# 这行定义了一个名为 smart_DDP 的函数,它接受一个参数.
# 1.model :这个参数应该是一个 PyTorch 模型。
def smart_DDP(model):# Model DDP creation with checks# 使用 assert 语句来确保当前的 PyTorch 版本不是 1.12.0。 check_version 函数用于检查 PyTorch 的版本, pinned=True 表示检查精确版本。如果版本是 1.12.0,将抛出一个异常,并给出提示信息,说明由于已知问题,不支持使用 PyTorch 1.12.0 和 torchvision 0.13.0 进行 DDP 训练,并建议升级或降级 PyTorch 版本。assert not check_version(torch.__version__, '1.12.0', pinned=True), \'torch==1.12.0 torchvision==0.13.0 DDP training is not supported due to a known issue. ' \'Please upgrade or downgrade torch to use DDP. See https://github.com/ultralytics/yolov5/issues/8395' # torch==1.12.0 torchvision==0.13.0 由于已知问题,不支持 DDP 训练。请升级或降级 torch 以使用 DDP。请参阅 https://github.com/ultralytics/yolov5/issues/8395 。# 检查当前的 PyTorch 版本是否是 1.11.0 或更高版本。如果是,将使用这个版本的特定参数来创建 DDP 实例。if check_version(torch.__version__, '1.11.0'):# 如果 PyTorch 版本是 1.11.0 或更高版本,将创建一个 DDP 实例,并传入 模型 、设备 ID 列表 ( [LOCAL_RANK] )、 输出设备 ( LOCAL_RANK )和 static_graph=True 参数。 LOCAL_RANK 是一个环境变量,通常在分布式训练中用来指定当前进程的设备 ID。 static_graph=True 是一个特定于 PyTorch 1.11.0 的参数,用于优化 DDP 的性能。return DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK, static_graph=True)# 如果 PyTorch 版本不是 1.11.0 或更高版本,将创建一个 DDP 实例,但不传入 static_graph 参数。这意味着将使用默认的动态图行为。else:return DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK)
# smart_DDP 函数根据 PyTorch 的版本来创建一个 DDP 实例。它首先检查 PyTorch 版本是否是 1.12.0,因为这个版本已知存在 DDP 训练的问题。然后,如果版本是 1.11.0 或更高版本,它将使用 static_graph=True 参数来创建 DDP 实例,以优化性能。对于其他版本,它将创建一个标准的 DDP 实例。这个函数确保了在不同版本的 PyTorch 中,DDP 的使用都是安全的,并且能够利用新版本的优化特性。
5.def reshape_classifier_output(model, n=1000):
# 这段代码定义了一个名为 reshape_classifier_output 的函数,它用于更新 PyTorch 模型的分类器输出层,以适应新的类别数量 n 。
# 定义了一个名为 reshape_classifier_output 的函数,它接受两个参数。
# model :是一个 PyTorch 模型, n 是新的类别数量,默认值为 1000。
def reshape_classifier_output(model, n=1000):# Update a TorchVision classification model to class count 'n' if required 如果需要,将 TorchVision 分类模型更新为类数为“n” 。# 从 models.common 模块中导入 Classify 类,这个类是自定义的分类器头部,用于构建 YOLO 目标检测模型中的分类头。from models.common import Classify# 获取模型的最后一个模块。如果模型有一个名为 model 的属性(即模型被封装在另一个模型中),则使用这个属性;否则,直接使用模型本身。 named_children() 方法返回模型中所有子模块的名称和模块对象的列表, [-1] 索引用于获取列表中的最后一个元素,即最后一个模块。name, m = list((model.model if hasattr(model, 'model') else model).named_children())[-1] # last module# 检查最后一个模块是否是 Classify 类的实例,如果是,说明这是一个 YOLOv5 模型的分类器头部。if isinstance(m, Classify): # YOLOv5 Classify() head# 如果 Classify 头部的线性层的输出特征数量不等于 n ,则更新这个线性层,使其输出特征数量等于 n 。if m.linear.out_features != n:m.linear = nn.Linear(m.linear.in_features, n)# 检查最后一个模块是否是 PyTorch 的 nn.Linear 层,如果是,说明这是一个 ResNet 或 EfficientNet 模型的分类器头部。elif isinstance(m, nn.Linear): # ResNet, EfficientNet# 如果线性层的输出特征数量不等于 n ,则使用 setattr 方法更新模型的这个属性,将其替换为一个新的 nn.Linear 层,其输入特征数量与原层相同,输出特征数量为 n 。if m.out_features != n:setattr(model, name, nn.Linear(m.in_features, n))# 检查最后一个模块是否是 PyTorch 的 nn.Sequential 容器,如果是,说明模型的分类器头部是一个序列容器。elif isinstance(m, nn.Sequential):# 获取 nn.Sequential 容器中所有模块的类型。types = [type(x) for x in m]# 如果容器中包含 nn.Linear 层,找到这个层的索引 i ,如果这个线性层的输出特征数量不等于 n ,则更新这个线性层,使其输出特征数量等于 n 。if nn.Linear in types:i = types.index(nn.Linear) # nn.Linear indexif m[i].out_features != n:m[i] = nn.Linear(m[i].in_features, n)# 如果容器中包含 nn.Conv2d 层,找到这个层的索引 i ,如果这个卷积层的输出通道数量不等于 n ,则更新这个卷积层,使其输出通道数量等于 n 。elif nn.Conv2d in types:i = types.index(nn.Conv2d) # nn.Conv2d indexif m[i].out_channels != n:m[i] = nn.Conv2d(m[i].in_channels, n, m[i].kernel_size, m[i].stride, bias=m[i].bias is not None)
# reshape_classifier_output 函数用于更新不同类型 PyTorch 模型的分类器输出层,以适应新的类别数量 n 。它支持 YOLOv5、ResNet、EfficientNet 等模型,以及包含 nn.Linear 或 nn.Conv2d 的自定义模型。这个函数通过检查模型的最后一个模块,并根据其类型进行相应的更新,以确保模型的输出层能够适应新的类别数量。
6.def torch_distributed_zero_first(local_rank: int):
# 这段代码定义了一个名为 torch_distributed_zero_first 的上下文管理器(context manager),它用于在分布式训练中同步所有进程,确保每个进程在执行特定操作之前等待主进程(通常是 local_rank 为 0 的进程)完成某些任务。# @contextmanager
# 在Python中, @contextmanager 是一个装饰器,它属于 contextlib 模块,用于将一个生成器函数转换为上下文管理器。上下文管理器是一种工具,它允许你以一种特定的方式执行进入和退出的上下文代码块,这通常用于管理资源,如文件操作、数据库连接等。
# 使用 @contextmanager 装饰器的基本语法如下:
# from contextlib import contextmanager
# @contextmanager
# def my_context_manager():
# # 进入上下文的代码
# yield
# # 退出上下文的代码
# my_context_manager 是一个生成器函数,它使用 yield 语句来分隔进入和退出上下文的代码。
# yield 之前的代码块是进入上下文的代码,通常用于设置或初始化资源。
# yield 之后的代码块是退出上下文的代码,通常用于清理或释放资源。
# @contextmanager 装饰器提供了一种简洁的方式来编写上下文管理器,使得代码更加清晰和易于维护。它特别适用于需要执行特定进入和退出操作的场景,如文件操作、数据库连接、网络请求等。# 这是一个装饰器,用于定义一个上下文管理器,它允许使用 with 语句来管理资源的获取和释放。
@contextmanager
# 这是上下文管理器的定义,它接受一个参数。
# 1.local_rank :表示当前进程的局部排名。
def torch_distributed_zero_first(local_rank: int):# Decorator to make all processes in distributed training wait for each local_master to do something 装饰器使分布式训练中的所有进程等待每个 local_master 执行某些操作。# 检查当前进程的 local_rank 是否不是 -1 或 0 。如果是 -1 ,则表示当前环境不是分布式环境;如果是 0 ,则表示当前进程是主进程。if local_rank not in [-1, 0]:# 如果当前进程不是主进程,使用 PyTorch 的 dist.barrier 函数在所有进程上创建一个屏障,等待所有进程到达这一点后再继续执行。这确保了主进程( local_rank 为 0 )在执行后续操作之前不会开始。dist.barrier(device_ids=[local_rank])# 上下文管理器的 yield 语句标志着上下文管理的开始,允许 with 语句中的代码块执行。yield# 检查当前进程是否是主进程。if local_rank == 0:# dist.barrier(device_ids=[device_id1, device_id2, ...])# 在 PyTorch 的分布式训练中, dist.barrier() 函数用于同步所有进程,确保它们在继续执行之前都达到了相同的执行点。当涉及到多个设备(例如,多个 GPU)时, dist.barrier() 可以接受一个 device_ids 参数,用于指定哪些设备参与同步。# 参数 :# device_ids :(可选)一个包含设备 ID 的列表。如果提供, dist.barrier() 将只同步指定的设备。如果不提供或为 None ,则同步所有设备。# 工作原理 :# 当一个进程中的某个设备调用 dist.barrier(device_ids) 后,它会暂停执行,直到所有进程中对应 device_ids 指定的设备都调用了该函数。# 一旦所有指定的设备都到达了 barrier 点, dist.barrier() 函数会返回,然后所有设备继续执行后续的操作。# 特点 :# dist.barrier() 函数通常用于确保在分布式训练中所有进程都完成了某个特定的操作后再进行后续的操作,从而保证训练的一致性。# 它可以用来同步数据加载、模型参数更新等操作,确保所有进程在执行下一步之前都已经完成了当前步骤。# 在使用 dist.barrier() 时,需要先初始化进程组,并在结束时销毁进程组。# dist.barrier(device_ids) 函数是 PyTorch 分布式训练中实现设备间同步的重要工具,它确保了在执行下一步之前,所有指定的设备都已经完成了当前步骤,从而保证了训练的一致性和稳定性。# 如果当前进程是主进程,再次使用 dist.barrier 函数创建一个屏障,这次只等待 local_rank 为 0 的进程到达这一点。这确保了在主进程执行特定操作之后,其他进程才会继续执行。dist.barrier(device_ids=[0])
# 使用这个上下文管理器的目的是确保在分布式训练中,所有进程在执行某些关键操作之前都等待主进程完成初始化或其他任务。这有助于避免数据不一致和潜在的竞态条件。
7.def device_count():
# 这段代码定义了一个名为 device_count 的函数,其目的是安全地返回可用的 CUDA 设备数量。这个函数特别考虑了不同操作系统(Linux 和 Windows)的兼容性,并提供了对 torch.cuda.device_count() 的一个替代方案。
# 定义了一个没有参数的函数 device_count 。
def device_count():# Returns number of CUDA devices available. Safe version of torch.cuda.device_count(). Supports Linux and Windows 返回可用的 CUDA 设备数量。torch.cuda.device_count() 的安全版本。支持 Linux 和 Windows 。# 使用 assert 语句确保当前操作系统是 Linux 或 Windows。如果不是这两个系统,将抛出异常。assert platform.system() in ('Linux', 'Windows'), 'device_count() only supported on Linux or Windows' # device_count() 仅支持 Linux 或 Windows 。# 开始一个 try 块,用于捕获并处理可能发生的异常。try:# 根据操作系统选择不同的命令。在 Linux 系统中,使用 nvidia-smi -L | wc -l 命令来获取 CUDA 设备的数量。在 Windows 系统中,使用 nvidia-smi -L | find /c /v "" 命令来实现相同的功能。cmd = 'nvidia-smi -L | wc -l' if platform.system() == 'Linux' else 'nvidia-smi -L | find /c /v ""' # Windows# subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, # check=False, timeout=None, device=None, text=None, encoding=None, errors=None, # shell=False, cwd=None, env=None, universal_newlines=False, bufsize=-1, # start_new_session=False, restore_signals=True, preexec_fn=None, # pass_fds=(), warn=False)# subprocess.run() 是 Python 标准库 subprocess 模块中的一个函数,用于执行外部命令和程序。这个函数在 Python 3.5 中被引入,作为替代旧的 subprocess.call() 、 subprocess.check_call() 和 subprocess.check_output() 函数的一个更现代的接口。# 参数说明 :# args :命令及其参数。可以是字符串列表或单个字符串。如果 shell=True ,则 args 应该是单个字符串。# stdin :指定 stdin 的文件描述符或文件对象。# input :如果提供,写入到 stdin 。# stdout 和 stderr :指定 stdout 和 stderr 的文件描述符或文件对象。# capture_output :如果为 True ,则捕获 stdout 和 stderr 。# check :如果为 True ,则在命令返回非零退出状态时引发 CalledProcessError 异常。# timeout :等待命令完成的秒数。超时则引发 TimeoutExpired 异常。# device 、 text 、 encoding 、 errors :控制输出和错误输出的编码。# shell :如果为 True ,则通过 shell 执行 args 。# cwd :子进程的当前工作目录。# env :子进程的环境变量。# universal_newlines :如果为 True ,则将输出和错误解释为文本。# bufsize :缓冲区大小。# start_new_session :如果为 True ,则在子进程中启动一个新的会话。# restore_signals :如果为 True ,则在子进程中恢复信号。# preexec_fn :在子进程中执行的函数。# pass_fds :需要传递给子进程的文件描述符列表。# warn :(已弃用)是否发出警告。# 返回值 :# 返回一个 CompletedProcess 实例,它包含执行命令后的信息,如 args 、 returncode 、 stdout 、 stderr 等。# 执行上面构建的命令,使用 subprocess.run 来运行 shell 命令,并捕获输出。 capture_output=True 表示捕获输出, check=True 表示如果命令执行失败则抛出异常。命令的输出被解码并分割成列表, [-1] 索引获取最后一个元素,即设备数量,并将其转换为整数返回。return int(subprocess.run(cmd, shell=True, capture_output=True, check=True).stdout.decode().split()[-1])# 如果在执行命令的过程中发生任何异常, except 块将捕获这些异常,并返回 0,表示没有可用的 CUDA 设备。except Exception:return 0
# device_count 函数是一个跨平台(Linux 和 Windows)的工具,用于返回系统中可用的 CUDA 设备数量。它通过执行系统命令来获取设备数量,并在发生异常时提供默认返回值 0。这个函数是 torch.cuda.device_count() 的一个安全替代方案,尤其是在某些环境中 torch.cuda.device_count() 可能无法正常工作时。
8.def select_device(device='', batch_size=0, newline=True):
# 这段代码定义了一个名为 select_device 的函数,其目的是根据用户提供的参数选择使用 CPU、单个 GPU 或多个 GPU,并返回一个对应的 PyTorch 设备对象。
# 定义了 select_device 函数,接受三个参数。
# 1.device :指定使用的设备,可以是 'cpu' 、空字符串(默认使用 CUDA)、 '0' (第一个 GPU)、 '0,1,2,3' (多个 GPU)等。
# 2.batch_size :批次大小,用于检查是否能够被 GPU 数量整除。
# 3.newline :布尔值,指示是否在日志信息后添加换行符。
def select_device(device='', batch_size=0, newline=True):# device = None or 'cpu' or 0 or '0' or '0,1,2,3'# 初始化一个字符串 s ,用于存储设备信息。# def git_describe(path=ROOT):# -> 获取 Git 仓库的描述信息。这个描述信息通常包含了最近的标签(tag)和自那个标签以来的提交次数,以及当前的提交哈希。使用 check_output 函数执行 Git 命令,获取仓库的描述信息。如果发生异常,则返回一个空字符串。# -> return check_output(f'git -C {path} describe --tags --long --always', shell=True).decode()[:-1] / return ''# def file_date(path=__file__): -> 返回指定文件的修改日期,格式为 年-月-日 。返回格式化的日期字符串,格式为 年-月-日 。 -> return f'{t.year}-{t.month}-{t.day}'s = f'YOLOv5 🚀 {git_describe() or file_date()} Python-{platform.python_version()} torch-{torch.__version__} '# 将 device 参数转换为字符串,并进行清理和格式化。device = str(device).strip().lower().replace('cuda:', '').replace('none', '') # to string, 'cuda:0' to '0'# 检查是否选择了 CPU 或 Apple MPS 设备。cpu = device == 'cpu'mps = device == 'mps' # Apple Metal Performance Shaders (MPS)# 如果选择了 CPU 或 MPS,则设置环境变量 CUDA_VISIBLE_DEVICES 为 -1 ,使得 PyTorch 无法看到任何 CUDA 设备。if cpu or mps:os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # force torch.cuda.is_available() = False# 如果指定了非 CPU 设备,则设置环境变量 CUDA_VISIBLE_DEVICES 并检查 CUDA 设备是否可用。elif device: # non-cpu device requestedos.environ['CUDA_VISIBLE_DEVICES'] = device # set environment variable - must be before assert is_available()# torch.cuda.device_count()# torch.cuda.device_count() 函数是 PyTorch 提供的一个 API,用于返回当前系统中可用的 GPU 设备数量。# 返回值 :# 返回一个整数,表示当前系统中可用的 GPU 设备的数量。# 功能描述 :# 这个函数用于检测系统中有多少个 GPU 可用。如果返回值大于 0,则说明系统中有可用的 GPU 设备;如果返回值为 0,则说明系统中没有可用的 GPU 设备。# 使用场景 :# 通常在深度学习模型训练前,使用这个函数来确定是否可以使用 GPU 进行加速计算。如果系统中有多个 GPU,这个函数可以帮助开发者了解可以利用的 GPU 资源数量。# 注意事项 :# 如果你的系统有多个 GPU,这个函数会返回所有可用 GPU 的数量。# 果你的系统没有安装 NVIDIA 驱动或者 CUDA 工具包,这个函数可能返回 0,即使物理上存在 GPU 设备。# 某些情况下,即使系统中有 GPU,如果 PyTorch 没有正确配置或者 GPU 被其他进程占用,也可能导致这个函数返回 0。# 这个函数是 PyTorch 进行 GPU 管理的基础函数之一,对于需要进行 GPU 编程的开发者来说非常重要。assert torch.cuda.is_available() and torch.cuda.device_count() >= len(device.replace(',', '')), \f"Invalid CUDA '--device {device}' requested, use '--device cpu' or pass valid CUDA device(s)"# 如果没有选择 CPU 或 MPS 且 CUDA 设备可用,则选择 GPU 设备。if not cpu and not mps and torch.cuda.is_available(): # prefer GPU if available# 获取设备列表,如果 device 参数为空,则默认为 '0' 。devices = device.split(',') if device else '0' # range(torch.cuda.device_count()) # i.e. 0,1,6,7# 获取设备数量。n = len(devices) # device count# 如果使用多个 GPU 且 batch_size 大于 0,则检查 batch_size 是否能被设备数量整除。if n > 1 and batch_size > 0: # check batch_size is divisible by device_countassert batch_size % n == 0, f'batch-size {batch_size} not multiple of GPU count {n}'space = ' ' * (len(s) + 1)# 遍历设备列表,打印每个设备的信息。for i, d in enumerate(devices):# torch.cuda.get_device_properties(device)# torch.cuda.get_device_properties() 函数是 PyTorch 中用于获取指定 GPU 设备属性的函数。# 参数 :# device (torch.device 或 int 或 str) :要返回设备属性的设备。可以是设备 ID(整数),类似于 gpu:x 的设备名称,或者是 torch.device 对象。如果 device 为空,则默认为当前设备。默认值为 None。# 返回值 :# 返回一个 _CudaDeviceProperties 对象,其中包含以下属性 :# name :GPU 设备的名称(字符串)。# total_memory :GPU 总显存大小,以字节为单位(整数)。# major :CUDA 计算能力的主要版本号(整数)。# minor :CUDA 计算能力的次要版本号(整数)。# multi_processor_count :GPU 设备的多处理器数量(整数)。# is_integrated :GPU 是否是集成显卡,返回 True 或 False(布尔值)。# is_multi_gpu_board :GPU 是否是多 GPU 板卡,返回 True 或 False(布尔值)。# 这个函数可以帮助你了解指定 GPU 设备的详细信息,例如显存大小、计算能力等,这些信息对于深度学习模型的训练和优化非常有用。p = torch.cuda.get_device_properties(i)s += f"{'' if i == 0 else space}CUDA:{d} ({p.name}, {p.total_memory / (1 << 20):.0f}MiB)\n" # bytes to MBarg = 'cuda:0'# 如果选择了 MPS 且 MPS 可用,则使用 MPS 设备。elif mps and getattr(torch, 'has_mps', False) and torch.backends.mps.is_available(): # prefer MPS if availables += 'MPS\n'arg = 'mps'# 如果没有可用的 GPU 或 MPS 设备,则使用 CPU。else: # revert to CPUs += 'CPU\n'arg = 'cpu'# 如果 newline 参数为 False ,则移除字符串 s 末尾的换行符。if not newline:s = s.rstrip()# 使用 LOGGER 打印设备信息。LOGGER.info(s)# torch.device(device_str=None, device_type=None)# torch.device 是 PyTorch 中的一个函数,用于创建一个表示设备的对象,这个对象可以用来指定张量(Tensor)应该被分配到哪个设备上(例如 CPU 或特定的 GPU)。# 参数 :# device_str (str, 可选) :一个字符串,指定设备。可以是以下形式之一 :# 'cpu' :表示 CPU 设备。# 'cuda' :表示任意 CUDA 兼容的 GPU 设备。# 'cuda:N' :表示编号为 N 的 CUDA 兼容 GPU 设备,N 是一个非负整数。# 'cuda:N,M' :表示编号为 N 和 M 的两个 CUDA 兼容 GPU 设备。# 'cuda:NcM' :表示编号为 N 的 CUDA 兼容 GPU 设备上的 M 个连续设备。# 'nccl' :表示使用 NCCL 进行多 GPU 通信。# device_type (str, 可选) :一个字符串,指定设备类型,可以是 'cpu' 或 'cuda' 。如果 device_str 已经指定了设备类型,则 device_type 会被忽略。# 返回值 :# 返回一个 torch.device 对象,表示指定的设备。# 功能描述 :# 这个函数用于创建一个设备对象,该对象可以用来指定 PyTorch 张量应该被分配到哪个设备上。这对于在多设备环境中进行张量操作和模型训练非常有用。# 使用场景 :# 在多 GPU 环境中,指定特定的 GPU 进行计算。# 在需要将张量移动到 CPU 或 GPU 上时,指定目标设备。# 在模型训练时,指定模型和数据应该在哪个设备上运行。# 返回一个 PyTorch 设备对象,用于指定后续计算应该在哪个设备上执行。return torch.device(arg)
# 这个函数提供了一个灵活的方式来选择和配置设备,支持 CPU、单个 GPU、多个 GPU 以及 Apple MPS 设备。通过检查环境变量和设备可用性,它可以确保在不同的硬件配置下都能正确地选择设备。
9.def time_sync():
# 这段代码定义了一个名为 time_sync 的函数,它用于在PyTorch中同步CUDA时间,以确保获取的时间是准确的。
# 这是 time_sync 函数的定义,其目的是提供一种准确测量时间的方法,特别是在涉及到CUDA操作时。
def time_sync():# PyTorch-accurate time# 函数检查CUDA是否可用。if torch.cuda.is_available():# 如果可用,调用 torch.cuda.synchronize() 函数。这个函数确保所有的CUDA流都完成了当前的操作,也就是说,它等待所有的CUDA核心完成它们的工作。# 这是必要的,因为在CUDA中,操作是异步执行的,直接调用 time.time() 可能不会返回操作实际完成的时间,而是返回操作被调度的时间。torch.cuda.synchronize()# 函数返回当前的时间(以秒为单位),这是通过Python标准库中的 time.time() 函数获得的。由于在调用 time.time() 之前同步了CUDA操作,所以这个时间更准确地反映了CUDA操作完成的时间。return time.time()
# time_sync 函数通常用于性能分析,特别是在需要测量CUDA操作的执行时间时。通过同步CUDA流,可以确保测量的时间不包括由于异步执行导致的延迟,从而提供更准确的性能指标。
10.def profile(input, ops, n=10, device=None):
# 这段代码定义了一个名为 profile 的函数,是一个性能分析工具,用于评估 PyTorch 模型或操作(ops)的性能。它计算给定输入(input)和操作(ops)的参数数量、浮点运算次数(GFLOPs)、GPU显存使用量、前向传播时间和反向传播时间。
# 定义一个名为 profile 的函数,接受四个参数。
# 1.input :输入数据。
# 2.ops :要评估的操作或模型。
# 3.n :迭代次数,默认为10。
# 4.device :设备,默认为None,即自动选择。
def profile(input, ops, n=10, device=None):# YOLOv5 速度/内存/FLOP 分析器。""" YOLOv5 speed/memory/FLOPs profilerUsage:input = torch.randn(16, 3, 640, 640)m1 = lambda x: x * torch.sigmoid(x)m2 = nn.SiLU()profile(input, [m1, m2], n=100) # profile over 100 iterations"""# 初始化一个空列表 results ,用于存储每次评估的结果。results = []# 检查 device 是否是 torch.device 类型,如果不是,则调用 select_device 函数来选择一个设备。if not isinstance(device, torch.device):# def select_device(device='', batch_size=0, newline=True):# -> 根据用户提供的参数选择使用 CPU、单个 GPU 或多个 GPU,并返回一个对应的 PyTorch 设备对象。返回一个 PyTorch 设备对象,用于指定后续计算应该在哪个设备上执行。# -> return torch.device(arg)device = select_device(device)# 打印表头,包括 参数数量 、 GFLOPs 、 GPU显存使用量 、 前向传播时间 、 反向传播时间 和 输入输出的形状 。print(f"{'Params':>12s}{'GFLOPs':>12s}{'GPU_mem (GB)':>14s}{'forward (ms)':>14s}{'backward (ms)':>14s}"f"{'input':>24s}{'output':>24s}")# 遍历输入数据,如果输入是列表则直接遍历,否则将输入包装成列表。for x in input if isinstance(input, list) else [input]:# 将输入数据 x 转移到指定的设备上。x = x.to(device)# 设置 requires_grad 属性为 True,以便在反向传播时计算梯度。x.requires_grad = True# 遍历操作列表,如果 ops 是列表则直接遍历,否则将 ops 包装成列表。for m in ops if isinstance(ops, list) else [ops]:# 如果操作 m 有 to 方法,则将其转移到指定的设备上。m = m.to(device) if hasattr(m, 'to') else m # device# 如果操作 m 支持半精度( half 方法),并且输入 x 是一个半精度的张量,则将操作 m 转换为半精度。m = m.half() if hasattr(m, 'half') and isinstance(x, torch.Tensor) and x.dtype is torch.float16 else m# 初始化变量 tf 和 tb 用于存储 前向和反向传播的时间 , t 用于存储 每次迭代的时间 。tf, tb, t = 0, 0, [0, 0, 0] # dt forward, backward# 尝试使用 thop.profile 函数计算操作 m 的浮点运算次数(GFLOPs),如果失败则设置为0。try:flops = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 # GFLOPsexcept Exception:flops = 0# 尝试执行以下代码块,如果发生异常,则转到 except 块处理。try:# 进行 n 次迭代,用于测量前向和反向传播的平均时间。for _ in range(n):# 在前向传播之前,使用 time_sync 函数获取当前时间(以秒为单位),并存储在 t[0] 中。# def time_sync(): -> 用于在PyTorch中同步CUDA时间,以确保获取的时间是准确的。函数返回当前的时间(以秒为单位),这是通过Python标准库中的 time.time() 函数获得的。 -> return time.time()t[0] = time_sync()# 执行模型或操作 m 的前向传播,输入为 x ,输出为 y 。y = m(x)# 在前向传播之后,再次使用 time_sync 函数获取当前时间,并存储在 t[1] 中。t[1] = time_sync()# 尝试执行以下代码块,如果发生异常,则转到内部的 except 块处理。try:# 如果输出 y 是列表,则对列表中的每个元素求和,然后对结果求和,执行反向传播。如果 y 不是列表,则直接对 y 求和后执行反向传播。_ = (sum(yi.sum() for yi in y) if isinstance(y, list) else y).sum().backward()# 在反向传播之后,再次使用 time_sync 函数获取当前时间,并存储在 t[2] 中。t[2] = time_sync()# 如果反向传播失败(可能是因为某些操作没有反向传播方法),则捕获异常。except Exception: # no backward method# print(e) # for debug# 打印异常信息(已注释,用于调试),并将 t[2] 设置为 NaN ,表示反向传播时间未知。t[2] = float('nan')# 计算前向传播的平均时间(以毫秒为单位),并累加到 tf 中。tf += (t[1] - t[0]) * 1000 / n # ms per op forward# 计算反向传播的平均时间(以毫秒为单位),并累加到 tb 中。tb += (t[2] - t[1]) * 1000 / n # ms per op backward# torch.cuda.memory_reserved(device=None)# torch.cuda.memory_reserved() 是 PyTorch 提供的一个函数,用于查询当前 GPU 上由 PyTorch 预先保留但尚未实际分配的显存量。# 参数 :# device (torch.device 或 str, 可选) :指定要查询的 CUDA 设备。可以是设备索引(例如 'cuda:0' ),或者是 torch.device 对象。如果未指定,则默认为当前设备。# 返回值 :# 返回一个整数,表示当前进程所分配的显存缓冲区总量,单位为字节。# 功能描述 :# 这个函数返回由 PyTorch 预先保留但尚未实际分配的显存量。PyTorch 在启动时会预留一定数量的显存作为缓冲区,以便在需要时快速分配。这部分显存并不会直接被张量或模型参数占用,因此不计入 torch.cuda.memory_allocated() 的已分配显存。# 使用场景 :# 监控和管理 GPU 显存使用情况,特别是在运行大型模型或处理大规模数据时,了解显存的预留情况对于优化性能和避免显存溢出非常重要。# 注意事项 :# 返回的显存预留量包括 PyTorch 为当前进程预留的所有显存,但不包括其他进程可能占用的显存。# torch.cuda.memory_reserved() 的值并不等同于 nvidia-smi 显示的值, nvidia-smi 显示的是 reserved_memory 和 PyTorch context 显存之和。# 如果 CUDA 可用,计算当前 GPU 显存的预留量(以 GB 为单位),否则设置为 0。mem = torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0 # (GB)# 获取输入 x 和输出 y 的形状,如果它们是张量,则返回形状元组,否则返回 'list'。s_in, s_out = (tuple(x.shape) if isinstance(x, torch.Tensor) else 'list' for x in (x, y)) # shapes# 如果 m 是一个 PyTorch 模块,则计算其所有参数的总数,否则设置为 0。p = sum(x.numel() for x in m.parameters()) if isinstance(m, nn.Module) else 0 # parameters# 打印一行结果,包括 参数数量 、 GFLOPs 、 显存使用量 、 前向传播时间 、 反向传播时间 、 输入和输出的形状 。print(f'{p:12}{flops:12.4g}{mem:>14.3f}{tf:14.4g}{tb:14.4g}{str(s_in):>24s}{str(s_out):>24s}')# 将这次测量的结果添加到 results 列表中。results.append([p, flops, mem, tf, tb, s_in, s_out])# 如果在整个测量过程中发生任何异常,则捕获异常。except Exception as e:# 打印异常信息。print(e)# 将 None 添加到 results 列表中,表示这次测量失败。results.append(None)# torch.cuda.empty_cache()# torch.cuda.empty_cache() 是 PyTorch 提供的一个函数,用于释放当前 PyTorch CUDA 缓存中的未被使用的显存。# 参数 :无参数。# 返回值 :无返回值。# 功能描述 :# 这个函数用于清理 CUDA 内存中的缓存。在 PyTorch 中,当一个张量(Tensor)被释放后,它所占用的显存并不会立即返回给操作系统,而是留在 PyTorch 的缓存中,以便后续的张量可以重用这些显存。# torch.cuda.empty_cache() 会释放这些未被引用的缓存显存,使得它们可以被其他程序使用,或者在 PyTorch 中被重新分配。# 使用场景 :# 当 GPU 显存紧张,需要清理未被使用的显存时。# 在模型训练或推理的循环中,为了确保有足够的连续显存,可以在每个循环结束后调用此函数。# 在加载新模型或进行大规模数据处理前,释放不再需要的显存。# 注意事项 :# torch.cuda.empty_cache() 不会影响当前分配的显存,只有未被引用的显存才会被释放。# 频繁调用 torch.cuda.empty_cache() 可能会导致性能开销,因为它可能会导致 PyTorch 失去对某些内存块的重用。# 在大多数情况下,PyTorch 会自动管理显存,只有在特定情况下(如显存不足或调试)才需要手动调用此函数。# 清空 CUDA 缓存,以释放未使用的显存。torch.cuda.empty_cache()# 返回包含所有评估结果的列表 results 。包括 参数数量 、 GFLOPs 、 显存使用量 、 前向传播时间 、 反向传播时间 、 输入和输出的形状 。return results
# 这个函数可以用于评估 PyTorch 模型的性能,特别是在优化和选择模型时非常有用。
11.def is_parallel(model):
# 这段代码定义了一个名为 is_parallel 的函数,其作用是检查传入的 PyTorch 模型是否是并行模型,即是否是 DataParallel 或 DistributedDataParallel 的实例。
# 定义了一个名为 is_parallel 的函数,它接受一个参数。
# 1.model :这个参数可以是任何 PyTorch 模型。
def is_parallel(model):# Returns True if model is of type DP or DDP 如果模型是 DP 或 DDP 类型,则返回 True 。# 函数的主体是一个返回语句,它检查传入的 model 是否是 DataParallel 或 DistributedDataParallel 的实例。# type(model) 获取模型的类型。# in 关键字用于检查 model 的类型是否在元组 ((nn.parallel.DataParallel, nn.parallel.DistributedDataParallel)) 中。# 如果 model 是这两种并行模型之一,函数返回 True ;否则返回 False 。return type(model) in (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel)
# is_parallel 函数提供了一种快速检查模型是否被并行化的方法,这对于确定模型是否需要被转换回单 GPU 或 CPU 模型非常有用。这个函数通过比较模型的类型与 PyTorch 中定义的并行模型类型来实现其功能。
12.def de_parallel(model):
# 这段代码定义了一个名为 de_parallel 的函数,其作用是将一个可能处于并行状态(例如使用 PyTorch 的 DataParallel 或 DistributedDataParallel 包装过的模型)转换回单个 GPU 或 CPU 上的模型。
# 定义了一个名为 de_parallel 的函数,它接受一个参数。
# 1.model :这个参数可以是任何 PyTorch 模型。
def de_parallel(model):# De-parallelize a model: returns single-GPU model if model is of type DP or DDP 取消并行化模型:如果模型类型为 DP 或 DDP,则返回单 GPU 模型。# 函数的主体包含一个条件表达式。# is_parallel(model) :这是一个未在代码段中定义的函数调用,它应该检查传入的 model 是否是 DataParallel 或 DistributedDataParallel 的实例。如果是,这个函数返回 True 。# 如果 model 是并行模型, 将返回原始的、未并行化的模型 model.module 。# 如果 model 不是并行模型,直接返回 model 。return model.module if is_parallel(model) else model
# de_parallel 函数用于处理可能被 DataParallel 或 DistributedDataParallel 包装过的模型,将其转换回原始的单 GPU 或 CPU 模型。这在某些情况下很有用,比如当你需要访问模型的特定部分或者在单个 GPU 上进行推理时。
13.def initialize_weights(model):
# 这段代码定义了一个名为 initialize_weights 的函数,它用于初始化神经网络模型中的权重和偏置。
# 这是 initialize_weights 函数的定义,它接受一个参数.
# 1.model :即要初始化权重的神经网络模型。
def initialize_weights(model):# 遍历模型中的所有模块。 model.modules() 返回模型中所有模块的迭代器,包括子模块。for m in model.modules():# 获取当前模块 m 的类型。t = type(m)# 如果模块 m 是 nn.Conv2d 类型(即卷积层),则不执行任何操作。# 这里有一个被注释掉的 kaiming_normal_ 初始化方法,它通常用于初始化卷积层的权重,但由于 pass 关键字,这部分代码不会执行。if t is nn.Conv2d:pass # nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')# 如果模块 m 是 nn.BatchNorm2d 类型(即批量归一化层),则设置其 eps 和 momentum 参数。 eps 是批量归一化中的小值,用于防止除以零; momentum 是运行均值和方差的动量。elif t is nn.BatchNorm2d:m.eps = 1e-3m.momentum = 0.03# 如果模块 m 是激活函数(Hardswish、LeakyReLU、ReLU、ReLU6 或 SiLU)中的一种,则设置其 inplace 参数为 True 。 inplace 参数为 True 表示激活函数的计算会在原地进行,这样可以减少内存消耗。elif t in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU]:m.inplace = True
# 这个函数的目的是为模型中的不同层设置适当的初始化参数。在神经网络训练的初期,合适的权重初始化对于模型的收敛速度和最终性能至关重要。通过调整批量归一化层的参数和激活函数的 inplace 属性,可以优化模型的性能和效率。
14.def find_modules(model, mclass=nn.Conv2d):
# 这段代码定义了一个名为 find_modules 的函数,其目的是在给定的模型中查找与指定模块类 mclass 匹配的层,并返回这些层的索引。
# 定义了一个名为 find_modules 的函数,它接受两个参数。
# 1.model :一个模型对象。
# 2.mclass :一个模块类,默认值为 nn.Conv2d ,即 PyTorch 中的二维卷积层。
def find_modules(model, mclass=nn.Conv2d):# Finds layer indices matching module class 'mclass' 查找与模块类“mclass”匹配的层索引。# 这行代码是一个列表推导式,用于生成一个包含索引的列表,这些索引对应的层是 model.module_list 中与 mclass 类型匹配的层。# enumerate(model.module_list) 会遍历 model.module_list 中的每个模块 m 并提供一个索引 i 和模块对象 m 。 if isinstance(m, mclass) 检查每个模块 m 是否是 mclass 类型的实例。如果是,索引 i 会被包含在最终返回的列表中。return [i for i, m in enumerate(model.module_list) if isinstance(m, mclass)]
# find_modules 函数通过列表推导式遍历模型的 module_list 属性,查找所有与指定模块类 mclass 匹配的层,并返回这些层的索引列表。默认情况下,它会查找所有二维卷积层( nn.Conv2d )。这个函数可以用于快速定位模型中的特定类型的层,例如在模型分析或修改时。
15.def sparsity(model):
# 这段代码定义了一个名为 sparsity 的函数,用于计算并返回给定模型的全局稀疏度。
# 定义了一个名为 sparsity 的函数,它接受一个参数。
# 1.model :这个参数应该是一个 PyTorch 模型。
def sparsity(model):# Return global model sparsity 返回全局模型稀疏度。# 初始化两个变量 a 和 b ,它们将分别用于存储模型中 所有参数的总数 和 所有参数中为零的元素数 。a, b = 0, 0# 开始一个循环,遍历模型的每个参数 p 。 model.parameters() 返回模型中所有参数的迭代器。for p in model.parameters():# 对于每个参数 p , p.numel() 返回参数中元素的总数,并将这个值累加到变量 a 。a += p.numel()# 对于每个参数 p , (p == 0) 创建一个与 p 形状相同、元素为布尔值的张量,其中值为零的元素对应 True 。 .sum() 方法计算这个布尔张量中 True 的数量(即参数中为零的元素数),并将这个值累加到变量 b 。b += (p == 0).sum()# 在循环结束后,函数返回变量 b 除以变量 a 的结果,即模型中为零的参数元素数占总参数元素数的比例,这就是模型的 全局稀疏度 。return b / a
# sparsity 函数通过遍历模型的所有参数,计算总参数数和为零的参数数,然后计算这两个数的比值来得到模型的全局稀疏度。这个度量可以用来评估模型参数的稀疏性,即模型中有多少比例的参数是未使用的(值为零)。这对于理解模型的复杂性和可能的优化空间很有帮助。
16.def prune(model, amount=0.3):
# 这段代码定义了一个名为 prune 的函数,用于将给定的 PyTorch 模型剪枝到请求的全局稀疏度。
# 定义了一个名为 prune 的函数,它接受两个参数。
# 1.model :一个 PyTorch 模型。
# 2.amount :一个浮点数,默认值为 0.3,表示要剪枝到的全局稀疏度比例。
def prune(model, amount=0.3):# Prune model to requested global sparsity 修剪模型以达到要求的全局稀疏性。# 导入 PyTorch 的 torch.nn.utils.prune 模块,并将其重命名为 prune ,以便在函数中使用。import torch.nn.utils.prune as prune# 开始一个循环,遍历模型的每个模块 m 及其名称 name 。 model.named_modules() 返回模型中所有模块的名称和模块对象的迭代器。for name, m in model.named_modules():# 检查每个模块 m 是否是 nn.Conv2d 类型的实例,即是否为二维卷积层。if isinstance(m, nn.Conv2d):# torch.nn.utils.prune.l1_unstructured(module, name, amount, **kwargs)# prune.l1_unstructured 是 PyTorch 中的一个函数,用于对模型的权重进行 L1 无结构剪枝。这种剪枝方式会随机选择一定比例的权重并将它们设置为零,而不考虑它们在张量中的结构或位置。# 参数说明 :# module :要剪枝的 PyTorch 模块,通常是 nn.Conv2d 或 nn.Linear 等包含权重参数的层。# name :模块中要剪枝的参数名称,通常是 'weight' 。# amount :剪枝的比例,取值范围在 0 到 1 之间,表示要剪枝的权重比例。# **kwargs :其他可选参数,例如 n=10, dim=None 等,用于控制剪枝的细节。# 返回值 :# 无返回值,但会直接修改传入的 module ,将其权重参数中的一部分设置为零。# 注意事项 :# 剪枝操作是不可逆的,一旦权重被剪枝,它们将永久地从模型中移除。# 在进行剪枝之前,确保模型的权重参数是可训练的(即 requires_grad=True )。# 剪枝后,可能需要重新训练模型以恢复或提高性能,因为剪枝可能会影响模型的精度。# prune.l1_unstructured 函数是 PyTorch 提供的一种方便的剪枝工具,可以帮助研究人员和开发人员在模型优化和压缩时减少模型的参数数量。# 如果模块是二维卷积层,使用 prune.l1_unstructured 方法对该层的权重进行 L1 无结构剪枝。 name='weight' 指定要剪枝的参数名称(这里是权重), amount=amount 指定剪枝的比例。prune.l1_unstructured(m, name='weight', amount=amount) # prune# torch.nn.utils.prune.remove(module, name)# prune.remove 是 PyTorch 中的一个函数,用于将剪枝操作永久化,即将剪枝后的结果重新参数化,使得剪枝效果成为模型的一部分,而不是通过掩码来临时应用。这个函数会移除与剪枝相关的掩码和原始参数,只保留剪枝后的参数。# 参数说明 :# module :要移除剪枝的 PyTorch 模块,通常是 nn.Conv2d 或 nn.Linear 等包含权重参数的层。# name :模块中要移除剪枝的参数名称,通常是 'weight' 或 'bias' 。# 返回值 :无返回值,但会直接修改传入的 module ,移除剪枝相关的掩码和原始参数。# 注意事项 :# 在使用 prune.remove 之前,确保已经完成了所有需要的剪枝操作,因为这一步会移除剪枝的原始参数和掩码,无法恢复。# 移除剪枝重新参数化后,模型的参数将被永久修改,这意味着剪枝效果将被保留,但无法通过简单地移除掩码来撤销剪枝。# 移除剪枝重新参数化后,模型的 state_dict 将不再包含剪枝相关的掩码和原始参数,这可能影响模型的序列化和保存。# prune.remove 函数是 PyTorch 剪枝流程中的一个关键步骤,用于将剪枝效果永久化,使得模型可以被进一步使用或部署,而不需要额外的剪枝掩码。# 剪枝后,使用 prune.remove 方法将剪枝操作永久化,移除权重中被剪枝的部分。prune.remove(m, 'weight') # make permanent# 在剪枝操作完成后,使用 LOGGER 记录一条信息,说明模型已经被剪枝到的全局稀疏度。 sparsity(model) 调用之前定义的 sparsity 函数来计算模型的全局稀疏度, :.3g 指定格式化为保留三位有效数字的浮点数。LOGGER.info(f'Model pruned to {sparsity(model):.3g} global sparsity')
# prune 函数通过遍历模型中的所有二维卷积层,并对每个卷积层的权重进行 L1 无结构剪枝,以达到请求的全局稀疏度。剪枝操作被永久化后,函数记录模型的最终稀疏度。这个函数可以用来减少模型的参数数量,从而减少模型的复杂性和计算需求,同时可能提高模型的泛化能力。# 剪枝对模型检测精度的影响 :
# 剪枝是一种有效的技术,用于减少深度神经网络(DNNs)的模型大小和推理时间,以便于在资源受限的环境中部署。文献中多次报告称,通过剪枝可以显著减少神经网络的大小并提高推理效率,且对性能的影响很小甚至可以忽略不计。
# 某些研究表明,剪枝可以在保持模型准确性的同时实现显著的模型压缩。例如,一项研究通过剪枝方法在不同数据集上实现了超过80%的计算复杂度降低和超过90%的参数减少,同时保持了模型的准确性。
# 另一项研究提出了一种基于不同尺度层重要性的滤波器协同贡献剪枝方法,该方法在不牺牲模型准确性的情况下实现了剪枝,显著减少了模型的参数数量和计算复杂度。
# 剪枝方法的优化 :
# 有研究提出了一种名为Rigorous Gradation Pruning (RGP)的方法,该方法使用一阶泰勒近似来评估滤波器的重要性,实现了精确剪枝。这种方法包括对每一层的重要性进行迭代重新评估,以保护关键层,确保有效的检测性能。在GTSDB、Seaships和COCO数据集上的测试表明,RGP在保持准确性的同时实现了显著的YOLOv8压缩。
# 还有研究提出了一种结合模型剪枝和知识蒸馏的方法,通过L1正则化稀疏性和瘦身剪枝方法,主要在训练期间对批量归一化(BN)层的缩放因子应用稀疏正则化。然后通过微调和知识蒸馏将教师模型的丰富先验知识迁移到剪枝网络中,以提高剪枝网络的检测精度。
# 剪枝的挑战和限制 :
# 尽管剪枝可以减少模型的参数数量和计算需求,但过度剪枝可能会导致性能显著下降。因此,如何平衡模型压缩和检测精度是一个挑战。
# 一些研究表明,通过剪枝可以实现高达99%的模型压缩率,同时保持与原始模型相似的性能。
# 总结 :
# 剪枝操作可以减少模型的大小和计算需求,而且如果剪枝策略得当,对模型的检测精度影响可以很小,甚至可以忽略不计。
# 通过精心设计的剪枝算法,可以在保持高检测精度的同时实现显著的模型压缩。
# 然而,剪枝的比例和方法需要仔细选择,以避免过度剪枝导致性能下降。
# 因此,对模块进行剪枝操作是否会降低模型的检测精度取决于剪枝的方法和比例。合理的剪枝策略可以在减少模型复杂度的同时保持或几乎不影响模型的性能。
17.def fuse_conv_and_bn(conv, bn):
# 这段代码定义了一个名为 fuse_conv_and_bn 的函数,它用于融合卷积层( Conv2d )和批量归一化层( BatchNorm2d )成一个单一的卷积层。这种融合可以减少模型的参数数量和计算量,同时可能提高推理速度。
# 这是 fuse_conv_and_bn 函数的定义,它接受两个参数。
# 1.conv :一个 nn.Conv2d 卷积层。
# 2.bn :一个 nn.BatchNorm2d 批量归一化层。
def fuse_conv_and_bn(conv, bn):# Fuse Conv2d() and BatchNorm2d() layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/# 创建一个新的卷积层 fusedconv ,它的参数与原始卷积层 conv 相同,但增加了 bias=True 以包含偏置项。 requires_grad_(False) 确保融合后的卷积层的权重不会在训练中更新。 to(conv.weight.device) 确保新卷积层的权重与原始卷积层的权重在同一设备上。fusedconv = nn.Conv2d(conv.in_channels,conv.out_channels,kernel_size=conv.kernel_size,stride=conv.stride,padding=conv.padding,dilation=conv.dilation,groups=conv.groups,bias=True).requires_grad_(False).to(conv.weight.device)# 这段代码是在执行卷积层和批量归一化层权重的融合过程。# Prepare filters# 准备卷积层的权重。# conv.weight 是原始卷积层的权重。# clone() 方法用于创建权重的副本,以避免直接修改原始权重。# view(conv.out_channels, -1) 将权重重塑为一个矩阵,其中行数等于卷积层的输出通道数,列数是所有输入通道和卷积核大小的乘积。w_conv = conv.weight.clone().view(conv.out_channels, -1)# torch.diag(input, diagonal=0, *, out=None) → Tensor# torch.diag() 是 PyTorch 中的一个函数,它用于从给定的矩阵中提取对角线元素,或者构造一个以给定对角线元素为值的对角矩阵。这个函数对于矩阵分解和转换等操作非常重要。# 参数 :# input :输入张量,可以是一维张量(向量)或二维张量(矩阵)。# diagonal :整数,可选参数,默认为0。用于指定要提取或构造的对角线 :# diagonal=0 :主对角线(默认)。# diagonal>0 :主对角线上方的对角线。# diagonal<0 :主对角线下方的对角线。# out :输出张量,可选参数。如果提供,结果将被存储在这个张量中。# 返回值 :# 返回一个张量 :# 如果 input 是一维张量,返回的是以 input 的元素为对角线元素的二维方阵。# 如果 input 是二维张量,返回的是包含 input 对角线元素的一维张量。# torch.diag() 函数是处理矩阵对角线元素的有力工具,它可以用来创建对角矩阵,也可以用来提取矩阵中的对角线元素。# 准备批量归一化层的权重。# bn.weight 是批量归一化层的可学习权重。# bn.eps 是批量归一化层的epsilon值,用于防止除以零。# bn.running_var 是批量归一化层的运行方差。# torch.sqrt(bn.eps + bn.running_var) 计算每个通道的标准差。# div() 方法将批量归一化层的权重除以标准差,得到每个通道的缩放因子。# torch.diag() 方法创建一个对角矩阵,对角线上的元素是缩放因子。w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps + bn.running_var)))# tensor.copy_(src, non_blocking=False)# copy_() 是 PyTorch 中的一个方法,用于将源张量(Tensor)的数据复制到目标张量中。这个方法在原地(in-place)操作目标张量,即直接修改目标张量的内容,而不是创建一个新的张量。# 参数 :# src :源张量,其数据将被复制到目标张量中。# non_blocking :一个布尔值,指示是否使用非阻塞复制。默认为 False 。如果设置为 True ,复制操作将尽可能非阻塞地执行,这在多线程环境中可能有助于提高性能。# 功能描述 :# copy_() 方法用于将 src 张量的数据复制到调用该方法的张量( self )中。如果 self 和 src 张量的形状不同, self 张量的形状将被调整以匹配 src 张量的形状。如果 self 张量已经有数据,这些数据将被 src 张量的数据覆盖。# copy_() 方法是原地操作,这意味着它直接修改目标张量,而不是返回一个新的张量。这在需要节省内存或避免额外内存分配时非常有用。# 融合权重。# torch.mm(w_bn, w_conv) 执行矩阵乘法,将 批量归一化层的对角矩阵 与 卷积层的权重矩 阵相乘,得到融合后的权重。# view(fusedconv.weight.shape) 将融合后的权重重塑为新卷积层权重的形状。# fusedconv.weight.copy_() 方法将融合后的权重复制到新卷积层的权重中。fusedconv.weight.copy_(torch.mm(w_bn, w_conv).view(fusedconv.weight.shape))# 这个过程的物理意义是将批量归一化层的效果直接融合到卷积层的权重中,从而在不改变网络结构的情况下,消除了批量归一化层的需要。这样做的好处是可以减少模型的参数数量和计算量,同时可能提高推理速度。融合后的卷积层可以直接应用到输入数据上,而不需要先经过批量归一化层。# 这段代码是在融合卷积层( conv )和批量归一化层( bn )的过程中,准备空间偏置( spatial bias )的部分。# Prepare spatial bias# 准备卷积层的偏置。# 如果卷积层没有偏置( conv.bias 为 None ),则创建一个大小为卷积层输出通道数的零向量作为偏置。 如果卷积层已经有偏置,则直接使用它。b_conv = torch.zeros(conv.weight.size(0), device=conv.weight.device) if conv.bias is None else conv.bias# 计算批量归一化层的偏置。# bn.bias 是批量归一化层的偏置项。# bn.weight 是批量归一化层的可学习权重。# bn.running_mean 和 bn.running_var 分别是批量归一化层的 运行均值 和 方差 。# bn.eps 是一个很小的数,用于防止除以零。# torch.sqrt(bn.running_var + bn.eps) 计算 标准差 。# bn.weight.mul(bn.running_mean) 计算 每个通道 的 缩放均值 。# div() 方法将 缩放均值 除以 标准差 ,得到每个通道的 偏置调整项 。# 最终, b_bn 表示批量归一化层的偏置项,考虑了均值和方差的调整。b_bn = bn.bias - bn.weight.mul(bn.running_mean).div(torch.sqrt(bn.running_var + bn.eps))# 融合偏置。# w_bn 是批量归一化层的对角矩阵,其中包含每个通道的缩放因子。# b_conv.reshape(-1, 1) 将卷积层的偏置向量重塑为列向量。# torch.mm(w_bn, b_conv.reshape(-1, 1)) 计算批量归一化层的偏置调整项与卷积层偏置的矩阵乘法。# reshape(-1) 将结果重塑为一维向量。# + b_bn 将批量归一化层的偏置项加到结果上。# fusedconv.bias.copy_() 将融合后的偏置复制到新卷积层的偏置中。fusedconv.bias.copy_(torch.mm(w_bn, b_conv.reshape(-1, 1)).reshape(-1) + b_bn)# 这个过程的物理意义是将批量归一化层的偏置调整项融合到卷积层的偏置中,从而在不改变网络结构的情况下,消除了批量归一化层的需要。这样做的好处是可以减少模型的参数数量和计算量,同时可能提高推理速度。融合后的卷积层可以直接应用到输入数据上,而不需要先经过批量归一化层。# 返回融合后的卷积层。return fusedconv
# fuse_conv_and_bn 函数通过融合卷积层和批量归一化层,减少了模型的参数数量和计算量,这在部署模型到资源受限的环境时特别有用。此外,这种方法还可以减少模型的内存占用,提高推理速度。
18.def model_info(model, verbose=False, imgsz=640):
# 这段代码定义了一个名为 model_info 的函数,它用于打印模型的详细信息,包括层数、参数数量、可训练参数数量以及浮点运算次数(FLOPs)。
# 这是 model_info 函数的定义,它接受三个参数。
# 1.model :要分析的模型。
# 2.verbose :一个布尔值,表示是否打印详细的层信息,默认为 False 。
# 3.imgsz :输入图像的尺寸,默认为640。可以是整数或列表,例如 img_size=640 或 img_size=[640, 320] 。
def model_info(model, verbose=False, imgsz=640):# Model information. img_size may be int or list, i.e. img_size=640 or img_size=[640, 320] 模型信息。img_size 可以是 int 或 list,例如 img_size=640 或 img_size=[640, 320]。# torch.Tensor.numel()# numel() 函数是 PyTorch 中的一个方法,用于返回一个张量(tensor)中元素的总数。这个方法是 torch.Tensor 类的一个实例方法,意味着它可以在任何 torch.Tensor 对象上调用。# 参数 :没有参数# 返回值 :# 返回一个整数,表示张量中的元素总数。# numel() 方法在处理张量时非常有用,尤其是当你需要知道张量的大小而不需要知道具体的维度大小时。这个函数返回的是张量中所有元素的总数,不考虑它的维度结构。# 计算模型中所有参数的总数。n_p = sum(x.numel() for x in model.parameters()) # number parameters# 计算模型中所有可训练参数的总数。n_g = sum(x.numel() for x in model.parameters() if x.requires_grad) # number gradients# 如果 verbose 为真,打印每一层的详细信息。if verbose:# 打印标题行。print(f"{'layer':>5} {'name':>40} {'gradient':>9} {'parameters':>12} {'shape':>20} {'mu':>10} {'sigma':>10}")# 遍历模型的所有参数。for i, (name, p) in enumerate(model.named_parameters()):# 清理参数名称,移除不必要的前缀。name = name.replace('module_list.', '')# 打印每一层的 索引 、 名称 、 是否需要梯度 、 参数数量 、 形状 、 均值 和 标准差 。print('%5g %40s %9s %12g %20s %10.3g %10.3g' %(i, name, p.requires_grad, p.numel(), list(p.shape), p.mean(), p.std()))# 尝试计算模型的浮点运算次数(FLOPs)。try: # FLOPs# next(iterator[, default])# 在Python中, next() 函数用于获取迭代器的下一个元素。它是一个内置函数,可以用于任何实现了迭代器协议(即具有 __iter__() 和 __next__() 方法)的对象。# 参数 :# iterator :一个迭代器对象,例如列表、元组、字典、集合、字符串或任何实现了 __iter__() 和 __next__() 方法的自定义对象。# default :可选参数,用于指定当迭代器耗尽时返回的值。如果不提供此参数,当迭代器耗尽时, next() 将抛出 StopIteration 异常。# 返回值 :# 返回迭代器的下一个元素。# 获取模型的第一个参数。p = next(model.parameters())# 获取模型的最大步长。stride = max(int(model.stride.max()), 32) if hasattr(model, 'stride') else 32 # max stride# 创建一个空的输入图像张量,用于计算FLOPs。im = torch.empty((1, p.shape[1], stride, stride), device=p.device) # input image in BCHW format# 使用 thop 库计算模型的FLOPs,并将其转换为十亿(Giga)规模。flops = thop.profile(deepcopy(model), inputs=(im,), verbose=False)[0] / 1E9 * 2 # stride GFLOPs# 确保 imgsz 是一个列表。imgsz = imgsz if isinstance(imgsz, list) else [imgsz, imgsz] # expand if int/float# 计算并格式化FLOPs信息。# 这行代码是 Python 中的一个 f-string(格式化字符串字面量),用于构建一个包含浮点数的字符串,该浮点数表示模型的浮点运算次数(FLOPs)针对特定输入图像尺寸的计算结果。# f'...' :这是 f-string 的开始,允许在字符串中直接嵌入表达式的值。# {flops * imgsz[0] / stride * imgsz[1] / stride:.1f} :这是 f-string 中的一个表达式,它会被求值并格式化为浮点数。# flops :模型的浮点运算次数,以十亿(Giga)为单位。# imgsz :一个列表,包含输入图像的宽度和高度。# stride :模型的最大步长。# imgsz[0] 和 imgsz[1] :分别表示输入图像的宽度和高度。# / stride :将图像的宽度和高度分别除以步长,以计算在每个方向上的 网格点数量 。# * :将 flops 与图像尺寸调整后的值相乘,以得到 针对特定输入尺寸 的 FLOPs。# .1f :这是格式化指令,表示浮点数应该保留一位小数。# GFLOPs :是 "Giga Floating Point Operations" 的缩写,表示十亿次浮点运算。# , :在 f-string 外部的逗号用于在前面的文本(如果有的话)和计算结果之间添加一个逗号和空格。# 注释 # 640x640 GFLOPs 解释了这行代码的目的,即计算并格式化模型在处理 640x640 像素图像时的 FLOPs。# 这行代码计算模型处理特定尺寸图像时的 FLOPs,并将其格式化为一个带有一位小数的字符串,以便在日志或其他输出中显示。fs = f', {flops * imgsz[0] / stride * imgsz[1] / stride:.1f} GFLOPs' # 640x640 GFLOPs# 如果计算FLOPs时出现异常,设置 fs 为空字符串。except Exception:fs = ''# 获取模型的名称。name = Path(model.yaml_file).stem.replace('yolov5', 'YOLOv5') if hasattr(model, 'yaml_file') else 'Model'# 记录模型的摘要信息,包括 层数 、 参数数量 、 可训练参数数量 和 FLOPs 。LOGGER.info(f"{name} summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}")
# 这个函数提供了一种快速获取模型概览的方式,帮助开发者在模型开发和优化过程中做出更明智的决策。通过打印模型信息,开发者可以验证模型是否按照预期构建,并且可以监控模型的参数数量和计算复杂度。
19.def scale_img(img, ratio=1.0, same_shape=False, gs=32):
# 这段代码定义了一个名为 scale_img 的函数,它用于按照给定的比例 ratio 缩放图像,同时确保缩放后的图像尺寸是 gs (grid size,网格尺寸)的倍数。这个函数可以用于数据增强或在训练神经网络时调整图像尺寸。
# 定义了 scale_img 函数,它接受以下参数。
# 1.img :输入的图像张量,形状为 (bs, 3, y, x) ,其中 bs 是批次大小, 3 是颜色通道数, y 和 x 是图像的高度和宽度。
# 2.ratio :缩放比例,默认为 1.0 ,即不缩放。
# 3.same_shape :布尔值,指示是否保持原始图像的形状。如果为 True ,则不进行裁剪或填充。
# 4.gs :网格尺寸,默认为 32 ,用于确保缩放后的图像尺寸是 gs 的倍数。
def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416)# Scales img(bs,3,y,x) by ratio constrained to gs-multiple 按限制为 gs-multiple 的比例缩放 img(bs,3,y,x)。# 如果缩放比例为 1.0 ,则直接返回原始图像。if ratio == 1.0:return img# 获取图像的 高度 和 宽度 。h, w = img.shape[2:]# 计算缩放后的新尺寸。s = (int(h * ratio), int(w * ratio)) # new size# 使用双线性插值方法对图像进行缩放。img = F.interpolate(img, size=s, mode='bilinear', align_corners=False) # resize# 如果 same_shape 为 False ,则进行裁剪或填充以确保图像尺寸是 gs 的倍数。if not same_shape: # pad/crop img# 计算调整后的图像尺寸,确保它们是 gs 的倍数。h, w = (math.ceil(x * ratio / gs) * gs for x in (h, w))# 对图像进行填充,使其宽度和高度分别等于调整后的尺寸,填充值为 0.447 (这是 ImageNet 数据集的均值)。return F.pad(img, [0, w - s[1], 0, h - s[0]], value=0.447) # value = imagenet mean
# 这个函数的作用是将输入图像按照指定的比例进行缩放,并确保缩放后的图像尺寸是 gs 的倍数,这对于某些神经网络结构(如 EfficientDet)来说是必要的,因为它们要求输入图像的尺寸是特定的网格尺寸的倍数。填充值 0.447 是 ImageNet 数据集的均值,用于保持数据的分布一致性。
20.def copy_attr(a, b, include=(), exclude=()):
# 这段代码定义了一个名为 copy_attr 的函数,它用于将一个对象 b 的属性复制到另一个对象 a 。这个函数提供了灵活的方式来选择性地复制属性,可以指定只复制某些属性( include ),或者排除某些属性( exclude )。
# 函数定义。
# 1.a : 目标对象,将要接收属性的对象。
# 2.b : 源对象,从中复制属性的对象。
# 3.include : 一个元组或列表,包含要复制的属性名称。如果为空,则复制所有属性,除了被 exclude 排除的。
# 4.exclude : 一个元组或列表,包含要排除的属性名称。函数实现
def copy_attr(a, b, include=(), exclude=()):# Copy attributes from b to a, options to only include [...] and to exclude [...]# 遍历源对象 b 的所有属性。 __dict__ 是一个字典,包含了对象的所有属性。for k, v in b.__dict__.items():# 检查当前属性 k 是否应该被排除 :# 如果 include 不为空且 k 不在 include 中,则排除。# 如果 k 以 _ 开头(通常是私有属性或特殊方法),则排除。# 如果 k 在 exclude 列表中,则排除。if (len(include) and k not in include) or k.startswith('_') or k in exclude:continue# 如果属性 k 没有被排除,则使用 setattr(a, k, v) 将其复制到目标对象 a 。else:setattr(a, k, v)
# copy_attr 函数提供了一个方便的方式来在两个对象之间复制属性,允许用户自定义要复制的属性列表,以及要排除的属性列表。这个函数在处理对象属性迁移时非常有用,尤其是在需要合并多个对象状态或配置时。
21.def smart_optimizer(model, name='Adam', lr=0.001, momentum=0.9, decay=1e-5):
# 这段代码定义了一个名为 smart_optimizer 的函数,用于创建一个智能优化器,根据模型的不同参数分配不同的学习率和衰减率。
# 定义了一个名为 smart_optimizer 的函数,接受以下参数 :
# 1.model :要优化的 PyTorch 模型。
# 2.name :优化器的名称,默认为 'Adam' 。
# 3.lr :学习率,默认为 0.001 。
# 4.momentum :动量参数,默认为 0.9 。
# 5.decay :权重衰减率,默认为 1e-5 。
def smart_optimizer(model, name='Adam', lr=0.001, momentum=0.9, decay=1e-5):# YOLOv5 3-param group optimizer: 0) weights with decay, 1) weights no decay, 2) biases no decay YOLOv5 3 参数组优化器:0)权重衰减,1)权重不衰减,2)偏差不衰减。# 初始化三个空列表 g ,用于存储不同的参数组,这些参数组将被分配到不同的优化器参数组中。g = [], [], [] # optimizer parameter groups 优化器参数组。# 创建一个元组 bn ,包含所有 PyTorch 命名空间中名称包含 'Norm' 的模块类,例如 BatchNorm2d 。bn = tuple(v for k, v in nn.__dict__.items() if 'Norm' in k) # normalization layers, i.e. BatchNorm2d()#for v in model.modules():# for p_name, p in v.named_parameters(recurse=0):# if p_name == 'bias': # bias (no decay)# g[2].append(p)# elif p_name == 'weight' and isinstance(v, bn): # weight (no decay)# g[1].append(p)# else:# g[0].append(p) # weight (with decay)# 遍历模型中的所有模块。for v in model.modules():# 如果模块有 bias 属性,并且 bias 是一个 nn.Parameter 实例,则将 bias 添加到第三个参数组 g[2] 中,这个组中的参数不会应用权重衰减。if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter): # bias (no decay)g[2].append(v.bias)# 如果模块是归一化层(例如 BatchNorm2d ),则将模块的权重添加到第二个参数组 g[1] 中,这个组中的参数不会应用权重衰减。if isinstance(v, bn): # weight (no decay)g[1].append(v.weight)# 如果模块有 weight 属性,并且 weight 是一个 nn.Parameter 实例,则将 weight 添加到第一个参数组 g[0] 中,这个组中的参数会应用权重衰减。elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter): # weight (with decay)g[0].append(v.weight)# 接下来的代码块处理模型中可能存在的自定义属性(如 im , ia , im2 , ia2 等),这些属性可能包含需要特别处理的参数。这些代码检查这些属性是否存在,并且是否包含 implicit 参数,如果存在,则将它们添加到相应的参数组中。if hasattr(v, 'im'):if hasattr(v.im, 'implicit'): g[1].append(v.im.implicit)else:for iv in v.im:g[1].append(iv.implicit)if hasattr(v, 'ia'):if hasattr(v.ia, 'implicit'): g[1].append(v.ia.implicit)else:for iv in v.ia:g[1].append(iv.implicit)if hasattr(v, 'im2'):if hasattr(v.im2, 'implicit'): g[1].append(v.im2.implicit)else:for iv in v.im2:g[1].append(iv.implicit)if hasattr(v, 'ia2'):if hasattr(v.ia2, 'implicit'): g[1].append(v.ia2.implicit)else:for iv in v.ia2:g[1].append(iv.implicit)if hasattr(v, 'im3'):if hasattr(v.im3, 'implicit'): g[1].append(v.im3.implicit)else:for iv in v.im3:g[1].append(iv.implicit)if hasattr(v, 'ia3'):if hasattr(v.ia3, 'implicit'): g[1].append(v.ia3.implicit)else:for iv in v.ia3:g[1].append(iv.implicit)if hasattr(v, 'im4'):if hasattr(v.im4, 'implicit'): g[1].append(v.im4.implicit)else:for iv in v.im4:g[1].append(iv.implicit)if hasattr(v, 'ia4'):if hasattr(v.ia4, 'implicit'): g[1].append(v.ia4.implicit)else:for iv in v.ia4:g[1].append(iv.implicit)if hasattr(v, 'im5'):if hasattr(v.im5, 'implicit'): g[1].append(v.im5.implicit)else:for iv in v.im5:g[1].append(iv.implicit)if hasattr(v, 'ia5'):if hasattr(v.ia5, 'implicit'): g[1].append(v.ia5.implicit)else:for iv in v.ia5:g[1].append(iv.implicit)if hasattr(v, 'im6'):if hasattr(v.im6, 'implicit'): g[1].append(v.im6.implicit)else:for iv in v.im6:g[1].append(iv.implicit)if hasattr(v, 'ia6'):if hasattr(v.ia6, 'implicit'): g[1].append(v.ia6.implicit)else:for iv in v.ia6:g[1].append(iv.implicit)if hasattr(v, 'im7'):if hasattr(v.im7, 'implicit'): g[1].append(v.im7.implicit)else:for iv in v.im7:g[1].append(iv.implicit)if hasattr(v, 'ia7'):if hasattr(v.ia7, 'implicit'): g[1].append(v.ia7.implicit)else:for iv in v.ia7:g[1].append(iv.implicit)# 根据优化器名称创建不同的优化器实例。如果 name 是 'Adam' ,则创建一个 Adam 优化器实例,使用第三个参数组 g[2] ,并将动量参数 momentum 设置为 beta1 。if name == 'Adam':optimizer = torch.optim.Adam(g[2], lr=lr, betas=(momentum, 0.999)) # adjust beta1 to momentum# 如果 name 是 'AdamW' ,则创建一个 AdamW 优化器实例。elif name == 'AdamW':optimizer = torch.optim.AdamW(g[2], lr=lr, betas=(momentum, 0.999), weight_decay=0.0, amsgrad=True)# 如果 name 是 'RMSProp' ,则创建一个 RMSProp 优化器实例。elif name == 'RMSProp':optimizer = torch.optim.RMSprop(g[2], lr=lr, momentum=momentum)# 如果 name 是 'SGD' ,则创建一个 SGD 优化器实例。elif name == 'SGD':optimizer = torch.optim.SGD(g[2], lr=lr, momentum=momentum, nesterov=True)# 如果 name 是 'LION' ,则创建一个 LION 优化器实例。elif name == 'LION':optimizer = Lion(g[2], lr=lr, betas=(momentum, 0.99), weight_decay=0.0)# 如果 name 不是支持的优化器名称,则抛出一个 NotImplementedError 异常。else:raise NotImplementedError(f'Optimizer {name} not implemented.') # 优化器 {name} 未实现。# 将第一个参数组 g[0] 添加到优化器中,并应用权重衰减。optimizer.add_param_group({'params': g[0], 'weight_decay': decay}) # add g0 with weight_decay# 将第二个参数组 g[1] 添加到优化器中,不应用权重衰减。optimizer.add_param_group({'params': g[1], 'weight_decay': 0.0}) # add g1 (BatchNorm2d weights)# 记录优化器的配置信息,包括优化器类型、学习率以及不同参数组的数量和衰减设置。LOGGER.info(f"{colorstr('optimizer:')} {type(optimizer).__name__}(lr={lr}) with parameter groups "f"{len(g[1])} weight(decay=0.0), {len(g[0])} weight(decay={decay}), {len(g[2])} bias") # {colorstr('optimizer:')} {type(optimizer).__name__}(lr={lr}) 带有参数组 {len(g[1])} weight(decay=0.0), {len(g[0])} weight(decay={decay}), {len(g[2])} bias 。# 返回创建的优化器实例。return optimizer
# smart_optimizer 函数根据模型的不同参数创建一个优化器,将参数分为三组:权重(应用衰减)、归一化层权重(不应用衰减)和偏置(不应用衰减)。然后根据指定的优化器名称创建优化器实例,并配置不同的参数组。这个函数提供了一种灵活的方式来配置模型的优化器,允许对不同类型的参数应用不同的优化策略。# 优化器(Optimizer)在机器学习和深度学习中扮演着至关重要的角色。它们的主要作用是调整模型的参数,以最小化损失函数(loss function),从而训练出性能良好的模型。以下是优化器的几个关键作用 :
# 参数更新 :优化器根据损失函数的梯度来更新模型的参数。在每次迭代(iteration)或 epoch 结束时,优化器会计算损失函数关于模型参数的梯度,然后根据这些梯度调整参数值。
# 学习率控制 :学习率(learning rate)是优化过程中最重要的超参数之一,它控制着参数更新的步长。优化器允许学习率随时间变化,可以是固定的,也可以根据预设的策略动态调整(例如学习率衰减)。
# 动量和加速 :一些优化器如SGD(随机梯度下降)的变种(如Momentum SGD)和Adam等,引入了动量(momentum)或自适应学习率的概念,帮助模型在训练过程中更平滑地收敛,减少震荡,并加速收敛。
# 正则化和权重衰减 :优化器可以结合权重衰减(weight decay)等正则化技术,防止模型过拟合。权重衰减通过在损失函数中添加一个惩罚项来实现,优化器在更新参数时会考虑这个惩罚项。
# 处理非凸优化问题 :深度学习中的优化问题通常是非凸的,这意味着存在多个局部最小值。优化器需要能够有效地探索参数空间,找到一个好的局部最小值。
# 适应不同的训练场景 :不同的优化器适用于不同的训练场景。例如,对于小数据集,SGD可能表现更好;而对于大数据集,Adam等自适应学习率优化器可能更有效。
# 提高训练效率 :优化器通过合理地调整参数,可以减少达到收敛所需的迭代次数,从而提高训练效率。
# 实现复杂的优化策略 :一些高级优化器可以实现复杂的优化策略,如学习率预热(learning rate warmup)、余弦退火(cosine annealing)等,这些策略可以在训练的不同阶段动态调整学习率,以改善模型性能。
# 总之,优化器是训练神经网络不可或缺的工具,它们通过智能地调整参数来最小化损失函数,帮助我们训练出准确、高效的模型。不同的优化器有不同的特点和适用场景,选择合适的优化器对于模型的训练效果至关重要。
22.def smart_hub_load(repo='ultralytics/yolov5', model='yolov5s', **kwargs):
# 这段代码定义了一个名为 smart_hub_load 的函数,用于从 PyTorch Hub 加载模型,并处理不同 PyTorch 版本可能引起的问题。
# 定义了一个名为 smart_hub_load 的函数,接受以下参数 :
# 1.repo :要加载的模型所在的仓库,默认为 'ultralytics/yolov5' 。
# 2.model :要加载的模型名称,默认为 'yolov5s' 。
# 3.**kwargs :接受任意数量的额外关键字参数,这些参数将被传递给 torch.hub.load 函数。
def smart_hub_load(repo='ultralytics/yolov5', model='yolov5s', **kwargs):# YOLOv5 torch.hub.load() wrapper with smart error/issue handling YOLOv5 torch.hub.load() 包装器,具有智能错误/问题处理功能。# 检查 PyTorch 版本是否为 1.9.1。如果是,将 kwargs 字典中的 'skip_validation' 键设置为 True ,以避免在加载模型时进行验证,因为验证可能会导致 GitHub API 速率限制错误。if check_version(torch.__version__, '1.9.1'):kwargs['skip_validation'] = True # validation causes GitHub API rate limit errors# 检查 PyTorch 版本是否为 1.12.0 或更高。如果是,将 kwargs 字典中的 'trust_repo' 键设置为 True ,因为从 PyTorch 0.12 开始,需要这个参数来从 PyTorch Hub 加载模型。if check_version(torch.__version__, '1.12.0'):kwargs['trust_repo'] = True # argument required starting in torch 0.12# 尝试使用 torch.hub.load 函数加载模型,并将 repo 、 model 以及 kwargs 中的额外参数传递给它。try:# torch.hub.load(repo_or_dir, model, *args, **kwargs)# torch.hub.load() 是 PyTorch 提供的一个函数,用于从 PyTorch Hub 或本地路径加载预训练的模型或代码库。# 参数说明 :# repo_or_dir :可以是 GitHub 上的仓库地址或本地目录的路径。如果是 GitHub 仓库,格式通常是 username/repo 。# model :要加载的模型名称或字符串。这通常对应于远程仓库中的一个特定模型或本地目录中的一个模块。# *args :传递给模型构造函数的位置参数。# **kwargs :传递给模型构造函数的关键字参数。可选关键字参数 :# skip_validation (bool, 可选) :如果设置为 False ,则 torchhub 会检查指定的 github 分支或提交是否确实属于仓库所有者。这将向 GitHub API 发送请求;可以通过设置 GITHUB_TOKEN 环境变量来指定一个非默认的 GitHub 令牌。默认值为 False 。# trust_repo (bool, 可选) :从 PyTorch 1.12.0 开始,这个参数是必需的,用于指定是否信任仓库。# force_reload (bool, 可选) :如果设置为 True ,则会丢弃现有缓存,并强制从 PyTorch Hub 重新下载模型。# source (str, 可选) :指定模型来源,可以是 "github" 或 "local" 。默认为 "github" 。# 返回值 :# 函数返回模型对象,该对象可以用于进一步的推理或微调。# torch.hub.load() 函数简化了从 PyTorch Hub 加载模型的过程,使得用户可以轻松地访问和使用预训练模型,而无需从头开始训练。这个函数是 PyTorch 生态系统中的一个重要组成部分,特别适合于那些希望快速实现机器学习和深度学习原型的研究人员和开发者。return torch.hub.load(repo, model, **kwargs)# 如果在加载模型的过程中发生任何异常,将捕获这个异常,并再次尝试加载模型,这次会添加 force_reload=True 参数,强制重新加载模型,忽略缓存。except Exception:return torch.hub.load(repo, model, force_reload=True, **kwargs)
# smart_hub_load 函数是一个封装了 torch.hub.load 的辅助函数,它根据 PyTorch 的版本自动设置必要的参数,并处理加载模型时可能遇到的异常。这个函数使得从 PyTorch Hub 加载模型的过程更加健壮,能够适应不同版本的 PyTorch 和网络环境的变化。
23.def smart_resume(ckpt, optimizer, ema=None, weights='yolov5s.pt', epochs=300, resume=True):
# 这段代码定义了一个名为 smart_resume 的函数,用于从部分训练的检查点(checkpoint)恢复训练。
# 定义了一个名为 smart_resume 的函数,接受以下参数。
# 1.ckpt :检查点字典,包含训练的状态信息。
# 2.optimizer :优化器对象,用于训练模型。
# 3.ema :可选参数,指数移动平均(Exponential Moving Average)对象,用于平滑模型权重。
# 4.weights :模型权重文件的名称,默认为 'yolov5s.pt' 。
# 5.epochs :训练的总轮数,默认为 300。
# 6.resume :布尔值,指示是否从检查点恢复训练,默认为 True 。
def smart_resume(ckpt, optimizer, ema=None, weights='yolov5s.pt', epochs=300, resume=True):# Resume training from a partially trained checkpoint 从部分训练的检查点恢复训练。# 初始化 best_fitness 变量,用于存储最佳适应度值。best_fitness = 0.0# 从检查点中获取当前的训练轮数,并将其加 1,得到恢复训练的起始轮数。start_epoch = ckpt['epoch'] + 1# 如果检查点中包含优化器状态,将其加载到优化器对象中,并更新 best_fitness 变量。if ckpt['optimizer'] is not None:optimizer.load_state_dict(ckpt['optimizer']) # optimizerbest_fitness = ckpt['best_fitness']# 如果提供了 ema 对象,并且检查点中包含 EMA 状态,将其加载到 EMA 对象中,并更新 ema.updates 变量。if ema and ckpt.get('ema'):ema.ema.load_state_dict(ckpt['ema'].float().state_dict()) # EMAema.updates = ckpt['updates']# 如果 resume 为 True ,则断言 start_epoch 大于 0,确保训练尚未完成。然后记录恢复训练的信息。if resume:assert start_epoch > 0, f'{weights} training to {epochs} epochs is finished, nothing to resume.\n' \f"Start a new training without --resume, i.e. 'python train.py --weights {weights}'" # {weights} 训练 {epochs} 个时期已完成,无需恢复。开始新的训练,无需 --resume,即‘python train.py --weights {weights}’ 。LOGGER.info(f'Resuming training from {weights} from epoch {start_epoch} to {epochs} total epochs') # 从 {weights} 恢复训练,从第 {start_epoch} 个时期开始,总共训练 {epochs} 个时期。# 如果指定的训练轮数 epochs 小于起始轮数 start_epoch ,则记录模型已经训练的轮数,并更新 epochs 以进行额外的微调轮数。if epochs < start_epoch:LOGGER.info(f"{weights} has been trained for {ckpt['epoch']} epochs. Fine-tuning for {epochs} more epochs.") # {weights} 已训练了 {ckpt['epoch']} 个时期。 还将进行 {epochs} 个时期的微调。epochs += ckpt['epoch'] # finetune additional epochs# 返回 最佳适应度值 、 起始轮数 和 更新后的训练轮数 。return best_fitness, start_epoch, epochs
# smart_resume 函数用于从部分训练的检查点恢复训练。它加载优化器和 EMA 状态,更新训练轮数,并根据需要调整训练配置。这个函数使得训练过程可以灵活地从中断点恢复,或者进行额外的微调。
24.class EarlyStopping:
# 这段代码定义了一个名为 EarlyStopping 的类,它用于实现早停(early stopping)机制,这是一种在训练机器学习模型时用来防止过拟合的技术。
class EarlyStopping:# YOLOv5 simple early stopper YOLOv5 简单早期停止器。# 类初始化方法 __init__ 。# 这是 EarlyStopping 类的构造函数,它接受一个参数。# 1.patience :表示在模型性能停止提升之前,训练可以继续进行的轮数(epochs)。如果没有提供 patience 参数,则默认值为 30。def __init__(self, patience=30):# 初始化 best_fitness 属性,用于存储观察到的 最佳模型性能 (例如 mAP)。self.best_fitness = 0.0 # i.e. mAP# 初始化 best_epoch 属性,用于存储最佳模型性能观察到的 轮数 。self.best_epoch = 0# 设置 patience 属性,如果 patience 参数被设置为 None 或者其他假值,则将其设置为无穷大,这意味着早停机制将被禁用。self.patience = patience or float('inf') # epochs to wait after fitness stops improving to stop# 初始化 possible_stop 属性,用于标记 是否可能在下一个轮次停止训练 。self.possible_stop = False # possible stop may occur next epoch# 调用方法 __call__ 。# 这是 __call__ 方法,它允许 EarlyStopping 实例像函数一样被调用。它接受两个参数。# 1.epoch :当前轮数。# 2.fitness :当前轮次的模型性能。def __call__(self, epoch, fitness):# 如果当前轮次的模型性能不小于之前的最佳性能,则更新 best_epoch 和 best_fitness 。if fitness >= self.best_fitness: # >= 0 to allow for early zero-fitness stage of trainingself.best_epoch = epochself.best_fitness = fitness# 计算从最佳性能以来经过的轮数。delta = epoch - self.best_epoch # epochs without improvement# 更新 possible_stop 属性,如果经过的轮数即将达到 patience 限制,则标记可能在下一个轮次停止。self.possible_stop = delta >= (self.patience - 1) # possible stop may occur next epoch# 判断是否应该停止训练,如果经过的轮数已经超过 patience 限制,则 stop 为 True 。stop = delta >= self.patience # stop training if patience exceeded# 如果应该停止训练,则记录一条信息,说明训练将提前停止,并提供有关最佳模型性能和如何更新 EarlyStopping 参数的说明。if stop:LOGGER.info(f'Stopping training early as no improvement observed in last {self.patience} epochs. ' # 由于在最后 {self.patience} 个时期没有观察到任何改进,因此提前停止训练。f'Best results observed at epoch {self.best_epoch}, best model saved as best.pt.\n' # 在时期 {self.best_epoch} 观察到最佳结果,最佳模型保存为 best.pt。f'To update EarlyStopping(patience={self.patience}) pass a new patience value, ' # 要更新 EarlyStopping(patience={self.patience}),请传递新的耐心值,即 `python train.py --patience 300` 或使用 `--patience 0` 禁用 EarlyStopping。f'i.e. `python train.py --patience 300` or use `--patience 0` to disable EarlyStopping.')# 返回 stop 值,指示是否应该停止训练。return stop
# EarlyStopping 类提供了一个早停机制,用于在模型性能在一定轮数内没有提升时停止训练,以防止过拟合。通过调用实例并传入当前轮次和性能,该类可以决定是否需要停止训练。这个类是训练过程中常用的一种技术,可以帮助节省资源并提高模型的泛化能力。
25.class ModelEMA:
# 这段代码定义了一个名为 ModelEMA 的类,用于实现模型的指数移动平均(Exponential Moving Average,简称 EMA)。EMA是一种技术,用于平滑模型参数,通常用于提高模型的稳定性和性能。
# 定义了一个名为 ModelEMA 的新类。
class ModelEMA:# 更新了来自 https://github.com/rwightman/pytorch-image-models 的指数移动平均线 (EMA) 。# 保持模型 state_dict 中所有内容的移动平均值(参数和缓冲区) 。# 有关 EMA 的详细信息,请参阅 https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage""" Updated Exponential Moving Average (EMA) from https://github.com/rwightman/pytorch-image-modelsKeeps a moving average of everything in the model state_dict (parameters and buffers)For EMA details see https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage"""# 这段代码是 ModelEMA 类的构造函数 __init__ ,它初始化类实例并设置指数移动平均(EMA)的相关参数。# 这是 ModelEMA 类的构造函数,它接受四个参数。# 1.self :类的实例本身。# 2.model :要创建EMA的原始模型。# 3.decay :EMA的衰减率,默认为 0.9999。# 4.tau :用于计算指数衰减的超参数,默认为 2000。# 5.updates :EMA更新的次数,默认为 0。def __init__(self, model, decay=0.9999, tau=2000, updates=0):# Create EMA 创建 EMA 。# 执行以下操作。# de_parallel(model) :一个自定义函数,用于去除模型的DataParallel包装,以便在多GPU环境中正常工作。# deepcopy(...) :创建模型的深拷贝,确保EMA副本与原始模型的参数初始状态相同。# .eval() :将EMA模型设置为评估模式,这意味着在推理时使用的特定层(如 Dropout 和 BatchNorm )将表现出不同的行为。# self.ema :将EMA模型存储在实例变量中,以便后续更新和使用。# def de_parallel(model): -> 将一个可能处于并行状态(例如使用 PyTorch 的 DataParallel 或 DistributedDataParallel 包装过的模型)转换回单个 GPU 或 CPU 上的模型。 -> return model.module if is_parallel(model) else modelself.ema = deepcopy(de_parallel(model)).eval() # FP32 EMA# 将传入的 updates 参数值存储在实例变量 self.updates 中,用于 跟踪EMA更新的次数 。self.updates = updates # number of EMA updates# 定义了一个lambda函数,用于计算EMA的动态衰减率。# decay :EMA的初始衰减率。# tau :控制衰减率变化速度的超参数。# math.exp(-x / tau) :计算指数衰减,其中 x 是更新次数。# 这个衰减率随着训练的进行逐渐增加,有助于在早期训练阶段更平滑地更新EMA参数。self.decay = lambda x: decay * (1 - math.exp(-x / tau)) # decay exponential ramp (to help early epochs)# 遍历EMA模型的所有参数,并设置 requires_grad 属性为 False ,这意味着在训练过程中不会计算这些参数的梯度。这是因为EMA参数是通过平均原始模型的参数来更新的,而不是通过反向传播。for p in self.ema.parameters():p.requires_grad_(False)# ModelEMA 类的构造函数初始化了一个EMA模型,设置了动态衰减率,并确保EMA参数在训练过程中不会更新。这个类允许用户在训练过程中定期更新EMA参数,以平滑模型权重并可能提高模型的泛化能力。# 这段代码是 ModelEMA 类的 update 方法,用于更新指数移动平均(EMA)模型的参数。# 定义了一个名为 update 的方法,它接受两个参数。# 1.self :类的实例本身。# 2.model :原始模型,其参数将用于更新EMA参数。def update(self, model):# Update EMA parameters 更新 EMA 参数。# 每次调用 update 方法时,增加 self.updates 的值,这个值用于跟踪EMA更新的次数。self.updates += 1# 使用之前在构造函数中定义的 self.decay 函数计算当前的 衰减率 d ,这个衰减率基于当前的 self.updates 值。d = self.decay(self.updates)# 调用 de_parallel 函数去除模型的DataParallel包装,并获取其状态字典 msd ,这个 状态字典 包含了 原始模型的参数 。msd = de_parallel(model).state_dict() # model state_dict# 遍历EMA模型的状态字典中的每个参数。 k 是参数的名称, v 是参数的值。for k, v in self.ema.state_dict().items():# 检查参数 v 是否是浮点数类型(即FP16或FP32)。这个条件确保了只更新浮点数类型的参数。if v.dtype.is_floating_point: # true for FP16 and FP32# 对于每个浮点数类型的参数,执行以下操作 :# v *= d :将EMA参数乘以衰减率 d 。# v += (1 - d) * msd[k].detach() :将EMA参数加上 (1 - d) 倍的原始模型参数的更新。这里使用 msd[k].detach() 来避免计算梯度,因为只关心参数的值,不关心其梯度。v *= dv += (1 - d) * msd[k].detach()# 这是一个被注释掉的断言语句,如果取消注释,它将确保EMA参数和原始模型参数的数据类型都是FP32。如果类型不匹配,将抛出异常。# assert v.dtype == msd[k].dtype == torch.float32, f'{k}: EMA {v.dtype} and model {msd[k].dtype} must be FP32' # {k}:EMA {v.dtype} 和模型 {msd[k].dtype} 必须是 FP32 。# update 方法通过结合原始模型的参数和EMA参数来更新EMA模型的参数。它使用指数衰减率来计算新的EMA参数值,这种方法可以平滑参数的变化,有助于提高模型的稳定性和性能。这个方法通常在每个训练轮次后被调用,以更新EMA参数。# 这段代码是 ModelEMA 类的 update_attr 方法,用于将原始模型的某些属性复制到EMA(指数移动平均)模型中。# 定义了一个名为 update_attr 的方法,它接受以下参数 :# 1.self :类的实例本身。# 2.model :原始模型,从中复制属性。# 3.include :一个元组,指定要复制的属性名称,默认为空,即不特别指定。# 4.exclude :一个元组,指定要排除的属性名称,默认排除 'process_group' 和 'reducer' 。def update_attr(self, model, include=(), exclude=('process_group', 'reducer')):# Update EMA attributes 更新 EMA 属性。# 调用 copy_attr 函数,将原始模型 model 的属性复制到EMA模型 self.ema 中。 include 参数用于指定要复制的属性, exclude 参数用于指定要排除的属性。这个操作通常用于确保EMA模型具有与原始模型相同的非参数属性,例如绑定的方法或特定的状态。# def copy_attr(a, b, include=(), exclude=()): -> 用于将一个对象 b 的属性复制到另一个对象 a 。这个函数提供了灵活的方式来选择性地复制属性,可以指定只复制某些属性( include ),或者排除某些属性( exclude )。copy_attr(self.ema, model, include, exclude)# update_attr 方法用于同步原始模型和EMA模型的非参数属性。这对于保持两个模型在行为上的一致性很有帮助,尤其是在使用EMA模型进行推理时,可以确保它不仅在参数上接近训练模型,而且在行为上也一致。这个方法通过排除某些特定的属性(如并行处理相关的属性)来避免不必要的错误或冲突。
# ModelEMA 类提供了一个EMA实现,用于平滑模型参数。通过定期更新EMA参数,可以在训练过程中保持模型参数的稳定性,这通常会导致更好的验证性能和泛化能力。这个类可以与训练循环集成,以在每个训练轮次后更新EMA参数。