朴素贝叶斯属于监督学习的生成模型,实现简单,没有迭代,学习效率高,在大样本量下会有较好表现。但因为假设太强——特征条件独立,在输入向量的特征条件有关联的场景下,并不适用。
朴素贝叶斯算法:主要思路是通过联合概率建模,运用贝叶斯定理求解后验概率;将后验概率最大者对应的类别作为预测类别。
首先定义训练集,其类别。
输入待预测的数据,预测类别:
由贝叶斯定理可知:
由于p(x)是恒等的,因此预测类别等价于:
最后,贝叶斯将分类问题转化成了求条件概率与先验概率的最大乘积问题。由于做了条件独立性的假设,求解公式可以转化为:
选取后验概率醉的的类别作为预测类别,是因为后验概率最大化,可以使得期望风险最小化。
极大似然估计:
在朴素贝叶斯学习中,需要估计先验概率与条件概率,一般时采用极大似然估计。先验概率的极大似然估计:
其中,是指示函数,满足括号内条件时为1否则为0;可以看作为计数
设第 j 维特征的取值空间为,且输入变量的第 j 维,则条件概率的极大似然估计为:
贝叶斯估计:
在估计先验概率与条件概率时,有可能出现为0的情况,则计算得到的后验概率亦为0,从而影响分类的效果。因此,需要在估计时做平滑,这种方法被称为贝叶斯估计。先验概率的贝叶斯估计:
后验概率的贝叶斯估计为:
当的时候,被称为拉普拉斯平滑。
以上就是朴素贝叶斯算法的数学原理。接下来附上案例代码。(详细过程见注释)
import numpy as np
from functools import reduce
#创建数据样本
#postingList的每一行,代表一个样本数据
#classVec是每个样本数据所对应的标签,0是非侮辱类,1是侮辱类
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]
return postingList,classVec
#根据vocabList词汇表,将inputSet向量化,向量的每个元素为1或0
#参数里,vocabList就是createVocabList方法最后返回的元素不重复的数组(词汇表),
#inputSet是取的原始数据集里的某一行,比如:['my', 'dog', 'has', 'flea', 'problems', 'help', 'please']
def setOfWords2Vec(vocabList,inputSet):
returnVec = [0]*len(vocabList) #创建一个所有元素都是0的数组,长度和vocabList一样,returnVec=[0,0,0,0..........0,0,0]
for word in inputSet: #将['my', 'dog', 'has', 'flea', 'problems', 'help', 'please']里的每个元素遍历
if word in vocabList: #如果某个元素在词汇表里
returnVec[vocabList.index(word)] = 1 #那就找到该元素在词汇表中的对应索引,并在returnVec里的同样索引位置赋值,值为1
# else: print("the word: %s is not in my Vocabulary!" % word)
return returnVec
# 该方法最终返回的结果如下:
# [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0]
# 其中"1"代表的索引位置,就是['my', 'dog', 'has', 'flea', 'problems', 'help', 'please']里每个元素在我的词汇表vocabList里的索引位置
#将切分的实验样本词条整理成不重复的此条例列表,也就是词汇表
#dataSet是最初的数据样本,也就是loadDataSet方法里的postingList数据集
def createVocabList(dataSet):
vocabSet = set([]) #先创建一个空的字典(set创建字典,注意字典是不能有重复元素的)
for document in dataSet: #将dataSet的每一行都进行遍历
vocabSet = vocabSet | set(document) #set(document)表示取每行的不重复数据,每次迭代后vocabSet返回值类似于:['my','dog','flea',.....]
print('词汇表为:\n',vocabSet) #"" | "是并集符号,表示将每次迭代的不重复数据集都和之前的vocabSet进行合并
return list(vocabSet) #将所有行的不重复数据都进行合并之后,最后得到整个dataSet的不重复的数据集,也就是词汇表
# 该方法最后返回的list(vocabSet)词汇表l类似如下结构:
# ['dog', 'help', 'ate', 'is', 'buying', 'him', 'mr',
# 'maybe', 'park', 'quit', 'so', 'stupid', 'stop', 'steak',
# 'licks', 'food', 'please', 'problems', 'cute', 'I', 'flea',
# 'posting', 'love', 'how', 'dalmation', 'take', 'garbage', 'not',
# 'my', 'to', 'has', 'worthless']
#分类器训练函数
# 其中trainMatrix是setOfWords2Vec方法返回的returnVec所组成的矩阵,如下:
# [[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,1,0,1,0],
# [1,0,0,0,0,1,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0],
# [0,0,0,1,0,1,0,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,1,0,1,0,0,0,1,0,0,0],
# [0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],
# [0,0,1,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,1,1,0,0],
# [1,0,0,0,1,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]]
# trainCategory表示类别标签向量,即loadDataSet方法里的classVec,classVec=[0,1,0,1,0,1]
#该方法最终将出现错误
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix) #返回文档数目,numTrainDocs = 6
numWords = len(trainMatrix[0]) #计算词汇表里的词条数,numWords = 32
pAbusive = sum(trainCategory)/float(numTrainDocs) #计算侮辱类文档占总文档的概率(比例)pAbusive = 3/6 = 0.5
p0Num = np.zeros(numWords);p1Num = np.zeros(numWords) #创建两个numpy数组,每个元素都是0,长度为32
p0Denom = 0.0; p1Denom = 0.0 #初始化两个数,这两个数是之后算条件概率的分母,p0Denom是非侮辱性相对应的条件概率的分母,p1Denom则是侮辱性的分母
for i in range(numTrainDocs): #对类别标签进行遍历,也就是对[0,1,0,1,0,1]的每个元素进行遍历,0代表非侮辱,1代表侮辱
if trainCategory[i]==1: #如果是侮辱的
p1Num += trainMatrix[i] #那就把trainMatrix矩阵中对应的那一行的标签累加到p1Num上,
p1Denom += sum(trainMatrix[i]) #然后,将所有词出现的次数累加到p1Denom上
#最后p1Num得到的结果是:
# p1Num = [2. 0. 0. 0. 1. 1. 0. 1. 1. 1. 0. 3. 1. 0. 0. 1. 0. 0. 0. 0. 0. 1. 0. 0. 0. 1. 1. 1. 0. 1. 0. 2.]
# 该数组的意思:在整个词条中,第一个词出现的频次为2,第二个词频次为0,以此类推
#p1Denom=19,意思是侮辱性文档中所有词汇出现的总次数,也就是p1Num中所有元素相加,p1Denom = 19
else: #如果是非侮辱的,同上所述,将得到非侮辱性文档的结果,
p0Num += trainMatrix[i] #p0Num = [1. 1. 1. 1. 0. 2. 1. 0. 0. 0. 1. 0. 1. 1. 1. 0. 1. 1. 1. 1. 1. 0. 1. 1. 1. 0. 0. 0. 3. 1. 1. 0.]
p0Denom += sum(trainMatrix[i]) #p0Denom = 24
p1Vect = p1Num/p1Denom #最后需要分别计算在侮辱类和非侮辱类中,每个词出现的概率,
p0Vect = p0Num/p0Denom #比如对于侮辱类,第一个词出现两次,除以所有侮辱类的次数19.得到2/19=0.10526316
return p0Vect,p1Vect,pAbusive #p0Vect是非侮辱类每个词出现的概率,p1Vect是侮辱类每个词出现的概率,pAbusive是侮辱类文档占总文档的比例
#该方法最后返回的结果将是:
# p1Vect = [0.10526316 0. 0. 0. 0.05263158 0.05263158
# 0. 0.05263158 0.05263158 0.05263158 0. 0.15789474
# 0.05263158 0. 0. 0.05263158 0. 0.
# 0. 0. 0. 0.05263158 0. 0.
# 0. 0.05263158 0.05263158 0.05263158 0. 0.05263158
# 0. 0.10526316]
# p0Vect = [0.04166667 0.04166667 0.04166667 0.04166667 0. 0.08333333
# 0.04166667 0. 0. 0. 0.04166667 0.
# 0.04166667 0.04166667 0.04166667 0. 0.04166667 0.04166667
# 0.04166667 0.04166667 0.04166667 0. 0.04166667 0.04166667
# 0.04166667 0. 0. 0. 0.125 0.04166667
# 0.04166667 0. ]
# pAbusive = 3/6 = 0.5
#修正后的贝叶斯分类器训练函数
def trainNB1(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:
p1Num += trainMatrix[i] #p1Num=[1. 1. 2. 2. 1. 2. 2. 1. 1. 3. 1. 2. 1. 1. 1. 1. 3. 2. 2. 1. 1. 1. 2. 2. 1. 2. 2. 1. 1. 4. 1. 2.]
p1Denom += sum(trainMatrix[i]) #p1Denom = 21
else:
p0Num += trainMatrix[i] #p0Num=[2. 2. 1. 1. 2. 1. 2. 2. 2. 1. 2. 1. 2. 2. 2. 2. 2. 1. 1. 2. 2. 2. 1. 2. 2. 1. 3. 4. 2. 1. 2. 1.]
p0Denom += sum(trainMatrix[i]) #p0Denom = 26
p1Vect = np.log(p1Num/p1Denom) #取对数,防止下溢出
print('p1Vect:\n',p1Vect)
p0Vect = np.log(p0Num/p0Denom)
print('p0Vect:\n',p0Vect)
return p0Vect,p1Vect,pAbusive
#该函数最后返回的p0Vect,p1Vect结果是:(相比之前的训练器,将初始数组的元素0改成1,分母初始值由0改为2,然后概率取对数,就得到以下结果
# 元素0改为1,属于拉普拉斯平滑,概率取对数是防止下溢出)
# p0Vect = [-2.56494936 -2.56494936 -3.25809654 -3.25809654 -2.56494936 -3.25809654
# -2.56494936 -2.56494936 -2.56494936 -3.25809654 -2.56494936 -3.25809654
# -2.56494936 -2.56494936 -2.56494936 -2.56494936 -2.56494936 -3.25809654
# -3.25809654 -2.56494936 -2.56494936 -2.56494936 -3.25809654 -2.56494936
# -2.56494936 -3.25809654 -2.15948425 -1.87180218 -2.56494936 -3.25809654
# -2.56494936 -3.25809654]
# p0Vect = [-3.04452244 -3.04452244 -2.35137526 -2.35137526 -3.04452244 -2.35137526
# -2.35137526 -3.04452244 -3.04452244 -1.94591015 -3.04452244 -2.35137526
# -3.04452244 -3.04452244 -3.04452244 -3.04452244 -1.94591015 -2.35137526
# -2.35137526 -3.04452244 -3.04452244 -3.04452244 -2.35137526 -2.35137526
# -3.04452244 -2.35137526 -2.35137526 -3.04452244 -3.04452244 -1.65822808
# -3.04452244 -2.35137526]
#朴素贝叶斯分类器分类函数
#vec2Classify是待测试的词条
#p0Vec是非侮辱类的条件概率数组
#p1Vec是侮辱类的条件概率数组
#pClass1是文档属于侮辱类的概率
def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1) #计算该文档属于侮辱类的概率
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1) #计算该文档属于非侮辱类的概率
print('属于p0的概率取对数:',p0) #注意上面两行的计算公式省略了贝叶斯公式里的分母
print('属于p1的概率取对数:',p1) #因为不管是计算p0还是p1,分母都是一样的,而我们要比较概率的大小,只需要比较分子即可
if p1 > p0:
return 1 #如果p1大于p0,说明属于侮辱类的可能性更大,返回1,(1代表侮辱性)
else:
return 0 #反之,则返回0(代表非侮辱性)
def testingNB():
listOPosts,listClasses = loadDataSet() #创建实验样本
myVocabList = createVocabList(listOPosts) #创建词汇表
trainMat=[]
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) #将实验样本向量化
p0V,p1V,pAb = trainNB1(np.array(trainMat),np.array(listClasses)) #训练朴素贝叶斯分类器
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()
最后打印信息,并分类得出的结果是:
词汇表为:
{'to', 'problems', 'help', 'buying', 'park', 'dalmation', 'how', 'I', 'is', 'posting', 'stupid', 'ate', 'mr', 'has', 'stop', 'take', 'quit', 'him', 'garbage', 'my', 'steak', 'not', 'cute', 'maybe', 'flea', 'love', 'dog', 'worthless', 'food', 'so', 'licks', 'please'}
p1Vect:
[-2.35137526 -3.04452244 -3.04452244 -2.35137526 -2.35137526 -3.04452244
-3.04452244 -3.04452244 -3.04452244 -2.35137526 -1.65822808 -3.04452244
-3.04452244 -3.04452244 -2.35137526 -2.35137526 -2.35137526 -2.35137526
-2.35137526 -3.04452244 -3.04452244 -2.35137526 -3.04452244 -2.35137526
-3.04452244 -3.04452244 -1.94591015 -1.94591015 -2.35137526 -3.04452244
-3.04452244 -3.04452244]
p0Vect:
[-2.56494936 -2.56494936 -2.56494936 -3.25809654 -3.25809654 -2.56494936
-2.56494936 -2.56494936 -2.56494936 -3.25809654 -3.25809654 -2.56494936
-2.56494936 -2.56494936 -2.56494936 -3.25809654 -3.25809654 -2.15948425
-3.25809654 -1.87180218 -2.56494936 -3.25809654 -2.56494936 -3.25809654
-2.56494936 -2.56494936 -2.56494936 -3.25809654 -3.25809654 -2.56494936
-2.56494936 -2.56494936]
属于p0的概率取对数: -7.694848072384611
属于p1的概率取对数: -9.826714493730215
['love', 'my', 'dalmation'] 属于非侮辱类
属于p0的概率取对数: -7.20934025660291
属于p1的概率取对数: -4.702750514326955
['stupid', 'garbage'] 属于侮辱类