|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:- [5 B0 U) R( z9 I' G+ W, D
学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。2 u1 p! |: V% [( ~6 o
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:3 o( s6 T& _* F9 y/ c; Z0 T$ v0 K1 c

9 a5 A4 \3 x) `6 o/ hSVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
9 r: z+ B ~# d2 B9 O- s ^& z
5 k8 a! E u- i上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。! e% m+ y L6 H' E9 X+ r
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。 q _. F1 ]; Q1 \/ U" Q- n
第一步:实现传统的 SMO 算法
% ~- ^ K& S0 R现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:& x2 R1 Z1 P9 U; H8 H- \
procedure takeStep(i1,i2)9 D+ d4 c% K7 t2 [( F5 p
if (i1 == i2) return 0
/ u8 N6 |( x! B4 L* L( m$ @ alph1 = Lagrange multiplier for i16 y. C5 V2 S5 A$ {/ a/ d
8 m2 N" G" v* o" s6 Hy1 = target[i1]
9 n8 E+ E' }1 W! m
3 F' b9 c* I4 {- b) [E1 = SVM output on point[i1] – y1 (check in error cache)) d+ A( F9 f: }' P+ p) P8 e0 ~
' `+ C4 u6 D; h& h! Z! Cs = y1*y2
9 g; D# \" h2 x, Y; a7 z: q- y
$ T2 C/ B9 {: |) d. J& e# KCompute L, H via equations (13) and (14)
6 C {0 z- p8 y( A
1 O( `- p- t' e6 [9 tif (L == H)6 e9 y/ h2 E6 S9 ?
2 w% n- A' z# C& x- P6 treturn 0
, @$ t8 e0 O$ R. ?
) J% X! d; m+ q5 s2 \0 d& s( Wk11 = kernel(point[i1],point[i1])
, }. q+ B0 I) R& V/ m2 D$ a7 X' g2 p
# ]6 k) s |3 F |3 m! Ck12 = kernel(point[i1],point[i2])) a9 B& L, D& M& P
4 _. r2 l0 N- B9 L8 Y
k22 = kernel(point[i2],point[i2])4 c# h; p- V9 P( ]! w8 X& ]
) |2 ?, t7 g. L5 f" T) n* \5 M
eta = k11+k22-2*k12
* }# Z# F' `9 G1 @
0 R/ X+ w6 ~. V0 [. L: w; Kif (eta > 0)* L( V! E3 {: X% q1 A' Q
) t' Y& `# k; c( `, I( d9 _
{7 M F+ j" c" y) [! ^5 Y C
8 z$ i$ L& P. h& B/ X5 w1 Da2 = alph2 + y2*(E1-E2)/eta* x8 U" n! `0 N
. k* `% M3 K3 G/ x, w, y0 T/ Bif (a2 < L) a2 = L- S! i8 _# J5 l( f6 X
E5 [1 x {; t6 C
else if (a2 > H) a2 = H$ Y, Q( U7 F. E! L7 E) M% X4 B0 u
8 O% y# q* J) {- R- u
}
6 n( @9 Y5 t! ~# S3 l7 G; V& y" T5 J/ N k
else7 u" K7 G% i* Q+ ^; X2 G
1 ~5 {9 s/ G) g" [6 ^{) m* O8 [ J# A4 \$ u; q0 p
& `! i8 R4 F! N; z' S# _; r$ FLobj = objective function at a2=L" ~; T2 Y7 E1 Z
8 v. e) H: M; }+ t+ P. D% y
Hobj = objective function at a2=H1 v0 j" N) O) ~$ f9 ]
/ B4 g5 _2 P- hif (Lobj < Hobj-eps)
# g. A9 R9 c) h: H+ j
7 L& l4 o2 |1 Z/ Ia2 = L0 P0 z' z y& H
. C- \* o( p1 o$ r3 O( I1 k2 uelse if (Lobj > Hobj+eps)0 J9 e' Z- ]! c2 K% b: n5 m
& K* x( H' @/ j3 C1 r0 h- ia2 = H
2 k- b D( F% J
( @$ o1 ^) E6 N# ^else
' v* ?7 z4 E" C2 s
% |7 i* V/ N- Ra2 = alph2
* _* T8 N# _1 W: [, O8 R) V5 ]3 W
4 K2 ]7 L {& k' n" q7 T# W' q7 c. R5 ^}
$ x5 b' Q( }; U; r' n; q
" ^3 U2 Q) I+ [: Q t+ @if (|a2-alph2| < eps*(a2+alph2+eps))4 L- s* h; F4 L$ E
- f7 D# `! J! M' Z, @( e
return 04 H3 q! e8 }+ K6 E0 t+ f+ s, r- w/ X/ g* K
, U5 B S! x' R" l1 } Oa1 = alph1+s*(alph2-a2)
6 K" ~6 o8 }7 Z+ P5 P) v1 l3 ]. Q9 D( G. B! O% U
Update threshold to reflect change in Lagrange multipliers+ C& B7 S9 P; v6 k, _! j3 X
' o, T" H8 i; [
Update weight vector to reflect change in a1 & a2, if SVM is linear
& x0 j+ J4 _" j$ R# J' n& c- s8 d. o3 e, a7 [. _+ v& h
Update error cache using new Lagrange multipliers' c/ I" e' t8 @9 y: I
+ Z! T, |/ `- C6 A! f; Q) U; iStore a1 in the alpha array
! Q' ^9 h% @0 Z+ @5 y
- d4 k5 I& p) R: D6 t& nStore a2 in the alpha array
) N9 `; p4 J$ y7 @/ I
6 H$ _. V$ M5 g% Ereturn 1) D& m; q+ }4 a- C% O* m
endprocedure# L; X, Q; ^ w/ ?
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
7 p9 B# f: X4 E# z9 k d" {第二步:实现核函数缓存( D6 k. U6 `( h
观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
0 _) w3 @- b H5 b$ x% b A样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。
* y7 B% a$ c% r5 O' v有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
8 ? `; i7 c" O9 [, [! n Q第三步:优化误差值求解# ~8 }0 ? b2 l& A u& d
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:& G( ?+ i5 ?/ M v. G
E(i) = f(xi) - yi
* S6 e# d# a4 n2 E# \+ I这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。
5 i* A- W4 l' |platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:/ I2 p D" o+ W* R( K! g$ Z
1 ~/ T" L8 e1 F- W- ^0 O& x也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:" t9 Y& Y% k3 a& j1 }* W8 T# v
E(j) = g(xj) + b - yj
4 G3 G& o5 G8 [/ Y; 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 @3 {( o4 R6 s$ Adouble Kik = kernel(i, k);
# s' Q3 g+ S9 Z$ ?0 e) Q) H2 {double Kjk = kernel(j, k);# A7 U% A [" c7 X( H' i: ^; b) H$ _
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];, v) g( e; c. k! t3 T: K) D- w. B' n
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。
+ o4 V2 o) Q9 u: c, P ?1 b这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。- o" B- _! V7 t9 I
第四步:实现冷热数据分离
* `, g8 i6 p1 L% O9 E7 sPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。7 R0 t ~" E: L: D; a t( H* o
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。/ {7 x; k. V1 i- G( L x: h2 c: ^- J+ c! `
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。" _$ t! s ?8 @5 ^4 |4 W+ |' v0 Y
第五步:支持 Ensemble% K$ w; C z' C, B1 r0 a( i
大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。/ [. v% K6 y+ t" V% V( C+ L6 g8 H
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
: u; @$ M' m# j4 `# p2 ]' a2 X' R8 K% j这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。
* P! M$ @ o7 {实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
. R) C6 a7 r/ w7 i7 f; ?第六步:继续优化核函数计算9 `8 [. t2 D: E+ Z& d8 l, _( t
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。8 a( A4 J; o0 e# }$ p
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
; o O- x: p2 `' ]! L. k+ N针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。
+ y& E% ^0 X# v0 m+ }6 w2 g; y @第七步:支持稀疏向量和非稀疏向量. B. C/ z8 E3 K
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。+ N( N, X2 F: Y3 `" b# V) H- h8 L
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
8 N5 N: V" J9 r1 Z+ R非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
# [/ X! r/ x& g4 `: f+ X# K所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
8 q" M# G( n/ }6 A! j& {" G' y1 s第八步:针对线性核进行优化! D- c2 b7 D) _. {
传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
+ i( q0 C5 k: |! a% ~4 w1 [) nK(xi, xj) = xi . xj
m( D8 @, J7 J1 l O还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。+ B9 ~( \& E. t( t3 s# {6 T
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。 x- y; o' v/ Y. r0 o
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。
0 Y9 y/ x' [+ b, l7 m( I或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。+ O7 R; n7 b- N7 t& r6 w4 c+ S
后话9 H. r' e( i# X
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。+ U; X( \+ s, [2 K
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。
0 l2 P( G& W, G, H5 p3 R) Y5 G+ h1 J& M8 A
来源:http://www.yidianzixun.com/article/0Lv0UIiC
5 J- i& [6 c9 U( C' x6 j免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|