【二十】机器学习之路——朴素贝叶斯实战(文本分类)

[写在前面:最近工作上事情比较多,加上年终述职和元旦假期去首都玩了一次,导致这篇博客前前后后写了快有半个月,跨越了2017和2018年,意义非凡。在这里祝大家新年快乐,也希望自己在新的一年能够坚持学习,提升自己!]

  上一篇博客机器学习之路——朴素贝叶斯分类写到了朴素贝叶斯的理论知识,今天来讲一下朴素贝叶斯实战——文本分类,本文内容参考《机器学习实战》。

  举个简单的例子,我们在网上发帖的时候,如果文章里有侮辱性的词汇的话,系统会识别该文章为不合格,并禁止发表,那么网络平台就需要一个过滤器来帮助其完成识别的工作。简单化该问题,假设我们需要将每篇文章分为两类:侮辱性1和非侮辱性0。如何利用朴素贝叶斯来进行分类呢?

  还记得朴素贝叶斯的公式么:

P(ci|w)=P(w|ci)P(ci)P(w)

这里 cj 可理解为文章的分类, c0 代表不合格, c1 代表合格。 w 可以理解为文章的各个特征,即词汇,每个词汇都算是一篇文章的独立特征,假设有n个独立特征,所有词都相互独立,上式可以写成:

P(ci|w)=P(w0,w1,w2,...,wn|ci)P(ci)P(w0,w1,w2,...,wn)=P(w0|ci)P(w1|ci)P(w2|ci)P(wn|ci)P(ci)P(w0)P(w1)P(wn)

上述公式即为我们实现分类器的理论指导。下面看一下分类器的伪代码:

计算每个类别中的文档数目  #求P(c_0),P(c_1)
对每篇训练文档:
    对每个类别:
        如果词条出现在文档中:增加该词条计数值
        增加所有词条计数值  #求P(w) 
对每个类别:
    对每个词条:
        将该词条的数目除以总词条数目得到条件概率 #求P(w|c_i)

  下面一起来看看代码:

1. 文本预处理:利用文本构建词汇向量

  下面代码里给了一段简单的样本做例子,用来简要说明如何处理文本,利用文本构建词汇向量,后面分类器训练函数里也会用到下面的部分代码。

import numpy as np
def loadDataSet(): #导入数据
    #假设数据为最简单的6篇文章,每篇文章大概7~8个词汇左右,如下
    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] #对应上述6篇文章的分类结果,1为侮辱性,0为非侮辱性
    return postingList,classVec 

def createVocabList(dataSet):# 将所有文章中的词汇取并集汇总
    vocabSet = set([])  # 定义一个set(set存储的内容无重复)
    for document in dataSet:# 遍历导入的dataset数据,将所有词汇取并集存储至vocabSet中
        vocabSet = vocabSet | set(document) # | 符号为取并集,即获得所有文章的词汇表
    return list(vocabSet)

#该函数输入参数为词汇表及某篇文章,输出为文档向量,向量每个元素为1或0,分别表示词汇表中的单词在输入文档中是否出现;
def setOfWords2Vec(vocabList, inputSet):
    returnVec = [0]*len(vocabList) #构建一个0向量;
    for word in inputSet: # 遍历词汇表,如果文档中出现了词汇表中单词,则将输出的文档向量中对应值设为1,旨在计算各词汇出现的次数;
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1#因为上一段代码里,给的文章例子里的单词都是不重复的,如果有重复的单词的话,这段代码改写为:returnVec[vocabList.index(word)] += 1更为合适;
        else: print "the word: %s is not in my Vocabulary!" % word
    return returnVec#返回向量化后的某篇文章

2. 训练函数

def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix) #计算有多少篇文章
    numWords = len(trainMatrix[0]) #计算第一篇文档的词汇数
    pAbusive = sum(trainCategory)/float(numTrainDocs) #计算p(c_1),p(c_0)=1-p(c_1)
    p0Num = np.zeros(numWords) #构建一个空矩阵,用来计算非侮辱性文章中词汇数
    p1Num = np.zeros(numWords) #构建一个空矩阵,用来计算侮辱性文章中词汇数
    p0Denom = 0.0; p1Denom = 0.0 
    for i in range(numTrainDocs): #遍历每一篇文章,来求P(w|c)
        if trainCategory[i] == 1: #判断该文章是否为侮辱性文章
            p1Num += trainMatrix[i] #累计每个词汇出现的次数
            p1Denom += sum(trainMatrix[i]) #计算所有该类别下不同词汇出现的总次数
        else:#如果该文章为非侮辱性文章
            p0Num += trainMatrix[i] 
            p0Denom += sum(trainMatrix[i])
    p1Vect = p1Num/p1Denom #计算每个词汇出现的概率P(w_i|c_1)
    p0Vect = p0Num/p0Denom #计算每个词汇出现的概率P(w_i|c_0)
    return p0Vect,p1Vect,pAbusive

  有了以上几段代码,我们将训练集的每篇文章向量化,然后计算了训练集中各分类中各词汇出现的概率 p(wi|c0),p(wi|c1) ,和侮辱性文章的概率 p(c1) 、非侮辱性文章的概率 p(c0)

  首先先来小试下牛刀,测试下上面这段代码的功能,假设就6篇文章,内容非常非常简单,只有几个单词,现在咱们利用上面几段代码,将这几篇文章向量化,再计算一下各个词汇在不同分类的文章里出现的概率:

if __name__ == '__main__':
    listOPosts,listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    trainMat = []
    for postinDoc in listOPosts:     
        trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
    # 将所有文章转化为向量,并将所有文章中的词汇append到traninMat中
    p0V,p1V,pAb = trainNB0(trainMat,listClasses)# 计算训练集的p(c_1),p(w_i|c_0),p(w_i|c_1)
    print p0V,p1V,pAb

上述代码运行后输出结果如下所示:

【二十】机器学习之路——朴素贝叶斯实战(文本分类)_第1张图片

前两个list为p0V,p1V,0.5代表训练集中侮辱性文章出现的概率。

3. 训练函数修正——避免 p(wi|ci)=0 的情况

  还记得上篇博客里提到的条件概率P(a|A)=0的处理么?

  当某个类别下某个特征项划分没有出现过一次时,就会导致 p(wi|ci)=0 ,0乘上其他的数结果为0。这就导致我们求得的条件概率结果不准确。为了解决这个问题,我们引入”拉普拉斯修正”,具体思路如下图所示:

【二十】机器学习之路——朴素贝叶斯实战(文本分类)_第2张图片

其中|D|所有分类的样本总数,|DC|表示分类为C的样本总数。

  在本例中,为了避免上述影响,将所有词汇出现的次数初始化为1,将分母初始化为2。同时为了避免求得概率数值较小导致程序下溢出。在计算概率的时候,对结果进行就对数,虽然结果会不相同,但是不影响最终结果。具体代码改变较简单,大家可以自己试着改改。

4. 分类函数登场

  前面几段代码分别实现的文本的向量化,并计算出了 P(wi|ci) P(ci) ,这个时候给定文章,将其向量化并计算 P(c0|wi)P(c1|wi) ,如果 P(c0|wi)>P(c1|wi) ,则判断该文章为非侮辱性;如果 P(c0|wi)<P(c1|wi) ,则判断该文章为侮辱性。前面已经给定了计算公式:

P(ci|w)=P(w|ci)P(ci)P(w)

由于同意篇文章p(w)的结果相同,因此这里只要计算并比较分子的乘积 P(w|ci)P(ci) 大小即可。代码如下所示:

def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):#vec2Classify为输入的一个向量化的新文章
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)# 由于是取对数,所以乘积变为直接求和即可,注意这里list和list是无法相乘,vec2Classify需要为array格式
    p0 = sum(vec2Classify * p0Vec) + log(1-pClass1)
    if(p1>p0):
        return 1
    if(p0>p1):
        return 0

5. 分类器检验

  好了,朴素贝叶斯分类函数就已经完成了,通过上述代码的描述,耐心的读者可能看出来,训练集中判断文章是否为侮辱性,主要关键点为是否含有”stupid”这个单词。光说不练假把式,现在来测试一下这个简单的朴素贝叶斯分类器:

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)

最终输出结果为:

['my', 'love', 'dalmation'] classified as:  0
['stupid', 'garbage'] classified as:  1

  其实大家可以试着改一下训练集里的内容,把’stupid’改成’fuck’或者其他词汇,然后再添加一些训练集,看看该分类器最终输出的结果是否正确,我试了一下还是挺准的。

  《机器学习实战》这一章里最后讲了利用上述代码实现垃圾邮件分类的内容,有兴趣的小伙伴可以看下书中的这一章。最后感谢大家的阅读,如有问题请多多指正!

你可能感兴趣的:(机器学习,Python)