“集体智慧编程”之第三章:带偏好条件的聚类及聚类的展示方式
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] viewplain 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] viewplain 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')
相关文章推荐
- 集体智慧编程:第三章 聚类
- “集体智慧编程”之第三章:“发现群组”的 列聚类
- “集体智慧编程”之第三章:“发现群组”的 分级聚类
- 集体智慧编程中的分级聚类学习
- 集体智慧编程_聚类
- 集体智慧编程第三章之发现群组
- 【集体智慧编程】第三章 发现群组
- 集体智慧编程错误总结第三章
- 集体智慧编程——博客文章聚类-Python实现
- 【集体智慧编程】第三章、发现群组
- 算法总结(集体编程智慧) - 聚类、优化
- 集体智慧编程第三章 发现群组
- 集体智慧编程-K均值聚类代码理解
- 集体智慧编程学习之聚类系统
- 【集体智慧编程】第三章、发现群组
- 集体智慧编程 第三章 发现群组
- “集体智慧编程”之第三章:“发现群组”的 k均值聚类
- 读集体编程智慧所感
- 集体编程智慧(1)
- 【集体智慧编程 学习笔记】统计订阅源中的单词数