|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:2 q% F% B/ U4 @$ C
学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。
# I5 V: V: z k: }9 X" b) Z- ]& h) l假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:& M3 S" F) o3 v) T; f# B4 Y
) i9 i7 x3 i* }, f9 w5 `; ]! I+ w7 ^
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:" t* t; {, j+ g- O# l; B- a4 {' u# t
# P, P: C/ T( N% o4 C
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。2 v, }9 V2 w u2 w% ~. s. S, r# K
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。) W9 K( S0 g/ M: u2 K
第一步:实现传统的 SMO 算法
2 K" i1 L) S! q3 f现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:2 z/ B9 e* k8 z) ~
procedure takeStep(i1,i2)8 H- u5 |6 N& |
if (i1 == i2) return 0( T9 ~. [6 U/ m# t0 k
alph1 = Lagrange multiplier for i1
7 x, d1 d1 r2 H5 J1 h. S
/ w6 H+ B1 j# M" A# l1 \/ ey1 = target[i1]
! N8 Z: r! t1 d& }4 C
+ j6 A/ I6 ?1 R. T6 w2 kE1 = SVM output on point[i1] – y1 (check in error cache)
% \' _: T( |+ |* }8 |
/ ?; _7 R6 ^: U- fs = y1*y2$ ^2 f- M9 \2 v
- Y8 a5 o/ L; T, d6 s T" BCompute L, H via equations (13) and (14)+ f) U+ [- y4 b3 | e
. B8 b6 O# k. u e, l" Q/ \1 t
if (L == H)- k1 b8 q& p. L" n# L
( a/ n& H- g6 o$ c* ?
return 0
' D0 X4 p* W; p: Y4 F' u: b! N
. y# [ g9 U( p, @k11 = kernel(point[i1],point[i1])
4 I1 u- G5 L8 H* U, H1 p* [7 e/ B* c4 P9 g8 n$ B; w9 n* s
k12 = kernel(point[i1],point[i2])0 }" G# P- Q/ m, S
% ?5 u& R6 H' {6 N1 I3 l4 v
k22 = kernel(point[i2],point[i2]), h" z- C; I" ]! ]
( H5 q9 |: _. i+ D% peta = k11+k22-2*k12
: O1 m( Y& h+ Y! y5 I) {
1 x6 a4 [( b" e9 Rif (eta > 0) z7 A! F' g. q6 {* `' c
8 s! A7 O! S5 d, @% O% `6 H) ~{& `0 {. G' `9 w
3 p J. }6 k% V2 w- ~a2 = alph2 + y2*(E1-E2)/eta* D! |. u: W4 ~/ C O( F1 O- b' ~
) u+ K0 b, K4 \& T7 x
if (a2 < L) a2 = L
+ g/ E) K0 I; E& J$ B& i6 {7 W- I) r$ A5 O1 H+ R$ y( R
else if (a2 > H) a2 = H
2 R3 w3 {' _- g# G; b; ~+ A! k0 }( v) Y! s
}+ Q: Q3 c+ ]# u9 }
5 p; a- v9 c/ _
else
1 O* e) n T8 z' [
% g9 X" S3 Q4 q# a3 D{
z9 G) a3 D& y; @4 V, K2 a7 f
" e# |$ A0 F9 u2 ]4 w: o' cLobj = objective function at a2=L
$ R3 h8 z5 w- V* `( ~; m
4 s- e- H+ t- s: \, E F* ZHobj = objective function at a2=H: {) V: b! W+ o
, N2 V% i/ V, v) {6 P# E9 xif (Lobj < Hobj-eps)
6 a: t5 `. g# e3 B0 W- o$ I! D! ]5 A1 c3 [" E
a2 = L% H/ I: O% N6 j3 ^# s
$ T1 f ^! e2 K" l" _- \else if (Lobj > Hobj+eps)
0 B) F1 [$ N, x. X, @& G3 l' n8 ^5 A2 u+ B8 f$ i
a2 = H
' f7 G2 m1 f# z9 X O5 |) m: h
, q/ x0 ~6 w: y6 [; f- zelse) e8 A+ p8 I6 P2 N; y" o
2 R& @5 h0 G- {, o( q3 K9 V7 Ua2 = alph20 V+ n8 t6 y ~% w
# Z5 B" E6 r: L" V" u
}2 y2 ?8 v% x, H6 W! {
6 h9 ]3 P2 l y8 Y1 zif (|a2-alph2| < eps*(a2+alph2+eps))7 g) W9 `7 ^) X- e$ w
4 J; H5 d# N3 Qreturn 0: A3 h. F* h+ U7 ^& {- R
/ D1 K1 w- V @( d! P1 ~
a1 = alph1+s*(alph2-a2)9 D7 L* b% g* t
2 _. [* P2 z7 _ {# ~1 R0 ]7 kUpdate threshold to reflect change in Lagrange multipliers# G! z7 q5 ^' A' ~
+ }3 p1 I2 {' m: c& n4 O
Update weight vector to reflect change in a1 & a2, if SVM is linear
2 I# U" O, I! c/ N+ f: i1 u/ d' ?& J+ e5 n) P
Update error cache using new Lagrange multipliers" e q5 R7 S( W0 T& }$ X
9 Q; o4 M# `2 [" _: K u7 v
Store a1 in the alpha array
2 r8 h3 E' Y x# T! v U
' W: s/ p) j4 s7 Y* MStore a2 in the alpha array
" v: r9 D( k& W2 w
5 |! A7 K( y( sreturn 1; _( }/ ]5 [# g7 r( G% z
endprocedure6 j3 }' c8 R: @" k+ ` e' x7 K! K
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
O" m; q1 m3 C: X第二步:实现核函数缓存- _% w/ o" x# Z3 {
观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。6 H" S4 M6 ?4 v
样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。
( h* S2 }6 F0 l7 P! t6 N有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
5 _9 J1 H1 ]! [/ C7 m第三步:优化误差值求解& T, E. n0 ^( z! c; W W6 s
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:# F8 c0 v/ B3 K d
E(i) = f(xi) - yi F Q5 F$ c0 y2 t( g
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。
9 G7 U$ L( h4 Uplatt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:: ?; P7 a; s4 \) S0 ^
& _) _$ `! c% Z4 j/ T
也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:* `' J3 E: h# F, O
E(j) = g(xj) + b - yj
$ O9 ^4 k& x: R0 v6 f所以最好的办法是对 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 的偏导数就行了,具体代码类似:7 z6 s3 R7 s7 ?. g
double Kik = kernel(i, k);
6 M# F8 ]0 F N" m3 L! R) Idouble Kjk = kernel(j, k);/ e# T/ c# ?+ v, T
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];& f/ R4 ?- y# h6 v
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。
! i% g1 _, i) f# O' j. p这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。
8 {/ R# P6 s& @, Y第四步:实现冷热数据分离9 f' B3 `! s: r2 c3 h
Platt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
* B, h% R- ]4 r4 y' C那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。% i% l; I! a; ]
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。
6 S% R$ |% y3 e4 x7 I- ~5 }5 h第五步:支持 Ensemble% O" k( I/ m" b q3 t/ Y: L9 x0 g
大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。. K( b) y! o+ F& f* L; s% O
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
3 B' j- c" ~" A( q9 ^这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。3 g& x- G, G1 @
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。( F3 K; d0 ~+ n9 Z; j. ~. A
第六步:继续优化核函数计算
: t! f: N1 s/ e核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。/ {' e% P$ _- Y+ _ L
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
% y; |* F2 y0 ], v针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。) W s4 u/ f. {: b; h& J( U+ g6 N
第七步:支持稀疏向量和非稀疏向量8 u V3 y- f( c* W: N
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。5 {- r, ~9 g3 d, t- s1 G
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
x$ l+ B4 b/ j# {# K) A非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。; R7 N# A; l+ t# h! w
所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
' g0 T3 y* z1 Y- V第八步:针对线性核进行优化
$ ]; O+ h- ]) J- w' n6 y$ t传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
2 F( H5 j m" @8 Q8 PK(xi, xj) = xi . xj; }% b/ w* R2 g! y! u
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。7 e3 `/ g1 W8 P+ ?
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。/ v8 I# x& o* Q- G" C' }6 B
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。3 Q$ A$ h# P( \: F
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。# j) p5 q7 i& K& p7 y* W v( ~: H! Z
后话: y- H* Z, n# }5 J
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。
" Q K0 C A- i7 L) O' _6 X. y上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。
0 j1 G/ r( G3 \4 O; |' s6 M5 n1 b6 ^9 K4 D
来源:http://www.yidianzixun.com/article/0Lv0UIiC
8 o* V$ r: n5 O: u0 V免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|