朴素贝叶斯概述与文本分类
书上对朴素贝叶斯的解释有点绕,我结合自己的理解用案例做一点解释。
朴素贝叶斯分类器是一类应用贝叶斯定理分类器的总称。朴素贝叶斯分类器以贝叶斯定理为基础, 朴素指的是假设各个特征之间相互独立,互不影响。贝叶斯定理基于条件概率,即:在事件B发生的前提下,事件A发生的概率。由此可知,贝叶斯思想是从先验概率计算出后验概率。
因为各个特征之间相互独立,所以:
P(X,Y) = P(X)·P(Y)
条件概率公式为:
P(Y|X) = P(X,Y)/P(X)
P(X|Y) = P(X,Y)/P(Y)
条件公式联立代换得:
P(Y|X) = P(X|Y)P(Y) / P(X)
这就是贝叶斯定理。
那么用于监督分类时,令X代表一个n维的向量,表示n个特征;C表示标签。分类的目的就是找到X和C之间的对应关系,从而计算出P(C|X),即当预测对象具备X的特征时,分类为C的概率。公式如下:
举个例子:
此处的C就是["男", "女"]标签;X指的就是["驾龄", "平均车速"]。我们现在有一个预测对象"驾龄=2","平均车速=80"。我们想预测一下驾驶员的性别。使用的还是这个公式:
所谓由先验概率计算出后验概率,就是指我们先对答案进行假设,最后挑选出概率大的。譬如该案例,先假设性别为男,在计算性别为男的前提下,每一个特征的概率(此处仅有两个特征);再假设性别为女,同样计算。最后比较两种答案的可能性,性别为男大于性别为女,所以分类器输出概率大的。
我们注意到计算过程中只用到了公式右边的分子,没有计算分母的原因是我们最后是要比较两个概率的大小,而此处的P(X)是不变的,所以计算省略。
由于条件独立性这个强假设的存在,朴素贝叶斯分类器十分简单。但是,它仍然有非常不错的效果。原因何在呢?人们在使用分类器之前,首先做的第一步(也是最重要的一步)往往是特征选择(feature selection),这个过程的目的就是为了排除特征之间的共线性、选择相对较为独立的特征。其次,当我们假设特征之间相互独立时,这事实上就暗含了正则化的过程;而不考虑变量之间的相关性有效的降低了朴素贝叶斯的分类方差。虽然这有可能提高分类的偏差,但是如果这样的偏差不改变样本的排列顺序,那么它对分类的结果影响不大。
Python进行文本分类
要从文本中获取特征,需要先拆分文本。这里说的特征是来自于文中的词条,一个词条是字符的任意组合。可以把词条想象为单词,也可以是非单词的URL、IP等)。然后将每一个文本片段表示为一个词条向量,其中值为1表示词条出现在文档中,0表示未出现。
以留言板为例,我们需要屏蔽侮辱性言论,所以要构建一个快速过滤器,如果某条留言出现了负面或者侮辱性言论,那么将该留言标记为内容不当。对此问题建立两个类别:侮辱类和非侮辱类,使用1和0表示。
接下来,我们首先将文本转换为数字向量,然后介绍如何基于这些向量来计算条件概率。
准备数据:从文本中构建词向量
我们要将句子转换为向量,然后考虑出现在所有文档中的所有单词,并决定将哪些单词纳入词汇表(词汇集合),然后必需将每一篇文档转换为词汇表上的向量。
创建bayes.py并添加下面程序:
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]
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) # 创建一个长度为len(vocabList)并且元素均为0的列表作为向量
for word in inputSet: # 检索输入文本的每一个单词
if word in vocabList: # 如果单词出现在词汇表中
returnVec[vocabList.index(word)] = 1 # 将向量对应位置的元素改为1
else:print("the word: %d is not in my Vocabulary!" % word)
return returnVec
注意,贝叶斯分类器通常有两种实现方式:一个是伯努利模型,一个是多项式模型。区别就在于一个是考虑是否出现,一个是考虑出现的次数。这里是基于伯努利模型。
上面我们通过函数loadDataSet()创建了数据集,该数据集来自于斑点狗爱好者。函数返回两个变量,一个是数据,一个是标签。标签是人工标注的,由0和1组成,表示非侮辱性和侮辱性。createVocbList(dataSet)函数首先创建了一个空集,接着遍历输入数据的每一个元素,将该元素与集合做交集,所以最终的列表是没有重复的。setOfWords2Vec(vocabList, inputSet)函数则用于判断输入是否含有侮辱性词汇。首先创建一个与输入等长的向量,向量均由0组成。接着判断输入中的每一个单词,如果该单词在词汇表中出现,就修改该词汇对应向量位置的元素。
测试一下:
import bayes
listOPosts, listClasses = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOPosts)
print(myVocabList)
>>>
['help', 'dalmation', 'take', 'maybe', 'I', 'problems', 'posting', 'steak', 'flea', 'quit', 'has', 'dog', 'him', 'mr', 'worthless', 'garbage', 'please', 'park', 'my', 'is', 'love', 'cute', 'not', 'buying', 'ate', 'stop', 'stupid', 'licks', 'how', 'so', 'to', 'food']
Process finished with exit code 0
可以看到结果中没有重复元素。
接着测试setOfWords2Vec()的运行效果,我们使用前面的结果作为词汇表,查询输入数据是否包含关键词:
import bayes
listClasses = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOPosts)
listOPosts[0]))
>>>
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0]
Process finished with exit code 0
训练算法,从词向量计算概率
前面讲了如何将一组单词转换成一组数字,那么如何使用这些数字计算概率?现在我们知道一个词是否出现在一篇文档中,也知道该文档所属的类别,接着通过贝叶斯定理计算概率。
将贝叶斯定理的X替换为w,表示一个向量。
首先用类别i的个数除以文档总数计算p(Ci)。之后计算p(w|Ci),这里就要用到朴素贝叶斯假设,如果将w展开为数个独立特征,则p(w|Ci)=p(w0|Ci)p(w1|Ci)p(w2|Ci)...p(wn|Ci),极大地简化了计算过程。
核心代码:
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 读取训练集
numWords = len(trainMatrix[0]) # 数据集实例的长度
pAbusive = sum(trainCategory)/float(numTrainDocs) # 此处计算的是p(i=1),先sum(trainCategory)计算了类别为”1“的个数,再除以数据集的大小
p0Num = zeros(numWords); p1Num =zeros(numWords)
p0Denom = 0.0; p1Denom = 0.0
for i in range(numTrainDocs): # 对于数据集中的每一个实例
if trainCategory[i] == 1: # 如果标签是”1“
p1Num += trainMatrix[i] # 修改向量为”1“
p1Denom += sum(trainMatrix[i]) # 向量相加
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
# 循环结束后,p1Num是一个存储了”1“出现次数的向量,向量每个值都是次数
# p1Denom将每一个标签为”1“的向量相加
p1Vect = p1Num / p1Denom # 对每个元素做除法
p0Vect = p0Num / p0Denom
return p0Vect, p1Vect, pAbusive
测试一下:
import bayes
listOPosts, listClasses = bayes.loadDataSet() # 加载测试数据集和标签列表
myVocabList = bayes.createVocabList(listOPosts) # 整理词汇表
print(myVocabList)
trainMat = []
for i in listOPosts:
trainMat.append(bayes.setOfWords2Vec(myVocabList, i)) # 将每个实例数据转化成向量
p0v, p1v, pAb = bayes.trainNB0(trainMat, listClasses)
print(p0v)
print(p1v)
print(pAb)
>>>
['my', 'please', 'ate', 'help', 'park', 'cute', 'quit', 'food', 'posting', 'problems', 'I', 'worthless', 'has', 'garbage', 'buying', 'so', 'stop', 'stupid', 'him', 'steak', 'is', 'mr', 'how', 'take', 'maybe', 'dog', 'love', 'flea', 'not', 'licks', 'dalmation', 'to']
32
[0.125 0.04166667 0.04166667 0.04166667 0. 0.04166667
0. 0. 0. 0.04166667 0.04166667 0.
0.04166667 0. 0. 0.04166667 0.04166667 0.
0.08333333 0.04166667 0.04166667 0.04166667 0.04166667 0.
0. 0.04166667 0.04166667 0.04166667 0. 0.04166667
0.04166667 0.04166667]
[0. 0. 0. 0. 0.05263158 0.
0.05263158 0.05263158 0.05263158 0. 0. 0.10526316
0. 0.05263158 0.05263158 0. 0.05263158 0.15789474
0.05263158 0. 0. 0. 0. 0.05263158
0.05263158 0.10526316 0. 0. 0.05263158 0.
0. 0.05263158]
ratio 1:0.5
Process finished with exit code 0
首先标签为侮辱的概率为0.5,正确。词汇表第二个单词是"please",在侮辱类未出现,概率是0;在非侮辱类出现1次,概率是0.04166667。下面我们找出概率最大的是0.15789474,下标为18,对应”stupid“。
测试算法:根据现实情况修改分类器
贝叶斯对文档进行分类时,要计算多个概率的乘积,如果有一个概率值为0,那最后的乘积也是0。为了降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。
修改bayes.py文件trainNB0()的第4行,第5行。
p0Num = ones(numWords); p1Num =ones(numWords)
p0Denom = 2.0; p1Denom = 2.0
另一个问题是下溢出,这是由于太多很小的数相乘造成的。当相乘的一系列因子都很小时,程序得不到正确答案(都会得到0)。一种解决办法是对乘积取自然对数,修改return前的代码:
p1Vect = log(p1Num/p1Denom)
p0Vect = log(p0Num/p0Denom)
至此,我们已经准备好构建完整的分类器了。
def classifyNB(vec2Classify, p0vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + log(pClass1) # 元素相乘
p0 = sum(vec2Classify * p0vec) + log(1 - pClass1)
if p1 > p0:
return 1
else:
return 0
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"]
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, "classified as: ", classifyNB(thisDoc, p0V, p1V, pAb))
testEntry = ["stupid", "garbage"]
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, "classified as: ", classifyNB(thisDoc, p0V, p1V, pAb))
测试代码:
import bayes
bayes.testingNB()
>>>
['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1
Process finished with exit code 0
对testingNB()这个测试器内的文本进行修改,会得到不同文本的判定结果,这个例子比较简单,但是展示了朴素贝叶斯分类器的工作原理。
词袋模型
上面我们建立词汇表时,采用的是去冗余集合,那么”一个词在文中是否出现“与“一个词在文中出现多次”代表的含义是不同的。这也是伯努利模型和多项式模型的区别。如果一个单词在文中出现多次,那么可能传达了某种信息,而这种信息是"这个词在文中是否出现"所无法传达的,这种方法是词袋模型。我们将上面的词集模型稍加修改:
def bagOfWordsVecMN(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
至此,完整的朴素贝叶斯分类就构建完成,第一次码这么多字。下面我们可以用这个分类器过滤垃圾邮件。