深度学习之目标检测(十)-- YOLO如何使用k-means获得Anchor

木卯 于 2021-08-24 发布

深度学习之目标检测(十)YOLO如何使用k-means获得Anchor

本章学习 YOLO 如何使用k-means获得Anchor,学习视频源于 Bilibili,参考的 CSDN 博客包括:

所使用的代码参考的是 yolov3 spp 以及 yolov5 中生成 Anchors 的方法。

img0

1. K-means 聚类算法

本小节参考 百度百科 进行讲解,k-means clustering algorithm 是一种迭代求解的聚类分析算法,其步骤是,预将数据分为 K 组,则随机选取 K 个对象作为初始的聚类中心,然后计算每个对象与各个种子聚类中心之间的距离(相似程度),把每个对象分配给距离它最近的聚类中心。聚类中心以及分配给它们的对象就代表一个聚类。每分配一个样本,聚类的聚类中心会根据聚类中现有的对象被重新计算。这个过程将不断重复直到满足某个终止条件。终止条件可以是以下任何一个:

K-means 算法对初始种子点的选择比较敏感 (选择种子点比较好,则聚类较快,反之则比较慢)。K-means 聚类算法具体实现如下所示:

import numpy as np
import pandas as pd
import random
import sys
import time

class KMeansClusterer:
    def __init__(self,ndarray, cluster_num):
        self.ndarray = ndarray
        self.cluster_num = cluster_num
        self.points = self.__pick_start_point(ndarray, cluster_num)
         
    def cluster(self):
        result = []
        for i in range(self.cluster_num):
            result.append([])
        for item in self.ndarray:
            distance_min = sys.maxsize
            index=-1
            for i in range(len(self.points)):                
                distance = self.__distance(item,self.points[i])
                if distance < distance_min:
                    distance_min = distance
                    index = i
            result[index] = result[index] + [item.tolist()]
        new_center=[]
        for item in result:
            new_center.append(self.__center(item).tolist())
        
        # 中心点未改变,说明达到稳态,结束递归
        if (self.points == new_center).all():
            return result
         
        self.points = np.array(new_center)
        return self.cluster()
             
    def __center(self, list):
        '''计算一组坐标的中心点
        '''
        # 计算每一列的平均值
        return np.array(list).mean(axis = 0)
      
    def __distance(self, p1, p2):
        '''计算两点间距
        '''
        tmp=0
        for i in range(len(p1)):
            tmp += pow(p1[i]-p2[i], 2)
        return pow(tmp, 0.5)
      
    def __pick_start_point(self, ndarray, cluster_num):
        
        if cluster_num < 0 or cluster_num > ndarray.shape[0]:
            raise Exception("簇数设置有误")
      
        # 随机点的下标
        indexes = random.sample(np.arange(0,ndarray.shape[0],step=1).tolist(), cluster_num)
        points=[]
        for index in indexes:
            points.append(ndarray[index].tolist())
        return np.array(points)

有两点需要进行注意:

下图给出了 k-means 算法的运行示例,生成以下聚类过程图片的代码见:plot_kmeans.py

img1

img2

2. YOLOv2 中的 K-means 算法

Faster R-CNN 使用的 Anchors 都是作者通过工程经验人为设计的, 但并没有给出设计的过程和细节。YOLOv2 论文中作者提到说:如果我们一开始就选择了合适的 Anchors,那么网络就更容易去学习如何进行好的预测。那如何评价什么是好的 Anchors 呢?YOLOv2 论文作者通过计算所有目标 GT boxes 与 Anchors 的最大 IOU 的均值作为指标,记为 Avg IOU, 其越大代表得到的 Anchors 越好。

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.

YOLOv2 中是怎么利用 k-means 聚类算法得到 Anchors 的呢 ? 作者对比了 k=5 和 k=9 的情况,最后为了计算量的取舍选择了 k=5。另一个核心问题是如何定义样本之间的距离。YOLOv2 论文中作者提到说:直接使用欧式距离其实并不好。因为衡量指标为 Avg IOU,所以选择 1-IOU(bboxes, anchors) 表示距离,如果 bbox 与对应的簇中心(Anchor)IOU 越大,则距离越近(1-IOU(bboxes, anchors)越小)。采用Cluster SSE (Sum of Square Error) 欧式距离误差平方和与采用Cluster IOU相比,后者对应的 Avg IOU 越大,效果越好。作者在 Anchors 个数为 9 的情况下对比 Cluster IOU 得到的 Avg IOU 比 Faster R-CNN 手工设计 Anchors 计算得到的 Avg IOU 更高。

If we use standard k-means with Euclidean distance larger boxes generate more error than smaller boxes.

img3

下展示使用k-means算法,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]
    '''
    利用了 numpy 的广播机制
    将 wh1 第一个维度自动复制 M 次为 [N,M,2]
    将 wh2 第零个维度自动复制 N 次为 [N,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)]	# replace=False 采样的 k 个数值不会重复

    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

3. YOLOv5 中的 K-means 算法

代码是 大佬 根据 YOLOv5 中聚类 Anchors 的代码修改得到的。主要的小改动了三点:1. 对代码做了些简化,更加易读;2.把使用pytorch 库的地方都改成了 numpy 库(也就方便搞到 Jittor 库了);3. 作者默认使用的 k-means 方法是 scipy 包提供的,且使用的是欧式距离。被改成了基于 1-IOU(bboxes, anchors) 距离的方法。YOLOv5 实际还不是直接使用欧氏距离的结果,而是经过了 Genetic Algorithm 遗传算法进行变异的结果。完整代码链接点击 此处

代码流程如下:

代码链接: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

4. 细节提醒

如果有时使用自己聚类得到的 anchors 的效果反而变差了,此时可以从以下两个方面进行检查:


如果不是使用 1-IOU(bboxes, anchors) 作为距离指标,而是使用 GIoU,DIoU,或者 CIoU 呢?

瞎搞!现在的相似度距离计算没有位置,只有大小,所以他们都是不可用的。只有 CIoU 中关于长宽比的说法似乎还有点帮助!