4、K-means聚类原理:用K-means从数据集中学习Anchor尺寸

上一章我们聊了Anchor的基础概念和YOLOv3/v5的默认设计。但有个问题一直困扰着我——这些预设的尺寸真的适合你的数据集吗?

说实话,我在早期做项目时吃过这个亏。用COCO的默认Anchor去检测工业零件,结果小目标漏检率高达40%。后来我才意识到:Anchor尺寸必须从数据中学习。而K-means聚类,就是干这个活的。

为什么需要从数据中学习Anchor?

你想想看,COCO数据集里的目标主要是行人、汽车、动物,尺寸分布相对集中。但如果你做的是遥感图像检测,目标可能是几百米外的车辆,或者足球场上的运动员。这些目标的宽高比和尺度,跟COCO完全不一样。

我举个例子:

数据集 典型目标 宽高比范围 尺度范围(像素)
COCO 行人、汽车、动物 0.5 ~ 2.0 32 ~ 512
遥感图像 车辆、建筑、飞机 0.3 ~ 4.0 8 ~ 256
工业质检 螺丝、芯片、划痕 0.2 ~ 1.5 4 ~ 64

看到差距了吧?用COCO的Anchor去检测遥感图像,就像穿别人的鞋走路——怎么都不舒服。

K-means聚类原理:一句话讲清楚

K-means说白了就是:把一堆数据点分成K个组,让每个组内的点尽量靠近,组与组之间尽量远离。

应用到Anchor学习上,我们的数据点是:训练集中所有标注框的宽度和高度。K-means会帮我们找到K个“中心点”,这些中心点就是最适合你数据集的Anchor尺寸。

核心思想:用K-means找到K个“代表性”的宽高组合,使得每个标注框都能找到离它最近的Anchor,且整体误差最小。

K-means聚类的数学步骤

嗯,这里我尽量不堆公式,但几个关键步骤你得记住:

  1. 准备数据:提取所有标注框的宽度和高度,归一化到0~1之间
  2. 初始化:随机选择K个框作为初始聚类中心
  3. 分配:计算每个框到K个中心的距离,分配到最近的中心
  4. 更新:重新计算每个簇的中心(取平均值)
  5. 迭代:重复步骤3和4,直到中心不再变化或达到最大迭代次数

这里有个关键点——距离度量不能用欧氏距离。为什么?因为欧氏距离对大框和小框不公平。一个100x100的框和一个110x110的框,欧氏距离是14.1;但一个10x10的框和一个20x20的框,欧氏距离也是14.1。显然,后者在尺度上的差异更大。

我的经验:YOLO官方用的是IoU距离,公式是:d = 1 - IoU(box, centroid)。这样不管框的大小,距离都在0~1之间,公平合理。

实战:用Python实现Anchor聚类

下面是我在实际项目中用过的代码,你直接拿去用就行:

import numpy as np
from sklearn.cluster import KMeans

def iou_distance(boxes, anchors):
    """
    计算每个box到每个anchor的IoU距离
    boxes: (N, 2) 格式为 [width, height]
    anchors: (K, 2) 格式为 [width, height]
    """
    # 计算交集面积
    inter_w = np.minimum(boxes[:, 0:1], anchors[:, 0:1].T)
    inter_h = np.minimum(boxes[:, 1:2], anchors[:, 1:2].T)
    inter_area = inter_w * inter_h
    
    # 计算并集面积
    box_area = boxes[:, 0:1] * boxes[:, 1:2]
    anchor_area = anchors[:, 0:1].T * anchors[:, 1:2].T
    union_area = box_area + anchor_area - inter_area
    
    # IoU距离
    iou = inter_area / (union_area + 1e-10)
    return 1 - iou

def kmeans_anchors(boxes, k=9, max_iter=300):
    """
    用K-means聚类生成Anchor尺寸
    boxes: (N, 2) 归一化后的宽高
    k: Anchor数量
    """
    # 随机初始化
    indices = np.random.choice(len(boxes), k, replace=False)
    anchors = boxes[indices].copy()
    
    for _ in range(max_iter):
        # 计算距离
        distances = iou_distance(boxes, anchors)
        
        # 分配簇
        labels = np.argmin(distances, axis=1)
        
        # 更新中心
        new_anchors = np.zeros_like(anchors)
        for i in range(k):
            mask = labels == i
            if mask.sum() > 0:
                new_anchors[i] = boxes[mask].mean(axis=0)
            else:
                new_anchors[i] = anchors[i]
        
        # 检查收敛
        if np.allclose(anchors, new_anchors, atol=1e-4):
            break
        anchors = new_anchors
    
    # 按面积排序
    areas = anchors[:, 0] * anchors[:, 1]
    sorted_idx = np.argsort(areas)
    return anchors[sorted_idx]

# 使用示例
# 假设你有标注数据,格式为 [x1, y1, x2, y2]
def extract_boxes_from_labels(label_files):
    boxes = []
    for file in label_files:
        with open(file, 'r') as f:
            for line in f.readlines():
                _, x, y, w, h = map(float, line.strip().split())
                boxes.append([w, h])
    return np.array(boxes)

# 加载数据并聚类
boxes = extract_boxes_from_labels(['train1.txt', 'train2.txt'])
anchors = kmeans_anchors(boxes, k=9)
print("学习到的Anchor尺寸:")
print(anchors * 416)  # 假设输入尺寸是416x416

避坑指南:我曾经在聚类时忘记归一化,结果大框主导了聚类过程,小目标完全被忽略。记得先把宽高归一化到0~1之间,或者缩放到相同的尺度范围。

如何选择K值?

K值就是Anchor的数量。YOLOv3用了9个,YOLOv5用了6个。但具体用多少,得看你的数据集。

我一般用肘部法则来选K值:

  • 计算不同K值下的平均IoU距离
  • 画一条曲线,找到“拐点”
  • 拐点对应的K值就是最佳选择

举个例子,我做过一个行人检测项目:

K值 平均IoU距离 说明
3 0.35 太粗糙,小目标漏检严重
6 0.22 效果不错,但还有提升空间
9 0.18 拐点,再增加K值收益递减
12 0.16 提升很小,但计算量增加

所以最终我选了K=9。当然,如果你的数据集目标种类单一,K=6可能就够了。

K-means聚类的可视化

下面这张图展示了K-means聚类的完整流程,从原始标注框到最终的Anchor尺寸:

K-means聚类学习Anchor尺寸流程图 原始标注框 (x, y, w, h) 提取宽高 (w, h) 归一化 K-means聚类 IoU距离度量 K个 Anchor 聚类过程示意 每个椭圆代表一个簇,中心点就是学习到的Anchor尺寸

聚类后的Anchor怎么用?

聚类完成后,你会得到K个宽高组合。比如K=9时,你会得到类似这样的结果:

# 假设输入尺寸是416x416
Anchor尺寸(按面积从小到大):
(10, 14), (23, 27), (37, 58),
(81, 82), (135, 169), (344, 319)

然后把这些尺寸分配到不同的特征图上:

  • 小尺度特征图(大感受野):分配大Anchor,检测大目标
  • 中尺度特征图:分配中等Anchor
  • 大尺度特征图(小感受野):分配小Anchor,检测小目标

我的习惯:聚类完成后,我会手动检查一下结果。如果某个Anchor的宽高比特别极端(比如1:10),可能是数据标注有问题,或者这个类别确实需要特殊处理。这时候我会适当调整,而不是完全依赖聚类结果。

总结一下关键点

  • K-means聚类是从数据中学习Anchor尺寸的标准方法
  • 距离度量要用IoU距离,而不是欧氏距离
  • K值选择用肘部法则,一般6~9个Anchor就够用
  • 聚类结果要按面积排序,分配到不同尺度的特征图上

说实话,Anchor聚类这一步虽然简单,但效果立竿见影。我做过对比实验:用COCO默认Anchor和用聚类Anchor训练同一个模型,mAP能提升3~5个点。尤其是小目标检测,提升更明显。