朴素贝叶斯 - 评估指标(三)

661 阅读27分钟

根据菜菜的课程进行整理,方便记忆理解

代码位置如下:

概率类模型的评估指标

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

布里尔分数Brier Score

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

image.png

其中N是样本数量, pip_i为朴素贝叶斯预测出的概率, oio_i是样本所对应的真实结果,只能取到0或者1,如果事件发生则为1,如果不发生则为0。这个指标衡量了我们的概率距离真实标签结果的差异,其实看起来非常像是均方误差。布里尔分数的范围是从0到1,分数越高则预测结果越差劲,校准程度越差,因此布里尔分数越接近0越好。由于它的本质也是在衡量一种损失,所以在sklearn当中,布里尔得分被命名为brier_score_loss。我们可以从模块metrics中导入这个分数来衡量我们的模型评估结果:

from sklearn.metrics import brier_score_loss
prob.shape 
# (540, 10)

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

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

from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression as LR
np.unique(Ytrain)
# array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

Xtrain.shape
# (1257, 64)

logi = LR(C=1., solver='lbfgs',max_iter=3000,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)
# 0.01143044170765264

svc.decision_function(Xtest)
"""
array([[ 6.04999568,  3.90384836,  2.90874813, ..., -0.25222764,
         8.30994867,  0.77613135],
       [-0.21449942,  9.28421434,  6.08114168, ...,  0.78220746,
         7.09763534,  3.98412779],
       [ 0.77940329,  2.9077248 ,  7.08524427, ...,  6.07973767,
         8.21109292,  5.10728844],
       ...,
       [ 4.96967342,  5.01152151, -0.18621393, ...,  8.13760707,
         6.13234107,  1.8288879 ],
       [ 2.9476444 ,  2.73284495,  6.10781709, ...,  2.99252742,
         8.26833873,  7.085248  ],
       [-0.245529  ,  9.32817444,  4.99213102, ...,  5.98483642,
         7.18223468,  1.87536216]])
"""

#由于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)
# 0.23818950248917947

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

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) #标签为i的时候的贝叶斯下的布里尔分数
    df.loc[i,name[1]] = brier_score_loss(Ytest,logi.predict_proba(Xtest)[:,i],pos_label=i) #标签为i的时候逻辑回归下的布里尔分数
    df.loc[i,name[2]] = brier_score_loss(Ytest,svc_prob[:,i],pos_label=i) #SVC下的布里尔分数
    
df.shape[1]
# 3

for i in range(df.shape[1]):
    plt.plot(range(10),df.iloc[:,i],c=color[i])
plt.legend()
plt.show()

image.png

df

image.png

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

对数似然函数Log Loss

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

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

image.png

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

image.png

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

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

from sklearn.metrics import log_loss
log_loss(Ytest,prob)
# 2.4725653911460683

log_loss(Ytest,logi.predict_proba(Xtest))
# 0.12753470253044222

log_loss(Ytest,svc_prob)
# 1.625556312147472

第一个参数是真实标签,第二个参数是我们预测的概率。如果我们使用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和逻辑回归,贝叶斯的原理简单,根本没有什么可用的参数。但是产出概率的算法有自己的调节方式,就是调节概率的校准程度。校准程度越高,模型对概率的预测越准确,算法在做判断时就越有自信,模型就会更稳定。如果我们追求模型在概率预测上必须尽量贴近真实概率,那我们就可以使用可靠性曲线来调节概率的校准程度。

可靠性曲线Reliability Curve

可靠性曲线(reliability curve),又叫做概率校准曲线(probability calibration curve),可靠性图(reliabilitydiagrams),这是一条以预测概率为横坐标,真实标签为纵坐标的曲线。我们希望预测概率和真实值越接近越好,最好两者相等,因此一个模型/算法的概率校准曲线越靠近对角线越好。校准曲线因此也是我们的模型评估指标之一。和布里尔分数相似,概率校准曲线是对于标签的某一类来说的,因此一类标签就会有一条曲线,或者我们可以使用一个多类标签下的平均来表示一整个模型的概率校准曲线。但通常来说,曲线用于二分类的情况最多,大家如果感兴趣可以自行探索多分类的情况。

根据这个思路,我们来绘制一条曲线试试看。

  • 导入需要的库和模块
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
import pandas as pd
  • 创建数据集
X, y = mc(n_samples=100000,n_features=20 #总共20个特征
          ,n_classes=2 #标签为2分类
          ,n_informative=2 #其中两个代表较多信息
          ,n_redundant=10 #10个都是冗余特征
          ,random_state=42)

X.shape
# (100000, 20)

np.unique(y)
# array([0, 1])

#样本量足够大,因此使用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] #我们的预测概率 - 横坐标
clf_score = gnb.score(Xtest,Ytest)
#Ytest - 我们的真实标签 - 横坐标

#在我们的横纵坐标上,概率是由顺序的(由小到大),为了让图形规整一些,我们要先对预测概率和真实标签按照预测概率进行一个排序,这一点我们通过DataFrame来实现

df = pd.DataFrame({"ytrue":Ytest[:500],"probability":prob_pos[:500]})
#利用字典来创建DataFrame({"列的名称":[列的值]})
df

image.png

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", clf_score))
ax1.set_ylabel("True label")
ax1.set_xlabel("predcited probability")
ax1.set_ylim([-0.05, 1.05])
ax1.legend()
plt.show()

image.png

这个图像看起来非常可怕,完全不止所云!为什么存在这么多上下穿梭的直线?反应快的小伙伴可能很快就发现了,我们是按照预测概率的顺序进行排序的,而预测概率从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("predcited probability")
ax1.set_ylim([-0.05, 1.05])
ax1.legend()
plt.show()

image.png

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

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

image.png

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

image.png

可见,分箱之后样本点的特征被聚合到了一起,曲线明显变得单调且平滑。这种分箱操作本质相当于是一种平滑,在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

#从类calibiration_curve中获取横坐标和纵坐标
trueproba, predproba = calibration_curve(Ytest, prob_pos
                                         ,n_bins=10 #输入希望分箱的个数
                                        )
trueproba.shape
# (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 predcited probability")
ax1.set_ylim([-0.05, 1.05])
ax1.legend()
plt.show()

image.png

  • 不同的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))
    ax1.set_ylabel("True probability for class 1")
    ax1.set_xlabel("Mean predcited probability")
    ax1.set_ylim([-0.05, 1.05])
    ax.legend()
plt.show()

image.png

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

  • 建立更多模型
name = ["GaussianBayes","Logistic","SVC"]

gnb = GaussianNB()
logi = LR(C=1., 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的接口,存在则返回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=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 predcited probability")
ax1.set_ylim([-0.05, 1.05])
ax1.legend()
ax1.set_title('Calibration plots  (reliability curve)')
plt.show()

image.png

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

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

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

预测概率的直方图

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

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的接口,存在则返回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()

image.png

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

概率密度曲线和概率分布直方图

大家也许还记得我们说过,我们是假设样本的概率分布为高斯分布,然后使用高斯的方程来估计连续型变量的概率。怎么现在我们绘制出的概率分布结果中,高斯普斯贝叶斯的概率分布反而完全不是高斯分布了呢?注意,千万不要把概率密度曲线和概率分布直方图混淆。

在称重汉堡的时候所绘制的曲线,是概率密度曲线,横坐标是样本的取值,纵坐标是落在这个样本取值区间中的样本个数,衡量的是每个X的取值区间之内有多少样本。服从高斯分布的是X的取值上的样本分布。现在我们的概率分布直方图,横坐标是概率的取值[0,1],纵坐标是落在这个概率取值范围中的样本的个数,衡量的是每个概率取值区间之内有多少样本。这个分布,是没有任何假设的。

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

校准可靠性曲线

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

在这里,我主要来为大家展示如果使用sklearn中的概率校正类CalibratedClassifierCV来对二分类情况下的数据集进行概率校正

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")

    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)

image.png

从校正朴素贝叶斯的结果来看,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)

image.png

可以看出,对于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:
	Brier:0.1630
	Accuracy:0.8633
SVC+isotonic:
	Brier:0.0999
	Accuracy:0.8639
SVC+sigmoid:
	Brier:0.0987
	Accuracy:0.8634
"""

可以看到,对于SVC来说,两种校正都改善了准确率和布里尔分数。可见,概率校正对于SVC非常有效。这也说明,概率校正对于原本的可靠性曲线是形容Sigmoid形状的曲线的算法比较有效

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