机器学习(九)——概率图模型之朴素贝叶斯

朴素贝叶斯分类是一种十分简单的分类算法,叫它朴素贝叶斯分类是因为这种方法的思想真的很朴素,朴素贝叶斯的思想基础是这样的:对于给出的待分类项,求解在此项出现的条件下各个类别出现的概率,哪个最大,就认为此待分类项属于哪个类别。通俗来说,就好比这么个道理,你在街上看到一个黑人,我问你你猜这哥们哪里来的,你十有八九猜非洲。为什么呢?因为黑人中非洲人的比率最高,当然人家也可能是美洲人或亚洲人,但在没有其它可用信息下,我们会选择条件概率最大的类别,这就是朴素贝叶斯的思想基础。

在前面的逻辑回归中,我们在讲逻辑回归的损失函数的推导时,讲到了贝叶斯定理。这里再复习一下:
P(A|B) = \frac{P(B|A)*P(A)}{P(B)}= \frac{P(B|A)*P(A)}{P(B|A)*P(A)+P(B|~A)*P(~A)}
贝叶斯公式到底在说什么呢?贝叶斯公式就是在描述,你有多大把握能相信一件证据。
假设我们把A计作“汽车被砸了”,B计作“警报响了”,带进贝叶斯公式里看,等式左边就是在求:当汽车的警报器响了后,我们得出汽车被砸了的概率。这即是说,警报响这个证据有了,多大把握能相信它确实是在报警说汽车被砸了?
那么等式右边说的是什么呢?等式右边说,警报器响了后,汽车被砸了的概率等于 用警报响起、汽车也被砸了这事件的数量,除以响警报事件的数量,也即,警报响起、汽车也被砸了的事件的数量,除以警报响起、汽车被砸了的事件数量加上警报响起、汽车没被砸的事件数量。
如果警报响起、汽车没被砸的事件数量很小,比如为0,即杜绝了汽车被球踢、被行人碰到等等其他所有情况,那自然,警报响了,只剩下一种可能——汽车被砸了。这即是提高了响警报这个证据的说服力。

把贝叶斯公式换成机器学习中的表达方式,即为:
P(类别|特征)=\frac{P(特征|类别)*P(类别)}{P(特征)}
更一般的情况:
P(类别|特征1,特征2,特征3……特征n)=\frac{P(特征1,特征2,特征3……特征n|类别)*P(类别)}{P(特征1,特征2,特征3……特征n)}

在给定样本的情况下,P(特征1,特征2,特征3……特征n)是常数,所以,我们在做分类时,如果要想在最大概率的情况下得到正确的分类,也即让P(类别|特征)能取到最大值,我们只需要保证分子,也就是P(特征|类别)*P(类别)能取到最大值。

为了便于计算,朴素贝叶斯假设各个特征之间相互独立,也就是说每个属性独立的对分类结果发生影响(这就是为什么要叫朴素的原因)。如果每个属性独立,则:P(特征1,特征2,特征3……特征n|类别)=P(特征1|类别)*P(特征2|类别)*P(特征3|类别)...P(特征n|类别)

因此,我们的目标就转换为:
max\ \ P(类别|特征1,特征2,特征3……特征n) = max\ \ P(类别)* \prod_{i=1}^nP(特征i|类别)
用数学公式来表达,则为:
\hat y = arg\ \ max\ \ P(c)\prod_{i=1}^nP(x_i|c) \quad c\in y
这就是朴素贝叶斯分类器的表达式。
D^{(c)}表示训练集D中c类样本组成的集合,|D^{(c)}|代表集合中的样本数量,若有充足的独立同分布样本,则可容易地估计出类先验概率:P(c)=\frac{|D^{(c)}|}{|D|}

对于离散属性而言,令D^{(c)}_{x_i}表示D^{(c)}中第i 个属性上取值为x_i的样本组成的集合,则条件概率P(x_i|c)可表示为:P(x_i|c)=\frac{|D^{(c)}_{x_i}|}{|D^{(c)}|}

对于连续属性可考虑概率密度函数,假定:P(x_i|c) ~ N(\mu_i,\sigma_i^2)
则,P(x_i|c)=\frac{1}{\sigma_i\sqrt{2\pi}} e^{-\frac{1}{2\sigma_i^2}(x_i-\mu_i)^2}

下面是用R的实现步骤:

library(NLP)
library(tm)

#设置文件路径
path_to_neg_folder<-"aclImdb/train/neg"
path_to_pos_folder<-"aclImdb/train/pos"

#读取文件
nb_pos <- VCorpus(DirSource(path_to_pos_folder), readerControl = list(reader = 
                                    reader(DirSource(path_to_pos_folder)), language = "en"))
nb_neg <- VCorpus(DirSource(path_to_neg_folder), readerControl = list(reader = 
                                    reader(DirSource(path_to_neg_folder)), language = "en"))
#合并两个数据
nb_all <- c(nb_pos,nb_neg)

#观察数据
View(nb_all[[2]])
meta(nb_all[[20000]])

#获得所有文件名,后面根据文件名解析出评分
ids<-sapply(1:length(nb_all),function(x) meta(nb_all[[x]],"id"))
head(ids)

#解析评分,大于等于5的分为postive,小于5分的为negative
scores<-as.numeric(sapply(ids,function(x) sub("[0-9]+_([0-9]+)\\.txt","\\1",x)))
scores<-factor(ifelse(scores>=5,"positive","negative"))
summary(scores)

#文本处理
nb_all <- tm_map(nb_all, content_transformer(removeNumbers)) #移除数字
nb_all <- tm_map(nb_all, content_transformer(removePunctuation)) #移除标点符号
nb_all <- tm_map(nb_all, content_transformer(tolower)) #转换为小写
nb_all <- tm_map(nb_all, content_transformer(removeWords), stopwords("english"))
nb_all <- tm_map(nb_all, content_transformer(stripWhitespace)) #去除空格

#将数据转换为矩阵
nb_dtm <-DocumentTermMatrix(nb_all)
dim(nb_dtm)

#压缩矩阵,去掉为空的项
nb_dtm<-removeSparseTerms(x=nb_dtm, sparse = 0.99)
dim(nb_dtm)

nb_dtm <- weightBin(nb_dtm) ## 所有元素变换成二元因子
inspect(nb_dtm[1000:1006,100:106]) #查看数据框
nb_df <- as.data.frame(as.matrix(nb_dtm)) #将矩阵变为dataframe

#划分训练集和测试集
library(caret)
set.seed(443452342)
nb_sampling_vector <- createDataPartition(scores, p = 0.80, list = FALSE)
nb_df_train <- nb_df[nb_sampling_vector,]
nb_df_test <- nb_df[-nb_sampling_vector,]
scores_train = scores[nb_sampling_vector]
scores_test = scores[-nb_sampling_vector]

#v:属性向量,包括了每个属性的具体值, data:全量样本数据;
#函数用于计算给定的属性向量,计算在不同分类条件下的每个属性上的概率乘积
#即计算:P(特征1|类别)*P(特征2|类别)*P(特征3|类别)...P(特征n|类别),
#然后对比不同分类下的概率大小,进行预测分类
calcPx <- function(v,data){
  data_p = data[scores_train == "positive",] #分类为“positive”的全量样本数据;
  data_n = data[scores_train == "negative",] #分类为“negative”的全量样本数据;
  pp = length(data_p) / length(data) #计算"positive"出现的概率
  np = length(data_n) / length(data) #计算"negative"出现的概率
  result_p <- 1
  result_n <- 1
  for(i in 1:length(v)){
    #计算两个类别下的:P(特征1|类别)*P(特征2|类别)*P(特征3|类别)...P(特征n|类别)
    result_p <- result_p * (sum(data_p[,i]==v[i])/length(data_p[,i])) 
    result_n <- result_n * (sum(data_n[,i]==v[i])/length(data_n[,i]))
  }
  result_p <- result_p * pp
  result_n <- result_n * np
  return(ifelse(result_p>=result_n,"positive","negative"))
}

#循环训练集上的每一条记录,根据模型统计预测值
rest <- apply(nb_df_train,1,function(p){calcPx(p,nb_df_train)})
#查看预测的准确性
mean(rest == scores_train)

当然,在R中,我们也可以使用naiveBayes函数来直接实现:

library("e1071")
nb_model <- naiveBayes(nb_df_train, scores_train)

nb_train_predictions <- predict(nb_model, nb_df_train)
mean(nb_train_predictions == scores_train)
table(actual = scores_train, predictions = nb_train_predictions)

nb_test_predictions <- predict(nb_model, nb_df_test)
mean(nb_test_predictions == scores_test)
table(actual = scores_test, predictions = nb_test_predictions)

拉普拉斯平滑

从上面的计算过程可以看出,我们来进行预测分类是依靠训练集数据在各属性上的分布情况来决定。但是在某些极端情况下,尤其是在文本分析的过程中,有可能我们的样本数据在某个属性上的分布并没有完全涵盖这个属性的所有可能情况。参考网上一个非常经典的案例:


假如男生的四个特征是,长相帅,性格爆好,身高高,上进,那么他的女朋友嫁还是不嫁呢?
我们会发现,在我们给定的训练集中,性格属性中,没有爆好这个分类。这样导致的结果会使得 P(性格=爆好|嫁) 的概率和P(性格=爆好|不嫁) 的概率都为0。根据朴素贝叶斯的公式:\hat y = arg\ \ max\ \ P(c)\prod_{i=1}^nP(x_i|c) \quad c\in y,我们将会发现无法进行分类,因为我们计算出来的嫁与不嫁的概率都为0(因为是连乘,有一项为0,则全部都会为0)。 为了解决这种问题,我们在计算P(特征|类别)时(主要是离散属性),采用如下公式进行修正:
P(x_i|c)=\frac{|D^{(c)}_{x_i}|+1}{|D^{(c)}|+N_i}

其中,N_i 代表第i个属性中,可能的取值个数。
在我们上面这个例子中,我们要计算 P(性格=爆好|嫁) 的概率,就等于 \frac{性格等于爆好在所有嫁的数据中的数量+1}{所有嫁的数据总数+3}
(因为性格属性有3个可能的值:爆好,好,不好。如果有四个属性值:爆好,好,一般,不好的话,分母就是加4)

以上就是对拉普拉斯平滑的通俗解释。下面来看代码:

#对函数进行修正,增加拉普拉斯平滑。新增的参数向量t,代表每个特征可能的属性值数量
calcPx <- function(v,t,data){
  data_p = data[scores_train == "positive",] #分类为“positive”的全量样本数据;
  data_n = data[scores_train == "negative",] #分类为“negative”的全量样本数据;
  pp = length(data_p) / length(data) #计算"positive"出现的概率
  np = length(data_n) / length(data) #计算"negative"出现的概率
  result_p <- 1
  result_n <- 1
  for(i in 1:length(v)){
    #计算两个类别下的:P(特征1|类别)*P(特征2|类别)*P(特征3|类别)...P(特征n|类别)
    #增加拉普拉斯平滑
    result_p <- result_p * (sum(data_p[,i]==v[i])+1/length(data_p[,i])+t[i]) 
    result_n <- result_n * (sum(data_n[,i]==v[i])+1/length(data_n[,i])+t[i])
  }
  result_p <- result_p * pp
  result_n <- result_n * np
  return(ifelse(result_p>=result_n,"positive","negative"))
}

如果使用naiveBayes函数,可以直接指定拉普拉斯平滑系数(一般为1,代表给每个特征加1,也可以为其他较小的数)

nb_model_laplace <- naiveBayes(nb_df_train, scores_train, laplace=1)

【参考文档】
如何通俗的理解贝叶斯定理
朴素贝叶斯的R实现
带你理解朴素贝叶斯分类算法
理解朴素贝叶斯分类的拉普拉斯平滑
概率图模型-朴素贝叶斯
概率图模型(3)朴素贝叶斯分类
机器学习21:概率图--朴素贝叶斯模型

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容