第三章是第二章的扩展,引入了“数据聚类”的概念,这是一种用以寻找紧密相关的事、人或观点,并将其可视化的方法。
聚类(data intensive)时常被用于数据量很大的应用中。本章第一个例子将对博客用户所讨论的话题,以及特殊词缀进行考察。第二个例子将对社区网站进行考察。
==================================================================================================
监督学习和无监督学习
之前在《机器学习实战》里面就看过一部分了,这里再复习一下。
利用样本输入和期望输出来学习如何预测的技术被称为监督学习法。包括神经网络、决策树、SVM,以及贝叶斯过滤。
聚类是无监督学习的一个例子。无监督学习算法不是利用带有正确答案样本数据进行“训练”。它们的目的是要在一组数据中找寻某种结构,而这些数据本身不是我们要找的答案。
==================================================================================================
对博客用户进行分类
本章我们将对两个示例数据集进行处理。第一个数据集中,被用来聚类的是排名前120位的博客。
为了对这些博客聚类,我们需要的是一组指定的词汇在每个博客订阅源中出现的次数。有关这一数据的一个小范围的子集。
根据单词出现的频率对博客进行聚类,对于搜索,分类和挖掘都是非常有价值的。
为了构造这样的数据集,需要下载一系列博客订阅源,然后处理。也可以从网上下载,不过书上给的网站似乎不见了,我们从网
上下载的代码包里面有 feedlist 文件。
==================================================================================================
对订阅源中的单词进行计数
这章跳过。因为下载的代码本身就包含了数据了。
==================================================================================================
分级聚类
上图中元素的相似程度通过位置体现。越近越相似。
不得不说一下,我感觉我要少打字,而是以跑代码为主。
建立一个名为 clusters.py 的文件。
# -*-coding:utf-8 -*-
def readfile(filename):
lines = [line for line in file(filename)]
# 第一行是标题
colnames = lines[0].strip().split('\t')[1:]
# Python strip() 方法用于移除字符串头尾指定的字符(默认为空格)
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))])
#计算 r (pearson score)
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.0 减去皮尔逊相关度之和的结果
皮尔逊相关度的计算结果在两者完全匹配的情况下为1.0,在两者毫无关系的情况下为0.0。
新建一个bicluster 类,将所有的这些属性存放其中,并以此来描述这颗层级树。
# 新建一个类,代表“聚类”这一类型
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
把 hcluster 算法加入:
def hcluster(rows, distance = pearson):
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) # .vec 在 class 里面定义了
# 遍历每一个配对,寻找最小距离
for i in range(len(clust)):
for j in range(i+1, len(clust)):
# 用 distance 来缓存距离的计算值
if (clust[i].id, clust[j].id) not in distances: # .vec 在 class 里面定义了
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用于list列表操作,删除一个或者连续几个元素
del clust[lowestpair[0]]
clust.append(newcluster)
return clust[0]
运行算法,我们要写一个 run1.py
import clusters
blognames, words, data = clusters.readfile('blogdata.txt')
clust = clusters.hcluster(data)
现在添加一个 printclust 函数到 clusters.py 中:
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)
print clusters.printclust(clust,labels = blognames)
就可以输出了。
结果太长我只放一部分。
=====================================================================
思考了一下,感觉理解最重要,于是先跑书本吧。讲真打程序和纠错太花时间了。程序放的是标准示例。
=====================================================================
负数标记代表那是一个分支,就是前面的短破折号。后面的具体文字就是叶节点。
接下来绘制树状图,我们需要下载 PIL,可以从网上下载。但是由于当初安装的时候是从Anaconda直接安装,所以这个库已经有了。
文件第一句话加入:
from PIL import Image,ImageDraw
然后:
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
函数 drawdendrogram 为每一个最终生成的聚类创建一个高度为20像素、宽度固定的图片。
该函数为图片建立相应的draw对象,然后在根节点调用 drawnode 函数。
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) # drawnode 函数
img.save(jpeg,'JPEG')
drawnode 接受一个聚类及其位置作为输入参数。函数取到子节点的高度,计算出这些节点所在位置,然后用线条将其连接起来。、线条越长越表明合并在一起的两个聚类差别很大,线条越短则越表明两个聚类相似度高。
当然我们要把 drawnode 加入到文件中。
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))
然后在运行脚本里面添加:
clusters.drawdendrogram(clust, blognames, jpeg = 'blogclust.jpg')
问题在于这里不管有没有print 似乎都没有出现那个图片。。不知道怎么回事
不过问题不大。
===============================================================================
列聚类
同时在行列聚类很有意义。
在博客数据中,列聚类代表的是单词。
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
从代码中可以看出,首先把整个数据集转置,使列变为行。每一行对应一组数字(以前是博客),数字指明了某个单词在每篇博客中的出现次数。
单词数量比博客要多,所以花费时间更长。在运行文件里面添加:
print '===========================================\n'
rdata = clusters.rotatematrix(data)
wordclust = clusters.hcluster(rdata)
clusters.drawdendrogram(wordclust, labels = words, jpeg = 'wordclust.jpg')
当然,还是没有图片。。。郁闷,使用print wordclust命令更是内存出错?看了看听说是像素点不够,没法输出。
http://blog.youkuaiyun.com/sunflower606/article/details/45870379 这个参考博客不错。
====================================================================
K-均值聚类
除了分级聚类以外,还有一种可供选择的聚类方法是K-均值聚类。和分级聚类不同,事先我们会告诉算法希望生成的聚类数量,然后算法会根据结构状况来确定聚类大小。
其实原理没多大意思,之前在《机器学习实战》里面已经初步了解了。
import random
def kcluster(rows,distance=pearson,k=4):
# 确定每个点的最小值和最大值
ranges=[(min([row[i] for row in rows]),max([row[i] for row in rows]))
for i in range(len(rows[0]))]
# 随机创建 k 个中心点
clusters=[[random.random()*(ranges[i][1]-ranges[i][0])+ranges[i][0]
for i in range(len(rows[0]))] for j in range(k)]
lastmatches=None
for t in range(100):
print 'Iteration %d' % t
bestmatches=[[] for i in range(k)]
# 在每一行中寻找距离最近的中心点
for j in range(len(rows)):
row=rows[j]
bestmatch=0
for i in range(k):
d=distance(clusters[i],row)
if d<distance(clusters[bestmatch],row): bestmatch=i
bestmatches[bestmatch].append(j)
# 如果结果与上一次相同,则整个过程结束
if bestmatches==lastmatches: break
lastmatches=bestmatches
# 把中心点移到其所有成员的平均位置处
for i in range(k):
avgs=[0.0]*len(rows[0])
if len(bestmatches[i])>0:
for rowid in bestmatches[i]:
for m in range(len(rows[rowid])):
avgs[m]+=rows[rowid][m]
for j in range(len(avgs)):
avgs[j]/=len(bestmatches[i])
clusters[i]=avgs
return bestmatches
针对博客数据集实验一下该数据,由于不需要前面的输出,所以之前的东西都注释掉:
# -*- coding:utf-8 -*-
import clusters
blognames, words, data = clusters.readfile('blogdata.txt')
clust = clusters.hcluster(data)
'''
print clusters.printclust(clust,labels = blognames)
print clusters.drawdendrogram(clust, blognames, jpeg = 'blogclust.jpg') # 不知道为啥没有图片
print '===========================================\n'
rdata = clusters.rotatematrix(data)
wordclust = clusters.hcluster(rdata)
print wordclust
clusters.drawdendrogram(wordclust, labels = words, jpeg = 'wordclust.jpg')
'''
print '===========================================\n'
kclust = clusters.kcluster(data, k = 10) # 可以尝试不同的 k 聚类
print [blognames[r] for r in kclust[0]]
print '===========================================\n'
print [blognames[r] for r in kclust[1]]
我们可以看见结果是:
可以尝试不同的 k 进行聚类。
====================================================================
这里新的一节要收集zebo的数据进行分析,但是我们已经有了,就在文件 zebo.txt 里面,所以直接跳过下载数据的阶段。
定义距离度量标准
皮尔逊相关度适合于博客数据集,包含的是单词的实际统计值。但是这里的数据集只有0和1两种取值,代表无、有。
我们在此采用一种被称为 Tanimoto 系数的度量方法,代表的是交集与并集的比率。利用向量我们定义这样的度量:
def tanimoto(v1,v2):
c1,c2,shr=0,0,0
for i in range(len(v1)):
if v1[i]!=0: c1+=1 # 在 v1 出现
if v2[i]!=0: c2+=1 # 在 v2 出现
if v1[i]!=0 and v2[i]!=0: shr+=1 # 两个向量都出现
# 返回一个介于 1.0 到 0.0 之间的值
# 1.0 代表不存在同时喜欢两件物品的人
# 0.0 代表所有人都同时喜欢两个向量中的物品
return 1.0-(float(shr)/(c1+c2-shr))
注意 是tanimoto 不是 tanamoto,实例程序写错了,书上是对的。
==============================================================对结果聚类
利用上面的函数并相应传入两个向量,我们可以实现聚类,在run1添加:
wants, people, data = clusters.readfile('zebo.txt')
clust = clusters.hcluster(data, distance = clusters.tanimoto)
clusters.drawdendrogram(clust, wants)
然后,,,并没有什么图片,当然也不能指望打印出来,因为太多了。
书上说改变搜索条件也许有更有意思的发现。这里略过
=================================================================
以二维形式展现数据
本届要介绍一种 多维缩放 技术,利用这个技术我们可以为数据集找到一种二维表达形式。
算法根据每对数据项之间的差距情况,尝试绘制一幅图。具体见书。P49
数据矩阵的距离就是实际距离。
简单说就是将目标距离和当前距离进行比较,并求出一个误差值,根据误差的情况再按照比例将每个数据项的所在位置移近或移远少许。
程序如下:
def scaledown(data,distance=pearson,rate=0.01):# 皮尔逊相关系数
n=len(data)
# 每一对数据项之间的真实距离
realdist=[[distance(data[i],data[j]) for j in range(n)]
for i in range(0,n)]
# 随机初始化节点在二维空间中的起始位置
loc=[[random.random(),random.random()] for i in range(n)] # 全随机只有i?
fakedist=[[0.0 for j in range(n)] for i in range(n)] # 初始全0
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 # and 前面不太懂
lasterror=totalerror
# 根据 rate 参数与 grad 值相乘的结果,移动每一个节点
for k in range(n):
loc[k][0]-=rate*grad[k][0] #这里 loc 应该是一个k行2列的矩阵
loc[k][1]-=rate*grad[k][1]
return loc
最后利用 PIL 生成一幅图,根据新的坐标值,在图上标出所有数据项的位置及其对应的标签。
def draw2d(data,labels,jpeg='mds2d.jpg'):
img = Image.new('RGB',(2000,2000),(255,255,255))
draw = ImageDraw.Draw(img)
for i in range(len(data)):
x=(data[i][0]+0.5)*1000
y=(data[i][1]+0.5)*1000
draw.text((x,y),labels[i],(0,0,0))
img.save(jpeg,'JPEG')
运行倒是没问题,就是不知道为啥没有图片,跑哪去了?
尴尬。