|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
- [7 Z3 G2 [0 v+ a- s9 Y学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。
& c5 V8 X3 x1 C) ?5 ^4 X假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:+ l( B3 F+ d I8 k9 Z

. [* v; z4 Y! F! @3 ?0 MSVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:% S" s, Z! B4 p& C+ [$ P" ]6 f

& m6 O f/ z7 R8 l0 U/ m K6 }4 ]上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。3 d8 Z6 C' N7 Q( [6 O7 ?, y9 B
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。
0 t8 j( d$ ]) ?4 s4 A第一步:实现传统的 SMO 算法! }0 p$ \, _/ ^# [7 i1 Y B
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
0 w6 s2 {* t! `! _5 _# sprocedure takeStep(i1,i2)4 a' x7 \- e2 H& m. i
if (i1 == i2) return 0
1 e6 J. h$ s# l% v alph1 = Lagrange multiplier for i1( w5 E. m6 j/ o# T8 s
* O. v+ b" {0 f
y1 = target[i1] t/ A$ @/ M1 \/ b) C5 P
% [$ k; x1 y7 f/ M- C# t6 F: H) AE1 = SVM output on point[i1] – y1 (check in error cache)
& ^. y; D, Y6 ^
3 Q/ ?* j1 Q* E# @0 ls = y1*y2" t- D8 E- k I% D# c/ _
$ [9 l6 I {. \0 I9 ICompute L, H via equations (13) and (14)
% N1 s9 Z( g8 r% X" I6 }% C
. ~+ J" E4 p2 E C( ^! F+ r+ tif (L == H)1 x1 h( V6 \: m6 ?% v
% M/ }# y1 r# Kreturn 0
+ ^7 ~% H9 u* L9 _8 I, Q. W5 `5 S ^- l' u' l! n1 X
k11 = kernel(point[i1],point[i1])/ V# ]% I) p# D) I" Z2 w$ y7 h
; p0 \5 c7 M2 S# X3 w% v' Rk12 = kernel(point[i1],point[i2])
* Y( Z! q' G# j7 I: i: X1 F* a! Y
k22 = kernel(point[i2],point[i2])/ @" s5 q8 l$ \& `1 r0 U2 I
1 H4 _. E2 B. z+ N& X4 \
eta = k11+k22-2*k12
9 Z) w, y9 N- ?( p% P1 Z& L! P3 B9 Z/ E# V
if (eta > 0)
+ T; M% H4 Q. Z' g, j8 [, s6 p `- x" g+ n% a
{
: \, h1 ^9 q1 e: j+ h, S5 G5 Q- w
/ p" e$ H( @0 z& e- K8 _a2 = alph2 + y2*(E1-E2)/eta
! U3 Y7 y- ]% ?" `, {, b2 U7 K9 O! s& V: Q8 B# C
if (a2 < L) a2 = L
2 q, `* l( i3 D/ _1 P; Z
, h5 h* M" N/ s3 b0 Qelse if (a2 > H) a2 = H
& f6 s5 S4 Q3 c7 [4 ?" b* Q6 l4 G9 e# U9 ~) {9 b5 A
}
, b6 G/ o& b0 G! B/ F$ G
4 h/ L" B+ N0 h5 i3 u! k+ Lelse
* H7 o6 n6 j' W% B: H/ O* }1 M/ B5 | l3 H" M
{: F$ t- O7 h7 O4 O
4 U( _. l. _& P) }* O) WLobj = objective function at a2=L
+ O+ u# K9 I( n
0 K! [# Q7 Q1 X/ `" [. ]6 D8 ^Hobj = objective function at a2=H
2 K8 ^5 l4 q4 A8 L. O9 H5 |; d) V G2 t; K. x: [% E; L Y' a# I8 a0 s# r' J
if (Lobj < Hobj-eps)
) \5 `* A/ R. E1 e6 d
l4 m- r! l& M9 L+ ba2 = L
4 E0 @* R0 L/ z' s3 P
. k; _" ?' w( I' S) S) M6 qelse if (Lobj > Hobj+eps). |% l: q) G# A9 X
& Y; q& Y1 J) g* o8 d% R" U7 O
a2 = H
% q; @* _& K3 p8 d7 u, u9 h2 j2 j2 Y' ]
else
- O. p! g1 @; g: L0 B" {) j6 C, V+ c6 t5 X7 T
a2 = alph2& C% w& r3 M1 `! O! `4 s
7 o; N, v( R8 s/ M' ], A1 S}
# ]! b f9 R r% O9 n' {% Y3 m$ Z0 b7 H
if (|a2-alph2| < eps*(a2+alph2+eps))1 v8 m) E2 Y U. m# N3 S
% P- L! G0 ^" s4 \$ Z7 U+ w5 }return 0
+ A8 v1 f) S+ p8 f; t. m8 R
+ d9 a0 J& [ @" Z8 I8 N, |0 Oa1 = alph1+s*(alph2-a2)
1 T; \8 \$ L9 I3 F' x' @; y) {% U4 d8 `6 S9 _
Update threshold to reflect change in Lagrange multipliers
, Z5 V# D. y! l! t" G; U# h% m; _
Update weight vector to reflect change in a1 & a2, if SVM is linear% k8 R# L/ O) y* J: j2 u5 p
% {1 ]* N3 j: A( U6 Z. h
Update error cache using new Lagrange multipliers6 z# e6 t& E0 U6 ~7 ]$ D8 @! }
0 z* F( k1 y9 n) D; y
Store a1 in the alpha array3 Y0 y P2 H+ l2 h4 n
' T7 G' c6 f+ l0 |8 K7 I# |
Store a2 in the alpha array( Z/ H/ W% {+ h# B3 z
3 m* G+ U* c( v M$ ?$ N3 Sreturn 1
5 G' K1 \& _! I" qendprocedure
$ _1 \' F1 j; m: w5 S核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
" {" K( c4 {6 P& j+ F) b第二步:实现核函数缓存 T8 a* ^5 J/ f
观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
- Z& o% J/ f1 K- Q& D5 k样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。( @' Y5 @" X$ K. B7 S/ u, I- D y
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
] t2 A* j4 ]. }, d. M# ^ v; i第三步:优化误差值求解/ g( |4 u, e8 j8 _+ x2 W7 A; S
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:0 |1 Z; ~. {/ b, M& p. H; l% D
E(i) = f(xi) - yi: Z: M9 V; a5 M8 m. n3 F
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。
: \3 D. l: N" W" ^platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:$ r, s _% u! R4 `9 S* m0 O2 V% E
3 J+ B; u" Q. q9 l; m也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:
/ x; v% t# |. [6 Z5 ^E(j) = g(xj) + b - yj6 k/ j. |/ C, z$ j) [0 J6 I3 r9 S9 U
所以最好的办法是对 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 的偏导数就行了,具体代码类似:: r( K, ~. M9 s: [7 n
double Kik = kernel(i, k);. R# R* K! N2 a, ~ D) q" ^+ [# Z8 {
double Kjk = kernel(j, k);8 C% P6 ]" s7 I% D# P2 _
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];
2 y" o: C$ M; X+ O8 ^5 @2 d5 j把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。7 C% l! K- k( u* }2 u' Z
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。+ ?0 v$ n: L3 l
第四步:实现冷热数据分离
/ U8 r8 C6 n2 y4 KPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
: b5 Z' {2 v) C那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。: _; T: v5 I) |( k9 |8 L# [
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。9 O4 ~1 z1 ~2 M6 p/ k
第五步:支持 Ensemble
) t8 b- B/ G7 I0 s0 g8 I- s大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。
) x% C0 ]5 z4 a2 C0 {: [$ z最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
$ g6 N1 O8 E5 G. G2 S这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。
- [) L0 N) w& w. T8 e实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
! u- m8 Q6 q5 I4 ?2 |第六步:继续优化核函数计算
, Y- G x9 _1 @1 ?核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。
4 [+ `" f1 h6 w2 v6 B: o' g, z由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
' w, e, c" A0 _1 \7 W) n$ z7 f针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。/ s6 G: B5 x' p
第七步:支持稀疏向量和非稀疏向量. o- v: C# z; l; v
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。+ i- [- Y4 J e+ @
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。$ I) ^1 |2 R- `7 y/ f
非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。: W z! Y1 i5 r( u; n9 R5 i, G
所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。 A, Q" i. @0 ?% @( a
第八步:针对线性核进行优化
- Z* y0 v0 L5 W6 L& y传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
; q- {" x8 w2 g7 Q5 RK(xi, xj) = xi . xj. [0 P/ p9 D* d- A5 i0 {- {
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。
|$ k8 W: X1 U' W5 R$ u- l同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。
7 W9 l* s% k0 `- F2 z但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。4 T- s" n6 f& P+ n3 m
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。" F: [, K, E+ J$ [
后话
( Y/ T# F5 D L上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。2 M; P7 N( V& V6 n; F$ z3 r
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。1 C" `, L" k z3 G3 ]. O9 _
: P/ l7 E4 W1 v来源:http://www.yidianzixun.com/article/0Lv0UIiC
( u$ c* y! O9 k a3 f( L免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|