|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
2 [- E$ }. ~. ]( O) f7 v学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。% D5 _. o4 ]+ a' ]
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
. Q, [) y6 R: v ' m! A4 ^9 _" {! H* z
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
+ D! k3 I& h1 C( v ' e7 h L8 V8 z, I& C G, H' x! H$ i
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。
) c, u1 r' L6 S( C( I那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。4 A7 Y- Q9 r. n+ _4 h8 I7 D+ o
第一步:实现传统的 SMO 算法$ p! B! c1 F4 Z- f p3 T7 a
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
7 g% Z" z2 v. }procedure takeStep(i1,i2)
3 @- X- v& j2 D8 E$ b2 q! h if (i1 == i2) return 08 u& E: W0 t+ ?& V F4 J9 G+ Q
alph1 = Lagrange multiplier for i1
" I) J, a# {& O) e2 E/ w+ R9 I# |. y9 R4 [
y1 = target[i1]
! ^# x8 t, H. W" a* j% t( v1 b1 E) V) \* l4 H
E1 = SVM output on point[i1] – y1 (check in error cache)" B. z, ~9 U6 ^
+ q7 _- {8 C7 C" k' ^- I
s = y1*y2
" a$ B9 d( K* ^( L5 [/ K
( W. U- ^ a! K" x' k# BCompute L, H via equations (13) and (14)
/ ?' d( l' D, f
* J$ Y) c$ T/ U5 }7 w% ?# Yif (L == H)
. U" x) ~# Y7 R% A. i) V" T
# s( ?6 ^ ?# S2 j- mreturn 0% j+ f/ b/ I' k" p5 Z( s
* [! W+ l4 K& l: M- T
k11 = kernel(point[i1],point[i1])
8 i( X( A0 u) k0 q8 g* ~' {8 n
d1 X- O) K& i1 X7 }k12 = kernel(point[i1],point[i2])
$ @3 M0 y ]; w) Y0 k* | a
! G8 f% G; p7 @1 ?7 f# ?k22 = kernel(point[i2],point[i2])0 f( [6 Z9 y- p6 t
, p% o; x N" i% ~, _% r% f5 i- C
eta = k11+k22-2*k12, P/ p: ]* w j" N2 X
& p: _1 k) b7 r* }
if (eta > 0)+ t/ i' j4 P! y% B( v
8 b' P0 L0 b) z" V1 ]{ ~' `: P" E8 }1 a, \
3 b6 O( u% K" |+ S" _) q/ ?& Ka2 = alph2 + y2*(E1-E2)/eta& k2 T: v0 ~" t+ e8 f
3 X) d; O( l8 X; s0 N+ Q. Sif (a2 < L) a2 = L
- y& g) |" h' ]9 `, U2 r& i; v! P/ [4 K
else if (a2 > H) a2 = H
2 `% r: E* U, K @9 }( Q& t3 E# Q) B j' ?/ ~9 z8 W
}
& [; w( s5 T9 D) j: X
% q# D3 b! K1 [else. C: ^. ^' d0 }- O
5 [5 u0 h) L" k( O
{% p- [2 T& S1 ?1 t* D8 Z8 m+ m
9 d; y S9 m9 E, y5 g" O# w, j) [( iLobj = objective function at a2=L
. [6 h. o$ K" g7 o8 x% i2 l7 f Z2 z7 e" k' S/ _" f
Hobj = objective function at a2=H/ g- P& P @+ H
3 R8 B8 N& V. Z
if (Lobj < Hobj-eps)
" Q# h& F1 k' ~9 d5 \8 x- }
4 w6 A/ P+ c! |( E8 Ca2 = L! m% p( [6 H3 ?; b8 ?1 Y" o
: G, Q+ ?' a7 s* j/ j5 l9 welse if (Lobj > Hobj+eps)
/ M; g" C4 i) X1 Z$ d
$ Y d0 _# j' }- s( Ia2 = H3 U6 W+ V7 L/ V: Y2 }3 f6 C
+ h; o J) q; s. m1 J. Celse* G- u" u* U/ `" t
6 |( x2 A. H" }. V
a2 = alph26 J4 A& M6 B' W: R1 U1 x
8 L* M+ Z) a) ^+ k/ k( b
}
+ T2 U1 A, C$ [( `
G3 O. \1 Q! }if (|a2-alph2| < eps*(a2+alph2+eps))$ x7 O0 X7 ~, @1 D- ?2 _
/ I3 M7 d4 s* W. @7 M/ z
return 0+ y$ T) X( x2 Z" R$ y
1 V0 I# Y/ ]+ ]$ Ca1 = alph1+s*(alph2-a2)
4 H: F! C- o$ j* d# I8 f- g+ e# T8 r2 k' d. B$ l& M# m3 H
Update threshold to reflect change in Lagrange multipliers) Z5 x& K3 {8 o. B! r# _
# d) b$ r l4 ~% F9 u. B
Update weight vector to reflect change in a1 & a2, if SVM is linear! a0 S$ K; t4 j7 t* ?) y. ?+ P
4 a- g% @: c6 R' ?Update error cache using new Lagrange multipliers& O4 C9 ~! W* D! g, E
F0 E/ R6 e9 n
Store a1 in the alpha array" V4 U& S& O1 ~1 t' H
3 O2 S) ~: u( h% j: P( Q: o, |" k9 t
Store a2 in the alpha array; s/ ]! X! x1 \' R J0 i: @4 P0 {
A6 T& C) d; zreturn 1
/ a4 v6 |& p' S# Eendprocedure
9 G. c. m, ?* i5 Z% G核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。( k& _7 k5 F1 `4 k
第二步:实现核函数缓存) i; i/ \" B! Q" J4 j
观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。, I6 O- E: S% I0 o ^
样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。 o+ y3 ]- Q5 M
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
5 a9 ]8 I5 E9 n6 t第三步:优化误差值求解# W' K5 A/ j6 Y, f! j+ F
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:: G- p- h- Q7 F/ R0 g
E(i) = f(xi) - yi
" F4 ^* c8 Z( K8 H* r7 I这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。2 e/ a, b" v# p
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:
1 Q* q5 k, z5 a
6 Y% e8 q% X& N1 f( K也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:& B7 g! u: _4 m) r( G* e9 Q# b
E(j) = g(xj) + b - yj
0 }1 \; d$ e# T7 P$ w: A所以最好的办法是对 g(x) 进行缓存,platt 的方法里因为所有 alpha 值初始化成了 0,所以 g(x) 一开始就可以全部设置成 0,稍微观察一下 g(x) 的公式,你就会发现,因为去掉了 b 的干扰,而每次 SMO 迭代更新 ai, aj 参数时,这两个值都是线性变化的,所以我们可以给 g(x) 求关于 a 的偏导,假设 ai,aj 变化了步长 delta,那么所有样本对应的 g(x) 加上 delta 乘以针对 ai, aj 的偏导数就行了,具体代码类似:
2 N- a9 N ]. Hdouble Kik = kernel(i, k); P/ s7 u) _, x$ z- h" @: a" k4 V( t$ T
double Kjk = kernel(j, k);" u0 y$ g7 q. Z) ~: W% u3 ?( A, x
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];' D0 ?- t/ j( L: D
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。$ F) o2 J4 u$ K6 U; u& d7 s! c
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。2 P+ Y/ p, F' N3 J# [
第四步:实现冷热数据分离
* ~7 r6 a Q8 f J* v# t1 r+ UPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。& U+ _9 f8 [+ c4 q3 |9 f
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。* y( k3 D/ H+ B$ I* s" b
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。 M; U! N, X: }+ T, r6 Q
第五步:支持 Ensemble( @* A6 ^" R2 D4 L
大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。
1 J5 v |3 B5 [! t! I, f最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。+ O" I Q# z2 o* r6 a6 h4 @- R
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。
" [- F5 @- I1 K# q, W; H o- T; i. Y实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
( Z! R& v+ g: n t2 N第六步:继续优化核函数计算4 M P7 S( Z1 L2 Z+ P
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。
2 M. w8 {, x% P& z7 F+ }由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
1 k# o( Y$ K: J. K针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。
$ @- m0 x9 o' h7 a, }第七步:支持稀疏向量和非稀疏向量5 R% e8 S a+ V/ c, t8 ^; }& F
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。
' ^( u# G2 H0 k$ n8 B但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
1 S+ V% r; ?" f1 T/ R3 L非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。0 {7 Q* h$ i7 h- z% `. q$ k% \+ M6 Z, X
所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。& @/ H8 Q0 H' F+ M7 o4 n2 Q8 y
第八步:针对线性核进行优化- W0 ~6 y" d* a* j$ g1 u
传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:" ~7 Y: u" S j; {
K(xi, xj) = xi . xj8 x& e) L8 u& k
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。' E' m- f7 O# s# ]7 Y1 ?
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。 g$ p5 G! K! d0 ]: o' Z' ^" ^3 I
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。
' P) Q7 P7 ~( P9 L或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。# b4 A# ^: `- p0 m
后话' {* t' v- g5 q; V7 O7 q
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。
# ~7 F: _7 b3 e$ y/ `2 H上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。, q! F0 c% C& {- I% n4 A
& E: v% K. Z; u' N% [来源:http://www.yidianzixun.com/article/0Lv0UIiC1 U! Y% o* @3 Q, B& N/ v& l4 N3 q
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|