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

(白宁超 2018年9月2日 11: 16:31)

 

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

案例场景1: 屏蔽社区留言板的侮辱性言论

项目概述

构建一个快速过滤器来屏蔽在线社区留言板上的侮辱性言论。如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标识为内容不当。对此问题建立两个类别: 侮辱类和非侮辱类,使用 1 和 0 分别表示。

本案例开发流程如下:

  1. 收集数据: 可以是文本数据、数据库数据、网络爬取的数据、自定义数据等等
  2. 数据预处理: 对采集数据进行格式化处理,文本数据的格式一致化,网络数据的分析抽取等,包括中文分词、停用词处理、词袋模型、构建词向量等。
  3. 分析数据: 检查词条确保解析的正确性,根据特征进行模型选择、特征抽取等。
  4. 训练算法: 从词向量计算概率
  5. 测试算法: 根据现实情况修改分类器
  6. 使用算法: 对社区留言板言论进行分类

收集数据

本案例我们采用自定义的数据集,我们选择6条社区评论,然后进行数据处理后以list形式存储在文档列表postingList中。其中每个词代表一个特征。将每条评论进行分类(即1代表侮辱性文字,0代表非侮辱文字)存在在类别列表classVec中。最后返回数据集和类标签。代码实现如下:

'''创建数据集:单词列表postingList, 所属类别classVec'''
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

代码分析:postingList列表存储6条评论信息,classVec列表存储每条信息类别(1代表侮辱性文字,0代表非侮辱文字)。最后返回文档列表和类别列表。

数据预处理

数据预处理包括对样本进行分词、词性筛选、停用词处理等,最后形成规范化干净的数据样本。由于本案例收集数据时默认进行了数据预处理,所以本节不在介绍(复旦新闻语料文本分类案例会详细介绍)。目前,我们采集的数据还是文本类型,计算机还不能直接处理,需要将文本数据转化成词向量进行处理。这里面需要获取特征的词汇集合(如果暂时不理解,先看看代码实现,下面会进行形式化描述)。其实现过程如下:

'''获取所有单词的集合:返回不含重复元素的单词列表'''
def createVocabList(dataSet):
    vocabSet = set([])
    for document in dataSet:
        vocabSet = vocabSet | set(document)  # 操作符 | 用于求两个集合的并集
    # print(vocabSet)
    return list(vocabSet)

代码分析:方法参数dataSet即加载数据集返回的文档列表。vocabSet是定义的不重复的数据集合。然后for循环对文档列表每条数据进行遍历处理,将不重复的词汇添加到vocabSet中,最终形成整个文档的词汇集,然后以list形式返回。

上面的方法已经获取了整个文档词汇集合,接着构建数据矩阵,代码实现如下:

'''词集模型构建数据矩阵'''
def setOfWords2Vec(vocabList, inputSet):
    # 创建一个和词汇表等长的向量,并将其元素都设置为0
    returnVec = [0] * len(vocabList)
    # 遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        else:
            print("单词: %s 不在词汇表之中!" % word)
    # print(returnVec)
    return returnVec

代码分析:本方法提供两个参数分别是整个训练文档词汇集(即全部训练文档6条评论不重复的单词集合),输入的数据列表。以整个词汇集等长的0向量。我们遍历输入数据列表,如果词特征在词汇集则标记1,不在词汇集保持为0.最后返回词向量矩阵。

与词集模型对应的,有个词袋模型。两者都是构建词向量,只是方式不一样,词袋模型也是推荐使用的词向量化方法,其实现如下:

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

分析数据

运行词集模型setOfWords2Vec(vocabList, dataSet[0])运行结果如下:

['dog', 'to', 'take', 'park', 'licks', 'has', 'help', 'stupid', 'him', 'so', 'not', 'love', 'buying', 'problems', 'cute', 'stop', 'steak', 'how', 'flea', 'maybe', 'food', 'I', 'please', 'dalmation', 'mr', 'posting', 'ate', 'garbage', 'worthless', 'my', 'is', 'quit']
[1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0]

结果分析:我们将dataSet[0]即第一条信息['my', 'dog', 'has', 'flea', 'problems', 'help', 'please']构建词集模型,词特征集为['dog', 'to', 'take', 'park', 'licks', 'has', 'help', 'stupid', 'him', 'so', 'not', 'love', 'buying', 'problems', 'cute', 'stop', 'steak', 'how', 'flea', 'maybe', 'food', 'I', 'please', 'dalmation', 'mr', 'posting', 'ate', 'garbage', 'worthless', 'my', 'is', 'quit']。结果显示[1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0]。即词特征集dog在dataSet[0]中,标记为1,to不在则保留原始的0.以此类推。我们也可以查看dataSet[1]等数据结果。

数据样本分析仅仅如上所述?当然不是,本例子中数据量比较小,容易分析。当数据量比较大,特征数以万计之时,人工分析就显得捉襟见肘了。我们可以采用图形化分析方法,根据具体业务需求,可以选择基于python自带的matplotlib可视化分析、或者其他图形可视化工具进行平面或多维数据分析,然后便于特征的选择。

如果是中文分词,我们还可以对词性进行分析,然后选择相应的词性特征,比如名词、动词、地名、人名、机构名等等,对虚词、助词等进行过滤,一方面达到数据降维另一方面防止模型训练拟合化等问题。

训练模型

现在已经知道了一个词是否出现在一篇文档中,也知道该文档所属的类别。接下来我们重写贝叶斯准则,将之前的 x, y 替换为 w. 粗体的 w 表示这是一个向量,即它由多个值组成。在这个例子中,数值个数与词汇表中的词个数相同。

我们使用上述公式,对每个类计算该值,然后比较这两个概率值的大小。根据上述公式可知,我们右边的式子等同于左边的式子,由于对于每个ci,P(w)是固定的。并且我们只需要比较左边式子值的大小来决策分类,那么我们就可以简化为通过比较右边分子值得大小来做决策分类。

首先可以通过类别 i (侮辱性留言或者非侮辱性留言)中的文档数除以总的文档数来计算概率 。接下来计算 ,这里就要用到朴素贝叶斯假设。如果将 w 展开为一个个独立特征,那么就可以将上述概率写作。这里假设所有词都互相独立,该假设也称作条件独立性假设(例如 A 和 B 两个人抛骰子,概率是互不影响的,也就是相互独立的,A 抛 2点的同时 B 抛 3 点的概率就是 1/6 * 1/6),它意味着可以使用来计算上述概率,这样就极大地简化了计算的过程。具体代码实现如下:

'''朴素贝叶斯分类器训练函数'''
def _trainNB0(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix) # 文件数
    numWords = len(trainMatrix[0]) # 单词数
    # 侮辱性文件的出现概率,即trainCategory中所有的1的个数,
    # 代表的就是多少个侮辱性文件,与文件的总数相除就得到了侮辱性文件的出现概率
    pAbusive = sum(trainCategory) / float(numTrainDocs)

    # 构造单词出现次数列表
    p0Num = zeros(numWords) # [0,0,0,.....]
    p1Num = zeros(numWords) # [0,0,0,.....]
    p0Denom = 0.0;p1Denom = 0.0 # 整个数据集单词出现总数
    for i in range(numTrainDocs):
        # 遍历所有的文件,如果是侮辱性文件,就计算此侮辱性文件中出现的侮辱性单词的个数
        if trainCategory[i] == 1:
            p1Num += trainMatrix[i] #[0,1,1,....]->[0,1,1,...]
            p1Denom += sum(trainMatrix[i])
        else:
            # 如果不是侮辱性文件,则计算非侮辱性文件中出现的侮辱性单词的个数
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    # 类别1,即侮辱性文档的[P(F1|C1),P(F2|C1),P(F3|C1),P(F4|C1),P(F5|C1)....]列表
    # 即 在1类别下,每个单词出现次数的占比
    p1Vect = p1Num / p1Denom# [1,2,3,5]/90->[1/90,...]
    # 类别0,即正常文档的[P(F1|C0),P(F2|C0),P(F3|C0),P(F4|C0),P(F5|C0)....]列表
    # 即 在0类别下,每个单词出现次数的占比
    p0Vect = p0Num / p0Denom
    return p0Vect, p1Vect, pAbusive

代码分析:本方法参数分别是文档特征向量矩阵和文档类别向量矩阵。首先计算侮辱性文档占总文档的概率,然后计算正常文档下特征词的概率向量和侮辱性特征词的向量,为了更好理解上面的代码我们看下运行p0V,p1V,pAb=_trainNB0(trainMatrix,Classlabels)结果:

词汇表集
['I', 'cute', 'help', 'dalmation', 'please', 'has', 'my', 'him', 'worthless', 'problems', 'so', 'mr', 'flea', 'love', 'take', 'stupid', 'dog', 'park', 'how', 'quit', 'buying', 'posting', 'steak', 'maybe', 'to', 'is', 'ate', 'not', 'garbage', 'food', 'stop', 'licks']
各条评论特征向量,其中1,3,5条为类别02,4,6条为类别1
[0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0]
[1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0]
[0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1]
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
类别0下特征词条件概率
[0.04166667 0.04166667 0.04166667 0.04166667 0.04166667 0.04166667
 0.125      0.08333333 0.         0.04166667 0.04166667 0.04166667
 0.04166667 0.04166667 0.         0.         0.04166667 0.
 0.04166667 0.         0.         0.         0.04166667 0.
 0.04166667 0.04166667 0.04166667 0.         0.         0.
 0.04166667 0.04166667] 
 类别1下特征词条件概率
[0.         0.         0.         0.         0.         0.
 0.         0.05263158 0.10526316 0.         0.         0.
 0.         0.         0.05263158 0.15789474 0.10526316 0.05263158
 0.         0.05263158 0.05263158 0.05263158 0.         0.05263158
 0.05263158 0.         0.         0.05263158 0.05263158 0.05263158
 0.05263158 0.        ] 
 0.5

结果分析:结合结果我们去理解上面的训练模型代码。首先最后一个是0.5代表侮辱性文档占全部文档的50%即一半,实际上我们标记3个正常评论词条,3个非正常的,这个显然正确。其次,第一次词I在类别1中出现0次,在类别0中出现1次。对应的条件概率分别是0.04166667和0

测试算法

在利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算 。如果其中一个概率值为 0,那么最后的乘积也为 0。为降低这种影响,可以将所有词的出现数初始化为 1,并将分母初始化为 2 (取1 或 2 的目的主要是为了保证分子和分母不为0,大家可以根据业务需求进行更改)。

另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积  时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。(用 Python 尝试相乘许多很小的数,最后四舍五入后会得到 0)。一种解决办法是对乘积取自然对数。在代数中有 ln(a * b) = ln(a) + ln(b), 于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。

下图给出了函数 f(x) 与 ln(f(x)) 的曲线。可以看出,它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。

根据朴素贝叶斯公式,我们观察分子进行条件概率连乘时候,由于有条件概率极小或者为0,最后导致结果为0 ,显然不符合我们预期结果,因此对训练模型进行优化,其优化代码如下:

'''训练数据优化版本'''
def trainNB0(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix) # 总文件数
    numWords = len(trainMatrix[0]) # 总单词数
    pAbusive = sum(trainCategory) / float(numTrainDocs) # 侮辱性文件的出现概率
    # 构造单词出现次数列表,p0Num 正常的统计,p1Num 侮辱的统计
    # 避免单词列表中的任何一个单词为0,而导致最后的乘积为0,所以将每个单词的出现次数初始化为 1
    p0Num = ones(numWords)#[0,0......]->[1,1,1,1,1.....],ones初始化1的矩阵
    p1Num = ones(numWords)

    # 整个数据集单词出现总数,2.0根据样本实际调查结果调整分母的值(2主要是避免分母为0,当然值可以调整)
    # p0Denom 正常的统计
    # p1Denom 侮辱的统计
    p0Denom = 2.0
    p1Denom = 2.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])
    # 类别1,即侮辱性文档的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表,取对数避免下溢出或浮点舍入出错
    p1Vect = log(p1Num / p1Denom)
    # 类别0,即正常文档的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表
    p0Vect = log(p0Num / p0Denom)
    return p0Vect, p1Vect, pAbusive

我们再看生成条件概率结果如下:

[-2.56494936 -2.15948425 -3.25809654 -2.56494936 -3.25809654 -3.25809654
 -2.56494936 -2.56494936 -3.25809654 -2.56494936 -3.25809654 -3.25809654
 -2.56494936 -2.56494936 -2.56494936 -3.25809654 -2.56494936 -2.56494936
 -2.56494936 -2.56494936 -1.87180218 -2.56494936 -3.25809654 -2.56494936
 -2.56494936 -2.56494936 -2.56494936 -3.25809654 -3.25809654 -2.56494936
 -3.25809654 -2.56494936] 
 [-3.04452244 -2.35137526 -2.35137526 -3.04452244 -2.35137526 -2.35137526
 -3.04452244 -3.04452244 -1.94591015 -2.35137526 -2.35137526 -2.35137526
 -3.04452244 -2.35137526 -3.04452244 -1.65822808 -1.94591015 -3.04452244
 -3.04452244 -3.04452244 -3.04452244 -3.04452244 -2.35137526 -3.04452244
 -3.04452244 -3.04452244 -3.04452244 -2.35137526 -2.35137526 -3.04452244
 -2.35137526 -3.04452244] 
 0.5

使用算法对社区留言板言论进行分类

构建朴素贝叶斯分类函数

将乘法转换为加法
乘法:

加法:

'''
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    # 计算公式  log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C))
    # 使用 NumPy 数组来计算两个向量相乘的结果,这里的相乘是指对应元素相乘,即先将两个向量中的第一个元素相乘,然后将第2个元素相乘,以此类推。这里的 vec2Classify * p1Vec 的意思就是将每个词与其对应的概率相关联起来
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)
    p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else:
        return 0

测试朴素贝叶斯算法

结合上面分析流程和实现方法,我们综合测试朴素贝叶斯对评论信息分类如下:

'''朴素贝叶斯算法屏蔽社区留言板的侮辱性言论的应用'''
def testingNB():
    # 1. 加载数据集
    dataSet, Classlabels = loadDataSet()
    # 2. 创建单词集合
    myVocabList = createVocabList(dataSet)
    # 3. 计算单词是否出现并创建数据矩阵
    trainMat = []
    for postinDoc in dataSet:
        # 返回m*len(myVocabList)的矩阵, 记录的都是0,1信息
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    # print('test',len(array(trainMat)[0]))
    # 4. 训练数据
    p0V, p1V, pAb = trainNB0(array(trainMat), array(Classlabels))
    # 5. 测试数据
    testEntry = ['love', 'my', 'dalmation']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    print(testEntry, '分类结果是: ', classifyNB(thisDoc, p0V, p1V, pAb))
    testEntry = ['stupid', 'garbage']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    print(testEntry, '分类结果是: ', classifyNB(thisDoc, p0V, p1V, pAb))

运行结果如下:

['love', 'my', 'dalmation'] 分类结果是:  0
['stupid', 'garbage'] 分类结果是:  1

案例场景2: 对社区留言板言论进行分类

项目概述

我们运行朴素贝叶斯分类进行电子邮件垃圾过滤。在文档分类中,整个文档(如一封电子邮件)是实例,而电子邮件中的某些元素则构成特征。我们可以观察文档中出现的词,并把每个词作为一个特征,而每个词的出现或者不出现作为该特征的值,这样得到的特征数目就会跟词汇表中的词的数目一样多。

本项目开发流程如下:

收集数据: 提供文本文件
准备数据: 将文本文件解析成词条向量
分析数据: 检查词条确保解析的正确性
训练算法: 使用我们之前建立的 trainNB() 函数
测试算法: 使用朴素贝叶斯进行交叉验证
使用算法: 构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上

收集数据并预处理

邮件格式内容如下:

对邮件进行读取实现如下:

'''读取文本'''
def testParseTest():
    print(textParse(open('./email/ham/1.txt').read()))

准备数据

对读取的文本进行词条向量化,其实现如下:

'''接收一个大字符串并将其解析为字符串列表'''
def textParse(bigString):
    import re
    # 使用正则表达式来切分句子,其中分隔符是除单词、数字外的任意字符串
    listOfTokens = re.split(r'\W*', bigString)
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]

分析数据

这个部分在案例场景1进行了详细描述,此处不在赘述。

训练算法

此处,我们去调用在场景1优化过的朴素贝叶斯训练模型trainNB0() 函数,这里也是一劳永逸的方法。还可以对数据预处理等进行封装。

测试算法

本测试方法中使用的数据集,即文档可以参见下文代码下载。采用方法跟场景1基本类似,这里不作代码解析。具体实现代码如下:

'''对贝叶斯垃圾邮件分类器进行自动化处理。'''
def spamTest():
    docList = [];classList = [];fullText = [] # 文档列表、类别列表、文本特征
    for i in range(1, 26): # 总共25个文档
        # 切分,解析数据,并归类为 1 类别
        wordList = textParse(open('./email/spam/%d.txt' % i).read())
        docList.append(wordList)
        classList.append(1)
        # 切分,解析数据,并归类为 0 类别
        wordList = textParse(open('./email/ham/%d.txt' % i,encoding='UTF-8').read())
        docList.append(wordList)
        classList.append(0)
        fullText.extend(wordList)
    # 创建词汇表
    vocabList = createVocabList(docList)
    trainingSet = list(range(50)) # 词汇表文档索引
    testSet = []
    # 随机取 10 个邮件用来测试
    for i in range(10):
        # random.uniform(x, y) 随机生成一个范围为 x - y 的实数
        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
    print('the errorCount is: ', errorCount)
    print('the testSet length is :', len(testSet))
    print('the error rate is :', float(errorCount)/len(testSet))

运行结果如下:

the errorCount is:  2
the testSet length is : 10
the error rate is : 0.2

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

项目概述

广告商往往想知道关于一个人的一些特定人口统计信息,以便能更好地定向推销广告。我们将分别从美国的两个城市中选取一些人,通过分析这些人发布的信息,来比较这两个城市的人们在广告用词上是否不同。如果结论确实不同,那么他们各自常用的词是哪些,从人们的用词当中,我们能否对不同城市的人所关心的内容有所了解。

开发流程如下:

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

收集数据

从 RSS 源收集内容,这里需要对 RSS 源构建一个接口,也就是导入 RSS 源,我们使用 python 下载文本,在http://code.google.com/p/feedparser/ 下浏览相关文档,安装 feedparse,首先解压下载的包,并将当前目录切换到解压文件所在的文件夹,然后在 python 提示符下输入:python setup.py install

准备数据

文档词袋模型

我们将每个词的出现与否作为一个特征,这可以被描述为 词集模型(set-of-words model)。如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为 词袋模型(bag-of-words model)。在词袋中,每个单词可以出现多次,而在词集中,每个词只能出现一次。为适应词袋模型,需要对函数 setOfWords2Vec() 稍加修改,修改后的函数为 bagOfWords2Vec() 。

如下给出了基于词袋模型的朴素贝叶斯代码。它与函数 setOfWords2Vec() 几乎完全相同,唯一不同的是每当遇到一个单词时,它会增加词向量中的对应值,而不只是将对应的数值设为 1 。

这部分在场景1中已经构建完成,并进行了阐述。

分析数据

这个部分在案例场景1进行了详细描述,此处不在赘述。

训练算法

此处,我们去调用在场景1优化过的朴素贝叶斯训练模型trainNB0() 函数,这里也是一劳永逸的方法。还可以对数据预处理等进行封装。

测试算法

观察错误率,确保分类器可用。可以修改切分程序,以降低错误率,提高分类结果。其具体实现如下:

'''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]   #返回出现次数最高的30个单词



def localWords(feed1,feed0):
    # import feedparser # feedparser是python中最常用的RSS程序库
    docList=[];classList=[];fullText=[]
    minLen=min(len(feed1['entries']),len(feed0['entries'])) # entries内容无法抓取,网站涉及反爬虫技术
    print(len(feed1['entries']),len(feed0['entries']))
    for i in range(minLen):
        wordList=textParse(feed1['entries'][i]['summary'])   #每次访问一条RSS源
        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])    #去掉出现次数最高的那些词
    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(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

运行结果:

ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
sf = feedparser.parse('http://sfbay.craigslist.org/stp/index.rss')
# print(ny)
vocabList,pSF,pNY=localWords(ny,sf)

由于如上两个地址抓取,得到feed0['entries']为空,所以没有进行结果分析,读者可以试用其他rss地址进行处理。如下是采用之前网站反爬虫抓取前的分析结果:

vocabList,pSF,pNY=bayes.localWords(ny,sf)
the error rate is: 0.2
vocabList,pSF,pNY=bayes.localWords(ny,sf)
the error rate is: 0.3
vocabList,pSF,pNY=bayes.localWords(ny,sf)
the error rate is: 0.55

参考文献

  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

作者 白 宁超

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

发表回复

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