sklearn中的支持向量机SVM(下)

1 二分类SVC的进阶

1.1 SVC用于二分类的原理复习

sklearn中的支持向量机SVM(上)

1.2 参数C的理解进阶

有一些数据,可能是线性可分的,但在线性可分状况下训练准确率不能达到100%,即无法让训练误差为0。这种数据被称为“存在软间隔的数据”。这时需要决策边界能够忍受一小部分训练误差,而不能单纯地寻求最大边际。
因为对于软间隔的数据来说,边际越大被分错的样本也就会越多,因此需要找出一个“最大边际”与“被分错的样本数量”之间的平衡。因此,引入松弛系数 ζ \zeta ζ和松弛系数的系数 C C C作为一个惩罚项,来惩罚对最大边际的追求。
**参数 C C C如何影响决策边界?**在硬间隔时,决策边界完全由两个支持向量和最小化损失函数(最大化边际)来决定,而支持向量是两个标签类别不一致的点,即分别是正样本和负样本。然而在软间隔时,边际依然由支持向量决定,但此时支持向量可能就不再是来自两种标签类别的点了,而是分布在决策边界两边的同类别的点。
在这里插入图片描述
此时虚线超平面 w ⋅ x i + b = 1 − ζ i \textbf{w}\cdot\textbf{x}_i+b=1-\zeta_i wxi+b=1ζi是由混杂在红色点中间的紫色点所决定的,因此这个紫色点就是现在的支持向量。所以软间隔让决定两条虚线超平面的支持向量可能是来自于同一个类别的样本点,而硬间隔时两条虚线超平面必须是由来自两个不同类别的支持向量所决定的。而C值会决定究竟是依赖红色点作为支持向量(只追求最大边界),还是要依赖软间隔中混杂在红色点中的紫色点来作为支持向量(追求最大边界和判断正确的平衡)。如果C值设定比较大,那么SVC可能会选择边际较小的,能够更好地分类所有训练样本点的决策边界,不过模型的训练时间也会更长。如果C的设定值较小,那么SVC会尽量最大化边际,尽量将掉落在决策边界一方的样本点预测正确,决策功能会更简单,但代价是训练的准确度,因为此时会有更多红色点被分类错误。换句话说,C在SVM中的影响就像正则化参数对逻辑回归的影响。
所有可能影响超平面的样本都可能会被定义为支持向量,所以支持向量就不再是所有压在虚线超平面上的点,而是所有可能影响超平面位置的那些混杂在彼此类别中的点。

1.3 二分类SVC中的样本不均衡问题:重要参数class_weight

样本不均衡会带来很多问题。**首先,分类模型天生会倾向于多数的类,让多数类更容易被判断正确,少数类被牺牲掉。**因为对于模型而言,样本量越大的标签可以学到的信息越多,算法就会更加依赖于从多数类中学到的信息来进行判断。如果希望捕获少数类,模型就会失效。其次,模型评估指标会失去意义。这种分类状况下,即便模型什么也不做,把全部样本都分到多数类中,准确率也能非常高,这使得模型评估指标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 i i的参数C设置为class_weight[i] ∗ * C,如果没有给出具体的class-weight,则所有类都被假设为占有相同的权重1,模型会根据数据原本的状况去训练。如果希望改善样本不均衡状况,需要输入形如{“标签的值1”:权重1,“标签的值2”:权重2}的字典,则参数C将会自动被设为:(标签的值1的C:权重1C,标签的值2的C:权重2C)。
    或者,可以使用“balanced”模式,这个模式使用y的值自动调整与输入数据中的类频率成反比的权重为n_samples/(n_classes*np.bincount(y))。
  • SVC的接口fit的参数:sample_weight
    数组,结构为(n_samples,),必须对应输入fit中的特征矩阵的每个样本。每个样本在fit时的权重,让权重 ∗ * 每个样本对应的C值来迫使分类器强调设定的权重更大的样本。通常,较大的权重加在少数类的样本上,以迫使模型向着少数类的方向建模。
    通常来说,class_weight和sample_weight,这两个参数只选取一个来设置。如果同时设置了两个参数,则C会同时受到两个参数的影响,即class_weight中设定的权重 ∗ * sample_weight中设定的权重 ∗ * C。
    接下来看看如何使用class_weight参数。首先自建一组样本不平衡的数据集,在这组数据集上建两个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,s=10,cmap = "rainbow")
#其中红色点事少数类,紫色点是多数类

sklearn中的支持向量机SVM(下)_第1张图片

#在数据集上分别建模
#不设定class_weight
clf = svm.SVC(kernel = "linear",C=1.0)
clf.fit(x,y)
#结果:SVC(kernel='linear')
#设定class_weight
wclf = svm.SVC(kernel = "linear",class_weight = {1:10})
wclf.fit(x,y)
#结果:SVC(class_weight={1: 10}, kernel='linear')
#给两个模型分别打分看看,分数是accuracy准确度
clf.score(x,y)
#结果:0.9418181818181818
wclf.score(x,y)
#结果: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()

#图例中的详细说明
a.collections#调用这个等高线对象中的画的
#结果:
#用{*}把它打开看看
[*a.collections]#返回了一个linecollection对象,其实就是等高线里所有的线的列表
#结果:[]
#现在只有一条线,所以可以使用索引0来锁定这个对象
a.collections[0]
#结果:
#plt.legend([对象列表],[图例列表],loc)
#只要对象列表和图例列表相对应,就可以显示出图例

sklearn中的支持向量机SVM(下)_第2张图片
从图像上可以看出,灰色是做样本平衡之前的决策边界,灰色线上方的点被分为一类,下方的点被分为另一类。可以看到,大约有一半少数类(红色)被分错,多数类(紫色点)几乎都被分类正确了。红色是做样本平衡之后的决策边界,同样红色线上方一类,红色线下方一类,可以看到,做了样本平衡后,少数类几乎全部都被分类正确了,但是多数类有许多被分错了。
但从准确率的角度来看,不做样本平衡时准确率反而更高,做了样本平衡准确率反而变低了,这是因为做了样本平衡后,为了要更有效的捕捉少数类,模型误伤了许多多数类样本,而多数类被分错的样本数量大于少数类被分类正确的样本数量,使得模型整体的准确率下降。如果目的是模型整体的准确率,那就要拒绝样本平衡,使用class_weight被设置之前的模型。
然而在现实中,往往都是在追求捕捉少数类,因为在很多情况下,将少数类判断错的代价是巨大的。希望不惜一切代价来捕获少数类,或者希望捕捉出尽量多的少数类,那就必须使用class_weight设置后的模型。

2 SVC的模型评价指标

如果目标是尽量捕获少数类,那么准确率这个模型评估指标就逐渐失效了,需要新的模型评估指标。简单来看,只需要查看模型在少数类上的准确率就好,只要能够将少数类尽量捕捉出来,就能够达到目的。但问题是对多数类判断错误后,会需要人工甄别或者更多的业务上的措施来一一排除判断错误的多数类,这种行为往往伴随着很高的成本。
也就是说,单纯地追求捕捉出少数类,就会成本太高,而不顾及少数类,又会无法达成模型的效果。所以在现实中,往往在寻找捕获少数类的能力将多数类判错后需要付出的成本的平衡。如果一个模型在能够尽量捕获少数类的情况下,还能够尽量对多数类判断正确,则这个模型就非常优秀了。为了评估这样的能力,将引入新的模型评估指标:混淆矩阵和ROC曲线。

2.1 混淆矩阵(Confusion Matrix)

混淆矩阵是二分类问题的多维衡量指标体系,在样本不平衡时极其有用。在混淆矩阵中,将少数类认为是正例,多数类认为是负例。在决策树、随机森林这些普通的分类算法里,即是说少数类是1,多数类是0。在SVM中,就是认为少数类是1,多数类是-1。普通的混淆矩阵,一般使用{0,1}来表示。
sklearn中的支持向量机SVM(下)_第3张图片
混淆矩阵中,永远是真实值在前,预测值在后。很容易看出,11和00的对角线就是全部预测正确的,01和10的对角线就是全部预测错误的。基于混淆矩阵,有六个不同的模型评估指标,这些评估指标的范围都在[0,1]之间,所有以11和00为分子的指标都是越接近1越好,所有以01和10为分析的指标都是越接近0越好。对于所有的指标,用橙色表示分母,用绿色表示分子。

2.1.1 模型整体效果:准确率(Accuracy)

sklearn中的支持向量机SVM(下)_第4张图片
A c c u r a c y = 11 + 00 11 + 10 + 01 + 00 Accuracy = \frac{11+00}{11+10+01+00} Accuracy=11+10+01+0011+00
准确率Accuracy就是所有预测正确的所有样本除以总样本,通常来说越接近1越好。

2.1.2 捕捉少数类的艺术:精确度(Precision)、召回率(Recall)和F1 score

sklearn中的支持向量机SVM(下)_第5张图片
P r e c i s i o n = 11 11 + 01 Precision = \frac{11}{11+01} Precision=11+0111
精确度Precision,又叫查准率,表示所有被预测为少数类的样本中,真正的少数类所占的比例。在支持向量机中,精确度又可以被形象地表示为决策边界上方的所有点中,红色点所占的比例。精确度越高,代表捕捉正确的红色点越多,对少数类的预测越精确。精确度越低,则代表误伤了过多的多数类。精确度是“将多数类判错后所需付出成本”的衡量。

#所有判断正确并确实为1的样本/所有被判断为1的样本
#对于没有class_weight,没有做样本平衡的灰色决策边界来说
(y[y == clf.predict(x)] == 1).sum()/(clf.predict(x) == 1).sum()
#结果:0.7142857142857143
#对于有class_weight,做了样本平衡的红色决策边界来说
(y[y == wclf.predict(x)] == 1).sum()/(wclf.predict(x) == 1).sum()
#结果:0.5102040816326531

可以看到,做了样本平衡之后,精确度是下降的。因为很明显,样本平衡之后,有更多的多数类紫色点被误判了。精确度可以帮助我们判断,是否每一次对少数类的预测都精确,所以又被称为“查准率”。在现实的样本不平衡问题中,当每一次将多数类判断错误的成本非常高昂的时候,会追求高精确度。精确度越低,对多数类的判断就会越错误。当然,如果目标是不计一切代价捕获少数类,那并不需要在意精确度。
sklearn中的支持向量机SVM(下)_第6张图片
R e c a l l = 11 11 + 10 Recall = \frac{11}{11+10} Recall=11+1011
召回率Recall,又被称为敏感度(sensitivity)、真正率、查全率,表示所有真实为少数类的样本中,被预测正确的样本所占的比例。在支持向量机中,召回率可以被表示为,决策边界上方的所有红色点占全部样本中的红色点的比例。召回率越高,代表尽量捕捉出越多的少数类,召回率越低,代表没有捕捉出足够的少数类。

#所有predict为1且确实为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

可以看出,做样本平衡之前,只成功捕捉了60%左右的少数类点,而做了样本平衡之后的模型捕捉出了100%的少数类点。从图像上来看,红色决策边界的确捕捉出了全部的少数类,而灰色决策边界只捕捉到了一半左右。召回率可以帮助判断,是否捕捉出了全部的少数类,所以又叫做查全率。如果希望不计一切代价,找出少数类,那么就会追求高召回率,相反如果目标不是尽量捕获少数类,则不需要在意召回率。
注意召回率和精确度的分子是相同的,只是分母不同,而召回率和精确度是此消彼长的,两者之间的平衡代表了捕获少数类的需求和尽量不要误判多数类的需求的平衡。究竟要偏向于哪一方,取决于业务需求:究竟是误伤多数类的成本更高,还是没有捕获少数类的代价更高。
为了同时兼顾精确度和召回率,创造了两者的调和平均数作为考量两者平衡的综合性指标,称之为F1 score。两个数之间的调和平均倾向于靠近两个数中比较小的那一个数。因此追求尽量高的F1 score,能够保证精确度和召回率都比较高。F1 score在[0,1]之间分布,越接近1越好。
F 1   s c o r e = 2 1 P r e c i s i o n + 1 R e c a l l = 2 ∗ P r e c i s i o n ∗ R e c a l l P r e c i s i o n + R e c a l l F1\ score=\frac{2}{\frac{1}{Precision}+\frac{1}{Recall}}=\frac{2*Precision*Recall}{Precision+Recall} F1 score=Precision1+Recall12=Precision+Recall2PrecisionRecall
从Recall延出来的另一个评估指标称为假负率(False Negative Rate),它等于1-Recall,用于衡量所有真实为1的样本中,被误判为0的,通常用得不多。
F N R = 10 11 + 10 FNR = \frac{10}{11+10} FNR=11+1010

2.1.3 判错多数类的考量:特异度(Specificity)与假正lv

sklearn中的支持向量机SVM(下)_第7张图片
S p e c i f i c i t y = 00 01 + 00 Specificity=\frac{00}{01+00} Specificity=01+0000
**特异度(Specificity)**表示所有真实为多数类的样本中,被正确预测为多数类的样本所占的比例。在支持向量机中,可以形象地表示为,决策边界下方的点占所有紫色点的比例。

#所有确实为0且被正确预测为0的样本/所有确实为0的样本
##对于没有class_weight,没有做样本平衡的灰色决策边界来说
(y[y == clf.predict(x)] == 0).sum()/(y == 0).sum()
#结果:0.976
#对于有class_weight,做了样本平衡的红色决策边界来说
(y[y == wclf.predict(x)] == 0).sum()/(y == 0).sum()
#结果:0.904

特异度衡量了一个模型将多数类判断正确的能力,而1-specificity就是一个模型将多数类判断错误的能力,这种能力被计算如下,并被称为假正率(False Positive Rate):
sklearn中的支持向量机SVM(下)_第8张图片
F P R = 01 01 + 00 FPR=\frac{01}{01+00} FPR=01+0001
在支持向量机中,假正率就是决策边界上方的紫色点(所有被判断错误的多数类)占所有紫色点的比例。根据对precision的分析,可以看出,当样本均衡过后,假正率会更高,因为有更多紫色点被判断错误,而样本均衡之前,假正率比较低,被判错的紫色点比较少。所以假正率其实类似于precision的反向指标,precision衡量有多少少数点被判断正确,而假正率FPR衡量有多少多数点被判断错误,性质是十分类似的。

2.1.4 sklearn中的混淆矩阵

sklearn中提供了大量的类来帮助了解和使用混淆矩阵。

含义
sklearn.metrics.confusion_matrix 混淆矩阵
sklearn.metrics.accuracy_score 准确率accuracy
sklearn.metrics.precision_score 精确度precision
sklearn.metrics.recall_score 召回率recall
sklearn.metrics.precision_recall_score 精确度-召回率平衡曲线,可以展示不同阈值下的精确度和召回率如何变化
sklearn.metrics.f1_score F1 score

2.2 ROC曲线以及相关问题

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

2.2.1 概率(probability)与阈值(threshold)

#自建数据集
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)

sklearn中的支持向量机SVM(下)_第9张图片

#建模,调用概率
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

sklearn中的支持向量机SVM(下)_第10张图片

#手动调节阈值,来改变模型效果
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

sklearn中的支持向量机SVM(下)_第11张图片

#使用混淆矩阵查看结果
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([[0, 4],
#	         [2, 5]], dtype=int64)
#试着手动计算Precision和Recall
P(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels = [1,0])#labels中1是少数类,必须真实值在前,少数类在前
#结果:0.0
R(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels = [1,0])
#结果:0.0

#假如使用0.2作为阈值
for i in range(prob.shape[0]):
    if prob.loc[i,"1"] > 0.2:
        prob.loc[i,"pred"] = 1
    else:
        prob.loc[i,"pred"] = 0
CM(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels = [1,0])
#结果:array([[3, 1],
#	         [7, 0]], dtype=int64)
P(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels = [1,0])
#结果:0.3
R(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels = [1,0])
#结果:0.75
#注意,降低或者升高阈值并不一定能够让模型的效果更好,一切都基于要追求怎样的模型效果

可见,在不同阈值下,模型评估指标会发生变化,利用这一点来观察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也可以生成概率。

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

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

参数 含义
probability 布尔值,可不填,默认为False。是否启用概率估计,必须在调用fit之前启用它,启用此功能会减慢SVM的运算速度。设置为True则会启动,启用之后,SVC的接口predict_proba和predict_log_proba将生效。在二分类情况下,SVC将使用Platt缩放来生成概率,即在decision_function生成的距离上进行Sigmoid压缩,并附加训练数据的交叉验证拟合,来生成类逻辑回归的SVM分数。在多分类状况下,参考Wu et al.(2004)发表的文章Probability estimates for multi-class classification by pairwise coupling来将二分类情况推广到多分类。
#实现概率预测
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
from sklearn import svm
#使用最初的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,s=10,cmap = "rainbow")
#其中红色点是少数类,紫色点是多数类
plt.show()

sklearn中的支持向量机SVM(下)_第12张图片

clf_proba = svm.SVC(kernel = "linear",C = 1.0,probability = True).fit(x,y)
clf_proba.predict_proba(x)
#结果:
#array([[0.68503883, 0.31496117],
#       [0.2732528 , 0.7267472 ],
#       [0.95940537, 0.04059463],
#       ...,
#       [0.1655047 , 0.8344953 ],
#       [0.36213703, 0.63786297],
#       [0.32525276, 0.67474724]])
clf_proba.predict_proba(x).shape#生成各类标签下的概率
#结果:(550, 2)
clf_proba.decision_function(x)
clf_proba.decision_function(x).shape#生成到决策边界的距离
#结果:(550,)

值得注意的是,在二分类过程中,decision_function只会生成一列距离,样本的类别由距离的符号来判断,但是predict_proba会生成两个类别分别对应的概率。SVM也可以生成概率,所以可以使用和逻辑回归同样的方式来在SVM上设定和调节阈值。
毋庸置疑,Platt缩放中涉及的交叉验证对于大型数据集来说非常昂贵,计算会非常缓慢。另外,由于Platt缩放的理论原因,在二分类过程中,有可能出现predict_proba返回的概率小于0.05,但样本依旧被标记为正类的情况出现,毕竟支持向量机本身并不依赖于概率来完成自己的分类。如果的确需要置信度分数,但不一定非要是概率形式的话,那建议可以将probability设置为False,使用decision_function这个接口,而不是predict_proba。

2.2.3 绘制SVM的ROC曲线

ROC是一条以不同阈值下的假正率FPR为横坐标,不同阈值下的召回率Recall为纵坐标的曲线。简单来说,只要有数据和模型,就可以在python中绘制出ROC曲线。要绘制ROC曲线,就必须在数据中不断调节阈值,不断计算混淆矩阵,然后不断获得横坐标(FPR)与纵坐标(Recall),最后才能将曲线绘制出来。

#建模,调用概率
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"]
#手动调节阈值,来改变模型效果
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

#首先看看如何从混淆矩阵中获取FPR和Recall
from sklearn.metrics import confusion_matrix as CM, precision_score as P, recall_score as R
cm = CM(prob.loc[:,"y_true"],prob.loc[:,"pred"],labels = [1,0])
cm
#结果:array([[2, 2],
#            [0, 7]], dtype=int64)
#FPR
cm[1,0]/cm[1,:].sum()
#结果:0.0
#Recall
cm[0,0]/cm[0,:].sum()
#结果:0.5
#开始绘图
from sklearn.metrics import confusion_matrix as CM,recall_score as R
import matplotlib.pyplot as plot
import numpy as np
recall = []
FPR = []
probrange = np.linspace(clf_proba.predict_proba(x)[:,1].min()
                        #概率clf_proba.predict_proba(x)[:,1]是样本属于类别1的概率
                       ,clf_proba.predict_proba(x)[:,1].max()
                       ,num = 50
                       ,endpoint = False)
#阈值,每一个阈值都对应着一次循环,每一次循环,都要有一个混淆矩阵,要有一组假正率 vs. Recall
#np.linspace(概率最小值,概率最大值,number,endpoint = False),概率最小值和概率最大值都可以取到
#endpoint = False表示不要取到最大值

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

sklearn中的支持向量机SVM(下)_第13张图片
如何理解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绘制ROC曲线。

2.2.4 sklearn中的ROC曲线和AUC面积

在sklearn中,有计算ROC曲线的横坐标假正率FPR、纵坐标Recall以及对应的阈值的类sklearn.metrics.roc_curve。同时,还有计算AUC面积的类sklearn.metrics.roc_auc_score。在一些老旧的sklearn版本中,可以使用sklearn.metrics.auc这个类来计算AUC面积。建议都使用roc_auc_score。

  • sklearn.metrics.roc_curve(y_true,y_score,pos_label = None,sample_weight = None,drop_intermediate = True)
参数 说明
y_true 数组,形状=[n_samples],真实标签
y_score 数组,形状=[n_samples],置信度分数,可以是正类样本的概率值,或置信度分数,或者decision_function返回的距离
pos_label 整数或者字符串,默认None,表示被认为是正类样本的类别
sample_weight 形如[n_samples]的类数组结构,可不填,表示样本的权重
drop_intermediate 布尔值,默认True,如果设置为True,表示会舍弃一些ROC曲线上不显示的阈值点,这对于计算一个比较轻量的ROC曲线来说非常有用

返回值:FPR、Recall和阈值

from sklearn.metrics import roc_curve
FPR,recall,thresholds = roc_curve(y,clf_proba.decision_function(x),pos_label = 1)
FPR
#结果:
#array([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.   ])
recall
#结果:
#array([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.  ])
thresholds#decision_function
#结果:
#array([  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])
  • sklearn.metrics.roc_auc_score(y_true,y_score,average = “macro”,sample_weight = None,max_fpr = None)
    AUC面积的分数使用以上类来进行计算,输入的参数也比较简单,就是真实标签,与roc_curve中一致的置信度分数或者概率值。
from sklearn.metrics import roc_auc_score as AUC
AUC(y,clf_proba.decision_function(x))
#结果:0.9696400000000001

#绘制图像
area = AUC(y,clf_proba.decision_function(x))
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()

sklearn中的支持向量机SVM(下)_第14张图片
如上就可以得到ROC曲线和AUC面积,可以看到,SVM在这个数据集上的效果很好。并且可以通过观察使用的decision_function画出的ROC曲线,对比之前使用概率画出的曲线,两者非常相似,因此在无法获取模型概率的情况下,不必强行使用概率。如果有置信度,也可以使用它绘制ROC曲线。

2.2.5 利用ROC曲线找出最佳阈值

什么状况下模型的效果最好? ROC曲线反映的是recall增加的时候FPR如何变化,也就是当模型捕获少数类的能力变强的时候,会误判多数类的情况是否严重。希望模型在捕获少数类的能力变强的时候,尽量不误判多数类,也就是说,随着recall的变大,FPR的大小越小越好。因此,希望找到的最优点其实是Recall和FPR差距最大的点。这个点也被称为约登指数

maxindex = (recall - FPR).tolist().index(max(recall-FPR))
#list.index(最大值),可以放回这个最大值再list中的索引
thresholds[maxindex]
#结果:-1.0860191749391461
#可以在图像上看看这个点在哪里
plt.scatter(FPR[maxindex],recall[maxindex],c = "black",s = 30);

sklearn中的支持向量机SVM(下)_第15张图片

#将上述代码放入绘图代码中
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")
plt.show()

sklearn中的支持向量机SVM(下)_第16张图片
这样最佳阈值就选取出来了,由于现在使用decision_function来绘制ROC曲线,所以选择的最佳阈值其实是最佳距离。如果使用的是概率,那么选取的最佳阈值就会是一个概率值。只要让这个距离/概率以上的点,都为正类,让这个距离/概率以下的点都为正类,模型就是最好的:即能够捕捉出少数类,又能够尽量不误判多数类,整体的精确性和对少数类的捕捉都得到了保证。
从找出的最优阈值点来看,这个点其实是图像上离左上角最近的点,离中间的虚线最远的点,也是ROC曲线的转折点。如果没有时间进行计算,或者横坐标比较清晰的时候,可以观察转折点来找到最佳阈值。还可以使用KS曲线,或者**收益曲线(profit chart)**来选择阈值。

3 使用SVC时的其他考虑

3.1 SVC处理多分类问题:重要参数decision_function_shape

之前的内容都是基于二分类的情况来说明的,因为支持向量机是天生二分类的模型。不过,它也可以做多分类,但是SVC在多分类情况上的推广有很大的难度,要从数学角度去理解几乎是不可能的,因为要研究透彻多分类状况下的SVC,就必须研究透彻多分类时所需要的决策边界个数,每个决策边界所需要的支持向量的个数,以及这些支持向量如何组合起来计算拉格朗日函数,要求必须对SMO或者梯度下降求解SVC的拉格朗日乘数的过程十分熟悉。
由于支持向量机是天生二分类的模型,所以支持向量机在处理多分类的问题时,是把多分类问题转换为二分类问题来解决。这种转换有两种模式,一种叫做“一对一”模式(one vs one),一种叫做“一对多”模式(one vs rest)。
在ovo模式下,标签中的所有类别会被两两组合,每两个类别之间建一个SVC模型,每个模型生成一个决策边界,分别进行二分类。这种模式下,对于含有n_class个标签类别的数据来说,SVC会生成总共 C n c l a s s 2 C_{n_class}^2 Cnclass2个模型,即会生成总共 C n c l a s s 2 C_{n_class}^2 Cnclass2个超平面,其中: C n c l a s s 2 = n c l a s s ∗ ( n c l a s s − 1 ) 2 C_{n_class}^2=\frac{n_class*(n_class-1)}{2} Cnclass2=2nclass(nclass1)
在ovr模式下,标签中所有的类别会分别与其他类别进行组合,建立n_classs个模型,每个模型生成一个决策边界,分别进行二分类。
当类别越多的时候,无论是ovr还是ovo模式,需要的决策边界都会越来越多,模型也会越来越复杂,不过ovo模式下的模型计算会更加复杂,因为ovo模式中的决策边界数量增加更快,而相对的ovo模式也会更加精确。ovr模式计算更快,但效果往往不是很好。在硬件可以支持的情况下,还是建议选择ovo模式。
一旦模型的超平面的数量变化了,SVC的很多计算过程,还有接口和属性都会发生变化:

  1. 在二分类中,所有的支持向量都服务于唯一的超平面。在多分类问题中,每个支持向量都会被用来服务于n_class-1个超平面。
  2. 在生成一个超平面的二分类过程中,计算一个超平面上的支持向量对应的拉格朗日乘数 α \alpha α。而当有多个超平面时,需要的支持向量个数增长了,因为求解拉格朗日乘数的需求也变得更多。在二分类问题中,每一个支持向量求借助一个拉格朗日乘数,因此拉格朗日乘数的数目与支持向量的数目一致。但在多分类问题中,两个不同超平面的支持向量被用来决定一个拉格朗日乘数的取值,并且规定一个支持向量至少要被两个超平面使用。假设一个多分类问题中分别有三个超平面,超平面A上有3个支持向量,超平面B和C上分别有2个支持向量,则总共7个支持向量就需要求解14个对应的拉格朗日乘数。以这样的考虑来看,拉格朗日乘数的计算会变得异常复杂。
  3. 在简单二分类中,decision_function只返回每个样本点到唯一的超平面的距离,而在多分类问题中这个接口将根据选择的多分类模式不同而返回不同的结构。
  4. 在二分类中只生成一条直线,所以属性coef_和intercept_返回的结构都很单纯,但在多分类问题,尤其是ovo类型下,两个属性都受到不同程度的影响。

参数decision-function_shape决定究竟使用哪种分类模式。 可输入“ovo”、“ovr”。默认“ovr”,对所有分类器,选择使用ovo或者ovr模式。选择ovr模式,则返回的decision_function结构为(n_samples,n_class)。二分类时,尽管选用ovr模式,却会返回(n_samples,)的结构。选择ovo模式,则使用libsvm中原始的,结构为 ( n _ s a m p l e s , n _ c l a s s ∗ ( n _ c l a s s − 1 ) 2 ) (n\_samples,\frac{n\_class*(n\_class-1)}{2}) (n_samples,2n_class(n_class1))的decision_function接口。在ovo模式并且核函数会线性核的情况下,属性coef_和intercept_会分别返回 ( n _ c l a s s ∗ ( n _ c l a s s − 1 ) 2 , n _ f e a t u r e s ) (\frac{n\_class*(n\_class-1)}{2},n\_features) (2n_class(n_class1),n_features) ( n _ c l a s s ∗ ( n _ c l a s s − 1 ) 2 , ) (\frac{n\_class*(n\_class-1)}{2},) (2n_class(n_class1),)的结构,每行对应一个生成的二元分类器。ovo模式只在多分类情况下使用。

3.2 SVM模型复杂度

支持向量机是强大的工具,但是随着训练向量的数量的增大,对计算和存储的需求迅速增加。SVM的核心是二次规划问题(QP),将支持向量与其余训练数据分开。这个基于libsvm的实现使用测QP解算器可以在实践中实现 O ( n f e a t u r e s ∗ n s a m p l e s 2 ) O(n_{features}*n^2_{samples}) O(nfeaturesnsamples2) O ( n f e a t u r e s ∗ n s a m p l e s 3 ) O(n_{features}*n^3_{samples}) O(nfeaturesnsamples3)之间的复杂度,一切基于libsvm的缓存在实践中的效率有多高。这个效率由数据集确定。如果数据集非常稀疏,则应该将时间复杂度中的 n f e a t u r e s n_{features} nfeatures替换为样本向量中非零特征的平均个数。注意,如果数据是线性的,使用类LinearSVC比使用SVC中的核函数“linear”要更有效,而且LinearSVC可以几乎线性地拓展到数百万个样本或特征的数据集上。

3.3 SVM中的随机性:参数random_state

SVC中包含参数random_state,受到probability参数的影响,仅在生成高概率估计的时候才会生效。在概率估计中,SVC使用随机数生成器来混合数据。如果概率设置为False,则random_state对结果没有影响。如果不实现概率估计,SVM中不存在有随机性的过程。

3.4 SVC的重要属性补充

#属性n_support_:调用每个类别下的支持向量的数目
clf_proba.n_support_
#结果:array([42, 41])
#属性coef_:每个特征的重要性,这个系数仅仅适合于线性核
clf_proba.coef_
#结果:array([[1.16964666, 0.98721505]])
#属性intercept_:查看生成的决策边界的截距
clf_proba.intercept_
#结果:array([-4.07936113])
#属性dual_coef_:查看生成的拉格朗日乘数
clf_proba.dual_coef_
clf_proba.dual_coef_.shape
#结果:(1, 83)
#查看支持向量的属性
clf_proba.support_vectors_
clf_proba.support_vectors_.shape
#结果:(83, 2)
#注意到dual_coef_中生成的拉格朗日乘数的数目和支持向量的数目一致
#注意到KKT条件中的条件5,所有非支持向量会让拉格朗日乘数为0,所以拉格朗日乘数的数目和支持向量的数目是一致的
#注意,此情况仅仅在二分类中适用

4 SVC真实数据案例:预测明天是否会下雨

SVC在现实中的应用十分广泛,尤其在图像和文字识别方面。然而这些数据非常难以获取。在实际工作中,数据预处理往往比建模难得多,耗时多得多,因此合理的数据预处理是非常必要的。本文使用从kaggle上下载的未经过预处理的澳大利亚天气数据集,目标是在这个数据集上预测明天是否会下雨。原数据集有将近15w条数据,本文从中随机选择5000条数据进行分析。

import random
weather = pd.read_csv(r"C:\Users\86188\Desktop\coding\kaggle-raininaustralia\raininaustralia.csv")
a = [*range(weather.shape[0])]
random.shuffle(a)
raininAUS5000 = weather.loc[a[0:5000],:]
raininAUS5000.index = range(5000)
raininAUS5000.to_csv(r"C:\Users\86188\Desktop\coding\kaggle-raininaustralia\raininAUS5000.csv",mode ="a+",index=False,header=True)

4.1 导库导数据,探索特征

#导入需要的库
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
#导入数据,探索数据
weather = pd.read_csv(r"C:\Users\86188\Desktop\coding\kaggle-raininaustralia\raininaustralia.csv")
weather.head()

特征代表的含义如下表所示:

特征/标签 含义
Date 观察日期
Location 获取该信息的气象站的名称
MinTemp 以摄氏度为单位的最低温度
MaxTemp 以摄氏度为单位的最高温度
Rainfall 当天记录的降雨量,单位为mm
Evaporation 到早上9点之前的24小时的A级蒸发量(mm)
Sunshine 百日受到日照的完整小时
WindGustDir 在到午夜12点前的24小时中的最强风的风向
WindGustSpeed 在到午夜12点前的24小时中的最强风速(km/h)
WindDir9am 上午9点时的风向
WindDir3pm 下午3点时的风向
WindSpeed9am 上午9点之前每个十分钟的风速的平均值(km/h)
WindSpeed3pm 下午3点之前每个十分钟的风速的平均值(km/h)
Humidity9am 上午9点的湿度(百分比)
Humidity3pm 下午3点的湿度(百分比)
Pressure9am 上午9点平均海平面上的大气压(hpa)
Pressure3pm 下午3点平均海平面上的大气压(hpa)
Cloud9am 上午9点的天空被云层遮蔽的程度,这是以“oktas”来衡量的,这个单位记录了云层遮蔽天空的程度。0表示完全晴朗的天空,而8表示完全是阴天
Cloud3pm 下午3点的天空被云层遮蔽的程度
Temp9am 上午9点的摄氏度温度
Temp3pm 下午3点的摄氏度温度
RainTomorrow 目标变量,标签:明天下雨了吗?
#将特征矩阵和标签Y分开
X = weather.iloc[:,:-1]
Y = weather.iloc[:,-1]
X.shape
#结果:(5000, 21)
#探索数据类型
X.info()

sklearn中的支持向量机SVM(下)_第17张图片

#探索缺失值
X.isnull().mean()#缺失值所占总值的比例
'''
结果:
Date             0.0000
Location         0.0000
MinTemp          0.0040
MaxTemp          0.0020
Rainfall         0.0106
Evaporation      0.4194
Sunshine         0.4718
WindGustDir      0.0656
WindGustSpeed    0.0652
WindDir9am       0.0668
WindDir3pm       0.0276
WindSpeed9am     0.0132
WindSpeed3pm     0.0182
Humidity9am      0.0148
Humidity3pm      0.0268
Pressure9am      0.0950
Pressure3pm      0.0938
Cloud9am         0.3798
Cloud3pm         0.4022
Temp9am          0.0062
Temp3pm          0.0186
dtype: float64
'''
#要有不同的缺失值填补策略
#探索标签的分类
np.unique(Y)
#结果:array(['No', 'Yes'], dtype=object)
#说明标签是二分类
Y.shape
#结果:(5000,)
Y.isnull().sum()#加和的时候,True是1,False是0
#结果:0

粗略观察可以发现,这个特征矩阵由一部分分类变量和一部分连续变量组成,其中云层遮蔽程度虽然是以数字表示,但是本质却是分类变量。大多数特征都是采集的自然数据,如蒸发量、温度等等,而少部分特征是人为构造的,还有一些是单纯表示样本信息的变量,如采集信息的地点以及采集的时间。

4.2 分集,优先探索标签

#分训练集和测试集,并做描述性统计
#分训练集和测试集
xtrain,xtest,ytrain,ytest = train_test_split(X,Y,test_size = 0.3,random_state = 420)
#恢复索引
for i in [xtrain,xtest,ytrain,ytest]:
    i.index = range(i.shape[0])

在现实中,会分训练集和测试集,再开始进行数据预处理。这是由于测试集在现实中往往是不可获得的,或者被假设为是不可获得的,不希望建模的任何过程受到测试集数据的影响,否则就相当于提前告知了模型一部分预测的结果。而先分训练集和测试集导致的结果是,对训练集执行的所有操作,都必须对测试集执行一次,工作量是翻倍的。

#探索标签的分类
np.unique(ytrain)
#结果:array(['No', 'Yes'], dtype=object)
#是否有样本不平衡问题
ytrain.value_counts()
#结果:
#No     2711
#Yes     789
#Name: RainTomorrow, dtype: int64
ytest.value_counts()
#结果:
#No     1182
#Yes     318
#Name: RainTomorrow, dtype: int64
#有轻微的样本不均衡问题
ytrain.value_counts()[0]/ytrain.value_counts()[1]
#结果:3.4359949302915083

#将标签编码
from sklearn.preprocessing import LabelEncoder#标签专用
encoder = LabelEncoder().fit(ytrain)#LabelEncoder允许一维数据的输入,其他的encoder方法往往是不允许一维数据输入的
#使用训练集进行训练,让机器认得了标签有两类:YES和NO,且YES为1,NO为0,然后在训练集和测试集上分别进行transform
#如果测试集中出现了训练集中没有出现过的标签类别,如测试集中有YES、NO、UNKNOWN,而训练集中只有YES和NO,则会报错
ytrain = pd.DataFrame(encoder.transform(ytrain))
ytest = pd.DataFrame(encoder.transform(ytest))

4.3 探索特征,开始处理特征矩阵

4.3.1 描述性统计与异常值

#描述性统计
xtrain.describe([0.01,0.05,0.1,0.25,0.5,0.75,0.9,0.99]).T

sklearn中的支持向量机SVM(下)_第18张图片

xtest.describe([0.01,0.05,0.1,0.25,0.5,0.75,0.9,0.99]).T
#如果发现异常值,首先要观察这个异常值出现的频率
#如果异常只出现了一次,多半是输入错误,可以直接把异常值删除
#如果异常值出现多次,要和业务人员沟通,人为造成的错误异常值留着也没有用
#如果异常值占到总数据量的10%左右,可以把异常值替换成非异常但是非干扰的项,如用0进行替换,或者吧异常值当作缺失值

sklearn中的支持向量机SVM(下)_第19张图片

#查看原始的数据结构
xtrain.shape
#结果:(3500, 21)
xtest.shape
#结果:(1500, 21)
'''
#查看原始的数据结构
xtrain.shape
xtest.shape
#观察异常值是大量存在还是少数存在
xtrain.loc[xtrain.loc[:,"Cloud9am"] == 9,"Cloud9am"]
xtest.loc[xtrain.loc[:,"Cloud9am"] == 9,"Cloud9am"]
xtest.loc[xtrain.loc[:,"Cloud3pm"] == 9,"Cloud3pm"]

#少数存在,于是采取删除的策略
#注意如果删除特征矩阵,则必须连对应的标签一起删除,特征矩阵的行和标签的行必须要一一对应
xtrain = xtrain.drop(index = 71737)
ytrain = ytrain.drop(index = 71737)

#删除完毕之后,观察原始的数据结构,确认删除正确
xtrain.shape

xtest = xtest.drop(index = [19646,29632])
ytest = ytest.drop(index = [19646,29632])
xtest.shape

#进行任何删除之后,千万要记得恢复索引
for i in [xtrain,xtest,ytrain,ytest]:
    i.index = range(i.shape[0])

xtrain.head()
xtest.head()
'''

4.3.2 处理困难特征:日期

探索一下采集日期的性质。

xtrainc = xtrain.copy()
xtrainc.sort_values(by = "Location")
xtrain.iloc[:,0].value_counts()
#首先日期不是独一无二的,日期有重复值
#其次,在分训练集和测试集之后,日期也不是连续的,而是分散的
#日期不会影响下雨与否,更多的是这一天的日照时间、湿度、稳定等等这些因素会影响下雨
#光看日期,感觉它对结果的判断没有直接影响
#如果把日期当作连续型变量处理,算法会认为它是一系列1~3000左右的数字,不会意识到这是日期
'''
结果为:
2017/6/14    7
2016/5/18    6
2016/5/27    5
2016/1/24    5
2010/2/7     5
            ..
2012/2/4     1
2009/6/7     1
2012/4/26    1
2014/6/15    1
2009/4/18    1
Name: Date, Length: 2095, dtype: int64
'''
xtrain.iloc[:,0].value_counts().count()
#结果为:2095
#如果把它当作是分类型变量处理,类别太多,有2095类,如果换成数值型,会被直接当成连续型变量,如果处理成哑变量,那么特征维度会爆炸

如果思考简单一点,可以直接删除这个特征,首先它不是一个直接影响标签的特征,并且处理日期非常困难。

xtrain = xtrain.drop(["Date"],axis = 1)
xtest = xtest.drop(["Date"],axis = 1)

但很多人会持不同意见,怎么能够随便删除一个特征?如果要删除,需要一些统计过程来判断这个特征是和标签无关的,可以先将“日期”这个特征编码后对它和标签做方差齐性检验(ANOVA),如果检验结果表示日期这个特征的确与标签无关,那可以放心的删除这个特征。但要编码“日期”这个特征,又会回到它到底是否被算法当成是分类变量的问题上。
其实可以想到,日期必然是和结果相关的,它会从两个角度来影响标签:(1)可以想到昨天的天气可能会影响今天的天气,而今天的天气又可能会影响明天的天气,也就是说,随着日期的逐渐改变,样本是会受到上一个样本的影响的。但是对于算法来说,普通的算法是无法捕捉到样本与样本之间的联系的,这些算法捕捉的是样本的每个特征与标签之间的联系(即列与列之间的联系),而无法捕捉样本与样本之间的联系(行与行之间的联系)。要让算法理解上一个样本的标签可能会影响下一个样本的标签,必须使用时间序列分析。**时间序列分析是指将同一个统计指标的数值按其发生的时间先后顺序排列而成的数列。时间序列分析的主要目的是根据已有的历史数据对未来进行预测。**然而。时间序列只能在单调的、唯一的时间上运行,即一次只能够对一个地点进行预测,不能实现一次性预测多个地点,除非进行循环。而时间数据本身不是单调的,也不是唯一的,经过抽样之后甚至也不是连续的。本文的时间是每个混杂在多个地点中,每个地点上的一小段时间。使用时间序列来处理这个问题会变得复杂。(2)可以换一种思路,既然算法处理的是列与列之间的关系,可以将“今天的天气会影响明天的天气”这个指标转换成一个特征来操作。特征中有一列叫做“Rainfall”,这是表示当前日期当前地区下的降雨量,换句话说,也就是“今天的降雨量”。根据常识,今天是否下雨应该会影响明天是否下雨。因此可以将时间对气候的连续影响,转换为“今天是否下雨”这个特征,巧妙的将样本对应标签之间的联系,转换成是特征与标签之间的联系。

xtrain["Rainfall"].head(20)
'''
结果为:
0      6.8
1      0.4
2      0.4
3      2.8
4      0.0
5      0.0
6      7.0
7      0.0
8     16.0
9      0.0
10     0.0
11     0.0
12     0.0
13     0.0
14     6.8
15     0.0
16     5.0
17     0.0
18     0.0
19     0.0
Name: Rainfall, dtype: float64
'''
xtrain["Rainfall"].isnull().sum()
#结果为:38
#Rainfall以1为界限是根据描述性统计分析观察数据分布确定的
xtrain.loc[xtrain["Rainfall"] >= 1,"RainToday"] = "Yes"
xtrain.loc[xtrain["Rainfall"] < 1,"RainToday"] = "No"
xtrain.loc[xtrain["Rainfall"] == np.nan,"RainToday"] = np.nan

xtest.loc[xtest["Rainfall"] >= 1,"RainToday"] = "Yes"
xtest.loc[xtest["Rainfall"] < 1,"RainToday"] = "No"
xtest.loc[xtest["Rainfall"] == np.nan,"RainToday"] = np.nan

xtrain.head()
xtest.head()

这样就创造一个特征:今天是否下雨“RainToday”。那么是否就可以将日期删除呢?其实日期本身并不影响天气,但是日期所在的月份和季节其实是影响天气的,如果任选梅雨季节的某一天,那明天下雨的可能性必然比非梅雨季节的那一天要大。虽然无法让机器学习体会不同月份是什么季节,但是可以对不同月份进行分组,算法可以通过训练感受到“某个月或者某个季节更容易下雨”。因此,可以将月份或者季度提取出来,作为一个特征使用,而舍弃掉具体的日期。这样就创造了第二个特征:月份“Month”。

int(xtrain.loc[0,"Date"].split("-")[1])#提取出月份
#结果为:3
xtrain["Date"] = xtrain["Date"].apply(lambda x:int(x.split("-")[1]))
xtest["Date"] = xtest["Date"].apply(lambda x:int(x.split("-")[1]))
#apply是对dataframe上的某一列进行处理的一个函数
#匿名函数lambda x:命令,表示在dataframe上这一列中的每一行都执行冒号后的命令
#替换完毕后,需要修改列的名称
#rename是比较少用的,可以用来修改单个列名的函数
#通常都直接使用df.columns = 某个列表 这样的形式来一次修改所有的列名
#rename允许只修改某个单独的列
xtrain = xtrain.rename(columns = {"Date":"Month"})
#rename中的参数inplace=False,表示重命名后没有覆盖,为True时表示覆盖
xtest = xtest.rename(columns = {"Date":"Month"})
xtrain.head()
xtest.head()

通过时间“Date”,处理出两个新特征:“RainToday”以及“Month”。

4.3.3 处理困难特征:地点

地点,是一个非常tricky的特征。常识上来说,地点肯定会对明天是否会下雨存在影响。但和时间一样,输入地点的名字对于算法来说就是一串字符,“London”和“Beijing”对算法来说,和0,1没有什么区别。同样,样本中含有49个不同地点,如果处理成分类型变量,算法就无法辨别究竟是不是分类变量。也就是说,需要让算法意识到,不同的地点因为气候不同,对“明天是否会下雨”有着不同的影响。如果能够将地点转换为该地的气候,那么就可以将不同的城市打包到同一个气候中,而同一个气候下反映的降雨情况应该是相似的。获取这一数据的过程可以参考这篇文章:爬虫-kaggle数据集Rain_in_AUS的Location气候分类

xtrain.loc[:,"Location"].value_counts().count()
#超过25个类别的分类型变量,都会被算法当成是连续型变量
#结果:49

为什么爬取城市的经纬度?如果直接使用样本中的城市来爬取城市本身的气候,由于样本中的地点名称其实是气候站的名称,而不是城市本身的名称,因此不是每一个城市都能够直接获取到城市的气候。因此需要澳大利亚气象局的数据来找到这些气候站对应的城市。有了澳大利亚全国主要城市的气候,也有了澳大利亚主要城市的经纬度(地点),可以计算样本中的每个气候站到各个主要城市的地理距离,来找出离这个气象站最近的主要城市,而这个主要城市的气候就是样本点所在的地点的气候。

#把cityclimate.csv和city_climate.csv导入
cityll = pd.read_csv(r"C:\Users\86188\Desktop\coding\kaggle-raininaustralia\city_climate.csv")
city_climate = pd.read_csv(r"C:\Users\86188\Desktop\coding\kaggle-raininaustralia\cityclimate.csv")

cityll.head()#每个城市对应的经纬度,这些城市是澳大利亚统计局地图上的城市
city_climate.head()#澳大利亚统计局制作的每个城市对应的气候

sklearn中的支持向量机SVM(下)_第20张图片
sklearn中的支持向量机SVM(下)_第21张图片
接下来,将两张表处理成可以使用的样子,首先要去掉cityll中经纬度上带有的度数符号,然后要将两张表合并起来。

#去掉度数符号
cityll["Latitudenum"] = cityll["Latitude"].apply(lambda x: float(x[:-1]))
cityll["Longitudenum"] = cityll["Longitude"].apply(lambda x: float(x[:-1]))
#古城南海所有经纬度方向都是一致的,全部都是南纬、东经,因为澳大利亚是在南半球,东半球
citylld = cityll.iloc[:,[0,5,6]]
#将city_climate中的气候添加到cityll中
citylld["climate"] = city_climate.iloc[:,-1]
citylld.head()

sklearn中的支持向量机SVM(下)_第22张图片

citylld.loc[:,"climate"].value_counts()
'''
Hot dry summer, cool winter          24
Hot dry summer, warm winter          18
Warm temperate                       18
High humidity summer, warm winter    17
Mild temperate                        9
Cool temperate                        9
Warm humid summer, mild winter        5
Name: climate, dtype: int64
#澳大利亚存在八种气候,这里只有七种气候是由于还有一种是极地气候,该气候覆盖的地区没有城市,因此根据城市来划分气候是不包含极地气候的
'''

接下来,如果想要计算距离,就会需要所有样本数据中的城市,可以认为只有出现在训练集中的地点才会出现在测试集中。基于这样的假设,需要爬取训练集中所有的地点所对应的经纬度,并保存在一个csv文件samplecity.csv中:

#训练集中所有的地点
cityname = xtrain.iloc[:,1].value_counts().index.tolist()
df = pd.DataFrame(index = range(len(cityname)))#创建新DataFrame用于存储爬取的数据
driver_url = r"D:\Anaconda\msedgedriver.exe"
driver = webdriver.Edge(executable_path = driver_url)#调用MS Edge浏览器
time0 = time.time()#计时开始
#循环开始
for num,city in enumerate(cityname):#在城市名称中进行遍历
    driver_url = "https://www.geodatos.net/en/coordinates"
    driver.get(driver_url)    #首先打开MS Edge主页
    time.sleep(0.3)#停留0.3秒,看看发生了什么
    search_box = driver.find_element_by_name(name = "qs")#锁定网站的搜索输入框
    search_box.send_keys("%s Australia" % (city))#在输入框中输入“城市”澳大利亚
    search_box.submit()#enter,确认开始搜索
    resulturl = driver.find_element_by_xpath("/html/body/div[2]/div[1]/ul/li[1]/a").get_attribute("href")#选择第一个超链接
    driver.get(resulturl)#获取第一个超链接的网址
    wait = ui.WebDriverWait(driver,60,1)
    wait.until(lambda driver: driver.find_element_by_xpath("/html/body/div[2]/div[1]/div[2]/p"))
    result = driver.find_element_by_xpath("/html/body/div[2]/div[1]/div[2]/p").text##爬取需要的经纬度
    resultsplit = result.split(" ")#将爬取的结果用split进行分割
    df.loc[num,"City"] = city#向提前创建好的df中输入爬取的数据,第一列是城市名
    df.loc[num,"Latitude"] = resultsplit[0]#第二列是维度
    df.loc[num,"Longitude"] = resultsplit[2]#第三列是经度
    df.loc[num,"Latitudedir"] = resultsplit[1]#第四列是维度的方向
    df.loc[num,"Longitudedir"] = resultsplit[3]#第五列是经度的方向
    print("%i webcrawler successful for city %s" % (num,city))#每次爬虫成功之后,打印“城市”爬取成功
time.sleep(1)#全部爬取完毕后,停留1秒钟
driver.quit()#关闭浏览器
print(time.time()-time0)#打印爬虫花费的时间
#结果:244.88815426826477
df.to_csv(r"C:\Users\86188\Desktop\coding\kaggle-raininaustralia\samplecity.csv",mode ="a+",index=False,header=True)

由于本文所使用的爬虫网站是主要针对城市的,而样本中的Location是气象站的名称,因此部分Location爬取的经纬度数据是不准确的,在此忽略这个问题。

samplecity = pd.read_csv(r"C:\Users\86188\Desktop\coding\kaggle-raininaustralia\samplecity.csv")
#对samplecity也执行同样的处理:去掉经纬度中度数符号,并且舍弃经纬度的方向
samplecity["Latitudenum"] = samplecity["Latitude"].apply(lambda x: float(x[:-1]))
samplecity["Longitudenum"] = samplecity["Longitude"].apply(lambda x: float(x[:-1]))
#古城南海所有经纬度方向都是一致的,全部都是南纬、东经,因为澳大利亚是在南半球,东半球
samplecityd = samplecity.iloc[:,[0,5,6]]
samplecityd.head()

sklearn中的支持向量机SVM(下)_第23张图片
现在有了澳大利亚主要城市的经纬度和对应的气候,也有样本地点所对应的经纬度,接下来开始计算样本上的地点到每个澳大利亚主要城市的距离,而离样本地点最近的那个主要城市的气候,就认为是样本点的气候。在地理上,两个地点之间的距离,由如下公式进行计算:
d i s t = R ∗ a r c c o s ( s i n ( s l a t ) ∗ s i n ( e l a t ) + c o s ( s l a t ) ∗ c o s ( e l a t ) ∗ c o s ( s l o n − e l o n ) ) dist = R* arccos(sin(slat)* sin(elat)+cos(slat)* cos(elat)* cos(slon-elon)) dist=Rarccos(sin(slat)sin(elat)+cos(slat)cos(elat)cos(slonelon))
其中 R R R是地球的半径,6371.01km,arccos是反三角余弦函数,slat是起始地点的维度,slon是起始地点的经度,elat是结束地点的维度,elon是结束地点的经度。本质还是计算两点之间的距离。而爬取的经纬度本质其实是角度,所以需要使用各种三角函数和弧度公式将角度转换成距离。

#首先使用radians将角度转换成弧度
from math import radians,sin,cos,acos
citylld.loc[:,"slat"] = citylld.iloc[:,1].apply(lambda x: radians(x))
citylld.loc[:,"slon"] = citylld.iloc[:,2].apply(lambda x: radians(x))
samplecityd.loc[:,"elat"] = samplecityd.iloc[:,1].apply(lambda x: radians(x))
samplecityd.loc[:,"elon"] = samplecityd.iloc[:,2].apply(lambda x: radians(x))

import sys
for i in range(samplecityd.shape[0]):
    slat = citylld.loc[:,"slat"]
    slon = citylld.loc[:,"slon"]
    elat = samplecityd.loc[i,"elat"]
    elon = samplecityd.loc[i,"elon"]
    dist = 6371.01 * np.arccos(np.sin(slat) * np.sin(elat) + np.cos(slat) * np.cos(elat)*np.cos(slon.values - elon))
    city_index = np.argsort(dist)[0]
    #每次计算后,取距离最近的城市,然后将最近的城市和城市对应的气候都匹配到samplecityd中
    samplecityd.loc[i,"closest_city"] = citylld.loc[city_index,"City"]
    samplecityd.loc[i,"climate"] = citylld.loc[city_index,"climate"]

#查看最后的结果,需要检查城市匹配是否及基本正确
samplecityd.head()

sklearn中的支持向量机SVM(下)_第24张图片

#查看气候分布
samplecityd["climate"].value_counts()
'''
Warm temperate                       11
Mild temperate                       10
Hot dry summer, cool winter           9
Cool temperate                        8
Hot dry summer, warm winter           5
High humidity summer, warm winter     4
Warm humid summer, mild winter        2
Name: climate, dtype: int64
'''
#确认无误后,取出样本城市所对应的气候,并保存
locafinal = samplecityd.iloc[:,[0,-1]]
locafinal.head()

sklearn中的支持向量机SVM(下)_第25张图片

locafinal.columns = ["Location","Climate"]
#在这里设定locafinal的索引为地点,是为之后进行map的匹配
locafinal = locafinal.set_index(keys = "Location")
locafinal.to_csv(r"C:\Users\86188\Desktop\coding\kaggle-raininaustralia\samplelocation.csv")
locafinal.head()

sklearn中的支持向量机SVM(下)_第26张图片
有了每个样本城市所对应的气候,接下来就使用气候来替换掉原本的气象站的名称。这里可以使用map功能,map能够将特征中的值一一对应到设定的字典中,并且用字典中的值来替换样本中的原本的值。

#先来看看训练集长什么样
xtrain.head()
#将location中的内容替换,并且确保匹配进入的气候字符串中不含有逗号,气候两边不含有空格
#使用re这个模块来消除逗号
#re.sub(希望替换的值,希望被替换成的值,要操作的字符串)
#x.strip()是去掉空格的函数
import re
xtrain["Location"] = xtrain["Location"].map(locafinal.iloc[:,0]).apply(lambda x: re.sub(",","",x.strip()))
xtest["Location"] = xtest["Location"].map(locafinal.iloc[:,0]).apply(lambda x: re.sub(",","",x.strip()))
#把“Location”替换成气候使用的是map映射,这一过程中将气象站的名字替换成了对应的城市对应的气候,并将城市的气候中所含的逗号和空格都去掉
#修改特征内容之后,只用新列名“Climate”来替换之前的列名“Location”
#注意这个命令一旦执行,就再也没有“Location”列了,使用索引时要特别注意
xtrain = xtrain.rename(columns = {"Location":"Climate"})
xtest = xtest.rename(columns = {"Location":"Climate"})
xtrain.head()
xtest.head()

4.3.4 处理分类型变量:缺失值

由于特征矩阵由两种类型的数据组成:分类型和连续型,因此必须对两种数据采用不同的填补缺失值策略。传统地,如果是分类性特征,可以采用众数进行填补,如果是连续型特征,则采用均值来填补。
由于已经划分好训练集和测试集,需要考虑:究竟使用哪一部分的数据进行众数填补?通常是,使用训练集上的众数对训练集和测试集都进行填补。按道理说就算用测试集上的众数对测试集进行填补,也不会使测试集数据进行建好的模型,不会给模型透露信息。然而现实中,测试集未必是很多条数据,也许测试集只有一条数据,而某个特征上是控制,此时测试集本身的众数根本不存在,要如何利用测试集本身的众数进行填补呢?因此为了避免这种情况的发生,假设测试集和训练集的数据分布和性质都是相似的,因此统一使用训练集的众数和均值来对测试集进行填补。
在sklearn中,即便是填补缺失值的类也需要由实例化、fit和接口调用执行填补三个步骤来进行,而这种分割其实一部分也是为了满足使用训练集的建模结果来填补测试集的需求。只需要实例化后,使用训练集进行fit,然后再调用接口执行填补时用训练集fit后的结果分别来填补测试集和训练集即可。

#查看缺失值的缺失情况
xtrain.isnull().mean()
'''
Month            0.000000
Climate          0.000000
MinTemp          0.004857
MaxTemp          0.002571
Rainfall         0.010857
Evaporation      0.413429
Sunshine         0.466000
WindGustDir      0.064571
WindGustSpeed    0.064286
WindDir9am       0.063429
WindDir3pm       0.028571
WindSpeed9am     0.012286
WindSpeed3pm     0.019143
Humidity9am      0.014571
Humidity3pm      0.026857
Pressure9am      0.092857
Pressure3pm      0.091429
Cloud9am         0.375143
Cloud3pm         0.399714
Temp9am          0.006571
Temp3pm          0.019429
RainToday        0.010857
dtype: float64
'''
#首先找出分类型特征有哪些
cate = xtrain.columns[xtrain.dtypes == "object"].tolist()
#除了特征类型为“object”的特征,还有虽然用数字表示,但是本质为分类型特征的云层遮蔽程度
cloud = ["Cloud9am","Cloud3pm"]
cate = cate + cloud
cate
#结果:['Climate',
# 'WindGustDir',
# 'WindDir9am',
# 'WindDir3pm',
# 'RainToday',
# 'Cloud9am',
# 'Cloud3pm']
#对于分类型特征,使用众数进行填补
from sklearn.impute import SimpleImputer
si = SimpleImputer(missing_values = np.nan,strategy = "most_frequent")
#注意,使用训练数据集来训练填补器,本质是生成训练集中的众数
si.fit(xtrain.loc[:,cate])
#然后使用训练集中的众数来同时填补训练集和测试集
xtrain.loc[:,cate] = si.transform(xtrain.loc[:,cate])
xtest.loc[:,cate] = si.transform(xtest.loc[:,cate])
xtrain.head()
xtest.head()

4.3.5 处理分类型变量:将分类变量编码

在编码中,和填补缺失值一样,也是需要先用训练集fit模型,本质是将训练集中已经存在的类别转换成是数字,然后再使用接口transform分别在测试集和训练集上编码特征矩阵。当使用接口在测试集上进行编码的时候,如果测试集上出现了训练集中从未出现过的类别,那代码就会报错,此时需要考虑测试集上或许存在异常值、错误值,或者的确有一个新的类别出现,这是就需要调整模型。

#将所有的分类型变量编码为数字,一个类别是一个数字
from sklearn.preprocessing import OrdinalEncoder
oe = OrdinalEncoder()
#利用训练集进行fit
oe = oe.fit(xtrain.loc[:,cate])
#用训练集的编码结果来编码训练集和测试集特征矩阵
#如果测试集特征矩阵报错,就说明测试集中出现了训练集从未出现过的类别
xtrain.loc[:,cate] = oe.transform(xtrain.loc[:,cate])
xtest.loc[:,cate] = oe.transform(xtest.loc[:,cate])
xtrain.loc[:,cate].head()
xtest.loc[:,cate].head()

sklearn中的支持向量机SVM(下)_第27张图片
sklearn中的支持向量机SVM(下)_第28张图片

4.3.6 处理连续型变量:填补缺失值

连续型变量的缺失值由均值来进行填补。连续型变量往往已经是数字,无需进行编码转换。与分类型变量一样,也是使用训练集上的均值对测试集进行填补。其实使用算法进行填补也是可以的,但在现实中,其实非常少用到算法进行填补,有以下理由:

  1. 算法是黑箱,解释性不强。如果是一个数据挖掘工程师,使用算法来填补缺失值后,不懂机器学习的老板或者同事问缺失值是怎么来的,可能需要从头到尾说明以下随机森林的原理,这种效率过低的事情不可能做,而他们不会接受他们无法理解的东西;
  2. 算法填补太过缓慢,运行一次森林需要有至少100棵树才能够基本保证森林的稳定性,而填补一个列就需要很长的时间。而在不知道森林的填补结果是好是坏的情况下,填补一个很大的数据集风险是非常高的,有可能需要跑好几个小时,但填补出来的结果却不怎么优秀,这是明显低效的做法。
    因此在现实工作时,往往使用易于理解的均值或中位数来进行填补,当然在算法比赛中,可以穷尽一切能够想到的方法来填补缺失值以追求让模型的效果更好,但在现实中,除了模型效果之外,还要追求可解释性。
col = xtrain.columns.tolist()
for i in cate:
    col.remove(i)#剔除分类型变量
col
#结果:['Month', 'MinTemp', 'MaxTemp', 'Rainfall', 'Evaporation', 'Sunshine', 'WindGustSpeed', 'WindSpeed9am', 'WindSpeed3pm', 'Humidity9am', 'Humidity3pm', 'Pressure9am', 'Pressure3pm', 'Temp9am', 'Temp3pm']
#实例化模型,填补策略为“mean”表示均值
impmean = SimpleImputer(missing_values = np.nan,strategy = "mean")
#用训练集来fit模型
impmean = impmean.fit(xtrain.loc[:,col])
#分别在训练集和测试集上进行均值填补
xtrain.loc[:,col] = impmean.transform(xtrain.loc[:,col])
xtest.loc[:,col] = impmean.transform(xtest.loc[:,col])
xtrain.head()
xtest.head()

#再检查一遍
xtrain.isnull().mean()
xtest.isnull().mean()

4.3.7 处理连续型变量:无量纲化

数据的无量纲化是SVM执行前的重要步骤,因此需要对数据进行无量纲化。但注意这个操作不对分类型变量进行。

from sklearn.preprocessing import StandardScaler
#数据转换为均值为0,方差为1的数据,且标准化不改变数据的分布,不会把数据变成正态分布
ss = StandardScaler()
ss = ss.fit(xtrain.loc[:,col])
xtrain.loc[:,col] = ss.transform(xtrain.loc[:,col])
xtest.loc[:,col] = ss.transform(xtest.loc[:,col])
xtrain.head()
xtest.head()

4.4 建模与模型评估

from time import time#随时监控模型的运行时间
import datetime
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score
from sklearn.metrics import roc_auc_score,recall_score
ytrain = ytrain.iloc[:,0].ravel()
ytest = ytest.iloc[:,0].ravel()
#建模选择支持向量机SVC,首先用核函数的学习曲线来选择核函数
#希望同时观察,精确性、recall以及AUC分数
times = time()#因为SVM是计算量很大的模型,所以需要时刻监控模型的运行时间
for kernel in ["linear","poly","rbf","sigmoid"]:
    clf = SVC(kernel = kernel
             ,gamma = "auto"
             ,degree = 1
             ,cache_size = 5000#设定越大,代表允许算法使用越多的内存来进行计算
             ).fit(xtrain,ytrain)
    result = clf.predict(xtest)#获取模型的预测结果
    score = clf.score(xtest,ytest)#接口score返回的是准确度accuracy
    recall = recall_score(ytest,result)
    auc = roc_auc_score(ytest,clf.decision_function(xtest))
    print("%s 's testing accuracy %f ,recall is %f', auc is %f" % (kernel,score,recall,auc))
    print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))
'''
linear 's testing accuracy 0.836000 ,recall is 0.477987', auc is 0.853196
00:05:652685
poly 's testing accuracy 0.834667 ,recall is 0.455975', auc is 0.850964
00:06:476390
rbf 's testing accuracy 0.824667 ,recall is 0.374214', auc is 0.811276
00:08:982227
sigmoid 's testing accuracy 0.644667 ,recall is 0.194969', auc is 0.453570
00:09:663389
'''

从结果来看,模型的准确度和AUC面积还是勉勉强强,但是每个核函数下的recall都不太高。相比之下,线性核函数模型的效果最好。在这种状况下,要向着什么方向调参?可以有不同的目标:

  1. 希望不计一切代价判断出少数类,得到最高的recall;
  2. 希望追求最高的预测准确率,一切目的都是为了让accuracy更高,不在意recall或者AUC;
  3. 希望达到recall、ROC和accuracy之间的平衡,不追求任何一个也不牺牲任何一个。

4.5 模型调参

4.5.1 追求最高recall

如果想要的是最高recall,可以牺牲准确度,希望不计一切代价来捕获少数类,那么首先可以调整class_weight参数,使用balanced模式来调节recall。

times = time()
for kernel in ["linear","poly","rbf","sigmoid"]:
    clf = SVC(kernel = kernel
             ,gamma = "auto"
             ,degree = 1
             ,cache_size = 5000
             ,class_weight = "balanced"
             ).fit(xtrain,ytrain)
    result = clf.predict(xtest)
    score = clf.score(xtest,ytest)
    recall = recall_score(ytest,result)
    auc = roc_auc_score(ytest,clf.decision_function(xtest))
    print("%s 's testing accuracy %f, recall is %f',auc is %f" % (kernel,score,recall,auc))
    print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))
'''
linear 's testing accuracy 0.780667, recall is 0.776730',auc is 0.853928
00:06:309506
poly 's testing accuracy 0.773333, recall is 0.764151',auc is 0.851951
00:07:471920
rbf 's testing accuracy 0.790000, recall is 0.669811',auc is 0.826964
00:10:250671
sigmoid 's testing accuracy 0.466000, recall is 0.421384',auc is 0.452716
00:11:517333
'''

在锁定了线性核函数之后,甚至可以将class_weight调节得更加倾向于少数类,来不计代价提升recall。

times = time()
for kernel in ["linear","poly","rbf","sigmoid"]:
    clf = SVC(kernel = kernel
             ,gamma = "auto"
             ,degree = 1
             ,cache_size = 5000
             ,class_weight = {1:10}#注意,这里写的其实是类别1:10,隐藏了类0:1这个比例
             ).fit(xtrain,ytrain)
    result = clf.predict(xtest)
    score = clf.score(xtest,ytest)
    recall = recall_score(ytest,result)
    auc = roc_auc_score(ytest,clf.decision_function(xtest))
    print("%s 's testing accuracy %f, recall is %f',auc is %f" % (kernel,score,recall,auc))
    print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))
'''
linear 's testing accuracy 0.615333, recall is 0.937107',auc is 0.851792
00:11:120368
poly 's testing accuracy 0.604000, recall is 0.937107',auc is 0.852137
00:12:313888
rbf 's testing accuracy 0.769333, recall is 0.663522',auc is 0.810491
00:14:725810
sigmoid 's testing accuracy 0.305333, recall is 0.776730',auc is 0.452620
00:16:313065
或class_weight = {1:15}时
linear 's testing accuracy 0.528667, recall is 0.959119',auc is 0.848048
00:11:244691
poly 's testing accuracy 0.515333, recall is 0.962264',auc is 0.845377
00:12:612316
rbf 's testing accuracy 0.770000, recall is 0.666667',auc is 0.809078
00:15:013488
sigmoid 's testing accuracy 0.260000, recall is 0.911950',auc is 0.452609
00:16:653288
'''

随着recall无节制地上升,精确度下降得十分厉害,不过看起来AUC面积还好,稳定保持在0.85左右。如果此时目的就是追求一个比较高的AUC分数和比较好的recall,那模型还是不错的。虽然现在精确度很低,但是的确精准地捕捉了每一个雨天。

4.5.2 追求最高准确率

在现有的目标(判断明天是否会下雨)下,追求最高准确率而不顾recall其实意义不大,但出于练习调参的目的,此时假设不在意recall,首先要观察一下样本不均衡的状况。如果样本非常不均衡,但是此时却有很多多数类被判错的话,可以让模型任性地将所有样本都判断为0,完全不顾少数类。

valuec = pd.Series(ytest).value_counts()
valuec
#结果;
#0    1182
#1     318
#dtype: int64
valuec[0]/valuec.sum()
#结果:0.788

初步判断,可以认为其实已经将大部分的多数类判断正确了,所以才能够得到现在的正确率。为了证明这一判断,可以使用混淆矩阵来计算特异度,如果特异度非常高,则证明多数类上已经很难被操作了。

#查看模型的特异度
from sklearn.metrics import confusion_matrix as CM
clf = SVC(kernel = "linear"
         ,gamma = "auto"
         ,cache_size = 5000
         ).fit(xtrain,ytrain)
result = clf.predict(xtest)
cm = CM(ytest,result,labels = (1,0))
cm
#结果:
#array([[ 152,  166],
#       [  80, 1102]], dtype=int64)
specificity = cm[1,1]/cm[1,:].sum()
specificity#几乎所有的0都被判断正确了,还有不少1也被判断正确了
#结果:0.9323181049069373

可以看到,特异度非常高,此时如果要求模型将所有的类都判断为0,则已经被判断正确的少数类会被误判,整体的准确度一定会下降,而如果希望通过让模型捕捉更多少数类来提升精确度,是无法实现的,因为一旦让模型更加倾向于少数类,就会有更多的多数类被判错。
可以试试看使用class_weight将模型向少数类的方向稍微调整,来查看是否有更多的空间来提升准确率。如果在轻微向少数类方向调整的过程中,出现了更高的准确率,则说明模型还没有到极限。

irange = np.linspace(0.01,0.05,10)
for i in irange:
    times = time()
    clf = SVC(kernel = "linear"
             ,gamma = "auto"
             ,cache_size = 5000
             ,class_weight = {1:i+1}
             ).fit(xtrain,ytrain)
    result = clf.predict(xtest)
    score = clf.score(xtest,ytest)
    recall = recall_score(ytest,result)
    auc = roc_auc_score(ytest,clf.decision_function(xtest))
    print("under ratio 1:%f testing accuracy %f,recall is %f',auc is %f" % (1+i,score,recall,auc))
    print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))
'''
结果为:
under ratio 1:1.010000 testing accuracy 0.835333,recall is 0.477987',auc is 0.853292
00:05:623844
under ratio 1:1.014444 testing accuracy 0.834667,recall is 0.474843',auc is 0.853268
00:04:978199
under ratio 1:1.018889 testing accuracy 0.835333,recall is 0.474843',auc is 0.853199
00:04:074104
under ratio 1:1.023333 testing accuracy 0.834000,recall is 0.477987',auc is 0.853204
00:04:072906
under ratio 1:1.027778 testing accuracy 0.833333,recall is 0.477987',auc is 0.853093
00:04:042533
under ratio 1:1.032222 testing accuracy 0.833333,recall is 0.481132',auc is 0.853204
00:03:980695
under ratio 1:1.036667 testing accuracy 0.832667,recall is 0.481132',auc is 0.853268
00:04:072622
under ratio 1:1.041111 testing accuracy 0.832667,recall is 0.481132',auc is 0.853255
00:04:108554
under ratio 1:1.045556 testing accuracy 0.831333,recall is 0.481132',auc is 0.853215
00:03:967504
under ratio 1:1.050000 testing accuracy 0.831333,recall is 0.481132',auc is 0.853210
00:04:049056
'''

模型的效果没有太好,并没有再出现比83.53%精确度更高的取值。可见,模型在不做样本平衡下,准确度其实已经非常接近极限了,让模型向着少数类的方向调节,不能够达到质变。如果真的希望再提升准确度,只能选择更换模型的方式,调整参数已经不能提升准确度了,可以考虑逻辑回归。

from sklearn.linear_model import LogisticRegression as LR
logclf = LR(solver = "liblinear").fit(xtrain,ytrain)
logclf.score(xtest,ytest)
#结果:0.8333333333333334
C_range = np.linspace(3,5,10)
for C in C_range:
    logclf = LR(solver = "liblinear",C=C).fit(xtrain,ytrain)
    print(C,logclf.score(xtest,ytest))
'''
3.0 0.8313333333333334
3.2222222222222223 0.8313333333333334
3.4444444444444446 0.8313333333333334
3.6666666666666665 0.8313333333333334
3.888888888888889 0.8313333333333334
4.111111111111111 0.8313333333333334
4.333333333333333 0.83
4.555555555555555 0.83
4.777777777777778 0.83
5.0 0.83
'''

模型的精确度还是没有提升,也许,要将模型的精确度提升到90%以上,需要集成算法,如梯度提升树等。

4.5.3 追求平衡

经过多种尝试,选定线性核,并发现调节class_weight并不能够使模型有较大的改善,现在试试看调节线性核函数的C值是否有效果:

import matplotlib.pyplot as plt
C_range = np.linspace(0.01,20,20)
recallall = []
aucall = []
scoreall = []
for C in C_range:
    times = time()
    clf = SVC(kernel = "linear"
             ,C = C
             ,cache_size = 5000
             ,class_weight = "balanced"
             ).fit(xtrain,ytrain)
    result = clf.predict(xtest)
    score = clf.score(xtest,ytest)
    recall = recall_score(ytest,result)
    auc = roc_auc_score(ytest,clf.decision_function(xtest))
    recallall.append(recall)
    aucall.append(auc)
    scoreall.append(score)
    print("under C %f, testing accuracy is %f, recall is %f', auc is %f" % (C,score,recall,auc))
    print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))

print(max(aucall),C_range[aucall.index(max(aucall))])
plt.figure()
plt.plot(C_raneg,recallall,c = "red",label = "recall")
plt.plot(C_range,aucall,c = "black",label = "auc")
plt.plot(C_range,scoreall,c = "orange",label = "accuracy")
plt.legend()
plt.show()
'''
under C 0.010000, testing accuracy is 0.767333, recall is 0.767296', auc is 0.846936
00:00:519626
under C 1.062105, testing accuracy is 0.778667, recall is 0.776730', auc is 0.853907
00:03:740877
under C 2.114211, testing accuracy is 0.779333, recall is 0.776730', auc is 0.854032
00:06:524793
under C 3.166316, testing accuracy is 0.779333, recall is 0.779874', auc is 0.854008
00:10:458461
under C 4.218421, testing accuracy is 0.779333, recall is 0.779874', auc is 0.854095
00:13:784382
under C 5.270526, testing accuracy is 0.779333, recall is 0.779874', auc is 0.854082
00:16:833437
under C 6.322632, testing accuracy is 0.780000, recall is 0.779874', auc is 0.854069
00:19:754229
under C 7.374737, testing accuracy is 0.780000, recall is 0.779874', auc is 0.854093
00:22:414182
under C 8.426842, testing accuracy is 0.780000, recall is 0.779874', auc is 0.854135
00:25:329386
under C 9.478947, testing accuracy is 0.780000, recall is 0.779874', auc is 0.854127
00:27:426425
under C 10.531053, testing accuracy is 0.780000, recall is 0.779874', auc is 0.854194
00:30:744247
under C 11.583158, testing accuracy is 0.780000, recall is 0.779874', auc is 0.854175
00:34:593600
under C 12.635263, testing accuracy is 0.780000, recall is 0.776730', auc is 0.854119
00:32:519237
under C 13.687368, testing accuracy is 0.780000, recall is 0.776730', auc is 0.854125
00:37:266080
under C 14.739474, testing accuracy is 0.780000, recall is 0.776730', auc is 0.854056
00:42:431313
under C 15.791579, testing accuracy is 0.780000, recall is 0.776730', auc is 0.854064
00:44:495523
under C 16.843684, testing accuracy is 0.780000, recall is 0.776730', auc is 0.854053
00:47:683816
under C 17.895789, testing accuracy is 0.780000, recall is 0.776730', auc is 0.854045
00:49:644612
under C 18.947895, testing accuracy is 0.780000, recall is 0.776730', auc is 0.854024
00:53:200646
under C 20.000000, testing accuracy is 0.780000, recall is 0.776730', auc is 0.854002
00:55:659939
0.8541939362981409 10.531052631578946
'''

sklearn中的支持向量机SVM(下)_第29张图片
可以观察到几个现象:(1)随着C值逐渐增大,模型的运行速度变得越来越慢。对于SVM这个本来运行就不快的模型来说,巨大的C值会是一个比较危险的消耗。所以正常来说,应该设定一个较小的C值范围来进行调整;(2)C很小的时候,模型的各项指标都很低,但当C到1以上之后,模型的表现开始逐渐稳定,在C逐渐增大之后,模型的效果并没有显著地提高。可以认为设定的C值范围太大了,然后继续增大或者缩小C值的范围,AUC面积也只能够在0.85上下进行变化,调节C值不能够让模型的任何指标实现质变。
把目前为止最佳的C值代入模型,看看准确率、Recall的具体值:

times = time()
clf = SVC(kernel = "linear"
         ,C = 10.531053
         ,cache_size = 5000
         ,class_weight = "balanced"
         ).fit(xtrain,ytrain)
result = clf.predict(xtest)
score = clf.score(xtest,ytest)
recall = recall_score(ytest,result)
auc = roc_auc_score(ytest,clf.decision_function(xtest))
print("testing accuracy is %f, recall is %f', auc is %f" % (score,recall,auc))
print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))
#结果:
#testing accuracy is 0.780000, recall is 0.779874', auc is 0.854162
#00:34:572906

可以看到,这种情况下模型的准确率、Recall和AUC都没有太差,但是也没有太好,这也许就是模型平衡后的一种结果。现在,光是调整支持向量机本身的参数,已经不能够满足需求了,要想让AUC的面积更进一步,需要绘制ROC曲线,查看是否可以通过调整阈值来对这个模型进行改进。

from sklearn.metrics import roc_curve as ROC
import matplotlib.pyplot as plt

FPR,Recall,thresholds = ROC(ytest,clf.decision_function(xtest),pos_label = 1)
area = roc_auc_score(ytest,clf.decision_function(xtest))
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()

sklearn中的支持向量机SVM(下)_第30张图片
以此模型作为基础,来求解最佳阈值:

maxindex = (Recall - FPR).tolist().index(max(Recall - FPR))
thresholds[maxindex]
#结果:0.050776909765431366

基于选出的最佳阈值来确定y_predict,并确定在这个阈值下的recall和准确度的值。

from sklearn.metrics import accuracy_score as AC
times = time()
clf = SVC(kernel = "linear"
          ,C = 10.531053
          ,cache_size = 5000
          ,class_weight = "balanced"
         ).fit(xtrain,ytrain)
prob = pd.DataFrame(clf.decision_function(xtest))
prob.loc[prob.iloc[:,0] >= thresholds[maxindex],"y_pred"] = 1
prob.loc[prob.iloc[:,0] < thresholds[maxindex],"y_pred"] = 0
prob.loc[:,"y_pred"].isnull().sum()
#结果:0
#检查模型本身的准确度
times = time()
score = AC(ytest,prob.loc[:,"y_pred"].values)
recall = recall_score(ytest,prob.loc[:,"y_pred"])
print("testing accuracy is %f, recall is %f" % (score,recall))
print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))
#结果:
#testing accuracy is 0.789333, recall is 0.770440
#00:00:003001

调整阈值前后的效果并没有太大的变化。可见,如果追求平衡,那么SVC本身的结果就已经非常接近最优结果了。调节阈值、调节参数C和调节class_weight都不一定有效果。但整体来看,使用的模型不是一个糟糕的模型,但这个结果还是比较差的。可以更加深入地探索模型,或者换别的方法来处理特征,以达到AUC面积达0.9以上,或者准确度和recall都提升到90%以上。

4.6 SVM总结&结语

SVM的原理,包括决策边界、损失函数、拉格朗日函数、拉格朗日对偶函数、软间隔硬间隔、核函数以及核函数的各种应用。SVC类的各种重要参数、属性和接口,其中参数包括软间隔的惩罚系数C、核函数kernel、核函数的相关参数gamma、coef0和degree、解决样本不平衡的参数class_weight、解决多分类问题的参数decision_function_shape、控制概率的参数probability、控制计算内存的参数cache_size,属性主要包括调用支持向量的属性support_vectors_和查看特征重要性的属性coef_,接口中最核心的是decision_function。分类模型的模型评估指标:混淆矩阵和ROC曲线。

参考资料:https://www.bilibili.com/video/BV1WL4y1H7rD?p=44&spm_id_from=pageDriver

你可能感兴趣的:(机器学习,支持向量机,sklearn,机器学习)