在Anchor-based目标检测模型中,根据数据集选择合适的Anchor有利于加快模型的收敛速度以及减少模型的边框预测误差。本篇文章首先介绍
Anchor
在目标检测模型中的作用;然后介绍K-means
聚类算法;最后介绍yolo源码
中自制数据集的Anchor的获取
方法。
1 Anchor的理解
在Anchor-based
目标检测模型中(SSD,YOLO v3/v4/v5等),其通过预先设定的具有不同宽高比
和尺度
的Anchor得到目标的粗略边框,并训练回归参数对Anchor进行修正
以得到目标的准确边框。以YOLOv5为例,其基于Anchor的检测流程如图1所示,在不同尺度的输出特征图使用不同尺度
的Anchor实现对不同大小目标的检测,在一个检测单元中,使用不同宽高比
的Anchor对目标进行预测。
- 在较大的特征图上使用较小的Anchor,实现对小目标的检测;
- 在中等的特征图上使用中等的Anchor,实现对中等大小目标的检测;
- 在较小的特征图上使用较大的Anchor,实现对大目标的检测;
在目标检测模型中,目标的类别预测结果不会随着其在图像中位置的改变而改变
;但目标的边框信息会随着其在图像中位置的改变而改变,通过设定Anchor,实现目标边框对Anchor的偏移量预测,该偏移量不会随着目标在图像中位置的改变而改变
,更有利于模型的回归,符合神经网络位移不变性
。
模型在训练中,根据Anchor和目标实际边框之间的IoU大小,分配用于检测该目标的Anchor。根据数据集科学的设置Anchor有利于加快模型训练时的收敛速度
,减少模型对目标的漏检率
。
2 K-means聚类算法
K-means是最常见的聚类算法,算法接受未标记的数据集,然后将数据聚集成不同的组,其方法如下:
- 首先选取K个随机的点,作为聚类中心;
- 对于数据集中的每一个数据,计算其与聚类中心点的距离,并将其与距离最近的中心点关联起来,与同一个中心点关联的所有点聚成一类;
- 计算每一类的平均值,将该类的中心点移动到平均值的位置;
- 重复步骤2-3,直到中心点不再改变。
3 基于K-means获取自制数据集Anchor
Anchors获取
基于Scipy库中的kmeans函数计算K-means聚类(kmean_anchors)
- 输入:
shapes
(原图大小),labels
(图像标签),n
(聚类中心点个数),img_size
(模型训练图像大小)- 输出:anchors(在模型训练图像上的
绝对大小
)
def kmean_anchors(shapes, labels, n=9, img_size=640):
'''
shapes: 数据集图像大小
labels: 数据集标签 [N, 5] 5->(cls, x, y, w, h)(相对大小)
n: Anchors数量(在img_size上绝对大小)
img_sie:模型训练图像大小
'''
from scipy.cluster.vq import kmeans # 导入kmeans函数, 返回-> k:Anchors; distortion:每一类内点到中心点的平均距离
# 数据集图像输入模型的大小(保持原图比例, 长边缩放为img_size)
shapes = img_size * shapes / shapes.max(1, keepdims=True)
# 得到所有目标的边框(在输入图像上绝对大小)
wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, labels)])
# 滤除边框像素过小的目标
wh = wh0[(wh0 >= 2.0).any(1)].astype(np.float32)
# K-means
try:
assert n <= len(wh) # 中心点个数应小于等于边框数目
s = wh.std(0) # 坐标标准差
k = kmeans(wh / s, n, iter=30)[0] * s # points
assert n == len(k) # # kmeans 可能无法找到足够的点,若输入数据太少或太小
except Exception:
k = np.sort(npr.rand(n * 2)).reshape(n, 2) * img_size # 随机初始化
k = k[np.argsort(k.prode(1))] # 根据面积从小到大对得到的Anchors进行排序
print('Anchors:', k)
随机生成300点,使用以上函数得到的聚类结果示例如图3所示。
Anchors评价
在YOLO中,判断Anchors与目标边框是否匹配有以下两种指标:
- 根据
Anchors和目标实际边框的宽高比(r > anchors_t)
- 根据
Anchors和目标实际边框的IoU(iou > iou_t)
def metrix(k, wh, m='ratio', thr=0.25):
k = k[np.argsort(k.prod(1))] # 根据面积从小到大对得到的Anchors进行排序
r = wh[:, None] / k[None] # 计算目标与所有Anchor的宽高比
# 目标匹配的所有Anchors
if m == 'ratio': # 宽高比指标
x = torch.min(r, 1 / r).min(2)[0] # 计算所有宽高比的最小值:目标/Anchors; Anchors/目标
elif m == 'iou': # iou指标
x = wh_iou(wh, k)
best = x.max(1)[0] # 目标匹配的所有Anchors, 目标匹配的最好Anchor(大小最接近)
fitness = (best * (best > thr).float()).mean() # fitness[0, 1], 反映目标实际边框与Anchors的匹配情况
return x, best, fitness
def print_result(x, best, thr=0.25, img_size=640):
bpr, aat = bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # 最好的匹配结果, 所有匹配结果
s = f'{PREFIX}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr\n' \
f'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: '
# 添加Anchors信息
for x in k:
s += '%i,%i, ' % (round(x[0]), round(x[1]))
print(s[:-2])
Anchors更新(遗传算法)
根据评价指标,基于遗传算法更新Anchors,以得到在数据集中表现最好的Anchors
def ga_anchors(k, wh, gen=1000):
_, _, f = metrix(k, wh)
sh, mp, s = k.shape, 0.9, 0.1 # 遗传算法中系数更新
for _ in range(gen):
v = np.ones(sh) # 随机更新系数
while (v==1).all():
v = ((np.random.random(sh) < mp) * random.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
kg = (k.copy() * v).clip(min=2.0) # 根据系数构造新的Anchors
_, _, fg = metrig(kg, wh) # 计算新的fitness
if fg > f: # 若效果更好, 则更新k
f, k = fg, kg.copy()
print('Evolving anchors with Genetic Algorithm: fitness = {f:.4f}')