|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:* _7 `: T4 J% y; `
学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。
6 \: w, [+ x+ U- T假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:( u0 T: d7 r* {4 N# l. ]6 q- r
+ e# L( K/ c4 W
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
4 f O% Q- y2 d% ~+ }2 C
7 e) W3 @( }6 {; ~* ?7 S) \; \上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。
: S! D' |* @9 S k那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。
1 l6 o- U5 e* [" w$ F0 i+ N4 T, T" Y0 \第一步:实现传统的 SMO 算法: V0 f5 P5 o0 m% \) A8 T& k
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
# |5 ?; y \) P1 c* x) Bprocedure takeStep(i1,i2)
1 f9 ]- ~1 F2 m! Q8 } if (i1 == i2) return 0
, {5 L* U) I! V; o/ }' U' r# h6 I alph1 = Lagrange multiplier for i19 Y) U+ d3 q. m d: }5 |
2 |. ]+ ?( L( g+ t% i
y1 = target[i1]
, M _9 k2 i6 E( s0 W0 @
5 t- a; W4 |/ ^, x& XE1 = SVM output on point[i1] – y1 (check in error cache)
5 G! e6 l. U V1 ~
( e: n1 \& `. n* y" E t5 Z9 ?s = y1*y21 r5 B. v' V! {' k3 b
( b% l# ]' s9 Z( X" Y$ H# k2 X5 i& j
Compute L, H via equations (13) and (14)6 I- }/ \8 g2 A9 {
& a. z+ k; ?3 `. t* L6 Vif (L == H)
. h: k: T! U+ W1 Z/ l5 p) C. N" \' L/ k
return 02 }+ D: n) `$ J; m' z
& A4 f. ]8 X3 H' }/ u9 U; _k11 = kernel(point[i1],point[i1])
9 Y! U: B0 W& c Z/ ~9 Z! H4 k' z
, u! y4 E+ P( H2 y5 _k12 = kernel(point[i1],point[i2])4 q2 i$ Y* ?" V+ p0 m; Y; G. b
! i/ y5 C! @( s1 @& b+ x5 s3 O J
k22 = kernel(point[i2],point[i2])
: E7 j( e! ~/ d* S* Z6 k( p2 i5 s+ Y
eta = k11+k22-2*k12
/ l) r' w: z4 g+ e1 d$ m
N3 b% w# l8 v# G0 S" k8 E% r Y% Rif (eta > 0); K: D; i1 ?% E3 `
6 r0 z6 X: `5 R6 s. T4 N
{4 A; Y# l0 v! ?8 c
& W% D6 x8 l* w# \: ca2 = alph2 + y2*(E1-E2)/eta2 J8 y% }/ P% |- f: Z+ X
( w- O/ \' M: L1 l0 u1 H8 Pif (a2 < L) a2 = L
a" k b6 Y5 t8 J( Y5 r$ V
3 E% D+ b# @( t- Qelse if (a2 > H) a2 = H
F2 ~, h7 O# \) t$ {7 H
' B& y; v" M2 d- V3 @7 u}
) v0 J8 N+ i" T) M5 E5 t+ L e" I# T: \& }1 B. }$ l
else8 z2 o" o9 y, B
2 Z0 D, F5 M& e: O
{
2 q8 l7 u1 q" K* L' S+ f
3 `- d! M/ N. d: DLobj = objective function at a2=L
4 m7 v. Q* H! H
) A9 M2 \+ J) X1 q0 Y; U6 XHobj = objective function at a2=H) n; T5 j. y- J) A
+ M8 w7 C& c6 [! t1 M% Q, b4 ]if (Lobj < Hobj-eps)! m6 C; H5 a: y: C& i& n
4 a; v) [6 f- b# X$ ga2 = L! C+ @& ?8 i; O" Z8 K
_ D1 N: j# Z& {% Oelse if (Lobj > Hobj+eps); e6 @5 M2 A2 m# N2 x( i( y: V
9 Z( H k$ O: G1 G( X+ W
a2 = H5 n, j8 |8 P0 D+ @$ j9 T
u0 z" U5 X8 E! L# n1 d# T
else- z+ y) q) r% ^
2 |4 m8 w z& [a2 = alph2
7 q% x: I: `' b9 p2 B
# S7 [; H) l, Y* j+ w; L}5 v( I; S. r: O0 q1 C1 O5 r$ U- Z4 S
9 j4 ]/ S) s8 r* e4 o2 ^
if (|a2-alph2| < eps*(a2+alph2+eps))8 L+ f0 H/ o N8 i0 t
- \2 b- d6 X0 h. Ureturn 0# q0 R/ `9 y* y0 k
! X5 C" J6 j) Y( [- X9 K) b/ S
a1 = alph1+s*(alph2-a2)
. I) v6 m# f7 |* J1 A! u5 R, r, W8 e3 ^$ c! i
Update threshold to reflect change in Lagrange multipliers t% X/ G& M2 \' ~' ~
' L& R, J6 ]8 `& v( u1 w" ~1 ^1 S. M
Update weight vector to reflect change in a1 & a2, if SVM is linear
/ t: C2 M- S# W$ _2 j3 A3 S: a& }$ k. n. k
Update error cache using new Lagrange multipliers
% M' v+ j; K- A' G! [
6 Q3 q. L% r4 e) P/ {/ n2 B" u( bStore a1 in the alpha array
1 a+ X# P, n7 H4 m; c1 |9 k" X1 z5 v) J7 ]7 u4 p& A
Store a2 in the alpha array
: a' v; {" U) C0 z! ^4 F
- v8 l5 W/ |4 p3 E. ^return 1
Y: ~1 g- N1 ~( \, G0 m" Vendprocedure
! G! e+ u" v* `8 M# p) ]& W8 m# `9 B( r核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。0 q5 u: n- c" m7 l, r
第二步:实现核函数缓存
5 C: u' O6 j0 L3 `: f1 X# L- K L观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
- d) j4 s/ Z/ t1 r3 K4 v# }样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。 I: H7 c- _$ O6 L: q
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。& s3 q! K" l/ c
第三步:优化误差值求解
% a+ Q+ j" k7 a1 d4 E0 d注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:* ?- ^4 c$ P2 ]( n8 _: X d
E(i) = f(xi) - yi! c r/ z% Z9 h
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。: {- x, H- M0 t$ u8 l1 C$ A
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:4 r3 F' w/ I" d0 G6 o
1 D ^1 U7 J2 Y8 @- T( w/ @也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:8 h; ?4 [6 p9 i. |% l8 y6 V+ A
E(j) = g(xj) + b - yj0 Z/ o: f: z, z6 @0 s
所以最好的办法是对 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 的偏导数就行了,具体代码类似:& p8 E* Z }5 Y! C; J
double Kik = kernel(i, k);
9 c0 T; p+ I {% j( v+ n' N( R6 Q( U/ Tdouble Kjk = kernel(j, k);4 t4 h, q5 T3 z. p+ h* I( s
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];+ `0 t7 v6 i, a' V7 S
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。2 ~. k' ~$ t2 Z7 _, g8 b# A. F
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。; Q D/ H1 w9 [# h/ ]! n# g; A
第四步:实现冷热数据分离1 }3 v9 D1 s1 A7 {5 u/ f# u3 x* m
Platt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。0 P( G( \$ s; y$ j0 h9 [/ @$ ]
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。
# k) D" R P1 c随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。" D/ ?- l, F' H! {/ Y; G' ^
第五步:支持 Ensemble
9 x/ b+ t, F6 l' E- D: J大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。, M8 p6 T! v& J& ]1 }6 ^# o/ N3 V* F
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
( f/ G* j. e* D9 t这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。
" W1 m' N. U% l5 O实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
" S. i+ m! n+ {3 W3 w( g6 ]第六步:继续优化核函数计算
4 k+ i0 o! T' _' f, Y% F核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。
+ k. p$ C$ h( ?" F4 U由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
* m4 P- K* T( L针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。4 Z- b% \4 \8 }6 [* N" N+ R2 {# P
第七步:支持稀疏向量和非稀疏向量+ P+ t) [! m# Z; h
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。
0 P3 A5 h* a% G" I1 R但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。5 H( V: s; {- Q- H
非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
9 q/ i: E2 y7 O$ a! l* O0 R所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。$ k! w1 g+ Z0 a# U
第八步:针对线性核进行优化8 Q2 y- c+ X/ ?! T; f) \; R$ p9 E
传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:1 y' l" A# ]! ]+ C0 ~5 Y7 ?/ b; |
K(xi, xj) = xi . xj r% K. O! w; y9 p
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。
3 X. u2 q% r0 [5 Z2 G3 _. K同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。
/ X) a# g4 p, `; P, D+ q但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。6 E4 X3 @5 D9 B( n! e
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。( J( }8 K' l! G c6 _- L* c
后话
; i& P7 Q; R6 W3 Z, _4 f! `) m上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。3 q6 ^# j5 Z* P. _2 `
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。+ z1 f( {/ z2 E1 D! a
, T9 j' J$ L* C7 ~1 L% }; A
来源:http://www.yidianzixun.com/article/0Lv0UIiC$ }) _1 y+ a( o2 \2 _* i
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|