代码参考书籍:《机器学习实战》
\qquad 关于支持向量机的一些笔记整理可参考:机器学习笔记——支持向量机的一些整理
\qquad 在前两次阅读SMO算法时,并未搞懂代码中的一些部分是什么作用,此处对书中的代码进行一个解读,这里以《机器学习实战》当中的SMO完整版为例。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
\qquad 在进行代码前先对SMO算法进行大体的分析,要完成支持向量机对“最大间隔”的寻找及对参数的优化,那么总体可以分为两个部分:第一寻找出可以优化的点 x x x(按照前面的介绍中一般为先寻找非边界样本进行优化,然后利用边界样本进行优化);第二利用“最大步长”找到 x x x对应的优化点 y y y进行优化。 不断重复上述的循环优化,直至精度达到一定程度,或者达到最大循环次数结束迭代。
\qquad 那么就可以考虑两个循环:外循环寻找 x x x点,同时进行非边界样本优化和边界样本优化的切换,同时控制整个算法程序的循环结束判断;内循环进行参数的优化。这里只对几个有疑问的点进行笔记,文章最后给出完整的代码。
\qquad 首先进行外循环的分析,在迭代的最初,我们将所有的参数( b b b和 α \alpha α)都初始化定义为0,那么也就是说在最初所有的点都在边界上,所以在一开始我们之间将其视为边界情况进行优化,进行整个样本数据的遍历。在第一次样本遍历后检测是否存在非边界可优化样本,然后进行非边界情况、边界情况的切换优化。伪代码如下:
初始化参数为0
未达到最大循环次数、参数仍可进行优化
属于非边界情况
参数优化
属于边界情况
参数优化
\qquad 接着的问题是如何判断是否存在非边界情况,这里用的是numpy.nonzero方法,提取出非边界情况样本的index,然后进行遍历。
\qquad 然后是如何进行非边界和边界情况的切换,看下面的外代码, a l p h a P a i r s C h a n g e d alphaPairsChanged alphaPairsChanged用于统计是否迭代对参数进行了优化,有优化则返回1,没有优化或不满足KKT条件则返回0。这里的方法是设置了一个 e n t i r e S e t entireSet entireSet参数,同时结合是否仍能优化来进行判断,可以看到一开始是遍历整个样本集的,因为所有的参数都在边界上,当第一次遍历完成后, e n t i r e S e t entireSet entireSet变为 F a l s e False False,那么第二次循环就会遍历非边界的点,而在非边界的点无法再进行优化时( a l p h a P a i r s C h a n g e d = 0 alphaPairsChanged=0 alphaPairsChanged=0), e n t i r e S e t entireSet entireSet被转换为 T r u e True True,于是下一次循环会遍历整个样本集,而此时样本集中的非边界情况对参数已经无法优化了,那么有优化的就是边界情况。
\qquad 通过这种方式达到了非边界、边界情况优化的切换,当两者都无法进行优化同时达到最大循环次数就结束迭代。
def svmSmo(datapath,C,toler,maxIters=40):
"""
主函数、外循环
"""
# 统计大循环次数
iters = 0
# 用于切换边界、非边界情况
entireSet = True
# 统计在边界、非边界情况下是否进行了优化,若当前没有不再有优化则进行切换
alphaPairsChanged =0
# 循环结束条件:达到最大迭代次数或迭代无法提高精度(非边界、边界情况下都无法再进行优化)
while (iters < maxIters) and ((alphaPairsChanged > 0) or (entireSet)):
# 每次循环重新统计
alphaPairsChanged =0
# 最初将所有的alpha都定义为0,所以先遍历整个训练集
if entireSet:
for i in range(oS.m):
alphaPairsChanged += innerL(oS,i)
iters += 1
else:
# 用nonzero方法筛选出非边界情况:即alpha!=0oralpha!=C的情况
nonBond = np.nonzero((oS.alpha.A > 0)*(oS.alpha.A < oS.C))[0]
for i in nonBond:
alphaPairsChanged += innerL(oS,i)
iters += 1
# 切换边界、非边界操作,同时结合着大循环的结束判断来理解
# 若第一次循环,则为遍历整个数据集;第一次循环完成后则先遍历非边界情况,再遍历边界情况,所以第一次True后则将其转换为False
if entireSet:
entireSet = False
# 将非边界情况的迭代结束条件设置为不再有精度提升,这时要考虑边界情况,则再次将entireSet设置为True,利用这种方法进行边界、非边界情况的切换
elif alphaPairsChanged == 0:
entireSet = True
\qquad 外循环当中使用了一个参数类Parameter,用于存储各种参数,在一开始并不明白 e C a c h e eCache eCache的作用,在后面的内循环中对其作用进行介绍。
class parameter:
"""
参数定义:toler为可容忍的误差或说精度;C为惩罚因子;eCache用于存储Ei,在选择最优j的时候要用到
"""
def __init__(self,dataMat,labelMat,C,toler):
self.x = dataMat
self.y = labelMat
self.m = dataMat.shape[0]
self.alpha = np.mat(np.zeros((self.m,1)))
self.b = 0
self.C = C
self.toler = toler
self.eCache = np.mat(np.zeros((self.m,2)))
\qquad 在外循环完成后,就要考虑如何对参数进行优化,这就涉及到:如何寻找与 x x x对应的最优 y y y,也就是寻找最大步长;如何进行参数的迭代优化,也就是文章SVM解释:五、SMO算法当中介绍的如何对参数进行迭代,这里对几个关键的点进行解释。
# 判断是否满足KKT条件,若不满足则进入优化
if ((oS.alpha[i,0] < oS.C) and (oS.y[i,0]*Ei < -oS.toler)) or ((oS.alpha[i,0] > 0) and (oS.y[i,0]*Ei > oS.toler)):
首先是对KKT条件的判断,这里的 i f if if判断条件来源于如下:
\qquad\qquad\qquad\qquad\qquad\qquad\qquad 当 α i ≥ 0 \alpha_i\ge 0 αi≥0时, y i ⋅ ( w ⋅ x i + b ) ≤ 1 y_i·(w·x_i+b)\le 1 yi⋅(w⋅xi+b)≤1
上述条件等价于():
\qquad\qquad\qquad\qquad\qquad\qquad\qquad 当 α i ≥ 0 \alpha_i\ge 0 αi≥0时, E i = y i ⋅ u i − y i 2 ≤ 0 E_i=y_i·u_i-y_i^2\le 0 Ei=yi⋅ui−yi2≤0
即:
\qquad\qquad\qquad\qquad\qquad\qquad\qquad 当 α i ≥ 0 \alpha_i\ge 0 αi≥0时, E i = y i ⋅ E i ≤ 0 E_i=y_i·E_i\le 0 Ei=yi⋅Ei≤0
那么当其大于 t o l e r toler toler时,就判定其不满足KKT条件。
\qquad 在上面的讨论中提到了在参数类的定义时,存在一个 e C a c h e eCache eCache参数,其作用是用于存储有效的 E i E_i Ei,那么如何理解呢?我们最初给 α \alpha α等参数定义时,是全部初始化为0的,那么就会造成一个问题,在选择最大步长时,所涉及到的 E j E_j Ej并不是其真实的值,所以这里定义一个 e C a c h e eCache eCache参数,在所有 E j E_j Ej都不是有效时,随机选择 j j j,在迭代过程中会逐渐出现有效的 E j E_j Ej,当存在有效的 E j E_j Ej时,就在这些有效的 E j E_j Ej当中选取 j j j。
# -*- coding: utf-8 -*-
"""
Created on Sat Aug 10 19:46:57 2019
@author:wangtao_zuel
E-mail:[email protected]
支持向量机SMO算法完整版
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
def svmSmo(dataPath,outerPath,C=0.6,toler=0.001,maxIters=40):
"""
主函数
"""
# 读取数据,此处用的为xlsx类型数据,其他类型的数据进行相应的改动
dataMat,labelMat = loadData(dataPath)
# 将数据放在参数类中,并定义一些后面需要用到参数
oS = parameter(dataMat,labelMat,C,toler)
# 统计大循环次数
iters = 0
# 用于切换边界、非边界情况
entireSet = True
# 统计在边界、非边界情况下是否进行了优化,若当前没有不再有优化则进行切换
alphaPairsChanged =0
# 循环结束条件:达到最大迭代次数或迭代无法提高精度(非边界、边界情况下都无法再进行优化)
while (iters < maxIters) and ((alphaPairsChanged > 0) or (entireSet)):
# 每次循环重新统计
alphaPairsChanged =0
# 最初将所有的alpha都定义为0,所以先遍历整个训练集
if entireSet:
for i in range(oS.m):
alphaPairsChanged += innerL(oS,i)
print("fullSet. iters:%d. alphaPairsChanged:%d"%(iters,alphaPairsChanged))
iters += 1
else:
# 用nonzero方法筛选出非边界情况:即alpha!=0oralpha!=C的情况
nonBond = np.nonzero((oS.alpha.A > 0)*(oS.alpha.A < oS.C))[0]
for i in nonBond:
alphaPairsChanged += innerL(oS,i)
print("nonBond. iters:%d. alphaPairsChanged:%d"%(iters,alphaPairsChanged))
iters += 1
# 切换边界、非边界操作,同时结合着大循环的结束判断来理解
# 若第一次循环,则为遍历整个数据集;第一次循环完成后则先遍历非边界情况,再遍历边界情况,所以第一次True后则将其转换为False
if entireSet:
entireSet = False
# 将非边界情况的迭代结束条件设置为不再有精度提升,这时要考虑边界情况,则再次将entireSet设置为True,利用这种方法进行边界、非边界情况的切换
elif alphaPairsChanged == 0:
entireSet = True
print("迭代优化完成!")
# print(oS.alpha)
# 计算参数
w = calcW(oS)
# 可视化,只适用于二维数据
# draw(oS,w)
# 敏感性分析,只适用于二维数据
# parameterAnalyze(oS,w)
# 训练集外数据预测
predict(oS,w,outerPath)
# return w[0,0],w[1,0],oS.b[0,0]
def loadData(datapath):
"""
数据读取
"""
data = pd.read_excel(datapath)
# 将训练集数据的特征和分类分开
features = np.mat(data.iloc[:,:-1])
labels = np.mat(data.iloc[:,-1]).T
return features,labels
class parameter:
"""
参数定义:toler为可容忍的误差或说精度;C为惩罚因子;eCache用于存储Ei,在选择最优j的时候要用到
"""
def __init__(self,dataMat,labelMat,C,toler):
self.x = dataMat
self.y = labelMat
self.m = dataMat.shape[0]
self.alpha = np.mat(np.zeros((self.m,1)))
self.b = 0
self.C = C
self.toler = toler
self.eCache = np.mat(np.zeros((self.m,2)))
def innerL(oS,i):
"""
迭代优化部分,成功优化则返回1;满足KKT条件、无法优化返回0
"""
Ei = calcEi(oS,i)
# 判断是否满足KKT条件,若不满足则进入优化
if ((oS.alpha[i,0] < oS.C) and (oS.y[i,0]*Ei < -oS.toler)) or ((oS.alpha[i,0] > 0) and (oS.y[i,0]*Ei > oS.toler)):
# 寻找最大步长的j
j,Ej = selectJ(oS,i,Ei)
# 保存一下上一步的alpha,在新alpha计算中需要用到
alphaIOld = oS.alpha[i,0].copy()
alphaJOld = oS.alpha[j,0].copy()
# 判断alpha上下界
if oS.y[i,0] != oS.y[j,0]:
L = max(0,oS.alpha[j,0]-oS.alpha[i,0])
H = min(oS.C,oS.C+oS.alpha[j,0]-oS.alpha[i,0])
else:
L = max(0,oS.alpha[j,0]+oS.alpha[i,0]-oS.C)
H = min(oS.C,oS.alpha[j,0]+oS.alpha[i,0])
# 若L=H,则alpha必定在边界上,没有优化的空间,可直接返回0值
if L == H:
return 0
eta = oS.x[i,:]*oS.x[i,:].T + oS.x[j,:]*oS.x[j,:].T - 2*oS.x[i,:]*oS.x[j,:].T
# 若eta为0,则返回0,因为分母不能为0,其实eta并不会为负数
if eta == 0:
return 0
# 求新的参数,要注意符号问题,尤其是在结果中出现alpha全为0时,可能出现了符号问题
oS.alpha[j,0] += oS.y[j,0]*(Ei-Ej)/eta
# 将新参数与上下界进行比较
oS.alpha[j,0] = clipAlpha(oS.alpha[j,0],H,L)
# 更新eChache
updateEi(oS,j)
# 若优化精度提高较小,则返回0
if abs(oS.alpha[j,0]-alphaJOld) < 0.00001:
# print("优化提高不大,放弃此次优化!")
return 0
# 更新alpha_i,根据alpha_i*y_i和alpha_j*y_j的变动程度相同但方向相反来计算
oS.alpha[i,0] += oS.y[i,0]*oS.y[j,0]*(alphaJOld-oS.alpha[j,0])
updateEi(oS,i)
# 更新参数b
bi = oS.b - Ei - oS.y[i,0]*(oS.alpha[i,0]-alphaIOld)*oS.x[i,:]*oS.x[i,:].T - oS.y[j,0]*(oS.alpha[j,0]-alphaJOld)*oS.x[i,:]*oS.x[j,:].T
bj = oS.b - Ej - oS.y[i,0]*(oS.alpha[i,0]-alphaIOld)*oS.x[i,:]*oS.x[j,:].T - oS.y[j,0]*(oS.alpha[j,0]-alphaJOld)*oS.x[j,:]*oS.x[j,:].T
# 判断b,且注意这里b值返回的不再是数值型数据
if (oS.alpha[i,0] > 0) and (oS.alpha[i,0] < oS.C):
oS.b = bi
elif (oS.alpha[j,0] > 0) and (oS.alpha[j,0] < oS.C):
oS.b = bj
else:
oS.b = (bi+bj)/2
# 所有参数都更新了则返回1
return 1
# 若满足KKT条件,则返回0
else:
return 0
def updateEi(oS,i):
"""
更新eChache
"""
Ei = calcEi(oS,i)
oS.eCache[i,:] = [1,Ei]
def clipAlpha(alpha,H,L):
"""
alpha与上下界比较
"""
if alpha < L:
alpha = L
elif alpha > H:
alpha = H
return alpha
def selectJ(oS,i,Ei):
"""
寻找和i对应最大步长的j
"""
maxJ = 0
maxdeltaE = 0
oS.eCache[i,:] = [1,Ei]
validEcacheList = np.nonzero(oS.eCache[:,0].A)[0]
# 在有效的j中寻找最大步长的j
if len(validEcacheList) > 1:
for j in validEcacheList:
if j == i:
continue
Ej = calcEi(oS,j)
deltaE = abs(Ei-Ej)
if deltaE > maxdeltaE:
maxJ = j
maxdeltaE = deltaE
best_Ej = Ej
return maxJ,best_Ej
# 若不存在有效的j,则随机选取一个作为j
else:
j = randomJ(i,oS.m)
Ej = calcEi(oS,j)
return j,Ej
def randomJ(i,m):
"""
随机选取j
"""
j = i
while j == i:
j = np.random.randint(0,m+1)
return j
def calcEi(oS,i):
"""
计算Ei,根据最大步长来选择最优的j
"""
ui = float(np.multiply(oS.alpha,oS.y).T*(oS.x*oS.x[i,:].T)+oS.b)
Ei = ui - float(oS.y[i,0])
return Ei
def calcW(oS):
"""
计算参数w
"""
w = oS.x.T*np.multiply(oS.alpha,oS.y)
return w
def draw(oS,w):
"""
拟合结果可视化:注意这里只适用于两特征的二维情况
"""
x1 = []
y1 = []
x2 = []
y2 = []
for i in range(oS.m):
if oS.y[i,0] == -1:
x1.append(oS.x[i,0])
y1.append(oS.x[i,1])
else:
x2.append(oS.x[i,0])
y2.append(oS.x[i,1])
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(x1,y1,marker='*')
ax.scatter(x2,y2)
x = np.arange(3,22,0.5)
y = -(w[0,0]*x+oS.b[0,0])/w[1,0]
ax.plot(x,y)
plt.show()
print(w)
print(oS.b)
def parameterAnalyze(k,b,c):
"""
参数分析:只适用于两特征二维样本
"""
fig = plt.figure()
ax1 = fig.add_subplot(211)
ax1.plot(c,k)
ax2 = fig.add_subplot(212)
ax2.plot(c,b)
plt.tight_layout()
plt.show()
def predict(oS,w,outerPath):
"""
训练集外数据分类
"""
data = pd.read_excel(outerPath)
# 转换为矩阵形式
dataMat = np.mat(data)
result = dataMat*w + oS.b[0,0]
# 计算结果大于0则分类为1,小于0则分类为-1类
result[result>0] = 1
result[result<0] = -1
# 数据保存
data['classLabel'] = result
data.to_excel(outerPath,index=False)
print("分类完成!")
机器学习笔记——支持向量机SMO算法完整版代码(核函数)