PCtitle: 支持向量机—机器学习经典算法
date: 2021-04-16
tags:
- ML
- 基础
categories: - dataAnalysis
typora-copy-images-to: ./img
支持向量机(上篇)
1. 概述
支持向量机(SVM,也称为支持向量网络),是机器学习中获得关注最多的算法没有之一。它源于统计学习理论,是我们除了集成算法之外,接触的第一个强学习器。它有多强呢?
从算法的功能来看,SVM几乎囊括了我们前六周讲解的所有算法的功能:
功能 | |
---|---|
有监督学习 | 线性二分类与多分类,非线形二分类与多分类,普通连续型变量的回归,概率性变量的回归 |
无监督学习 | 支持向量聚类,异常值检测 |
半监督学习 | 转导支持向量机 |
从实际应用来看,SVM在各种实际问题中都表现非常优秀。它在手写识别数字和人脸识别中应用广泛,在文本和超文本的分类中举足轻重,因为SVM可以大量减少标准归纳(standard inductive)和转换设置(transductive settings)中对标记训练实例的需求。同时,SVM也被用来执行图像的分类,并用于图像分割系统。实验结果表明,在仅仅三到四轮相关反馈之后,SVM就能实现比传统的查询细化方案(query refifinement schemes)高出一大截的搜索精度。除此之外,生物学和许多其他科学都是SVM的青睐者,SVM现在已经广泛被用于蛋白质分类,现在化合物分类的业界平均水平可以达到90%以上的准确率。在生物科学的尖端研究中,人们还使用支持向量机来识别用于模型预测的各种特征,以找出各种基因表现结果的影响因素。
从学术的角度来看,SVM是最接近深度学习的机器学习算法。线性SVM可以看成是神经网络的单个神经元(虽然损失函数与神经网络不同),非线性的SVM则与两层的神经网络相当,非线性的SVM中如果添加多个核函数,则可以模仿多层的神经网络。而从数学的角度来看,SVM的数学原理是公认的对初学者来说难于上青天的水平,对于没有数学基础和数学逻辑熏陶的人来说,探究SVM的数学原理本身宛如在知识的荒原上跋涉。
当然了,没有算法是完美的,比SVM强大的算法在集成学习和深度学习中还有很多很多。但不可否认,它是我们目前为止接触到的最强大的算法。接下来的两周,我们将一起来探索SVM的神秘世界。
2. 支持向量机分类器是如何工作的
支持向量机所作的事情其实非常容易理解。先来看看下面这一组数据的分布,这是一组两种标签的数据,两种标签分别由圆和方块代表。支持向量机的分类方法,是在这组分布中找出一个超平面作为决策边界,使模型在数据上的分类误差尽量接近于小,尤其是在未知数据集上的分类误差尽量小
超平面:
在几何中,超平面是一个空间的子空间,它是维度比所在空间小一维的空间。 如果数据空间本身是三维的,则其超平面是二维平面,而如果数据空间本身是二维的,则其超平面是一维的直线。在二分类问题中,如果一个超平面能够将数据划分为两个集合,其中每个集合中包含单独的一个类别,我们就说这个超平面是数据的“决策边界”
决策边界一侧的所有点在分类属于一个类,而另一侧所有点分类属于另一个类。如果我们能够找出决策边界,分类问题就可以变成探讨每个样本对于决策边界而言的相对位置。比如上面的数据分布,我们很容易就可以在方块和圆的中间画出一条线,并让所有落在直线左边的样本被分类为方块,在直线右边的样本被分类为圆。如果把数据当作我们的训练集,只要直线的一边只有一种类型的数据,就没有分类错误,我们的训练误差就会为0。但是,对于一个数据集来说,让训练误差为0的决策边界可以有无数条。
但在此基础上,我们无法保证这条决策边界在未知数据集(测试集)上的表现也会优秀。对于现有的数据集来说,我们有B1和B2两条可能的决策边界。我们可以把决策边界B1向两边平移,直到碰到离这条决策边界最近的方块和圆圈后停下,形成两个新的超平面,分别是b11和b12,并且我们将原始的决策边界移动到 和 的中间,确保B1到b11和b12的距离相等。在b11和b12中间的距离,叫做B1这条决策边界的边际(margin),通常记作d。
为了简便,我们称b11和b12为“虚线超平面”,在其他博客或教材中可能有着其他的称呼,但大家知道是这两个超平面是由原来的决策边界向两边移动,直到碰到距离原来的决策边界最近的样本后停下而形成的超平面就可以了。
对B2也执行同样的操作,然后我们来对比一下两个决策边界。现在两条决策边界右边的数据都被判断为圆,左边的数据都被判断为方块,两条决策边界在现在的数据集上的训练误差都是0,没有一个样本被分错。
我们引入和原本的数据集相同分布的测试样本(红色所示),平面中的样本变多了,此时我们可以发现,对于B1而言,依然没有一个样本被分错,这条决策边界上的泛化误差也是0。但是对于 而言,却有三个方块被误人类成了圆,二有两个圆被误分类成了方块,这条决策边界上的泛化误差就远远大于B1了。这个例子表现出,拥有更大****边际的决策边界在分类中的泛化误差更小,这一点可以由结构风险最小化定律来证明(SRM)。如果边际很小,则任何轻微扰动都会对决策边界的分类产生很大的影响。**边际很小的情况,是一种模型在训练集上表现很好,却在测试集上表现糟糕的情况,所以会“过拟合”。所以我们在找寻决策边界的时候,希望边际越大越好。
**支持向量机,就是通过找出边际最大的决策边界,来对数据进行分类的分类器。**也因此,支持向量分类器又叫做最大边际分类器。这个过程在二维平面中看起来十分简单,但将上述过程使用数学表达出来,就不是一件简单的事情
了。
3. 支持向量机原理的三层理解
目标是"找出边际最大的决策边界",听起来是一个十分熟悉的表达,这是一个最优化问题,而最优化问题往往和损失函数联系在一起。和逻辑回归中的过程一样,SVM也是通过最小化损失函数来求解一个用于后续模型使用的重要信息:决策边界。
sklearn中的支持向量机
注意,除了特别标明是线性的两个类LinearSVC和LinearSVR之外,其他的所有类都是同时支持线性和非线性的。NuSVC和NuSVC可以手动调节支持向量的数目,其他参数都与最常用的SVC和SVR一致。注意OneClassSVM是无监督的类。
除了本身所带的类之外,sklearn还提供了直接调用libsvm库的几个函数。Libsvm是台湾大学林智仁(Lin Chih-Jen)教授等人开发设计的一个简单、易于使用和快速有效的英文的SVM库,它提供了大量SVM的底层计算和参数选择,也是sklearn的众多类背后所调用的库。目前,LIBSVM拥有C、Java、Matlab、Python、R等数十种语言版本,每种语言版本都可以在libsvm的官网上进行下载:
4. 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)
4.1. 线形SVM用于分类的原理
4.1.1. 线性SVM损失函数详解
核心误区:p与r的符号
注意,在这里,p和r的符号是我们人为规定的。在一些博客或教材中,会认为p和r的符号是由原本的决策边界上下移动得到。这是一种误解。
如果k和k’是由原本的决策边界平移得到的话,紫色的点在决策边界上方, 应该要向上平移,直
线向上平移的话是增加截距,也就是说应该写作W*x+b+ 一个正数=0 ,那p在等号的右边,怎么可能是一个大于0的数呢?同理,向下平移的话应该是截距减小,所以 也不可能是一个小于0的数。所以p和r的符号,不完全是平移的结果。
有人说,“直线以上的点带入直线为正,直线以下的点带入直线为负”是直线的性质,这又是另一种误解。假设我们有穿过圆点的直线y=x ,我们取点(x,y) = (0,1)这个在直线上的点为例,如果直线的表达式写作y-x=0,则点(0,1)带入后为正1,如果我们将直线的表达式写作x-y=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的求解。 |
有了这个理解,剩下的推导就简单多了。我们之前说过,决策边界的两边要有两个超平面,这两个超平面在二维空间中就是两条平行线(就是我们的虚线超平面),而他们之间的距离就是我们的边际 。而决策边界位于这两条线的中间,所以这两条平行线必然是对称的。(决策边界的表达式为w*x+b=0)所以另外两条平行线,设边际为2k。:
4.1.2. 函数间隔和几何间隔
4.1.3. 线型SVM的拉格朗日对偶函数和决策函数
有了我们的损失函数过后,我们就需要对损失函数进行求解。这个求解过程异常复杂,涉及到的数学的难度不是推导损失函数的部分可比。并且,在sklearn当中,我们作为使用者完全无法干涉这个求解的过程。因此作为使用sklearn的人,这部分属于进阶内容。如果实在对数学感到苦手,大家也可根据自己的需求选读。
我们之前得到了线形SVM损失的最初形态:
4.1.4. 将损失函数从最初形态转换为拉格朗日形态
-
为什么要进行转换?
-
为什么可以进行转换?
我们的损失函数是二次的(quadratic),并且我们损失函数中的约束条件在参数w和b下是线性的,求解这样的损失函数被称为“凸优化问题”(convex optimization problem)。拉格朗日乘数法正好可以用来解决凸优化问题,这种方法也是业界常用的,用来解决带约束条件,尤其是带有不等式的约束条件的函数的数学方法。首先第一步,我们需要使用拉格朗日乘数来将损失函数改写为考虑了约束条件的形式:
- 怎样进行转换?
4.1.5. 把拉格朗日函数转换为拉格朗日对偶函数
- 为什么要进行转换?
要求极值,最简单的方法还是对参数求导后让一阶导数等于0,我们先来试试看对拉格朗日函数求极值,在这里,我们对参数向量w和截距b分别求偏导并且让他们等于0,
-
为什么能够进行转换?
-
怎样进行转换?
4.1.6. 求解拉格朗日对偶函数及其后续过程
4.2 线形SVM决策过程的可视化
我们可以使用sklearn中的式子来为可视化我们的决策边界,支持向量,以及决策边界平行的两个超平面。
- 导入需要的模块
from sklearn.datasets import make_blobs
from sklearn.svm import SVC
import matplotlib.pyplot as plt
import numpy as np
- 实例化数据集,可视化数据集
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()
- 画决策边界:理解函数contour
matplotlib.axes.Axes.contour([X, Y,] Z, [levels], **kwargs)
Contour是我们专门用来绘制等高线的函数。等高线,本质上是在二维图像上表现三维图像的一种形式,其中两维X和Y是两条坐标轴上的取值,而Z表示高度。Contour就是将由X和Y构成平面上的所有点中,高度一致的点连接线段的函数,在同一条等高线上的点一定具有相同的Z值。我们可以利用这个性质来绘制我们的决策边界。
参数 | 含义 |
---|---|
X,Y | 选填,两维平面上所有的点的横纵坐标取值,一般要求是二维结构并且形状需要与Z相同。往往通过numpy.meshgrid()这样的函数来创建。如果X,Y都是一维,则Z的结构形状必须为(len(Y),len(X)).如果不填写,则默认X=range(Z.shape[1],Y=range(Z.shape[0])) |
Z | 必填,平面上所有的点对应的高度 |
levels | 可不填,不填默认显示所有的等高线,填写用于确定等高线的数量和位置,如果填写整数n,则显示n个数据区间,即绘制n+1条等高线,水平高度自己选择。如果填写的是数组或者列表,则在指定的高度级别绘制等高线,列表或数组中的值必须按照递增顺序排列。 |
回忆一下,我们的决策边界是 ,并在决策边界的两边找出两个超平面,使得超平面到决策边界的相对距离为1。那其实,我们只需要在我们的样本构成的平面上,把所有到决策边界的距离为0的点相连,就是我们的决策边界,而把所有到决策边界的相对距离为1的点相连,就是我们的两个平行于决策边界的超平面了。此时,我们的Z就是平面上的任意点到达超平面的距离。
那首先,我们需要获取样本构成的平面,作为一个对象。
#首先要有散点图
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap="rainbow")
ax = plt.gca() # 获取当前的子图,如果不存在,则创建新的子图
有了这个平面,我们需要在平面上制作一个足够细的网格,来“代表我们平面上的所有点”
- 画决策边界:制作网格,理解函数meshgrid
# 1. 获取平面上两条坐标轴的最大值和最小值
xlim = ax.get_xlim()
ylim = ax.get_ylim()
print(xlim, ylim)
(-0.7425578984849813, 3.3721920271976598)
(-0.41872382476349596, 5.754870487889891)
# 在最大值和最小值之间形成30个规律的数据
# 在横坐标和纵坐标分别取30个点
# np.linspace(起始点,终止点,数目)
axisx = np.linspace(xlim[0], xlim[1], 30)
axisy = np.linspace(ylim[0], ylim[1], 30)
print(axisx.shape) # (30,) 一维
axisy, axisx = np.meshgrid(axisy, axisx)
""""我们将使用这里形成的二维数组作为我们contour函数中的X和Y
使用meshgrid函数将两个一维向量转换为特征矩阵
核心是将两个特征向量广播,
以便获取y.shape * x.shape这么多个坐标点的横坐标和纵坐标
"""
print(axisx.shape) # (30,30) 二维
print(axisx.ravel().shape) # 拉平后(900,)
xy = np.vstack([axisx.ravel(), axisy.ravel()]).T #取出900个点
print(xy.shape) #[900,2]
# 其中ravel()是降维函数,vstack能够将多个结构一致的一维数组按行堆叠起来
# xy就是已经形成的网格,它是遍布在整个画布上的密集的点
#理解函数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)
print(v1)
print(v2)
v=np.vstack([v1.ravel(),v2.ravel()]).T
plt.scatter(xy[:, 0], xy[:, 1], s=1, cmap="rainbow")
plt.show()
-
建模,计算决策边界并找出网格上每个点到决策边界的距离
有了网格之后,我们需要计算网格所代表的“平面上所有的点”到我们决策边界的距离,所以我们需要我们的模型和决策边界。
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,0,1 , alpha=0.5 , linestyles=["--", "-", "--"] #第一条,第三条是虚线。第二条是实线 ) ax.set_xlim(xlim) ax.set_ylim(ylim)
-
绘图封装为一个函数
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) plt.show()
-
探索建好的模型
# 根据决策边界,对样本进行分类,返回的结构为n_samples print(clf.predict(X)) # [1 1 0 0 1 1 1 1 1 0 0 0 0 1 0 0 0 1 0 0 1 1 1 1 1 0 0 0 1 0 1 0 0 0 0 1 1 # 0 1 0 1 0 1 1 0 1 1 0 1 0] # 返回给定测数据和标签的平均准确度 print(clf.score(X, y)) #1.0 # 返回支持向量坐标 print(clf.support_vectors_) # [[0.44359863 3.11530945] # [2.33812285 3.43116792] # [2.06156753 1.96918596]] # 返回每个类中支持向量的个数 print(clf.n_support_) #[2 1]
-
推广到非线性情况
我们之前所讲解的原理,以及绘图的过程,都是基于数据本身是线性可分的情况。如果把数据推广到非线性数据.比如说环形数据上呢?
from sklearn.datasets import make_blobs from sklearn.datasets import make_circles from sklearn.svm import SVC import matplotlib.pyplot as plt import numpy as np X, y = make_circles(100, factor=0.1, noise=0.1) print(X.shape, y.shape) # (100,2), (100,) plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap="rainbow") plt.show()
X, y = make_circles(100, factor=0.1, noise=0.1)
print(X.shape, y.shape) # (100,2), (100,)
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)
plt.show()
print(clf.score(X,y)) #0.68在训练集上的分数也仅有0.68
明显,现在线性SVM已经不适合于我们的状况了,我们无法找出一条直线来划分我们的数据集,让直线的两边分别是两种类别。这个时候,如果我们能够在原本的X和y的基础上,添加一个维度r,变成三维,我们可视化这个数据,来看看添加维度让我们的数据如何变化。
- 为非线性数据增加维度并绘制3D图像
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_xlabel("y")
ax.set_xlabel("z")
plt.show()
plot_3D()
可以看见,此时此刻我们的数据明显是线性可分的了:我们可以使用一个平面来将数据完全分开,并使平面的上方的所有数据点为一类,平面下方的所有数据点为另一类。
此时我们的数据在三维空间中,我们的超平面就是一个二维平面。明显我们可以用一个平面将两类数据隔开,这个平面就是我们的决策边界了。我们刚才做的,计算r,并将r作为数据的第三维度来将数据升维的过程,被称为“核变换”,即是将数据投影到高维空间中,以寻找能够将数据完美分割的超平面,即是说寻找能够让数据线性可分的高维空间。为了详细解释这个过程,我们需要引入SVM中的核心概念:核函数。
5. 非线性SVM与核函数
5.1 SVC在非线性数据上的推广
5.2 重要参数kernel
from sklearn.datasets import make_blobs
from sklearn.datasets import make_circles
from sklearn.svm import SVC
import matplotlib.pyplot as plt
import numpy as np
X, y = make_circles(100, factor=0.1, noise=0.1)
print(X.shape, y.shape) # (100,2), (100,)
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="rbf").fit(X, y)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap="rainbow")
plot_svc_decision_function(clf)
plt.show()
5.3 探索核函数值在不同数据集上的表现
除了"linear"以外的核函数都能够处理非线性情况,那究竟什么时候选择哪一个核函数呢?遗憾的是,关于核函数在不同数据集上的研究甚少,谷歌学术上的论文中也没有几篇是研究核函数在SVM中的运用的,更多的是关于核函数在深度学习,神经网络中如何使用。在sklearn中,也没有提供任何关于如何选取核函数的信息。
但无论如何,我们还是可以通过在不同的核函数中循环去找寻最佳的核函数来对核函数进行一个选取。接下来我们就通过一个例子,来探索一下不同数据集上核函数的表现。我们现在有一系列线性或非线性可分的数据,我们希望通过绘制SVC在不同核函数下的决策边界并计算SVC在不同核函数下分类准确率来观察核函数的效用。
- 导入需要的库和模块
import numpy as np
from matplotlib.pyplot import plt
from matplotlib.colors import ListedColormap
from sklearn import svm
# 也可以from sklearn.svm import SVC
from sklearn.datasets import make_circles, make_moons, make_blobs, make_classification
- 创建数据集,定义核函数的选择
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=0),
make_blobs(n_samples=n_samples, centers=0.2, random_state=0),
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")
对于figsize=(5,4)我们总共有四个数据集,四种核函数,我们希望观察每种数据集下每个核函数的表现。以核函数为列,以图像分布为行,我们总共需要16个子图来展示分类结果。而同时,我们还希望观察图像本身的状况,所以我们总共需要20个子图,其中第一列是原始图像分布,后面四列分别是这种分布下不同核函数的表现。
- 构建子图
nrows = len(datasets)
ncols = len(Kernel) + 1
fig, axes = plt.subplots(nrows, ncols, figsize=(20, 16))
plt.show()
- 开始进行子图循环
# 开始子图循环
# 第一层循环,在不同的数据集中循环
print([*enumerate(datasets)]) # [(索引,array([特征矩阵X],[标签Y]))]
for ds_cnt, (X, Y) in enumerate(datasets):
# [(索引,array([特征矩阵X],[标签Y]))]
# 在图像的第一列,放置原数据的分布
# ds_cnt表示索引[0,1,2,3]
# (X, Y)表示特征矩阵,标签
ax = axes[ds_cnt, 0] # 第一列的第一行
if ds_cnt == 0:
ax.set_title('Input Data') # 在每一列的第一个设置标题。
# zorder = 10在哪一个层级画图。越大显示在越上面。表示散点图优先,放在最上面
# cmap = plt.cm.Paired散点图的颜色
ax.scatter(X[:, 0], X[:, 1], c=Y, zorder=10, cmap=plt.cm.Paired, edgecolors="k")
ax.set_xticks(())
ax.set_yticks(())
plt.tight_layout()
plt.show()
第二层循环:
# 从第二层循环:在不同的核函数中循环
# 从图像的第二列开始。一个个填充分类结果
print([*enumerate(Kernel)])
# [(0, 'linear'), (1, 'poly'), (2, 'rbf'), (3, 'sigmoid')]
for est_idx, kernel in enumerate(Kernel):
# 定义子图位置
ax = axes[ds_cnt, est_idx + 1] # 第0行的第一个图
# 建模
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')
# 绘制支持向量
# s = 50,尺寸
# facecolors = 'none',透明的
# zorder = 10,和散点图在同一个高度
# 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() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
# np.mgrid.合并了我们之前使用的np.linspace和np.meshgrid的用法
# 一次性使用最大值和最小值来生成网格
# 表示为[起始值:结束值:步长]
# 如果步长是复数,则其整数部分就是在起始值和结束值之间创建的点的数量,并且结束值包含在其中
# 200j就是复数,所以生成200个
XX, YY = np.mgrid[x_min:x_max:200j, y_min:y_max:200j]
# np.c类似于np.vstack的功能
""" # 点到决策边界距离
# reshape(XX.shape)为了能够放到contour函数里
"""
Z = clf.decision_function(np.c_[XX.ravel(), YY.ravel()]).reshape(XX.shape)
# 填充等高线不同区域的颜色
# Z>0的部分帮我填上一种颜色,Z<0的部分帮我随便选择另一种颜色
ax.pcolormesh(XX, YY, Z > 0, cmap=plt.cm.Paired)
# 绘制等高线
# 画三条线,这三条线到决策边界的距离分别是-1,0,1
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)
# 为每张图添加分类的分数
# 横坐标的位置0.95, ,纵坐标的位置0.06
# lstrip('0')不要显示0。只显示.XX
ax.text(0.95, 0.06, ('%.2f' % score).lstrip('0')
, size=15
# 为分数添加一个白色格子做底色
, bbox=dict(boxstyle='round', alpha=0.8, facecolor="white")
# 也就是0.95和0.06都是参照子图的坐标轴本身
, transform=ax.transAxes # 确定文字所对应的坐标轴,就是ax子图的坐标轴本身
, horizontalalignment='right' # 位于坐标轴的什么方向
)
plt.tight_layout()
plt.show()
可以观察到,线性核函数和多项式核函数在非线性数据上表现会浮动,如果数据相对线性可分,则表现不错,如果是像环形数据那样彻底不可分的,则表现糟糕。在线性数据集上,线性核函数和多项式核函数即便有扰动项也可以表现不错,可见多项式核函数是虽然也可以处理非线性情况,但更偏向于线性的功能。
Sigmoid核函数就比较尴尬了,它在非线性数据上强于两个线性核函数,但效果明显不如rbf,它在线性数据上完全比不上线性的核函数们,对扰动项的抵抗也比较弱,所以它功能比较弱小,很少被用到。
rbf,高斯径向基核函数基本在任何数据集上都表现不错,属于比较万能的核函数。我个人的经验是,无论如何先试试看高斯径向基核函数,它适用于核转换到很高的空间的情况,在各种情况下往往效果都很不错,如果rbf效果不好,那我们再试试看其他的核函数。另外,多项式核函数多被用于图像处理之中。
5.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
print(X.shape) # (569, 30)
print(np.unique(y)) # [0 1]
plt.scatter(X[:, 0], X[:, 1], c=y,s=30 )
plt.show()
data = load_breast_cancer()
X = data.data
y = data.target
print(X.shape) # (569, 30)
print(np.unique(y)) # [0 1]
# plt.scatter(X[:, 0], X[:, 1], c=y, s=30)
# 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 #表示次数,默认是3次方
, cache_size=5000 # cache_size表示你可以允许我使用多大的内存来进行计算,
# 默认200
# 越高,计算越快
).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"))
然后我们发现,怎么跑都没有跑出来,模型一直停留在线性核函数之后,就没有打印结果了。这证明,多项式核函数此时此刻要花大量的时间,运算非常缓慢,让我们在循环中去掉多项式核函数poly,再试试看能否跑出结果。
Kernel = ["linear","rbf", "sigmoid"]
for kernel in Kernel:
time0 = time()
clf = SVC(kernel=kernel
, gamma="auto"
# , degree = 1 #表示次数,默认是3次方
, cache_size=5000 # cache_size表示你可以允许我使用多大的内存来进行计算,
# 默认200
# 越高,计算越快
).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 # 表示次数,默认是3次方
, cache_size=5000 # cache_size表示你可以允许我使用多大的内存来进行计算,
# 默认200
# 越高,计算越快
).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
print(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)
print(data.describe([0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99]).T)
量纲统一之后,可以观察到,所有核函数的运算时间都大大地减少了,尤其是对于线性核来说,而多项式核函数居然变成了计算最快的。其次,rbf表现出了非常优秀的结果。经过我们的探索,我们可以得到的结论是:
-
线性核,尤其是多项式核函数在高次项时计算非常缓慢
-
rbf和多项式核函数都不擅长处理量纲不统一的数据集
幸运的是,这两个缺点都可以由数据无量纲化来解决。因此,**SVM执行之前,非常推荐先进行数据的无量纲化!**到了这一步,我们是否已经完成建模了呢?虽然线性核函数的效果是最好的,但它是没有核函数相关参数可以调整的,rbf和多项式却还有着可以调整的相关参数,接下来我们看看这些参数。
5.5 选取与核函数相关的参数degree&gamma&coef0
但从核函数的公式来看,我们其实很难去界定具体每个参数如何影响了SVM的表现。当gamma的符号变化,或者degree的大小变化时,核函数本身甚至都不是永远单调的。所以如果我们想要彻底地理解这三个参数,我们要先推导出它们如何影响核函数地变化,再找出核函数的变化如何影响了我们的预测函数(可能改变我们的核变化所在的维度),再判断出决策边界随着预测函数的改变发生了怎样的变化。无论是从数学的角度来说还是从实践的角度来说,这个过程太复杂也太低效。所以,我们往往避免去真正探究这些参数如何影响了我们的核函数,而直接使用学习曲线或者网格搜索来帮助我们查找最佳的参数组合。
对于高斯径向基核函数,调整gamma的方式其实比较容易,那就是画学习曲线。我们来试试看高斯径向基核函数rbf的参数gamma在乳腺癌数据集上的表现:
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
print(X.shape) # (569, 30)
print(np.unique(y)) # [0 1]
# 标准化
from sklearn.preprocessing import StandardScaler
X = StandardScaler().fit_transform(X)
Xtrain, Xtest, Ytrain, Ytest = train_test_split(X, y, test_size=0.3, random_state=420)
score = []
# np.logspace(起始值, 结束值, 数目) e^-10====>e^1中取50个数
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()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ci9TmCN0-1620549033756)(/Users/wadong/Desktop/blog/src/dataAnalysis/ML/img/image-20210509161354703.png)]
通过学习曲线,很容就找出了rbf的最佳gamma值。
关于多项式核函数的调参,就没有那么容易了。通过调整了参数之后,发现多项式结果不如rbf和线性函数,所以直接选择调整rbf或者直接使用线性。
6. 硬间隔与软间隔:重要参数
6.1 SVM在软间隔数据上的推广
6.2 重要参数C
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
import pandas as pd
data = load_breast_cancer()
X = data.data
y = data.target
print(X.shape) # (569, 30)
print(np.unique(y)) # [0 1]
from sklearn.preprocessing import StandardScaler
X=StandardScaler().fit_transform(X)
Xtrain, Xtest, Ytrain, Ytest = train_test_split(X, y, test_size=0.3, random_state=420)
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))])
#0.9766081871345029 1.2340816326530613
plt.plot(C_range, score)
plt.show()
# 换rbf
# 0.9766081871345029 0.012067926406393264
score = []
C_range = np.linspace(0.01, 10, 50)
for i in C_range:
clf = SVC(kernel="rbf", C=i, gamma=
0.012067926406393264, cache_size=5000).fit(Xtrain, Ytrain)
score.append(clf.score(Xtest, Ytest))
print(max(score), C_range[score.index(max(score))])
#0.9824561403508771 6.330204081632653
plt.plot(C_range, score)
plt.show()
此时,我们找到了乳腺癌数据集上的最优解.rbf核函数下,98.24%的准确率!当然,我们还可以使用交叉验证来改进我们的模型,获得不同测试集和训练集上的交叉验证结果。
参考
菜菜的sklearn课堂直播间: https://live.bilibili.com/12582510