《机器学习实战》程序清单4-2 朴素贝叶斯分类器训练函数


 此文旨在把trainNB0这个函数详细讲清楚。

下面所做的工作都是为了求下面这个贝叶斯概率,也叫条件概率:

为了计算方便,书中的操作实际上是把这个式子转化为了下式:

概率P(ci)就是通过类别i(侮辱性留言或非侮辱性留言)中文档数除以总的文档数来得到的,也就是最后得到的计算结果0.5。

这里有一个重要的转化,因为w是一个词条向量,它可以展开为[w0, w1, w2,.......wn]。因为我们此例用到的是朴素贝叶斯假设,所以所有词条都互相独立,

此假设也称为条件独立性假设。那么就意味着我们可以做这样的变换:

p(w|ci) == p(w0,w1,w2,......w2|ci)  ==  p(w0|ci)p(w1|ci)p(w2|ci).......p(wn|ci)

然后这部分就可以转化为 p(w0,w1,w2,......w2|ci)p(ci) /p(w),进一步转化为:p(w0|ci)p(w1|ci)p(w2|ci).......p(wn|ci)/p(w)

这个转化,是本例能够成立的一个必要条件。 

def trainNB0(trainMatrix,trainCategory):
    numTrainDocs 
= len(trainMatrix)
    numWords 
= len(trainMatrix[0])
    pAbusive 
= sum(trainCategory)/float(numTrainDocs)
    
#(以下两行)初始化概率 
    p0Num = zeros(numWords); p1Num = zeros(numWords)
    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 #change to log()
    # 对每个元素做除法
    p0Vect = p0Num/p0Denom #change to log()
    return p0Vect,p1Vect,pAbusive

下面把这个函数逐步分解:

1.参数

此函数的参数有两个,一个是trainMatrix,另一个是trainCategory,这两个参数是一步一步的数据处理产生的结果,本节的目的是说明这两个参数值的产生过程。详细如下:

1.1第一步 创建实验样本

可能是为了简化操作,突出重点,作者在这里手工创建了数据集,手工设置了类别,在实际的应用场景中,应当是自动判断自动生成的。
listOPosts,listClasses = bayes.loadDataSet()

这一句产生了listOPosts和listClasses

详细内容分别是:

listOPosts:

[['my','dog','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']
]

listClasses:
[0,1,0,1,0,1]

其中的
listOPosts即list Of Posts,文档列表,就是帖子列表、邮件列表等等。你可以认为列表中的一元素就是一个帖子或者回复,
在此例中一共6个文档、帖子、回复(以后统称文档)。
分别是:
['my','dog','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']

可以看到,2、4、6句标红部分,存在侮辱性词条,第1、3、5个句子,不存在侮辱性词条,所以,对应的类别标签设置为
listClasses = [0,1,0,1,0,1]

1.2第二步 创建包含所有不重复词条的集合(词汇表)

这一步是为了产生一个大而全的集合,这个集合包括了所有文档(即第一步产生的6个文档)中的词条,但每个词条都不重复。

#创建一个所有文档中的不重复单词列表
def createVocabList(dataSet):
vocabSet
= set([]) #创建一个空集
n = 0
for document in dataSet:
vocabSet
= vocabSet | set(document) #创建两个集合的并集
n += 1
# print('vocabSet:',n,vocabSet)
# print('文档集合的总长度:',len(vocabSet))

a
= list(vocabSet)
a.sort()
return a

Python中的集合(set)具有消除重复元素的功能。

书中代码没有排序。为了看得更清楚,我加上了排序。

上述代码中的

vocabSet = vocabSet | set(document)

并集操作,相当于 += 操作

此函数的参数dataSet,即是上一步产生的listOPosts

调用方式:
myVocablList = createVocabList(listOfPosts)
 
运行结果是:
['I', 'ate', 'buying', 'cute', 'dalmation', 'dog', 'flea', 'food', 'garbage', 'has', 'help', 'him', 'how', 'is', 'licks', 'love',
'maybe', 'mr', 'my', 'not', 'park', 'please', 'posting', 'problems', 'quit', 'so', 'steak', 'stop', 'stupid', 'take', 'to', 'worthless']

1.3第三步 文档向量

  获得词汇表后,便可以使用函数setOfWords2Vec(),该函数的输入参数为词汇表及某个文档,输出的是文档向量,向量的每一元素为1或0,分别表示词汇表中的单词在输入文档中是否出现。

def setOfWords2Vec(vocabList, inputSet):
returnVec
= [0] * len(vocabList)

  for word in inputSet: if word in vocabList:
# print("word:",word)
returnVec[vocabList.index(word)] = 1
else:
print("the word:%s is not in my Vocabulary!" % word)

return returnVec # 返回一个list
vocabList即上一步产生的词汇表,inputSet可以是任意一篇文档,此处为了简化操作,在6篇文档中选取。
调用方式:
    listOfPosts,listClasses = loadDataSet()
print(listOfPosts)
myVocablList
= createVocabList(listOfPosts)
print(myVocablList)

l
= listOfPosts[0]
l.append(
"中华人民共和国")
l.append(
"kk")
print("listOfPosts:", listOfPosts[0])
b
= setOfWords2Vec(myVocablList, listOfPosts[0])
print(b)
 
我们的输入是:
listOfPosts[0],它的值是:
['my', 'dog', 'dog', 'has', 'flea', 'problems', 'help', 'please']
从索引为0的元素开始循环,如果这个元素存在于词汇表中,则把要返回的
类别向量returnVec中对应位置的值设为1。

此处第1个值是my,它存在于词汇表中,位置是18,所以
把returnVec中的对应位置的值设置为1
得到:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0
I ate buying cute dalmation dog flea food garbage has help him how is licks love maybe mr my not park please posting problems quit so steak stop stupid take to worthless

第2个值是dog,它存在于词汇表中,位置是5,把returnVec中的对应位置的值设置为1
得到:[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0
I ate buying cute dalmation dog flea food garbage has help him how is licks love maybe mr my not park please posting problems quit so steak stop stupid take to worthless

以此类推,直到最后得到:
[0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]

至此,我们得到了一篇文档listOfPosts[0]的词向量

用同样的方式,我们还可以得到listOfPosts[1]、listOfPosts[2]、listOfPosts[3]、listOfPosts[4]、listOfPosts[5]文档的词向量,分别是:

[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0]

[1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]

[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1]

[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0]

[0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1]

1.4第四步 

 至此,我们可以说明trainNB0(trainMatrix, trainCategory)中的参数是什么了。

trainMatrix就是由各个文档转化成的词向量构成的矩阵,而trainCategory就是这几个文档的类别,也就是这几个文档是不是含有侮辱性词条。

trainMatrix的值为:

[

[0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],

[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0],

[1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],

[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1],

[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0],

[0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1]

]

trainCategory的值为:

[0, 1, 0, 1, 0, 1]

2.过程

numTrainDocs = len(trainMatrix)

这句是取得词向量矩阵的长度,也就是说文档的数量,此例中是6个。

numWords = len(trainMatrix[0])

取得词向量矩阵中第一条记录的长度,也就是词条(即特征)的数量,此例应当是32个。

pAbusive = sum(trainCategory)/float(numTrainDocs)

p表示概率,abusive的意思是辱骂的、滥用的,pAbusive表示辱骂文档的概率。这个值即是第一节的公式中所需要的P(Ci),是通过类别i(侮辱性留言或非侮辱性留言)中文档数除以总的文档数来计算的。

sum(trainCategory) ==> sum([0, 1, 0, 1, 0, 1]) ==> 3
此处用==>符号表示“推出”、“等于”
numTrainDocs==6
所以
pAbusive = sum(trainCategory)/float(numTrainDocs)==>
pAbusive == 3/6 ==>
pAbusive == 0.5

也就是说,6篇文档,其中有3篇含有侮辱性词条,概率是0.5,即P(C1)==0.5。
需要求的3个值,已经求出了一个,还需要P(w|Ci)和P(w)两个值。

p0Num = zeros(numWords) 
p1Num = zeros(numWords)

上面这两句是要初始化一个概率,是什么概率?

p0Denom = 0.0; p1Denom = 0.0

上式中的Denom是分母的意思,把分母项置为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])

按照文档个数,从0到5循环。

如果文档类别是侮辱性(trainCategory[i] == 1),则把侮辱性文档的词向量相叠加,否则把非侮辱性文档的词向量相叠加。这样说有点拗口,看看下面的实际执行过程:

由前面的计算结果可知,trainCategory的值是[0, 1, 0, 1, 0, 1],放在这里看着方便。

  i==0时:

  trainCategory[0]是0,

  所以p0Num += trainMatrix[0],
  而trainMatrix[0]是[0, 0, 0, 0, 0, 1[5], 1[6], 0, 0, 1[9], 1[10], 0, 0, 0, 0, 0, 0, 0, 1[18], 0, 0, 1[21], 0, 1[22], 0, 0, 0, 0, 0, 0, 0, 0]

  为了方便比较,我在列表中增加了中括号括起来的索引值

  同时,p0Denom += sum(trainMatrix[0]),trainMatrix[0]中有7个1,所以此时p0Denom的值是7
i==1时:
  trainCategory[1]是1,
  所以p1Num += trainMatrix[1],
  而trainMatrix[1]是[0, 0, 0, 0, 0, 1[5], 0, 0, 0, 0, 0, 1[11], 0, 0, 0, 0, 1[16], 0, 0, 1[19], 1[20], 0, 0, 0, 0, 0, 0, 0, 1[28], 1[29], 1[30], 0]
  
  同时,p1Denom += sum(trainMatrix[1]),trainMatrix[1]中有8个1,所以此时p1Denom的值是8
i==2时:
  trainCategory[2]是0,所以p0Num += trainMatrix[2]
  而trainMatrix[2]是[1[0], 0, 0, 1[3], 1[4], 0, 0, 0, 0, 0, 0, 1[5], 0, 1[7], 0, 1[9], 0, 0, 1[12], 0, 0, 0, 0, 0, 0, 1[19], 0, 0, 0, 0, 0, 0]

  叠加之后,p0Num的值为:[ 1. 0. 0. 1. 1. 1. 1. 0. 0. 1. 1. 1. 0. 1. 0. 1. 0. 0.2. 0. 0. 1. 0. 1. 0. 1. 0. 0. 0. 0. 0. 0.]
  可以看到,是列表中的每个位置对应的值相加。

同时,p0Denom += sum(trainMatrix[2]),trainMatrix[2]中有8个1,所以此时p0Denom的值是7+8=15

以此类推,最后的结果是:
p0Num == [ 1. 1. 0. 1. 1. 1. 1. 0. 0. 1. 1. 2. 1. 1. 1. 1. 0. 1.3. 0. 0. 1. 0. 1. 0. 1. 1. 1. 0. 0. 1. 0.]
p1Num == [ 0. 0. 1. 0. 0. 2. 0. 1. 1. 0. 0. 1. 0. 0. 0. 0. 1. 0.
0. 1. 1. 0. 1. 0. 1. 0. 0. 1. 3. 1. 1. 2.]

p0Denom==24
p1Denom==19

插播一句,发现了一个翻译错误:
英文版第70页,原文是The numerator
is a NumPy array with the same number of elements as you have words in your vocabulary.
中文版第61页,译文是“上述程序中的分母变量是一个元素个数等于词汇表大小的NumPy数组。”
应改为:“上述程序中的分子变量是一个元素个数等于词汇表大小的NumPy数组。”
 
 

 运行结果如下:

p0V: [ 0.04166667 0.04166667 0. 0.04166667 0.04166667 0.04166667 0.04166667 0. 0. 0.04166667 0.04166667 0.08333333 0.04166667     0.04166667 0.04166667 0.04166667 0. 0.04166667 0.125 0. 0. 0.04166667 0. 0.04166667 0. 0.04166667 0.04166667 0.04166667 0. 0.
    0.04166667 0. ]


p1V: [ 0. 0. 0.05263158 0. 0. 0.10526316 0. 0.05263158 0.05263158 0. 0. 0.05263158 0. 0. 0. 0. 0.05263158 0. 0. 0.05263158 0.05263158 0. 0.05263158 0. 0.05263158 0. 0. 0.05263158 0.15789474 0.05263158 0.05263158 0.10526316]


pAb: 0.5

 

对于这个结果,我曾经对作者的说明感到困惑不解。下面列出我经过逐步了解后的解释:

首先,我们发现文档属于侮辱类的概率pAb为0.5,该值是正确的。
接下来,看一看在给定文档类别条件下词汇表中单词的出现概率,看看是否正确。
词汇表中的第一个词是cute,其在类别0中出现1次,而在类别1中从未出现。对应的条件概率分别为0.041 666 67与0.0。该计算是正确的。
我们找找所有概率中的最大值,该值出现在P(1)数组第26个下标位置,大小为0.15789474。在myVocabList的第26个下标位置上可以查到该单词是stupid。
这意味着stupid是最能表征类别1(侮辱性文档类)的单词。


第一句说,“我们发现文档属于侮辱类的概率pAb为0.5,该值是正确的。”,0.5这个数值的来源是清楚的,但此处作者做了一个诊断,说该值是正确的,是什么意思?一直没太明白。
可能一:有3个非侮辱,3个侮辱,所以概率是0.5,正确的。
可能二:经过计算,和我们肉眼可见的3/6符合,所以结果是正确的。如果是这样,那这是一句废话,本来就是按照这个算法计算的,何必要强调一下。
还有其它可能吗?待定,也许将来更加深入以后会知道。

第二句,接下来,看一看在给定文档类别条件下词汇表中单词的出现概率,看看是否正确。这里指的是P(w|ci)

第三句,最大值是0.15789474,对应的词条是stupid,它在类别为1的类别中出现了3次,所以它是最能表往类别1的词条。此处存疑。
如果把3个stupid分别改成
stupid、fuck、shit,那么它就会和其它只出现1一次的词条一样,值变为0.05263158。
这个时候,谁是更能突出表征类别1的词条?

这一节只是把词条的出现概率计算完毕,没有完成整个算式。

4.5.3 测试算法:根据现实情况修改分类器


前面所提到的概率公式转化结
p(w0|ci)p(w1|ci)p(w2|ci).......p(wn|ci)/p(w)的意义是,列表内部的概率相乘,得到的积除以p(w)。

如果有概率为0,那么乘积就是0,凭经验也可以知道这是不合理的,对于这个问题,书中给出了一个方法,将所有词的出现数初始化为1,并将分母初始化为2。
书中只给出了方法,并没有解释为什么这么做。后经查询,这种方法叫拉普拉斯平滑。来源:https://www.cnblogs.com/knownx/p/7860174.html

背景:为什么要做平滑处理?

 

  零概率问题,就是在计算实例的概率时,如果某个量x,在观察样本库(训练集)中没有出现过,会导致整个实例的概率结果是0。在文本分类的问题中,当一个词语没有在训练样本中出现,该词语调概率为0,使用连乘计算文本出现概率时也为0。这是不合理的,不能因为一个事件没有观察到就武断的认为该事件的概率是0。

 

拉普拉斯的理论支撑

 

  为了解决零概率的问题,法国数学家拉普拉斯最早提出用加1的方法估计没有出现过的现象的概率,所以加法平滑也叫做拉普拉斯平滑。
  假定训练样本很大时,每个分量x的计数加1造成的估计概率变化可以忽略不计,但可以方便有效的避免零概率问题。

 

 


它的背后的原理就是当数量特别庞大时,个体就没有那么重要。99%和100%在概率上来讲也没什么区别。

 p(w0|ci)p(w1|ci)p(w2|ci).......p(wn|ci)

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com