目录
- 机器学习 - 概率图模型 之 朴素贝叶斯算法
- 一、贝叶斯与朴素贝叶斯算法
- 1、相关概念
- 2、朴素贝叶斯分类的工作流程
- 3、朴素贝叶斯的优缺点
- 二、文本分类
- 1、one-hot 编码
- 2、TF-IDF 词频-逆文档频率
- 3、代码:基于朴素贝叶斯实现文本分类(使用 Python 手动实现)
机器学习 - 概率图模型 之 朴素贝叶斯算法
一、贝叶斯与朴素贝叶斯算法
1、相关概念
贝叶斯公式
贝叶斯公式最早是由英国神学家贝叶斯提出来的,用来描述两个条件概率之间的关系。
朴素贝叶斯算法
用示例理解朴素贝叶斯:
朴素贝叶斯的各特征间相互独立假设:
2、朴素贝叶斯分类的工作流程
3、朴素贝叶斯的优缺点
朴素贝叶斯优点
- 算法逻辑简单,易于实现(算法思路很简单,只要使用贝叶斯公式转化即可)
- 分类过程中,时空开销小(假设特征相互独立,只会涉及到二维存储)
朴素贝叶斯缺点
- 理论上,朴素贝叶斯模型与其他分类方法相比具有最小的误差率。但是实际上并非总是如此。
- 这是因为朴素贝叶斯模型假设属性之间相互独立,这个假设在实际应用中往往是不成立的,在属性个数比较多或者属性之间相关性较大时,分类效果不好(改善:可先做数据的预处理,如 PCA,以减小相关性)。
问:既然相互独立假设在实际应用中往往不成立,为何还需要朴素贝叶斯模型?
答:
-
朴素贝叶斯模型(Naive Bayesian Model,简称 NB) 的朴素(Naive)的含义是:“很简单很天真” 地假设样本特征彼此独立。
-
这个假设现实中基本上不存在,但特征相关性很小的实际情况还是很多的,所以这个模型仍然能够工作得很好。
二、文本分类
在处理文本分类之前,引入两个概念:one-hot 编码、TF-IDF
1、one-hot 编码
one-hot 表达:
- 说明:one-hot 表达是一种稀疏的表达方式。
- 特点:相互独立地表示语料中的每个词。
- 词与词在句子中的相关性被忽略了,这正符合朴素贝叶斯对文本的假设。
关于 one-hot 编码,推荐参考的文章有:
- 参考1:https://www.cnblogs.com/shuaishuaidefeizhu/p/11269257.html(文中举例子说明了英文的 one-hot 编码与中文的 one-hot 编码)
- 参考2: https://blog.csdn.net/qq_15192373/article/details/89552498(文中给出了多个样本多个特征的示例 + 代码)
2、TF-IDF 词频-逆文档频率
概念的引入:(one-hot 编码存在一定的不足,下面对其进行改进)
TF-IDF 权重策略:
TF-IDF:
3、代码:基于朴素贝叶斯实现文本分类(使用 Python 手动实现)
回顾:
- 在进行算法实现前,先来回顾一下朴素贝叶斯分类的工作流程:
- 代码实现思路:
代码:( 工具:PyCharm,基于:python3 )
import numpy as np
"""
对 朴素贝叶斯模型(Naive Bayesian Model,简称 NB) 的实现,并做文本分类
⭐ 学习时间:2023.1.4;再次梳理:2023.1.25
"""
def loadDataSet():
"""
一个给定的数据集(此处:未使用文本分词,而是直接给出了每句话分好词后的内容)
· postingList:一个二维数组(x),每一个列表(每行)代表一句话分完词后的情况;
· classVec:一维数组(y),代表某文章是否是'stupid'(1:该篇文章是;0:否)
"""
postingList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
['my', 'dalmatian', 'is', 'so', 'cute', 'I', 'love', 'him', 'my'],
['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表示该文章属于辱骂性的('stupid'),0不是
return postingList, classVec # 相当于 返回 x, y
# 定义了一个类:朴素贝叶斯模型(Naive Bayesian Model,简称 NB)
class NBayes(object):
def __init__(self): # 构造函数
self.vocabulary = [] # 词典
self.idf = []
self.tf = []
self.tdm = 0 # p(x|yi)
self.Pcates = {} # p(yi)是一个类别词典
self.labels = [] # 存放类别号
self.doclength = 0 # 文档个数(文档长度)
self.vocablen = 0 # 词典个数(词典长度)
self.testset = 0 # 测试集
def cate_prob(self, classVec):
"""
计算在数据集中每个分类的概率 p(yi)
"""
self.labels = classVec
labeltemps = set(self.labels) # 去重
for labeltemp in labeltemps: # 分别对每一种类别(分类)求概率,并赋值给 Pcates
# p(yi) = 某类别的个数/类别总个数。其中:统计列表中重复的分类 self.labels.count(labeltemp) —— 这是 python 原生的一个方法
self.Pcates[labeltemp] = float(self.labels.count(labeltemp)) / float(len(self.labels))
def calc_wordfreq(self, trainset):
"""
生成普通的词频向量(用的整型计数方式,未进行归一化,不是概率分布形式)
"""
self.tf = np.zeros([self.doclength, self.vocablen])
self.idf = np.zeros([1, self.vocablen])
for index in range(self.doclength):
for word in trainset[index]:
self.tf[index, self.vocabulary.index(word)] += 1
for signleword in set(trainset[index]):
self.idf[0, self.vocabulary.index(signleword)] += 1
def calc_tfidf(self, trainset):
"""
TF-IDF (词频-逆文档频率)【目的:生成每篇文章的向量】
"""
self.tf = np.zeros([self.doclength, self.vocablen]) # 定义 TF(文章行*词典列):初始化所有全为0,有多少篇文章就有多少行,列数为词典长度。
self.idf = np.zeros([1, self.vocablen]) # 定义 IDF(1行*词典列):1 行,词典多长就有多少列。
for index in range(self.doclength): # 遍历每篇文章,取到每篇文章的索引号
for word in trainset[index]: # 取到每篇文章所对应的词的列表(遍历每篇文章的每个词)
# 统计每篇文章中每个词所对应的在一篇文章中出现的次数。其中,self.vocabulary.index(word) 得到每个词所在词典中的索引号
self.tf[index, self.vocabulary.index(word)] += 1
# 消除不同句子长度导致的偏差。【求 TF 词频:一篇文章对应的向量(某词在文章中的出现次数) ÷ 一篇文章的长度 = 某一个给定的词语(某词)在该文章中出现的频率】
self.tf[index] = self.tf[index] / float(len(trainset[index]))
for singleword in set(trainset[index]): # 把一篇文章内容拿出来后,做去重,得到一篇文章中不同的词
# 统计整个语料库中每个词在几篇文章(文档)中出现
self.idf[0, self.vocabulary.index(singleword)] += 1 # 对一篇文章中出现不同的词进行累加,加到 idf 中
self.idf = np.log(float(self.doclength) / self.idf) # IDF:逆文档频率。分子:语料库中文章的总数;分母:包含给定词语(某一个词)的文章数目; 之后取 log
self.tf = np.multiply(self.tf, self.idf) # 求的是 TF-IDF(= TF × IDF):词频-逆文档频率。
def build_tdm(self):
"""
按分类(类别)累计向量空间的每维值:p(x|yi)
"""
self.tdm = np.zeros([len(self.Pcates), self.vocablen]) # 初始化,构建一个 类别行*词典列 的矩阵
sumlist = np.zeros([len(self.Pcates), 1]) # 类别行(有多少类就有多少行)*1列
for index in range(self.doclength): # 遍历每篇文章
# 将同一个类别的词向量空间值加和(相当于加权求和)
self.tdm[self.labels[index]] += self.tf[index] # labels[index]:某篇文章的类别;tf[index]:某篇文章的TF-IDF向量(包含每个词对应的权重)
# 统计每个分类的总值,是一个标量
sumlist[self.labels[index]] = np.sum(self.tdm[self.labels[index]])
self.tdm = self.tdm / sumlist # 生成 p(x|yi)
def train_set(self, trainset, classVec):
self.cate_prob(classVec) # 计算每个分类在数据集中的概率:p(yi)
self.doclength = len(trainset) # 语料库中文档的个数
tempset = set() # 创建空的 set 是为了去重,生成词典
# 生成词典
# 下面是 python 列表生成式,其含义为:
# ·(双重for循环)先遍历 trainset 中的每篇文章doc,再遍历 每篇doc 得到里面的词word,最后将每个word 加入 tempset 中。
# · 添加完后,set 集合(tempset)中会有所有词取值完后的结果。由于生成完后列表本身是不需要的,所以未用变量去接它。
[tempset.add(word) for doc in trainset for word in doc]
self.vocabulary = list(tempset) # 强转为 list 列表(列表带有顺序,我们可根据索引号去查询到列表中具体某个元素的值)
self.vocablen = len(self.vocabulary) # 求词典的个数
# 计算词频数据集
# self.calc_wordfreq(trainset) # 可用词频表达
self.calc_tfidf(trainset) # 也可用 TF-IDF 表达每篇文章中每个词的权重
# 按分类累计向量空间的每维值 p(x|yi)
self.build_tdm()
def map2vocab(self, testdata):
"""
将测试集映射到当前词典 【说明:此处是简化的方式,由于我们的测试集中只有一条样本,所以只计算了 count 值(词出现的次数)】
(待改进之处:训练集中计算得是 TF-IDF,测试集中也应该计算 TF-IDF)
"""
self.testset = np.zeros([1, self.vocablen])
for word in testdata: # 遍历文本中每个词,进行索引化赋值(累加:得到词在文本中出现的次数)
self.testset[0, self.vocabulary.index(word)] += 1 # self.vocabulary.index(word):找到某词在词典中的索引号
def predict(self, testset):
"""
预测分类结果,输出预测的分类类别号
"""
if np.shape(testset)[1] != self.vocablen:
# 如果向量化的测试集长度与词典不相等,退出程序
print('输入错误')
exit(0)
predvalue = 0
preclass = '' # 类别号
for tdm_vect, keyclass in zip(self.tdm, self.Pcates): # 相当于遍历字典
# 遍历每个类别
# P(x|yi) P(yi)
print(testset, testset.shape)
print(tdm_vect, len(tdm_vect))
print(self.Pcates[keyclass])
# temp 是对应位置的某个类别的一些概率乘积【P(x|yi)*P(yi)】的累加值(此处:可看成对数似然——原公式的连乘可变成添加log后的连加 又∵log是单调递增 ∴ 可不用管log,直接求累加值)
temp = np.sum(testset * tdm_vect * self.Pcates[keyclass])
# 计算最大分类值
if temp > predvalue:
predvalue = temp
preclass = keyclass
return preclass, temp
if __name__ == '__main__':
dataSet, listClasses = loadDataSet() # 加载数据,相当于 返回 x, y
nb = NBayes() # 通过构造函数构造了 nb 对象
nb.train_set(dataSet, listClasses) # 训练模型。调用对象中的 train_set 方法,并传入 x, y
nb.map2vocab(dataSet[0]) # 使用模型,传入第一句话
print(nb.predict(nb.testset)) # 预测
运行结果:
补充:下面是上述代码中涉及的部分 python 知识点梳理
-
在 NBayes 类里 train_set 函数中,
[tempset.add(word) for doc in trainset for word in doc]
属于 python 的列表生成式- 含义:
# 列表生成式: #(双重for循环)先遍历 trainset 中的每篇文章doc,再遍历 每篇doc 得到里面的词word; # 最后将每个word 加入 tempset(创建的set 集合)中。 [tempset.add(word) for doc in trainset for word in doc]
- 示例:列表生成式
- 含义:
-
在 NBayes 类里 cate_prob 函数中,统计列表中重复的分类
self.labels.count(labeltemp)
涉及 python 的一个原生方法 count()- 含义:
self.labels.count(labeltemp) # 统计列表(self.labels)中 labeltemp 类别 的个数
- 示例: count() 用于统计某个元素在列表中出现的次数
- 含义:
—— 说明:本文写于 2023.1.3、2023.1.24~1.25,文中内容基于 python3,使用工具 PyCharm
编写的代码