目录
6.3、list.append和list.extend的区别
一、前言
朴素贝叶斯是经典的机器学习算法之一,也是为数不多的基于概率论的分类算法。对于大多数的分类算法,在所有的机器学习分类算法中,朴素贝叶斯和其他绝大多数的分类算法都不同。比如决策树,KNN,逻辑回归,支持向量机等,他们都是判别方法,也就是直接学习出特征输出Y和特征X之间的关系,要么是决策函数,要么是条件分布。但是朴素贝叶斯却是生成方法,该算法原理简单,也易于实现。
朴素贝叶斯算法是一种基于贝叶斯定理的概率统计分类方法。它被广泛用于文本分类、垃圾邮件过滤、情感分析和更多应用中。朴素贝叶斯算法的核心思想是根据特征之间的条件独立性,对样本进行分类。尽管“朴素”一词表明了对特征之间的独立性的过度简化,但在实际应用中,它表现出良好的分类性能。
二、条件概率
已知两个独立事件A和B,事件B发生的前提下,事件A发生的概率可以表示为P(A|B)。条件概率表示为P(A|B)。
条件概率的公式:
三、朴素贝叶斯分类器
3.1先验概率P(X):
先验概率是指根据以往经验和分析得到的概率。
3.2后验概率P(Y|X):
事情已发生,要求这件事情发生的原因是由某个因素引起的可能性的大小,后验分布P(Y|X)表示事件X已经发生的前提下,事件Y发生的概率,称事件X发生下事件Y的条件概率。
3.3朴素
朴素贝叶斯算法是假设各个特征之间相互独立,也是朴素这词的意思,那么贝叶斯公式中3P(X|Y)
朴素贝叶斯公式:
朴素贝叶斯分类器:朴素贝叶斯分类器(Naïve Bayes Classifier)采用了“属性条件独立性假设” ,即每个属性独立地对分类结果发生影响。为方便公式标记,不妨记P(C=c|X=x)为P(c|x),基于属性条件独立性假设,贝叶斯公式可重写为:
其中d为属性数目, 为
在第
个属性上的取值。
由于对所有类别来说 P(x)相同,因此MAP判定准则可改为:
h_n_b(x)=argmaxP(c)\prod_{i=1}^{d}P(x_i|c)
其中 和
为目标参数。
朴素贝叶斯分类器的训练器的训练过程就是基于训练集D估计类先验概率 ,并为每个属性估计条件概率
。
令 表示训练集D中第c类样本组合的集合,则类先验概率:
3.4拉普拉斯修正
若某个属性值在训练集中没有与某个类同时出现过,则训练后的模型会出现 over-fitting 现象。比如训练集中没有该样例,因此连乘式计算的概率值为0,这显然不合理。因为样本中不存在(概率为0),不代该事件一定不可能发生。所以为了避免其他属性携带的信息,被训练集中未出现的属性值“ 抹去” ,在估计概率值时通常要进行“拉普拉斯修正”。,我们要修正
的值。
令 N 表示训练集 D 中可能的类别数, 表示第i个属性可能的取值数,则贝叶斯公式可修正为:
3.5防溢出策略
条件概率乘法计算过程中,因子一般较小(均是小于1的实数)。当属性数量增多时候,会导致累乘结果下溢出的现象。
在代数中有 ln(a∗b)=ln(a)+ln(b),因此可以把条件概率累乘转化成对数累加。分类结果仅需对比概率的对数累加法运算后的数值,以确定划分的类别。
四、实验之朴素贝叶斯进行文档分类
4.1准备数据:从文本中构建词向量
函数loadDataSet()创建了一些实验样本。该函数返回的第一个变量是进行词条切分后的文档集合。这些留言文本被切分成一系列的词条集合,标点符号从文本中去掉loadDataSet( )函数返回的第二个变量是一个类别标签的集合。这里有两类,侮辱性和非侮辱性。这些文本的类别由我们人工标注,这些标注信息用于训练程序以便自动检测侮辱性留言。
# 创建实验样本
def loadDataSet():
postingList = [['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 postingList, classVec
函数createVocabList()会创建一个包含在所有文档中出现的不重复词的列表,为此使用了Python的set数据类型。将词条列表输给set构造函数,set就会返回一个不重复词表。 首先,创建一个空集合 ,然后将每篇文档返回的新词集合添加到该集合中 。操作符|用于求两个集合的并集,这也是一个按位或操作符。在数学符号表示上,按位或操作与集合求并操作使用相同记号。
# 创建不重复词的列表 ———— 词汇表
def createVocabList(dataSet):
vocabSet = set([]) # 创建一个空集
for document in dataSet:
vocabSet = vocabSet | set(document) # 创建两个集合的并集
return list(vocabSet) # 返回不重复的词条列表
获得词汇表后,使用函数setOfWords2Vec(),该函数的输入参数为词汇表及某个文档,输出的是文档向量,向量的每一元素为1或0,分别表示词汇表中的单词在输入文档中是否出现。函数首先创建一个和词汇表等长的向量,并将其元素都设置为0 。接着,遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1。
# 输出文档向量
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0] * len(vocabList) # 创建一个其中所含元素都为0的向量
for word in inputSet: # 遍历文档中的所有单词
if word in vocabList: # 如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1
returnVec[vocabList.index(word)] = 1
else:
print("单词 %s 不在词汇表中!" % word)
return returnVec
测试:
# 测试函数效果
# 创建实验样本
listPosts, listClasses = loadDataSet()
print('数据集\n', listPosts)
# 创建词汇表
myVocabList = createVocabList(listPosts)
print('词汇表:\n', myVocabList)
# 输出文档向量
print(setOfWords2Vec(myVocabList, listPosts[5]))
测试结果:
4.2 训练算法:从词向量计算概率
上面我们已经实现将一组单词转换为一组数字,接下来我们要做的是如何使用这些数字计算概率。
现在我们会使用到上面所讲得贝叶斯格式。将之前的x、y 替换为 w
表示这是一个向量,即它由多个数值组成。在这个例子中,数值个数与词汇表中的词个数相同。
p(ci|w)=p(w|ci)p(ci)/p(w)
上述公式,对每个类计算该值,然后比较这两个概率值的大小。首先可以通过类别 (侮辱性留言或非侮辱性留言)里文档数除以总的文档数来计算概率 p(ci)。接下来计算 p(w|ci),这里就要用到朴素贝叶斯假设。如果将 w展开为一个个独立特征,那么就可以将上述概率写成 p(w0,w1,w2⋅⋅⋅wn|ci)
这里假设所有词都互相独立,该假设也称作条件独立性假设,它意味着可使用 p(w0|ci)p(w1|ci)p(w2|ci)...p(wn|ci)来计算上述概率,这就极大地简化了计算的过程。
编写代码:
该函数的伪代码如下:
计算每个类别中的文档数目
对每篇训练文档:
对每个类别:
如果词条出现在文档中→ 增加该词条的计数值
增加所有词条的计数值
对每个类别:
对每个词条:
将该词条的数目除以总词条数目得到条件概率
返回每个类别的条件概率
# 朴素贝叶斯分类器训练函数
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 获得训练的文档总数
numWords = len(trainMatrix[0]) # 获得每篇文档的词总数
pAbusive = sum(trainCategory) / float(numTrainDocs) # 计算文档是侮辱类的概率
p0Num = zeros(numWords) # 创建numpy.zeros数组,初始化概率
p1Num = zeros(numWords) # 创建numpy.zeros数组,初始化概率
p0Denom = 0.0 # 初始化为0
p1Denom = 0.0 # 初始化为0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i] # 向量相加,统计侮辱类的条件概率的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i] # 向量相加,统计非侮辱类的条件概率的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Denom += sum(trainMatrix[i])
p1Vect = p1Num / p1Denom # 侮辱类,每个元素除以该类别中的总词数
p0Vect = p0Num / p0Denom # 非侮辱类,每个元素除以该类别中的总词数
return p0Vect, p1Vect, pAbusive # p0Vect非侮辱类的条件概率数组、p1Vect侮辱类的条件概率数组、pAbusive文档属于侮辱类的概率
# 测试代码
listPosts, listClasses = loadDataSet() # 创建实验样本
myVocabList = createVocabList(listPosts) # 创建词汇表
trainMat = []
for postinDoc in listPosts: # for循环使用词向量来填充trainMat列表
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V, p1V, pAb = trainNB0(trainMat, listClasses)
print('p0V:\n', p0V)
print('p1V:\n', p1V)
print('pAb:\n', pAb)
结果:
结果分析:
p0V存放的是属于类别0的各单词的条件概率,即各个单词属于非侮辱类的条件概率;
p1V存放的是属于类别1的各单词的条件概率,即各个单词属于侮辱类的条件概率。
pAb是所有侮辱类的样本占所有样本的概率,在listClasses列表[0, 1, 0, 1, 0, 1]中,
1表示侮辱类文字,0表示非侮辱类文字,既有3个侮辱类,3个非侮辱类,
所以侮辱类的概率是0.5,非侮辱类的概率是也0.5,pAb就是先验概率。
4.3 测试算法
根据现实情况修改分类器
利用朴素贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概 率,即计算 p(w0|1)p(w1|1)p(w2|1)如果其中一个概率值为0,那么最后的乘积也为0。为降低 这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。
# 解决办法:
p0Num = ones(numWords) # 创建numpy.ones数组,初始化概率
p1Num = ones(numWords) # 创建numpy.ones数组,初始化概率
p0Denom = 2.0 # 初始化为2
p1Denom = 2.0 # 初始化为2
另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积 p(w0|ci)p(w1|ci)p(w2|ci)...p(wn|ci) 时,由于大部分因子都非常小,所以程序会下溢出或者得到不正确的答案。一种解决办法是对乘积取自然对数。在代数中有 ln(a∗b)=ln(a)+ln(b),于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。下图给出函数f(x)与ln(f(x))的曲线。检查这两条曲线,就会发现它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。通过修改代码,将上述做法用到分类器中:
# 解决办法:
p1Vect = log(p1Num/p1Denom) # 使用log函数
p0Vect = log(p0Num/p0Denom)
# 改进后的朴素贝叶斯分类器训练函数
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 获得训练的文档总数
numWords = len(trainMatrix[0]) # 获得每篇文档的词总数
pAbusive = sum(trainCategory) / float(numTrainDocs) # 计算文档是侮辱类的概率
p0Num = ones(numWords) # 创建numpy.ones数组,初始化概率
p1Num = ones(numWords) # 创建numpy.ones数组,初始化概率
p0Denom = 2.0 # 初始化为2.0
p1Denom = 2.0 # 初始化为2.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i] # 向量相加,统计侮辱类的条件概率的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i] # 向量相加,统计非侮辱类的条件概率的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Denom += sum(trainMatrix[i])
p1Vect = log(p1Num / p1Denom) # 侮辱类,每个元素除以该类别中的总词数
p0Vect = log(p0Num / p0Denom) # 非侮辱类,每个元素除以该类别中的总词数
return p0Vect, p1Vect, pAbusive # p0Vect非侮辱类的条件概率数组、p1Vect侮辱类的条件概率数组、pAbusive文档属于侮辱类的概率
结果:
从运行结果可以看出,没有出现概率为0的情况了,问题得到了比较好的解决。
实现分类器:
# 朴素贝叶斯分类器分类函数
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + log(pClass1) # 元素相乘,侮辱类概率
p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1) # 非侮辱类概率
if p1 > p0:
return 1
else:
return 0
要分类的向量vec2Classify以及使用函数trainNB0()计算得到的三个概率。使用NumPy的数组来计算两个向量相乘的结果 。这里的相乘是指对应元素相乘,即先将两个向量中的第1个元素相乘,然后将第2个元素相乘,以此类推。接下来将词汇表 中所有词的对应值相加,然后将该值加到类别的对数概率上。最后,比较类别的概率返回大概率对应的类别标签。
# 测试朴素贝叶斯分类器
def testingNB():
listOPosts, listClasses = loadDataSet() # 创建实验样本
myVocabList = createVocabList(listOPosts) # 创建词汇表
trainMat = [] # 文档矩阵
for postinDoc in listOPosts: # for循环使用词向量来填充trainMat列表
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V, p1V, pAb = trainNB0(array(trainMat), array(listClasses)) # 获得概率数组及先验概率
testEntry = ['love', 'my', 'dalmation'] # 输入测试1
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
testEntry = ['stupid', 'garbage'] # 输入测试2
thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
五、实验之中文垃圾邮件过滤
5.1实验环境
pytorch 1.10.0+cpu
python3.7.5
win10+vscode
安装相关等库(注意:中文切词需要用到jieba库),提示缺哪个库就装哪个库,只需输入pip install xxx(库名)即可(换源:pip +库名+ -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
)
实验目录:
5.2导入数据集:
共有150个训练集,7个测试集
5.3实验步骤
1.从电子邮箱中收集垃圾和非垃圾邮件训练集。
2.读取全部训练集,删除其中的干扰字符,如例如【】*。、,等等,然后分词,删除长度为1的单个字。
3.统计全部训练集中词语的出现次数,截取出现最多的前N个(可以根据实际情况进行调整)
4.根据每个经过第二步预处理后垃圾邮件和非垃圾邮件内容生成特征向量,统计第三步中得到的N个词语在本邮5.件中出现的频率。
6.根据第四步中得到的特征向量和已知邮件分类创建并训练朴素叶贝斯模型。
7.读取测试邮件,参考第二步,对邮件文本进行预处理,提取特征向量。
8.使用第五步中训练好的模型,根据第六步提取的特征向量对邮件进行分类。
5.4编写实现
导包:
# -*- coding: utf-8 -*
from os import listdir
from re import sub # 字符串正则过滤
from collections import Counter # 单词计数
import numpy as np
import warnings
from jieba import cut # 切词
from sklearn.naive_bayes import MultinomialNB # 多项式朴素叶贝斯模型
from sklearn.metrics import accuracy_score # 测试
切词:
def getWordsFromFile(file_path):
"""
对文本进行切词
:param file_path: txt文本路径
:return: 用空格分词的字符串
"""
words = []
with open(file_path, encoding='utf-8') as fp:
for line in fp:
line = line.strip()
# 过滤干扰字符或者无效字符
line = sub(r'[.【】0-9、一。,!~\*]', '', line)
# 使用jieb的cut函数进行分词
line = cut(line)
# 过滤长度为1的词
line = filter(lambda word: len(word) > 1, line)
words.extend(line)
return words
读取数据集:
def getWords(file_dir):
"""
将路径下的所有文件加载
:param file_dir: 保存txt文件目录
:return: 分词后的文档列表
"""
words=[]
file_list = listdir(file_dir)
for file in file_list:
file_path = file_dir + '/' + file
words.append(getWordsFromFile(file_path))
return words
统计训练集中出现最多TOPN个单词
def getTopNWords(words,topN):
"""
获取出现次数最多的前topN个单词
:param words: 需要统计的序列
:param topN: 统计的个数
:return: 出现次数最多的前topN个单词
"""
# 因为需要对所有的文本中的单词计数,需要将allWords中的元素(子列表)合并,这里使用了列表推导式实现
freq = Counter([x for l in words for x in l])
# freq.most_common(topN) 返回 [('blue', 3), ('red', 2)] 我们取每个元素的第一个元素即可
return [w[0] for w in freq.most_common(topN)]
生成特征向量:
def getTopNWords(words,topN):
"""
获取出现次数最多的前topN个单词
:param words: 需要统计的序列
:param topN: 统计的个数
:return: 出现次数最多的前topN个单词
"""
# 因为需要对所有的文本中的单词计数,需要将allWords中的元素(子列表)合并,这里使用了列表推导式实现
freq = Counter([x for l in words for x in l])
# freq.most_common(topN) 返回 [('blue', 3), ('red', 2)] 我们取每个元素的第一个元素即可
return [w[0] for w in freq.most_common(topN)]
函数调用和模型学习:
# 获取训练数据 topN个单词和特征向量
train_words_list = getWords('data/train')
topWords = getTopNWords(train_words_list, 800)
train_features = get_feature(train_words_list, topWords)
# 获取测试数据 和特征向量
test_words_list = getWords('data/test')
test_features = get_feature(test_words_list, topWords)
# 邮箱标签,1表示垃圾邮件,0表示正常邮件
train_labels = np.array([1] * 127 + [0] * 24)
test_labels = np.array([1, 1, 1, 1, 1, 0, 0])
# 创建叶贝斯模型 ,使用已有数据进行训练
clf = MultinomialNB(fit_prior=False, alpha=0.01).fit(train_features, train_labels)
# 先验概率
print('先验概率为:', clf.class_log_prior_)
# 统计数据集中垃圾邮件和非垃圾邮件的数量
num_spam = np.sum(train_labels)
num_ham = len(train_labels) - num_spam
print('训练集中垃圾邮件数量:', num_spam)
print('训练集中非垃圾邮件数量:', num_ham)
# 统计测试集中垃圾邮件和非垃圾邮件的数量
num_spam_test = np.sum(test_labels)
num_ham_test = len(test_labels) - num_spam_test
print('测试集中垃圾邮件数量:', num_spam_test)
print('测试集中非垃圾邮件数量:', num_ham_test)
# 测试准确率
predicted_labels = clf.predict(test_features)
print('训练集精度:', clf.score(train_features, train_labels))
print('预测准确率为:', accuracy_score(test_labels, predicted_labels))
模型评估:
# 计算在不同概率阈值下的精确率和召回率
precision, recall, thresholds = precision_recall_curve(test_labels, clf.predict_proba(test_features)[:, 1])
# 计算PR曲线下的面积
area_under_curve = auc(recall, precision)
# 绘制PR曲线
plt.figure(figsize=(8, 8))
plt.plot(recall, precision, color='darkorange', lw=2, label=f'PR曲线 (AUC = {area_under_curve:.2f})')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision-Recall Curve')
plt.legend(loc='upper right')
plt.show()
运行结果:
六、知识点查缺补漏
6.1、jieba库
jieba是一个中文文本处理的Python库,主要用于中文分词。分词是将一个句子切分成一个个有意义的词语的过程,对于中文而言,这是一个重要的预处理步骤,因为中文词语之间没有像英文那样的空格来进行明确的分隔。
jieba 提供了几种分词模式,包括精确模式、全模式和搜索引擎模式。它是一个开源的项目,通过算法实现高效的中文分词,广泛应用于中文自然语言处理的各个领域,如文本挖掘、搜索引擎、信息检索等。
实验代码中使用了 jieba 库来进行中文分词,具体而言,通过 jieba.cut 方法将中文文本切分成一个个词语,然后用于构建词典和进行文本处理。
6.2、re.sub方法
re.sub() 方法是 Python 中正则表达式模块 re 提供的一个函数,用于替换字符串中的指定模式。它的基本语法如下:
re.sub(pattern, repl, string, count=0, flags=0)
pattern: 要搜索的正则表达式模式。
repl: 替换 pattern 的内容的字符串。
string: 要进行替换操作的原始字符串。
count: 可选参数,表示替换的次数。如果指定为 0 或者省略,则替换所有匹配项。
flags: 可选参数,用于控制正则表达式的匹配方式,例如是否区分大小写等。
这个方法会在 string 中搜索匹配 pattern 的部分,并用 repl 进行替换。如果 count 参数不为 0,则只替换前 count 次匹配。
在以上实验中使用了 re.sub() 方法,目的是从字符串 line 中过滤掉一些干扰字符或无效字符。
' '是一个正则表达式模式,表示要匹配的字符集合。这个集合包括了点号(.)、零宽空格(​)、反引号()、中括号(`​【oaicite:0】``)、数字(0-9)、中文字符(一)、逗号(,)、句号(。)、感叹号(!)、波浪线(~)和星号(*`)等。
' ' 是替换的字符串,即将匹配到的字符替换为空字符串,即删除这些字符。
6.3、list.append和list.extend的区别
list.append(object)
向列表中添加一个对象object。list.extend(sequence)
把一个序列seq的内容添加到列表中。