机器学习笔记——支持向量机SMO算法完整版代码分析

机器学习笔记——支持向量机SMO算法完整版代码分析

  • 代码大体分析
    • 外循环
      • 参数类
    • 内循环
      • KKT条件判断
      • eCache参数
  • 完整SMO代码
  • 添加核函数代码

代码参考书籍:《机器学习实战》

\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)))

内循环

KKT条件判断

\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 αi0时, y i ⋅ ( w ⋅ x i + b ) ≤ 1 y_i·(w·x_i+b)\le 1 yi(wxi+b)1
上述条件等价于():
\qquad\qquad\qquad\qquad\qquad\qquad\qquad α i ≥ 0 \alpha_i\ge 0 αi0时, E i = y i ⋅ u i − y i 2 ≤ 0 E_i=y_i·u_i-y_i^2\le 0 Ei=yiuiyi20
即:
\qquad\qquad\qquad\qquad\qquad\qquad\qquad α i ≥ 0 \alpha_i\ge 0 αi0时, E i = y i ⋅ E i ≤ 0 E_i=y_i·E_i\le 0 Ei=yiEi0
那么当其大于 t o l e r toler toler时,就判定其不满足KKT条件。

eCache参数

\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

完整SMO代码

# -*- 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算法完整版代码(核函数)

你可能感兴趣的:(机器学习笔记)