机器学习推导+python实现(八):线性可分支持向量机

写在开头:今天提前开始一下线性可分支持向量机的内容,因为最近在准备找实习,所以先来温习一下支持向量机方面的,后面再支持向量机完了后,可能会优先更新XGboost的内容,然后中间缺少的章节会在后面进行补充。本节代码的实现部分参考机器学习实验室

内容安排

线性回归(一)、逻辑回归(二)、K近邻(三)、决策树值ID3(四)、CART(五)、感知机(六)、神经网络(七)、线性可分支持向量机(八)、线性支持向量机(九)、线性不可分支持向量机(十)、朴素贝叶斯(十一)、Lasso回归(十二)、Ridge岭回归(十三)等。
其实很大一部分都是在做回归,比如逻辑回归试图通过一个多元回归的线性关系来拟合出我们的类别,K近邻试图通过样本点周围最近的k个点的类别来判断自身类别,决策树可以通过特征分类规则类模型进行判断等等还有很多其他的,在这里的支持向量机则基于支持向量来进行分类,其实分类的本质就是在于找出不同类别独有的特点,然后进行归类(鄙人粗鄙的见解)。支持向量机有线性可分支持向量机、线性支持向量机和非线性支持向量机。今天主要介绍的就是线性可分支持向量机,那一起开始吧。

1.线性可分支持向量机的数学推导

我们这里的讲解以理解为主,可能存在细节的错误,详细的内容可以查看参考文献《统计学习方法(第二版)》。在这里我们调整一下内容的顺序和精细程度,并在分享过程中引入问题来加以说明我对于此部分的理解。好了让我们开始吧。
Q1:线性可分支持向量机能得到什么?
类似于逻辑回归可以得到一个函数来拟合特征,K近邻只是用于搜索没有函数式。那么对于线性可分支持向量机来说,其学习目标就是在空间中找到一个分离超平面,能将实例分到不同的类。线性可分支持向量机能通过给定的线性可分数据,通过间隔最大化或等价地求解相应的凸二次规划问题学习得到分离超平面
w x + b = 0 w^*\cdot x+b^*=0 以及类似于sigmoid的分类函数,这里采用的是符号函数sign,
f ( x ) = s i g n ( w x + b ) f(x)=sign(w^*\cdot x+b^*) 作为最后类别的输出,然后我们将超平面法向量方向的样本设置为正例,反之为负例(这里都是使用的是向量)
Q2:什么是间隔最大化?
这是求解线性可分的第一个关键知识,下面简单介绍一下间隔,间隔又分为函数间隔和几何间隔,选择几何间隔最大的分离超平面为我们最终的分离超平面。那么如何得到几何间隔呢?几何间隔是改进的函数间隔,那么先看一下函数间隔。
γ ^ = min i = 1 , , N γ i ^ = min i = 1 , , N y i ( w x i + b ) \hat{\gamma}={\underset {i=1,\dots,N}{\operatorname {min} }}\hat{\gamma_i}={\underset {i=1,\dots,N}{\operatorname {min} }}y_i(w\cdot x_i+b) 函数间隔的理解,可以看作样本点多于超平面的距离,然后乘上类别 y i y_i ,这样就分类错误的点得到的间隔是负数,分类正确的点得到的间隔是正数。所以函数间隔应该是正得越大越好,确信度很高,两个类别离得够远。然后从为了衡量某个分离超平面的确信度就选择他最小的那个函数间隔,有点像木桶原理,选择最短的来衡量能力。
那么几何间隔又是个啥呢?几何间隔是对函数间隔的约束,因为函数间隔存在着 w , b w,b 同比例变化会导致几何间隔 γ ^ \hat\gamma 成倍增加但集合超平面 w x + b = 0 w\cdot x+b=0 不变。因此几何间隔的的公式为:
γ = min i = 1 , , N γ i = min i = 1 , , N y i ( w w x i + b w ) = γ ^ w \gamma={\underset {i=1,\dots,N}{\operatorname {min} }}\gamma_i={\underset {i=1,\dots,N}{\operatorname {min} }}y_i(\frac{w}{||w||}\cdot x_i+\frac{b}{||w||})=\frac{\hat{\gamma}}{||w||}
那么定义每个分离超平面的最小几何间隔即为该超平面的间隔。然后之前我们说过,间隔越大说明分类的确信度越高,两类数据相隔就越远,所以来找到划分训练数据集正确的最大间隔分离超平面,表示最棒的超平面。
于是我们得到基于间隔最大化,求解分离超平面的目标函数和约束条件。
max w , b γ s . t .      y i ( w w x i + b w ) γ , i = 1 , 2 , , N \begin{aligned} {\underset {w,b}{\operatorname {max} }}&\quad\gamma \\ s.t.&\ \ \ \ y_i(\frac{w}{||w||}\cdot x_i+\frac{b}{||w||})\geqslant\gamma,i=1,2,\dots,N \end{aligned} 然后利用 γ = γ ^ w \gamma=\frac{\hat{\gamma}}{||w||} γ \gamma 换做 γ i ^ \hat {\gamma_i}
max w , b γ ^ w s . t .      y i ( w x i + b ) γ ^ , i = 1 , 2 , , N \begin{aligned} {\underset {w,b}{\operatorname {max} }}&\quad\frac{\hat{\gamma}}{||w||} \\ s.t.&\ \ \ \ y_i(w\cdot x_i+b)\geqslant\hat \gamma,i=1,2,\dots,N \end{aligned} 又因为在前文中说过当 w , b w,b 成比例变化时,函数间隔也会变化都相同的倍数,但变化了的间隔并不会影响目标函数和约束条件,而且分离超平面不会改变。而且对 1 w \frac{1}{||w||} 求最大值等价于对 1 2 w 2 \frac{1}{2}||w||^2 求最小值,于是就可以得到最大间法的目标函数和约束条件,这就是一个凸二次规划问题
min w , b 1 2 w 2 s . t .      y i ( w x i + b ) 1 0 , i = 1 , 2 , , N \begin{aligned} {\underset {w,b}{\operatorname {min} }} &\quad\frac{1}{2}||w||^2 \\ s.t.&\ \ \ \ y_i(w\cdot x_i+b)-1\geqslant 0,i=1,2,\dots,N \end{aligned} Q3:支持向量是啥呢?
既然是支持向量机,前面将间隔最大法寻思也没说支持向量的事,那支持向量是啥呢?

在线性可分情况下, 训练数据集的样本中与分离超平面距离最近的样本点的实例称为支持向量----《统计学习方法(第二版)》

其实从前面的内容可以理解为,定义超平面间隔的那个点。放在约束条件里就是,使得 y i ( w x i + b ) 1 = 0 y_i(w\cdot x_i+b)-1=0 成立的点。在超平面正负空间都可能存在支持向量。图片来源于《统计学习方法(第二版)p119》
为什么叫支持向量呢?直观理解就是衡量该超平面效果的点仅与支持向量点有关,也就是仅和超平面最近的实例点有关,与其他点无关。所以可以理解为支持向量在支持。
Q4:什么是对偶算法?
在解决凸优化问题的时候,往往可以通过求解对偶问题来解决原始问题的最优解,这就是线性可分支持向量机的对偶算法。一般对偶算法更容易求解,并且能够为后文推广非线性分类引入核函数。对于对偶问题的讨论,我们仅简单在文中穿插介绍概念。
首先就需要构造一个拉格朗日函数,
L ( w , b , a ) = 1 2 w 2 i = 1 N a i y i ( w x i + b ) + i = 1 N a i L(w,b,a)=\frac{1}{2}||w||^2-\sum\limits_{i=1}^{N}a_iy_i(w\cdot x_i+b)+\sum\limits_{i=1}^{N}a_i 原始问题也就是我们的最开始的问题是需要先求拉格朗日函数最大化,再求最小化,所以对于原始问题为,
min w , b max a L ( w , b , a ) {\underset {w,b}{\operatorname {min} }}{\underset {a}{\operatorname {max} }}L(w,b,a) 所以原始问题的对偶问题就为,
max a min w , b L ( w , b , a ) {\underset {a}{\operatorname {max} }}{\underset {w,b}{\operatorname {min} }}L(w,b,a) 求解上述函数就需要先对内部求最小,于是令拉格朗日对于 w , b w,b 的偏导数等于0,
w L ( w , b , a ) = w i = 1 N a i y i x i = 0 \nabla_wL(w,b,a)=w-\sum\limits_{i=1}^{N}a_iy_ix_i=0 b L ( w , b , a ) = i = 1 N a i y i = 0 \nabla_bL(w,b,a)=-\sum\limits_{i=1}^{N}a_iy_i=0 所以得到,
w = i = 1 N a i y i x i w=\sum\limits_{i=1}^{N}a_iy_ix_i i = 1 N a i y i = 0 \sum\limits_{i=1}^{N}a_iy_i=0 然后把上面两个式子代入 L ( w , b , a ) L(w,b,a) 中得到,
L ( w , b , a ) = 1 2 i = 1 N j = 1 N a i a j y i y j ( x i x j ) j = 1 N a i y i ( ( i = 1 N a i y i ) x i + b ) + j = 1 N a i = 1 2 i = 1 N j = 1 N a i a j y i y j ( x i x j ) + j = 1 N a i \begin{aligned} L(w,b,a)&=\frac{1}{2}\sum\limits_{i=1}^{N}\sum\limits_{j=1}^{N}a_ia_jy_iy_j(x_i\cdot x_j)-\sum\limits_{j=1}^{N}a_iy_i((\sum\limits_{i=1}^{N}a_iy_i)\cdot x_i+b)+\sum\limits_{j=1}^{N}a_i \\ &=-\frac{1}{2}\sum\limits_{i=1}^{N}\sum\limits_{j=1}^{N}a_ia_jy_iy_j(x_i\cdot x_j)+\sum\limits_{j=1}^{N}a_i \end{aligned} 然后就需要对这个式子求外面的最大值,
max a   1 2 i = 1 N j = 1 N a i a j y i y j ( x i x j ) + i = 1 N a i s . t .   i = 1 N a i y i = 0   a i 0 ,      i = 1 , 2 , , N \begin{aligned} {\underset {a}{\operatorname {max} }}&\ -\frac{1}{2}\sum\limits_{i=1}^{N}\sum\limits_{j=1}^{N}a_ia_jy_iy_j(x_i\cdot x_j)+\sum\limits_{i=1}^{N}a_i \\ s.t.&\ \sum\limits_{i=1}^{N}a_i y_i=0\\ &\ a_i\geqslant0,\ \ \ \ i=1,2,\dots,N\\ \end{aligned} 根据某个定理可查阅《统计学习方法(第二版)》附录C.2定理,认为可以有 a a^* 来求得 w , b w^*,b^* ,于是根据在由根据KKT条件成立可得,
w = i = 1 N a i y i x i w^*=\sum\limits_{i=1}^{N}a_i^*y_ix_i b = y j i = 1 N a i y i ( x i x j ) b^*=y_j-\sum\limits_{i=1}^{N}a_i^*y_i(x_i\cdot x_j) 且在对偶问题中的支持向量为,对应于 a i > 0 a^*_i>0 的样本点。其实带到KKT互补条件可知,在对偶算法中的支持向量与间隔最大的支持向量相同,即样本点一定在间隔边界上,也就是 y i ( w x i + b ) 1 = 0 y_i(w\cdot x_i+b)-1=0 上的点。这样我们就得到了线性可分支持向量机的整个过程和两个算法。但一般使用对偶形式进行。

2.线性可分支持向量机的python实现

首先就是加载数据,我们这里的python实现并不是一个具有普适性的代码,利用一种简单的搜索方法搜索到可能的最优值。但此处我们的目的是为了体验线性可分支持向量机的一个逻辑,具体的变成思路我会在程序中进行说明。本程序主要借鉴机器学习实验室代码。
首先我们需要加载包和今天所需要使用的数据,这个数据只有靠我们自己来生成,于是数据分布和具体数据如下,

import numpy as np 
import pandas as pd 
import matplotlib.pyplot as plt

np.random.seed(529)
x_1= np.random.randint(0,5,6),np.random.randint(0,5,6)
x_2= np.random.randint(5,10,6),np.random.randint(5,10,6)

plt.scatter(x_1[0], x_1[1], label='1', color='#ffb07c', s=100)
plt.scatter(x_2[0], x_2[1], label='-1', color='#c94cbe', s=100)
plt.legend()
plt.show()

data = {
    1 : np.hstack((x_1[0].reshape(-1,1),x_1[1].reshape(-1,1))),
    -1: np.hstack((x_2[0].reshape(-1,1),x_2[1].reshape(-1,1)))
}

在这里插入图片描述

{1: array([[1, 1],
       [2, 0],
       [3, 3],
       [4, 0],
       [2, 3],
       [1, 0]]), 
-1: array([[9, 7],
       [8, 9],
       [6, 5],
       [9, 7],
       [9, 9],
       [5, 6]])}

然后构建训练模型,这里的目标函数和约束条件不变,但整体思路为,先确定两个 w |w| ,通过变化两个 w w 的正负号,然后对b进行搜索判断,筛选出符合约束条件的目标函数,并以字典形式记录目标函数与 w , b w,b ,然后再对 w |w| 进行更新,减去一个步长,如此循环,直到 w |w| 被减到小于0时停止,于是选择出目标函数最大的 w , b w,b 。然后在上一个循环的最优结果的基础上进行下一个步长的讨论,这里的搜索范围是根据特征中的最大值进行规定,具体见代码。这里为了实现方便,我们选择对最大间隔的目标函数与约束条件进行优化。

def train(data):

    opt_dict={} #定义最优结果集

    #定义w转化搜索
    transforms = np.array([[1,1], [1,-1], [-1,1], [-1,-1]])
    
    #寻找最大值用于划定参数训练范围
    features = []
    for classes in data:
        for sets in data[classes]:
            for feat in sets:
                features.append(feat)

    max_value = max(features)                               #获取最大特征值
    steps = max_value * np.array([0.1, 0.01, 0.001, 0.0001])#设置不同的步长,用于更新参数速度的调整
    optim_inter = max_value*10                              #为了避免设置的w参数起始数据太小,于是设置为最大值的10倍

    #不同步长的优化循环
    for step in steps:

        w = [optim_inter*0.99, optim_inter*1.01]            #初始化w,并加入一个抖动,使得两个参数不一致
        optim_key = False                                   #用于判断当前步长是否训练完成
        while(not optim_key):
            for b in np.arange(-2*(max_value),2*(max_value),step*5): #确定b的搜索范围已经搜索步长
                for trans in transforms:
                    w_trans = w*trans                       #将w分别转换为4种状态
                    key = True                              #用于判断是否当前w,b下所有样本点满足约束
                    for classes in data:
                        for sets in data[classes]:
                            y = classes
                            if (y*(np.dot(w_trans, sets.T) + b)) < 1:#约束条件
                                key = False
                    if key:                                             #满足约束的话就对目标函数与w,b进行记录
                        opt_dict[1/2*np.linalg.norm(w, ord=2)] = [w_trans, b]
            #对w进行更新
            if w[0]<0 or w[1]<0:
                print("完成此次更新。")
                optim_key = True
            else:
                w = w - step #将w减去一个步长进行衰减式搜索
        
        #循环完本次step选择最大的目标函数的参数b进行记录,然后为了避免错过最优点,进行最优参数会调,然后再继续迭代不同的step
        max_obj = sorted(opt_dict,reverse=True)
        optim_ans = opt_dict[max_obj[0]]
        optim_inter = optim_ans[0][0] + step*2
    w = optim_ans[0]
    b = optim_ans[1]
    #输出每个样本的类别测试
    for classes in data:
        y = classes
        for sets in data[classes]:     
            print(sets,":", np.sign(np.dot(sets, w) + b))

    return w, b

我们来看一下训练函数的输出,

w, b = train(data)
print("w{} : |b : {}".format(w, b))
完成此次更新。
完成此次更新。
完成此次更新。
完成此次更新。
[1 3] : 1.0
[1 4] : 1.0
[4 3] : 1.0
[2 3] : 1.0
[2 1] : 1.0
[0 0] : 1.0
[9 9] : -1.0
[5 9] : -1.0
[9 6] : -1.0
[7 5] : -1.0
[7 5] : -1.0
[8 6] : -1.0
w[-0.9 -2.7] : |b : 13.5

下面再来定义一下预测函数,

def predict(feature_all, w, b):
    prediction = []
    for classes in feature_all:
        for sets in feature_all[classes]:
            y = classes
            prediction.append(np.sign((np.dot(sets, w) + b)))
    return prediction #返回每个样本所预测的类

再来测试一下测试函数的效果

feature_1 = []
feature_2 = []
np.random.seed(576)
for i in range(20):
    feature1.append([np.random.randint(0,5), np.random.randint(0,5)])
    feature_2.append([np.random.randint(5,10), np.random.randint(5,10)])

features_test = {
    1 : np.array(feature_1),
    -1: np.array(feature_2)
}
predict(features_test, w, b)
[1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0,
 -1.0]

最后呢我们来绘图康一下我们的效果咋样,用不同的图形表示训练集和测试集,然后我们在话两根过支持向量的间隔线。

#绘制原始数据与预测数据点
colors= {1:'#ffb07c', -1:'#c94cbe'}
[[plt.scatter(sets[0],sets[1], color = colors[classes], s=100, marker="*") for sets in features_test[classes]]for classes in features_test]
[[plt.scatter(sets[0],sets[1], color = colors[classes], s=100) for sets in data[classes]]for classes in data]

#绘制分隔线与间隔线
x = np.linspace(0, 10, 50)
y = -w[0]/w[1]*x-b/w[1]
y1 = -w[0]/w[1]*x-(b+1)/w[1] #正间隔线
y2 = -w[0]/w[1]*x-(b-1)/w[1] #负间隔线
plt.plot(x, y, color='#087804')
plt.plot(x, y1, color='#048243', ls='--')
plt.plot(x, y2, color='#048243', ls='--')
plt.show()

在这里插入图片描述
最后按照惯例我们将代码进行封装,

import numpy as np 
import pandas as pd 
import matplotlib.pyplot as plt

class LinearSvm():
    def __init__(self):
        self.colors= {1:'#ffb07c', -1:'#c94cbe'}
        self.fig = plt.figure()
        self.ax = self.fig.add_subplot(111)

    def load_data(self):
        np.random.seed(529)
        x_1= np.random.randint(0,5,6),np.random.randint(0,5,6)
        x_2= np.random.randint(5,10,6),np.random.randint(5,10,6)
        train_data = {
            1 : np.hstack((x_1[0].reshape(-1,1),x_1[1].reshape(-1,1))),
            -1: np.hstack((x_2[0].reshape(-1,1),x_2[1].reshape(-1,1)))
        }
        w, b = lsvm.train(train_data)

        feature_1 = []
        feature_2 = []
        np.random.seed(576)
        for i in range(20):
            feature_1.append([np.random.randint(0,5), np.random.randint(0,5)])
            feature_2.append([np.random.randint(5,10), np.random.randint(5,10)])

        test_data = {
            1 : np.array(feature_1),
            -1: np.array(feature_2)
        }

        return train_data, test_data
    
    def train(self, data):
        
        opt_dict={} #定义最优结果集

        #定义w转化搜索
        transforms = np.array([[1,1], [1,-1], [-1,1], [-1,-1]])

        #寻找最大值用于划定参数训练范围
        features = []
        for classes in data:
            for sets in data[classes]:
                for feat in sets:
                    features.append(feat)

        max_value = max(features)                               #获取最大特征值
        steps = max_value * np.array([0.1, 0.01, 0.001, 0.0001])#设置不同的步长,用于更新参数速度的调整
        optim_inter = max_value*10                              #为了避免设置的w参数起始数据太小,于是设置为最大值的10倍

        #不同步长的优化循环
        for step in steps:

            w = [optim_inter*0.99, optim_inter*1.01]            #初始化w,并加入一个抖动,使得两个参数不一致
            optim_key = False                                   #用于判断当前步长是否训练完成
            while(not optim_key):
                for b in np.arange(-2*(max_value),2*(max_value),step*5): #确定b的搜索范围已经搜索步长
                    for trans in transforms:
                        w_trans = w*trans                       #将w分别转换为4种状态
                        key = True                              #用于判断是否当前w,b下所有样本点满足约束
                        for classes in data:
                            for sets in data[classes]:
                                y = classes
                                if (y*(np.dot(w_trans, sets.T) + b)) < 1:#约束条件
                                    key = False
                        if key:                                             #满足约束的话就对目标函数与w,b进行记录
                            opt_dict[1/2*np.linalg.norm(w, ord=2)] = [w_trans, b]
                            
                #对w进行更新
                if w[0]<0 or w[1]<0:
                    print("完成此次更新。")
                    optim_key = True
                else:
                    w = w - step #将w减去一个步长进行衰减式搜索

            #循环完本次step选择最大的目标函数的参数b进行记录,然后为了避免错过最优点,进行最优参数会调,然后再继续迭代不同的step
            if len(opt_dict) == 0:
                optim_inter = w + step*10
            else:
                max_obj = sorted(opt_dict, reverse=True)
                optim_ans = opt_dict[max_obj[0]]
                optim_inter = optim_ans[0][0] + step*2
        w = optim_ans[0]
        b = optim_ans[1]
        #输出每个样本的类别测试
        for classes in data:
            y = classes
            for sets in data[classes]:     
                print(sets,":", np.sign(np.dot(sets, w) + b))

        return w, b
    
    def test(self, feature_all, w, b):

        prediction = []
        for classes in feature_all:
            for sets in feature_all[classes]:
                prediction.append(np.sign((np.dot(sets, w) + b)))
        return prediction
    
    def vis(self, train_data, test_data):
        
        [[self.ax.scatter(sets[0],sets[1], color = self.colors[classes], s=100, marker="*") for sets in test_data[classes]]for classes in test_data]
        [[self.ax.scatter(sets[0],sets[1], color = self.colors[classes], s=100) for sets in train_data[classes]]for classes in train_data]
        x = np.linspace(0, 10, 50)
        y = -w[0]/w[1]*x-b/w[1]
        y1 = -w[0]/w[1]*x-(b+1)/w[1] #正间隔线
        y2 = -w[0]/w[1]*x-(b-1)/w[1] #负间隔线
        self.ax.plot(x, y, color='#087804')
        self.ax.plot(x, y1, color='#048243', ls='--')
        self.ax.plot(x, y2, color='#048243', ls='--')


if __name__ == "__main__":
    lsvm = LinearSvm()
    train_data, test_data = lsvm.load_data()
    print("w{} : |b : {}".format(w, b))
    prediction = lsvm.test(test_data, w, b)
    lsvm.vis(train_data, test_data)

结语
好啦,对于线性可分支持向量机的分享就到这里啦,线性可分支持向量机最大的前提就是线性可分,然后用硬间隔的方法,将数据隔开。这样的方法对于现实生活中更复杂的数据显得不太实用,后面会继续补充支持向量机的其他内容。整个代码设计的逻辑来源于参考文献。
谢谢阅读
参考
公众号:机器学习实验室
《统计学习方法(第二版)》—李航

猜你喜欢

转载自blog.csdn.net/qq_35149632/article/details/105489330
今日推荐