概率论是许多机器学习算法的基础。贝叶斯算法是一类算法,这是一类以条件概率的计算为核心进行分类的算法,而朴素贝叶斯算法是其中最简单的概率分类器。之所以称之为朴素,是因为整个形式化过程只做最原始、最简单的假设,并且假设数据中的特征间都是不相关的。
我们现在有一个数据集,它由两类数据组成,数据分布如下图所示:
我们现在用 p1(x,y) 表示数据点 (x,y) 属于类别 1(图中用圆点表示的类别)的概率,用 p2(x,y) 表示数据点 (x,y) 属于类别 2(图中三角形表示的类别)的概率,那么对于一个新数据点 (x,y),可以用下面的规则来判断它的类别:
- 如果 p1(x,y) > p2(x,y) ,那么类别为1
- 如果 p2(x,y) > p1(x,y) ,那么类别为2
也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策。
贝叶斯方法的基本概念
1. 事件
统计学中用“事件”表示有概率可言的任何事情,也就是说,事件是人们能指出发生可能性的任何事情。比如天气为晴天和非晴天,收到垃圾邮件和非垃圾邮件。
2. 概率
一个事件发生的概率可以通过观测到的数据来估计,即用该事件发生的试验的次数除以试验的总次数。通常用符号p(A)来表示事件A发生的概率。
例如,如果10天中有3天下雨了,那么就可以估计下雨的概率为30%。同样,如果50封电子邮件中有10封是垃圾邮件,那么可以估计垃圾邮件的概率为20%,即P(垃圾邮件)=0.2。
3. 联合概率
联合概率是指多个条件同时成立的概率。特别的,如果这两个条件是独立的,如抛硬币结果和天气是否下雨可以认为是独立的,则
4. 贝叶斯定理
p(c)称为先验概率,p(c|x)称为后验概率
如下图,有7个球,其中3个白球,4个黑球,随机取出一个球,那么取到白球的概率是3/7,取到黑球的概率是4/7。
可是,如果把这7个球放入两个桶中,等于是事先加了一个条件,又该怎么计算呢?
根据条件概率计算公式,P(黑球|桶A)= P(桶A ,黑球)/P(桶A)
易知,P(黑球)= 4/7,P(桶A)= 4/7(球在桶A中的概率)
P(桶A,黑球)是指黑球并且在桶A的概率,用桶A中黑球数除以所有球的总数=2/7
所以,可以求得P(黑球|桶A)= (2/7)/(4/7)=1/2,因此桶A中取到黑球的概率是1/2,这也与我们的观察一致。
也可以用贝叶斯定理来求:P(黑球|桶A)= P(桶A|黑球)* P(黑球)/ P(桶A)
P(桶A|黑球)是指黑球中在桶A的概率=2/4=1/2,P(黑球)= 4/7,P(桶A)= 4/7
所以P(黑球|桶A)= (1/2*4/7)/(4/7) = 1/2,结果和用条件概率求解的一样。
具体案例
按照书中所说,以在线社区留言板为例。为了不影响社区的发展,我们要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标识为内容不当。对此问题建立两个类别:侮辱类和非侮辱类,使用1和0分别表示。
现在需求是这样的,给我们一个留言,需要判断是否是侮辱性的。输入是一些预先分好类的留言和分类结果
加载数据
def loadDataSet():
"""
创建数据集
:return: 单词列表postingList, 所属类别classVec
"""
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] # 1代表有侮辱性,0代表没有侮辱性
return postingList,classVec
创建不重复的词典表
def createVocabList(dataSet):
"""
获取所有单词的集合
:param dataSet: 数据集
:return: 所有单词的集合(即不含重复元素的单词列表)
"""
vocabSet = set([])
for document in dataSet:
# 求并集
vocabSet = vocabSet|set(document)
# 返回单位的列表
return list(vocabSet)
dataSet,classVec = loadDataSet()
vocabList = createVocabList(dataSet)
print(vocabList)
输出
['licks', 'maybe', 'I', 'quit', 'posting', 'cute', 'dog', 'flea', 'help', 'park', 'is', 'worthless', 'take', 'my', 'so', 'ate', 'steak', 'has', 'dalmation', 'love', 'buying', 'not', 'to', 'problems', 'stop', 'mr', 'how', 'food', 'stupid', 'garbage', 'him', 'please']
使用词典把输入的留言转换成0,1向量
def setOfWords2Vec(vocabList,inputSet):
"""
遍历查看该单词是否出现,出现该单词则将该单词置1
:param vocabList: 所有单词集合列表
:param inputSet: 输入数据集
:return: 匹配列表[0,1,0,1...],其中 1与0 表示词汇表中的单词是否出现在输入的数据集中
"""
# 创建一个和词汇表长度相同的列表,其元素全部赋为0
returnVec = [0]*len(vocabList)
# 遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
print("%s is not in the vocabList"%word)
return returnVec
原版的训练函数,其实就是求概率
# 训练数据原版
def trainNB0(trainMatrix,trainCategory):
"""
训练数据原版
:param trainMatrix: 文件单词矩阵 [[1,0,1,1,1....],[],[]...]
:param trainCategory: 文件对应的类别[0,1,1,0....],列表长度等于单词矩阵数,其中的1代表对应的文件是侮辱性文件,0代表不是侮辱性矩阵
:return:
"""
numTrainDocs = len(trainMatrix) # 文件数量
words = len(trainMatrix[0]) # 文件中的单词数
# 侮辱性文件的出现概率,即trainCategory中所有的1的个数,
# 代表的就是多少个侮辱性文件,与文件的总数相除就得到了侮辱性文件的出现概率
pAbusive = np.sum(trainCategory)/float(numTrainDocs)
# 构造单词出现次数列表
p0Num = zeros(words) # [0,0,0,.....]
p1Num = zeros(words) # [0,0,0,.....]
# 整个数据集单词出现总次数
p0Denom = 0.0
p1Denom = 0.0
for i in range(numTrainDocs):
# 遍历所有的文件,如果是侮辱性文件,就计算此侮辱性文件中出现的侮辱性单词的个数
if trainCategory[i]==1:
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
# 如果不是侮辱性文件,统计侮辱性单词的个数
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
# 统计在侮辱性文件的类别下,每个单词的出现次数。这里p1Num是一个数组,p1Denom是一个数字
p1Vect = p1Num/p1Denom
# 统计在正常文件的类别下
p0Vect = p0Num/p0Denom
return p0Vect,p1Vect,pAbusive
训练数据改进版
其实就是把p0Num和p1Num改成ones(words),p0Denom和p1Denom改成2.0,防止因为某一项为0,使得概率的乘积为0
# 改进的训练数据方法,其实就是取了对数
def trainNB(trainMatrix,trainCategory):
"""
训练数据优化版本
:param trainMatrix: 文件单词矩阵
:param trainCategory: 文件对应的类别
:return:
"""
numTrainDocs = len(trainMatrix)
words = len(trainMatrix[0])
pAbusive = sum(trainCategory)/float(numTrainDocs)
# 这里就和原版有区别了。避免单词列表中的任何一个单词为0,而导致最后的乘积为0,所以将每个单词的出现次数初始化为1
# p(w|c),这里w是一个向量,因为是朴素贝叶斯,所以p(w|c)=p(w1|c)*p(w2|c)*...*p(wn|c)
# 而最后要计算的是p(w|c)p(c)/p(w)其实就是p(w1|c)*p(w2|c)*...*p(wn|c)*p(c)/p(w),而p(w)对于两类来说都一样,比较大小时可以不考虑
# p(w1|c)就是分类为c的类别中,w1出现的次数/所有词表中单词的出现次数,也就是这里的p1Num/p1Denom和p0Num/p0Denom
p0Num = ones(words)
p1Num = ones(words)
# 这里设置p0Denom和p1Denom为2.0,主要是为了避免除法的分母为0,这个值可以随便设置
p0Denom = 2.0
p1Denom = 2.0
for i in range(numTrainDocs):
if trainCategory[i]==1:
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
# 求对数,就是为了接下来的分类方法需要把乘法改成加法
p1Vect = log(p1Num/p1Denom)
p0Vect = log(p0Num/p0Denom)
return p0Vect,p1Vect,pAbusive
实际的分类方法,就是比较那个概率大,概率大就取那个分类
# 分类方法
'''
:param vec2Classify: 待测数据[0,1,1,1,1...],即要分类的向量
:param p0Vec: 类别0,即正常文档的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表
:param p1Vec: 类别1,即侮辱性文档的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表
:param pClass1: 类别1,侮辱性文件的出现概率
:return: 类别1 or 0
'''
def classNB(vec,p0Vect,p1Vect,pClass1):
# 这里通过取对数把乘法转换成了加法
'''
将乘法转换为加法
乘法:P(C | F1F2...Fn) = P(F1F2...Fn | C)P(C) / P(F1F2...Fn)
加法:P(F1 | C) * P(F2 | C)....P(Fn | C)P(C) -> log(P(F1 | C)) + log(P(F2 | C)) + .... + log(P(Fn | C)) + log(P(C))
'''
p1 = sum(vec*p1Vect)+log(pClass1)
p0 = sum(vec*p0Vect)+log(1.0-pClass1)
if p1>p0:
return 1
else:
return 0
测试方法
# 测试方法
def testNB():
# 1.加载数据集
listPosts,listClass = loadDataSet()
# 2.创建单词集合
myVocabList = createVocabList(listPosts)
# 3.计算单词是否出现并创建数据矩阵
trainMat = []
for postinDoc in listPosts:
# 返回m*len(myVocabList)的矩阵,元素都是0,1
trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
# 4.训练数据
pv0,pv1,pAb = trainNB(array(trainMat),array(listClass))
# 5.测试数据
testEntry = ['love','my','dalmation']
thisDoc = array(setOfWords2Vec(myVocabList,testEntry))
print("%s classified as: %s"%(testEntry,classNB(thisDoc,pv0,pv1,pAb)))
testEntry = ['stupid','garbage']
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print("%s classified as: %s" % (testEntry, classNB(thisDoc, pv0, pv1, pAb)))
testNB()
输出结果:
['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1