本篇博客主要内容为:
- SVM公式推导;
- SMO公式推导;
- Python实现SMO和SVM,两种核(linear和RBF);
- 逻辑斯蒂回归实现非线性分类;(有待更新….)
由于SVM公式比较多,《统计学习方法》这本书做了详细的讲解,我结合原作者的论文和《统计学习方法》以及周志华老师的《机器学习》做个个人总结,便于之后的复习。对于过多的公式不做赘述,着重介绍自己在学习SVM时遇到的一些绊脚石,大家或多或少都遇到了,如果能帮助解决一两个小疑惑那是再好不过了,本人才疏学浅,如果有错误疏漏的地方,还请在评论指出。
题记:
说实话,整理SVM我的内心是拒绝的,因为难啊!但你要问它重不重要?我的标题都加“重磅”了,表明它非常重要啊,怎么看出的?在图像分类方面,一直都是SVM的表现最好,直到Hinton团队提出的深度学习碾压了SVM误差率,才开启了深度学习的春天。再次,看《统计学习方法》这本书,薄薄的200页,11章内容,SVM一章就占了40页,无出其右,可见其内容繁多和重要性了。
怎么办?硬着头皮上吧,难不成指望面试官不考?
支持向量机和之前的逻辑回归同属于判别模型,即利用一个分离超平面去分隔样本。
与逻辑回归不同的是,SVM使用的是最大间隔超平面,能够保证很好的泛化能力。并且其具有强大的核技巧能够使得SVM应用于非线性分类中,事实上,逻辑斯蒂回归也可以使用类似的想法实现非线性分类,我们在博客的最后也会讲解和实现。
支持向量机实现了以下想法:
它通过一些非线性映射方法将输入向量从输入空间 X 映射到高维的特征空间 Z ,在特征空间中,构造了一个线性决策平面,并且其有些特性能够保证支持向量机的高泛化能力。
上述想法有两个问题需要解决:
- 如何找到一个分离超平面保证其具有良好的泛化能力,因为特征空间的维度非常大;
- 在高维特征空间如何计算的问题;
**针对第一个问题:**SVM中最优超平面定义为一个线性决策函数,其对两种类别的特征向量有最大间隔。
这种方法构造的超平面只需要考虑被称为支持向量的少数样本,即支持向量决定了间隔大小。
我们来用数学公式表述这种想法:
我们使用输入向量映射到特征空间后的特征向量为 z⃗ ,则特征空间中的超平面可表示为( ω是向量,b是标量 ):
化简得:
至此我们得到了目标函数和约束条件的最终条件。(之前在这里推导比较困惑我的是最大间隔
为什么是 2∥ω∥ ,以及某样本到超平面的距离
,所以我着重推导了一下,不是很难,其他基本和书上推导过程一致)
目标函数得到了,是一个典型的凸二次规划问题。这是我们第一次遇到含有约束条件的优化问题,之前都是无约束的优化利用梯度法就可以求解了。
含有约束条件的凸优化问题,使用拉格朗日对偶来求解。
对待拉格朗日对偶,判断好是在哪个变量下取得极值还是比较好理解的。
拉格朗日对偶,是对于满足下式情况的约束最优化问题,构造原始问题的对偶问题,通过求解对偶问题,来得到原始问题的解。
其对偶问题为拉格朗日问题的极大极小问题,同理推出:
KKT条件
时,得到了强对偶性,此时对偶问题的解和原始问题的解相等。
KKT条件中有个重要的等式(对偶互补条件)即
则对最终分类函数有影响的仅是支持向量这些样本。
援引SVM论文中的一段话:
实验显示,如果训练向量能够被最优超平面无错误的分离,其在测试集向量上的错误率满足下面不等式,其上界为支持向量数量/训练集向量总数。
从上式可以看出,其和特征空间的维度没有关系,只和少量的支持向量以及训练向量数量有关,说明支持向量机的泛化能力很高(甚至在无限维度空间中)。论文中实验证明,其错误率低至0.03,在数十亿维度的特征空间中仍然泛化的很好。
通过拉格朗日函数构造原始问题的对偶问题,可以很容易得到线性可分支持向量机,线性支持向量机的对偶问题。
这里列举线性可分支持向量机的对偶问题(线性可分支持向量机使其特殊情况):
我们看到对偶问题最优解是个向量,其维度和样本量是一样的,在样本量非常大的时候,求解非常慢,这也是后续SMO引出的原因。
到此解决了作者提出的第一个问题——找到这个分离超平面且保证模型具有良好的泛化性能,对于第二个问题——在高维空间中如何计算的问题。
正如我们之前所表述的,对于线性可分和线性不可分的情况,一般我们不需要将输入向量映射到特征空间中即可求出分离超平面了(或者成为输入空间和特征空间相同)。但是实质上,线性超平面是存在于特征空间中的,所以上式最准确的表达式是:
注意看上式的分类决策函数,决策函数内部,是将输入向量( xj )和支持向量( xi )先映射到高维的特征空间 Z 中,求点积,而特征空间的维度一般是非常大,很难计算。
如果在输入空间存在一个函数 K(x1,x2)=ϕ(x1)⋅ϕ(x2) ,它恰好等于高维空间中的这个内积,那么支持向量机就不用计算复杂的非线性变换后的内积了,大大简化了计算。
对于给定的核 K(x1,x2) ,特征空间 H 和映射函数 ϕ 的取法不唯一,可以取不同的特征空间,即使在同一特征空间也可以取不同的映射。
一般使用的多项式核和高斯核,我们在代码中实现了高斯核(RBF)。
作者一开始提出的两个问题(超平面的选取以及高维特征空间的计算问题)都已经有了相应的解决方式,下面就需要方法来求解对偶问题的解了。
对偶问题的解 α⃗ ,其维度和样本数一样,当样本容量很大时,求解非常困难。
目前广泛使用的序列最小最优化算法来求对偶问题的解。
SMO的思想:训练支持向量机需要解决非常大的二次规划优化问题(对偶问题的解 α⃗ 维度和样本量大小一样)。 SMO将这个大的二次规划问题分解为一系列尽可能小的二次规划问题。SMO所需的内存量对于训练集大小上是线性的,这允许SMO处理非常大的训练集。
SMO算法通过两层循环,外层循环选择第一个变量,内层循环选择第二个变量。
外层:
- 首先一次迭代
所有的训练集,如果某个样本违反KKT条件,则将其作为第一个变量。(按照这样,迭代一次所有的训练样本);
- 多次迭代
非边界的训练样本( α非0和C ),如果某个样本违反KKT条件,同样将其作为第一个变量。(按照这样,迭代多次非边界的训练样本),直到所有非边界的训练样本都符合KKT条件。
外层在上述两个策略中循环直到所有的训练样本在检验范围 ϵ (一般选择 10−3 )内都符合KKT条件或者到达迭代次数为止。
内层:内层循环寻找的变量 α2 希望使得 |E1−E2| 最大(编程时将每个样本对应的 Ei 保存在列表中,便于快速查找)。
内层循环会出现些特殊情况导致目标函数无法有足够的下降(论文:if the above heuristic does not make positive progress)
我在程序中使用的是下列条件判断:
那么内层会使用下列启发式规则寻找第二个变量:
- 迭代非边界的样本直到有足够的下降;
- 迭代全体样本直到有足够的下降;
上述两次迭代都随机选择开始点
,如果仍没有足够的下降,则重新选择 α1
阈值 b 的使用 b1和b2 的中点更新。
结合SMO论文的伪代码,就可以开始愉快的编写SMO求解SVM的Python代码了。
代码如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date : 2018-03-29 17:06:57
# @Author : guanglinzhou ([email protected])
# @Link : https://github.com/GuanglinZhou
import numpy as np
from random import shuffle
import matplotlib.pyplot as plt
import math
# 加载数据集
def loadDataSet(fileName):
dataMat = []
labelMat = []
fr = open(fileName)
for line in fr.readlines():
lineArr = line.strip().split('\t')
dataMat.append([float(lineArr[0]), float(lineArr[1])])
labelMat.append(float(lineArr[2]))
dataMat = np.mat(dataMat)
labelMat = np.mat(labelMat).transpose()
return dataMat, labelMat
# 定义一个类作为数据结构,存储一些关键值
class classSMO:
def __init__(self, dataMat, labelMat, C, tol): # Initialize the structure with the parameters
self.dataMat = dataMat
self.labelMat = labelMat
self.C = C
self.tol = tol
self.m = np.shape(dataMat)[0]
self.alphas = np.mat(np.zeros((self.m, 1)))
self.b = 0
self.eCache = np.mat(np.zeros((self.m, 1))) # first column is valid flag
self.kernelType = 'linear'
# self.kernelType = 'Gaussian'
# 储存K(xi,xj)的值
self.kernelFunctionValue = np.mat(np.zeros((self.m, self.m)))
for i in range(self.m):
self.kernelFunctionValue[:, i] = kernel(i, self)
def kernel(i, objectSMO):
if (objectSMO.kernelType == 'linear'):
return np.dot(objectSMO.dataMat[:, :], objectSMO.dataMat[i, :].transpose())
if (objectSMO.kernelType == 'Gaussian'):
sigma = 1
temp = -np.square(objectSMO.dataMat[:, :] - objectSMO.dataMat[i, :]) / (2 * math.pow(sigma, 2))
ndarr = np.array(temp)
ndarr = np.sum(ndarr, axis=1)
temp = np.mat(ndarr).transpose()
return np.exp(temp)
# 计算g(xi)的值
def computeGx(i, objectSMO):
if (objectSMO.kernelType == 'linear'):
g_xi1 = np.multiply(objectSMO.alphas, objectSMO.labelMat)
g_xi2 = np.multiply(objectSMO.dataMat, objectSMO.dataMat[i])
g_xi = np.sum(np.multiply(g_xi1, g_xi2)) + objectSMO.b
elif (objectSMO.kernelType == 'Gaussian'):
g_xi = np.multiply(objectSMO.alphas, objectSMO.labelMat)
g_xi = np.multiply(g_xi, objectSMO.kernelFunctionValue[:, i])
g_xi = np.sum(g_xi) + objectSMO.b
else:
g_xi = 0
return g_xi
# 计算Ei的值
def computeEk(k, objectSMO):
print('computeEi')
g_xi = computeGx(k, objectSMO)
return (g_xi - objectSMO.labelMat[k]).item()
# 检查误分类的个数
def checkClassifierErrorSample(objectSMO):
errorNum = 0
for i in range(objectSMO.dataMat.shape[0]):
predi = np.sign(computeGx(i, objectSMO))
yi = objectSMO.labelMat[i].item()
if (predi != yi):
errorNum += 1
print('核函数为:{}, 样本总数为:{},分类错误的样本个数为:{}'.format(objectSMO.kernelType, objectSMO.m, errorNum))
# 绘制原始数据
def plotData(Xmat, ymat):
Xarray = np.array(Xmat)
yarray = np.array(ymat)
col = {+1: 'r', -1: 'b'}
plt.figure()
for i in range(Xarray.shape[0]):
plt.plot(Xarray[i, 0], Xarray[i, 1], col[yarray[i][0]] + 'o')
plt.show()
# 更新Ei
def updateEk(objectSMO, k):
objectSMO.eCache[k] = computeEk(k, objectSMO)
# 更新alpha1和alpha2
def takeStep(i, j):
if (i == j):
return 0
# y1 y2 E1 E2为标量形式
y1 = objectSMO.labelMat[i].item()
y2 = objectSMO.labelMat[j].item()
E1 = computeEk(i, objectSMO)
E2 = computeEk(j, objectSMO)
# alpha1old alpha2old为标量形式
alpha1old = objectSMO.alphas[i].item()
alpha2old = objectSMO.alphas[j].item()
s = y1 * y2
if (labelMat[i] != labelMat[j]):
L = max(0, alpha2old - alpha1old)
H = min(C, C + alpha2old - alpha1old)
else:
L = max(0, alpha2old + alpha1old - C)
H = min(C, alpha2old + alpha1old)
if (L == H):
return 0
# Kij为标量形式
K11 = objectSMO.kernelFunctionValue[1, 1]
K22 = objectSMO.kernelFunctionValue[2, 2]
K12 = objectSMO.kernelFunctionValue[1, 2]
eta = K11 + K22 - 2 * K12
if (eta > 0):
alpha2New = alpha2old + y2 * (E1 - E2) / eta
if (alpha2New < L):
alpha2New = L
elif (alpha2New > H):
alpha2New = H
else:
print('eta<=0')
return 0
if (np.abs(alpha2New - alpha2old) < 0.00001):
print('alpha2 step size too small')
return 0
alpha1New = alpha1old + s * (alpha2old - alpha2New)
b1 = -E1 - y1 * K11 * (alpha1New - alpha1old) - y2 * K12 * (alpha2New - alpha2old) + objectSMO.b
b2 = -E2 - y1 * K12 * (alpha1New - alpha1old) - y2 * K22 * (alpha2New - alpha2old) + objectSMO.b
# update
objectSMO.b = (b1 + b2) / 2
updateEk(objectSMO, i)
updateEk(objectSMO, j)
objectSMO.alphas[i] = alpha1New
objectSMO.alphas[j] = alpha2New
return 1
# 选择第2个变量
def secondChoice(i1, objectSMO):
maxAbsEDelta = 0
i2OfMaxAbsEDelta = 0
Ei1 = objectSMO.eCache[i1].item()
for i2 in range(objectSMO.eCache.shape[0]):
Ei2 = objectSMO.eCache[i2].item()
absEDelta = np.abs(Ei1 - Ei2)
if (absEDelta > maxAbsEDelta):
maxAbsEDelta = absEDelta
i2OfMaxAbsEDelta = i2
return i2OfMaxAbsEDelta
# 内层循环
def examineExample(i1, objectSMO, C):
y1 = objectSMO.labelMat[i1]
alpha1Old = objectSMO.alphas[i1]
E1 = computeEk(i1, objectSMO)
r1 = E1 * y1
if ((r1 < -tol and alpha1Old < C) or (r1 > tol and alpha1Old > 0)):
set1 = set(np.where(objectSMO.alphas > 0)[0])
set2 = set(np.where(objectSMO.alphas < C)[0])
indexAlphaNot0andC = set1 & set2
if (len(indexAlphaNot0andC) > 1):
i2 = secondChoice(i1, objectSMO)
if (takeStep(i1, i2)):
return 1
# loop over all no-bound alpha,starting at a random point
indexAlphaNot0andCList = list(indexAlphaNot0andC)
shuffle(indexAlphaNot0andCList)
for i2 in indexAlphaNot0andCList:
if (takeStep(i1, i2)):
return 1
# loop over all possible i1,starting at a random point
indexAllDataSet = list(range(objectSMO.dataMat.shape[0]))
shuffle(indexAllDataSet)
for i2 in indexAllDataSet:
if (takeStep(i1, i2)):
return 1
return 0
# 外循环
def mainRoutine(dataMat, C, maxIter):
m, n = np.shape(dataMat)
numChanged = 0
examineAll = True
iterNum = 0
while ((iterNum < maxIter) and (numChanged > 0 or examineAll)):
numChanged = 0
if (examineAll):
for i in range(m):
numChanged += examineExample(i, objectSMO, C)
iterNum += 1
else:
set1 = set(np.where(objectSMO.alphas > 0)[0])
set2 = set(np.where(objectSMO.alphas < C)[0])
indexAlphaNot0andC = set1 & set2
for i in indexAlphaNot0andC:
numChanged += examineExample(i, objectSMO, C)
iterNum += 1
if (examineAll == True):
examineAll = False
elif (numChanged == 0):
examineAll = True
# 计算linear的权重
def computeTheta(objectSMO):
theta = np.dot(np.multiply(objectSMO.alphas, objectSMO.labelMat).transpose(), objectSMO.dataMat)
b = objectSMO.b
thetaList = theta.tolist()
thetaList[0].append(b)
theta = np.mat(thetaList).transpose()
return theta
# 绘制核为linear的超平面
def plotHyperplaneLinear(Xmat, ymat, theta, objectSMO):
m, n = np.shape(Xmat)
Xarray = np.array(Xmat)
yarray = np.array(ymat)
col = {+1: 'r', -1: 'b'}
plt.figure()
for i in range(Xarray.shape[0]):
plt.plot(Xarray[i, 0], Xarray[i, 1], col[yarray[i][0]] + 'o')
plt.ylim([-6, 6])
plt.plot(Xarray[:, 0],
(-(theta[0][0] * Xarray[:, 0] + np.multiply(theta[2][0], np.ones((m, 1)))) / theta[1][0]).transpose(),
c='g')
# 标出支持向量
for i in range(objectSMO.m):
if (round(objectSMO.alphas[i].item(), 3) < objectSMO.C and round(objectSMO.alphas[i].item(), 3) > 0):
plt.plot(Xarray[i, 0], Xarray[i, 1], 'y' + 'o')
plt.show()
if __name__ == '__main__':
'''
通过更改此处文件名,以及类别classSMO中成员kernelType,切换linear kernel和RBF kernel
'''
fileName = 'testSetLinear.txt'
# fileName = 'testSetRBF.txt'
dataMat, labelMat = loadDataSet(fileName)
print(np.shape(dataMat))
C = 200
tol = 0.001
maxIter = 200
objectSMO = classSMO(dataMat, labelMat, C, tol)
mainRoutine(dataMat, C, maxIter)
plotData(objectSMO.dataMat, objectSMO.labelMat)
theta = computeTheta(objectSMO)
plotHyperplaneLinear(objectSMO.dataMat, objectSMO.labelMat, theta, objectSMO)
checkClassifierErrorSample(objectSMO)
高斯核函数将二维输入空间映射到高维特征空间,不知道在二维上如何绘制超平面的轮廓,所以这里就用checkClassifierErrorSample(objectSMO)函数打印输出误分类点的个数,根据结果可看到高斯核函数解决了非线性分类的问题。
todo
,这两天导师干活逼迫的紧,这部分待更新……
后记
真的,推导并且实现一遍,发现其实SVM真的不难,也许是我们觉得它比较难,导致它变难了吧。
参考资料:
- SVM,SMO论文
- 书《机器学习实战》《机器学习》
- KKT条件与拉格朗日对偶性
- 核技巧
- 知乎—机器学习有很多关于核函数的说法,核函数的定义和作用是什么?
- SMO算法
- SMO算法剖析