朴素贝叶斯算法简单高效,在处理分类问题上,应该是首先要考虑的方法之一
1. 准备知识
贝叶斯分类是一类算法的总称,这类算法均以贝叶斯定理为基础,故称贝叶斯分类
这个定理解决了生活里经常遇到的问题:已知某条件概率,如何得到两个事件交换后的概率,也就是在已知P(X|Y)的情况下如何求得P(Y|X)。这里先解释什么是条件概率。
P ( X ∣ Y ) = P ( X Y ) P ( Y ) P(X|Y)=\frac{P(XY)}{P(Y)} P(X∣Y)=P(Y)P(XY)
P(Y|X)表示事件X已经发生的条件下,事件Y发生的概率,其基本的求解公式维:
P
(
Y
∣
X
)
=
P
(
X
∣
Y
)
P
(
Y
)
P
(
X
)
P(Y|X)=\frac{P(X|Y)P(Y)}{P(X)}
P(Y∣X)=P(X)P(X∣Y)P(Y)
它其实是由以下的联合概率推导出来:
P
(
Y
,
X
)
=
P
(
Y
∣
X
)
P
(
X
)
=
P
(
X
∣
Y
)
P
(
Y
)
P(Y,X)=P(Y|X)P(X)=P(X|Y)P(Y)
P(Y,X)=P(Y∣X)P(X)=P(X∣Y)P(Y)
其中P(Y)叫做类标签的先验(prior)概率(可以根据数据计算出来的概率),P(Y|X)叫做后验概率(根据先验概率计算出来的概率),P(Y,X)叫做联合概率。P(X|Y)是样本X相对于类标签Y的类条件概率。P(X)是用于归一化的证据因子,它于类的标签无关。
因此估计P(Y|X)问题就转化为如何基于训练数据D来估计先验概率P(Y)和条件概率P(X|Y)。
2. 极大似然估计
估计类标签的条件概率的一种常用策略是先假定其具有某种确定的概率分布,再基于训练样本对概率分布的参数进行估计。令
D
c
D_{c}
Dc表示训练集D中第c类样本组成的集合,假设这些样本是独立同分布的,则参数
θ
c
\theta_{c}
θc对于数据集
D
c
D_{c}
Dc的似然是
P
(
D
c
∣
θ
c
)
=
∏
x
=
D
c
P
(
x
∣
θ
c
)
P(D_{c}|\theta_{c})=\prod_{x=D_{c}} P(x|\theta_{c})
P(Dc∣θc)=x=Dc∏P(x∣θc)
极大似然估计试图在
θ
c
\theta_{c}
θc的所有可能取值中,找到一个能使得数据出现的可能性最大的值。
3.朴素贝叶斯分类
使用贝叶斯准则来最小化决策风险,首先就要获得后验概率P(Y|X)。然而在现实任务中通常很难直接获得。机器学习所要实现的就是基于有限的训练样本集尽可能准确的估计出后验概率P(Y|X)。
主要有两种方案:第一,给定X,可以直接通过建模P(Y|X)来预测Y,这样得到的是“判别式模型”。第二,先对联合概率P(X,Y)建模,然后获得后验概率,这样得到的是“生成式模型”。决策树、BP神经网络、支持向量机都是判别式模型。
之所以叫朴素贝叶斯分类,是因为贝叶斯公式来估计后验概率P(Y|X)的主要困难在于类标签的条件概率P(X|Y)是所有属性上的联合概率。难以从有限的训练样本中直接估计而得出。为了解决这个问题,朴素贝叶斯采用了“属性条件独立性假设”,即对已知类别,假设所有属性相互独立,换言之,假设每个属性独立的对分类结果产生影响。
基于独立性假设,贝叶斯定理可以写成:
P
(
Y
∣
X
)
=
P
(
X
∣
Y
)
P
(
Y
)
P
(
X
)
=
P
(
Y
)
P
(
X
)
∏
i
=
1
d
P
(
x
i
∣
y
)
P(Y|X)=\frac{P(X|Y)P(Y)}{P(X)}=\frac{P(Y)}{P(X)}\prod_{i=1}^{d}P(x_{i}|y)
P(Y∣X)=P(X)P(X∣Y)P(Y)=P(X)P(Y)i=1∏dP(xi∣y)
其中d为属性数目,
x
i
x_{i}
xi为X在第i个属性上的取值。
3.1朴素贝叶斯的分类步骤
- 设 x i = a 1 , a 2 , . . . , a m x_{i}={a_{1},a_{2},...,a_{m}} xi=a1,a2,...,am是一个待分类项,而每个 a a a是 X X X的一个特征属性
- 有类别集合 C = y 1 , y 2 , . . . , y n C={y_{1},y_{2},...,y_{n}} C=y1,y2,...,yn
- 计算 P ( y 1 ∣ x i ) , P ( y 2 ∣ x i ) , . . . , P ( y n ∣ x i ) P(y_{1}|x_{i}),P(y_{2}|x_{i}),...,P(y_{n}|x_{i}) P(y1∣xi),P(y2∣xi),...,P(yn∣xi)条件概率
- 如果 P ( y k ∣ x i ) = m a x { P ( y 1 ∣ x i ) , P ( y 2 ∣ x i ) , . . . , P ( y n ∣ x i ) } P(y_{k}|x_{i})=max\{P(y_{1}|x_{i}),P(y_{2}|x_{i}),...,P(y_{n}|x_{i})\} P(yk∣xi)=max{P(y1∣xi),P(y2∣xi),...,P(yn∣xi)},则 x x x属于 y k y_{k} yk
那么现在的关键是如何计算第3步的各个条件概率,我们可以这么做:
- 找到一个已知分类的待分类项集合,这个集合叫做训练样本集
- 统计得到在各个类别下各个属性的条件概率估计,即
P ( a 1 ∣ y 1 ) , P ( a 2 ∣ y 1 ) , . . . , P ( a m ∣ y 1 ) ; P ( a 1 ∣ y 2 ) , P ( a 2 ∣ y 2 ) , . . . , P ( a m ∣ y 2 ) ; . . . ; P ( a 1 ∣ y n ) , P ( a 2 ∣ y n ) , . . . , P ( a m ∣ y n ) P(a_{1}|y_{1}),P(a_{2}|y_{1}),...,P(a_{m}|y_{1});P(a_{1}|y_{2}),P(a_{2}|y_{2}),...,P(a_{m}|y_{2});...;P(a_{1}|y_{n}),P(a_{2}|y_{n}),...,P(a_{m}|y_{n}) P(a1∣y1),P(a2∣y1),...,P(am∣y1);P(a1∣y2),P(a2∣y2),...,P(am∣y2);...;P(a1∣yn),P(a2∣yn),...,P(am∣yn)
- 如果各个特征属性是条件独立的,则根据贝叶斯定理的:
P ( y i ∣ x ) = P ( x ∣ y i ) P ( y i ) P ( x ) P(y_{i}|x)=\frac{P(x|y_{i})P(y_{i})}{P(x)} P(yi∣x)=P(x)P(x∣yi)P(yi)
因为分母对于所有类别是常数,因为我们只要讲分子最大化即可,又因为各个特征属性是条件独立的,所以有:
P ( x ∣ y i ) P ( y i ) = P ( a 1 ∣ y i ) , P ( a 2 ∣ y i ) , . . . , P ( a m ∣ y i ) P ( y i ) = P ( y i ) ∏ j = 1 m P ( a j ∣ y i ) P(x|y_{i})P(y_{i})=P(a_{1}|y_{i}),P(a_{2}|y_{i}),...,P(a_{m}|y_{i})P(y_{i})=P(y_{i})\prod_{j=1}^{m}P(a_{j}|y_{i}) P(x∣yi)P(yi)=P(a1∣yi),P(a2∣yi),...,P(am∣yi)P(yi)=P(yi)j=1∏mP(aj∣yi)
3.2 朴素贝叶斯分类流程图
整个朴素贝叶斯分类分为三个阶段:
**第一阶段:**准备工作阶段。整个阶段的任务是为朴素贝叶斯分类做必要的准备,主要工作是根据具体情况确定特定属性,并对每个属性进行适当的划分,然后由人工对一部分待分类项进行分类,形成训练集。这一阶段的输入是所有待分类数据,输出是特征属性和训练样本。这一阶段是整个朴素贝叶斯分类中唯一需要人工完成的阶段,其质量对整个过程将有重要影响,分类器的质量很大程度上由特征属性、特征属性划分和训练样本质量决定。
**第二阶段:**分类器训练阶段。这个阶段的任务就是生成分类器,主要工作是计算每个类别在训练样本中的出现频率及每个特征属性划分对每个类别的条件概率估计,并将结果记录。其输入是特征属性和训练样本,输出是分类器。这一阶段是机械性阶段,根据前面讨论的公式可以由程序自动计算完成。
**第三阶段:**应用阶段。这个阶段的任务是使用分类器对待分类项进行分类,其输入是分类器和待分类项,输出是待分类项与类别的映射关系。这一阶段也是机械性阶段,由程序完成。
4. 是否出去打球案例
import pandas as pd
data = pd.read_csv('tennis.csv')
data.info()
<class ‘pandas.core.frame.DataFrame’>
RangeIndex: 14 entries, 0 to 13
Data columns (total 5 columns):
outlook 14 non-null object
temp 14 non-null object
humidity 14 non-null object
windy 14 non-null bool
play 14 non-null object
dtypes: bool(1), object(4)
memory usage: 542.0+ bytes
data.head()
outlook | temp | humidity | windy | play | |
---|---|---|---|---|---|
0 | sunny | hot | high | False | no |
1 | sunny | hot | high | True | no |
2 | overcast | hot | high | False | yes |
3 | rainy | mild | high | False | yes |
4 | rainy | cool | normal | False | yes |
data.columns
Index([‘outlook’, ‘temp’, ‘humidity’, ‘windy’, ‘play’], dtype=‘object’)
data.index
RangeIndex(start=0, stop=14, step=1)
data.head(14)
outlook | temp | humidity | windy | play | |
---|---|---|---|---|---|
0 | sunny | hot | high | False | no |
1 | sunny | hot | high | True | no |
2 | overcast | hot | high | False | yes |
3 | rainy | mild | high | False | yes |
4 | rainy | cool | normal | False | yes |
5 | rainy | cool | normal | True | no |
6 | overcast | cool | normal | True | yes |
7 | sunny | mild | high | False | no |
8 | sunny | cool | normal | False | yes |
9 | rainy | mild | normal | False | yes |
10 | sunny | mild | normal | True | yes |
11 | overcast | mild | high | True | yes |
12 | overcast | hot | normal | False | yes |
13 | rainy | mild | high | True | no |
outlook_count = data.groupby(['outlook','play']).size()
outlook_count
outlook play
overcast yes 4
rainy no 2
yes 3
sunny no 3
yes 2
dtype: int64
outlook_total = data.groupby(['outlook']).size()
outlook_total
outlook
overcast 4
rainy 5
sunny 5
dtype: int64
outlook_count = data.groupby(['outlook', 'play']).size()
outlook_total = data.groupby(['outlook']).size()
temp_count = data.groupby(['temp', 'play']).size()
temp_total = data.groupby(['temp']).size()
humidity_count = data.groupby(['humidity', 'play']).size()
humidity_total = data.groupby(['outlook']).size()
windy_count = data.groupby(['windy', 'play']).size()
windy_total = data.groupby(['windy']).size()
print(outlook_count)
print(windy_total)
print(outlook_total)
print(temp_count)
print(temp_total)
print(humidity_count)
print(humidity_total)
print(windy_count)
print(windy_total)
outlook play
overcast yes 4
rainy no 2
yes 3
sunny no 3
yes 2
dtype: int64
windy
False 8
True 6
dtype: int64
outlook
overcast 4
rainy 5
sunny 5
dtype: int64
temp play
cool no 1
yes 3
hot no 2
yes 2
mild no 2
yes 4
dtype: int64
temp
cool 4
hot 4
mild 6
dtype: int64
humidity play
high no 4
yes 3
normal no 1
yes 6
dtype: int64
outlook
overcast 4
rainy 5
sunny 5
dtype: int64
windy play
False no 2
yes 6
True no 3
yes 3
dtype: int64
windy
False 8
True 6
dtype: int64
X_train = pd.get_dummies(data[['outlook', 'temp', 'humidity', 'windy']]) #one-hot
y_train = pd.DataFrame(data['play'])
print(X_train.info())
print(y_train.info())
<class ‘pandas.core.frame.DataFrame’>
RangeIndex: 14 entries, 0 to 13
Data columns (total 9 columns):
windy 14 non-null bool
outlook_overcast 14 non-null uint8
outlook_rainy 14 non-null uint8
outlook_sunny 14 non-null uint8
temp_cool 14 non-null uint8
temp_hot 14 non-null uint8
temp_mild 14 non-null uint8
humidity_high 14 non-null uint8
humidity_normal 14 non-null uint8
dtypes: bool(1), uint8(8)
memory usage: 206.0 bytes
None
<class ‘pandas.core.frame.DataFrame’>
RangeIndex: 14 entries, 0 to 13
Data columns (total 1 columns):
play 14 non-null object
dtypes: object(1)
memory usage: 192.0+ bytes
None
print(X_train.head())
print(data.head())
windy outlook_overcast outlook_rainy outlook_sunny temp_cool temp_hot
0 False 0 0 1 0 1
1 True 0 0 1 0 1
2 False 1 0 0 0 1
3 False 0 1 0 0 0
4 False 0 1 0 1 0
temp_mild humidity_high humidity_normal
0 0 1 0
1 0 1 0
2 0 1 0
3 1 1 0
4 0 0 1
outlook temp humidity windy play
0 sunny hot high False no
1 sunny hot high True no
2 overcast hot high False yes
3 rainy mild high False yes
4 rainy cool normal False yes
#Import Library of Gusasian Navie Bayes model
from sklearn.naive_bayes import GaussianNB
import numpy as np
#Create a Guassian Classifier
model = GaussianNB()
#Train the model using the training set
model.fit(X_train,y_train)
#Predict Output
predicted = model.predict([[False,1,0,0,0,1,0,1,0]])
print(predicted)
[‘yes’]
/usr/local/lib/python3.6/dist-packages/sklearn/utils/validation.py:724: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
y = column_or_1d(y, warn=True)
5. 用机器学习的视角理解贝叶斯公式
在机器学习视角下,我们把X理解维**“具有某些特征”,把Y理解成 “类别标签” (一般机器学习问题中都是X=>特征,Y=>结果)。在最简单的二分类问题(是与否判定)下,我们将Y理解为属于某类**的标签,于是贝叶斯公式就变成额下面这个样子:
P
(
“
属
于
某
类
”
∣
“
具
有
某
些
特
征
”
)
=
P
(
“
具
有
某
些
特
征
”
∣
“
属
于
某
类
”
)
P
(
“
属
于
某
类
”
)
P
(
“
具
有
某
些
特
征
”
)
P(“属于某类”|“具有某些特征”)=\frac{P(“具有某些特征”|“属于某类”)P(“属于某类”)}{P(“具有某些特征”)}
P(“属于某类”∣“具有某些特征”)=P(“具有某些特征”)P(“具有某些特征”∣“属于某类”)P(“属于某类”)
我们简化下上述公式:
P ( “ 属 于 某 类 ” ∣ “ 具 有 某 些 特 征 ” ) P(“属于某类”|“具有某些特征”) P(“属于某类”∣“具有某些特征”):在某些样本“具有某些特征”的条件下,该样本“属于某类”的概率。所以叫做【后验概率】
P ( “ 具 有 某 些 特 征 ” ∣ “ 属 于 某 类 ” ) P(“具有某些特征”|“属于某类”) P(“具有某些特征”∣“属于某类”):在已知某样本“属于某类”的条件下,该样本“具有某些特征”的概率。叫做【条件概率】
P ( “ 属 于 某 类 ” ) P(“属于某类”) P(“属于某类”):(在未知某样本具有“具有某特征”的条件下),该样本“属于某类”的概率。所以叫做【先验概率】
P ( “ 具 有 某 些 特 征 ” ) P(“具有某些特征”) P(“具有某些特征”):(在未知某些样本"属于某类"的条件下)该样本“具有某些特征”的概率。
而我们二分类问题的最终目的是要判断 P ( “ 属 于 某 类 ” ∣ “ 具 有 某 些 特 征 ” ) P(“属于某类”|“具有某些特征”) P(“属于某类”∣“具有某些特征”)是否大于1/2就够了。贝叶斯方法就是把计算**“某些特征条件下的属于某类”的概率转换成需要计算“属于某类条件下具有那些些特征”**的概率,而后者的获取方法就简单多了,我们只需要找到一些包含已知特征标签的样本,就可以进行训练。而样本的类别标签都是明确的,所以贝叶斯方法属于有监督的方法。
6.垃圾邮件识别
举个例子好啦,我们现在要对垃圾邮件进行分类,识别垃圾邮件和普通邮件。如果我们选择使用朴素贝叶斯分类器,那么目标就是判断 P ( “ 垃 圾 邮 件 ” ∣ “ 具 有 某 些 特 征 ” ) P(“垃圾邮件”|“具有某些特征”) P(“垃圾邮件”∣“具有某些特征”)是否大于1/2。现在假设我们有垃圾邮件和正常邮件各1万封作为训练集。需要判断以下这个邮件是否属于垃圾邮件:
“我司可办理正规发票(保真)17%增值税发票点数优惠!”
也就是判断概率 P(“垃圾邮件”|“我司可办理正规发票(保真)17%增值税发票点数优惠!”) 是否大于1/2。
咳咳,有木有发现,转换成的这个概率,计算的方法:就是写个计数器,然后+1 +1 +1统计出所有垃圾邮件和正常邮件中出现这句话的次数啊!!!好,具体点说:
P
(
“
垃
圾
邮
件
”
∣
“
我
司
可
办
理
正
规
发
票
(
保
真
)
17
%
增
值
税
发
票
点
数
优
惠
”
)
=
垃
圾
邮
件
中
出
现
这
句
话
的
次
数
垃
圾
邮
件
中
出
现
这
句
话
的
次
数
+
正
常
邮
件
出
现
这
句
话
的
次
数
P(“垃圾邮件”|“我司可办理正规发票(保真)17\% 增值税发票点数优惠”)=\frac{垃圾邮件中出现这句话的次数}{垃圾邮件中出现这句话的次数+正常邮件出现这句话的次数}
P(“垃圾邮件”∣“我司可办理正规发票(保真)17%增值税发票点数优惠”)=垃圾邮件中出现这句话的次数+正常邮件出现这句话的次数垃圾邮件中出现这句话的次数
7.分词
一个很悲哀的结论:训练集是有限的,而句子的可能性是无限的,所有覆盖所有的句子可能性的训练集是不存在的。
所以解决方法是? 句子的可能性无限,但是词语就那么些!!汉语常用字2500个,常用词语也就56000个(你终于明白小学语文老师的用心良苦了)。按人们的经验理解,两句话意思相近并不强求非得每个字、词语都一样。比如“我司可办理正规发票,17%增值税发票点数优惠!”,这句话就比之前那句话少了“(保真)”这个词,但是意思基本一样。如果把这些情况也考虑进来,那样本数量就会增加,这就方便我们计算了。
于是,我们可以不拿句子作为特征,而是拿句子里面的词语(组合)作为特征去考虑。比如“正规发票”可以作为一个单独的词语,“增值税”也可以作为一个单独的词语等等。
句子“我司可办理正规发票,17%增值税发票点数优惠!”就可以变成(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)。
于是你接触到了中文NLP中的最最重要的技术:分词! 也就是把一整句话拆分成更细粒度的词语来表示。另外分词之后去除标点符合、数字甚至无关成分(停用词)是特征预处理的一项重要技术。
中文分词是一个专门的技术领域
我们观察(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”),这可以理解成一个向量:向量的每一维度都表示该特征词在文本中的特定位置。这种将特征拆分成更小的单元,依据这些更灵活、更细粒度的特征进行判断的思维方式,在自然语言和机器学习中都是非常常见和有效的。
P
(
“
垃
圾
邮
件
”
∣
“
我
”
,
“
司
”
,
“
可
”
,
“
办
理
”
,
“
正
规
发
票
”
,
“
保
真
”
,
“
增
值
税
”
,
“
发
票
”
,
“
点
数
”
,
“
优
惠
”
)
)
=
P
(
(
“
我
”
,
“
司
”
,
“
可
”
,
“
办
理
”
,
“
正
规
发
票
”
,
“
保
真
”
,
“
增
值
税
”
,
“
发
票
”
,
“
点
数
”
,
“
优
惠
”
)
|
“
垃
圾
邮
件
”
)
P
(
“
垃
圾
邮
件
”
)
P
(
(
“
我
”
,
“
司
”
,
“
可
”
,
“
办
理
”
,
“
正
规
发
票
”
,
“
保
真
”
,
“
增
值
税
”
,
“
发
票
”
,
“
点
数
”
,
“
优
惠
”
)
)
P(“垃圾邮件”|“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))=\frac{P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|“垃圾邮件”)P(“垃圾邮件”)}{P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))}
P(“垃圾邮件”∣“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))=P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|“垃圾邮件”)P(“垃圾邮件”)
P ( “ 正 常 邮 件 ” ∣ “ 我 ” , “ 司 ” , “ 可 ” , “ 办 理 ” , “ 正 规 发 票 ” , “ 保 真 ” , “ 增 值 税 ” , “ 发 票 ” , “ 点 数 ” , “ 优 惠 ” ) ) = P ( ( “ 我 ” , “ 司 ” , “ 可 ” , “ 办 理 ” , “ 正 规 发 票 ” , “ 保 真 ” , “ 增 值 税 ” , “ 发 票 ” , “ 点 数 ” , “ 优 惠 ” ) | “ 正 常 邮 件 ” ) P ( “ 正 常 邮 件 ” ) P ( ( “ 我 ” , “ 司 ” , “ 可 ” , “ 办 理 ” , “ 正 规 发 票 ” , “ 保 真 ” , “ 增 值 税 ” , “ 发 票 ” , “ 点 数 ” , “ 优 惠 ” ) ) P(“正常邮件”|“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))=\frac{P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|“正常邮件”)P(“正常邮件”)}{P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))} P(“正常邮件”∣“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))=P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|“正常邮件”)P(“正常邮件”)
8.条件独立假设
下面我们会马上看到一个简单粗暴的假设。
概率P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|“垃圾邮件”)依然不好求,我们引进一个很朴素的近似。为了让公式显得更加紧凑,我们令字母S表示“垃圾邮件”,令字母H表示“正常邮件”。近似公式如下:
P
(
(
“
我
”
,
“
司
”
,
“
可
”
,
“
办
理
”
,
“
正
规
发
票
”
,
“
保
真
”
,
“
增
值
税
”
,
“
发
票
”
,
“
点
数
”
,
“
优
惠
”
)
∣
S
)
=
P
(
“
我
”
∣
S
)
×
P
(
“
司
”
∣
S
×
P
(
“
可
”
∣
S
)
×
P
(
“
办
理
”
∣
S
)
×
P
(
“
正
规
发
票
”
∣
S
)
×
P
(
“
保
真
”
∣
S
)
P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|S) =P(“我”|S)×P(“司”|S×P(“可”|S)×P(“办理”|S)×P(“正规发票”|S)×P(“保真”|S)
P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)∣S)=P(“我”∣S)×P(“司”∣S×P(“可”∣S)×P(“办理”∣S)×P(“正规发票”∣S)×P(“保真”∣S)
×
P
(
“
增
值
税
”
∣
S
)
×
P
(
“
发
票
”
∣
S
)
×
P
(
“
点
数
”
∣
S
)
×
P
(
“
优
惠
”
∣
S
)
×P(“增值税”|S)×P(“发票”|S)×P(“点数”|S)×P(“优惠”|S)
×P(“增值税”∣S)×P(“发票”∣S)×P(“点数”∣S)×P(“优惠”∣S)
这就是传说中的条件独立假设。这样处理后的式子中的每一项就特别好求了!只需要分别统计各个类别邮件中该关键词出现的概率就可以了,例如
P
(
“
发
票
”
|
S
)
=
垃
圾
邮
件
中
的
“
发
票
个
数
”
垃
圾
遇
见
中
所
有
词
语
的
次
数
P(“发票”|S)=\frac{垃圾邮件中的“发票个数”}{垃圾遇见中所有词语的次数}
P(“发票”|S)=垃圾遇见中所有词语的次数垃圾邮件中的“发票个数”
统计次数非常方便,而且样本数量足够大,算出来的概率比较接近真实。于是垃圾邮件识别的问题就可解了。
9.朴素贝叶斯(Naive Bayes),"Naive"在何处?
将句子(“我”,“司”,“可”,“办理”,“正规发票”) 中的 (“我”,“司”)与(“正规发票”)调换一下顺序,就变成了一个新的句子(“正规发票”,“可”,“办理”, “我”, “司”)。新句子与旧句子的意思完全不同。但由于乘法交换律,朴素贝叶斯方法中算出来二者的条件概率完全一样!计算过程如下:
P
(
(
“
我
”
,
“
司
”
,
“
可
”
,
“
办
理
”
,
“
正
规
发
票
”
)
∣
S
)
=
P
(
“
我
”
∣
S
)
P
(
“
司
”
∣
S
)
P
(
“
可
”
∣
S
)
P
(
“
办
理
”
∣
S
)
P
(
“
正
规
发
票
”
∣
S
)
=
P
(
“
正
规
发
票
”
∣
S
)
P
(
“
可
”
∣
S
)
P
(
“
办
理
”
∣
S
)
P
(
“
我
”
∣
S
)
P
(
“
司
”
∣
S
)
=
P
(
(
“
正
规
发
票
”
,
“
可
”
,
“
办
理
”
,
“
我
”
,
“
司
”
)
∣
S
)
P((“我”,“司”,“可”,“办理”,“正规发票”)|S) =P(“我”|S)P(“司”|S)P(“可”|S)P(“办理”|S)P(“正规发票”|S) =P(“正规发票”|S)P(“可”|S)P(“办理”|S)P(“我”|S)P(“司”|S)=P((“正规发票”,“可”,“办理”,“我”,“司”)|S)
P((“我”,“司”,“可”,“办理”,“正规发票”)∣S)=P(“我”∣S)P(“司”∣S)P(“可”∣S)P(“办理”∣S)P(“正规发票”∣S)=P(“正规发票”∣S)P(“可”∣S)P(“办理”∣S)P(“我”∣S)P(“司”∣S)=P((“正规发票”,“可”,“办理”,“我”,“司”)∣S)
也就是说,在朴素贝叶斯眼里,**“我司可办理正规发票”与“正规发票可办理我司”完全相同。朴素贝叶斯失去了词语之间的顺序信息。**这就相当于把所有的词汇扔进到一个袋子里随便搅和,贝叶斯都认为它们一样。因此这种情况也称作词袋子模型(bag of words)。
10. 处理重复词语的三种方式
我们之前处理的垃圾邮件向量(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”),其中每个词都不重复。而这在现实中其实很少见。因为如果文本长度增加,或者分词方法改变,必然会有许多词重复出现。例如
“
代
开
发
票
。
增
值
税
发
票
,
正
规
发
票
。
”
分
词
后
为
向
量
:
(
“
代
开
”
,
“
发
票
”
,
“
增
值
税
”
,
“
发
票
”
,
“
正
规
”
,
“
发
票
”
)
“代开发票。增值税发票,正规发票。” 分词后为向量: (“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)
“代开发票。增值税发票,正规发票。”分词后为向量:(“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)
其中“发票”重复了三次。
- 多项式模型(考虑重复次数)
P ( ( “ 代 开 ” , “ 发 票 ” , “ 增 值 税 ” , “ 发 票 ” , “ 正 规 ” , “ 发 票 ” ) ∣ S ) = P ( “ 代 开 ” ” ∣ S ) P ( “ 发 票 ” ∣ S ) P ( “ 增 值 税 ” ∣ S ) P ( “ 发 票 ” ∣ S ) P ( “ 正 规 ” ∣ S ) P ( “ 发 票 ” ∣ S ) = P ( “ 代 开 ” ” ∣ S ) P 3 ( “ 发 票 ” ∣ S ) P ( “ 增 值 税 ” ∣ S ) P ( “ 正 规 ” ∣ S ) 注 意 这 一 项 : P 3 ( “ 发 票 ” ∣ S ) P((“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)|S)=P(“代开””|S)P(“发票”|S)P(“增值税”|S)P(“发票”|S)P(“正规”|S)P(“发票”|S)=P(“代开””|S)P^3(“发票”|S)P(“增值税”|S)P(“正规”|S) 注意这一项: P^3(“发票”|S) P((“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)∣S)=P(“代开””∣S)P(“发票”∣S)P(“增值税”∣S)P(“发票”∣S)P(“正规”∣S)P(“发票”∣S)=P(“代开””∣S)P3(“发票”∣S)P(“增值税”∣S)P(“正规”∣S)注意这一项:P3(“发票”∣S) - 伯努利模型(不考虑重复次数,将重复的词语都视为1次,但是效果会稍微差点)
P ( ( “ 代 开 ” , “ 发 票 ” , “ 增 值 税 ” , “ 发 票 ” , “ 正 规 ” , “ 发 票 ” ) ∣ S ) = P ( “ 发 票 ” ∣ S ) P ( “ 代 开 ” ” ∣ S ) P ( “ 增 值 税 ” ∣ S ) P ( “ 正 规 ” ∣ S ) P((“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)|S)=P(“发票”|S)P(“代开””|S)P(“增值税”|S)P(“正规”|S) P((“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)∣S)=P(“发票”∣S)P(“代开””∣S)P(“增值税”∣S)P(“正规”∣S) - 混合模型(在训练时考虑词语的重复次数,测试时不考虑重复次数)
在实践中采用那种模型,关键看具体的业务场景,一个简单经验就是,对于垃圾邮件识别,混合模型会好一些。
11 去除停用词与选择关键词
我们继续观察(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”) 这句话。其实,像“我”、“可”之类词其实非常中性,无论其是否出现在垃圾邮件中都无法帮助判断的有用信息。所以可以直接不考虑这些典型的词。这些无助于我们分类的词语叫作“停用词”(Stop Words)。这样可以减少我们训练模型、判断分类的时间。 于是之前的句子就变成了(“司”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”) 。
我们进一步分析。以人类的经验,其实“正规发票”、“发票”这类的词如果出现的话,邮件作为垃圾邮件的概率非常大,可以作为我们区分垃圾邮件的“关键词”。而像“司”、“办理”、“优惠”这类的词则有点鸡肋,可能有助于分类,但又不那么强烈。如果想省事做个简单的分类器的话,则可以直接采用“关键词”进行统计与判断,剩下的词就可以先不管了。于是之前的垃圾邮件句子就变成了(“正规发票”,“发票”) 。这样就更加减少了我们训练模型、判断分类的时间,速度非常快。
**“停用词”和“关键词”**一般都可以提前靠人工经验指定。不同的“停用词”和“关键词”训练出来的分类器的效果也会有些差异。
12 浅谈平滑技术(防止关键词出现概率为0的情况)
在计算以下条件假设的概率时:
P
(
(
“
我
”
,
“
司
”
,
“
可
”
,
“
办
理
”
,
“
正
规
发
票
”
)
∣
S
)
=
P
(
“
我
”
∣
S
)
P
(
“
司
”
∣
S
)
P
(
“
可
”
∣
S
)
P
(
“
办
理
”
∣
S
)
P
(
“
正
规
发
票
”
∣
S
)
P((“我”,“司”,“可”,“办理”,“正规发票”)|S) =P(“我”|S)P(“司”|S)P(“可”|S)P(“办理”|S)P(“正规发票”|S)
P((“我”,“司”,“可”,“办理”,“正规发票”)∣S)=P(“我”∣S)P(“司”∣S)P(“可”∣S)P(“办理”∣S)P(“正规发票”∣S)
我们扫描一下训练集,发现**“正规发票”这个词从出现过!!!,于是P(“正规发票”|S)=0 …问题严重了,整个概率都变成0了!!!朴素贝叶斯方法面对一堆0,很凄惨地失效了…更残酷的是**这种情况其实很常见,**因为哪怕训练集再大,也可能有覆盖不到的词语。**本质上还是样本数量太少,不满足大数定律,计算出来的概率失真。为了解决这样的问题,一种分析思路就是直接不考虑这样的词语,但这种方法就相当于默认给P(“正规发票”|S)赋值为1。其实效果不太好,大量的统计信息给浪费掉了。我们进一步分析,既然可以默认赋值为1,为什么不能默认赋值为一个很小的数?这就是平滑技术的基本思路,依旧保持着一贯的作风,朴实/土但是直接而有效。
对于伯努利模型(不计算词语的频数),P(“正规发票”|S)的一种平滑算法是:
P
(
“
正
规
发
票
”
∣
S
)
=
出
现
“
正
规
发
票
”
的
垃
圾
邮
件
的
封
数
+
1
每
封
垃
圾
邮
件
中
所
有
词
汇
出
现
的
次
数
(
出
现
了
只
计
算
一
次
)
的
总
和
+
2
P(“正规发票”|S)=\frac{出现“正规发票”的垃圾邮件的封数+1}{每封垃圾邮件中所有词汇出现的次数(出现了只计算一次)的总和+2}
P(“正规发票”∣S)=每封垃圾邮件中所有词汇出现的次数(出现了只计算一次)的总和+2出现“正规发票”的垃圾邮件的封数+1
对于多项式模型(计算词语的频数),P(“正规发票”|S)的一种平滑算法是:
P
(
“
发
票
”
|
S
)
=
每
封
垃
圾
邮
件
中
出
现
“
发
票
”
的
次
数
总
和
+
1
每
封
垃
圾
邮
件
中
所
有
词
出
现
次
数
(
计
算
重
复
次
数
)
的
总
和
+
被
统
计
的
词
表
的
词
语
数
量
P(“发票”|S)=\frac{每封垃圾邮件中出现“发票”的次数总和+1}{每封垃圾邮件中所有词出现次数(计算重复次数)的总和+被统计的词表的词语数量}
P(“发票”|S)=每封垃圾邮件中所有词出现次数(计算重复次数)的总和+被统计的词表的词语数量每封垃圾邮件中出现“发票”的次数总和+1
平滑技术都是给未出现在训练集中的词语一个估计的概率,而相应地调低其他已经出现的词语的概率。 平滑技术是因为数据集太小而产生的现实需求。如果数据集足够大,平滑技术对结果的影响将会变小。
13. 内容小结
我们找了个最简单最常见的例子:垃圾邮件识别,说明了一下朴素贝叶斯文本分类的思路过程。基本思路是先区分好训练集和测试集,对文本集进行分词、去除标点符号邓特征预处理的操作,然后使用条件独立假设,将原概率转换成词概率乘积,再进行后续处理。
贝
叶
斯
公
式
+
条
件
独
立
假
设
=
朴
素
贝
叶
斯
方
法
贝叶斯公式+条件独立假设=朴素贝叶斯方法
贝叶斯公式+条件独立假设=朴素贝叶斯方法
基于重复词在训练阶段和判断(测试)阶段的三种不同处理方式,我们相应的有伯努利模型、多项式模型和混合模型。在训练阶段,如果样本集合太小导致某些词并未出现,我们可以采用平滑技术对概率给一个估计值。而且并不是所有的词语都需要统计,我们可以按照相应的“停用词”和“关键词”对模型进行进一步简化,提高训练和判断速度。
14. 为什么不直接匹配关键词来识别垃圾邮件?
有同学可能会问:“何必费这么大劲算那么多词的概率?直接看邮件中有没有‘代开发票’、‘转售发票’之类的关键词不就得了?如果关键词比较多就认为是垃圾邮件呗。”
其实关键词匹配的方法如果有效的话真不必用朴素贝叶斯。毕竟这种方法简单嘛,**就是一个字符串匹配。**从历史来看,之前没有贝叶斯方法的时候主要也是用关键词匹配。**但是这种方法准确率太低。**我们在工作项目中也尝试过用关键词匹配的方法去进行文本分类,发现大量误报。感觉就像扔到垃圾箱的邮件99%都是正常的!这样的效果不忍直视。而加一个朴素贝叶斯方法就可能把误报率拉低近一个数量级,体验好得不要不要的。
**另一个原因是词语会随着时间不断变化。**发垃圾邮件的人也不傻,当他们发现自己的邮件被大量屏蔽之后,也会考虑采用新的方式,**如变换文字、词语、句式、颜色等方式来绕过反垃圾邮件系统。比如对于垃圾邮件“我司可办理正规发票,17%增值税发票点数优惠”,他们采用火星文:“涐司岢办理㊣規髮票,17%增値稅髮票嚸數優蕙”,**那么字符串匹配的方法又要重新找出这些火星文,一个一个找出关键词,重新写一些匹配规则。更可怕的是,这些规则可能相互之间的耦合关系异常复杂,要把它们梳理清楚又是大一个数量级的工作量。等这些规则失效了又要手动更新新的规则……无穷无尽猫鼠游戏最终会把猫给累死。
而朴素贝叶斯方法却显示出无比的优势。因为它是基于统计方法的,只要训练样本中有更新的垃圾邮件的新词语,哪怕它们是火星文,都能自动地把哪些更敏感的词语(如“髮”、“㊣”等)给凸显出来,并根据统计意义上的敏感性给他们分配适当的权重 ,这样就不需要什么人工了,非常省事。你只需要时不时地拿一些最新的样本扔到训练集中,重新训练一次即可。
小补充一下,对于火星文、同音字等替代语言,一般的分词技术可能会分得不准,最终可能只把一个一个字给分出来,成为“分字”。效果可能不会太好。也可以用过n-gram之类的语言模型,拿到最常见短语。当然,对于英文等天生自带空格来间隔单词的语言,分词则不是什么问题,使用朴素贝叶斯方法将会更加顺畅。
# -*- coding: UTF-8 -*-
"""
搜狗新闻文本分类
"""
import os
import time
import random
import jieba #处理中文
import nltk #处理英文
import sklearn
from sklearn.naive_bayes import MultinomialNB #多项式模式,考虑词频信息
import numpy as np
import pylab as pl
import matplotlib.pylab as plt
#词语去重
def make_word_set(words_file):
words_set = set()
with open(words_file,'r',encoding="utf-8") as fp:
for line in fp.readlines():
word = line.strip()
if len(word)>0 and word not in words_set: #去重
words_set.add(word)
return words_set
#本文处理,也就是样本生成过程
def text_processing(folder_path,test_size=0.2):
folder_list = os.listdir(folder_path)
data_list = []
class_list = []
#遍历文件夹
for folder in folder_list:
new_folder_path = os.path.join(folder_path+folder)
files = os.listdir(new_folder_path)
#读取文件
j=1
for file in files:
if j>100: #怕内存爆掉,你可以注释掉取晚
break
with open(os.path.join(new_folder_path,file),'r',encoding='utf-8') as fp:
raw = fp.read()
##是的,随处可见的jieba中文分词
jieba.enable_parallel(4) #开启并行分词模式,参数为并行进程数,不支持windows
word_cut = jieba.cut(raw,cut_all=True) #精确模式,返回的结构是一个可迭代的generator
word_list = list(word_cut) #generator转换维list,每个词unicode模式
jieba.disable_parallel()
data_list.append(word_list) #训练集list
class_list.append(folder) #类别
j+=1
# ##粗暴的划分训练集和测试集
data_class_list = list(zip(data_list, class_list))
# print(data_class_list[1])
random.shuffle(data_class_list)
index = int(len(data_class_list) * test_size) + 1
train_list = data_class_list[index:]
test_list = data_class_list[:index]
train_data_list,train_class_list = zip(*train_list)
test_data_list,test_class_list = zip(*test_list)
#其实也可以用sklearn自带的部分做
# train_data_list,test_data_list, train_class_list,test_class_list = sklearn.model_selection.train_test_split(train_list,test_list,test_size=test_size)
#统计词频放入all_words_dict
all_words_dict = {}
for word_list1 in train_data_list:
for word in word_list1:
if word in all_words_dict.keys():
all_words_dict[word]+=1
else:
all_words_dict[word]=1
# key函数利用词频进行降序排序
all_words_tuple_list = sorted(all_words_dict.items(),key=lambda f:f[1],reverse=True) #字典(Dictionary) items() 函数以列表返回可遍历的(键, 值) 元组数组。
all_words_list = list(zip(*all_words_tuple_list))[0]
return all_words_list,train_data_list,test_data_list,train_class_list,test_class_list
def words_dict(all_words_list,deleteN,stopwords_set=set()):
#选取特征词
feature_words = []
n=1
for t in range(deleteN,len(all_words_list),1): #并不是从频数最大的选取特征
if n>1000:# features_words的维度是1000
break
if not all_words_list[t].isdigit() and all_words_list[t] not in stopwords_set and 1<len(all_words_list[t])<5:
feature_words.append(all_words_list[t])
n+=1
return feature_words
#文本特征
def text_features(train_data_list,test_data_list,feature_words,flag='nltk'):
def text_features(text,feature_words):
text_words = set(text)
##----------------------------------------------------------------------
if flag=='nltk':
##nltk特征 dict
features = {word:1 if word in text_words else 0 for word in feature_words}
elif flag=='sklearn':
##sklearn特征,list
features = [1 if word in text_words else 0 for word in feature_words]
else:
features =[]
return features
train_feature_list = [text_features(text,feature_words) for text in train_data_list]
test_feature_list = [text_features(text,feature_words) for text in test_data_list]
return train_feature_list,test_feature_list
#分类,同时输出准确率
def text_classifier(train_feature_list,test_feature_list,train_class_list,test_class_list,flag = 'nltk'):
if flag=='nltk':
#使用nltk分类器
train_flist = zip(train_feature_list,train_class_list)
test_flist = zip(test_feature_list,test_class_list)
classifier = nltk.classify.NaiveBayesClassifier.train(train_flist)
test_accuracy = nltk.classify.accuracy(classifier,test_flist)
elif flag=='sklearn':
##使用sklearn分类器
classifier = MultinomialNB().fit(train_feature_list,train_class_list)
test_accuracy = classifier.score(test_feature_list,test_class_list)
else:
test_accuracy=[]
return test_accuracy
if __name__ == '__main__':
print("Start.......")
#文本预处理
folder_path = './Database/SogouC/Sample/'
all_words_list, train_data_list, test_data_list, train_class_list, test_class_list = text_processing(folder_path, test_size=0.2)
#生成stopwords_set
stopwords_file = './stopwords_cn.txt'
stopwords_set = make_word_set(stopwords_file)
##文本特征提取和分类
# flag = 'nltk'
flag = 'sklearn'
deleteNs = range(0,1000,20)
test_accuracy_list = []
for deleteN in deleteNs:
features_words = words_dict(all_words_list,deleteN,stopwords_set)
train_feature_list,test_feature_list = text_features(train_data_list,test_data_list,features_words,flag=flag)
test_accuracy = text_classifier(train_feature_list,test_feature_list,train_class_list,test_class_list,flag)
test_accuracy_list.append(test_accuracy)
print(test_accuracy_list)
#结果评价
plt.figure()
plt.plot(deleteNs,test_accuracy_list)
plt.title("Relationship of deletNs and test accuracy")
plt.xlabel('deleteNs')
plt.ylabel('test_accuracy')
plt.show()
print("finished")