k-Nearest Neighbors
- Proposed by Fix and Hodges in 1951, one of the oldest non-parametric classifiers
- No training phase - all computation happens at prediction time
- Universally consistent: as data grows, error approaches Bayes-optimal
K-nearest neighbors classifies a new point by finding the k most similar training examples and taking a majority vote among their labels (or averaging for regression). No model parameters are learned - the training data IS the model.
- Motivates why distance metrics and feature scaling matter enormously
- The curse of dimensionality is most viscerally understood via kNN degradation in high dimensions
- Connects to locality-sensitive hashing and approximate nearest-neighbor search in production systems
- State the k-NN classification and regression rules and the role of the distance metric.
- Recognize the curse of dimensionality via the concentration-of-distances argument.
- Reproduce the Cover-Hart bound R_1NN <= 2 R*(1 - R*) and explain its consequences.
- Explain when KD-trees, ball trees, and HNSW each pay off, and when none of them do.
- Implement brute-force k-NN in NumPy and call it through sklearn or FAISS at scale.
TL;DR
k-NN is a non-parametric classifier: given a query , find the training points closest under a chosen metric and take their majority vote (classification) or mean (regression). It has zero training cost and inference, so it is the textbook lazy learner. Cover and Hart proved that the asymptotic 1-NN error is at most twice the Bayes error [2]. The whole approach breaks under the curse of dimensionality - distances concentrate, and “nearest” loses meaning - which is why production systems use approximate methods like LSH [3], HNSW [4], and FAISS for large-scale retrieval.
Intuition
The k-NN rule is the simplest learning algorithm that works. To classify a new point, look at its closest training neighbours and vote. To regress, average their targets. There is no model to fit, no parameters to optimize: the training set is the model. That makes k-NN powerful (it can represent any decision boundary if you have enough data) and brittle (every prediction touches the entire training set, and it falls apart in high dimensions).
Three knobs control the behaviour:
- The distance metric . Euclidean is the default, but Manhattan, cosine, and Mahalanobis are all useful when the geometry matches the data.
- The number of neighbours . Small has low bias and high variance (jagged boundary); large smooths the boundary and adds bias. Choose by cross-validation.
- The vote weighting. Uniform votes treat all neighbours equally; inverse-distance and Gaussian-kernel weights down-weight far-away neighbours.
k-NN does not learn a model of the data; it memorizes the training set and defers all work to query time. Its decision boundary at any test point is the locally majority-voted class within the -th nearest ball, which is a piecewise constant function of . As grows, the boundary smooths; as grows, it converges to the Bayes-optimal boundary almost everywhere. This is why k-NN is consistent - it eventually gets every example right, given enough data [1].
Distance metrics
| Metric | Formula | When it fits |
|---|---|---|
| Euclidean (L2) | sqrt(sum_i (x_i - y_i)^2) | Continuous features on comparable scales; the default. |
| Manhattan (L1) | sum_i |x_i - y_i| | Robust to a few large per-feature deviations; grids, taxicab geometry. |
| Mahalanobis | sqrt((x - y)^T Sigma^{-1} (x - y)) | Correlated or differently-scaled features; whitens the input space using the inverse covariance. |
| Cosine | 1 - (x . y) / (||x|| ||y||) | High-dimensional sparse vectors (text, embeddings) where direction matters more than magnitude. |
| Hamming | count of positions where x_i != y_i | Categorical or binary features. |
The Mahalanobis metric is worth pausing on. With , distance is the Euclidean distance after whitening the data with . Equivalently, you map each axis to its z-score and decorrelate. This is the right metric whenever your features are on different scales or correlated - e.g. height-and-weight pairs - and it is the unique linear distance that makes “one standard deviation” mean the same thing along every direction.
Math
Curse of dimensionality: distances concentrate
A surprising fact about high-dimensional metric spaces: pairwise distances concentrate around a common value, so “nearest” stops carrying information. Concretely, suppose are i.i.d. samples in with i.i.d. coordinates of finite variance. For a fixed query , define
Beyer, Goldstein, Ramakrishnan, and Shaft (1999) showed that under mild moment conditions on the coordinate distribution,
That is, the contrast ratio between the farthest and the nearest training point shrinks towards zero. In high dimensions everything is roughly equidistant from your query, and any nearest-neighbour query is essentially picking a random point.
A back-of-the-envelope reason: if each coordinate contributes independent noise of variance , then is a sum of i.i.d. terms with mean and variance . By the central limit theorem, , so distances cluster around with relative width . Doubling shrinks the relative gap between nearest and farthest by a factor of .
The practical consequence: brute-force k-NN on raw, very-high-dimensional features (e.g. 4096-dim image embeddings) returns a “nearest neighbour” whose distance is barely smaller than the median distance - the lookup carries little signal. The fix is one of: dimensionality reduction (PCA, autoencoder), a learned distance (metric learning), or an approximate method that admits the loss of contrast and uses fewer probes [1][3].
The Cover-Hart 1-NN bound
Statement
Let be the Bayes error rate - the minimum achievable error of any classifier - and let be the asymptotic () error rate of the 1-nearest-neighbour rule. Then for two-class problems,
In words: in the limit of infinite data, 1-NN’s error is at most twice the Bayes error. For multi-class problems with classes the bound generalizes to [2].
Proof sketch (two-class case)
Fix a query and let . The Bayes-optimal rule predicts with conditional error
Now consider 1-NN. Let be the nearest training point to and its label. As , almost-sure convergence of to implies . The 1-NN classifier predicts . Conditional on and , is Bernoulli and the true label is independently Bernoulli. So 1-NN errs iff :
Taking the limit ,
Use when to get
Take expectations over and apply Jensen’s inequality (the function is concave) to obtain . The lower bound holds because no classifier beats the Bayes rule. ∎
The bound is tight in the following sense. The first inequality is achieved with equality whenever is a.s. constant on the support of the input distribution, because Jensen’s inequality holds with equality on a constant. Equivalently, the condition is that a.s. for some . The second inequality becomes tight as . In practice 1-NN often does better than but never asymptotically worse.
Why helps
For neighbours with and , the k-NN rule is Bayes-consistent: . The intuition is that neighbours are an empirical estimate of in a shrinking ball around , and a majority vote over Bernoulli trials of mean converges to as . So at infinite , -NN with the right rate of is as good as the Bayes rule, while 1-NN pays a factor-of-2 surcharge for being maximally noisy [2][1].
Choosing k
There is no formula for . The standard recipe:
- Hold out a validation set, or use -fold cross-validation.
- Try a grid of - odd values for binary classification to avoid ties.
- Pick the that minimizes validation error (or maximizes whatever objective you care about: accuracy, AUC, log-loss).
A useful sanity check is the bias-variance picture: is high-variance, low-bias; predicts the global majority class with low variance and maximum bias. The sweet spot trades these off.
Weighted kNN
Instead of a uniform vote, weight neighbour by (inverse distance) or by a Gaussian kernel . This is the bridge from k-NN to kernel density estimation and Nadaraya-Watson regression [1]: as you recover hard k-NN; as you recover global mean. With weights, ties are rarer and the boundary is smoother.
Fast lookup at scale
| Method | Build cost | Query cost | Exact? | Best dimensionality | Notes |
|---|---|---|---|---|---|
| Brute force | O(n d) (just store the data) | O(n d) per query | yes | any | The reference. Always correct, hopeless past a few million points. |
| KD-tree | O(n d log n) | O(log n) average; O(n) worst case | yes | low (d < ~20) | Splits axes one at a time. Degrades sharply once d grows because the bounding boxes overlap most of space. |
| Ball tree | O(n d log n) | O(log n) average; O(n) worst case | yes | low to mid (d < ~30) | Hyperspheres instead of axis-aligned boxes. Better for non-axis-aligned clusters than KD-trees, still beaten in high d. |
| LSH (Indyk-Motwani) | O(n L k_h) | O(L) hash lookups + O(L) candidates per query | no (probabilistic) | mid to high | Hash colliding points to the same bucket with high probability. Trades off accuracy for speed via L hash tables. |
| HNSW | O(n log n) | O(log n) hops on a navigable small-world graph | no (high recall) | mid to high | State-of-the-art ANN graph. Very high recall at sub-linear query cost. |
| IVF / FAISS / ScaNN | O(n) | O(n / nlist + nprobe) | no | mid to high | Inverted lists from k-means coarse quantization, often combined with PQ for memory. |
KD-trees and ball trees are exact and wonderful in low dimensions. Past about
, the bounding regions of sibling nodes overlap so heavily that the
tree barely prunes the search and the algorithm degenerates to brute force.
The empirical rule of thumb in sklearn is to switch to brute force above
. For high-dimensional embeddings, use approximate methods
(LSH, HNSW, FAISS) or reduce dimensionality first.
Algorithm
# Brute-force k-NN classification (pseudocode)
def knn_predict(X_train, y_train, X_query, k=5):
preds = []
for q in X_query:
# 1) compute distances to every training point
d = euclidean(X_train, q) # shape (n,)
# 2) take the k smallest
idx = argpartition(d, k)[:k]
# 3) majority vote over their labels
preds.append(majority(y_train[idx]))
return preds
# Weighted variant: replace majority(...) with argmax over class-weighted sums.Implementations
import numpy as np
from collections import Counter
def knn_predict(X_train, y_train, X_query, k=5):
"""Brute-force k-NN classification with Euclidean distance."""
# squared distances via the (a-b)^2 = a^2 - 2 a.b + b^2 trick (vectorized)
train_sq = (X_train ** 2).sum(axis=1) # (n,)
query_sq = (X_query ** 2).sum(axis=1) # (m,)
cross = X_query @ X_train.T # (m, n)
d2 = query_sq[:, None] - 2 * cross + train_sq[None, :] # (m, n)
nn = np.argpartition(d2, kth=k - 1, axis=1)[:, :k] # (m, k) indices
preds = []
for row in nn:
vote = Counter(y_train[row]).most_common(1)[0][0]
preds.append(vote)
return np.array(preds)
# tiny synthetic 2D dataset with two overlapping classes
rng = np.random.default_rng(0)
X_pos = rng.normal(loc=[+1, +1], scale=0.7, size=(60, 2))
X_neg = rng.normal(loc=[-1, -1], scale=0.7, size=(60, 2))
X = np.vstack([X_pos, X_neg]); y = np.array([1] * 60 + [0] * 60)
# 80/20 split
perm = rng.permutation(len(y))
X, y = X[perm], y[perm]
n_tr = int(0.8 * len(y))
X_tr, y_tr = X[:n_tr], y[:n_tr]
X_te, y_te = X[n_tr:], y[n_tr:]
for k in (1, 3, 5, 10):
yhat = knn_predict(X_tr, y_tr, X_te, k=k)
acc = float((yhat == y_te).mean())
print(f"k={k:2d} test accuracy = {acc:.3f}")Run the from-scratch NumPy brute-force kNN live in the browser:
(no output yet)
How it compares to nearby classifiers
| Method | Training cost | Inference cost | Parametric? | Non-linear boundary? | Interpretable? | Notes |
|---|---|---|---|---|---|---|
| k-NN | O(1) (just store data) | O(n d) per query, brute force | no (lazy learner) | yes (any boundary in the limit) | low (no learned weights) | Memory-bound. Use ANN at scale. |
| Logistic regression | O(n d) per IRLS iter | O(d) per query | yes | no (linear boundary in feature space) | high (per-feature coefficients) | Fast inference; needs feature engineering for non-linear boundaries. |
| Decision tree | O(n d log n) | O(depth) | no (non-parametric in depth) | yes (axis-aligned) | high (path-as-rule) | No distance metric needed; handles mixed types naturally. |
Real-world uses
| Domain | System | Why nearest neighbour |
|---|---|---|
| Image / video retrieval | Reverse-image search, content-id, fingerprinting | Embed once, then nearest-neighbour over the embedding space; FAISS / ScaNN handles billions of vectors. |
| Recommender systems | Two-tower candidate generation | Embed users and items; ANN over item embeddings with the user embedding as query produces the candidate set in O(log n). |
| Anomaly detection | kNN-distance score (LOF, kNN-graph) | Points whose k-th nearest neighbour distance is unusually large are flagged as outliers. |
| Computer vision | k-NN on deep features (e.g. perceptual loss, retrieval baselines) | Cheap baseline that surprisingly often matches a fine-tuned classifier on out-of-distribution settings. |
| NLP | kNN-LM, kNN-MT (retrieval-augmented language models) | A large embedded datastore is queried at inference to inject memorized examples into the model output. |
Pitfalls
Brute-force k-NN on raw, very-high-dimensional features fails to discriminate because the contrast ratio between far and near collapses. Either reduce dimensionality (PCA, UMAP, an autoencoder), learn a metric, or switch to ANN methods that admit the loss of contrast.
A feature in metres dominates a feature in millimetres by a factor of a million in squared Euclidean distance. Always standardize (zero mean, unit variance) or use Mahalanobis to put features on a comparable footing.
There is no model to ship - the whole training set has to live somewhere. For 1M 1024-dim float32 vectors that is 4 GB. Production deployments use quantization (PQ, OPQ), inverted-file indices (IVF), or hierarchical graphs (HNSW) to fit and query within latency budgets.
It is the textbook baseline classifier. Run it on your problem first - if a parametric model cannot beat well-tuned k-NN, the parametric model is broken or the features are wrong, not the other way round.
Interview Q&A
Sketch the proof of the Cover-Hart 1-NN bound and state the result.research
Statement. As , the asymptotic error of the 1-nearest-neighbour rule is
where is the Bayes error rate [2].
Sketch. Fix a query with , and let be its 1-NN with label . Bayes-optimal conditional error is . As , almost surely, so . Conditional on and , the true label and the neighbour label are independent Bernoullis with parameters and , so
Using , . Take expectations over and apply Jensen (the function is concave) to get . The lower bound is trivial because no rule beats Bayes. ∎
The bound is tight when takes only the values . Note that no other classifier with infinite data is guaranteed to be within without further assumptions; that is what makes 1-NN special.
What changes for k-NN with k > 1, and why is k-NN consistent?research
For neighbours, the k-NN rule predicts the majority label among the closest training points. The crucial result is Bayes consistency: if satisfies and , then as .
Heuristically, the nearest neighbours give an empirical estimate of in a shrinking ball around . As with the right schedule, the ball shrinks (so the bias from varying inside the ball vanishes), but grows (so the variance of the empirical proportion of class 1 vanishes). A majority vote on Bernoulli trials converges to , which is the Bayes rule. So with a properly chosen schedule, k-NN matches the Bayes error in the limit.
In practice is chosen by cross-validation: small has low bias and high variance; large smooths but introduces bias by averaging far-away neighbours [1].
Why does the curse of dimensionality break k-NN, and what does the concentration-of-distances argument actually show?onsite
In high dimensions, pairwise distances concentrate around a common value, so the contrast between the nearest and farthest training points to a query collapses. Formally, for i.i.d. coordinates of finite variance,
The intuition: is a sum of i.i.d. terms, with mean and variance , so the relative fluctuation is . Doubling shrinks the gap between near and far by .
For k-NN this means: in raw 4096-dimensional embeddings, the “nearest neighbour” is barely closer than the median training point - the discriminative signal in the lookup is gone. The fixes in practice:
- Reduce dimensionality (PCA, UMAP, an autoencoder, or a learned projection).
- Learn a metric (large-margin nearest neighbour, triplet losses), so distances reflect class structure rather than raw geometry.
- Switch to approximate methods (LSH, HNSW) that explicitly trade a bit of recall for orders-of-magnitude speed-ups, since exact lookup is buying you very little anyway in this regime [3][4].
Compare KD-tree, ball tree, and HNSW. When does each pay off?onsite
KD-tree. Recursively splits the training set on one axis at a time at the median. Build , query on average for low . Beautiful in ; degrades to once the bounding boxes of sibling nodes overlap most of space, which happens fast as grows. Exact.
Ball tree. Same recursive partitioning, but with hyperspherical bounding regions instead of axis-aligned boxes. Handles non-axis-aligned data better than KD-tree (e.g. correlated features), but suffers from the same dimensionality issue around . Exact.
HNSW (Hierarchical Navigable Small World graph). Builds a multi-layer proximity graph: top layers have few well-connected nodes for long-range hops, bottom layers densely connect every node to its near neighbours [4]. Search starts at the top, greedily descends. Build , query with very high recall in mid-to-high dimensions. Approximate, with a knob (efSearch) trading recall for latency.
Rules of thumb.
- : KD-tree on raw data is fastest and exact.
- : ball tree if data is non-axis-aligned, otherwise brute force.
- and : HNSW or IVF-PQ in FAISS / ScaNN, after you have reduced dimensionality if possible.
- at any : just brute-force; cache locality wins.
- 1. What does k-NN do at training time?
- 2. What is the asymptotic Cover-Hart bound on the 1-NN error rate (binary case)?
- 3. Why does brute-force k-NN fail in very high dimensions even with large n?
- 4. In sklearn, why does the documentation switch from KD-tree / ball tree to brute force around d ~ 30?
- 5. Which production scenario does HNSW especially target?
A short evening project:
- Take MNIST. Implement brute-force k-NN with Euclidean distance and report test accuracy across .
- Add inverse-distance weighting and re-run; you should see a small improvement.
- Replace raw pixels with a 64-dim PCA projection and confirm accuracy improves and queries run faster.
- Switch to
sklearn.neighbors.NearestNeighbors(algorithm='ball_tree')and confirm exact agreement with brute force, then time the difference. - Stretch: index the same projected vectors in
faiss.IndexHNSWFlat(d=64, M=32), sweepefSearch, and plot the recall@10 vs query-latency curve.
Interactive Demo
References
- Hastie, T., Tibshirani, R., and Friedman, J. (2009). The Elements of Statistical Learning, 2nd ed.. Springer. https://hastie.su.domains/ElemStatLearn/
- Cover, T. M. and Hart, P. E. (1967). Nearest neighbor pattern classification. IEEE Transactions on Information Theory 13(1), 21-27. doi:10.1109/TIT.1967.1053964
- Indyk, P. and Motwani, R. (1998). Approximate nearest neighbors: towards removing the curse of dimensionality. Proceedings of the 30th Annual ACM Symposium on Theory of Computing (STOC), 604-613. doi:10.1145/276698.276876
- Malkov, Y. A. and Yashunin, D. A. (2018). Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs. IEEE Transactions on Pattern Analysis and Machine Intelligence. arXiv:1603.09320