找回密码
 立即注册
查看: 578|回复: 0

SVM算法详解

[复制链接]
发表于 2021-12-2 08:37 | 显示全部楼层 |阅读模式
线性模型

线性可分训练集

一个训练数据集线性可分是指:,使对,有

  • ,则
  • ,则
即(公式1)
对于线性可分的数据集,我们需要划一条线来分割两类不同的样本。但是分割的线有无数条,我们怎么判断哪一条线更好呢?




很多人认为第二条线是最好的。但是因为根据免费午餐定理,这三条曲线是一样好的。那么我们为什么会认为第二条曲线是最好的呢?这是因为我们在研究问题之前,对此问题存在先验假设。有很多先验假设认为第二条直线比其余两条要好。我们只考虑其中一种假设,即假设训练样本的位置在特征空间有测量误差。如下图所示:




假设红色的叉和圆圈的真实位置为红色虚线圆圈,则线3和1都会分类错误,而2不会,这说明2号线更能抵御训练样本位置的误差。
那么2号线是怎么画出来的呢?
支持向量机的创造者Vapnik是这样回答的,它首先将直线向一侧平行移动,直到它叉到一个或几个样本为止;之后再向另一侧移动,直到叉到一个或多个样本未知。




我们要找的2号线找的是使得间隔最大的且位于间隔中间的线。
在多维的情况下,直线变为超平面
之后我们将支持向量机转化为一个优化问题,优化问题为:


那么这是怎么得到的呢?那面我们详细讨论一下:
事实一:是同一个平面,。即若满足公式1,则也满足公式一。
公式1:
事实二:点到平面的距离公式。
向量到超平面的距离:


当为支持向量时,我们要做的就是最大化。
根据事实1,我们可以用去缩放:


最终使在支持向量上上,有:


此时支持向量与平面距离:


因为最大化相当于最小化,所以得到上述的目标函数。
下面看约束条件是如何得到的。
因为在上面的描述中我们有,对于所有的支持向量,我们有


所以对于非支持向量,我们有:


又因为:,所以综上我们有


这样我们就得到了上面提到的优化问题。
这个优化问题为凸优化问题中的二次优化问题
二次规划问题:

  • 目标函数是二次项
  • 限制条件是一次项
这样就会导致要么无解,要么只有一个极值。
非线性可分

我们改写目标函数和约束条件,使其变为:


其中称为松弛变量,称为正则项。
非线性问题

对于下图所示的问题,我们不能找到一个很好的直线将两类分开:




但是我们可以将其映射到高维空间中的点,然后在高维空间中寻找直线。
我们定义一个从低维到高维的映射:


其中为低维向量,而为一个高维映射。
下面我们举一个例子:如下图所示的异或问题




这个问题我们在二维空间里无法找到一条直线将其分开。
在上图中我们令四个点分别为:




我们令


则经过映射得到:




我们可以令


来达到区分的目的。
有证明显示:在越高维度情况下,找打一个线性超平面来将样本分开的概率越大。我们如何选取,我们将选择为无限维。但是为无限维,将为无限维,优化问题将不可做。
我们可以不知道无限维映射的显式表达,我们只要知道一个核函数:


下面的优化问题:


仍然可解。
在SVM中常用的核函数:


为高斯核函数


为多项式核函数,为阶数。
核函数必须满足某些条件才能被拆成内积的形式:
能被写成的充要条件为:


  • ,有$$\sum_{i=1}^N\sum_{i=1}^Nc_ic_jK(x_i,x_j)\ge 0$$
原问题和对偶问题

原问题

最小化:
限制条件:



对偶问题

定义:


对偶问题的定义
最大化:表示下界
限制条件:
原问题和对偶问题解的关系

定理:如果是原问题的解,而是对偶问题的解,则有:


证明:


定义:



叫做原问题与对偶问题的间距。对于某些特定优化问题,可以证明:
强对偶定理:若为凸函数,且,则此优化问题的原问题与对偶问题的间距为。即


此时我们易得对

  • 或者
  • 或者
这被称为KKT条件
利用对偶问题求解SVM

我们先复习一下原问题:


根据原问题的定义形式,我们将上述问题改写:


凸函数定义:


我们SVM的对偶问题为:
最大化:


限制条件:



我们要想求得,首先要最优化,对函数求偏导:


将其代入,得到:


其中
所以对偶优化问题变为:
最大化:


限制条件:



这也是一个凸优化问题,解这个问题有一个标准的算法(SMO)算法。
这样我们就可以求出,但是我们要求的是和,那么我们应该如何求出呢,我们可以用之前得到的,但问题是我们并不知道
但是在判断样本属于哪一类的时候我们并不需要知道,假设有测试样本,我们知道:

  • ,则
  • ,则



但是应该怎么算呢?
应用KKT条件,我们有

  • 要么,要么
  • 要么,要么
我们取一个
此时,因为。也可以找到所有不等于的,求得取平均。
SMO算法

最大化:


限制条件:



因为我们要优化的变量()很多,所以我们每次迭代只选择几个变量进行更新;又因为我们的是有约束的优化问题,所以每次更新可能会破坏我们的约束条件,为了不破坏我们的约束条件,我们每次至少选择两个变量进行优化。因此我们每次选择两个变量来进行优化。
两个变量二次规划的求解过程


  • 选择两个变量,其他变量固定
  • SMO将对偶问题转化成一系列子问题



  • 根据约束条件,可以表示为的函数
  • 优化问题有解析解
  • 基于初始可行解,可以得到
两个变量,约束条件用二维空间中的图形表示:




下面首先考虑第一种情况,根据不等式条件的取值范围:


其中


同理对于第二种情况,根据不等式条件的取值范围:


其中


下面开始求解,求得的过程为:

  • 先求沿着约束方向未经剪辑时的
  • 再求剪辑后的
我们记
,即我们的判别表达式,令:


为输入的预测值和真实输出的差。
为了简便,引进记号:


目标函数写成:


,我们得,代入上式得到只是的函数的目标函数:


对求导并令其等于,得:


代入:


代入:


我们之后对解进行剪辑:


得到的解:


关于KKT条件,我们有:


下面我们计算阈值和
由KKT条件,如果,则









的表达式与相结合,得:


同理,如果,则


如果同时满足条件,那么。如果是或者,那么以及它们之间的数都是符合KKT条件的阈值,这时选择它们的中点作为。
在每次完成两个变量的优化之后,还必须更新对应的值,并将它们保存在列表中。值的更新要用到值,以及所有支持向量对应的


其中,是所有支持向量的集合。
关于此方面的解释
变量的启发式选择

SMO算法在每个子问题中选择两个变量优化,其中至少一个变量是违反KKT条件的。

  • 第一个变量的选择:外循环

    • 违反KKT条件最严重的样本点
    • 检验样本点是否满足KKT条件:
    • \end{array}$$

该检验是在范围内进行的。在检验过程中,外层循环首先遍历满足条件的样本点,即在间隔边界上的支持向量点,检验它们是否满足KKT条件,如果这些样本点都满足KKT条件,那么遍历整个训练集,检验他们是否满足KKT条件。

  • 第二个变量的检查:内循环

    • 选择的标准是希望能使目标函数有足够大的变化

      • 即对应最大




    • 如果内循环通过上述方法找到的点不能使目标函数有足够大的下降,则:遍历间隔边界上的样本点,测试目标函数下降

      • 如果下降不大,则遍历所有样本点
      • 如果依然下降不大,则丢弃外循环点,重新选择




算法实现
def kernal(a, b):
    # 高斯核, s为方差
    return a.dot(b)
class SVM:
    def __init__(self,data,label):
        self.data = data
        self.label = label
        self.len = data.shape[0]
        self.E = np.zeros((self.len,1))
        self.alpha = np.random.random((self.len,1))
        self.b = 0
    def f(self,i):
        # 传入索引
        s = 0
        for k in range(self.len):
            s += self.alpha[k]*self.label[k]*kernal(self.data[i,:],self.data[k,:])
        return s + self.b
    def error(self):
        # 计算E
        for j in range(self.len):
            E = 0
            for i in range(self.len):
                E += self.alpha*self.label*kernal(self.data[i,:],self.data[j,:])
            self.E[j] = E + self.b - self.label[j]
        
    def bound(self, i,j,alpha_i, alpha_j, C):
        # 传入两个索引,为需要优化的alpha的索引
        # 求解alpha_j的范围
        # C为正则化项系数
        if self.label != self.label[j]:
            L, H = np.max([0, alpha_j-alpha_i]), np.min([C, C+alpha_j-alpha_i])
        else:
            L, H = np.max([0, alpha_j+alpha_i-C]), np.min([C, alpha_j+alpha_i])
        return (L,H)
   
    def update(self, i,j, C):
        # 更新alpha和b
        # 传入索引
        eta = kernal(self.data[i,:],self.data[i,:]) + kernal(self.data[j,:],self.data[j,:]) - 2*kernal(self.data[i,:],self.data[j,:])
        
        # 下面需要补充逻辑关系
        alpha_old_i = self.alpha
        alpha_old_j = self.alpha[j]
        self.alpha[j] = self.alpha[j] + self.label[j]*(self.E-self.E[j])/eta
        L, H = self.bound(i,j,alpha_old_i,alpha_old_j,C)
        if self.alpha[j] >= H:
            self.alpha[j] = H
        elif self.alpha[j] <= L:
            self.alpha[j] = L
        else:
            self.alpha[j] = self.alpha[j]
        self.alpha = self.alpha + self.label*self.label[j]*(alpha_old_j-self.alpha[j])
        
        b1 = self.b - self.E - self.label*(self.alpha-alpha_old_i)*kernal(self.data[i,:],self.data[i,:])-self.label[j]*(self.alpha[j]-alpha_old_j)*kernal(self.data[j,:],self.data[i,:])
        b2 = self.b - self.E[j] - self.label*(self.alpha-alpha_old_i)*kernal(self.data[i,:],self.data[j,:])-self.label[j]*(self.alpha[j]-alpha_old_j)*kernal(self.data[j,:],self.data[j,:])
        
        self.b = (b1+b2)/2
        self.error()
    def smo(self, epsilon, max_iter, C):
        for k in range(max_iter):
            I = np.intersect1d(np.argwhere(self.alpha>0),np.argwhere(self.alpha<C))
            d = []
            for i in I:
                d.append(np.abs(self.label*self.f(i)-1))
            i = np.argmax(np.array(d))
            j = np.argmax(self.E-self.E)
            self.update(i,j,C)
        
            ge = np.array([i  for i in range(self.len) if self.alpha>=1])
            eq = np.array([i  for i in range(self.len) if self.alpha==1])
            le = np.array([i  for i in range(self.len) if self.alpha<=1])
            Ge = []
            Eq = []
            Le = []
            for ig in ge:
                Ge.appned(self.label[ig]*self.f(ig))
            for ie in eq:
                Eq.append(self.label[ie]*self.f(ie))
            for il in le:
                Le.append(self.label[il]*self.f(il))
            
            Ge = np.array(Ge)
            Eq = np.array(Eq)
            Le = np.array(Le)
            
            ne = np.sum(np.abs(Eq-1)>epsilon)
            ng1 = np.sum(np.abs(Ge-1)<0)
            ng2 = np.sum(np.abs(Ge-1)>epsilon)
            nl1 = np.sum(np.abs(Le-1)>0)
            nl2 = np.sum(np.abs(Le-1)>epsilon)
            if (ne+ng1+ng2+nl1+nl2)==0:
                break
    def predict(self, x):
        s = 0
        for k in range(self.len):
            s += self.alpha[k]*self.label[k]*kernal(self.data[k,:],x)
        s = s + self.b
        if s >=0:
            return 1
        else:
            return -1
本文使用 Zhihu On VSCode 创作并发布

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-23 03:25 , Processed in 0.114023 second(s), 33 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表