Past tasks
Discord

4Classical Machine Learning 4.4Classical Machine Learning Models

4.4.3K-Nearest Neighbors

Predict a new point's label from the k training points closest to it.

Edit this page

The idea

K-NN has no training step. It stores the training set, and when a new point arrives, it finds the kk stored points closest to it and lets them decide.

Because nothing is fitted in advance, K-NN is called a lazy or instance-based method: all of the work happens at prediction time. It is also non-parametric. The model is the training data itself, so with enough data it can follow a decision boundary of any shape.

Algorithm K-NN prediction for one query point
  1. for each training point xix_i, compute d(x,xi)d(x, x_i)
  2. NN \leftarrow indices of the kk smallest distances
  3. if classifying, return the most common label among yiy_i for iNi \in N
  4. else return the mean 1kiNyi\frac{1}{k} \sum_{i \in N} y_i

Measuring distance

A K-NN model is only as good as its distance. For vectors x,zRDx, z \in \mathbb{R}^D the common choices are:

Distance Norm Formula When to use it
Euclidean L2L_2 j(xjzj)2\sqrt{\sum_j (x_j - z_j)^2} The default for continuous features on similar scales
Manhattan L1L_1 jxjzj\sum_j \lvert x_j - z_j \rvert One large difference in a single feature counts for less than under L2L_2
Minkowski LpL_p (jxjzjp)1/p\left(\sum_j \lvert x_j - z_j \rvert^p\right)^{1/p} Covers both: p=1p = 1 is Manhattan and p=2p = 2 is Euclidean
Cosine 1xzxz1 - \dfrac{x^\top z}{\lVert x \rVert \, \lVert z \rVert} Embeddings and text vectors, where direction matters more than length

scikit-learn uses Minkowski with p=2p = 2 by default. metric="cosine" also works; scikit-learn then searches by brute force.

Scale your features

Data Processing covers other normalization methods.

Choosing k

The value of kk trades variance against bias.

  • Small kk follows individual training points, noisy ones included. The boundary is jagged, and k=1k = 1 scores 100% on the training set because every point is its own nearest neighbor.
  • Large kk averages over a wide region, so the boundary is smooth but can miss real structure. At k=nk = n, every query gets the most common class in the whole training set.

In practice:

  • With two classes, an odd kk avoids tied votes.
  • weights="distance" weights each neighbor by 1/d(x,xi)1/d(x, x_i), so closer neighbors count for more. The result is usually less sensitive to the exact kk.
  • Choose kk with cross-validation, never with the test set. Trying every kk from 1 to 30 is cheap on small datasets.

Cost in time and memory

Training only stores the data. Brute-force prediction compares the query with all nn training points in DD dimensions, which costs O(nD)O(nD) time per query and O(nD)O(nD) memory for the stored set. For the 60,000 MNIST training images of 784 pixels, that is about 47 million subtractions for every test image.

Two ways to search faster:

  • KD-trees and ball trees split space so that whole regions can be skipped. They are fast for fewer than about 20 dimensions and lose their advantage as DD grows. With algorithm="auto", scikit-learn chooses for you.
  • Approximate nearest neighbor libraries such as FAISS give up a little accuracy for much faster search. This is how search over millions of embeddings works in practice.

The curse of dimensionality

In high dimensions, distances stop being informative: the nearest and the farthest points from a query end up almost equally far away. The table measures this for 1,000 points drawn uniformly from the unit cube [0,1]D[0, 1]^D, averaged over 200 random queries.

Dimension DD Nearest ÷ farthest distance
2 0.016
10 0.271
100 0.697
1,000 0.894

As the ratio approaches 1, "nearest" means less and less. Reduce the dimension first with PCA, or run K-NN on learned embeddings, where distance reflects meaning. Real data usually lies near a much lower-dimensional structure than uniform noise, which is part of why K-NN still does reasonably well on 784-pixel digits.

In scikit-learn

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=0
)
model = make_pipeline(StandardScaler(), KNeighborsClassifier())
param_grid = {
    "kneighborsclassifier__n_neighbors": range(1, 31),
    "kneighborsclassifier__weights": ["uniform", "distance"],
}
search = GridSearchCV(model, param_grid, cv=5)
search.fit(X_train, y_train)
print(search.best_params_)
print("test accuracy:", search.score(X_test, y_test))

make_pipeline names each step after its class in lowercase, which is where the kneighborsclassifier__ prefix comes from. Because the scaler is inside the pipeline, every cross-validation fold fits it on that fold's training part only. Hyperparameter Tuning explains grid search in more depth. For regression, use KNeighborsRegressor in the same way.

Parameters

Parameter Default What it controls
n_neighbors 5 kk, the number of neighbors that vote
weights "uniform" "distance" weights each neighbor by 1/d1/d
metric "minkowski" The distance function
p 2 The Minkowski power: 1 is Manhattan, 2 is Euclidean
algorithm "auto" How neighbors are found. It changes speed; results differ only when distances tie

From scratch in NumPy

Writing K-NN yourself takes a few lines and shows exactly what the library computes.

import numpy as np

def knn_predict(X_train, y_train, X_query, k=5):
    # Squared distance from every query to every training point, shape (q, n)
    d2 = ((X_query[:, None, :] - X_train[None, :, :]) ** 2).sum(axis=2)
    # Indices of the k smallest distances in each row, in no particular order
    nearest = np.argpartition(d2, kth=k - 1, axis=1)[:, :k]
    votes = y_train[nearest]
    return np.array([np.bincount(row).argmax() for row in votes])

On the Iris split above, it makes the same predictions as KNeighborsClassifier(algorithm="brute") for all 30 test points. A few details:

  • The square root is skipped because it doesn't change which points are closest.
  • np.argpartition finds the kk smallest values in O(n)O(n) per row, faster than a full O(nlogn)O(n \log n) sort.
  • np.bincount(row).argmax() needs integer labels 0,1,2,0, 1, 2, \dots and breaks ties toward the smaller label.
  • Broadcasting builds a (q,n,D)(q, n, D) array. For large inputs, use xz2=x2+z22xz\lVert x - z \rVert^2 = \lVert x \rVert^2 + \lVert z \rVert^2 - 2x^\top z with one matrix product, which needs only a (q,n)(q, n) array.

K-NN in olympiad tasks

K-NN rarely wins a task on raw features, but it is useful in two ways.

As a baseline. It takes a minute to write and has one hyperparameter that really matters. Its validation score tells you how much a heavier model has to beat to be worth the time.

On top of embeddings. A pretrained network maps images, text or audio to vectors where similar inputs lie close together. K-NN on those vectors gives a classifier with no training, and supporting a new class only means adding its examples.

Reach for K-NN when the dataset is small, the features are few or come from a good embedding, and you need a working model quickly. Avoid it on many raw features, or when the training set is too large to scan for every query within the time limit.

Resources

SourceTitleWhy read it
scikit-learnUser Guide: Nearest NeighborsClassification and regression with neighbors, and when to use brute force, KD-trees or ball trees.
Stanford CS231nImage Classification: k-Nearest NeighborBuilds K-NN on raw CIFAR-10 pixels, tunes k on a validation set, and shows why pixel distance is a weak measure for images.
James et al.An Introduction to Statistical Learning, chapters 2 and 3Free book. Uses K-NN to explain the bias–variance tradeoff and compares K-NN regression with linear regression.
scikit-learnImportance of Feature ScalingRuns K-NN on the Wine dataset with and without scaling, with plots of the decision boundaries.

Practice problems

SolvedSourceProblemDifficultyTags
IOAI 2024 Help BOBAI: More classification in an unknown language Medium nlp, embeddings
Kaggle Digit Recognizer Easy vision, baseline
Kaggle Titanic Easy tabular, preprocessing