1. 程式人生 > >[python] 基於k-means和tfidf的文字聚類程式碼簡單實現

[python] 基於k-means和tfidf的文字聚類程式碼簡單實現

俗話說“外行看熱鬧,內行看門道“,作為一個機器學習的門外漢,剛研究python機器學習scikit-learn兩週時間,雖然下面這段程式可能對於那些專研演算法或機器學習的人來說非常簡單,但對於一些入門的同學和我自己還是非常有幫助的。如果文章中有錯誤或不足之處,還請你微微一笑,原諒之;當然也非常歡迎你提出建議或指正~

基本步驟包括:
        1.使用python+selenium分析dom結構爬取百度|互動百科文字摘要資訊;
        2.使用jieba結巴分詞對文字進行中文分詞,同時插入字典關於關鍵詞;
        3.scikit-learn對文字內容進行tfidf計算並構造N*M矩陣(N個文件 M個特徵詞)

        4.再使用K-means進行文字聚類(省略特徵詞過來降維過程);
        5.最後對聚類的結果進行簡單的文字處理,按類簇歸類,也可以計算P/R/F特徵值;
        6.總結這篇論文及K-means的缺點及知識圖譜的一些內容。

當然這只是一篇最最基礎的文章,更高深的分類、聚類、LDA、SVM、隨機森林等內容,自己以後慢慢學習吧!這篇作為線上筆記,路漫漫其修遠兮,fighting~


一. 爬蟲實現

爬蟲主要通過Python+

實現原理:
首先從Tourist_spots_5A_BD.txt中讀取景點資訊,然後通過呼叫無介面瀏覽器PhantomJS(Firefox可替代)訪問百度百科連結"

http://baike.baidu.com/",通過Selenium獲取輸入對話方塊ID,輸入關鍵詞如"故宮",再訪問該百科頁面。最後通過分析DOM樹結構獲取摘要的ID並獲取其值。核心程式碼如下:
driver.find_elements_by_xpath("//div[@class='lemma-summary']/div")

PS:Selenium更多應用於自動化測試,推薦Python爬蟲使用scrapy等開源工具。

# coding=utf-8  
""" 
Created on 2015-09-04 @author: Eastmount  
"""  
  
import time          
import re          
import os  
import sys
import codecs
import shutil
from selenium import webdriver      
from selenium.webdriver.common.keys import Keys      
import selenium.webdriver.support.ui as ui      
from selenium.webdriver.common.action_chains import ActionChains  
  
#Open PhantomJS  
driver = webdriver.PhantomJS(executable_path="G:\phantomjs-1.9.1-windows\phantomjs.exe")  
#driver = webdriver.Firefox()  
wait = ui.WebDriverWait(driver,10)

#Get the Content of 5A tourist spots  
def getInfobox(entityName, fileName):  
    try:  
        #create paths and txt files
        print u'檔名稱: ', fileName
        info = codecs.open(fileName, 'w', 'utf-8')  

        #locate input  notice: 1.visit url by unicode 2.write files
        #Error: Message: Element not found in the cache -
        #       Perhaps the page has changed since it was looked up
        #解決方法: 使用Selenium和Phantomjs
        print u'實體名稱: ', entityName.rstrip('\n') 
        driver.get("http://baike.baidu.com/")  
        elem_inp = driver.find_element_by_xpath("//form[@id='searchForm']/input")  
        elem_inp.send_keys(entityName)  
        elem_inp.send_keys(Keys.RETURN)  
        info.write(entityName.rstrip('\n')+'\r\n')  #codecs不支援'\n'換行
        time.sleep(2)  
  
        #load content 摘要
        elem_value = driver.find_elements_by_xpath("//div[@class='lemma-summary']/div")
        for value in elem_value:
            print value.text
            info.writelines(value.text + '\r\n')
        time.sleep(2)  
          
    except Exception,e:    #'utf8' codec can't decode byte  
        print "Error: ",e  
    finally:  
        print '\n'  
        info.close() 
  
#Main function  
def main():
    #By function get information
    path = "BaiduSpider\\"
    if os.path.isdir(path):
        shutil.rmtree(path, True)
    os.makedirs(path)
    source = open("Tourist_spots_5A_BD.txt", 'r')
    num = 1
    for entityName in source:  
        entityName = unicode(entityName, "utf-8")  
        if u'故宮' in entityName:   #else add a '?'  
            entityName = u'北京故宮'
        name = "%04d" % num
        fileName = path + str(name) + ".txt"
        getInfobox(entityName, fileName)
        num = num + 1
    print 'End Read Files!'  
    source.close()  
    driver.close()
    
if __name__ == '__main__':
    main()  
執行結果如下圖所示:

二. 中文分詞

中文分詞主要使用的是Python+Jieba分詞工具,同時匯入自定義詞典dict_baidu.txt,裡面主要是一些專業景點名詞,如"黔清宮"分詞"黔/清宮",如果詞典中存在專有名詞"乾清宮"就會先查詢詞典。
參考前文:[python] 使用Jieba工具中文分詞及文字聚類概念

#encoding=utf-8
import sys
import re
import codecs
import os
import shutil
import jieba
import jieba.analyse

#匯入自定義詞典
jieba.load_userdict("dict_baidu.txt")

#Read file and cut
def read_file_cut():
    #create path
    path = "BaiduSpider\\"
    respath = "BaiduSpider_Result\\"
    if os.path.isdir(respath):
        shutil.rmtree(respath, True)
    os.makedirs(respath)

    num = 1
    while num<=204:
        name = "%04d" % num 
        fileName = path + str(name) + ".txt"
        resName = respath + str(name) + ".txt"
        source = open(fileName, 'r')
        if os.path.exists(resName):
            os.remove(resName)
        result = codecs.open(resName, 'w', 'utf-8')
        line = source.readline()
        line = line.rstrip('\n')
        
        while line!="":
            line = unicode(line, "utf-8")
            seglist = jieba.cut(line,cut_all=False)  #精確模式
            output = ' '.join(list(seglist))         #空格拼接
            print output
            result.write(output + '\r\n')
            line = source.readline()
        else:
            print 'End file: ' + str(num)
            source.close()
            result.close()
        num = num + 1
    else:
        print 'End All'

#Run function
if __name__ == '__main__':
    read_file_cut()
按照Jieba精確模式分詞且空格拼接,"0003.txt 頤和園"分詞結果如下圖所示:

為方便後面的計算或對接一些sklearn或w2v等工具,下面這段程式碼將結果儲存在同一個txt中,每行表示一個景點的分詞結果。
# coding=utf-8            
import re          
import os  
import sys
import codecs
import shutil

def merge_file():
    path = "BaiduSpider_Result\\"
    resName = "BaiduSpider_Result.txt"
    if os.path.exists(resName):
        os.remove(resName)
    result = codecs.open(resName, 'w', 'utf-8')

    num = 1
    while num <= 204:
        name = "%04d" % num 
        fileName = path + str(name) + ".txt"
        source = open(fileName, 'r')
        line = source.readline()
        line = line.strip('\n')
        line = line.strip('\r')

        while line!="":
            line = unicode(line, "utf-8")
            line = line.replace('\n',' ')
            line = line.replace('\r',' ')
            result.write(line+ ' ')
            line = source.readline()
        else:
            print 'End file: ' + str(num)
            result.write('\r\n')
            source.close()
        num = num + 1
        
    else:
        print 'End All'
        result.close()    

if __name__ == '__main__':
    merge_file()
每行一個景點的分詞結果,執行結果如下圖所示:

三. 計算TF-IDF

此時,需要將文件相似度問題轉換為數學向量矩陣問題,可以通過VSM向量空間模型來儲存每個文件的詞頻和權重,特徵抽取完後,因為每個詞語對實體的貢獻度不同,所以需要對這些詞語賦予不同的權重。計算詞項在向量中的權重方法——TF-IDF。

相關介紹:
它表示TF(詞頻)和IDF(倒文件頻率)的乘積:

其中TF表示某個關鍵詞出現的頻率,IDF為所有文件的數目除以包含該詞語的文件數目的對數值。
|D|表示所有文件的數目,|w∈d|表示包含詞語w的文件數目。
最後TF-IDF計算權重越大表示該詞條對這個文字的重要性越大,它的目的是去除一些"的、了、等"出現頻率較高的常用詞。

參考前文:Python簡單實現基於VSM的餘弦相似度計算
                 基於VSM的命名實體識別、歧義消解和指代消解

下面是使用scikit-learn工具呼叫CountVectorizer()和TfidfTransformer()函式計算TF-IDF值,同時後面"四.K-means聚類"程式碼也包含了這部分,該部分程式碼先提出來介紹。
# coding=utf-8  
""" 
Created on 2015-12-30 @author: Eastmount  
"""  
  
import time          
import re          
import os  
import sys
import codecs
import shutil
from sklearn import feature_extraction  
from sklearn.feature_extraction.text import TfidfTransformer  
from sklearn.feature_extraction.text import CountVectorizer

'''
sklearn裡面的TF-IDF主要用到了兩個函式:CountVectorizer()和TfidfTransformer()。
    CountVectorizer是通過fit_transform函式將文字中的詞語轉換為詞頻矩陣。
    矩陣元素weight[i][j] 表示j詞在第i個文字下的詞頻,即各個詞語出現的次數。
    通過get_feature_names()可看到所有文字的關鍵字,通過toarray()可看到詞頻矩陣的結果。
    TfidfTransformer也有個fit_transform函式,它的作用是計算tf-idf值。
'''

if __name__ == "__main__":
    corpus = [] #文件預料 空格連線

    #讀取預料 一行預料為一個文件
    for line in open('BaiduSpider_Result.txt', 'r').readlines():
        print line
        corpus.append(line.strip())
    #print corpus
    time.sleep(5)
    
    #將文字中的詞語轉換為詞頻矩陣 矩陣元素a[i][j] 表示j詞在i類文字下的詞頻
    vectorizer = CountVectorizer()

    #該類會統計每個詞語的tf-idf權值
    transformer = TfidfTransformer()

    #第一個fit_transform是計算tf-idf 第二個fit_transform是將文字轉為詞頻矩陣
    tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus))

    #獲取詞袋模型中的所有詞語  
    word = vectorizer.get_feature_names()

    #將tf-idf矩陣抽取出來,元素w[i][j]表示j詞在i類文字中的tf-idf權重
    weight = tfidf.toarray()

    resName = "BaiduTfidf_Result.txt"
    result = codecs.open(resName, 'w', 'utf-8')
    for j in range(len(word)):
        result.write(word[j] + ' ')
    result.write('\r\n\r\n')

    #列印每類文字的tf-idf詞語權重,第一個for遍歷所有文字,第二個for便利某一類文字下的詞語權重  
    for i in range(len(weight)):
        print u"-------這裡輸出第",i,u"類文字的詞語tf-idf權重------"  
        for j in range(len(word)):
            result.write(str(weight[i][j]) + ' ')
        result.write('\r\n\r\n')

    result.close()
    

其中輸出如下所示,由於文字摘要不多,總共8368維特徵,其中共400個景點(百度百科200 互動百科200)文字摘要,故構建的矩陣就是[400][8368],其中每個景點都有對應的矩陣儲存TF-IDF值。

缺點:可以嘗試出去一些停用詞、數字等,同時可以如果文件維數過多,可以設定固定的維度,同時進行一些降維操作或構建稀疏矩陣,大家可以自己去研究下。
推薦一些優秀的關於Sklearn工具TF-IDF的文章:
        python scikit-learn計算tf-idf詞語權重 - liuxuejiang158
        用Python開始機器學習(5:文字特徵抽取與向量化) - lsldd大神
        官方scikit-learn文件 4.3. Preprocessing data


四. K-means聚類

# coding=utf-8  
""" 
Created on 2016-01-06 @author: Eastmount  
"""  
  
import time          
import re          
import os  
import sys
import codecs
import shutil
import numpy as np
from sklearn import feature_extraction  
from sklearn.feature_extraction.text import TfidfTransformer  
from sklearn.feature_extraction.text import CountVectorizer  

if __name__ == "__main__":
    
    #########################################################################
    #                           第一步 計算TFIDF
    
    #文件預料 空格連線
    corpus = []
    
    #讀取預料 一行預料為一個文件
    for line in open('BHSpider_Result.txt', 'r').readlines():
        print line
        corpus.append(line.strip())
    #print corpus
    #time.sleep(1)
    
    #將文字中的詞語轉換為詞頻矩陣 矩陣元素a[i][j] 表示j詞在i類文字下的詞頻
    vectorizer = CountVectorizer()

    #該類會統計每個詞語的tf-idf權值
    transformer = TfidfTransformer()

    #第一個fit_transform是計算tf-idf 第二個fit_transform是將文字轉為詞頻矩陣
    tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus))

    #獲取詞袋模型中的所有詞語  
    word = vectorizer.get_feature_names()

    #將tf-idf矩陣抽取出來,元素w[i][j]表示j詞在i類文字中的tf-idf權重
    weight = tfidf.toarray()

    #列印特徵向量文字內容
    print 'Features length: ' + str(len(word))
    resName = "BHTfidf_Result.txt"
    result = codecs.open(resName, 'w', 'utf-8')
    for j in range(len(word)):
        result.write(word[j] + ' ')
    result.write('\r\n\r\n')

    #列印每類文字的tf-idf詞語權重,第一個for遍歷所有文字,第二個for便利某一類文字下的詞語權重  
    for i in range(len(weight)):
        print u"-------這裡輸出第",i,u"類文字的詞語tf-idf權重------"  
        for j in range(len(word)):
            #print weight[i][j],
            result.write(str(weight[i][j]) + ' ')
        result.write('\r\n\r\n')

    result.close()


    ########################################################################
    #                               第二步 聚類Kmeans

    print 'Start Kmeans:'
    from sklearn.cluster import KMeans
    clf = KMeans(n_clusters=20)
    s = clf.fit(weight)
    print s

    #20箇中心點
    print(clf.cluster_centers_)
    
    #每個樣本所屬的簇
    print(clf.labels_)
    i = 1
    while i <= len(clf.labels_):
        print i, clf.labels_[i-1]
        i = i + 1

    #用來評估簇的個數是否合適,距離越小說明簇分的越好,選取臨界點的簇個數
    print(clf.inertia_)

輸出如下圖所示,20個類簇中心點和408個簇,對應408個景點,每個文件對應聚在相應的類0~19。


五. 結果處理

為了更直觀的顯示結果,通過下面的程式對景點進行簡單結果處理。

# coding=utf-8  
import os  
import sys
import codecs

'''
@2016-01-07 By Eastmount
功能:合併實體名稱和聚類結果 共類簇20類
輸入:BH_EntityName.txt Cluster_Result.txt
輸出:ZBH_Cluster_Merge.txt ZBH_Cluster_Result.txt
'''

source1 = open("BH_EntityName.txt",'r')
source2 = open("Cluster_Result.txt",'r')
result1 = codecs.open("ZBH_Cluster_Result.txt", 'w', 'utf-8')

#########################################################################
#                        第一部分 合併實體名稱和類簇

lable = []       #儲存408個類標 20個類
content = []     #儲存408個實體名稱
name = source1.readline()
#總是多輸出空格 故設定0 1使其輸出一致
num = 1
while name!="":
    name = unicode(name.strip('\r\n'), "utf-8")
    if num == 1:
        res = source2.readline()
        res = res.strip('\r\n')
        
        value = res.split(' ')
        no = int(value[0]) - 1   #行號
        va = int(value[1])       #值
        lable.append(va)
        content.append(name)
        
        print name, res
        result1.write(name + ' ' + res + '\r\n')
        num = 0
    elif num == 0:
        num = 1
    name = source1.readline()
    
else:
    print 'OK'
    source1.close()
    source2.close()
    result1.close()

#測試輸出 其中實體名稱和類標一一對應
i = 0
while i < len(lable):
    print content[i], (i+1), lable[i]
    i = i + 1

#########################################################################
#                      第二部分 合併類簇 類1 ..... 類2 .....

#定義定長20字串陣列 對應20個類簇
output = ['']*20
result2 = codecs.open("ZBH_Cluster_Merge.txt", 'w', 'utf-8')

#統計類標對應的實體名稱
i = 0
while i < len(lable):
    output[lable[i]] += content[i] + ' ' 
    i = i + 1

#輸出
i = 0
while i < 20:
    print '#######'
    result2.write('#######\r\n')
    print 'Label: ' + str(i)
    result2.write('Label: ' + str(i) + '\r\n')
    print output[i]
    result2.write(output[i] + '\r\n')
    i = i + 1

result2.close()
輸出結果如下圖所示,其中label19可以發現百度百科和互動百科的"大昭寺、法門寺"文字內容都劃分為一類,同時也會存在一些錯誤的類別,如Label15中的"橘子洲"。

PS:如果你想進行準確率、迴歸率、F特徵值比較,可以進一步去學習sklearn官方文件。通常的文字資料集的類標如"教育、體育、娛樂",把不同內容的新聞聚在一類,而這個略有區別,它主要是應用於我實際的畢設。

六. 總結與不足

這篇文章更多的是一些基礎內容的程式碼實現,可能對一些初學者有用,同時也是我的線上筆記吧!主要內容包括:
         1.python+selenium爬取
         2.jieba中文分詞
         3.sklearn+tfidf矩陣權重計算
         4.kmeans簡單實現及結果對比

Kmeans聚類是一種自下而上的聚類方法,它的優點是簡單、速度快;缺點是聚類結果與初始中心的選擇有關係,且必須提供聚類的數目。
Kmeans的第二個缺點是致命的,因為在有些時候,我們不知道樣本集將要聚成多少個類別,這種時候kmeans是不適合的,推薦使用hierarchical 或meanshift來聚類。第一個缺點可以通過多次聚類取最佳結果來解決。

推薦一些關於Kmeans及實驗評估的文章:
        淺談Kmeans聚類 - easymind223
        基於K-Means的文字聚類(強推基礎介紹) - freesum
        基於向量空間模型的文字聚類演算法 - helld123
        KMeans文件聚類python實現(程式碼詳解) - skineffect
        Kmeans文字聚類系列之全部C++程式碼 - finallyliuyu
        文字聚類—kmeans - zengkui111

不論如何,最後還是希望文章對你有所幫助!深夜寫文不易,且看且珍惜吧~
(By:Eastmount 2016-01-08 深夜3點    )