SVM支持向量机实验(基于SVM的手写体数字识别)

文章目录

  • 最大间隔与分类
  • 对偶问题
    • 等式约束
    • 不等式约束的KKT条件
    • 二次规划
    • SMO
  • 核函数
  • 软间隔与正则化
  • 支持向量回归
  • 实现SMO算法处理小规模数据集
    • 简化版SMO算法
    • 利用完整Platt SMO算法加速优化
    • 在复杂数据上应用核函数
  • 基于SVM的数字识别
  • 实验总结

最大间隔与分类

线性模型:
在样本空间中寻找一个超平面,将不同类别样本分开。当数据点在二维平面上时,分隔超平面是一条直线。若数据集是三维的,分隔数据的即为一个平面。高维情况时分隔数据的是超平面,也就是分类的决策边界。分布在超平面一侧的所有数据都属于某个类别,而分布在另一侧的所有数据则属于另一个类别。
SVM支持向量机实验(基于SVM的手写体数字识别)_第1张图片
我们希望采用这种方式构建分类器,即如果数据点离决策边界越远,那么其最后的预测结果也就越可信。那么我们如何选择超平面?我们希望找到离分隔超平面最近的点(支持向量),确保它们离分隔面的距离尽可能远。这里点到分隔面的距离称为间隔,我们希望间隔尽可能的大,即最大化决策边界的边缘,这是因为若我们犯错或在有限数据上训练分类器的话,我们希望分类器尽可能健壮。
如图所示:
SVM支持向量机实验(基于SVM的手写体数字识别)_第2张图片
我们在选择超平面时应选择“正中间”的这条直线,容忍性好,鲁棒性高,泛化能力最强。

我们可将分隔超平面方程写为: w T x + b = 0 w^Tx+b=0 wTx+b=0
SVM支持向量机实验(基于SVM的手写体数字识别)_第3张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第4张图片
如图所示:令 x + 和 x − x_+和x_- x+x位于决策边界上,标签为正负两个样本, x + x_+ x+到分类线距离为: d + = ∣ W T x + + b ∣ ∣ ∣ W ∣ ∣ d_+=\frac{|W^Tx_++b|}{||W||} d+=WWTx++b.则分类间隔为: w i d t h = 2 ∣ ∣ W ∣ ∣ width=\frac{2}{||W||} width=W2.

间隔最大化即要找到参数w和b,使得以下公式最大:
SVM支持向量机实验(基于SVM的手写体数字识别)_第5张图片
我们举一个间隔最大化的简单的例子:
SVM支持向量机实验(基于SVM的手写体数字识别)_第6张图片
根据已知条件,联立方程组并化简后可得到以下式子:
SVM支持向量机实验(基于SVM的手写体数字识别)_第7张图片
这样我们便得到了一个圆的方程 1 2 ( w 1 2 + w 2 2 ) \frac{1}{2}(w_1^2+w_2^2) 21(w12+w22)以及两条直线方程 w 1 + w 2 > 1 w_1+w_2>1 w1+w2>1 3 2 w 1 + w 2 > 1 \frac{3}{2}w_1+w_2>1 23w1+w2>1,用图表示如下:
SVM支持向量机实验(基于SVM的手写体数字识别)_第8张图片
我们要求的为 1 2 ( w 1 2 + w 2 2 ) \frac{1}{2}(w_1^2+w_2^2) 21(w12+w22)的最小值,显而易见该方程最小值即为0,但在求该方程最小值时同时要满足 w 1 + w 2 > 1 w_1+w_2>1 w1+w2>1 3 2 w 1 + w 2 > 1 \frac{3}{2}w_1+w_2>1 23w1+w2>1的约束条件,即图中两条直线的右边相交部分。因此以原点为中心,我们可以将圆不断放大直至与约束区域边界相切,这样便可以找到 1 2 ( w 1 2 + w 2 2 ) \frac{1}{2}(w_1^2+w_2^2) 21(w12+w22)的最小值。
SVM支持向量机实验(基于SVM的手写体数字识别)_第9张图片
如图所示:求得w和b后在代入便可得到分隔超平面方程及最大化间隔。

对偶问题

等式约束

给定目标函数f: R n − > R R^n->R Rn>R,希望找到x∈ R n R^n Rn,在满足约束条件g(x)=0的前提下,使得f(x)有最小值。该约束优化问题记为:
min f(x) s.t. g(x)=0.
建立拉格朗日函数:
L(x,λ)=f(x)+λg(x)
λ为拉格朗日乘数,因此,将原本的约束优化问题转换为等价无约束优化问题:
在这里插入图片描述
分别对待求解参数求导,得:
SVM支持向量机实验(基于SVM的手写体数字识别)_第10张图片
一般联立方程组即可得到相应的解。

不等式约束的KKT条件

将约束条件g(x)=0推广为g(x)<=0,约束优化问题便改为:
在这里插入图片描述
拉格朗日函数为:
L ( x , λ ) = f ( x ) + λ g ( x ) L(x,\lambda)=f(x)+\lambda g(x) L(x,λ)=f(x)+λg(x)
其约束范围为不等式,可等价转化为Karush-Kuhn-Tucker (KKT)条件:
SVM支持向量机实验(基于SVM的手写体数字识别)_第11张图片
在此基础上,通过优化方式(如二次规划或SMO)求解其最优解。

几何解释:
SVM支持向量机实验(基于SVM的手写体数字识别)_第12张图片
当解位于 g i ( x ) < 0 g_i(x)<0 gi(x)<0范围时,那么 g i ( x ) = 0 g_i(x)=0 gi(x)=0这一条件就未起到了约束作用,因此,当解满足 g i ( x ) = 0 g_i(x)=0 gi(x)=0这一约束条件时,约束条件才有意义。

拉格朗日乘子法:

  1. 引入拉格朗日乘子 α i > = 0 \alpha_i>=0 αi>=0得到拉格朗日函数: L ( w , b , α ) = 1 2 ∣ ∣ w ∣ ∣ 2 − ∑ i = 1 m α i ( y i ( w T x i + b ) − 1 ) L(w,b,\alpha)=\frac{1}{2}||w||^2-\sum_{i=1}^m\alpha_i(y_i(w^Tx_i+b)-1) L(w,b,α)=21w2i=1mαi(yi(wTxi+b)1)
  2. L ( w , b , α ) L(w,b,\alpha) L(w,b,α)对w和b的偏导为0: w = ∑ i = 1 m α i y i x i , ∑ i = 1 m α i y i = 0 w=\sum_{i=1}^m\alpha_iy_ix_i,\sum_{i=1}^m\alpha_iy_i=0 w=i=1mαiyixi,i=1mαiyi=0
  3. 将w和b回代到第一步:
    L ( w , b , α ) = 1 2 ∣ ∣ w ∣ ∣ 2 − ∑ i = 1 m α i ( y i ( w T x i + b ) − 1 ) L(w,b,\alpha)=\frac{1}{2}||w||^2-\sum_{i=1}^m\alpha_i(y_i(w^Tx_i+b)-1) L(w,b,α)=21w2i=1mαi(yi(wTxi+b)1)

= 1 2 w T w − w T ∑ i = 1 m α i y i x i − b ∑ i = 1 m α i y i + ∑ i = 1 m α i \frac{1}{2}w^Tw-w^T\sum_{i=1}^m\alpha_iy_ix_i-b\sum_{i=1}^m\alpha_iy_i+\sum_{i=1}^m\alpha_i 21wTwwTi=1mαiyixibi=1mαiyi+i=1mαi

= 1 2 w T ( ∑ i = 1 m α i y i x i ) − w T ∑ i = 1 m α i y i x i + ∑ i = 1 m α i \frac{1}{2}w^T(\sum_{i=1}^m\alpha_iy_ix_i)-w^T\sum_{i=1}^m\alpha_iy_ix_i+\sum_{i=1}^m\alpha_i 21wT(i=1mαiyixi)wTi=1mαiyixi+i=1mαi

= − 1 2 w T ∑ i = 1 m α i y i x i + ∑ i = 1 m α i -\frac{1}{2}w^T\sum_{i=1}^m\alpha_iy_ix_i+\sum_{i=1}^m\alpha_i 21wTi=1mαiyixi+i=1mαi

= − 1 2 ∑ i = 1 m ∑ j = 1 m α i α j y i y j x i T x j + ∑ i = 1 m α i -\frac{1}{2}\sum_{i=1}^m\sum_{j=1}^m\alpha_i\alpha_jy_iy_jx_i^Tx_j+\sum_{i=1}^m\alpha_i 21i=1mj=1mαiαjyiyjxiTxj+i=1mαi

m i n α min_\alpha minα 1 2 ∑ i = 1 m ∑ j = 1 m α i α j y i y j x i T x j − ∑ i = 1 m α i \frac{1}{2}\sum_{i=1}^m\sum_{j=1}^m\alpha_i\alpha_jy_iy_jx_i^Tx_j-\sum_{i=1}^m\alpha_i 21i=1mj=1mαiαjyiyjxiTxji=1mαi
s.t. ∑ i = 1 m α i y i = 0 , α i > = 0 , i = 1 , 2 , . . . , m . \sum_{i=1}^m\alpha_iy_i=0,\alpha_i>=0,i=1,2,...,m. i=1mαiyi=0,αi>=0,i=1,2,...,m.

由于 m i n w , b 1 2 w T w = m i n w , b m a x α L ( w , b , α ) = m a x α m i n w , b L ( w , b , α ) min_{w,b}\frac{1}{2}w^Tw=min_{w,b}max_\alpha L(w,b,\alpha)=max_\alpha min_{w,b}L(w,b,\alpha) minw,b21wTw=minw,bmaxαL(w,b,α)=maxαminw,bL(w,b,α)

则等价于 m a x α ∑ i = 1 m α i − 1 2 ∑ i = 1 m ∑ j = 1 m α i α j y i y j x i T x j max_\alpha\sum_{i=1}^m\alpha_i-\frac{1}{2}\sum_{i=1}^m\sum_{j=1}^m\alpha_i\alpha_jy_iy_jx_i^Tx_j maxαi=1mαi21i=1mj=1mαiαjyiyjxiTxj
s.t. ∑ i = 1 m α i y i = 0 , α i > = 0 , i = 1 , 2 , . . . , m . \sum_{i=1}^m\alpha_iy_i=0,\alpha_i>=0,i=1,2,...,m. i=1mαiyi=0,αi>=0,i=1,2,...,m.
SVM支持向量机实验(基于SVM的手写体数字识别)_第13张图片
最终模型: f ( x ) = w T x + b = ∑ i = 1 m α I y i x i T x + b f(x)=w^Tx+b=\sum_{i=1}^m\alpha_Iy_ix_i^Tx+b f(x)=wTx+b=i=1mαIyixiTx+b
此处 α i \alpha_i αi为未知数
据Karush-Kuhn-Tucker(KKT)条件,函数最优解满足以下条件:
SVM支持向量机实验(基于SVM的手写体数字识别)_第14张图片
对于不在最大边缘边界上的点,由于 y i f ( x i ) > 1 , 因 此 y_if(x_i)>1,因此 yif(xi)>1, α i = 0 \alpha_i=0 αi=0

支持向量机解的稀疏性:
训练完成后,大部分训练样本都无需保留,最终模型只与支持向量有关。
SVM支持向量机实验(基于SVM的手写体数字识别)_第15张图片

二次规划

SVM支持向量机实验(基于SVM的手写体数字识别)_第16张图片
调用开源工具的二次规划程序求得 α 1 , α 2 , α 3 , α 4 \alpha_1,\alpha_2,\alpha_3,\alpha_4 α1,α2,α3,α4的值,并代入求得w和b的值。
SVM支持向量机实验(基于SVM的手写体数字识别)_第17张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第18张图片
显然当数据集样本很大时,计算量也很大。

SMO

m a x α ∑ i = 1 m α i − 1 2 ∑ i = 1 m ∑ j = 1 m α i α j y i y j x i T x j max_\alpha\sum_{i=1}^m\alpha_i-\frac{1}{2}\sum_{i=1}^m\sum_{j=1}^m\alpha_i\alpha_jy_iy_jx_i^Tx_j maxαi=1mαi21i=1mj=1mαiαjyiyjxiTxj
s.t. ∑ i = 1 m α i y i = 0. \sum_{i=1}^m\alpha_iy_i=0. i=1mαiyi=0.
基本思路:不断重复执行以下两个步骤直至收敛。

  1. 选取一对需要更新的变量 α i , α j \alpha_i,\alpha_j αiαj
  2. 固定 α i , α j \alpha_i,\alpha_j αiαj以外的参数,求解对偶问题更新 α i , α j \alpha_i,\alpha_j αiαj

当仅考虑 α i , α j \alpha_i,\alpha_j αiαj时,对偶问题的约束条件变为:
α i y i + α j y j = − ∑ k ! = i , j α k y k , α i > = 0 , α j > = 0 \alpha_iy_i+\alpha_jy_j=-\sum_{k!=i,j}\alpha_ky_k,\alpha_i>=0,\alpha_j>=0 αiyi+αjyj=k!=i,jαkyk,αi>=0,αj>=0
偏移项b:通过支持向量确定。
算法流程:每次选取两个 α \alpha α进行更新
SVM支持向量机实验(基于SVM的手写体数字识别)_第19张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第20张图片
需要注意的是我们要同时改变两个 α \alpha α,若只选取一个,那么该变量可以通过其他变量和约束条件联合求得,可能会导致约束条件失效,因此我们需要同时改变两个 α \alpha α
SVM支持向量机实验(基于SVM的手写体数字识别)_第21张图片

核函数

线性不可分->高维可分
当不存在一个能正确划分两类样本的超平面时,我们可以将样本从原始空间映射到一个更高维的特征空间,使得样本在该特征空间内线性可分。
SVM支持向量机实验(基于SVM的手写体数字识别)_第22张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第23张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第24张图片
设样本x映射后的向量为ϕ(x), 划分超平面为 f ( x ) = w T ϕ ( x ) + b f(x)=w^Tϕ(x)+b f(x)=wTϕ(x)+b
SVM支持向量机实验(基于SVM的手写体数字识别)_第25张图片
基本想法:不显式的构造该映射,而是设计核函数。
在这里插入图片描述
Mercer定理(充分非必要):只要对称函数值所对应的核矩阵半正定,则该函数可作为核函数。
SVM支持向量机实验(基于SVM的手写体数字识别)_第26张图片
常用核函数: 仍然可用SMO算法求解
SVM支持向量机实验(基于SVM的手写体数字识别)_第27张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第28张图片

软间隔与正则化

在实际应用中,很难选择合适的核函数使样本在特征空间中线性可分,此外,线性可分的结果也很难断定是否是由过拟合造成的。因此,我们引入软间隔的概念,允许SVM在一些样本上不满足约束。
SVM支持向量机实验(基于SVM的手写体数字识别)_第29张图片
部分样本允许:在这里插入图片描述
基本思想:最大化间隔的同时,让不满足约束的样本应尽可能的少。
在这里插入图片描述
C>0为惩罚参数, l 0 / 1 l_{0/1} l0/1是“0/1损失函数”
SVM支持向量机实验(基于SVM的手写体数字识别)_第30张图片
但是0/1损失函数非凸,非连续,不宜优化,因此我们选择替代损失函数,替代损失函数数学性质较好,一般是0/1损失函数的上界。
SVM支持向量机实验(基于SVM的手写体数字识别)_第31张图片
Hinge Loss:
SVM支持向量机实验(基于SVM的手写体数字识别)_第32张图片
据KKT条件推得最终模型只与支持向量有关,即hinge损失函数保留了支持向量机解的稀疏性。
支持向量机学习模型的更一般形式:
SVM支持向量机实验(基于SVM的手写体数字识别)_第33张图片
通过替换上图中的两个部分便可得到其他学习模型:对数几率回归(Logistic Regression),最小绝对收缩选择算子(LASSO)。

支持向量回归

特点:允许模型输出和实际输出间存在2ε的偏差。
SVM支持向量机实验(基于SVM的手写体数字识别)_第34张图片
对于落入中间2ε间隔带的样本我们不计算损失,从而获得模型的稀疏性。
SVM支持向量机实验(基于SVM的手写体数字识别)_第35张图片
形式化:
SVM支持向量机实验(基于SVM的手写体数字识别)_第36张图片
训练策略:
SVM支持向量机实验(基于SVM的手写体数字识别)_第37张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第38张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第39张图片

实现SMO算法处理小规模数据集

简化版SMO算法

SMO算法中的辅助函数:

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):#i为第一个alpha的下标,m为所有alpha的数目
    j=i
    while(j==i):
        j=int(random.uniform(0,m))#随机选择alpha
    return j
def clipAlpha(aj,H,L):#调整大于H或小于L的alpha值
    if aj>H:
        aj=H
    if L>aj:
        aj=L
    return aj

loadDataSet函数打开文件并对其逐行解析,得到每行的类标签和整个数据矩阵。selectJrand函数进行随机选择alpha。clipAlpha函数调整大于H或小于L的alpha值。
SMO函数伪代码如下所示:

  1. 创建一个alpha向量并将其初始化为0向量
  2. 当迭代次数小于最大迭代次数时(外循环):
  3. 对数据集中的每个数据向量(内循环):
  4. 如果该数据向量可以被优化:
  5. 随机选择另外一个数据向量
  6. 同时优化这两个向量
  7. 如果两个向量都不能被优化,退出内循环
  8. 如果所有向量都没被优化,增加迭代次数,继续下一次循环

简化版SMO算法如下所示:

def smoSimple(dataMatIn,classLabels,C,toler,maxIter):#数据集,类别标签,常数C,容错率,最大循环次数
    start=time.time()
    dataMatrix=mat(dataMatIn);
    labelMat=mat(classLabels).transpose()#转置类别标签
    b=0
    m,n=shape(dataMatrix)
    alphas=mat(zeros((m,1)))#初始化alpha列矩阵
    iter=0#存储在没有任何alpha改变情况下遍历数据集的次数
    while(iter<maxIter):
        alphaPairsChanged=0#记录alpha是否进行优化
        for i in range(m):
            fxi=float(multiply(alphas,labelMat).T*\
                      (dataMatrix*dataMatrix[i,:].T))+b #预测类别
            Ei=fxi-float(labelMat[i])#计算误差
            if((labelMat[i]*Ei<-toler)and(alphas[i]<C))or\
                    ((labelMat[i]*Ei>toler)and\
                     (alphas[i]>0)):
                j=selectJrand(i,m)#选择第二个alpha值
                fxj=float(multiply(alphas,labelMat).T*\
                          (dataMatrix*dataMatrix[j,:].T))+b
                Ej=fxj-float(labelMat[j])
                alphaIold=alphas[i].copy();
                alphaJold=alphas[j].copy();
                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
                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[j]-=labelMat[j]*(Ei-Ej)/eta
                alphas[j]=clipAlpha(alphas[j],H,L)
                if(abs(alphas[j]-alphaJold)<0.00001):
                    print('j not moving enough')
                    continue
                alphas[i]+=labelMat[j]*labelMat[i]*\
                           (alphaJold-alphas[j])#对i进行修改,修改量与j相同,但方向相反
                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
                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('iter:%d i:%d,pairs changed %d'%\
                      (iter,i,alphaPairsChanged))
        if(alphaPairsChanged==0):
            iter+=1
        else:
            iter=0
        print('iteration number: %d' %iter)
    end=time.time()
    rtime=end-start
    print("the running time is:%f"%(end-start))
    return b,alphas,rtime
	dataArr,labelArr=loadDataSet('D:/machinelearning/machinelearninginaction/Ch06/testSet.txt')
    # print(labelArr)
    b,alphas,rtime=smoSimple(dataArr,labelArr,0.6,0.001,40)

运行结果:
SVM支持向量机实验(基于SVM的手写体数字识别)_第40张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第41张图片
在这里插入图片描述
由运行结果可以看出简化版的SMO算法运行时间较久,并且随着迭代次数的增加,程序的运行时间也在增加,这里我运行了十次算法并计算出来程序的平均运行时间为4s左右,虽然不是太久,但该数据集规模较小,当数据集规模较大时,程序运行时间将会更长。
对支持向量用圆圈标记后的结果如图所示:
SVM支持向量机实验(基于SVM的手写体数字识别)_第42张图片

利用完整Platt SMO算法加速优化

在之前实现的简化版SMO算法中,对于小规模数据集运行时间并不会太久,但在更大规模数据集上时简化版SMO算法的运行时间就会变长。因此我们通过完整的Platt SMO算法进行加速优化。在简化版与完整版SMO算法中,实现alpha的更改和代数运算的优化环节一模一样,在优化过程中,唯一不同为选择alpha的方式。
Platt SMO算法是通过一个外循环来选择第一个alpha值的,并且其选择过程会在两种方式之间进行交替:一种方式为在所有数据集上进行单遍扫描,另一种方式是在非边界alpha中实现单遍扫描。非边界alpha指的是不等于边界0或C的alpha值,对整个数据集的扫描相当容易,而实现非边界alpha值的扫描时,首先需要建立这些alpha值的列表,然后再对这个表进行遍历,同时,该步骤会跳过已知的不会改变的alpha值。
在选择第一个alpha值后,算法会通过一个内循环来选择第二个alpha值,在优化过程中,会通过最大化步长的方式来获得第二个alpha值,在简化版SMO算法中,我们会在选择j之后计算错误率Ej,但在完整版SMO算法中,我们会建立一个全局的缓存用于保存误差值,并从中选择使得步长或者说Ei-Ej最大的alpha值。

完整版Platt SMO的支持函数:

class optStruct:
    def __init__(self,dataMatIn,classLabels,C,toler):
        self.X=dataMatIn
        self.labelMat=classLabels
        self.C=C
        self.tol=toler
        self.m=shape(dataMatIn)[0]
        self.alphas=mat(zeros((self.m,1)))
        self.b=0
        self.eCache=mat(zeros((self.m,2)))#误差缓存

def calcEk(oS,k):#计算E值并返回
    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):#选择第二个alpha(内循环的alpha值)
    maxK=-1
    maxDeltaE=0
    Ej=0
    oS.eCache[i]=[1,Ei]#将输入值Ei在缓存中设置为有效的(已经计算好的)
    validEcacheList=nonzero(oS.eCache[:,0].A)[0]#构建出非零表
    if(len(validEcacheList))>1:
        for k in validEcacheList:
            if k==1:
                continue
            Ek=calcEk(oS,k)
            deltaE=abs(Ei-Ek)
            if(deltaE>maxDeltaE):#选择具有最大步长的j
                maxk=k;maxDeltaE=deltaE;Ej=Ek
        return maxK,Ej
    else:
        j=selectJrand(i,oS.m)
        Ej=calcEk(oS,j)
    return j,Ej

def updateEk(oS,k):#计算误差值并存入缓存当中
    Ek=calcEk(oS,k)
    oS.eCache[k]=[1,Ek]

首先建立一个数据结构保存所有重要值,这个过程可以通过一个对象来完成,这里使用对象的目的不是为了面向对象编程,而是作为一个数据结构来使用对象。构建一个仅包含init方法的optStruct类,该方法可以实现其成员变量的填充。calcEk函数用于计算E值并返回,在之前简化版SMO算法中,该过程是内嵌的,但由于该过程在完整版Platt SMO算法中出现频繁,因此将其单独拎出来。selectJ函数用于选择第二个alpha(内循环的alpha值)。updateEk函数计算误差值并存入缓存当中。

完整Platt SMO算法中的优化例程:

def innerL(i,oS):
    Ei=calcEk(oS,i)
    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)):
        j,Ej=selectJ(i,oS,Ei)#采用第二个alpha选择中的启发式方法
        alphaIold=oS.alphas[i].copy()
        alphaJold=oS.alphas[j].copy()
        if(oS.labelMat[i]!=oS.labelMat[j]):
            L=max(0,oS.alphas[j]-oS.labelMat[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
        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
        oS.alphas[j]-=oS.labelMat[j]*(Ei-Ej)/eta
        oS.alphas[j]=clipAlpha(oS.alphas[j],H,L)
        updateEk(oS,j)#更新误差缓存
        if(abs(oS.alphas[j]-alphaJold)<0.00001):
            print("j not moving enough")
            return 0
        oS.alphas[i]+=oS.labelMat[j]*oS.labelMat[i]*\
                      (alphaJold-oS.alphas[j])
        updateEk(oS,i)
        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
        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

该部分代码与smoSimple函数几乎一样,但该部分代码使用了自己的数据结构,该结构在参数oS中传递,另外该部分代码使用了selectJ函数来选择第二个alpha值,最后在alpha值改变时更新Ecache。

完整版Platt SMO算法的外循环代码

def smoP(dataMatIn,classLabels,C,toler,maxIter,kTrup=('lin',0)):
    start=time.time()
    oS=optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler)#构建数据结构容纳数据
    iter=0
    entireSet=True;alphaPairsChanged=0
    while(iter<maxIter)and((alphaPairsChanged>0)or(entireSet)):
        alphaPairsChanged=0
        if entireSet:#遍历所有的值
            for i in range(oS.m):
                alphaPairsChanged+=innerL(i,oS)
                print("fullSet,iter:%d i:%d,pairs changed %d"%(iter,i,alphaPairsChanged))
            iter+=1
        else:#遍历非边界值
            nonBoundIs=nonzero((oS.alphas.A>0) * (oS.alphas.A<C))[0]
            for i in nonBoundIs:
                alphaPairsChanged+=innerL(i,oS)
                print("non-bound,iter:%d i:%d,pairs changed %d"%(iter,i,alphaPairsChanged))
            iter+=1
        if entireSet:
            entireSet=False
        elif(alphaPairsChanged==0):
            entireSet=True
        print("iteration number:%d"%iter)
    end=time.time()
    rtime=end-start
    print("the running time is:%f"%(end-start))
    return oS.b,oS.alphas,rtime

该算法首先构建一个数据结构容纳所有数据,再对控制函数退出的一些变量进行初始化,代码主体为while循环,与smoSimple类似,但该算法的循环退出条件更多,当迭代次数超过指定最大值,或遍历整个集合都未对任意alpha值对进行修改时,就退出循环。while循环内部与smoSimple也不同,一开始for循环遍历数据集上任意可能的alpha,我们调用innerL函数选择第二个alpha,并在可能时对其进行优化处理,若任意一对alpha值发生改变,那么返回1,第二个for循环遍历所有非边界alpha值,即不在边界0或C上的值。

	dataArr,labelArr=loadDataSet('D:/machinelearning/machinelearninginaction/Ch06/testSet.txt')
    # print(labelArr)
    b,alphas,rtime=smoP(dataArr,labelArr,0.6,0.001,40)

运行结果:
SVM支持向量机实验(基于SVM的手写体数字识别)_第43张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第44张图片
SVM支持向量机实验(基于SVM的手写体数字识别)_第45张图片
由运行结果可以看出完整版的Platt SMO算法相比简化版SMO算法的运行时间快得多,运行10次的平均时间为0.5s左右,而简化版SMO算法则需要4s左右。

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

    dataArr,labelArr=loadDataSet('D:/machinelearning/machinelearninginaction/Ch06/testSet.txt')
    b, alphas, rtime = smoP(dataArr, labelArr, 0.6, 0.001, 100)
ws=calcWs(alphas,dataArr,labelArr)
    print(ws)
    datMat=mat(dataArr)
    print(datMat[0]*mat(ws)+b)
    print(labelArr[0])
    datMat=mat(dataArr)
    print(datMat[1]*mat(ws)+b)
    print(labelArr[1])
    datMat=mat(dataArr)
    print(datMat[2]*mat(ws)+b)
    print(labelArr[2])

运行结果:
SVM支持向量机实验(基于SVM的手写体数字识别)_第46张图片
若值大于0则属于1类,若值小于0则属于-1类,对于数据点0,1,2,我们分别通过查看类别标签来验证分类的正确性,可以发现数据分类结果正确。

在复杂数据上应用核函数

核转换函数:

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))#元素间的除法
    else:
        raise NameError('Houston We Have a Problem--That Kernel is not recognized')
    return K

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]
        self.alphas=mat(zeros((self.m,1)))
        self.b=0
        self.eCache=mat(zeros((self.m,2)))
        self.K=mat(zeros((self.m,self.m)))
        for i in range(self.m):
            self.K[:,i]=kernelTrans(self.X,self.X[i,:],kTup)


kernelTrans函数有三个输入参数:2个数据型变量,1个元组。元组kTup时核函数的信息,元组第一个参数是描述所用核函数类型的一个字符串,其他2个参数都是核函数可能需要的可选参数,该函数首先构建出一个列向量,然后检查元组以确定核函数的类型。

需要对innerL函数和calcEk函数做的修改:

def innerL(i,oS):
    Ei=calcEk(oS,i)
    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)):
        j,Ej=selectJ(i,oS,Ei)#采用第二个alpha选择中的启发式方法
        alphaIold=oS.alphas[i].copy()
        alphaJold=oS.alphas[j].copy()
        if(oS.labelMat[i]!=oS.labelMat[j]):
            L=max(0,oS.alphas[j]-oS.labelMat[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
        # eta=2.0*oS.X[i,:]*oS.X[j,:].T-oS.X[i,:]*oS.X[i,:].T-\
        #     oS.X[j,:]*oS.X[j,:].T
        eta = 2.0 * oS.K[i, j] - oS.K[i, i] - oS.K[j, j]  # changed for kernel
        if eta>=0:
            print("eta>=0")
            return 0
        oS.alphas[j]-=oS.labelMat[j]*(Ei-Ej)/eta
        oS.alphas[j]=clipAlpha(oS.alphas[j],H,L)
        updateEk(oS,j)#更新误差缓存
        if(abs(oS.alphas[j]-alphaJold)<0.00001):
            print("j not moving enough")
            return 0
        oS.alphas[i]+=oS.labelMat[j]*oS.labelMat[i]*\
                      (alphaJold-oS.alphas[j])
        updateEk(oS,i)
        # 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
        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]
        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
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 testRbf(k1=1.3):
    dataArr,labelArr=loadDataSet('D:/machinelearning/machinelearninginaction/Ch06/testSetRBF.txt')
    b,alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, ('rbf', k1)) #C=200 important
    datMat=mat(dataArr)
    labelMat=mat(labelArr).transpose()
    svInd=nonzero(alphas.A>0)[0]
    sVs=datMat[svInd]#构建支持向量矩阵
    labelSV=labelMat[svInd]
    print("there are %d support vectors"%shape(sVs)[0])
    m,n=shape(datMat)
    errorCount=0
    for i in range(m):
        kernelEval=kernelTrans(sVs,datMat[i,:],('rbf',k1))
        predict=kernelEval.T*multiply(labelSV,alphas[svInd])+b
        if sign(predict)!=sign(labelArr[i]):
            errorCount+=1
    print("the training error rate is:%f"%(float(errorCount)/m))
    dataArr,labelArr=loadDataSet('D:/machinelearning/machinelearninginaction/Ch06/testSetRBF2.txt')
    errorCount=0
    datMat=mat(dataArr)
    labelMat=mat(labelArr).transpose()
    m,n=shape(datMat)
    for i in range(m):
        kernelEval=kernelTrans(sVs,datMat[i,:],('rbf',k1))
        predict=kernelEval.T*multiply(labelSV,alphas[svInd])+b
        if sign(predict)!=sign(labelArr[i]):
            errorCount+=1
    print("the test error rate is:%f"%(float(errorCount)/m))

    testRbf()

运行结果:
SVM支持向量机实验(基于SVM的手写体数字识别)_第47张图片
在这里插入图片描述
SVM支持向量机实验(基于SVM的手写体数字识别)_第48张图片

如图所示:当k1参数为1.3时,支持向量个数为5,训练错误率为0.46,测试错误率为0.43,当我们改变k1参数为0.3或2.3时,支持向量个数发生改变,同时训练错误率及测试错误率也发生了改变。支持向量数目存在一个最优值,SVM优点在于能对数据进行高效分类,若支持向量太少,可能会得到一个很差的决策边界,若支持向量太多,相当于每次都利用整个数据集进行分类,这种分类方法称为k近邻。

基于SVM的数字识别

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('D:/machinelearning/machinelearninginaction/Ch06/digits/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("there are %d support vectors"%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("the training error rate is:%f"%(float(errorCount)/m))
    dataArr,labelArr=loadImages('D:/machinelearning/machinelearninginaction/Ch06/digits/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("the test error rate is:%f"%(float(errorCount)/m))

支持向量机是一个二类分类器,因此我们对手写体识别的数据集进行处理,只保留1和9的数据样本,当碰到数字9输出类别标签-1,否则输出+1.
运行结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
结果表明:当改变参数分别为0.1,10,100时,支持向量个数及错误率也发生了变化。

实验总结

在SVM支持向量机的实验当中,首先要对实验原理足够熟悉以及掌握对数据集的处理才能方便实验进行。

你可能感兴趣的:(支持向量机,机器学习,算法)