首先要明确的一点是朴素贝叶斯属于生成式模型,指导思想是贝叶斯公式。
假设现在有一些评论数据,需要识别出这篇文本属于正向评论还是负面评论,也就是对文本进行分类。用数学语言描述就是: 假设已经有分好类的N篇文档:(d1,c1)、(d2,c2)、(d3,c3)……(dn,cn),di表示第i篇文档,ci表示第i个类别。目标是:寻找一个分类器,这个分类器能够:当丢给它一篇新文档d,它就输出d(最有可能)属于哪个类别c。
文本分类需要寻找文本的特征。而词袋模型就是表示文本特征的一种方式。词袋模型只考虑一篇文档中单词出现的频率(次数),用每个单词出现的频率作为文档的特征。
朴素贝叶斯分类器是一个概率分类器。假设现有的类别C={c1,c2,……cm}。给定一篇文档d,文档d最有可能属于哪个类呢?这个问题用数学公式表示如下:
\hat{c}=\underset{c \in C}{\operatorname{argmax}} P(c | d)
c^ 就是:在所有的类别C={c1,c2,……cm} 中,使得:条件概率P(c|d)取最大值的类别。使用贝叶斯公式,将上式转换成如下形式:
\hat{c}=\underset{c \in C}{\operatorname{argmax}} P(c | d)=\underset{c \in C}{\operatorname{argmax}} \frac{P(d | c) P(c)}{P(d)}
对类别C中的每个类型,计算 (p(d|c)* p(c))/p(d) 的值,然后选取最大值对应的那个类型ci ,该ci就是最优解c^,因此,可以忽略掉分母 p(d),可以变成如下形式:
\hat{c}=\underset{c \in C}{\operatorname{argmax}} P(c | d)=\underset{c \in C}{\operatorname{argmax}} P(d | c) P(c)
这个公式由两部分组成,前面那部分P(d|c) 称为似然函数,后面那部分P(c) 称为先验概率。 前面提到使用词袋模型来表示 文档d,文档d的每个特征表示为:d={f1,f2,f3……fn},那么这里的特征fi 其实就是单词wi 出现的频率(次数),因此可以转化成如下形式:
\hat{c}=\underset{c \in C}{\operatorname{argmax}} \overbrace{P\left(f_{1}, f_{2}, \ldots, f_{n} | c\right)}^{\text { likelihood }} \overbrace{P(c)}^{\text { prior }}
对文档d 做个假设:假设各个特征之间是相互独立的。那么p(f1,f2……fn|c)=p(f1|c) p(f2|c) …… * p(fn|c),转化成如下形式:
c_{N B}=\underset{c \in C}{\operatorname{argmax}} P(c) \prod_{f \in F} P(f | c)
由于每个概率值很小(比如0.0001)若干个很小的概率值直接相乘,得到的结果会越来越小。为了避免计算过程出现下溢(underflower),引入对数函数Log,在 log space中进行计算。然后使用词袋模型的每个单词wi 出现频率作为特征,得到如下公式:
c_{N B}=\underset{c \in C}{\operatorname{largmax}} \log P(c)+\sum_{i \in \text {positions}} \log P\left(w_{i} | c\right)
训练朴素贝叶斯的过程其实就是计算先验概率和似然函数的过程。 ①先验概率P(c)的计算 P(c)的意思是:在所有的文档中,类别为c的文档出现的概率有多大?假设训练数据中一共有Ndoc篇文档,只要数一下类别c的文档有多少个就能计算p(c)了,类别c的文档共有Nc篇,先验概率的计算公式如下:
\hat{P}(c)=\frac{N_{c}}{N_{d o c}}
先验概率其实就是准备干一件事情时,目前已经掌握了哪些信息了。 ②似然函数P(wi|c)的计算 由于是用词袋模型表示一篇文档d,对于文档d中的每个单词wi,找到训练数据集中所有类别为c的文档,数一数 单词wi在这些文档(类别为c)中出现的次数:count(wi,c), 然后,再数一数训练数据集中类别为c的文档一共有多少个单词,计算二者之间的比值,就是似然函数的值。似然函数计算公式如下:
\hat{P}\left(w_{i} | c\right)=\frac{\operatorname{count}\left(w_{i}, c\right)}{\sum_{w \in V} \operatorname{count}(w, c)}
其中V,就是词库。(有些单词在词库中,但是不属于类别C,那么 count(w,c)=0)
假设只考虑文本二分类:将文档分成 positve类别,或者negative类别,C={positive, negative}。 在训练数据集中,类别为positive的所有文档 都没有 包含 单词wi = fantastic(fantastic可能出现在类别为negative的文档中)那么count(wi=fantastic,ci=positive)=0 。那么:
而注意到前面公式五中的累乘,整篇文档的似然函数值为0,也就是说:如果文档d中有个单词fantastic在类别为c的训练数据集文档中从未出现过,那文档d被分类到类别c的概率为0,尽管文档d中还有一些其他单词(特征),而这些单词所代表的特征认为文档d应该被分类到类别c中。 解决方案就是 add-one smoothing。似然函数公式变成如下形式:
\hat{P}\left(w_{i} | c\right)=\frac{\operatorname{count}\left(w_{i}, c\right)+1}{\sum_{w \in V}(\operatorname{count}(w, c)+1)}=\frac{\operatorname{count}\left(w_{i}, c\right)+1}{\left(\sum_{w \in V} \operatorname{count}(w, c)\right)+|V|}
假设训练数据集有五篇文档,其中Negative类别的文档有三篇,用符号 ‘-‘ 标识;Positive类别的文档有二篇,用符号 ‘+’ 标识,它们的内容如下:
- just plain boring
- entirely predictable and lacks energy
- no surprises and very few laughs
+ very powerful
+ the most fun film of the summer
测试数据集T 有一篇文档dt,内容如下:
predictable with no fun
朴素贝叶斯分类器会把“predictable with no fun”归为哪个类呢?根据训练朴素贝叶斯分类器的过程,需要计算先验概率和似然函数。
由于训练数据集中一共有5篇文档,其中类别 ‘+’ 的文档有2篇,类别为 ‘-‘ 的文档有3篇,因此先验概率:P(c)=P(‘-‘)=Nc/Ndoc=3/5=0.6
类别为’+’ 的文档有2篇,故 P(c)=P(‘+’)=Nc/Ndoc=2/5=0.4
对测试数据集文档dt中的每个单词,似然函数采用“add-one smoothing”处理,计算相应的似然概率:
首先单词 predictable 在训练数据集中 类别为’-‘的文档中只出现了1次,类别为’-‘的文档一共有14个单词,训练数据集中两种类型的文档加起来一共有23个单词,但是有三个单词(and、
very、the)重复出现了两次,故词库V的大小为 20。因此单词predictable对应的似然概率如下:
p(predictable|’-‘)=(1+1)/(14+20)=2/34
同理:p(predictable|’+’)=(0+1)/(9+20)=1/29 (predictable没有在类别为’+’的训练数据集中出现过)
类似地:p(no|’-‘)=(1+1)/(14+20) p(no|’+’)=(0+1)/(9+20)
p(fun|’-‘)=(0+1)/(14+20) p(fun|’+’)=(1+1)/(9+20)
因此,测试集中的文档d归类为 ‘-‘ 的概率为:0.6 (221)/343 = 6.110-5
测试集中的文档d归类为 ‘+’ 的概率为:0.4(112)/293 =3.210-5
比较上面两个概率的大小,就可以知道将“predictable with no fun”归为 ‘-‘ 类别。
import numpy as np
def loaddata():
"""
:return: 文本数据集 和 对应的 label
"""
text=[['just','plain','boring'],
['entirely','predictable','and','lacks','energy'],
['no','surprises','and','very','few','laughs'],
['very','powerful'],
['the' ,'most', 'fun' ,'film' ,'of' ,'the', 'summer']]
label=[0,0,0,1,1]
return text,label
def createVocabList(text):
"""
:param text: 文本数据集
:return: 词语表
"""
vocabSet=set([])
for document in text:
vocabSet=vocabSet|set(document)
return list(vocabSet)
def bag_words_vec(vocab, text):
"""
:param vocab: 词表
:param text: 文本数据集
:return: 通过词袋模型转换后的向量
"""
data = []
for t in text:
vec = [0]*len(vocab)
for word in t:
if word in vocab:
vec[vocab.index(word)]+=1
data.append(vec)
return data
class NB():
def __init__(self,vocab):
self.data = None
self.label = None
self.vocab = vocab
self.vocab_len = len(self.vocab)
def fit(self,data,label):
self.data = data
self.label = label
# 计算每个类别的先验概率
self.pc0 = label.count(0)/len(label)
self.pc1 = label.count(1)/len(label)
# 分出不同类别的数据
self.data0 = [data[i] for i,c in enumerate(label) if c==0]
self.data1 = [data[i] for i,c in enumerate(label) if c==1]
# 计算每个类别中单词的数量,注意如果是len(),就不考虑重复元素,sum()考虑重复元素
self.word_num_0 = sum([i for t in self.data0 for i in t if i!=0])
self.word_num_1 = sum([i for t in self.data1 for i in t if i!=0])
#打印不同类别的单词个数和词表长度
print("类别0的单词个数:{},类别1的单词个数:{},总词表长度:{}".format(self.word_num_0,self.word_num_1,self.vocab_len))
self.word_freq_0 = np.sum(self.data0, axis = 0) #计算每个类别中单词的频率
self.word_freq_1 = np.sum(self.data1, axis = 0)
def pridict(self, text):
# 预测过程
pred = []
# 对于预测集的每一个文本
for t in text:
# 计算属于class 0的概率
p0 = []
for w in t:
# 不在词表中的不计算
if w in vocab:
p0.append((self.word_freq_0[self.vocab.index(w)] + 1)/(self.word_num_0+ self.vocab_len))
# 计算属于class 0的概率
p1 = []
for w in t:
# 不在词表中的不计算
if w in vocab:
p1.append((self.word_freq_1[self.vocab.index(w)] + 1)/(self.word_num_1+self.vocab_len))
print(p0)
print(p1)
print(self.pc0*np.prod(p0))
print(self.pc1*np.prod(p1))
if self.pc0*np.prod(p0)> self.pc1*np.prod(p1):
pred.append(0)
else:
pred.append(1)
return pred
if __name__ == '__main__':
text,label = loaddata()
vocab = createVocabList(text)
data = bag_words_vec(vocab, text)
pred_text = [["predictable","with","no","fun"]]
model = NB(vocab)
model.fit(data,label)
result = model.pridict(pred_text)
print(result)
输出结果:
类别0的单词个数:14,类别1的单词个数:9,总词表长度:20
[0.058823529411764705, 0.058823529411764705, 0.029411764705882353]
[0.034482758620689655, 0.034482758620689655, 0.06896551724137931]
6.106248727864848e-05
3.280167288531715e-05
[0]