朴素贝叶斯

朴素贝叶斯是贝叶斯决策理论的一部分,贝叶斯概率引入先验知识和逻辑推理来处理不确定命题。又可以称为“条件概率”(Conditional probability),与之相对的则是“频数概率”(frequency probability)。

强烈推荐看阮一峰大牛的这篇文章,看完基本贝叶斯就明白了。

我们把\(P(A|B)\)称为在B条件下A的概率,若只有A、B两个事件那么\(P(A|B)= {P(AB) \over P(B)} \) 。

全概率 公式: $$P(B)=P(B|A)P(A) + P(B|\overline A)P(\overline A) $$

贝叶斯 公式:

$$P(A|B)={P(B|A) × P(A) \over P(B)}$$

对公式进行变形:

$$P(A|B)={P(A){P(B|A) \over P(B)}}$$

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

这里以贝叶斯界的"Hello World"为例,判断某种疾病患病概率问题:

已知某种疾病的发病率是0.001,即1000人中会有1个人得病。现有一种试剂可以检验患者是否得病,它的准确率是0.99,即在患者确实得病的情况下,它有99%的可能呈现阳性。它的误报率是5%,即在患者没有得病的情况下,它有5%的可能呈现阳性。现有一个病人的检验结果为阳性,请问他确实得病的可能性有多大?

我们假定阳性概率为P(B),发病的概率为P(A)=0.001,则没得病的概率\(P(\overline A)\)=0.999,确实得病且阳性概率为P(B|A)=0.99,没患病但阳性\(P(B|\overline A)\)=0.05,那么我们要求的问题就是P(A|B)了。

结合上面提到的全概率公式和贝叶斯公式,可以计算出他确实得病的概率仅为0.019,即“假阳性”。

如果把问题改成检验结果为阴性,问其患病的可能性。

我们假定阴性概率为P(B),发病的概率为P(A)=0.001,没得病的概率\(P(\overline A)\)=0.999,确实得病但阴性概率为P(B|A)=0.01,没患病且阴性\(P(B|\overline A)\)=0.95,那么检查为阴性但患病的概率就是:

$$P(A|B)={0.001×0.01 \over {0.01×0.001 + 0.95×0.999}} \approx 0.000011 $$

即便阴性也是有患病几率的,但概率十分低。

还有一个重要的公式叫做 联合概率 ,作用就是在已知多个事件发生的情况下,另一个事件发生的概率:

$$P ={ {P_1P_2…P_n } \over { {P_1P_2…P_n}+{(1-P_1)(1-P_2)…(1-P_n)} } } $$

接下来编写一个简陋的朴素贝叶斯分类器,这里还需要引用 条件独立性假设 ,即若各个条件互相独立,那么: $$ P(w_1,w_2,…w_n|c_i) = P(w_1|c_i)P(w_2|c_i)…P(w_n|c_i) $$

现在假设有一句话,要判断这句话是消极的还是积极的,已知数据集如下:

def load_dataset():
    posting_list = [
        ['我','喜欢','你'],
        ['我','喜欢','吃','包子'],
        ['我','讨厌','你'],
        ['我','讨厌','吃','馒头'],
        ['我','喜欢','肉','包子'],
    ]
    class_vec = [1,1,0,0,1]
    return posting_list, class_vec

这里使用1代表积极,0代表消极,为了方便这里不涉及中文分词相关问题。

接下来创建数据集合,把所有句子中涉及到的词都放进一个列表中:

def create_vocablist(dataset):
    vocabset = set()
    for doc in dataset:
        vocabset = vocabset | set(doc)
    return list(vocabset)

要计算概率肯定涉及到文本向量化,这里又分为"词集模型"和"词袋模型":

def words2vec(vocablist, inputset):
    '''文本转向量,词集模型,即每个词只能出现一次'''
    returnvec = [0] * len(vocablist)
    for word in inputset:
        if word in vocablist:
            returnvec[vocablist.index(word)] = 1
        else:
            print("%s is not found!" % word)
    return returnvec


def bag_words2vec(vocablist, inputset):
    '''文本转向量,词袋模型,即每个词能出现多次'''
    returnvec = [0] * len(vocablist)
    for word in inputset:
        if word in vocablist:
            returnvec[vocablist.index(word)] += 1
        else:
            print("%s is not found!" % word)
    return returnvec

这两者的区别注释中已经给出,再下面将会看到具体结果。接下来创建分类器:

def train_nb(train_matrix, train_category):
    '''朴素贝叶斯训练函数,这里仅是二元分类,如果类别数量更多需要进行更改'''
    train_docs_num = len(train_matrix)
    word_num = len(train_matrix[0])
    p_ab = sum(train_category) / float(train_docs_num)
    p0_num = np.ones(word_num)  # 使用1来初始化分子
    p1_num = np.ones(word_num)
    p0_denom = 2.0
    p1_denom = 2.0  # 使用2来初始化分母,为了减低某个概率为0时乘积后对结果影响。
    for i in range(train_docs_num):
        if train_category[i] == 1:
            p1_num += train_matrix[i]
            p1_denom += sum(train_matrix[i])
        else:
            p0_num += train_matrix[i]
            p0_denom += sum(train_matrix[i])
    p1_vect = np.log(p1_num / p1_denom)  # py中多个很小的数相乘会下溢或得到错误答案,使用log可以解决这点。
    p0_vect = np.log(p0_num / p0_denom)
    return p0_vect, p1_vect, p_ab

其中,p_ab指先验概率,比如我们这里使用的例子里5个句子中有3个积极2个消极,那么P(消极)的先验概率就是0.4。而p1_vect中则记录出现的单词的可能性函数值(参考上面可能性函数的解释)。简单说,假设一个单词在积极的句子中出现了10次,而表示积极的单词共有100个,那么一个积极的句子中出现该单词的概率就是0.1。

def classify_nb(vec, p0_vect, p1_vect, p_class1):
    '''计算概率,取概率高的分类'''
    p1 = sum(vec * p1_vect) + np.log(p_class1)
    p0 = sum(vec * p0_vect) + np.log(1 - p_class1)
    return '积极' if p1 > p0 else '消极'

一个句子往往由多个单词组成,所以我们需要对其中出现的单词是积极还是消极进行求和,后面注意和该类别的概率对数相加。

下面来进行测试:

list_oposts, list_class = load_dataset()
myvocablist = create_vocablist(list_oposts)
train_mat = []
for post_doc in list_oposts:
    train_mat.append(bag_words2vec(myvocablist, post_doc))
p0v, p1v, pab = train_nb(train_mat, list_class)
test_list = ['我', '讨厌', '包子']
this_doc = np.array(bag_words2vec(myvocablist, test_list))
print(test_list, "classify result:", classify_nb(this_doc, p0v, p1v, pab))

输出为['我', '讨厌', '包子'] classify result: 消极

注意这里使用的是词袋模型,如果把句子改一改:

['讨厌', '包子'] classify result: 消极

['讨厌', '包子', '包子'] classify result: 积极

第二句中居然变成了积极,因为有2个包子!这种情况下使用词集模型就不会出现这种错误,所以使用哪种模型因该看情况。