tuner.py
ultralytics\engine\tuner.py
目录
tuner.py
1.所需的库和模块
2.class Tuner:
1.所需的库和模块
# Ultralytics YOLO 🚀, AGPL-3.0 license# 模块提供用于对象检测、实例分割、图像分类、姿势估计和多对象跟踪的 Ultralytics YOLO 模型的超参数调整功能。
# 超参数调整是系统地搜索产生最佳模型性能的最佳超参数集的过程。这在 YOLO 等深度学习模型中尤为重要,因为超参数的微小变化可能会导致模型准确性和效率的显著差异。
"""
Module provides functionalities for hyperparameter tuning of the Ultralytics YOLO models for object detection, instance
segmentation, image classification, pose estimation, and multi-object tracking.Hyperparameter tuning is the process of systematically searching for the optimal set of hyperparameters
that yield the best model performance. This is particularly crucial in deep learning models like YOLO,
where small changes in hyperparameters can lead to significant differences in model accuracy and efficiency.Example:Tune hyperparameters for YOLOv8n on COCO8 at imgsz=640 and epochs=30 for 300 tuning iterations.```pythonfrom ultralytics import YOLOmodel = YOLO("yolov8n.pt")model.tune(data="coco8.yaml", epochs=10, iterations=300, optimizer="AdamW", plots=False, save=False, val=False)```
"""import random
import shutil
import subprocess
import timeimport numpy as np
import torchfrom ultralytics.cfg import get_cfg, get_save_dir
from ultralytics.utils import DEFAULT_CFG, LOGGER, callbacks, colorstr, remove_colorstr, yaml_print, yaml_save
from ultralytics.utils.plotting import plot_tune_results
2.class Tuner:
# 这段代码是一个Python类的实现,名为 Tuner 。这个类是用于机器学习或深度学习中的超参数调整(tuning)。
# 定义了一个名为 Tuner 的类。
class Tuner:# 负责 YOLO 模型超参数调整的类。# 该类通过根据搜索空间对 YOLO 模型超参数进行变异并重新训练模型以评估其性能,在给定的迭代次数内演化 YOLO 模型超参数。# 方法:# _mutate(hyp: dict) -> dict :在 `self.space` 中指定的界限内变异给定的超参数。# __call__() :在多次迭代中执行超参数演化。"""Class responsible for hyperparameter tuning of YOLO models.The class evolves YOLO model hyperparameters over a given number of iterationsby mutating them according to the search space and retraining the model to evaluate their performance.Attributes:space (dict): Hyperparameter search space containing bounds and scaling factors for mutation.tune_dir (Path): Directory where evolution logs and results will be saved.tune_csv (Path): Path to the CSV file where evolution logs are saved.Methods:_mutate(hyp: dict) -> dict:Mutates the given hyperparameters within the bounds specified in `self.space`.__call__():Executes the hyperparameter evolution across multiple iterations.Example:Tune hyperparameters for YOLOv8n on COCO8 at imgsz=640 and epochs=30 for 300 tuning iterations.```pythonfrom ultralytics import YOLOmodel = YOLO("yolov8n.pt")model.tune(data="coco8.yaml", epochs=10, iterations=300, optimizer="AdamW", plots=False, save=False, val=False)```Tune with custom search space.```pythonfrom ultralytics import YOLOmodel = YOLO("yolov8n.pt")model.tune(space={key1: val1, key2: val2}) # custom search space dictionary```"""# 这段代码是 Tuner 类的构造函数( __init__ ),它初始化类的实例。# 这是构造函数的定义,它接受两个参数。# 1.args :这是一个具有默认值的参数。 args 代表一个字典,用于存储配置信息。如果调用 Tuner 类时没有提供 args 参数,那么它将使用 DEFAULT_CFG 作为默认值。# 2._callbacks :这是一个可选参数,其默认值为 None 。 _callbacks 用于传递一个回调函数列表或回调对象,这些回调将在调优过程中的特定点被调用。如果调用 Tuner 类时没有提供 _callbacks 参数,那么它将保持为 None ,后续可能会使用默认的回调函数。def __init__(self, args=DEFAULT_CFG, _callbacks=None):# 使用配置初始化调谐器。"""Initialize the Tuner with configurations.Args:args (dict, optional): Configuration for hyperparameter evolution."""# 这行代码尝试从 args 字典中移除并返回键为 "space" 的值,如果 "space" 不存在,则使用默认的字典。 self.space 是一个字典,包含了各种超参数的搜索空间,每个键对应一个元组,元组中包含了超参数的最小值、最大值和(可选的)增益值。self.space = args.pop("space", None) or { # key: (min, max, gain(optional))# 超参数列表。# 'optimizer': tune.choice(['SGD', 'Adam', 'AdamW', 'NAdam', 'RAdam', 'RMSProp']),# 初始学习率的范围。"lr0": (1e-5, 1e-1), # initial learning rate (i.e. SGD=1E-2, Adam=1E-3)# 最终OneCycleLR学习率的范围( lr0 乘以 lrf )。"lrf": (0.0001, 0.1), # final OneCycleLR learning rate (lr0 * lrf)# SGD动量/Adam beta1的范围和增益。"momentum": (0.7, 0.98, 0.3), # SGD momentum/Adam beta1# 优化器权重衰减的范围。"weight_decay": (0.0, 0.001), # optimizer weight decay 5e-4# 预热周期的范围(可以是分数)。"warmup_epochs": (0.0, 5.0), # warmup epochs (fractions ok)# 预热初始动量的范围。"warmup_momentum": (0.0, 0.95), # warmup initial momentum# "box" 、 "cls" 、 "dfl" :不同损失函数的增益范围。"box": (1.0, 20.0), # box loss gain"cls": (0.2, 4.0), # cls loss gain (scale with pixels)"dfl": (0.4, 6.0), # dfl loss gain# hsv_h" 、 "hsv_s" 、 "hsv_v" :HSV色彩空间中Hue、Saturation、Value的增强范围。"hsv_h": (0.0, 0.1), # image HSV-Hue augmentation (fraction)"hsv_s": (0.0, 0.9), # image HSV-Saturation augmentation (fraction)"hsv_v": (0.0, 0.9), # image HSV-Value augmentation (fraction)# 图像旋转的范围。"degrees": (0.0, 45.0), # image rotation (+/- deg)# 图像平移的范围。"translate": (0.0, 0.9), # image translation (+/- fraction)# 图像缩放的范围。"scale": (0.0, 0.95), # image scale (+/- gain)# 图像剪切的范围。"shear": (0.0, 10.0), # image shear (+/- deg)# 图像透视变换的范围。"perspective": (0.0, 0.001), # image perspective (+/- fraction), range 0-0.001# "flipud" 、 "fliplr" :图像上下翻转和左右翻转的概率。"flipud": (0.0, 1.0), # image flip up-down (probability)"fliplr": (0.0, 1.0), # image flip left-right (probability)# 图像通道BGR交换的概率。"bgr": (0.0, 1.0), # image channel bgr (probability)# "mosaic" 、 "mixup" :图像混合(mosaic和mixup)的概率。"mosaic": (0.0, 1.0), # image mixup (probability)"mixup": (0.0, 1.0), # image mixup (probability)# 图像复制粘贴的概率。"copy_paste": (0.0, 1.0), # segment copy-paste (probability)}# 这行代码调用 get_cfg 函数,传入 args 作为参数,并把返回的配置赋值给 self.args 。# def get_cfg(cfg: Union[str, Path, Dict, SimpleNamespace] = DEFAULT_CFG_DICT, overrides: Dict = None):# -> 从一个配置源(可以是字符串、路径、字典或 SimpleNamespace 对象)获取配置,并允许通过 overrides 字典来覆盖默认配置。返回一个 IterableSimpleNamespace 对象,它是一个可迭代的命名空间对象,其属性由 cfg 字典中的键值对初始化。# -> return IterableSimpleNamespace(**cfg)self.args = get_cfg(overrides=args)# 这行代码调用 get_save_dir 函数,传入 self.args 和 name="tune" 作为参数,并把返回的目录路径赋值给 self.tune_dir 。# def get_save_dir(args, name=None): -> 获取或创建一个用于保存特定数据(如模型权重、日志等)的目录路径。返回保存目录的路径。返回一个 Path 对象,表示保存目录的路径。 -> return Path(save_dir)self.tune_dir = get_save_dir(self.args, name="tune")# 这行代码将 self.tune_dir 与 "tune_results.csv" 拼接,得到 self.tune_csv 的路径,用于存储调优结果。self.tune_csv = self.tune_dir / "tune_results.csv"# 这行代码将 _callbacks 赋值给 self.callbacks ,如果 _callbacks 为 None ,则调用 callbacks.get_default_callbacks() 获取默认的回调函数列表。# def get_default_callbacks():# -> 返回默认的回调函数列表。返回一个 defaultdict 对象,它使用 list 作为默认工厂函数,这意味着如果访问的键不存在,将会创建一个空列表作为该键的值。# -> return defaultdict(list, deepcopy(default_callbacks))self.callbacks = _callbacks or callbacks.get_default_callbacks()# 这行代码将 "Tuner: " 字符串通过 colorstr 函数处理后赋值给 self.prefix , colorstr 函数用于给字符串添加颜色的。# def colorstr(*input):# -> 函数通过遍历 args 中的每个元素(颜色或样式),从 colors 字典中获取对应的ANSI转义序列,并将其与传入的 string 字符串连接起来。最后,它还会添加一个 colors["end"] 序列,用于重置终端的颜色和样式到默认状态。# -> return "".join(colors[x] for x in args) + f"{string}" + colors["end"]self.prefix = colorstr("Tuner: ")# 回调函数是一种以函数对象作为参数并在完成某个特定任务后调用的函数。在编程中,回调函数是一种常用的设计模式,它允许程序在某个事件或条件触发时执行额外的代码。这种模式增强了代码的灵活性和模块化,因为它允许用户或开发者在不修改现有代码的情况下,插入自定义的行为。# 回调函数的特点 :# 参数传递 :回调函数作为参数传递给另一个函数。# 延迟执行 :回调函数不是立即执行的,而是在特定事件发生时由其他代码调用。# 事件处理 :常用于事件驱动编程,例如用户界面事件(如按钮点击)或系统事件(如文件读写完成)。# 异步操作 :在异步编程中,回调函数用于处理操作完成后的结果或错误。# 回调函数的使用场景 :# 事件监听 :在GUI编程中,回调函数用于响应用户的输入或操作。# 异步编程 :在Node.js或Python的异步IO操作中,回调函数用于处理异步操作的结果。# 测试和断言 :在单元测试中,回调函数可以用于验证异步操作的结果。# 插件和扩展 :在支持插件的软件中,回调函数允许插件介入主程序的执行流程。# 装饰器 :在装饰器模式中,回调函数用于在执行主要逻辑前后添加额外的行为。# 这行代码调用 callbacks.add_integration_callbacks 函数,传入 self 作为参数,用于添加集成回调。# def add_integration_callbacks(instance): -> 向一个实例添加集成的回调函数。这些回调函数通常用于训练过程中的不同阶段,例如记录训练日志、跟踪实验进度等。callbacks.add_integration_callbacks(self)# 这行代码使用 LOGGER 对象的 info 方法记录一条信息日志,包含了初始化 Tuner 实例的信息和一些提示信息。LOGGER.info(f"{self.prefix}Initialized Tuner instance with 'tune_dir={self.tune_dir}'\n" # {self.prefix}使用“tune_dir={self.tune_dir}”初始化 Tuner 实例。f"{self.prefix}💡 Learn about tuning at https://docs.ultralytics.com/guides/hyperparameter-tuning" # {self.prefix}💡 了解有关调整的信息,请访问 https://docs.ultralytics.com/guides/hyperparameter-tuning。)# 这个构造函数的主要作用是初始化 Tuner 类的实例,设置超参数搜索空间、配置、调优结果存储路径、回调函数,并记录日志。# 这段代码定义了一个名为 _mutate 的方法,它是 Tuner 类的一部分。这个方法用于生成一个新的超参数组合,通过变异(mutate)已有的超参数。# 参数定义。# 1.self :指向类实例本身的引用。# 2.parent="single" :选择父代的策略,可以是"single"(单个最佳)或"weighted"(加权组合)。# 3.n=5 :考虑用于生成新超参数组合的前n个最佳结果的数量。# 4.mutation=0.8 :变异的概率,即每个超参数有多大几率被变异。# 5.sigma=0.2 :变异的强度,控制随机扰动的幅度。def _mutate(self, parent="single", n=5, mutation=0.8, sigma=0.2):# 根据 `self.space` 中指定的边界和缩放因子对超参数进行变异。"""Mutates the hyperparameters based on bounds and scaling factors specified in `self.space`.Args:parent (str): Parent selection method: 'single' or 'weighted'.n (int): Number of parents to consider.mutation (float): Probability of a parameter mutation in any given iteration.sigma (float): Standard deviation for Gaussian random number generator.Returns:(dict): A dictionary containing mutated hyperparameters."""# 这段代码是 _mutate 方法中处理CSV文件存在时的逻辑部分。它的目的是从已有的调优结果中选择最佳的超参数组合(父代),然后基于这些父代进行变异以生成新的超参数组合。# 检查CSV文件是否存在。检查 self.tune_csv 路径指向的CSV文件是否存在。if self.tune_csv.exists(): # if CSV file exists: select best hyps and mutate# Select parent(s)# 加载数据。使用 numpy 的 loadtxt 函数加载CSV文件中的数据, ndmin=2 确保结果至少是二维数组, delimiter="," 指定了CSV的分隔符, skiprows=1 跳过第一行(通常是标题行)。x = np.loadtxt(self.tune_csv, ndmin=2, delimiter=",", skiprows=1)# 提取适应度值。 提取第一列的值,假设这一列包含了适应度评分。fitness = x[:, 0] # first column# 确定考虑的结果数量。 确保 n 参数不超过已有结果的数量。n = min(n, len(x)) # number of previous results to consider# 选择最佳结果。 对适应度值进行降序排序,并选择前 n 个最佳结果。x = x[np.argsort(-fitness)][:n] # top n mutations# 计算权重。计算每个结果的权重,确保权重和大于0, 1e-6 是为了防止除以0的情况。w = x[:, 0] - x[:, 0].min() + 1e-6 # weights (sum > 0)# 选择父代。# 如果 parent 参数为"single"或者结果数量为1,使用加权随机选择一个父代。if parent == "single" or len(x) == 1:# x = x[random.randint(0, n - 1)] # random selection# 使用 random.choices 函数根据权重 w 从 n 个结果中随机选择一个。x = x[random.choices(range(n), weights=w)[0]] # weighted selection# 如果 parent 参数为"weighted",则计算加权组合作为父代。elif parent == "weighted":# 计算所有结果的加权平均值,其中 w.reshape(n, 1) 将权重转换为列向量,以便与结果矩阵 x 进行点乘。x = (x * w.reshape(n, 1)).sum(0) / w.sum() # weighted combination# 这段代码的目的是从一个已有的调优结果集合中,根据适应度评分选择最佳的超参数组合,并根据选择策略(单个最佳或加权组合)来确定父代,以便后续进行变异操作。# 这段代码是 _mutate 方法中负责变异操作的部分。它生成一个新的超参数组合,通过随机扰动已有的超参数(父代)。# Mutate# 设置随机数生成器。将 np.random 模块赋值给变量 r ,用于后续的随机数生成。r = np.random # method# 设置随机数生成器的种子,确保每次运行代码时生成的随机数序列不同,这里使用当前时间的整数部分作为种子。r.seed(int(time.time()))# 计算增益值。# 这行代码遍历 self.space 字典,如果某个超参数的元组长度为3,则取第三个元素作为增益值;否则,默认增益值为1.0。g = np.array([v[2] if len(v) == 3 else 1.0 for k, v in self.space.items()]) # gains 0-1# 初始化变异向量。# 获取超参数空间的大小。ng = len(self.space)# 初始化一个全为1的向量,用于存储每个超参数的变异倍数。v = np.ones(ng)# 变异操作。# 循环直到 v 中至少有一个元素不等于1,以确保至少有一个超参数发生了变异,防止生成与父代完全相同的超参数组合。while all(v == 1): # mutate until a change occurs (prevent duplicates)# 这行代码执行实际的变异操作 :# r.random(ng) < mutation 生成一个随机数数组,小于 mutation 参数值的位置将被标记为True,表示该位置的超参数将被变异。# r.randn(ng) 生成一个标准正态分布的随机数数组,用于控制变异的方向。# r.random() * sigma 生成一个随机数,用于控制变异的幅度, sigma 参数控制变异的强度。# g * ... + 1 将上述结果与增益值相乘并加1,确保变异后的值至少为1。# .clip(0.3, 3.0) 将结果限制在0.3到3.0的范围内,防止变异后的值过大或过小。v = (g * (r.random(ng) < mutation) * r.randn(ng) * r.random() * sigma + 1).clip(0.3, 3.0)# 生成新的超参数组合。# 这行代码根据变异后的倍数 v 和父代的超参数值 x 生成新的超参数组合。这里 x[i + 1] 是因为 x 数组的第一列是适应度值,所以超参数值从第二列开始。hyp = {k: float(x[i + 1] * v[i]) for i, k in enumerate(self.space.keys())}# 默认超参数组合。# 如果 self.tune_csv 文件不存在,即没有之前的调优结果,那么 hyp 将直接从 self.args 中获取默认的超参数值。else:hyp = {k: getattr(self.args, k) for k in self.space.keys()}# 这段代码实现了超参数的变异过程,通过随机扰动和增益调整来探索新的超参数组合,以期找到性能更优的模型配置。# Constrain to limits# 这段代码是 _mutate 方法中对生成的新超参数组合进行约束处理的部分。它确保每个超参数的值都在预定义的范围内,并且将值四舍五入到指定的有效数字。# 遍历超参数空间。这行代码遍历 self.space 字典中的每一项, k 是超参数的名称, v 是对应的元组,包含超参数的最小值、最大值和(可选的)增益值。for k, v in self.space.items():# 应用下界约束。这行代码确保 hyp 字典中对应超参数 k 的值不小于 v 元组中的第一个元素,即超参数的最小值。hyp[k] = max(hyp[k], v[0]) # lower limit# 应用上界约束。这行代码确保 hyp 字典中对应超参数 k 的值不大于 v 元组中的第二个元素,即超参数的最大值。hyp[k] = min(hyp[k], v[1]) # upper limit# 四舍五入到指定有效数字。这行代码将 hyp 字典中对应超参数 k 的值四舍五入到5位有效数字。hyp[k] = round(hyp[k], 5) # significant digits# 返回处理后的超参数组合。这行代码返回经过约束处理和四舍五入的新超参数组合。return hyp# 这段代码的目的是确保新生成的超参数组合在合理的范围内,并且数值表示上是精确的。通过这种方式,可以避免生成无效的超参数值,同时保持数值的简洁性和可读性。# 这个方法是超参数调优过程中的关键步骤,通过变异和选择机制探索新的超参数组合,以期找到性能更优的模型配置。# 这段代码定义了 Tuner 类的特殊方法 __call__ ,它允许类的实例像函数一样被调用。这个方法执行模型的训练和超参数调优的过程。# 参数定义。# 1.self :指向类实例本身的引用。# 2.model=None :模型参数,可以传递模型实例,但在这段代码中没有使用。# 3.iterations=10 :调优过程中要进行的训练迭代次数。# 4.cleanup=True :是否在每次迭代后清理权重文件,以节省存储空间。def __call__(self, model=None, iterations=10, cleanup=True):# 调用 Tuner 实例时执行超参数演化过程。# 此方法遍历迭代次数,在每次迭代中执行以下步骤:# 1. 加载现有超参数或初始化新超参数。# 2. 使用 `mutate` 方法变异超参数。# 3. 使用变异超参数训练 YOLO 模型。# 4. 将适应度得分和变异超参数记录到 CSV 文件中。# 注意:# 该方法利用 `self.tune_csv` Path 对象读取和记录超参数和适应度得分。确保在 Tuner 实例中正确设置此路径。"""Executes the hyperparameter evolution process when the Tuner instance is called.This method iterates through the number of iterations, performing the following steps in each iteration:1. Load the existing hyperparameters or initialize new ones.2. Mutate the hyperparameters using the `mutate` method.3. Train a YOLO model with the mutated hyperparameters.4. Log the fitness score and mutated hyperparameters to a CSV file.Args:model (Model): A pre-initialized YOLO model to be used for training.iterations (int): The number of generations to run the evolution for.cleanup (bool): Whether to delete iteration weights to reduce storage space used during tuning.Note:The method utilizes the `self.tune_csv` Path object to read and log hyperparameters and fitness scores.Ensure this path is set correctly in the Tuner instance."""# 这段代码是 __call__ 方法的一部分,它负责初始化调优过程,并在每次迭代中变异超参数、训练模型,并保存相关的目录和权重文件。# 初始化计时器。记录调优过程开始的时间。t0 = time.time()# 初始化最佳结果变量。初始化变量以存储 最佳模型保存目录 和 最佳度量指标 。best_save_dir, best_metrics = None, None# 创建权重目录。在 self.tune_dir 路径下创建一个名为 weights 的目录,用于存储每次迭代生成的模型权重。 parents=True 表示如果父目录不存在则创建, exist_ok=True 表示如果目录已存在则不抛出异常。(self.tune_dir / "weights").mkdir(parents=True, exist_ok=True)# 迭代调优。循环进行指定次数的迭代, iterations 是方法的一个参数,表示要进行多少次调优迭代。for i in range(iterations):# Mutate hyperparameters# 变异超参数。调用 _mutate 方法生成新的超参数组合。mutated_hyp = self._mutate()# 记录当前迭代的超参数。使用 LOGGER 记录 当前迭代的编号 和 使用的超参数组合 。LOGGER.info(f"{self.prefix}Starting iteration {i + 1}/{iterations} with hyperparameters: {mutated_hyp}") # {self.prefix} 使用超参数开始迭代 {i + 1}/{iterations}:{mutated_hyp}。# 准备训练参数。# 初始化一个空字典,用于存储本次迭代的度量指标。metrics = {}# vars(object)# vars() 函数在 Python 中用于获取对象的属性字典。这个字典包含了对象的大部分属性,但不包括方法和其他一些特殊的属性。对于用户自定义的对象, vars() 返回的字典包含了对象的 __dict__ 属性,这是一个包含对象所有属性的字典。# 参数说明 :# object :要获取属性字典的对象。# 返回值 :# 返回指定对象的属性字典。# 注意事项 :# vars() 对于内置类型(如 int 、 float 、 list 等)返回的是一个包含魔术方法和特殊属性的字典,这些属性通常是不可访问的。# 对于自定义对象, vars() 返回的是对象的 __dict__ 属性,如果对象没有定义 __dict__ ,则可能返回一个空字典或者抛出 TypeError 。# 在 Python 3 中, vars() 也可以用于获取内置函数的全局变量字典。# vars() 函数是一个内置函数,通常用于调试和访问对象的内部状态,但在处理复杂对象时应该谨慎使用,因为直接修改对象的属性可能会导致不可预测的行为。# 将原始参数 self.args 和变异后的超参数 mutated_hyp 合并,形成完整的训练参数。train_args = {**vars(self.args), **mutated_hyp}# 调用 get_cfg 函数获取训练配置,并使用 get_save_dir 函数获取本次迭代的保存目录。# def get_save_dir(args, name=None): -> 获取或创建一个用于保存特定数据(如模型权重、日志等)的目录路径。返回保存目录的路径。返回一个 Path 对象,表示保存目录的路径。 -> return Path(save_dir)# def get_cfg(cfg: Union[str, Path, Dict, SimpleNamespace] = DEFAULT_CFG_DICT, overrides: Dict = None):# -> 从一个配置源(可以是字符串、路径、字典或 SimpleNamespace 对象)获取配置,并允许通过 overrides 字典来覆盖默认配置。返回一个 IterableSimpleNamespace 对象,它是一个可迭代的命名空间对象,其属性由 cfg 字典中的键值对初始化。# -> return IterableSimpleNamespace(**cfg)save_dir = get_save_dir(get_cfg(train_args))# 在本次迭代的保存目录下创建一个 weights 子目录,用于存储本次迭代的模型权重文件。weights_dir = save_dir / "weights"# 这段代码为每次迭代的模型训练和超参数调优做准备,包括创建必要的目录、生成新的超参数组合、记录日志以及准备训练参数。接下来的步骤将包括实际的训练模型、保存结果和选择最佳模型。# 这段代码是 __call__ 方法中负责训练模型的部分,它使用变异后的超参数来训练YOLO模型,并处理可能出现的异常。try:# Train YOLO model with mutated hyperparameters (run in subprocess to avoid dataloader hang)# 构建训练命令。构建一个包含 yolo train 命令和超参数的训练命令列表。这里使用列表推导式将 train_args 字典中的每个键值对转换为 key=value 格式的字符串,并展开到命令列表中。cmd = ["yolo", "train", *(f"{k}={v}" for k, v in train_args.items())]# 在子进程中运行训练命令。使用 subprocess.run 在子进程中执行训练命令。 check=True 参数表示如果命令执行失败(即返回码非0),则抛出异常。 returncode 属性存储命令的返回码。return_code = subprocess.run(cmd, check=True).returncode# 确定检查点文件。确定要加载的检查点文件。如果存在 best.pt 文件,则使用它;否则,使用 last.pt 文件。ckpt_file = weights_dir / ("best.pt" if (weights_dir / "best.pt").exists() else "last.pt")# 加载训练度量指标。使用PyTorch的 torch.load 函数加载检查点文件,并获取其中的 train_metrics 字典,该字典包含本次训练的度量指标。metrics = torch.load(ckpt_file)["train_metrics"]# 断言训练成功。断言命令的返回码为0,即训练成功。如果训练失败,将抛出异常。assert return_code == 0, "training failed" # 训练失败。# 异常处理。# 捕获训练过程中的任何异常。except Exception as e:# 如果发生异常,使用 LOGGER 记录警告信息,包括迭代编号和异常信息。LOGGER.warning(f"WARNING ❌️ training failure for hyperparameter tuning iteration {i + 1}\n{e}") # 警告❌️超参数调整迭代 {i + 1}\n{e} 训练失败。# 这段代码确保了即使训练过程中出现异常,也能被捕获并记录下来,同时避免了数据加载器挂起的问题,这是通过在子进程中运行训练命令实现的。如果训练失败,它还会清理并记录相关的错误信息,以便进一步分析和调试。# 数据加载器挂起是指在使用数据加载器(如PyTorch中的 DataLoader )进行数据加载和预处理时,程序出现停滞或无响应的状态。这种情况可能由多种原因引起,以下是一些常见的原因 :# 多线程/多进程问题 :# 如果 DataLoader 的 num_workers 参数设置不当,可能会导致数据加载器创建过多的子进程,从而消耗过多内存或导致资源竞争,使得数据加载器无法继续执行。# 资源限制 :# 系统资源不足,如CPU、内存或磁盘I/O能力不足,可能导致数据加载器无法及时读取或处理数据。# 数据问题 :# 数据集中可能包含损坏或格式不正确的数据,导致数据加载器在处理时卡住。# 数据预处理步骤可能存在错误或异常,如数据类型不匹配、维度错误等。# I/O瓶颈 :# 数据存储介质(如硬盘)的读写速度慢,或者网络延迟(如果数据存储在远程服务器上),可能导致数据加载器等待数据读取。# Python对象的Copy-On-Write(COW)机制 :# 在多进程环境中,Python的某些对象可能会触发COW机制,导致不必要的内存拷贝,增加内存消耗和延迟。# 系统负载过高 :# 系统可能因为运行了过多的任务或服务,导致负载过高,使得数据加载器无法获得足够的资源来执行。# 死锁或竞态条件 :# 在多线程或多进程环境中,不当的资源共享可能导致死锁或竞态条件,使得数据加载器无法继续执行。# 解决数据加载器挂起问题的常用方法包括 :# 调整 num_workers 参数 :# num_workers 参数控制着 DataLoader 创建的子进程数量。如果设置过大,可能会导致内存消耗过快,引起内存溢出;如果设置过小,可能会导致CPU无法及时向GPU提供数据。通常,这个值应该根据机器的CPU核心数和内存容量来合理设置。# 优化数据预处理 :# 确保数据预处理步骤没有错误或异常,比如对图像进行变换时使用了正确的参数。优化数据预处理流程可以减少内存占用和提高数据加载效率。# 检查数据集完整性 :# 确保数据集中没有损坏的文件、错误的路径或标签。在加载数据前,可以先对数据进行检查和清理,确保数据的完整性和正确性。# 使用 pin_memory :# 将 DataLoader 中的 pin_memory 参数设置为 True ,这可以加速数据从CPU到GPU的传输,但要求数据类型为tensors、maps或包含tensor的可迭代对象。# 避免使用Python内置的可变类型对象 :# 在多进程环境中,Python内置的可变类型对象(如list、dict)可能会触发copy-on-write机制,导致不必要的内存拷贝。可以使用non-refcounted对象(如pandas, numpy, pyarrow, torch tensor)来避免。# 管理共享资源 :# 使用 multiprocessing.Manager 来管理共享资源,如列表和字典,以避免多进程中的数据竞争问题。# 检查系统资源限制 :# 确保系统有足够的资源供 DataLoader 使用,如内存和磁盘空间。如果系统资源不足,可能会导致工作进程无法正常运行。# 错误日志分析 :# 当 DataLoader 工作进程退出时,通常会在错误日志中打印出具体的异常信息。查看错误日志是定位问题的关键。# 避免竞态条件和死锁 :# 在并发编程中,使用互斥机制(如锁)来确保共享资源在同一时间只被一个线程访问,避免不必要的共享,减少竞态条件和死锁的发生。# 减小 batch_size :# 降低每次迭代所需要的内存,通过减小 batch_size 来减少内存压力。# 数据加载器异常处理 :# 在迭代过程中添加异常处理逻辑,捕获如“StopIteration”等异常,并在异常发生时采取适当的操作。# 合理分配资源 :# 合理地分配和释放资源,避免资源的浪费和过度竞争。# 通过上述方法,可以有效地解决数据加载器挂起的问题,提高数据加载的效率和稳定性。# Save results and mutated_hyp to CSV# 这段代码负责将每次迭代的结果和变异后的超参数组合保存到CSV文件中。# 计算适应度值。从 metrics 字典中获取 fitness 键对应的值,如果不存在则默认为0.0。适应度值是评估模型性能的一个指标。fitness = metrics.get("fitness", 0.0)# 构建日志行。创建一个列表 log_row ,首先包含四舍五入到5位小数的适应度值,然后追加变异后的超参数值。这里使用列表推导式从 mutated_hyp 字典中获取每个超参数的值。log_row = [round(fitness, 5)] + [mutated_hyp[k] for k in self.space.keys()]# 检查文件是否存在并写入表头。# 检查 self.tune_csv 路径指向的CSV文件是否存在。如果不存在,则创建一个包含列名的字符串 headers ,列名包括 fitness 和 self.space 中的所有超参数名称,列名之间用逗号分隔,并在末尾添加换行符。headers = "" if self.tune_csv.exists() else (",".join(["fitness"] + list(self.space.keys())) + "\n")# 写入CSV文件。# 以追加模式( "a" )打开CSV文件,如果文件不存在则创建。with open(self.tune_csv, "a") as f:# map(function, iterable, ...)# Python中的 map() 函数是一个内置函数,用于将一个函数应用于一个可迭代对象(如列表、元组等)的每个元素,并返回一个新的迭代器。# 参数 :# function :一个函数,它将被应用于 iterable 中的每个元素。# iterable :一个或多个可迭代对象,其元素将被传递给 function 。# 返回值 :# map() 函数返回一个迭代器,该迭代器生成将 function 应用于 iterable 中每个元素后的结果。# map() 也可以接受多个可迭代对象,并将函数应用于对应元素。# 将 headers 和 log_row 列表转换为字符串后用逗号分隔,然后写入文件。 map(str, log_row) 确保列表中的所有元素都被转换为字符串格式,以便写入CSV文件。# map(str, log_row) 是一个Python表达式,它使用内置的 map 函数将 log_row 列表中的每个元素转换为字符串类型。# map(str, log_row) 会对 log_row 列表中的每个元素应用 str 函数,即将列表中的每个元素(可能是整数、浮点数或其他类型)转换为字符串。这样做是为了确保所有的数据都能被正确地写入CSV文件,因为CSV文件中的数据通常以文本格式存储。f.write(headers + ",".join(map(str, log_row)) + "\n")# 这段代码确保了每次迭代的结果都被记录和保存,便于后续分析和比较不同超参数组合的性能。如果CSV文件是新创建的,它还会包含列名作为文件的首行。# Get best results# 这段代码是 __call__ 方法中用于获取最佳调优结果的部分。它从CSV文件中加载所有迭代的结果,找到具有最佳适应度值的迭代,并根据条件保存最佳模型的权重文件。# 加载CSV文件中的数据。使用NumPy的 loadtxt 函数加载CSV文件中的数据。 ndmin=2 确保结果至少是二维数组, delimiter="," 指定了CSV的分隔符, skiprows=1 跳过第一行(通常是标题行)。x = np.loadtxt(self.tune_csv, ndmin=2, delimiter=",", skiprows=1)# 提取适应度值。提取第一列的值,假设这一列包含了适应度评分。fitness = x[:, 0] # first column# 找到最佳适应度值的索引。使用NumPy的 argmax 函数找到适应度值最大的索引,即最佳迭代。best_idx = fitness.argmax()# 检查最佳迭代是否是当前迭代。如果最佳迭代的索引等于当前迭代的索引 i ,则 best_is_current 为 True 。best_is_current = best_idx == i# 保存最佳模型。# 如果当前迭代是最佳迭代( best_is_current 为True),则执行以下操作。if best_is_current:# 保存最佳模型的保存目录。best_save_dir = save_dir# 将最佳度量指标四舍五入到5位小数,并保存。best_metrics = {k: round(v, 5) for k, v in metrics.items()}# 将当前迭代的权重文件复制到 self.tune_dir / "weights" 目录下。for ckpt in weights_dir.glob("*.pt"):shutil.copy2(ckpt, self.tune_dir / "weights")# 清理权重目录。# 如果当前迭代不是最佳迭代,并且 cleanup 参数为True,则执行以下操作。elif cleanup:# 删除当前迭代的权重目录,以减少存储空间的使用。 ignore_errors=True 参数表示如果目录不存在或删除过程中出现错误,则忽略这些错误。shutil.rmtree(weights_dir, ignore_errors=True) # remove iteration weights/ dir to reduce storage space# 这段代码确保了在调优过程中,只有最佳模型的权重文件被保存,而其他迭代的权重文件在迭代结束后被清理,从而节省存储空间。同时,它还记录了最佳迭代的度量指标和保存目录,以便后续分析和模型部署。# 这段代码是 __call__ 方法中用于绘制调优结果图表、保存和打印调优结果的部分。# Plot tune results# 绘制调优结果图表。调用 plot_tune_results 函数,传入 self.tune_csv 作为参数,该函数负责读取CSV文件并绘制调优结果的图表。plot_tune_results(self.tune_csv)# Save and print tune results# 构建日志头部信息。# 构建一个包含调优过程信息的字符串,包括 当前迭代次数 、 总迭代次数 、 已耗时 、 结果保存位置 、 最佳适应度值 、 最佳适应度指标 、 最佳模型保存位置 和 最佳超参数的信息 。header = (f'{self.prefix}{i + 1}/{iterations} iterations complete ✅ ({time.time() - t0:.2f}s)\n' # '{self.prefix}{i + 1}/{iterations} 次迭代完成✅ ({time.time() - t0:.2f}s)。# def colorstr(*input):# -> 函数通过遍历 args 中的每个元素(颜色或样式),从 colors 字典中获取对应的ANSI转义序列,并将其与传入的 string 字符串连接起来。最后,它还会添加一个 colors["end"] 序列,用于重置终端的颜色和样式到默认状态。# -> return "".join(colors[x] for x in args) + f"{string}" + colors["end"]f'{self.prefix}Results saved to {colorstr("bold", self.tune_dir)}\n' # {self.prefix}结果保存至 {colorstr("bold", self.tune_dir)}。f'{self.prefix}Best fitness={fitness[best_idx]} observed at iteration {best_idx + 1}\n' # {self.prefix}在迭代 {best_idx + 1} 时观察到的最佳适应度 = {fitness[best_idx]}。f'{self.prefix}Best fitness metrics are {best_metrics}\n' # {self.prefix}最佳适应度指标为 {best_metrics}。f'{self.prefix}Best fitness model is {best_save_dir}\n' # {self.prefix}最佳适应度模型为 {best_save_dir}。f'{self.prefix}Best fitness hyperparameters are printed below.\n' # {self.prefix}最佳适应度超参数打印如下。)# 记录日志。使用 LOGGER 记录构建的头部信息。LOGGER.info("\n" + header)# 保存最佳超参数。从加载的数据 x 中提取最佳适应度值对应的超参数值,并构建一个字典 data 。data = {k: float(x[best_idx, i + 1]) for i, k in enumerate(self.space.keys())}# 保存最佳超参数到YAML文件。# 调用 yaml_save 函数,将最佳超参数和头部信息保存到YAML文件中。 remove_colorstr 函数用于移除字符串中的颜色代码, header.replace(self.prefix, "# ") 用于替换日志头部中的前缀。# def yaml_save(file="data.yaml", data=None, header=""): -> 将 Python 数据结构保存为 YAML 格式的文件。yaml_save(self.tune_dir / "best_hyperparameters.yaml",data=data,# def remove_colorstr(input_string):# -> 从输入字符串中移除所有的 ANSI 转义序列,这些序列通常用于在终端中设置文本颜色和样式。将 input_string 中所有匹配的 ANSI 转义序列替换为空字符串(即移除它们)。# -> return ansi_escape.sub("", input_string)header=remove_colorstr(header.replace(self.prefix, "# ")) + "\n",)# 打印YAML文件内容。调用 yaml_print 函数,打印YAML文件的内容,以便用户查看最佳超参数。# def yaml_print(yaml_file: Union[str, Path, dict]) -> None: -> 是打印 YAML 文件的内容。yaml_print(self.tune_dir / "best_hyperparameters.yaml")# 这段代码确保了调优过程中的关键信息被记录下来,并且最佳超参数被保存和打印出来,方便用户分析和进一步使用。 yaml_save 和 yaml_print 函数是自定义的,用于处理YAML文件的保存和打印。# 这个方法实现了一个完整的超参数调优流程,包括变异、训练、结果记录和最佳结果的选择。通过这种方式,可以系统地探索超参数空间,以找到最佳的模型配置。