朴素贝叶斯实战(Python屏蔽留言板的侮辱性言论)

项目概述

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

准备数据

准备数据的方法比较简单,只是自己构造的训练数据集而已,嵌套列表里的每个列表表示已经分好词的句子样本。可以看到,为简单起见,每个句子里的词都不重复。

def loadDataSet():
    """
    创建数据集
    :return: 文档列表postingList,所属类别classVec
    """
    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

创建词表

将我们刚刚自创的数据样本集里出现的所有单词提取出来,创建一个不重复的单词词表,创建词表的目的是为了接下来对每个样本进行词向量化作准备,其词向量里每个特征出现的位置要与词表里各词的位置相对应。

def createVocabList(dataSet):
    """
    获取文档列表中出现的所有单词集合
    :param dataSet: 数据集文档列表[[], [], [],...]
    :return: 所有单词的列表(去重)
    """
    vocabSet = set([])  # 创建一个空set集合
    for document in dataSet:
        vocabSet = vocabSet | set(document)

    return list(vocabSet)

构建词向量

前面的样本是以字符列表的形式,现在要将它们向量化,对每个文章样本构建词向量。在构建中,对于每个文章里重复出现的词语(但本实例中无此现象),我们选用伯努利模型,即每个样本的词向量长度都与词表长度一致,并且一个单词重复出现我们只认为它出现了一次,如果某个单词出现了,则基于词表里该词的索引位置,在词向量的相同位置下赋特征值为1,否则为0

def setOfWords2Vec(vocabList, inputSet):
    """
    为文档列表中每个文档构建文档词向量
    遍历查看每个单词是否出现,出现则将其向量位,置1
    :param vocabList: 所有单词集合列表
    :param inputSet: 一篇训练文档数据集['','','']
    :return: 文档词向量列表
    """

    # 创建一个和词汇表等长的向量,并将其元素都置为0,即默认每个单词都没有在词汇表中出现
    returnVec = [0] * len(vocabList) # [0,0,0,,...]
    # 遍历这篇文档中的所有单词,如果单词在词汇表中,则将输出的文档词向量中的对应值置为1
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        else:
            print("thw word: ", word, " is not in my vocabList!")
    return returnVec

训练朴素贝叶斯模型

利用已向量化的文章词向量矩阵和对应类别标签,进行贝叶斯模型的训练。其目的是计算出每个类别下每个单词的条件概率:p{w1|c0},…,p{wi|c0},p{w1|c1},…,p{wi|c1}以及每个类别文档出现的概率:p{c0} p{c1}

首先侮辱性类别的概率p(c1)即先验概率比较好求,就是:
P ( “ 侮 辱 性 类 ” ) = 侮 辱 性 文 章 的 个 数 总 文 章 个 数 P(“侮辱性类”)=\frac{侮辱性文章的个数}{总文章个数} P(=

接下来计算各单词的条件概率:
定义两个矩阵p0Num和p1Num,分别表示侮辱性文章/正常文章中各单词出现的次数,其长度与词表长度一致,假设词表长度为n,xi表示特征词,ci表示该特征词出现次数

词 表 v o c a b L i s t = [ x 1 x 2 x 3 x 4 . . . x n ] 词表vocabList = \begin{bmatrix} x_1 &x _2& x_3 & x_4 & ... &x_n\\ \end{bmatrix} vocabList=[x1x2x3x4...xn]
p 0 N u m = [ c 1 c 2 c 3 c 4 . . . c n ] p0Num = \begin{bmatrix} c_1 &c _2& c_3 & c_4 & ... &c_n\\ \end{bmatrix} p0Num=[c1c2c3c4...cn]
p 1 N u m = [ c 1 c 2 c 3 c 4 . . . c n ] p1Num = \begin{bmatrix} c_1 &c _2& c_3 & c_4 & ... &c_n\\ \end{bmatrix} p1Num=[c1c2c3c4...cn]

其中p0Num和p1Num的值都初始化为1,这是为了防止这样的情形:在训练时,某个词若没有在某个类的文章里出现,其计算条件概率的分子就为0,那么其条件概率p(xi|c)=0,由于要取对数log{p(xi|c)}作为最终的条件概率值,那对0取对数就会造成异常。因此,对所有单词的出现次数都取一个初始值1,也是运用了平滑技术,相应地对每个类别文章里出现的单词总数要初始化为2。

运用平滑技术的另一个用处是在预测一个新文章里时,出现了在训练样本的词表里从未出现过的词,此时就利用平滑处理计算一个默认概率值,我们在后文里再细说。

接下来就是遍历样本词向量矩阵,如果是侮辱性样本或正常样本,就将其词向量trainMatrix[i]累加到p1Num或p0Num矩阵里,同时对该词向量里的特征值进行累加。因为我们选的模型是伯努利模型,词向量里各维度非0即1,运用平滑处理在计算单词条件概率时的公式如下:
P ( “ s t u p i d ” ∣ 侮 辱 性 类 ) = 出 现 “ s t u p i d ” 的 侮 辱 性 文 章 个 数 + 1 每 个 侮 辱 性 文 章 中 所 有 词 出 现 次 数 ( 出 现 了 只 计 算 一 次 ) 的 总 和 + 2 P(“stupid”|侮辱性类)=\frac{出现“stupid”的侮辱性文章个数+1}{每个侮辱性文章中所有词出现次数(出现了只计算一次)的总和+2} P(stupid=+2stupid+1

p1Num或p0Num保存的即是各单词对应上述公式的分子,在各类别下,向量的累加求和,其各维度下特征值对应的即是某个类别文章里各单词出现的次数,也就是出现某个单词的该类别文章个数
在这里插入图片描述
这样条件概率公式里的分子就算出了。

而对每个词向量的特征值求和,即表示该类别文章里出现的单词个数(不重复),从而求得侮辱类/正常文章里的总词数p1Demo和p0Demo,就算出了公式里的分母。

最后分子矩阵和分母矩阵相除,再求对数,即得出训练样本里每个词的对应各类别的条件概率p{wi|c}

def trainNB(trainMatrix, trainCategory):
    """
    一篇文档用w词向量表示,w=(w1, w2, w3, ...wn)
    利用训练文档数据列表学习出每个类别下每个单词的条件概率:p{w1|c0},...,p{wi|c0},p{w1|c1},...,p{wi|c1}
    以及每个类别文档出现的概率:p{c0} p{c1}
    :param trainMatrix: 文档词向量矩阵 [[1 0 0 1..], [1 1 0 0..], [0 1 0 1..], ...]
    :param trainCategory: 文档对应的类别列表[1 0 1 0...],其长度等于文档词向量矩阵的长度,1表示侮辱性,0表示正常
    :return: 正常类别下出现各词的条件概率矩阵p0Vect,侮辱性类别下出现各词的条件概率矩阵p1Vect,侮辱性类别的概率pAbusive
    """
    # 文档数
    numTrainDocs = len(trainMatrix)
    # 词向量里包含的单词数
    numWords = len(trainMatrix[0])

    # 侮辱性文档出现的概率,即p{c1},等于所有侮辱性文档的个数 / 总文档数
    pAbusive = sum(trainCategory) / float(numTrainDocs)

    # 构造在每个类别文档中,各单词出现次数数组,用来计算p{wi|c0}=Count(wi,c0) / Count(c0) = c0类出现wi词的文档数 / c0类文档数
    #                                                                            or = c0类中wi词的出现次数 / c0类单词数
    p0Num = np.ones(numWords) # 初始化为1是避免p1Num / p1Demo有分量为0,从而导致log(p1Num / p1Demo)异常
    p1Num = np.ones(numWords)

    # 各类别文档下出现的单词总数
    p0Demo = 2.0
    p1Demo = 2.0

    for i in range(numTrainDocs):
        # 如果是侮辱性文档
        if trainCategory[i] == 1:
            # 将其对应的词向量累加到p1Num
            p1Num += trainMatrix[i]
            # 累加侮辱性文档中出现的单词总数
            p1Demo += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]
            p0Demo += sum(trainMatrix[i])

    # 计算各类别下出现wi词的条件概率p{wi|c0},p{wi|c1}数组
    p1Vect = np.log(p1Num / p1Demo)  # array[ln(p{w1|c1}), ln(p{w2|c1}), ln(p{w3|c1}), ...]
    p0Vect = np.log(p0Num / p0Demo)  # array[ln(p{w1|c0}), ln(p{w2|c0}), ln(p{w3|c0}), ...]

    return p0Vect, p1Vect, pAbusive

预测分类

前面我们已从训练样本出计算出了朴素贝叶斯分类模型,即各类别下出现各单词的条件概率矩阵,和出现各类别的先验概率值

下面就是对传入的一个待测文章词向量,利用已算好的条件概率和先验概率,判断其所属类别,其最终取对数后的预测条件概率的公式如下,以侮辱性类别为例,X表示词向量,xi表示特征单词:
l o g P ( 侮 辱 性 ∣ X ) = l o g P ( x 1 ∣ 侮 辱 性 ) + l o g P ( x 2 ∣ 侮 辱 性 ) + l o g P ( x 3 ∣ 侮 辱 性 ) + . . . + l o g P ( “ x n ” ∣ 侮 辱 性 ) + l o g P ( 侮 辱 性 ) log{P(侮辱性|X)} = log{P(x_1|侮辱性)}+log{P(x_2|侮辱性)}+log{P(x_3|侮辱性)}+...+log{P(“x_n”|侮辱性)}+log{P(侮辱性)} logP(X)=logP(x1)+logP(x2)+logP(x3)+...+logP(xn)+logP()

其中每个 l o g P ( x i ∣ 侮 辱 性 ) log{P(x_i|侮辱性)} logP(xi)已计算在矩阵p1Vec里

预测时,若新文章内的词出现在训练样本的词表里,则直接取出该词对应的条件概率值。

若没有,则取默认概率值(这里就是平滑处理算出一个从未出现过的单词的概率值),还是基于训练样本数据集来算默认值,对于伯努利模型,其平滑处理新词的默认概率计算公式为,其中xi表示一个新单词:
P ( x i ∣ 侮 辱 类 ) = 出 现 x i 的 侮 辱 性 文 章 数 + 1 每 篇 侮 辱 性 文 章 中 所 有 词 出 现 次 数 ( 出 现 了 只 计 算 一 次 ) 的 总 和 + 2 = 1 每 篇 侮 辱 性 文 章 中 所 有 词 出 现 次 数 ( 出 现 了 只 计 算 一 次 ) 的 总 和 + 2 P(x_i|侮辱类)=\frac{出现x_i的侮辱性文章数+1}{每篇侮辱性文章中所有词出现次数(出现了只计算一次)的总和+2}\\=\frac{1}{每篇侮辱性文章中所有词出现次数(出现了只计算一次)的总和+2} P(xi=+2xi+1=+21

但在本例中,我们直接将待测词向量与类别条件概率矩阵相乘了(默认待测文章里的词都是在训练集词表里出现过的),这样的话如果某个词对应的特征值为0即没出现该词,相乘后也不会参与到累加运算中,最后加上各类别先验概率,得到最终预测值

def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    """
    给出待测文档词向量,利用训练好的各类别下出现各单词的条件概率及类别概率,计算该待测文档属于各类别的概率值,将其分类
    :param vec2Classify: 待测文档词向量,即[0,1,1,0,1...]
    :param p0Vec: 已训练好的类别0下出现各单词的条件概率,即array[ln(p{w1|c0}), ln(p{w2|c0}), ln(p{w3|c0}), ...]
    :param p1Vec: 已训练好的类别1下出现各单词的条件概率,即array[ln(p{w1|c1}), ln(p{w2|c1}), ln(p{w3|c1}), ...]
    :param pClass1: 已训练好的类别1文档出现的概率
    :return: 类别1 or 0
    """
    # 这里的向量相乘表示将词向量里每个词与其对应的概率相关联,比如词向量某个词的值为0,那么相乘后该词的概率值为0,
    # 若某个词的值为1,那么相乘后该词的概率值为所训练出的条件概率值p{w|c},最后累加和(取对数的作用)
    p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
    p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
    if p0 > p1:
        return 0
    else:
        return 1

测试朴素贝叶斯

写好上述各模块后,让我们测试一下简单实现的朴素贝叶斯

def testingNB():
    """
    测试朴素贝叶斯算法
    :return:
    """

    # 1 加载文档数据集,获取所有文档对象和标签列表
    postingList, classVec = loadDataSet()
    # 2 创建文档中出现的所有单词列表
    vocabList = createVocabList(postingList)
    # 3 创建文档词向量矩阵(数组)
    trainMat = []
    for document in postingList:
        trainMat.append(setOfWords2Vec(vocabList, document))
    # 4 训练数据,获取类别概率及类别下单词条件概率
    p0V, p1V, pAb = trainNB(np.array(trainMat), classVec)
    # 5 测试数据
    testEntry = ['love', 'my', 'dalmation']
    thisDoc = np.array(setOfWords2Vec(vocabList, testEntry))
    print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
    testEntry = ['stupid', 'garbage']
    thisDoc = np.array(setOfWords2Vec(vocabList, testEntry))
    print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))


if __name__ == "__main__":
    testingNB()

结果:
在这里插入图片描述

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值