朴素贝叶斯--垃圾邮件分类

一.垃圾邮件数据集

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)

$$

自然语言处理中文垃圾邮件分类 垃圾邮件分类算法_条件概率_02

2.生成式

一般会对每一个类建立一个模型,有多少个类别,就建立多少个模型。比如说类别标签有{猫,狗,猪},那首先根据猫的特征学习出一个猫的模型,再根据狗的特征学习出狗的模型,之后分别计算新样本 x 跟三个类别的联合概率 P(x,y) ,然后根据贝叶斯公式:

自然语言处理中文垃圾邮件分类 垃圾邮件分类算法_数据集_03

分别计算P(y|x),选择三类中最大的P(y|x)作为样本的分类。
$$
P(X|C) C=c1,c2,...cL,X=(X1,..,Xn)
$$

自然语言处理中文垃圾邮件分类 垃圾邮件分类算法_自然语言处理中文垃圾邮件分类_04

朴素贝叶斯分类器

朴素贝叶斯分类器(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)

做了这么多准备工作,还好结果没让人心寒。

自然语言处理中文垃圾邮件分类 垃圾邮件分类算法_自然语言处理中文垃圾邮件分类_05

准确率为: 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)