Python《机器学习实战》读书笔记(四)——朴素贝叶斯

  • 第四章 基于概率论的分类方法朴素贝叶斯
    • 4-1 基于贝叶斯决策理论的分类方法
    • 4-2 条件概率
    • 4-3 使用条件概率来分类
    • 4-4 使用朴素贝叶斯进行文档分类
    • 4-5 使用Python进行文本分类
      • 4-5-1 准备数据从文本中构建词向量
      • 4-5-2 训练算法从词向量计算概率
      • 4-5-3 测试算法根据现实情况修改分类器
      • 4-5-4 准备数据文档词袋模型
    • 4-6 示例使用朴素贝叶斯过滤垃圾邮件
      • 4-6-1 准备数据切分文本
      • 4-6-2 测试算法使用朴素贝叶斯进行交叉验证
    • 4-7 示例使用朴素贝叶斯分类器从个人广告中获取区域倾向
      • 4-7-1 收集数据导入RSS源
      • 4-7-2 分析数据显示地域相关的用词
    • 4-8 本章小结
    • 4-9 参考文献

第四章 基于概率论的分类方法:朴素贝叶斯

4-1 基于贝叶斯决策理论的分类方法

优点:在数据较小的情况下仍然有效,可以处理多类别问题
缺点:对于输入数据的准备方式较为敏感。
适用数据类型:标称型数据。

假设现在我们有一个数据集,它由两类数据组成,数据分布如下图所示:
Python《机器学习实战》读书笔记(四)——朴素贝叶斯_第1张图片

假设有位读者找到了描述途中两类数据的统计参数。我们现在用p1(x,y)表示数据点(x,y)属于类别1(途中用圆点表示的类别)的概率,用p2(x,y)表示数据点(x,y)属于类别2(途中用三角形表示的类别)的概率,那么对于一个新数据点,可以用下面的规则来判断他的类别:

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

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

4-2 条件概率

假设现在有一个装了7块石头的罐子,其中3块是灰色的,4块是黑色的。如下图所示:
Python《机器学习实战》读书笔记(四)——朴素贝叶斯_第2张图片

由于取石头有7种可能,漆黑中3种为灰色,所以取出灰色石头的概率为3/7。显然,取到黑色石头的概率是4/7。使用P(gray)来表示渠道灰色石头的概率,其概率值可以通过灰色石头数目除以总的石头数目来得到。

如果7块石头如下图放在两个桶中,上诉概率应如何计算?
Python《机器学习实战》读书笔记(四)——朴素贝叶斯_第3张图片

要计算P(gray)或者P(black),事先得知道石头所在桶的信息会不会改变结果?你有可能已经想到计算从B桶中取到灰色石头的概率的方法,这就是所谓的条件概率(conditional probability)。假定计算的是从B桶取到灰色石头的概率,这个概率可以记作P(gray|bucketB),我们称之为“在已知石头出自B桶的条件下,取出灰色石头的概率”。不难得到,P(gray|bucketA)值为2/4,P(gray|bucketB)的值为1/3。

条件概率的计算公式如下所示:
条件概率的计算公式

现在来看看上述公式是否合理。首先,用B桶中灰色石头的个数除以两个桶中总的石头数,得到P(gray and bucketB) = 1/7。其次,由于B桶中有3块石头,而总石头数为7,于是P(bucketB)就等于3/7。由于P(gray|bucketB) = P(gray and bucketB)/P(bucketB) = (1/7)/(3/7) = 1/3。

另一种有效计算条件概率的方法称为贝叶斯准则。如果已知P(x|c),要求P(c|x),那么可以使用下面的计算方法:
贝叶斯准则计算条件概率

4-3 使用条件概率来分类

4-1提到贝叶斯决策理论要求计算两个概率p1(x,y)和p2(x,y):

  • -如果p1(x,y) > p2(x,y),那么属于类别1;
  • 如果p2(x,y) > p1(x,y),那么属于类别2。

但是这两个准则并不是贝叶斯决策理论的所有内容。使用p()和p()只是为了尽可能简化描述,而真正需要计算和比较的是p(c1|x,y)和p(c2|x,y)。

这些符号所代表的具体意义是:给定某个由x、y表示的数据点,那么该数据点来自类别c1的概率是多少?数据点来自类别c2的概率又是多少?这些概率与刚才给出的概率p(x,y|c2)并不一样,不过可以使用贝叶斯准测来交换概率中条件与结果。具体地,应用贝叶斯准则得到:
Python《机器学习实战》读书笔记(四)——朴素贝叶斯_第4张图片

使用这些定义,可以定义贝叶斯分类准则为:

  • -如果p1(c1|x,y) > p2(c2|x,y),那么属于类别1;
  • 如果p1(c1|x,y) < p2(c2|x,y),那么属于类别2。

使用贝叶斯准则,可以通过已知的三个概率值来计算未知的概率值。

4-4 使用朴素贝叶斯进行文档分类

朴素贝叶斯的一般过程:

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

由统计学知,如果每个特征需要N个样本,那么对于10个特征将需要 N10 个样本,对于包含1000个特征的词汇表将需要 N1000 个样本。可以看出,所需要的样本数会随着特征数目增大而迅速增长。

如果特征之间相互独立,那么样本数就可以从 N1000 减少到 1000N 。所谓独立(independence)指的是统计意义上的独立,即一个特征或者单词出现的可能性与它和其他单词相邻没有关系。举个例子说:假设单词bacon出现在unhealthy后面与出现在delicious后面的概率相同。当然,我们知道这种假设并不正确,bacon常常出现在delicious附近,而很少出现在unhealthy附近,这个假设正式朴素贝叶斯分类器中朴素(native)依次的含义。

朴素贝叶斯分类器中的另一个假设是,每个特征同等重要。其实这个假设也有问题。尽管上述假设存在一些小的瑕疵,但朴素贝叶斯的实际效率却很好。

4-5 使用Python进行文本分类

要从文本中获取特征,需要先拆分文本。

这里的特征是来自文本的词条(token),一个词条是字符的任意组合。可以把此条想象为单词,也可以使用非单词词条,如URL、IP地址或者任意其它字符串。然后将每一个文本片段表示为一个词条向量,其中值为1表示词条出现的文档中,0表示词条未出现。

以在线社区的留言板为例。为了不影响社区的发展,我们需要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言表示为内容不当。对此问题建立两个类别:侮辱类非侮辱类,使用10分别表示。

4-5-1 准备数据:从文本中构建词向量

我们将文本看成单词向量或者词条向量,也就是说将句子转换为向量。考虑出现在所有文档中的所有单词,再决定将哪些词纳入词汇表或者说所要的词汇集合,然后必须将每一篇文档转换为词汇表上的向量。

打开文本编辑器,创建bayes.py的新文件,并输入下列程序。

"""
    Function:
        创建一些实验样本
    Parameters:
        None
    Return:
        postingList——进行词条切分后的文档集合
        classVec——类别标签的集合
    Modify:
        2017-12-25    
"""
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:
        创建一个包含所有文档中出现的不重复的列表
    Parameters:
        dataSet—— 输入数据
    Return:
        list(vocabSet)——包含所有文档中出现的不重复的列表
    Modify:
        2017-12-25    
"""
def createVocabList(dataSet):
    vocabSet = set([])  
    #创建一个空集
    for document in dataSet:
        vocabSet = vocabSet | set(document) 
        #创建两个集合的并集
    return list(vocabSet)

"""
    Function:
        输出词汇表中单词在输入文档中是否出现,向量的每个元素为1(出现)或0(未出现)
    Parameters:
        vocabList—— 词汇表
        inputSet—— 文档向量
    Return:
        returnVec——文档向量
    Modify:
        2017-12-25    
"""
def setOfWords2Vec(vocabList, inputSet):
    returnVec = [0]*len(vocabList) 
    #创建一个其中所有元素都为0的向量
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        else: print "the word: %s is not in my Vocabulary!" % word
    return returnVec

第一个函数loadDataSet()中返回的第二个变量是一个类别标签的集合,这里分为侮辱性和非侮辱性两类。这些类别标签由人工标注,这些标注信息用于训练程序以便自动检测侮辱性留言。

现在在Python提示符下输入下列代码,看一下这些函数的执行效果:

>>> import sys
>>> sys.path.append('E:\Python_Files\CodeofMe\Chapter4')
>>> import bayes
>>> listOposts,listClasses = bayes.loadDataSet()

>>> myVocabList = bayes.createVocabList(listOposts)
>>> myVocabList
['cute', 'love', 'help', 'garbage', 'quit', 'I', 'problems', 'is', 'park', 'stop', 'flea', 'dalmation', 'licks', 'food', 'not', 'him', 'buying', 'posting', 'has', 'worthless', 'ate', 'to', 'maybe', 'please', 'dog', 'how', 'stupid', 'so', 'take', 'mr', 'steak', 'my']

检查上述词表,发现这里不会出现重复的单词。

>>> bayes.setOfWords2Vec(myVocabList,listOposts[0])
[0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1]
>>> bayes.setOfWords2Vec(myVocabList,listOposts[3])
[0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
>>> 

上述代码是setOfWords2Vec()的运行效果。

>>> listOposts[0]
['my', 'dog', 'has', 'flea', 'problems', 'help', 'please']
>>> listOposts[3]
['stop', 'posting', 'stupid', 'worthless', 'garbage']`

listOposts[0]与listOposts[3]
与词表进行对比,会发现setOfWords2Vec()输出结果无误。

4-5-2 训练算法:从词向量计算概率

p(c1|w)=p(c1|w)p(ci)p(w)

我们将用到上诉公式,对每个类计算该值,然后比较这两个概率值的大小。

如何计算呢?首先可以通过类别i(侮辱性留言或非侮辱性留言)中文档数除以总的文档数来计算概率p(c1)。接下来计算p(w|c1),这里就要用到朴素贝叶斯假设。如果将w展开为一个个独立特征,那么就可以将上诉概率写作p(w0,w1,w2…wn|ci)。这里假设所有词都互相独立,该假设也称作条件独立性假设,它意味着可以使用p(w0|c1)p(w1|c1)p(w2|c2)…p(wn|ci)来计算上诉概率,这就极大地简化了计算的过程。

该函数的伪代码如下:

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

利用下面代码实现上诉伪代码:

"""
    Function:
        朴素贝叶斯分类器训练函数        
    Parameters:
        trainMatrix—— 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
        trainCategory—— 训练类别标签向量,即loadDataSet返回的classVec
    Return:
        p0Vect——侮辱类的条件概率数组
        p1Vect——非侮辱类的条件概率数组
        pAbusive——文档属于侮辱类的概率
    Modify:
        2017-12-28    
"""
def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)
    #计算训练的文档数目    
    numWords = len(trainMatrix[0])
    #计算每篇文档的词条数    
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    #文档属于侮辱类的概率    
    p0Num = zeros(numWords); p1Num = zeros(numWords)      
    #change to ones()
    #创建numpy.zeros数组,词条出现数初始化为0 
    p0Denom = 0.0; p1Denom = 0.0                        
    #change to 2.0
    #分母初始化为0.0
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:
        #统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|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 = p1Num/p1Denom
    p0Vect = p0Num/p0Denom 
    #p1Vect = log(p1Num/p1Denom)          
    #change to log()
    #p0Vect = log(p0Num/p0Denom)          
    #change to log()
    return p0Vect,p1Vect,pAbusive
    #返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率

该函数使用了Numpy的一些函数,故应确保将

from numpy import *

语句添加到bayes.py文件的最前面。

最后函数返回两个向量和一个概率。

在Python提示符下输入下列代码看一下效果:

>>> import sys
>>> sys.path.append('E:\Python_Files\CodeofMe\Chapter4')
>>> import bayes
>>> from numpy import *
>>> list0Posts,listClasses = bayes.loadDataSet()

该语句从预先加载值中调入数据。

>>> myVocabList = bayes.createVocabList(list0Posts)

至此我们构建了一个包含所有次的列表myVocabList :

>>> myVocabList
['cute', 'love', 'help', 'garbage', 'quit', 'I', 'problems', 'is', 'park', 'stop', 'flea', 'dalmation', 'licks', 'food', 'not', 'him', 'buying', 'posting', 'has', 'worthless', 'ate', 'to', 'maybe', 'please', 'dog', 'how', 'stupid', 'so', 'take', 'mr', 'steak', 'my']

for循环使用词向量来填充trainMat列表:

>>> trainMat = []
>>> for postinDoc in list0Posts:
    trainMat.append(bayes.setOfWords2Vec(myVocabList,postinDoc))

下面给出属于侮辱性性文档的概率以及两个类别的概率向量:

>>> p0V,p1V,pAb = bayes.trainNB0(trainMat,listClasses)

这些变量的内部值:

>>> p0V
array([ 0.04166667,  0.04166667,  0.04166667,  0.        ,  0.        ,
        0.04166667,  0.04166667,  0.04166667,  0.        ,  0.04166667,
        0.04166667,  0.04166667,  0.04166667,  0.        ,  0.        ,
        0.08333333,  0.        ,  0.        ,  0.04166667,  0.        ,
        0.04166667,  0.04166667,  0.        ,  0.04166667,  0.04166667,
        0.04166667,  0.        ,  0.04166667,  0.        ,  0.04166667,
        0.04166667,  0.125     ])
>>> p1V
array([ 0.        ,  0.        ,  0.        ,  0.05263158,  0.05263158,
        0.        ,  0.        ,  0.        ,  0.05263158,  0.05263158,
        0.        ,  0.        ,  0.        ,  0.05263158,  0.05263158,
        0.05263158,  0.05263158,  0.05263158,  0.        ,  0.10526316,
        0.        ,  0.05263158,  0.05263158,  0.        ,  0.10526316,
        0.        ,  0.15789474,  0.        ,  0.05263158,  0.        ,
        0.        ,  0.        ])
>>> pAb
0.5

属于侮辱类的概率pAb为0.5,从我们自己建立的实验样本来看,6个文档,3个是侮辱性的,所以该值是正确的。

 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代表正常言论 

接下来,看一看在给定文档类别条件下词汇表中单词的出现概率,看看是否正确。词汇表中的第一词是cute:

>>> myVocabList
['cute', 'love', 'help', 'garbage', 'quit', 'I', 'problems', 'is', 'park', 'stop', 'flea', 'dalmation', 'licks', 'food', 'not', 'him', 'buying', 'posting', 'has', 'worthless', 'ate', 'to', 'maybe', 'please', 'dog', 'how', 'stupid', 'so', 'take', 'mr', 'steak', 'my']

其在类别0中出现1次,而在类别1中从未出现。对应的条件概率分别为0.04166667与0.0。该计算值是正确的。我们找找所有概率中的最大值,该值出现在P(1)数组里,大小为0.15789474。在myVocabList的第26个下标位置上可以查到该单词是stupid。这意味着stupid是最能表征类别1(侮辱性文档类)的单词。

4-5-3 测试算法:根据现实情况修改分类器

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

在bayes.py文件的trainNB0()函数中的:

    p0Num = zeros(numWords); p1Num = zeros(numWords)      
    p0Denom = 0.0; p1Denom = 0.0 

改为:

p0Num = one(numWords); p1Num = one(numWords)      
p0Denom = 2.0; p1Denom = 2.0 

另一个问题是下溢出,这是由于太多很小的数相乘造成的。

解决方法:对乘积取自然对数。

在代数中有

ln(ab)=ln(a)+ln(b)
,于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。

如下图,是f(x)与ln(f(x))的曲线:

Python《机器学习实战》读书笔记(四)——朴素贝叶斯_第5张图片

检查两条曲线,发现他们在相同区域内同时增加或者减少,并且在相同点上取到极值。虽然取值不同,但不影响最终结果。

通过修改return前的两行代码,将上诉做法用到分类器中:

    p1Vect = log(p1Num/p1Denom)          
    #change to log()
    p0Vect = log(p0Num/p0Denom)          
    #change to log()

下面利用朴素贝叶斯分类器进行分类测试,在bayes.py添加下列代码:

"""
    Function:
        朴素贝叶斯分类函数      
    Parameters:
        vec2Classify—— 待分类的向量
        p0Vec——非侮辱类的条件概率数组
        p1Vec——侮辱类的条件概率数组
        pClass1——文档属于侮辱类的概率
    Return:
        1或0——侮辱性或者非侮辱性类别
    Modify:
        2017-12-29    
"""
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)  
    #元素相乘 这里的 vec2Classify * p1Vec相乘是指矩阵里对应的元素相乘
    #element-wise mult
    p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
    #元素相乘 这里的 vec2Classify * p0Vec相乘是指矩阵里对应的元素相乘 
    if p1 > p0:
        return 1
        #返回文档属于侮辱性类别
    else: 
        return 0
        #返回文档属于非侮辱性类别
"""
    Function:
        朴素贝叶斯分类器测试函数        
    Parameters:
        None
    Return:
        Noee
    Modify:
        2017-12-29   
"""
def testingNB():
    listOPosts,listClasses = loadDataSet()
    #获取文档集合和文档类别标签的集合
    myVocabList = createVocabList(listOPosts)
    #得到所有文档中出现的词且不重复的列表
    trainMat=[]
    #建立一个空集用来存储元素为0(未出现)或1(出现)的文档向量(该文档向量每一个文档含有所有训练集的词汇)
    for postinDoc in listOPosts:
    #根据得到的训练集词汇列表,对每个文档的词汇进行检测,并将输出与词汇列表大小一样的标称型文档向量
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
        #得到训练集文档的标称型文档向量
    p0V,p1V,pAb = trainNB0(array(trainMat),array(listClasses))
    #得到训练集的非侮辱类的条件概率数组、侮辱类的条件概率数组、文档属于侮辱类的概率
    testEntry = ['love', 'my', 'dalmation']
    #待测试的文档集合
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    #将待测试的文档集合转换成标称型文档向量并转换成矩阵形式方便后面计算
    print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)
    #对测试文档进行分类测试并输出测试结果
    testEntry = ['stupid', 'garbage']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)

代码中的第二个函数是一个便利函数(convenience function),该函数封装所有测试操作。

下面在Python提示符下输入下列代码,看看实际测试结果:

>>> reload(bayes)
'bayes' from 'E:\Python_Files\CodeofMe\Chapter4\bayes.py'>
>>> bayes.testingNB()
['love', 'my', 'dalmation'] classified as:  0
['stupid', 'garbage'] classified as:  1

4-5-4 准备数据:文档词袋模型

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

"""
    Function:
        词袋模型:每个单词可以出现多次
    Parameters:
        vocabList—— 词汇表
        inputSet—— 文档向量
    Return:
        returnVec——包含词汇出现次数信息的文档向量
    Modify:
        2017-12-29    
"""
def bagOfWords2VecMN(vocabList, inputSet):
    returnVec = [0]*len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return returnVec

该函数与setOfWords2Vec()几乎完全相同,唯一不同的是每当遇到一个单词时,它会增加词向量中的对应值,而不只是将对应的数值设为1。

4-6 示例:使用朴素贝叶斯过滤垃圾邮件

下面这个例子,是朴素贝叶斯的一个最著名的应用:电子邮箱垃圾过滤

首先看一下如何使用通用框架来解决该问题:

示例:使用朴素贝叶斯对电子邮件进行分类

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

4-6-1 准备数据:切分文本

前面我们通过自己创建词向量,并基于这些词向量进行朴素贝叶斯分类的过程。前面的词向量使我们预先给订的,现在看看如何从文本文档中构建自己的词列表。

在Python提示符下输入下列代码:

>>> mySent = 'This book is the best book on Python or M.L. I have ever laid eyes upon.'
>>> mySent.split()
['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python', 'or', 'M.L.', 'I', 'have', 'ever', 'laid', 'eyes', 'upon.']

上诉代码是使用了Python的String.split()方法将其切分,其切分的结果不错,但是标点符号也被当成了词的一部分。

可以使用正则表示式来切分句子,其中分隔符是除单词、数字外的任意字符串。

>>> import re
>>> regEx = re.compile('\\W*')
>>> listOfTokens = regEx.split(mySent)
>>> listOfTokens
['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python', 'or', 'M', 'L', 'I', 'have', 'ever', 'laid', 'eyes', 'upon', '']

现在得到了一系列词组成的词表,但是里面的空字符串需要去掉。可以计算每个字符串的长度只返回长度大于0的字符串。

>>> [tok for tok in listOfTokens if len(tok)>0]
['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python', 'or', 'M', 'L', 'I', 'have', 'ever', 'laid', 'eyes', 'upon']

现在来看数据集中一封完整的电子邮件的实际处理结果。

>>> emailText = open(r'E:\Python_Files\CodeofMe\Chapter4\email\ham\6.txt').read()
>>> emailText
'Hello,\n\nSince you are an owner of at least one Google Groups group that uses the customized welcome message, pages or files, we are writing to inform you that we will no longer be supporting these features starting February 2011. We made this decision so that we can focus on improving the core functionalities of Google Groups -- mailing lists and forum discussions.  Instead of these features, we encourage you to use products that are designed specifically for file storage and page creation, such as Google Docs and Google Sites.\n\nFor example, you can easily create your pages on Google Sites and share the site (http://www.google.com/support/sites/bin/answer.py?hl=en&answer=174623) with the members of your group. You can also store your files on the site by attaching files to pages (http://www.google.com/support/sites/bin/answer.py?hl=en&answer=90563) on the site. If you\x92re just looking for a place to upload your files so that your group members can download them, we suggest you try Google Docs. You can upload files (http://docs.google.com/support/bin/answer.py?hl=en&answer=50092) and share access with either a group (http://docs.google.com/support/bin/answer.py?hl=en&answer=66343) or an individual (http://docs.google.com/support/bin/answer.py?hl=en&answer=86152), assigning either edit or download only access to the files.\n\nyou have received this mandatory email service announcement to update you about important changes to Google Groups.'
>>> listOfTokens = regEx.split(emailText)
>>> listOfTokens
['Hello', 'Since', 'you', 'are', 'an', 'owner', 'of', 'at', 'least', 'one', 'Google', 'Groups', 'group', 'that', 'uses', 'the', 'customized', 'welcome', 'message', 'pages', 'or', 'files', 'we', 'are', 'writing', 'to', 'inform', 'you', 'that', 'we', 'will', 'no', 'longer', 'be', 'supporting', 'these', 'features', 'starting', 'February', '2011', 'We', 'made', 'this', 'decision', 'so', 'that', 'we', 'can', 'focus', 'on', 'improving', 'the', 'core', 'functionalities', 'of', 'Google', 'Groups', 'mailing', 'lists', 'and', 'forum', 'discussions', 'Instead', 'of', 'these', 'features', 'we', 'encourage', 'you', 'to', 'use', 'products', 'that', 'are', 'designed', 'specifically', 'for', 'file', 'storage', 'and', 'page', 'creation', 'such', 'as', 'Google', 'Docs', 'and', 'Google', 'Sites', 'For', 'example', 'you', 'can', 'easily', 'create', 'your', 'pages', 'on', 'Google', 'Sites', 'and', 'share', 'the', 'site', 'http', 'www', 'google', 'com', 'support', 'sites', 'bin', 'answer', 'py', 'hl', 'en', 'answer', '174623', 'with', 'the', 'members', 'of', 'your', 'group', 'You', 'can', 'also', 'store', 'your', 'files', 'on', 'the', 'site', 'by', 'attaching', 'files', 'to', 'pages', 'http', 'www', 'google', 'com', 'support', 'sites', 'bin', 'answer', 'py', 'hl', 'en', 'answer', '90563', 'on', 'the', 'site', 'If', 'you', 're', 'just', 'looking', 'for', 'a', 'place', 'to', 'upload', 'your', 'files', 'so', 'that', 'your', 'group', 'members', 'can', 'download', 'them', 'we', 'suggest', 'you', 'try', 'Google', 'Docs', 'You', 'can', 'upload', 'files', 'http', 'docs', 'google', 'com', 'support', 'bin', 'answer', 'py', 'hl', 'en', 'answer', '50092', 'and', 'share', 'access', 'with', 'either', 'a', 'group', 'http', 'docs', 'google', 'com', 'support', 'bin', 'answer', 'py', 'hl', 'en', 'answer', '66343', 'or', 'an', 'individual', 'http', 'docs', 'google', 'com', 'support', 'bin', 'answer', 'py', 'hl', 'en', 'answer', '86152', 'assigning', 'either', 'edit', 'or', 'download', 'only', 'access', 'to', 'the', 'files', 'you', 'have', 'received', 'this', 'mandatory', 'email', 'service', 'announcement', 'to', 'update', 'you', 'about', 'important', 'changes', 'to', 'Google', 'Groups', '']
>>> 

该6.txt文件非常长,这是某公司告知他们不再进行某些支持的一些邮件。

4-6-2 测试算法:使用朴素贝叶斯进行交叉验证

将文本解析器集成到一个完成分类器中。在bayes.py文件中添加下列代码。

"""
    Function:
        文件解析以达到切分文本生成词列表
    Parameters:
        bigString——文本中的字符串
    Return:
        [tok.lower() for tok in listOfTokens if len(tok) > 2]—— 词汇列表
    Modify:
        2017-12-29    
"""
def textParse(bigString):    
#input is big string, 
#output is word list
    import re
    listOfTokens = re.split(r'\W*', bigString)
    #切分文本字符串生成词汇列表
    return [tok.lower() for tok in listOfTokens if len(tok) > 2] 
    #返回字符串长度大于2的词汇

"""
    Function:
        垃圾邮箱测试函数
    Parameters:
        None
    Return:
        None
    Modify:
        2017-12-29    
"""
def spamTest():
    docList=[]; classList = []; fullText =[]
    #docList每封邮件的词汇作为元素的列表、classList人为设定的邮件的类别(垃圾邮件或非垃圾邮件)、fullText所有邮件的词汇列表,一个词作为一个元素
    for i in range(1,26):
    #对25封垃圾邮件和非垃圾邮件进行导入并解析文本文件
        wordList = textParse(open(r'E:\Python_Files\CodeofMe\Chapter4\email\spam\%d.txt' % i).read())
        #读取垃圾邮件并将其生成词列表
        docList.append(wordList)
        #垃圾邮件词列表作为一个元素存放在docList文档列表中
        fullText.extend(wordList)
        #将垃圾邮件里的词汇全部存放在fullText列表中,一个词汇作为一个元素
        classList.append(1)
        #用1标记垃圾邮件存放在classList列表中
        wordList = textParse(open(r'E:\Python_Files\CodeofMe\Chapter4\email\ham\%d.txt' % i).read())
        #读取非垃圾邮件并将其生成词列表
        docList.append(wordList)
        #非垃圾邮件词列表作为一个元素存放在docList文档列表中        
        fullText.extend(wordList)
        #将非垃圾邮件里的词汇全部存放在fullText列表中,一个词汇作为一个元素
        classList.append(0)
        #用0标记垃圾邮件存放在classList列表中
        #这儿采用交替标记垃圾和非垃圾邮件,即classList=[1,0,1,0...]
    vocabList = createVocabList(docList)
    #create vocabulary 建立词典(该词典包括了25封垃圾和25封非垃圾邮件的所有长度大于2的词汇) 词典里的词不重复
    trainingSet = range(50)
    #设置训练长度(共有50封邮件)
    testSet=[]           
    #create test set 创建测试集
    for i in range(10):
    #随机构建训练集
        randIndex = int(random.uniform(0,len(trainingSet)))
        #生成0到50之间的随机数,并将其转换成整型作为索引号
        testSet.append(trainingSet[randIndex])
        #将生成的随机整数存放在testSet列表中
        del(trainingSet[randIndex])
        #删除 trainingSet列表中对应随机生成的索引号的值从而得到训练集索引号
    trainMat=[]; trainClasses = []
    #建立trainMat空集用来存储文档向量,建立trainClasses空集用来存储邮件分类向量
    for docIndex in trainingSet:
    #train the classifier (get probs) trainNB0 训练分类器
        trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
        #得到针对词汇出现次数训练集计数的文档向量
        trainClasses.append(classList[docIndex])
        #得到训练集文档对应的类别向量标签
    p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
    #根据训练集得到非垃圾类的条件概率数组、垃圾类的条件概率数组、文档属于垃圾类的概率
    errorCount = 0
    #错误计数初始化
    for docIndex in testSet:        
    #classify the remaining items 分类器测试并测试错误率
        wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
        #得到关于测试组词汇出现次数的文档向量
        if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
        #检测分类器分类的结果是否与测试集实际的类别一样
            errorCount += 1
            #不一样,错误计数加1
            print "classification error",docList[docIndex]
            #输出分类错误的文档
    print 'the error rate is: ',float(errorCount)/len(testSet)
    #输出分类器的错误率

第一个函数textParse()接受一个大字符串并将起解析为字符串列表。该函数去掉少于两个字符的字符串,并将所有字符串转换为小写。

第二个函数spamTest()对贝叶斯垃圾邮件分类系进行自动化处理。

导入文件夹spam和ham下的文本文件,并将他们解析为词列表。接下来构建一个测试集和一个训练集,两个集合中的邮件都是随机选出的。分类器所需要的概率计算只利用训练集中的文档来完成。Python变量trainingSet是一个整数列表,其中的值从0到49。接下来,随机选择其中10个文件。选择出的数字所对应的文档被添加到测试集,同时也将其从训练集中剔除。这种随机选择数据的一部分作为训练集,而剩余部分作为测试集的过程称为留存交叉验证(hold-out cross validation)

接下来的for循环遍历训练集的所有文档,对每封邮件基于词汇表并使用bagOfWords2VecMN()函数来构建词向量。这些词在trainNB0()函数中用于计算分类所需的概率。然后遍历测试集,对其中每封电子邮件进行分类。如果邮件分类错误,则错误数加1,最后给出总的错误百分比。

在Python提示符下输入下列代码,测试效果:

>>> import sys
>>> sys.path.append('E:\Python_Files\CodeofMe\Chapter4')
>>> import bayes
>>> bayes.spamTest()
classification error ['experience', 'with', 'biggerpenis', 'today', 'grow', 'inches', 'more', 'the', 'safest', 'most', 'effective', 'methods', 'of_penisen1argement', 'save', 'your', 'time', 'and', 'money', 'bettererections', 'with', 'effective', 'ma1eenhancement', 'products', 'ma1eenhancement', 'supplement', 'trusted', 'millions', 'buy', 'today']
the error rate is:  0.1

函数spamTest()会输出在10封随机选择的电子邮件上的分类错误率。

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

前面介绍了朴素贝叶斯的两个实际应用的例子,第一个例子是过滤网站的恶意留言,第二个是过滤垃圾邮箱。

广告商往往想知道关于一个人的一些特定人口统计信息,以便能够更好地定向推销广告。从哪里可以获得这些训练数据呢?事实上,互联网上拥有大量的训练数据。

最后一个例子,我们将分别从美国的脸各个城市中选取一些人,通过分析这些人发布的征婚广告信息,来比较这两个城市的人们在广告用词上是否不同。如果结论的确不同, 那么他们各自常用的词是哪些?从人们的用词当中,我们能否对不同城市的人所关心的内容有所了解?

示例:使用朴素贝叶斯来发现地域相关的用词

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

4-7-1 收集数据:导入RSS源

接下来要做的第一件事是使用Python下载文本。利用RSS,这些文本很容易得到。Universal Feed Parser 是Python中最常用的RSS程序库。

feedparser地址

feedparser的安装教程

下面使用Craigslist上的个人广告,打开Craigslist上的RSS源,在Python提示符下输入:

>>> import feedparser
>>> ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')

要访问所有条目的列表,输入:

>>> ny['entries'] 
>>> len(ny['entries'])
25

下面构建类似于spamTest()的函数来测试过程自动化。在bayes.py文件中增加下列代码:

"""
    Function:
        获取文档的高频词
    Parameters:
        vocabList——不重复词向量列表
        fullText——文档对应的所有词汇
    Return:
        sortedFreq[:30]——频率高的前30个词汇
    Modify:
        2017-12-30    
"""
def calcMostFreq(vocabList,fullText):
    import operator
    #operator——运算符模块
    freqDict = {}
    #创建空集词典
    for token in vocabList:
        freqDict[token]=fullText.count(token)
        #计算词汇列表中的词在所有文档中出现的次数
    sortedFreq = sorted(freqDict.iteritems(), key=operator.itemgetter(1), reverse=True) 
    #将词汇按照健值从大到小排序
    return sortedFreq[:30]  
    #返回排序在前30的词汇

"""
    Function:
        RSS源分类器
    Description:
        该部分的程序spamTest()类似,仅添加了剔除高频词函数
    Parameters:
        feed1——RSS源1
        feed0——RSS源2
    Return:
        vocabList——词汇表
        p0V——NY类的条件概率数组
        p1V——SF类的条件概率数组
    Modify:
        2017-12-30    
"""
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) 
        #NY is class 1
        wordList = textParse(feed0['entries'][i]['summary'])
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList)
    #create vocabulary
    top30Words = calcMostFreq(vocabList,fullText)   
    #remove top 30 words
    for pairW in top30Words:
        if pairW[0] in vocabList: vocabList.remove(pairW[0])
    trainingSet = range(2*minLen); testSet=[]           
    #create test set
    for i in range(20):
        randIndex = int(random.uniform(0,len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(trainingSet[randIndex])  
    trainMat=[]; trainClasses = []
    for docIndex in trainingSet:
    #train the classifier (get probs) trainNB0
        trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex])
    p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
    errorCount = 0
    for docIndex in testSet:        
    #classify the remaining items
        wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
        if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
    print 'the error rate is: ',float(errorCount)/len(testSet)
    return vocabList,p0V,p1V

上述localWords()函数与spamTest()函数类似,只是添加了一个辅助函数calcMostFreq()。该函数遍历词汇表中的每个词并统计他在文本中出现的次数,然后根据出现次数从高到低对字典进行排序,最后返回排序最高的30个单词。

采用calcMostFreq()函数,是为了去除高频词汇,因为词汇表中的小部分单词占据了所有文本用词的大部分。产生这种现象的原因是因为语言中大部分都是冗余和结构辅助性内容。另一个常用的方法是不仅一出高频词,同时从某个预定词表中移除结构上的辅助词。该词表称为停用词表(stop word list)

在Python命令提示符下输入下列代码,测试结果:

>>> import sys
>>> sys.path.append('E:\Python_Files\CodeofMe\Chapter4')
>>> import bayes
>>> import feedparser
>>> ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
>>> sf = feedparser.parse('http://sfbay.craigslist.org/stp/index.rss')
>>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
the error rate is:  0.5
>>> vocabList,pSF,pNY = bayes.localWords(ny,sf)
the error rate is:  0.45

4-7-2 分析数据:显示地域相关的用词

可以先对向量pSF与pNY进行排序,然后按照顺序将此打印出来。在bayes.py文件中添加下列代码:

"""
    Function:
        最具表征性的词汇显示函数
    Parameters:
        ny——RSS源1
        sf——RSS源2
    Return:
        None
    Modify:
        2017-12-30    
"""
def getTopWords(ny,sf):
    import operator
    #operator——运算符模块
    vocabList,p0V,p1V=localWords(ny,sf)
    #得到词汇表、0类条件概率数组、1类条件概率数组
    topNY=[]; topSF=[]
    #构建列表存储最具表征性词汇元组
    for i in range(len(p0V)):
        #条件概率大于-6.0的为最具表征词汇元组
        if p0V[i] > -6.0 : topSF.append((vocabList[i],p0V[i]))
        if p1V[i] > -6.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**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**NY**NY**NY**NY**NY**NY**"
    for item in sortedNY:
        print item[0]

函数getTopWords()使用两个RSS源作为输入,然后训练并测试朴素贝叶斯分类器,返回使用概率值。然后创建两个列表用于元组的存储。与之前返回排名最高的X个,这里可以返回大于某个阈值的所有词。这些元组会按照他们的条件概率进行排序。

在Python提示符下输入下列代码:

>>> reload(bayes)
'bayes' from 'E:\Python_Files\CodeofMe\Chapter4\bayes.py'>
>>> bayes.getTopWords(ny,sf)
the error rate is:  0.2
SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**
min
grandfather
because
get
things
friends
says
body
massage
more
...
NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**
friend
let
don
get
them
barefoot
send
did
ever
full
...

4-8 本章小结

对于分类而言,使用概率有时要比使用硬规则更有效。贝叶斯概率及贝叶斯准则提供了一种利用已知值来估计未知概率的有效方法。

可以通过特征之间的条件独立性假设,降低对数据量的需求。尽管条件独立性假设并不正确,但是朴素贝叶斯任然是一种有效的分类器。

实现朴素贝叶斯时考虑很多实际因素:下溢出可以通过对概率取对数来解决;词袋模型在解决文档分类问题上比词集模型有所提高;等其他方面的改进。

4-9 参考文献

《机器学习实战》

你可能感兴趣的:(学习笔记,python,机器学习,读书笔记,算法)