目录
一、基于最大间隔分隔数据
1.1 线性模型
在二维空间上,两类点被一条直线完全分开叫做线性可分。如下图,在二维坐标下,样本空间中找到直线, 将不同类别的样本分开。
上述将数据集分隔开来的直线称为分隔超平面,即。
线性可分的数学定义:
和
是
维欧氏空间中的两个点集。如果存在
维向量
和实数
,使得所有属于
的点
都有
,而对于所有属于
的点
则有
,则我们称
和
线性可分。
1.2 超平面
由于数据点都在二维平面上,所以此时分隔超平面就只是一条直线。但是,如果所给的数据 集是三维的,那么此时用来分隔数据的就是一个平面。显而易见,更高维的情况可以依此类推。如果数据集是1000维的,那么就需要一个999维的某某对象来对数据进行分隔。当数据集是N维时,需要一个N-1维的某某对象来对数据进行分隔。N-1维的该对象被称为超平面(hyperplane),也就是分类的决策边界。 分布在超平面一侧的所有数据都属于某个类别,而分布在另一侧的所有数据则属于另一个类别。
![](https://i-blog.csdnimg.cn/blog_migrate/c91adabc5fdae4dd7147d9dadeebcc9a.png)
问题:将训练样本分开的超平面可能有很多, 哪一个好?
从二维扩展到多维空间中时,将 和
完全正确地划分开的
就成了一个超平面。为了使这个超平面更具鲁棒性,我们会去找最佳超平面,以最大间隔把两类样本分开的超平面,也称之为最大间隔超平面。
如上图,有五条直线,它们都能将数据分隔开,但是其中哪一条最好呢?我们希望找到离分隔超平面最近的点,确保它们离分隔面的距离尽可能远。这里点到分隔面的距离被称为间隔(margin)。我们需要的是间隔尽可能地大,这是因为如果犯错或者在有限数据上训练分类器的话,分类器尽可能健壮。所以,应选择”正中间”的那条直线 , 容忍性好, 鲁棒性高, 泛化能力最强,选择最大化决策边界的边缘。
1.3 支持向量
如下图,支持向量(support vector)就是离分隔超平面最近的那些点。
超平面方程:
1.4 支持向量机
支持向量机(Support Vector Machines, SVM)是一种二分类模型,它的基本模型是定义在特征空间上的间隔最大的线性分类器;SVM还包括核函数,这使它成为实质上的非线性分类器。SVM有很多实现,下面介绍的是序列最小优化(Sequential Minimal Optimization,SMO)算法。 在此之后,将介绍如何使用核函数(kernel)的方式将SVM扩展到更多数据集上。
支持向量机 |
---|
优点:泛化错误率低,计算开销不大,结果易解释。 缺点:对参数调节和核函数的选择敏感,原始分类器不加修改仅适用于处理二类问题。 适用数据类型:数值型和标称型数据 |
SVM分为线性可区分和线性不可分:
当数据在输入空间上线性可分时就将其称之为线性SVM,而线性不可分,说得形象一些,就是“你中有我,或者我中有你”,我们找不到一个超平面,恰好能把两类。往往前者较为简单,而后者则需要运用一些核技巧将输入空间映射到特征空间来得到特征向量,然后进行分类学习。
二、寻找最大间隔
首先,我们来了解一下超平面:
分隔超平面的形式:,其中
,当n = 2时,这就是在二维坐标上,其中
为超平面的法向量,
为偏置值。
从数学表示的层面上来看,实数域维空间中的超平面定义如下:
超平面有以下几个性质:
性质1:法向量和偏置项以任意相同的倍数放缩,新表达式描述的仍然是原来的超平面。假设放缩比例为,令
后得到的超平面表达式为
,显然,这个表达式表示的仍然是原来的超平面。举个浅显的例子,直线
与直线
是同一条直线,虽然他们的系数之比为2。
性质2:如下图,要计算点 X到分隔超平面的距离
就必须给出点到分隔面的法线或垂线的长度,点X到超平面的距离为:
其中 表示的是,所有元素的平方和的开平方。
性质3:超平面将维空间划分为3部分,分为是:①点
在超平面里
=0;②点
在超平面的“上方”
;③点
在超平面的“下方”
。如下图所示:需要注意的是,“上方”和“下方”并不是方位上的超平面上下方,而是以超平面的法向量
的指向为准,
指向的方向称为“上方”,反之则为“下方”。
![](https://i-blog.csdnimg.cn/blog_migrate/ca56e230d5514f09e463d332ab58ace9.png)
SVM中,分隔超平面是一个能够将正负样本恰好隔开的超平面,并且使得正样本在分隔超平面“上方”,负样本在分隔超平面”下方“。这就意味着,分隔超平面中的
需要满足以下条件:
其中为正样本点,
为负样本点,而正负样本对应的标记值为
,所以这两个条件可以改写成下面两个式子:
于是,对于线性可分的样本集:
,其中,
,
分类正确的超平面需要满足的条件为:
令,我们可以得到更加紧凑的表达:
"分类正确"
在SVM中,被称为样本点到超平面的函数间隔。
因此,我们可以得出结论,对于给定的线性可分的样本集合,必然存在分隔超平面可以将正负样本分开,该分类正确的超平面需要满足的条件为:样本点到超平面的函数间隔大于零。
在数学上,我们可以得到以下式子等效:
所以,为了方便后续的计算,简化方程为:
令 和
位于决策边界上,标签分别为正、负的两个样本,考虑
到分类线的距离为:
因此,分类间隔为:
最大化间隔也就是寻找参数和
, 使得
最大,即:
通过数学知识可知,求的最大值,就是求
的最小值,求最大值我们利用求导获取极值来解题,为了简化计算,因此问题可以等价于求
的最小值:
到了这里,就得出了求解最大间隔超平面的最终表达式。
现在,让我们来看一下简单的间隔最大化样例计算:
如上,D表示了是三个二维数据,第一列表示x坐标,第二类列表示y坐标,第三列表示样本类型,+1表示正样本,-1表示负样本,画出了这三个点的二维坐标图。
求解最大间隔超平面公式 : ①
②
我们三个坐标带入公式①②,因为是二维平面,所以,所以:
化简,得:
其中,,和
和
,可以看成是,在两直线的约束区间内,取满足原点为中心的圆的最小半径,如下图:
因为圆心,在约束区间外,所以当圆与约束区间相切的时候,就可以获得解, ③
把③带入,原①②式子:
所以,求得最大间隔超平面为:,如上图,直线B;
如上图,直线A,
如上图,直线C。
三、拉格朗日乘子法与对偶问题
我们想要求解式得到最大间隔划分超平面对应的模型:
其中是模型参数,这里我们使用拉格朗日乘子法得到其对偶问题,从而高效的求出结果,下面就看一下什么是拉格朗日乘子法和对偶问题。
拉格朗日乘子法是一种寻找多元函数在一组约束下的极值的方法,通过引入拉格朗日乘子,可将有d个变量与k个约束条件的优化问题转换为具有d+k个变量的无约束优化问题。
3.1 对偶问题:等式约束
给定一个目标函数 f : Rn→R,希望找到x∈Rn ,在满足约束条件g(x)=0的前提下,使得f(x)有最小值。该约束优化问题记为:
可建立拉格朗日函数:
其中 λ 称为拉格朗日乘数。因此,可将原本的约束优化问题转换成等价的无约束优化问题:
分别对待求解参数求偏导,可得:
一般联立方程组可以得到相应的解。
3.2 不等式约束的KKT条件
将约束等式 g(x)=0 推广为不等式 g(x)≤0。这个约束优化问题可改为:
同理,其拉格朗日函数为:
拉格朗日乘子法的几何意义即在等式g(x)=0或在不等式约束g(x)≤0下最小化目标函数f(x),如下图:
①当 g(x)<0 时:对f(x)求极值相当于闭区间求极值,最值点即为极值点,令λ=0,直接对f求梯度即可得到极值。
②当 g(x) = 0 时:说明极值点在边界取到,即g(x)<0内的点值都大于边界,梯度的定义是向函数值增加最快的方向,所以f的梯度与g的梯度相反,从而存在常数λ>0,使得:
其约束范围为不等式,因此可等价转化成Karush-Kuhn-Tucker (KKT)条件:
在此基础上,通过优化方式(如二次规划或SMO)求解其最优解。
通俗意义理解KKT条件的话就是目标函数在约束条件下取得极值的充要条件,也就是目标函数在约束条件下取得极值时对应的x,λ必须满足KKT条件,反之亦然。
3.3 最大间隔问题的拉格朗日乘法
我们先看一下支持向量机的目标函数与约束函数:
第一步:对每条约束添加拉格朗日乘子αi≥0,则该问题的拉格朗日函数可写为:
第二步:令对w和b的偏导为零:
第三步:w, b回代到第一步:
第四步:从而得到对偶问题
第五步:此式为关于的极大值求解,当求出解
之后,求出
,有
即可得到w, b的求解通过任一支持向量即可求出,因为在支持向量处,满足现在我们已经求出w,xi,yi也已知,所以也可以顺利求出b,这样参数w,b全部求出,我们的最优超平面也就被w和b所限定。根据多约束推广的KKT条件,推出支持向量机优化问题的KKT条件:
对于不在最大边缘边界上的点:;
时,必有
,即支持向量。
支持向量机解的稀疏性: 训练完成后, 大部分的训练样本都 不需保留, 最终模型仅与支持向量有关。
四、SMO算法
1996年,John Platt发布了一个称为SMO的强大算法,用于训练SVM。SMO表示序列最小优 化(Sequential Minimal Optimization)。Platt的SMO算法是将大优化问题分解为多个小优化问题来 求解的。这些小优化问题往往很容易求解,并且对它们进行顺序求解的结果与将它们作为整体来 求解的结果是完全一致的。在结果完全相同的同时,SMO算法的求解时间短很多。
SMO算法的目标是求出一系列α和b,一旦求出了这些α,就很容易计算出权重向量w 并得到分隔超平面。
SMO算法的工作原理是:每次循环中选择两个α进行优化处理。一旦找到一对合适的α,那么就增大其中一个同时减小另一个。这里所谓的“合适”就是指两个α必须要符合 一定的条件,条件之一就是这两个α必须要在间隔边界之外,而其第二个条件则是这两个α还没有进行过区间化处理或者不在边界上。
假设最优解为 :
可得:
得到分类平面:
4.1 小规模数据集
数据集,如下:
from numpy import *
import matplotlib.pyplot as plt
# 读取数据
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 showData():
dataMat, labelMat = loadDataSet('testSet.txt') # 加载数据集,标签
dataArr = array(dataMat) # 转换成numPy的数组
n = shape(dataArr)[0] # 获取数据总数
xcord1 = []; ycord1 = [] # 存放正样本
xcord2 = []; ycord2 = [] # 存放负样本
for i in range(n): # 依据数据集的标签来对数据进行分类
if int(labelMat[i]) == 1: # 数据的标签为1,表示为正样本
xcord1.append(dataArr[i, 0]); ycord1.append(dataArr[i, 1])
else: # 否则,若数据的标签不为1,表示为负样本
xcord2.append(dataArr[i, 0]); ycord2.append(dataArr[i, 1])
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(xcord1, ycord1, s=15, c='blue') # 绘制正样本
ax.scatter(xcord2, ycord2, s=15, c='red', marker='s') # 绘制负样本
plt.title('DateSet') # 标题
plt.xlabel('X1'); plt.ylabel('X2') # x,y轴的标签
plt.show()
运行结果如下,从肉眼既可以看出,我们很容易划分:
4.2 应用简化版 SMO 算法处理小规模数据集
SMO 算法中的辅助函数:
from numpy import *
import matplotlib.pyplot as plt
# 读取数据
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
# 随机选择alpha
def selectJrand(i, m):
j = i # 选择一个不等于i的j
while (j == i): # 只要函数值不等于输入值i,函数就会进行随机选择
j = int(random.uniform(0, m))
return j
# 修剪alpha
def clipAlpha(aj, H, L): # 用于调整大于H或小于L的alpha值
if aj > H:
aj = H
if L > aj:
aj = L
return aj
简化版 SMO 算法
# 伪代码
创建一个alpha向量并将其初始化为0向量
当迭代次数小于最大迭代次数时(外循环):
对数据集中的每个数据向量(内循环):
如果该数据向量可以被优化:
随机选择另外一个数据向量
同时优化这两个向量
如果两个向量都不能被优化,退出内循环
如果所有向量都没被优化,增加迭代数目,继续下一次循环
# 简化版SMO算法
def smoSimple(dataMatIn, classLabels, C, toler, maxIter):
dataMatrix = mat(dataMatIn) # 数据矩阵dataMatIn转换为numpy的mat存储
labelMat = mat(classLabels).transpose() # 数据标签classLabels转换为numpy的mat存储
b = 0; m, n = shape(dataMatrix) # 初始化b参数,统计dataMatrix的维度m*n
alphas = mat(zeros((m, 1))) # 初始化alpha参数为0
iter = 0 # 初始化迭代次数0
while (iter < maxIter): # matIter表示最多迭代次数,iter变量达到输入值maxIter时,函数结束运行并退出
alphaPairsChanged = 0 # 变量alphaPairsChanged用于记录alpha是否已经进行优化
for i in range(m):
# 步骤1:计算误差Ei
fXi = float(multiply(alphas, labelMat).T * (dataMatrix * dataMatrix[i, :].T)) + b
Ei = fXi - float(labelMat[i])
# 优化alpha,同时设定容错率
if ((labelMat[i] * Ei < -toler) and (alphas[i] < C)) or ((labelMat[i] * Ei > toler) and (alphas[i] > 0)):
j = selectJrand(i, m) # 随机选择另一个与alpha_i成对优化的alpha_j
# 步骤1:计算误差Ej
fXj = float(multiply(alphas, labelMat).T * (dataMatrix * dataMatrix[j, :].T)) + b
Ej = fXj - float(labelMat[j])
# 保存更新前的aplpha值,使用拷贝
alphaIold = alphas[i].copy(); alphaJold = alphas[j].copy()
# 步骤2:计算上下界L和H
if (labelMat[i] != labelMat[j]):
L = max(0, alphas[j] - alphas[i])
H = min(C, C + alphas[j] - alphas[i])
else:
L = max(0, alphas[j] + alphas[i] - C)
H = min(C, alphas[j] + alphas[i])
if L == H:
print("L==H")
continue
# 步骤3:计算eta
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
# 步骤4:更新alpha_j
alphas[j] -= labelMat[j] * (Ei - Ej) / eta
# 步骤5:修剪alpha_j
alphas[j] = clipAlpha(alphas[j], H, L)
if (abs(alphas[j] - alphaJold) < 0.00001):
print("j not moving enough")
continue
# 步骤6:更新alpha_i
alphas[i] += labelMat[j] * labelMat[i] * (alphaJold - alphas[j]) # 按与alpha_j相同的方法更新alpha_i
# 步骤7:更新b_1和b_2,更新方向相反
b1 = b - Ei - labelMat[i] * (alphas[i] - alphaIold) * dataMatrix[i, :] * dataMatrix[i, :].T - labelMat[j] * (alphas[j] - alphaJold) * dataMatrix[i, :] * dataMatrix[j, :].T
b2 = b - Ej - labelMat[i] * (alphas[i] - alphaIold) * dataMatrix[i, :] * dataMatrix[j, :].T - labelMat[j] * (alphas[j] - alphaJold) * dataMatrix[j, :] * dataMatrix[j, :].T
# 步骤8:根据b_1和b_2更新b
if (0 < alphas[i]) and (C > alphas[i]):
b = b1
elif (0 < alphas[j]) and (C > alphas[j]):
b = b2
else:
b = (b1 + b2) / 2.0
# 统计优化次数
alphaPairsChanged += 1
print("第%d次迭代 样本:%d, alpha优化次数:%d" % (iter, i, alphaPairsChanged))
# 更新迭代次数
if (alphaPairsChanged == 0):
iter += 1
else:
iter = 0
print("迭代次数: %d" % iter)
return b, alphas
# 计算w值
def calcWs(alphas, dataArr, classLabels):
X = mat(dataArr);
labelMat = mat(classLabels).transpose()
m, n = shape(X)
w = zeros((n, 1))
for i in range(m):
w += multiply(alphas[i] * labelMat[i], X[i, :].T)
return w
# 绘制数据集以及划分直线
def showDataLine(w, b):
x, y = loadDataSet('testSet.txt')
xarr = array(x)
n = shape(x)[0]
x1 = []; y1 = []
x2 = []; y2 = []
for i in arange(n):
if int(y[i]) == 1:
x1.append(xarr[i, 0]);
y1.append(xarr[i, 1])
else:
x2.append(xarr[i, 0]);
y2.append(xarr[i, 1])
plt.scatter(x1, y1, s=30, c='r', marker='s')
plt.scatter(x2, y2, s=30, c='g')
# 画出 SVM 分类直线
xx = arange(0, 10, 0.1)
# 由分类直线 weights[0] * xx + weights[1] * yy1 + b = 0 易得下式
yy1 = (-w[0] * xx - b) / w[1]
# 由分类直线 weights[0] * xx + weights[1] * yy2 + b + 1 = 0 易得下式
yy2 = (-w[0] * xx - b - 1) / w[1]
# 由分类直线 weights[0] * xx + weights[1] * yy3 + b - 1 = 0 易得下式
yy3 = (-w[0] * xx - b + 1) / w[1]
plt.plot(xx, yy1.T)
plt.plot(xx, yy2.T)
plt.plot(xx, yy3.T)
# 画出支持向量点
for i in range(n):
if alphas[i] > 0.0:
plt.scatter(xarr[i, 0], xarr[i, 1], s=150, c='none', alpha=0.7, linewidth=1.5, edgecolor='red')
plt.xlim((-2, 12))
plt.ylim((-8, 6))
plt.show()
运行结果:
# 主函数
if __name__ == '__main__':
dataMat, labelMat = loadDataSet('testSet.txt')
b, alphas = smoSimple(dataMat, labelMat, 0.6, 0.001, 40)
w = calcWs(alphas, array(dataMat), labelMat)
showDataLine(w, b)
运行了多次程序并取其平均时间,虽然结果看起来并不是太差,但这只是一个仅有100个点的小规模数据集而已。在更大的数据集上,收敛时间会变得更长。
4.3、利用完整Platt SMO算法加速优化
上面我们实现了在100个点组成的小规模数据集上的简化版SMO算法,运行效果是可行的,但是在更大 的数据集上的运行速度就会变慢。下面我们就讨论完整版的 Platt SMO算法。在这两个版本中,实现alpha的更改和代数运算的优化环节一模一样。在优化过程中,唯一的不同就是选择alpha的方式。完整版的Platt SMO算法应用了一些能够提速的启发方法。
完整Platt SMO算法原理
Platt SMO算法是通过一个外循环来选择第一个alpha值的,并且其选择过程会在两种方式之 间进行交替:一种方式是在所有数据集上进行单遍扫描,另一种方式则是在非边界alpha中实现单遍扫描。而所谓非边界alpha指的就是那些不等于边界0或C的alpha值。对整个数据集的扫描相当 容易,而实现非边界alpha值的扫描时,首先需要建立这些alpha值的列表,然后再对这个表进行 遍历。同时,该步骤会跳过那些已知的不会改变的alpha值。
在选择第一个alpha值后,算法会通过一个内循环来选择第二个alpha值。在优化过程中,会通过最大化步长的方式来获得第二个alpha值。在简化版SMO算法中,选择j之后计算错误率Ej。但在这里,则是建立一个全局的缓存用于保存误差值,并从中选择使得步长或者说 Ei-Ej最大的alpha值。
# 读取数据
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
# 随机选择alpha
def selectJrand(i, m):
j = i # 选择一个不等于i的j
while (j == i): # 只要函数值不等于输入值i,函数就会进行随机选择
j = int(random.uniform(0, m))
return j
# 修剪alpha
def clipAlpha(aj, H, L): # 用于调整大于H或小于L的alpha值
if aj > H:
aj = H
if L > aj:
aj = L
return aj
# 类
class optStruct:
def __init__(self, dataMatIn, classLabels, C, toler, kTup): # 使用参数初始化结构
self.X = dataMatIn # 数据矩阵
self.labelMat = classLabels # 数据标签
self.C = C # 松弛变量
self.tol = toler # 容错率
self.m = shape(dataMatIn)[0] # 数据矩阵行数m
self.alphas = mat(zeros((self.m, 1))) # 根据矩阵行数初始化alpha参数为0
self.b = 0 # 初始化b参数为0
self.eCache = mat(zeros((self.m, 2))) # 第一列是有效标志
# 计算误差
def calcEk(oS, k):
fXk = float(multiply(oS.alphas, oS.labelMat).T * (oS.X*oS.X[k,:].T)) + oS.b
Ek = fXk - float(oS.labelMat[k])
return Ek
# 内循环启发方式
def selectJ(i, oS, Ei):
maxK = -1; maxDeltaE = 0; Ej = 0 # 初始化
oS.eCache[i] = [1, Ei] # 选择给出最大增量E的alpha
validEcacheList = nonzero(oS.eCache[:, 0].A)[0]
if (len(validEcacheList)) > 1:
for k in validEcacheList: # 循环使用有效的Ecache值并找到使delta E最大化的值
if k == i: continue # 如果k对于i,不计算i
Ek = calcEk(oS, k) # 计算Ek的值
deltaE = abs(Ei - Ek) # 计算|Ei-Ek|
if (deltaE > maxDeltaE): # 找到maxDeltaE
maxK = k; maxDeltaE = deltaE; Ej = Ek
return maxK, Ej
else: # 在这种情况下(第一次),没有任何有效的eCache值
j = selectJrand(i, oS.m) # 随机选择alpha_j的索引值
Ej = calcEk(oS, j)
return j, Ej
# 计算Ek并更新误差缓存
def updateEk(oS, k): # 任何alpha更改后,更新缓存中的新值
Ek = calcEk(oS, k)
oS.eCache[k] = [1, Ek]
# 优化的SMO算法
def innerL(i, oS):
Ei = calcEk(oS, i) # 计算误差Ei
if ((oS.labelMat[i]*Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or ((oS.labelMat[i]*Ei > oS.tol) and (oS.alphas[i] > 0)):
# 使用内循环启发方式选择alpha_j并计算Ej
j,Ej = selectJ(i, oS, Ei)
# 保存更新前的aplpha值,拷贝
alphaIold = oS.alphas[i].copy(); alphaJold = oS.alphas[j].copy()
# 步骤2:计算上下界L和H
if (oS.labelMat[i] != oS.labelMat[j]):
L = max(0, oS.alphas[j] - oS.alphas[i])
H = min(oS.C, oS.C + oS.alphas[j] - oS.alphas[i])
else:
L = max(0, oS.alphas[j] + oS.alphas[i] - oS.C)
H = min(oS.C, oS.alphas[j] + oS.alphas[i])
if L==H: print("L==H"); return 0
# 步骤3:计算eta
eta = 2.0 * oS.X[i,:]*oS.X[j,:].T - oS.X[i,:]*oS.X[i,:].T - oS.X[j,:]*oS.X[j,:].T
if eta >= 0: print("eta>=0"); return 0
# 步骤4:更新alpha_j
oS.alphas[j] -= oS.labelMat[j]*(Ei - Ej)/eta
# 步骤5:修剪alpha_j
oS.alphas[j] = clipAlpha(oS.alphas[j],H,L)
# 更新Ej至误差缓存
updateEk(oS, j)
if (abs(oS.alphas[j] - alphaJold) < 0.00001): print("j not moving enough"); return 0
# 步骤6:更新alpha_i
oS.alphas[i] += oS.labelMat[j]*oS.labelMat[i]*(alphaJold - oS.alphas[j])
# 更新Ei至误差缓存
updateEk(oS, i)
# 步骤7:更新b_1和b_2
b1 = oS.b - Ei - oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.X[i,:]*oS.X[i,:].T - oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.X[i,:]*oS.X[j,:].T
b2 = oS.b - Ej - oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.X[i,:]*oS.X[j,:].T - oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.X[j,:]*oS.X[j,:].T
# 步骤8:根据b_1和b_2更新b
if (0 < oS.alphas[i]) and (oS.C > oS.alphas[i]):
oS.b = b1
elif (0 < oS.alphas[j]) and (oS.C > oS.alphas[j]):
oS.b = b2
else:
oS.b = (b1 + b2)/2.0
return 1
else:
return 0
# 完整的线性SMO算法
def smoP(dataMatIn, classLabels, C, toler, maxIter,kTup=('lin', 0)):
oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(), C, toler, kTup)# 初始化
iter = 0 # 初始化迭代次数为0
entireSet = True; alphaPairsChanged = 0
while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)): # 超过最大迭代次数或者遍历整个数据集都alpha也没有更新,则退出循环
alphaPairsChanged = 0
if entireSet:
for i in range(oS.m): # 遍历整个数据集
alphaPairsChanged += innerL(i, oS) # 使用优化的SMO算法
print("全样本遍历,第%d次迭代 样本:%d, alpha优化次数:%d" % (iter, i, alphaPairsChanged))
iter += 1
else: # 遍历非边界值
nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0] # 遍历不在边界0和C的alpha
for i in nonBoundIs:
alphaPairsChanged += innerL(i, oS)
print("非边界遍历,第%d次迭代 样本:%d, alpha优化次数:%d" % (iter, i, alphaPairsChanged))
iter += 1
if entireSet:
entireSet = False # 切换整个集合循环
elif (alphaPairsChanged == 0):
entireSet = True
print("迭代次数: %d" % iter)
return oS.b, oS.alphas
# 计算w
def calcWs(alphas, dataArr, classLabels):
X = mat(dataArr);
labelMat = mat(classLabels).transpose()
m, n = shape(X)
w = zeros((n, 1))
for i in range(m):
w += multiply(alphas[i] * labelMat[i], X[i, :].T)
return w
# 绘制数据集以及划分直线
def showData(w, b):
x, y = loadDataSet('testSet.txt')
xarr = array(x)
n = shape(x)[0]
x1 = []; y1 = []
x2 = []; y2 = []
for i in arange(n):
if int(y[i]) == 1:
x1.append(xarr[i, 0]);
y1.append(xarr[i, 1])
else:
x2.append(xarr[i, 0]);
y2.append(xarr[i, 1])
plt.scatter(x1, y1, s=30, c='r', marker='s')
plt.scatter(x2, y2, s=30, c='g')
# 画出 SVM 分类直线
xx = arange(0, 10, 0.1)
# 由分类直线 weights[0] * xx + weights[1] * yy1 + b = 0 易得下式
yy1 = (-w[0] * xx - b) / w[1]
# 由分类直线 weights[0] * xx + weights[1] * yy2 + b + 1 = 0 易得下式
yy2 = (-w[0] * xx - b - 1) / w[1]
# 由分类直线 weights[0] * xx + weights[1] * yy3 + b - 1 = 0 易得下式
yy3 = (-w[0] * xx - b + 1) / w[1]
plt.plot(xx, yy1.T)
plt.plot(xx, yy2.T)
plt.plot(xx, yy3.T)
# 画出支持向量点
for i in range(n):
if alphas[i] > 0.0:
plt.scatter(xarr[i, 0], xarr[i, 1], s=150, c='none', alpha=0.7, linewidth=1.5, edgecolor='red')
plt.xlim((-2, 12))
plt.ylim((-8, 6))
plt.show()
运行结果:
if __name__ == '__main__':
dataMat, labelMat = loadDataSet('testSet.txt')
b, alphas = smoP(dataMat, labelMat, 0.6, 0.001, 40)
w = calcWs(alphas, array(dataMat), labelMat)
showData(w, b)
在数据集上运行完整版SMO算法之后得到的支持向量,其结果与简单SMO稍有不同。
五、示例:基于SVM的手写数字识别
对于手写数字书别,之前用过KNN算法来进行实现,具体可以看我的这篇博客:机器学习——K-近邻算法实例实战_DreamWendy的博客-CSDN博客_k近邻实战
示例:基于SVM的数字识别流程步骤
(1) 收集数据:提供的文本文件。
(2) 准备数据:基于二值图像构造向量。
(3) 分析数据:对图像向量进行目测。
(4) 训练算法:采用两种不同的核函数,并对径向基核函数采用不同的设置来运行SMO算法。
(5) 测试算法:编写一个函数来测试不同的核函数并计算错误率。
(6) 使用算法:一个图像识别的完整应用还需要一些图像处理的知识,这里并不打算深入介绍。
5.1 数据集
5.2 算法实现
from numpy import *
# 随机选择alpha
def selectJrand(i, m):
j = i # 选择一个不等于i的j
while (j == i): # 只要函数值不等于输入值i,函数就会进行随机选择
j = int(random.uniform(0, m))
return j
# 修剪alpha
def clipAlpha(aj, H, L): # 用于调整大于H或小于L的alpha值
if aj > H:
aj = H
if L > aj:
aj = L
return aj
# 类
class optStruct:
def __init__(self, dataMatIn, classLabels, C, toler, kTup): # 使用参数初始化结构
self.X = dataMatIn # 数据矩阵
self.labelMat = classLabels # 数据标签
self.C = C # 松弛变量
self.tol = toler # 容错率
self.m = shape(dataMatIn)[0] # 数据矩阵行数m
self.alphas = mat(zeros((self.m, 1))) # 根据矩阵行数初始化alpha参数为0
self.b = 0 # 初始化b参数为0
self.eCache = mat(zeros((self.m, 2))) # 第一列是有效标志
self.K = mat(zeros((self.m,self.m))) # 初始化核K
for i in range(self.m): # 计算所有数据的核K
self.K[:,i] = kernelTrans(self.X, self.X[i,:], kTup)
# 通过核函数将数据转换更高维的空间
def kernelTrans(X, A, kTup):
m,n = shape(X)
K = mat(zeros((m,1)))
if kTup[0] == 'lin': K = X * A.T #线性核函数,只进行内积。
elif kTup[0] == 'rbf': #高斯核函数,根据高斯核函数公式进行计算
for j in range(m):
deltaRow = X[j,:] - A
K[j] = deltaRow*deltaRow.T
K = exp(K/(-1*kTup[1]**2)) #计算高斯核K
else: raise NameError('核函数无法识别')
return K
# 计算误差
def calcEk(oS, k):
fXk = float(multiply(oS.alphas, oS.labelMat).T*oS.K[:,k] + oS.b)
Ek = fXk - float(oS.labelMat[k])
return Ek
# 内循环启发方式
def selectJ(i, oS, Ei):
maxK = -1; maxDeltaE = 0; Ej = 0 # 初始化
oS.eCache[i] = [1, Ei] # 选择给出最大增量E的alpha
validEcacheList = nonzero(oS.eCache[:, 0].A)[0]
if (len(validEcacheList)) > 1:
for k in validEcacheList: # 循环使用有效的Ecache值并找到使delta E最大化的值
if k == i: continue # 如果k对于i,不计算i
Ek = calcEk(oS, k) # 计算Ek的值
deltaE = abs(Ei - Ek) # 计算|Ei-Ek|
if (deltaE > maxDeltaE): # 找到maxDeltaE
maxK = k; maxDeltaE = deltaE; Ej = Ek
return maxK, Ej
else: # 在这种情况下(第一次),没有任何有效的eCache值
j = selectJrand(i, oS.m) # 随机选择alpha_j的索引值
Ej = calcEk(oS, j)
return j, Ej
# 计算Ek并更新误差缓存
def updateEk(oS, k): # 任何alpha更改后,更新缓存中的新值
Ek = calcEk(oS, k)
oS.eCache[k] = [1, Ek]
# 优化的SMO算法
def innerL(i, oS):
Ei = calcEk(oS, i) # 计算误差Ei
if ((oS.labelMat[i]*Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or ((oS.labelMat[i]*Ei > oS.tol) and (oS.alphas[i] > 0)):
# 使用内循环启发方式选择alpha_j并计算Ej
j,Ej = selectJ(i, oS, Ei)
# 保存更新前的aplpha值,拷贝
alphaIold = oS.alphas[i].copy(); alphaJold = oS.alphas[j].copy()
# 步骤2:计算上下界L和H
if (oS.labelMat[i] != oS.labelMat[j]):
L = max(0, oS.alphas[j] - oS.alphas[i])
H = min(oS.C, oS.C + oS.alphas[j] - oS.alphas[i])
else:
L = max(0, oS.alphas[j] + oS.alphas[i] - oS.C)
H = min(oS.C, oS.alphas[j] + oS.alphas[i])
if L==H: print("L==H"); return 0
# 步骤3:计算eta
eta = 2.0 * oS.X[i,:]*oS.X[j,:].T - oS.X[i,:]*oS.X[i,:].T - oS.X[j,:]*oS.X[j,:].T
if eta >= 0: print("eta>=0"); return 0
# 步骤4:更新alpha_j
oS.alphas[j] -= oS.labelMat[j]*(Ei - Ej)/eta
# 步骤5:修剪alpha_j
oS.alphas[j] = clipAlpha(oS.alphas[j],H,L)
# 更新Ej至误差缓存
updateEk(oS, j)
if (abs(oS.alphas[j] - alphaJold) < 0.00001): print("j not moving enough"); return 0
# 步骤6:更新alpha_i
oS.alphas[i] += oS.labelMat[j]*oS.labelMat[i]*(alphaJold - oS.alphas[j])
# 更新Ei至误差缓存
updateEk(oS, i)
# 步骤7:更新b_1和b_2
b1 = oS.b - Ei - oS.labelMat[i] * (oS.alphas[i] - alphaIold) * oS.K[i, i] - oS.labelMat[j] * (oS.alphas[j] - alphaJold) * oS.K[i, j]
b2 = oS.b - Ej - oS.labelMat[i] * (oS.alphas[i] - alphaIold) * oS.K[i, j] - oS.labelMat[j] * (oS.alphas[j] - alphaJold) * oS.K[j, j]
# 步骤8:根据b_1和b_2更新b
if (0 < oS.alphas[i]) and (oS.C > oS.alphas[i]):
oS.b = b1
elif (0 < oS.alphas[j]) and (oS.C > oS.alphas[j]):
oS.b = b2
else:
oS.b = (b1 + b2)/2.0
return 1
else:
return 0
# 完整的线性SMO算法
def smoP(dataMatIn, classLabels, C, toler, maxIter,kTup=('lin', 0)):
oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(), C, toler, kTup)# 初始化
iter = 0 # 初始化迭代次数为0
entireSet = True; alphaPairsChanged = 0
while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)): # 超过最大迭代次数或者遍历整个数据集都alpha也没有更新,则退出循环
alphaPairsChanged = 0
if entireSet:
for i in range(oS.m): # 遍历整个数据集
alphaPairsChanged += innerL(i, oS) # 使用优化的SMO算法
print("全样本遍历,第%d次迭代 样本:%d, alpha优化次数:%d" % (iter, i, alphaPairsChanged))
iter += 1
else: # 遍历非边界值
nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0] # 遍历不在边界0和C的alpha
for i in nonBoundIs:
alphaPairsChanged += innerL(i, oS)
print("非边界遍历,第%d次迭代 样本:%d, alpha优化次数:%d" % (iter, i, alphaPairsChanged))
iter += 1
if entireSet:
entireSet = False # 切换整个集合循环
elif (alphaPairsChanged == 0):
entireSet = True
print("迭代次数: %d" % iter)
return oS.b, oS.alphas
# 图像转换为向量
def img2vector(filename):
returnVect = zeros((1, 1024))
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVect[0, 32 * i + j] = int(lineStr[j])
return returnVect
# 加载图像数据
def loadImages(dirName):
from os import listdir
hwLabels = []
trainingFileList = listdir(dirName) # 加载训练集
m = len(trainingFileList)
trainingMat = zeros((m, 1024))
for i in range(m):
fileNameStr = trainingFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
if classNumStr == 9:
hwLabels.append(-1)
else:
hwLabels.append(1)
trainingMat[i, :] = img2vector('%s/%s' % (dirName, fileNameStr))
return trainingMat, hwLabels
# 测试
def testDigits(kTup=('rbf', 10)):
dataArr, labelArr = loadImages('trainingDigits')
b, alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, kTup)
datMat = mat(dataArr);
labelMat = mat(labelArr).transpose()
svInd = nonzero(alphas.A > 0)[0]
sVs = datMat[svInd]
labelSV = labelMat[svInd];
print("支持向量机是 %d " % shape(sVs)[0])
m, n = shape(datMat)
errorCount = 0
for i in range(m):
kernelEval = kernelTrans(sVs, datMat[i, :], kTup)
predict = kernelEval.T * multiply(labelSV, alphas[svInd]) + b
if sign(predict) != sign(labelArr[i]): errorCount += 1
print("训练集错误率: %f" % (float(errorCount) / m))
dataArr, labelArr = loadImages('testDigits')
errorCount = 0
datMat = mat(dataArr);
labelMat = mat(labelArr).transpose()
m, n = shape(datMat)
for i in range(m):
kernelEval = kernelTrans(sVs, datMat[i, :], kTup)
predict = kernelEval.T * multiply(labelSV, alphas[svInd]) + b
if sign(predict) != sign(labelArr[i]): errorCount += 1
print("测试错误率: %f" % (float(errorCount) / m))
测试结果:
if __name__ == '__main__':
testDigits()
六、实验总结
SVM算法的主要优点有:
- 解决高维特征的分类回归问题很有效,在特征维度大于样本数时依然有很好的效果。
- 仅仅使用一部分支持向量来做超平面的决策,无需依赖全部数据。
- 使用核函数可以灵活的来解决各种非线性的分类回归问题。
- 样本量不是海量数据的时候,分类准确率高,泛化能力强。
SVM算法的主要缺点有:
- 特征维度远大于样本数时,SVM表现一般。
- SVM在样本量非常大,核函数映射维度非常高时,计算量过大,不太适合使用。
- 非线性问题的核函数的选择没有通用标准,难以选择一个合适的核函数。
- SVM对缺失数据敏感。
- SVM要进行距离计算,需要对数据进行标准化处理,而决策树不需要。
SVM算法理解起来还真是费了一番功夫,头秃头秃!!!!感觉自己还没有真正掌握SMO,主要是算法代码这块需要多琢磨琢磨,公式推导什么的还是可以,勇敢牛牛不怕困难!继续努力!