1.算法概述
朴素贝叶斯是一种基于贝叶斯定理的概率图模型,适合用于分类和预测问题,和之前几章具体明确属于哪种类型的算法不同,朴素贝叶斯是通过概率来选择更大可能类别的分类器,下面用一个简单的例子来说明:
假设有一个垃圾邮件过滤器,使用朴素贝叶斯分类器来判断一封邮件是否是垃圾邮件。我们的训练数据中有两个类别:垃圾邮件(1)和非垃圾邮件(0)。以下是一些训练样本的特征和标签:
- “免费”,“优惠”,“限时”,标签:1
- “您好”,“最近好吗”,“周末过得怎么样”,标签:0
- “立刻购买”,“促销”,“折扣”,标签:1
- “刚刚出差回来”,“天气不错”,“问候”,标签:0
接下来,我们需要计算每个特征在两个类别中的概率,即 P ( X i ∣ Y = 1 ) 和 P ( X i ∣ Y = 0 ) P(X_i|Y=1) 和 P(X_i|Y=0) P(Xi∣Y=1)和P(Xi∣Y=0)。其中 X i X_i Xi 表示第 i 个特征, Y Y Y 表示邮件类别(垃圾邮件或非垃圾邮件)
为了计算这些概率,我们可以使用训练数据中的特征词频。例如,在垃圾邮件中,“免费” 出现了 3 次,“优惠” 出现了 2 次,“限时” 出现了 1 次。因此,我们可以计算:
P(免费|垃圾邮件) = 3 / (3 + 2 + 1) = 0.6
P(优惠|垃圾邮件) = 2 / (3 + 2 + 1) = 0.4
P(限时|垃圾邮件) = 1 / (3 + 2 + 1) = 0.2
对于非垃圾邮件,我们可以计算出相应的概率。
现在,给定一封邮件,我们可以通过计算每个特征的条件概率,然后根据贝叶斯定理计算邮件属于垃圾邮件的概率,如下所示:
P ( 垃圾邮件 ∣ 特征 ) = P ( 特征 ∣ 垃圾邮件 ) ∗ P ( 垃圾邮件 ) / P ( 特征 ) P(垃圾邮件|特征) = P(特征|垃圾邮件) * P(垃圾邮件) / P(特征) P(垃圾邮件∣特征)=P(特征∣垃圾邮件)∗P(垃圾邮件)/P(特征)
最后,我们可以选择具有最高概率的类别作为邮件的预测类别。
条件概率
贝叶斯定理是以概率为基础的,其中最重要的就是条件概率,条件概率的计算公式为 P ( A ∣ B ) = P ( B ∣ A ) ∗ P ( A ) / P ( B ) P(A|B) = P(B|A) * P(A) / P(B) P(A∣B)=P(B∣A)∗P(A)/P(B)其中, P ( B ∣ A ) ∗ P ( A ) = P ( A B ) P(B|A) * P(A)=P(AB) P(B∣A)∗P(A)=P(AB), P ( A ∣ B ) P(A|B) P(A∣B)表示在已知B发生的情况下,A发生的概率; P ( B ∣ A ) P(B|A) P(B∣A)表示在已知A发生的情况下,B发生的概率; P ( A ) P(A) P(A)表示A发生的概率; P ( B ) P(B) P(B)表示B发生的概率,另一种计算条件概率的方法叫贝叶斯准则,它适合根据结果计算条件 P ( A ∣ B ) = P ( B ∣ A ) ∗ P ( A ) P ( B ) P(A|B)=\frac{P(B|A)*P(A)}{P(B)} P(A∣B)=P(B)P(B∣A)∗P(A)
朴素贝叶斯文档分类
朴素贝叶斯的一般过程如下:
(1)收集数据:可以使用任何方法。本章使用RSS源
(2)准备数据:需要数值型或者布尔型数据
(3)分析数据:有大童特征时,绘制特征作用不大,此时使用直方图效果更好
(4)训练算法:计算不同的独立特征的条件概率
(5)测试算法:计算错误率
(6)使用算法:常见的朴素贝叶斯应用于文档分类。可以在任意的分类场景中使用朴
素贝叶斯分类器,不一定非要是文本
朴素贝叶斯有两个假设:独立和同等重要,前者是指一个特征或单词出现的概率与其他特征或单词无关,这也是“朴素”的由来,后者指每个特征同等重要
python文本分类
首先创建一个如下的实验样本
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
函数创建词汇的并集并用函数setOfWords2Vec
将单词转为向量表示以及显示某一个单词是否在词汇表中
def createVocabList(dataSet):
vocabSet = set([]) # 创建一个空集合
for document in dataSet:
vocabSet = vocabSet | set(document) # 使用联合运算符(|)合并两个集合
return list(vocabSet) # 将集合转换为列表并返回
def setOfWords2Vec(vocabList, inputSet):
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 # 返回向量表示
结果如下图所示
在数据预处理之后就要开始训练算法了,在前面已经将单词转换为数字,接下来就要使用这些数据来计算概率,利用上面的贝叶斯准则公式
P
(
c
i
∣
w
)
=
P
(
w
∣
c
i
)
∗
P
(
c
i
)
P
(
w
)
P(c_i|w)=\frac{P(w|c_i)*P(c_i)}{P(w)}
P(ci∣w)=P(w)P(w∣ci)∗P(ci)其中
w
w
w表示这是一个向量,
i
i
i表示类别,
P
(
c
i
)
P(c_i)
P(ci)表示某类别中文档在总文章中的概率,下面是利用trainNB0
函数
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory) / float(numTrainDocs)
p0Num = np.ones(numWords) # 将向量的所有元素初始化为 1
p1Num = np.ones(numWords) # 将向量的所有元素初始化为 1
p0Denom = 2.0 # 初始化分母
p1Denom = 2.0 # 初始化分母
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = np.log(p1Num / p1Denom) # 对数变换
p0Vect = np.log(p0Num / p0Denom) # 对数变换
return p0Vect, p1Vect, pAbusive
该函数接受训练矩阵(trainMatrix)和训练类别(trainCategory)作为输入,然后返回表示非垃圾邮件(p0Vect)和垃圾邮件(p1Vect)概率的向量,以及文档集合中垃圾邮件的概率(pAbusive),在这个函数中,我们首先计算垃圾邮件的概率(pAbusive),然后,我们初始化与非垃圾邮件(p0)和垃圾邮件(p1)相关的计数器和分母。接下来,我们遍历训练矩阵中的每个文档,根据文档类别(垃圾邮件或非垃圾邮件)更新计数器和分母。最后计算 p0Vect 和 p1Vect,并对它们进行对数变换,这些概率向量和垃圾邮件概率将用于预测新的电子邮件属于哪个类别
以下是返回变量的内部值
能发现,文档属于侮辱类的概率pAb为0.5而之后的几个变量通过验证也可以发现是正确的
在将分类器的一些缺陷改善之后(比如为防止概率为0时成绩也为0,将分母初始化改为2.0),接下来就是构建完整的分类器了,以下就是朴素贝叶斯分类函数的完整代码
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1) # 元素级乘法,然后求和
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
# 返回分类结果,1表示类别1,0表示类别0
if p1 > p0:
return 1
else:
return 0
def testingNB():
# 加载数据集并创建词汇表
listOPosts, listClasses = loadDataSet()
myVocabList = createVocabList(listOPosts)
# 将数据集转换为特征矩阵
trainMat = []
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
# 训练朴素贝叶斯分类器并获取概率向量
p0V, p1V, pAb = trainNB0(np.array(trainMat), np.array(listClasses))
# 对新的测试样本进行分类
testEntry = ['love', 'my', 'dalmation']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
testEntry = ['stupid', 'garbage']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
函数classifyNB
有四个参数,分别是要分类的向量vec2Classify和三个概率,;第二个函数testingNB
是将所有的函数封装,以方便节省时间,这个函数首先加载数据集并创建词汇表。然后,它使用trainNB0
函数来训练分类器并获取概率向量p0V
、p1V
和先验概率pAb
,最后,它使用classifyNB
函数(上一个问题中给出的函数)对新的测试样本进行分类。下面是使用testingNB
的结果图
每个词出现作为一个特征可被描述为词集模型,若一个词不止出现一次,那么这个词可能传达了额外的信息,这些信息不能仅仅通过词在文档中出现与否来理解,这被描述为词袋模型,为了适应词袋模型,对setOfWords2Vec
函数作修改,得到bagOfWords2VecMN
函数
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1
return returnVec
当遇到一个单词时,它会增加词向量中的对应值,而非把数值设置为1
2.算法示例
经过上述的操作,完整的分类器就已经构建好了,现在将它运用到电子垃圾邮件过滤中
基本流程
- 收集数据:提供文本文件
- 准备数据:将文本文件解析成词条向量
- 分析数据:检查词条确保解析的正确性
- 训练算法:使用我们之前建立的
trainNB0()
函数。 - 测试算法:使用
classifyNB()
函数,并且构建一个新的测试函数来计算文档集的错误率 - 使用算法:构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕上
准备数据
使用string.split()
方法能将文本字符串切分
但是能看到标点符号也被当成了词的一部分,于是尝试使用正则表达式来切分
能发现里面有空字符串,可以通过计算字符串长度的方法来解分,同时句子中第一个单词是大写的,借助python自带的.lower
或者.upper()
来解决
测试算法
按照上个阶段的字段分割,对目标数据进行分割以及完整的垃圾邮箱测试函数如下
def textParse(bigString): # 输入是一个大字符串,输出是一个单词列表
import re
listOfTokens = re.split(r'\W+', bigString)
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
def spamTest():
docList = []; classList = []; fullText = []
for i in range(1, 26):
wordList = textParse(open('email/spam/%d.txt' % i, encoding="ISO-8859-1").read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
wordList = textParse(open('email/ham/%d.txt' % i, encoding="ISO-8859-1").read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
vocabList = createVocabList(docList) # 创建词汇表
trainingSet = range(50); testSet = [] # 创建测试集
for i in range(10):
randIndex = int(np.random.uniform(0, len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(list(trainingSet)[randIndex])
trainMat = []; trainClasses = []
for docIndex in trainingSet: # 训练分类器(获取概率)trainNB0
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses))
errorCount = 0
for docIndex in testSet: # 对剩余的项目进行分类
wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
errorCount += 1
print("分类错误", docList[docIndex])
print('错误率为:', float(errorCount)/len(testSet))
这段代码的主要步骤如下:
- 定义文本解析函数
textParse
,用于将文本文件转换为单词列表 - 定义
spamTest
函数,首先读取 “email/spam” 和 “email/ham” 文件夹下的文本文件,将其转换为单词列表,并将这些列表添加到docList
、fullText
和classList
中 - 创建词汇表
vocabList
。 - 划分训练集和测试集
- 遍历训练集,将文档列表中的每个文档转换为词向量,并将其添加到
trainMat
和trainClasses
中 - 训练朴素贝叶斯分类器,并计算在训练集上的概率
- 遍历测试集,对每个文档进行分类,并将分类结果与实际类别进行比较。如果有误判,则将错误计数加一
- 输出错误率
结果如下
3.总结
朴素贝叶斯是一种基于贝叶斯定理和特征条件独立假设的简单概率分类算法。它广泛应用于文本分类、情感分析、垃圾邮件过滤等任务。
- 贝叶斯定理:朴素贝叶斯基于贝叶斯定理进行分类。贝叶斯定理描述了如何根据先验概率和条件概率来计算后验概率。数学表达式为:P(A|B) = P(B|A) * P(A) / P(B),其中 P(A|B) 表示在已知 B 发生的情况下 A 发生的概率,P(B|A) 表示在已知 A 发生的情况下 B 发生的概率,P(A) 表示 A 发生的概率,P(B) 表示 B 发生的概率。
- 特征条件独立假设:朴素贝叶斯算法的核心假设是特征之间相互独立,即一个特征的出现不影响其他特征的出现。这使得模型可以简化为各个特征概率的乘积。在实际应用中,这个假设往往不成立,但在很多情况下,朴素贝叶斯的性能仍然很好。
- 拉普拉斯平滑:由于数据稀疏性问题,某个类别下某些特征可能出现次数为0,这会导致概率计算结果为0。为解决这个问题,引入拉普拉斯平滑,即将每个类别下所有特征的出现次数加1,并将每个类别下所有特征的计数加1。这样,在计算概率时,分母中会出现每个类别的总实例数加上特征总数,确保概率值大于0。
- 多分类问题:对于多分类问题,朴素贝叶斯为每个类别计算后验概率,将样本分为具有最高后验概率的类别。
朴素贝叶斯算法的优点:1. 模型简单,易于实现和理解。2. 对于大规模数据集,具有较高的分类准确率和效率。3. 对缺失数据不敏感。缺点:1. 特征条件独立假设在实际应用中往往不成立,可能导致较差的性能。2. 对输入数据的表达形式敏感,需要适当的特征选择和预处理。