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


啦啦啦聚類算法~這一章我學得比較迷糊,還需要反復理解琢磨。

我剛看到這一章的時候內心是崩潰的,許多傻瓜軟件點一下鼠標就能完成的事兒,到書里這一章需要許多行代碼來完成,也說明了,學數據挖掘,算法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代表所有人同事喜歡兩個向量中的物品

(三)對結果進行聚類

七.以二維形式展現數據

八.有關聚類的其他事宜


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM