AI Knowledge Tree

One-class SVM

Carve out a small region containing most of the data with a kernel SVM, and use the boundary as a novelty detector.

⏱ 24 minPrerequisites:The kernel trick

The two-class SVM finds the hyperplane separating two labeled classes. The one-class SVM treats anomaly detection as a degenerate classification: all the training data is "normal", and the algorithm finds the smallest region in feature space that contains the bulk of it. Anything outside is flagged as anomalous[2].

Two equivalent formulations

Two algorithms are commonly called "one-class SVM" and they are equivalent under a change of kernel:

Schölkopf's ν\nu-SVM. Find a hyperplane in kernel feature space that separates the data from the origin with maximum margin[1]. Equivalently, solve

min⁡w,ρ,ξ  12∥w∥2−ρ+1νn∑iξis.t.w⊤ϕ(xi)≥ρ−ξi, ξi≥0.\min_{w, \rho, \xi}\; \tfrac{1}{2}\lVert w\rVert^2 - \rho + \frac{1}{\nu n}\sum_i \xi_i \quad \text{s.t.}\quad w^\top \phi(x_i) \ge \rho - \xi_i,\ \xi_i \ge 0.

The hyperparameter ν∈(0,1]\nu \in (0, 1] is both an upper bound on the fraction of training points outside the boundary (i.e. flagged) and a lower bound on the fraction of support vectors.

Tax & Duin's SVDD (Support Vector Data Description). Find the smallest ball in feature space containing the data:

min⁡c,R,ξ  R2+1νn∑iξis.t.∥ϕ(xi)−c∥2≤R2+ξi, ξi≥0.\min_{c, R, \xi}\; R^2 + \frac{1}{\nu n}\sum_i \xi_i \quad \text{s.t.}\quad \lVert\phi(x_i) - c\rVert^2 \le R^2 + \xi_i,\ \xi_i \ge 0.

With the RBF kernel, the two are equivalent: every point in feature space has the same norm k(x,x)=1k(x, x) = 1, so the smallest enclosing ball and the hyperplane farthest from the origin are the same boundary.

The decision function

After training, score a query point by

f(x)=w⊤ϕ(x)−ρ=∑i∈SVαi k(xi,x)−ρ.f(x) = w^\top \phi(x) - \rho = \sum_{i \in SV}\alpha_i\, k(x_i, x) - \rho.

Flag xx as anomalous if f(x)<0f(x) < 0, normal if f(x)>0f(x) > 0. The magnitude of ff is a continuous score, useful for ranking.

Hyperparameters

  • ν\nu: the fraction of allowed outliers. Set to your expected anomaly rate. Smaller ν\nu = stricter boundary (fewer flags), larger ν\nu = looser.
  • RBF γ\gamma: bandwidth. Small γ\gamma produces wide, smooth boundaries (under-fits the normal region); large γ\gamma produces tight boundaries (over-fits, flags everything). Cross-validate against expected anomaly rate or held-out anomalies if available.

Strengths

  • Kernel flexibility: any positive-definite kernel works, so the boundary can be arbitrarily nonlinear.
  • Sparse representation: only support vectors matter at test time.
  • Provable bound on training-error rate via ν\nu.

Limits

  • Cost: training a kernel SVM is O(n2)O(n^2) memory and worse in time. Impractical above ~10^5 samples.
  • High dimensions: like all distance-based methods, suffers from the curse.
  • Hyperparameter sensitivity: γ\gamma and ν\nu both need cross-validation, and the boundary is sensitive to both.

For large datasets, Isolation Forest (previous lesson) or deep AD methods (next lesson) are usually faster.


References

  1. [1]Schölkopf, B., Platt, J. C., Shawe-Taylor, J., Smola, A. J., & Williamson, R. C. (2001). "Estimating the support of a high-dimensional distribution". Neural Computation, 13(7), 1443-1471. doi:10.1162/089976601750264965. Introduces the ν\nu-SVM that separates data from the origin and the ν\nu bound on outliers and support vectors.↩
  2. [2]Chandola, V., Banerjee, A., & Kumar, V. (2009). "Anomaly detection: a survey". ACM Computing Surveys, 41(3), 1-58. doi:10.1145/1541880.1541882. Frames one-class SVMs within classification-based anomaly detection.↩
A one-class SVM learns a boundary that tightly encloses the normal data; the ring of anomalies around the dense cluster falls outside and is flagged.
A one-class SVM learns a boundary that tightly encloses the normal data; the ring of anomalies around the dense cluster falls outside and is flagged.

Check it on arrays

python · numpy
import numpy as np

rng = np.random.default_rng(0)

# Normal cluster + ring of anomalies far away
n_normal = 200
X_normal = rng.normal(scale=0.7, size=(n_normal, 2))
theta = np.linspace(0, 2 * np.pi, 30, endpoint=False)
X_anom = 4 * np.column_stack([np.cos(theta), np.sin(theta)]) + 0.1 * rng.normal(size=(30, 2))

# One-class SVM via projected gradient on the dual:
# max -0.5 alpha^T K alpha + sum(alpha)   s.t. 0 <= alpha <= 1/(nu n),  sum(alpha) = 1
def rbf(X1, X2, gamma):
    d2 = np.sum(X1**2, 1)[:, None] + np.sum(X2**2, 1) - 2 * X1 @ X2.T
    return np.exp(-gamma * d2)

def one_class_svm(X, nu=0.1, gamma=0.5, steps=5000, lr=1e-3):
    n = len(X)
    K = rbf(X, X, gamma)
    alpha = np.full(n, 1 / n)                            # init to feasible
    cap = 1.0 / (nu * n)
    for _ in range(steps):
        grad = np.ones(n) - K @ alpha
        alpha = np.clip(alpha + lr * grad, 0, cap)
        alpha /= alpha.sum()                              # project onto sum = 1
        alpha = np.clip(alpha, 0, cap)
    # rho recovered from support vectors at the boundary (0 < alpha < cap)
    boundary = (alpha > 1e-4) & (alpha < cap - 1e-4)
    rho = np.median((K @ alpha)[boundary]) if boundary.any() else np.median(K @ alpha)
    return alpha, rho, gamma

alpha, rho, gamma = one_class_svm(X_normal, nu=0.05, gamma=0.5)

def score(X_query, X_train, alpha, rho, gamma):
    return rbf(X_query, X_train, gamma) @ alpha - rho

scores_normal = score(X_normal, X_normal, alpha, rho, gamma)
scores_anom = score(X_anom, X_normal, alpha, rho, gamma)
print(f"mean score on normal:    {scores_normal.mean():.3f}  (>= 0 = normal)")
print(f"mean score on anomalies: {scores_anom.mean():.3f}")
print(f"fraction of anomalies flagged: {(scores_anom < 0).mean():.2f}")
print(f"fraction of normal points flagged: {(scores_normal < 0).mean():.2f}  (nu = 0.05 budget)")

The one-class SVM keeps normal points on the positive side of the boundary and flags most of the ring anomalies; the false-positive rate on normal points sits near the ν\nu budget.

Exercises

Exercise 1. The decision function is f(x)=∑i∈SVαi k(xi,x)−ρf(x) = \sum_{i \in SV}\alpha_i\, k(x_i, x) - \rho. Suppose a model has two support vectors with weights α1=0.6\alpha_1 = 0.6, α2=0.4\alpha_2 = 0.4 and offset ρ=0.5\rho = 0.5, using an RBF kernel k(xi,x)=e−γ∥xi−x∥2k(x_i, x) = e^{-\gamma\lVert x_i - x\rVert^2} with γ=1\gamma = 1. A query point sits at squared distance 0.250.25 from the first support vector and 44 from the second. Compute f(x)f(x) and classify the point.

Solution

Step 1. Evaluate each kernel value:

k1=e−1⋅0.25=e−0.25≈0.779,k2=e−1⋅4=e−4≈0.0183.k_1 = e^{-1 \cdot 0.25} = e^{-0.25} \approx 0.779, \qquad k_2 = e^{-1 \cdot 4} = e^{-4} \approx 0.0183.

Step 2. Form the weighted sum and subtract ρ\rho:

f(x)=0.6⋅0.779+0.4⋅0.0183−0.5≈0.467+0.007−0.5=−0.026.f(x) = 0.6 \cdot 0.779 + 0.4 \cdot 0.0183 - 0.5 \approx 0.467 + 0.007 - 0.5 = -0.026.

Step 3. Since f(x)<0f(x) < 0, the point is flagged as anomalous, just barely: it is close to the first support vector but too far from the second to clear the offset ρ\rho.

Exercise 2. In Schölkopf's ν\nu-SVM, ν∈(0,1]\nu \in (0, 1] is both an upper bound on the fraction of training points that end up outside the boundary and a lower bound on the fraction of support vectors. Explain why these two bounds force a floor on how few support vectors the model can use, and what that implies for the test-time cost when ν\nu is large.

Solution

Step 1. A point lands outside the boundary only if its slack ξi>0\xi_i > 0, which at the optimum means its dual weight is pinned at the cap αi=1/(νn)\alpha_i = 1/(\nu n). Every such margin-error point is a support vector, so the outlier fraction (at most ν\nu) is a subset of the support-vector fraction (at least ν\nu).

Step 2. The lower bound says support vectors are at least a ν\nu fraction of the nn training points: roughly νn\nu n of them. Raising ν\nu to flag more outliers raises this floor, so the model cannot stay sparse.

Step 3. Test-time cost is proportional to the number of support vectors, since f(x)f(x) sums a kernel evaluation per support vector. A large ν\nu therefore buys a looser, more permissive boundary at the price of a denser, slower scorer: the ν\nu knob trades the outlier budget against inference cost.

Exercise 3 (code). Implement the uniform-weight special case of the one-class SVM, where every training point gets equal weight αi=1/n\alpha_i = 1/n so the score is f(x)=1n∑ik(xi,x)−ρf(x) = \frac{1}{n}\sum_i k(x_i, x) - \rho. Set ρ\rho to the ν\nu-quantile of the training scores. On the lesson's normal cluster plus ring of anomalies, check that every ring point is flagged and the normal false-positive rate matches ν\nu.

Solution
python · numpy
import numpy as np

rng = np.random.default_rng(0)

# Uniform-weight one-class scorer: every training point has weight 1/n, so
# f(x) = mean_i k(x_i, x) - rho is large near the dense normal region and
# small far from it. A stable special case of the one-class SVM.
def rbf(X1, X2, gamma):
    d2 = np.sum(X1**2, 1)[:, None] + np.sum(X2**2, 1) - 2 * X1 @ X2.T
    return np.exp(-gamma * np.maximum(d2, 0.0))

n_normal = 200
X_normal = rng.normal(scale=0.7, size=(n_normal, 2))
theta = np.linspace(0, 2 * np.pi, 30, endpoint=False)
X_anom = 4 * np.column_stack([np.cos(theta), np.sin(theta)]) + 0.1 * rng.normal(size=(30, 2))

gamma = 0.5
alpha = np.full(n_normal, 1.0 / n_normal)            # uniform weights

nu = 0.05
train_sim = rbf(X_normal, X_normal, gamma) @ alpha
rho = np.quantile(train_sim, nu)                     # nu-quantile offset

def f(Xq):
    return rbf(Xq, X_normal, gamma) @ alpha - rho

s_normal = f(X_normal)
s_anom = f(X_anom)
print(f"mean score on normal:    {s_normal.mean():.3f}")
print(f"mean score on anomalies: {s_anom.mean():.3f}")
print(f"fraction of anomalies flagged: {(s_anom < 0).mean():.2f}")
print(f"fraction of normal flagged:    {(s_normal < 0).mean():.2f}  (nu = {nu})")

assert (s_anom < 0).all()                            # every ring point flagged
assert abs((s_normal < 0).mean() - nu) < 0.02        # normal false-positive rate ~ nu

Every ring anomaly sits far from the cluster, so all 3030 are flagged. Choosing ρ\rho as the ν\nu-quantile of training scores makes exactly a ν≈0.05\nu \approx 0.05 fraction of normal points fall below zero, reproducing the ν\nu false-alarm budget the full SVM enforces.

The next lesson uses neural-network reconstruction error as the AD signal.