python3《机器学习实战系列》学习笔记----2.朴素贝叶斯法

前言

     机器学习实战系列之学习笔记主要是本人进行学习机器学习的整理。本系列所有代码是用python3编写,并使用IDE PycharmWindows平台上编译通过。本系列所涉及的所有代码和资料可在我的github或者码云上下载到,gitbub地址:https://github.com/mcyJacky/MachineLearning,码云地址:https://gitee.com/mcyHome/MachineLearning,如有问题,欢迎指出~。

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

1.1 条件概率

(一)条件概率
     定义:设A,B是两个事件,且 P(A)>0 P ( A ) > 0 ,称

P(B|A)=P(AB)P(A) P ( B | A ) = P ( A B ) P ( A )
为在事件A发生的条件下事件B发生的概率。
(二)乘法定理
     定义:设 P(A)>0 P ( A ) > 0 ,则有
P(AB)=P(B|A)P(A) P ( A B ) = P ( B | A ) P ( A )
为乘法公式。
     一般,设 A1,A2,Annn2,P(A1A2...An)>0 A 1 , A 2 … , A n 为 n 个 事 件 , n ≥ 2 , 且 P ( A 1 A 2 . . . A n ) > 0 ,则有
P(A1A2...An)=P(An|A1A2...An1)P(An1|A1A2...An2)...P(A2|A1)P(A1) P ( A 1 A 2 . . . A n ) = P ( A n | A 1 A 2 . . . A n − 1 ) P ( A n − 1 | A 1 A 2 . . . A n − 2 ) . . . P ( A 2 | A 1 ) P ( A 1 )

(三)全概率公式和贝叶斯公式
     全概率公式定义:设试验 E E 的样本空间为S A A E的事件, B1,B2...Bn B 1 , B 2 . . . B n S S 的一个划分,且P(Bi)>0,则
P(A)=P(A|B1)P(B1)+P(A|B2)P(B2)++P(A|Bn)P(Bn) P ( A ) = P ( A | B 1 ) P ( B 1 ) + P ( A | B 2 ) P ( B 2 ) + … + P ( A | B n ) P ( B n )
为全概率公式。
     贝叶斯公式定义:设试验 E E 的样本空间为S A A E的事件, B1,B2...Bn B 1 , B 2 . . . B n S S 的一个划分,且P(A)>0,P(Bi)>0,则
P(Bi|A)=P(BiA)P(A)=P(A|Bi)P(Bi)nj=1P(A|Bj)P(Bj) P ( B i | A ) = P ( B i A ) P ( A ) = P ( A | B i ) P ( B i ) ∑ j = 1 n P ( A | B j ) P ( B j )
为贝叶斯公式,其中 P(Bi) P ( B i ) 先验概率 P(A|Bi) P ( A | B i ) 似然函数

1.2 朴素贝叶斯理论(Naive Bayes)

     朴素贝叶斯法是基于贝叶斯定理与特征条件独立假设的分类方法。对于给定的训练数据集,首先基于特征条件独立假设学习输入/输出的联合概率分布;然后基于此模型,对给定的输入x,利用贝叶斯定理求出后验概率最大的输出y。
     设输入空间 ψRn ψ ∈ R n n n 维向量的集合,输出空间为类标记集合ω={c1,c2...,cn}.输入为特征向量 xψ x ∈ ψ ,输出为类标记(class label) yω y ∈ ω 。 X 是 定 义 在 输 入 空 间 ψ 上 的 随 机 向 量 , Y 是 定 义 在 输 出 空 间 ω 上 的 随 机 变 量 。 P(X,Y)是 X X Y的联合概率分布,训练数据集:

T={(x1,y1),(x2,y2)...(xn,yn)} T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) . . . ( x n , y n ) }
P(X,Y) P ( X , Y ) 独立同分布产生。
     朴素贝叶斯法通过训练数据集学习联合概率分布 P(X,Y) P ( X , Y ) ,具体地,学习一下先验概率分布及条件概率分布。 先验概率分布:
P(Y=ck),k=1,2,...K P ( Y = c k ) , k = 1 , 2 , . . . K

      条件概率(似然函数)分布:
P(X=x|Y=ck)=P(X(1)=x(1),...,X(n)=x(n)|Y=ck),k=1,2...,K P ( X = x | Y = c k ) = P ( X ( 1 ) = x ( 1 ) , . . . , X ( n ) = x ( n ) | Y = c k ) , k = 1 , 2... , K

     朴素贝叶斯法对条件概率分布作了 条件独立性的假设。正是由于这是一个较强的假设,朴素贝叶斯才由此得名。具体地, 条件独立性假设是
P(X=x|Y=ck)=P(X(1)=x(1),...,X(n)=x(n)|Y=ck)=j=1nP(X(j)=x(j)|Y=ck) P ( X = x | Y = c k ) = P ( X ( 1 ) = x ( 1 ) , . . . , X ( n ) = x ( n ) | Y = c k ) = ∏ j = 1 n P ( X ( j ) = x ( j ) | Y = c k )

     根据条件独立假设,对给定的输入 x x ,构建朴素贝叶斯公式
P(Y=ck|X=x)=P(X=x|Y=ck)P(Y=ck)P(X=x)=P(X=x|Y=ck)P(Y=ck)kP(X=x|Y=ck)P(Y=ck)=P(Y=ck)j=1P(X(j)=x(j)|Y=ck)kP(X=x|Y=ck)P(Y=ck),k=1,2...K

     这就是朴素贝叶斯的基本公式,由于上式分母对所有 ck c k 都是相同的。于是,朴素贝叶斯的分类器可表示为:
y=f(x)=argmaxP(Y=ck)j=1P(X(j)=x(j)|Y=ck) y = f ( x ) = a r g m a x P ( Y = c k ) ∏ j = 1 P ( X ( j ) = x ( j ) | Y = c k )

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

     机器学习的一个重要应用就是文档的自动分类。在文档分类中,整个文档(如一封电子邮件)是实例,而电子邮件中的某个元素则构成特征。我们可以观察文档中出现的词,并把每个词的出现或者不出现作为一个特征,这样得到的特征数目就会跟词汇表中的词目一样多。使用朴素贝叶斯的一般流程如下:

  • (1)收集数据:可以使用任何方法。如RSS源
  • (2)准备数据:需要数值型或布尔型数据
  • (3)分析数据:有大量特征时,绘制特征作用不大,此时用直方图效果更好
  • (4)训练数据:计算不同的独立特征的条件概率
  • (5)测试数据:计算错误率
  • (6)使用算法:一个常见的朴素贝叶斯应用是文档分类。可以在任意的分类场景中使用朴素贝叶斯分类器,不一定非要是文本。
2.1 准备数据:从文本中构建词向量

     要从文本中获取特征,需要先拆分文本。具体如何做呢?这里的特征是来自文本的词条(token),一个词条可以使字符的任意组合。可以把词条想象成单词,也可以使用非单词词条,如URL、IP地址或者任意其他字符串。然后将每一个文本片段表示为一个词条向量,其中值1表示词条出现在文档中,0表示词条未出现
     以在线社区留言板为例,为了不影响社区的发展,我们要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标识为内容不当。对这个问题建立两个类别:侮辱类和非侮辱类,使用1和0分别表示。首先,我们把文本看成单词向量或者词条向量,也就是说将句子转换为向量。考虑出现在所有文档中的所有单词,再决定将哪些词纳入词汇表或者说所要的词汇集合,然后必须要将每一篇文档转换为词汇表上的向量。接下来,我们创建一个bayes.py的文件来实现:

import numpy as np
import time
'''
@function:创建一个实验样本
@#param: None
@return: 
    postingList [list] 词条切分后的文档集合
    classsVect [list] 一个类别标签的集合
'''
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

'''
@function:创建一个包含在所有文档中出现的不重复词的列表
@param: 
    dataSet 样本数据集
@return: 
    vocabSet [list] 不重复词条列表
'''
def createVocabList(dataSet):
    vocabSet = set([])      #创建一个空集合
    for document in dataSet:
        vocabSet = vocabSet | set(document) #集合求并集
    return list(vocabSet)

'''
@function:词集模型,根据词汇表,将文档inputSet转化为向量
@param: 
    vocabList [list] 词汇表
    inputSet [list] 某个文档
@return: 
    returnVec [list] 对应文档向量
'''
def setOfWords2Vec(vocabList, inputSet):
    returnVec = [0]*len(vocabList)  #构建与词汇列表相同长度的列表
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1    #如果词条在词汇表中存在,则相应位置标1
        else:print('the word: %s is not in my vocabulary' % word)
    return returnVec

if __name__ == '__main__':
    start = time.clock()
    listOPosts, listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    print(myVocabList)
    returnVec = setOfWords2Vec(myVocabList, listOPosts[0])
    print(returnVec)
    returnVec = setOfWords2Vec(myVocabList, listOPosts[1])
    print(returnVec)
    end = time.clock()
    print('Finish in', end - start)

'''输出结果
['dog', 'how', 'park', 'him', 'buying', 'problems', 'quit', 'stupid', 'maybe', 'dalmation', 
'to', 'love', 'is', 'mr', 'cute', 'take', 'help', 'ate', 'I', 'stop', 'garbage', 'so', 'steak', 
'flea', 'licks', 'my', 'food', 'not', 'has', 'posting', 'worthless', 'please']
[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1]
[1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
Finish in 0.00014211268667586977
'''

     通过上述我们已经可以构建从词表到向量的转换,首先通过第一个函数loadDataSet()创建实验样本。接着通过createVocabList()创建一个包含在所有文档中出现的不重复的列表。获得词汇表后,最后通过setOfWords2Vec()将某文档转换为文档向量。完成这三步的操作后,就完成了文档向向量的转换。

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

     之前我们已经知道了如何将一组单词转换为一组数字,接下来看看如何利用这些数字计算概率。现在我们已经知道一个词是否出现在一篇文档中,也知道该文档所属的类别。根据朴素贝叶斯公式:

P(ci|w)=P(w|ci)P(ci)P(w) P ( c i | w ) = P ( w | c i ) P ( c i ) P ( w )
我们将根据上述公式,对每个类计算该值,然后比较这两个概率值的大小。如何计算呢? 首先先通过类别 i i (侮辱性留言或非侮辱性留言)中文档数除以总的文档数来计算先验概率P(ci)。接着,将 w w 展开为一个个独立特征,那么上述概率写作P(w0,w1,..wN|ci),这里假设所有词都是相互独立,该假设也称为条件独立性假设,它意味着可以使用 P(w0|ci)P(w1|ci)...P(wN|ci) P ( w 0 | c i ) P ( w 1 | c i ) . . . P ( w N | c i ) 来计算。
     该函数的伪代码如下:

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

     实现如上伪代码如下:

'''
@function:朴素贝叶斯分类器训练函数
@param: 
    trainMatrix [list] 训练文档矩阵
    trainCtegory [list] 训练文档标签
@return: 
    p0Vect [list] 非侮辱性词条条件概率数组
    p1Vect [list] 侮辱性词条条件概率数组
    pAbusive [float] 文档属于侮辱类的概率
'''
def trainNB0(trainMatrix, trainCtegory):
    numTrainDoc = len(trainMatrix)  #训练样本个数
    pAbusive = sum(trainCtegory)/float(numTrainDoc) # 侮辱性文档的概率
    numWords = len(trainMatrix[0])
    p0Num = np.zeros(numWords); p1Num = np.zeros(numWords) #创建0数组,初始化概率
    p0Denum = 0.0; p1Denum = 0.0    #初始化分母
    for i in range(numTrainDoc):
        if trainCtegory[i] == 1: #侮辱性词条条件概率数组
            p1Num += trainMatrix[i]
            p1Denum += sum(trainMatrix[i])
        else:                   #非侮辱性词条条件概率数组
            p0Num += trainMatrix[i]
            p0Denum += sum(trainMatrix[i])
    p1Vect = p1Num/p1Denum
    p0Vect = p0Num/p0Denum
    return p0Vect, p1Vect, pAbusive

if __name__ == '__main__':
    start = time.clock()
    listOPosts, listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    trainMat = []
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    print('trainMat:\n',trainMat)
    p0V, p1V, pAb = trainNB0(trainMat, listClasses)
    print('p0V:\n', p0V)
    print('p1V:\n', p1V)
    print('pAb:', pAb)
    end = time.clock()
    print('Finish in', end - start)

'''输出结果
trainMat: 
[[0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 
[0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0], 
[0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 
[0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0], 
[1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1], 
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0]]
p0V: 
[0.04166667 0.         0.04166667 0.04166667 0.04166667 0.
 0.04166667 0.04166667 0.04166667 0.04166667 0.         0.04166667
 0.         0.04166667 0.08333333 0.04166667 0.04166667 0.
 0.04166667 0.         0.04166667 0.04166667 0.04166667 0.04166667
 0.         0.04166667 0.         0.         0.         0.
 0.04166667 0.125     ]
p1V: 
[0.         0.05263158 0.         0.05263158 0.         0.05263158
 0.         0.         0.         0.05263158 0.10526316 0.
 0.05263158 0.         0.05263158 0.10526316 0.         0.05263158
 0.         0.05263158 0.         0.         0.         0.
 0.15789474 0.         0.05263158 0.05263158 0.05263158 0.05263158
 0.         0.        ]
pAb: 0.5
Finish in 0.0013086209898069676
'''

     通过朴素贝叶斯的训练函数,我们计算得到pAb=0.5,即文档中侮辱类的概率为0.5。并分别计算出侮辱性类别中不同词条出现的概率和非侮辱性类别中不同词条出现的概率。当然,在使用该函数之前,我们还需解决函数中的一些缺陷。

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

     利用上述贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算 P(w0|1)P(w1|1)P(w2|1) P ( w 0 | 1 ) P ( w 1 | 1 ) P ( w 2 | 1 ) 问题①:在计算过程中,如果出现其中一个概率为0,那么无论怎么乘积的结果也还会是0。这将影响到后验概率的计算结果,使分类产生偏差。为降低这种影响,我们通常采取拉普拉斯平滑(Laplace smoothing),将所有出现词初始化为1,并将分母初始为为2问题②:后验概率由于太多很小的数相乘,而大部分因子都非常小,所以程序会下溢出或找不到正确的答案(用python进行很多很小的数相乘,最后四舍五入会得到0)。一种解决的方法是对乘数求自然对数,在代数中有 ln(ab)=ln(a)+ln(b) l n ( a b ) = l n ( a ) + l n ( b ) ,于是通过求对数可以避免下溢出或者浮点数舍入错误。同时,采用自然对数进行处理不会有任何损失。如图2-1为函数 f(x)ln(f(x)) f ( x ) 和 l n ( f ( x ) ) 的曲线。检查这两条曲线,就会发现它们在相同区域内同时增加或减少,并且在相同点上去到极值,不会影响最终的计算结果。

这里写图片描述
图2.1 函数f(x)和ln(f(x))

     下面是修改后的朴素贝叶斯分类器训练函数:

import math

'''
@function:朴素贝叶斯分类器训练函数(修改版)
@param: 
    trainMatrix [list] 训练文档矩阵
    trainCtegory [list] 训练文档标签
@return: 
    p0Vect [list] 非侮辱性词条条件概率数组
    p1Vect [list] 侮辱性词条条件概率数组
    pAbusive [float] 文档属于侮辱类的概率
'''
def trainNB(trainMatrix, trainCtegory):
    numTrainDoc = len(trainMatrix)  # 训练样本个数
    pAbusive = sum(trainCtegory) / float(numTrainDoc)  # 侮辱性文档的概率
    numWords = len(trainMatrix[0])
    p0Num = np.ones(numWords);
    p1Num = np.ones(numWords)  # 创建单位数组,初始化概率
    p0Denum = 2.0;
    p1Denum = 2.0  # 初始化分母
    for i in range(numTrainDoc):
        if trainCtegory[i] == 1: #侮辱性词条条件概率数组
            p1Num += trainMatrix[i]
            p1Denum += sum(trainMatrix[i])
        else:                   #非侮辱性词条条件概率数组
            p0Num += trainMatrix[i]
            p0Denum += sum(trainMatrix[i])
    p1Vect = np.log(p1Num / p1Denum)
    p0Vect = np.log(p0Num / p0Denum)
    return p0Vect, p1Vect, pAbusive

if __name__ == '__main__':
    start = time.clock()
    listOPosts, listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    trainMat = []
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    print('trainMat:\n',trainMat)
    p0V p1V, pAb = trainNB(trainMat, listClasses)
    print('p0V:\n', p0V)
    print('p1V:\n', p1V)
    print('pAb:', pAb)
    end = time.clock()
    print('Finish in', end - start)

'''输出结果
trainMat:
 [[1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0], 
 [0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0], 
 [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0], 
 [0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1], 
 [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1], 
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0]]
p0V:
 [-2.56494936 -2.56494936 -2.56494936 -3.25809654 -3.25809654 -2.56494936
 -2.56494936 -2.56494936 -3.25809654 -3.25809654 -3.25809654 -2.56494936
 -2.56494936 -2.56494936 -2.56494936 -3.25809654 -2.56494936 -2.56494936
 -2.56494936 -2.56494936 -3.25809654 -2.56494936 -2.15948425 -2.56494936
 -2.56494936 -3.25809654 -1.87180218 -3.25809654 -2.56494936 -3.25809654
 -3.25809654 -2.56494936]
p1V:
 [-3.04452244 -3.04452244 -3.04452244 -2.35137526 -2.35137526 -3.04452244
 -3.04452244 -3.04452244 -2.35137526 -1.94591015 -2.35137526 -3.04452244
 -3.04452244 -2.35137526 -3.04452244 -2.35137526 -3.04452244 -1.94591015
 -3.04452244 -3.04452244 -2.35137526 -3.04452244 -2.35137526 -3.04452244
 -3.04452244 -1.65822808 -3.04452244 -2.35137526 -3.04452244 -2.35137526
 -2.35137526 -2.35137526]
pAb: 0.5
Finish in 0.0012261166800423655
'''

     现在我们已经构建好完整的分类器了,下面使用numpy向量处理功能:

'''
@function:朴素贝叶斯的分类函数
@param: 
    vec2Classify [list] 要分类的向量
    p0Vect [list] 非侮辱性的条件概率数组
    p1Vect [list] 侮辱性的条件概率数组 
    pClass1 [list] 某一分类的概率(先验概率)
@return: 分类结果
'''
def classifyNB(vec2Classify, p0Vect, p1Vect, pClass1):
    p1 = sum(vec2Classify*p1Vect) + np.log(pClass1)     #取对数
    p0 = sum(vec2Classify*p0Vect) + np.log(1- pClass1)  #取对数
    if p1 > p0:
        return 1
    else:
        return 0

'''
@function:算法测试
@param: None
@return: 测试结果
'''
def testingNB():
    listOPosts, listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    trainMat = []
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    p0V, p1V, pAb = trainNB(np.array(trainMat), np.array(listClasses))
    testEntry = ['love', 'my', 'dalmation']
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
    print(testEntry, 'classified as: ', classifyNB(thisDoc, p1V, p0V, pAb))
    testEntry = ['stupid', 'garbage']
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
    print(testEntry, 'classified as: ', classifyNB(thisDoc, p1V, p0V, pAb))

if __name__ == '__main__':
    start = time.clock()
    testingNB()
    end = time.clock()
    print('Finish in', end - start)

'''输出结果
['love', 'my', 'dalmation'] classified as:  1
['stupid', 'garbage'] classified as:  0
Finish in 0.0004077844592671486
'''

     我们已经通过构建朴素贝叶斯分类函数对testEntry = [‘love’, ‘my’, ‘dalmation’]testEntry = [‘stupid’, ‘garbage’]进行分类,通过计算结果可知,前者是属于侮辱性文档,后者是属于非侮辱性文档。下面我们对代码做些修改,使分类器工作得更好。

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

     目前为止,我们将每个词的出现与否作为一个特征,这可以被描述成词集模型(set-of-words model)。如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)。在词袋中,一个词可以出现多次,而在词集中每个词只能出现一次。为适应词袋模型,对函数setOfWords2Vec()做修改,修改为bagOfWords2Vec():

'''
@function:词袋模型,根据词汇表,将文档inputSet转化为向量
@param: 
    vocabList [list] 词汇表
    inputSet [list] 某个文档
@return: 
    returnVec [list] 对应文档向量
'''
def bagOfWords2Vec(vocabList, inputSet):
    returnVec = [0]*len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1   #如果有单词出现,改为相应值增加1.
    return  returnVec

     现在分类器已经构建好了,下面我们将利用该分类器来过滤垃圾邮件。

三、示例:使用朴素贝叶斯过滤邮件

     之前那个简单的例子中,我们直接引入了字符串列表。而使用朴素贝叶斯解决一些现实生活中的问题时,需要先从文本内容得到字符串列表,然后生成词向量。下面我们将了解朴素贝叶斯一个最著名的例子:电子邮件垃圾过滤。首先,我们看一下如何用通用框架解决这一问题。

  • (1)收集数据:提供文本文件
  • (2)准备数据:将本文文件解析成词条向量
  • (3)分析数据:检查词条确保解析的正确性
  • (4)训练数据:使用之前的训练函数trainNB()计算不同的独立特征的条件概率
  • (5)测试数据:使用classifyNB(),并且构建一个新的测试函数来计算文档集的错误率
  • (6)使用算法:构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上
3.1 准备数据:切分文本

     具体在切分文本文件时,我们使用python正则表达式和string.split()来进行切分。如下示例:

if __name__ == '__main__':
    import re
    mySent = 'This book is the best book on Python or M.L. I habe ever laid eyes upon.'
    regEx = re.compile('\\W*') #非字母数字
    listOfTockens = regEx.split(mySent) #切分得到词条列表,包括空格词条
    print(listOfTockens)
    listOfTockens = [tok.lower() for tok in listOfTockens if len(tok) > 0]  #去掉空字符串,并小写表示
    print(listOfTockens)

'''输出结果
['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python', 'or', 'M', 'L', 'I', 'habe', 'ever', 'laid', 'eyes', 'upon', '']

['this', 'book', 'is', 'the', 'best', 'book', 'on', 'python', 'or', 'm', 'l', 'i', 'habe', 'ever', 'laid', 'eyes', 'upon']
'''

     本次在文件夹email中包含两个子文件夹,分别是spam和ham。我们可以通过读取文本文件,并根据自己的需求做正则分切文本(本文只做最简单的切分)。

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

     下面我们构建文件解析及完整的垃圾邮件测试函数:

'''
@function:切分分本为字符串列表
@param: 
    bigString [string] 长本文
@return: 
    [list] 切分后的文本
'''
def textParse(bigString):
    import re
    listOfTokens = re.split('\\W*', bigString) #按非数字字母切换
    return [tok.lower() for tok in listOfTokens if len(tok) > 2] #词条长度>2,且用小写表示

'''
@function:文件解析和完整的垃圾邮件测试函数
@param: 
    bigString [string] 长本文
@return: 测试样本的错误率
'''
def spamTest():
    docList = []; classList = []; fullList = []
    for i in range(1,26):   #spam和ham样本分别为25
        wordList = textParse(open('./email/spam/%d.txt' % i).read())   #解析垃圾邮件
        docList.append(wordList)
        fullList.extend(wordList)
        classList.append(1)
        wordList = textParse(open('./email/ham/%d.txt' % i).read())  # 解析非邮件
        docList.append(wordList)
        fullList.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList)    #创建词汇列表
    trainingSet = list(range(50)); testSet = [] #创建训练数据和测试数据,测试数据为10个
    for i in range(10):
        randIndex = int(np.random.uniform(0, len(trainingSet))) #随机取索引
        testSet.append(trainingSet[randIndex])  #测试集添加索引
        del(trainingSet[randIndex]) #删除训练集中的相应值
    trainMat = []; trainClasses = [] #创建训练矩阵和标签
    for docIndex in trainingSet:
        trainMat.append(bagOfWords2Vec(vocabList, docList[docIndex]))   #将邮件词条转化为词条向量
        trainClasses.append(classList[docIndex])  #添加邮件类型输出标签
    p0V,p1V,pSam = trainNB(np.array(trainMat), np.array(trainClasses)) #朴素贝叶斯训练函数
    errorCount = 0
    for docIndex in testSet:    #对测试集进行测试
        wordVector = bagOfWords2Vec(vocabList, docList[docIndex])
        if classifyNB(np.array(wordVector), p0V, p1V, pSam) != classList[docIndex]: #判断朴素贝叶斯分类的正确性
            errorCount += 1
            print("classification error", docList[docIndex])
    print('the error rate is: ', float(errorCount)/len(testSet))

if __name__ == '__main__':
    start = time.clock()
    spamTest()
    end = time.clock()
    print('Finish in', end - start)

'''输出结果
classification error ['home', 'based', 'business', 'opportunity', 'knocking', 'your', 'door', 'don抰', 'rude', 'and', 'let', 'this', 'chance', 'you', 'can', 'earn', 'great', 'income', 'and', 'find', 'your', 'financial', 'life', 'transformed', 'learn', 'more', 'here', 'your', 'success', 'work', 'from', 'home', 'finder', 'experts']
the error rate is:  0.1
Finish in 0.0435192469922442
'''

     函数spamTest()会输出在10封随机选择的电子邮件上的分类错误率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。如果发现错误的话,函数会输出错分文档的词表,这样就可以了解到底是哪篇文档发生了错误。如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如10次,然后求评价值。当然,这里一直出现的错误是将垃圾邮件误判为正常邮件,相比之下,将垃圾邮件误判为正常邮件比将正常邮件归到垃圾邮件要好。到目前为止,我们已经使用朴素贝叶斯对文档进行分类。

四、示例:使用朴素贝叶斯分类器从个人广告中获取区域倾向

     在这个例子当中,我们将分别从美国的两个城市选取一些政治事件(原文是征婚广告,而原链接地址已经无效),通过分析政治事件来比较来比较这两个城市的政治事件是否不同。如果结论确实是不同,那么它们各自的词是哪些?这写词是否对不同城市的人所关心的内容有所了解。下面是使用朴素贝叶斯来发现地域相关用词的框架流程:

  • (1)收集数据:从RSS源收集内容,这里需要对RSS源构建一个接口。
  • (2)准备数据:将本文文件解析成词条向量
  • (3)分析数据:检查词条确保解析的正确性
  • (4)训练数据:使用之前的训练函数trainNB()计算不同的独立特征的条件概率
  • (5)测试数据:观察错误率,确保分类器可用。可以修改切分程序,以降低错误率,提高分类结果
  • (6)使用算法:构建一个完整的程序,封装所有的内容。给定两个RSS源,该程序会显示最常用的公共词
4.1 收集数据:导入RSS源

     下面使用feedParser程序库来收集Craigslist上的广告事件。首先,我们构建calcMostReq()函数来过滤频率最高的前30个词。然后使用localWords()函数,以两个RSS源为参数,来进行朴素贝叶斯分类。

'''
@function:计算词频出现最高的前30个词
@param: 
    vocabList [list] 词汇表
    fullText [list] 统计的文本
@return: 
    sortedFreq [list] 词频最高的前30个词
'''
def calcMostFreq(vocabList, fullText):
    import operator
    freqDict = {}   #定义字典
    for token in vocabList:
        freqDict[token] = fullText.count(token) #字典存放个词条出现的个数
    sortedFreq = sorted(freqDict.items(), key=operator.itemgetter(1), reverse=True) #对字典按值进行从大到小排序
    return sortedFreq[:30] #返回词频最高的前30个单词

'''
@function:RSS分类器测试函数
@param: 
    feed1 RSS源
    feed0 RSS源
@return: 
    vocabList [list] 词频最高的前30个词
    p0V [list]  出现0的条件概率数组 
    p1V [list]  出现1的条件概率数组
'''
def localWords(feed1, feed0):
    import feedparser
    docList = []; classList = []; fullText = []
    minLen = min(len(feed1['entries']), len(feed0['entries'])) #取词源中最小的
    for i in range(minLen):
        wordList = textParse(feed1['entries'][i]['summary'])
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1)
        wordList = textParse(feed0['entries'][i]['summary'])
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList) #构建词汇表
    top30Words = calcMostFreq(vocabList, fullText) #词频最高的前30个词
    for pairW in top30Words:
        if pairW in vocabList: vocabList.remove(pairW) #移除词频最高的前30个词
    trainingSet = list(range(2*minLen)); testSet=[] #构建训练集和测试集
    for i in range(20): #20个作为测试集
        randIndex = int(np.random.uniform(0, len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(trainingSet[randIndex])
    trainMat = []; trainClasses = [] #训练矩阵和标签
    for docIndex in trainingSet:
        trainMat.append(bagOfWords2Vec(vocabList, docList[docIndex]))   #将词源转换为词向量
        trainClasses.append(classList[docIndex])    #词向量对应的标签
    p0V,p1V,pSpam = trainNB(np.array(trainMat), np.array(trainClasses))
    errorCount = 0
    for docIndex in testSet: #测试集错误率计算
        wordVector = bagOfWords2Vec(vocabList, docList[docIndex])
        if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
            errorCount += 1
    print('the error rate is ', float(errorCount)/len(testSet))
    return vocabList, p0V, p1V

if __name__ == '__main__':
    start = time.clock()
    import feedparser
    ny = feedparser.parse('https://newyork.craigslist.org/search/pol?format=rss')
    sf = feedparser.parse('https://sfbay.craigslist.org/search/pol?format=rss')
    vocabList, pSF, pNY = localWords(ny, sf)
    vocabList, pSF, pNY = localWords(ny, sf)
    end = time.clock()
    print('Finish in', end - start)

'''输出结果
the error rate is  0.4
the error rate is  0.15
Finish in 5.16733689904591
'''

     我们现在已经完成RSS源分类器及高频词去除函数,我们将前30个词进行去除原因是这30个词约涵盖文本中的大部分内容,而这些词大部分都是冗余和结构辅助性内容(我们也可以使用停用词表来预订词表中移除结构上的辅助词)。在RSS源的选取,使用纽约和三藩市两个城市的Craigslist事件。

4.2 分析数据:显示地域相关的用词

     现在我们可以对输出的pSF,pNY进行排序,就可以返回大于某个阈值的所有词,然后显示分析最具表特征性的词汇。

'''
@function:计算词频出现最高的前30个词
@param: 
    ny 纽约RSS源
    sf 三藩市RSS源
@return: 按从高到低进行排序
'''
def getToWords(ny, sf):
    import operator
    vocabList,p0V,p1V = localWords(ny, sf)
    topNY = []; topSF = [] #构建列表
    for i in range(len(p0V)):
        if p0V[i] > -5.0: topSF.append((vocabList[i], p0V[i])) #对数概率>-6.0添加至列表
        if p1V[i] > -5.0: topNY.append((vocabList[i], p1V[i]))
    sortedSF = sorted(topSF, key=lambda pair:pair[1], reverse=True)    #对列表元组概率从大到小排序
    print('SF**SF**SF**SF**SF**SF**SF**SF**SF**SF')
    for item in sortedSF: print(item[0])
    sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)
    print('NY**NY**NY**NY**NY**NY**NY**NY**NY**NY')
    for item in sortedNY: print(item[0])

if __name__ == '__main__':
    start = time.clock()
    import feedparser
    ny = feedparser.parse('https://newyork.craigslist.org/search/pol?format=rss')
    sf = feedparser.parse('https://sfbay.craigslist.org/search/pol?format=rss')
    getToWords(ny,sf)
    end = time.clock()
    print('Finish in', end - start)

'''输出结果
the error rate is  0.45
SF**SF**SF**SF**SF**SF**SF**SF**SF**SF
liberal
...
app
help
with
law
want
since
america
NY**NY**NY**NY**NY**NY**NY**NY**NY**NY
paid
time
wmq
participants
study
corrupt
00pm
midterms
political
Finish in 5.349823010491469
'''

     通过计算可以输出两个区域相应的词。当然从输入词条可以看出,如果能移除固定的停用词结果应该会十分有趣。至此,我们已经完成了朴素贝叶斯法的几个示例。

四、小结

     对于分类来说,使用概率有时要比使用硬规则更为有效。而贝叶斯概率及贝叶斯准则提供了一种利用已知值来估计未知概率的有效方法。
     朴素贝叶斯是通过特征之间的条件独立性假设,降低对数据量的需求。当然我们知道这个假设过于简单,所以我们称为朴素贝叶斯,但是朴素贝叶斯仍然是一种有效的分类器。在使用朴素贝叶斯时,我们要考虑条件概率(似然函数)的溢出现象,通过我们通过取自然对数来解决。而对于某个特征的条件概率为0的情况,我们通过用拉普拉斯平滑来解决。词袋模型在解决文档分类问题上比词集模型有所提高,同时,移除停用词,也可以对切分器进行优化。

【参考】:
     1. 《机器学习实战》作者:Peter Harrington   第4章 基于概率论的分类方法:朴素贝叶斯
     2. 《统计学习方法》作者:李航   第4章 朴素贝叶斯
     3. 《机器学习》作者:周志华


转载声明:
版权声明:非商用自由转载-保持署名-注明出处
署名 :mcyJacky
文章出处:https://blog.csdn.net/mcyJacky

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值