我的第一篇学习笔记——使用朴素贝叶斯对文档分类

朴素贝叶斯算法可以实现对文档的分类,其中最著名的应用之一就是过滤垃圾邮件。先做一个简单的分类,以论坛的留言为例,构建一个快速的过滤器,来区分哪些留言是负面言论,哪些是正面言论。

我对算法思路的理解:首先计算训练集中每个词语分别在正面(负面)文档中出现的概率以及正面(负面)文档的概率,再计算待分类样本中的每个词语属于正面(负面)文档的概率和正面(负面)文档概率的乘积,即为该样本属于正面(负面)样本的概率,样本属于哪一类文档的概率较大就归为哪类文档(读着有点绕),下面详细介绍分类的过程。

1. 条件概率

首先来学习一下基于条件概率的分类思想。对于样本A,它属于类别c_1的概率为P(c_1 |A),属于样本c_2的概率为P(c_2 |A),定义贝叶斯分类准则为:

  • 如果P(c_1 |A)>P(c_2 |A),那么样本A属于类别c_1
  • 如果P(c_1 |A)<P(c_2 |A),那么样本A属于类别c_2

完整的贝叶斯公式如下:

P(B_i |A)=P(A|B_i )P(B_i )/(\sum_{j=1}^{n}P(A|B_j )P(B_j ) ) \: \:\: \: \, \, \, \, i=1,2,...,n

在此分类算法中,我们用它的简化形式:

P(c_1 |A)=P(A|c_1 )P(c_1 )/P(A)

用分类的思想可以这样理解这个公式:A是待分类样本的特征集合,那么要求得A属于类别c_1的概率,就转化为求训练集中,类别c_1的样本集中特征集A的概率、类别c_1的概率、特征集A的概率,这3个值通过训练集数据可以更容易计算得出。

2. 准备数据——构建词袋模型

对每个文本,构建一个向量,向量的维度与词条列表的维度相同,向量的值是词条列表中每个词条在该文本中出现的次数,这种模型叫做词袋模型。使用Python编程实现,代码来源《机器学习实践》,创建bayes.py文件,加入如下代码:

# 加载数据集
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


# 创建不重复的词条列表
def createVocabList(dataSet):
    vocabSet = set([])
    for document in dataSet:
        vocabSet = vocabSet | set(document)
    return list(vocabSet)


# 朴素贝叶斯词袋模型
def bagOfWords2Vec(vocabList, inputSet):
    returnVec = [0] * len(vocabList)
    for word in inputSet:
        if word in vocabList:
            return Vec[vocabList.index(word)] += 1
        else:
            print("the word: %s is not in my Vocabulary!" % word)

    return returnVec

看一下执行效果:

>>> import bayes
>>> listOPosts, listClasses = bayes.loadDataSet()
>>> myVocabList = bayes.createVocabList(listOPosts)
>>> myVocabList
['steak', 'dalmation', 'garbage', 'love', 'cute', 'please', 'help', 'stop', 'buying', 'worthless', 
'ate', 'dog', 'to', 'I', 'food', 'is', 'how', 'not', 'mr', 'quit', 'flea', 'licks', 'problems', 
'take', 'so', 'park', 'has', 'my', 'posting', 'stupid', 'maybe', 'him']
>>> bayes.bagOfWords2Vec(myVocabList, listOPosts[0])
[0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0]
>>> bayes.bagOfWords2Vec(myVocabList, listOPosts[1])
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1]

词条列表中索引为5的词语是please,它在第一个文档中出现过1次,最后一个词语是him,它在第二个文档中出现过1次。

3. 训练算法——从词向量计算概率

现在我们知道一个词是否出现在一篇文档中,也知道文档所属的类别。为了方便理解,把条件概率公式中的A替换为\boldsymbol{w}(代表词向量)

P(c_i |\boldsymbol{w})=P(\boldsymbol{w}|c_i )P(c_i )/P(\boldsymbol{w})

\boldsymbol{w}=[w_0,w_1,w_2,...,w_n]

之所以称为“朴素”贝叶斯,是因为假设样本中的所有特征是相互独立的,那么就有如下简化的计算方法:

P(\boldsymbol{w}|c_i )=P(w_0 |c_i )P(w_1 |c_i )P(w_2 |c_i )...P(w_n |c_i )

其中,P(w_k |c_i )表示在第i类样本中,第k个词语出现的概率;

P(c_{i})表示第i类样本所占的概率,如示例中有6个样本,正负样本各3个,因此P(c_0 )P(c_1 )均为0.5;

P(\boldsymbol{w})同样可以按P(\boldsymbol{w}|c_i )的方法求得,但是因为我们只需比较P(c_0 |\boldsymbol{w})P(c_1 |\boldsymbol{w})的大小来进行分类,作为相同的分母,P(\boldsymbol{w})在代码中没有计算。

因为要使用数值计算类库Numpy的一些函数,需要在文件的最上面添加引用语句:from numpy import *

# 朴素贝叶斯分类器训练函数,参数为样本矩阵和类别矩阵
def trainNB0(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix)  # 样本数
    numWords = len(trainMatrix[0])  # 词条数
    pAbusive = sum(trainCategory) / float(numTrainDocs)  # 计算类别为1 的概率
    p0Num = zeros(numWords)
    p1Num = zeros(numWords)
    p0Denom = 0.0
    p1Denom = 0.0
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:  # 类别为1 的文档
            p1Num += trainMatrix[i]  # 类别为1 的词条矩阵
            p1Denom += sum(trainMatrix[i])  # 类别为1 的文档的总词数
        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    p1Vect = p1Num / p1Denom  # 类别为1 的文档中,每个词出现的概率
    p0Vect = p0Num / p0Denom
    return p0Vect, p1Vect, pAbusive

测试一下效果:

>>> import bayes
>>> listOPosts, listClasses = bayes.loadDataSet()
>>> myVocabList = bayes.createVocabList(listOPosts)
>>> trainMat = []
>>> for postinDoc in listOPosts:
...     trainMat.append(bayes.bagOfWords2Vec(myVocabList, postinDoc))
...     
>>> p0V, p1V, pAb = bayes.trainNB0(trainMat, listClasses)
>>> p0V
array([0.04166667, 0.04166667, 0.        , 0.04166667, 0.04166667,
       0.04166667, 0.04166667, 0.04166667, 0.        , 0.        ,
       0.04166667, 0.04166667, 0.04166667, 0.04166667, 0.        ,
       0.04166667, 0.04166667, 0.        , 0.04166667, 0.        ,
       0.04166667, 0.04166667, 0.04166667, 0.        , 0.04166667,
       0.        , 0.04166667, 0.125     , 0.        , 0.        ,
       0.        , 0.08333333])
>>> p1V
array([0.        , 0.        , 0.05263158, 0.        , 0.        ,
       0.        , 0.        , 0.05263158, 0.05263158, 0.10526316,
       0.        , 0.10526316, 0.05263158, 0.        , 0.05263158,
       0.        , 0.        , 0.05263158, 0.        , 0.05263158,
       0.        , 0.        , 0.        , 0.05263158, 0.        ,
       0.05263158, 0.        , 0.        , 0.05263158, 0.15789474,
       0.05263158, 0.05263158])
>>> pAb
0.5

负面样本的概率pAb为0.5,与前面计算的相同。正面样本中共有23个词语,负面样本中共有19个词语,词条列表中的第一个词为steak,它在正面样本中出现一次,在负面样本中没有出现,该词的条件概率为分别为0.04166667和0,计算正确;倒数第3个词语是stupid,它在正面样本中没有出现,在负面样本中出现3次,该词的条件概率分别是0和0.15789474,计算正确。

至此,我们已经求得每个词语在某类文档中的条件概率:

P(w_i |c_0 )=p0V[i]P(w_i |c_1 )=p1V[i]

P(c_0 )=1- pAbP(c_1 )= pAb

在分类之前,有两个地方需要优化:

(1)根据公式P(\boldsymbol{w}|c_i )=P(w_0 |c_i )P(w_1 |c_i )P(w_2 |c_i )...P(w_n |c_i ),如果其中有一个词语的条件概率为0,那么计算结果就是0,为了避免这种情况,将所有词出现的次数初始化为1,分母初始化为2。

(2) 多个概率非常小的值相乘,最后四舍五入可能会得到0,可以采用求对数的方法来避免这种现象。

修改trainNB0()方法的第6到9行,和return前的2行,修改后的代码:

# 朴素贝叶斯分类器训练函数,参数为样本矩阵和类别矩阵
def trainNB0(trainMatrix, trainCategory):
    numTrainDocs = len(trainMatrix)  # 样本数
    numWords = len(trainMatrix[0])  # 词条数
    pAbusive = sum(trainCategory) / float(numTrainDocs)  # 计算类别为1 的概率
    p0Num = ones(numWords)
    p1Num = ones(numWords)
    p0Denom = 2.0
    p1Denom = 2.0
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:  # 类别为1 的文档
            p1Num += trainMatrix[i]  # 类别为1 的词条矩阵
            p1Denom += sum(trainMatrix[i])  # 类别为1 的文档的总词数
        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    p1Vect = log(p1Num / p1Denom)  # 类别为1 的文档中,每个词出现的概率
    p0Vect = log(p0Num / p0Denom)
    return p0Vect, p1Vect, pAbusive

4. 测试算法——利用分类器对样本分类

有了前面的准备工作,现在可以构建分类器了,分类器classifyNB有4个参数,依次是待分类的向量,以及trainNB0计算的3个概率值。再写一个分类的测试函数testingNB,避免每次测试都要输入测试代码。

# 朴素贝叶斯分类函数
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)
    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(trainMat, 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))

看看实际效果,很明显‘love’一类的文本被归为正面言论,‘stupid’、‘garbage’被归为负面言论。

>>> import bayes
>>> bayes.testingNB()
['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1

以上就是我对朴素贝叶斯算法的学习和理解,理论和代码主要来自《机器学习实践》,并尝试用更容易理解的表述和公式加以说明,不足之处欢迎探讨。

猜你喜欢

转载自blog.csdn.net/leaf_zizi/article/details/82143882
今日推荐