数据挖掘实例:朴素贝叶斯分类器进行垃圾邮件过滤

概述

朴素贝叶斯是基于贝叶斯,定理与特征条件独立假设的分类方法。最为广泛的两种分类模型是决策树模型和朴素贝叶斯模型。 和决策树模型相比,朴素贝叶斯分类器(Naive Bayesian Classifier, NBC)发源于古典数学理论,有着坚实的数学基础,以及稳定的分类效率。同时,NBC模型所需估计的参数很少,对缺失数据不敏感,算法也比较简单。理论上,NBC模型与其他分类方法相比,具有最小的误差率。但是实际上并非总是如此,这是因为NBC模型假设属性之间相互独立,这个假设在实际应用中往往是不成立的,这个NBC模型的正确分类带来了一定影响。

优点:在数据较少的情况下任然有效,可以处理多类别问题 缺点:对于输入数据的准备方式较为敏感 使用数据类型:标称型数据

贝叶斯决策理论的核心思想是,选择具有最高概率的决策。

算法流程

  1. 收集数据:可以使用任何方法。
  2. 准备数据:需要数值型或者布尔型数据
  3. 分析数据:有大量特征时,绘制特征作用不大,此时使用直方图效果更好
  4. 训练算法:计算不同的独立特征的条件概率
  5. 测试算法:计算错误率
  6. 使用算法:一个常见的朴素贝叶斯应用是文档分类。可以在任意的分类场景中使用朴素贝叶斯分类器,不一定非要是文本

文本分类

准备数据:从文本中构建词向量

将文本看成单词向量或词条向量,也就是说把句子转换为向量。

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']]    #  1:代表侮辱性文字, 0:代表正常言论
    classVec = [0, 1, 0, 1, 0, 1] 
    return postingList, classVecdef createVocabList(dataSet):
    '''
    创建文本中单词列表
    '''
    vocabSet = set([])    for document in dataSet:
        vocabSet = vocabSet | set(document)    return list(vocabSet)def setOfWords2Vec(vocabList, inputSet):
    '''
    单词是否在文档中出现,出现设为1,不出现为0
    param vocabList: 单词列表
    param 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

函数验证

listOPosts, listClasses = loadDataSet()
myVocabList = createVocabList(listOPosts)
myVocabList
[out] ['cute', 'love', 'help', 'garbage', 'quit', 'I', 'problems', 'is', 'park', 'stop', 'flea', 'dalmation', 'licks', 'food', 'not', 'him', 'buying', 'posting', 'has', 'worthless', 'ate', 'to', 'maybe', 'please', 'dog', 'how', 'stupid', 'so', 'take', 'mr', 'steak', 'my']
 setOfWords2Vec(myVocabList, listOPosts[0])
 [out] [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1]

训练算法:从词向量计算概率

贝叶斯准则:

p(ci|w)=p(w|ci)p(ci)p(w)p(ci|w)=p(w|ci)p(ci)p(w)

ww表示这是一个向量,即它由多个数值组成。ww中元素众多,使用Numpy数组快速计算这些值。

import numpy as npdef trainNB0(trainMatrix, trainCategroy):
    '''
    朴素贝叶斯分类器训练函数
    param trainMatrix: 文档矩阵
    param trainCategory: 文档类别标签向量
    return p0Num: 正常言论概率向量
    return p1Num: 侮辱性言论概率向量
    return pAbusive: 侮辱性文档概率向量
    '''
    # 文档数量
    numTrainDocs = len(trainMatrix)    # 单词数量
    numWords = len(trainMatrix[0])    # 侮辱性语句概率(侮辱性语句数量除以语句总数)
    pAbusive = sum(trainCategroy)/float(numTrainDocs)    # 各单词出现向量初始化
    p0Num = np.zeros(numWords)
    p1Num = np.zeros(numWords)
    p0Denom = 0.0
    p1Denom = 0.0

    # 遍历文档矩阵
    for i in range(numTrainDocs):        # 判定文档所对应的分类,并对该分类向量进行累加
        if trainCategroy[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

函数测试

对样本数据集进行朴素贝叶斯分类,得到出现侮辱性语言的概率为0.5。 从样本数据中可以看到,总共有6句话,有三句是侮辱性语句,因此概率0.5是正确的。

# 加载样本数据集listOPosts, listClasses = loadDataSet()# 单词列表myVocabList = createVocabList(listOPosts)
trainMat = []# 遍历样本数据集for postinDoc in listOPosts:    # 将文本转换为单词向量
    trainMat.append(setOfWords2Vec(myVocabList, postinDoc))

p0V, p1V, pAb = trainNB0(trainMat, listClasses)
pAb
[out] 0.5

查看侮辱性言论中各单词出现的概率。

p1V
[out] array([ 0.        ,  0.        ,  0.        ,  0.05263158,  0.05263158,        0.        ,  0.        ,  0.        ,  0.05263158,  0.05263158,        0.        ,  0.        ,  0.        ,  0.05263158,  0.05263158,        0.05263158,  0.05263158,  0.05263158,  0.        ,  0.10526316,        0.        ,  0.05263158,  0.05263158,  0.        ,  0.10526316,        0.        ,  0.15789474,  0.        ,  0.05263158,  0.        ,        0.        ,  0.        ])

找出侮辱性言论中单词出现概率最大的值和其对应的索引

p1V.max(), p1V.argmax()
[out] (0.15789473684210525, 26)

单词列表中找到对应索引的单词,发现该单词为’stupid’。这意味着’stupid’是最能表征侮辱性言论类别的单词

myVocabList[26]
[out] 'stupid'

测试算法:根据现实情况修改分类器

利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算p(w0|1)p(w1|1)p(w2|1)p(w0|1)p(w1|1)p(w2|1)。如果其中一个概率为0,那么最后的乘积也为0。 为了降低这种影响,可以将所有词出现数初始化为1,并将分母初始化为2。

另一个问题是下溢出,这是由于太多很小的数相乘造成的。由于大部分因子都非常小,所以程序会下溢出或者得不到正确答案。 一种解决办法是对乘积取自然对数。在代数中有ln(a∗b)=ln(a)+ln(b)ln(a∗b)=ln(a)+ln(b),于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。

def trainNB0(trainMatrix, trainCategroy):
    '''
    朴素贝叶斯分类器训练函数
    param trainMatrix: 文档矩阵
    param trainCategory: 文档类别标签向量
    return p0Num: 正常言论概率向量
    return p1Num: 侮辱性言论概率向量
    return pAbusive: 侮辱性文档概率向量
    '''
    numTrainDocs = len(trainMatrix)
    numWords = len(trainMatrix[0])
    pAbusive = sum(trainCategroy)/float(numTrainDocs)    # 各单词出现向量初始化为1
    p0Num = np.ones(numWords)
    p1Num = np.ones(numWords)    # 分母初始化为2
    p0Denom = 2.0
    p1Denom = 2.0

    for i in range(numTrainDocs):        if trainCategroy[i] == 1:            
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])    # 修改为取对数
    p1Vect = np.log(p1Num / p1Denom)
    p0Vect = np.log(p0Num / p0Denom)    return p0Vect, p1Vect, pAbusive

编写朴素贝叶斯分类函数

def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    '''
    朴素贝叶斯分类函数
    param vec2Classify: 要分类的向量
    param p0Vec: 正常言论单词概率向量
    param p1Vec: 侮辱性言论单词概率向量
    param pClass1: 侮辱性言论概率
    '''
    # 单词出现概率和 + 分类概率
    p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
    p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)    # 返回概率大的类别
    if p1 > p0:        return 1
    else:        return 0def testingNB():
    # 训练朴素贝叶斯分类器
    listOPosts, listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    trainMat = []    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))

    p0V, p1V, pAb = trainNB0(np.array(trainMat), np.array(listClasses))    # 测试朴素贝叶斯分类器
    testEntry = ['love', 'my', 'dalmation']
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))    print testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb)

    testEntry = ['stupid', 'garbage']
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))    print testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb)

执行测试

testingNB()
[out] 
['love', 'my', 'dalmation'] classified as:  0['stupid', 'garbage'] classified as:  1

准备数据:文档词袋模型

上面将每个单词在文本中出现与否作为一个特征,这可以被描述为词集模型(set-of-words model)。 如果一个词在文档中出现不止一次,这可能意味着该词是否出现在文档中不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)。 词袋中每个单词可以出现多次,而词集中每个单词只能出现一次。

def bagOfwords2VecMN(vocabList, inputSet):
    returnVec = [0] * len(vocabList)    for word in inputSet:        if word in vocabList:
            returnVec[vocabList.index(word)] += 1

    return returnVec

示例:电子邮件垃圾过滤

收集数据:提供文本文件 准备数据:将文本文件解析成词条向量 分析数据;检查词条确保解析的正确性 训练算法:使用之前建立的trainNB0()函数 测试算法:使用classifyNB(),并且构建一个新的测试函数来计算文档集的错误率 使用算法:构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上

准备数据:切分文本

使用正则表达式切分,其中分隔符是除单词、数字外的任意字符

import re
mySent = 'This book is the best book on Python or M.L. I have ever laid eyes upon.'
regEx = re.compile('\\W*')
listOfTokens = regEx.split(mySent)
# 去掉长度小于0的单词,并转换为小写
[tok.lower() for tok in listOfTokens if len(tok) > 0]
[out]
['this', 'book', 'is', 'the', 'best', 'book', 'on', 'python', 'or', 'm', 'l', 'i', 'have', 'ever', 'laid', 'eyes', 'upon']

切分邮件

emailText = open('email/ham/6.txt').read()
listOfTokens = regEx.split(emailText)

测试算法:使用朴素贝叶斯进行交叉验证

import randomdef textParse(bigString):
    '''
    字符串解析
    '''
    import re    # 根据非数字字母的任意字符进行拆分
    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):        # 读取spam文件夹下的文件,并转换为特征和标签向量
        wordList = textParse(open('email/spam/%d.txt' % i).read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1)        # 读取ham文件夹下的文件,并转换为特征和标签向量
        wordList = textParse(open('email/ham/%d.txt' % i).read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)    # 转换为词列表
    vocabList = createVocabList(docList)    # 初始化训练集和测试集
    trainingSet = 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(np.array(trainMat), np.array(trainClasses))
    errorCount = 0

    # 朴素贝叶斯分类模型测试
    for docIndex in testSet:
        wordVector = setOfWords2Vec(vocabList, docList[docIndex])        if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
            errorCount += 1
            print 'classification error', docList[docIndex]    print 'the error rate is: ',float(errorCount)/len(testSet)

由于SpamTest()构造的测试集和训练集是随机的,所以每次运行的分类结果可能不一样。如果发生错误,函数会输出错分文档的词表,这样就可以了解到底哪篇文档发生了错误。 这里出现的错误是将垃圾邮件误判为了正常邮件。

spamTest()
[out]
classification error ['benoit', 'mandelbrot', '1924', '2010', 'benoit', 'mandelbrot', '1924', '2010', 'wilmott', 'team', 'benoit', 'mandelbrot', 'the', 'mathematician', 'the', 'father', 'fractal', 'mathematics', 'and', 'advocate', 'more', 'sophisticated', 'modelling', 'quantitative', 'finance', 'died', '14th', 'october', '2010', 'aged', 'wilmott', 'magazine', 'has', 'often', 'featured', 'mandelbrot', 'his', 'ideas', 'and', 'the', 'work', 'others', 'inspired', 'his', 'fundamental', 'insights', 'you', 'must', 'logged', 'view', 'these', 'articles', 'from', 'past', 'issues', 'wilmott', 'magazine']
the error rate is:  0.1spamTest()
[out]
the error rate is:  0.0

示例:使用朴素贝叶斯分类器从个人广告中获取区域倾向

分别从美国的两个城市中选取一些人,通过这些人发布的征婚广告信息,来比较这两个城市的人们在广告用词上是否不同。如果结论确实不同,那么各自的常用词有哪些?从人们的用词当中,我们能否对不同城市的人所关心的内容有所了解?

收集数据:从RSS源收集内容 准备数据:将文本解析成词条向量 分析数据:检查词条以确保词条的正确性 训练算法:使用之前建立的traingNB0()函数 测试算法:观察错误率,确保分类器可用。可以修改切片程序,以降低错误率,提高分类结果 使用算法:构建一个完整的程序,封装所有内容。给定两个RSS源,该程序会显示常用的公共词

收集数据:导入RSS源

import operatorimport feedparserdef calcMostFreq(vocabList, fullText):
    '''
    返回前30个频率最高的单词
    '''
    freqDict = {}    for token in vocabList:
        freqDict[token] = fullText.count(token)

    sortedFreq = sorted(freqDict.iteritems(), key=operator.itemgetter(1), reverse=True)    return sortedFreq[:30]def localWords(feed1, feed0):
    '''
    RSS分类器
    '''
    docList = []
    classList = []
    fullText = []    # 获取两个RSS源最小条目数
    minLen = min(len(feed1['entries']), len(feed0['entries']))    # 解析RSS内容,并转换为特征和标签向量
    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)    # 词列表中移除频度出现最高的前30个单词
    top30Words = calcMostFreq(vocabList, fullText)    for pairW in top30Words:        if pairW[0] in vocabList:
            vocabList.remove(pairW[0])

    trainingSet = 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(np.array(trainMat), np.array(trainClasses))
    errorCount = 0
    # 测试模型
    for docIndex in testSet:
        wordVector = bagOfwords2VecMN(vocabList, docList[docIndex])        if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
            errorCount += 1

    print 'the error rate is: ', float(errorCount)/len(testSet)    return vocabList, p0V, p1V

验证RSS分类器

ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
sf = feedparser.parse('http://sfbay.craigslist.org/stp/index.rss')
vocabList, pSF, pNY = localWords(ny, sf)
[out]
the error rate is:  0.45

分析数据:显示地域相关的用词

def getTopWords(ny, sf):
    '''
    显示最具表征性的词汇
    '''
    import operator    # 训练并测试朴素贝叶斯分类器
    vocabList, p0V, p1V = localWords(ny, sf)
    topNY = []
    topSF = []    # 将概率大于-6.0的单词加入列表
    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**'*14
    for item in sortedSF:        print item[0]

    sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)    print 'NY**'*14
    for item in sortedNY:        print item[0]

验证函数 从结果可以看出两者之间的常用词差距还是比较明显

getTopWords(ny, sf)[out]the error rate is:  0.4
SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**
love
seeking
naked
down
all
thursday
says
great
from
few
girl
got
dont
friend
relationship
sex
whatever
doesn
coffee
NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**
what
free
massage
this
travel
magazine
over
platonic
dont
friend
don
professional
life
real
other
chat
time
buddy
good
gwazoozy
putting
cool
full
smoke
music
train
could
maybe
its
honest
little
girls
hurkazoolitz
hangout
sensual
most
sex
fleekibobo
myself
will
craigslist
person
guys
know
amp

·END·

原文发布于微信公众号 - 机器学习与统计学(tjxj666)

原文发表时间:2018-07-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券