朴素贝叶斯的应用----文档分类、垃圾邮件过滤

1. 基于朴素贝叶斯的文档分类

​ 以在线社区的留言板为例。为了不影响社区的发展,需要屏蔽侮辱性的言论,所以要构件一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么久将该留言标识为内容不当。过滤这类内容是一个很常见的需求。对此问题建立两个类别:侮辱类和非侮辱类,使用1和0分别表示。

​ 把文本看成单词向量或者词条向量,也就是说将句子转换为向量。考虑出现在所有文档中的所有单词,需要构件词汇表,然后必须要将每一篇文档转换为词汇表上的向量。

​ 这里的留言文本数据,已经被切分好,存放在列表中,并且人为标注好类别,这些标注信息用于训练程序以便自动检测侮辱性留言。有两个类别:侮辱类(1)和非侮辱类(0)。主要包括以下函数:

  • loadDataSet: 构建数据集
  • createVocabList: 构建词汇表
  • setOfWords2Vec: 构建词集向量
  • trainNB: 朴素贝叶斯分类器训练函数
  • classifyNB: 朴素贝叶斯分类函数
  • testingNB: 朴素贝叶斯测试函数
import numpy as np  # 导入numpy包

def loadDataSet():
    """
    构建数据集
    :return:
        dataSet:切分好的样本词条
        classVec:类标签向量
    """
    dataSet =[['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 dataSet,classVec

def createVocabList(dataSet):
    """
    构建词汇表,即创建一个包含在所有文档中出现的不重复词的列表
    :param dataSet: 切分好的样本词条
    :return:
        vocabList:不重复的词汇表
    """
    vocabSet = set([])  # 创建一个空的集合
    for document in dataSet:
        vocabSet = vocabSet | set(document)  # 取并集
    vocabList = list(vocabSet)  # 将集合转换为列表
    return vocabList

def setOfWords2Vec(vocabList,inputSet):
    """
    基于词集模型创建文档向量,向量的每一元素为1或0,分别表示词汇表中的单词在输入文档中是否出现
    :param vocabList: 词汇表
    :param inputSet: 某一条切分好的词条列表
    :return:
        returnVec:文档向量
    """
    returnVec = [0]*len(vocabList)   # 创建一个和词汇表等长的向量,并将其元素设置为0
    for word in inputSet:  # 遍历每个词条
        if word in vocabList:  # 如果词条出现在词汇表中
            returnVec[vocabList.index(word)] = 1  # 文档向量中对应值设置为1
        else:
            print("the word: %s is not in my vocabulary!" % word)
    return returnVec

def trainNB(trainMatrix,classVec):
    """
    朴素贝叶斯分类器训练函数
    :param trainMatrix: 经过转换的文档矩阵,每行都是一个文档向量
    :param classVec: 由每篇文档类别标签所构成的向量
    :return:
        p0V: 非侮辱类的条件概率数组
        p1V:侮辱类的条件概率数组
        pAb:文档属于侮辱类的概率
    """
    num_docs = len(trainMatrix)  # 文档数量
    num_words = len(trainMatrix[0])  # 词表数
    pAb = sum(classVec)/float(num_docs)  # 文档属于侮辱类的概率
    # 初始化概率
    p0Num = np.zeros(num_words)  # 词条出现数初始化为0
    p0Denom = 0.0               # 分母初始化为0
    p1Num = np.zeros(num_words)
    p1Denom = 0.0
    for i in range(num_docs):
        if classVec[i] == 1:
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    p0V = p0Num/p0Denom  # 对每个元素做除法后,取对数
    p1V = p1Num/p1Denom
    return p0V,p1V,pAb

def classifyNB(vec2Classify,p0V,p1V,pAb):
    """
    朴素贝叶斯分类函数
    :param vec2Classify: 待分类的词条
    :param p0V: 非侮辱类的条件概率数组
    :param p1V: 侮辱类的条件概率数组
    :param pAb: 文档属于侮辱类的概率
    :return:
        0: 属于非侮辱类
        1: 属于侮辱类
    """
    from functools import reduce
    # reduce从左到右对一个序列的项累计地应用有两个参数的函数,以此合并序列到一个单一值。
    # reduce(lambda x, y: x * y, [1, 2, 3, 4, 5]),其实就是((((1*2)*3)*4)*5)=120
    p1 = reduce(lambda x,y:x*y,vec2Classify * p1V)*pAb  # 元素相乘
    p0 = reduce(lambda x,y:x*y,vec2Classify * p0V)*(1-pAb)
    if p1 > p0:
        return 1
    else:
        return 0
    
def testingNB(testVec):
    """
    朴素贝叶斯测试函数
    :param testVec: 测试样本
    :return: 测试样本的预测类别
    """
    # 构建数据集
    dataSet,classVec = loadDataSet()
    # 生成词汇表
    vocabList = createVocabList(dataSet)
    # 将训练数据转换为词集文档向量
    trainMat = []
    for inputSet in dataSet:
        trainMat.append(setOfWords2Vec(vocabList,inputSet))
    # 获取三个概率
    p0V,p1V,pAb = trainNB(np.array(trainMat),np.array(classVec))
    thisDoc = np.array(setOfWords2Vec(vocabList,testVec))
    if classifyNB(thisDoc,p0V,p1V,pAb):
        print(testEntry,'属于侮辱类')   # 打印分类结果
    else:
        print(testEntry, '属于非侮辱类')

输入测试文本,具体代码如下

# 测试文本1
testEntry = ['love', 'my', 'dalmation']
testingNB0(testEntry)
# 测试文本2
testEntry = ['stupid', 'garbage']
testingNB0(testEntry)

运行结果如下:

[‘love’, ‘my’, ‘dalmation’] 属于非侮辱类
[‘stupid’, ‘garbage’] 属于非侮辱类

这是什么原因呢?因为有一个概率为0,多个概率乘积后也为0,这种严重缺陷是要通过下面的方法来处理的。

朴素贝叶斯改进之拉普拉斯平滑

利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率。即计算 p ( x 0 ∣ 1 ) p ( x 1 ∣ 1 ) p ( x 2 ∣ 1 ) p(x_0|1)p(x_1|1)p(x_2|1) p(x01)p(x11)p(x21)。如果其中一个概率为0,那么最后的乘积也为0。为了降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2(因为所有属性的取值都只有0和1两个值)。这种做法叫做拉普拉斯平滑(Laplace Smoothing),又被称为加1平滑,是比较常用的平滑方法,它就是为了解决0概率问题。

原来的条件概率公式为:
P ( x i ∣ c ) = ∣ D c , x i ∣ ∣ D c ∣ P(x_i|c)= \frac{|D_{c,x_i}|}{|D_c|} P(xic)=DcDc,xi
被拉普拉斯修正为:
P ( x i ∣ c ) = ∣ D c , x i ∣ + 1 ∣ D c ∣ + N i P(x_i|c)= \frac{|D_{c,x_i}|+1}{|D_c|+N_i} P(xic)=Dc+NiDc,xi+1
其中, ∣ D c , x i ∣ |D_{c,x_i}| Dc,xi是c类别下第i个属性上取值为 x i x_i xi的样本数, ∣ D c ∣ |D_c| Dc是c类的样本数。 N i N_i Ni表示第i个属性可能的取值数,本例中每个属性的取值都是2,所以 N i N_i Ni为2。

另一个问题就是下溢出,这是由于太多很小的数相乘造成的。在计算乘积时,由于大部分因子都非常小,所以程序会下溢出或得到不正确答案。一种解决方法是对乘积取自然对数,即ln(a*b)=ln(a)+ln(b)。通过求对数避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。下图给出函数f(x)和ln(f(x))曲线。

在这里插入图片描述

检查这两条曲线,会发现它们在相同区域内同时增加或减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。具体修改代码如下:

def trainNB(trainMatrix,classVec):
    """
    朴素贝叶斯分类器训练函数
    :param trainMatrix: 经过转换的文档矩阵,每行都是一个文档向量
    :param classVec: 由每篇文档类别标签所构成的向量
    :return:
        p0V: 非侮辱类的条件概率数组
        p1V:侮辱类的条件概率数组
        pAb:文档属于侮辱类的概率
    """
    num_docs = len(trainMatrix)  # 文档数量
    num_words = len(trainMatrix[0])  # 词表数
    pAb = sum(classVec)/float(num_docs)  # 文档属于侮辱类的概率
    # 初始化概率
    p0Num = np.ones(num_words)  # 词条出现数初始化为1
    p0Denom = 2.0               # 分母初始化为2
    p1Num = np.ones(num_words)
    p1Denom = 2.0
    for i in range(num_docs):
        if classVec[i] == 1:
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    p0V = np.log(p0Num/p0Denom)  # 对每个元素做除法后,取对数
    p1V = np.log(p1Num/p1Denom)
    return p0V,p1V,pAb

def classifyNB(vec2Classify,p0V,p1V,pAb):
    """
    朴素贝叶斯分类函数
    :param vec2Classify: 待分类的词条
    :param p0V: 非侮辱类的条件概率数组
    :param p1V: 侮辱类的条件概率数组
    :param pAb: 文档属于侮辱类的概率
    :return:
        0: 属于非侮辱类
        1: 属于侮辱类
    """
    p1 = sum(vec2Classify*p1V) + np.log(pAb)  # 元素相乘
    p0 = sum(vec2Classify*p0V) + np.log(1-pAb)
    if p1 > p0:
        return 1
    else:
        return 0

输入上面的测试样本,运行结果如下

[‘love’, ‘my’, ‘dalmation’] 属于非侮辱类
[‘stupid’, ‘garbage’] 属于侮辱类

这个结果,看起来就没问题了!

2. 基于朴素贝叶斯的电子垃圾邮件过滤

使用朴素贝叶斯解决现实生活中的问题时,需要先从文本内容得到字符串列表,然后生成词向量。主要有一下函数:

  • textParse:文本解析
  • spamTest:基于朴素贝叶斯的垃圾邮件测试函数
def textParse(bigString):
    """
    文本解析:将给的大字符串解析为字符串列表,并去掉少于两个字符的字符串,并将字符串转换为小写
    :param bigString: 大字符串
    :return: 词条向量
    """
    import re
    # 用正则表示来切分句子,其中分隔符是除单词,数字外的任意字符串
    listOfTokens = re.split(r'\W*',bigString)
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]

def spamTest():
    """
    基于朴素贝叶斯的垃圾邮件分类测试函数
    """
    docList =[]
    classList = []
    # 导入50封电子邮件,并解析文件
    for i in range(1,26):
        wordList = textParse(open('email/spam/%d.txt' % i).read())
        docList.append(wordList)
        classList.append(1)
        wordList = textParse(open('email/ham/%d.txt' % i).read())
        docList.append(wordList)
        classList.append(0)
    # 解析为词典列表
    vocabList = createVocabList(docList)
    # 随机构建训练集和测试集
    traingSet = list(np.arange(50))
    testSet = []
    for i in range(10):
        randIndex = int(np.random.uniform(0,len(traingSet)))
        testSet.append(traingSet[randIndex])
        del traingSet[randIndex]
    trainMat = []
    trainClass = []
    for docIndex in traingSet:
        trainMat.append(setOfWords2Vec(vocabList,docList[docIndex]))
        trainClass.append(classList[docIndex])
    p0V,p1V,pSpam = trainNB(np.array(trainMat),np.array(trainClass))
    # 对测试集进行分类
    errorCount = 0
    for docIndex in testSet:
        wordVect = setOfWords2Vec(vocabList,docList[docIndex])
        if classifyNB(wordVect,p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
            print("classificaiton error ",docList[docIndex])  # 输出分类错误的词条向量
    print('the error rate is : ',float(errorCount)/len(testSet))  # 分类错误率
    return float(errorCount)/len(testSet)

输入下列内容

spamTest()

运行结果如下:

classificaiton error [‘oem’, ‘adobe’, ‘microsoft’, ‘softwares’, ‘fast’, ‘order’, ‘and’, ‘download’, ‘microsoft’, ‘office’, ‘professional’, ‘plus’, ‘2007’, ‘2010’, ‘129’, ‘microsoft’, ‘windows’, ‘ultimate’, ‘119’, ‘adobe’, ‘photoshop’, ‘cs5’, ‘extended’, ‘adobe’, ‘acrobat’, ‘pro’, ‘extended’, ‘windows’, ‘professional’, ‘thousand’, ‘more’, ‘titles’]

the error rate is : 0.1

再次运行,结果如下

the error rate is : 0.0

spamTest函数会输出在10封随机选择的电子邮件上的分类错误率。由于这些电子邮件是随机选择的,所以每次的输出结果可能会有差别。如果发现错误的话,函数会食醋胡错分文档的词条列表,这样就可以廖家到底是哪篇文档发生了错误。

​ 如果想更好地估计错误率,可以将上述过程迭代多次。文本迭代10次,输入下列内容:

errorrate = 0.0
for i in range(10):
    errorrate += spamTest()
    print("迭代10次的平均错误率为:",errorrate/10)

运行结果如下:

在这里插入图片描述

这里一直出现的错误是将垃圾邮件误判为正常邮件。相比之下,将垃圾邮件误判为正常邮件要比将正常邮件归为垃圾邮件好。

报错

如果没有出现报错,直接跳过

报错:UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0xae in position 199: illegal multib

原因:表示读取ham文件夹中的文件有异常。

解决方法: 打开23.txt 文件发现,包含一个?字符。“SciFinance?is”,删除该“?”即可。

3. 基于鸾尾花数据集的朴素贝叶斯分类

应用GaussianNB原理来对鸾尾花数据集进行分类,主要有一下函数:

  • loadDataSet:加载数据集
  • randSplit:按比例随机拆分数据集
  • gnb_classify:基于高斯分布的朴素贝叶斯分类函数
import pandas as pd
import numpy as np
import random

def loadDataSet():
    """加载数据集"""
    from sklearn import datasets
    iris_data = datasets.load_iris()
    # print(iris_data.DESCR)  # 查看数据集简介
    dataSets = pd.DataFrame(iris_data.data)
    # sepal_length:花萼长度,sepal_width:花萼宽度  单位是厘米
    # petal_length:花瓣长度,petal_width:花瓣宽度  单位是厘米
    dataSets.columns = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
    dataSets['类别'] = iris_data.target
    return dataSets

def randSplit(dataSet,rate):
    """
    随机拆分数据集,分为训练集和测试集
    :param dataSet: 数据集
    :param rate:训练集数据量占比
    :return: 拆分后的训练集和测试集
    """
    l = list(dataSet.index)  # 提取出数据集的索引
    random.shuffle(l)  # 随机打乱索引
    dataSet.index = l  # 将打乱后的索引重新赋值给原数据集
    m = dataSet.shape[0]
    m_train = int(rate * m)  # 训练集数量
    train_data = dataSet.loc[range(m_train),:]  # 提取前m_train个样本作为训练集
    test_data = dataSet.loc[range(m_train,m),:] # 剩下的作为测试集
    # 重置索引
    train_data.index = range(len(train_data))
    test_data.index = range(len(test_data))
    return train_data,test_data

def gnb_classify(train_data,test_data):
    """
    基于高斯分布的朴素贝叶斯分类函数
    :param train_data:训练数据集
    :param test_data:测试数据集
    :return:含预测分类结果的测试数据集
    """
    labels = list(set(train_data.iloc[:,-1]))  # 获取类别
    mean_list = []  # 存放每个类别的均值 3行4列
    var_list = []   # 存放每个类别的方差 3行4列
    for label in labels:
        curr_label_data = train_data.loc[train_data.iloc[:,-1]==label,:]  # 提取某种类别的数据
        m = curr_label_data.iloc[:,:-1].mean()  # 计算当前类别的平均值
        v = np.sum((curr_label_data.iloc[:,:-1]-m)**2)/curr_label_data.shape[0]  # 当前类别的方差
        mean_list.append(m)
        var_list.append(v)
    mean_df = pd.DataFrame(mean_list,index=labels)
    var_df = pd.DataFrame(var_list,index=labels)
    result = []
    for j in range(test_data.shape[0]):
        curr_test = test_data.iloc[j,:-1].tolist()
        predict_prob = np.exp(-1*(curr_test-mean_df)**2/(2*var_df))/(np.sqrt(2*np.pi*var_df)) # 正态分布公式
        prob = 1 # 初始化当前实例总概率
        for k in range(test_data.shape[1]-1):
            prob *= predict_prob.iloc[:,k]
        cla = prob.index[np.argmax(prob.values)]  # 返回最大概率的类别
        result.append(cla)
    test_data['predict'] = result
    acc = (test_data.iloc[:,-1]==test_data.iloc[:,-2]).mean() # 计算预测准确率
    print("预测准确率为:",acc)
    return test_data

输入下列内容

dataSet = loadDataSet()
# 迭代10次,看下准确率情况
for i in range(10):
    train_data, test_data = randSplit(dataSet, 0.8)
    test_result = gnb_classify(train_data, test_data)

运行结果如下

在这里插入图片描述

若直接调用sklearn包,则用下列函数

def gbn_classify_by_sklearn(train_data,test_data):
    """
    调用sklearn实现基于高斯分布的朴素贝叶斯分类函数
    :param train_data: 训练数据集
    :param test_data: 测试数据集
    :return: 含预测分类结果的测试数据集
    """
    from sklearn.naive_bayes import GaussianNB  # 高斯分布
    from sklearn.metrics import accuracy_score
    # 建模
    gnb_clf = GaussianNB()
    gnb_clf.fit(train_data.iloc[:,:-1], train_data.iloc[:,-1])
    # 对测试集进行预测
    # predict():直接给出预测的类别
    predict_class = gnb_clf.predict(test_data.iloc[:,:-1])
    test_data['predict'] = list(predict_class)
    acc = accuracy_score(test_data.iloc[:, -1], test_data.iloc[:,-2])  # 计算预测准确率
    print("预测准确率为:", acc)
    return predict_class

运行下面内容

dataSet = loadDataSet()
# 迭代10次,看下准确率情况
for i in range(10):
    train_data, test_data = randSplit(dataSet, 0.8)
    test_result = gbn_classify_by_sklearn(train_data, test_data)

运行结果如下

在这里插入图片描述
如果本文对你有帮助,记得“点赞”哦~

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值