autoanchor.py
utils\autoanchor.py
目录
autoanchor.py
1.所需的库和模块
2.def check_anchor_order(m):
3.def check_anchors(dataset, model, thr=4.0, imgsz=640):
4.def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
1.所需的库和模块
import random
import numpy as np
import torch
import yaml
from tqdm import tqdm
from utils import TryExcept
from utils.general import LOGGER, TQDM_BAR_FORMAT, colorstr
PREFIX = colorstr('AutoAnchor: ')
2.def check_anchor_order(m):
# 这段代码定义了一个名为 check_anchor_order 的函数,它用于检查目标检测模型中的锚点(anchor)顺序是否与其对应的特征图(feature map)的步长(stride)顺序相匹配。如果锚点顺序与步长顺序不一致,函数将反转锚点的顺序。
# 定义了一个函数 check_anchor_order ,它接受一个参数。
# 1.m :这个参数代表模型,可能是一个包含锚点和步长的属性的对象。
def check_anchor_order(m):
# Check anchor order against stride order for YOLOv5 Detect() module m, and correct if necessary 检查 YOLOv5 Detect() 模块 m 的锚点顺序与步幅顺序,并在必要时进行更正。
# 计算每个输出层的锚点面积。 m.anchors.prod(-1) 计算每个锚点的面积(假设锚点是以宽度和高度表示的), .mean(-1) 计算每个输出层的锚点面积的平均值, .view(-1) 将结果展平为一维数组。
# 这行代码是用于计算模型中每个输出层的锚点(anchor)平均面积的。
# m.anchors.prod(-1) : m.anchors 是一个包含锚点尺寸的张量。在目标检测模型中,锚点通常表示为宽度和高度的一对值。 .prod(-1) 是一个 PyTorch 操作,它计算张量中每个元素的宽度和高度的乘积,即每个锚点的面积。 -1 表示沿着最后一个维度(即每个锚点的宽度和高度)进行操作。
# .mean(-1) : .mean(-1) 是另一个 PyTorch 操作,它计算每个输出层的所有锚点面积的平均值。在目标检测模型中,可能有多个输出层,每个输出层可能有不同的锚点尺寸。 再次使用 -1 表示沿着最后一个维度(即每个输出层的所有锚点)进行操作。
# .view(-1) : .view(-1) 是一个 PyTorch 操作,它将张量展平成一维数组。 -1 表示让 PyTorch 自动计算这个维度的大小,以便将张量展平成一维。
# 这行代码计算了模型中每个输出层的锚点平均面积,并将结果存储在一个一维张量 a 中。这个结果可以用来分析锚点尺寸的变化趋势,或者用于检查锚点顺序是否与特征图的步长顺序一致。
a = m.anchors.prod(-1).mean(-1).view(-1) # mean anchor area per output layer
# 计算锚点面积从第一个输出层到最后一个输出层的变化量 da 。
da = a[-1] - a[0] # delta a
# 计算步长从第一个输出层到最后一个输出层的变化量 ds 。
ds = m.stride[-1] - m.stride[0] # delta s
# 检查锚点面积的变化量 da 和步长的变化量 ds 的符号是否相反。如果 da 和 ds 的符号相反,即锚点面积和步长的变化趋势不一致,那么需要反转锚点的顺序。
if da and (da.sign() != ds.sign()): # same order
# 如果需要反转锚点顺序,则记录一条信息日志,表明锚点顺序被反转了。 LOGGER 是一个已经配置好的日志记录器对象, PREFIX 是一个定义好的前缀字符串。
LOGGER.info(f'{PREFIX}Reversing anchor order')
# 反转锚点的顺序。 m.anchors.flip(0) 沿着第一个维度(通常是输出层的维度)反转锚点数组。
m.anchors[:] = m.anchors.flip(0)
# 这个函数通常用于目标检测模型中,以确保锚点的排列顺序与特征图的步长顺序一致,这对于模型的性能和准确性是重要的。
3.def check_anchors(dataset, model, thr=4.0, imgsz=640):
# 这段代码定义了一个名为 check_anchors 的函数,它用于检查目标检测模型中的锚点(anchor)是否适合给定的数据集,并在必要时重新计算锚点。
# 这是一个装饰器,用于捕获函数执行过程中的异常,并在异常发生时打印错误信息。 PREFIX 是一个变量,用于在错误信息前添加前缀。
# class TryExcept(contextlib.ContextDecorator):
# -> 这个上下文管理器的作用是在代码块执行过程中捕获异常,并在异常发生时打印一条包含自定义消息和异常值的消息。
# -> def __init__(self, msg=''):
@TryExcept(f'{PREFIX}ERROR')
# 定义了一个名为 check_anchors 的函数,它接受四个参数。
# 1.dataset :数据集。
# 2.model :模型。
# 3.thr :阈值,默认为4.0。
# 4.imgsz :图像尺寸,默认为640。
def check_anchors(dataset, model, thr=4.0, imgsz=640):
# Check anchor fit to data, recompute if necessary 检查锚点是否适合数据,如有必要重新计算。
# 这段代码是函数 check_anchors 的一部分,它负责处理模型和数据集的信息,以便于后续的锚点检查和调整。
# 检查 model 对象是否有 module 属性。这个属性通常存在于使用PyTorch的DataParallel包装过的模型中,用于区分模型是在单个GPU上运行还是在多个GPU上并行运行。
# 如果 model 有 module 属性,那么它就取 model.module.model 的最后一个元素,这通常是模型的检测层。如果没有 module 属性,那么就直接取 model.model 的最后一个元素。
# # Detect() 是一个注释,说明这行代码的作用是检测模型是否被包装在DataParallel中。
m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect()
# 计算每个图像的缩放比例。 dataset.shapes 是一个包含每个图像尺寸的数组, imgsz 是目标图像尺寸。 dataset.shapes.max(1, keepdims=True) 计算每个图像尺寸中的最大值,并保持维度不变,这样可以得到一个与 dataset.shapes 形状相同的最大值数组。 然后,将每个图像尺寸除以对应的最大值,再乘以 imgsz ,得到缩放后的形状。
shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
# np.random.uniform(low=0.0, high=1.0, size=None)
# np.random.uniform() 是 NumPy 库中的一个函数,用于生成在指定范围内均匀分布的随机数。
# 参数说明 :
# low :随机数生成的下限,默认为0.0。
# high :随机数生成的上限,默认为1.0。
# size :输出数组的形状。如果为 None ,则返回一个单一的随机数。如果指定了 size ,则返回一个随机数数组。
# 返回值 :
# 返回一个或一组随机数,取决于 size 参数的设置。
# 功能说明 :
# np.random.uniform() 函数从指定的区间 [low, high) 中生成均匀分布的随机数。这意味着随机数可以取到 low 的值,但是取不到 high 的值。
# 生成一个随机缩放因子数组 scale ,用于数据增强。缩放因子的范围是0.9到1.1,这意味着图像可以在原始尺寸的基础上随机缩放至90%到110%。 shapes.shape[0] 是图像的数量,所以 size=(shapes.shape[0], 1) 创建了一个与图像数量相同行数的列向量。
scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale
# 计算所有图像中目标的宽度和高度,并将其转换为PyTorch张量。
# shapes * scale 将每个图像的尺寸与随机缩放因子相乘,得到缩放后的尺寸。
# dataset.labels 是一个包含所有图像标签的数组,其中每个标签的第3和第4列分别代表目标的宽度和高度。
# l[:, 3:5] 提取每个标签的宽度和高度, l[:, 3:5] * s 将这些宽度和高度与缩放后的尺寸相乘。
# zip(shapes * scale, dataset.labels) 将缩放后的尺寸与标签配对,然后通过列表推导式计算每个目标的缩放后的宽度和高度。
# np.concatenate(...) 将所有目标的宽度和高度连接成一个数组。
# torch.tensor(...).float() 将NumPy数组转换为PyTorch张量,并确保数据类型为浮点数。
wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float() # wh
# 这些步骤为后续的锚点质量评估和可能的锚点调整提供了必要的数据。
# 这段代码定义了一个内部函数 metric ,用于计算锚点的质量指标。
# 定义了一个名为 metric 的函数,它接受一个参数。
# 1.k :这个参数代表一组锚点。
def metric(k): # compute metric
# 计算每个 目标 的 宽度 和 高度 与每个 锚点 的 宽度 和 高度 的比率。 wh 是一个包含所有目标宽度和高度的张量, k 是一组锚点的宽度和高度。 wh[:, None] 将 wh 的第二维扩展为1, k[None] 将 k 的第一维扩展为1,这样就可以进行广播除法。
# 这行代码是函数 metric 的一部分,用于计算每个目标的宽度和高度与每个锚点的宽度和高度的比率。
# wh[:, None] :
# wh 是一个包含所有目标宽度和高度的张量,其形状为 [N, 2] ,其中 N 是目标的数量。
# [:, None] 是一个索引操作,它在 wh 的第二维上添加一个新维度,使得 wh 的形状变为 [N, 1, 2] 。
# k[None] :
# k 是一组锚点的宽度和高度,其形状为 [M, 2] ,其中 M 是锚点的数量。
# [None] 是一个索引操作,它在 k 的第一维上添加一个新维度,使得 k 的形状变为 [1, M, 2] 。
# wh[:, None] / k[None] :
# 这是一个广播除法操作。由于 wh 的形状为 [N, 1, 2] , k 的形状为 [1, M, 2] ,它们可以进行广播除法。
# 广播除法的结果是一个形状为 [N, M, 2] 的张量,其中每个元素 r[i, j, :] 表示第 i 个目标的宽度和高度与第 j 个锚点的宽度和高度的比率。
# 这个操作的结果是一个三维张量 r ,它包含了所有目标与所有锚点之间的宽度和高度比率。这个比率将用于后续的计算,以评估锚点的质量。
# 在Python的PyTorch库中,当你对两个张量进行逐元素的乘法(element-wise multiplication)或除法(element-wise division)时,PyTorch会应用广播(broadcasting)机制。广播机制允许你对不同形状的张量进行数学运算,只要它们在某些维度上是兼容的。
# 在提到的情况中,有两个张量 :
# wh[:, None] 的形状为 [N, 1, 2] ,其中 N 是目标的数量, 1 是新添加的维度, 2 代表每个目标的宽度和高度。
# k[None] 的形状为 [1, M, 2] ,其中 M 是锚点的数量, 1 是新添加的维度, 2 代表每个锚点的宽度和高度。
# 广播规则如下 :
# 如果两个张量在某个维度上的大小相同,或者其中一个张量在该维度上的大小为1,那么这两个张量在该维度上是兼容的。
# 如果一个张量在某个维度上的大小为1,那么在进行运算时,该维度会被“拉伸”以匹配另一个张量在该维度上的大小。
# 在本例中 :
# 第一个张量 [N, 1, 2] 的第二个维度是 1 ,第二个张量 [1, M, 2] 的第一个维度也是 1 。根据广播规则,这两个 1 维度会被拉伸以匹配对方的大小,即第一个张量的 1 会被拉伸到 M ,第二个张量的 1 会被拉伸到 N 。
# 这样,两个张量在每个维度上都变得兼容:第一个维度 N 和 N (通过拉伸 1 ),第二个维度 M 和 M (通过拉伸 1 ),第三个维度 2 和 2 (它们已经是相同的)。
# 因此,当你对这两个张量进行除法运算时,结果的形状会是 [N, M, 2] ,其中 N 是目标的数量, M 是锚点的数量, 2 代表每个计算结果包含宽度和高度两个值。这个结果张量包含了每个目标与每个锚点之间的宽度和高度比率。
# 结果张量包含了每个目标与每个锚点之间的宽度和高度比率,是因为在进行广播除法时,PyTorch会逐个元素地对两个张量进行操作。具体来说,对于张量 wh[:, None] 和 k[None] ,它们的操作如下 :
# wh[:, None] 的形状为 [N, 1, 2] ,这意味着它有 N 个目标,每个目标有两个维度(宽度和高度)。
# k[None] 的形状为 [1, M, 2] ,这意味着它有 M 个锚点,每个锚点也有两个维度(宽度和高度)。
# 当执行 wh[:, None] / k[None] 时,PyTorch会按照以下步骤进行广播和除法 :
# 第一个张量的 N 个目标会与第二个张量的 M 个锚点分别进行比较,因为第一个张量的第二个维度( 1 )和第二个张量的第一个维度( 1 )都会被广播到对方的形状,即 M 和 N 。
# 对于 wh 中的每个目标,它的宽度和高度会与 k 中的每个锚点的宽度和高度进行比较,产生一个 2 元素的向量,其中包含目标与锚点宽度的比率和高度的比率。
# 由于 wh 有 N 个这样的 2 元素向量, k 有 M 个锚点,所以最终结果是 N 乘以 M 个这样的 2 元素向量,排列成一个形状为 [N, M, 2] 的张量。
# 具体来说,结果张量 r 中的每个元素 r[i, j, :] 是这样计算的 :
# r[i, j, 0] 是第 i 个目标的宽度与第 j 个锚点的宽度的比率。
# r[i, j, 1] 是第 i 个目标的高度与第 j 个锚点的高度的比率。
# 因此,结果张量 r 包含了每个目标与每个锚点之间的宽度和高度比率,这为后续计算提供了必要的数据,以便评估锚点的质量。这些比率将用于确定哪些锚点最适合每个目标,以及锚点的整体性能如何。
r = wh[:, None] / k[None]
# 计算每个目标与每个锚点的最佳比例匹配度。 torch.min(r, 1 / r) 计算每个比率和其倒数的最小值,这样可以得到一个比例度量,范围在0到1之间。 .min(2)[0] 在第二维(即每个目标与所有锚点的比例度量)上取最小值,得到每个目标与最佳锚点的比例度量。
# 在表达式 x = torch.min(r, 1 / r).min(2)[0] 中, .min(2) 指的是在张量 r 的第2个维度上取最小值。这里的张量 r 的形状是 [N, M, 2] ,其中 :
# N 是目标的数量。
# M 是锚点的数量。
# 2 代表每个目标与每个锚点之间的宽度和高度比率。
# 因此, .min(2) 会在每个 [N, M] 位置上的两个元素(即宽度和高度的比率)中取最小值,得到一个形状为 [N, M] 的张量,其中每个元素代表对应目标与锚点之间的最佳比例匹配度。
# 表达式 x = torch.min(r, 1 / r).min(2)[0] 中,
# 首先计算 torch.min(r, 1 / r) ,这会得到一个与 r 形状相同的张量,即 [N, M, 2] ,其中每个元素是 r 中对应元素和其倒数的最小值。
# 接下来, .min(2) 在这个结果张量的第2个维度上取最小值,即在每个 [N, M] 位置上的两个元素中取最小值。这将得到一个形状为 [N, M] 的张量,其中每个元素代表对应目标与锚点之间的最佳比例匹配度。
# 因此, x 的形状是 [N, M] 。
x = torch.min(r, 1 / r).min(2)[0] # ratio metric
# 对于每个目标,找到最佳的比例匹配度。 .max(1)[0] 在第一维(即所有目标与最佳锚点的比例度量)上取最大值。
# 在表达式 best = x.max(1)[0] 中, .max(1) 指的是在张量 x 的第1个维度上取最大值。这里的张量 x 的形状是 [N, M] ,其中 :
# N 是目标的数量。
# M 是锚点的数量。
# 因此, .max(1) 会在每个 [N] 位置上的 M 个元素中取最大值,得到一个形状为 [N] 的张量,其中每个元素代表对应目标与所有锚点之间的最佳比例匹配度的最大值。
# 所以, best 的形状是 [N] 。
best = x.max(1)[0] # best_x
# 计算超过阈值的锚点比例。 x > 1 / thr 创建一个布尔张量,表示每个目标与最佳锚点的比例度量是否超过阈值。 .float() 将布尔张量转换为浮点张量, .sum(1) 在第一维上求和,得到每个目标超过阈值的锚点数量。 .mean() 计算所有目标超过阈值的锚点数量的平均值。
# 在表达式 aat = (x > 1 / thr).float().sum(1).mean() 中, .sum(1) 指的是在张量的第1个维度上求和。这里的张量 x 的形状是 [N, M] ,其中 :
# N 是目标的数量。
# M 是锚点的数量。
# 首先, (x > 1 / thr) 会创建一个与 x 形状相同的布尔张量,其中每个元素表示 x 中对应元素是否大于 1 / thr 。
# 接着, .float() 将这个布尔张量转换为浮点张量,其中 True 变为 1.0 , False 变为 0.0 。
# 然后, .sum(1) 在每个 [N] 位置上的 M 个元素中求和,得到一个形状为 [N] 的张量,其中每个元素代表对应目标超过阈值的锚点数量。
# 最后, .mean() 计算这个形状为 [N] 的张量中所有元素的平均值,得到一个标量,表示 平均每个目标超过阈值的锚点数量 。
# 因此, aat 的形状是一个标量。
aat = (x > 1 / thr).float().sum(1).mean() # anchors above threshold
# 计算最佳可能的召回率。 best > 1 / thr 创建一个布尔张量,表示每个目标的最佳比例匹配度是否超过阈值。 .float() 将布尔张量转换为浮点张量, .mean() 计算所有目标的最佳比例匹配度超过阈值的平均值。
# 在表达式 bpr = (best > 1 / thr).float().mean() 中, best 的形状是 [N] ,其中 N 是目标的数量。
# 首先, (best > 1 / thr) 会创建一个与 best 形状相同的布尔张量,其中每个元素表示 best 中对应元素是否大于 1 / thr 。
# 接着, .float() 将这个布尔张量转换为浮点张量,其中 True 变为 1.0 , False 变为 0.0 。
# 然后, .mean() 计算这个形状为 [N] 的张量中所有元素的平均值,得到一个标量,表示 最佳可能的召回率 。
# 因此, bpr 的形状是一个标量。
bpr = (best > 1 / thr).float().mean() # best possible recall
# 返回 最佳可能的召回率 和 超过阈值的锚点比例 。 bpr : 最佳可能的召回率 。 aat : 平均每个目标超过阈值的锚点数量 。
return bpr, aat
# 这个函数的目的是评估一组锚点对于给定数据集的质量,通过计算最佳可能的召回率和超过阈值的锚点比例来衡量。这些指标可以帮助我们确定当前的锚点是否适合数据集,或者是否需要重新计算新的锚点。
# 这段代码是用于评估目标检测模型中锚点(anchors)的性能,并构建一条包含性能指标的日志信息。
# m.stride 获取模型中每个特征层的步长(stride),这通常是指特征图上一个像素点对应输入图像上多少像素点的比例。
# .to(m.anchors.device) 将步长张量移动到与模型锚点相同的设备(例如,如果锚点在GPU上,则步长也会被移动到GPU)。
# .view(-1, 1, 1) 将步长张量重塑为一个三维张量,其中 -1 表示自动计算该维度的大小,确保所有步长值都被包含在内,而 1, 1 表示在最后两个维度上各有一个元素,这样做是为了在后续计算中保持维度的一致性。
stride = m.stride.to(m.anchors.device).view(-1, 1, 1) # model strides
# m.anchors.clone() 创建模型中锚点的副本,以避免直接修改原始锚点。
# * stride 将每个锚点乘以对应的步长,以得到 实际的锚点尺寸 。这是因为模型中的锚点通常是相对于特征图的尺寸,需要乘以步长来转换为输入图像的尺寸。
anchors = m.anchors.clone() * stride # current anchors
# anchors.cpu() 将锚点张量移动到CPU(如果它们不在CPU上)。
# .view(-1, 2) 将锚点张量重塑为一个二维张量,其中 -1 表示自动计算该维度的大小, 2 表示每个锚点有两个维度(宽度和高度)。
# metric() 是一个函数,用于计算锚点的性能指标,包括 最佳可能的召回率 (Best Possible Recall, BPR)和 超过阈值的锚点比例 (Anchors Above Threshold, AAT)。
bpr, aat = metric(anchors.cpu().view(-1, 2))
# 构建一个字符串 s ,其中包含锚点比例(AAT)和最佳可能的召回率(BPR),并格式化为两位小数和三位小数。
s = f'\n{PREFIX}{aat:.2f} anchors/target, {bpr:.3f} Best Possible Recall (BPR). ' # {PREFIX}{aat:.2f} 锚点/目标,{bpr:.3f} 最佳召回率(BPR)。
# 这条日志信息用于报告当前锚点的性能, aat 表示有多少比例的锚点与目标的宽高比在给定的阈值之内, bpr 表示在给定的阈值下,模型能够达到的最佳召回率。这些指标对于评估和调整锚点至关重要,因为它们直接影响目标检测模型的性能。
# 这段代码是 check_anchors 函数的主体部分,它根据 最佳可能的召回率 (BPR)来决定是否需要重新计算锚点。
# 这里设置了一个阈值,如果计算出的最佳可能的召回率(BPR)大于0.98,则认为当前锚点已经很好地适合数据集,不需要重新计算。
if bpr > 0.98: # threshold to recompute
# 如果BPR大于0.98,使用日志记录器 LOGGER 记录一条信息,表示当前锚点适合数据集,并显示一个绿色的勾选标记(✅)。
LOGGER.info(f'{s}Current anchors are a good fit to dataset ✅') # {s}当前锚点与数据集非常契合✅。
# 如果BPR不大于0.98,即锚点不适合数据集,执行以下操作。
else:
# 记录一条警告信息,表示锚点不适合数据集,并提示正在尝试改进。
LOGGER.info(f'{s}Anchors are a poor fit to dataset ⚠️, attempting to improve...') # {s}Anchors 与数据集⚠️的契合度较差,正在尝试改进……
# 计算模型中锚点的总数, numel() 返回张量中元素的总数,因为每个锚点有两个维度(宽度和高度),所以除以2得到锚点的数量。
na = m.anchors.numel() // 2 # number of anchors
# 使用K均值聚类算法( kmean_anchors )根据数据集中的目标边界框重新计算锚点。 n=na 指定了要生成的锚点数量, img_size=imgsz 指定了图像的尺寸, thr=thr 指定了锚点质量评估的阈值, gen=1000 指定了聚类算法的迭代次数, verbose=False 表示不输出详细的日志信息。
anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
# 使用新的锚点计算最佳可能的召回率(BPR)。
new_bpr = metric(anchors)[0]
# 如果新计算的BPR大于原来的BPR,表示新的锚点比原来的更好。
if new_bpr > bpr: # replace anchors
# 将新锚点转换为PyTorch张量,并确保它们与模型中的锚点在同一个设备上,且数据类型相同。
anchors = torch.tensor(anchors, device=m.anchors.device).type_as(m.anchors)
# 用新锚点替换模型中的原始锚点。
m.anchors[:] = anchors.clone().view_as(m.anchors)
# 确保锚点的顺序正确,它们必须在像素空间中,而不是网格空间中。
check_anchor_order(m) # must be in pixel-space (not grid-space)
# 将新锚点除以步长,转换回特征图空间中的锚点。
m.anchors /= stride
# 构建一条日志信息,表示锚点更新完成,并提示用户可以选择更新模型配置文件以使用新的锚点。
s = f'{PREFIX}Done ✅ (optional: update model *.yaml to use these anchors in the future)' # {PREFIX}完成✅(可选:更新模型*.yaml以便将来使用这些锚点)。
# 如果新计算的BPR不大于原来的BPR,表示新的锚点没有原来的好。
else:
# 构建一条日志信息,表示锚点更新完成,但使用原始锚点,因为它们比新锚点更好。
s = f'{PREFIX}Done ⚠️ (original anchors better than new anchors, proceeding with original anchors)' # {PREFIX}完成⚠️(原始锚点比新锚点更好,继续使用原始锚点)。
# 记录最终的锚点更新结果。
LOGGER.info(s)
# 这段代码的目的是确保模型使用的锚点尽可能地适合数据集,以提高目标检测的准确性。如果当前锚点不适合,它会尝试通过K均值聚类算法来找到更好的锚点。如果新锚点表现更好,则更新模型中的锚点;否则,保留原始锚点。
# 这个函数的目的是确保模型中的锚点适合给定的数据集,以提高目标检测的准确性。它通过计算锚点的质量指标,并在必要时使用 k-means 算法重新计算锚点来实现这一目标。
4.def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
# 这段代码定义了一个名为 kmean_anchors 的函数,它使用 K-means 聚类算法和遗传算法来优化目标检测模型中的锚点(anchors)。
# 定义了一个名为 kmean_anchors 的函数,它接受六个参数。
# 1.dataset : 数据集的路径,默认为 './data/coco128.yaml' 。
# 2.n : 要生成的锚点(anchors)数量,默认为9。
# 3.img_size : 图像的尺寸,默认为640。
# 4.thr : 用于评估锚点质量的阈值,默认为4.0。
# 5.gen : 遗传算法的迭代次数,默认为1000。
# 6.verbose : 是否输出详细的日志信息,默认为True。
def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
# 从训练数据集创建 kmeans 进化的锚点。
""" Creates kmeans-evolved anchors from training dataset
Arguments:
dataset: path to data.yaml, or a loaded dataset
n: number of anchors
img_size: image size used for training
thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0
gen: generations to evolve anchors using genetic algorithm
verbose: print all results
Return:
k: kmeans evolved anchors
Usage:
from utils.autoanchor import *; _ = kmean_anchors()
"""
# 从 scipy.cluster.vq 模块导入 kmeans 函数,该函数用于执行K-means聚类算法。
# scipy.cluster.vq.kmeans(X, k_or_init, iter=100, thresh=1e-5)
# scipy.cluster.vq.kmeans 是 SciPy 库中的一个函数,用于执行 K-means 聚类算法。
# 参数 :
# X :待聚类的数据点,通常是一个二维数组,其中每行代表一个数据点,每列代表一个特征。
# k_or_init :要生成的聚类中心(centroids)的数量 k ,或者是一个初始化聚类中心的数组。
# iter :(可选)最大迭代次数,默认为 100。
# thresh :(可选)停止算法的阈值,当聚类中心的变化小于这个阈值时,算法停止迭代,默认为 1e-5 。
# 该函数返回以下值 :
# centroids :形状为 (k, n) 的数组,其中 k 是聚类中心的数量, n 是特征的数量。
# labels :形状为 (m,) 的数组,其中 m 是输入数据点的数量,表示每个数据点属于哪个聚类中心。
# inertia :聚类中心的惯性,即所有数据点到其最近聚类中心的欧氏距离平方和。
# order :包含每次迭代后聚类中心索引的数组。
# 请注意, scipy.cluster.vq.kmeans 函数的行为可能会根据 SciPy 的不同版本有所变化,所以具体的参数和返回值可能需要根据实际使用的 SciPy 版本进行调整。
from scipy.cluster.vq import kmeans
# 将 numpy.random 模块简化为 npr ,用于后续的随机数生成。
npr = np.random
# 计算阈值的倒数。由于传入的 thr 参数是锚点质量评估的阈值,计算其倒数可以在后续计算中方便地使用。例如,如果原始阈值是4.0,那么其倒数是0.25,这将用于确定锚点是否适合目标(即锚点与目标的宽高比是否在1/thr到thr之间)。
thr = 1 / thr
# 这段代码定义了一个名为 metric 的函数,它用于计算锚点(anchors)与真实目标(ground truth boxes)之间的匹配度量。
# 定义了一个名为 metric 的函数,它接受两个参数。
# 1.k :代表锚点的宽度和高度,形状为 [n, 2] ,其中 n 是锚点的数量。
# 2.wh :代表真实目标的宽度和高度,形状为 [N, 2] ,其中 N 是目标的数量。
def metric(k, wh): # compute metrics
# 计算每个真实目标与每个锚点之间的宽度和高度比率。这里使用 wh[:, None] 将 wh 张量的形状从 [N, 2] 变为 [N, 1, 2] ,使用 k[None] 将 k 张量的形状从 [n, 2] 变为 [1, n, 2] ,以便进行广播运算。
# 结果 r 是一个形状为 [N, n, 2] 的张量,其中每个元素 r[i, j, :] 包含了第 i 个目标与第 j 个锚点的宽度和高度比率。
r = wh[:, None] / k[None]
# 计算每个目标与每个锚点之间的最小比率。
# 首先, torch.min(r, 1 / r) 计算 r 和其倒数的最小值,得到一个形状为 [N, n, 2] 的张量。
# 然后, .min(2)[0] 在第三个维度(即每个目标与锚点的两个比率)上取最小值,得到一个形状为 [N, n] 的张量 x ,其中每个元素 x[i, j] 表示第 i 个目标与第 j 个锚点的最佳比例匹配度。
x = torch.min(r, 1 / r).min(2)[0] # ratio metric
# 这是一个被注释掉的代码行,表示另一种计算锚点与目标之间匹配度量的方法,即使用交并比(IoU)。这里没有使用 IoU 度量,而是使用了比例度量。
# x = wh_iou(wh, torch.tensor(k)) # iou metric
# 返回两个值。 x 和 x.max(1)[0] 。 x 是每个目标与每个锚点之间的最佳比例匹配度,形状为 [N, n] 。 x.max(1)[0] 是每个目标的最佳比例匹配度(即每个目标与所有锚点中最佳匹配度的最大值),形状为 [N] 。
return x, x.max(1)[0] # x, best_x
# 这个 metric 函数提供了一种评估锚点质量的方法,通过计算锚点与真实目标之间的比例匹配度,可以用来评估锚点是否适合用于目标检测任务。
# 这段代码定义了一个名为 anchor_fitness 的函数,它用于计算锚点的适应度,这通常用于遗传算法中的选择和变异过程。
# 定义了一个名为 anchor_fitness 的函数,它接受一个参数。
# 1.k :代表候选锚点的宽度和高度,形状为 [n, 2] ,其中 n 是锚点的数量。
def anchor_fitness(k): # mutation fitness
# 调用 metric 函数来计算候 选锚点 k 与所有 真实目标 的匹配度量。
# torch.tensor(k, dtype=torch.float32) 将候选锚点 k 转换为 PyTorch 张量,并指定数据类型为 float32 。
# wh 是所有真实目标的宽度和高度,形状为 [N, 2] 。
# metric 函数返回两个值。每个目标与每个锚点之间的 最佳比例匹配度 ( best ),以及每个目标与每个锚点之间的 比例匹配度 ( x )。这里只关心 best ,所以使用 _ 来忽略第一个返回值。
_, best = metric(torch.tensor(k, dtype=torch.float32), wh)
# 计算适应度值。
# 首先, (best > thr).float() 创建一个布尔张量,表示每个最佳比例匹配度是否超过了阈值 thr ,然后将这个布尔张量转换为浮点张量,其中 True 变为 1.0 , False 变为 0.0 。
# 然后, best * (best > thr).float() 将每个最佳比例匹配度与其对应的布尔值相乘,这样只有超过阈值的比例匹配度会被保留,其他的则变为 0 。
# .mean() 计算上述张量的平均值,得到一个标量,表示候选锚点的整体适应度。
return (best * (best > thr).float()).mean() # fitness
# 这个 anchor_fitness 函数提供了一种评估候选锚点质量的方法,通过计算超过阈值的最佳比例匹配度的平均值来确定锚点的适应度。这个适应度值可以用于遗传算法中的选择过程,以保留更优质的锚点。
# 这段代码定义了一个名为 print_results 的函数,它用于打印和记录锚点优化的结果。
# 定义了一个名为 print_results 的函数,它接受两个参数。
# 1.k :代表优化后的锚点的宽度和高度,形状为 [n, 2] ,其中 n 是锚点的数量。
# 2.verbose :一个布尔值,指示是否打印详细信息,默认为 True 。
def print_results(k, verbose=True):
# 使用 np.argsort 对锚点 k 按照它们的面积(宽度乘以高度)进行排序,从小到大排列。 k.prod(1) 计算每个锚点的面积, np.argsort 返回排序后的索引,然后使用这些索引对 k 进行重排。
k = k[np.argsort(k.prod(1))] # sort small to large
# 调用 metric 函数来计算优化后的锚点 k 与所有真实目标 wh0 之间的匹配度量。
x, best = metric(k, wh0)
# 计算最佳可能的 召回率 ( BPR )和超过阈值的 锚点比例 ( AAT )。 bpr : best 中超过阈值 thr 的比例的平均值,表示最佳可能的召回率。 aat : x 中超过阈值 thr 的比例的平均值乘以锚点数量 n ,表示超过阈值的锚点比例。
bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr
# 构建一个字符串 s ,包含 阈值 thr 、 最佳可能的召回率 bpr 、 超过阈值的锚点比例 aat 、 锚点数量 n 、 图像尺寸 img_size 、 所有匹配度量的平均值 x.mean():.3f 、 最佳匹配度量的平均值 best.mean():.3f 以及 超过阈值的匹配度量的平均值 x[x > thr].mean():.3f 。
s = f'{PREFIX}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr\n' \
f'{PREFIX}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, ' \
f'past_thr={x[x > thr].mean():.3f}-mean: '
# 遍历排序后的锚点 k 。
for x in k:
# 将每个锚点的宽度和高度(四舍五入到最接近的整数)添加到字符串 s 中,格式为 宽度,高度, 。
s += '%i,%i, ' % (round(x[0]), round(x[1]))
# 如果 verbose 为 True ,则执行以下操作。
if verbose:
# 使用日志记录器 LOGGER 记录构建的字符串 s ,但不包括最后的逗号和空格。
LOGGER.info(s[:-2])
# 函数返回优化后的锚点 k 。
return k
# 这个 print_results 函数提供了一种打印和记录锚点优化结果的方法,包括锚点的质量指标和具体的锚点尺寸。这些信息对于评估锚点的性能和调整优化过程非常有用。
# 这段代码处理了当 dataset 参数为字符串类型时的情况,通常表示一个指向 YAML 文件的路径。
# 检查 dataset 参数是否为字符串类型。如果是字符串,通常意味着它是一个文件路径,特别是一个 YAML 文件,该文件包含了数据集的配置信息。
if isinstance(dataset, str): # *.yaml file
# 使用 with 语句打开 dataset 指定的文件, errors='ignore' 参数意味着在读取文件时忽略任何编码错误。
with open(dataset, errors='ignore') as f:
# 使用 yaml.safe_load(f) 从文件中加载 YAML 格式的数据,并将结果存储在 data_dict 变量中。这个字典包含了模型的配置信息,通常包括数据集的路径、训练参数等。
data_dict = yaml.safe_load(f) # model dict
# 从 utils.dataloaders 模块导入 LoadImagesAndLabels 类。这个类用于加载图像和标签数据。
from utils.dataloaders import LoadImagesAndLabels
# 创建 LoadImagesAndLabels 类的实例,将 data_dict 字典中 'train' 键对应的值作为参数传递,这个值通常包含了训练数据集的路径。 augment=True 表示在加载数据时应用数据增强。 rect=True 表示加载矩形训练数据。
dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
# 这段代码的目的是将 YAML 文件中的数据集配置信息转换为一个可以直接用于训练模型的数据加载器对象。这样,模型就可以使用这些数据进行训练或评估。通过这种方式,数据集的配置和实际的数据加载过程被解耦,提高了代码的灵活性和可维护性。
# 这段代码负责从数据集中提取目标的宽度和高度信息,并进行一些预处理。
# Get label wh
# dataset.shapes 包含了数据集中每张图像的宽度和高度信息。 dataset.shapes.max(1, keepdims=True) 计算每张图像尺寸中的最大值,并保持维度不变,以便可以广播。 然后,将每张图像的尺寸除以它们的最大值,再乘以 img_size (通常是模型输入图像的尺寸),得到归一化的图像尺寸。
shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
# dataset.labels 包含了数据集中每张图像的标签信息,其中每行代表一个目标,第3和第4列分别是目标的宽度和高度。
# l[:, 3:5] 提取每个标签的宽度和高度。
# l[:, 3:5] * s 将每个目标的宽度和高度乘以对应的归一化尺寸 s ,以得到实际的宽度和高度。
# zip(shapes, dataset.labels) 将归一化尺寸与标签配对,然后通过列表推导式计算每个目标的实际宽度和高度。
# np.concatenate(...) 将所有目标的宽度和高度连接成一个数组 wh0 。
wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)]) # wh
# Filter 表明接下来的代码将过滤掉一些目标。
# 计算 wh0 中宽度或高度小于3像素的目标数量。 (wh0 < 3.0) 创建一个布尔数组,表示哪些目标的宽度或高度小于3像素。 .any(1) 在每行(每个目标)上计算是否有 True 值,即是否有小于3像素的维度。 .sum() 计算 True 值的总数,即小于3像素的目标数量。
i = (wh0 < 3.0).any(1).sum()
# 如果存在小于3像素的目标,则执行以下操作。
if i:
# 使用日志记录器 LOGGER 记录一条警告信息,指出发现了多少小于3像素的目标。
LOGGER.info(f'{PREFIX}WARNING ⚠️ Extremely small objects found: {i} of {len(wh0)} labels are <3 pixels in size') # {PREFIX}警告 ⚠️ 发现极小的物体:{len(wh0)} 个标签中的 {i} 个尺寸小于 3 像素。
# 过滤掉宽度或高度小于2像素的目标。
# (wh0 >= 2.0) 创建一个布尔数组,表示哪些目标的宽度和高度都大于或等于2像素。 .any(1) 在每行(每个目标)上计算是否有 True 值,即是否所有维度都大于或等于2像素。 wh0[...] 选择满足条件的目标。 .astype(np.float32) 将结果转换为 float32 类型。
wh = wh0[(wh0 >= 2.0).any(1)].astype(np.float32) # filter > 2 pixels
# 这是一个被注释掉的代码行,表示另一种可能的数据增强方法,即通过随机缩放因子(在0到1之间)来调整目标的宽度和高度。
# wh = wh * (npr.rand(wh.shape[0], 1) * 0.9 + 0.1) # multiply by random scale 0-1
# 这段代码的目的是提取和过滤数据集中的目标宽度和高度信息,以便用于后续的锚点优化过程。通过过滤掉过小的目标,可以减少它们对锚点优化过程的不利影响。
# 这段代码是 kmean_anchors 函数中执行 K-means 聚类算法的部分,如果聚类失败,则会切换到随机初始化锚点的策略。
# Kmeans init
# 开始一个 try 块,用于捕获在执行 K-means 聚类过程中可能发生的任何异常。
try:
# 使用日志记录器 LOGGER 记录一条信息,表明正在对 len(wh) 个数据点运行 K-means 聚类算法以生成 n 个锚点。
LOGGER.info(f'{PREFIX}Running kmeans for {n} anchors on {len(wh)} points...') # {PREFIX} 在 {len(wh)} 个点上对 {n} 个锚点运行 kmeans……
# 断言锚点数量 n 不超过数据点 wh 的数量,确保问题是过度确定的,即有足够的数据点来确定锚点。
assert n <= len(wh) # apply overdetermined constraint
# 计算 wh 中每个维度(宽度和高度)的标准差 s ,用于后续的白化(whitening)过程。
s = wh.std(0) # sigmas for whitening
# 执行 K-means 聚类算法,将 wh 数据点白化(除以标准差),尝试 n 次聚类,迭代 30 次,并获取聚类中心点。 将聚类中心点乘以标准差 s 以还原到原始尺度。
k = kmeans(wh / s, n, iter=30)[0] * s # points
# 断言聚类结果 k 的数量等于请求的锚点数量 n ,确保聚类算法返回了足够数量的聚类中心点。
assert n == len(k) # kmeans may return fewer points than requested if wh is insufficient or too similar 如果 wh 不足或太相似,kmeans 可能会返回比要求更少的点。
# 如果在 try 块中发生任何异常,将执行 except 块中的代码。
except Exception:
# 使用日志记录器 LOGGER 记录一条警告信息,表明由于异常,聚类策略从 K-means 切换到随机初始化。
LOGGER.warning(f'{PREFIX}WARNING ⚠️ switching strategies from kmeans to random init') # {PREFIX}警告 ⚠️ 将策略从 kmeans 切换到随机初始化。
# 如果 K-means 聚类失败,使用随机初始化策略生成锚点。首先生成 n * 2 个随机数,排序后重塑为 n 行 2 列的数组,然后乘以图像尺寸 img_size 以获得锚点的尺寸。
k = np.sort(npr.rand(n * 2)).reshape(n, 2) * img_size # random init
# 将 wh 和 wh0 数据转换为 PyTorch 张量,并指定数据类型为 float32 。
wh, wh0 = (torch.tensor(x, dtype=torch.float32) for x in (wh, wh0))
# 调用 print_results 函数打印和记录初始锚点的结果, verbose 设置为 False 表示不打印详细信息。
k = print_results(k, verbose=False)
# 这段代码的目的是尝试使用 K-means 聚类算法来确定锚点,如果失败,则退回到随机初始化策略。这样可以确保即使在数据点不足以进行有效聚类的情况下,也能获得一组初始锚点。
# 这段代码是用于绘制聚类结果和目标宽度、高度分布的直方图的注释部分。
# Plot 表明接下来的代码将用于绘图。
# 初始化两个列表 k 和 d ,分别用于存储不同数量的 聚类中心点 和 对应的平均距离 。
# k, d = [None] * 20, [None] * 20
# 使用 tqdm 库来创建一个进度条,遍历从1到20的整数,用于表示不同的聚类中心点数量。
# for i in tqdm(range(1, 21)):
# 对于每个 i ,执行 kmeans 聚类算法,将白化后的数据点 wh / s 聚类为 i 个中心点,并将结果存储在 k 和 d 中。
# k[i-1], d[i-1] = kmeans(wh / s, i) # points, mean distance
# 使用 matplotlib 的 subplots 函数创建一个图形窗口,其中包含两个子图(1行2列),设置图形的大小为14x7英寸,并启用紧凑布局。
# fig, ax = plt.subplots(1, 2, figsize=(14, 7), tight_layout=True)
# 将 ax 从二维数组扁平化为一维数组,以便于索引。
# ax = ax.ravel()
# 在第一个子图中绘制聚类数量从1到20的平均距离平方,使用点标记每个数据点。
# ax[0].plot(np.arange(1, 21), np.array(d) ** 2, marker='.')
# 再次创建一个图形窗口,包含两个子图,用于绘制宽度和高度的直方图。
# fig, ax = plt.subplots(1, 2, figsize=(14, 7)) # plot wh
# 在第一个子图中绘制宽度的直方图,只包括宽度小于100的目标,使用400个柱子。
# ax[0].hist(wh[wh[:, 0]<100, 0],400)
# 在第二个子图中绘制高度的直方图,只包括高度小于100的目标,使用400个柱子。
# ax[1].hist(wh[wh[:, 1]<100, 1],400)
# 将图形保存为名为 'wh.png' 的文件,设置分辨率为200 DPI。
# fig.savefig('wh.png', dpi=200)
# 这段代码的目的是可视化聚类的效果和目标尺寸的分布情况,帮助理解数据特征和聚类算法的性能。由于代码被注释掉了,所以不会执行这些绘图操作。如果需要进行绘图,可以取消注释并确保环境中已安装了 matplotlib 和 tqdm 库。
# 这段代码实现了遗传算法的核心部分,用于进化和优化锚点。
# Evolve 表明接下来的代码将用于进化(优化)锚点。
# 计算初始锚点 k 的适应度 f 。 获取锚点 k 的形状 sh ,用于后续生成变异。 设置变异概率 mp 为 0.9。 设置变异的强度 s 为 0.1,这影响变异的幅度。
f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
# 创建一个 tqdm 进度条,用于跟踪遗传算法的迭代进度, gen 是总的迭代次数。
pbar = tqdm(range(gen), bar_format=TQDM_BAR_FORMAT) # progress bar
# 遍历进度条,进行 gen 次迭代。
for _ in pbar:
# 初始化一个与锚点形状相同的数组 v ,填充为 1。
v = np.ones(sh)
# 检查 v 中的所有值是否都等于 1。
while (v == 1).all(): # mutate until a change occurs (prevent duplicates)
# 生成变异。对于每个锚点,以一定的概率 mp 随机生成一个变异值,该值是随机数与标准正态分布的乘积,再乘以 s 并加 1。然后使用 clip 函数限制变异值在 0.3 到 3.0 之间。
v = ((npr.random(sh) < mp) * random.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
# 应用变异到锚点上,生成新的候选锚点 kg ,并确保所有值不小于 2.0。
kg = (k.copy() * v).clip(min=2.0)
# 计算新候选锚点 kg 的适应度 fg 。
fg = anchor_fitness(kg)
# 如果新候选锚点的适应度高于当前最佳适应度,则更新最佳适应度和锚点。
if fg > f:
# 更新适应度 f 和锚点 k 。
f, k = fg, kg.copy()
# 更新进度条的描述,显示当前的最佳适应度。
pbar.desc = f'{PREFIX}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}' # {PREFIX} 使用遗传算法进化锚点:适应度 = {f:.4f}。
# 如果 verbose 为 True ,则执行以下操作。
if verbose:
# 打印和记录当前的锚点结果。
print_results(k, verbose)
# 在遗传算法完成后,返回最终的锚点结果,并将其转换为 float32 类型。
return print_results(k).astype(np.float32)
# 这段代码通过遗传算法不断进化锚点,以提高其适应度。适应度是基于锚点与真实目标之间的匹配度量计算的。通过迭代变异和选择,算法试图找到一组更优的锚点,以提高目标检测模型的性能。
# 这个函数的目的是自动生成适合特定数据集的锚点,以提高目标检测模型的性能。通过结合 K-means 聚类和遗传算法,它能够在较大的搜索空间中找到高质量的锚点。