啥是聚类算法
物以类聚人以群分,对于一组样本,自动地分成几个类,使同一类对象的相似度尽可能地大;不同类对象之间的相似度尽可能地小。这样的算法就是聚类算法。
手撕 K-Means
算法原理
K-means算法是指定分成K类,而每类的质心取平均值的一种聚类算法。其基本步骤是
- 确定簇数K和初始的质心,初始的簇标签。
- 计算每一个样本到K个质心的距离,取距离最新的那个质心所属的簇作为簇标签。
- K个簇的所有样本,分别取质心,更新质心
- 重复上述过程,直到所有样本的簇标签不再变化。
K-means代码演示
首先我们先构造一些样本点,在空间(0,0)到(300,300)的矩形空间里构造100个随机点,在空间(500,500)到(800,800)的矩形空间里构造100个随机点,在空间(0,300)到(500,800)的矩形空间里构造100个随机点,并在空间(0,800)到(800,800)的矩形空间里构造50个随机点。取K=3,即三个簇,代码如下
import random
import math
import matplotlib.pyplot as plt
points=[]
clusters=[]
pNum=100
K=3
markers = ['^', 'x', 'o', '*', '+']
color = ['r', 'b', 'g', 'm', 'c']
for i in range(pNum):
point=[random.randint(0,300),random.randint(0,300)]
points.append(point)
clusters.append(random.randint(0,K-1))
point = [random.randint(500, 800), random.randint(500, 800)]
points.append(point)
clusters.append(random.randint(0, K-1))
point = [random.randint(500, 800), random.randint(0, 300)]
points.append(point)
clusters.append(random.randint(0, K-1))
if i % 2 == 0:
point = [random.randint(0, 800), random.randint(0, 800)]
points.append(point)
clusters.append(random.randint(0, K-1))
for i in range(len(points)):
plt.scatter(points[i][0],points[i][1],marker=markers[clusters[i]],c=color[clusters[i]], alpha=0.5)
plt.show()
把初始数据绘制出来,结果如图所示,可见其杂乱无章。
下面我们初始化质心
centers=[]
for i in range(K):
center = [random.randint(0, 800), random.randint(0, 800)]
centers.append(center)
下一步进行迭代进行聚类操作。设置终止条件即当所有节点的聚类标签不变化时结束。
更新节点所处的聚类
for i in range(len(points)):
dislist=[]
for center in centers:
dislist.append(distEclud(points[i],center))
clusterNum=dislist.index(min(dislist))
clustersNodes[clusterNum].append(points[i])
if clusterNum!=clusters[i]:
clusterChanged=True
clusters[i]=clusterNum
更新质心
for i in range(K):
x=0
y=0
for cN in clustersNodes[i]:
x+=cN[0]
y+=cN[1]
centers[i][0]=x / (len(clustersNodes[i])-1)
centers[i][1] = y / (len(clustersNodes[i]) - 1)
我们可以查看聚类的过程,从第一次迭代到第四次迭代
问题考察
如果我们把聚类数量设置为4,经过上述代码运算可以得到聚类结果,我们发现,结果并不好。问题在于我们用平均值作为计算质心的参数,忽略了方差。同时初始质心的选择对结果的影响也较大。
手撕密度聚类算法-DBscan聚类算法
啥是密度聚类
密度是单位面积上物体的数量。只要一个区域中的点的密度大过某个阈值那么我们认为这是一个聚类。密度有面积和物体个数决定,同样的DBscan算法需要两个参数,一个是半径Eps,另一个是指定的数目MinPts。在一某点为中心,Eps为半径的圆圈里,有至少MinPts个物体。那么这个圈圈里的物体构成一个聚类。其基本步骤如下:
- 输入数据集,半径Eps,阈值MinPts,此时所有样本都没有被访问,被标记为unvisited。
- 开始选择一个unvisited样本,随机的。
- 此时该样本已经被访问了,标记为已访问visited。
- 查找该样本距离eps内的其他样本。记为P
- 若P的个数大于Minpts,证明满足密度聚类点条件。
- 则创建一个新的簇,记为C,那么该样本必然是要在C中。
- 对于P中的任何一个样本,记为a
- 如果a没有被访问,那就先标记为被访问了。
- 然后计算a的距离eps内的样本,记为P1,说明a也构成一个簇,但是a已经是簇C中的元素了,所以不需要创建新的簇。a的邻居们也可能是C中的元素。
- 若P1的个数大于Minpts,则把P1加入到P中。同时若a还没有分簇,则a加入到C中。
- 若P的个数小于Minpts,则该样本形单影只,不构成簇,标记为噪声点。
- 当所有的样本都访问了,那么分簇结束。
DBscan代码演示
先构建数据集
points=[]
visited=[]
labels=[]
pNum=100
markers = ['^', 'x', 'o', '*', '+','*']
color = ['r', 'b', 'g', 'm', 'c','k']
unvisited=[]
Eps=200
MinPts=80
j=0
for i in range(pNum):
point=[random.randint(0,300),random.randint(0,300)]
points.append(point)
visited.append(0)
labels.append(0)
unvisited.append(j)
j+=1
point = [random.randint(500, 800), random.randint(500, 800)]
points.append(point)
visited.append(0)
labels.append(0)
unvisited.append(j)
j += 1
point = [random.randint(500, 800), random.randint(0, 300)]
points.append(point)
visited.append(0)
labels.append(0)
unvisited.append(j)
j += 1
if i % 2 == 0:
point = [random.randint(0, 800), random.randint(0, 800)]
points.append(point)
visited.append(0)
labels.append(0)
unvisited.append(j)
j += 1
构建簇类
class cluster():
def __init__(self,clsnum):
self.cluster_num=clsnum
self.cluster_points=[]
根据算法查找满足密度聚类的点和相对应的聚类。
总体代码如下:
# -*- coding: utf-8 -*-
# @Time : 2020/4/24 19:27
# @Author : HelloWorld!
# @FileName: DBscan.py
# @Software: PyCharm
# @Operating System: Windows 10
import random
import math
import matplotlib.pyplot as plt
class cluster():
def __init__(self,clsnum):
self.cluster_num=clsnum
self.cluster_points=[]
points=[]
visited=[]
labels=[]
pNum=100
markers = ['^', 'x', 'o', '*', '+','*']
color = ['r', 'b', 'g', 'm', 'c','k']
unvisited=[]
Eps=200
MinPts=80
j=0
for i in range(pNum):
point=[random.randint(0,300),random.randint(0,300)]
points.append(point)
visited.append(0)
labels.append(0)
unvisited.append(j)
j+=1
point = [random.randint(500, 800), random.randint(500, 800)]
points.append(point)
visited.append(0)
labels.append(0)
unvisited.append(j)
j += 1
point = [random.randint(500, 800), random.randint(0, 300)]
points.append(point)
visited.append(0)
labels.append(0)
unvisited.append(j)
j += 1
if i % 2 == 0:
point = [random.randint(0, 800), random.randint(0, 800)]
points.append(point)
visited.append(0)
labels.append(0)
unvisited.append(j)
j += 1
noise=[]
def distEclud(A,B):
return math.sqrt(math.pow(A[0]-B[0],2)+math.pow(A[1]-B[1],2))
clusternum=0
def findneihbor(select_p,points):
neighbor = []
for i in range(len(points)):
if distEclud(points[i], select_p) < Eps:
neighbor.append(i)
return neighbor
clusters=[]
while sum(visited)<len(visited):
select_num=random.randint(0,len(unvisited)-1)
vis=unvisited[select_num]
select_p=points[unvisited[select_num]]
visited[unvisited[select_num]]=1
unvisited.remove(unvisited[select_num])
neighbor=findneihbor(select_p,points)
if neighbor and len(neighbor)>= MinPts:
clusternum+=1
newcluster=cluster(clusternum)
newcluster.cluster_points.append(select_p)
for nb in neighbor:
if visited[nb]==0:
visited[nb]=1
if nb in unvisited:
unvisited.remove(nb)
neighbor_sub= findneihbor(points[nb],points)
if neighbor_sub and len(neighbor_sub)>= MinPts:
neighbor.extend(neighbor_sub)
if labels[nb]==0:
newcluster.cluster_points.append(points[nb])
labels[nb]=clusternum
clusters.append(newcluster)
else:
noise.append(select_p)
for i in range(len(clusters)):
for point in clusters[i].cluster_points:
plt.scatter(point[0],point[1],marker=markers[i%len(markers)],c=color[i%len(color)], alpha=0.5)
if noise:
for point in noise:
plt.scatter(point[0], point[1], marker='o', c='k', alpha=0.9)
plt.title('Estimated number of clusters: %d' % len(clusters))
plt.show()
在设置Eps=200,MinPts=80情况下的聚类效果如下,看起来效果还不错,右下右上左下的黑色噪音点是因为随机被选中但是不满足密度聚类。
但是如果修改Eps=200,MinPts=60,效果却可能变成如下所示。由此可见,虽然不需要输入分簇的个数,但是需要确定距离r和minPoints才能取得好的效果。