该书主要以原理简介+项目实战为主,本人学习的主要目的是为了结合李航老师的《统计学习方法》以及周志华老师的西瓜书的理论进行学习,从而走上机器学习的“不归路”。因此,该笔记主要详细进行代码解析,从而透析在进行一项机器学习任务时候的思路,同时也积累自己的coding能力。
正文由如下几部分组成:
1、实例代码(详细注释)
2、知识要点(函数说明)
3、调试及结果展示
朴素贝叶斯算法(NBC),“朴素”在于该理论有个假设前提,那就是——特征与特征之间相互独立,这一假设使得该模型在输入向量特征条件有关联的场景下表现不佳;“贝叶斯”意思是该算法基于贝叶斯定理,属于监督学习中的生成模型。其他生成模型还有GMM、HMM等。该算法广泛应用于NLP等领域。
该章节涉及到的概念有:条件概率、边缘概率、联合概率、独立分布、全概率公式、贝叶斯公式、先验概率和后验概率等等。
下面对贝叶斯分类器表达式进行推导:
→step1:条件概率
P ( A ∣ B ) = P ( A B ) P ( B ) , P ( B ∣ A ) = P ( A B ) P ( A ) P(A|B)=\frac{P(AB)}{P(B)},P(B|A)=\frac{P(AB)}{P(A)} P(A∣B)=P(B)P(AB),P(B∣A)=P(A)P(AB)
→step2:由上二式联立可得“贝叶斯定理”
P ( A ∣ B ) = P ( B ∣ A ) ⋅ P ( A ) P ( B ) P(A|B)=\frac{P(B|A)·P(A)}{P(B)} P(A∣B)=P(B)P(B∣A)⋅P(A)
→step3:全概率公式
→step4:贝叶斯公式
将全概率公式整理可得:
P ( B ) = ∑ i = 1 n P ( A i ) ⋅ P ( B ∣ A i ) P(B)=\sum_{i=1}^{n}P(A_{i})\cdot P(B|A_{i}) P(B)=i=1∑nP(Ai)⋅P(B∣Ai)
则有
P ( A ∣ B ) = P ( B ∣ A ) ⋅ P ( A ) ∑ i = 1 n P ( A i ) ⋅ P ( B ∣ A i ) P(A|B)=\frac{P(B|A)·P(A)}{\sum_{i=1}^{n}P(A_{i})\cdot P(B|A_{i})} P(A∣B)=∑i=1nP(Ai)⋅P(B∣Ai)P(B∣A)⋅P(A)
→step5:条件独立
朴素贝叶斯对条件概率分布做了条件独立性假设。则贝叶斯公式中的分子
P ( B ∣ A ) = P ( B = b 1 , b 2 ⋅ ⋅ ⋅ b n ∣ A i ) = ∏ j = 1 n P ( B j = b j ∣ A i ) P(B|A)=P(B=b_{1},b_{2}\cdot \cdot \cdot b_{n}|A_{i})=\prod_{j=1}^{n}P(B_{j}=b_{j}|A_{i}) P(B∣A)=P(B=b1,b2⋅⋅⋅bn∣Ai)=j=1∏nP(Bj=bj∣Ai)
→step6:贝叶斯判定准则(贝叶斯分类器)
将5式代入贝叶斯公式,整理可得
P ( A i ∣ B ) = P ( A i ) ⋅ ∏ j = 1 n P ( B j = b j ∣ A i ) ∑ i = 1 n P ( A i ) ⋅ P ( B ∣ A i ) P(A_{i}|B)=\frac{P(A_{i})·\prod_{j=1}^{n}P(B_{j}=b_{j}|A_{i})}{\sum_{i=1}^{n}P(A_{i})\cdot P(B|A_{i})} P(Ai∣B)=∑i=1nP(Ai)⋅P(B∣Ai)P(Ai)⋅∏j=1nP(Bj=bj∣Ai)
因为上式分母对所有 A i A_{i} Ai都是相同的,因此左式正比于右式分子部分,即
P ( A i ∣ B ) ∝ P ( A i ) ⋅ ∏ j = 1 n P ( B j = b j ∣ A i ) P(A_{i}|B)∝{P(A_{i})·\prod_{j=1}^{n}P(B_{j}=b_{j}|A_{i})} P(Ai∣B)∝P(Ai)⋅j=1∏nP(Bj=bj∣Ai)即可得朴素贝叶斯分类器的表达式
f ( B ) = a r g m a x ( P ( A i ) ⋅ ∏ j = 1 n P ( B j = b j ∣ A i ) ) f(B)=argmax({P(A_{i})·\prod_{j=1}^{n}P(B_{j}=b_{j}|A_{i})}) f(B)=argmax(P(Ai)⋅j=1∏nP(Bj=bj∣Ai))
1、本章例程是要做一个文本分类问题,我们可以将整个文档看成是实例,而文档中的元素相应的构成特征。通过观察文档中出现的词,并把每个词的出现与否相应的作为特征,进而构造分类器对文档进行分类。该例程以一个留言社区为例,为了过滤掉那些内容不当的侮辱性言论,对此我们可以建立两个类别:侮辱性和非侮辱性,分别用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] #1 is abusive, 0 not
return postingList,classVec
#将数据集中的所有词条进行求并,输出不重复的所有元素形成词汇表
def createVocabList(dataSet):
vocabSet = set([]) #创建一个集合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:
returnVec[vocabList.index(word)] = 1#如果输入数据的词条存在于数据表中,则对应位的值设为1
else:
print("the word: %s is not in my Vocabulary!"%word)
return returnVec
知识要点:
①set():set() 函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可以计算交集、差集、并集等。
②index():Python index() 方法检测字符串中是否包含子字符串 str。
2、下面我们执行一下程序:
******
import sys; print('Python %s on %s' % (sys.version, sys.platform))
sys.path.extend(['E:\\ML_text\\machinelearninginaction\\Ch04', 'E:/ML_text/machinelearninginaction/Ch04'])
PyDev console: starting.
Python 3.6.7 |Anaconda, Inc.| (default, Oct 28 2018, 19:44:12) [MSC v.1915 64 bit (AMD64)] on win32
>>>import bayes
>>>listOPosts, listClasses = bayes.loadDataSet()
>>>myVocabList = bayes.createVocabList(listOPosts)
>>>myVocabList
['stop', 'stupid', 'dalmation', 'take', 'I', 'ate', 'has', 'please', 'love', 'flea', 'buying', 'worthless', 'park', 'my', 'cute', 'problems', 'food', 'to', 'is', 'help', 'posting', 'steak', 'quit', 'licks', 'garbage', 'mr', 'how', 'maybe', 'him', 'not', 'dog', 'so']
>>>bayes.setOfWords2Vec(myVocabList, listOPosts[0])
[0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]
>>>bayes.setOfWords2Vec(myVocabList, listOPosts[3])
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
从我们运行的结果可以看到,setOfWords2Vec函数的两个输入分别是我们createVocabList函数生成的词汇表,以及loadDataSet函数中导入的数据集中的单个语句所构成的词条集合,输出的returnVec为通过词条集合转换好的词向量。
1、下面构造的trainNB0函数是一个过程产物,因为该函数中还会有一些缺陷,到后面会作出修改来改善它的功能。下面还是先学习trainNB0函数:
#朴素贝叶斯分类器训练函数
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix)#获取文档总数
numWords = len(trainMatrix[0])#获取词汇表词汇个数
pAbusive = sum(trainCategory)/float(numTrainDocs)# 基于训练集估计类先验概率:P(侮辱性文档)=侮辱性文档个数/文档总数
p0Num = zeros(numWords); p1Num = zeros(numWords)#创建两个和词汇表维度一致的全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])
p1Vect = p1Num/p1Denom# P(词向量特征j|侮辱性文档)
p0Vect = p0Num/p0Denom# P(词向量特征j|非侮辱性文档)
return p0Vect,p1Vect,pAbusive
由上面代码,我们可以看出,为了估计类后验概率 P(类别 i |词向量特征 j ),我们已经计算出了 类先验概率 P(侮辱性文档) (注:此例为二分类问题,因此 P(类别2)=1-P(类别1),即已知P(类别 i ))及 类条件概率 P(词向量特征 j |类别 i ),即可完成贝叶斯分类器的构造:
P ( 类 别 i ∣ 词 向 量 特 征 j ) = P ( 类 别 i ) P ( 词 向 量 特 征 集 合 ) ⋅ ∏ j = 1 n P ( 词 向 量 特 征 j ∣ 类 别 i ) P(类别 i |词向量特征 j )=\frac{P(类别i)}{P(词向量特征集合)}{·\prod_{j=1}^{n}P(词向量特征j|类别i)} P(类别i∣词向量特征j)=P(词向量特征集合)P(类别i)⋅j=1∏nP(词向量特征j∣类别i) ↓ ↓ ↓ 由 于 P ( 词 向 量 特 征 集 合 ) 对 于 所 有 类 别 来 说 是 相 同 的 , 因 此 有 : 由于P(词向量特征集合)对于所有类别来说是相同的,因此有: 由于P(词向量特征集合)对于所有类别来说是相同的,因此有:
P ( 类 别 i ∣ 词 向 量 特 征 j ) ∝ P ( 类 别 i ) ⋅ ∏ j = 1 n P ( 词 向 量 特 征 j ∣ 类 别 i ) P(类别 i |词向量特征 j )∝{P(类别i)}{·\prod_{j=1}^{n}P(词向量特征j|类别i)} P(类别i∣词向量特征j)∝P(类别i)⋅j=1∏nP(词向量特征j∣类别i) ↓ ↓ ↓ f ( 词 向 量 特 征 集 合 ) = a r g m a x ( P ( 类 别 i ) ⋅ ∏ j = 1 n P ( 词 向 量 特 征 j ∣ 类 别 i ) ) f(词向量特征集合)=argmax({P(类别i)·\prod_{j=1}^{n}P(词向量特征j|类别i)}) f(词向量特征集合)=argmax(P(类别i)⋅j=1∏nP(词向量特征j∣类别i))
2、上述trainNB0函数,其输入变量trainMatrix是通过setOfWords2Vec函数输出的多个returnVec向量组成,即将文档数据集完全转换后的词向量数据集。我们为了测试trainNB0函数,构造main函数:
if __name__ == '__main__':
listOPosts, listClasses = loadDataSet()
myVocabList = createVocabList(listOPosts)
trainMat = []
for postinDoc in listOPosts:#构造词向量数据集
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V, p1V, pAb = trainNB0(trainMat, listClasses)
print(pAb, '\n\n', p0V, '\n\n', p1V)
输出结果如下:
0.5
[0. 0.04166667 0.08333333 0. 0.04166667 0.04166667
0.04166667 0.04166667 0.04166667 0.04166667 0.125 0.
0.04166667 0. 0.04166667 0.04166667 0.04166667 0.
0.04166667 0.04166667 0. 0. 0. 0.04166667
0. 0. 0.04166667 0. 0.04166667 0.04166667
0.04166667 0.04166667]
[0.05263158 0. 0.05263158 0.15789474 0. 0.
0.05263158 0. 0. 0. 0. 0.05263158
0. 0.05263158 0. 0. 0. 0.05263158
0. 0.05263158 0.05263158 0.10526316 0.05263158 0.
0.05263158 0.05263158 0. 0.05263158 0. 0.
0.10526316 0. ]
1、贝叶斯分类器表达式中计算独立同分布数据概率的乘积,如果其中一个概率为0,那么最后整个乘积结果直接等于0,为了降低该影响,例程中对初始化矩阵进行了修改,由原本的全0矩阵修改为全1矩阵,并将分母部分初始化由0.0改变为2.0。
另外一个问题就是下溢出,太多很小的数相乘容易造成计算溢出,因此该例程采用对乘积取自然对数,修改后的trainNB0()函数为:
#朴素贝叶斯分类器训练函数
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix)#获取文档总数
numWords = len(trainMatrix[0])#获取词汇表词汇个数
pAbusive = sum(trainCategory)/float(numTrainDocs)# 基于训练集估计类先验概率:P(侮辱性文档)=侮辱性文档个数/文档总数
p0Num = ones(numWords); p1Num = ones(numWords)#创建两个和词汇表维度一致的全1矩阵
p0Denom = 2.0; p1Denom = 2.0#change to 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)#change to log()
p0Vect = log(p0Num/p0Denom)#change to log()
return p0Vect,p1Vect,pAbusive
知识要点:
①数据溢出包括上溢和下溢:
overflow(上溢):int类型来保存一个非常大的数,而这个超出了int类型所能表示的最大的数的范围,不过在python中,int型数据是动态长度的,而且对于python3版本的int型数据在理论上是无限长度的(只要你内存够大)。
underflow(下溢):如果要用double来表示一个非常小的数,超出它所能表示的最小数时,就会发生数据溢出错误。
下面重新执行一下main函数,输出如下结果:
0.5
[-2.56494936 -2.56494936 -2.56494936 -2.56494936 -3.25809654 -2.56494936
-2.15948425 -2.56494936 -2.56494936 -1.87180218 -2.56494936 -3.25809654
-3.25809654 -2.56494936 -3.25809654 -2.56494936 -3.25809654 -2.56494936
-3.25809654 -2.56494936 -2.56494936 -2.56494936 -3.25809654 -3.25809654
-2.56494936 -3.25809654 -2.56494936 -3.25809654 -3.25809654 -2.56494936
-2.56494936 -2.56494936]
[-3.04452244 -3.04452244 -1.94591015 -3.04452244 -2.35137526 -3.04452244
-2.35137526 -3.04452244 -3.04452244 -3.04452244 -3.04452244 -2.35137526
-2.35137526 -3.04452244 -2.35137526 -3.04452244 -2.35137526 -3.04452244
-1.65822808 -3.04452244 -3.04452244 -3.04452244 -2.35137526 -2.35137526
-3.04452244 -2.35137526 -3.04452244 -2.35137526 -1.94591015 -2.35137526
-2.35137526 -3.04452244]
2、改进完分类器训练函数之后,下面构造最后的朴素贝叶斯分类器:
#朴素贝叶斯分类器
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + log(pClass1)#vec2Classify * p1Vec即计算 P(词向量特征j∣类别i)累乘,p1=logA+logB等价于logAB
p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
该函数输入分别有vec2Classify, p0Vec, p1Vec, pClass1,其中vec2Classify表示的是新输入的样本,而p0Vec, p1Vec, pClass1分别是trainNB0函数的3个输出。
另外p1和p0其实是计算了下面这个表达式:
P ( A i ) ⋅ ∏ j = 1 n P ( B j = b j ∣ A i ) {P(A_{i})·\prod_{j=1}^{n}P(B_{j}=b_{j}|A_{i})} P(Ai)⋅j=1∏nP(Bj=bj∣Ai)
下面我们整合之前我们写的main函数,像书中例程那样构造一个便利函数testingNB(),用来封装所有操作,并输入两个样本,用于测试算法的结果:
#输入样本并给出分类,便利函数
def testingNB():
listOPosts,listClasses = loadDataSet()
myVocabList = createVocabList(listOPosts)
trainMat=[]
for postinDoc in listOPosts:
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))
testEntry = ['stupid', 'garbage']#输入样本2
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))
下面在python交互环境下,执行如下语句测试代码:
******
PyDev console: starting.
Python 3.6.7 |Anaconda, Inc.| (default, Oct 28 2018, 19:44:12) [MSC v.1915 64 bit (AMD64)] on win32
>>>import bayes
>>>bayes.testingNB()
['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1
3、上述方式是将每个词的出现与否作为一个特征,这可以被描述为词集模型,“如果一个词在文档中出现不止一次,这可能意味着包含 / 该词是否出现在文档中 / 所不能表达的某种信息”,这种方法被称为词袋模型,为了适应词袋模型,需要对setOfWords2Vec函数进行修改,改成bagOfWords2VecMN:
#词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0]*len(vocabList)#创建一个与词汇表等长的向量,所有元素初始化为0
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec