Skip to content

SVM Implementation ​

Maximum margin classifier with kernel methods

Support Vector Machines find the optimal hyperplane that maximizes the margin between classes.

Maximum Margin Hyperplane ​

SVM Maximum Margin Hyperplane

Key equations: Hyperplane wTx+b=0, Decision f(x)=sign(wTx+b), Margin γ=2∥w∥


Hard Margin SVM ​

For linearly separable data:

minw,b12∥w∥2s.t.yi(wTxi+b)≥1

Lagrangian Formulation ​

L(w,b,α)=12∥w∥2−∑i=1nαi[yi(wTxi+b)−1]

KKT Conditions:

  1. ∂L∂w=0⇒w=∑i=1nαiyixi
  2. ∂L∂b=0⇒∑i=1nαiyi=0
  3. αi≥0
  4. αi[yi(wTxi+b)−1]=0 (complementary slackness)

Dual Problem ​

Substituting the KKT conditions:

maxα∑i=1nαi−12∑i=1n∑j=1nαiαjyiyjxiTxj

Subject to:

  • αi≥0 for all i
  • ∑i=1nαiyi=0
python
import numpy as np
from typing import Tuple, List, Callable, Optional
import matplotlib.pyplot as plt
from scipy.optimize import minimize

class HardMarginSVM:
    """
    Hard Margin SVM using quadratic programming.
    Only works for linearly separable data.
    """

    def __init__(self, tol: float = 1e-5):
        self.tol = tol
        self.w = None
        self.b = None
        self.support_vectors = None
        self.support_labels = None
        self.alphas = None

    def fit(self, X: np.ndarray, y: np.ndarray) -> 'HardMarginSVM':
        """
        Fit hard margin SVM using dual formulation.

        Args:
            X: Training features (n_samples, n_features)
            y: Training labels in {-1, +1}
        """
        n_samples, n_features = X.shape

        # Compute Gram matrix
        K = np.dot(X, X.T)

        # Quadratic programming: minimize 0.5 * alpha^T @ P @ alpha - q^T @ alpha
        # P[i,j] = y_i * y_j * K[i,j]
        P = np.outer(y, y) * K
        q = -np.ones(n_samples)

        # Constraints: sum(alpha_i * y_i) = 0, alpha_i >= 0
        def objective(alpha):
            return 0.5 * np.dot(alpha, np.dot(P, alpha)) + np.dot(q, alpha)

        def gradient(alpha):
            return np.dot(P, alpha) + q

        # Equality constraint: sum(alpha_i * y_i) = 0
        constraints = {'type': 'eq', 'fun': lambda a: np.dot(a, y)}

        # Bounds: alpha_i >= 0
        bounds = [(0, None) for _ in range(n_samples)]

        # Initial guess
        alpha0 = np.zeros(n_samples)

        # Solve
        result = minimize(objective, alpha0, jac=gradient,
                         constraints=constraints, bounds=bounds,
                         method='SLSQP')

        alphas = result.x

        # Find support vectors (alpha > tol)
        sv_mask = alphas > self.tol
        self.alphas = alphas[sv_mask]
        self.support_vectors = X[sv_mask]
        self.support_labels = y[sv_mask]

        # Compute w = sum(alpha_i * y_i * x_i)
        self.w = np.sum(self.alphas[:, np.newaxis] *
                        self.support_labels[:, np.newaxis] *
                        self.support_vectors, axis=0)

        # Compute b using any support vector
        # y_s(w^T x_s + b) = 1 => b = y_s - w^T x_s
        self.b = np.mean(self.support_labels -
                        np.dot(self.support_vectors, self.w))

        return self

    def predict(self, X: np.ndarray) -> np.ndarray:
        """Predict class labels."""
        return np.sign(np.dot(X, self.w) + self.b)

    def decision_function(self, X: np.ndarray) -> np.ndarray:
        """Compute decision function values."""
        return np.dot(X, self.w) + self.b

Soft Margin SVM ​

Introduce slack variables ξi≥0 for non-separable data:

minw,b,ξ12∥w∥2+C∑i=1nξis.t.yi(wTxi+b)≥1−ξi

The C Parameter ​

Effect of C Parameter

Dual Formulation ​

maxα∑i=1nαi−12∑i,jαiαjyiyjxiTxjs.t.0≤αi≤C,∑iαiyi=0

Hinge Loss Formulation ​

Unconstrained form using hinge loss Lhinge=max(0,1−y⋅f(x)):

minw,b12∥w∥2+C∑i=1nmax(0,1−yi(wTxi+b))

Subgradient: ∂L∂f=0 if y⋅f(x)>1, else −y

python
class SoftMarginSVM:
    """
    Soft Margin SVM using hinge loss and gradient descent.
    """

    def __init__(self, C: float = 1.0, learning_rate: float = 0.001,
                 n_iterations: int = 1000, tol: float = 1e-5):
        self.C = C
        self.learning_rate = learning_rate
        self.n_iterations = n_iterations
        self.tol = tol
        self.w = None
        self.b = None
        self.loss_history = []

    def _hinge_loss(self, X: np.ndarray, y: np.ndarray) -> float:
        """Compute total hinge loss."""
        margins = y * (np.dot(X, self.w) + self.b)
        hinge = np.maximum(0, 1 - margins)
        return 0.5 * np.dot(self.w, self.w) + self.C * np.sum(hinge)

    def fit(self, X: np.ndarray, y: np.ndarray) -> 'SoftMarginSVM':
        """
        Fit soft margin SVM using subgradient descent.

        Args:
            X: Training features (n_samples, n_features)
            y: Training labels in {-1, +1}
        """
        n_samples, n_features = X.shape

        # Initialize weights
        self.w = np.zeros(n_features)
        self.b = 0

        for iteration in range(self.n_iterations):
            # Compute margins
            margins = y * (np.dot(X, self.w) + self.b)

            # Find misclassified or margin violations
            violations = margins < 1

            # Compute gradients
            # dL/dw = w - C * sum(y_i * x_i) for violations
            dw = self.w - self.C * np.sum(
                y[violations, np.newaxis] * X[violations], axis=0
            )

            # dL/db = -C * sum(y_i) for violations
            db = -self.C * np.sum(y[violations])

            # Update with learning rate decay
            lr = self.learning_rate / (1 + iteration * 0.001)
            self.w -= lr * dw
            self.b -= lr * db

            # Record loss
            loss = self._hinge_loss(X, y)
            self.loss_history.append(loss)

            # Check convergence
            if len(self.loss_history) > 1:
                if abs(self.loss_history[-1] - self.loss_history[-2]) < self.tol:
                    break

        return self

    def predict(self, X: np.ndarray) -> np.ndarray:
        """Predict class labels."""
        return np.sign(np.dot(X, self.w) + self.b)

    def decision_function(self, X: np.ndarray) -> np.ndarray:
        """Compute decision function values."""
        return np.dot(X, self.w) + self.b

    def score(self, X: np.ndarray, y: np.ndarray) -> float:
        """Compute accuracy."""
        predictions = self.predict(X)
        return np.mean(predictions == y)

The Kernel Trick ​

Kernel Trick Visualization

Maps data to higher dimensions implicitly: K(xi,xj)=ϕ(xi)Tϕ(xj)

Kernel Comparison ​

Kernel Comparison

KernelFormula
LinearK(x,y)=xTy
PolynomialK(x,y)=(γxTy+r)d
RBFK(x,y)=exp⁡(−γ|x−y|2)
SigmoidK(x,y)=tanh⁡(γxTy+r)
python
class KernelFunctions:
    """Collection of kernel functions for SVM."""

    @staticmethod
    def linear(x1: np.ndarray, x2: np.ndarray) -> np.ndarray:
        """Linear kernel: K(x, y) = x^T y"""
        return np.dot(x1, x2.T)

    @staticmethod
    def polynomial(x1: np.ndarray, x2: np.ndarray,
                   degree: int = 3, gamma: float = 1.0,
                   coef0: float = 1.0) -> np.ndarray:
        """Polynomial kernel: K(x, y) = (gamma * x^T y + coef0)^degree"""
        return (gamma * np.dot(x1, x2.T) + coef0) ** degree

    @staticmethod
    def rbf(x1: np.ndarray, x2: np.ndarray,
            gamma: float = 1.0) -> np.ndarray:
        """
        RBF (Gaussian) kernel: K(x, y) = exp(-gamma * ||x - y||^2)

        Handles both vector and matrix inputs.
        """
        if x1.ndim == 1:
            x1 = x1.reshape(1, -1)
        if x2.ndim == 1:
            x2 = x2.reshape(1, -1)

        # Compute squared Euclidean distances
        # ||x - y||^2 = ||x||^2 + ||y||^2 - 2 * x^T y
        x1_sq = np.sum(x1 ** 2, axis=1).reshape(-1, 1)
        x2_sq = np.sum(x2 ** 2, axis=1).reshape(1, -1)
        distances = x1_sq + x2_sq - 2 * np.dot(x1, x2.T)

        return np.exp(-gamma * distances)

    @staticmethod
    def sigmoid(x1: np.ndarray, x2: np.ndarray,
                gamma: float = 0.01, coef0: float = 0.0) -> np.ndarray:
        """Sigmoid kernel: K(x, y) = tanh(gamma * x^T y + coef0)"""
        return np.tanh(gamma * np.dot(x1, x2.T) + coef0)


class KernelSVM:
    """
    Kernelized SVM using dual formulation.
    """

    def __init__(self, C: float = 1.0, kernel: str = 'rbf',
                 gamma: float = 1.0, degree: int = 3, coef0: float = 1.0,
                 tol: float = 1e-5, max_iter: int = 1000):
        self.C = C
        self.kernel = kernel
        self.gamma = gamma
        self.degree = degree
        self.coef0 = coef0
        self.tol = tol
        self.max_iter = max_iter

        # Model parameters
        self.alphas = None
        self.support_vectors = None
        self.support_labels = None
        self.b = None

    def _compute_kernel(self, X1: np.ndarray, X2: np.ndarray) -> np.ndarray:
        """Compute kernel matrix between X1 and X2."""
        if self.kernel == 'linear':
            return KernelFunctions.linear(X1, X2)
        elif self.kernel == 'polynomial':
            return KernelFunctions.polynomial(X1, X2, self.degree,
                                              self.gamma, self.coef0)
        elif self.kernel == 'rbf':
            return KernelFunctions.rbf(X1, X2, self.gamma)
        elif self.kernel == 'sigmoid':
            return KernelFunctions.sigmoid(X1, X2, self.gamma, self.coef0)
        else:
            raise ValueError(f"Unknown kernel: {self.kernel}")

    def fit(self, X: np.ndarray, y: np.ndarray) -> 'KernelSVM':
        """
        Fit kernelized SVM using quadratic programming.

        Args:
            X: Training features (n_samples, n_features)
            y: Training labels in {-1, +1}
        """
        n_samples = X.shape[0]

        # Compute kernel matrix
        K = self._compute_kernel(X, X)

        # Setup QP problem
        P = np.outer(y, y) * K
        q = -np.ones(n_samples)

        def objective(alpha):
            return 0.5 * np.dot(alpha, np.dot(P, alpha)) + np.dot(q, alpha)

        def gradient(alpha):
            return np.dot(P, alpha) + q

        constraints = {'type': 'eq', 'fun': lambda a: np.dot(a, y)}
        bounds = [(0, self.C) for _ in range(n_samples)]
        alpha0 = np.zeros(n_samples)

        result = minimize(objective, alpha0, jac=gradient,
                         constraints=constraints, bounds=bounds,
                         method='SLSQP', options={'maxiter': self.max_iter})

        alphas = result.x

        # Find support vectors
        sv_mask = alphas > self.tol
        self.alphas = alphas[sv_mask]
        self.support_vectors = X[sv_mask]
        self.support_labels = y[sv_mask]

        # Compute bias using support vectors on margin
        # These have 0 < alpha < C
        margin_sv_mask = (alphas > self.tol) & (alphas < self.C - self.tol)

        if np.sum(margin_sv_mask) > 0:
            margin_sv = X[margin_sv_mask]
            margin_labels = y[margin_sv_mask]

            # b = y_s - sum(alpha_i * y_i * K(x_i, x_s))
            K_sv = self._compute_kernel(self.support_vectors, margin_sv)
            predictions = np.dot(self.alphas * self.support_labels, K_sv)
            self.b = np.mean(margin_labels - predictions)
        else:
            # Fallback: use all support vectors
            K_sv = self._compute_kernel(self.support_vectors,
                                        self.support_vectors)
            predictions = np.dot(self.alphas * self.support_labels, K_sv)
            self.b = np.mean(self.support_labels - predictions)

        return self

    def decision_function(self, X: np.ndarray) -> np.ndarray:
        """Compute decision function values."""
        K = self._compute_kernel(self.support_vectors, X)
        return np.dot(self.alphas * self.support_labels, K) + self.b

    def predict(self, X: np.ndarray) -> np.ndarray:
        """Predict class labels."""
        return np.sign(self.decision_function(X))

    def score(self, X: np.ndarray, y: np.ndarray) -> float:
        """Compute accuracy."""
        return np.mean(self.predict(X) == y)

Sequential Minimal Optimization (SMO) ​

Overview ​

SMO solves the SVM optimization by breaking it into smallest possible subproblems: optimizing two α values at a time.

Why Two at a Time? ​

The constraint ∑iαiyi=0 means we cannot change just one α.

With two variables α1,α2:

α1y1+α2y2=constant

The Two-Variable Subproblem ​

Given old values α1old,α2old, find new values.

Bounds on α2:

  • If y1≠y2: L=max(0,α2old−α1old), H=min(C,C+α2old−α1old)
  • If y1=y2: L=max(0,α1old+α2old−C), H=min(C,α1old+α2old)

Update formula:

α2new=α2old+y2(E1−E2)η

where:

  • Ei=f(xi)−yi (prediction error)
  • η=2K(x1,x2)−K(x1,x1)−K(x2,x2)

Clip to bounds: α2new=clip(α2new,L,H)

Update α1:

α1new=α1old+y1y2(α2old−α2new)
python
class SMO_SVM:
    """
    Support Vector Machine using Sequential Minimal Optimization.

    SMO breaks the large QP problem into small 2-variable subproblems.
    """

    def __init__(self, C: float = 1.0, kernel: str = 'rbf',
                 gamma: float = 1.0, degree: int = 3, coef0: float = 1.0,
                 tol: float = 1e-3, max_passes: int = 100):
        self.C = C
        self.kernel = kernel
        self.gamma = gamma
        self.degree = degree
        self.coef0 = coef0
        self.tol = tol
        self.max_passes = max_passes

        # Model state
        self.alphas = None
        self.b = 0
        self.X = None
        self.y = None
        self.K = None  # Cached kernel matrix
        self.errors = None  # Cached errors

    def _compute_kernel_matrix(self, X: np.ndarray) -> np.ndarray:
        """Compute full kernel matrix."""
        if self.kernel == 'linear':
            return KernelFunctions.linear(X, X)
        elif self.kernel == 'rbf':
            return KernelFunctions.rbf(X, X, self.gamma)
        elif self.kernel == 'polynomial':
            return KernelFunctions.polynomial(X, X, self.degree,
                                              self.gamma, self.coef0)

    def _decision_function_single(self, i: int) -> float:
        """Compute f(x_i) = sum(alpha_j * y_j * K(x_j, x_i)) + b"""
        return np.dot(self.alphas * self.y, self.K[:, i]) + self.b

    def _compute_error(self, i: int) -> float:
        """Compute E_i = f(x_i) - y_i"""
        return self._decision_function_single(i) - self.y[i]

    def _take_step(self, i1: int, i2: int) -> bool:
        """
        Optimize alpha[i1] and alpha[i2].
        Returns True if optimization made progress.
        """
        if i1 == i2:
            return False

        alpha1_old = self.alphas[i1]
        alpha2_old = self.alphas[i2]
        y1, y2 = self.y[i1], self.y[i2]
        E1 = self.errors[i1]
        E2 = self.errors[i2]

        s = y1 * y2

        # Compute bounds L and H
        if y1 != y2:
            L = max(0, alpha2_old - alpha1_old)
            H = min(self.C, self.C + alpha2_old - alpha1_old)
        else:
            L = max(0, alpha1_old + alpha2_old - self.C)
            H = min(self.C, alpha1_old + alpha2_old)

        if L >= H:
            return False

        # Compute eta
        k11 = self.K[i1, i1]
        k12 = self.K[i1, i2]
        k22 = self.K[i2, i2]
        eta = 2 * k12 - k11 - k22

        if eta < 0:
            # Standard case: compute new alpha2
            alpha2_new = alpha2_old - y2 * (E1 - E2) / eta

            # Clip to bounds
            if alpha2_new > H:
                alpha2_new = H
            elif alpha2_new < L:
                alpha2_new = L
        else:
            # Rare case: evaluate objective at bounds
            # (handling edge cases in numerical stability)
            return False

        # Check if change is significant
        if abs(alpha2_new - alpha2_old) < self.tol * (alpha2_new + alpha2_old + self.tol):
            return False

        # Compute new alpha1
        alpha1_new = alpha1_old + s * (alpha2_old - alpha2_new)

        # Update bias
        b1 = E1 + y1 * (alpha1_new - alpha1_old) * k11 + \
             y2 * (alpha2_new - alpha2_old) * k12 + self.b
        b2 = E2 + y1 * (alpha1_new - alpha1_old) * k12 + \
             y2 * (alpha2_new - alpha2_old) * k22 + self.b

        if 0 < alpha1_new < self.C:
            self.b = b1
        elif 0 < alpha2_new < self.C:
            self.b = b2
        else:
            self.b = (b1 + b2) / 2

        # Update alphas
        self.alphas[i1] = alpha1_new
        self.alphas[i2] = alpha2_new

        # Update error cache
        self.errors[i1] = self._compute_error(i1)
        self.errors[i2] = self._compute_error(i2)

        return True

    def _examine_example(self, i2: int) -> int:
        """
        Try to find a good i1 to pair with i2.
        Returns 1 if optimization succeeded, 0 otherwise.
        """
        y2 = self.y[i2]
        alpha2 = self.alphas[i2]
        E2 = self.errors[i2]
        r2 = E2 * y2

        # Check if alpha2 violates KKT conditions
        if (r2 < -self.tol and alpha2 < self.C) or \
           (r2 > self.tol and alpha2 > 0):

            # Get indices of non-bound alphas
            non_bound = np.where((self.alphas > 0) & (self.alphas < self.C))[0]

            if len(non_bound) > 1:
                # Second choice heuristic: maximize |E1 - E2|
                if E2 > 0:
                    i1 = np.argmin(self.errors)
                else:
                    i1 = np.argmax(self.errors)

                if self._take_step(i1, i2):
                    return 1

            # Loop over non-bound alphas
            for i1 in np.random.permutation(non_bound):
                if self._take_step(i1, i2):
                    return 1

            # Loop over all alphas
            for i1 in np.random.permutation(len(self.alphas)):
                if self._take_step(i1, i2):
                    return 1

        return 0

    def fit(self, X: np.ndarray, y: np.ndarray) -> 'SMO_SVM':
        """
        Fit SVM using SMO algorithm.

        Args:
            X: Training features (n_samples, n_features)
            y: Training labels in {-1, +1}
        """
        n_samples = X.shape[0]

        self.X = X
        self.y = y
        self.alphas = np.zeros(n_samples)
        self.b = 0

        # Cache kernel matrix (for small datasets)
        self.K = self._compute_kernel_matrix(X)

        # Initialize error cache
        self.errors = -y.astype(float)  # E_i = f(x_i) - y_i = -y_i initially

        num_changed = 0
        examine_all = True
        passes = 0

        while (num_changed > 0 or examine_all) and passes < self.max_passes:
            num_changed = 0

            if examine_all:
                # Loop over all training examples
                for i in range(n_samples):
                    num_changed += self._examine_example(i)
            else:
                # Loop over non-bound examples
                non_bound = np.where((self.alphas > 0) &
                                    (self.alphas < self.C))[0]
                for i in non_bound:
                    num_changed += self._examine_example(i)

            passes += 1

            if examine_all:
                examine_all = False
            elif num_changed == 0:
                examine_all = True

        # Store support vectors
        sv_mask = self.alphas > 1e-7
        self.support_vectors = X[sv_mask]
        self.support_labels = y[sv_mask]
        self.support_alphas = self.alphas[sv_mask]

        return self

    def decision_function(self, X: np.ndarray) -> np.ndarray:
        """Compute decision function for new samples."""
        if self.kernel == 'rbf':
            K = KernelFunctions.rbf(self.support_vectors, X, self.gamma)
        elif self.kernel == 'linear':
            K = KernelFunctions.linear(self.support_vectors, X)
        elif self.kernel == 'polynomial':
            K = KernelFunctions.polynomial(self.support_vectors, X,
                                           self.degree, self.gamma, self.coef0)

        return np.dot(self.support_alphas * self.support_labels, K) + self.b

    def predict(self, X: np.ndarray) -> np.ndarray:
        """Predict class labels."""
        return np.sign(self.decision_function(X))

Multi-Class Strategies ​

SVMs are inherently binary classifiers. For multi-class problems:

One-vs-Rest (OvR) ​

Train K binary classifiers, one for each class vs. all others.

Prediction: Choose class with highest decision value.

One-vs-One (OvO) ​

Train (K2)=K(K−1)2 binary classifiers for each pair.

Prediction: Voting - each classifier votes for one class.

python
class MultiClassSVM:
    """
    Multi-class SVM using One-vs-Rest or One-vs-One strategies.
    """

    def __init__(self, strategy: str = 'ovr', C: float = 1.0,
                 kernel: str = 'rbf', gamma: float = 1.0):
        self.strategy = strategy
        self.C = C
        self.kernel = kernel
        self.gamma = gamma
        self.classifiers = {}
        self.classes = None

    def fit(self, X: np.ndarray, y: np.ndarray) -> 'MultiClassSVM':
        """
        Fit multi-class SVM.

        Args:
            X: Training features
            y: Training labels (any integer labels)
        """
        self.classes = np.unique(y)
        n_classes = len(self.classes)

        if self.strategy == 'ovr':
            # One-vs-Rest: K classifiers
            for cls in self.classes:
                # Create binary labels: +1 for this class, -1 for others
                y_binary = np.where(y == cls, 1, -1)

                clf = KernelSVM(C=self.C, kernel=self.kernel, gamma=self.gamma)
                clf.fit(X, y_binary)
                self.classifiers[cls] = clf

        elif self.strategy == 'ovo':
            # One-vs-One: K(K-1)/2 classifiers
            for i, cls1 in enumerate(self.classes):
                for cls2 in self.classes[i+1:]:
                    # Get samples from both classes
                    mask = (y == cls1) | (y == cls2)
                    X_pair = X[mask]
                    y_pair = np.where(y[mask] == cls1, 1, -1)

                    clf = KernelSVM(C=self.C, kernel=self.kernel, gamma=self.gamma)
                    clf.fit(X_pair, y_pair)
                    self.classifiers[(cls1, cls2)] = clf

        return self

    def predict(self, X: np.ndarray) -> np.ndarray:
        """Predict class labels."""
        n_samples = X.shape[0]

        if self.strategy == 'ovr':
            # Get decision values from each classifier
            decision_values = np.zeros((n_samples, len(self.classes)))

            for i, cls in enumerate(self.classes):
                decision_values[:, i] = self.classifiers[cls].decision_function(X)

            # Choose class with highest decision value
            return self.classes[np.argmax(decision_values, axis=1)]

        elif self.strategy == 'ovo':
            # Voting
            votes = np.zeros((n_samples, len(self.classes)))

            for (cls1, cls2), clf in self.classifiers.items():
                predictions = clf.predict(X)

                cls1_idx = np.where(self.classes == cls1)[0][0]
                cls2_idx = np.where(self.classes == cls2)[0][0]

                votes[predictions == 1, cls1_idx] += 1
                votes[predictions == -1, cls2_idx] += 1

            return self.classes[np.argmax(votes, axis=1)]

    def score(self, X: np.ndarray, y: np.ndarray) -> float:
        """Compute accuracy."""
        return np.mean(self.predict(X) == y)

Complete Implementation Example ​

python
def create_classification_data(n_samples: int = 200,
                               noise: float = 0.1,
                               pattern: str = 'circles') -> Tuple[np.ndarray, np.ndarray]:
    """Create synthetic classification data."""
    np.random.seed(42)

    if pattern == 'linear':
        # Linearly separable data
        X = np.random.randn(n_samples, 2)
        y = np.sign(X[:, 0] + X[:, 1] + noise * np.random.randn(n_samples))

    elif pattern == 'circles':
        # Concentric circles (not linearly separable)
        t = np.random.uniform(0, 2 * np.pi, n_samples)
        inner = n_samples // 2

        r_inner = 1 + noise * np.random.randn(inner)
        r_outer = 3 + noise * np.random.randn(n_samples - inner)

        X = np.vstack([
            np.column_stack([r_inner * np.cos(t[:inner]),
                            r_inner * np.sin(t[:inner])]),
            np.column_stack([r_outer * np.cos(t[inner:]),
                            r_outer * np.sin(t[inner:])])
        ])
        y = np.array([-1] * inner + [1] * (n_samples - inner))

    elif pattern == 'moons':
        # Two moons
        n_each = n_samples // 2
        t = np.linspace(0, np.pi, n_each)

        moon1 = np.column_stack([np.cos(t), np.sin(t)])
        moon2 = np.column_stack([1 - np.cos(t), 0.5 - np.sin(t)])

        X = np.vstack([moon1, moon2]) + noise * np.random.randn(n_samples, 2)
        y = np.array([-1] * n_each + [1] * n_each)

    # Shuffle
    perm = np.random.permutation(n_samples)
    return X[perm], y[perm]


def plot_decision_boundary(clf, X: np.ndarray, y: np.ndarray,
                          title: str = "SVM Decision Boundary"):
    """Visualize the decision boundary and support vectors."""
    plt.figure(figsize=(10, 8))

    # Create mesh grid
    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))

    # Predict on mesh
    Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)

    # Plot decision boundary and margins
    plt.contourf(xx, yy, Z, levels=np.linspace(Z.min(), Z.max(), 20),
                 cmap='RdBu', alpha=0.6)
    plt.contour(xx, yy, Z, levels=[-1, 0, 1], colors=['blue', 'black', 'red'],
                linestyles=['--', '-', '--'], linewidths=[1.5, 2, 1.5])

    # Plot data points
    plt.scatter(X[y == -1, 0], X[y == -1, 1], c='blue', marker='o',
                edgecolors='k', s=50, label='Class -1')
    plt.scatter(X[y == 1, 0], X[y == 1, 1], c='red', marker='s',
                edgecolors='k', s=50, label='Class +1')

    # Highlight support vectors
    if hasattr(clf, 'support_vectors') and clf.support_vectors is not None:
        plt.scatter(clf.support_vectors[:, 0], clf.support_vectors[:, 1],
                   s=200, facecolors='none', edgecolors='green', linewidths=2,
                   label=f'Support Vectors ({len(clf.support_vectors)})')

    plt.xlabel('Feature 1')
    plt.ylabel('Feature 2')
    plt.title(title)
    plt.legend()
    plt.colorbar(label='Decision Function')
    plt.tight_layout()
    plt.show()


def compare_kernels(X: np.ndarray, y: np.ndarray):
    """Compare different kernel functions."""
    kernels = ['linear', 'polynomial', 'rbf']

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

    for ax, kernel in zip(axes, kernels):
        clf = KernelSVM(C=1.0, kernel=kernel, gamma=1.0, degree=3)
        clf.fit(X, y)

        # Create mesh
        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, 100),
                             np.linspace(y_min, y_max, 100))

        Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()])
        Z = Z.reshape(xx.shape)

        ax.contourf(xx, yy, Z, levels=20, cmap='RdBu', alpha=0.6)
        ax.contour(xx, yy, Z, levels=[0], colors='black', linewidths=2)
        ax.scatter(X[y == -1, 0], X[y == -1, 1], c='blue', edgecolors='k')
        ax.scatter(X[y == 1, 0], X[y == 1, 1], c='red', edgecolors='k')
        ax.set_title(f'{kernel.upper()} Kernel\nAccuracy: {clf.score(X, y):.2%}')

    plt.tight_layout()
    plt.show()


# Main demonstration
if __name__ == "__main__":
    # Generate data
    X, y = create_classification_data(n_samples=200, noise=0.3, pattern='circles')

    # Split data
    n_train = int(0.8 * len(X))
    X_train, X_test = X[:n_train], X[n_train:]
    y_train, y_test = y[:n_train], y[n_train:]

    print("=" * 60)
    print("SVM IMPLEMENTATION DEMONSTRATION")
    print("=" * 60)

    # Test Soft Margin SVM with gradient descent
    print("\n1. Soft Margin SVM (Gradient Descent):")
    svm_soft = SoftMarginSVM(C=1.0, learning_rate=0.01, n_iterations=1000)
    svm_soft.fit(X_train, y_train)
    print(f"   Training Accuracy: {svm_soft.score(X_train, y_train):.2%}")
    print(f"   Test Accuracy: {svm_soft.score(X_test, y_test):.2%}")

    # Test Kernel SVM
    print("\n2. Kernel SVM (RBF):")
    svm_kernel = KernelSVM(C=1.0, kernel='rbf', gamma=0.5)
    svm_kernel.fit(X_train, y_train)
    print(f"   Training Accuracy: {svm_kernel.score(X_train, y_train):.2%}")
    print(f"   Test Accuracy: {svm_kernel.score(X_test, y_test):.2%}")
    print(f"   Number of Support Vectors: {len(svm_kernel.support_vectors)}")

    # Test SMO
    print("\n3. SMO Algorithm (RBF):")
    svm_smo = SMO_SVM(C=1.0, kernel='rbf', gamma=0.5)
    svm_smo.fit(X_train, y_train)
    print(f"   Training Accuracy: {np.mean(svm_smo.predict(X_train) == y_train):.2%}")
    print(f"   Test Accuracy: {np.mean(svm_smo.predict(X_test) == y_test):.2%}")

    # Multi-class example
    print("\n4. Multi-Class SVM:")
    # Create 3-class problem
    X_multi = np.vstack([
        np.random.randn(50, 2) + [0, 0],
        np.random.randn(50, 2) + [3, 0],
        np.random.randn(50, 2) + [1.5, 2.5]
    ])
    y_multi = np.array([0] * 50 + [1] * 50 + [2] * 50)

    mc_svm = MultiClassSVM(strategy='ovr', C=1.0, kernel='rbf', gamma=0.5)
    mc_svm.fit(X_multi, y_multi)
    print(f"   OvR Accuracy: {mc_svm.score(X_multi, y_multi):.2%}")

    mc_svm_ovo = MultiClassSVM(strategy='ovo', C=1.0, kernel='rbf', gamma=0.5)
    mc_svm_ovo.fit(X_multi, y_multi)
    print(f"   OvO Accuracy: {mc_svm_ovo.score(X_multi, y_multi):.2%}")

Hyperparameter Tuning ​

Key parameters: C (margin vs. penalty), gamma (RBF bandwidth), degree (polynomial complexity)

python
def grid_search_svm(X: np.ndarray, y: np.ndarray,
                    C_values: List[float], gamma_values: List[float],
                    n_folds: int = 5) -> Tuple[float, float, float]:
    """Find best hyperparameters using cross-validation."""
    n_samples = len(X)
    fold_size = n_samples // n_folds

    best_score = 0
    best_C, best_gamma = C_values[0], gamma_values[0]

    for C in C_values:
        for gamma in gamma_values:
            scores = []

            for fold in range(n_folds):
                # Create validation fold
                val_start = fold * fold_size
                val_end = val_start + fold_size

                X_val = X[val_start:val_end]
                y_val = y[val_start:val_end]
                X_train = np.vstack([X[:val_start], X[val_end:]])
                y_train = np.concatenate([y[:val_start], y[val_end:]])

                # Train and evaluate
                clf = KernelSVM(C=C, kernel='rbf', gamma=gamma)
                clf.fit(X_train, y_train)
                scores.append(clf.score(X_val, y_val))

            mean_score = np.mean(scores)
            if mean_score > best_score:
                best_score = mean_score
                best_C = C
                best_gamma = gamma

    return best_C, best_gamma, best_score

Summary ​

AspectKey Points
Hard MarginMaximum margin for linearly separable data
Soft MarginSlack variables ξ with regularization C
Kernel TrickImplicit high-dimensional mapping via K(x,y)
SMOEfficient QP solver using 2-variable subproblems
Multi-classOvR (K classifiers) or OvO (K(K-1)/2 classifiers)

Complexity: Training O(n2) to O(n3), Prediction O(nsv⋅d)

Best for: High-dimensional data, clear margins, sparse solutions, small-medium datasets