目录
1 二分类SVC的样本不均衡问题
1.1 样本不平衡定义
1.2 解决方法
1.2.1 SVC的参数class_weight
1.2.2 SVC的接口fit的参数:sample_weight
1.3 实例
1.3.1 导入需要的库和模块
1.3.2 创建样本不均衡的数据集
1.3.3 在数据集上分别建模
1.3.4 绘制两个模型下数据的决策边界
1.3.5 结论
2 SVC的模型评估指标
2.1 混淆矩阵(Confusion Matrix)
2.2 精确度、召回率、F1 score
2.2.1 精确度
2.2.2 召回率
2.2.3 F1 score
2.3 Sklearn中的混淆矩阵
2.4 ROC曲线及相关问题
2.4.1 概率与阈值
2.4.2 SVM实现概率预测:重要参数probility ,接口predict_function以及decision_function
对于分类问题,永远都逃不过的一个痛点就是样本不均衡问题。样本不均衡是指在一组数据集中,标签的一类天生 占有很大的比例,但我们有着捕捉出某种特定的分类的需求的状况。比如,我们现在要对潜在犯罪者和普通人进行 分类,潜在犯罪者占总人口的比例是相当低的,也许只有2%左右,98%的人都是普通人,而我们的目标是要捕获 出潜在犯罪者。这样的标签分布会带来许多问题。
我们在逻辑回归中已经介绍了一些基本方法,比如上采样下采样。但这些采样方法会增加样本 的总数,对于支持向量机这个样本总是对计算速度影响巨大的算法来说,我们完全不想轻易地增加样本数量。况 且,支持向量机中地决策仅仅决策边界的影响,而决策边界又仅仅受到参数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来调整的。
可输入字典或者"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))
数组,结构为 (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 matplotlib.colors import ListedColormap
from sklearn import svm
from sklearn.datasets import make_circles, make_moons, make_blobs,make_classification
class_1 = 500 #类别1有500个样本,10:1
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)
plt.show()
#其中红色点是少数类,紫色点是多数类
#不设定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)
clf.score(X,y)
wclf.score(X,y)
0.9127272727272727
.collections #调用这个等高线对象中画的所有线,返回一个惰性对象用[*]把它打开试试看
[*a.collections] #返回了一个linecollection对象,其实就是我们等高线里所有的线的列表
现在我们只有一条线,所以我们可以使用索引0来锁定这个对象——a.collections[0]
plt.legend([对象列表],[图例列表],loc)
只要对象列表和图例列表相对应,就可以显示出图例
#首先要有数据分布
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设置后的模型。
单纯地追求捕捉出少数类,就会成本太高,而不顾及少数类,又会无法达成模型的效果。所以在现实 中,我们往往在寻找捕获少数类的能力和将多数类判错后需要付出的成本的平衡。
混淆矩阵是二分类问题的多维衡量指标体系,在样本不平衡时极其有用。在混淆矩阵中,我们将少数类认为是正例,多数类认为是负例。在决策树,随机森林这些普通的分类算法里,即是说少数类是1,多数类是0。在SVM里, 就是说少数类是1,多数类是-1。普通的混淆矩阵,一般使用{0,1}来表示。混淆矩阵阵如其名,十分容易让人混淆,在许多教材中,混淆矩阵中各种各样的名称和定义让大家难以理解难以记忆。我为大家找出了一种简化的方式,如下图:
真实值写在前面,预测值写在后面。11和00 的对角线就是全部预测正确的。基于混淆矩阵,我们有六个不同的模型评估指标,这些评估指标的范围都在 [0,1]之间,所有以11和00为分子的指标都是越接近1越好,所以以01和10为分子的指标都是越接近0越好。对于所 有的指标,我们用橙色表示分母,用绿色表示分子。
精确度Precision,又叫查准率,表示所有被我们预测为是少数类的样本中,真正的少数类所占的比例。在支持向 量机中,精确度可以被形象地表示为决策边界上方的所有点中,红色点所占的比例。精确度越高,代表我们捕捉正 确的红色点越多,对少数类的预测越精确。精确度越低,则代表我们误伤了过多的多数类。精确度是”将多数类判 错后所需付出成本“的衡量。
y == clf.predict(X)真实值等于预测值的全部点,决策边界上方红色点,决策边界下方的紫色点。返回的是T和F
未平衡过:
#所有判断正确并确实为1的样本 / 所有被判断为1的样本
#对于没有class_weight,没有做样本平衡的灰色决策边界来说:
(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.5102040816326531降低了
召回率Recall,又被称为敏感度(sensitivity),真正率,查全率,表示所有真实为1的样本中,被我们预测正确的样 本所占的比例。在支持向量机中,召回率可以被表示为,决策边界上方的所有红色点占全部样本中的红色点的比 例。召回率越高,代表我们尽量捕捉出了越多的少数类,召回率越低,代表我们没有捕捉出足够的少数类。
未平衡:
#所有predict为1的点 / 全部为1的点的比例
#对于没有class_weight,没有做样本平衡的灰色决策边界来说:
(y[y == clf.predict(X)] == 1).sum()/(y == 1).sum()
0.6
平衡过:
#对于有class_weight,做了样本平衡的红色决策边界来说:
(y[y == wclf.predict(X)] == 1).sum()/(y == 1).sum()
1.0
从Recall延申出来的另一个评估指标叫做假负率(False Negative Rate),它等于 1 - Recall,用于衡量所有真实 为1的样本中,被我们错误判断为0的,通常用得不多。
可以看出,做样本平衡之前,我们只成功捕获了60%左右的少数类点,而做了样本平衡之后的模型,捕捉出了 100%的少数类点,从图像上来看,我们的红色决策边界的确捕捉出了全部的少数类,而灰色决策边界只捕捉到了 一半左右。召回率可以帮助我们判断,我们是否捕捉除了全部的少数类,所以又叫做查全率。 如果我们希望不计一切代价,找出少数类(比如找出潜在犯罪者的例子),那我们就会追求高召回率,相反如果我 们的目标不是尽量捕获少数类,那我们就不需要在意召回率。 注意召回率和精确度的分子是相同的(都是11),只是分母不同。而召回率和精确度是此消彼长的,两者之间的平 衡代表了捕捉少数类的需求和尽量不要误伤多数类的需求的平衡。究竟要偏向于哪一方,取决于我们的业务需求: 究竟是误伤多数类的成本更高,还是无法捕捉少数类的代价更高。 为了同时兼顾精确度和召回率,我们创造了两者的调和平均数作为考量两者平衡的综合性指标,称之为F1 measure。两个数之间的调和平均倾向于靠近两个数中比较小的那一个数,因此我们追求尽量高的F1 measure, 能够保证我们的精确度和召回率都比较高。F1 measure在[0,1]之间分布,越接近1越好。
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为纵坐标的曲线。让我们先从概率和阈值开始讲起。
要理解概率与阈值,最容易的状况是来回忆一下我们用逻辑回归做分类的时候的状况。逻辑回归的predict_proba 接口对每个样本生成每个标签类别下的似然(类概率)。对于这些似然,逻辑回归天然规定,当一个样本所对应的 这个标签类别下的似然大于0.5的时候,这个样本就被分为这一类。比如说,一个样本在标签1下的似然是0.6,在 标签0下的似然是0.4,则这个样本的标签自然就被分为1。逻辑回归的回归值本身,其实也就是标签1下的似然。在 这个过程中,0.5就被称为阈值。来看看下面的例子:
1 自建数据集
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)
plt.show()
2 建模、调用概率
from sklearn.linear_model import LogisticRegression as LogiR
clf_lo = LogiR().fit(X_,y_)
prob = clf_lo.predict_proba(X_)
prob.shape#将样本和概率放到一个DataFrame中
import pandas as pd
prob = pd.DataFrame(prob)
prob.columns = ["0","1"]
3 使用阈值0.5,大于0.5的样本被预测为1,小于0.5的样本被预测为0
#手动调节阈值,来改变我们的模型效果
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)
4 使用混淆矩阵查看结果
rom 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])
#试试看手动计算Precision和Recall?
2/3
0.5
P(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0])
R(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0])
混淆矩阵结果:array([[2, 2], [1, 6]], dtype=int64)
用P和R计算和手动计算结果是一样的。
5 假如我们使用0.4作为阈值呢?
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
CM(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0])
P(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0])
R(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels=[1,0])
array([[4, 0], [2, 5]], dtype=int64)
注意,降低或者升高阈值并不一定能够让模型的效果变好,一切都基于我们要追求怎样的模型效果 通常来说,降低阈值能够升高Recall
我们在画等高线,也就是决策边界的时候曾经使用SVC的接口decision_function,它返回我们输入的特征矩阵中每 个样本到划分数据集的超平面的距离。我们在SVM中利用超平面来判断我们的样本,本质上来说,当两个点的距离 是相同的符号的时候,越远离超平面的样本点归属于某个标签类的概率就很大。比如说,一个距离超平面0.1的 点,和一个距离超平面100的点,明显是距离为0.1的点更有可能是负类别的点混入了边界。同理,一个距离超平面 距离为-0.1的点,和一个离超平面距离为-100的点,明显是-100的点的标签更有可能是负类。所以,到超平面的距 离一定程度上反应了样本归属于某个标签类的可能性。接口decision_function返回的值也因此被我们认为是SVM的置信度。
clf_proba = svm.SVC(kernel="linear",C=1.0,probability=True).fit(X,y)
clf_proba.predict_proba(X).shape #生成的各类标签下的概率
clf_proba.predict_proba(X).shape
clf_proba.decision_function(X)
clf_proba.decision_function(X).shape