机器学习 —— 朴素贝叶斯

目录

一、基于贝叶斯决策理论的分类方法

二、条件概率

三、朴素贝叶斯分类器

四、使用朴素贝叶斯进行文档分类

4.1 准备数据:从文本中构建词向量

4.2 训练算法:从词向量计算概率

4.3 测试算法:根据现实情况修改分类器

4.4 准备数据:文档词袋模型

五、示例:使用朴素贝叶斯过滤垃圾邮件

5.1  准备数据:切分文本

5.2 测试算法:使用朴素贝叶斯进行交叉验证

5.3 垃圾邮件分类完整代码 

 六、朴素贝叶斯实验总结


一、基于贝叶斯决策理论的分类方法

        朴素贝叶斯是经典的机器学习算法之一,也是为数不多的基于概率论的分类算法。对于大多数的分类算法,在所有的机器学习分类算法中,朴素贝叶斯和其他绝大多数的分类算法都不同。比如决策树,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)。

即上图中橙色部分占红色部分的比例,可以表示为:

P(A|B) = \frac{P(A\cap B) }{P(B)}

则我们可以得到:

P(A\cap B) = P(A|B)P(B)

同样地,在事件A发生的条件下事件B发生的概率为:

P(B|A) = \frac{P(B\cap A) }{P(A)}

我们也可以得到:

P(A\cap B) = P(B|A)P(A)

于是我们就可以,结合上面的式子得到:

P(B|A)P(A) = P(A|B)P(B)

 通过变换上面的等式,所以有条件概率的公式:

P(A|B) = \frac{P(B| A)P(A) }{P(B)}

三、朴素贝叶斯分类器

先验概率P(X):先验概率是指根据以往经验和分析得到的概率。

后验概率P(Y|X):事情已发生,要求这件事情发生的原因是由某个因素引起的可能性的大小,后验分布P(Y|X)表示事件X已经发生的前提下,事件Y发生的概率,称事件X发生下事件Y的条件概率。

后验概率P(X|Y):在已知Y发生后X的条件概率,也由于知道Y的取值而被称为X的后验概率。

朴素:朴素贝叶斯算法是假设各个特征之间相互独立,也是朴素这词的意思,那么贝叶斯公式中的P(X|Y)可写成:

 P(X|Y) = P(x_1|Y)P(x_2|Y)\cdot\cdot \cdot P(x_n|Y)

朴素贝叶斯公式

P(Y|X) = \frac{P(x_1|Y)P(x_2|Y)\cdot \cdot \cdot P(x_n|Y)P(Y)}{P(X)}

朴素贝叶斯分类器:朴素贝叶斯分类器(Naïve Bayes Classifier)采用了“属性条件独立性假设” ,即每个属性独立地对分类结果发生影响。为方便公式标记,不妨记P(C=c|X=x)为P(c|x),基于属性条件独立性假设,贝叶斯公式可重写为:

P(c|x) = \frac{P(x|c)P(c)}{P(x)} = \frac{P(c)}{P(x)}\prod_{i=1}^{d}P(x_i|c)

其中d为属性数目,x_i 为  x 在第 i 个属性上的取值。

由于对所有类别来说 P(x)相同,因此MAP判定准则可改为:

h_n_b(x)=argmaxP(c)\prod_{i=1}^{d}P(x_i|c)

其中  P(c)  和  P(x_i|c) 为目标参数。

        朴素贝叶斯分类器的训练器的训练过程就是基于训练集D估计类先验概率 P(c) ,并为每个属性估计条件概率  P(x_i|c)

        令  D_c  表示训练集D中第c类样本组合的集合,则类先验概率:   

P(c)=\frac{|D_c|}{D}

拉普拉斯修正:若某个属性值在训练集中没有与某个类同时出现过,则训练后的模型会出现 over-fitting 现象。比如训练集中没有该样例,因此连乘式计算的概率值为0,这显然不合理。因为样本中不存在(概率为0),不代该事件一定不可能发生。所以为了避免其他属性携带的信息,被训练集中未出现的属性值“ 抹去” ,在估计概率值时通常要进行“拉普拉斯修正

P(c|x) = \frac{P(x|c)P(c)}{P(x)} = \frac{P(c)}{P(x)}\prod_{i=1}^{d}P(x_i|c),我们要修正  P(x_i|c)   的值。

令 N 表示训练集 D 中可能的类别数,N_i  表示第i个属性可能的取值数,则贝叶斯公式可修正为:

\hat{P}(C) =\frac{|D_c|+1}{|D|+N}          \hat{P}(x_i|c) =\frac{|D_c,_x_i|+1}{|D|+N_i}

防溢出策略:条件概率乘法计算过程中,因子一般较小(均是小于1的实数)。当属性数量增多时候,会导致累乘结果下溢出的现象。

在代数中有  ln(a*b) = ln(a)+ln(b) ,因此可以把条件概率累乘转化成对数累加。分类结果仅需对比概率的对数累加法运算后的数值,以确定划分的类别。

四、使用朴素贝叶斯进行文档分类

        机器学习的一个重要应用就是文档的自动分类。在文档分类中,整个文档(如一封电子邮件)是实例,而电子邮件中的某些元素则构成特征。虽然电子邮件是一种会不断增加的文本,但我们同样也可以对新闻报道、用户留言、政府公文等其他任意类型的文本进行分类。我们可以观察文档中出现的词,并把每个词的出现或者不出现作为一个特征,这样得到的特征数目就会跟词汇表中的词目一样多。朴素贝叶斯是上节介绍的贝叶斯分类器的一个扩展,是用于文档分类的常用算法。

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 替换为 w。粗体 w 表示这是一个向量,即它由多个数值组成。在这个例子中,数值个数与词汇表中的词个数相同。

 p(c_i|w)=\frac{p(w|c_i)p(c_i)}{p(w)}

上述公式,对每个类计算该值,然后比较这两个概率值的大小。首先可以通过类别 i(侮辱性留言或非侮辱性留言)里文档数除以总的文档数来计算概率 p(c_i) 。接下来计算  p(w|c_i) ,这里就要用到朴素贝叶斯假设。如果将 w 展开为一个个独立特征,那么就可以将上述概率写作p(w_0,w_1,w_2\cdot \cdot \cdot w_n|c_i)。这里假设所有词都互相独立,该假设也称作条件独立性假设,它意味着可使用 p(w_0|ci)p(w_1|ci)p(w_2|ci)...p(w_n|ci) 来计算上述概率,这就极大地简化了计算的过程。

该函数的伪代码如下:
计算每个类别中的文档数目
对每篇训练文档:
        对每个类别:
                如果词条出现在文档中→ 增加该词条的计数值
                增加所有词条的计数值
        对每个类别:
                对每个词条:
                        将该词条的数目除以总词条数目得到条件概率
返回每个类别的条件概率

# 朴素贝叶斯分类器训练函数
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),即为非侮辱性文档的概率。计算 p(w_i|c_=_1) 和 p(w_i|c_=_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 测试算法:根据现实情况修改分类器

        利用朴素贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概 率,即计算 p(w_0|1)p(w_1|1)p(w_2|1)如果其中一个概率值为0,那么最后的乘积也为0。为降低 这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。

# 解决办法: 
p0Num = ones(numWords)                              # 创建numpy.ones数组,初始化概率
p1Num = ones(numWords)                              # 创建numpy.ones数组,初始化概率
p0Denom = 2.0                                       # 初始化为2
p1Denom = 2.0                                       # 初始化为2

        另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积 p(w_0|ci)p(w_1|ci)p(w_2|ci)...p(w_n|ci) 时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。一种解决办法是对乘积取自然对数。在代数中有 ln(a*b) = ln(a)+ln(b) ,于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。下图给出函数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)).
 

参数

  • str -- 分隔符,默认为所有的空字符,包括空格、换行(\n)、制表符(\t)等。
  • num -- 分割次数。默认为 -1, 即分隔所有。

返回值

返回分割后的字符串列表。

# 例子
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,于是就可以了。

        朴素贝叶斯的缺点:如果我们使用了样本属性真实情况其实并不是相互独立性的,那么其实这样的分类效果可能不会很好。而且需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候,会由于假设的先验模型的原因导致预测效果不佳。

  • 67
    点赞
  • 494
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DreamWendy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值