自实现朴素贝叶斯分类器with案例:基于SMS Spam Collection数据集的广告邮件分类

news2024/10/7 9:29:18

目录

  • 贝叶斯分类器
  • 何为朴素
  • 案例:基于SMS Spam Collection数据集的广告邮件分类
    • SMS数据集
    • 词向量表示
    • Laplacian平滑
    • 训练过程
    • 分类过程
  • 完整代码


贝叶斯分类器

首先要理解贝叶斯决策的理论依据,引用西瓜书上的原话:对于分类任务,在所有相关概率都已知的理想情形下,贝叶斯决策论考虑如何基于这些概率误判损失来选择最优的类别标记。

然后引入我们很熟悉的贝叶斯公式:
P ( c ∣ x ) = P ( c ) P ( x ∣ c ) P ( x ) P(c\mid \boldsymbol{x}) = \frac{P(c)P(\boldsymbol{x} \mid c)}{P(\boldsymbol{x})} P(cx)=P(x)P(c)P(xc)
其中 c c c 是类别标记, x x x 是样本点(一个包含n维属性的向量)。 P ( c ) P(c) P(c)就是所谓的“先验”概率,这个概率是可以通过数据集统计直接得到的,那么 P ( c ∣ x ) P(c \mid \boldsymbol{x}) P(cx)就是所谓的“后验“概率,即我们要在已有数据的信息背景下推断得到的。


与其它机器学习的算法不同,贝叶斯分类算法似乎看不出一个明显的待训练参数,但观察公式也能明白,我们要求出的 P ( c ∣ x ) P(c \mid \boldsymbol{x}) P(cx)是由 P ( c ) 、 P ( x ∣ c ) P(c)、P(\boldsymbol{x} \mid c) P(c)P(xc)以及 P ( x ) P(\boldsymbol{x}) P(x)三者变量所共同决定的,而这三者的现实意义其实就是给定的信息背景(数据集)——多数情况下,我们在不同的信息背景下总能得到不同的 P ( c ∣ x ) 、 P ( c ) 、 P ( x ∣ c ) P(c \mid \boldsymbol{x})、P(c)、P(\boldsymbol{x} \mid c) P(cx)P(c)P(xc),进而推出不同的 P ( c ∣ x ) P(c \mid \boldsymbol{x}) P(cx)

有些信息背景对于作出决策的贡献是“好的”,这时 P ( c ∣ x ) P(c \mid \boldsymbol{x}) P(cx)体现出来的意义能很真实地反映出作出某项决策的正确性,而在有些信息背景(比如样本过于稀疏)下得出的结果就并不能很好地反映待检测样本所属的真实类别,进而造成误分类。

于是Bayes分类器的训练意义在于寻求“好的”数据集,使得后验概率值能较好地反映出决策的真实性。


何为朴素

从概率学原理来讲,类条件概率 P ( x ∣ c ) P(\boldsymbol{x} \mid c) P(xc),是所有属性上的联合概率,很难从有限的训练样本直接估计而得。那么为避开这个障碍,朴素贝叶斯分类器采用了“属性条件独立性假设”:对已知类别假设所有属性之间相互独立

此时类条件概率满足:
P ( x ∣ c ) = ∏ i = 1 d P ( x i ∣ c ) P(\boldsymbol{x} \mid c)=\prod_{i=1}^{d}P(x_i \mid c) P(xc)=i=1dP(xic)
其中 d d d 代表样本点的属性个数, x i x_i xi 代表 x \boldsymbol{x} x的各个属性。

于是开头的贝叶斯公式进一步推:
P ( c ∣ x ) = P ( c ) P ( x ∣ c ) P ( x ) = P ( c ) ∏ i = 1 d P ( x i ∣ c ) P ( x ) P(c\mid \boldsymbol{x}) = \frac{P(c)P(\boldsymbol{x} \mid c)}{P(\boldsymbol{x})}=\frac{P(c)\prod_{i=1}^{d}P(x_i \mid c)}{P(\boldsymbol{x})} P(cx)=P(x)P(c)P(xc)=P(x)P(c)i=1dP(xic)
于是在此假设前提下,进而大大地简化了计算,这也正是“朴素(Naive)”一词修饰的由来。

而且在一般分类任务下,是不会计算 P ( x ) P(\boldsymbol x) P(x)的,而是只计算分子便进行比较。


案例:基于SMS Spam Collection数据集的广告邮件分类

SMS数据集

SMS Spam Collection是用于广告短信识别的经典数据集,完全来自真实短信内容,包括4831条正常短信和747条广告短信。

其内容如下,每一个line都表示一段邮件,"ham"和"spam"分别表示邮件的类别“正常邮件”和“广告邮件”,然后以'\t'为间隔右边的长字符串为邮件内容。

在这里插入图片描述
这里选取了数据集中的3308条记录。


词向量表示

  • 每个词语的出现各看成一个事件,先分别计算单个词语的事件概率进行训练,然后将一个完整的邮件看成这些事件的交事件。

给定一系列邮件的文本,将每个邮件的关键词提取出来,我们认为长度>2的单词才为关键词,然后将这些关键词转换为小写,其组成的列表作为一个数据样本,所以加载数据集如下

# 创建数据集,加载数据
adClass = 1  # 广告,垃圾标识

def textParse(bigString):
    '''接收一个长字符串解析成字符串列表'''
    #将特殊符号作为划分标志进行字符串切分,即非字母,非数字
    listOfTokens = re.split(r'\W+', bigString)
    #除单字母  其它单词全变成小写
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]

def loadDataSet():
    '''加载数据集合及其对应的分类'''
    classVec = []    #0-1列表 第i个元素标识了wordList第i行类别
    wordList = []    #提取出的词矩阵,每一行是对应一个邮件的单词列表
    
    smss = open("./SMSSpamCollection.txt", 'r', encoding = 'utf-8')
    data = csv.reader(smss, delimiter = '\t')
    for line in data:      #line:左边一个"ham" or "spam",右边一个大字符串
        if line[0] == "ham":
            classVec.append(0)
        else:
            classVec.append(1)
        wordList.append(textParse(line[1]))

    return wordList, classVec

打印数据样本,可以看到邮件文本以及它的关键词列表:

在这里插入图片描述


然后,将这些词全放在一起,构成一个“语料库”。

def doc2VecList(docList):		#docList是一个二维矩阵,每行表示一个邮件的关键词组成的列表
    """数据进行并集操作,最后返回一个词不重复的并集"""
    a = list(reduce(lambda x, y:set(x) | set(y), docList))
    return a

这么做的意义在于我们要改变这个数据样本的表示方式(否则不利于概率计算),在这里就是用词向量的表示方法:

对于一个数据样本,将其视作一个长度为 n = ∣ 语 料 库 中 词 的 个 数 ∣ n=\left | 语料库中词的个数 \right | n= 的01向量,如果样本某个词在语料库中出现了,那就在这个词的对应位置记1,否则记0.

于是该词向量就有了n个属性,每个属性取值∈{0,1}。

def words2Vec(vecList, inputWords):     #vecList:语料库,inputWords:输入的词组
    '''把单词转化为词向量'''
    dimensions = len(vecList)
    resultVec = [0] * dimensions
    for i in range(dimensions):
        if vecList[i] in inputWords:
            resultVec[i] += 1
    #转化为一维数组
    return array(resultVec)

Laplacian平滑

接下来就是计算

  1. P ( c ) P(c) P(c)
  2. P ( x ∣ c ) = ∏ i = 1 d P ( x i ∣ c ) P(\boldsymbol{x} \mid c)=\prod_{i=1}^{d}P(x_i \mid c) P(xc)=i=1dP(xic)

但这里尤其需要注意的是待分类样本词向量中可能存在“词没有记录在语料库”中的情况,也即它属于任何类别的概率值为0,显然会导致 ∏ i = 1 d P ( x i ∣ c ) \prod_{i=1}^{d}P(x_i \mid c) i=1dP(xic)直接变成0不合理,因此进行**+1的Laplacian平滑处理**:
P ^ ( c ) = ∣ D c ∣ + 1 ∣ D ∣ + N \hat{P}{(c)} = \frac{|D_c| + 1} {|D|+N} P^(c)=D+NDc+1

P ^ ( x i ∣ c ) = ∣ D c , x i ∣ + 1 ∣ D c ∣ + N i \hat{P}({x_i\mid c}) = \frac{|D_{c,x_i}| + 1} {|D_c|+N_i} P^(xic)=Dc+NiDc,xi+1

  • 也就是令没出现过的词“所属类别0和类别1的次数均+1”,“类别0的样本数和类别1的样本数均+2(类别数)”。

在代码中体现为:

'''Laplacian +1平滑'''
# 全部都初始化为1(全1数组), 防止出现概率为0的情况
p0Num = ones(numWords)
p1Num = ones(numWords)
# 相应的单词初始化为2
p0Words = 2.0
p1Words = 2.0

训练过程

:这里的训练过程跟测试集的内容无关系,而是一种预存储*。

首先,根据训练集计算从 i ∈ i\in i[1~ n ] n] n]所有 P ( x i ∣ 0 ) P(x_i \mid 0) P(xi0) P ( x i ∣ 1 ) P(x_i \mid 1) P(xi1),在这里 x i x_i xi指代的语料库中第 i i i个词,然后存到两个数组里——p0Vecp1Vec,在这里有个小技巧就是在存储时对每个P值都取了一个log运算,这样可以扩大数值域,方便后面的计算。

# 统计每个分类的词的总数
    for i in range(numTrainClass):
        if trainClass[i] == 1:
            # 数组在对应的位置上相加
            p1Num += trainMatrix[i]
            p1Words += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]
            p0Words += sum(trainMatrix[i])

    # 计算每种类型里面, 每个单词出现的概率
    # 在计算过程中,由于概率的值较小,于是取对数扩大数值域
    p0Vec = log(p0Num / p0Words)	#P(所有xi|0)
    p1Vec = log(p1Num / p1Words)	#P(所有xi|1)
    # 计算在类别中1出现的概率,0出现的概率可通过1-p得到
    pClass1 = sum(trainClass) / float(numTrainClass)	#P(c=1)

分类过程

对于测试集的词向量testVec存在如下关系:
∏ i = 1 d P ( x i ∣ 1 或 0 ) = ∏ i ∈ { i ∣ t e s t V e c [ i ] = 1 } P ( x i ∣ 1 或 0 ) \prod_{i=1}^{d}P(x_i \mid 1或0)=\prod_{i\in\{i\mid testVec[i] = 1\}}P(x_i \mid 1或0) i=1dP(xi10)=i{itestVec[i]=1}P(xi10)
也就是说,只需要testVec * p0VectestVec * p1Vec 便可分别得到在testVec第i个位置上为1 P ( x i ∣ 0 ) P(x_i \mid 0) P(xi0) P ( x i ∣ 1 ) P(x_i \mid 1) P(xi1)了!

所以只要计算 P ( c ) ∏ i ∈ { i ∣ t e s t V e c [ i ] = 1 } P ( x i ∣ 1 或 0 ) P(c)\prod_{i\in\{i\mid testVec[i] = 1\}}P(x_i \mid 1或0) P(c)i{itestVec[i]=1}P(xi10)便得到两个后验概率,同样根据对数特性 l n [ P ( c ) × P ( X 1 ∣ c ) × P ( X 2 ∣ c ) × . . . × P ( X n ∣ c ) ] = l n P ( c ) + l n P ( X 1 ∣ c ) + . . . + l n P ( X n ∣ c ) ln[{ P(c)×P(X1|c)×P(X2|c)×...×P(Xn|c)}] = lnP(c) + lnP(X1|c) + ... + lnP(Xn|c) ln[P(c)×P(X1c)×P(X2c)×...×P(Xnc)]=lnP(c)+lnP(X1c)+...+lnP(Xnc),将乘法改成加法:

def classifyNB(testVec, p0Vec, p1Vec, pClass1):
    """分类, 返回分类结果 0 or 1"""
    # 因为概率的值太小了,所以乘法改加法,可以简化计算且不失精度
    p1 = sum(testVec * p1Vec) + log(pClass1)
    p0 = sum(testVec * p0Vec) + log(1 - pClass1)
    if p0 > p1:
        return 0
    return 1

完整代码

#导入所需库文件
from numpy import *
from functools import reduce
import re
import csv

def textParse(bigString):
    '''接收一个长字符串解析成字符串列表'''
    #将特殊符号作为划分标志进行字符串切分,即非字母,非数字
    listOfTokens = re.split(r'\W+', bigString)
    #除单字母  其它单词全变成小写
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]

# 创建数据集,加载数据
adClass = 1  # 广告,垃圾标识

def loadDataSet():
    '''加载数据集合及其对应的分类'''
    classVec = []  # 0-1列表 第i个元素标识了wordList第i行类别
    wordList = []  # 提取出的词矩阵,每一行是对应一个邮件的单词列表

    smss = open("./SMSSpamCollection.txt", 'r', encoding='utf-8')
    data = csv.reader(smss, delimiter='\t')
    for line in data:  # line:左边一个"ham" or "spam",右边一个大字符串
        if line[0] == "ham":
            classVec.append(0)
        else:
            classVec.append(1)
        wordList.append(textParse(line[1]))

    return wordList, classVec

def doc2VecList(docList):
    """函数说明:数据进行并集操作,最后返回一个词不重复的并集"""
    #reduce(function, iterable[, initializer]): 从左至右积累地应用到 iterable 的条目,以便将该可迭代对象缩减为单一的值
    a = list(reduce(lambda x, y:set(x) | set(y), docList))
    return a  #['','',...,'']

def words2Vec(vecList, inputWords):     #所有词,输入的词组
    '''把单词转化为词向量'''
    dimensions = len(vecList)
    resultVec = [0] * dimensions
    for i in range(dimensions):
        if vecList[i] in inputWords:
            resultVec[i] += 1
    #转化为一维数组
    return array(resultVec)


def trainNB(trainMatrix, trainClass):
    """函数说明:计算,生成每个词对于类别上的概率"""
    # 类别行数
    numTrainClass = len(trainClass)
    # 列数
    numWords = len(trainMatrix[0])

    # 全部都初始化为1(全1数组), 防止出现概率为0的情况出现
    p0Num = ones(numWords)
    p1Num = ones(numWords)

    # 相应的单词初始化为2
    p0Words = 2.0
    p1Words = 2.0

    # 统计每个分类的词的总数
    for i in range(numTrainClass):
        if trainClass[i] == 1:
            # 数组在对应的位置上相加
            p1Num += trainMatrix[i]
            p1Words += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]
            p0Words += sum(trainMatrix[i])

    # 计算每种类型里面, 每个单词出现的概率
    # 在计算过程中,由于概率的值较小,所以我们就取对数进行比较,其中ln可替换为log的任意对数底
    p0Vec = log(p0Num / p0Words)
    p1Vec = log(p1Num / p1Words)

    # 计算在类别中1出现的概率,0出现的概率可通过1-p得到
    pClass1 = sum(trainClass) / float(numTrainClass)
    return p0Vec, p1Vec, pClass1

def classifyNB(testVec, p0Vec, p1Vec, pClass1):
    """分类, 返回分类结果 0 or 1"""
    # 因为概率的值太小了,所以乘法改加法
    # 根据对数特性ln{ P(c)×P(X1|c)×P(X2|c)×...×P(Xn|c) } = lnP(c) + lnP(X1|c) + ... + lnP(Xn|c)
    # 可以简化计算且不失精度
    '''test * pVec已经在trainNB中取过对数了直接相加'''
    p1 = sum(testVec * p1Vec) + log(pClass1)
    p0 = sum(testVec * p0Vec) + log(1 - pClass1)
    if p0 > p1:
        return 0
    return 1

def printClass(words, testClass):
    if testClass == adClass:
        print(words, '推测为:广告邮件')
    else:
        print(words, '推测为:正常邮件')


def tNB():
    # 加载训练数据集
    docList, classVec = loadDataSet()  # 单词矩阵、 01向量

    # 生成包含所有单词的list
    allWordsVec = doc2VecList(docList)

    # 构建词向量矩阵
    '''lambda中的x对应docList的每一行词组'''
    trainMat = list(map(lambda x: words2Vec(allWordsVec, x), docList))  # 和docList对应的行(词)向量组

    # 训练计算每个词在分类上的概率
    # 其中p0V:每个单词在“非”分类出现的概率, p1V:每个单词在“是”分类出现的概率  pClass1:类别中是1的概率
    p0V, p1V, pClass1 = trainNB(trainMat, classVec)

    # 测试数据集
    text1 = "As a valued cutomer, I am pleased to advise you that following recent review of your Mob No"
    testwords1 = textParse(text1)
    # 转换成单词向量,32个单词构成的数组,如果此单词在数组中,数组的项值置1
    testVec1 = words2Vec(allWordsVec, testwords1)
    # 通过将单词向量testVec代入,根据贝叶斯公式,比较各个类别的后验概率,判断当前数据的分类情况
    testClass1 = classifyNB(testVec1, p0V, p1V, pClass1)
    # 打印出测试结果
    printClass(testwords1, testClass1)

    text2 = "Please don't text me anymore. I have nothing else to say"
    testwords2 = textParse(text2)
    testVec2 = words2Vec(allWordsVec, testwords2)
    testClass2 = classifyNB(testVec2, p0V, p1V, pClass1)
    printClass(testwords2, testClass2)

if __name__ == '__main__':
    tNB()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/476261.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【小呆的力学笔记】非线性有限元的初步认识【二】

文章目录 1.2 有限元分析的数学原理1.2.1 基于最小势能原理的变分法提法1.2.1.a 弹性力学方程简化记法1.2.1.b 应变能密度和应变余能密度1.2.1.c 最小势能原理变分基础 1.2 有限元分析的数学原理 书接上回,我们已经回顾了线性有限元分析的理论基础——线弹性力学的…

TryHackMe-Lunizz CTF(boot2root)

Lunizz CTF 端口扫描 循例nmap Web枚举 进80,apache默认页面 gobuster扫一下目录 /hidden一个文件上传点, 图片上传后无权访问/hidden/uploads/ /whatever一个假的命令执行点 /instructions.txt 由 CTF_SCRIPTS_CAVE 制作(不是真实的)感谢…

如何看待人工智能技术的变革与未来?

人工智能是当今科技领域中最具前景的技术之一。从最初的逻辑推理到现在的深度学习,人工智能技术的发展已经经历了多个阶段。在本文中,我们将从技术的角度,探讨人工智能的发展历程和未来发展趋势。 一、起源和逻辑推理阶段 人工智能的起源可…

【五一创作】Java 反射

在了解反射前,我们先要知道一些相关知识 Class类 Class类的实例表示java应用运行时的类或接口,每个java类运行时都在JVM里表现为一个class对象,可通过类名.class、类型.getClass()、Class.forName("类名")等方法获取class对象。 …

关于 IO、存储、硬盘和文件系统

关于IO、存储、硬盘和文件系统 0.引入1.了解IO1.1.存储器IO1.2.设备IO 2.存储介质和存储类型2.1.内存2.2.硬盘2.3.固态硬盘(SSD)2.4.U盘 3.硬盘的工作原理3.1.磁头3.2.盘片3.3.电动机3.4.硬盘的读写操作 4.文件系统概述4.1.文件系统的类型4.2.文件系统的…

vagrant virtualbox 复制

菜鸟学习,记录一下 vagrant virtualbox 虚拟机复制。 目录 第一步,使用 virtualbox 复制虚拟机 第二步,复制 vagrant 文件 第三步,重命名相关文件夹及文件并修改配置: 第四步,注册运行复制后的虚拟机 第…

ImageJ实践——测量大小/长短(以细胞为例)

ImageJ是一款功能强大的图像处理软件。毫无疑问它在测量方面提供了十分便利的功能。下面我将以测量细胞的长短、大小(面积)为例,详细介绍ImageJ的测量操作流程。 1. ImageJ打开图像文件 在弹出的文件选择对话框中选择目标文件,即…

Spring Data Elasticsearch--ElasticsearchRestTemplate--使用/教程/实例

原文网址:Spring Data Elasticsearch--ElasticsearchRestTemplate--使用/教程/实例_IT利刃出鞘的博客-CSDN博客 简介 说明 本文用实例来介绍如何使用Spring Data Elasticsearch的ElasticsearchRestTemplate来操作ES。包括:索引的增删等、文档的增删改查…

【拓扑排序】课程表系列

文章目录 课程表(环检测算法)1. DFS2. BFS 课程表 II(拓扑序列)1. DFS2. BFS 课程表 IV(记忆化搜索)1. DFS2. BFS 课程表(环检测算法) 1. DFS 先修课程之间的关系可以用有向图表示&…

AI题目整理

1、网络配置时batchsize的大小怎样设置?过小和过大分别有什么特点? Batch size是指一次迭代过程中,输入到神经网络的样本数量。 batchsize太小的缺点: ①耗时长,训练效率低。 ②训练数据就会非常难收敛,从而导致欠拟合。 batch…

MySQL后台线程详解

前言 MySQL的服务实现通过后台多个线程、内存池、文件交互来实现对外服务的,不同线程实现不同的资源操作,各个线程相互协助,共同来完成数据库的服务。本章简单总结MySQL的一些后台线程以及主要作用。 本章收录在MySQL性能优化原理实战专栏&am…

js常见混淆加密技术

下面,我将通过一个案例来演示如何使用JavaScript混淆加密技术来保护你的网站。 假设你有一个网站,其中包含一个登录页面,该页面的JavaScript代码如下所示: function login(username, password) {if (username "admin"…

Doris(21):Doris的函数—日期函数

1 CONVERT_TZ(DATETIME dt, VARCHAR from_tz, VARCHAR to_tz) 转换datetime值dt,从 from_tz 由给定转到 to_tz 时区给出的时区,并返回的结果值。 如果参数无效该函数返回NULL。 select convert_tz(2019-08-01 13:21:03, Asia/Shanghai, America/Los_Angeles); select co…

大数据-玩转数据-初识FLINK

一、初识Flink Flink采用一只松鼠的彩色图案作为logo Apache Flink是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算。Flink被设计在所有常见的集群环境中运行,以内存执行速度和任意规模来执行计算 二、Flink的重要特点 1、事件驱动…

mysql与redis区别

一、.redis和mysql的区别总结 (1)类型上 从类型上来说,mysql是关系型数据库,redis是缓存数据库 (2)作用上 mysql用于持久化的存储数据到硬盘,功能强大,但是速度较慢 redis用于存储使…

一篇你看得懂的SNP

单核苷酸多态性,(Single Nucleotide Polymorphism,简称SNP)指的是由单个核苷酸—A,T,C或G的改变而引起的DNA序列的改变,造成包括人类在内的物种之间染色体基因组的多样性。是指在基因组上单个核苷酸的变异,…

朴素贝叶斯分类器with案例:基于SMS Spam Collection数据集的广告邮件分类

目录 贝叶斯分类器何为朴素案例:基于SMS Spam Collection数据集的广告邮件分类SMS数据集词向量表示Laplacian平滑训练过程分类过程 完整代码 贝叶斯分类器 首先要理解贝叶斯决策的理论依据,引用西瓜书上的原话:对于分类任务,在所…

(基础算法)高精度加法,高精度减法

高精度加法 什么叫做高精度加法呢?包括接下来的高精度减法,高精度乘法与除法都是同一个道理。正常来讲的话加减乘除,四则运算的数字都是整数,也就是需要在int的范围之内,但当这个操作数变得非常"大"的时候&…

《面试1v1》java反射

我是 javapub,一名 Markdown 程序员从👨‍💻,八股文种子选手。 面试官: 你好,请问你对 Java 反射有了解吗? 候选人: 是的,我了解一些。 面试官: 那你能简单…

离散数学集合论

集合论 主要内容 集合基本概念 属于、包含幂集、空集文氏图等 集合的基本运算 并、交、补、差等 集合恒等式 集合运算的算律,恒等式的证明方法 集合的基本概念 集合的定义 集合没有明确的数学定义 理解:由离散个体构成的整体称为集合&#xff0c…