第7章 基於樸素貝葉斯的垃圾郵件分類

weixin_34234823發表於2018-09-22

1. 引言

在正式學習樸素貝葉斯之前,需要明確的是機器學習所要實現的是基於有限的訓練樣本集儘可能準確地估計出後驗概率P(c|x),即根據特徵得到所屬類別的概率,首先引入兩個概念。
判別式模型(discriminative models):給定x,直接建模P(c|x)來預測c,比如決策樹、BP神經網路、支援向量機等;
生成式模型(generative models):聯合概率分佈P(x,c)進行建模,然後由此獲得p(c|x),典型代表就是下面要講的樸素貝葉斯。

2. 問題描述

本文基於樸素貝葉斯構建一個分類垃圾郵件的模型,研究物件是英文的垃圾郵件,一來英文垃圾郵件資料集比較容易找到比較多,二來難度較中文的稍小,並且很多人都在用英文郵件,可比性較強,適合新手入門。

3. 演算法流程

A. 資料準備

a. 資料集使用 UCI Machine Learning Repository,已上傳至GitHub,可進行下載。
b. 訓練集中,positive郵件數量為12500,negative郵件數量為12500;測試集中,positive郵件數量和negative郵件數量同為12500。
c. 資料預處理。去除郵件中的特殊符號以及沒有任何意義的詞語,如一些html標籤或者"the"、"a"等詞語。具體方式為使用現成的停用詞表進行操作。

B. 理論依據

a. 貝葉斯定理

貝葉斯定理是該演算法的核心思想。


13122291-043b1cc361ecd2c2.png
貝葉斯定理

其中,P(c)是類先驗概率,p(x|c)是條件概率,p(x)是“證據因子”(在同一問題中p(x)全相同);
p(c)=類別c的數目/總數。
因此,求解p(c|x)的問題變成了求解p(x|c)的問題,接下來引出下面兩種解決方法。

b1. 極大似然估計(源自頻率主義學派)

試錯,此路不通。
假設p(x|c)具有確定的形式並且被引數向量θc唯一確定,則要利用訓練集估計θc的值,即p(x|c)-->p(x|θc)。
Frequentist:引數雖然未知,但是客觀存在的固定值。
Bayesian:引數是未觀察到的隨機變數,其本身也有分佈。
若樣本Dc滿足獨立同分布,則引數θc對於資料集Dc的似然是

13122291-272db724a7800bbd.png
極大似然估計

注:使用對數似然的原因是避免連乘造成下溢。
這種引數化的方法的弊端是結果的準確性嚴重依賴於所假設的概率分佈形式是否符合潛在的真實資料分佈。下面就是貝葉斯的優越性了。

b2. 樸素貝葉斯分類器

樸素貝葉斯分類器(naive Bayes classifier)採用了屬性條件獨立性假設(attribute conditional independence assumption),即對於已知類別,假設所有的屬性都相互獨立(雖然這是不對的,但至少在垃圾郵件分類這裡假設所有的屬性獨立取得了還不賴的結果)。換言之,假設所有屬性獨立地對分類結果發生影響。

13122291-0f4fe68c1acee168.png
樸素貝葉斯分類器

另外,為了避免進行計算時c類樣本的數量和在樣本中第i個屬性的取值為0導致p(c)或者p(x|c)為0,這裡使用拉普拉斯修正(Laplacian correction),即
13122291-bae24b31851fd770.png
Laplace

C. 垃圾郵件實際問題

  • a. 建立詞彙表(之後可以建立TF-IDF詞彙表,現在僅僅簡化)
    統計郵件中出現的所有詞彙並記作一維向量的形式,即(abandon,ahead,buy,go,...,zero)。
def word_create(ori_data):
    #還沒有進行去停用詞的操作nltk.stopwoords的包沒能下載下來
#    ori_data.data = [word for word in ori_data.data if(word not in stopwords.words('english'))]
    print("Vectorzing dataset ...")
    #建立一個集合列表
    word_dic = set([])
    #詞向量的時間
    vectorTime = time()
    #詞典的構造
    for doc in ori_data.data:
        #doc是byte,這裡將byte轉化為string
        doc = str(doc, encoding = "utf-8")
        #使用正規表示式將特殊符號去除
        doc = re.sub("[\s+\.\!\/_,$%^*(+\"\'-]+|[+——!,。?、~@#¥%……&*()<>]+", " ", doc)
        #使用預設的空格方式將email分隔開,然後轉化為小寫字母,與原集合取並集
        word_dic = word_dic|set(doc.lower().split())
    #向量化的時間和詞典中詞的數量
    print("Vectorzing time:{0}\nThe number of word_dictionary:{1}".format(vectorTime,len(word_dic)))
    return list(word_dic)
  • b. 詞向量表示(之後使用Word2Vec表示詞向量,現在僅僅簡化)
    將每封郵件依據詞彙表以向量的形式表示出來,該詞在郵件中出現記為1,反之記為0,即比如(1,1,0,0,...,1)。
    無疑這會造成維數災難,Word2Vec會好很多,不過現在的計算量不是很大也不涉及其他演算法就用one-hot方式表示了。
def doc_represent(wordDic,ori_data):
    #建立一個文件數(行)*詞向量(列)長度的二維陣列
    doc_re = numpy.zeros((len(ori_data.data),len(wordDic)),dtype= numpy.int)
    #計數器
    count = 0
    #用來記錄詞向量表示時間
    representTime = time()
    for doc in ori_data.data:
        #同word_create函式,進行同樣的操作
        doc = str(doc, encoding = "utf-8")
        doc = re.sub("[\s+\.\!\/_,$%^*(+\"\'-]+|[+——!,。?、~@#¥%……&*()<>]+", " ", doc)
        for word in doc.lower().split():
            if word in wordDic:
                #將對應詞向量位置置1
                doc_re[count][wordDic.index(word)] = 1
        count = count+1
    print("Represent doc time:{0}\nThe number of doc:{1}".format(representTime-time(),len(doc_re)))
    #返回表示文件的二維陣列
    return doc_re
  • c. 計算先驗概率p(c)
    p(正常郵件)=D(正常郵件數)+1/D(總郵件數)+2
    p(垃圾郵件)=D(垃圾郵件數)+1/D(總郵件數)+2
def pre_probabilty(ori_data):
    s_pre_pro = []

    #正常郵件的先驗概率
    P_normal = (normal + 1.0)/(len(ori_data.data) + 2.0)
    s_pre_pro.append(P_normal)
    #垃圾郵件的先驗概率
    P_spam = (spam + 1.0)/(len(ori_data.data) + 2.0)
    s_pre_pro.append(P_spam)
    #返回先驗概率的列表
    return s_pre_pro
  • d. 計算每個詞彙的條件概率
    p(abandon=1|正常郵件)=D(正常郵件中有abandon的數目)+1/D(正常郵件數)+2
    p(abandon=1|垃圾郵件)=D(垃圾郵件中有abandon的數目)+1/D(垃圾郵件數)+2
    p(ahead=1|正常郵件)=D(正常郵件中有ahead的數目)+1/D(正常郵件數)+2
    p(ahead=1|垃圾郵件)=D(垃圾郵件中有ahead的數目)+1/D(垃圾郵件數)+2
    ...
    p(zero=1|正常郵件)=D(正常郵件中有zer的數目)+1/D(正常郵件數)+2
    p(zero=1|垃圾郵件)=D(垃圾郵件中有zero的數目)+1/D(垃圾郵件數)+2
#計算每個詞在正常郵件垃圾郵件中的數目
def wordNum_email(email_repre,wordDic):
    #用二維向量儲存
    num_word = numpy.zeros((2,len(wordDic)),dtype= numpy.int)
    for i in range(len(wordDic)):
        #在正常郵件的數目
        for j in range(normal):
            num_word[0][i] += email_repre[j][i]
        #在垃圾郵件中的數目
        for j in range(normal, spam+normal):
            num_word[1][i] += email_repre[j][i]
    return num_word

#條件概率
def con_probabilty(email_repre,wordDic):
    #得到每個詞彙在正常郵件、垃圾郵件中的數目
    word_num = wordNum_email(email_repre,wordDic)
    word_pro = numpy.zeros((2,len(wordDic)),dtype = numpy.double)
    for i in range(len(wordDic)):
        word_pro[0][i] = round((word_num[0][i]+1)/(normal + 2),8)
        word_pro[1][i] = round((word_num[1][i]+1)/(spam + 2 ),8)
    return word_pro
  • e. 測試
    p(email1=正常郵件)=p(正常郵件)p(zero|正常郵件)p(buy|正常郵件)p(achieve|正常郵件)=...
    p(email1=垃圾郵件)=p(垃圾郵件)
    p(zero|垃圾郵件)p(buy|垃圾郵件)p(achieve|垃圾郵件)=...
    若p(email1=正常郵件)>p(email1=垃圾郵件),則為正常郵件;
    若p(email1=正常郵件)<p(email1=垃圾郵件),則為垃圾郵件;
    若p(email1=正常郵件)=p(email1=垃圾郵件),則用一個隨機數隨機決定。
    將測試結果與實際結果進行比較,並記錄下分類正確和分類錯誤的數目,計算出TP、FP、FN和TN,最後得到準確率、精確率和召回率。如果有必要的話,畫出相應的圖進行說明。
    看到要計算準確率、精確率和召回率這裡我表示很慚愧,只計算了準確率
#測試
def test_spam(test_repre,pre_pro,con_pro):
    email_pro = numpy.zeros((len(test_repre),2),dtype = numpy.double)
    email_judge = []
    normal_num = 0
    spam_num = 0
    for i in range(len(test_repre)):
        email_pro[i][0] = round(pre_pro[0],8)
        email_pro[i][1] = round(pre_pro[1],8)
        for j in range(len(test_repre[0])):
            if test_repre[i][j] != 0:
                email_pro[i][0] *= con_pro[0][j]
                email_pro[i][1] *= con_pro[1][j]
        if email_pro[i][0] > email_pro[i][1] :
            email_judge.append(0)
        elif email_pro[i][0] < email_pro[i][1] :
            email_judge.append(1)
        else :
            if random.random() > 0.5:
                email_judge.append(1)
            else:
                email_judge.append(0)
    for i in range(normal_test):
        if email_judge[i] == 0:
            normal_num +=1
    for i in range(normal_test,len(test_repre)):
        if email_judge[i] == 1:
            spam_num +=1
    print("email_judge\n")
    print(email_judge)
    print("normal_num="+str(normal_num)+"\nspam_num="+str(spam_num))
    return (normal_num + spam_num)/len(test_repre)

4.結論

由於諸多因素(設定閾值去除稀有詞彙,使用TF-IDF表示詞彙向量,使用log防止乘數下溢)沒有考慮,所以訓練大量的資料集時會導致記憶體不足的現象,故只使用了36個訓練資料進行訓練,使用36個測試資料測試準確率。最終準確率只有55.5%多,只比瞎猜的大了一點,個人覺得還是訓練集太少,再放大一點肯定還是會提升很多的。樸素貝葉斯的基本思想在這,這裡我就不多改了,其他的問題之後注意,要進入下一個演算法的學習了。在這裡附上程式碼地址,(樸素貝葉斯-spamClassification)[https://github.com/RuiDream/MachineLearning.git]
自己手寫的Bayes結果展示:

13122291-21441e276dbcbfbe.png
手寫Bayes結果

調包的結果展示(人家的很齊全,還有混淆矩陣,服):
13122291-4540576044df00d9.png
庫中Bayes結果

13122291-07261d4c1193a5dc.png
庫中Bayes混淆矩陣

注:準確率相差這麼大很大的一個原因是訓練集的大小不同,大的為25000,我自己寫的為36.。。不過還是要怪自己的程式碼不精,革命剛剛起步……

相關文章