|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
6 Z' O. N5 [* i. V0 Y0 j学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。
0 F. o5 b0 H% B" g' W1 i假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
& } l: a u& @$ r& p' ^9 K 7 z& d7 M! W+ t. x3 b
SVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:. {; E" ~! t+ q$ f, p: @* P
( @6 i. j5 O( @" k( {9 d
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。' F. O1 v) ~$ l" q5 o2 r b- B0 s) }
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。 h, I, `' b5 w& M& i% {
第一步:实现传统的 SMO 算法
7 ~% x3 s" Y! E" G' x现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:! K* p) \6 A5 O0 T& X5 q u. g) W
procedure takeStep(i1,i2)
' U/ p- f$ w0 Y0 d$ G if (i1 == i2) return 06 b9 m) q4 H5 r
alph1 = Lagrange multiplier for i1) \& G% @7 Z' ` y* `3 l
; D5 V& ~4 Q$ A/ z; ?8 ~6 H
y1 = target[i1]: {8 ?8 A { i, A% @3 i5 K+ |6 _
n/ O8 s! w6 s+ {. BE1 = SVM output on point[i1] – y1 (check in error cache)7 {) G% f1 b( g9 n
) c7 T# C" m+ Q( b% }s = y1*y28 r5 w5 I& u* y: x/ R
( Y0 y9 X6 j: ZCompute L, H via equations (13) and (14)
5 E# L3 W+ l( |/ X& W: [* T" U# `. g! b( X& F5 j
if (L == H)
' p# H: \: A/ N( f
9 v! y& P3 ]6 k! }, rreturn 02 X$ ?, {- g* ^1 ^5 ~
/ W2 Q2 w8 S9 i0 i5 H: @' b) ^k11 = kernel(point[i1],point[i1])
6 Y( ?! H5 D6 q+ |, g6 a* L7 A8 \8 {2 t0 L M! `
k12 = kernel(point[i1],point[i2])
' ]2 i2 V8 N8 F' V; K8 P. [
# r+ W2 _/ U8 ]* Ck22 = kernel(point[i2],point[i2])1 a) {) y& r9 K+ T7 a
& C/ ^3 b* k: c+ t0 Qeta = k11+k22-2*k12
. `" v( T) f0 ?9 X. ?1 r# Q2 L. @3 Z8 K: i
if (eta > 0). j1 q% p. H- X1 g& K! A4 B
3 M4 H8 S( j4 e{
8 _& G5 j9 D7 R8 X* |$ Q* o
# Y! _+ g( P0 U1 s* ~+ Pa2 = alph2 + y2*(E1-E2)/eta6 F# f! v, p, U
7 c) d6 t2 I) z- N+ g. v+ uif (a2 < L) a2 = L, U) _+ {4 M$ d$ K% F& n
8 e$ W: {3 [6 r9 g2 {% B1 p$ {
else if (a2 > H) a2 = H
_- \) o0 G B o$ d5 A6 i2 L! _
}% O: m: P+ u/ e* [9 ^& F+ `2 n
/ M4 y( d4 r" R9 X; C; f5 [
else5 b6 u: w- F" \1 F2 z Q
/ V+ s9 c- e% w' E
{$ g1 ]+ M" g; z. i
4 ~0 u) J* T. {( G; _Lobj = objective function at a2=L5 _; ?9 J5 H! y v! P
" ^3 y0 q% ~% O9 Q9 ?1 eHobj = objective function at a2=H, y. u7 c, e" f8 @' m8 _1 b, S' A0 s
5 g8 N D: G2 h! T
if (Lobj < Hobj-eps)
& K; g4 A& c$ n, c9 i# E5 W g4 Z2 C1 z# P5 O$ s% w' k
a2 = L
! O5 F' g: X. P# M2 F" K& Q1 e) }' Y' g+ \/ l: s
else if (Lobj > Hobj+eps)
9 _% J! e/ S/ a [5 Z( A2 b! x5 `6 y7 Y4 F% d+ ^5 b# l+ {
a2 = H
& j d2 D; j! W/ T [2 k% H' a& I7 h7 @' B
else6 ?# P$ F; B2 P3 W& f* }) I
4 V5 g6 p; \1 P! T% Aa2 = alph2
T) Z6 b/ \& j+ m7 P# K
F, R! D$ O# X5 W2 |4 `}* w6 Y1 l+ m5 a. z/ Q9 v+ f8 y
( A: X) q' c$ B/ Y& Y- g0 X8 @
if (|a2-alph2| < eps*(a2+alph2+eps))
' _1 ]% Q# _& _7 \! ?$ G3 \# I3 i ~1 I+ d* p0 `. K
return 0
/ c& j: L! D9 j4 R
& S! `! u( f, m; @6 fa1 = alph1+s*(alph2-a2)
, r+ D% `: ?% o4 t7 J; M6 [
; w' _* C% H3 C7 B! G* k: TUpdate threshold to reflect change in Lagrange multipliers U+ x# {1 t- M; G8 {+ V8 R# t
+ g: p8 C, |+ V* VUpdate weight vector to reflect change in a1 & a2, if SVM is linear
7 p) ^& F$ }/ A4 a
# h- Q6 F+ _; N ^( V/ o3 TUpdate error cache using new Lagrange multipliers
" d' |/ @. ]9 ?: I8 u4 e# e
% ~& q* y& u0 U# G \# @8 B6 b0 `' lStore a1 in the alpha array1 |. D2 e! ?. x* I+ V( H+ Q7 p) O' S
& I; K/ N5 |+ y/ l6 Z8 M8 @% E: x
Store a2 in the alpha array6 b# p. `/ y& n- m) q* [
; e& O' f6 v2 B0 r4 D0 y' G# z1 j
return 1& k4 ?' R2 ]# K0 C; l, ~0 y1 h
endprocedure; a- f( G4 W g6 N+ }) s. r0 ?
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。# L1 r# C B% j! W# E$ D% Z9 L \
第二步:实现核函数缓存
$ y0 s: C: B; n# ^3 T# X% [) p观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。
8 H* i6 F% q, A, @0 x* s9 s样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。% h. J- c/ `% ]( _* C. K$ j
有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。; p* r+ G0 w4 o
第三步:优化误差值求解( R" [3 q) v3 e' m4 v
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
! I5 M- p- S/ f6 q* M( SE(i) = f(xi) - yi
: P2 G2 b' x' u这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。9 b6 e1 Y+ y$ X& m" ~. ]9 H# j
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:
% k4 w6 g& ^* l
8 o: r0 p3 d. [, ~, s# O也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:
* v2 w: F V ^" Z7 pE(j) = g(xj) + b - yj: e" u, G: ^, ~6 T
所以最好的办法是对 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 的偏导数就行了,具体代码类似:
7 k8 B ~. z) x, _: |: ddouble Kik = kernel(i, k);0 H. L9 v/ q5 t! z, ~: N
double Kjk = kernel(j, k);. f5 ?, O+ a0 u% a
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];1 T5 E" j* r4 F0 D+ f" w+ R
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。4 u- V8 f% g7 ^/ w4 b. H: K5 T& g
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。
3 y4 Y3 I) e1 [( v6 X第四步:实现冷热数据分离
) T, R3 I, B+ U9 q3 |& {Platt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。% B7 t7 B* z6 A3 l
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。
% ~! k/ {1 q* @7 \$ d随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。! |, a% _5 ^/ d5 `0 V5 R
第五步:支持 Ensemble5 F+ `3 k8 _/ k& z, d, }
大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。1 z: @# h4 c8 A. p/ Q
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。: [- V+ U: [& k1 \
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。
: f) \0 p# m. N( f+ [实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
" b9 ^& G; S9 Y% f! f* z% u- q# D第六步:继续优化核函数计算! |& }& l/ d. s3 A6 c
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。8 J- v4 I& U8 r3 Y8 N9 a8 a
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
4 |' A$ Y* J$ @针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。
|; _& {; A! v* U4 ?8 L5 C- s第七步:支持稀疏向量和非稀疏向量+ g6 o; `5 P& X1 g! h
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。3 q. Q* x `4 H! j1 |" U( Y" d4 [
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。6 ]6 B6 B: n8 v4 f5 H7 y3 Q
非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
$ u8 @# l+ _- M4 l2 @6 o所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。( f" D I( u, K5 f5 z5 g
第八步:针对线性核进行优化7 a$ c0 [6 X$ `
传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:$ l6 R$ ~% q& ?, B9 v" t7 u7 ^ C
K(xi, xj) = xi . xj
! C( w# m1 X: R& \, b$ b1 B$ \还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。
9 h4 I- F4 f7 m( V. v& L6 t h同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。1 n2 @& r& z8 v; Y
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。3 K+ D& j7 e. B$ a# V$ I- W, q4 `
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。
& z2 d9 Y0 y/ k, f( m( F后话8 v% g. A& G' O. l1 i6 I" [" o, @
上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。
1 A3 M* A I. m8 O上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。- ^( O* G3 @& \8 j. r+ m' Y
( e \( C4 [) U+ ~+ S$ v5 l* {来源:http://www.yidianzixun.com/article/0Lv0UIiC
( Z* f, s) _0 y6 f! J0 O( {8 D免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|