本系列代码托管于:https://github.com/chintsan-code/machine-learning-tutorials
本篇使用的项目为:bayes_create

在前面的篇章,我们使用knn或dt时都要求分类器做出决策,给出 “该数据实例属于哪一类”这类问题的明确答案。不过,分类器有时会产生错误结果,这时可以要求分类器给出一个最优的类别猜测结果,同时给出这个猜测的概率估计值。

概率论是许多机器学习算法的基础,所以深刻理解这一主题就显得十分重要。在决策树的学习中,我们在计算特征值取某个值的概率时涉及了一些概率知识,在那里我们先统计特征在数据集中取某个特定值的次数,然后除以数据集的实例总数,就得到了特征取该值的概率(即计算平均信息期望时,不同特征取值所占的“权重”)。

本篇将从一个最简单的概率分类器开始,然后给出一些假设来学习朴素贝叶斯分类器。我们称之为“朴素”,是因为整个形式化过程只做最原始、 最简单的假设。并充分利用Python的文本处理能力将 文档切分成词向量,然后利用词向量对文档进行分类。

在正式开始之前,我们有必要讨论一下条件概率,我们从一个小球问题引入:

第一个问题:假设一个盒子中有4个黑球和3个白球,问从中随机取一个小球,取到白球的概率是多少?
答案很简单,是\(\frac{3}{7}\),像这类概率问题,我们称为正向概率。

第二个问题:假设有两个盒子,1号盒子中装有2个黑球和2个白球;2号盒子装有2个黑球和1个白球。问随机取出一个白球,这个白球是从2号盒中取出来的概率是多少?
剖析一下这个问题:前提条件是已经取出一个白球,要求的是在这个条件之下,选中的盒子是2号盒的概率。那么根据题目给出的信息,我们可以求出答案是\(\frac{1}{3}\),因为总共的白球就3个,且2号盒子中占1个。
还有没有其他求法呢?有的,且看:

记事件A记为“随机取出一个小球,结果为白球”,可以得出\(p(A)=\frac{3}{7}\);
记事件B为“随机取一个小球,它是从2号盒子取出来的”,可以得出 \(p(B)=\frac{3}{7}\) ;
记事件A|B为“从2号盒中取出一个球 ,结果为白球 ”,可以得出 \(p(A|B)=\frac{1}{3}\) ,像这种在某个事件发生的前提之下,另一个事件发生的概率称为条件概率;
记事件B|A为“ 随机取出一个白球,这个白球是从2号盒中取出来的 ”,根据贝叶斯公式,有
$$p(B|A)=p(B)\frac{p(A|B)}{p(A)}$$
这个公式是怎么来的呢?我们可以推导一下:
由条件概率和联合概率的公式:
$$p(A,B)=p(B|A)p(A)$$
有:
$$p(A,B)=p(B|A)p(A)$$
$$p(A,B)=p(A|B)p(B)$$
联立方程,有:
$$ p(B|A)p(A) = p(A|B)p(B) $$
即: $$p(B|A)=p(B)\frac{p(A|B)}{p(A)}$$

回到刚刚的问题:
$$p(B|A)=p(B)\frac{p(A|B)}{p(A)}=\frac{3}{7}\times\frac{\frac{1}{3}}{ \frac{3}{7} }=\frac{1}{3}$$

到这里,你可能会有一个疑问:这样算多麻烦啊,我按之前那样不是也可以得出同样的结果吗?那是因为我们事先已经知道了1号盒、2号盒中所有黑球和白球的个数,信息是完全透明的,但是现实生活中我们往往无法得知具体有多少个小球,而是只知道概率,如:已知全部小球中白球占\(\frac{3}{7}\),黑球占 \(\frac{4}{7}\) ,将这些小球分在两个盒子中,1号盒中白球占\(\frac{1}{2}\),黑球占 \(\frac{1}{2}\) , 2号盒中白球占\(\frac{1}{3}\),黑球占 \(\frac{2}{3}\) ,求 随机取出一个白球,这个白球是从2号盒中取出来的概率是多少 ?像这种问题,我们就不能通过之前的方法来计算,而是得依靠贝叶斯公式。
再举一个例子说明何时应该使用贝叶斯:假设某种疾病的发病率是0.001,即1000人中会有1个人得病。现有一种试剂可以检验患者是否得病,它的准确率是0.99,即在患者确实得病的情况下,它有99%的可能呈现阳性。它的误报率是5%,即在患者没有得病的情况下,它有5%的可能呈现阳性。现有一个病人的检验结果为阳性,请问他确实得病的可能性有多大?
有兴趣的可以计算一下。可以记事件A:得病,事件B:检测呈阳性,求\(p(A|B)\)

接下来我们使用朴素贝叶斯进行文档分类 :

首先看下我们的数据集:

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

我们给出了6句文字,其中有3句有侮辱性,另外3句则是正常言论。我们的目的是使用这些数据集构建一 个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标识为内容不当。为此我们需要做以下工作:

第一步, 准备数据:从文本中构建词向量

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

def createVocabList(dataSet):
    vocabSet = set([])  # create empty set
    for document in dataSet:
        vocabSet = vocabSet | set(document)  # union of the two sets
    return list(vocabSet)

函数createVocabList()会创建一个包含在所有文档中出现的不重复词的列表,为
此使用了Python的set数据类型。将词条列表输给set构造函数,set就会返回一个不重复词表。首先,创建一个空集合 ,然后将每篇文档返回的新词集合添加到该集合中 。操作符|用于求两个集合的并集,这也是一个按位或(OR)操作符。在数学符号表示上,按位或操作与集合求并操作使用相同记号。需要注意的是集合set是没有先后顺序的,所以我们每次执行,得到的词汇表vocabSet都是不同的。

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

获得词汇表后,便可以使用函数setOfWords2Vec(),该函数的输入参数为词汇表及某个文 档,输出的是文档向量,向量的每一元素为1或0,分别表示词汇表中的单词在输入文档中是否出 现。函数首先创建一个和词汇表等长的向量,并将其元素都设置为0 。接着,遍历文档中的所 有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1。一切都顺利的话, 就不需要检查某个词是否还在vocabList中,后边可能会用到这一操作。
朴素贝叶斯分类器通常有两种实现方式:一种基于贝努利模型实现,一种基于多项式模型实现。这里采用前一种实现方式。该实现方式中并不考虑词在文档中出现的次数,只考虑出不出现,因此在这个意义上相当于假设词是等权重的。在后面的篇章也会介绍多项式模型,它考虑词在文档中的出现次数。

第二步, 训练算法:从词向量计算概率

前面介绍了如何将一组单词转换为一组数字,接下来看看如何使用这些数字计算概率。现在已经知道一个词是否出现在一篇文档中,也知道该文档所属的类别(侮辱性文字或正常言论),有贝叶斯公式:
$$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..w_N|c_i)\)。这里假设所有词都互相独立,该假设也称作条件独立性 假设,它意味着可以使用 \( p(w_0|c_i)p(w_1|c_i)p(w_2|c_i)…p(w_N|c_i) \) 来计算上述概率,这就极大地 简化了计算的过程。

注释:
$$p(w_0):一句话中出现单词w_0的概率 $$
$$ p(c_i):一句话属于i类型的概率 $$
$$ p(w_0|c_i):i类型的一句话中出现单词w_0的概率 $$
$$ p(c_i|w_0):当一句话中出现单词w_0,这句话属于i类型的概率 $$
由于一句话中包含\(w_0、w_2…w_N\),所以我们计算一句话是不是i类型,应该计算
$$ p(c_i|w) = p(c_i|w_0,w_1,w_2…w_N)$$

def trainNB0(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix)
    numWords = len(trainMatrix[0])
    pAbusive = sum(trainCategory) / float(numTrainDocs) # 计算计算文档属于侮辱性文档(class=1)的概率
    p0Num = zeros(numWords)
    p1Num = zeros(numWords)
    p0Denom = 0.0
    p1Denom = 0.0
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    p1Vect = p1Num / p1Denom
    p0Vect = p0Num / p0Denom
    return p0Vect, p1Vect, pAbusive

首先,计算文档属于侮辱性文档(class=1)的概率,即 \( p(1) \) 。因为这是一个二分类问题,所以可以通过\(1-p(1)\)得到 \( p(0) \) 。对于多于两类的分类问题,则需要对代码稍加修改。
计算 \( p(w_i|c_1) \) 和 \( p(w_i|c_0) \) ,需要初始化程序中的分子变量和分母变量 。由于w中元素如此 众多,因此可以使用NumPy数组快速计算这些值。上述程序中的分母变量是一个元素个数等于词汇表大小的NumPy数组。在for循环中,要遍历训练集trainMatrix中的所有文档。一旦某个词 语(侮辱性或正常词语)在某一文档中出现,则该词对应的个数(p1Num或者p0Num)就加1, 而且在所有的文档中,该文档的总词数也相应加1 。对于两个类别都要进行同样的计算处理。 最后,对每个元素除以该类别中的总词数 。利用NumPy可以很好实现,用一个数组除以浮 点数即可,若使用常规的Python列表则难以完成这种任务,读者可以自己尝试一下。最后,函数 会返回两个向量和一个概率。
注:程序最终得到的结果为:
$$p1Vect = p(w|c_1)$$
$$p0Vect = p(w|c_0)$$
$$pAbusive=p(c_1)$$

现在我们回顾一下最开始讲的小球问题,和这个程序做一个类比:
小球问题:两个盒子;程序:两个类别,0或1
小球问题:黑球或白球;程序:32个单词(理解成32种小球)
小球问题:1号盒有2黑2白,2号盒有2黑1白;程序:单词有点多我就举一个,0类别有1个dog,1类别有2个dog

现在我们已经完成了朴素贝叶斯分类器的基本功能,但是还有一些缺陷,我们将再下一讲进行解释和修复。