AI Knowledge Tree

The SVM dual and support vectors

Form the SVM dual, identify support vectors via complementary slackness, and read sparsity off KKT.

The SVM primal is a constrained QP over ww and bb. The dual is a QP over one multiplier αi\alpha_i per training example, with a structure that makes two things explicit: the solution is sparse in α\alpha, and it depends on the data only through inner products[1]. The second fact is what kernel methods exploit.

Forming the dual

Start with the soft-margin primal:

min⁡w,b,ξ  12∥w∥2+C∑iξis.t.yi(w⊤xi+b)≥1−ξi,  ξi≥0.\min_{w, b, \xi}\; \tfrac{1}{2}\lVert w\rVert^2 + C\sum_i \xi_i \quad \text{s.t.}\quad y_i(w^\top x_i + b) \ge 1 - \xi_i,\ \ \xi_i \ge 0.

Introduce multipliers αi≥0\alpha_i \ge 0 for the margin constraints and μi≥0\mu_i \ge 0 for the nonnegativity of slack. The Lagrangian is

L=12∥w∥2+C∑iξi−∑iαi(yi(w⊤xi+b)−1+ξi)−∑iμiξi.\mathcal{L} = \tfrac{1}{2}\lVert w\rVert^2 + C\sum_i \xi_i - \sum_i \alpha_i\big(y_i(w^\top x_i + b) - 1 + \xi_i\big) - \sum_i \mu_i \xi_i.

Stationarity:

∂L∂w=w−∑iαiyixi=0,∂L∂b=−∑iαiyi=0,∂L∂ξi=C−αi−μi=0.\frac{\partial \mathcal{L}}{\partial w} = w - \sum_i \alpha_i y_i x_i = 0, \quad \frac{\partial \mathcal{L}}{\partial b} = -\sum_i \alpha_i y_i = 0, \quad \frac{\partial \mathcal{L}}{\partial \xi_i} = C - \alpha_i - \mu_i = 0.

So w=∑iαiyixiw = \sum_i \alpha_i y_i x_i, ∑iαiyi=0\sum_i \alpha_i y_i = 0, and 0≤αi≤C0 \le \alpha_i \le C (eliminating μi=C−αi\mu_i = C - \alpha_i).

Substituting back gives the dual QP:

max⁡α  ∑iαi−12∑i,jαiαjyiyj xi⊤xjs.t.0≤αi≤C, ∑iαiyi=0.\max_{\alpha}\; \sum_i \alpha_i - \tfrac{1}{2}\sum_{i, j} \alpha_i \alpha_j y_i y_j\, x_i^\top x_j \quad \text{s.t.}\quad 0 \le \alpha_i \le C,\ \sum_i \alpha_i y_i = 0.

The data enters only through the inner products xi⊤xjx_i^\top x_j, which is the hook the kernel trick will exploit.

Three regimes of αi\alpha_i (KKT)

The complementary slackness conditions classify every training example into exactly one bucket:

  • αi=0\alpha_i = 0: the constraint yi(w⊤xi+b)≥1y_i(w^\top x_i + b) \ge 1 is slack (strict inequality). The point sits outside the margin and contributes nothing to ww.
  • 0<αi<C0 < \alpha_i < C: the constraint is tight, yi(w⊤xi+b)=1y_i(w^\top x_i + b) = 1. The point lies exactly on the margin boundary. These are the margin support vectors.
  • αi=C\alpha_i = C: the constraint is tight with positive slack, so the point is inside the margin or misclassified. These are the bound support vectors, the noisy or hard-to-classify points.

Only the support vectors have αi>0\alpha_i > 0, and only they contribute to ww. The classifier is sparse in examples: most training points are forgotten after fitting[2].

Recovering ww and bb

The weight vector is

w=∑i∈SVαiyixi,w = \sum_{i \in SV} \alpha_i y_i x_i,

a linear combination of the support vectors. The bias bb is recovered from any margin support vector (0<αi<C0 < \alpha_i < C): such a point satisfies yi(w⊤xi+b)=1y_i(w^\top x_i + b) = 1, so b=yi−w⊤xib = y_i - w^\top x_i. In practice average across all margin support vectors for stability.

Why the dual is interesting

Three reasons:

  • Sparsity: only support vectors matter at prediction time, so the model size is the number of SVs, not the dataset size.
  • Kernelization: the dual depends on data only via inner products, so replacing xi⊤xjx_i^\top x_j with a kernel k(xi,xj)k(x_i, x_j) lifts the SVM to any reproducing kernel Hilbert space without ever forming the lifted features.
  • Algorithms: dual coordinate ascent (SMO) is the basis of LIBSVM and remains competitive on small to medium problems.

Check it on arrays

python · numpy
import numpy as np

rng = np.random.default_rng(0)

# Mildly nonseparable data
n_per = 40
X_pos = rng.normal([1.2, 0.5], scale=0.8, size=(n_per, 2))
X_neg = rng.normal([-1.2, -0.5], scale=0.8, size=(n_per, 2))
X = np.vstack([X_pos, X_neg])
y = np.array([1.0] * n_per + [-1.0] * n_per)

def svm_dual(X, y, C=1.0, lr=1e-3, steps=20000):
    K = X @ X.T
    Q = (y[:, None] * y[None, :]) * K
    n = len(y)
    alpha = np.zeros(n)
    for _ in range(steps):
        grad = 1 - Q @ alpha
        alpha = alpha + lr * grad
        alpha = np.clip(alpha, 0, C)
        # project onto sum(y * alpha) = 0
        s = (y @ alpha) / (y @ y)
        alpha -= s * y
        alpha = np.clip(alpha, 0, C)
    return alpha

C = 1.0
alpha = svm_dual(X, y, C=C)
w = (alpha * y) @ X
margin_sv = (alpha > 1e-4) & (alpha < C - 1e-4)
b = np.mean(y[margin_sv] - X[margin_sv] @ w)

# Classify and check the three buckets
n_zero = int(np.sum(alpha < 1e-4))
n_margin = int(np.sum(margin_sv))
n_bound = int(np.sum(alpha > C - 1e-4))
print(f"alpha = 0      : {n_zero} / {len(y)}  (outside margin)")
print(f"0 < alpha < C  : {n_margin}            (on margin)")
print(f"alpha = C      : {n_bound}            (inside margin or misclassified)")
print(f"accuracy: {np.mean(np.sign(X @ w + b) == y):.3f}")
print(f"margin = 1/||w||: {1.0 / np.linalg.norm(w):.3f}")

The training set splits cleanly into the three KKT buckets; only the support vectors (margin + bound) shape the decision boundary.

Exercises

Exercise 1. A soft-margin SVM with C=1C = 1 returns multipliers α=(0,1,0.4,0)\alpha = (0, 1, 0.4, 0) on four training points. Classify each point into its KKT bucket and state which points contribute to ww.

Solution

Step 1. Compare each αi\alpha_i against the endpoints 00 and C=1C = 1:

  • α1=0\alpha_1 = 0: the margin constraint is slack; the point is outside the margin and contributes nothing to ww.
  • α2=1=C\alpha_2 = 1 = C: a bound support vector; the point is inside the margin or misclassified.
  • α3=0.4\alpha_3 = 0.4, strictly between 00 and CC: a margin support vector, lying exactly on the margin boundary y3(w⊤x3+b)=1y_3(w^\top x_3 + b) = 1.
  • α4=0\alpha_4 = 0: outside the margin, like point 1.

Step 2. Since w=∑iαiyixiw = \sum_i \alpha_i y_i x_i, only points with αi>0\alpha_i > 0 enter the sum. Points 2 and 3 contribute; points 1 and 4 do not. Of these, only the margin support vector (point 3) can be used to recover bb.

Exercise 2. Given the optimal dual solution and a margin support vector xkx_k (with 0<αk<C0 < \alpha_k < C), derive the bias b=yk−∑iαiyi xi⊤xkb = y_k - \sum_i \alpha_i y_i\, x_i^\top x_k.

Solution

Step 1. A margin support vector satisfies the tight constraint yk(w⊤xk+b)=1y_k(w^\top x_k + b) = 1. Multiply both sides by yky_k and use yk2=1y_k^2 = 1:

w⊤xk+b=yk.w^\top x_k + b = y_k.

Step 2. Substitute the stationarity expression w=∑iαiyixiw = \sum_i \alpha_i y_i x_i, so w⊤xk=∑iαiyi xi⊤xkw^\top x_k = \sum_i \alpha_i y_i\, x_i^\top x_k:

∑iαiyi xi⊤xk+b=yk.\sum_i \alpha_i y_i\, x_i^\top x_k + b = y_k.

Step 3. Solve for bb:

b=yk−∑iαiyi xi⊤xk.b = y_k - \sum_i \alpha_i y_i\, x_i^\top x_k.

The inner products xi⊤xkx_i^\top x_k are the only way the data enters, which is the exact hook the kernel trick replaces with k(xi,xk)k(x_i, x_k). Averaging this over all margin support vectors reduces the numerical noise in bb.

Exercise 3 (code). Run the dual coordinate-ascent on a mildly nonseparable dataset and verify the equality constraint ∑iαiyi=0\sum_i \alpha_i y_i = 0 holds and that the three KKT buckets partition all training points.

Solution
python · numpy
import numpy as np

rng = np.random.default_rng(0)
n_per = 40
X = np.vstack([rng.normal([1.2, 0.5], 0.8, (n_per, 2)),
               rng.normal([-1.2, -0.5], 0.8, (n_per, 2))])
y = np.array([1.0] * n_per + [-1.0] * n_per)

def svm_dual(X, y, C=1.0, lr=1e-3, steps=20000):
    Q = (y[:, None] * y[None, :]) * (X @ X.T)
    alpha = np.zeros(len(y))
    for _ in range(steps):
        grad = 1 - Q @ alpha
        alpha = np.clip(alpha + lr * grad, 0, C)
        alpha -= (y @ alpha) / (y @ y) * y        # project onto sum(y*alpha)=0
        alpha = np.clip(alpha, 0, C)
    return alpha

C = 1.0
alpha = svm_dual(X, y, C)

assert abs(y @ alpha) < 1e-2                       # equality constraint

n_zero = int(np.sum(alpha < 1e-4))
n_margin = int(np.sum((alpha > 1e-4) & (alpha < C - 1e-4)))
n_bound = int(np.sum(alpha > C - 1e-4))
print(f"buckets: zero={n_zero}, margin={n_margin}, bound={n_bound}")
assert n_zero + n_margin + n_bound == len(y)      # exact partition

w = (alpha * y) @ X
sv = alpha > 1e-4
b = np.mean(y[sv] - X[sv] @ w)
acc = np.mean(np.sign(X @ w + b) == y)
print(f"accuracy: {acc:.3f}")
assert acc > 0.85

The equality constraint holds to within solver tolerance, the three buckets account for every one of the 80 points, and the recovered classifier separates most of the overlapping data. Only the margin and bound support vectors carried any of that information.

The next lesson exploits the dual's inner-product structure to lift the SVM into nonlinear feature spaces via the kernel trick.


References

  1. [1]Cortes, C., & Vapnik, V. (1995). "Support-vector networks". Machine Learning, 20(3), 273-297. Derives the dual QP and the inner-product structure (doi:10.1007/BF00994018).↩
  2. [2]Vapnik, V. N. (1998). Statistical Learning Theory. Wiley. Treats the KKT conditions and support-vector sparsity in depth.↩