假设我们有两组数据都是只有两个特征:
这两个特征分别成为一个坐标轴,那么每一个样本在图像中表现出来的都是坐标系中的一个点
- 支持向量机 想要做的就是找到一个超平面来进行空间的划分。
超平面
如果这个分类不能在这个维度上用平面或者直线进行划分,那么就用高一维度的空间进行划分。
比如:
我们不可能直接在一维用一个点将两种颜色的小球划分开
但是在二维
我们很轻松的可以用一条直线将两者分开
这条直线就可以称为是超平面。如果读者认为将一条线称为是超平面有点难以理解。
那么一个二维的用一条直线不能很好划分的问题,可以在三维用一个平面来划分。这个平面也是一个超平面。
支持向量机的基本型
由以上的描述我们可以看出来,一切线性不可分问题都可以通过提高维度而变得线性可分
那么现在我们开始谈一谈线性的问题
- 对于一个二维的线性问题我们可以写成:
形式 - 对于一个三维的线性问题我们可以写成:
形式 - .....
我们希望用一个简化的形式来表现多维的线性方程
因此用矩阵的方式来完成,因此:
相信大家高中都有学过,点到直线距离公式:
这里更新为:
这里的 在线的一侧为正,在线的另一侧为负
这样就同时表现了相对于直线的方向和距离
一条直线将空间分成了两类,因此类别 1 或者 -1 代表第i个样本是这一类还是另一类,至于为什么是1和-1 而不是 像以前一样的 0和-1 ,这是为了表达上的简洁,后期会说明为什么这样设定表达上会简洁
我们设定在这一面的类别为1,这一面的类别为-1, 即:
但并不是说只有一条直线可以将样本进行划分,就像开头的第一张图一样,会有两个支持面,而两个支持面正中央的线是最优的划分
假设 两个支持面之间的距离为2d, 即最优面到支持面之间的距离为d
那么,上面的式子可以进一步改写为:
也就是:
左右两边同除d
得到:
其中:
我们可以注意到,对于来说将和 用 和 对于原方程完全没有影响
因此后面所指的 和 指的是 和
方程式为:
样本满足条件为:
这个时候就表现出了 分类时候用 -1 和 1 来表示的优势了
因为以上样本满足的条件可以写成为:
可以注意到以上的操作我们都是只是在一个确定方向上寻找最优面
但是我们怎样找到一个最优的方向呢
我们的想法是:在这个方向上的支持面距离越大,说明在这个方向上两个样本分得更"开",也就说明这个方向的划分更优
因此使得两个支持面距离最大的方向就是最优方向
也就是说使得2d最大的方向就是最优方向
d是同方向上的最优面到支持面的距离,因此:
而由于我们以上的更新操作,可以看出,支持面上的样本满足:
因此
我们想要 2d 最大,也就是想要 d 最大
也就是||w|| 最小
相信大家对于让某一个向量的模最小的处理方式已经不陌生了,一般都是做平方处理,同时,为便于求导,除以2
也就是支持向量机的基本型:
在满足的情况下(),尽力让达到最小
支持向量机的基本型是一个凸二次规划问题
凸二次规划问题
凸函数:
如果一个函数上任意两个点之间的连线上的中间点高于对应横坐标的函数上的点就称为凸函数:
数学表达:
凸二次规划问题:
用于表示一类问题:约束条件为线性不等式,求一个凸二次函数的最小值问题
一般形式为:
满足的情况下
求的最小值
对于这种有约束条件的求极值问题:
有一种很好的解决手段:拉格朗日乘子法
拉格朗日乘子法与KKT条件
拉格朗日乘子法是一种处理有约束条件的极值问题非常常用的手段
等式约束条件:
约束条件:
要求的极值:
那么
对于各个参数进行求导并让结果等于0:
....
且
所有条件这些条件最后能够求出结果
不等式约束条件:
这种情况就是我们在这里需要解决的情况
我们有很多的不等式约束
也可以有等式约束:
求 g(x) 的极值
解决方法还是使用拉格朗日乘子法:
这个时候:最优解满足KKT条件
KKT条件:
对 x 求导为0
并且有 的已知条件
然后正常使用拉格朗日乘子法就可以解决问题
对偶问题:
对于不等式约束条件的处理方式正常来说,以上就可以解决问题了,但是有的时候,由于方程难解,就会利用对偶的性质
由
易知:
因为:
对于满足条件的x来说 且 因此 要想达到最大,只有让
而, 因此
而不要忘了我们的目标是要求的最小值,即
也就是
它的对偶性质在于 只要 满足KKT条件:
继续计算
有了以上的知识储备
我们回顾一下,支持向量机的基本型是:
在满足的情况下(),尽力让达到最小
在满足KKT条件的情况下,可以使用拉格朗日乘子法 可以使用对偶性质:
也就是说,我们可以从求
变成求:
先开始求:
将w和b消掉,因数只有:
(*)
并满足:
然后求:
由KKT条件可知:
还有已知条件:
因此:
可以分为 和 两种情况
但显然,如果 代表第i个样本没有起到约束作用
因此
由
设
那么
也就是说对于 的样本来说,
由此可见,只有在支持面上的样本才起到了约束作用
修正
以上的一切推导我们都基于了一个想法:一切线性可分问题都是百分百的线性可分,但是大部分的实际情况,即使这个问题可以用线性的方式分类,即使我们找到了非常好的分割函数,我们仍然避免不了会有错误分类的样本,因此我们推导不得不对于这种情况表现出一定的容忍度。
因此,要对于以上的推导进行修正
引入参数:
- 惩罚参数 : 用于表现,我们对于错误分类的样本的重视程度(惩罚力度)
- 松弛变量 : 用于表现,错误分类的样本的偏离距离
也就是支持向量机的基本型更新为:
在满足和 的情况下(),尽力让达到最小
那么:
同样的更新方法:
分别对进行求导:
可以得到:
(*)
并满足:
但是不同于之前要求 这里要求
根据KKT条件:
对于第i个样本:
如果 说明这个样本对于f(x) 没有任何影响
如果 说明这个样本是支持向量
根据:
可知:
如果 因此 样本恰在最大分隔边界上
如果 :
- 若 最大间隔内部
- 若 错误分类
SMO算法
前面已经介绍过了什么是二次规划问题
因此我们可以看出,我们的后期推导出来的形式:
(*)
并满足:
这种形式,仍然是一个二次规划问题
而SMO算法是一种解决二次优化问题的常用手段,基本思想是:先认为有两个参数是变量,并认为其他参数为常数值,对这两个变量做更新
下面我们就要用SMO算法解决问题:
由: 和 为更新之后的结果
可以看出来,如果我们只认为一个参数为变量,其余都为常量的话,的值会被直接推导出来,而非给出一种关系
因此,我们会认为有两个变量和
由:
代表常数
由上面的:
另 和 分别为1 和-1
则
因此从这里计算得到
本身有的限制
因此下界为:
上界:
因此取值范围如下:
同理:
如果
取值范围如下
因此:L 表示上界 H 表示下界
(*)
并满足:
(常数)
常数
常数
设
设(常数)
所以:
由:(*)
将 带入,并对 求偏导
为了以下的推导方便,以上选择的i和j,用o和r代替, 和 为旧参数, 和 为新参数
将以上带入的D B 都代回去,设
获得更新公式:
因为 满足 这是之前的推导
因此
根据 可以获得
因为 和
可见如果超出这个范围的 对应的样本是无效样本
根据前面的 和
两边同时乘 得到:
由于: 没有经历过更新,可以从原式中得到:
b得到更新
同理将换成o也可以获得更新
但是正如上面提到过的,o样本 和 i样本 可能无用
如果两者都可用: 两者更新结果一致
如果只有一者可用: 取可用者更新
如果两者都不可用: 取两个更新结果的中间值
以上就是支持向量机的过程
有了这个过程,以下代码相信不难理解,但是是一个简易版本,为了提升运算效率,会引入核方法。
梳理
介绍了这么多,现在我们梳理一下流程:
-
判断当前这个训练数据的参数是否需要更新:
- 如果 那么根据前面的推导,有结论这个样本在最大分隔线上,但是这样的情况下, 说明方向的划分存在问题,需要更新
- 如果 说明这个样本为非支持向量,但是 , 说明这个样本对训练结果有影响,显然不对,需要更新
一旦判断这个参数需要更新:
开始使用smo,因此需要另一个参数一起进行更新。
别忘了,我们后期更新后的参数需要上下界的限制:
- 因此先用旧的参数计算上下界:
- 然后用更新公式对其中一个参数进行更新:
- 将该更新后参数用L H 剪切
- 用两个参数之间的关系对另一个参数进行更新:
- 判断这两个参数是否符合在0 到C 之间,符合的可以用来更新b,如果两个都不符合就取两者各自计算的b的平均值
就这样对每一个参数都检查一遍,只要存在参数不符合条件经历过,就再重新检查,知道没有参数不符合条件
这个时候对w进行更新,然后用更新后的w重复以上步骤,就这样不停迭代,直到达到预先设置好的条件
完成训练
svm.py
import random
from numpy import *
import draw
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])])
return dataMat, labelMat
def selectJrand(i, m): # 获得0到m之间的随机数
j = i
while j == i:
j = int(random.uniform(0, m))
return j
def clipAlpha(aj, H, L): # 剪切获得的更新后的aj
if aj > H:
aj = H
elif L > aj:
aj = L
return aj
def smoSimple(dataMatIn, classLabels, C, toler, maxIter):
dataMatrix = mat(dataMatIn)
labelMat = mat(classLabels).T
b = 0.0
w = mat(zeros((1, 2)))
m, n = shape(dataMatrix)
alphas = mat(zeros((m, 1))).T
iter = 0
while iter < maxIter: # 限制一个最多迭代次数
alphaPairsChanged = 0
for i in range(m):
# print(dot(multiply(alphas, labelMat), dot(dataMatrix, dataMatrix[i, :].T)))
# multiply 作用是可以获得对应项相乘得到的矩阵
fXi = float(dot(multiply(alphas, labelMat), dot(dataMatrix, dataMatrix[i, :].T))) + b # f(x_i) 项
Ei = fXi - float(labelMat[0, i]) # 误差
# if 中两种情况需要更新参数
# 第一种:\alpha_i < C 由文中可知 代表 样本在最大分割线上,可是即使如此还是不能满足距离中间分割线1的距离
# 第二种:\alpha_i > 0 由文中可知 代表 样本对f(x)有影响,可是这个样本已经在距离中间分割线超过1了,非支持向量不应该对f(x)有影响
if (labelMat[0, i]*Ei < -toler and alphas[0, i] < C) or (labelMat[0, i]*Ei > toler and alphas[0, i] > 0):
j = selectJrand(i, m) # 随机选取另一个自变量
fXj = float(dot(multiply(alphas, labelMat), dot(dataMatrix, dataMatrix[j, :].T))) + b
Ej = fXj - float(labelMat[0, j])
alphaIold = alphas[0, i].copy()
alphaJold = alphas[0, j].copy()
# 即为文章中的限制上界和下界
if labelMat[0, i] != labelMat[0, j]:
L = max(0, alphas[0, j])
H = min(C, C + alphas[0, j] - alphas[0, i])
else:
L = max(0, alphas[0, j] + alphas[0, i] - C)
H = min(C, alphas[0, j] + alphas[0, i])
if L == H:
print("L==H")
continue
eta = 2.0 * dataMatrix[i, :] * dataMatrix[j, :].T -\
dataMatrix[i, :] * dataMatrix[i, :].T -\
dataMatrix[j, :] * dataMatrix[j, :].T
if eta >= 0:
print("eta >= 0")
continue
alphas[0, j] -= labelMat[0, j] * (Ei - Ej)/eta # 更新公式
alphas[0, j] = clipAlpha(alphas[0, j], H, L) # 由上下界剪切得到的公式
if abs(alphas[0, j] - alphaJold) < 0.00001: # 几乎没有实质上的更新
print("j not moving enough")
continue
alphas[0, i] += labelMat[0, j]*labelMat[0, i]*(alphaJold - alphas[0, j]) # 由更新后的\alpha_j 对\alpha_i 进行更新
b1 = b - Ei - labelMat[0, i] * (alphas[0, i] - alphaIold) *\
dot(dataMatrix[i, :], dataMatrix[i, :].T) -\
labelMat[0, j] * (alphas[0, j] - alphaJold) *\
dot(dataMatrix[i, :], dataMatrix[j, :].T) # b 的更新公式
b2 = b - Ej - labelMat[0, i] * (alphas[0, i] - alphaIold) * \
dot(dataMatrix[i, :], dataMatrix[j, :].T) -\
labelMat[0, j] * (alphas[0, j] - alphaJold) *\
dot(dataMatrix[j, :], dataMatrix[j, :].T)
# if 0 < alphas[i] and C > alphas[i]:
# 分情况来判断取哪一个b
if 0 < alphas[0, i] < C:
b = b1
elif 0 < alphas[0, j] < C:
b = b2
else:
b = (b1 + b2)/2.0
alphaPairsChanged += 1
print("iter : %d i: %d, pairs changed %d" % (iter, i, alphaPairsChanged))
if alphaPairsChanged == 0: # 只有在每一个样本都做过变量且都不需要进行更新了,才会进行下一次的迭代
iter += 1
else:
iter = 0
print("iteration number: %d" % iter)
w = dot(multiply(alphas, labelMat), dataMatrix)
return w, b
data, label = loadDataSet("testSet.txt")
draw.draw_scatter_figure(data)
w, b = smoSimple(data, label, 0.6, 0.001, 40)
print(b)
print(w)
draw.draw_line_figure(w, b[0, 0])
draw.py
from pylab import *
import matplotlib.pyplot as plt
def draw_scatter_figure(data):
newData = mat(data)
X = newData[:, 0].A
Y = newData[:, 1].A
plt.scatter(X, Y)
def draw_line_figure(w, b):
X = linspace(0, 12, 20, endpoint=True)
Y = (w[0, 0] * X + b)/w[0, 1]
print(X)
print(Y)
plt.plot(X, Y)
plt.show()
效果图:
参考:
- https://cuijiahua.com/blog/2017/11/ml_8_svm_1.html
- 《机器学习》by 周志华
- 及一系列数学博客