从机器学习的门缝开始窥了那一眼
朴素贝叶斯的一般流程
(1)收集数据:可以使用任何方法,本章使用RSS源
(2)准备数据:需要数值型或者布尔型数据
(3)分析数据:有大量特征时,绘制特征作用不大,此时使用直方图效果更好
(4)训练算法:计算不同的独立特征的条件概率
(5)测试算法:计算错误率
(6)使用算法:一些常见的的朴素贝叶斯应用是文档分类,可以在任意的分类场景中使用朴素贝尔斯分类器,不一定
非要是文本不可
朴素所定义的假设:
特征之间相互独立—>特征的出现的概率与其他特征的出现没有关系
每个特征同等重要
为了简便研究 —>实际场景中,以上两个假设往往都不成立
但是朴素贝叶斯的效果却很好
def load_data_set():
# 此套切分后的文档集合
posting_li = [['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']
]
# 词条向量对应的分类向量
class_vec = [0, 1, 0, 1, 0, 1] # 1代表侮辱性文字 ,0代表正常言论
return posting_li, class_vec
def create_vocab_li(data_set):
"""
收集所有文档的所有单词构建一个单词不重复的词汇表
"""
vocab_set = set([])
for document in data_set:
# | 求集合的并集
vocab_set = vocab_set | set(document)
return list(vocab_set)
def set_of_words2vec(vocab_li: list, input_set):
"""
将词条向量转换为数字向量
前提:词汇表包含了所有的使用到的单词(只对训练数据有效)
方法:从词汇表映射出一个等长的数字向量,数字向量元素只分别为0,1,且初始值全为0
我们从词条向量中取出单词,查找出此单词在词汇表中的位置索引,并根据此索引
将数字向量的此索引位置的元素值改为1(记录词条的单词),当整个词条向量读取完成
那么它的所有信息都记录在了数字向量中了(我们可以通过元素值为1,解析出原词条的内容)
步骤:1.从词汇表映射出一个等长的数字向量
2.将词条向量的单词信息参照词汇表映射到数字向量
最终我们得到了一个记录了词条信息的数字向量
注意:当词汇表并不包含所有的文档所有词时,我们在做词条信息映射的时候会出现词条信息缺失
"""
# 创建词汇表的映射,初始所有元素值为0
# 0---未出现
# 1---出现在词汇表中
return_vec = [0] * len(vocab_li)
# 从输入中取出词条
for word in input_set:
# 判断词条是否在词汇表
if word in vocab_li:
# 取出词汇表中word的索引,在映射表的此索引处的值改为1
# 记录出现词条在词汇表中的位置
return_vec[vocab_li.index(word)] = 1
else:
print("the word: %s is not in my Vocabulary!" % word)
return return_vec
训练器使用到了贝叶斯的公式:
p(Ci|w) = p(w|Ci)p(Ci) / p(w)
其中:p(w)可以使用全概率公式进行计算:
划分分类,通过计算每个分类中p(w)发生概率来计算总概率,因此需要做的就是
p(w) = p(C1w) + p(C2w) + p(C3w) + p(C4w) +…+ p(Cnw)
====>
p(w) = p(C1)p(w|C1) + p(C2)p(w|C2) + p(C3)p(w|C3) +… + p(Cn)p(w|Cn)
如果把w数字向量当成多个相互独立事件组成的,即w的每个元素之间相互独立,
则根据n个事件相互独立可以推广:p(w0,w1,w2,w3,w4…wn) = p(w0)p(w1)p(w2)p(w3)…p(wn)
条件概率,本身就是利用先验概率的知识来优化概率,或者说是将样本总体利用先验知识来缩小,或者说归一化新样本概率为1
如:
p(B | A) = p(AB) / p(A) = (A中B的数量) / (A的数量)
我们使用AB同时发生的概率 除以 p(A),就是在归一化p(A)为总体1,所以,以下对w进行划分的时候,
它的条件不受影响,因为条件没什么神秘的,就是用来归一化新的样本概率的
p(w|Ci) = p((w0,w1,w2,w3,w4…wn)|Ci) = p(w0|Ci)p(w1|Ci)p(w2|Ci)p(w3|Ci)…p(wn|Ci)
from numpy import *
def train_NBO(train_matrix, train_category):
"""
朴素贝叶斯分类器训练函数
:param train_matrix: 训练文档的矩阵(词条向量依照词汇表转换的数字向量组合的矩阵)
:param train_category: 词条向量对应的分类向量
:return:
"""
# 训练文档数(词条向量数--这里的词条向量是已经转化的数字向量)
num_train_docs = len(train_matrix)
# 数字向量的长度,词汇表长度,总词数
num_words = len(train_matrix[0])
# 向量求和,1--侮辱性言论词条数sum(train_category)
# 侮辱性文档数占总文档数的频率
p_abusive = sum(train_category) / float(num_train_docs)
# 创建0填充的num_words长度的向量
p0_num = zeros(num_words)
p1_num = zeros(num_words)
# 初始化
p0_demo = 0.0 # 非侮辱性文档的词汇总数
p1_demo = 0.0 # 侮辱性文档的词汇总数
for i in range(num_train_docs):
# i 是当前处理文档的索引
# train_category[i]当前文档的类别
if train_category[i] == 1:
# 将侮辱性的文档的数字向量元素对位相加到p1_num(记录侮辱文档的词汇特征的数量)
p1_num += train_matrix[i]
# sum(train_matrix[i])此文档数字向量元素的和,或者说是此文档的单词数
p1_demo += sum(train_matrix[i]) # 最终表示侮辱文档的总词数
else:
p0_num += train_matrix[i]
p0_demo += sum(train_matrix[i])
# 每一个词汇是一个特征,p1_num是侮辱性文档总的词汇特征的数字向量
# 每一个特征记录了此词汇的数量
# p1_demo侮辱性文档总词数
# p1_vect侮辱性文档中每一个词汇的频率,组成的概率向量
# 每一个元素代表:已知是侮辱性文档,单词某某出现的概率
# 整个分类词汇向量的概率和为1
p1_vect = p1_num / p1_demo # change to log()
p0_vect = p0_num / p0_demo # change to log()
return p0_vect, p1_vect, p_abusive
问题1:
一个词条向量或者说一个文档由多个词组成,假设这些词的出现是相互独立的
那么这一个词条属于侮辱文档的概率,需要计算其中每个词在侮辱文档词概率向量
中的概率的乘积(当某一词未出现的时候概率为0,最终乘积也为0)
解决方法:将所有词的出现数初始化为1,并将分母初始化为2
问题2:
下溢出,由于太多很小的数相乘造成的结果失真,或者得不到结果
解决方法:对乘积取自然对数,最终乘积结果虽然变化,但是他们的变化曲线是相同的
即他们的斜率变化是相似的,也即是两种最终结果归一化之后是基本相似的,所以不影响比较
ln(a*b) = ln(a) + ln(b)
def train_NBO(train_matrix, train_category):
"""
朴素贝叶斯分类器训练函数
:param train_matrix: 训练文档的矩阵(词条向量依照词汇表转换的数字向量组合的矩阵)
:param train_category: 词条向量对应的分类向量
:return:
"""
# 训练文档数(词条向量数--这里的词条向量是已经转化的数字向量)
num_train_docs = len(train_matrix)
# 数字向量的长度,词汇表长度,总词数
num_words = len(train_matrix[0])
# 向量求和,1--侮辱性言论词条数sum(train_category)
# 侮辱性文档数占总文档数的频率
p_abusive = sum(train_category) / float(num_train_docs)
# 创建1填充的num_words长度的向量
p0_num = ones(num_words)
p1_num = ones(num_words)
# 初始化
p0_demo = 2.0 # 非侮辱性文档的词汇总数
p1_demo = 2.0 # 侮辱性文档的词汇总数
for i in range(num_train_docs):
# i 是当前处理文档的索引
# train_category[i]当前文档的类别
if train_category[i] == 1:
# 将侮辱性的文档的数字向量元素对位相加到p1_num(记录侮辱文档的词汇特征的数量)
p1_num += train_matrix[i]
# sum(train_matrix[i])此文档数字向量元素的和,或者说是此文档的单词数
p1_demo += sum(train_matrix[i]) # 最终表示侮辱文档的总词数
else:
p0_num += train_matrix[i]
p0_demo += sum(train_matrix[i])
# 每一个词汇是一个特征,p1_num是侮辱性文档总的词汇特征的数字向量
# 每一个特征记录了此词汇的数量
# p1_demo侮辱性文档总词数
# p1_vect侮辱性文档中每一个词汇的频率,组成的概率向量
# 每一个元素代表:已知是侮辱性文档,单词某某出现的概率
p1_vect = log(p1_num / p1_demo) # change to log()
p0_vect = log(p0_num / p0_demo) # change to log()
return p0_vect, p1_vect, p_abusive
注意:
vect2classify:是带分类的词条转化的数字向量,与词汇表等长
p0_vect, p1_vect:相应分类的词汇出现的概率向量,现在已经转了自然对数
p(Ci|w) = p(w|Ci)p(Ci) / p(w)
我们对最终比较的表达式进行变形导出
首先<表达式左右取自然对数
ln(p(Ci|w))=ln(p(w|Ci)p(Ci) / p(w))
ln(p(Ci|w))=ln(p(w|Ci)) + ln(p(Ci)) + ln( p(w))
由于我们只需要比较概率映射值大小,而不需要实际的概率,所以在这里去掉难以计算的ln( p(w))
因为此时w作为已知词条,故ln( p(w))是一个常数,在不同类别中的值相同,故我们去除不影响比较
====> ln(p(w|Ci)) + ln(p(Ci))
====> ln(p(w0,w1,…wn)|Ci) + ln(p_class1)
====> sum(p1_vect) + ln(p_class1)
p1_vect本来就是概率向量并且取对数之后的结果,现在每一个元素值都是表示的是已知类的词汇的概率的对数
sum(p1_vect) + ln(p_class1)计算的值表示:当输入的待分类的词条向量的单词完全与侮辱文档中的所有单词一致时候的概率比较值的大小
而实际中我们输入的文档转化的词条向量的单词不可能一定朗阔整个侮辱文档的词汇表,所以我们需要待出现的单词进行挑选
在概率中的做法是:独立事件同时发生,概率等于每件事件概率的和,我们使用了对数之后,比较值等于出现的所有词汇的概率
对数值的和ln(a*b) = ln(a) + ln(b)
由于要使得输入词条向量的出现的词条的概率对数值保持不变,未出现的使得它变为0,然后计算他们的和,最后用于比较
方法:将输入向量同样转化为与总词汇表等长的数字向量,0表示未出现,1表示出现
根据对数:
0 * ln x = ln x ^ 0 = ln 1 = 0
1 * ln x = ln x ^ 1 = ln x
所有我们只需要将词条数字向量与我们的概率对数向量相乘,根据numpy的 向量 * 向量 等于对位元素相乘就可以实现,最后求挑
选之后的概率对数向量的和就可以得到我们想要的结果了
所以最终应该计算的表达式是:sum(vect2classify * p1_vect) + ln(p_class1)
def classifyNB(vect2classify, p0_vect, p1_vect, p_class1):
"""计算输入词汇数字向量的概率对数"""
# 计算是侮辱文档的概率对数
p1 = sum(vect2classify * p1_vect) + log(p_class1)
# 计算是正常文档的概率对数
p0 = sum(vect2classify * p0_vect) + log(1.0 - p_class1)
# 比较大小,返回分类
if p1 > p0:
return 1
else:
return 0
def testingNB():
"""测试分类器"""
# 获取训练集的词条,和分类向量
posts_li, class_li = load_data_set()
# 创建总词汇表
my_vocab_li = create_vocab_li(posts_li)
# 初始化训练矩阵
train_mat = []
for post_in_doc in posts_li:
# 构造训练矩阵
# train_mat.append(set_of_words2vec(my_vocab_li, post_in_doc))
train_mat.append(bag_of_words2vec(my_vocab_li, post_in_doc))
# 计算各分类的概率对数向量和侮辱文档的概率
p0V, p1V, pAb = train_NBO(array(train_mat), array(class_li))
# 待分类词条向量
test_entry = ['love', 'my', 'dalmation']
# 将输入词条转化为词条数字向量
# this_doc = array(set_of_words2vec(my_vocab_li, test_entry))
this_doc = array(bag_of_words2vec(my_vocab_li, test_entry))
# 输入词条数字向量,返回分类结果
print(test_entry, 'classified as: ', classifyNB(this_doc, p0V, p1V, pAb))
test_entry = ['stupid', 'garbage']
# this_doc = array(set_of_words2vec(my_vocab_li, test_entry))
this_doc = array(bag_of_words2vec(my_vocab_li, test_entry))
print(test_entry, 'classified as: ', classifyNB(this_doc, p0V, p1V, pAb))
在词集中,每个词我们只会关注是否出现,而对于出现的次数未做考虑,现在引入词袋模型,它允许单词多次出现
现在对基于词袋模型的贝叶斯来对set_of_words2vec进行修改
def bag_of_words2vec(vocab_li: list, input_set):
return_vec = [0] * len(vocab_li)
# 从输入中取出词条
for word in input_set:
# 判断词条是否在词汇表
if word in vocab_li:
# 取出词汇表中word的索引,在映射表的此索引处的值改为1
# 记录出现词条在词汇表中的位置
return_vec[vocab_li.index(word)] += 1
else:
print("the word: %s is not in my Vocabulary!" % word)
return return_vec