贝叶斯决策理论定义
贝叶斯决策理论是主观贝叶斯派归纳理论的重要组成部分。 贝叶斯决策就是在不完全情报下,对部分未知的状态用主观概率估计,然后用贝叶斯公式对发生概率进行修正,最后再利用期望值和修正概率做出最优决策。
假设有一个数据集,它由两类数据组成,数据分布如图所示:
现在用p1(x,y)表示数据点(x,y)属于类型1(图中圆形表示的类型)的概率,用p2(x,y)表示数据点(x,y)属于类型2(图中加号表示的类型)的概率,那么对于一个新的数据点(x,y),可以用下面的规则来判断它的类别:
- 如果p1(x,y)>p2(x,y),那么为类别1。
- 如果p1(x,y)<p2(x,y),那么为类别2。
也就是说,我们会选择高概率对应的类别。这就是贝叶斯决策理论的核心思想,即选择最高概率的决策。
使用条件概率分类
上面提到的p1,p2只是为了尽可能简化描述,真正需要计算和比较的是
p
(
c
1
∣
x
,
y
)
p(c_{1}|x,y)
p(c1∣x,y)和
p
(
c
2
∣
x
,
y
)
p(c_{2}|x,y)
p(c2∣x,y)。符号表达的具体意义是:
给定某个由x,y表示的数据点,那么该数据点来自类别
c
1
c_{1}
c1的概率是多少?数据点来自类别
c
2
c_{2}
c2的概率又是多少?通过贝叶斯准则可以得到:
p
(
c
i
∣
x
,
y
)
=
p
(
x
,
y
∣
c
i
)
p
(
c
i
)
p
(
x
,
y
)
p(c_{i}|x,y)=\frac{p(x,y|c_{i})p(c_{i})}{p(x,y)}
p(ci∣x,y)=p(x,y)p(x,y∣ci)p(ci)
使用这些定义,可以定义贝叶斯分类准则为:
- 如果 p ( c 1 ∣ x , y ) > p ( c 2 ∣ x , y ) p(c_{1}|x,y)>p(c_{2}|x,y) p(c1∣x,y)>p(c2∣x,y),那么属于类别 c 1 c_{1} c1
- 如果 p ( c 1 ∣ x , y ) < p ( c 2 ∣ x , y ) p(c_{1}|x,y)<p(c_{2}|x,y) p(c1∣x,y)<p(c2∣x,y),那么属于类别 c 2 c_{2} c2
使用贝叶斯进行文本分类
要从文本中获取特征,需要先拆分文本。这里的特征是来自文本的词条,一个词条是字符的任意组合。可以把词条想象成单词,也可以使用非单词词条,比如url,ip地址等。然后将每一个文本片段表示为一个词条向量,其中1表示词条出现,0表示词条未出现。
准备数据:从文本中构建向量
首先将出现在所有文档中的所有单词汇聚成一张词汇表,然后将每一篇文档转换成词汇表中的向量。
第一步导入数据,因为数据量较少可以手工导入,使用python代码实现如下:
def loadDataSet():
postingList=[['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']]
classVec = [0,1,0,1,0,1]
return postingList,classVec #返回值中postingList为文档集合,classVec为类别集合
再数据导入完毕后我们需要一个词汇表,用于存放所有的单词,将文档中所有的单词汇聚到一个数组中,再进行去重即可。代码实现如下:
def create_vocablist(datas): #datas为文档集合
v_list=set([]) #set集合
for d in datas:
v_list=v_list|set(d) #取并集
return list(v_list)
其中v_list=v_list|set(d)表示取并集操作,将每一个文档中的单词使用set函数去重后再和词汇表求并集,逐步扩充词汇表中的元素,直到所有文档都执行完毕。
在获得词汇表后,就可以通过该表把每一个文档转换成词向量具体操作如下:
制定一个和词汇表等长的一个向量,文档中出现的每一个词在词汇表中的位置都在该向量相对应的位置设为1,其他地方设成0。用代码描述如下:
def word2vec(v_list,inset): #将单词转换成词向量
vec=[0]*len(v_list) #创建一个长度与词汇表 等长的0向量组
for word in inset:
if word in v_list:
vec[v_list.index(word)]=1 #如果单词在词汇表中存在,则在词汇表对应位置置1
else:
print('单词%s不存在' % word)
return vec
下面我们用这几个函数测试一下效果,代码如下:
data_set,class_list=loadDataSet()
v_list=create_vocablist(data_set)
print(v_list)
print(word2vec(v_list,data_set[0]))
输出结果如下:
['food', 'flea', 'I', 'love', 'has', 'ate', 'help', 'him', 'how', 'maybe', 'garbage', 'buying', 'posting', 'park', 'stop', 'not', 'dog', 'worthless', 'dalmation', 'mr', 'steak', 'to', 'licks', 'stupid', 'take', 'so', 'is', 'please', 'my', 'problems', 'cute', 'quit']
[0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0]
我们看到,第二个数组中第二个元素被置为1,对应词汇表的单词是flea,现在它也出现在第一篇文档中。代码结果运行正确。
从词向量计算概率
我们通过前面的运算知道了一个词是否出现在一篇文档中,也知道了该文档所属的类别。重写贝叶斯准则,将中之前的x,y替换为w。粗体w表示这是一个向量,它由多个数值组成。在这里,数值的个数等于词汇表元素的个数。
p
(
c
i
∣
w
)
=
p
(
w
∣
c
i
)
p
(
c
i
)
p
(
w
)
p(c_{i}|\textbf{w})=\frac{p(\textbf{w}|c_{i})p(c_{i})}{p(\textbf{w})}
p(ci∣w)=p(w)p(w∣ci)p(ci)
我们运用上述公式,对每个类计算该值,然后比较这两个概率的大小。
首先我们可以通过类别i(侮辱性留言或非侮辱性留言)中文档数除以总的文档数计算出
p
(
c
1
)
p(c_{1})
p(c1)。接着计算
p
(
w
∣
c
i
)
p(\textbf{w}|c_{i})
p(w∣ci),这里用到贝叶斯假设:如果将w展开为一个个独立特征,假设所有词都互相独立,那么
p
(
w
∣
c
i
)
p(\textbf{w}|c_{i})
p(w∣ci)等价于
p
(
w
0
∣
c
1
)
p
(
w
1
∣
c
1
)
p
(
w
2
∣
c
1
)
⋯
p
(
w
n
∣
c
1
)
p(w_{0}|c_{1})p(w_{1}|c_{1})p(w_{2}|c_{1})\cdots p(w_{n}|c_{1})
p(w0∣c1)p(w1∣c1)p(w2∣c1)⋯p(wn∣c1),这样就简化了计算过程。
该过程的流程如下:
计算每个类别的文档总数
对每篇训练文档:
对每个类别:
如果词条出现在文档中,则增加该词条的计数值
增加所有词条的计数值
对每个类别:
对于每个词条:
将该词条的数目除以该类别总数目得到条件概率
返回每个类别的条件概率
用代码实现如下:
'''
第一个参数为文档矩阵,它由每篇文档构成的词向量组成
第二个参数为各个文档类别向量
返回值前两个是在类别i条件下,词汇表中各个词汇出现的概率,即为p(wi|c),第三个为p(ci)
'''
def trainNB0(trainMat,class_list):
doc_num=len(trainMat) #文档数量
word_num=len(trainMat[0]) #词向量长度
pa=sum(class_list)/float(doc_num) #随机一个文档,该文档是侮辱性文档的概率
p0_num=np.zeros(word_num);p1_num=np.zeros(word_num) #词库中所有单词在0,1两类中出现的次数
p0_all=0.0;p1_all=0.0 #属于各类的所有单词数量
for i in range(doc_num):
if class_list[i]==0: #该文档属于侮辱性文档
p0_num+=trainMat[i] #该分类中的所有单词数量+1
p0_all+=sum(trainMat[i])
else:
p1_num += trainMat[i]
p1_all += sum(trainMat[i])
p1vec=p1_num/p1_all #计算该向量中每一个元素的概率
p0vec=p0_num/p0_all
return p0vec,p1vec,pa
代码引用了numpy库方便运算。
下面我们对打代码进行测试:
data_set,class_list=loadDataSet()
v_list=create_vocablist(data_set)
print(v_list)
trainM=[]
for d in data_set:
trainM.append(word2vec(v_list,d))
p0vec, p1vec, pa=trainNB0(trainM,class_list)
print(pa)
print(p1vec)
执行结果如下:
['help', 'my', 'dalmation', 'garbage', 'maybe', 'cute', 'love', 'has', 'licks', 'ate', 'to', 'buying', 'is', 'stop', 'problems', 'not', 'stupid', 'worthless', 'steak', 'food', 'quit', 'I', 'how', 'take', 'so', 'dog', 'him', 'flea', 'please', 'posting', 'mr', 'park']
0.5
[0. 0. 0. 0.05263158 0.05263158 0.
0. 0. 0. 0. 0.05263158 0.05263158
0. 0.05263158 0. 0.05263158 0.15789474 0.10526316
0. 0.05263158 0.05263158 0. 0. 0.05263158
0. 0.10526316 0.05263158 0. 0. 0.05263158
0. 0.05263158]
pa的概率是0.5,结果正确,p1vec表示的是在类别1的条件下,词汇表中各个词汇出现的概率,可以看到最大值为0.15789474,在词汇表中对应的位置是stupid。这意味着stupid是最能表现类别1的单词。
根据现实情况的改进
我们初步得到了 p ( c i ) p(c_{i}) p(ci)和 p ( w i ∣ c j ) p(w_{i}|c_{j}) p(wi∣cj),根据上面的贝叶斯公式,我们需要计算 p ( w 0 ∣ c 1 ) p ( w 1 ∣ c 1 ) p ( w 2 ∣ c 1 ) ⋯ p ( w n ∣ c 1 ) p(w_{0}|c_{1})p(w_{1}|c_{1})p(w_{2}|c_{1})\cdots p(w_{n}|c_{1}) p(w0∣c1)p(w1∣c1)p(w2∣c1)⋯p(wn∣c1)。如果其中一个概率为0,那么最后的结果也为0,为了避免这种影响,可以将所有词出现的次数初始化为2,并将分母初始化为2。相应的代码修改如下:
p0_num = np.ones(word_num);p1_num = np.ones(word_num)
p0_all = 2.0;p1_all = 2.0
另一个问题是下溢出,我们需要将多个概率连乘,它们都是在0-1之间的数,结果是越乘越小,最后python可能四舍五入到0,得到不正确的答案。最常见的处理办法就是取对数,由对数性质可以得出,函数在取对数后,它们的值虽然不同,但并不影响大小关系,对我们的结果没有影响。修改对应的代码如下:
p1vec=np.log(p1_num/p1_all)
p0vec=np.log(p0_num/p0_all)
现在已经准备好要计算的各个部分,只需要套用公式就可以得到结果,具体代码如下:
'''
第一个参数为被测试的词向量,注意要是np.array格式
后面三个参数为上一个函数计算所得的结果
'''
def classifyNB(input_list,p0vec,p1vec,pa):
p1=sum(input_list*p1vec)+np.log(pa)
p0=sum(input_list*p0vec)+np.log(1.0-pa)
if(p1>p0):
return 1
else:
return 0
由于贝叶斯公式的分母是一样的,在比较大小的时候只需要考虑分子的大小即可。函数中的相乘表示两个向量中各个元素的相乘。下面对所写代码进行测试:
def test_fun():
datas,class_list=loadDataSet()
v_list=create_vocablist(datas)
trainM=[]
for d in datas:
trainM.append(word2vec(v_list,d))
p0v,p1v,pa=trainNB0(trainM,class_list)
t_doc=[['love','my','dalmation'],
['stupid','garbage']]
t_vec=[]
for d in t_doc:
t_vec.append(word2vec(v_list,d))
t_vec=np.array(t_vec)
x=len(t_vec)
for i in range(x):
print('%s属于类型为:%s' % (t_doc[i],classifyNB(t_vec[i],p0v,p1v,pa)))
运行结果如下:
['love', 'my', 'dalmation']属于类型为:0
['stupid', 'garbage']属于类型为:1
符合实际。
实例:应用贝叶斯过滤垃圾邮件
数据来源机器学习实战-图书-图灵社区
随书附带的源码Ch04/email文件夹中
分割文本
要想构建词向量,首先要把邮件中的文本转换成词汇数组。下面给出一个字符串
mystr='Hello everyone, I am very glad to meet you.'
对于一个文本字符串,可以使用python自带的split函数划分
print(mystr.split()) #split参数默认为所有的空字符,包括空格、换行(\n)、制表符(\t)等
结果如下:
['Hello', 'everyone,', 'I', 'am', 'very', 'glad', 'to', 'meet', 'you.']
整体看起来还可以,但是把标点符号也算进去了,我们可以通过正则来去除这些:
import re
print(re.split(r'\W+',mystr))
(注:书本里面提供的是’\W*‘,我感觉应该是不对的,因为*表示大于等于0,就是0也是可以的,那么久把字符串里面的每一个字符都分割开了,不符合要求)
这样得到的结果如下:
['Hello', 'everyone', 'I', 'am', 'very', 'glad', 'to', 'meet', 'you', '']
标点去除后还存在着部分空字符串。我们可以计算每个字符串的长度,只返回大于0的字符串。
最后,我们发现每句话首字母都大写了,这对于我们以单词做基础的词汇表不符,由于大小写的关系一个单词可能就会被我们分成两个不同的词汇。所以我们要通过python自带的转小写(lower())方法达到目的。
同时,如果字符串中包含网站等信息时候,如https://translate.google.cn/#view=home&op=translate&sl=en&tl=zh-CN&text=Hello进行分割可能会得到cn,op,en之类无意义的词汇,我们想要去掉这些单词,因此在实际过滤中我们仅返回了长度大于2的单词。分割字符串函数如下:
def split_txt(str):
import re
w_list=re.split(r'\W+',str)
return [tok.lower() for tok in w_list if len(tok)>2]
算法测试
下面进行测试,首先要导入数据,相应代码如下:
def create_data():
doc_list=[] #文档列表
class_list=[] #分类列表
fulltxt=[] #全部单词
for i in range(1,26):
with open('email/spam/%d.txt' % i) as f:
word_list=split_txt(f.read())
doc_list.append(word_list)
class_list.append(1)
fulltxt.extend(word_list)
with open('email/ham/%d.txt' % i) as f:
word_list=split_txt(f.read())
doc_list.append(word_list)
class_list.append(0)
fulltxt.extend(word_list)
return doc_list,class_list,fulltxt
之后是训练数据集,这次我们随机选出10个样本作为测试样本,剩下40个作为训练样本,代码如下:
def test():
doc_list,class_list,fulltxt=create_data()
v_list=p1.create_vocablist(doc_list)
train_index=list(range(50)) #训练样本索引值
test_index=[] #测试样本索引值
for i in range(10): #随机选10个做测试样本
rand_index=int(r.uniform(0,len(train_index))) #随机选择数组里面的一个元素
test_index.append(train_index[rand_index]) #将该数存入测试组
del(train_index[rand_index]) #删除该元素
trainM=[];train_class=[]
for i in range(len(train_index)):
trainM.append(bag_word2vec(v_list,doc_list[i]))
train_class.append(class_list[i])
p0vec, p1vec, pa=p1.trainNB0(trainM,class_list)
errcount=0 #测试错误次数
for i in test_index:
word_vec=bag_word2vec(v_list,doc_list[i])
if(p1.classifyNB(np.array(word_vec),p0vec,p1vec,pa)!=class_list[i]):
errcount+=1
print('错误率:%f' % (float(errcount)/len(test_index)))
由于是随机获取到的训练样本,在每次计算后所得的结果应该是不一样的,经过多次运算,错误率稳定在10%以内,准确率还是较高的。
(注:在测试中可能会出现形如’‘gbk’ codec can’t decode byte 0xae in position 199: illegal multibyte sequen‘的错误,解决办法参考链接)