是否可以使用 scikit-learn K-Means 聚类指定自己的距离函数?

Is it possible to specify your own distance function using scikit-learn K-Means Clustering?

提问人:bmasc 提问时间:4/3/2011 最后编辑:Salvador Dalibmasc 更新时间:1/17/2023 访问量:124707

问:

是否可以使用 scikit-learn K-Means 聚类指定自己的距离函数?

Python 机器学习 聚类分析 k-means scikit-learn

评论

55赞 Has QUIT--Anony-Mousse 3/27/2012
请注意,k-means 是针对欧几里得距离设计的。当均值不再是聚类“中心”的最佳估计时,它可能会停止与其他距离收敛。
3赞 curious 1/7/2014
为什么 k-means 仅适用于欧几里得远距离?
12赞 ely 10/16/2014
@Anony-慕斯 说 k-means 只为欧几里得距离而设计是不正确的。可以对其进行修改,以使用观测点空间上定义的任何有效距离度量。例如,看看关于 k-medoids 的文章
5赞 Has QUIT--Anony-Mousse 10/16/2014
@curious:均值最小化平方差(= 欧几里得距离的平方)。如果需要不同的距离函数,则需要将均值替换为适当的中心估计。K-medoids 就是这样一种算法,但找到 medoid 要昂贵得多。
4赞 jakevdp 10/27/2015
这里有点相关:目前有一个实现内核 K-Means 的开放拉取请求。完成后,您将能够为计算指定自己的内核。

答:

66赞 ogrisel 4/4/2011 #1

不幸的是没有:scikit-learn 当前 k-means 的实现仅使用欧几里得距离。

将 k-means 扩展到其他距离并非易事,Denis 上面的答案并不是为其他指标实现 k-means 的正确方法。

评论

3赞 Clumsy cat 11/25/2020
为什么丹尼斯给出的实现不正确?
1赞 ogrisel 12/8/2020
例如,对于曼哈顿距离(p=1 的 Minkowski),您需要一个专用算法(K-Medoids en.wikipedia.org/wiki/K-medoids),该算法在内部与 K-Means 完全不同。
1赞 Antoine 11/2/2021
@ogrisel 这完全是错误的。曼哈顿距离的 k 均值是......k 中位数,即中心更新为聚类的中位数(分别针对每个维度计算)。
89赞 denis 4/5/2011 #2

这是一个使用 scipy.spatial.distance 中 20 多个距离中的任何一个的小 kmeans,或者一个用户函数。
欢迎发表评论(到目前为止,这只有一个用户,还不够); 特别是,你的 N、dim、k、公制是多少?

#!/usr/bin/env python
# kmeans.py using any of the 20-odd metrics in scipy.spatial.distance
# kmeanssample 2 pass, first sample sqrt(N)

from __future__ import division
import random
import numpy as np
from scipy.spatial.distance import cdist  # $scipy/spatial/distance.py
    # http://docs.scipy.org/doc/scipy/reference/spatial.html
from scipy.sparse import issparse  # $scipy/sparse/csr.py

__date__ = "2011-11-17 Nov denis"
    # X sparse, any cdist metric: real app ?
    # centres get dense rapidly, metrics in high dim hit distance whiteout
    # vs unsupervised / semi-supervised svm

#...............................................................................
def kmeans( X, centres, delta=.001, maxiter=10, metric="euclidean", p=2, verbose=1 ):
    """ centres, Xtocentre, distances = kmeans( X, initial centres ... )
    in:
        X N x dim  may be sparse
        centres k x dim: initial centres, e.g. random.sample( X, k )
        delta: relative error, iterate until the average distance to centres
            is within delta of the previous average distance
        maxiter
        metric: any of the 20-odd in scipy.spatial.distance
            "chebyshev" = max, "cityblock" = L1, "minkowski" with p=
            or a function( Xvec, centrevec ), e.g. Lqmetric below
        p: for minkowski metric -- local mod cdist for 0 < p < 1 too
        verbose: 0 silent, 2 prints running distances
    out:
        centres, k x dim
        Xtocentre: each X -> its nearest centre, ints N -> k
        distances, N
    see also: kmeanssample below, class Kmeans below.
    """
    if not issparse(X):
        X = np.asanyarray(X)  # ?
    centres = centres.todense() if issparse(centres) \
        else centres.copy()
    N, dim = X.shape
    k, cdim = centres.shape
    if dim != cdim:
        raise ValueError( "kmeans: X %s and centres %s must have the same number of columns" % (
            X.shape, centres.shape ))
    if verbose:
        print "kmeans: X %s  centres %s  delta=%.2g  maxiter=%d  metric=%s" % (
            X.shape, centres.shape, delta, maxiter, metric)
    allx = np.arange(N)
    prevdist = 0
    for jiter in range( 1, maxiter+1 ):
        D = cdist_sparse( X, centres, metric=metric, p=p )  # |X| x |centres|
        xtoc = D.argmin(axis=1)  # X -> nearest centre
        distances = D[allx,xtoc]
        avdist = distances.mean()  # median ?
        if verbose >= 2:
            print "kmeans: av |X - nearest centre| = %.4g" % avdist
        if (1 - delta) * prevdist <= avdist <= prevdist \
        or jiter == maxiter:
            break
        prevdist = avdist
        for jc in range(k):  # (1 pass in C)
            c = np.where( xtoc == jc )[0]
            if len(c) > 0:
                centres[jc] = X[c].mean( axis=0 )
    if verbose:
        print "kmeans: %d iterations  cluster sizes:" % jiter, np.bincount(xtoc)
    if verbose >= 2:
        r50 = np.zeros(k)
        r90 = np.zeros(k)
        for j in range(k):
            dist = distances[ xtoc == j ]
            if len(dist) > 0:
                r50[j], r90[j] = np.percentile( dist, (50, 90) )
        print "kmeans: cluster 50 % radius", r50.astype(int)
        print "kmeans: cluster 90 % radius", r90.astype(int)
            # scale L1 / dim, L2 / sqrt(dim) ?
    return centres, xtoc, distances

#...............................................................................
def kmeanssample( X, k, nsample=0, **kwargs ):
    """ 2-pass kmeans, fast for large N:
        1) kmeans a random sample of nsample ~ sqrt(N) from X
        2) full kmeans, starting from those centres
    """
        # merge w kmeans ? mttiw
        # v large N: sample N^1/2, N^1/2 of that
        # seed like sklearn ?
    N, dim = X.shape
    if nsample == 0:
        nsample = max( 2*np.sqrt(N), 10*k )
    Xsample = randomsample( X, int(nsample) )
    pass1centres = randomsample( X, int(k) )
    samplecentres = kmeans( Xsample, pass1centres, **kwargs )[0]
    return kmeans( X, samplecentres, **kwargs )

def cdist_sparse( X, Y, **kwargs ):
    """ -> |X| x |Y| cdist array, any cdist metric
        X or Y may be sparse -- best csr
    """
        # todense row at a time, v slow if both v sparse
    sxy = 2*issparse(X) + issparse(Y)
    if sxy == 0:
        return cdist( X, Y, **kwargs )
    d = np.empty( (X.shape[0], Y.shape[0]), np.float64 )
    if sxy == 2:
        for j, x in enumerate(X):
            d[j] = cdist( x.todense(), Y, **kwargs ) [0]
    elif sxy == 1:
        for k, y in enumerate(Y):
            d[:,k] = cdist( X, y.todense(), **kwargs ) [0]
    else:
        for j, x in enumerate(X):
            for k, y in enumerate(Y):
                d[j,k] = cdist( x.todense(), y.todense(), **kwargs ) [0]
    return d

def randomsample( X, n ):
    """ random.sample of the rows of X
        X may be sparse -- best csr
    """
    sampleix = random.sample( xrange( X.shape[0] ), int(n) )
    return X[sampleix]

def nearestcentres( X, centres, metric="euclidean", p=2 ):
    """ each X -> nearest centre, any metric
            euclidean2 (~ withinss) is more sensitive to outliers,
            cityblock (manhattan, L1) less sensitive
    """
    D = cdist( X, centres, metric=metric, p=p )  # |X| x |centres|
    return D.argmin(axis=1)

def Lqmetric( x, y=None, q=.5 ):
    # yes a metric, may increase weight of near matches; see ...
    return (np.abs(x - y) ** q) .mean() if y is not None \
        else (np.abs(x) ** q) .mean()

#...............................................................................
class Kmeans:
    """ km = Kmeans( X, k= or centres=, ... )
        in: either initial centres= for kmeans
            or k= [nsample=] for kmeanssample
        out: km.centres, km.Xtocentre, km.distances
        iterator:
            for jcentre, J in km:
                clustercentre = centres[jcentre]
                J indexes e.g. X[J], classes[J]
    """
    def __init__( self, X, k=0, centres=None, nsample=0, **kwargs ):
        self.X = X
        if centres is None:
            self.centres, self.Xtocentre, self.distances = kmeanssample(
                X, k=k, nsample=nsample, **kwargs )
        else:
            self.centres, self.Xtocentre, self.distances = kmeans(
                X, centres, **kwargs )

    def __iter__(self):
        for jc in range(len(self.centres)):
            yield jc, (self.Xtocentre == jc)

#...............................................................................
if __name__ == "__main__":
    import random
    import sys
    from time import time

    N = 10000
    dim = 10
    ncluster = 10
    kmsample = 100  # 0: random centres, > 0: kmeanssample
    kmdelta = .001
    kmiter = 10
    metric = "cityblock"  # "chebyshev" = max, "cityblock" L1,  Lqmetric
    seed = 1

    exec( "\n".join( sys.argv[1:] ))  # run this.py N= ...
    np.set_printoptions( 1, threshold=200, edgeitems=5, suppress=True )
    np.random.seed(seed)
    random.seed(seed)

    print "N %d  dim %d  ncluster %d  kmsample %d  metric %s" % (
        N, dim, ncluster, kmsample, metric)
    X = np.random.exponential( size=(N,dim) )
        # cf scikits-learn datasets/
    t0 = time()
    if kmsample > 0:
        centres, xtoc, dist = kmeanssample( X, ncluster, nsample=kmsample,
            delta=kmdelta, maxiter=kmiter, metric=metric, verbose=2 )
    else:
        randomcentres = randomsample( X, ncluster )
        centres, xtoc, dist = kmeans( X, randomcentres,
            delta=kmdelta, maxiter=kmiter, metric=metric, verbose=2 )
    print "%.0f msec" % ((time() - t0) * 1000)

    # also ~/py/np/kmeans/test-kmeans.py

2012 年 3 月 26 日添加的一些注释:

1)对于余弦距离,首先将所有数据向量归一化为|X|= 1;然后

cosinedistance( X, Y ) = 1 - X . Y = Euclidean distance |X - Y|^2 / 2

速度很快。对于位向量,将范数与向量分开 而不是扩展到浮动 (尽管某些程序可能会为您扩展)。 对于稀疏向量,比如 N, X 的 1 %。Y 应花时间 O( 2 % N ), 空间O(N);但我不知道哪些程序可以做到这一点。

2) Scikit-learn 聚类对 k-means、mini-batch-k-means ... 使用适用于 scipy.sparse 矩阵的代码。

3) 始终在 k-means 之后检查簇大小。 如果你期待大致相等大小的集群,但它们出来了......(挠头的声音)。[44 37 9 5 5] %

评论

1赞 Legend 7/11/2011
+1 首先,感谢您分享您的实现。我只是想确认该算法非常适合我在 700 维空间中包含 900 个向量的数据集。我只是想知道是否也可以评估生成的集群的质量。是否可以重用代码中的任何值来计算聚类质量,以帮助选择最佳聚类的数量?
6赞 denis 7/11/2011
传奇,不客气。(更新了代码以打印簇 50 % / 90 % 半径)。“集群质量”是一个庞大的话题:你有多少个集群,你是否有已知集群的训练样本,例如来自专家的训练样本?有关簇数,请参阅 SO how-do-i-determine-k-when-using-k-means-clustering-when-using-k-means-clustering
1赞 Legend 7/12/2011
再次感谢你。实际上,我没有训练样本,但正在尝试在分类后手动验证集群(也尝试扮演领域专家的角色)。在将 SVD 应用于某些原始文档并减小其尺寸后,我正在执行文档级分类。结果看起来不错,但我不确定如何验证它们。在初始阶段,在探索各种聚类有效性指标时,我遇到了 Dunn's Index、Elbow 方法等,但不确定要使用哪一个,所以我想我将从 Elbow 方法开始。
9赞 Nevoris 12/19/2017
我知道这是在挖掘一些非常古老的东西,但我只是开始使用 kmeans 并偶然发现了这一点。对于将来想使用此代码的读者:首先查看 @Anony-Mousse 对上述问题的评论!据我所知,这种实现做出了错误的假设,即您仍然可以以某种方式使用“聚类中点的平均值”来确定该聚类的质心。除了欧几里得距离之外,这没有任何意义(除非在单位球面上非常特殊的情况下,等等)。同样,Anony-Mousse对这个问题的评论是正确的。
5赞 denis 12/19/2017
@Nevoris,是的,我同意,除了余弦距离:请参阅此处了解原因,以及 why-does-k-means-clustering-algorithm-use-only-euclidean-distance-metric
17赞 Adam 3/26/2012 #3

是的,您可以使用差异度量函数;但是,根据定义,k 均值聚类算法依赖于与每个聚类均值的欧氏距离。

您可以使用不同的指标,因此即使您仍在计算平均值,也可以使用类似 mahalnobis 距离的东西。

评论

32赞 Has QUIT--Anony-Mousse 3/27/2012
+1:让我强调一下,取平均值仅适用于某些距离函数,例如欧几里得距离。对于其他距离函数,您也需要替换聚类中心估计函数!
2赞 curious 1/7/2014
@Anony慕斯。例如,当我使用余弦距离时,我应该改变什么?
7赞 Has QUIT--Anony-Mousse 1/7/2014
我不知道。我还没有看到与余弦趋同的证据。我相信,如果你的数据是非负的,并且归一化为单位球面,它就会收敛,因为这样它本质上是不同向量空间中的k-means。
1赞 Chiraz BenAbdelkader 3/29/2018
我同意@Anony-慕斯的观点。对我来说,这只是一个垃圾进垃圾出的情况:你可以用任何你想要的距离函数运行 K-means,但如果该函数违反了算法的基本假设,不要指望它会产生有意义的结果!
0赞 Cecilia 7/29/2019
@Anony-慕斯,但是如何使用 mahalnobis 距离实现 K-means?
5赞 Dr Fabio Gori 3/31/2013 #4

Spectral Python 的 k-means 允许使用 L1(曼哈顿)距离。

34赞 mdubez 9/12/2016 #5

只需在可以执行此操作的地方使用 nltk 即可,例如

from nltk.cluster.kmeans import KMeansClusterer
NUM_CLUSTERS = <choose a value>
data = <sparse matrix that you would normally give to scikit>.toarray()

kclusterer = KMeansClusterer(NUM_CLUSTERS, distance=nltk.cluster.util.cosine_distance, repeats=25)
assigned_clusters = kclusterer.cluster(data, assign_clusters=True)

评论

6赞 Nikana Reklawyks 1/17/2017
这种实现的效率如何?聚类到5k个点(在100维中)似乎需要很长时间。
3赞 Nikana Reklawyks 1/18/2017
在维度 100 中,聚类 1k 点每次运行 () 需要 1 秒,1.5k 点需要 2 分钟,2k 点需要...太长了。repeats
6赞 Chiraz BenAbdelkader 3/29/2018
事实上;根据下面的 @Anony-Mousse 评论,余弦距离似乎可能存在收敛问题。对我来说,这实际上是一个垃圾进垃圾出的情况:你可以使用任何你想要的距离函数,但如果该函数违反了算法的假设,不要指望它会产生有意义的结果!
0赞 irritable_phd_syndrome 10/26/2022
仅供参考,' 不适用于汉明距离,因为在计算新质心 ( in 中,它使用浮点运算。它需要一些修改以适应使用汉明距离。nltk/3.7KMeansClusterernltk/cluster/kmeans.py : 186KMeansClusterer._centroid()
12赞 CpILL 8/7/2018 #6

Pyclustering是python / C++(所以它很快!),并允许您指定自定义指标函数

from pyclustering.cluster.kmeans import kmeans
from pyclustering.utils.metric import type_metric, distance_metric

user_function = lambda point1, point2: point1[0] + point2[0] + 2
metric = distance_metric(type_metric.USER_DEFINED, func=user_function)

# create K-Means algorithm with specific distance metric
start_centers = [[4.7, 5.9], [5.7, 6.5]];
kmeans_instance = kmeans(sample, start_centers, metric=metric)

# run cluster analysis and obtain results
kmeans_instance.process()
clusters = kmeans_instance.get_clusters()

实际上,我还没有测试过这段代码,而是从票证示例代码中拼凑出来的。

评论

0赞 CpILL 8/7/2018
需要安装 Matplotlib,它需要“Python 作为 Mac OS X 上的框架”:(
4赞 user10595337 5/27/2019 #7

Sklearn Kmeans 使用欧几里得距离。它没有指标参数。也就是说,如果要对时间序列进行聚类,则可以使用 python 包,此时可以指定指标 (, , )。tslearndtwsoftdtweuclidean

-2赞 Rahul Nanda 9/2/2020 #8
def distance_metrics(dist_metrics):
    kmeans_instance = kmeans(trs_data, initial_centers, metric=dist_metrics)

    label = np.zeros(210, dtype=int)
    for i in range(0, len(clusters)):
        for index, j in enumerate(clusters[i]):
            label[j] = i

评论

2赞 Jan Stránský 9/3/2020
请考虑添加一些单词注释
0赞 AmphotericLewisAcid 7/28/2021
这甚至与 or 的签名不匹配?sklearn.cluster.KMeanssklearn.cluster.k_means
0赞 neoscene 10/28/2022 #9

是的,在当前稳定版本的 sklearn (scikit-learn 1.1.3) 中,您可以轻松使用自己的距离指标。您所要做的就是创建一个继承并覆盖其方法的类。sklearn.cluster.KMeans_transform

以下示例显示了与 Yolov2 论文的 IOU 距离。

import sklearn.cluster
import numpy as np

def anchor_iou(box_dims, centroid_box_dims):
    box_w, box_h = box_dims[..., 0], box_dims[..., 1]
    centroid_w, centroid_h = centroid_box_dims[..., 0], centroid_box_dims[..., 1]
    inter_w = np.minimum(box_w[..., np.newaxis], centroid_w[np.newaxis, ...])
    inter_h = np.minimum(box_h[..., np.newaxis], centroid_h[np.newaxis, ...])
    inter_area = inter_w * inter_h
    centroid_area = centroid_w * centroid_h
    box_area = box_w * box_h
    return inter_area / (
        centroid_area[np.newaxis, ...] + box_area[..., np.newaxis] - inter_area
    )

class IOUKMeans(sklearn.cluster.KMeans):
    def __init__(
        self,
        n_clusters=8,
        *,
        init="k-means++",
        n_init=10,
        max_iter=300,
        tol=1e-4,
        verbose=0,
        random_state=None,
        copy_x=True,
        algorithm="lloyd",
    ):
        super().__init__(
            n_clusters=n_clusters,
            init=init,
            n_init=n_init,
            max_iter=max_iter,
            tol=tol,
            verbose=verbose,
            random_state=random_state,
            copy_x=copy_x,
            algorithm=algorithm
        )

    def _transform(self, X):
        return anchor_iou(X, self.cluster_centers_)

rng = np.random.default_rng(12345)
num_boxes = 10
bboxes = rng.integers(low=0, high=100, size=(num_boxes, 2))

kmeans = IOUKMeans(num_clusters).fit(bboxes)

评论

1赞 Negative Correlation 11/2/2022
我想你的意思是写“kmeans = IOUKMeans(num_boxes).fit(bboxes)”?另外,您能否提供 jaccard 函数而不是anchor_iou,我认为它对像我这样的 n00bs 会更有帮助:)
1赞 Fady Samann 12/14/2022 #10

sklearn 库中的 Affinity 传播算法允许您传递相似性矩阵而不是样本。因此,您可以使用指标来计算相似性矩阵(而不是相异性矩阵),并通过将“affinity”项设置为“precomputed”将其传递给函数 https://scikit-learn.org/stable/modules/generated/sklearn.cluster.AffinityPropagation.html#sklearn.cluster.AffinityPropagation.fit。 就 K-Mean 而言,我认为这也是可能的,但我还没有尝试过。 然而,正如其他答案所述,使用不同的指标找到平均值将是问题所在。相反,您可以使用 PAM (K-Medoids) 算法来计算总偏差 (TD) 的变化,因此它不依赖于距离指标。https://python-kmedoids.readthedocs.io/en/latest/#fasterpam