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

news2024/10/7 11:23:48

目录

  • 贝叶斯分类器
  • 何为朴素
  • 案例:基于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(X1∣c)×P(X2∣c)×...×P(Xnc)]=lnP(c)+lnP(X1∣c)+...+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/476192.html

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

相关文章

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

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

《面试1v1》java反射

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

离散数学集合论

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

【五一创作】【Midjourney】Midjourney 连续性人物创作 ② ( 获取大图和 Seed 随机种子 | 通过 seed 随机种子生成类似图像 )

文章目录 一、获取大图和 Seed 随机种子二、通过 seed 种子生成类似图像 一、获取大图和 Seed 随机种子 注意 : 一定是使用 U 按钮 , 在生成的大图的基础上 , 添加 信封 表情 , 才能获取该大图的 Seed 种子编码 ; 在上一篇博客生成图像的基础上 , 点击 U3 获取第三张图的大图 ;…

电子数据取证之宝塔面板

一、宝塔面板介绍 1、官网bt.com,是提升运维效率的服务器管理软件,支持一键WAMP/LAMP/LNMP等100多项服务器管理功能;是跨平台的软件,同时支持Windows和Linux。开源永久免费。提高工作效率,对小白比较友好。 2、怎么看服…

【网络socket编程----预备知识和UDP服务器模拟实现】

文章目录 一、预备知识1.1 理解IP地址和端口号1.2 认识TCP协议和UDP协议1.3 网络字节序1.4 socket编程接口和sockaddr结构 二、封装 UdpSocket 一、预备知识 1.1 理解IP地址和端口号 众所周知,每台主机都有一个IP地址。而主机和主机之间通信,也需要依赖…

对比学习论文阅读:CoCLR算法笔记

标题:Self-supervised Co-training for Video Representation Learning 会议:NIPS2020 论文地址:https://dl.acm.org/doi/abs/10.5555/3495724.3496201 官方代码:https://www.robots.ox.ac.uk/~vgg/research/CoCLR/ 作者单位&…

软考算法-排序篇-上

数据排序 一:故事背景二:直接插入排序2.1 概念2.2 画图表示2.3 代码实现2.4 总结提升 三:希尔排序3.1 概念3.2 画图表示3.3 代码实现3.4 总结提升 四:直接选择排序4.1 概念4.2 画图表示4.3 代码实现4.4 总结提升 五:堆…

组播PIM协议

PIM(Protocol Independent Multicast)称为协议无关组播(组播分发树)。这里的协议无关指的是与单播路由协议无关,即PIM不需要维护专门的单播路由信息。作为组播路由解决方案,它直接利用单播路由表的路由信息…

LeetCode:142. 环形链表 II

🍎道阻且长,行则将至。🍓 🌻算法,不如说它是一种思考方式🍀 算法专栏: 👉🏻123 题解目录 一、🌱[142. 环形链表 II](https://leetcode.cn/problems/linked-l…

瑞吉外卖:后台系统登录功能

文章目录 需求分析代码开发创建实体类导入返回结果类Rcontroller、service与mapperlogin.html 需求分析 点击登录按钮后,浏览器以POST方式向employee/login提交username和password,服务器经过处理后向浏览器返回某种格式的数据,其中包含&…

Java SE(十一)之异常处理(Exception)

文章目录 异常概述1.什么是异常?2.为什么要异常? 异常体系及分类1.运行时异常2.编译时异常 异常处理1.JVM默认处理方案2.try…catch…3.throw & throws(1)抛出异常throw(2)声明异常throws(3…

Android Studio制作手机App:通过手机蓝牙(Bluetooth)与STM32上的低功耗蓝牙(HC-42)连接通信,实现手机端对单片机的控制。

背景: 本文的内容是针对单片机蓝牙模块(HC-42)开发的手机App。在这之前,我想先声明一点,手机与手机间的蓝牙连接方式”与“手机与HC间的蓝牙连接方式”是不一样的。原因就是手机搭配的是“经典蓝牙”模块,…

HTML【前端基础】

目录 1.HTML 结构 1.1 HTML 标签 1.2 HTML 文件基本结构 1.3 标签层次结构 1.4 快速生成代码框架 2.HTML 常见标签 2.1 注释标签 2.2 标题标签: h1-h6 2.3 段落标签:p 2.4 换行标签: br 2.5 格式化标签 2.6 图片标签: img 2.7 超链接标签: a 2.8 表格…

Android Java 音频采集 AudioRecord

在 Android Java 应用中,一般用 AudioRecord 管理从平台的音频输入设备采集音频数据所需的资源。音频采集和音频播放密切关系,Android 系统中 Java AudioRecord 和 AudioTrack 在许多方面,都有着很高的相似性,无论是代码的目录组织…

java基础知识——25.异常

这篇文章,我们来讲一下java的异常体系 目录 1.异常概述 2 java的异常继承体系 2.1 编译时异常 2.2 运行时异常 2.3 底层原理 2.4 异常的作用 3.异常的处理方式 3.1 JVM默认的处理方式 3.2 自己处理(捕获异常) 3.2.1自己处理的4个问…

端到端NVMe?| NVMe-OF或FC-NVMe

声明 主页:元存储的博客_CSDN博客 依公开知识及经验整理,如有误请留言。 个人辛苦整理,付费内容,禁止转载。 内容摘要 前言 NVMe全称是Nonvolatile Memory Express(非易失性内存标准),在它首次…

( 字符串) 647. 回文子串 ——【Leetcode每日一题】

❓647. 回文子串 难度:中等 给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。 回文字符串 是正着读和倒过来读一样的字符串。 子字符串 是字符串中的由连续字符组成的一个序列。 具有不同开始位置或结束位置的子串,即使…

JSP 的本质原理解析:“编写的时候是JSP,心里想解读的是 java 源码“

JSP 的本质原理解析:“编写的时候是JSP,心里想解读的是 java 源码” 文章目录 JSP 的本质原理解析:"编写的时候是JSP,心里想解读的是 java 源码"每博一文案1. JSP 概述2. 第一个 JSP 程序3. JSP 的本质就是 Servlet4. J…

appium的手动安装步骤教程及appium-doctor报错解决集合

前言 相信你不少软件测试行业小伙伴应该在用npm安装appium或者是cpm安装appium途中也碰到下面一些报错吧,接下来Darren洋教你改为手动安装appium吧!整理不易,请点赞加关注后查看。 一、安装Node.js 下载地址: Previous Releases …