贝叶斯分类器

贝叶斯分类器

@(机器学习经典算法总结)

自己的博客地址www.jameszhou.com,阅读体验更佳。


这篇博客主要介绍:
- 机器学习中参数估计方法(最大似然估计,最大后验估计);
- 利用朴素贝叶斯分类做个垃圾邮件过滤器;


机器学习中参数估计方法

机器学习中的参数估计方法主要为频率学派的最大似然估计和贝叶斯学派的最大后验估计。
对样本建模,用 θ θ 表示模型的参数,解决问题的本质就是求出 θ θ
- 频率派:认为数据是不确定的,参数 θ θ 是未知的定值。比如抛硬币100次,20次正面向上,频率派就认为正面向上的概率为0.2; 当数据量很大时,这种方法会给出精准的估计,但是样本量很少时,会出现过拟合。
- 贝叶斯派:认为数据能直接观测到,是确定的,参数 θ θ 是个随机变量,服从一定分布。根据贝叶斯公式利用先验和似然来求解出后验概率。

MLE(最大似然估计)

在之前的博客——线性回归一开始介绍了机器学习中应用非常广泛的参数估计方法—最大似然估计的定义以及利用最大似然估计来推导最小二乘法,在逻辑回归更新篇中利用最大似然估计,得出了逻辑回归的损失函数。
但是在线性回归中一个示例中,即对于抛硬币的例子中,进行十次实验出现七次正面,三次反面,假设正面出现概率为 p p ,对抛硬币这个例子,使用的是二项分布这个假设,利用最大似然估计求出概率p=0.7,很符合样本情况,但是和我们实际的经验相悖。
当时我们分析了:

我们发现解出来的概率值为0.7,很符合我们的观察数据,但是我们从过往的经验中得知,掷硬币正反面的概率各为0.5,这里却为0.7,那么问题在什么地方呢?因为样本很少,实验只做了10组。

如果在样本数很少的情况下,也想得到更符合实际情况的概率值?怎么进行参数估计呢?就引出了最大后验概率MAP贝叶斯估计两种参数估计方法了,它们在参数估计中加入了先验概率,比如加入掷硬币正反概率各为0.5的先验概率。

最大似然估计过分关注训练数据拟合程度,在样本很少时,将错误数据也加入到模型中,非常容易造成过拟合。

最大后验估计

贝叶斯学派的最大后验估计需要用到著名的贝叶斯公式

P(θ | x)=P(θ)P(x | θ)P(x) P ( θ   |   x ) = P ( θ ) P ( x   |   θ ) P ( x )

其中 P(θ) P ( θ ) 称为先验概率, P(x | θ) P ( x   |   θ ) 称为类条件概率或者似然(一般叫做似然)。 P(x) P ( x ) 为用于归一化的evidence可以忽略不计了。
所以后验概率 P(θ | x) P ( θ   |   x ) 可看做先验 P(θ) P ( θ ) 和似然 P(x | θ) P ( x   |   θ ) 的乘积。

P(θ | x)=P(θ)P(x | θ) P ( θ   |   x ) = P ( θ ) ∗ P ( x   |   θ )

请注意,仔细看这个式子,突然有点理解频率派和贝叶斯派所主张的不同。
最大似然估计就是使用等式右边的似然,使得:

θ=argmax(P(x | θ)) θ = a r g m a x ( P ( x   |   θ ) )

而最大后验估计是:
θ=argmax(P(θ | x)) θ = a r g m a x ( P ( θ   |   x ) )

因为最大似然估计是将参数 θ θ 看做个定值,将数据x看做未知的,所以使用条件概率 P(x | θ) P ( x   |   θ ) 最大化,即利用一个定值参数 θ θ 使得未知数据x发生可能性最大,来得到最佳的参数值。
而最大后验估计反过来将数据看做是确定的,参数是个随机变量,具有一定的分布,用已知数据估计参数的分布,实际 P(θ | x) P ( θ   |   x ) 对应了一个后验分布,而最大后验估计只是取了后验分布的峰值。
利用贝叶斯公式得到了
argmax(P(θ | x))=argmax(P(x | θ)P(θ)) a r g m a x ( P ( θ   |   x ) ) = a r g m a x ( P ( x   |   θ ) ∗ P ( θ ) )

从之前的示例可看出,最大似然可能会对样本过拟合,那么加入了先验是否会改善过拟合呢?
高斯分布的概率密度函数为
f(x)=12πσexp((xμ)22σ2) f ( x ) = 1 2 π σ e x p ( − ( x − μ ) 2 2 σ 2 )

则对数似然函数
log L(x | μ,σ)=i=1nlog(12πσ)i=1n(xiμ)22σ2 l o g   L ( x   |   μ , σ ) = ∑ i = 1 n l o g ( 1 2 π σ ) − ∑ i = 1 n ( x i − μ ) 2 2 σ 2

当最大后验估计来估计高斯分布的参数时:
参数 μ μ 本身是个随机变量,通过先验条件,假设其服从均值为 u0 u 0 ,方差为 σ0 σ 0 的高斯分布

P(μ | μ0,σ0)=12πσ0exp((μμ0)22σ20) P ( μ   |   μ 0 , σ 0 ) = 1 2 π σ 0 e x p ( − ( μ − μ 0 ) 2 2 σ 0 2 )

将上式代入
log(P(μ | μ0,σ0,x))=+(μμ0)22σ20log(12πσ0) l o g ( P ( μ   |   μ 0 , σ 0 , x ) ) = 最 大 似 然 + ( μ − μ 0 ) 2 2 σ 0 2 − l o g ( 1 2 π σ 0 )

我们忽略参数 σ0 σ 0 ,可以看到增加的一项 (μμ0)2 ( μ − μ 0 ) 2 ,这实际上式对参数 μ μ 做了个L2正则了,这和《深度学习》书上所说的具有高斯先验权重的MAP贝叶斯推断对应着权重衰减是一致的。(正则化我在下一篇笔记会做出总结,可直观理解为减少了过拟合。)
用同样的方法去估计拉普拉斯分布的参数,
拉普拉斯分布的概率密度函数为:

f(x)=12λexp(|xμ|λ) f ( x ) = 1 2 λ e x p ( − | x − μ | λ )

可得出:
log(MAP)=MLE+k|μμ0| l o g ( M A P ) = M L E + k ∗ | μ − μ 0 |

这就在最大似然基础上做了个L1正则了,可以自己推导一下,基本是照葫芦画瓢。
简单说一下过拟合及偏差和方差,
- 偏差(bias)度量了学习算法的期望预测与真实结果的偏离程度,刻画了学习算法本身的拟合能力;
- 方差(variance)表示同样大小的训练集的变动导致的学习性能的变化,刻画了数据扰动造成的影响。

在之前说过最大似然估计很容易过拟合,因为其本身思想就是通过调整参数,使当前样本发生的可能性最大,对训练样本拟合程度非常高。
由bias-variance tradeoff可知,需要减小方差,增大偏差,所以加入的先验其实就增加了模型的偏差。


利用朴素贝叶斯分类做个垃圾邮件过滤器

P(c | x)=P(x | c)P(c)P(x) P ( c   |   x ) = P ( x   |   c ) ∗ P ( c ) P ( x )

朴素贝叶斯分类器,做了一个 属性条件独立性假设,对给定类别,假设所有属性相互独立,即各属性独立地对分类结果产生影响。
此时,
P(x | c)=i=1dP(xi | c) P ( x   |   c ) = ∏ i = 1 d P ( x i   |   c )

P(c) P ( c ) 可以从数据中计算得到, P(x) P ( x ) 利用全概率公式计算可得。


示例:利用朴素贝叶斯分类器构建一个垃圾邮件过滤器
先看一个单独的词汇对邮件类别的作用,词汇为 W W ,垃圾邮件Spam,正常邮件Ham。判断邮件是否是垃圾邮件,可以计算条件概率得到:

P(S | W)=P(W | S)P(S)P(W | S)P(S)+P(W | H)P(H)

  • P(S|W) P ( S | W ) 表示词汇W的邮件是垃圾邮件的条件概率(即后验概率);
  • P(S) P ( S ) 表示训练阶段邮件数据集中垃圾邮件的概率(即先验概率);
  • P(W|S) P ( W | S ) 表示垃圾邮件中词汇W出现的概率;
  • P(H) P ( H ) 表示训练阶段邮件数据集中正常邮件的概率;
  • P(W|H) P ( W | H ) 表示 正常邮件中词汇W出现的概率

当对一整封邮件的词汇进行判断时,使用如下的概率:

P=p1p2..pNp1p2..pN+(1p1)(1p2)...(1pN) P = p 1 p 2 . . p N p 1 p 2 . . p N + ( 1 − p 1 ) ( 1 − p 2 ) . . . ( 1 − p N )

其中 pi p i 表示词汇对应垃圾邮件的概率。
我们使用的邮件数据集(SMSCollection.txt)包含747封垃圾邮件,4827封正常邮件。

先对SMSCollection.txt每行提取label(ham和spam),并进行分词,利用正则化去除标点符号,利用Map操作生成wordSpam.txtwordHam.txt
wordSpam.txt和wordHam.txt做reduce操作,统计各个词在正常垃圾和正常邮箱的词频,放在result.txt
如下所示,依次为word,在垃圾邮件出现次数,在正常邮件出现次数

代码如下:


#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date    : 2018-03-28 10:29:00
# @Author  : guanglinzhou (xdzgl812@163.com)
# @Link    : https://github.com/GuanglinZhou
# @Version : $Id$

import re
from collections import defaultdict
import math

'''
先对SMSCollection.txt每行提取label(ham和spam),并进行分词,利用正则化去除标点符号,生成wordSpam.txt和wordHam.txt
对wordSpam.txt和wordHam.txt做reduce操作,统计各个词在正常垃圾和正常邮箱的词频,放在result.txt

'''


def loadDataSet():
    regEx = re.compile('\\W*')
    wordSpamList = []
    wordHamList = []
    numSpamMail = 0
    numHamMail = 0
    with open("SMSCollection.txt", "r") as file:
        for line in file.readlines():
            wordString = line.strip()
            wordLine = regEx.split(wordString)
            if (wordLine[0] == 'spam'):
                numSpamMail += 1
                for word in wordLine[1:-1]:
                    wordSpamList.append(word)
            else:
                numHamMail += 1
                for word in wordLine[1:-1]:
                    wordHamList.append(word)
        print('有{}封垃圾邮件'.format(numSpamMail))
        print('有{}封正常邮件'.format(numHamMail))
        wordSpamList = [tok for tok in wordSpamList if (len(tok) > 0)]
        wordHamList = [tok for tok in wordHamList if (len(tok) > 0)]

    with open("wordSpam.txt", "w") as file:
        for word in wordSpamList:
            file.write(word)
            file.write('\n')

    with open("wordHam.txt", "w") as file:
        for word in wordHamList:
            file.write(word)
            file.write('\n')

    wordDict = defaultdict(lambda: defaultdict(lambda: 0))
    with open("wordSpam.txt", 'r') as file:
        for line in file.readlines():
            word = line.strip()
            wordDict[word]['Spam'] += 1

    with open("wordHam.txt", 'r') as file:
        for line in file.readlines():
            word = line.strip()
            wordDict[word]['Ham'] += 1

    with open("result.txt", 'w') as file:
        for word in wordDict:
            file.write(word)
            file.write(',')
            file.write(str(wordDict[word]['Spam']))
            file.write(',')
            file.write(str(wordDict[word]['Ham']))
            file.write('\n')

    return wordDict, numSpamMail, numHamMail


# 测试某封邮件是否是垃圾邮件
def testMail(testFileName):
    regEx = re.compile('\\W*')
    wordTestList = []
    with open(testFileName, "r") as file:
        for line in file.readlines():
            wordString = line.strip()
            wordLine = regEx.split(wordString)
            for word in wordLine:
                wordTestList.append(word)
        wordTestList = [tok for tok in wordTestList if (len(tok) > 0)]
    spamProbaList = []
    hamProbaList = []
    # 对于测试邮件的每个word都计算其条件概率,并保存在probaList中
    for wordTest in wordTestList:
        spamProbaList.append(probaCompute(wordDict, wordTest, numSpamMail, numHamMail)[0])
        hamProbaList.append(probaCompute(wordDict, wordTest, numSpamMail, numHamMail)[1])

    TopK = 5
    spamLogProbaResult = 0
    for logproba in spamProbaList[:TopK]:
        spamLogProbaResult += logproba
    hamLogProbaResult = 0
    for logproba in hamProbaList[:TopK]:
        hamLogProbaResult += logproba
    spamProbaResult = math.exp(spamLogProbaResult)
    hamProbaResult = math.exp(hamLogProbaResult)
    return spamProbaResult, hamProbaResult


# 返回某个词对应垃圾邮件和正常邮件的概率
def probaCompute(wordDict, wordTest, numSpamMail, numHamMail):
    if (wordTest not in wordDict.keys()):
        probaWordOfSpam = 0.5
        probaWordOfHam = 0.5
    else:
        probaWordOfSpam = wordDict[wordTest]['Spam'] / numSpamMail
        if (probaWordOfSpam == 0):
            probaWordOfSpam += 1
        probaWordOfHam = wordDict[wordTest]['Ham'] / numHamMail
        if (probaWordOfHam == 0):
            probaWordOfHam += 1

    probaHam = numHamMail / (numSpamMail + numHamMail)
    probaSpam = numSpamMail / (numSpamMail + numHamMail)
    probaWord = probaWordOfSpam * probaSpam + probaWordOfHam * probaHam

    return math.log(probaWordOfSpam * probaSpam / probaWord), math.log(probaWordOfHam * probaSpam / probaWord)


if __name__ == '__main__':
    wordDict, numSpamMail, numHamMail = loadDataSet()
    spamProbaResult, hamProbaResult = testMail('testMail.txt')
    print("该邮件为垃圾邮件概率为{}".format(spamProbaResult / (spamProbaResult + hamProbaResult)))
    print("该邮件为正常邮件概率为{}".format(hamProbaResult / (spamProbaResult + hamProbaResult)))

两点注意(在程序中也可以看到):
- 可能邮件中存在某个词是训练集中未出现过的,如果不做处理,可知 P(W | S)=0 P ( W   |   S ) = 0 ,使得结果不准确的,对没有出现过的词语使得概率为0.5;
- 使用概率连乘,出现了数值下溢情况,所以使用log求和,得到了结果再通过指数函数求得最终的概率;

使用的测试邮件内容为:






项目地址:
GitHub

参考博客
- 详解最大似然估计(MLE)、最大后验概率估计(MAP),以及贝叶斯公式的理解中对频率派和贝叶斯派举的示例非常简单生动,易于理解。
- Google工程师做的简述,值得阅读——频率学派还是贝叶斯学派?聊一聊机器学习中的MLE和MAP
- 频率学派和贝叶斯学派的参数估计
- 最大似然、参数估计
- 贝叶斯推断及其互联网应用(二):过滤垃圾邮件
- 基于贝叶斯推断的中文垃圾邮件过滤

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值