目录
一、基于贝叶斯决策理论的分类方法
朴素贝叶斯是经典的机器学习算法之一,也是为数不多的基于概率论的分类算法。对于大多数的分类算法,在所有的机器学习分类算法中,朴素贝叶斯和其他绝大多数的分类算法都不同。比如决策树,KNN,逻辑回归,支持向量机等,他们都是判别方法,也就是直接学习出特征输出Y和特征X之间的关系,要么是决策函数,要么是条件分布。但是朴素贝叶斯却是生成方法,该算法原理简单,也易于实现。
朴素贝叶斯 |
---|
优点:在数据较少的情况下仍然有效,可以处理多类别问题。 缺点:对于输入数据的准备方式较为敏感。 适用数据类型:标称型数据 |
朴素贝叶斯是贝叶斯决策理论的一部分,所以讲述朴素贝叶斯之前有必要快速了解一下贝叶斯决策理论。
假设现在我们有一个数据集,它由两类数据组成,数据分布如下图所示:
我们现在用p1(x,y)表示数据点(x,y)属于类别1(图中用圆点表示的类别)的概率,用p2(x,y)表示数据点(x,y)属于类别2(图中用三角形表示的类别)的概率,那么对于一个新数据点(x,y),可以用下面的规则来判断它的类别:
- 如果 p1(x,y) > p2(x,y),那么类别为1。
- 如果 p2(x,y) > p1(x,y),那么类别为2。
也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有 最高概率的决策。看图1,如果该图中的整个数据使用6个浮点数来表示,并且计算类别概率的Python代码只有两行,那么我们应该使用下面哪种方法来对该数据点进行分类?
- 使用kNN算法,则需要进行1000次距离计算,和简单的概率计算相比,kNN的计算量太大;
- 使用决策树,则需要分别沿x轴、y轴划分数据,似乎效果并不是很好;
- 还有就是计算数据点属于每个类别的概率,并进行比较,这是最佳选择。
接下来,我们要学会p1及p1概率计算方法。为了能够计算p1与p2,则需要明白什么是条件概率。
二、条件概率
已知两个独立事件A和B,事件B发生的前提下,事件A发生的概率可以表示为P(A|B)。条件概率表示为P(A|B)。
即上图中橙色部分占红色部分的比例,可以表示为:
则我们可以得到:
同样地,在事件A发生的条件下事件B发生的概率为:
我们也可以得到:
于是我们就可以,结合上面的式子得到:
通过变换上面的等式,所以有条件概率的公式:
三、朴素贝叶斯分类器
先验概率P(X):先验概率是指根据以往经验和分析得到的概率。
后验概率P(Y|X):事情已发生,要求这件事情发生的原因是由某个因素引起的可能性的大小,后验分布P(Y|X)表示事件X已经发生的前提下,事件Y发生的概率,称事件X发生下事件Y的条件概率。
后验概率P(X|Y):在已知Y发生后X的条件概率,也由于知道Y的取值而被称为X的后验概率。
朴素:朴素贝叶斯算法是假设各个特征之间相互独立,也是朴素这词的意思,那么贝叶斯公式中的P(X|Y)可写成:
朴素贝叶斯公式:
朴素贝叶斯分类器:朴素贝叶斯分类器(Naïve Bayes Classifier)采用了“属性条件独立性假设” ,即每个属性独立地对分类结果发生影响。为方便公式标记,不妨记P(C=c|X=x)为P(c|x),基于属性条件独立性假设,贝叶斯公式可重写为:
其中d为属性数目, 为
在第
个属性上的取值。
由于对所有类别来说 P(x)相同,因此MAP判定准则可改为:
其中 和
为目标参数。
朴素贝叶斯分类器的训练器的训练过程就是基于训练集D估计类先验概率 ,并为每个属性估计条件概率
。
令 表示训练集D中第c类样本组合的集合,则类先验概率:
拉普拉斯修正:若某个属性值在训练集中没有与某个类同时出现过,则训练后的模型会出现 over-fitting 现象。比如训练集中没有该样例,因此连乘式计算的概率值为0,这显然不合理。因为样本中不存在(概率为0),不代该事件一定不可能发生。所以为了避免其他属性携带的信息,被训练集中未出现的属性值“ 抹去” ,在估计概率值时通常要进行“拉普拉斯修正”。
,我们要修正
的值。
令 N 表示训练集 D 中可能的类别数, 表示第i个属性可能的取值数,则贝叶斯公式可修正为:
防溢出策略:条件概率乘法计算过程中,因子一般较小(均是小于1的实数)。当属性数量增多时候,会导致累乘结果下溢出的现象。
在代数中有 ,因此可以把条件概率累乘转化成对数累加。分类结果仅需对比概率的对数累加法运算后的数值,以确定划分的类别。
四、使用朴素贝叶斯进行文档分类
机器学习的一个重要应用就是文档的自动分类。在文档分类中,整个文档(如一封电子邮件)是实例,而电子邮件中的某些元素则构成特征。虽然电子邮件是一种会不断增加的文本,但我们同样也可以对新闻报道、用户留言、政府公文等其他任意类型的文本进行分类。我们可以观察文档中出现的词,并把每个词的出现或者不出现作为一个特征,这样得到的特征数目就会跟词汇表中的词目一样多。朴素贝叶斯是上节介绍的贝叶斯分类器的一个扩展,是用于文档分类的常用算法。
4.1 准备数据:从文本中构建词向量
我们将把文本看成单词向量或者词条向量,也就是说将句子转换为向量。考虑出现在所有文档中的所有单词,再决定将哪些词纳入词汇表或者说所要的词汇集合,然后必须要将每一篇文档转换为词汇表上的向量。
# 创建实验样本
def loadDataSet():
postingList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
['stop', 'posting', 'stupid', 'worthless', 'garbage'],
['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
classVec = [0, 1, 0, 1, 0, 1] # 标签向量,1表示侮辱性文字,0表示正常言论
return postingList, classVec
函数loadDataSet()创建了一些实验样本。该函数返回的第一个变量是进行词条切分后的文档集合。这些留言文本被切分成一系列的词条集合,标点符号从文本中去掉loadDataSet( )函数返回的第二个变量是一个类别标签的集合。这里有两类,侮辱性和非侮辱性。这些文本的类别由我们人工标注,这些标注信息用于训练程序以便自动检测侮辱性留言。
# 创建不重复词的列表 ———— 词汇表
def createVocabList(dataSet):
vocabSet = set([]) # 创建一个空集
for document in dataSet:
vocabSet = vocabSet | set(document) # 创建两个集合的并集
return list(vocabSet) # 返回不重复的词条列表
函数createVocabList()会创建一个包含在所有文档中出现的不重复词的列表,为此使用了Python的set数据类型。将词条列表输给set构造函数,set就会返回一个不重复词表。 首先,创建一个空集合 ,然后将每篇文档返回的新词集合添加到该集合中 。操作符|用于求两个集合的并集,这也是一个按位或操作符。在数学符号表示上,按位或操作与集合求并操作使用相同记号。
# 输出文档向量
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0] * len(vocabList) # 创建一个其中所含元素都为0的向量
for word in inputSet: # 遍历文档中的所有单词
if word in vocabList: # 如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1
returnVec[vocabList.index(word)] = 1
else:
print("单词 %s 不在词汇表中!" % word)
return returnVec
获得词汇表后,使用函数setOfWords2Vec(),该函数的输入参数为词汇表及某个文档,输出的是文档向量,向量的每一元素为1或0,分别表示词汇表中的单词在输入文档中是否出现。函数首先创建一个和词汇表等长的向量,并将其元素都设置为0 。接着,遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1。
# 测试函数效果
# 创建实验样本
listPosts, listClasses = loadDataSet()
print('数据集\n', listPosts)
# 创建词汇表
myVocabList = createVocabList(listPosts)
print('词汇表:\n', myVocabList)
# 输出文档向量
print(setOfWords2Vec(myVocabList, listPosts[5]))
4.2 训练算法:从词向量计算概率
上面我们已经实现将一组单词转换为一组数字,接下来我们要做的是如何使用这些数字计算概率。
现在我们会使用到上面所讲得贝叶斯格式。将之前的x、y 替换为 。粗体
表示这是一个向量,即它由多个数值组成。在这个例子中,数值个数与词汇表中的词个数相同。
上述公式,对每个类计算该值,然后比较这两个概率值的大小。首先可以通过类别 (侮辱性留言或非侮辱性留言)里文档数除以总的文档数来计算概率
。接下来计算
,这里就要用到朴素贝叶斯假设。如果将
展开为一个个独立特征,那么就可以将上述概率写作
。这里假设所有词都互相独立,该假设也称作条件独立性假设,它意味着可使用
来计算上述概率,这就极大地简化了计算的过程。
该函数的伪代码如下:
计算每个类别中的文档数目
对每篇训练文档:
对每个类别:
如果词条出现在文档中→ 增加该词条的计数值
增加所有词条的计数值
对每个类别:
对每个词条:
将该词条的数目除以总词条数目得到条件概率
返回每个类别的条件概率
# 朴素贝叶斯分类器训练函数
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 获得训练的文档总数
numWords = len(trainMatrix[0]) # 获得每篇文档的词总数
pAbusive = sum(trainCategory) / float(numTrainDocs) # 计算文档是侮辱类的概率
p0Num = zeros(numWords) # 创建numpy.zeros数组,初始化概率
p1Num = zeros(numWords) # 创建numpy.zeros数组,初始化概率
p0Denom = 0.0 # 初始化为0
p1Denom = 0.0 # 初始化为0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i] # 向量相加,统计侮辱类的条件概率的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i] # 向量相加,统计非侮辱类的条件概率的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Denom += sum(trainMatrix[i])
p1Vect = p1Num / p1Denom # 侮辱类,每个元素除以该类别中的总词数
p0Vect = p0Num / p0Denom # 非侮辱类,每个元素除以该类别中的总词数
return p0Vect, p1Vect, pAbusive # p0Vect非侮辱类的条件概率数组、p1Vect侮辱类的条件概率数组、pAbusive文档属于侮辱类的概率
在trainNB0函数中的输入参数为文档矩阵trainMatrix,以及由每篇文档类别标签所构成的向量 trainCategory。首先,计算文档属于侮辱性文档(类别为1)的概率,即P(1)。因为这是一个二类分类问题,所以可以通过1- P(1)得到P(0),即为非侮辱性文档的概率。计算 和
,需要初始化程序中的分子变量(p0Num、p1Num)和分母变量(p0Denom、p1Denom)。由于w中元素如此众多,因此可以使用NumPy数组快速计算这些值。上述程序中的分母变量是一个元素个数等于词汇表大小的NumPy数组。在for循环中,要遍历训练集trainMatrix中的所有文档。一旦某个词语(侮辱性或正常词语)在某一文档中出现,则该词对应的个数(p1Num或者p0Num)就加1, 而且在所有的文档中,该文档的总词数也相应加1 。对于两个类别都要进行同样的计算处理。 最后,对每个元素除以该类别中的总词数 。利用NumPy可以很好实现,用一个数组除以浮点数即可。最后,函数会返回两个向量和一个概率。
# 测试代码
listPosts, listClasses = loadDataSet() # 创建实验样本
myVocabList = createVocabList(listPosts) # 创建词汇表
trainMat = []
for postinDoc in listPosts: # for循环使用词向量来填充trainMat列表
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V, p1V, pAb = trainNB0(trainMat, listClasses)
print('p0V:\n', p0V)
print('p1V:\n', p1V)
print('pAb:\n', pAb)
对运行结果分析,p0V存放的是属于类别0的各单词的条件概率,即各个单词属于非侮辱类的条件概率;p1V存放的是属于类别1的各单词的条件概率,即各个单词属于侮辱类的条件概率。pAb是所有侮辱类的样本占所有样本的概率,在listClasses列表[0, 1, 0, 1, 0, 1]中,1表示侮辱类文字,0表示非侮辱类文字,既有3个侮辱类,3个非侮辱类,所以侮辱类的概率是0.5,非侮辱类的概率是也0.5,pAb就是先验概率。
4.3 测试算法:根据现实情况修改分类器
利用朴素贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概 率,即计算 。如果其中一个概率值为0,那么最后的乘积也为0。为降低 这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。
# 解决办法:
p0Num = ones(numWords) # 创建numpy.ones数组,初始化概率
p1Num = ones(numWords) # 创建numpy.ones数组,初始化概率
p0Denom = 2.0 # 初始化为2
p1Denom = 2.0 # 初始化为2
另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积 时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。一种解决办法是对乘积取自然对数。在代数中有
,于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。下图给出函数f(x)与ln(f(x))的曲线。检查这两条曲线,就会发现它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。通过修改代码,将上述做法用到分类器中:
# 解决办法:
p1Vect = log(p1Num/p1Denom) # 使用log函数
p0Vect = log(p0Num/p0Denom)
# 改进后的朴素贝叶斯分类器训练函数
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 获得训练的文档总数
numWords = len(trainMatrix[0]) # 获得每篇文档的词总数
pAbusive = sum(trainCategory) / float(numTrainDocs) # 计算文档是侮辱类的概率
p0Num = ones(numWords) # 创建numpy.ones数组,初始化概率
p1Num = ones(numWords) # 创建numpy.ones数组,初始化概率
p0Denom = 2.0 # 初始化为2.0
p1Denom = 2.0 # 初始化为2.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i] # 向量相加,统计侮辱类的条件概率的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i] # 向量相加,统计非侮辱类的条件概率的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Denom += sum(trainMatrix[i])
p1Vect = log(p1Num / p1Denom) # 侮辱类,每个元素除以该类别中的总词数
p0Vect = log(p0Num / p0Denom) # 非侮辱类,每个元素除以该类别中的总词数
return p0Vect, p1Vect, pAbusive # p0Vect非侮辱类的条件概率数组、p1Vect侮辱类的条件概率数组、pAbusive文档属于侮辱类的概率
从运行结果可以看出,没有出现概率为0的情况了,问题得到了比较好的解决。
下面我们要来实现分类器:
# 朴素贝叶斯分类器分类函数
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + log(pClass1) # 元素相乘,侮辱类概率
p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1) # 非侮辱类概率
if p1 > p0:
return 1
else:
return 0
要分类的向量vec2Classify以及使用函数trainNB0()计算得到的三个概率。使用NumPy的数组来计算两个向量相乘的结果 。这里的相乘是指对应元素相乘,即先将两个向量中的第1个元素相乘,然后将第2个元素相乘,以此类推。接下来将词汇表 中所有词的对应值相加,然后将该值加到类别的对数概率上。最后,比较类别的概率返回大概率对应的类别标签。
# 测试朴素贝叶斯分类器
def testingNB():
listOPosts, listClasses = loadDataSet() # 创建实验样本
myVocabList = createVocabList(listOPosts) # 创建词汇表
trainMat = [] # 文档矩阵
for postinDoc in listOPosts: # for循环使用词向量来填充trainMat列表
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V, p1V, pAb = trainNB0(array(trainMat), array(listClasses)) # 获得概率数组及先验概率
testEntry = ['love', 'my', 'dalmation'] # 输入测试1
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
testEntry = ['stupid', 'garbage'] # 输入测试2
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
4.4 准备数据:文档词袋模型
将每个词的出现与否作为一个特征,这可以被描述为词集模型(set-of-words model)。如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)。在词袋中,每个单词可以出现多次,而在词集中,每个词只能出现一次。为适应词袋模型,需要对函数setOfWords2Vec() 稍加修改,修改后的函数称为bagOfWords2Vec()。
# 朴素贝叶斯词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1 # 加1
return returnVec
函数setOfWords2Vec()几乎 完全相同,唯一不同的是每当遇到一个单词时,它会增加词向量中的对应值,而不只是将对应的数值设为1。
五、示例:使用朴素贝叶斯过滤垃圾邮件
5.1 准备数据:切分文本
Python split()方法 |
---|
描述 Python split() 通过指定分隔符对字符串进行切片,如果参数 num 有指定值,则分隔 num+1 个子字符串 语法 split() 方法语法: str.split(str="", num=string.count(str)). 参数
返回值 返回分割后的字符串列表。 |
# 例子
mySent = 'This book is the best book on Python or M.L. I have ever laid eyes upon.'
print(mySent.split())
为了避免标点符号也被当成了词的一部分,我们可以使用正则表示式来切分句子,其中分隔符是除单词、数字外的任意字符串,并且字符串全部转换成小写,统一格式。
5.2 测试算法:使用朴素贝叶斯进行交叉验证
# 文件解析
def textParse(bigString): # 输入字符串, 输出单词列表
import re
listOfTokens = re.split(r'[\W*]', bigString) # 字符串切分,去掉除单词、数字外的任意字符串
return [tok.lower() for tok in listOfTokens if len(tok) > 2] # 除了单个字母外,其他字符串全部转换成小写
函数textParse()接受一个大字符串并将其解析为字符串列表。该函数去掉少于两个字符的字符串,并将所有字符串转换为小写。
# 完整的垃圾邮件测试函数
def spamTest():
docList = [] # 文档列表
classList = [] # 文档标签
fullText = [] # 全部文档内容集合
for i in range(1, 26): # 遍历垃圾邮件和非垃圾邮件各25个
wordList = textParse(open('email/spam/%d.txt' % i).read()) # 读取垃圾邮件,将大字符串并将其解析为字符串列表
docList.append(wordList) # 垃圾邮件加入文档列表
fullText.extend(wordList) # 把当前垃圾邮件加入文档内容集合
classList.append(1) # 1表示垃圾邮件,标记垃圾邮件
wordList = textParse(open('email/ham/%d.txt' % i).read()) # 读非垃圾邮件,将大字符串并将其解析为字符串列表
docList.append(wordList) # 非垃圾邮件加入文档列表
fullText.extend(wordList) # 把当前非垃圾邮件加入文档内容集合
classList.append(0) # 0表示垃圾邮件,标记非垃圾邮件,
vocabList = createVocabList(docList) # 创建不重复的词汇表
trainingSet = list(range(50)) # 为训练集添加索引
testSet = [] # 创建测试集
for i in range(10): # 目的为了从50个邮件中,随机挑选出40个作为训练集,10个做测试集
randIndex = int(random.uniform(0, len(trainingSet))) # 随机产生索引
testSet.append(trainingSet[randIndex]) # 添加测试集的索引值
del (trainingSet[randIndex]) # 在训练集中,把加入测试集的索引删除
trainMat = [] # 创建训练集矩阵训练集类别标签系向量
trainClasses = [] # 训练集类别标签
for docIndex in trainingSet: # for循环使用词向量来填充trainMat列表t
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex])) # 把词集模型添加到训练矩阵中
trainClasses.append(classList[docIndex]) # 把类别添加到训练集类别标签中
p0V, p1V, pSpam = trainNB0(array(trainMat), array(trainClasses)) # 朴素贝叶斯分类器训练函数
print('词表:\n', vocabList)
print('p0V:\n', p0V)
print('p1V:\n', p1V)
print('pSpam:\n', pSpam)
errorCount = 0 # 用于计数错误分类
for docIndex in testSet: # 循环遍历训练集
wordVector = bagOfWords2VecMN(vocabList, docList[docIndex]) # 获得测试集的词集模型
if classifyNB(array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
errorCount += 1 # 预测值和真值不一致,则错误分类计数加1
print("分类错误集", docList[docIndex])
print('错误率: ', float(errorCount) / len(testSet))
函数spamTest()对贝叶斯垃圾邮件分类器进行自动化处理。导入文件夹spam与ham下的文本文件,并将它们解析为词列表 。
接下来构建一个测试集与一个训练集,两个集合中的 邮件都是随机选出的。有50封电子邮件,并不是很多,其中的10封电子邮件被随机选择为测试集。分类器所需要的概率计算只利用训练集中的文档来完成。Python变量trainingSet是一个整数列表,其中的值从0到49。接下来,随机选择其中10个文件 。选择出的数字所对应的文档被添加到测试集,同时也将其从训练集中剔除。
运行结果1:
运行结果2:
第一次的运行结果,错误率为0.1,第二次的运行结果,错误率为0.0,为更好的估计错误率,可以进行多次结果测试,去平均值。多次重复后,可以看出,错误率的稳定在0.1左右。
5.3 垃圾邮件分类完整代码
from numpy import *
# 创建不重复词的列表 ———— 词汇表
def createVocabList(dataSet):
vocabSet = set([]) # 创建一个空集
for document in dataSet:
vocabSet = vocabSet | set(document) # 创建两个集合的并集
return list(vocabSet) # 返回不重复的词条列表
# 输出文档向量
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0] * len(vocabList) # 创建一个其中所含元素都为0的向量
for word in inputSet: # 遍历文档中的所有单词
if word in vocabList: # 如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1
returnVec[vocabList.index(word)] = 1
else:
print("单词 %s 不在词汇表中!" % word)
return returnVec
# 朴素贝叶斯分类器训练函数
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 获得训练的文档总数
numWords = len(trainMatrix[0]) # 获得每篇文档的词总数
pAbusive = sum(trainCategory) / float(numTrainDocs) # 计算文档是侮辱类的概率
p0Num = ones(numWords) # 创建numpy.ones数组,初始化概率
p1Num = ones(numWords) # 创建numpy.ones数组,初始化概率
p0Denom = 2.0 # 初始化为2.0
p1Denom = 2.0 # 初始化为2.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i] # 向量相加,统计侮辱类的条件概率的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i] # 向量相加,统计非侮辱类的条件概率的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Denom += sum(trainMatrix[i])
p1Vect = log(p1Num / p1Denom) # 侮辱类,每个元素除以该类别中的总词数
p0Vect = log(p0Num / p0Denom) # 非侮辱类,每个元素除以该类别中的总词数
return p0Vect, p1Vect, pAbusive # p0Vect非侮辱类的条件概率数组、p1Vect侮辱类的条件概率数组、pAbusive文档属于侮辱类的概率
# 朴素贝叶斯分类器分类函数
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + log(pClass1) # 元素相乘
p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
# 朴素贝叶斯词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
# 文件解析
def textParse(bigString): # 输入字符串, 输出单词列表
import re
listOfTokens = re.split(r'[\W*]', bigString) # 字符串切分,去掉除单词、数字外的任意字符串
return [tok.lower() for tok in listOfTokens if len(tok) > 2] # 除了单个字母外,其他字符串全部转换成小写
# 完整的垃圾邮件测试函数
def spamTest():
docList = [] # 文档列表
classList = [] # 文档标签
fullText = [] # 全部文档内容集合
for i in range(1, 26): # 遍历垃圾邮件和非垃圾邮件各25个
wordList = textParse(open('email/spam/%d.txt' % i).read()) # 读取垃圾邮件,将大字符串并将其解析为字符串列表
docList.append(wordList) # 垃圾邮件加入文档列表
fullText.extend(wordList) # 把当前垃圾邮件加入文档内容集合
classList.append(1) # 1表示垃圾邮件,标记垃圾邮件
wordList = textParse(open('email/ham/%d.txt' % i).read()) # 读非垃圾邮件,将大字符串并将其解析为字符串列表
docList.append(wordList) # 非垃圾邮件加入文档列表
fullText.extend(wordList) # 把当前非垃圾邮件加入文档内容集合
classList.append(0) # 0表示垃圾邮件,标记非垃圾邮件,
vocabList = createVocabList(docList) # 创建不重复的词汇表
trainingSet = list(range(50)) # 为训练集添加索引
testSet = [] # 创建测试集
for i in range(10): # 目的为了从50个邮件中,随机挑选出40个作为训练集,10个做测试集
randIndex = int(random.uniform(0, len(trainingSet))) # 随机产生索引
testSet.append(trainingSet[randIndex]) # 添加测试集的索引值
del (trainingSet[randIndex]) # 在训练集中,把加入测试集的索引删除
trainMat = [] # 创建训练集矩阵训练集类别标签系向量
trainClasses = [] # 训练集类别标签
for docIndex in trainingSet: # for循环使用词向量来填充trainMat列表t
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex])) # 把词集模型添加到训练矩阵中
trainClasses.append(classList[docIndex]) # 把类别添加到训练集类别标签中
p0V, p1V, pSpam = trainNB0(array(trainMat), array(trainClasses)) # 朴素贝叶斯分类器训练函数
print('词表:\n', vocabList)
print('p0V:\n', p0V)
print('p1V:\n', p1V)
print('pSpam:\n', pSpam)
errorCount = 0 # 用于计数错误分类
for docIndex in testSet: # 循环遍历训练集
wordVector = bagOfWords2VecMN(vocabList, docList[docIndex]) # 获得测试集的词集模型
if classifyNB(array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
errorCount += 1 # 预测值和真值不一致,则错误分类计数加1
print("分类错误集", docList[docIndex])
print('错误率: ', float(errorCount) / len(testSet))
# return vocabList,fullText
实现邮件分类:
# 邮件分类器
def classifyEmail():
docList = [] # 文档列表
classList = [] # 文档标签
fullText = [] # 全部文档内容集合
for i in range(1, 26): # 遍历垃圾邮件和非垃圾邮件各25个
wordList = textParse(open('email/spam/%d.txt' % i).read()) # 读取垃圾邮件,将大字符串并将其解析为字符串列表
docList.append(wordList) # 垃圾邮件加入文档列表
fullText.extend(wordList) # 把当前垃圾邮件加入文档内容集合
classList.append(1) # 1表示垃圾邮件,标记垃圾邮件
wordList = textParse(open('email/ham/%d.txt' % i).read()) # 读非垃圾邮件,将大字符串并将其解析为字符串列表
docList.append(wordList) # 非垃圾邮件加入文档列表
fullText.extend(wordList) # 把当前非垃圾邮件加入文档内容集合
classList.append(0) # 0表示垃圾邮件,标记非垃圾邮件,
vocabList = createVocabList(docList) # 创建不重复的词汇表
trainingSet = list(range(50)) # 为训练集添加索引
trainMat = [] # 创建训练集矩阵训练集类别标签系向量
trainClasses = [] # 训练集类别标签
for docIndex in trainingSet: # for循环使用词向量来填充trainMat列表
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex])) # 把词集模型添加到训练矩阵中
trainClasses.append(classList[docIndex]) # 把类别添加到训练集类别标签中
p0V, p1V, pSpam = trainNB0(array(trainMat), array(trainClasses)) # 朴素贝叶斯分类器训练函数
testList = textParse(open('email/test/1.txt').read()) # 读取邮件,将大字符串并将其解析为字符串列表
testVector = bagOfWords2VecMN(vocabList, testList) # 获得测试集的词集模型
if classifyNB(array(testVector), p0V, p1V, pSpam):
result = "垃圾邮件"
else:
result = "正常邮件"
print("输入邮件内容为: ")
print(' '.join(testList))
print('该邮件被分类为: ', result)
if __name__ == '__main__':
classifyEmail()
测试结果:
六、朴素贝叶斯实验总结
本次实验为实现垃圾邮件分类,从条件概率的格式推演到文本分类的基本实现,再到最终的垃圾邮件分类,做了一系列的工作。利用朴素贝叶斯来进行垃圾邮件分类的好处就是,朴素贝叶斯模型发源于古典数学理论,有着坚实的数学基础,以及稳定的分类效率,并且算法也比较简单,容易实现,对于小规模的数据效果很不错。
实验中遇到的两个问题,都是关于读取垃圾邮件txt文件的。第一个问题是运行输出的结果数据集为空,经过查找发现是读取问题,解决办法读取的方式改为r'[\W*]';把另外一个问题是txt文件的编码出错,于是把txt文件的编码改为了UTF-8,于是就可以了。
朴素贝叶斯的缺点:如果我们使用了样本属性真实情况其实并不是相互独立性的,那么其实这样的分类效果可能不会很好。而且需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候,会由于假设的先验模型的原因导致预测效果不佳。