核函数
一、核心思想
在前面我们所讨论的分类器中,基本都是线性分类器,但是当数据集不存在一个线性的决策边界时,线性分类器便无法很好得进行分类。
事实证明,有一种优雅的方法可以将非线性问题合并到大多数的线性分类器可解决的问题中,即将线性分类器非线性化,这便是核函数。
我们可以看一个经典的例子:如下图所示的数据集,显然它是不存在线性的决策边界的,但是我们可以通过函数 ϕ 对特征向量进行特征变换使得数据线性可分。
我们不妨将特征变换函数定义为:
ϕ(x)=ϕ([x1,x2]T)=[x1,x2,∣x1⋅x2∣]T
我们所添加的维度捕捉了原始特征之间的非线性交互,使得数据变为了线性可分,升维的方法较为简便,并且使得问题保持凸且表现良好,但是缺点是升维可能会导致维度过高,使得模型变得复杂,比如下面这个例子所示:
通过特征变换,维度从 d 维变为了 2d 维,这种新的表示法 ϕ(x) 非常有表现力,允许复杂的非线性决策边界,但维数非常高。这使得我们的算法速度慢得令人无法忍受。
二、核技巧
核技巧是一种通过在更高维空间中学习函数来绕过这一困境的方法,而无需计算单个向量 ϕ(x) 或完整向量 w 。
2.1 梯度下降
我们考虑平方损失函数的梯度下降过程:
l(w)=i=1∑n(wTxi−yi)2
在梯度下降过程中,我们每次需要选择一个步长进行更新:
wt+1=wt−s⋅(∂w∂l(w))∂w∂l(w)=2i=1∑n(wTxi−yi)⋅xi
此处我们要引入一个重要的假设:我们可以将参数 w 表示为特征向量 xi 的线性组合:
w=i=1∑nαixi
根据该假设我们可以得到: wt+1 与 xi 线性相关, wt 与 xi 线性相关,由此:梯度 ∂w∂l(w) 与 xi 线性相关:
∂w∂l(w)=2i=1∑n(wTxi−yi)⋅xi=i=1∑nγixi
由于损失函数为凸函数,最终解与初始化无关我们可以将 w0 初始化为我们想要的任何值,我们不妨令:
w0=[0,0,...,0]T,α0=[0,0,...,0]T
根据上述分析我们可以得到梯度下降的过程为:
w1=w0−s⋅2i=1∑n(w0Txi−yi)xi=i=1∑nαi0xi−si=1∑nγi0xi=i=1∑nαi1xi α1=α0−sγ0w2=w1−s⋅2i=1∑n(w1Txi−yi)xi=i=1∑nαi1xi−si=1∑nγi1xi=i=1∑nαi2xi α2=α1−sγ1w3=w2−s⋅2i=1∑n(w2Txi−yi)xi=i=1∑nαi2xi−si=1∑nγi2xi=i=1∑nαi2xi α3=α2−sγ2...wt=wt−1−s⋅2i=1∑n(wt−1Txi−yi)xi=i=1∑nαit−1xi−si=1∑nγit−1xi=i=1∑nαitxi αt=αt−1−sγt−1
因为 αi0=0 ,则有:
αi1= 0 −sγi0=−sγi0αi2=αi1−sγ01=−sγi0−sγi1αi3=αi2−sγi2=−sγi0−sγi1−sγi2...αit=αit−1−sγit−1=−sr=1∑t−1γir
我们用 xi 的线性组合代替 w ,得到新的模型和损失函数:
h(xi)=wtTxi=j=1∑nαjtxjTxil(w)=i=1∑n(wtTxi−yi)2=i=1∑n(j=1∑nαjtxjTxi−yi)2
由此我们可以发现:为了学习具有平方损失的超平面分类器,我们需要的唯一信息是所有数据的特征向量对之间的内积。
2.2 计算内积
有了上述推导,我们将模型简化为只需要求解向量对之间的内积,我们回归到上述的升维操作中去:
在升维之后,内积的计算公式为:
ϕ(x)Tϕ(z)=1+x1z1+x2z2+...x1x2...xdz1z2...zd=k=1∏d(1+xkzk)
我们可以发现,尽管特征向量是 2d 维的,但是计算其内积仅需要 d 次乘法运算,这极大提高了算法的速度。
我们由此即可定义核函数:
k(xi,xj)=ϕ(xi)Tϕ(xj)
核函数计算出的结果存储在核矩阵中:
Kij=ϕ(xi)Tϕ(xj)
诸如 ϕ 之类的用于升维的映射并不好找,因此我们用核函数 k(xi,xj) 去代替这样的映射,处理线性不可分问题。
则上述模型可以表示为:可以发现模型中唯一的未知参数即为 α ,我们需要对它进行求解
h(xi)=wTxi=j=1∑nαjxjTxi=j=1∑nαjk(xj,xi)
同时我们也已经得到了:
αit=−sr=1∑t−1γir
所以当下我们的求解目标变为了 γ :
∂w∂l(w)=2i=1∑n(wTxi−yi)⋅xi=i=1∑nγixiγi=2(wTxi−yi)
在经过 ϕ 特征变换的新的高维空间中有:
γi=2(wTϕ(xi)−yi)=2(j=1∑nαjk(xj,xi)−yi)
则梯度下降的过程为:
αit+1=αit−sγit=αit−2s(j=1∑nαjtk(xj,xi)−yi)
梯度下降过程中,每次更新 α 的计算量为 O(n2) ,远好于 O(2d)
三、一般核函数
3.1 常用核函数
(1)线性核函数:
K(x,z)=xTz
(2)多项式核函数:
K(x,z)=(1+xTz)d
(3)高斯核函数(RBF):
K(x,z)=e−σ2∣∣x−z∣∣22
(4)指数核函数:
K(x,z)=e−2σ2∣∣x−z∣∣
(5)拉普拉斯核函数:
K(x,z)=e−σ∣x−z∣
(6)Sigmoid核函数: tanh(x)=ex+e−xex−e−x
K(x,z)=tanh(γxTz+r)
3.2 良定义核函数
良定义的核函数定义如下:通过递归组合以下一个或多个规则构建的核称为定义良好的核:
① k(x,z)=xTz② k(x,z)=ck1(x,z)③ k(x,z)=k1(x,z)+k2(x,z)④ k(x,z)=g(k(x,z))⑤ k(x,z)=k1(x,z)⋅k2(x,z)⑥ k(x,z)=f(x)k1(x,z)f(z)⑦ k(x,z)=ek1(x,z)⑧ k(x,z)=xTAz
上述规则中 k1(x,z) 和 k2(x,z) 都是良定义的核函数, c≥0 , g 是一个正系数多项式函数, f 是任何函数, A 是半正定的
某个核函数是良定义的等价为:
①核矩阵 K 的特征值都是非负的
②存在实矩阵 P 使得: K=PTP
③核矩阵 K 是半正定的,即对于任何向量 x ,都有: xTKx≥0
定理 3-1
RBF核函数:k(x,z)=e−σ2(x−z)2是良定义的
证明如下:
k(x,z)=e−σ2(x−z)2=e−σ21(xTx−2xTz+zTz)=e−σ2xTx⋅eσ22xTz⋅e−σ2xTx
根据规则⑥:f(x)=e−σ2xTx,k1(x,z)=eσ22xTz→k(x,z)=f(x)⋅k1(x,z)⋅f(z)根据规则⑦:k2(x,z)=σ22xTz→k1(x,z)=ek2(x,z)根据规则②:k3(x,z)=xTz,c=σ22xTz→k2(x,z)=c⋅k3(x,z)根据规则①:k3(x,z) is well defined→k2(x,z) is well defined→k1(x,z) is well defined
综上推导:
k(x,z) is well defined
定理 3-2
S1,S2∈Ω,k(S1,S2)=e∣S1∩S2∣是良定义的
证明如下:
将 Ω 中所有可能的元素排列成一个列表,S1,S2分别用一个大小为 ∣Ω∣ 的向量 xS1,xS2 表示,如果 Ω 中的第 i 个元素属于 S ,则 xiS=1 ,反之则 xiS=0 ,则上述核函数可以表示为:
k(S1,S2)=exS1TxS2
根据规则⑦和规则①,我们可以得到: k(S1,S2) 为良定义核函数。
四、模型核化
一个算法可以通过三步实现核化:
①证明解决方案位于训练点的范围内,即对于某些 αi :
w=i=1∑nαixi
②重构算法与分类器,使得输入的特征向量仅用于内积的计算
③将内积替换为核函数:
xiTxj→ϕ(xi)Tϕ(xj)
4.1 线性回归核化
我们回顾一下普通的最小二乘回归 OLS :
l(w)=i=1∑n(xiTw−yi)w^MLE=wargmin i=1∑n(xiTw−yi)h(x)=wTx
输入的训练集为 X 和 Y ,则其闭合形式为:
w=(XTX)−1XTY
我们对该模型进行核化: X 为 n×d 维矩阵, Y 为 n×1 维矩阵, w,xi 为 d×1 维矩阵, α 为 n×1 维矩阵
①将 w 表示为 xi 的线性组合: α=[ α1,α2,...,αn ]T
w=i=1∑nαixi=XTα
②将模型修正为输入特征向量的内积:
h(xi)=j=1∑nαjxjTxi=wTxi=αTXxi
③用核函数代替内积:
h(xi)=j=1∑nαjk(xj,xi)
求解 α 的闭合形式:
w=XTα=(XTX)−1XTY→XTXXTα=XTY→XXTα=YKij=k(xi,xj)→K=XXT→Kα=Yα=K−1Y
岭回归同理,我们也可以实现同样的核化并求解 α 的闭合形式:
模型为:
l(w)=i=1∑n(xiTw−yi)+λ∣∣w∣∣22w^MAP=wargmin i=1∑n(xiTw−yi)+λ∣∣w∣∣22h(x)=j=1∑nαjk(xj,xi)
α 的闭合形式为:
w=XTα=(XTX+λI)−1XTY→(XTX+λI)XTα=XTY→(XTXXT+λXT)α=XTY→(XXT+λI)α=Y→α=(K+λI)−1Y
4.2 KNN核化
我们以欧拉距离的 KNN 模型为例:
dist(xi,xj)=(xi−xj)T(xi−xj)=xiTxi−2xiTxj+xjTxj=k(xi,xi)−2k(xi,xj)+k(xj,xj)
但是上述核化的意义并不大,因此我们一般不对 KNN 算法进行核化。
4.3 支持向量机核化
线性支持向量机结合核函数是一个很强大的模型,我们往往求解其对偶问题,所以我们需要先了解对偶问题的定义。
4.3.1 拉格朗日乘子法与KKT条件
在求解最优化问题中,拉格朗日乘子法(Lagrange Multiplier)和KKT(Karush Kuhn Tucker)条件是两种最常用的方法。在有等式约束时使用拉格朗日乘子法,在有不等约束时使用KKT条件。
我们这里提到的最优化问题通常是指对于给定的某一函数,求其在指定作用域上的全局最小值,支持向量机模型即为该类最优化为问题。
在求解最优化问题时一般会遇到三种情况:
(1)无约束条件:
这是最简单的情况,解决方法通常是函数对变量求导,令求导函数等于0的点可能是极值点。将结果带回原函数进行验证即可。
(2)等式约束条件:
设目标函数为 f(x) ,约束条件为 hk(x) ,有 l 个约束条件,则等式约束条件的最优化问题可以表示为:
求解目标:minf(x)约束条件:hk(x)=0,k=1,2,...l
我们要用到拉格朗日乘子法处理该最优化问题:首先定义拉格朗日函数:
F(x.λ)=f(x)+k=1∑lλkhk(x)
然后解变量的偏导方程:
∂x1∂F(x,λ)=0,∂x2∂F(x,λ)=0,...,∂xd∂F(x,λ)=0∂λ1∂F(x,λ)=0,∂λ2∂F(x,λ)=0,...,∂λk∂F(x,λ)=0
(3)不等式约束条件:
设目标函数为 f(x) ,不等式约束条件为 gk(x) ,有 q 个不等式约束条件,等式约束条件为 hj(k) ,有 p 个等式约束条件:
求解目标:minf(x)约束条件:hj(x)=0,j=1,2,...,p gk(x)≤0,k=1,2,...,q
则我们可以定义不等式约束条件下的拉格朗日函数为:
L(x,λ,μ)=f(x)+j=1∑pλjhj(x)+k=1∑qμkgk(x)
常用的方法是 KKT 条件:即最优值(局部最小值)必须满足以下条件:
① ∂x∂L(x,λ,μ)∣x=x∗=0② hj(x∗)=0③ μkgk(x∗)=0
4.3.2 对偶问题的核化
我们首先回顾支持向量机的模型:为了便于计算我们引入一个常系数 21
求解目标:(w,b)=w,bargmin 21∣∣w∣∣22约束条件:∀i , yi(wTxi+b)≥1
则拉格朗日函数为:
L(w,b,α)=21∣∣w∣∣22+i=1∑nαi(1−yi(wTxi+b))
根据 KKT 条件可得:
∂w∂L(w,b,α)=w−i=1∑nαiyixi=0∂b∂L(w,b,α)=−i=1∑nαiyi=0
我们同时也知道,在 w,b 取得最优值时有: yi(wTxi+b)=1 满足了 KKT 条件
综上,我们可得:
w=i=1∑nαiyixii=1∑nαiyi=0
我们上述条件代入原式可得:
(w,b)=w,bargmin 21i=1∑nj=1∑nαiαjyiyjxiTxj+i=1∑nαi−i=1∑nj=1∑nαiyiyjxiTxj−b⋅i=1∑nαiyi→(w,b)=w,bargmin i=1∑nαi−21i=1∑nj=1∑nαiαjyiyjxiTxj
由此我们得到了支持向量机模型的对偶问题:
求解目标:α=αargmax i=1∑nαi−21i=1∑nj=1∑nαiαjyiyjxiTxj约束条件:i=1∑nαiyi=0
显然我们可以对该对偶问题进行核化:
α=αargmax i=1∑nαi−21i=1∑nj=1∑nαiαjyiyjk(xi,xj)
最终的模型为:
h(x)=sign(wTx+b)=sign(i=1∑nαiyik(xi,x)+b)
从支持向量的角度对对偶问题有一个很好的解释:对于原始公式,我们知道只有支持向量满足等式约束:
yi(wTϕ(xi)+b)=1
在对偶问题中我们可以使得支持向量所对应的 αi>0 ,而其他的输入向量对应的 αi=0 ,在测试时我们只需要计算支持向量上 h(x) 的和,并在训练后丢弃所有 αi=0 的特征向量。
对偶有一个明显的问题,就是 b 不再是优化的一部分了,但是我们需要它来进行分类,在对偶中支持向量是那些 αi>0 的向量,因此我们可以推导出 b :
yi(wTϕ(xi)+b)=1,yi∈{−1,+1}→b=yi−wTϕ(xi)→b=yi−j=1∑nαjyjk(xj,xi)
同时如果使用软间隔模型,则仅需添加一个新的约束:
0≤αi≤C
五、模型实现
我们本次要手动实现核化的软间隔支持向量机,主要运用其对偶问题求解最优化问题。
5.1 数据集
我们本次要生成线性不可分的数据集,因为这样才能体现出核函数的优势所在,生成线性不可分数据集的代码如下所示:我们主要用到的是sklearn所提供的make_moons生成双半月环数据集,这是一个经典的线性不可分数据集。
'''屏蔽warning'''
import warnings
warnings.filterwarnings("ignore")
'''导入重要库'''
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
'''生成数据'''
data,target=make_moons(n_samples=1000,noise=0.1)
'''可视化'''
ax=plt.figure(figsize=(7,4),dpi=100).add_subplot()#初始化画板
plt.scatter(data[:,0],data[:,1],c=target)
plt.show()
'''存储数据集'''
df=pd.DataFrame(data=data,columns=["feature1","feature2"])
df['label']=target
df.to_csv("data/moons_data.csv",index=False)
生成的数据集如下图所示:
5.2 手动实现模型
5.2.1 模型阐述
求解思路来自:支持向量机(SVM)——对偶问题
我们首先要了解对于对偶形式的支持向量机模型的求解方法,考虑到软约束,我们的模型为:
求解目标:α=αargmax i=1∑nαi−21i=1∑nj=1∑nαiαjyiyjxiTxj约束条件:i=1∑nαiyi=0,0≤αi≤C
我们最后求解出的模型为:
h(xi)=j=1∑nαjyjxjTxi+b
我们首先实现该模型的代码:
'''模型计算值h(xi)'''
def _h(self,i):
r=self.b
for j in range(self.n):
r+=self.alpha[j]*self.Y[j]*self.kernel(self.X[j],self.X[i])
return r
基于原始模型以及我们作出的假设, KKT 条件为:
⎩⎪⎨⎪⎧0≤αi≤Cyi⋅h(xi)−1≥0αi(yi⋅h(xi)−1)=0
因为我们有:支持向量所对应的 αi>0 ,而其他的输入向量对应的 αi=0 的设定,支持向量是那些满足yih(xi)=1的点,所以有:
αi(yi⋅h(xi)−1)=0
接着我们需要求解核心问题:
α=αargmax i=1∑nαi−21i=1∑nj=1∑nαiαjyiyjxiTxj
5.2.2 求解思路
这是一个二次规划问题,我们用到 SMO 算法对其进行求解,算法思路来自:SMO算法详解
我们不妨先将问题作一个变形:
α=αargmin 21i=1∑nj=1∑nαiαjyiyjxiTxj−i=1∑nαi
我们要寻找一个满足约束条件的 α=[ α1,α2,...,αn ] ,假设我们已经找到了这样的一个 α ,即最终的超平面已经确定:
h(xi)=wTxi+b
那么该超平面是满足 KKT 条件的,则有:
⎩⎪⎨⎪⎧yi⋅h(xi)≥1→ αi=0 ,xi在边界内,正确分类yi⋅h(xi)=1→0<αi<C,xi在边界上,是支持向量,正确分类yi⋅h(xi)≤1→ αi=C ,xi在两条边界之间
反过来想,我们求解出来的 αi 和 xi 也要满足上述关系。
综上我们的求解过程便是初始化一个 α 并不断得对其进行优化,直到找到最优的那个 α , SMO 算法每次选择两个 αi,αj 进行更新。
根据我们的约束,显然这两个 αi,αj 满足:
αiyi+αjyj=−k=1,k=i,j∑nαkyk=ξ
因此有:
αi=yiξ−αjyj=(ξ−αjyj)yi , yi∈{−1,+1}
因此我们只需要找到一个 αj 进行优化,并且求出优化后的值,那么我们的 αi 也完成了优化,为了便于表示,后面我们将挑选出的两个 αi,αj 记作 α1,α2
从效果上考虑我们应该优化那些最不满足目标条件的 αi ,而在我们优化过后它不满足目标条件的程度应该减小,而我们为了衡量一个 αi 满足目标条件的程度,引入一个指标:我们首先定义一个误差:
Ei=h(xi)−yi
我们发现 ∣E1−E2∣ 越大,优化后的 α1,α2 满足目标条件的程度就越大。
计算 Ei 的代码如下所示:
'''计算Ei'''
def _E(self,i):
return self._h(i)-self.Y[i]
接着我们思考一个问题:我们该怎样确定优化后的 α 是朝着不满足目标条件程度减小的方向移动的呢?
我们回归到原始的问题:
求解目标:α=αargmin 21i=1∑nj=1∑nαiαjyiyjxiTxj−i=1∑nαi约束条件:i=1∑nαiyi=0,0≤αi≤C
我们求解目标是求解目标函数的局部最小值,那么我们不妨将 α1,α2 看作变量,其他 αi 看作常量,则:
α=αargmin W(α1,α2)W(α1,α2)=Aα12+Bα22+Cα1α2+Dα1+Eα2+F
代入 α1=(ξ−α2y2)y1 可得:
W(α2)=A[(ξ−α2y2)y1]2+Bα22+C(ξ−α2y2)y1α2+D(ξ−α2y2)y1+Eα2+F =(A+B−Cy1y2)α22+(E−2Ay2−Dy1y2)α2+Aξ2+(C+D)ξ+F
那么求 W 的极小值,只需要简单得令 ∂α2∂W(α2)=0 即可,即:
2(A+B−Cy1y2)α2+(E−2Ay2−Dy1y2)=0
最终我们只需要保证我们优化后得 α 是使得目标函数变小的,就可以确定优化后的 α 是朝着不满足目标条件程度减小的方向移动。
5.2.3 SMO算法
α1,α2 的更新
为了处理线性不可分的数据,我们引入了核函数:
求解目标:α=αargmin 21i=1∑nj=1∑nαiαjyiyjk(xi,xj)−i=1∑nαi约束条件:i=1∑nαiyi=0,0≤αi≤C
核函数的实现代码如下所示,我们有多样的选择:
'''核函数'''
def kernel(self,xi,xj):
if self._kernel=="linear": #线性核函数
return np.dot(xi,xj)
if self._kernel=="poly": #多项式核函数
return (1+np.dot(xi,xj))**self.d
if self._kernel=="gauss": #高斯核函数
return np.exp(-((xi-xj)**2).sum()/self.sigma**2)
优化前后 α 都必须满足 ∑i=1nαiyi=0 ,即:
α1newy1+α2newy2=α1oldy1+α2oldy2=ξ
在更新 α1,α2 时,有:
α1=(ξ−α2y2)y10≤α1≤C,0≤α2≤C
将上述两个信息结合我们可以进一步得缩小 α2 的范围:我们将 α2 的上界与下界定义为 H,L
L≤α2≤H
将 α1y1+α2y2=ξ 看作一个方程,显然几何上这是一条直线,结合取值范围我们可以绘制出图像:
(1) y1=y2 时:有两种情况: α1−α2=ξ , α1−α2=−ξ
0≤α2≤C0≤α1=α2+ξ≤C→−ξ≤α2≤C−ξ0≤α1=α2−ξ≤C→ ξ≤α2≤C+ξ
① α1−α2=ξ 时:
L=max(0,−ξ)=max(0,α2−α1) H=min(C,C−ξ)=min(C,C+α2−α1)
② α1−α2=−ξ 时:
L=max(0,ξ)=max(0,α2−α1)H=min(C,C+ξ)=min(C,C+α2−α1)
可以发现两种情况所得的上下界求解公式相同。
(2) y1=y2 时,有两种情况: α1+α2=ξ , α1+α2=−ξ ,推导过程与 y1=y2 时的完全相同:
0≤α2≤C0≤α1= ξ−α2≤C → ξ−C≤α2≤ξ0≤α1=−ξ−α2≤C→−ξ−C≤α2≤−ξ
① α1+α2=ξ 时:
L=max(0,ξ−C)=max(0,α1+α2−C)H=min(C,ξ)=min(C,α1+α2)
② α1+α2=−ξ 时:
L=max(0,−ξ−C)=max(0,α1+α2−C)H=min(C,−ξ)=min(C,α1+α2)
两种情况所得的上下界求解公式也完全相同。
综上,上下界的计算通式如下:
{L=max(0,α2−α1) ,H=min(C,C+α2−α1) if y1=y2L=max(0,α1+α2−C),H=min(C,α1+α2) if y1=y2
接着我们要对α1,α2进行更新:
α=αargmin 21i=1∑nj=1∑nαiαjyiyjk(xi,xj)−i=1∑nαi=αargmin W(α1,α2)
将 α1,α2 视为变量,其余 αi 视为常量,则有:核矩阵 Kij=k(xi,xj)
W(α1,α2)=21K11α12+21K22α22+K12y1y2α1α2+y1α1i=3∑nαiyiKi1+y2α2i=3∑nαiyiKi2−α1−α2−i=3∑nαi
我们定义: vj=∑i=3nαiyiKij , Constant=∑i=3nαi ,则有:
W(α1,α2)=21K11α12+21K22α22+K12y1y2α1α2+y1α1v1+y2α2v2−α1−α2−Constant
代入 α1=(ξ−y2α2)y1 可得:
W(α2)=21K11(ξ−y2α2)2+21K22α22+K12y2(ξ−y2α2)α2+v1(ξ−y2α2)+y2v2α2−(ξ−y2α2)y1−α2−Constant =21(K11+K22−2K12)α22+(K12ξy2−K11ξy2−v1y2+v2y2+y2y1−1)α2+21K11ξ2+v1ξ−ξy1−Constant
对 W 进行求导可得:
∂α2∂W(α2)=(K11+K22−2K12)α2+(K12ξy2−K11ξy2−v1y2+v2y2+y2y1−1)
我们令导数为0即可求得我们所期望更新的 α2 :
(K11+K22−2K12)α2=(y2−y1+K11ξ−K12ξ+v1−v2)y2
我们可以不计算 ξ ,通过 α2old 更新 α2new :
vj=i=3∑nαiyiKij=h(xj)−i=1∑2αiyik(xi,xj)−bv1=h(x1)−α1y1K11−α2y2K21−bv2=h(x2)−α1y1K12−α2y2K22−b
由此可得:
v1−v2=h(x1)−h(x2)−(y1K11−y1K12)α1−(y2K21−y2K22)α2
代入 α1=(ξ−y2α2)y1 可得:
v1−v2=h(x1)−h(x2)−(K11−K12)(ξ−y2α2)−(y2K21−y2K22)α2 =h(x1)−h(x2)+(K11+K22−2K12)y2α2+(K12−K11)ξ
将 v1−v2 代入导数为0的式子可得:
(K11+K22−2K12)α2new=([h(x1)−y1]−[h(x2)−y2])y2+(K11+K22−2K12)α2old
根据我们之前设置的误差:
Ei=h(xi)−yi
代入可得:
α2new=α2old+K11+K22−2K12(E1−E2)y2
综上我们终于得到了 α2 更新的递归式,但是不要忘记了约束条件,于是我们将该 α2 记作未经修剪的(unclipped): α2new,unc
前面我们已经求出了 α2 上下界应该满足的条件,即:
{L=max(0,α2−α1) ,H=min(C,C+α2−α1) if y1=y2L=max(0,α1+α2−C),H=min(C,α1+α2) if y1=y2
所以可以得到修剪后的 α2 :
α2new=⎩⎪⎨⎪⎧ H , α2new,unc >H α2new,unc , L≤α2new,unc ≤H L , α2new,unc <L
选取修剪后的 α2 的代码如下所示:
'''修剪alpha'''
def _compare(self,_alpha,L,H):
'''
L<=alpha<=H时,返回alpha,否则返回H或L
'''
if _alpha > H:
return H
elif _alpha < L:
return L
else:
return _alpha
我们还知道:
α1newy1+α2newy2=α1oldy1+α2oldy2
由此我们也可以得到 α1 的更新公式:
α1new=α1old+y1y2(α2old−α2new)
b 的更新
我们每更新一次 α 后,都要对 b 进行一次更新,因为这关系到 h(x) 的计算,进而关系到 Ei 的计算。
①当 0<α1new<C 时, x1 为支持向量,则有:
y1(wTx1+b1)=1b1new=y1−i=1∑nαiyiKi1=y1−v1−α1newy1K11−α2newy2K12v1=h(x1)−α1oldy1K11−α2oldy2K21−bold
则我们可以得到:
b1new=bold−E1+(α1old−α1new)y1K11+(α2old−α2new)y2K21
②当 0<α2new<C 时, x2 为支持向量,同理有:
b2new=bold−E2+(α1old−α1new)y1K12+(α2old−α2new)y2K22
③当 α1,α2 都不满足上述情况时,有:
bnew=2b1new+b2new
综上我们可以得到更新 b 的通式:
bnew=⎩⎪⎪⎪⎨⎪⎪⎪⎧ b1new ,0<α1new<C b2new , 0<α2new<C2b1new+b2new,otherwise
α1,α2 的选取
有了上述更新的方法,我们只剩下一个问题,就是如何选择 α1,α2 ,选择方法如下:
①选取 α1 :遍历所有 αi ,把第一个不满足 KKT 条件的作为 α1
②选取 α2 :在所有不违反 KKT 条件的 αi 中选取 ∣E1−E2∣ 最大的作为 α2
根据上述规则我们可以得到选取 α1,α2 的代码:
'''选取α1,α2'''
def _init_alpha(self):
'''
选取一对alpha
'''
index_list=[i for i in range(self.n) if 0<self.alpha[i]<self.C] #支持向量的alpha索引列表
non_satisfy_list = [i for i in range(self.n) if i not in index_list] #其他的alpha索引列表
index_list.extend(non_satisfy_list) #优先考虑不满足kkt条件的支持向量对应的alpha
for i in index_list: #遍历所有的alpha
if self._KKT(i): #满足KKT条件,跳过
continue
E1=self._E(i)
#找到使得|E1-E2|最大的alpha
if E1>=0: #估计值大于实际值
j = min(range(self.n), key=lambda x: self.E[x])
else: #估计值小于实际值
j = max(range(self.n), key=lambda x: self.E[x])
return i,j
我们知道 KKT 条件为:
⎩⎪⎨⎪⎧yi⋅h(xi)≥1→ αi=0 ,xi在边界内yi⋅h(xi)=1→0<αi<C,xi在边界上,是支持向量yi⋅h(xi)≤1→ αi=C ,xi在两条边界之间
那么违反 KKT 条件的情况为:
① yih(xi)>1 但 αi>0② yih(xi)=1 但 αi=0或C③ yih(xi)<1 但 αi<C
为了便于判断我们可以进一步得进行处理:
yih(xi)−1=yiEi
那么违反 KKT 条件的情况为:
① yiEi<0 但 αi<C② yiEi>0 但 αi>0
但在实际中 KKT 条件是极其苛刻的,我们通过引入容错率 tol 来缓解, tol 一般取值为 0.0001
① yiEi<tol 但 αi<C② yiEi>tol 但 αi>0
由此我们可以定义 KKT 条件的相关代码:
'''kkt条件'''
def _KKT(self,i):
'''
:param i: alpha下标
:return: 满足KKT条件返回True,否则返回False
'''
y_Ei=self.Y[i]*self._E(i)
if y_Ei<self.tol and self.alpha[i]<self.C:
return False
elif y_Ei>self.tol and self.alpha[i]>0:
return False
else:
return True
综上,我们可以实现 SMO 算法+核函数的高效的支持向量机模型:
'''屏蔽warning'''
import warnings
warnings.filterwarnings("ignore")
'''导入重要的库'''
import numpy as np
import pandas as pd
import random
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
'''处理二分类问题的支持向量机'''
class SVM(object):
'''初始化'''
def __init__(self,max_iter=100,kernel='linear',C=100,sigma=1,tol=0.0001):
self.max_iter=max_iter #最大训练轮数
self._kernel=kernel #核函数的选择:'linear'线性核函数,'poly'多项式核函数,'gauss'高斯核函数
self.C=C #惩罚系数
self.sigma=sigma #高斯核函数参数sigma
self.tol=tol #KKT条件容错率
'''初始化参数'''
def _init_args(self,features,labels):
self.n,self.d=features.shape #训练集大小n,特征向量维度d
self.X=features #训练集的特征向量
self.Y=labels #训练集的标签
self.b=0.0 #模型的偏差项
self.alpha=np.zeros(self.n) #模型参数α
'''E存储对于每个结点的预测误差'''
self.E=[self._E(i) for i in range(self.n)] #预先计算并存储可以极大提高算法速度
'''kkt条件'''
def _KKT(self,i):
'''
:param i: alpha下标
:return: 满足KKT条件返回True,否则返回False
'''
y_Ei=self.Y[i]*self._E(i)
if y_Ei<self.tol and self.alpha[i]<self.C:
return False
elif y_Ei>self.tol and self.alpha[i]>0:
return False
else:
return True
'''模型计算值h(xi)'''
def _h(self,i):
r=self.b
for j in range(self.n):
r+=self.alpha[j]*self.Y[j]*self.kernel(self.X[j],self.X[i])
return r
'''核函数'''
def kernel(self,xi,xj):
if self._kernel=="linear": #线性核函数
return np.dot(xi,xj)
if self._kernel=="poly": #多项式核函数
return (1+np.dot(xi,xj))**self.d
if self._kernel=="gauss": #高斯核函数
return np.exp(-((xi-xj)**2).sum()/self.sigma**2)
'''计算Ei'''
def _E(self,i):
return self._h(i)-self.Y[i]
'''选取α1,α2'''
def _init_alpha(self):
'''
选取一对alpha
'''
index_list=[i for i in range(self.n) if 0<self.alpha[i]<self.C] #支持向量的alpha索引列表
non_satisfy_list = [i for i in range(self.n) if i not in index_list] #其他的alpha索引列表
index_list.extend(non_satisfy_list) #优先考虑不满足kkt条件的支持向量对应的alpha
for i in index_list: #遍历所有的alpha
if self._KKT(i): #满足KKT条件,跳过
continue
E1=self._E(i)
#找到使得|E1-E2|最大的alpha
# if E1>=0: #估计值大于实际值
# j = min(range(self.n), key=lambda x: self.E[x])
# else: #估计值小于实际值
# j = max(range(self.n), key=lambda x: self.E[x])
j=max(range(self.n),key=lambda x:abs(E1-self.E[x]))
return i,j
'''修剪alpha'''
def _compare(self,_alpha,L,H):
'''
L<=alpha<=H时,返回alpha,否则返回H或L
'''
if _alpha > H:
return H
elif _alpha < L:
return L
else:
return _alpha
'''训练模型'''
def fit(self,X,Y):
self._init_args(X,Y) #初始化参数
for t in range(self.max_iter): #进行最多max_iter轮训练
print("====第{}轮====".format(t+1))
i1,i2=self._init_alpha() #选择一对alpha
'''计算alpha[i2]的上下界'''
if self.Y[i1]==self.Y[i2]:
L=max(0,self.alpha[i1]+self.alpha[i2]-self.C)
H=min(self.C,self.alpha[i1]+self.alpha[i2])
else:
L = max(0, self.alpha[i2]-self.alpha[i1])
H = min(self.C, self.C+self.alpha[i2]-self.alpha[i1])
E1 = self.E[i1]
E2 = self.E[i2]
'''计算更新alpha[i2]用到的底数K11+K22-2*K12'''
eta = self.kernel(self.X[i1], self.X[i1]) + self.kernel(self.X[i2], self.X[i2]) \
- 2*self.kernel(self.X[i1], self.X[i2])
if eta==0: #不更新,进行下一轮训练
print("eta=0")
continue
'''计算alpha新值'''
alpha2_new_unc = self.alpha[i2] + self.Y[i2] * (E1 - E2) / eta #未修剪的alpha2
alpha2_new = self._compare(alpha2_new_unc, L, H) #计算修剪后的alpha2
alpha1_new = self.alpha[i1] + self.Y[i1] * self.Y[i2] * (self.alpha[i2] - alpha2_new)
'''计算b新值'''
b1_new = self.b-E1 - self.Y[i1] * self.kernel(self.X[i1], self.X[i1]) * (alpha1_new-self.alpha[i1]) \
- self.Y[i2] * self.kernel(self.X[i2], self.X[i1]) * (alpha2_new-self.alpha[i2])
b2_new = self.b-E2 - self.Y[i1] * self.kernel(self.X[i1], self.X[i2]) * (alpha1_new-self.alpha[i1]) \
- self.Y[i2] * self.kernel(self.X[i2], self.X[i2]) * (alpha2_new-self.alpha[i2])
if 0 < alpha1_new < self.C:
b_new = b1_new
elif 0 < alpha2_new < self.C:
b_new = b2_new
else:
b_new = (b1_new + b2_new) / 2
'''更新值'''
self.alpha[i1] = alpha1_new
self.alpha[i2] = alpha2_new
self.b = b_new
print("L=",L,"H=",H)
print("alpha2_new_unc=",alpha2_new_unc)
print("alpha[{}]_new=".format(i1),alpha1_new)
print("alpha[{}]_new=".format(i2),alpha2_new)
print("b_new=",b_new)
self.E[i1] = self._E(i1)
self.E[i2] = self._E(i2)
'''进行预测'''
def predict(self, X_test):
y_pred=[]
for x_t in X_test:
r = self.b
for i in range(self.n):
r += self.alpha[i] * self.Y[i] * self.kernel(x_t, self.X[i])
y_pred.append(1 if r > 0 else -1)
return np.array(y_pred)
'''计算权重w'''
def _weight(self):
yx = self.Y.reshape(-1, 1)*self.X
self.w = np.dot(yx.T, self.alpha)
return self.w
'''求支持向量'''
def support_vector(self):
vector=[]
for i in range(self.n):
if (self.alpha[i]>0):
vector.append(self.X[i])
return np.array(vector)
'''决策函数值'''
def decision_function(self,x):
r=self.b
for j in range(self.n):
r+=self.alpha[j]*self.Y[j]*self.kernel(self.X[j],x)
return r
'''读取数据'''
df=pd.read_csv("data/moons_data.csv")
target=np.array(list(df['label']))
target=np.where(target==0,-1,1) #修正标签{0,1}->{-1,+1}
df.drop("label",axis=1,inplace=True)
data=df.values
'''数据集分割'''
X_train,X_test,y_train,y_test=train_test_split(data,target,test_size=0.2,random_state=10)#选取20%的数据作为测试集
'''初始化模型'''
params={
"max_iter":100,
"kernel":"gauss",
"C":100,
"sigma":0.39,
"tol":0.0001
}
svm=SVM(**params) #高斯核的支持向量机
'''模型训练与预测'''
svm.fit(X_train,y_train)
y_pred=svm.predict(X_test)
print(accuracy_score(y_test,y_pred))
为了避免随机性影响模型评估,我们使用固定的数据集,并且固定得划分训练集与测试集,用到的测试集如下图所示:
我们可以发现对于上述线性不可分的数据,我们使用高斯核,通过调整参数达到了0.99的正确率,效果很好。
5.3 可视化
我们可以通过如下代码实现对决策边界的可视化:
def draw_hyperplane(svm,data,target):
'''
:param svm: 支持向量机模型
:param data: 用到的特征向量数据集
:param target: 响应的标签数据集
:return: 绘制出的决策边界
'''
plt.figure(figsize=(7,4),dpi=100).add_subplot() #初始化画板
plt.scatter(data[:,0],data[:,1],c=target) #绘制数据分布散点
'''获取坐标轴范围'''
ax = plt.gca() # 获取坐标轴
xlim = ax.get_xlim() # 获得Axes的 x坐标范围
ylim = ax.get_ylim() # 获得Axes的 y坐标范围
'''创建等差数列'''
xx = np.linspace(xlim[0], xlim[1], 40)
yy = np.linspace(ylim[0], ylim[1], 40)
'''生成网格点坐标矩阵'''
YY, XX = np.meshgrid(yy, xx)
xy = np.vstack([XX.ravel(), YY.ravel()]).T #xy中存储了整个坐标系中的网格点坐标
Z=np.array([svm.decision_function(x_t) for x_t in xy]).reshape(XX.shape)
'''绘制决策边界和分隔'''
ax.contour(XX, YY, Z, colors='k', levels=[-1, 0, 1], alpha=0.5,linestyles=['--', '-', '--'])
'''填充等高线不同区域的颜色'''
plt.contourf(XX,YY,Z,10,cmap="Blues_r",alpha=0.75)
'''添加颜色条说明'''
plt.colorbar(shrink=0.8)
plt.show()
我们可以观察不同的核函数得到的决策边界:
①高斯核函数:
②线性核函数:
③多项式核函数:
我们可以发现对于线性不可分数据高斯核函数的效果非常好。
5.4 调库实现模型
当然sklearn也提供了可以利用核函数的支持向量机,主要用到的函数为SVC,其函数原型为:
SVC(*, C=1.0, kernel='rbf', degree=3, gamma='scale', 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', break_ties=False, random_state=None)
其中一些重要的参数为:
①C
:软间隔的惩罚系数
②kernel
:用到的核函数:线性核:”linear“,多项式核:”poly“,高斯核:”rbf“,sigmoid核:”sigmoid“,预计算:”precomputed“
③degree
:多项式核的指数
④gamma
:”poly“,”rbf“,”sigmoid“核的系数选择方式
● if gamma='scale'
(default) is passed then it uses 1 / (n_features * X.var()) as value of gamma,
● if ‘auto’, uses 1 / n_features.
更多参数作用请详见官方说明文档:sklearn.svm.SVC
'''屏蔽warning'''
import warnings
warnings.filterwarnings("ignore")
'''导入重要的库'''
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
def draw_hyperplane(svm,data,target):
'''
:param svm: 支持向量机模型
:param data: 用到的特征向量数据集
:param target: 响应的标签数据集
:return: 绘制出的决策边界
'''
plt.figure(figsize=(7,4),dpi=100).add_subplot() #初始化画板
plt.scatter(data[:,0],data[:,1],c=target) #绘制数据分布散点
'''获取坐标轴范围'''
ax = plt.gca() # 获取坐标轴
xlim = ax.get_xlim() # 获得Axes的 x坐标范围
ylim = ax.get_ylim() # 获得Axes的 y坐标范围
'''创建等差数列'''
xx = np.linspace(xlim[0], xlim[1], 40)
yy = np.linspace(ylim[0], ylim[1], 40)
'''生成网格点坐标矩阵'''
YY, XX = np.meshgrid(yy, xx)
xy = np.vstack([XX.ravel(), YY.ravel()]).T #xy中存储了整个坐标系中的网格点坐标
Z=svm.decision_function(xy).reshape(XX.shape)
'''绘制决策边界和分隔'''
ax.contour(XX, YY, Z, colors='k', levels=[-1, 0, 1], alpha=0.5,linestyles=['--', '-', '--'])
'''填充等高线不同区域的颜色'''
plt.contourf(XX,YY,Z,10,cmap="Blues_r",alpha=0.75)
'''添加颜色条说明'''
plt.colorbar(shrink=0.8)
plt.show()
'''读取数据'''
df=pd.read_csv("data/moons_data.csv")
target=np.array(list(df['label']))
target=np.where(target==0,-1,1) #修正标签{0,1}->{-1,+1}
df.drop("label",axis=1,inplace=True)
data=df.values
'''数据集分割'''
X_train,X_test,y_train,y_test=train_test_split(data,target,test_size=0.2,random_state=10)#选取20%的数据作为测试集
'''模型参数'''
params={
"C":1.0,
"kernel":'rbf',
"degree":3,
"gamma":'scale'
}
'''初始化模型'''
svm=SVC(**params)
svm.fit(X_train,y_train)
y_pred=svm.predict(X_test)
print(accuracy_score(y_test,y_pred))
'''可视化'''
draw_hyperplane(svm,data,target)
注意可视化处作了部分调整:
Z=svm.decision_function(xy).reshape(XX.shape)
最终我们得到了0.995的准确率,决策边界如下图所示:
对比之下不难发现我们的模型虽然正确率也很高,但是存在一定程度的过拟合,调库所得的决策边界更好一些。