1. 程式人生 > 其它 >樸素貝葉斯--垃圾郵件分類

樸素貝葉斯--垃圾郵件分類

樸素貝葉斯--垃圾郵件分類

一.垃圾郵件資料集

smsspamcollection資料集

本文資料集來源github:https://github.com/w1449550206/Spam-classification.git

ham:非垃圾簡訊

spam:垃圾簡訊

二.樸素貝葉斯原理

說到貝葉斯公式,可能大家並不陌生,這在概率論中也有學習。但是說到它的由來,大家知道嗎?

機器學習的兩個視角: 生成式 vs 判別式建模

判別式(Discriminative): modeling X  Y directly

生成式(Generative): Modeling assumptions about where data came from

1.判別式

根據訓練資料得到分類函式和分介面,比如說根據SVM模型得到一個分介面,然後直接計算條件概率 P(y|x) ,我們將最大的 P(y|x) 作為新樣本的分類。判別式模型是對條件概率建模,學習不同類別之間的最優邊界,無法反映訓練資料本身的特性,能力有限,其只能告訴我們分類的類別。
$$
P(C|X) C=c1,c2,...cL,X=(X1,..,Xn)
$$

2.生成式

一般會對每一個類建立一個模型,有多少個類別,就建立多少個模型。比如說類別標籤有{貓,狗,豬},那首先根據貓的特徵學習出一個貓的模型,再根據狗的特徵學習出狗的模型,之後分別計算新樣本 x 跟三個類別的聯合概率 P(x,y) ,然後根據貝葉斯公式:

分別計算P(y|x),選擇三類中最大的P(y|x)作為樣本的分類。
$$
P(X|C) C=c1,c2,...cL,X=(X1,..,Xn)
$$

樸素貝葉斯分類器

樸素貝葉斯分類器(Naïve Bayes Classifier)採用了“屬性條件獨立性假設”,即每個屬性獨立地對分類結果發生影響。
為方便公式標記,不妨記P(C=c|X=x)為P(c|x),基於屬性條件獨立性假設,貝葉斯公式可重寫為
$$
P(c|x)=\frac{P(c)P(x|c)}{P(x)}
$$

三.程式碼實現

1.預處理

資料處理的好壞直接關乎到最終結果的好壞,有時,甚至高於模型對結果的影響,所以在預處理時,需要多花功夫。

1.1 將資料用pandas處理

pandas對資料處理有很多天然的優勢,再結合jupyter,可以提高效率。

pandas資料型別具有的方法apply十分方便一次性對資料進行多種操作。

1.2 nltk工具包

1.2.1 安裝nltk工具包,不同於一般的包,pip以後並不能直接呼叫。因為它所需要的資源並沒有下載下來。比如停詞表。所以試用前需要下載安裝。import nltk, nltk.download()。但是大多數會失敗。也可以去官網官網試試:NLTK Data

令人難過的是,官網如果沒有梯子很可能也無法下載下來

所以,我推薦使用github的地址;NLTK Data

1.2.2 使用nltk中的stopword表,將無用的停詞去除。

1.2.3 WordNetLemmatizer()可以找到單詞原型,比如複數,過去式等,都返回原型,這樣能夠提高準確率。不過注意的是,在測試時,也得經過同樣的處理。

1.2.4 去除數字,在郵件中,數字大多數為電話號碼或者價錢,這對分類並沒有幫助,所以在這裡用startswith找到數字字串,並去除。

def data_processing(msg):
    msg = msg.str.lower()
    stwords = stopwords.words('english')

    def process(text):
        lemmatizer = WordNetLemmatizer()#找出單詞原型
        text = re.sub('[{}]'.format(".\!\,\$\£\...\?\-\&\@\:"), "", text)#去除句號等標點符號
        num_sign = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')#電話號碼對分類沒有作用,為下面去除數字字串做準備
        text = [lemmatizer.lemmatize(w) for w in text.split() if w not in stwords and (
                    w.startswith(num_sign) or w.endswith(num_sign)) is not True and lemmatizer.lemmatize(w) not in stwords]
        return text

    msg = msg.apply(process)
    msg = [" ".join(text) for text in msg]#轉成一個列表,為後續呼叫 CountVectorizer準備
    return msg

2.模型訓練

受到拉普拉斯修正思想的影響,在訓練時,如果某一個單詞條件概率為0,將它置為一個很小的數字,在這裡我設定的MIN_NUM=1e-8。

具體設定需要根據自己的資料集而定。一般的,相差100倍即可忽略不計,所以可以設定小於最小值100倍左右。

def train_bayes(x_train,y_train): #x_train已經經過了預處理
    #spam和ham的下標
    spam_index=np.where(y_train=="spam")[0]
    ham_index=np.where(y_train=="ham")[0]
    total=len(y_train)
    #先驗概率
    p_spam =len(spam_index)/total
    p_ham=len(ham_index)/total
    #例項化sklearn的CountVercorizer,返回list,對應每一個單詞出現的次數
    cv = CountVectorizer(lowercase=False)#預處理時已經變小寫
    counts = cv.fit_transform(x_train).toarray()
    #將spam和ham分開
    counts_spam=counts[spam_index,:]
    counts_ham=counts[ham_index,:]
    #記錄每一個單詞的條件概率
    P={}

    for i,word in enumerate(cv.get_feature_names_out()):
        #防止數字太小而顯示為0,MIN_NUM=1e-8
        P[word+'|spam']=counts_spam[:,i].sum()/total+MIN_NUM
        P[word+'|ham']=counts_ham[:,i].sum()/total+MIN_NUM
    return P,p_ham,p_spam,cv.get_feature_names_out()

3.在測試集上執行

if __name__=="__main__":
    path = "D:/DataSet/smsspamcollection/smsspamcollection.txt"
    data = pd.read_csv(path, delimiter='\t')
    x_train, x_test, y_train, y_test = train_test_split(data.iloc[:, 1], data.iloc[:, 0], random_state=7)
    y_test=y_test.tolist()
    x_train=data_processing(x_train)
    x_test=data_processing(x_test)
    bayse_P,p_ham,p_spam,words=train_bayes(x_train,y_train)
    #測試
    correct=0
    #遍歷每一個郵件內容
    for i,text in enumerate(x_test):
        #先乘上先驗概率
        ham_p=p_ham
        spam_p=p_spam
        for word in text.split():
            # 找到每一個單詞的條件概率並相乘
            if word in words :
                ham_p*=bayse_P[word+'|ham']
                spam_p*=bayse_P[word+'|spam']
            #沒有的話不能置為0,給予最小值1e-8
            else:
                ham_p *=MIN_NUM
                spam_p *=MIN_NUM
        #預測
        pred='ham' if ham_p>spam_p else 'spam'
        if pred==y_test[i]:
            correct+=1
    accuracy=correct/len(y_test)
    print("準確率為:",accuracy)


做了這麼多準備工作,還好結果沒讓人心寒。

準確率為: 0.9798994974874372

最後,附上完整程式碼:

import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction.text import CountVectorizer
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
from sklearn.model_selection import train_test_split
import re
import string
import numpy as np

MIN_NUM=1e-8
def data_processing(msg):
    msg = msg.str.lower()
    stwords = stopwords.words('english')

    def process(text):
        lemmatizer = WordNetLemmatizer()#找出單詞原型
        text = re.sub('[{}]'.format(".\!\,\$\£\...\?\-\&\@\:"), "", text)#去除句號等標點符號
        num_sign = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')#電話號碼對分類沒有作用,為下面去除數字字串做準備
        text = [lemmatizer.lemmatize(w) for w in text.split() if w not in stwords and (
                    w.startswith(num_sign) or w.endswith(num_sign)) is not True and lemmatizer.lemmatize(w) not in stwords]
        return text

    msg = msg.apply(process)
    msg = [" ".join(text) for text in msg]#轉成一個列表,為後續呼叫 CountVectorizer準備
    return msg



def train_bayes(x_train,y_train): #x_train已經經過了預處理
    #spam和ham的下標
    spam_index=np.where(y_train=="spam")[0]
    ham_index=np.where(y_train=="ham")[0]
    total=len(y_train)
    #先驗概率
    p_spam =len(spam_index)/total
    p_ham=len(ham_index)/total
    #例項化sklearn的CountVercorizer,返回list,對應每一個單詞出現的次數
    cv = CountVectorizer(lowercase=False)#預處理時已經變小寫
    counts = cv.fit_transform(x_train).toarray()
    #將spam和ham分開
    counts_spam=counts[spam_index,:]
    counts_ham=counts[ham_index,:]
    #記錄每一個單詞的條件概率
    P={}

    for i,word in enumerate(cv.get_feature_names_out()):
        #防止數字太小而顯示為0,MIN_NUM=1e-8
        P[word+'|spam']=counts_spam[:,i].sum()/total+MIN_NUM
        P[word+'|ham']=counts_ham[:,i].sum()/total+MIN_NUM
    return P,p_ham,p_spam,cv.get_feature_names_out()






if __name__=="__main__":
    path = "D:/DataSet/smsspamcollection/smsspamcollection.txt"
    data = pd.read_csv(path, delimiter='\t')
    x_train, x_test, y_train, y_test = train_test_split(data.iloc[:, 1], data.iloc[:, 0], random_state=7)
    y_test=y_test.tolist()
    x_train=data_processing(x_train)
    x_test=data_processing(x_test)
    bayse_P,p_ham,p_spam,words=train_bayes(x_train,y_train)
    #測試
    correct=0
    #遍歷每一個郵件內容
    for i,text in enumerate(x_test):
        ham_p=p_ham
        spam_p=p_spam
        for word in text.split():
            # 找到每一個單詞的條件概率
            if word in words :
                ham_p*=bayse_P[word+'|ham']
                spam_p*=bayse_P[word+'|spam']
            #沒有的話不能置為0,給予最小值1e-8
            else:
                ham_p *=MIN_NUM
                spam_p *=MIN_NUM
        #預測
        pred='ham' if ham_p>spam_p else 'spam'
        if pred==y_test[i]:
            correct+=1
    accuracy=correct/len(y_test)
    print("準確率為:",accuracy)