|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
" q4 I0 j' _! [/ C学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。2 j t) _! O2 j3 M0 I, o
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
3 u+ F) Z! P% t( R' t & q2 n9 n3 l, j+ `0 t i
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:& c7 G2 m+ S' g$ I, a/ r/ b
, t1 a6 V: A) R. g" j
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。
4 L# S" S3 m3 n, N4 `- C" w2 X6 k那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。5 b5 l0 F( i5 x% `! T. X8 V) i
第一步:实现传统的 SMO 算法
; F; T' W' D. w4 P% w8 u! }现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
3 B9 J N) ^7 j+ v/ T" ?7 M# {* Tprocedure takeStep(i1,i2) _8 O( R5 j' v, ?8 C& A6 Z
if (i1 == i2) return 0
, ?* X X) ]' L; k- d alph1 = Lagrange multiplier for i1
9 U2 L5 K, X7 B: q M5 \1 K" u3 }
y1 = target[i1]; R: e3 y$ D+ J
3 P: c- V; Q7 J2 N4 X8 o
E1 = SVM output on point[i1] – y1 (check in error cache)
! Z. R) g, l6 W/ ^) F' m8 ? D' P0 J$ r( w
s = y1*y2
7 n' L- Z& p$ M5 U; [: b% t4 G4 S9 c
Compute L, H via equations (13) and (14)* b, e0 [5 m6 ^: C# s
# s8 c% b5 I& H* K9 c7 L' W' R
if (L == H)9 R! T+ M; \1 c
! l1 p7 H. T! ?9 t: ^ O8 y
return 0
# Y& H' U! I$ Z4 k! s' E' l
! T6 v. \8 p" @- M5 yk11 = kernel(point[i1],point[i1])- H- W, J8 R- W3 q3 _* x# N$ }1 p
$ `' ]" l, u+ M; R: r, S
k12 = kernel(point[i1],point[i2])
3 Y' f* T2 [8 F' _1 ?
6 b' W: X3 z% D) x: q8 Vk22 = kernel(point[i2],point[i2])8 z! [: r+ ^+ c! t" ?
z9 G! t* P; T0 M3 ?( [9 P& R" @
eta = k11+k22-2*k12
0 W9 s& u+ P# E& o5 s7 Z5 z
" ^$ \3 a$ _# j9 @7 v: hif (eta > 0)& s6 a$ r( n% j( _. }- n
& o5 [8 ^9 U$ e7 T7 c{1 t7 C$ u0 G; g2 }# T" l
' } X3 U( O F; C+ y# Ia2 = alph2 + y2*(E1-E2)/eta' }7 m5 ^% v* d: h
8 v- j# b- Q% l' Y9 J6 L2 `# Tif (a2 < L) a2 = L s" t3 i. n5 ~. i+ ?% w* B" Q1 h
0 E C% L3 n/ C+ \
else if (a2 > H) a2 = H
4 A- q( q& g' j5 u% C% w3 n1 R- V# g* K4 R% u6 c
}7 C! |6 a. G1 D
5 D6 n+ z. w9 r5 F6 H
else, D% P2 C6 S2 O8 _" t6 |
7 J. Q3 s0 G- |; N$ ]0 T8 y{# r% v2 ]8 j2 ?( i1 c6 e# O8 y
) l! G, p; }) T8 p" R3 LLobj = objective function at a2=L+ H6 N2 r8 W2 E& `3 v% t4 D
1 m# [. l/ W; F: P( E+ D' k) O4 g
Hobj = objective function at a2=H
- [( \7 D% t' [7 H
# s$ G9 [* j1 cif (Lobj < Hobj-eps)! }$ U* o9 `% Z2 X# ]9 i7 k
j' j2 y2 H$ D4 ca2 = L
7 k3 B) O. ~ v
. h$ q0 B0 M. K( S) T- n) c3 ]: Relse if (Lobj > Hobj+eps)
) e0 S7 L% }& O1 F1 n8 w' M7 X- e6 {6 B* t- _
a2 = H
# i# `: Z; K A' `8 A
( a/ X8 i4 z: {7 Y( Yelse$ a( P, \6 L+ ^7 R; H
- Q V* J: X* z, [# U6 |6 L Ea2 = alph2# L% w/ f# S" ]9 j- b
7 ~( Q" r! L6 ~% ~$ K' [: A$ Z
}
& ?: p+ _ J( ~
8 `) K8 {7 [) yif (|a2-alph2| < eps*(a2+alph2+eps))
. L @7 O S9 h y5 e+ S' s
# W1 i- x" ~3 I- ~3 ?! w' |! oreturn 0, r. q! c) B* b0 O' X/ V
+ |2 a4 n0 l/ U$ S2 Y8 E. za1 = alph1+s*(alph2-a2)
7 j0 \* H9 W* O
, p! V0 P2 u1 }, U) I' W6 O/ |% {Update threshold to reflect change in Lagrange multipliers
, Q/ Z( ^+ H. l1 q" n& b1 ?1 Q$ b. R
Update weight vector to reflect change in a1 & a2, if SVM is linear
/ b; O5 R' j, s* q8 l# ~
- P2 g/ B8 F& w$ N; QUpdate error cache using new Lagrange multipliers
% N8 f+ c- H. c
) ^. K6 K7 c5 }0 g8 Q; A5 A& O* U4 {Store a1 in the alpha array
8 d9 c* s( m, {2 s; z( \1 s, i$ j
Store a2 in the alpha array
, c5 b: I6 m2 B2 [5 p7 X* ~+ V2 j
return 1
8 o2 K9 | k' L3 n- e( Bendprocedure
' n8 |2 j2 w8 h5 H- N$ f核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。) W# q* \! q) N) T0 b1 J6 e
第二步:实现核函数缓存
% {1 s- z. h$ w* H9 @观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
/ \* }# B0 I5 k7 x S样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。
& `) k: `1 T" [( E2 W2 @! B. o有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。2 E$ K: D7 N( [ h/ L$ e: ^4 B
第三步:优化误差值求解: A6 s% f! {9 c4 @' @0 U8 M
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
- n x7 Y1 ?1 A, F0 {E(i) = f(xi) - yi' l+ w0 L- N1 l( O4 a: u
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。8 @: `0 m- y+ i
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:7 d# V# S$ V: Q# x# B
9 m' _ _8 a& B) R
也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:+ q1 g/ X' s9 e. }& t. ?
E(j) = g(xj) + b - yj
- a. }8 C- B7 d所以最好的办法是对 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 U7 s o- k2 M) C& h, Edouble Kik = kernel(i, k);
! V! M- f6 H2 J) L" A( odouble Kjk = kernel(j, k);
$ A; E7 L+ l( `1 bG[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];) u5 c$ J3 M0 [/ I
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。
, k) X, p" n! u' i这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。3 j! y! O) M% X8 Y* H |8 o
第四步:实现冷热数据分离
0 k) f; d; R V3 D% d+ VPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。8 b) h: c5 o% W5 L" F, Y
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。
# l/ Y: ~. }# p e随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。
2 W+ V/ W2 \6 J( y第五步:支持 Ensemble9 g- k b- {4 Z' W. l7 o
大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。
' Q6 h1 S' K4 G4 {, O2 c最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。! {/ L) i0 k/ J1 U9 T. L
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。. a/ i! S4 i. A+ Z/ f& k/ v. q* ?7 t
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
( E5 l$ C2 n2 E! z, B- B第六步:继续优化核函数计算6 v1 a/ R6 h" [3 D: W L
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。5 L) }4 y2 s& j5 Z3 S; B
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。, d5 g( }& ^7 S; I( u, Y, v3 m8 g
针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。
+ V9 O3 E2 t. d5 n8 T第七步:支持稀疏向量和非稀疏向量$ {0 D4 O4 ~! ~
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。
* c9 H+ P- h1 e6 W: ]" P但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
3 L+ |) E" X b! N- X6 |非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
k% R" M+ t* s/ \/ |, P4 Z所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。4 S- k; C% G0 A0 z) f- S1 C
第八步:针对线性核进行优化
1 _0 L2 H: V3 c# T6 R7 B传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
: x5 X# [! R! K2 p: l. r2 S. pK(xi, xj) = xi . xj/ }, u, [, s- O
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。
3 {. o0 F3 d5 G) ^; ^, G3 {同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。0 I+ P) Z1 G9 N5 x2 C; P
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。
8 s" h+ |# \, [8 l; _8 n& I' v或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。/ {4 r: t/ F0 ^' v" v! W- W! \8 |
后话
/ I. p1 T0 g6 m. Q上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。" b9 \; x3 e* K$ ]8 W" _% o7 S4 W
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。* X" N7 i# z; Y1 w
W- J* @! l! I' w8 d. z来源:http://www.yidianzixun.com/article/0Lv0UIiC
0 c3 r4 F5 X8 v8 g免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|