目录
1. 高斯朴素贝叶斯GaussianNB
1.1 认识高斯朴素贝叶斯
class sklearn.naive_bayes.GaussianNB(priors=None,var_smoothing=1e-09) 高斯朴素贝叶斯,通过假设是服从高斯分布(也就是正态分布)来估计每个特征每个类别的条件概率。对于每个特征下的取值,高斯朴素贝叶斯有如下公式:
对于任意一个Y的取值,贝叶斯都以求解最大化的为目标,这样才能够比较在不同标签下样本究竟更靠近哪一个取值。以最大化为目标,高斯朴素贝叶斯会为我们求解公式中的,求解出参数后,带入一个的值,就能够得到一个的概率取值。这个类包括两个参数,但在实例化的时候,我们并不需要对高斯朴素贝叶斯类输入任何的参数,可以说是一个非常轻量级的类。所以贝叶斯也没有太多的参数可以调整。
# 导入需要的库和数据
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,y=digits.data,digits.target
xtrain,xtest,ytrain,ytest=train_test_split(x,y,test_size=0.3,random_state=420)
# 建模
gnb=GaussianNB().fit(xtrain,ytrain)
acc_score=gnb.score(xtest,ytest) #返回预测的精确性accuracy
acc_score
0.8592592592592593
# 查看预测结果
y_pred=gnb.predict(xtest)
y_pred
array([6, 1, 3, 0, 4, 5, 0, 8, 3, 8, 6, 8, 7, 8, 8, 8, 5, 9, 5, 6, 5, 4, 7, 4, 8, 2, 7, 2, 8, 9, 2, 8, 3, 6, 0, 3, 8, 8, 1, 5, 2, 8, 8, 9, 2, 2, 0, 7, 3, 6, 7, 2, 8, 0, 5, 4, 1, 9, 4, 0, 5, 8, 9, 1, 7, 8, 7, 5, 8, 2, 4, 4, 8, 2, 6, 1, 2, 1, 7, 8, 8, 5, 9, 4, 3, 6, 9, 7, 4, 2, 4, 8, 0, 5, 7, 7, 7, 4, 7, 8, 8, 7, 0, 7, 2, 1, 9, 9, 8, 7, 1, 5, 1, 8, 0, 4, 8, 9, 5, 6, 4, 8, 3, 8, 0, 6, 8, 6, 7, 6, 1, 8, 5, 0, 8, 2, 1, 8, 8, 6, 6, 0, 2, 4, 7, 8, 9, 5, 9, 4, 7, 8, 8, 6, 7, 0, 8, 4, 7, 2, 2, 6, 4, 4, 1, 0, 3, 4, 3, 8, 7, 0, 6, 9, 7, 5, 5, 3, 6, 1, 6, 6, 2, 3, 8, 2, 7, 3, 1, 1, 6, 8, 8, 8, 7, 7, 2, 5, 0, 0, 8, 6, 6, 7, 6, 0, 7, 5, 5, 8, 4, 6, 5, 1, 5, 1, 9, 6, 8, 8, 8, 2, 4, 8, 6, 5, 9, 9, 3, 1, 9, 1, 3, 3, 5, 5, 7, 7, 4, 0, 9, 0, 9, 9, 6, 4, 3, 4, 8, 1, 0, 2, 9, 7, 6, 8, 8, 0, 6, 0, 1, 7, 1, 9, 5, 4, 6, 8, 1, 5, 7, 7, 5, 1, 0, 0, 9, 3, 9, 1, 6, 3, 7, 2, 7, 1, 9, 9, 8, 3, 3, 5, 7, 7, 7, 3, 9, 5, 0, 7, 5, 5, 1, 4, 9, 2, 0, 6, 3, 0, 8, 7, 2, 8, 1, 6, 4, 1, 2, 5, 7, 1, 4, 9, 5, 4, 2, 3, 5, 9, 8, 0, 0, 0, 0, 4, 2, 0, 6, 6, 8, 7, 1, 1, 8, 1, 1, 7, 8, 7, 8, 3, 1, 4, 6, 1, 8, 1, 6, 6, 7, 2, 8, 5, 3, 2, 1, 8, 7, 8, 5, 1, 7, 2, 1, 1, 7, 8, 9, 5, 0, 4, 7, 8, 8, 9, 5, 5, 8, 5, 5, 8, 1, 0, 4, 3, 8, 2, 8, 5, 7, 6, 9, 9, 5, 8, 9, 9, 1, 8, 6, 4, 3, 3, 3, 3, 0, 8, 0, 7, 7, 6, 0, 8, 9, 8, 3, 6, 6, 8, 7, 5, 8, 4, 5, 8, 6, 7, 6, 7, 7, 8, 0, 8, 2, 2, 0, 5, 7, 3, 0, 2, 8, 2, 0, 2, 3, 6, 8, 1, 7, 5, 7, 1, 7, 7, 2, 7, 5, 2, 6, 5, 8, 0, 0, 8, 1, 3, 7, 6, 1, 5, 6, 2, 0, 1, 5, 7, 8, 0, 3, 5, 0, 7, 5, 4, 4, 1, 5, 9, 5, 3, 7, 1, 7, 3, 5, 8, 5, 8, 5, 6, 1, 6, 7, 4, 3, 7, 0, 5, 4, 9, 3, 3, 6, 3, 5, 2, 9, 8, 9, 3, 9, 7, 3, 4, 9, 4, 3, 1])
# 查看预测的概率结果
prob=gnb.predict_proba(xtest)
prob.shape #10列,每一列对应一个标签类别下的概率
# 使用混淆矩阵来查看贝叶斯结果
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)
1.2 探索贝叶斯:高斯朴素贝叶斯的拟合效果与运算速度
通过绘制高斯朴素贝叶斯的学习曲线与分类树,随机森林和支持向量机的学习曲线的对比,来探究朴素贝叶斯算法在算法在拟合上的性质。使用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
#根据输入的分类器,数据,画图所需要的一系列参数,交叉验证的模式,以及其它可能的参数,一次性画出所有的学习曲线
# 找出每个图像所需要的横纵坐标-learning_curve
# 需要绘制子图所在的画布 plt.figure()
def plot_learning_curve(estimator,title,x,y,
ax,#选择子图
ylim=None,#设置纵坐标的取值范围
cv=None #交叉验证
):
train_sizes,train_scores,test_scores=learning_curve(estimator,#分类器
x,y,#特征矩阵和标签
cv=cv#表示交叉验证模式
)
ax.set_title(title)
if ylim is not None:
ax.set_ylim(*ylim) #设定y轴取值范围一致,方便比较
ax.set_xlabel("Traing example")
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
digits=load_digits()
x,y=digits.data,digits.target
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],cv=cv)
print("{}:{}".format(title_,datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f")))
Naive Bayes:00:01:562608 DecisionTree:00:02:984543 SVM,rbf kernel:00:25:783674 RandomForest:00:26:049973 Logistic:00:11:874073
可以发现,在几种模型中,贝叶斯用时较短,但其在测试集的精确性却不如其它模型。对于其它模型,随着训练样本的增加,训练集的精确性基本保持接近100%的分数,且测试集的分数也随之增加(当样本量较少时,模型易过拟合导致测试集精确性较低) ,较符合常理。而贝叶斯随着训练样本量的增加,虽然测试集的精确性在增加,但训练集的精确性却在减少,可以预测,当样本量足够大时,贝叶斯的分数最多也只能是85%左右(很少出现测试集分数高于训练集)。
2. 概率类模型评估指标
混淆矩阵和精确性可以帮助我们了解贝叶斯分类结果。然而,我们选择贝叶斯分类,大对数时候不单单追求效果,而是希望看到预测的相关概率。这种概率给出预测的可信度,所以对于概率类模型,我们可以通过概率类模型独有的模型评估指标来帮助我们判断。接下来就来看看几种概率类模型独有的评估指标。
2.1 布里尔分数 Brier Score
from sklearn.metrics import brier_score_loss
ytest=pd.get_dummies(ytest).loc[:,1]
brier_score_loss(ytest,prob[:,1],pos_label=1)
# 注意,第一个参数是真实标签,第二个参数是预测出的概率值
# pos_label与prob中的索引一致,就可以查看这个类别下的布里尔分数是多少
0.032619662406118764
布里尔分数可以用于任何可以使用predict_proba接口调用概率的模型,接下来来探索手写数字数据集上,逻辑回归、SVC、高斯朴素贝叶斯的效果如何:
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression as LR
from sklearn.datasets import load_digits
digits=load_digits() #手写数据集
x,y=digits.data,digits.target
xtrain,xtest,ytrain,ytest=train_test_split(x,y,test_size=0.3,random_state=420)
gnb=GaussianNB().fit(xtrain,ytrain)
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,logi.predict_proba(xtest)[:,1],pos_label=1)
0.01142992902197938
# 由于svc的置信度不是概率,为了可比性,我们需要将SVC的置信度“距离”归一化1,压缩到【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.24286480465579566
# 如果将每个分类器每个标签类别下的布里尔分数可视化:
import pandas as pd
name=["Bayes","Logistic","SVC"]
color=["red","black","orange"]
df=pd.DataFrame(index=range(1,10),columns=name)
for i in range(1,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(1,10),df.iloc[:,i],c=color[i],label=name[i])
plt.legend()
plt.show()
可以观察到,逻辑回归的布里尔分数有着压倒性的优势,SVC的效果明显弱于贝叶斯和逻辑回归(SVC是强行利用sigmoid函数来压缩概率,因此SVC产出概率的结果并不是那么可靠)。贝叶斯位于逻辑回归和SVC之间,效果不错,但比起逻辑回归还是不够精确和稳定。
2.2 对数似然函数 Log Loss
另一种常用的概率损失衡量是对数损失(log loss),又叫对数似然,逻辑损失或者交叉熵损失,它是多元逻辑回归以及一些拓展算法比如神经网络中使用的损失函数。它被定义为:对于一个给定的概率分类器,在预测概率为条件的情况下,真实概率发生的可能性的负对数。由于是损失,因此对数似然函数的取值越小越好,则证明概率估计越准确,模型越理想。值得注意的是,对数损失只能用于评估分类型模型。
对于一个样本,如果样本的真实标签在{0,1}取值,并且这个样本在类别1下的估计概率为,则这个样本所对应的损失函数为:(注:这里的log表示以e为底的自然对数)
在sklearn中,可以从metrics模块中导入对数似然函数:
from sklearn.metrics import log_loss
score1=[]
for i in range(10):
score1.append(log_loss(ytest,prob[:,i]))
sum(score1)/len(score1)
5.358355031493363
score2=[]
for i in range(10):
score2.append(log_loss(ytest,logi.predict_proba(xtest)[:,1]))
sum(score2)/len(score2)
0.05586444292410096
score3=[]
for i in range(10):
score3.append(log_loss(ytest,svc_prob[:,1]))
sum(score3)/len(score3)
0.7065400146541764
log_loss第一个参数是真实标签,第二个参数是预测概率。注意到,使用log_loss得出的结论和使用布里尔分数得出的结论不一致。当时用布里尔分数作为评判标准的时候,SVC的估计效果是最差的,逻辑回归和贝叶斯的结果想接近。而使用对数似然时,虽然依然是逻辑回归效果最好,但贝叶斯的效果却不如SVC,这是因为逻辑回归和SVC都是以最优化来求解模型,然后进行分类的算法;而朴素贝叶斯没有这个过程。对数似然函数直接指向模型最优化的方向,甚至就是逻辑回归的损失函数本身,因此在逻辑回归和SVC上表现得更好。
在现实应用中,对数似然函数是概率类模型评估的黄金指标,往往是评估概率类模型的优先选择。但是它也有一些缺点,首先它没有界,不像布里尔分数有上限,可以作为模型效果的参考。其次,它的解释性不如布里尔分数。第三,它在以最优化为目标的模型上明显表现得更好。而且在一些数学问题上,比如不能接受0或1的概率,否则对数似然会取到极限值(考虑以e为底的自然底数在取到0或1的情况)。所以通常来说有以下规则:
需求 | 优先使用对数似然 | 优先使用布里尔分数 |
---|---|---|
衡量模型 | 要对比多个模型,或者衡量模型的不同变化 | 衡量单一模型的表现 |
可解释性 | 机器学习和深度学习之间的行家交流,学术论文 | 商业报告,业务模型的衡量 |
最优化指向 | 逻辑回归,SVC | 朴素贝叶斯 |
数字问题 | 概率只能无限接近于0或1,无法取到0或1 | 概率可以取到0或1 |
贝叶斯的效果不如其它模型,而贝叶斯原理简单,没有什么可用的参数。但是产出概率的算法有自己的调节方式,就是调节概率的校准程度。校准程度越高,模型对概率的预测越准确,算法在做判断时就越有自信,模型就会更稳定。如果追求模型在概率预测上必须尽量贴近真实概率,那就可以使用可靠性曲线来调节概率的校准程度。
2.3 可靠性曲线 Reliability Curve
可靠性曲线(reliability curve),又叫概率校准曲线(probalility calibration curve),可靠性图(realibility diagrams),这是一条以预测概率为横坐标,真实概率为纵坐标的曲线。我们希望预测概率和真实概率越接近越好。校准曲线因此也是模型评估指标之一。和布里尔分数相似,概率校准曲线是对于标签的某一类来说的,因此一类标签就会有一条曲线,或者可以使用一个多类标签下的平均来表示一整个模型的概率校准曲线。但通常来讲,曲线用于二分类的情况较多。
和ROC曲线类似,在sklearn中,使用类calibration_curve来获取横纵坐标,然后使用matplotlib来绘图。在画可靠性曲线时,纵坐标是真实概率,而在现实中真实概率是不可获取的。但我们可以使用类概率的指标类帮助我们进行校准。一个简单的做法就是:将数据进行分箱,然后规定每个箱子中真实的少数类所占的比例为这个箱子上的真实概率trueproba,这个箱子中预测概率的均值为这个箱子的预测概率predproba,然后以predproba为横坐标,以trueproba为纵坐标,来绘制可靠性曲线。
举个例子,来看这张表,这是一组数据不分箱时表现的图像:
分箱后的图像:
可见,分箱之后样本点的特征被聚合到了一起,曲线明显变得单调且平滑。这种分箱操作本质相当于是一种平滑。在sklearn中,这样的做法可以通过绘制可靠性曲线的类calibration_curve来实现。和ROC曲线类似,类calibration_curve可以帮助我们获取横纵坐标,然后使用matplotlib来绘制图像。该类有如下参数:
参数 | 含义 |
---|---|
y_true | 真实标签 |
y_prob | 预测返回的,正类别下的概率值或置信度 |
normalize | 布尔值,默认为False 是否将y_prob中输入的内容归一化到[0,1]之间 |
n_bins | 整数值,表示分箱的个数。如果箱数很大,则需要更多的数据 |
返回的 | 含义 |
---|---|
trueproba | 可靠性曲线的纵坐标,结构为(n_bins,),是每个箱子中少数类(Y=1)的占比 |
predproba | 可靠性曲线的横坐标,结构为(n_bins,),是每个箱子中概率的均值 |
2.3.1 使用可靠性曲线的类在贝叶斯上绘制一条校准曲线
import numpy as np
import matplotlib.pyplot as plt
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression as LR
from sklearn.datasets import make_classification as mc
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)
# 建立模型,绘制图像
gnb=GaussianNB()
gnb.fit(xtrain,ytrain)
y_pred=gnb.predict(xtest) #预测标签
prob_pos=gnb.predict_proba(xtest)[:,1] #预测概率
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="Bayes")
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()
2.3.2 不同n_bins取值下曲线如何改变
fig,axes=plt.subplots(1,3,figsize=(18,4))
for ind,i in enumerate([3,10,100]): #i取值为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 predicted probability")
ax1.set_ylim([-0.05,1.05])
ax.legend()
plt.show()
很明显可以看出n_bins越大,箱子越多,概率校准曲线越精确,但太过精确的曲线不够平滑,无法和我们希望的完美概率曲线相比较。n_bins越小,箱子越小,概率校准曲线就越粗糙,虽然靠近完美概率曲线,但无法真实地展现模型概率预测的结果。所以应选取一个既不是太大也不是太小的箱子个数,让概率校准曲线既不是太精确,也不是太粗糙,二十一条相对平滑,又可以反映出模型对概率预测的趋势曲线。
2.3.3 建立更多模型
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的接口,存在则返回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 predicted probability")
ax1.set_ylim([-0.05,1.05])
ax1.legend()
ax1.set_title('Calibration plots (reliability curve)')
plt.show()
从图像的结果可以明显看出,逻辑回归的概率估计是最接近完美的概率校准曲线,所以逻辑回归的效果最完美。相对的,高斯朴素贝叶斯和支持向量机分类器的结果都比较糟糕。支持向量机呈现类似于sigmoid函数的形状,而高斯朴素贝叶斯呈现和Sigmoid函数相反的形状。
sigmoid函数图像:
对于贝叶斯,如果概率校准曲线呈现sigmoid函数的镜像的情况,则说明数据集中的特征不是相互条件独立的。贝叶斯原理中的”朴素“原则:特征相互条件独立原则被违反了(这其实是我们自己的设定,我们设定了10个冗余特征,这些特征就是噪音,他们之间不可能完全独立),因此贝叶斯的表现不够好。
而支持向量机的概率校准曲线效果其实是典型的置信度不足的分类器(under-confident classifier)的表现:大量的样本点集中在决策边界的附近,因此许多样本点的置信度靠近0.5左右,即便决策边界能够将样本点判断正确,模型本身对这个结果也不是非常确信的。相对的,离决策边界很远的点的置信度就会很高,因为它很大可能性上不会被判断错误。支持向量机在面对混合度较高的数据的时候,有着天生的置信度不足的缺点。
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的接口,存在则返回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.5 校准可靠性曲线
等近似回归有两种回归可以使用,一种是基于Platt的Sigmoid模型的参数校准方法,一种是基于等渗回归(isotonic calibration)的非参数的校准方法。概率校准应该发生在测试集上,必须是模型未曾见过的数据。在数学上,使用这两种方式来对概率进行校准的原理十分复杂,而此过程我们在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,分类器必须已经拟合数据完毕。 |
method | 进行概率校准的方法,可输入"sigmoid"或者"isotonic" 输入’sigmoid’,使用基于Platt的Sigmoid模型来进行校准 当校准的样本量太少(比如,小于等于1000个测试样本)的时候,不建议使用等渗回归,因为它倾向于过拟合。样本量过少时请使用sigmoids,即Platt校准。 |
cv | 整数,确定交叉验证的策略。可能输入是: 1)None,表示使用默认的3折交叉验证。 |
2.5.1 包装函数
首先,我们将之前绘制可靠性曲线和直方图的代码包装成函数。考虑函数的参数为:模型,模型的名字,数据,和需要分箱的个数。
import matplotlib.pyplot as plt
from sklearn.metrics import brier_score_loss
from sklearn.calibration import calibration_curve
def plot_calib(models,name,xtrain,xtest,ytrain,ytest,n_bins=10):
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=10)
ax1.plot(predproba,trueproba,"s-",label="%s(%1.3f)" % (name_,clf_score))
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.legend(loc=9)
ax1.set_ylabel("True probability for class 1")
ax1.set_xlabel("mean predicted probability")
ax1.set_ylim([-0.05,1.05])
ax1.legend(loc=9)
ax1.set_title('Calibration plots (reliability curve)')
plt.show()
2.5.2 基于函数绘图
from sklearn.calibration import CalibratedClassifierCV
name=["GaussianBayes","Logistic","Bayes+isotonic","Bayes+sigmoid"]
gnb=GaussianNB()
models=[gnb,
LR(C=1.0,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.095,比逻辑回归还低一个点。Sigmoid校准的方式也对曲线进行了稍稍的改善,不过效果不明显。从直方图来看,Isotonic校正让高斯朴素贝叶斯的效果接近逻辑回归,而Sigmoid校正后的结果依然和原本的高斯朴素贝叶斯更相近。可见,当数据的特征之间不是相互条件独立的时候,使用Isotonic方式来校准概率曲线,可以得到不错的结果。
2.5.3 基于校准结果查看朴素贝叶斯精确性的变化
gnb=GaussianNB().fit(xtrain,ytrain)
gnb.score(xtest,ytest)
0.8639292929292929
brier_score_loss(ytest,gnb.predict_proba(xtest)[:,1],pos_label=1)
0.11749080113888638
gnbisotonic=CalibratedClassifierCV(gnb,cv=2,method='isotonic').fit(xtrain,ytrain)
gnbisotonic.score(xtest,ytest)
0.8637272727272727
brier_score_loss(ytest,gnbisotonic.predict_proba(xtest)[:,1],pos_label=1)
0.09780399457496632
2.5.4 SVC的校准
name_svc=["SVC","Logistic","SVC+isotonic","SVC+sigmoid"]
svc=SVC(kernel="linear",gamma=1)
models_svc=[svc,
LR(C=1.0,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)
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)
#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()) #正样本
score=clf.score(xtest,ytest)
print("{}:".format(name_))
print("\tBrier:{:.4f}".format(clf_score))
print("\tAccuracy:{:.4f}".format(score))
SVC: Brier:0.1632 Accuracy:0.8665 SVC+isotonic: Brier:0.0982 Accuracy:0.8641 SVC+sigmoid: Brier:0.0976 Accuracy:0.8654
可以看出,校准概率后,布里尔分数明显变小了,但整体的准确率却略有下降,这证明算法在校准之后,尽管对概率的预测更准确了,但模型的判断力略有降低。来思考一下:布里尔分数衡量模型概率预测的准确率,布里尔分数越低,代表模型的概率越接近真实概率,当进行概率校准后,本来标签是1的样本的概率应该会更接近1,而标签本来是0的样本应该会更接近0,没有理由布里尔分数提升了,模型的判断准确率居然下降了。但从我们的结果来看,模型的准确率和概率预测的正确性并不是完全一致的,为什么会这样呢?
对于不同的概率类模型,原因是不同的。对于SVC,决策树这样的模型来说,概率不是真正的概率,而更偏向于是一个“置信度”,这些模型也不是依赖于概率预测来进行分类(决策树依赖于树杈而SVC依赖于决策边界),因此对于这些模型,可能存在着类别1下的概率为0.4但样本依然被分类为1的情况,这种情况代表着——模型很没有信心认为这个样本是1,但是还是坚持把这个样本的标签分类为1了。这种时候,概率校准可能会向着更加错误的方向调整(比如把概率为0.4的点调节得更接近0,导致模型最终判断错误),因此出现布里尔分数可能会显示和精确性相反的趋势。
而对于朴素贝叶斯这样的模型,却是另一种情况。注意在朴素贝叶斯中,我们有各种各样的假设,除了我们的“朴素”假设,还有我们对概率分布的假设(比如说高斯),这些假设使得我们的贝叶斯得出的概率估计其实是有偏估计,也就是说,这种概率估计其实不是那么准确和严肃。我们通过校准,让模型的预测概率更贴近于真实概率,本质是在统计学上让算法更加贴近我们对整体样本状况的估计,这样的一种校准在一组数据集上可能表现出让准确率上升,也可能表现出让准确率下降,这取决于我们的测试集有多贴近我们估计的真实样本的面貌。这一系列有偏估计使得我们在概率校准中可能出现布里尔分数和准确度的趋势相反的情况。
当然,可能还有更多更深层的原因,比如概率校准过程中的数学细节如何影响了我们的校准,类calibration_curve中是如何分箱,如何通过真实标签和预测值来生成校准曲线使用的横纵坐标的,这些过程中也可能有着让布里尔分数和准确率向两个方向移动的过程。
当两者相悖的时候,应以准确率为标准。但是这不代表说布里尔分数和概率校准曲线就无效了。概率类模型几乎没有参数可以调整,除了换模型之外,鲜有更好的方式帮助我们提升模型的表现,概率校准是难得的可以帮助我们针对概率提升模型的方法。
在现实中,我们可以选择调节模型的方向,我们不一定要追求最高的准确率或者追求概率拟合最好,我们可以根据自己的需求来调整模型。当然,对于概率类模型来说,由于可以调节的参数甚少,所以我们更倾向于追求概率拟合,并使用概率校准的方式来调节模型。如果你的确希望追求更高的准确率和Recall,可以考虑使用天生就非常准确的概率类模型逻辑回归,也可以考虑使用除了概率校准之外还有很多其他参数可调的支持向量机分类器。