YOLOv5之autoanchor看这一篇就够了

文章详细解析了YOLOv5中如何通过自动anchor聚类来优化目标检测模型的性能。主要涉及了anchor_t参数的调整、自动化anchor聚类的实现以及聚类效果与人工设置的对比。通过kmeans算法和遗传算法进行微调,寻找最优的anchor配置,提高模型在特定数据集上的精度和召回率。
摘要由CSDN通过智能技术生成

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

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值