在基于anchor的目标检测算法中,anchor一般都是通过人工设计的。例如,在SSD、Faster-RCNN中,设计了9个不同大小和宽高比的anchor。然而,通过人工设计的anchor存在一个弊端,就是并不能保证它们一定能很好的适合数据集,如果anchor的尺寸和目标的尺寸差异较大,则会影响模型的检测效果。
在论文YOLOv2中提到了这个问题,作者建议使用K-means聚类来代替人工设计,通过对训练集的bounding box进行聚类,自动生成一组更加适合数据集的anchor,可以使网络的检测效果更好。
“The network can learn to adjust the boxes appropriately but if we pick better priors for the network to start with we can make it easier for the network to learn to predict good detections. Instead of choosing priors by hand, we run k-means clustering on the training set bounding boxes to automatically find good priors.”
首先简单复习一下标准的K-means算法,K-means是一种简单且常用的无监督学习算法,它旨在将数据集划分成K个簇,使得相同簇之内的数据相似性高,不同簇之间的数据相似性低。
算法步骤:
接下来介绍如何对bounding box进行K-means。
度量选择
通常,bounding box由左上角顶点和右下角顶点表示,即 (x1,y1,x2,y2) 。在对box做聚类时,我们只需要box的宽和高作为特征,并且由于数据集中图片的大小可能不同,还需要先使用图片的宽和高对box的宽和高做归一化,即
w = w b o x w i m g , h = h b o x h i m g w=\frac{w_{box}}{w_{img}}, h=\frac{h_{box}}{h_{img}} w=wimgwbox,h=himghbox
如果直接使用标准K-means中的欧氏距离作为度量,则会有个问题,就是在聚类结果中,大box簇会比小box簇产生更大的误差(squared error)。由于我们只关心anchor与box的IOU,不关心的box的大小,因此,使用IOU作为度量更加合适。
假设有 a n c h o r = ( w a , h a ) , b o x = ( w b , h b ) anchor=(w_{a},h_{a}),box=(w_{b},h_{b}) anchor=(wa,ha),box=(wb,hb) ,则
I O U ( b o x , a n c h o r ) = i n t e r s e c t i o n ( b o x , a n c h o r ) u n i o n ( b o x , a n c h o r ) − i n t e r s e c t i o n ( b o x , a n c h o r ) IOU(box,anchor)=\frac{intersection(box,anchor)}{union(box,anchor)-intersection(box,anchor)} IOU(box,anchor)=union(box,anchor)−intersection(box,anchor)intersection(box,anchor)
= m i n ( w a , w b ) ⋅ m i n ( h a , h b ) w a h a + w b h b − m i n ( w a , w b ) ⋅ m i n ( h a , h b ) =\frac{min(w_{a},w_{b})\cdot min(h_{a},h_{b})}{w_{a}h_{a}+w_{b}h_{b}-min(w_{a},w_{b})\cdot min(h_{a},h_{b})} =waha+wbhb−min(wa,wb)⋅min(ha,hb)min(wa,wb)⋅min(ha,hb)
需要说明的一点是,这里计算IOU时,不用管box的位置,我们假设所有box的左上顶点都在原点,如下图所示:
)
显然,IOU的取值在0到1之间,如果两个box越相似,则它们的IOU值越大。由于在习惯上,我们希望两个box越相似则它们的距离应该越近,所以最终的度量公式为:
d ( b o x , a n c h o r ) = 1 − I O U ( b o x , a n c h o r ) d(box,anchor)=1-IOU(box,anchor) d(box,anchor)=1−IOU(box,anchor)
由上式可知,当box与anchor完全重叠,即IOU=1时,它们之间的距离为0。
我们都知道yolov3对训练数据使用了k-means聚类的算法来获得anchor boxes大小,但是具体其计算过程是怎样的呢?下面我们来详细的分析其具体计算过程:
第一步:首先我们要知道我们需要聚类的是bounding box,所以我们无需考虑其所属类别,第一步我们需要将所有的bounding box坐标提取出来,也许一张图有一个矩形框,也许有多个,但是我们需要无区别的将所有图片的所有矩形框提取出来,放在一起。
第二步:数据处理获得所有训练数据bounding boxes的宽高数据。给的训练数据往往是其bounding box的4个坐标,但是我们后续需要聚类分析的是bounding box的宽高大小,所以我们需要将坐标数据转换为框的宽高大小,计算方法很简单:长=右下角横坐标-左上角横坐标、宽=右下角纵坐标-左上角纵坐标。
第三步:初始化k个anchor box,通过在所有的bounding boxes中随机选取k个值作为k个anchor boxes的初始值。
第四步:计算每个bounding box与每个anchor box的iou值。传统的聚类方法是使用欧氏距离来衡量差异,也就是说如果我们运用传统的k-means聚类算法,可以直接聚类bounding box的宽和高,产生k个宽、高组合的anchor boxes,但是作者发现此方法在box尺寸比较大的时候,其误差也更大,所以作者引入了iou值,可以避免这个问题。iou值计算方法:这里参考下图和计算代码:
min_w_matrix = np.minimum(cluster_w_matrix, box_w_matrix) #cluster_w_matrix, box_w_matrix分别代表anchor box和bounding box宽大小
min_h_matrix = np.minimum(cluster_h_matrix, box_h_matrix) #cluster_h_matrix, box_h_matrix分别代表anchor box和bounding box高大小
inter_area = np.multiply(min_w_matrix, min_h_matrix) #inter_area表示重叠面积
IOU = inter_area / (box_area + cluster_area - inter_area)#box_area表示bounding box面积 ;cluster_area表示anchor box面积
由于iou值往往越大越好,所以作者定义了一个距离d参数,用来表示其误差:
d=1-IOU
第五步:分类操作。经过前一步的计算可以的到每一个bounding box对于每个anchor box的误差d(n,k),我们通过比较每个bounding box其对于每个anchor box的误差大小{d(i,1),d(i,2),…,d(i,k)},选取最小误差的那个anchor box,将这个bounding box分类给它,对于每个bounding box都做这个操作,最后记录下来每个anchor box有哪些bounding box属于它。
第六步:anchor box更新。经过上一步,我们就知道每一个anchor box都有哪些bounding box属于它,然后对于每个anchor box中的那些bounding box,我们再求这些bounding box的宽高中值大小(这里参照github上作者qqwweee那个yolov3项目,也许也有使用平均值进行更新),将其作为该anchor box新的尺寸。
第七步:重复操作第四步到第六步,直到在第五步中发现对于全部bounding box其所属的anchor box类与之前所属的anchor box类完全一样。(这里表示所有bounding box的分类已经不再更新)
第八步:计算anchor boxes精确度。至第七步,其实已经通过k-means算法计算出anchor box。但是细心的同学可能已经发现,k-means.py还给出其精确度大小,其计算方法如下:使用最后得到的anchor boxes与每个bounding box计算其IOU值,对于每个bounding box选取其最高的那个IOU值(代表其属于某一个anchor box类),然后求所有bounding box该IOU值的平均值也即最后的精确度值。
应网友要求附上代码(代码来源):
import numpy as np
import xml.etree.ElementTree as ET
import glob
import random
def cas_iou(box,cluster):
x = np.minimum(cluster[:,0],box[0])
y = np.minimum(cluster[:,1],box[1])
intersection = x * y
area1 = box[0] * box[1]
area2 = cluster[:,0] * cluster[:,1]
iou = intersection / (area1 + area2 -intersection)
return iou
def avg_iou(box,cluster):
return np.mean([np.max(cas_iou(box[i],cluster)) for i in range(box.shape[0])])
def kmeans(box,k):
# 取出一共有多少框
row = box.shape[0]
# 每个框各个点的位置
distance = np.empty((row,k))
# 最后的聚类位置
last_clu = np.zeros((row,))
np.random.seed()
# 随机选5个当聚类中心
cluster = box[np.random.choice(row,k,replace = False)]
# cluster = random.sample(row, k)
while True:
# 计算每一行距离五个点的iou情况。
for i in range(row):
distance[i] = 1 - cas_iou(box[i],cluster)
# 取出最小点
near = np.argmin(distance,axis=1)
if (last_clu == near).all():
break
# 求每一个类的中位点
for j in range(k):
cluster[j] = np.median(
box[near == j],axis=0)
last_clu = near
return cluster
def load_data(path):
data = []
# 对于每一个xml都寻找box
for xml_file in glob.glob('{}/*xml'.format(path)):
tree = ET.parse(xml_file)
height = int(tree.findtext('./size/height'))
width = int(tree.findtext('./size/width'))
# 对于每一个目标都获得它的宽高
for obj in tree.iter('object'):
xmin = int(float(obj.findtext('bndbox/xmin'))) / width
ymin = int(float(obj.findtext('bndbox/ymin'))) / height
xmax = int(float(obj.findtext('bndbox/xmax'))) / width
ymax = int(float(obj.findtext('bndbox/ymax'))) / height
xmin = np.float64(xmin)
ymin = np.float64(ymin)
xmax = np.float64(xmax)
ymax = np.float64(ymax)
# 得到宽高
data.append([xmax-xmin,ymax-ymin])
return np.array(data)
if __name__ == '__main__':
# 运行该程序会计算'./VOCdevkit/VOC2007/Annotations'的xml
# 会生成yolo_anchors.txt
SIZE = 416
anchors_num = 6
# 载入数据集,可以使用VOC的xml
path = r'./VOCdevkit/VOC2007/Annotations'
# 载入所有的xml
# 存储格式为转化为比例后的width,height
data = load_data(path)
# 使用k聚类算法
out = kmeans(data,anchors_num)
out = out[np.argsort(out[:,0])]
print('acc:{:.2f}%'.format(avg_iou(data,out) * 100))
print(out*SIZE)
data = out*SIZE
f = open("yolo_anchors.txt", 'w')
row = np.shape(data)[0]
for i in range(row):
if i == 0:
x_y = "%d,%d" % (data[i][0], data[i][1])
else:
x_y = ", %d,%d" % (data[i][0], data[i][1])
f.write(x_y)
f.close()
今天补下之前没有细讲的聚类anchors相关知识,所使用的代码参考的是yolov3 spp
以及yolov5
中生成anchors的方法。
k-means是非常经典且有效的聚类方法,通过计算样本之间的距离(相似程度)将较近的样本聚为同一类别(簇)。使用k-means时主要关注两个问题(个人认为):1.如何表示样本与样本之间的距离(核心问题),这个一般需要根据具体场景去设计,不同的方法聚类效果也不同,最常见的就是欧式距离。2.分为几类,这个也是需要根据应用场景取选择的,也是一个超参数。
k-means算法主要流程如下:
生成以上聚类过程图片的代码:
plot_kmeans.py
在之前讲faster rcnn
理论时,使用的anchors都是作者通过经验手工设计的, 但为什么这么设计作者并未提及。那这里为什么要聚类anchors?yolov2
论文中有这么一段话The network can learn to adjust the boxes appropriately but if we pick better priors for the network to start with we can make it easier for the network to learn to predict good detections.
简单的说如果我们一开始就选择了合适的anchors,那么网络就更容易去学习得到好的检测器。那什么才算好的anchors呢?作者通过计算Avg IOU
即所有目标bboxes与anchors最大IOU的均值作为指标,Avg IOU
越大代表得到的anchors越好。
上面已经简单介绍了k-means算法的过程,下面在说下yolov2中是怎么利用k-means算法进行聚类得到anchors的。这里主要关注的是如何定义样本之间的距离。论文中有这么一句话,If we use standard k-means with Euclidean distance larger boxes generate more error than smaller boxes.
简单的说就是直接使用欧式距离来计算效果不是很好。那么用什么表示距离呢,论文中使用1-IOU(bboxes, anchors)
表示距离,如果bbox与对应的簇中心(anchor)IOU越大,则距离越近(1-IOU(bboxes, anchors)
越小)。如下图所示采用Cluster SSE
(Sum of Square Error) 误差平方和(欧式距离)和采用Cluster IOU
相比,Cluster IOU
对应的Avg IOU更大,当然你想使用Cluster SSE
也是可以的。并且在anchors个数相同的情况下Cluster IOU
得到的Avg IOU比Faster RCNN
中手工设计(Anchor Boxes)的Avg IOU更高。
下面是我参考几个开源项目自己改的代码。使用k-means算法,1-IOU(bboxes, anchors)
作为样本之间的距离进行聚类。代码很简单,简要介绍下:
1-IOU(bboxes, anchors)
yolo_kmeans.py
import numpy as np
def wh_iou(wh1, wh2):
# Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2
wh1 = wh1[:, None] # [N,1,2]
wh2 = wh2[None] # [1,M,2]
inter = np.minimum(wh1, wh2).prod(2) # [N,M]
return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter)
def k_means(boxes, k, dist=np.median):
"""
yolo k-means methods
refer: https://github.com/qqwweee/keras-yolo3/blob/master/kmeans.py
Args:
boxes: 需要聚类的bboxes
k: 簇数(聚成几类)
dist: 更新簇坐标的方法(默认使用中位数,比均值效果略好)
"""
box_number = boxes.shape[0]
last_nearest = np.zeros((box_number,))
# 在所有的bboxes中随机挑选k个作为簇的中心。
clusters = boxes[np.random.choice(box_number, k, replace=False)]
while True:
# 计算每个bboxes离每个簇的距离 1-IOU(bboxes, anchors)
distances = 1 - wh_iou(boxes, clusters)
# 计算每个bboxes距离最近的簇中心
current_nearest = np.argmin(distances, axis=1)
# 每个簇中元素不在发生变化说明以及聚类完毕
if (last_nearest == current_nearest).all():
break # clusters won't change
for cluster in range(k):
# 根据每个簇中的bboxes重新计算簇中心
clusters[cluster] = dist(boxes[current_nearest == cluster], axis=0)
last_nearest = current_nearest
return clusters
代码链接:
https://github.com/WZMIAOMIAO/deep-learning-for-image-processing/blob/master/others_project/kmeans_anchors/yolo_kmeans.py
如果你是直接使用yolov5的训练脚本,那么它会自动去计算下默认的anchors与你数据集中所有目标的best possible recall,如果小于0.98就会根据你自己数据集的目标去重新聚类生成anchors,反之使用默认的anchors。
下面代码是我根据yolov5中聚类anchors的代码简单修改得到的。基本流程不变,主要改动了三点:1.对代码做了些简化。2.把使用pytorch的地方都改成了numpy(感觉这样会更通用点,但numpy效率确实没有pytorch高)。3.作者默认使用的k-means方法是scipy包提供的,使用的是欧式距离。我自己改成了基于1-IOU(bboxes, anchors)
距离的方法。当然我只是注释掉了作者原来的方法,如果想用自己把注释取消掉就行了。但在我使用测试过程中,还是基于1-IOU(bboxes, anchors)
距离的方法会略好点。
完整代码链接:
https://github.com/WZMIAOMIAO/deep-learning-for-image-processing/tree/master/others_project/kmeans_anchors
其实在yolov5生成anchors中不仅仅使用了k-means聚类,还使用了Genetic Algorithm遗传算法,在k-means聚类的结果上进行mutation变异。接下来简单介绍下代码流程:
img_size
,由于读取的bboxes是相对坐标所以不需要改动main.py
import random
import numpy as np
from tqdm import tqdm
from scipy.cluster.vq import kmeans
from read_voc import VOCDataSet
from yolo_kmeans import k_means, wh_iou
def anchor_fitness(k: np.ndarray, wh: np.ndarray, thr: float): # mutation fitness
r = wh[:, None] / k[None]
x = np.minimum(r, 1. / r).min(2) # ratio metric
# x = wh_iou(wh, k) # iou metric
best = x.max(1)
f = (best * (best > thr).astype(np.float32)).mean() # fitness
bpr = (best > thr).astype(np.float32).mean() # best possible recall
return f, bpr
def main(img_size=512, n=9, thr=0.25, gen=1000):
# 从数据集中读取所有图片的wh以及对应bboxes的wh
dataset = VOCDataSet(voc_root="/data", year="2012", txt_name="train.txt")
im_wh, boxes_wh = dataset.get_info()
# 最大边缩放到img_size
im_wh = np.array(im_wh, dtype=np.float32)
shapes = img_size * im_wh / im_wh.max(1, keepdims=True)
wh0 = np.concatenate([l * s for s, l in zip(shapes, boxes_wh)]) # wh
# Filter 过滤掉小目标
i = (wh0 < 3.0).any(1).sum()
if i:
print(f'WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.')
wh = wh0[(wh0 >= 2.0).any(1)] # 只保留wh都大于等于2个像素的box
# Kmeans calculation
# print(f'Running kmeans for {n} anchors on {len(wh)} points...')
# s = wh.std(0) # sigmas for whitening
# k, dist = kmeans(wh / s, n, iter=30) # points, mean distance
# assert len(k) == n, print(f'ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}')
# k *= s
k = k_means(wh, n)
# 按面积排序
k = k[np.argsort(k.prod(1))] # sort small to large
f, bpr = anchor_fitness(k, wh, thr)
print("kmeans: " + " ".join([f"[{int(i[0])}, {int(i[1])}]" for i in k]))
print(f"fitness: {f:.5f}, best possible recall: {bpr:.5f}")
# Evolve
# 遗传算法(在kmeans的结果基础上变异mutation)
npr = np.random
f, sh, mp, s = anchor_fitness(k, wh, thr)[0], k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
pbar = tqdm(range(gen), desc=f'Evolving anchors with Genetic Algorithm:') # progress bar
for _ in pbar:
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)
kg = (k.copy() * v).clip(min=2.0)
fg, bpr = anchor_fitness(kg, wh, thr)
if fg > f:
f, k = fg, kg.copy()
pbar.desc = f'Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'
# 按面积排序
k = k[np.argsort(k.prod(1))] # sort small to large
print("genetic: " + " ".join([f"[{int(i[0])}, {int(i[1])}]" for i in k]))
print(f"fitness: {f:.5f}, best possible recall: {bpr:.5f}")
if __name__ == "__main__":
main()
运行结果如下,注意由于随机性每次结果都会有些差异,如果要能够复现,需要固定numpy以及random包的随机数种子。
read data info.: 100%|██████████| 5717/5717 [00:00<00:00, 6549.98it/s]
kmeans: [12, 18] [27, 31] [33, 69] [75, 48] [65, 118] [125, 137] [164, 268] [299, 166] [382, 337]
fitness: 0.73256, best possible recall: 0.99956
Evolving anchors with Genetic Algorithm: fitness = 0.7358: 100%|██████████| 1000/1000 [00:05<00:00, 182.22it/s]
genetic: [13, 23] [34, 31] [30, 75] [79, 66] [69, 143] [142, 134] [169, 270] [331, 177] [391, 338]
fitness: 0.73582, best possible recall: 0.99930
有时使用自己聚类得到的anchors的效果反而变差了,此时你可以从以下几方面进行检查: