朴素贝叶斯--垃圾邮件分类
一.垃圾邮件数据集
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)