简介
朴素贝叶斯是经典的机器学习算法之一,也是为数不多的基于概率论的分类算法。对于大多数的分类算法,在所有的机器学习分类算法中,朴素贝叶斯和其他绝大多数的分类算法都不同。比如决策树,KNN,逻辑回归,支持向量机等,他们都是判别方法,也就是直接学习出特征输出Y和特征X之间的关系,要么是决策函数,要么是条件分布。但是朴素贝叶斯却是生成方法,该算法原理简单,也易于实现。
一、朴素贝叶斯算法
朴素贝叶斯是贝叶斯分类器的一个扩展,是用于文档分类的常用算法。 贝叶斯分类是一类分类算法的总称,这类算法均以贝叶斯定理为基础,故统称为贝叶斯分类。而朴素贝叶斯分类时贝叶斯分类中最简单,也是最常见的一种分类方法。
1.1贝叶斯定理
先验概率:即基于统计的概率,是基于以往历史经验和分析得到的结果,不需要依赖当前发生的条件。
后验概率:则是从条件概率而来,由因推果,是基于当下发生了事件之后计算的概率,依赖于当前发生的条件。
条件概率:记事件A发生的概率为P(A),事件B发生的概率为P(B),则在B事件发生的前提下,A事件发生的概率即为条件概率,记为P(A|B)。
由老师上课举的例子去,我们可以通过如下图片清晰的明白条件概率如何计算:
联合概率:表示两个事件共同发生的概率。A与B的联合概率表示为P(AB),或者P(A∩B)。
贝叶斯公式:贝叶斯公式便是基于条件概率,通过P(B|A)来求P(A|B),如下:
其中,P(A|B)为后验概率,P(B|A)为条件概率,P(A)为先验概率。
全概率公式:表示若事件构成一个完备事件组且都有正概率,则对任意一个事件B都有公式成立:
将全概率公式代入贝叶斯公式中,得到:
1.2朴素贝叶斯算法的原理
特征条件假设:假设每个特征之间没有联系,给定训练数据集,其中每个样本都包括维特征,即,类标记集合含有种类别,即。
对于给定的新样本,判断其属于哪个标记的类别,根据贝叶斯定理,可以得到属于类别的概率:
后验概率最大的类别记为预测类别,即:。
朴素贝叶斯算法对条件概率分布作出了独立性的假设,通俗地讲就是说假设各个维度的特征x1,x2,⋯,xn互相独立,在这个假设的前提上,条件概率可以转化为:
代入贝叶斯公式中,得到:
于是,朴素贝叶斯分类器可表示为:
因为对所有的,上式中的分母的值都是一样的,所以可以忽略分母部分,朴素贝叶斯分类器最终表示为:
1.3拉普拉斯平滑
为了解决零概率的问题,法国数学家拉普拉斯最早提出用加1的方法估计没有出现过的现象的概率,所以加法平滑也叫做拉普拉斯平滑。假定训练样本很大时,每个分量的计数加1造成的估计概率变化可以忽略不计,但可以方便有效的避免零概率问题。
是一个多项乘法公式,其中有一项数值为0,则整个公式就为0显然不合理,避免每一项为零的做法就是在分子、分母上各加一个数值。
,表示分类y的样本数,|D|为样本总数,N是分类总数。
,表示分类y的样本数,表示分类y的样本数,表示i属性的可能的取值数。
在实际的使用中也经常使用加λ(0≤λ≤1)来代替简单加1。 如果对N个计数都加上λ,这时分母也要记得加上N×λ。
1.4防溢出策略
条件概率乘法计算过程中,因子一般较小(均是小于1的实数)。当属性数量增多时候,会导致累乘结果下溢出的现象。在代数中有 ,因此可以把条件概率累乘转化成对数累加。分类结果仅需对比概率的对数累加法运算后的数值,以确定划分的类别。
1.5朴素贝叶斯算法的优缺点
优点
1、朴素贝叶斯模型有稳定的分类效率。
2、对小规模的数据表现很好,能处理多分类任务,适合增量式训练,尤其是数据量超出内存时,可以一批批的去增量训练。
3、对缺失数据不太敏感,算法也比较简单,常用于文本分类。
缺点
1、需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候会由于假设的先验模型的原因导致预测效果不佳。
2、对输入数据的表达形式很敏感(离散、连续,值极大极小之类的)。
二、使用朴素贝叶斯进行文档分类
2.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] # 标签向量,1表示侮辱性文字,0表示正常言论
return postingList, classVec
# 创建不重复词的列表 ———— 词汇表
def createVocabList(dataSet):
vocabSet = set([]) # 创建一个空集
for document in dataSet:
vocabSet = vocabSet | set(document) # 创建两个集合的并集
return list(vocabSet) # 返回不重复的词条列表
# 输出文档向量
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("单词 %s 不在词汇表中!" % word)
return returnVec
# 测试函数效果
# 创建实验样本
listPosts, listClasses = loadDataSet()
print('数据集\n', listPosts)
# 创建词汇表
myVocabList = createVocabList(listPosts)
print('词汇表:\n', myVocabList)
# 输出文档向量
print(setOfWords2Vec(myVocabList, listPosts[5]))
如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1,其余设置为0。
2.2训练算法:从词向量计算概率
# 朴素贝叶斯分类器训练函数
from gettext import npgettext
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 获得训练的文档总数
numWords = len(trainMatrix[0]) # 获得每篇文档的词总数
pAbusive = sum(trainCategory) / float(numTrainDocs) # 计算文档是侮辱类的概率
p0Num = ones(numWords) # 创建numpy.ones数组,词条出现次数初始化为1,拉普拉斯平滑
p1Num = ones(numWords)
p0Denom = 2.0 # 分母初始化为2,拉普拉斯平滑
p1Denom = 2.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i] # 向量相加,统计侮辱类的条件概率的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i] # 向量相加,统计非侮辱类的条件概率的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Denom += sum(trainMatrix[i])
p1Vect = np.log(p1Num / p1Denom) # 侮辱类,每个元素除以该类别中的总词数
p0Vect = np.log(p0Num / p0Denom) # 非侮辱类,每个元素除以该类别中的总词数
return p0Vect, p1Vect, pAbusive # p0Vect非侮辱类的条件概率数组、p1Vect侮辱类的条件概率数组、pAbusive文档属于侮辱类的概率
# 测试代码
listPosts, listClasses = loadDataSet() # 创建实验样本
print('数据集\n', listPosts)
myVocabList = createVocabList(listPosts) # 创建词汇表
print('词汇表:\n', myVocabList)
trainMat = []
for postinDoc in listPosts: # for循环使用词向量来填充trainMat列表
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V, p1V, pAb = trainNB0(trainMat, listClasses)
print('p0V:\n', p0V)
print('p1V:\n', p1V)
print('classVec:\n',listClasses)
print('pAb:\n', pAb)
对运行结果分析,p0V存放的是属于类别0的各单词的条件概率,即各个单词属于非侮辱类的条件概率;p1V存放的是属于类别1的各单词的条件概率,即各个单词属于侮辱类的条件概率。pAb是所有侮辱类的样本占所有样本的概率,在listClasses列表[0, 1, 0, 1, 0, 1]中,1表示侮辱类文字,0表示非侮辱类文字,既有3个侮辱类,3个非侮辱类,所以侮辱类的概率是0.5,非侮辱类的概率是也0.5,pAb就是先验概率。
2.3测试算法:根据现实情况修改分类器
使用NumPy的数组来计算两个向量相乘的结果 。这里的相乘是指对应元素相乘,即先将两个向量中的第1个元素相乘,然后将第2个元素相乘,以此类推。接下来将词汇表 中所有词的对应值相加,然后将该值加到类别的对数概率上。最后,比较类别的概率返回大概率对应的类别标签。
# 测试朴素贝叶斯分类器
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1) # 元素相乘,侮辱类概率
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1) # 非侮辱类概率
if p1 > p0:
return 1
else:
return 0
def testingNB():
listOPosts, listClasses = loadDataSet() # 创建实验样本
myVocabList = createVocabList(listOPosts) # 创建词汇表
trainMat = [] # 文档矩阵
for postinDoc in listOPosts: # for循环使用词向量来填充trainMat列表
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V, p1V, pAb = trainNB0(array(trainMat), array(listClasses)) # 获得概率数组及先验概率
testEntry = ['love', 'my', 'dalmation'] # 输入测试1
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
if classifyNB(thisDoc,p0V,p1V,pAb):
print(testEntry,'属于侮辱类')
else:
print(testEntry,'属于非侮辱类')
testEntry = ['stupid', 'garbage'] # 输入测试2
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
if classifyNB(thisDoc,p0V,p1V,pAb):
print(testEntry,'属于侮辱类')
else:
print(testEntry,'属于非侮辱类')
if __name__ == '__main__':
testingNB()
2.4准备数据:文档词袋模型
将每个词的出现与否作为一个特征,这可以被描述为词集模型(set-of-words model)。如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)。在词袋中,每个单词可以出现多次,而在词集中,每个词只能出现一次。
# 朴素贝叶斯词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1 # 加1
else:
print("the word: %s is not in my vocabulary!" % word)
return returnVec
三、使用朴素贝叶斯过滤垃圾邮件
普通邮件(ham)
垃圾邮件(spam)
3.1准备数据:切分文本
# 例子
mySent = 'This book is the best book on Python or M.L. I have ever laid eyes upon.'
print(mySent.split())
3.2测试算法:使用朴素贝叶斯进行交叉验证
# 文件解析
def textParse(bigString): # 输入字符串, 输出单词列表
listOfTokens = re.split(r'[\W*]', bigString) # 字符串切分,去掉除单词、数字外的任意字符串
return [tok.lower() for tok in listOfTokens if len(tok) > 2] # 除了单个字母外,其他字符串全部转换成小写
# 完整的垃圾邮件测试函数
def spamTest():
docList = [] # 文档列表
classList = [] # 文档标签
fullText = [] # 全部文档内容集合
for i in range(1, 22): # 遍历垃圾邮件和非垃圾邮件各21个
wordList = textParse(open('email/spam/%d.txt' % i).read()) # 读取垃圾邮件,将大字符串并将其解析为字符串列表
docList.append(wordList) # 垃圾邮件加入文档列表
fullText.extend(wordList) # 把当前垃圾邮件加入文档内容集合
classList.append(1) # 1表示垃圾邮件,标记垃圾邮件
wordList = textParse(open('email/ham/%d.txt' % i).read()) # 读非垃圾邮件,将大字符串并将其解析为字符串列表
docList.append(wordList) # 非垃圾邮件加入文档列表
fullText.extend(wordList) # 把当前非垃圾邮件加入文档内容集合
classList.append(0) # 0表示垃圾邮件,标记非垃圾邮件,
vocabList = createVocabList(docList) # 创建不重复的词汇表
trainingSet = list(range(42)) # 为训练集添加索引
testSet = [] # 创建测试集
for i in range(8): # 目的为了从42个邮件中,随机挑选出34个作为训练集,8个做测试集
randIndex = int(random.uniform(0, len(trainingSet))) # 随机产生索引
testSet.append(trainingSet[randIndex]) # 添加测试集的索引值
del (trainingSet[randIndex]) # 在训练集中,把加入测试集的索引删除
trainMat = [] # 创建训练集矩阵训练集类别标签系向量
trainClasses = []
#print(vocabList)
#print(docList[docIndex]) # 训练集类别标签
for docIndex in trainingSet: # for循环使用词向量来填充trainMat列表t
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex])) # 把词集模型添加到训练矩阵中
trainClasses.append(classList[docIndex]) # 把类别添加到训练集类别标签中
p0V, p1V, pSpam = trainNB0(array(trainMat), array(trainClasses)) # 朴素贝叶斯分类器训练函数
#print('词表:\n', vocabList)
#print('p0V:\n', p0V)
#print('p1V:\n', p1V)
#print('pSpam:\n', pSpam)
errorCount = 0 # 用于计数错误分类
for docIndex in testSet: # 循环遍历训练集
wordVector = bagOfWords2VecMN(vocabList, docList[docIndex]) # 获得测试集的词集模型
if classifyNB(array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
errorCount += 1 # 预测值和真值不一致,则错误分类计数加1
print("分类错误集", docList[docIndex])
print('错误率: ', float(errorCount) / len(testSet))
函数spamTest()对贝叶斯垃圾邮件分类器进行自动化处理。导入文件夹spam与ham下的文本文件,并将它们解析为词列表 。接下来构建一个测试集与一个训练集,两个集合中的 邮件都是随机选出的。有50封电子邮件,并不是很多,其中的9封电子邮件被随机选择为测试集。分类器所需要的概率计算只利用训练集中的文档来完成。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。如果发现错误的话,函数会输出错误的文档的此表,这样就可以了解到底是哪篇文档发生了错误。如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说9次,然后求平均值。
四、问题及总结
问题
1、出现name 'log' is not defined错误无法完成防溢出策略
解决方法:导入numpy库,使用np.log可成功运行
2、读取ham和spam文件失败
解决方法:python中文件路径与之前所学编程语言略微不同,需使用r“路径”,路径用\隔开
总结
本次实验我们学习了朴素贝叶斯,并学习了它的算法,优化及代码实现。朴素贝叶斯算法对大数量训练和查询时具有较高的速度。即使使用超大规模的训练集,针对每个项目通常也只会有相对较少的特征数,并且对项目的训练和分类也仅仅是特征概率的数学运算而已;对小规模的数据表现很好,能个处理多分类任务,适合增量式训练(即可以实时的对新增的样本进行训练;对缺失数据不太敏感,算法也比较简单,常用于文本分类;此外朴素贝叶斯对结果解释容易理解。但对于输入数据的准备方式较为敏感,因此对数据的准备方式要符合条件。总的来说是一次收获满满的实验。