1. 基本环境介绍
系统
Ubuntu Kylin
IDE
Pycharm Community
语言
Python3.4
2. 朴素贝叶斯简介
朴素贝叶斯是一种基于概率论的分类方法,利用条件概率对类别进行判决。 —— [ 维基百科 ]
以二类分类为例:假设现在我们有一个数据集,它有两类数据组成,并且使用
(x,y)
表示任一数据点的坐标。现在,用
p1(x,y)
表示数据点属于类别1
的概率,用
p2(x,y)
表示数据点属于类别2
的概率。那么对于一个新的数据点,可以使用下面的规则来判断它的类别
如果 p1(x,y) > p2(x,y) ,那么类别为 1 。
如果
p2(x,y) > p1(x,y) ,那么类别为 2 。
也就是说,我们会选择高概率对应的类别,这就是贝叶斯决策理论的思想。
但这两个准则并不是贝叶斯决策理论的所有内容,真正需要计算和比较的是
具体的,应用贝叶斯准则得到:
其中, ci 表示类别; p(x,y|ci) 是后验概率,表示已知为 ci 类别下,为 (x,y) 数据点的概率; p(ci|x,y) 是先验概率,表示已知数据点 (x,y) 的前提下,为类别 ci 的概率。
如果 p(c1|x,y) > p(c2|x,y) ,那么属于类别 c1 。
如果 p(c2|x,y) > p(c1|x,y) ,那么属于类别 c2 。
3. 具体实现
下面,我们以文本分类的例子来说明朴素贝叶斯的具体实现:以在线社区的留言板为例,为了不影响社区的发展,我们要屏蔽侮辱性言论,所以要构建一个快速过滤器,如果某条留言使用了负面或侮辱性语言,那么就将该留言标示为内容不当。对此问题建立两个类别:侮辱类和非侮辱类,使用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']]
# 是否含有侮辱性词汇: 0-未含有 1-含有
classVec = [0, 1, 0, 1, 0, 1]
return postingList, classVec
# 构造不含有重复词汇的集合vocabSet
def createVocabList(dataSet):
vocabSet = set([])
for document in dataSet:
# 取并集
vocabSet = vocabSet | set(document)
return list(vocabSet)
# 输入参数为:词汇表,输入文档
def setOfWords2Vec(vocabList, inputSet):
# 输出文档向量,表示词汇表中的单词是否出现:1-出现 0-未出现
returnVec = [0] * len(vocabList)
# 对于输入文档中的每个词汇
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
print("the word: %s is not in my Vocabulary!" % word)
return returnVec
# ============测试文档===========
listOPosts, listClasses = loadDataSet()
myVocabList = createVocabList(listOPosts)
print("VocabSet:", myVocabList, "\n")
print(setOfWords2Vec(myVocabList, listOPosts[0]))
2. 训练算法:从词向量计算概率
# 训练算法
from numpy import *
# 二类分类问题
# 该函数未考虑相乘概率值为0(即p0Num, p1Num, p0Denom, p1Denom的初始化)和下溢出(p1Vect和p0Vect)的问题
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'''
# 以上四行代码修改为:
p0Num = ones(numWords)
p1Num = ones(numWords)
p0Denom = 2.0
p1Denom = 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 = p1Num/p1Denom
p0Vect = p0Num/p0Denom'''
# 以上两行代码修改为:
p1Vect = log(p1Num/p1Denom)
p0Vect = log(p0Num/p0Denom)
return p0Vect, p1Vect, pAbusive
# 多类分类问题(以3类为例,其在trainCategory中的分类为0,1,2)
def trainNB4(trainMatrix, trainCategory):
# 取矩阵行数,作为文档数
numTrainDocs = len(trainMatrix)
# 取矩阵第一行的列数,作为词汇数
numWords = len(trainMatrix[0])
# 提取训练类别中的非重复元素(0,1,2)
mySet = set(trainCategory)
# 计算不同类别的发生概率,并存储在类别p中
p = []
for item in mySet:
p.append((trainCategory.count(item))/float(numWords))
# 构造向量用来存放对应三种类别的词汇出现的频次
# 不使用zeros的原因是:
# 当使用公式 p(w0,w1,w2,...,wN|Ci)=p(w0|Ci)*p(w1|Ci)*...*p(wN|Ci) 计算条件概率时,避免出现计算结果为0的情况
p0Num = ones(numWords)
p1Num = ones(numWords)
p2Num = ones(numWords)
# 以下三个变量用来存储三种类别下总词汇数,作为分母,为避免出现0值,因此初始化为2
# 不能初始化为1,因为分子初始化为1
p0Denom = 2.0
p1Denom = 2.0
p2Denom = 2.0
# 取每一个训练文本
for i in range(numTrainDocs):
if trainCategory[i] == 0:
# p0Num是一个长度为numWords的向量,p0Num += trainMatrix[i]表示对应词汇个数+1
p0Num += trainMatrix[i]
# 计算总的词汇个数,计算概率时作为分母
p0Denom += sum(trainMatrix[i])
elif trainCategory[i] == 1:
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else:
p2Num += trainMatrix[i]
p2Denom = sum(trainMatrix[i])
# 计算概率,取对数的原因是避免下溢出
# 下溢出:大量很小的浮点数相乘,四舍五入后得到0
p0Vect = log(p0Num/p0Denom)
p1Vect = log(p1Num/p1Denom)
p2Vect = log(p2Num/p2Denom)
return p, p0Vect, p1Vect, p2Vect
#=======测试代码=======
'''randMat = mat(random.random_integers(0,1,[6,32]))
用该代码生成一个6X32的伯努利矩阵,但不符合trainNB的规范,因此使用原书中的矩阵合并方式
# print(randMat)
Classes = [0,1,2,0,1,1]
pp, p0V, p1V, p2V = trainNB4(randMat, Classes)'''
# 测试二类分类器
trainMat = []
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
# print(trainMat)
p0Vec, p1Vec, pAb = trainNB0(trainMat, listClasses)
# 测试三类分类器
Classes = [0,1,2,0,1,1]
pp, p0V, p1V, p2V = trainNB4(trainMat, Classes)
print(pp,"\n",p0V,"\n",p1V,"\n",p2V,"\n")
3. 测试算法:朴素贝叶斯分类函数
# 朴素贝叶斯分类函数
# 输入参数:测试文本列表,文档分类为0的概率向量(对数),文档分类为1的概率向量,全部文档分类为1的概率
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
# 概率相乘,因为是对数形式,所以为求和形式
p1 = sum(vec2Classify * p1Vec) + log(pClass1)
p0 = sum(vec2Classify * p0Vec) + log(1.0 - 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))
testingNB()
# bag-of-words model:使用词带替换set
# 该模型考虑了词出现多次的情况
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
4. 垃圾邮件过滤
有了文本分类器以后,我们可以将其应用于垃圾邮件的过滤,并测试错误率。
#=======测试样例1:垃圾邮件过滤=======
import re
def textParse(bigString):
# 使用正则表达式去除文本中的空格和标点符号
listOfTokens = re.split(r'\\W*',bigString)
# 词汇全部小写,并且仅保留长度大于2的词汇
return[tok.lower() for tok in listOfTokens if len(tok) > 2]
# 交叉验证
def spamTest():
docList = []; classList = []; fullText = []
for i in range(1,26):
data = open('email/spam/%d.txt' % i,'r+',encoding = 'iso-8859-15').read()
wordList = textParse(data)
# wordList = textParse(open('email/spam/%d.txt' % i).read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
data = open('email/ham/%d.txt' % i,'r+',encoding = 'iso-8859-15').read()
wordList = textParse(data)
# wordList = textParse(open('email/ham/%d.txt' % i).read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
vocabList = createVocabList(docList)
trainingSet = list(range(50)); testSet = []
for i in range(10):
randIndex = int(random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
trainMat = []; trainClasses = []
for docIndex in trainingSet:
trainMat.append(setOfWords2Vec(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
p0V, p1V, pAb = trainNB0(array(trainMat), array(trainClasses))
errorCount = 0
for docIndex in testSet:
wordVector = setOfWords2Vec(vocabList, docList[docIndex])
if classifyNB(array(wordVector), p0V, p1V, pAb) != classList[docIndex]:
errorCount += 1
print("The error rate is: ", float(errorCount)/len(testSet))
spamTest()
测试结果为:The error rate is: 0.3
当然,由于是随机选取的训练序列,因此每次运行结果不同。另外,我们在上述代码中还需要注意的是
python2.X
与
python3.X
的不同。在
python2.X
中,.txt
文档的读取是可以直接使用
wordList = textParse(open('email/spam/%d.txt' % i).read())
但是在
python3.X
中,情况则略有不同,此时的.txt
文档是以默认的UTF-8
格式读取的,而我们所使用的.txt
文档则并不是以UTF-8
格式进行编码的,因此运行程序时会报错。调试时我使用了各种编码格式包括utf-8, gbk, asicc
,发现都有问题。最后在stack overflow中找到了解决方法,就是使用如下代码进行读取即可完成
data = open('email/ham/%d.txt' % i,'r+',encoding = 'iso-8859-15').read()
wordList = textParse(data)
还需要说明的一点是:在
python3.X
中,要使用list(range(50))
代替range(50)
来生成列表。