|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:7 P) x- a) W& h. F9 }
学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。) k T) N! H, u& C! ^7 s
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
" Z! A* r7 y& x9 r2 j" B s; v! G4 M( H x' H4 Z
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:3 K0 J& Q. x$ ]8 ~% t% V! g, S( I
" `; a) N3 q7 P
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。; R' @* q" n0 r' ?2 G9 o
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。
9 b2 w, Q$ j9 g: z4 d第一步:实现传统的 SMO 算法- h7 W/ E6 A( W" I# U4 j
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
) I0 R, [# l2 O% c' x7 Wprocedure takeStep(i1,i2)) t( P( _0 `# F9 `
if (i1 == i2) return 0
9 i5 b, a. k/ J7 `6 W alph1 = Lagrange multiplier for i1: t( j0 ~: v2 N/ M7 m8 @
) F |5 }" k- |; V$ a0 l
y1 = target[i1]
) k8 Q- ^4 f: x( Q
. T% ~! x n" F6 Z b% TE1 = SVM output on point[i1] – y1 (check in error cache)! F- X$ O6 M# h
f# m I+ u6 ?0 V, b# \
s = y1*y2; O# k! J" L! N8 f8 V
: m, e5 M f. j |2 Q) X, UCompute L, H via equations (13) and (14)
: j c1 N5 t- G' @# X2 d$ b0 O4 y/ n O- Y0 }1 H: Y
if (L == H)
& w+ _) w X+ G' e F2 z. `' H h; }0 H: O0 t) r+ z9 f
return 0% e9 Q: p6 g3 ^' p! A1 R
- t0 S! y; O0 m1 M. {; [! yk11 = kernel(point[i1],point[i1])
: X3 r6 Q7 ]1 | O, z
2 q9 j9 `4 } w/ N1 C- gk12 = kernel(point[i1],point[i2])2 T+ X6 x. W7 ^8 H
+ j0 S+ S ?/ v3 B0 d6 S' Ak22 = kernel(point[i2],point[i2])
/ u8 o0 D7 j! V# i! R
' z9 u0 U2 [9 p2 Z& Ueta = k11+k22-2*k12; k* h+ |& g, P) j% ^# D. C) [
' `) \3 J4 d8 R( q+ s/ z3 x
if (eta > 0)! `$ D8 ?0 B( f# ?1 s4 v! C
& x3 `0 I) Y6 e i8 l{: W! g G! l- F! m
: q( H% g( `1 ]
a2 = alph2 + y2*(E1-E2)/eta
4 i9 Z1 i/ u4 @, @1 ^; t* A% _) \# r1 A2 X a# E2 v6 Q
if (a2 < L) a2 = L
8 r0 n$ n! _" Y) X H) H
. ?. Y* A6 n4 S& L) C% Z3 ?. helse if (a2 > H) a2 = H
% \% U: R6 W$ l; Z F e3 V/ i3 j# S* J( r0 Q8 x+ H
}
. t9 g4 I+ @; y+ M& M
' A* ~; V+ v5 t! _: F' `else2 x) r' t5 H, y! e" K% p2 P3 _
. d9 c: Y' f9 c- p* ~! J- {{3 W) g* k# H% Y6 Y- S! U( `
6 w5 x7 |1 F M% H" L6 l7 s% R0 Q
Lobj = objective function at a2=L
- I7 @% i2 u, |+ H$ o7 j& O( k2 S8 I$ D, K ]- Q8 h0 ]
Hobj = objective function at a2=H2 G2 w. U8 ?+ z% I
; S7 u% ?* ?9 u! dif (Lobj < Hobj-eps)0 p4 r+ Y* q- _! T! f% _
# Z1 X1 q: s& ^2 n4 Ma2 = L }6 u5 B9 Q& ?+ H& r/ `
4 R% j& ]2 g8 N( d, V+ w
else if (Lobj > Hobj+eps)" ^) \/ j& S2 ~! l7 M
2 m$ m/ I7 U. g0 g' va2 = H
* V6 `2 a! i" m% c# ?$ l9 ^% _, N! t6 Y1 `9 ]! K
else
# N$ w, A8 c: C3 A3 I) ~+ ]& v& C1 f1 I; Y! t0 p) K9 Q. [
a2 = alph20 E0 `# C* U$ {8 n4 K) z+ y+ j0 Y% j
4 N2 G7 E% k% i; S! ^}; b( w% p* T# Y R4 \
7 c$ Z0 V+ Q. n" F1 ]4 @8 h0 R
if (|a2-alph2| < eps*(a2+alph2+eps))' ]8 l" J, c' N" N( `
; h% ]& d& w& x: `$ [return 07 |/ H' I$ Y/ z9 ^2 T
. h+ w" A7 A, f' U4 n$ F& @$ Va1 = alph1+s*(alph2-a2)
% g) C1 {3 s; Q z% Z9 I& r5 P3 c" O+ P; V
Update threshold to reflect change in Lagrange multipliers2 U% ^9 |( N4 ^2 N8 k9 M$ c7 F, v, h P
' v/ \, o2 }. ~- m' n
Update weight vector to reflect change in a1 & a2, if SVM is linear
0 E2 \5 l( l: V2 V% j. }7 G6 j8 n# g# }3 O; I
Update error cache using new Lagrange multipliers
# |- s/ p f8 g6 U5 Q) h1 B- B" N
1 z: w# d) Y! d: Q" H- q4 z7 LStore a1 in the alpha array
9 A: m; }* u# X& V1 k( K9 P* P9 u, j
Store a2 in the alpha array4 }. z# s2 Y! m& Z
" F; a; d$ A1 p* J/ A- k! O" p, t7 ureturn 14 ~7 ~; ?: I$ o8 P& r
endprocedure2 u; F+ r2 G- n' ?' N% m6 ^8 }
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
7 C9 ^4 f7 I4 W$ Z第二步:实现核函数缓存
) H3 b8 W% M5 c观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。! J8 w g, L% ?0 |) u+ N H
样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。
3 T, U7 A4 m+ Y2 @( d' x有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
0 p& ^; Z! l2 E0 f: w第三步:优化误差值求解* H) V$ j C+ s# c& b0 ~; ]
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
) N" q1 `! s' r: G; t U. T5 S8 lE(i) = f(xi) - yi+ \# p$ {- i: _% d9 R' E; L
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。
0 u; p! j% @0 P# Mplatt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:3 U8 M4 y, F* x) `- ^
8 j" U/ z$ h" Z, b0 U2 [' p, V. e也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:1 a9 U$ w# F, b
E(j) = g(xj) + b - yj* n' Q0 }0 J! E! r2 Q/ 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 的偏导数就行了,具体代码类似:
( j) \3 z3 u Z: bdouble Kik = kernel(i, k);
% U$ `7 P" \5 j" B3 Gdouble Kjk = kernel(j, k);' ]. ~& ^/ a8 g4 b( Z- l1 X" [
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];9 f5 T# w( Z8 u' x* t9 O$ s8 A
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。) M L3 a: H3 f; T, w
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。9 c4 d: a) E# }1 M7 u
第四步:实现冷热数据分离 Z& d, }) k6 e6 l; C
Platt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
1 Q- e% K$ {/ G9 @0 C那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。+ G* {( B- A$ f$ X6 q% L
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。
7 M- |# {, e) M) _1 K9 s第五步:支持 Ensemble, ^5 g$ V9 h* H8 R, z
大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。
6 R |4 i, C2 b+ Q6 ^& w- d4 u最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。% k/ L5 k- U1 z- W9 {% r
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。" Z5 p% D0 C7 F( v' K" p8 k
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。. l! s6 c: A; z" D9 P! v" C* {7 V
第六步:继续优化核函数计算9 n7 v0 D) _" C: L' V
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。6 `1 `% `/ B8 Z& f$ b. }6 z4 i5 A
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
7 Q# m2 `4 t! Z; S4 Z( J+ W h: W针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。/ h- `0 Q" s, n1 s4 t2 a5 I
第七步:支持稀疏向量和非稀疏向量
9 ? m* T- n! \: V$ q1 G对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。
# A4 Z6 m4 l9 L% H @但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。4 F; ?6 W( E4 k Z9 X5 f& p8 I5 W
非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。+ f; Q* H' [3 O6 B
所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
' ?( X5 t1 u. w2 l第八步:针对线性核进行优化
9 W- B) ^- V- g# ?! s; a+ l8 u传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:0 S$ C* m8 k5 p, z
K(xi, xj) = xi . xj
" N1 x, n5 w, s) A还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。; @% {; H" Y2 P; R7 F
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。
: g% Y \( b$ Q9 Z, I, n但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。
/ v- b3 F% y: ]# P# [$ [或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。: x4 F! [) n5 T- z% `
后话
. J9 [8 n- `# r, Y; u) y$ {6 R上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。
( E$ |( L9 T" v- J. A上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。" U; Z6 Z2 |* C1 K0 Q4 w
( v/ f' u! H& T7 V
来源:http://www.yidianzixun.com/article/0Lv0UIiC# _8 u- K1 f- p9 u& W# Z7 M3 R
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|