机器学习(四) 朴素贝叶斯实现垃圾邮件分类(Python代码)


参考:https://blog.csdn.net/c406495762/article/details/77500679?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166962094416800182754092%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=166962094416800182754092&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-1-77500679-null-null.article_score_rank_blog&utm_term=%E9%82%AE%E4%BB%B6&spm=1018.2226.3001.4450

一、相关概念

1.条件概率

X,Y为两个事件,且P(X)>0,则称 P ( X Y ) P ( X ) \frac{P(XY)}{P(X)} P(X)P(XY)事件X已经发生的条件下事件Y发生的条件概率,记为P(Y|X),即:
P ( Y ∣ X ) = P ( X Y ) P ( X ) P(Y|X)= \frac{P(XY)}{P(X)} P(YX)=P(X)P(XY)

2.贝叶斯公式

公式:
P ( Y │ X ) = P ( Y ) P ( X ∣ Y ) P ( X ) P(Y│X)=P(Y)\frac{P(X|Y)}{ P(X)} P(YX)=P(Y)P(X)P(XY)
上式中的P(Y)先验概率P(Y|X)后验概率P(X|Y)/P(X)可能性函数

解释:
先验概率(Prior probability):
  在X事件发生之前,对Y事件的预先判断。

后验概率(Posterior probability):
  在X事件发生之后,我们对Y事件概率的重新判断。

可能性函数(Likelyhood):
  调整因子,使得预估概率更接近真实概率。

3.朴素贝叶斯

与贝叶斯的不同:
   朴素贝叶斯分类器(Naïve Bayes Classifier) 采用了 “属性条件独立性假设”,即每个属性独立地对分类结果发生影响。

公式:
  由于每个属性之间相互独立,得:
P ( y │ x ) = P ( y ) P ( x 1 , x 2 , . . . , x d ∣ y ) P ( x ) = P ( y ) P ( x ) ∏ i = 1 d P ( x i ∣ y ) P(y│x)=P(y)\frac{P(x_1,x_2,...,x_d|y)}{ P(x)}=\frac{P(y)}{ P(x)}\prod_{i=1}^d{P(x_i|y)} P(yx)=P(y)P(x)P(x1,x2,...,xdy)=P(x)P(y)i=1dP(xiy)

4.拉普拉斯修正

导入:
   在训练过程中,若某个 属性值 x i x_i xi 没有与 某个类y 同时出现过,上式中的某一个 P ( x i ∣ y ) {P(x_i|y)} P(xiy) 就会为0,导致最后连乘的结果也为0,致使模型无法正确分类

公式:
   令 N 表示训练集 D可能的类别数 N i N_i Ni 表示第i个属性可能的取值数,则贝叶斯公式可修正为
P ( y ) = ∣ D y ∣ + 1 ∣ D ∣ + N P(y) = \frac{|D_y| + 1}{|D| + N} P(y)=D+NDy+1
P ( x i ∣ y ) = ∣ D y , x i ∣ + 1 ∣ D ∣ + N i P(x_i|y) = \frac{|D_{y,x_i}| + 1}{|D| + N_i} P(xiy)=D+NiDy,xi+1

   修正后类别概率与条件概率都不再可能等于0,使模型可以顺利进行分类。

5.模型

  文本特征提取两个非常重要的模型

  1.词集模型单词构成的集合,集合自然每个元素都只有一个,也即词集中的每个单词都只有一个

  2.词袋模型:在词集的基础上如果一个单词在文档中出现不止一次,统计其出现的次数(频数)。

  两者本质上的区别: 词袋是在词集的基础上增加了频率的维度,词集只关注有和没有,词袋还要关注有几个。

二、数据集准备

数据集来源:https://github.com/Jack-kui/machine-learning/tree/2272a56dcf13c98b8a3946eede2fb4fa02cb7c4b/Naive%20Bayes/email

邮件数据集下有两个文件夹,其中一个文件夹为spam(垃圾邮件),另一个文件夹为ham(正常邮件),这两个文件夹下各有25个txt文件

spam(垃圾邮件)示例:

Bargains Here! Buy Phentermin 37.5 mg (K-25)
Buy Genuine Phentermin at Low Cost
VISA Accepted
30 - $130.50
60 - $219.00
90 - $292.50
120 - $366.00
180 - $513.00

ham(正常邮件)示例:

WHat is going on there?
I talked to John on email. We talked about some computer stuff that’s it.
I went bike riding in the rain, it was not that cold.
We went to the museum in SF yesterday it was $3 to get in and they had
free food. At the same time was a SF Giants game, when we got done we
had to take the train with all the Giants fans, they are 1/2 drunk.

三、代码实现

1.创建词汇表

def createVocabList(dataSet):
    '''
    创建词汇表
    
    Parameter:
    dataset: 包含多个文档的数据集
    
    Return:
    vocabSet:去重词汇表
    '''
    
    vocabSet = set([])  					
    for document in dataSet:				
        vocabSet = vocabSet | set(document) #取并集
    return list(vocabSet)

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

2.词集模型

def setOfWords2Vec(vocabList, inputSet):
    '''
    词集模型
    
    Parameter:
    vocabList:去重词汇表
    inputSet:输入的文档
    
    Return:
    returnVec:词集模型,与词汇表中位置一一对应,如存在某位置的单词则该位置=1
    '''
    
    returnVec = [0] * len(vocabList)  # 建立一个长度与词汇表相同的全0向量								
    for word in inputSet:		      # 遍历句子中每个单词,如单词存在于词汇表中,								
        if word in vocabList:	      # 则将向量的对应位置=1										
            returnVec[vocabList.index(word)] = 1
        else: print("the word: %s is not in my Vocabulary!" % word)
    return returnVec

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

3.词袋模型

def bagOfWords2VecMN(vocabList, inputSet):
    '''
    词袋模型
    
    Parameter:
    vocabList:去重词汇表
    inputSet:输入的文档
    
    Return:
    returnVec:词袋模型,与词汇表中位置一一对应,如存在某位置的单词则该位置=文档中该单词个数
    '''
    
    returnVec = [0]*len(vocabList)  #建立一个全0向量	
    for word in inputSet:		    #遍历句子中每个单词,如单词存在于词汇表中,
        if word in vocabList:	    #则在向量的对应位置+1	
            returnVec[vocabList.index(word)] += 1
    return returnVec

调试结果:
在这里插入图片描述
可以发现,词集模型是记录单词是否出现,而词袋模型是记录单词出现几次。

4.训练朴素贝叶斯模型

def trainNB0(trainMatrix,trainClasses):
    '''
    训练朴素贝叶斯模型
    
    Parameter:
    trainMatrix:每个returnVec所组成的矩阵
    trainClasses:每个returnVec所对应的类别, 1:侮辱类 或 0:正常类
    
    Return:
    p0Vect:正常类中每一个词的条件概率
    p1Vect:侮辱类中每一个词的条件概率
    pAbusive:侮辱类占总样本概率
    '''
    
    numTrainDocs = len(trainMatrix)					    #总文档数
    numWords = len(trainMatrix[0])						#每个文档的总字数
    pAbusive = sum(trainClasses)/float(numTrainDocs)	#文档属于侮辱类的概率
    # 使用拉普拉斯平滑
    p0Num = np.ones(numWords)                           #分子各单词出现数初始化为1
    p1Num = np.ones(numWords)	
    p0Denom = 2.0                                       #分母总单词数初始化为类别数2
    p1Denom = 2.0                                       
    
    for i in range(numTrainDocs):                       #遍历每个训练样本
        if trainClasses[i] == 1:					    
            p1Num += trainMatrix[i]                     #统计属于侮辱类的各个单词数量
            p1Denom += sum(trainMatrix[i])              #统计属于侮辱类的总单词数量
        else:												
            p0Num += trainMatrix[i]                     #统计属于正常类的各个单词数量
            p0Denom += sum(trainMatrix[i])              #统计属于正常类的总单词数量
            
    p1Vect = np.log(p1Num/p1Denom)						#取对数,防止下溢出          
    p0Vect = np.log(p0Num/p0Denom)          
    return p0Vect,p1Vect,pAbusive

调试结果(P0Vect):
在这里插入图片描述

5.对测试文档进行分类

方法:
  计算该文档为侮辱类的概率该文档为正常类概率大的概率对应类别即为该文档类别

注意:
  1.'+‘号左边从p1Vec(正常类中每一个词的条件概率)中选取该文档中的词的条件概率
  2.由于log(ab) = log(a) + log(b),下面所有的’+‘实际等同于在log内部的’×’
  3.原贝叶斯公式需除P(vec2Classify),但无论哪个类别该值都相同,直接比较分子大小

def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    '''
    对测试文档进行分类
    
    Parameter:
    vec2Classify:测试文档向量
    p0Vec:正常类中每一个词的条件概率
    p1Vec:侮辱类中每一个词的条件概率
    pClass1:侮辱类占总样本概率
    
    Return:
    1:侮辱类
    0:正常类
    '''
    
    # 计算p1(该文档为侮辱类的概率),与p0(正常类概率),大的概率对应类别即为该文档类别
    # 注意:
    # 1.'+'号左边从p1Vec(正常类中每一个词的条件概率)中选取该文档中的词的条件概率
    # 2.由于log(ab) = log(a) + log(b),下面所有的'+'实际等同于在log内部的'×'
    # 3.原贝叶斯公式需除P(vec2Classify),但无论哪个类别该值都相同,直接比较分子大小即可
    p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)    	
    p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else: 
        return 0

6.字符串解析

def textParse(bigString):   
    '''
    将字符串转换为小写字符列表
    
    Parameter:
    bigString:输入字符串
    
    Return:
    tok.lower():小写字符列表
    '''       
                                             
    #将特殊符号作为切分标志进行字符串切分,即非字母、非数字
    listOfTokens = re.split(r'\W+', bigString)
    #除了单个字母,其它单词变成小写                   
    return [tok.lower() for tok in listOfTokens if len(tok) > 2] 

7.测试分类器
可以选用词袋模型或词集模型进行测试。

def spamTest(method = 'bag'):
    '''
    测试朴素贝叶斯分类器,默认使用词袋模型
    
    Parameter:
    method:为 ['bag', 'set']中的一种
    'bag' 为使用词袋模型
    'set' 为使用词集模型
    
    Return:
    errorRate:对测试集的分类错误率
    '''
    
    if method == 'bag':                   #判断使用词袋模型还是词集模型
        words2Vec = bagOfWords2VecMN
    elif method == 'set':
        words2Vec = setOfWords2Vec
        
    docList = [] 
    classList = [] 
    
    # 分别遍历两个文件夹中25个txt文件
    for i in range(1, 26): 
        # 读取每个垃圾邮件,将字符串转换成字符串列表                                                 
        wordList = textParse(open('email/spam/%d.txt' % i, 'r').read())
        # 将该列表记录加入到文档列表当中,对应类别列表添加一个1,代表对应文档为侮辱类     
        docList.append(wordList)
        classList.append(1) 
        # 读取每个正常邮件,将字符串转换成字符串列表                                                  
        wordList = textParse(open('email/ham/%d.txt' % i, 'r').read())      
        # 将该列表记录加入到文档列表当中,对应类别列表添加一个0,代表对应文档为正常类
        docList.append(wordList)
        classList.append(0)      
    
    # 创建去重词汇表                                             
    vocabList = createVocabList(docList)    
    #print(vocabList)
    
    # 创建存储训练集的索引值的列表与测试集的索引值的列表,初始训练集索引值为所有文件的索引                                                       
    trainingSet = list(range(50))
    testSet = []  
    
    # 从50个邮件中,随机挑选出40个作为训练集,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(words2Vec(vocabList, docList[docIndex]))       
        trainClasses.append(classList[docIndex]) 
        
    # 训练朴素贝叶斯模型                           
    p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses))  
    
    # 错误分类计数器
    errorCount = 0   
    
    # 遍历测试集中每一个文档                                                       
    for docIndex in testSet: 
        # 生成该文档的(词集模型\词袋模型)
        # 使用训练好的朴素贝叶斯分类器分类,记录分类错误次数                                             
        wordVector = words2Vec(vocabList, docList[docIndex])           
        if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:    
            errorCount += 1                                                 
            #print("分类错误的测试集:",docList[docIndex])
    #print('错误率:%.2f%%' % (float(errorCount) / len(testSet) * 100))
    errorRate = float(errorCount) / len(testSet) # 分类错误率
    return errorRate

8.完整代码

import numpy as np
import random
import re


def createVocabList(dataSet):
    '''
    创建词汇表
    
    Parameter:
    dataset: 包含多个文档的数据集
    
    Return:
    vocabSet:去重词汇表
    '''
    
    vocabSet = set([])  					
    for document in dataSet:				
        vocabSet = vocabSet | set(document) #取并集
    return list(vocabSet)


def setOfWords2Vec(vocabList, inputSet):
    '''
    词集模型
    
    Parameter:
    vocabList:去重词汇表
    inputSet:输入的文档
    
    Return:
    returnVec:词集模型,与词汇表中位置一一对应,如存在某位置的单词则该位置=1
    '''
    
    returnVec = [0] * len(vocabList)  # 建立一个长度与词汇表相同的全0向量								
    for word in inputSet:		      # 遍历句子中每个单词,如单词存在于词汇表中,								
        if word in vocabList:	      # 则将向量的对应位置=1										
            returnVec[vocabList.index(word)] = 1
        else: print("the word: %s is not in my Vocabulary!" % word)
    #print(returnVec)
    return returnVec													

def bagOfWords2VecMN(vocabList, inputSet):
    '''
    词袋模型
    
    Parameter:
    vocabList:去重词汇表
    inputSet:输入的文档
    
    Return:
    returnVec:词袋模型,与词汇表中位置一一对应,如存在某位置的单词则该位置=文档中该单词个数
    '''
    
    returnVec = [0]*len(vocabList)  #建立一个全0向量	
    for word in inputSet:		    #遍历句子中每个单词,如单词存在于词汇表中,
        if word in vocabList:	    #则在向量的对应位置+1	
            returnVec[vocabList.index(word)] += 1
    #print(returnVec)
    return returnVec													


def trainNB0(trainMatrix,trainClasses):
    '''
    训练朴素贝叶斯模型
    
    Parameter:
    trainMatrix:每个returnVec所组成的矩阵
    trainClasses:每个returnVec所对应的类别, 1:侮辱类 或 0:正常类
    
    Return:
    p0Vect:正常类中每一个词的条件概率
    p1Vect:侮辱类中每一个词的条件概率
    pAbusive:侮辱类占总样本概率
    '''
    
    numTrainDocs = len(trainMatrix)					    #总文档数
    numWords = len(trainMatrix[0])						#每个文档的总字数
    pAbusive = sum(trainClasses)/float(numTrainDocs)	#文档属于侮辱类的概率
    # 使用拉普拉斯平滑
    p0Num = np.ones(numWords)                           #分子各单词出现数初始化为1
    p1Num = np.ones(numWords)	
    p0Denom = 2.0                                       #分母总单词数初始化为类别数2
    p1Denom = 2.0                                       
    
    for i in range(numTrainDocs):                       #遍历每个训练样本
        if trainClasses[i] == 1:					    
            p1Num += trainMatrix[i]                     #统计属于侮辱类的各个单词数量
            p1Denom += sum(trainMatrix[i])              #统计属于侮辱类的总单词数量
        else:												
            p0Num += trainMatrix[i]                     #统计属于正常类的各个单词数量
            p0Denom += sum(trainMatrix[i])              #统计属于正常类的总单词数量
            
    p1Vect = np.log(p1Num/p1Denom)						#取对数,防止下溢出          
    p0Vect = np.log(p0Num/p0Denom)  
    #print(p0Vect) 
    return p0Vect,p1Vect,pAbusive						


def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    '''
    对测试文档进行分类
    
    Parameter:
    vec2Classify:测试文档向量
    p0Vec:正常类中每一个词的条件概率
    p1Vec:侮辱类中每一个词的条件概率
    pClass1:侮辱类占总样本概率
    
    Return:
    1:侮辱类
    0:正常类
    '''
    
    # 计算p1(该文档为侮辱类的概率),与p0(正常类概率),大的概率对应类别即为该文档类别
    # 注意:
    # 1.'+'号左边从p1Vec(正常类中每一个词的条件概率)中选取该文档中的词的条件概率
    # 2.由于log(ab) = log(a) + log(b),下面所有的'+'实际等同于在log内部的'×'
    # 3.原贝叶斯公式需除P(vec2Classify),但无论哪个类别该值都相同,直接比较分子大小即可
    p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)    	
    p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else: 
        return 0


def textParse(bigString):   
    '''
    将字符串转换为小写字符列表
    
    Parameter:
    bigString:输入字符串
    
    Return:
    tok.lower():小写字符列表
    '''       
                                             
    #将特殊符号作为切分标志进行字符串切分,即非字母、非数字
    listOfTokens = re.split(r'\W+', bigString)
    #除了单个字母,其它单词变成小写                   
    return [tok.lower() for tok in listOfTokens if len(tok) > 2] 


def spamTest(method = 'bag'):
    '''
    测试朴素贝叶斯分类器,默认使用词袋模型
    
    Parameter:
    method:为 ['bag', 'set']中的一种
    'bag' 为使用词袋模型
    'set' 为使用词集模型
    
    Return:
    errorRate:对测试集的分类错误率
    '''
    
    if method == 'bag':                   #判断使用词袋模型还是词集模型
        words2Vec = bagOfWords2VecMN
    elif method == 'set':
        words2Vec = setOfWords2Vec
        
    docList = [] 
    classList = [] 
    
    # 分别遍历两个文件夹中25个txt文件
    for i in range(1, 26): 
        # 读取每个垃圾邮件,将字符串转换成字符串列表                                                 
        wordList = textParse(open('email/spam/%d.txt' % i, 'r').read())
        # 将该列表记录加入到文档列表当中,对应类别列表添加一个1,代表对应文档为侮辱类     
        docList.append(wordList)
        classList.append(1) 
        # 读取每个正常邮件,将字符串转换成字符串列表                                                  
        wordList = textParse(open('email/ham/%d.txt' % i, 'r').read())      
        # 将该列表记录加入到文档列表当中,对应类别列表添加一个0,代表对应文档为正常类
        docList.append(wordList)
        classList.append(0)      
    
    # 创建去重词汇表                                             
    vocabList = createVocabList(docList)    
    #print(vocabList)
    
    # 创建存储训练集的索引值的列表与测试集的索引值的列表,初始训练集索引值为所有文件的索引                                                       
    trainingSet = list(range(50))
    testSet = []  
    
    # 从50个邮件中,随机挑选出40个作为训练集,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(words2Vec(vocabList, docList[docIndex]))       
        trainClasses.append(classList[docIndex]) 
        
    # 训练朴素贝叶斯模型                           
    p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses))  
    
    # 错误分类计数器
    errorCount = 0   
    
    # 遍历测试集中每一个文档                                                       
    for docIndex in testSet: 
        # 生成该文档的(词集模型\词袋模型)
        # 使用训练好的朴素贝叶斯分类器分类,记录分类错误次数                                             
        wordVector = words2Vec(vocabList, docList[docIndex])           
        if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:    
            errorCount += 1                                                 
            #print("分类错误的测试集:",docList[docIndex])
    #print('错误率:%.2f%%' % (float(errorCount) / len(testSet) * 100))
    errorRate = float(errorCount) / len(testSet) # 分类错误率
    return errorRate

if __name__ == '__main__':
    total = 100
    print('使用词袋模型训练:')
    sum_bag_error = 0
    for i in range(total):
        sum_bag_error += spamTest(method = 'bag')
    print('使用词袋模型训练' + str(total) + '次得到的平均错误率为: ' + str((sum_bag_error / total)))
    
    print('--------------------------')
    
    print('使用词集模型训练:')
    sum_set_error = 0
    for i in range(total):
        sum_set_error += spamTest(method = 'set')
    print('使用词集模型训练' + str(total) + '次得到的平均错误率为: ' + str((sum_set_error / total)))

四、结果

最终结果展示
在这里插入图片描述

结果分析
使用当前的邮件数据集,在分别都训练100次后,词集模型的平均错误率略低于词袋模型,效果稍好一些。

不足
如能做出中文的垃圾邮件分类更好。


五、代码获取

链接: https://pan.baidu.com/s/1_1zr0x4sfaAOjhWrHNf7AA?pwd=wg5b
提取码: wg5b

  • 7
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yunggemmy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值