朴素贝叶斯
优点: 在数据较少的情况下仍然有效,可以处理多类别问题。
缺点: 对于输入数据的准备方式较为敏感。
适用数据类型: 标称型数据。
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']]
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)
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
在正式开始前,我们先准备一组简单的数据。loadDataSet函数创建了一组词条切分后的文档和标签列表,这些文档来自斑点犬爱好者留言板。
createVocabList函数将列表中的各词条去重后返回。
setOfWords2Vec首先创建一个词条列表长度的全0向量,遍历输入文档,如果词条列表中词条在文档中出现,那么将对应索引的0该为1。最后返回该向量。
2.训练算法
from numpy import *
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])
else:
p0Num += trainMatrix[I]
p0Denom += sum(trainMatrix[I])
p1Vect = p1Num / p1Denom
p0Vect = p0Num / p0Denom
return p0Vect, p1Vect, pAbusive
函数需要两个参数。trainMatrix使用我们第1节的函数来创建,其为每行长度等于vocabSet(即无重复词条数),行数等于文档数(loadDataSet创建的列表中的子列表数)。这两个数据我们分别保存在变量numWords和numTrainDocs中。
trainCategory参数接受标签列表。用列表中要素的和(侮辱性语句标签为1,所以和即侮辱性语句总数),除以文档数(句数),即为侮辱性语句出现的概率。
为什么要计算这个概率?这里首先简要说明一下贝叶斯准则。
贝叶斯准则使用了条件概率的概念。其中,
p(c)是c的先验概率或边缘概率。之所以称为"先验"是因为它不考虑任何x方面的因素。同理p(x)是x的先验概率。
p(c|x)是已知x发生后c的条件概率,也由于得自x的取值而被称作c的后验概率。
p(x|c)是已知c发生后x的条件概率,也由于得自c的取值而被称作x的后验概率。
如果P(c1|x, y) > P(c2|x, y),那么属于类别c1。
如果P(c1|x, y) < P(c2|x, y),那么属于类别c2。
代码中使用的公式如上,其中w为ci发生条件下的词条个数。如果将w展开为一个个独立特征,那么就可以将上述概率写作p(w0,w1,w2..wN|ci)
代码中的p0Num和p1Num分别为侮辱性语句中各词条个数和正常语句中各词条个数的向量。
p0Denom和p1Denom则分别为侮辱性语句和正常语句中的词条总数。
我们通过向量运算可以计算出p1Vect和p0Vect,分别为侮辱性语句和正常语句中各个词条的出现概率。
最后把这2个概率和之前计算的侮辱性语句出现的概率pAbusive返回。
3.实现分类器
利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算p(w0|1)p(w1|1)p(w2|1)。如果其中一个概率值为0,那么最后的乘积也为0。为降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。
p0Num = ones(numWords); p1Num = ones(numWords)
p0Denom = 2.0; p1Denom = 2.0
另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积p(w0|ci)p(w1|ci)p(w2|ci)...p(wN|ci)时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。一 种解决办法是对乘积取自然对数。在代数中有ln(a*b) = ln(a)+ln(b),于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。这里我们使用的是numpy的log函数。
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.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))
classifyNB函数中,p1为分类为第1种标签的概率。我们用输入向量乘上之前计算得到的p1Vec,就可以得到输入向量在第1种分类下的条件概率。然后按照贝叶斯准则,乘以第1种分类出现概率取自然对数(自然对数函数为增函数,概率越大值越大),就可得到输入数据分类到该分类的几率。
同理获得p0,比较两概率大小,返回1或0。在这里你可能会奇怪为何没有除以p(w),在比较不同c值后验概率时,p(w)总为常数,所以可以忽略。
testingNB函数中,我们准备了几个数据测试算法是否正常工作。
可以看到函数正常运行。
3.文档词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
之前的setOfWords2Vec函数将每个词的出现与否作为一个特征,这可以被描述为词集模型(set-of-words model)。
如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)。在词袋中,每个单词可以出现多次,而在词集中,每个词只能出现一次。为适应词袋模型,我们对函数稍作修改,在遍历时使用累加符号+=即可。
4.使用分类器过滤垃圾邮件
def textParse(bigString):
import re
listOfTokens = re.split(r'\W+', bigString)
return [tok.lower() for tok in listOfTokens if len(tok) > 2] #
首先创建字符串预处理函数,这段代码中使用了正则表达式去除了字符串中的非字母字符并创建词条列表,'\W+'中的'\W'表示匹配非字母字符,'+'表示匹配前面的表达式('\W')大于等于1次。然后通过lower方法返回列表中大于2个字符的词条的小写。
import random
def spamTest():
docList = []; classList = []; fullText = []
for i in range(1, 26):
wordList = textParse(open('email/spam/%d.txt' % i, encoding='gbk', errors='ignore').read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
wordList = textParse(open('email/ham/%d.txt' % i, encoding='gbk', errors='ignore').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(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("classification error", docList[docIndex])
print('the error rate is: ', float(errorCount)/len(testSet))
在spamTest函数中,我们导入spam和ham文件夹下的各25封邮件文本文件。将这些文本转化为列表,并通过textParse函数作预处理。
这里我们随机取10封邮件作为测试邮件。要注意的是,我们需要从训练数据中剔除测试数据以保证测试的准确性。
之后的分类流程和第2节大同小异,这里我们使用词袋模型创建测试文档向量。
因为测试数据是随机选取的,所以运行结果不唯一。如果想更好的测试准确率,可以运行多次并取平均值。
5.RSS源分类器及高频词去除函数
def calcMostFreq(vocabList, fullText):
import operator
freqDict = {}
for token in vocabList:
freqDict[token] = fullText.count(token)
sortedFreq = sorted(freqDict.items(), key=operator.itemgetter(1), \
reverse=True)
return sortedFreq[:30]
高频词去除函数的实现非常简单,我们创建一个哈希表freqDict对输入的词汇表vocabList中各词条在fullText中的出现次数进行记录。然后按照哈希表的值(即次数)进行排序并建立以键(即词条)为元素的列表,返回出现频率最高的前30个词条的列表。
def localWords(feed1, feed0):
import feedparser
import re
docList = []; classList = []; fullText = []
minLen = min(len(feed1['entries']), len(feed0['entries'])) # 为保证数据一一对应,取小的长度
for i in range(minLen):
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])
stopWordsList = open('stopwords.txt').read()
stopWordsList = re.split(r'\W*', stopWordsList)
for word in stopWordsList:
if word in vocabList:
vocabList.remove(word)
trainingSet = list(range(2*minLen)); testSet = []
for i in range(20):
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
我们要使用分类器对输入文章属于哪个RSS源进行分类。这里我们需要使用feedparser模组抓取RSS源。localWords函数的参数是2个用feedparser.parse方法抓取的RSS源,其数据结构为哈希表。
['entries']键的值为列表,内容为订阅的文章,我们获取两个RSS源中文章篇数的最小值。(保证双方有足够篇数一一对应)
然后和第4节的代码一样,创建词汇表。这里我们用刚刚实现的calcMostFreq函数计算出出现频率top30的词条列表。然后,我们要将这些词从词汇表中去除。
那么为什么要去除这些词呢?因为通常来说,出现频率很高的词多为一些无意义的助词,感叹词等,例如hi, hello。这些词不包含任何与分类相关的信息,剔除它们可以提高分类器的识别率。
同理,我们利用停用词列表stopwords.txt来过滤词汇表。停用词至一些无实际意义的词汇。而文章中的名词通常与文章的内容关系最为密切。
接下来我们抽取20篇文章作为测试数据,和第4节一样,调用分类器测试。
ny = feedparser.parse('https://newyork.craigslist.org/stp/index.rss')
sf = feedparser.parse('https://sfbay.craigslist.org/stp/index.rss')
print("---RSS---")
vocabList, p0V, p1V = localWords(ny, sf)
RSS源必须差异化,分类才有意义。这里我们使用Craigslist个人广告网站的两个不同地区的RSS源,期望通过分类器鉴别出输入文章的地区性差异。
看来仅仅通过朴素贝叶斯的方法无法准确分类出文章属于哪个RSS源,亦或许两个RSS源之间并没有显著的差别。
5.显示地域相关的用词
def getTopWords(ny, sf):
import operator
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**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**")
count = 0
for item in sortedSF:
if count == 10:
break
print(item[0])
count += 1
sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)
print("NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**")
count = 0
for item in sortedNY:
if count == 10:
break
print(item[0])
count += 1
利用localWords函数返回的词汇表,和两个分类的词条出现概率向量。初始化两个地区的频出词列表。我们设定一个概率阈值,这里设置为-6.0(因为我们计算概率时取的是自然对数,所以值为负数)然后遍历词汇表,将词条和概率放入一个元组添加到相应地区的列表中。
按照元组第2项即词条概率对列表进行排序,最后分别输出两个地区的top10词条。
肉眼并不能看出明显的倾向性。RSS源是不断更新的,因此每次运行结果都会不同。
参考
《机器学习实战》