|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
+ {7 t" O$ a* c学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。" M: c# |0 @/ d3 U2 g9 U
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
: s) o; f" t6 z4 D. j ; S! F( h$ C' S2 o( R, f
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
1 N- c C/ b* s' L* n/ b, X / C1 I; [0 J$ D) ~0 ^
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。5 l% g( v0 g* N2 u
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。
+ D) D5 _0 P& S6 K, {7 n7 E/ t第一步:实现传统的 SMO 算法; T; v% }+ n0 b6 q( _3 t. c
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
, h: S) @$ @0 C1 G4 r: g! H! T8 I+ @procedure takeStep(i1,i2)/ C1 l1 P, f d) K* L" _/ K
if (i1 == i2) return 03 y( n5 n6 P/ n# x7 ^8 K. q
alph1 = Lagrange multiplier for i1
q# H" T# ]) W1 x; u2 d5 x0 Q3 W7 K ~9 i1 O
y1 = target[i1]
: s: A9 E* p' T. x V) t
1 d' u4 E! k3 }6 K8 U) a6 yE1 = SVM output on point[i1] – y1 (check in error cache)
2 @2 w$ ?5 D2 R* B- H; G
. ~' _9 Z/ m& Ws = y1*y2
4 ` m# N, ?. C/ B; B
) ?+ k$ R& X9 g" F7 g8 PCompute L, H via equations (13) and (14)/ `, B! b3 ^* O# c, V3 a
9 Z" M4 C. U! O" w0 Z/ o- w
if (L == H)* u: U' N. M5 j# g8 I0 r
3 Q4 d9 \- {2 O! W, v a! _! l
return 05 v2 P' ]( u7 E0 [2 }
1 q. \( E3 {: j9 @) ^k11 = kernel(point[i1],point[i1])
+ _$ ~0 s; P( n1 H' a7 m" E9 O2 n( n M
k12 = kernel(point[i1],point[i2])
3 o h' l) c' G, P; Z
: Y- }' d, U3 Z" k5 [k22 = kernel(point[i2],point[i2])
9 c8 U- Q+ C. `# ^1 W
2 K+ h3 d8 x: V# ^6 C0 r9 eeta = k11+k22-2*k12
\0 n7 p" @1 J- u! S2 J+ d! S, y8 w7 P2 z3 b# {
if (eta > 0)
9 L k4 g8 [. _; g4 p* R/ S. b7 \) D7 d; M1 s8 e' _1 a- A
{( Q$ y7 @1 q! D( [5 K2 [5 U
3 @+ }" ^5 j# S, }- ?' p# S: y+ Ja2 = alph2 + y2*(E1-E2)/eta( u0 ~0 r& g8 w' x
& P% L' D: ~( b' D
if (a2 < L) a2 = L, |3 v: V+ Q; u" g
% U" y( y+ z' I$ Z2 m5 w! u
else if (a2 > H) a2 = H
9 O7 ^" n6 j+ x4 L/ \
$ ^( x$ C3 B8 D. Q1 I" M}
~( S0 L# Q: V6 \! d) a/ x' E, a1 t
else" Z/ A( b7 i% {' w; u
T& p" V; J. Z+ Q& k* D{4 [; Q; t4 s$ B$ c% I
! n3 p0 l8 C) E& e2 gLobj = objective function at a2=L' U9 J: n% o4 ?7 w: v- d |+ g
) ?; R- d7 V( ~! E2 \6 D( W
Hobj = objective function at a2=H7 |% ] _% a# N. g. ~
0 U/ A, q! i: {5 qif (Lobj < Hobj-eps)1 q9 g, j5 z# m+ j' {
4 Q5 b' R; Y# u$ da2 = L1 \7 ^; {% r5 i: C& C- A. x7 c
" H: X q5 d) Melse if (Lobj > Hobj+eps)
8 D" I5 X; v5 b; T M+ H, `# e$ s, ^8 K' s! l$ M
a2 = H+ D0 O6 R1 Z9 G3 D
6 W3 T/ B! d0 U. o4 q7 L3 n" telse
, F* \: [( U6 B! j/ l
9 Z9 c' N% `/ v. K8 ka2 = alph29 {* p& c$ ]% {& l$ |5 b
; q# O5 i2 l. o+ Z+ `. ]}" p( { O( P4 p7 [/ W4 Q: b" _
7 U9 e( I$ _8 w% ]7 W* Oif (|a2-alph2| < eps*(a2+alph2+eps)); A9 g9 R* D5 e7 H% [/ P" g1 J
$ T: N8 l* s& H9 V) u# Preturn 0
- B- O. s1 T( \1 ?, `
, F4 C8 z! V. N! f9 f) la1 = alph1+s*(alph2-a2)
% A3 a7 X8 \ K1 p
/ Z7 m1 V. I0 i- P( N) P, OUpdate threshold to reflect change in Lagrange multipliers2 X3 C/ A+ R$ T# y2 m( [
8 h$ O8 ]1 S+ l* Q" F: X. K3 N9 m. }Update weight vector to reflect change in a1 & a2, if SVM is linear
& }! M1 K7 q7 F6 E9 Y- S& b& J% f- t. d$ [; L5 F, D% ^8 R$ a' f
Update error cache using new Lagrange multipliers( N' @& ^0 Q7 Q+ z' _
2 E3 l' v) A: p k* UStore a1 in the alpha array }* A; m/ N3 h0 i7 A% M8 s3 Q
" T" r( H. x2 [9 D+ Z. zStore a2 in the alpha array
Y% Y1 S4 c; ]9 f4 K, n1 O8 z h
return 1
2 E0 v( @2 B/ r0 p8 l# k: q/ f$ rendprocedure
4 L- c( U8 C/ o( j6 Q核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。% Q J8 Z* x/ B
第二步:实现核函数缓存
+ ^; i" r6 ?& M. {观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。6 d4 Z9 y7 p D% N! g! }% V
样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。0 k( u8 w6 n3 a6 U, c# Q7 u
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。5 v* w: G0 b! y- p" |% U$ t
第三步:优化误差值求解
3 |# B; \. x" w$ n1 _# {注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:3 e, P( ?! }5 ^( ]
E(i) = f(xi) - yi
5 p" a4 r* G1 d8 ?这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。
# R% L% d7 p; ?. l. F8 A7 Zplatt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:/ G1 i$ L) l; G2 u/ Z. u+ D3 _9 v
/ \& B6 {- u9 c+ }4 ~; q' i也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:( w- F1 k6 b6 }, k, |5 y
E(j) = g(xj) + b - yj& d5 F( I( L8 z, G* U/ d% z: j
所以最好的办法是对 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 的偏导数就行了,具体代码类似:9 Q, s, Q4 N# ~, a
double Kik = kernel(i, k);
e. b3 ?; d" p- r' s3 vdouble Kjk = kernel(j, k);) }; D8 E9 Y% ~% w2 j
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];( M1 u _+ x! |0 ~$ Q- H1 D
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。
; }) I. J6 E( k* r" B; f3 r) G这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。2 v. h5 O. y3 r! j* _! k7 ~
第四步:实现冷热数据分离* E2 H/ e( u3 F+ Q9 A$ [8 y
Platt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。! } z/ r, j( v6 f0 ~% v4 ^3 ^6 m
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。
1 W# r! u; w) G! d随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。
( A! y. o& Q( o4 j3 K第五步:支持 Ensemble9 B# D' g* R& q0 n0 v o
大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。
( ?3 x$ \% j3 f1 y* f# ~# r/ P' ^* I. r+ X最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。+ `5 k4 Q6 [3 [; Y C+ {& }0 i
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。" f. j4 n- [ D5 T T; U3 j" F
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。9 O9 J; ^, G3 m& a7 p
第六步:继续优化核函数计算
9 \. d8 t/ W3 K核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。, W3 o; F9 r' @" `2 m) }; `3 A3 H
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。* f' V A/ c( ?7 ^! Q# f; M
针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。+ ?: e) K- N) ?" \) B* ^ |5 N
第七步:支持稀疏向量和非稀疏向量
2 h3 U! |$ k# I; O2 O, a% [对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。- w' g! U7 h7 r k# W, h- P
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
, N' [' o9 m) t) g非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。; i' e3 |0 s) ~
所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
4 K- t! v2 _9 O7 K/ J第八步:针对线性核进行优化
2 E* W5 q% t* T5 ^0 v# G' `传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:4 h4 c8 b- W0 o. p
K(xi, xj) = xi . xj0 P+ t! i6 p4 g: M0 i
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。2 f+ z8 V2 k3 r) N- x" q' n
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。& {3 O+ q2 q/ i$ o1 U
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。- ]2 @$ P, U5 Z/ F. Z
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。
7 n s. v: t/ F% H* _/ H后话& r7 t* H+ I" W
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。+ Q X* z9 z( r2 ?/ t% s
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。3 S1 m! L+ o4 D6 R- @5 x
9 v% L1 i7 e: S来源:http://www.yidianzixun.com/article/0Lv0UIiC
: {" s! i: \, [+ l& v' x- a免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|