机器学习:详解朴素贝叶斯分类原理 | 例题分析 | Python实现

时间:2022-09-26 10:57:50

0 写在前面

机器学习强基计划聚焦深度和广度,加深对机器学习模型的理解与应用。“深”在详细推导算法模型背后的数学原理;“广”在分析多个机器学习模型:决策树、支持向量机、贝叶斯与马尔科夫决策、强化学习等。

????详情:机器学习强基计划(附几十种经典模型源码合集)


从本节开始正式进入贝叶斯模型,贝叶斯模型属于概率图模型的范畴,在逻辑推断方面有大量的应用——专家系统、推荐系统等,但同时理论难度比较大。本文首先铺垫贝叶斯基本方法,接着以一个实例引出朴素贝叶斯的概念。

1 贝叶斯方法

贝叶斯方法(Bayesian)是贝叶斯学派思考问题的模式,定义如下:

参数先验信息 π ( θ ) + 样本观测数据 X = 后验分布 P ( θ ∣ X ) \text{参数先验信息}\pi \left( \theta \right) +\text{样本观测数据}X=\text{后验分布}P\left( \theta |X \right) 参数先验信息π(θ)+样本观测数据X=后验分布P(θX)

上述思考模式意味着,新观察到的样本信息将修正人们以前对事物的认知。换言之,在得到新的样本信息之前,人们对模型的认知是先验分布 π ( θ ) \pi \left( \theta \right) π(θ),在得到新的样本信息 X X X后,人们对模型的认知为后验分布 P ( θ ∣ X ) P\left( \theta |X \right) P(θX)

贝叶斯方法的深刻原因在于:现实世界本身就是不确定的,人类观察能力有局限性,日常所见几乎都是事物表面。我们往往只能知道从一个袋子里取出来的球是什么颜色,而不能直接看到袋子内的全貌。所以这种通过观测样本的补充,不断更新对事物规律认识的思维方法符合机器学习思想和人类认识规律

贝叶斯方法的数学基础是贝叶斯定理,其数学表述为:若 ⋃ k = 1 n B k = S \bigcup\nolimits_{k=1}^n{B_k}=S k=1nBk=S,且 B i B j = ∅ ( i ≠ j ,      i , j = 1 , 2 , 3 , . . . , n ) B_iB_j=\varnothing \left( i\ne j,\;\;i,j=1,2,3,...,n \right) BiBj=(i=j,i,j=1,2,3,...,n) P ( B k ) > 0 ( k = 1 , 2 , . . . , n ) P\left( B_k \right) >0\left( k=1,2,...,n \right) P(Bk)>0(k=1,2,...,n) P ( A ) > 0 P\left( A \right) >0 P(A)>0,则有:

   P ( B k ∣ A ) = P ( B k ) ⋅ P ( A ∣ B k ) ∑ i = 1 n P ( B i ) ⋅ P ( A ∣ B i ) = 边缘化 全概率公式 P ( B k ) ⋅ P ( A ∣ B k ) P ( A ) \;P\left( B_k\mid A \right) =\frac{P\left( B_k \right) \cdot P\left( A\mid B_k \right)}{\sum_{i=1}^n{P\left( B_i \right) \cdot P\left( A\mid B_i \right)}}\xlongequal[\text{边缘化}]{\text{全概率公式}}\frac{P\left( B_k \right) \cdot P\left( A\mid B_k \right)}{P\left( A \right)} P(BkA)=i=1nP(Bi)P(ABi)P(Bk)P(ABk)全概率公式 边缘化P(A)P(Bk)P(ABk)

其中的等式也称为贝叶斯公式

2 贝叶斯风险

设数据集 D = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , ⋯   , ( x m , y m ) } \boldsymbol{D}=\left\{ \left( \boldsymbol{x}_1, y_1 \right) , \left( \boldsymbol{x}_2, y_2 \right) , \cdots , \left( \boldsymbol{x}_m, y_m \right) \right\} D={(x1,y1),(x2,y2),,(xm,ym)},标签 y i ∈ { C 1 , C 2 , ⋯   , C N } y_i\in \left\{ C_1, C_2, \cdots , C_N \right\} yi{C1,C2,,CN} λ i j \lambda _{ij} λij是将一个真实标记为 C j C_j Cj的样本误分类为 C i C_i Ci所产生的损失,可以设为

λ i j = { 1 , i ≠ j 0 , i = j \lambda _{ij}=\begin{cases} 1, i\ne j\\ 0, i=j\\\end{cases} λij={1,i=j0,i=j

则基于后验概率表示的条件风险(conditional risk)

R ( C i ∣ x ) = ∑ j = 1 N λ i j P ( C j ∣ x ) = 1 − P ( C i ∣ x ) R\left( C_i|\boldsymbol{x} \right) =\sum_{j=1}^N{\lambda _{ij}P\left( C_j|\boldsymbol{x} \right)}=1-P\left( C_i|\boldsymbol{x} \right) R(Cix)=j=1NλijP(Cjx)=1P(Cix)

即将样本 x \boldsymbol{x} x预测为类别 C i C_i Ci的风险为将其预测为其他错误类别的概率之和。

贝叶斯判定准则指出:为最小化总体风险,只需最小化每个样本的条件风险,即对于模型 f f f,有

f ∗ ( x ) = a r g min ⁡ C ∈ Y R ( C ∣ x ) = a r g max ⁡ C ∈ Y P ( C ∣ x ) = a r g max ⁡ C ∈ Y P ( C ) P ( x ∣ C ) P ( x ) = a r g max ⁡ C ∈ Y P ( C ) P ( x ∣ C ) \begin{aligned}f^*\left( \boldsymbol{x} \right) &=\underset{C\in \mathcal{Y}}{\mathrm{arg}\min}R\left( C|\boldsymbol{x} \right)\\ &=\underset{C\in \mathcal{Y}}{\mathrm{arg}\max}P\left( C|\boldsymbol{x} \right) \\&=\underset{C\in \mathcal{Y}}{\mathrm{arg}\max}\frac{P\left( C \right) P\left( \boldsymbol{x}|C \right)}{P\left( \boldsymbol{x} \right)}\\&={\underset{C\in \mathcal{Y}}{\mathrm{arg}\max}P\left( C \right) P\left( \boldsymbol{x}|C \right) }\end{aligned} f(x)=CYargminR(Cx)=CYargmaxP(Cx)=CYargmaxP(x)P(C)P(xC)=CYargmaxP(C)P(xC)

此时 f ∗ f^* f称为贝叶斯最优分类器,与之对应的总体风险

R ( f ∗ ) = E x [ R ( f ( x ) ∣ x ) ] R\left( f^* \right) =\mathbb{E} _{\boldsymbol{x}}\left[ R\left( f\left( \boldsymbol{x} \right) |\boldsymbol{x} \right) \right] R(f)=Ex[R(f(x)x)]

称为贝叶斯风险(Bayes Risk), 表征了贝叶斯分类器通过机器学习产生模型的理论精度上限。

3 从例子出发

例1:考虑下面的数据集

症状 职业 疾病
打喷嚏 护士 感冒
打喷嚏 农夫 过敏
头痛 建筑工人 脑震荡
头痛 建筑工人 感冒
打喷嚏 教师 感冒
头痛 教师 脑震荡

问:一个打喷嚏的建筑工人,患上感冒的概率有多大?

不考虑机器学习背景,仅依靠概率论知识计算。这个问题问的就是

P ( 疾病 = 感冒 ∣ 症状 = 打喷嚏,职业 = 建筑工人 ) P(疾病=感冒|症状=打喷嚏,职业=建筑工人) P(疾病=感冒症状=打喷嚏,职业=建筑工人)

根据贝叶斯公式有

P ( 疾病 = 感冒 ∣ 症状 = 打喷嚏,职业 = 建筑工人 ) = P ( 症状 = 打喷嚏,职业 = 建筑工人 ∣ 疾病 = 感冒 ) P ( 疾病 = 感冒 ) P ( 症状 = 打喷嚏,职业 = 建筑工人 ) \begin{aligned}P(疾病=感冒|症状=打喷嚏,职业=建筑工人)&=\frac{P(症状=打喷嚏,职业=建筑工人|疾病=感冒)P(疾病=感冒)}{P(症状=打喷嚏,职业=建筑工人)}\end{aligned} P(疾病=感冒症状=打喷嚏,职业=建筑工人)=P(症状=打喷嚏,职业=建筑工人)P(症状=打喷嚏,职业=建筑工人疾病=感冒)P(疾病=感冒)

假设各特征独立(至于为什么后面解释),则

P ( 疾病 = 感冒 ∣ 症状 = 打喷嚏,职业 = 建筑工人 ) = P ( 症状 = 打喷嚏 ∣ 疾病 = 感冒 ) P ( 职业 = 建筑工人 ∣ 疾病 = 感冒 ) P ( 疾病 = 感冒 ) P ( 症状 = 打喷嚏 ) P ( 职业 = 建筑工人 ) \begin{aligned}P(疾病=感冒|症状=打喷嚏,职业=建筑工人)&=\frac{P(症状=打喷嚏|疾病=感冒)P(职业=建筑工人|疾病=感冒)P(疾病=感冒)}{P(症状=打喷嚏)P(职业=建筑工人)}\end{aligned} P(疾病=感冒症状=打喷嚏,职业=建筑工人)=P(症状=打喷嚏)P(职业=建筑工人)P(症状=打喷嚏疾病=感冒)P(职业=建筑工人疾病=感冒)P(疾病=感冒)

这里的每一个因子根据表格计算

{ P ( 症状 = 打喷嚏 ∣ 疾病 = 感冒 ) = 1 3 P ( 职业 = 建筑工人 ∣ 疾病 = 感冒 ) = 2 3 P ( 疾病 = 感冒 ) = 1 2 P ( 症状 = 打喷嚏 ) = 1 2 P ( 职业 = 建筑工人 ) = 1 3 \begin{cases}P(症状=打喷嚏|疾病=感冒)=\frac{1}{3}\\ P(职业=建筑工人|疾病=感冒)=\frac{2}{3}\\ P(疾病=感冒)=\frac{1}{2}\\ P(症状=打喷嚏)=\frac{1}{2}\\ P(职业=建筑工人)=\frac{1}{3} \end{cases} P(症状=打喷嚏疾病=感冒)=31P(职业=建筑工人疾病=感冒)=32P(疾病=感冒)=21P(症状=打喷嚏)=21P(职业=建筑工人)=31

所以

P ( 疾病 = 感冒 ∣ 症状 = 打喷嚏,职业 = 建筑工人 ) = 2 3 P(疾病=感冒|症状=打喷嚏,职业=建筑工人)=\frac{2}{3} P(疾病=感冒症状=打喷嚏,职业=建筑工人)=32

4 朴素贝叶斯分类

4.1 核心原理

假设例1中疾病是类别标签,而症状职业是特征,那么例1问的就是观察到一系列特征,样本属于某个类的概率是多少?

这就是朴素贝叶斯分类器的基本原理,用一句话概括:采用属性独立性假设对类后验概率建模

f ∗ ( x ) = a r g max ⁡ C ∈ Y P ( C ) ∏ i = 1 d P ( x i ∣ C ) {f^*\left( \boldsymbol{x} \right) =\underset{C\in \mathcal{Y}}{\mathrm{arg}\max}P\left( C \right) \prod_{i=1}^d{P\left( x_i|C \right)}} f(x)=CYargmaxP(C)i=1dP(xiC)

此式就是贝叶斯风险中,用独立性假设将特征分解出来的形式。因为想法非常简单,所以称为“朴素”的贝叶斯分类,当不采用独立性假设之后,就会衍生出其他的贝叶斯分类算法,后面再专门介绍。

4.2 拉普拉斯平滑

为避免训练集中未出现的属性值抹去该样本的信息量,即 P ( x i ∣ C ) = 0 P\left( x_i|C \right) =0 P(xiC)=0导致其他属性无效,造成预测失败,需要进行拉普拉斯平滑修正(Laplacian Smoothing Correction)

{ P ( C ) = ∣ D C ∣ + 1 ∣ D ∣ + N P ( x i ∣ C ) = ∣ D C , x i ∣ + 1 ∣ D C ∣ + N i \begin{cases} P\left( C \right) =\frac{|\boldsymbol{D}_C|+1}{|\boldsymbol{D}|+N}\\ P\left( x_i|C \right) =\frac{|\boldsymbol{D}_{C,x_i}|+1}{|\boldsymbol{D}_C|+N_i}\\\end{cases} {P(C)=D+NDC+1P(xiC)=DC+NiDC,xi+1

其中 D C \boldsymbol{D}_C DC为训练集 D \boldsymbol{D} D中类别为 C C C的子集, N N N为训练集 D \boldsymbol{D} D中可能的类别数; D C , x i \boldsymbol{D}_{C,x_i} DC,xi为子集 D C \boldsymbol{D}_C DC中属性 I I I取值为 x i x_i xi的子集, N i N_i Ni为子集 D C \boldsymbol{D}_C DC中属性 i i i的可能取值数。

拉普拉斯平滑避免了因训练样本不充分导致的概率估计值为0的情形,并且在训练集扩大时,修正过程引入的先验影响会逐渐减弱,使修正值趋近于实际概率

5 Python实现

5.1 计算类先验概率

'''
* @breif: 计算类先验概率P(C)
* @param[in]: None
* @retval: None
* @note: 类先验概率P(C)结构

{
    C1: { 
            p(C1): float
            num(C1): int
        }
    ...
    Cn: { 
            p(Cn): float
            num(Cn): int
        }
    num: 类别数
}

'''    
def calPrior(self):
    # 可选的类别数
    label = np.unique(self.y)
    self.prior['num'] = len(label)

    # 计算先验概率
    for _label in label:
        self.prior[_label] = {}
        n = int(sum(self.y == _label))
        self.prior[_label]['p'] = (n + self.laplace) / (self.m + len(label))
        self.prior[_label]['N'] = n

5.2 计算类后验概率

'''
* @breif: 计算属性后验概率P(x|C)
* @param[in]: None
* @retval: None
* @note: 后验概率P(x|C)结构

{
    C1: { 
            a1: {
                    type: discrete 离散属性
                    x1: p(x1)
                    ...
                    xn: p(xn)
                    num(a1): int 属性a1的可取值数
                }
            a2: {
                    type: continous 连续属性
                    mean: 样本均值
                    std: 标准差
                }
            ...
            an: ...
        }
    ...
    Cn: ...
    num: 类别数
}
'''
def calPosterior(self):
    if not self.prior:
        raise ValueError("please calculate prior first!")
    # 可选的类别数
    label = np.unique(self.y)
    self.posterior['num'] = len(label)

    # 标签层
    for _label in label:
        self.posterior[_label] = {}
        # 获取标签取值_label的样本集
        labelIndex = np.squeeze(np.argwhere(np.squeeze(self.y)==_label))
        labelX = self.X[:, labelIndex]           
        # 特征层
        for i in range(self.d):
            self.posterior[_label][str(i)] = {}
            # 属性i的可选属性值列表
            attr = np.unique(self.X[i, :])
            # 可选属性数
            attrNum = len(attr)
            # 离散属性
            if attrNum <= 0.85 * self.m:
                self.posterior[_label][str(i)]['num'] = attrNum
                self.posterior[_label][str(i)]['type'] = 'discrete'
                # 计算每个取值的后验概率
                for a in attr:
                    n = int(sum(labelX[i, :] == a))
                    self.posterior[_label][str(i)][a] = (n + self.laplace) / (self.prior[_label]['N'] + attrNum)
            # 连续属性
            else:
                self.posterior[_label][str(i)]['type'] = 'continous'
                self.posterior[_label][str(i)]['std'] = labelX[i, :].std(ddof=1)
                self.posterior[_label][str(i)]['mean'] = labelX[i, :].mean()

5.3 预测

采用机器学习强基计划2-3:图文详解决策树预剪枝、后剪枝原理+Python实现的数据集训练,获得准确率如下

model = NaiveBayes(X, y)
# 训练模型
model.train()
# 模型预测
predictY = model.predict(X)
print("错误:", np.sum(predictY!=y.T), "个\n准确率为:", np.sum(predictY==y.T)/y.size)

>>> 错误: 3>>> 准确率为: 0.8235294117647058

接下来的文章可以看到引入条件依赖假设后的半朴素贝叶斯模型会提升准确率。

完整代码联系下方博主名片获取


???? 更多精彩专栏


????源码获取 · 技术交流 · 抱团学习 · 咨询分享 请联系????