参考:https://blog.csdn.net/c406495762/article/details/77500679?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166962094416800182754092%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=166962094416800182754092&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-1-77500679-null-null.article_score_rank_blog&utm_term=%E9%82%AE%E4%BB%B6&spm=1018.2226.3001.4450
设X,Y为两个事件,且P(X)>0,则称 P ( X Y ) P ( X ) \frac{P(XY)}{P(X)} P(X)P(XY)为事件X已经发生的条件下事件Y发生的条件概率,记为P(Y|X),即:
P ( Y ∣ X ) = P ( X Y ) P ( X ) P(Y|X)= \frac{P(XY)}{P(X)} P(Y∣X)=P(X)P(XY)
公式:
P ( Y │ X ) = P ( Y ) P ( X ∣ Y ) P ( X ) P(Y│X)=P(Y)\frac{P(X|Y)}{ P(X)} P(Y│X)=P(Y)P(X)P(X∣Y)
上式中的P(Y)为先验概率,P(Y|X)为后验概率,P(X|Y)/P(X)为可能性函数。
解释:
先验概率(Prior probability):
在X事件发生之前,对Y事件的预先判断。
后验概率(Posterior probability):
在X事件发生之后,我们对Y事件概率的重新判断。
可能性函数(Likelyhood):
调整因子,使得预估概率更接近真实概率。
与贝叶斯的不同:
朴素贝叶斯分类器(Naïve Bayes Classifier) 采用了 “属性条件独立性假设”,即每个属性独立地对分类结果发生影响。
公式:
由于每个属性之间相互独立,得:
P ( y │ x ) = P ( y ) P ( x 1 , x 2 , . . . , x d ∣ y ) P ( x ) = P ( y ) P ( x ) ∏ i = 1 d P ( x i ∣ y ) P(y│x)=P(y)\frac{P(x_1,x_2,...,x_d|y)}{ P(x)}=\frac{P(y)}{ P(x)}\prod_{i=1}^d{P(x_i|y)} P(y│x)=P(y)P(x)P(x1,x2,...,xd∣y)=P(x)P(y)i=1∏dP(xi∣y)
导入:
在训练过程中,若某个 属性值 x i x_i xi 没有与 某个类y 同时出现过,上式中的某一个 P ( x i ∣ y ) {P(x_i|y)} P(xi∣y) 就会为0,导致最后连乘的结果也为0,致使模型无法正确分类。
公式:
令 N 表示训练集 D 中可能的类别数, N i N_i Ni 表示第i个属性可能的取值数,则贝叶斯公式可修正为:
P ( y ) = ∣ D y ∣ + 1 ∣ D ∣ + N P(y) = \frac{|D_y| + 1}{|D| + N} P(y)=∣D∣+N∣Dy∣+1
P ( x i ∣ y ) = ∣ D y , x i ∣ + 1 ∣ D ∣ + N i P(x_i|y) = \frac{|D_{y,x_i}| + 1}{|D| + N_i} P(xi∣y)=∣D∣+Ni∣Dy,xi∣+1
修正后类别概率与条件概率都不再可能等于0,使模型可以顺利进行分类。
文本特征提取有两个非常重要的模型:
1.词集模型:单词构成的集合,集合自然每个元素都只有一个,也即词集中的每个单词都只有一个。
2.词袋模型:在词集的基础上如果一个单词在文档中出现不止一次,统计其出现的次数(频数)。
两者本质上的区别: 词袋是在词集的基础上增加了频率的维度,词集只关注有和没有,词袋还要关注有几个。
数据集来源:https://github.com/Jack-kui/machine-learning/tree/2272a56dcf13c98b8a3946eede2fb4fa02cb7c4b/Naive%20Bayes/email
该邮件数据集下有两个文件夹,其中一个文件夹为spam(垃圾邮件),另一个文件夹为ham(正常邮件),这两个文件夹下各有25个txt文件。
spam(垃圾邮件)示例:
Bargains Here! Buy Phentermin 37.5 mg (K-25)
Buy Genuine Phentermin at Low Cost
VISA Accepted
30 - $130.50
60 - $219.00
90 - $292.50
120 - $366.00
180 - $513.00
ham(正常邮件)示例:
WHat is going on there?
I talked to John on email. We talked about some computer stuff that’s it.
I went bike riding in the rain, it was not that cold.
We went to the museum in SF yesterday it was $3 to get in and they had
free food. At the same time was a SF Giants game, when we got done we
had to take the train with all the Giants fans, they are 1/2 drunk.
1.创建词汇表
def createVocabList(dataSet):
'''
创建词汇表
Parameter:
dataset: 包含多个文档的数据集
Return:
vocabSet:去重词汇表
'''
vocabSet = set([])
for document in dataSet:
vocabSet = vocabSet | set(document) #取并集
return list(vocabSet)
2.词集模型
def setOfWords2Vec(vocabList, inputSet):
'''
词集模型
Parameter:
vocabList:去重词汇表
inputSet:输入的文档
Return:
returnVec:词集模型,与词汇表中位置一一对应,如存在某位置的单词则该位置=1
'''
returnVec = [0] * len(vocabList) # 建立一个长度与词汇表相同的全0向量
for word in inputSet: # 遍历句子中每个单词,如单词存在于词汇表中,
if word in vocabList: # 则将向量的对应位置=1
returnVec[vocabList.index(word)] = 1
else: print("the word: %s is not in my Vocabulary!" % word)
return returnVec
3.词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
'''
词袋模型
Parameter:
vocabList:去重词汇表
inputSet:输入的文档
Return:
returnVec:词袋模型,与词汇表中位置一一对应,如存在某位置的单词则该位置=文档中该单词个数
'''
returnVec = [0]*len(vocabList) #建立一个全0向量
for word in inputSet: #遍历句子中每个单词,如单词存在于词汇表中,
if word in vocabList: #则在向量的对应位置+1
returnVec[vocabList.index(word)] += 1
return returnVec
调试结果:
可以发现,词集模型是记录单词是否出现,而词袋模型是记录单词出现几次。
4.训练朴素贝叶斯模型
def trainNB0(trainMatrix,trainClasses):
'''
训练朴素贝叶斯模型
Parameter:
trainMatrix:每个returnVec所组成的矩阵
trainClasses:每个returnVec所对应的类别, 1:侮辱类 或 0:正常类
Return:
p0Vect:正常类中每一个词的条件概率
p1Vect:侮辱类中每一个词的条件概率
pAbusive:侮辱类占总样本概率
'''
numTrainDocs = len(trainMatrix) #总文档数
numWords = len(trainMatrix[0]) #每个文档的总字数
pAbusive = sum(trainClasses)/float(numTrainDocs) #文档属于侮辱类的概率
# 使用拉普拉斯平滑
p0Num = np.ones(numWords) #分子各单词出现数初始化为1
p1Num = np.ones(numWords)
p0Denom = 2.0 #分母总单词数初始化为类别数2
p1Denom = 2.0
for i in range(numTrainDocs): #遍历每个训练样本
if trainClasses[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
5.对测试文档进行分类
方法:
计算该文档为侮辱类的概率与该文档为正常类概率,大的概率对应类别即为该文档类别。
注意:
1.'+‘号左边从p1Vec(正常类中每一个词的条件概率)中选取该文档中的词的条件概率
2.由于log(ab) = log(a) + log(b),下面所有的’+‘实际等同于在log内部的’×’
3.原贝叶斯公式需除P(vec2Classify),但无论哪个类别该值都相同,直接比较分子大小
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
'''
对测试文档进行分类
Parameter:
vec2Classify:测试文档向量
p0Vec:正常类中每一个词的条件概率
p1Vec:侮辱类中每一个词的条件概率
pClass1:侮辱类占总样本概率
Return:
1:侮辱类
0:正常类
'''
# 计算p1(该文档为侮辱类的概率),与p0(正常类概率),大的概率对应类别即为该文档类别
# 注意:
# 1.'+'号左边从p1Vec(正常类中每一个词的条件概率)中选取该文档中的词的条件概率
# 2.由于log(ab) = log(a) + log(b),下面所有的'+'实际等同于在log内部的'×'
# 3.原贝叶斯公式需除P(vec2Classify),但无论哪个类别该值都相同,直接比较分子大小即可
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
6.字符串解析
def textParse(bigString):
'''
将字符串转换为小写字符列表
Parameter:
bigString:输入字符串
Return:
tok.lower():小写字符列表
'''
#将特殊符号作为切分标志进行字符串切分,即非字母、非数字
listOfTokens = re.split(r'\W+', bigString)
#除了单个字母,其它单词变成小写
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
7.测试分类器
可以选用词袋模型或词集模型进行测试。
def spamTest(method = 'bag'):
'''
测试朴素贝叶斯分类器,默认使用词袋模型
Parameter:
method:为 ['bag', 'set']中的一种
'bag' 为使用词袋模型
'set' 为使用词集模型
Return:
errorRate:对测试集的分类错误率
'''
if method == 'bag': #判断使用词袋模型还是词集模型
words2Vec = bagOfWords2VecMN
elif method == 'set':
words2Vec = setOfWords2Vec
docList = []
classList = []
# 分别遍历两个文件夹中25个txt文件
for i in range(1, 26):
# 读取每个垃圾邮件,将字符串转换成字符串列表
wordList = textParse(open('email/spam/%d.txt' % i, 'r').read())
# 将该列表记录加入到文档列表当中,对应类别列表添加一个1,代表对应文档为侮辱类
docList.append(wordList)
classList.append(1)
# 读取每个正常邮件,将字符串转换成字符串列表
wordList = textParse(open('email/ham/%d.txt' % i, 'r').read())
# 将该列表记录加入到文档列表当中,对应类别列表添加一个0,代表对应文档为正常类
docList.append(wordList)
classList.append(0)
# 创建去重词汇表
vocabList = createVocabList(docList)
#print(vocabList)
# 创建存储训练集的索引值的列表与测试集的索引值的列表,初始训练集索引值为所有文件的索引
trainingSet = list(range(50))
testSet = []
# 从50个邮件中,随机挑选出40个作为训练集,10个做测试集
for i in range(10):
# 随机选取索索引值,将该索引值加入测试集中,并从训练集中删除该索引值
randIndex = int(random.uniform(0, len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
# 创建训练集矩阵与训练集类别向量
trainMat = []
trainClasses = []
# 遍历训练集中每一个文档
for docIndex in trainingSet:
# 将生成的(词集模型\词袋模型)添加到训练矩阵中,并记录对应类别
trainMat.append(words2Vec(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
# 训练朴素贝叶斯模型
p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses))
# 错误分类计数器
errorCount = 0
# 遍历测试集中每一个文档
for docIndex in testSet:
# 生成该文档的(词集模型\词袋模型)
# 使用训练好的朴素贝叶斯分类器分类,记录分类错误次数
wordVector = words2Vec(vocabList, docList[docIndex])
if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
errorCount += 1
#print("分类错误的测试集:",docList[docIndex])
#print('错误率:%.2f%%' % (float(errorCount) / len(testSet) * 100))
errorRate = float(errorCount) / len(testSet) # 分类错误率
return errorRate
8.完整代码
import numpy as np
import random
import re
def createVocabList(dataSet):
'''
创建词汇表
Parameter:
dataset: 包含多个文档的数据集
Return:
vocabSet:去重词汇表
'''
vocabSet = set([])
for document in dataSet:
vocabSet = vocabSet | set(document) #取并集
return list(vocabSet)
def setOfWords2Vec(vocabList, inputSet):
'''
词集模型
Parameter:
vocabList:去重词汇表
inputSet:输入的文档
Return:
returnVec:词集模型,与词汇表中位置一一对应,如存在某位置的单词则该位置=1
'''
returnVec = [0] * len(vocabList) # 建立一个长度与词汇表相同的全0向量
for word in inputSet: # 遍历句子中每个单词,如单词存在于词汇表中,
if word in vocabList: # 则将向量的对应位置=1
returnVec[vocabList.index(word)] = 1
else: print("the word: %s is not in my Vocabulary!" % word)
#print(returnVec)
return returnVec
def bagOfWords2VecMN(vocabList, inputSet):
'''
词袋模型
Parameter:
vocabList:去重词汇表
inputSet:输入的文档
Return:
returnVec:词袋模型,与词汇表中位置一一对应,如存在某位置的单词则该位置=文档中该单词个数
'''
returnVec = [0]*len(vocabList) #建立一个全0向量
for word in inputSet: #遍历句子中每个单词,如单词存在于词汇表中,
if word in vocabList: #则在向量的对应位置+1
returnVec[vocabList.index(word)] += 1
#print(returnVec)
return returnVec
def trainNB0(trainMatrix,trainClasses):
'''
训练朴素贝叶斯模型
Parameter:
trainMatrix:每个returnVec所组成的矩阵
trainClasses:每个returnVec所对应的类别, 1:侮辱类 或 0:正常类
Return:
p0Vect:正常类中每一个词的条件概率
p1Vect:侮辱类中每一个词的条件概率
pAbusive:侮辱类占总样本概率
'''
numTrainDocs = len(trainMatrix) #总文档数
numWords = len(trainMatrix[0]) #每个文档的总字数
pAbusive = sum(trainClasses)/float(numTrainDocs) #文档属于侮辱类的概率
# 使用拉普拉斯平滑
p0Num = np.ones(numWords) #分子各单词出现数初始化为1
p1Num = np.ones(numWords)
p0Denom = 2.0 #分母总单词数初始化为类别数2
p1Denom = 2.0
for i in range(numTrainDocs): #遍历每个训练样本
if trainClasses[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)
#print(p0Vect)
return p0Vect,p1Vect,pAbusive
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
'''
对测试文档进行分类
Parameter:
vec2Classify:测试文档向量
p0Vec:正常类中每一个词的条件概率
p1Vec:侮辱类中每一个词的条件概率
pClass1:侮辱类占总样本概率
Return:
1:侮辱类
0:正常类
'''
# 计算p1(该文档为侮辱类的概率),与p0(正常类概率),大的概率对应类别即为该文档类别
# 注意:
# 1.'+'号左边从p1Vec(正常类中每一个词的条件概率)中选取该文档中的词的条件概率
# 2.由于log(ab) = log(a) + log(b),下面所有的'+'实际等同于在log内部的'×'
# 3.原贝叶斯公式需除P(vec2Classify),但无论哪个类别该值都相同,直接比较分子大小即可
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
def textParse(bigString):
'''
将字符串转换为小写字符列表
Parameter:
bigString:输入字符串
Return:
tok.lower():小写字符列表
'''
#将特殊符号作为切分标志进行字符串切分,即非字母、非数字
listOfTokens = re.split(r'\W+', bigString)
#除了单个字母,其它单词变成小写
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
def spamTest(method = 'bag'):
'''
测试朴素贝叶斯分类器,默认使用词袋模型
Parameter:
method:为 ['bag', 'set']中的一种
'bag' 为使用词袋模型
'set' 为使用词集模型
Return:
errorRate:对测试集的分类错误率
'''
if method == 'bag': #判断使用词袋模型还是词集模型
words2Vec = bagOfWords2VecMN
elif method == 'set':
words2Vec = setOfWords2Vec
docList = []
classList = []
# 分别遍历两个文件夹中25个txt文件
for i in range(1, 26):
# 读取每个垃圾邮件,将字符串转换成字符串列表
wordList = textParse(open('email/spam/%d.txt' % i, 'r').read())
# 将该列表记录加入到文档列表当中,对应类别列表添加一个1,代表对应文档为侮辱类
docList.append(wordList)
classList.append(1)
# 读取每个正常邮件,将字符串转换成字符串列表
wordList = textParse(open('email/ham/%d.txt' % i, 'r').read())
# 将该列表记录加入到文档列表当中,对应类别列表添加一个0,代表对应文档为正常类
docList.append(wordList)
classList.append(0)
# 创建去重词汇表
vocabList = createVocabList(docList)
#print(vocabList)
# 创建存储训练集的索引值的列表与测试集的索引值的列表,初始训练集索引值为所有文件的索引
trainingSet = list(range(50))
testSet = []
# 从50个邮件中,随机挑选出40个作为训练集,10个做测试集
for i in range(10):
# 随机选取索索引值,将该索引值加入测试集中,并从训练集中删除该索引值
randIndex = int(random.uniform(0, len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
# 创建训练集矩阵与训练集类别向量
trainMat = []
trainClasses = []
# 遍历训练集中每一个文档
for docIndex in trainingSet:
# 将生成的(词集模型\词袋模型)添加到训练矩阵中,并记录对应类别
trainMat.append(words2Vec(vocabList, docList[docIndex]))
trainClasses.append(classList[docIndex])
# 训练朴素贝叶斯模型
p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses))
# 错误分类计数器
errorCount = 0
# 遍历测试集中每一个文档
for docIndex in testSet:
# 生成该文档的(词集模型\词袋模型)
# 使用训练好的朴素贝叶斯分类器分类,记录分类错误次数
wordVector = words2Vec(vocabList, docList[docIndex])
if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
errorCount += 1
#print("分类错误的测试集:",docList[docIndex])
#print('错误率:%.2f%%' % (float(errorCount) / len(testSet) * 100))
errorRate = float(errorCount) / len(testSet) # 分类错误率
return errorRate
if __name__ == '__main__':
total = 100
print('使用词袋模型训练:')
sum_bag_error = 0
for i in range(total):
sum_bag_error += spamTest(method = 'bag')
print('使用词袋模型训练' + str(total) + '次得到的平均错误率为: ' + str((sum_bag_error / total)))
print('--------------------------')
print('使用词集模型训练:')
sum_set_error = 0
for i in range(total):
sum_set_error += spamTest(method = 'set')
print('使用词集模型训练' + str(total) + '次得到的平均错误率为: ' + str((sum_set_error / total)))
结果分析:
使用当前的邮件数据集,在分别都训练100次后,词集模型的平均错误率略低于词袋模型,效果稍好一些。
不足:
如能做出中文的垃圾邮件分类更好。
链接: https://pan.baidu.com/s/1_1zr0x4sfaAOjhWrHNf7AA?pwd=wg5b
提取码: wg5b