今天这篇文章写一下层次聚类,这也是除了k-means之外较为常用的另一种聚类方法。
假设有N个待聚类的样本,对于层次聚类来说,步骤:
1.(初始化)把每个样本归为一类,计算每两个类之间的距离,也就是样本与样本之间的相似度
2.寻找各个类之间最近的两个类,把他们归为一类(这样类的总数就少了一个);
3.重新计算新生成的这个类与各个旧类的相似度;
4.重复2和3直到所有样本点归为一类,结束
比如在以上图中,开始分为5类,计算两两之间距离,发现1和2的距离最近,所以1和2生成新的类—6,现在的类别是:3,4,5,6,继续计算两两之间距离,发现4和5距离最近,生成新的类—7,继续重复计算,直到所有样本点都归为一类。
整个聚类过程其实是建立了一棵树,在建立的过程中,可以通过在第二步上设置一个阈值,当最近的两个类的距离大于这个阈值,则认为迭代可以终止。另外关键的一步就是第三步,如何判断两个类之间的相似度有不少种方法,这里介绍以下三种:
(1)Single-Linkage:又叫做 nearest-neighbor,就是取两个类中距离最近的两个样本的距离作为这两个集合的距离,也就是说,最近两个样本之间的距离越小,这两个类之间的相似度就越大。容易造成一种叫做Claining的效果,两个Cluster明明从“大局”上离得较远,但是由于其中个别的点距离比较近就被合并了,并且这样合并之后Claining效应会进一步扩大,最后会得到比较松散的cluster
(2)Complete-Linkage:这个则完全是Single-Linkage的反面极端,取两个集合中距离最远的两个点的距离作为两个集合的距离。其效果也是刚好相反的,限制非常大,两个cluster即使已经很接近了,但是只要有不配合的点存在,就顽固到底,老死不相合并,也是不太好的办法。这两种相似度的定义方法的共同问题就是只考虑了某个有特点的数据,而没有考虑内数据的整体特点。
(3)Average-Linkage:这种方法就是把两个集合的点两两的距离全部放在一起求一个平均值,相对也能得到合适一点的结果。
Average-Linkage的一个变种就是取两两距离的中值,与取均值相比更加能够解除个别偏离样本对结果的干扰。
代码:
from numpy import *
class cluster_node:#定义结点类
def __init__(self,vec,left=None,right=None,distance=0.0,id=None,count=1):
self.left=left
self.right=right
self.vec=vec
self.id=id
self.distance=distance
self.count=count #构造函数
def L2dist(v1,v2):#两个向量间对应元素相减后平方
return sqrt(sum((v1-v2)**2))
def L1dist(v1,v2):#向量间绝对值求出来后求和
return sum(abs(v1-v2))
def hcluster(features,distance=L2dist):
distance={}
currentclusterid=-1#目前跟踪的当前类,初始化为-1
clust=[cluster_node(array(features[i]),id=i) for i in range(len(features))]
while len(clust)>1:#聚类条件,聚类点的数量大于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)):
if (clust[i].id,clust[j].id) not in distances:
distance[(clust[i].id,clust[j].id)]=distance(clust[i].vec,clust[j].vec)
d=distance[(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=cluster_node(array(mergevec),left=clust[lowestpair[0]],
right=clust[lowestpair[1]],
distance=closest,id=currentclustid)
currentclustid-=1
del clust[lowestpair[1]]
del clust[lowestpair[0]]
clust.append(newcluster)
return clust[0]
def extract_clusters(clust,dist):
clusters={}
if clust.distance<dist:
return [clust]
else:
cl=[]#左儿子
cr=[]#右儿子
if clust.left!=None:
cl=extract_clusters(clust.left,dist=dist)
if clust.right!=None:
cr=extract_clusters(clust.right,dist=dist)
return cl+cr
def get_cluster_elements(clust):
if clust.id>=0:
return [clust.id]
else:
cl=[]
cr=[]
if clust.left!=None:
cl=get_cluster_elements(clust.left)
if clust.right!=None:
cr=get_cluster_elements(clust.right)
return cl+cr
def printclust(clust,labels=None,n=0):
for i in range(n):
print(' ')
if clust.id<0:
print('----')
else:
if labels==None:
print(clust.id)
else:
print(labels[clust.id])
if clust.left!=None:
printclust(clust.left,labels=labels,n=n+1)
if clust.right!=None:
printclust(clust.right,labels=labels,n=n+1)
def getheight(clust):
if clust.left==None and clust.right==None:
return 1
return getheight(clust.left)+getheight(clust.right)
def getdepth(clust):
if clust.left==None and clust.right==None:
return 0
return max(getdepth(clust.left),getdepth(clust.right))+clust.distance
测试代码:
import os
from PIL import Image,ImageDraw
from HierarchicalClustering import hcluster
from HierarchicalClustering import getheight
from HierarchicalClustering import getdepth
import numpy as np
def drawdendrogram(clust,imlist,jpeg='cluster.jpg'):
h=getheight(clust)*20#定义高度
w=1200#定义宽度
depth=getdepth(clust)
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,clust,10,int(h/2),scaling,inlist,img)
img.save(jpeg,'JPEG')
def drawnode(draw,clust,x,y,scaling,inlist,img):
if clust.id<0:
h1=getheight(clust.left)*20
h2=getheight(clust.right)*20
top=y-(h1+h2)/2
bottom=y+(h1+h2)/2
l1=clust.distance*scaling#线的长度
draw.line((x,top+h1/2,x,bottom-h2/2),fill=(255,0,0))#聚类到其子节点的垂直线
draw.line((x,top+h1/2,x+l1,top+h1/2),fill=(255,0,0))#连接左侧节点的水平线
draw.line((x,bottom-h2/2,x+l1,bottom-h2/2),fill=(255,0,0))#连接右侧节点的水平线
#绘制左右节点
drawnode=(draw,clust.left,x+l1,top+h1/2,scaling,imlist,img)
drawnode=(draw,clust.right,x+l1,bottom-h2/2,scaling,imlist,img)
else:
nodeim=Image.open(imlist[clust.id])
nodeim.thumbnail((20,20))#把图变小
ns=nodeim.size
print(x,y-ns[1]/2)
print(x+ns[0])
print(img.paste(nodeim,(int(x),int(y-ns[1]//2),int(x+ns[0]),int(y+ns[1]-ns[1]//2))))
imlist=[]
folderPath=r'flicker-sunsets-small'
for filename in os.listdir(folderPath):
if os.path.splitext(filename)[1]=='.jpg':
imlist.append(os.path.join(folderPath,filename))
n=len(imlist)
print(n)
features=np.zeros((n,3))
for i in range(n):
im=np.array(Image.open(imlist[i]))
R=np.mean(im[:,:,0].flatten())#红色通道
G=np.mean(im[:,:,1].flatten())#绿色通道
B=np.mean(im[:,:,2].flatten())#蓝色通道
features[i]=np.array([R,G,B])
tree=hcluster(features)
drawdendrogram(tree,imlist,jpeg='sunset.jpg')