0
点赞
收藏
分享

微信扫一扫

patch 生成合入补丁

at小涛 2023-11-18 阅读 43

第一种做法

在基于anchor的目标检测算法中,anchor一般都是通过人工设计的。例如,在SSD、Faster-RCNN中,设计了9个不同大小和宽高比的anchor。然而,通过人工设计的anchor存在一个弊端,就是并不能保证它们一定能很好的适合数据集,如果anchor的尺寸和目标的尺寸差异较大,则会影响模型的检测效果。

在论文YOLOv2中提到了这个问题,作者建议使用K-means聚类来代替人工设计,通过对训练集的bounding box进行聚类,自动生成一组更加适合数据集的anchor,可以使网络的检测效果更好。

本文将解释如何使用k-means聚类来生成一组anchor。

Standard K-means

首先简单复习一下标准的K-means算法,K-means是一种简单且常用的无监督学习算法,它旨在将数据集划分成K个簇,使得相同簇之内的数据相似性高,不同簇之间的数据相似性低。

算法步骤:

  1. 初始化K个簇中心;
  2. 使用相似性度量(一般是欧氏距离),将每个样本分配给与其距离最近的簇中心;
  3. 计算每个簇中所有样本的均值,更新簇中心;
  4. 重复2、3步,直到均簇中心不再变化,或者达到了最大迭代次数。

img

Anchor K-means

接下来介绍如何对bounding box进行K-means。

度量选择

通常,bounding box由左上角顶点和右下角顶点表示,即 (x1,y1,x2,y2) 。在对box做聚类时,我们只需要box的宽和高作为特征,并且由于数据集中图片的大小可能不同,还需要先使用图片的宽和高对box的宽和高做归一化,即

w=\frac{w_{box}}{w_{img}}, h=\frac{h_{box}}{h_{img}}

如果直接使用标准K-means中的欧氏距离作为度量,则会有个问题,就是在聚类结果中,大box簇会比小box簇产生更大的误差(squared error)。由于我们只关心anchor与box的IOU,不关心的box的大小,因此,使用IOU作为度量更加合适。

假设有 anchor=(w_{a},h_{a}),box=(w_{b},h_{b}) ,则

IOU(box,anchor)=\frac{intersection(box,anchor)}{union(box,anchor)-intersection(box,anchor)}

=\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})}

需要说明的一点是,这里计算IOU时,不用管box的位置,我们假设所有box的左上顶点都在原点,如下图所示:

在这里插入图片描述)

显然,IOU的取值在0到1之间,如果两个box越相似,则它们的IOU值越大。由于在习惯上,我们希望两个box越相似则它们的距离应该越近,所以最终的度量公式为:

d(box,anchor)=1-IOU(box,anchor)

由上式可知,当box与anchor完全重叠,即IOU=1时,它们之间的距离为0。

步骤

对box进行K-means的步骤为:

  1. 随机选取K个box作为初始anchor;
  2. 使用IOU度量,将每个box分配给与其距离最近的anchor;
  3. 计算每个簇中所有box宽和高的均值,更新anchor;
  4. 重复2、3步,直到anchor不再变化,或者达到了最大迭代次数。

与标准K-means的步骤基本一致,主要的不同是第2步中使用的度量是上面定义的 ( , ℎ ) ,这里anchor作为簇的中心。

相关代码

def iou(boxes, anchors):
    """
    Calculate the IOU between boxes and anchors.

    :param boxes: 2-d array, shape(n, 2)
    :param anchors: 2-d array, shape(k, 2)
    :return: 2-d array, shape(n, k)
    """
    # Calculate the intersection,
    # the new dimension are added to construct shape (n, 1) and shape (1, k),
    # so we can get (n, k) shape result by numpy broadcast
    w_min = np.minimum(boxes[:, 0, np.newaxis], anchors[np.newaxis, :, 0])
    h_min = np.minimum(boxes[:, 1, np.newaxis], anchors[np.newaxis, :, 1])
    inter = w_min * h_min
       
    # Calculate the union
    box_area = boxes[:, 0] * boxes[:, 1]
    anchor_area = anchors[:, 0] * anchors[:, 1]
    union = box_area[:, np.newaxis] + anchor_area[np.newaxis]

    return inter / (union - inter)

def fit(self, boxes):
        """
        Run K-means cluster on input boxes.

        :param boxes: 2-d array, shape(n, 2), form as (w, h)
        :return: None
        """
        # If the current number of iterations is greater than 0, then reset
        if self.n_iter > 0:
            self.n_iter = 0

        np.random.seed(self.random_seed)
        n = boxes.shape[0]

        # Initialize K cluster centers (i.e., K anchors)
        self.anchors_ = boxes[np.random.choice(n, self.k, replace=True)]

        self.labels_ = np.zeros((n,))

        while True:
            self.n_iter += 1

            # If the current number of iterations is greater than max number of iterations , then break
            if self.n_iter > self.max_iter:
                break

            self.ious_ = self.iou(boxes, self.anchors_)
            distances = 1 - self.ious_
            cur_labels = np.argmin(distances, axis=1)

            # If anchors not change any more, then break
            if (cur_labels == self.labels_).all():
                break

            # Update K anchors
            for i in range(self.k):
                self.anchors_[i] = np.mean(boxes[cur_labels == i], axis=0)

            self.labels_ = cur_labels

完整的代码以及下面的demo代码请戳:

Demo

以VOC 2012数据集为例,整个流程可以分三步走:

1. 准备数据

def parse_xml(annot_dir):
    """
    Parse XML annotation files in VOC dataset

    :param annot_dir: directory path to annotation files
    :return: 2-d array
    """
    boxes = []

    for xml_file in glob.glob(os.path.join(annot_dir, '*.xml')):
        tree = ET.parse(xml_file)

        h_img = int(tree.findtext('./size/height'))
        w_img = int(tree.findtext('./size/width'))

        for obj in tree.iter('object'):
            xmin = int(round(float(obj.findtext('bndbox/xmin'))))
            ymin = int(round(float(obj.findtext('bndbox/ymin'))))
            xmax = int(round(float(obj.findtext('bndbox/xmax'))))
            ymax = int(round(float(obj.findtext('bndbox/ymax'))))

            w_norm = (xmax - xmin) / w_img
            h_norm = (ymax - ymin) / h_img

            boxes.append([w_norm, h_norm])

    return np.array(boxes)

annot_dir = "/PATH TO YOUR/VOCdevkit/VOC2012/Annotations"
boxes = parse_xml(annot_dir)
print('boxes shape : {}'.format(boxes.shape))

"""OUTPUT
boxes shape : (40138, 2)
"""

可以看到,VOC2012训练集中总共标注了40138个目标。

2. 选择多个K值进行聚类

使用K-means聚类时,面临的一个重要问题就是如何选择一个合适的K值,也就是我们需要选择几个anchor。考虑到计算复杂度,anchor的数量最好不要超过10,因此通常的做法是:对K在[2,10]这个区间内进行多次聚类,然后画出平均IOU随K值的变化曲线,从中发现最佳的anchor数量。

for k in range(2, 11):
    model = AnchorKmeans(k, random_seed=333)
    model.fit(boxes)
    avg_iou = model.avg_iou()
    print("K = {}, Avg IOU = {:.4f}".format(k, avg_iou))

"""OUTPUT
K = 2, Avg IOU = 0.4646
K = 3, Avg IOU = 0.5391
K = 4, Avg IOU = 0.5801
K = 5, Avg IOU = 0.6016
K = 6, Avg IOU = 0.6252
K = 7, Avg IOU = 0.6434
K = 8, Avg IOU = 0.6596
K = 9, Avg IOU = 0.6732
K = 10, Avg IOU = 0.6838
"""

可以看出,平均IOU随K值的增大而增大,如果K等于box的数量,则平均IOU会等于1。

3. 确定Anchor数量

画出平均IOU随K值的变化曲线,如下:

img)

elbow方法是用来估计最佳K值的常用方法,其思想简单来说就是:如果某个K值使得平均IOU的斜率发生了明显的变化,那么这个K值就是我们想要的。

因此,根据elbow方法,同时考虑更高的召回率,这里我们可以选择K=5作为anchor的数量,这5个anchor分别为:

print(model.anchors_)

"""OUTPUT
[[0.7794355  0.8338808 ]
 [0.33883529 0.68815335]
 [0.61044288 0.40655773]
 [0.19493034 0.35335266]
 [0.07805765 0.13006786]]
"""

可视化:

img

第二种做法

第一步:首先我们要知道我们需要聚类的是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()
举报

相关推荐

0 条评论