决策树、线性和逻辑回归都是比较常用的机器学习算法,他们虽然有着不同的功能,但却都属于“有监督学习” 的⼀部分,即是说,模型在训练的时候,即需要特征矩阵X,也需要真实标签y。机器学习当中,还有相当⼀部分算法属于 “无监督学习” ,无监督的算法在训练的时候只需要特征矩阵X,不需要标签。无监督学习的代表算法有聚类算法、降维算法。
聚类算法又叫做“无监督分类”,其目的是将数据划分成有意义或有用的组(或簇)。这种划分可以基于我们的业务需求或建模需求来完成,也可以单纯地帮助我们探索数据的自然结构和分布。
比如在商业中,如果我们手头有大量的当前和潜在客户的信息,我们可以使用聚类将客户划分为若干组,以便进一步分析和开展营销活动,最有名的客户价值判断模型RFM(Recency FrequencyMonetary),就常常和聚类分析共同使用。再比如,聚类可以用于降维和矢量量化(vectorquantization),可以将高维特征压缩到一列当中,常常用于图像,声音,视频等非结构化数据,可以大幅度压缩数据量。
聚类算法是无监督类机器学习算法中最常用的⼀类,其目的是将数据划分成有意义或有用的组(也被称为簇)。这种划分可以基于我们的业务需求或建模需求来完成,也可以单纯地帮助我们探索数据的自然结构和分布。如果目标是划分成有意义的组,则簇应当捕获数据的自然结构。然而,在某种意义下,聚类分析只是解决其他问题(如数据汇总)的起点。无论是旨在理解还是应用,聚类分析都在广泛的领域扮演着重要角色。这些领域包括:心理学和其他社会科学、生物学、统计学、模式识别、信息检索、机器学习和数据挖掘。
聚类分析在许多实际问题上都有应用,下面是⼀些具体的例⼦,按聚类目的是为了理解数据⾃然结构还用于数据处理来组织。
关键概念:簇和质心
KMeans 算法将一组 N 个样本的特征矩阵 X 划分为 K 个无交集的簇,直观上来看是簇是一组一组聚集在一起的数据,在一个簇中的数据就认为是同一类。簇就是聚类的结果表现。
簇中所有数据的均值通常被称为这个簇的“质心”(centroids)。在一个二维平面中,一簇数据点的质心的横坐标就是这一簇数据点的横坐标的均值,质心的纵坐标就是这一簇数据点的纵坐标的均值。同理可推广至高维空间。
在 KMeans 算法中,簇的个数 K 是一个超参数,需要我们人为输入来确定。KMeans 的核心任务就是根据我们设定好的 K,找出 K 个最优的质心,并将离这些质心最近的数据分别分配到这些质心代表的簇中去。
具体过程可以总结如下:
当我们找到一个质心,在每次迭代中被分配到这个质心上的样本都是一致的,即每次新生成的簇都是一致的,所有的样本点都不会再从一个簇转移到另一个簇,质心就不会变化了。
这个过程在可以由下图来显示,我们规定,将数据分为4簇(K=4),其中白色X代表质心的位置:
在数据集下多次迭代(iteration),就会有:
第六次迭代之后,基本上质心的位置就不再改变了,生成的簇也变得稳定。此时我们的聚类就完成了,我们可以明显看出,K-Means按照数据的分布,将数据聚集成了我们规定的4类,接下来我们就可以按照我们的业务需求或者算法需求,对这四类数据进行不同的处理。
聚类算法聚出的类有什么含义呢?这些类有什么样的性质?我们认为,被分在同一个簇中的数据是有相似性的,而不同簇中的数据是不同的,当聚类完毕之后,我们就要分别去研究每个簇中的样本都有什么样的性质,从而根据业务需求制定不同的商业或者科技策略。
聚类算法的目的就是追求“簇内差异小,簇外差异大”。而这个“差异“,由样本点到其所在簇的质心的距离来衡量。
对于一个簇来说,所有样本点到质心的距离之和越小,我们就认为这个簇中的样本越相似,簇内差异就越小。而距离的衡量方法有多种,令:
◼ x 表示簇中的一个样本点;
◼ μ表示该簇中的质心;
◼ n 表示每个样本点中的特征数目;
◼ i 表示组成点 x 的每个特征编号;
则该样本点到质心的距离可以由以下距离来度量:
如我们采用欧几里得距离,则一个簇中所有样本点到质心的距离的平方和为:
◼ 其中,m 为一个簇中样本的个数;
◼ j 是每个样本的编号;
这个公式被称为簇内平方和(cluster Sum of Square),又叫做 Inertia。
而将一个数据集中的所有簇的簇内平方和相加,就得到了整体平方和(Total Cluster Sum ofSquare),又叫做total inertia:
Total Inertia 越小,代表着每个簇内样本越相似,聚类的效果就越好。
因此 KMeans 追求的是,求解能够让 Inertia 最小化的质心。
实际上,在质心不断变化不断迭代的过程中,总体平方和是越来越小的。当整体平方和最小的时候,质心就不再发生变化了。
大家可以发现,我们的 Inertia 是基于欧几里得距离的计算公式得来的。实际上,我们也可以使用其他距离,每个距离都有自己对应的 Inertia。在过去的经验中,我们总结出不同距离所对应的质心选择方法和 Inertia,在Kmeans 中,只要使用了正确的质心和距离组合,无论使用什么样的距离,都可以达到不错的聚类效果:
而这些组合,都可以由严格的数学证明来推导。在实际中我们往往都使用欧式距离,因此我们也无需去担忧这些距离所搭配的质心选择是如何得来的了。
现在我们尝试用手写 Python 代码来实现 Kmeans 算法。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# 解决坐标轴刻度负号乱码
plt.rcParams['axes.unicode_minus'] = False
# 解决中文乱码问题
plt.rcParams['font.sans-serif'] = ['Simhei']
此处先以经典的鸢尾花数据集为例,来帮助我们建模,数据存放在 iris.txt 中
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
#导入数据集
iris = pd.read_csv("iris.txt",header = None)
iris.head()
iris.shape
我们需要定义一个两个长度相等的数组之间欧式距离计算函数,在不直接应用计算距离计算结果,只比较距离远近的情况下,我们可以用距离平方和代替距离进行比较,化简开平方运算,从而减少函数计算量。此外需要说明的是,涉及到距离计算的,一定要注意量纲的统一。如果量纲不统一的话,模型极易偏向量纲大的那一方。此处选用鸢尾花数据集,基本不需要考虑量纲问题。
def distEclud(arrA, arrB):
d = arrA - arrB
dist = np.sum(np.power(d, 2), axis=1)
return dist
在定义随机质心生成函数时,首先需要计算每列数值的范围,然后从该范围中随机生成指定个数的质心。此处我们使用 numpy.random.uniform()函数生成随机质心。
def randCent(dataSet, k):
n = dataSet.shape[1] # n为列数,iris一共5列
data_min = dataSet.iloc[:, :n-1].min() # 前4列,每一列最小值
data_max = dataSet.iloc[:, :n-1].max() # 前4列,每一列最大值
data_cent = np.random.uniform(data_min,data_max,(k, n-1)) # 均匀分布中抽样,形状为(k, n-1)
return data_cent
验证上述定义函数,在 iris 中随机生成三个质心
iris_cent = randCent(iris, 3)
iris_cent
在执行 K-Means 的时候,需要不断的迭代质心,因此我们需要两个可迭代容器来完成该目标:
第一个容器用于存放和更新质心,该容器可考虑使用 list 来执行,list 不仅是可迭代对象,同时 list内不同元素索引位置也可用于标记和区分各质心,即各簇的编号;即代码中的 centroids。
第二个容器则需要记录、保存和更新各点到质心之间的距离,并能够方便对其进行比较,该容器考虑使用一个三列的数组来执行,其中:
函数功能:k-均值聚类算法
参数说明:
返回:
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent): # iris为150*5
m,n = dataSet.shape # m是行数(数据量),n是列数iris为150*5
# 下面生成的centroids,即第一个容器,后面用来存储最新更新的质心
centroids = createCent(dataSet, k) # centroids为3*4,用三个长度为4的一维数组记载3个质心
# 第一次centroids是随机生成的
# 这段生成的result_set,即第二个容器
# result_set结构: [数据集, 该行到最近质心的距离, 本次迭代中最近质心编号,上次迭代中最近质心编号]
clusterAssment = np.zeros((m,3)) # clusterAssment为150*3的数组
clusterAssment[:, 0] = np.inf # np.inf为无穷大
clusterAssment[:, 1: 3] = -1 # 此时clusterAssment为150*3
result_set = pd.concat([dataSet, pd.DataFrame(clusterAssment)],axis=1,ignore_index = True) # result_set为150*8
clusterChanged = True
while clusterChanged:
clusterChanged = False
for i in range(m): # 遍历result_set中每一行,一共m行
# 小心,下面的n为5,而resulit_set的列数已经变成8
dist = distMeas(dataSet.iloc[i, :n-1].values, centroids) # 第i行与三个质心的距离,dist为3*1
result_set.iloc[i, n] = dist.min() #
result_set[i,n]记录该行与3个质心的最小距离
result_set.iloc[i, n+1] = np.where(dist == dist.min())[0] #
result_set[i,n]记录最近质心的索引
clusterChanged = not (result_set.iloc[:, -1] ==
result_set.iloc[:,-2]).all()
# 只要result_set最后两列不完全相等,意味着本次for循环结束时,m行所有的新质心与上次while循环留下的不完全一样
# 后果:clusterChanged为True,while继续循环
# clusterChanged为True,则需要运行下面的if语句代码块,重置第一个容器centroids和第二个容器result_set
if clusterChanged:
cent_df = result_set.groupby(n+1).mean() # 按照列索引为n+1(质心索引)(第6列)进行分组求均值
# 即:按照最新的簇分类,计算最新3个质心的位置
centroids = cent_df.iloc[:,:n-1].values # 重置
centroids,用最新质心位置,替换上次的。3*4
result_set.iloc[:, -1] = result_set.iloc[:, -2] # result_set最后一列,本次的簇分类编码,替换掉上次的
return centroids, result_set
鸢尾花数据集带进去,查看模型运行效果:
iris_cent,iris_result = kMeans(iris, 3)
iris_cent
iris_result.head()
if not (result_set.iloc[:, -1] == result_set.iloc[:, -2]).all()
函数编写完成后,先以 testSet 数据集测试模型运行效果(为了可以直观看出聚类效果,此处采用
一个二维数据集进行验证)。testSet 数据集是一个二维数据集,每个观测值都只有两个特征,且数据之
间采用空格进行分隔,因此可采用 pd.read_table()函数进行读取。
testSet = pd.read_table('testSet.txt', header=None)
testSet.head()
testSet.shape
plt.scatter(testSet.iloc[:,0], testSet.iloc[:,1]);
可以大概看出数据大概分布在空间的四个角上,后续我们将对此进行验证。然后利用我们刚才编写的 K-Means 算法对其进行聚类,在执行算法之前需要添加一列虚拟标签列(算法是从倒数第二列开始计算特征值,因此这里需要人为增加多一列到最后)
label = pd.DataFrame(np.zeros(testSet.shape[0]).reshape(-1, 1))
test_set = pd.concat([testSet, label], axis=1, ignore_index = True)
test_set.head()
然后带入算法进行计算,根据二维平面坐标点的分布特征,我们可考虑设置四个质心,即将其分为四个簇,并简单查看运算结果:
test_cent, test_cluster = kMeans(test_set, 4)
test_cent
test_cluster.head()
将分类结果进行可视化展示,使用 scatter 函数绘制不同分类点不同颜色的散点图,同时将质心也放入同一张图中进行观察:
plt.scatter(test_cluster.iloc[:,0], test_cluster.iloc[:,1],c=test_cluster.iloc[:, -1])
plt.scatter(test_cent[:, 0], test_cent[:, 1], color='red',marker='x',s=100);
class sklearn.cluster.KMeans (n_clusters=8, init=’k-means++’, n_init=10, max_iter=300, tol=0.0001,
precompute_distances=’auto’, verbose=0, random_state=None,copy_x=True,
n_jobs=None,
algorithm=’auto’)
n_clusters 是 KMeans 中的 k,表示着我们告诉模型我们要分几类。这是 KMeans 当中唯一一个必填的参数,默认为 8 类,但通常我们的聚类结果会是一个小于 8 的结果。通常,在开始聚类之前,我们并不知道n_clusters 究竟是多少,因此我们要对它进行探索。
当我们拿到一个数据集,如果可能的话,我们希望能够通过绘图先观察一下这个数据集的数据分布,以此来为我们聚类时输入的 n_clusters 做一个参考。
首先,我们来自己创建一个数据集。这样的数据集是我们自己创建,所以是有标签的。
from sklearn.datasets import make_blobs
#自己创建数据集
X, y = make_blobs(n_samples=500,n_features=2,centers=4,random_state=1)
plt.scatter(X[:, 0], X[:, 1], marker='o')#点的形状 ,s=8 #点的大小
#查看分布
color = ["red","pink","orange","gray"]
for i in range(4):
plt.scatter(X[y==i, 0], X[y==i, 1]
,marker='o' #点的形状
,s=8 #点的大小
,c=color[i]
)
plt.show()
基于这个分布,我们来使用 Kmeans 进行聚类。首先,我们要猜测一下,这个数据中有几簇?
重要属性 labels_,查看聚好的类别,每个样本所对应的类
from sklearn.cluster import KMeans
n_clusters = 3
cluster = KMeans(n_clusters=n_clusters, random_state=0).fit(X)
y_pred = cluster.labels_
y_pred
array([0, 0, 2, 1, 2, 1, 2, 2, 2, 2, 0, 0, 2, 1, 2, 0, 2, 0, 1, 2, 2, 2,
2, 1, 2, 2, 1, 1, 2, 2, 0, 1, 2, 0, 2, 0, 2, 2, 0, 2, 2, 2, 1, 2,
2, 0, 2, 2, 1, 1, 1, 2, 2, 2, 0, 2, 2, 2, 2, 2, 1, 1, 2, 2, 1, 2,
0, 2, 2, 2, 0, 2, 2, 0, 2…])
fit_pre = KMeans(n_clusters=3, random_state=0).fit_predict(X)
(cluster.predict(X)==fit_pre).sum()
(fit_pre== cluster.labels_).sum()
500
500
我们什么时候需要 predict 呢? 当数据量太大的时候!
当我们数据量非常大的时候,为了提高模型学习效率,我们可以使用部分数据来帮助我们确认质心
剩下的数据的聚类结果,使用 predict 来调用
cluster_smallsub = KMeans(n_clusters=3, random_state=0).fit(X[:200])
sample_pred = cluster_smallsub.predict(X)
y_pred == sample_pred
array([False, False, True, False, True, False, True, True, True,
True, False, False, True, False, True, False, True, False,
False, True, True, True, True, False…])
但这样的结果,肯定与直接 fit 全部数据会不一致。有时候,当我们不要求那么精确,或者我们的数据量实在太大,那我们可以使用这样的方法。
查看质心
centroid = cluster.cluster_centers_
centroid
centroid.shape
查看总距离平方和
inertia = cluster.inertia_
inertia
1903.4503741659223
如果我们把猜测的簇数换成 4,Inertia 会怎么样?
n_clusters = 4
cluster_ = KMeans(n_clusters=n_clusters, random_state=0).fit(X)
inertia_ = cluster_.inertia_
inertia_
908.3855684760613
n_clusters = 5
cluster_ = KMeans(n_clusters=n_clusters, random_state=0).fit(X)
inertia_ = cluster_.inertia_
inertia_
811.0841324482415
n_clusters = 6
cluster_ = KMeans(n_clusters=n_clusters, random_state=0).fit(X)
inertia_ = cluster_.inertia_
inertia_
733.153835008308
不同于分类模型和回归,聚类算法的模型评估不是一件简单的事。在分类中,有直接结果(标签)的输出,并且分类的结果有正误之分,所以我们使用预测的准确度,混淆矩阵,ROC 曲线等等指标来进行评估,但无论如何评估,都是在”模型找到正确答案“的能力。而回归中,由于要拟合数据,我们有 SSE均方误差,有损失函数来衡量模型的拟合程度。但这些衡量指标都不能够使用于聚类。
面试高危问题:如何衡量聚类算法的效果?
聚类模型的结果不是某种标签输出,并且聚类的结果是不确定的,其优劣由业务需求或者算法需求来决定,并且没有永远的正确答案。那我们如何衡量聚类的效果呢?
记得我们说过,KMeans 的目标是确保“簇内差异小,簇外差异大”,我们就可以通过衡量簇内差异来衡量聚类的效果。我们刚才说过,Inertia 是用距离来衡量簇内差异的指标,因此,我们是否可以使用Inertia 来作为聚类的衡量指标呢?Inertia 越小模型越好。
可以,但是这个指标的缺点和极限太大。
那我们可以使用什么指标呢?聚类是没有标签,即不知道真实答案的预测算法,我们必须完全依赖评价簇内的稠密程度(簇内差异小)和簇间的离散程度(簇外差异大)来评估聚类的效果。其中轮廓系数是最常用的聚类算法的评价指标。它是对每个样本来定义的,它能够同时衡量:
1)样本与其自身所在的簇中的其他样本的相似度 a,等于样本与同一簇中所有其他点之间的平均距离
2)样本与其他簇中的样本的相似度 b,等于样本与下一个最近的簇中的所有点之间的平均距离
如果一个簇中的大多数样本具有比较高的轮廓系数,则簇会有较高的总轮廓系数,则整个数据集的平均轮廓系数越高,则聚类是合适的。
如果许多样本点具有低轮廓系数甚至负值,则聚类是不合适的,聚类的超参数 K 可能设定得太大或者太小。
在 sklearn 中,我们使用模块 metrics 中的类 silhouette_score 来计算轮廓系数,它返回的是一个数据集中,所有样本的轮廓系数的均值。但我们还有同在 metrics 模块中的 silhouette_sample,它的参数与轮廓系数一致,但返回的是数据集中每个样本自己的轮廓系数。我们来看看轮廓系数在我们自建的数据集上表现如何:
from sklearn.metrics import silhouette_score
from sklearn.metrics import silhouette_samples
X
y_pred
array([[-6.92324165e+00, -1.06695320e+01],
[-8.63062033e+00, -7.13940564e+00],
[-9.63048069e+00, -2.72044935e+00],
[-2.30647659e+00…]])
array([0, 0, 2, 1, 2, 1, 2, 2, 2, 2, 0, 0, 2, 1, 2, 0, 2, 0, 1, 2, 2, 2,
2, 1, 2, 2, 1, 1, 2, 2…])
观察一下不同的 K 下,轮廓系数发生什么变化?
#观察一下不同的K下,轮廓系数发生什么变化?
cluster = KMeans(n_clusters=3, random_state=0).fit(X)
silhouette_score(X,cluster_.labels_) #计算所有样本的轮廓系数均值。
silhouette_samples(X,cluster.labels_) #计算每个样本的轮廓系数。
array([ 3.32141517e-01, 5.02867308e-01, 5.69286243e-01, 8.26239736e-01,
3.52953508e-01, 7.40603896e-01, 6.92621728e-01, 5.90288043e-01,
4.17153302e-02, 7.35814660e-01…])
import pandas as pd
score=[]
for i in range(2,100):
cluster= KMeans(n_clusters=i, random_state=0).fit(X)
score.append(silhouette_score(X,cluster.labels_))
plt.plot(range(2,100),score)
plt.axvline(pd.DataFrame(score).idxmin()[0]+2,ls=':')
轮廓系数有很多优点,它在有限空间中取值,使得我们对模型的聚类效果有一个“参考”。并且,轮廓系数对数据的分布没有假设,因此在很多数据集上都表现良好。但它在每个簇的分割比较清洗时表现最好。但轮廓系数也有缺陷,比如基于密度进行的聚类,或通过 DBSCAN 获得的聚类结果,如果使用轮廓系数来衡量,则会表现出比真实聚类效果更高的分数。
我们通常会绘制轮廓系数分布图和聚类后的数据分布图来选择我们的最佳 n_clusters。
在 K-Means 中有一个重要的环节,就是放置初始质心。如果有足够的时间,K-means 一定会收敛,但 Inertia 可能收敛到局部最小值。是否能够收敛到真正的最小值很大程度上取决于质心的初始化。init 就是用来帮助我们决定初始化方式的参数。
初始质心放置的位置不同,聚类的结果很可能也会不一样,一个好的质心选择可以让 K-Means 避免更多的计算,让算法收敛稳定且更快。在之前讲解初始质心的放置时,我们是使用”随机“的方法在样本点中抽取 k 个样本作为初始质心,这种方法显然不符合”稳定且更快“的需求。为此,我们可以使用random_state参数来控制每次生成的初始质心都在相同位置,甚至可以画学习曲线来确定最优的random_state 是哪个整数。
一个 random_state 对应一个质心随机初始化的随机数种子。如果不指定随机数种子,则 sklearn中的K-means 并不会只选择一个随机模式扔出结果,而会在每个随机数种子下运行多次,并使用结果最好的一个随机数种子来作为初始质心。我们可以使用参数 n_init 来选择,每个随机数种子下运行的次数。这个参数不常用到,默认 10 次,如果我们希望运行的结果更加精确,那我们可以增加这个参数n_init 的值来增加每个随机数种子下运行的次数。
然而这种方法依然是基于随机性的。
为了优化选择初始质心的方法,2007年Arthur, David, and Sergei Vassilvitskii三人发表了论文“kmeans++: The advantages of careful seeding”,他们开发了”k-means ++“初始化方案,使得初始质心(通常)彼此远离,以此来引导出比随机初始化更可靠的结果。在 sklearn 中,我们使用参数 init ='kmeans ++'来选择使用 k-means ++作为质心初始化的方案。通常来说,建议保留默认的"kmeans++"的方法。
可输入"k-means++",“random"或者一个 n 维数组
◼ 初始化质心的方法,默认"k-means++”
◼ 输入"k-means++":一种为 K 均值聚类选择初始聚类中心的聪明的办法,以加速收敛
◼ 如果输入了 n 维数组,数组的形状应该是(n_clusters,n_features)并给出初始质心
cluster_01 = KMeans(n_clusters = 8,init='k-means++').fit(X)
cluster_01.n_iter_ # 输出运行的迭代次数
silhouette_score(X,cluster_01.labels_)
cluster_02 = KMeans(n_clusters = 8,init="random").fit(X)
cluster_02.n_iter_
silhouette_score(X,cluster_02.labels_)
8
0.32661746724551005
12
0.3425432424623584
整数,默认 10
◼ 使用不同的质心随机初始化的种子来运行 k-means 算法的次数。最终结果会是基于 Inertia 来计算的
◼ n_init 次连续运行后的最佳输出
cluster_01 = KMeans(n_clusters = 10,n_init=500).fit(X)
cluster_01.n_iter_ # 输出运行的迭代次数
silhouette_score(X,cluster_01.labels_)
cluster_02 = KMeans(n_clusters = 10,n_init=500,random_state=10).fit(X) # 每次运行结果将会一样
cluster_02.n_iter_
silhouette_score(X,cluster_02.labels_)
8
0.3280527630493156
7
0.328279610118647
在之前描述 K-Means 的基本流程时我们提到过,当质心不再移动,Kmeans 算法就会停下来。但在完全收敛之前,我们也可以使用 max_iter,最大迭代次数,或者 tol,两次迭代间 Inertia 下降的量,这两个参数来让迭代提前停下来。有时候,当我们的 n_clusters 选择不符合数据的自然分布,或者我们为了业务需求,必须要填入与数据的自然分布不合的 n_clusters,提前让迭代停下来反而能够提升模型的表现。
但是如果最佳簇数偏离太严重,max_iter 再大也不会有再高的轮廓系数
cluster_01 = KMeans(n_clusters = 10,n_init=500).fit(X)
cluster_01.n_iter_ # 输出运行的迭代次数
silhouette_score(X,cluster_01.labels_)
10
0.3253421339367222
cluster_01 = KMeans(n_clusters =8,init='k-means++',tol=1e-4).fit(X)
cluster_01.n_iter_ # 输出运行的迭代次数
silhouette_score(X,cluster_01.labels_)
2
0.3268230449212373