|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
0 ?" ~3 I6 U- a( l学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。! A- I! c4 U7 g( A% F3 [
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
& A3 R! h- S! u& m
; ~9 C/ d9 q4 A4 MSVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
8 p# x9 m4 M \$ d" x0 T
3 R9 d4 {- m8 C; N8 ]7 R8 J1 B上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。1 V0 {8 j- Q' ]3 j; d9 y
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。! l4 Y3 d6 }/ v4 F3 i6 ^
第一步:实现传统的 SMO 算法
% \. n) z v) ]& A* I5 z现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:9 M3 u3 o5 i9 {2 }* {) E. i
procedure takeStep(i1,i2)
2 i. g, {) D: Q; r0 I5 b* \ if (i1 == i2) return 01 I! r) p- s1 `! V( M$ o
alph1 = Lagrange multiplier for i17 \$ x) M4 ]' X% F* n/ H% M, A
- [( D6 b S9 o' b, ]0 wy1 = target[i1]
* t- I5 e2 t. Q3 W3 }5 p
6 C# @4 [( [ GE1 = SVM output on point[i1] – y1 (check in error cache)
9 \' U2 ~* }; c, }
, N! c' ]3 ^: h5 ys = y1*y2( e: }; C4 S; `( ~4 ~
# X1 V( h) T. P& bCompute L, H via equations (13) and (14)
1 ^3 p, Y; x: n" J0 H( V% o: v. J( z/ _
if (L == H)) o* r/ S1 Q0 U1 \$ u' T
% M0 S* s) K4 h( T A2 [return 0
1 k, Z1 l0 W$ T( K! D8 j
8 K3 t1 D- ]; mk11 = kernel(point[i1],point[i1]), c* T, T4 ^3 h! G
) ~+ m f2 F; x+ Yk12 = kernel(point[i1],point[i2])
, l* T [9 U7 Y; h: U( M: O9 b4 {8 o6 L& V1 u1 E' t
k22 = kernel(point[i2],point[i2])
: k3 T3 F7 D4 S2 H3 e/ m* O; U' Q
eta = k11+k22-2*k12
& _; L% K" f. x' L5 J% |/ `, f" [; @( ?8 y
if (eta > 0)( H) f- t$ c1 Z8 t1 a0 L( ^
' ]" R$ L- `7 M! e* K; U; h% Y- u{, U% V( Q, F* w* k) T
. r0 C) F, \; ?4 j
a2 = alph2 + y2*(E1-E2)/eta7 }' h$ n5 |. I
2 R" G) G! I& T2 a- r4 s
if (a2 < L) a2 = L
$ F+ H: u; `) {6 w V# c8 Q
" F2 F$ U! D. kelse if (a2 > H) a2 = H) z* H+ [+ ^( D6 O* `
' h8 e( [# x O$ ^}
& t: N% j4 I7 t6 P. N& E- O$ B6 R, i
else2 S3 L" t' l: l% a% I. t# p- |
; D; b* p, d, t* R2 C$ q1 {
{7 `, S" X! G7 P. }$ M# ^- m
7 Z) o+ l6 s# ~: V8 k6 D0 M! KLobj = objective function at a2=L
& [1 G7 n$ h7 o! s- X* |% y5 |7 M; z- `% O
Hobj = objective function at a2=H
7 G* x1 ~$ j: [+ C H& \
. x( {/ a, k+ X) j. t$ A2 Mif (Lobj < Hobj-eps)
6 k: Y0 o/ |! i! j9 i9 M& d9 @3 N1 ]9 e7 u% M9 b* ]
a2 = L
$ ?% s- O# G9 [2 N1 j2 w/ l. P1 o, F( b. ?
else if (Lobj > Hobj+eps)
. h+ L* ~4 E$ S8 ~& T0 \2 w
6 d) w# N8 }: j/ P- r. m/ o4 ga2 = H, }' @. q( k' H3 O) m" z0 z
# p8 z- `" U% Z" Y! d9 L Q, N
else
( p A. j# F! U) q5 Z2 [) {9 j
/ k4 y* I( {/ ?$ a' u! y) h$ X/ ba2 = alph2
8 c' l! J1 x9 z9 Y- |' |& ^# A z; R$ T: t% Z) h9 X' k1 f
}
- f; _; H2 _7 l( s5 f; |2 P1 h) b+ k# z( S& ]8 R, t( z* ?, ~3 O; y
if (|a2-alph2| < eps*(a2+alph2+eps))5 B4 I2 ]! r9 S s0 I: h9 ~
! O& F" p" Z' B) _) Dreturn 0
" ?5 ?. }4 ^, \) D& X* y! k! d4 V% m5 ]0 R4 u: T/ Q
a1 = alph1+s*(alph2-a2)+ r) o2 o) Z4 t- R6 O* c
4 {# W1 b' @' `- f8 ]: j# ]Update threshold to reflect change in Lagrange multipliers
. [. L) q9 e2 I; \, z: J, }# b4 o4 c9 T+ ~9 F$ k" l
Update weight vector to reflect change in a1 & a2, if SVM is linear
& w# y7 A4 h% J9 G( O
; N* Z# d. {/ _3 EUpdate error cache using new Lagrange multipliers
- D: w/ j) R9 _& k. Z% a- x3 ]8 f; ~1 U
Store a1 in the alpha array
% X' @2 h+ j$ f8 k
: g5 E/ V) L1 l, ^$ y! yStore a2 in the alpha array
# \9 B) _* L' j$ l) R7 v; U% x
4 g {( L( @% k* a1 m, Mreturn 1
3 X+ q9 E/ d6 Bendprocedure
1 Y% W# h% H( y% j; n4 H, Z核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
+ j' t" S1 K( F1 B第二步:实现核函数缓存
& q! X* [* X8 ]4 A m: b观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
3 Z7 w3 u: d0 _' { A) h, Y6 G样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。9 y8 u- m5 ~1 e& ?0 Z
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
+ J* ^9 V3 D0 W* S第三步:优化误差值求解9 U8 a8 Q' _8 A& ^
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:8 |: G' \ I2 r1 e
E(i) = f(xi) - yi' T& R$ C7 A" ?8 c& r& u" J
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。
7 |: n. A$ t6 U( W: p3 F4 iplatt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:1 W9 H- `+ {1 X. a5 P3 A) b% J
6 _; O5 O2 G% n4 J. ?+ Z7 l. B
也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:
: E+ v8 Z+ o2 A. L0 M* |! XE(j) = g(xj) + b - yj
" s+ G5 L k! R4 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 的偏导数就行了,具体代码类似:
( P. H/ N2 [* u" Ldouble Kik = kernel(i, k);
( y$ L! C2 P! x# C) g2 T. K* z0 Ldouble Kjk = kernel(j, k);
6 [2 }% Y( N/ p% t4 _1 F" bG[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];. q: J# G3 y5 N2 |" g# h
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。
% t$ H$ W- s! ]3 ?这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。5 r, S+ Q/ e& o/ Y" r9 L
第四步:实现冷热数据分离
4 a( j2 i1 n# \( h+ b: ~9 QPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。5 {$ `3 p2 ]8 m4 U' k% Z" w/ H
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。. } z; f% h3 N
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。
7 f5 Z: F) v# q$ d4 F0 q2 X9 c第五步:支持 Ensemble
0 W2 n3 F( V/ V大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。9 w1 o$ G) A+ `' N' _4 w
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。4 |/ x# ^: M- M6 T1 r
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。
! }& {" Q' J X3 x* U2 @实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
% k8 @5 A8 D% X3 Y P第六步:继续优化核函数计算6 U- N& c& I7 x( [
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。
% Z" ]$ A- Q$ O Q由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。( \5 ] r: [- A/ Q# B! I
针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。 `0 L# q: Y: [8 z* O3 ^; c# g
第七步:支持稀疏向量和非稀疏向量8 D. a+ n6 w+ P" y3 [2 F
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。+ C7 O7 M X! w+ X+ j
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
8 K6 \8 V9 a5 r) p8 m非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
3 r$ R$ R R3 ~9 C所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。/ k: k; \+ Y$ {7 X0 p
第八步:针对线性核进行优化
/ p2 K- I* _# U6 X+ h传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:+ N/ {1 l( c( c
K(xi, xj) = xi . xj" R9 [- v# K* N5 V
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。( c6 V/ }1 N4 N2 g: Z0 P$ s
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。
( X- c0 x( n' D# V7 }& _7 H& s但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。
& h; {: u9 L T+ L' E或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。1 N+ f* f! K u- n1 \* Z
后话/ K* S2 ]9 G6 e3 z
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。
% O- Y$ G/ @+ C4 a: ?: r' |2 x上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。
0 m# i" w, V' N' o# Q
" u5 y1 K0 i2 _) [" y* _2 o0 t来源:http://www.yidianzixun.com/article/0Lv0UIiC$ b* c" H n N( Z& U0 J$ n' b- @
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|