前言
K-Means是非常基础也是非常有效的一种聚类机器学习算法,即便过去数十年,它也有非常广泛的应用场景。从K-Means开始机器学习之路也是由于这算是我接触的第一个机器学习算法(自认为),由于我是自动化/控制专业的,其实很多机器学习方法我原来是没有接触过的,这也算是我的一个机器学习起点了,虽然讲的人很多啦,但是这个专栏的目的就是和大家一起学习啦,有什么说的不正确的地方还请大家批评指正!
聚类
什么是聚类?其实一开始学习中我并不太能理解聚类和分类,只是单纯从字面上理解。简单来说,聚类是一种无监督的学习方法,在没有人为影响(标签)的情况下,将一堆数据通过算法自行地根据一定规则(可以区分不同类型数据的一种特征),将具有相似特征的数据划分在一起,从而形成多个部分,我们称之为“簇”,譬如:
对于食物集合:x={黄瓜,香蕉,苹果,西瓜,西蓝花,芹菜,荠菜,韭菜,草莓}
我们知道这里面包含了不止一类事物,但有什么类我们不知道,假设其中包含两类,根据一定规则我们进行划分:
x1={香蕉,苹果,西瓜,草莓},x2={黄瓜,西蓝花,芹菜,荠菜,韭菜}
x1’={黄瓜,西蓝花,荠菜,韭菜,西瓜,香蕉},x2’={苹果,草莓}
如上我们进行了两次划分,x1和x2显然是根据它的从属划分成了水果和蔬菜,但这并非这些食物的唯一特征,因此x1’和x2’又根据食物的主要颜色进行了划分,也许乍一看很反直觉,但是还是有一定道理的,因为它们遵从了某种规则、捕获了某种特征。
这样对聚类就有了一个初步初步的概念了,接下来引入我们的主角——K-Means。
K-Means
算法名称可以拆成K和Means,K即我们设定划分的簇的数量,Means即这个算法中我们应用到了均值这个概念。在讨论这个算法前我们再看一个概念:
相似性度量
正如前文所说,我们划分簇的标准是这些数据具有相似特征,而相似究竟该如何度量呢?在K-Means中采用的是欧氏距离的方法,欧氏距离是非常经典的距离计算方式,也是相似性度量的常用指标。对于两个点
z
1
=
(
x
1
,
y
1
)
,
z
2
=
(
x
2
,
y
2
)
z_1=(x_1,y_1),z_2=(x_2,y_2)
z1=(x1,y1),z2=(x2,y2)
d
i
s
t
(
z
1
,
z
2
)
=
[
(
x
1
′
−
x
1
)
2
+
(
y
1
′
−
y
2
)
]
dist(z_1,z_2)=\sqrt{ \left[ (x_1'-x_1)^2+(y_1'-y_2)\right]}
dist(z1,z2)=[(x1′−x1)2+(y1′−y2)]
其实就是两点之间的直接距离,如果扩展到n维向量,则改写为:
d
i
s
t
(
z
1
,
z
2
)
=
∑
i
=
1
n
(
x
i
′
−
x
i
)
2
dist(z_1,z_2)=\sqrt{ \sum_{i=1}^n (x_i'-x_i)^2}
dist(z1,z2)=i=1∑n(xi′−xi)2
两个点之间的距离越小,说明两个点在空间中的距离更近,也就是我们所说的更相似,因此我们通过欧氏距离的大小来评估数据之间的相似程度。
当然,相似性度量并不限于欧氏距离,也包含很多其他的计算方式,如曼哈顿距离、余弦相似度、马氏距离(主成分分析处理后的欧氏距离)等等。
簇与质心
簇可以理解为一个集合,该集合内的所有数据都具有相似的特征,我们利用一定的方法把它们划分到了一起;
质心即簇的中心,这里的中心是通过对所有数据点各维度坐标平均后得到的,K-Means的质心即以一个圆(高维空间球体)为簇的圆心(球心)。
介绍完上述概念后我们就可以进入正题了,试想一下如果我们有一堆数据,想要让它们根据彼此之间距离大小来划分类别应该怎么做?
假设一维坐标上的点[1.1,1.2,1.5,1.9,2.1,2.3]
如果我们要将它们划为两个簇,那应该如何划分呢?
1.1,1.2,1.5看着比较近,划分为一个簇,1.9,2.1,2.3划分一个簇看着挺好,
可是等等等等,人乍一看可能会得到很接近的结果,但是机器不是啊,机器并不知道这些点之间的关系,因为还没有进行距离计算,那么我们该从何算起呢?
K-Means算法流程
K-Means需要根据我们的需求设定一个K值,即簇的数量,如上例子中我们设定的是K=2,其次就是机器学习中常见的——迭代次数,即我们需要经过多少次计算来完成本次任务,简单理解就是for循环的次数。
回到之前的问题,该从何算起呢?
首先需要在一开始选择K个点作为我们的初始质心;从这K个质心开始,我们计算其他点到K个质心的距离,对于一个点,将其归纳到距离最短的簇中,当对每个点分配完成后,我们就完成了一轮的计算,我们下一轮的任务就是用每一个簇中所有的点平均计算出该簇的新质心,然后再重复之前的过程直到迭代完毕(到迭代次数或分配不再改变)。
需要注意的是,其实初始的点不必是我们现有的点,但是用现有的点比较方便~
用一张流程图来表示这个过程:
这么看来,K-Means确实是个很简单的方法,在这个迭代更新的过程中也体现出了机器学习的思想,想起数字图像处理课老师讲到的:“机器学习和人学习的区别是,机器真的在学,人就不一定了”。但随着我对机器学习的学习,我的看法是,机器并没有在“学习”,但它确实在“学习”,一句很绕的话,希望未来我可以用一个更全面的方式阐述一下我的想法。
Maltab代码
data = [2,2,8,5,7,6,1,4;10,5,4,8,5,4,2,9]';
k = 3; %簇的数量
initial = {};
d = inf(1,k); %距离向量初始化为无穷大
figure;
initial{1} = [2,10]; %初始化三个质心
initial{2} = [5,8];
initial{3} = [2,5];
colors = {'bo','go','ro'};
for t = 1:10
clus1 = {}; %存放横坐标
clus2 = {}; %存放纵坐标
for s = 1:k
clus1{end+1} = [];
clus2{end+1} = [];
end
for i = 1:length(data)
for j = 1:k
d(j) = norm(initial{j}-data(i,:),2); %欧氏距离计算
end
[~,idx] = min(d); %取出该点最小值对应的索引,即质心索引
clus1{idx}(end+1) = data(i,1);
clus2{idx}(end+1) = data(i,2);
end
for q = 1:k
scatter(clus1{q},clus2{q},colors{q},'filled');hold on;
plot(initial{q}(1),initial{q}(2),colors{q},'LineWidth',2);
text(initial{q}(1), initial{q}(2), ['(', num2str(initial{q}(1)), ', ', num2str(initial{q}(2)), ')']);
grid on;
end
hold off;
for j = 1:k
temp1 = mean(clus1{j}); %平均计算
temp2 = mean(clus2{j}); %平均计算
initial{j} = [temp1,temp2]; %新的质心
end
for q = 1:k
scatter(clus1{q},clus2{q},colors{q},'filled');hold on;
plot(initial{q}(1),initial{q}(2),colors{q},'LineWidth',2);
text(initial{q}(1), initial{q}(2), ['(', num2str(initial{q}(1)), ', ', num2str(initial{q}(2)), ')']);
end
tt = ['第', num2str(t),'次迭代'];
title(tt)
end
根据上述代码我们可以得到如下结果:
经过三次迭代后,K-Means的结果就不再变化了,注意到我们得到的结果还是比较完美的,至少符合我们人一眼看过去的,但是不要忘记,我们有一个初始化质心的过程。
如果我修改初始的质心会怎么样呢?
initial{1} = [4,9];%更改第一个质心
initial{2} = [5,8];
initial{3} = [2,10];
这时的聚类结果就大不相同了,因此对于初始质心一定的情况,最终K-Means划分的结果是不变的,但是初始质心不同,最终得到的结果可能是不同的,会永远停留在一个局部最优中,因此初始质心的设定也是影响K-Means算法的重要因素。
Python代码
def dist(x, y):
return np.sqrt(np.sum((x - y) ** 2))
def kmeans(data, K, t_max):
clus_center = []
data_size = len(data)
for i in range(K):
idx = np.random.choice(data_size)
# 注意这里初始质心依靠的是随机选择,随机性地引入有助于我们获得更好的簇分类结果,这个方法也被称为K-Means++
clus_center.append(data[idx])
plt.figure()
for t in range(t_max):
clus = [[] for _ in range(K)]
clus1 = [[] for _ in range(K)]
clus2 = [[] for _ in range(K)]
for i in range(data_size):
d = [dist(np.array(clus_center[j]), np.array(data[i])) for j in range(K)]
idx = np.argmin(d)
clus[idx].append(data[i])
clus1[idx].append(data[i][0])
clus2[idx].append(data[i][1])
num = 0
for j in range(K):
temp = np.mean(clus[j],0)
if sum(clus_center[j]-temp)==0:
num += 1
clus_center[j] = temp
if num == K:
break
for j in range(K):
plt.scatter(clus1[j],clus2[j],marker='o')
plt.plot(clus_center[j][0],clus_center[j][1],'o',linewidth=2)
plt.text(clus_center[j][0],clus_center[j][1],f'({clus_center[j][0]}, {clus_center[j][1]})')
plt.grid(True)
plt.show(block=False)
plt.show()
分成横纵坐标两个列表是为了好画图,其实没必要,这个代码有冗余的地方,不过很好理解啦~
K-Means存在的问题
除了上述的初始质心选择问题,K-Means还存在一个关键的问题,即欧氏距离度量。
根据前文提到的,质心相当于簇圆/球的球心,这导致它只能按照球体表面到圆心距离这样的方式来划分簇,很多具有一些空间规律的数据无法被很好的划分,如:
左边一列为正确的聚类,右边为K-Means聚类的结果,显然相差非常大,因为对于二维数据来说,这样有嵌套、凹凸的形状不能简单的用两个圆形的簇去划分。有解决方法吗?当然也是有的,那就是核方法。
简单说说核方法
由于在原始维度往往存在一些线性不可分的非线性成分,因此我们难以通过基本的K-Means将数据正确的划分簇,因此学者们提出了一种方法,即利用非线性核函数将原始数据映射到高维空间中,使其在高维空间中可以被线性划分。
用一张图表示一下:
原始数据是一个圆环套着一个圆的形式,在原始维度上,用K-Means是肯定不能很好地划分出来的,但是通过核函数将其扩展至三维,这个时候靠近圆心的部分提升的少,而远离圆心的提升的多,这样我们可以利用一个平面将这两个簇很好的划分出来。当然,为了得到更好的效果,我们需要选择合适的核函数来进行升维处理。(上图数据+代码来自Steven L. Brunton和J. Nathan Kutz的《Data-driven science and engineering: machine learning, dynamical systems, and control》)
结尾
这是《机器学习之路》专栏的第一篇文章,还是希望能在这个过程中和大家分享、学习一些想法,有任何问题还请批评指正!