YOLOv5之autoanchor看这一篇就够了

2023-05-16

简单粗暴,废话也不罗嗦了,学习目的就是解决下面三个问题,

1. 默认anchor_t设置为4,这个参数如何调整?有没有必要调整?(首先网上很多说这个参数是长宽比是错误的,其只是控制anchor设置宽松度的阈值)

2. 代码如何完成自动化anchor聚类的,有点魔化如何实现的?

3. 聚类之后结果一定比人工算感受野的好吗?

传送门: yolov5/autoanchor.py

https://github.com/ultralytics/yolov5/blob/master/utils/autoanchor.py​github.com/ultralytics/yolov5/blob/master/utils/autoanchor.py

粗看结构:

        来先粗略的概览一下函数名来了解一下整个过程,以coco128数据作为学习数据集,这是一个阉割版的COCO数据集,里面只有128张图片,929个标注框。总揽一下yolov5工程下utils/autoanchor.py这个文件,文件中函数

def check_anchor_order(m)

def check_anchors(dataset, model, thr=4.0, imgsz=640)
    def metric(k)

def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True)
    def metric(k, wh)
    def anchor_fitness(k)
    def print_results(k, verbose=True)

        从上面函数名还是比较明确的,kmeans就是很常用的聚类方法,说明anchor是通过kmeans聚类而来。明确后,进一步查看函数内子函数,metric应该是某种评价指标用于判断是否需要聚类或者聚类的好坏,anchor_fitness一看就是在根据kmeans在拟合anchor了,print_result看名字就不重要直接忽略。


细看门道:

1)check_anchor_order(不重要)

def check_anchor_order(m):
 # Check anchor order against stride order for YOLOv5 Detect() module m, and correct if necessary
1:    a = m.anchors.prod(-1).mean(-1).view(-1)  # mean anchor area per output layer
2:    da = a[-1] - a[0]  # delta a
3:    ds = m.stride[-1] - m.stride[0]  # delta s
4:    if da and (da.sign() != ds.sign()):  # same order
5:        LOGGER.info(f'{PREFIX}Reversing anchor order')
6:       m.anchors[:] = m.anchors.flip(0)

        1:m.anchors是从配置文件读取的anchor[3x3x2]分别表示3层特征 x 3个anchor x [w, h],第1行先计算每个anchor的面积,然后在计算每一层的均值,最终a表示三层特征层平均anchor的小3x1的矩阵。以models/yolov5s.yaml

在代码段中1:表示标记,用于在描述和代码中进行对应,
在后文标记中[400, 300],表示变量或者tensor的shape为[400, 300]

        yolov5s模型默认的anchor配置

a = [(10*13+16*30+33*23)/3, (30*61+62*45+59*119)/3, (116*90+156*198+375*326)/3] = tensor([ 456.33334, 3880.33325, 54308.66797])

2: da, 计算差值最大的两个anchor平均值,通常最后一个特征层感受野最大。

3: ds, 计算下采样最大最小的差值,stride=[8, 16, 32],所以差值32-8=24

4: 判断anchor是不是最后一层比第一层大,如果不是说明顺序错了。anchors需要按照从特征层8, 16,32的顺序,从小到大写

        就只是判断了stride和anchor大小是不是同向的,也比较tricky.

2) check_orders(重点)

        代码太长,不过精髓就在这了一定要仔细看细细品,第九行是整个聚类的精髓

1: m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1]  # Detect()
2: shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
3: scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1))  # augment scale
4: wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float()  

1: 获得检测头,获取anchor、stride、na(number of anchors)、nc(number of classes)、nl(number of layers)等检测头相关属性

2: dataset.shapes记录了训练集上所有图像大小,以coco128数据集为例,是一个 [128 x 2]的numpy array。这句找到128张图像上宽高最大的数值,将dataset.shapes/最大值 归一化到0到1,按照比例所放到最大640边长。

3: 根据均匀分布,随机生成0.9~1.1之间的缩放尺度

4: dataset.labels其中格式为[类别,x, y , w, h],xywh均已经归一化了,最终得到缩放之后标签的宽和高,shape为[929,2]的tensor, coco128训练集上共有929个标注框,由此得到wh一个轻微扰动后在训练集上所有标注框的宽高的集合

5: stride = m.stride.to(m.anchors.device).view(-1, 1, 1)  # model strides
6: anchors = m.anchors.clone() * stride  # current anchors
7: bpr, aat = metric(anchors.cpu().view(-1, 2))

5~6: stride=[8, 16,32],是一个[3,1]的张量;m.anchors是根据当前stride归一的结果,anchors计算完将三层特征上的anchor全部还原到[640,640]这个图像尺度上。

7: metric其实输入是anchors[9x2]和第4步中wh[929x2]

def metric(k):  # compute metric
8:     r = wh[:, None] / k[None]
9:     x = torch.min(r, 1 / r).min(2)[0]  # ratio metric
10:    best = x.max(1)[0]  # best_x
11:    aat = (x > 1 / thr).float().sum(1).mean()  # anchors above threshold
12:    bpr = (best > 1 / thr).float().mean()  # best possible recall
13:    return bpr, aat

8: [929x2] / [9, 2] = [929x9x2],计算得到929个标注框的宽高与9个anchor宽高取比值,宽比宽,高比高。这时候标注框有比anchor大的有比他小的,且尺度从零点几到几百都有可能,很难设定阈值。

9:明确一点,比值的目的是让标签和anchor尽量相近,当比值接近1表示设置的很合理,跟标签都重合了,反之数值越大或者越小都表示设置的不好。这里有个小技巧,既然过大过小都没意义,那取倒数把特别大的变特别小,接近1的几乎没变。再看min(r, 1/r).min(),将比值都变换到0到1之间,这里面越接近1越好,把特别大的或特特别小的数值统一转换为特别小。妙还是妙的,也足够优雅!

10: 此时x [929x9] ,取出匹配程度最高的,也就是越接近1的所以用max。9个anchor中和标签宽高比匹配成对最高的,也就是数值最大最接近1的。best[929x1]

11: 还有个点需要提示一下,我们在认定anchor是否匹配标签,只要9个anchor有一个超过阈值即可,并不需要9个anchor都匹配。所以上一步中只取最大值,在第9步时选择宽高比值最小的最小的都匹配宽高肯定都匹配。

在一个 为什么是x>1/thr 不是 x>thr? 可以这么理解,x已经是比值且归一化0到1,1/thr中1表示标准直就是1,thr表示相差几倍比值以内。比如设置为4,含义就是gt与anchor宽高相差不能超过4倍,其实是很宽泛的要求。

12:两个指标解释一下含义,

        aat表示在训练集上平均有几个anchor超过阈值,所有anchor都参与计算。例如使用coco128/yolov5s配置文件计算为4.26695,表示平均每个标签可以匹配4.26个anchor,这个结果也是很不错的。

        bpr计算了最佳情况下,挑选9个anchor中最高比值的一个,每个标签对应一个最高分匹配结果,最后判断最佳情况下有多少超过阈值。

由此我们解决了问题1,也弄清楚了 bpraat两个关键指标的含义
thresh是否需要调整?如何调整?实际效果好坏需要实验调整,不过整体上thresh设置越大对anchor设置的要求越松,越小对anchor设置的要求越高。个人理解越小适合打比赛更好的挖掘数据分布来聚类anchor,当然可能会影响到算法的范化能力。甚至可以将anchor_t作为参数,将1-5之间步进式画出anchor_t和bpr或anchor_t和aat的图像作为判断依据。

3)kmean_anchors(水到渠成)

        当bpr <= 0.98时,对anchor进行聚类,不过在做这个实验时要先将预设的anchor调整一下才能走到下一步。先回顾一下kmeans

        kmeans聚类算法具体实现过程

def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):

     npr = np.random
1:  thr = 1 / thr

2:  def metric(k, wh):  # compute metrics
        r = wh[:, None] / k[None]
        x = torch.min(r, 1 / r).min(2)[0]  # ratio metric
        # x = wh_iou(wh, torch.tensor(k))  # iou metric
    return x, x.max(1)[0]  # x, best_x

3:  def anchor_fitness(k):  # mutation fitness
         _, best = metric(torch.tensor(k, dtype=torch.float32), wh)
         return (best * (best > thr).float()).mean()  # fitness

4:  def print_results(k, verbose=True):
        k = k[np.argsort(k.prod(1))]  # sort small to large
        x, best = metric(k, wh0)
        bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n  # best possible recall, anch > thr
        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: '
     for x in k:
        s += '%i,%i, ' % (round(x[0]), round(x[1]))
     if verbose:
         LOGGER.info(s[:-2])
     return k

     # Get label wh
5:  shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
6:  wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)])  # wh

    # Filter
7:  i = (wh0 < 3.0).any(1).sum()
     if i:
        LOGGER.info(f'{PREFIX}WARNING: Extremely small objects found: {i} of {len(wh0)} labels are < 3 pixels in size')
8:   wh = wh0[(wh0 >= 2.0).any(1)]  # filter > 2 pixels
      # wh = wh * (npr.rand(wh.shape[0], 1) * 0.9 + 0.1)  # multiply by random scale 0-1

      # Kmeans init
      try:
          LOGGER.info(f'{PREFIX}Running kmeans for {n} anchors on {len(wh)} points...')
          assert n <= len(wh)  # apply overdetermined constraint
9:       s = wh.std(0)  # sigmas for whitening
10:      k = kmeans(wh / s, n, iter=30)[0] * s  # points
         assert n == len(k)  # kmeans may return fewer points than requested if wh is insufficient or too similar
      except Exception:
         LOGGER.warning(f'{PREFIX}WARNING: switching strategies from kmeans to random init')
11:     k = np.sort(npr.rand(n * 2)).reshape(n, 2) * img_size  # random init
      wh, wh0 = (torch.tensor(x, dtype=torch.float32) for x in (wh, wh0))
      k = print_results(k, verbose=False)

3: fitness计算anchor和标签宽高比超过阈值的那些比值的均值,其实就是作为评价匹配程度好不好的一个数值,最佳为1最差为0,前面已经介绍国原因了就不多废话了。

5~6: 跟之前一样处理,获得标签宽高并缩放

7~8:any其中一个元素不为空/0/None输出True, all全部都得不为空/0/None输出True.就是过滤了一下,宽高必须有一个大于2,感觉用all更合适

9:这里对数据进行了白化(whitening),一方面降低数据间依赖,另一方面让每一个特征的方差为1。主要是为了主成分分析,消除方差占比较小的特征维,再一个就是标准化。

10:标签宽高标准化后作为kmeans的输入,kmeans需要制定聚类中心数量,这里为n=9,kemans叠代30轮。最终返回聚类后的anchor框,要是返回数量少于9个,就随机生成从9个anchor.

# Evolve
    f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1  # fitness, generations, mutation prob, sigma
    pbar = tqdm(range(gen), bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}')  # progress bar
    for _ in pbar:
1:     v = np.ones(sh)
        while (v == 1).all():  # mutate until a change occurs (prevent duplicates)
            v = ((npr.random(sh) < mp) * random.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
2:      kg = (k.copy() * v).clip(min=2.0)
         fg = anchor_fitness(kg)
3:      if fg > f:
4:         f, k = fg, kg.copy()
            pbar.desc = f'{PREFIX}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'
            if verbose:
                print_results(k, verbose)
    return print_results(k)

        这一段主要是由于kmeans聚类之后fitness未必能够达到要求,在通过1000步相对较小的扰动可能会找到最好的结果。在获取v的数值时,也是以高斯分布随的方式获得扩展倍数,更符合在聚类基础上左右扰动切保证中心高概率的需要,最后做到优中选优。聚类其实也不是什么黑魔法,anchor有9个最简单的就是kmeans, 但毕竟聚类有一定随机性(随机初始化聚类中心)所以后续通过进化的方式又模拟了1000次细微扰动并计算评价指标,在其中优中取优,最终输出聚类后anchor,一切都正正好。

        最后一个问题的答案也就比较清晰了,通过kmeans确实可以提升anchor在特定数据集上的精度或者召回,但是不见得人工设置的anchor就会差,只是作为生产力工具更加简化了训练流程,基本不需要考虑anchor怎么设置,少做了一些发散而已,其实相差不多回归都能handle。在一个工具可以帮我们发现一些隐含的问题,更可以作为一个数据和模型的诊断工具,既方便又高效~

来源:YOLOv5之autoanchor看这一篇就够了 - 知乎 (zhihu.com)

参考:Training YOLO? Select Anchor Boxes Like This | by Olga Chernytska | Towards Data Science

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

YOLOv5之autoanchor看这一篇就够了 的相关文章

随机推荐

  • GDAL重采样与裁剪图像示例

    GDAL重采样 xff0c 可以通过写文件时改变图像尺寸和geo transformes的分辨率信息实现 核心代码示例如下 xff1a in ds 61 gdal Open fi gdal GA ReadOnly geotrans 61 i
  • pycharm专业版连接远程docker容器

    一 配置远程docker容器 1 启动带有端口的docker容器 6006端口是用来运行tensorboard的 xff0c 这里重要的是22端口 如果希望通过ssh远程连接docker xff0c 需要对容器的22端口做端口映射 dock
  • VScode 远程开发配置

    一 配置免密远程登录 因为是要远程登录 xff0c 那么需要通过使用ssh进行密钥对登录 xff0c 这样每次登录服务器就可以不用输入密码了 先来一句官方介绍 xff1a ssh 公钥认证是一种方便 高安全性的身份验证方法 xff0c 它将
  • np.meshgrid()与torch.meshgrid()的区别

    比如要生成一张图像 h 61 6 w 61 10 的xy坐标点 xff0c 看下两者的实现方式 xff1a 两种方式的差异在于 xff1a xs ys 61 np meshgrid np arange w np arange h xs ys
  • JSON是什么

    提起 JSON xff0c 作为如今最受欢迎的数据交换格式 xff0c 可以说是无人不知 无人不晓了 JSON 全称 JavaScript Object Notation xff08 JS 对象简谱 xff09 xff0c 自诞生之初的小目
  • 【C++】数组定义引发Stack overflow错误(运行时是报段错误)

    C 43 43 xff08 实际是C的语法 xff09 定义数组时出错 xff0c 代码如下 xff1a float t1 9830400 调试时触发Stack overflow错误 xff08 可执行文件运行时 xff0c 是报段错误 x
  • 【C/C++】数组初始化

    数组定义不初始化会被随机赋值 因此如果数组的所有元素在下面没有逐一赋值 xff0c 但是又会使用到的话 xff0c 最后不要只定义而不初始化 会带来问题 数组初始化的几种形式 可以直接用 xff1a a 10 61 xff0c 就可以让a
  • 【C++】指针数组与数组指针

    指针数组 指针数组可以说成是 指针的数组 xff0c 首先这个变量是一个数组 xff0c 其次 xff0c 指针 修饰这个数组 xff0c 意思是说这个数组的所有元素都是指针类型 xff0c 在32位系统中 xff0c 指针占四个字节 定义
  • 【旋转框目标检测】2201_The KFIoU Loss For Rotated Object Detection

    paper with code paper code Jittor Code https github com Jittor JDet PyTorch Code https github com open mmlab mmrotate Te
  • CUDA编译报错unsupported GNU version! gcc versions later than 10 are not supported!

    问题 xff1a python编译用于cuda的so文件中 xff0c 使用编译 cu文件出错 xff1a error unsupported GNU version gcc versions later than 10 are not s
  • RuntimeError: CUDA error: no kernel image is available for execution on the device

    问题 xff1a 代码换机器执行时 xff0c 使用包含自行编译的cuda算子库so时出错 xff1a RuntimeError CUDA error no kernel image is available for execution o
  • Ubuntu非LTS版本安装nvidia-docker出错:Unsupported distribution!

    问题 xff1a 按照Nvidia官方流程 xff0c 在Ubuntu22 10安装nvidia docker在执行以下命令时 distribution 61 etc os release echo ID VERSION ID amp am
  • 测试torch方法是否支持半精度

    并不是所有的torch方法都支持半精度计算 测试半精度计算需要在cuda上 xff0c cpu不支持半精度 因此首先需要创建半精度变量 xff0c 并放到cuda设备上 部分方法在低版本不支持 xff0c 在高版本支持半精度计算 xff0c
  • yolov5关闭wandb

    yolov5训练过程中wandb总是提示登入账号 xff0c 不登入还不能继续训练 xff0c 想要关闭wandb xff0c 直接不使用即可 在 yolov5 utils loggers wandb wandb utils py中 imp
  • 目标检测 YOLOv5的loss权重,以及与图像大小的关系

    1 目标检测 YOLOv5的loss权重 YOLOv5中有三个损失分别是 box obj cls 在超参数配置文件hyp yaml中可以设置基础值 xff0c 例如 box 0 05 cls 0 5 obj 1 训练使用时 xff0c 在t
  • 手写一个JSON反序列化程序

    上一篇文章 JSON是什么 给大家介绍了JSON的标准规范 xff0c 今天就自己动手写一个JSON的反序列化程序 xff0c 并命名它为 zjson 0 开始之前 本篇文章的目的是学习实践 xff0c 所以我们选择相对简单的Python实
  • yolov5源码解析--输出

    本文章基于yolov5 6 2版本 主要讲解的是yolov5是怎么在最终的特征图上得出物体边框 置信度 物体分类的 一 总体框架 首先贴出总体框架 xff0c 直接就拿官方文档的图了 xff0c 本文就是接着右侧的那三层输出开始讨论 Bac
  • yolov5源码解析--损失计算与anchor

    本文章基于yolov5 6 2版本 主要讲解的是yolov5在训练过程中是怎么由推理结果和标签来进行损失计算的 损失函数往往可以作为调优的一个切入点 xff0c 所以我们首先要了解它 一 代码入口 损失函数的调用点如下 xff0c 在tra
  • 多任务学习中各loss权重应该如何设计呢?

    来源 xff1a 22 封私信 80 条消息 多任务学习中各loss权重应该如何设计呢 xff1f 知乎 zhihu com 多损失在深度学习中很常见 xff0c 例如 xff1a 目标检测 xff1a 以 YOLO 为例 xff0c 它的
  • YOLOv5之autoanchor看这一篇就够了

    简单粗暴 xff0c 废话也不罗嗦了 xff0c 学习目的就是解决下面三个问题 xff0c 1 默认anchor t设置为4 xff0c 这个参数如何调整 xff1f 有没有必要调整 xff1f xff08 首先网上很多说这个参数是长宽比是