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

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

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

本章须要安装:python

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.gzapp

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

注意: 导入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表明全部人同事喜欢两个向量中的物品

(三)对结果进行聚类

七.以二维形式展示数据

八.有关聚类的其余事宜

相关文章
相关标签/搜索