1 概述

1.1 真正的概率分类器

在许多分类算法应用中,特征和标签之间的关系并非是决定性的。如想预测一个人究竟是否能在泰坦尼克号海难中生存下来,可以建一棵决策树来学习训练集。在训练中,其中一个人的特征为30岁、男、普通舱,最后他在海难中去世了。当测试时,发现有另一个人的特征也为30岁、男、普通舱。基于在训练集中的学习,决策树必然会给这个人打上标签:去世。然而这个人的真实情况一定是去世吗?并非如此。
也许这个人是心脏病患者,得到了上救生艇的优先权,也可能,这个人就是挤上了救生艇,活了下来。对分类算法来说,基于训练的经验,这个人“很有可能”是没有活下来,但是算法永远也无法确定“这个人一定没有活下来”。即便这个人最后真的没有活下来,算法也无法确定基于训练数据给出的判断,是否真的解释了这个人没有活下来的真实原因。也就是说,算法得出的结论,永远不是100%确定的,更多的是判断出了一种“样本的标签更可能是某类的可能性”,而非一种“确定”。通过某些规定,如在决策树的叶子节点上占比较多的标签,就是叶子节点上所有样本的标签,来强行让算法返回一个固定结果。但许多时候,也希望能够理解算法判断出的可能性本身。
每种算法使用不同的指标来衡量这种可能性。如,决策树使用的就是叶子节点上占比较多的标签所占的比例(接口predict_proba调用),逻辑回归使用的是sigmoid函数压缩后的似然(接口predict_proba调用),而SVM使用的是样本点到决策边界的距离(接口decision_function调用)。但这些指标的本质,其实都是一种“类概率”的表示,可以通过归一化或sigmoid函数将这些指标压缩到0-1之间,让它们表示模型对预测的结果究竟有多大的把握(置信度)。但无论如何,都希望使用真正的概率来衡量可能性,因此就有真正的概率算法:朴素贝叶斯。
朴素贝叶斯是一种直接衡量标签与特征之间的概率关系的有监督学习算法,是一种专注分类的算法。朴素贝叶斯的算法根源就是基于概率论和数理统计的贝叶斯理论,因此它是真正的概率模型。

1.2 朴素贝叶斯是如何工作的

朴素贝叶斯被认为是最简单的分类算法之一。首先,需要了解一些概率论的基本理论。假设有两个随机变量 X X X和 Y Y Y,分别可以取值为 x x x和 y y y。有这两个随机变量,可以定义两种概率:

关键概念:联合概率与条件概率
联合概率:“ X X X取值为 x x x”和“ Y Y Y取值为 y y y”两个事件同时发生的概率,表示为 P ( X = x , Y = y ) P(X=x,Y=y) P(X=x,Y=y)
条件概率:在“ X X X取值为 x x x”的前提下,“ Y Y Y取值为 y y y”的概率,表示为 P ( Y = y ∣ X = x ) P(Y=y\mid X=x) P(Y=y∣X=x)

在概率论中,可以证明两个事件的联合概率等于这个两个事件任意条件概率*这个条件事件本身的概率。
举个例子:令 X X X为“气温”, Y Y Y为“七星瓢虫冬眠”,则 X X X和 Y Y Y可能的取值分别为 x x x和 y y y,其中 x = { 0 , 1 } x = \{0,1\} x={0,1},表示没有下降到0度以下,1表示下降到了0度以下。 y = { 0 , 1 } y=\{0,1\} y={0,1},其中0表示否,1表示是。两个时间分别发生的概率为:
P ( X = 1 ) = 50 % P(X = 1)=50\% P(X=1)=50%,则是说明气温下降到0度以下的可能性为50%,则 P ( X = 0 ) = 1 − P ( X = 1 ) = 50 % P(X=0)=1-P(X=1) = 50\% P(X=0)=1−P(X=1)=50%
P ( Y = 1 ) = 70 % P(Y = 1)=70\% P(Y=1)=70%,则是说明七星瓢虫会冬眠的可能性为70%,则 P ( Y = 0 ) = 1 − P ( Y = 1 ) = 30 % P(Y=0) = 1-P(Y=1)=30\% P(Y=0)=1−P(Y=1)=30%
则这两个事件的联合概率为 P ( X = 1 , Y = 1 ) P(X=1,Y=1) P(X=1,Y=1),这个概率代表了气温下降到0度以下和七星瓢虫去冬眠这两件事情同时发生的概率。而两个事件之间的条件概率为 P ( Y = 1 ∣ X = 1 ) P(Y=1\mid X=1) P(Y=1∣X=1),这个概率代表了气温下降到0度以下这个条件被满足之后,七星瓢虫会去冬眠的概率。也就是说,气温下降到0度以下,一定程度上影响力七星瓢虫去冬眠这个事件。
P ( X = x , Y = y ) = P ( Y = y ∣ X = x ) ∗ P ( X = x ) = P ( X = x ∣ Y = y ) ∗ P ( Y = y ) P(X = x,Y=y)=P(Y=y \mid X=x)*P(X=x) = P(X=x \mid Y=y)*P(Y=y) P(X=x,Y=y)=P(Y=y∣X=x)∗P(X=x)=P(X=x∣Y=y)∗P(Y=y)
简单一些,则可以将上面的式子写成:
P ( X , Y ) = P ( Y ∣ X ) ∗ P ( X ) = P ( X ∣ Y ) ∗ P ( Y ) P(X ,Y)=P(Y \mid X)*P(X) = P(X \mid Y)*P(Y) P(X,Y)=P(Y∣X)∗P(X)=P(X∣Y)∗P(Y)
由上面的式子,可以得到贝叶斯理论等式:
P ( Y ∣ X ) = P ( X ∣ Y ) ∗ P ( Y ) P ( X ) P(Y\mid X) = \frac{P(X\mid Y)*P(Y)}{P(X)} P(Y∣X)=P(X)P(X∣Y)∗P(Y)​
而这个式子,就是一切贝叶斯算法的根源理论。可以把特征 X X X当成是条件事件,而要求解的标签 Y Y Y当成是被满足条件后会被影响的结果,而两者之间的概率关系就是 P ( Y ∣ X ) P(Y\mid X) P(Y∣X), 这个概率在机器学习中,被称之为是标签的后验概率(posterior probability),即是说知道了条件,再去求解结果,而标签 Y Y Y在没有任何条件限制下取值为某个值的概率,被写作 P ( Y ) P(Y) P(Y),与后验概率相反,这是完全没有任何限制的,标签的先验概率(prior probability)。而 P ( X ∣ Y ) P(X \mid Y) P(X∣Y)被称为“类的条件概率”(其中类是指标签的分类),表示当 Y Y Y的取值固定的时候, X X X为某个值的概率。

1.2.1 瓢虫冬眠:理解 P ( Y ∣ X ) P(Y|X) P(Y∣X)

假设 X X X是“气温”,也是特征, Y Y Y是“七星瓢虫冬眠”,就是标签。建模的目的是预测七星瓢虫是否会冬眠,也就是说现在求解的是 P ( Y ∣ X ) P(Y\mid X) P(Y∣X),然后根据贝叶斯理论等式开始做各种计算和分析。那么写作 P ( Y ∣ X ) P(Y\mid X) P(Y∣X)的这个概率,代表了什么?更具体一点,可以代表多少种概率?

P ( Y ∣ X ) P(Y\mid X) P(Y∣X)代表了多少种情况的概率?
P ( Y = 1 ∣ X = 1 ) P(Y=1\mid X=1) P(Y=1∣X=1)表示气温0度以下的条件下,七星瓢虫冬眠的概率
P ( Y = 1 ∣ X = 0 ) P(Y=1\mid X=0) P(Y=1∣X=0)表示气温0度以上的条件下,七星瓢虫冬眠的概率
P ( Y = 0 ∣ X = 1 ) P(Y=0\mid X=1) P(Y=0∣X=1)表示气温0度以下的条件下,七星瓢虫没有冬眠的概率
P ( Y = 0 ∣ X = 0 ) P(Y=0\mid X=0) P(Y=0∣X=0)表示气温0度以上的条件下,七星瓢虫没有冬眠的概率

数学中的第一个不步骤,也就是最重要的事情,就是定义清晰。其实在数学中, P ( Y ∣ X ) P(Y\mid X) P(Y∣X)代表了全部的可能性,而不是单一的概率本身。现在 Y Y Y有两种取值, X X X也有两种取值,就让概率 P ( Y ∣ X ) P(Y\mid X) P(Y∣X)的定义变得很模糊,排列组合之后竟然有4种可能。在机器学习中,一个特征 X X X下取值可能远远不止两种,标签也可能是多分类的,还会有多个特征,排列组合下,最后求解的 P ( Y ∣ X ) P(Y\mid X) P(Y∣X)是什么东西,令人感到困惑。同理, P ( Y ) P(Y) P(Y)随着标签中分类的个数,可以有不同的取值。 P ( Y ∣ X ) P(Y\mid X) P(Y∣X)也是一样的。在这里需要澄清的是:机器学习中的简写 P ( Y ) P(Y) P(Y),通常表示标签取到少数类的概率,少数类往往使用正样本表示,也就是 P ( Y = 1 ) P(Y=1) P(Y=1),本质就是所有样本中标签为1的样本所占的比例。如果没有样本不均衡问题,则必须在求解的时候明确, Y Y Y的取值是什么。
而 P ( Y ∣ X ) P(Y\mid X) P(Y∣X)是对于任意一个样本而言,如果这个样本的特征 X X X的取值为1,则表示求解 P ( Y = 1 ∣ X = 1 ) P(Y=1\mid X=1) P(Y=1∣X=1)。如果这个样本下的特征 X X X取值为0,则表示求解 P ( Y = 1 ∣ X = 0 ) P(Y=1\mid X=0) P(Y=1∣X=0)。也就是说, P ( Y ∣ X ) P(Y\mid X) P(Y∣X)是具体到每一个样本上的,究竟求什么概率,由样本本身的特征的取值决定。每个样本的 P ( Y ∣ X ) P(Y\mid X) P(Y∣X)如果大于阈值0.5,则认为样本是少数类(正样本,1),如果这个样本的 P ( Y ∣ X ) P(Y\mid X) P(Y∣X)小于阈值0.5,则认为样本是多数类(负样本,0或者-1)。如果没有具体的样本,只是说明例子,则必须明确 P ( Y ∣ X ) P(Y\mid X) P(Y∣X)中 X X X的取值。
在机器学习中,对每一个样本,不可能只有一个特征 X X X,而是会存在着包含 n n n个特征的取值的特征向量 X \textbf X X。因此机器学习中的后验概率,被写作 P ( Y ∣ X ) P(Y\mid \textbf X) P(Y∣X),其中 X \textbf X X中包含样本在 n n n个特征 X i X_i Xi​上的分别的取值 x i x_i xi​,由此可以表示为 X = { X 1 = x 1 , X 2 = x 2 , . . . , X n = x n } \textbf X = \{ X_1=x_1,X_2=x_2,...,X_n = x_n \} X={X1​=x1​,X2​=x2​,...,Xn​=xn​}。因此,有:

虽然写法不同,但其实都包含着同样的含义。以此为基础,机器学习中,对每一个样本有:
P ( Y = 1 ∣ X ) = P ( X ∣ Y = 1 ) ∗ P ( Y = 1 ) P ( X ) = P ( x 1 , x 2 , . . . , x n ∣ Y = 1 ) ∗ P ( Y = 1 ) P ( x 1 , x 2 , . . . , x n ) P(Y=1\mid \textbf X) = \frac{P(\textbf X\mid Y=1)*P(Y=1)}{P(\textbf X)} = \frac{P(x_1,x_2,...,x_n\mid Y=1)*P(Y=1)}{P(x_1,x_2,...,x_n)} P(Y=1∣X)=P(X)P(X∣Y=1)∗P(Y=1)​=P(x1​,x2​,...,xn​)P(x1​,x2​,...,xn​∣Y=1)∗P(Y=1)​
对于分子而言, P ( Y = 1 ) P(Y=1) P(Y=1)就是少数类占总样本量的比例, P ( X ∣ Y = 1 ) P(\textbf X\mid Y=1) P(X∣Y=1)则需要稍微复杂一点的过程来求解。假设只有两个特征 X 1 , X 2 X_1,X_2 X1​,X2​,由联合概率公式,可以有如下证明:
P ( X 1 , X 2 ∣ Y = 1 ) = P ( X 1 , X 2 , Y = 1 ) P ( Y = 1 ) = P ( X 1 , X 2 , Y = 1 ) P ( X 2 , Y = 1 ) ∗ P ( X 2 , Y = 1 ) P ( Y = 1 ) = P ( X 1 ∣ X 2 , Y = 1 ) ∗ P ( X 2 ∣ Y = 1 ) P(X_1,X_2\mid Y=1) = \frac{P(X_1,X_2,Y=1)}{P(Y=1)}=\frac{P(X_1,X_2,Y=1)}{ P(X_2,Y=1)}*\frac{P(X_2,Y=1)}{P(Y=1)} = P(X_1\mid X_2,Y=1)*P(X_2\mid Y=1) P(X1​,X2​∣Y=1)=P(Y=1)P(X1​,X2​,Y=1)​=P(X2​,Y=1)P(X1​,X2​,Y=1)​∗P(Y=1)P(X2​,Y=1)​=P(X1​∣X2​,Y=1)∗P(X2​∣Y=1)
假设 X 1 X_1 X1​和 X 2 X_2 X2​是有条件独立的:
P ( X 1 , X 2 ∣ Y = 1 ) = P ( X 1 ∣ Y = 1 ) ∗ P ( X 2 ∣ Y = 1 ) P(X_1,X_2\mid Y=1) = P(X_1\mid Y=1)*P(X_2\mid Y=1) P(X1​,X2​∣Y=1)=P(X1​∣Y=1)∗P(X2​∣Y=1)
若推广到 n n n个 X X X上,则有: P ( X ∣ Y = 1 ) = ∏ i = 1 n P ( X i = x i ∣ Y = 1 ) P(\textbf X\mid Y=1)=\prod_{i=1}^nP(X_i=x_i\mid Y=1) P(X∣Y=1)=i=1∏n​P(Xi​=xi​∣Y=1)这个式子证明,在 Y = 1 Y=1 Y=1的条件下,多个特征的取值被同时取到的概率,就等于 Y = 1 Y=1 Y=1的条件下,多个特征的取值被分别取到的概率相乘。其中,假设 X i X_i Xi​与 X 2 X_2 X2​是有条件独立,即可以让公式 P ( X 1 ∣ X 2 , Y = 1 ) = P ( X 1 ∣ Y = 1 ) P(X_1\mid X_2,Y=1)=P(X_1\mid Y=1) P(X1​∣X2​,Y=1)=P(X1​∣Y=1),这是在假设 X 2 X_2 X2​是一个对 X 1 X_1 X1​在某个条件下的取值完全无影响的变量。
假设特征之间是有条件独立的,可以解决众多问题,也简化了很多计算过程,这是朴素贝叶斯被称为“朴素”的理由。因此,贝叶斯在特征之间有较多相关性的数据集上表现不佳,而现实中的数据多多少少都会有一些相关性,所以贝叶斯的分类效力在分类算法中不算特别强大。同时,一些影响特征本身的相关性的降维算法,如PCA和SVD,和贝叶斯连用效果也会不佳。但无论如何,有了这个式子,就可以求解出分子了。
对于贝叶斯理论等式的分母而言,可以使用全概率公式来求解 P ( X ) P(\textbf X) P(X): P ( X ) = ∑ i = 1 m P ( y i ) ∗ P ( X ∣ Y i ) P(\textbf X) = \sum_{i=1}^mP(y_i)*P(\textbf X\mid Y_i) P(X)=i=1∑m​P(yi​)∗P(X∣Yi​)其中 m m m代表标签的种类,也就是说,对于二分类而言有:
P ( X ) = P ( Y = 1 ) ∗ P ( X ∣ Y = 1 ) + P ( Y = 0 ) ∗ P ( X ∣ Y = 0 ) P(\textbf X) = P(Y=1)*P(\textbf X\mid Y=1)+P(Y=0)*P(\textbf X\mid Y=0) P(X)=P(Y=1)∗P(X∣Y=1)+P(Y=0)∗P(X∣Y=0)

1.2.2 贝叶斯的性质与最大后验估计

之前学习的分类算法总是有一个特点:这些算法先从训练集中学习,获取某种信息来建立模型,然后用模型去对测试集进行预测。如逻辑回归,要先从训练集中获取让损失函数最小的参数,然后用参数建立模型,再对测试集进行预测。如支持向量机,要先从训练集中获取让边际最大的决策边界,然后用决策边界对测试集进行预测。相同的流程在决策树、随机森林中也出现,在fit的时候必然已经构造好了能够对测试集进行判断的模型。而朴素贝叶斯,似乎没有这个过程。
提供一张有标签的表,然后提出要预测零下的情况下,年龄为20天的瓢虫,会冬眠的概率,这可以直接计算出来,没有利用训练集求解某个模型的过程,也没有训练完毕了来做测试的过程,而是直接对有标签的数据提出要求,就可以得到预测结果。
这说明,朴素贝叶斯是一个不建模的算法。其他的不建模算法,如KMeans、PCA,都是无监督学习,而朴素贝叶斯是第一个有监督的、不建模的分类算法。刚才的例子中,有标签的表格是训练集,而提出的要求“零下的实话,年龄为20天的瓢虫”就是没有标签的测试集。认为训练集和测试集都来自于同一个不可获得的大样本下,并且这个大样本下的各种属性所表现出来的规律应当是一致的,因此训练集上计算出来的各种概率,可以直接放在测试集上使用。即便不建模,也可以完成分类。
但在实际中,贝叶斯的决策过程并没有给出的例子那么简单。
P ( Y = 1 ∣ X ) = P ( Y = 1 ) ∗ ∏ i = 1 n P ( x i ∣ Y = 1 ) P ( X ) P(Y=1\mid \textbf X) = \frac{P(Y=1)*\prod_{i=1}^nP(x_i\mid Y=1)}{P(\textbf X)} P(Y=1∣X)=P(X)P(Y=1)∗∏i=1n​P(xi​∣Y=1)​
对于这个式子来说,从训练集中求解 P ( Y = 1 ) P(Y=1) P(Y=1)很容易,但 P ( X ) P(\textbf X) P(X)和 P ( x i ∣ Y = 1 ) P(x_i\mid Y=1) P(xi​∣Y=1)这一部分就没有那么容易了。通过全概率公式求解分母,两个特征就求解了四项概率。随着特征数目的逐渐增多,分母上的计算会成指数级增长,而分子中的 P ( x i ∣ Y = 1 ) P(x_i\mid Y=1) P(xi​∣Y=1)也越来越难计算。
对于同一个样本来说,在二分类状况下有:
P ( Y = 1 ∣ X ) = P ( Y = 1 ) ∗ ∏ i = 1 n P ( x i ∣ Y = 1 ) P ( X ) P(Y=1\mid \textbf X) = \frac{P(Y=1)*\prod_{i=1}^nP(x_i\mid Y=1)}{P(\textbf X)} P(Y=1∣X)=P(X)P(Y=1)∗∏i=1n​P(xi​∣Y=1)​
P ( Y = 0 ∣ X ) = P ( Y = 0 ) ∗ ∏ i = 1 n P ( x i ∣ Y = 0 ) P ( X ) P(Y=0\mid \textbf X) = \frac{P(Y=0)*\prod_{i=1}^nP(x_i\mid Y=0)}{P(\textbf X)} P(Y=0∣X)=P(X)P(Y=0)∗∏i=1n​P(xi​∣Y=0)​
并且: P ( Y = 1 ∣ X ) + P ( Y = 0 ∣ X ) = 1 P(Y=1\mid \textbf X)+P(Y=0\mid \textbf X)=1 P(Y=1∣X)+P(Y=0∣X)=1
在分类的时候,选择 P ( Y = 1 ∣ X ) P(Y=1\mid \textbf X) P(Y=1∣X)和 P ( Y = 0 ∣ X ) P(Y=0\mid \textbf X) P(Y=0∣X)中较大的一个所对应的 Y Y Y的取值,作为这个样本的分类。在比较两个类别的时候,两个概率计算的分母是一致的,因此可以不用计算分母,只考虑分子的大小。当分别计算出分子的大小之后,就可以通过让两个分子相加,来获得分母的值,以此来避免计算一个样本上所有特征下的概率 P ( X ) P(\textbf X) P(X)。这个过程被称为“最大后验估计(MAP)”。在最大后验估计中,只需要求解分子,主要是求解一个样本下每个特征取值下的概率 P ( x i ∣ Y = y i ) P(x_i\mid Y = y_i) P(xi​∣Y=yi​),再求连乘便能够获得相应的概率。
在现实中,要求解分子也会有各种各样的问题,如测试集中出现的某种概率组合,是训练集中从未出现的状况,这种时候就会出现某一个概率为0的情况,贝叶斯概率的分子就会为0。还有现实中的大多数标签还是连续型变量,要处理连续型变量的概率,就不是单纯的数样本个数的占比的问题了。接下来看看如何对连续型特征求解概率

1.2.3 汉堡称重:连续型变量的概率估计

要处理连续型变量,可以有两种方法。第一种是把连续型变量分成 j j j个箱,把连续型强行变成分类型变量,分箱后,将每个箱中的均值 x i ˉ \bar{x_i} xi​ˉ​当作一个特征 X i X_i Xi​上的取值,然后计算箱 j j j中 Y = 1 Y=1 Y=1所占的比例,就是 P ( x i ∣ Y = 1 ) P(x_i\mid Y=1) P(xi​∣Y=1)。这个过程的主要问题是,箱子不能太大也不能太小,如果箱子太大,就失去了分箱的基本意义,如果箱子太小,可能每个箱子里就没有足够的样本来帮助计算 P ( x i ∣ Y ) P(x_i\mid Y) P(xi​∣Y),因此必须要适当地衡量分箱的效果。
但其实没有必要这样做,可以直接通过概率论来计算连续型变量的概率分布。在分类型变量的情况中,如掷骰子的情况,有且仅有六种可能的结果1-6,并且每种结果的可能性为1/6。此时每个基本的随机事件发生的概率都是相等的,所以可以使用 1 N \frac{1}{N} N1​来表示有 N N N个基本随机事件可以发生的情况。
基于此,来思考一个简单的问题:汉堡王向客户承诺说他们的汉堡至少是100g一个,但如果去汉堡王买个汉堡,可以预料到他肯定不是标准的100g。设汉堡重量为特征 X i X_i Xi​,100g就是取值 x i x_i xi​,那买到一个汉堡是100g的概率 P ( 100 g ∣ Y ) P(100g\mid Y) P(100g∣Y)是多少?如果买n个汉堡,很可能n个汉堡都不一样重,只要称重足够精确,100.000001g和100.000002g就可以是不一致的。这种情况下可以买无限个汉堡,可能得到无限个重量,可以有无限个基本随机事件发生,所以有: P ( 100 g ∣ Y ) = lim ⁡ N → ∞ 1 N = 0 P(100g\mid Y)=\lim_{N\to \infty}\frac{1}{N}=0 P(100g∣Y)=N→∞lim​N1​=0即买到的汉堡刚好是100g的概率为0。当一个特征下有无数种可能发生的事件时,这个特征的取值就是连续型的,如现在的特征“汉堡的重量”。从上面的例子可以看出,当特征为连续型时,随机取到某一个事件发生的概率就为0
换一个问题,随机买一个汉堡,汉堡的重量在98g-102g之间的概率是多少?也就是说,现在求解概率 P ( 98 g < x < 102 g ) P(98g<x<102g) P(98g<x<102g)。那现在随机购买100个汉堡,称重后记下所有重量在98g-102g之间的汉堡个数,假设为 m m m,则有:
P ( 98 g < x < 102 g ) = m 100 P(98g<x<102g)=\frac{m}{100} P(98g<x<102g)=100m​
如果基于100个汉堡绘制直方图,并规定每4g为一个区间,横坐标为汉堡的重量的分布,纵坐标为这个区间上汉堡的个数。

可以看到最左边的图, m m m就是中间的浅绿色区间中所对应的纵坐标轴。则对于概率,可以变换为:
P ( 98 g < x < 102 g ) = m ∗ 4 100 ∗ 4 = 浅 绿 色 区 间 的 面 积 直 方 图 中 所 有 区 间 的 面 积 P(98g<x<102g)=\frac{m*4}{100*4}=\frac{浅绿色区间的面积}{直方图中所有区间的面积} P(98g<x<102g)=100∗4m∗4​=直方图中所有区间的面积浅绿色区间的面积​
如果购买一万个汉堡并绘制直方图(如中间的图),将直方图上的区间缩小, P ( 98 g < x < 102 g ) P(98g<x<102g) P(98g<x<102g)依然是所有浅绿色区域的面积除以所有柱状图的面积,可以看见现在直方图变得更加平滑了。假设购买100w个,或者无限个汉堡,则可以想象直方图最终会变成仿佛一条曲线,而汉堡重量的概率 P ( 98 g < x < 102 g ) P(98g<x<102g) P(98g<x<102g)依然是所有浅绿色柱子的面积除以曲线下所有柱状图的总面积。当购买无数个汉堡的时候形成的这条曲线就叫做概率密度曲线(probability density function,PDF)
一条曲线下的面积,就是这条曲线所代表的函数的积分。如果定义曲线可以用函数 f ( x ) f(x) f(x)来表示的话,整条曲线下的面积就是: ∫ − ∞ + ∞ f ( x ) d x \int_{-\infty}^{+\infty}f(x)dx ∫−∞+∞​f(x)dx。其中 d x dx dx是 f ( x ) f(x) f(x)在 x x x上的微分。在某些特定的 f ( x ) f(x) f(x)下,可以证明上述积分等于1,这说明一个连续型特征 X X X的取值 x x x取到某个区间 [ x i , x i + ϵ ] [x_i,x_i+\epsilon] [xi​,xi​+ϵ]之内的概率就为这个区间上概率密度曲线下的面积,所以特征 X i X_i Xi​在区间 [ x i , x i + ϵ ] [x_i,x_i+\epsilon] [xi​,xi​+ϵ]中取值的概率可以表示为:
P ( x i < x < x i + ϵ ) = ∫ x i x i + ϵ f ( x ) d x ≈ f ( x i ) ∗ ϵ P(x_i<x<x_i+\epsilon)=\int_{x_i}^{x_i+\epsilon}f(x)dx\approx f(x_i)*\epsilon P(xi​<x<xi​+ϵ)=∫xi​xi​+ϵ​f(x)dx≈f(xi​)∗ϵ
非常幸运的是,在后验概率的计算过程中,可以将常量 ϵ \epsilon ϵ抵消掉,然后可以利用 f ( x i ) f(x_i) f(xi​)的某种变化来估计 P ( x i ∣ Y ) P(x_i\mid Y) P(xi​∣Y)。现在,求解连续型变量下某个点取值的概率问题,转化成了求解一个函数 f ( x ) f(x) f(x)在点 x i x_i xi​上的取值的问题。那接下来只要找到 f ( x ) f(x) f(x),就可以求解出不同的条件概率。
在现实中,往往假设 f ( x ) f(x) f(x)是满足某种统计学中的分布,最常见的就是高斯分布(正态分布,如购买汉堡的例子),常用的还有伯努利分布、多项式分布。这些分布对应着不同的贝叶斯算法,其实它们的本质都是相同的,只不过它们计算之中的 f ( x ) f(x) f(x)不同。每个 f ( x ) f(x) f(x)都对应着一系列需要估计的参数,因此在贝叶斯中,fit过程其实是在估计对应分布的参数,predict过程是在该参数下的分布中去进行概率预测。

1.3 sklearn中的朴素贝叶斯

sklearn基于这些分布以及这些分布上的概率估计的改进,提供了四个朴素贝叶斯的分类器。

含义
naive_bayes.BernoulliNB 伯努利分布下的朴素贝叶斯
naive_bayes.GaussianNB 高斯分布下的朴素贝叶斯
naive_bayes.MultinomialNB 多项式分布下的朴素贝叶斯
naive_bayes.ComplementNB 补集朴素贝叶斯
naive_bayes.BayesianRidge 贝叶斯岭回归,在参数估计过程中使用贝叶斯回归技术来包括正则化系数

虽然朴素贝叶斯使用了过于简化的假设,这个分类器在许多实际情况中都运行良好,著名的是文档分类和垃圾邮件过滤。而且由于贝叶斯是从概率角度进行估计,它所需要的样本量比较少,极端情况下甚至可以使用1%的数据作为训练集,依然可以得到很好的拟合效果。当然,如果样本量少于特征数目,贝叶斯效果就会被削弱(这对于任何算法都是一样的)。
与SVM和随机森林相比,朴素贝叶斯运行速度更快,因为求解 P ( X i ∣ Y ) P(X_i\mid Y) P(Xi​∣Y)本质是在每个特征上单独对概率进行计算,然后再求乘积,所以每个特征上的计算可以是独立并且并行的,因此贝叶斯的计算速度比较快。不过相对的,贝叶斯的运行效果不是那么好,所以贝叶斯的接口调用的predict_proba其实也不是总指向真正的分类结果,这一点需要注意。

2 不同分布下的贝叶斯

2.1 高斯朴素贝叶斯GaussianNB

2.1.1认识高斯朴素贝叶斯

sklearn.naive_bayes.GaussianNB(priors = None, var_smoothing = 1e-09)
高斯朴素贝叶斯,通过假设 P ( x i ∣ Y ) P(x_i\mid Y) P(xi​∣Y)是服从高斯分布(也就是正态分布),来估计每个特征下每个类别上的条件概率。对于每个特征下的取值,高斯朴素贝叶斯有如下公式:
P ( x i ∣ Y ) = f ( x i ; μ y , σ y ) ∗ ϵ = 1 2 π σ y 2 e x p ( − ( x i − μ y ) 2 2 σ y 2 ) P(x_i\mid Y)=f(x_i;\mu_y,\sigma_y)*\epsilon=\frac{1}{\sqrt{2\pi \sigma_y^2}}exp(-\frac{(x_i-\mu_y)^2}{2\sigma_y^2}) P(xi​∣Y)=f(xi​;μy​,σy​)∗ϵ=2πσy2​ ​1​exp(−2σy2​(xi​−μy​)2​)
对于任意一个 Y Y Y的取值,贝叶斯都以求解最大化的 P ( x i ∣ Y ) P(x_i\mid Y) P(xi​∣Y)为目标,这样才能比较在不同标签下样本究竟更靠近哪一个取值。以最大化 P ( x i ∣ Y ) P(x_i\mid Y) P(xi​∣Y)为目标,高斯朴素贝叶斯会求解公式中的参数 σ y \sigma_y σy​和 μ y \mu_y μy​。求解出参数后,代入一个 x i x_i xi​的值,就能够得到一个 P ( x i ∣ Y ) P(x_i\mid Y) P(xi​∣Y)的概率取值。
这个类包含两个参数:

参数 含义
prior 可输入任何类数组结构,形状为(n_classes,)。表示类的先验概率,如果指定,则不根据数据调整先验,如果不指定,则自行根据数据计算先验概率 P ( Y ) P(Y) P(Y)。
var_smoothing 浮点数,可不填(默认值=1e-9)。在估计方差时,为了追求估计的稳定性,将所有特征的方差中最大的方差以某个比例添加到估计的方差中。这个比例,由var_smoothing参数控制。

在实例化的时候,不需要对高斯朴素贝叶斯输入任何的参数,调用的接口也全部都是sklearn中比较标准的一些搭配,可以说是一个非常轻量级的类,操作非常容易。但是过于简单也意味着贝叶斯没有太多的参数可以调整,因此贝叶斯算法的成长空间并不是太大,如果贝叶斯算法的效果不是太理想,一般都会考虑换模型。
无论如何,先进行一次预测试试看:
首先,展示所使用的设备以及各个库的版本。在这里使用watermark这个便利的模块,这是一个能够一行代码查看设备和库的版本的模块。如果没有watermark,可能需要在cmd中运行pip来安装,也可以直接使用魔法命令%%cmd作为一个cell的开头来帮助我们在jupyter lab中安装watermark。

#%%cmd
#pip install watermark
#魔法命令必须是一个cell的第一部分内容
#注意load_ext这个命令只能够执行一次,再执行就会报错,要求用reload命令
%load_ext watermark
%watermark -a "86188" -d -v -m -p numpy,pandas,matplotlib,scipy,sklearn

#导入需要的库和数据
import numpy as np
import matplotlib.pyplot as plt
from sklearn.naive_bayes import GaussianNB
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_splitdigits = load_digits()
x,y = digits.data, digits.target
xtrain,xtest,ytrain,ytest = train_test_split(x,y,test_size = 0.3,random_state = 420)
xtrain.shape#64个特征
#结果:(1257, 64)
xtest.shape
#结果:(540, 64)
np.unique(ytrain)#多分类问题,类别是10个
#结果:array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])#建模,探索建模结果
gnb = GaussianNB().fit(xtrain,ytrain)
#查看分数
acc_score = gnb.score(xtest,ytest)
acc_score
#结果:0.8592592592592593
#查看预测结果
y_pred = gnb.predict(xtest)
#查看预测的概率结果
prob = gnb.predict_proba(xtest)
prob.shape#每一列对应一个标签类别下的概率
#结果:(540, 10)
prob[1:].sum()#每一行的和都是1
#结果:1.000000000000003
prob.sum(axis=1).shape
#结果:(540,)#使用混淆矩阵来查看贝叶斯的分类结果
from sklearn.metrics import confusion_matrix as CM
CM(ytest,y_pred)
'''
结果:
array([[47,  0,  0,  0,  0,  0,  0,  1,  0,  0],[ 0, 46,  2,  0,  0,  0,  0,  3,  6,  2],[ 0,  2, 35,  0,  0,  0,  1,  0, 16,  0],[ 0,  0,  1, 40,  0,  1,  0,  3,  4,  0],[ 0,  0,  1,  0, 39,  0,  1,  4,  0,  0],[ 0,  0,  0,  2,  0, 58,  1,  1,  1,  0],[ 0,  0,  1,  0,  0,  1, 49,  0,  0,  0],[ 0,  0,  0,  0,  0,  0,  0, 54,  0,  0],[ 0,  3,  0,  1,  0,  0,  0,  2, 55,  0],[ 1,  1,  0,  1,  2,  0,  0,  3,  7, 41]], dtype=int64)
'''
#注意,ROC曲线是不能用于多分类的。多分类状况下最佳的模型评估指标是混淆矩阵和整体的准确度

2.2 探索贝叶斯:高斯朴素贝叶斯擅长的数据集

那么高斯朴素贝叶斯擅长什么样的数据集呢?还是使用常用的三种数据分布:月亮型、环形数据以及二分型数据。核心就是构建分类器然后绘制决策边界

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_moons,make_circles,make_classification
from sklearn.naive_bayes import GaussianNB,MultinomialNB,BernoulliNB,ComplementNBh = 0.02
names = ["Multinomial","Gaussian","Bernoulli","Complement"]
classifiers = [MultinomialNB(),GaussianNB(),BernoulliNB(),ComplementNB()]
x,y = make_classification(n_features = 2, n_redundant = 0, n_informative = 2, random_state = 1, n_clusters_per_class = 1)
rng = np.random.RandomState(2)
x += 2 * rng.uniform(size = x.shape)
linearly_separable = (x,y)datasets = [make_moons(noise = 0.3,random_state = 0),make_circles(noise = 0.2,factor = 0.5, random_state = 1),linearly_separable]
i = 1for ds_index, ds in enumerate(datasets):figure = plt.figure(figsize = (6,9))x,y = dsx = StandardScaler().fit_transform(x)x_train,x_test,y_train,y_test = train_test_split(x,y,test_size = 0.4, random_state = 42)x1_min,x1_max = x[:,0].min()-0.5,x[:,0].max()+0.5x2_min,x2_max = x[:,1].min()-0.5,x[:,1].max()+0.5array1,array2 = np.meshgrid(np.arange(x1_min,x1_max,0.2),np.arange(x2_min,x2_max,0.2))cm = plt.cm.RdBucm_bright = ListedColormap(["#FF0000","#0000FF"])ax = plt.subplot(len(datasets),2,i)if ds_index == 0:ax.set_title("Input data")ax.scatter(x_train[:,0],x_train[:,1],c = y_train, cmap = cm_bright, edgecolors = "k")ax.scatter(x_test[:,0],x_test[:,1],c = y_test, cmap = cm_bright, alpha = 0.6, edgecolors = "k")ax.set_xlim(array1.min(),array1.max())ax.set_ylim(array2.min(),array2.max())ax.set_xticks(())ax.set_yticks(())i += 1ax = plt.subplot(len(datasets),2,i)clf = GaussianNB().fit(x_train,y_train)score = clf.score(x_test,y_test)z = clf.predict_proba(np.c_[array1.ravel(),array2.ravel()])[:,1]z = z.reshape(array1.shape)ax.contourf(array1,array2,z,cmap = cm, alpha = 0.8)ax.scatter(x_train[:,0],x_train[:,1],c = y_train, cmap = cm_bright, edgecolors = "k")ax.scatter(x_test[:,0],x_test[:,1],c = y_test, cmap = cm_bright, edgecolors = "k", alpha = 0.6)ax.set_xlim(array1.min(),array1.max())ax.set_ylim(array2.min(),array2.max())ax.set_xticks(())ax.set_yticks(())if ds_index == 0:ax.set_title("Gaussian Bayes")ax.text(array1.max()-0.3,array2.min()+0.3,('{:.1f}%'.format(score*100)),size = 15, horizontalalignment = "right")i += 1plt.tight_layout()plt.show()


从图上来看,高斯朴素贝叶斯属于比较特殊的一类分类器,其分类效果在二分数据和月亮型数据上表现优秀,但是环形数据不太擅长。许多线性模型,如逻辑回归、线性SVM等等,在线性数据集上回绘制直线决策边界,因此难以对月亮型和环形数据进行区分,但高斯朴素贝叶斯的决策边界是曲线,可以是环形也可以是弧线,所以尽管贝叶斯本身更加擅长线性可分的二分数据,但朴素贝叶斯在环形数据和月亮型数据上也可以有远远胜过其他线性模型的表现。

2.3 探索贝叶斯:高斯朴素贝叶斯的拟合效果与运算速度

决策树是天生过拟合的模型,而支持向量机是不调参的情况下就非常接近极限的模型。希望通过绘制高斯朴素贝叶斯的学习曲线与分类树、随机森林以及支持向量机的学习曲线对比,来看看高斯朴素贝叶斯算法在拟合上的性质。绘制学习曲线可以以算法类的某个参数的取值为横坐标,也可以使用sklearn中自带的绘制学习曲线的类learning_curve,在这个类中执行交叉验证并从中获得不同样本量下的训练和测试的准确度。

#首先导入需要的模块和库
import numpy as np
import matplotlib.pyplot as plt
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.tree import DecisionTreeClassifier as DTC
from sklearn.linear_model import LogisticRegression as LR
from sklearn.datasets import load_digits
from sklearn.model_selection import learning_curve#画学习曲线的类
from sklearn.model_selection import ShuffleSplit#设定交叉验证模式的类
from time import time
import datetime#输入分类器,数据,画图所需要的一系列参数,交叉验证的模式,以及其他可能的参数
#一次性画出所有的学习曲线
#定义绘制学习曲线的函数
def plot_learning_curve(estimator,title,x,y,ax #选择子图,ylim = None #设置纵坐标的取值范围,cv = None #交叉验证,n_jobs = None #设定所要使用的线程):train_sizes,train_scores,test_scores = learning_curve(estimator#分类器,x,y#特征矩阵和标签,cv = cv#表示交叉验证模式,n_jobs = n_jobs#每次运行的时候可以允许算法使用多少运算资源)#train_sizes是每次分训练集和测试集建模之后,训练集上的样本数量#train_scores是训练集上的分数#test_scores是测试集上的分数ax.set_title(title)#设置标题if ylim is not None:ax.set_ylim(*ylim)ax.set_xlabel("Training examples")ax.set_ylabel("Score")ax.grid()#显示网格作为背景 ,不是必须ax.plot(train_sizes,np.mean(train_scores,axis = 1),"o-",color = "r",label = "Training score")ax.plot(train_sizes,np.mean(test_scores,axis = 1),"o-",color = "g",label = "Test score")ax.legend(loc = "best")return axdigits = load_digits()
x,y = digits.data,digits.target
x.shape
#结果:(1797, 64)
x#是一个稀疏矩阵
'''
结果:
array([[ 0.,  0.,  5., ...,  0.,  0.,  0.],[ 0.,  0.,  0., ..., 10.,  0.,  0.],[ 0.,  0.,  0., ..., 16.,  9.,  0.],...,[ 0.,  0.,  1., ...,  6.,  0.,  0.],[ 0.,  0.,  2., ..., 12.,  0.,  0.],[ 0.,  0., 10., ..., 12.,  1.,  0.]])
'''title = ["Naive Bayes","DecisionTree","SVM,RBF kernel","RandomForest","Logistic"]
model = [GaussianNB(),DTC(),SVC(gamma = 0.001),RFC(n_estimators = 50),LR(C=0.1,solver = "lbfgs")]
cv = ShuffleSplit(n_splits = 50 #把数据分为多少份,test_size = 0.2 #20%*50份的数据会被作为测试集,random_state = 0 #分交叉验证份数的时候进行的随机抽样的模式)
#设定好用来绘制子图所在的画布,就开始在画布上绘图
fig,axes = plt.subplots(1,5,figsize = (30,6))
for ind,title_,estimator in zip(range(len(title)),title,model):times = time()plot_learning_curve(estimator,title_,x,y,ax = axes[ind],ylim = [0.7,1.05],n_jobs = 4,cv = cv)print("{}:{}".format(title_,datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f")))
plt.show()

2.2 概率类模型的评估指标

混淆矩阵和精确性可以帮助了解贝叶斯的分类结果,然而,选择贝叶斯进行分类,大多数时候不单单追求效果,而是希望看到预测的相关概率。这种概率给出预测的可信度,所以对于概率类模型,希望有其他的模型评估指标来帮助判断,模型在“概率预测”这项工作上,完成得如何。接下来,介绍概率类模型独有的评估指标

2.2.1 布里尔分数Brier Score

概率预测的准确程度被称为“校准程度”,是衡量算法预测出的概率和真实结果的差异的一种方式。一种比较常用的指标叫做布里尔分数,它被计算为是概率预测相对于测试样本的均方误差,表示为: B r i e r S c o r e = 1 N ∑ i = 1 n ( p i − o i ) 2 Brier Score = \frac{1}{N}\sum_{i=1}^n(p_i-o_i)^2 BrierScore=N1​i=1∑n​(pi​−oi​)2其中 N N N是样本数量, p i p_i pi​为朴素贝叶斯预测出的概率, o i o_i oi​是样本所对应的真实结果,只能取到0或者1,如果事件发生则为1,如果不发生则为0。这个指标衡量了概率距离真实标签结果的差异,其实看起来非常像是均方误差。**布里尔分数的范围是从0到1,分数越高则预测结果越差劲,校准程度越差,因此布里尔分数越接近0越好。**由于它的本质也是在衡量一种损失,所以在sklearn中,布里尔得分被命名为brier_score_loss。可以从模块metrics中导入这个分数来衡量模型评估结果:

from sklearn.metrics import brier_score_loss
#注意,弟弟一个参数是真实标签,第二个参数是预测出的概率值
#在二分类情况下,接口predict_proba会返回两列,但SVC的接口decision_function却只会返回一列
#要随时注意,使用了怎样的概率分类器,以辨别查找置信度的接口,以及这些接口的结构
brier_score_loss(ytest,prob[:,1],pos_label = 1)
#pos_label与prob中的索引一直,就可以查看这个类别下的布里尔分数是多少

这个代码会报错:ValueError: Only binary classification is supported. The type of the target is multiclass.这是由于新版sklearn的brier_score_loss不支持多分类了。如果想要将布里尔分数运用在多分类数据上,需要先对测试集的标签进行哑变量变换,再计算每个类别的布里尔分数,就可以成功计算出来。

import pandas as pd
ytest_= ytest.copy()
ytest_ = pd.get_dummies(ytest_)
for i in range(10):bs = brier_score_loss(ytest_[i],prob[:,i])print(bs)
'''
结果:
0.003680512744365077
0.032619662406118764
0.04073796355303327
0.024227451061575995
0.013717124391784064
0.012770720966518715
0.008906952589951798
0.028006698663252037
0.0680707157691044
0.031524759298859326
'''

布里尔分数可以用于任何可以使用predict_proba接口调用概率的模型,接下来探索手写数字数据集上,逻辑回归、SVC和高斯朴素贝叶斯的效果(识别数字1 ):

from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression as LR
logi = LR(C=1.0,solver = "lbfgs",max_iter = 3000,multi_class="auto").fit(xtrain,ytrain)
svc = SVC(kernel = "linear",gamma = 1).fit(xtrain,ytrain)
brier_score_loss(ytest_[1],logi.predict_proba(xtest)[:,1],pos_label = 1)
#结果:0.011449524293716193
#由于SVC的置信度并不是概率,为了可比性,需要将SVC的置信度“距离”归一化,压缩到[0,1]之间
svc_prob = (svc.decision_function(xtest)-svc.decision_function(xtest).min())/(svc.decision_function(xtest).max()-svc.decision_function(xtest).min())
brier_score_loss(ytest_[1],svc_prob[:,1],pos_label = 1)
#结果:0.24286480465579566

如果将没和分类器每个标签类别下的布里尔分数可视化:

import pandas as pd
name = ["Bayes","Logistic","SVC"]
color = ["red","black","orange"]
df = pd.DataFrame(index = range(10),columns = name)
for i in range(10):df.loc[i,name[0]]=brier_score_loss(ytest_[i],prob[:,i])df.loc[i,name[1]]=brier_score_loss(ytest_[i],logi.predict_proba(xtest)[:,i])df.loc[i,name[2]]=brier_score_loss(ytest_[i],svc_prob[:,i])
for i in range(df.shape[1]):plt.plot(range(10),df.iloc[:,i],c = color[i],label = name[i])
plt.legend()
plt.show()
df



可以观察到,逻辑回归的布里尔分数有着压倒性优势,SVC的效果明显弱于贝叶斯和逻辑回归(因为SVC是强行利用sigmoid函数来压缩概率,因此SVC产出的概率结果并不是那么可靠)。贝叶斯位于逻辑回归和SVC之间,效果也不错,但比起逻辑回归,还是不够精确和稳定。

2.2.2 对数似然函数Log Loss

另一种常用的概率损失衡量是对数损失(log_loss),又叫做对数似然、逻辑损失或者交叉熵损失,它的本质其实是多元逻辑回归以及一些拓展算法,如神经网络中使用的损失函数。它被定义为,对于一个给定的概率分类器,求解预测概率为条件的情况下,真实概率发生的可能性的负对数。由于是损失,因此对数似然函数的取值越小,则证明概率估计越准确,模型越理想。
对于一个样本,如果样本的真实标签 y t r u e y_{true} ytrue​在{0,1}中取值,并且这个样本在类别1下的概率估计为 y p r e d y_{pred} ypred​,则这个样本所对应的对数损失是:
− l o g P ( y t r u e ∣ y p r e d ) = − ( y t r u e ∗ l o g ( y p r e d ) + ( 1 − y t r u e ) ∗ l o g ( 1 − y p r e d ) ) -logP(y_{true}\mid y_{pred})=-(y_{true}*log(y_{pred})+(1-y_{true})*log(1-y_{pred})) −logP(ytrue​∣ypred​)=−(ytrue​∗log(ypred​)+(1−ytrue​)∗log(1−ypred​))
和逻辑回归的损失函数一模一样: J ( θ ) = − ∑ i = 1 m ( y i ∗ l o g ( y θ ( x i ) ) + ( 1 − y i ) ∗ l o g ( 1 − y θ ( x i ) ) ) J(\theta)=-\sum_{i=1}^m(y_i*log(y_{\theta}(x_i))+(1-y_i)*log(1-y_{\theta}(x_i))) J(θ)=−i=1∑m​(yi​∗log(yθ​(xi​))+(1−yi​)∗log(1−yθ​(xi​)))只不过在逻辑回归的损失函数中,真实标签是由 y i y_i yi​表示,预测值(概率估计)是由 y θ ( x i ) y_{\theta}(x_i) yθ​(xi​)来表示,仅仅是表示方式的不同。注意,这里的 l o g log log表示以 e e e为底的自然对数。
在sklearn中,可以从metrics模块中导入对数似然函数:

from sklearn.metrics import log_loss
log_loss(ytest,prob)
#结果:2.4725653911460683
log_loss(ytest,logi.predict_proba(xtest))
#结果:0.1275282688659348
log_loss(ytest,svc_prob)
#结果:1.6074987533411256

第一个参数是真实标签,第二个参数是预测的概率。如果使用shift tab来查看log_loss的参数,会发现第二个参数写着y_pred,这会让人误解为预测标签。由于log_loss是专门用于产出概率的算法的,因此它假设预测出的y就是以概率形式呈现,但在sklearn中,y_pred往往是已经根据概率归类后的类别{0,1,2},真正的概率必须要以接口predict_proba来调用,千万避免混淆。
注意到,用log_loss得出的结论和使用布里尔分数得出的结论不一致:当使用布里尔分数作为评判标准的时候,SVC的估计效果是最差的,逻辑回归和贝叶斯的结果想接近。而使用对数似然的时候,虽然依然是逻辑回归最强大,但贝叶斯却没有SVC的效果好。
因为逻辑回归和SVC都是以最优化为目的来求解模型,然后进行分类的算法,而朴素贝叶斯中,却没有最优化的过程。对数似然函数直接指向模型最优化的方向,甚至就是逻辑回归的损失函数本身,因此在逻辑回归和SVC上表现得更好。
**那么什么时候使用对数似然,什么时候使用布里尔分数?**在现实应用中,对数似然函数是概率类模型评估的黄金指标,往往是评估概率类模型的优先选择。但它也有一些缺点,首先它没有界,不像布里尔分数有上限,可以作为模型效果的参考。其次,它的解释性不如布里尔分数,很难与非技术人员去交流对数似然存在的可靠性和必要性。第三,它在以最优化为目标的模型上明显表现更好。而且,还有一些数学上的问题,如不能接受为0或1的概率,否则对数似然就会取到极限值(考虑以 e e e为底的自然对数在取到0或者1的时候的情况)。因此通常来说,有以下使用规则:

需求 优先使用对数似然 优先使用布里尔分数
衡量模型 要对比多个模型,或者衡量模型的不同变化 衡量单一模型的表现
可解释性 机器学习和深度学习之间的行家交流,学术论文 商业报告,老板开会,业务模型的衡量
最优化指向 逻辑回归,SVC 朴素贝叶斯
数学问题 概率只能无限接近于0或1,无法取到0或1 概率可以取到0或1,比如树、随机森林

回到贝叶斯来看,如果贝叶斯的模型效果不如其他模型,而又不想更换模型,那怎么办呢?如果以精确度为指标来调整参数,贝叶斯估计是无法拯救了——不同于SVC和逻辑回归,贝叶斯的原理简单,根本没有什么可用的参数。但是产出概率的算法有自己的调节方式,就是调节概率的校准程度。校准程度越高,模型对概率的预测越准确,算法在做判断时就越有自信,模型就会更稳定。如果追求模型在概率预测上必须尽量贴近真实概率,那就可以使用可靠性曲线来调节概率的校准程度。

2.2.3 可靠性曲线Reliability Curve

可靠性曲线(reliability curve),又叫做概率校准曲线(probability calibration curve)、可靠性图(reliability diagrams),这是一条以预测概率为横坐标,真实概率为纵坐标的曲线。希望预测概率和真实概率越接近越好,最好两者相等,因此一个模型/算法的概率校准曲线越靠近对角线越好。校准曲线因此也是模型评估指标之一。和布里尔分数相似,概率校准曲线是对于标签的某一类来说的,因此一类标签就会有一条曲线,或者可以使用一个多类标签下的平均来表示一整个模型的概率校准曲线。但通常来说,曲线用于二分类的情况最多
根据这个思路,来绘制一条曲线看看:

#导入需要的库和模块
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification as mc
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression as LR
from sklearn.metrics import brier_score_loss
from sklearn.model_selection import train_test_split
#创建数据集
x,y = mc(n_samples = 100000, n_features = 20#总共20个特征,n_classes = 2#标签为2分类,n_informative = 2#其中两个代表较多信息,n_redundant = 10#10个都是冗余特征,random_state = 42)
#样本量足够大,因此使用1%的样本作为训练集
xtrain,xtest,ytrain,ytest = train_test_split(x,y,test_size = 0.99,random_state = 42)
xtrain.shape
#结果:(1000, 20)
xtest.shape
#结果:(99000, 20)
np.unique(ytrain)
#结果:array([0, 1])#建立模型,绘制图像
gnb = GaussianNB()
gnb.fit(xtrain,ytrain)
y_pred = gnb.predict(xtest)
prob_pos = gnb.predict_proba(xtest)[:,1]#预测概率——横坐标
#ytest——真实标签——纵坐标
#在横纵坐标上,概率是有顺序的(由小到大),为了让图形规整一些,要先对预测概率和真实标签按照预测概率进行一个排序,这一点通过DataFrame来实现
df = pd.DataFrame({"ytrue":ytest[:500],"probability":prob_pos[:500]})
#利用字典来创建DataFrame({"列的名称":[列的值]})
#df
#在横纵坐标上,概率是有顺序的(由小到大),为了让图形规整一些,要先对预测概率和真实标签按照预测概率进行一个排序,这一点通过DataFrame来实现
df = pd.DataFrame({"ytrue":ytest[:500],"probability":prob_pos[:500]})
#df
df = df.sort_values(by = "probability")
df.index = range(df.shape[0])
df

from sklearn.metrics import accuracy_score
clf_score = accuracy_score(ytest,y_pred)#准确率
#紧接着就可以画图
fig = plt.figure()#画布
ax1 = plt.subplot()#建立一个子图
ax1.plot([0,1],[0,1],"k:",label = "Perfectly calibrated")#得做一条对角线来对比
ax1.plot(df["probability"],df["ytrue"],"s-",label = "%s (%1.3f)" % ("Bayes",clf_score))
ax1.set_ylabel("True probability for class 1")
ax1.set_xlabel("Mean predicted probability")
ax1.set_ylim([-0.05,1.05])
ax1.legend()
plt.show()


由于按照预测概率的顺序进行排序,而预测概率从0开始到1的过程中,真实取值不断在0和1之间变化,而绘制的是折线图,因此无数个纵坐标分布在0和1之间被连接起来。下面换成散点图来看看:

fig = plt.figure()
ax1 = plt.subplot()
ax1.plot([0,1],[0,1],"k:",label = "Perfectly calibrated")
ax1.scatter(df["probability"],df["ytrue"],s = 10)
ax1.set_ylabel("True label")
ax1.set_xlabel("predicted probability")
ax1.set_ylim([-0.05,1.05])
plt.show()


可以看到,由于真实标签是0和1,所以所有的点都在y=1和y=0这两条直线上分布,这完全不是希望看到的图像。根据可靠性曲线的横纵坐标:横坐标是预测概率,而纵坐标是真实值,希望预测概率很靠近真实值,那么真实取值必然也需要是一个概率才可以,如果使用真实标签,那么绘制出来的图像是完全没有意义的。但是去哪里寻找真实值的概率呢?这是不可能找到的——如果能够找到真实的概率,那何必还用算法来估计概率呢?直接获取真实的概率不就好了吗?所以真实概率在现实中是不可获得的。但是,可以获得类概率的指标来帮助进行校准。一个简单的做法是,将数据进行分箱,然后规定每个箱子中真实的少数类所占的比例为这个箱上的真实概率trueproba,这个箱子中预测概率的均值为这个箱子的预测概率predproba,然后以trueproba为纵坐标,predproba为横坐标,来绘制可靠性曲线。
举个例子,来看下面这张表,这是一组数据不分箱时表现出来的图像:

再来看看分箱之后的图像

可见,分箱之后样本点的特征被聚合到了一起,曲线明显变得单调且平滑。这种分箱操作本质相当于是一种平滑。在sklearn中,这样的做法可以通过绘制可靠性曲线的类calibration_curve来实现。和ROC曲线类似,在sklearn中,使用类calibration_curve来获取横纵坐标,然后使用matplotlib来绘制图像。该类有如下参数:

参数 含义
y_true 真实标签
y_prob 预测返回的,正类别下的概率值或置信度
normalize 布尔值,默认为False。是否将y_prob中输入的内容归一化到[0,1]之间,比如当y_prob并不是真正的概率的时候可以使用。如果这是为True,则会将y_prob中最小的值归一化为0,最大值归一化为1。
n_bins 整数值,表示分箱的个数。如果箱数很大,则需要更多的数据。
返回 含义
trueproba 可靠性曲线的纵坐标,结构为(n_bins,) ,是每个箱子中少数类(Y=1)的占比
predproba 可靠性曲线的横坐标,结构为(n_bins,),是每个箱子中概率的均值
#使用可靠性曲线的类在贝叶斯上绘制一条校准曲线
from sklearn.calibration import calibration_curve
#从类calibration_curve中获取横坐标和纵坐标
trueproba,predproba = calibration_curve(ytest,prob_pos,n_bins=10#输入希望分箱的个数)
fig = plt.figure()
ax1 = plt.subplot()
ax1.plot([0,1],[0,1],"k:",label = "Perfectly calibrated")
ax1.plot(predproba,trueproba,"s-",label = "%s (%1.3f)"%("Bayes",clf_score))
ax1.set_ylabel("True probability for class 1")
ax1.set_xlabel("Mean predicted probability")
ax1.set_ylim([-0.05,1.05])
ax1.legend()
plt.show()

#不同的n_bins取值下曲线如何改变?
fig,axes = plt.subplots(1,3,figsize = (18,4))
for ind,i in enumerate([3,10,100]):ax = axes[ind]ax.plot([0,1],[0,1],"k:",label = "Perfectly calibrated")trueproba,predproba = calibration_curve(ytest,prob_pos,n_bins = i)ax.plot(predproba,trueproba,"s-",label = "n_bins={}".format(i))ax.set_ylabel("True probability for class 1")ax.set_xlabel("Mean predicted probability")ax.set_ylim([-0.05,1.05])ax.legend()
plt.show()


很明显可以看出,n_bins越大,箱子越多,概率校准曲线就越精确,但是太过精确的曲线不够平滑,无法和希望的完美概率密度曲线相比较。n_bins越小,箱子越少,概率校准曲线就越粗糙,虽然靠近完美概率密度曲线,但是无法真实地展现模型概率预测的结果。因此需要取一个既不是太大,也不是太小的箱子个数,让概率校准曲线既不是太精确,也不是太粗糙,而是一条相对平滑,又可以反应出模型对概率预测趋势的曲线。通常来说,建议先试试看箱子数等于10的情况。箱子的数目越大,所需要的样本量也越多,否则曲线就会太过精确。

#建立更多模型
name = ["GaussianBayes","Logistic","SVC"]
gnb = GaussianNB()
logi = LR(C = 1.0,solver = "lbfgs",max_iter = 3000,multi_class = "auto")
svc = SVC(kernel = "linear",gamma = 1)
#建立循环,绘制多个模型的概率校准曲线
fig,ax1 = plt.subplots(figsize = (8,6))
ax1.plot([0,1],[0,1],"k:",label = "Perfectly calibrated")
for clf,name_ in zip([gnb,logi,svc],name):clf.fit(xtrain,ytrain)y_pred = clf.predict(xtest)#hasattr(obj,name):查看一个类obj中是否存在名字为name的接口,存在则返回Trueif hasattr(clf,"predict_proba"):prob_pos = clf.predict_proba(xtest)[:,1]else:  #use decision functionprob_pos = clf.decision_function(xtest)prob_pos = (prob_pos - prob_pos.min())/(prob_pos.max()-prob_pos.min())#返回布里尔分数clf_score = brier_score_loss(ytest,prob_pos,pos_label = y.max())trueproba,predproba = calibration_curve(ytest,prob_pos,n_bins = 10)ax1.plot(predproba,trueproba,"s-",label = "%s (%1.3f)" % (name_,clf_score))ax1.set_ylabel("True probability for class 1")
ax1.set_xlabel("Mean predicted probability")
ax1.set_ylim([-0.05,1.05])
ax1.legend()
ax1.set_title("Calibration plots (reliablity curve)")
plt.show()


从图像的结果来看,可以明显看出 ,逻辑回归的概率估计是最接近完美的概率校准曲线,所以逻辑回归的效果最完美。相对的,高斯朴素贝叶斯和支持向量机分类器的结果都比较糟糕。支持向量机呈现类似于sigmoid函数的形状,而高斯朴素贝叶斯呈现和sigmoid函数相反的形式。
**对于贝叶斯,如果概率校准曲线呈现sigmoid函数的镜像的情况,则说明数据集中的特征不是相互条件独立的。**贝叶斯原理中的“朴素”原则:特征相互条件独立原则被违反了(这其实是自己设定的,设定了10个冗余特征,这些特征就是噪音,它们之间不可能完全独立),因此贝叶斯的表现不够好。
而支持向量机的概率校准曲线效果其实是典型的置信度不足的分类器(under-confident classifier)的表现:**大量的样本点集中在决策边界的附近,因此许多样本点的置信度靠近0.5左右,即便决策边界能够将样本点判断正确,模型本身对这个结果也不是非常确信的。**相对的,离决策边界很远的点的置信度就会很高,因为它很大可能性上不会被判断错误,支持向量机在面对混合度较高的数据的时候,有着天生的置信度不足的缺点。

2.2.4 预测概率的直方图

可以通过绘制直方图来查看模型的预测概率的分布。直方图以样本的预测概率分箱后的结果为横坐标,每个箱中的样本数量为纵坐标的一个图像。注意,这里的分箱和在可靠性曲线中的分箱不同,这里的分箱是将预测概率均匀分为一个个的区间,与之前的可靠性中为了平滑的分箱完全是两码事。接下来绘制一下直方图:

fig,ax2 = plt.subplots(figsize = (8,6))
for clf,name_ in zip([gnb,logi,svc],name):clf.fit(xtrain,ytrain)y_pred = clf.predict(xtest)#hasattr(obj,name):查看一个类obj中是否存在名字为name的接口,存在则返回Trueif hasattr(clf,"predict_proba"):prob_pos = clf.predict_proba(xtest)[:,1]else:  #use decision functionprob_pos = clf.decision_function(xtest)prob_pos = (prob_pos - prob_pos.min())/(prob_pos.max()-prob_pos.min())ax2.hist(prob_pos   #预测概率,bins = 10,label = name_,histtype = "step"  #设置直方图为透明,lw = 2  #设置直方图每个柱子描边的粗细)
ax2.set_ylabel("Distribution of probability")
ax2.set_xlabel("Mean predicted probability")
ax2.set_xlim([-0.05,1.05])
ax2.set_xticks([0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1])
ax2.legend(loc = 9)
plt.show()


可以看到,高斯朴素贝叶斯的概率分布是两边非常高,中间非常低,几乎90%以上的样本都在0和1的附近,可以说是置信度最高的算法,但是贝叶斯的布里尔分数却不如逻辑回归,这证明贝叶斯在0和1附近的样本中有一部分是被分错的。支持向量机和贝叶斯则完全相反,明显是中间高、两边低,类似于正态分布的状况,证明大部分样本都在决策边界附近,置信度都徘徊在0.5左右的情况。而逻辑回归位于高斯朴素贝叶斯和支持向量机的中间,既没有太多的样本过度靠近0和1,也没有形成像支持向量机那样的正态分布。一个比较健康的正样本的概率分布,就是逻辑回归的直方图显示出来的样子。

避免混淆:概率密度曲线和概率分布直方图
(假设样本的概率分布为高斯分布,然后使用高斯的方程来估计连续型变量的概率。怎么现在绘制出的概率分布结果中,高斯朴素贝叶斯的概率反而完全不是高斯分布呢?)注意,千万不要把概率密度曲线概率分布直方图混淆。在称重汉堡的时候所绘制的曲线,是概率密度曲线,横坐标是样本的取值,纵坐标是落在这个样本取值区间中的样本个数,衡量的是每个 X X X的取值区间之内有多少样本。服从高斯分布的是 X X X的取值上的样本分布。现在概率分布直方图,横坐标是概率的取值[0,1],纵坐标是落在这个概率取值范围中的样本的个数,衡量的是每个概率取值区间之内有多少样本。这个分布,是没有任何假设。

已经得知了朴素贝叶斯和SVC预测概率的效果各方面都不如逻辑回归,那在这种情况下,如何来帮助模型或者算法,让它们对自己的预测更有信心、置信度更高呢?可以使用等近似回归来矫正概率算法

2.2.5 校准可靠性曲线

等近似回归有两种回归可以使用,一种是基于Platt的Sigmoid模型的参数校准方法,一种是基于等渗回归(isotonic calibration)的非参数的校准方法。概率校准应该发生在测试集上,必须是模型未曾见过的数据。在数学上,使用这两种方式来对概率进行校准的原理十分复杂,而此过程在sklearn中无法进行干涉,不必过于深究。如果希望深入研究利用回归校准概率的细节,可以查看sklearn中的案例。这是一个三分类数据上的概率校准过程:https://scikit-learn.org/stable/auto_examples/calibration/plot_calibration_multiclass.html#data
在这里,主要展示如何使用sklearn中的概率校正类CalibratedClasiifierCV来对二分类情况下的数据集进行概率校正。
sklearn.calibration.CalibratedClassifierCV(base_estimator = None, method = ‘sigmoid’, cv = ‘warn’)
这是一个带交叉验证的概率校准类,它使用交叉验证生成器,对交叉验证中的每一份数据,它都在训练样本上进行模型参数估计,在测试样本上进行概率校准,然后返回最佳的一组参数估计和校准结果。每一份数据的预测概率会被求解平均。注意,类CalibratedClassifierCV没有接口decision_function,要查看这个类下校准过后的模型生成的概率,必须调用predict_proba接口。

参数 含义
base_estimator 需要校准其输出决策功能的分类器,必须存在predict_proba或decision_function接口。如果参数cv = prefit,分类器必须已经拟合数据完毕。
cv 整数,确定交叉验证的策略。可能输入是:None,表示使用默认的3折交叉验证;任意整数,指定折数,对于输入整数和None的情况下来说,如果是二分类,则自动使用类sklearn.model_selection.StratifiedKFold进行折数分割,如果y是连续型变量,则使用sklearn.model_selection.KFold进行分割;已经使用其他类建好的交叉验证模式或生成器cv;可迭代的,已经分割完毕的测试集和训练集索引数组;输入“prefit”,则假设已经在分类器上拟合完毕数据,在这种模式下,使用者必须手动确定用来拟合分类器的数据与即将被校准的数据没有交集。(在版本0.20中更改:在0.22版本中输 入“None”,将由使用3折交叉验证改为5折交叉验证)
method 进行概率校准的方法,可输入“sigmoid”或者“isotonic”。输入“sigmoid”,使用基于Platt的Sigmoid模型来进行校准;输入“isotonic”,使用等渗回归来进行校准。当校准的样本量太少(如小于等于1000个测试样本)的时候,不建议使用等渗回归,因为它倾向于过拟合。样本量过少时请使用sigmoid,即Platt校准。

首先,将之前绘制可靠性曲线和直方图的代码包装成函数。考虑函数的参数为:模型、模型的名字、数据,和需要分箱的个数。在这里将直方图和可靠性曲线打包在同一个函数中,并让他们并排显示。

#包装函数
def plot_calib(models, name, xtrain, xtest, ytrain, ytest, n_bins = 10):import matplotlib.pyplot as pltfrom sklearn.metrics import brier_score_lossfrom sklearn.calibration import calibration_curvefig,(ax1,ax2) = plt.subplots(1,2,figsize = (20,6))ax1.plot([0,1],[0,1],"k:",label = "Perfectly calibrated")for clf,name_ in zip(models,name):clf.fit(xtrain,ytrain)y_pred = clf.predict(xtest)#hasattr(obj,name):查看一个类obj中是否存在名字为name的接口,存在则返回Trueif hasattr(clf,"predict_proba"):prob_pos = clf.predict_proba(xtest)[:,1]else:   #use decision functionprob_pos = clf.decision_function(xtest)prob_pos = (prob_pos-prob_pos.min())/(prob_pos.max()-prob_pos.min())#返回布里尔分数clf_score = brier_score_loss(ytest,prob_pos,pos_label = y.max())trueproba,predproba = calibration_curve(ytest,prob_pos,n_bins = n_bins)ax1.plot(predproba,trueproba,"s-",label = "%s (%1.3f)" % (name_,clf_score))ax2.hist(prob_pos,range = (0,1),bins = n_bins,label = name_,histtype = "step",lw = 2)ax2.set_ylabel("Distribution of probability")ax2.set_xlabel("Mean predicted probability")ax2.set_xlim([-0.05,1.05])ax2.legend(loc = 9)ax2.set_title("Distribution of probability")ax1.set_ylabel("True probability for class 1")ax1.set_xlabel("Mean predicted probability")ax1.set_ylim([-0.05,1.05])ax1.legend()ax1.set_title("Calibration plots(reliability curve)")plt.show()#实例化模型,设定模型的名字
from sklearn.calibration import CalibratedClassifierCV
name = ["GaussianBayes","Logistic","Bayes+isotonic","Bayes+sigmoid"]
gnb = GaussianNB()
models = [gnb,LR(C = 1, solver = "lbfgs", max_iter = 3000, multi_class = "auto")#定义两种校准方式,CalibratedClassifierCV(gnb, cv = 2, method = "isotonic"),CalibratedClassifierCV(gnb, cv = 2, method = "sigmoid")]#基于函数进行绘图
plot_calib(models,name,xtrain,xtest,ytrain,ytest)


从校正朴素贝叶斯的结果来看,isotonic等渗校正大大改善了曲线的形状,几乎让贝叶斯的效果和逻辑回归持平,并且布里尔分数也下降到了0.098,比逻辑回归还低一个点。Sigmoid校准的方式也对曲线进行了稍稍的改善,不过效果不明显。从直方图来看,isotonic校正让高斯朴素贝叶斯的效果接近逻辑回归,而sigmoid校正后的结果依然和原本的高斯朴素贝叶斯更相近。可见,当数据的特征之间不是相互条件独立的时候,使用isotonic方式来校准概率曲线,可以得到不错的结果,让模型在预测上更加“自信”。

#基于校准结果查看精确性的变化
gnb = GaussianNB().fit(xtrain,ytrain)
gnb.score(xtest,ytest)
#结果:0.8650606060606061
brier_score_loss(ytest,gnb.predict_proba(xtest)[:,1],pos_label = 1)
#结果:0.11760826355000836
gnbisotonic = CalibratedClassifierCV(gnb, cv = 2, method = "isotonic").fit(xtrain,ytrain)
gnbisotonic.score(xtest,ytest)
#结果:0.8626767676767677
brier_score_loss(ytest,gnbisotonic.predict_proba(xtest)[:,1],pos_label = 1)
#结果:0.09833190251353853

可以看出,校准概率后,布里尔分数明显变小了,但整体的准确率却略有下降,这证明算法在校准之后,尽管对概率的预测更准确了,但模型的判断力略有降低。思考一下:布里尔分数衡量模型概率预测的准确率,布里尔分数越低,代表模型的概率越接近真实概率,当进行概率校准后,本来标签是1的样本的概率应该会更接近1,而标签本来是0的样本的概率应该会更接近0,没有理由布里尔分数提升了,模型的判断准确率居然下降了。但从结果来看,模型的准确率和概率预测的正确性并不是完全一致的。为什么会这样呢?
**对于不同的概率类模型,原因是不同的。**对于SVC、决策树这样的模型来说,概率不是真正的概率,而更偏向于是一个“置信度”,这些模型也不是依赖于概率预测来进行分类(决策树依赖于树杈,而SVC依赖于决策边界),因此对于这些模型,可能存在着类别1下的概率为0.4但样本依然被分类为1的情况,这种情况代表着——模型很没有信心认为这个样本是1,但还是坚持把这个样本的标签分类为1了。这种时候,概率校准可能会向着更加错误的方向调整(如把概率为0.4的点调节得更接近0,导致模型最终判断错误),因此出现布里尔分数可能会显示和精确性相反的趋势。而对于朴素贝叶斯这样的模型,却是另一种情况。注意在朴素贝叶斯中,有各种各样的假设,除了“朴素”假设,还有对概率分布的假设(如高斯分布 ),这些假设使得贝叶斯得出的概率估计其实是有偏估计,也就是说,这种概率估计其实不是那么准确和严肃。通过校准让模型的预测概率更贴近于真实概率,本质是在统计学上让算法更加贴近对整体样本状况的估计,这样的一种校准在一组数据集上可能表现出让准确率上升,也可能表现出让准确率下降,这取决于测试集有多贴近估计的真实样本的面貌。这一系列有偏估计使得在概率校准中可能出现布里尔分数和准确度的趋势相反的情况。当然,可能还有更多更深层的原因,如概率校准过程中的数学细节如何影响校准,类calibration_curve中是如何分箱,如何通过真实标签和预测值来生成校准曲线使用的横纵坐标,这些过程中也可能有着让布里尔分数和准确率向两个方向移动的过程。
在现实中,当两者相悖的时候,请务必以准确率为标准。但是这不代表说布里尔分数和概率校准曲线就无效了。概率类模型几乎没有参数可以调整,除了换模型之外,鲜有更好的方式能够提升模型的表现,概率校准是难得的可以针对概率提升模型的方法。

#试试看对于SVC,哪种校准更有效?
name_svc = ["SVC","Logistic","SVC+isotonic","SVC+sigmoid"]
svc = SVC(kernel = "linear",gamma = 1)
models_svc = [svc,LR(C = 1,solver = "lbfgs",max_iter = 3000,multi_class = "auto")#依然定义两种校准方式,CalibratedClassifierCV(svc,cv = 2,method = "isotonic"),CalibratedClassifierCV(svc,cv = 2,method = "sigmoid")]
plot_calib(models_svc,name_svc,xtrain,xtest,ytrain,ytest)


可以看出,对于SVC,sigmoid和isotonic的校准效果都非常不错,无论是从校准曲线来看,还是从概率分布图来看,两种校准都让SVC的结果接近逻辑回归,其中sigmoid更加有效。来看看不同的SVC下的精确度结果:

name_svc = ["SVC","Logistic","SVC+isotonic","SVC+sigmoid"]
svc = SVC(kernel = "linear",gamma = 1)
models_svc = [svc,CalibratedClassifierCV(svc,cv = 2,method = "isotonic"),CalibratedClassifierCV(svc,cv = 2,method = "sigmoid")]
for clf,name in zip(models_svc,name_svc):clf.fit(xtrain,ytrain)y_pred = clf.predict(xtest)if hasattr(clf,"predict_proba"):prob_pos = clf.predict_proba(xtest)[:,1]else:prob_pos = clf.decision_function(xtest)prob_pos = (prob_pos-prob_pos.min())/(prob_pos.max()-prob_pos.min())clf_score = brier_score_loss(ytest,prob_pos,pos_label = y.max())score = clf.score(xtest,ytest)print("{}:".format(name))print("\tBrier:{:.4f}".format(clf_score))print("\tAccuracy:{:.4f}".format(score))
'''
结果:
SVC:Brier:0.1630Accuracy:0.8633
Logistic:Brier:0.0999Accuracy:0.8639
SVC+isotonic:Brier:0.0987Accuracy:0.8634
'''

可以看到,对于SVC来说,两种校正都改善了准确率和布里尔分数。可见,概率校正对于SVC非常有效。这也说明,概率校正对于原本的可靠性曲线是形如sigmoid形状的曲线的算法比较有效
在现实中,可以选择调节模型的方向,不一定要追求最高的准确率或者追求概率拟合最好,可以根据自己的需求来调整模型。当然,对于概率类模型来说,由于可以调节的参数甚少,所以更倾向于追求概率拟合,并使用概率校正的方式来调节模型。如果的确希望追求更高的准确率和Recall,可以考虑使用天生就非常准确的概率类模型——逻辑回归,也可以考虑使用除了概率校准之外还有很多其他参数可调的支持向量机分类器。

2.3 多项式朴素贝叶斯及其变化

2.3.1 多项式朴素贝叶斯MultinomialNB

sklearn.naive_bayes.MultinomialNB(alpha = 1.0, fit_prior = True, class_prior = None)
多项式贝叶斯可能是除了高斯贝叶斯之外,最为人所知的贝叶斯算法了。它也是基于原始的贝叶斯理论,但假设概率分布是服从一个简单多项式分布。多项式分布来源于统计学中的多项式实验,这种实验可以具体解释为:实验包括n次重复试验,每项试验都有不同的可能结果。在任何给定的试验中,特定结果发生的概率是不变的。
举个例子,假设一个特征矩阵 X X X表示投掷硬币的结果,则得到正面的概率为 P ( X = 正 面 ∣ Y ) = 0.5 P(X=正面\mid Y)=0.5 P(X=正面∣Y)=0.5,得到反面的概率为 P ( X = 反 面 ∣ Y ) = 0.5 P(X=反面\mid Y)=0.5 P(X=反面∣Y)=0.5,只有这两种可能并且两种结果互不干涉,并且两个随机事件的概率加和为1,这就是一个二项分布。这种情况下,适合于多项式朴素贝叶斯的特征矩阵如下表:

测试编号 X 1 X_1 X1​:出现正面 X 2 X_2 X2​:出现反面
0 0 1
1 1 0
2 1 0
3 0 1

假设另一个特征 X ′ X' X′表示投掷骰子的结果,则 i i i就可以在[1,2,3,4,5,6]中取值,六种结果互不干涉,且只要样本量足够大,概率都为 1 / 6 1/6 1/6,这就是一个多项分布。多想分布的特征矩阵应该如下表:

测试编号 出现1 出现2 出现3 出现4 出现5 出现6
0 1 0 0 0 0 0
1 0 0 0 0 0 1
2 0 0 1 0 0 0
m 0 0 0 0 0 1

可以看出:

  1. 多项式分布擅长的是分类型变量,在其原理假设中, P ( x i ∣ Y ) P(x_i\mid Y) P(xi​∣Y)的概率是离散的,并且不同的 x i x_i xi​下的 P ( x i ∣ Y ) P(x_i\mid Y) P(xi​∣Y)相互独立,互不影响。虽然sklearn中的多项式分布也可以处理连续型变量,但现实中,如果真的想要处理连续型变量,应当使用高斯朴素贝叶斯。
  2. 多项式实验中的实验结果都很具体,它所涉及的特征往往是次数、频率、计数、出现与否这样的概念,这些概念都是离散的正整数,因此sklearn中的多项式朴素贝叶斯不接受负值的输入
    由于这样的特性,多项式朴素贝叶斯的特征矩阵经常是稀疏矩阵(不一定总是稀疏矩阵),并且它经常被用于文本分类。可以使用著名的TF-IDF向量技术,也可以使用常见并且简单的单词计数向量手段与贝叶斯配合使用。这两种手段都属于常见的文本特征提取方式,可以很简单地通过sklearn来实现。
    从数学的角度来看,在一种标签类别 Y = c Y=c Y=c下,有一组分别对应特增的参数向量 θ c = ( θ c 1 , θ c 2 , . . . , θ c n ) \theta_c = (\theta_{c1},\theta_{c2},...,\theta_{cn}) θc​=(θc1​,θc2​,...,θcn​),其中 n n n表示特征的总数。一个 θ c i \theta_{ci} θci​表示这个标签类别下的第 i i i个特征所对应的参数。这个参数被定义为:
    θ i = 特 征 X i 在 Y = c 这 个 分 类 下 的 所 有 样 本 的 取 值 总 和 所 有 特 征 在 Y = c 这 个 分 类 下 的 所 有 样 本 的 取 值 总 和 \theta_i = \frac{特征X_i在Y=c这个分类下的所有样本的取值总和}{所有特征在Y=c这个分类下的所有样本的取值总和} θi​=所有特征在Y=c这个分类下的所有样本的取值总和特征Xi​在Y=c这个分类下的所有样本的取值总和​
    记作 P ( X i ∣ Y = c ) P(\textbf X_i\mid Y=c) P(Xi​∣Y=c),表示当 Y = c Y=c Y=c这个条件固定的时候,一组样本在 X i X_i Xi​这个特征上的取值被取到的概率。在多项式朴素贝叶斯中, x i x_i xi​的取值往往代表着特征 X i X_i Xi​或事件 X i X_i Xi​发生的次数,所以 θ c i \theta_{ci} θci​就是特征 X i X_i Xi​在 y y y这个分类下所有样本上发生次数的总和,占 y y y这个分类下所有特征在所有样本上的发生次数的概率。注意,在高斯朴素贝叶斯中求解的概率 P ( x i ∣ Y ) P(x_i\mid Y) P(xi​∣Y)是对于一个样本来说,而现在求解的 P ( X i ∣ Y = c ) P(\textbf X_i\mid Y=c) P(Xi​∣Y=c)是对于一个特征 X i X_i Xi​来说的概率。
    对于一个在标签类别 Y = c Y=c Y=c下,结构为(m,n)的特征矩阵来说,有: X y = [ x 11 x 12 x 13 . . . x 1 n x 21 x 22 x 23 . . . x 2 n x 31 x 32 x 33 . . . x 3 n . . . x m 1 x m 2 x m 3 . . . x m n ] \textbf X_y= \begin{bmatrix} x_{11} & x_{12} & x_{13} & ... & x_{1n} \\ x_{21} & x_{22} & x_{23} & ... & x_{2n} \\ x_{31} & x_{32} & x_{33} & ... & x_{3n} \\ & & ... & & \\ x_{m1} & x_{m2} & x_{m3} & ... & x_{mn}\\ \end{bmatrix} Xy​=⎣⎢⎢⎢⎢⎡​x11​x21​x31​xm1​​x12​x22​x32​xm2​​x13​x23​x33​...xm3​​............​x1n​x2n​x3n​xmn​​⎦⎥⎥⎥⎥⎤​其中每个 x j i x_{ji} xji​都是特征 X i X_i Xi​发生的次数。基于这些理解,通过平滑后的最大似然估计来求解参数 θ y \theta_y θy​:
    θ c i = ∑ y j = c x j i + α ∑ i = 1 n ∑ y j = c x j i + α n \theta_{ci}=\frac{\sum_{y_j=c}x_{ji}+\alpha}{\sum_{i=1}^n\sum_{y_j=c}x_{ji}+\alpha n} θci​=∑i=1n​∑yj​=c​xji​+αn∑yj​=c​xji​+α​
    对于每个特征 ∑ y j = c x j i \sum_{y_j=c}x_{ji} ∑yj​=c​xji​是特征 X i X_i Xi​下所有标签为 c c c的样本的特征取值之和,其实就是特征矩阵中每列的和。 ∑ i = 1 n ∑ y j = c x j i \sum_{i=1}^n\sum_{y_j=c}x_{ji} ∑i=1n​∑yj​=c​xji​是所有标签类别为 c c c的样本上,所有特征的取值之和,其实就是特征矩阵 X y \textbf X_y Xy​中所有元素的和。 α \alpha α被称为平滑系数,令 α > 0 \alpha>0 α>0来防止训练数据中出现过的一些词汇没有出现在测试集中导致的0概率,以避免让参数 θ \theta θ为0的情况。如果将 α \alpha α设置为1,则这个平滑叫做拉普拉斯平滑,如果 α \alpha α小于1,则把它叫做利德斯通平滑。两种平滑都属于自然语言处理中比较常用的用来平滑分类数据的统计手段。
    在sklearn中,用来执行多项式朴素贝叶斯的类MultinomialNB包含如下的参数和属性:
    sklearn.naive_bayes.MultinomialNB(alpha = 1.0, fit_prior = True, class_prior = None)
参数 含义
alpha 浮点数,可不填(默认为1.0)。拉普拉斯或利德斯通平滑的参数 α \alpha α,如果设置为0则表示完全没有平滑选项。但是需要注意的是,平滑相当于认为给概率加上一些噪音,因此 α \alpha α设置得越大,多项式朴素贝叶斯的精确性会越低(虽然影响不是非常大),布里尔分数也会逐渐升高。
fit_prior 布尔值,可不填(默认为True)。是否学习先验概率 P ( Y = c ) P(Y=c) P(Y=c)。如果设置为False,则不使用先验概率,而使用统一先验概率(uniform prior),即认为每个标签出现的概率是 1 n c l a s s e s \frac{1}{n_{classes}} nclasses​1​
class_prior 形似数组的结构,结构为(n_classes,),可不填,默认为None。类的先验概率 P ( Y = c ) P(Y=c) P(Y=c)。如果没有给出具体的先验概率则自动根据数据来进行计算。

通常在实例化多项式朴素贝叶斯的时候,会让所有的参数保持默认。接下来简单建立一个多项式朴素贝叶斯的例子试试看:

#导入需要的模块和库
from sklearn.preprocessing import MinMaxScaler
from sklearn.naive_bayes import MultinomialNB
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_blobs
from sklearn.metrics import brier_score_loss
#建立数据集
class_1 = 500
class_2 = 500 #两个类别分别设定500个样本
centers = [[0.0,0.0],[2.0,2.0]]  #设定两个类别的中心
clusters_std = [0.5,0.5]  #设定两个类别的方差
x,y = make_blobs(n_samples = [class_1,class_2],centers = centers,cluster_std= clusters_std,random_state = 0,shuffle = False)
xtrain,xtest,ytrain,ytest = train_test_split(x,y,test_size = 0.3,random_state = 420)
#归一化,确保输入的矩阵不带有负数
#先归一化,保证输入多项式朴素贝叶斯的特征矩阵中不带有负数
mms = MinMaxScaler().fit(xtrain)
xtrain_ = mms.transform(xtrain)
xtest_ = mms.transform(xtest)
#建立一个多项式朴素贝叶斯分类器
#注意,这里的xtrain_是连续型变量
mnb = MultinomialNB().fit(xtrain_,ytrain)
#重要属性:调用根据数据获取的,每个标签类的对数先验概率log(P(Y)),由于概率永远是在[0,1]之间,因此对数先验概率返回的永远是负值
mnb.class_log_prior_
#结果:array([-0.69029411, -0.69600841])
np.unique(ytrain)
#结果:array([0, 1])
(ytrain == 1).sum()/ytrain.shape[0]
#结果:0.49857142857142855
mnb.class_log_prior_.shape#永远等于标签中所带的类别数量
#结果:(2,)
#可以使用np.exp来查看真正的概率值
np.exp(mnb.class_log_prior_)
#结果:array([0.50142857, 0.49857143])
#重要属性:返回一个固定标签类别下的每个特征的对数概率log(P(Xi|y))
mnb.feature_log_prob_
'''
结果:
array([[-0.76164788, -0.62903951],[-0.72500918, -0.6622691 ]])
'''
mnb.feature_log_prob_.shape#2个特征2个标签
#结果:(2, 2)
#重要属性:在fit时每个标签类别下包含的样本数。当fit接口中的sample_weight被设置时,该接口返回的值也会受到加权的影响
mnb.class_count_
#结果:array([351., 349.])
mnb.class_count_.shape#返回和标签类别一样的结构
#结果:(2,)#那么分类器的效果如何
#一些传统的接口
mnb.predict(xtest_)
mnb.predict_proba(xtest_)#每个样本在每个标签取值下的概率
mnb.score(xtest_,ytest)
#结果:0.5433333333333333
brier_score_loss(ytest,mnb.predict_proba(xtest_)[:,1],pos_label = 1)
#结果:0.24977828412546035#效果不太理想,思考一下多项式朴素贝叶斯的性质,能够再做些什么
#来试试看把xtrain转换成分类型数据
#注意xtrain没有经过归一化,因为做哑变量之后自然所有的数据就不会有负数了
from sklearn.preprocessing import KBinsDiscretizer
kbs = KBinsDiscretizer(n_bins = 10, encode = "onehot").fit(xtrain)
xtrain_ = kbs.transform(xtrain)
xtest_ = kbs.transform(xtest)
mnb = MultinomialNB().fit(xtrain_,ytrain)
mnb.score(xtest_,ytest)
#结果:0.9966666666666667
brier_score_loss(ytest,mnb.predict_proba(xtest_)[:,1], pos_label = 1)
#结果:0.0014593932778211862

可以看出,多项式朴素贝叶斯的基本操作和代码都非常简单。同样的数据,如果采用哑变量的方式进行分箱处理,多项式朴素贝叶斯的效果会突飞猛进。

2.3.2 伯努利朴素贝叶斯BernoulliNB

多项式朴素贝叶斯可同时处理二项分布(抛硬币)和多项分布(掷骰子),其中二项分布又叫做伯努利分布,是一种现实中常见,并且拥有很多优越数学性质的分布。因此,既然有着多项式朴素贝叶斯,自然也有专门用来处理二项分布的朴素贝叶斯:伯努利朴素贝叶斯。
伯努利贝叶斯类BernoulliNB假设数据服从多元伯努利分布,并在此基础上应用朴素贝叶斯的训练和分类过程。多元伯努利分布简单来说,就是数据集中可以存在多个特征,但每个特征都是二分类的,可以以布尔变量表示,也可以表示为{0,1}或者{-1,1}等任意二分类组合。因此,这个类要求将样本转换为二分类特征向量,如果数据本身不是二分类的,那可以使用类中专门用来二值化的参数binarize来改变数据。
伯努利朴素贝叶斯与多项式朴素贝叶斯非常相似, 都常用于处理文本分类数据。但由于伯努利朴素贝叶斯是处理二项分布,所以它更加在意的是“存在与否”,而不是“出现多少次”这样的次数或频率,这是伯努利贝叶斯与多项式贝叶斯的根本性不同。在文本分类的情况下,伯努利朴素贝叶斯可以使用单词出现向量(而不是单词计数向量)来训练分类器。文档较短的数据集上,伯努利朴素贝叶斯的效果会更加好。如果时间允许,建议两种模型都试试看。
伯努利朴素贝叶斯类的参数:sklearn.naive_bayes.BernoulliNB(alpha = 1.0, binarize = 0.0, fit_prior = True, class_prior = None)

参数 含义
alpha 浮点数,可不填,默认为1.0。拉普拉斯或利德斯通平滑的参数 α \alpha α,如果设置为0则表示完全没有平滑选项。但是需要注意的是,平滑相当于人为给概率加上一些噪音,因此 α \alpha α设置得越大,多项式朴素贝叶斯的精确性会越低(虽然影响不是非常大),布里尔分数也会逐渐升高。
binarize 浮点数或None,可不填,默认为0。将特征二值化的阈值,如果设定为None,则会假定说特征已经被二值化完毕。
fit_prior 布尔值,可不填,默认为True。是否学习先验概率 P ( Y = c ) P(Y=c) P(Y=c)。如果设置为False,则不使用先验概率,而使用统一先验概率(uniform prior),即认为每个标签出现的概率是 1 n c l a s s e s \frac{1}{n_{classes}} nclasses​1​
class_prior 形似数组的结构,结构为(n_classes,),可不填,默认为None。类的先验概率 P ( Y = c ) P(Y=c) P(Y=c)。如果没有给出具体的先验概率则自动根据数据来进行计算。

在sklearn中,伯努利朴素贝叶斯的实现也非常简单 :

from sklearn.naive_bayes import BernoulliNB
#普通来说应该使用二值化的类sklearn.preprocessing.Binarizer来将特征一个个二值化
#然而这样效率过低,因此选择归一化之后直接设置一个阈值
mms = MinMaxScaler().fit(xtrain)
xtrain_ = mms.transform(xtrain)
xtest_ = mms.transform(xtest)#不设置二值化
bnl_ = BernoulliNB().fit(xtrain_,ytrain)
bnl_.score(xtest_,ytest)
#结果:0.49666666666666665
brier_score_loss(ytest,bnl_.predict_proba(xtest_)[:,1],pos_label = 1)
#结果:0.25000009482193225#设置二值化阈值0.5
bnl = BernoulliNB(binarize = 0.5).fit(xtrain_,ytrain)
bnl.score(xtest_,ytest)
#结果:0.9833333333333333
brier_score_loss(ytest,bnl.predict_proba(xtest_)[:,1],pos_label = 1)
#结果:0.010405875827339534

和多项式贝叶斯一样,伯努利贝叶斯的结果也收到数据结构非常大的影响。因此,根据数据结构来选择贝叶斯,是贝叶斯模型选择中十分重要的一点。

2.3.3 探索贝叶斯:贝叶斯的样本不均衡问题

接下来,来探讨一个分类算法永远都逃不过的核心问题:样本不平衡。贝叶斯由于分类效力不算太顶尖,所以它对样本不平衡极为敏感,接下来就看一看样本不平衡如何影响了贝叶斯:

#导入需要的模块,建立样本不平衡的数据集
from sklearn.naive_bayes import MultinomialNB, GaussianNB, BernoulliNB
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_blobs
from sklearn.preprocessing import KBinsDiscretizer
from sklearn.metrics import brier_score_loss as BS, recall_score, roc_auc_score as AUC
class_1 = 50000  #多数类为50000个样本
class_2 = 500  #少数类为500个样本  1%
centers = [[0.0,0.0],[5.0,5.0]]  #设定两个类别的中心
clusters_std = [3,1]  #设定两个类别的方差
x,y = make_blobs(n_samples = [class_1,class_2],centers = centers,cluster_std = clusters_std,random_state = 0,shuffle = False)
x.shape
#结果:(50500, 2)
np.unique(y)
#结果:array([0, 1])#查看所有贝叶斯在样本不平衡数据集上的表现
name = ["Multinomial","Gaussian","Bernoulli"]
models = [MultinomialNB(),GaussianNB(),BernoulliNB()]
for name,clf in zip(name,models):#分测试集和训练集xtrain,xtest,ytrain,ytest = train_test_split(x,y,test_size = 0.3, random_state = 420)#预处理if name != "Gaussian":kbs = KBinsDiscretizer(n_bins = 10, encode = "onehot").fit(xtrain)xtrain = kbs.transform(xtrain)xtest = kbs.transform(xtest)#拟合&结果clf.fit(xtrain,ytrain)y_pred = clf.predict(xtest)proba = clf.predict_proba(xtest)[:,1]score = clf.score(xtest,ytest)#准确率print(name)print("\tBrier:{:.3f}".format(BS(ytest,proba,pos_label = 1)))print("\tAccuracy:{:.3f}".format(score))print("\tRecall:{:.3f}".format(recall_score(ytest,y_pred)))print("\tAUC:{:.3f}".format(AUC(ytest,proba)))
'''
结果:
MultinomialBrier:0.007Accuracy:0.990Recall:0.000AUC:0.991
GaussianBrier:0.006Accuracy:0.990Recall:0.438AUC:0.993
BernoulliBrier:0.009Accuracy:0.987Recall:0.771AUC:0.987
'''

从结果上来看,多项式朴素贝叶斯判断出了所有的多数类样本,但放弃了全部的少数类样本,受到样本不均衡影响最严重。高斯比多项式在少数类的判断上更加成功一些,至少得到了43.8% 的recall。伯努利贝叶斯虽然整体的准确度和布里尔分数不如多项式和高斯朴素贝叶斯,但至少成功捕捉出了77.1%的少数类。可见,伯努利贝叶斯最能够忍受样本不均衡问题。
可是,伯努利贝叶斯只能用于处理二项分布数据,在现实中,强行将所有的数据都二值化不会永远得到好结果,在有多个特征的时候,更需要一个个去判断究竟二值化的阈值该取多少才能让算法的效果优秀。这样做无疑是非常低效的。那如果目标是捕捉少数类,那应该怎么办?高斯朴素贝叶斯的效果虽然比多项式好,但是也没有好到可以用来帮助捕捉少数类的程度,还不如抛硬币的结果。因此,孜孜不倦的统计学家们改进了多项式朴素贝叶斯,改进了多项式朴素贝叶斯的多项缺点,包括在处理样本不均衡问题时的表现。

2.3.4 改进多项式朴素贝叶斯:补集朴素贝叶斯ComplementNB

补集朴素贝叶斯(complement naive Bayes,CNB)算法是标准多项式朴素贝叶斯算法的改进。最开始创造出CNB的初衷是为了解决贝叶斯中的“朴素”假设带来的各种问题,希望能够创造出数学方法以逃避贝叶斯中的朴素假设,让算法能够不去关心所有特征之间是否是条件独立的。以此为基础,创造出了能够解决样本不平衡问题,并且能够一定程度上忽略朴素假设的补集朴素贝叶斯。在实验中,CNB的参数估计已经被证明比普通多项式朴素贝叶斯更稳定,并且它特别适合于样本不平衡的数据集。有时候,CNB在文本分类任务情况下,有时能够优于多项式朴素贝叶斯,因此现有补集朴素贝叶斯也开始逐渐流行。
关于补集朴素贝叶斯具体是如何逃避朴素假设,或者如何让样本不均衡问题得到改善,背后有深刻的数学原理和复杂的数学证明过程,具体内容参阅这篇论文:https://www.aaai.org/Library/ICML/2003/icml03-081.php
简单来说,CNB使用来自每个标签类别的补集的概率,并以此来计算每个特征的权重。
θ ^ i , y ≠ c = α i + ∑ y j ≠ c x i j α i n + ∑ i , y ≠ c ∑ i = 1 n x i j \hat\theta_{i,y\neq c} = \frac{\alpha_i+\sum_{y_j\neq c}x_{ij}}{\alpha_in+\sum_{i,y\neq c}\sum_{i=1}^nx_{ij}} θ^i,y​=c​=αi​n+∑i,y​=c​∑i=1n​xij​αi​+∑yj​​=c​xij​​
其中 j j j表示每个样本, x i j x_{ij} xij​表示在样本 j j j上对于特征 i i i下的取值,在文本分类中通常是技术的值或者是TF-IDF值。 α \alpha α是像标准多项式朴素贝叶斯中一样的平滑系数。可以看出,这个看似复杂的公式其实很简单, ∑ y j ≠ c x i j \sum_{y_j\neq c}x_{ij} ∑yj​​=c​xij​其实指的就是,一个特征 i i i下,所有标签类别不等于 c c c值的样本的特征取值之和。而 ∑ i , y ≠ c ∑ i = 1 n x i j \sum_{i,y\neq c}\sum_{i=1}^nx_{ij} ∑i,y​=c​∑i=1n​xij​其实就是所有特征下,所有标签类别不等于 c c c值的样本的特征取值之和。其实就是多项式分布的逆向思路。
w c i = l o g θ ^ i , y ≠ c w_{ci} = log\hat \theta _{i,y\neq c} wci​=logθ^i,y​=c​或者可以选择: w c i = l o g θ ^ i , y ≠ c ∑ j ∣ l o g θ ^ i , y ≠ c ∣ w_{ci} = \frac{log\hat \theta _{i,y\neq c}}{\sum_j\mid log\hat \theta _{i,y\neq c}\mid} wci​=∑j​∣logθ^i,y​=c​∣logθ^i,y​=c​​
对于这个概率,对它取对数之后得到权重。还可以选择除以它的L2范式,以解决在多项式分布中,特征取值比较多的样本(如比较长的文档)支配参数估计的情况。很多时候特征矩阵是稀疏矩阵,但也不排除在有一些事件中,可以一次在两个特征中取值的情况。如果一个样本下的很多歌随机事件可以同时发生,并且互不干涉,那么这个样本上可能很多个特征下都有取值——文本分类中就有很多这样的情况。如可以有如下特征矩阵:

索引 X1 X2
0 1 1
1 0 1

这种状况下,索引为0的样本就会在参数估计中占更多的权重。更甚,如果一个样本下的很多个随机事件同时发生,还在一次实验中发生了多次,那这个样本在参数估计中也会占有更大的权重。

索引 X1 X2
0 5 1
1 0 1

基于这个权重,补集朴素贝叶斯中的一个样本的预测规则为: P ( Y ≠ x ∣ X ) = arg min ⁡ c ∑ i x i w c i P(Y\neq x\mid \textbf X) = \argmin_c\sum_ix_iw_{ci} P(Y​=x∣X)=cargmin​i∑​xi​wci​即求解出的最小补集概率所对应的标签就是样本的标签,因为 Y ≠ c Y\neq c Y​=c的概率越小,则意味着 Y = c Y = c Y=c的概率越大。所以样本属于标签类别 c c c。
在sklearn中,补集朴素贝叶斯由类ComplementNB完成,它包含的参数和多项式贝叶斯也非常相似:
sklearn.naive_bayes.ComplementNB(alpha = 1.0, fit_prior = True, class_prior = None, norm = False)

参数 含义
alpha 浮点数,可不填,默认为1.0,。拉普拉斯或利德斯通平滑的参数 α \alpha α,如果设置为0则表示完全没有平滑选项。但是需要注意的是,平滑相当于人为给概率加上一些噪音,因此 α \alpha α设置得越大,多项式朴素贝叶斯的精确性会越低(虽然影响不是非常大),布里尔分数也会逐渐升高。
norm 布尔值,可不填,默认为F
fit_prior 布尔值,可不填,默认为True。是否学习先验概率 P ( Y = c ) P(Y=c) P(Y=c)。如果设置为False,则不使用先验概率,而使用统一先验概率(uniform prior),即认为每个标签出现的概率是 1 n c l a s s e s \frac{1}{n_{classes}} nclasses​1​
class_prior 形似数组的结构,结构为(n_classes,),可不填,默认为None。类的先验概率 P ( Y = c ) P(Y=c) P(Y=c)。如果没有给出具体的先验概率则自动根据数据来进行计算。

接下来看看补集朴素贝叶斯在不平衡样本上的表现,同时来计算一下每种贝叶斯的计算速度:

from sklearn.naive_bayes import ComplementNB
from time import time
import datetime
name = ["Multinomial","Gaussian","Bernoulli","Complement"]
models = [MultinomialNB(),GaussianNB(),BernoulliNB(),ComplementNB()]
times = time()
for name,clf in zip(name,models):xtrain,xtest,ytrain,ytest = train_test_split(x,y,test_size = 0.3, random_state = 420)#预处理if name == "Gaussian":passelse:kbs = KBinsDiscretizer(n_bins = 10,encode = "onehot").fit(xtrain)xtrain = kbs.transform(xtrain)xtest = kbs.transform(xtest)clf.fit(xtrain,ytrain)y_pred = clf.predict(xtest)proba = clf.predict_proba(xtest)[:,1]score = clf.score(xtest,ytest)print(name)print("\tBrier:{:.3f}".format(BS(ytest,proba,pos_label = 1)))print("\tAccuracy:{:.3f}".format(score))print("\tRecall:{:.3f}".format(recall_score(ytest,y_pred)))print("\tAUC:{:.3f}".format(AUC(ytest,proba)))print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))
'''
结果:
MultinomialBrier:0.007Accuracy:0.990Recall:0.000AUC:0.991
00:00:036025
GaussianBrier:0.006Accuracy:0.990Recall:0.438AUC:0.993
00:00:053025
BernoulliBrier:0.009Accuracy:0.987Recall:0.771AUC:0.987
00:00:082032
ComplementBrier:0.038Accuracy:0.953Recall:0.987AUC:0.991
00:00:109038
'''

可以发现,补集朴素贝叶斯牺牲了部分整体的准确度和布里尔指数,但是得到了十分高的召回率Recall,捕捉出了98.7%的少数类,并且在此基础上维持了和原本的多项式朴素贝叶斯一致的AUC分数。和其他的贝叶斯算法比起来,补集朴素贝叶斯的运行速度也十分优秀。如果目标是捕捉少数类,那毫无疑问会希望选择补集朴素贝叶斯作为算法。

3 案例:贝叶斯分类器做文本分类

文本分类是现代机器学习应用中的一大模块,更是自然语言处理的基础之一。可以通过将文字数据处理成数字数据,然后使用贝叶斯来帮助判断一段话,或者一篇文章中的主题分类、情感倾向,甚至文章体裁。现在绝大多数社交媒体数据的自动化采集,都是依靠首先将文本编码成数字,然后按分类结果采集需要的信息。虽然现在自然语言处理领域大部分由深度学习所控制,贝叶斯分类器依然是文本分类中的一颗明珠。接下来看看,贝叶斯分类器是怎样实现文本分类的。

3.1 文本编码技术简介

3.1.1 单词计数向量

在开始分类之前,必须先将文本编码成数字。一种常用的方法是单词计数向量技术。在这种计数下,一个样本可以包含一段话或一篇文章,这个样本中如果出现了10个单词,就会有10个特征(n=10),每个特征 X i X_i Xi​代表一个单词,特征的取值 x i x_i xi​表示这个单词在这个样本中总共出现了几次,是一个离散的,代表次数的,正整数。
在sklearn当中,单词计数向量技术可以通过feature.extraction.text模块中的CountVectorizer类实现,看一个简单的例子:

sample = {"Machine learning is fascinating. it is wonderful","Machine learning is a sensational techonology","Elsa is a popular character"}
from sklearn.feature_extraction.text import CountVectorizer
vec = CountVectorizer()
x = vec.fit_transform(sample)
x
'''
结果:
<3x11 sparse matrix of type '<class 'numpy.int64'>'with 15 stored elements in Compressed Sparse Row format>
'''
#使用接口get_feature_names()调用每个列的名称
vec.get_feature_names()
'''
接口:
['character','elsa','fascinating','is','it','learning','machine','popular','sensational','techonology','wonderful']
'''
import pandas as pd
#注意稀疏矩阵是无法输入pandas的
CVresult = pd.DataFrame(x.toarray(),columns = vec.get_feature_names())
CVresult


从这个编码结果,可以发现两个问题。首先来回忆一下多项式朴素贝叶斯的计算公式:
t h e t a c i = ∑ y j = c x j i + α ∑ i = 1 n ∑ y j = c x j i + α n theta_{ci}=\frac{\sum_{y_j = c}x_{ji}+\alpha}{\sum_{i=1}^n\sum_{y_j=c}x_{ji}+\alpha n} thetaci​=∑i=1n​∑yj​=c​xji​+αn∑yj​=c​xji​+α​
如果将每一列加和,除以整个特征矩阵的和,就是每一列对应的概率 θ i \theta_i θi​。由于是将 x j i x_{ji} xji​进行加和,对于一个在很多特征下都有值的样本来说,这个样本在对 θ c i \theta_{ci} θci​的贡献就会比其他的样本更大。对于句子特别长的样本而言,这个样本对 θ i \theta_i θi​的影响是巨大的。因此补集朴素贝叶斯让每个特征的权重除以自己的L2范式,就是为了避免这种情况发生。另外,观察矩阵会发现"is"这个单词出现了四次,经过计算,这个单词出现的概率就会最大,但其实它对语义并没有什么影响(除非希望判断的是,文章描述的是过去的事件还是现在发生的事件)。可以预见,如果使用单词计数向量,可能会导致一部分常用词(如中文中的“的”)频繁出现在矩阵中并且占有很高的权重,对分类来说,这明显是对算法的一种舞蹈。为了解决这个问题,比起使用次数,使用单词在句子中所占的比例来编码单词特征,就是著名的TF-IDF方法。

3.1.2 TF-IDF

TF-IDF全称term frequency-inverse document frequency,词频逆文档频率,是通过单词在文档中出现的频率来衡量其权重,也就是说,IDF的大小与一个词的常见程度成反比,这个词越常见,编码后为它设置的权重会倾向于越小,以此来压制频繁出现的一些无意义的词。在sklearn当中,使用feature_extraction.text中类的TfidfVectorizer来执行这种编码。

from sklearn.feature_extraction.text import TfidfVectorizer as TFIDF
vec = TFIDF()
x = vec.fit_transform(sample)
x  #每一个单词作为一个特征,每个单词在这个句子中所占的比例
'''
结果:
<3x11 sparse matrix of type '<class 'numpy.float64'>'with 15 stored elements in Compressed Sparse Row format>
'''
#同样使用接口get_feature_names()调用每个列的名称
TFIDFresult = pd.DataFrame(x.toarray(),columns = vec.get_feature_names())
TFIDFresult

#使用TF-IDF编码之后,出现得多的单词的权重被降低了吗
CVresult.sum(axis = 0)/CVresult.sum(axis = 0).sum()
'''
结果:
character      0.0625
elsa           0.0625
fascinating    0.0625
is             0.2500
it             0.0625
learning       0.1250
machine        0.1250
popular        0.0625
sensational    0.0625
techonology    0.0625
wonderful      0.0625
dtype: float64
'''
TFIDFresult.sum(axis = 0)/TFIDFresult.sum(axis = 0).sum()
#将原本出现次数比较多的词进行压缩,以实现压缩权重
#将原本出现次数比较少的词进行拓展,以实现增加权重
'''
结果:
character      0.083071
elsa           0.083071
fascinating    0.064516
is             0.173225
it             0.064516
learning       0.110815
machine        0.110815
popular        0.083071
sensational    0.081192
techonology    0.081192
wonderful      0.064516
dtype: float64
'''

在之后的例子中,都会使用TF-IDF的编码方式。

3.2探索文本数据

在现实中,文本数据的处理是十分耗时耗力的,尤其是不规则的长文本的处理方式,绝对不是一两句话可以解释的,因此在这里将使用的数据集是sklearn中自带的文本数据集fetch_20newsgroups。这个数据集是20个网络新闻组的语料库,其中包含约2万篇新闻,全部以英文显示,如果希望使用中文规则处理过程会更加困难,并且需要自己加在中文的语料库。这个例子主要目的是展示贝叶斯的用法和效果,因此就使用sklearn自带的语料库。

from sklearn.datasets import fetch_20newsgroups
#初次使用这个数据集的时候,会在实例化的时候开始下载
data = fetch_20newsgroups()
#通常使用data来查看data里面到底包含了什么内容,但由于fetch_20newsgroups这个类加载出的数据巨大,数据结构中混杂很多文字,因此很难去看清
#不同类型的新闻,标签的分类有哪些
data.target_names
'''
结果:
['alt.atheism','comp.graphics','comp.os.ms-windows.misc','comp.sys.ibm.pc.hardware','comp.sys.mac.hardware','comp.windows.x','misc.forsale','rec.autos','rec.motorcycles','rec.sport.baseball','rec.sport.hockey','sci.crypt','sci.electronics','sci.med','sci.space','soc.religion.christian','talk.politics.guns','talk.politics.mideast','talk.politics.misc','talk.religion.misc']
'''
#其实fetch_20newsgroups也是一个类,既然是类,应该就有可以调用的参数
#面对简单数据集,往往在实例化过程中什么都不写,但是现在data中数据量太多,不方便探索
#因此需要看看;类fetch_20newsgroups都有什么样的参数可以调整

sklearn.datasets.fetch_20newsgroups(data_home = None, subset = ‘train’, categories = None, shuffle = True, random_state = 42, remove = (), download_if_missing = True)
在这之中,有几个比较重要的参数:

参数 含义
subset 选择类中包含的数据子集,输入“train”表示训练集,“test”表示输入测试集,“all”表示加载所有的数据。
categories 可输入None或者数据所在的目录。选择一个子集下,不同类型或不同内容的数据所在的目录。如果不输入默认None,则会加载全部的目录。
download_if_missing 可选,默认是True。如果发现本地数据不全,是否自行进行下载。
shuffle 布尔值,可不填,表示是否打乱样本顺序。对于假设样本之间互相独立并且服从相同分布的算法或模型(如随机梯度下降)来说可能很重要。

现在可以直接通过参数来提取希望得到的数据了。

import numpy as np
import pandas as pd
categories = ["sci.space"#科学技术-太空,"rec.sport.hockey"#运动-曲棍球,"talk.politics.guns"#政治-枪支问题,"talk.politics.mideast"#政治-中东问题]
train = fetch_20newsgroups(subset = "train",categories = categories)
test = fetch_20newsgroups(subset = "test",categories = categories)
#train
#可以观察到,里面依然是类字典结构,可以通过使用键的方式来提取内容
train.target_names
'''
结果:
['rec.sport.hockey','sci.space','talk.politics.guns','talk.politics.mideast']
'''
#查看总共有多少篇文章存在
len(train.data)
#结果:2303
#随意提取一篇文章来看看
#train.data[0]
#print(train.data[0])
#查看一下标签
np.unique(train.target)
#结果:array([0, 1, 2, 3], dtype=int64)
len(train.target)
#结果:2303
#是否存在样本不平衡问题?
for i in [0,1,2,3]:print(i,(train.target == i).sum()/len(train.target))
'''
结果:
0 0.26052974381241856
1 0.25749023013460703
2 0.23708206686930092
3 0.24489795918367346
'''

3.3 使用TF-IDF将文本数据编码

from sklearn.feature_extraction.text import TfidfVectorizer as TFIDF
xtrain = train.data
xtest = test.data
ytrain = train.target
ytest = test.target
tfidf = TFIDF().fit(xtrain)
xtrain_ = tfidf.transform(xtrain)
xtest_ = tfidf.transform(xtest)
xtrain_
'''
结果:
<2303x40725 sparse matrix of type '<class 'numpy.float64'>'with 430306 stored elements in Compressed Sparse Row format>
'''
tosee = pd.DataFrame(xtrain_.toarray(),columns = tfidf.get_feature_names())
tosee.shape
#结果:(2303, 40725)
tosee.head()

3.4 在贝叶斯上分别建模,查看结果

from sklearn.naive_bayes import MultinomialNB,ComplementNB,BernoulliNB
from sklearn.metrics import brier_score_loss as BS
name = ["Multinomial","Complement","Bernoulli"]
#注意高斯朴素贝叶斯不接受稀疏矩阵
models = [MultinomialNB(),ComplementNB(),BernoulliNB()]for name,clf in zip(name,models):clf.fit(xtrain_,ytrain)y_pred = clf.predict(xtest_)proba = clf.predict_proba(xtest_)score = clf.score(xtest_,ytest)print(name)#4个不同的标签取值下的布里尔分数Bscore = []ytest_= ytest.copy()ytest_ = pd.get_dummies(ytest_)for i in range(len(np.unique(ytrain))):bs = brier_score_loss(ytest_[i],proba[:,i])Bscore.append(bs)print("\tBrier under {}:{:.3f}".format(train.target_names[i],bs))print("\tAverage Brier:{:.3f}".format(np.mean(Bscore)))print("\tAccuracy:{:.3f}".format(score))print("\n")
'''
结果:
MultinomialBrier under rec.sport.hockey:0.018Brier under sci.space:0.033Brier under talk.politics.guns:0.030Brier under talk.politics.mideast:0.026Average Brier:0.027Accuracy:0.975ComplementBrier under rec.sport.hockey:0.023Brier under sci.space:0.039Brier under talk.politics.guns:0.039Brier under talk.politics.mideast:0.033Average Brier:0.033Accuracy:0.986BernoulliBrier under rec.sport.hockey:0.068Brier under sci.space:0.025Brier under talk.politics.guns:0.045Brier under talk.politics.mideast:0.053Average Brier:0.048Accuracy:0.902
'''

从结果上来看,两种贝叶斯的效果都很不错。虽然补集贝叶斯的布里尔分数更高,但它的精确度更高。可以使用概率校准来是时间按能否让模型进一步突破:

from sklearn.calibration import CalibratedClassifierCV
name = ["Multinomial","Multinomial + Isotonic","Multinomial + Sigmoid","Complement","Complement + Isotonic","Complement + Sigmoid","Bernoulli","Bernoulli + Isotonic","Bernoulli + Sigmoid"]
models = [MultinomialNB(),CalibratedClassifierCV(MultinomialNB(),cv = 2,method = 'isotonic'),CalibratedClassifierCV(MultinomialNB(),cv = 2,method = 'sigmoid'),ComplementNB(),CalibratedClassifierCV(ComplementNB(),cv = 2,method = 'isotonic'),CalibratedClassifierCV(ComplementNB(),cv = 2,method = 'sigmoid'),BernoulliNB(),CalibratedClassifierCV(BernoulliNB(),cv = 2,method = 'isotonic'),CalibratedClassifierCV(BernoulliNB(),cv = 2,method = 'sigmoid')]
for name,clf in zip(name,models):clf.fit(xtrain_,ytrain)y_pred = clf.predict(xtest_)proba = clf.predict_proba(xtest_)score = clf.score(xtest_,ytest)print(name)Bscore = []ytest_= ytest.copy()ytest_ = pd.get_dummies(ytest_)for i in range(len(np.unique(ytrain))):bs = brier_score_loss(ytest_[i],proba[:,i])Bscore.append(bs)print("\tBrier under {}:{:.3f}".format(train.target_names[i],bs))print("\tAverage Brier:{:.3f}".format(np.mean(Bscore)))print("\tAccuracy:{:.3f}".format(score))print("\n")
'''
结果:
MultinomialBrier under rec.sport.hockey:0.018Brier under sci.space:0.033Brier under talk.politics.guns:0.030Brier under talk.politics.mideast:0.026Average Brier:0.027Accuracy:0.975Multinomial + IsotonicBrier under rec.sport.hockey:0.006Brier under sci.space:0.012Brier under talk.politics.guns:0.013Brier under talk.politics.mideast:0.009Average Brier:0.010Accuracy:0.973Multinomial + SigmoidBrier under rec.sport.hockey:0.006Brier under sci.space:0.012Brier under talk.politics.guns:0.013Brier under talk.politics.mideast:0.009Average Brier:0.010Accuracy:0.973ComplementBrier under rec.sport.hockey:0.023Brier under sci.space:0.039Brier under talk.politics.guns:0.039Brier under talk.politics.mideast:0.033Average Brier:0.033Accuracy:0.986Complement + IsotonicBrier under rec.sport.hockey:0.004Brier under sci.space:0.007Brier under talk.politics.guns:0.009Brier under talk.politics.mideast:0.006Average Brier:0.006Accuracy:0.985Complement + SigmoidBrier under rec.sport.hockey:0.004Brier under sci.space:0.009Brier under talk.politics.guns:0.010Brier under talk.politics.mideast:0.007Average Brier:0.007Accuracy:0.986BernoulliBrier under rec.sport.hockey:0.068Brier under sci.space:0.025Brier under talk.politics.guns:0.045Brier under talk.politics.mideast:0.053Average Brier:0.048Accuracy:0.902Bernoulli + IsotonicBrier under rec.sport.hockey:0.020Brier under sci.space:0.017Brier under talk.politics.guns:0.032Brier under talk.politics.mideast:0.035Average Brier:0.026Accuracy:0.937Bernoulli + SigmoidBrier under rec.sport.hockey:0.066Brier under sci.space:0.030Brier under talk.politics.guns:0.056Brier under talk.politics.mideast:0.059Average Brier:0.053Accuracy:0.879
'''

可以观察到,多项式分布下无论如何调整,算法的效果都不如朴素贝叶斯来得好。因此在分类的时候,应该选择补集朴素贝叶斯。对于补集朴素贝叶斯来说,使用sigmoid进行概率校准的模型综合最优秀:准确率最高,对数损失和布里尔分数都在0.1以下,可以说是非常理想的模型了。
对于机器学习而言,朴素贝叶斯也许不是最常用的分类算法,但作为概率预测算法中唯一一个真正依赖概率来进行计算,并且简单快捷的算法,朴素贝叶斯还是常常被人们提起。并且,朴素贝叶斯在文本分类上的效果的确非常优秀。由此可见,只要能够提供足够的数据,合理利用高维数据进行训练,朴素贝叶斯就可以提供意想不到的效果。

参考资料:https://www.bilibili.com/video/BV1Ng411K7H6?p=16&spm_id_from=pageDriver

sklearn中的朴素贝叶斯相关推荐

  1. sklearn中的朴素贝叶斯算法

    sklearn中的朴素贝叶斯分类器 之前理解朴素贝叶斯中的结尾对sklearn中的朴素贝叶斯进行了简单的介绍. 此处对sklearn中的则对sklearn中的朴素贝叶斯算法进行比较详细介绍.不过手下还 ...

  2. 机器学习 | Sklearn中的朴素贝叶斯全解

    前期文章介绍了朴素贝叶斯理论,掌握理论后如何去使用它,是数据挖掘工作者需要掌握的实操技能,下面来看看Sklearn中都有哪些朴素贝叶斯. 朴素贝叶斯是运用训练数据学习联合概率分布 及 ,然后求得后验概 ...

  3. sklearn中的朴素贝叶斯模型及其应用

    1.使用朴素贝叶斯模型对iris数据集进行花分类 尝试使用3种不同类型的朴素贝叶斯: (1)多项式型 from sklearn import datasets iris=datasets.load_i ...

  4. sklearn中的朴素贝叶斯#01

    文章目录 概述 真正的概率分类器 概述 真正的概率分类器 朴素贝叶斯是一种直接衡量标签和特征之间的概率关系的有监督学习算法,是一种专注分类的算法.朴素贝叶斯的算 法根源就是基于概率论和数理统计的贝叶斯 ...

  5. sklearn中的朴素贝叶斯#02

    文章目录 概率类模型的评估指标 布里尔分数Brier Score 概率类模型的评估指标 布里尔分数Brier Score 概率预测的准确程度被称为"校准程度",是衡量算法预测出的概 ...

  6. sklearn模块之朴素贝叶斯:(二)伯努利模型的实现

      多元Bernoulli模型的朴素贝叶斯分类器适用于离散数据.与MultinomialNB不同之处为: MultinomialNB使用出现次数(频数) BernoulliNB设计用于二进制/布尔特征 ...

  7. 朴素贝叶斯 半朴素贝叶斯_SQL Server中的朴素贝叶斯预测

    朴素贝叶斯 半朴素贝叶斯 In this article, we will walk through Microsoft Naive Bayes algorithm in SQL Server. 在本 ...

  8. 贝叶斯分类器_Sklearn 中的朴素贝叶斯分类器

    (给Python开发者加星标,提升Python技能) 作者:Martin Müller,翻译:github-sisibeloved https://github.com/xitu/gold-miner ...

  9. sklearn分类器:朴素贝叶斯

    朴素贝叶斯 朴素贝叶斯(Naive Bayes)是一个非常简单,但是实用性很强的分类模型.朴素贝叶斯分类器的构造基础是贝叶斯理论. 概率论基础 概率定义为一件事情发生的可能性.事情发生的概率可以 通过 ...

最新文章

  1. Android应用程序键盘(Keyboard)消息处理机制分析(17)
  2. 【干货】掌握这5招,Linux排障不再怕
  3. java web从入门到精通视频_JavaWeb从入门到精通(视频实战版)
  4. Recyclerview删除数据后无法加载下一页数据(或者是漏了一条数据)
  5. android十进制转十六进制算法,十进制转十六进制 代码
  6. 数据链路层的一些总结
  7. java的多重循环实现杨辉三角_java使用for循环输出杨辉三角
  8. Java之super
  9. word字体放大后只显示一半_太实用了!5个Word冷门技巧!第3个你肯定不知道!...
  10. CAS在Java类中的应用
  11. Linux系统编程(28)——线程间同步
  12. android下的jni
  13. php实现先序、中序、后序遍历二叉树
  14. python打开excel执行vba代码_xlwings:操作Excel,通过宏调用Pyhton(VBA调Python)-Go语言中文社区...
  15. 英语一窍不通能学计算机吗,英语一窍不通能学好软件么
  16. 笔记本电脑装android系统安装教程,电脑安装安卓系统教程 三分钟教学换装系统...
  17. vue背景图片资源加载问题
  18. Xcelsius2008系统
  19. OpenGL 4.0 GLSL 采用平行光照模型
  20. Android facebook集成

热门文章

  1. 百信银行开局漂亮,百度和中信会成为智能金融模范夫妻吗?
  2. URI、URL、URN区别
  3. 操作系统 - 基础、硬件基础
  4. 这些年,我穿过的那些队服
  5. Pandas API 文档索引中文翻译版(一)—— Series
  6. 根据贝叶斯定理实现的新闻自动分类
  7. mac 上的开发语言
  8. catkin_make:编译之后devel与build的关系
  9. ISO27001笔记
  10. php获取汉字音调,PHP汉字转拼音 - xiangqian1的个人空间 - OSCHINA - 中文开源技术交流社区...