模式分類與應用-貝葉斯垃圾郵件分類
垃圾郵件分類
任務要求
使用檔案spambase.data中的資料,訓練垃圾郵件分類的貝葉斯分類器,並測試分類效能。
資料初步分析
spambase.data是一個垃圾郵件的資料庫,來自於惠普公司的Hewlett Packard Labs實驗室,採集時間是1999年的6~7月份。
該資料庫中包含了4601個樣本,其中1813條為垃圾郵件(spam);每個樣本有58個屬性。在資料檔案中,每行為一個樣本,58個屬性按順序排列,使用","分隔。格式如下:
圖 21 資料檔案的格式
各屬性的含義和數值範圍如下表所列:
表格 21 樣本屬性說明
屬性序號 |
含義 |
範圍 |
最大值 |
1-48 |
特定單詞的出現頻率 |
[0, 100] |
<100 |
49-54 |
特定字元的出現頻率 |
[0, 100] |
<100 |
55 |
大寫字母遊程均長 |
[1, …] |
1102.5 |
56 |
最長大寫字母遊程 |
[1, …] |
9989 |
57 |
大寫字母遊程總長 |
[1, …] |
15841 |
58 |
垃圾郵件標識(1代表垃圾郵件) |
{0, 1} |
1 |
第1-54屬性可以看成統計特性相似的一類,因為它們都代表了單詞或字元的出現頻率,範圍都是[0, 100]. 考察第1個屬性的分佈規律,對全部4601個樣本做頻數統計,如圖2-1(a), 可見大部分樣本的值為0,還有少部分分佈在1以內,均值在零點幾左右。
同樣,第55~57個屬性的統計特性應當是一致的,都表示遊程的長度。對第55個屬性的數值作頻數統計,如圖2-1(b), 雖然數值的分佈範圍很大,但主要集中分佈在數值較小的區域。
圖 22 (a)屬性1的數值分佈, (b)屬性55的數值分佈
顯然,這樣的分佈不能用高斯分佈或其他分佈函式來表示。所以可以建立離散的概率密度函式,用樣本的頻數估計概率密度。
頻數統計首先確定組距,或者稱之為量化階:
\[Quantization\_order = \frac{{Max - Min}}{{Bins}}\]
其中,Max是可接受資料的最大值,min是最小值,Bins為組數。
然後建立頻數表,其長度就是組數Bins。遍歷待統計的資料,判斷每個資料落入何組中,頻數表中對於的數值就加1.
如果要得到概率,只需將頻數除以總樣本數。
貝葉斯分類器
原理:
貝葉斯分類是一種有監督的分類方法。其原理如下:首先統計各類的先驗概率,以及類條件概率分佈,然後通過貝葉斯公式:
\(P({\omega _i}|{\mathbf{x}}) = \frac{{P({\mathbf{x}}|{\omega _i}) \cdot P({\omega _i})}}{{P({\mathbf{x}})}} = \frac{{p({\mathbf{x}}|{\omega _i})d{\mathbf{x}} \cdot P({\omega _i})}}{{p({\mathbf{x}})d{\mathbf{x}}}} = \frac{{p({\mathbf{x}}|{\omega _i}) \cdot P({\omega _i})}}{{p({\mathbf{x}})}}\)( 1)
可以利用已知量轉換得到後驗概率 \(P({\omega _i}|{\bf{x}})\),即表示在特徵x時屬於類\({\omega _i}\) 的概率分佈。
從式(1)還可以看出,因為 \(P({\bf{x}}|{\omega _i})\)=\(p({\bf{x}}|{\omega _i})d{\bf{x}}\),分子分母同時消去後,後驗概率分佈實際上也可以通過先驗概率密度來計算。而且實際上,\(p({\bf{x}}|{\omega _i})\) 比\(P({\bf{x}}|{\omega _i})\)更容易表示,所以一般使用\(p({\bf{x}}|{\omega _i})\)計算後驗概率。
樸素貝葉斯
如果樣本的特徵向量x維數很大,會給條件概率密度函式\(p({\bf{x}}|{\omega _i})\)的求取帶來困難。假設各屬性的值相互獨立,則\(p({\bf{x}}|{\omega _i})\)可以表示為:
$$p({\bf{x}}|{\omega _i}) = \prod p({x_k}|{\omega _i})$$(2)
將式(2)代入式(1),就得到了樸素貝葉斯的表示式:
$$P({\omega _i}|{\bf{x}}) = \frac{{\prod p({x_k}|{\omega _i}) \cdot P({\omega _i})}}{{p({\bf{x}})}} = $$(3)
而\(\prod p({x_k}|{\omega _i})\)中的每一項,可以使用第2節中所說的頻數進行估計。
決策規則
對於兩類分類問題,根據後驗概率的大小判決所屬分類,即:
基於上式,代入貝葉斯公式,由於分母都一樣,可以得到變換形式的決策式:
(4-a)
(4-b)
(4-c)
他們都是等價的,針對不同的後驗概率形式,選擇最簡單的表示式即可。在本題中,使用4-a更方便。
程式設計
通過上述原理的分析,該題的主要計算工作可以分為4步,用順序結構即可實現。框圖歸納如下:
圖 41程式流程圖
圖中標註的步驟編號與程式原始碼中的註釋相對應。
程式設計實現
程式按照框圖中的4個主要步驟設計。為了將更清楚地顯示執行過程,在相同目錄下建立了一個文字檔案Tracking.log,每一個Step執行完成後,使用file.write()函式在檔案中寫入執行時間戳和執行結果。
建立文字檔案的程式碼如下,使用寫入模式w,若不存在則建立,若已存在同名檔案,會清空內容後重新建立。
file= open('Tracking.log','w',buffering=100)#建立一個檔案,用於寫入執行報告
Step1讀入資料,並統計長度
spambase.data中的資料一行有58個,用","分隔。使用numpy的loadtxt()函式可以直接讀入,返回一個array物件屬性。然後使用array.shape方法得到資料的維度。
original_data_readin = np.loadtxt("spambase.data", dtype=float, delimiter=",")
size = original_data_readin.shape
sampleNums_of_classALl = size[0];#樣本總數
sampleNums_of_class_spam=int(sum(original_data_readin[:,-1]))#統計屬於垃圾郵件的樣本數量(1813)
sampleNums_of_class_good= sampleNums_of_classALl - sampleNums_of_class_spam #正常郵件的數量
length_of_attributes = size[1];#屬性向量的長度(應該是58)
程式執行後,original_data_readin變數中儲存了讀取的原始資料, sampleNums_of_classALl是所有樣本的數量,sampleNums_of_class_spam是垃圾郵件樣本數量,sampleNums_of_class_good是正常郵件的樣本數量,length_of_attributes是特徵屬性長度。
Step2隨機產2/3的資料用於訓練,並計算類先驗概率
這一部分位於原始碼的line49-70.
所給的資料庫中,垃圾郵件和非垃圾郵件已經分類,前面是垃圾郵件,後面是非垃圾郵件。以垃圾郵件中產生2/3訓練樣本為例,說明隨機抽取的方法:
遍歷所有垃圾郵件樣本,使用random.uniform(0,3)函式產生0-3均勻分佈的隨機數,與1比較,如果小於1,歸類為測試樣本;大於等於1,則歸類為訓練樣本。將垃圾郵件的訓練樣本存入矩陣sampls_spam_train,測試樣本存入sampls_spam_test.
同理,對正常郵件也進行2/3隨機抽取,訓練樣本存入矩陣sampls_good_train,測試樣本存入sampls_good_test.
隨後,根據sampls_spam_train和sampls_good_train的長度,計算了類先驗概率和.
檔案Tracing.log中寫入這一步的執行結果。
Step3 根據頻數估計類條件概率
如第二節中所述,使用頻數估計類條件概率密度。這裡定義了一個函式:Hist_Estimate(data,bins, min,max,normal),具體如下:
def Hist_Estimate(data=np.zeros((1,54)),bins=quant_order, min=0.0,max=100,normal=1 ):
#@對輸入的資料進行頻數估計,返回bins×data.shape[1]維的向量
deltaX=float(bins)/(max-min)
#print deltaX
pdf=np.zeros((bins,data.shape[1]))
for i in range(data.shape[1]):#pdf的列遍歷
#print i
for j in range (data.shape[0]):#統計頻數
#print str(i)+str(j)
pdf[int(deltaX*data[j,i]),i] += 1
if normal!=1: #返回不歸一化的頻數
return pdf
else: #返回歸一化的頻數,可以看作概率密度函式
return pdf/data.shape[0]
它實現了第2節中所述的頻數統計功能,輸入data是N*n的樣本(N為樣本數,n為特徵數),bins量化階數,min和max分別表示頻數表的最小和最大值,normal=1返回歸一化的頻率,否則返回頻數。返回值是一個bins*n的矩陣,每一列就是一個屬性的頻數(或頻率)表。由於樣本中前54個屬性的值範圍為[0,100],而55,56,57三個屬性的值從1到幾千,所以使用對他們分兩種情況求頻數。呼叫Hist_Estimate()函式時,前者取min=0, max=100,後者的min=1, max=20000. 量化階bins都取同樣值,這樣返回的頻數表長度都是一樣的。
pdf54_spam = Hist_Estimate(sampls_spam_train[:,:54],bins=quant_order)
pdf54_good = Hist_Estimate(sampls_good_train[:,:54],bins=quant_order)
pdf57_spam = Hist_Estimate(sampls_spam_train[:,54:57],min=0.0,max=20000,bins=quant_order)
pdf57_good = Hist_Estimate(sampls_good_train[:,54:57],min=0.0,max=20000,bins=quant_order)
pdf54_spam中儲存了垃圾郵件類中前54個屬性的頻數表,pdf57_spam是後3個屬性的頻數表。非垃圾郵件也類似處理。
這部分的程式碼在line 84 – 103.
Step4 使用測試集資料對分類器作效能測試
通過之前的步驟,已經得到了類先驗概率和類條件概率密度.由式(3)可以計算出後驗概率,再根據式(4-a)做出決策。
函式Gx_Compute(data,px,bins, min,max)用於計算一個輸入樣本的各個屬性的先驗概率乘積,即. 由於有些值的概率密度為0,為了避免乘0後結果恆為0,加上了一個微小的常數0.001,所以實際上計算的是.函式Gx_Compute()的輸入引數data是一個樣本中某些屬性組成的向量(1*nk),px是對於的頻率表(bins*nk), bin是量化階數,min和max是頻率表中的最小值和最大值。
首先測試垃圾郵件的分類效能。呼叫函式Gx_Compute(),對sampls_spam_test中的一個樣本,計算式(4-a)左右兩項。由於57個屬性分成了兩種量化階不同的統計,所以計算需要使用兩次Gx_Compute(),計算也需要使用兩次Gx_Compute(). 比較和的值,如果前者大,則判為垃圾郵件,即正確分類了,計數值amount_of_test_spam_correct加一。遍歷完所有待測試垃圾郵件之後,計算正確識別的比例,即amount_of_test_spam_correct / amount_of_test_spam_total.垃圾郵件分類測試的程式碼如下:
#對spam測試樣本進行檢驗
amount_of_test_spam_total = sampls_spam_test.shape[0]
amount_of_test_spam_correct = 0
for i in range(amount_of_test_spam_total):
if (Gx_Compute(sampls_spam_test[i,0:54],pdf54_spam)*Gx_Compute(sampls_spam_test[i,54:57],pdf57_spam,quant_order,0.0,20000)*Pw1>Gx_Compute(sampls_spam_test[i,0:54],pdf54_good)*Gx_Compute(sampls_spam_test[i,54:57],pdf57_good,quant_order,0.0,20000)*Pw2 ):
amount_of_test_spam_correct +=1
print u'垃圾郵件正確檢出率'
print float(amount_of_test_spam_correct) / amount_of_test_spam_total
檢驗正常郵件識別效能的方法與之相同,只是改變了輸入樣本,並反轉判決條件。
這部分的程式碼位於line 124 – 145, 檢測結果會列印在螢幕上,同時寫入日誌。
執行結果
使用pyinstaller工具打包編譯成了單檔案exe,將資料檔案和程式放在同一目錄下,執行exe。
圖 61資料檔案和編譯得到的程式
圖 62 執行結果
等待程式將所需的動態連結庫解壓到系統快取後,就會執行python程式,本次執行,垃圾郵件檢出率在0.83,正常郵件識別率在0.93,運行了大約1.15秒。開啟目錄下生成的Tracking.log文字檔案,如下所示:
圖 63 log檔案
檔案中記錄了每步完成的時間,也記錄了關鍵步驟的運算結果。從結果來看,程式功能是正確的。
總結
本次的程式設計過程比較順利,寫完一個步驟就進行驗證,沒有出現找很久Bug的情況。由於不涉及複雜矩陣運算,幾乎所有的運算操作都是從0開始自己寫的,沒有使用別的庫,也就沒有參考程式。但這直接造成了程式冗長繁雜,寫的有些隨意,有些計算方法也不是最優的。幸好我認為變數定義還是很清楚的,看到名稱可以大概想到其含義,否則就真的寫完之後我自己也看不懂了。
在一開始寫的時候,我感覺訓練好分類器,然後檢測一下分類結果,輸出一個正確率,程式就結束了,似乎不是很直觀,而且編寫時也正好需要除錯,突發奇想就想把關鍵步驟的執行結果寫到文字檔案裡。後來又加了一個毫秒精度的時間戳,這樣每一步執行了多少時間,都會寫到日誌裡,這就很有意思了,看到日誌似乎可以回顧程式的執行過程。
這次還使用了pyinstaller工具,將python程式編譯成了exe,這樣即使在沒有安裝python的計算機上也可以執行。
此外,還遇到了一些問題。首先是print語句列印中文亂碼問題,表現為在IDLE的shell中是正常的,但在控制檯中執行就會亂碼。最後查詢到原因是系統的編碼與py2.7不同。這在python3中已經解決,但對於python2,需要對編碼進行轉換:
import sys
type = sys.getfilesystemencoding()
print '輸出中文測試..'.decode('utf-8').encode(type)
或者在字元前加u,表示unicode編碼:
print u'中文測試'
對於概率密度的計算,使用了頻數進行估計。這就涉及到量化階的問題,即把多大的範圍看作一個頻數統計區間。如果把量化階取得過大,一些原本有差異的數值會落入同一個統計區間中,這樣就使得模型的靈敏度降低,影響分類效果;但是如果量化階取得過小,即頻數分組很多,在有限的訓練樣本下,就會造成估計的失準,最後也影響分類準確率。程式中使用了500個量化值進行統計,效果較好。
取不同的頻數量化組數,識別率變化如下,說明量化組數在200到1000內比較理想。
頻數量化組數 |
垃圾郵件識別率 |
非垃圾郵件正確識別率 |
100 |
0.835820895522 |
0.891832229581 |
200 |
0.85641025641 |
0.929399367756 |
500 |
0.855687606112 |
0.949506037322 |
1000 |
0.809825673534 |
0.953241232731 |
1500 |
0.786991869919 |
0.93237704918 |
2000 |
0.802931596091 |
0.932584269663 |
3000 |
0.78813559322 |
0.924528301887 |
此外,垃圾郵件的識別率總是比正常郵件的識別率低,我就想到一句話:正常郵件都是相似的,垃圾郵件各有各的不同。
好了,寫到這裡正好跨年了,我就回去把第一頁的日期也改了,祝大家新年快樂!