EVERYTHING AIAI engineering, made visual
0/18 complete
LESSON 06 · MACHINE LEARNING × AI · BUILD

No training.
Just the nearest neighbours.

predict = vote of the k closest points is a complete learning algorithm. Store the data, choose a ruler, let the neighbourhood decide — and choose k to balance noise against blur.

75 MIN · 7 CHAPTERSPREREQ · LESSONS 02 & 14
FIG. 06 / K NEAREST NEIGHBOURS, AS K CHANGES
K = 1 · PREDICT = A A B C query
LESSON 06TYPE · BUILD~75 MINPREREQ · PHASE 1 · LESSONS 02, 14ORIGINAL LESSON ↗
THE 60-SECOND VERSIONThen show me the neighbours ↓
01 / STORE, DON'T TRAIN

The model is the training set.

KNN does no fitting: keep every labelled example, and when a query arrives measure its distance to all of them. That makes it a lazy learner — zero training time, all the work at prediction time — the opposite trade-off from an SVM or a neural network.

fit = store · predict = scan + vote
02 / K IS THE DIAL

Small k is noisy, large k is blurry.

k = 1 follows every point, including the noisy ones (high variance, overfit). Very large k ignores local structure and predicts the global majority (high bias, underfit). Odd k avoids ties; a common start is k ≈ √N, tuned by cross-validation.

k↑ = smoother regions, less variance
03 / THE RULER CHOOSES THE NEIGHBOURS

“Near” is a decision, not a fact.

Euclidean distance measures straight-line closeness; Manhattan sums axis-aligned steps; cosine measures angle and ignores length. Different rulers pick different neighbours — and unlike the others, cosine needs no feature scaling because it divides the lengths away.

L2 · L1 · cosine = three answers
MENTAL MODEL IN ONE SENTENCE

K-nearest neighbours is memory as a model: keep every labelled example, measure the query against all of them with a chosen distance, and let the k closest — or, weighted, the closest to closest — vote.

By the end you will be able to run KNN by hand on a small set, read the bias–variance trade-off off the k dial, choose between Euclidean, Manhattan and cosine distance for a dataset, explain why unscaled features break the ruler, and say when exact search needs a KD-tree or an approximate index instead.

STORE, DON'T TRAIN

No loss function.
Just distance and a vote.

Most algorithms compress the training set into parameters. K-nearest neighbours keeps the training set itself and looks things up at prediction time. That single design choice decides everything else.

K-nearest neighbors (KNN) is an instance-based, or lazy, learner. “Training” is one line: store every labelled example. When a new query point arrives, KNN computes its distance to every stored point, sorts the list, keeps the k closest, and lets them vote — the majority label for classification, the average value for regression. There are no weights, no gradients, no epochs.

That sounds almost too simple, but the design carries real consequences. The model can adapt to new data instantly (append a point), it can represent arbitrarily wiggly boundaries, and it never underfits by construction — with k = 1 it reproduces the training set exactly. The price is paid at prediction time, when every query scans the whole dataset.

Before the algorithm can call anything “nearest,” someone has to define near. That definition — the distance function — is not a detail. It is a modelling choice as important as the loss function in a neural network, and the next chapters show it changing predictions on the same data. KNN is the algorithm; the distance is the opinion.

The KNN playground: drag the query, change the k

Drag the ringed query point (or click anywhere) and slide k. The rings and connectors show exactly which points get a vote; the table below lists every distance, so the prediction is never a mystery.

query (3.10, 2.90) · Euclidean · k = 5 1. P 8 (3.6, 2.1) B d = 0.943 ← 2. P 3 (2.0, 2.7) A d = 1.118 ← 3. P 5 (2.4, 4.1) A d = 1.389 ← 4. P 6 (1.7, 1.9) A d = 1.720 ← 5. P 9 (5.0, 2.5) B d = 1.942 ← 6. P 7 (4.4, 1.3) B d = 2.062 7. P 1 (1.2, 3.8) A d = 2.102 8. P10 (3.9, 0.7) B d = 2.341 9. P11 (5.2, 1.6) B d = 2.470 10. P 2 (0.6, 2.9) A d = 2.500 11. P 4 (1.0, 4.6) A d = 2.702 votes: A 3 · B 2 → predict A the ring is the k-th neighbour distance: everything inside it counts.

With k = 1 the nearest point alone decides; with k = 9 nearly the whole set votes and the answer tends toward the bigger cluster.

AspectLazy (KNN)Eager (SVM, neural net)
Training timeO(1) — just store the dataO(n · epochs) — fit parameters
Prediction timeO(n · d) per query — compare to everythingO(d) or O(parameters) — apply the model
Memory at predictionThe entire training setThe model parameters only
New data arrivesAdd the point — no retrainingRetrain the model
Decision boundaryImplicit, computed on the flyExplicit, fixed after training
What “nearest” means, numerically

The lab’s six-point set, with the query at (2.0, 2.0). Two candidates, one per class:

#1 A (1.2, 2.0): d² = (2.0 − 1.2)² + (2.0 − 2.0)² = 0.64 + 0.00 = 0.64 d = √0.64 = 0.800 #4 B (2.9, 1.6): d² = (2.0 − 2.9)² + (2.0 − 1.6)² = 0.81 + 0.16 = 0.97 d = √0.97 = 0.985 same order by d² or by d: √ is strictly increasing, so ranking by 0.64 < 0.97 is the same as ranking by 0.800 < 0.985 — and saves a square root per comparison.

With k = 1 the prediction is #1 → A. With k = 3 the third neighbour joins: #2, also class A. The vote is 2–1 and the prediction stays A — but with k = 5 the B points outnumber A 3–2 and the prediction flips. Same data, same ruler; only the size of the jury changed.

Quick check

What must be chosen before KNN can decide which point is 'nearest'?

THE ALGORITHM

Five steps.
No training loop.

Given a labelled dataset and a query point, the entire method fits on an index card. The craft lives in the choices around it: the ruler, the k, and what the neighbours vote on.

  1. Compute the distance from the query to every stored point.
  2. Sort by distance, nearest first.
  3. Take the first k.
  4. Classification: majority vote among their labels. Regression: average (or weighted average) of their target values.
  5. Return the prediction. That is the whole algorithm — no loss, no epochs, no learned parameters.

Everything hangs on step 1: “distance” is a choice, and chapter 04 swaps the ruler. Everything else is bookkeeping. Because the scan touches every stored point, a prediction costs O(n · d) time for n examples with d features — cheap to train, expensive to serve.

The regression flavour. Swap the vote for an average and KNN predicts numbers instead of labels: the prediction is the mean target of the k neighbours (chapter 06 weights that average by closeness). Numeric check: neighbours with targets 10, 20 and 40 give (10 + 20 + 40) / 3 = 23.33. The result is piecewise-constant — KNN can only output values that exist in the neighbourhood, so if every training target lies between 0 and 100, it will never predict 200. No extrapolation, ever.

The distance console

The six-point worked example from this chapter. Move the query, switch the ruler, change k — every rank, vote and prediction is recomputed from the displayed coordinates, and the arithmetic is printed.

training set
#1 A (1.2, 2.0)
#2 A (2.0, 3.1)
#3 A (0.4, 3.4)
#4 B (2.9, 1.6)
#5 B (3.4, 2.6)
#6 B (0.6, 0.8)
query (2.00, 2.00) · Euclidean (L2) · k = 3 · uniform all training points, distances to the query: #1 A (1.2, 2.0) Δ=(-0.80, +0.00) d = 0.800 ← #2 A (2.0, 3.1) Δ=(+0.00, +1.10) d = 1.100 ← #3 A (0.4, 3.4) Δ=(-1.60, +1.40) d = 2.126 #4 B (2.9, 1.6) Δ=(+0.90, -0.40) d = 0.985 ← #5 B (3.4, 2.6) Δ=(+1.40, +0.60) d = 1.523 #6 B (0.6, 0.8) Δ=(-1.40, -1.20) d = 1.844 selected neighbours (k = 3): 1. #1 A d = 0.800 2. #4 B d = 0.985 3. #2 A d = 1.100 votes: A = 2 B = 1 → predict A same k, other rulers: Euclidean #1 0.800, #4 0.985, #2 1.100 → A Manhattan #1 0.800, #2 1.100, #4 1.300 → A Cosine #5 0.009, #6 0.010, #2 0.022 → B numeric check: Euclidean check for #1: √((2.00 − 1.20)² + (2.00 − 2.00)²) = √(0.6400 + 0.0000) = 0.800 Try it: at (2.0, 2.0) with k = 3, Euclidean and Manhattan pick the close A, while cosine picks B because the B points lie at a smaller angle.
RankPointClassΔ from querydVotes at k = 3
1#1 (1.2, 2.0)A(0.80, 0.00)0.640.800
2#4 (2.9, 1.6)B(-0.90, 0.40)0.970.985
3#2 (2.0, 3.1)A(0.00, -1.10)1.211.100
4#5 (3.4, 2.6)B(-1.40, -0.60)2.321.523
5#6 (0.6, 0.8)B(1.40, 1.20)3.401.844
6#3 (0.4, 3.4)A(1.60, -1.40)4.522.126
Why sorting squared distances is enough
the six squared distances from query (2.0, 2.0), sorted: 0.64 (#1 A) < 0.97 (#4 B) < 1.21 (#2 A) < 2.32 (#5 B) < 3.40 (#6 B) < 4.52 (#3 A) square root is strictly increasing: a < b ⟺ √a < √b so ranking by d² gives exactly the same order as ranking by d, and skips n square roots per query. actual distances: 0.800 < 0.985 < 1.100 < 1.523 < 1.844 < 2.126 k = 3 neighbours: #1, #4, #2 → A 2, B 1 → predict A k = 4 neighbours: #1, #4, #2, #5 → A 2, B 2 → tie! k = 5 neighbours: add #6 → B 3, A 2 → predict B

Every number in this table comes from the coordinates displayed in the console above. Change the query there and watch the whole trace — distances, ranks, votes — recompute from the same formula.

The prediction step, from the lesson's knn.pypython
def _predict_one(self, x):
    distances = []
    for i in range(len(self.X_train)):
        d = self.distance_fn(x, self.X_train[i])
        distances.append((d, self.y_train[i]))
    distances.sort(key=lambda pair: pair[0])
    neighbors = distances[:self.k]
    return self._classify(neighbors)

def _classify(self, neighbors):
    votes = {}
    for _, label in neighbors:
        votes[label] = votes.get(label, 0) + 1
    return max(votes, key=votes.get)
Twenty lines, no fitting. For regression, _classify is replaced by an average of the neighbour values.
CHOOSING K

The only dial.
Noise against blur.

KNN has a single hyperparameter, and it controls the entire bias–variance trade-off. Turning it up smooths the decision boundary; turning it down lets the model chase every point.

k = 1 is the most flexible model possible: each query copies its single nearest neighbour, the decision regions wrap every training point in its own island, and training accuracy is exactly 100%. That perfect training score is the tell — the model has memorized the noise, and on new data it wobbles (high variance).

Large k is the opposite: every vote averages over a big neighbourhood, so regions smooth out and noisy points are out-voted. Push it all the way to k = n and the query no longer matters at all — every prediction is the global majority class (high bias). The useful range is in between: a common starting point is k ≈ √n, refined by cross-validation. For binary classification use an odd k so a vote can never split evenly.

The decision regions, as k grows

Every pixel is classified by majority vote of its k nearest points. Slide k from 1 to 25 and watch the jagged islands smooth into continents. The inset line is honest leave-one-out accuracy: each point is predicted with itself removed.

k = 1 leave-one-out accuracy = 89.7% (26 / 29) best k on this set = 5 at 96.6% at k = 1 the model can copy any single point, so the regions wrap each point in its own small island. at k = 25 each query averages a large neighbourhood (25 of the 28 other points), so the regions flatten toward the majority class. training accuracy would be 100% at k = 1 — which is exactly why it is the wrong number to trust.

The curve is computed from the displayed points only, one fold per point. A U-shape — high error at both ends, a low middle — is the bias–variance trade-off made visible.

kBehaviour
k = 1Boundary follows every point. Zero training error, high variance. Overfits
Small k (3–5)Sensitive to local structure. Can capture complex boundaries
Large kSmoother boundary. More robust to noise. May underfit
k = NPredicts the majority class for every query. Maximum bias
Worked check: one query, four jury sizes

Seven points on a line — three class A close together, one noisy class B beside them, then three far-away B points. The query is x = 0.45:

k = 1: 0.4 A (0.05) votes A 1, B 0 → A k = 3: 0.4 A (0.05) 0.6 B (0.15) 0.2 A (0.25) votes A 2, B 1 → A k = 5: 0.4 A (0.05) 0.6 B (0.15) 0.2 A (0.25) 0.0 A (0.45) 3.0 B (2.55) votes A 3, B 2 → A k = 7: 0.4 A (0.05) 0.6 B (0.15) 0.2 A (0.25) 0.0 A (0.45) 3.0 B (2.55) 3.2 B (2.75) 3.4 B (2.95) votes A 3, B 4 → B Note the noisy B at 0.6. At k = 1, 3 and 5 the local structure wins; only at k = 7 does the global majority (B 4–3) take over. k = 7 = n, so it predicts B for every query — the underfitting extreme.

The lesson’s decision-region lab draws exactly this trade-off across a two-dimensional dataset: the inset leave-one-out curve is high at both ends and lowest somewhere in the middle. The middle is where the model has enough neighbours to ignore noise but still enough locality to be useful.

Quick check

You raise k from 1 all the way to the dataset size n. What happens to the decision regions?

WHAT “NEAREST” MEANS

The ruler decides
who counts as a neighbour.

Euclidean, Manhattan and cosine distance are three different answers to “how close are these two points?” On the same data, they can pick different neighbours and deliver different predictions.

Euclidean distance (L2) is the straight-line ruler: d = √(Σ(aᵢ − bᵢ)²) — square every feature difference, add them, take the square root. It is the default in most libraries, and it is the one that suffers when features are not scaled.

Manhattan distance (L1) walks city blocks: d = Σ|aᵢ − bᵢ| — add the absolute differences. Because it does not square, a single large difference is not amplified, which makes L1 more robust to outliers than L2.

Cosine distance measures the angle between vectors and ignores their lengths: d = 1 − (a·b)/(‖a‖‖b‖), which runs from 0 (same direction) to 2 (opposite). It is the standard ruler for text and embeddings, where a long document and a short one on the same topic should count as similar. It also needs no feature scaling, because dividing by the two norms already removes the units.

Minkowski distance is the family that contains L1 and L2: d = (Σ|aᵢ − bᵢ|^p)^(1/p) — sum the p-th powers and take the p-th root. p = 1 is Manhattan, p = 2 is Euclidean, and p → ∞ is Chebyshev distance, the largest single feature difference.

Three rulers, three different neighbours

Click or drag the query, switch the ruler, and watch both the highlighted neighbours and the background regions change. Same points, same k — only the definition of “near” changed.

selected: Euclidean (L2) — √(Δx² + Δy²) query (2.00, 2.00) · k = 3 Euclidean (L2) B 0.89, A 1.41, B 1.53 → B Manhattan (L1) B 1.20, B 1.80, A 2.00 → B Cosine A 0.00, A 0.00, B 0.01 → A The class A points sit on the query's ray but far away; the class B points are close in a straight line. Euclidean and Manhattan pick B; cosine picks A because it only sees angle. Move the query to (4, 4) and the three rulers agree again.

Cosine distance never looks at length, so its regions are wedges of direction. Near the origin the direction is ill-defined — a reminder that zero vectors have no angle.

Data typeBest metricWhy
Numeric features, similar scaleL2 (Euclidean)The default: straight-line closeness in space
Numeric features with outliersL1 (Manhattan)Does not square the differences, so one huge gap cannot dominate
Text embeddingsCosineLength is noise, direction is meaning
High-dimensional and sparseCosine or L1L2 suffers most from distance concentration
Mixed feature typesCustom metricSum per-type distances with weights you choose
The Minkowski family, with numbers

Take a = (1, 2, 3) and b = (4, 0, 6). The per-feature differences are 3, 2 and 3:

p = 1: 3 + 2 + 3 = 8.000 (Manhattan) p = 1.5: 5.591 (in between) p = 2: √(9 + 4 + 9) = √22 = 4.690 (Euclidean) p = 3: ³√(27 + 8 + 27) = ³√62 = 3.958 p = ∞: max(3, 2, 3) = 3.000 (Chebyshev) every p from 1 to ∞ gives a different number, and they are ordered: L∞ ≤ … ≤ L3 ≤ L2 ≤ L1.

Cosine distance needs no per-feature p; it compares direction only. Here a·b = 22, ‖a‖ = √14, ‖b‖ = √52, so dcos = 1 − 22 / (√14·√52) = 0.185 — the two vectors point in fairly similar directions even though their straight-line distance is large.

Now the payoff. The six-point set from chapter 02, query (2.0, 2.0), k = 3:

Euclidean neighbours: #1 A 0.800, #4 B 0.985, #2 A 1.100 → A Cosine neighbours: #5 B 0.009, #6 B 0.010, #2 A 0.022 → B same six points, same k, opposite predictions. Euclidean asks "which point is closest?"; cosine asks "which point points the same way?" — and on this data the B points win the second question. Numeric anchor: #5 B (3.4, 2.6) has Euclidean distance 1.523 but cosine distance 0.0088 because (2.0, 2.0) and (3.4, 2.6) point in almost the same direction.

Phase 1, Lesson 14 develops norms and distances properly — including why every p-norm obeys that ordering and when the differences matter. This lesson only needs the three rulers that KNN uses most.

Quick check

You are comparing TF-IDF vectors of news articles. One article is 10× longer than another but covers the same story. Which metric is designed to call them similar?

SCALE BEFORE DISTANCE

The column with the biggest numbers
does almost all the voting.

A distance is a sum over features, and features carry units. Unless every column is put on a comparable scale first, KNN is not measuring similarity — it is measuring whichever feature happens to be written in the largest units.

A squared Euclidean distance is (Δx₁)² + (Δx₂)² + … — a sum of per-feature contributions. Salary measured in dollars produces differences in the tens of thousands; age measured in years produces differences in the tens. Squared, that is a factor of roughly a million, so the salary column contributes essentially 100% of the distance and age might as well not exist.

The standard fix is standardization: for each feature, subtract its mean and divide by its standard deviation, z = (x − μ) / σ. Plain English: express every value as “how many typical deviations away from average is this?” After that, one year of age and one salary σ are numerically comparable. (Min–max scaling, (x − min)/(max − min), is the other common choice when you need a bounded range; cosine distance needs neither.)

Two disciplines keep this honest. First, fit the scaler on the training set only, then apply the same μ and σ to validation and test data — otherwise information from the test set leaks into training. Second, scaling never changes cosine distance, because dividing every vector by its length removes the units before the angle is measured.

Why scaling decides the answer

Eight people with an age and a salary (class A / B). Move the query and compare the two panels: raw units on the left, z-scores on the right. Same k, same people — but the neighbours, the vote, and sometimes the prediction change.

query: age 37 · salary $62,000 RAW UNITS (salary dominates): 1. P7 (58.00, 56000.00) B d = 6000.037 2. P4 (35.00, 52000.00) A d = 10000.000 3. P6 (52.00, 79000.00) B d = 17000.007 vote → B STANDARDIZED (z-scores): 1. P4 (-0.47, -0.30) A d = 0.457 2. P2 (-1.07, -0.65) A d = 1.079 3. P6 (0.67, 0.88) B d = 1.252 scaled query = (-0.336, 0.137) vote → A salary's share of the raw squared distance: P7: 99.999% P4: 100.000% P6: 100.000% means: age 42.0 · salary $58,875 std devs: age 14.89 · salary $22,872

A salary difference of 40,000 contributes 1,600,000,000 to a squared distance; an age difference of 30 contributes 900. Until both features are rescaled, the salary column is the only one the ruler can see.

Worked scaling check: raw units vs range-scaled

The classic two-feature example. Query: age 30, salary $50,000. Candidate P1: age 31, salary $90,000. Candidate P2: age 60, salary $55,000.

raw L2 (salary in dollars): P1 = √(1² + 40,000²) = 40000.0000 P2 = √(30² + 5,000²) = √25,000,900 = 5000.0900 raw units say P1 is nearer — but nearly all of that gap is salary: 100.0000% of P1's squared distance, and 99.9964% of P2's. range-scaled L2 (divide age by 100, salary by 100,000): P1 = √(0.01² + 0.40²) = 0.4001 P2 = √(0.30² + 0.05²) = 0.3041 scaled units say P2 is nearer, because age finally counts. standardization of a tiny column [10, 20, 30]: μ = 20 σ = √((10² + 0² + 10²)/3) = √66.667 = 8.165 z = [(10−20)/8.165, 0, (30−20)/8.165] = [−1.225, 0.000, +1.225] the column now has mean 0 and standard deviation 1.

Notice that scaling does not invent information — it changes which information the ruler can hear. Whether P2 should be nearer is a modelling question. But silently letting salary drown out age is a bug, not a model.

LET CLOSE NEIGHBOURS VOTE LOUDER

One neighbour at distance 0.5
knows more than one at 4.0.

Uniform KNN gives every chosen neighbour exactly one vote. Distance weighting turns that vote into a volume knob: the closer the neighbour, the louder it speaks.

Standard KNN treats all k neighbours as equals. But a neighbour at distance 0.5 is far better evidence than one at distance 4.0. Distance-weighted KNN gives neighbour i the weight wᵢ = 1 / (dᵢ + ε), where ε is a tiny constant that keeps the division finite when a training point coincides with the query. For classification, the votes become weighted sums: score(class) = Σ wᵢ over neighbours of that class. For regression, the prediction becomes the weighted average ŷ = Σ wᵢyᵢ / Σ wᵢ.

The practical payoff: weighted KNN is much less sensitive to k, because adding a far-away neighbour contributes very little to either side. That makes larger k safer — the smoothness of a big neighbourhood without letting distant points drown out the local signal.

Uniform vote vs distance-weighted vote

Five points on a line, two of class A close to the query and three of class B farther away. Slide the query and k: the uniform vote counts bodies, the weighted vote counts 1 / (distance + ε).

query 1.8 · k = 5 chosen neighbours: 1. P2 A d = 0.200 1 vote w = 5.000 2. P1 A d = 0.800 1 vote w = 1.250 3. P3 B d = 1.200 1 vote w = 0.833 4. P4 B d = 1.800 1 vote w = 0.556 5. P5 B d = 2.400 1 vote w = 0.417 uniform weighted A total 2 6.250 B total 3 1.806 predict B A the two votes disagree: the head count says B, but the close neighbours are worth more than the far ones, so weighting says A.

Weighting is a volume knob on distance, not a new algorithm: sort, take k, then let each neighbour speak in proportion to how close it stands.

Worked votes: when weighting changes the answer

First the smallest possible flip. Three neighbours vote: class A at distance 0.5, class B at 1.0, class B at 2.0.

uniform: A 1, B 2 → predict B weights: A: 1/0.5 = 2.000 B: 1/1.0 = 1.000 and 1/2.0 = 0.500 weighted: A 2.000, B 1.500 → predict A numeric check: A total = 2.000, B total = 1.500. The single close neighbour outvotes the two distant ones.

Now the six-point set from chapter 02. At k = 4 the uniform vote is a perfect 2–2 tie; weighting breaks it. And with the Manhattan ruler at k = 5, weighting flips the majority entirely:

k = 4, Euclidean, uniform: A 2, B 2 → tie k = 4, Euclidean, weighted: A 2.159, B 1.672 → A k = 5, Manhattan, uniform: A 2, B 3 → B k = 5, Manhattan, weighted: A 2.159, B 1.654 → A the two close A points earn weights 1.25 and 0.909; the three B points earn 0.769, 0.500 and 0.385.

Weighting is not automatically better — it is a different prior. If local labels are noisy but the broader region is reliable, uniform voting over a larger k can beat weighting. The honest way to choose is cross-validation on both variants.

Quick check

A class-A neighbour sits at distance 0.5 and a class-B neighbour at distance 2.0. In a distance-weighted vote, who has more say?

COST, TREES & HIGH DIMENSIONS

Free to train.
Expensive to ask — and fragile in 100D.

KNN’s laziness shifts all cost to prediction, and its accuracy falls apart as dimensions grow. Both problems have standard, engine-room answers.

Storing the data is O(1) per example — KNN’s famous zero training time. Predicting is the bill: each query computes n · d per-feature differences for n examples with d features, then sorts n distances. A million 768-dimensional document embeddings means roughly 768 million subtractions and squares per query. Brute force does not scale to that, so production systems swap the scan for an index.

A KD-tree recursively splits the data along one feature axis at a time, at the median of the current region. A query walks down to its leaf, then backtracks, skipping any region whose bounding box is farther than the best neighbour found so far. In low dimensions that prunes almost all of the tree — average O(log n) per query. A ball tree does the same trick with nested hyperspheres instead of axis-aligned boxes, and keeps pruning longer in moderate dimensions (up to roughly 50).

Beyond that, exact search is abandoned for approximate nearest neighbour indexes: HNSW (a navigable similarity graph), IVF (cluster first, scan a few clusters), and product quantization (compress vectors, compare compressed codes). They trade a little recall for orders of magnitude of speed, and they are what vector databases actually run.

split on xsplit on ysplit on yleaf 1leaf 2leaf 3leaf 4
A KD-tree splits the space along one feature at a time, at the median of the current region. Finding a neighbour means walking to the query’s leaf, then backtracking — and visiting a neighbouring region only if its bounding box could still contain something closer. In two dimensions that prunes almost everything; in a hundred dimensions there is almost nothing to prune.
The curse of dimensionality, with measured numbers

One seeded run: 80 uniform random points in the unit cube [0, 1]^d, all 3,160 pairwise Euclidean distances measured. The ratio of the farthest to the nearest says how much the notion of “nearest” still means; the spread column says how tightly the distances bunch around their average:

Dimensions dmax / min distancestd / mean distancemean distance
286.740.4670.514
523.340.2810.927
105.470.1981.258
501.850.0822.876
1001.480.0574.070

At d = 2 the ratio is wide — 87 to 1 — so there are genuinely near and far points. By d = 100 it has collapsed to about 1.5, and the standard deviation of the distances is only 5.7% of their mean: every point is nearly the same distance from the query, and the ranking KNN depends on becomes noise. That is distance concentration, a mathematical fact and not a quirk of the sample. The exact extremes depend on the sample (the source’s 200-point run reports a max/min ratio of about 1.01 at d = 100 — the same phenomenon, measured a slightly different way).

A second view is geometric. Inscribe a sphere in the unit cube: the sphere’s share of the cube’s volume collapses as dimension grows, because the corners of the cube hold almost all of the volume and the sphere touches none of them.

Dimensions dsphere volume / cube volume
20.7854
40.3084
100.0025
501.5e-28
1001.9e-70

Practical consequence: KNN works well up to roughly 20–50 features. Past that, either reduce dimensionality first (PCA, UMAP, t-SNE) and run KNN in the smaller space, or use an index that exploits the data’s intrinsic lower-dimensional structure — or switch to a model class that does not rely on raw distances at all.

The industrial version, in three linespython
import faiss

index = faiss.IndexFlatL2(dimension)
index.add(embeddings)
distances, indices = index.search(query_vectors, k=5)
IndexFlatL2 is exact brute force; swapping it for IndexHNSWFlat trades a little recall for a huge speed-up. The API stays the same.
CHECK YOURSELF

Six questions.
Then the terms worth keeping.

Answer before you look. The scaling question and the weighted-vote question are the ones that show up in real debugging sessions.

0 / 6 answered · 0 correct

01KNN is called a “lazy learner.” What does that mean?

02Why is feature scaling critical for KNN?

03In 100 dimensions with uniform random points, what happens to the ratio of max distance to min distance?

04Which distance metric is most appropriate for comparing text documents represented as TF-IDF vectors?

05What happens to the KNN decision boundary as K increases from 1 to N (the full dataset size)?

06What does distance-weighted KNN change about the vote?

Key terms, demystified

Click a card to swap the lazy description for what it actually means.

Exercises from the lesson

Four short problems with exact numbers. Try first; a worked answer is one click away.

  1. The lesson's six-point set, query (2.0, 2.0): compute the L2, L1, and cosine distance from the query to every point, rank them, and report the k = 3 vote under each metric. Which ruler changes the answer, and why?
    Show one worked answer

    Points: #1 A (1.2, 2.0), #2 A (2.0, 3.1), #3 A (0.4, 3.4), #4 B (2.9, 1.6), #5 B (3.4, 2.6), #6 B (0.6, 0.8). L2: #1 0.800, #4 0.985, #2 1.100, #5 1.523, #6 1.844, #3 2.126 → k = 3 neighbours #1, #4, #2 → A 2, B 1 → predict A. L1: #1 0.800, #2 1.100, #4 1.300, #5 2.000, #6 2.600, #3 3.000 → #1, #2, #4 → A 2, B 1 → predict A. Cosine: #5 0.0088, #6 0.0101, #2 0.0225, #1 0.0299, #4 0.0393, #3 0.2151 → #5, #6, #2 → B 2, A 1 → predict B. Cosine changes the answer because it ignores vector length: #5 (3.4, 2.6) and #6 (0.6, 0.8) point in directions closer to (2, 2) than #1 and #2 do, even though a straight line puts #1 closest. Same data, same k — the definition of “near” decided the label.

  2. A dataset has age (range 0–80) and salary (range 0–100,000). The query is age 30, salary 50,000. P1 = (31, 90,000), P2 = (60, 55,000). Compute the raw L2 distance to each. Then divide each feature difference by its range (100 for age to be generous, 100,000 for salary) and recompute. Which point is nearer before scaling, which is nearer after, and what fraction of each raw squared distance comes from salary?
    Show one worked answer

    Raw: P1 = √((31 − 30)² + (90,000 − 50,000)²) = √(1 + 1,600,000,000) = 40,000.0000125. P2 = √((60 − 30)² + (55,000 − 50,000)²) = √(900 + 25,000,000) = 5,000.09. Raw units say P1 is “nearer.” Range-scaled: P1 = (0.01, 0.40) → √(0.0001 + 0.16) = 0.4001; P2 = (0.30, 0.05) → √(0.09 + 0.0025) = 0.3041. Now P2 is nearer. Salary's share of the squared raw distance: P1 = 1,600,000,000 / 1,600,000,001 = 99.9999999%; P2 = 25,000,000 / 25,000,900 = 99.9964%. Almost all of the ruler was measuring salary, so the “nearest” point had almost nothing to do with age.

  3. Seven one-dimensional points: x = 0, 0.2, 0.4 are class A; x = 0.6 is class B (a noisy point); x = 3.0, 3.2, 3.4 are class B. The query is x = 0.45. Tabulate distances and the majority vote for k = 1, 3, 5, and 7. What happens, and what does k = 7 predict for every query?
    Show one worked answer

    Distances from 0.45: 0.4 A 0.05, 0.6 B 0.15, 0.2 A 0.25, 0 A 0.45, 3.0 B 2.55, 3.2 B 2.75, 3.4 B 2.95. k = 1: nearest is the A at 0.4 → A. k = 3: A (0.4), B (0.6), A (0.2) → A 2, B 1 → A. k = 5: adds A (0) and B (3.0) → A 3, B 2 → A. k = 7: every point votes → A 3, B 4 → B. The noisy B at 0.6 lives beside the A cluster, so local votes keep seeing A; only when k grows large enough for the three far-away B points to outvote the local structure does the prediction flip. At k = N the result is independent of the query — it is always the global majority class, which is the underfitting extreme.

  4. Distance-weighted regression with three neighbours: values 10, 20, 40 at distances 1, 2, 4. Compute the unweighted average and the distance-weighted average. Then explain what the ε in w = 1 / (d + ε) protects against when a training point sits exactly on the query.
    Show one worked answer

    Unweighted: (10 + 20 + 40) / 3 = 70 / 3 = 23.333. Weights: 1/1 = 1.000, 1/2 = 0.500, 1/4 = 0.250, total 1.750. Weighted: (10·1 + 20·0.5 + 40·0.25) / 1.750 = (10 + 10 + 10) / 1.750 = 30 / 1.750 = 17.143. Weighting pulled the prediction toward the nearest neighbour (10) instead of letting two distant points dominate. A point at d = 0 would give w = 1/0 = ∞ without ε, making the weighted average undefined (or letting one point take over completely); with ε, its weight is a huge but finite 1/ε, so the prediction copies that neighbour's value, which is exactly what a duplicate of the query should do.

Terms this lesson borrows from later lessons (or outside)

You do not need to master these here. Each one gets a proper treatment in its own lesson; the one-line meaning is enough to keep reading. Orange dotted underlines in the prose point back to this list.

  • norm ‖x‖The length of a vector, √(x₁² + x₂² + …). Euclidean and Manhattan distance are both built from differences and norms; norms get their full treatment in Phase 1, Lesson 14.
  • dot productx·y = Σ xᵢyᵢ, the sum of coordinate-wise products. It is the numerator of cosine similarity and of every projection. (Phase 1, Lesson 02)
  • standardizationRescaling each feature to mean 0 and standard deviation 1 with z = (x − μ) / σ, fitted on the training data only. (Phase 2, Lesson 02)
  • cross-validationSplit the data into folds; train on all but one fold and score on the held-out one; average the scores. The honest way to choose k and every other hyperparameter. (Phase 2, Lesson 12)
  • bias–variance trade-offSmall k means low bias and high variance (the model follows noise); large k means high bias and low variance (the model smooths everything away). KNN is the clearest picture of the trade-off in ML. (Phase 2, Lesson 12)
  • TF-IDFA weighted word-count vector for a document — term frequency scaled by how rare the word is. The classic input to cosine-distance text search. (Phase 5, Lesson 02)
  • embeddingA learned vector that represents meaning, so that nearby vectors mean similar things. Retrieval searches nearest embeddings with cosine distance. (Phase 11, Lesson 04)
KEEP GOING

A picture is a start.
Practice is the rest.

This lesson is a port of an open course. Everything here traces back to it — and the next step is running the code yourself.

Lesson text adapted from AI Engineering from Scratch (Phase 02, Lesson 06) and the Math Foundations Notebook reference build. Interactive figures, the six-point worked example and its exact distance tables, the decision-region and metric labs, the scaling panels, the weighted-vote board, the KD-tree and curse-of-dimensionality figures, worked exercise answers and the numeric checks are original to this page. Every lab runs in your browser.