学生による学生のためのプログラミング学習サイト

【さきどりPython#7】scikit-learnでサポートベクターマシンの基礎を学ぼう

historoid
historoid

こんにちは。清野(@historoid1)です。

機械学習を勉強していると何かと登場するサポートベクターマシン(SVM)ですが、その基本原理をご存知でしょうか。

SVMについては数学的にも深く研究されており、一見難しく見えてしまうかもしれませんが、基本的な原理を理解するのは簡単です。

今回は、SVMの基本的な原理をscikit-learnで実装しながら学んでいきましょう。

サポートベクターマシンの基本原理

前提となるデータ

まずデータが上記の図のように分布しているとします。

今は説明変数が2つ(xとy)の単純なものを示しています。説明変数とは、身長や体重、血圧などの個々のパラメータのことです。

上の図では、データがなんとなく2つの群に分かれているように見えます。

今はわざと2つに分かれるようにイラストを描いただけなので、実際のデータはこんなにキレイに分かれないこともあります。

線形分離とは

線形分離とは、その名のとおり1本の直線でデータを二分割することです。

直線というのがミソです。直線は$$y=ax+b$$で表すことができます。SVMはこの傾きaと切片bを見つけるモデルといえます。

境界線は1つとは限らない

データを分割する境界線は1本とは限りません。つまり傾きや切片は一意ではないということです。

上の図のように、はじめに示した例とは異なる境界線を引くことができます。

どちらがより適切かは新しいデータが加えられたときにわかります。新しい点が打たれたときに、それでもデータを二分割できていれば良いモデルであるといえます。

ポイント

線形分離のための境界線自体は、複数引くことができます。

しかしSVMでは、境界線がほぼ一意に定まります。小数点の何桁目では多少ズレますが、概ね同じくらいの傾きと切片を得ることができます。

それはマージン最大化というアルゴリズムを使っているためです。マージン最大化については後述します。

非線形分離とは

非線形分離とは、直線では分割できないものを指します。上の図のように、人間の目からすれば明らかにデータが2つの群に分かれていても、線形分離不可能です。

きれいに分離するためには直線ではなく、曲線であれば分離可能です。このように分離することを非線形分離といいます。

サポートベクターとは

サポートベクターマシンの名前にあるサポートベクターとは、境界線を決定する2つの点を指します。

もう一度、上の例を見てみましょう。1本の直線によってデータ群が分割されています。

このとき、境界線に最も近い2つの点をサポートベクターといいます。距離が等しければサポートベクターが2点以上になることもあります。

サポートとは「支持する」という意味で、ベクターとは点のことを指します。

データは説明変数が何個だろうと$$(a_1, b_1, c_1, d_1, \cdots )$$と表されます。つまり行ベクトルの形になるわけです。そのため1つの標本はベクターと呼ばれます。

マージン最大化

サポートベクターマシンではマージン最大化というアルゴリズムを使っています。

マージン最大化とは、境界線とそれに最も近い点との距離を最大化することです。

つまりサポートベクターとの距離を最大化するわけです。そのためSVMで定まる境界線はほぼ一意に定まるのです。

注意

「マージン最大化で境界線はほぼ位置に定まる」と書きましたが、実際にはそこまで単純ではありません

データがキレイに分離していればマージン最大化は可能です。しかし実データは分離していないのがほとんどです。

この場合マージンの最大化だけではなく、分類に失敗する点の最小化も考慮します。

scikit-learnでは分類に失敗した場合のペナルティを指定することもできます。

次元の拡張

さてSVMの真骨頂である次元の拡張について理解しましょう。次元を拡張することによって非線形問題を線形分離できるようになります

まずは線形分離できない例をおさらいします。

このように直線では分割できない分布を考えましょう。

次元の拡張とは、次数を増やして考えるということです。この例は説明変数が2つなので2次元データです。これを3次元にしてみましょう。

単にz軸を加えただけならこんな感じです。これだけでは線形分離できません。

カーネルトリック

では上のイラストのように、赤い点と青い点をz軸方向に分離させます。

すると直線の境界線(次数が増えたので平面)を引くことができるようになります。

このように次元を拡張して、データを分離しやすい状態にすることをカーネルトリックといいます。

historoid
historoid

z軸方向に分離させる……。

そりゃそう移動させれば分離できますけど、あまりに恣意的過ぎませんか?

上で示した例はあくまでもSVMのカーネルトリックを理解しやすくするためのイラストです。単純にz軸方向に移動させるわけではありません。

scikit-learnでの実装

では早速scikit-learnでSVMを実装してみましょう。

モジュールのインポートとデータのロード

# ライブラリをロード
from sklearn.svm import LinearSVC
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
import numpy as np

# データをロード with only two classes and two features
iris = datasets.load_iris()
features = iris.data[:100,:2]
target = iris.target[:100]

まずはいつもどおりライブラリのインポートですね。データセットもおなじみのアイリスです。

特徴量の標準化

# 特徴量を標準化
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)

次に特徴量の標準化を行います。標準化は正規化とも呼ばれますが、分野によって意味が異なります

scikit-learnの場合、平均値を0、分散を1にする操作を指します

重要なことなので繰り返しますが、標準化と正規化は分野によって意味が異なります。異なるライブリを使う場合はその中身をよく確認してください。

データのプロット

# ライブラリをロード
from matplotlib import pyplot as plt

# 観測値をそれぞれの色でプロット
color = ["black" if c == 0 else "lightgrey" for c in target]
plt.scatter(features_standardized[:,0], features_standardized[:,1], c=color)

ではデータをプロットしてみましょう。

データはこのように分布していることがわかりました。

SVMを作成して訓練させる

# サポートベクタクラス分類器を作成
svc = LinearSVC(C=1.0)

# サポートベクタクラス分類器を訓練
model = svc.fit(features_standardized, target)

簡単ですね。LinearSVCというクラスを使っています。SVCはsupport vector classifierの略です。

引数のCは、分類を間違えたときのペナルティの強さを指定します。これが大きいほどモデルはペナルティを受けます。

境界線を描画

ここで訓練したSVCで得られた境界線をを先ほどの散布図に描画してみましょう。

# 観測値をそれぞれの色でプロット
color = ["black" if c == 0 else "lightgrey" for c in target]
plt.scatter(features_standardized[:,0], features_standardized[:,1], c=color)

# 超平面を作成
w = svc.coef_[0]
a = -w[0] / w[1]
xx = np.linspace(-2.5, 2.5)
yy = a * xx - (svc.intercept_[0]) / w[1]

# 超平面をプロット
plt.plot(xx, yy)
plt.show();

すると以下のような結果になります。

うまく分割されていますね。

新しい観測値を追加

# 新たな観測値の作成
new_observation = [[ -2,  3]]

# 新たな観測値のクラスを予測
svc.predict(new_observation)

新しいデータを配列として渡してあげれば、それに対する分類予測を求めることもできます。

非線形分離SVMの実装

ここからはアドバンスなので、読まなくてもOKです。詳しい解説は別の記事で行う予定です。

モジュールのインポートからモデルの訓練まで

基本的にはSVCと同じです。

線形分離できないデータを用いて、あえてそこにSVCを適用させてみましょう。

# ライブラリをロード
from sklearn.svm import SVC
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
import numpy as np

# 乱数シードを設定
np.random.seed(0)

# 2つの特徴量を生成
features = np.random.randn(200, 2)

# XORゲート(ここでは分からなくて良い)を用いて、
# 線形分離不能なデータを作成
target_xor = np.logical_xor(features[:, 0] > 0, features[:, 1] > 0)
target = np.where(target_xor, 0, 1)

可視化

# 観測値と決定境界超平面をプロット
from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt

def plot_decision_regions(X, y, classifier):
    cmap = ListedColormap(("red", "blue"))
    xx1, xx2 = np.meshgrid(np.arange(-3, 3, 0.02), np.arange(-3, 3, 0.02))
    Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
    Z = Z.reshape(xx1.shape)
    plt.contourf(xx1, xx2, Z, alpha=0.1, cmap=cmap)

    for idx, cl in enumerate(np.unique(y)):
        plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1],
                    alpha=0.8, c=cmap(idx),
                    marker="+", label=cl)

##########

# 線形カーネルを用いたサポートベクタクラス分類器を作成
svc_linear = SVC(kernel="linear", random_state=0, C=1)

# クラス分類器を訓練
svc_linear.fit(features, target)

##########

# 観測値と超平面をプロット
plot_decision_regions(features, target, classifier=svc_linear)
plt.axis("off"), plt.show();

可視化します。線形分離できないようにデータを用意してあるので、当然うまくいきません。

赤と青の領域がバラバラですね。

カーネル関数の変更

ここでカーネル関数を変更して、非線形分類を行える状態にしましょう。

# 放射基底関数カーネルを用いたサポートベクタクラス分類器を作成
svc = SVC(kernel="rbf", random_state=0, gamma=1, C=1)

# クラス分類器を訓練
model = svc.fit(features, target)

###########

# 観測値と超平面をプロット
plot_decision_regions(features, target, classifier=svc)
plt.axis("off"), plt.show();

このようにうまく分割できていることがわかります。


カーネル関数についての詳細な解説は今回は行いません。

historoid
historoid

すみません!! 数学的な理解が深まったらまた記事にします。

今回はSVMの基礎を理解できればOKです。より詳細な解説はまた別の機会に行います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です