Hard-margin SVM
Define the geometric margin and derive the hard-margin SVM as a quadratic program.
The perceptron settles for any separating hyperplane. The support vector machine (SVM) demands a particular one: the hyperplane that maximizes the distance to the nearest training point[1]. This maximum-margin formulation turns into a convex quadratic program, with a clean geometric story.
The margin
For a hyperplane with unit normal , the signed distance from a point to the hyperplane is .
For labels , the geometric margin of a point is
It is positive when the point is on the correct side, negative otherwise. The margin of the dataset is , the distance to the closest point.
The maximum-margin problem
Maximizing the dataset margin subject to all points being correctly classified:
The objective is invariant under positive scaling of , so fix the scale by requiring the closest point to satisfy . Then , and maximizing is minimizing .
The scaling invariance is worth pinning down, because two "margins" are easy to conflate. The functional margin is the raw score; the geometric margin divides it by and is the actual distance. Take the hyperplane , and a point at : the functional margin is and the geometric margin is , a real distance of two units. Rescale to (same hyperplane, the line is unchanged): the functional margin balloons to , but the geometric margin is , unchanged. Only the geometric margin means anything; the functional one can be inflated to any value by scaling . Pinning the closest functional margin to removes that freedom, after which reads off the geometric margin directly: is a margin of , is a margin of .
The hard-margin SVM
The factor is for derivative cleanness. The objective is convex quadratic and the constraints are affine, so this is a convex quadratic program (QP). It has a unique global solution whenever the data are linearly separable; otherwise the constraint set is empty and the problem is infeasible (which is what the next lesson fixes with slack).
KKT conditions and support vectors
Form the Lagrangian with multipliers :
Stationarity gives
The first equation says the optimal is a linear combination of the training points (with -signed weights). Complementary slackness says unless the margin constraint is tight, i.e. .
Points with sit exactly on the margin boundary. They are the support vectors, and they are the only data points that contribute to . Everything else is irrelevant to the classifier; this is the SVM's celebrated sparsity-in-examples property[2].
What hard-margin can't handle
Three failure modes drive the next lessons:
- Nonseparable data. No feasible exists. Soft margin (next lesson) introduces slack.
- Outliers near the boundary. A single bad label forces the entire hyperplane to compensate, hurting generalization. Soft margin also fixes this.
- Nonlinear separators. The decision boundary is forced to be linear in . The kernel trick lifts the problem to a higher-dimensional space where linear separation suffices.
Check it on arrays
import numpy as np
rng = np.random.default_rng(0)
# Linearly separable 2D data
n_per = 30
X_pos = rng.normal(loc=[2.5, 1.0], scale=0.6, size=(n_per, 2))
X_neg = rng.normal(loc=[-1.0, -1.0], scale=0.6, size=(n_per, 2))
X = np.vstack([X_pos, X_neg])
y = np.array([1.0] * n_per + [-1.0] * n_per)
# Hard-margin SVM via projected gradient on the dual:
# max sum(alpha) - 0.5 alpha^T (y y^T * K) alpha s.t. alpha >= 0, sum(y * alpha) = 0
def hard_margin_dual(X, y, lr=1e-3, steps=20000):
K = X @ X.T # linear kernel
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.maximum(alpha, 0) # alpha >= 0
# project to satisfy sum(y * alpha) = 0
alpha -= y * (y @ alpha) / (y @ y)
alpha = np.maximum(alpha, 0)
return alpha
alpha = hard_margin_dual(X, y)
# Recover w and b from the support vectors
w = (alpha * y) @ X
sv_idx = np.where(alpha > 1e-4)[0]
print(f"support vectors: {len(sv_idx)} / {len(y)}")
b = np.mean(y[sv_idx] - X[sv_idx] @ w)
print(f"w = {w}, b = {b:.3f}")
# Margin and accuracy
margin = 1.0 / np.linalg.norm(w)
print(f"geometric margin = {margin:.3f}")
acc = np.mean(np.sign(X @ w + b) == y)
print(f"training accuracy = {acc:.3f}")
# The decision boundary passes between the two classes
print("min positive margin:", np.min(y * (X @ w + b)))The dual ascent identifies a handful of support vectors; the recovered weight vector classifies every training point correctly with a positive margin.
Exercises
Exercise 1. Two points lie on a line through the origin: with and with . Find the hard-margin solution and the geometric margin, by enforcing that both points sit on the margin boundary.
Solution
Step 1. By symmetry the separating hyperplane passes through the origin, so , and points along , i.e. for some .
Step 2. The scale-fixing convention sets the closest functional margin to . At : , so and .
Step 3. The geometric margin is
Both points are from the line , which matches: the distance from to that line is .
Exercise 2. From the stationarity condition and , prove that the squared norm equals , so the squared margin is . (Use that for support vectors .)
Solution
Step 1. Expand using :
Step 2. Only support vectors have , and each satisfies the tight constraint , hence . Substitute:
Step 3. The second sum is zero by the stationarity condition , so . The geometric margin is , so . A larger total multiplier mass means a smaller margin.
Exercise 3 (code). Take the two points of Exercise 1, run the projected- gradient dual ascent, and confirm it recovers , , a margin of , and equal multipliers .
Solution
import numpy as np
X = np.array([[1.0, 1.0], [-1.0, -1.0]])
y = np.array([1.0, -1.0])
def hard_margin_dual(X, y, lr=1e-3, steps=50000):
K = X @ X.T
Q = (y[:, None] * y[None, :]) * K
alpha = np.zeros(len(y))
for _ in range(steps):
grad = 1 - Q @ alpha
alpha = np.maximum(alpha + lr * grad, 0)
alpha -= y * (y @ alpha) / (y @ y) # project onto sum(y*alpha)=0
alpha = np.maximum(alpha, 0)
return alpha
alpha = hard_margin_dual(X, y)
w = (alpha * y) @ X
sv = alpha > 1e-4
b = np.mean(y[sv] - X[sv] @ w)
margin = 1.0 / np.linalg.norm(w)
print(f"w = {w}, b = {b:.3f}, margin = {margin:.4f}, alpha = {alpha}")
assert np.allclose(w, [0.5, 0.5], atol=1e-2)
assert np.isclose(margin, np.sqrt(2), atol=1e-2)
assert np.isclose(alpha[0], alpha[1], atol=1e-3) # symmetric multipliersThe dual reproduces the hand solution: equal multipliers on the two symmetric support vectors, weight , and margin .
The next lesson relaxes the hard constraints with slack variables so the SVM survives noisy and nonseparable data.
References
- [1]Boser, B. E., Guyon, I. M., & Vapnik, V. N. (1992). "A training algorithm for optimal margin classifiers". COLT. Introduces the maximum-margin classifier (doi:10.1145/130385.130401).↩
- [2]Vapnik, V. N. (1998). Statistical Learning Theory. Wiley. The foundational treatment of margins, support vectors, and generalization.↩