目录

1,sklearn中的贝叶斯分类器

1.1,高斯朴素贝叶斯GaussianNB

1.1.1,认识高斯朴素贝叶斯

1.1.2,参数说明

1.1.3,高斯朴素贝叶斯建模案例

1.1.4,探索高斯朴素贝叶斯擅长的数据集

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

2,概率模型的评估指标

2.2.1,布里尔分数Brier Score

2.2.2,对数似然函数Log Loss

2.2.3,可靠性曲线Reliability Curve

2.2.4,预测概率的直方图

2.2.5,校准可靠性曲线

3,多项式朴素贝叶斯以及其变化

3.1,多项式朴素贝叶斯MultinomialNB

3.2,伯努利朴素贝叶斯BernoulliNB

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

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


上一篇文章我向大家介绍了朴素贝叶斯工作的理论部分,需要看的小伙伴请移步:贝叶斯分类器,接下来,我们基于Sklearn机器学习库来使用以下具体的贝叶斯分类器算法。

1,sklearn中的贝叶斯分类器

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

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

由于贝叶斯是从概率角度进行估计,它所需要的样本量比较少,极端情况下甚至我们可以使用1%的数据作为训练集,依然可以得到很好的拟合效果。当然,如果样本量少于特征数目,贝叶斯的效果就会被削弱。与SVM和随机森林相比,朴素贝叶斯运行速度更快,因为求解P(Y_{i}|X)本质是在每个特征上单独对概率进行计算,然后再求乘积,所以每个特征上的计算可以是独立并且并行的,因此贝叶斯的计算速度比较快。不过相对的,贝叶斯的运行效果不是那么好,所以贝叶斯的接口调用的predict_proba其实也不是总指向真正的分类结果,这一点需要注意。

1.1,高斯朴素贝叶斯GaussianNB

1.1.1,认识高斯朴素贝叶斯

class sklearn.naive_bayes.GaussianNB (priors=None, var_smoothing=1e-09)

高斯朴素贝叶斯,通过假设P(X_{i}|Y)是服从高斯分布(也就是正态分布),来估计每个特征下每个类别上的条件概率。对于每个特征下的取值,高斯朴素贝叶斯有如下公式:p(x_{i}|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}}),对于任意一个Y的取值,贝叶斯都以求解最大化P(X_{i}|Y)的为目标,这样我们才能够比较在不同标签下我们的样本究竟更靠近哪一个取值。以最大化P(X_{i}|Y)为目标,高斯朴素贝叶斯会为我们求解公式中的参数\sigma _{y}\mu _{y}。求解出参数后,带入一个x_{i}的值,就能够得到一个P(X_{i}|Y)的概率取值。

1.1.2,参数说明

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

但在实例化的时候,我们不需要对高斯朴素贝叶斯类输入任何的参数,调用的接口也全部是sklearn中比较标准的一些搭配,可以说是一个非常轻量级的类,操作非常容易。但过于简单也意味着贝叶斯没有太多的参数可以调整,因此贝叶斯算法的成长空间并不是太大,如果贝叶斯算法的效果不是太理想,我们一般都会考虑换模型。

1.1.3,高斯朴素贝叶斯建模案例

  • 查看开发工具版本

%%cmd
pip install watermark #这是一个魔法命令
# 魔法命令必须写在一个cell的第一行

#在这里必须分开cell,魔法命令必须是一个cell的第一部分内容
#注意load_ext这个命令只能够执行一次,再执行就会报错,要求用reload命令
%load_ext watermark

%watermark -a "ruirui" -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_split

# 导入数据集
digits=load_digits()

X=digits.data
y=digits.target

# 划分测试集和训练数据集,划分后,训练数据1257个样本,测试数据集540个样本
xtrain,xtest,ytrain,ytest=train_test_split(X,y,test_size=0.3,random_state=0)

# 查看标签种类
np.unique(ytrain)

# 实例化模型并且训练模型,其中fit()过程就是在计算概率的过程
gnb=GaussianNB().fit(xtrain,ytrain)

# score()接口对于我们的分类型算法,返回预测的精确性,也就是accuracy,使用测试数据集测试
acc_score=gnb.score(xtest,ytest)

# 返回所有样本对应的类别,这里的样本标签是用我们下面得到的概率中,
# 选取每一个样本中概率最大的作为此样本的标签
y_pred=gnb.predict(xtest)

# 查看我们的概率结果
yprob=gnb.predict_proba(xtest)
# 可以看到,返回的结果是540*10的二维矩阵,其中应为分类有10个,所以一共返回10列概率
# 取其中概率最大的哪一个分类作为最后的分类结果,并且每一行的概率之和是1

#注意,ROC曲线是不能用于多分类的。多分类状况下最佳的模型评估指标是混淆矩阵和整体的准确度
from sklearn.metrics import confusion_matrix as CM
CM(ytest,y_pred)

1.1.4,探索高斯朴素贝叶斯擅长的数据集

我们还是使用常用的三种数据分布:月亮型,环形数据以及二分型数据来看看高斯朴素贝叶斯的情况。

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,ComplementNB

h = .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
]
# 创建画布
figure = plt.figure(figsize=(6, 9))
i = 1

for ds_index, ds in enumerate(datasets):
    X, y = ds
#     标准化数据集
    X = StandardScaler().fit_transform(X)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4,random_state=42)
#     对画布画网格线
    x1_min, x1_max = X[:, 0].min() - .5, X[:, 0].max() + .5
    x2_min, x2_max = X[:, 1].min() - .5, X[:, 1].max() + .5
    array1,array2 = np.meshgrid(np.arange(x1_min, x1_max, 0.2),
    np.arange(x2_min, x2_max, 0.2))
    cm = plt.cm.RdBu
    cm_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 += 1
    ax = 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=.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() - .3, array2.min() + .3, ('{:.1f}%'.format(score*100)),
    size=15, horizontalalignment='right')
    i += 1
plt.tight_layout()
plt.show()

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

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

我们已经了解高斯朴素贝叶斯属于分类效果不算顶尖的模型,但我们依然好奇,这个算法在拟合的时候还有哪些特性呢?比如说我们了解,决策树是天生过拟合的模型,而支持向量机是不调参数的情况下就非常接近极限的模型。我们希望通过绘制高斯朴素贝叶斯的学习曲线与分类树,随机森林和支持向量机的学习曲线的对比,来探索高斯朴素贝叶斯算法在拟合上的性质。过去绘制学习曲线都是以算法类的某个参数的取值为横坐标,今天我们来使用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
  • 交叉验证的模式
datas=load_digits()
X=datas.data
y=datas.target
clf=GaussianNB()
# 交叉验证的模式
# 这里使用的训练集数据比例是默认的,5次
cv = ShuffleSplit(n_splits=50# 标示把数据分成多少份
                  , test_size=0.2# 其中有20%数据作为测试集
                  , random_state=0)# 在交叉验证的时候进行的随机抽样的模式
# 把数据分50分,其中20%作为测试集,50此测试,每一次都用20%的数据集作为测试
learning_curve(clf# 标示分类器
               , X, y# 特征矩阵和标签
                ,cv=cv# 设置交叉验证的模式
               ,n_jobs=4)# 每一次运行可以使用的线程数目

train_sizes, train_scores, test_scores = learning_curve(clf, X, y
    ,cv=cv,n_jobs=3)

train_sizes
# 训练集里面数据的比例,array([ 143,  467,  790, 1113, 1437])
# 分成5次训练,训练数据样本逐渐增加,每一次划分,都把数据集划分为50分
train_scores.shape #(5, 50)

test_scores.shape# (5, 50)

# 画曲线的函数
# 第一步:找出每一个图的横纵坐标

def plot_learning_curve(estimator,title, X, y,# estimator标示使用的分类器
    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:# 设置y坐标的量纲形式一样,也就是把多个子图的y坐标设置为一样
        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 ax
  • 绘制曲线
title = ["Naive Bayes","DecisionTree","SVM, RBF kernel","RandomForest","Logistic"]
model = [GaussianNB(),DTC(),SVC(gamma=0.001)
,RFC(n_estimators=50),LR(C=.1,solver="lbfgs")]# 实例化我们的模型算法
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()
# 图像的横坐标标示训练样本的个数
# 如果数据集的维度很高,我们就使用贝叶斯算法,对于一般的数据集我们首选逻辑回归

我们首先返回的结果是各个算法的运行时间。可以看到,决策树和贝叶斯不相伯仲(如果你没有发现这个结果,那么可以多运行几次,你会发现贝叶斯和决策树的运行时间逐渐变得差不多)。决策树之所以能够运行非常快速是因为sklearn中的分类树在选择特征时有所“偷懒”,没有计算全部特征的信息熵而是随机选择了一部分特征来进行计算,因此速度快可以理解,但我们知道决策树的运算效率随着样本量逐渐增大会越来越慢,但朴素贝叶斯却可以在很少的样本上获得不错的结果,因此我们可以预料,随着样本量的逐渐增大贝叶斯会逐渐变得比决策树更快。朴素贝叶斯计算速度远远胜过SVM,随机森林这样复杂的模型,逻辑回归的运行受到最大迭代次数的强烈影响和输入数据的影响(逻辑回归一般在线性数据上运行都比较快,但在这里应该是受到了稀疏矩阵的影响)。因此在运算时间上,朴素贝叶斯
还是十分有优势的。

紧接着,我们来看一下每个算法在训练集上的拟合。手写数字数据集是一个较为简单的数据集,决策树,森林,SVC和逻辑回归都成功拟合了100%的准确率,但贝叶斯的最高训练准确率都没有超过95%,这也应证了我们最开始说的,朴素贝叶斯的分类效果其实不如其他分类器,贝叶斯天生学习能力比较弱。并且我们注意到,随着训练样本量的逐渐增大,其他模型的训练拟合都保持在100%的水平,但贝叶斯的训练准确率却逐渐下降,这证明样本量越大,贝叶斯需要学习的东西越多,对训练集的拟合程度也越来越差。反而比较少量的样本可以让贝叶斯有较高的训练准确率。

再来看看过拟合问题。首先一眼看到,所有模型在样本量很少的时候都是出于过拟合状态的(训练集上表现好,测试集上表现糟糕),但随着样本的逐渐增多,过拟合问题都逐渐消失了,不过每个模型的处理手段不同。比较强大的分类器们,比如SVM,随机森林和逻辑归,是依靠快速升高模型在测试集上的表现来减轻过拟合问题。相对的,决策树虽然也是通过提高模型在测试集上的表现来减轻过拟合,但随着训练样本的增加,模型在测试集上的表现善生却非常缓慢。朴素贝叶斯独树一帜,是依赖训练集上的准确率下降,测试集上的准确率上升来逐渐解决过拟合问题。

接下来,看看每个算法在测试集上的拟合结果,即泛化误差的大小。随着训练样本数量的上升,所有模型的测试表现都上升了,但贝叶斯和决策树在测试集上的表现远远不如SVM,随机森林和逻辑回归。SVM在训练数据量增大到1500个样本左右的时候,测试集上的表现已经非常接近100%,而随机森林和逻辑回归的表现也在95%以上,而决策树和朴素贝叶斯还徘徊在85%左右。但这两个模型所面临的情况十分不同:决策树虽然测试结果不高,但是却依然具有潜力,因为它的过拟合现象非常严重,我们可以通过减枝来让决策树的测试结果逼近训练结果。然而贝叶斯的过拟合现象在训练样本达到1500左右的时候已经几乎不存在了,训练集上的分数和测试集上的分数非常接近,只有在非常少的时候测试集上的分数才能够比训练集上的结果更高,所以我们基本可以判断,85%左右就是贝叶斯在这个数据集上的极限了。可以预测到,如果我们进行调参,那决策树最后应该可以达到90%左右的预测准确率,但贝叶斯却几乎没有潜力了。

在这个对比之下,我们可以看出:贝叶斯是速度很快,但分类效果一般,并且初次训练之后的结果就很接近算法极限的算法,几乎没有调参的余地。也就是说,如果我们追求对概率的预测,并且希望越准确越好,那我们应该先选择逻辑回归。如果数据十分复杂,或者是稀疏矩阵,那我们坚定地使用贝叶斯。如果我们分类的目标不是要追求对概率的预测,那我们完全可以先试试看高斯朴素贝叶斯的效果(反正它运算很快速,还不需要太多的样本),如果效果很不错,我们就很幸运地得到了一个表现优秀又快速的模型。如果我们没有得到比较好的结果,那我们完全可以选择再更换成更加复杂的模型。

2,概率模型的评估指标

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

2.2.1,布里尔分数Brier Score

概率预测的准确程度被称为“校准程度”,是衡量算法预测出的概率和真实结果的差异的一种方式。一种比较常用的指标叫做布里尔分数,它被计算为是概率预测相对于测试样本的均方误差,表示为:

Brier Score =\frac{1}{N}\sum_{i=1}^{n}(p_{i}-o_{i})^{2},其中N是样本数量,p_{i}为朴素贝叶斯预测出的概率, o_{i}是样本所对应的真实结果,只能取到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中的索引一致,就可以查看这个类别下的布里尔分数是多少

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

brier_score_loss(Ytest,prob[:,8],pos_label=8)

# 创建模型并且拟合数据
logi = LR(C=1., solver='lbfgs',max_iter=5000,multi_class="auto").fit(xtrain,ytrain)
svc = SVC(kernel = "linear",gamma=1).fit(xtrain,ytrain)

# 逻辑回归的布里尔分数
brier_score_loss(ytest,logi.predict_proba(xtest)[:,1],pos_label=1)

#由于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,svc_prob[:,1],pos_label=1)

# 数据可视化
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,prob[:,i],pos_label=i)
    df.loc[i,name[1]] = brier_score_loss(Ytest,logi.predict_proba(Xtest)
    [:,i],pos_label=i)
    df.loc[i,name[2]] = brier_score_loss(Ytest,svc_prob[:,i],pos_label=i)
for i in range(df.shape[1]):
    plt.plot(range(10),df.iloc[:,i],c=color[i])
plt.legend()
plt.show()
df

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

2.2.2,对数似然函数Log Loss

另一种常用的概率损失衡量是对数损失(log_loss),又叫做对数似然,逻辑损失或者交叉熵损失,它是多元逻辑回归以及一些拓展算法,比如神经网络中使用的损失函数。它被定义为,对于一个给定的概率分类器,在预测概率为条件的情况下,真实概率发生的可能性的负对数(如何得到这个损失函数的证明过程和推导过程在逻辑回归的章节中有完整得呈现)。由于是损失,因此对数似然函数的取值越小,则证明概率估计越准确,模型越理想。值得注意得是,对数损失只能用于评估分类型模型。

对于一个样本,如果样本的真实标签Y_{true}在{0,1}中取值,并且这个样本在类别1下的概率估计为y_{pred},则这个样本所对应的对数损失是:

和我们逻辑回归的损失函数一模一样:

只不过在逻辑回归的损失函数中,我们的真实标签是由y_{i}表示,预测值(概率估计)是由y_{\theta }(x_{i})来表示,仅仅是表示方式的不同。注意,这里的log表示以e为底的自然对数。

在sklearn中,我们可以从metrics模块中导入我们的对数似然函数:

# 似然对数损失
from sklearn.metrics import log_loss
# 似然损失是可以衡量多分类的

log_loss(ytest,yprob)

# 第一个参数是真实的标签,第二个参数是概率值
log_loss(ytest,logi.predict_proba(xtest))

log_loss(ytest,svc_prob)

第一个参数是真实标签,第二个参数是我们预测的概率。如果我们使用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的概率,否则的话对数似然就会取到极限值(考虑以为底的自然对数在取到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)
xtrain,xtest,ytrain,ytest=train_test_split(X,y,test_size=0.99,random_state=0)
  • 建立模型
gns=GaussianNB()
gns=gns.fit(xtrain,ytrain)
# 导出分类的结果
y_pred=gns.predict(xtest)
# 导出预测概率
y_prob=gns.predict_proba(xtest)[:,1]
  • 绘制图像
y_prob.shape
# 我们的预测概率,作为横坐标
# ytest 我们的真实标签,作为纵坐标

import pandas as pd 
#在我们的横纵表坐标上,概率是由顺序的(由小到大),为了让图形规整一些,我们要先对预测概率和真实标签按照预测
# 概率进行一个排序,这一点我们通过DataFrame来实现
df=pd.DataFrame({"ytrue":ytest[:500],"probability":y_prob[:500]})

# 对df中的数据进行排序
df=df.sort_values(by="probability")
# 现在恢复我们的索引
df.index=range(df.shape[0])

#紧接着我们就可以画图了
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", 3))
ax1.set_ylabel("True label")
ax1.set_xlabel("predcited probability")
ax1.set_ylim([-0.05, 1.05])
ax1.legend()
plt.show()

这个图像看起来非常可怕,完全不止所云!为什么存在这么多上下穿梭的直线?反应快的小伙伴可能很快就发现了,我们是按照预测概率的顺序进行排序的,而预测概率从0开始到1的过程中,真实取值不断在0和1之间变化,而我们是绘制折线图,因此无数个纵坐标分布在0和1的被链接起来了,所以看起来如此混乱。

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

来看下面这张表,这是一组数据不分箱时表现出来的图像:

再来看看分箱之后的图像:

可见,分箱之后样本点的特征被聚合到了一起,曲线明显变得单调且平滑。这种分箱操作本质相当于是一种平滑,在sklearn中,这样的做法可以通过绘制可靠性曲线的类calibration_curve来实现。和ROC曲线类似,类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

# 分成10箱,ytest代表是真实标签,y_prob标示返回的概率
trueproba,predproba=calibration_curve(ytest,y_prob,n_bins=10)
# predproba代表每一个箱子中概率的均值
# trueproba代表每一个箱子中少数类的占比,均值
trueproba
# 我们要求分10个箱子,所以会返回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", 10))# 10代表是分10个箱子
ax1.set_ylabel("True label")
ax1.set_xlabel("predcited probability")
ax1.set_ylim([-0.05, 1.05])
ax1.legend()
plt.show()

在这里,我们如何确定一个合适的箱子个数呢?可以通过学习曲线,改变箱子的数量。

# 探索不同箱子的个数对模型的影响
fig,axes=plt.subplots(1,3,figsize=(18,4))
for ind,i in enumerate([3,10,100]):
    ax1=axes[ind]
    trueproba,predproba=calibration_curve(ytest,y_prob,n_bins=i)
    ax1.plot([0, 1], [0, 1], "k:", label="Perfectly calibrated") #得做一条对角线来对比呀
    ax1.plot(predproba,trueproba,"s-",label="%s (%1.3f)" % ("Bayes", i))
    ax1.set_ylabel("True label")
    ax1.set_xlabel("predcited probability")
    ax1.set_ylim([-0.05, 1.05])
    ax1.legend()
plt.show()
# 当n=100的时候,明显是过拟合
# 我们绘制出的图像,越接近y=x这条直线,越好

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

逻辑回归的概率估计是最接近完美的概率校准曲线,所以逻辑虎归的效果最完美。相对的,高斯朴素贝叶斯和支持向量机分类器的结果都比较糟糕。支持向量机呈现类似于sigmoid函数的形状,而高斯朴素贝叶斯呈现和Sigmoid函数相反的形状。


对于贝叶斯,如果概率校准曲线呈现sigmoid函数的镜像的情况,则说明数据集中的特征不是相互条件独立的。贝叶斯原理中的”朴素“原则:特征相互条件独立原则被违反了(这其实是我们自己的设定,我们设定了10个冗余特征,这些特征就是噪音,他们之间不可能完全独立),因此贝叶斯的表现不够好。


而支持向量机的概率校准曲线效果其实是典型的置信度不足的分类器(under-confident classifier)的表现:大量的样本点集中在决策边界的附近,因此许多样本点的置信度靠近0.5左右,即便决策边界能够将样本点判断正确,模型本身对这个结果也不是非常确信的。相对的,离决策边界很远的点的置信度就会很高,因为它很大可能性上不会被判断错误。支持向量机在面对混合度较高的数据的时候,有着天生的置信度不足的缺点。

2.2.4,预测概率的直方图

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

# 直方图的操作
fig,ax2=plt.subplots(figsize=(8,6))
name = ["GaussianBayes","Logistic","SVC"]
gnb = GaussianNB()
logi = LR(C=1., solver='lbfgs',max_iter=3000,multi_class="auto")
svc = SVC(kernel = "linear",gamma=1)
for clf, name_ in zip([gnb,logi,svc],name):
    clf.fit(xtrain,ytrain)
    y_pred = clf.predict(xtest)
#hasattr(obj,name):查看一个类obj中是否存在名字为name的接口,存在则返回True
    if hasattr(clf, "predict_proba"):
        prob_pos = clf.predict_proba(xtest)[:,1]
    else: # use decision function
        prob_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的取值上的样本分布。

现在我们的概率分布直方图,横坐标是概率的取值[0,1],纵坐标是落在这个概率取值范围中的样本的个数,衡量的是每个概率取值区间之内有多少样本。这个分布,是没有任何假设的。

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

2.2.5,校准可靠性曲线

等近似回归有两种回归可以使用,一种是基于Platt的Sigmoid模型的参数校准方法,一种是基于等渗回归(isotoniccalibration)的非参数的校准方法。概率校准应该发生在测试集上,必须是模型未曾见过的数据。在数学上,使用这两种方式来对概率进行校准的原理十分复杂,而此过程我们在sklearn中无法进行干涉,大家不必过于去深究。

class 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",则假设已经在分类器上拟合完毕数据。在这种模式下,使用者必须手动确定用来拟合分类器的数据与即将倍校准的数据没有交集
  • method:进行概率校准的方法,可输入"sigmoid"或者"isotonic"
    • 输入'sigmoid',使用基于Platt的Sigmoid模型来进行校准
    • 输入'isotonic',使用等渗回归来进行校准

当校准的样本量太少(比如,小于等于1000个测试样本)的时候,不建议使用等渗回归,因为它倾向于过拟合。样本量过少时请使用sigmoids,即Platt校准。

  • 函数包装

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

def plot_calib(models,name,Xtrain,Xtest,Ytrain,Ytest,n_bins=10):
    import matplotlib.pyplot as plt
    from sklearn.metrics import brier_score_loss
    from sklearn.calibration import calibration_curve
    fig, (ax1, ax2) = plt.subplots(1, 2,figsize=(20,6))
    ax1.plot([0, 1], [0, 1], "k:", label="Perfectly calibrated")# 画出y=x的直线
    for clf, name_ in zip(models,name):
        clf.fit(xtrain,ytrain)
        y_pred = clf.predict(Xtest)
    #hasattr(obj,name):查看一个类obj中是否存在名字为name的接口,存在则返回True
        if hasattr(clf, "predict_proba"):
            prob_pos = clf.predict_proba(Xtest)[:,1]
        else: # use decision function
            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())
        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 probablity")
ax1.set_ylabel("True probability for class 1")
ax1.set_xlabel("Mean predcited 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)
brier_score_loss(Ytest,gnb.predict_proba(Xtest)[:,1],pos_label = 1)
gnbisotonic = CalibratedClassifierCV(gnb, cv=2, method='isotonic').fit(Xtrain,Ytrain)
gnbisotonic.score(Xtest,Ytest)
brier_score_loss(Ytest,gnbisotonic.predict_proba(Xtest)[:,1],pos_label = 1)

可以看出,校准概率后,布里尔分数明显变小了,但整体的准确率却略有下降,这证明算法在校准之后,尽管对概率的预测更准确了,但模型的判断力略有降低。来思考一下:布里尔分数衡量模型概率预测的准确率,布里尔分数越低,代表模型的概率越接近真实概率,当进行概率校准后,本来标签是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","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来说,两种校正都改善了准确率和布里尔分数。可见,概率校正对于SVC非常有效。这也说明,概率校正对于原本的可靠性曲线是形容Sigmoid形状的曲线的算法比较有效。

在现实中,我们可以选择调节模型的方向,我们不一定要追求最高的准确率或者追求概率拟合最好,我们可以根据自己的需求来调整模型。当然,对于概率类模型来说,由于可以调节的参数甚少,所以我们更倾向于追求概率拟合,并使用概率校准的方式来调节模型。如果你的确希望追求更高的准确率和Recall,可以考虑使用天生就非常准确的概率类模型逻辑回归,也可以考虑使用除了概率校准之外还有很多其他参数可调的支持向量机分类器。

3,多项式朴素贝叶斯以及其变化

3.1,多项式朴素贝叶斯MultinomialNB

多项式贝叶斯可能是除了高斯之外,最为人所知的贝叶斯算法了。它也是基于原始的贝叶斯理论,但假设概率分布是服从一个简单多项式分布。多项式分布来源于统计学中的多项式实验,这种实验可以具体解释为:实验包括n次重复试验,每项试验都有不同的可能结果。在任何给定的试验中,特定结果发生的概率是不变的

  • 多项式分布擅长的是分类型变量,在其原理假设中,p(x_{i}|y)的概率是离散的,并且不同x_{i}下的p(x_{i}|y)相互独立,互不影响。虽然sklearn中的多项式分布也可以处理连续型变量,但现实中,如果我们真的想要处理连续型变量,我们应当使用高斯朴素贝叶斯。
  • 多项式实验中的实验结果都很具体,它所涉及的特征往往是次数,频率,计数,出现与否这样的概念,这些概念都是离散的正整数,因此sklearn中的多项式朴素贝叶斯不接受负值的输入。

由于这样的特性,多项式朴素贝叶斯的特征矩阵经常是稀疏矩阵(不一定总是稀疏矩阵),并且它经常被用于文本分类。我们可以使用著名的TF-IDF向量技术,也可以使用常见并且简单的单词计数向量手段与贝叶斯配合使用。这两种手段都属于常见的文本特征提取的方法,可以很简单地通过sklearn来实现,

从数学的角度来看,在一种标签类别Y=c下,我们有一组分别对应特征的参数向量\theta _{c}=(\theta _{c1},\theta _{c2}......\theta _{cn}),其中n表示特征的总数。一个\theta _{ci}表示这个标签类别下的第i个特征所对应的参数。这个参数被我们定义为:

记作P(X_{i}|y=c),表示当Y=c这个条件固定的时候,一组样本在X_{I}这个特征上的取值被取到的概率。注意,我们在高斯朴素贝叶斯中求解的概率p(x_{i}|Y)是对于一个样本来说,而我们现在求解的P(X_{i}|Y=c)是对于一个特征来说的概率。

对于一个在标签类别Y=c下,结构为(m, n)的特征矩阵来说,我们有:

其中每个x_{ji}都是特征x_{i}发生的次数。基于这些理解,我们通过平滑后的最大似然估计来求解参数\theta _{y}

对于每个特征,\sum_{y_{j}=c}^{x}x_{ij}是特征X_{i}下所有标签为c的样本的特征取值之和,其实就是特征矩阵中每一列的和。\sum_{i=1}^{n}\sum_{y_{j}=c}^{x}x_{ij}是所有标签类别为c的样本上,所有特征的取值之和,其实就是特征矩阵X_{y}中所有元素的和。\alpha被称为平滑系数,我们令\sigma > 0来防止训练数据中出现过的一些词汇没有出现在测试集中导致的0概率,以避免让参数\theta为0的情况。如果我们将\alpha设置为1,则这个平滑叫做拉普拉斯平滑,如果\alpha小于1,则我们把它叫做利德斯通平滑。两种平滑都属于自然语言处理中比较常用的用来平滑分类数据的统计手段。

在sklearn中,用来执行多项式朴素贝叶斯的类MultinomialNB包含如下的参数和属性:

class sklearn.naive_bayes.MultinomialNB (alpha=1.0, fit_prior=True, class_prior=None)
参数
alpha : 浮点数, 可不填 (默认为1.0)
拉普拉斯或利德斯通平滑的参数,如果设置为0则表示完全没有平滑选项。但是需要注意的是,平滑相当于人
为给概率加上一些噪音,因此\alpha设置得越大,多项式朴素贝叶斯的精确性会越低(虽然影响不是非常大),布里
尔分数也会逐渐升高。
fit_prior : 布尔值, 可不填 (默认为True)
是否学习先验概率p(y=c)。如果设置为false,则不使用先验概率,而使用统一先验概率(uniform
prior),即认为每个标签类出现的概率是\frac{1}{n_classes}
class_prior:形似数组的结构,结构为(n_classes, ),可不填(默认为None)
类的先验概率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
centers=[[0,0],[2,2]]# 设置数据集的中心
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,random_state=0,test_size=0.3)

# 对数据进行归一化处理,防止出现负数
mms=MinMaxScaler().fit(xtrain)
xtrain_=mms.transform(xtrain)
xtest_=mms.transform(xtest)

# 现在实例化模型,进行拟合
# 训练的时候使用归一化之后的特征矩阵
mnb=MultinomialNB().fit(xtrain_,ytrain)

# 获取每一个特征的先验概率
# 返回每一个标签类对应的先验概率
mnb.class_log_prior_

mnb.class_log_prior_.shape
# 形状永远等于标签中所带的类别数量

# 返回每一个固定类别标签下的每一个特征的对数概率log(p(xi|y))
mnb.feature_log_prob_
# 矩阵形状是2*2,因为有2个特征,2个类别

# 在fit时每一个标签类别下包含的样本数
# 当fit时候接口中sample_weight被设置的时候,该接口返回的值也会受到加权的影响
mnb.class_count_
# 返回和我们标签类别一样的结构

mnb.predict(xtest)
# 返回我们的预测结果

# 返回每一个样本在每一个标签下面的取值概率
mnb.predict_proba(xtest)

# 返回我们的准确度
mnb.score(xtest,ytest)

brier_score_loss(ytest,mnb.predict_proba(xtest)[:,1],pos_label=1)
# 返回我们的布里尔分数
  • 把连续性变量转换为离散型变量
#来试试看把Xtiain转换成分类型数据吧
#注意我们的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)
brier_score_loss(Ytest,mnb.predict_proba(Xtest_)[:,1],pos_label=1)

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

3.2,伯努利朴素贝叶斯BernoulliNB

多项式朴素贝叶斯可同时处理二项分布(抛硬币)和多项分布(掷骰子),其中二项分布又叫做伯努利分布,它是一种现实中常见,并且拥有很多优越数学性质的分布。因此,既然有着多项式朴素贝叶斯,我们自然也就又专门用来处理二项分布的朴素贝叶斯:伯努利朴素贝叶斯。

伯努利贝叶斯类BernoulliN假设数据服从多元伯努利分布,并在此基础上应用朴素贝叶斯的训练和分类过程。多元伯努利分布简单来说,就是数据集中可以存在多个特征,但每个特征都是二分类的,可以以布尔变量表示,也可以表示为{0,1}或者{-1,1}等任意二分类组合。因此,这个类要求将样本转换为二分类特征向量,如果数据本身不是二分类的,那可以使用类中专门用来二值化的参数binarize来改变数据。

伯努利朴素贝叶斯与多项式朴素贝叶斯非常相似,都常用于处理文本分类数据。但由于伯努利朴素贝叶斯是处理二项分布,所以它更加在意的是“存在与否”,而不是“出现多少次”这样的次数或频率,这是伯努利贝叶斯与多项式贝叶斯的根本性不同。

class sklearn.naive_bayes.BernoulliNB (alpha=1.0, binarize=0.0, fit_prior=True, class_prior=None)

参数说明:

alpha : 浮点数, 可不填 (默认为1.0)
拉普拉斯或利德斯通平滑的参数,如果设置为0则表示完全没有平滑选项。但是需要注意的是,平滑相当于人
为给概率加上一些噪音,因此设置得越大,多项式朴素贝叶斯的精确性会越低(虽然影响不是非常大),布里
尔分数也会逐渐升高。
binarize : 浮点数或None,可不填,默认为0
将特征二值化的阈值,如果设定为None,则会假定说特征已经被二值化完毕
fit_prior : 布尔值, 可不填 (默认为True)
是否学习先验概率。如果设置为false,则不使用先验概率,而使用统一先验概率(uniform
prior),即认为每个标签类出现的概率
class_prior:形似数组的结构,结构为(n_classes, ),可不填(默认为None)
类的先验概率。如果没有给出具体的先验概率则自动根据数据来进行计算。
# 使用伯努利贝叶斯或者多项式贝叶斯,一定要注意我们的数据结构
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

# 处理二项分布的朴素贝叶斯----伯努利朴素贝叶斯
from sklearn.naive_bayes import BernoulliNB
# 接下来进行数据的归一化
mms=MinMaxScaler().fit(xtrain)
xtrain__=mms.transform(xtrain)
xtest__=mms.transform(xtest)
# 先将数值全部进行归一化后,然后设置一个阈值,将数值进行二值化
# 先不设置二值化
bnl=BernoulliNB().fit(xtrain__,ytrain)
bnl.score(xtest__,ytest)
brier_score_loss(ytest,bnl.predict_proba(xtest__)[:,1],pos_label=1)
# 现在设置二值化的阈值
bnl=BernoulliNB(binarize=0.5).fit(xtrain__,ytrain)
bnl.score(xtest__,ytest)

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

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

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

# 查看所有贝叶斯样本不均衡的表现
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)))
# 在这里解释一下:recall返佣的是少数类判断正确的比例

从结果上来看,多项式朴素贝叶斯判断出了所有的多数类样本,但放弃了全部的少数类样本,受到样本不均衡问题影响最严重。高斯比多项式在少数类的判断上更加成功一些,至少得到了43.8%的recall。伯努利贝叶斯虽然整体的准确度和布里尔分数不如多项式和高斯朴素贝叶斯和,但至少成功捕捉出了77.1%的少数类。可见,伯努利贝叶斯最能够忍受样本不均衡问题。

可是,伯努利贝叶斯只能用于处理二项分布数据,在现实中,强行将所有的数据都二值化不会永远得到好结果,在我们有多个特征的时候,我们更需要一个个去判断究竟二值化的阈值该取多少才能够让算法的效果优秀。这样做无疑是非常低效的。那如果我们的目标是捕捉少数类,我们应该怎么办呢?高斯朴素贝叶斯的效果虽然比多项式好,但是也没有好到可以用来帮助我们捕捉少数类的程度——43.8%,还不如抛硬币的结果。因此,孜孜不倦的统计学家们改进了朴素贝叶斯算法,修正了包括无法处理样本不平衡在内的传统朴素贝叶斯的众多缺点,得到了新兴贝叶斯算法:补集朴素贝叶斯。

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

补集朴素贝叶斯(complement naive Bayes,CNB)算法是标准多项式朴素贝叶斯算法的改进。CNB的发明小组创造出CNB的初衷是为了解决贝叶斯中的“朴素”假设带来的各种问题,他们希望能够创造出数学方法以逃避朴素贝叶斯中的朴素假设,让算法能够不去关心所有特征之间是否是条件独立的。以此为基础,他们创造出了能够解决样本不平衡问题,并且能够一定程度上忽略朴素假设的补集朴素贝叶斯。在实验中,CNB的参数估计已经被证明比普通多项式朴素贝叶斯更稳定,并且它特别适合于样本不平衡的数据集。有时候,CNB在文本分类任务上的表现有时能够优于多项式朴素贝叶斯,因此现在补集朴素贝叶斯也开始逐渐流行。

简单来说,CNB使用来自每个标签类别的补集的概率,并以此来计算每个特征的权重。

其中表示每个样本, 表示在样本上对于特征的下的取值,在文本分类中通常是计数的值或者是TF-IDF值。是像标准多项式朴素贝叶斯中一样的平滑系数。可以看出,这个看似复杂的公式其实很简单, 其实指的就是,一个特征下,所有标签类别不等于c值的样本的特征取值之和。而其实就是,所有特征下,素有标签类别不等于c值得样本的特征取值之和。其实就是多项式分布的逆向思路。

对于这个概率,我们对它取对数后得到权重。我们还可以选择除以它的L2范式,以解决了在多项式分布中,特征取值比较多的样本(比如说比较长的文档)支配参数估计的情况。很多时候我们的特征矩阵是稀疏矩阵,但也不排除在有一些随机事件中,可以一次在两个特征中取值的情况。如果一个样本下的很多个随机事件可以同时发生,并且互不干涉,那么这个样本上可能有很多个特征下都有取值——文本分类中就有很多这样的情况。

基于这个权重,补充朴素贝叶斯中一个样本的预测规则为:

即我们求解出的最小补集概率所对应的标签就是样本的标签,因为的概率越小,则意味着的概率越大,所以样本属于标签类别c。
在sklearn中,补集朴素贝叶斯由类ComplementNB完成,它包含的参数和多项式贝叶斯也非常相似:

class sklearn.naive_bayes.ComplementNB (alpha=1.0, fit_prior=True, class_prior=None, norm=False)
alpha : 浮点数, 可不填 (默认为1.0)
拉普拉斯或利德斯通平滑的参数,如果设置为0则表示完全没有平滑选项。但是需要注意的是,平滑相当于人
为给概率加上一些噪音,因此设置得越大,多项式朴素贝叶斯的精确性会越低(虽然影响不是非常大),布里
尔分数也会逐渐升高。
norm : 布尔值,可不填,默认False
在计算权重的时候是否适用L2范式来规范权重的大小。默认不进行规范,即不跟从补集朴素贝叶斯算法的全部
内容,如果希望进行规范,请设置为True。
fit_prior : 布尔值, 可不填 (默认为True)
是否学习先验概率。如果设置为false,则不使用先验概率,而使用统一先验概率(uniform
prior),即认为每个标签类出现的概率是。
class_prior:形似数组的结构,结构为(n_classes, ),可不填(默认为None)
类的先验概率。如果没有给出具体的先验概率则自动根据数据来进行计算。
from sklearn.naive_bayes import ComplementNB
from time import time
import datetime
name = ["Multinomial","Gaussian","Bernoulli","Complement"]
models = [MultinomialNB(),GaussianNB(),BernoulliNB(),ComplementNB()]
for name,clf in zip(name,models):
times = time()
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)))
print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))

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

参考资料:

[1] https://www.bilibili.com/video/BV1WJ411k7L3?p=228

 

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐