Linear SVM with Subgradient Descent - Supervised Learning
Implement Linear SVM with Subgradient Descent in Python with NumPy. medium exercise with examples, hints, and instant feedback. No setup or signup required.
Linear SVM with Subgradient Descent
Train a linear Support Vector Machine by full-batch (sub)gradient descent on the L2-regularised hinge loss.
Background
An SVM looks for the hyperplane x · w + b = 0 with the widest margin between the classes. The soft-margin version trades margin width (small ||w||) against hinge-loss violations. The hinge max(0, 1 - margin) has a kink at margin 1, so we use a subgradient: the gradient of whichever piece is active. Uses the loss from Hinge Loss.
Objective
J(w, b) = (lam/2) * ||w||^2 + (1/m) * sum_i max(0, 1 - y_i * (X_i · w + b))
X_i: rowiofX, shape(n,);w: shape(n,);b: scalary_iin {-1, +1}lam(lambda_param): regularisation strength. The bias is not regularised.
Subgradient
Let V = { i : y_i * (X_i · w + b) < 1 } be the samples that violate the margin (strictly less than 1; a margin of exactly 1 counts as satisfied). Then
dJ/dw = lam * w - (1/m) * sum_{i in V} y_i * X_i
dJ/db = - (1/m) * sum_{i in V} y_i
The regularisation term lam * w appears once, whatever the samples do; samples outside V contribute nothing to the hinge part.
Algorithm
- Start from
w = zeros(n),b = 0. - Repeat
iterationstimes: compute all margins with the currentw, b, findV, compute both subgradients, thenw -= learning_rate * dJ/dwandb -= learning_rate * dJ/db.
Inputs
X: features, shape(m, n)y: labels in {-1, +1}, shape(m,)learning_rate,lambda_param: floats;iterations: int
Expected Return Format
A tuple (w, b):
w: NumPy array of shape(n,), rounded to 4 decimalsb: Python float, rounded to 4 decimals
Predict with np.sign(X @ w + b).
Constraints
- NumPy only, vectorised over samples (a boolean mask for
V); no sklearn. - This objective is the one minimised by
sklearn.linear_model.SGDClassifier(loss='hinge', penalty='l2', alpha=lam), but sklearn uses stochastic updates and a learning-rate schedule, so its numbers differ.LinearSVCusesC(roughly1 / (lam * m)) and the squared hinge by default.
Follow-up
- Why does the solution depend only on the support vectors?
- How would you swap the linear score for a kernel, and why is that natural in the dual but not in this primal formulation?
Examples
Input: linear_svm(np.array([[2, 1], [3, 2], [-2, -1], [-3, -2]]), np.array([1, 1, -1, -1]), 0.01, 0.01, 1000) Output: (array([0.4009, 0.2231]), 0.0)
The data are symmetric around the origin, so b stays 0; sign(X @ w) classifies all four points correctly.
Input: Same data, learning_rate=0.1, iterations=1 Output: (array([0.25, 0.15]), 0.0)
At w = 0 every margin is 0 < 1, so all samples are in V: dw = -(1/4) * sum(y_i X_i) = -[2.5, 1.5], db = 0.
Practice in Python
import numpy as np
def linear_svm(X, y, learning_rate=0.01, lambda_param=0.01, iterations=1000):
"""
Train a linear SVM by subgradient descent on
(lambda_param / 2) * ||w||^2 + mean(max(0, 1 - y * (X @ w + b))).
Args:
X: Features, shape (m, n)
y: Labels in {-1, +1}, shape (m,)
learning_rate: Step size
lambda_param: L2 regularization strength (bias not regularized)
iterations: Number of full-batch updates
Returns:
w: Learned weights, shape (n,), rounded to 4 decimals
b: Learned bias (Python float), rounded to 4 decimals
"""
m, n = X.shape
w = np.zeros(n)
b = 0.0
# Your gradient descent implementation here
pass
return np.round(w, 4), round(float(b), 4)
The interactive editor loads with Python and NumPy. Run your implementation against 5 tests, or use the progressive hints.