Anchor Box
在讲解YOLO之前,有必要先解释什么是Anchor box。
在做目标检测任务时,我们首先需要将数据标注并得到训练集、验证集、测试集。已标注的数据label,其实就是在原始图片中用矩形框框出目标,得到的矩形框参数(中心点坐标、长、宽)就是label。Anchor box其实就是从训练集中将所有的矩形框的大小尺寸统计处最常出现的某几个矩形框,这里我们可以采用K-Means来得到。这样也就不难理解使用anchor的目的了:目标先验。即我们提前告诉模型,应该去用多大的矩形框去寻找目标,帮助模型快速收敛。
在使用K-Means前,需要将标注数据转换获得每个bounding box(标注框)的高宽并归一化:
(假设你的标注数据是voc数据集的格式)
import numpy as np
import glob
import xml.etree.ElementTree as ET
# normalize width and height
def normal_wh(path):
dataset = list()
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(obj.findtext('bndbox/xmin')) / width
ymin = int(obj.findtext('bndbox/ymin')) / height
xmax = int(obj.findtext('bndbox/xmax')) / width
ymax= int(obj.findtext('bndbox/ymax')) / height
dataset.append([xmax-xmin, ymax-ymin])
return np.array(dataset)
接下来就是使用K-Means算法将刚刚处理后的bounding box的高宽集合进行分类,按照YOLOv3论文中设定的9类。K-Means中的距离计算使用1-IOU,每个bounding box的坐标由其高宽表示。记住:我们要寻找的是高宽相似的bounding box聚类后中心box作为anchor box。
import numpy as np
def iou(bbox, clusters):
"""
计算某一个bounding box与当前的聚类中心框(这里为9个)的交并比。
:param bbox:单个待归类的bounding box
:param clusters:当前的聚类中心框
:return: 交并比
"""
# 将clusters中的中心框宽限定不大于当前box的宽
x = np.minimum(clusters[:, 0], box[0])
# 将clusters中的中心框高限定不大于当前box的高
y = np.minimum(clusters[:, 1], box[1])
if np.count_nonzero(x == 0) > 0 or np.np.count_nonzero(y == 0) > 0:
raise ValueError('This box is invalid!')
intersection = x * y
box_area = box[0] * box[1]
clusters_area = clusters[:, 0] * clusters[:, 1]
#交并比公式
box_iou = intersection / (box_area + cluster_area - intersection)
return box_iou
解释一下上面的intersection:我们用某一个box去和clusters中每一个中心框求交并比,所以主要地,也就是两个框的共同覆盖区域。那么,共同覆盖区域自然地也就是两者高宽中较小的,因此求得了任意两个框的共同覆盖区域=intersection。
接下来便是K-Means算法:
def kmeans(boxes, k, dist=np.median):
"""
使用任意两个框的交并比作为距离矩阵进行K-Means计算
:params boxes: numpy array of shape (r, 2), where r is the number of rows
:param k: number of clusters
:param dist: distance function
:return: numpy array of shape (k, 2)
"""
rows = boxes.shape[0]
#距离矩阵 r * k
distances = np.empty((rows, k))
#上一次聚类中心,用以记录算法是否收敛
last_clusters = np.zeros((rows, ))
np.random.seed(42)
#初始化聚类中心,随机选择k个框
clusters = boxes[np.random.choice(rows, k, replace=False)]
while True:
for row in range(rows):
#定义距离公式:d[box, centroid] = 1 - IOU(box, centroid)
#这里有个矛盾的地方,我们希望IOU越大越好,而距离越小越好
#所以距离的设定为1-IOU
distances[row] = 1 - iou(boxes[row], clusters)
#将当前框分配给距离最近的聚类中心
nearest_clusters = np.argmin(distances, axis=1)
#cluster是否已经收敛:每个中心不再变化(all)则终止循环
if (last_clusters == nearest_clusters).all():
break
#更新每个聚类的中心,这里将每个类中的中位数作为新的中心
for cluster in range(k):
clusters[cluster] = dist(boxes[neasrest_clusters == cluster], axis=0)
last_clusters = clusters
return clusters
解释一下为什么要用中位数来计算和更新聚类中心:根据参考1中的原因,在YOLO9000论文中,作者在VOC2007数据集上的结果为:
YOLO9000 IOU结果可是,如果我们选择平均数来计算的话会发现很难得到这个结果。于是参考1的作者选择了中位数的方法,得到的结果如下:
5个cluster 9个cluster可以看到,这个结果十分接近论文的结果。而且K-means的初始状态会对结果有一定的影响,所以可能每次的结果会有浮动。我在代码里将随机因子设定为固定值。
参考:
网友评论