|
|
雷锋网 AI 科技评论按,本文为韦易笑在知乎问题如何学习SVM(支持向量机)以及改进实现SVM算法程序下面的回复,雷锋网 AI 科技评论获其授权转载。以下为正文:
: E5 X, p0 W; b9 F+ j学习 SVM 的最好方法是实现一个 SVM,可讲理论的很多,讲实现的太少了。
! B+ }8 w* F! L& V1 b4 m4 r" R4 j假设你已经读懂了 SVM 的原理,并了解公式怎么推导出来的,比如到这里:! g) n& }+ f ]8 W! k- D& _$ }

* z# V% q" w9 ~, xSVM 的问题就变成:求解一系列满足约束的 alpha 值,使得上面那个函数可以取到最小值。然后记录下这些非零的 alpha 值和对应样本中的 x 值和 y 值,就完成学习了,然后预测的时候用:
! u% i; f% a/ l: T. U0 N& h 7 }) Q# K3 x0 z1 ~$ G$ J
上面的公式计算出 f(x) ,如果返回值 > 0 那么是 +1 类别,否则是 -1 类别,先把这一步怎么来的,为什么这么来找篇文章读懂,不然你会做的一头雾水。2 O& v4 U X- _1 n, t
那么剩下的 SVM 实现问题就是如何求解这个函数的极值。方法有很多,我们先找个起点,比如 Platt 的 SMO 算法,它后面有伪代码描述怎么快速求解 SVM 的各个系数。4 ?# B. ~) e' F5 x" T, `: g' ~5 a
第一步:实现传统的 SMO 算法
2 k, r* a+ G9 c2 K; o/ _& o现在大部分的 SVM 开源实现,源头都是 platt 的 smo 算法,读完他的文章和推导,然后照着伪代码写就行了,核心代码没几行:3 x5 L \2 q% }! O
procedure takeStep(i1,i2)
1 a5 S; N1 X) ^ if (i1 == i2) return 0
( U- f* X. y3 C: g! S alph1 = Lagrange multiplier for i1' H6 h- ~4 }: y+ i1 f
+ s( x! \" e& k3 q7 M" v! f
y1 = target[i1]
* |8 M; v9 h' p4 Y3 a7 p
( X4 |# u2 n1 _' N" VE1 = SVM output on point[i1] – y1 (check in error cache)
4 q. \6 I# _2 H6 n/ h( Q/ v" }$ g5 T i
s = y1*y2
' D/ a8 Z% j; T: e3 d% r5 l: b, H# g4 i, w# E
Compute L, H via equations (13) and (14)
! H' ?& U8 X: ]! g
% y3 p+ m8 e% Lif (L == H)
( t7 M$ ], |1 K ^/ d7 a7 N" n" |
# k3 h/ F& k' P0 D. Qreturn 02 N. {1 `5 q. i5 J9 @
7 y$ Y8 W6 ~( y
k11 = kernel(point[i1],point[i1])$ J3 W) C+ x" J# i6 [1 y
/ t3 N+ u/ T8 D
k12 = kernel(point[i1],point[i2])8 p* Q- o* J. J; \5 F
& e7 D0 ]+ A3 Y- E* Z, ]6 Xk22 = kernel(point[i2],point[i2]): y! ]; }$ e, ^, g6 ^7 g( o& Y
% @8 [) S+ s& O3 y; r
eta = k11+k22-2*k12
3 i& x/ T7 o j: A& C6 s. s, B, D& A% a: }& N3 F
if (eta > 0)3 o% H/ D, @1 a$ l5 ]) `
: I7 a! r" _' Y5 @
{! Y2 ~6 Q; T' Z# ]- r! [
4 A: k/ `) U# ya2 = alph2 + y2*(E1-E2)/eta
/ ]! l) u: n/ t. @ Z+ d2 `6 g- C2 {& m* ?! n# C
if (a2 < L) a2 = L
; m! X3 V7 I$ P- M9 r, J/ ~; ]
" d7 ^6 {. @8 F( Uelse if (a2 > H) a2 = H3 v* }& @" G8 ^) }; x; z$ ~: m$ Q
- }8 y- ]! i% t% v, j7 W% C
}
4 S4 v8 a0 }% W! O ^
0 A" Q' A2 m# I8 }5 Q3 v- x/ Felse
) u2 {& s O& s$ G' C+ K9 P6 g% D, R2 D+ r
{# k0 }- m. c2 Q
- K2 u' }! q6 s" p! c7 S0 C' v) HLobj = objective function at a2=L
# f2 N/ K a; M* b0 M9 Y4 j
$ n3 p2 E, G- L6 m0 s$ o! HHobj = objective function at a2=H
8 u( Z' M$ r# \( y5 X
# e0 d5 O3 P7 f, s! D% nif (Lobj < Hobj-eps)
8 r2 }, d' {# C0 v9 s/ q% Z; T0 ]# Z x5 W
a2 = L
; h- V: A" |# O& F% [& a3 @, _- C" S( l# P1 C& a3 o
else if (Lobj > Hobj+eps)* W0 c8 M3 z3 y) i/ e
: A! Y9 r, _& C7 f8 x7 ]9 B
a2 = H
9 l) g+ J2 m! |) }' g8 [9 a
1 }) b2 n: ^7 s, u" c! I* D& W7 h4 H. lelse7 o: [" Q U# `! `
6 `8 E) N# K! l( s; m* i
a2 = alph2- i. U: ^! o3 ~! U g
5 l9 P' X( Z. p3 k7 p. a}5 {: ^) m: a7 i; ]
. X% c% A0 m' E$ [. ~
if (|a2-alph2| < eps*(a2+alph2+eps))
# j( e! W Z) Z: |* t, ]2 O
l2 T& M3 J1 jreturn 0
7 q5 I- G: |, X' c5 X5 L' T6 l: y$ S! a, O4 W ]& I A
a1 = alph1+s*(alph2-a2)
* W- k2 E& }0 V: d- R- u. V$ o3 R# F' w8 F. }5 V) F" ?
Update threshold to reflect change in Lagrange multipliers! j/ D# O6 H* |% _
: C( k {6 Y! ]2 c5 l" bUpdate weight vector to reflect change in a1 & a2, if SVM is linear
6 h# a0 Y/ t# B$ R% n: k' f1 m5 A' e) c. |8 K# \ G
Update error cache using new Lagrange multipliers$ p) R' H# q. k9 E# e$ Q d: O
% j; }0 w/ O" s& C6 W# b
Store a1 in the alpha array
5 I; q7 r# e, Z, ^$ d8 d9 f4 `3 j4 z
Store a2 in the alpha array' }/ y+ Q5 v& Y) e9 y0 E* V* V
) i' _1 K N9 k# z, p
return 1# `2 k: m# `8 k' \- W: [
endprocedure; `* v5 B7 E' G( k" Q K
核心代码很紧凑,就是给定两个 ai, aj,然后迭代出新的 ai, aj 出来,还有一层循环会不停的选择最需要被优化的系数 ai, aj,然后调用这个函数。如何更新权重和 b 变量(threshold)文章里面都有说,再多调试一下,可以用 python 先调试,再换成 C/C++,保证得到一个正确可用的 SVM 程序,这是后面的基础。
1 o+ _- W( s- Z4 _% K第二步:实现核函数缓存. X! M: H0 D7 u$ n8 b
观察下上面的伪代码,开销最大的就是计算核函数 K(xi, xj),有些计算又反复用到,一个 100 个样本的数据集求解,假设总共要调用核函数 20 万次,但是 xi, xj 的组和只有 100x100=1 万种,有缓存的话你的效率可以提升 20 倍。' ^ ^) f. n+ y, G( f- z% C
样本太大时,如果你想存储所有核函数的组和,需要 N*N * sizeof(double) 的空间,如果训练集有 10 万个样本,那么需要 76 GB 的内存,显然是不可能实现的,所以核函数缓存是一个有限空间的 LRU 缓存,SVM 的 SMO 求解过程中其实会反复用到特定的几个有限的核函数求解,所以命中率不用担心。
6 p* z8 L% y! C有了这个核函数缓存,你的 SVM 求解程序能瞬间快几十倍。; K8 V& K# H$ K+ ?; \# r% A
第三步:优化误差值求解6 i5 V0 w* v. x) T5 y& v* K
注意看上面的伪代码,里面需要计算一个估计值和真实值的误差 Ei 和 Ej,他们的求解方法是:, a& N3 N# R6 e- w% `0 d4 u
E(i) = f(xi) - yi
, `/ a0 E$ a9 h: r5 a/ |这就是目前为止 SMO 这段为代码里代价最高的函数,因为回顾下上面的公式,计算一遍 f(x) 需要 for 循环做乘法加法。' Y4 ]' D+ N/ p$ X0 s
platt 的文章建议是做一个 E 函数的缓存,方便后面选择 i, j 时比较,我看到很多入门版本 SVM 实现都是这么做。其实这是有问题的,后面我们会说到。最好的方式是定义一个 g(x) 令其等于: j. \* H/ k9 ~$ _' M3 Z5 b
; p4 C& R5 y( A" c也就是 f(x) 公式除了 b 以外前面的最费时的计算,那么我们随时可以计算误差: s8 p' G0 ]) v6 V* H1 l
E(j) = g(xj) + b - yj
9 \3 R2 `3 N X所以最好的办法是对 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 的偏导数就行了,具体代码类似:
2 m* w+ p, ]5 ] p% ?& I% fdouble Kik = kernel(i, k);
# J4 X0 B; k% Pdouble Kjk = kernel(j, k); x' d+ z+ \& e: R
G[k] += delta_alpha_i * Kik * y + delta_alpha_j * Kjk * y[j];
5 O) q2 q( s0 q: B- K0 x: B5 Z4 c把这段代码放在 takeStep 后面,每次成功更新一对 ai, aj 以后,更新所有样本对应的 g(x) 缓存,这样通过每次迭代更新 g(x) 避免了大量的重复计算。1 i$ [# o3 f, v- `
这其实是很直白的一种优化方式,我查了一下,有人专门发论文就讲了个类似的方法。+ i& g- A9 M1 J! P9 @
第四步:实现冷热数据分离
. ^# a- j- o( I3 b; ~) yPlatt 的文章里也证明过一旦某个 alpha 出于边界(0 或者 C)的时候,就很不容易变动,而且伪代码也是优先在工作集里寻找 > 0 and < C 的 alpha 值进行优化,找不到了,再对工作集整体的 alpha 值进行迭代。
. @( p$ I$ I4 M ]: m那么我们势必就可以把工作集分成两个部分,热数据在前(大于 0 小于 C 的 alpha 值),冷数据在后(小于等于 0 或者大于等于 C 的 alpha)。
7 T# U! f9 \9 a随着迭代加深,会发现大部分时候只需要在热数据里求解,并且热数据的大小会逐步不停的收缩,所以区分了冷热以后 SVM 大部分都在针对有限的热数据迭代,偶尔不行了,再全部迭代一次,然后又回到冷热迭代,性能又能提高不少。
& y" u7 P7 E+ w: R2 y第五步:支持 Ensemble% F3 K% A1 k: O8 n# n
大家都知道,通过 Ensemble 可以让多个不同的弱模型组和成一个强模型,而传统 SVM 实现并不能适应一些类似 AdaBoost 的集成方法,所以我们需要做一些改动。可以让外面针对某一个分类传入一个“权重”过来,修正 SVM 的识别结果。) j; i/ ~2 [$ `# [! z
最传统的修改方式就是将不等式约束 C 分为 Cp 和 Cn 两个针对 +1 分类的 C 及针对 -1 分类的 C。修改方式是直接用原始的 C 乘以各自分类的权重,得到 Cp 和 Cn,然后迭代时,不同的样本根据它的 y 值符号,用不同的 C 值带入计算。' @; t a* P- I2 l) _' M6 g
这样 SVM 就能用各种集成方法同其他模型一起组成更为强大精准的模型了。
5 b v1 [+ k( e实现到这一步你就得到了功能上和性能上同 libsvm 类似的东西,接下来我们继续优化。
$ y R& Q8 l0 U/ w第六步:继续优化核函数计算) j j6 U! k$ `
核函数缓存非常消耗内存,libsvm 数学上已经没得挑了,但是工程方面还有很大改进余地,比如它的核缓存实现。. j" l* l# t2 Y$ @- c4 u
由于标准 SVM 核函数用的是两个高维矢量的内积,根据内积的几个条件,SVM 的核函数又是一个正定核,即 K(xi, xj) = K(xj, xi),那么我们同样的内存还能再多存一倍的核函数,性能又能有所提升。
3 P8 D0 p) ]8 j( Z1 Y针对核函数的计算和存储有很多优化方式,比如有人对 NxN 的核函数矩阵进行采样,只计算有限的几个核函数,然后通过插值的方式求解出中间的值。还有人用 float 存储核函数值,又降低了一倍空间需求。
* Y1 {* r. ?% L第七步:支持稀疏向量和非稀疏向量2 b+ F( J+ X! m4 }* y, i; S# F
对于高维样本,比如文字这些,可能有上千维,每个样本的非零特征可能就那么几个,所以稀疏向量会比较高效,libsvm 也是用的稀疏向量。
, V) P& ] }1 E7 l8 S& B& x但是还有很多时候样本是密集向量,比如一共 200 个特征,大部分样本都有 100个以上的非零特征,用稀疏向量存储的话就非常低效了,openCV 的 SVM 实现就是非稀疏向量。3 M7 z! [4 S3 _9 G
非稀疏向量直接是用数组保存样本每个特征的值,在工程方面就有很多优化方式了,比如用的最多的求核函数的时候,直接上 SIMD 指令或者 CUDA,就能获得更好的计算性能。
; }) `" Q9 ?" I4 L2 P3 A所以最好的方式是同时支持稀疏和非稀疏,兼顾时间和空间效率,对不同的数据选择最适合的方式。, w: O9 U6 y# ~' c& j
第八步:针对线性核进行优化
: B3 \, C/ g6 [$ X9 g8 U: L传统的 SMO 方法,是 SVM 的通用求解方法,然而针对线性核,就是:
' i* G) c: b$ }K(xi, xj) = xi . xj2 X: g# p6 q5 f: O' Q
还有很多更高效的求解思路,比如 Pegasos 算法就用了一种类似随机梯度下降的方法,快速求 SVM 的解权重 w,如果你的样本适合线性核,使用一些针对性的非 SMO 算法可以极大的优化 SVM 求解,并且能处理更加庞大的数据集,LIBLINEAR 就是做这件事情的。; k7 h# X) M1 @% y
同时这类算法也适合 online 训练和并行训练,可以逐步更新增量训练新的样本,还可以用到多核和分布式计算来训练模型,这是 SMO 算法做不到的地方。2 \: C2 n% x" Y5 }
但是如果碰到非线性核,权重 w 处于高维核空间里(有可能无限维),你没法梯度下降迭代 w,并且 pegasos 的 pdf 里面也没有提到如何用到非线性核上,LIBLINEAR 也没有办法处理非线性核。4 y& f8 I5 L0 T" [4 R
或许哪天出个数学家又找到一种更好的方法,可以用类似 pegasos 的方式求解非线性核,那么 SVM 就能有比较大的进展了。
+ F1 }- h5 M8 f% ^2 O后话
' e$ Y% I1 Q1 \上面八条,你如果实现前三条,基本就能深入理解 SVM 的原理了,如果实现一大半,就可以得到一个类似 libsvm 的东西,全部实现,你就能得到一个比 libsvm 更好用的 SVM 库了。/ M4 b9 d) ~$ }) ~$ j. O. V
上面就是如何实现一个相对成熟的 SVM 模型的思路,以及配套优化方法,再往后还有兴趣,可以接着实现支持向量回归,也是一个很有用的东西。
' |% U; d: A' B; B3 m2 X& x }1 R$ I7 }: M7 f9 F2 v. }
来源:http://www.yidianzixun.com/article/0Lv0UIiC
+ h; r) K- N8 Y! O% t8 _免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|