简单粗暴,废话也不罗嗦了,学习目的就是解决下面三个问题,
1. 默认anchor_t设置为4,这个参数如何调整?有没有必要调整?(首先网上很多说这个参数是长宽比是错误的,其只是控制anchor设置宽松度的阈值)
2. 代码如何完成自动化anchor聚类的,有点魔化如何实现的?
3. 聚类之后结果一定比人工算感受野的好吗?
传送门: yolov5/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,也弄清楚了 bpr, aat两个关键指标的含义
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