啥是聚類算法
物以類聚人以羣分,對於一組樣本,自動地分成幾個類,使同一類對象的相似度儘可能地大;不同類對象之間的相似度儘可能地小。這樣的算法就是聚類算法。
手撕 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才能取得好的效果。