Instance-Based Learning & k-Nearest Neighbors
Explore the mathematics of non-parametric instance-based learning, distance metric derivations in metric spaces, the geometric curse of dimensionality, and zero-dependency implementations in Python and Rust.
Unlike parametric models that compress training examples into a fixed set of weights during an upfront training phase, Instance-Based Learning (often termed lazy learning) postpones computation until inference time.
The canonical algorithm in this family, k-Nearest Neighbors (k-NN), makes predictions by querying the local geometric neighborhood of a query point in a defined metric space.
k-NN has zero upfront training cost; it stores raw feature vectors directly in memory.
1. Distance Metrics in Metric Spaces#
For a function to qualify as a valid metric on a set , it must satisfy four axioms for all :
- Non-negativity:
- Identity of Indiscernibles:
- Symmetry:
- Triangle Inequality:
The Minkowski Distance Family#
The Minkowski distance of order generalizes standard geometric distances across coordinate spaces :
Step 1: Manhattan Distance ($p = 1$)
Setting computes grid-aligned rectilinear distance, corresponding to the norm:
Step 2: Euclidean Distance ($p = 2$)
Setting computes straight-line distance in Euclidean space, corresponding to the norm:
Step 3: Chebyshev Distance ($p \to \infty$)
Taking the limit as isolates the single coordinate axis with the maximum discrepancy, corresponding to the norm:
Cosine Distance vs Angular Similarity#
In text retrieval and dense embedding vectors, directional orientation matters more than Euclidean magnitude. The Cosine Similarity measures the angle between vectors:
The associated Cosine Distance is:
2. Mathematical Proof: The Curse of Dimensionality#
As the dimensionality increases, intuitive geometric properties break down. We prove why distance-based algorithms like k-NN suffer degradation in high-dimensional spaces.
Hypersphere to Hypercube Volume Ratio#
Consider a -dimensional hypercube of side length , with volume:
Now consider an inscribed -dimensional hypersphere of radius . Its volume is given by:
Where denotes Euler's Gamma function ( for positive integers).
Let us compute the ratio of the volume of the inscribed hypersphere to the enclosing hypercube:
Applying Stirling's asymptotic formula with :
As , the term exponentially:
3. KD-Trees: Accelerating Spatial Queries#
Brute-force k-NN requires scanning all data points, yielding query latency. A k-d Tree (k-dimensional tree) is a space-partitioning binary search tree that indexes data to enable expected search time.
Partitioning Rules:#
- Cycling Dimensions: At depth , split on feature dimension:
- Median Split: Find the median along coordinate to partition instances into balanced left and right subtrees.
- Branch Pruning: During nearest neighbor queries, if the distance from the query point to the splitting hyperplane is greater than the current radius to the -th best neighbor, the opposite subtree can be completely pruned.
4. From-Scratch Implementation: Zero Dependencies#
The following implementations demonstrate pure mathematical k-NN classification in Python and Rust without external packages.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152import math from typing import List, Tuple from collections import Counter class KNNClassifierFromScratch: def __init__(self, k: int = 3, metric: str = "euclidean"): self.k = k self.metric = metric self.X_train: List[List[float]] = [] self.y_train: List[int] = [] def fit(self, X: List[List[float]], y: List[int]) -> None: # Non-parametric lazy model: store instances directly self.X_train = [list(row) for row in X] self.y_train = list(y) def _compute_distance(self, a: List[float], b: List[float]) -> float: if self.metric == "manhattan": return sum(abs(x_i - y_i) for x_i, y_i in zip(a, b)) # Default: Euclidean distance (L2) return math.sqrt(sum((x_i - y_i) ** 2 for x_i, y_i in zip(a, b))) def predict_single(self, query: List[float]) -> int: distances: List[Tuple[float, int]] = [] for i, sample in enumerate(self.X_train): dist = self._compute_distance(query, sample) distances.append((dist, self.y_train[i])) # Sort by distance in ascending order distances.sort(key=lambda item: item[0]) # Extract top k nearest labels k_nearest_labels = [label for _, label in distances[:self.k]] # Majority vote vote_counts = Counter(k_nearest_labels) most_common = vote_counts.most_common(1)[0][0] return most_common def predict(self, X_test: List[List[float]]) -> List[int]: return [self.predict_single(query) for query in X_test] if __name__ == "__main__": X_train = [[1.0, 2.0], [1.5, 1.8], [5.0, 8.0], [8.0, 8.0], [1.0, 0.6], [9.0, 11.0]] y_train = [0, 0, 1, 1, 0, 1] knn = KNNClassifierFromScratch(k=3, metric="euclidean") knn.fit(X_train, y_train) X_test = [[1.2, 1.5], [6.0, 7.5]] predictions = knn.predict(X_test) print(f"Predicted labels: {predictions}")
5. Active Knowledge Checks#
Why does the ratio of an inscribed hypersphere volume to enclosing hypercube volume approach zero as dimensionality d approaches infinity?
In a KD-Tree nearest neighbor query, under what exact condition can the search algorithm prune and skip an entire subtree?