Torch 基于距离的自定义聚类

最近在研究 Yolov2 论文的时候,发现作者在做先验框聚类使用的指标并非欧式距离,而是IoU。在找了很多资料之后,基本确定 Python 没有自定义指标聚类的函数,所以打算自己做一个

设训练集的 shape 是 [n_sample, n_feature],基本思路是:

  • 簇中心初始化:第 1 个簇中心取样本的特征均值,shape = [n_feature, ];从第 2 个簇中心开始,用距离函数 (自定义) 计算每个样本到最近中心点的距离,归一化后作为选取下一个簇中心的概率 —— 迭代到选取到足够的簇中心为止
  • 簇中心调整:训练多轮,每一轮以样本点到最近中心点的距离之和作为 loss,梯度下降法 + Adam 优化器逼近最优解,在 loss 浮动值小于 0 的次数达到一定值时停止训练

因为设计之初就打算使用自定义距离函数,所以求导是很大的难题。笔者不才,最终决定借助 Torch 自动求导的天然优势

欧氏距离

即 n 维空间中,两点的直线距离

def Eu_dist(data, center):
    ''' 以 欧氏距离 为聚类准则的距离计算函数
        data: 形如 [n_sample, n_feature] 的 tensor
        center: 形如 [n_cluster, n_feature] 的 tensor'''
    return ((data[:, None] - center[None]) ** 2).sum(dim=2)

聚类器对象

使用时只需关注 __init__、fit、classify 函数

距离优化模式有三种:

  • max:以簇的覆盖半径为优化目标,该模式会考虑到所有的样本点,适用于样本较为集中、样本分布密度差异巨大的情况
  • mean:以簇中心到其样本的平均距离为优化目标,适用于离群点较多的情况
  • sum:以样本到所有簇中心的最小距离之和为优化目标,适用情况与mean差不多

使用这个聚类器需导入 optimize 模块,其代码见另一篇文章:

Torch 梯度下降法 minimizehttps://hebitzj.blog.csdn.net/article/details/124533862

from typing import Sequence

from optimize import *


class Dist_Cluster:
    ''' 基于距离的聚类器
        n_cluster: 簇中心数
        dist_fun: 距离计算函数
            kwargs:
                data: 形如 [n_sample, n_feather] 的 tensor
                center: 形如 [n_cluster, n_feature] 的 tensor
            return: 形如 [n_sample, n_cluster] 的 tensor
        mode: 距离优化模式 ('max', 'mean', 'sum')
        init: 初始簇中心
        patience: 允许 loss 无进展的次数
        lr: 中心点坐标学习率
        
        cluster_centers: 聚类中心
        labels: 聚类结果'''

    def __init__(self, n_cluster: int,
                 dist_fun: Callable[[torch.tensor, torch.tensor],
                                    torch.tensor] = Eu_dist,
                 mode: str = 'max',
                 init: Optional[Sequence[Sequence]] = None,
                 patience: int = 50,
                 lr: float = 0.08):
        self._n_cluster = n_cluster
        self._dist_fun = dist_fun
        self._patience = patience
        self._lr = lr
        self._mode = mode
        # 初始化参数
        self.cluster_centers = None if init is None else torch.tensor(init).float()
        self.labels = None
        self._bar_len = 20

    def fit(self, data: torch.tensor, prefix='Cluster'):
        ''' data: 形如 [n_sample, n_feature] 的 tensor
            return: 簇惯性'''
        LOGGER.info(('%10s' * 3) % ('', 'cur_loss', 'min_loss'))
        self._init_cluster(data, self._patience // 5, prefix)
        inertia = self._train(data, self._lr, self._patience, prefix=prefix)
        # 开始若干轮次的训练,得到簇惯性
        self.classify(data)
        return inertia

    def classify(self, data: torch.tensor):
        ''' data: 形如 [n_sample, n_feature] 的 tensor
            return: 分类标签'''
        dist = self._dist_fun(data, self.cluster_centers)
        # 将标签加载到实例属性
        self.labels = dist.argmin(axis=1)
        return self.labels

    def _init_cluster(self, data, patience, prefix):
        # 没有中心点时,初始化一个中心点
        if self.cluster_centers is None:
            self.cluster_centers = data.mean(dim=0).reshape(1, -1)
        # 补全中心点
        for cur_center_num in range(self.cluster_centers.shape[0], self._n_cluster):
            dist = np.array(self._dist_fun(data, self.cluster_centers).min(dim=1)[0].cpu())
            dist -= dist.min()
            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).float()
            self._train(data, self._lr * 2.5, patience, prefix=f'Init_{cur_center_num}'.ljust(len(prefix)), init=True)
            # 初始化簇中心时使用较大的lr

    def _train(self, data, lr, patience, prefix, init=False):
        loss_fun = lambda center: self._loss(data, center)
        self.cluster_centers, interia, _ = minimize(self.cluster_centers, loss_fun, lr=lr, patience=patience,
                                                    max_iter=None, prefix=prefix, title=False, leave=not init)
        return interia

    def _loss(self, data, center):
        sample_dist = self._dist_fun(data, center)
        min_dist, self.labels = sample_dist.min(dim=1)
        # 按照距离进行分类
        clf_result = []
        for idx in range(len(center)):
            own = self.labels == idx
            if torch.any(own):
                clf_result.append(min_dist[self.labels == idx])
            else:
                clf_result.append(sample_dist[:, idx].min())
        # 计算 loss 值
        if self._mode == 'max':
            loss = sum([dists.max() + .05 * dists.mean() for dists in clf_result])
        elif self._mode == 'mean':
            loss = sum([dists.mean() for dists in clf_result])
        elif self._mode == 'sum':
            loss = sum([dists.sum() for dists in clf_result])
        else:
            raise KeyError('mode 参数出错')
        return loss

与KMeans++比较

if __name__ == '__main__':
    import matplotlib.pyplot as plt
    from sklearn import datasets
    from sklearn.cluster import KMeans
    import numpy as np
    import warnings

    warnings.filterwarnings('ignore')


    class Timer:

        def __new__(cls, fun, *args, **kwargs):
            import time
            start = time.time()
            fun(*args, **kwargs)
            cost = time.time() - start
            return cost * 1e3  # ms


    def draw_result(train_x, labels, cents, idx, title):
        ''' 聚类结果可视化'''
        # 创建子图, 并设置标题
        fig = plt.subplot(1, 2, idx, projection='3d')
        plt.title(title)
        # 簇中心数量, 以及每个簇的颜色
        n_clusters = np.unique(labels).shape[0]
        color = ["red", "orange", "yellow"]
        # 分别绘制每个类别的样本
        for i in range(n_clusters):
            samples = train_x[labels == i]
            fig.scatter(*samples[:, :3].T, c=color[i])
        fig.scatter(*cents.T, c="deepskyblue", marker="*", s=100)
        # 视角变换
        fig.view_init(60, 110)


    # 读取数据集
    iris = datasets.load_iris()
    iris_x = torch.tensor(iris.data)
    iris_y = torch.tensor(iris.target, dtype=torch.int)

    # 使用 KMeans 聚类
    clf = KMeans(n_clusters=3)
    print(f'Kmeans: {Timer(clf.fit, iris_x):.0f} ms')
    draw_result(iris_x, clf.labels_, clf.cluster_centers_, 1, "KMeans++")

    # 使用基于距离的自定义聚类
    clf = Dist_Cluster(n_cluster=3, dist_fun=Eu_dist, lr=1., mode='mean')
    print(f'My Cluster: {Timer(clf.fit, iris_x):.0f} ms')
    draw_result(iris_x, clf.labels, clf.cluster_centers, 2, "My Cluster")

    plt.show()

KMeans++ 是以欧式距离为聚类准则的经典聚类算法。在 iris 数据集上,KMeans++ 远远快于我的聚类器。但在我反复对比测试的几轮里,我的聚类器精度也是不差的 —— 可以看到下图里的聚类结果完全一致

KMeans++My Cluster
Cost145 ms1597 ms
Center

[[5.9016, 2.7484, 4.3935, 1.4339],

[5.0060, 3.4280, 1.4620, 0.2460],
[6.8500, 3.0737, 5.7421, 2.0711]]

[[5.9016, 2.7485, 4.3934, 1.4338],
[5.0063, 3.4284, 1.4617, 0.2463],
[6.8500, 3.0741, 5.7420, 2.0714]]

虽然速度方面与老牌算法对比的确不行,但是我的这个聚类器最大的亮点还是自定义距离函数

余弦相似度

根据余弦公式,给出如下计算函数

gif.latex?%5Ccos%7B%5Ctheta%7D%3D%5Cfrac%7B%5Cvec%7Ba%7D%5Ccdot%20%5Cvec%7Bb%7D%7D%20%7B%7C%5Cvec%7Ba%7D%7C%7C%5Cvec%7Bb%7D%7C%7D

余弦距离和其它距离不同,其值越大,则距离反而越近。但是我的聚类器的机制是缩小距离和,所以需要给这个余弦距离取负

def Cos_similarity(data, refer):
    ''' 余弦相似度计算
        data: 形如 [n_sample, n_feature] 的 tensor
        refer: 形如 [n_cluster, n_feature] 的 tensor'''
    data_len = (data ** 2).sum(dim=1) ** 0.5
    refer_len = (refer ** 2).sum(dim=1) ** 0.5
    # 计算向量模
    vec_dot = (data[:, None] * refer[None]).sum(dim=-1)
    # 计算余弦相似度
    return - vec_dot / (data_len[:, None] * refer_len[None])

DIoU 距离

本来想用 Yolov4 检测框聚类引入的 CIoU 做聚类,但是没法解决梯度弥散的问题,所以退其次用了 DIoU

def DIoU_dist(boxes, anchor):
    """ 以 DIoU 为聚类准则的距离计算函数
        boxes: 形如 [n_sample, 2] 的 tensor
        anchor: 形如 [n_cluster, 2] 的 tensor"""
    anchor = torch.abs(anchor)
    # 计算欧式距离
    dist = Eu_dist(boxes, anchor)
    boxes = boxes[:, None]
    anchor = anchor[None]
    # 计算交并面积
    union_and_inter = torch.prod(boxes, dim=-1) + torch.prod(anchor, dim=-1)
    inter = torch.prod(torch.minimum(boxes, anchor), dim=-1)
    iou = inter / (union_and_inter - inter)
    # 计算对角线长度
    diag = (torch.maximum(boxes, anchor) ** 2).sum(dim=-1)
    return 1 - iou + dist / diag

我提取了 DroneVehicle 数据集的 650156 个预测框的尺寸做聚类,在这个过程中发现因为小尺寸的预测框过多,导致聚类中心聚集在原点附近。所以对 loss 函数做了改进:先分类,再计算每个分类下的最大距离之和

横轴表示检测框的宽度,纵轴表示检测框的高度,其数值都是相对于原图尺寸的比例。若原图尺寸为 608 * 608,则得到的 9 个先验框为:

[ 10,  4 ][ 5,  12 ][ 21,  10 ]
[ 43,  3 ][ 13,  29 ][ 14,  64 ]
[ 58,  20 ][ 135,  37 ][ 43, 141 ]
  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在PyTorch中,我们可以通过编写自定义的`forward`方法来定义自己的模型。首先,我们需要创建一个继承自`nn.Module`的类,并在类的初始化函数中定义我们的模型结构。然后,在该类中重写`forward`方法,以定义模型的前向传播过程。 具体而言,我们需要在初始化函数中定义网络的各层结构。比如,可以使用`nn.Linear`创建全连接层,使用`nn.Conv2d`创建卷积层,使用`nn.ReLU`创建激活函数等。在定义这些层时,需要注意指定输入和输出的维度。 接下来,在`forward`方法中,我们需要根据模型的结构,按照自己的需求将输入数据传递到各个层中,并进行计算和变换。具体来说,我们可以通过调用已定义的层,并将输入作为参数传递给这些层来实现。最后,我们可以返回最终的输出结果。 例如,假设我们要创建一个简单的线性模型,其输入是一个维度为`n_input`的向量,输出是一个维度为`n_classes`的向量。我们可以如下定义一个自定义的模型类: ```python import torch import torch.nn as nn class CustomModel(nn.Module): def __init__(self, n_input, n_classes): super(CustomModel, self).__init__() self.fc = nn.Linear(n_input, n_classes) def forward(self, x): out = self.fc(x) return out ``` 在上述代码中,我们通过`nn.Linear(n_input, n_classes)`创建了一个全连接层,并在`forward`方法中将输入`x`传递给该层,得到输出`out`。最后,我们将`out`作为模型的最终输出返回。 自定义模型的`forward`方法允许我们在前向传播过程中自由地使用各种层和操作,并根据输入数据进行相应的计算。这样,我们可以灵活地定义自己的深度学习模型,并根据实际需求进行调整和修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

荷碧TongZJ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值