朴素贝叶斯分类算法的核心是贝叶斯公式:
‘朴素’二字的含义是指,某一特征取值的概率相对于其他特征的取值独立,也就是实例的各个特征的分布相对独立。
朴素贝叶斯分类器的另一个假设是,每个特征同等重要。
关于朴素贝叶斯讲的清晰易懂的文章可见这里。
朴素贝叶斯是用于文档分类的常用算法,本章也是使用文档分类来讲解的。
- 从文本中构建词向量
- 使用词向量训练分类器
- 使用分类器来分类
1. 从文本中构建词向量
想要对文本进行分类,一个问题是不同文本的长度并不相同,该如何操作呢?可以先构建一个词典(词汇表),其中包含n个词。对于一篇文章将其表示为一个长为n的词典向量,其中值为1表示词典中的对应词在文章中出现过(不论次数),0表示未出现。这样对于每篇文章都可以得到一个对应的词向量,可用于训练分类器,以及进行分类。
那么该如何构造词典呢?可以使用训练集,或者通过对大量文章的统计来构建词典。注意词典中的词都是唯一的。书中使用了一个小小的训练集来讲解构造词典的过程。
"""
实验样本,注意这里给的例子只是二分类问题。
"""
def loadDataSet():
# 每一行为一个进行了词条切分的句子
# 可用正则表达式来处理文本。
postingList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
['my', 'dalmation', 'is' , 'so', 'sute', '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
"""
根据输入数据集构造词典,注意这里没有排序
"""
# dataSet即为上面postingList的形式。
def createVocabList(dataSet):
vocabSet = set([])
for document in dataSet:
# 或,即为集合的并。。。
vocabSet = vocabSet | set(document)
return list(vocabSet)
>>> listOPosts, listClasses = loadDataSet()
# myVocabList就是根据数据集构造好的词典
# 其中的单词是随机排列的
>>> myVocabList = createVocabList(listOPosts)
下面代码根据上面构造好的词典对于属于文章构造对应的词向量:
"""
根据构造好的字典,对于输入文档,构造该文档的词向量
"""
# vocabList为构造好的词典
# inputSet为进行了词条切分的文章,形式如postingList的各个行。
# 返回returnVec为对应的词向量。
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
pirnt("the word: %s is not in my Vocabulary!"%word)
return returnVec
# 得到第一个句子对应的词向量
>>> setOfWords2Vec(myVocabList, listOPosts[0])
2. 使用词向量训练分类器
对于朴素贝叶斯分类器,训练分类器时到底是使分类器学习什么呢?
已知输入实例为w,w=[w_1, w_2, w_3, ..., w_n](有n个特征,w_j为特征wj的取值),要预测其类别,即找到类别Ci,使P(Ci | w)最大。
而根据贝叶斯公式可得到:
P(Ci | w) = P(w | Ci ) * P(Ci) / P(w)。
又由于朴素贝叶斯的假设,即各个特征的分布独立,则可得到:
P(w | Ci) = P(w1 | Ci) * P(w2 | Ci) * P(w3 | Ci) * ... * P(wn | Ci)
也就是对于各个类别Ci,在该类别中,实例w各特征取值的概率。
P(w) = P(w1) * P(w2) * P(w3) * ... * P(wn)
也就是实例各特征取值的概率。
所以,学习的就是三个东西,对应公式中的三项(注意,都是根据训练集来学习的这些概率,如果训练集分布越全面,那么学到的效果也就越好),知道了这三项就可以对新实例进行分类:
- P(wj | Ci) :j为特征索引(1<= j <= n),i即为类别索引。该概率也就是指,对于各个类别i,在该类别中,求出各特征j取各个值的概率。
- P(Ci) :求训练集中各类别出现的概率,很简单,用各类别数量除以样本数量即可。
- P(wj) :就是特征j的各个取值,在样本中出现的频率。注意,该项与类别无关,训练集确定了,该项的值也就确定了,所以在进行预测时不需要计算该项,只需计算上面两项即可。
下面为书中给的训练分类器的代码,只适用于二分类的问题,如果要用于多分类则需要重写代码喽:
"""
该代码针对应于两种特殊情况写的:
1. 这是一个二分类问题,label为0或1.
2. 由于要对文本分类,且使用的是词向量,那么每个特征的取值只有两种:
1或0(该词出现与否)。
由于都是0或1的取值,所有代码中有取巧的部分。
如果是特征值有多种取值情况,或多分类,那么该代码不适用了。
"""
import numpy as np
# trianMatrix为文档矩阵,每一行都是词向量
# trainCategory对应于trainMatrix中每一行的类别
# 这里代码为二分类,所以trainCategory为0、1组成的向量。
def trainNB0(trainMatrix, trainCategory):
# 样本数量
numTrainDocs = len(trainMatrix)
# 特征个数
numWords = len(trainMatrix[0])
# 类别1的数量/总样本数量=类别1出现的概率
pAbusive = np.sum(trainCategory) / float(numTrainDocs)
# 用于存储两个类别下各个特征出现的次数。
p0Num = np.zeros(numWords); p1Num = np.zeros(numWords)
# 按理来说P(wj | Ci)为类别Ci时,特征wj取各个值的概率
# 应等于wj为某一特征值的数量处理该类别Ci中样本数量的。
# 但这里的做法是除以所有单词出现的次数了,这样条件概率的值就小得很多了。
p0Denom = 0.0; p1Denom = 0.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
# 由于词向量为0、1组成的向量,所以直接向量相加可得该
# 类别下某一特征值出现的次数(词出现的次数)。
p1Num += trainMatrix[i]
# 这里p1Denom为类别1时所有词出现的次数的和。
# 然后用pqNum / p1Denom。
# 我感觉这是不对的,应改为p1Denom += 1
# 使p1Denom统计类别1时的样本数量,因为各特征值只有
# 两种取值,所以这样pqNum / p1Denom就为类别1时
# 特征值为1的频率(也就是该词出现的频率)。
p1Denom += np.sum(trainMatrix[i])
else:
# 同上,不过是类别0
p0Num += trainMatrix[i]
p0Denom += np.sum(trainMatrix[i])
# 如上注释所示。
p1Vect = p1Num / p1Denom
p0Vect = p0Num / p0Denom
# p0Vect各列应为类别0时,各特征为1的概率(也就是各词出现的概率)
return p0Vect, p1Vect, pAbusive
文中对上述代码的改进有下面两点:
由于在进行预测时,进行的计算P(w | Ci) = P(w1 | Ci) * P(w2 | Ci) * P(w3 | Ci) * ... * P(wn | Ci),当特征过多时,很容易造成下溢出问题。所以这里应对p0Vect和p1Vect取log,那么就变为:
log(P(w | Ci) * P(Ci)) = log(P(w | Ci) ) + log(Ci))
log(P(w | Ci) )= log(P(w1 | Ci)) + log(P(w2 | Ci)) + log(P(w3 | Ci)) + ... + log(P(wn | Ci))另一点,对于某一i和j,P(wj | Ci)可能为0,也就是在该类别中该词从未出现过。这很正常,如骂人的词就只能出现在脏话中。而对0取log会得到负无穷,这可不行,所以修改了上面代码,假设每个词在每一类中都出现过一次:p0Num = np.ones(numWords); p1Num = np.ones(numWords)
文中的另一项修改是p0Denom = 2.0; p1Denom = 2.0,这我就没看懂了,感觉改的毫无意义。。。
以上就为训练过程了,很简单,对于文本的二分类是适用的。更复杂的分类就要重写代码了。过程不重要,重要的是思路。
3. 使用分类器来分类
"""
很简单,思路就如上所示。
"""
# vec2Classify为待分类的词向量
# p0Vec和p1Vec各列为条件概率log(P(wj | Ci))
# pClass1为P(C1)
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
# log(P(w | C1) * P(C1))
p1 = np.sum(vec2Classify * p1Vec) + np.log(pClass1)
# log(P(w | C2) * P(C2))
p0 = np.sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0