Soft-margin SVM and hinge loss
Relax separability with slack variables, get the hinge-loss primal, and tune the regularization C.
Hard-margin SVM is infeasible on nonseparable data and brittle near outliers. The soft-margin SVM relaxes the constraints with slack variables, charging a penalty for every margin violation[1]. Rewriting the slack penalty as a loss gives the hinge loss, which is what production SVMs actually minimize.
Slack variables
For each example, introduce a nonnegative slack that absorbs any margin shortfall:
Three regimes:
- : the constraint holds with room to spare; the point sits outside or on the margin.
- : the point is inside the margin band but on the correct side.
- : the point is misclassified.
The total violation should be small.
The slack is the margin shortfall, so its size reads off directly from the signed margin , which is . Several points spanning the range, to fix what a unit of slack means:
- A confidently correct point at sits well outside the margin: . No penalty.
- A point right on the margin, : . The constraint binds but is not violated.
- A point inside the margin band but still on the correct side, : .
- A point on the decision boundary, : . Slack of exactly one marks the boundary between "inside the margin" and "misclassified".
- A point misclassified with room, : . Slack grows without bound as a point moves deeper onto the wrong side.
The term sums these, so the objective pays per unit of this shortfall.
The QP
The regularization knob trades margin width against constraint violation:
- Large : heavy penalty on violations, narrow margin, close to hard-margin.
- Small : tolerates violations, wider margin, more bias.
The problem is convex (quadratic objective + affine constraints).
Hinge loss: same problem, no slacks
Minimize over for fixed . The optimal slack is : just enough to satisfy the constraint, zero if not needed. Substituting,
The per-example loss
is the hinge loss. It is zero on correctly classified points outside the margin (), and grows linearly inside the margin and across the boundary.
The soft-margin SVM is regularized hinge-loss minimization, the same shape as L2-regularized logistic regression but with hinge instead of cross-entropy[2].
Convex but not differentiable
The hinge is not differentiable at (the kink). Standard remedies:
- Subgradient descent: replace the derivative at the kink with any value in the subdifferential (typically if else ).
- Proximal / smoothing: replace the hinge with a smoothed version (e.g. squared hinge).
- Dual / SMO: solve the dual QP, as in the original SVM literature.
For small to medium problems, the dual is standard; for large ones, stochastic subgradient descent on the primal (Pegasos) is the workhorse.
Choosing C
is the central hyperparameter. Cross-validate over on a log scale. Larger may help on clean, well-separated data; smaller helps when outliers or label noise are present. The trade is exactly the bias-variance tradeoff with a different parameterization: plays the role of in ridge.
Check it on arrays
import numpy as np
rng = np.random.default_rng(0)
# Nonseparable 2D data: two overlapping Gaussians
n = 200
X = np.vstack([rng.normal([0.8, 0], scale=1.0, size=(n//2, 2)),
rng.normal([-0.8, 0], scale=1.0, size=(n//2, 2))])
y = np.array([1.0] * (n//2) + [-1.0] * (n//2))
def soft_margin_svm(X, y, C=1.0, lr=0.01, steps=5000):
n, d = X.shape
w = np.zeros(d)
b = 0.0
for _ in range(steps):
margins = y * (X @ w + b)
viol = margins < 1.0 # which points violate
grad_w = w - C * (y[viol, None] * X[viol]).sum(axis=0)
grad_b = - C * y[viol].sum()
w -= lr * grad_w / n
b -= lr * grad_b / n
return w, b
# Sweep C: more weight on violations -> tighter margin
for C in [0.01, 1.0, 100.0]:
w, b = soft_margin_svm(X, y, C=C, lr=0.1)
margin = 1.0 / np.linalg.norm(w)
acc = np.mean(np.sign(X @ w + b) == y)
viol = np.mean(y * (X @ w + b) < 1.0)
print(f"C={C:>6}: margin={margin:.3f}, accuracy={acc:.3f}, fraction with violation={viol:.3f}")
# Verify the hinge-loss equivalence: optimal slack is max(0, 1 - y(w^T x + b))
w, b = soft_margin_svm(X, y, C=1.0, lr=0.1)
hinge_loss = np.maximum(0, 1 - y * (X @ w + b)).sum()
print(f"\ntotal hinge loss = {hinge_loss:.3f} (= sum of optimal slacks)")Larger tightens the margin and reduces the fraction of margin violations; the total hinge loss equals the sum of optimal slacks.
Exercises
Exercise 1. Four points have signed margins equal to , , , and . Compute the optimal slack for each, the per-example hinge loss, and classify each point as outside the margin, inside the band, on the boundary, or misclassified.
Solution
Step 1. The optimal slack is , which is also the hinge loss :
Step 2. Read off the regime from and :
- , : outside the margin (confidently correct).
- , : inside the band, still on the correct side ().
- , : on the decision boundary; slack of exactly is the inside/misclassified boundary.
- , : misclassified ().
The total hinge loss is .
Exercise 2. For fixed , the soft-margin objective in the slack is subject to with . Show the minimizer is , which is why the slack penalty collapses to the hinge loss.
Solution
Step 1. The cost is increasing in (since ), so the minimizer is the smallest that is still feasible.
Step 2. Feasibility requires both and , so the smallest feasible value is the larger of the two lower bounds: .
Step 3. Two cases confirm it. If , then and the binding bound is , so (no penalty). If , then is the binding bound, so . Both cases are summarized by , the hinge loss. Substituting into turns the constrained QP into unconstrained regularized hinge-loss minimization.
Exercise 3 (code). On two overlapping Gaussian classes, sweep with subgradient descent and confirm that the margin width is non-increasing in (larger penalizes violations harder, so the margin tightens).
Solution
import numpy as np
rng = np.random.default_rng(0)
n = 200
X = np.vstack([rng.normal([0.8, 0], 1.0, (n // 2, 2)),
rng.normal([-0.8, 0], 1.0, (n // 2, 2))])
y = np.array([1.0] * (n // 2) + [-1.0] * (n // 2))
def soft_margin_svm(X, y, C, lr=0.1, steps=5000):
n, d = X.shape
w = np.zeros(d)
b = 0.0
for _ in range(steps):
viol = y * (X @ w + b) < 1.0 # points inside the margin
grad_w = w - C * (y[viol, None] * X[viol]).sum(axis=0)
grad_b = -C * y[viol].sum()
w -= lr * grad_w / n
b -= lr * grad_b / n
return w, b
margins = []
for C in [0.01, 1.0, 100.0]:
w, b = soft_margin_svm(X, y, C)
margins.append(1.0 / np.linalg.norm(w))
print("margins by C:", [round(m, 3) for m in margins])
assert margins[0] >= margins[1] >= margins[2] # margin shrinks as C growsThe margin falls from about at to about at : tightening buys fewer violations at the cost of a narrower band, the bias-variance trade in SVM clothing.
The next lesson casts the SVM in its dual form, where the support-vector structure becomes explicit.
References
- [1]Cortes, C., & Vapnik, V. (1995). "Support-vector networks". Machine Learning, 20(3), 273-297. Introduces the soft-margin SVM with slack variables (doi:10.1007/BF00994018).↩
- [2]Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning (2nd ed.). Springer. §12.3 frames the SVM as penalized hinge-loss minimization (doi:10.1007/978-0-387-84858-7).↩