|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:9 W4 Q9 t8 _ T3 O1 ]2 ]
学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。
5 Z! W' D8 f% ^+ A# P, {假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:& F4 Z8 D" n- I+ J
) N8 q& R( \3 h* Q4 P
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
; `! W5 x: w9 t* {
5 _- h1 v& D7 A" L+ \. s上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。# L* p6 j7 p: |
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。
$ l, j& V* ?! V: F第一步:实现传统的 SMO 算法
q c, J' x1 Q4 t' s8 h现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:- H$ p! o( d* y9 K! U# u
procedure takeStep(i1,i2)
" P) V" p7 b' Q1 m7 u; W5 Z if (i1 == i2) return 0
- G( Y: u1 l5 D5 u alph1 = Lagrange multiplier for i1
( p7 o4 V3 _0 w" B4 e4 `
4 |. F0 c6 s3 H8 K, C' Zy1 = target[i1]0 O1 j1 ?( H( s' T1 s# R
* T) f/ K9 @8 l
E1 = SVM output on point[i1] – y1 (check in error cache)
2 u' D. g7 _& J4 ^$ d
2 Z1 g% F/ \' u- i6 a1 E9 @ Qs = y1*y2/ s! y0 B6 l" Q- a
/ u1 B+ Y8 D/ r1 \6 xCompute L, H via equations (13) and (14)
# x# W/ m& G$ ?2 z3 \. x1 {
2 l1 U; ^) F" q" R! {: V* U- A/ [if (L == H)
! N8 o5 X4 M8 b- ]. L3 @/ l# l8 V+ d1 m, E
return 0- C n# c! K W# s l" ]# F2 [' @
u9 L- v+ k4 A' R& I, fk11 = kernel(point[i1],point[i1])
9 d8 B) f0 ]$ ]3 f; c, P! Q$ k0 I4 t3 q/ B3 F9 |: t2 M
k12 = kernel(point[i1],point[i2])
% M" K& P) u- E# y- s& c9 s a( o; Y1 {) ?" d, z
k22 = kernel(point[i2],point[i2])( q. G* p7 b8 E# u y
! @: r) X" I: X! i7 I( K7 I+ teta = k11+k22-2*k12
& s4 W- T/ T( m1 u) x
% z6 t+ ^* m. h. N0 h' s* u6 Gif (eta > 0)
+ M+ v% _) V2 q% u W
$ B1 j7 f6 u; y" W; k" m( n{
! o/ K' y' c7 l8 _0 O% _' b) f, {7 y% l. K" {
a2 = alph2 + y2*(E1-E2)/eta
+ ]7 F" [1 M% v6 a1 B* n, h6 u |1 m- M& A, s2 a9 D
if (a2 < L) a2 = L
, F% H; J4 h8 U) Q
6 D, {) {# Y% F* k: ~" Aelse if (a2 > H) a2 = H ?$ L1 Y6 c2 _9 n1 k: u
) K! X9 x n* K3 J}
- N( n* k! }3 K
& L! l, i5 Q8 e% J- @else ]) t4 S, P: F! G- S' s
/ _8 B( f( E( ^: n& [{
a! v; \* {- o/ p1 m* ?+ W5 b. Z! S8 F
Lobj = objective function at a2=L
* R1 v$ g) u1 Z8 Q3 I0 q' J' f6 f
3 R; Y) b) S8 f4 ` L! f nHobj = objective function at a2=H
6 {8 [ j) d' z7 i v0 I% }* S/ h3 g) ]9 U8 d$ n
if (Lobj < Hobj-eps)1 D6 D$ H h, G: _/ r2 w7 @
" j: K- u' Z [! p& o$ S% D4 Z
a2 = L2 R4 g7 x! P& V
$ t2 a; {9 @! \+ y
else if (Lobj > Hobj+eps)
6 v \2 @3 h! W3 L
/ b$ }) A M5 a5 X0 R, K0 W% la2 = H
" G3 m0 H8 U' w# Q! a k7 h( P: m. L7 f0 u
else
; k, E" {1 g1 N: w
7 {5 w u! L$ h1 a% v3 H6 W0 ba2 = alph20 Y% A" C3 E* k; Y& D
$ i' _+ }& r& b% C9 N, n
}9 K& x* D1 J! q a5 @7 ]
2 Q( _2 D( y: R+ @2 Y% _
if (|a2-alph2| < eps*(a2+alph2+eps)), D% \$ F& ~( H+ t' \
5 K+ C8 h* w5 y4 w+ m
return 0
' w0 j- ^4 [4 b {8 U5 x. h. A1 J4 c% ]* d% q: P
a1 = alph1+s*(alph2-a2)
2 s4 o; B2 _: v' e' Y) W4 s) ^ a# p
Update threshold to reflect change in Lagrange multipliers/ v- G/ O. [# o6 L# J
) V9 U# `4 G' o( \ nUpdate weight vector to reflect change in a1 & a2, if SVM is linear
5 H; a9 I# G2 { D4 O
S/ f5 e2 R$ u$ z5 T8 T/ c* H vUpdate error cache using new Lagrange multipliers
- }. p1 T6 ^4 H% _; D3 P& S, T1 D: m; o& z! f
Store a1 in the alpha array
8 B- N! j# {2 |5 p! K7 l0 b# ~
+ y/ w6 {1 R0 y9 b" M: e) ?& V) aStore a2 in the alpha array
- c) Y2 D' ~: [& H. C9 J. d2 S4 R
h7 `, ]4 ~3 G# L( e. k _# Jreturn 14 {3 o1 j7 q( ]% P" G
endprocedure
' N3 [- K( d4 L9 L6 b! L核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
8 x0 H5 j9 T; L% G* p3 e第二步:实现核函数缓存
1 X1 \8 Z5 F' q5 |) P" V9 r, c观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。5 |" L0 `4 I Z8 V4 V& z% q
样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。
! v# H# l: N" g' p$ {有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
* G6 i& _ z; N+ f4 X/ n, t% F/ }第三步:优化误差值求解
. J5 S0 d7 |% t) H3 ?4 [注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
# i! t9 g- k- S0 N- _( HE(i) = f(xi) - yi3 U7 ^2 Z" D8 q+ q8 k+ B3 f0 o2 d
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。
# |- u% U! A/ S: R# Q1 X' Xplatt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:: i9 J) r* z4 b7 a
2 r# K: G% a. c, p. J+ H( C也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:4 m( x9 |* ]" Z4 I, v: D( ^) O! a
E(j) = g(xj) + b - yj
2 t+ t- y2 Y" J- ~1 ]. l所以最好的办法是对 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 的偏导数就行了,具体代码类似:
/ f! Y. d8 a. \double Kik = kernel(i, k);
3 X) l7 z' t5 [double Kjk = kernel(j, k);& Q1 F- r( V6 u
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];
) `2 i/ H3 J/ M2 M3 ^把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。
% f* j- S$ ~( M- \这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。8 Z4 h! g0 G/ q) ^$ h( j/ ~! o' A
第四步:实现冷热数据分离) I! `$ Y2 w0 O4 Q* z5 |
Platt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
V& H% l- Q( `; L' o那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。. X- o' h' |; _/ B6 T* r
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。, M( Q: K) M+ A8 G" f3 x
第五步:支持 Ensemble
6 f" m7 F' u% M! k2 b0 h1 C大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。
- h* f5 z1 u( R3 W7 M4 s. y最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
b6 T+ E3 m4 F& J) j C/ r这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。
: N0 b: {9 } I1 u) j- R实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
1 N z1 e% z; l7 A9 w) b第六步:继续优化核函数计算0 ?9 p3 S% D1 K; b3 V
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。
) u- z; y1 p ^8 b" I, A+ o% A9 @由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。2 }+ n- T$ f( `; G
针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。) U' P' E: s: @! a3 v$ i
第七步:支持稀疏向量和非稀疏向量
% G6 x' x2 e' i: v& q* _3 o: t" B对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。# M. z' ~+ @ n4 y7 V+ d
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
, Z( }" t+ }/ {' l; Q4 L非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
' v2 k: }2 m/ l' h) V6 ^9 m所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。& i* w( e2 `1 B: K# `6 M2 K7 g
第八步:针对线性核进行优化
' C# k! `3 r' U/ R% c传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
/ b4 u! F. m3 T# ]# S1 ^" e/ OK(xi, xj) = xi . xj0 X3 e8 D& H" o$ ~- p
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。( U" \7 U E: G& i5 D4 W, {
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。, b+ J2 i0 E% |; s7 R3 h- u0 x W& N
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。% [* ~# u9 N4 N8 u3 Z
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。4 H0 I# d) A+ j
后话% I8 z( y @: V
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。: q* |( [1 A% l* W2 T+ ^
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。8 v, ]- Q) l) t. c4 x& E
3 i4 D \. K1 |; Z1 i" k+ d
来源:http://www.yidianzixun.com/article/0Lv0UIiC
( k; j( W0 h* O/ i1 B) V& A免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|