这片文章主要是最近学习朴素贝叶斯的学习笔记。

什么是朴素贝叶斯

简单的说:

朴素贝叶斯 = 朴素 + 贝叶斯

举个例子: 假设有一片文档,一旦我们知道某个分类的词在文档中出现的概率,反过来我们就能知道某一篇文档属于某个分类的概率。

接着上边这个贝叶斯理论,根据某个分类在文档中出现的概率,我们就可以得出文档属于这个分类的概率,最终选择概率较大的分类作为文档的类别,从而实现文档分类的目的,这就是朴素贝叶斯分类器的原理。

其他知识点

拉普拉斯平滑

上边就是拉普拉斯平滑的公式。

这里为什么要用到拉普拉斯平滑?主要的原因就是为了防止出现概率为 0 的情况。

举个例子: 文档一共有两种分类分别记为分类1与分类2,那么如果在另外一篇文档中两种分类的词汇都没有出现,那么我们能想到最好的分类结果就分类1与分类2发生的概率各占一半,分别为 50%。但是按照我们的公式来计算的话,两种分类的概率均为 0/n = 0。这样的结果不是我们想要的,因为理论上来说,P(分类1)+P(分类2)=100%。所以拉普拉斯平滑很好的解决了这里的问题。

代码实现

不多说废话,先贴代码,之后再详细解释。

#!/usr/bin/env python  
# encoding: utf-8  

"""
@version: v1.0 
@author: zhangyw
@site: http://blog.zhangyingwei.com
@software: PyCharm 
@file: naivebayes.py 
@time: 2017/11/29 16:40
@desc: 朴素贝叶斯分类器
"""
import numpy as np

class NaiveBayes:

    def __init__(self):
        self.__data_set = None # 训练数据集
        self.__list_of_world = None # 去重之后的词集
        self.__targets = None # 样本标签
        self.__vec_set = None
        self.__p0 = 0.0 # 正样本的概率
        self.__p1 = 0.0 # 负样本的概率

    def trainNB(self,data_set = None,targets = None):
        '''
        :param data_set:
        :return:
        '''
        self.__data_set = data_set
        self.__targets = targets
        self.__words = self.__get_list_of_worlds(data_set)

        vec_set = []
        for item in data_set:
            base_word_vec = self.__get_word_vec(item)
            vec_set.append(base_word_vec)
        self.__vec_set = vec_set

        num0 = np.ones(len(self.__words))
        num1 = np.ones(len(self.__words))
        count0 = 2.0
        count1 = 2.0
        for index,item in enumerate(vec_set):
            if targets[index] == 0:
                num0+=item
                count0+=sum(item)
            elif targets[index] == 1:
                num1+=item
                count1 += sum(item)

        self.__p0 = np.log(num0/count0)
        self.__p1 = np.log(num1/count1)

    def classify(self,word_list):
        word_vec = self.__get_word_vec(word_list)
        pbad = sum(self.__targets)/len(self.__targets)
        p0 = sum(word_vec * self.__p0) + np.log(1 - pbad)
        p1 = sum(word_vec * self.__p1) + np.log(pbad)
        # print("p0 is {} and p1 is {}".format(p0,p1))
        if p0 > p1:
            return 0
        else:
            return 1

    def __get_list_of_worlds(self,data_set):
        '''
        构造词集
        :param data_set:
        :return:
        '''
        result = set([])
        for item in data_set:
            result = result | set(item)
        return list(result)

    def __get_word_vec(self, item):
        base_word_vec = [0] * len(self.__words)
        for word in item:
            if word in self.__words:
                base_word_vec[self.__words.index(word)] = 1
        return base_word_vec

代码解释

这里详细解释一下代码。

trainNB 方法

首先从 trainNB 方法作为入口进行分析。

这个方法接收两个参数,一个是训练数据,一个是训练数据的分类信息。

在方法中首先涉及到的知识点就是下边这行:

self.words = self.get_list_of_worlds(data_set)

这里调用了 __get_list_of_worlds 方法。这个方法的作用主要是把数据集中所有单词做了一个并集,得到的结果就是一个不重复的包含数据集中所有单词的 list,在下文中,称这个 list 为词集。

举个例子:

假设给定的数据集为:

[
    ["a","b","c"],
    ["a","d","e"],
    ["f","c","b"]
]

那么经过 __get_list_of_worlds 方法之后,会得到如下结果:

["a","b","c","d","e","f"]

接下来的这一步就是要把给定数据集中所有文档信息,全部转换为词汇向量。换个说法就是把每一篇文档转换成一个向量。

vec_set = []
for item in data_set:
    base_word_vec = self.__get_word_vec(item)
    vec_set.append(base_word_vec)

上边这几句代码的作用就是转换文档为向量。这里有一个转换方法为 __get_word_vec

def __get_word_vec(self, item):
    base_word_vec = [0] * len(self.__words)
    for word in item:
        if word in self.__words:
            base_word_vec[self.__words.index(word)] = 1
    return base_word_vec

解释一下,这里主要是以上一步构造的词集为标杆。先构造一个与词集相同大小的元素都为 0 的数组。然后对比给定数据集中的词汇与词集中的词汇,如果一个词汇在词集中出现,则在相应的位置标 1。

然后接下来这几句就是上文中提到的拉普拉斯平滑。

num0 = np.ones(len(self.__words))
num1 = np.ones(len(self.__words))
count0 = 2.0
count1 = 2.0

这里 num0,num1 是为了统计每个单词出现的次数,我们构造的时候以 1 为基数,相当于 λ=1,这里讨论的分类问题是一个标准的二分类,所以 k=2。所以 count0,count1 的基数都为 2,即 kλ=1*2=2。

接下来到了下边这一段代码

for index,item in enumerate(vec_set):
    if targets[index] == 0:
        num0+=item
        count0+=sum(item)
    elif targets[index] == 1:
        num1+=item
        count1 += sum(item)

self.__p0 = np.log(num0/count0)
self.__p1 = np.log(num1/count1)

解释一下,这段代码主要是计算每个单词的概率,举个例子如果分类1中一共有100个单词,而某一个单词出现了10次,那么这个单词的概率就是 10/100=0.1。

另外这里还有一个知识点。就是 np.log(num0/count0) 这一句。实际上依据贝叶斯公式可以得出,对与某一个分类中的所有词汇的概率我们需要求其乘积,即 p1p2...,但是这里存在一个隐患,就是当某一个单词的概率为 0 的时候,会对整个结果造成非常严重的影响,所以这里使用 log 函数将所有的乘法关系转换为加法关系,依赖的原理就是 log(a b) = log(a)+log(b)。

所以在程序中对每一个词汇的概率求 log。

到这里 trainNB 这个方法就已经结束了。

classify 方法

接下来分析 classify 方法,在整个算法中,跟数据直接接触的方法就是 classifytrainNB 方法了。

classify 这个方法非常简洁,总共分为 5 个步骤。

首先将需要分类的数据集进行向量话,这里向量话使用的方法跟上边数据预处理的时候使用的方法是一样的。接下来求了数据集中每类样本所占的比例,这两步都是非常简单的,就不拆开来说了。着重解释一下求数据属于每一个分类的概率这部分。

 sum(word_vec * self.__p1) + np.log(pbad)

最主要的就是上边这个步骤了。我们重新看下贝叶斯公式。

这里 sum(word_vec * self.__p0) 就是 p(x|y),而 pbad 就是 p(y),因为 分母 [p(x)] 都是一样的,所以只要比较分子的大小就可以,所以这里神略了分母部分。

这里还有另外一个知识,就是上边所提到的 log(a * b) = log(a)+log(b)。

到这里整个朴素贝叶斯就解释完了,这个坑也算是填上了。。。