“机器学习实战”刻意练习——分类问题:朴素贝叶斯

参考:
Python3《机器学习实战》学习笔记(四):朴素贝叶斯基础篇之言论过滤器 - Jack-Cui - CSDN博客
Python3《机器学习实战》学习笔记(五):朴素贝叶斯实战篇之新浪新闻分类 - Jack-Cui - CSDN博客

一、贝叶斯决策理论

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

贝叶斯决策理论的核心思想,即选择具有最高概率的决策

贝叶斯准则告诉我们如何交换条件概率中
的条件与结果,即如果已知P(x|c),要求P(c|x),那么可以使用下面的计算方法:
P ( c ∣ x ) = P ( x ∣ c ) P ( c ) P ( x ) P(c|x)=\frac{P(x|c)P(c)}{P(x)} P(cx)=P(x)P(xc)P(c)

则同理可得,若给定某个由x、y表示的数据点,那么该数据点来自类别c1的概率可通过如下公式计算:
P ( c i ∣ x , y ) = P ( x , y ∣ c i ) P ( c i ) P ( x , y ) P(c_{i}|x,y)=\frac{P(x,y|c_{i})P(c_{i})}{P(x,y)} P(cix,y)=P(x,y)P(x,yci)P(ci)

由此,定义贝叶斯分类准则为:
如果P(c1|x, y) > P(c2|x, y),那么属于类别c1。
如果P(c1|x, y) < P(c2|x, y),那么属于类别c2。

二、朴素贝叶斯算法简介

我们称之为“朴素”,是因为整个形式化过程只做最原始、最简单的假设。

优缺点:

  • 优点:在数据较少的情况下仍然有效,可以处理多类别问题。
  • 缺点:对于输入数据的准备方式较为敏感;由于朴素贝叶斯的**“朴素”特点**,所以会带来一些准确率上的损失。

适用数据类型:

  • 标称型数据

朴素贝叶斯的一般过程:

  1. 收集数据:可以使用任何方法。本章使用RSS源。
  2. 准备数据:需要数值型或者布尔型数据。
  3. 分析数据:有大量特征时,绘制特征作用不大,此时使用直方图效果更好。
  4. 训练算法:计算不同的独立特征的条件概率。
  5. 测试算法:计算错误率。
  6. 使用算法:一个常见的朴素贝叶斯应用是文档分类。可以在任意的分类场景中使用朴素贝叶斯分类器,不一定非要是文本。

三、代码实现(python3)

使用 Python 进行文本分类:
我们可以观察文档中出现的词,并把每个词的出现或者不出现作为一个特征,这样得到的特征数目就会跟词汇表中的词目一样多。

如果特征之间相互独立,那么样本数就可以从N1000减少到1000×N。
所谓独立(independence)指的是统计意义上的独立,即一个特征或者单词出现的可能性与它和其他单词相邻没有关系

1.准备数据:从文本中构建词向量

要从文本中获取特征,需要先拆分文本。这里的特征是来自文本的词条(token),一个词条是字符的任意组合。
可以把词条想象为单词,也可以使用非单词词条,如URL、IP地址或者任意其他字符串。
然后将每一个文本片段表示为一个词条向量,其中值为1表示词条出现在文档中,0表示词条未出现
考虑出现在所有文档中的所有单词,再决定将哪些词纳入词汇表或者说所要的词汇集合,然后必须要将每一篇文档转换为词汇表上的向量。

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']]
    #类别标签向量(0为侮辱性,1为非侮辱性)
    classVec = [0,1,0,1,0,1]                                                                   #类别标签向量,1代表侮辱性词汇,0代表不是
    return postingList,classVec

def createVocabList(dataSet):
    """
    将切分的实验样本词条整理成不重复的词条列表
    - - - -
    dataSet - 样本数据集
    """
    #创建一个空集
    vocabSet = set([])
    for document in dataSet: 
        #取并集              
        vocabSet = vocabSet | set(document) 
    return list(vocabSet)

def setOfWords2Vec(vocabList, inputSet):
    """
    根据词汇表,将inputSet向量化(变为0和1组成的向量)
    - - - -
    vocabList - 词汇表
    inputSet - 切分的词条列表
    """
    returnVec = [0] * len(vocabList)
    for word in inputSet:    
        #如果词条存在于词汇表中,则令对应位置为1
        if word in vocabList: 
            returnVec[vocabList.index(word)] = 1
        else: print("the word: %s is not in my Vocabulary!" % word)
    return returnVec  

if __name__ == '__main__':
    postingList, classVec = loadDataSet()
    print('postingList:\n',postingList)
    myVocabList = createVocabList(postingList)
    print('myVocabList:\n',myVocabList)
    trainMat = []
    for postinDoc in postingList:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    print('trainMat:\n', trainMat)

结果:
“机器学习实战”刻意练习——分类问题:朴素贝叶斯_第1张图片

2.训练算法:从词向量计算概率

我们再改进一下贝叶斯准则的公式,将之前的x、y 替换为表示向量的w:
P ( c i ∣ w ) = P ( w ∣ c i ) P ( c i ) P ( w ) P(c_{i}|w)=\frac{P(w|c_{i})P(c_{i})}{P(w)} P(ciw)=P(w)P(wci)P(ci)
如果将w展开为一个个独立特征,那么就可以将上述概率写作 p ( w 0 , w 1 , w 2 . . w n ∣ c i ) p(w_{0},w_{1},w_{2}..w_{n}|c_{i}) p(w0,w1,w2..wnci)

(“朴素”地) 假设所有词都互相独立,则
p ( w 0 , w 1 , w 2 . . w n ∣ c i ) = p ( w 0 ∣ c i ) p ( w 1 ∣ c i ) p ( w 2 ∣ c i ) . . . p ( w n ∣ c i ) p(w_{0},w_{1},w_{2}..w_{n}|c_{i})=p(w_{0}|c_{i})p(w_{1}|c_{i})p(w_{2}|c_{i})...p(w_{n}|c_{i}) p(w0,w1,w2..wnci)=p(w0ci)p(w1ci)p(w2ci)...p(wnci)
这就极大地简化了计算的过程。

首先,计算文档属于侮辱性文档(class=1)的概率,即P(1)。因为这是一个二
类分类问题,所以可以通过1-P(1)得到P(0)。
然后,在for循环中,要遍历训练集trainMatrix中的所有文档。一旦某个词
语(侮辱性或正常词语)在某一文档中出现,则该词对应的个数(p1Num或者p0Num)就加1,而且在所有的文档中,该文档的总词数也相应加1。
最后,对每个元素除以该类别中的总词数。

import numpy as np

def trainNB0(trainMatrix,trainCategory):
    """
    分类器训练函数
    - - - -
    trainMatrix - 训练文档矩阵
	trainCategory - 训练类别标签向量,即classVec
    """
    #训练的文档数目
    numTrainDocs =len(trainMatrix)
    #词条数
    numWords = len(trainMatrix[0])
    #文档属于侮辱类的概率
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    #初始化概率
    p0Num = np.zeros(numWords)
    p1Num = np.zeros(numWords)
    p0Denom = 0.0
    p1Denom = 0.0
    for i in range(numTrainDocs):
        #统计属于侮辱类的条件概率所需的数据,即P(wi|1)
		if trainCategory[i] == 1:
			p1Num += trainMatrix[i]
			p1Denom += sum(trainMatrix[i])
        #统计属于非侮辱类的条件概率所需的数据,即P(wi|0)
		else:		
			p0Num += trainMatrix[i]
			p0Denom += sum(trainMatrix[i])
    #对每个元素做除法
    p1Vect = p1Num/p1Denom
    p0Vect = p0Num/p0Denom 
    return p0Vect,p1Vect,pAbusive

3.测试算法:根据现实情况修改分类器

利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,如果其中一个概率值为0,那么最后的乘积也为0。
为降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2

将trainNB0()对应部分修改为:

p0Num = np.ones(numWords)
p1Num = np.ones(numWords)
p0Denom = 2.0
p1Denom = 2.0

另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。
一种解决办法是对乘积取自然对数。在代数中有ln(a*b) = ln(a)+ln(b),于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。

将trainNB0()对应部分修改为:

p1Vect = np.log(p1Num/p1Denom)
p0Vect = np.log(p0Num/p0Denom)

最后分类和测试函数如下:

def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    """
    朴素贝叶斯分类器分类函数
    - - - -
    vec2Classify - 待分类的词条数组

	p0Vec - 侮辱类的条件概率数组

	p1Vec -非侮辱类的条件概率数组

	pClass1 - 文档属于侮辱类的概率
    """
    p1 = np.sum(vec2Classify * p1Vec) + np.log(pClass1)
    p0 = np.sum(vec2Classify * p0Vec) + np.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(np.array(trainMat),np.array(listClasses))
    
    #测试样本1
    testEntry = ['love', 'my', 'dalmation']
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
    print (testEntry,'classified as:' ,classifyNB(thisDoc,p0V,p1V,pAb))
   
    #测试样本2
    testEntry = ['stupid', 'garbage']										
    thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
    print (testEntry,'classified as:' ,classifyNB(thisDoc,p0V,p1V,pAb))
	
if __name__ == '__main__':
    testingNB()

结果:

['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1

4.准备数据:文档词袋模型

目前为止,我们将每个词的出现与否作为一个特征,这可以被描述为词集模型(set-of-wordsmodel)
如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)

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

def bagOfWords2VecMN(vocabList, inputSet): 
    """
    根据词汇表,将inputSet向量化(词袋化)
    - - - -
    vocabList - 词汇表

    inputSet - 切分的词条列表
    """
    returnVec = [0]*len(vocabList)
    for word in inputSet:
        if word in vocabList: 
            returnVec[vocabList.index(word)] += 1
    return returnVec

四、实战:使用朴素贝叶斯过滤垃圾邮件

1.准备数据:切分文本

import re

def textParse(bigString):
    """
    将字符串转换为字符列表
    - - - -
    bigString - 一个含大写字母的字符串
    """
    ##将特殊符(非字母数字)作为切分标志进行字符串切分
    listOfTokens = re.split(r'\W*', bigString)
    #除了单个字母,例如大写的I,其它单词变成小写
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]

2.测试算法:使用朴素贝叶斯进行交叉验证

本例中共有50封电子邮件,并不是很多,其中的10封电子邮件被随机选择为测试集,同时也将其从训练集中剔除。分类器所需要的概率计算只利用训练集中的文档来完成。

这种随机选择数据的一部分作为训练集,而剩余部分作为测试集的过程称为留存交叉验证(hold-out cross validation)
假定现在只完成了一次迭代,那么为了更精确地估计分类器的错误率,就应该进行多次迭代后求出平均错误率。

def spamTest():
    """
    垃圾邮件分类测试函数
    - - - -
    """
    docList = []; classList = []; fullText = []
    #遍历25个txt文件
    for i in range(1, 26):  
        #读取每个垃圾邮件,并字符串转换成字符串列表
        wordList = textParse(open('3-NaiveBayes/email/spam/%d.txt'%i,encoding='ISO-8859-1').read()) 
        docList.append(wordList)
        fullText.append(wordList)
        #用1标记垃圾邮件
        classList.append(1)  
        #读取每个非垃圾邮件,并字符串转换成字符串列表
        wordList = textParse(open('3-NaiveBayes/email/ham/%d.txt'%i,encoding='ISO-8859-1').read())
        docList.append(wordList)
        fullText.append(wordList)
        #用0标记非垃圾邮件
        classList.append(0)
    #词汇表
    vocabList = createVocabList(docList)
    #存储训练集的索引值的列表和测试集的索引值的列表
    trainingSet = list(range(50)); testSet = []
    #从50个邮件中,随机挑选出10个做测试集
    for i in range(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(np.array(trainMat), np.array(trainClasses)) 
    
    #分类测试
    errorCount = 0
    for docIndex in testSet:
        wordVector = setOfWords2Vec(vocabList, docList[docIndex])
        if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
            errorCount += 1  
    print('the error rate is: ',float(errorCount) / len(testSet))                     

if __name__ == '__main__':
    #1.分类器测试
    #testingNB()
    #2.实战:过滤垃圾邮件
    spamTest()

结果:

the error rate is:  0.3

函数spamTest()会输出在10封随机选择的电子邮件上的分类错误率。
既然这些电子邮件是随机选择的,所以每次的输出结果可能有些差别。

如果发现错误的话,可设置函数输出错分文档的词表,这样就可以了解到底是哪篇文档发生了错误。
如果想要更好地估计错误率,那么就应该将上述过程重复多次,比如说10次,然后求平均值。

五、朴素贝叶斯小结

对于分类而言,使用概率有时要比使用硬规则更为有效。贝叶斯概率及贝叶斯准则提供了一种利用已知值来估计未知概率的有效方法。

可以通过特征之间的条件独立性假设,降低对数据量的需求。独立性假设是指一个词的出现概率并不依赖于文档中的其他词。
当然我们也知道这个假设过于简单。这就是之所以称为朴素贝叶斯的原因。尽管条件独立性假设并不正确,但是朴素贝叶斯仍然是一种有效的分类器。

利用现代编程语言来实现朴素贝叶斯时需要考虑很多实际因素。下溢出就是其中一个问题,它可以通过对概率取对数来解决。

词袋模型在解决文档分类问题上比词集模型有所提高。

还有其他一些方面的改进,比如说移除停用词,当然也可以花大量时间对切分器进行优化

你可能感兴趣的:(机器学习,算法,机器学习,python,朴素贝叶斯)