|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
- I+ R$ w# L% a+ V1 Y( ]8 _1 t" B学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。3 V! g$ `2 a( w/ `, i
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
, F7 E$ l' h8 ?1 @" q3 F1 K " s" |4 @" G5 D5 x! l& _
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
2 f3 z+ S7 f" H* h! M
( p Y1 j, x; Y5 Y# v% D1 O5 S) n! v! t上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。# L2 s! J _6 Z! ?
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。$ a* u/ |' x8 j4 p. p8 s
第一步:实现传统的 SMO 算法$ P% W2 a) h9 J. ]0 k8 ~
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
4 R$ z7 ~6 I, _) Kprocedure takeStep(i1,i2)" q* m& Y# ~# z( f! M
if (i1 == i2) return 0: |) C& p% s9 l& ?6 j' y/ k9 y
alph1 = Lagrange multiplier for i17 p* C# D7 B( J1 B& |# _/ E
2 a- O9 e$ ^; S3 r5 Q2 Ry1 = target[i1]. r) ]9 ]3 _+ \5 N% i' y0 A1 A
4 |9 U. h( m1 ^7 [* p3 W7 pE1 = SVM output on point[i1] – y1 (check in error cache)$ h; i# M7 c; }5 J4 {0 _: D
3 q) ]2 Y- E! n' U8 i* }7 @
s = y1*y2
3 a, I# \1 k, u& v+ {: S" s4 ~% A+ E+ K5 Y# M. C9 N# G, B
Compute L, H via equations (13) and (14)# [/ t2 C: |, b; ?- B( b! a
3 O9 [& i+ z+ t" K/ Y t- N9 ]if (L == H)' i: u$ Y5 @$ I) g; v
+ E" m0 m8 z4 h) D, w
return 0
- U; ]/ l+ o9 U6 x# r" j8 }4 i- W$ Q0 B) h& I( Z
k11 = kernel(point[i1],point[i1])2 i+ t9 J3 R4 b4 Y4 J
' I$ J* H/ Q3 S: f; [% a
k12 = kernel(point[i1],point[i2])
/ h5 `" R, h3 a
: {; Y+ s2 ^3 Zk22 = kernel(point[i2],point[i2])! w# Y0 X6 b( q
! C( M) J/ J3 @
eta = k11+k22-2*k12
' t2 k2 Y: ?# X# [7 z/ o, w' O! ~/ I5 [( B1 A; A O& G
if (eta > 0)
: \5 {6 ~9 b9 U1 C% u5 \+ I8 v# G/ |; J. J2 d$ A7 X
{% ]+ R- N2 S/ j( R4 T; X( C& a N8 x
' r" m! A% V/ [# J! S5 W- J
a2 = alph2 + y2*(E1-E2)/eta$ D, f9 V0 I6 m+ T
. J T: ]& e& L; o# Z3 \$ t' }
if (a2 < L) a2 = L
8 F5 j! Z9 h+ f# x! D6 Y- e) z& r( Z
else if (a2 > H) a2 = H
+ {+ b4 B H5 t+ Y
, v& ?: z1 m% j}1 d+ o! [; S( u9 T
- i8 `1 e4 o% I( ~
else5 | g* t) z% y! i! S* t, P% [
% s: {3 U+ P& N: F4 [
{
2 l6 i$ R/ I9 K3 L {) y8 Y5 z; I& ?1 C/ y
Lobj = objective function at a2=L' |9 o9 n1 z! D' O; A# Y
4 y1 ]7 L2 e' r" W+ c9 [& DHobj = objective function at a2=H$ O9 O; h; ^( g9 u9 K2 R
6 ?* F7 Q+ h+ d. j
if (Lobj < Hobj-eps)
% I& f# T5 S7 W: r* |- x
" g% L3 q8 L6 ?6 {3 ]. Q' ra2 = L* W( u! u3 q- A9 C; u8 E4 g/ O2 o( n
% S4 x, y* @0 \, W$ s4 G, jelse if (Lobj > Hobj+eps)
+ [7 s5 k8 Y! w: w. f
# @( \/ R3 u8 G* F& m% ma2 = H
3 |9 }1 D% K4 q! _3 A+ }2 d1 E* q( ` m0 \: P7 Z' m4 R
else
' j* V' U) ]2 ~0 m3 H _1 i% t9 u! C! k
a2 = alph2
0 O1 w; q( o# ^) \ z3 p# ^9 @8 Y3 O7 ]' M. ?' Z4 ], W
}
. o& \- e; f+ U# ^2 F9 g) K) _' I% a2 I' b1 r2 m. J' N4 {4 B. V4 [& s
if (|a2-alph2| < eps*(a2+alph2+eps))$ X( p V6 O/ k/ D
( B! H- [( r: \: [ _9 ?% x
return 07 t1 o2 l+ J2 \8 F( U( E# N' l
1 P! V0 S2 h0 g2 J! v+ q# x
a1 = alph1+s*(alph2-a2)
' X0 g& v3 [& i) q6 T8 A& e$ m3 x$ R. L; Z. r
Update threshold to reflect change in Lagrange multipliers
. W6 A9 W& j) D: o5 ?8 S* [
8 G- O$ i/ h4 |; n1 nUpdate weight vector to reflect change in a1 & a2, if SVM is linear
1 l9 M' t$ B# I( ~9 X# Z% s g( S4 @2 x
Update error cache using new Lagrange multipliers9 ?6 D( z; Z4 l0 ~
0 N& G$ ~. C; @$ R7 l+ ~9 s& G% b3 i: _Store a1 in the alpha array
% D$ E* g+ w' c8 m! C" n
. u1 }0 S `, T! K0 f- sStore a2 in the alpha array4 r: v4 t% L1 p% a
8 i" {9 T. T$ ~4 Qreturn 1
" ~7 m9 }1 }- a" v, Z* Rendprocedure
- ?; M1 k$ t0 q& t5 ]6 a; W5 r核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
7 q! O- }* E) R第二步:实现核函数缓存, _. m6 d4 U+ k+ @$ y
观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
+ ~( Y& l7 T2 Q- ~% E样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。6 o* h$ R6 T9 l6 \. Z" \
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
& a$ Q% b/ Y9 v* I- Y" j+ `! G& r第三步:优化误差值求解' T- Y7 u! b: L; j. {
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
+ y: u& P# \/ w9 D$ \3 jE(i) = f(xi) - yi4 T$ n: @0 D' W3 w: Z
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。; R+ \( z9 ]! S+ E; d6 b
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:
1 V; f5 b/ {8 v# u4 z' m' D4 Y2 g" G$ `5 @, D0 b' B9 s: P
也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:8 g3 @4 ]: r$ c; V6 d: l
E(j) = g(xj) + b - yj9 X: O0 z/ p8 j* |3 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 的偏导数就行了,具体代码类似:
6 ^+ _, ?; b4 y+ Z% A! H5 Gdouble Kik = kernel(i, k);0 A `( ]4 l+ m& f, t- G9 i
double Kjk = kernel(j, k);) U+ C3 `# @) D! s2 J' t
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];
6 e) B3 g* E8 b9 r; m4 B把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。
# g5 W* r& [1 a3 ^1 T% ]这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。
& M3 v* z; s% j) C第四步:实现冷热数据分离* \) k8 h" l' Y4 ~4 A
Platt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
( H$ H3 Q2 M, e那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。; \% g) H' K, M; I' A* S! b, Y
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。5 m: _% k+ I- L* [( J4 P8 Z
第五步:支持 Ensemble4 G( @4 G$ V8 g1 }0 T$ P
大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。
2 u7 h6 F# u3 Q- I最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。! @ o. C4 T8 S* \" k# H. F" Q
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。( _, ?$ u* Y/ u( l
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
& m: t2 W( S7 _. N第六步:继续优化核函数计算
; P# C, A' ^% O" o2 U5 j+ T核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。
: [2 R0 U5 c d% k. F由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
* g2 q1 J* A& t* [6 I" {6 w针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。
; m ~, O5 N' y8 i6 l第七步:支持稀疏向量和非稀疏向量/ A) e( _( p* f6 u$ _
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。# A' W3 q; A" j# e
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。. W% z4 r% c" M
非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。5 _9 z1 p1 k7 I3 R$ w/ P3 z; M9 m
所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
# [* ]3 L* t* X/ |- A# R第八步:针对线性核进行优化
) `* J5 ~; F7 c+ C5 ]传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
) R6 B; r$ l2 X. ^5 e' _K(xi, xj) = xi . xj
7 w3 T/ q. ]1 n4 G w0 R1 l. w s还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。
* H |! {2 I2 S& Z6 d同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。
. w ?1 f5 g, U但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。9 \2 y# }% ~, s+ K* ] V
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。, G; p; p5 q4 ?7 r3 P/ p W& e
后话# W1 b& W- |! n' ?; Y
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。
8 Q A ?# {2 E2 n# Z+ \上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。
8 c I6 I e: C: M5 ^& ?6 p) G1 ?( Z1 ~* Q
来源:http://www.yidianzixun.com/article/0Lv0UIiC
- l% I# p1 q4 @8 v1 j" ]免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|