- Compute k-nearest-neighbour predictions under different distance metrics and explain why feature scaling and the choice of k matter.
- Explain the curse of dimensionality and quantify how neighbourhoods and distances behave in high dimensions.
- Derive the naive Bayes decision rule, apply Laplace smoothing, and classify a text example by hand in probability and log space.
- Formulate the soft-margin SVM with hinge loss, compute margins and losses, and explain the kernel trick and the roles of C and gamma.
- Choose among kNN, naive Bayes, SVMs and logistic regression for a given problem and justify the choice.
Before this: The ML Workflow: Framing, Data, Splits and Leakage, Linear and Logistic Regression, Probability and Statistics for AI
Before deep learning and gradient boosting, three classifiers carried much of applied machine learning: k-nearest neighbours, naive Bayes and support vector machines. They are still worth mastering. k-nearest neighbours is the conceptual ancestor of today's vector search and retrieval-augmented generation; naive Bayes is a fast, strong baseline for text that trains in milliseconds; and support vector machines introduced the margin, hinge loss and kernel ideas that run throughout modern ML. Each embodies a different philosophy, and knowing when each fits is part of a practitioner's toolkit.
Three philosophies in one picture
Imagine sorting fruit at a market stall.
- k-nearest neighbours keeps every fruit it has ever seen. Given a new one, it finds the most similar fruits on the table and goes with the majority. No model, just memory and a notion of "similar".
- Naive Bayes keeps a tally per fruit type: how often oranges are orange, round, heavy. For a new fruit it asks which type would most probably produce these characteristics, pretending each characteristic is independent of the others given the type.
- A support vector machine draws the widest possible street between apples and oranges on the table, and only the fruits standing at the curb (the support vectors) determine where the street runs.
k-nearest neighbours (kNN)
The algorithm
Store the training set . To classify a query :
- Compute the distance to every training point.
- Take the set of the closest.
- Predict the majority label among them (classification), or their average target (regression). Class probabilities are the label fractions within .
A variant weights each neighbour's vote by , so closer points count more. There is no training step beyond storing data (kNN is a lazy or instance-based learner), so all the cost moves to prediction: a naive query is . Tree indexes (KD-trees, ball trees) speed this up in low dimensions, and approximate nearest-neighbour indexes (such as HNSW graphs) make it practical for millions of high-dimensional vectors; this is exactly the machinery inside vector databases (see vector-databases-and-indexing).
Cover and Hart (1967) proved a classic guarantee: as the training set grows, the error rate of the 1-nearest-neighbour rule is at most twice the Bayes error rate, the lowest error achievable by any classifier on that problem. That is a striking result for such a simple method, but it is asymptotic; with finite data in many dimensions kNN can do badly, as we will see.
Distance metrics
For vectors , the Minkowski distance of order is
is Euclidean distance (straight line); is Manhattan distance (sum of absolute differences), somewhat more robust to a single large coordinate difference. For text and embeddings, cosine distance compares direction and ignores length. For categorical data, Hamming distance counts mismatches. The metric encodes your notion of similarity, so choose it deliberately.
Worked example
Training points: red, red, blue, blue, blue. Query .
| Point | Euclidean distance | Manhattan distance | Label |
|---|---|---|---|
| B | 1 | red | |
| C | 2 | blue | |
| D | 2 | blue | |
| A | 4 | red | |
| E | 3 | blue |
With the prediction is red (B). With (B, C, D) it is blue, with estimated probability . The choice of is a bias-variance knob: small gives jagged, high-variance boundaries that react to individual noisy points; large gives smooth, high-bias boundaries. Choose it by cross-validation, and prefer odd in binary problems to avoid ties.
Feature scaling is mandatory
Distances add up differences across features, so a feature measured in large units swamps the others. Query: age 30, income $50,000. Neighbour P1: age 31, income $52,000. Neighbour P2: age 60, income $50,500. Unscaled Euclidean distances: P1 ; P2 . P2 is "nearer" despite a 30-year age gap, because income dominates. After standardising (say age standard deviation 10 years, income $20,000): P1 ; P2 . Now P1 is correctly the near neighbour. Standardise (or otherwise scale) features inside a pipeline before any distance-based method.
The curse of dimensionality
In high dimensions, intuitions about "near" break down, for two reasons.
First, neighbourhoods stop being local. Suppose data are spread uniformly in the unit hypercube and you want a neighbourhood that captures 10% of the data. A sub-cube with that volume needs edge length : 0.1 when , 0.32 when , 0.79 when and 0.98 when . To capture even 10% of the points you must span almost the whole range of every feature, so the "neighbours" are not similar at all.
Second, distances concentrate: the nearest and farthest points become almost equally far away. This short simulation measures the ratio of the nearest to the farthest distance from a random query to 1,000 random points:
import numpy as np
rng = np.random.default_rng(0)
for d in [2, 10, 100, 1000]:
X = rng.uniform(size=(1000, d))
q = rng.uniform(size=d)
dist = np.linalg.norm(X - q, axis=1)
print(f"d={d:5d} nearest/farthest distance ratio = {dist.min() / dist.max():.3f}")
The ratio climbs from about 0.01 in 2 dimensions to about 0.22 at 10, 0.72 at 100 and 0.90 at 1,000. When everything is roughly equally far, "nearest" carries little information. kNN works in practice in high dimensions only when the data actually lie near a lower-dimensional structure (as images and learned embeddings do), or after dimensionality reduction or a learned embedding has done the hard work.
Naive Bayes
The model
Bayes' theorem gives the posterior probability of class given features :
Estimating for all combinations of features is hopeless. The naive assumption is that features are conditionally independent given the class:
We work with sums of logs because a product of hundreds of small probabilities underflows to zero in floating point. Each factor is estimated by counting, so training is a single pass over the data.
The variants differ in how they model :
- Multinomial NB: features are counts (word counts in a document). is the share of all word tokens in class that are .
- Bernoulli NB: features are binary (word present or absent), and absence is also evidence.
- Gaussian NB: continuous features, each modelled as a normal distribution with a per-class mean and variance.
Laplace smoothing
If a word never appeared in spam during training, its estimated probability is 0, and a single occurrence would veto the spam class no matter what else the email says. Additive (Laplace) smoothing adds a pseudo-count (usually 1) to every word:
where is the vocabulary size.
Worked example: a tiny spam filter
Vocabulary , so . Training data: 4 spam and 6 ham (legitimate) emails, so the priors are and . Word counts:
| Word | Spam count | Ham count | , | , |
|---|---|---|---|---|
| free | 8 | 2 | ||
| win | 5 | 1 | ||
| meeting | 1 | 9 | ||
| report | 2 | 12 | ||
| total | 16 | 24 | denominator | denominator |
New email: "free win meeting".
- Spam score: .
- Ham score: .
- Normalising: .
In log space, the same computation compares with for ham; spam wins. "free" and "win" each favour spam by a factor of about 4, and "meeting" favours ham by about 3.6, not enough to overturn them. Note what the model ignores: word order, and the fact that "free" and "win" tend to co-occur. Because co-occurring words are counted as independent evidence, naive Bayes tends to produce overconfident probabilities (pushed toward 0 or 1), even when its ranking of emails is good. Use it for decisions or ranking, and calibrate before trusting its probabilities.
The same idea in scikit-learn, with real tokenisation:
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline
emails = ["win free money now", "free prize win win", "claim your free reward",
"meeting moved to monday", "quarterly report attached", "lunch meeting tomorrow?",
"please review the report", "free pizza at the team meeting"]
labels = [1, 1, 1, 0, 0, 0, 0, 0] # 1 = spam
nb = make_pipeline(CountVectorizer(), MultinomialNB(alpha=1.0)) # alpha = Laplace smoothing
nb.fit(emails, labels)
for msg in ["free money", "report for the meeting", "win a free meeting"]:
p = nb.predict_proba([msg])[0, 1]
print(f"{msg!r:28s} P(spam) = {p:.2f}")
This prints roughly 0.78, 0.03 and 0.69. Words never seen in training ("for", "a") are simply ignored by CountVectorizer. Eight emails is of course far too few for a real filter; production spam filtering today combines many signals and models, but naive Bayes on word counts remains a fast, hard-to-beat baseline for text classification.
Support vector machines (SVMs)
Maximum margin
Take binary labels and a linear classifier that predicts . If the classes are linearly separable, infinitely many hyperplanes separate them. The SVM picks the one with the largest margin, the distance from the hyperplane to the closest training points. Scaling and so that the closest points satisfy , that distance is and the full width of the "street" between the two margin hyperplanes is , and the hard-margin SVM solves
A wide margin means small changes in the data are less likely to flip predictions, which is the geometric intuition for good generalisation. The solution depends only on the points lying on the margin, the support vectors; you could delete every other training point and get the same classifier.
Soft margin and hinge loss
Real data overlap, so we allow violations. The soft-margin SVM (Cortes and Vapnik, 1995) minimises
The second term is the hinge loss: zero for points correctly classified with functional margin , growing linearly as a point enters the margin or crosses to the wrong side. Compare logistic regression, whose log-loss never reaches exactly zero; the hinge's flat zero region is why only some points (those on or inside the margin) end up as support vectors. trades margin width against violations: large punishes violations heavily (narrow margin, fits the training data closely, risk of overfitting); small tolerates them (wide margin, smoother boundary, more support vectors). plays the role of an inverse regularisation strength, like C in logistic regression.
Worked example. Take , , so the margin width is .
| Point | Label | Hinge loss | ||
|---|---|---|---|---|
| 3.0 | 3.0 | 0 (safely outside margin) | ||
| 1.0 | 1.0 | 0 (exactly on margin: support vector) | ||
| 0.5 | 0.5 | 0.5 (inside margin, correct side) | ||
| 2.0 | 3.0 (misclassified) |
Kernels
A linear boundary is often too simple. We could map inputs to a richer feature space and find a linear separator there, but explicit maps can be huge. The key fact is that the SVM's dual optimisation problem and its decision function use the data only through dot products:
where the are learned dual coefficients, non-zero only for support vectors. A kernel computes the dot product in feature space without building at all: the kernel trick. Common kernels:
- Linear: .
- Polynomial: , for degree .
- RBF (Gaussian): , corresponding to an infinite-dimensional feature space.
For instance, in two dimensions with : the kernel evaluates a three-dimensional dot product using a two-dimensional one.
For the RBF kernel, sets how far each support vector's influence reaches. Large means each point influences only a tiny neighbourhood: wiggly boundaries that can wrap around individual points (overfitting). Small means broad influence and nearly linear boundaries (underfitting). and interact, so tune them jointly on a logarithmic grid (for example to ) with cross-validation. RBF SVMs are distance-based, so feature scaling is essential.
Costs and practicalities
Kernel SVM training (scikit-learn's SVC, built on LIBSVM) scales at least quadratically with the number of samples, which becomes impractical beyond a few tens of thousands of rows. For large or very high-dimensional sparse data (text), use a linear SVM (LinearSVC or SGDClassifier with hinge loss), which scales roughly linearly. SVMs output scores, not probabilities; SVC(probability=True) fits an extra calibration step using internal cross-validation, which is slow and can disagree slightly with predict. Multiclass problems are handled by combining binary SVMs (scikit-learn's SVC uses one-vs-one internally).
Putting them side by side
The following compares the three on scikit-learn's wine dataset (178 wines, 13 chemical measurements on very different scales, 3 cultivars), with and without scaling:
from sklearn.datasets import load_wine
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
X, y = load_wine(return_X_y=True)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
models = {
"kNN, raw features": KNeighborsClassifier(n_neighbors=5),
"kNN, scaled": make_pipeline(StandardScaler(), KNeighborsClassifier(5)),
"Gaussian NB": GaussianNB(),
"SVM RBF, raw": SVC(kernel="rbf", C=1.0),
"SVM RBF, scaled": make_pipeline(StandardScaler(), SVC(kernel="rbf", C=1.0)),
"Logistic reg, scaled": make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)),
}
for name, m in models.items():
s = cross_val_score(m, X, y, cv=cv)
print(f"{name:21s} {s.mean():.3f} +/- {s.std():.3f}")
Typical results: kNN on raw features about 0.66, and 0.96 after scaling; the RBF SVM about 0.66 raw and 0.98 scaled; Gaussian naive Bayes about 0.97 (per-feature Gaussians are unaffected by scale); scaled logistic regression about 0.98. The lesson is less about which model wins on this small, easy dataset than about the 30-point swing that scaling causes for distance-based methods.
| kNN | Naive Bayes | SVM (kernel) | Logistic regression | |
|---|---|---|---|---|
| Training cost | None (store data) | One counting pass, very fast | At least quadratic in | Fast, iterative |
| Prediction cost | High: compare with stored points | Very low | Proportional to number of support vectors | Very low |
| Needs feature scaling | Yes | No | Yes | Yes, if regularised |
| Decision boundary | Arbitrary, local | Linear in log-space for multinomial/Bernoulli; quadratic for Gaussian | Linear or flexible (via kernel) | Linear |
| High-dimensional sparse data (text) | Poor with raw counts | Strong baseline | Linear SVM strong | Strong |
| Probability quality | Coarse (fractions of ) | Often overconfident | Not native; needs calibration | Usually reasonable |
| Key hyperparameters | , metric, weighting | Smoothing | , kernel, | (regularisation) |
| Best fit | Low-dimensional data, embeddings with an ANN index, simple baselines | Text, very small data, speed-critical baselines | Small-to-medium data with complex boundaries | Default linear baseline |
Common pitfalls
- Forgetting to scale features for kNN and SVMs, and fitting the scaler outside the cross-validation pipeline.
- Using kNN on raw high-dimensional features and blaming the algorithm when distances become meaningless.
- Choosing , or by test-set performance instead of cross-validation.
- Zero probabilities in naive Bayes from skipping smoothing, or multiplying probabilities instead of summing logs.
- Trusting naive Bayes probabilities as calibrated risks.
- Running kernel SVMs on hundreds of thousands of rows; use linear SVMs, kernel approximations or gradient boosting instead.
- Ignoring class imbalance: all three can be dominated by the majority class; use class weights (
class_weight="balanced"for SVMs) or appropriate priors and metrics.
In practice
- For text classification, start with TF-IDF features and a linear model (logistic regression or a linear SVM) plus multinomial naive Bayes as a baseline; then compare with fine-tuned language-model embeddings.
- For small tabular datasets with complex boundaries, a scaled RBF SVM tuned over and can still compete; on larger tabular data, gradient-boosted trees usually win.
- kNN's modern life is retrieval: with learned embeddings and approximate nearest-neighbour indexes, nearest-neighbour search powers semantic search, recommendation and RAG systems (
embeddings-and-rag). - Always compare against a logistic regression baseline under the same pipeline and cross-validation scheme, and report variability across folds.
- When a stakeholder needs probabilities, calibrate the chosen model and check calibration on held-out data (
model-evaluation-and-metrics).
Key takeaways
- kNN stores the data and votes among the k closest points; it needs scaled features and suffers in high dimensions unless the data lie near a low-dimensional structure.
- In high dimensions neighbourhoods stop being local and distances concentrate, which is the curse of dimensionality.
- Naive Bayes multiplies per-feature likelihoods under a conditional-independence assumption; it is fast and strong for text but its probabilities are usually overconfident.
- SVMs maximise the margin; hinge loss makes the solution depend only on support vectors, and C trades margin width against violations.
- Kernels let SVMs learn non-linear boundaries through dot products; the RBF gamma controls each support vector's reach and must be tuned jointly with C.
- Distance-based methods can lose tens of accuracy points without feature scaling inside a pipeline.
Terms in this lesson
- k-nearest neighbours
- A classifier that predicts the majority label (or mean target) of the k training points closest to the query.
- Minkowski distance
- The p-th root of the sum of absolute coordinate differences raised to the power p; p=2 is Euclidean and p=1 is Manhattan distance.
- Curse of dimensionality
- The collection of effects by which data become sparse and distances less informative as the number of dimensions grows.
- Bayes error rate
- The lowest error rate achievable by any classifier on a given data distribution.
- Naive Bayes
- A probabilistic classifier that applies Bayes' theorem assuming features are independent given the class.
- Laplace smoothing
- Adding a pseudo-count to every feature-class count so that unseen features never receive zero probability.
- Margin
- The distance from a classifier's decision boundary to the closest training points; for a scaled SVM it is 1/||w||, and the full width between the two margin hyperplanes is 2/||w||.
- Hinge loss
- The loss max(0, 1 - y f(x)), zero for points classified correctly with functional margin at least 1.
- Support vectors
- The training points on or inside the margin that alone determine an SVM's decision boundary.
- Kernel trick
- Computing dot products in a high-dimensional feature space through a kernel function, without constructing the feature vectors.
Check your understanding
- 1.
kNN with Euclidean distance is applied to features 'age in years' and 'annual income in dollars' without scaling. What happens?
- 2.
Data are uniform in a 10-dimensional unit hypercube. What edge length must a sub-cube have to contain 1% of the data on average?
- 3.
In a naive Bayes spam filter without smoothing, the word 'invoice' never appeared in any spam email during training. What happens when a new email containing 'invoice' is classified?
- 4.
An SVM has w = (3, 4) and b = -2. What is the margin width 2/||w||, and what is the hinge loss for the point x = (1, 0) with label y = +1?
- 5.
Your RBF SVM reaches 100% training accuracy but 70% cross-validated accuracy. Which change is most likely to help?
Exercises
Work each problem before opening the solution.
- Exercise 1Core
Using the training points from the lesson, red, red, blue, blue, blue, classify the query with and under Euclidean distance, and with under Manhattan distance.
Show solution
Euclidean distances from : ; ; ; ; .
- : nearest is B, so red.
- : B, A, C give red, red, blue, so red (probability 2/3).
Manhattan distances: , , , , . The three nearest are B (1), A (2), and then C and D tie at 4. Either way the vote is two red to one blue, so red. (Ties at the boundary of the neighbourhood are broken by implementation details, e.g. training order in scikit-learn; another reason to prefer settings that avoid them.)
- Exercise 2Core
Using the lesson's spam table (Laplace smoothing , priors 0.4 spam / 0.6 ham), compute . Explain the result in terms of the per-word likelihood ratios.
Show solution
Spam score: .
Ham score: .
, so the email is classified as ham, narrowly.
Likelihood ratios (spam : ham): "free" ; "report" . Their product is about 1.36, which favours spam, but the prior odds are . Posterior odds , consistent with 0.475.
- Exercise 3Core
Write a scikit-learn script that tunes a kNN classifier on the wine dataset over , uniform vs distance weighting, and Manhattan vs Euclidean distance, with scaling inside the pipeline and a held-out test set. Report the best settings, CV accuracy and test accuracy.
Show solution
from sklearn.datasets import load_wine from sklearn.model_selection import GridSearchCV, train_test_split from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsClassifier X, y = load_wine(return_X_y=True) X_tr, X_te, y_tr, y_te = train_test_split(X, y, stratify=y, random_state=0) pipe = make_pipeline(StandardScaler(), KNeighborsClassifier()) grid = {"kneighborsclassifier__n_neighbors": [1, 3, 5, 7, 9, 15, 25], "kneighborsclassifier__weights": ["uniform", "distance"], "kneighborsclassifier__p": [1, 2]} # p=1 Manhattan, p=2 Euclidean search = GridSearchCV(pipe, grid, cv=5).fit(X_tr, y_tr) print(search.best_params_, "CV acc %.3f" % search.best_score_) print("test acc %.3f" % search.score(X_te, y_te))In our run the search selected Manhattan distance with uniform weights and a fairly large (25), with CV accuracy about 0.96 and test accuracy about 0.98. Your exact selection may differ: many settings score within one or two points of each other on a dataset this small, so the tuned values are not strongly determined. Because the scaler sits inside the pipeline, it is re-fit on each training fold and the test set is used only once.
- Exercise 4Core
For the SVM with , , compute , the functional margin and the hinge loss for: with ; with ; with . Which are support vectors (on or inside the margin)?
Show solution
.
- , : , , loss . Outside the margin, not a support vector.
- , : , , loss . Correctly classified but inside the margin: a (margin-violating) support vector.
- , : , , loss . Misclassified: also a support vector.
In the soft-margin solution, every point with (inside the margin or misclassified) is a support vector with dual coefficient ; points exactly on the margin () have and are support vectors whenever , which is the usual case. Points with have .
- Exercise 5Challenge
(a) Show that for the kernel equals for . (b) Verify numerically for , , by hand and with
sklearn.metrics.pairwise.polynomial_kernel. (c) For degree in dimensions, the explicit feature map of has monomials. How many for , , and what does that say about the kernel trick?Show solution
(a) .
(b) , so . , ; dot product .
import numpy as np from sklearn.metrics.pairwise import polynomial_kernel def phi(v): # explicit feature map for (x . z)^2 in 2-D return np.array([v[0]**2, np.sqrt(2) * v[0] * v[1], v[1]**2]) x, z = np.array([1.0, 2.0]), np.array([3.0, 1.0]) print(phi(x) @ phi(z)) # 25.0 print(polynomial_kernel(x[None], z[None], degree=2, gamma=1, coef0=0)[0, 0]) # 25.0(scikit-learn's polynomial kernel is ; with , it is our kernel.)
(c) features. The kernel computes the same dot product with one 100-dimensional dot product and a cube, about 100 multiplications instead of building two 171,700-dimensional vectors. For the RBF kernel the feature space is infinite-dimensional, so the kernel trick is the only way to use it. The price is that kernel methods work with the matrix of pairwise kernel values, which is why they scale poorly with the number of samples.
Further reading
- paperCorinna Cortes and Vladimir Vapnik, Support-Vector Networks, Machine Learning 20 (1995)
- paperThomas M. Cover and Peter E. Hart, Nearest Neighbor Pattern Classification, IEEE Transactions on Information Theory (1967)
- courseTengyu Ma and Andrew Ng, CS229 Lecture Notes (Stanford): generative learning, naive Bayes, kernel methods and SVMs
- bookJames, Witten, Hastie, Tibshirani (and Taylor, Python ed.), An Introduction to Statistical Learning, ch. 2, 4 and 9
- docsscikit-learn developers, RBF SVM parameters (example on C and gamma)
Go further
Sources
Fact-checked and last reviewed 2026-11-05