|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
, H, l1 k/ R: _1 |学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。
, E) Y) h5 M( U假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:) m, n+ c9 F8 Z! K9 \2 J, T

. Y5 N7 H" s( V" d0 GSVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
/ O+ M4 R% b4 p; Z5 |+ [: ]4 ` " m! u2 B4 R) O9 ]% C0 n8 L' T$ l
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。: ?1 ]4 k# {4 f4 Y+ E: W* q$ p
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。- o0 w6 d, O2 i5 h) }
第一步:实现传统的 SMO 算法+ _4 I: h9 r4 m9 `9 k) ^! J
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:3 L% ~: \ t1 D4 k9 q
procedure takeStep(i1,i2)
8 F: j% l7 \9 w, q( }, t6 h if (i1 == i2) return 0
$ I( k' u/ m0 T9 Y( I* y! Y) T4 _ alph1 = Lagrange multiplier for i1
+ m7 A9 ]1 s6 K7 Y
* s/ j1 A6 f0 F: p" N H: ^9 ]y1 = target[i1]
$ R& A1 E! p: V2 |3 A$ \! J Y' A( |' Y4 x/ j4 `5 S/ T4 u2 j
E1 = SVM output on point[i1] – y1 (check in error cache)
K& h: p: V3 J" _* K* M; u6 t9 S1 {3 \; b- S9 {# E
s = y1*y2
3 W5 \1 g8 k" K1 A5 S" n1 r% t5 J+ l" H8 B
Compute L, H via equations (13) and (14)/ m' Z$ _8 A2 @, F8 S
& w- D* D0 J. Y
if (L == H)
; F! X/ \3 Q+ i+ g* R2 I# S: ~% C; C1 y* x( A! h
return 0
# I3 y+ s( N+ ]# r) C& d& E4 T" e w1 S* y' v' \9 r+ m! j/ S
k11 = kernel(point[i1],point[i1])
1 N2 V. U9 N$ T( D
7 y' e! T: f/ Y- f' {. p/ Ok12 = kernel(point[i1],point[i2])3 ~9 I" N$ t' P
- p- ~ z# p h8 H- H0 v
k22 = kernel(point[i2],point[i2])2 m( r* n" k: a Q. i w
1 {4 s5 ~6 ?* x" U _+ r" ^: J
eta = k11+k22-2*k12
5 d e: \8 v* i. w2 ?* {% Z, c% M
if (eta > 0)
! J: E" G* y1 g/ t5 g R. e K. c8 {- L/ H" T( H1 U! p) F; W
{
( A% R& V' u% F6 U0 _: u) T* E/ g1 l' c. M$ m8 ~' H3 z( P2 h v
a2 = alph2 + y2*(E1-E2)/eta3 ?" O( E2 l9 F% L X" ~
6 K0 a! Q+ ~0 i2 [6 b X/ Bif (a2 < L) a2 = L: g0 C7 I' o1 _+ J; z% l: e
5 a4 M8 P$ T) N0 \* y
else if (a2 > H) a2 = H( ~" ^4 |2 o' u" d
% o8 k8 g4 Q, O& F* S( W8 d2 K
}! M6 z) \1 S' m! a. h4 z2 _8 n
, ^' D% ^5 k" O: C8 Melse
, @+ P$ B4 k' `5 F# f
; E( O* G3 R+ G2 V$ Q- W{
' C) S6 q3 L, s# r: Y: K1 a6 P( y* p( w! R- h% r5 `
Lobj = objective function at a2=L
5 ~+ c, L$ c+ ~" B' l7 p' |* M6 U+ {2 S; d2 K$ G+ |2 P
Hobj = objective function at a2=H" k9 ^1 ]+ V' h9 i* L# y* t& ?
* [8 u0 q6 u- C
if (Lobj < Hobj-eps)
9 {. w4 C. ~" q2 u" t' m1 d+ z% s& b$ j* K& `. R& `3 x/ u
a2 = L* l, O b$ n4 ^* U
, m- \& a8 X+ R+ w, Y& a" yelse if (Lobj > Hobj+eps)
7 D* s+ i) f. x0 t) l( n' {8 y0 M+ Q% p: U) [; D+ g
a2 = H) k$ G% s9 O5 S4 X. ^
$ {- K! i8 M5 X7 R7 X
else
3 Z4 H4 J) }2 t* Z9 f6 M9 @( e) o+ g& ]& c; J2 u
a2 = alph2
, H- n! z _/ t+ c$ s
, v+ i" \' \: i9 q}) Q; H9 p/ t! i0 l" y5 E6 e
$ u* g1 E2 n5 q# n, G% ^- wif (|a2-alph2| < eps*(a2+alph2+eps))3 q3 X) m! M' B6 H) B9 ?2 N
) `! {; {- E) r' |0 b
return 0+ j, l* s# y2 _2 ^
" [5 R, p/ F- ?1 B; d7 ^a1 = alph1+s*(alph2-a2)( m- p. p3 W. ]- X" T" R1 D- ^2 @
, [! H$ o# d+ N/ H1 tUpdate threshold to reflect change in Lagrange multipliers
! @8 U# n# _6 c+ W1 ?" A2 Y: u. I- D1 J
Update weight vector to reflect change in a1 & a2, if SVM is linear
+ w. }. z$ n, Q6 i' i) e7 T1 O G5 x/ n4 Y7 |
Update error cache using new Lagrange multipliers
# J" N7 ?! m5 [$ K, E9 W- ^/ d3 k k% i$ X- r; J
Store a1 in the alpha array$ ?( b8 c& D5 e' N. @0 [7 U
/ p n% h5 k- p3 I! n; S3 c6 V
Store a2 in the alpha array( L3 o; M5 b; F" l3 g7 w
$ L! I+ |5 [' Z: G6 O' k" f5 T: qreturn 1) p6 \6 i2 [, G4 y: i$ F6 u
endprocedure7 F1 r$ Q, r/ L) V! w
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
9 U. Q1 J8 L8 [# V9 G* D第二步:实现核函数缓存1 ] e. B$ Y3 t+ I- J
观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。5 s9 {8 e7 a6 K# H2 M5 F. _' T
样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。 t; G) Z( b. N" J
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
( ^ P6 a6 C# T4 a第三步:优化误差值求解) ~0 f/ O6 h7 S. m) Z& G2 S
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
& _8 o# S1 ?' {9 U7 G( xE(i) = f(xi) - yi/ r) f1 A& _1 m
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。6 a; {6 _( b# U) |& g7 K' k H
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:; A: _$ U* B3 o- }) T
6 p( R( b$ y% }
也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:
/ y; A) n1 D- M+ \: }E(j) = g(xj) + b - yj' j9 b- O1 j7 O5 I0 ?! K
所以最好的办法是对 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 `& z5 Q( U. T* M f' O
double Kik = kernel(i, k);
, L @ n- m& ]6 Sdouble Kjk = kernel(j, k);
6 D/ k6 c5 Q4 |5 v2 A, yG[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];
, N2 U/ I, T! ^3 ?( J% W, Z* R把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。2 C, Y" g# f8 _
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。
/ `$ R9 }2 A; n第四步:实现冷热数据分离
5 T6 `6 M9 X; u. C4 |, EPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
3 [/ _5 l( ^$ h那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。
% Q, \2 J2 g- o: C9 c随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。
; E5 A |, m5 f& j" N2 v第五步:支持 Ensemble
) `; R4 l9 M `* @$ c8 @% \% U3 S Y( X; }大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。8 K- X0 o. u6 [
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
4 u4 Y. l- e; a4 ^$ f这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。
* z k" l. [* k% y. n+ F实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
) W' X, h% k" g* r6 _1 k第六步:继续优化核函数计算
8 r& S6 `" X; x: S3 Q N6 S7 r* P1 P核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。) z& v( ^1 z+ z% R& f# G4 e" l
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
9 G! l6 `; B; L; _针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。
6 d( d/ \: z& E+ u! M3 S第七步:支持稀疏向量和非稀疏向量- y5 b" j* [/ ^ W" h' @
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。% T- G0 P. {& {( K
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
* h- g' i. u1 U. V- M6 `! a q8 Z+ u非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。, x9 f% B1 M/ D/ y
所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
3 \2 [- ]) Y. {9 N1 ~: r第八步:针对线性核进行优化
^- l; `( A; u7 W# y* I$ m5 _9 Q传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:8 {3 K3 ]+ }( P8 U1 { D# a
K(xi, xj) = xi . xj8 A, B- P& R- k4 f
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。
( w$ v: y, \/ i" j" d, C' T& i同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。7 Q: j6 g- V1 C
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。
# u& g" }: _/ l! i或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。
- e0 }* V1 g3 V, |3 d后话$ ?- U( U( A2 W2 N
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。6 N6 k# ?: \! N6 h; K+ B2 \
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。
3 @; O) g: g! Q3 A6 V# W' z6 W2 `+ c* J2 ]. U r& _
来源:http://www.yidianzixun.com/article/0Lv0UIiC
{$ W# Z0 k6 E免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|