|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
7 g7 r5 V8 E; _9 Z$ z学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。1 n6 f4 A4 D8 j1 ?* s0 H3 G# J ^
假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:
5 f4 ]( `: x6 ~, Y; `
7 E5 T/ Q) [2 E' d Z, MSVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:# p7 r& e8 h* C* G! s
# w( h% P$ m( `% Q0 X( O
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。
/ p" Z/ L; P5 X! r8 L2 K那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。4 h/ p l) [' s% L
第一步:实现传统的 SMO 算法# C' {; S+ @% |# ~ X
现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:
# l5 B, b O& s) b' Wprocedure takeStep(i1,i2)/ s4 n9 r; D( ~: s: i
if (i1 == i2) return 0
* Q0 Y E0 A2 E alph1 = Lagrange multiplier for i1
, ?0 U* b! r6 _) J% ~9 l& L, s: x% c
y1 = target[i1]
, t/ R5 e" @3 ]
# n* ?( E- T" O& qE1 = SVM output on point[i1] – y1 (check in error cache)7 F, q1 o6 m: w3 h& g. y( @1 A! S/ P
6 p9 \" D: g z$ f
s = y1*y2( j7 Z* M; g7 w1 L, F G) c4 g
2 x; d) ?1 i5 S3 h/ D: C
Compute L, H via equations (13) and (14)
5 f7 y9 }4 R! P2 H. Q4 m1 U& g( |# p2 w, o
if (L == H)
9 y) p+ g& ?, a$ R$ T5 j% X$ M8 b+ m1 s/ X5 H2 N' R
return 0
$ _# [6 j. P- l4 p1 S0 d8 x4 I h* d0 t; d; P! [& ~
k11 = kernel(point[i1],point[i1])
2 M% ?% n) [; ~
; d6 r! H8 B n; G6 R6 Rk12 = kernel(point[i1],point[i2])
5 S5 z6 l3 Z. R6 R4 F; _# S0 A0 `$ D- ~2 Q3 u
k22 = kernel(point[i2],point[i2])
3 l& U4 m+ ~: F9 B1 T$ p/ T1 H9 I. \% |- J5 R% P, M
eta = k11+k22-2*k12
% Y. ~+ X0 ~- z7 Q6 B) Y, o6 G8 C* U1 a4 g* m! U9 l
if (eta > 0)
+ w4 L9 S" \' l8 _: m
' g$ l* @: x' E- \) U1 N1 A{
* i% ^, H1 \" Q2 G5 ^
$ ?+ ^7 l" H- S9 Q" s7 }/ na2 = alph2 + y2*(E1-E2)/eta
4 h! ?* e) g+ L) N% ?" n4 ?' }1 c9 |0 G9 w" v* s
if (a2 < L) a2 = L
W2 Q% E! @: e3 j' ` B1 t9 g3 c7 R* ?* I% Q7 |2 A' l U: [. h
else if (a2 > H) a2 = H) \* X6 W {5 p7 y) c9 k+ j o- R
, }/ ~7 J0 t9 w7 T& `
}0 J5 R. ~! a- B) {5 R/ L
# v' ^4 h7 _# e3 l3 G5 ~
else$ b) Z- M+ J5 Z: ]& O" ~/ s
* [4 ^; \# J" Y# Z6 d$ I{
& F( U" e0 W$ g: k" q5 k% ]9 D
# B# o+ U7 A$ s" ]1 k, `& zLobj = objective function at a2=L
' k# @% R9 ]: }& }9 v' y# h/ _% d! N1 S) [- |3 h1 S
Hobj = objective function at a2=H3 h; v. B" o# Y4 C. b
" ~, K1 ~/ F7 j4 V; O4 y+ o& I9 m$ Z
if (Lobj < Hobj-eps); f" Q! `# I" }# n, b- q
/ R7 c. h+ |+ Q# l* L- U0 Ma2 = L
3 g1 q; s/ H# G9 t* {% z7 u: c9 `! p& u% Z H2 b
else if (Lobj > Hobj+eps)
7 h3 c: F9 A# |: n! [0 x6 l6 I# t8 a; q5 E5 P" \: r
a2 = H
, M; ?) H9 Z9 i2 h; h/ l. U& M+ G d0 {0 d* Q4 i/ ^/ W
else
1 }/ C2 B" m& F
" o% _/ A" v8 O6 `: ca2 = alph2" ?" Q- A/ r# Q
& u0 }+ \7 D: s i0 v" f
}
6 W( l: Q- H" }* Q- X5 y9 `9 \6 u: o% J" Z3 O9 y, a, M) w' q
if (|a2-alph2| < eps*(a2+alph2+eps))" V& d" f. _# M0 g2 r
/ S( n: R" G! {" d7 V0 preturn 0' H2 [0 k6 r. q0 U7 Z
- l: \( Y o' M
a1 = alph1+s*(alph2-a2); H: A" \9 q! _3 ~1 Y
0 b- o: y& x* o, W* [! U# aUpdate threshold to reflect change in Lagrange multipliers! i! X5 R% R- k+ K& B. Y. _! k
_. J$ q6 U; {+ U$ }
Update weight vector to reflect change in a1 & a2, if SVM is linear
9 H: _' u! I2 c% E- u
4 l4 }; y1 ~7 z) i! ]7 WUpdate error cache using new Lagrange multipliers
& u/ Z0 s& G7 e2 @4 k
4 B, z; \# P- {0 ~" DStore a1 in the alpha array8 R, b' d1 j7 b- s" c
; ~0 [4 E9 y o' [2 U* D
Store a2 in the alpha array- u v4 E3 q" O- ?8 Y1 I! {1 x
1 `( T- Q. N" M! U) C
return 1' N1 F+ b, L4 u7 n7 {5 |
endprocedure/ g9 g, Z o4 R6 B# s& r
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。* q0 W9 o0 R+ X7 U7 \
第二步:实现核函数缓存
; q. ^, m( s) G5 G! Z- i7 ~观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。. n1 G& F; j6 ]( r
样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。
1 e; g. e1 \1 f1 y X. {9 N3 `( _6 {有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。
) z! Z8 n" a4 Z# N$ h; T第三步:优化误差值求解5 s* e; g$ t5 j0 N3 Y
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:
5 X2 ~) M7 Y5 J8 S) JE(i) = f(xi) - yi' S6 e+ c- G/ n; m4 U |% J
这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。1 p4 p* Y+ Y. N0 m5 Z
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于:2 S8 X& `+ R7 t7 ] z
. ?1 I2 R) l9 O" C$ {
也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差:
+ N) B8 i& e! }* GE(j) = g(xj) + b - yj
6 T1 ]! z$ Z# @5 T4 o% }7 |所以最好的办法是对 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 的偏导数就行了,具体代码类似:
8 Y2 J- G7 H5 h4 {- x- Vdouble Kik = kernel(i, k);4 m. |9 I. \# |! G
double Kjk = kernel(j, k);8 E& P+ J( o7 v; m
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];7 k* `0 H* g5 X: X: @. Q
把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。, i8 J, Z0 b1 w) W; c4 D
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。
4 b& q% h& ~# U; Z* G/ u第四步:实现冷热数据分离* w p5 I% |: l8 e% l' a5 ]9 b
Platt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。/ g7 W9 D8 ]# j2 `3 a8 `3 e4 E
那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。 K7 z: m; c* H' _8 z( U
随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。
% T& V9 v$ Q0 Z2 h# P, B第五步:支持 Ensemble
" C% J: v: v' E, I. C大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。$ f! O5 J6 F+ |
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。
5 O$ X. d( m/ `& ^: \$ a) }这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。/ M5 u% @4 C6 e w
实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
$ u* D# O6 I! A: L! \( C( d第六步:继续优化核函数计算
Y0 ?; U! `# c2 A核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。+ K! S8 s. G0 ]* B( X
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
" r7 B" G1 F; u3 K) _- o1 ]$ H7 B U针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。
$ k) h5 i/ C5 u2 X' ^* M第七步:支持稀疏向量和非稀疏向量
; |% D+ P* q, `5 S0 p5 M对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。! U1 @) B) b- _+ E2 i
但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。
; ^3 o" i D5 K& @: T' C非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
* p( }/ q# q4 A' k所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。
) c' ~8 n- O6 |# H第八步:针对线性核进行优化. U8 `( T% @+ k$ ~, \3 }6 E3 C0 @
传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
$ _& C, q7 q- X% X8 l; l2 b( pK(xi, xj) = xi . xj
3 [ G! l4 b6 w还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。0 P3 k Z1 G: v9 Z
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。; s, l( y7 }- O3 H
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。! n: V# |) k' U A1 r; K( I
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。
$ F" o1 R) m/ _( E" `& k后话
; w+ q# a0 f/ Q1 r4 X- ]. k( n, T上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。7 Z" F+ ?. j' v/ ?3 K' H' L
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。% J* L) E* w" F, M# d+ q4 b
* N" T) |( [) B% M+ n) V8 y' }, b1 Z来源:http://www.yidianzixun.com/article/0Lv0UIiC
5 ~, c& ^! [1 E. j免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|