|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
% P; A' E: G) R$ V2 {学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。! a A3 Q; N0 w3 r
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:3 Y- C( A6 S' ? n8 e" F$ \( y" }
/ F5 \, w; c* S
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
- g1 X# Q: g/ O1 x% R
" h3 p, Q4 ^, X0 U上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。
, v' z0 g \0 \, r那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。* w u, y. e* f; V1 J$ K' m
第一步:实现传统的 SMO 算法
v% _ z* l5 o! @* ~现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:9 Y) l$ T! l+ U+ s& J9 j+ a6 }
procedure takeStep(i1,i2)
# K( I$ }' E+ a; z$ I# z" D, K if (i1 == i2) return 0
- U# P% V) S% X$ X alph1 = Lagrange multiplier for i1
( h9 @* a p- ^: x: V1 ]* e; Y1 O3 O6 I4 {+ O
y1 = target[i1]
. O( W. p9 \" C3 W) E
3 M$ c5 B8 Q$ N& ME1 = SVM output on point[i1] – y1 (check in error cache)
Y/ d/ t7 s0 g4 B( G8 ^8 q! q/ X0 x9 x1 V) T4 j5 d/ S; Y& ?- P
s = y1*y2' N0 n9 e: ^+ \5 k( j
0 X. y% @' K. U; y* [
Compute L, H via equations (13) and (14)# N0 s' X% M3 ~2 J+ `! {, w
& M* x; e8 @4 P, X- P& ]# y
if (L == H)
% V+ b! K; h/ h$ l, e0 E: H
9 t5 P" c( V; b2 w. \4 ^2 Greturn 0( P8 `$ E! i; K" O! k) a' f
' B* e9 _0 w/ Q8 d6 H/ q: ^) i
k11 = kernel(point[i1],point[i1])
5 e T+ \) J* b& C7 P
$ p8 f2 y+ m: a/ |9 B6 p. o7 T3 bk12 = kernel(point[i1],point[i2])
+ _8 { s8 X/ x9 k8 \4 x0 N& `1 N" d& R8 p* P! J7 i
k22 = kernel(point[i2],point[i2])
5 D! ~& m+ Q8 }8 s. C8 B( K6 X
7 L( W _4 ~, p, {eta = k11+k22-2*k12
' S6 p. W4 D1 A9 }! Q1 I" k8 ~9 `1 z1 o: ]8 T% Q; f
if (eta > 0)( B& L; J' X% r' p
2 }* J8 \' c% k x9 w{ u. ?# u4 A/ E" i7 S. A
, k/ G$ K: V$ g9 g" `' k- g, Ra2 = alph2 + y2*(E1-E2)/eta
7 W" P8 W5 c a* w% c+ F. w0 o: z) _( @$ m8 v
if (a2 < L) a2 = L
$ J* v- N, i5 p7 t, [& |' z$ w+ p9 T4 d: y& I6 `
else if (a2 > H) a2 = H
/ ^1 k% t7 R7 t
# P) z8 M9 V- f2 w7 Q}- c L7 V; y% w5 I
: \1 b( O6 D% ~) `: B" _& |
else: |3 R- c5 r) ~* y% h; l' F$ q
3 Z4 z% [) w8 ?7 A' C
{
% [0 N* {" n G/ H( G/ z
. v( C2 r1 }9 P- r. b, G. d7 sLobj = objective function at a2=L! Y. f M; x' Y$ a4 i% }
1 M' n0 `- ]2 r" B5 g0 a
Hobj = objective function at a2=H
8 X$ a6 X2 x. d4 ^. v M/ |
0 L+ l, n1 v; y. f, u& Aif (Lobj < Hobj-eps). [2 ^6 t6 b A) p/ C% l/ x
3 {% W- W3 _8 [6 v+ J* e: ~
a2 = L
% m. w' s! p9 k; i9 W7 h. A' }2 w# S
else if (Lobj > Hobj+eps)3 u8 t2 G9 I; W% f
0 g" K8 ], P8 [" p% j$ c
a2 = H
2 I1 D' {; Q$ Q& q/ u& w2 C- D7 ]8 \8 O8 k" H
else
5 `- w6 O% _4 `7 [& D- u0 F0 f" Q4 T2 q' m W2 H) i' O
a2 = alph2
, |, V: g z+ m* q) G9 Y8 N$ A' Y V; M% k* a. G7 s9 Y: L
}
- \- O+ x0 j# I. W2 x) \7 [" g9 @7 n- Y
if (|a2-alph2| < eps*(a2+alph2+eps))" O7 J/ g; K8 H n F1 U7 z/ x6 s
8 {! R: A) z; y0 E+ Z u( f
return 0
4 k( v8 B! |8 p& ^ [( `. r5 d5 D' t% G( i# B
a1 = alph1+s*(alph2-a2)
. N. T5 S$ Q. u' ^2 F% O
( o8 j* d5 F5 I h* ~2 Z- b# ~, b: x! fUpdate threshold to reflect change in Lagrange multipliers' e6 b8 ?" N. F
& X/ Y, s+ P- \: K6 q! W: r
Update weight vector to reflect change in a1 & a2, if SVM is linear
; M, e. a5 b& x& z* y- T
6 j6 g: x# W3 ^9 v9 y# J+ Q, e/ zUpdate error cache using new Lagrange multipliers$ B3 C/ M# f" r+ p# ]
+ d. j: ]8 r; {. j0 o
Store a1 in the alpha array+ P! B2 C8 E* U+ C8 f
, s" W. b- z8 ^- a, v
Store a2 in the alpha array
( L; j* G. g' [4 o# R, n' q
8 S: k9 w& a' s$ Q: Ureturn 16 s! j( D0 W7 ^4 h @$ X7 X& Q+ X
endprocedure
) @7 I0 d- c8 n; [- {& |9 v核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。 `$ m3 K8 M0 r4 d4 ?/ l( H3 J7 R( H
第二步:实现核函数缓存
6 X- k% {% @& t: I+ S观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
# ?+ k( h0 r: y6 N% G样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。7 {9 Y/ z( x1 p0 B* u
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。4 z* _( i6 b8 E
第三步:优化误差值求解
! v% o% t5 ]. n) a2 {" B注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
9 @( \$ y/ n# n' TE(i) = f(xi) - yi
% j) a" B) h8 t2 e5 O1 ?这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。( d, f( p8 B1 C: h1 m1 u+ H+ {- G
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:* o* G4 ~% H/ m' z3 K" Z
& {( a" {4 l/ p! X
也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:
6 g' [# b# r$ M |5 ?7 v jE(j) = g(xj) + b - yj
0 ?* x# G, `0 f& Y2 a/ e所以最好的办法是对 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 的偏导数就行了,具体代码类似:( z9 e+ q+ ~" v5 D6 P2 i
double Kik = kernel(i, k);% S- s* O/ V- A. A5 j
double Kjk = kernel(j, k);# R! x7 W! {; l. ?+ _6 O( {
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];9 r- c5 O; U6 p5 ~9 q! p. I6 B8 z" W
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。4 C& E* `" n+ I' y
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。
9 H0 c3 e9 x o$ E- W" C第四步:实现冷热数据分离! g5 t6 l% C4 D7 r
Platt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
' _2 e; }/ T9 v4 u0 K; w( `那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。
9 B1 @$ M! x, h. B1 [/ o随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。4 O! g, t8 V8 I5 d+ F. s( n b$ d
第五步:支持 Ensemble
8 v1 R! `% ^7 V& R大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。: c- a) z, E: C, }( ?1 I; s o
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
- `, t! v: ] b% b这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。* i4 f! V( C; ~2 @
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。" Q6 Z* o) @+ g6 W
第六步:继续优化核函数计算! i8 w9 k S2 r' U* t
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。! D( N8 o# \3 o% G3 S( b# k
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
+ q1 Y; M8 c$ k针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。7 ]3 R# v! P! {
第七步:支持稀疏向量和非稀疏向量
& a3 }- m, A( F: R# ~: V$ ~对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。
7 ~+ Y R" H6 w- i) w但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
q; p7 u, |6 ^非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
0 b8 |8 }# Z! t j. E% g- e' K8 e所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
+ \7 H4 k- I7 i% ^. g第八步:针对线性核进行优化) a6 [" [2 P8 I0 D! m/ s& ]9 w7 K
传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:5 F) F0 H' }* L2 d
K(xi, xj) = xi . xj
6 @1 d w0 s) L" E8 W& v* x- A, m还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。
3 E/ Z. J) ?! |2 e同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。
- q3 D) l% R) N3 h但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。
0 N" q6 ?0 ~3 \ r' ?6 m i或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。6 Y+ d( [7 a* q
后话
* [" m! U1 m" M' L, c上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。+ P8 L9 q4 p: v% E& }7 N
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。
# P8 X% C% X! n( M5 u- P: j6 l" S' \7 M! U6 N, [" ^
来源:http://www.yidianzixun.com/article/0Lv0UIiC& M" L9 B3 _( F& E( I( |
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|