自然语言处理中的文本分类可以做很多事情,比如情感极性的分析、新闻内容的分类,今天要实现的是垃圾邮件的识别。垃圾邮件有一些共同的特征,尤其是表现在一系列词汇上,如"free","discount"等等。有监督的机器学习能从大量预标注的语料中分别学习垃圾邮件和正常邮件的用词规律,从而对新邮件做出预测。
机器学习离不开对对象的形式化表示。而向量化则是其形式化表示的核心。把文档转化成机器能看懂的“语言”——向量和矩阵,才能使机器读得懂、学得会。在表达文本的向量中有两种代表性的向量:词频向量和tf-idf向量。本文分别用词频向量和tf_idf方法建立特征,用朴素贝叶斯分类器给英文的垃圾/非垃圾邮件分类,准确率达到97%。其中,词频向量(98%)的效果比tf_idf方法(96%)准确率更高。这和我一开始想的不一样,毕竟tf-idf方法更高级,计算方法也比机械的词频计数更复杂。所以这也说明,没有一个模型在任何情况下都是最好的,要多次验证选取最好的模型。
本文用到的垃圾邮件预标注数据集:http://www.dt.fee.unicamp.br/~tiago/smsspamcollection/smsspamcollection.zipwww.dt.fee.unicamp.br
解压后把txt文件复制到excel表格中,在两列之间插入一列,写if函数用0和1表示ham/spam,便于作为标签。再在第一列添加列标签:type和Text。把表格存成csv文件。
# 读取csv文件
import pandas as pd
df = pd.read_csv("hamspam", encoding='latin')#因为是英文文本,编码统一为latin防止乱码
df.head()初始数据长这样
什么是词频向量和tf-idf向量?
词频向量是把训练文档中所有的词项拿出来形成集合,如{'apple', 'sale', 'monday', 'business',......},这个集合每个位置上的词是确定的。再用这个集合表示每一篇文档的用词情况。如果该文档中有10个apple,1个sale,3个monday,5个business,那么对于这篇文档,词频向量为{10,1,3,5...}。
tf-idf是另一种获得文档向量的方法,和机械地词频计数不同,它能“智能”地识别出哪些词是能体现这篇文档的特殊性的,而哪些词是无足轻重的。词语之于文档,并不是出现的越多就越有区别性,也并不是出现得越少就越不重要。比如说,虚词、人称代词在任何文档里都是很多的,比如a, an, the, she, it...,但是它们对于区分邮件是否为垃圾邮件起不到作用,有时甚至会干扰真正重要的词汇的识别,tf-idf方法正好屏蔽了这样的干扰。它让在所有文档中都频繁出现的词变得毫无意义,秘诀在于“逆文档频率”——也就是N/N(w)上。N表示文档总数,我们有5572个邮件。N(w)表示包含词w的文档数量。某个词的tf-idf,也就是这个词的重要性,是词频乘以逆文档频率。
我们假设一个极端情况:假如每个文档里都有个“you”,那么you的“逆文档频率”就是5572/5572=1。
一般来说,为了平滑起见,“逆文档频率”一般还要取log(默认以2为底)。这样一来'you'的逆文档频率就是log1=0。
那么如果假设you在邮件中的总词频是6800,但是因为它的逆文档频率为0,那么'you'的tf-idf就是6800*0=0。这就是我们所假设的极端情况:如果一个词在每一个文档中都出现,那么它对于区分文档的作用为0。
接下来用代码实现着两种获得向量表示的方法吧~
#声明两种构建文本特征的模型
#方法一:基于词频的文本向量
from sklearn.feature_extraction.text import CountVectorizer
vectorizer1 = CountVectorizer(binary=True)
#方法2:tfidf方法
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer2=TfidfVectorizer(binary=True)
分别用两种方法构建文本向量
X1 = vectorizer1.fit_transform(df.Text)
X2 = vectorizer2.fit_transform(df.Text)
y = df.type
把数据分为训练和测试数据
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X1, y, test_size=0.20, random_state=100)
print ("训练数据中的样本个数: ", X_train.shape[0], "测试数据中的样本个数: ", X_test.shape[0])
#上下两种每次只运行一种
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X2, y, test_size=0.20, random_state=100)
print ("训练数据中的样本个数: ", X_train.shape[0], "测试数据中的样本个数: ", X_test.shape[0])
利用朴素贝叶斯做训练
任务是求状态序列X(X1,X2,X3...),使它最好地解释观察值Y(是否垃圾邮件)。也就是求Y在已知X情况下的概率P(Y|X)为最大值时,X为多少。(就相当于是哪些关键词使得Y是垃圾邮件/非垃圾邮件)
根据贝叶斯公式:
P(Y∣X)=P(Y)P(X∣Y)/P(X)
由于P(X)、P(Y)是定值,可以从数据中直接计算获得,因此只需要算出P(X|Y)的最大值。注意,里面的X不是一个事件,而是一系列属性(X1,X2,....Xn),在垃圾邮件识别的任务中,X的每一个属性相当于单词集合中的每一个词。因此:
P(X|Y)=P(X1|Y)P(X2|Y)...P(Xn|Y)
在实际情况中,条件都是互相依赖的,一个词出现的概率与它的所有前序词相关:P(Xn)=P(Xn|X1,X2,...Xn-1)。但是如果这样代进去计算,所需要的时间复杂度就会成指数级上升,而且不能保证效果好。尤其是在当前的邮件分类任务中,由于形成的向量中,词与词之间本身就是独立的,其中的依赖关系已经在构建向量的时候被取消了,因此朴素贝叶斯法在这个任务中再合适不过了。
因此朴素贝叶斯在这里做了个重要的事:它把这个式子简化为每一个自变量都是独立的,不必依赖前序属性。P(Xn)=P(Xn), P(X)=P(X1)P(X2)...P(Xn)。这就是“朴素”的含义。
因此: P(X|Y)=P(X1|Y)P(X2|Y)...P(Xn|Y)
要求的就是上式取得最大值时X的序列(X1,X2...Xn)
总结一下朴素贝叶斯为什么朴素:假定每一个属性条件Xi是独立的。每个自变量特征都只与y值有直接关联,而互相之间没有关联。在垃圾邮件任务里面,向量里面的每个词项就相当于每个特征,它只和邮件是不是垃圾邮件相关,但互相之间没有关系。比如“sale”这个词只和预测值“垃圾/非垃圾邮件”有关,和其中的另一个词“Christmas”无关。(尽管它们有可能一起出现)。这样大大简化了后验概率的计算,节省了算力。
原理讲完了,现在用sklearn实现朴素贝叶斯吧:
from sklearn.naive_bayes import MultinomialNB#选取多项式分布贝叶斯,适用于多项式
from sklearn.metrics import accuracy_score
clf = MultinomialNB(alpha=1, fit_prior=True)#alpha用于平滑化,默认为1即可;fit_prior:是否学习类的先验概率,默认是True
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print("accuracy on test data: ", accuracy_score(y_test, y_pred))
打印混淆矩阵
from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, y_pred, labels=[0, 1])
结果显示,两种获得属性序列的方法(词频向量 vs. tf-idf)获得的准确率分别是0.98和0.96。有了这个分类器,对于新输入的文本(邮件内容),它就可以帮你判断是不是垃圾邮件啦~