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

“集体智慧编程”之第三章:“发现群组”的 列聚类

2017-05-19 18:55 267 查看


什么是列聚类?

对博客数据使用了分级聚类的方式分析,我们可以又学的一种聚类方式:列聚类。刚刚的分级聚类就是对行进行了一个聚类。行是什么?行就是一个又一个的博客名,列是什么?列就是一个又一个的单词,那么进行行聚类的时候,我们是根据单词的词频将不同的博客聚了一次类。当我们对单词进行聚类的时候,我们就称之为列聚类。


意义何在

正面回答,我们知道了哪些单词会时常一起使用。对于这个列子来讲,似乎没什么意义。
但是如何行是消费者,列是购买的物品,那么每一行将是某位客户购买某一个商品的数量的列表数组,此时,如果去统计购买的物品的聚类,就非常有意义了。曾经有一个尿布与啤酒的故事,这两个毫无关系的商品居然销售量居然有着正相关的关系。后来发现是父亲来买尿布的时候往往会就会为自己买上一点啤酒,很显然这非常有意义,可以进行捆绑销售。在货物的摆放方面也更加有意义。


博客的例子

还是沿用之前那个数据集blogdata.txt。实现过程其实非常简单,简直用一句话就能说清:帮行和列做一个对换,然后直接调用之前写的过的函数。就可以产生聚类的结果。

首先是行列对置的函数:

[python] view
plain copy

def rotatematrix(data):

newdata=[]

for i in range(len(data[0])):

newrow=[data[j][i] for j in range(len(data))]

newdata.append(newrow)

return newdata

其他的都是调用了在分级聚类中使用过的代码。

执行以下代码可以得到结果:

[python] view
plain copy

blognames,words,data=readfile('blogdata.txt')

rdata=rotatematrix(data)

wordclust=hcluster(rdata)

drawdendrogram(wordclust,labels=words,jpeg='列聚类图.jpg')

结果是一幅图,首先看截取得其中的一部分。



我们可以看到,microsoft/software/windows和pro/mac/apple经常一起使用。相信在读这篇博客的人都知道上面留六个词的含义,当然software为什么会和windows经常一起用,看起来似乎有点不太合理,但是谁知道会不会又是像“尿布和啤酒”一样呢?

当然,并不是说图中所有词的聚类都具有意义,实际上,大多数恐怕都没有意义,或者看不出有什么意义。

最后得到的一张大图,由于csdn限制了图片宽度的像素。所以下一幅图看起来不完整,但是需要的朋友可以通过右键另存为图片观看。




总结

到此,整个列聚类的学习已经完成。我认为本次学习的重点并不是说代码多难写,原理有多难。

关键在于:对数据的充分利用,一种有效的思维的启示。当我们在对行进行聚类的时候,如果我没看过这本,恐怕也不知道对列聚类也能产生有意义的结果。在学习推荐的过程中,也遇到了这样一种情况,刚开始用户对电影的打分,转换为电影对用户的适应度(只是我这么称呼它而已),立刻就得到了很有意义的结果。

所以,请注意这个思维上的启示。


对项目的启示

和思维上的启示类似。当我拿到歌曲和标签的一个数据集的时候,如果我们可以记录用对某首歌曲打的标签的话,还是有意义的。比如说,A用户给歌曲x打了标签o。那么歌曲x对应标签o,我们就可以记录一次。因此,也可以用来找到类似歌曲和对标签进行聚类。

很显然,用户收藏的歌曲能够更直接的使用这种方法,用户为行,歌曲为列,能得到用户的聚类,歌曲为行,用户为列能够得到相似歌曲。用户收藏的艺人,用户收藏的专辑。这些都行。

下面两个问题:

那还有什么样的情况呢?

计算速度的问题。开

答案:

用心观察。

歌单不错,抓很多很多歌单,歌单里面有歌,如果列为歌单,行为歌,如果歌在这个歌单里面,就为1,不然就为0.那么又可以把歌的聚类做出来了把?

拿到数据集再分析。而且还要使用C/C++写代码,估计和python运行速度又不是一回事了。


对项目深思

学到此处,突然对我的项目有了很深的思考,总结如下(以后最后单开一篇博客):

我应该利用各种各样的手段提前算好相似歌,主要就是我的博客里面:聚类或者推荐的方式,单本文提到歌单-歌曲,用户-歌曲,歌曲-标签都可以用来计算出歌曲的相似度。
我们对每一首歌,保存10首相似度的歌,当然可以根据实际情况调整,这是为了防止如果这10歌中相似度最高的歌曲已经被用户收藏或者用户才收听了之类的情况。
然后我们用户收藏里的歌里挑几首出来,当然怎么挑,挑多少这些多可以实际考虑,增加各种各样的策略,比如用户最新收藏的5首。
然后找到这5首歌的全部的相似歌,也就50首,然后根据前天学推荐里面的,加权之类的搞一下,(最分,收藏了就是1,没收藏就是0,这里全是收藏的,也就是全是1),无论加权没加权(这里倒可以对用户收藏的不喜欢的歌,也就是听电台的时候删除的歌,与这50首歌的关系加进去,降低其权重),对着50首歌排个序,再过一下滤,取出排序中最高的一首,推给用户。

上述做法最关键的地方,我认为在速度完全搞得定。因为,我觉得我们必须对用户的实时操作做出反馈,包括用户正在听电台的时候,他删除了一首歌,那么我们必须马上在电台里播放下一首了吧?那么在即将推荐的这一首歌的产生过程中,我希望能够将刚刚删除的那一首歌的影响带入算法里面。同样,如果用户点击下一曲呢?点击了收藏呢?我都希望这些操作影响用户马上听到的歌曲的产生过程。

那么实际上速度上是否反应的过来,肯定需要更多的工程的实践。在这里是只是一个想法。不过我认为在检索速度上还是有很大的可能性来的及,这是因为我观察了一下豆瓣的电台,他里面有个选项就是让我选华语电台、粤语电台、欧美电台,每个电台都写的有总歌曲数,3个电台都不超过1万,那么我就算是3万吧,如果这三万歌,每一首歌对应十首相似度歌,那么有一张表来存这个关系,并且还有一个字段是相似度,也就是一个0到1的小数。现在也就是从30万条数据搜50条出来。我觉得肯定跟得上吧?如果在用户点击的0.1秒内出现下一首的话。
当然实际上这些有点过多的过滤,如果我们能宽容一点,再用户实时操作的下一首的下一首歌来完成上面刚刚所说的操作,那我认为应该轻松应对,因为最长的时间几分钟,一首歌听完,最短时间就是也得有个几秒,除非用户一定点那个下一曲,下一曲,那都有一点暴力测试的感觉了。我们产生的推荐列表在没有被更新之前可以一直保留,比如就刚刚那50首。

呼`````恩,上述想法我觉得很好。哈哈。


全部代码

[python] view
plain copy

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

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

#图中每一个点都是一个该类的对象,而其中叶节点显然就是原始数据,而枝节点的数据主要来自其叶节点的均值。

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=pearson):

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

#blognames,words,data=readfile('blogdata.txt')

#clust=hcluster(data)

#printclust(clust,labels=blognames)

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

def rotatematrix(data):

newdata=[]

for i in range(len(data[0])):

newrow=[data[j][i] for j in range(len(data))]

newdata.append(newrow)

return newdata

blognames,words,data=readfile('blogdata.txt')

#rdata=rotatematrix(data)

#wordclust=hcluster(rdata)

#drawdendrogram(wordclust,labels=words,jpeg='列聚类图.jpg')

print len(data[0])
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: