最近在研究 Yolov2 论文的时候,发现作者在做先验框聚类使用的指标并非欧式距离,而是IOU。在找了很多资料之后,基本确定 Python 没有自定义指标聚类的函数,所以打算自己做一个
设训练集的 shape 是 [n_sample, n_feature],基本思路是:
因为设计之初就打算使用自定义距离函数,所以求导是很大的难题。笔者不才,最终决定借助 PyTorch 自动求导的天然优势
先给出欧式距离的计算函数
def Eu_dist(data, center):
""" 以 欧氏距离 为聚类准则的距离计算函数
data: 形如 [n_sample, n_feature] 的 tensor
center: 形如 [n_cluster, n_feature] 的 tensor"""
data = data.unsqueeze(1)
center = center.unsqueeze(0)
dist = ((data - center) ** 2).sum(dim=2)
return dist
然后就是聚类器的代码
import torch
import numpy as np
Adam = torch.optim.Adam
class Cluster:
""" 聚类器
n_cluster: 簇中心数
dist_fun: 距离计算函数
kwargs:
data: 形如 [n_sample, n_feather] 的 tensor
center: 形如 [n_cluster, n_feature] 的 tensor
return: 形如 [n_sample, n_cluster] 的 tensor
max_iter: 最大迭代轮数
init: 初始簇中心
cluster_centers_: 聚类中心
labels_: 聚类结果
lr: 中心点坐标学习率"""
def __init__(self, n_cluster, dist_fun, max_iter=40, init=None):
self.n_cluster = n_cluster
self.dist_fun = dist_fun
self.max_iter = max_iter
self.cluster_centers_ = torch.FloatTensor(init) if init else None
self.labels_ = None
self.lr = 0.2
def fit(self, data, interval=20):
if self.cluster_centers_ is None:
self._init_cluster(data)
self.lr = 0.08
# 初始化簇中心时使用较大的lr,而后切换为正常的lr
for epoch in range(1, self.max_iter + 1):
grad_sum = self._classify(data)
if not epoch % interval:
print(f"epoch: {epoch}, grad_sum: {grad_sum:.4f}")
# 开始若干轮次的训练
def _init_cluster(self, data):
self.cluster_centers_ = data.mean(dim=0).reshape(1, -1)
for _ in range(1, self.n_cluster):
dist = np.array(self.dist_fun(data, self.cluster_centers_).min(dim=1)[0])
new_cluster = data[np.random.choice(range(data.shape[0]), p=dist / dist.sum())].reshape(1, -1)
# 取新的中心点
self.cluster_centers_ = torch.cat([self.cluster_centers_, new_cluster], dim=0)
self._classify(data)
def _classify(self, data, epochs=50):
# 对样本分类并更新中心点
dist = self.dist_fun(data, self.cluster_centers_)
self.labels_ = dist.argmin(axis=1)
# 计算距离得到类别
self.cluster_container_ = [[] for _ in range(dist.shape[1])]
for sample_idx, cluster_idx in enumerate(self.labels_):
self.cluster_container_[cluster_idx].append(data[sample_idx])
# 将样本分到对应的簇容器
self.cluster_container_ = list(map(torch.stack, self.cluster_container_))
grad_sum = 0
for cluster_idx, cluster in enumerate(self.cluster_container_):
center = self.cluster_centers_[cluster_idx].cuda()
center.requires_grad = True
cluster = cluster.cuda()
# 将数据加载到 GPU 上
optimizer = Adam([center], lr=self.lr)
for epoch in range(1, epochs + 1):
loss = self.dist_fun(cluster, center).sum()
loss.backward()
optimizer.step()
optimizer.zero_grad()
# 反向传播梯度更新簇中心
loss = self.dist_fun(cluster, center).sum()
loss.backward()
grad_sum += torch.abs(center.grad.data).sum().item()
self.cluster_centers_[cluster_idx] = center.cpu().detach()
return grad_sum
KMeans++ 是以欧式距离为聚类准则的经典聚类算法。在 iris 数据集上,KMeans++ 远远快于我的聚类器;初始化簇中心的时候,KMeans++ 也比我的聚类器更稳定。但在我反复对比测试的几轮里,我的聚类器精度也是不差的 —— 可以看到下图里的聚类结果完全一致
虽然各方面与老牌算法对比的确不行,但是我的这个聚类器最大的亮点还是自定义距离函数
在目标检测领域里,IOU 是指两个检测框的交并比 (交区域的面积 / 并区域的面积)。Yolov2 作者做检测框聚类的时候,以 1 - IOU 来计算两个检测框的距离。距离函数定义如下:
def neg_IOU_dist(data, center):
""" 以 (1 - IOU) 为聚类准则的距离计算函数
data: 形如 [n_sample, 2] 的 tensor
center: 形如 [n_cluster, 2] 的 tensor"""
n_sample = data.shape[0]
n_cluster = center.shape[0]
union_inter = (torch.prod(data, dim=1) + torch.prod(center, dim=1)).reshape(1, -1)
data = data.unsqueeze(1).repeat(1, n_cluster, 1)
center = center.unsqueeze(0).repeat(n_sample, 1, 1)
inter = torch.prod(torch.stack([data, center], dim=2).min(dim=2)[0], dim=2)
dist = 1 - inter / (union_inter - inter)
return dist
Adam 优化器不像 SGD 一样容易陷入局部最优解,从我做神经网络的经验看,优化这种简单函数不成问题。但是验证就还没有时间做,可以的话后续会补上 COCO 数据集检测框的聚类结果