K-Nearest Neighbors (KNN)
Objectives
Section titled “Objectives”After this lesson, you will be able to:
- Understand how K-Nearest Neighbors (KNN) makes predictions: finding the closest training points and applying majority voting for classification or averaging for regression.
- Calculate Euclidean distances, interpret decision boundaries, and evaluate classifier accuracy.
- Explain why leads to overfitting, excessively large causes underfitting, and how to select via cross-validation.
- Recognize two primary vulnerabilities of KNN: strict sensitivity to feature scaling and degradation in high dimensions caused by the curse of dimensionality.
Prerequisites
Section titled “Prerequisites”- Introduction to Machine Learning, in Machine Learning (Learned)
- Linear Algebra, in Math for AI(Not translated yet) (Learned)
Intuition
Section titled “Intuition”A common proverb says: “You are the average of the five people you spend the most time with.” KNN operates on the exact same premise: to determine properties of a new data point, query its closest neighbors.
You encountered this principle in Introduction to Machine Learning: classifying whether an apartment is expensive or cheap by inspecting the 3 apartments closest in floor area. Similarly, a 1-nearest-neighbor memorization model matched the price of the single closest apartment, achieving training MSE of 0.0000. This lesson deconstructs how neighbor-based learning works.
Two Interlocking Moons
Section titled “Two Interlocking Moons”Our working dataset consists of 50 2D points, each characterized by features and with a binary label (class 0 or class 1). Data is generated via scikit-learn’s make_moons: class 0 forms a downward crescent at the top, while class 1 forms an upward crescent below, interlocking with each other. Random noise shifts several points into territory dominated by the opposing class. We partition this dataset into 35 training points and 15 test points.
The true boundary separating both classes is an S-shaped curve. No single straight line can separate these interlocking crescents. With neighbor-based methods, however, you do not need to assume the mathematical shape of the boundary: you simply inspect the local neighborhood around the query point.
One Neighbor or Five Neighbors
Section titled “One Neighbor or Five Neighbors”Consider a query point at , situated inside the upper crescent (class 0 territory). Its five nearest training points are:
| Neighbor | Coordinates | True Class | Euclidean Distance |
|---|---|---|---|
| 1 | 1 | 0.155 | |
| 2 | 0 | 0.443 | |
| 3 | 0 | 0.511 | |
| 4 | 0 | 0.539 | |
| 5 | 0 | 0.566 |
The single closest neighbor happens to belong to class 1—an outlier perturbed by noise into class 0 space. If you consult only one neighbor, you predict class 1. If you consult five neighbors, the vote tallies 4 against 1, and you correctly predict class 0.
How many neighbors should we poll? How do we measure distance rigorously? When can geometric distance mislead us?
The fundamental problem of this lesson is: predict the label of a query point from its nearest neighbors, selecting neighborhood size to filter out isolated noise without smoothing away true decision patterns.
Theory
Section titled “Theory”Problem Formulation
Section titled “Problem Formulation”We have training samples , each containing features. For our two moons dataset, , , and . Given a new query point , we predict .
KNN follows three steps:
- Compute distances between and all training points.
- Identify the training points with smallest distances. Their indices form neighborhood set .
- Aggregate the labels of these neighbors into a single prediction.
Here is a hyperparameter: an architectural choice set in advance rather than learned by parameters.
Distance Metrics
Section titled “Distance Metrics”The standard metric is Euclidean distance, representing straight-line distance between two points:
Compute coordinate differences, square them, sum across all dimensions, and take the square root. For query point and its nearest neighbor :
Notice that the difference along (0.15) is roughly larger than along (0.04), but after squaring contributes more to the distance. Dimensions with larger numerical magnitudes dominate Euclidean metrics, an issue addressed in the feature scaling section.
An alternative metric is Manhattan distance (taxicab distance), summing absolute coordinate differences: , representing grid travel. For the two points above, Manhattan distance equals . In scikit-learn, KNeighborsClassifier uses Euclidean distance by default; setting p=1 switches to Manhattan.
Majority Voting and Averaging
Section titled “Majority Voting and Averaging”Classification. Each neighbor casts one vote for its label. The majority class wins:
where is the indicator function evaluating to 1 if true and 0 otherwise, counting votes for class . For point with , class 0 receives 4 votes and class 1 receives 1 vote, yielding .
Regression. Labels are continuous numbers. Instead of voting, predictions average neighbor values:
In the apartment example, a 50 m² unit surrounded by apartments of 48, 55, and 42 m² (priced at 9.4, 12.2, and 8.8 million) is predicted at million under (KNeighborsRegressor).
Ties. Even values of in binary classification can result in ties (e.g., 2 vs. 2). Scikit-learn breaks ties by selecting whichever class appears first in classes_ (the smaller label index). To avoid arbitrary tie-breaking, use odd values of for binary classification. When multiple training points share identical distances along the -th boundary, scikit-learn documentation specifies that selection depends on internal data order.
Lazy Learning
Section titled “Lazy Learning”Linear regression concentrates computation during training: computing parameters and , after which training data can be discarded. Prediction requires only scalar multiplications.
KNN does the exact opposite. Calling fit merely stores the training data, learning zero model weights. All computational workload is deferred to prediction time: every query point computes distances to all training samples, taking roughly operations per query. This paradigm is known as lazy learning or instance-based learning. For 1,000,000 samples and 100 features, a single inference demands operations. While spatial indexing trees (KD-trees or Ball trees) accelerate queries during fit, their speedups diminish rapidly in higher dimensions.
Decision Boundaries and Accuracy
Section titled “Decision Boundaries and Accuracy”Coloring every coordinate on a plane by its KNN predicted class partitions feature space into distinct decision regions separated by decision boundaries. KNN decision boundaries bend flexibly around data clusters rather than remaining constrained to hyperplanes, allowing it to conform to interlocking crescents.
In classification, performance is measured by accuracy: the proportion of correct predictions:
If 11 out of 15 test points are correct, accuracy is . Higher accuracy is better, opposite to MSE. On imbalanced datasets, however, raw accuracy can be misleading, as examined in Model Evaluation.
Choosing k
Section titled “Choosing k”Neighborhood size governs model capacity, behaving inversely to polynomial degree in Overfitting and Underfitting: smaller represents higher flexibility.
- : Every training point commands a small territory around itself. An isolated noise point like carves out an island of class 1 inside class 0 territory, creating highly jagged boundaries. Training accuracy is guaranteed to be 1.000 (each training point matches itself at distance 0). This is extreme overfitting.
- Moderate : Minority noise points are outvoted by surrounding clusters. Boundaries smooth out, conforming to true crescent trajectories.
- Excessively large : Neighborhoods expand excessively, absorbing opposite-class points across the boundary. Boundaries flatten into coarse partitions. At , all training points vote on every query, unconditionally predicting the majority training class globally—the definition of underfitting.
Performance across our two moons dataset illustrates this behavior:
| Training Accuracy | Test Accuracy | |
|---|---|---|
| 1 | 1.000 | 0.733 |
| 3 | 0.943 | 0.800 |
| 7 | 0.943 | 1.000 |
| 15 | 0.914 | 0.800 |
| 25 | 0.743 | 0.800 |
| 35 | 0.514 | 0.467 |
Training accuracy declines monotonically as increases, whereas test accuracy rises to a peak at before declining—an inverted U-curve characteristic of model capacity trade-offs. At , the training set contains 18 class-1 points and 17 class-0 points; every query point is predicted as class 1, plummeting test accuracy to 0.467.
A test accuracy of 1.000 at on 15 samples should not be used as the criterion to pick ; that commits the error of evaluating on the test set. Parameter should be selected using cross-validation on the training set.
Feature Scaling
Section titled “Feature Scaling”Euclidean distance calculates the sum of squared differences in raw measurement units. Features with large numerical ranges completely overpower those with smaller ranges.
In the load_wine dataset (13 chemical features), proline ranges from 278 to 1680, whereas hue ranges from 0.48 to 1.71. A difference of 100 units in proline—a modest relative variation—contributes to squared distance. The entire range of hue () contributes at most . Unscaled KNN essentially ignores hue: proline accounts for 99.76% of all pairwise squared distances.
The solution is standardization via z-scores, as introduced in Regularization:
where and are the feature mean and standard deviation computed strictly on the training set (StandardScaler). With wine data, is 325.4 for proline and 0.221 for hue. After scaling, a 100-unit proline difference scales to , while a 1.23-unit hue variation scales to . Each feature is measured in standard deviations, preventing arbitrary units from dominating.
The moons dataset did not require this step because both coordinates naturally share comparable scales (ranging roughly from to ).
Distance-Weighted Neighbors
Section titled “Distance-Weighted Neighbors”Standard voting treats the 1st neighbor and the -th neighbor equally. A distance-weighted variant assigns each neighbor weight , where :
Closer points exert stronger influence. At with , class 1 receives weight, while class 0 accumulates . Class 0 still wins, but by a narrower margin. In scikit-learn, this is enabled via weights='distance'. If a query point coincides with a training point (), scikit-learn directly returns that point’s label.
The Curse of Dimensionality
Section titled “The Curse of Dimensionality”KNN relies on the assumption that close points share similar properties. In low dimensions, this assumption holds. As dimensionality grows, the concept of spatial proximity breaks down.
Suppose data points are uniformly distributed inside an -dimensional unit hypercube . To gather a local neighborhood of 10% of total data points, you construct a smaller hypercube with edge length . Its volume is , so its edge length is:
- : (a small 10% interval).
- : .
- : .
- : (spanning nearly the entire unit cube).
In 100 dimensions, capturing merely 10% of data requires spanning almost the entire width of every coordinate axis. Consequently, “nearest neighbors” are not genuinely local: all pairwise distances become nearly identical. This phenomenon is the curse of dimensionality. To make KNN viable in high-dimensional spaces, one needs exponential sample sizes or dimensionality reduction techniques such as PCA.
Limitations
Section titled “Limitations”- Slow inference on large datasets. Every query scans the training set ().
- Sensitive to feature scale and irrelevant attributes. Noise features degrade Euclidean distances as much as informative ones.
- Degrades in high dimensions due to the curse of dimensionality.
- Provides zero feature interpretability. Unlike linear regression weights, KNN provides no direct measure of feature importance.
Visualization
Section titled “Visualization”The interactive tool below displays 35 training points (solid dots) and 15 test points (open circles) from the two moons dataset. Background coloring indicates the class predicted by KNN across space, tracing decision boundaries where colors change. The slider adjusts across odd integers from 1 to 35, updating training and test accuracies below. You can drag the query marker (crosshair) anywhere: it connects to its nearest neighbors, displaying vote counts per class. Try the following:
- Initial state at : boundaries are jagged, with an isolated class-1 island around noisy point inside class 0 territory. Training accuracy is 1.000, test accuracy is 0.733.
- Drag the crosshair to : at , it connects to the outlier, predicting class 1. Increasing to shifts the vote to 4 vs. 1, correctly predicting class 0.
- Slide to 7: the noise island vanishes, forming smooth boundaries matching the crescents. Training accuracy settles at 0.943 while test accuracy reaches 1.000.
- Advance to 15 and 35: boundaries flatten, and test accuracy drops to 0.800 then 0.467. At , the entire background turns class 1.
Code Examples
Section titled “Code Examples”The three cells below execute independently.
The first cell implements KNN from scratch using NumPy for query point at . np.argsort(dist) orders points by increasing distance, [:k] extracts the nearest neighbors, and np.bincount tallies votes.
Custom KNN implementation for a query point
import numpy as npfrom sklearn.datasets import make_moonsfrom sklearn.model_selection import train_test_split
# Two interleaving "moons": 50 points, 2 features, label 0 or 1.X, y = make_moons(n_samples=50, noise=0.3, random_state=11)X = np.round(X, 2)X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0, stratify=y)
x_new = np.array([-0.4, 0.6]) # point to classifyk = 5
# Step 1: Euclidean distance from x_new to every training point.dist = np.sqrt(((X_train - x_new) ** 2).sum(axis=1))# Step 2: indices of the k nearest points.nearest = np.argsort(dist)[:k]for i in nearest: print(f"point {X_train[i]}, label {y_train[i]}, " f"distance {dist[i]:.3f}")
# Step 3: count the votes per class; the class with most votes wins.votes = np.bincount(y_train[nearest], minlength=2)print(f"votes: class 0 = {votes[0]}, class 1 = {votes[1]}")print(f"k = {k}: predict {votes.argmax()}")print(f"k = 1: predict {y_train[nearest[0]]}")The first run downloads Python, about 10 MB.
The first five rows match our manual intuition table: neighbor belongs to class 1 at distance 0.155, followed by four class-0 neighbors. The vote is 4 to 1 for class 0, whereas would predict class 1.
The second cell runs KNeighborsClassifier across all odd values of from 1 to 35, printing sample accuracies and plotting train and test curves against .
Training and test accuracy across k
import numpy as npimport matplotlib.pyplot as pltfrom sklearn.datasets import make_moonsfrom sklearn.model_selection import train_test_splitfrom sklearn.neighbors import KNeighborsClassifier
# Same 50 points and same split as the first cell: 35 train, 15 test.X, y = make_moons(n_samples=50, noise=0.3, random_state=11)X = np.round(X, 2)X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0, stratify=y)
ks = range(1, 36, 2) # odd k only, so votes never tieacc_train, acc_test = [], []for k in ks: model = KNeighborsClassifier(n_neighbors=k).fit(X_train, y_train) # A classifier's score() returns accuracy: correct / total. acc_train.append(model.score(X_train, y_train)) acc_test.append(model.score(X_test, y_test)) if k in (1, 3, 5, 7, 9, 15, 25, 35): print(f"k = {k:2d}: train {acc_train[-1]:.3f}, " f"test {acc_test[-1]:.3f}")
plt.plot(ks, acc_train, "o-", label="train")plt.plot(ks, acc_test, "s--", label="test")plt.xlabel("k")plt.ylabel("accuracy")plt.legend()plt.show()The first run downloads Python, about 10 MB.
The output verifies our table: training accuracy decreases monotonically, while test accuracy peaks at before falling, forming the classic inverted U-curve. At , exactly two training points are misclassified: and —both outliers. Tolerating errors on these two points yields a regularized, generalizable boundary.
The third cell evaluates the load_wine dataset: 178 bottles, 13 features, 3 cultivars. It prints standard deviations for proline and hue, their squared distance contributions, and benchmarks KNN accuracy with and without StandardScaler. Finally, GridSearchCV selects optimal via 5-fold cross-validation on training data.
Standardization and parameter tuning via cross-validation
from sklearn.datasets import load_winefrom sklearn.model_selection import GridSearchCV, train_test_splitfrom sklearn.neighbors import KNeighborsClassifierfrom sklearn.pipeline import make_pipelinefrom sklearn.preprocessing import StandardScaler
# 178 wines, 13 chemical features, 3 grape cultivars (labels 0, 1, 2).X, y = load_wine(return_X_y=True)X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0, stratify=y)# Column 12 (proline) is thousands of times larger than column 10 (hue).print(f"std: proline {X_train[:, 12].std():.1f}, " f"hue {X_train[:, 10].std():.3f}")# Squared difference per column, for every (test, train) pair.diff2 = (X_test[:, None, :] - X_train[None, :, :]) ** 2print(f"proline share: {diff2[:, :, 12].sum() / diff2.sum():.2%}")
raw = KNeighborsClassifier(n_neighbors=5)scaled = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))raw.fit(X_train, y_train)scaled.fit(X_train, y_train) # the scaler learns mean, std on trainprint(f"raw k = 5: test accuracy {raw.score(X_test, y_test):.3f}")print(f"scaled k = 5: test accuracy {scaled.score(X_test, y_test):.3f}")
# Choose k by 5-fold cross-validation, on the training set only.grid = GridSearchCV(scaled, {"kneighborsclassifier__n_neighbors": list(range(1, 32, 2))}, cv=5)grid.fit(X_train, y_train)best_k = grid.best_params_["kneighborsclassifier__n_neighbors"]print(f"best k = {best_k}, CV accuracy {grid.best_score_:.3f}")print(f"test accuracy {grid.score(X_test, y_test):.3f}")The first run downloads Python, about 10 MB.
- Standard deviations are 325.4 for proline and 0.221 for hue. Proline accounts for 99.76% of unscaled squared distances, blinding the model to other chemical dimensions.
- With , standardizing features boosts test accuracy from 0.722 (39/54 correct) to 0.963 (52/54 correct)—purely from normalization without model alterations.
- Cross-validation identifies with CV score 0.976. Retrained on full training data, final test accuracy reaches 0.981 (53/54 bottles).
Notice that StandardScaler resides inside the pipeline, ensuring means and variances are recomputed per fold to prevent validation leakage.
Common Pitfalls
Section titled “Common Pitfalls”Quick Check
Section titled “Quick Check”Summary and Further Reading
Section titled “Summary and Further Reading”Key Takeaways:
- KNN predicts using the closest training points under Euclidean distance : majority voting for classification and mean averaging for regression. As a lazy learner,
fitmerely stores samples, with inference costing . - Small creates jagged boundaries and overfits (training accuracy 1.000); large over-smooths, with unconditionally predicting the global majority class (underfitting). Tune via cross-validation, using odd for binary classification.
- Always scale features inside a pipeline. On the wine dataset,
StandardScaleralone lifts test accuracy from 0.722 to 0.963. - Parameter
weights='distance'weights votes inversely by proximity. In high dimensions, the curse of dimensionality expands hypercube neighborhoods to , rendering distance metrics indistinguishable.
Further Reading:
- Vũ Hữu Tiệp (2017). Machine Learning cơ bản, Bài 6: K-nearest neighbors. Introductory guide with iris examples, distance weighting, and regression variants.
- scikit-learn developers (2007–2026). scikit-learn 1.8 User Guide: Nearest Neighbors, Sections 1.6.1 and 1.6.2. Details on neighbor search algorithms (
kd_tree,ball_tree,brute), weighting schemes, and tie-breaking policies. - James, G., Witten, D., Hastie, T., Tibshirani, R. (2021). An Introduction to Statistical Learning (2nd ed.). Section 2.2.3 and 3.5 analyze KNN flexibility, comparing it with linear models and examining the curse of dimensionality.
- Cover, T. M., Hart, P. E. (1967). Nearest neighbor pattern classification. IEEE Transactions on Information Theory 13(1), 21–27. Foundational theoretical paper proving error bounds for 1-nearest neighbor classification.
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About