|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
1 M) j- ~( Y& | j; e: Q学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。
H; r7 M0 B# V: v/ e假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
& i+ h. O* f& z( k2 t/ s: D
0 J. J; N- D2 c/ ~SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
" q$ z+ K2 C( o3 l5 L, e
) Y7 T4 \9 X8 e/ w; q上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。+ _( K. z7 z. e& M" {0 a( S) \% {
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。/ c9 v2 i0 c- l8 s2 {
第一步:实现传统的 SMO 算法
- K' L! E( I ]' r, d现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
* ]1 M9 f# I& K: B6 }procedure takeStep(i1,i2)! A& ~" x7 \ E% T* R+ p9 X6 [3 W
if (i1 == i2) return 0% t& D- x/ n9 {- r8 D( u
alph1 = Lagrange multiplier for i14 v/ u e3 W; B' c4 l
. v' g$ V/ M; z9 |6 Ey1 = target[i1]
7 O3 T: _) j0 l2 v2 N- E1 e$ ?! v' Q4 R. k3 D
E1 = SVM output on point[i1] – y1 (check in error cache)8 w* r# [8 q6 S% E, l
6 l Z8 C9 x+ L
s = y1*y2$ w0 b* ^) s" u% x3 d. r, S, H
4 H7 M5 r1 Y; K& G% T7 B- d7 o
Compute L, H via equations (13) and (14)! z! ^3 Q, F5 a" |" c8 Q
! b& m. k+ w& V6 U# P* W6 x5 cif (L == H), U: e" N! L9 }& {' T$ _# F; j
" t% K; |$ m# \( l* O0 treturn 0( y( W* G3 K! z* U( \$ r! F$ t# n/ y
) H& k0 G* p, |+ z1 z' \
k11 = kernel(point[i1],point[i1])0 R5 C4 I1 s+ n3 r7 v
5 o. b# u. g! H: m7 F- ?
k12 = kernel(point[i1],point[i2])
6 c1 s5 ^- o" y% o( m9 ^
4 J; L' }1 t2 `, [k22 = kernel(point[i2],point[i2])
) j4 j! U5 \' `9 [" A7 t' A2 ?; }1 c% S M
eta = k11+k22-2*k12! p7 i6 F( S4 C" i
" E4 k7 j4 y% ]. s1 K! L0 lif (eta > 0)
& n& j {/ h6 [. P6 X0 I0 O8 h" S- m" H
{# \- K k. ] z3 c
4 E& H0 ?* A( c% Ua2 = alph2 + y2*(E1-E2)/eta$ ~$ k3 A) f' }9 `
# x- v) n A/ T3 t3 ]% G
if (a2 < L) a2 = L
* i# ~- D! I8 {4 v9 D
! d2 R# @! n5 eelse if (a2 > H) a2 = H5 b8 z# N0 ~- ~ S( `% `6 h
; k! R* C# _/ j+ V' K}
9 j+ x" w4 C6 t. [/ c* J) _" G1 t/ m% Q
else; y0 H6 E0 V% S1 u9 Z% `% o! [
& G' x# T, l1 }4 t{
# M# h$ x; }% _+ X/ d+ t: l
$ X! E# y9 |- D+ T x# uLobj = objective function at a2=L
; y, V5 U$ A" P0 t/ Y& ~5 h
' J4 e' C' }. b2 C; rHobj = objective function at a2=H; }2 `( C, }0 F' r9 {# ]
8 d* c# R3 r: L- E) K
if (Lobj < Hobj-eps)
( d2 U4 d+ O: ^9 ]7 ~- R2 `& m* J: {/ G, O- S% b
a2 = L
$ e. i Y; @0 }. ^- V( B% ?5 x; F! y# d7 d3 J$ T
else if (Lobj > Hobj+eps)* @9 W. O& [0 { O1 m; |
" f! U* K3 T# t4 Z2 O5 i5 k' ba2 = H
; t( c5 C7 y' I O% A+ @' K7 G+ i, @; o
else Y+ @# M$ r: f% W
. D) d$ ~# Q$ J8 x l% l
a2 = alph2
) m* @ ^/ U9 h1 ] J* C T3 G# x- U+ I3 {, z8 S0 S
}* J6 a+ d4 S( @. x. a
4 H9 ^! @+ D# r2 T' H
if (|a2-alph2| < eps*(a2+alph2+eps))/ _% ]: j, }7 O" B8 T) X9 ]: C
7 W( p1 K1 \9 L% [
return 09 S, g$ [/ [# u$ j& J2 s. p4 l. l
5 S0 [' p. ^* i( S. ha1 = alph1+s*(alph2-a2) H" Y- m( f c' k5 A( w2 S
6 q( e5 Z8 k5 rUpdate threshold to reflect change in Lagrange multipliers
3 y' N1 o% D6 ~9 L
/ \8 M2 g3 h! k0 u% w0 e7 e! zUpdate weight vector to reflect change in a1 & a2, if SVM is linear: x# P2 K( g3 f4 n2 s' A: p
$ v! \7 l, w! iUpdate error cache using new Lagrange multipliers3 Y4 f# w% c* }
. L: n) `2 J: P7 [3 |( L4 h* P
Store a1 in the alpha array4 c5 M+ v4 o; H. u- U: f4 v/ p+ R
0 M; D+ ?6 @1 w- ]5 j, n0 nStore a2 in the alpha array
4 E3 f6 Z1 J3 e% B
7 s1 \* A' ?0 C$ r- Jreturn 1
9 Q+ c0 A P* z7 Aendprocedure! W; J6 D% m" `6 e8 p
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。" H, v9 Y- d8 a
第二步:实现核函数缓存
$ C3 a, P# [: `1 S观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
9 w# n! w; u7 `样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。7 Y% L8 g8 i, _9 E/ p
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。: o+ n7 _2 I3 [* z% A0 b) o
第三步:优化误差值求解5 s6 E4 C. \: X
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:( m2 x9 K6 ` o8 X/ e$ y
E(i) = f(xi) - yi: j& g ^+ K) B, m) t
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。: P( I) n. Q; A! \" [ @: ~2 b
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:0 ` g2 K0 W: l9 Y$ M+ h
7 m2 C, Z4 x8 ?$ u, Z1 |& X也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:
4 x7 s# ^0 ^3 F4 [3 _! YE(j) = g(xj) + b - yj
# j, A& S2 Z! f所以最好的办法是对 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 的偏导数就行了,具体代码类似:! r) U( T5 U. h
double Kik = kernel(i, k);
- f9 N6 M7 [7 O s- C' Cdouble Kjk = kernel(j, k);- [ V: F& R2 f1 k
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];/ ~6 o" v/ a) Y0 A; }
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。* i5 m: ^0 n A( z; D
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。* t \' e( c L! r
第四步:实现冷热数据分离
0 T* C6 U, ^: H7 w2 O- a! lPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
: q" D5 X+ Y. y$ `# @ O* h! Y那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。" Y1 H5 s! \ s" ~3 ]
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。) I5 [; e* j: r9 W+ p7 \* J
第五步:支持 Ensemble
+ u5 b4 Q0 R2 }" j6 k: H" y; o大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。 |' e( ~% M# Q: ^; B
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。5 _" Z3 L8 z4 n: ]0 U1 a
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。' S7 c$ G* t2 X) n: b
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。0 L- m; {! }5 _& b+ ^4 l" d: z
第六步:继续优化核函数计算1 m3 ~7 L2 F. ^) c+ w& } _
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。
4 }/ [2 Q3 F. }5 a由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。# c4 D' X8 C1 ~. ]. G
针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。# K8 r$ O2 W' b& ^" j& C
第七步:支持稀疏向量和非稀疏向量
( D! m) M: d; i& C6 I. [4 [' w m对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。
5 H: d) l* v3 K, c, B( t2 v但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
# {; {7 y3 ~+ a f) [非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。& X% H% Q+ T4 E8 E
所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。7 V) p D$ _4 `
第八步:针对线性核进行优化 S+ y" O1 u* L. x
传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
$ m4 B S; f& T7 L; l9 G }K(xi, xj) = xi . xj, t# a" o7 @$ @, A5 s
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。0 r: n [% Q, ^0 p( Q5 I
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。
8 j {! z1 f M) i' A. f+ J( U但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。
& U" I ~0 r6 B3 o6 E3 l或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。- h, M8 X d. ?( M, q
后话. ^( o; z( M {1 N' g% G/ h
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。9 B& d2 E8 j: a
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。
" E8 H8 U3 P+ N$ C: J' g( u1 C& U1 I. p% w+ z1 d
来源:http://www.yidianzixun.com/article/0Lv0UIiC% ~5 k" r- T4 i; q% F
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|