Libliner 中的-s 参数选择:primal 和dual
Libliner 中的-s 参数选择:primal 和dual
- LIBLINEAR的优化算法主要分为两大类,即求解原问题(primal problem)和对偶问题(dual problem)。求解原问题使用的是TRON的优化算法,对偶问题使用的是Coordinate Descent优化算法。总的来说,两个算法的优化效率都较高,但还是有各自更加擅长的场景。对于样本量不大,但是维度特别高的场景,如文本分类,更适合对偶问题求解,因为由于样本量小,计算出来的Kernel Matrix也不大,后面的优化也比较方便。而如果求解原问题,则求导的过程中要频繁对高维的特征矩阵进行计算,如果特征比较稀疏的话,那么就会多做很多无意义的计算,影响优化的效率。相反,当样本数非常多,而特征维度不高时,如果采用求解对偶问题,则由于Kernel Matrix过大,求解并不方便。反倒是求解原问题更加容易。
下面是libliner 和libsvm的详细介绍:
转自:http://blog.chinaunix.net/uid-20761674-id-4840100.html
LIBSVM与LIBLINEAR
模型与优化
LIBSVM和LIBLINEAR都提供了多种不同的模型供使用者选择,不同的模型有各自适用的场景。下面分别介绍LIBSVM和LIBLINEAR所提供的各种模型。
LIBSVM
下面是LIBSVM帮助内容提供的介绍,给出了LIBSVM支持的5种模型。其中模型0和1对应的都是SVM的分类模型,2对应的是one-class分类器,也就是只需要标注一个标签,模型3和4对应的是SVM的回归模型。
1 -s svm_type : set type of SVM (default 0) 2 0 -- C-SVC (multi-class classification) 3 1 -- nu-SVC (multi-class classification) 4 2 -- one-class SVM 5 3 -- epsilon-SVR (regression) 6 4 -- nu-SVR (regression)
首先来看最基础的C-SVC模型。SVM可以写成如下的优化目标函数(这里不详细介绍推导算法了):
当模型使用linear kernel,也就是?(x)=x时,上面的问题一个标准的二次凸优化问题,可以比较方便的对每一个变量进行求导。求解这样的问题是有很多快速的优化方法的,这些方法在LIBLINEAR中都有应用。但是如果是引入kernel的SVM,情况就大不一样了。因为很多时候我们既不能得到核函数的具体形式,又无法得到特征在核空间中新的表达。这个时候,之前用在线性SVM上的的求解思路就完全不work了。为了解决这个问题,就必须采用标准的SVM求解思路,首先把原问题转化为对偶问题,得到下面的目标函数(具体过程可以参考任何介绍SVM的资料):
通过对偶变化,上面的目标函数变成了一个关于变量α的二次型。很显然,上面目标函数中最重要的常亮是矩阵Q,既训练样本的Kernel Matrix,满足Qi.j=?(xi)T?(xj)。先看好的一方面,根据核函数的定义,能够保证Q是一个正定的矩阵。也就是说,上面的目标函数还是一个凸函数,优化收敛后能保证得到的解是全局最优解, 这也是SVM的重要优势之一。但是问题也随之而来,使用常用的核函数,只要任意给出两个向量,总是能够计算出一个非0的距离。这也就意味着矩阵Q将会是一个非常稠密的矩阵,如果训练样本足够多,那么矩阵Q的存储和计算将成为很大的问题,这也是SVM的优化算法中的最大挑战。
由于矩阵Q过大,所以想一次性优化整个α是比较困难的。所以常用的方法都是先把Q大卸八块,每次选择一部分的Q,然后update与这部分Q相关的α的值。这其中最著名的算法就是1998由John C. Platt提出的SMO算法,而LIBSVM的优化过程也是基于SMO算法进行的。SMO算法的每一步迭代都选择最小的优化单元,也就是固定其他的α,只挑选两个α的值进行优化。之所以不选择一个,是因为有yTα=0的约束,至少选择两个α的坐标才有可能进行更新。本文主要目的是介绍LIBSVM,所以就不详细讨论SMO的细节了。至于LIBSVM中的具体算法实现,在LIBSVM的官方论文中介绍的很详细,这里总结部分关键问题:
- Working Set,也就是需要优化的α部分的选取
- 迭代停止条件的设置
- α的更新算法,也就是每一步子问题的求解方法
- Shrinking,即移除一些已经满足条件的α,加快收敛速度
- Cache,当Q矩阵过大时,需要对矩阵进行缓存。
上面的每个问题,处理起来都不简单。作为使用者,或许也没有必要深谙里面的所有细节。我觉得最需要认识的两个问题是:1) SVM的目标函数看起来好像是一个标准的优化问题,但实际求解却要复杂得多。为了提高求解的速度,既要做算法上的优化,也需要做工程上的改进。如果只是简简单单按照教科书的方法,甚至直接调用一些优化的工具包来实现的SVM算法,最多也就算个demo。要能够真正写一个高效稳定、能处理大规模数据的SVM工具还是非常不容易的。所以用LIBSVM还是比自己实现算法要简单靠谱不少。2)SVM的求解之所以要优化,就是因为这个问题本身计算和存储比较麻烦。所以虽然做了这么多的优化,整个算法求解的效率仍然较低。所以我们在使用时还要注意各种程序的细节,提高运行的效率。另外,样本量过大时,有时候为了充分利用数据,也不得不忍痛割爱,放弃kernel的使用。
除了标准的C-SVM,LIBSVM也提供了对其他一些SVM方法的支持。其中ν-SVM与C-SVM的算法与应用场景基本是相同的,唯一的区别是原本的参数C变成了参数ν。C-SVM中参数C调整范围在[0,+∞),而ν-SVM中与之对应的参数ν的调整范围变成了 (0,1]。这样的设置使得ν-SVM更具解释性,有时在参数设置上也能提供一定的方便。但ν-SVM与C-SVM并不存在本质上的差别,通过参数的调节,两者可以达到完全相同的效果。所以在使用LIBSVM处理分类问题是,选择上面任何一种方法都是OK的,只需要遵循自己的习惯就好了。
One-Class SVM也是LIBSVM所支持的一种分类方法。顾名思义,使用One Class时,只需要提供一类样本,算法会学习一个尽量小的超球面包裹所有的训练样本。One-Class SVM看起来很有诱惑力,因为我们经常会遇到有一类样本而需要学习分类器的情况。但事实上,一方面很多时候我们得到的正样本在采样过程中存在很大的偏差,导致学习出的One Class分类器不一定考虑到了所有正样本的情形;另一方面,大部分问题还是存在很多构造人工负样本的办法。根据我的经验,采用普通的SVM效果通常还是会好过One-Class SVM,而One-Class SVM在真实场景中的使用也并算不上多。因此在使用这个方法前也需要对问题进行更深入的研究。
最后,LIBSVM也支持基于SVM的回归模型,即SVR。与分类模型类似,SVR也分为C-SVR和ν-SVR。SVR的目标函数与SVM的分类模型稍有区别。由于回归问题预测值与目标值的偏差可大可小,因此SVR使用了两个slack variable用来刻画预测的误差边界。虽然存在这样的差别,但是两者的基本思路和优化算法与还是基本一致的。
在LIBSVM的实现中,上面五种模型,即C-SVM,ν-SVM,One-class SVM,C-SVR,ν-SVR,最终都可以转化为一个更通用的优化框架,然后用同样的策略进行求解,这也是LIBSVM所实现的主要功能。在实际使用中,最常用到的方法还是C-SVM,这是最传统的SVM分类模型。
LIBLINEAR
LIBLINEAR是在LIBSVM流行多年后才开发的,要解决的问题本质上也比LIBSVM更简单,其优势主要在于效率与scalablility。之所以存在这样的优势,是因为线性SVM的求解要比kernel SVM简单许多。
还从上面的对偶问题说起,之前SVM的求解很大程度上受到yTα=0的困扰,因此每次必须选择一组 α进行优化。如果对这一约束项追根述源,可以发现这一项是通过令模型的常数项b导数为0而得到的。而在线性模型中,我们可以通过一个简单地trick,令x=[x,1]和w=[w,b],这样,在模型中的常数项就不存在了。当然,这样的trick只能在线性模型中才适用。没有了上面的约束,优化的目标函数变成了:
这个时候,就可以每次只选择一个αi进行优化,每一轮遍历α的所有维度,多轮迭代,直至最后收敛。这样的优化算法叫做coordinate descent(坐标下降法)。利用线性函数的特殊性,直接根据α就可以计算出w的向量表示,于是大大提高了算法的效率。具体的优化算法可以参考文献 A Dual Coordinate Descent Method for Large-scale Linear SVM。
换一个看问题的角度,线性SVM的目标函数可以写成下面的形式:
进一步对问题进行抽象,可以把一类分类问题写成下面的形式:
其中的?作为误差函数,用来度量预测值与目标值的损失。在上面的线性SVM的情形中,有
这里的?称为Hinge Loss。
又如在Logistic Regression中,loss function ?被定义为
Ω一般被称为正则化项(Regularizer),最常使用的就是前面出现的?2-norm,写作wTw,也可以写作∥w∥22,即向量w中所有元素的平方和。除?2-norm之外,?1-norm也是经常使用regularizer,而且会带来一些特别的效果(后面会进行讨论)。大量的监督学习模型都可以写成loss function + regularizer的形式,而参数C则控制了两者在最终损失函数中所占的比重。不同loss function与regularizer的选取以及两者之间的平衡,是机器学习的最重要主题之一。
对于上面的问题,有很多成熟的算法可以进行模型的求解,比如最速梯度法,牛顿法等,对于样本量较大时,也可以采用随机梯度的方法进行训练。 一般来说,由于考虑了二阶导数,牛顿法本身的优化效率要高于只考虑一阶导数的最速梯度法。但由于牛顿法本身在计算量和收敛性上存在很多局限性,所以很少直接使用,而是在牛顿法思想基础上进行一定的改进。其中普遍使用的算法有BFGS和L-BFGS等。具体到liblinear软件包,作者采用的是Trust Region Newton (TRON) method对模型对传统牛顿法进行了改进,该方法被证明比L-BFGS训练更加高效。
LIBLINEAR中实现了基于TRON方法的L-2 SVM和Logistical Regression模型训练。其中的L2-loss SVM是标准SVM的变种,loss function变成了:
从实际效果来说,L2-loss SVM与标准的L1-loss SVM并没有太大的区别。但是在计算上,前者的求导形式更加简单,便于梯度的计算与优化。LIBLINEAR并没有实现Trust Region Newton法的标准L1-loss SVM实现,一方面是因为直接对hinge loss求导需要分段讨论比较复杂,另一方面L2-loss SVM基本可以直接替代L1-loss SVM。不过在其他的一些软件包中,如SVMLIN中,则实现了L1-loss SVM的原问题求解,但使用的优化算法是L-BGFS而不是TRON。
总结
前面介绍了LIBSVM和LIBLINEAR的优化算法,下面简单总结一下不同算法的应用场景吧:
- 所有线性问题都是用LIBLINEAR,而不要使用LIBSVM。
- LIBSVM中的不同算法,如C-SVM和nu-SVM在模型和求解上并没有本质的区别,只是做了一个参数的变换,所以选择自己习惯的就好。
- LIBLINEAR的优化算法主要分为两大类,即求解原问题(primal problem)和对偶问题(dual problem)。求解原问题使用的是TRON的优化算法,对偶问题使用的是Coordinate Descent优化算法。总的来说,两个算法的优化效率都较高,但还是有各自更加擅长的场景。对于样本量不大,但是维度特别高的场景,如文本分类,更适合对偶问题求解,因为由于样本量小,计算出来的Kernel Matrix也不大,后面的优化也比较方便。而如果求解原问题,则求导的过程中要频繁对高维的特征矩阵进行计算,如果特征比较稀疏的话,那么就会多做很多无意义的计算,影响优化的效率。相反,当样本数非常多,而特征维度不高时,如果采用求解对偶问题,则由于Kernel Matrix过大,求解并不方便。反倒是求解原问题更加容易。
多分类问题
LIBSVM和LIBLINEAR都支持多分类(Multi-class classification)问题。所谓多分类问题,就是说每一个样本的类别标签可以超过2个,但是最终预测的结果只能是一个类别。比如经典的手写数字识别问题,输入是一幅图像,最后输出的是0-9这十个数字中的某一个。
LIBSVM与LIBLINEAR但实现方式却完全不同。LIBSVM采取的one vs one的策略,也就是所有的分类两两之间都要训练一个分类器。这样一来,如果存在k个class,理论上就需要训练 k(k?1)/2个分类器。实际上,libsvm在这一步也进行了一定的优化,利用已有分类的关系,减少分类器的个数。尽管如此,LIBSVM在多分类问题上还是要多次训练分类器。但是,考虑到前面说的LIBSVM的优化方法,随着样本数量的增加,训练的复杂度会非线性的增加。而通过1VS1的策略,可以保证每一个子分类问题的样本量不至于太多,其实反倒是方便了整个模型的训练。
而LIBLINEAR则采取了另一种训练策略,即one vs all。每一个class对应一个分类器,副样本就是其他类别的所有样本。由于LIBLINEAR能够和需要处理的训练规模比LIBSVM大得多,因此这种方式要比one vs one更加高效。此外,LIBLINEAR还实现了基于Crammer and Singer方法的SVM多分类算法,在一个统一的目标函数中学习每一个class对应的分类器。
输出文件
一般来说,我们使用LIBLINEAR或者LIBSVM,可以直接调用系统的训练与预测函数,不需要直接去接触训练得到的模型文件。但有时候我们也可能需要在自己的平台实现预测的算法,这时候就不可避免的要对模型文件进行解析。
由于LIBLINEAR与LIBSVM的训练模型不同,因此他们对应的模型文件格式也不同。LIBLINEAR训练结果的格式相对简单,例如:
1 solver_type L2R_L2LOSS_SVC_DUAL 2 nr_class 3 3 label 0 1 2 4 nr_feature 5 5 bias -1 6 w 7 -0.4021097293855418 0.1002472498884907 -0.1619908595357437 8 0.008699468444669581 0.2310109611908343 -0.2295723940247394 9 -0.6814324057724231 0.4263611607497726 -0.4190714505083906 10 -0.1505088594898125 0.2709227166451816 -0.1929294695905781 11 2.14656708009991 -0.007495770268046003 -0.1880325536062815
上面的solver_type表示求解算法,w以下表示求解得到的模型权重。其中每一列对应一个class的分类器,而每一行对应特征的一个维度。其中nr_class表示求解的个数,nr_feature表示特征的维度,bias表示模型的bias,可以人工设置权重。这里容易产生误解的是label这个字段,表示的是每一个用户训练文件中label对应w的列数。比如在上面的模型中,用户指定编号为0的分类器对应w的第一列。但是上面的对应关系并不是一定存在的,比如在二分类场景中,用将整样本标为1,负样本标为0,但在模型训练中,LIBLINEAR会按照自己的编号系统进行训练,因而有可能出现负样本在前,正样本在后的情形。这时候,就必须要根据label 1 0将LIBLIENAR内部的编号体系与真实的用户标签进行对应。当然,后来LIBLINEAR和LIBSVM做了一些优化,在二分类时,如果正负样本标签分别是-1和+1,那么可以始终保证正样本出现在w的第一列。但是这个机制也不是完全靠谱,比如说在LIBLINEAR的spark实现代码中,就没有实现这个特性,曾经把我整的很惨。因此在这里还是需要十分注意。
LIBSVM的训练结果格式就更复杂一些,例如:
1 kernel_type rbf 2 gamma 0.0769231 3 nr_class 3 4 total_sv 140 5 rho -1.04496 0.315784 1.03037 6 label 1 0 -1 7 nr_sv 2 2 1 8 SV 9 0 1 1:0.583333 2:-1 3:0.333333 4:-0.603774 5:1 6:-1 7:1 8:0.358779 9:-1 10:-0.483871 12:-1 13:1 10 0 0.6416468628860974 1:0.125 2:1 3:0.333333 4:-0.320755 5:-0.406393 6:1 7:1 8:0.0839695 9:1 10:-0.806452 12:-0.333333 13:0.5 11 0 1 1:0.333333 2:1 3:-1 4:-0.245283 5:-0.506849 6:-1 7:-1 8:0.129771 9:-1 10:-0.16129 12:0.333333 13:-1 12 0.2685466895842373 0 1:0.583333 2:1 3:1 4:-0.509434 5:-0.52968 6:-1 7:1 8:-0.114504 9:1 10:-0.16129 12:0.333333 13:1 13 0 1 1:0.208333 2:1 3:0.333333 4:-0.660377 5:-0.525114 6:-1 7:1 8:0.435115 9:-1 10:-0.193548 12:-0.333333 13:1
上面参数的意义都比较直接,需要注意的是SV后面就是训练出的模型参数,以支持向量的方式进行存储。nr_sv给出了每一个支持向量所对应的模型,比如“2 2 1”就表示前两行是标签为1类的支持向量,其后面两行是标签为0类的支持向量,最后一行是标签为-1类的支持向量。而具体每一行支持向量,在上面的模型中,由于存在三类,所以每一个支持向量有可能都会存在于两个分类器中,所以前两列的数分别对应了对剩下两个分类作为支持向量时候的α值,后面才是真正的支持向量。