1 原理概述
今天,我们将基于我们已经学过的理论,开始对核函数性质的探索。支持向量机分类器,是在数据空间中找出一个超平面作为决策边界,利用这个决策边界来对数据进行分类,并使分类误差尽量小的模型。决策边界是比所在数据空间小一维的空间,在三维数据空间中就是一个平面,在二维数据空 间中就是一条直线。
平行于决策边界的两条虚线是距离决策边界相对距离为1的超平面,他们分别压过两类样本中距离决策边界最近的样本点,这些样本点就被成为支持向量。两条虚线超平面之间的距离叫做边际,简写为d 。支持向量机分类器,就是以找出最大化的边际d为目标来求解损失函数,以求解出参数和 ,以构建决策边界,然后用决策边界来分类的 分类器。
当然,不是所有数据都是一条直线,或一个平面,甚至一个超平面可以将数据完全分开。比如上面的环形数据。对于这样的数据,我们需要对它进行一个升维变化,来数据从原始的空间投射到新空间中。升维之后,我们明显可以找出一个平面,能够将数据切分开来。 是一个映射函数,它代表了某种能够将数据升维的非线性的变换,我们对数据进行这样的变换,确保数据在自己的空间中一定能够线性可分。
这种变换非常巧妙,但也带有一些实现问题。 首先,我们可能不清楚应该什么样的数据应该使用什么类型的映射函数来确保可以在变换空间中找出线性决策边界。极端情况下,数据可能会被映射到无限维度的空间中,这种高维空 间可能不是那么友好,维度越多,推导和计算的难度都会随之暴增。其次,即使已知适当的映射函数,我们想要计算类似于这样的点积,计算量可能会无比巨大,要找出超平面所付出的代价是非常昂贵的。
关键概念:核函数
而解决这些问题的数学方式,叫做“核技巧”(Kernel Trick),是一种能够使用数据原始空间中的向量计算来表示 升维后的空间中的点积结果的数学方式。
核函数能够帮助我们解决三个问题:
第一,有了核函数之后,非线性SVM中的核函数都是正定核函数 (positive definite kernel functions),他们都满足美世定律(Mercer’s theorem),确保了高维空间中任意两个向量 的点积一定可以被低维空间中的这两个向量的某种计算来表示(多数时候是点积的某种变换)。
第二,使用核函数计算低维度中的向量关系比计算原本的要简单太多了。
第三,因为计算是在原始空间中进行,所以避免了维度诅咒的问题。
选用不同的核函数,就可以解决不同数据分布下的寻找超平面问题。在sklearn的SVC中,这个功能由参 数“kernel”(ˈkərnl)和一系列与核函数相关的参数来进行控制。今天,我们就在乳腺癌数据集上,来探索一下各种核 函数的功能和选择。
2 SVC的重要参数kernel
SVC类:
sklearn.svm.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)
作为SVC类最重要的参数之一,“kernel"在sklearn中可选以下几种选项:
无论如何,我们还是可以通过在不同的核函数中循环去找寻最佳的核函数来对核函数进行一个选取。我创造了一 系列线性或非线性可分的数据,绘制出每个数据集上SVC在不同核函数下的决策边界,并计算SVC在不同核函数下分类准确率来观察核函数的效用。
可以观察到,线性核函数和多项式核函数在非线性数据上表现会浮动,如果是像环形数据那样彻底不可分的,则表现糟糕。在线性数据集上,线性核函数和多项式核函数即便有扰动项也可以,表现不错。可见多项式核函数是虽然也可以处理非线性情况,但更偏向于线性的功能。
Sigmoid核函数就比较尴尬了,它在非线性数据上强于两个线性核函数,但效果明显不如rbf,它在线性数据上完全比不上线性的核函数们,对扰动项的抵抗也比较弱,所以它功能比较弱小,很少被用到。
rbf,高斯径向基核函数基本在任何数据集上都表现不错,属于比较万能的核函数。我个人的经验是,无论如何先试试看高斯径向基核函数,它适用于核转换到很高的空间的情况,在各种情况下往往效果都很不错,如果rbf效果不好,那我们再试试看其他的核函数。另外,多项式核函数多被用于图像处理之中。
3 乳腺癌数据集下探索核函数的性质
3.1 探索kernel该如何选取
看起来,除了Sigmoid核函数,其他核函数效果都还不错。但其实各个核函数都有自己的问题。接下来,我们就使用乳腺癌数据集作为例子来展示一下:
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
from time import time
from datetime import datetime
data=load_breast_cancer().data
target=load_breast_cancer().target
X_train,X_test,y_train,y_test=train_test_split(data,target,test_size=0.3,random_state=420)
# plt.scatter(data[:,0],data[:,1],c=target)
# plt.show()
kernel=['linear','poly','sigmoid','rbf']
for i in kernel:
time0=time()
clf = SVC(kernel=i
, gamma="auto"
# , degree = 1
, cache_size=5000
).fit(X_train, y_train)
print('accuracy:',clf.score(X_test,y_test))
print(datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))
发现:
- 多项式核函数此时此刻要消耗大量的时间,运算非常的缓慢。
- 乳腺癌数据集是一个线性数据集,线性核函数跑出来的效果很好。rbf和sigmoid两个擅长非线性的数据从效果上来看完全不可用。
- 线性核函数的运行速度远远不如非线性的两个核函数。如果数据是线性的,那如果我们把degree参数调整为1,多项式核函数应该也可以得到不错的结果
问题:
多项式核函数的运行速度立刻加快了,并且精度也提升到了接近线性核函数的水平,可喜可贺。但是,我们之前的 实验中,我们了解说,rbf在线性数据上也可以表现得非常好,那在这里,为什么跑出来的结果如此糟糕呢?
其实,这里真正的问题是数据的量纲问题。回忆一下我们如何求解决策边界,如何判断点是否在决策边界的一边? 是靠计算”距离“,虽然我们不能说SVM是完全的距离类模型,但是它严重受到数据量纲的影响。让我们来探索一下 乳腺癌数据集的量纲:
标准化完毕后,再次让SVC在核函数中遍历,此时我们把degree的数值设定为1,观察各个核函数在去量纲后的数 据上的表现:
data=StandardScaler().fit_transform(data)
data=pd.DataFrame(data)
print(data.describe())
kernel=['linear','poly','sigmoid','rbf']
for i in kernel:
time0=time()
clf = SVC(kernel=i
, gamma="auto"
, degree = 1
, cache_size=5000
).fit(X_train, y_train)
print('accuracy:',clf.score(X_test,y_test))
print(datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))
控制台
accuracy: 0.9298245614035088
00:00:430454
accuracy: 0.9239766081871345
00:00:050045
accuracy: 0.5964912280701754
00:00:008492
accuracy: 0.5964912280701754
00:00:014117
量纲统一之后,可以观察到,所有核函数的运算时间都大大地减少了,尤其是对于线性核来说,而多项式核函数居 然变成了计算最快的。其次,rbf表现出了非常优秀的结果。经过我们的探索,我们可以得到的结论是:
- 线性核,尤其是多项式核函数在高次项时计算非常缓慢
- rbf和多项式核函数都不擅长处理量纲不统一的数据集
幸运的是,这两个缺点都可以由数据无量纲化来解决。因此,SVM执行之前,非常推荐先进行数据的无量纲化!到 了这一步,我们是否已经完成建模了呢?虽然线性核函数的效果是最好的,但它是没有核函数相关参数可以调整 的,rbf和多项式却还有着可以调整的相关参数,接下来我们就来看看这些参数。
3.2 选取与核函数相关的参数:degree & gamma & coef0
在知道如何选取核函数后,我们还要观察一下除了kernel之外的核函数相关的参数。对于线性核函数,"kernel"是 唯一能够影响它的参数,但是对于其他三种非线性核函数,他们还受到参数gamma,degree以及coef0的影响。 参数gamma就是表达式中的 ,degree就是多项式核函数的次数 ,参数coef0就是常数项 。其中,高斯径向基核函数受到gamma的影响,而多项式核函数受到全部三个参数的影响。
但从核函数的公式来看,我们其实很难去界定具体每个参数如何影响了SVM的表现。当gamma的符号变化,或者 degree的大小变化时,核函数本身甚至都不是永远单调的。所以如果我们想要彻底地理解这三个参数,我们要先推导出它们如何影响核函数地变化,再找出核函数的变化如何影响了我们的预测函数(可能改变我们的核变化所在的维度),再判断出决策边界随着预测函数的改变发生了怎样的变化。无论是从数学的角度来说还是从实践的角度来 说,这个过程太复杂也太低效。所以,我们往往避免去真正探究这些参数如何影响了我们的核函数,而直接使用学习曲线或者网格搜索来帮助我们查找最佳的参数组合。
对于高斯径向基核函数,调整gamma的方式其实比较容易,那就是画学习曲线。我们来试试看高斯径向基核函数 rbf的参数gamma在乳腺癌数据集上的表现:
通过学习曲线,很容就找出了rbf的最佳gamma值。但我们观察到,这其实与线性核函数的准确率一模一样之前的准确率。我们可以多次调整gamma_range来观察结果,可以发现97.6608应该是rbf核函数的极限了。
但对于多项式核函数来说,一切就没有那么容易了,因为三个参数共同作用在一个数学公式上影响它的效果,因此我们往往使用网格搜索来共同调整三个对多项式核函数有影响的参数。依然使用乳腺癌数据集。
arr=np.logspace(-1,10,50)
score=[]
for i in arr:
clf=SVC(kernel="linear",
gamma=i,
cache_size=5000).fit(X_train,y_train)
score.append(clf.score(X_test,y_test))
print(arr[score.index(max(score))],max(score))
plt.plot(arr,score,c="red")
plt.show()
控制台
0.1 0.9298245614035088
网格搜索调参完整代码
from sklearn.datasets import load_breast_cancer
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split,StratifiedShuffleSplit,GridSearchCV
import matplotlib.pyplot as plt
from time import time
from datetime import datetime
import numpy as np
from sklearn.preprocessing import StandardScaler
import pandas as pd
data=load_breast_cancer().data
target=load_breast_cancer().target
X_train,X_test,y_train,y_test=train_test_split(data,target,test_size=0.3,random_state=420)
data=StandardScaler().fit_transform(data)
data=pd.DataFrame(data)
time0 = time()
gamma_range = np.logspace(-10,1,20)
coef0_range = np.linspace(0,5,10)
param_grid = dict(gamma = gamma_range
,coef0 = coef0_range)
cv = StratifiedShuffleSplit(n_splits=5, test_size=0.3, random_state=420)
grid = GridSearchCV(SVC(kernel = "poly",degree=1,cache_size=5000),
param_grid=param_grid, cv=cv)
grid.fit(X_train, y_train)
print("The best parameters are %s with a score of %0.5f" % (grid.best_params_,
grid.best_score_))
print(datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))
可以发现,网格搜索为我们返回了参数coef0=0,gamma=0.18329807108324375,但整体的分数是0.96959,虽 然比调参前略有提高,但依然没有超过线性核函数核rbf的结果。可见,如果最初选择核函数的时候,你就发现多 项式的结果不如rbf和线性核函数,那就不要挣扎了,试试看调整rbf或者直接使用线性。
4 软间隔与重要参数C
当然,不是所有数据都是完全线性可分的。可能存在着一条直线能够将大部分数据点的类别划分正确,但无论如何也无法将全部的点分对,如同上图所展示的图,存在着混杂在红色类中的紫色点,这种情况下没有一条直线能够将两类数据完全分类正确。
关键概念:硬间隔与软间隔
当两组数据是完全线性可分,我们可以找出一个决策边界使得训练集上的分类误差为0,这两种数据就被称为 是存在”硬间隔“的。当两组数据几乎是完全线性可分的,但决策边界在训练集上存在较小的训练误差,这两种数据就被称为是存在”软间隔“。
这个时候,我们的决策边界就不是单纯地寻求最大边际了,因为对于软间隔地数据来说,边际越大被分错的样本也 就会越多,因此我们需要找出一个”最大边际“与”被分错的样本数量“之间的平衡。参数C用于权衡”训练样本的正确 分类“与”决策函数的边际最大化“两个不可同时完成的目标,希望找出一个平衡点来让模型的效果最佳。
在实际使用中,C和核函数的相关参数(gamma,degree等等)们搭配,往往是SVM调参的重点。与gamma不 同,C没有在对偶函数中出现,并且是明确了调参目标的,所以我们可以明确我们究竟是否需要训练集上的高精确 度来调整C的方向。默认情况下C为1,通常来说这都是一个合理的参数。 如果我们的数据很嘈杂,那我们往往减小 C。当然,我们也可以使用网格搜索或者学习曲线来调整C的值。
kernel="linear"
kernel=“rbf”
进一步细化
#调线性核函数
score = []
C_range = np.linspace(0.01,30,50) for i in C_range:
clf = SVC(kernel="linear",C=i,cache_size=5000).fit(Xtrain,Ytrain)
score.append(clf.score(Xtest,Ytest))
print(max(score), C_range[score.index(max(score))])
plt.plot(C_range,score)
plt.show()
#换rbf
score = []
C_range = np.linspace(0.01,30,50) for i in C_range:
clf = SVC(kernel="rbf",C=i,gamma =
0.012742749857031322,cache_size=5000).fit(Xtrain,Ytrain)
score.append(clf.score(Xtest,Ytest))
print(max(score), C_range[score.index(max(score))])
plt.plot(C_range,score)
plt.show()
#进一步细化
score = []
C_range = np.linspace(5,7,50) for i in C_range:
clf = SVC(kernel="rbf",C=i,gamma =
0.012742749857031322,cache_size=5000).fit(Xtrain,Ytrain)
score.append(clf.score(Xtest,Ytest))
print(max(score), C_range[score.index(max(score))])
plt.plot(C_range,score)
plt.show()
此时,我们找到了乳腺癌数据集上的最优解:rbf核函数下的98.24%的准确率。当然,我们还可以使用交叉验证来 改进我们的模型,获得不同测试集和训练集上的交叉验证结果。但上述过程,为大家展现了如何选择正确的核函 数,以及如何调整核函数的参数,过程虽然简单,但是希望可以对大家有所启发。