Module 5: Instance-based and probabilistic models 30 min6 min quick readClassical ML
Support vector machines
Find the separating boundary with the widest margin, trade margin for errors with C, and bend the boundary with kernels.
Before this: Logistic regression, Vectors and matrices
Quick read: the intuition, interactive visualization, summary, and interview questions. The math, implementation, engineering, and knowledge-check sections are hidden.
Mental model. Of all the lines that separate two classes, choose the one with the widest empty street around it. Only the points on the edge of the street (the support vectors) decide where it goes. Kernels let the street curve.
You will learn to
- Define the margin and support vectors, and compute the margin width from the weights.
- Explain the soft-margin trade-off controlled by C.
- Write the hinge loss and compare it with logistic loss.
- Explain the kernel trick and the RBF kernel's parameter.
- Decide when an SVM is a good choice in practice.
Why it matters. Linear SVMs are excellent on high-dimensional sparse data like TF-IDF text. Kernel SVMs dominated many benchmarks before deep learning and remain strong on small and medium datasets. The margin idea also explains why some models generalize better than others that fit the training data equally well.
1. Intuition
Many lines can separate two well-separated groups. A line that barely squeezes past some points will misclassify new points that land slightly off. A line with a wide buffer on both sides is more robust. The SVM maximizes that buffer, the margin.
Real data overlaps, so a soft margin allows some points inside the street or on the wrong side, at a cost. The hyperparameter C sets the price: large C insists on classifying training points correctly (narrow street, risk of overfitting); small C accepts some errors for a wider street (smoother, more regularized).
When no straight line works, the kernel trick computes similarities as if the data had been mapped into a richer feature space where a linear separator exists, without ever building that space explicitly.
2. Visualization

Synthetic data. Both linear models reach the same training accuracy (0.925); the wider margin is the more regularized choice.
3. The math
Symbols
| Symbol | Meaning |
|---|---|
| labels (SVMs use ±1) | |
| boundary | |
| slack: how far point violates the margin | |
| penalty per unit of slack | |
| kernel: similarity between two points | |
| RBF kernel width parameter |
Hard margin
The margin edges are . The distance between them is , so maximizing the margin means minimizing :
Soft margin and hinge loss
The hinge loss is zero for points correctly classified beyond the margin, so they have no influence. That is why only support vectors matter.
Kernels
The solution depends on the data only through dot products . Replacing them with a kernel gives a nonlinear boundary:
Large : each support vector influences only its immediate neighborhood (wiggly boundary, overfitting risk). Small : smooth boundary.
Worked example
A trained linear SVM has and .
- Margin width: .
- Point with : . , so it is outside the margin: hinge loss 0, not a support vector.
- Point with : . : misclassified, hinge loss .
- RBF similarity between and with : .
4. Implementation
Build it from scratch
The full script is code/05-instance-and-probabilistic/svm_scratch.py. It trains a linear SVM with plain NumPy by subgradient descent on the soft-margin objective, then checks it against scikit-learn. Production solvers use the dual form or coordinate descent instead, but they minimize the same objective.
Hinge loss and the objective. Each point pays . Points with sit outside the street and pay nothing; points inside the street or on the wrong side pay linearly. We write the soft margin as . This is the section 3 objective divided by , so it has the same minimizer when : large means small , a wider street with more violations.
def hinge(X, y, w, b):
margins = y * (X @ w + b) # y_i f(x_i): >= 1 means safely outside the street
return np.maximum(0.0, 1.0 - margins) # [n], zero for points beyond the margin
def objective(X, y, w, b, lam): # lambda/2 ||w||^2 + mean hinge
return 0.5 * lam * w @ w + hinge(X, y, w, b).mean()Predict: a correctly classified point has . What is its hinge loss? And if you multiply by 10, does the margin width grow or shrink?
The script's first print applies hinge to the two points of the worked example in section 3 (, ).
One subgradient step. The hinge has a corner at , so we use a subgradient. Points outside the margin contribute zero gradient; only the "active" violators pull on and . The regularizer shrinks every step. This is the support-vector idea in code: points that never become active never affect the answer.
def step(X, y, w, b, lam, lr):
active = y * (X @ w + b) < 1 # only margin violators push on w and b
grad_w = lam * w - (y[active, None] * X[active]).sum(axis=0) / len(y)
grad_b = -y[active].sum() / len(y)
return w - lr * grad_w, b - lr * grad_bPredict: you delete a training point that ends far outside the margin and retrain. Does the boundary move?
Train. Repeat the step with a learning rate that decays as . A fixed step would bounce around the corner of the hinge forever; the decay lets it settle. Labels must be , so the script converts scikit-learn's with 2 * y01 - 1.
def fit_linear_svm(X, y, lam=0.01, lr=0.5, epochs=3000):
w, b = np.zeros(X.shape[1]), 0.0
for t in range(epochs):
w, b = step(X, y, w, b, lam, lr / np.sqrt(1 + t)) # decaying step for a non-smooth loss
return w, bPredict: if you forget the label conversion and train on , what do the class-0 points contribute to grad_w?
The RBF kernel matrix. A kernel SVM never builds features; it needs only pairwise similarities. Expanding computes all pairs with one matrix product. Clipping at 0 removes tiny negative values from floating-point rounding.
def rbf_kernel(A, B, gamma=0.5):
sq = (A**2).sum(1)[:, None] + (B**2).sum(1)[None, :] - 2 * A @ B.T # ||a - b||^2 for all pairs
return np.exp(-gamma * np.maximum(sq, 0.0))
K = rbf_kernel(np.array([[0.0, 0.0], [1.0, 1.0], [3.0, 3.0]]), np.array([[0.0, 0.0]]))
print(f"\nRBF similarity to (0,0), gamma=0.5: {K.ravel().round(4)}")Predict: the kernel matrix for training points has how many entries? At in float64, about how many gigabytes is that?
Use the library
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC, LinearSVC
# Linear SVM: fast on large sparse data (e.g. TF-IDF)
text_clf = make_pipeline(TfidfVectorizer(), LinearSVC(C=1.0))
# RBF SVM: tune C and gamma together, always with scaling
rbf = make_pipeline(StandardScaler(), SVC(kernel="rbf"))
search = GridSearchCV(rbf, {"svc__C": [0.1, 1, 10, 100], "svc__gamma": [0.01, 0.1, 1]}, cv=5)
search.fit(X_train, y_train)TfidfVectorizer comes from sklearn.feature_extraction.text.
| Scratch | scikit-learn |
|---|---|
fit_linear_svm(..., lam=) | SVC(kernel="linear", C=1/(lam*n)); LinearSVC(C=) for large data |
w, b | coef_, intercept_ |
points with y * f(x) <= 1 | support_, support_vectors_ |
rbf_kernel(..., gamma=) | SVC(kernel="rbf", gamma=) |
One difference: LinearSVC by default uses the squared hinge and also regularizes the intercept, so its weights differ slightly from SVC(kernel="linear"). The script compares against SVC, which minimizes exactly our objective.
Run it
From guide/:
uv run code/05-instance-and-probabilistic/svm_scratch.pyKey output:
worked example: margin width 0.40, hinge losses [0. 1.2]
scratch: w=[1.439 0.334] b=0.042 objective 0.1531 train acc 0.935
sklearn: w=[1.439 0.334] b=0.042 objective 0.1531 train acc 0.935 support vectors 35
RBF similarity to (0,0), gamma=0.5: [1.000e+00 3.679e-01 1.000e-04]On this synthetic data the scratch weights match scikit-learn's to three decimals. The RBF line matches the worked example: for , and for . The figure script is code/05-instance-and-probabilistic/nb_svm.py.
5. Engineering
Good fits. Text classification with TF-IDF (linear SVM); small-to-medium datasets with complex boundaries (RBF); high-dimensional data where .
Poor fits. Very large for kernel SVMs: training scales between and and memory holds a kernel matrix. Use linear SVMs, approximate kernels, or gradient boosting instead.
Preprocessing. Scale features; SVMs use distances and dot products.
Probabilities. SVM outputs are margins, not probabilities. SVC(probability=True) fits a calibration step with extra cross-validation; or use CalibratedClassifierCV.
Watch outFailure modes. Unscaled features; untuned C and γ (defaults are rarely right); kernel SVMs on hundreds of thousands of rows; treating decision-function values as probabilities.
Common mistakes
- Tuning C without tuning γ (they interact).
- Using an RBF kernel on sparse text where linear works better and is much faster.
- Expecting interpretable weights from a kernel SVM.
6. Knowledge check
Quiz: Support vector machines
A linear SVM has . What is the margin width ?
Practice exercise. With and , compute the margin width and the hinge loss for , .
Solution
, so the margin is . ; , so hinge loss : correctly classified but inside the margin, so it is a support vector.
Implementation challenge. Implement a linear SVM with subgradient descent on the hinge loss plus and compare its accuracy and weights with LinearSVC.
Summary
- SVMs maximize the margin between classes; only support vectors define the boundary.
- C trades margin width against training errors; hinge loss ignores well-classified points.
- Kernels (RBF with width γ) give nonlinear boundaries using only pairwise similarities.
- Linear SVMs shine on sparse text; kernel SVMs don't scale to very large datasets; scale features and calibrate if you need probabilities.
Next: Decision trees
Related: Logistic regression · K-nearest neighbors · Model card: SVM
Go deeper, code-first (AI Engineering from Scratch): Convex Optimization
Parts of this lesson are adapted from AI Engineering from Scratch by Rohit Ghumare (MIT license, notice): Support Vector Machines.
Interview angle
What are support vectors, and why do only they determine the SVM?
Support vectors are the training points on or inside the margin, or misclassified: those with . The hinge loss is exactly zero for every point correctly classified beyond the margin, so those points contribute nothing to the objective or its gradient. You could move or delete them, as long as they don't cross the margin, and the solution wouldn't change. The dual form makes this explicit: , with for every non-support vector. Consequences: the model is defined by a subset of the data; prediction cost scales with the number of support vectors; and a large fraction of support vectors, from small or heavily overlapping classes, means slow prediction and hints that the classes are hard to separate.
Explain the kernel trick. Why not just compute the expanded features explicitly?
The SVM solution depends on the data only through dot products . A kernel computes a dot product in some feature space without ever constructing . A degree-3 polynomial kernel on features corresponds to roughly monomial features, yet costs only . The RBF kernel corresponds to an infinite-dimensional feature space, which can't be built explicitly at all. The cost moves elsewhere: the kernel matrix is , so training scales roughly between and , and each prediction needs a kernel evaluation against every support vector. For large , approximate the feature map instead (random Fourier features, Nyström) and train a fast linear model on it.
Your RBF SVM gets 100% training accuracy and 60% test accuracy. What do you tune?
That's overfitting, and in an RBF SVM it usually comes from too large, too large, or both. A large makes each support vector's influence very local, so the boundary wraps around individual training points; a quick check is whether nearly every training point is a support vector. A large penalizes every margin violation heavily, so the model contorts itself to classify noisy points. Fix: search and jointly on a log grid (say to ) with cross-validation, because they interact: a smaller often wants a larger . Also confirm features were scaled: RBF distances on unscaled data are dominated by one feature, and the default gamma="scale" assumes comparable feature scales. For high-dimensional sparse text, try a linear kernel first; it's often both more accurate and much faster.
A linear SVM has w = (3, 4) and b = -2. What's the margin width, how far is x = (1, 0.5) from the boundary, and what changes if you double w and b?
Margin width . At , the functional value is , and the geometric distance to the boundary is , so the point sits 0.4 beyond the margin edge, which is 0.2 from the boundary. Doubling to , leaves the boundary unchanged, but becomes 6 and the margin edges move closer: the width drops to . That's why the SVM fixes the scale with the constraint and minimizes : otherwise functional margins could be inflated for free. It's also why raw decision-function values aren't probabilities and aren't comparable across models.