机器学习实战之朴素贝叶斯算法实战详解:言论过滤器、过滤垃圾邮件

这一系列博客文章都是基于前人的经验,加入一些自己的拙见,仅供参考。
参考文章:转载请注明作者和出处: http://blog.csdn.net/c406495762

一、算法介绍

朴素贝叶斯算法是有监督的学习算法,解决的是分类问题,如客户是否流失、是否值得投资、信用等级评定等多分类问题。该算法的优点在于简单易懂、学习效率高、在某些领域的分类问题中能够与决策树、神经网络相媲美。

但由于该算法以自变量之间的独立(条件特征独立)性和连续变量的正态性假设为前提,就会导致算法精度在某种程度上受影响。

朴素贝叶斯理论

我们现在用p1(x,y)表示数据点(x,y)属于类别1(图中红色圆点表示的类别)的概率,用p2(x,y)表示数据点(x,y)属于类别2(图中蓝色三角形表示的类别)的概率,那么对于一个新数据点(x,y),可以用下面的规则来判断它的类别:

  • 如果p1(x,y) > p2(x,y),那么类别为1
  • 如果p1(x,y) < p2(x,y),那么类别为2

也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策。

已经了解了贝叶斯决策理论的核心思想,那么接下来,就是学习如何计算p1和p2概率。

条件概率

指在事件B发生的情况下,事件A发生的概率,用P(A|B)来表示。
在这里插入图片描述
在这里插入图片描述

全概率公式

假定样本空间S,是两个事件A与A’的和。
在这里插入图片描述

上图中,红色部分是事件A,绿色部分是事件A’,它们共同构成了样本空间S。
在这里插入图片描述
事件B的概率可以划分为:
在这里插入图片描述
这就是全概率公式。

含义:如果A和A’构成样本空间的一个划分,那么事件B的概率,就等于A和A’的概率分别乘以B对这两个事件的条件概率之和。

将这个公式代入上一节的条件概率公式,就得到了条件概率的另一种写法:
在这里插入图片描述

贝叶斯推断

条件概率公式变形后:
在这里插入图片描述

  • 我们把P(A)称为"先验概率"(Prior probability),即在B事件发生之前,我们对A事件概率的一个判断。
  • P(A|B)称为"后验概率"(Posterior probability),即在B事件发生之后,我们对A事件概率的重新评估。
  • P(B|A)/P(B)称为"可能性函数"(Likelyhood),这是一个调整因子使得预估概率更接近真实概率。
  • 所以,条件概率可以理解成:后验概率 = 先验概率 x 调整因子

这就是贝叶斯推断的含义。我们先预估一个"先验概率",然后加入实验结果,看这个实验到底是增强还是削弱了"先验概率",由此得到更接近事实的"后验概率"。

在这里,
如果"可能性函数"P(B|A)/P(B)>1,意味着"先验概率"被增强,事件A的发生的可能性变大;
如果"可能性函数"=1,意味着B事件无助于判断事件A的可能性;
如果"可能性函数"<1,意味着"先验概率"被削弱,事件A的可能性变小。

在这里插入图片描述
两个一模一样的碗,一号碗有30颗水果糖和10颗巧克力糖,二号碗有水果糖和巧克力糖各20颗。

现在随机选择一个碗,从中摸出一颗糖,发现是水果糖。请问这颗水果糖来自一号碗的概率有多大?

我们假定,H1表示一号碗,H2表示二号碗。由于这两个碗是一样的,所以P(H1)=P(H2),也就是说,在取出水果糖之前,这两个碗被选中的概率相同。因此,P(H1)=0.5,我们把这个概率就叫做"先验概率",即没有做实验之前,来自一号碗的概率是0.5。

再假定,E表示水果糖,所以问题就变成了在已知E的情况下,来自一号碗的概率有多大,即求P(H1|E)。我们把这个概率叫做"后验概率",即在E事件发生之后,对P(H1)的修正。

根据条件概率公式,得到
在这里插入图片描述
已知,P(H1)等于0.5,P(E|H1)为一号碗中取出水果糖的概率,等于30÷(30+10)=0.75,那么求出P(E)就可以得到答案。根据全概率公式,
在这里插入图片描述
即为:在一号碗中抽中水果糖的概率x选中一号碗的概率+在二号碗中抽中水果糖的概率x选中二号碗的概率

所以,
在这里插入图片描述
将数字代入原方程,得到:

在这里插入图片描述

这表明,来自一号碗的概率是0.6。也就是说,取出水果糖之后,H1事件的可能性得到了增强。

同时再思考一个问题,在使用该算法的时候,如果不需要知道具体的类别概率,即上面P(H1|E)=0.6,只需要知道所属类别,即来自一号碗,我们有必要计算P(E)这个全概率吗?要知道我们只需要比较 P(H1|E)和P(H2|E)的大小,找到那个最大的概率就可以。

既然如此,两者的分母都是相同的,那我们只需要比较分子即可。即比较P(E|H1)P(H1)和P(E|H2)P(H2)的大小,所以为了减少计算量,全概率公式在实际编程中可以不使用。

朴素贝叶斯推断(假设条件都独立)

理解了贝叶斯推断,那么让我们继续看看朴素贝叶斯。贝叶斯和朴素贝叶斯的概念是不同的,区别就在于“朴素”二字,朴素贝叶斯对条件个概率分布做了条件独立性的假设。

比如下面的公式,假设有n个特征:
在这里插入图片描述
由于每个特征都是独立的,我们可以进一步拆分公式:
在这里插入图片描述
这样我们就可以进行计算了。如果有些迷糊,让我们从一个例子开始讲起,你会看到贝叶斯分类器很好懂,一点都不难。

某个医院早上来了六个门诊的病人,他们的情况如下表所示:
在这里插入图片描述
现在又来了第七个病人,是一个打喷嚏的建筑工人。请问他患上感冒的概率有多大?
根据贝叶斯定理:
在这里插入图片描述
可得:
在这里插入图片描述

根据朴素贝叶斯条件独立性的假设可知,"打喷嚏"和"建筑工人"这两个特征是独立的,因此,上面的等式就变成了:
在这里插入图片描述

这里可以计算:
在这里插入图片描述

因此,这个打喷嚏的建筑工人,有66%的概率是得了感冒。同理,可以计算这个病人患上过敏或脑震荡的概率。比较这几个概率,就可以知道他最可能得什么病。

这就是贝叶斯分类器的基本方法:在统计资料的基础上,依据某些特征,计算各个类别的概率,从而实现分类。

同样,在编程的时候,如果不需要求出所属类别的具体概率,P(打喷嚏) = 0.5和P(建筑工人) = 0.33的概率是可以不用求的。(因为它们分母都一样,只需要比较分子即可)

是基于已有的数据,做出的推断

二、实践:言论过滤器

以在线社区留言为例。为了不影响社区的发展,我们要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标志为内容不当。

过滤这类内容是一个很常见的需求。对此问题建立两个类型:侮辱类和非侮辱类,使用1和0分别表示。

我们把文本看成单词向量或者词条向量,也就是说将句子转换为向量。 考虑出现所有文档中的单词,再决定将哪些单词纳入词汇表或者说所要的词汇集合,然后必须要将每一篇文档转换为词汇表上的向量。简单起见,我们先假设已经将本文切分完毕,存放到列表中,并对词汇向量进行分类标注。

编写代码如下:

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']]
    # 类别标签向量,1代表侮辱性词汇,0代表不是
    classVec = [0,1,0,1,0,1]
    return postingList,classVec

if __name__ == '__main__':
    postingLIst, classVec = loadDataSet()
    for each in postingLIst:
        print(each)
    print(classVec)

从运行结果可以看出,我们已经将postingList是存放词条列表中,classVec是存放每个词条的所属类别,1代表侮辱类 ,0代表非侮辱类。

在这里插入图片描述

继续编写代码,前面我们已经说过我们要先创建一个词汇表,并将切分好的词条转换为词条向量。

set()

set() 函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可以计算交集、差集、并集等。

class set([iterable])

返回新的集合对象。

>>>x = set('runoob')
>>> y = set('google')
>>> x, y
(set(['b', 'r', 'u', 'o', 'n']), set(['e', 'o', 'g', 'l']))   # 重复的被删除
>>> x & y         # 交集
set(['o'])
>>> x | y         # 并集
set(['b', 'e', 'g', 'l', 'o', 'n', 'r', 'u'])
>>> x - y         # 差集
set(['r', 'b', 'u', 'n'])
>>>
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']]
    # 类别标签向量,1代表侮辱性词汇,0代表不是
    classVec = [0,1,0,1,0,1]
    return postingList,classVec

# 根据vocabList词汇表,将inputSet向量化,向量的每个元素为1或0
def setOfWords2Vec(vocabList, inputSet):
    # 创建一个其中所含元素都为0的向量
    returnVec = [0] * len(vocabList)
    # 遍历每个词条
    for word in inputSet:
        # 如果词条存在于词汇表中,则置1
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        # 如果词条不在,则输出下面语句
        else: print("the word: %s is not in my Vocabulary!" % word)
    # 返回文档向量
    return returnVec

# 将切分的实验样本词条整理成不重复的词条集合,也就是词汇表
def createVocabList(dataSet):
    # 创建一个空的不重复集合
    vocabSet = set([])
    for document in dataSet:
        # 取并集
        vocabSet = vocabSet | set(document)
    # 返回不重复的词条列表,也就是词汇表
    return list(vocabSet)

if __name__ == '__main__':
    # 先导入数据
    postingList, classVec = loadDataSet()
    print('postingList:\n', postingList)
    # 创建词汇表(消除重复)
    myVocabList = createVocabList(postingList)
    print('myVocabList:\n', myVocabList)
    trainMat = []
    for postinDoc in postingList:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    print('trainMat:\n', trainMat)

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']]
myVocabList:
 ['how', 'park', 'not', 'take', 'is', 
 'worthless', 'my', 'has', 'stupid', 
 'quit', 'him', 'please', 'posting', 
 'so', 'flea', 'I', 'buying', 'help', 
 'dalmation', 'ate', 'dog', 'to', 'maybe',
 'love', 'garbage', 'licks', 'cute', 'problems', 
 'stop', 'mr', 'food', 'steak']
trainMat:
 [[0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], 
 [0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 
 [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0], 
 [0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0], 
 [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1], 
 [0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]]

从运行结果可以看出,postingList是原始的词条列表myVocabList是词汇表

myVocabList是所有单词出现的集合,没有重复的元素

词汇表是用来干什么的?没错,它是用来将词条向量化的,一个单词在词汇表中出现过一次,那么就在相应位置记作1,如果没有出现就在相应位置记作0

trainMat是所有的词条向量组成的列表。它里面存放的是根据myVocabList向量化的词条向量。

我们已经得到了词条向量。接下来,我们就可以通过词条向量训练朴素贝叶斯分类器。

import numpy as np

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']]
    # 类别标签向量,1代表侮辱性词汇,0代表不是
    classVec = [0,1,0,1,0,1]
    return postingList,classVec

# 将切分的实验样本词条整理成不重复的词条集合,也就是词汇表
def createVocabList(dataSet):
    # 创建一个空的不重复集合
    vocabSet = set([])
    for document in dataSet:
        # 取并集
        vocabSet = vocabSet | set(document)
    # 返回不重复的词条列表,也就是词汇表
    return list(vocabSet)

# 根据vocabList词汇表,将inputSet向量化,向量的每个元素为1或0(存在为1,不存在为0)
def setOfWords2Vec(vocabList, inputSet):
    # 创建一个其中所含元素都为0的向量
    # 根据词汇表的长度来进行创建,
    returnVec = [0] * len(vocabList)
    # 遍历inputSet中的每个词条
    for word in inputSet:
        # 如果词条存在于词汇表中,则置1
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        # 如果词条不在,则输出下面语句
        else: print("the word: %s is not in my Vocabulary!" % word)
    # 返回该词条列表向量化后的向量
    return returnVec

# 朴素贝叶斯分类器训练函数
# trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵(即主函数中的trainMat)
# trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
def trainNB0(trainMatrix,trainCategory):
    # 计算训练的文档数目(有多少行)
    numTrainDocs = len(trainMatrix)
    # 计算每篇文档的词条数:以第一(0)行的数据来计算有多少列,就代表有多少词条数,因为它们都是根据词汇表来创建的,所以每行的词条数相同
    numWords = len(trainMatrix[0])
    # 文档属于侮辱类的概率,即:将每一行属于侮辱类的加起来,除以总的行数
    # sum(trainCategory),因为trainCategory标识了每一行的类别(0、1),所以可以直接求和计算侮辱类的个数
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    # 创建numpy.zeros数组,词条出现数初始化为0
    p0Num = np.zeros(numWords)
    p1Num = np.zeros(numWords)
    # 分母初始化为0
    p0Denom = 0.0
    p1Denom = 0.0
    for i in range(numTrainDocs):
        # 统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
        # 类别标签向量,1代表侮辱性词汇,0代表不是
        if trainCategory[i] == 1:
            #p1Num、p0Num:对应元素相加
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        # 统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    p1Vect = p1Num/p1Denom
    p0Vect = p0Num/p0Denom
    # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
    return p0Vect,p1Vect,pAbusive

if __name__ == '__main__':
    # 先导入数据
    postingList, classVec = loadDataSet()
    print('postingList:\n', postingList)
    # 创建词汇表(消除重复)
    myVocabList = createVocabList(postingList)
    print('myVocabList:\n', myVocabList)
    trainMat = []
    # 将原始的词条列表逐一进行向量化(标注0、1),然后添加到trainMat中
    for postinDoc in postingList:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    print('trainMat:\n', trainMat)
    # 构造朴素贝叶斯分类器
    p0V, p1V, pAb = trainNB0(trainMat, classVec)
    print('p0V:\n', p0V)
    print('p1V:\n', p1V)
    print('classVec:\n', classVec)
    print('pAb:\n', pAb)

输出结果如下:

在这里插入图片描述

p0V存放的是每个单词属于类别0,也就是非侮辱类词汇的概率。 比如p0V的倒数第6个概率,就是stupid这个单词属于非侮辱类的概率为0。

同理,p1V存放的是每个单词属于类别1,也就是侮辱类词汇的概率。 p1V的倒数第6个概率,就是stupid这个单词属于侮辱类的概率为0.15789474,也就是约等于15.79%的概率。显而易见,这个单词属于侮辱类。

pAb是所有侮辱类的样本占所有样本的概率,从classVec中可以看出,一用有3个侮辱类,3个非侮辱类。所以侮辱类的概率是0.5。

因此p0V存放的就是P(him|非侮辱类) = 0.0833、P(is|非侮辱类) = 0.0417,一直到P(dog|非侮辱类) = 0.0417,这些单词的条件概率。

同理,p1V存放的就是各个单词属于侮辱类的条件概率。

pAb就是先验概率。

需要多多理解下的是构造分类器的训练函数,其中如何计算条件概率。

# 朴素贝叶斯分类器训练函数
# trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵(即主函数中的trainMat)
# trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
def trainNB0(trainMatrix,trainCategory):
    # 计算训练的文档数目(有多少行)
    numTrainDocs = len(trainMatrix)
    # 计算每篇文档的词条数:以第一(0)行的数据来计算有多少列,就代表有多少词条数,因为它们都是根据词汇表来创建的,所以每行的词条数相同
    numWords = len(trainMatrix[0])
    # 文档属于侮辱类的概率,即:将每一行属于侮辱类的加起来,除以总的行数
    # sum(trainCategory),因为trainCategory标识了每一行的类别(0、1),所以可以直接求和计算侮辱类的个数
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    # 创建numpy.zeros数组,词条出现数初始化为0
    p0Num = np.zeros(numWords)
    p1Num = np.zeros(numWords)
    # 分母初始化为0,分别代表:属于侮辱类(或非侮辱类)的总分词个数(通过一个个词条遍历进行统计)
    p0Denom = 0.0
    p1Denom = 0.0
    for i in range(numTrainDocs):
        # 统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
        # 类别标签向量,1代表侮辱性词汇,0代表不是
        if trainCategory[i] == 1:
            #p1Num、p0Num:对应元素相加,统计每个分词在侮辱类(非侮辱类)中出现的次数(频数)
            p1Num += trainMatrix[i]
            #统计属于侮辱类的分词个数
            p1Denom += sum(trainMatrix[i])
        # 统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
        else:
            p0Num += trainMatrix[i]
            #统计不属于侮辱类的分词个数
            p0Denom += sum(trainMatrix[i])
    #矩阵中的每个元素除以p1Denom
    p1Vect = p1Num/p1Denom
    p0Vect = p0Num/p0Denom
    # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
    return p0Vect,p1Vect,pAbusive

已经训练好分类器,接下来,使用分类器进行分类:

reduce()函数

reduce() 函数会对参数序列中元素进行累积。

函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。

注意:Python3.x reduce() 已经被移到 functools 模块里,如果我们要使用,需要引入 functools 模块来调用 reduce() 函数:

from functools import reduce
reduce(function, iterable[, initializer])

function – 函数,有两个参数
iterable – 可迭代对象
initializer – 可选,初始参数

from functools import reduce

def add(x, y) :            # 两数相加
    return x + y
sum1 = reduce(add, [1,2,3,4,5])   # 计算列表和:1+2+3+4+5
sum2 = reduce(lambda x, y: x+y, [1,2,3,4,5])  # 使用 lambda 匿名函数
print(sum1)
print(sum2)
15
15

reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) 计算的就是((((1+2)+3)+4)+5)。

lambda 匿名函数

计算sum2时,使用了lambda匿名函数:x,y相当于传入的参数,整个函数会返回x+y的值。

1、lambda匿名函数与def区别

lambda 和def它两个的基本用法差不多,参数都是可选,也都会返回对象

2、不同之处:

1)lambda可以定义一个匿名函数,而def定义的函数必须有一个名字。这应该是lambda与def两者最大的区别。
2)lambda是一个表达式,而不是一个语句。
3)lambda的主体是一个单个的表达式,而不是一个代码块。

lambda是一个为编写简单的函数而设计的,而def用来处理更大的任务。

import numpy as np
from functools import reduce

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']]
    # 类别标签向量,1代表侮辱性词汇,0代表不是
    classVec = [0,1,0,1,0,1]
    return postingList,classVec

# 将切分的实验样本词条整理成不重复的词条集合,也就是词汇表
def createVocabList(dataSet):
    # 创建一个空的不重复集合
    vocabSet = set([])
    for document in dataSet:
        # 取并集
        vocabSet = vocabSet | set(document)
    # 返回不重复的词条列表,也就是词汇表
    return list(vocabSet)

# 根据vocabList词汇表,将inputSet向量化,向量的每个元素为1或0(存在为1,不存在为0)
def setOfWords2Vec(vocabList, inputSet):
    # 创建一个其中所含元素都为0的向量
    # 根据词汇表的长度来进行创建,
    returnVec = [0] * len(vocabList)
    # 遍历inputSet中的每个词条
    for word in inputSet:
        # 如果词条存在于词汇表中,则置1
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        # 如果词条不在,则输出下面语句
        else: print("the word: %s is not in my Vocabulary!" % word)
    # 返回该词条列表向量化后的向量
    return returnVec

# 朴素贝叶斯分类器训练函数
# trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵(即主函数中的trainMat)
# trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
def trainNB0(trainMatrix,trainCategory):
    # 计算训练的文档数目(有多少行)
    numTrainDocs = len(trainMatrix)
    # 计算每篇文档的词条数:以第一(0)行的数据来计算有多少列,就代表有多少词条数,因为它们都是根据词汇表来创建的,所以每行的词条数相同
    numWords = len(trainMatrix[0])
    # 文档属于侮辱类的概率,即:将每一行属于侮辱类的加起来,除以总的行数
    # sum(trainCategory),因为trainCategory标识了每一行的类别(0、1),所以可以直接求和计算侮辱类的个数
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    # 创建numpy.zeros数组,词条出现数初始化为0
    p0Num = np.zeros(numWords)
    p1Num = np.zeros(numWords)
    # 分母初始化为0,分别代表:属于侮辱类(或非侮辱类)的总分词个数(通过一个个词条遍历进行统计)
    p0Denom = 0.0
    p1Denom = 0.0
    for i in range(numTrainDocs):
        # 统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
        # 类别标签向量,1代表侮辱性词汇,0代表不是
        if trainCategory[i] == 1:
            #p1Num、p0Num:对应元素相加,统计每个分词在侮辱类(非侮辱类)中出现的次数(频数)
            p1Num += trainMatrix[i]
            #统计属于侮辱类的分词个数
            p1Denom += sum(trainMatrix[i])
        # 统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
        else:
            p0Num += trainMatrix[i]
            #统计不属于侮辱类的分词个数
            p0Denom += sum(trainMatrix[i])
    #矩阵中的每个元素除以p1Denom
    p1Vect = p1Num/p1Denom
    p0Vect = p0Num/p0Denom
    # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
    return p0Vect,p1Vect,pAbusive

# 朴素贝叶斯分类器分类函数
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    # 匿名函数:对应元素相乘
    # 由之前的朴素贝叶斯推断可以知道,因为p1、p0概率的分母相同,所以只需要计算并比较分子的值就可以知道概率的大小
    # 这里利用reduce + 匿名函数:计算出分子的值(也即:p(w0|1)*p(w1|1)*p(w2|1)...)
    # 但如果其中有一个概率值为0,则最后的概率也为0
	p1 = reduce(lambda x,y:x*y, vec2Classify * p1Vec) * pClass1
	p0 = reduce(lambda x,y:x*y, vec2Classify * p0Vec) * (1.0 - pClass1)
	print('p0:',p0)
	print('p1:',p1)
    # 将p1和p0的概率进行比较,取较大的为该语句(语句分为多个词条)的类别
	if p1 > p0:
		return 1
	else:
		return 0

# 测试朴素贝叶斯分类器
def testingNB():
    # 创建实验样本
    listOPosts,listClasses = loadDataSet()
    # 创建词汇表
    myVocabList = createVocabList(listOPosts)
    trainMat=[]
    for postinDoc in listOPosts:
        # 将实验样本向量化
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    # 训练朴素贝叶斯分类器
    p0V,p1V,pAb = trainNB0(np.array(trainMat),np.array(listClasses))
    # 先训练之后,再测试样本
    # 测试样本1
    testEntry = ['love', 'my', 'dalmation']
    # 测试样本向量化(测试样本同样需要向量化):根据已有的词汇表来进行向量化
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
    # 执行分类并打印分类结果
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')
    else:
        print(testEntry,'属于非侮辱类')
    #测试样本2
    testEntry = ['stupid', 'garbage']
    #测试样本向量化
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
    #执行分类并打印分类结果
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')
    else:
        print(testEntry,'属于非侮辱类')

if __name__ == '__main__':
    # # 先导入数据
    # postingList, classVec = loadDataSet()
    # print('postingList:\n', postingList)
    # # 创建词汇表(消除重复)
    # myVocabList = createVocabList(postingList)
    # print('myVocabList:\n', myVocabList)
    # trainMat = []
    # # 将原始的词条列表逐一进行向量化(标注0、1),然后添加到trainMat中
    # for postinDoc in postingList:
    #     trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    # print('trainMat:\n', trainMat)
    # # 构造朴素贝叶斯分类器
    # p0V, p1V, pAb = trainNB0(trainMat, classVec)
    # print('p0V:\n', p0V)
    # print('p1V:\n', p1V)
    # print('classVec:\n', classVec)
    # print('pAb:\n', pAb)
    testingNB()

运行结果:

p0: 0.0
p1: 0.0
['love', 'my', 'dalmation'] 属于非侮辱类
p0: 0.0
p1: 0.0
['stupid', 'garbage'] 属于非侮辱类

你会发现,这样写的算法无法进行分类,p0和p1的计算结果都是0,这里显然存在问题。这是为什么呢?

其实上面的算法存在一定的问题,需要进行改进。

那么需要改进的地方在哪里呢?利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算p(w0|1)p(w1|1)p(w2|1)。

如果其中有一个概率值为0,那么最后的概率也为0。

拉普拉斯平滑(加1平滑)

显然,这样是不合理的,为了降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。这种做法就叫做拉普拉斯平滑(Laplace Smoothing)又被称为加1平滑,是比较常用的平滑方法,它就是为了解决0概率问题。

下溢出

除此之外,另外一个遇到的问题就是下溢出,这是由于太多很小的数相乘造成的。学过数学的人都知道,两个小数相乘,越乘越小,这样就造成了下溢出。在程序中,在相应小数位置进行四舍五入,计算结果可能就变成0了。

为了解决这个问题,对乘积结果取自然对数。通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。下图给出函数f(x)和ln(f(x))的曲线。
在这里插入图片描述
检查这两条曲线,就会发现它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。

因此我们可以对上篇文章的trainNB0(trainMatrix, trainCategory)函数进行更改

当然除此之外,我们还需要对代码进行修改classifyNB(vec2Classify, p0Vec, p1Vec, pClass1)函数

最终代码如下:

import numpy as np
from functools import reduce

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']]
    # 类别标签向量,1代表侮辱性词汇,0代表不是
    classVec = [0,1,0,1,0,1]
    return postingList,classVec

# 将切分的实验样本词条整理成不重复的词条集合,也就是词汇表
def createVocabList(dataSet):
    # 创建一个空的不重复集合
    vocabSet = set([])
    for document in dataSet:
        # 取并集
        vocabSet = vocabSet | set(document)
    # 返回不重复的词条列表,也就是词汇表
    return list(vocabSet)

# 根据vocabList词汇表,将inputSet向量化,向量的每个元素为1或0(存在为1,不存在为0)
def setOfWords2Vec(vocabList, inputSet):
    # 创建一个其中所含元素都为0的向量
    # 根据词汇表的长度来进行创建,
    returnVec = [0] * len(vocabList)
    # 遍历inputSet中的每个词条
    for word in inputSet:
        # 如果词条存在于词汇表中,则置1
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        # 如果词条不在,则输出下面语句
        else: print("the word: %s is not in my Vocabulary!" % word)
    # 返回该词条列表向量化后的向量
    return returnVec

# 朴素贝叶斯分类器训练函数
# 算法改进后:
# trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵(即主函数中的trainMat)
# trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
def trainNB0(trainMatrix,trainCategory):
    # 计算训练的文档数目
    numTrainDocs = len(trainMatrix)
    # 计算每篇文档的词条数
    numWords = len(trainMatrix[0])
    # 文档属于侮辱类的概率
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    # 创建numpy.ones数组,词条出现数初始化为1,拉普拉斯平滑
    p0Num = np.ones(numWords); p1Num = np.ones(numWords)
    # 分母初始化为2,拉普拉斯平滑
    p0Denom = 2.0; p1Denom = 2.0
    for i in range(numTrainDocs):
        # 统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
        if trainCategory[i] == 1:
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        else:
            # 统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    # 取对数,防止下溢出
    p1Vect = np.log(p1Num/p1Denom)
    p0Vect = np.log(p0Num/p0Denom)
    # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
    return p0Vect,p1Vect,pAbusive

# 算法改进前:
# def trainNB0(trainMatrix,trainCategory):
#     # 计算训练的文档数目(有多少行)
#     numTrainDocs = len(trainMatrix)
#     # 计算每篇文档的词条数:以第一(0)行的数据来计算有多少列,就代表有多少词条数,因为它们都是根据词汇表来创建的,所以每行的词条数相同
#     numWords = len(trainMatrix[0])
#     # 文档属于侮辱类的概率,即:将每一行属于侮辱类的加起来,除以总的行数
#     # sum(trainCategory),因为trainCategory标识了每一行的类别(0、1),所以可以直接求和计算侮辱类的个数
#     pAbusive = sum(trainCategory)/float(numTrainDocs)
#     # 创建numpy.zeros数组,词条出现数初始化为0
#     p0Num = np.zeros(numWords)
#     p1Num = np.zeros(numWords)
#     # 分母初始化为0,分别代表:属于侮辱类(或非侮辱类)的总分词个数(通过一个个词条遍历进行统计)
#     p0Denom = 0.0
#     p1Denom = 0.0
#     for i in range(numTrainDocs):
#         # 统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
#         # 类别标签向量,1代表侮辱性词汇,0代表不是
#         if trainCategory[i] == 1:
#             #p1Num、p0Num:对应元素相加,统计每个分词在侮辱类(非侮辱类)中出现的次数(频数)
#             p1Num += trainMatrix[i]
#             #统计属于侮辱类的分词个数
#             p1Denom += sum(trainMatrix[i])
#         # 统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
#         else:
#             p0Num += trainMatrix[i]
#             #统计不属于侮辱类的分词个数
#             p0Denom += sum(trainMatrix[i])
#     #矩阵中的每个元素除以p1Denom
#     p1Vect = p1Num/p1Denom
#     p0Vect = p0Num/p0Denom
#     # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
#     return p0Vect,p1Vect,pAbusive


# 朴素贝叶斯分类器分类函数
# 改进后:
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)        #对应元素相乘。logA * B = logA + logB,所以这里加上log(pClass1)
    p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
    print('p0:',p0)
    print('p1:',p1)
    if p1 > p0:
        return 1
    else:
        return 0

# 改进前:
# def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
#     # 匿名函数:对应元素相乘
#     # 由之前的朴素贝叶斯推断可以知道,因为p1、p0概率的分母相同,所以只需要计算并比较分子的值就可以知道概率的大小
#     # 这里利用reduce + 匿名函数:计算出分子的值(也即:p(w0|1)*p(w1|1)*p(w2|1)...)
#     # 但如果其中有一个概率值为0,则最后的概率也为0
#     print("vec2Classify * p1Vec:\n",vec2Classify*p1Vec)
#     p1 = reduce(lambda x,y:x*y, vec2Classify * p1Vec) * pClass1
#     p0 = reduce(lambda x,y:x*y, vec2Classify * p0Vec) * (1.0 - pClass1)
#     print('p0:',p0)
#     print('p1:',p1)
#     # 将p1和p0的概率进行比较,取较大的为该语句(语句分为多个词条)的类别
#     if p1 > p0:
#         return 1
#     else:
#         return 0

# 测试朴素贝叶斯分类器
def testingNB():
    # 创建实验样本
    listOPosts,listClasses = loadDataSet()
    # 创建词汇表
    myVocabList = createVocabList(listOPosts)
    trainMat=[]
    for postinDoc in listOPosts:
        # 将实验样本向量化
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    # 训练朴素贝叶斯分类器
    p0V,p1V,pAb = trainNB0(np.array(trainMat),np.array(listClasses))
    # print('p0V:\n', p0V)
    # print('p1V:\n', p1V)
    # print('pAb:\n', pAb)
    # 先训练之后,再测试样本
    # 测试样本1
    testEntry = ['love', 'my', 'dalmation']
    # 测试样本向量化(测试样本同样需要向量化):根据已有的词汇表来进行向量化
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
    # 执行分类并打印分类结果
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')
    else:
        print(testEntry,'属于非侮辱类')
    #测试样本2
    testEntry = ['stupid', 'garbage']
    #测试样本向量化
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
    #执行分类并打印分类结果
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')
    else:
        print(testEntry,'属于非侮辱类')

if __name__ == '__main__':
    # # 先导入数据
    # postingList, classVec = loadDataSet()
    # print('postingList:\n', postingList)
    # # 创建词汇表(消除重复)
    # myVocabList = createVocabList(postingList)
    # print('myVocabList:\n', myVocabList)
    # trainMat = []
    # # 将原始的词条列表逐一进行向量化(标注0、1),然后添加到trainMat中
    # for postinDoc in postingList:
    #     trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    # print('trainMat:\n', trainMat)
    # # 构造朴素贝叶斯分类器
    # p0V, p1V, pAb = trainNB0(trainMat, classVec)
    # print('p0V:\n', p0V)
    # print('p1V:\n', p1V)
    # print('classVec:\n', classVec)
    # print('pAb:\n', pAb)
    # 测试分类器
    testingNB()

结果查看:
在这里插入图片描述
这样我们得到的结果就没有问题了,不存在0概率。
在这里插入图片描述
成功进行了言论分类!

三、实践:过滤垃圾邮件

在上篇文章那个简单的例子中,我们引入了字符串列表。使用朴素贝叶斯解决一些现实生活中的问题时,需要先从文本内容得到字符串列表,然后生成词向量。

下面这个例子中,我们将了解朴素贝叶斯的一个最著名的应用:电子邮件垃圾过滤。

首先看一下使用朴素贝叶斯对电子邮件进行分类的步骤:

收集数据:提供文本文件。
数据处理:将文本文件解析成词条向量。
分析数据:检查词条确保解析的正确性。
训练算法:使用我们之前建立的trainNB0()函数。
测试算法:使用classifyNB(),并构建一个新的测试函数来计算文档集的错误率。
使用算法:构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上。

数据处理

对于英文文本,我们可以以非字母、非数字作为符号进行切分,使用split函数即可。编写代码如下:

正则表达式

    listOfTokens = re.split(r'\W', bigString)

\W:

匹配任何非单词字符。等价于“[^A-Za-z0-9_]”。

目的:以非字母、非数字作为符号进行切分。

import re

# 接收一个大字符串并将其解析为字符串列表
# 将字符串转换为字符列表
def textParse(bigString):
    # 将特殊符号作为切分标志进行字符串切分,即非字母、非数字
    listOfTokens = re.split(r'\W', bigString)
    # 除了单个字母,例如大写的I,其它单词变成小写
    return [tok.lower() for tok in listOfTokens if len(tok) >1]

# 将切分的实验样本词条整理成不重复的词条列表,也就是词汇表
def createVocabList(dataSet):
    # 创建一个空的不重复列表
    vocabSet = set([])
    for document in dataSet:
        # 取并集
        vocabSet = vocabSet | set(document)
    return list(vocabSet)

if __name__ == '__main__':
    # docList用来存语句,classList用来存每一条语句的标签(1、0)
    docList = []
    classList = []
    # 遍历25个txt文件
    for i in range(1, 26):
        # 读取每个垃圾邮件,并字符串转换成字符串列表
        wordList = textParse(open('B:/电子书/机器学习实战+源码/machinelearninginaction/Ch04/email/spam/%d.txt' % i , 'r').read())
        docList.append(wordList)
        # 标记垃圾邮件,1表示垃圾文件
        classList.append(1)
        # 读取每个非垃圾邮件,并字符串转换成字符串列表
        wordList = textParse(open('B:/电子书/机器学习实战+源码/machinelearninginaction/Ch04/email/ham/%d.txt' % i , 'r').read())
        docList.append(wordList)
        # 标记非垃圾邮件,1表示垃圾文件
        classList.append(0)
    # 创建词汇表,不重复
    vocabList = createVocabList(docList)
    print(vocabList)

获得的词汇表如下:

['owner', 'see', 'success', 'express',
 'least', 'possible', 'incoming', 'zach',
  'create', 'ones', 'derivatives', 'winter',
  'both', 'faster', 'writing', 'famous', 
  'couple', '180', '50mg', 'as', 'brained',
  'py', 'quality', 'style', 'supporting', 
  'pavilion', 'supplement', '20', 'rain', 'used',...]
#数据量很大,这里只做一部分展示

根据词汇表,我们就可以将每个文本向量化。我们将数据集分为训练集和测试集,使用交叉验证的方式测试朴素贝叶斯分类器的准确性。

编写代码如下:

import numpy as np
import random
import re

# 接收一个大字符串并将其解析为字符串列表
# 将字符串转换为字符列表
def textParse(bigString):
    # 将特殊符号作为切分标志进行字符串切分,即非字母、非数字
    listOfTokens = re.split(r'\W', bigString)
    # 除了单个字母,例如大写的I,其它单词变成小写
    return [tok.lower() for tok in listOfTokens if len(tok) >1]

# 将切分的实验样本词条整理成不重复的词条列表,也就是词汇表
def createVocabList(dataSet):
    # 创建一个空的不重复列表
    vocabSet = set([])
    for document in dataSet:
        # 取并集
        vocabSet = vocabSet | set(document)
    return list(vocabSet)

# 根据vocabList词汇表,将inputSet向量化,向量的每个元素为1或0
def setOfWords2Vec(vocabList, inputSet):
    # 创建一个其中所含元素都为0的向量
    returnVec = [0] * len(vocabList)
    # 遍历每个词条
    for word in inputSet:
        if word in vocabList:
            # 如果词条存在于词汇表中,则置1
            returnVec[vocabList.index(word)] = 1
        else: print("the word: %s is not in my Vocabulary!" % word)
    # 返回文档向量
    return returnVec

# # 根据vocabList词汇表,构建词袋模型
# def bagOfWords2VecMN(vocabList, inputSet):
#     # 创建一个其中所含元素都为0的向量
#     returnVec = [0]*len(vocabList)
#     # 遍历每个词条
#     for word in inputSet:
#         # 如果词条存在于词汇表中,则计数加一
#         if word in vocabList:
#             returnVec[vocabList.index(word)] += 1
#     # 返回词袋模型
#     return returnVec

# 朴素贝叶斯分类器训练函数
# trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵(即主函数中的trainMat)
# trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
def trainNB0(trainMatrix,trainCategory):
    # 计算训练的文档数目
    numTrainDocs = len(trainMatrix)
    # 计算每篇文档的词条数
    numWords = len(trainMatrix[0])
    # 文档属于侮辱类的概率
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    # 创建numpy.ones数组,词条出现数初始化为1,拉普拉斯平滑
    p0Num = np.ones(numWords); p1Num = np.ones(numWords)
    # 分母初始化为2,拉普拉斯平滑
    p0Denom = 2.0; p1Denom = 2.0
    for i in range(numTrainDocs):
        # 统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
        if trainCategory[i] == 1:
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        else:
            # 统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    # 取对数,防止下溢出
    p1Vect = np.log(p1Num/p1Denom)
    p0Vect = np.log(p0Num/p0Denom)
    # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
    return p0Vect,p1Vect,pAbusive

# 朴素贝叶斯分类器分类函数
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    # 对应元素相乘。logA * B = logA + logB,所以这里加上log(pClass1)
    p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
    p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
    # print('p0:',p0)
    # print('p1:',p1)
    if p1 > p0:
        return 1
    else:
        return 0

# 测试朴素贝叶斯分类器
def spamTest():
    # docList用来存语句
    docList = []
    # classList用来存每一条语句的标签(1、0)
    classList = []
    # 遍历25个txt文件
    for i in range(1, 26):
        # 读取每个垃圾邮件,并字符串转换成字符串列表
        # 无论是不是垃圾邮件都要存入docList(数据集)
        wordList = textParse(open('B:/电子书/机器学习实战+源码/machinelearninginaction/Ch04/email/spam/%d.txt' % i, 'r').read())
        docList.append(wordList)
        # 标记垃圾邮件,1表示垃圾文件
        classList.append(1)
        # 读取每个非垃圾邮件,并字符串转换成字符串列表
        wordList = textParse(open('B:/电子书/机器学习实战+源码/machinelearninginaction/Ch04/email/ham/%d.txt' % i, 'r').read())
        docList.append(wordList)
        # 标记非垃圾邮件,1表示垃圾文件
        classList.append(0)
    # 创建词汇表,不重复
    vocabList = createVocabList(docList)
    # print(vocabList)
    # 创建存储训练集的索引值的列表和测试集的索引值的列表,它们都只存索引值
    trainingSet = list(range(50))
    testSet = []
    # 从50个邮件中,随机挑选出40个作为训练集,10个做测试集 (8:2)
    for i in range(10): # for循环控制选的个数
        # 随机选取索引值
        randIndex = int(random.uniform(0, len(trainingSet)))
        # 添加测试集的索引值
        testSet.append(trainingSet[randIndex])
        # 在训练集列表中删除添加到测试集的索引值
        del(trainingSet[randIndex])
    # 创建训练集矩阵和训练集类别标签系向量
    trainMat = []
    trainClasses = []
    # 遍历训练集
    for docIndex in trainingSet:
        # 将生成的词集模型添加到训练矩阵中
        trainMat.append(setOfWords2Vec(vocabList, docList[docIndex]))
        # 将类别添加到训练集类别标签系向量中
        trainClasses.append(classList[docIndex])
    # 训练朴素贝叶斯模型
    p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses))
    # 错误分类计数
    errorCount = 0
    # 遍历测试集,验证测试结果
    for docIndex in testSet:
        # 测试集的词集模型
        wordVector = setOfWords2Vec(vocabList, docList[docIndex])
        # 如果分类错误
        if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
            # 错误计数加1
            errorCount += 1
            print("分类错误的测试集:",docList[docIndex])
    # 计算错误率
    print('错误率:%.2f%%' % (float(errorCount) / len(testSet) * 100))

if __name__ == '__main__':
    spamTest()

运行结果:

分类错误的测试集: ['home', 'based', 'business', 'opportunity', 
'is', 'knocking', 'at', 'your', 'door', 'don抰', 'be', 'rude', 
'and', 'let', 'this', 'chance', 'go', 'by', 'you', 'can', 'earn',
'great', 'income', 'and', 'find', 'your', 'financial', 'life',
'transformed', 'learn', 'more', 'here', 'to', 'your', 'success', 
'work', 'from', 'home', 'finder', 'experts']
错误率:10.00%

函数spamTest()会输出在10封随机选择的电子邮件上的分类错误概率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。

四、总结

朴素贝叶斯推断的一些优点:

  • 生成式模型,通过计算概率来进行分类,可以用来处理多分类问题。

  • 对小规模的数据表现很好,适合多分类任务,适合增量式训练,算法也比较简单。

朴素贝叶斯推断的一些缺点:

  • 对输入数据的表达形式很敏感。

  • 由于朴素贝叶斯的“朴素”特点,所以会带来一些准确率上的损失。

  • 需要计算先验概率,分类决策存在错误率。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页