第四章:Anchor优化:针对小目标的Anchor聚类与自适应调整
Anchor,说白了就是预设的候选框。
很多同学刚开始做小目标检测时,总觉得模型效果不好,调了半天学习率、改网络结构,结果发现——问题出在Anchor上。嗯,我当年也踩过这个坑。
4.1 为什么小目标需要特殊的Anchor?
YOLO系列默认的Anchor,是基于COCO数据集算出来的。COCO里大中目标占多数,小目标比例不高。你想想看,用大目标的先验去预测小目标,就像让一个打篮球的人去打乒乓球——不是不能打,但肯定别扭。
我在项目中遇到过这样一个案例:用YOLOv5s检测无人机航拍图像中的行人。默认Anchor下,小行人的召回率只有35%。后来重新聚类Anchor,召回率直接飙到62%。
为什么会这样?
因为小目标的尺寸分布和大目标完全不同。小目标往往集中在几个特定的像素范围内(比如8x8到32x32),而默认Anchor可能覆盖了32x32到128x128的范围。说白了,你的Anchor和真实目标「对不上焦」。
核心结论:Anchor的尺寸分布,必须和你的数据集真实分布匹配。尤其是小目标,差几个像素,召回率可能差十几个点。
4.2 针对小目标的Anchor聚类方法
我个人的习惯是,拿到一个新数据集,第一件事就是做Anchor聚类。别急着调模型,先把「地基」打牢。
4.2.1 传统K-means聚类的问题
K-means聚类大家都会用。但直接用在Anchor上,有个隐藏问题:
- K-means用欧氏距离,大框的误差天然比小框大
- 聚类结果会偏向大目标,小目标被「平均」掉了
举个例子:一个100x100的大框和一个10x10的小框,欧氏距离计算时,大框的贡献是小框的100倍。聚类中心自然往大框偏移。
避坑指南:我曾经直接用K-means聚类小目标数据集,结果9个Anchor里有7个是大中尺寸。后来才发现,是距离度量出了问题。
4.2.2 改进的IoU距离聚类
正确的做法是用IoU距离代替欧氏距离:
def iou_distance(box, clusters):
"""
计算box与所有聚类中心的IoU距离
距离 = 1 - IoU
"""
ious = []
for cluster in clusters:
# 计算交集
inter_w = min(box[0], cluster[0])
inter_h = min(box[1], cluster[1])
inter_area = inter_w * inter_h
# 计算并集
box_area = box[0] * box[1]
cluster_area = cluster[0] * cluster[1]
union_area = box_area + cluster_area - inter_area
iou = inter_area / (union_area + 1e-6)
ious.append(1 - iou) # 距离 = 1 - IoU
return np.min(ious) # 返回到最近聚类中心的距离
用IoU距离的好处是:
- 对大框和小框一视同仁,不会偏向大目标
- 距离范围固定在[0,1],方便设置阈值
- 聚类结果更符合检测任务的本质
4.2.3 小目标专用的Anchor数量选择
YOLOv5默认9个Anchor,每层3个。但小目标检测时,我建议:
| 特征层 | 默认Anchor数 | 小目标推荐数 | 说明 |
|---|---|---|---|
| 大特征图(P3/8倍下采样) | 3 | 4-5 | 小目标主要在这里检测,多分配Anchor |
| 中特征图(P4/16倍下采样) | 3 | 3 | 保持默认 |
| 小特征图(P5/32倍下采样) | 3 | 1-2 | 大目标少,减少冗余 |
我个人习惯是:小目标数据集用10-12个Anchor,大特征图分配一半以上。这样小目标有更多「专属」的候选框。
4.3 自适应Anchor调整策略
聚类是一次性的。但训练过程中,模型对Anchor的「偏好」会变化。怎么办?
4.3.1 训练中动态调整Anchor
我记得有一次做交通标志检测,小标志只有15x15像素。聚类后Anchor效果不错,但训练到一半,模型开始漏检。后来发现,是因为模型学到了更精细的特征,对Anchor的尺寸要求变了。
解决方案:每N个epoch重新聚类一次Anchor。
def adaptive_anchor_update(model, dataloader, epoch, update_interval=10):
"""
每10个epoch重新聚类Anchor
"""
if epoch % update_interval == 0 and epoch > 0:
print(f"Epoch {epoch}: 重新聚类Anchor...")
# 收集当前epoch的ground truth框
all_boxes = []
for images, targets in dataloader:
for t in targets:
boxes = t['boxes'].cpu().numpy()
# 只保留小目标(面积 < 32x32)
small_boxes = [b for b in boxes if b[2]*b[3] < 1024]
all_boxes.extend(small_boxes)
# 重新聚类
new_anchors = kmeans_iou(all_boxes, n_clusters=9)
# 更新模型Anchor
model.model[-1].anchors = new_anchors
print(f"新Anchor: {new_anchors}")
小技巧:动态调整时,只保留小目标框参与聚类。这样Anchor会越来越「聚焦」小目标,而不是被偶尔出现的大目标带偏。
4.3.2 基于损失反馈的Anchor微调
还有一种更精细的方法:根据每个Anchor的损失来调整它的尺寸。
原理很简单:如果某个Anchor经常产生高损失,说明它和真实目标匹配不好。那就把它往「损失最小」的方向微调。
def anchor_fine_tune(anchors, losses, lr=0.01):
"""
根据损失微调Anchor尺寸
losses: 每个Anchor的平均损失
"""
new_anchors = anchors.copy()
for i in range(len(anchors)):
# 损失越大,调整幅度越大
gradient = losses[i] / (sum(losses) + 1e-6)
# 向减小损失的方向调整
new_anchors[i] *= (1 - lr * gradient)
return new_anchors
嗯,这个方法我试过几次,效果还行。但要注意学习率不能太大,否则Anchor会「震荡」。我一般设lr=0.001,每100个batch微调一次。
4.4 实战:在YOLOv5中替换自定义Anchor
说了这么多理论,咱们直接上代码。以YOLOv5为例,替换Anchor分三步:
4.4.1 第一步:聚类生成新Anchor
import numpy as np
from sklearn.cluster import KMeans
def generate_anchors(dataset_path, img_size=640, n_anchors=9):
"""
生成针对小目标的Anchor
"""
# 读取所有标注框
boxes = []
with open(dataset_path, 'r') as f:
for line in f:
parts = line.strip().split()
# YOLO格式: class x_center y_center width height
w = float(parts[3]) * img_size
h = float(parts[4]) * img_size
# 只保留小目标(面积 < 32x32)
if w * h < 1024:
boxes.append([w, h])
boxes = np.array(boxes)
# 使用IoU距离的K-means
kmeans = KMeans(n_clusters=n_anchors, random_state=0)
kmeans.fit(boxes)
# 按面积排序
anchors = kmeans.cluster_centers_
anchors = anchors[np.argsort(anchors[:, 0] * anchors[:, 1])]
return anchors
4.4.2 第二步:写入模型配置文件
YOLOv5的Anchor在models/yolov5s.yaml中:
# 原始Anchor(COCO默认)
anchors:
- [10,13, 16,30, 33,23] # P3/8
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32
# 替换为小目标Anchor(示例)
anchors:
- [4,5, 8,8, 12,16, 16,12] # P3/8: 4个极小Anchor
- [20,20, 28,32, 32,28] # P4/16: 3个中等Anchor
- [40,40, 64,64] # P5/32: 2个大Anchor
注意:替换后要检查Anchor的尺寸是否和特征图匹配。P3层是80x80的特征图,Anchor不能超过特征图尺寸。我见过有人把64x64的Anchor放在P3层,结果训练直接崩了。
4.4.3 第三步:验证效果
替换后,跑几个epoch看看mAP变化:
# 训练命令
python train.py --data your_data.yaml --cfg models/yolov5s_custom.yaml --epochs 100
# 重点关注
# 1. 小目标AP(AP_small)是否提升
# 2. 召回率(Recall)是否提升
# 3. 训练损失是否下降更快
我个人的经验是:好的Anchor调整,能在前10个epoch就看到效果。如果20个epoch后mAP还没变化,说明聚类可能有问题,或者数据集本身小目标太少。
4.5 常见问题与避坑
- Anchor数量越多越好? 不是。Anchor太多会导致正负样本不平衡,反而降低性能。小目标数据集9-12个足够了。
- 聚类后直接替换就行? 不行。要检查聚类结果是否合理。我见过聚类出0x0的Anchor,显然是数据标注有问题。
- 动态调整频率越高越好? 不是。太频繁会导致Anchor不稳定,模型难以收敛。我一般10-20个epoch调整一次。
- 小目标Anchor和大目标Anchor能共存吗? 能。但建议分层管理,小Anchor放高层特征图,大Anchor放低层特征图。
最后一个小建议:Anchor优化不是一劳永逸的。每次数据集更新、标注标准变化,都要重新聚类。我习惯把聚类脚本放在项目根目录,每次训练前跑一遍,确保Anchor始终是最优的。
好了,Anchor优化就讲到这里。下一章咱们聊聊损失函数——怎么让模型更「关注」小目标。嗯,那个坑更多,到时候我慢慢讲。