Typesetting math: 69%

统计学习:朴素贝叶斯模型(Numpy实现)

模型

生成模型介绍

我们定义样本空间为XRnXRn,输出空间为Y={c1,c2,...,cK}Y={c1,c2,...,cK}XX为输入空间上的随机向量,其取值为xx,满足xXxXYY为输出空间上的随机变量,设其取值为yy,满足yYyY。我们将容量为mm的训练样本表示为:

D={{x(1),y(1)},{x(2),y(2)},...,{x(m),y(m)}}D={{x(1),y(1)},{x(2),y(2)},...,{x(m),y(m)}}(1)

我们遵循机器学习的一个基本假设,即训练样本是从一个未知的总体分布P(X=x,Y=y)P(X=x,Y=y)中采样产生,且训练样本独立同分布。

我们采取概率模型的视角,即将分类模型表示为条件概率分布P(Y=y|X=x)P(Y=y|X=x)。而依据分布P(Y=y|X=x)P(Y=y|X=x)的求解可将模型分为判别模型和生成模型。判别模型直接对条件概率分布P(Y=y|X=x)P(Y=y|X=x)进行参数估计(估计方法可采用极大似然估计或贝叶斯估计);而生成模型则利用条件概率公式P(Y=y|X=x)=P(X=x,Y=y)P(X=x)P(Y=y|X=x)=P(X=x,Y=y)P(X=x)来计算分布。分子P(X=x,Y=y)P(X=x,Y=y)是一个联合概率分布,能够还原出联合概率分布P(X=x,Y=y)P(X=x,Y=y)是生成模型的一大特性。

朴素贝叶斯模型推导

我们对分子继续运用条件概率公式,进一步得到

P(Y=y|X=x)=P(X=x|Y=y)P(Y=y)P(X=x)P(Y=y|X=x)=P(X=x|Y=y)P(Y=y)P(X=x)(2)

这个公式即大名鼎鼎的贝叶斯公式。 这里我们采用贝叶斯学派的视角,将P(Y=y)P(Y=y)称为先验概率分布,表示在数据观测之前对YY的信念;P(Y=y|X=x)P(Y=y|X=x)称为后验概率分布,表示经过观测数据XX(也称“证据”)校正后对YY的信念。注意不要和和贝叶斯估计中参数θθ的先验和后验分布搞混了,贝叶斯估计也应用了贝叶斯公式,但先验概率分布和后验概率分布的实际含义与这里完全不同。

我们再将分母运用全概率公式展开,我们得到

P(Y=y|X=x)=P(X=x|Y=y)P(Y=y)yYP(X=x|Y=y)P(Y=y)P(Y=y|X=x)=P(X=x|Y=y)P(Y=y)yYP(X=x|Y=y)P(Y=y)(3)

这意味着我们只需要学习概率分布P(Y=y)P(Y=y)P(X=x|Y=y)P(X=x|Y=y),而无需关心P(X=x)P(X=x)

将随机向量xx沿着其特征维度展开,我们继续得到

P(X=x|Y=y)=P(X1=x1,...,Xn=xn|Y=y),n为特征维度P(X=x|Y=y)=P(X1=x1,...,Xn=xn|Y=y),n(4)

这里我们为了简单起见,假设样本属性是离散的,第jj个属性xjxj的属性集为Aj={aj1,aj2,...,ajl,...,ajNj}Aj={aj1,aj2,...,ajl,...,ajNj},满足xjAjxjAj。可以看出,条件概率分布P(X=x|Y=y)P(X=x|Y=y)的参数总量是指数级的(xjxj的属性集AjAj大小为NjNjj=1,2,...,nj=1,2,...,nYY可取值有KK个,那么参数个数为Knj=1NjKnj=1Nj),不能对其直接进行参数估计。

因此,我们决定对原本拥有指数级参数数量的分布进行拆分。这里,朴素贝叶斯法做出了条件独立性假设:样本特征在类确定的条件下条件独立(这也是“朴素”(Naive)一词的得名)。这样我们就能将原本拥有庞大参数的概率分布进行拆分:

P(X=x|Y=y)=P(X1=x1,...,Xn=xn|Y=y)=nj=1P(Xj=xj|Y=y)P(X=x|Y=y)=P(X1=x1,...,Xn=xn|Y=y)=nj=1P(Xj=xj|Y=y)(5)

这样,我们就可以对P(X|Y=y)P(X|Y=y)分布进行高效的参数估计。之后,我们对于输入样本xx,计算概率分布P(Y=y|X=x)P(Y=y|X=x)

P(Y=y|X=x)=P(X=x|Y=y)P(Y=y)yYP(X=x|Y=y)P(Y=y)=nj=1P(Xj=xj|Y=y)P(Y=y)yY[nj=1P(Xj=xj|Y=y)P(Y=y)]P(Y=y|X=x)=P(X=x|Y=y)P(Y=y)yYP(X=x|Y=y)P(Y=y)=nj=1P(Xj=xj|Y=y)P(Y=y)yY[nj=1P(Xj=xj|Y=y)P(Y=y)](6)

我们采取后验概率最大化原则(即最终的输出分类取使条件概率最大的那个),设f(x)f(x)为分类决策函数,即

y=f(x)=argmaxyP(Y=y|X=x)=argmaxynj=1P(Xj=xj|Y=y)P(Y=y)yY[nj=1P(Xj=xj|Y=y)P(Y=y)]y=f(x)=argmaxyP(Y=y|X=x)=argmaxynj=1P(Xj=xj|Y=y)P(Y=y)yY[nj=1P(Xj=xj|Y=y)P(Y=y)](7)

我们发现,不管yy取何值,式(7)(7)中分母总是恒定的,因此我们可以将式(7)(7)化简为

y=f(x)=argmaxyP(Y=y|X=x)=argmaxynj=1P(Xj=xj|Y=y)P(Y=y)y=f(x)=argmaxyP(Y=y|X=x)=argmaxynj=1P(Xj=xj|Y=y)P(Y=y)(8)

这就是朴素贝叶斯模型分类决策函数的最终表达式。

参数估计

极大似然估计

如式(8)(8)中所述,我们需要对先验概率分布P(Y=y)P(Y=y)和条件概率分布P(Xj=xj|Y=y)P(Xj=xj|Y=y)进行参数估计。根据极大似然估计(具体的推导过程可以参见李航《统计学习方法》中的习题解答),我们可以运用训练集DD将先验概率分布P(Y=y)P(Y=y)估计为

P(Y=y)=mi=1(y(i)=y)m,mD中样本个数P(Y=y)=mi=1(y(i)=y)m,mD(9)

同样,条件概率分布P(Xj=xj|Y=y)的估计为

P(Xj=xj|Y=y)=P(Xj=xj,Y=y)P(Y=y)=mi=1I(x(i)j=xj,y(i)=y)mi=1I(y(i)=y),mD中样本个数

贝叶斯估计(平滑修正)

观察式(10)可知,如果训练集中属性值xj和类y没有同时出现过,即P(Xj=xj,Y=y)=0,那么P(Xj=xj|Y=y)=0会直接导致连乘式。这就意味着不管其他属性如何,哪怕其他属性明显符合要求,样本nj=1P(Xj=xj|Y=y)=0x属于类y的概率都会被判为0,这明显不太合理。

因此,为了避免其他属性携带的信息被训练集中未出现的属性值“抹去”,我们采用贝叶斯估计,等价于在估计概率值时通常进行“平滑”(smoothing)(具体的推导过程可以参见李航《统计学习方法》中的习题解答)。即令式(10)修正为

Pλ(Xj=xj|Y=y)=mi=1I(x(i)j=xj,y(i)=y)+λmi=1I(y(i)=y)+Njλ,λ>0,Nj为属性xj可能的取值数

我们常取λ=1,这时称为拉普拉斯平滑(Laplacian smoothing)。

类似地,式(9)中先验概率被修正为:

Pλ(Y=y)=mi=1(y(i)=y)+λm+Kλ,λ>0,K为标签y可能的取值数

可以看出,拉普拉斯平滑解决了训练集样本数不足导致的概率值为 0 的问题。拉普拉斯修正实际上假设了属性值与类别均匀分布,这是在参数估计的过程中额外引入的关于数据的先验 (prior)。当样本容量趋近于无穷时,我们发现修正过程所引入的先验的影响也趋近于 0,使得计算的概率值趋近于实际的概率值。

算法

在实际的应用中,朴素贝叶斯模型有两种训练方式。

若使用的场景对模型的预测速度要求较高,在给定训练集D的情况下,我们将概率分布Pλ(Y=y)和概率分布Pλ(Xj=xj|Y=y)所有可能的取值(yYxjAjAj为第j个样本属性的取值集合)都计算出来存好,然后在测试样本x来了之后,通过“查表”的方式将对应的概率值检索出来,然后再对其类别进行判别。这样,我们计算概率分布Pλ(Y=y)和概率分布Pλ(Xj=xj|Y=y)所有可能取值的过程即对朴素贝叶斯模型进行显式训练的过程。
朴素贝叶斯的训练和测试算法如下:
朴素贝叶斯算法

如果我们不断有新的训练数据产生,可以采用“懒惰学习”(lazy learning)的方法,先不进行任何训练,测试样本来了之后再依照测试样本的属性xj和当前数据集的状况来计算单点概率,这样可以避免对所有可能的属性都计算单点概率。若训练数据不断增加,则可在现有计算结果的基础上,仅仅对新增样本的属性值所涉及的单点概率进行计数修正,这样可以实现“增量学习”。

代码实现

使用Python语言实现朴素贝叶斯算法如下(这里我们采用Iris数据集进行测试)。

from numpy.lib.index_tricks import c_
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
from copy import deepcopy
class NaiveBayes():
    def __init__(self, A, C, lambda_v=1):
        #常常取lambda_v=1,此时称为拉普拉斯平滑
        self.K = len(C)
        self.A = deepcopy(A)
        self.C = deepcopy(C)
        self.y_cnt = {} #记录sum I(y_i = y)
        self.y_prob = {}  # 记录平滑后的P_lambda(Y = y)
        self.lambda_v = lambda_v # 平滑参数
        self.x_condition_y_prob = {} 
        #记录P_lambda(X_j = x_j | Y = y),这是个字典-列表-字典嵌套
        # 初始化y_cnt和y_prob
        for c_k in self.C:
            self.y_cnt[c_k], self.y_prob[c_k] = 0, 0
        # 初始化attr_cnt和x_condition_y_prob
        for c_k in self.C:
            self.x_condition_y_prob[c_k] = [dict() for j in range(len(self.A))]
            for j in range(len(self.A)):
                for a_j_l in self.A[j]:
                    self.x_condition_y_prob[c_k][j][a_j_l] = 0

    def fit(self, X_train, y_train):
        try:
            assert(X_train.shape[0] == y_train.shape[0])
        except:
            AssertionError("input dimension 0 should be the same!")

        # 记录 sum I(y_i = y)
        for y in y_train:
            try:
                self.y_cnt[y] += 1 
                #如果训练集中没出现过,此处y_cnt[y]将一直为0
            except:
                raise ValueError("invalid label!")   
        # 计算P_lambda(Y = c_k)
        # 并对所有概率进行平滑处理
        for c_k in self.C:
            self.y_prob[c_k] = self.y_cnt[c_k] 
            self.y_prob[c_k] += self.lambda_v
            self.y_prob[c_k] /= (X_train.shape[0] + self.K * self.lambda_v)
    
        # 记录sum I(x_j(i) = x_j, y(i) = y)
        # 遍历每一个训练样本
        for x, y in zip(X_train, y_train):
            # 遍历训练样本中的每一个属性
            for j, attr in enumerate(x):
                try:
                    self.x_condition_y_prob[y][j][attr] += 1 
                    #如果训练集中没出现过,此处self.x_condition_y_prob[y][attr] 将一直为0
                except:
                    raise ValueError("invalid attribution %.1f!" % attr)
        # 计算P_lambda(X_j = x_j | Y = c_k)
        # 并对所有样本进行平滑处理
        for c_k in self.C:
            c_cnt = self.y_cnt[c_k] 
            # 遍历所有属性的属性集合
            for j in range(len(self.A)):
                # 遍历每一个属性的取值集合
                for a_j_l in self.A[j]:
                    self.x_condition_y_prob[c_k][j][a_j_l] += self.lambda_v
                    self.x_condition_y_prob[c_k][j][a_j_l]/=(c_cnt + len(self.A[j])*self.lambda_v)             
            
    def pred(self, X_test):
        # 遍历测试集中的每一个样本
        y_pred = []
        for x in X_test:
            max_ck = self.C[0]
            max_prob = -1

            # 考察每一种可能的标签
            for c_k in self.C:
                prob = 1.0
                # 考察该测试样本的每一个属性
                for j, attr in enumerate(x):
                    prob *= self.x_condition_y_prob[c_k][j][attr]

                prob *= self.y_prob[c_k]
                if prob == 0:
                    raise ValueError("prob underflow!")
                if prob > max_prob:
                    max_prob = prob
                    max_ck = c_k
            y_pred.append(max_ck)
        return np.array(y_pred)
                
# 获得特征和类标记的结合
def get_A_and_C(X, y):
    # A为第j各属性的可能取值集合
    A, C = [set() for j in range(X.shape[1])], set()
    # 遍历每个样本的特征向量
    for x in X:
        for j, attr in enumerate(x):
            A[j].add(attr)
    # 遍历每个样本的标签
    for c_k in y:
        C.add(c_k)
    A = [ list(A_j) for A_j in A]
    return list(A), list(C)

if __name__ == "__main__":
    X, y = load_iris(return_X_y=True)
    A, C = get_A_and_C(X, y)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=0)
    clf = NaiveBayes(A, C)
    clf.fit(X_train, y_train)
    y_pred = clf.pred(X_test)
    acc_score = accuracy_score(y_test, y_pred)
    print("The accuracy is: %.1f" % acc_score)

最终的测试结果如下:

The accuracy is: 0.9

可以看出我们实现的算法在Iris数据集上取得了90%的精度,说明我们算法的效果不错。

参考文献

  • [1] 李航. 统计学习方法(第2版)[M]. 清华大学出版社, 2019.
  • [2] 周志华. 机器学习[M]. 清华大学出版社, 2016.
  • [3] Calder K. Statistical inference[J]. New York: Holt, 1953.
posted @   orion-orion  阅读(801)  评论(0编辑  收藏  举报
编辑推荐:
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示