第三章发现群组
这章中的主要内容:
- 从各种不同的来源中构造算法所需的数据
- 两种不同的聚类算法;
- 更多有关距离度量(distance metrics)的知识
- 简单的图形可视化代码,用以观察所生成的群组
- 如何将异常复制的数据集投影到二维空间中
监督学习和无监督学习
利用样本输入和期望输出来学习如何预测的技术被称为监督学习法(supervised learning methods)。例如神经网络、决策树、向量支持机、贝叶斯过滤。当我们想要利用这些方法中的任何一种来提取信息时,我们可以传入一组输入,然后期望应用程序能够根据其此前学到的知识来产生一个输出。
聚类是无监督学习。与神经网络或决策树不同,无监督学习算法不是利用带有正确答案的样本数据进行“训练”。它们的目的是要在一组数据中找寻某种结构,而这些数据本身不是我们要找的答案。
对博客用户进行分类
对订阅源中的单词进行计数
对wc={}的定义不解,原来是python中的字典类型的定义。这篇文章有讲解,关于feedparser的用法解释
RSS订阅源是一个包含博客及其所有文章条目信息的简单XML文档。为了给每个博客中的单词技术,首先第一步就是要解析这些订阅源
import feedparser
import re
#返回一个RSS订阅源的标题和包含单词统计情况的字典
def getwordcounts(url):
#解析订阅源
d=feedparser.parse(url)
wc={
}
#循环遍历所有的文章条目
for e in d.entries:
if 'summary' in e: summary = e.summary
else: summary=e.description
#提取一个单词列表
words=getwords(e.title+' '+summary)
for word in words:
wc.setdefault(word,0)
wc[word]+=1
return d.feed.title,wc
每个RSS和Atom订阅源都会包含一个标题和一组文章条目。通常,每个文章条目都有一个摘要,或者是包含了条目中世纪文本的描述性标签。函数getwordcounts将摘要传给函数getwords,后者会将其中所有的HTML标记剥离掉,并以非字母字符作为分隔符拆分出单词,再将结果以列表的形式加以返回。
generatefeedvector.py文件中的主体代码(这些代码不单独构成一个函数)循环遍历订阅源并生成数据集。代码的第一部分遍历feedlist.txt文件中的每一行,然后生成针对每个博客的单词统计,以及出现这些单词的博客数目(apcount)。
apcount={
}
wordcounts={
}
feedlist=[line for line in open('data/feedlist.txt')]
for feedurl in feedlist:
title,wc=getwordcounts(feedurl)
wordcounts[title]=wc
for word,count in wc.items():
apcount.setdefault(word,0)
if count>1:
apcount[word]+=1
下一步,建立一个单词列表,将其实际用于针对每个博客的单词计数。因为想“the"这样的单词几乎到处都是,而像”flim-flam"这样的单词则有可能只出现在个别博客中,所以通过只选择介于某个百分比范围内的单词,我们可以减少需要考查的单词总量。在本例中,我们可以将10%定为下届,将50%定为上届,不过假如你发现有过多常见或鲜见的单词出现,不妨尝试不同的边界值。
wordlist=[]
for w,bc in apcount.items():
frac=float(bc)/len(feedlist)
if frac>0.1 and frac<0.5: wordlist.append(w)
最后,我们利用上述单词列表和博客列表来建立一个文本文件,其中包含一个大矩阵,记录着针对每个博客的所有单词的统计情况:
out=open('data/blogdata.txt','w')
out.write('Blog')
for word in wordlist: out.write('\t%s' % word)
out.write('\n')
for blog,wc in wordcounts.items():
out.write(blog)
for word in wordlist:
if word in wc: out.write('\t%d' % wc[word])
else: out.write('\t0')
out.write('\n')
网络问题feedparser的访问没有返回值,还好网上能找到对应的数据,之后会放到GitHub中。
大矩阵的大概样子就是这样的
分级聚类
分级聚类通过连续不断地将最为相似地群组两两合并,来构造一个群组地层级结构。其中地每个群组都是从单一元素开始地,在本章地例子中,这个单一元素就是博客。在每次迭代地过程中,分级聚类算法会计算每两个群组间地距离,并将距离最近的两个群组合并成一个新的群组。这一过程会一直重复下去,知道只剩一个群组为止。
待分级聚类完成之后,我们可以采用一种图形化的方式来展现所得的结果,这种图被称为树状图
我们将示范如何对博客数据集进行聚类,以构造博客的层级结构;如果构造成功,我们将实现按主题对博客进行分组。首先,我们需要一个方法来加载数据文件。新建一个名为clusters.py的文件
def readfile(filename):
lines=[line for line in open(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
上述函数将数据集中的头一行数据读入一个代表列名的列表,并将最左边一列读入一个代表行名的列表,最后它又将剩下的所有数据都放入一个大列表,其中的每一项对应于数据集中的一行数据。数据集中任一单元格内的计数值,都可以由一个行号和列号来唯一定位,此行号和列号同时还对应于列表rownames和colnames中的索引。
在本章的例子中,一些博客比其他博客包含更多的文章条目,或者文章条目的长度比其他博客的更长,这样会导致这些博客在总体上比其他博客包含更多的词汇。皮尔逊相关度可以纠正这一问题,因为它判断的其实是两组数据与某条直线的拟合程度。此处,皮尔逊相关度的计算代码将接受两个数字列表作为参数,并返回这两个列表的相关度分值:
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))