SVM(サポートベクターマシン)の理論と双対問題を導出する

サポートベクターマシン(Support Vector Machine, SVM)は、マージン最大化という幾何学的に明快な原理に基づく分類アルゴリズムです。1990年代に Vapnik らによって提案され、カーネル法との組み合わせにより非線形分類にも対応できる強力な手法として広く利用されてきました。

SVM の理論的な美しさは、マージン最大化問題がラグランジュ双対問題に変換されることで、サポートベクターという少数のデータ点のみが決定境界を定めるというスパースな解が自然に得られる点にあります。本記事では、ハードマージン SVM からソフトマージン SVM、カーネル SVM、ヒンジ損失との等価性まで、数式の導出を省略せずに解説し、Python でスクラッチ実装を行います。

本記事の内容

  • ハードマージン SVM の定式化(マージン $2/\|\bm{w}\|$ の最大化)
  • KKT 条件を用いたラグランジュ双対問題の完全な導出
  • サポートベクターの幾何学的意味
  • ソフトマージン SVM(スラック変数 $\xi$ と $C$ パラメータ)
  • ソフトマージンの双対問題の導出
  • カーネル SVM($\bm{x}^T\bm{x}’ \to k(\bm{x}, \bm{x}’)$)
  • ヒンジ損失 $\max(0, 1 – y_i f(\bm{x}_i))$ との等価性の証明
  • Python でのスクラッチ実装と決定境界の可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

マージンの幾何学

問題設定

二値分類問題を考えます。訓練データ $\{(\bm{x}_i, y_i)\}_{i=1}^{N}$ が与えられ、$\bm{x}_i \in \mathbb{R}^d$、$y_i \in \{-1, +1\}$ です。

線形分類器は超平面 $\bm{w}^T \bm{x} + b = 0$ によって定義され、分類規則は

$$ h(\bm{x}) = \text{sign}(\bm{w}^T \bm{x} + b) $$

です。

データ点から超平面までの距離

データ点 $\bm{x}_i$ から超平面 $\bm{w}^T\bm{x} + b = 0$ までの符号付き距離は

$$ d(\bm{x}_i) = \frac{\bm{w}^T \bm{x}_i + b}{\|\bm{w}\|} $$

で与えられます。この導出を示しましょう。超平面上の任意の点を $\bm{x}_0$($\bm{w}^T\bm{x}_0 + b = 0$ を満たす)とすると、$\bm{x}_i$ から超平面への射影の長さは

$$ d(\bm{x}_i) = \frac{\bm{w}^T(\bm{x}_i – \bm{x}_0)}{\|\bm{w}\|} = \frac{\bm{w}^T\bm{x}_i – \bm{w}^T\bm{x}_0}{\|\bm{w}\|} = \frac{\bm{w}^T\bm{x}_i + b}{\|\bm{w}\|} $$

最後の等号で $\bm{w}^T\bm{x}_0 = -b$ を用いました。

関数マージンと幾何学的マージン

関数マージン: サンプル $(\bm{x}_i, y_i)$ の関数マージンは

$$ \hat{\gamma}_i = y_i(\bm{w}^T \bm{x}_i + b) $$

正しく分類されていれば $\hat{\gamma}_i > 0$ です。ただし、$\bm{w}$ と $b$ を同じ定数倍しても超平面は変わらないのに関数マージンは変わるため、直接的な比較には不適切です。

幾何学的マージン: サンプル $(\bm{x}_i, y_i)$ の幾何学的マージンは

$$ \gamma_i = y_i \frac{\bm{w}^T \bm{x}_i + b}{\|\bm{w}\|} = \frac{\hat{\gamma}_i}{\|\bm{w}\|} $$

幾何学的マージンは $\bm{w}$ のスケーリングに対して不変であり、データ点から超平面までの実際の距離を表します。

データセット全体のマージン: 全サンプルの幾何学的マージンの最小値

$$ \gamma = \min_{i=1,\dots,N} \gamma_i = \min_{i=1,\dots,N} y_i \frac{\bm{w}^T \bm{x}_i + b}{\|\bm{w}\|} $$

これは、超平面に最も近いデータ点までの距離を表します。

ハードマージン SVM

マージン最大化問題

SVM は、マージン $\gamma$ を最大化する超平面を求めます。

$$ \max_{\bm{w}, b} \quad \gamma = \min_{i} \frac{y_i(\bm{w}^T\bm{x}_i + b)}{\|\bm{w}\|} $$

関数マージンのスケーリングの自由度を利用して、最も近いデータ点の関数マージンを 1 に正規化します。

$$ \min_{i} y_i(\bm{w}^T\bm{x}_i + b) = 1 $$

このとき、幾何学的マージンは $\gamma = 1/\|\bm{w}\|$ となり、マージン最大化は $1/\|\bm{w}\|$ の最大化、すなわち $\|\bm{w}\|$ の最小化と等価です。

主問題の定式化

計算の便宜上 $\frac{1}{2}\|\bm{w}\|^2$ を最小化します(凸二次計画問題として扱えるため)。

$$ \begin{align} \min_{\bm{w}, b} \quad & \frac{1}{2}\|\bm{w}\|^2 \\ \text{subject to} \quad & y_i(\bm{w}^T\bm{x}_i + b) \geq 1, \quad i = 1, \dots, N \end{align} $$

制約 $y_i(\bm{w}^T\bm{x}_i + b) \geq 1$ は「全てのデータ点が超平面から少なくとも $1/\|\bm{w}\|$ の距離にある」ことを要求しています。マージンの幅は $2/\|\bm{w}\|$ であり、これを最大化することがSVMの目標です。

ラグランジュ双対問題の導出

ラグランジュ関数の構成

各制約 $y_i(\bm{w}^T\bm{x}_i + b) – 1 \geq 0$ に対してラグランジュ乗数 $\alpha_i \geq 0$ を導入し、ラグランジュ関数を構成します。

$$ \mathcal{L}(\bm{w}, b, \bm{\alpha}) = \frac{1}{2}\|\bm{w}\|^2 – \sum_{i=1}^{N} \alpha_i \left[y_i(\bm{w}^T\bm{x}_i + b) – 1\right] $$

主変数の消去

$\bm{w}$ と $b$ について微分して 0 とおきます。

$\bm{w}$ についての微分:

$$ \frac{\partial \mathcal{L}}{\partial \bm{w}} = \bm{w} – \sum_{i=1}^{N} \alpha_i y_i \bm{x}_i = \bm{0} $$

$$ \therefore \quad \bm{w} = \sum_{i=1}^{N} \alpha_i y_i \bm{x}_i $$

この式は、最適な $\bm{w}$ がデータ点の線形結合で表されることを示しています。

$b$ についての微分:

$$ \frac{\partial \mathcal{L}}{\partial b} = -\sum_{i=1}^{N} \alpha_i y_i = 0 $$

$$ \therefore \quad \sum_{i=1}^{N} \alpha_i y_i = 0 $$

双対問題の導出

得られた条件 $\bm{w} = \sum_i \alpha_i y_i \bm{x}_i$ と $\sum_i \alpha_i y_i = 0$ をラグランジュ関数に代入します。

第1項:

$$ \frac{1}{2}\|\bm{w}\|^2 = \frac{1}{2}\bm{w}^T\bm{w} = \frac{1}{2}\sum_{i=1}^{N}\sum_{j=1}^{N} \alpha_i \alpha_j y_i y_j \bm{x}_i^T \bm{x}_j $$

第2項:

$$ \begin{align} \sum_{i=1}^{N} \alpha_i y_i (\bm{w}^T\bm{x}_i + b) &= \sum_{i=1}^{N} \alpha_i y_i \bm{w}^T\bm{x}_i + b\sum_{i=1}^{N} \alpha_i y_i \\ &= \sum_{i=1}^{N}\sum_{j=1}^{N} \alpha_i \alpha_j y_i y_j \bm{x}_j^T\bm{x}_i + 0 \\ &= \sum_{i=1}^{N}\sum_{j=1}^{N} \alpha_i \alpha_j y_i y_j \bm{x}_i^T\bm{x}_j \end{align} $$

第3項:

$$ \sum_{i=1}^{N} \alpha_i $$

全てを合わせると

$$ \begin{align} \mathcal{L}(\bm{\alpha}) &= \frac{1}{2}\sum_{i,j} \alpha_i \alpha_j y_i y_j \bm{x}_i^T\bm{x}_j – \sum_{i,j} \alpha_i \alpha_j y_i y_j \bm{x}_i^T\bm{x}_j + \sum_{i} \alpha_i \\ &= \sum_{i=1}^{N} \alpha_i – \frac{1}{2}\sum_{i=1}^{N}\sum_{j=1}^{N} \alpha_i \alpha_j y_i y_j \bm{x}_i^T\bm{x}_j \end{align} $$

双対問題の完成形

$$ \begin{align} \max_{\bm{\alpha}} \quad & \sum_{i=1}^{N} \alpha_i – \frac{1}{2}\sum_{i=1}^{N}\sum_{j=1}^{N} \alpha_i \alpha_j y_i y_j \bm{x}_i^T\bm{x}_j \\ \text{subject to} \quad & \alpha_i \geq 0, \quad i = 1, \dots, N \\ & \sum_{i=1}^{N} \alpha_i y_i = 0 \end{align} $$

これは $\bm{\alpha}$ に関する凸二次計画問題であり、グローバル最適解が保証されます。

サポートベクターと KKT 条件

KKT 条件

最適解において、以下の KKT(Karush-Kuhn-Tucker)条件が成り立ちます。

  1. 主実行可能性: $y_i(\bm{w}^T\bm{x}_i + b) \geq 1$
  2. 双対実行可能性: $\alpha_i \geq 0$
  3. 相補性条件: $\alpha_i [y_i(\bm{w}^T\bm{x}_i + b) – 1] = 0$

相補性条件の解釈

相補性条件 $\alpha_i [y_i(\bm{w}^T\bm{x}_i + b) – 1] = 0$ は、各データ点について以下のいずれかが成り立つことを意味します。

  • $\alpha_i = 0$ の場合: データ点はマージンの外側($y_i(\bm{w}^T\bm{x}_i + b) > 1$)にあり、分類器の定義に寄与しません。
  • $\alpha_i > 0$ の場合: $y_i(\bm{w}^T\bm{x}_i + b) = 1$ が成り立ち、データ点はちょうどマージン境界上にあります。

サポートベクターの定義

$\alpha_i > 0$ を満たすデータ点をサポートベクターと呼びます。$\bm{w} = \sum_i \alpha_i y_i \bm{x}_i$ であり、$\alpha_i = 0$ の項は寄与しないため

$$ \bm{w} = \sum_{i \in SV} \alpha_i y_i \bm{x}_i $$

決定境界はサポートベクターのみによって定まります。データの大部分を除去しても、サポートベクターさえ残っていれば同じ分類器が得られます。

バイアス項 $b$ の計算

任意のサポートベクター $\bm{x}_s$($\alpha_s > 0$)に対して $y_s(\bm{w}^T\bm{x}_s + b) = 1$ が成り立つので

$$ b = y_s – \bm{w}^T\bm{x}_s = y_s – \sum_{i \in SV} \alpha_i y_i \bm{x}_i^T\bm{x}_s $$

数値的な安定性のため、通常は全サポートベクターの平均を取ります。

$$ b = \frac{1}{|SV|}\sum_{s \in SV}\left(y_s – \sum_{i \in SV} \alpha_i y_i \bm{x}_i^T\bm{x}_s\right) $$

ソフトマージン SVM

線形分離不可能な場合

現実のデータは完全には線形分離できないことが多く、ハードマージン SVM は適用できません。ソフトマージン SVM は、一部のデータ点がマージン内に侵入することを許容します。

スラック変数の導入

各データ点に対してスラック変数 $\xi_i \geq 0$ を導入します。

$$ y_i(\bm{w}^T\bm{x}_i + b) \geq 1 – \xi_i $$

  • $\xi_i = 0$: 正しくマージン外に分類されている
  • $0 < \xi_i < 1$: マージン内だが正しい側に分類されている
  • $\xi_i = 1$: ちょうど決定境界上
  • $\xi_i > 1$: 誤分類されている

ソフトマージン SVM の主問題

$$ \begin{align} \min_{\bm{w}, b, \bm{\xi}} \quad & \frac{1}{2}\|\bm{w}\|^2 + C\sum_{i=1}^{N}\xi_i \\ \text{subject to} \quad & y_i(\bm{w}^T\bm{x}_i + b) \geq 1 – \xi_i, \quad i = 1, \dots, N \\ & \xi_i \geq 0, \quad i = 1, \dots, N \end{align} $$

$C > 0$ はペナルティパラメータで、マージン違反に対するペナルティの強さを制御します。

  • $C \to \infty$: マージン違反を許さない(ハードマージンに近づく)
  • $C \to 0$: マージン違反を大いに許容する(正則化が強い)

ソフトマージンの双対問題の導出

ラグランジュ関数を構成します。$\alpha_i \geq 0$(マージン制約)と $\mu_i \geq 0$($\xi_i \geq 0$ の制約)を導入します。

$$ \mathcal{L} = \frac{1}{2}\|\bm{w}\|^2 + C\sum_i \xi_i – \sum_i \alpha_i[y_i(\bm{w}^T\bm{x}_i + b) – 1 + \xi_i] – \sum_i \mu_i \xi_i $$

各変数で微分して 0 とおきます。

$\bm{w}$ について:

$$ \frac{\partial \mathcal{L}}{\partial \bm{w}} = \bm{w} – \sum_i \alpha_i y_i \bm{x}_i = \bm{0} \quad \Rightarrow \quad \bm{w} = \sum_i \alpha_i y_i \bm{x}_i $$

$b$ について:

$$ \frac{\partial \mathcal{L}}{\partial b} = -\sum_i \alpha_i y_i = 0 \quad \Rightarrow \quad \sum_i \alpha_i y_i = 0 $$

$\xi_i$ について:

$$ \frac{\partial \mathcal{L}}{\partial \xi_i} = C – \alpha_i – \mu_i = 0 \quad \Rightarrow \quad \mu_i = C – \alpha_i $$

$\mu_i \geq 0$ より $\alpha_i \leq C$ が得られます。

これらをラグランジュ関数に代入すると、ハードマージンの場合と全く同じ双対目的関数が得られます。

$$ \begin{align} \max_{\bm{\alpha}} \quad & \sum_{i=1}^{N} \alpha_i – \frac{1}{2}\sum_{i=1}^{N}\sum_{j=1}^{N} \alpha_i \alpha_j y_i y_j \bm{x}_i^T\bm{x}_j \\ \text{subject to} \quad & 0 \leq \alpha_i \leq C, \quad i = 1, \dots, N \\ & \sum_{i=1}^{N} \alpha_i y_i = 0 \end{align} $$

ハードマージンとの唯一の違いは、$\alpha_i$ の上界が $C$ になった点です(ボックス制約)。

ソフトマージンの KKT 条件と解の分類

KKT 条件から、データ点は以下の3つに分類されます。

$\alpha_i$ の値 $\xi_i$ 位置 分類
$\alpha_i = 0$ $\xi_i = 0$ マージン外(正しい側) 非サポートベクター
$0 < \alpha_i < C$ $\xi_i = 0$ マージン境界上 サポートベクター(free)
$\alpha_i = C$ $\xi_i > 0$ マージン内または誤分類 サポートベクター(bounded)

バイアス項 $b$ は、free サポートベクター($0 < \alpha_i < C$)を用いて計算します。

カーネル SVM

双対問題へのカーネルの導入

双対問題とその予測式において、データは内積 $\bm{x}_i^T \bm{x}_j$ を通じてのみ参照されます。これをカーネル関数 $k(\bm{x}_i, \bm{x}_j)$ に置き換えるだけで、非線形分類が可能になります。

カーネル SVM の双対問題:

$$ \begin{align} \max_{\bm{\alpha}} \quad & \sum_{i=1}^{N} \alpha_i – \frac{1}{2}\sum_{i=1}^{N}\sum_{j=1}^{N} \alpha_i \alpha_j y_i y_j k(\bm{x}_i, \bm{x}_j) \\ \text{subject to} \quad & 0 \leq \alpha_i \leq C, \quad i = 1, \dots, N \\ & \sum_{i=1}^{N} \alpha_i y_i = 0 \end{align} $$

予測関数:

$$ f(\bm{x}) = \sum_{i \in SV} \alpha_i y_i k(\bm{x}_i, \bm{x}) + b $$

RBF カーネルを使えば、元の入力空間では複雑な非線形決定境界を実現できます。

ヒンジ損失との等価性

ヒンジ損失の定義

ヒンジ損失(hinge loss)は

$$ \ell_{\text{hinge}}(y, f) = \max(0, 1 – yf) $$

と定義されます。$yf \geq 1$(正しく分類かつマージン外)のとき損失 0、$yf < 1$ のとき線形にペナルティが増加します。

等価性の証明

ソフトマージン SVM の主問題

$$ \min_{\bm{w}, b, \bm{\xi}} \frac{1}{2}\|\bm{w}\|^2 + C\sum_i \xi_i \quad \text{s.t.} \quad y_i(\bm{w}^T\bm{x}_i + b) \geq 1 – \xi_i, \; \xi_i \geq 0 $$

において、制約を整理します。

$y_i(\bm{w}^T\bm{x}_i + b) \geq 1 – \xi_i$ かつ $\xi_i \geq 0$ より、$\xi_i$ の最適値は

$$ \xi_i^* = \max(0, 1 – y_i(\bm{w}^T\bm{x}_i + b)) $$

これを確認します。

  • $y_i(\bm{w}^T\bm{x}_i + b) \geq 1$ のとき: $\xi_i = 0$ が最適(制約を満たし、ペナルティ最小)
  • $y_i(\bm{w}^T\bm{x}_i + b) < 1$ のとき: $\xi_i = 1 - y_i(\bm{w}^T\bm{x}_i + b) > 0$ が最適(制約を等式で満たす)

よって $\xi_i^*$ はまさにヒンジ損失 $\max(0, 1 – y_i f(\bm{x}_i))$ に一致し、主問題は

$$ \min_{\bm{w}, b} \frac{1}{2}\|\bm{w}\|^2 + C\sum_{i=1}^{N} \max(0, 1 – y_i(\bm{w}^T\bm{x}_i + b)) $$

と書き換えられます。$\lambda = 1/(2C)$ とおくと

$$ \min_{\bm{w}, b} \sum_{i=1}^{N} \max(0, 1 – y_i(\bm{w}^T\bm{x}_i + b)) + \lambda \|\bm{w}\|^2 $$

これは「ヒンジ損失 + L2 正則化」の形であり、正則化付き経験リスク最小化の枠組みに自然に収まります。

Python でのスクラッチ実装

二次計画法を用いた SVM

import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import minimize

class SVM:
    """SVMのスクラッチ実装(二次計画法ベース)"""
    def __init__(self, C=1.0, kernel='linear', gamma=1.0):
        self.C = C
        self.kernel_type = kernel
        self.gamma = gamma
        self.alpha = None
        self.b = None
        self.support_vectors = None
        self.support_vector_labels = None
        self.support_vector_alphas = None

    def _kernel(self, x1, x2):
        """カーネル関数"""
        if self.kernel_type == 'linear':
            return np.dot(x1, x2)
        elif self.kernel_type == 'rbf':
            return np.exp(-self.gamma * np.linalg.norm(x1 - x2) ** 2)
        elif self.kernel_type == 'polynomial':
            return (1 + np.dot(x1, x2)) ** 3

    def _kernel_matrix(self, X):
        """グラム行列の計算"""
        N = X.shape[0]
        K = np.zeros((N, N))
        for i in range(N):
            for j in range(N):
                K[i, j] = self._kernel(X[i], X[j])
        return K

    def fit(self, X, y):
        """学習"""
        N = X.shape[0]
        K = self._kernel_matrix(X)

        # 双対問題を最小化問題に変換: min -Σα_i + (1/2)ΣΣα_i α_j y_i y_j K_ij
        # scipy.optimize.minimize を使用
        def objective(alpha):
            return 0.5 * np.sum((alpha * y)[:, None] * (alpha * y)[None, :] * K) - np.sum(alpha)

        def gradient(alpha):
            return np.sum((alpha * y)[None, :] * K * y[:, None], axis=1) - 1

        # 制約: Σα_i y_i = 0
        constraints = {'type': 'eq', 'fun': lambda a: np.dot(a, y)}
        # 境界: 0 <= α_i <= C
        bounds = [(0, self.C) for _ in range(N)]

        # 最適化
        alpha0 = np.zeros(N)
        result = minimize(objective, alpha0, jac=gradient,
                         bounds=bounds, constraints=constraints,
                         method='SLSQP', options={'maxiter': 1000, 'ftol': 1e-10})
        self.alpha = result.x

        # サポートベクターの抽出(α > 閾値)
        sv_threshold = 1e-5
        sv_mask = self.alpha > sv_threshold
        self.support_vectors = X[sv_mask]
        self.support_vector_labels = y[sv_mask]
        self.support_vector_alphas = self.alpha[sv_mask]

        # バイアス項の計算(freeサポートベクターを使用)
        free_sv_mask = (self.alpha > sv_threshold) & (self.alpha < self.C - sv_threshold)
        if np.any(free_sv_mask):
            free_indices = np.where(free_sv_mask)[0]
            b_values = []
            for idx in free_indices:
                b_val = y[idx]
                for j in range(len(self.support_vectors)):
                    b_val -= self.support_vector_alphas[j] * self.support_vector_labels[j] * \
                             self._kernel(self.support_vectors[j], X[idx])
                b_values.append(b_val)
            self.b = np.mean(b_values)
        else:
            # 全てのサポートベクターで平均
            sv_indices = np.where(sv_mask)[0]
            b_values = []
            for idx in sv_indices:
                b_val = y[idx]
                for j in range(len(self.support_vectors)):
                    b_val -= self.support_vector_alphas[j] * self.support_vector_labels[j] * \
                             self._kernel(self.support_vectors[j], X[idx])
                b_values.append(b_val)
            self.b = np.mean(b_values)

    def decision_function(self, X):
        """決定関数 f(x) = Σα_i y_i k(x_i, x) + b"""
        N = X.shape[0]
        f = np.zeros(N)
        for i in range(N):
            for j in range(len(self.support_vectors)):
                f[i] += self.support_vector_alphas[j] * self.support_vector_labels[j] * \
                         self._kernel(self.support_vectors[j], X[i])
            f[i] += self.b
        return f

    def predict(self, X):
        """予測"""
        return np.sign(self.decision_function(X))

ハードマージン SVM の可視化

# 線形分離可能なデータの生成
np.random.seed(42)
N = 40
X_pos = np.random.randn(N // 2, 2) + np.array([2, 2])
X_neg = np.random.randn(N // 2, 2) + np.array([-2, -2])
X = np.vstack([X_pos, X_neg])
y = np.array([1] * (N // 2) + [-1] * (N // 2), dtype=float)

# ハードマージンSVM(C を大きくしてハードマージンを近似)
svm_hard = SVM(C=1000.0, kernel='linear')
svm_hard.fit(X, y)

# 決定境界の可視化
fig, ax = plt.subplots(figsize=(8, 8))

# 決定境界のメッシュ
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
                      np.linspace(y_min, y_max, 200))
X_grid = np.c_[xx.ravel(), yy.ravel()]
Z = svm_hard.decision_function(X_grid).reshape(xx.shape)

# 決定境界とマージン
ax.contour(xx, yy, Z, levels=[-1, 0, 1], colors=['blue', 'black', 'red'],
           linestyles=['--', '-', '--'], linewidths=[1.5, 2, 1.5])
ax.contourf(xx, yy, Z, levels=[-100, 0, 100], alpha=0.1, colors=['blue', 'red'])

# データ点
ax.scatter(X[y == 1, 0], X[y == 1, 1], c='red', edgecolors='k', s=50, label='$y=+1$', zorder=5)
ax.scatter(X[y == -1, 0], X[y == -1, 1], c='blue', edgecolors='k', s=50, label='$y=-1$', zorder=5)

# サポートベクターの強調
ax.scatter(svm_hard.support_vectors[:, 0], svm_hard.support_vectors[:, 1],
           s=200, facecolors='none', edgecolors='green', linewidths=2, label='Support vectors', zorder=6)

ax.set_xlabel('$x_1$', fontsize=12)
ax.set_ylabel('$x_2$', fontsize=12)
ax.set_title(f'Hard Margin SVM (# support vectors = {len(svm_hard.support_vectors)})', fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('svm_hard_margin.png', dpi=150, bbox_inches='tight')
plt.show()

ソフトマージン SVM と C パラメータの影響

# ノイズを含むデータの生成
np.random.seed(42)
N = 100
X_pos = np.random.randn(N // 2, 2) + np.array([1, 1])
X_neg = np.random.randn(N // 2, 2) + np.array([-1, -1])
X = np.vstack([X_pos, X_neg])
y = np.array([1] * (N // 2) + [-1] * (N // 2), dtype=float)

fig, axes = plt.subplots(1, 3, figsize=(18, 5))
C_values = [0.1, 1.0, 100.0]

for ax, C in zip(axes, C_values):
    svm = SVM(C=C, kernel='linear')
    svm.fit(X, y)

    # 決定境界
    x_min, x_max = X[:, 0].min() - 1.5, X[:, 0].max() + 1.5
    y_min, y_max = X[:, 1].min() - 1.5, X[:, 1].max() + 1.5
    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 150),
                          np.linspace(y_min, y_max, 150))
    X_grid = np.c_[xx.ravel(), yy.ravel()]
    Z = svm.decision_function(X_grid).reshape(xx.shape)

    ax.contour(xx, yy, Z, levels=[-1, 0, 1], colors=['blue', 'black', 'red'],
               linestyles=['--', '-', '--'], linewidths=[1, 2, 1])
    ax.contourf(xx, yy, Z, levels=[-100, 0, 100], alpha=0.1, colors=['blue', 'red'])

    ax.scatter(X[y == 1, 0], X[y == 1, 1], c='red', edgecolors='k', s=30, zorder=5)
    ax.scatter(X[y == -1, 0], X[y == -1, 1], c='blue', edgecolors='k', s=30, zorder=5)
    ax.scatter(svm.support_vectors[:, 0], svm.support_vectors[:, 1],
               s=120, facecolors='none', edgecolors='green', linewidths=2, zorder=6)

    ax.set_title(f'C = {C} (# SV = {len(svm.support_vectors)})', fontsize=13)
    ax.set_xlim(x_min, x_max)
    ax.set_ylim(y_min, y_max)
    ax.grid(True, alpha=0.3)

plt.suptitle('Soft Margin SVM: Effect of C Parameter', fontsize=15, y=1.02)
plt.tight_layout()
plt.savefig('svm_soft_margin_C.png', dpi=150, bbox_inches='tight')
plt.show()

カーネル SVM の可視化

# 非線形データの生成(月型データ)
from sklearn.datasets import make_moons

np.random.seed(42)
X, y_01 = make_moons(n_samples=100, noise=0.15, random_state=42)
y = 2 * y_01 - 1  # {0,1} -> {-1,+1}

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

kernel_configs = [
    ('linear', 1.0, 'Linear Kernel'),
    ('rbf', 0.5, 'RBF Kernel ($\\gamma=0.5$)'),
    ('rbf', 5.0, 'RBF Kernel ($\\gamma=5.0$)'),
]

for ax, (kernel, gamma, title) in zip(axes, kernel_configs):
    svm = SVM(C=10.0, kernel=kernel, gamma=gamma)
    svm.fit(X, y)

    # 決定境界
    x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
    y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 150),
                          np.linspace(y_min, y_max, 150))
    X_grid = np.c_[xx.ravel(), yy.ravel()]
    Z = svm.decision_function(X_grid).reshape(xx.shape)

    ax.contourf(xx, yy, Z, levels=[-100, 0, 100], alpha=0.2, colors=['blue', 'red'])
    ax.contour(xx, yy, Z, levels=[0], colors='black', linewidths=2)

    ax.scatter(X[y == 1, 0], X[y == 1, 1], c='red', edgecolors='k', s=30, zorder=5)
    ax.scatter(X[y == -1, 0], X[y == -1, 1], c='blue', edgecolors='k', s=30, zorder=5)
    ax.scatter(svm.support_vectors[:, 0], svm.support_vectors[:, 1],
               s=120, facecolors='none', edgecolors='green', linewidths=2, zorder=6)

    ax.set_title(f'{title}\n(# SV = {len(svm.support_vectors)})', fontsize=12)
    ax.grid(True, alpha=0.3)

plt.suptitle('Kernel SVM: Nonlinear Classification', fontsize=15, y=1.05)
plt.tight_layout()
plt.savefig('svm_kernel.png', dpi=150, bbox_inches='tight')
plt.show()

損失関数の比較

# ヒンジ損失と他の損失関数の比較
fig, ax = plt.subplots(figsize=(10, 6))
yf = np.linspace(-2, 3, 500)

# ヒンジ損失
hinge = np.maximum(0, 1 - yf)
# 指数損失(AdaBoost)
exp_loss = np.exp(-yf)
# 対数損失(ロジスティック回帰)
log_loss = np.log(1 + np.exp(-yf)) / np.log(2)
# 0-1損失
zero_one = (yf < 0).astype(float)

ax.plot(yf, hinge, 'b-', linewidth=2.5, label='Hinge loss: $\\max(0, 1-yf)$')
ax.plot(yf, exp_loss, 'r-', linewidth=2, label='Exponential loss: $e^{-yf}$')
ax.plot(yf, log_loss, 'g-', linewidth=2, label='Log loss: $\\log_2(1+e^{-yf})$')
ax.plot(yf, zero_one, 'k--', linewidth=2, label='0-1 loss')

ax.set_xlabel('$yf(x)$ (margin)', fontsize=13)
ax.set_ylabel('Loss', fontsize=13)
ax.set_title('Comparison of Loss Functions for Classification', fontsize=14)
ax.legend(fontsize=11, loc='upper right')
ax.set_xlim(-2, 3)
ax.set_ylim(-0.1, 4)
ax.grid(True, alpha=0.3)
ax.axvline(x=0, color='gray', linestyle=':', alpha=0.5)
ax.axvline(x=1, color='gray', linestyle=':', alpha=0.5)

plt.tight_layout()
plt.savefig('loss_functions_comparison.png', dpi=150, bbox_inches='tight')
plt.show()

ヒンジ損失は、0-1損失の凸上界であり、$yf = 1$ で折れ点を持つ区分線形関数です。$yf \geq 1$(マージン外で正しく分類)で損失が 0 になる点が特徴で、これがサポートベクターのスパース性を生み出す要因です。

まとめ

本記事では、SVM の理論を双対問題の導出まで丁寧に解説しました。

  • ハードマージン SVM: マージン $2/\|\bm{w}\|$ の最大化を $\frac{1}{2}\|\bm{w}\|^2$ の最小化として定式化し、制約付き二次計画問題として解きます
  • ラグランジュ双対問題: KKT 条件を用いた導出により、$\bm{w}$ と $b$ を消去し、$\bm{\alpha}$ のみの最大化問題に変換されます
  • サポートベクター: $\alpha_i > 0$ のデータ点のみが決定境界を定めるスパースな解が得られます
  • ソフトマージン SVM: スラック変数 $\xi_i$ の導入により線形分離不可能なデータにも対応し、$C$ パラメータで正則化の強さを制御します
  • カーネル SVM: 双対問題の $\bm{x}_i^T\bm{x}_j$ を $k(\bm{x}_i, \bm{x}_j)$ に置換するだけで非線形分類が実現されます
  • ヒンジ損失との等価性: ソフトマージン SVM は「ヒンジ損失 + L2 正則化」の最小化と等価であることを証明しました

次のステップとして、以下の記事も参考にしてください。