1 概述
支持向量机(SVM,也称为支持向量网络),是机器学习中获得关注最多的算法没有之一。它源于统计学习理论,是我们除了集成算法之外,接触的第一个强学习器。它有多强呢?
从算法的功能来看,SVM几乎囊括了我们前六周讲解的所有算法的功能:
从分类效力来讲,SVM在无论线性还是非线性分类中,都是明星般的存在:
从实际应用来看,SVM在各种实际问题中都表现非常优秀。它在手写识别数字和人脸识别中应用广泛,在文本和超文本的分类中举足轻重,因为SVM可以大量减少标准归纳(standard inductive)和转换设置(transductive settings)中对标记训练实例的需求。同时,SVM也被用来执行图像的分类,并用于图像分割系统。实验结果表明,在仅仅三到四轮相关反馈之后,SVM就能实现比传统的查询细化方案(query refinement schemes)高出一大截的搜索精度。除此之外,生物学和许多其他科学都是SVM的青睐者,SVM现在已经广泛被用于蛋白质分类,现在化合物分类的业界平均水平可以达到90%以上的准确率。在生物科学的尖端研究中,人们还使用支持向量机来识别用于模型预测的各种特征,以找出各种基因表现结果的影响因素。
从学术的角度来看,SVM是最接近深度学习的机器学习算法。线性SVM可以看成是神经网络的单个神经元(虽然损失函数与神经网络不同),非线性的SVM则与两层的神经网络相当,非线性的SVM中如果添加多个核函数,则可以模仿多层的神经网络。而从数学的角度来看,SVM的数学原理是公认的对初学者来说难于上青天的水平,对于没有数学基础和数学逻辑熏陶的人来说,探究SVM的数学原理本身宛如在知识的荒原上跋涉。
当然了,没有算法是完美的,比SVM强大的算法在集成学习和深度学习中还有很多很多。但不可否认,它是我们目前为止接触到的最强大的算法。接下来的两周,我们将一起来探索SVM的神秘世界。
1.1 支持向量机分类器是如何工作的
支持向量机所作的事情其实非常容易理解。先来看看下面这一组数据的分布,这是一组两种标签的数据,两种标签分别由圆和方块代表。支持向量机的分类方法,是在这组分布中找出一个超平面作为决策边界,使模型在数据上的分类误差尽量接近于小,尤其是在未知数据集上的分类误差(泛化误差)尽量小。
关键概念:超平面
在几何中,超平面是一个空间的子空间,它是维度比所在空间小一维的空间。 如果数据空间本身是三维的,则其超平面是二维平面,而如果数据空间本身是二维的,则其超平面是一维的直线。
在二分类问题中,如果一个超平面能够将数据划分为两个集合,其中每个集合中包含单独的一个类别,我们就说这个超平面是数据的“决策边界”。
决策边界一侧的所有点在分类为属于一个类,而另一侧的所有点分类属于另一个类。如果我们能够找出决策边界,分类问题就可以变成探讨每个样本对于决策边界而言的相对位置。比如上面的数据分布,我们很容易就可以在方块和圆的中间画出一条线,并让所有落在直线左边的样本被分类为方块,在直线右边的样本被分类为圆。如果把数据当作我们的训练集,只要直线的一边只有一种类型的数据,就没有分类错误,我们的训练误差就会为0。但是,对于一个数据集来说,让训练误差为0的决策边界可以有无数条。
单击 “编辑” 按钮更改此文本。这是测试文本。
单击 “编辑” 按钮更改此文本。这是测试文本。
支持向量机,就是通过找出边际最大的决策边界,来对数据进行分类的分类器。也因此,支持向量分类器又叫做最大边际分类器。这个过程在二维平面中看起来十分简单,但将上述过程使用数学表达出来,就不是一件简单的事情了。
1.2 支持向量机原理的三层理解
目标是”找出边际最大的决策边界”,听起来是一个十分熟悉的表达,这是一个最优化问题,而最优化问题往往和损失函数联系在一起。和逻辑回归中的过程一样,SVM也是通过最小化损失函数来求解一个用于后续模型使用的重要信息:决策边界。
1.2 sklearn中的支持向量机
注意,除了特别表明是线性的两个类LinearSVC和LinearSVR之外,其他的所有类都是同时支持线性和非线性的。NuSVC和NuSVC可以手动调节支持向量的数目,其他参数都与最常用的SVC和SVR一致。注意OneClassSVM是无监督的类。
除了本身所带的类之外,sklearn还提供了直接调用libsvm库的几个函数。Libsvm是台湾大学林智仁(Lin Chih-Jen)教授等人开发设计的一个简单、易于使用和快速有效的英文的SVM库,它提供了大量SVM的底层计算和参数选择,也是sklearn的众多类背后所调用的库。目前,LIBSVM拥有C、Java、Matlab、Python、R等数十种语言版本,每种语言版本都可以在libsvm的官网上进行下载:
https://www.csie.ntu.edu.tw/~cjlin/libsvm/
2 sklearn.svm.SVC
class 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)
2.1 线性SVM用于分类的原理
2.1.1 线性SVM的损失函数详解
其中[a, -1]就是我们的参数向量 , W,X就是我们的特征向量, b是我们的截距。注意,这个表达式长得非常像我们线性回归的公式:
线性回归中等号的一边是标签,回归过后会拟合出一个标签,而决策边界的表达式中却没有标签的存在,全部是由参数,特征和截距组成的一个式子,等号的一边是0。
核心误区:p和r的符号
注意,在这里,p和r的符号是我们人为规定的。在一些博客或教材中,会认为p和r的符号是由原本的决策边界上下移动得到。这是一种误解。
如果k和k’是由原本的决策边界平移得到的话,紫色的点在决策边界上方, wx+b=0应该要向上平移,直线向上平移的话是增加截距,也就是说应该写作 wx+b+一 个 正 数=0,那p在等号的右边,怎么可能是一个大于0的数呢?同理,向下平移的话应该是截距减小,所以 也不可能是一个小于0的数。所以p和r的符号,不完全是平移的结果。
有人说,“直线以上的点带入直线为正,直线以下的点带入直线为负”是直线的性质,这又是另一种误解。假设我们有穿过圆点的直线 y=x,我们取点(x,y) = (0,1)这个在直线上的点为例,如果直线的表达式写作,则点(0,1)带入后为正1,如果我们将直线的表达式写作y-x=0 ,则带入(0,1)后结果为-1。所以,一个点在直线的上方,究竟会返回什么样的符号,是跟直线的表达式的写法有关的,不是直线上的点都为正,直线下的点都为负。
可能细心的小伙伴会发现,我们规定了p 和 r的符号与标签的符号一致,所以有人会说, p和r的符号,由所代表的点的标签的符号决定。这不是完全错误的,但这种说法无法解释,为什么我们就可以这样规定。并且,标签可以不是{-1,1},可以是{0, 1},可以是{1,2},两个标签之间并不需要是彼此的负数,标签的取值其实也是我们规定的。
那p和r的符号,到底是依据什么来定的呢?数学中很多过程,都是可以取巧的,来看以下过程。记得我们的决策边界如果写成矩阵,可以表示为:
为了推导和计算的简便,我们规定:
标签是{-1,1}
决策边界以上的点,标签都为正,并且通过调整w和 b的符号,让这个点在wx+b 上得出的结果为正。
决策边界以下的点,标签都为负,并且通过调整w和b的符号,让这个点在wx+b 上得出的结果为负。
结论:决策边界以上的点都为正,以下的点都为负,是我们为了计算简便,而人为规定的。这种规定,不会影响对参数向量w和截距b的求解。
1. 导入需要的模块
from sklearn.datasets import make_blobs
from sklearn.svm import SVC
import matplotlib.pyplot as plt
import numpy as np
2. 实例化数据集,可视化数据集
X,y = make_blobs(n_samples=50, centers=2, random_state=0,cluster_std=0.6)
plt.scatter(X[:,0],X[:,1],c=y,s=50,cmap="rainbow")
plt.xticks([])
plt.yticks([])
plt.show()
3. 画决策边界:理解函数contour
matplotlib.axes.Axes.contour([X, Y,] Z, [levels], **kwargs)
Contour是我们专门用来绘制等高线的函数。等高线,本质上是在二维图像上表现三维图像的一种形式,其中两维X和Y是两条坐标轴上的取值,而Z表示高度。Contour就是将由X和Y构成平面上的所有点中,高度一致的点连接成线段的函数,在同一条等高线上的点一定具有相同的Z值。我们可以利用这个性质来绘制我们的决策边界。
回忆一下,我们的决策边界是 wx+b=0,并在决策边界的两边找出两个超平面,使得超平面到决策边界的相对距离为1。那其实,我们只需要在我们的样本构成的平面上,把所有到决策边界的距离为0的点相连,就是我们的决策边界,而把所有到决策边界的相对距离为1的点相连,就是我们的两个平行于决策边界的超平面了。此时,我们的Z就是平面上的任意点到达超平面的距离。
那首先,我们需要获取样本构成的平面,作为一个对象.
#首先要有散点图
plt.scatter(X[:,0],X[:,1],c=y,s=50,cmap="rainbow")
ax = plt.gca() #获取当前的子图,如果不存在,则创建新的子图
有了这个平面,我们需要在平面上制作一个足够细的网格,来代表我们“平面上的所有点”。
4. 画决策边界:制作网格,理解函数meshgrid
#获取平面上两条坐标轴的最大值和最小值
xlim = ax.get_xlim()
ylim = ax.get_ylim()
#在最大值和最小值之间形成30个规律的数据
axisx = np.linspace(xlim[0],xlim[1],30)
axisy = np.linspace(ylim[0],ylim[1],30)
axisy,axisx = np.meshgrid(axisy,axisx)
#我们将使用这里形成的二维数组作为我们contour函数中的X和Y
#使用meshgrid函数将两个一维向量转换为特征矩阵
#核心是将两个特征向量广播,以便获取y.shape * x.shape这么多个坐标点的横坐标和纵坐标
xy = np.vstack([axisx.ravel(), axisy.ravel()]).T
#其中ravel()是降维函数,vstack能够将多个结构一致的一维数组按行堆叠起来
#xy就是已经形成的网格,它是遍布在整个画布上的密集的点
plt.scatter(xy[:,0],xy[:,1],s=1,cmap="rainbow")
#理解函数meshgrid和vstack的作用
a = np.array([1,2,3])
b = np.array([7,8])
#两两组合,会得到多少个坐标?
#答案是6个,分别是 (1,7),(2,7),(3,7),(1,8),(2,8),(3,8)
v1,v2 = np.meshgrid(a,b)
v1
v2
v = np.vstack([v1.ravel(), v2.ravel()]).T
有了网格后,我们需要计算网格所代表的“平面上所有的点”到我们的决策边界的距离。所以我们需要我们的模型和决策边界。
5. 建模,计算决策边界并找出网格上每个点到决策边界的距离
#建模,通过fit计算出对应的决策边界
clf = SVC(kernel = "linear").fit(X,y)
Z = clf.decision_function(xy).reshape(axisx.shape)
#重要接口decision_function,返回每个输入的样本所对应的到决策边界的距离
#然后再将这个距离转换为axisx的结构,这是由于画图的函数contour要求Z的结构必须与X和Y保持一致
#画决策边界和平行于决策边界的超平面
ax.contour(axisx,axisy,Z
,colors="k"
,levels=[-1,0,1] #画三条等高线,分别是Z为-1,Z为0和Z为1的三条线
,alpha=0.5
,linestyles=["--","-","--"])
ax.set_xlim(xlim)
ax.set_ylim(ylim)
#记得Z的本质么?是输入的样本到决策边界的距离,而contour函数中的level其实是输入了这个距离
#让我们用一个点来试试看
plt.scatter(X[:,0],X[:,1],c=y,s=50,cmap="rainbow")
plt.scatter(X[10,0],X[10,1],c="black",s=50,cmap="rainbow")
clf.decision_function(X[10].reshape(1,2))
plt.scatter(X[:,0],X[:,1],c=y,s=50,cmap="rainbow")
ax = plt.gca()
ax.contour(axisx,axisy,P
,colors="k"
,levels=[-3.33917354]
,alpha=0.5
,linestyles=["--"])
6. 将绘图过程包装成函数
#将上述过程包装成函数:
def plot_svc_decision_function(model,ax=None):
if ax is None:
ax = plt.gca()
xlim = ax.get_xlim()
ylim = ax.get_ylim()
x = np.linspace(xlim[0],xlim[1],30)
y = np.linspace(ylim[0],ylim[1],30)
Y,X = np.meshgrid(y,x)
xy = np.vstack([X.ravel(), Y.ravel()]).T
P = model.decision_function(xy).reshape(X.shape)
ax.contour(X, Y, P,colors="k",levels=[-1,0,1],alpha=0.5,linestyles=["--","-","--"])
ax.set_xlim(xlim)
ax.set_ylim(ylim)
#则整个绘图过程可以写作:
clf = SVC(kernel = "linear").fit(X,y)
plt.scatter(X[:,0],X[:,1],c=y,s=50,cmap="rainbow")
plot_svc_decision_function(clf)
7. 探索建好的模型
clf.predict(X)
#根据决策边界,对X中的样本进行分类,返回的结构为n_samples
clf.score(X,y)
#返回给定测试数据和标签的平均准确度
clf.support_vectors_
#返回支持向量
clf.n_support_
#返回每个类中支持向量的个数
8. 推广到非线性情况
我们之前所讲解的原理,以及绘图的过程,都是基于数据本身是线性可分的情况。如果把数据推广到非线性数据,比如说环形数据上呢?
from sklearn.datasets import make_circles
X,y = make_circles(100, factor=0.1, noise=.1)
X.shape
y.shape
plt.scatter(X[:,0],X[:,1],c=y,s=50,cmap="rainbow")
plt.show()
试试看用我们已经定义的函数来划分这个数据的决策边界:
clf = SVC(kernel = "linear").fit(X,y)
plt.scatter(X[:,0],X[:,1],c=y,s=50,cmap="rainbow")
plot_svc_decision_function(clf)
明显,现在线性SVM已经不适合于我们的状况了,我们无法找出一条直线来划分我们的数据集,让直线的两边分别是两种类别。这个时候,如果我们能够在原本的X和y的基础上,添加一个维度r,变成三维,我们可视化这个数据,来看看添加维度让我们的数据如何变化。
9. 为非线性数据增加维度并绘制3D图像
#定义一个由x计算出来的新维度r
r = np.exp(-(X**2).sum(1))
rlim = np.linspace(min(r),max(r),0.2)
from mpl_toolkits import mplot3d
#定义一个绘制三维图像的函数
#elev表示上下旋转的角度
#azim表示平行旋转的角度
def plot_3D(elev=30,azim=30,X=X,y=y):
ax = plt.subplot(projection="3d")
ax.scatter3D(X[:,0],X[:,1],r,c=y,s=50,cmap='rainbow')
ax.view_init(elev=elev,azim=azim)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("r")
plt.show()
plot_3D()
可以看见,此时此刻我们的数据明显是线性可分的了:我们可以使用一个平面来将数据完全分开,并使平面的上方的所有数据点为一类,平面下方的所有数据点为另一类。
10. 将上述过程放到Jupyter Notebook中运行
#如果放到jupyter notebook中运行
from sklearn.svm import SVC
import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import make_circles
X,y = make_circles(100, factor=0.1, noise=.1)
plt.scatter(X[:,0],X[:,1],c=y,s=50,cmap="rainbow")
def plot_svc_decision_function(model,ax=None):
if ax is None:
ax = plt.gca()
xlim = ax.get_xlim()
ylim = ax.get_ylim()
x = np.linspace(xlim[0],xlim[1],30)
y = np.linspace(ylim[0],ylim[1],30)
Y,X = np.meshgrid(y,x)
xy = np.vstack([X.ravel(), Y.ravel()]).T
P = model.decision_function(xy).reshape(X.shape)
ax.contour(X, Y, P,colors="k",levels=[-1,0,1],alpha=0.5,linestyles=["--","-","--"])
ax.set_xlim(xlim)
ax.set_ylim(ylim)
clf = SVC(kernel = "linear").fit(X,y)
plt.scatter(X[:,0],X[:,1],c=y,s=50,cmap="rainbow")
plot_svc_decision_function(clf)
r = np.exp(-(X**2).sum(1))
rlim = np.linspace(min(r),max(r),0.2)
from mpl_toolkits import mplot3d
def plot_3D(elev=30,azim=30,X=X,y=y):
ax = plt.subplot(projection="3d")
ax.scatter3D(X[:,0],X[:,1],r,c=y,s=50,cmap='rainbow')
ax.view_init(elev=elev,azim=azim)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("r")
plt.show()
from ipywidgets import interact,fixed
interact(plot_3D,elev=[0,30],azip=(-180,180),X=fixed(X),y=fixed(y))
plt.show()
此时我们的数据在三维空间中,我们的超平面就是一个二维平面。明显我们可以用一个平面将两类数据隔开,这个平面就是我们的决策边界了。我们刚才做的,计算r,并将r作为数据的第三维度来将数据升维的过程,被称为“核变换”,即是将数据投影到高维空间中,以寻找能够将数据完美分割的超平面,即是说寻找能够让数据线性可分的高维空间。为了详细解释这个过程,我们需要引入SVM中的核心概念:核函数。
from sklearn.datasets import make_blobs
from sklearn.svm import SVC
import matplotlib.pyplot as plt
import numpy as np
可以看出,除了选项”linear”之外,其他核函数都可以处理非线性问题。多项式核函数有次数d,当d为1的时候它就是再处理线性问题,当d为更高次项的时候它就是在处理非线性问题。我们之前画图时使用的是选项“linear”,自然不能处理环形数据这样非线性的状况。而刚才我们使用的计算r的方法,其实是高斯径向基核函数所对应的功能,在参数”kernel“中输入”rbf“就可以使用这种核函数。我们来看看模型找出的决策边界时什么样:
clf = SVC(kernel = "rbf").fit(X,y)
plt.scatter(X[:,0],X[:,1],c=y,s=50,cmap="rainbow")
plot_svc_decision_function(clf)
可以看到,决策边界被完美地找了出来。
2.2.3 探索核函数在不同数据集上的表现
除了”linear”以外的核函数都能够处理非线性情况,那究竟什么时候选择哪一个核函数呢?遗憾的是,关于核函数在不同数据集上的研究甚少,谷歌学术上的论文中也没有几篇是研究核函数在SVM中的运用的,更多的是关于核函数在深度学习,神经网络中如何使用。在sklearn中,也没有提供任何关于如何选取核函数的信息。
但无论如何,我们还是可以通过在不同的核函数中循环去找寻最佳的核函数来对核函数进行一个选取。接下来我们就通过一个例子,来探索一下不同数据集上核函数的表现。我们现在有一系列线性或非线性可分的数据,我们希望通过绘制SVC在不同核函数下的决策边界并计算SVC在不同核函数下分类准确率来观察核函数的效用。
1. 导入所需要的库和模块
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
2. 创建数据集,定义核函数的选择
n_samples = 100
datasets = [
make_moons(n_samples=n_samples, noise=0.2, random_state=0),
make_circles(n_samples=n_samples, noise=0.2, factor=0.5, random_state=1),
make_blobs(n_samples=n_samples, centers=2, random_state=5),
make_classification(n_samples=n_samples,n_features =
2,n_informative=2,n_redundant=0, random_state=5)
]
Kernel = ["linear","poly","rbf","sigmoid"]
#四个数据集分别是什么样子呢?
for X,Y in datasets:
plt.figure(figsize=(5,4))
plt.scatter(X[:,0],X[:,1],c=Y,s=50,cmap="rainbow")
我们总共有四个数据集,四种核函数,我们希望观察每种数据集下每个核函数的表现。以核函数为列,以图像分布为行,我们总共需要16个子图来展示分类结果。而同时,我们还希望观察图像本身的状况,所以我们总共需要20个子图,其中第一列是原始图像分布,后面四列分别是这种分布下不同核函数的表现。
3. 构建子图
nrows=len(datasets)
ncols=len(Kernel) + 1
fig, axes = plt.subplots(nrows, ncols,figsize=(20,16))
4. 开始进行子图循环
#第一层循环:在不同的数据集中循环
for ds_cnt, (X,Y) in enumerate(datasets):
#在图像中的第一列,放置原数据的分布
ax = axes[ds_cnt, 0]
if ds_cnt == 0:
ax.set_title("Input data")
ax.scatter(X[:, 0], X[:, 1], c=Y, zorder=10, cmap=plt.cm.Paired,edgecolors='k')
ax.set_xticks(())
ax.set_yticks(())
#第二层循环:在不同的核函数中循环
#从图像的第二列开始,一个个填充分类结果
for est_idx, kernel in enumerate(Kernel):
#定义子图位置
ax = axes[ds_cnt, est_idx + 1]
#建模
clf = svm.SVC(kernel=kernel, gamma=2).fit(X, Y)
score = clf.score(X, Y)
#绘制图像本身分布的散点图
ax.scatter(X[:, 0], X[:, 1], c=Y
,zorder=10
,cmap=plt.cm.Paired,edgecolors='k')
#绘制支持向量
ax.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1], s=50,
facecolors='none', zorder=10, edgecolors='k')
#绘制决策边界
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
#np.mgrid,合并了我们之前使用的np.linspace和np.meshgrid的用法
#一次性使用最大值和最小值来生成网格
#表示为[起始值:结束值:步长]
#如果步长是复数,则其整数部分就是起始值和结束值之间创建的点的数量,并且结束值被包含在内
XX, YY = np.mgrid[x_min:x_max:200j, y_min:y_max:200j]
#np.c_,类似于np.vstack的功能
Z = clf.decision_function(np.c_[XX.ravel(), YY.ravel()]).reshape(XX.shape)
#填充等高线不同区域的颜色
ax.pcolormesh(XX, YY, Z > 0, cmap=plt.cm.Paired)
#绘制等高线
ax.contour(XX, YY, Z, colors=['k', 'k', 'k'], linestyles=['--', '-', '--'],
levels=[-1, 0, 1])
#设定坐标轴为不显示
ax.set_xticks(())
ax.set_yticks(())
#将标题放在第一行的顶上
if ds_cnt == 0:
ax.set_title(kernel)
#为每张图添加分类的分数
ax.text(0.95, 0.06, ('%.2f' % score).lstrip('0')
, size=15
, bbox=dict(boxstyle='round', alpha=0.8, facecolor='white')
#为分数添加一个白色的格子作为底色
, transform=ax.transAxes #确定文字所对应的坐标轴,就是ax子图的坐标轴本身
, horizontalalignment='right' #位于坐标轴的什么方向
)
plt.tight_layout()
plt.show()
可以观察到,线性核函数和多项式核函数在非线性数据上表现会浮动,如果数据相对线性可分,则表现不错,如果是像环形数据那样彻底不可分的,则表现糟糕。在线性数据集上,线性核函数和多项式核函数即便有扰动项也可以表现不错,可见多项式核函数是虽然也可以处理非线性情况,但更偏向于线性的功能。
Sigmoid核函数就比较尴尬了,它在非线性数据上强于两个线性核函数,但效果明显不如rbf,它在线性数据上完全比不上线性的核函数们,对扰动项的抵抗也比较弱,所以它功能比较弱小,很少被用到。
rbf,高斯径向基核函数基本在任何数据集上都表现不错,属于比较万能的核函数。我个人的经验是,无论如何先
试试看高斯径向基核函数,它适用于核转换到很高的空间的情况,在各种情况下往往效果都很不错,如果rbf效果
不好,那我们再试试看其他的核函数。另外,多项式核函数多被用于图像处理之中。
2.2.4 探索核函数的优势和缺陷
看起来,除了Sigmoid核函数,其他核函数效果都还不错。但其实rbf和poly都有自己的弊端,我们使用乳腺癌数据
集作为例子来展示一下:
然后我们发现,怎么跑都跑不出来。模型一直停留在线性核函数之后,就没有再打印结果了。这证明,多项式核函
数此时此刻要消耗大量的时间,运算非常的缓慢。让我们在循环中去掉多项式核函数,再试试看能否跑出结果:
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
import numpy as np
from time import time
import datetime
data = load_breast_cancer()
X = data.data
y = data.target
X.shape
np.unique(y)
plt.scatter(X[:,0],X[:,1],c=y)
plt.show()
Xtrain, Xtest, Ytrain, Ytest = train_test_split(X,y,test_size=0.3,random_state=420)
Kernel = ["linear","poly","rbf","sigmoid"]
for kernel in Kernel:
time0 = time()
clf= SVC(kernel = kernel
, gamma="auto"
# , degree = 1
, cache_size=5000
).fit(Xtrain,Ytrain)
print("The accuracy under kernel %s is %f" % (kernel,clf.score(Xtest,Ytest)))
print(datetime.datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))
然后我们发现,怎么跑都跑不出来。模型一直停留在线性核函数之后,就没有再打印结果了。这证明,多项式核函数此时此刻要消耗大量的时间,运算非常的缓慢。让我们在循环中去掉多项式核函数,再试试看能否跑出结果:
Kernel = ["linear","rbf","sigmoid"]
for kernel in Kernel:
time0 = time()
clf= SVC(kernel = kernel
, gamma="auto"
# , degree = 1
, cache_size=5000
).fit(Xtrain,Ytrain)
print("The accuracy under kernel %s is %f" % (kernel,clf.score(Xtest,Ytest)))
print(datetime.datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f")
我们可以有两个发现。首先,乳腺癌数据集是一个线性数据集,线性核函数跑出来的效果很好。rbf和sigmoid两个擅长非线性的数据从效果上来看完全不可用。其次,线性核函数的运行速度远远不如非线性的两个核函数。
如果数据是线性的,那如果我们把degree参数调整为1,多项式核函数应该也可以得到不错的结果:
Kernel = ["linear","poly","rbf","sigmoid"]
for kernel in Kernel:
time0 = time()
clf= SVC(kernel = kernel
, gamma="auto"
, degree = 1
, cache_size=5000
).fit(Xtrain,Ytrain)
print("The accuracy under kernel %s is %f" % (kernel,clf.score(Xtest,Ytest)))
print(datetime.datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))
多项式核函数的运行速度立刻加快了,并且精度也提升到了接近线性核函数的水平,可喜可贺。但是,我们之前的实验中,我们了解说,rbf在线性数据上也可以表现得非常好,那在这里,为什么跑出来的结果如此糟糕呢?其实,这里真正的问题是数据的量纲问题。回忆一下我们如何求解决策边界,如何判断点是否在决策边界的一边?是靠计算”距离“,虽然我们不能说SVM是完全的距离类模型,但是它严重受到数据量纲的影响。让我们来探索一下乳腺癌数据集的量纲:
import pandas as pd
data = pd.DataFrame(X)
data.describe([0.01,0.05,0.1,0.25,0.5,0.75,0.9,0.99]).T
一眼望去,果然数据存在严重的量纲不一的问题。我们来使用数据预处理中的标准化的类,对数据进行标准化:
from sklearn.preprocessing import StandardScaler
X = StandardScaler().fit_transform(X)
data = pd.DataFrame(X)
data.describe([0.01,0.05,0.1,0.25,0.5,0.75,0.9,0.99]).T
标准化完毕后,再次让SVC在核函数中遍历,此时我们把degree的数值设定为1,观察各个核函数在去量纲后的数据上的表现:
Xtrain, Xtest, Ytrain, Ytest = train_test_split(X,y,test_size=0.3,random_state=420)
Kernel = ["linear","poly","rbf","sigmoid"]
for kernel in Kernel:
time0 = time()
clf= SVC(kernel = kernel
, gamma="auto"
, degree = 1
, cache_size=5000
).fit(Xtrain,Ytrain)
print("The accuracy under kernel %s is %f" % (kernel,clf.score(Xtest,Ytest)))
print(datetime.datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))
量纲统一之后,可以观察到,所有核函数的运算时间都大大地减少了,尤其是对于线性核来说,而多项式核函数居然变成了计算最快的。其次,rbf表现出了非常优秀的结果。经过我们的探索,我们可以得到的结论是:
1. 线性核,尤其是多项式核函数在高次项时计算非常缓慢
2. rbf和多项式核函数都不擅长处理量纲不统一的数据集
幸运的是,这两个缺点都可以由数据无量纲化来解决。因此,SVM执行之前,非常推荐先进行数据的无量纲化!到了这一步,我们是否已经完成建模了呢?虽然线性核函数的效果是最好的,但它是没有核函数相关参数可以调整的,rbf和多项式却还有着可以调整的相关参数,接下来我们就来看看这些参数。
2.2.5 选取与核函数相关的参数:degree & gamma & coef0
在知道如何选取核函数后,我们还要观察一下除了kernel之外的核函数相关的参数。对于线性核函数,”kernel”是唯一能够影响它的参数,但是对于其他三种非线性核函数,他们还受到参数gamma,degree以及coef0的影响。
参数gamma就是表达式中的 ,degree就是多项式核函数的次数 ,参数coef0就是常数项 。其中,高斯径向基核函数受到gamma的影响,而多项式核函数受到全部三个参数的影响
score = []
gamma_range = np.logspace(-10, 1, 50) #返回在对数刻度上均匀间隔的数字
for i in gamma_range:
clf = SVC(kernel="rbf",gamma = i,cache_size=5000).fit(Xtrain,Ytrain)
score.append(clf.score(Xtest,Ytest))
print(max(score), gamma_range[score.index(max(score))])
plt.plot(gamma_range,score)
plt.show()
通过学习曲线,很容就找出了rbf的最佳gamma值。但我们观察到,这其实与线性核函数的准确率一模一样之前的准确率。我们可以多次调整gamma_range来观察结果,可以发现97.6608应该是rbf核函数的极限了。但对于多项式核函数来说,一切就没有那么容易了,因为三个参数共同作用在一个数学公式上影响它的效果,因此我们往往使用网格搜索来共同调整三个对多项式核函数有影响的参数。依然使用乳腺癌数据集。
from sklearn.model_selection import StratifiedShuffleSplit
from sklearn.model_selection import GridSearchCV
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, y)
print("The best parameters are %s with a score of %0.5f" % (grid.best_params_,
grid.best_score_))
print(datetime.datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))
可以发现,网格搜索为我们返回了参数coef0=0,gamma=0.18329807108324375,但整体的分数是0.96959,虽然比调参前略有提高,但依然没有超过线性核函数核rbf的结果。可见,如果最初选择核函数的时候,你就发现多项式的结果不如rbf和线性核函数,那就不要挣扎了,试试看调整rbf或者直接使用线性。
2.3 硬间隔与软间隔:重要参数C
2.3.1 SVM在软间隔数据上的推广
到这里,我们已经了解了线性SVC的基本原理,以及SVM如何被推广到非线性情况下,还了解了核函数的选择和应用。但实际上,我们依然没有完全了解sklearn当中的SVM用于二分类的全貌。我们之前在理论推导中使用的数据都有一个特点,那就是他们或是完全线性可分,或者是非线性的数据。在我们对比核函数时,实际上用到了一种不同的数据,那就是不完全线性可分的数据集。比如说如下数据集:
2.3.2 重要参数C
参数C用于权衡”训练样本的正确分类“与”决策函数的边际最大化“两个不可同时完成的目标,希望找出一个平衡点来让模型的效果最佳。
在实际使用中,C和核函数的相关参数(gamma,degree等等)们搭配,往往是SVM调参的重点。与gamma不同,C没有在对偶函数中出现,并且是明确了调参目标的,所以我们可以明确我们究竟是否需要训练集上的高精确度来调整C的方向。默认情况下C为1,通常来说这都是一个合理的参数。 如果我们的数据很嘈杂,那我们往往减小C。当然,我们也可以使用网格搜索或者学习曲线来调整C的值。
#调线性核函数
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%的准确率。当然,我们还可以使用交叉验证来改进我们的模型,获得不同测试集和训练集上的交叉验证结果。但上述过程,为大家展现了如何选择正确的核函数,以及如何调整核函数的参数,过程虽然简单,但是希望可以对大家有所启发。
2.4 总结
本周,我们讲解了支持向量机的原理,向大家介绍了支持向量机的损失函数,拉格朗日函数,拉格朗日对偶函数,预测函数以及这些函数在非线性,软间隔这些情况上的推广。我们介绍了四种核函数,包括它们的特点,适合什么样的数据,有什么相关数,优缺点,以及什么时候使用。最后我们还讲解了核函数在相关参数上的调参。本节课的内容非常多,但我们对SVM的探索还远远没有结束。下一周我们会继续讲解SVM,包括SVC的模型评价指标,SVC使用中的其他重要参数,重要属性和接口,以及其他重要问题,当然还有案例。希望大家尽力消化这一周的内容,SVC的探索道路依然任重而道远。
3 附录
3.1 SVC的参数列表
3.2 SVC的属性列表
3.3 SVC的接口列表
Comments NOTHING