机器学习实战(三)朴素贝叶斯 (Peter Harrington著)

知识储备:

一、概率论和数理统计

第一章 概率论的基本概念

1、必须要掌握的名词

(1) 样本空间

     一般可以认为是整个样本

(2) 样本点

     其中的一个样本,其中每个样本一般可以理解为特征向量

(3) 放回抽样和不放回抽样

2、必须掌握的公式

(1) 条件概率公式

上式表示 A 事件已经发生后,B 事件发生的概率

通俗理解:A 事件已经发生后,B 事件发生的概率等效为:A 所占面积中 B 占的比率。

第二章 随机变量及其分布

1、必须要掌握的名词

(1) 离散型和连续型的区别 离散型(标称型):可以全部列举出来的变量 连续型:无法完全列举

(2) 分布函数

表征落在某一区间的概率

(3) 连续型随机变量的概率密度(函数) f (x)

2、必须掌握的公式

2.1 必须熟记三个重要的离散型随机变量(最重要的是后面的特点)

(1) 0-1 分布

取值只有0和1

(2) 二项分布

经过 n 重伯努利实验(独立重复试验),每次试验取值只有 A 和 A 两种情况

X 表示 n 重伯努利实验中,事件 A 发生的次数,如果 n = 1 ,则退化为(0-1)分布 

(3) 泊松分布

2.2 必须熟记三个重要的连续型随机变量(最重要的是后面的特点)

(1) 均匀分布

(2) 指数分布

(3) 正态分布、高斯分布 

第三章 多维随机变量及其分布

第四章 随机变量的数字特征

1、必须要掌握的名词

(1) 数学期望-均值

即中心值

(2) 极差

一组数据中的最大值与最小值之差。利用极差有助于及时发现问题,以便采取措施,保证产品质量


(3) 方差、标准差(均方差)

 方差:评价整体偏离中心点的程度

(4) 偏差与方差的关系

偏差:描述的是估计值的期望与真实值之间的差距。偏差越大,越偏离真实数据;

方差:描述的是估计值的变化范围,离散程度,也就是离其期望值的距离。方差越大,数据的分布越分散

欠拟合:表现为代价函数取值较大,偏差较大

过拟合:表现为代价函数取值教小,拟合的曲线波动较大,方差较大 

(5) 协方差

方差仅是针对一维数据的情况进行统计描述,而协方差是度量两个随机变量之间的统计量,例如你想知道银行给人贷款的数额和他有多少套房这两个随机变量的关系,此次就可以 使用协方差计算:

如果协方差为负值,表示房越多,银行贷给你的钱越少,即变化趋势相反 如果协方差为正值,表示房越多,银行贷给你的钱越多,即变化趋势相同 如果协方差为负值,房子数目和银行贷款数目没有线性关系(可能有非线性关系)

(6) 相关系数

衡量两个随机变量的线性相关性,相关系数(皮尔逊系数)

(7) 协方差矩阵
衡量 n 维随机变量中任意两个随机变量之间的线性相关性,非常重要的矩阵,广泛应用于特征降维算法和相关性度量中。

 

第五章 大数定理及中心极限定理

  只需要掌握以下几个结论即可:

1、大数定理:在重复次数足够多的条件下,随机事件往往呈现几乎必然的统计特性,奠定 了概率的基础

2、中心极限定理:当随机变量的个数不断增加时,其和的分布趋近于正态分布,而不需要 考虑和式里面的随机变量服从什么分布 3、正态分布:自然界很多随机现象都符合的分布规律,类似于“二八定则”

第六章 样本和抽样分布

前 5 章属于概率论部分,后面 4 章属于数理统计部分。

数理统计:前 4 章研究的随机变量均假设已知了其分布,在该前提下再研究其性质、特 点和规律,而数理统计正好相反,利用重复独立试验得到的大量样本,对这些数据进行分析, 从而对其分布进行估计。

1、必须要掌握的名词

(1) 总体:一般是未知的,样本只是属于总体中的一个个体而已
(2) 简单随机变量:来自总体的独立同分布变量
(3) 统计量:切记统计量不是一个值,而是一个函数,是除了一个样本值外不包含任何未知参 数的随机变量的函数。
(4) 统计量的作用:统计量是基于样本的指标,是进行统计推断的工具,而实际的总体的指 标是参数。样本是进行统计分析的依据,但当取得一组样本观察值时,往往是些杂乱无章的 数据,并不能直接由此推断总体的性质。这时,还必须针对不同问题由样本构造一个相应的 函数,我们称之为统计量,用它来推断总体的性质,例如样本均值、样本方差等统计量。 (5) 抽样分布:统计量的分布叫抽样分布,注意其指的不是一个总体内所有变量的概率分布。 而是你针对一个总体 N,用固定的样本容量进行多次重复抽样,得到的一个有关样本方差、 均值的组合。
(6) 自由度:可以独立变化的随机变量个数

2、必须要掌握的三大抽样分布

由上面的统计量定义可以知道,统计量有无限个,然后在实际应用中,对于随意构造的 统计量,要计算出其分布非常困难,所以目前只要知道课本上的来自正态总体的三个统计量 的分布即可,以下三个都是指小样本统计量分布。

3、必须要掌握的公式和知识

3.1 直方图和箱线图 一定要会看懂直方图和箱线图(盒型图),具体的绘制可以使用工具箱实现

如何通过箱线图看懂数据分布特点,以及如何通过箱线图识别疑似异常值

 

第七章 参数估计—极其重要的一章

1、重点内容

1.1 最大似然估计 以逻辑回归为例,讲解最大似然思想

第八章 假设检验

在总体的分布未知或者只知道形式但是有未知参数情况下,为了推断总体的一些未知特 性,例如总体均值为多少,首先提出关于总体的一些特性假设,然后根据样本对所提出的假 设做出接受还是拒绝的决策,整个过程称为假设检验。

1、必须要掌握的名词

(1) 原假设和备择假设

研究者想收集证据予以反对的假设,原假设总是“受到保护的假设”,没有充分的证据

是不能拒绝原假设的,带有主观意愿。原假设如何选择不能随意,有一定原则,通常可以将 那些不证自明的、符合规则的放到原假设。

一些比较好的博客或者专栏

总结的非常好的博客或者专栏:

https://woaielf.github.io/2017/03/20/sta-all/#update-log

https://zhuanlan.zhihu.com/p/28069288

http://www.jianshu.com/p/dde6635f4dd9

 参考链接:https://blog.csdn.net/yangshaojun1992/article/details/87865437

一 前言
 

    朴素贝叶斯算法是有监督的学习算法,解决的是分类问题,如客户是否流失、是否值得投资、信用等级评定等多分类问题。该算法的优点在于简单易懂、学习效率高、在某些领域的分类问题中能够与决策树、神经网络相媲美。但由于该算法以自变量之间的独立(条件特征独立)性和连续变量的正态性假设为前提,就会导致算法精度在某种程度上受影响。

    本篇文章将从朴素贝叶斯推断原理开始讲起,通过实例进行辅助讲解。最后,使用Python3编程实现一个简单的言论过滤器。

二 朴素贝叶斯理论

    朴素贝叶斯是贝叶斯决策理论的一部分,所以在讲述朴素贝叶斯之前有必要快速了解一下贝叶斯决策理论。

1 贝叶斯决策理论

    假设现在我们有一个数据集,它由两类数据组成,数据分布如下图所示:

    我们现在用p1(x,y)表示数据点(x,y)属于类别1(图中红色圆点表示的类别)的概率,用p2(x,y)表示数据点(x,y)属于类别2(图中蓝色三角形表示的类别)的概率,那么对于一个新数据点(x,y),可以用下面的规则来判断它的类别:

如果p1(x,y) > p2(x,y),那么类别为1
如果p1(x,y) < p2(x,y),那么类别为2
    也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策。已经了解了贝叶斯决策理论的核心思想,那么接下来,就是学习如何计算p1和p2概率。

2 条件概率

    在学习计算p1和p2概率之前,我们需要了解什么是条件概率(Condittional probability),就是指在事件B发生的情况下,事件A发生的概率,用P(A|B)来表示。

    根据文氏图,可以很清楚地看到在事件B发生的情况下,事件A发生的概率就是P(A∩B)除以P(B)。

                                                  

因此,

                                                

同理可得,

                                                

所以,

                                             

                                            

这就是条件概率的计算公式。

3 全概率公式

    除了条件概率以外,在计算p1和p2的时候,还要用到全概率公式,因此,这里继续推导全概率公式。

    假定样本空间S,是两个事件A与A’的和。

                                                      

   上图中,红色部分是事件A,绿色部分是事件A’,它们共同构成了样本空间S。

    在这种情况下,事件B可以划分成两个部分。

                                                      

                                                      

在上一节的推导当中,我们已知

                                                    
所以,

                                                   

这就是全概率公式。它的含义是,如果A和A’构成样本空间的一个划分,那么事件B的概率,就等于A和A’的概率分别乘以B对这两个事件的条件概率之和。

将这个公式代入上一节的条件概率公式,就得到了条件概率的另一种写法:

                                                  

4 贝叶斯推断

    对条件概率公式进行变形,可以得到如下形式

                                                  

    我们把P(A)称为"先验概率"(Prior probability),即在B事件发生之前,我们对A事件概率的一个判断。

    P(A|B)称为"后验概率"(Posterior probability),即在B事件发生之后,我们对A事件概率的重新评估。

    P(B|A)/P(B)称为"可能性函数"(Likelyhood),这是一个调整因子,使得预估概率更接近真实概率。

    所以,条件概率可以理解成下面的式子:后验概率 = 先验概率 x 调整因子

     在这里,如果"可能性函数"P(B|A)/P(B)>1,意味着"先验概率"被增强,事件A的发生的可能性变大;如果"可能性函数"=1,意味着B事件无助于判断事件A的可能性;如果"可能性函数"<1,意味着"先验概率"被削弱,事件A的可能性变小。

    为了加深对贝叶斯推断的理解,我们一个例子。
                                         

    两个一模一样的碗,一号碗有30颗水果糖和10颗巧克力糖,二号碗有水果糖和巧克力糖各20颗。现在随机选择一个碗,从中摸出一颗糖,发现是水果糖。请问这颗水果糖来自一号碗的概率有多大?

    我们假定,H1表示一号碗,H2表示二号碗。由于这两个碗是一样的,所以P(H1)=P(H2),也就是说,在取出水果糖之前,这两个碗被选中的概率相同。因此,P(H1)=0.5,我们把这个概率就叫做"先验概率",即没有做实验之前,来自一号碗的概率是0.5。

    再假定,E表示水果糖,所以问题就变成了在已知E的情况下,来自一号碗的概率有多大,即求P(H1|E)。我们把这个概率叫做"后验概率",即在E事件发生之后,对P(H1)的修正。

    根据条件概率公式,得到      
                                                          

    已知,P(H1)等于0.5,P(E|H1)为一号碗中取出水果糖的概率,等于30÷(30+10)=0.75,那么求出P(E)就可以得到答案。根据全概率公式,

                                                       

所以

                                                        

将数字代入原方程,得到

                                                        

    这表明,来自一号碗的概率是0.6。也就是说,取出水果糖之后,H1事件的可能性得到了增强。

    同时再思考一个问题,在使用该算法的时候,如果不需要知道具体的类别概率,即上面P(H1|E)=0.6,只需要知道所属类别,即来自一号碗,我们有必要计算P(E)这个全概率吗?要知道我们只需要比较 P(H1|E)和P(H2|E)的大小,找到那个最大的概率就可以。既然如此,两者的分母都是相同的,那我们只需要比较分子即可。即比较P(E|H1)P(H1)和P(E|H2)P(H2)的大小,所以为了减少计算量,全概率公式在实际编程中可以不使用。

5 朴素贝叶斯推断

    理解了贝叶斯推断,那么让我们继续看看朴素贝叶斯。贝叶斯和朴素贝叶斯的概念是不同的,区别就在于“朴素”二字,朴素贝叶斯对条件个概率分布做了条件独立性的假设。 比如下面的公式,假设有n个特征:

                                      

    由于每个特征都是独立的,我们可以进一步拆分公式

                                   

这样我们就可以进行计算了。如果有些迷糊,让我们从一个例子开始讲起,你会看到贝叶斯分类器很好懂,一点都不难。

某个医院早上来了六个门诊的病人,他们的情况如下表所示:

     

 现在又来了第七个病人,是一个打喷嚏的建筑工人。请问他患上感冒的概率有多大?

 根据贝叶斯定理:

                                                                 

可得:

                                                             

 根据朴素贝叶斯条件独立性的假设可知,"打喷嚏"和"建筑工人"这两个特征是独立的,因此,上面的等式就变成了

                                                   

这里可以计算:

                                             

因此,这个打喷嚏的建筑工人,有66%的概率是得了感冒。同理,可以计算这个病人患上过敏或脑震荡的概率。比较这几个概率,就可以知道他最可能得什么病。

这就是贝叶斯分类器的基本方法:在统计资料的基础上,依据某些特征,计算各个类别的概率,从而实现分类。

同样,在编程的时候,如果不需要求出所属类别的具体概率,P(打喷嚏) = 0.5和P(建筑工人) = 0.33的概率是可以不用求的。

三 动手实战

    说了这么多,没点实践编程怎么行?

    以在线社区留言为例。为了不影响社区的发展,我们要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标志为内容不当。过滤这类内容是一个很常见的需求。对此问题建立两个类型:侮辱类和非侮辱类,使用1和0分别表示。

    我们把文本看成单词向量或者词条向量,也就是说将句子转换为向量。考虑出现所有文档中的单词,再决定将哪些单词纳入词汇表或者说所要的词汇集合,然后必须要将每一篇文档转换为词汇表上的向量。简单起见,我们先假设已经将本文切分完毕,存放到列表中,并对词汇向量进行分类标注。编写代码如下:
 

# -*- coding:utf-8 -*-
from math import log
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from numpy import *


# 训练样本 (这里的训练样本已经被切分为了词条)
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]  # 类别标签向量 1 代表侮辱性的文字 0 代表正常的言论
    return postingList, classVec


"""
函数说明:创建词汇表
1、将文档的单词去重(set)
2、然后保存到list集合中作为词汇表 (后续将样本单词转为词向量会用到)
Parameters:
    dataSet:训练集
Returns:
    list(vocabSet) - 创建的词汇表
['my', 'so', 'buying', 'park', 'help', 'licks', 'dalmation', 'please', 'has', 'ate', 'problems', 'garbage', 'him', 
'maybe', 'take', 'steak', 'love', 'cute', 'stupid', 'I', 'stop', 'is', 'flea', 'quit', 'food', 'worthless', 'to', 
'how', 'dog', 'posting', 'mr', 'not']
"""


def createVocabList(dataSet):
    vocabSet = set([])  # 创建一个空集
    for document in dataSet:
        vocabSet = vocabSet | set(document)  # | 创建两个集合的并集
    return list(vocabSet)


"""
函数说明:根据词汇表将训练样本 转为词向量

Parameters:
    vocabSList:词汇表
    inputSet:训练样本
Returns:
    returnVec - 训练样本转为词向量
    [1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0]
"""


def setOfWords2Vec(vocabSList, inputSet):
    returnVec = [0] * len(vocabSList)  # 创建一个一行 len(vocabSList) 列 元素值为0的矩阵
    for word in inputSet:
        if word in vocabSList:
            returnVec[vocabSList.index(word)] = 1  # 获取指定单词的索引
        else:
            print("the word:%s is not in my Vocabulary!" % word)
    return returnVec


"""
函数说明:朴素贝叶斯分类器训练函数 
        求出的结果作为后续分类使用
Parameters:
    trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
    trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
Returns:
    p0Vect - 侮辱类的条件概率数组
    p1Vect - 非侮辱类的条件概率数组
    pAbusive - 文档属于侮辱类的概率

"""


def trainNB0(trainMatrix, trainGategory):
    numTrainDocs = len(trainMatrix)  # 训练样本数
    numWords = len(trainMatrix[0])  # 样本单词数
    pAbusive = sum(trainGategory) / float(numTrainDocs)  # 训练样本中侮辱性言论所占的比率
    p0Num = ones(numWords)  # 正常言论样本中,增加词条的计数值
    p1Num = ones(numWords)  # 侮辱言论样本中,增加词条的计数值
    p0Demo = 2.0  # 正常言论样本中,所有词条的计数值
    p1Demo = 2.0  # 侮辱言论样本中,所有词条的计数值
    for i in range(numTrainDocs):
        if trainGategory[i] == 1:
            p1Num += trainMatrix[i]  # 侮辱性言论样本中 各单词出现的次数
            p1Demo += sum(trainMatrix[i])  # 侮辱性言论样本中 总共的单词数
        else:
            p0Num += trainMatrix[i]  # 正常言论样本中 各单词出现的次数
            p0Demo += sum(trainMatrix[i])  # 正常言论样本中 各单词出现的次数

    p1Vect = log(p1Num / p1Demo)
    p0Vect = log(p0Num / p0Demo)
    return p0Vect, p1Vect, pAbusive


def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + log(pClass1) # 自然对数计算 ln(a*b)=ln(a)+ln(b) 
    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))


if __name__ == '__main__':
    postingList, classVec = loadDataSet()
    myVocabList = createVocabList(postingList)
    print(myVocabList)
    wordVec = setOfWords2Vec(myVocabList, postingList[0])
    print(wordVec)
    trainMat = []
    for postinDoc in postingList:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    print(trainMat)
    p0V, p1V, pAb = trainNB0(trainMat, classVec)
    print(p0V)
    print(p1V)
    print(pAb)
    testingNB()

四 总结

朴素贝叶斯推断的一些优点:

生成式模型,通过计算概率来进行分类,可以用来处理多分类问题。
对小规模的数据表现很好,适合多分类任务,适合增量式训练,算法也比较简单。

朴素贝叶斯推断的一些缺点:

对输入数据的表达形式很敏感。
由于朴素贝叶斯的“朴素”特点,所以会带来一些准确率上的损失。
需要计算先验概率,分类决策存在错误率。

五、朴素贝叶斯之过滤垃圾邮件

在上篇文章那个简单的例子中,我们引入了字符串列表。使用朴素贝叶斯解决一些现实生活中的问题时,需要先从文本内容得到字符串列表,然后生成词向量。下面这个例子中,我们将了解朴素贝叶斯的一个最著名的应用:电子邮件垃圾过滤。首先看一下使用朴素贝叶斯对电子邮件进行分类的步骤:

收集数据:提供文本文件。
准备数据:将文本文件解析成词条向量。
分析数据:检查词条确保解析的正确性。
训练算法:使用我们之前建立的trainNB0()函数。
测试算法:使用classifyNB(),并构建一个新的测试函数来计算文档集的错误率。
使用算法:构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上。
1 收集数据
数据我已经为大家准备好了,可以在我的Github上下载:https://github.com/Jack-Cherish/Machine-Learning/tree/master/Naive%20Bayes/email

有两个文件夹ham和spam,spam文件下的txt文件为垃圾邮件。

2 准备数据
对于英文文本,我们可以以非字母、非数字作为符号进行切分,使用split函数即可。编写代码如下:

# 垃圾邮件分类
"""
函数说明:接收一个大字符串并将其解析为字符串列表
Parameters:
    bigString
Returns:
    无
"""
def textParse(bigString):                                                   #将字符串转换为字符列表
    listOfTokens = re.split(r'\W+', bigString)                              #将特殊符号作为切分标志进行字符串切分,即非字母、非数字
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]            #除了单个字母,例如大写的I,其它单词变成小写

# 使用朴素贝叶斯进行垃圾邮件过滤
def spamTest():
    docList = [];
    classList = [];
    for i in range(1, 26):  # 遍历25个txt文件
        wordList = textParse(open('/Users/yangshaojun/python_workspace/chapter04/email/spam/%d.txt' % i, 'r').read())  # 读取每个垃圾邮件,并字符串转换成字符串列表
        docList.append(wordList)
        classList.append(1)  # 标记垃圾邮件,1表示垃圾文件
        wordList = textParse(open('/Users/yangshaojun/python_workspace/chapter04/email/ham/%d.txt' % i, 'r').read())  # 读取每个非垃圾邮件,并字符串转换成字符串列表
        docList.append(wordList)
        classList.append(0)  # 标记非垃圾邮件,1表示垃圾文件
    vocabList = createVocabList(docList)  # 创建词汇表,不重复
    trainingSet = list(range(50));
    testSet = []  # 创建存储训练集的索引值的列表和测试集的索引值的列表
    for i in range(10):  # 从50个邮件中,随机挑选出40个作为训练集,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(array(trainMat), array(trainClasses))  # 训练朴素贝叶斯模型
    errorCount = 0  # 错误分类计数
    for docIndex in testSet:  # 遍历测试集
        wordVector = setOfWords2Vec(vocabList, docList[docIndex])  # 测试集的词集模型
        if classifyNB(array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:  # 如果分类错误
            errorCount += 1  # 错误计数加1
            print("分类错误的测试集:", docList[docIndex])
    print('错误率:%.2f%%' % (float(errorCount) / len(testSet) * 100))

运行结果如下:

        函数spamTest()会输出在10封随机选择的电子邮件上的分类错误概率。既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。如果发现错误的话,函数会输出错误的文档的此表,这样就可以了解到底是哪篇文档发生了错误。如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说10次,然后求平均值。相比之下,将垃圾邮件误判为正常邮件要比将正常邮件归为垃圾邮件好。为了避免错误,有多种方式可以用来修正分类器,这些内容会在后续文章中进行讨论。
 

六、 朴素贝叶斯之新浪新闻分类(Sklearn)

1 中文语句切分


考虑一个问题,英文的语句可以通过非字母和非数字进行切分,但是汉语句子呢?就比如我打的这一堆字,该如何进行切分呢?我们自己写个规则?

幸运地是,这部分的工作不需要我们自己做了,可以直接使用第三方分词组件,即jieba,没错就是”结巴”。

jieba已经兼容Python2和Python3,使用如下指令直接安装即可:

pip3 install jieba
1
Python中文分词组件使用简单:

官方教程:https://github.com/fxsjy/jieba
民间教程:https://www.oschina.net/p/jieba
新闻分类数据集我也已经准备好,可以到我的Github进行下载:https://github.com/Jack-Cherish/Machine-Learning/tree/master/Naive%20Bayes/SogouC

数据集已经做好分类,分文件夹保存,分类结果如下:

                                                                   

数据集已经准备好,接下来,让我们直接进入正题。切分中文语句,编写如下代码:

# -*- coding: UTF-8 -*-
import os
import jieba

def TextProcessing(folder_path):
    folder_list = os.listdir(folder_path)                        #查看folder_path下的文件
    data_list = []                                                #训练集
    class_list = []

    #遍历每个子文件夹
    for folder in folder_list:
        new_folder_path = os.path.join(folder_path, folder)        #根据子文件夹,生成新的路径
        files = os.listdir(new_folder_path)                        #存放子文件夹下的txt文件的列表

        j = 1
        #遍历每个txt文件
        for file in files:
            if j > 100:                                            #每类txt样本数最多100个
                break
            with open(os.path.join(new_folder_path, file), 'r', encoding = 'utf-8') as f:    #打开txt文件
                raw = f.read()

            word_cut = jieba.cut(raw, cut_all = False)            #精简模式,返回一个可迭代的generator
            word_list = list(word_cut)                            #generator转换为list

            data_list.append(word_list)
            class_list.append(folder)
            j += 1
        print(data_list)
        print(class_list)
if __name__ == '__main__':
    #文本预处理
    folder_path = './SogouC/Sample'                #训练集存放地址
    TextProcessing(folder_path)

2 文本特征选择

我们将所有文本分成训练集和测试集,并对训练集中的所有单词进行词频统计,并按降序排序。也就是将出现次数多的词语在前,出现次数少的词语在后进行排序。编写代码如下:

# -*- coding: UTF-8 -*-
import os
import random
import jieba

"""
函数说明:中文文本处理

Parameters:
    folder_path - 文本存放的路径
    test_size - 测试集占比,默认占所有数据集的百分之20
Returns:
    all_words_list - 按词频降序排序的训练集列表
    train_data_list - 训练集列表
    test_data_list - 测试集列表
    train_class_list - 训练集标签列表
    test_class_list - 测试集标签列表
"""
def TextProcessing(folder_path, test_size = 0.2):
    folder_list = os.listdir(folder_path)                        #查看folder_path下的文件
    data_list = []                                                #数据集数据
    class_list = []                                                #数据集类别

    #遍历每个子文件夹
    for folder in folder_list:
        new_folder_path = os.path.join(folder_path, folder)        #根据子文件夹,生成新的路径
        files = os.listdir(new_folder_path)                        #存放子文件夹下的txt文件的列表

        j = 1
        #遍历每个txt文件
        for file in files:
            if j > 100:                                            #每类txt样本数最多100个
                break
            with open(os.path.join(new_folder_path, file), 'r', encoding = 'utf-8') as f:    #打开txt文件
                raw = f.read()

            word_cut = jieba.cut(raw, cut_all = False)            #精简模式,返回一个可迭代的generator
            word_list = list(word_cut)                            #generator转换为list

            data_list.append(word_list)                            #添加数据集数据
            class_list.append(folder)                            #添加数据集类别
            j += 1

    data_class_list = list(zip(data_list, class_list))            #zip压缩合并,将数据与标签对应压缩
    random.shuffle(data_class_list)                                #将data_class_list乱序
    index = int(len(data_class_list) * test_size) + 1            #训练集和测试集切分的索引值
    train_list = data_class_list[index:]                        #训练集
    test_list = data_class_list[:index]                            #测试集
    train_data_list, train_class_list = zip(*train_list)        #训练集解压缩
    test_data_list, test_class_list = zip(*test_list)            #测试集解压缩

    all_words_dict = {}                                            #统计训练集词频
    for word_list in train_data_list:
        for word in word_list:
            if word in all_words_dict.keys():
                all_words_dict[word] += 1
            else:
                all_words_dict[word] = 1

    #根据键的值倒序排序
    all_words_tuple_list = sorted(all_words_dict.items(), key = lambda f:f[1], reverse = True)
    all_words_list, all_words_nums = zip(*all_words_tuple_list)    #解压缩
    all_words_list = list(all_words_list)                        #转换成列表
    return all_words_list, train_data_list, test_data_list, train_class_list, test_class_list

if __name__ == '__main__':
    #文本预处理
    folder_path = './SogouC/Sample'                #训练集存放地址
    all_words_list, train_data_list, test_data_list, train_class_list, test_class_list = TextProcessing(folder_path, test_size=0.2)
    print(all_words_list)

        all_words_list就是将所有训练集的切分结果通过词频降序排列构成的单词合集。观察一下打印结果,不难发现,这里包含了很多标点符号,很显然,这些标点符号是不能作为新闻分类的特征的。总不能说,应为这个文章逗号多,所以它是xx类新闻吧?为了降低这些高频的符号对分类结果的影响,我们应该怎么做呢?答曰:抛弃他们! 除了这些,还有”在”,”了”这样对新闻分类无关痛痒的词。并且还有一些数字,数字显然也不能作为分类新闻的特征。所以要消除它们对分类结果的影响,我们可以定制一个规则。

        一个简单的规则可以这样制定:首先去掉高频词,至于去掉多少个高频词,我们可以通过观察去掉高频词个数和最终检测准确率的关系来确定。除此之外,去除数字,不把数字作为分类特征。同时,去除一些特定的词语,比如:”的”,”一”,”在”,”不”,”当然”,”怎么”这类的对新闻分类无影响的介词、代词、连词。怎么去除这些词呢?可以使用已经整理好的stopwords_cn.txt文本。下载地址 https://github.com/Jack-Cherish/Machine-Learning/blob/master/Naive%20Bayes/stopwords_cn.txt

这个文件是这个样子的:

所以我们可以根据这个文档,将这些单词去除,不作为分类的特征。我们先去除前100个高频词汇,然后编写代码如下:

# -*- coding: UTF-8 -*-
import os
import random
import jieba

"""
函数说明:中文文本处理

Parameters:
    folder_path - 文本存放的路径
    test_size - 测试集占比,默认占所有数据集的百分之20
Returns:
    all_words_list - 按词频降序排序的训练集列表
    train_data_list - 训练集列表
    test_data_list - 测试集列表
    train_class_list - 训练集标签列表
    test_class_list - 测试集标签列表
"""
def TextProcessing(folder_path, test_size = 0.2):
    folder_list = os.listdir(folder_path)                        #查看folder_path下的文件
    data_list = []                                                #数据集数据
    class_list = []                                                #数据集类别

    #遍历每个子文件夹
    for folder in folder_list:
        new_folder_path = os.path.join(folder_path, folder)        #根据子文件夹,生成新的路径
        files = os.listdir(new_folder_path)                        #存放子文件夹下的txt文件的列表

        j = 1
        #遍历每个txt文件
        for file in files:
            if j > 100:                                            #每类txt样本数最多100个
                break
            with open(os.path.join(new_folder_path, file), 'r', encoding = 'utf-8') as f:    #打开txt文件
                raw = f.read()

            word_cut = jieba.cut(raw, cut_all = False)            #精简模式,返回一个可迭代的generator
            word_list = list(word_cut)                            #generator转换为list

            data_list.append(word_list)                            #添加数据集数据
            class_list.append(folder)                            #添加数据集类别
            j += 1

    data_class_list = list(zip(data_list, class_list))            #zip压缩合并,将数据与标签对应压缩
    random.shuffle(data_class_list)                                #将data_class_list乱序
    index = int(len(data_class_list) * test_size) + 1            #训练集和测试集切分的索引值
    train_list = data_class_list[index:]                        #训练集
    test_list = data_class_list[:index]                            #测试集
    train_data_list, train_class_list = zip(*train_list)        #训练集解压缩
    test_data_list, test_class_list = zip(*test_list)            #测试集解压缩

    all_words_dict = {}                                            #统计训练集词频
    for word_list in train_data_list:
        for word in word_list:
            if word in all_words_dict.keys():
                all_words_dict[word] += 1
            else:
                all_words_dict[word] = 1

    #根据键的值倒序排序
    all_words_tuple_list = sorted(all_words_dict.items(), key = lambda f:f[1], reverse = True)
    all_words_list, all_words_nums = zip(*all_words_tuple_list)    #解压缩
    all_words_list = list(all_words_list)                        #转换成列表
    return all_words_list, train_data_list, test_data_list, train_class_list, test_class_list

"""
函数说明:读取文件里的内容,并去重

Parameters:
    words_file - 文件路径
Returns:
    words_set - 读取的内容的set集合
"""
def MakeWordsSet(words_file):
    words_set = set()                                            #创建set集合
    with open(words_file, 'r', encoding = 'utf-8') as f:        #打开文件
        for line in f.readlines():                                #一行一行读取
            word = line.strip()                                    #去回车
            if len(word) > 0:                                    #有文本,则添加到words_set中
                words_set.add(word)                               
    return words_set                                             #返回处理结果

"""
函数说明:文本特征选取

Parameters:
    all_words_list - 训练集所有文本列表
    deleteN - 删除词频最高的deleteN个词
    stopwords_set - 指定的结束语
Returns:
    feature_words - 特征集
"""
def words_dict(all_words_list, deleteN, stopwords_set = set()):
    feature_words = []                            #特征列表
    n = 1
    for t in range(deleteN, len(all_words_list), 1):
        if n > 1000:                            #feature_words的维度为1000
            break                               
        #如果这个词不是数字,并且不是指定的结束语,并且单词长度大于1小于5,那么这个词就可以作为特征词
        if not all_words_list[t].isdigit() and all_words_list[t] not in stopwords_set and 1 < len(all_words_list[t]) < 5:
            feature_words.append(all_words_list[t])
        n += 1
    return feature_words

if __name__ == '__main__':
    #文本预处理
    folder_path = './SogouC/Sample'                #训练集存放地址
    all_words_list, train_data_list, test_data_list, train_class_list, test_class_list = TextProcessing(folder_path, test_size=0.2)

    #生成stopwords_set
    stopwords_file = './stopwords_cn.txt'
    stopwords_set = MakeWordsSet(stopwords_file)

    feature_words = words_dict(all_words_list, 100, stopwords_set)
    print(feature_words)

运行结果如下:

可以看到,我们已经滤除了那些没有用的词组,这个feature_words就是我们最终选出的用于新闻分类的特征。随后,我们就可以根据feature_words,将文本向量化,然后用于训练朴素贝叶斯分类器。这个向量化的思想和第三章的思想一致,因此不再累述。

3 使用Sklearn构建朴素贝叶斯分类器

数据已经处理好了,接下来就可以使用sklearn构建朴素贝叶斯分类器了。

官方英文文档地址:http://scikit-learn.org/dev/modules/generated/sklearn.naive_bayes.MultinomialNB.html

朴素贝叶斯是一类比较简单的算法,scikit-learn中朴素贝叶斯类库的使用也比较简单。相对于决策树,KNN之类的算法,朴素贝叶斯需要关注的参数是比较少的,这样也比较容易掌握。在scikit-learn中,一共有3个朴素贝叶斯的分类算法类。分别是GaussianNB,MultinomialNB和BernoulliNB。其中GaussianNB就是先验为高斯分布的朴素贝叶斯,MultinomialNB就是先验为多项式分布的朴素贝叶斯,而BernoulliNB就是先验为伯努利分布的朴素贝叶斯。上篇文章讲解的先验概率模型就是先验概率为多项式分布的朴素贝叶斯。    

对于新闻分类,属于多分类问题。我们可以使用MultinamialNB()完成我们的新闻分类问题。另外两个函数的使用暂且不再进行扩展,可以自行学习。MultinomialNB假设特征的先验概率为多项式分布,即如下式:

                                      

其中,P(Xj = Xjl | Y = Ck)是第k个类别的第j维特征的第l个取值条件概率。mk是训练集中输出为第k类的样本个数。λ为一个大于0的常数,尝尝取值为1,即拉普拉斯平滑,也可以取其他值。

接下来,我们看下MultinamialNB这个函数,只有3个参数:

参数说明如下:

alpha:浮点型可选参数,默认为1.0,其实就是添加拉普拉斯平滑,即为上述公式中的λ ,如果这个参数设置为0,就是不添加平滑;
fit_prior:布尔型可选参数,默认为True。布尔参数fit_prior表示是否要考虑先验概率,如果是false,则所有的样本类别输出都有相同的类别先验概率。否则可以自己用第三个参数class_prior输入先验概率,或者不输入第三个参数class_prior让MultinomialNB自己从训练集样本来计算先验概率,此时的先验概率为P(Y=Ck)=mk/m。其中m为训练集样本总数量,mk为输出为第k类别的训练集样本数。
class_prior:可选参数,默认为None。

总结如下:

MultinomialNB一个重要的功能是有partial_fit方法,这个方法的一般用在如果训练集数据量非常大,一次不能全部载入内存的时候。这时我们可以把训练集分成若干等分,重复调用partial_fit来一步步的学习训练集,非常方便。GaussianNB和BernoulliNB也有类似的功能。 在使用MultinomialNB的fit方法或者partial_fit方法拟合数据后,我们可以进行预测。此时预测有三种方法,包括predict,predict_log_proba和predict_proba。predict方法就是我们最常用的预测方法,直接给出测试集的预测类别输出。predict_proba则不同,它会给出测试集样本在各个类别上预测的概率。容易理解,predict_proba预测出的各个类别概率里的最大值对应的类别,也就是predict方法得到类别。predict_log_proba和predict_proba类似,它会给出测试集样本在各个类别上预测的概率的一个对数转化。转化后predict_log_proba预测出的各个类别对数概率里的最大值对应的类别,也就是predict方法得到类别。具体细节不再讲解,可参照官网手册。

了解了这些,我们就可以编写代码,通过观察取不同的去掉前deleteN个高频词的个数与最终检测准确率的关系,确定deleteN的取值:
 

# -*- coding: UTF-8 -*-
from sklearn.naive_bayes import MultinomialNB
import matplotlib.pyplot as plt
import os
import random
import jieba

"""
函数说明:中文文本处理

Parameters:
    folder_path - 文本存放的路径
    test_size - 测试集占比,默认占所有数据集的百分之20
Returns:
    all_words_list - 按词频降序排序的训练集列表
    train_data_list - 训练集列表
    test_data_list - 测试集列表
    train_class_list - 训练集标签列表
    test_class_list - 测试集标签列表
"""
def TextProcessing(folder_path, test_size = 0.2):
    folder_list = os.listdir(folder_path)                        #查看folder_path下的文件
    data_list = []                                                #数据集数据
    class_list = []                                                #数据集类别

    #遍历每个子文件夹
    for folder in folder_list:
        new_folder_path = os.path.join(folder_path, folder)        #根据子文件夹,生成新的路径
        files = os.listdir(new_folder_path)                        #存放子文件夹下的txt文件的列表

        j = 1
        #遍历每个txt文件
        for file in files:
            if j > 100:                                            #每类txt样本数最多100个
                break
            with open(os.path.join(new_folder_path, file), 'r', encoding = 'utf-8') as f:    #打开txt文件
                raw = f.read()

            word_cut = jieba.cut(raw, cut_all = False)            #精简模式,返回一个可迭代的generator
            word_list = list(word_cut)                            #generator转换为list

            data_list.append(word_list)                            #添加数据集数据
            class_list.append(folder)                            #添加数据集类别
            j += 1

    data_class_list = list(zip(data_list, class_list))            #zip压缩合并,将数据与标签对应压缩
    random.shuffle(data_class_list)                                #将data_class_list乱序
    index = int(len(data_class_list) * test_size) + 1            #训练集和测试集切分的索引值
    train_list = data_class_list[index:]                        #训练集
    test_list = data_class_list[:index]                            #测试集
    train_data_list, train_class_list = zip(*train_list)        #训练集解压缩
    test_data_list, test_class_list = zip(*test_list)            #测试集解压缩

    all_words_dict = {}                                            #统计训练集词频
    for word_list in train_data_list:
        for word in word_list:
            if word in all_words_dict.keys():
                all_words_dict[word] += 1
            else:
                all_words_dict[word] = 1

    #根据键的值倒序排序
    all_words_tuple_list = sorted(all_words_dict.items(), key = lambda f:f[1], reverse = True)
    all_words_list, all_words_nums = zip(*all_words_tuple_list)    #解压缩
    all_words_list = list(all_words_list)                        #转换成列表
    return all_words_list, train_data_list, test_data_list, train_class_list, test_class_list

"""
函数说明:读取文件里的内容,并去重

Parameters:
    words_file - 文件路径
Returns:
    words_set - 读取的内容的set集合
"""
def MakeWordsSet(words_file):
    words_set = set()                                            #创建set集合
    with open(words_file, 'r', encoding = 'utf-8') as f:        #打开文件
        for line in f.readlines():                                #一行一行读取
            word = line.strip()                                    #去回车
            if len(word) > 0:                                    #有文本,则添加到words_set中
                words_set.add(word)                               
    return words_set                                             #返回处理结果

"""
函数说明:根据feature_words将文本向量化

Parameters:
    train_data_list - 训练集
    test_data_list - 测试集
    feature_words - 特征集
Returns:
    train_feature_list - 训练集向量化列表
    test_feature_list - 测试集向量化列表
"""
def TextFeatures(train_data_list, test_data_list, feature_words):
    def text_features(text, feature_words):                        #出现在特征集中,则置1                                               
        text_words = set(text)
        features = [1 if word in text_words else 0 for word in feature_words]
        return features
    train_feature_list = [text_features(text, feature_words) for text in train_data_list]
    test_feature_list = [text_features(text, feature_words) for text in test_data_list]
    return train_feature_list, test_feature_list                #返回结果


"""
函数说明:文本特征选取

Parameters:
    all_words_list - 训练集所有文本列表
    deleteN - 删除词频最高的deleteN个词
    stopwords_set - 指定的结束语
Returns:
    feature_words - 特征集
"""
def words_dict(all_words_list, deleteN, stopwords_set = set()):
    feature_words = []                            #特征列表
    n = 1
    for t in range(deleteN, len(all_words_list), 1):
        if n > 1000:                            #feature_words的维度为1000
            break                               
        #如果这个词不是数字,并且不是指定的结束语,并且单词长度大于1小于5,那么这个词就可以作为特征词
        if not all_words_list[t].isdigit() and all_words_list[t] not in stopwords_set and 1 < len(all_words_list[t]) < 5:
            feature_words.append(all_words_list[t])
        n += 1
    return feature_words

"""
函数说明:新闻分类器

Parameters:
    train_feature_list - 训练集向量化的特征文本
    test_feature_list - 测试集向量化的特征文本
    train_class_list - 训练集分类标签
    test_class_list - 测试集分类标签
Returns:
    test_accuracy - 分类器精度
"""
def TextClassifier(train_feature_list, test_feature_list, train_class_list, test_class_list):
    classifier = MultinomialNB().fit(train_feature_list, train_class_list)
    test_accuracy = classifier.score(test_feature_list, test_class_list)
    return test_accuracy

if __name__ == '__main__':
    #文本预处理
    folder_path = './SogouC/Sample'                #训练集存放地址
    all_words_list, train_data_list, test_data_list, train_class_list, test_class_list = TextProcessing(folder_path, test_size=0.2)

    # 生成stopwords_set
    stopwords_file = './stopwords_cn.txt'
    stopwords_set = MakeWordsSet(stopwords_file)


    test_accuracy_list = []
    deleteNs = range(0, 1000, 20)                #0 20 40 60 ... 980
    for deleteN in deleteNs:
        feature_words = words_dict(all_words_list, deleteN, stopwords_set)
        train_feature_list, test_feature_list = TextFeatures(train_data_list, test_data_list, feature_words)
        test_accuracy = TextClassifier(train_feature_list, test_feature_list, train_class_list, test_class_list)
        test_accuracy_list.append(test_accuracy)

    plt.figure()
    plt.plot(deleteNs, test_accuracy_list)
    plt.title('Relationship of deleteNs and test_accuracy')
    plt.xlabel('deleteNs')
    plt.ylabel('test_accuracy')
    plt.show()

 运行结果如下:

我们绘制出了deleteNs和test_accuracy的关系,这样我们就可以大致确定去掉前多少的高频词汇了。每次运行程序,绘制的图形可能不尽相同,我们可以通过多次测试,来决定这个deleteN的取值,然后确定这个参数,这样就可以顺利构建出用于新闻分类的朴素贝叶斯分类器了。我测试感觉450还不错,最差的分类准确率也可以达到百分之50以上。将if __name__ == '__main__'下的代码修改如下:
 

if __name__ == '__main__':
    #文本预处理
    folder_path = './SogouC/Sample'                #训练集存放地址
    all_words_list, train_data_list, test_data_list, train_class_list, test_class_list = TextProcessing(folder_path, test_size=0.2)

    # 生成stopwords_set
    stopwords_file = './stopwords_cn.txt'
    stopwords_set = MakeWordsSet(stopwords_file)


    test_accuracy_list = []
    feature_words = words_dict(all_words_list, 450, stopwords_set)
    train_feature_list, test_feature_list = TextFeatures(train_data_list, test_data_list, feature_words)
    test_accuracy = TextClassifier(train_feature_list, test_feature_list, train_class_list, test_class_list)
    test_accuracy_list.append(test_accuracy)
    ave = lambda c: sum(c) / len(c)

运行结果:

七、 总结


在训练朴素贝叶斯分类器之前,要处理好训练集,文本的清洗还是有很多需要学习的东西。
根据提取的分类特征将文本向量化,然后训练朴素贝叶斯分类器。
去高频词汇数量的不同,对结果也是有影响的的。
拉普拉斯平滑对于改善朴素贝叶斯分类器的分类效果有着积极的作用。

发布了110 篇原创文章 · 获赞 22 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/yangshaojun1992/article/details/104029029
今日推荐