朴素贝叶斯(Naive Bayes)

一. 生成式(generative)学习算法

如果算法直接学习,或者尝试学习从输入空间到类别的映射关系的算法,称为判别式(discriminative)学习算法;比线性回归(lineaar regression)的模型:

再比如逻辑回归(logistic regression):

这里的是sigmoid函数

而另外一种算法是建立(和)的模型,这类算法称为生成式(generative)学习算法,我们今天要讨论的朴素贝叶斯算法即是其中一个。对(先验 prior)和,使用贝叶斯规则来推导给定的的后验(posterior)分布:

其中分母可由全概率公式计算:

实际上,我们在计算做预测时,可以不用计算分母,因为:

二. 朴素贝叶斯(Naive Bayes)

2.1 朴素贝叶斯算法

假设我们想利用机器学习来构建一个言论过滤器,希望对网站的留言评论自动区分是侮辱性言论,还是非侮辱性言论。现在已知一个训练数据集(一些已经标记为侮辱或者非侮辱的言论集合)如下:

"""
函数说明:创建实验样本

Parameters:
    无
Returns:
    postingList - 实验样本切分的词条
    classVec - 类别标签向量
"""
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]           # 对应postingList中每个样本的类别标签向量,1代表侮辱性言论, 0代表非侮辱性言论
    return postingList, classVec

令 表示该言论是否是侮辱性言论:

接下来,我们将样本数据,即postingList,转化成特征向量,用 表示,其中包含单词特征:

编码到向量中的单词的集合称为词汇表,例如“ my”,“stupid”等等,向量的长度等于词汇表的长度。

注意,这里的词汇表不是将英语词典中单词全部列出来,通常是将训练数据集中的所有单词,即使仅出现过一次,放到词汇表中。这样做可以减少模型的词汇数量,从而减少计算量,节省空间;还有个好处是能够将英语词典中不会出现的词,但会出现在留言评论中的词,比如“”放到词汇表中;同时还需要剔除一些高频词,比如 “” “” “”,这些词在很多的文本中都会出现,对区分是否是侮辱性言论没有任何帮助。

特征向量与词汇表如下图所示,言论中包含“”,“buy”,不包含“aardvard”,“aardwolf”,“zygmurgy”:

创建词汇表代码如下:

"""
函数说明:将切分的实验样本词条整理成不重复的词条列表,也就是词汇表

Parameters:
    dataSet - 样本数据集 postingList
Returns:
    vocabSet - 返回不重复的词条列表,也就是词汇表
"""
def createVocabList(dataSet):
    vocabSet = set([])
    for document in dataSet:
        vocabSet = vocabSet | set(document)
    return list(vocabSet)

将样本数据转换成特征向量代码如下:

"""
函数说明:根据vocabList词汇表,将inputSet向量化,向量的每个元素为1或0

Parameters:
    vocabList - 词汇表
    inputSet - 某条言论文档
Returns:
    returnVec - 言论文档向量
"""
def setOfWords2Vec(vocabList, inputSet):
    returnVec = [0] * len(vocabList)            # 创建一个其中所含元素都为0的向量
    for word in inputSet:                       # 遍历每个词条
        if word in vocabList:                   # 如果词条存在于词汇表中,则置1
            returnVec[vocabList.index(word)] = 1
        else:
            print("the word: %s is not in my vocabulary !" % word)
    return returnVec

现在我们来构建。上面代码示例中词汇表中单词较少,但如果词汇表中包含50000个单词,那么(是50000维0和1组成的向量),如果我们直接用多项式来构造的个可能的结果,那么多项式的参数向量有维,显然参数太多了。

因此算法做了一个强假设,即假设给定的情况下条件独立,这个假设就称为朴素贝叶斯假设,算法称为朴素贝叶斯分类。例如,如果表示侮辱性言论,“”是第个单词,“”是第个单词,那么我们假设如果已知,那么的值(“”是否出现在言论中) 对的值(“”是否出现在言论中)没有任何影响。正式一点来描述,上述可以写成。需要注意的是,这里并不是说和相互独立,相互独立表示为,而是仅假设和在给定的情况下条件独立。根据上述假设,有如下等式成立:

第一个等式是概率的基本性质,第二个等式用了朴素贝叶斯假设。

我们的模型中的参数为,和,其中为词汇表中第个单词。给定一个训练数据集,我们可以写出似然函数:

最大化关于参数, , 的上述似然函数,得到第个单词相关参数的极大似然估计:


即是对的估计;


即是对的估计;


即是对的估计;同理

即是对的估计;参数表达式中的 表示“与”。上面的参数不难理解,就是侮辱性言论()中单词出现的百分比。训练阶段代码如下:

"""
函数说明:朴素贝叶斯分类器训练函数

Parameters:
    trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
    trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
Returns:
    p0Vect - 非侮辱类的条件概率数组
    p1Vect - 侮辱类的条件概率数组
    pAbusive - 文档属于侮辱类的概率
"""
def trainNB0(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix)         #计算训练的文档数目
    numWords = len(trainMatrix[0])          #计算每篇文档的词条数
    pAbusive = sum(trainCategory)/float(numTrainDocs)  #文档属于侮辱类的概率
    p0Num = np.zeros(numWords)              #创建numpy.zeros数组,词条出现数初始化为0
    p1Num = np.zeros(numWords)
    p0Denom = 0.0                           #分母初始化为0
    p1Denom = 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
    return p0Vect, p1Vect, pAbusive         #返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率

已知参数估计之后,对一个特征向量的新样本计算是侮辱性言论的概率如下:
\begin{eqnarray} p(y=1|x) &=& \frac{p(x|y)p(y=1)} {p(x)} \\ &=& \frac{(\prod_{j=1}^{n} p(x_j | y=1))p(y=1)} {(\prod_{j=1}^{n} p(x_j | y= 1))p(y=1) + (\prod_{j=1}^{n} p(x_j | y= 0))p(y=0)} \end{eqnarray}
同样,计算非侮辱性言论的概率如下:
\begin{eqnarray} p(y=0|x) &=& \frac{p(x|y)p(y=0)} {p(x)} \\ &=& \frac{(\prod_{j=1}^{n} p(x_j | y=0))p(y=0)} {(\prod_{j=1}^{n} p(x_j | y= 1))p(y=1) + (\prod_{j=1}^{n} p(x_j | y= 0))p(y=0)} \end{eqnarray}
如果计算得出,则认为言论是侮辱性言论,反之是非侮辱性言论,预测代码如下:

"""
函数说明:朴素贝叶斯分类器分类函数

Parameters:
    vec2Classify - 待分类的词条数组
    p0Vec - 侮辱类的条件概率数组
    p1Vec -非侮辱类的条件概率数组
    pClass1 - 文档属于侮辱类的概率
Returns:
    0 - 属于非侮辱类
    1 - 属于侮辱类
"""
def classifyNB0(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = reduce(lambda x,y : x * y, vec2Classify * p1Vec) * pClass1             # 对应元素相乘
    p0 = reduce(lambda x,y : x * y, vec2Classify * p0Vec) * (1 - pClass1)
    print('p0:', p0)
    print('p1', p1)
    if p1 > p0:
        return p1
    else:
        return p0

2.2 下溢出与拉普拉斯平滑

2.1节中我们使用朴素贝叶斯算法构造了言论分类器,下面我们对算法进行测试:

"""
函数说明:测试朴素贝叶斯分类器

Parameters:
    无
Returns:
    无
"""
def testingNB0():
    listOPosts, classVec = loadDataSet()  # 创建实验样本

    myVocabList = createVocabList(listOPosts)  # 创建词汇表

    # 打印中间结果
    print('myVocabList:\n', myVocabList)

    trainMat = []
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))  # 将实验样本向量化
    p0V, p1V, pAb = trainNB0(np.array(trainMat), np.array(classVec))  # 训练朴素贝叶斯分类器

    # 打印中间结果
    print('p0V:\n', p0V)
    print('p1V:\n', p1V)
    print('classVec:\n', classVec)
    print('pAb:\n', pAb)


    testEntry = ['love', 'my', 'dalmation']  # 测试样本1
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))  # 测试样本向量化
    if classifyNB0(thisDoc, p0V, p1V, pAb):
        print(testEntry, '属于侮辱类')  # 执行分类并打印分类结果
    else:
        print(testEntry, '属于非侮辱类')  # 执行分类并打印分类结果

    testEntry = ['stupid', 'garbage']  # 测试样本2
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))  # 测试样本向量化
    if classifyNB0(thisDoc, p0V, p1V, pAb):
        print(testEntry, '属于侮辱类')  # 执行分类并打印分类结果
    else:
        print(testEntry, '属于非侮辱类')  # 执行分类并打印分类结果

if __name__ == "__main__":
    testingNB0()

运行结果截图如下:

小伙伴们,发现问题了吗?显然这个结果是不对的。算法存在两个问题:

  • 某些单词0概率,导致整体乘积为0,例如“”;
  • 下溢出(underflow):

对于第一个问题,我们通过打印中间结果可以看出:

又或者对一个训练数据集中未出现的词,概率也是0,显然,这样是不合理的,因为训练集有限,不能因为训练集中没有出现,就认为这个词永远不会出现。记得吴恩达老师在机器学习课上讲了个段子,斯坦福大学的校篮球队接连输了5场比赛,问下一场比赛赢的概率?连输5场,下一场肯定输吗?合理的预测是有赢的概率,只是比较小,设置赢的概率为1/7吧。这种做法就叫做拉普拉斯平滑(Laplace Smoothing)。具体做法是修改极大似然估计的参数:


即是对的估计;


即是对的估计。

其中代表的可能取值数量,我们的例子中的取值只有和两种,因此。
那么为什么分子加,分母加呢?因为要保证相加仍然是。下面我们检验一下,假设随机变量的取值有个

其中,则应用拉普拉斯平滑之后


不难推出

除此之外,另外一个遇到的问题就是下溢出,这是由于太多很小的数相乘造成的。学过数学的人都知道,两个小数相乘,越乘越小,这样就造成了下溢出。在程序中,在相应小数位置进行四舍五入,计算结果可能就变成0了。为了解决这个问题,对乘积结果取自然对数。通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。下图给出函数f(x)和ln(f(x))的曲线:

检查这两条曲线,就会发现它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。因此我们可以对上篇文章的trainNB0(trainMatrix, trainCategory)函数进行更改,修改如下:

"""
函数说明:朴素贝叶斯分类器训练函数

Parameters:
    trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
    trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
Returns:
    p0Vect - 非侮辱类的条件概率数组
    p1Vect - 侮辱类的条件概率数组
    pAbusive - 文档属于侮辱类的概率
"""
def trainNB(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix)         # 计算训练的文档数目
    numWords = len(trainMatrix[0])          # 计算每篇文档的词条数
    pAbusive = sum(trainCategory) / float(numTrainDocs)  # 文档属于侮辱类的概率
    p0Num = np.ones(numWords)
    p1Num = np.ones(numWords)               # 创建numpy.ones数组,词条出现数初始化为1,拉普拉斯平滑
    p0Denom = 2.0
    p1Denom = 2.0                           # 分母初始化为2,拉普拉斯平滑
    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 = np.log(p1Num / p1Denom)        # 取对数,防止下溢出
    p0Vect = np.log(p0Num / p0Denom)
    return p0Vect, p1Vect, pAbusive         # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率

"""
函数说明:朴素贝叶斯分类器分类函数

Parameters:
    vec2Classify - 待分类的词条数组
    p0Vec - 非侮辱类的条件概率数组
    p1Vec -侮辱类的条件概率数组
    pClass1 - 文档属于侮辱类的概率
Returns:
    0 - 属于非侮辱类
    1 - 属于侮辱类
"""
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)  # 对应元素相乘。logA * B = logA + logB,所以这里加上log(pClass1)
    p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else:
        return 0

"""
函数说明:测试朴素贝叶斯分类器

Parameters:
    无
Returns:
    无
"""
def testingNB():
    listOPosts, classVec = loadDataSet()  # 创建实验样本

    myVocabList = createVocabList(listOPosts)  # 创建词汇表

    # 打印中间结果
    print('myVocabList:\n', myVocabList)

    trainMat = []
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))  # 将实验样本向量化
    p0V, p1V, pAb = trainNB(np.array(trainMat), np.array(classVec))  # 训练朴素贝叶斯分类器

    # 打印中间结果
    print('p0V:\n', p0V)
    print('p1V:\n', p1V)
    print('classVec:\n', classVec)
    print('pAb:\n', pAb)


    testEntry = ['love', 'my', 'dalmation']  # 测试样本1
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))  # 测试样本向量化
    if classifyNB(thisDoc, p0V, p1V, pAb):
        print(testEntry, '属于侮辱类')  # 执行分类并打印分类结果
    else:
        print(testEntry, '属于非侮辱类')  # 执行分类并打印分类结果

    testEntry = ['stupid', 'garbage']  # 测试样本2
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))  # 测试样本向量化
    if classifyNB(thisDoc, p0V, p1V, pAb):
        print(testEntry, '属于侮辱类')  # 执行分类并打印分类结果
    else:
        print(testEntry, '属于非侮辱类')  # 执行分类并打印分类结果

if __name__ == "__main__":
    testingNB()

运行结果如下:

Reference

  1. Naive_Bayes_classifier wiki

  2. Gaussian Discriminant Analysis an example of Generative Learning Algorithms

  3. cs229 lecture notes Part IV Generative Learning algorithms

  4. Machine Learning in Action

  5. Jack Cui 机器学习实战教程(四):朴素贝叶斯基础篇之言论过滤器

你可能感兴趣的:(朴素贝叶斯(Naive Bayes))