|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
7 B' O( x" d- P, q. _学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。4 r+ `/ _: Q4 D0 ~ [; Q# H7 t
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:; u( j& x+ R5 ]1 A7 ]* h. O& t
/ m6 s7 k$ ?- ^" _5 F
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:; Q% M. j* D# A( ?6 K- e5 d

. W1 Y. Z; M/ ` ^7 c上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。
% p2 |2 e1 H5 P! w% H* B2 [) e那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。8 r0 l& V9 z7 z, ?6 v; [* n
第一步:实现传统的 SMO 算法
' R* T, V/ f" Q; _$ t- C0 ~1 x现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
: e8 f/ s+ E1 X; zprocedure takeStep(i1,i2)
4 m' Y1 L$ F% B! _( `/ ?7 B if (i1 == i2) return 0
% e9 i4 A8 Z6 [4 f6 ~- i alph1 = Lagrange multiplier for i1' |# U4 s+ f8 q' G( j& |
! u- E1 f( A5 ]/ y0 |
y1 = target[i1]# S/ Y0 | m# ]8 P
: ~# r7 T8 h3 mE1 = SVM output on point[i1] – y1 (check in error cache)
$ I% {$ Z! y. t* }1 k
# ]1 e, t3 U2 c5 d& ds = y1*y2 E$ Y6 j# y5 d% Z! M3 s2 k$ V
. Z- J) L1 d' A( h1 k, A6 B
Compute L, H via equations (13) and (14)
1 y$ }! R/ {. B9 B/ `0 K. J4 P0 ?0 P& m
if (L == H): n! i7 h% B8 A& Y* O
+ y. Y# q1 u) F4 ~4 V" P8 Q9 Q3 Zreturn 01 @/ C8 p% |9 P: F: W3 y
1 d7 F. Q! B! \- T- W7 F5 A
k11 = kernel(point[i1],point[i1])
% I O2 h* R5 X( x V
- [0 X1 a" p( w3 c* ok12 = kernel(point[i1],point[i2]); p- ^7 ?2 s1 O. \$ g6 P( C/ e# b: @
( W# r5 J% Y3 s0 I% L4 R9 r: ^
k22 = kernel(point[i2],point[i2])1 g2 p3 c9 L6 e" \1 y; o# Z1 w
% j8 T* Q# O5 T2 ]9 H! {! \/ w! veta = k11+k22-2*k12- J% o7 I: \/ F8 F+ x
: f# d5 i2 a I7 O! f6 E h, m0 }if (eta > 0)- m: E+ q6 ^4 y3 B0 F+ k, v2 X2 W
) k) f1 y$ ?9 X B/ B1 x
{
- Y; C8 {* B y X1 E
( X0 a& y* {2 h! i( ~5 xa2 = alph2 + y2*(E1-E2)/eta6 x Z" U8 j3 s9 I
9 ^$ ^% @$ F5 l/ i/ c
if (a2 < L) a2 = L
' @8 ^( D9 A( v' ]) V( W Q4 R K% q/ ]2 z7 N0 Q
else if (a2 > H) a2 = H; S E, k( y3 K% X% w- M" {
1 i2 E" }3 C$ `: _$ j2 h% j
}7 m2 d `9 g% j
2 J& y V; L3 E3 r
else0 ~! e ^3 D, I" c/ ^0 l& j
k( u% ?* }, E$ P, e
{
6 m9 U4 e2 x7 e( Y; q# E3 p" ^" X, y, ]6 J/ r2 s+ K
Lobj = objective function at a2=L8 Q8 |( m. l- m8 P G
2 G# `' P6 @% ?- P( h+ @, c8 w" N. u1 EHobj = objective function at a2=H
, X: T: n9 g4 K$ Q
. e6 N7 c1 {- k% a5 E! fif (Lobj < Hobj-eps)
* V) ^! g, d" p8 ]4 q- ]! U. Y
9 |$ n' B: G" ^( {* H a6 ?a2 = L" _6 R. Q4 K7 f) `1 f
+ @0 w4 |9 k; w0 I: H4 ]3 n% P
else if (Lobj > Hobj+eps)) A$ s. k! z% g& a& G
; L" f. E" P6 n- Ca2 = H
5 x' O2 X3 n" V( H2 W0 w# K( X
% x: f7 T7 G zelse+ o; d5 K4 u }# j) r; I, v! m
$ V! Z2 N' e: ] L
a2 = alph2
2 e0 g* Z$ G" C- ^/ |- d
, [* O. s; l! B3 n& e, O6 Z2 j9 |0 D}; s" ^) Y4 q# {
! G+ a& r$ {* Z' I3 k" D7 mif (|a2-alph2| < eps*(a2+alph2+eps))" _2 q: p( l7 o2 H+ k4 V
" ^! q. m( Q, z$ d3 w6 Z
return 0: d5 A& U/ J/ l8 T! N! D
5 c1 q+ v: b8 ]a1 = alph1+s*(alph2-a2)" X* A( T8 }8 r0 w9 C
9 X% H% m9 m) o8 y7 X( D+ `3 E2 J
Update threshold to reflect change in Lagrange multipliers1 t0 v( _9 `) @9 ~8 K) A" N
! E, X3 v4 w- m1 J5 a
Update weight vector to reflect change in a1 & a2, if SVM is linear) D7 Y" n' \& Z; H* H' Q
( G! i) X! D3 k
Update error cache using new Lagrange multipliers1 e7 @# c+ W+ X& E7 D: W% s
7 g J7 f0 V: Y) u& P
Store a1 in the alpha array
: A6 `5 K/ g* \2 d
9 k! |, d5 V( o! [2 SStore a2 in the alpha array
! X* N3 \# l5 O( e( s1 Y/ W' h. E$ I+ I |9 J2 Q8 P5 H3 N) Y8 ~6 l. Q
return 1
! r6 C& E# U" w$ Gendprocedure
8 X! Y+ @7 f/ \8 ^4 z/ F) u核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
2 V; V' {1 I5 z; h( \! }. [第二步:实现核函数缓存
9 I# W6 N5 }0 ~& ~观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
$ B( ], \ U0 q样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。8 w d& e3 y& f2 u- d' y
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
6 K b) t z/ j( f第三步:优化误差值求解
2 L9 H$ O% f2 A8 a: o$ D$ }注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
, L z- r* c" Y0 L% bE(i) = f(xi) - yi, D: U ~5 h- R# a7 N7 J
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。
& X7 x9 I; N! f0 A# o! oplatt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:
3 U s: j. a: J5 Z1 a
I5 w8 J: [8 i- a/ i也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:( J" j7 \% p; U4 a& o0 c1 l5 }% A
E(j) = g(xj) + b - yj- @: Q' x# o7 N9 r" [
所以最好的办法是对 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 的偏导数就行了,具体代码类似:
* I! j T, y5 {1 C: edouble Kik = kernel(i, k);; X0 {/ m6 \% }# S) e5 x
double Kjk = kernel(j, k);
# F# l* N* T1 I2 nG[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];# d1 K1 ^; m4 x% e5 Q$ Q% j6 a
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。) i9 I5 i1 C; V/ \0 b, ?# F4 v
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。# A5 C( ^4 o) r* u$ ~& `5 v2 F# m
第四步:实现冷热数据分离
+ c+ m3 j I- Z, h& L* A$ U' OPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。: p* o6 Z( ?0 p. c' j! F
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。0 K5 G+ c( r9 D
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。3 e/ [/ N0 p" I
第五步:支持 Ensemble
$ ~5 l3 }* D( z0 }大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。
4 i, U( t5 ]% J: G2 D最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。7 G, G$ b! L, M3 ~3 T
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。 J$ g( w/ F5 Y* r2 o; n% b( \) T1 s; G
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
7 z2 }/ P- I$ A: f第六步:继续优化核函数计算
+ K3 ]* L9 _8 a$ W% R. }% }5 z核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。
3 N" A6 i2 x) {/ O+ Y" U0 t: E) H/ `由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
- V2 b6 j" e2 J) a" C C针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。
2 m& T6 u& m( d+ `- W第七步:支持稀疏向量和非稀疏向量
8 e2 o) k8 k- H对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。
$ c) Z) s( L! c. I9 j7 _7 y但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。4 ?4 n' \8 w+ H; Z
非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
" g) D$ u3 j) W7 H* q所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。* ~4 Z2 a/ H; v: k
第八步:针对线性核进行优化
, j/ o! t- u7 W$ B d$ s传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
, N& O6 j- \1 ~' N, x; Y$ R1 l5 FK(xi, xj) = xi . xj
/ {* y8 Z8 _4 P: Z5 }还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。! Z+ R0 J3 A2 l7 U; A, _
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。2 ]' v* z' {$ C
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。. S* C# z9 Q' E1 q- G
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。2 b' G4 Y6 p3 [# S; U, _
后话
! R3 X- P m. X5 I上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。
( O# v, _" B \7 u$ g上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。
8 j. a$ E: O% b
! L5 y0 v" r7 ~4 u来源:http://www.yidianzixun.com/article/0Lv0UIiC- I4 H4 y3 ^' m$ r: [" m
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|