【爬蟲、貝葉斯、SVM、LDA一條龍服務】從數據收集到文本分類:從零開始你自己的數據挖掘工程

0. 前言

對於每個學習數據挖掘的人來說,總會在某個時刻想要自己從頭開始一項數據挖掘工程。這不同於用一用搜狗的新聞資料庫,或者是kaggle、天池等競賽的資料庫,要自己從頭開始收集數據,使用爬蟲收集,然後去重,數據清理等等。

使用已有的數據庫,好處是省略了自己收集的過程,遇到各種奇奇怪怪的問題,網上也有解答,更重要的是,我們心裏有底,知道如果過程不出問題,最後總是能得到一個不錯的結果。但是自己收集數據則不是這樣,我們收集的數據是獨一無二的,可能隱藏許許多多問題,需要我們自己去處理。在一個數據挖掘工程中,這部分要花費80%以上的時間,但是我們所做的實驗,由於種種原因,會將這一步簡化甚至略過。這不能不說是一種遺憾。這也是我開始自己從頭開始的初衷。

1. 準備工作

在本實驗中,我選擇進行英文文本的分類任務。

數據預處理中,訓練集和測試集均由本人自己編寫爬蟲獲得,數據源爲浙江大學圖書館文獻資料庫。

分詞、標記、去停用詞:使用Windows下Python 3.7 的nltk包。

所使用的分類算法包括如下幾個:自編樸素貝葉斯算法; svm:libsvm和liblinear;lda:Python 3.7 的 lda包。

2. 實驗環境介紹

2.1 數據庫介紹

浙江大學圖書館的求是學術搜索擁有較爲全面的中英文期刊數據。對於每篇文獻,擁有對應的文獻關鍵詞、文獻類別、文獻作者、期刊名稱等信息。並且期刊的摘要爲顯式顯示。對比其他數據庫如社交信息、新聞等數據,該數據庫數據較爲規範,分類較爲準確,具有較高的實驗價值。

本次實驗爬取了2005-2019年數據庫中十餘類共約200萬篇英文文獻(3.51G),類別分別爲農學、解剖學、經濟學、化學、教育學、地理學、語言學、法學、數學、物理學等。每類文檔從四萬到十五萬不等。

數據來源:http://libweb.zju.edu.cn

2.2 NLTK

NLTK大概是最知名的Python自然語言處理工具了,全稱"Natural Language Toolkit", 誕生於賓夕法尼亞大學,以研究和教學爲目的而生,因此也特別適合入門學習。NLTK雖然主要面向英文,但是它的很多NLP模型或者模塊是語言無關的,因此如果某種語言有了初步的Tokenization或者分詞,NLTK的很多工具包是可以複用的。

本次實驗使用了NLTK包的WordNetLemmatizer進行詞形還原,pos_tag進行詞性標註。詞形還原是把一個任何形式的語言詞彙還原爲一般形式(能表達完整語義)。相對而言,詞幹提取是簡單的輕量級的詞形歸併方式,最後獲得的結果爲詞幹,並不一定具有實際意義。詞形還原處理相對複雜,獲得結果爲詞的原形,能夠承載一定意義,與詞幹提取相比,更具有研究和應用價值。

詳細的介紹參見該網址

2.3 libsvm

LIBSVM是臺灣大學林智仁(Lin Chih-Jen)教授等開發設計的一個簡單、易於使用和快速有效的SVM模式識別與迴歸的軟件包,他不但提供了編譯好的可在Windows系列系統的執行文件,還提供了源代碼,方便改進、修改以及在其它操作系統上應用;該軟件對SVM所涉及的參數調節相對比較少,提供了很多的默認參數,利用這些默認參數可以解決很多問題;並提供了交互檢驗(Cross Validation)的功能。該軟件可以解決C-SVMν-SVMε-SVRν-SVR等問題,包括基於一對一算法的多類模式識別問題。

本次實驗首先使用了libsvm進行分類,但是出現了訓練時間過長的缺點。因此又選擇了liblinear進行嘗試。Liblinearlibsvm的特殊實現,其核函數指定爲線性核,因此進行了許多優化。另外,libsvm進行n類多分類時,需要訓練n(n-1)/2個模型,而liblinear僅需訓練n個,這極大縮短了訓練和分類時間。

2.4 lda

LDA(Latent dirichlet allocation)是由Blei2003年提出的三層貝葉斯主題模型,通過無監督的學習方法發現文本中隱含的主題信息,目的是要以無指導學習的方法從文本中發現隱含的語義維度-即“Topic”或者“Concept”。隱性語義分析的實質是要利用文本中詞項(term)的共現特徵來發現文本的Topic結構,這種方法不需要任何關於文本的背景知識。

3. 實驗過程

3.1 數據收集

在綜合考慮了各個數據來源之後,我選擇了浙江大學圖書館作爲數據源。一方面,這個網站我比較熟悉,另一方面,其反爬蟲機制也較爲孱弱:若約三十分鐘內同一IP訪問次數多餘若干次(約爲2000次),則在一段時間內不再響應該IP的訪問。另外,對於某一關鍵詞的搜索,最多顯示前1000條數據,其餘數據不予顯示。

這兩種機制很容易進行反反爬蟲,前者使用代理,我從該網站購買了三百萬次IP轉發,從而解決了同一IP訪問過多的問題。但由於IP轉發比直接訪問延遲要大得多,因此我選擇將爬蟲程序部署在我的服務器上,同時進行並行化,對所有類別數據同時進行爬取。在這裏我發現,Python自帶的並行化方法(threads.append)並不是起若干個進程同時爬取,而是若干個線程進行爬取,這樣對效率的提高微乎其微。還是粗暴的同時運行所有腳本效率最高。另外,在爬取過程中可能出現許多意外情況,比如某篇文章缺失作者、缺失摘要等情況,因此要靈活使用except進行異常處理。爲了防止腳本意外關閉,使用supervisor守護進程確保在進程被殺掉的時候重啓進程。這裏要注意一點,浙江大學圖書館可以根據發表時間、分類等進行精確查找。因此可以設置監視哨監視當前的爬取進度,每過一段時間更新監視哨,這樣在重啓進程時可以根據監視哨快速定位爬取進度。這裏我選擇時間作爲監視哨,爬取方式爲按周進行某一類別的查詢。這樣就可以獲取一週內某一類文檔的前1000篇,這樣也同時解決了後一個反爬蟲的問題。

由於轉發機制的存在,我從11月23日開始爬取,爬取到11月28日才全部爬取完畢。如下圖所示爲在服務器上保存的全部原始數據:

如圖爲使用的監視哨:

圖中參數表示最後一次請求搜索的條件爲2019年12月22日到29日,第40頁,共爬取到154115條數據。

最開始我是選擇中文文獻進行爬取,但是在爬完第一類農業時,發現該網站的文獻標註的一大問題:大量文獻沒有中文標註,中英文標註混亂等情況時有發生。具體表現爲:當我選擇類別爲農業時,嘗試爬取條件爲“語言:中文”、“類別:農業”、“時間:2010年-2019年”的數據時,共爬取到約十萬條數據,其中,中文數據約三萬條,其餘均爲英文數據。然後我去尋找原因,才發現許多中文文獻的內容爲中文,但是發表在諸如《上海交通大學學報·農業版(英文版)》上面,因此摘要爲英文。因此爲了達到要求的數據量,我不得不將原來的中文文本分類改爲英文文本分類。

然後我發現了另一類問題,即分類不完善,例如,通過關鍵詞查找,可以找到許多有關某一類別的文檔,但是直接指定該類別進行查找時卻找不到,即存在大量缺失類別標註的文獻。同樣爲了達到數據量,我選擇關鍵詞+類別兩類同時進行爬取。這樣最終得到了可以使用的數據集,但是由於關鍵詞與類別之間的對應關係並不一定是絕對準確的,這也在一定程度上影響了分類的準確性。

3.2 數據預處理

3.2.1 詞形還原與詞性標註

我們知道無論是樸素貝葉斯、svm還是lda,在對數據進行分類時,輸入的都不是一串字符串,而是n維向量,數據預處理就是把上一節爬取到的數據轉換爲n維向量。

在轉換之前,我們需要一個字典,字典是單詞與數字序號的映射,通過查詢字典,我們可以將任何一條數據轉換爲一個n維向量。因此數據預處理首先要獲得字典。

英文文本分類相比於中文,少了一步分詞,這是其較中文文本分類難度下降的一大原因。但是比中文文本分類多了詞幹提取這一步驟。由於英文單詞存在變形,因此可能不同單詞是同一單詞的其他形式,我們應將其還原,從而構建出字典。

字典的鍵是單詞的原始形式,鍵值爲數字序號。然而,若是提取原始數據中的所有單詞,是很不必要的,因爲介詞、冠詞等意義不大,而且這樣一來冗餘過多。實際上選擇的優先級是名詞>形容詞>動詞。因此我選擇將這三類作爲字典的元素。由於需要提取這三類單詞,那麼詞性標註就是必須的。

在具體處理之前,首先將數據集分成測試集和訓練集兩部分。由於我的數據集按時間有序,因此不能簡單的進行分隔,需要使用sklearn將其分割成訓練集和測試集兩部分。之後使用nltk可以很容易的實現。首先使用word_tokenize將原始數據按空格進行轉換。然後使用stopwords去除停用詞。接下來使用pos_tag進行詞性標註。對於詞性爲動詞、名詞、形容詞的詞語,進行詞形還原並加入字典。代碼如下:

for row in csv_reader:
	tmp1 = nltk.word_tokenize(row[3])
	tmp2=[w for w in tmp1 if len(w.lower())>1 and (w.lower() not in stopwords.words("english"))]
	tmp3=nltk.pos_tag(tmp2)
	tmp_pure=list()
	for i in tmp3:
		if "NN" in i[1] or "JJ" in i[1] or "VB" in i[1]:
			tmp4=""
			if "NN" in i[1]:
				if easy=="easy":
					tmp4=i[0]
				else:
					tmp4=wnl.lemmatize(i[0],'n')
				if tmp4 not in dic_n:
					dic_n[tmp4]=0
				dic_n[tmp4]+=1
			if "JJ" in i[1]:
				if easy=="easy":
					tmp4=i[0]
				else:
					tmp4=wnl.lemmatize(i[0],'a')
				if tmp4 not in dic_adj:
					dic_adj[tmp4]=0
				dic_adj[tmp4]+=1
			if "VB" in i[1]:
				if easy=="easy":
					tmp4=i[0]
				else:
					tmp4=wnl.lemmatize(i[0],'v')
				if tmp4 not in dic_v:
					dic_v[tmp4]=0
				dic_v[tmp4]+=1
			if tmp4 not in dic:
				dic[tmp4]=0
			dic[tmp4]+=1
			tmp_pure.append(tmp4)

以農業爲例,得到的字典如下:

經過處理後的訓練集數據如下:

3.2.2 維度壓縮

在上一節我們提到,算法輸入需要向量。向量維數越高,那麼算法複雜度就越大,需要的算力也就越大。同樣以農業爲例,全部訓練集得到的字典爲25萬維,所有類別的字典合在一起將達到上百萬維。因此必須進行維度壓縮。維度壓縮應選擇最能夠代表本類的單詞,本實驗中選擇CHI算法。CHI算法通過卡方檢驗來獲得較爲具有代表性的單詞。其具體計算方法如下:

設A:包含特徵詞w且屬於類別c的文檔頻數,B:包含特徵詞w但不屬於類別c的文檔頻數,C:屬於類別c但不包含特徵詞w的文檔頻數,D:既不屬於c也不包含特徵詞w的文檔頻數,N:文檔總數,計算

\frac{N(AD-BC)^2}{(A+C)(A+B)(B+D)(C+D)}

然後由大到小排列即可。代碼如下:

def create_A(self):
	for name in self.major:
		for key in self.dict_all[name]["num"]:
			self.A[name][key]=self.dict_all[name]["num"][key]

def create_B(self):
	for name in self.major:
		for key in self.dict_all[name]["num"]:
			if key not in self.B[name]:
				self.B[name][key]=0
			for _name in self.major:
				if _name!=name:
					if key in self.dict_all[_name]["num"]:
						self.B[name][key]+=self.dict_all[_name]["num"][key]

def create_C(self):
	for name in self.major:
		for key in self.dict_all[name]["num"]:
			self.C[name][key]=self.train_data_num[name]-self.A[name][key]

def create_D(self):
	for name in self.major:
		for key in self.dict_all[name]["num"]:
			self.D[name][key]=self.train_data_sum-self.train_data_num[name]-self.B[name][key]

def create_N(self):
	self.N=self.train_data_sum

def cul_CHI(self):
	for name in self.major:
		i=0
		for key in self.dict_all[name]["num"]:
			AD_BC=self.A[name][key]*self.D[name][key]-self.B[name][key]*self.C[name][key]
			i+=1
			if AD_BC<=0 or i>20000:
				self.chi_data[name][key]=0
			else:
				self.chi_data[name][key]=self.N*AD_BC*AD_BC*1.0/(1.0*(self.A[name][key]+self.C[name][key])*(self.B[name][key]+self.D[name][key])*(self.A[name][key]+self.B[name][key])*(self.C[name][key]+self.D[name][key]))

以農業爲例,計算得到的CHI如下:

與上面的用頻率排序的字典進行對比,可以發現CHI得到的單詞更能代表農業類別。下圖爲前2000單詞CHI的數值分佈.

可以看出,單詞分佈呈現出明顯的左側堆積特徵,這說明左側的部分單詞已經可以極大的代表該類別了。

3.2.3 權重計算

雖然我們已經將維度進行了壓縮,但是不同單詞的權重仍可能不同。這裏使用tf-idf進行單詞的權重計算。tf-idf是一種簡單的衡量單詞重要性的指標,其中tf爲某一單詞在所有文章中出現的次數/所有文章的總單詞數,idf爲文章總數/(含某一單詞的文章數+1),取對數。代碼如下:

def cul_tf(self):
	for name in self.major:
		for key in self.CHI[name]["num"]:
			for _name in self.major:
				if key in self.dict_all[_name]["num"]:
					if key not in self.tf[name]:
						self.tf[name][key]=0
					self.tf[name][key]+=self.dict_all[_name]["num"][key]
		for key in self.tf[name]:
			self.tf[name][key]=self.tf[name][key]/(self.word_count*1.0)
	#for name in self.major:
	#	for key in self.dict_all[name]["num"]:
	#		self.tf[name][key]=self.dict_all[name]["num"][key]/(self.class_word_count[name]*1.0)

def cul_tf_idf(self):
	for name in self.major:
		for key in self.CHI[name]["num"]:
			for _name in self.major:
				if key in self.dict_all[_name]["num"]:
					if key not in self.idf[name]:
						self.idf[name][key]=0
					self.idf[name][key]+=self.dict_all[_name]["num"][key]
		tfidf_min=1000
		tfidf_max=0
		for key in self.idf[name]:
			self.idf[name][key]=np.log(self.train_count*1.0/self.idf[name][key])
			self.tf_idf[name][key]=self.tf[name][key]*1.0*self.idf[name][key]
			if self.tf_idf[name][key]>tfidf_max:
				tfidf_max=self.tf_idf[name][key]
			if self.tf_idf[name][key]<tfidf_min:
				tfidf_min=self.tf_idf[name][key]
		for key in self.tf_idf[name]:
			self.tf_idf[name][key]=round((self.tf_idf[name][key]-tfidf_min)*1.0/(tfidf_max-tfidf_min),6)
			# CHI["num"] 
			self.tf_idf_num[name][self.CHI[name]["num"][key]]=self.tf_idf[name][key]

注意要進行歸一化處理。如下爲農業類別的tf-idf值:

使用CHI查詢對應單詞的tf-idf值就可以得到對應的權重了。

3.3 樸素貝葉斯實現

3.3.1 樸素貝葉斯原理

樸素貝葉斯的原理很簡單,就是基於貝葉斯公式:

P(B|A)=\frac{P(A|B)P(B)}{P(A)}

想要自己實現樸素貝葉斯,必須理解這一公式。在這一公式中,以我們的實驗爲例,

P(B|A)就是“當測試樣本含有A這一系列單詞時,樣本被分類爲B的概率”,

P(A|B)就是“訓練集中分類爲B的樣本中含有A這一系列單詞的比例”,

P(B)是“訓練集中分類爲B的樣本在全部訓練集樣本中的比例”,

P(A)是“訓練集中包含A這一系列單詞的樣本在全部訓練集樣本中的比例”。

可以看出,右側的所有值都是通過訓練集得到的,因此訓練集的好壞是否具有較強的代表性將極大影響貝葉斯的效果。

由於本實驗是多分類,因此針對每一個類別都需要有一個樸素貝葉斯分類器。每個分類器都會給出一個測試樣本屬於該類別的概率,從中選擇出最大的即可。

3.3.2 樸素貝葉斯代碼

我的實現思路如下:考慮到P(A|B)中包含一系列單詞,因此需要分別計算每個單詞在該類別下的條件概率,P(B)則可以直接計算得到。因此計算條件概率的代碼如下:

def train(self):
	#self.word_num=self.major_dict.iloc[0:len(self.pVect_CHI)]["num"].sum()
	self.word_num=self.major_dict.iloc[0:self.major_dict.shape[0]]["num"].sum()
	self.fault_value=1.0/(self.word_num+len(self.pVect_CHI))
	for key in self.feature_dict_CHI['word']:
		#self.pVect_CHI[i]=np.log((self.major_dict.iloc[i]["num"]+1)/(self.word_num*1.0+len(self.pVect_CHI)))
		#print(self.major_dict[self.major_dict.word==key]["num"])
		self.pVect_CHI[self.feature_dict_CHI['word'][key]]=np.log((self.major_dict[self.major_dict.word==key]["num"]+1)/(self.word_num*1.0+len(self.pVect_CHI)))
		self.word_weight[self.feature_dict_CHI['word'][key]]=np.log(self.feature_dict_TFIDF['weight'][key])
	#for key in self.feature_dict_TFIDF['word']:
		#self.pVect_TFIDF[self.feature_dict_TFIDF['word'][key]]=np.log((self.major_dict[self.major_dict.word==key]["num"]+1)/(self.word_num*1.0+len(self.pVect_TFIDF)))

輸入的文本數據需要轉爲詞向量形式,轉換函數如下:

def word2vec(self,sen:str):
	res_CHI=np.zeros(len(self.pVect_CHI))
	match_num_CHI=0
	for word in sen.split():
		if word in self.feature_dict_CHI['word']:
			#res_CHI[self.feature_dict_CHI['word'][word]]+=1
			res_CHI[self.feature_dict_CHI['word'][word]]=self.weight_dict["tfidf"][word]
			match_num_CHI+=1

最終計算結果代碼如下:

def test(self,test_case:np.ndarray,unmatch_num:int,all_sum:int,alpha:int):
	res_CHI=sum(alpha*test_case*self.pVect_CHI+(1-alpha)*test_case*self.word_weight)+(np.log(self.fault_value))*unmatch_num+np.log(self.word_num/(all_sum*1.0))
	return res_CHI

這裏有一點很重要,就是我選擇的字典大概率不會包含全部輸入的單詞,對於字典中沒有的單詞,也需要給它計算一個條件概率。本實驗中我選擇了拉普拉斯平滑。

3.3.3 樸素貝葉斯性能

本實驗中,樸素貝葉斯耗費了我最多的時間。主要原因在於最開始使用的維度壓縮的方法不好。同時由於學科交叉與分類模糊,部分數據難以劃分到一個特別準確的類別。例如,討論語言的教育的論文,其分類可以既是語言又是教育,而不是單獨的一個。這使得我的準確率不高——長期徘徊在72%左右。如下表所示:

最初我每類選擇的字典爲800維,爲了減少測試時間,我選擇了一個小一點的測試子集進行訓練,其準確率約爲72.8%。最高的是數學,準確率88%,最低的是語言,僅有53%。隨後我將字典擴展到1200維,得到了73.7%的準確率。之後我嘗試將語言類的字典單獨增長到1200維,其餘仍保持在800維,準確率也有73.4%。也就是說,我僅增加語言的字典維度就有了0.6%的準確率提高,將所有的字典全部增加到1200維,則僅僅再提高了0.3%。這說明可以通過有針對性的提高某一表現效果較差的分類器的字典來提高分類性能。實際上來看也是這樣,語言類的準確率在僅將其提高到1200維時由53.8%提高到61.9%。這也很好理解,畢竟字典維數高了,命中的機率肯定就大。然後我分別增加字典維度,最後的結果是其他字典2000維,農學和語言學增加到4000維,在總的113萬訓練集上得到了75.3%的準確率。對比全2000維的74.2%的準確率有了1.1%的提高,對比全800維的72.8%的準確率有了2.5%的提高。這是我沒有加入tfidf計算的權重,僅僅使用頻率的結果。在使用權重代替頻率之後的準確率爲76.6%,又有了1.3%的提高。因此我們可以看出,字典能否保證“該類命中率高同時其他類命中率低”,是取得較好效果的關鍵。簡而言之,字典的質量決定最後的效果。

最終的混淆矩陣如下:

可以看出,農學與解剖學、經濟學的重疊最高;解剖學與化學重疊率最高等。

最終的準確率和召回率如下:

時間花費方面,樸素貝葉斯的訓練時間爲5.27分鐘,測試時間爲117.29分鐘。測試時間很長是因爲要將字符串轉換爲詞向量等操作,因此耗費了許多時間。

3.4 Libsvm應用

Libsvm由於有了現成的工具箱,因此不必進行繁瑣的推導和實現,實在是很好。最開始我調用libsvm的python庫,但是遇到了很大的麻煩——訓練集需要一次性調入內存,然後整體進行計算,這要求很大的內存,我的8G是根本不夠看的。即使我使用了gc等機制仍然無法克服這個問題,因此最後去使用了C++版,C++可以說是運行速度又快,消耗內存又少。很完美了。

Libsvm中,我的主要工作是將數據轉換爲libsvm可以接受的格式:與樸素貝葉斯類似,但是樸素貝葉斯的詞向量僅爲800-4000維,但是libsvm要求將所有的字典合在一起,那麼總的維度在8000-40000維。40000維是無法忍受的,因此我選擇8000維。同時,由於這個向量十分稀疏,8000維中非零向量最多在100個左右,因此可以用“序號:值”的方式進行壓縮存儲,減少內存消耗。將數據轉換爲libsvm的形式並不難,只是有點繁瑣。

def create_train(self):
	current_path = os.getcwd()
	path = current_path+"\\train_data_chosen\\"
	if os.path.exists(current_path+"\\SVMData\\train_data_win_dic_800_20000.csv")==True:
		return        
	for i in range(len(self.major)):
		#train_data=csv.reader(open(path+self.major[i]+"_train_chosen_2.csv","r",encoding='utf-8'))
		train_data=pd.DataFrame(pd.read_csv(path+self.major[i]+"_train_chosen_2.csv",index_col=0,error_bad_lines=False))
		data_num=0
		#with open(path+self.major[i]+"_train_chosen_2.csv","r",encoding='utf-8',errors='ignore') as train_data:
		for j in range(len(train_data)):
			if data_num>20000:
				break
			data_num+=1
			try:
				tmp=train_data.iloc[j,2].split()
				if len(tmp)<20 or len(tmp)>300:
					continue
				word_vec=[0]*len(self.main_dict["num"])
				for word in tmp:
					if word in self.main_dict["num"]:
						word_vec[int(self.main_dict["num"][word])]=self.main_dict_num["tfidf"][word]
				win_vec=str(i)
				k=1
				for d in word_vec:
					if d>0:
						win_vec+=" "+str(k)+":"+str(round(d,10))
					k+=1
				with open(current_path+"\\SVMData\\train_data_win_dic_800_20000.csv","a",newline='',encoding='utf-8') as csvfile: 
					csv_writer = csv.writer(csvfile)
					csv_writer.writerow([win_vec])
			except:
				continue
		print("已創建"+self.major[i]+"的訓練集")

得到的輸入文件內容如下:

最開始我使用正態核,效果很差:準確率約爲34%。後來改用線性核,參數爲-t 0 –m 4000,即線性核,限制內存使用爲4G,效果如下圖:

在小規模樣本上準確率爲79.8576%,在總的數據集上準確率爲79.4989%。混淆矩陣如下:

準確率和召回率如下:

其最大缺點是訓練時間過長,約12小時,測試時間約14小時。這令我難以忍受。因此我開始尋找其他解決方案。

3.5 Liblinear應用

最終我找到的解決方案爲liblinear。其輸入格式與libsvm相同,也就不再需要進行格式轉換了。

最開始我使用參數爲-s 0,即L2正則邏輯迴歸,訓練速度極快,約五分鐘,測試速度也很快。最終準確率有78.9799%,如下圖:

之後我使用-s 2L2正則化L2損失支持向量分類,達到了最好的性能。其有一個參數C,即代價係數,針對這一系數進行交叉驗證,如下:

最終結果如下:

即當C取到64時準確率最高,使用新的參數再次訓練如下:

結果如下:

準確率高達81.9997%。已經很讓我滿意了。在極大縮短了訓練時間和測試時間的基礎上又明顯提高了準確率。混淆矩陣如下:

準確率和召回率如下:

3.6 Lda應用

Lda我是直接使用的python的lda包進行實驗。由於我使用的lda包沒有針對稀疏向量進行優化,因此輸入的向量都是近八千維。其直接後果是,使用我自己的電腦,無法對所有測試集和訓練集進行訓練和測試。實際上當數據超過十萬條,原始csv文件大小就超過了2G,這還是在我選擇使用六位小數保存而不是python默認的64位double的基礎上進行壓縮的。因此我只對五萬訓練集進行訓練,使用五萬測試集測試來看看效果。即使這樣,還需要一定的gc機制來釋放內存,否則會導致內存錯誤。實在是太難了。

做這個實驗process explorer是常開的,在跑lda時,如下圖:

可以看到雖然我已經用的是很少的數據集了,但內存佔用仍然高達5G。

首先我們載入數據,如下:

def load_data():
	'''
	加載lda中的數據
	'''
	current_path = os.getcwd()
	tmp1=pd.read_csv(current_path+"\\LDAData\\train_data_dic_800_5000.csv",header=None,dtype=np.int)
	print("train.CSV讀完了~")
	tmp2=pd.DataFrame(tmp1,dtype=np.int)
	print("train.CSV轉爲dataframe~")
	del tmp1
	gc.collect()
	X=tmp2.values
	print("label.dataframe轉爲list~")
	del tmp2
	gc.collect()
	current_path = os.getcwd()
	tmp1=pd.read_csv(current_path+"\\LDAData\\test_data_dic_800_5000.csv",header=None,dtype=np.int)
	print("test.CSV讀完了~")
	tmp2=pd.DataFrame(tmp1,dtype=np.int)
	print("test.CSV轉爲dataframe~")
	del tmp1
	gc.collect()
	Y=tmp2.values
	print("label.dataframe轉爲list~")
	del tmp2
	gc.collect()
	print(X.shape)
	print(X.sum())
	return X,Y

由於lda需要的數據爲list,因此要先讀入,然後轉爲dataframe,再轉爲list。之後把中間變量刪除。接下來進行訓練:

def Model(X,Y):
	'''
	構建模型
	'''
	model=lda.LDA(n_topics=10, n_iter=800, random_state=1)
	model.fit(X)  # model.fit_transform(X) is also available
	topic_word=model.topic_word_  # model.components_ also works
	n_top_words = 8
	print('-|'*50)
	doc_topic = model.doc_topic_
	for i in range(10):
		predict=[0]*10
		for j in range(5000):
		#print("{} (top topic: {})".format(" ", doc_topic[i].argmax()))
			predict[doc_topic[i*5000+j].argmax()]+=1
		print("第"+str(i)+"類準確率="+str(max(predict)*1.0/5000))
	print('-|'*50)
	plt.plot(model.loglikelihoods_[5:])
	plt.savefig('lda_test.png')
	Z=model.transform(Y)
	whole_right=0
	for i in range(10):
		predict=[0]*10
		for j in range(5000):
		#print("{} (top topic: {})".format(" ", doc_topic[i].argmax()))
			predict[Z[i*5000+j].argmax()]+=1
		print("第"+str(i)+"類準確率="+str(max(predict)*1.0/5000))
		whole_right+=max(predict)
	print("總準確率="+str(whole_right*1.0/50000))

方法fit用於訓練,lda爲無監督模型,類似kmeans,所以對訓練集的訓練就有一個準確度了。參數中,n_topics爲分類的類別數,這裏爲10;n_tier爲迭代次數,這裏選擇爲800。進行測試時使用transform方法即可,結果如下:

這是訓練集的:

這是測試集的:

可以看到準確率不是很高,這是由於lda對於長文本有較好的效果。我已經將數據限制在最少50個單詞了,但效果仍然不是十分理想。

​​​​​​​3.7 優化方向

第一個優化方向就是從最開始進行數據清洗,這是我在後來慢慢體會到的。可以使用kmeans等先進行一次聚類,將那些離羣點等都刪除掉,效果應該好很多。第二個就是數據源問題,應選擇更好的數據源,使得數據之間差異更大,但是目前我還沒找到。第三個是刪除過長或過短的數據,我還是太仁慈了,選擇區間爲[20,300]的數據,實際上如果數據的長度相近的話,效果會更加好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章