深度 | 朴素贝叶斯模型算法研究与实例分析

(白宁超 2018年9月3日15: 56:20)

导读:朴素贝叶斯模型是机器学习常用的模型算法之一,其在文本分类方面简单易行,且取得不错的分类效果。所以很受欢迎,对于朴素贝叶斯的学习,本文首先介绍理论知识即朴素贝叶斯相关概念和公式推导,为了加深理解,采用一个维基百科上面性别分类例子进行形式化描述。然后通过编程实现朴素贝叶斯分类算法,并在屏蔽社区言论、垃圾邮件、个人广告中获取区域倾向等几个方面进行应用,包括创建数据集、数据预处理、词集模型和词袋模型、朴素贝叶斯模型训练和优化等。然后结合复旦大学新闻语料进行朴素贝叶斯的应用。最后,大家熟悉其原理和实现之后,采用机器学习sklearn包进行实现和优化。由于篇幅较长,采用理论理解、案例实现、sklearn优化三个部分进行学习。(本文原创,转载必须注明出处:朴素贝叶斯模型算法研究与实例分析)

复旦新闻语料:朴素贝叶斯中文文本分类

项目概述

本节介绍朴素贝叶斯分类算法模型在中文领域中的应用。我们对新闻语料进行多文本分类操作,本文选择艺术、文学、教育、哲学、历史五个类别的训练文本,然后采用新的测试语料进行分类预测。

收集数据

数据集是从复旦新闻语料库中抽取出来的,考虑学习使用,样本选择并不大。主要抽选艺术、文学、教育、哲学、历史五个类别各10篇文章。全部数据文档50篇。具体情况不同对收集数据要求不同,你也可以选择网络爬取,数据库导出等。这文档读取时候可能会遇到gbk,utf-8等格式共存的情况,这里建议采用BatUTF8Conv.exe点击下载)工具,进行utf-8格式批量转化。

准备数据

创建数据集代码如下:

'''创建数据集和类标签'''
def loadDataSet():
    docList = [];classList = [] # 文档列表、类别列表
    dirlist = ['C3-Art','C4-Literature','C5-Education','C6-Philosophy','C7-History']
    for j in range(5):
        for i in range(1, 11): # 总共10个文档
            # 切分,解析数据,并归类为 1 类别
            wordList = textParse(open('./fudan/%s/%d.txt' % (dirlist[j],i),encoding='UTF-8').read())
            docList.append(wordList)
            classList.append(j)
            # print(i,'\t','./fudan/%s/%d.txt' % (dirlist[j],i),'\t',j)
    return docList,classList

''' 利用jieba对文本进行分词,返回切词后的list '''
def textParse(str_doc):
    # 正则过滤掉特殊符号、标点、英文、数字等。
    import re
    r1 = '[a-zA-Z0-9’!"#$%&\'()*+,-./:;<=>?@,。?★、…【】《》?“”‘’![\$$^_`{|}~]+'
    str_doc=re.sub(r1, '', str_doc)

    # 创建停用词列表
    stwlist = set([line.strip() for line in open('./stopwords.txt', 'r', encoding='utf-8').readlines()])
    sent_list = str_doc.split('\n')
    # word_2dlist = [rm_tokens(jieba.cut(part), stwlist) for part in sent_list]  # 分词并去停用词
    word_2dlist = [rm_tokens([word+"/"+flag+" " for word, flag in pseg.cut(part) if flag in ['n','v','a','ns','nr','nt']], stwlist) for part in sent_list] # 带词性分词并去停用词
    word_list = list(itertools.chain(*word_2dlist)) # 合并列表
    return word_list



''' 去掉一些停用词、数字、特殊符号 '''
def rm_tokens(words, stwlist):
    words_list = list(words)
    for i in range(words_list.__len__())[::-1]:
        word = words_list[i]
        if word in stwlist:  # 去除停用词
            words_list.pop(i)
        elif len(word) == 1:  # 去除单个字符
            words_list.pop(i)
        elif word == " ":  # 去除空字符
            words_list.pop(i)
    return words_list

代码分析:loadDataSet()方法是遍历读取文件夹,并对每篇文档进行处理,最后返回全部文档集的列表和类标签。textParse()方法是对每篇文档字符串进行数据预处理,我们首选使用正则方法保留文本数据,然后进行带有词性的中文分词和词性选择,rm_tokens()是去掉一些停用词、数字、特殊符号。最终返回相对干净的数据集和标签集。

分析数据

前面两篇文章都介绍了,我们需要把文档进行向量化表示,首先构建全部文章的单词集合,实现代码如下:

'''获取所有文档单词的集合'''
def createVocabList(dataSet):
    vocabSet = set([])
    for document in dataSet:
        vocabSet = vocabSet | set(document)  # 操作符 | 用于求两个集合的并集
    # print(len(vocabSet),len(set(vocabSet)))
    return list(vocabSet)

基于文档模型的基础上,我们将特征向量转化为数据矩阵向量,这里使用的词袋模型,构造与实现方法如下:

'''文档词袋模型,创建矩阵数据'''
def bagOfWords2VecMN(vocabList, inputSet):
    returnVec = [0] * len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return returnVec

对矩阵数据可以采用可视化分析方法或者结合NLTK进行数据分析,检查数据分布情况和特征向量构成情况及其特征选择作为参考。

训练算法

我们在前面两篇文章介绍了朴素贝叶斯模型训练方法,我们在该方法下稍微改动就得到如下实现:

'''朴素贝叶斯模型训练数据优化'''
def trainNB0(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix) # 总文件数
    numWords = len(trainMatrix[0]) # 总单词数

    p1Num=p2Num=p3Num=p4Num=p5Num = ones(numWords) # 各类为1的矩阵
    p1Denom=p2Denom=p3Denom=p4Denom=p5Denom = 2.0 # 各类特征和
    num1=num2=num3=num4=num5 = 0 # 各类文档数目

    pNumlist=[p1Num,p2Num,p3Num,p4Num,p5Num]
    pDenomlist =[p1Denom,p2Denom,p3Denom,p4Denom,p5Denom]
    Numlist = [num1,num2,num3,num4,num5]

    for i in range(numTrainDocs): # 遍历每篇训练文档
        for j in range(5): # 遍历每个类别
            if trainCategory[i] == j: # 如果在类别下的文档
                pNumlist[j] += trainMatrix[i] # 增加词条计数值
                pDenomlist[j] += sum(trainMatrix[i]) # 增加该类下所有词条计数值
                Numlist[j] +=1 # 该类文档数目加1

    pVect,pi = [],[]
    for index in range(5):
        pVect.append(log(pNumlist[index] / pDenomlist[index]))
        pi.append(Numlist[index] / float(numTrainDocs))
    return pVect, pi

构建分类函数,其优化后的代码实现如下:

'''朴素贝叶斯分类函数,将乘法转换为加法'''
def classifyNB(vec2Classify, pVect,pi):
    # 计算公式  log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
    bnpi = [] # 文档分类到各类的概率值列表
    for x in range(5):
        bnpi.append(sum(vec2Classify * pVect[x]) + log(pi[x]))
    # print([bnp for bnp in bnpi])
    # 分类集合
    reslist = ['Art','Literature','Education','Philosophy','History']
    # 根据最大概率,选择索引值
    index = [bnpi.index(res) for res in bnpi if res==max(bnpi)]
    return reslist[index[0]] # 返回分类值

测试算法

我们加载构建的数据集方法,然后创建单词集合,集合词袋模型进行特征向量化,构建训练模型和分类方法,最终我们从复旦新闻语料中选择一篇未加入训练集的教育类文档,进行开放测试,具体代码如下:

'''朴素贝叶斯新闻分类应用'''
def testingNB():
    # 1. 加载数据集
    dataSet,Classlabels = loadDataSet()
    # 2. 创建单词集合
    myVocabList = createVocabList(dataSet)

    # 3. 计算单词是否出现并创建数据矩阵
    trainMat = []
    for postinDoc in dataSet:
        trainMat.append(bagOfWords2VecMN(myVocabList, postinDoc))
    with open('./word-bag.txt','w') as f:
        for i in trainMat:
            f.write(str(i)+'\r\n')
    # 4. 训练数据
    pVect,pi= trainNB0(array(trainMat), array(Classlabels))
    # 5. 测试数据
    testEntry = textParse(open('./fudan/test/C5-1.txt',encoding='UTF-8').read())
    thisDoc = array(bagOfWords2VecMN(myVocabList, testEntry))
    print(testEntry[:10], '分类结果是: ', classifyNB(thisDoc, pVect,pi))

实现结果如下:

Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\ADMINI~1\AppData\Local\Temp\jieba.cache
Loading model cost 0.892 seconds.
Prefix dict has been built succesfully.
['全国/n ', '举办/v ', '电影/n ', '新华社/nt ', '北京/ns ', '国家教委/nt ', '广播电影电视部/nt ', '文化部/n ', '联合/v ', '决定/v '] 分类结果是:  Literature
耗时:29.4882 s

结果分析:我们运行分类器得出结果易知,预测结果是文化类,且运行时间为29s。首先分析为什么预测错误,这里面主要是训练集样本比较少和特征选择的原因。运行时间是由于将特征矩阵存储本地后,后面直接读取文本,相当于加载缓存,大大缩短运行时间。但是这里还有值得优化的地方,比如每次运行都会加载训练模型,大大消耗时间,我们能不能训练模型加载一次,多次调用呢?当然是可以的,这个问题下文继续优化。我们重点关注下特征选择问题

特征选择问题讨论

  • 做文本分类的时候,遇到特征矩阵1.5w。在测试篇幅小的文章总是分类错误?这个时候如何做特征选择?是不是说去掉特征集中频率极高和极低的一部分,对结果有所提升?
    答:你说的这个情况是很普遍的现象,篇幅小的文章,特征小,所以模型更容易判断出错!去掉高频和低频通常是可以使得训练的模型泛化能力变强
  • 比如:艺术,文化,历史,教育。界限本来就不明显,比如测试数据“我爱艺术,艺术是我的全部”。结果会分类为文化。其实这个里面还有就是不同特征词的权重问题,采用tf-idf优化下应该会好一些?答:我个人觉得做文本特征提取,还是需要自己去分析文本本身内容的文字特点,你可以把每一类的文本的实体提取出来,然后统计一下每个词在每一类上的数量,看看数量分布,也许可以发现一些数据特点
  • 我就是按照这个思路做的,还有改进时候的停用词,其实可以分析特征文本,针对不同业务,使用自定义的停用词要比通用的好
    还有提前各类见最具表征性的词汇加权,凸显本类的权重是吧?
    答:比如,艺术类文章中,哪些词出现较多,哪些词出现少,再观察这些词的词性主要是哪些,这样可能会对你制定提取特征规则方式的时候提供一定的思路参考,我可以告诉你的是,有些词绝对会某一类文章出出现多,然后在其他类文章出现很少,这一类的词就是文章的特征词
  • 那样的思路可以是:对某类文章单独构建类内的词汇表再进行选择。最后对类间词汇表叠加就ok了。
    答:词汇表有个缺点就是,不能很好的适应新词
  • 改进思路呢
    答:我给你一个改进思路:你只提取每个文本中的名词、动词、形容词、地名,用这些词的作为文本的特征来训练试一试,用文本分类用主题模型(LDA)来向量化文本,再训练模型试一试。如果效果还是不够好,再将文本向量用PCA进行一次特征降维,然后再训练模型试一试,按常理来说,效果应该会有提高
  • 还有我之前个人写的程序分类效果不理想,后来改用sklearn内置BN运行依旧不理想。适当改进了特征提取,还是不理想。估计每类10篇文章的训练数据太少了
    答:文本本身特征提取就相对难一些,再加上训练数据少,训练出来的模型效果可想而已,正常的

sklearn:朴素贝叶斯分类调用

数据准备和数据预处理

加载文档数据集和分类集

数据准备和数据预处理上文已经介绍了,本节增加了一个全局变量存储词汇表,目的是写入到本地文本里,本地读取词汇集,避免每次都做特征向量时加载训练集,提高运行时间。

myVocabList = [] # 设置词汇表的全局变量

'''创建数据集和类标签'''
def loadDataSet():
    docList = [];classList = []  # 文档列表、类别列表、文本特征
    dirlist = ['C3-Art','C4-Literature','C5-Education','C6-Philosophy','C7-History']
    for j in range(5):
        for i in range(1, 11): # 总共10个文档
            # 切分,解析数据,并归类为 1 类别
            wordList = textParse(open('./fudan/%s/%d.txt' % (dirlist[j],i),encoding='UTF-8').read())
            docList.append(wordList)
            classList.append(j)
            # print(i,'\t','./fudan/%s/%d.txt' % (dirlist[j],i),'\t',j)
    # print(len(docList),len(classList),len(fullText))
    global myVocabList
    myVocabList = createVocabList(docList)  # 创建单词集合
    return docList,classList,myVocabList



''' 利用jieba对文本进行分词,返回切词后的list '''
def textParse(str_doc): #与上文方法一致


''' 去掉一些停用词、数字、特殊符号 '''
def rm_tokens(words, stwlist):  #与上文方法一致

文档数据集和分类集在本地读写操作

# 本地存储数据集和标签
def storedata():
    # 3. 计算单词是否出现并创建数据矩阵
    # trainMat =[[0,1,2,3],[2,3,1,5],[0,1,4,2]] # 训练集
    # classList = [0,1,2] #类标签
    docList,classList,myVocabList = loadDataSet()
    # 计算单词是否出现并创建数据矩阵
    trainMat = []
    for postinDoc in docList:
        trainMat.append(bagOfWords2VecMN(myVocabList, postinDoc))
    res = ""
    for i in range(len(trainMat)):
        res +=' '.join([str(x) for x in trainMat[i]])+' '+str(classList[i])+'\n'
    # print(res[:-1]) # 删除最后一个换行符
    with open('./word-bag.txt','w') as fw:
        fw.write(res[:-1])
    with open('./wordset.txt','w') as fw:
        fw.write(' '.join([str(v) for v in myVocabList]))


# 读取本地数据集和标签
    def grabdata():
        f = open('./word-bag.txt') # 读取本地文件
        arrayLines = f.readlines() # 行向量
        tzsize = len(arrayLines[0].split(' '))-1 # 列向量,特征个数减1即数据集
        returnMat = zeros((len(arrayLines),tzsize))    # 0矩阵数据集
        classLabelVactor = []                     # 标签集,特征最后一列

        index = 0
        for line in arrayLines: # 逐行读取
            listFromLine = line.strip().split(' ')    # 分析数据,空格处理
            # print(listFromLine)
            returnMat[index,:] = listFromLine[0:tzsize] # 数据集
            classLabelVactor.append(int(listFromLine[-1])) # 类别标签集
            index +=1
        # print(returnMat,classLabelVactor)
        myVocabList=writewordset()
        return returnMat,classLabelVactor,myVocabList

    def writewordset():
        f1 = open('./wordset.txt')
        myVocabList =f1.readline().split(' ')
        for w in myVocabList:
            if w=='':
                myVocabList.remove(w)
        return myVocabList

获取文档集合和构建词袋模型

'''获取所有文档单词的集合'''
def createVocabList(dataSet):
    vocabSet = set([])
    for document in dataSet:
        vocabSet = vocabSet | set(document)  # 操作符 | 用于求两个集合的并集
    # print(len(vocabSet),len(set(vocabSet)))
    return list(vocabSet)



'''文档词袋模型,创建矩阵数据'''
def bagOfWords2VecMN(vocabList, inputSet):
    returnVec = [0] * len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return returnVec

高斯朴素贝叶斯

GaussianNB 实现了运用于分类的高斯朴素贝叶斯算法。特征的可能性(即概率)假设为高斯分布:

参数使用最大似然法估计。

高斯朴素贝叶斯实现方法代码:

'''高斯朴素贝叶斯'''
def MyGaussianNB(trainMat='',Classlabels='',testDoc=''):
    # -----sklearn GaussianNB-------
    # 训练数据
    X = np.array(trainMat)
    Y = np.array(Classlabels)
    # 高斯分布
    clf = GaussianNB()
    clf.fit(X, Y)
    # 测试预测结果
    index = clf.predict(testDoc) # 返回索引
    reslist = ['Art','Literature','Education','Philosophy','History']
    print(reslist[index[0]])

多项朴素贝叶斯

MultinomialNB 实现了服从多项分布数据的朴素贝叶斯算法,也是用于文本分类(这个领域中数据往往以词向量表示,尽管在实践中 tf-idf 向量在预测时表现良好)的两大经典朴素贝叶斯算法之一。 分布参数由每类 y 的  向量决定, 式中 n 是特征的数量(对于文本分类,是词汇量的大小) 是样本中属于类 y 中特征 i 概率 。

参数 使用平滑过的最大似然估计法来估计,即相对频率计数:

式中 是 训练集 T 中 特征 i 在类 y 中出现的次数,
 是类 y 中出现所有特征的计数总和。
先验平滑因子应用于在学习样本中没有出现的特征,以防在将来的计算中出现0概率输出。 把 被称为拉普拉斯平滑(Lapalce smoothing),而 被称为利德斯通(Lidstone smoothing)。

多项朴素贝叶斯实现方法代码:

'''多项朴素贝叶斯'''
def MyMultinomialNB(trainMat='',Classlabels='',testDoc=''):
    # -----sklearn MultinomialNB-------
    # 训练数据
    X = np.array(trainMat)
    Y = np.array(Classlabels)
    # 多项朴素贝叶斯
    clf = MultinomialNB()
    clf.fit(X, Y)
    # 测试预测结果
    index = clf.predict(testDoc) # 返回索引
    reslist = ['Art','Literature','Education','Philosophy','History']
    print(reslist[index[0]])

伯努利朴素贝叶斯

BernoulliNB 实现了用于多重伯努利分布数据的朴素贝叶斯训练和分类算法,即有多个特征,但每个特征 都假设是一个二元 (Bernoulli, boolean) 变量。 因此,这类算法要求样本以二元值特征向量表示;如果样本含有其他类型的数据, 一个 BernoulliNB 实例会将其二值化(取决于 binarize 参数)。伯努利朴素贝叶斯的决策规则基于

与多项分布朴素贝叶斯的规则不同 伯努利朴素贝叶斯明确地惩罚类 y 中没有出现作为预测因子的特征 i ,而多项分布分布朴素贝叶斯只是简单地忽略没出现的特征。

在文本分类的例子中,词频向量(word occurrence vectors)(而非词数向量(word count vectors))可能用于训练和用于这个分类器。 BernoulliNB 可能在一些数据集上可能表现得更好,特别是那些更短的文档。 如果时间允许,建议对两个模型都进行评估。

伯努利朴素贝叶斯代码实现如下:

'''伯努利朴素贝叶斯'''
def MyBernoulliNB(trainMat='',Classlabels='',testDoc=''):
    # -----sklearn BernoulliNB-------
    # 训练数据
    X = np.array(trainMat)
    Y = np.array(Classlabels)
    # 多项朴素贝叶斯
    clf = BernoulliNB()
    clf.fit(X, Y)
    # 测试预测结果
    index = clf.predict(testDoc) # 返回索引
    reslist = ['Art','Literature','Education','Philosophy','History']
    print(reslist[index[0]])

各种贝叶斯模型分类测试

代码实现如下:
def testingNB():

# 加载数据集和单词集合
trainMat,Classlabels,myVocabList = grabdata() # 读取训练结果
# 测试数据
testEntry = textParse(open('./fudan/test/C6-2.txt',encoding='UTF-8').read())
testDoc = np.array(bagOfWords2VecMN(myVocabList, testEntry)) # 测试数据
# 测试预测结果
MyGaussianNB(trainMat,Classlabels,testDoc)
MyMultinomialNB(trainMat,Classlabels,testDoc)
MyBernoulliNB(trainMat,Classlabels,testDoc)

运行结果:

Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\ADMINI~1\AppData\Local\Temp\jieba.cache
Loading model cost 1.014 seconds.
Prefix dict has been built succesfully.
高斯朴素贝叶斯:Education
多项朴素贝叶斯分类结果:Art
伯努利朴素贝叶斯分类结果:Literature
耗时:2.3996 s

参考文献

  1. scikit中文社区:http://sklearn.apachecn.org/cn/0.19.0/
  2. 中文维基百科:https://zh.wikipedia.org/wiki/
  3. 文本分类特征选择:https://www.cnblogs.com/june0507/p/7601001.html
  4. GitHub:https://github.com/BaiNingchao/MachineLearning-1
  5. 图书:《机器学习实战》
  6. 图书:《自然语言处理理论与实战》

完整代码下载

源码请进【机器学习和自然语言QQ群:436303759】文件下载:

作者声明

本文版权归作者所有,旨在技术交流使用。未经作者同意禁止转载,转载后需在文章页面明显位置给出原文连接,否则相关责任自行承担。白宁超的官网 https://bainingchao.github.io/


http://credit-n.ru/zaymyi-next.html

作者 白 宁超

白宁超,工学硕士,现工作于四川省计算机研究院,研究方向是自然语言处理和机器学习。曾参与国家自然基金项目和四川省科技支撑计划等多个省级项目。著有《自然语言处理理论与实战》一书。

《深度 | 朴素贝叶斯模型算法研究与实例分析》有2条评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注