文章目录
1.朴素贝叶斯法的Python实现
本小节将以文本分类为例,介绍朴素贝叶斯实现的整个过程。
朴素贝叶斯法相关概念及原理中提到,朴素贝叶斯法是典型的生成学习方法。
生成方法:
-
由训练数据学习联合概率分布P(X,Y)。
利用训练数据学习P(X|Y)和P(Y)的估计,得到联合概率分布:P(X,Y)=P(Y)P(X|Y); 概率估计方法:可以是极大似然估计或贝叶斯估计。
-
然后利用贝叶斯定理与学到的联合概率模型求得后验概率分布P(Y|X),进行分类预测。
P ( Y ∣ X ) = P ( Y ) P ( X ∣ Y ) ∑ Y P ( Y ) P ( X ∣ Y ) P(Y|X)=\frac{P(Y)P(X|Y)}{\sum_{Y}P(Y)P(X|Y)} P(Y∣X)=∑YP(Y)P(X∣Y)P(Y)P(X∣Y)
朴素贝叶斯法的基本假设是条件独立性。
- 分类预测:将输入x分到后验概率最大的类y。 y = a r g m a x c k P ( Y = c k ) ∏ j P ( X ( j ) = x ( j ) ∣ Y = c k ) y=argmax_{c_k}P(Y=c_k)\prod_jP(X^{(j)}=x^{(j)}|Y=c_k) y=argmaxckP(Y=ck)j∏P(X(j)=x(j)∣Y=ck)
下面我们就依照这个步骤框架来一步步实现朴素贝叶斯法。
1.1 准备数据:从文本中构建词向量
这算是一个预处理的部分。
要从文本中获取特征,需要先拆分文本。具体来说,这里的特征是来自文本的词条(token),一个词条是字符的任意组合。可以把词条想象为单词,也可以使用非单词词条,如URL、IP地址或者任意其他字符串。然后将每一个文本片段表示为一个词条向量,其中值为1表示词条出现在文档中,0表示词条未出现。(即one-hot编码)
本节例子是以在线社区额留言板为例,将侮辱类、非侮辱类发言进行分类,使用1和0分别表示。
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代表正常言论
return postingList,classVec
# 创建一个dataSet的所有文档中出现的不重复词的列表
def createVocabList(dataSet):
vocabSet = set([]) # 创建一个空集
for document in dataSet:
vocabSet = vocabSet | set(document) # 创建两个集合的并集
return list(vocabSet)
# 为输入的某个文档的单词集合构造词向量
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0]*len(vocabList) # 创建一个0向量
for word in inputSet:
if word in inputSet:
returnVec[vocabList.index(word)] = 1 # 贝努利模型
else:
print("the word: %s is not in my Vocabulary!" % word)
return returnVec
注释:
朴素贝叶斯分类器通常有两种实现方式:
- 基于贝努利模型实现:该实现方式中不考虑词在文档中出现的次数,只考虑出不出现。因此,在这个意义上相当于假设,词是等权重的。(本节采用了此种实现方式)
- 基于多项式模型实现:考虑词在文档中出现的次数。
1.2 训练算法:从词向量计算概率
假设我们用
w
⃗
\vec{w}
w表示词向量,
c
i
c_i
ci表示某一类别标签,那么我们最终的目标就是:
y
=
a
r
g
m
a
x
c
i
P
(
Y
=
c
i
)
∏
j
P
(
w
j
⃗
∣
Y
=
c
i
)
y=argmax_{c_i}P(Y=c_i)\prod_jP(\vec{w_j}|Y=c_i)
y=argmaxciP(Y=ci)j∏P(wj∣Y=ci)
现在我们有了可以用的训练数据:特征向量、类标签,下一步就是利用训练数据集来学习模型,也就是联合概率分布 P ( w ⃗ , c i ) P(\vec{w},c_i) P(w,ci),进一步地就是:
- 先验概率 P ( Y = c i ) P(Y=c_i) P(Y=ci)
- 条件概率 P ( w ⃗ ∣ c i ) P(\vec{w}|c_i) P(w∣ci),加上条件独立性假设, P ( w ⃗ ∣ c i ) = ∏ j P ( w j ⃗ ∣ Y = c i ) P(\vec{w}|c_i)=\prod_{j}P(\vec{w_j}|Y=c_i) P(w∣ci)=∏jP(wj∣Y=ci)
对于以上两个概率的估计,我们这里使用极大似然估计。于是我们有训练函数:
# 朴素贝叶斯分类器训练函数
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]) 原书中的错误写法
p1Denom += 1
else:
p0Num += trainMatrix[i]
# p0Denom += sum(trainMatrix[i]) 原书中的错误写法
p0Denom += 1
# 对每个元素做除法
p1Vect = p1Num/p1Denom
p0Vect = p0Num/p0Denom
return p0Vect,p1Vect,pAbusive
1.3 测试算法:根据现实情况修改分类器
trainNB0(trainMatrix, trainCategory)完成了对上面先验概率、条件概率的概率估计,但是就像我们在朴素贝叶斯法相关概念及原理中提到的,现在我们的训练函数有两个问题:
- 可能出现某一个特征的条件概率 P ( w j ⃗ ∣ Y = c i ) P(\vec{w_j}|Y=c_i) P(wj∣Y=ci)为0的情况,进而影响后验概率的计算结果,使分类产生偏差,所以我们改用贝叶斯估计。
将所有词的出现次数初始化为1,并将分母初始化为2:
# 根据现实情况修改分类器
p0Num = ones(numWords); p1Num = ones(numWords) # 分子
p0Denom = 2.0; p1Denom = 2.0 # 分母
- 另一个问题就是下溢出,我们通过对乘积取自然对数来解决这个问题。
# 根据现实情况修改分类器
p1Vect = log(p1Num/p1Denom)
p0Vect = log(p0Num/p0Denom)
到目前为止,我们已经构建了一个比较完整的训练函数了,下面我们就利用训练函数得到的先验概率及条件概率去构建我们完整的分类器。
# 朴素贝叶斯分类函数
# 根据训练得到的三个概率对要分类的向量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
# 便利函数(convenience function),该函数封装所有操作
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))
关于向量/矩阵乘法与对应元素相乘的注释:
- np.dot(a,b):表示矩阵/向量乘法。特别地,a,b均为向量的时候,.dot实现内积,不用考虑a,b具体是行向量还是列向量;
- a*b或np.multiply(a,b):表示矩阵/向量按对应位置相乘。特别地,当a,b分别为行向量和列向量时,不管a,b谁在前边都是一样的结果。
来看一下算法运行效果如何:
testingNB()
-->Out:['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1
到目前为止,我们已经完整实现了一个朴素贝叶斯分类器,并且可以成功用它来对留言进行分类。是不是瞬间感觉朴素贝叶斯非常easy!下面两节我们将再用两个经典的应用实例来巩固我们对朴素贝叶斯的掌握。
1.4 准备数据:文档词袋模型
我们前面提到,朴素贝叶斯分类器通常有两种实现方式:
-
基于贝努利模型实现:将每个词出现与否作为一个特征,这可以描述为词集模型(set-of-words model)
-
基于多项式模型实现:若一个词在文档中出现不止一次,这可能意味着包含/该词是否出现在文档中/(即词集模型)所不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)。
也就是说: 在词集中,每个词只能出现一次; 在词袋中,每个单词可以出现多次
为了适应词袋模型,需要对函数setOfWords2Vec()稍加修改,修改后的函数名称为bagOfWords2VecMN(),如下:
# 朴素贝叶斯词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1 # 词袋模型
return returnVec
2.示例1:使用朴素贝叶斯过滤垃圾邮件
2.1 准备数据:切分文本
在第1节的例子中,我们直接引入了字符串列表,但是在现实生活中,我们需要先从文本内容得到字符串列表,然后生成词向量。
# 文本解析,将一个大字符串解析为字符串列表
def textParse(bigString):
listOfTokens = re.split(r'\W+', bigString)
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
2.2 测试算法:使用朴素贝叶斯进行交叉验证
# 完整的垃圾邮件测试函数,对朴素贝叶斯垃圾邮件分类器进行自动化处理
def spamTest():
docList=[]; classList=[]; fullText=[]
for i in range(1,26):
# 导入并解析文本文件
wordList = textParse(open("email/spam/%d.txt" % i).read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
wordList = textParse(open("email/ham/%d.txt" % i).read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
vocabList = createVocabList(docList)
# trainingSet = range(50) 原书中的错误写法
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,pSpam = trainNB0(array(trainMat), array(trainClasses))
errorCount = 0
# 对测试集分类
for docIndex in testSet:
wordVector = setOfWords2Vec(vocabList, docList[docIndex])
if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
errorCount += 1
print("classification error",docList[docIndex])
errorRate = float(errorCount)/len(testSet)
print("the error rate is: ", errorRate)
return (float(errorCount)/len(testSet))
注释:
-
留存交叉验证(hold-out cross validation):随机选择数据的一部分作为训练集,而剩余部分作为测试集的过程。
-
正则表达式:
\w:用于匹配字母,数字或下划线字符; \W:用于匹配所有与\w不匹配的字符; *:前一个字符0次或多次扩展; +:前一个字符1次或多次扩展; re.compile(pattern, flags=0) Compile a regular expression pattern, returning a pattern object.返回一个匹配对象,它单独使用就没有任何意义,需要和findall(), search(), match()等搭配使用。
-
带有条件判断的列表推导式,共有两种形式:
[x for x in data if condition] [exp1 if condition else exp2 for x in data]
-
random.uniform(a,b):随机生成一个在[a,b)的实数
运行一下看看效果:
spamTest()
-->Out: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词,求平均错误率:
# 将spamTest()重复10次,求平均错误率
errorRateList = []
for i in range(10):
errorRateList.append(spamTest())
print("the average errorrate is: ",average(errorRateList))
3.示例2:使用贝叶斯分类器从个人广告中获取区域倾向
书中作者的意思是以来自源 http://newyork.craigslist.org/stp/index.rss 中的文章作为分类为1的文章,以来自源 http://sfbay.craigslist.org/stp/index.rss 中的文章作为分类为0的文章,但是由于FW的原因,访问不到作者所说的数据,所以在网上找了两个替代源:
- NASA Image of the Day:http://www.nasa.gov/rss/dyn/image_of_the_day.rss
- Yahoo Sports - NBA - Houston Rockets News:http://sports.yahoo.com/nba/teams/hou/rss.xml
import operator
import feedparser
# RSS源分类器及高频词去除函数
# 统计词频,并降序排序,返回词频最高的top-k
def calcMostFreq(vocabList,fullText):
freqDict = {}
# 计算出现频率
for token in vocabList:
freqDict[token] = fullText.count(token)
sortedFreq = sorted(freqDict.items(), key=operator.itemgetter(1), reverse=True)
return sortedFreq[:30]
# 与spam_Test()函数几乎相同
def localWords(feed1,feed0):
docList=[]; classList=[]; fullText=[]
minLen = min(len(feed1['entries']),len(feed0['entries']))
for i in range(minLen):
# 每次访问一条RSS源
wordList = textParse(feed1['entries'][i]['summary'])
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
wordList = textParse(feed0['entries'][i]['summary'])
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
vocabList = createVocabList(docList)
top30Words = calcMostFreq(vocabList, fullText)
# 从词汇表中,去掉出现次数最高的那些词
for pairW in top30Words:
if pairW[0] in vocabList:
vocabList.remove(pairW[0])
# 去除停用词
stopwords=open("stop_words.utf8", encoding="utf-8").readlines()
stopwords = [st.replace('\n', '') for st in stopwords]
vocabList = [pairW for pairW in vocabList if pairW not in stopwords]
trainingSet = list(range(2*minLen)); testSet=[]
for i in range(4):
randIndex = int(random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
trainMat=[]; trainClasses=[]
for docIndex in trainingSet:
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
errorCount = 0
for docIndex in testSet:
wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
errorCount += 1
print("the error rate is: ",float(errorCount)/len(testSet))
return vocabList,p0V,p1V
nasa = feedparser.parse('http://www.nasa.gov/rss/dyn/image_of_the_day.rss')
yahoo = feedparser.parse('http://sports.yahoo.com/nba/teams/hou/rss.xml')
vocabList,pYahoo,pNasa=localWords(nasa,yahoo)
-->Out:the error rate is: 0.25
注释:
-
localWords与spamTest几乎相同,唯一的区别就是:这里访问的是RSS源,而不是文件。
-
值得注意的是,RSS源要在函数外导入,这样做的原因是RSS源会随时间而改变。
-
上面函数中不仅移除了高频词,也加载了停用词表(stop word list)去除停用词。
-
operator.itemgetter(1):operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为一些序号。
-
del,list.remove(),list.pop()
list.remove():删除单个元素,删除首个符合条件的元素,按值删除,返回值为空; list.pop():删除索引位置元素,无参情况下删除最后一个元素,返回删除的元素值; del:根据索引位置来删除单个值或指定范围内的值
同样地,为了得到错误率的精确估计,应该多次进行上述实验,然后取平均值。可以发现,这里的错误率要远高于垃圾邮件分类中的错误率,由于这里关注的是单词概率而不是实际分类,所以问题不大。
下面就来显示地域相关的用词。可以先对向量pSF与pNY进行排序,然后按照顺序将词打印出来。
# 最具表征性的词汇显示函数
def getTopWords(ny,sf):
vocabList,p0V,p1V=localWords(ny,sf)
topNY=[]; topSF=[]
for i in range(len(p0V)):
if p0V[i] > -6.0:
topSF.append((vocabList[i],p0V[i]))
if p1V[i] > -6.0:
topNY.append((vocabList[i],p1V[i]))
sortedSF = sorted(topSF, key=lambda pair: pair[1], reverse=True)
print("SF**"*15)
for item in sortedSF:
print(item[0])
sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)
print("NY**"*15)
for item in sortedNY:
print(item[0])
注释:
-
lambda表达式:通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数。
例子:将列表中的元素按照绝对值大小进行升序排列 list1 = [3,5,-4,-1,0,-2,-6] sorted(list1, key=lambda x: abs(x))
参考资料:
1.《机器学习实战》第4章
2.另外的RSS源