本文总体架构:
一、朴素贝叶斯分类器的理论解释和计算步骤
二、代码框架展示
三、结合朴素贝叶斯理论解释代码
四、此次试验总结
本次试验主要解决对短信类型的分类问题,其实也是解决文本分类问题。
一、朴素贝叶斯分类器理论
朴素贝叶斯与贝叶斯的区别,如图
这里我们运用朴素贝叶斯分类器是因为这个算法的属性我们假设是独立的,而事实上一些文本中的词与词之间是存在联系的,我们为了简便就假设词与词之间,即属性间的关系是独立的。所以我们的后验概率表示为
我们要计算出先验概率 P(c) 和每个属性的条件概率P(xi|c),P(X)为证据因子
下面解释一下先验概率、条件概率、证据因子之间的关系,以及为什么证据因子不变的
朴素贝叶斯计算步骤
第一步计算P(c)
为了避免某个属性值在训练集没有出现,导致该类情况出现概率为0,影响判断,我们引入拉普拉斯修正,N表示训练集D中可能出现的类别
第二步计算P(xi | c)
同理引入拉普拉斯修正,Ni表示第i个属性可能取值的个数
拉普拉斯修正解释链接:https://www.cnblogs.com/misszhu-home/p/7693168.html
第三步计算出P(C|X)
第四步使用对数似然 log p(c|x),取最大值
我们根据上面的计算步骤,直接上代码解释
二、代码步骤
思路先定义各个非主函数,然后我们在定义主函数数的时候,往各个函数中传入参数,然后利用这些函数处理传入的参数,返回出我们要的数据。比如我们需要将词条转换成向量,我们就将词条传入之前定义好的函数,然后返回出向量形式的参数。
第一步先定义函数模块
1、根据词汇表将文档中的词条转换成文档向量函数
2、解析数据集中的每一行返回词条向量和短信类型函数
3、解析文件中的数据函数
第二步、定义朴素贝叶斯分类器
1、朴素贝叶斯模型训练函数
2、朴素贝叶斯分类函数
第三步、定义主函数
三、结合朴素贝叶斯理论解释代码
首先,导入下面几个库和定义编码格式、训练数据的比例
import random
import numpy as np
import matplotlib.pyplot as plt
from bayes import NaiveBayesClassifier
ENCODING = 'ISO-8859-1'#编码
TRAIN_PERCENTAGE = 0.9#训练数据所占的百分率
函数1、根据词汇表将短信文档中的词条转换成文档向量,因为计算机无法识别文字,所以我们将每条短信中的文字变成向量
words: 文档中的词条列表(一个文档) vocabulary: 总的词汇列表(里面的词是用数据集中所有的文档里面的不重复值) doc_vect: 用于贝叶斯分析的文档向量
''' 根据词汇表将文档中的词条转换成文档向量
:param words: 文档中的词条列表
:type words: list of str
:param vocabulary: 总的词汇列表
:type vocabulary: list of str
:return doc_vect: 用于贝叶斯分析的文档向量
:type doc_vect: list of int
'''
doc_vect = [0]*len(vocabulary)#产生一个列表[0,0......]vocabulery里有几个词就有几个0
#print(doc_vect)
#print(len(vocabulary))
for word in words:
if word in vocabulary:
idx = vocabulary.index(word) #index:从vocabulary 中找到word位置
doc_vect[idx] = 1 #本来列表doc_vect一开始就是全部0,然后如果word在vocabulary中,
# 则找到一个word在vocabulary中的位置,就把那个位置的0替换成1.
#print(doc_vect)
return doc_vect#返回一个列表里面可能是[0,1,1,1,1,0,1,1,1,0,1,]
函数2、解析数据集中的每一行返回词条向量和短信类型.
''' 解析数据集中的每一行返回词条向量和短信类型.
'''
cls = line.split(',')[-1].strip()#Python strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。
#注意:该方法只能删除开头或是结尾的字符,不能删除中间部分的字符。
#print("cls:",cls)
content = ','.join(line.split(',')[: -1])
#print("content:",content)
# join(): 连接字符串数组。将字符串、元组、列表中的元素以指定的字符(分隔符)连接生成一个新的字符串
word_vect = [word.lower() for word in re.split(r'\W+', content) if word]
#print("word_vect:",word_vect)
#re.split(r'\W+', content)content是一个字符串,分割content,得到每个含有word字符的一个新content列表
return word_vect, cls
cls:是每条短信所属的类型,比如垃圾短信spam、非垃圾短信ham。content:就是每条短信的内容。 word_vect:词条向量。 如下图
函数3、解析文件中的数据
''' 解析文件中的数据
'''
vocabulary, word_vects, classes = [], [], []
with open(filename, 'r', encoding=ENCODING) as f:
for line in f:
if line:
word_vect, cls = parse_line(line)
vocabulary.extend(word_vect)#extend:在vocabulary列表后面增加word_vect
word_vects.append(word_vect)#把将word_vect写进列表word_vects
classes.append(cls)#cls就是每行词条最后的那个ham和spam
vocabulary = list(set(vocabulary))# vocabulary是一个词库(不重复词的)
# set()函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,
# 还可以计算交集、差集、并集等。(s1-s2)差集
# (s1&s2)#交集
#(s1|s2)#并集
return vocabulary, word_vects, classes
这里我们对函数parse_line(line)进行了传参,传入的参数正是我们读取到的文档数据,然后函数parse_line(line)返回每条短信的词条向量和类型,将词条向量word_vect写入列表vocabulary和word_vects,将类别写入classes列表。最后列表word_vects和vocabulary中里面有文档所有词汇,将列表vocabulary中重复的词除去,制作一个词库vocabulary。
定义朴素贝叶斯分类器
导入库
from collections import defaultdict
import numpy as np
函数4、训练函数
def train(self, dataset, classes):
''' 训练朴素贝叶斯模型'''
# 按照不同类型记性分类
sub_datasets = defaultdict(lambda: [])
#相当建一个词典,但是key是待定的,而value值是空列表,即{“ ”:[]}
cls_cnt = defaultdict(lambda: 0)
#相当建一个词典,但是key是待定的,而value值是0,即{“ ”:0}
#defaultdict解释链接:https://blog.csdn.net/weixin_42160653/article/details/80297894
for doc_vect, cls in zip(dataset, classes):
sub_datasets[cls].append(doc_vect)
#sub_datasets即{spam:[[文档向量1],[文档向量1]] ham:[[文档向量3],[文档向量4]] }字典格式
cls_cnt[cls] += 1#cls_cnt是即{spam:数目,ham:数目}字典格式
#相当于统计classes中每个词出现的数量,即spam和ham的数量
# 计算类型概率 #cls_cnt是即{spam:数目,ham:数目}字典格式
cls_probs = {k: v/len(classes) for k, v in cls_cnt.items()}
#cls_cnt是即{spam:数目,ham:数目}字典格式
#用k,v去遍历cls_cnt,k即key=spam和ham,v即value=spam和ham对应的个数,然后k: v/len(classes)算出各类的占比即概率
#items()函数a = {'first':'hello','second':'world'}-->a.items()-->dict_items([(‘first’, ‘hello’), (‘second’, ‘world’)])
# 计算不同类型的条件概率
cond_probs = {}
dataset = np.array(dataset)#array函数:生成矩阵
defaultdict解释链接:https://blog.csdn.net/weixin_42160653/article/details/80297894
根据朴素贝叶斯的计算步骤
第一步:计算P(C)即cls_probs
第二步:计算P(Xi |C),P(Xi |C)=(np.sum(sub_dataset, axis=0) +1)/(np.sum(dataset) +2),运用了拉普拉斯修正
第三步:取对数,log p(Xi | C)
for cls, sub_dataset in sub_datasets.items():
#将key和value分开,循环第一遍得到cls=["spam"],sub_dataset=[[文档向量1],[文档向量2],[文档向量3].....,[文档向量n]]
#循环第二遍得到cls = ["ham"], sub_dataset = [[文档向量5], [文档向量6], [文档向量7]....., [文档向量n-1]]
#sub_datasets即{spam:[[文档向量1],[文档向量2]] ham:[[文档向量3],[文档向量4]] }字典格式
sub_dataset = np.array(sub_dataset)
#sub_dataset=['[文档向量1]' '[文档向量2]' '[文档向量3]'.....'[文档向量n]']
# Improve the classifier.
cond_prob_vect = np.log((np.sum(sub_dataset, axis=0) + 1)/(np.sum(dataset) + 2))
#运用了拉普拉斯修正,计算每个一个词在不同类型的短信中占该词在所有短信的比率,然后对这个比率取对数,将这个结果赋予cond_prob_vect
cond_probs[cls] = cond_prob_vect
#将上面的计算结果,分类,比如ham中yes这个词出现的概率和spam出现yes的概率
return cond_probs, cls_probs
我将上图的计算细节展示出来
然后我们这里返回出来的是P(C):cls_probs, log p(Xi | C)的累加:cond_prob_vect = np.log((np.sum(sub_dataset, axis=0) +1)/(np.sum(dataset) +2))
函数5、分类函数
分类函数就是计算出log P(C | X)通过前面计算出来的所有词条件概率的对数似然值,与每条短信向量相乘得出每条短信中的词对应的条件概率的对数似然值,最后乘上P(C),将计算结果最大的那个类别返回出来,因为P(x)都是一样的,所以不做处理。
''' 使用朴素贝叶斯将doc_vect进行分类.
'''
pred_probs = {}
for cls, cls_prob in cls_probs.items():#cls_probs类似{ham:cond_prob_vect,spam:cond_prob_vect}
cond_prob_vect = cond_probs[cls]
pred_probs[cls] = np.sum(cond_prob_vect*doc_vect) + np.log(cls_prob)
#计算出短信的属于spam和ham的一个似然值,返回大的那个即为我们的预测答案
return max(pred_probs, key=pred_probs.get)
第三步、主函数
主函数这里我认为
第一步:定义测试集和训练集的数据量
第二步:往各个函数里传参
第三步:拿预测类型与实际类型比较看否预测正确
第四步:计算错误率
第五步:画图
(在注释里基本已经解释了)
if '__main__' == __name__:
clf = NaiveBayesClassifier()
vocabulary, word_vects, classes = parse_file('english_big.txt')
# 训练数据 & 测试数据
ntest = int(len(classes)*(1-TRAIN_PERCENTAGE)) #测试数据的数量
test_word_vects = []
test_classes = []
for i in range(ntest):
idx = random.randint(0, len(word_vects)-1)#在(0, len(word_vects)-1)之间生成随机数
test_word_vects.append(word_vects.pop(idx))#在word_vects中移除一个idx,将移除的那个词写入test_woed_vects中
test_classes.append(classes.pop(idx))#同理
train_word_vects = word_vects#此时的word_vects已经移除了测试集的那部分了
train_classes = classes
#下面就是往上面各个定义的函数传参数,返回我们想要的值
train_dataset = [get_doc_vector(words, vocabulary) for words in train_word_vects]
# 训练贝叶斯模型
cond_probs, cls_probs = clf.train(train_dataset, train_classes)
#计算出cond_probs, cls_probs=条件概率的似然值,两种类型的概率
# 测试模型
error = 0
for test_word_vect, test_cls in zip(test_word_vects, test_classes):
test_data = get_doc_vector(test_word_vect, vocabulary)#传入数据,转化成向量
pred_cls = clf.classify(test_data, cond_probs, cls_probs)#传入向量,预测类型
if test_cls != pred_cls:#预测出来的类型与实际类型做判断
print('Predict: {} -- Actual: {}'.format(pred_cls, test_cls))
error += 1
print('Error Rate: {}'.format(error/len(test_classes)))
# 绘制不同类型的概率分布曲线
fig = plt.figure()
ax = fig.add_subplot(111)#把画布分成一行一列,然后画在从左往右从上往下的第一个
for cls, probs in cond_probs.items():
ax.scatter(np.arange(0, len(probs)),
probs*cls_probs[cls],
label=cls,
alpha=0.2)
ax.legend()
plt.show()
第六、结果展示
总结:
朴素贝叶斯除了解决文本分类问题,还能解决拼写纠错、新闻分类等等诸如此类分类问题。我们从第一二章好瓜坏瓜正例反例的二分类问题,到我们学习线性模型、决策树、神经网络、支持向量机、贝叶斯,我们一直在解决这个分类问题。从简单的一维到二维、三维我们不断在优化这个分类的方法和角度。
我们知道算法之间很难说分个高下,不是说学了神经网络,决策树这个算法就没神经网络那么好。每个算法在不同的应用场景的作用不一样,就好像我现在有自行车、汽车、飞机,可能我从北苑到南苑,自行车比飞机更好使。但是我要出国的话,自行车就没有飞机好使。决策树的超强的学习能力和泛化能力,训练速度快,适用场景在搜索排序。神经网络适用于图像处理(手写数字识别)、支持向量机适用在高维分类问题上、朴素贝叶斯则适用在文本分类(垃圾短信分类)。各个算法各有长处。
拉普拉斯修正解释链接:https://www.cnblogs.com/misszhu-home/p/7693168.html
defaultdict解释链接:https://blog.csdn.net/weixin_42160653/article/details/80297894
代码和数据文档:https://gitee.com/liu_ji_duan/DuanGe/tree/master/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/%E6%9C%B4%E7%B4%A0%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%9E%83%E5%9C%BE%E7%9F%AD%E4%BF%A1%E5%88%86%E7%B1%BB%E5%AE%9E%E7%8E%B0