详解支持向量机-ROC曲线中的概率和阈值【菜菜的sklearn课堂笔记】

时间:2022-11-29 11:02:22

视频作者:[菜菜TsaiTsai] 链接:[【技术干货】菜菜的机器学习sklearn【全85集】Python进阶_哔哩哔哩_bilibili]

基于混淆矩阵,我们学习了总共六个指标:准确率Accuracy,精确度Precision,召回率Recall,精确度和召回度的平衡指标F measure,特异度Specificity,以及假正率FPR。 其中,假正率有一个非常重要的应用:我们在追求较高的Recall的时候,Precision会下降,就是说随着更多的少数类被捕捉出来,会有更多的多数类被判断错误,但我们很好奇,随着Recall的逐渐增加,模型将多数类判断错误的能力如何变化呢?我们希望理解,我每判断正确一个少数类,就有多少个多数类会被判断错误。假正率正好可以帮助我们衡量这个能力的变化。相对的,Precision无法判断这些判断错误的多数类在全部多数类中究竟占多大的比例,所以无法在提升Recall的过程中也顾及到模型整体的Accuracy。因此,我们可以使用Recall和FPR之间的平衡,来替代Recall和Precision之间的平衡,让我们衡量模型在尽量捕捉少数类的时候,误伤多数类的情况如何变化,这就是我们的ROC曲线衡量的平衡。 ROC曲线,全称The Receiver Operating Characteristic Curve,译为受试者操作特性曲线。这是一条以不同阈值下的假正率FPR为横坐标,不同阈值下的召回率Recall为纵坐标的曲线

概率(probability)与阈值(threshold)

要理解概率与阈值,最容易的状况是来回忆一下我们用逻辑回归做分类的时候的状况。逻辑回归的predict_proba接口对每个样本生成每个标签类别下的似然(类概率)。对于这些似然,逻辑回归天然规定,当一个样本所对应的这个标签类别下的似然大于0.5的时候,这个样本就被分为这一类。比如说,一个样本在标签1下的似然是0.6,在标签0下的似然是0.4,则这个样本的标签自然就被分为1。逻辑回归的回归值本身,其实也就是标签1下的似然。在这个过程中,0.5就被称为阈值。来看看下面的例子:

from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import numpy as np

class_1 = 7
class_2 = 4
centers = [[0.0,0.0],[1,1]]
clusters_std = [0.5,1]
X, y = make_blobs(n_samples=[class_1,class_2]
                 ,centers=centers
                 ,cluster_std=clusters_std
                 ,random_state=0
                 ,shuffle=False
                 )

plt.scatter(X[:,0],X[:,1],c=y,cmap='rainbow',s=30)

详解支持向量机-ROC曲线中的概率和阈值【菜菜的sklearn课堂笔记】

from sklearn.linear_model import LogisticRegression as LogiR

clf_lo = LogiR().fit(X,y)
prob = clf_lo.predict_proba(X)
prob,prob.shape # 返回11个样本属于类别0和类别1的概率(似然)
# 谁的概率更大,就认为样本点是哪一类
---
(array([[0.60466356, 0.39533644],
        [0.45891589, 0.54108411],
        [0.71798647, 0.28201353],
        [0.67910911, 0.32089089],
        [0.66473898, 0.33526102],
        [0.56277457, 0.43722543],
        [0.66205409, 0.33794591],
        [0.35516738, 0.64483262],
        [0.38160618, 0.61839382],
        [0.58528378, 0.41471622],
        [0.50149311, 0.49850689]]), (11, 2))

import pandas as pd

prob = pd.DataFrame(prob,columns=['0','1'])
prob # 往后只写前五行,有必要的话展示全部
---
	0	1
0	0.604664	0.395336
1	0.458916	0.541084
2	0.717986	0.282014
3	0.679109	0.320891
4	0.664739	0.335261

for i in range(prob.shape[0]):
    if prob.loc[i,'1'] > 0.5: # 这里0.5就是我们设定的阈值
        prob.loc[i,'pred'] = 1
    else:
        prob.loc[i,'pred'] = 0
prob
---
	0	1	pred
0	0.604664	0.395336	0.0
1	0.458916	0.541084	1.0
2	0.717986	0.282014	0.0
3	0.679109	0.320891	0.0
4	0.664739	0.335261	0.0

prob['y_true'] = y
prob = prob.sort_values(by='1',ascending=False)
# 按照'1'列数字大小排序,使用非升序(也就是降序)
prob
---
	0	1	pred	y_true
7	0.355167	0.644833	1.0	1
8	0.381606	0.618394	1.0	1
1	0.458916	0.541084	1.0	0
10	0.501493	0.498507	0.0	1
5	0.562775	0.437225	0.0	0
# 得到一组pred和y_true就可以生成一个混淆矩阵,从而得到Recall和FPR,进而得到ROC曲线上的一个点
# 但下面我们先计算precision和recall,说明阈值变化的一个问题
confusion_matrix(y_true, y_pred, labels=None, sample_weight=None)
# y_true:样本的真实标签,也就是上面的y_True
# y_pred:样本指定模型和阈值下的预测标签,也就是上面的pred
# labels:说明需要哪些类,注意,少数类在前

precision_score(
    ['y_true', 'y_pred', 'labels=None', 'pos_label=1', "average='binary'", 'sample_weight=None'],
)
# 同confusion_matrix

recall_score(
    ['y_true', 'y_pred', 'labels=None', 'pos_label=1', "average='binary'", 'sample_weight=None'],
)
# 同confusion_matrix
from sklearn.metrics import confusion_matrix as CM, precision_score as P, recall_score as R

CM(prob.loc[:,'y_true'],prob.loc[:,'pred'],labels=[1,0])
---
array([[2, 2],
       [1, 6]], dtype=int64)

通过这个表格我们可以先自己计算一下 $$ Precision = \frac{2}{3}=0.666,Recall = \frac{2}{4}=0.5 $$ 用代码验证一下

P(prob.loc[:,'y_true'],prob.loc[:,'pred'],labels=[1,0])
---
0.6666666666666666

R(prob.loc[:,'y_true'],prob.loc[:,'pred'],labels=[1,0])
---
0.5

改变阈值,观察precision和recall的变化

for i in range(prob.shape[0]):
    if prob.loc[i,'1'] > 0.4:
        prob.loc[i,'pred'] = 1
    else:
        prob.loc[i,'pred'] = 0

prob
---
	0	1	pred	y_true
7	0.355167	0.644833	1.0	1
8	0.381606	0.618394	1.0	1
1	0.458916	0.541084	1.0	0
10	0.501493	0.498507	1.0	1
5	0.562775	0.437225	1.0	0

CM(prob.loc[:,'y_true'],prob.loc[:,'pred'],labels=[1,0])
---
array([[4, 0],
       [2, 5]], dtype=int64)
# 显然我们的混淆矩阵发生了变化

P(prob.loc[:,'y_true'],prob.loc[:,'pred'],labels=[1,0])
---
0.6666666666666666
# 但精确度没有发生变化,因此阈值变化不一定让参数单调变化
# 因此这个参数绘制曲线可能会导致不单调,不好研究最优值
# 因此之后我们换成假正率FPR

R(prob.loc[:,'y_true'],prob.loc[:,'pred'],labels=[1,0]) # 完全捕获少数类
---
1.0

注意,降低或者升高阈值并不一定能够让模型的效果一定变好,一切基于我们要追求怎样的模型效果。通常来说,降低阈值能够升高Recall 而要体现阈值的影响,首先必须的得到分类器在少数类下的预测概率。对于逻辑回归这样天生生成似然的算法和朴素贝叶斯这样就是在计算概率的算法,自然非常容易得到概率,但对于一些其他的分类算法,比如决策树,比如SVM,他们的分类方式和概率并不相关。那在他们身上,我们就无法画ROC曲线了吗?并非如此。

决策树有叶子节点,一个叶子节点上可能包含着不同类的样本。假设一个样本被包含在叶子节点a中,节点a包含10个样本,其中6个为1,4个为0,则1这个正类在这个叶子节点中的出现概率就是60%,类别0在这个叶子节点中的出现概率就是40%。对于所有在这个叶子节点中的样本而言,节点上的1和0出现的概率,就是这个样本对应的取到1和0的概率,大家可以去自己验证一下。但是思考一个问题,由于决策树可以被画得很深,在足够深的情况下,决策树的每个叶子节点上可能都不包含多个类别的标签了,可能一片叶子中只有唯一的一个标签,即叶子节点的不纯度为0,此时此刻,对于每个样本而言,他们所对应的“概率”就是0或者1了。这个时候,我们就无法调节阈值来调节我们的Recall和FPR了。对于随机森林,也是如此。

不过其实,SVM也可以生成概率

SVM实现概率预测:重要参数probability,接口predict_proba以及decision_function

我们在画等高线,也就是决策边界的时候曾经使用SVC的接口decision_function,它返回我们输入的特征矩阵中每个样本到决策边界的距离。我们在SVM中利用决策边界来判断我们的样本,本质上来说,当两个点的距离是相同的符号的时候,越远离超平面的样本点归属于某个标签类的概率就很大。比如说,一个距离超平面0.1的点,和一个距离超平面100的点,明显是距离为0.1的点更有可能是负类别的点混入了边界。同理,一个距离超平面距离为-0.1的点,和一个离超平面距离为-100的点,明显是-100的点的标签更有可能是负类。所以,到超平面的距离一定程度上反应了样本归属于某个标签类的可能性。接口decision_function返回的值也因此被我们认为是SVM中的置信度(confidence)。 不过,置信度始终不是概率,它没有边界,可以无限大,大部分时候也不是以百分比或者小数的形式呈现,而SVC 的判断过程又不像决策树一样可以求解出一个比例。为了解决这个矛盾,SVC有重要参数probability。

SVC(
    ['C=1.0', "kernel='rbf'", 'degree=3', "gamma='auto_deprecated'", 'coef0=0.0', 'shrinking=True', 'probability=False', 'tol=0.001', 'cache_size=200', 'class_weight=None', 'verbose=False', 'max_iter=-1', "decision_function_shape='ovr'", 'random_state=None'],
)
# 如果我们想要使用概率接口predict_proba和predict_log_proba,就要在实例化模型的时候设定probability=True
# 启用此功能会减慢SVM的运算速度
# 在二分类情况下,SVC将使用Platt缩放来生成概率,即在decision_function生成的距离上进行Sigmoid压缩,并附加训练数据的交叉验证拟合,来生成类逻辑回归的SVM分数。

Sigmoid函数由下列公式定义 $$ S(x)=\frac{1}{1+e^{-x}} $$ 可以看出Sigmoid函数值在$(0,1)$,也正好符合我们的概率值的要求。即使一个数字无穷大,其代入Sigmoid函数后映射值也在$(0,1)$内,那么,其属于另一个类的概率就是$1-p$ 详解支持向量机-ROC曲线中的概率和阈值【菜菜的sklearn课堂笔记】

链接:Sigmoid函数_百度百科 (baidu.com)

from sklearn.svm import SVC

class_1 = 500
class_2 = 50
centers = [[0.0,0.0],[2.0,2.0]]
cluster_std = [1.5,0.5]
X,y = make_blobs(n_samples=[class_1,class_2]
                ,centers=centers
                ,cluster_std=cluster_std
                ,random_state=0
                ,shuffle=False)

plt.scatter(X[:,0],X[:,1],c=y,cmap='rainbow',s=10)

详解支持向量机-ROC曲线中的概率和阈值【菜菜的sklearn课堂笔记】

clf = SVC(kernel="linear",C=1,probability=True).fit(X,y)
clf.predict_proba(X).shape
# 生成的各类标签下的概率
# 因为我们之前设置了probability=True
---
(550, 2) # 我们有550个样本,标签的取值有0,1两个,因此shape为(550,2)
# 我们可以使用和逻辑回归同样的方式来在SVM上设定和调节我们的阈值。

Platt缩放中涉及的交叉验证对于大型数据集来说非常昂贵,计算会非常缓慢。另外,由于Platt缩放的理论原因,在二分类过程中,有可能出现predict_proba返回的概率小于0.5,但样本依旧被标记为正类的情况出现,毕竟支持向量机本身并不依赖于概率来完成自己的分类。如果我们的确需要置信度分数,但不一定非要是概率形式的话,那建议可以将probability设置为False,使用decision_function这个接口而不是predict_proba。