The Widest Street, the Fewest Nails
There is a dataset of 119 points in two classes with an empty band in the middle. The course's bundled program, after training, announces: 16 of these 119 points are support vectors — the key points holding up the decision boundary.
I handed the same data to scikit-learn, turned the tolerance to its strictest, and forced it to produce the widest street in the textbook sense. It said: 2.
119 points, and only 2 nails actually determine the line. This is the most beautiful thing about support vector machines — and it is where the course itself never arrives.
Building the Street First
Two clumps of points, a line drawn between them. Infinitely many lines could work — which to choose?
Imagine the line as the centerline of a street, with each side of the street pressed against points of one class. The wider the street, the harder it is for a new point to fall into the wrong territory. The training data can only tell you so much; future points will wobble near the training points, and street width is the fault-tolerance buffer you build in advance. The SVM's answer: pick the line with the widest street.
How do you write "widest" mathematically? A line is w·x + b = 0, and a point's distance to it is |w·x + b| divided by the length of w. To compare street widths, first pin each edge of the street to its nearest points — that is, require every point to satisfy y·(w·x + b) ≥ 1. Under this constraint, the street width equals 2 divided by the length of w. The problem becomes: minimize the length of w while keeping all points outside the street.
This is a convex quadratic program. The benefit of convexity was covered in lesson 18: a single global optimum, no local traps, every starting point leads to the same answer. The points sitting on the street's edges — those with y·(w·x + b) exactly equal to 1 — are called support vectors. They are the only nails. Delete any point standing outside the street and the line doesn't move; nudge a nail and the line follows.
The textbook provides 50 lines of pure Python: gradient descent directly minimizing hinge loss — a loss whose rule is that a point standing far enough from the street edge pays nothing, and once it crosses, the fine rises linearly with depth. No QP solver needed. To be fair first: those 50 lines are correct. On the same data and parameters, the hand-written version aligns with the industrial library rung by rung: sweeping C from 0.001 to 100, the hand-written street widths are 6.41, 2.99, 1.74, 1.45, 1.39; scikit-learn gives 6.44, 2.99, 1.69, 1.45, 1.40 — glued together the whole way. Reaching this level in an afternoon means the foundation is solid.
The trouble is in the story it tells itself.
The C Knob Doesn't Turn What It Claims
Real data is rarely cleanly separable. A few points always stand on the wrong side or shrink inside the street. Hence soft margin: give every point an overstay permit, tally the depth of the overstay, multiply by a price C. The textbook's table says: large C, harsh penalty, narrow street, fewer misclassifications, risk of overfitting; small C, loose management, wide street, more misclassifications, risk of underfitting.
On its own demo data, both ends of this sentence are reversed.
The demo program generates 300 noisy points, sweeps six C values, and prints a table: at C=0.001, street width 6.28, training accuracy 0.9042 — the highest of the six; test accuracy 0.8667 — the lowest of the six. At C=100, street width 1.39, training 0.8875, test 0.9000 — the highest of the six. Right below the table, the summary says: small C misclassifies more and generalizes better; large C misclassifies less and risks overfitting. The summary contradicts the table directly above it, printed by the same program on the same data.
Fearing a one-off fluke, I reran with ten random seeds. The direction was stable: small C's training accuracy was higher every time, averaging 0.9008 vs 0.8879; test accuracy was lower every time, 0.8683 vs 0.9000. Cross-checked with an exact solver: C=0.001 misclassifies 24 points, C=100 misclassifies 27. "Larger C misclassifies less" does not hold on this data.
So what does C actually optimize? Sum up each point's overstay depth — exactly the amount on the violation ticket: at C=0.001 it is 135.4, at C=0.01 it is 79.6, at C=0.1 it is 63.2, at C=100 it is 61.2. Monotonically decreasing, never turning back. Meanwhile, the misclassification count sits motionless between 24 and 27.
The C knob doesn't turn "how many misclassifications" — it turns "how deep the violations." On clean, separable data the two rise and fall together: as the street narrows, points that no longer fit get pushed across the line and become misclassifications. On noisy data they diverge. Pushing three points entirely across the line is cheaper on the ticket than letting six points sink deep into the street. The textbook's table describes the laws of the former world; the demo's data lives in the latter.
Nails Stand Inside the Street, Not Only on the Edge
A deeper problem lies in the definition of the nails.
The textbook says support vectors are "points sitting exactly on the street's edge." That definition belongs to the hard-margin world. Turn on soft margin and the landscape changes. Behind every point hides a number α, understandable as the force with which that point pulls on the line: if α is zero, the point lets go entirely. It can be proven rigorously that α > 0 if and only if the point satisfies y·(w·x + b) ≤ 1. Points on the street's edge, points allowed to shrink into the street, and points standing outright on the wrong side all carry non-zero force — all pull on the line. The nails aren't only on the edge; a batch of them stands inside the street.
The course's counting method only counts the former: open a thin band around y·(w·x + b) = 1 and count what falls in it. At the C=0.001 row, it counts 64. I counted from scratch with y ≤ 1 instead: 229 of 240 training points. That 6.28-wide street swallows 95% of the training set. scikit-learn's support vector count at the same setting: 230. Two independent implementations agree the true answer is around 229. The course's printed 64 is less than a third of the real number — the batch standing inside the street is missed entirely.
This isn't a matter of miscounting one number. The support vector count matters because it bounds the generalization error: the leave-one-out error rate cannot exceed the fraction of support vectors in the data. With a miscount this large, that bound is off the table.
One more detail worth savoring. The textbook's hands-on section has two steps: step 2 trains a model with gradient descent; step 4 finds support vectors, with tolerance set to 0.001. Run these two steps together and you get an empty list — not a single nail. The program doesn't crash; the geometry doesn't match: in the soft-margin solution, the nearest point stands at y·f(x) = 0.69, six points shrink inside the street, the rest are outside — but none equals exactly 1. "Sitting on the street's edge" is a hard-margin phenomenon — in that solution, the nearest points are pinned precisely at 1. The repository's full code quietly loosens the tolerance to 0.1, 0.3, 0.5 before it can count anything; the 0.001 version printed in the text, run on the model trained in the text's own step 2, finds zero nails.
The Printed Street Width Isn't That Street
Reading on, the "margin width" printed at the end of the program also deserves a second look. It prints 2 divided by the length of w. The premise for this equaling the street width is still "the nearest point sits exactly on the edge."
First demo, measured: it prints 0.906. How wide is the empty street this solution actually leaves? The nearest positive-class point is 0.313 from the centerline; the nearest negative-class point is 0.429; summing: 0.742 — 18% narrower than printed. If this same data were trained as a hard-margin problem, the best achievable street is 0.778, with exactly 3 nails. Three numbers — 0.906, 0.742, 0.778 — measure three different things, and the program prints the most optimistic one. The second dataset is blunter: by its printed weights, the nominal street width is 1.57; the true hard-margin solution is 1.29. Its own table contains a point with y·f(x) = 0.656 — live evidence that "this street isn't actually that wide" — but the program never looks there.
The Good Part: Hiding High Dimensions in the Dot Product
The good part of this course also deserves a full airing.
SVM has a famous rewriting: converting the optimization problem to its equivalent dual form, after which the entire problem sees the data only through dot products, x_i·x_j, and nothing else. Someone then realized: swap the dot product for an arbitrary two-argument function K, and you've secretly swapped out the underlying geometry. The polynomial kernel (x·z + c)^d implicitly sends the data into a space whose dimension explodes with the d-th power, while the computation itself stays at the original cost; the RBF kernel corresponds outright to an infinite-dimensional space. You never actually visit that space — you only need inner-product values within it, and the kernel function computes them in one step.
The demo data is two concentric circles. A linear SVM's test accuracy is 0.55 — a coin flip. Manually add three features — x₁ squared, x₂ squared, and their product (a circle's equation is a linear combination of exactly these) — and rerun the same linear SVM: 0.90. What the kernel trick does is hide this "feature engineering on your behalf" inside the dot product, automated.
Tied Rivals, Different Luggage
Finally, the comparison with logistic regression. On the same data, both tie exactly in test accuracy: 0.9250 vs 0.9250. The difference is in the luggage: by the correct counting method, the SVM's solution is propped up by 22 points; logistic regression's luggage carries all 160. A side correction to the textbook: it says SVM prediction saves memory because it "only stores support vectors" — true for kernelized SVMs; a linear SVM's prediction needs only the two numbers w and b, doesn't even need those 22 points, and saves more than it claims.
The text's exercise 2 asks you to sweep C from 0.001 to 1000 and watch the street narrow. Worth doing one extra step: at each C, count how many points stand inside the street — using y ≤ 1, not the band. You'll see that at C=0.001, 229 of 240 points shrink inside the street; at C=100, only 62 remain. A street that once swallowed the entire dataset narrows until it wraps just a quarter of the points. C's whole story lives between the numbers 229 and 62.