A Hands-On Look at the Soft-Margin SVM
Support Vector Machines are often introduced through the lens of the quadratic programming dual problem, but the primal formulation tells a much more intuitive story: it’s just an unconstrained (well, almost) optimization problem where we’re minimizing a hinge loss term balanced against an L2 regularization term. Today we’ll build this from scratch, watch gradient descent carve out an optimal decision boundary, and visualize the loss landscape in 3D.
The Objective Function
The soft-margin SVM primal objective for a binary classification problem with labels $y_i \in {-1, +1}$ is:
$$
\mathcal{L}(w, b) = \frac{1}{2}|w|^2 + C \sum_{i=1}^{n} \max\left(0,\ 1 - y_i(w^\top x_i + b)\right)
$$
Here:
- The first term, $\frac{1}{2}|w|^2$, is the L2 regularizer — it penalizes large weight vectors, which corresponds to maximizing the margin $\frac{2}{|w|}$ between the two classes.
- The second term is the hinge loss, which only penalizes points that are either misclassified or sitting inside the margin. Points correctly classified with enough margin contribute zero loss.
- $C$ controls the trade-off between margin width and classification error tolerance. Large $C$ pushes toward fewer margin violations (harder margin); small $C$ favors a wider margin at the cost of some misclassifications.
Since the hinge loss $\max(0, 1 - z)$ is not differentiable at $z = 1$, we can’t use plain gradient descent — but we can use subgradient descent, which picks a valid subgradient wherever the function is non-smooth. The subgradient of the objective with respect to $w$ and $b$ is:
$$
\nabla_w \mathcal{L} = w - C \sum_{i \in \mathcal{V}} y_i x_i, \qquad
\nabla_b \mathcal{L} = -C \sum_{i \in \mathcal{V}} y_i
$$
where $\mathcal{V} = { i : y_i(w^\top x_i + b) < 1 }$ is the set of points violating the margin.
The Example Problem
We’ll generate a 2D dataset of two overlapping Gaussian blobs, which forces the optimizer to genuinely trade off margin width against a handful of unavoidable violations — a much more interesting case than perfectly separable data.
Source Code
1 | import numpy as np |
Console Output
Optimal w: [ 2.32509315 -0.46864248] Optimal b: -0.0653 Final objective value: 17.9965 Margin width (2/||w||): 0.8432 Number of margin violations: 22 / 200 Number of misclassified points: 5 / 200
Code Walkthrough
Data generation and preprocessing. We use make_blobs with a fairly large cluster_std=1.8 so the two classes overlap somewhat — this is what makes soft-margin behavior (as opposed to hard-margin) actually necessary. Standardizing the features to zero mean and unit variance is important here: gradient-based optimization of the SVM objective converges much faster and more reliably on standardized inputs, since the regularization term $\frac{1}{2}|w|^2$ and the hinge term operate on comparable scales.
compute_loss. This directly implements $\mathcal{L}(w,b)$ from the formula above. The margin for every sample, $y_i(w^\top x_i + b)$, is computed in a single matrix-vector product X @ w, then combined with the hinge via np.maximum(0.0, 1.0 - margins) — no loop over individual samples.
compute_subgradient. This is the core of the optimization. Rather than looping over each sample and checking if margin < 1, we build a boolean mask violating over the entire array at once. Then X[violating] selects only the rows corresponding to margin-violating points, and (y[violating, None] * X[violating]).sum(axis=0) computes $\sum_{i \in \mathcal{V}} y_i x_i$ as a single reduction. This vectorization is what keeps the whole training loop fast even though we run 2000 iterations — there’s no per-sample Python-level iteration anywhere in the hot path.
train_svm. We use subgradient descent with momentum (momentum=0.9), which accumulates a velocity vector v_w/v_b instead of stepping directly along the raw subgradient. This significantly speeds up convergence compared to vanilla subgradient descent, because it dampens the oscillation that hinge-loss subgradients tend to cause near the optimum (since the active set $\mathcal{V}$ can flip abruptly from iteration to iteration). We record loss_history and w_history at every step so we can later visualize both convergence and the optimization path in weight space.
Post-training diagnostics. After training, we recompute the final margins to count how many points are margin violators (margin < 1) versus actually misclassified (margin < 0) — these are different things: a point can sit inside the margin and still be correctly classified. We also report the margin width $2/|w|$, which is the geometric quantity the regularization term is implicitly maximizing.
Visualizing the Results
Plot 1 — Decision boundary. The solid white line is the decision boundary $w^\top x + b = 0$; the dashed cyan and pink lines are the margin boundaries $w^\top x + b = \pm 1$. Points circled in yellow are margin violators — some of these are still correctly classified but sit too close to the boundary, while others have crossed to the wrong side entirely. The background shading shows the signed distance field.

Plot 2 — Convergence curve. This tracks $\mathcal{L}(w,b)$ over all 2000 iterations. Because momentum is used, the curve typically drops sharply in the first few hundred iterations and then flattens as the active constraint set stabilizes.

Plot 3 — 3D loss landscape. This is the most illuminating visualization: we fix $b$ at its converged value and sweep $w_1, w_2$ over a grid to render $\mathcal{L}(w_1, w_2, b^*)$ as a 3D surface. Because hinge loss is piecewise-linear and the regularizer is quadratic, the surface is convex but has visible creases where individual hinge terms switch from active to inactive. The green trajectory shows the actual path subgradient descent took through this landscape, sampled every 20 iterations, ending at the white marker — the converged optimum.

Interpreting the Trade-off
Try re-running the training with different values of C_value. Increasing C (e.g. to 10.0) will shrink the margin width and reduce the number of violations, since misclassification becomes more costly relative to margin size. Decreasing C (e.g. to 0.1) will widen the margin substantially and tolerate more violations — the regularization term starts to dominate. This single hyperparameter is the entire story of the bias-variance trade-off for SVMs, and watching the 3D surface’s minimum shift as C changes is a great way to build intuition for it.






















