二分类SVC中的样本不均衡问题:重要参数class_weight
对于分类问题,永远都逃不过的一个痛点就是样本不均衡问题。样本不均衡是指在一组数据集中,标签的一类天生 占有很大的比例,但我们有着捕捉出某种特定的分类的需求的状况。比如,我们现在要对潜在犯罪者和普通人进行分类,潜在犯罪者占总人口的比例是相当低的,也许只有2%左右,98%的人都是普通人,而我们的目标是要捕获出潜在犯罪者。这样的标签分布会带来许多问题。
首先,分类模型天生会倾向于多数的类,让多数类更容易被判断正确,少数类被牺牲掉。因为对于模型而言,样本量越大的标签可以学习的信息越多,算法就会更加依赖于从多数类中学到的信息来进行判断。如果我们希望捕获少数类,模型就会失败。 其次,模型评估指标会失去意义。这种分类状况下,即便模型什么也不做,全把所有人都当成不会犯罪的人,准确率也能非常高,这使得模型评估指标accuracy变得毫无意义,根本无法达到我们的“要识别出会犯罪的人”的建模目的。
所以现在,我们首先要让算法意识到数据的标签是不均衡的,通过施加一些惩罚或者改变样本本身,来让模型向着捕获少数类的方向建模。然后,我们要改进我们的模型评估指标,使用更加针对于少数类的指标来优化模型。
要解决第一个问题,我们在逻辑回归中已经介绍了一些基本方法,比如上采样下采样。但这些采样方法会增加样本的总数,对于支持向量机这个样本总是对计算速度影响巨大的算法来说,我们完全不想轻易地增加样本数量。况且,支持向量机中地决策仅仅决策边界的影响,而决策边界又仅仅受到参数C和支持向量的影响,单纯地增加样本数量不仅会增加计算时间,可能还会增加无数对决策边界无影响的样本点。因此在支持向量机中,我们要大力依赖我们调节样本均衡的参数:SVC类中的class_weight和接口fit中可以设定的sample_weight。
在逻辑回归中,参数class_weight默认None,此模式表示假设数据集中的所有标签是均衡的,即自动认为标签的比例是1:1。所以当样本不均衡的时候,我们可以使用形如{"标签的值1":权重1,"标签的值2":权重2}的字典来输入真实的样本标签比例,来让算法意识到样本是不平衡的。或者使用”balanced“模式,直接使用n_samples/(n_classes * np.bincount(y))作为权重,可以比较好地修正我们的样本不均衡情况。
但在SVM中,我们的分类判断是基于决策边界的,而最终决定究竟使用怎样的支持向量和决策边界的参数是参数C,所以所有的样本均衡都是通过参数C来调整的。
SVC的参数:class_weight
可输入字典或者"balanced”,可不填,默认None对SVC,将类i的参数C设置为class_weight [i] * C。如果没有给出具体的class_weight,则所有类都被假设为占有相同的权重1,模型会根据数据原本的状况去训练。如果希望改善样本不均衡状况,请输入形如{"标签的值1":权重1,"标签的值2":权重2}的字典,则参数C将会自动被设为:标签的值1的C:权重1 * C,标签的值2的C:权重2*C。
或者,可以使用“balanced”模式,这个模式使用y的值自动调整与输入数据中的类频率成反比的权重为n_samples/(n_classes * np.bincount(y))
SVC的接口fit的参数:sample_weight
数组,结构为 (n_samples, ),必须对应输入fit中的特征矩阵的每个样本
每个样本在fit时的权重,让权重 * 每个样本对应的C值来迫使分类器强调设定的权重更大的样本。通常,较大的权重加在少数类的样本上,以迫使模型向着少数类的方向建模
通常来说,这两个参数我们只选取一个来设置。如果我们同时设置了两个参数,则C会同时受到两个参数的影响, 即 class_weight中设定的权重 * sample_weight中设定的权重 * C。
我们接下来就来看看如何使用这个参数。
首先,我们来自建一组样本不平衡的数据集。我们在这组数据集上建两个SVC模型,一个设置有class_weight参数,一个不设置class_weight参数。我们对两个模型分别进行评估并画出他们的决策边界,以此来观察class_weight带来的效果。
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.datasets import make_blobs
class_1 = 500 #类别1有500个样本
class_2 = 50 #类别2只有50个
centers = [[0.0, 0.0], [2.0, 2.0]] #设定两个类别的中心
clusters_std = [1.5, 0.5] #设定两个类别的方差,通常来说,样本量比较大的类别会更加松散
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=10)
#其中红色点是少数类,紫色点是多数类
#不设定class_weight
clf = svm.SVC(kernel='linear', C=1.0)
clf.fit(X, y)
#设定class_weight
wclf = svm.SVC(kernel='linear', class_weight={1: 10})
wclf.fit(X, y)
#给两个模型分别打分看看,这个分数是accuracy准确度
print(clf.score(X,y))
wclf.score(X,y)
结果:
0.9418181818181818
0.9127272727272727
绘制两个模型下数据的决策边界
#首先要有数据分布
plt.figure(figsize=(6,5))
plt.scatter(X[:,0], X[:,1], c=y, cmap="rainbow",s=10)
ax = plt.gca()
#获取当前的子图,如果不存在,则创建新的子图
#绘制决策边界的第一步:要有网格
xlim = ax.get_xlim()
ylim = ax.get_ylim()
xx = np.linspace(xlim[0], xlim[1], 30)
yy = np.linspace(ylim[0], ylim[1], 30)
YY, XX = np.meshgrid(yy, xx)
xy = np.vstack([XX.ravel(), YY.ravel()]).T
#第二步:找出我们的样本点到决策边界的距离
Z_clf = clf.decision_function(xy).reshape(XX.shape)
a = ax.contour(XX, YY, Z_clf, colors='black', levels=[0], alpha=0.5, linestyles=['-'])
Z_wclf = wclf.decision_function(xy).reshape(XX.shape)
b = ax.contour(XX, YY, Z_wclf, colors='red', levels=[0], alpha=0.5, linestyles=['-'])
#第三步:画图例
plt.legend([a.collections[0], b.collections[0]], ["non weighted" , "weighted"],loc="upper right")
plt.show()
可以看出,从准确率的角度来看,不做样本平衡的时候准确率反而更高,做了样本平衡准确率反而变低了,这是因 为做了样本平衡后,为了要更有效地捕捉出少数类,模型误伤了许多多数类样本,而多数类被分错的样本数量 > 少数类被分类正确的样本数量,使得模型整体的精确性下降。现在,如果我们的目的是模型整体的准确率,那我们就要拒绝样本平衡,使用class_weight被设置之前的模型。
然而在现实中,我们往往都在追求捕捉少数类,因为在很多情况下,将少数类判断错的代价是巨大的。比如我们之前提到的,判断潜在犯罪者和普通人的例子,如果我们没有能够识别出潜在犯罪者,那么这些人就可能去危害社会,造成恶劣影响,但如果我们把普通人错认为是潜在犯罪者,我们也许只是需要增加一些监控和人为甄别的成本。所以对我们来说,我们宁愿把普通人判错,也不想放过任何一个潜在犯罪者。我们希望不惜一切代价来捕获少数类,或者希望捕捉出尽量多的少数类,那我们就必须使用class_weight设置后的模型。
SVC的模型评估指标
从上一节的例子中可以看出,如果我们的目标是希望尽量捕获少数类,那准确率这个模型评估逐渐失效,所以我们需要新的模型评估指标来帮助我们。如果简单来看,其实我们只需要查看模型在少数类上的准确率就好了,只要能够将少数类尽量捕捉出来,就能够达到我们的目的。
但此时,新问题又出现了,我们对多数类判断错误后,会需要人工甄别或者更多的业务上的措施来一一排除我们判断错误的多数类,这种行为往往伴随着很高的成本。比如银行在判断”一个申请信用卡的客户是否会出现违约行为“的时候,如果一个客户被判断为”会违约“,这个客户的信用卡申请就会被驳回,如果为了捕捉出”会违约“的人,大量地将”不会违约“的客户判断为”会违约“的客户,就会有许多无辜的客户的申请被驳回。信用卡对银行来说意味着利息收入,而拒绝了许多本来不会违约的客户,对银行来说就是巨大的损失。同理,大众在召回不符合欧盟标准的汽车时,如果为了找到所有不符合标准的汽车,而将一堆本来符合标准了的汽车召回,这个成本是不可估量的。
也就是说,单纯地追求捕捉出少数类,就会成本太高,而不顾及少数类,又会无法达成模型的效果。所以在现实中,我们往往在寻找捕获少数类的能力和将多数类判错后需要付出的成本的平衡。如果一个模型在能够尽量捕获少 数类的情况下,还能够尽量对多数类判断正确,则这个模型就非常优秀了。为了评估这样的能力,我们将引入新的模型评估指标:混淆矩阵和ROC曲线来帮助我们。
混淆矩阵(Confusion Matrix)
混淆矩阵是二分类问题的多维衡量指标体系,在样本不平衡时极其有用。在混淆矩阵中,我们将少数类认为是正例,多数类认为是负例。在决策树,随机森林这些普通的分类算法里,即是说少数类是1,多数类是0。在SVM里,就是说少数类是1,多数类是-1。普通的混淆矩阵,一般使用{0,1}来表示。混淆矩阵阵如其名,十分容易让人混淆,在许多教材中,混淆矩阵中各种各样的名称和定义让大家难以理解难以记忆。我为大家找出了一种简化的方式来显示标准二分类的混淆矩阵,如图所示:
混淆矩阵中,永远是真实值在前,预测值在后。其实可以很容易看出,11和00的对角线就是全部预测正确的,01和10的对角线就是全部预测错误的。基于混淆矩阵,我们有六个不同的模型评估指标,这些评估指标的范围都在[0,1]之间,所有以11和00为分子的指标都是越接近1越好,所以以01和10为分子的指标都是越接近0越好。对于所有的指标,我们用橙色表示分母,用绿色表示分子,则我们有:
准确率
#对于没有class_weight,没有做样本平衡的灰色决策边界来说:
print((y[y == clf.predict(X)] == 1).sum()/(clf.predict(X) == 1).sum())
#对于有class_weight,做了样本平衡的红色决策边界来说:
(y[y == wclf.predict(X)] == 1).sum()/(wclf.predict(X) == 1).sum()
结果:
0.7142857142857143
0.5102040816326531
精确度也叫查准率,也就是决策边界上方的所有点中红色点所占的比例
可以看出,做了样本平衡之后,精确度是下降的。因为很明显,样本平衡之后,有更多的多数类紫色点被我们误伤了。精确度可以帮助我们判断,是否每一次对少数类的预测都精确,所以又被称为”查准率“。在现实的样本不平衡
召回率
#所有predict为1的点 / 全部为1的点的比例
#对于没有class_weight,没有做样本平衡的灰色决策边界来说:
print((y[y == clf.predict(X)] == 1).sum()/(y == 1).sum())
#对于有class_weight,做了样本平衡的红色决策边界来说:
(y[y == wclf.predict(X)] == 1).sum()/(y == 1).sum()
0.6
1.0
如果我们希望不计一切代价,找出少数类(比如找出潜在犯罪者的例子),那我们就会追求高召回率,相反如果我们的目标不是尽量捕获少数类,那我们就不需要在意召回率。
注意召回率和精确度的分子是相同的(都是11),只是分母不同。而召回率和精确度是此消彼长的,两者之间的平 衡代表了捕捉少数类的需求和尽量不要误伤多数类的需求的平衡。究竟要偏向于哪一方,取决于我们的业务需求:究竟是误伤多数类的成本更高,还是无法捕捉少数类的代价更高。
F1分数
为了同时兼顾精确度和召回率,我们创造了两者的调和平均数作为考量两者平衡的综合性指标,称之为F1 measure。两个数之间的调和平均倾向于靠近两个数中比较小的那一个数,因此我们追求尽量高的F1 measure, 能够保证我们的精确度和召回率都比较高。F1 measure在[0,1]之间分布,越接近1越好。
假负率(False Negative Rate),它等于 1 - Recall。
特异度和假正率
#所有被正确预测为0的样本 / 所有的0样本
#对于没有class_weight,没有做样本平衡的灰色决策边界来说:
print((y[y == clf.predict(X)] == 0).sum()/(y == 0).sum())
#对于有class_weight,做了样本平衡的红色决策边界来说:
(y[y == wclf.predict(X)] == 0).sum()/(y == 0).sum()
结果:
0.976
0.904
混淆矩阵
sklearn当中提供了大量的类来帮助我们了解和使用混淆矩阵。
基于混淆矩阵,我们学习了总共六个指标:准确率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为纵坐标的曲线。让我们先从概率和阈值开始讲起。
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)
from sklearn.linear_model import LogisticRegression as LogiR
clf_lo = LogiR().fit(X_,y_)
prob = clf_lo.predict_proba(X_)
#将样本和概率放到一个DataFrame中
import pandas as pd
prob = pd.DataFrame(prob)
prob.columns = ["0","1"]
prob
#手动调节阈值,来改变我们的模型效果
for i in range(prob.shape[0]):
if prob.loc[i,"1"] > 0.5:
prob.loc[i,"pred"] = 1
else:
prob.loc[i,"pred"] = 0
prob["y_true"] = y_
prob = prob.sort_values(by="1",ascending=False)
prob
from sklearn.metrics import confusion_matrix as CM, precision_score as P, recall_score as R
print(CM(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0])) #1是少数类(写前面)0是多数类
#试试看手动计算Precision和Recall?
print(P(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0]))
R(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0])
[[2 2]
[0 7]]
1.0
0.5
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
print(CM(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0]))
print(P(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0]))
print(R(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0]))
[[2 2]
[1 6]]
0.6666666666666666
0.5
可见,在不同阈值下,我们的模型评估指标会发生变化,我们正利用这一点来观察Recall和FPR之间如何互相影响。 但是注意,并不是升高阈值,就一定能够增加或者减少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)。
#使用最初的X和y,样本不均衡的这个模型
class_1 = 500 #类别1有500个样本
class_2 = 50 #类别2只有50个
centers = [[0.0, 0.0], [2.0, 2.0]] #设定两个类别的中心
clusters_std = [1.5, 0.5] #设定两个类别的方差,通常来说,样本量比较大的类别会更加松散
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=10)
#其中红色点是少数类,紫色点是多数类
clf_proba = svm.SVC(kernel="linear",C=1.0,probability=True).fit(X,y)
print(clf_proba.predict_proba(X)) #生成在各类标签下的概率
print(clf_proba.decision_function(X))
print(clf_proba.decision_function(X).shape)
[[0.69850902 0.30149098]
[0.28868513 0.71131487]
[0.96116579 0.03883421]
...
[0.17705259 0.82294741]
[0.38049218 0.61950782]
[0.34181702 0.65818298]]
[ -0.39182241 0.95617053 -2.24996184 -2.63659269 -3.65243197
-1.67311996 -2.56396417 -2.80650393 -1.76184723 -4.7948575
-7.59061196 -3.66174848 -2.2508023 -4.27626526 0.78571364
-3.24751892 -8.57016271 -4.45823747 -0.14034183 -5.20657114
-8.02181046 -4.18420871 -5.6222409 -5.12602771 -7.22592707
-5.07749638 -6.72386021 -3.4945225 -3.51475144 -5.72941551
-5.79160724 -8.06232013 -4.36303857 -6.25419679 -5.59426696
-2.60919281 -3.90887478 -4.38754704 -6.46432224 -4.54279979
-4.78961735 -5.53727469 1.33920817 -2.27766451 -4.39650854
-2.97649872 -2.26771979 -2.40781748 -1.41638181 -3.26142275
-2.7712218 -4.87288439 -3.2594128 -5.91189118 1.48676267
0.5389064 -2.76188843 -3.36126945 -2.64697843 -1.63635284
-5.04695135 -1.59196902 -5.5195418 -2.10439349 -2.29646147
-4.63162339 -5.21532213 -4.19325629 -3.37620335 -5.0032094
-6.04506666 -2.84656859 1.5004014 -4.02677739 -7.07160609
-1.66193239 -6.60981996 -5.23458676 -3.70189918 -6.74089425
-2.09584948 -2.28398296 -4.97899921 -8.12174085 -1.52566274
-1.99176286 -3.54013094 -4.8845886 -6.51002015 -4.8526957
-6.73649174 -8.50103589 -5.35477446 -5.93972132 -3.09197136
-5.95218482 -5.87802088 -3.41531761 -1.50581423 1.69513218
-5.08155767 -1.17971205 -5.3506946 -5.21493342 -3.73358514
-2.01273566 -3.39045625 -6.34357458 -3.54776648 -0.17804673
-6.26887557 -4.17973771 -6.68896346 -3.46095619 -5.47965411
-7.30835247 -4.41569899 -4.95103272 -4.52261342 -2.32912228
-5.78601433 -4.75347157 -7.10337939 -0.4589064 -7.67789856
-4.01780827 -4.3031773 -1.83727693 -7.40091653 -5.95271547
-6.91568411 -5.20341905 -7.19695832 -3.02927263 -4.48056922
-7.48496425 -0.07011269 -5.80292499 -3.38503533 -4.58498843
-2.76260661 -3.01843998 -2.67539002 -4.1197355 -0.94129257
-5.89363772 -1.6069038 -2.6343464 -3.04465464 -4.23219535
-3.91622593 -5.29389964 -3.59245628 -8.41452726 -3.09845691
-2.71798914 -7.1383473 -4.61490324 -4.57817871 -4.34638288
-6.5457838 -4.91701759 -6.57235561 -1.01417607 -3.91893483
-4.52905816 -4.47582917 -7.84694737 -6.49226452 -2.82193743
-2.87607739 -7.0839848 -5.2681034 -4.4871544 -2.54658631
-7.54914279 -2.70628288 -5.99557957 -8.02076603 -4.00226228
-2.84835501 -1.9410333 -3.86856886 -4.99855904 -6.21947623
-5.05797444 -2.97214824 -3.26123902 -5.27649982 -3.13897861
-6.48514315 -9.55083209 -6.46488612 -7.98793665 -0.94456569
-3.41380968 -7.093158 -5.71901588 -0.88438995 -0.24381463
-6.78212695 -2.20666714 -6.65580329 -2.56305221 -5.60001636
-5.43216357 -4.96741585 -0.02572912 -3.21839147 1.13383091
-1.58640099 -7.57106914 -4.16850181 -6.48179088 -4.67852158
-6.99661419 -2.1447926 -5.31694653 -2.63007619 -2.55890478
-6.4896746 -3.94241071 -2.71319258 -4.70525843 -5.61592746
-4.7150336 -2.85352156 -0.49195707 -8.16191324 -3.80462978
-6.43680611 -4.58137592 -1.38912206 -6.93900334 -7.7222725
-8.41592264 -5.613998 0.44396046 -3.07168078 -1.36478732
-1.20153628 -6.30209808 -6.49846303 -0.60518198 -3.83301464
-6.40455571 -0.22680504 0.54161373 -5.99626181 -5.98077412
-3.45857531 -2.50268554 -5.54970836 -9.26535525 -4.22097425
-0.47257602 -9.33187038 -4.97705346 -1.65256318 -1.0000177
-5.82202444 -8.34541689 -4.97060946 -0.34446784 -6.95722208
-7.41413036 -1.8343221 -7.19145712 -4.8082824 -4.59805445
-5.49449995 -2.25570223 -5.41145249 -5.97739476 -2.94240518
-3.64911853 -2.82208944 -3.34705766 -8.19712182 -7.57201089
-0.61670956 -6.3752957 -5.06738146 -2.54344987 -3.28382401
-5.9927353 -2.87730848 -3.58324503 -7.1488302 -2.63140119
-8.48092542 -4.91672751 -5.7488116 -3.80044426 -9.27859326
-2.475992 -6.06980518 -2.90059294 -5.22496057 -5.97575155
-6.18156775 -5.38363878 -7.41985155 -6.73241325 -4.43878791
-9.06614408 -1.69153658 -3.71141045 -3.19852116 -4.05473804
-3.45821856 -4.92039492 -6.55332449 -1.28332784 -4.17989583
-5.45916562 -3.80974949 -4.27838346 -5.31607024 -0.62628865
-2.21276478 -3.7397342 -6.66779473 -2.38116892 -2.83460004
-7.01238422 -2.75282445 -3.01759368 -6.14970454 -6.1300394
-7.58620719 -3.14051577 -5.82720807 -2.52236034 -7.03761018
-7.82753368 -8.8447092 -3.11218173 -4.22074847 -0.99624534
-3.45189404 -1.46956557 -9.42857926 -2.75093993 -0.61665367
-2.09370852 -9.34768018 -3.39876535 -5.8635608 -2.12987936
-8.40706474 -3.84209244 -0.5100329 -2.48836494 -1.54663048
-4.30920238 -5.73107193 -1.89978615 -6.17605033 -3.10487492
-5.51376743 -4.32751131 -8.20349197 -3.87477609 -1.78392197
-6.17403966 -6.52743333 -3.02302099 -4.99201913 -5.72548424
-7.83390422 -1.19722286 -4.59974076 -2.99496132 -6.83038116
-5.1842235 -0.78127198 -2.88907207 -3.95055581 -6.33003274
-4.47772201 -2.77425683 -4.44937971 -4.2292366 -1.15145162
-4.92325347 -5.40648383 -7.37247783 -4.65237446 -7.04281259
-0.69437244 -4.99227188 -3.02282976 -2.52532913 -6.52636286
-5.48318846 -3.71028837 -6.91757625 -5.54349414 -6.05345046
-0.43986605 -4.75951272 -1.82851406 -3.24432919 -7.20785221
-4.0583863 -3.27842271 -0.68706448 -2.76021537 -5.54119808
-4.08188794 -6.4244794 -4.76668274 -0.2040958 -2.42898945
-2.03283232 -4.12879797 -2.70459163 -6.04997273 -2.79280244
-4.20663028 0.786804 -3.65237777 -3.55179726 -5.3460864
-10.31959605 -6.69397854 -6.53784926 -7.56321471 -4.98085596
-1.79893146 -3.89513404 -5.18601688 -3.82352518 -5.20243998
-3.11707515 -5.80322513 -4.42380099 -5.74159836 -6.6468986
-3.18053496 -4.28898663 -6.73111304 -3.21485845 -4.79047586
-4.51550728 -2.70659984 -3.61545839 -7.86496861 -0.1258212
-7.6559803 -3.15269699 -2.87456418 -6.74876767 -0.42574712
-7.58877495 -5.30321115 -4.79881591 -4.5673199 -3.6865868
-4.46822682 -1.45060265 -0.53560561 -4.94874171 -1.26112294
-1.66779284 -5.57910033 -5.87103484 -3.35570045 -6.25661833
-1.51564145 0.85085628 -3.82725071 -1.47077448 -3.36154118
-5.37972404 -2.22844631 -2.78684422 -3.75603932 -1.85645
-3.33156093 -2.32968944 -5.06053069 -1.73410541 -1.68829408
-3.79892942 -1.62650712 -1.00001873 -6.07170511 -4.89697898
-3.66269926 -3.13731451 -5.08348781 -3.71891247 -2.09779606
-3.04082162 -5.12536015 -2.96071945 -4.28796395 -6.6231135
1.00003406 0.03907036 0.46718521 -0.3467975 0.32350521
0.47563771 1.10055427 -0.67580418 -0.46310299 0.40806733
1.17438632 -0.55152081 0.84476439 -0.91257798 0.63165546
-0.13845693 -0.22137683 1.20116183 1.18915628 -0.40676459
1.35964325 1.14038015 1.27914468 0.19329823 -0.16790648
-0.62775078 0.66095617 2.18236076 0.07018415 -0.26762451
-0.25529448 0.32084111 0.48016592 0.28189794 0.60568093
-1.07472716 -0.5088941 0.74892526 0.07203056 -0.10668727
-0.15662946 0.09611498 -0.39521586 -0.79874442 0.65613691
-0.39386485 -1.08601917 1.44693858 0.62992794 0.76536897]
(550,)
#生成的数据550行,1列
值得注意的是,在二分类过程中, decision_function只会生成一列距离,样本的类别由距离的符号来判断,但是 predict_proba会生成两个类别分别对应的概率。SVM也可以生成概率,所以我们可以使用和逻辑回归同样的方式来在SVM上设定和调节我们的阈值。
毋庸置疑,Platt缩放中涉及的交叉验证对于大型数据集来说非常昂贵,计算会非常缓慢。另外,由于Platt缩放的理论原因,在二分类过程中,有可能出现predict_proba返回的概率小于0.5,但样本依旧被标记为正类的情况出现, 毕竟支持向量机本身并不依赖于概率来完成自己的分类。如果我们的确需要置信度分数,但不一定非要是概率形式的话,那建议可以将probability设置为False,使用decision_function这个接口而不是predict_proba。
绘制SVM的ROC曲线
#开始绘图
recall = []
FPR = []
probrange = np.linspace(clf_proba.predict_proba(X)[:,1].min(),clf_proba.predict_proba(X)[:,1].max(),num=50,endpoint=False)
from sklearn.metrics import confusion_matrix as CM, recall_score as R
import matplotlib.pyplot as plot
for i in probrange:
y_predict = []
for j in range(X.shape[0]):
if clf_proba.predict_proba(X)[j,1] > i:
y_predict.append(1)
else:
y_predict.append(0)
cm = CM(y,y_predict,labels=[1,0])
recall.append(cm[0,0]/cm[0,:].sum())
FPR.append(cm[1,0]/cm[1,:].sum())
recall.sort()
FPR.sort()
plt.plot(FPR,recall,c="red")
plt.plot(probrange+0.05,probrange+0.05,c="black",linestyle="--")
plt.show()
现在我们就画出了ROC曲线了,那我们如何理解这条曲线呢?先来回忆一下,我们建立ROC曲线的根本目的是找寻Recall和FPR之间的平衡,让我们能够衡量模型在尽量捕捉少数类的时候,误伤多数类的情况会如何变化。横坐标是FPR,代表着模型将多数类判断错误的能力,纵坐标Recall,代表着模型捕捉少数类的能力,所以ROC曲线代表着,随着Recall的不断增加,FPR如何增加。我们希望随着Recall的不断提升,FPR增加得越慢越好,这说明我们可 以尽量高效地捕捉出少数类,而不会将很多地多数类判断错误。所以,我们希望看到的图像是,纵坐标急速上升, 横坐标缓慢增长,也就是在整个图像左上方的一条弧线。这代表模型的效果很不错,拥有较好的捕获少数类的能力。
中间的虚线代表着,当recall增加1%,我们的FPR也增加1%,也就是说,我们每捕捉出一个少数类,就会有一个多数类被判错,这种情况下,模型的效果就不好,这种模型捕获少数类的结果,会让许多多数类被误伤,从而增加我们的成本。ROC曲线通常都是凸型的。对于一条凸型ROC曲线来说,曲线越靠近左上角越好,越往下越糟糕,曲线如果在虚线的下方,则证明模型完全无法使用。但是它也有可能是一条凹形的ROC曲线。对于一条凹型ROC曲线来说,应该越靠近右下角越好,凹形曲线代表模型的预测结果与真实情况完全相反,那也不算非常糟糕,只要我们手动将模型的结果逆转,就可以得到一条左上方的弧线了。最糟糕的就是,无论曲线是凹形还是凸型,曲线位于图像中间,和虚线非常靠近,那我们拿它无能为力。
好了,现在我们有了这条曲线,我们的确知道模型的效果还算是不错了。但依然非常摸棱两可,有没有具体的数字来帮助我们理解ROC曲线和模型的效果呢?的确存在,这个数字就叫做AUC面积,它代表了ROC曲线下方的面积,这个面积越大,代表ROC曲线越接近左上角,模型就越好。AUC面积的计算比较繁琐,因此,我们使用sklearn来帮助我们。接下来我们来看看,在sklearn当中,如何绘制我们的ROC曲线,找出我们的的AUC面积。
利用ROC曲线找出最佳阈值
在sklearn中,我们有帮助我们计算ROC曲线的横坐标假正率FPR,纵坐标Recall和对应的阈值的类sklearn.metrics.roc_curve。同时有帮助我们计算AUC面积的类sklearn.metrics.roc_auc_score。在一些比较老旧的sklearn版本中,我们使用sklearn.metrics.auc这个类来计算AUC面积,但这个类即将在0.22版本中被放弃,因此建议大家都使用roc_auc_score,来看看我们的这两个类:
置信度,概率作为y_score都可
from sklearn.metrics import roc_curve
FPR, recall, thresholds = roc_curve(y,clf_proba.decision_function(X), pos_label=1)
print(FPR)
print(recall)
print(thresholds)
结果:
[0. 0. 0.006 0.006 0.008 0.008 0.01 0.01 0.014 0.014 0.018 0.018
0.022 0.022 0.024 0.024 0.028 0.028 0.03 0.03 0.032 0.032 0.036 0.036
0.04 0.04 0.042 0.042 0.044 0.044 0.05 0.05 0.054 0.054 0.058 0.058
0.066 0.066 0.072 0.072 0.074 0.074 0.086 0.086 1. ]
[0. 0.02 0.02 0.06 0.06 0.16 0.16 0.2 0.2 0.22 0.22 0.36 0.36 0.42
0.42 0.6 0.6 0.62 0.62 0.64 0.64 0.68 0.68 0.7 0.7 0.74 0.74 0.76
0.76 0.82 0.82 0.84 0.84 0.86 0.86 0.88 0.88 0.92 0.92 0.94 0.94 0.96
0.96 1. 1. ]
[ 3.18236076 2.18236076 1.48676267 1.35964325 1.33920817
1.14038015 1.13383091 1.00003406 0.85085628 0.84476439
0.78571364 0.60568093 0.5389064 0.46718521 0.44396046
0.03907036 -0.07011269 -0.10668727 -0.1258212 -0.13845693
-0.14034183 -0.16790648 -0.2040958 -0.22137683 -0.24381463
-0.26762451 -0.34446784 -0.3467975 -0.39182241 -0.40676459
-0.4589064 -0.46310299 -0.49195707 -0.5088941 -0.53560561
-0.55152081 -0.62628865 -0.67580418 -0.78127198 -0.79874442
-0.88438995 -0.91257798 -1.01417607 -1.08601917 -10.31959605]
from sklearn.metrics import roc_auc_score as AUC
area = AUC(y,clf_proba.decision_function(X))
area
结果:
0.9696400000000001
plt.figure()
plt.plot(FPR, recall, color='red',label='ROC curve (area = %0.2f)' % area)
plt.plot([0, 1], [0, 1], color='black', linestyle='--')
plt.xlim([-0.05, 1.05])
plt.ylim([-0.05, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('Recall')
plt.title('Receiver operating characteristic example')
plt.legend(loc="lower right")
plt.show()
结果:
现在,有了ROC曲线,了解了模型的分类效力,以及面对样本不均衡问题时的效力,那我们如何求解我们最佳的阈值呢?我们想要了解,什么样的状况下我们的模型的效果才是最好的。回到我们对ROC曲线的理解来:ROC曲线反应的是recall增加的时候FPR如何变化,也就是当模型捕获少数类的能力变强的时候,会误伤多数类的情况是否严重。我们的希望是,模型在捕获少数类的能力变强的时候,尽量不误伤多数类,也就是说,随着recall的变大,FPR 的大小越小越好。所以我们希望找到的最有点,其实是Recall和FPR差距最大的点。这个点,又叫做约登指数。
maxindex = (recall - FPR).tolist().index(max(recall - FPR))
print(thresholds[maxindex])
#我们可以在图像上来看看这个点在哪里
plt.scatter(FPR[maxindex],recall[maxindex],c="black",s=30)
#把上述代码放入这段代码中:
plt.figure()
plt.plot(FPR, recall, color='red',label='ROC curve (area = %0.2f)' % area)
plt.plot([0, 1], [0, 1], color='black', linestyle='--')
plt.scatter(FPR[maxindex],recall[maxindex],c="black",s=30)
plt.xlim([-0.05, 1.05])
plt.ylim([-0.05, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('Recall')
plt.title('Receiver operating characteristic example')
plt.legend(loc="lower right")
结果:
最佳阈值就这样选取出来了,由于现在我们是使用decision_function来画ROC曲线,所以我们选择出来的最佳阈值其实是最佳距离。如果我们使用的是概率,我们选取的最佳阈值就会使一个概率值了。只要我们让这个距离/概率以上的点,都为正类,让这个距离/概率以下的点都为负类,模型就是最好的:即能够捕捉出少数类,又能够尽量不误伤多数类,整体的精确性和对少数类的捕捉都得到了保证。
而从找出的最优阈值点来看,这个点,其实是图像上离左上角最近的点,离中间的虚线最远的点,也是ROC曲线的转折点。如果没有时间进行计算,或者横坐标比较清晰的时候,我们就可以观察转折点来找到我们的最佳阈值。
到这里为止,SVC的模型评估指标就介绍完毕了。但是,SVC的样本不均衡问题还可以有很多的探索。另外,我们还可以使用KS曲线,或者收益曲线(profit chart)来选择我们的阈值,都是和ROC曲线类似的用法。大家若有余力,可以自己深入研究一下。模型评估指标,还有很多深奥的地方。