|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:" z* B& u& ^, I2 r- n
学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。
% m; N+ j8 q; \1 [9 e1 o假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
% d9 v& O4 M$ W" k: {3 A
; T& ^9 Z3 h! @+ USVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
& N% _6 v b9 w4 b , u9 h+ ]& \9 h# e" R" K" [ o$ [
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。
, V. G( J, O+ y- M9 a那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。
# ~. I' G& F; H第一步:实现传统的 SMO 算法, U8 b c2 X6 J$ t
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:6 T% }7 I& j o+ b: G
procedure takeStep(i1,i2)) ~0 `! `. y q
if (i1 == i2) return 0
8 A4 Q5 y8 B% D4 T- u alph1 = Lagrange multiplier for i19 w4 y+ d5 z% |7 p
A- K9 K$ ]9 Sy1 = target[i1]
' ]4 I O6 Q$ C* G8 ^) q' h2 }0 q( D3 I$ k! l
E1 = SVM output on point[i1] – y1 (check in error cache)/ _7 B; N: i# `7 N. y
' e9 Y r3 Y$ r& o. m' W- A
s = y1*y2/ L; w# P! o Q) A# ~6 x3 h: B
# ~( e; a0 l, S# KCompute L, H via equations (13) and (14)
8 o. b5 `8 H& l- W; D, h- R& Y
, {9 q5 m- n7 |' k+ H* ^if (L == H)
2 B1 c) K R- x& \2 Y
. X5 p, z% G( V" {return 0
) |# o4 ` |8 U& d: O
" ]; Y( V, o2 V# F( x( f" n3 y2 }k11 = kernel(point[i1],point[i1])
% n- ]/ L/ e$ n! ^4 [% _
+ ^1 o- Y% T) C6 I4 D( Hk12 = kernel(point[i1],point[i2])9 J# d1 p; [7 Q) [+ t/ k% s
' z5 ?1 |' J: }0 f$ Z, Y, Ck22 = kernel(point[i2],point[i2]). P+ N( J3 D: o% A/ o
5 U; L. d( Y' s
eta = k11+k22-2*k12
6 Q3 r7 F! ?' p- b
4 \( j* p5 n& `$ F( t4 eif (eta > 0) \1 @) `' h* R9 @
" \' X% k$ U; ? h& X{2 ^2 E3 z5 W( R
9 ^5 S* k( {& U# I6 r% w8 w& H1 Wa2 = alph2 + y2*(E1-E2)/eta
; B& T: j% R: U9 A7 B& ?- y5 t/ t7 ]
if (a2 < L) a2 = L+ C! |. Z% D1 n5 Q2 v! d- `
2 K$ e. c& ^8 a* u) N) G
else if (a2 > H) a2 = H
! Y/ B' s- X2 _, S/ v
w1 U+ W, W# a}
2 t* W6 E' @- U! {1 w6 B4 x5 ], @, U2 d" g1 ~! q' t: O+ x' B
else, B" q" f, Y4 X' G% z0 I2 S0 Q, a
, C: u& [ e& i F
{3 ~ K/ d( I" W1 L
W( z3 ^4 @3 v% N/ o
Lobj = objective function at a2=L- u, ^% N+ D2 r3 w% U
8 }& `0 r3 k) c5 i: l, G N
Hobj = objective function at a2=H
5 r. @/ m( |4 d m1 F
* R1 F9 ~$ @/ [( O% D3 U5 Qif (Lobj < Hobj-eps)
8 x$ J9 F/ M+ Y3 o2 I* K' ~- [. R7 U0 J7 A" z$ ]
a2 = L6 t1 _! [1 `9 ~6 }- F
. Y% |$ c1 ^5 s" a- Q% l6 P7 z1 Kelse if (Lobj > Hobj+eps)
2 r/ L# _& l1 c1 h0 d9 L" |) l$ P+ Z+ p& N+ b, N" `% w
a2 = H; F( n) K6 {& k- b O/ r
* m n8 g7 g* p% B6 relse
' }1 A$ Z# ?. C, c4 S( o- ^% Y" @; r" I- r/ F0 H
a2 = alph2* @$ B6 J8 S3 K2 a' r7 G
3 e; o8 A ]4 D}; d5 s) \* g' ~* L# G$ u
2 f# A! S6 ?8 p0 @" s; @5 Z2 `if (|a2-alph2| < eps*(a2+alph2+eps)): q: r. d1 D1 j2 A. e' y( F
6 c6 [$ ^8 L+ f
return 0
" @& U6 y. Y+ o+ u9 U9 J3 P( r/ I9 c' V9 V4 m3 }: q9 j! L9 s
a1 = alph1+s*(alph2-a2)6 ^+ d( D2 k& C/ @& T, y% {
4 H1 @7 g& o2 h: J {: w1 QUpdate threshold to reflect change in Lagrange multipliers
$ n, f J9 g, c5 `3 q) s
- m3 ^0 n7 I- d0 o% O0 }Update weight vector to reflect change in a1 & a2, if SVM is linear1 W9 x* N, f' A2 y% L# L
8 j! p: s6 v1 R! c' C
Update error cache using new Lagrange multipliers. E: t" S# ^( R1 V1 i1 ]4 V' |/ q
: s+ D- p) d8 e, x8 C+ Z
Store a1 in the alpha array! L* e( X2 Y1 j# h' L2 _
' N( n; H( w9 J) ~0 H: N1 wStore a2 in the alpha array
' _6 _4 Y" h- B0 V) e5 i- a# ?% r3 l2 A; C; \
return 1
# ~( J) [/ }, i" D" ~endprocedure4 y9 O3 }2 [. J, G
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
5 w1 ]# U1 }! |& G& {第二步:实现核函数缓存 F7 @. W6 y7 D5 u% X# _( B
观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。, I; S. e a. h3 S0 u$ h8 E
样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。
x2 a5 w J/ D0 R+ ]8 D% u有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
; d3 Y1 Z s7 U( H第三步:优化误差值求解. K" \* c) J5 U. }2 h
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:4 Q" {& e% `; W5 z" R
E(i) = f(xi) - yi
. `- `1 l( I0 P1 _这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。) Q- V; Y6 Y" A3 t
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:
6 p+ U( _4 _& L5 z* l" ]0 B# `- a; Q
也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:
x+ ^: t/ i; m9 s5 [, kE(j) = g(xj) + b - yj3 R% Y3 `. | p, C2 U
所以最好的办法是对 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 的偏导数就行了,具体代码类似: b7 }: I* c8 j' m3 s
double Kik = kernel(i, k);, R7 U& ^! ^5 ~ |, L
double Kjk = kernel(j, k);( z" l" i5 F0 J9 f: @
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];/ O3 l, w% ]8 V9 Y t
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。
0 w) O6 M. [# ^2 I这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。
5 q% p2 `9 X b' e第四步:实现冷热数据分离
, o, v# K) D: V" rPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。5 Z& y6 k( f6 `& m0 t
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。
* T$ X k+ F a! V9 G" j随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。/ [- {/ b, w; B, c5 l
第五步:支持 Ensemble
/ L! P9 j# e' E& f7 J D8 \6 {0 l大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。. K5 Y3 z2 Q9 N/ `
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
5 E4 y* i' s3 \7 j. p这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。8 v3 k. ], T# m" [0 o
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。# y+ E- ]/ N6 {0 o; s9 S% ?
第六步:继续优化核函数计算! R' v8 ?5 r5 A0 j( Y3 Q5 x, _
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。6 E: d; C2 l" t* k U1 z
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
. y B( z, o v+ B% m% Y针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。; @2 _5 j% c: ?' G( Q( L4 _: D
第七步:支持稀疏向量和非稀疏向量
2 G! ~% S! F8 I3 e Y- S' K" V对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。) M; ^# f4 s6 _& D2 h& U; Q
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
6 {& H7 O6 ], Q+ |# m: G非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
: J! u5 P( }. e" v' M所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。" \! {0 x- `( U: b' p
第八步:针对线性核进行优化$ _0 K; d# |' h9 |. q9 [
传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
! R2 Q: E8 a/ `K(xi, xj) = xi . xj
' U- \0 i5 H# H7 U3 {; a; b还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。1 m& J& ~# |$ N- `7 @
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。$ u2 x- S2 B- k/ m, u- H
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。
/ f; O% E/ d' j% p或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。& K4 z v# i# ]) e# I
后话) X& W" B) e- |" b Y0 E
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。
% ^- [0 V: u! j( s1 t上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。/ Z2 q) M. Y2 q- ^% y
0 z8 h2 w' |3 g6 R- v0 Y来源:http://www.yidianzixun.com/article/0Lv0UIiC+ ~3 ^( B) F! C, ~2 y" z9 l# N
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|