7  サポートベクターマシン

機械学習において、与えられたデータセットを正確に分類するための適切な決定境界を見つけることが重要です。決定境界とは、分類問題において、異なるクラスを分けるために描かれる境界線や面のことを指します。適切な決定境界を見つけ出すことによって、教師ありの学習モデルは未知の新たなデータに対しても正確な分類を行う能力を獲得します。

サポートベクターマシン(Support Vector Machines: SVM)は、適切な決定境界を見つけるための手法を提供します。サポートベクターマシンによる境界線の探求は、境界線から最も近いデータ点(サポートベクターと呼ばれる)からの距離(マージンと呼ばれる)が最大になるように行われます。マージンの最大化の結果、分類されるデータどうしの距離が最も離れるように境界線が決定されます。また新たなデータが境界の近くに位置する場合でも、適切な分類が可能になります。

サポートベクターマシンははじめ、2クラス分類問題に対して考案され、後に多クラス分類問題にも拡張されました。そのため、ロジスティック回帰モデルと同様に、線形分離可能なデータに対して機能します。一方、ロジスティック回帰モデルでは線形分離不可能なデータに対しては十分に機能しません。サポートベクターマシンではカーネル関数を用いることで、非線形な境界線を描くことも可能です。

7.1 カーネルトリック

サポートベクターマシンが非線形の境界線を見つけられる背景には、カーネルトリックという特殊な機能を利用していることがあります。カーネルトリックとは、データを高次元空間に「投影」することで、直線で分けられない問題に対処する能力です。例えば、直線に分けられない2次元のデータを3次元に投影することで、直線で分けられるようになる場合があります。ただし、非線形変換によって高次元空間に投影されたデータに対して、元の特徴空間での直感的な理解とは一致しないことがある点は注意が必要です。

7.1.1 カーネル関数とハイパーパラメータ

カーネルトリックでは、データを高次元空間に投影する際にカーネル関数と呼ばれる関数を用います。カーネル関数は、2つのベクトル間の類似度を測るための関数であり、その計算は高次元空間での内積を計算することと等価です。サポートベクターマシンで利用されるカーネルトリックでは、以下のようなカーネル関数が用いられます。

  • 線形カーネル: ベクトルの内積を計算。これは基本的にはカーネルトリックを使用せずにSVMを適用するのと同等
  • 非線形カーネル
    • 多項式カーネル
    • RBFカーネル(ガウシアンカーネル、Radial Basis Function kernel)

RBFカーネルは、サポートベクターマシンによる非線形な境界線を用いた分類で広く利用されます。RBFカーネルは、以下のような式で表されます。

\[ K(x, z) = exp(-\gamma ||x - z||^2) \]

  • \(x, z\): 入力データ点。異なる2つのデータ点を表す。
  • \(||x - z||^2\): \(x\)\(z\) のユークリッド距離の二乗。
  • \(\gamma\): カーネルの形状を左右するハイパーパラメータ。正の実数として設定する。大きい値にすると、カーネルの幅が狭くなり、決定境界が複雑になる。小さい値にすると、カーネルの幅が広がるため、データ点が多く含まれるようになり、決定境界は単純になりやすい。

この関数では2つのデータ点が近いほど、カーネルの値が大きくなります。つまり、カーネル関数は2つのデータ点の類似度を表していると言えます。RBFカーネルは、2つのデータ点が近いほど、カーネルの値が大きくなるという性質を持ちます。そのため、データ点が密集している領域では、モデルはそれらの間で複雑な決定境界を形成しやすくなります。一方、データ点が疎になる領域では、決定境界は一般的に単純になります。

サポートベクターマシンを実装する際、カーネル関数の種類とパラメータを指定する必要があります。具体的にはRBFカーネルでは、カーネルの形状を決定する \(\gamma\) です。モデルが学習を行う前に、事前に決めておく必要がある、モデルの振る舞いを決定するパラメータをハイパーパラメータと呼びます。

7.2 サポートベクターマシンに与えるデータ

サポートベクターマシンでは入力データが欠損値を含まない状態に加えて、標準化されていることを想定します。それはサポートベクターマシンが、データの特徴量のスケールに敏感であるためです。具体的にはベクトルの内積を計算する際に、各特徴量の値が大きいほど、その特徴量の影響が大きくなるためです。そのため、サポートベクターマシンを適用する前に、データの標準化を行う必要があります。

標準化とは、変数の値を平均0、標準偏差1に変換することです。標準化により、変数の値のスケールを揃えることができます1

7.3 Scikit-learnでのサポートベクターマシンの実装

Scikit-learnでは、SVCクラスを用いてサポートベクターマシンを実装することができます。SVCクラスは、さきに述べたように、線形分離可能なデータに対してはロジスティック回帰モデルと同様に機能します。また、RBFなどのカーネル関数を指定することで、非線形な境界線を描くことも可能です。

ここではロジスティック回帰モデルと同様に、ペンギンのデータセットを用いて、ペンギンの種名を分類するモデルをサポートベクターマシンで実装してみましょう。

import pandas as pd
from sklearn.model_selection import StratifiedKFold, cross_validate, cross_val_score
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score, classification_report
import seaborn as sns
# データの読み込み
penguins = sns.load_dataset("penguins")

7.3.1 前処理・特徴量エンジニアリング

前処理・特徴量エンジニアリングの工程を関数化しておきます。

def penguins_svm_preprocess(data):
  from sklearn.preprocessing import LabelEncoder, StandardScaler
  
  # 元のデータを保持するためデータをコピー
  data_processed = data.copy()
  
  # 前処理: 欠損値を含む行を削除
  data_processed.dropna(inplace=True)
  
  # 特徴量エンジニアリング: ラベルラベルエンコーディング
  le = LabelEncoder()
  for col in ["species", "island", "sex"]:
        data_processed[col] = le.fit_transform(data_processed[col])
        
  # 特徴量エンジニアリング: 標準化
  scaler = StandardScaler()
  data_processed[
        ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]
    ] = scaler.fit_transform(
        data_processed[["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]]
    )
    
  return data_processed

定義した関数にペンギンデータを適用します。

penguins_mod = penguins_svm_preprocess(penguins)

penguins_mod.head(n = 3)
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex
0 0 2 -0.896042 0.780732 -1.426752 -0.568475 1
1 0 2 -0.822788 0.119584 -1.069474 -0.506286 0
2 0 2 -0.676280 0.424729 -0.426373 -1.190361 0
pd.DataFrame({
  "mean": penguins_mod.mean(),
  "standard_deviation": penguins_mod.std(),
})
mean standard_deviation
species 9.189189e-01 0.889718
island 6.516517e-01 0.714715
bill_length_mm 3.840772e-16 1.001505
bill_depth_mm 6.401286e-16 1.001505
flipper_length_mm 2.133762e-16 1.001505
body_mass_g -1.707010e-16 1.001505
sex 5.045045e-01 0.500732

7.3.2 データ分割

続いて、モデルで予測したい目的変数と、モデルの学習に用いる説明変数を分離します。

# 説明変数と目的変数の分離
X = penguins_mod.drop(columns="species")
y = penguins_mod["species"]

今回のモデルでは、単純な訓練データとテストデータの分割によるホールドアウト法ではなく、交差検証法を適用してモデルの評価を行いましょう。交差検証法では、データをいくつかのセット(ホールド)に分割し、それぞれのグループをテストデータとして用いることで、モデルの汎化性能を評価します。

\(k\) 分割交差検証は、データを \(k\) 個のグループに分割し、\(k\) 回の学習と評価を行う方法です。\(k\) 回の学習のうち、\(k-1\) 個のグループを訓練データとして用い、残りの1つのグループをテストデータとして用います。このようにして得られた \(k\) 回の評価の平均値を、モデルの評価値とします。

K分割交差検証法

K分割交差検証法

データに偏りがある場合、単純にデータをランダムに分割すると、各グループのデータに偏りが生じてしまう可能性があります。交差検証法の過程においても、分割後の訓練データとテストデータの間でデータの偏りが生じてしまうと、モデルが過学習を起こす心配があります。そのため、各グループのデータの割合が同じになるように分割する層化抽出法を利用します。層化抽出法では、データを分割する際に、各クラスの割合が同じになるように分割します。

今回のデータでは、目的変数のクラスの割合が同じになるようにデータを分割する必要があります。そのため、層化 \(K\) 分割交差検証を行うために、sklearn.model_selectionモジュールのStratifiedKFoldを用いたデータ分割を行います。

# 交差検証法の例: 層化 k 分割交差検証法
# n_splitで分割数(ホールド数)を指定
# shuffleでデータをシャッフルするか指定(元のデータの並び順を保持する場合はFalse)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=20230515)
# 各ホールドにおけるテストデータの種名の割合を確認
for train_index, test_index in skf.split(X, y):
    print(y.iloc[test_index].value_counts() / len(y.iloc[test_index]))
species
0    0.447761
2    0.358209
1    0.194030
Name: count, dtype: float64
species
0    0.432836
2    0.358209
1    0.208955
Name: count, dtype: float64
species
0    0.432836
2    0.358209
1    0.208955
Name: count, dtype: float64
species
0    0.439394
2    0.348485
1    0.212121
Name: count, dtype: float64
species
0    0.439394
2    0.363636
1    0.196970
Name: count, dtype: float64

各ホールドでのテストデータの種名の割合がほぼ同じになっていることが確認できました。

7.3.3 モデルの学習と評価

sklearn.svmのSVC()関数を用いて、サポートベクターマシンの分類モデルを構築します。今回は、カーネル関数にRBFカーネルを利用します。SVC()関数にはいくつかの引数があり、カーネル関数の種類やハイパーパラメータの指定が可能です。

SVC()関数の引数・ハイパーパラメータは、以下の通りです。

  • C… ソフトマージンのためのハイパーパラメータ。マージンの大きさを調整する。
  • kernel… カーネル関数の種類。デフォルトで利用されるRBFカーネル(rbf)のほか、線形カーネル(linear)、多項式カーネル(poly)などがある。
  • gamma… カーネル関数のパラメータ。RBFカーネルのみ使用。カーネルの幅を調整する。
# サポートベクターマシンモデルの構築
# カーネル関数はRBFカーネルを使用(デフォルト)
svm = SVC(kernel = 'rbf', gamma='scale')

交差検証法を用いてモデルの評価を行うための関数としてcross_val_score()が利用できます。この関数は、モデルとデータを引数として受け取り、交差検証法による評価値を返します。評価指標にはデフォルトではモデルの精度(accuracy)が用いられますが、scoring引数を用いて、他の評価指標を用いることもできます。

scores = cross_val_score(svm, X, y, cv=skf)
# 各ホールドにおける正解率を平均
print("Average score: ", scores.mean())
Average score:  0.9939846223428311

この出力は、各ホールドにおける評価値の平均値を表しています。今回のモデルでは、平均で約0.994の正解率が得られました。

# 再現率 (recall)を評価指標として用いる
scores = cross_val_score(svm, X, y, cv=skf, scoring='recall_macro')
print("Average score: ", scores.mean())
Average score:  0.9901098901098901

cross_validate()関数を用いると、複数の評価指標を同時に用いることができます。この関数はcross_val_score()関数と同じく、モデルとデータを引数として与えると、交差検証法による評価値を返却します。一方でscoring引数に複数の評価指標を指定でき、同時に複数の評価指標を比較可能です。加えて、return_train_score引数をTrueにすることで、テストデータのほかに訓練データに対する評価値も得ることができます。

# 複数の評価指標(正解率、再現率)を用いる
cross_validate(svm, X, y, cv=skf, scoring=['accuracy', 'recall_macro'], return_train_score=True)
{'fit_time': array([0.00293994, 0.00225997, 0.00217056, 0.00217962, 0.00218701]),
 'score_time': array([0.00374651, 0.00297809, 0.00288939, 0.00290704, 0.00291681]),
 'test_accuracy': array([0.98507463, 1.        , 1.        , 0.98484848, 1.        ]),
 'train_accuracy': array([0.9924812 , 0.9924812 , 0.9924812 , 0.99250936, 0.99625468]),
 'test_recall_macro': array([0.97435897, 1.        , 1.        , 0.97619048, 1.        ]),
 'train_recall_macro': array([0.98787879, 0.98765432, 0.98765432, 0.98765432, 0.99393939])}

  1. 線形回帰モデルでの特徴量エンジニアリングを参考↩︎