《机器学习实战》基于朴素贝叶斯的分类方法

朴素贝叶斯

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


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的后验概率。
贝叶斯准则(2个变量)

  如果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函数中,我们准备了几个数据测试算法是否正常工作。

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节大同小异,这里我们使用词袋模型创建测试文档向量。

spamTest()函数运行结果

  因为测试数据是随机选取的,所以运行结果不唯一。如果想更好的测试准确率,可以运行多次并取平均值。

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源,期望通过分类器鉴别出输入文章的地区性差异。

localWord()函数运行结果

  看来仅仅通过朴素贝叶斯的方法无法准确分类出文章属于哪个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词条。

getTopWords()函数运行结果

  肉眼并不能看出明显的倾向性。RSS源是不断更新的,因此每次运行结果都会不同。





参考

《机器学习实战》

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

推荐阅读更多精彩内容