朴素贝叶斯是有监督学习算法,解决的分类的问题,如客户是否流失,是否值得投资,信用登记评定,文档分类等多分类问题,下面将给出一些使用概率论分类的方法。
朴素:只做原始,最简单的假设,所有特征之前是统计独立的。
假设某个样本有a1,a2,a3,a4,.... an
个属性,则P(X) = P(a1,a2,a3,...an) = P(a1) * P(a2) * ...P(an)
朴素贝叶斯是贝叶斯理论的一部分。
假设我们有一个数据集,由两类数据组成,数据分布如下:
现在用p1(x,y) 表示数据点(x,y)属于类别1(红色原点)的概率,用p2(x, y)表示数据点(x, y)属于类别2(蓝色三角形)的概率,那么对于一个新的数据点(x, y),可以通过下面的规则来判断它的类别:
这里会选择概率较高对应的类别,这就是贝叶斯决策核心理论,即选择具有最高概率的决策。
条件概率指在事件B发生的情况下,事件A发生的概率,用P(A|B)
来表示。
若只有A,B两个事件,那么P(A|B) = P(AB) / P(B)
。
由乘法公式可得: P(AB) = P(A)P(B|A) 或 P(AB) = P(B)P(A|B)
可得条件概率公式:P(A|B) = P(A)P(B|A) / P(B)
由条件概率公式得,
先验概率: 即P(A)
,即在B发生之前,对A事件概率的有个判断。
后验概率: 即P(A|B)
,即在B发生条件下,对A的概率评估。
可能性函数: 即P(B|A) / P(B)
这是一个调整因子,使得预估概率接近真实概率。
因此条件概率可以理解为下面的式子:
后验概率 = 先验概率 * 调整因子 P(A|B) = P(A) * (P(B/A) / P(B))
上述为贝叶斯推断的含义,先预估一个先验概率,然后加上实验结果,看这个实验到底是增强了还是削弱了先验概率,因此得到更接近事实的后验概率。
如果可能性函数即调整因子 > 1
,则意味着先验概率被增强,事件A发生的概率可能性变大;
如果可能性函数 = 1
,意味着事件B对判断事件A的可能性无帮助;
如果可能性函数 < 1
,意味着先验概率被削弱,事件A的可能性变小。
朴素贝叶斯分类的原理
思想基础:对于给出的待分类项
,求解在此项出现的条件下,各个类别出现的概率,哪个最大,就认为此待分类项属于哪个类别。
定义如下:
x={a1, a2, a3, ..., am}
为一个待分类项,而a为x的一系列特征属性。C = {y1, y2, y3, ...., yn}
P(y1| x), P(y2| x), P(y3|x), ..., P(yn|x)
P(yk|x) = max{P(y1|x), P(y2|x), P(y3|x), ..., P(yn|x)},则待分类项x属于yk类别。
注意: 如何计算P(yi| x), 其中i为1...n
找到已知分类
的待分类项集合,这个集合叫做训练样本集
。
统计得到在各个类别条件下各个特征属性的概率,即
P(a1|y1), P(a2|y1), P(a3|y1), P(a4|y1), ...P(am|y1);P(a1|y2), P(a2|y2), P(a3|y2), ..., P(am|y2);... P(a1|yn), P(a2|yn), P(a3|yn), ..., P(am|yn);
根据贝叶斯定理可得
P(yi| x) = P(x|yi) * P(yi) / P(x)
又因特征属性是条件独立的,故可得
P(x|yi)P(yi) = P(a1|yi) * P(a2|yi) * P(a3|yi) * ... P(am|yi) * P(yi) = P(yi) π P(aj|yi) 其中j为1...m
示例: 门诊病人情况表
症状 | 职业 | 疾病 |
---|---|---|
打喷嚏 | 护士 | 感冒 |
打喷嚏 | 农夫 | 过敏 |
头痛 | 建筑工 | 南震荡 |
头痛 | 建筑工 | 感冒 |
打喷嚏 | 教师 | 感冒 |
头痛 | 教师 | 脑震荡 |
问:现在又来了一个病人,是打喷嚏的建筑工,他感冒的概率多大?
根据贝叶斯公式:
P(A|B) = P(AB) / P(B) = P(A)P(B|A) / P(B)
则可得:
P(感冒| 打喷嚏 * 建筑工) = P(感冒) * P(打喷嚏 * 建筑工 | 感冒) / P(打喷嚏 * 建筑工) P(感冒) = 1 / 2 # 由于A和B独立,可得P(AB|C) = P(A|C)*P(B|C)。 P(打喷嚏 * 建筑工 | 感冒) = P(打喷嚏 | 感冒)* P(建筑工 | 感冒) P(打喷嚏 | 感冒) = 2 / 3 P(建筑工 | 感冒) = 1 / 3 # 由于A和B独立 <=> P(AB)=P(A)*P(B) P(打喷嚏 * 建筑工) = P(打喷嚏) * P(建筑工) = 1 / 2 * 2 / 6 P(感冒| 打喷嚏 * 建筑工) = P(感冒) * P(打喷嚏 | 感冒) * P(建筑工 | 感冒) / P(打喷嚏 * 建筑工) = (1/2 * 2 / 3 * 1 / 3) / (1 / 2 * 2 / 6 ) = 2 / 3
以在线社区留言为例,构建一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标志为不当内容。对此问题,建立两个类别:侮辱类和非侮辱类,用1和0分别表示。
我们把文本看成单词向量或者词条向量,也就啥将句子转换为向量。首先考虑文档中出现的所有文档中的单词,在决定将那些单词纳入词汇表或者所有的词汇集合,然后必须将每一篇文档转换为词汇表的向量。
开发流程:
收集数据 准备数据: 从文本中构建词向量 分析数据: 检查词条确保解析的正确性 训练算法: 根据构建后词向量计算概率 测试算法 使用算法
示例:
import pandas as pd def load_datasets(): posting_list = [ # 切分的词条 ['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'], ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'], ['my', 'dalmation', 'is', 'so', 'cut', '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_list, class_vec def create_vocab_list(data_sets): """创建一个包含所有文档中出现的不重复词的列表。 Args: data_sets: 整理的样本数据集 returns: vocab_set: 返回的不重复的词条列表,也就是词汇表(就是所有单词放在一个列表中,去重操作) """ vocab_set = set() for d in data_sets: vocab_set = vocab_set | set(d) # 取并集 return list(vocab_set) def set_of_words_to_vec(vocab_list, input_dataset): """ 根据vocablist词汇表,将input dataset量化,向量的每个元素为1或0 Args: vocab_list : 不重复词汇列表 input_set : 切分的词条列表 returns: return_vec : 返回将词条向量化后列表。(转为1和0的列表) """ return_vec = [0] * len(vocab_list) for word in input_dataset: if word in vocab_list: return_vec[vocab_list.index(word)] = 1 else: print("the word: %s is not in my vocabulary!" % word) return return_vec if __name__ == "__main__": data_sets, classes_vec = load_datasets() # data_sets是原始的词条列表 print(data_sets, classes_vec) my_vocab_list = create_vocab_list(data_sets) # my_vocab_list是词汇表,用于将词条向量化,如果一个单词在词汇表中,那么相应位置记作为1,否则为0 print(my_vocab_list) train_mat = [] for data_set in data_sets: r = set_of_words_to_vec(my_vocab_list, data_set) train_mat.append(r) print(train_mat)
train_mat
是所有的词条向量组成的列表,它里面存放的是根据my_vocab_list
向量化的词条向量,下面编写训练函数,通过之前的条件概率公式,对每个类别计算概率,然后比较概率的大小。
P(Ci| W) = P(Ci) * P(W|Ci) / P(W) # 其中i表示类别,w表示一个向量,由多个值组成,一个向量中数值个数和词汇表词个数相同。
训练函数:
def _trainNB0(train_mat, train_category): """ 朴素贝叶斯分类器训练函数 Args: train_mat : 训练文档(一条文档就是一个留言,一个list)矩阵,即set_of_words_to_vec 返回的词条向量化后的列表 train_category : 训练类别标签向量。 returns: p0_vect: 非侮辱性的条件概率数组 [P(W0|C0), P(W1|C0), P(W2|C0), P(W3|C0), ...] p1_vect: 侮辱性的条件概率数组 [P(W0|C1), P(W1|C1), P(W2|C1), P(W3|C1), ...] p_abusive 文档属于侮辱类的概率 P(C1) """ num_train_docs = len(train_mat) # 统计文档的数目 num_words = len(train_mat[0]) # 统计每篇文档的词条数 32 p_absive = sum(train_category) / float(num_train_docs) # 计算类别为1即为侮辱类的概率P(Ci) p0_num = np.zeros(num_words) # 构造单词出现列表 p1_num = np.zeros(num_words) p0_denom = 0.0 # 整个数据集单词出现的总数 p1_denom = 0.0 for i in range(num_train_docs): # range(6) [0 1 0 1 0 1] if train_category[i] == 1: # 统计侮辱类的条件概率所需的数据 p1_num += train_mat[i] # 将侮辱类文档向量相加,即[0,1,1,....] + [0,1,1,....] + ... ->[0,2,3,...] p1_denom += sum(train_mat[i]) # 统计侮辱单词出现的总数 else: # 统计非侮辱类的条件概率所需的数据 p0_num += train_mat[i] p0_denom += sum(train_mat[i]) p0_vect = p0_num / p0_denom # 统计非侮辱类数组,每个单词出现的概率,即每个单词占非侮辱类总单词数比率, [1,2,3,5]/90->[1/90,...] p1_vect = p1_num / p1_denom return p0_vect, p1_vect, p_absive def classify(vec_classify, p0_vect, p1_vect, p_class1): """分类函数 Args: vec_classify : 待分类向量化后一条文档(留言) [0, 1, 1, 0, 0, 1] p0_vect : 是非侮辱类每个单词概率数组 p1_vect : 是侮辱类每个单词概率数组 p_class1 : 文档属于侮辱类概率 """ # vec_classify * p1_vect 将每个词与对应的概率相关联??? p1 = reduce(lambda x,y: x*y, vec_classify*p1_vect) * p_class1 # P(W|C1) * P(C1) p0 = reduce(lambda x,y: x*y, vec_classify*p0_vect) * (1.0 - p_class1) # P(W|C0) * P(C0) ,这里没有除以P(W), 因为两个式子,P(W)相同,只考虑分子。 print("p0:", p0) print("p1:", p1) if p1 > p0: return 1 else: return 0 def testing(): data_sets, classes_vec = load_datasets() my_vocab_list = create_vocab_list(data_sets) train_mat = [] for data_set in data_sets: r = set_of_words_to_vec(my_vocab_list, data_set) train_mat.append(r) p0_v,p1_v,p_ab = trainNB0(train_mat,classes_vec) test_ = ['love', 'my', 'dalmation'] test_doc = np.array(set_of_words_to_vec(my_vocab_list, test_)) if classify(test_doc, p0_v, p1_v, p_ab) == 1: print(test_, '属于侮辱类') else: print(test_, "属于非侮辱类") test_ = ['stupid', 'garbage'] test_doc = set_of_words_to_vec(my_vocab_list, test_) if classify(test_doc, p0_v, p1_v, p_ab) == 1: print(test_, "属于侮辱类") else: print(test_, "属于非侮辱类") """ p0: 0.0 p1: 0.0 ['love', 'my', 'dalmation'] 属于非侮辱类 p0: 0.0 p1: 0.0 ['stupid', 'garbage'] 属于非侮辱类 """
训练函数出现的问题:
P(W1|1) * P(W2|1) * P(W3|1)
, 如果其中一个概率为0,则最终结果为0。下溢出
的问题,由于在python中很多非常小的数字相乘,使得最后得不到正确的结果。解决办法:
将所有词的出现次数初始化为1,将分母初始化为2。(拉普拉斯平滑)
解决下溢出问题,对乘积取自然对数log。下图给出了函数f(x)
与ln(f(x))
的曲线。可以看出,它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。
示例:
def trainNB0(train_mat, train_category): """模型训练函数 Args: train_mat : 词条向量化后列表 train_category : 类别 """ num_train_docs = len(train_mat) # 总文档数 num_words = len(train_mat[0]) # 总单词数 p_absive = sum(train_category) / float(num_train_docs) # 侮辱性文档出现的概率 p0_num = np.ones(num_words) # 单词出现的次数列表,非侮辱类单词出现次数列表 p1_num = np.ones(num_words) p0_denom = 2.0 # 统计非侮辱类单词的总数,初始化为2,拉普拉斯平滑 p1_denom = 2.0 for x in range(num_train_docs): if train_category[x] == 1: p1_num += train_mat[x] # 侮辱类单词次数相加 p1_denom += sum(train_mat[x]) # 统计侮辱类所有文档的总单词数 else: p0_num += train_mat[x] p0_denom += sum(train_mat[x]) p0_vect = np.log(p0_num / p0_denom) # 以e为底取log p1_vect = np.log(p1_num / p1_denom) return p0_vect, p1_vect, p_absive def classifyNB(vec_classify, p0_vect, p1_vect, p_class1): """分类函数 将之前分类函数的乘法转换为加法: 乘法: P(Ci|W0W1W2...Wn) = P(W0W1W2...Wn | Ci) * P(Ci) / P(W0W1W2...Wn) = P(W0|Ci) * P(W1|Ci) * P(W2|Ci)...P(Wn | Ci) * P(Ci) / P(W0W1W2...Wn) 优化后加法: P(Ci|W0W1W2...Wn) = P(W0W1W2...Wn | Ci) * P(Ci) / P(W0W1W2...Wn) -> P(W0|Ci)*P(W1|Ci)....P(Wn|Ci)P(Ci) ->(取log)-> log(P(W0|Ci)) + log(P(W1|Ci)) + ... + log(P(Wn|Ci)) + log(P(Ci)) Args: vec_classify: 待分类的向量 p0_vect: 非侮辱类单词概率数组,即 [log(P(W0|C0)), log(P(W1|C0)), log(P(W2|C0)), log(P(W3|C0)), ...] p1_vect: 侮辱类单词概率数组,即 [log(P(W0|C1)), log(P(W1|C1)), log(P(W2|C1)), log(P(W3|C1)), ...] p_class1: 文档属于类别1侮辱类概率 """ p1 = sum(vec_classify * p1_vect) + np.log(p_class1) # 取log,以e为底,计算P(W|C1) * P(C1) p0 = sum(vec_classify * p0_vect) + np.log(1.0 - p_class1) if p1 > p0: return 1 else: return 0 def testing(): data_sets, classes_vec = load_datasets() my_vocab_list = create_vocab_list(data_sets) train_mat = [] for data_set in data_sets: r = set_of_words_to_vec(my_vocab_list, data_set) train_mat.append(r) # p0_v,p1_v,p_ab = _trainNB0(train_mat,classes_vec) p0_v,p1_v,p_ab = trainNB0(train_mat,classes_vec) test_ = ['love', 'my', 'dalmation'] test_doc = np.array(set_of_words_to_vec(my_vocab_list, test_)) if classifyNB(test_doc, p0_v, p1_v, p_ab) == 1: print(test_, '属于侮辱类') else: print(test_, "属于非侮辱类...") test_ = ['stupid', 'garbage'] test_doc = set_of_words_to_vec(my_vocab_list, test_) if classifyNB(test_doc, p0_v, p1_v, p_ab) == 1: print(test_, "属于侮辱类") else: print(test_, "属于非侮辱类")
参考: