Support Vector Machine#

マージン#

データがクラス{C1,C2}のどちらに含まれるかを判断する2クラス識別問題について考える。 教師ラベルはy{+1,1}であり、それぞれデータがC1,C2のどちらに含まれるかを示すとする。 係数ベクトルをw=(w1,...,wd)T、バイアス項をb、特徴量ベクトルをx=(x1,...,xd)Tとおくと、線形識別関数は

f(x)=wTx+b

と表すことができる。

識別境界(識別超平面)はf(x)=0となる位置に描かれるとし、クラス1をf(x)>=0、クラス2をf(x)<0で表現するように学習させるとする。例えば次の図のように、ある識別関数が存在したとする。

Hide code cell source
# 2次元に描いた場合
import numpy as np
import matplotlib.pyplot as plt

# このようなデータがあったとする
x_c1 = np.array([
    [.50, .55],
    [.45, .75],
    [.7, .50],
    [.7, .75],
])
x_c2 = - x_c1
X = np.append(x_c1, x_c2, axis=0)
y = np.array([1] * x_c1.shape[0] + [-1] * x_c2.shape[0])

# 重みベクトルが仮にこのようなwだったとする
w = np.array([1, 1])

# 描画範囲
x1 = np.linspace(-1, 1, 100)

def plot_hyperplane(X, y, w, ax, x1=x1):
    # データ点の散布図
    is_1 = y == 1
    ax.scatter(X[is_1, 0], X[is_1, 1], label="C1", color="orange")
    ax.scatter(X[~is_1, 0], X[~is_1, 1], label="C2", color="steelblue")

    # x軸, y軸を描く
    ax.hlines(0, -1, 1, colors="black", linewidth=1)
    ax.vlines(0, -1.1, 1.1, colors="black", linewidth=1)

    # 超平面:x2 = -(w1/w2) * x1の形にする
    # x1 = np.linspace(-1, 1, 100)
    x2 = -(w[0] / w[1]) * x1
    ax.plot(x1, x2, color="dimgray", label="f(x)=w'x")

    ax.legend(loc="upper left")
    return ax

fig, ax = plt.subplots()
plot_hyperplane(X, y, w, ax)
fig.show()
../_images/f6bad3767aec588c5bdc29e959d9e484f8022fb6aa761953397faf1a8c16232e.png

訓練データ中に存在しなかったノイズがテストデータに含まれていた場合、ノイズの分だけ識別を誤りやすくなる。 しかし、訓練データの点が識別超平面からある値h>0よりも離れるように学習させれば、hより小さなノイズに対しては正しく識別できるようになる。

例えば、以下の図の(a)と(b)はいずれもサンプルをうまく分離できているものの、(a)よりも(b)のほうがデータ点と識別超平面の距離があり、ノイズに対してより頑健で望ましい分類器であると考えられる。

Hide code cell source
fig, axes = plt.subplots(ncols=2, figsize=(12, 3))

w_ = np.array([-3, 5])
plot_hyperplane(X, y, w_, axes[0])
axes[0].set(title="(a)")

w_ = np.array([1, 2])
plot_hyperplane(X, y, w_, axes[1])
axes[1].set(title="(b)")

fig.show()
../_images/bf7a0a9b7e410acf9ad69aa4dd16cdba554cde7c76e2d9fd2d792250c2d35f3b.png

となれば、 「識別超平面が訓練データからもっとも離れるように(両クラスの中間になるように)学習させればよいのではないか」 という考えが湧く。

これがサポートベクターマシン(Support Vector Machine: SVM)の考え方である。

識別境界f(x)=0と最も近い各クラスの訓練データの点を サポートベクトル (support vector)といい、サポートベクトルと識別境界との距離(識別境界と最も近いデータ点の距離)を マージン (margin)という。

ある識別関数に対してとれるマージンの大きさは、両クラスの学習データを識別関数の法線ベクトル上に射影した長さの最小値

ρ(w)=minxC1wTx||w||maxxC2wTx||w||

の半分である。ρ(w)はクラス間マージンという。次の図中の2つの破線の間の距離がρ(w)である

Hide code cell source
fig, ax = plt.subplots()
plot_hyperplane(X, y, w, ax)

# 識別関数 f(x)=w'x の下での、class={C1, C2}のどちらに属するかについての判定結果
outputs = [w.T @ X[i, :] for i in range(X.shape[0])]
is_class_1 = [o >= 0 for o in outputs]
is_class_2 = np.invert(is_class_1)

# マージンとその合計ρ
margin1 = min([(w.T @ X[is_class_1, :][i, :] / np.linalg.norm(w)) for i in range(X[is_class_1, :].shape[0])])
margin2 = max([(w.T @ X[is_class_2, :][i, :] / np.linalg.norm(w)) for i in range(X[is_class_2, :].shape[0])])
rho = margin1 - margin2

# margins
m1 = -(w[0] / w[1]) * x1 + margin1
ax.plot(x1, m1, color="orange", linestyle="--")
m2 = -(w[0] / w[1]) * x1 + margin2
ax.plot(x1, m2, color="steelblue", linestyle="--")

ax.legend(loc="upper left")
ax.set(ylim=(-1,1), xlim=(-1,1))
fig.show()
../_images/ca0c8a0b89dada709a9a1fbef4ce64720d9985235e3fc44ef2613cdcfac64ab4.png

ハードマージンSVM#

学習データの集合をDL={(yi,xi)}(i=1,...,N)とする。係数ベクトルはバイアス項bを外に出す形で、w=(w1,...,wd)Tと表記する。特徴量ベクトルはx=(x1,...,xd)Tである。yi={1,+1}は教師データで、学習データxiRdがどちらのクラスに属するかを示す。

線形識別関数のマージンをκとすれば全ての学習データで

|wTxi+b|κ

が成り立つ。

係数ベクトルとバイアス項をマージンで正規化(wTxi=bを定数倍)したものをあらためてw,bとおけば

{wTxi+b+1ifyi=+1wTxi+b1ifyi=1

となり、まとめて表記すると

yi×(wTxi+b)1

クラス間マージンは $ρ(w,b)=minxCy=+1wTx||w||maxxCy=1wTx||w||$

第1項の分子はwTxi+b+1の最小値がwTxi+b=1であることからminwTxi=1b

第2項の分子はwTxi+b1の最大値がwTxi+b=1であることからmaxwTxi=1b

であることを使えば

ρ(w,b)=minxCy=+1wTx||w||maxxCy=1wTx||w||=1b||w||1b||w||=1+1b+b||w||=2||w||

となる。

識別関数の最大マージンは最大クラス間マージンの半分であるため、1||w||となる。

最適識別超平面#

最適な識別超平面は、「すべての訓練データを正しく識別できる」という制約条件

yi(wTxi+b)1(i=1,...,N)

の下でマージン1wを最大化した解として得られる。 マージンの最大化はwの最小化と等しいため、

w0=minw

として求めることができる。これは次の不等式制約条件つき最適化問題を解くことで得られる。

主問題

minimizeLp(w)=12wTwsubject toyi(wTxi+b)1; i

この問題はラグランジュの未定乗数法を用いて解かれ、次のラグランジュ関数として定式化される

L~p(w,b,α)=12wTwi=1Nαi(yi(wTxi+b)1)

ここでα=(α1,...,αN)Tαi0であり、αiはラグランジュ未定乗数と呼ばれる。

この最適化問題の解wbは以下のKKT(Karush-Kuhn-Tucker)条件を満たす解として知られている。

KKT条件

(1) L~p(w,b,α)w|w=w=wi=1Nαiyixi=0

(2) L~p(w,b,α)b=i=1Nαiyi=0

(3) yi(wTxi+b)10

(4) αi0

(5) αi(yi(wTxi+b)1)=0

ラグランジュ関数のwwに置き換えてKKT条件(1)と(2)を代入して整理すると

Ld(α)=12wTwi=1NαiyiwTxibi=1Nαiyi+i=1Nαi=i=1Nαi12wTw(i=1Nαiyi=0)=i=1Nαi12i=1Nj=1NαiαjyiyjxiTxj

となり、ラグランジュ未定乗数のみの関数にすることができる

KKT条件(1)より最適解はw=i=1Nαiyixiのようになることがわかっているので、最適な係数αiを求める問題に置き換えることができる。

双対問題

maximizeLd(α)=αT112αTHαsubject toαTy=0

ここで

1=(1,...,1)TH=(Hij=yiyjxiTxj)y=(y1,...,yN)T

である。

双対問題のラグランジュ関数は、ラグランジュ未定乗数をβとすれば次の関数になる。

L~d(α,β)=αT112αTHαβαTy

KKT条件(5)よりαi(yi(wTxi+b)1)=0がすべてのiで成り立てば良いため、

{αi>0ifyi(wTxi+b)1=0αi=0ifyi(wTxi+b)10

となる。αi>0となるxiサポートベクトルという。

最適なバイアスbはサポートベクトルの一つxsを用いて

ys(wTxs+b)1=0

を解いて求めるか、それらの平均を用いる。

実装例(cvxpy)#

主問題をそのままソルバーに通すパターン

minw12wTw=12w22s.t.yi(wTxi+b)1; i
import cvxpy as cp
n = X.shape[0]
d = X.shape[1]

b = cp.Variable()
w = cp.Variable(d)
prob = cp.Problem(cp.Minimize( (1/2) * cp.norm(w, 2)**2 ),
                  [y[i] * (w.T @ X[i, :] + b) >= 1 for i in range(n)])
prob.solve()
print("The optimal value is", prob.value)
print("w is", w.value)
print("b is", b.value)
The optimal value is 0.9049773766503894
w is [0.90497738 0.99547511]
b is -2.2003337529347888e-17
Hide code cell source
w = w.value

fig, ax = plt.subplots()
plot_hyperplane(X, y, w, ax)
fig.show()
../_images/171061485513c62b57c19f4df8dbed3ab69225ee4520d3e95345b5d19205232e.png

双対問題をソルバーに通すパターン#

双対問題

maximizeLd(α)=αT112αTHαsubject toαTy=0

cvxpyの二次計画問題のソルバーを使って解いてみる

# データ数が多くなるとソルバーが上手く動かないので一旦暫定的対処としてデータ数を絞る
y = y[3:6]
X = X[3:6]
# Hを作成
n = X.shape[0]
H = np.zeros(shape=(n, n))
for i in range(n):
    for j in range(n):
        H[i, j] = y[i] * y[i] * X[i] @ X[j]

ones = np.ones(shape=(n, ))
import cvxpy as cp

alpha = cp.Variable(n)  # 長さnのベクトル
prob = cp.Problem(cp.Maximize( alpha.T @ ones - (1/2) * cp.quad_form(alpha, H) ),
                  [alpha.T @ y == 0])
prob.solve()
print("The optimal value is", prob.value)
print("alpha is", alpha.value)

a = alpha.value
w = sum([a[i] * y[i] * X[i] for i in range(n)])
print(f"w={w}")
The optimal value is 33.99999999999996
alpha is [34. 10. 24.]
w=[39.6 49. ]
Hide code cell source
fig, ax = plt.subplots()
plot_hyperplane(X, y, w, ax)
fig.show()
../_images/586c13cd50d14b9d7ddf618184ff1d15a34290a6005b06b15f74be44eb02eaf2.png

実装例(scikit-learn)#

from sklearn import svm
clf = svm.SVC(random_state=0, kernel='linear', shrinking=False)
clf.fit(X, y)

print(f"b={clf.intercept_}, w={clf.coef_}")
b=[-0.15974441], w=[[0.76677316 0.83067093]]
Hide code cell source
from sklearn.inspection import DecisionBoundaryDisplay
fig, ax = plt.subplots()
disp = DecisionBoundaryDisplay.from_estimator(
    clf,
    X,
    response_method="predict",
    cmap=plt.cm.coolwarm,
    alpha=0.8,
    ax=ax,
    xlabel="x1",
    ylabel="x2",
)
ax.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.coolwarm, s=20, edgecolors="k")
ax.set(title="Decision Boundary of SVM")
fig.show()
../_images/aa7bdd67ae87ecc0432f5dfef66748a8a4ed6e8238452129f276ff986ffaf980.png

ソフトマージンSVM#

C-SVM#

スラック変数と呼ばれる変数ξiを追加する。

{ξi=0()0<ξi1()ξi>1()

以下のように書くこともできる

ξi=max[0,1yi(wTxi+b)]=f+(1yi(wTxi+b))

ここでf+(x)はヒンジ(hinge)関数と呼ばれるもので

f+(x):={x(x>0)0()

である

ソフトマージン識別器の主問題は以下のように定式化される。

主問題

minimizeLp(w,ξ)=12wTw+Ci=1Nξisubject toyi(wTxi+b)1+ξi0ξi0

すべての訓練データのスラック変数の和ξi(ξi0)は誤識別数の上限を与える。 パラメータCは誤識別数に対するペナルティの強さであり、Cが大きいほどwのノルム最小化よりも誤識別数を小さくする方を優先することになる。

このSVMはC-SVMと呼ばれる。

ν-SVM#

上限サポートベクトル(マージン誤りξi>0のベクトルの数)の割合の上限を規定するハイパーパラメータνが指定できるようになった

カーネルトリック#

カーネルモデル#

線形モデルをカーネルモデルに拡張することを考える。

線形モデル
flinear(xi)=wTxi+b
  • xi=(xi1,,xiD)

  • w=(x1,,xD)T

カーネルモデル
fkernel(xi)=j=1Nwjϕ(xi)Tϕ(xj)+b
  • xi=(xi1,,xiD)

  • w=(x1,,xN)T

ここでϕ()は任意の関数で、ϕ()によって入力ベクトルxを高次元空間に写像し(2次元では線形分離不可能なものを3次元に写して線形分離不可にするイメージ)、高次元空間上の類似度を内積ϕ(xi)Tϕ(x)で表す。

カーネルモデルはNの和が入っているように、訓練データ数Nが増えるとモデルの表現力は高まるが計算量が増える。

カーネルトリック#

ϕ(x)上での内積計算を緩和するために 正定値関数 (positive definite function)を用いる。

正定値関数

関数 k(xi,xj) は次の条件を満たすとき正定値関数と呼ばれる。

(1) 対称性:k(xi,xj)=k(xj,xi)

(2) 正定値性:デー夕点 x1,x2,,xN に対する以下の グラム行列 (Gram matrix) K が半正定値である。

K=(k(x1,x1)k(x1,x2)k(x1,xN)k(x2,x1)k(x2,x2)k(x2,xN)k(xN,x1)k(xN,x2)k(xN,xN))

すなわち、任意の N 次元のベクトル z に対し zKz0 が成り立つ。

正定値関数 k(xi,xj)再生核ヒルベルト空間 Hkへの写像 ϕ(xi),ϕ(xj)Hkの内積に対応する。

k(xi,xj)=ϕ(xi)Tϕ(xj)

これを用いて高次元空間上での内積をより単純な2変数関数の計算k(,)に置き換える事ができる。この正定値関数k(,)のことを カーネル関数 (kernel function)、グラム行列Kカーネル行列 (kernel matrix) と呼ぶ。

カーネル関数とカーネル行列を用いると、カーネルモデルは以下のように表現できる

カーネル行列を用いたカーネルモデル
fkernel(xi)=j=1Nwjk(xi,xj)+b=Ki:w+b

ここでKi:はカーネル行列のi行目の行ベクトルを表す。

カーネル関数の例#

線形カーネル(linear kernel)
k(xi,xj)=xixjT

入力をそのまま出力する写像関数ϕ(x)=xTに対応する

ガウスカーネル(Gaussian kernel)
k(xi,xj)=exp(xixj22σ2)

カーネルモデルを用いたソフトマージン最大化問題(主問題)

minw,b,ξLp(w,ξ)=12wTw+Ci=1Nξis. t.yi(Ki:w+b)1+ξi0ξi0 i

カーネルモデルを用いたソフトマージン最大化のラグランジュ双対問題

maxλL(λ,γ,w,b,ξ)=12i=1Nj=1Nλiyik(xi,xj)tjλj+i=1Nλi s.t. λi0λiCi=1Nλiyi=0i
# TODO: コードかく

参考#

  • 八谷大岳. (2020). ゼロからつくるPython機械学習プログラミング入門.

# TODO
from sklearn import datasets
X, y = datasets.load_iris(return_X_y=True, as_frame=False)
X = X[:, 0:2]

fig, ax = plt.subplots()
for c, label in enumerate(['setosa', 'versicolor', 'virginica']):
    is_c = y == c
    ax.scatter(X[is_c, 0], X[is_c, 1], label=label)
ax.legend()
<matplotlib.legend.Legend at 0x7f08e7443760>
../_images/6ddf228690434730b2cae9cd784aa114668de7edb162dd48f44884d98388e23a.png
# setosaとvirginicaを1つにまとめて2クラスにする
y[y == 2] = 0


fig, ax = plt.subplots()
for c, label in enumerate(['setosa', 'versicolor', 'virginica']):
    is_c = y == c
    ax.scatter(X[is_c, 0], X[is_c, 1], label=label)
ax.legend()
<matplotlib.legend.Legend at 0x7f08e7148490>
../_images/9e475804d11fddbcd33156b0badced825e4ec29eacc766025ea581dd6290a710.png

GaussianKernel (RBF kernel)#

一般的には $k(x,x)=exp(xx22σ2)$

David Duvenaud (2014). “The Kernel Cookbook: Advice on Covariance functions”.

x1 = np.array([[1], [2]])
x2 = np.array([[3], [4]])
x1
array([[1],
       [2]])
def rbf_kernel(x1, x2, sigma=1.0) -> float:
    return - np.linalg.norm(x1 - x2)**2 / (2 * sigma**2)
rbf_kernel(x1, x2)
-4.000000000000001
class GaussianKernel:

    def fit(self, X):
        self.alpha = 0.001
        self.mu = np.mean(X, axis=0)
        self.sigma = np.cov(X, rowvar=False, bias=False)

    def transform(self, X) -> np.array:
        return np.array([self._transform(x) for x in X])

    def _transform(self, x) -> float:
        return np.exp( - self.alpha * (x - self.mu) @ np.linalg.inv(self.sigma) @ (x - self.mu) )
    
    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} alpha={self.alpha}, mu={self.mu}, sigma={self.sigma}>"

Memo 4#

RBFカーネルが無限次元になるのは指数関数の冪級数による定義が無限和であるため

exp(x)=n=01n!xn

ref: https://ja.wikipedia.org/wiki/指数関数#厳密な定義a

TODO: もっと図とか入れたい

9. サポートベクトルマシン — 機械学習帳

のように

Support vector machine classifier with (\ell_1)-regularization — CVXPY 1.3 documentation

はじパタ全力解説: 第8章 サポートベクトルマシン - Qiita