[python] 基於k-means和tfidf的文字聚類程式碼簡單實現
基本步驟包括:
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可替代)訪問百度百科連結"
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(倒文件頻率)的乘積:
|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點 )