@(机器学习经典算法总结)
自己的博客地址www.jameszhou.com,阅读体验更佳。
这篇博客主要介绍:
- 机器学习中参数估计方法(最大似然估计,最大后验估计);
- 利用朴素贝叶斯分类做个垃圾邮件过滤器;
机器学习中的参数估计方法主要为频率学派的最大似然估计和贝叶斯学派的最大后验估计。
对样本建模,用 θ θ 表示模型的参数,解决问题的本质就是求出 θ θ 。
- 频率派:认为数据是不确定的,参数 θ θ 是未知的定值。比如抛硬币100次,20次正面向上,频率派就认为正面向上的概率为0.2; 当数据量很大时,这种方法会给出精准的估计,但是样本量很少时,会出现过拟合。
- 贝叶斯派:认为数据能直接观测到,是确定的,参数 θ θ 是个随机变量,服从一定分布。根据贝叶斯公式利用先验和似然来求解出后验概率。
在之前的博客——线性回归一开始介绍了机器学习中应用非常广泛的参数估计方法—最大似然估计的定义以及利用最大似然估计来推导最小二乘法,在逻辑回归更新篇中利用最大似然估计,得出了逻辑回归的损失函数。
但是在线性回归中一个示例中,即对于抛硬币的例子中,进行十次实验出现七次正面,三次反面,假设正面出现概率为 p p ,对抛硬币这个例子,使用的是二项分布这个假设,利用最大似然估计求出概率 p=0.7 p = 0.7 ,很符合样本情况,但是和我们实际的经验相悖。
当时我们分析了:
我们发现解出来的概率值为0.7,很符合我们的观察数据,但是我们从过往的经验中得知,掷硬币正反面的概率各为0.5,这里却为0.7,那么问题在什么地方呢?因为样本很少,实验只做了10组。
如果在样本数很少的情况下,也想得到更符合实际情况的概率值?怎么进行参数估计呢?就引出了最大后验概率
MAP
和贝叶斯估计
两种参数估计方法了,它们在参数估计中加入了先验概率,比如加入掷硬币正反概率各为0.5的先验概率。
最大似然估计
过分关注训练数据拟合程度,在样本很少时,将错误数据也加入到模型中,非常容易造成过拟合。
贝叶斯学派的最大后验估计需要用到著名的贝叶斯公式
其中 P(θ) P ( θ ) 称为先验概率, P(x | θ) P ( x | θ ) 称为类条件概率或者似然(一般叫做似然)。 P(x) P ( x ) 为用于归一化的evidence可以忽略不计了。
所以后验概率 P(θ | x) P ( θ | x ) 可看做先验 P(θ) P ( θ ) 和似然 P(x | θ) P ( x | θ ) 的乘积。
请注意,仔细看这个式子,突然有点理解频率派和贝叶斯派所主张的不同。
最大似然估计就是使用等式右边的似然,使得:
当最大后验估计来估计高斯分布的参数时:
参数 μ μ 本身是个随机变量,通过先验条件,假设其服从均值为 u0 u 0 ,方差为 σ0 σ 0 的高斯分布
则
我们忽略参数 σ0 σ 0 ,可以看到增加的一项 (μ−μ0)2 ( μ − μ 0 ) 2 ,这实际上式对参数 μ μ 做了个L2正则了,这和《深度学习》书上所说的具有高斯先验权重的MAP贝叶斯推断对应着权重衰减
是一致的。(正则化我在下一篇笔记会做出总结,可直观理解为减少了过拟合。)
用同样的方法去估计拉普拉斯分布的参数,
拉普拉斯分布的概率密度函数为:
在之前说过最大似然估计很容易过拟合,因为其本身思想就是通过调整参数,使当前样本发生的可能性最大,对训练样本拟合程度非常高。
由bias-variance tradeoff可知,需要减小方差,增大偏差,所以加入的先验其实就增加了模型的偏差。
示例:利用朴素贝叶斯分类器构建一个垃圾邮件过滤器
先看一个单独的词汇对邮件类别的作用,词汇为 W W ,垃圾邮件Spam,正常邮件Ham。判断邮件是否是垃圾邮件,可以计算条件概率得到:
当对一整封邮件的词汇进行判断时,使用如下的概率:
先对SMSCollection.txt每行提取label(ham和spam),并进行分词,利用正则化去除标点符号,利用Map操作生成wordSpam.txt和wordHam.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 ([email protected])
# @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
- 频率学派和贝叶斯学派的参数估计
- 最大似然、参数估计
- 贝叶斯推断及其互联网应用(二):过滤垃圾邮件
- 基于贝叶斯推断的中文垃圾邮件过滤