贝叶斯简介
贝叶斯定理是一个特别简单但是特别有用的定理,这个定理解决了生活中经常遇到的一个问题:在已知P(A|B)的情况下如何计算P(B|A),既交换条件与结果。朴素贝叶斯是在假设各个条件完全独立的情况下对贝叶斯定理的一种简化算法。在机器学习中,朴素贝叶斯是一种简单但是非常强大的线性分类器,它在垃圾邮件分类,疾病诊断中都取得了很大的成功。
以下摘一段 wikipedia 上的简介:
所谓的贝叶斯方法源于他生前为解决一个“逆概”问题写的一篇文章,而这篇文章是在他死后才由他的一位朋友发表出来的。在贝叶斯写这篇文章之前,人们已经能够计算“正向概率”,如“假设袋子里面有N个白球,M个黑球,你伸手进去摸一把,摸出黑球的概率是多大”。而一个自然而然的问题是反过来:“如果我们事先并不知道袋子里面黑白球的比例,而是闭着眼睛摸出一个(或好几个)球,观察这些取出来的球的颜色之后,那么我们可以就此对袋子里面的黑白球的比例作出什么样的推测”。这个问题,就是所谓的逆概问题。
实际上,贝叶斯当时的论文只是对这个问题的一个直接的求解尝试,并不清楚他当时是不是已经意识到这里面包含着的深刻的思想。然而后来,贝叶斯方法席卷了概率论,并将应用延伸到各个问题领域,所有需要作出概率预测的地方都可以见到贝叶斯方法的影子,特别地,贝叶斯是机器学习的核心方法之一。这背后的深刻原因在于,现实世界本身就是不确定的,人类的观察能力是有局限性的(否则有很大一部分科学就没有必要做了——设想我们能够直接观察到电子的运行,还需要对原子模型争吵不休吗?),我们日常所观察到的只是事物表面上的结果,沿用刚才那个袋子里面取球的比方,我们往往只能知道从里面取出来的球是什么颜色,而并不能直接看到袋子里面实际的情况。这个时候,我们就需要提供一个猜测,所谓猜测,当然就是不确定的,但也绝对不是两眼一抹黑瞎蒙——具体地说,我们需要做两件事情:1. 算出各种不同猜测的可能性大小。2. 算出最靠谱的猜测是什么。第一个就是计算特定猜测的后验概率,对于连续的猜测空间则是计算猜测的概率密度函数。第二个则是所谓的模型比较,模型比较如果不考虑先验概率的话就是最大似然方法。
贝叶斯公式
贝叶斯公式是怎么来的?我们还是使用 wikipedia 上的一个例子:
一所学校里面有 60% 的男生,40% 的女生。男生总是穿长裤,女生则一半穿长裤一半穿裙子。有了这些信息之后我们可以容易地计算“随机选取一个学生,他(她)穿长裤的概率和穿裙子的概率是多大”,这个就是前面说的“正向概率”的计算。然而,假设你走在校园中,迎面走来一个穿长裤的学生(很不幸的是你高度近似,你只看得见他(她)穿的是否长裤,而无法确定他(她)的性别),你能够推断出他(她)是男生的概率是多大吗?
我们来算一算:假设学校里面人的总数是 U 个。60% 的男生都穿长裤,于是我们得到了 U * P(Boy) * P(Pants|Boy) 个穿长裤的(男生)(其中 P(Boy) 是男生的概率 = 60%,这里可以简单的理解为男生的比例;P(Pants|Boy) 是条件概率,即在 Boy 这个条件下穿长裤的概率是多大,这里是 100% ,因为所有男生都穿长裤)。40% 的女生里面又有一半(50%)是穿长裤的,于是我们又得到了 U * P(Girl) * P(Pants|Girl) 个穿长裤的(女生)。加起来一共是 U * P(Boy) * P(Pants|Boy) + U * P(Girl) * P(Pants|Girl) 个穿长裤的,其中有 U * P(Girl) * P(Pants|Girl) 个女生。两者一比就是你要求的答案。
下面我们把这个答案形式化一下:我们要求的是 P(Girl|Pants),我们计算的结果是 U * P(Girl) * P(Pants|Girl) / [U * P(Boy) * P(Pants|Boy) + U * P(Girl) * P(Pants|Girl)] 。容易发现这里校园内人的总数是无关的,可以消去。于是得到
P(Girl|Pants) = P(Girl) * P(Pants|Girl) / [P(Boy) * P(Pants|Boy) + P(Girl) * P(Pants|Girl)]
把上式收缩起来,分母其实就是 P(Pants) ,分子其实就是 P(Pants, Girl) 。而这个比例很自然地就读作:在穿长裤的人( P(Pants) )里面有多少(穿长裤)的女孩( P(Pants, Girl) )。
上式中的 Pants 和 Boy/Girl 可以指代一切东西,所以其一般形式就是:
P(B|A) = P(A|B) * P(B) / [P(A|B) * P(B) + P(A|~B) * P(~B) ]
收缩起来就是:
P(B|A) = P(AB) / P(A)
其实这个就等于:
P(B|A) * P(A) = P(AB)
朴素贝叶斯
朴素贝叶斯就是在贝叶斯的基础上引入了条件独立假设。
对于贝叶斯公式P(B|A) = P(AB) / P(A)
,
从机器学习的角度来理解,我们把A理解成‘具有某些特征’,把B理解成‘类别标签’,贝叶斯方法是把计算具有某些特征的条件下属于某类别的概率转换成计算属于某类别的条件下具有某些特征。
假设某个体有n项特征(Feature),分别为F1、F2、...、Fn,当个体具有不同特征时属于不同的类别,即
P(C|F1F2...Fn) = P(F1F2...Fn|C)P(C) / P(F1F2...Fn)
。
现在假设特征F1,F2...Fn之间是相互独立的,就得到P(F1F2...Fn|C)P(C) = P(F1|C)P(F2|C) ... P(Fn|C)P(C)
,等式右边每一项都可以从统计资料中获得,由此就可以计算了具有某些特征条件下属于某个类别的概率。
用一个例子来理解机器学习中贝叶斯相关的几个概念
对于一封邮件“我司可办理正规发票”,该邮件是垃圾邮件的概率有多少。
P(属于某个分类|具有某种特征) = P(具有某种特征|属于某个分类)/P(具有某种特征)
贝叶斯
P(S|我司可办理正规发票) = P(我司可办理正规发票|S)/P(我司可办理正规发票)
朴素贝叶斯
假设“我司可办理正规发票”这几个词在邮件中的出现是相互独立的,则
P(S|我司可办理正规发票) = P(我|S)P(司|S)P(可|S)P(办理|S)P(正规|S)P(发票|S)
三种模型
- 多项式模型:考虑一个词重复出现,统计多次
- 伯努利模型:不考虑一个词重复出现,只统计一次
- 混合模型:训练时考虑一个词重复出现,预测时不考虑
平滑技术
如果训练集中没有“正规发票”这个词,测试集中有,那么预测时P(正规|S)P(发票|S)=0,于是整个概率都成0,这就需要平滑技术来处理。
这种情况很常见,因为哪怕训练集再大,也可能有未覆盖的词语。这本质上还是样本数量太少,不满足大数定律,计算出来的概率失真。平滑技术就是为了处理这样的问题。
对于伯努利模型的一种平滑算法是:
P(正规发票|S)=(出现'正规发票'的垃圾邮件的数量+1)/ 每封垃圾邮件中所有词出现的次数(不考虑重复出现的词)和+2
机器学习中朴素贝叶斯分类器的构建方法
计算每个类别中的文档数目
对每篇训练文档:
对每个类别:
如果词条出现文档中―增加该词条的计数值
增加所有词条的计数值
对每个类别:
对每个词条:
将该词条的数目除以总词条数目得到条件概率
返回每个类别的条件概率
代码实现
用朴素贝叶斯算法实现一个情分分析的分类器
def load_data(self, pos_file, neg_file):
"""载入数据"""
neg_docs = codecs.open(neg_file, 'r', 'utf-8').readlines()
pos_docs = codecs.open(pos_file, 'r', 'utf-8').readlines()
return pos_docs, neg_docs
def handle_data(self, sentence, stop_words='data/stop_words.txt'):
"""进行分词,去除停止词"""
stop_words = [word.strip() for word in open(stop_words, 'r')]
return [word.strip() for word in jieba.cut(sentence.strip()) if word and word not in stop_words]
def create_vocal_list(self, sentence_list):
"""创建词汇表,词汇表包含了所有训练样本中出现的词(不包含停止词)"""
vocab_list = set([])
for sentence in sentence_list:
vocab_list = vocab_list | set(sentence)
return list(vocab_list)
def word_to_vec(self, vocab_list, sentence):
"""把一组词转换成词向量,词向量的长度等于词汇表的长度"""
words_vec = [0]*len(vocab_list)
for word in sentence:
if word in vocab_list:
words_vec[vocab_list.index(word)] = 1 # 伯努利模型,不考虑重复出现的词,对于一条数据中的每个词,如果改词存在于词汇表中,则把词向量对应位置设为1
# words_vec[vocab_list.index(word)] += 1 # 多项式模型,考虑重复出现的词
return words_vec
def train(self, train_data, label):
num_sentence = len(train_data)
pos_each_word = np.ones(len(train_data[0])) # 统计正向文本中每个词出现的次数,默认每个词出现至少一次
neg_each_word = np.ones(len(train_data[0])) # 统计负向文本中每个词出现的次数,默认每个词出现至少一次
pos_words = 2.0 # 正向文本中所有词的总数
neg_words = 2.0 # 负向文本中所有词的总数
for i in range(num_sentence):
if label[i] == 1:
pos_each_word += train_data[i]
pos_words += sum(train_data[i])
if label[i] == 0:
neg_each_word += train_data[i]
neg_words += sum(train_data[i])
# 对每个词出现的概率取对数,这是因为大多数概率都很小,如果再对很多很小的数进行乘法操作,会下溢或得不得正确答案。
# 比如a=0.001, b=0.0003, a*b = exp(ln(a)+ln(b))
pos_each_word_prob = np.log(pos_each_word/pos_words)
neg_each_word_prob = np.log(neg_each_word/neg_words)
print(pos_each_word_prob, neg_each_word_prob, sum(label)/num_sentence)
return pos_each_word_prob, neg_each_word_prob, sum(label)/num_sentence
def classify(self, test_data, pos_each_word_prob, neg_each_word_prob, pos_prob):
p1 = sum(test_data*pos_each_word_prob) + np.log(pos_prob)
p0 = sum(test_data*neg_each_word_prob) + np.log(1-pos_prob)
if p1 > p0:
return 1, np.exp(p1), np.exp(p0)
else:
return 0, np.exp(p1), np.exp(p0)
这里对pos_each_word = np.ones(len(train_data[0]))
,p1 = sum(test_data*pos_each_word_prob) + np.log(pos_prob)
这几个地方简单解释下:
假如词汇表vocal=[.....,我,......爱......],其中包含了“我爱”两个词
正向样本中某条数据中有“我爱”,而负样本中没有“爱”这个词。如果每个词的出现次数不加一的话,则
正样本中每个词出现的概率为P正=[.......p(我)>0,........p(爱)>0,.......]
负样本中每个词出现的概率为P负=[.......p(我)>0,........p(爱)=0,.......]
现在来一个测试数据“我爱学习”,转换为词向量为test=[0,0,.....1(我),0,0.......1(爱),0,0.......]
计算“我爱学习”属于正样本的概率就是计算"我爱学习"中每个词在正样本中出现的概率的乘积,由于概率值都是很小的数,如果再对多个很小的数进行乘法操作,可能会出现下溢或得不到正确结果。解决方法就是对每个词的概率值取对数,a*b=exp(lna+lnb),这样就可以避免很多乘法操作
P(正|我爱学习) = P(正|我)P(正|爱)P(正|学习)=test*ln(P正)=[0,0,.....1(我),0,0.......1(爱),0,0.......]*[.......ln(p(我)),........ln(p(爱)),.......]=ln(p(我))+ln(p(爱))。
当计算“我爱学习”是负样本的概率时,因为负样本中没有“爱”,所以“我爱学习”属于负样本的概率就是0,这显然不合理,所以需要进行调整,让训练集中正负样本中每个词的出现次数=出现次数+1,保证最少出现一次。
如果测试数据中某个词在词汇表中不存在,比如“学习”就不存在词汇表中,当把“我爱学习”转换为词向量的时候,词向量中只有“我”和“爱”对应的位置为1,直接忽略了“学习”,也就不会出现“学习”出现的概率为0这个问题了。当然,这样计算的结果不是太准确,但是也不可能那么准确,因为训练样本不可能包含了所有词。
def predict(self, test_data):
pos_sentence_list, neg_sentence_list = self.load_data(pos_data, neg_data)
sentences = []
# 把正负样本集合到一起
for sentence in pos_sentence_list:
sentence = self.handle_data(sentence)
sentences.append(sentence)
for sentence in neg_sentence_list:
sentence = self.handle_data(sentence)
sentences.append(sentence)
label = [1] * len(pos_sentence_list) + [0] * len(neg_sentence_list) # 每个正负样本对应的类别
vocab_list = self.create_vocal_list(sentences)
train_data = []
for sentence in sentences:
sentence_vec = self.word_to_vec(vocab_list, sentence)
train_data.append(sentence_vec)
# 需要对train_data和label转换成矩阵,因为训练时要进行矩阵运算
pos_each_word_prob, neg_each_word_prob, pos_prob = self.train(np.array(train_data), np.array(label))
test_data = self.handle_data(test_data)
test_vec = self.word_to_vec(vocab_list, test_data)
pred = self.classify(np.array(test_vec), pos_each_word_prob, neg_each_word_prob, pos_prob)
return pred