带偏好条件的聚类
含义
第五章讲求最优解的时候,也有一个涉及偏好的对宿舍分配人员的例子。我发现这个挺喜欢讲针对偏好的情况。然而,我个人认为这个针对偏好这一说法,还是比较复杂的,所以我认为最好不要试着去理解其词语背后深刻的含义,没有深刻的含义。举例子说明就很好办
有一个网站:Zebo(www.zebo.com),允许人们列出自己有拥有的物品和希望拥有的物品。我们今天的例子就是针对其列出来的信息做一次聚类。说实话,光凭感觉就觉得这些数据非常有价值,对其研究很值得。
数据集的获得可以自己从网上抓获,也可以直接使用书中为我们附带的zebo.txt文档,在zebo.txt文档中,只有列出用户希望拥有的物品:1/0,1表示想要,0表示不想要。例子暂时没有研究自己的已经拥有的物品,实际上我没有上过zebo,要列出自己需要的物品我觉这个还比较复杂,自己的东西那么多,怎么方便列的完呢?
相似度的计算
书中认为:皮尔逊相关度非常适合前几个例子的博客数据集,因为:单词的词频对应某篇博客是有具体的几个几个的。此处对于,无论是拥有物品或者想拥有的物品,却只有两个数,有或没有:1或0,想要或者不想要:1或0.所以我们使用Tanimoto系数计算两个用户的相关度。Tanimoto系统的具体含义在推荐的那篇博客里已经说清楚了,就是交集与并集的比率。
代码如下:
- def tanimoto(v1,v2):
- c1,c2,share=0,0,0
-
-
- for i in range(len(v1)):
- if v1[i]!=0:c1+=1
- if v2[i]!=0:c2+=1
- if v1[i]!=0 and v2[i]!=0:share+=1
-
-
- return 1.0-(float(share)/(c1+c2-share))
对相似度计算的感悟
对应哪个计算方式好,书中的意思是在针对0/1的情况时,用Tanimoto系统好,但是其实我发现Tanimoto系数算出来的和我想象中不太一样,而又比较倾向与想象中的使用方式,当然后来向师哥证实,这确实是一种公式,只是我还没学习过。
想象中的方式是:并集乘以2/交集。但这个交集不要减去并集。
比如,用1,2...代替物品,组成集合
A用户:[1,2,3,4,5,6]
B用户:[2,4,6,7,8,9]
显然两者并集为[2,4,6],供三个元素。
两者相似度,用我的想法算出来就(3*2)/12=0.5
从集合内观察,我觉得有一半是一样的,所以就是0.5
然而如果用Tanimoto计算的话,就是3/(12-3)=0.33333333。这就是使我困惑的地方。
因为用Tanimoto计算出来的相似不是0.5,而且觉得目测就该是0.5。
此外,在我推荐那篇博客里已经证实:就算用Pearson计算出来的结果也不会差太差,也就是pearson也可以用来计算1/0这种的情况。实际上书中在在推荐那一章中对标签的处理也使用了pearson公式。我认为现在纠结这样相似度的计算意义不是特别大,因为真实的数据我还没有到手,到手之后,我们再比较不同的公式,改进不同的公式,这才是非常有必要的。
聚类展示
树状图
Tanimoto计算相似度
因为之前,我们已经写好了产生树状图的一系列代码。相当于现在我们只是调用一下之前写好的代码就可以产生结果。
调用代码:
- wants,people,data=readfile('zebo.txt')
- clust=hcluster(data,distance=tanimoto)
- drawdendrogram(clust,wants,jpeg='zebo树状图.jpg')
我们可以得到下一幅图:
我们截取其中一部分来看,会有有趣的发现:
很有意思吧,想要鞋子的想要衣服,想要psp的想要tv,想要xbox360的想要ps3。想要laptop的也想要MP3。没看懂为什么想要playstation 3想要马。
皮尔逊计算相似度
其实虽然数据部分只是0和1,其实也是可以用皮尔逊计算的,计算的代码:
- <span style="font-family: Arial, Helvetica, sans-serif;">blognames,words,data=readfile('zebo.txt')</span>
- clust=hcluster(data)
- drawdendrogram(clust,blognames,jpeg='zebo皮尔逊计算相似度.jpg')
得到的大图:
分析其中一部分:
结果和tanimoto算出来的不太一样,但还是比较类似的,不过也都是很有趣的结果,比如想要frind的话也想要家人,也想要boyfriend
二维图
原理
树状图可以展示两个物品之间的相似情况和差距,比如最右边结合的必然是相似度最高的。然而,现在我们要展示一种更为方便的、直观的方式。说白了,就是把所有元素放在一张图上,然后离它近的他们的相似度就高,离的远的相似度就低。
原理:
书上管它叫多维缩放技术,使用这个技术,最终确定了元素怎么放在一张图上。
首先我们用相似度计算方式计算出各个元素之间的相似度(可以理解为博客名)。
然后我们把这几个元素随机的放在图上:如下图所示

这个几个在图上元素有距离吧?就用差平方之和算,如下图所示,

上图距离不对,我们要让它们几个符合相似度的数值,比如A元素,应该离B,近一些,离C、D远一些。所以,对应A元素而已,就有一个受力的情况,然后我们就对A进行移动,使他满足与其他几个元素的相似度的数值。
然后反反复复,就可以直到所有的点都无法移动为止。
代码
计算每个元素的坐标,代码如下:
-
- def scaledown(data,distance=tanimoto,rate=0.01):
- n(data)
-
-
-
-
- realdist=[[distance(data[i],data[j]) for j in range(n)] for i in range(0,n)]
-
-
- outersum=0.0
-
-
-
- loc=[[random.random(),random.random()]for i in range(n)]
-
- fakedist=[[0.0 for j in range(n)] for i in range(n)]
-
-
- lasterror=None
- for m in range(0,1000):
- for i in range(n):
- for j in range(n):
-
-
- fakedist[i][j]=sqrt(sum([pow(loc[i][x]-loc[j][x],2) for x in range(len(loc[i]))]))
-
-
- grad=[[0.0,0.0] for i in range(n)]
-
- totalerror=0
- for k in range(n):
- for j in range(n):
- if j==k:continue
-
-
- errorterm=(fakedist[j][k]-realdist[j][k])/realdist[j][k]
-
-
-
-
- grad[k][0]+=((loc[k][0]-loc[j][0])/fakedist[j][k])*errorterm
- grad[k][1]+=((loc[k][1]-loc[j][1])/fakedist[j][k])*errorterm
-
- totalerror+=abs(errorterm)
- print totalerror
-
-
-
- if lasterror and lasterror<totalerror:break
- lasterror=totalerror
-
- for k in range(n):
- loc[k][0]-=rate*grad[k][0]
- loc[k][1]-=rate*grad[k][1]
- return loc
在画出这些元素的代码如下:
- def draw2d(data,labels,jpeg='mds2d.jpg'):
- img=Image.new('RGB',(800,800),(255,255,255))
- draw=ImageDraw.Draw(img)
- for i in range(len(data)):
- x=(data[i][0]+0.5)*400
- y=(data[i][1]+0.5)*400
- draw.text((x,y),labels[i],(0,0,0))
- img.show()
- img.save(jpeg,'JPEG')
执行以下代码:
- blognames,words,data=readfile('zebo.txt')
- coords=scaledown(data)
- print coords
- draw2d(coords,blognames,jpeg='zebo二维图 Tanimoto相似度2.jpg')
结果图
可以得到想要的结果:
我对博客数据执行了一次,对zebo数据执行了4次,两次pearson相关度计算,两次Tanimoto相关度计算。
博客数据的图:

zebo数据
共四份,两份是用Tanimoto计算,两份用pearson计算
首先是pearson计算的:
第二份
Tanimoto计算:
第二份:
针对zebo的结果,结论如下:
产生的结果和最初随机产生的位置很有关系,所以每次都一样
产生的结果有点不如预期,因为在树状图中,无论如何改,鞋子和衣服相似度总是很高的,然后在二维图中看,无论改了相似度计算方式还是多试几次,鞋子和衣服我觉得都可以远。当然是不是还有什么关键我没有察觉到。
启示
针对项目好像暂时没想到有什么帮助。不过在学个途中,突然想起了k均值聚类,我发现还是对项目很有帮助的。因为我们不是要做一个页面让用户选艺人或者是标签吗?比如一个页面出10个艺人,最多10页,那么我们就可以使用k均值聚类,要求分10类。然后每一页从每一个类中选一个。就能预防一个页面出现了同类型的艺人。
总的来说,在聚类时,我们针对不同的情况,应该多考虑几种不同的相似度计算方式。多考虑几种结果描述的方式:树状图、二维图,然后再结合项目的实际情况来选择。
全部源代码
myClustersOfPreferences
-
- def tanimoto(v1,v2):
- c1,c2,share=0,0,0
-
- for i in range(len(v1)):
- if v1[i]!=0:c1+=1
- if v2[i]!=0:c2+=1
- if v1[i]!=0 and v2[i]!=0:share+=1
-
- return 1.0-(float(share)/(c1+c2-share))
- from PIL import Image,ImageDraw
-
- def readfile(filename):
- lines=[line for line in file(filename)]
-
-
- colnames=lines[0].strip().split('\t')[1:]
- rownames=[]
- data=[]
- for line in lines[1:]:
- p=line.strip().split('\t')
-
- rownames.append(p[0])
-
- data.append([float(x) for x in p[1:]])
- return rownames,colnames,data
-
-
-
- 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
- self.distance=distance
-
- def hcluster(rows,distance=tanimoto):
- distances={}
- currentclustid=-1
-
-
-
- clust=[bicluster(rows[i],id=i) for i in range(len(rows))]
-
- 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)):
-
- if(clust[i].id,clust[j].id) not in distances:
- distances[(clust[i].id,clust[j].id)]=distance(clust[i].vec,clust[j].vec)
- d=distances[(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)
-
-
- currentclustid-=1
- del clust[lowestpair[1]]
- del clust[lowestpair[0]]
- clust.append(newcluster)
-
-
- return clust[0]
-
-
-
- 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
-
- def drawdendrogram(clust,labels,jpeg='clusters.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,(h/2),scaling,labels)
- img.save(jpeg,'JPEG')
-
- 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))
-
- wants,people,data=readfile('zebo.txt')
- clust=hcluster(data,distance=tanimoto)
- drawdendrogram(clust,wants,jpeg='zebo树状图.jpg')
MyViewDataInTwoDimensions
-
- from PIL import Image,ImageDraw
- import random
-
- def readfile(filename):
- lines=[line for line in file(filename)]
-
-
- colnames=lines[0].strip().split('\t')[1:]
- rownames=[]
- data=[]
- for line in lines[1:]:
- p=line.strip().split('\t')
-
- rownames.append(p[0])
-
- data.append([float(x) for x in p[1:]])
- return rownames,colnames,data
-
-
- from math import sqrt
- 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))])
-
-
- 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 tanimoto(v1,v2):
- c1,c2,share=0,0,0
-
- for i in range(len(v1)):
- if v1[i]!=0:c1+=1
- if v2[i]!=0:c2+=1
- if v1[i]!=0 and v2[i]!=0:share+=1
-
- return 1.0-(float(share)/(c1+c2-share))
-
- def scaledown(data,distance=tanimoto,rate=0.01):
- n=len(data)
-
-
-
- realdist=[[distance(data[i],data[j]) for j in range(n)] for i in range(0,n)]
-
- outersum=0.0
-
-
- loc=[[random.random(),random.random()]for i in range(n)]
-
- fakedist=[[0.0 for j in range(n)] for i in range(n)]
-
- lasterror=None
- for m in range(0,1000):
- for i in range(n):
- for j in range(n):
-
-
- fakedist[i][j]=sqrt(sum([pow(loc[i][x]-loc[j][x],2) for x in range(len(loc[i]))]))
-
-
- grad=[[0.0,0.0] for i in range(n)]
-
- totalerror=0
- for k in range(n):
- for j in range(n):
- if j==k:continue
-
-
- errorterm=(fakedist[j][k]-realdist[j][k])/realdist[j][k]
-
-
-
-
- grad[k][0]+=((loc[k][0]-loc[j][0])/fakedist[j][k])*errorterm
- grad[k][1]+=((loc[k][1]-loc[j][1])/fakedist[j][k])*errorterm
-
- totalerror+=abs(errorterm)
- print totalerror
-
-
- if lasterror and lasterror<totalerror:break
- lasterror=totalerror
-
- for k in range(n):
- loc[k][0]-=rate*grad[k][0]
- loc[k][1]-=rate*grad[k][1]
- return loc
-
- def draw2d(data,labels,jpeg='mds2d.jpg'):
- img=Image.new('RGB',(800,800),(255,255,255))
- draw=ImageDraw.Draw(img)
- for i in range(len(data)):
- x=(data[i][0]+0.5)*400
- y=(data[i][1]+0.5)*400
- draw.text((x,y),labels[i],(0,0,0))
- img.show()
- img.save(jpeg,'JPEG')
-
-
-
- blognames,words,data=readfile('zebo.txt')
- coords=scaledown(data)
- print coords
- draw2d(coords,blognames,jpeg='zebo二维图 Tanimoto相似度2.jpg')