K-Means

K-均值聚类(K-Means Clustering)是最基础和最常用的聚类算法。它的基本思想是,通过迭代方式寻找 K 个簇(Cluster)的一种划分方案,使得聚类结果对应的代价函数最小。特别地,代价函数可以定义为各个样本距离所属簇中心点的平方误差和。

给定样本集 D = { x 1 , x 2 , ⋯   , x m } D = \{x_1, x_2, \cdots, x_m\} D={x1,x2,,xm},K-均值算法针对聚类所得的簇划分 C = { C 1 , C 2 , ⋯   , C k } C = \{C_1, C_2, \cdots, C_k\} C={C1,C2,,Ck} 进行最小化平方误差和。

  • 先求出每个簇的均值向量 μ i \mu_i μi ∣ C i ∣ |C_i| Ci 表示簇内的元素个数。
    μ i = 1 ∣ C i ∣ ∑ x ∈ C i x \mu_i = \frac{1}{|C_i|}\sum_{x \in C_i}x μi=Ci1xCix
  • 假设簇 C1 有三个向量 x1、x2 和 x3,分别为 (1, 2)、(2, 2) 以及 (2, 3),则簇 C1 的均值向量 μ 1 \mu_1 μ1 为:
    μ 1 = 1 3 ∑ x i ∈ C 1 x i = 1 3 ( ( 1 , 2 ) + ( 2 , 2 ) + ( 2 , 3 ) ) = ( 5 3 , 7 3 ) \mu_1 = \frac{1}{3}\sum_{x_i \in C_1}x_i = \frac{1}{3}((1, 2) + (2, 2) + (2, 3)) = (\frac{5}{3}, \frac{7}{3}) μ1=31xiC1xi=31((1,2)+(2,2)+(2,3))=(35,37)
  • 接着,求每个簇内的元素到簇均值向量的距离平方和。
    E = ∑ i = 1 k ∑ x ∈ C i ∣ ∣ x − μ i ∣ ∣ 2 2 ( 1 ) E = \sum_{i=1}^k\sum_{x \in C_i}||x - \mu_i||_2^2 \quad (1) E=i=1kxCixμi22(1)
    E 刻画了簇内样本围绕簇均值向量的紧密程度,E 值越小则簇内样本相似度越高。

【难点】:最小化平方误差并不容易,找到最优解需考察样本集 D 所有可能的簇划分,这是一个 NP 难问题。见下图,左图为初始化后的三个簇中心,右图是迭代结束后的三个簇划分。初始簇的选择会极大影响最终的簇划分,很显然下图初始簇的选择使得优化过程陷入局部最优解,而非全局最优解。

kmeans.jpg

关于初始值的选择放到后面再讲,先介绍 K-Means 算法的描述与实现过程。

【算法】:K-Means 算法采用了贪心策略,通过迭代优化来近似求解式子(1)。

  • 输入:样本集 D = { x 1 , x 2 , ⋯   , x n } D = \{x_1, x_2, \cdots, x_n\} D={x1,x2,,xn};聚类簇数 k。
  • 输出:簇划分 C = { C 1 , C 2 , ⋯   , C k } C = \{C_1, C_2, \cdots, C_k\} C={C1,C2,,Ck}
  • 过程:
  1. 数据预处理,如归一化、离群点处理等。
  2. 从样本集 D 中随机挑选 K 个样本作为初始簇均值向量;
  3. 计算每个样本与各簇均值向量的距离,并根据距离确定当前样本所属的簇;
  4. 依据划分后的簇重新计算簇均值向量,并判断簇均值向量是否发生变化,若发生变化则更新,否则保持不变;
  5. 重复(2)、(3)步骤,直到所有簇均值向量均未更新。

代码实现

【实现准备】:

  • 所需包:numpy

【随机挑选初始均值向量函数】:随机确定起始位置,然后根据样本集长度和挑选数量计算出步长。这么做可以避免挑选出重复值,以及尽可能地从样本集各个区域挑选数据,而不是聚集在某一区域。

def random_select(dataset, count):
    # 获取样本集长度
    length = dataset.shape[0]
    # 计算步长和初始位置
    step, start = length // count, np.random.randint(length)
    data_select = []
    # 按照起始位置和步长挑选数据
    for i in range(count):        
        data_select.append(dataset[start])
        start += step
        start = start % length
    return data_select

【实现 K-means 函数】:

  • 函数定义
def k_mean(dataset, k):
  • 获取样本集长度和初始化所需变量
length = dataset.shape[0]
dataset_cluster_info = np.array(np.zeros((length, 2)))
flag = True
  • flag:用作循环判断的标志;
  • dataset_cluster_info:创建一个二维数组,行数等于样本集长度,对应样本集数据。列数为二,第一列为当前样本所属的簇,第二列为当前样本距离所属簇的均值向量的值。
  • 随机从样本集中挑选 k 个样本向量作为簇的均值向量
cluster_vectors = random_select(dataset, k)
  • 进入循环,每次清空簇信息并将 flag 变量设置为 False
while flag:
    cluster = []
    flag = False
    # ...
return cluster
  • 循环每一个样本,将其划分到相应的簇
for i in range(length):
    min_dist = np.inf
    cluster_index = -1
    for index in range(k):
        dist = np.linalg.norm(dataset[i] - cluster_vectors[index])
        if dist < min_dist:
            min_dist = dist
            cluster_index = index
    dataset_cluster_info[i] = (cluster_index, min_dist)
  • 更新每个簇的均值向量
for i in range(k):
    cluster_data = dataset[dataset_cluster_info[:, 0] == i]
    cluster.append(cluster_data)
    cluster_new = np.mean(cluster_data, axis=0)
    if (cluster_new != cluster_vectors[i]).all():
        flag = True
        cluster_vectors[i] = cluster_new

【完整代码】:

def k_mean(dataset, k):
    # 初始化
    length = dataset.shape[0]
    # 随机从样本集中挑选 k 个样本向量作为簇的均值向量
    cluster_vectors = random_select(dataset, k)
    dataset_cluster_info = np.array(np.zeros((length, 2)))
    flag = True
    
    while flag:
        cluster = []
        flag = False
        # 循环每一个样本,将其划分到相应的簇内
        for i in range(length):
            min_dist = np.inf
            cluster_index = -1
            for index in range(k):
                dist = np.linalg.norm(dataset[i] - cluster_vectors[index])
                if dist < min_dist:
                    min_dist = dist
                    cluster_index = index
            dataset_cluster_info[i] = (cluster_index, min_dist)

        # 更新每个簇的均值向量
        for i in range(k):
            cluster_data = dataset[dataset_cluster_info[:, 0] == i]
            cluster.append(cluster_data)
            cluster_new = np.mean(cluster_data, axis=0)
            if (cluster_new != cluster_vectors[i]).all():
                flag = True
                cluster_vectors[i] = cluster_new
    return cluster

上述代码的终止条件是当每个簇的均值向量都不发生变化。但随着数据量的增加以及数据复杂性的提高,要达到终止条件或许不是一件容易的事情。为避免运行时间过长,通常要设置一个最大运行轮数最小调整幅度阈值。若达到最大轮数或调整幅度小于阈值,则停止运行。

我们再对代码进行改进,允许使用者自动选择代码的终止条件。

【改进代码】:

class KMeans:
    
    def __init__(self, k, stop_criterion=True, max_iter=500, min_threshold=1):
        self.k = k
        self.stop_criterion = stop_criterion
        self.max_iter = max_iter
        self.iter = 0
        self.min_threshold = min_threshold
        self.data_cluster_info = None
    
    def _random_select(self, data, k):
        length = data.shape[0]
        step, start = length // k, numpy.random.randint(length)
        data_select = []
        for i in range(k):        
            data_select.append(data[start])
            start += step
            start = start % length
        return data_select
    
    def fit(self, data):
        # 初始化
        length = data.shape[0]
        # 随机从样本集中挑选 k 个样本向量作为簇的均值向量
        cluster_vectors = self._random_select(data, self.k)
        self.data_cluster_info = numpy.array(numpy.zeros((length, 2)))
        flag = True
        self.iter = 0

        while self._stop_criterion():
            cluster = []
            flag = False
            # 循环每一个样本,将其划分到相应的簇内
            for i in range(length):
                min_dist = numpy.inf
                cluster_index = -1
                for index in range(self.k):
                    dist = numpy.linalg.norm(data[i] - cluster_vectors[index], 2)**2
                    if dist < min_dist:
                        min_dist = dist
                        cluster_index = index
                self.data_cluster_info[i] = (cluster_index, min_dist)

            # 更新每个簇的均值向量
            for i in range(self.k):
                cluster_data = data[self.data_cluster_info[:, 0] == i]
                cluster.append(cluster_data)
                cluster_new = numpy.mean(cluster_data, axis=0)
                if (cluster_new != cluster_vectors[i]).all():
                    flag = True
                    cluster_vectors[i] = cluster_new
            
            self.iter += 1
            if not flag:
                break
        return cluster
    
    def _stop_criterion(self):
        # 最大运行轮数
        if self.stop_criterion:
            return True if self.iter <= self.max_iter else False
        # 最小调整幅度阈值
        else:
            if self.iter == 0:
                error = numpy.infty
            else:
                error = numpy.sum(self.data_cluster_info[1], axis=0)
            print(error)
            return True if error > self.min_threshold else False

优缺点

【优点】:对于大数据集,K-Means 算法相对是可伸缩和高效的,它的计算复杂度是 O(NKt) 接近于线性,其中 N 是样本的数目,K 是聚类的簇数,t 是迭代的轮数。尽管算法经常以局部最优解结束,但一般情况下达到的局部最优已经可以满足聚类的需求。

【缺点】:

  • 受初始簇和离群点的影响,每次的结果都不稳定(可能会不相同)、结果通常不是全局最优而是局部最优解;
  • 无法很好地解决数据簇分布差别较大的情况,例如某一类样本是另一类样本数量的 100 倍。
  • 不太适用于离散分类。
  • K 值难以确定。
  • 很难发现任意形状的簇。

算法优化

K-Means 算法的优化主要可分为初始点(簇均值向量)的选择以及 K 值的选择。

初始点的选择

初始点的选择对最后的聚类结果和运行时间都有很大的影响,如果仅仅是随机的选择,有可能导致算法收敛很慢。

【解决方案】:K-Means++。

  1. 从输入的数据集中随机选择一个点作为第一个聚类的簇均值向量。
  2. 对于数据集中的每一个点 x i x_i xi,计算该点与已选择的簇均值向量中最近的距离。
    D ( x i ) = a r g m i n ∣ ∣ x i − μ r ∣ ∣ 2 2 r = 1 , 2 , ⋯ &ThinSpace; , k s e l e c t e d D(x_i) = argmin||x_i - \mu_r||_2^2 \quad r = 1,2,\cdots,k_{selected} D(xi)=argminxiμr22r=1,2,,kselected
  3. 选择 D(x) 最大的数据点作为新的簇均值向量。
  4. 重复(2)和(3)步,直到选择出 K 个簇均值向量。

K 值的选择

K 值选择的方法主要有以下几种:

  • 按需选择
  • 观察法
  • 拍脑袋法
  • 肘部法则
  • 其余方法后续更新
按需选择

按照业务的需求和目的来选择聚类的个数(K 的值)。一般来说,聚类的结果通常会用于下游业务,因此聚类簇个数的设置会根据下游任务的效果进行评估。

【例如】:一个游戏公司想把所有玩家做聚类分析,分成顶级、高级、中级、菜鸟四类,那么 K 可以设置为 4;一家房地产公司想把当地的商品房分成高、中、低三档,那么 K 可以设置为 3。

【缺点】:按需选择虽然合理,但是未必能保证在做 K-Means 时能够得到清晰的分界线,从而将数据集很好地划分为 K 个簇。

观察法

通过数据可视化的方式呈现数据,然后用肉眼去观察数据集的分布情况,从而决定 K 值。

肉眼观察法.png

【缺点】:适用于低维度的数据集,当数据集的维数大于 3 维时,无法使用数据可视化的方式呈现数据,也就无法用肉眼去观察。当然也可以先进行降维操作,将维度降低到二维或者三维,然后再进行观察。但能不能降到二维和三维也是一个问题。

拍脑袋法

K 近似于样本数除以 2 后开方。
K ≈ N 2 K \approx \sqrt{\frac{N}{2}} K2N
例如,现在有 100 个样本,我们可以将 K 设置为:
K ≈ 100 2 ≈ 7 K \approx \sqrt{\frac{100}{2}} \approx 7 K2100 7

肘部法则(Elbow Method)

肘部法则适用于 K 值相对较小的情况。随着 K 的增大,样本划分会更加精细,每个簇的聚合程度会逐渐提高,那么误差平方和自然会逐渐变小。

  • 当选择的 K 值小于真正的 K 值时,K 每增加 1,误差平方和的下降幅度会很大;
  • 当选择的 K 值大于真正的 K 值时,K 每增加 1,误差平方和的下降幅度会骤减而趋于平缓。

基于上述考量,正确的 K 值就会在这个转折点。观察下图可以很明显地看出,曲线的形状类似人的肘部,故而取名为肘部法则。

Elbow Method.jpg

但并不是所有的问题都可以通过画肘部图(K 与 cost function 的关系曲线图)来解决。肘点位置不明显,这时就无法确定 K 值了。

【Python 代码实现】:

from sklearn.cluster import KMeans
from scipy.spatial.distance import cdist

x1 = np.array([3, 1, 1, 2, 1, 6, 6, 6, 5, 6, 7, 8, 9, 8, 9, 9, 8])
x2 = np.array([5, 4, 5, 6, 5, 8, 6, 7, 6, 7, 1, 2, 1, 2, 3, 2, 3])

plt.plot()
plt.xlim([0, 10])
plt.ylim([0, 10])
plt.title('Dataset')
plt.scatter(x1, x2)
plt.show()

# create new plot and data
plt.plot()
X = np.array(list(zip(x1, x2))).reshape(len(x1), 2)
colors = ['b', 'g', 'r']
markers = ['o', 'v', 's']

# k means determine k
distortions = []
K = range(1, 10)
for k in K:
    kmeanModel = KMeans(n_clusters=k).fit(X)
    kmeanModel.fit(X)
    distortions.append(sum(np.min(cdist(X, kmeanModel.cluster_centers_, 'euclidean'), axis=1)) / X.shape[0])

# Plot the elbow
plt.plot(K, distortions, 'bx-')
plt.xlabel('k')
plt.ylabel('Distortion')
plt.title('The Elbow Method showing the optimal k')
plt.show()

【语法】:scipy.spatial.distance.cdist

cdist(XA, XB, metric='euclidean')
  • XA:ndarray,参与距离计算的向量 A;
  • XB:ndarray,参与距离计算的向量 B;
  • metric:str or callable,可选。距离度量方法,默认为欧氏距离。
  • 返回值:ndarray。

【总结】:Elbow Method 是可以可以尝试的一种方法,但不适用于所有问题。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值