基于朴素贝叶斯,实现垃圾邮件分类及相关的文本处理操作。
例子来自《Machine Learning in Action》 Peter Harrington
基本思想
这里对朴素贝叶斯概念不做过多解释。
构建词向量
实验数据
数据描述的是一个留言板的留言信息,每条留言被标记为侮辱性or无侮辱性。
"""创建一个数据集"""
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
构建词汇表
利用set集合无重复元素的特点,创建一个包含训练集合所有字段的集合
"""获得词汇表"""
def createVocabList(dataSet):
vocabSet = set([]) #创建一个空的set集合
for document in dataSet:
vocabSet = vocabSet | set(document) #合并两个Set集合(求并集)
return list(vocabSet)
测试
"""测试获取词汇表"""
def testCreateVocabList():
listOposts,listClass = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOposts)
print(myVocabList)
结果
['stop', 'stupid', 'licks', 'park', 'my', 'has', 'not', 'love', 'quit', 'I', 'so', 'posting', 'flea', 'garbage', 'food', 'how', 'take', 'him', 'dalmation', 'worthless', 'is', 'maybe', 'help', 'ate', 'problems', 'mr', 'to', 'steak', 'cute', 'please', 'buying', 'dog']
创建文档向量
"""获取文档向量"""
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
测试
"""测试获取文档向量"""
def testSetOfWords2Vec():
listOposts,listClass = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOposts)
myVec = bayes.setOfWords2Vec(myVocabList,listOposts[0])
print(myVec)
结果
输入文本对比词表,用0,1进行标注,进而转换为向量表示。
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1]
计算概率
基于之前的向量表示和每个向量的标签,计算相应的概率值
由于计算中存在乘积运算,防止一个为零的结果影响整个结果,所以初始化为1。同时概率是小于一的量,防止大量相乘后的数值过小,而溢出丢失精度,所以取每个概率的对数进行计算。
"""从词向量计算概率"""
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix) # 文本个数
numWords = len(trainMatrix[0]) # 词表长度
pAbusive = sum(trainCategory)/float(numTrainDocs) # 所有训练文本中,是侮辱性质的所占比例。
p0Num = ones(numWords); p1Num = ones(numWords) #生成与词表长度一致的单位向量 (防止出现0)
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 = log(p1Num/p1Denom) # 防止最后的乘积过小,溢出
p0Vect = log(p0Num/p0Denom)
return p0Vect,p1Vect,pAbusive
测试
""" 测试训练函数"""
def testTrainNB0():
listOposts,listClass = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOposts)
trainMat = []
for postinDoc in listOposts:
trainMat.append(bayes.setOfWords2Vec(myVocabList,postinDoc))
p0V,p1V,pAb = bayes.trainNB0(trainMat,listClass)
print(p0V)
print("--------------------------------------------------")
print(p1V)
print("--------------------------------------------------")
print(pAb)
结果
分别计算出贝叶斯公式中需要的相应概率值
[-2.56494936 -3.25809654 -2.56494936 -3.25809654 -1.87180218 -2.56494936
-3.25809654 -2.56494936 -3.25809654 -2.56494936 -2.56494936 -3.25809654
-2.56494936 -3.25809654 -3.25809654 -2.56494936 -3.25809654 -2.15948425
-2.56494936 -3.25809654 -2.56494936 -3.25809654 -2.56494936 -2.56494936
-2.56494936 -2.56494936 -2.56494936 -2.56494936 -2.56494936 -2.56494936
-3.25809654 -2.56494936]
--------------------------------------------------
[-2.35137526 -1.65822808 -3.04452244 -2.35137526 -3.04452244 -3.04452244
-2.35137526 -3.04452244 -2.35137526 -3.04452244 -3.04452244 -2.35137526
-3.04452244 -2.35137526 -2.35137526 -3.04452244 -2.35137526 -2.35137526
-3.04452244 -1.94591015 -3.04452244 -2.35137526 -3.04452244 -3.04452244
-3.04452244 -3.04452244 -2.35137526 -3.04452244 -3.04452244 -3.04452244
-2.35137526 -1.94591015]
--------------------------------------------------
0.5
依据概率大小分类
有了之前的概率值,就可以利用概率大小来判断一个新数据点的类型。
"""朴素贝叶斯分类函数"""
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + log(pClass1) #element-wise mult
p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
测试
""" 测试分类函数 """
def testingNB():
listOposts,listClass = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOposts)
trainMat = []
for postinDoc in listOposts:
trainMat.append(bayes.setOfWords2Vec(myVocabList,postinDoc))
p0V,p1V,pAb = bayes.trainNB0(trainMat,listClass)
testEntry = ['love', 'my', 'dalmation']
thisDoc = array(bayes.setOfWords2Vec(myVocabList, testEntry))
print (testEntry,'classified as: ',bayes.classifyNB(thisDoc,p0V,p1V,pAb))
testEntry = ['stupid', 'garbage']
thisDoc = array(bayes.setOfWords2Vec(myVocabList, testEntry))
print (testEntry,'classified as: ',bayes.classifyNB(thisDoc,p0V,p1V,pAb))
结果
可以看到,算法可以根据新输入的句子(词组段)来判断是否是侮辱性质的内容。
['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1
基于朴素贝叶斯的垃圾邮件分类
垃圾邮件分类貌似是朴素贝叶斯最好的例子。(至少Andrew Y. Ng在他的课程中不止一次提起这个例子)。
词袋模型
之前的向量构建是基于词集模型的,也就是说只通过0,1的形式记录某个单词时候出现,但是出现的个数无法记录。而词袋模型可以记录每个单词出现的次数。
""" 词袋模型"""
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
文本划分
训练数据是以txt格式存放的内容,为了方便使用之前的功能模块,需要把原始数据分割为词列表
""" 切分文本"""
def textParse(bigString):
import re
listOfTokens = re.split(r'\W*', bigString)
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
分类
正常邮件和垃圾邮件各有25个(ham/spam)。随机选取10个作为测试集,40个最为训练集,进行试验。
"""垃圾邮件处理"""
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 = list(range(50)); testSet=[]
for i in range(10): # 随机划分训练集和测试集合 (40/10)
randIndex = int(random.uniform(0,len(trainingSet))) # 随机生成0到50的一个整数
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))
结果
每次的试验结果不尽相同,但是错误率较低。
classification error ['yeah', 'ready', 'may', 'not', 'here', 'because', 'jar', 'jar', 'has', 'plane', 'tickets', 'germany', 'for']
the error rate is: 0.1
其他
需要把随书提供的邮件数据ham\23.txt
的第二行SciFinance
后的?
改为空格
。不然读取文件会报错。
以上完整代码见GitHub。