算法三:朴素贝叶斯算法
前两个算法都被要求做出一个艰难的决定,给出数据所属分类的明确答案,但往往因为分类特征统计不足,或者分类特征选择有误导致了错误的分类结果,哪怕是训练集也有可能出现不能正确分类的情形。这时,前两种方法都如同现实生活一样是用“少数服从多数”的办法来做出决策。正如帕斯卡指出的:“少数服从多数未必是因为多数人更正义,而是多数人更加强力”,所以为了保证“少数人的权利”,我们要求分类器给出一个最优的猜测结果,同时给出猜测的概率估计值。
在说朴素贝叶斯算法之前,还是要说说贝叶斯统计,关于贝叶斯统计,这里只给出最最最基本的简述,其余的还请参阅further reading中的《数学之美番外篇:平凡而又神奇的贝叶斯方法》
先说贝叶斯公式:
定义:设A、B是两个事件,且P(A)>0,称
P(B|A)=P(AB)/P(A)
为在事件A发生的条件下事件B发生的条件概率。
相关公式:
乘法公式 P(XYZ)=P(Z|XY)P(Y|X)P(X)
全概率公式 P(X)=P(XY1)+ P(XY2)+…+ P(XYn)
贝叶斯公式:
如上所示,其中P(A|B)是在B发生的情况下A发生的可能性。在贝叶斯定理中,每个名词都有约定俗成的名称:
按这些术语,Bayes定理可表述为:后验概率 = (相似度*先验概率)/标准化常量,也就是說,后验概率与先验概率和相似度的乘积成正比。另外,比例 P(B|A)/P(B)也有时被称作标准相似度,Bayes定理可表述为:后验概率 =标准相似度*先验概率。
再说说朴素贝叶斯,朴素贝叶斯在英文中叫做naive Bayes,是不是这个贝叶斯方法too simple,sometimes naive呢?我们一起来看看他的基本假设:条件独立性。
给定类标号A,朴素贝叶斯分类器在估计类条件概率时假设属性之间条件独立。条件独立假设可以形式化的表达如下:
P(B|A)=P(b1|A)*P(b2|A)*…*P(bn|A)
其中每个训练样本可用一个属性向量B=(b1,b2,b3,…,bn)表示,各个属性之间条件独立。
比如,对于一篇文章,
“Good good study, Day day up.”
可以用一个文本特征向量来表示,x=(Good, good, study, Day, day , up)。一般各个词语之间肯定不是相互独立的,有一定的上下文联系。但在朴素贝叶斯文本分类时,我们假设个单词之间没有联系,可以用一个文本特征向量来表示这篇文章,这就是“朴素”的来历。
有了条件独立假设,就不必计算X和Y的每一种组合的类条件概率,只需对给定的Y,计算每个xi的条件概率。后一种方法更实用,因为它不需要很大的训练集就能获得较好的概率估计。
其实这种条件独立也不是在日常中看不到,比如Markov过程,再比如我们前面说的脊椎动物数据集的各个指标都可以看作条件独立的(前者是严格的,后者是近似的)
我们为了说明这个问题,使用Tom Mitchell的《机器学习》一书的playing tennis数据集(点击这里下载本文所有代码及用到数据集)来说明这个问题。R代码如下:
data <-read.csv("D:/R/data/playing tennis.csv") data<-data[,-1]#去掉了日期这一个没有可作为分类变量价值的变量 prior.yes<-sum(data[,5] =="Yes") / length(data[,5]); prior.no<-sum(data[,5] =="No") / length(data[,5]); bayespre<- function(condition) { post.yes <- sum((data[,1] == condition[1]) & (data[,5] == "Yes")) /sum(data[,5] == "Yes") * sum((data[,2] == condition[2]) & (data[,5] == "Yes")) /sum(data[,5] == "Yes") * sum((data[,3] == condition[3]) & (data[,5] == "Yes")) /sum(data[,5] == "Yes") * sum((data[,4] == condition[4]) & (data[,5] == "Yes")) /sum(data[,5] == "Yes") * prior.yes; post.no <- sum((data[,1] == condition[1]) & (data[,5] == "No")) /sum(data[,5] == "No") * sum((data[,2] == condition[2]) & (data[,5] == "No")) /sum(data[,5] == "No") * sum((data[,3] == condition[3]) & (data[,5] == "No")) /sum(data[,5] == "No") * sum((data[,4] == condition[4]) & (data[,5] == "No")) /sum(data[,5] == "No") * prior.no; return(list(prob.yes = post.yes, prob.no = post.no, prediction = ifelse(post.yes>=post.no, "Yes", "No"))); }
bayespre(c("Rain","Hot","High","Strong")) bayespre(c("Sunny","Mild","Normal","Weak")) bayespre(c("Overcast","Mild","Normal","Weak"))
上面三个测试集输出结果为:
>bayespre(c("Rain","Hot","High","Strong"))
$prob.yes
[1] 0.005291005
$prob.no
[1] 0.02742857
$prediction
[1] "No"
>bayespre(c("Sunny","Mild","Normal","Weak"))
$prob.yes
[1] 0.02821869
$prob.no
[1] 0.006857143
$prediction
[1] "Yes"
>bayespre(c("Overcast","Mild","Normal","Weak"))
$prob.yes
[1] 0.05643739
$prob.no
[1] 0
$prediction
[1] "Yes"
我们同样可以来训练一下我们之前提到的脊椎动物数据集(略去代码),来看看分类效果:
>bayespre(animals,c("no","yes","no","sometimes","yes"))
$prob.mammals
[1] 0
$prob.amphibians
[1] 0.1
$prob.fishes
[1] 0
$prob.reptiles
[1] 0.0375
$prediction
[1] amphibians
Levels: amphibians birds fishesmammals reptiles
这里我们仍然没有区分出是两栖动物还是爬行动物,但是至少它告诉我们选择时要考虑到爬行动物这种可能,而不是像决策树那样告诉你他是两栖动物。
>bayespre(animals,c("no","yes","no","yes","no"))
$prob.mammals
[1] 0.0004997918
$prob.amphibians
[1] 0
$prob.fishes
[1] 0.06666667
$prob.reptiles
[1] 0
$prediction
[1] fishes
Levels: amphibians birds fishesmammals reptiles
这个是第三条数据作为测试数据的,也就是得到了正确分类,他告诉我们有极小的可能他是哺乳动物,我们可以忽略它,毕竟两个概率相差太大了
> bayespre(animals,c("yes","no","no","yes","no"))
$prob.mammals
[1] 0.0179925
$prob.amphibians
[1] 0
$prob.fishes
[1] 0.01666667
$prob.reptiles
[1] 0
$prediction
[1] mammals
Levels: amphibians birds fishesmammals reptiles
这个分类相当不好,两个分类的概率也相差无几,我们确实需要考虑。
这至少告诉了我们两个事实:这个学习器的分类效果不太好;这个数据集的生物特征统计信息不够。
除此以外,我们还发现这个学习器处理不了他没见过的情况,以playing tennis数据为例:
假设有来了一个新样本 x1= (Outlook = Foggy,Temprature = Hot,Humidity = High,Wind =Strong),要求对其分类。我们来开始计算:
>bayespre(c("foggy","Hot","High","Strong"))
$prob.yes
[1] 0
$prob.no
[1] 0
$prediction
[1] "Yes"
计算到这里,大家就会意识到,这里出现了一个新的属性值,在训练样本中所没有的。如果有一个属性的类条件概率为0,则整个类的后验概率就等于0,我们可以直接得到后验概率P(Yes | x1)= P(No | x1)=0,这时二者相等,无法分类。(虽说程序设定时我遵从疑罪从无的思想偏向了正例)
当训练样本不能覆盖那么多的属性值时,都会出现上述的窘境。简单的使用样本比例来估计类条件概率的方法太脆弱了,尤其是当训练样本少而属性数目又很大时。
如何解决?引入m估计(m-estimate)方法来估计条件概率:
P(xi|yj)=(nc+mp)/(n+m)
n是类yj中的样本总数,nc是类yj中取值xi的样本数,m是称为等价样本大小的参数,而p是用户指定的参数。如果没有训练集(即n=0),则P(xi|yj)=p, 因此p可以看作是在类yj的样本中观察属性值xi的先验概率。等价样本大小决定先验概率和观测概率nc/n之间的平衡,提高了估计的稳健性。
朴素贝叶斯方法是一个很特别的方法,所以值得介绍一下。在众多的分类模型中,应用最为广泛的两种分类模型是决策树模型(Decision Tree Model)和朴素贝叶斯模型(Naive Bayes Model,NBC)。朴素贝叶斯模型发源于古典数学理论,有着坚实的数学基础,以及稳定的分类效率。
同时,NBC模型所需估计的参数很少,对缺失数据不太敏感,算法也比较简单。理论上,NBC模型与其他分类方法相比具有最小的误差率。但是实际上并非总是如此,这是因为NBC模型假设属性之间相互独立,这个假设在实际应用中往往是不成立的,这给NBC模型的正确分类带来了一定影响。在属性个数比较多或者属性之间相关性较大时,NBC模型的分类效率比不上决策树模型。而在属性相关性较小时,NBC模型的性能最为良好。(所以在文本分类时能够用关键词就更好了)
R的e1071包的naiveBayes函数提供了naive bayes的具体实现,其用法如下:
## S3 method for class 'formula' naiveBayes(formula, data, laplace = 0, ..., subset, na.action = na.pass) ## Default S3 method: naiveBayes(x, y, laplace = 0, ...)我们以titanic数据集为例,看看titanic上的符合怎样条件的人更容易得救:
data(Titanic) m <- naiveBayes(Survived ~ ., data = Titanic) m
在介绍贝叶斯文本挖掘之前,我想我们先得把R处理文本的一些工具简单的介绍一下,比如处理文本的tm包,R语言处理正则表达式之类的,关于tm包你可以参阅的是tm的帮助文档《Introduction to the tm Package Text Mining in R》,关于正则表达式可以参阅furtherreading 的《文本(字符串)处理与正则表达式》
先说tm包,在tm 中导入数据需要函数Corpus(或VCorpus),函数的用法如下:
Corpus(x,
readerControl = list(reader = x$DefaultReader, language = "en"),...)
对于这些资料来源(x),tm 包提供了一些相关的函数,比如:DirSource(处理目录)、 VectorSource(由文档构成的向量)、 DataframeSource(数据框)等。
一旦导入了训练文档,需要后续文档处理,比如填充、停止词去除。(在英文里,有些单词是会发生变化,比如我们要识别cat 这个字符,但还可能有cats 等单词,这时候就需要进行填充(stemming)将他们视为一个词,但遗憾的是在tm包中英文中有些不规则的动词过去式可能没法识别为一个词)在tm 包里,这些函数都归到信息转化里面,其主要函数就是tm_map(),这个函数可以通过maps 方式将转化函数实施到每一个单词上。tm_map()的主要用法如下:
tm_map(x, FUN, ..., useMeta = FALSE, lazy = FALSE)
提供的FUN常用的有as.PlainTextDocument(将xml转化为纯文本)、stripWhitespace(去除多余空白)、tolower(转化为小写)、removeWords(去除停止词)、stemDocument(填充)等。
Dictionary() 函数常用于在文本挖掘中展现相关的词条时。当将字典(Dictionary)传递到DocumentTermMatrix() 以后,生成的矩阵会根据字典提取计算词汇出现在每篇文档的频率。(这个在之后会有例子,就不多说了)
再介绍字符串的处理,分割函数:strsplit。使用格式为:
strsplit(x, split, fixed = FALSE, perl = FALSE, useBytes = FALSE)
主要参数说明:
X:字串向量,每个元素都将单独进行拆分。
Split:为拆分位置的字串向量,默认为正则表达式匹配(fixed=FALSE)fixed=TRUE,表示使用普通文本匹配或正则表达式的精确匹配。
Perl:表示可以选择是否兼容Perl的正则表达式的表述方式。
下面开始介绍Naive Bayes算法:
计算每个类别中的文档数目:
for每篇训练文档:
for每个类别:
if 词条in 文档:增加该词条计数值,增加所有词条计数值
For 每个类别:
For 每个词条
Prob=词条数目/总词条数目
Return prob
举例说明,比如我要对我最近阅读的文献,有spc与doe两类,他们的关键词列表如下(部分关键词)
docId |
Key word |
class |
1 |
“Adaptive weighting” “run length” “control chart” |
spc |
2 |
“run length” “control chart” |
spc |
3 |
“control chart” “EWMA” “run length” |
spc |
4 |
“D-Efficiency” ”Main Effect” “Quadratic Effect” |
doe |
给定一个新样本”control chart” “run length””main effect” “EWMA”,对其进行分类。
该文本用属性向量表示为d=(”control chart” ,“run length” ,”main effect” ,“EWMA”),类别集合为Y={spc,doe}。类spc下总共有8个单词,类doe下总共有3个单词,训练样本单词总数为11,因此P(spc)=8/11, P(doe)=3/11。部分类条件概率计算如下(使用m估计):
P(”control chart”| spc)=(3+1)/(8+7)=4/15=2/7
P(”main effect”| spc) = (0+1)/(8+7)=1/15
P(”control chart”|doe)=(0+1)/(7+3)=0.1
分母中的8,是指spc类别下训练样本的关键词总数,7是指训练样本有有7个不同的关键词,3是指doe类下共有3个关键词词。
利用类条件概率,开始计算后验概率,
P(spc |d)=4/15*4/15*1/15*8/11≈0.003447811
P(doe|d)= 0.1*0.1*0.2*0.1*3/11≈5.454545e-05
因此,这个文档属于类别spc。
下面来说说朴素贝叶斯分类器在文本分类中的应用。下面是一个使用Naive Bayes分类垃圾邮件的很小的例子,数据来自《机器学习实战》,数据集已上传至百度云盘。
这个邮件集合分为两个文件夹ham,spam,各有25封邮件。其中spam中的是广告邮件。这里我们从各文件夹中抽取2封作为测试集,其余作为训练集。作为垃圾邮件分类器,我们总假定不能判决(即两个类别的概率相差不足一个数量级的,这个判别标准需要根据训练的情况确定)的为正常邮件。
R代码:
1、建立词袋:
library(tm) txt1<-"D:/R/data/email/ham" txtham<-Corpus(DirSource(txt1),readerControl=list(language= "en")) txtham<-tm_map(txtham,stripWhitespace) txtham<-tm_map(txtham,tolower) txtham<-tm_map(txtham,removeWords,stopwords("english")) txtham<-tm_map(txtham,stemDocument) txt2<-"D:/R/data/email/spam" txtspam<-Corpus(DirSource(txt2),readerControl=list(language= "en")) txtspam<-tm_map(txtspam,stripWhitespace) txtspam<-tm_map(txtspam,tolower) txtspam<-tm_map(txtspam,removeWords,stopwords("english")) txtspam<-tm_map(txtspam,stemDocument)
2、词汇计数(包括词类数目与词量数目)
dtm1<-DocumentTermMatrix(txtham) n1<-length(findFreqTerms(dtm1,1)) dtm2<-DocumentTermMatrix(txtspam) n2<-length(findFreqTerms(dtm2,1)) setwd("D:/R/data/email/spam") name<-list.files(txt2) data1<-paste("spam",1:23) lenspam<-0 for(i in 1:length(names)){ assign(data1[i],scan(name[i],"character")) lenspam<-lenspam+length(get(data[i])) } setwd("D:/R/data/email/ham") names<-list.files(txt1) data<-paste("ham",1:23) lenham<-0 for(i in 1:length(names)){ assign(data[i],scan(names[i],"character")) lenham<-lenham+length(get(data[i])) }
3、naive Bayes模型建立(使用m估计,p=1/m,m为词汇总数)
prob<-function(char,corp,len,n){ d<-Dictionary(char) re<-DocumentTermMatrix(corp, list(dictionary = d)); as.matrix(re) dtm<-DocumentTermMatrix(corp) n<-length(findFreqTerms(dtm, 1)) prob<-(sum(re[,1])+1)/(n+len) return(prob) } testingNB<-function(sentences){ pro1<-0.5 pro2<-0.5 for(i in1:length(sentences)){ pro1<-pro1*prob(sentences[i],txtham,lenham,n1) } for(i in1:length(sentences)){ pro2<-pro2*prob(sentences[i],txtspam,lenspam,n2) } return(list(prob.ham = pro1, prob.span =pro2, prediction =ifelse(pro1>=pro2/10, "ham", "spam"))) }
#读取文档,并且实现分词与填充 email<-scan("D:/R/data/email/test/ham2.txt","character") sentences<-unlist(strsplit(email,",|\\?|\\;|\\!"))#分词 library(Snowball)#实现填充 a<-tolower(SnowballStemmer(sentences))# 实现填充并除去大小写因素 #测试 testingNB(a)
$prob.ham
[1] 3.537766e-51
$prob.span
[1] 4.464304e-51
$prediction
[1] "ham"
类似的使用spam1.txt得到结果:
$prob.ham
[1] 5.181995e-95
$prob.span
[1] 1.630172e-84
$prediction
[1] "spam"
可以看出分类效果一般,但是典型的广告邮件还是可以区分出来的。对于分类而言,我们使用tm包来做naive Bayes多少有些高射炮打蚊子的意思,因为这里面除了去除停止词外我们几乎没有用到tm的更多不能利用base包实现的东西了(比如词类统计可以使用table,字典的查询可以使用grep, regexpr来实现它)。但是去掉停止词对于这样的文本分类是十分重要的,可以让分类准确率更高。
另外,tm在文本挖掘方面还有很多值得学习的东西,我也正在学习如何使用它。附上一个十分有价值的学习连接:刘思喆的《R语言环境下的文本挖掘》本文也有很大一部分是在参考他的成果上完成的。
本文之后,待写的几篇文章罗列如下:
(to be continue)