机器学习——朴素贝叶斯

朴素贝叶斯分类器(Naive Bayesian Classifier)

概述

朴素贝叶斯是基于贝叶斯,定理与特征条件独立假设的分类方法。最为广泛的两种分类模型是决策树模型和朴素贝叶斯模型。
和决策树模型相比,朴素贝叶斯分类器(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, classVec

def 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]

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

贝叶斯准则:


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

import numpy as np

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)
    # 各单词出现向量初始化
    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(w_0|1)p(w_1|1)p(w_2|1)$。如果其中一个概率为0,那么最后的乘积也为0。
为了降低这种影响,可以将所有词出现数初始化为1,并将分母初始化为2。

另一个问题是下溢出,这是由于太多很小的数相乘造成的。由于大部分因子都非常小,所以程序会下溢出或者得不到正确答案。
一种解决办法是对乘积取自然对数。在代数中有$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 0
    
def 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 random

def 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.1
spamTest()
[out]
the error rate is:  0.0

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

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

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

收集数据:导入RSS源

import operator
import feedparser

def 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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355

推荐阅读更多精彩内容