支持向量机(support vector machines, SVM)是一种分类模型,该模型在特征空间中求解间隔最大的分类超平面。当训练数据近似线性 可分时,可以通过增加软间隔学习一个线性分类器。当线性不可分时,利用核技巧,隐式的将特征空间映射到高维特征空间,从而达到线性 可分。使用序列最小最优化算法(SMO),可以快速求解模型的参数。 关键字:支持向量机; SVM; SMO; 矩阵运算; 矩阵求导;numpy;sklearn.
当训练数据线性可分,可以得到一个线性超平面 ,将在超平面上方的归为正类,将在超平面下方的归为负类。当数据点 与超平面距离越远时,表示分类的确定性越高,这样虽然线性分类的超平面可能有无数多个,但是我们可以找到一个所有点距离超平面最大的一个 超平面。相应的决策函数为:
为正时,为正,当 为负时,为负,则可以定义 为几何间隔,表示数据点距离超平面的距离。 模型最终会找到一个参数为和的分离超平面,所有点距离超平面的距离都大于等于d,将距离正好等于d的数据点称之为支持向量。
将 和 进行一定比例的缩放
可以{eq1}将式化简为:
将{eq2} 和 {eq3} 利用拉格朗日乘数法,获得拉格朗日原始问题形式:
当满足KTT条件时, 拉格朗日对偶问题的解等价于{eq4} 的解:
首先求L以 w、b为参数的极小值,通过求微分得到偏导数形式。
从而得到w、b 偏导数,并令偏导数为0。
推导出如下关系:
将{eq6}式 和 {eq7}式 代入{eq5}式中,
去除负号,将极大转换成极小形式:
为了使拉格朗日对偶问题的解与原始问题解相同,需要同时满足KTT条件:
当训练数据近似线性可分,有些异常点或噪声点导致无法找到分离超平面,可以对每个数据点加一个松弛变量,从而让所有数据点均满足约束。
加入软间隔参数,每个向量点距离分类超平面距离增加,同时增加一个惩罚系数C,代入{eq5}新形式如下:
将其转换为拉格朗日对偶形式:
求得对于w、b、 的偏导并令其为0:
代入 {eq9} 式中:
、 、约束可以化简为:
为了使拉格朗日对偶问题的解与原始问题解相同,需要同时满足KTT条件:
近似线性可分用软间隔方式解决,然而当训练数据是非线性数据,会出现无法在原特征空间找到分离超平面。可以使用一个非线性变换,将数据从原特征空间映射到更高维的新空间,然后在新空间中寻找线性分类超平面,这种方法被称为核技巧。观察 {eq10} 式中计算,即需要计算 内积。将核技巧应用到SVM,定义核函数为:
即将原来的向量内积,改成先让向量映射到新空间,然后再求内积。核技巧的另外一个优点是,不需要显示的定义 而是直接计算出 的结果,以高斯核函数为例:
则 {eq10} 式利用核技巧转化为:
支持向量机的拉格朗日对偶问题是一个凸二次规划问题,具有全局最优解,序列最小最优化算法即SMO算法,是高效求解支持向量机解的一种算法。其基本思路是,如果所有变量都满足了KTT条件,那么就求得了问题的解。SMO算法过程如下:
当选择两个变量,如时,将其他变量看做常数:
则上式子去除不包含 的项后化简为:
利用约束 得到:
将代入:
针对求导,并令导数为0:
则可以推得:
考虑到 令 ,将将内积转换成核函数形式,并且定义函数为函数与的误差值:
则 {eq12} 式得到未经剪辑的解为:
因为有约束 并且 ,下面讨论的上限下限,当时:
当时:
则经剪辑后解为:
对于偏置,由于KTT条件约束有:
对于任一有 整理后得到,当时:
当时:
若 、都满足条件,那么。当所有满足KTT条件时,模型得解。
当训练数据线性可分时,支持向量机可以得到一个最大间隔的分离超平面,若近似可分,则可以添加软间隔的方式,若线性不可分,则可以利用核技巧确保可以找到最大间隔分离超平面。由于得到的分离间隔最大,支持向量机有较好的鲁棒性。
通过拉格朗日对偶变换,通过KTT条件,可以将原始问题转换为拉格朗日对偶问题,从而自然的引入核函数。SMO是常用的模型训练算法,其本质上是凸二次规划问题,当所有变量都满足KTT条件,可以得到最佳解。SMO的计算过程直接利用解析解,求解速度较快。
[1] Ian Goodfellow / Yoshua Bengio / Aaron Courville . Deep Learning[M]. 北京: 清华 大学出版社,2017-08-01. [2] 张贤达. 矩阵分析与应用[M]. 北京: 人民邮电出版社,2013-11-01. [3] 李航. 统计学习方法[M]. 北京: 清华大学出版社,2012-03. [4] David C. Lay / Steven R. Lay / Judi J. McDonald . 线性代数及其应用[M]. 北京: 机械工业出版社,2018-07. [5] 史蒂文·J. 米勒. 普林斯顿概率论读本[M]. 北京: 人民邮电出版社,2020-08.
xxxxxxxxxx
# -*- coding: utf8 -*-
import math
import time
import numpy as np
import sklearn
from sklearn import datasets
from sklearn.datasets import load_breast_cancer
from sklearn import preprocessing
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score
dataset = sklearn.datasets.load_breast_cancer()
dataset.target[dataset.target == 0] = -1 #0/1 标记替换成1/-1标记
class GSKernal:
def __init__(self):
self.sigma = 10
def cal(self, x, z):
result = None
if len(x.shape) == 2:
result = np.linalg.norm(x-z, axis=1) ** 2
else:
result = np.linalg.norm(x-z) ** 2
return np.exp(-1 * result / (2 * self.sigma**2))
class SVM:
def __init__(self, kn = None, C = 1, toler = 0.001):
self.w = None #w
self.b = None #b
if not kn:
kn = GSKernal()
self.kn= kn
self.C = C
self.toler = toler
self.alpha = None
self.X = None
self.Y = None
self.kCache = {}
return
def calK(self, i, j):
key = ''
if i <= j:
key = '%d_%d'%(i, j)
else:
key = '%d_%d'%(j, i)
ret = self.kCache.get(key)
if ret:
return ret
x1 = self.X[i]
x2 = self.X[j]
ret = self.kn.cal(x1, x2)
self.kCache[key] = ret
return ret
def calK2(self, j):
key = 'All_%d'%(j)
ret = self.kCache.get(key)
if ret:
return ret['data']
ret = np.zeros(self.X.shape[0])
x2 = self.X[j]
for i in range(self.X.shape[0]):
x1 = self.X[i]
ret[i] = self.kn.cal(x1, x2)
self.kCache[key] = {'data':ret}
return ret
def isZero(self, v):
return math.fabs(v) < self.toler
def calGxi(self, i):
xi = self.X[i]
ay = np.multiply(self.alpha, self.Y)
gxi = np.dot(ay, self.calK2(i)) + self.b
return gxi
def calcEi(self, i):
gxi = self.calGxi(i)
return gxi - self.Y[i]
def isSatisfyKKT(self, i):
gxi = self.calGxi(i)
yi = self.Y[i]
if self.isZero(self.alpha[i]) and (yi * gxi >= 1):
return True
elif self.isZero(self.alpha[i] - self.C) and (yi * gxi <= 1):
return True
elif (self.alpha[i] > -self.toler) and (self.alpha[i] < (self.C + self.toler)) \
and self.isZero(yi * gxi - 1):
return True
return False
def getAlphaJ(self, E1, i):
E2 = 0
maxE1_E2 = -1
maxIndex = -1
for j in range(self.X.shape[0]):
if j == i:
continue
E2_tmp = self.calcEi(j)
if E2_tmp == 0:
continue
if math.fabs(E1 - E2_tmp) > maxE1_E2:
maxE1_E2 = math.fabs(E1 - E2_tmp)
E2 = E2_tmp
maxIndex = j
if maxIndex == -1:
maxIndex = i
while maxIndex == i:
maxIndex = int(random.uniform(0, self.X.shape[0]))
E2 = self.calcEi(maxIndex)
return E2, maxIndex
def fit(self, X, Y, iterMax = 10):
self.kCache.clear()
scalerX = sklearn.preprocessing.StandardScaler().fit(X)#StandardScaler
#Y = Y.reshape(-1, 1)
X = scalerX.transform(X)
w = np.zeros(X.shape[1])
self.b = 0
a = np.zeros(X.shape[0])
self.alpha = a
self.X = X
self.Y = Y
iterStep = 0; parameterChanged = 1
calTims = 0
while (iterStep < iterMax) and (parameterChanged > 0):
iterStep += 1
parameterChanged = 0
for i in range(self.X.shape[0]):
if self.isSatisfyKKT(i):
continue
E1 = self.calcEi(i)
E2, j = self.getAlphaJ(E1, i)
y1 = self.Y[i]
y2 = self.Y[j]
x1 = self.X[i]
x2 = self.X[j]
a1_old = a[i]
a2_old = a[j]
if y1 != y2:
L = max(0, a2_old - a1_old)
H = min(self.C, self.C + a2_old - a1_old)
else:
L = max(0, a2_old + a1_old - self.C)
H = min(self.C, a2_old + a1_old)
if L == H:
continue
k11 = self.calK(i, i)
k12 = self.calK(i, j)
k21 = k12
k22 = self.calK(j, j)
ay = np.multiply(a, self.Y)
ayk1 = np.dot(ay, self.calK2(i))
ayk2 = np.dot(ay, self.calK2(j))
a2_new = a2_old + (y2 * (ayk1 - y1 - (ayk2 - y2))) / (k11 - 2*k12 + k22)
a1_new = a1_old + y1 * y2 * (a2_old - a2_new)
b1New = -1 * E1 - y1 * k11 * (a1_new - a1_old) \
- y2 * k21 * (a2_new - a2_old) + self.b
b2New = -1 * E2 - y1 * k12 * (a1_new - a1_old) \
- y2 * k22 * (a2_new - a2_old) + self.b
bNew = 0
if (a1_new > 0) and (a1_new < self.C):
bNew = b1New
elif (a2_new > 0) and (a2_new < self.C):
bNew = b2New
else:
bNew = (b1New + b2New) / 2
self.alpha[i] = a1_new
self.alpha[j] = a2_new
self.b = bNew
if math.fabs(a2_new - a2_old) >= 0.00001:
parameterChanged += 1
calTims += 1
if calTims % 50 == 0:
print('train iter:%d SMO times:%d'%(iterStep, calTims))
#time.sleep(1)
#return
Yo = self.predict(self.X)
#print(Yo)
score = r2_score(Yo, self.Y)
print('score', score)
return
def predict(self, X):
if len(X.shape) == 1:
return self.predictOne(X)
ret = np.zeros(X.shape[0])
for i in range(X.shape[0]):
ret[i] = self.predictOne(X[i])
return ret
def predictOne(self, x):
ret = np.zeros(self.X.shape[0])
x2 = x
for i in range(self.X.shape[0]):
x1 = self.X[i]
ret[i] = self.kn.cal(x1, x2)
ay = np.multiply(self.alpha, self.Y)
gxi = np.dot(ay, ret) + self.b
if gxi > 0:
return 1
return -1
if __name__ == '__main__':
svm = SVM()
print('data', dataset.data.shape)
svm.fit(dataset.data, dataset.target)