サポートベクターマシンについて
サポートベクターマシン(SVM)について書きたいと思います。最初にサポートベクターマシンのざっくりとした理論について述べて、そのあとに実装について書きたいと思います。
線形SVM
与えられた線形分離可能なデータを2つのクラスに分けることを考えます。このとき、識別平面の引き方は割と遊びがあります。例えば、以下のようなデータを2クラスに分けるときには
色々な引き方が考えられます。この中で、最適な引き方を考えます。
マージンという距離を定義します。これは2つのクラスのうち、異なるクラスに最も近いデータ同士の距離のことみたいなイメージです。このマージンを最大にするような識別平面を定義することで最適な引き方とします。
マージンの定義とか識別平面とかについて
重みwとバイアスbを用いて識別平面を以下のように定義します。
このとき、マージンを以下の2つの平面の距離とします。この2つの平面は、分類されるクラスの端っこの点を通る平面を表しています。
すなわち、マージンはで表されます。(点と平面の距離とか久しく使ってないので忘れてた)
よって以下の条件を満たしながらマージンを最大化(要するにを最小化)すれば良いことになります。yはデータのクラスラベルです。(はてなブログでのcasesの使い方がいまいちわからん)
これはにまとめられるので、結果としてという条件下でを最小化する問題とすることができます。
この問題はラグランジュ未定乗数によって以下のような双対形式に変形して解くことができます。(ここではではなくを最小化することを考えます)ここの変形の妥当性をあんまりしっかり理解していないのごめんなさい...
(このLをについて最大化し、について最小化する)
また、この式には以下の条件が付随します。
より, よりが成立する必要があります。
したがって、元々の問題は以下のLを最大化する変形することができます。(はてなブログってalign環境使えないんです?)
subject to
これを二次計画問題として解きます。(二次計画問題を解くところはライブラリにぶん投げたので省きます。理解してないし)
プログラムによる実装
要するに二次計画問題にぶっ込めばいいということになります。cvxoptを使っています。
線形SVMを二次計画問題として解釈してcvxoptに突っ込むとこんな感じになります。
h=x*l qpP = cvxopt.matrix(h.T.dot(h)) qpq = cvxopt.matrix(-np.ones(n), (n, 1)) qpG = cvxopt.matrix(-np.eye(n)) qph = cvxopt.matrix(np.zeros(n), (n, 1)) qpA = cvxopt.matrix(l.astype(float), (1, n)) qpb = cvxopt.matrix(0.) cvxopt.solvers.options['abstol'] = 1e-5 cvxopt.solvers.options['reltol'] = 1e-10 cvxopt.solvers.options['show_progress'] = False res=cvxopt.solvers.qp(qpP, qpq, qpG, qph, qpA, qpb) alpha = np.reshape(np.array(res['x']),-1)
xはデータ、lはラベル、nはデータ数です。
ソフトマージン
線形SVMは与えられたデータが線形分離可能という割ときつい制約がありました。これをある程度解決するのがソフトマージンです。
ソフトマージンはを満たせないデータを許容する代わりに各データにペナルティとしてスラック変数を以下のように与えます。
が0のときは線形SVMと同様にマージン内で正しく識別できていることを表します。0以上1以下の場合はマージンの境界を超えてしまうものの、識別平面を踏み越えているわけではないので(識別平面は=0の式)正しく識別できていることを表します。1以上になってしまった場合は識別平面を踏み越えて誤識別されていることを表します。
線形SVMのときにはを最小にすることを考えましたが、スラック変数を導入したことで最小化する関数は以下のように変化します。
subject to
Cはペナルティであるをどれだけ重く見るかを表す定数です。重く見れば見るほど線形SVMライクになるということであってると思います。
これを線形SVMのときと同じようにラグランジュ未定乗数とかでごにょごにょすると、以下の条件でLを最大化する問題に変形することができます。
subject to
実際のところ、ソフトマージンを導入したことによる差はの条件が追加されることになります。
プログラムによる実装
線形SVMのときの二次計画問題の条件にが追加されるだけなので以下のように計算すればいいです。
h=x*l C = 1.0 qpP = cvxopt.matrix(h.T.dot(h)) qpq = cvxopt.matrix(-np.ones(n), (n, 1)) qpG = cvxopt.matrix(np.vstack((-np.eye(n), np.eye(n)))) qph = cvxopt.matrix(np.hstack((np.zeros(n), C*np.ones(n))), (2*n, 1)) qpA = cvxopt.matrix(l.astype(float), (1, n)) qpb = cvxopt.matrix(0.) cvxopt.solvers.options['abstol'] = 1e-5 cvxopt.solvers.options['reltol'] = 1e-10 cvxopt.solvers.options['show_progress'] = False res=cvxopt.solvers.qp(qpP, qpq, qpG, qph, qpA, qpb) alpha = np.reshape(np.array(res['x']),-1)
xはデータ、lはラベル、nはデータ数です。不等式の条件の部分で配列を結合しています。
カーネルトリック
ソフトマージンとかではもうどうしようもないほど線形分離不可能だけど分離自体は可能である場合に対応する方法を考えます。
その場合に使うのがガウスカーネルsigma2)などのカーネル関数です。(はてなブログのtex記法なんか文字数多くなると機能しなくなる気がする?)これを今まで内積で計算していた部分に代わりに代入することで線形分離不可能なデータにも対応できます。
データをxを単純に非線型変換してから内積を取るとカーネル関数が表す関数への非線形変換は一般的には難しいです。でも、内積をカーネルごと入れ替えることでxの非線形変換に落とし込むことなく変換をすることができます。
プログラムによる実装
今まで内積を適用していた部分をガウスカーネルとかに変えてやれば良いです。カーネル関数はガウスカーネルを使っています。
h=x*l C = 1.0 sigma = 1.0 def gauss_kernel(x, y): return np.exp(-np.linalg.norm(x-y)**2 / sigma) P = np.zeros((n, n)) for i in range(n): for j in range(n): P[i][j] = l[i]*l[j]*gauss_kernel(x.T[i], x.T[j]) qpP = cvxopt.matrix(P) qpq = cvxopt.matrix(-np.ones(n), (n, 1)) qpG = cvxopt.matrix(np.vstack((-np.eye(n), np.eye(n)))) qph = cvxopt.matrix(np.hstack((np.zeros(n), C*np.ones(n))), (2*n, 1)) qpA = cvxopt.matrix(l.astype(float), (1, n)) qpb = cvxopt.matrix(0.) cvxopt.solvers.options['abstol'] = 1e-5 cvxopt.solvers.options['reltol'] = 1e-10 cvxopt.solvers.options['show_progress'] = False res=cvxopt.solvers.qp(qpP, qpq, qpG, qph, qpA, qpb) alpha = np.reshape(np.array(res['x']),-1)
まとめ
以上です。説明省いたり、実装とかほざいて二次計画問題のライブラリを使ってるだけだったりとグダグダですがある程度まとめられたかなとは思います。