《集體智慧編程》 第三章 發現羣組 學習筆記

啦啦啦聚類算法~這一章我學得比較迷糊,還須要反覆理解琢磨。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表明全部人同事喜歡兩個向量中的物品

(三)對結果進行聚類

七.以二維形式展示數據

八.有關聚類的其餘事宜

相關文章
相關標籤/搜索