本篇内容为《机器学习实战》第 4 章 基于概率论的分类方法:朴素贝叶斯程序清单。所用代码为 python3。
朴素贝叶斯优点:在数据较少的情况下仍然有效,可以处理多类别问题。
缺点:对于输入数据的准备方式较为敏感。
适用数据类型:标称型数据。
使用 Python 进行文本分类
简单描述这个过程为:从文本中获取特征,构建分类器,进行分类输出结果。这里的特征是来自文本的词条 (token),需要将每一个文本片段表示为一个词条向量,其中值为 1 表示词条出现在文档中,0 表示词条未出现。
接下来给出将文本转换为数字向量的过程,然后基于这些向量来计算条件概率,并在此基础上构建分类器。
下面我们以在线社区的留言板为例,给出一个用来过滤的例子。
为了不影响社区的发展,我们需要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用来负面或者侮辱性的语言,就将该留言标识为内容不当。对此问题建立两个类别:侮辱类和非侮辱类,分别使用 1 和 0 来表示。
准备数据:从文本中构建词向量
程序清单 4-1 词表到向量的转换函数
'''
Created on Sep 10, 2018
@author: yufei
'''
# coding=utf-8
from numpy import *
# 创建一些实例样本
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 代表侮辱性文字,0 代表正常言论
"""
变量 postingList 返回的是进行词条切分后的文档集合。
留言文本被切分成一些列词条集合,标点符号从文本中去掉
变量 classVec 返回一个类别标签的集合。
这些文本的类别由人工标注,标注信息用于训练程序以便自动检测侮辱性留言。
"""
return postingList, classVec
"""
创建一个包含在所有文档中出现的不重复词的列表
是用python的 Set 数据类型
将词条列表输给 Set 构造函数,set 就会返回一个不重复词表
"""
def createVocabList(dataSet):
# 创建一个空集合
vocabSet = set([])
# 将每篇文档返回的新词集合添加进去,即创建两个集合的并集
for document in dataSet:
vocabSet = vocabSet | set(document)
# 获得词汇表
return list(vocabSet)
# 参数:词汇表,某个文档
def setOfWords2Vec(vocabList, inputSet):
# 创建一个和词汇表等长的向量,将其元素都设置为 0
returnVec = [0] * len(vocabList)
# 遍历文档中所有单词
for word in inputSet:
# 如果出现词汇表中的单词,将输出的文档向量中的对应值设为 1
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
print('the word: %s is not in my Vocabulary!' % word)
# 输出文档向量,向量元素为 1 或 0
return returnVec
在 python 提示符下,执行代码并得到结果:
>>> import bayes
>>> list0Posts, listClasses = bayes.loadDataSet()
>>> myVocabList = bayes.createVocabList(list0Posts)
>>> myVocabList
['problems', 'mr', 'ate', 'buying', 'not', 'garbage', 'how', 'maybe', 'stupid', 'cute', 'stop', 'help', 'dalmation', 'take', 'is', 'worthless', 'him', 'flea', 'park', 'my', 'I', 'to', 'licks', 'steak', 'dog', 'love', 'quit', 'so', 'please', 'posting', 'has', 'food']
即可得到的一个不会出现重复单词的词表myVocabList
,目前该词表还没有排序。
继续执行代码:
>>> bayes.setOfWords2Vec(myVocabList, list0Posts[3])
[0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
>>> bayes.setOfWords2Vec(myVocabList, list0Posts[0])
[0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0]
函数setOfWords2Vec
使用词汇表或者说想要检查的所有单词作为输入,然后为其中每一个单词构建一个特征。一旦给定一篇文章(本例中指一条留言),该文档就会被转换为词向量。
训练算法:从词向量计算概率
函数伪代码如下:
··· 计算每个类别中的文档数目
··· 对每篇训练文档:
······ 对每个类别:
········· 如果词条出现在文档中—>增加该词条的计数值
········· 增加所有词条的计数值
······ 对每个类别:
········· 对每个词条:
············ 将该词条对数目除以总词条数目得到条件概率
······ 返回每个类别对条件概率
程序清单 4-2 朴素贝叶斯分类器训练函数
'''
Created on Sep 11, 2018
@author: yufei
'''
# 参数:文档矩阵 trainMatrix,每篇文档的类别标签所构成的向量 trainCategory
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) #文档的个数
numWords = len(trainMatrix[0]) #获取第一篇文档的单词长度
"""
计算文档属于侮辱性文档的概率
用类别为1的个数除以总篇数
sum([0,1,0,1,0,1])=3,也即是 trainCategory 里面 1 的个数
"""
pAbusive = sum(trainCategory) / float(numTrainDocs)
"""
初始化概率
当利用贝叶斯分类器对文档分类时,计算多个概率的乘积以获得属于某个类别的概率
把所有词出现次数初始化为1,分母初始化为2,用log避免数太小被约掉
"""
p0Num = ones(numWords)
p1Num = ones(numWords)
p0Denom = 2.0
p1Denom = 2.0
# 遍历训练集 trainMatrix 中的所有文档
for i in range(numTrainDocs):
# 侮辱性词语在某个文档中出现
if trainCategory[i] == 1:
# 该词对应个数加一,即分子把所有的文档向量按位置累加
# trainMatrix[2] = [1,0,1,1,0,0,0];trainMatrix[3] = [1,1,0,0,0,1,1]
p1Num += trainMatrix[i]
# 文档总词数加一,即对于分母
# 把trainMatrix[2]中的值先加起来为3,再把所有这个类别的向量都这样累加起来,这个是计算单词总数目
p1Denom += sum(trainMatrix[i])
# 正常词语在某个文档中出现,同上
else:
p0Num += trainMatrix[i]
p0Denom +=sum(trainMatrix[i])
"""
对每个元素除以该类别的总词数,得条件概率
防止太多的很小的数相乘造成下溢。对乘积取对数
# p1Vect = log(p1Num / p1Denom)
# p0Vect = log(p0Num / p0Denom)
"""
p1Vect = p1Num / p1Denom
p0Vect = p0Num / p0Denom
"""
函数返回两个向量和一个概率
返回每个类别的条件概率,是一个向量
在向量里面和词汇表向量长度相同
每个位置代表这个单词在这个类别中的概率
"""
return p0Vect, p1Vect, pAbusive
在 python 提示符下,执行代码并得到结果:
>>> from numpy import *
>>> importlib.reload(bayes)
>>> list0Posts, listClasses = bayes.loadDataSet()
>>> myVocabList = bayes.createVocabList(list0Posts)
以上,调入数据后构建了一个包含所有词的列表myVocabList
>>> trainMat = []
>>> for postinDoc in list0Posts:
... trainMat.append(bayes.setOfWords2Vec(myVocabList, postinDoc))
这个for
循环使用词向量来填充trainMat
列表。
继续给出属于侮辱性文档的概率以及两个类别的概率向量。
>>> p0V, p1V, pAb = bayes.trainNB0(trainMat, listClasses)
查看变量的内部值
>>> pAb
0.5
>>> p0V
array([0.03846154, 0.07692308, 0.03846154, 0.07692308, 0.07692308,
0.07692308, 0.07692308, 0.03846154, 0.03846154, 0.03846154,
0.07692308, 0.07692308, 0.15384615, 0.07692308, 0.07692308,
0.07692308, 0.03846154, 0.07692308, 0.07692308, 0.07692308,
0.07692308, 0.07692308, 0.03846154, 0.07692308, 0.11538462,
0.07692308, 0.07692308, 0.03846154, 0.03846154, 0.03846154,
0.07692308, 0.03846154])
>>> p1V
array([0.0952381 , 0.04761905, 0.0952381 , 0.0952381 , 0.14285714,
0.04761905, 0.04761905, 0.0952381 , 0.0952381 , 0.14285714,
0.04761905, 0.04761905, 0.04761905, 0.04761905, 0.04761905,
0.04761905, 0.0952381 , 0.04761905, 0.04761905, 0.04761905,
0.0952381 , 0.04761905, 0.0952381 , 0.04761905, 0.0952381 ,
0.04761905, 0.04761905, 0.19047619, 0.0952381 , 0.0952381 ,
0.04761905, 0.0952381 ])
我们发现文档属于侮辱类的概率pAb
为 0.5,查看pV1
的最大值 0.19047619,它出现在第 27 个下标位置,查看myVocabList
的第 27 个下标位置该词为 stupid,说明这是最能表征类别 1 的单词。
测试算法:根据现实情况修改分类器
程序清单 4-3 朴素贝叶斯分类函数
'''
Created on Sep 11, 2018
@author: yufei
'''
# vec2Classify: 要分类的向量
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():
list0Posts, listClasses = loadDataSet()
myVocabList = createVocabList(list0Posts)
trainMat = []
for posinDoc in list0Posts:
trainMat.append(setOfWords2Vec(myVocabList, posinDoc))
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))
在 python 提示符下,执行代码并得到结果:
>>> importlib.reload(bayes)
>>> bayes.testingNB()
['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1
分类器输出结果,分类正确。
准备数据:文档词袋模型
词集模型:将每个词的出现与否作为一个特征。即我们上面所用到的。
词袋模型:将每个词出现次数作为一个特征。每遇到一个单词,其词向量对应值 +1,而不是全设置为 1。
对函数setOfWords2Vec()
进行修改,修改后的函数为bagOfWords2VecMN
。
程序清单 4-4 朴素贝叶斯词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in inputSet:
returnVec[vocabList.index(word)] += 1
return returnVec
修改的地方为:每当遇到一个单词时,它会增加词向量中的对应值,而不只是将对应的数值设为 1。
下面我们将利用该分类器来过滤垃圾邮件。
示例:使用朴素贝叶斯过滤垃圾邮件
测试算法:使用朴素贝叶斯进行交叉验证
程序清单 4-5 文件解析及完整的垃圾邮件测试函数
'''
Created on Sep 11, 2018
@author: yufei
'''
"""
接受一个大字符串并将其解析为字符串列表
"""
def textParse(bigString): #input is big string, #output is word list
import re
listOfTokens = re.split(r'\W*', bigString)
# 去掉小于两个字符的字符串,并将所有字符串转换为小写
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
"""
对贝叶斯垃圾邮件分类器进行自动化处理
"""
def spamTest():
docList=[]; classList = []; fullText =[]
#导入并解析文本文件为词列表
for i in range(1,26):
wordList = textParse(open('email/spam/%d.txt' % i, encoding='ISO-8859-1').read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
wordList = textParse(open('email/ham/%d.txt' % i, encoding='ISO-8859-1').read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
vocabList = createVocabList(docList)#create vocabulary
trainingSet = list(range(50)); testSet=[] #create test set
for i in range(10):
randIndex = int(random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
trainMat=[]; trainClasses = []
# 遍历训练集的所有文档,对每封邮件基于词汇表并使用 bagOfWords2VecMN 来构建词向量
for docIndex in trainingSet:#train the classifier (get probs) trainNB0
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
# 用上面得到的词在 trainNB0 函数中计算分类所需的概率
p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
errorCount = 0
# 对测试集分类
for docIndex in testSet: #classify the remaining items
wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
# 如果邮件分类错误,错误数加 1
if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
errorCount += 1
print ("classification error",docList[docIndex])
# 给出总的错误百分比
print ('the error rate is: ',float(errorCount)/len(testSet))
#return vocabList,fullText
在 python 提示符下,执行代码并得到结果:
>>> importlib.reload(bayes)
>>> bayes.spamTest()
classification error ['home', 'based', 'business', 'opportunity', 'knocking', 'your', 'door', 'don', 'rude', 'and', 'let', 'this', 'chance', 'you', 'can', 'earn', 'great', 'income', 'and', 'find', 'your', 'financial', 'life', 'transformed', 'learn', 'more', 'here', 'your', 'success', 'work', 'from', 'home', 'finder', 'experts']
the error rate is: 0.1
函数spamTest()
会输出在 10 封随机选择的电子邮件上的分类错误率。由于是随机选择的,所以每次的输出结果可能有些差别。如果想要更好地估计错误率,那么就应该将上述过程重复多次求平均值。
这里的代码需要注意的两个地方是:1、直接使用语句
wordList = textParse(open('email/spam/%d.txt' % i).read())
报错UnicodeDecodeError: 'utf-8' codec can't decode byte 0x92 in position 884: invalid start byte
。这是因为在文件里可能存在不是以 utf-8 格式保存的字符,需改为wordList = textParse(open('email/spam/%d.txt' % i, encoding='ISO-8859-1').read())
。2、将随机选出的文档添加到测试集后,要同时将其从训练集中删除,使用语句
del(trainingSet[randIndex])
,此时会报错TypeError: 'range' object doesn't support item deletion
,这是由于 python2 和 python3 的不同而导致的。在 python2 中可以直接执行,而在 python3 中需将trainingSet
设为trainingSet = list(range(50))
,而不是trainingSet = range(50)
,即必须让它是一个list
再进行删除操作。
以上,我们就用朴素贝叶斯对文档进行了分类。
参考链接:
《机器学习实战》笔记之四——基于概率论的分类方法:朴素贝叶斯
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x92 in position 884: invalid start byte
不足之处,欢迎指正。