第12章支持向量机
在机器学习中,支持向量机(Support Vector Machine,SVM)是最经典的算法之一,应用领域也非常广,其效果自然也是很厉害的。本章对支持向量机算法进行解读,详细分析其每一步流程及其参数对结果的影响。
12.1支持向量机工作原理
前面已经给大家讲解了一些机器学习算法,有没有发现其中的一些套路呢?它们都是从一个要解决的问题出发,然后将实际问题转换成数学问题,接下来优化求解即可。支持向量机涉及的数学内容比较多,下面还是从问题开始一步步解决。
12.1.1支持向量机要解决的问题
现在由一个小例子来引入支持向量机,图12-1中有两类数据点,目标就是找到一个最好的决策方程将它们区分开。
图12-1 决策方程的选择
图12-1中有3条直线都能将两类数据点区分开,那么,这3条线的效果相同吗?肯定是有所区别的。大家在做事情的时候,肯定希望能够做到最好,支持向量机也是如此,不只要做这件事,还要达到最好的效果,那么这3条线中哪条线的效果最好呢?现在放大划分的细节进行观察,如图12-2所示。
由图可见,最明显的一个区别,就是左边的决策边界看起来窄一点,而右边的宽一点。假设现在有一个大部队在道路上前进,左边埋着地雷,右边埋伏敌人,为了大部队能够最安全地前进,肯定希望选择的道路能够避开这些危险,也就是离左右两边都尽可能越远越好。
想法已经很明确,回到刚才的数据点中,选择更宽的决策边界更佳,因为这样才能离这些雷更远,中间部分可以看作隔离带,这样容忍错误能力更强,效果自然要比窄的好。
图12-2 边界的选择
12.1.2距离与标签定义
上一小节一直强调一定要避开危险的左右雷区,在数学上首先要明确指出离“雷区”的距离,也就是一个点(雷)到决策面的距离,然后才能继续优化目标。还是举一个例子,假设平面方程为wTx+b=0,平面上有x’和x”两个点,W为平面的法向量,要求x点到平面h的距离,如图12-3所示。既然x’和x”都在平面上,因此满足:
wTx’+b=0,wTx”+b=0 (12.1)
直接计算x点到平面的距离看起来有点难,可以转换一下,如果得到x到x’的距离后,再投影到平面的法向量方向上,就容易求解了,距离定义如下:
图12-3 点到决策边界的距离
其中,WT /||W||为平面法向量的方向,也就是要投影的方向,由于只是投影而已,所以只需得到投影方向的单位方向向量即可;(x-x’)为间接地通过x和x’计算距离;又由于x’在平面上,wTx’就等于−b。这样就有了距离的计算方法。
接下来开始定义数据集:(X1,Y1)(X2,Y2)...(Xn,Yn),其中,Xn为数据的特征,Yn为样本的标签。当Xn为正例时候,Yn=+1;当Xn为负例时候,Yn=−1。这样定义是为了之后的化简做准备,前面提到过,逻辑回归中定义的类别编号0和1也是为了化简。
最终的决策方程如下:
这个方程看起来很熟悉,其中x和y是已知的(数据中给出,有监督问题),目标就是要求解其中的参数,但是x怎么有点特别呢?其中φ(x)表示对数据进行了某种变换,这里可以先不管它,依旧把它当作数据即可。
对于任意输入样本数据x,有:
因此可得:
现在相信大家已经发现标签Y定义成±1的目的了,式(12.5)中的这个条件主要用于完成化简工作。
12.1.3目标函数
再来明确一下已知的信息和要完成的任务,根据前面介绍可知,目标就是找到一个最好的决策方程(也就是w和b的值),并且已知数据点到决策边界的距离计算方法。下面就要给出目标函数,大家都知道,机器学习的思想都是由一个实际的任务出发,将之转换成数学表达,再得到目标函数,最后去优化求解。
这里要优化的目标就是使得离决策方程最近的点(雷)能够越远越好,这句话看似简单,其实只要理解了以下两点,支持向量机已经弄懂一半了,再来解释一下。
1.为什么要选择离决策方程最近的点呢?可以这么想,如果你踩到了最近的“雷”,还需要验证更远的“雷”吗?也就是危险是由最近的“雷”带来的,只要避开它,其他的构不成威胁。
2.为什么越宽越好呢?因为在选择决策方程的时候,肯定是要找最宽的边界,越远离边界才能越安全。
目标函数非常重要,所有机器学习问题都可以归结为通过目标函数选择合适的方法并进行优化。
式(12.2)中已经给出了点到边界距离的定义,只不过是带有绝对值的,用起来好像有点麻烦,需要再对它进行简化。通过定义数据标签已经有结论,即yiy(xi)>0。其中的y(xi)就是wTx+b,由于标签值只能是±1,所以乘以它不会改变结果,但可以直接把绝对值去掉,用起来就方便多了,新的距离公式如下:
按照之前目标函数的想法,可以定义为:
遇到一个复杂的数学公式,可以尝试从里向外一步步观察。
式(12.7)看起来有点复杂,其实与之前的解释完全一致,首先min要求的就是距离,目的是找到离边界最近的样本点(雷)。然后再求的距离越大越好。
式(12.7)虽然给出了优化的目标,但看起来还是比较复杂,为了方便求解,还需对它进行一番化简,已知yiy(xi)>0,现在可以通过放缩变换把要求变得更严格,即
yi(wTφ(xi)+b)≥1,可得的最小值就是1。
因此只需考虑即可,现在得到了新的目标函数。但是,不要忘记它是有条件的。
概况如下:
式(12.8)是一个求极大值的问题,机器学习中常规套路就是转换成求极小值问题,因此可以转化为:
式(12.9)中,求一个数的最大值等同于求其倒数的极小值,条件依旧不变。求解过程中,关注的是极值点(也就是w和b取什么值),而非具体的极值大小,所以,对公式加入常数项或进行某些函数变换,只要保证极值点不变即可。现在有了要求解的目标,并且带有约束条件,接下来就是如何求解了。
12.1.4拉格朗日乘子法
拉格朗日乘子法用于计算有约束条件下函数的极值优化问题,计算式如下:
式(12.10)可以转化为:
回顾下式(12.9)给出的标函数和约束条件,是不是恰好满足拉格朗日乘子法的要求呢?接下来直接套用即可,注意约束条件只有一个:
有些同学可能对拉格朗日乘子法不是特别熟悉,式(12.12)中引入了一个乘子α,概述起来就像是原始要求解的w和b参数在约束条件下比较难解,能不能把问题转换一下呢?如果可以找到w和b分别与α的关系,接下来得到每一个合适的α值,自然也就可以求出最终w和b的值。
此处还有其中一个细节就是KKT条件,3个科学家做了对偶性质的证明,此处先不建议大家深入KKT细节,对初学者来说,就是从入门到放弃,先记住有这事即可,等从整体上掌握支持向量机之后,可以再做深入研究,暂且默认有一个定理可以帮我们把问题进行转化:
既然是要求解w和b以得到极值,需要对式(12.12)中w,b求偏导,并令其偏导等于0,可得:
现在似乎把求解w和b的过程转换成与α相关的问题,此处虽然没有直接得到b和α的关系,但是,在化简过程中,仍可基于对b参数进行化简。
接下来把上面的计算结果代入式(12.12),相当于把w和b全部替换成与α的关系,化简如下:
此时目标就是α值为多少时,式(12.15)中L(w,b,α)的值最大,这又是一个求极大值的问题,所以按照套路还是要转换成求极小值问题,相当于求其相反数的极小值:
约束条件中,是对b求偏导得到的,αi≥0是拉格朗日乘子法自身的限制条件,它们非常重要。到此为止,我们完成了支持向量机中的基本数学推导,剩下的就是如何求解。
12.2支持向量的作用
大家是否对支持向量基这个概念的来源有过疑问,在求解参数之前先向大家介绍支持向量的定义及其作用。
12.2.1支持向量机求解
式(12.16)中已经给出了要求解的目标,为了更直白地理解支持向量的含义,下述实例中,只取3个样本数据点,便于计算和化简。
假设现在有3个数据,其中正例样本为X1(3,3),X2(4,3);负例为X3(1,1),如图12-4所示。
首先,将3个样本数据点(x和y已知)代入式(12.16),可得:
图12-4 数据样本点
由于只有3个样本数据点,并且样本的标签已知,可得:
暂且认为φ(x)=x,其中(xi,xj)是求内积的意思,将3个样本数据点和条件α1+α2-α3=0代入式(12.17)可得:
既然要求极小值,对式(12.18)中分别计算偏导,并令偏导等于零,可得:两个结果并不满足给定的约束条件αi≥0。因此需要考虑边界上的情况,α1=0或α2=0。分别将这两个值代入上式,可得:
将式(12.19)结果分别代入公式,通过对比可知,S(0.25,0)时取得最小值,即a1=0.25,a2=0符合条件,此时a3=a1+a2=0.25。
由于之前已经得到w与α的关系,求解出来全部的α值之后,就可以计算w和b,可得:
求解b参数的时候,选择用其中一个样本点数据计算其结果,但是,该样本的选择必须为支持向量。
计算出所有参数之后,只需代入决策方程即可,最终的结果为:
这也是图12-4中所画直线,这个例子中α值可以直接求解,这是由于只选了3个样本数据点,但是,如果数据点继续增多,就很难直接求解,现阶段主要依靠SMO算法及其升级版本进行求解,其基本思想就是对α参数两两代入求解,感兴趣的读者可以找一份SMO求解代码,自己一行一行debug观察求解方法。
12.2.2支持向量的作用
在上述求解过程中,可以发现权重参数w的结果由α,x,y决定,其中x,y分别是数据和标签,这些都是固定不变的。如果求解出αi=0,意味着当前这个数据点不会对结果产生影响,因为它和x,y相乘后的值还为0。只有αi≠0时,对应的数据点才会对结果产生作用。
由图12-5可知,最终只有x1和X3参与到计算中,x2并没有起到任何作用。细心的读者可能还会发现x1和X3都是边界上的数据点,而x2与x1相比,就是非边界上的数据点。这些边界上的点,就是最开始的时候解释的离决策方程最近的“雷”,只有它们会对结果产生影响,而非边界上的点只是凑热闹罢了。
到此揭开了支持向量机名字的含义,对于边界上的数据点,例如x1和X3就叫作支持向量,它们把整个框架支撑起来。对于非边界上的点,自然就是非支持向量,它们不会对结果产生任何影响。
图12-6展示了支撑向量对结果的影响。图12-6(a)选择60个数据点,其中圈起来的就是支持向量。图12-6(b)选择120个数据点,但仍然保持支持向量不变,使用同样的算法和参数来建模,得到的结果完全相同。这与刚刚得到的结论一致,只要不改变支持向量,增加部分数据对结果没有任何影响。
▲图12-5 支持向量的作用
▲图12-6 支持向量对结果的影响
12.3支持向量机涉及参数
在建模过程中,肯定会涉及调参问题,那么在支持向量机中都有哪些参数呢?其中必不可缺的就是软间隔和核函数,本节向大家解释其作用。
12.3.1软间隔参数的选择
在机器学习任务中,经常会遇到过拟合问题,之前在定义目标函数的时候给出了非常严格的标准,就是要在满足能把两类数据点完全分得开的情况下,再考虑让决策边界越宽越好,但是,这么做一定能得到最好的结果吗?
假设有两类数据点分别为○和×,如果没有左上角的○,看起来这条虚线做得很不错,但是,如果把这个可能是异常或者离群的数据点考虑进去,结果就会发生较大变化,此时为了满足一个点的要求,只能用实线进行区分,决策边界一下子窄了好多,如图12-7所示。
总而言之,模型为了能够满足个别数据点做出了较大的牺牲,而这些数据点很可能是离群点、异常点等。如果还要严格要求模型必须做到完全分类正确,结果可能会适得其反。
如果在一定程度上放低对模型的要求,可以解决过拟合问题,先来看看定义方法:
图12-7 过拟合问题
观察发现,原来的约束条件中,要求yi(w(xi)+b)≥1,现在加入一个松弛因子,就相当于放低要求了。
此时,新的目标函数定义为:。在目标函数中,引入了一个新项,它与正则化惩罚的原理类似,用控制参数C表示严格程度。目标与之前一致,还是要求极小值,下面用两个较极端的例子看一下C参数的作用。
1.当C趋近于无穷大时,只有让ξi非常小,才能使得整体得到极小值。这是由于C参数比较大,如果ξi再大一些的话,就没法得到极小值,这意味着与之前的要求差不多,还是要让分类十分严格,不能产生错误。
2.当C趋近于无穷小时,即便ξi大一些也没关系,意味着模型可以有更大的错误容忍度,要求就没那么高,错几个数据点也没关系。
虽然目标函数发生了变换,求解过程依旧与之前的方法相同,下面直接列出来,了解一下即可。
此时约束定义为:
经过化简可得最终解:
支持向量机中的松弛因子比较重要,过拟合的模型通常没什么用,后续实验过程,就能看到它的强大了。
12.3.2核函数的作用
还记得式(12.3)中的φ(x)吗?它就是核函数。下面就来研究一下它对数据做了什么。大家知道可以对高维数据降维来提取主要信息,降维的目的就是找到更好的代表特征。那么数据能不能升维呢?低维的数据信息有点少,能不能用高维的数据信息来解决低维中不好解决的问题呢?这就是核函数要完成的任务。
假设有两类数据点,在低维空间中进行分类任务有些麻烦,但是,如果能找到一种变换方法,将低维空间的数据映射到高维空间中,这个问题看起来很容易解决,如图12-8所示。
图12-8 核函数作用
如何进行升维呢?先来看一个小例子,了解一下核函数的变换过程。需要大家考虑的另外一个问题是,如果数据维度大幅提升,对计算的要求自然更苛刻,在之前的求解的过程中可以发现,计算时需要考虑所有样本,因此计算内积十分麻烦,这是否大大增加求解难度呢?
假设有两个数据点:x=(x1,x2,x3),y=(x1,x2,x3),注意它们都是数据。假设在三维空间中已经不能对它们进行线性划分,既然提到高维的概念,那就使用一种函数变换,将这些数据映射到更高维的空间,例如映射到九维空间,假设映射函数如下:
F(x)=(x1x1,x2x2,x1x3,x2x1,x2x2,x2x3,x3x1,x3x2,x3x3)(12.26)
已知数据点x=(1,2,3),y=(4,5,6),代入式(12.26)可得F(x)=(1,2,3,2,4,5,3,6,9,),F(y)=(16,40,24,20,25,36,24,30,36)。求解过程中主要计算量就在内积运算上,则 。
这个计算看着很简单,但是,当数据样本很多并且数据维度也很大的时候,就会非常麻烦,因为要考虑所有数据样本点两两之间的内积。那么,能不能巧妙点解决这个问题呢?我们试着先在低维空间中进行内积计算,再把结果映射到高维当中,得到的数值竟然和在高维中进行内积计算的结果相同,其计算式为:
由此可得:K(x,y)=(<x,y>)2=<F(x),F(y)> 。
但是,K(x,y)的运算却比<F(x),F(y)>简单得多。也就是说,只需在低维空间进行计算,再把结果映射到高维空间中即可。虽然通过核函数变换得到了更多的特征信息,但是计算复杂度却没有发生本质的改变。这一巧合也成全了支持向量机,使得其可以处理绝大多数问题,而不受计算复杂度的限制。
通常说将数据投影到高维空间中,在高维上解决低维不可分问题实际只是做了一个假设,真正的计算依旧在低维当中,只需要把结果映射到高维即可。
在实际应用支持向量机的过程中,经常使用的核函数是高斯核函数,公式如下:
对于高斯核函数,其本身的数学内容比较复杂,直白些的理解是拿到原始数据后,先计算其两两样本之间的相似程度,然后用距离的度量表示数据的特征。如果数据很相似,那结果就是1。如果数据相差很大,结果就是0,表示不相似。如果对其进行泰勒展开,可以发现理论上高斯核函数可以把数据映射到无限多维。
还有一些核函数也能完成高维数据映射,但现阶段通用的还是高斯核函数,大家在应用过程中选择它即可。
如果做了核函数变换,能对结果产生什么影响呢?构建了一个线性不可分的数据集,如图12-9(a)所示。如果使用线性核函数(相当于不对数据做任何变换,直接用原始数据来建模),得到的结果并不尽如人意,实际效果很差。如果保持其他参数不变,加入高斯核函数进行数据变换,得到的结果如图12-9(b)所示,效果发生明显变化,原本很复杂的数据集就被完美地分开。
图12-9 核函数的作用
在高斯核函数中,还可以通过控制参数σ来决定数据变换的复杂程度,这对结果也会产生明显的影响,接下来开始完成这些实验,亲自动手体验一下支持向量机中参数对结果的影响。
12.4案例:参数对结果的影响
上一节列举了支持向量机中的松弛因子和核函数对结果的影响,本节就来实际动手看看其效果如何。首先使用sklearn工具包制作一份简易数据集,并完成分类任务。
12.4.1SVM基本模型
基于SVM的核心概念以及推导公式,下面完成建模任务,首先导入所需的工具包:
1 %matplotlib inline 2 #为了在notebook中画图展示 3 import numpy as np 4 import matplotlib.pyplot as plt 5 from scipy import stats 6 import seaborn as sns; sns.set()
接下来为了方便实验观察,利用sklearn工具包中的datasets模块生成一些数据集,当然大家也可以使用自己手头的数据:
1 #随机来点数据 2 #其中 cluster_std是数据的离散程度 3 from sklearn.datasets.samples_generator import make_blobs 4 X, y = make_blobs(n_samples=50, centers=2, 5 random_state=0, cluster_std=0.60) 6 plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
d: oolspython37libsite-packagessklearnutilsdeprecation.py:144: FutureWarning: The sklearn.datasets.samples_generator module is deprecated
in version 0.22 and will be removed in version 0.24. The corresponding classes / functions s
hould instead be imported from sklearn.datasets. Anything that cannot be imported from sklearn.datasets is now part of the private API. warnings.warn(message, FutureWarning)
Datasets.samples_generator是sklearn工具包中的数据生成器函数,可以定义生成数据的结构。Make_blobs是其中另一个函数,使用该函数时,只需指定样本数目n_samples、数据簇的个数centers、随机状态random_state以及其离散程度cluster_std等。
上述代码一共构建50个数据点,要进行二分类任务。从输出结果可以明显看出,这两类数据点非常容易分开,在中间随意画一条分割线就能完成任务。
1 #随便的画几条分割线,哪个好来这? 2 xfit = np.linspace(-1, 3.5) 3 plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn') 4 5 for m, b in [(1, 0.65), (0.5, 1.6), (-0.2, 2.9)]: 6 plt.plot(xfit, m * xfit + b, '-k') 7 #限制一下X的取值范围 8 plt.xlim(-1, 3.5);
上述输出结果绘制了3条不同的决策边界,都可以把两类数据点完全分开,但是哪个更好呢?这就回到支持向量机最基本的问题——如何找到最合适的决策边界,大家已经知道,肯定要找最宽的边界,可以把其边界距离绘制出来:
1 xfit = np.linspace(-1, 3.5) 2 plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn') 3 4 for m, b, d in [(1, 0.65, 0.33), (0.5, 1.6, 0.55), (-0.2, 2.9, 0.2)]: 5 yfit = m * xfit + b 6 plt.plot(xfit, yfit, '-k') 7 plt.fill_between(xfit, yfit - d, yfit + d, edgecolor='none', 8 color='#AAAAAA', alpha=0.4) 9 10 plt.xlim(-1, 3.5);
从上述输出结果可以发现,不同决策方程的宽窄差别很大,此时就轮到支持向量机登场了,来看看它是怎么决定最宽的边界的:
1 #分类任务 2 from sklearn.svm import SVC 3 #线性核函数 相当于不对数据进行变换 4 model = SVC(kernel='linear') 5 model.fit(X, y)
SVC(C=1.0, break_ties=False, cache_size=200, class_weight=None, coef0=0.0, decision_function_shape='ovr', degree=3, gamma='scale', kernel='linear', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False)
选择核函数是线性函数,其他参数暂用默认值,借助工具包能够很轻松地建立一个支持向量机模型,下面来绘制它的结果:
1 #绘图函数 2 def plot_svc_decision_function(model, ax=None, plot_support=True): 3 4 if ax is None: 5 ax = plt.gca() 6 xlim = ax.get_xlim() 7 ylim = ax.get_ylim() 8 9 # 用SVM自带的decision_function函数来绘制 10 x = np.linspace(xlim[0], xlim[1], 30) 11 y = np.linspace(ylim[0], ylim[1], 30) 12 Y, X = np.meshgrid(y, x) 13 xy = np.vstack([X.ravel(), Y.ravel()]).T 14 P = model.decision_function(xy).reshape(X.shape) 15 16 # 绘制决策边界 17 ax.contour(X, Y, P, colors='k', 18 levels=[-1, 0, 1], alpha=0.5, 19 linestyles=['--', '-', '--']) 20 21 # 绘制支持向量 22 if plot_support: 23 ax.scatter(model.support_vectors_[:, 0], 24 model.support_vectors_[:, 1], 25 s=300, linewidth=1, alpha=0.2); 26 ax.set_xlim(xlim) 27 ax.set_ylim(ylim) 28 29 plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn') 30 plot_svc_decision_function(model)
上述代码生成了SVM建模结果,预料之中,这里选到一个最宽的决策边界。其中被圈起来的就是支持向量,在sklearn中它们存储在 support_vectors_属性下,可以使用下面代码进行查看:
1 model.support_vectors_
array([[0.44359863, 3.11530945], [2.33812285, 3.43116792], [2.06156753, 1.96918596]])
接下来分别使用60个和120个数据点进行实验,保持支持向量不变,看看决策边界会不会发生变化:
1 def plot_svm(N=10, ax=None): 2 X, y = make_blobs(n_samples=200, centers=2, 3 random_state=0, cluster_std=0.60) 4 X = X[:N] 5 y = y[:N] 6 model = SVC(kernel='linear', C=1E10) 7 model.fit(X, y) 8 9 ax = ax or plt.gca() 10 ax.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn') 11 ax.set_xlim(-1, 4) 12 ax.set_ylim(-1, 6) 13 plot_svc_decision_function(model, ax) 14 # 分别对不同的数据点进行绘制 15 fig, ax = plt.subplots(1, 2, figsize=(16, 6)) 16 fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1) 17 for axi, N in zip(ax, [60, 120]): 18 plot_svm(N, axi) 19 axi.set_title('N = {0}'.format(N))
从上述输出结果可以看出,左边是60个数据点的建模结果,右边的是120个数据点的建模结果。它与原理推导时得到的答案一致,只有支持向量会对结果产生影响。
12.4.2核函数变换
接下来感受一下核函数的效果,同样使用datasets.samples_generator产生模拟数据,但是这次使用make_circles函数,随机产生环形数据集,加大了游戏难度,先来看看线性SVM能不能解决:
1 from sklearn.datasets.samples_generator import make_circles 2 # 绘制另外一种数据集 3 X, y = make_circles(100, factor=.1, noise=.1) 4 #看看这回线性和函数能解决嘛 5 clf = SVC(kernel='linear').fit(X, y) 6 7 plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn') 8 plot_svc_decision_function(clf, plot_support=False);
上图为线性SVM在解决环绕形数据集时得到的效果,有些差强人意。虽然在二维特征空间中做得不好,但如果映射到高维空间中,效果会不会好一些呢?可以想象一下三维空间中的效果:
1 #加入了新的维度r 2 from mpl_toolkits import mplot3d 3 r = np.exp(-(X ** 2).sum(1)) 4 # 可以想象一下在三维中把环形数据集进行上下拉伸 5 def plot_3D(elev=30, azim=30, X=X, y=y): 6 ax = plt.subplot(projection='3d') 7 ax.scatter3D(X[:, 0], X[:, 1], r, c=y, s=50, cmap='autumn') 8 ax.view_init(elev=elev, azim=azim) 9 ax.set_xlabel('x') 10 ax.set_ylabel('y') 11 ax.set_zlabel('r') 12 13 plot_3D(elev=45, azim=45, X=X, y=y)
上述代码自定义了一个新的维度,此时两类数据点很容易分开,这就是核函数变换的基本思想,下面使用高斯核函数完成同样的任务:
1 #加入高斯核函数 2 clf = SVC(kernel='rbf') 3 clf.fit(X, y)
SVC(C=1.0, break_ties=False, cache_size=200, class_weight=None, coef0=0.0, decision_function_shape='ovr', degree=3, gamma='scale', kernel='rbf', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False)
1 #这回厉害了! 2 plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn') 3 plot_svc_decision_function(clf) 4 plt.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1], 5 s=300, lw=1, facecolors='none');
核函数参数选择常用的高斯核函数’rbf’,其他参数保持不变。从上述输出结果可以看出,划分的结果发生了巨大的变化,看起来很轻松地解决了线性不可分问题,这就是支持向量机的强大之处。
12.4.3SVM参数选择
(1)松弛因子的选择。讲解支持向量机的原理时,曾提到过拟合问题,也就是软间隔(Soft Margin),其中松弛因子C用于控制标准有多严格。当C趋近于无穷大时,这意味着分类要求严格,不能有错误;当C趋近于无穷小时,意味着可以容忍一些错误。下面通过数据集实例验证其参数的大小对最终支持向量机模型的影响。首先生成一个模拟数据集,代码如下:
1 # 这份数据集中cluster_std稍微大一些,这样才能体现出软间隔的作用 2 X, y = make_blobs(n_samples=100, centers=2, 3 random_state=0, cluster_std=0.8) 4 plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
进一步加大游戏难度,来看一下松弛因子C可以发挥的作用:
1 #加大游戏难度的数据集 2 X, y = make_blobs(n_samples=100, centers=2, 3 random_state=0, cluster_std=0.8) 4 5 fig, ax = plt.subplots(1, 2, figsize=(16, 6)) 6 fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1) 7 # 选择两个C参数来进行对别实验,分别为10和0.1 8 for axi, C in zip(ax, [10.0, 0.1]): 9 model = SVC(kernel='linear', C=C).fit(X, y) 10 axi.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn') 11 plot_svc_decision_function(model, axi) 12 axi.scatter(model.support_vectors_[:, 0], 13 model.support_vectors_[:, 1], 14 s=300, lw=1, facecolors='none'); 15 axi.set_title('C = {0:.1f}'.format(C), size=14)
上述代码设定松弛因子控制参数C的值分别为10和0.1,其他参数保持不变,使用同一数据集训练支持向量机模型。上面左图对应松弛因子控制参数C为10时的建模结果,相当于对分类的要求比较严格,以分类对为前提,再去找最宽的决策边界,得到的结果虽然能够完全分类正确,但是边界实在是不够宽。右图对应松弛因子控制参数C为0.1时的建模结果,此时并不要求分类完全正确,有点错误也是可以容忍的,此时得到的结果中,虽然有些数据点“越界”了,但是整体还是很宽的。
对比不同松弛因子控制参数C对结果的影响后,结果的差异还是很大,所以在实际建模的时候,需要好好把控C值的选择,可以说松弛因子是支持向量机中的必调参数,当然具体的数值需要通过实验判断。
(2)gamma参数的选择。高斯核函数可以通过改变σ值进行不同的数据变换,在sklearn工具包中,σ对应着gamma参数值,以控制模型的复杂程度。Gamma值越大,模型复杂度越高;而gamma值越小,则模型复杂度越低。先进行实验,看一下其具体效果:
1 X, y = make_blobs(n_samples=100, centers=2, 2 random_state=0, cluster_std=1.1) 3 4 fig, ax = plt.subplots(1, 2, figsize=(16, 6)) 5 fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1) 6 # 选择不同的gamma值来观察建模效果 7 for axi, gamma in zip(ax, [10.0, 0.1]): 8 model = SVC(kernel='rbf', gamma=gamma).fit(X, y) 9 axi.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn') 10 plot_svc_decision_function(model, axi) 11 axi.scatter(model.support_vectors_[:, 0], 12 model.support_vectors_[:, 1], 13 s=300, lw=1, facecolors='none'); 14 axi.set_title('gamma = {0:.1f}'.format(gamma), size=14)
上述代码设置gamma值分别为10和0.1,以观察建模的结果。上面左图为gamma取10时的输出结果,训练后得到的模型非常复杂,虽然可以把所有的数据点都分类正确,但是一看其决策边界,就知道这样的模型过拟合风险非常大。右图为gamma取0.1时的输出结果,模型并不复杂,有些数据样本分类结果出现错误,但是整体决策边界比较平稳。那么,究竟哪个参数好呢?一般情况下,需要通过交叉验证进行对比分析,但是,在机器学习任务中,还是希望模型别太复杂,泛化能力强一些,才能更好地处理实际任务,因此,相对而言,右图中模型更有价值。
12.4.4SVM人脸识别实例
Sklearn工具包提供了丰富的实例,其中有一个比较有意思,就是人脸识别任务,但当拿到一张人脸图像后,看一下究竟是谁,属于图像分类任务。
第①步:数据读入。数据源可以使用sklearn库直接下载,它还提供很多实验用的数据集,感兴趣的读者可以参考一下其API文档,代码如下:
1 #读取数据集 2 from sklearn.datasets import fetch_lfw_people 3 faces = fetch_lfw_people(min_faces_per_person=60) 4 #看一下数据的规模 5 print(faces.target_names) 6 print(faces.images.shape)
#Downloading LFW metadata: https://ndownloader.figshare.com/files/5976012 #Downloading LFW metadata: https://ndownloader.figshare.com/files/5976009 #Downloading LFW metadata: https://ndownloader.figshare.com/files/5976006 #Downloading LFW data (~200MB): https://ndownloader.figshare.com/files/5976015
比较大,可行下载文件到C:Users用户名scikit_learn_datalfw_home----------邀月注
['Ariel Sharon' 'Colin Powell' 'Donald Rumsfeld' 'George W Bush' 'Gerhard Schroeder' 'Hugo Chavez' 'Junichiro Koizumi' 'Tony Blair'] (1348, 62, 47)
为了使得数据集中每一个人的样本都不至于太少,限制了每个人的样本数至少为60,因此得到1348张图像数据,每个图像的矩阵大小为[62,47]。
第②步:数据降维及划分。对于图像数据来说,它是由像素点组成的,如果直接使用原始图片数据,特征个数就显得太多,训练模型的时候非常耗时,先用PCA降维(后续章节会涉及PCA原理),然后再执行SVM,代码如下:
1 from sklearn.svm import SVC 2 from sklearn.decomposition import PCA 3 from sklearn.pipeline import make_pipeline 4 5 #降维到150维 6 pca = PCA(n_components=150, whiten=True, random_state=42) 7 svc = SVC(kernel='rbf', class_weight='balanced') 8 #先降维然后再SVM 9 model = make_pipeline(pca, svc)
数据降到150维就差不多了,先把基本模型实例化,接下来可以进行实际建模,因为还要进行模型评估,需要先对数据集进行划分:
1 from sklearn.model_selection import train_test_split 2 Xtrain, Xtest, ytrain, ytest = train_test_split(faces.data, faces.target, 3 random_state=40)
第③步:SVM模型训练。SVM中有两个非常重要的参数——C和gamma,这个任务比较简单,可以用网络搜索寻找比较合适的参数,这里只是举例,大家在实际应用的时候,应当更仔细地选择参数空间:
1 from sklearn.model_selection import GridSearchCV 2 param_grid = {'svc__C': [1, 5, 10], 3 'svc__gamma': [0.0001, 0.0005, 0.001]} 4 grid = GridSearchCV(model, param_grid) 5 6 %time grid.fit(Xtrain, ytrain) 7 print(grid.best_params_)
Wall time: 28.2 s
{'svc__C': 5, 'svc__gamma': 0.001}
第④步:结果预测。模型已经建立完成,来看看实际应用效果:
1 model = grid.best_estimator_ 2 yfit = model.predict(Xtest) 3 yfit.shape
(337,)
如果想得到各项具体评估指标,在sklearn工具包中有一个非常方便的函数,可以一次将它们全搞定:
1 from sklearn.metrics import classification_report
2 print(classification_report(ytest, yfit,
3 target_names=faces.target_names))
precision recall f1-score support Ariel Sharon 0.50 0.50 0.50 16 Colin Powell 0.70 0.81 0.75 54 Donald Rumsfeld 0.83 0.85 0.84 34 George W Bush 0.94 0.88 0.91 136 Gerhard Schroeder 0.70 0.85 0.77 27 Hugo Chavez 0.81 0.72 0.76 18 Junichiro Koizumi 0.87 0.87 0.87 15 Tony Blair 0.85 0.76 0.80 37 accuracy 0.82 337 macro avg 0.77 0.78 0.77 337 weighted avg 0.83 0.82 0.82 337
只需要把预测结果和标签值传进来即可,任务中每一个人就相当于一个类别,F1指标还没有用过,它是将精度和召回率综合在一起的结果,公式如下。
F1=2×精度召回率/(精度+召回率)
其中,精度(precision)=正确预测的个数(TP)/被预测正确的个数(TP+FP),召回率(recall)=正确预测的个数(TP)/预测个数(TP+FN)。
整体效果看起来还可以,如果想具体分析哪些人容易被错认成别的人,使用混淆矩阵观察会更方便,同样,sklearn工具包中已经提供好方法。
1 from sklearn.metrics import confusion_matrix
2 mat = confusion_matrix(ytest, yfit)
3 sns.heatmap(mat.T, square=True, annot=True, fmt='d', cbar=False,
4 xticklabels=faces.target_names,
5 yticklabels=faces.target_names)
6 plt.xlabel('true label')
7 plt.ylabel('predicted label');
对角线上的数值表示将一个人正确预测成他自己的样本个数。例如第一列是Ariel Sharon,测试集中包含他24个图像数据,其中17个正确分类,3个被分类成Colin Powell,1个被分类成Donald Rumsfeld,1个被分类成George W Bush,2个被分类成Gerhard Schroeder。通过混淆矩阵可以更清晰地观察哪些样本类别容易混淆在一起。
本章小结:
本章首先讲解了支持向量机算法的基本工作原理,大家在学习过程中,应首先明确目标函数及其作用,接下来就是优化求解的过程。支持向量机不仅可以进行线性决策,也可以借助核函数完成难度更大的数据集划分工作。接下来,通过实验案例对比分析了支持向量机中最常调节的松弛因子C和参数gamma,对于核函数的选择,最常用的就是高斯核函数,但一定要把过拟合问题考虑进来,即使训练集中做得再好,也可能没有实际的用武之地,所以参数选择还是比较重要的。
第12章完。
该书资源下载,请至异步社区:https://www.epubit.com