前几日YOLO系列迎来了YOLOv4,再来回看一遍YOLOv3。
anchor box
YOLO v1中,bounding-box做回归时没有限制,导致可能会预测一个距离很远的object,效率不高。在YOLO v2中,开始引入了anchor box的概念,只对网格邻近的object负责,正所谓各司其职。
anchor-box用于在边界框预测时,通过伸缩、平移变换,最终能够标定该物体。其尺寸大小的设定,对于网络的运行效率影响较大。因为图像中不同类型的特殊形状的物体,通常在图像中呈现的大小是不尽相同的(有的物体大,有的物体小)。因此,有必要对训练数据集设定适合其anchor-box的大小(即anchor-box的宽高)。K-Means便可以用于解决这一问题。
(此时不需要对图像进行416的处理 ,只需要标记的数据)
数据准备
在数据集标定的过程中,产生的样本集中包含 数据的路径、标定图像四周的坐标、以及标定的类别编码(4+classe), 如下所示,路径与标定信息按照空格分隔:
1 D:keras-yolo3-master-person/VOCdevkit/VOC2007/JPEGImages/person0000.jpg 115,93,414,519,0
在YOLO v3中,有三种尺度的预测,每种尺度根据其大小赋予其相应大小的anchor-box,即共需要9个anchor-box,这就决定了在K-Means中的聚类个数为9类。
K-Means代码的梳理:
代码主线:
1 def txt2clusters(self): 2 all_boxes = self.txt2boxes() # 将txt中数值信息转化为图像标记框的宽高,并返回 3 result = self.kmeans(all_boxes, k=self.cluster_number) 4 result = result[np.lexsort(result.T[0, None])] 5 self.result2txt(result) 6 print("K anchors: {}".format(result)) 7 print("Accuracy: {:.2f}%".format( 8 self.avg_iou(all_boxes, result) * 100))
1. 获取训练样本标定数据框的宽高信息 self.txt2boxes()
逐行读取文件,按空格将路径和信息分隔。对信息中四周的坐标进行计算(右-左;下-上),获取宽高信息,最终返回宽、高的二维数组。
1 for line in f: 2 infos = line.split(" ") 3 length = len(infos) 4 for i in range(1, length): 5 width = int(infos[i].split(",")[2]) - 6 int(infos[i].split(",")[0]) 7 height = int(infos[i].split(",")[3]) - 8 int(infos[i].split(",")[1]) 9 dataSet.append([width, height]) 10 result = np.array(dataSet)
2. 对获取到的宽高信息进行聚类self.kmeans(all_boxes, k=self.cluster_number)
聚类过程:
1)随机选取k个类中心
首先,在宽高信息中,随机选取k个类中心。
1 clusters = boxes[np.random.choice(box_number, k, replace=False)]
2) 计算各点到类中心的距离
传统的聚类是计算各样本到各类中心的距离,将样本归为到类中心最近的类。最终的目的是使得属于某一类的样本到类中心的距离越小越好。
此处,由于样本信息是宽高,是一个具体的形状。此处采用的是IOU越大越好,为了与聚类的选择方式相同 此处采用d=1-IOU 越小越好。
1 distances = 1 - self.iou(boxes, clusters)
IOU的实现 实际上式通过面积的计算来衡量的交集和并集的比例,下图所示的是理解这么做是交集和并集的示例,红色的为类中心的框,黑色的是3个示例样本。
1 box_area = boxes[:, 0] * boxes[:, 1] # 把要聚类的框的宽高相乘,作为了一个box_area 2 box_area = box_area.repeat(k) # 要算到k个类中心的距离,需要搞一个每个都有k个的矩阵 3 box_area = np.reshape(box_area, (n, k)) 4 5 cluster_area = clusters[:, 0] * clusters[:, 1] 6 cluster_area = np.tile(cluster_area, [1, n]) 7 cluster_area = np.reshape(cluster_area, (n, k)) 8 # 把box和cluster的宽都整理成n行k列的形式,并把两者做比较,最后还是一个n行k列的形式,这个 9 # 过程其实在比较box和两个cluster的宽,并选出小的 10 box_w_matrix = np.reshape(boxes[:, 0].repeat(k), (n, k)) 11 cluster_w_matrix = np.reshape(np.tile(clusters[:, 0], (1, n)), (n, k)) 12 min_w_matrix = np.minimum(cluster_w_matrix, box_w_matrix) 13 # 把box和cluster的高都整理成n行k列的形式,并把两者做比较,最后还是一个n行k列的形式,这个 14 # 过程其实在比较box和两个cluster的高,并选出小的 15 box_h_matrix = np.reshape(boxes[:, 1].repeat(k), (n, k)) 16 cluster_h_matrix = np.reshape(np.tile(clusters[:, 1], (1, n)), (n, k)) 17 min_h_matrix = np.minimum(cluster_h_matrix, box_h_matrix) 18 # 将筛选出来的小的宽高 相乘 19 inter_area = np.multiply(min_w_matrix, min_h_matrix) 20 21 result = inter_area / (box_area + cluster_area - inter_area)
3)类中心的更新
为了避免标记的物体中,框存在特别小,或者特别大的情况,在类中心的更新中,采用的是聚到一类中的个体的中位数,而不是均值。如下述代码所示,dist设置的是median(中位数)。
1 def kmeans(self, boxes, k, dist=np.median): 2 ... 3 for cluster in range(k): 4 clusters[cluster] = dist(boxes[current_nearest == cluster], axis=0)