机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)

       机器学习算法与Python实践这个系列主要是参考《机器学习实战》这本书。因为自己想学习Python,然后也想对一些机器学习算法加深下了解,所以就想通过Python来实现几个比较常用的机器学习算法。恰好遇见这本同样定位的书籍,所以就参考这本书的过程来学习了。

       在这一节我们主要是对支持向量机进行系统的回顾,以及通过Python来实现。由于内容很多,所以这里分成三篇博文。第一篇讲SVM初级,第二篇讲进阶,主要是把SVM整条知识链理直,第三篇介绍Python的实现。SVM有很多介绍的非常好的博文,具体可以参考本文列出的参考文献和推荐阅读资料。在本文中,定位在于把集大成于一身的SVM的整体知识链理直,所以不会涉及细节的推导。网上的解说的很好的推导和书籍很多,大家可以进一步参考。

 

目录

一、引入

二、线性可分SVM与硬间隔最大化

三、Dual优化问题

       3.1、对偶问题

       3.2、SVM优化的对偶问题

四、松弛向量与软间隔最大化

五、核函数

六、多类分类之SVM

       6.1、“一对多”的方法

       6.2、“一对一”的方法

七、KKT条件分析

八、SVM的实现之SMO算法

       8.1、坐标下降算法

       8.2、SMO算法原理

       8.3、SMO算法的Python实现

九、参考文献与推荐阅读

 

八、SVM的实现之SMO算法

      终于到SVM的实现部分了。那么神奇和有效的东西还得回归到实现才可以展示其强大的功力。SVM有效而且存在很高效的训练算法,这也是工业界非常青睐SVM的原因。

      前面讲到,SVM的学习问题可以转化为下面的对偶问题:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第1张图片

       需要满足的KKT条件:

       也就是说找到一组αi可以满足上面的这些条件的就是该目标的一个最优解。所以我们的优化目标是找到一组最优的αi*。一旦求出这些αi*,就很容易计算出权重向量w*和b,并得到分隔超平面了。

       这是个凸二次规划问题,它具有全局最优解,一般可以通过现有的工具来优化。但当训练样本非常多的时候,这些优化算法往往非常耗时低效,以致无法使用。从SVM提出到现在,也出现了很多优化训练的方法。其中,非常出名的一个是1982年由Microsoft Research的John C. Platt在论文《Sequential Minimal Optimization: A Fast Algorithm for TrainingSupport Vector Machines》中提出的Sequential Minimal Optimization序列最小化优化算法,简称SMO算法。SMO算法的思想很简单,它将大优化的问题分解成多个小优化的问题。这些小问题往往比较容易求解,并且对他们进行顺序求解的结果与将他们作为整体来求解的结果完全一致。在结果完全一致的同时,SMO的求解时间短很多。在深入SMO算法之前,我们先来了解下坐标下降这个算法,SMO其实基于这种简单的思想的。

 

8.1、坐标下降(上升)法

      假设要求解下面的优化问题:

      在这里,我们需要求解m个变量αi,一般来说是通过梯度下降(这里是求最大值,所以应该叫上升)等算法每一次迭代对所有m个变量αi也就是α向量进行一次性优化。通过误差每次迭代调整α向量中每个元素的值。而坐标上升法(坐标上升与坐标下降可以看做是一对,坐标上升是用来求解max最优化问题,坐标下降用于求min最优化问题)的思想是每次迭代只调整一个变量αi的值,其他变量的值在这次迭代中固定不变。

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第2张图片

       最里面语句的意思是固定除αi之外的所有αj(i不等于j),这时W可看作只是关于αi的函数,那么直接对αi求导优化即可。这里我们进行最大化求导的顺序i是从1到m,可以通过更改优化顺序来使W能够更快地增加并收敛。如果W在内循环中能够很快地达到最优,那么坐标上升法会是一个很高效的求极值方法。

      用个二维的例子来说明下坐标下降法:我们需要寻找f(x,y)=x2+xy+y2的最小值处的(x*, y*),也就是下图的F*点的地方。

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第3张图片

       假设我们初始的点是A(图是函数投影到xoy平面的等高线图,颜色越深值越小),我们需要达到F*的地方。那最快的方法就是图中黄色线的路径,一次性就到达了,其实这个是牛顿优化法,但如果是高维的话,这个方法就不太高效了(因为需要求解矩阵的逆,这个不在这里讨论)。我们也可以按照红色所指示的路径来走。从A开始,先固定x,沿着y轴往让f(x, y)值减小的方向走到B点,然后固定y,沿着x轴往让f(x, y)值减小的方向走到C点,不断循环,直到到达F*。反正每次只要我们都往让f(x, y)值小的地方走就行了,这样脚踏实地,一步步走,每一步都使f(x, y)慢慢变小,总有一天,皇天不负有心人的。到达F*也是时间问题。到这里你可能会说,这红色线比黄色线贫富差距也太严重了吧。因为这里是二维的简单的情况嘛。如果是高维的情况,而且目标函数很复杂的话,再加上样本集很多,那么在梯度下降中,目标函数对所有αi求梯度或者在牛顿法中对矩阵求逆,都是很耗时的。这时候,如果W只对单个αi优化很快的时候,坐标下降法可能会更加高效。

 

8.2、SMO算法

       SMO算法的思想和坐标下降法的思想差不多。唯一不同的是,SMO是一次迭代优化两个α而不是一个。为什么要优化两个呢?

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第4张图片

       我们回到这个优化问题。我们可以看到这个优化问题存在着一个约束,也就是

       假设我们首先固定除α1以外的所有参数,然后在α1上求极值。但需要注意的是,因为如果固定α1以外的所有参数,由上面这个约束条件可以知道,α1将不再是变量(可以由其他值推出),因为问题中规定了:

      因此,我们需要一次选取两个参数做优化,比如αi和αj,此时αi可以由αj和其他参数表示出来。这样回代入W中,W就只是关于αj的函数了,这时候就可以只对αj进行优化了。在这里就是对αj进行求导,令导数为0就可以解出这个时候最优的αj了。然后也可以得到αi。这就是一次的迭代过程,一次迭代只调整两个拉格朗日乘子αi和αj。SMO之所以高效就是因为在固定其他参数后,对一个参数优化过程很高效(对一个参数的优化可以通过解析求解,而不是迭代。虽然对一个参数的一次最小优化不可能保证其结果就是所优化的拉格朗日乘子的最终结果,但会使目标函数向极小值迈进一步,这样对所有的乘子做最小优化,直到所有满足KKT条件时,目标函数达到最小)。

       总结下来是:

重复下面过程直到收敛{

(1)选择两个拉格朗日乘子αi和αj

(2)固定其他拉格朗日乘子αk(k不等于i和j),只对αi和αj优化w(α);

(3)根据优化后的αi和αj,更新截距b的值;

}

        那训练里面这两三步骤到底是怎么实现的,需要考虑什么呢?下面我们来具体分析下:

(1)选择αi和αj

        我们现在是每次迭代都优化目标函数的两个拉格朗日乘子αi和αj,然后其他的拉格朗日乘子保持固定。如果有N个训练样本,我们就有N个拉格朗日乘子需要优化,但每次我们只挑两个进行优化,我们就有N(N-1)种选择。那到底我们要选择哪对αi和αj呢?选择哪对才好呢?想想我们的目标是什么?我们希望把所有违法KKT条件的样本都纠正回来,因为如果所有样本都满足KKT条件的话,我们的优化就完成了。那就很直观了,哪个害群之马最严重,我们得先对他进行思想教育,让他尽早回归正途。OK,那我们选择的第一个变量αi就选违法KKT条件最严重的那一个。那第二个变量αj怎么选呢?

       我们是希望快点找到最优的N个拉格朗日乘子,使得代价函数最大,换句话说,要最快的找到代价函数最大值的地方对应的N个拉格朗日乘子。这样我们的训练时间才会短。就像你从广州去北京,有飞机和绿皮车给你选,你选啥?(就算你不考虑速度,也得考虑下空姐的感受嘛,别辜负了她们渴望看到你的期盼,哈哈)。有点离题了,anyway,每次迭代中,哪对αi和αj可以让我更快的达到代价函数值最大的地方,我们就选他们。或者说,走完这一步,选这对αi和αj代价函数值增加的值最多,比选择其他所有αi和αj的结合中都多。这样我们才可以更快的接近代价函数的最大值,也就是达到优化的目标了。再例如,下图,我们要从A点走到B点,按蓝色的路线走c2方向的时候,一跨一大步,按红色的路线走c1方向的时候,只能是人类的一小步。所以,蓝色路线走两步就迈进了成功之门,而红色的路线,人生曲折,好像成功遥遥无期一样,故曰,选择比努力更重要!

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第5张图片

       真啰嗦!说了半天,其实就一句话:为什么每次迭代都要选择最好的αi和αj,就是为了更快的收敛!那实践中每次迭代到底要怎样选αi和αj呢?这有个很好听的名字叫启发式选择,主要思想是先选择最有可能需要优化(也就是违反KKT条件最严重)的αi,再针对这样的αi选择最有可能取得较大修正步长的αj。具体是以下两个过程:

1)第一个变量αi的选择:

       SMO称选择第一个变量的过程为外层循环。外层训练在训练样本中选取违法KKT条件最严重的样本点。并将其对应的变量作为第一个变量。具体的,检验训练样本(xi, yi)是否满足KKT条件,也就是:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第6张图片

       该检验是在ε范围内进行的。在检验过程中,外层循环首先遍历所有满足条件0<αj<C的样本点,即在间隔边界上的支持向量点,检验他们是否满足KKT条件,然后选择违反KKT条件最严重的αi。如果这些样本点都满足KKT条件,那么遍历整个训练集,检验他们是否满足KKT条件,然后选择违反KKT条件最严重的αi

       优先选择遍历非边界数据样本,因为非边界数据样本更有可能需要调整,边界数据样本常常不能得到进一步调整而留在边界上。由于大部分数据样本都很明显不可能是支持向量,因此对应的α乘子一旦取得零值就无需再调整。遍历非边界数据样本并选出他们当中违反KKT 条件为止。当某一次遍历发现没有非边界数据样本得到调整时,遍历所有数据样本,以检验是否整个集合都满足KKT条件。如果整个集合的检验中又有数据样本被进一步进化,则有必要再遍历非边界数据样本。这样,不停地在遍历所有数据样本和遍历非边界数据样本之间切换,直到整个样本集合都满足KKT条件为止。以上用KKT条件对数据样本所做的检验都以达到一定精度ε就可以停止为条件。如果要求十分精确的输出算法,则往往不能很快收敛。

       对整个数据集的遍历扫描相当容易,而实现对非边界αi的扫描时,首先需要将所有非边界样本的αi值(也就是满足0<αi<C)保存到新的一个列表中,然后再对其进行遍历。同时,该步骤跳过那些已知的不会改变的αi值。

2)第二个变量αj的选择:

       在选择第一个αi后,算法会通过一个内循环来选择第二个αj值。因为第二个乘子的迭代步长大致正比于|Ei-Ej|,所以我们需要选择能够最大化|Ei-Ej|的第二个乘子(选择最大化迭代步长的第二个乘子)。在这里,为了节省计算时间,我们建立一个全局的缓存用于保存所有样本的误差值,而不用每次选择的时候就重新计算。我们从中选择使得步长最大或者|Ei-Ej|最大的αj

(2)优化αi和αj

       选择这两个拉格朗日乘子后,我们需要先计算这些参数的约束值。然后再求解这个约束最大化问题。

       首先,我们需要给αj找到边界L<=αj<=H,以保证αj满足0<=αj<=C的约束。这意味着αj必须落入这个盒子中。由于只有两个变量(αi, αj),约束可以用二维空间中的图形来表示,如下图:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第7张图片

       不等式约束使得(αij)在盒子[0, C]x[0, C]内,等式约束使得(αi, αj)在平行于盒子[0, C]x[0, C]的对角线的直线上。因此要求的是目标函数在一条平行于对角线的线段上的最优值。这使得两个变量的最优化问题成为实质的单变量的最优化问题。由图可以得到,αj的上下界可以通过下面的方法得到:

       我们优化的时候,αj必须要满足上面这个约束。也就是说上面是αj的可行域。然后我们开始寻找αj,使得目标函数最大化。通过推导得到αj的更新公式如下:

       这里Ek可以看做对第k个样本,SVM的输出与期待输出,也就是样本标签的误差。

       而η实际上是度量两个样本i和j的相似性的。在计算η的时候,我们需要使用核函数,那么就可以用核函数来取代上面的内积。

       得到新的αj后,我们需要保证它处于边界内。换句话说,如果这个优化后的值跑出了边界L和H,我们就需要简单的裁剪,将αj收回这个范围:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第8张图片

       最后,得到优化的αj后,我们需要用它来计算αi

       到这里,αi和αj的优化就完成了。

(3)计算阈值b:

       优化αi和αj后,我们就可以更新阈值b,使得对两个样本i和j都满足KKT条件。如果优化后αi不在边界上(也就是满足0<αi<C,这时候根据KKT条件,可以得到yigi(xi)=1,这样我们才可以计算b),那下面的阈值b1是有效的,因为当输入xi时它迫使SVM输出yi

       同样,如果0<αj<C,那么下面的b2也是有效的:

      如果0<αi<C和0<αj<C都满足,那么b1和b2都有效,而且他们是相等的。如果他们两个都处于边界上(也就是αi=0或者αi=C,同时αj=0或者αj=C),那么在b1和b2之间的阈值都满足KKT条件,一般我们取他们的平均值b=(b1+b2)/2。所以,总的来说对b的更新如下:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第9张图片

       每做完一次最小优化,必须更新每个数据样本的误差,以便用修正过的分类面对其他数据样本再做检验,在选择第二个配对优化数据样本时用来估计步长。

(4)凸优化问题终止条件:

       SMO算法的基本思路是:如果说有变量的解都满足此最优化问题的KKT条件,那么这个最优化问题的解就得到了。因为KKT条件是该最优化问题的充分必要条件(证明请参考文献)。所以我们可以监视原问题的KKT条件,所以所有的样本都满足KKT条件,那么就表示迭代结束了。但是由于KKT条件本身是比较苛刻的,所以也需要设定一个容忍值,即所有样本在容忍值范围内满足KKT条件则认为训练可以结束;当然了,对于对偶问题的凸优化还有其他终止条件,可以参考文献。

 

8.3、SMO算法的Python实现

8.3.1、Python的准备工作

      我使用的Python是2.7.5版本的。附加的库有Numpy和Matplotlib。而Matplotlib又依赖dateutil和pyparsing两个库,所以我们需要安装以上三个库。前面两个库还好安装,直接在官网下对应版本就行。但我找后两个库的时候,就没那么容易了。后来发现,其实对Python的库的下载和安装可以借助pip工具的。这个是安装和管理Python包的工具。感觉它有点像ubuntu的apt-get,需要安装什么库,直接下载和安装一条龙服务。

       首先,我们需要到pip的官网:https://pypi.python.org/pypi/pip下载对应我们python版本的pip,例如我的是pip-1.4.1.tar.gz。但安装pip需要另一个工具,也就是setuptools,我们到https://pypi.python.org/pypi/setuptools/#windows下载ez_setup.py这个文件回来。然后在CMD命令行中执行:(注意他们的路径)

#python ez_setup.py

这时候,就会自动下载.egg等等文件然后安装完成。

      然后我们解压pip-1.4.1.tar.gz。进入到该目录中,执行:

#python setup.py install

这时候就会自动安装pip到你python目录下的Scripts文件夹中。我的是C:\Python27\Scripts。

       在里面我们可以看到pip.exe,然后我们进入到该文件夹中:

#cd C:\Python27\Scripts

#pip install dateutil

#pip install pyparsing

这样就可以把这些额外的库给下载回来了。非常高端大气上档次!

8.3.2、SMO算法的Python实现

       在代码中已经有了比较详细的注释了。不知道有没有错误的地方,如果有,还望大家指正(每次的运行结果都有可能不同,另外,感觉有些结果似乎不太正确,但我还没发现哪里出错了,如果大家找到有错误的地方,还望大家指点下,衷心感谢)。里面我写了个可视化结果的函数,但只能在二维的数据上面使用。直接贴代码:

SVM.py

[python] view plain copy
  1. #################################################  
  2. # SVM: support vector machine  
  3. # Author : zouxy  
  4. # Date   : 2013-12-12  
  5. # HomePage : http://blog.csdn.net/zouxy09  
  6. # Email  : [email protected]  
  7. #################################################  
  8.   
  9. from numpy import *  
  10. import time  
  11. import matplotlib.pyplot as plt   
  12.   
  13.   
  14. # calulate kernel value  
  15. def calcKernelValue(matrix_x, sample_x, kernelOption):  
  16.     kernelType = kernelOption[0]  
  17.     numSamples = matrix_x.shape[0]  
  18.     kernelValue = mat(zeros((numSamples, 1)))  
  19.       
  20.     if kernelType == 'linear':  
  21.         kernelValue = matrix_x * sample_x.T  
  22.     elif kernelType == 'rbf':  
  23.         sigma = kernelOption[1]  
  24.         if sigma == 0:  
  25.             sigma = 1.0  
  26.         for i in xrange(numSamples):  
  27.             diff = matrix_x[i, :] - sample_x  
  28.             kernelValue[i] = exp(diff * diff.T / (-2.0 * sigma**2))  
  29.     else:  
  30.         raise NameError('Not support kernel type! You can use linear or rbf!')  
  31.     return kernelValue  
  32.   
  33.   
  34. # calculate kernel matrix given train set and kernel type  
  35. def calcKernelMatrix(train_x, kernelOption):  
  36.     numSamples = train_x.shape[0]  
  37.     kernelMatrix = mat(zeros((numSamples, numSamples)))  
  38.     for i in xrange(numSamples):  
  39.         kernelMatrix[:, i] = calcKernelValue(train_x, train_x[i, :], kernelOption)  
  40.     return kernelMatrix  
  41.   
  42.   
  43. # define a struct just for storing variables and data  
  44. class SVMStruct:  
  45.     def __init__(self, dataSet, labels, C, toler, kernelOption):  
  46.         self.train_x = dataSet # each row stands for a sample  
  47.         self.train_y = labels  # corresponding label  
  48.         self.C = C             # slack variable  
  49.         self.toler = toler     # termination condition for iteration  
  50.         self.numSamples = dataSet.shape[0# number of samples  
  51.         self.alphas = mat(zeros((self.numSamples, 1))) # Lagrange factors for all samples  
  52.         self.b = 0  
  53.         self.errorCache = mat(zeros((self.numSamples, 2)))  
  54.         self.kernelOpt = kernelOption  
  55.         self.kernelMat = calcKernelMatrix(self.train_x, self.kernelOpt)  
  56.   
  57.           
  58. # calculate the error for alpha k  
  59. def calcError(svm, alpha_k):  
  60.     output_k = float(multiply(svm.alphas, svm.train_y).T * svm.kernelMat[:, alpha_k] + svm.b)  
  61.     error_k = output_k - float(svm.train_y[alpha_k])  
  62.     return error_k  
  63.   
  64.   
  65. # update the error cache for alpha k after optimize alpha k  
  66. def updateError(svm, alpha_k):  
  67.     error = calcError(svm, alpha_k)  
  68.     svm.errorCache[alpha_k] = [1, error]  
  69.   
  70.   
  71. # select alpha j which has the biggest step  
  72. def selectAlpha_j(svm, alpha_i, error_i):  
  73.     svm.errorCache[alpha_i] = [1, error_i] # mark as valid(has been optimized)  
  74.     candidateAlphaList = nonzero(svm.errorCache[:, 0].A)[0# mat.A return array  
  75.     maxStep = 0; alpha_j = 0; error_j = 0  
  76.   
  77.     # find the alpha with max iterative step  
  78.     if len(candidateAlphaList) > 1:  
  79.         for alpha_k in candidateAlphaList:  
  80.             if alpha_k == alpha_i:   
  81.                 continue  
  82.             error_k = calcError(svm, alpha_k)  
  83.             if abs(error_k - error_i) > maxStep:  
  84.                 maxStep = abs(error_k - error_i)  
  85.                 alpha_j = alpha_k  
  86.                 error_j = error_k  
  87.     # if came in this loop first time, we select alpha j randomly  
  88.     else:             
  89.         alpha_j = alpha_i  
  90.         while alpha_j == alpha_i:  
  91.             alpha_j = int(random.uniform(0, svm.numSamples))  
  92.         error_j = calcError(svm, alpha_j)  
  93.       
  94.     return alpha_j, error_j  
  95.   
  96.   
  97. # the inner loop for optimizing alpha i and alpha j  
  98. def innerLoop(svm, alpha_i):  
  99.     error_i = calcError(svm, alpha_i)  
  100.   
  101.     ### check and pick up the alpha who violates the KKT condition  
  102.     ## satisfy KKT condition  
  103.     # 1) yi*f(i) >= 1 and alpha == 0 (outside the boundary)  
  104.     # 2) yi*f(i) == 1 and 0<alpha< C (on the boundary)  
  105.     # 3) yi*f(i) <= 1 and alpha == C (between the boundary)  
  106.     ## violate KKT condition  
  107.     # because y[i]*E_i = y[i]*f(i) - y[i]^2 = y[i]*f(i) - 1, so  
  108.     # 1) if y[i]*E_i < 0, so yi*f(i) < 1, if alpha < C, violate!(alpha = C will be correct)   
  109.     # 2) if y[i]*E_i > 0, so yi*f(i) > 1, if alpha > 0, violate!(alpha = 0 will be correct)  
  110.     # 3) if y[i]*E_i = 0, so yi*f(i) = 1, it is on the boundary, needless optimized  
  111.     if (svm.train_y[alpha_i] * error_i < -svm.toler) and (svm.alphas[alpha_i] < svm.C) or\  
  112.         (svm.train_y[alpha_i] * error_i > svm.toler) and (svm.alphas[alpha_i] > 0):  
  113.   
  114.         # step 1: select alpha j  
  115.         alpha_j, error_j = selectAlpha_j(svm, alpha_i, error_i)  
  116.         alpha_i_old = svm.alphas[alpha_i].copy()  
  117.         alpha_j_old = svm.alphas[alpha_j].copy()  
  118.   
  119.         # step 2: calculate the boundary L and H for alpha j  
  120.         if svm.train_y[alpha_i] != svm.train_y[alpha_j]:  
  121.             L = max(0, svm.alphas[alpha_j] - svm.alphas[alpha_i])  
  122.             H = min(svm.C, svm.C + svm.alphas[alpha_j] - svm.alphas[alpha_i])  
  123.         else:  
  124.             L = max(0, svm.alphas[alpha_j] + svm.alphas[alpha_i] - svm.C)  
  125.             H = min(svm.C, svm.alphas[alpha_j] + svm.alphas[alpha_i])  
  126.         if L == H:  
  127.             return 0  
  128.   
  129.         # step 3: calculate eta (the similarity of sample i and j)  
  130.         eta = 2.0 * svm.kernelMat[alpha_i, alpha_j] - svm.kernelMat[alpha_i, alpha_i] \  
  131.                   - svm.kernelMat[alpha_j, alpha_j]  
  132.         if eta >= 0:  
  133.             return 0  
  134.   
  135.         # step 4: update alpha j  
  136.         svm.alphas[alpha_j] -= svm.train_y[alpha_j] * (error_i - error_j) / eta  
  137.   
  138.         # step 5: clip alpha j  
  139.         if svm.alphas[alpha_j] > H:  
  140.             svm.alphas[alpha_j] = H  
  141.         if svm.alphas[alpha_j] < L:  
  142.             svm.alphas[alpha_j] = L  
  143.   
  144.         # step 6: if alpha j not moving enough, just return       
  145.         if abs(alpha_j_old - svm.alphas[alpha_j]) < 0.00001:  
  146.             updateError(svm, alpha_j)  
  147.             return 0  
  148.   
  149.         # step 7: update alpha i after optimizing aipha j  
  150.         svm.alphas[alpha_i] += svm.train_y[alpha_i] * svm.train_y[alpha_j] \  
  151.                                 * (alpha_j_old - svm.alphas[alpha_j])  
  152.   
  153.         # step 8: update threshold b  
  154.         b1 = svm.b - error_i - svm.train_y[alpha_i] * (svm.alphas[alpha_i] - alpha_i_old) \  
  155.                                                     * svm.kernelMat[alpha_i, alpha_i] \  
  156.                              - svm.train_y[alpha_j] * (svm.alphas[alpha_j] - alpha_j_old) \  
  157.                                                     * svm.kernelMat[alpha_i, alpha_j]  
  158.         b2 = svm.b - error_j - svm.train_y[alpha_i] * (svm.alphas[alpha_i] - alpha_i_old) \  
  159.                                                     * svm.kernelMat[alpha_i, alpha_j] \  
  160.                              - svm.train_y[alpha_j] * (svm.alphas[alpha_j] - alpha_j_old) \  
  161.                                                     * svm.kernelMat[alpha_j, alpha_j]  
  162.         if (0 < svm.alphas[alpha_i]) and (svm.alphas[alpha_i] < svm.C):  
  163.             svm.b = b1  
  164.         elif (0 < svm.alphas[alpha_j]) and (svm.alphas[alpha_j] < svm.C):  
  165.             svm.b = b2  
  166.         else:  
  167.             svm.b = (b1 + b2) / 2.0  
  168.   
  169.         # step 9: update error cache for alpha i, j after optimize alpha i, j and b  
  170.         updateError(svm, alpha_j)  
  171.         updateError(svm, alpha_i)  
  172.   
  173.         return 1  
  174.     else:  
  175.         return 0  
  176.   
  177.   
  178. # the main training procedure  
  179. def trainSVM(train_x, train_y, C, toler, maxIter, kernelOption = ('rbf'1.0)):  
  180.     # calculate training time  
  181.     startTime = time.time()  
  182.   
  183.     # init data struct for svm  
  184.     svm = SVMStruct(mat(train_x), mat(train_y), C, toler, kernelOption)  
  185.       
  186.     # start training  
  187.     entireSet = True  
  188.     alphaPairsChanged = 0  
  189.     iterCount = 0  
  190.     # Iteration termination condition:  
  191.     #   Condition 1: reach max iteration  
  192.     #   Condition 2: no alpha changed after going through all samples,  
  193.     #                in other words, all alpha (samples) fit KKT condition  
  194.     while (iterCount < maxIter) and ((alphaPairsChanged > 0or entireSet):  
  195.         alphaPairsChanged = 0  
  196.   
  197.         # update alphas over all training examples  
  198.         if entireSet:  
  199.             for i in xrange(svm.numSamples):  
  200.                 alphaPairsChanged += innerLoop(svm, i)  
  201.             print '---iter:%d entire set, alpha pairs changed:%d' % (iterCount, alphaPairsChanged)  
  202.             iterCount += 1  
  203.         # update alphas over examples where alpha is not 0 & not C (not on boundary)  
  204.         else:  
  205.             nonBoundAlphasList = nonzero((svm.alphas.A > 0) * (svm.alphas.A < svm.C))[0]  
  206.             for i in nonBoundAlphasList:  
  207.                 alphaPairsChanged += innerLoop(svm, i)  
  208.             print '---iter:%d non boundary, alpha pairs changed:%d' % (iterCount, alphaPairsChanged)  
  209.             iterCount += 1  
  210.   
  211.         # alternate loop over all examples and non-boundary examples  
  212.         if entireSet:  
  213.             entireSet = False  
  214.         elif alphaPairsChanged == 0:  
  215.             entireSet = True  
  216.   
  217.     print 'Congratulations, training complete! Took %fs!' % (time.time() - startTime)  
  218.     return svm  
  219.   
  220.   
  221. # testing your trained svm model given test set  
  222. def testSVM(svm, test_x, test_y):  
  223.     test_x = mat(test_x)  
  224.     test_y = mat(test_y)  
  225.     numTestSamples = test_x.shape[0]  
  226.     supportVectorsIndex = nonzero(svm.alphas.A > 0)[0]  
  227.     supportVectors      = svm.train_x[supportVectorsIndex]  
  228.     supportVectorLabels = svm.train_y[supportVectorsIndex]  
  229.     supportVectorAlphas = svm.alphas[supportVectorsIndex]  
  230.     matchCount = 0  
  231.     for i in xrange(numTestSamples):  
  232.         kernelValue = calcKernelValue(supportVectors, test_x[i, :], svm.kernelOpt)  
  233.         predict = kernelValue.T * multiply(supportVectorLabels, supportVectorAlphas) + svm.b  
  234.         if sign(predict) == sign(test_y[i]):  
  235.             matchCount += 1  
  236.     accuracy = float(matchCount) / numTestSamples  
  237.     return accuracy  
  238.   
  239.   
  240. # show your trained svm model only available with 2-D data  
  241. def showSVM(svm):  
  242.     if svm.train_x.shape[1] != 2:  
  243.         print "Sorry! I can not draw because the dimension of your data is not 2!"  
  244.         return 1  
  245.   
  246.     # draw all samples  
  247.     for i in xrange(svm.numSamples):  
  248.         if svm.train_y[i] == -1:  
  249.             plt.plot(svm.train_x[i, 0], svm.train_x[i, 1], 'or')  
  250.         elif svm.train_y[i] == 1:  
  251.             plt.plot(svm.train_x[i, 0], svm.train_x[i, 1], 'ob')  
  252.   
  253.     # mark support vectors  
  254.     supportVectorsIndex = nonzero(svm.alphas.A > 0)[0]  
  255.     for i in supportVectorsIndex:  
  256.         plt.plot(svm.train_x[i, 0], svm.train_x[i, 1], 'oy')  
  257.       
  258.     # draw the classify line  
  259.     w = zeros((21))  
  260.     for i in supportVectorsIndex:  
  261.         w += multiply(svm.alphas[i] * svm.train_y[i], svm.train_x[i, :].T)   
  262.     min_x = min(svm.train_x[:, 0])[00]  
  263.     max_x = max(svm.train_x[:, 0])[00]  
  264.     y_min_x = float(-svm.b - w[0] * min_x) / w[1]  
  265.     y_max_x = float(-svm.b - w[0] * max_x) / w[1]  
  266.     plt.plot([min_x, max_x], [y_min_x, y_max_x], '-g')  
  267.     plt.show()  

       测试的数据来自这里。有100个样本,每个样本两维,最后是对应的标签,例如:

3.542485 1.977398          -1

3.018896 2.556416          -1

7.551510 -1.580030         1

2.114999 -0.004466         -1

……

       测试代码中首先加载这个数据库,然后用前面80个样本来训练,再用剩下的20个样本的测试,并显示训练后的模型和分类结果。测试代码如下:

test_SVM.py

[python] view plain copy
  1. #################################################  
  2. # SVM: support vector machine  
  3. # Author : zouxy  
  4. # Date   : 2013-12-12  
  5. # HomePage : http://blog.csdn.net/zouxy09  
  6. # Email  : [email protected]  
  7. #################################################  
  8.   
  9. from numpy import *  
  10. import SVM  
  11.   
  12. ################## test svm #####################  
  13. ## step 1: load data  
  14. print "step 1: load data..."  
  15. dataSet = []  
  16. labels = []  
  17. fileIn = open('E:/Python/Machine Learning in Action/testSet.txt')  
  18. for line in fileIn.readlines():  
  19.     lineArr = line.strip().split('\t')  
  20.     dataSet.append([float(lineArr[0]), float(lineArr[1])])  
  21.     labels.append(float(lineArr[2]))  
  22.   
  23. dataSet = mat(dataSet)  
  24. labels = mat(labels).T  
  25. train_x = dataSet[0:81, :]  
  26. train_y = labels[0:81, :]  
  27. test_x = dataSet[80:101, :]  
  28. test_y = labels[80:101, :]  
  29.   
  30. ## step 2: training...  
  31. print "step 2: training..."  
  32. C = 0.6  
  33. toler = 0.001  
  34. maxIter = 50  
  35. svmClassifier = SVM.trainSVM(train_x, train_y, C, toler, maxIter, kernelOption = ('linear'0))  
  36.   
  37. ## step 3: testing  
  38. print "step 3: testing..."  
  39. accuracy = SVM.testSVM(svmClassifier, test_x, test_y)  
  40.   
  41. ## step 4: show the result  
  42. print "step 4: show the result..."    
  43. print 'The classify accuracy is: %.3f%%' % (accuracy * 100)  
  44. SVM.showSVM(svmClassifier)  

运行结果如下:

[python] view plain copy
  1. step 1: load data...  
  2. step 2: training...  
  3. ---iter:0 entire set, alpha pairs changed:8  
  4. ---iter:1 non boundary, alpha pairs changed:7  
  5. ---iter:2 non boundary, alpha pairs changed:1  
  6. ---iter:3 non boundary, alpha pairs changed:0  
  7. ---iter:4 entire set, alpha pairs changed:0  
  8. Congratulations, training complete! Took 0.058000s!  
  9. step 3: testing...  
  10. step 4: show the result...  
  11. The classify accuracy is100.000%  

训练好的模型图:

 机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第10张图片

 

<span style="font-size:12px;">#coding=utf-8

'''
Created on Nov 4, 2010
Chapter 5 source file for Machine Learing in Action
@author: Peter


支持向量机SVM
序列最小优化算法  核函数

基于最大间隔分割数据
优点 泛化错误率低 计算开销不大 结果易于解释
缺点 对参数调节和核函数的选择敏感 原始分类器不加修改仅适用于处理二类问题
适用数据类型 数值型和标称型数据

数据线性可分  分割超平面

我们希望找到离分隔超平面最
这里点到分隔面的距离被称为间隔①( margin )。我
们希望间隔尽可能地大,这是因为如果我们犯错或者在有限数据上训练分类器的话,我们希望分类器尽可能健壮

支持向量(support, vector)就是离分隔超平面最近的那些点。接下来要试着最大化支持向量
到分隔面的距离,需要找到此问题的优化求解方法。

分类器求解的优化问题输入数据黑分类器会输出一个类别标签,相当于一个类似于sigmoid函数在作用。下面将使用类似海维赛德阶跃函数(即单位阶跃函数)的函数对
w^T*x+b作用得到f(w^T*x+b) 其中当u<0时f(u)输出-1,反之则输出+1

SVM一般流程如下:
O)收集数据:可以使用任意方法。
(2)准备数据:需要数值型数据。
(3)分析数据:有助于可视化分隔超平面。
(4)训练算法:SVM的大部分时间都源自训练,该过程主要实现两个参数的调优。
(5)测试算法:十分简单的计算过程就可以实现。
(6)使用算法:几乎所有分类问题都可以使用SVM,值得一提的是,SVM本身是一个二类
  分类器,对多类问题应用SVM需要对代码做一些修改。



SMO高效优化算法(序列最小优化)
Platt的SMO算法是将大优化问题分解为多个小优化问题来
求解的。这些小优化问题往往很容易求解,并且对它们进行顺序求解的结果与将它们作为整体来
求解的结果是完全一致的。在结果完全相同的同时,SMO算法的求解时间短很多。
    SMO算法的目标是求出一系列alpha和b,一旦求出了这些alpha,就很容易计算出权重向量w
并得到分隔超平面。
    SMO算法的工作原理是:每次循环中选择两个alpha进行优化处理。一旦找到一对合适的
alpha,那么就增大其中一个同时减小另一个。这里所谓的“合适”就是指两个alpha必须要符合
一定的条件,条件之一就是这两个alpha必须要在间隔边界之外,而其第二个条件则是这两个alpha
还没有进行过区间化处理或者不在边界上。


  核方法或者说核技巧会将数据(有时是非线性数据)从一个低维空间映射到一个高维空间,
可以将一个在低维空间中的非线性问题转换成高维空间下的线性问题来求解。核方法不止在SVM
中适用,还可以用于其他算法中。而其中的径向基函数是一个常用的度量两个向量距离的核函数。
    支持向量机是一个二类分类器。当用其解决多类问题时,则需要额外的方法对其进行扩展。
SVM的效果也对优化参数和所用核函数中的参数敏感。

'''
from numpy import *
from time import sleep
'''
由于改变一个alpha可能会导致该约束条件失效,因此我们总是同时改变两个alphao
 为此,我们将构建一个辅助函数,用于在某个区间范围内随机选择一个整数。同时,我们也
需要另一个辅助函数,用于在数值太大时对其进行调整。

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
'''
    下一个函数。electJrand()有两个参数值,其中i是第一个alpha的下标,m是所有alpha的数
目。只要函数值不等于输人值i,函数就会进行随机选择。
    最后一个辅助函数就是clipAlpha(),它是用于调整大于H或小于L的a扣ha值。尽管上述3
个辅助函数本身做的事情不多,但在分类器中却很有用处。

可以看得出来,这里采用的类别标签是一1和1,而不是0和to

'''
def selectJrand(i,m):   #i第一个alpha的下标  m所有alpha的数目,函数值不等于输入值i,函数随机选择
    j=i #we want to select any J not equal to i
    while (j==i):
        j = int(random.uniform(0,m))
    return j

def clipAlpha(aj,H,L):
    if aj > H: 
        aj = H
    if L > aj:
        aj = L
    return aj
'''
该SMO函数的伪代码大致如下:
创建一个alpha向量并将其初始化为0向量
当迭代次数小于最大迭代次数时(外循环)
    对数据集中的每个数据向量(内循环):
    如果该数据向量可以被优化:
        随机选择另外一个数据向量
        同时优化这两个向量
        如果两个向量都不能被优化,退出内循环
如果所有向量都没被优化,增加迭代数目,继续下一次循环

在Python中,如果某行以\符号结束,那
么就意味着该行语句没有结束并会在下一行延续。下面的代码当中有很多很长的语句必须要分成
多行来写。因此,下面的程序中使用了多个\符号。




    这个函数比较大,或许是我所知道的本书中最大的一个函数。该函数有5个输人参数,分别
是:数据集、类别标签、常数C、容错率和取消前最大的循环次数。在本书,我们构建函数时采
用了通用的接口,这样就可以对算法和数据源进行组合或配对处理。上述函数将多个列表和输人
参数转换成NumPy矩阵,这样就可以简化很多数学处理操作。由于转置了类别标签,因此我们得
到的就是一个列向量而不是列表。于是类别标签向量的每行元素都和数据矩阵中的行一一对应。
我们也可以通过矩阵dataMatIn的shape属性得到常数m和n。最后,我们就可以构建一个alpha列
矩阵,矩阵中元素都初始化为。,并建立一个iter变量。该变量存储的则是在没有任何alpha改变
的情况下遍历数据集的次数。当该变量达到输人值max工ter时,函数结束运行并退出。

    每次循环当中,将alphaPairsChanged先设为。,然后再对整个集合顺序遍历。变量
alphaPairsChanged用于记录alpha是否已经进行优化。当然,在循环结束时就会得知这一点。
首先,fXi能够计算出来,这就是我们预测的类别。然后,基于这个实例的预测结果和真实结果
的比对,就可以计算误差Ei。如果误差很大,那么可以对该数据实例所对应的alph植进行优化。
对该条件的测试处于上述程序清单的0处。在if语句中,不管是正间隔还是负间隔都会被测试。
并且在该if语句中,也要同时检查alpha值,以保证其不能等于0或C。由于后面alphai}“于0或大于
C时将被调整为。或C,所以一旦在该i晤句中它们等于这两个值的话,那么它们就已经在“边界”
上了,因而不再能够减小或增大,因此也就不值得再对它们进行优化了。’

    接下来,可以利用程序清单6-1中的辅助函数来随机选择第二个alpha值,即alpha [ j ].。
同样,可以采用第一个alpha (alpha [ i ] )的误差计算方法,来计算这个alpha值的误差。这个
过程可以通过c opy ( )的方法来实现,因此稍后可以将新的alph植与老的alph植进行比较。Python
则会通过引用的方式传递所有列表,所以必须明确地告知Python要为alpha工。1d和alphaJold
分配新的内存;否则的话,在对新值和旧值进行比较时,我们就看不到新旧值的变化。之后我们
开始计算L和H),它们用于将alpha[j]调整到。到C之间。如果L和H相等,就不做任何改变,直
接执行continue语句。这在Python中,则意味着本次循环结束直接运行下一次for的循环。
    Eta是alpha [ j]的最优修改量,在那洲反长的计算代码行中得到。如果eta为。,那就是说
需要退出for循环的当前迭代过程。该过程对真实SMO算法进行了简化处理。如果eta为0,那么
计算新的alpha[j]就比较麻烦了,这里我们就不对此进行详细的介绍了。有需要的读者可以阅
读Platt的原文来了解更多的细节。现实中,这种情况并不常发生,因此忽略这一部分通常也无伤
大雅。于是,可以计算出一个新的alpha[j],然后利用程序清单6-1中的辅助函数以及L与H值对
其进行调整。
    然后,就是需要检查alpha [ j]是否有轻微改变。如果是的话,就退出for循环。然后,
alpha[i]和alpha[j]同样进行改变,虽然改变的大小一样,但是改变的方向正好相反(即如果
一个增加,那么另外一个减少)0。在对alpha[i]和alpha[j]进行优化之后,给这两个alpha
值设置一个常数项b)o
    最后,在优化过程结束的同时,必须确保在合适的时机结束循环。如果程序执行到for循环
的最后一行都不执行continue语句,那么就已经成功地改变了一对alpha,同时可以增加
alphaPairsChanged的值。在for循环之外,需要检查alph植是否做了更新,如果有更新则将
iter设为0后继续运行程序。只有在所有数据集上遍历max工七er次,且不再发生任何alph谁改之
后,程序才会停止并退出while循环。


>>> alphas[alphas>0]
matrix([[ 0.13417013,  0.00448528,  0.21063476,  0.01164689,  0.36093706]])
由于SMO算法的随机性,读者运行后所得到的结果可能会与上述结果不同。
alphas[alphas>0〕命令是数组过滤(array filtering)的一个实例,而且它只又栩umPy类型有用,
却并不适用于Python中的正则表(regular list )。如果输人alpha>0,那么就会得到一个布尔数组,
并且在不等式成立的情况下,其对应值为正确的。于是,在将该布尔数组应用到原始的矩阵当中
时,就会得到一个NumPy矩阵,并且其中矩阵仅仅包含大于。的值。


支持向量机的个数:
>>> shape(alphas[alphas>0])
(1, 5)


了解哪些数据点支持向量,输入:
>>> for i in range(100):
	if alphas[i]>0.0:print dataArr[i],labelArr[i]

得到如下结果:	
[4.658191, 3.507396] -1.0
[3.223038, -0.552392] -1.0
[3.457096, -0.082216] -1.0
[2.893743, -1.643468] -1.0
[6.080573, 0.418886] 1.0

简化版SMO算法
'''
def smoSimple(dataMatIn, classLabels, C, toler, maxIter):  #五个输入函数,数据集 类别标签 常数C 容错率 取消前最大的循环次数
    dataMatrix = mat(dataMatIn); labelMat = mat(classLabels).transpose()
    b = 0; m,n = shape(dataMatrix)  #得到常数项n m
    alphas = mat(zeros((m,1)))  #构建alpha列矩阵,矩阵中元素都初始化为0,并建立一个iter变量
    iter = 0
    while (iter < maxIter):   #如果alpha可以更改 进入优化过程  当变量iter达到输入值maxItem时,函数结束运行并退出
        alphaPairsChanged = 0  #每次循环中 将alphaPairsChanged先设为0,然后再对整个结婚顺序遍历  alphaPairsChanged用于记录alpha是否已经进行优化
        for i in range(m):
            fXi = float(multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[i,:].T)) + b  #能够计算出来,这就是预测的类别
            Ei = fXi - float(labelMat[i])#if checks if an example violates KKT conditions   实例的预测结果与真是的预测结果之间的比对
            if ((labelMat[i]*Ei < -toler) and (alphas[i] < C)) or ((labelMat[i]*Ei > toler) and (alphas[i] > 0)):  #如果误差很大,那么可以对该数据实例所对应的alpha值进行优化  不管是正间隔还是负间隔都会被检测  同时检查alpha的值不能等于0 or C
                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]):  #保证alpha在0~c之间
                    L = max(0, alphas[j] - alphas[i])  #两个alpha值进行比较
                    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  #如果L 和 H的值相等 不做任何改变 直到continue
                eta = 2.0 * dataMatrix[i,:]*dataMatrix[j,:].T - dataMatrix[i,:]*dataMatrix[i,:].T - dataMatrix[j,:]*dataMatrix[j,:].T   #eta最优修改量  eta为0,退出for循环的当前迭代过程
                if eta >= 0: print "eta>=0"; continue
                alphas[j] -= labelMat[j]*(Ei - Ej)/eta
                alphas[j] = clipAlpha(alphas[j],H,L)  #检查是否有轻微改变,如果是的话,就退出for循环 然后alpha[i]和alpha[j]同样进行改变,改变的大小一样,方向相反
                if (abs(alphas[j] - alphaJold) < 0.00001): print "j not moving enough"; continue
                alphas[i] += labelMat[j]*labelMat[i]*(alphaJold - alphas[j])#update i by the same amount as j   对i进行修改,修改量与j相同但是方向相反
                                                                        #the update is in the oppostie direction
                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  #如果程序执行到for循环的最后一行都不执行continue语句,那么就已经成功地改变了一对alpha值,同事增加alphaPairsChanged的值
                print "iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)
        if (alphaPairsChanged == 0): iter += 1  #检查alpha是否做了更新,如果更新则将iter设为0后继续执行程序。遍历所有数据集maxIter次且不再发生任何alpha修改之后,停止并推出while循环
        else: iter = 0
        print "iteration number: %d" % iter
    return b,alphas
'''
    svlvl优化中一个特别好的地方就是,所有的运算都可以写成内积(inner product,也称点积:
的形式。向量的内积指的是两个向量相乘,之后得到单个标量或者数值。我们可以把内积运算替
换成核函数,而不必做简化处理。将内积替换成核函数的方式被称为核技巧(kernel trick)或者
核“变电”( kernel substation )

径向基函数:
径向基函数是SVM中常用的一个核函数。径向基函数是一个采用向量作为自变量的函数,能
够基于向量距离运算输出一个标量。这个距离可以是从<<0,0>向量或者其他向量开始计算的距离。

kTup是一个包含核函数信息的元组,待会儿我们就能看到它的作用
了。在初始化方法结束时,矩阵K先被构建,然后再通过调用函数kernelTrans()进行填充。全
局的K值只需计算一次。然后,当想要使用核函数时,就可以对它进行调用。这也省去了很多冗
余的计算开销。
    当计算矩阵K时,该过程多次调用了函数kernelTrans()。该函数有3个输人参数:2个数值
型变量和1个元组。元组 kTup给出的是核函数的信息。元组的第一个参数是描述所用核函数类型
的一个字符串,其他2个参数则都是核函数可能需要的可选参数。该函数首先构建出了一个列向
量,然后检查元组以确定核函数的类型。这里只给出了2种选择,但是依然可以很容易地通过添
加elif语句来扩展到更多选项。
    在线性核函数的情况下,内积计算在“所有数据集”和“数据集中的一行”这两个输人之间
展开。在径向基核函数的情况下,在for循环中对于矩阵的每个元素计算高斯函数的值。而在for
循环结束之后,我们将计算过程应用到整个向量上去。值得一提的是,在NumPy矩阵中,除法符
号意味着对矩阵元素展开计算而不像在MATLAB中一样计算矩阵的逆
    最后,如果遇到一个无法识别的元组,程序就会抛出异常,因为在这种情况下不希望程序再
继续运行,这一点相当重要。


核转换函数
'''
def kernelTrans(X, A, kTup): #calc the kernel or transform data to a higher dimensional space  X,A数值型变量 kTup元祖核函数的信息 元祖第一个参数描述核函数类型的一个字符串  其他两个参数都是核函数可能需要的可选参数
    m,n = shape(X)
    K = mat(zeros((m,1)))
    if kTup[0]=='lin': K = X * A.T   #linear kernel
    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)) #divide in NumPy is element-wise not matrix like Matlab 元素间作除法
    else: raise NameError('Houston We Have a Problem -- \
    That Kernel is not recognized')  #遇到无法是别的元祖 抛出异常
    return K


'''
利用完整Platt SMO算法加速优化
    在几百个点组成的小规模数据集上,简化版SMO算法的运行是没有什么问题的,但是在更大
的数据集上的运行速度就会变慢。刚才已经讨论了简化版SMO算法,下面我们就讨论完整版的
Platt SMO算法。在这两个版本中,实现alpha的更改和代数运算的优化环节一模一样。在优化过
程中,唯一的不同就是选择alpha的方式。完整版的Platt SMO算法应用了一些能够提速的启发方fa

    Platt SMO算法是通过一个外循环来选择第一个a如h植的,并且其选择过程会在两种方式之
间进行交替:一种方式是在所有数据集上进行单遍扫描,另一种方式则是在非边界alpha中实现单
遍扫描。而所谓非边界alpha指的就是那些不等于边界。或C的alpha值。对整个数据集的扫描相当
容易,而实现非边界alpha值的扫描时,首先需要建立这些alpha值的列表,然后再对这个表进行
遍历。同时,该步骤会跳过那些已知的不会改变的a扣ha值。
    在选择第一个alpha值后,算法会通过一个内循环来选择第二个alpha值。在优化过程中,会
通过最大化步长的方式来获得第二个alpha值。在简化版SMO算法中,我们会在选择j之后计算错
误率Ej。但在这里,我们会建立一个全局的缓存用于保存误差值,并从中选择使得步长或者说
Ei一Ej最大的alphas 值


    首要的事情就是建立一个数据结构来保存所有的重要值,而这个过程可以通过一个对象来完
成。这里使用对象的目的并不是为了面向对象的编程,而只是作为一个数据结构来使用对象。在
将值传给函数时,我们可以通过将所有数据移到一个结构中来实现,这样就可以省掉手工输人的
麻烦了。而此时,数据就可以通过一个对象来进行传递。实际上,当完成其实现时,可以很容易
通过Python的字典来完成。但是在访问对象成员变量时,这样做会有更多的手工输人操作,对比
一下myObject.X和myObject['X']就可以知道这一点。为达到这个目的,需要构建一个仅包含
ini七方法的。ptStruc七类。该方法可以实现其成员变量的填充。除了增加了一个mx2的矩阵成
员变量eCache之外.,这些做法和简化版SMO一模一样。eCache的第一列给出的是eCache是
否有效的标志位,而第二列给出的是实际的E值。
    对于给定的alpha,第一个辅助函数calcEk()能够计算E值并返回。以前,该过程是采用内嵌
的方式来完成的,但是由于该过程在这个版本的SMO算法中出现频繁,这里必须要将其单别I拎出来。


SMO算法是通过一个外循环来选择第一个alpha值的,并且其选择过程会在两种方式间进行交替
一种方式是在所有数据集上进行单遍扫描 另一种方式是在非边界alpha上进行一遍扫描

所谓的非边界alpha指的是那些不等于0或者C的alpha值  对整个数据集的扫描相当容易,而实现非边界alpha扫描时
首先需要建立这些alpha值的列表,然后再对这个表进行遍历  同时 该步骤会跳过那些已知的不会改变的alpha值


在选择第一个alpha值后,算法会通过一个内循环来选择第二个alpha。在优化过程中,会通过最大化步长的
的方式获得第二个alpha值。建立一个全局的缓存,用于保存误差值  并从中选择时的步长(Ei-Ej)最大的值
'''
class optStruct:   #建立一个数据结构保存所有重要值
    def __init__(self,dataMatIn, classLabels, C, toler, kTup):  # Initialize the structure with the parameters   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))) #first column is valid flag  误差缓存
        self.K = mat(zeros((self.m,self.m)))  #构建空矩阵K 之后调用kernelTrans函数进行填充  全局K只需要计算一次
        for i in range(self.m):
            self.K[:,i] = kernelTrans(self.X, self.X[i,:], kTup)
        
def calcEk(oS, k):  #计算E值并返回
    fXk = float(multiply(oS.alphas,oS.labelMat).T*oS.K[:,k] + oS.b)
    Ek = fXk - float(oS.labelMat[k])
    return Ek


'''
    下一个函数selec七J()用于选择第二个alpha或者说内循环的alpha值4。回想一下,这里的
目标是选择合适的第二个alpha值以保证在每次优化中采用最大步长。该函数的误差值与第一个
alpha}Ei和下标i有关。首先将输人值Ei在缓存中设置成为有效的。这里的有效(valid)意味着
它已经计算好了。在eCache中,代码nonzero(oS.eCache[:,0].A)[0]构建出了一个非零表。
NumPy函数nonzero()返回了一个列表,而这个列表中包含以输人列表为目录的列表值,当然
读者可以猜得到,这里的值并非零。nonzero()语句返回的是非零E值所对应的alpha值,而不是
E值本身。程序会在所有的值上进行循环并选择其中使得改变最大的那个值O。如果这是第一次
循环的话,那么就随机选择一个alpha值。当然,也存在有许多更复杂的方式来处理第一次循环的
I清况,而上述做法就能够满足我们的目的。
    程序清单6-3的最后一个辅助函数是updateEk(),它会计算误差值并存人缓存当中。在对
alpha进行优化之后会用到这个值。
    程序清单6-3中的代码本身的作用并不大,但是当和优化过程及外循环组合在一起时,就能
组成强大的SMO算法。
'''        
def selectJ(i, oS, Ei):         #this is the second choice -heurstic, and calcs Ej  内循环的启发式方法  选择第二个alpha(内循环的alpha值)
    maxK = -1; maxDeltaE = 0; Ej = 0
    oS.eCache[i] = [1,Ei]  #set valid #choose the alpha that gives the maximum delta E
    validEcacheList = nonzero(oS.eCache[:,0].A)[0]  #构建一个非0表  返回一个列表  包含以输入列表为目录的列表值  返回非0  E值对应的alpha值
    if (len(validEcacheList)) > 1:
        for k in validEcacheList:   #loop through valid Ecache values and find the one that maximizes delta E
            if k == i: continue #don't calc for i, waste of time
            Ek = calcEk(oS, k)
            deltaE = abs(Ei - Ek)
            if (deltaE > maxDeltaE):
                maxK = k; maxDeltaE = deltaE; Ej = Ek  #选择具有最大步长的j值  对所有值进行循环并获得改变最大 的那个值   第一次循环随机选取alpha值
        return maxK, Ej
    else:   #in this case (first time around) we don't have any valid eCache values
        j = selectJrand(i, oS.m)
        Ej = calcEk(oS, j)
    return j, Ej

def updateEk(oS, k):#after any alpha has changed update the new value in the cache  计算误差值并存入缓存中,对alpha值进行优化之后用到这个值
    Ek = calcEk(oS, k)
    oS.eCache[k] = [1,Ek]



'''
完整的Platt SMO算法中的优化例程
    程序清单6-4中的代码几乎和程序清单6-2中给出的smoSimple()函数一模一样,但是这里的
代码已经使用了自己的数据结构。该结构在参数。S中传递。第二个重要的修改就是使用程序清单
6-3中的SelectJ()而不是selectJrand()来选择第二个alpha的值.。最后,在alpha值改变时
更新Ecache)。
'''        
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) #this has been changed from selectJrand  第二个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.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
        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) #added this for the Ecache  更新误差缓存
        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])#update i by the same amount as j
        updateEk(oS, i) #added this for the Ecache  更新误差缓存                  #the update is in the oppostie direction
        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
'''
完整版Platt SMO的外循环代码
    程序清单6-5给出的是完整版的Platt SMO算法,其输人和函数smoSimple()完全一样。函数
一开始构建一个数据结构来容纳所有的数据,然后需要对控制函数退出的一些变量进行初始化。
整个代码的主体是while循环,这与。moSimple()有些类似,但是这里的循环退出条件更多一些。
当迭代次数超过指定的最大值,或者遍历整个集合都未对任意alpha对进行修改时,就退出循环。
这里的max工七er变量和函数smoSimple()中的作用有一点不同,后者当没有任何alpha发生改变
时会将整个集合的一次遍历过程计成一次迭代,而这里的一次迭代定义为一次循环过程,而不管
该循环具体做了什么事。此时,如果在优化过程中存在波动就会停止,因此这里的做法优于
smoSimple()函数中的计数方法。
    while循环的内部与smoSimple()中有所不同,一开始的for循环在数据集上遍历任意可能
的alpha0。我们通过调用innerL()来选择第二个alpha,并在可能时对其进行优化处理。如果有
任意一对alpha值发生改变,那么会返回1。第二个for循环遍历所有的非边界alpha值,也就是不
在边界0或C上的值.。
    接下来,我们对for循环在非边界循环和完整遍历之间进行切换,并打印出迭代次数。最后
程序将会返回常数b和alpha值。



'''
def smoP(dataMatIn, classLabels, C, toler, maxIter,kTup=('lin', 0)):    #full Platt SMO
    oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler, kTup)
    iter = 0
    entireSet = True; alphaPairsChanged = 0
    while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):  #迭代次数超过指定的最大值或者遍历整个集合都为对任意alpha进行修改时 就退出循环
        alphaPairsChanged = 0
        if entireSet:   #go over all  遍历所有的值
            for i in range(oS.m):        
                alphaPairsChanged += innerL(i,oS)  #选择第二个alpha并在可能时对其进行优化处理  如果有任意一对alpha值发生变化 返回1
                print "fullSet, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)
            iter += 1
        else:#go over non-bound (railed) alphas  遍历非边界值 不在边界0或C上的值
            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 #toggle entire set loop
        elif (alphaPairsChanged == 0): entireSet = True  
        print "iteration number: %d" % iter
    return oS.b,oS.alphas
'''
>>> datMat = mat(dataArr)
>>> datMat[0]*mat(ws)+b
matrix([[-0.94421679]])

如果该值大于0,那么其属于1类;如果该值小于0,那么则属于-1类。
对于数据点0,应该得到的类别标签是-1。可以通过如下命令来确认分类结果的正确性

    上述代码只有一个可选的输人参数,该输人参数是高斯径向基函数中的一个用户定义变量。
整个代码主要是由以前定义的函数集合构成的。首先,程序从文件中读人数据集,然后在该数据
集上运行Platt SMO算法,其中核函数的类型为·rbf'o
    优化过程结束后,在后面的矩阵数学运算中建立了数据的矩阵副本,并且找出那些非零的
alpha值,从而得到所需要的支持向量;同时,也就得到了这些支持向量和alpha的类别标签值。
这些值仅仅是需要分类的值。
    整个代码中最重要的是for循环开始的那两行,它们给出了如何利用核函数进行分类。首先
利用结构初始化方法中使用过的kernelTrans()函数,得到转换后的数据。然后,再用其与前
面的alpha及类别标签值求积。其中需要特别注意的另一件事是,在这几行代码中,是如何做到只
需要支持向量数据就可以进行分类的。除此之外,其他数据都可以直接舍弃。
    与第一个for循环相比,第二个for循环仅仅只有数据集不同,后者采用的是测试数据集。
读者可以比较不同的设置在测试集和训练集上表现出的性能。

对数据进行分类处理
>>> ws = svmMLiA.calcWs(alpha,dataArr,labelArr)
>>> ws
array([[ 0.65307162],
       [-0.17196128]])
>>> datMat = mat(dataArr)
>>> datMat[0]*mat(ws)+b
matrix([[-0.92555695]])

如果该值大于0,那么其属于1;如果该值小鱼0,那么则属于-1
对于数据0 应该得到的类别标签是-1
'''
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
'''
这时观察一下函数testRbf()
的输出结果就会发现,此时的测试错误率也在下降。该数据集在这个设置的某处存在着最优值。
如果降低。,那么训练错误率就会降低,但是测试错误率却会上升。
    支持向量的数目存在一个最优值。SVM的优点在于它能对数据进行高效分类。如果支持向量
太少,就可能会得到一个很差的决策边界(下个例子会说明这一点);如果支持向量太多,也就
相当于每次都利用整个数据集进行分类,这种分类方法称为k近邻。


利用核函数进行分类的径向基测试函数
'''
def testRbf(k1=1.3):  #k1 可选参数  高斯径向基函数中的一个用户定义变量
    dataArr,labelArr = loadDataSet('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] #get matrix of only support vectors  构建支持向量函数
    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))  #调用kernelTrans函数进行结构初始化函数,得到转换后的数据
        predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b  #用得到的数据与前面的alpha及类别标签值求积
        if sign(predict)!=sign(labelArr[i]): errorCount += 1
    print "the training error rate is: %f" % (float(errorCount)/m)
    dataArr,labelArr = loadDataSet('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)    
    
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


'''
基于SVM的数字识别:

(1)收集数据:提供的文本文件。
(2)准备数据:基于二值图像构造向量。
(3)分析数据:对图像向量进行目测。
(4)训练算法:采用两种不同的核函数,并对径向基核函数采用不同的设置来运行SMO算法。
(5)测试算法:编写一个函数来测试不同的核函数并计算错误率。
(6)使用算法:一个图像识别的完整应用还需要一些图像处理的知识,这里并不打算深入介绍。


基于SVM的手写数字识别
'''
def loadImages(dirName):
    from os import listdir
    hwLabels = []
    trainingFileList = listdir(dirName)           #load the training set
    m = len(trainingFileList)
    trainingMat = zeros((m,1024))
    for i in range(m):
        fileNameStr = trainingFileList[i]
        fileStr = fileNameStr.split('.')[0]     #take off .txt
        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    
'''
    下一个函数七estDigi七。()并不是全新的函数,它和testRbf()的代码几乎一样,唯一的大
区别就是它调用了load工mages()函数来获得类别标签和数据。另一个细小的不同是现在这里的
函数元组kTup是输人参数,而在testRbf()中默认的就是使用rb唯函数。如果对于函数
七estDigi七。()不增加任何输人参数的话,那么kTup的默认值就是('rbf ,10)0

    你可能注意到了一个有趣的现象,即最小的训练错误率并不对应于最小的支持向量数目。另
一个值得注意的就是,线性核函数的效果并不是特别的糟糕。可以以牺牲线性核函数的错误率来
换取分类速度的提高。尽管这一点在实际中是可以接受的,但是还得取决于具体的应用。

'''
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 "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('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) 


'''#######********************************
Non-Kernel VErsions below
'''#######********************************

class optStructK:
    def __init__(self,dataMatIn, classLabels, C, toler):  # Initialize the structure with the parameters 
        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))) #first column is valid flag
        
def calcEkK(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 selectJK(i, oS, Ei):         #this is the second choice -heurstic, and calcs Ej
    maxK = -1; maxDeltaE = 0; Ej = 0
    oS.eCache[i] = [1,Ei]  #set valid #choose the alpha that gives the maximum delta E
    validEcacheList = nonzero(oS.eCache[:,0].A)[0]
    if (len(validEcacheList)) > 1:
        for k in validEcacheList:   #loop through valid Ecache values and find the one that maximizes delta E
            if k == i: continue #don't calc for i, waste of time
            Ek = calcEk(oS, k)
            deltaE = abs(Ei - Ek)
            if (deltaE > maxDeltaE):
                maxK = k; maxDeltaE = deltaE; Ej = Ek
        return maxK, Ej
    else:   #in this case (first time around) we don't have any valid eCache values
        j = selectJrand(i, oS.m)
        Ej = calcEk(oS, j)
    return j, Ej

def updateEkK(oS, k):#after any alpha has changed update the new value in the cache
    Ek = calcEk(oS, k)
    oS.eCache[k] = [1,Ek]
        
def innerLK(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) #this has been changed from selectJrand
        alphaIold = oS.alphas[i].copy(); alphaJold = oS.alphas[j].copy();
        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
        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) #added this for the Ecache
        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])#update i by the same amount as j
        updateEk(oS, i) #added this for the Ecache                    #the update is in the oppostie direction
        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

def smoPK(dataMatIn, classLabels, C, toler, maxIter):    #full Platt SMO
    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:   #go over all
            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:#go over non-bound (railed) alphas
            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 #toggle entire set loop
        elif (alphaPairsChanged == 0): entireSet = True  
        print "iteration number: %d" % iter
    return oS.b,oS.alphas
</span>

九、参考文献与推荐阅读

[1] JerryLead的博客,作者根据斯坦福的讲义给出了流畅和通俗的推导:SVM系列。

[2]嘉士伯的SVM入门系列,讲得很好。

[3] pluskid的支持向量机系列,非常好。其中关于dual问题推导非常赞。

[4] Leo Zhang的SVM学习系列,博客中还包含了很多其他的机器学习算法。

[5] v_july_v的支持向量机通俗导论(理解SVM的三层境界)。结构之法算法之道blog。

[6] 李航的《统计学习方法》,清华大学出版社

[7] SVM学习——Sequential Minimal Optimization

[8] SVM算法实现(一)

[9] Sequential Minimal Optimization: A FastAlgorithm for Training Support Vector Machines

[10] SVM --从“原理”到实现

[11] 支持向量机入门系列

[12]SVM的各个版本及其多种语言实现代码合集

[13] Karush-Kuhn-Tucker(KKT) conditions

[14] 深入理解拉格朗日乘子法(Lagrange Multiplier) 和KKT条件

 

SVM入门知识总结:

(一)SVM的简介

支持向量机(Support Vector Machine)是Cortes和Vapnik于1995年首先提出的,它在解决小样本、非线性及高维模式识别中表现出许多特有的优势,并能够推广应用到函数拟合等其他机器学习问题中[10]。 
支持向量机方法是建立在统计学习理论的VC 维理论和结构风险最小原理基础上的,根据有限的样本信息在模型的复杂性(即对特定训练样本的学习精度,Accuracy)和学习能力(即无错误地识别任意样本的能力)之间寻求最佳折衷,以期获得最好的推广能力[14](或称泛化能力)。

以上是经常被有关SVM 的学术文献引用的介绍,我来逐一分解并解释一下。

Vapnik是统计机器学习的大牛,这想必都不用说,他出版的《Statistical Learning Theory》是一本完整阐述统计机器学习思想的名著。在该书中详细的论证了统计机器学习之所以区别于传统机器学习的本质,就在于统计机器学习能够精确的给出学习效果,能够解答需要的样本数等等一系列问题。与统计机器学习的精密思维相比,传统的机器学习基本上属于摸着石头过河,用传统的机器学习方法构造分类系统完全成了一种技巧,一个人做的结果可能很好,另一个人差不多的方法做出来却很差,缺乏指导和原则。

所谓VC维是对函数类的一种度量,可以简单的理解为问题的复杂程度,VC维越高,一个问题就越复杂。正是因为SVM关注的是VC维,后面我们可以看到,SVM解决问题的时候,和样本的维数是无关的(甚至样本是上万维的都可以,这使得SVM很适合用来解决文本分类的问题,当然,有这样的能力也因为引入了核函数)。

结构风险最小听上去文绉绉,其实说的也无非是下面这回事。

机器学习本质上就是一种对问题真实模型的逼近(我们选择一个我们认为比较好的近似模型,这个近似模型就叫做一个假设),但毫无疑问,真实模型一定是不知道的(如果知道了,我们干吗还要机器学习?直接用真实模型解决问题不就可以了?对吧,哈哈)既然真实模型不知道,那么我们选择的假设与问题真实解之间究竟有多大差距,我们就没法得知。比如说我们认为宇宙诞生于150亿年前的一场大爆炸,这个假设能够描述很多我们观察到的现象,但它与真实的宇宙模型之间还相差多少?谁也说不清,因为我们压根就不知道真实的宇宙模型到底是什么。

这个与问题真实解之间的误差,就叫做风险(更严格的说,误差的累积叫做风险)。我们选择了一个假设之后(更直观点说,我们得到了一个分类器以后),真实误差无从得知,但我们可以用某些可以掌握的量来逼近它。最直观的想法就是使用分类器在样本数据上的分类的结果与真实结果(因为样本是已经标注过的数据,是准确的数据)之间的差值来表示。这个差值叫做经验风险Remp(w)。以前的机器学习方法都把经验风险最小化作为努力的目标,但后来发现很多分类函数能够在样本集上轻易达到100%的正确率,在真实分类时却一塌糊涂(即所谓的推广能力差,或泛化能力差)。此时的情况便是选择了一个足够复杂的分类函数(它的VC维很高),能够精确的记住每一个样本,但对样本之外的数据一律分类错误。回头看看经验风险最小化原则我们就会发现,此原则适用的大前提是经验风险要确实能够逼近真实风险才行(行话叫一致),但实际上能逼近么?答案是不能,因为样本数相对于现实世界要分类的文本数来说简直九牛一毛,经验风险最小化原则只在这占很小比例的样本上做到没有误差,当然不能保证在更大比例的真实文本上也没有误差。

统计学习因此而引入了泛化误差界的概念,就是指真实风险应该由两部分内容刻画,一是经验风险,代表了分类器在给定样本上的误差;二是置信风险,代表了我们在多大程度上可以信任分类器在未知文本上分类的结果。很显然,第二部分是没有办法精确计算的,因此只能给出一个估计的区间,也使得整个误差只能计算上界,而无法计算准确的值(所以叫做泛化误差界,而不叫泛化误差)。

置信风险与两个量有关,一是样本数量,显然给定的样本数量越大,我们的学习结果越有可能正确,此时置信风险越小;二是分类函数的VC维,显然VC维越大,推广能力越差,置信风险会变大。

泛化误差界的公式为:

R(w)≤Remp(w)+Ф(n/h)

公式中R(w)就是真实风险,Remp(w)就是经验风险,Ф(n/h)就是置信风险。统计学习的目标从经验风险最小化变为了寻求经验风险与置信风险的和最小,即结构风险最小。

SVM正是这样一种努力最小化结构风险的算法。

SVM其他的特点就比较容易理解了。

小样本,并不是说样本的绝对数量少(实际上,对任何算法来说,更多的样本几乎总是能带来更好的效果),而是说与问题的复杂度比起来,SVM算法要求的样本数是相对比较少的。

非线性,是指SVM擅长应付样本数据线性不可分的情况,主要通过松弛变量(也有人叫惩罚变量)和核函数技术来实现,这一部分是SVM的精髓,以后会详细讨论。多说一句,关于文本分类这个问题究竟是不是线性可分的,尚没有定论,因此不能简单的认为它是线性可分的而作简化处理,在水落石出之前,只好先当它是线性不可分的(反正线性可分也不过是线性不可分的一种特例而已,我们向来不怕方法过于通用)。

高维模式识别是指样本维数很高,例如文本的向量表示,如果没有经过另一系列文章(《文本分类入门》)中提到过的降维处理,出现几万维的情况很正常,其他算法基本就没有能力应付了,SVM却可以,主要是因为SVM 产生的分类器很简洁,用到的样本信息很少(仅仅用到那些称之为“支持向量”的样本,此为后话),使得即使样本维数很高,也不会给存储和计算带来大麻烦(相对照而言,kNN算法在分类时就要用到所有样本,样本数巨大,每个样本维数再一高,这日子就没法过了……)。

下一节开始正式讨论SVM。别嫌我说得太详细哦。

SVM入门(二)线性分类器Part 1

线性分类器(一定意义上,也可以叫做感知机) 是最简单也很有效的分类器形式.在一个线性分类器中,可以看到SVM形成的思路,并接触很多SVM的核心概念.

用一个二维空间里仅有两类样本的分类问题来举个小例子。如图所示

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第11张图片

C1和C2是要区分的两个类别,在二维平面中它们的样本如上图所示。中间的直线就是一个分类函数,它可以将两类样本完全分开。一般的,如果一个线性函数能够将样本完全正确的分开,就称这些数据是线性可分的,否则称为非线性可分的。

什么叫线性函数呢?在一维空间里就是一个点,在二维空间里就是一条直线,三维空间里就是一个平面,可以如此想象下去,如果不关注空间的维数,这种线性函数还有一个统一的名称——超平面(Hyper Plane)!

实际上,一个线性函数是一个实值函数(即函数的值是连续的实数),而我们的分类问题(例如这里的二元分类问题——回答一个样本属于还是不属于一个类别的问题)需要离散的输出值,例如用1表示某个样本属于类别C1,而用0表示不属于(不属于C1也就意味着属于C2),这时候只需要简单的在实值函数的基础上附加一个阈值即可,通过分类函数执行时得到的值大于还是小于这个阈值来确定类别归属。 例如我们有一个线性函数

g(x)=wx+b

【看到好多人都在问g(x)=0 和 g(x)的问题,我在这里帮楼主补充一下:g(x)实际是以w为法向量的一簇超平面,在二维空间表示为一簇直线(就是一簇平行线,他们的法向量都是w),而g(x)=0只是这么多平行线中的一条。】

我们可以取阈值为0,这样当有一个样本xi需要判别的时候,我们就看g(xi)的值。若g(xi)>0,就判别为类别C1,若g(xi)<0,则判别为类别C2(等于的时候我们就拒绝判断,呵呵)。此时也等价于给函数g(x)附加一个符号函数sgn(),即f(x)=sgn [g(x)]是我们真正的判别函数。

关于g(x)=wx+b这个表达式要注意三点:一,式中的x不是二维坐标系中的横轴,而是样本的向量表示,例如一个样本点的坐标是(3,8),则xT=(3,8) ,而不是x=3(一般说向量都是说列向量,因此以行向量形式来表示时,就加上转置)。二,这个形式并不局限于二维的情况,在n维空间中仍然可以使用这个表达式,只是式中的w成为了n维向量(在二维的这个例子中,w是二维向量,为了表示起来方便简洁,以下均不区别列向量和它的转置,聪明的读者一看便知);三,g(x)不是中间那条直线的表达式,中间那条直线的表达式是g(x)=0,即wx+b=0,我们也把这个函数叫做分类面。

实际上很容易看出来,中间那条分界线并不是唯一的,我们把它稍微旋转一下,只要不把两类数据分错,仍然可以达到上面说的效果,稍微平移一下,也可以。此时就牵涉到一个问题,对同一个问题存在多个分类函数的时候,哪一个函数更好呢?显然必须要先找一个指标来量化“好”的程度,通常使用的都是叫做“分类间隔”的指标。下一节我们就仔细说说分类间隔,也补一补相关的数学知识。

SVM入门(三)线性分类器Part 2

上回说到对于文本分类这样的不适定问题(有一个以上解的问题称为不适定问题),需要有一个指标来衡量解决方案(即我们通过训练建立的分类模型)的好坏,而分类间隔是一个比较好的指标。

在进行文本分类的时候,我们可以让计算机这样来看待我们提供给它的训练样本,每一个样本由一个向量(就是那些文本特征所组成的向量)和一个标记(标示出这个样本属于哪个类别)组成。如下:

Di=(xi,yi)

xi就是文本向量(维数很高),yi就是分类标记。

在二元的线性分类中,这个表示分类的标记只有两个值,1和-1(用来表示属于还是不属于这个类)。有了这种表示法,我们就可以定义一个样本点到某个超平面的间隔:

δi=yi(wxi+b)

这个公式乍一看没什么神秘的,也说不出什么道理,只是个定义而已,但我们做做变换,就能看出一些有意思的东西。

首先注意到如果某个样本属于该类别的话,那么wxi+b>0(记得么?这是因为我们所选的g(x)=wx+b就通过大于0还是小于0来判断分类),而yi也大于0;若不属于该类别的话,那么wxi+b<0,而yi也小于0,这意味着yi(wxi+b)总是大于0的,而且它的值就等于|wxi+b|!(也就是|g(xi)|)

现在把w和b进行一下归一化,即用w/||w||和b/||w||分别代替原来的w和b,那么间隔就可以写成

【点到直线的距离,做解析几何中为:  
D = (Ax + By + c) /sqrt(A^2+B^2)  
sqrt(A^2+B^2)就相当于||W||, 其中向量W=[A, B];  
(Ax + By + c)就相当于g(X), 其中向量X=[x,y]。】

这个公式是不是看上去有点眼熟?没错,这不就是解析几何中点xi到直线g(x)=0的距离公式嘛!(推广一下,是到超平面g(x)=0的距离, g(x)=0就是上节中提到的分类超平面)

小Tips:||w||是什么符号?||w||叫做向量w的范数,范数是对向量长度的一种度量。我们常说的向量长度其实指的是它的2-范数,范数最一般的表示形式为p-范数,可以写成如下表达式

    向量w=(w1, w2, w3,…… wn)

它的p-范数为

看看把p换成2的时候,不就是传统的向量长度么?当我们不指明p的时候,就像||w||这样使用时,就意味着我们不关心p的值,用几范数都可以;或者上文已经提到了p的值,为了叙述方便不再重复指明。

当用归一化的w和b代替原值之后的间隔有一个专门的名称,叫做几何间隔,几何间隔所表示的正是点到超平面的欧氏距离,我们下面就简称几何间隔为“距离”。以上是单个点到某个超平面的距离(就是间隔,后面不再区别这两个词)定义,同样可以定义一个点的集合(就是一组样本)到某个超平面的距离为此集合中离超平面最近的点的距离。下面这张图更加直观的展示出了几何间隔的现实含义:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第12张图片

H是分类面,而H1和H2是平行于H,且过离H最近的两类样本的直线,H1与H,H2与H之间的距离就是几何间隔。

之所以如此关心几何间隔这个东西,是因为几何间隔与样本的误分次数间存在关系:

其中的δ是样本集合到分类面的间隔,R=max ||xi||  i=1,...,n,即R是所有样本中(xi是以向量表示的第i个样本)向量长度最长的值(也就是说代表样本的分布有多么广)。先不必追究误分次数的具体定义和推导过程,只要记得这个误分次数一定程度上代表分类器的误差。而从上式可以看出,误分次数的上界由几何间隔决定!(当然,是样本已知的时候)

至此我们就明白为何要选择几何间隔来作为评价一个解优劣的指标了,原来几何间隔越大的解,它的误差上界越小。因此最大化几何间隔成了我们训练阶段的目标,而且,与二把刀作者所写的不同,最大化分类间隔并不是SVM的专利,而是早在线性分类时期就已有的思想。

SVM入门(四)线性分类器的求解——问题的描述Part1

上节说到我们有了一个线性分类函数,也有了判断解优劣的标准——即有了优化的目标,这个目标就是最大化几何间隔,但是看过一些关于SVM的论文的人一定记得什么优化的目标是要最小化||w||这样的说法,这是怎么回事呢?回头再看看我们对间隔和几何间隔的定义:

间隔:δ=y(wx+b)=|g(x)|

几何间隔:

可以看出δ=||w||δ几何。注意到几何间隔与||w||是成反比的,因此最大化几何间隔与最小化||w||完全是一回事。而我们常用的方法并不是固定||w||的大小而寻求最大几何间隔,而是固定间隔(例如固定为1),寻找最小的||w||。

而凡是求一个函数的最小值(或最大值)的问题都可以称为寻优问题(也叫作一个规划问题),又由于找最大值的问题总可以通过加一个负号变为找最小值的问题,因此我们下面讨论的时候都针对找最小值的过程来进行。一个寻优问题最重要的部分是目标函数,顾名思义,就是指寻优的目标。例如我们想寻找最小的||w||这件事,就可以用下面的式子表示:

但实际上对于这个目标,我们常常使用另一个完全等价的目标函数来代替,那就是:

(式1)

不难看出当||w||2达到最小时,||w||也达到最小,反之亦然(前提当然是||w||描述的是向量的长度,因而是非负的)。之所以采用这种形式,是因为后面的求解过程会对目标函数作一系列变换,而式(1)的形式会使变换后的形式更为简洁(正如聪明的读者所料,添加的系数二分之一和平方,皆是为求导数所需)。

接下来我们自然会问的就是,这个式子是否就描述了我们的问题呢?(回想一下,我们的问题是有一堆点,可以被分成两类,我们要找出最好的分类面)

如果直接来解这个求最小值问题,很容易看出当||w||=0的时候就得到了目标函数的最小值。但是你也会发现,无论你给什么样的数据,都是这个解!反映在图中,就是H1与H2两条直线间的距离无限大,这个时候,所有的样本点(无论正样本还是负样本)都跑到了H1和H2中间,而我们原本的意图是,H1右侧的被分为正类,H2 左侧的被分为负类,位于两类中间的样本则拒绝分类(拒绝分类的另一种理解是分给哪一类都有道理,因而分给哪一类也都没有道理)。这下可好,所有样本点都进入了无法分类的灰色地带。

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第13张图片

造成这种结果的原因是在描述问题的时候只考虑了目标,而没有加入约束条件,约束条件就是在求解过程中必须满足的条件,体现在我们的问题中就是样本点必须在H1或H2的某一侧(或者至少在H1和H2上),而不能跑到两者中间。我们前文提到过把间隔固定为1,这是指把所有样本点中间隔最小的那一点的间隔定为1(这也是集合的间隔的定义,有点绕嘴),也就意味着集合中的其他点间隔都不会小于1,按照间隔的定义,满足这些条件就相当于让下面的式子总是成立:

    yi[(w·xi)+b]≥1 (i=1,2,…,l) (l是总的样本数)

但我们常常习惯让式子的值和0比较,因而经常用变换过的形式:

    yi[(w·xi)+b]-1≥0 (i=1,2,…,l) (l是总的样本数)

因此我们的两类分类问题也被我们转化成了它的数学形式,一个带约束的最小值的问题:

下一节我们从最一般的意义上看看一个求最小值的问题有何特征,以及如何来解。

SVM入门(五)线性分类器的求解——问题的描述Part2

从最一般的定义上说,一个求最小值的问题就是一个优化问题(也叫寻优问题,更文绉绉的叫法是规划——Programming),它同样由两部分组成,目标函数和约束条件,可以用下面的式子表示:

(式1)

约束条件用函数c来表示,就是constrain的意思啦。你可以看出一共有p+q个约束条件,其中p个是不等式约束,q个等式约束。

关于这个式子可以这样来理解:式中的x是自变量,但不限定它的维数必须为1(视乎你解决的问题空间维数,对我们的文本分类来说,那可是成千上万啊)。要求f(x)在哪一点上取得最小值(反倒不太关心这个最小值到底是多少,关键是哪一点),但不是在整个空间里找,而是在约束条件所划定的一个有限的空间里找,这个有限的空间就是优化理论里所说的可行域。注意可行域中的每一个点都要求满足所有p+q个条件,而不是满足其中一条或几条就可以(切记,要满足每个约束),同时可行域边界上的点有一个额外好的特性,它们可以使不等式约束取得等号!而边界内的点不行。

关于可行域还有个概念不得不提,那就是凸集,凸集是指有这么一个点的集合,其中任取两个点连一条直线,这条线上的点仍然在这个集合内部,因此说“凸”是很形象的(一个反例是,二维平面上,一个月牙形的区域就不是凸集,你随便就可以找到两个点违反了刚才的规定)。

回头再来看我们线性分类器问题的描述,可以看出更多的东西。

(式2)

在这个问题中,自变量就是w,而目标函数是w的二次函数,所有的约束条件都是w的线性函数(哎,千万不要把xi当成变量,它代表样本,是已知的),这种规划问题有个很有名气的称呼——二次规划(Quadratic Programming,QP),而且可以更进一步的说,由于它的可行域是一个凸集,因此它是一个凸二次规划。

一下子提了这么多术语,实在不是为了让大家以后能向别人炫耀学识的渊博,这其实是我们继续下去的一个重要前提,因为在动手求一个问题的解之前(好吧,我承认,是动计算机求……),我们必须先问自己:这个问题是不是有解?如果有解,是否能找到?

对于一般意义上的规划问题,两个问题的答案都是不一定,但凸二次规划让人喜欢的地方就在于,它有解(教科书里面为了严谨,常常加限定成分,说它有全局最优解,由于我们想找的本来就是全局最优的解,所以不加也罢),而且可以找到!(当然,依据你使用的算法不同,找到这个解的速度,行话叫收敛速度,会有所不同)

对比(式2)和(式1)还可以发现,我们的线性分类器问题只有不等式约束,因此形式上看似乎比一般意义上的规划问题要简单,但解起来却并非如此。

因为我们实际上并不知道该怎么解一个带约束的优化问题。如果你仔细回忆一下高等数学的知识,会记得我们可以轻松的解一个不带任何约束的优化问题(实际上就是当年背得烂熟的函数求极值嘛,求导再找0点呗,谁不会啊?笑),我们甚至还会解一个只带等式约束的优化问题,也是背得烂熟的,求条件极值,记得么,通过添加拉格朗日乘子,构造拉格朗日函数,来把这个问题转化为无约束的优化问题云云(如果你一时没想通,我提醒一下,构造出的拉格朗日函数就是转化之后的问题形式,它显然没有带任何条件)。

读者问:如果只带等式约束的问题可以转化为无约束的问题而得以求解,那么可不可以把带不等式约束的问题向只带等式约束的问题转化一下而得以求解呢?

聪明,可以,实际上我们也正是这么做的。下一节就来说说如何做这个转化,一旦转化完成,求解对任何学过高等数学的人来说,都是小菜一碟啦。

SVM入门(六)线性分类器的求解——问题的转化,直观角度

让我再一次比较完整的重复一下我们要解决的问题:我们有属于两个类别的样本点(并不限定这些点在二维空间中)若干,如图,

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第14张图片

圆形的样本点定为正样本(连带着,我们可以把正样本所属的类叫做正类),方形的点定为负例。我们想求得这样一个线性函数(在n维空间中的线性函数):

g(x)=wx+b

使得所有属于正类的点+代入以后有g(x+)≥1,而所有属于负类的点x-代入后有g(x-)≤-1(之所以总跟1比较,无论正一还是负一,都是因为我们固定了间隔为1,注意间隔和几何间隔的区别)。代入g(x)后的值如果在1和-1之间,我们就拒绝判断。

求这样的g(x)的过程就是求w(一个n维向量)和b(一个实数)两个参数的过程(但实际上只需要求w,求得以后找某些样本点代入就可以求得b)。因此在求g(x)的时候,w才是变量。

你肯定能看出来,一旦求出了w(也就求出了b),那么中间的直线H就知道了(因为它就是wx+b=0嘛,哈哈),那么H1和H2也就知道了(因为三者是平行的,而且相隔的距离还是||w||决定的)。那么w是谁决定的?显然是你给的样本决定的,一旦你在空间中给出了那些个样本点,三条直线的位置实际上就唯一确定了(因为我们求的是最优的那三条,当然是唯一的),我们解优化问题的过程也只不过是把这个确定了的东西算出来而已。

样本确定了w,用数学的语言描述,就是w可以表示为样本的某种组合:

w=α1x1+α2x2+…+αnxn

式子中的αi是一个一个的数(在严格的证明过程中,这些α被称为拉格朗日乘子),而xi是样本点,因而是向量,n就是总样本点的个数。为了方便描述,以下开始严格区别数字与向量的乘积和向量间的乘积,我会用α1x1表示数字和向量的乘积,而用<x1,x2>表示向量x1,x2的内积(也叫点积,注意与向量叉积的区别)。因此g(x)的表达式严格的形式应该是:

g(x)=<w,x>+b

但是上面的式子还不够好,你回头看看图中正样本和负样本的位置,想像一下,我不动所有点的位置,而只是把其中一个正样本点定为负样本点(也就是把一个点的形状从圆形变为方形),结果怎么样?三条直线都必须移动(因为对这三条直线的要求是必须把方形和圆形的点正确分开)!这说明w不仅跟样本点的位置有关,还跟样本的类别有关(也就是和样本的“标签”有关)。因此用下面这个式子表示才算完整:

w=α1y1x1+α2y2x2+…+αnynxn (式1)

其中的yi就是第i个样本的标签,它等于1或者-1。其实以上式子的那一堆拉格朗日乘子中,只有很少的一部分不等于0(不等于0才对w起决定作用),这部分不等于0的拉格朗日乘子后面所乘的样本点,其实都落在H1和H2上,也正是这部分样本(而不需要全部样本)唯一的确定了分类函数,当然,更严格的说,这些样本的一部分就可以确定,因为例如确定一条直线,只需要两个点就可以,即便有三五个都落在上面,我们也不是全都需要。这部分我们真正需要的样本点,就叫做支持(撑)向量!(名字还挺形象吧,他们“撑”起了分界线)

式子也可以用求和符号简写一下:

因此原来的g(x)表达式可以写为:

注意式子中x才是变量,也就是你要分类哪篇文档,就把该文档的向量表示代入到 x的位置,而所有的xi统统都是已知的样本。还注意到式子中只有xi和x是向量,因此一部分可以从内积符号中拿出来,得到g(x)的式子为:

发现了什么?w不见啦!从求w变成了求α。

但肯定有人会说,这并没有把原问题简化呀。嘿嘿,其实简化了,只不过在你看不见的地方,以这样的形式描述问题以后,我们的优化问题少了很大一部分不等式约束(记得这是我们解不了极值问题的万恶之源)。但是接下来先跳过线性分类器求解的部分,来看看 SVM在线性分类器上所做的重大改进——核函数。

SVM入门(七)为何需要核函数

生存?还是毁灭?——哈姆雷特

可分?还是不可分?——支持向量机

之前一直在讨论的线性分类器,器如其名(汗,这是什么说法啊),只能对线性可分的样本做处理。如果提供的样本线性不可分,结果很简单,线性分类器的求解程序会无限循环,永远也解不出来。这必然使得它的适用范围大大缩小,而它的很多优点我们实在不原意放弃,怎么办呢?是否有某种方法,让线性不可分的数据变得线性可分呢?

有!其思想说来也简单,来用一个二维平面中的分类问题作例子,你一看就会明白。事先声明,下面这个例子是网络早就有的,我一时找不到原作者的正确信息,在此借用,并加进了我自己的解说而已。

例子是下面这张图:

/机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第15张图片

我们把横轴上端点a和b之间红色部分里的所有点定为正类,两边的黑色部分里的点定为负类。试问能找到一个线性函数把两类正确分开么?不能,因为二维空间里的线性函数就是指直线,显然找不到符合条件的直线。

但我们可以找到一条曲线,例如下面这一条:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第16张图片

显然通过点在这条曲线的上方还是下方就可以判断点所属的类别(你在横轴上随便找一点,算算这一点的函数值,会发现负类的点函数值一定比0大,而正类的一定比0小)。这条曲线就是我们熟知的二次曲线,它的函数表达式可以写为:

问题只是它不是一个线性函数,但是,下面要注意看了,新建一个向量y和a:

这样g(x)就可以转化为f(y)=<a,y>,你可以把y和a分别回带一下,看看等不等于原来的g(x)。用内积的形式写你可能看不太清楚,实际上f(y)的形式就是:

g(x)=f(y)=ay

在任意维度的空间中,这种形式的函数都是一个线性函数(只不过其中的a和y都是多维向量罢了),因为自变量y的次数不大于1。

看出妙在哪了么?原来在二维空间中一个线性不可分的问题,映射到四维空间后,变成了线性可分的!因此这也形成了我们最初想解决线性不可分问题的基本思路——向高维空间转化,使其变得线性可分。

而转化最关键的部分就在于找到x到y的映射方法。遗憾的是,如何找到这个映射,没有系统性的方法(也就是说,纯靠猜和凑)。具体到我们的文本分类问题,文本被表示为上千维的向量,即使维数已经如此之高,也常常是线性不可分的,还要向更高的空间转化。其中的难度可想而知。

小Tips:为什么说f(y)=ay是四维空间里的函数?

大家可能一时没看明白。回想一下我们二维空间里的函数定义 
  g(x)=ax+b 
变量x是一维的,为什么说它是二维空间里的函数呢?因为还有一个变量我们没写出来,它的完整形式其实是 
  y=g(x)=ax+b 
即 
  y=ax+b 
看看,有几个变量?两个。那是几维空间的函数?(作者五岁的弟弟答:五维的。作者:……) 
再看看 
f(y)=ay 
里面的y是三维的变量,那f(y)是几维空间里的函数?(作者五岁的弟弟答:还是五维的。作者:……)

用一个具体文本分类的例子来看看这种向高维空间映射从而分类的方法如何运作,想象一下,我们文本分类问题的原始空间是1000维的(即每个要被分类的文档被表示为一个1000维的向量),在这个维度上问题是线性不可分的。现在我们有一个2000维空间里的线性函数

f(x’)=<w’,x’>+b

注意向量的右上角有个 ’哦。它能够将原问题变得可分。式中的 w’和x’都是2000维的向量,只不过w’是定值,而x’是变量(好吧,严格说来这个函数是2001维的,哈哈),现在我们的输入呢,是一个1000维的向量x,分类的过程是先把x变换为2000维的向量x’,然后求这个变换后的向量x’与向量w’的内积,再把这个内积的值和b相加,就得到了结果,看结果大于阈值还是小于阈值就得到了分类结果。

你发现了什么?我们其实只关心那个高维空间里内积的值,那个值算出来了,分类结果就算出来了。而从理论上说, x’是经由x变换来的,因此广义上可以把它叫做x的函数(有一个x,就确定了一个x’,对吧,确定不出第二个),而w’是常量,它是一个低维空间里的常量w经过变换得到的,所以给了一个w 和x的值,就有一个确定的f(x’)值与其对应。这让我们幻想,是否能有这样一种函数K(w,x),他接受低维空间的输入值,却能算出高维空间的内积值<w’,x’>?

如果有这样的函数,那么当给了一个低维空间的输入x以后,

g(x)=K(w,x)+b

f(x’)=<w’,x’>+b

这两个函数的计算结果就完全一样,我们也就用不着费力找那个映射关系,直接拿低维的输入往g(x)里面代就可以了(再次提醒,这回的g(x)就不是线性函数啦,因为你不能保证K(w,x)这个表达式里的x次数不高于1哦)。

万幸的是,这样的K(w,x)确实存在(发现凡是我们人类能解决的问题,大都是巧得不能再巧,特殊得不能再特殊的问题,总是恰好有些能投机取巧的地方才能解决,由此感到人类的渺小),它被称作核函数(核,kernel),而且还不止一个,事实上,只要是满足了Mercer条件的函数,都可以作为核函数。核函数的基本作用就是接受两个低维空间里的向量,能够计算出经过某个变换后在高维空间里的向量内积值。几个比较常用的核函数,俄,教课书里都列过,我就不敲了(懒!)。

回想我们上节说的求一个线性分类器,它的形式应该是:

现在这个就是高维空间里的线性函数(为了区别低维和高维空间里的函数和向量,我改了函数的名字,并且给w和x都加上了 ’),我们就可以用一个低维空间里的函数(再一次的,这个低维空间里的函数就不再是线性的啦)来代替,

又发现什么了?f(x’) 和g(x)里的α,y,b全都是一样一样的!这就是说,尽管给的问题是线性不可分的,但是我们就硬当它是线性问题来求解,只不过求解过程中,凡是要求内积的时候就用你选定的核函数来算。这样求出来的α再和你选定的核函数一组合,就得到分类器啦!

明白了以上这些,会自然的问接下来两个问题:

1. 既然有很多的核函数,针对具体问题该怎么选择?

2. 如果使用核函数向高维空间映射后,问题仍然是线性不可分的,那怎么办?

第一个问题现在就可以回答你:对核函数的选择,现在还缺乏指导原则!各种实验的观察结果(不光是文本分类)的确表明,某些问题用某些核函数效果很好,用另一些就很差,但是一般来讲,径向基核函数是不会出太大偏差的一种,首选。(我做文本分类系统的时候,使用径向基核函数,没有参数调优的情况下,绝大部分类别的准确和召回都在85%以上,可见。虽然libSVM的作者林智仁认为文本分类用线性核函数效果更佳,待考证)

对第二个问题的解决则引出了我们下一节的主题:松弛变量。

SVM入门(八)松弛变量

现在我们已经把一个本来线性不可分的文本分类问题,通过映射到高维空间而变成了线性可分的。就像下图这样:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第17张图片

圆形和方形的点各有成千上万个(毕竟,这就是我们训练集中文档的数量嘛,当然很大了)。现在想象我们有另一个训练集,只比原先这个训练集多了一篇文章,映射到高维空间以后(当然,也使用了相同的核函数),也就多了一个样本点,但是这个样本的位置是这样的:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第18张图片

就是图中黄色那个点,它是方形的,因而它是负类的一个样本,这单独的一个样本,使得原本线性可分的问题变成了线性不可分的。这样类似的问题(仅有少数点线性不可分)叫做“近似线性可分”的问题。

以我们人类的常识来判断,说有一万个点都符合某种规律(因而线性可分),有一个点不符合,那这一个点是否就代表了分类规则中我们没有考虑到的方面呢(因而规则应该为它而做出修改)?

其实我们会觉得,更有可能的是,这个样本点压根就是错误,是噪声,是提供训练集的同学人工分类时一打瞌睡错放进去的。所以我们会简单的忽略这个样本点,仍然使用原来的分类器,其效果丝毫不受影响。

但这种对噪声的容错性是人的思维带来的,我们的程序可没有。由于我们原本的优化问题的表达式中,确实要考虑所有的样本点(不能忽略某一个,因为程序它怎么知道该忽略哪一个呢?),在此基础上寻找正负类之间的最大几何间隔,而几何间隔本身代表的是距离,是非负的,像上面这种有噪声的情况会使得整个问题无解。这种解法其实也叫做“硬间隔”分类法,因为他硬性的要求所有样本点都满足和分类平面间的距离必须大于某个值。

因此由上面的例子中也可以看出,硬间隔的分类法其结果容易受少数点的控制,这是很危险的(尽管有句话说真理总是掌握在少数人手中,但那不过是那一小撮人聊以自慰的词句罢了,咱还是得民主)。

但解决方法也很明显,就是仿照人的思路,允许一些点到分类平面的距离不满足原先的要求。由于不同的训练集各点的间距尺度不太一样,因此用间隔(而不是几何间隔)来衡量有利于我们表达形式的简洁。我们原先对样本点的要求是:

意思是说离分类面最近的样本点函数间隔也要比1大。如果要引入容错性,就给1这个硬性的阈值加一个松弛变量,即允许

因为松弛变量是非负的,因此最终的结果是要求间隔可以比1小。但是当某些点出现这种间隔比1小的情况时(这些点也叫离群点),意味着我们放弃了对这些点的精确分类,而这对我们的分类器来说是种损失。但是放弃这些点也带来了好处,那就是使分类面不必向这些点的方向移动,因而可以得到更大的几何间隔(在低维空间看来,分类边界也更平滑)。显然我们必须权衡这种损失和好处。好处很明显,我们得到的分类间隔越大,好处就越多。回顾我们原始的硬间隔分类对应的优化问题:

||w||2就是我们的目标函数(当然系数可有可无),希望它越小越好,因而损失就必然是一个能使之变大的量(能使它变小就不叫损失了,我们本来就希望目标函数值越小越好)。那如何来衡量损失,有两种常用的方式,有人喜欢用

而有人喜欢用

其中l都是样本的数目。两种方法没有大的区别。如果选择了第一种,得到的方法的就叫做二阶软间隔分类器,第二种就叫做一阶软间隔分类器。把损失加入到目标函数里的时候,就需要一个惩罚因子(cost,也就是libSVM的诸多参数中的C),原来的优化问题就变成了下面这样:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第19张图片

这个式子有这么几点要注意:

一是并非所有的样本点都有一个松弛变量与其对应。实际上只有“离群点”才有,或者也可以这么看,所有没离群的点松弛变量都等于0(对负类来说,离群点就是在前面图中,跑到H2右侧的那些负样本点,对正类来说,就是跑到H1左侧的那些正样本点)。

【在迭代求w的时候如何样本点非离群点,即分类正确,那么就设它的松弛变量为0了。。。】

二是松弛变量的值实际上标示出了对应的点到底离群有多远,值越大,点就越远。

三是惩罚因子C决定了你有多重视离群点带来的损失,显然当所有离群点的松弛变量的和一定时,你定的C越大,对目标函数的损失也越大,此时就暗示着你非常不愿意放弃这些离群点,最极端的情况是你把C定为无限大,这样只要稍有一个点离群,目标函数的值马上变成无限大,马上让问题变成无解,这就退化成了硬间隔问题。

四是惩罚因子C不是一个变量,整个优化问题在解的时候,C是一个你必须事先指定的值,指定这个值以后,解一下,得到一个分类器,然后用测试数据看看结果怎么样,如果不够好,换一个C的值,再解一次优化问题,得到另一个分类器,再看看效果,如此就是一个参数寻优的过程,但这和优化问题本身决不是一回事,优化问题在解的过程中,C一直是定值,要记住。

五是尽管加了松弛变量这么一说,但这个优化问题仍然是一个优化问题(汗,这不废话么),解它的过程比起原始的硬间隔问题来说,没有任何更加特殊的地方。

从大的方面说优化问题解的过程,就是先试着确定一下w,也就是确定了前面图中的三条直线,这时看看间隔有多大,又有多少点离群,把目标函数的值算一算,再换一组三条直线(你可以看到,分类的直线位置如果移动了,有些原来离群的点会变得不再离群,而有的本来不离群的点会变成离群点),再把目标函数的值算一算,如此往复(迭代),直到最终找到目标函数最小时的w。

啰嗦了这么多,读者一定可以马上自己总结出来,松弛变量也就是个解决线性不可分问题的方法罢了,但是回想一下,核函数的引入不也是为了解决线性不可分的问题么?为什么要为了一个问题使用两种方法呢?

其实两者还有微妙的不同。一般的过程应该是这样,还以文本分类为例。在原始的低维空间中,样本相当的不可分,无论你怎么找分类平面,总会有大量的离群点,此时用核函数向高维空间映射一下,虽然结果仍然是不可分的,但比原始空间里的要更加接近线性可分的状态(就是达到了近似线性可分的状态),此时再用松弛变量处理那些少数“冥顽不化”的离群点,就简单有效得多啦。

本节中的(式1)也确实是支持向量机最最常用的形式。至此一个比较完整的支持向量机框架就有了,简单说来,支持向量机就是使用了核函数的软间隔线性分类法。

下一节会说说松弛变量剩下的一点点东西,顺便搞个读者调查,看看大家还想侃侃SVM的哪些方面。

SVM入门(九)松弛变量(续)

接下来要说的东西其实不是松弛变量本身,但由于是为了使用松弛变量才引入的,因此放在这里也算合适,那就是惩罚因子C。回头看一眼引入了松弛变量以后的优化问题:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第20张图片

注意其中C的位置,也可以回想一下C所起的作用(表征你有多么重视离群点,C越大越重视,越不想丢掉它们)。这个式子是以前做SVM的人写的,大家也就这么用,但没有任何规定说必须对所有的松弛变量都使用同一个惩罚因子,我们完全可以给每一个离群点都使用不同的C,这时就意味着你对每个样本的重视程度都不一样,有些样本丢了也就丢了,错了也就错了,这些就给一个比较小的C;而有些样本很重要,决不能分类错误(比如中央下达的文件啥的,笑),就给一个很大的C。

当然实际使用的时候并没有这么极端,但一种很常用的变形可以用来解决分类问题中样本的“偏斜”问题。

先来说说样本的偏斜问题,也叫数据集偏斜(unbalanced),它指的是参与分类的两个类别(也可以指多个类别)样本数量差异很大。比如说正类有10,000个样本,而负类只给了100个,这会引起的问题显而易见,可以看看下面的图:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第21张图片

方形的点是负类。H,H1,H2是根据给的样本算出来的分类面,由于负类的样本很少很少,所以有一些本来是负类的样本点没有提供,比如图中两个灰色的方形点,如果这两个点有提供的话,那算出来的分类面应该是H’,H2’和H1,他们显然和之前的结果有出入,实际上负类给的样本点越多,就越容易出现在灰色点附近的点,我们算出的结果也就越接近于真实的分类面。但现在由于偏斜的现象存在,使得数量多的正类可以把分类面向负类的方向“推”,因而影响了结果的准确性。

对付数据集偏斜问题的方法之一就是在惩罚因子上作文章,想必大家也猜到了,那就是给样本数量少的负类更大的惩罚因子,表示我们重视这部分样本(本来数量就少,再抛弃一些,那人家负类还活不活了),因此我们的目标函数中因松弛变量而损失的部分就变成了:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第22张图片

其中i=1…p都是正样本,j=p+1…p+q都是负样本。libSVM这个算法包在解决偏斜问题的时候用的就是这种方法。

那C+和C-怎么确定呢?它们的大小是试出来的(参数调优),但是他们的比例可以有些方法来确定。咱们先假定说C+是5这么大,那确定C-的一个很直观的方法就是使用两类样本数的比来算,对应到刚才举的例子,C-就可以定为500这么大(因为10,000:100=100:1嘛)。

但是这样并不够好,回看刚才的图,你会发现正类之所以可以“欺负”负类,其实并不是因为负类样本少,真实的原因是负类的样本分布的不够广(没扩充到负类本应该有的区域)。说一个具体点的例子,现在想给政治类和体育类的文章做分类,政治类文章很多,而体育类只提供了几篇关于篮球的文章,这时分类会明显偏向于政治类,如果要给体育类文章增加样本,但增加的样本仍然全都是关于篮球的(也就是说,没有足球,排球,赛车,游泳等等),那结果会怎样呢?虽然体育类文章在数量上可以达到与政治类一样多,但过于集中了,结果仍会偏向于政治类!所以给C+和C-确定比例更好的方法应该是衡量他们分布的程度。比如可以算算他们在空间中占据了多大的体积,例如给负类找一个超球——就是高维空间里的球啦——它可以包含所有负类的样本,再给正类找一个,比比两个球的半径,就可以大致确定分布的情况。显然半径大的分布就比较广,就给小一点的惩罚因子。

但是这样还不够好,因为有的类别样本确实很集中,这不是提供的样本数量多少的问题,这是类别本身的特征(就是某些话题涉及的面很窄,例如计算机类的文章就明显不如文化类的文章那么“天马行空”),这个时候即便超球的半径差异很大,也不应该赋予两个类别不同的惩罚因子。

看到这里读者一定疯了,因为说来说去,这岂不成了一个解决不了的问题?然而事实如此,完全的方法是没有的,根据需要,选择实现简单又合用的就好(例如libSVM就直接使用样本数量的比)。

SVM入门(十)将SVM用于多类分类

从 SVM的那几张图可以看出来,SVM是一种典型的两类分类器,即它只回答属于正类还是负类的问题。而现实中要解决的问题,往往是多类的问题(少部分例外,例如垃圾邮件过滤,就只需要确定“是”还是“不是”垃圾邮件),比如文本分类,比如数字识别。如何由两类分类器得到多类分类器,就是一个值得研究的问题。

还以文本分类为例,现成的方法有很多,其中一种一劳永逸的方法,就是真的一次性考虑所有样本,并求解一个多目标函数的优化问题,一次性得到多个分类面,就像下图这样:

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第23张图片

多个超平面把空间划分为多个区域,每个区域对应一个类别,给一篇文章,看它落在哪个区域就知道了它的分类。

看起来很美对不对?只可惜这种算法还基本停留在纸面上,因为一次性求解的方法计算量实在太大,大到无法实用的地步。

稍稍退一步,我们就会想到所谓“一类对其余”的方法,就是每次仍然解一个两类分类的问题。比如我们有5个类别,第一次就把类别1的样本定为正样本,其余2,3,4,5的样本合起来定为负样本,这样得到一个两类分类器,它能够指出一篇文章是还是不是第1类的;第二次我们把类别2 的样本定为正样本,把1,3,4,5的样本合起来定为负样本,得到一个分类器,如此下去,我们可以得到5个这样的两类分类器(总是和类别的数目一致)。到了有文章需要分类的时候,我们就拿着这篇文章挨个分类器的问:是属于你的么?是属于你的么?哪个分类器点头说是了,文章的类别就确定了。这种方法的好处是每个优化问题的规模比较小,而且分类的时候速度很快(只需要调用5个分类器就知道了结果)。但有时也会出现两种很尴尬的情况,例如拿一篇文章问了一圈,每一个分类器都说它是属于它那一类的,或者每一个分类器都说它不是它那一类的,前者叫分类重叠现象,后者叫不可分类现象。分类重叠倒还好办,随便选一个结果都不至于太离谱,或者看看这篇文章到各个超平面的距离,哪个远就判给哪个。不可分类现象就着实难办了,只能把它分给第6个类别了……更要命的是,本来各个类别的样本数目是差不多的,但“其余”的那一类样本数总是要数倍于正类(因为它是除正类以外其他类别的样本之和嘛),这就人为的造成了上一节所说的“数据集偏斜”问题。

因此我们还得再退一步,还是解两类分类问题,还是每次选一个类的样本作正类样本,而负类样本则变成只选一个类(称为“一对一单挑”的方法,哦,不对,没有单挑,就是“一对一”的方法,呵呵),这就避免了偏斜。因此过程就是算出这样一些分类器,第一个只回答“是第1类还是第2类”,第二个只回答“是第1类还是第3类”,第三个只回答“是第1类还是第4类”,如此下去,你也可以马上得出,这样的分类器应该有5 X 4/2=10个(通式是,如果有k个类别,则总的两类分类器数目为k(k-1)/2)。虽然分类器的数目多了,但是在训练阶段(也就是算出这些分类器的分类平面时)所用的总时间却比“一类对其余”方法少很多,在真正用来分类的时候,把一篇文章扔给所有分类器,第一个分类器会投票说它是“1”或者“2”,第二个会说它是“1”或者“3”,让每一个都投上自己的一票,最后统计票数,如果类别“1”得票最多,就判这篇文章属于第1类。这种方法显然也会有分类重叠的现象,但不会有不可分类现象,因为总不可能所有类别的票数都是0。看起来够好么?其实不然,想想分类一篇文章,我们调用了多少个分类器?10个,这还是类别数为5的时候,类别数如果是1000,要调用的分类器数目会上升至约500,000个(类别数的平方量级)。这如何是好?

看来我们必须再退一步,在分类的时候下功夫,我们还是像一对一方法那样来训练,只是在对一篇文章进行分类之前,我们先按照下面图的样子来组织分类器(如你所见,这是一个有向无环图,因此这种方法也叫做DAG SVM)

机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码)_第24张图片

这样在分类时,我们就可以先问分类器“1对5”(意思是它能够回答“是第1类还是第5类”),如果它回答5,我们就往左走,再问“2对5”这个分类器,如果它还说是“5”,我们就继续往左走,这样一直问下去,就可以得到分类结果。好处在哪?我们其实只调用了4个分类器(如果类别数是k,则只调用k-1个),分类速度飞快,且没有分类重叠和不可分类现象!缺点在哪?假如最一开始的分类器回答错误(明明是类别1的文章,它说成了5),那么后面的分类器是无论如何也无法纠正它的错误的(因为后面的分类器压根没有出现“1”这个类别标签),其实对下面每一层的分类器都存在这种错误向下累积的现象。。

不过不要被DAG方法的错误累积吓倒,错误累积在一对其余和一对一方法中也都存在,DAG方法好于它们的地方就在于,累积的上限,不管是大是小,总是有定论的,有理论证明。而一对其余和一对一方法中,尽管每一个两类分类器的泛化误差限是知道的,但是合起来做多类分类的时候,误差上界是多少,没人知道,这意味着准确率低到0也是有可能的,这多让人郁闷。

而且现在DAG方法根节点的选取(也就是如何选第一个参与分类的分类器),也有一些方法可以改善整体效果,我们总希望根节点少犯错误为好,因此参与第一次分类的两个类别,最好是差别特别特别大,大到以至于不太可能把他们分错;或者我们就总取在两类分类中正确率最高的那个分类器作根节点,或者我们让两类分类器在分类的时候,不光输出类别的标签,还输出一个类似“置信度”的东东,当它对自己的结果不太自信的时候,我们就不光按照它的输出走,把它旁边的那条路也走一走,等等。

大Tips:SVM的计算复杂度

使用SVM进行分类的时候,实际上是训练和分类两个完全不同的过程,因而讨论复杂度就不能一概而论,我们这里所说的主要是训练阶段的复杂度,即解那个二次规划问题的复杂度。对这个问题的解,基本上要划分为两大块,解析解和数值解。

解析解就是理论上的解,它的形式是表达式,因此它是精确的,一个问题只要有解(无解的问题还跟着掺和什么呀,哈哈),那它的解析解是一定存在的。当然存在是一回事,能够解出来,或者可以在可以承受的时间范围内解出来,就是另一回事了。对SVM来说,求得解析解的时间复杂度最坏可以达到O(Nsv3),其中Nsv是支持向量的个数,而虽然没有固定的比例,但支持向量的个数多少也和训练集的大小有关。

数值解就是可以使用的解,是一个一个的数,往往都是近似解。求数值解的过程非常像穷举法,从一个数开始,试一试它当解效果怎样,不满足一定条件(叫做停机条件,就是满足这个以后就认为解足够精确了,不需要继续算下去了)就试下一个,当然下一个数不是乱选的,也有一定章法可循。有的算法,每次只尝试一个数,有的就尝试多个,而且找下一个数字(或下一组数)的方法也各不相同,停机条件也各不相同,最终得到的解精度也各不相同,可见对求数值解的复杂度的讨论不能脱开具体的算法。

一个具体的算法,Bunch-Kaufman训练算法,典型的时间复杂度在O(Nsv3+LNsv2+dLNsv)和O(dL2)之间,其中Nsv是支持向量的个数,L是训练集样本的个数,d是每个样本的维数(原始的维数,没有经过向高维空间映射之前的维数)。复杂度会有变化,是因为它不光跟输入问题的规模有关(不光和样本的数量,维数有关),也和问题最终的解有关(即支持向量有关),如果支持向量比较少,过程会快很多,如果支持向量很多,接近于样本的数量,就会产生O(dL2)这个十分糟糕的结果(给10,000个样本,每个样本1000维,基本就不用算了,算不出来,呵呵,而这种输入规模对文本分类来说太正常了)。

这样再回头看就会明白为什么一对一方法尽管要训练的两类分类器数量多,但总时间实际上比一对其余方法要少了,因为一对其余方法每次训练都考虑了所有样本(只是每次把不同的部分划分为正类或者负类而已),自然慢上很多。


你可能感兴趣的:(机器学习算法与Python实践之(四)支持向量机(SVM)实现(源码))