《集体智慧编程》 第三章 发现群组 学习笔记

啦啦啦聚类算法~这一章我学得比较迷糊,还需要反复理解琢磨。

我刚看到这一章的时候内心是崩溃的,许多傻瓜软件点一下鼠标就能完成的事儿,到书里这一章需要许多行代码来完成,也说明了,学数据挖掘,算法real重要。。

本章需要安装:

feedparser(第二章安装pydelicious已经安装过了,pip install即可)

BeautifulSoup,

Beautiful Soup 是用Python写的一个HTML/XML的解析器,它可以很好的处理不规范标记并生成剖析树(parse tree)。 它提供简单又常用的导航(navigating),搜索以及修改剖析树的操作。它可以大大节省你的编程时间。

下载:http://www.crummy.com/software/BeautifulSoup/bs4/download/4.2/

解压:tar -xzvf beautifulsoup4-4.2.0.tar.gz

cmd进入解压目录,输入python setup.py install

注意: 导入beautifulsoup应该输入

from bs4 import BeautifulSoup

输入 import beautifulsoup我这儿会报错。

PIL,下载地址:http://pythonware.com/products/pil/

还涉及到一些正则表达式的知识,非常非常强烈推荐下面这个教程,写得很好:

www.cnblogs.com/huxi/archive/2010/07/04/1771073.html

一.监督学习和无监督学习

https://www.zhihu.com/question/23194489

二.单词向量

(一)对博客用户进行分类

(二)对订阅源中的单词进行计数

#基础导入
import feedparser  #用来解析RSS订阅源(XML文档),可以就从RSS或Atom订阅源中得到标题链接和文章的条目了
import re  #正则表达式
# 返回一个RSS订阅源的标题和包含单词计数情况的字典
def getwordcounts(url):
  # 解析订阅源
  d=feedparser.parse(url)  #传入的是博客的rss地址,这时候rss的全部内容就都在d里面了
  wc={}

  # 遍历所有文章条目
  for e in d.entries:   #d.entries:文章条目
    if 'summary' in e:
      summary=e.summary
    else:
      summary=e.description   #summary=文章内容

    # 提取一个单词列表
    words=getwords(e.title+' '+summary)  #getwords(题目+空格+文章)
    for word in words:
      wc.setdefault(word,0)#如果键在字典中,返回这个键所对应的值。如果键不在字典中,向字典中插入这个键,并且以default为这个键的值,并返回 default。default的默认值为None
      wc[word]+=1  #得到字典wc类似{u'limited': 1, u'all': 5, u'searchable': 1, u'results': 1, u'browsers': 2}
  return d.feed.title,wc  #返回 博客订阅源,字典wc
def getwords(html):
  #去除所有HTML标记:<XXXXXXX>
  txt=re.compile(r'<[^>]+>').sub('',html)
  #re.compile(pattern[, flags])作用:把正则表达式语法转化成正则表达式对象。r是raw(原始)的意思。因为在表示字符串中有一些转义符,如表示回车'\n'。如果要表示\表需要写为'\\'。但如果我就是需要表示一个'\'+'n',不用r方式要写为:'\\n'。但使用r方式则为r'\n'这样清晰多了。
  #re.sub(pattern, repl, string, count=0, flags=0)

  # 利用非字母字符拆分出单词  split()通过指定分隔符对字符串进行切片
  words=re.compile(r'[^A-Z^a-z]+').split(txt)

  # 转换成小写模式
  return [word.lower() for word in words if word!='']
apcount={} #出现某单词的博客数目
wordcounts={}
feedlist=[line for line in file('feedlist.txt')]  #建立一个包含feedlist.txt中每一个url的列表
for feedurl in feedlist:
  try:
    title,wc=getwordcounts(feedurl)   #title,wc类似Google Blogoscoped {u'limited': 1, u'all': 5, u'searchable': 1, u'results': 1, u'browsers': 2}
    wordcounts[title]=wc  #得到wordcounts类似{u'Google Blogoscoped': {u'limited': 1, u'all': 5, u'searchable': 1, u'results': 1, u'browsers': 2}
    for word,count in wc.items():  #items()方法返回字典的(键,值)元组对的列表;wc.items=[(词汇,计数),(词汇,计数)]
      '''得到:
      词汇 计数
      词汇 计数'''
      apcount.setdefault(word,0)  #此时 apcount={word:0}
      if count>1:
        apcount[word]+=1  #得到apcount类似{u'limited': 0, u'all': 1, u'searchable': 0, u'results': 0}
  except:
    print 'Failed to parse feed %s' % feedurl
wordlist=[]
for w,bc in apcount.items(): #apcount.items()类似[(u'limited', 0), (u'all', 1), (u'searchable', 0), (u'results', 0)]
  frac=float(bc)/len(feedlist)  #变成浮点数算除法不然结果不精确
  if frac>0.1 and frac<0.5:
    wordlist.append(w)  #wordlist=['limited','all','searchable']
out=file('blogdata1.txt','w')
out.write('Blog')
for word in wordlist: out.write('\t%s' % word)  #'\t'是tab
out.write('\n')
for blog,wc in wordcounts.items():
  print blog
  out.write(blog)
  for word in wordlist:
    if word in wc: out.write('\t%d' % wc[word])
    else: out.write('\t0')
  out.write('\n')

ps.最后会得到blogdata.txt文件,效果如下图(我节选了一部分),不想进行这一步的同学可以直接找我要数据23333

 

用excel打开的效果

三.分级聚类

分级聚类的概念在P34,写得很清楚啦。

本节我们将示范如何对博客数据集进行聚类,以构造博客的层级结构;如果构造成功,我们将实现按主题对博客进行分组。

 (一)加载数据文件

##加载数据文件
def readfile(filename):
  lines=[line for line in file(filename)]
  #加载的是blogdata.txt的话,lines=['blog\tword\tword...','blogname\t词频\t词频...',...]
  colnames=lines[0].strip().split('\t')[1:]:]#之所以从1开始,是因为第0列是用来放置博客名了
  #colnames列标题,按\t进行切分
  #加载的是blogdata.txt的话,colnames=['blog','word','word',...]
  rownames=[] #即将填入行标题的空列表
  data=[] #即将填入计数值的空列表
  for line in lines[1:]::]:#第一列是单词,但二列开始才是对不同的单词的计数
    p=line.strip().split('\t')
    '''加载的是blogdata.txt的话,
    p=['blogname','xx','xx',...]
      ['blogname','xx','xx',...]
      ...'''
    rownames.append(p[0])
    '''加载的是blogdata.txt的话,
       p[0]=blogname
            blogname
            ...'''
    data.append([float(x) for x in p[1:]])
  return rownames,colnames,data
  '''上述函数将数据集中的头一行数据读入了一个代表列名的列表,
  并将最左边的一列读入了一个代表行名的列表,
  最后它又将剩下的所有数据都放入一个大列表,其中每一项对应于数据集中的一行数据。'''

(二)定义紧密度

第二章已经有讲到了,这儿直接把代码粘过来,用的是皮尔逊相关性度量。

from math import sqrt

def pearson(v1,v2):
  # Simple sums
  sum1=sum(v1)
  sum2=sum(v2)
  
  # Sums of the squares
  sum1Sq=sum([pow(v,2) for v in v1])
  sum2Sq=sum([pow(v,2) for v in v2])    
  
  # Sum of the products
  pSum=sum([v1[i]*v2[i] for i in range(len(v1))])
  
  # Calculate 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

 (三)新建bicluster类,将所有属性存放给其中,并以此来描述层级树

class bicluster:
#定义一个bicluster类,将每一篇博客看成是一个对象,为此定义一个类。
#分级聚类算法中的每一个聚类,可以是树中的枝节点,也可以是叶节点。每一个聚类还包含了只是其位置的信息,这一信息可以是来自叶节点的行数据,也可以是来自枝节点的经合并后的数据
#我们可以定义一个bicluster类,将所有这些属性存放其中,并以此来描述这颗层级树
  def __init__(self,vec,left=None,right=None,distance=0.0,id=None):
    self.left=left
    self.right=right
    #每次聚类都是一堆数据,left保存其中一个,right保存另一个
    self.vec=vec#代表该聚类的特征向量,保存两个数据聚类后形成新的中心
    self.id=id#用来标志该节点是叶节点还是内部节点,如果是叶节点,则为正数,如果不是叶节点,则为负数。
    self.distance=distance#表示合并左子树和右子树时,两个特征向量之间的距离。 

(四)hcluster算法

书P35最下方有介绍:

分级聚类算法以一组对应于原始数据项的聚类开始。函数的主循环部分会尝试每一组可能的配对并计算它们的相关度,以此来找出最佳配对。最佳配对的两个聚类会被合并成一个新的聚类。新生成的聚类中所包含的数据,等于将两个旧聚类的数据求均值之后得到的结果。这一过程会一直重复下去,直到只剩下一个聚类为止。由于整个计算过程可能会非常耗时,所以不妨将每个配对的相关度计算结果保存起来,因为这样的计算会反复发生,直到配对中的某一项被合并到另一个聚类中为止。

 

####hcluster算法(hierarchical cluster)
def hcluster(rows,distance=pearson):
  distances={}#每计算一对节点的距离值就会保存在这个里面,这样避免了重复计算
  currentclustid=-1

  ##最开始的聚类就是数据集中的一行一行,每一行都是一个元素
  clust=[bicluster(rows[i],id=i) for i in range(len(rows))]#clust是一个列表,列表里面是一个又一个bicluster的对象
  #此时 clust=[bcluster(rows[1],id=1),bcluster(rows[2],id=2),...]
  while len(clust)>1:
    '''while 判断条件:
           执行语句……'''
    #Python 编程中 while 语句用于循环执行程序,即在某条件下,循环执行某段程序,以处理需要重复处理的相同任务。
    lowestpair=(0,1)#先假如lowestpair是0和1号
    #lowestpair为距离最近的两个id
    closest=distance(clust[0].vec,clust[1].vec)
    #先计算第一第二行的相关度,赋值给closest,此时lowestpair=(0,1)
    # 遍历每一个配对,寻找最小距离
    for i in range(len(clust)):
      for j in range(i+1,len(clust)):
    #用distances来缓存距离的计算值
#遍历,使得i不等于j # 用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) # 计算两个聚类的平均值 # 将找到的距离最小的簇对合并为新簇,新簇的vec为原来两个簇vec的平均值 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为负数 #id:如果是叶节点,则为正数,如果不是叶节点,则为负数。 currentclustid-=1 del clust[lowestpair[1]] del clust[lowestpair[0]] #删除聚在一起的两个数据 #del用于list列表操作,删除一个或连续几个元素 clust.append(newcluster) return clust[0]#当只有一个元素之后,就返回,这个节点相当于根节点 #返回最终的簇

 

 (五)检视执行结果P37

  为了检视执行结果,我们可以编写一个简单的函数,递归遍历聚类树,并将其以类似文件系统层级结构的形式打印出来。

def printclust(clust,labels=None,n=0):
  '''参数解释:本例中,labels=blognames
  clust:层次遍历最后输出的一个簇
  n:在本例中代表树的层数'''
  # 利用缩进来建立层级布局
  for i in range(n): print ' ', #n代表当前遍历的层数,层数越多,前面的空格越多
  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)

成果在书上P37最下方

(六)绘制树状图

基础导入

from PIL import Image,ImageDraw

首先,需要利用一个函数来返回给定聚类的总体高度。
如果聚类是一个叶节点,其高度为1,;否则,高度为所有分支高度之和。

 

def getheight(clust):
#返回给定给定聚类的总体高度
  #如果高度为1(没有左右分枝),高度为1
  if clust.left==None and clust.right==None: return 1
  # 否则高度为每个分支的高度之和
  return getheight(clust.left)+getheight(clust.right)

除此之外,我们还需要知道根节点的总体误差。因为线条的长度会根据每个阶段的误差进行相应的调整,所以我们需要根据总的误差值声场一个缩放因子。
一个节点的误差深度等于其下所属的每个分支的最大可能误差。ps.两幅图片我都是竖着来画的,书本上是横着看的。

###计算误差
def getdepth(clust):
  #一个叶节点的距离是0
  if clust.left==None and clust.right==None: return 0

  return max(getdepth(clust.left),getdepth(clust.right))+clust.distance
  #distance#表示合并左子树和右子树时,两个特征向量之间的距离。
  #一个枝节点的距离等于左右两侧分支中距离较大者加上自身距离
  #自身距离:节点与节点合并时候的相似度

 

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))
    #text(self, xy, text, fill=None, font=None, anchor=None)

结果在书本P41,图3-3

四.列聚类

和行聚类类似,在书上的例子里,行聚类是对博客进行聚类,列聚类是对单词进行聚类。

方法依然是转置,类似于第二章的 基于用户的推荐和基于物品的推荐的转换。

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

五.K-均值聚类
概念介绍不摘抄了,在书本P42

import random

def kcluster(rows,distance=pearson,k=4):#默认使用皮尔逊相关系数,聚为4类
  #K均值聚类,针对博客名,单词作为向量进行聚类,k代表簇的个数
  #确定每个点的最大值和最小值
  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)]
  #random.random用于生成一个0到1的浮点数
  lastmatches=None
  for t in range(100): #最多循环100次
    print 'Iteration %d' % t
    bestmatches=[[] for i in range(k)] #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)# 在簇bestmatch中加入元素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

六.针对偏好的聚类
http://www.zebo.com/  大家进得去么?我进不去哎

摘抄书本:该网站鼓励人们在网上建立账号,并将他们已经拥有的和希望拥有的物品列举出来,广告商可以借此找到方法,将偏好相近这很自然地分在一组。

(一)获取数据和准备数据

提取每位用户希望拥有的物品。ps.我这儿有现成的txt结果文件,不想学爬虫的同学可以直接问我要数据哈~

基础导入

from BeautifulSoup import BeautifulSoup
import urllib2
import re

 

1.Beautiful Soup

简单易学,大家可以百度百度,但是他的效率似乎不如xpath

推荐大家一个教程:http://cuiqingcai.com/1319.html

2.搜集来自Zebo的结果

chare=re.compile(r'[!-\.&]') #包含!-\.&任一字符
#使用re的一般步骤是先使用re.compile()函数,将正则表达式的字符串形式编译为Pattern实例,然后使用Pattern实例处理文本并获得匹配结果
itemowners={}

# 要去除的单词
dropwords=['a','new','some','more','my','own','the','many','other','another']

currentuser=0
for i in range(1,51):#遍历1~50页
  # 搜索“用户希望拥有的物品”所对应的url
  c=urllib2.urlopen(
  'http://member.zebo.com/Main?event_key=USERSEARCH&wiowiw=wiw&keyword=car&page=%d'
  % (i))
  '''urllib2的很多应用就是那么简单(记住,除了"http:",URL同样可以使用"ftp:","file:"等等来替代)。但这篇文章是教授HTTP的更复杂的应用。

HTTP是基于请求和应答机制的--客户端提出请求,服务端提供应答。urllib2用一个Request对象来映射你提出的HTTP请求,在它最简单的使用形式中你将用你要请求的

地址创建一个Request对象,通过调用urlopen并传入Request对象,将返回一个相关请求response对象,这个应答对象如同一个文件对象,所以你可以在Response中调用.read()。

'''
  soup=BeautifulSoup(c.read())
  for td in soup('td'):
    #寻找带有bgverdanasmall类的表格单元格
    if ('class' in dict(td.attrs) and td['class']=='bgverdanasmall'):
      items=[re.sub(chare,'',str(a.contents[0]).lower()).strip() for a in td('a')]
      for item in items:
        # 去除多余单词
        txt=' '.join([t for t in item.split(' ') if t not in dropwords])
        if len(txt)<2: continue
        itemowners.setdefault(txt,{})
        itemowners[txt][currentuser]=1
      currentuser+=1
##保存文件
out=file('zebo.txt','w')
out.write('Item')
for user in range(0,currentuser): out.write('\tU%d' % user)
out.write('\n')
for item,owners in itemowners.items():
  if len(owners)>10:
    out.write(item)
    for user in range(0,currentuser):
      if user in owners: out.write('\t1')
      else: out.write('\t0')
    out.write('\n')

和博客数据集相比,此处唯一的区别在于没有的计数。如果一个人希望拥有某件物品,那么我们将其标记为1,否则就标记为0

(二)定义距离度量标准
在这个例子里,数据集只有1和0两种取值,分别代表有或无。并且,假如我们队同事希望拥有两件物品的人在物品方面互有重叠的情况进行度量,那或许是一件更有意义的事情。

书中采取Tanimoto系数的度量方法,它代表的是交集与并集的比率。

Tanimoto系数(广义Jaccard系数又称Tanimoto系数)

百度百科:http://baike.baidu.com/link?url=hPyScHrndVxR8KcqUnW4M805NXzZaVt2iYtN529WsHRi2PduNGFR3jp68P3nRmNU-ZAIezPlsNBBWzLW8hnXBa

def tanamoto(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 #在两个向量中同时出现
  
  return 1.0-(float(shr)/(c1+c2-shr))
#上述代码将返回一个介于1.0和0.0之间的值
#1.0代表不存在同事喜欢两件物品的人,0.0代表所有人同事喜欢两个向量中的物品

(三)对结果进行聚类

七.以二维形式展现数据

八.有关聚类的其他事宜

posted @ 2016-03-19 11:47  小星星☆  阅读(2632)  评论(3编辑  收藏  举报