|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:8 {( ?2 h3 f9 ^
学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。2 f( U' U8 X4 R. Y4 W- L
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:) [+ s4 W7 Z G3 U
& ^ {- s8 q3 Z& ~$ ?
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用: M: N, l' w) f: J/ @' g$ s+ o8 i
Q+ a$ L. n# ]' |8 d
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。
- d; ]/ Y8 D! S. M7 J+ O8 V( s那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。; o7 ~$ s; j$ D7 m
第一步:实现传统的 SMO 算法
% s0 t, H; q) W现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:$ L, U& h% `+ r
procedure takeStep(i1,i2)- \ P6 L1 m5 z( q) N
if (i1 == i2) return 0
! z0 e M, a; D | alph1 = Lagrange multiplier for i1
5 C9 [! Y$ ~' F4 Y. J8 @
5 {$ }& g4 \. C+ b9 O, ^y1 = target[i1]( `/ l) X% L- h* V
4 c) P) x& d& G: |8 Q% D2 {7 K" h$ T
E1 = SVM output on point[i1] – y1 (check in error cache)
( d& `0 K' _$ w+ [% N* S3 t( a% f' N& C) V0 {
s = y1*y2
: ]5 ? s- ?9 e. M9 A
: _, O X! g7 k- P3 p3 TCompute L, H via equations (13) and (14)
2 m d$ U$ V) n8 d* {+ L, S5 ]/ w
1 y y* G; B# k1 w9 ~if (L == H)8 `4 T: Z% y- P
8 R' x7 {0 J6 e& q4 H, Ereturn 03 n" \" p) J7 f0 ]
* }$ [4 d* B5 u \* R5 a
k11 = kernel(point[i1],point[i1])1 q! A0 p' w; Z
: t p! p0 J) B: h D+ Mk12 = kernel(point[i1],point[i2])# n9 i7 _/ i7 P: P
3 a: b, _' b% p' q8 {: `, Nk22 = kernel(point[i2],point[i2])4 p' n4 u' [3 i! l6 n
: w5 @. e" n7 i! f2 s
eta = k11+k22-2*k128 I) B4 \; A! A, C
3 d( }6 I3 {- @) v
if (eta > 0)
5 y+ a- L; X, P4 M: I+ \" f
/ R! S, w8 B. Y6 G7 y9 f% N8 c4 Q{
" ?5 Z3 Z! H1 p3 N
! j4 J5 _8 R3 a/ k. wa2 = alph2 + y2*(E1-E2)/eta" v3 P% l5 t9 q. s
+ Q& }) \+ U/ K6 M' i
if (a2 < L) a2 = L
7 |+ o+ C5 G: a* l& G
7 u2 T% S2 T/ pelse if (a2 > H) a2 = H
4 f! K2 g; P6 _/ f
( E) ?4 u( i5 q J1 f4 s}
& T& A# B$ m+ m; t, s/ j2 J0 _# }% r8 W* [) P. Z
else% _* X. Z1 a g5 N0 l* o& ~/ H% W
4 g: t3 w' s# g! J9 O( d" g' p{1 b" r* C) }- J. a( q, O
$ P' P3 P) a) X @$ h3 iLobj = objective function at a2=L, g) d2 d6 j) W2 w, d( H: ? S
2 Q9 o# \" Z7 n' a; k4 N8 h5 j* ZHobj = objective function at a2=H
, W9 N6 t% G9 E9 s# s5 Z {
+ i( o- w0 V! V& c" nif (Lobj < Hobj-eps)) n1 ^; n" p0 h! }' {
4 \9 K( \9 Z. d7 Y- b0 S3 Q" Va2 = L
! r- z1 K* \0 l* [/ t; K3 j. K7 I2 ^
" O5 _0 `; B6 q# t0 q$ c; helse if (Lobj > Hobj+eps)
, I' w$ n3 j/ P3 K2 x6 {6 \# p
4 f+ p `: O! Xa2 = H
6 f0 Q0 k; ?+ j7 j+ w% x3 q+ l8 n V: U, U8 l, u
else8 c6 h: @/ n4 s" I
# [9 {) d# P" X
a2 = alph2
9 i2 I8 m$ Q* n0 j# v) n3 N* ^2 T' K+ }( A9 A3 r$ f
}
5 I/ U7 C, y% O$ T0 _8 y2 v; S O3 F% z1 C* c
if (|a2-alph2| < eps*(a2+alph2+eps))
8 f# |5 G* L' C# }5 g& _3 r9 Q. Y0 H e( V5 K' N
return 0
9 ~$ d) n) R7 M: R# H; {; e: c8 B( H6 w. T, _' J# [
a1 = alph1+s*(alph2-a2)
. d; R7 n/ B6 j, K2 F4 v
( q5 N6 R8 {& W |8 V. A, GUpdate threshold to reflect change in Lagrange multipliers
0 M2 S! c7 Z J. ?' |* w
5 b' g( z9 M9 s& w. E- j2 JUpdate weight vector to reflect change in a1 & a2, if SVM is linear
, e; O" d/ b# t; K4 _7 X
, \8 X8 d7 E( d C2 C- U" iUpdate error cache using new Lagrange multipliers0 D' }3 Z) A- y" V9 G/ z
f8 ^2 x" @" O( mStore a1 in the alpha array, q- o% E5 j5 I$ M5 X& L' ~
' u, C- i4 Q: g/ x! R# o9 D
Store a2 in the alpha array/ i* D. F4 E% D$ m1 q) v# N/ l# _$ ?2 r
L, }) f5 @" @return 12 K: n# h: }) b
endprocedure
3 `( }& Q0 |" n0 H) I2 u9 \. t核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。- T, D3 ?& w8 K6 Z2 q
第二步:实现核函数缓存
- l' Q' t5 v, z6 I+ C观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
$ |0 B* X+ J. h6 N% s4 ^; j样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。: c9 b- l/ [6 z. O0 n+ S4 n
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
+ H2 P5 Y' k0 m2 @+ x' k* G, H第三步:优化误差值求解
* Y7 g: S8 v" H1 X2 @/ J5 A注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:8 q5 e3 t; e, Q# K7 [
E(i) = f(xi) - yi7 T5 E& G$ {2 N/ r) p" d( k# O) F/ e
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。/ U+ w( r7 ?5 `/ {
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:
1 u4 H5 ^8 T8 E
$ }" k7 E5 G2 _也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:( j, m4 V) P' L1 @4 Y
E(j) = g(xj) + b - yj
3 i# z2 U& t" J/ p5 N& l所以最好的办法是对 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 的偏导数就行了,具体代码类似:; n4 `/ n, m0 K I8 ?- p
double Kik = kernel(i, k);" I9 n! }7 L( J
double Kjk = kernel(j, k);0 f) p7 s" `' B' m7 [; d/ f" C
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];
1 Z" w! k! t6 `! x" b把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。( Z0 S" ~6 Y4 b" j- u
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。
L$ B: `! n; u% {& n第四步:实现冷热数据分离
4 ]0 U: W+ K9 `0 z: F" zPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
4 r( A' N- Y( p+ `& H, C7 C" M+ N4 S$ G那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。
3 P& Y5 b; f6 X/ e随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。
6 p" i/ [9 n! q v5 k第五步:支持 Ensemble
& z5 h9 N4 a- N6 l9 r0 b大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。7 R/ }* K9 d+ G/ p1 ~( f3 N; j
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
' u: ^( W: ~5 I# t1 y3 g O- p% ?+ ?这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。, J% e) o/ b' j0 ]
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
9 @! ]+ @" j% X* d第六步:继续优化核函数计算6 d$ L% _% |2 c% H$ S5 s: r
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。- C5 y8 l [& b* Z" p9 A& ]
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。 |. H; D8 C8 v2 z* e* x
针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。# X7 B% O9 g/ w, s& D
第七步:支持稀疏向量和非稀疏向量
7 ]! Y9 w4 B2 t对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。 U! Z, n& G3 G' A$ l
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。: B/ x. Z4 Y0 |# F& u* Y
非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。2 S; l& t. Q8 ?7 y+ E4 g& l: j
所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
& M, x7 N. W ^! |. q( O4 @. k第八步:针对线性核进行优化9 ^+ z0 b4 _1 P3 u6 t
传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:( B( K# v7 j% j' O! Z
K(xi, xj) = xi . xj* c, t/ S- W0 L" k. ?9 u
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。
n$ b9 T4 p2 r3 \ Y* c同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。
# O2 ]2 t/ F4 z但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。
* {( h3 L0 Z! }) q6 W' H或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。 s# h9 F; J$ M2 ^' b1 M, w
后话$ A2 I3 T7 P6 G, Z4 N
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。; W' X+ ~, P G9 |$ s8 }8 l
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。( ~: `7 |( @" ^/ K6 Q2 y- @' C6 r5 n
& Y; U( T3 v2 g7 ^4 u; {1 L( }
来源:http://www.yidianzixun.com/article/0Lv0UIiC
4 L( Y3 O2 ]3 [' g+ g免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|