|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
% i/ ~$ p: I7 ]4 a3 I学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。
8 Y A, m, @& ^- J假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
. I" p/ W! D- z3 x. C1 a
% U+ w) f+ N. GSVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:" d8 V& `4 m4 f8 t/ b

6 K. C9 C8 ^' n上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。
( y$ L l, B. {; T6 g v, A- [. p那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。( q, ^' N" t1 @8 V* u
第一步:实现传统的 SMO 算法* S8 M. H- Q9 M2 n% U! a4 q
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:7 n8 c$ c0 t) A0 |
procedure takeStep(i1,i2)& Q3 c$ X) a9 a1 }, [+ V
if (i1 == i2) return 0
: D" j. g& K/ D* ^- r( m7 G+ i+ R7 g alph1 = Lagrange multiplier for i1
) X1 Y+ d6 R6 K0 n5 G0 p
* u2 W6 }7 g8 J( ]) [y1 = target[i1]. y6 d6 g3 t, _7 t
6 l& k2 Y% P F0 }6 w
E1 = SVM output on point[i1] – y1 (check in error cache)
. D# ^: A- g: f, D% V Q
$ g2 q* @; D4 \, g. Vs = y1*y2
3 k6 P% K# W' @) _: ?- Q) }# w# ~* `) y
Compute L, H via equations (13) and (14)
/ [& x* J0 I( b4 o
/ r- K- W3 x2 [: w5 V% _; Xif (L == H)
5 i; M4 ^; v8 Z1 \8 N h; t( r# k$ M( C3 u* F% _1 p0 i8 B
return 07 @6 v9 b0 K `1 i- C' S2 @
1 K5 u; X; G8 [- ~( Sk11 = kernel(point[i1],point[i1]) i. V2 j% f. V
7 m. j | @$ t, _9 ]* D! i& x
k12 = kernel(point[i1],point[i2])- e! a8 j# ~& i* K0 k; a
- ]! ^1 d% B' @$ k4 E' z1 Q. `3 pk22 = kernel(point[i2],point[i2])6 W: y3 |7 ~/ x. v) }8 D
0 |9 j; [, @3 p$ G' Peta = k11+k22-2*k121 O5 B3 w4 }7 Q" {, J, ^
9 V2 G- {. [+ l4 f$ vif (eta > 0): m1 U, B- N" {4 v1 K% `! Y
0 P& I% d6 ]4 \* O) r
{
k" C8 J3 a5 g2 c, C) M9 A+ R6 P% G' x' i; p
a2 = alph2 + y2*(E1-E2)/eta
. P& f; T6 g9 H! K/ Q
( y1 Q- C5 s) i& Kif (a2 < L) a2 = L
2 t) [# s2 x, d p- ?- I( z% }0 p- k5 u* W" h4 S5 {, Q, m0 G: @
else if (a2 > H) a2 = H+ }+ }3 k1 _! E- X. c, T; |
, U/ [# F- Z' h}; {9 @: v1 I e: y
1 @: O. S) H. ~/ Q+ I
else
' t- ?4 t3 c& R# i+ [
. p& m+ N( g/ k; z& m5 f{% g* | \( }( f L& K
9 e9 ]. m L& `) H- ^" ~, Q- d
Lobj = objective function at a2=L$ Z) S- F( q3 x: {# w1 d+ {& @$ z
]1 i: o9 P0 `Hobj = objective function at a2=H
! K0 b& z/ E4 f Q( Q
- H* r2 `8 S, m! x* o( Hif (Lobj < Hobj-eps)- B% z$ V9 e0 L+ i" n5 R3 x2 @1 O1 K
- p7 b" j0 E3 g6 ~5 w; p
a2 = L: @" K* B5 c1 @( c
, h# g8 t3 t! _else if (Lobj > Hobj+eps)0 Q' z0 c6 t0 E8 _; U- {: w/ y% k7 c
5 m* p W, f; U3 K8 G0 V1 S- h
a2 = H
- R' I2 ~+ G I6 P2 q
+ \. r* O! m. velse' ?; b1 [- f7 S: N V
% N5 ]+ Y3 T% a5 J9 s
a2 = alph2
( M: w7 n# d* h, P! i" e# W. B5 N, x' t
}0 h! B+ @# w4 y$ w5 s4 ]
! h& D; u$ Y) `- g' I& v; [' L7 e8 l
if (|a2-alph2| < eps*(a2+alph2+eps))
6 s4 O5 w' h8 L1 k; b" Q( S2 s
7 W/ G8 v1 L2 x x0 [, J ^1 Freturn 0' S! ]3 b1 D% q+ C( v2 g3 o. _$ S: t
9 g# ^0 `3 \9 H7 `1 F( V& y4 K7 C
a1 = alph1+s*(alph2-a2)" r& T6 {3 ^9 b. l: R' \
, ?3 S6 Y7 M e6 N- w x
Update threshold to reflect change in Lagrange multipliers
+ b9 A. n0 I* _4 w) t
" ?) O5 @* M9 f2 {( _! F# r' nUpdate weight vector to reflect change in a1 & a2, if SVM is linear
7 I$ R" y/ r4 j& t4 \$ K5 U
3 q; ]1 |! K% _Update error cache using new Lagrange multipliers5 C; f5 ?7 N; d. X
* t" c0 h6 c$ L8 r6 Q9 r/ i
Store a1 in the alpha array
0 V3 I* n+ b5 j7 m3 }) K& i4 u* Y
: _5 B3 i& m4 I# b3 R% pStore a2 in the alpha array
2 C0 C% i! }% b0 X$ X8 a! k) O' K* [6 v, O: |% v
return 1
$ U& G( s& b% n; Bendprocedure/ p5 f2 X# B# f4 Z% A5 t/ R
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
}# U) b9 p+ J, H2 B% b第二步:实现核函数缓存
) n% U9 q C* R5 @. {9 m; U) U1 K8 G8 j观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。. J# w* ~ g) q
样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。
( e9 j- F% a4 x+ b+ r& d! U& ~有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。 y; S- w- y& Q/ o/ s
第三步:优化误差值求解( i% T3 r7 p- J) \) Y0 j- L- V6 }
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
& h" T' s. T, _- hE(i) = f(xi) - yi
" H; w9 H! V) r4 m. {7 ~这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。
3 k# l/ y; o* C3 P4 z* Mplatt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:- X5 t! E0 |9 O. o0 p
& ~9 }( B# n5 Z( V: m1 e也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:( E, f8 F( U/ o1 O! t
E(j) = g(xj) + b - yj
1 m h7 p. o6 z6 L% w7 B所以最好的办法是对 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 的偏导数就行了,具体代码类似:" l7 M# S8 a! X; {. |" Y7 _9 S4 r
double Kik = kernel(i, k);8 b" p3 H" k" M- Y; w
double Kjk = kernel(j, k);# }% C, Q" V z4 I# R$ ^$ m/ f! @- j
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];9 C" D* s0 U! T \
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。5 m- P' }. j* j) `2 D
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。
# N/ V: G5 F* y7 i0 s7 c第四步:实现冷热数据分离
- q" h& F* T. x6 X6 BPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。7 b5 G' j( K& X" e6 J v9 ~
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。" f% Q, N* g, R. p* x1 r& C
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。: {) l; T3 q8 S9 R0 j! C- D
第五步:支持 Ensemble
) r- J. |7 e8 ~& p0 z3 ^+ S( B大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。
8 d. @9 C% A3 h* Y% j* m+ I最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
, F4 d7 z' f# p+ F# _' j. y0 d这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。' M+ \8 Q5 Q: h
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。" ? i$ m8 T! j# t9 N# I; m4 ]
第六步:继续优化核函数计算! k* U' E8 k) g/ g; f1 c5 I, P# ^: x* U
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。
5 |' r. y. E5 R4 N- E由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
% b( D6 f" f6 O! s/ r4 q针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。; l6 `3 F. S& x2 o6 y8 W$ p
第七步:支持稀疏向量和非稀疏向量5 ]$ U- e: a* X8 |; ^) A
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。
( @) g) o- z. }% l但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。1 V) p) S1 ]0 j* `, q
非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。0 {6 p! k) x8 T3 i0 D
所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
' w) m6 P8 T% x3 C. V9 v第八步:针对线性核进行优化
- L5 n& S$ K; x+ m0 P% p, e* k) F传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:; g+ F' S( k j7 p
K(xi, xj) = xi . xj$ ~* z# a& ]0 a5 n
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。! b1 K1 J; N3 ^& p% D3 j9 s
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。
' Y# B9 M& v; g) t% w9 ~但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。% p3 y; M$ K. x: K* k7 W) m9 m
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。
7 I% T" t3 E& G7 O后话
0 o8 f8 `, \% y* A! ^ z+ W, H: T上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。
- W0 g& j$ C$ s) J上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。# J' }, N# } ~: F Y
9 B a! ^- E( q" b9 P9 a
来源:http://www.yidianzixun.com/article/0Lv0UIiC+ ~4 o& y6 V$ y
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|