【机器学习】2. 支持向量机

2. 支持向量机

对偶优化

拉格朗日乘数法可用于解决带条件优化问题,其基本形式为:

(1)minwf(w),(2)s.t.{gi(w)0,hi(w)=0.

该问题的拉格朗日函数为

L(w,α,β)=f(w)+iαigi(w)+jβjhj(w),

定义

(3)P(w)=maxα0,βL(w,α,β),(4)D(α,β)=minwL(w,α,β),

容易看出,若w打破了原问题的某个限制条件,则P(w)=+,否则P(w)=f(w),因此原始问题可以表示为minwP(w);对应地,对偶问题可以表示为maxα0,βD(α,β),也就是

(5)Primal: minwmaxα0,βL(w,α,β);(6)Dual: maxα0,βminwL(w,α,β).

这就是原始问题对应的对偶问题,从上面可以得到原始问题与对偶问题最优解之间的关系:

d=maxα0,βminwL(w,α,β)minwmaxα0,βL(w,α,β)=p.

一般不能肯定原始问题与对偶问题具有相同的解,然而已有证明表明在一定条件下原始问题与对偶问题具有同一组解(w,α,β),使得p=d=L(w,α,β),并且这组解满足KKT条件:

wL(w,α,β)=0;βjL(w,α,β)=0,j;αigi(w)=0,i;gi(w)0,i;αi0,i.

其中,第三条αigi(w)=0被称为互补松弛性.

支持向量机的优化目标

支持向量机的训练集D={(xi,yi)}中,yi{1,1},正例标签为1,负例标签为1. 在几何直观上,支持向量机试图找到一个超平面wx+b=0,使得所有样本在正确分类的前提下,所有点到超平面的直线距离的最小值最大.

  • 由于yi{1,1},故样本被正确分类意味着yi(wxi+b)>0.
  • 对任意点xi,点到超平面wx+b=0的距离为d=|wxi+b|w.
  • 支持向量机的目的是让所有样本被正确分类的同时,最大化所有点到超平面的最小距离.

综合以上三点,支持向量机意图找到一个γ>0作为所有点到超平面的最小距离,并最大化这个λ,因此支持向量机的初始优化问题是

(7)maxγ>0,(8)s.t.yi(wxi+b)wγ.

此时约束条件是非凸的,令γ:=γw,上述问题就转化成

(9)maxγw,(10)s.t.yi(wxi+b)γ.

此时目标函数是非凸的,然而(w,b)作为超平面的参数可以进行伸缩变换,且问题的解(w,b,γ)在目标函数和约束条件上均是齐次的,这就使得任意一组解(w,b,γ)都可以等效为(wγ,bγ,1),即设定γ1不会影响解的存在,因此可以在问题的形式中隐去参数γ,最后将目标函数等效变换,就得到SVM原始问题:

(11)max1wmin12w2,(12)s.t.yi(wxi+b)1,i{1,,n}.

现在求其对偶问题. 此问题的拉格朗日函数是

L(w,b,α)=12w2+i=1nαi(1yi(wxi+b)).

对偶问题即maxα0minw,bL(w,b,α),要计算minw,bL(w,b,α)需对w,b求偏导,并令偏导数等于0,即

Lw=wi=1nαiyixi=0w=i=1nαiyixi;Lb=i=1nαiyi=0i=1nαiyi=0.

w=i=1nαiyixi代回L(w,b,α),再结合i=1nαiyi=0以及αi0的条件,就得到对偶问题为

(13)maxf(α)=i=1nαi12i=1nj=1nαiαjyiyj(xixj)(14)s.t.{αi0,i0,i=1nαiyi=0.

线性不可分情形

线性不可分情形下,原始问题不可解,即不可能满足对所有样本都有yi(wxi+b)1,因此软间隔SVM对每个样本考虑一个容错ξi0,允许样本在一定程度上违背分类原则,但相应地在目标函数中施加惩罚,优化问题就产生如下变化:

minw,b12w2,s.t.yi(wxi+b)1.minw,b,ξ12w2+Ci=1nξi,s.t.{yi(wxi+b)1ξi,ξi0.

这里引入了超参数C表示对分类错误的容忍程度,C代表惩罚力度,C=+时软间隔SVM就退化为硬间隔SVM. 同样求其对偶问题,拉格朗日函数是

L(w,b,ξ,α,r)=12w2+Ci=1nξii=1nαi(ξi+yi(wxi+b)1)i=1nriξi,

对偶问题是maxα0,r0minw,b,ξL(w,b,ξ,α,r)要求minw,b,ξ(α,r),就对w,b,ξ求偏导并令之等于0,得到

Lw=wi=1nαiyixi=0w=i=1nαiyixi,Lb=i=1nαiyi=0i=1nαiyi=0,Lξi=Cαiri=0αi+ri=C,i,

w=i=1nαiyixi代入到L(w,b,ξ,α,r)中,并结合i=1nαiyi=0αi+ri=C的约束条件,就得到对偶问题

(15)max(α,r)0i=1nαi12i=1nj=1nαiαjyiyjxixj,(16)s.t.{i=1nαiyi=0,αi0,ri0,αi+ri=C{i=1nαiyi=0,0αiC.

可以看到,软间隔SVM相比硬间隔SVM,它们的对偶问题优化目标一致,唯一区别在于为每一个αi添加了上界C,因此仅在求解时有些许区别.

原始问题与对偶问题的关联

现在根据原始问题推导得到对偶问题,并且假定已经了解对偶问题的解法. 然而最终回到问题本身,还是要找到对应的分组超平面(w,b):wx+b=0,因此需要了解原始问题与对偶问题的关联.

首先,讨论的前提是原始问题与对偶问题同解,对支持向量机而言,已有证明表明这一条件是成立的,即原始问题与对偶问题共享拉格朗日函数的一组解,记作(w,b,α[,ξ,r])(中括号内是软间隔SVM独有的变量),且KKT条件也成立,这意味着互补松弛性αigi(w)=0对任何样本i都成立.

在上述前提下,考虑下面的几个问题:

  1. 互补松弛性αigi(w)=0意味着什么?
  2. 求得对偶问题的解α后如何回推分类超平面(w,b)
  3. 如何利用优化后的支持向量机(w,b,α)判别样本点?

首先,第一个问题中,互补松弛性成立表明对每个样本i,都有αigi(w)=0,回到问题定义上,αi0是对偶问题中约束条件gi(w)0yi(wix+b)1ξi的对应系数,既然αi0gi(w)0,那么要使αigi(w)=0,就必须满足αi=0gi(w)=0. 而α是通过对偶问题求解直接获得的,并且与每个样本对应,因此对每个样本,必定满足下面两个条件中的至少一个(硬间隔SVM中不含ξi):

  • αi=0,此时yi(wxi+b)>1ξi,这样的样本称为非支持向量,它经过ξi调节后落在分隔超平面的间隔外.
  • αi>0,此时yi(wxi+b)=1ξi,这样的样本称为支持向量,它经过ξi调节后落在分隔超平面的间隔上.

实际数据集中,非支持向量的数量总是远多于支持向量,即大多数样本总是落在分隔超平面以外,支持向量的数量相对较少,但只有它们对分类器产生实质影响,因此分类器才被称为支持向量机. 为什么说只有支持向量会实质上地影响分类器,要看第二个问题与第三个问题,下面用SV表示支持向量(support vector)所构成的集合.

第二个问题,即如何根据α反推(w,b),这由优化过程中的偏导条件可得. 注意到令Lw=0时得到了w=i=1nαiyixi,这正是最优解所满足的条件,即

w=i=1nαiyixi,

进一步地,因为当且仅当样本为支持向量时αi>0,即对大多数样本有αi=0,这部分样本对w没有贡献,也就是

w=iSVαiyixi.

接下来考虑b,同样根据互补松弛性,当且仅当样本为支持向量时成立yi(wxi+b)=1ξi,因此对每个支持向量,如果能求得ξi的值就能得到b的值. 注意到当αi<Cri>0,从而ξi=0,因此只要能够找到一个αi(0,C),就能通过计算得到b=yi(w)xi(注意到1yi=yi). 往往可以对所有这样的样本做一个平均以平滑,即

b=1|{i:0<αi<C}|i:0<αi<C(yijSVαjyjxjxi),

特别对硬间隔支持向量机,此时在正负样本中都必定存在一个支持向量,落在超平面间隔上,从而

(17)mini:yi=1wxi+b=1,(18)maxi:yi=1wxi+b=1,(19)b=mini:yi=1(wxi)+maxi:yi=1(wxi)2.

最后一个问题即如何判别样本,这个问题相对简单,因为训练集中正样本总满足(w)xi+b1,负样本总满足(w)xi+b1ξi是针对特殊情况的补偿,因此在模型使用时不需考虑,未知样本x0的分类就是

y^0=sign((w)x0+b)=sign(iSVαiyixix0+b).

可以看到,求解完毕的支持向量机中,无论是参数表示还是模型使用,均只与支持向量有关.

核技巧

线性不可分时,除了能使用软间隔SVM以外,还可以用特征映射φ()将样本(xi,yi)映射到高维(φ(xi),yi),因为高维空间中的数据更系数,线性可分的可能性也更大. 然而,φ()往往是形式未知、维度极高甚至是无限维的,在这种情形下,核技巧(kernel trick)提供了一种绕开φ()的计算,不显式地计算转化后的数据集φ(xi),也能在转化后的高维空间中使用支持向量机的方法.

先回顾支持向量机应用于非特征映射数据集时的步骤:

  1. 构建对偶问题求解,在i=1nαiyi=0αi[0,C]的情况下求解maxα(iαi12i,jαiαjyiyj(xixj)).1.
  2. 得到最优解α后,找到一个αk(0,C),反推b=ykiαiyi(xixk).
  3. 得到分割超平面iαiyi(xix)+b=0,判别新样本y0=sign(iαiyi(xix0)+b).

注意到上面的过程中,有意地隐去了w的计算,因为不论在模型表示还是样本归类中,都不需要显式地得到w的具体值;并且,任何出现了样本特征x的地方,总是以内积xixj的形式出现. 这说明,即使使用了特征映射φ(),也不必显式地得到每个样本x对应的高维特征φ(x),但是对任意样本对(xi,xj),它们的内积φ(xi)φ(xj)却是必要的.

因此,核技巧不显式地求解φ(),而是用一个对称二元函数κ:(Rd,Rd)R定义了内积:κ(xi,xj)=φ(xi)φ(xj):=xi,xj,这样,将核技巧运用于支持向量机就可以将上面的步骤直接改写为:

  1. 构建对偶问题求解,在i=1nαiyi=0αi[0,C]的情况下求解maxα(iαi12i,jαiαjyiyjxi,xj).
  2. 得到最优解α后,找到一个αk(0,C),反推b=ykiαiyixi,xk.
  3. 得到分割超平面iαiyixi,x+b=0,判别新样本y0=sign(iαiyixi,x0+b).

综上所述,核技巧就是在不使用特征映射φ()的显式表达的同时,利用内积完成了特征映射所需要达到的目标,使得用户不需要考虑如何变换样本的特征,只需尝试不同的核函数即可. 常用的核函数有:

  • 线性核:κ(xi,xj)=xixj.
  • 多项式核:κ(xi,xj)=(xixj)d.
  • 高斯核:κ(xi,xj)=exp(xixj22σ2).
  • 拉普拉斯核:κ(xi,xj)=exp(xixjσ).
  • Sigmoid核:κ(xi,xj)=tanh(βxixj+θ).

甚至核函数的形式也不是必要的,只要有办法得到每一个样本对的核函数值即可,因此在训练过程中,传入一个核矩阵KRn×n代表样本对的核函数值,并在使用过程中传入样本x0的同时,传入一个向量k0Rn代表x0与所有训练样本的核函数值,就可以等效地作为核函数使用.

不过,由于核函数本身是特征映射内积的替代,所以核函数本身需要满足一定的条件. Mercer表明任何半正定函数都可以作为核函数使用,半正定函数指的是对任意数据集(x1,,xn),核函数κ诱导的矩阵KRn×n:Kij=κ(xi,xj)都是对称半正定的. 即使传入核矩阵K代替核函数,那么K本身也要是对称半正定矩阵,并且加入训练样本后,它与其他样本构成的核向量也要在增广意义下是对称半正定的.

SMO算法

最后进入到求解对偶问题的算法,常使用SMO(序列最小优化)算法对SVM的对偶问题进行求解,其思想是在对偶问题的n个变量中每次只选择两个进行优化,通过约束i=1nαiyi=0,固定n2个变量使得每次优化只有一个αi可以自由变化,因此可以通过求梯度的方式直接优化.

对软间隔SVM,令Kij=xi,xjxi,xj的核函数值,目标是最大化

maxαi=1nαi12i,jαiαjyiyjKij,

现选择变量对(α1,α2)优化并将其他变量用无关值C代替,那么约束变为α1y1+α2y2=ζ,目标就变成

maxα1,α2W(α1,α2)=α1+α212(α12K11+α22K22+2α1α2y1y2K12)(α1y1j=3nαjyjK1j+α2y2j=3nαjyjK2j)+C.

再将约束条件:α2=ζy2α1y1y2代入,并令j=3nαjyjKij=vi(i=1,2),就得到关于α1的一元二次优化问题

W(α1)=12(α12K11+(ζy2α1y1y2)2K22+2α1(ζy2α1y1y2)y1y2K12)+α1+ζy2α1(y1y2)α1y1v1(ζy2α1y1y2)y2v2+C=12(α12K11+α12K22+ζ2K222α1ζy1K22+2ζy1α1K122α12K12)+α1+ζy2α1(y1y2)α1y1v1ζv2+α1y1v2+C=(K1212K1112K22)α12+(ζy1K22ζy1K12+1y1y2y1v1+y1v2)α1+C+ζy2ζv212ζ2K22.

W(α1)α1=0就得到α1应满足的条件为

α1(K11+K122K12)=y1(ζK12ζK22+y1y2v1+v2),

在求得ζv1v2的同时,已经可以计算得到α1α2的值. 注意,在软间隔SVM中还需要满足α1[0,C]α2[0,C],并结合约束α1y1+α2y2=ζ. 这里分两种情况:

  1. y1y2时,约束变为α1α2=k,其中k的正负性未知,因此max(0,k)α1min(C,C+k).
  2. y1=y2时,约束变为α1+α2=k,其中kC的大小关系未知,因此max(0,kC)α1min(C,k).
  3. k的求值,注意到参数更新前后始终有α1y1+α2y2=ζ(y1,y2)不变,因此可以直接用更新前的参数计算k.
  4. 得到的上下界同时适用于α1α2,若求得的α1,α2超出此边界,则进行矩形修剪.

上述形式还可以继续化简,注意对αnewαold作区分. SVM对样本x的预测为f(x)=i=1nαioldyixi,x+b,因此

v1=f(x1)α1oldy1K11α2oldy2K12b,v2=f(x2)α1oldy1K12α2oldy2K22b,v1v2=f(x1)f(x2)α1oldy1(K11K12)α2oldy2(K12K22),

再将α2=ζy2α1y1y2代入(此式子对αnewαold均成立),就得到

v1v2=f(x1)f(x2)α1oldy1(K11K12)(ζα1oldy1)(K12K22)=f(x1)f(x2)α1oldy1(K11+K222K12)ζ(K12K22)

将其带回W(α1)α1得到

α1new(K11+K122K12)=y1(y1y2(f(x1)f(x2)α1oldy1(K11+K122K12)))=y1(y1f(x1)(y2f(x2)))+α1old(K11+K122K12),

Ei=f(xi)yi为旧模型在第i个样本上的预测误差,就有

α1new=α1oldy1(E1E2)K11+K122K12,

上式略过了一些繁琐参数(如Cζ)的计算,形式美观,但应用上依赖于旧模型f(),这意味着每次更新参数后需要得到新的模型,即需要对b进行更新. 注意到,至少有一个αi不在边界[0,C]上时xi是支持向量,此时有

yij=1nαjnewyjxj,xi+bnew=1bnew=1yij=1nαjnewyixj,xi.

若两个αi经更新后均不在边界上,则通过两个xi计算得到的bnew相等;若两个αi经更新后均在边界上,则取用这两个样本计算得到的b的中点作为bnew.

综上,给出SMO算法更新软间隔SVM的含代数步骤:

  1. 随机选择一对样本对应的变量作为更新变量(αi,αj).
  2. Ek=f(xk)yk为旧SVM的预测误差,计算无修剪的更新值αinew=αioldyi(EiEj)Kii+Kjj2Kij.
  3. αinew的修剪上下限:若yiyj则约束为αiαj=k,因此L=max(0,αioldαjold)H=min(C,C+αioldαjold);若yi=yj则约束为αi+αj=k,因此L=max(0,αiold+αjoldC)H=min(C,αiold+αjold).
  4. 修剪解:将αinew修剪至[L,H]之间,并根据αioldyi+αjoldyj=αinewyi+αjnewyj得到αjnew.
  5. 更新b:若αinewαjnew中至少有一个是支持向量(设为i),则更新bnew=1yijαjnewyjKji;若二者均不是支持向量,则依然用此法计算两个b,取它们的平均作为bnew.
  6. 循环上述步骤,取不同的更新变量对进行更新.

SVM分类的建议代码实现

下面给出了使用SMO算法训练SVM分类器的具体实现,这里使用了随机抽选的方式选择每次更新的变量对(αi,αj),并且使用内积核函数,指定C=+代表一个硬间隔分类器. 模型对一个人造数据集合真实数据集breast_cancer都有较为不错的分辨能力,需注意要对模型标签进行{1,1}化,同时这里还对特征进行归一化以防止数值溢出.

import numpy as np
from sklearn.datasets import load_breast_cancer

class SVM:
    def __init__(self, X, y, kernel=None):
        self.X = np.array(X)
        self.y = np.array(y)  # {-1, 1}
        if not kernel:
            kernel = np.dot
        self.kernel = kernel
        self.n_samples, self.dim = self.X.shape
        self.alpha = np.zeros(self.n_samples)
        self.b = 0
    
    def fit(self, max_iter=10000, C=float('inf')):
        for k in range(max_iter):
            i, j = np.random.choice(self.n_samples, 2, replace=False)
            Ei = self.predict(self.X[i]) - self.y[i]
            Ej = self.predict(self.X[j]) - self.y[j]
            eta = self.kernel(self.X[i], self.X[i]) + self.kernel(self.X[j], self.X[j]) - 2 * self.kernel(self.X[i], self.X[j])
            alpha_i_unclipped = self.alpha[i] - self.y[i] * (Ei-Ej) / eta
            alpha_i_clipped, alpha_j_clipped = 0, 0
            if self.y[i] == self.y[j]:
                K_constant = self.alpha[i] + self.alpha[j]
                lower_bound = max(0, K_constant - C)
                upper_bound = min(C, K_constant)
                alpha_i_clipped = self.__clip(alpha_i_unclipped, lower_bound, upper_bound)
                alpha_j_clipped = K_constant - alpha_i_clipped
            else:
                K_constant = self.alpha[i] - self.alpha[j]
                lower_bound = max(0, K_constant)
                upper_bound = min(C, C + K_constant)
                alpha_i_clipped = self.__clip(alpha_i_unclipped, lower_bound, upper_bound)
                alpha_j_clipped = alpha_i_clipped - K_constant
            self.alpha[i], self.alpha[j] = alpha_i_clipped, alpha_j_clipped
            if self.alpha[i] > 0 and self.alpha[i] < C:
                self.b = self.__calculate_b(i)
            elif self.alpha[j] > 0 and self.alpha[j] < C:
                self.b = self.__calculate_b(j)
            else:
                self.b = (self.__calculate_b(i) + self.__calculate_b(j)) / 2.0
    
    def __clip(self, alpha, L, H):
        if alpha < L:
            return L
        elif alpha > H:
            return H
        else:
            return alpha
    
    def __calculate_b(self, ind):
        return 1 - self.y[ind] * np.dot(self.y * self.alpha, self.kernel(self.X, self.X[ind]))
    
    def predict(self, x):
        x = np.array(x)
        if len(x.shape) == 1:
            return self.b + np.sum(self.alpha * self.y * self.kernel(self.X, x))
        elif len(x.shape) == 2:
            return self.b + np.dot(self.alpha * self.y, self.kernel(self.X, x.T))
    
    def predict_label(self, x):
        y_pred = self.predict(x)
        return np.sign(y_pred)
    
def acc(pred, label):
    return (pred == label).mean()

np.random.seed(0)
d = 5; n = 200
X_syn = [np.random.multivariate_normal(mean=np.random.normal(size=d), cov=np.identity(d) / 5, size=(100,)) for i in range(2)]
X_syn = np.vstack(X_syn)
y_syn = np.repeat([-1, 1], 100)
clf = SVM(X_syn, y_syn)
clf.fit()
print(f'acc = {acc(clf.predict_label(X_syn), y_syn)}')

df = load_breast_cancer()
X_real, y_real = df['data'], df['target']
X_real = (X_real - X_real.mean(0)) / X_real.std(0)
y_real = y_real * 2 - 1
clf_real = SVM(X_real, y_real, kernel=np.dot)
clf_real.fit()
print(f'acc of breast cancer = {acc(clf_real.predict_label(X_real), y_real)}')
posted @   江景景景页  阅读(286)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 提示词工程——AI应用必不可少的技术
· 地球OL攻略 —— 某应届生求职总结
· 字符编码:从基础到乱码解决
· SpringCloud带你走进微服务的世界
点击右上角即可分享
微信分享提示
主题色彩