您的位置:首页 > 编程语言

“集体智慧编程”之第三章:带偏好条件的聚类及聚类的展示方式

2017-05-19 20:17 639 查看


带偏好条件的聚类


含义

第五章讲求最优解的时候,也有一个涉及偏好的对宿舍分配人员的例子。我发现这个挺喜欢讲针对偏好的情况。然而,我个人认为这个针对偏好这一说法,还是比较复杂的,所以我认为最好不要试着去理解其词语背后深刻的含义,没有深刻的含义。举例子说明就很好办
有一个网站:Zebo(www.zebo.com),允许人们列出自己有拥有的物品和希望拥有的物品。我们今天的例子就是针对其列出来的信息做一次聚类。说实话,光凭感觉就觉得这些数据非常有价值,对其研究很值得。

数据集的获得可以自己从网上抓获,也可以直接使用书中为我们附带的zebo.txt文档,在zebo.txt文档中,只有列出用户希望拥有的物品:1/0,1表示想要,0表示不想要。例子暂时没有研究自己的已经拥有的物品,实际上我没有上过zebo,要列出自己需要的物品我觉这个还比较复杂,自己的东西那么多,怎么方便列的完呢?


相似度的计算

书中认为:皮尔逊相关度非常适合前几个例子的博客数据集,因为:单词的词频对应某篇博客是有具体的几个几个的。此处对于,无论是拥有物品或者想拥有的物品,却只有两个数,有或没有:1或0,想要或者不想要:1或0.所以我们使用Tanimoto系数计算两个用户的相关度。Tanimoto系统的具体含义在推荐的那篇博客里已经说清楚了,就是交集与并集的比率。
代码如下:

[python] view
plain copy

def tanimoto(v1,v2):

c1,c2,share=0,0,0

for i in range(len(v1)):

if v1[i]!=0:c1+=1#v1中有这个物品

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计算相似度

因为之前,我们已经写好了产生树状图的一系列代码。相当于现在我们只是调用一下之前写好的代码就可以产生结果。

调用代码:

[python] view
plain copy

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,其实也是可以用皮尔逊计算的,计算的代码:

[python] view
plain copy

<span style="font-family: Arial, Helvetica, sans-serif;">blognames,words,data=readfile('zebo.txt')</span>

[python] view
plain copy

clust=hcluster(data)

drawdendrogram(clust,blognames,jpeg='zebo皮尔逊计算相似度.jpg')

得到的大图:



分析其中一部分:



结果和tanimoto算出来的不太一样,但还是比较类似的,不过也都是很有趣的结果,比如想要frind的话也想要家人,也想要boyfriend


二维图




原理

树状图可以展示两个物品之间的相似情况和差距,比如最右边结合的必然是相似度最高的。然而,现在我们要展示一种更为方便的、直观的方式。说白了,就是把所有元素放在一张图上,然后离它近的他们的相似度就高,离的远的相似度就低。

原理:

书上管它叫多维缩放技术,使用这个技术,最终确定了元素怎么放在一张图上。

首先我们用相似度计算方式计算出各个元素之间的相似度(可以理解为博客名)。



然后我们把这几个元素随机的放在图上:如下图所示



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



上图距离不对,我们要让它们几个符合相似度的数值,比如A元素,应该离B,近一些,离C、D远一些。所以,对应A元素而已,就有一个受力的情况,然后我们就对A进行移动,使他满足与其他几个元素的相似度的数值。



然后反反复复,就可以直到所有的点都无法移动为止。


代码

计算每个元素的坐标,代码如下:

[python] view
plain copy

#可以随意修改相似度

def scaledown(data,distance=tanimoto,rate=0.01):

n(data)

#每一对数据项之间的由相似度计算出来的正确距离

#当然计算出来是一个二维数组,关键是为什么第一for循环就用n,第二个for循环就用(0,n)我觉得没区别吧

realdist=[[distance(data[i],data[j]) for j in range(n)] for i in range(0,n)]

outersum=0.0

#随机初始化节点在二维空间中的起始位置,一共有n个节点,所有loc是一个二维坐标数组,有n个(x,y)的形式

loc=[[random.random(),random.random()]for i in range(n)]

#用fakedist装初始位置的时候,各个节点之间的差距

fakedist=[[0.0 for j in range(n)] for i in range(n)]

lasterror=None

for m in range(0,1000):#最多循环1000次

for i in range(n):

for j in range(n):

#算的就是i和j之间的距离。这里是取了i节点的(x,y)和j节点的(x,y)

#不要被loc[i]迷惑了,其实他就是2,这里就相当于取了x坐标,然后相减再平凡,再取y坐标,相减再平方,最后都加起来。

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]

#每个节点都要根据误差的多少,按比例移离或者移向其他节点

#[k][0]代码x坐标,[k][1]代表y坐标

#这一段需要数学功底比较好的人才能处理吧

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

#根据rate参数和grad值相乘,移动每一个节点

for k in range(n):

loc[k][0]-=rate*grad[k][0]

loc[k][1]-=rate*grad[k][1]

return loc

在画出这些元素的代码如下:

[python] view
plain copy

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#为什么画布的大小为800,为什么x,y要乘以400

y=(data[i][1]+0.5)*400#实际上是实践得出来的,因为返回来的数都是-0.5到1.3之间,所有800的画布和乘以400比较合适,加0.5只是为了让图像尽可能的居中

draw.text((x,y),labels[i],(0,0,0))

img.show()

img.save(jpeg,'JPEG')

执行以下代码:

[python] view
plain copy

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

[python] view
plain copy

# -*- coding: cp936 -*-

def tanimoto(v1,v2):

c1,c2,share=0,0,0

for i in range(len(v1)):

if v1[i]!=0:c1+=1#v1中有这个物品

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

# -*- coding: cp936 -*-

def readfile(filename):

lines=[line for line in file(filename)]

#第一行是列标题,也就是被统计的单词是哪些

colnames=lines[0].strip().split('\t')[1:]#之所以从1开始,是因为第0列是用来放置博客名了

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:]])#data是一个列表,这个列表里每一个元素都是一个列表,每一列表的元素就是对应了colnames[]里面的单词

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是一个列表,列表里面是一个又一个biccluster的对象

clust=[bicluster(rows[i],id=i) for i in range(len(rows))]

while len(clust)>1:

lowestpair=(0,1)#先假如说lowestpair是0和1号

closest=distance(clust[0].vec,clust[1].vec)#同样将0和1的pearson相关度计算出来放着。

#遍历每一对节点,找到pearson相关系数最小的

for i in range(len(clust)):

for j in range(i+1,len(clust)):

#用distances来缓存距离的计算值

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)

#不在初始集合中的聚类,其id设置为负数

currentclustid-=1

del clust[lowestpair[1]]

del clust[lowestpair[0]]

clust.append(newcluster)

#当只有一个元素之后,就返回,这个节点相当于根节点

return clust[0]

def getheight(clust):

#这是一个叶节点吗?若是,则高度为1

if clust.left==None and clust.right ==None:return 1

#否则,高度为每个分支的高度之和

return getheight(clust.left)+getheight(clust.right)

def getdepth(clust):

#一个叶节点的距离是0.0,这是因为叶节点之后就没有了,将其放在最左边也没事

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)

#我们固定了宽度,所以需要对每一个节点的横向摆放做一个缩放,而不像高度一样,每一个叶节点都分配20

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#如果是第一次画点的话,top居然是最高点,也就是等于0。是上面边界。针对某一个节点,其高度就是左节点的高度加右节点的高度。

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

[python] view
plain copy

# -*- coding: cp936 -*-

from PIL import Image,ImageDraw

import random

# -*- coding: cp936 -*-

def readfile(filename):

lines=[line for line in file(filename)]

#第一行是列标题,也就是被统计的单词是哪些

colnames=lines[0].strip().split('\t')[1:]#之所以从1开始,是因为第0列是用来放置博客名了

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:]])#data是一个列表,这个列表里每一个元素都是一个列表,每一列表的元素就是对应了colnames[]里面的单词

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))])

#计算pearson相关系数

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去减它们

def tanimoto(v1,v2):

c1,c2,share=0,0,0

for i in range(len(v1)):

if v1[i]!=0:c1+=1#v1中有这个物品

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)

#每一对数据项之间的由相似度计算出来的正确距离

#当然计算出来是一个二维数组,关键是为什么第一for循环就用n,第二个for循环就用(0,n)我觉得没区别吧

realdist=[[distance(data[i],data[j]) for j in range(n)] for i in range(0,n)]

outersum=0.0

#随机初始化节点在二维空间中的起始位置,一共有n个节点,所有loc是一个二维坐标数组,有n个(x,y)的形式

loc=[[random.random(),random.random()]for i in range(n)]

#用fakedist装初始位置的时候,各个节点之间的差距

fakedist=[[0.0 for j in range(n)] for i in range(n)]

lasterror=None

for m in range(0,1000):#最多循环1000次

for i in range(n):

for j in range(n):

#算的就是i和j之间的距离。这里是取了i节点的(x,y)和j节点的(x,y)

#不要被loc[i]迷惑了,其实他就是2,这里就相当于取了x坐标,然后相减再平凡,再取y坐标,相减再平方,最后都加起来。

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]

#每个节点都要根据误差的多少,按比例移离或者移向其他节点

#[k][0]代码x坐标,[k][1]代表y坐标

#这一段需要数学功底比较好的人才能处理吧

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

#根据rate参数和grad值相乘,移动每一个节点

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#为什么画布的大小为2000,为什么x,y要乘以1000

y=(data[i][1]+0.5)*400#实际上是实践得出来的,因为返回来的数都是-0.5到1.3之间,所有2000的画布和乘以1000比较合适,加0.5只是为了让图像尽可能的居中

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')
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: