目录
[层次聚类 (文档聚类).ipynb](#层次聚类 (文档聚类).ipynb)
[1. 密度聚类原理](#1. 密度聚类原理)
[2. DBSCAN密度定义](#2. DBSCAN密度定义)
[3. DBSCAN密度聚类思想](#3. DBSCAN密度聚类思想)
[4. DBSCAN聚类算法](#4. DBSCAN聚类算法)
[5. DBSCAN小结](#5. DBSCAN小结)
[1. BIRCH概述](#1. BIRCH概述)
[2. 聚类特征CF与聚类特征树CF Tree](#2. 聚类特征CF与聚类特征树CF Tree)
[3. 聚类特征树CF Tree的生成](#3. 聚类特征树CF Tree的生成)
[4. BIRCH算法](#4. BIRCH算法)
[5. BIRCH算法小结](#5. BIRCH算法小结)

上一篇我们学习了层次聚类。层次聚类只是迭代的把最相近的两个聚类匹配起来。
并没有给出能给出多少的分组。
今天我们来研究一个K均值聚类。就是给定分组数目的基础上再来聚类。即将所有的样本数据集分成K个组,每个组内尽可能相似,每个组间又尽可能不相似。
k均值聚类和k中心点聚类属于基于划分的聚类。
也就是给定了k值(簇的个数),我们通过不停的设计每个对象该属于哪个簇而实现聚类。
我们今天使用k均值为文档进行分组。
K均值聚类
K-means算法是很典型的基于距离的聚类算法,采用距离作为相似性的评价指标,即认为两个对象的距离越近,其相似度就越大。
该算法认为簇是由距离靠近的对象组成的,因此把得到紧凑且独立的簇作为最终目标。算法采用误差平方和准则函数作为聚类准则函数。
算法过程如下:
1)从N个文档随机选取K个文档作为质心
2)对剩余的每个文档测量其到每个质心的距离,并把它归到最近的质心的类
3)重新计算已经得到的各个类的质心
4)迭代2~3步直至新的质心与原质心相等或小于指定阈值,算法结束
具体如下:
输入:k, data; 其中data为n个样本集
(1) 随机选择k个初始中心点,例如c0=data0,...ck-1=datak-1;
(2) 对于data0....datan,分别与c0...ck-1比较,假定与ci差值最少,就标记为i;
(3) 对于所有标记为i点,重新计算ci={ 所有标记为i的dataj之和}/标记为i的个数;
(4) 重复(2)(3),直到所有ci值的变化小于给定阈值。

案例实现
加载数据集
使用我们一贯的文本数据集结构。每行为一个文档(链接或者文档名),每列为一个文档特征(单词),每个单元格的取值为单词在文档中出现的次数。
博客数据集点击下载(http://luanpeng.oss-cn-qingdao.aliyuncs.com/csdn/python/聚类/blogdata.txt)
** **代码** **
!wget http://luanpeng.oss-cn-qingdao.aliyuncs.com/csdn/python/%E8%81%9A%E7%B1%BB/blogdata.txt
** **输出** **
--2022-09-20 23:56:00-- http://luanpeng.oss-cn-qingdao.aliyuncs.com/csdn/python/聚类/blogdata.txt
Resolving luanpeng.oss-cn-qingdao.aliyuncs.com (luanpeng.oss-cn-qingdao.aliyuncs.com)... 47.104.37.237
Connecting to luanpeng.oss-cn-qingdao.aliyuncs.com (luanpeng.oss-cn-qingdao.aliyuncs.com)|47.104.37.237|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 147123 (144K) text/plain
Saving to: 'blogdata.txt'
blogdata.txt 100%===================\> 143.67K --.-KB/s in 0.06s
2022-09-20 23:56:00 (2.25 MB/s) - 'blogdata.txt' saved 147123/147123
Blog china kids music yahoo want ...
Wonkette 0 2 1 0 6 ...
Publishing 2 0 0 7 4 ...
... ... ... ... ... ... ...
代码完成加载数据集,获取列名,行名,样本数据集
** **代码** **
# 读取表格型数据,获取特征数据集。
def readfile(filename):
lines=[line for line in open(filename)]
# 第一行是列标题
colnames=lines[0].strip().split('\t')[1:]
rownames=[]
data=[]
for line in lines[1:]:
p=line.strip().split('\t')
# 每行的第一列是行名
rownames.append(p[0])
# 剩余部分就是该行对应的数据
onerow = [float(x) for x in p[1:]]
data.append(onerow)
return rownames,colnames,data
距离计算
前面的文章我们已经了解了有欧式距离和皮尔逊相似度代表两个数组的亲密度。
欧式距离计算简单,皮尔逊相似度稍微复杂些,不过在下面几种情况时效果好。
1、在两个数组的尺度不相同,但是变化率相同时,效果好。比如,一个数组为7,8,9,一个数组为1,2,3其实这两种具有很强的相似度,只不过在尺度表现上不同罢了。
2、在两个数组所具有的属性数目上不同时效果好。比如,一个数组为1,5,9和另一个数组1,2,3,4,5,6,7,8,9这两个数组也是具有很好的相似度的。
在层次聚类中我们使用皮尔逊相似度来代替节点间距离。
这是因为每个博客所拥有的特征属性的数目差距可能是很大的。
皮尔逊的实现代码
** **代码** **
# 计算两行的皮尔逊相似度
def pearson(v1,v2):
# 简单求和
sum1=sum(v1)
sum2=sum(v2)
# 求平方和
sum1Sq=sum([pow(v,2) for v in v1])
sum2Sq=sum([pow(v,2) for v in v2])
# 求乘积之和
pSum=sum([v1[i]*v2[i] for i in range(len(v1))])
# 计算r
num=pSum-(sum1*sum2/len(v1))
den=sqrt((sum1Sq-pow(sum1,2)/len(v1))*(sum2Sq-pow(sum2,2)/len(v1)))
if den==0: return 0
return 1.0-num/den
注意,在我们使用的博客数据集中,每个单元格的取值是单词在博客中出现的次数。 不过在有些统计中,只关系单词是否在博客中出现,而不关系出现的次数。
只要单词出现,则取值为1,单词不出现则取值为0。假如我们对同时希望拥有两件物品的人在物品方面互有重叠的情况进行度量。
我们采用Tanimoto系数的度量方法,它代表的是交集与并集的比率。
Tanimoto系数的计算实现
代码
# 使用tanimoto系数表示距离(相似度):两个向量的交集/两个向量的并集
def tanamoto(v1,v2):
c1,c2,shr=0,0,0
for i in range(len(v1)):
if v1[i]!=0: c1+=1 # 出现在v1中
if v2[i]!=0: c2+=1 # 出现在v2中
if v1[i]!=0 and v2[i]!=0: shr+=1 # 在两个向量中都出现
return 1.0-(float(shr)/(c1+c2-shr))
迭代实现k均值聚类
随机设置k个聚类点,根据每个点到k个聚类点的距离,为每个点分组。移动聚类点到组成员中心位置。重新计算重新分组重新移动。迭代至成员不再变化。
函数 返回k个聚类点,以及所包含的所有成员
** **代码** **
import random
# 每个点代表一行,每个聚类点,代表一类。参数:rows数据集,distance距离计算算法,k聚类的数目。
def kcluster(rows,distance=pearson,k=4):
# 确定每个点的特征的最小值和最大值
ranges=[(min([row[i] for row in rows]),max([row[i] for row in rows])) for i in range(len(rows[0]))]
# 随机创建K个中心点
clusters=[[random.random()*(ranges[i][1]-ranges[i][0])+ranges[i][0]
for i in range(len(rows[0]))] for j in range(k)]
lastmatches=None
for t in range(100): #默认迭代100次。
print('迭代 %d' % t)
bestmatches=[[] for i in range(k)] #生成k个空数组,用于存储k个聚类点包含的成员
# 在每一行中寻找距离最近的中心点
for j in range(len(rows)):
row=rows[j]
bestmatch=0
for i in range(k):
d=distance(clusters[i],row)
if d<distance(clusters[bestmatch],row): bestmatch=i #计算与哪个聚类点最近
bestmatches[bestmatch].append(j) #每个聚类点记录它包含的组成员
# 如果结果与上一次相同,则整个过程结束
if bestmatches==lastmatches: break
lastmatches=bestmatches
# 把中心点移动到成员的平均位置处
for i in range(k):
avgs=[0.0]*len(rows[0])
if len(bestmatches[i])>0:
for rowid in bestmatches[i]:
for m in range(len(rows[rowid])):
avgs[m]+=rows[rowid][m]
for j in range(len(avgs)): #在每个维度都计算均值
avgs[j]/=len(bestmatches[i])
clusters[i]=avgs
return bestmatches #返回k个聚类点,以及所包含的所有成员
测试
下面我们就可以来测试一下了。
** **代码** **
if __name__=='__main__':
from math import sqrt
blognames,words,data = readfile('blogdata.txt') #加载数据集
kclust = kcluster(data,k=10) #k均值聚类,形成k个聚类
print([blognames[r] for r in kclust[0]]) # 打印第一个聚类的所有博客名称
** **输出** **
迭代 0
迭代 1
迭代 2
迭代 3
迭代 4
'Mashable!', 'Slashdot', 'The Blotter', 'Schneier on Security', 'Signum sine tinnitu--by Guy Kawasaki', 'MetaFilter'
输出结果
迭代 0
迭代 1
迭代 2
迭代 3
迭代 4
"The Superficial - Because You're Ugly", 'Joho the Blog', 'Go Fug Yourself', "O'Reilly Radar", 'Topix.net Weblog', 'flagrantdisregard', 'The Blotter', 'Schneier on Security'
可以看到只迭代了5次就成功聚类完成。
k-means聚类的优缺点:
k-means 算法的优点:
(1)k-means算法是解决聚类问题的一种经典算法,算法简单、快速。
(2)对处理大数据集,该算法是相对可伸缩的和高效率的,因为它的复杂度大约是O(nkt),其中n是所有对象的数目,k是簇的数目,t是迭代的次数。通常k<<n。这个算法通常局部收敛。
(3)算法尝试找出使平方误差函数值最小的k个划分。当簇是密集的、球状或团状的,且簇与簇之间区别明显时,聚类效果较好。
缺点:
(1)k-平均方法只有在簇的平均值被定义的情况下才能使用,且对有些分类属性的数据不适合。
(2)要求用户必须事先给出要生成的簇的数目k。
(3)对初值敏感,对于不同的初始值,可能会导致不同的聚类结果。
(4)不适合于发现非凸面形状的簇,或者大小差别很大的簇。
(5)对于"噪声"和孤立点数据敏感,少量的该类数据能够对平均值产生极大影响。
k中心点算法
k中心算法的基本过程是:
首先为每个簇随意选择一个代表对象,剩余的对象根据其与每个代表对象的距离(此处距离不一定是欧氏距离,也可能是曼哈顿距离)分配给最近的代表对象所代表的簇;
然后反复用非代表对象来代替代表对象,以优化聚类质量。
聚类质量用一个代价函数来表示。当一个中心点被某个非中心点替代时,除了未被替换的中心点外,其余各点被重新分配。
为了减轻k均值算法对孤立点的敏感性,k中心点算法不采用簇中对象的平均值作为簇中心,而选用簇中离平均值最近的对象作为簇中心。
算法如下:
输入:包含n个对象的数据库和簇数目k;
输出:k个簇
(1)随机选择k个代表对象作为初始的中心点
(2)指派每个剩余对象给离它最近的中心点所代表的簇
(3)随机地选择一个非中心点对象y
(4)计算用y代替中心点x的总代价s
(5)如果s为负,则用可用y代替x,形成新的中心点
(6)重复(2)(3)(4)(5),直到k个中心点不再发生变化.
层次聚类 (文档聚类).ipynb
层次聚类算法

层次聚类(Hierarchical Clustering)是聚类算法的一种,通过计算不同类别数据点间的相似度来创建一棵有层次的嵌套聚类树。
在聚类树中,不同类别的原始数据点是树的最低层,树的顶层是一个聚类的根节点。
创建聚类树有自下而上合并和自上而下分裂两种方法,本篇文章介绍合并方法。

层次聚类的合并算法AGNES(凝聚算法)
层次聚类的合并算法通过计算两类数据点间的相似性,对所有数据点中最为相似的两个数据点进行组合,并反复迭代这一过程。
简单的说层次聚类的合并算法是通过计算每一个类别的数据点与所有数据点之间的距离来确定它们之间的相似性,距离越小,相似度越高。
并将距离最近的两个数据点或类别进行组合,生成聚类树。
而当两个数据点进行组合成后,用一个新的代表点,替换原有的两个数据点,再进行下次组合。
其中距离的计算可以使用曼哈顿距离、欧几里得或者皮尔徐相似度进行计算。
距离的度量包含四种方式 :
(1)最小距离(单链(MIN)):定义簇的邻近度为不同两个簇的两个最近的点之间的距离。
(2)最大距离(全链(MAX)):定义簇的邻近度为不同两个簇的两个最远的点之间的距离。
(3)平均距离(组平均):定义簇的邻近度为取自两个不同簇的所有点对邻近度的平均值。
(4)均值距离(Ward 方法的接近函数):质心方法通过计算集群的质心之间的距离来计算两个簇的接近度。
对于 Ward 方法来说,两个簇的接近度指的是当两个簇合并时产生的平方误差的增量。

这里我们使用两个数据点的中心位置替换原有数据点。
当然除了凝聚的层次聚类,还有分裂的层次聚类算法DIANA,过程与凝聚的算法相反。
层次聚类树状图
将前面的每一步的计算结果以树状图的形式展现出来就是层次聚类树。
最底层是原始A到G的7个数据点。
依照7个数据点间的相似度组合为聚类树的第二层(A,F),(B,C),(D,E)和G。以此类推生成完整的层次聚类树状图。
以下为简单的示意图。

加载数据集
使用我们一贯的文本数据集结构。
每行为一个文档(链接或者文档名),每列为一个文档特征(单词),每个单元格的取值为单词在文档中出现的次数。
** **代码** **
# 数据集下载
!wget http://luanpeng.oss-cn-qingdao.aliyuncs.com/csdn/python/%E8%81%9A%E7%B1%BB/blogdata.txt
** **输出** **
--2022-09-18 10:52:25-- http://luanpeng.oss-cn-qingdao.aliyuncs.com/csdn/python/聚类/blogdata.txt
Resolving luanpeng.oss-cn-qingdao.aliyuncs.com (luanpeng.oss-cn-qingdao.aliyuncs.com)... 47.104.37.237
Connecting to luanpeng.oss-cn-qingdao.aliyuncs.com (luanpeng.oss-cn-qingdao.aliyuncs.com)|47.104.37.237|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 147123 (144K) text/plain
Saving to: 'blogdata.txt'
blogdata.txt 100%===================\> 143.67K --.-KB/s in 0.1s
2022-09-18 10:52:25 (1.10 MB/s) - 'blogdata.txt' saved 147123/147123
Blog china kids music yahoo want ...
Wonkette 0 2 1 0 6 ...
Publishing 2 0 0 7 4 ...
... ... ... ... ... ... ...
可以试试运行以下代码完成加载数据集,获取列名,行名,样本数据集
** **代码** **
# 读取表格型数据,获取特征数据集。
def readfile(filename):
lines=[line for line in open(filename)]
# 第一行是列标题
colnames=lines[0].strip().split('\t')[1:]
rownames=[]
data=[]
for line in lines[1:]:
p=line.strip().split('\t')
# 每行的第一列是行名
rownames.append(p[0])
# 剩余部分就是该行对应的数据
onerow = [float(x) for x in p[1:]]
data.append(onerow)
return rownames,colnames,data
层次聚类构建聚类树
层次聚类构建决策树的过程:
1、将每个样本点都初始化为一个独立的聚类
2、计算数据集中每两个聚类之间的相似度,寻找最相似的两个聚类
3、将最相似的两个聚类化为一个聚类,即用两个聚类的均值点做为新聚类替换原有的两个聚类。
4、重复2、3直到只剩下一个聚类。
这样最后形成的聚类树,每个聚类下都包含左右子聚类。聚类树的叶子节点就是由样本对象初始化来的样本点聚类。
我们先来创建聚类的面向对象类
每个聚类都包含左右子聚类,以及代表此聚类的中心点。
另外我们还添加了聚类的id代表聚类在聚类树中的深度,以及左右子聚类的相似度的值。
决策树就是根节点这个聚类。
** **代码** **
# 定义一个聚类,包含左右子聚类。
class bicluster:
def __init__(self,vec,left=None,right=None,distance=0.0,id=None):
self.left=left #左子聚类
self.right=right #右子聚类
self.vec=vec #聚类的中心点
self.id=id #聚类的id
self.distance=distance #左右子聚类间的距离(相似度)
计算每个聚类点之间的相似度。
构建了聚类的类型,我们还要能计算两个聚类的相似度。
我们知道每个聚类都有一个代表该聚类的中心点,我们通过两个聚类的中心点,计算两个聚类的相似度。
在推荐和匹配一章中,我们使用欧氏距离和皮尔逊相似度为例对两个影评者的相似度进行计算。
在文本聚类中,一些博客比其他博客的文章更长一些,包含更多的文章单词。
皮尔逊相似度是判断两个数组与一条直线的拟合程度,因此更适合对两个不同长度的数组进行计算。(两个数组的长度其实是相同的,只是两个数值包含非0值的长度是不同的)
我们使用皮尔逊相似度计算两个中心点的相似度。
代码
# 计算两行的皮尔逊相似度
def pearson(v1,v2):
# 简单求和
sum1=sum(v1)
sum2=sum(v2)
# 求平方和
sum1Sq=sum([pow(v,2) for v in v1])
sum2Sq=sum([pow(v,2) for v in v2])
# 求乘积之和
pSum=sum([v1[i]*v2[i] for i in range(len(v1))])
# 计算r
num=pSum-(sum1*sum2/len(v1))
den=sqrt((sum1Sq-pow(sum1,2)/len(v1))*(sum2Sq-pow(sum2,2)/len(v1)))
if den==0: return 0
return 1.0-num/den
接下面我们就来构建决策树,获取决策树的根节点。
** **代码** **
# 根据数据集形成聚类树
def hcluster(rows,distance=pearson):
distance_set={}
currentclustid=-1
# 最开始聚类就是数据集中的行,每行一个聚类
clust=[bicluster(rows[i],id=i) for i in range(len(rows))] #原始集合中的聚类都设置了不同的正数id,(使用正数是为了标记这是一个叶节点)(使用不同的数是为了建立配对集合)
while len(clust)>1:
lowestpair=(0,1)
closest=distance(clust[0].vec,clust[1].vec)
# 遍历每一对聚类,寻找距离最小的一对聚类
for i in range(len(clust)):
for j in range(i+1,len(clust)):
# 用distance_set来缓存距离最小的计算值
if (clust[i].id,clust[j].id) not in distance_set:
distance_set[(clust[i].id,clust[j].id)]=distance(clust[i].vec,clust[j].vec)
d=distance_set[(clust[i].id,clust[j].id)]
if d<closest:
closest=d
lowestpair=(i,j)
# 计算距离最近的两个聚类的平均值作为代表新聚类的中心点
mergevec=[(clust[lowestpair[0]].vec[i]+clust[lowestpair[1]].vec[i])/2.0 for i in range(len(clust[0].vec))]
# 将距离最近的两个聚类合并成新的聚类
newcluster=bicluster(mergevec,left=clust[lowestpair[0]],
right=clust[lowestpair[1]],
distance=closest,id=currentclustid)
# 不再原始集合中的聚类id设置为负数。为了标记这是一个枝节点
currentclustid-=1
# 删除旧的聚类。(因为旧聚类已经添加为新聚类的左右子聚类了)
del clust[lowestpair[1]]
del clust[lowestpair[0]]
clust.append(newcluster)
return clust[0] #返回聚类树
绘制决策树
1、获取聚类树要显示完整需要的高度
** **代码** **
def getheight(clusttree):
# 若是叶节点则高度为1
if clusttree.left==None and clusttree.right==None: return 1
# 否则,高度为左右分枝的高度之和
return getheight(clusttree.left)+getheight(clusttree.right)
2、获取聚类树要显示完整需要的深度(宽度)
代码
def getdepth(clusttree):
# 一个叶节点的距离是0.0
if clusttree.left==None and clusttree.right==None: return 0
# 一个叶节点的距离=左右两侧分支中距离较大者 + 该支节点自身的距离
return max(getdepth(clusttree.left),getdepth(clusttree.right))+clusttree.distance
3、画聚类节点以及子聚类节点
** **代码** **
def drawnode(draw,clust,x,y,scaling,labels):
if clust.id<0:
h1=getheight(clust.left)*20
h2=getheight(clust.right)*20
top=y-(h1+h2)/2
bottom=y+(h1+h2)/2
# 线的长度
ll=clust.distance*scaling
# 聚类到其子节点的垂直线
draw.line((x,top+h1/2,x,bottom-h2/2),fill=(255,0,0))
# 连接左侧节点的水平线
draw.line((x,top+h1/2,x+ll,top+h1/2),fill=(255,0,0))
# 连接右侧节点的水平线
draw.line((x,bottom-h2/2,x+ll,bottom-h2/2),fill=(255,0,0))
# 调用函数绘制左右子节点
drawnode(draw,clust.left,x+ll,top+h1/2,scaling,labels)
drawnode(draw,clust.right,x+ll,bottom-h2/2,scaling,labels)
else:
# 如果这是一个叶节点,则绘制节点的标签文本
draw.text((x+5,y-7),labels[clust.id],(0,0,0))
接着就可以绘制聚类树
代码
# 绘制树状图------为每一个最终生成的聚类创建一个高度为20像素,宽度固定的图片。其中缩放因子是由固定宽度除以总的深度得到的
def drawdendrogram(clusttree, labels, jpeg='clusters.jpg'):
# 高度和宽度
h = getheight(clusttree) * 20
w = 1200
depth = getdepth(clusttree)
# 由于宽度是固定的,因此我们需要对距离值做相应的调整。(因为显示窗口宽度固定,高度可上下拖动)
scaling = float(w - 150) / depth
# 新建一个白色背景的图片
img = Image.new('RGB', (w, h), (255, 255, 255))
draw = ImageDraw.Draw(img)
draw.line((0, h / 2, 10, h / 2), fill=(255, 0, 0))
# 画根节点(会迭代调用画子节点)
drawnode(draw, clusttree, 10, (h / 2), scaling, labels)
img.save(jpeg, 'JPEG')
我们来测试一下
** **代码** **
! pip install Pillow
** **输出** **
Looking in indexes: https://mirrors.tencent.com/pypi/simple/, https://mirrors.tencent.com/repository/pypi/tencent_pypi/simple
Requirement already satisfied: Pillow in /usr/local/lib/python3.8/dist-packages (9.2.0)
[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\[0m[33m
[0m
** **代码** **
from math import sqrt #导库
from PIL import Image,ImageDraw #导库
blognames,words,data = readfile('blogdata.txt') #加载数据集
clust = hcluster(data) #构建聚类树
drawdendrogram(clust,blognames,jpeg='blogclust.jpg') # 绘制聚类树

一个聚类树就构建完成了。
我们看到这个聚类树还是有明显的分组的。
对特征进行聚类
上面的内容我们是对每个对象(也就是每行数据)形成聚类点,然后对聚类点进行聚类。
那么我们是否也可以对每个特征(每列数据)形成聚类点,然后对特征进行聚类,看看哪些单词经常用在一个主题中。
我们只要对特征数据集进行一下转置,让每行为一种特征(单词),每列代表一个对象(文档)。然后再调用上面的层次聚类算法就可以了。
数据集转置代码
** **代码** **
# 数据集转置,进行列聚类。
def rotatematrix(data):
newdata=[]
for i in range(len(data[0])):
newrow=[data[j][i] for j in range(len(data))]
newdata.append(newrow)
return newdata #一行表示一个单词在每篇博客中出现的次数
下面我们来测试看看
** **代码** **
blognames,words,data = readfile('blogdata.txt') #加载数据集
rdata = rotatematrix(data) #旋转数据集矩阵,对特征属性进行聚类
wordclust = hcluster(rdata) #构建特征的聚类树
drawdendrogram(wordclust,labels=words,jpeg='wordclust.jpg') # 绘制特征的聚类树

DBSCAN密度聚类.ipynb

DBSCAN(Density-Based Spatial Clustering of Applications with Noise,具有噪声的基于密度的聚类方法)是一种很典型的密度聚类算法,和K-Means,BIRCH这些一般只适用于凸样本集的聚类相比,DBSCAN既可以适用于凸样本集,也可以适用于非凸样本集。
下面我们就对DBSCAN算法的原理做一个总结
1. 密度聚类原理
DBSCAN是一种基于密度的聚类算法,这类密度聚类算法一般假定类别可以通过样本分布的紧密程度决定。
同一类别的样本,他们之间的紧密相连的,也就是说,在该类别任意样本周围不远处一定有同类别的样本存在。
通过将紧密相连的样本划为一类,这样就得到了一个聚类类别。通过将所有各组紧密相连的样本划为各个不同的类别,则我们就得到了最终的所有聚类类别结果。
2. DBSCAN密度定义
在上一节我们定性描述了密度聚类的基本思想,本节我们就看看DBSCAN是如何描述密度聚类的。
DBSCAN是基于一组邻域来描述样本集的紧密程度的,参数(ϵ, MinPts)用来描述邻域的样本分布紧密程度。
其中,ϵ描述了某一样本的邻域距离阈值,MinPts描述了某一样本的距离为ϵ的邻域中样本个数的阈值。
假设我的样本集是D=(x_1,x_2,...,x_m),则DBSCAN具体的密度描述定义如下:
1) ϵ-邻域:对于x_j∈D,其ϵ-邻域包含样本集D中与x_j的距离不大于ϵ的子样本集,即N_ϵ(x_j)={x_i∈D\|distance(x_i,x_j)≤ϵ}, 这个子样本集的个数记为\|N_ϵ(x_j)\|
- 核心对象:对于任一样本x_j∈D,如果其ϵ-邻域对应的N_ϵ(x_j)至少包含MinPts个样本,即如果\|N_ϵ(x_j)\|≥MinPts,则x_j是核心对象。
3)密度直达:如果x_i位于x_j的ϵ-邻域中,且x_j是核心对象,则称x_i由x_j密度直达。注意反之不一定成立,即此时不能说x_j由x_i密度直达, 除非且x_i也是核心对象。
4)密度可达:对于x_i和x_j,如果存在样本样本序列p_1,p_2,...,p_T,满足p_1=x_i,p_T=x_j, 且p_{t+1}由p_t密度直达,则称x_j由x_i密度可达。也就是说,密度可达满足传递性。此时序列中的传递样本p_1,p_2,...,p_{T−1}均为核心对象,因为只有核心对象才能使其他样本密度直达。注意密度可达也不满足对称性,这个可以由密度直达的不对称性得出。
5)密度相连:对于x_i和x_j,如果存在核心对象样本x_k,使x_i和x_j均由x_k密度可达,则称x_i和x_j密度相连。注意密度相连关系是满足对称性的。
从下图可以很容易看出理解上述定义,图中MinPts=5,红色的点都是核心对象,因为其ϵ-邻域至少有5个样本。
黑色的样本是非核心对象。所有核心对象密度直达的样本在以红色核心对象为中心的超球体内,如果不在超球体内,则不能密度直达。
图中用绿色箭头连起来的核心对象组成了密度可达的样本序列。在这些密度可达的样本序列的ϵ-邻域内所有的样本相互都是密度相连的。

3. DBSCAN密度聚类思想
DBSCAN的聚类定义很简单:由密度可达关系导出的最大密度相连的样本集合,即为我们最终聚类的一个类别,或者说一个簇。
这个DBSCAN的簇里面可以有一个或者多个核心对象。
如果只有一个核心对象,则簇里其他的非核心对象样本都在这个核心对象的ϵ-邻域里;
如果有多个核心对象,则簇里的任意一个核心对象的ϵ-邻域中一定有一个其他的核心对象,否则这两个核心对象无法密度可达。
这些核心对象的ϵ-邻域里所有的样本的集合组成的一个DBSCAN聚类簇。
那么怎么才能找到这样的簇样本集合呢?DBSCAN使用的方法很简单,它任意选择一个没有类别的核心对象作为种子,然后找到所有这个核心对象能够密度可达的样本集合,即为一个聚类簇。接着继续选择另一个没有类别的核心对象去寻找密度可达的样本集合,这样就得到另一个聚类簇。一直运行到所有核心对象都有类别为止。
基本上这就是DBSCAN算法的主要内容了,是不是很简单?但是我们还是有三个问题没有考虑。
第一个是一些异常样本点或者说少量游离于簇外的样本点,这些点不在任何一个核心对象在周围,在DBSCAN中,我们一般将这些样本点标记为噪音点。
第二个是距离的度量问题,即如何计算某样本和核心对象样本的距离。在DBSCAN中,一般采用最近邻思想,采用某一种距离度量来衡量样本距离,比如欧式距离。这和KNN分类算法的最近邻思想完全相同。对应少量的样本,寻找最近邻可以直接去计算所有样本的距离,如果样本量较大,则一般采用KD树或者球树来快速的搜索最近邻。也可以采用绝对值距离,或者采用每个维度分量距离。
第三种问题比较特殊,某些样本可能到两个核心对象的距离都小于ϵ,但是这两个核心对象由于不是密度直达,又不属于同一个聚类簇,那么如果界定这个样本的类别呢?一般来说,此时DBSCAN采用先来后到,先进行聚类的类别簇会标记这个样本为它的类别。也就是说BDSCAN的算法不是完全稳定的算法。
4. DBSCAN聚类算法
下面我们对DBSCAN聚类算法的流程做一个总结。
输入:样本集D=(x_1,x_2,...,x_m),邻域参数(ϵ,MinPts), 样本距离度量方式
输出: 簇划分C.
1)初始化未分簇核心对象集合Ω=∅, 初始化聚类簇数k=0,初始化未访问样本集合Γ = D, 簇划分C = ∅
- 对于j=1,2,...m, 按下面的步骤找出所有的核心对象:
a) 通过距离度量方式,找到样本x_j的ϵ-邻域子样本集N_ϵ(x_j)
b) 如果子样本集样本个数满足\|N_ϵ(x_j)\|≥MinPts, 将样本x_j加入未分簇核心对象样本集合:Ω=Ω∪{x_j}
3)如果未分簇核心对象集合Ω=∅,则算法结束,否则转入步骤4.
4)在未分簇核心对象集合Ω中,随机选择一个核心对象o,初始化当前簇核心对象队列Ω_{cur}={o}, 初始化类别序号k=k+1,初始化当前簇样本集合C_k={o}, 更新未访问样本集合Γ=Γ−{o}
5)如果当前簇核心对象队列Ω_{cur}=∅,则当前聚类簇C_k生成完毕, 更新簇划分C={C_1,C_2,...,C_k}, 更新未分簇核心对象集合Ω=Ω−C_k, 转入步骤3。否则转化步骤6。
6)在当前簇核心对象队列Ω_{cur}中取出一个核心对象o′,通过邻域距离阈值ϵ找出所有的ϵ-邻域子样本集N_ϵ(o′),令Δ=N_ϵ(o′)∩Γ,以及将剩余样本集中的邻域样本加入到当前簇。 更新当前簇样本集合C_k=C_k∪Δ, 更新未访问样本集合Γ=Γ−Δ, 更新Ω_{cur}=Ω_{cur}∪(N_ϵ(o′)∩Ω),转入步骤5.
输出结果为: 簇划分C={C_1,C_2,...,C_k}
5. DBSCAN小结
和传统的K-Means算法相比,DBSCAN最大的不同就是不需要输入类别数k,当然它最大的优势是可以发现任意形状的聚类簇,而不是像K-Means,一般仅仅使用于凸的样本集聚类。
同时它在聚类的同时还可以找出异常点,这点和BIRCH算法类似。
那么我们什么时候需要用DBSCAN来聚类呢?一般来说,如果数据集是稠密的,并且数据集不是凸的,那么用DBSCAN会比K-Means聚类效果好很多。
如果数据集不是稠密的,则不推荐用DBSCAN来聚类。
下面对DBSCAN算法的优缺点做一个总结。
DBSCAN的主要优点有:
1) 可以对任意形状的稠密数据集进行聚类,相对的,K-Means之类的聚类算法一般只适用于凸数据集。
2) 可以在聚类的同时发现异常点,对数据集中的异常点不敏感。
3) 聚类结果没有偏倚,相对的,K-Means之类的聚类算法初始值对聚类结果有很大影响。
DBSCAN的主要缺点有:
1)如果样本集的密度不均匀、聚类间距差相差很大时,聚类质量较差,这时用DBSCAN聚类一般不适合。
2) 如果样本集较大时,聚类收敛时间较长,此时可以对搜索最近邻时建立的KD树或者球树进行规模限制来改进。
3) 调参相对于传统的K-Means之类的聚类算法稍复杂,主要需要对距离阈值ϵ,邻域样本数阈值MinPts联合调参,不同的参数组合对最后的聚类效果有较大影响。
案例 -- 代码中需要的数据集文件DBSCAN_data.txt(http://luanpeng.oss-cn-qingdao.aliyuncs.com/csdn/python/聚类/DBSCAN_data.txt)
python3.6下代码实现
** **代码** **
!wget http://luanpeng.oss-cn-qingdao.aliyuncs.com/csdn/python/%E8%81%9A%E7%B1%BB/DBSCAN_data.txt
** **输出** **
--2022-09-20 16:28:14-- http://luanpeng.oss-cn-qingdao.aliyuncs.com/csdn/python/聚类/DBSCAN_data.txt
Resolving luanpeng.oss-cn-qingdao.aliyuncs.com (luanpeng.oss-cn-qingdao.aliyuncs.com)... 47.104.37.237
Connecting to luanpeng.oss-cn-qingdao.aliyuncs.com (luanpeng.oss-cn-qingdao.aliyuncs.com)|47.104.37.237|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1466 (1.4K) text/plain
Saving to: 'DBSCAN_data.txt'
DBSCAN_data.txt 100%===================\> 1.43K --.-KB/s in 0s
2022-09-20 16:28:14 (199 MB/s) - 'DBSCAN_data.txt' saved 1466/1466
** **代码** **
import pylab as pl
from collections import defaultdict,Counter
# 加载数据集
def loaddata(filepath):
points = [[float(eachpoint.split(",")[0]), float(eachpoint.split(",")[1])] for eachpoint in open(filepath,"r")]
return points
# 以距离最大的维度上的距离为两个对象之间的距离
def distance(point1,point2):
return max(abs(point1[0] - point2[0]),abs(point1[1] - point2[1]))
# 计算每个数据点相邻的数据点,邻域定义为以该点为中心以边长为2*EPs的网格
def getSurroundPoint(points,Eps=1):
surroundPoints = {} # 每个元素默认是一个空列表
for idx1,point1 in enumerate(points):
for idx2,point2 in enumerate(points):
if (idx1 < idx2):
if(distance(point1,point2)<=Eps):
surroundPoints.setdefault(idx1,[]) # 设置每个点的默认值邻节点为空列表
surroundPoints.setdefault(idx2, []) # 设置每个点的默认值邻节点为空列表
surroundPoints[idx1].append(idx2)
surroundPoints[idx2].append(idx1)
return surroundPoints
# 定义邻域内相邻的数据点的个数大于4的为核心点,获取核心点以及核心点的周边点
def findallCore(points,surroundPoints,Eps=10,MinPts=5):
# 获取所有核心点
corePointIdx = [pointIdx for pointIdx,surPointIdxs in surroundPoints.items() if len(surPointIdxs)>=MinPts]
# 邻域内包含某个核心点的非核心点,定义为边界点
borderPointIdx = []
for pointIdx,surPointIdxs in surroundPoints.items():
if (pointIdx not in corePointIdx): # 边界点本身不是核心点
for onesurPointIdx in surPointIdxs:
if onesurPointIdx in corePointIdx: # 边界点周边至少包含一个核心点
borderPointIdx.append(pointIdx)
break
corePoint = [points[pointIdx] for pointIdx in corePointIdx] # 核心点
borderPoint = [points[pointIdx] for pointIdx in borderPointIdx] #边界点
return corePointIdx,borderPointIdx
# 获取所有噪声点。噪音点既不是边界点也不是核心点
def findallnoise(points,corePointIdx,borderPointIdx):
noisePointIdx = [pointIdx for pointIdx in range(len(points)) if pointIdx not in corePointIdx and pointIdx not in borderPointIdx]
noisePoint = [points[pointIdx] for pointIdx in noisePointIdx]
return noisePoint
# 根据邻域关系,核心点,边界点进行分簇
def divideGroups(points,surroundPoints,corePointIdx,borderPointIdx):
groups = [idx for idx in range(len(points))] # groups记录每个节点所属的簇编号
# 各个核心点与其邻域内的所有核心点放在同一个簇中
for pointidx,surroundIdxs in surroundPoints.items():
for oneSurroundIdx in surroundIdxs:
if (pointidx in corePointIdx and oneSurroundIdx in corePointIdx and pointidx < oneSurroundIdx):
for idx in range(len(groups)):
if groups[idx] == groups[oneSurroundIdx]:
groups[idx] = groups[pointidx]
# 边界点跟其邻域内的某个核心点放在同一个簇中
for pointidx,surroundIdxs in surroundPoints.items():
for oneSurroundIdx in surroundIdxs:
if (pointidx in borderPointIdx and oneSurroundIdx in corePointIdx):
groups[pointidx] = groups[oneSurroundIdx]
break
return groups
# 绘制分簇图
def plotgroup(points,groups,noisePoint):
# 取簇规模最大的3个簇
finalGroup = Counter(groups).most_common(3)
finalGroup = [onecount[0] for onecount in finalGroup]
group1 = [points[idx] for idx in range(len(points)) if groups[idx]==finalGroup[0]]
group2 = [points[idx] for idx in range(len(points)) if groups[idx]==finalGroup[1]]
group3 = [points[idx] for idx in range(len(points)) if groups[idx]==finalGroup[2]]
pl.plot([eachpoint[0] for eachpoint in group1], [eachpoint[1] for eachpoint in group1], 'or')
pl.plot([eachpoint[0] for eachpoint in group2], [eachpoint[1] for eachpoint in group2], 'oy')
pl.plot([eachpoint[0] for eachpoint in group3], [eachpoint[1] for eachpoint in group3], 'og')
# 打印噪音点,黑色
pl.plot([eachpoint[0] for eachpoint in noisePoint], [eachpoint[1] for eachpoint in noisePoint], 'ok')
pl.show()
if __name__=='__main__':
points = loaddata('DBSCAN_data.txt') # 加载数据
surroundPoints=getSurroundPoint(points,Eps=2) # 获取邻域关系
corePointIdx, borderPointIdx = findallCore(points,surroundPoints,Eps=2,MinPts=3) # 获取核心节点和边界节点
noisePoint = findallnoise(points,corePointIdx,borderPointIdx) # 获取噪音节点
groups = divideGroups(points,surroundPoints,corePointIdx,borderPointIdx) # 节点分簇
plotgroup(points, groups, noisePoint) # 可视化绘图

BIRCH聚类.ipynb
1. BIRCH概述

BIRCH的全称是利用层次方法的平衡迭代规约和聚类(Balanced Iterative Reducing and Clustering Using Hierarchies),名字实在是太长了,不过没关系,其实只要明白它是用层次方法来聚类和规约数据就可以了。
刚才提到了,BIRCH只需要单遍扫描数据集就能进行聚类,那它是怎么做到的呢?
BIRCH算法利用了一个树结构来帮助我们快速的聚类,这个数结构类似于平衡B+树,一般将它称之为聚类特征树(Clustering Feature Tree,简称CF Tree)。这颗树的每一个节点是由若干个聚类特征(Clustering Feature,简称CF)组成。
从下图我们可以看看聚类特征树是什么样子的:树中的每个节点(包括叶子节点)都有若干个CF,而内部节点的CF有指向孩子节点的指针,所有的叶子节点用一个双向链表链接起来。

有了聚类特征树的概念,我们再对聚类特征树和其中节点的聚类特征CF做进一步的讲解。
树中的每个节点都对应一个簇(或者叫聚类)。子节点对应的簇是父节点对应的簇的子簇,所以 BIRCH 算法算是基于层次的聚类算法。
2. 聚类特征CF与聚类特征树CF Tree
在聚类特征树中,一个节点的聚类特征CF是这样定义的:每一个CF是一个三元组,可以用(N,LS,SS)表示。其中N代表了这个簇中拥有的样本点的数量,这个好理解;LS代表了这个簇中拥有的样本点各特征维度的和向量,SS代表了这个簇中拥有的样本点各特征维度的平方和。
举个例子如下图,在CF Tree中的某一个节点对应的簇中,有下面5个样本(3,4), (2,6), (4,5), (4,7), (3,8)。则它对应的N=5, LS=(3+2+4+4+3,4+6+5+7+8)=(16,30), SS =(32+22+42+42+32+42+62+52+72+82)=(54+190)=244

CF有一个很好的性质,就是满足线性关系,也就是CF1+CF2=(N1+N2,LS1+LS2,SS1+SS2)。这个性质从定义也很好理解。
如果把这个性质放在CF Tree上,也就是说,在CF Tree中,对于每个父节点中的CF,它的(N,LS,SS)三元组的值等于它的子节点的三元组之和。如下图所示:

从上图中可以看出,根节点的CF1的三元组的值,可以从它指向的6个子节点(CF7 - CF12)的值相加得到。这样我们在更新CF Tree的时候,可以很高效。
对于CF Tree,我们一般有几个重要参数,第一个参数是分支因子B,表示每个非叶节点的子女的最大数目,第二个参数是阈值T,表示存储在树的叶节点中的子簇的最大直径。
也就是说,叶节点对应的子簇中的所有样本点一定要在直径小于T的一个超球体内。
有时还会定义叶节点的分支因子L,表示每个叶节点对应的子簇的最大样本个数。
对于上图中的CF Tree,限定了B=7, L=5, 也就是说内部节点最多有7个子节点,而叶子节点对应的簇最多有5个样本。
3. 聚类特征树CF Tree的生成
│ (注意区分:样本点、CF节点(树中的才分为节点))
下面我们看看怎么生成CF Tree。
我们先定义好CF Tree的参数: 即内部节点的最大子节点数B, 叶子节点对应的簇的最大样本数L, 叶节点对应的簇的最大样本直径阈值T。
在最开始的时候,CF Tree是空的,没有任何样本,我们从训练集读入第一个样本点x1,将它放入一个新的节点A,这个节点的CF值中N=1,将这个新的节点作为根节点,此时的CF Tree如下图:

现在我们继续读入第二个样本点x2,我们发现这个样本点和第一个样本点x1,在直径为T的超球体范围内,也就是说,他们属于一个簇,我们将第二个点也加入节点A所代表的簇中,此时需要更新节点A的CF值。
此时A的CF值中N=2。此时的CF Tree如下图:

此时来了第三个节点x3,结果我们发现这个节点不能融入刚才前面的节点形成的超球体内,也就是说,我们需要新建一个簇来容纳x3,同时为这个簇在CF树中添加一个新的节点B。
此时根节点有两个子节点,A和B,此时的CF Tree如下图:

当来到第四个样本点x4的时候,我们发现和节点B代表的簇在直径小于T的超球体,这样更新后的CF Tree如下图:

那个什么时候CF Tree的节点需要分裂呢?假设我们现在的CF Tree 如下图, 叶子节点LN1代表的簇有三个样本, LN2和LN3各有两个样本。我们的叶子节点的最大样本数L=3。此时一个新的样本点来了,我们发现它离LN1节点代表的簇最近,因此开始判断它是否在sc1,sc2,sc3这3个样本对应的超球体之内,但是很不幸,它不在,因此它需要建立一个新的叶子节点来容纳它。问题是我们的L=3,也就是说LN1的样本个数已经达到最大值了,不能再添加新的样本了,怎么办?此时就要将LN1叶子节点一分为二了。

我们将LN1里所有样本中,找到两个最远的样本做这两个新叶子节点的种子样本,然后将LN1节点里所有样本sc1, sc2, sc3,以及新样本点sc8划分到两个新的叶子节点上。将LN1节点划分后的CF Tree如下图:

如果我们的内部节点的最大子节点数B=3,则此时叶子节点一分为二会导致根节点的最大子节点数超了,也就是说,我们的根节点现在也要分裂,分裂的方法和叶子节点分裂一样,分裂后的CF Tree如下图:

有了上面这一系列的图,相信大家对于CF Tree的插入就没有什么问题了,总结下CF Tree的插入:
-
从根节点向下寻找和新样本距离最近的叶子节点和叶子节点里最近的样本节点
-
如果新样本加入后,这个样本节点对应的超球体直径仍然满足小于阈值T,则将新样本点加入叶子节点对应的簇中,并更新路径上所有的树节点,插入结束。否则转入3.
3.将当前叶子节点划分为两个新叶子节点,选择旧叶子节点中所有样本点中距离最远的样本点,分布作为两个新叶子节点的第一个样本节点。将其他样本和新样本按照距离远近原则放入对应的叶子节点。依次向上检查父节点是否也要分裂,如果需要,按和叶子节点分裂方式相同。
4. BIRCH算法
上面讲了半天的CF Tree,终于我们可以步入正题BIRCH算法,其实将所有的训练集样本建立了CF Tree,一个基本的BIRCH算法就完成了,对应的输出就是若干树节点,每个节点里的样本点就是一个聚类的簇。也就是说BIRCH算法的主要过程,就是建立CF Tree的过程。
当然,真实的BIRCH算法除了建立CF Tree来聚类,其实还有一些可选的算法步骤的,现在我们就来看看 BIRCH算法的流程。
1) 将所有的样本依次读入,在内存中建立一颗CF Tree, 建立的方法参考上一节。
2)(可选)将第一步建立的CF Tree进行筛选,去除一些异常CF节点,这些节点一般里面的样本点很少。对于一些超球体距离非常近的元组进行合并
3)(可选)利用其它的一些聚类算法比如K-Means对所有的CF元组进行聚类,得到一颗比较好的CF Tree.这一步的主要目的是消除由于样本读入顺序导致的不合理的树结构,以及一些由于节点CF个数限制导致的树结构分裂。
4)(可选)利用第三步生成的CF Tree的所有CF节点的质心,作为初始质心点,对所有的样本点按距离远近进行聚类。这样进一步减少了由于CF Tree的一些限制导致的聚类不合理的情况。
从上面可以看出,BIRCH算法的关键就是步骤1,也就是CF Tree的生成,其他步骤都是为了优化最后的聚类结果。
5. BIRCH算法小结
BIRCH算法可以不用输入类别数K值,这点和K-Means,Mini Batch K-Means不同。如果不输入K值,则最后的CF元组的组数即为最终的K,否则会按照输入的K值对CF元组按距离大小进行合并。
一般来说,BIRCH算法适用于样本量较大的情况,这点和Mini Batch K-Means类似,但是BIRCH适用于类别数比较大的情况,而Mini Batch K-Means一般用于类别数适中或者较少的时候。
BIRCH除了聚类还可以额外做一些异常点检测和数据初步按类别规约的预处理。
但是如果数据特征的维度非常大,比如大于20,则BIRCH不太适合,此时Mini Batch K-Means的表现较好。
对于调参,BIRCH要比K-Means,Mini Batch K-Means复杂,因为它需要对CF Tree的几个关键的参数进行调参,这几个参数对CF Tree的最终形式影响很大。
最后总结下BIRCH算法的优缺点:
BIRCH算法的主要优点有:
-
节约内存,所有的样本都在磁盘上,CF Tree仅仅存了CF节点和对应的指针。
-
聚类速度快,只需要一遍扫描训练集就可以建立CF Tree,CF Tree的增删改都很快。
-
可以识别噪音点,还可以对数据集进行初步分类的预处理
BIRCH算法的主要缺点有:
-
由于CF Tree对每个节点的CF个数有限制,导致聚类的结果可能和真实的类别分布不同.
-
对高维特征的数据聚类效果不好。此时可以选择Mini Batch K-Means
-
如果数据集的分布簇不是类似于超球体,或者说不是凸的,则聚类效果不好。
整体对比
总览对比
| 维度 | k 均值 | k 中心点 | 层次聚类(凝聚 AGNES ) | DBSCAN | BIRCH |
|---|---|---|---|---|---|
| 类型 | 划分式 | 划分式 | 层次式(自底向上) | 密度式 | 层次 + 增量(CF 树) |
| 核心思想 | 用簇均值当中心,迭代分配 | 用簇内真实样本当中心,换中心降代价 | 不断合并最相似的两类,建树 | 由密度可达得到最大密度相连簇 | 用 CF 树压缩数据,单遍/少遍扫描聚类 |
| 簇表示 | 均值(可不在数据点上) | 中心点 medoid(必为样本) | 子树 / 树状图 | 核心对象邻域扩张 | CF 三元组 (N, LS, SS) |
| 是否需预设 k | 需要 | 需要 | 可不预设,事后切树 | 不需要 | 可后处理得到 k 个簇 |
| 簇形状 | 偏球形/凸 | 偏球形/凸 | 取决于链接方式 | 任意形状 | 偏球形/凸 |
| 噪声处理 | 差(离群点拉均值) | 较好(中心是真实点) | 一般 | 强(可标噪声点) | 一般(阈值 T 有一定抗噪) |
| 主要参数 | k、距离、初值 | k、代价函数 | 链接方式(单链/全链/平均/Ward)、切分高度 | ε、MinPts | B、L、阈值 T |
| 典型复杂度 | 约 Onkt ,较快 |
更高(试换中心) | 约 O n2 ~O n3 ,大数据慢 |
约 O n logn (有索引)~O n2 ![]() |
适合大数据,近似线性级 |
| 可解释 / 可视化 | 中心直观 | 中心是样本,更可解释 | 树状图很直观 | 簇边界不固定 | CF 树结构,偏工程 |
| notebook 应用 | 文档分组等 | 抗离群的划分聚类 | 文档聚类 + 画树 | 非凸簇、带噪声数据 | 大规模层次规约聚类 |
优缺点速览
| 算法 | 优点 | 缺点 / 注意 |
|---|---|---|
| k 均值 | 简单快、易实现、大数据常用 | 对初值/噪声敏感;要先定 k;非凸簇效果差 |
| k 中心点 | 比 k 均值更抗离群点;中心可解释 | 计算更贵;仍要定 k,仍偏凸簇 |
| 层次聚类 | 不必事先定 k;树状图好看;链接方式灵活 | 大数据慢;合并不可逆;单链易"链式效应" |
| DBSCAN | 任意形状;自动发现簇数;能标噪声 | ε/MinPts 难调;密度不均时易失效 |
| BIRCH | 适合大规模;增量、省内存;可做数据规约 | 对非球形簇较弱;参数(B/L/T)需调 |
怎么选(实用)
| 场景 | 更合适 |
|---|---|
| 数据量大、要快、簇大致球形 | k 均值 ;要更抗噪 → k 中心点 |
| 要看"怎么逐步合并"、文档小样本、要树状图 | 层次聚类 |
| 簇形状不规则、有明显噪声/离群点 | DBSCAN |
| 超大规模、流式/增量、先压缩再聚类 | BIRCH |
一句话:
划分(k 均值/中心点)靠"定中心";层次靠"建树合并";DBSCAN 靠"密度连通";BIRCH 靠"CF 树压缩后层次聚类"。
,较快
~O n3
,大数据慢
(有索引)~O n2 