机器学习-聚类(k-means)笔记
image-20221108151151564

无监督学习的基本想法是对给定数据进行某种压缩,从而找到数据的潜在结构。(横向压缩、纵向压缩)

比如说对于一个训练数据 Xm×nX_{m\times n}

Xm×n=[x11x12x1nx21x22x2nxm1xm2xmn]X_{m\times n}= \begin{bmatrix} x_{11}& x_{12}& \cdots & x_{1n} \\ x_{21}& x_{22}& \cdots & x_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ x_{m1}& x_{m2}& \cdots & x_{mn} \end{bmatrix}

训练数据由n个样本组成,每一个样本是一个M维的向量。每一行对应一个特征,每一列对应一个样本。

  • 横向压缩(压缩特征):降维
  • 纵向压缩(压缩样本):聚类
  • 横向纵向:概率模型估计

k-means

K均值聚类:基于距离的聚类算法,采用距离作为相似性的评价指标,认为两个对象的距离越小,其相似度就越大。

k-means 的目标函数

首先,采用欧式距离平方,作为样本之间的距离 d(xi,xj)d({x_i},{x_j})

簇内任意一点到簇中心的距离:

d(x,μk)2=xμk2=(xμk)T(xμk){d}\left(\boldsymbol{x}, \boldsymbol{\mu}_{k}\right)^{2}=\left\|\boldsymbol{x}-\boldsymbol{\mu}_{k}\right\|^{2}=\left(\boldsymbol{x}-\boldsymbol{\mu}_{k}\right)^{\mathrm{T}}\left(\boldsymbol{x}-\boldsymbol{\mu}_{k}\right)

x=[x1x2xD]T,μk=[μk,1μk,2μk,D]T\boldsymbol{x}=\left[\begin{array}{llll} x_{1} & x_{2} & \cdots & x_{D} \end{array}\right]^{\mathrm{T}}, \quad \boldsymbol{\mu}_{k}=\left[\begin{array}{llll} \mu_{k, 1} & \mu_{k, 2} & \cdots & \mu_{k, D} \end{array}\right]^{\mathrm{T}}

簇内距离平方和相当于残差平方和 (Sum of Squared Error, SSE),SSE可以度量样本数据的聚集程度。

SSE(CK)=xCkxμk2SSE(C_K)=\sum_{\boldsymbol{x} \in C_{k}}\left\|\boldsymbol{x}-\boldsymbol{\mu}_{k}\right\|^{2}

然后,定义样本与其所属类的簇中心之间的距离总和为损失函数,即:

W(C)=k=1KxCkxμk2W(C)=\sum_{k=1}^{K} \sum_{\boldsymbol{x} \in C_{k}}\left\|\boldsymbol{x}-\boldsymbol{\mu}_{k}\right\|^{2}

W(C)W(C)越小,表示相同类别中样本的相似程度越高。

因此,k均值聚类的优化问题为:

argminCW(C)\underset{C}{\arg \min } W(C)

argminC,μk=1KxCkxμk2\underset{C, \boldsymbol{\mu}}{\arg \min } \sum_{k=1}^{K} \sum_{\boldsymbol{x} \in C_{k}}\left\|\boldsymbol{x}-\boldsymbol{\mu}_{k}\right\|^{2}

  • x\boldsymbol{x} 是无标记数据(列向量){x1,x2,,xn}\left\{x_{1}, x_{2}, \ldots, x_{n}\right\}

  • C={C1,C2,,CK}C=\left\{C_{1}, C_{2}, \ldots, C_{K}\right\}

  • μk\boldsymbol{\mu}_{k} 是任意一处 CKC_{K} 样本数据的簇中心。

  • 一共 KK 个簇

  • xCkxμk2\sum_{\boldsymbol{x} \in C_{k}}\left\|\boldsymbol{x}-\boldsymbol{\mu}_{k}\right\|^{2} 是描述簇内样本到簇中心的平方和距离最小,即要求簇内的样本是紧密的。

在这个目标函数中,有两个变量

{μkC\left\{\begin{matrix}簇中心:\boldsymbol{\mu}_{k} \\聚类结果:C \end{matrix}\right.


换一种符号进行表达:

argmin{μi,mji}i=1Kj=1nmjixjμi2mji={1, 若 xjCi0, 若 xjCi\underset{\left\{\mu_{i}, m_{j i}\right\}}{\arg \min } \sum_{i=1}^{K} \sum_{j=1}^{n} m_{j i}\left\|x_{j}-\mu_{i}\right\|^{2} \\ \\ m_{j i}=\left\{\begin{array}{l} 1, \text { 若 } \mathrm{x}_{j} \in C_{i} \\ 0, \text { 若 } \mathrm{x}_{j} \notin C_{i} \end{array}\right.

这个 mjim_{j i} 矩阵是M(n×K)M(n \times K)

M=[100010100]}C1,C2,C3...,CKx1x2...xnM = \underbrace{\left . \begin{bmatrix} 1 & 0 & \cdots & 0 \\ 0 & 1 & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 1 & 0 & \cdots & 0 \end{bmatrix} \right \}}_{\text C_1, C_2, C_3...,C_K} \begin{matrix} \\x_1 \\x_2 \\... \\x_n \\ \\ \end{matrix}

每一行只有一个元素为1,其他都是0。因为每一个样本只能归为一个簇(硬分类)。

在这个目标函数中,有两个变量:

{μkmij\left\{\begin{matrix}簇中心:\boldsymbol{\mu}_{k} \\聚类结果:m_{ij} \end{matrix}\right.


对于这样的有两组变量的优化问题,是一个NP难的问题。所以实际中,我们采用交替优化的方法进行求解。(固定一个参数,优化另一个,一直这样迭代)

交替迭代优化

分为两步:

  1. 更新簇成员:选择k个类的簇中心,将样本分类到离他最近的簇中心里。(得到聚类结果)(固定μk\boldsymbol{\mu}_{k}优化CC
  2. 更新簇中心:计算每个类的样本均值作为新的簇中心。(得到μk\boldsymbol{\mu}_{k})(固定CC优化μk\boldsymbol{\mu}_{k}

更新簇成员

for j=1,2,...,m:xjμkd(xj,μk)2=xjμk2 λj=argmini1,2,...,K d(xj,μk)2Cλj=Cλjxj\begin{aligned} \boldsymbol{for}& \ j = 1,2,...,m: \\&计算样本x_j与各个簇中心 \boldsymbol{\mu}_{k} 之间的距离:{d}\left(\boldsymbol{x_j}, \boldsymbol{\mu}_{k}\right)^{2}=\left\|\boldsymbol{x_j}-\boldsymbol{\mu}_{k}\right\|^{2} \\&根据距离最近,确定簇标记\ \lambda_j = \underset{i\in{1,2,...,K}}{\arg \min} \ {d}\left(\boldsymbol{x_j}, \boldsymbol{\mu}_{k}\right)^{2} \\&将样本划入相应的簇:C_{\lambda_j}=C_{\lambda_j}\cup{\boldsymbol{x_j}} \end{aligned}

这一步的计算量最大,复杂度 O(Knp)O(Knp) :K 个类别,n个样本,每个样本是p个维度

更新簇中心

固定簇成员,对于每个类别,这个时候 i 是固定的。

argminμij=1nmjixjμi2\underset{\mu_{i}}{\arg \min }\sum_{j=1}^{n} m_{j i}\left\|x_{j}-\mu_{i}\right\|^{2}

找到使SSE最小的 μi\mu_{i}

  • 求导使得梯度为0,然后求解出μi\mu_{i}

j=1nmji(xjμi)=0μi=j=1nmjixjj=1nmji=1CixjCixj\sum_{j=1}^{n} m_{j i}(x_{j}-\mu_{i})=0 \\ \downarrow \\ \mu_{i}=\frac{\sum_{j=1}^{n} m_{j i}x_{j}}{\sum_{j=1}^{n} m_{j i}} =\frac{1}{|C_i|}\sum_{x_j \in C_i}{x_j}

j=1nmji\sum_{j=1}^{n} m_{j i} 其实就是M矩阵中行相加,表示的是每个类别中的样本个数 Ci|C_i|

for i=1,2,...,K:: μi=1CixjCixjif μiμi: : μi=μielse μiμi: \begin{aligned} \boldsymbol{for}& \ i = 1,2,...,K: \\ & 新的簇中心:\ \mu'_{i}=\frac{1}{|C_i|}\sum_{x_j \in C_i}{x_j} \\ & \boldsymbol{if}\ \mu'_{i} \ne \mu_{i}:\ 更新簇中心:\ \mu_{i}=\mu'_{i} \\& \boldsymbol{else}\ \mu'_{i} \ne \mu_{i}:\ 保持当前的簇中心不变 \\ \end{aligned}

迭代过程

西瓜书 https://github.com/Visualize-ML/Book7_Machine-Learning

算法步骤

西瓜书

能不能顺序反一下?一开始初始化 C 而不是 μ\mu 呢?

可以,但更复杂,也没必要。

算法复杂度

  • 计算两个 p-维向量之间的欧式距离的复杂度为 O(p)O(p)
  • 更新簇成员:计算所有点到每个簇中线间的距离,复杂度 O(Knp)O(Knp)
  • 更新簇中心:计算每个簇的簇中心,μi=1CixjCixj\mu'_{i}=\frac{1}{|C_i|}\sum_{x_j \in C_i}{x_j},复杂度 O(np)O(np)
  • 假设迭代次数 ll 步算法收敛,总的计算复杂度为 O(lKnp)O(l Knp)

k-means 的一些技巧

聚类中心的初值

有些初值会导致比较差的结果。

解决办法:

  • 使用启发式方法选择比较好的初值,(例如初值点之间有比较大的距离)、
  • 尝试使用多个初值 选择SSE最小的那一组。

聚类数目k

手肘法:

https://github.com/Visualize-ML/Book7_Machine-Learning

当K 小于 “合适”聚类数时,K 增大时,会导致SSE 大幅下降;但是,K 大于“合适”聚类数时,K 再增大,SSE 下降幅度不断变缓。

所以选择手肘上的 K。(上面这个图中可以选择2,3,4)

但是实际中,情况可能会更加复杂。

局限性

由于 k-means采用距离作为度量,但某些情况下,距离度量不适合划分数据。

image-20221108143221636

k-means 的延申

层次k-means:

Scalable Recognition with a Vocabulary  tree, CVPR 2006.

一开始先分成10类,然后每类再分成10类,这样就能迅速达到100类的划分。适合大规模的聚类,每个样本比对的数目可以快速的降低。但是也有问题,前面划分错了,后面就对不回来了。

Nister D, Stewenius H. Scalable recognition with a vocabulary tree[C]//2006 IEEE Computer Society Conference on Computer Vision and Pattern Recognition (CVPR’06). Ieee, 2006, 2: 2161-2168.

kd树

在样本空间上建立一个kd树,利用二分法划分数据。对一个样本计算和簇中心之间的距离的话,就可以在一个小的范围内找,不需要所有簇中心都遍历一遍。

原来是每一个样本 x 要和 K 个簇中心比对。现在是和很少的一部分簇中心比对。也可以做一个大规模的聚类。

Philbin, James, et al. “Object retrieval with large vocabularies and fast spatial matching.” 2007 IEEE conference on computer vision and pattern recognition. IEEE, 2007.

乘积量化

乘积量化 : Product Quantization for Nearest Neighbor Search, PAMI 2011

k-means 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class Kmeans():
def __init__(self, data, kc, slicesize = 50000):
"""
:param data: shape: (5000404,128) (样本,属性值)
:param kc: 中心数量
"""
self.slicesize = slicesize
self.iter_MAX = 10
self.data = data # (5000404,128)
self.kc = kc
self.n = data.shape[0] # 5000404
self.d = data.shape[1] # 128
self.cluster_centers = self.init_cluster_centers()

def run(self):
"""
迭代循环
:return: cluster_centers.shape: (kc, 128)
"""
# 初始化聚类中心
cluster_centers = self.cluster_centers
result_class = np.zeros(self.n)
count = 0
while (count <= self.iter_MAX):
# 根据聚类中心进行分类
class_index = self.classify(cluster_centers)
# 如果分类结果一样,就终止循环
if (result_class == class_index).all():
break
# 更新分类结果
result_class[:] = class_index
# 按照新的分类结果更新聚类中心
cluster_centers = self.new_centers(class_index)
# print(count)
count += 1
self.cluster_centers = cluster_centers
return cluster_centers

def init_cluster_centers(self):
"""
初始化聚类中心,随机选取K个数据点作为簇中心。
:return: cluster_centers.shape: (kc, 128)
"""
centers = self.data[np.random.choice(np.arange(self.n), self.kc, replace=False)]
return centers

def classify(self, cluster_centers = None):
"""
根据聚类中心,将样本进行归簇。
:param cluster_centers:
:return: shape: (n, )
"""
if (cluster_centers is None): cluster_centers = self.cluster_centers

index_list = []
for i in range(0, self.n, self.slicesize):
endi = min(i + self.slicesize, self.n)
# 50000, kc, 128
X = np.repeat(self.data[i: endi, :], self.kc, axis=0).reshape((endi-i, self.kc, self.d))
C = np.tile(np.expand_dims(cluster_centers, axis = 0), (X.shape[0],1,1))
# 距离
distance = np.sum(np.square(X-C), axis=-1)
# 最小距离的索引值
index_list.append(np.argmin(distance, axis=1))

return np.concatenate(index_list, axis=0)

def new_centers(self, indexs):
"""
重新计算每个簇的均值,作为新的簇中心。
:param indexs:
:return:
"""
class_sum_list = [[] for _ in range(self.kc)]
for i in range(0, self.n, self.slicesize):
endi = min(i + self.slicesize, self.n)
for k in range(self.kc):
X = self.data[i: endi, :]
samples = X[indexs[i: endi]==k]
class_sum_list[k].append(np.mean(samples, axis=0))

return np.mean(class_sum_list, axis=1)

samples = X[indexs[i: endi]==k]

reference