机器学习实战04 朴素贝叶斯算法
1. 概述
1.1. 数学基础知识
朴素贝叶斯算法主要是利用概率论中的贝叶斯公式来构建分类器给文档分类.
涉及到以下数学知识:
- 古典概型
- 条件概率
- 贝叶斯公式
古典概型就是计算概率的基本方法这里略去不表. 条件概率是指在已知A已经发生的情况下,求B的概率,可表示为 $P(B|A)$. 其计算公式为:
$$P(B|A)=\frac{P(AB)}{P(A)}$$
虽然有这个公式,但是有时候$P(B|A)$并不能计算出来,而$P(A|B)$却很容易求,于是可以用后者求前者.根据贝叶斯公式,可以有下面的计算公式:
$$P(B|A) = \frac{P(A|B)P(B)}{P(A)}$$
要了解这些基础知识, 看完(浙大第四版)的第一章就够了。
1.2. 本实例的理论基础
在讨论理论基础之前,首先明确下本实例的任务是什么.
假设我们有以下训练数据:
单词 1 | 单词 2 | 单词 3 | 有无攻击性 |
---|---|---|---|
1 | 0 | 1 | 1 |
0 | 0 | 1 | 0 |
1 | 1 | 0 | 1 |
0 | 0 | 0 | 0 |
这组数据,前三列表示某三个单词是否在一则评论中出现过(1=出现过,0=没有),最后一列是这篇评论的分类,1表示带有攻击性,0没有攻击性。
有了这组训练数据之后,我们的任务是:根据训练数据生成一种分类器,将给定的任意的一则评论中这三个单词是否出现的组合传入分类器,就能给出该评论是否带有攻击性。
假设有以下任意一则评论中,单个单词出现与否的组合如下:
单词 1 | 单词 2 | 单词 3 |
---|---|---|
0 | 1 | 0 |
为了给上面这个评论分类,采用的算法如下:
用$P(c_1)$表示训练数据中带有攻击性的评论的出现概率,$P(c_0)$表示没有攻击性评论的概率,显然$P(c_0) = 1 – P(c_1)$。
用向量$w(w_1,w_2,w_3)$表示三个单词是否出现的组合。
贝叶斯分类器是要分别计算:
- $P_1 = P(c_1|w(w_1,w_2,w_3))$
- $P_0 = P(c_0|w(w_1,w_2,w_3))$
- 比较$P_0$和$P_1$大小,如果$P_1 > P_0$,则该评论有攻击性,否则没有攻击性。
$P_1$ 表示在这则评论中三个单词是否出现的组合$w(w_1,w_2,w_3)$出现的情况下,能把该则评论归类成有攻击性的一类的概率,同样$P_0$表示归类为没有攻击性的概率。
因此,任务转化成计算$P_0$和$P_1$,显然这两个是难以计算的。但是,根据贝叶斯公式,可以得到:
$$P_1 = P(c_1|w(w_1,w_2,w_3)) = \frac{P(w(w_1,w_2,w_3)|c_1)P(c_1)}{P(w(w_1,w_2,w_3))} \tag{1}$$
$$P_0 = P(c_0|w(w_1,w_2,w_3)) = \frac{P(w(w_1,w_2,w_3)|c_0)P(c_0)}{P(w(w_1,w_2,w_3))} \tag{2}$$
观察上面两个式子发现,分母都一样,因为最后是要比较两者大小,为了简化计算,可以不考虑分母,只计算分子大小即可。而分子中$P(c_1)$和$P(c_0)$很好计算,就是上述训练数据表格中1和0出现的概率。
剩下的就是计算$P(w(w_1,w_2,w_3)|c_1)$和$P(w(w_1,w_2,w_3)|c_0)$了。那么这两个式子表示的意思是什么呢?拿$P(w(w_1,w_2,w_3)|c_1)$来说,它表示在训练数据集中,所有有无攻击性=1的数据中(见下表),三个单词是否出现的组合$w(w_1,w_2,w_3)$出现的概率。
单词 1 | 单词 2 | 单词 3 | 有无攻击性 |
---|---|---|---|
1 | 0 | 1 | 1 |
1 | 1 | 0 | 1 |
假如$w_1,w_2,w_3$相互独立,则:
$$P(w(w_1,w_2,w_3)|c_1) = P(w_1|c_1) P(w_2|c_1) P(w_3|c_1)$$
其中 $P(w_1|c_1)$ 表示有无攻击性=1时,单词1出现的次数 / 所有单词出现的总次数,于是有:
$$P(w_1|c_1) = \frac{2}{4} = 0.5$$
同理,可计算$P(w_2|c_1)$,$P(w_3|c_1)$,$P(w_1|c_0)$,$\cdots$等。
需要注意的是,现实世界中$w_1,w_2,w_3$并不是相互独立的,这里们强制假设他们相互独立,这也正是这个算法中朴素一词的来源。
至此,$P_1 = P(c_1|w(w_1,w_2,w_3))$和$P_0 = P(c_0|w(w_1,w_2,w_3))$都能计算出来了,任务完成。
2. 代码实现
#!/usr/bin/env python3
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
"""
准备训练数据分位两步:
1) 首先通过 loadDataset 创建测试数据, 模拟6个评论的所有单词, 数据结构为二维数组, 一行数据是包含一个评论的所有单词
loadDataset 同时返回特征数组, 给每一行评论 生成一个特征
2) createVocabList 方法返回所有不重复的单词, 为一个Set, 可以看成是一维数组, 然后让每一条评论都过一遍VocabList,
返回一个新的测试数据returnVec, 二维数组,列数为 VocabList的len, 全部初始化为0,
如果评论中的单词出现在VocabList, returnVec在对应行的位置置为1,
returnVec 的行数依然是训练数据的评论数, 所以loadDataset中返回特征数组不变
"""
def main():
listOPosts, listClasses = loadDataset()
myVocabList = createVocabList(listOPosts)
print(f"list of class = \n {listClasses}")
# print(myVocabList)
# print(listOPosts[0])
# print(setOfWords2Vec(myVocabList, listOPosts[0])) # 测试 文档的第一行的所有词汇是否在
trainMat = []
for postInDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postInDoc))
print(np.array(trainMat))
i = 0
trainMat0 = []
trainMat1 = []
for mat in trainMat:
if i % 2 == 0:
trainMat0.append(mat)
else:
trainMat1.append(mat)
i += 1
print(f"trainMat0=\n{np.array(trainMat0)}")
print(f"trainMat1=\n{np.array(trainMat1)}")
p0v, p1v, pAb = trainNB0(trainMat, listClasses)
testEntry = ['love', 'my', 'dalmation']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 转换成跟词汇表等宽矩阵
print(f"{testEntry} is classified as {classifyNB(thisDoc, p0v, p1v, pAb)}") # 使用分类器分类
testEntry = ['stupid', 'garbage']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 转换成跟词汇表等宽矩阵
print(f"{testEntry} is classified as {classifyNB(thisDoc, p0v, p1v, pAb)}") # 使用分类器分类
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 is abusive, 0 not
return postingList, classVec
def createVocabList(dataSet):
vocabSet = set([])
for document in dataSet:
vocabSet = vocabSet | set(document) # 求并集
return list(vocabSet)
def setOfWords2Vec(vocabList, inputSet):
"""
计算inputSet中的单词在词汇表vocabList中 是否出现过,
:param vocabList: 单词列表
:param inputSet: 一维数组, 待检测单词列表,
:return: 返回一个列表, 该列表和vocabList长度相同, 数据为1或0;
1 表示inputSet中的词汇在vocabList中出现过, 0 表示在vocabList没有出现过
"""
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
print(f"the word {word} is not in my vocabulary!")
return returnVec
def trainNB0_old(trainMatrix, trainCategory):
"""
没有优化过的训练法,可能因为概率为0,或者乘数太小时积为0,从而导致算法失效
:param trainMatrix: 是每一条评论数据过setOfWords2Vec 之后生成的矩阵
:param trainCategory: 就是 loadDataset 返回的 classVec
:return:
"""
numTrainDocs = len(trainMatrix) # 等于 len(trainCategory)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory) / float(numTrainDocs) # 计算p(ci) ci = 1时
print(
f"pAbusive={pAbusive}, sum(trainCategory) / float(len(trainCategory)) = {sum(trainCategory) / float(len(trainCategory))}")
p0Num = np.zeros(numWords)
p1Num = np.zeros(numWords)
p0Denom = 1.0
p1Denom = 1.0
for i in range(numTrainDocs):
if trainCategory[i] == 1: # 分类 = 1 的
p1Num += trainMatrix[i]
p1Denom += np.sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Denom += np.sum(trainMatrix[i])
p1Vect = p1Num / p1Denom # p{wi|c1}
p0Vect = p0Num / p0Denom # p{wi|c0}
print(f"p1Num=\n {np.array(p1Num)}")
print(f"p1Denom=\n {np.array(p1Denom)}")
print(f"p0Num=\n {np.array(p0Num)}")
print(f"p0Denom=\n {np.array(p0Denom)}")
return p0Vect, p1Vect, pAbusive
def trainNB0(trainMatrix, trainCategory):
"""
优化过的算法
:param trainMatrix: 是每一条评论数据过setOfWords2Vec 之后生成的矩阵
:param trainCategory: 就是 loadDataset 返回的 classVec
:return:
"""
numTrainDocs = len(trainMatrix) # 等于 len(trainCategory)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory) / float(numTrainDocs) # 计算p(ci) ci = 1时
print(
f"pAbusive={pAbusive}, sum(trainCategory) / float(len(trainCategory)) = {sum(trainCategory) / float(len(trainCategory))}")
p0Num = np.ones(numWords) # 见p62, 修改分类器, 计算概率时,如果当前的单词没有出现用0表示,则概率为0, 在做乘法时结果为0就没法比较了
p1Num = np.ones(numWords) # 所以这里初始化成1
p0Denom = 2.0 # 因为初始化时取1, 所以这里
p1Denom = 2.0
for i in range(numTrainDocs):
if trainCategory[i] == 1: # 分类 = 1 的
p1Num += trainMatrix[i]
p1Denom += np.sum(trainMatrix[i]) # 分类=1 时的所有单词出现的总次数
else:
p0Num += trainMatrix[i]
p0Denom += np.sum(trainMatrix[i])
p1Vect = np.log(p1Num / p1Denom) # p{wi|c1}, 这里使用log 将后续的乘法转换成加法, 防止应为乘数太小导致积为0
p0Vect = np.log(p0Num / p0Denom) # p{wi|c0}
print(f"p1Num=\n {np.array(p1Num)}")
print(f"p1Denom=\n {np.array(p1Denom)}")
print(f"p0Num=\n {np.array(p0Num)}")
print(f"p0Denom=\n {np.array(p0Denom)}")
return p0Vect, p1Vect, pAbusive
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass):
"""
根据训练结构构建的朴素贝叶斯分类器
:param vec2Classify: 单词表中的单词是否出现, 一行数据, 长度和词汇表一样
:param p0Vec: 训练结果, p{w|c0}
:param p1Vec: 训练结果, p{w|c1}
:param pClass: 训练结果, p{c1}, p{c0} = 1 - p{c1}
:return:
"""
p1 = np.sum(vec2Classify * p1Vec) + np.log(pClass)
# 计算p{c1|w}, = [p{w|c1} * p{c1}] / p{w} , 因为最终是比较 p1和p0, 而
# p1 和 p0 都要除 p{w}, 因此这里可以不计算分母p{w}, 直接计算分子然后比较即可
p0 = np.sum(vec2Classify * p0Vec) + np.log(1.0 - pClass)
if p1 > p0:
return 1
else:
return 0
if __name__ == '__main__':
main()
3. 另一个例子,过滤垃圾邮件并验证
#!/usr/bin/env python3
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import bayes as bayes
def main():
spamTest()
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
def textParse(bigString): # input is big string, #output is word list
import re
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, 26):
spamStr = open(f"email/spam/{i}.txt").read()
wordList = textParse(spamStr)
docList.append(wordList) # 二维数组每一行是一个doc的单词列表
fullText.extend(wordList)
classList.append(1) # spam 文件夹中的都是垃圾邮件
wordList = textParse(open(f"email/ham/{i}.txt").read())
docList.append(wordList) # 二维数组每一行是一个doc的单词列表
fullText.extend(wordList)
classList.append(0) # ham 中存放的是正常邮件
vocabList = bayes.createVocabList(docList)
trainingSet = list(range(50))
testSet = []
for i in range(10):
randIndex = int(np.random.uniform(0, len(trainingSet)))
testSet.append(trainingSet[randIndex])
del (trainingSet[randIndex])
trainMat = []
trainClasses = []
# print(docList)
for docIndex in trainingSet:
trainMat.append(bayes.setOfWords2Vec(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
# print(trainMat)
p0V, p1V, pSpam = bayes.trainNB0(np.array(trainMat), np.array(trainClasses))
# print(f"p0V={p0V}, p1V={p1V},pSpam={pSpam}")
errorCount = 0
for docIndex in testSet:
wordVector = bayes.setOfWords2Vec(vocabList, docList[docIndex])
if bayes.classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
errorCount += 1
print(f"errorCount = {errorCount},\nthe error rate is : {float(errorCount) / len(testSet)}")
if __name__ == '__main__':
main()
4. 代码下载

微信赞赏
支付宝赞赏
本文固定链接: https://www.jack-yin.com/coding/ml/python/3068.html | 边城网事