|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:3 v0 o; y2 z* S
学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。0 j7 p+ ?( ]3 `5 }2 G9 K: X
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
9 p( j1 \9 R2 R4 q
" X8 o3 j( v" i, Q, H' xSVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
, W. z* t$ E, J2 L$ x8 ?
% |% |; F* Z F: O8 w( w5 ~上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。/ y1 A4 |3 q0 X C. I
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。
. i; M5 c2 X5 ^第一步:实现传统的 SMO 算法9 W- }; V6 i, ?
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
# X+ G- `# ?$ i8 Y1 ^; k6 y8 f" ]3 kprocedure takeStep(i1,i2)4 D$ P: S6 \" N7 F4 Q( Y; E
if (i1 == i2) return 0
6 t! @ e# g3 y; [3 B# D( R5 R alph1 = Lagrange multiplier for i1
- w+ h- Y+ j' f3 n) w7 L7 T' l! N/ u
y1 = target[i1]) H r; g) i3 g1 b! N3 I7 E
, K/ y) x4 |% b
E1 = SVM output on point[i1] – y1 (check in error cache)
- e0 [2 J6 X$ \
7 ^' Y2 Z2 ? As = y1*y2
8 S6 Y3 z8 G0 ]6 D$ ?& i( D; a; k3 |9 A Z0 p6 v& D$ ~6 |! _- z' }
Compute L, H via equations (13) and (14)) Q1 c" i; g. v) ~% ~
m6 x" S1 l7 v U+ q+ o. Y
if (L == H)
. @. F- o6 C1 e! E2 D
# y3 S* L1 }0 a' u& s2 W9 yreturn 00 B; D5 A4 S7 D2 W1 I. K
! X6 A+ P9 t. W& j, v
k11 = kernel(point[i1],point[i1])4 k7 a) `4 b/ M- b- N- R: `' e1 K
6 D/ \' }! P) ?- Y% s' i& [k12 = kernel(point[i1],point[i2])( Z% k$ a! J0 ^+ p5 y- q
9 H" M- Z& @. N) o. G9 c% yk22 = kernel(point[i2],point[i2])
2 U- e# G% L/ @- W# d: {& _7 j: j+ l ~5 f
eta = k11+k22-2*k12
) R" D6 o0 B4 {3 i* d: s: w* V0 r* D
if (eta > 0)+ G" @( N* i% N9 z% P, K; r
+ X" `* N- X1 O( z
{: [* W# X/ ~. k. i1 Z! p" {* l7 t
; Q# F* j4 |; a7 W9 Z8 j7 ^0 k4 {8 \
a2 = alph2 + y2*(E1-E2)/eta Z7 N$ @' w1 \1 l; N" V- M% R
( _- I) e% K: x0 h" N' f
if (a2 < L) a2 = L
: T& o1 C5 F# Y. A" H6 c2 h4 U4 ^0 ^/ X
else if (a2 > H) a2 = H
" a% Y5 i: ? C
7 {& L" o8 l( G% S, A8 M K}9 c6 |4 a* {5 g* E# R" t
: Z+ t( b# i* E: w+ U5 {else, Y+ S) c5 P, Y# F5 R9 J
) O# M, E& y& f* _1 C{' v: y& b( T9 v1 O! r8 ^3 ?: A. o
1 M- N9 K6 I( I* Q5 S% d* \) @% TLobj = objective function at a2=L
) M1 T+ C/ ~& Y% t4 @% n* ^' X; e: B6 ]& a: q+ \# e
Hobj = objective function at a2=H! u1 e9 F1 V+ @8 t: X
* h) r' |% m6 f- h. e3 Q+ e& Z' @
if (Lobj < Hobj-eps)& b$ y! B$ s( b8 Z
8 \$ Z5 {& n h; x9 p4 N+ b7 Oa2 = L7 @* S, Z% Z* u k2 Z& E
3 f0 O# ]# `5 [8 s
else if (Lobj > Hobj+eps)
$ [. t+ \0 o5 y6 w# L/ u+ P# a; h/ d2 T9 W+ \
a2 = H. {) j( a8 I4 l! g7 P9 n! h! l
5 n j8 G! w3 Y2 w, b0 relse
: {5 c2 _' ~- o7 Q2 \) m
& I+ N" o9 o! [1 u0 pa2 = alph2
, y/ [. B6 {- l
- t; f; c, O' i- Q( B+ V/ }}
* d! w: E# P& x: L3 r. B
9 g3 @% z; S; J. U6 H; B5 l2 M- uif (|a2-alph2| < eps*(a2+alph2+eps)); Q4 ~& `" E( k: g8 x; c( X& _
' v, j. L& S7 @return 0% @6 _. R9 v' r. A/ q- I3 M. N
! t) V8 s0 h) T {" I7 R$ Z; s
a1 = alph1+s*(alph2-a2)+ N! s7 V' U% X6 s; z
! |& h) `; d4 Z/ g! j7 I aUpdate threshold to reflect change in Lagrange multipliers
0 n# D5 P' |3 y& ?# K7 Q* p2 u
2 t2 T7 t `+ xUpdate weight vector to reflect change in a1 & a2, if SVM is linear
+ P: ]/ Y7 Y R- @# j8 L: N1 R$ Y
7 l2 ^& W% E3 X& C7 K7 R- f* A uUpdate error cache using new Lagrange multipliers; q/ Z) t7 k3 g
+ \2 b2 N$ i* g+ Q6 W% [
Store a1 in the alpha array
$ d2 F3 |/ V: m4 y' Z1 K& f; c7 J: u) E5 J6 c. d
Store a2 in the alpha array
6 [1 N/ v' B* r g0 U# T5 s# @
2 \$ w& }# M% s4 Treturn 1
: C+ I) w. ?/ ]; e8 Z- Z& s% eendprocedure; j1 b* Q2 C' o/ f
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。( h, K3 E8 U) y0 C3 F" |) Z- ^
第二步:实现核函数缓存
$ A+ R: e8 U6 o; Z* X观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
8 _2 V( t5 `+ n样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。. r) w, w% r, O* Q
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。8 W" g3 g J Y4 U" N, [
第三步:优化误差值求解! u. D5 H/ N4 @5 ~+ N
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
4 T+ Q" Q/ ]2 x/ g& F% e+ L7 uE(i) = f(xi) - yi
( _! L. e$ g% b/ T, k7 |8 {7 Z这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。
2 ?- r8 S4 {% W- N, `platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:; J( J2 k4 T1 {$ i: P* q6 |8 S
& p0 Y6 d: {! i0 A# ^4 {5 m- U' J p
也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:5 e3 }$ F7 s& Y& E" {
E(j) = g(xj) + b - yj
! M% V+ l' b" E3 Z所以最好的办法是对 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 的偏导数就行了,具体代码类似:; L9 x, m2 D* f9 u; K
double Kik = kernel(i, k);
. x# n3 s% Q/ z, ?0 mdouble Kjk = kernel(j, k);
6 t8 I* I" x* o! f2 t. S4 IG[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j]; ? n4 p0 }/ m4 ?; @
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。
2 M. e' E! K4 i这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。
! i9 I9 v0 @ A- M1 w6 I第四步:实现冷热数据分离
5 [# Z# |% n* g E( |+ _$ KPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
+ k0 Z5 E# K! Q5 s那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。
, N1 Y0 r' T6 y, S随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。$ J2 |1 r5 h( e/ z
第五步:支持 Ensemble
# L# y- R2 Q, d+ p; \大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。
5 z4 p7 n0 v/ [" J最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。7 B! g( o- u2 U: Y- E+ u
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。
7 |, I b9 s9 W6 `+ H7 ?7 _) S实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。2 G& y! Y+ j: l5 r
第六步:继续优化核函数计算
- u- p) F& i) f `, v% A0 m- V核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。
+ i6 l/ ]$ l! {1 V6 L; G1 H由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。9 `) D; c6 m4 J: O
针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。5 E/ b% O. `# t$ O8 X+ w( }. _
第七步:支持稀疏向量和非稀疏向量: k: Y& L6 d' U" V1 t, \" S, }1 d/ U
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。
4 _3 V# _' H' y* n3 R* f. @1 z但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
, h9 Z: r0 m+ s2 @3 u y( P) ]非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
( L+ y8 J: |% j3 Y) b; L3 Q6 @2 A所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
9 X6 {/ | h$ o! W) I0 P第八步:针对线性核进行优化% f% }( B8 g0 i4 K6 x; s
传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
- c5 N8 F5 n: S) [K(xi, xj) = xi . xj
/ b% d7 P' ]/ G3 ^ _* Y% n0 x还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。
2 J# v6 E! R% W! }: g" C同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。- P$ e: Z4 z! ~
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。5 c- i0 {/ n% q1 D! A: G$ ?2 X
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。# a& }9 D% a: B4 P+ h9 S6 M" [& j
后话% z* V" M! B5 g; }- S' _
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。. U6 _" p5 a0 D5 Z4 ^; o
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。$ |, O6 o( _7 e6 O! w' X2 f
+ T, F! \' a1 C) f: d8 J. L来源:http://www.yidianzixun.com/article/0Lv0UIiC; G8 s6 W0 T3 |: z0 J6 G5 v: ^
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|