朴素贝叶斯算法:实现邮件分类
朴素贝叶斯算法:实现邮件分类
一、实验准备
1、实验内容和目的
-
使用5000封邮件作为训练集,训练朴素贝叶斯分类器,然后使用该分类器对1000封邮件进行分类,给出准确率结果
-
其中训练集为文件spam_train.txt,测试集为文件spam_test.txt。它们的存储形式为:每封邮件以一个标签(0或1)开头,然后以一个换行符来标示一封邮件的结尾
-
这里先给出我最后测试的准确率:98.3% ,接下来对我的实现过程进行详细说明
2、实验原理
- 朴素贝叶斯算法是有监督的学习算法,可以用来解决分类问题。朴素贝叶斯基于 贝叶斯定理 和 特征条件独立假设,它假设每个特征条件之间是独立的, 比如一个单词出现的概率和其它相邻单词没有关系,因此我们称之为“天真”的贝叶斯算法
2.1 基于贝叶斯决策理论的分类方法
-
朴素贝叶斯是贝叶斯决策理论的一部分,要完全掌握朴素贝叶斯的话有必要了解一下贝叶斯决策理论
-
假设有一个数据集,由两类数据组成(体现为圆点和三角形),分布情况如下图所示:
-
我们现在用p0(x,y)表示数据点(x,y)属于类别0(图中用圆点表示的类别)的概率,用p1(x,y)表示数据点(x,y)属于类别1(图中用三角形表示的类别)的概率,那么对于一个新数据点(x,y),可以使用下面的规则来判断它的类别:
-
如果p0(x,y) > p1(x,y),那么数据点(x,y)的类别为0
-
如果p1(x,y) > p0(x,y),那么数据点(x,y)的类别为1
-
-
也就是说,我们会高概率对应的类别。这也就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策
-
2.2 条件概率
-
在2.1中提到了概率p0和p1,那么如何计算p0和p1?这就需要理解条件概率的计算。相信只要上过概率论,肯定对符号p(x,y | c)很熟悉,下面通过一个例子来复习一下条件概率
-
假设现在有一个装了7块石头的罐子,其中3块灰色的,4块黑色的(如下图所示)。如果从罐中随机取出一块石头,那么是灰色石头的可能性是多少?显然是3/7。我们使用P(gray)来表示取到灰色石头的概率,其概率值可以通过灰色石头数量除以总的石头数量来得到
-
那如果这7块石头如下图所示放在两个桶中,那么上述概率应该怎么计算?
-
要计算P(gray)或者P(black),事先得知道石头所在桶的信息会不会改变结果?你有可能已经想到计算从B桶中取到灰色石头概率的办法,这就是所谓的条件概率。假定计算的是从B桶中取到灰色石头的概率,这个概率可以记作P(gray | bucketB),我们称之为“在已知石头出自B桶的条件下,取出灰色石头的概率”。不难算出,P(gray | bucketA)=1/2,P(gray | bucketB)=1/3。条件概率的计算公式如下所示:
- P(gray | bucketB) = P(gray and bucketB) / P(bucketB)
-
2.3 使用条件概率来分类
-
2.1节提到贝叶斯决策理论要求计算两个概率p0(x,y)和p1(x,y):
-
如果p0(x,y) > p1(x,y),那么数据点(x,y)的类别为0
-
如果p1(x,y) > p0(x,y),那么数据点(x,y)的类别为1
-
-
但这两个准则并不是贝叶斯决策理论的所有内容。使用p0()和p1()只是为了尽可能简化描述,而真正需要计算和比较的是p(c0 | x,y)和p(c1 | x,y)。这些符号所代表的具体意义是:给定某个由x、y表示的数据点,那么该数据点来自类别c0的概率是多少?数据点来自类别c1的概率又是多少?使用这些定义,可以定义贝叶斯准则为:
-
如果P(c0 | x,y) > P(c1 | x,y),那么属于类别0
-
如果P(c1 | x,y) > P(c0 | x,y),那么属于类别1
-
-
基于贝叶斯准则,可以训练出一个功能强大的贝叶斯分类器,接下来对我构造的分类器进行详细说明
二、进行实验
1、算法思路
- 其实在实验原理部分,已经是详细的算法思路了,因为朴素贝叶斯的核心思想,就是实验原理中说明的三个部分。简单来说,整体的思路过程就是:对于给定的训练集,首先基于特征条件独立假设学习输入/输出的联合概率分布。然后基于此模型,对于给定的输入x,利用贝叶斯定理求出后验概率最大的输出y
2、算法步骤
-
(1) 对训练数据进行处理,提出每一个训练数据和其对应的标签
-
(2) 根据训练数据,生成一个词汇表
-
(3) 向量化每个训练样本,然后结合生成的词汇表,训练朴素贝叶斯分类器
-
(4) 对测试数据进行处理,提出每一个测试数据和其对应的标签
-
(5) 向量化每个测试数据,使用朴素贝叶斯分类器对其进行分类
-
(6) 即通过比较概率p0和p1的大小,判断其属于哪个类别
3、代码实现
-
注:代码中的所有函数功能已注释在函数头部,具体的代码作用也在代码后面进行了注释说明
-
(1) 处理数据文件。这里因为训练数据和测试数据的存放格式一样的,因此能够使用同一个功能函数loadFile进行处理。对邮件内容进行切分,取出每封邮件对应的类别标签,生成每封邮件对应的词条向量,返回这两部分
def loadFile(filename):
"""
函数说明:
加载数据文件
:param filename:
文件名
:return:
contentList - 切分邮件内容得到的词条
classVec - 类别标签向量
"""
file = open(filename)
contentList = []
classVec = []
contents = file.readlines()
for line in contents:
content = line.strip('\n').split(' ') #以空格为分割符,切分邮件的内容,得到该邮件对应的词条
classVec.append(int(content[0])) #取出邮件的类别标签
del(content[0]) #删掉词条中的类别标签
contentList.append(content)
return contentList, classVec
- (2) 根据处理训练数据得到的词条,汇总生成一个词汇表。其中使用set取并集的特性,去除重复的词汇
def createVocabList(dataSet):
"""
函数说明:
根据训练数据,生成一个词汇表
:param dataSet:
切分所有邮件得到的词条
:return:
list(vocabSet) - 使用训练数据生成的不重复的词汇表
"""
vocabList = set([]) #创建一个空集合
for content in dataSet:
vocabList = vocabList | set(content) #通过取并集的方式去重,扩充词汇表
return list(vocabList) #以list的形式返回词汇表
- (3) 根据vocabList词汇表,将每个wordsSet词条向量化,向量的每个值为1或0,分别表示该词有或者没有在词汇表中出现
def Words_to_Vec(vocabList, wordsSet):
"""
函数说明:
根据vocabList词汇表,将每个wordsSet词条向量化,向量的每个值为1或0,分别表示该词有或者没有在词汇表中出现
:param vocabList:
词汇表
:param inputSet:
切分每封邮件得到的词条
:return:
词条向量
"""
returnVec = [0] * len(vocabList)
for word in wordsSet: #判断每个词是否在词汇表中出现
if word in vocabList:
returnVec[vocabList.index(word)] = 1 #在词汇表中出现的话则该词对应的位置标记为1
else:
print("The word %s is not in the VocabList!" % word)
return returnVec
- (4) 向量化每个训练样本,然后结合生成的词汇表,训练朴素贝叶斯分类器。其中使用了拉普拉斯平滑的方法
def trainNB(trainMat, trainLabel):
"""
函数说明:
朴素贝叶斯分类训练函数
:param trainMat:
训练文档,即Words_to_Vec函数返回的词向量构成的矩阵
:param trainLabel:
训练数据的类别标签,即loadFile函数返回的classVec
:return:
p0Vec - 侮辱类的条件概率数组
p1Vec - 非侮辱类的条件概率数组
pNotAbusive - 文档属于侮辱类的概率
"""
numTraindocs = len(trainMat) #训练集的数量
numWords = len(trainMat[0]) #每个词条向量的长度
pNotAbusive = sum(trainLabel) / float(numTraindocs) #文档属于非侮辱类的概率
p0Num = np.ones(numWords) #创建numpy.ones数组,词条出现数初始化为1,拉普拉斯平滑方法
p1Num = np.ones(numWords)
p0Denom = 2.0 ##分母初始化为2,拉普拉斯平滑方法
p1Denom = 2.0
for i in range(numTraindocs):
if trainLabel[i] == 1:
p1Num += trainMat[i] #统计属于非侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Denom += sum(trainMat[i])
else:
p0Num += trainMat[i] #统计属于侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Denom += sum(trainMat[i])
p1Vec = np.log(p1Num / p1Denom) #取对数
p0Vec = np.log(p0Num / p0Denom)
return p0Vec, p1Vec, pNotAbusive
- (5) 向量化每个测试数据,使用朴素贝叶斯分类器对其进行分类,通过比较概率p0和p1的大小,判断其属于哪个类别
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass0):
"""
函数说明:
朴素贝叶斯分类函数
:param vec2Classify:
待分类的词条向量
:param p0Vec:
侮辱类的条件概率数组
:param p1Vec:
非侮辱类的条件概率数组
:param pClass0:
文档属于侮辱类的概率
:return:
0 - 文档属于侮辱类
1 - 文档属于分侮辱类
"""
p1 = sum(vec2Classify * p1Vec) + np.log(pClass0)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass0)
if p1 > p0:
return 1
else:
return 0
- (6) 主函数,综合调用上述的功能函数,加载处理数据,训练分类器,然后对测试集邮件进行分类,最后计算分类的准确率
def main():
trainList, trainLabel = loadFile('spam_train.txt') #处理训练数据
VocabList = createVocabList(trainList) #生成词汇表
'''
#由于数据集较大,生成的词汇表内容较多,因此我把生成的词汇表写出到txt文件,后续可以直接调用
file = open('VocabList.txt', 'w')
file.write(str(VocabList))
file.close()
'''
trainMat = []
cnt = 0 #用来标记处理到第几组数据,在处理完每组数据后就会加1输出
for train in trainList: #生成训练集矩阵
trainMat.append(Words_to_Vec(VocabList, train))
cnt += 1
print("当前处理到第%s组训练数据." % cnt)
'''
#由于数据集较大,生成训练集矩阵较慢,内容也较多,因此我把生成的矩阵写出到txt文件,后续可以直接调用
file = open('trainMat.txt', 'w')
file.write(str(trainMat))
file.close()
'''
p0V, p1V, pAb = trainNB(np.array(trainMat), np.array(trainLabel)) #使用训练集矩阵训练分类器
testList, testLabel = loadFile('spam_test.txt') #处理测试数据
resultLabel = []
cnt = 0
for test in testList: #分类测试数据,将分类的标签放入resultLabel
doc = np.array(Words_to_Vec(VocabList, test))
if classifyNB(doc, p0V, p1V, pAb):
resultLabel.append(1)
else:
resultLabel.append(0)
cnt += 1
print("当前处理到第%s组测试数据." % cnt)
cc = 0 #分类正确的个数
for i in range(len(testLabel)): #对比分类标签和真实标签
if testLabel[i] == resultLabel[i]:
cc += 1
print('预测准确率:' + str(100 * cc / float(len(testLabel))) + '%') #计算准确率
4、实验总结
-
测试最后得到的准确率为98.3%,自认为效果不错。有一处不足就是生成训练集矩阵trainMat要花较多时间,有考虑结合多线程或者多进程来加速这一过程
-
完成这次作业,个人总结了一点朴素贝叶斯算法的优缺点:
-
优点:
-
有稳定的分类效率
-
对小规模的数据表现很好,能个处理多分类任务,适合增量式训练
-
对缺失数据不太敏感,算法也比较简单,常用于文本分类
-
-
缺点:
-
理论上,朴素贝叶斯模型与其他分类方法相比具有最小的误差率。但是实际上并非总是如此,这是因为朴素贝叶斯模型给定输出类别的情况下,假设属性之间相互独立,这个假设在实际应用中往往是不成立的,在属性个数比较多或者属性之间相关性较大时,分类效果不好。而在属性相关性较小时,朴素贝叶斯性能最为良好
-
需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候会由于假设的先验模型的原因导致预测效果不佳
-
由于我们是通过先验和数据来决定后验的概率从而决定分类,所以分类决策存在一定的错误率
-
-
三、完整代码
#!/usr/bin/python
# -*- coding utf-8 -*-
# Project: NB
# Author: jiangnan
# Mail: [email protected]
# Date: 2018/9/26
import numpy as np
def loadFile(filename):
"""
函数说明:
加载数据文件
:param filename:
文件名
:return:
contentList - 切分邮件内容得到的词条
classVec - 类别标签向量
"""
file = open(filename)
contentList = []
classVec = []
contents = file.readlines()
for line in contents:
content = line.strip('\n').split(' ') #以空格为分割符,切分邮件的内容,得到该邮件对应的词条
classVec.append(int(content[0])) #取出邮件的类别标签
del(content[0]) #删掉词条中的类别标签
contentList.append(content)
return contentList, classVecx
def createVocabList(dataSet):
"""
函数说明:
根据训练数据,生成一个词汇表
:param dataSet:
切分所有邮件得到的词条
:return:
list(vocabSet) - 使用训练数据生成的不重复的词汇表
"""
vocabList = set([]) #创建一个空集合
for content in dataSet:
vocabList = vocabList | set(content) #通过取并集的方式去重,扩充词汇表
return list(vocabList) #以list的形式返回词汇表
def Words_to_Vec(vocabList, wordsSet):
"""
函数说明:
根据vocabList词汇表,将每个wordsSet词条向量化,向量的每个值为1或0,分别表示该词有或者没有在词汇表中出现
:param vocabList:
词汇表
:param inputSet:
切分每封邮件得到的词条
:return:
词条向量
"""
returnVec = [0] * len(vocabList)
for word in wordsSet: #判断每个词是否在词汇表中出现
if word in vocabList:
returnVec[vocabList.index(word)] = 1 #在词汇表中出现的话则该词对应的位置标记为1
else:
print("The word %s is not in the VocabList!" % word)
return returnVec
def trainNB(trainMat, trainLabel):
"""
函数说明:
朴素贝叶斯分类训练函数
:param trainMat:
训练文档,即Words_to_Vec函数返回的词向量构成的矩阵
:param trainLabel:
训练数据的类别标签,即loadFile函数返回的classVec
:return:
p0Vec - 侮辱类的条件概率数组
p1Vec - 非侮辱类的条件概率数组
pNotAbusive - 文档属于侮辱类的概率
"""
numTraindocs = len(trainMat) #训练集的数量
numWords = len(trainMat[0]) #每个词条向量的长度
pNotAbusive = sum(trainLabel) / float(numTraindocs) #文档属于非侮辱类的概率
p0Num = np.ones(numWords) #创建numpy.ones数组,词条出现数初始化为1,拉普拉斯平滑方法
p1Num = np.ones(numWords)
p0Denom = 2.0 ##分母初始化为2,拉普拉斯平滑方法
p1Denom = 2.0
for i in range(numTraindocs):
if trainLabel[i] == 1:
p1Num += trainMat[i] #统计属于非侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Denom += sum(trainMat[i])
else:
p0Num += trainMat[i] #统计属于侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Denom += sum(trainMat[i])
p1Vec = np.log(p1Num / p1Denom) #取对数
p0Vec = np.log(p0Num / p0Denom)
return p0Vec, p1Vec, pNotAbusive
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass0):
"""
函数说明:
朴素贝叶斯分类函数
:param vec2Classify:
待分类的词条向量
:param p0Vec:
侮辱类的条件概率数组
:param p1Vec:
非侮辱类的条件概率数组
:param pClass0:
文档属于侮辱类的概率
:return:
0 - 文档属于侮辱类
1 - 文档属于分侮辱类
"""
p1 = sum(vec2Classify * p1Vec) + np.log(pClass0) # 对应元素相乘。logA * B = logA + logB,所以这里加上log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass0)
if p1 > p0:
return 1
else:
return 0
def main():
trainList, trainLabel = loadFile('spam_train.txt') #处理训练数据
VocabList = createVocabList(trainList) #生成词汇表
'''
#由于数据集较大,生成的词汇表内容较多,因此我把生成的词汇表写出到txt文件,后续可以直接调用
file = open('VocabList.txt', 'w')
file.write(str(VocabList))
file.close()
'''
trainMat = []
cnt = 0 #用来标记处理到第几组数据,在处理完每组数据后就会加1输出
for train in trainList: #生成训练集矩阵
trainMat.append(Words_to_Vec(VocabList, train))
cnt += 1
print("当前处理到第%s组训练数据." % cnt)
'''
#由于数据集较大,生成训练集矩阵较慢,内容也较多,因此我把生成的矩阵写出到txt文件,后续可以直接调用
file = open('trainMat.txt', 'w')
file.write(str(trainMat))
file.close()
'''
p0V, p1V, pAb = trainNB(np.array(trainMat), np.array(trainLabel)) #使用训练集矩阵训练分类器
testList, testLabel = loadFile('spam_test.txt') #处理测试数据
resultLabel = []
cnt = 0
for test in testList: #分类测试数据,将分类的标签放入resultLabel
doc = np.array(Words_to_Vec(VocabList, test))
if classifyNB(doc, p0V, p1V, pAb):
resultLabel.append(1)
else:
resultLabel.append(0)
cnt += 1
print("当前处理到第%s组测试数据." % cnt)
cc = 0 #分类正确的个数
for i in range(len(testLabel)): #对比分类标签和真实标签
if testLabel[i] == resultLabel[i]:
cc += 1
print('预测准确率:' + str(100 * cc / float(len(testLabel))) + '%') #计算准确率
if __name__ == '__main__':
main()