Let’s consider another example using handwritten digits. Each observation is a pixel image, so there are variables. Each variable records the grayscale value at one pixel. Can nearest neighbors recognize the digit from these pixel values?
# Handwritten Digit Recognition Datalibrary(ElemStatLearn)# the first column is the true digitdim(zip.train)
[1] 7291 257
Show the reproducible code
dim(zip.test)
[1] 2007 257
Show the reproducible code
# look at one sampleimage(zip2image(zip.train, 1), col=gray(256:0/256), zlim=c(0,1),xlab="", ylab="", axes =FALSE)
[1] "digit 6 taken"
Use the original training data and test data. The files contain the same observations and split as the R data. Save them in data/knn and run the code from the folder containing data.
Show the reproducible code
import numpy as npimport matplotlib.pyplot as pltfrom pathlib import Path# Handwritten digit recognition data; the first column is the true digit.zip_train = np.loadtxt(Path("data/knn/zip-train.csv.gz"), delimiter=",")zip_test = np.loadtxt(Path("data/knn/zip-test.csv.gz"), delimiter=",")print(zip_train.shape)
(7291, 257)
Show the reproducible code
print(zip_test.shape)
(2007, 257)
Show the reproducible code
# Look at the same sample as in R.print("digit", int(zip_train[0, 0]), "taken")
The training data contain 7,291 images, and the test data contain 2,007 images. We use 3NN to predict the digits in the test data and examine the error rate and confusion matrix. The matrix has predicted digits in rows and observed digits in columns.
Most predictions lie on the diagonal of the confusion matrix, so 3NN recognizes most test digits correctly. There are 256 predictors here. Does this mean that nearest neighbors will work well whenever there are many predictors?1
The Curse of Dimensionality
Many practical problems have high-dimensional data. The handwritten digits have pixel measurements, and genetic studies can contain many thousands of gene-expression measurements. For a fixed sample size , observations become sparse as the number of variables increases. A target point may then have few training observations close to it, making local averaging less effective.
To see why, consider observations drawn uniformly from a -dimensional unit cube. We want to use neighbors. If the regression function changes little over a small neighborhood, averaging responses from that neighborhood can have small bias.
For , a square with side length has area . It contains observations in expectation. More generally, a cube of side length contained in the unit cube has expected count . To obtain an expected count of , we need
Keeping fixed gives:
If , .
If , .
If , .
As dimension increases, the cube must span much more of each coordinate’s range to contain the same expected number of observations. The side length describes this cube; it is not the distance of every neighbor from the target in every coordinate. The calculation shows why a neighborhood can stop being local. If the regression function changes substantially over that neighborhood, the bias can be large. Decreasing may not solve the problem because even the closest observations can be far away, while averaging fewer noisy responses increases variance.
A small cube of side length ell inside a unit cube.
An Experiment
Let’s examine this issue with a small simulation. Generate independent covariates, each uniformly distributed on , and let the outcome depend on the first three predictors:
with errors independent of the covariates and one another. Thus . At the target , the true mean is . This target is a corner of the covariate support.
Recall the repeated simulations from ridge regression. Here, we generate a new training dataset, fit 5NN, and predict at the same target in each repetition. Repeating this 300 times estimates
This is mean squared error for estimating the regression function at the target. We compare the prediction with the true mean, so this quantity does not include a new noisy outcome at . The expectation averages over the randomly generated training covariates and responses.
# Let's try a new package FNNlibrary(FNN) p =10 n =100# the target prediction point# be careful that this needs to be a 1xp matrix x0 =matrix(rep(0, p), nrow =1, ncol = p)# number of simulations nsim =300# vector to store predicted values allerror =rep(NA, nsim)for (l in1:nsim) {# generate data X =matrix(runif(n*p), nrow = n, ncol = p) y = X[, 1] + X[, 2] + X[, 3] +rnorm(n, sd =0.5)# "5-nearest neighbor" regression using the FNN package# for this question, use the "brute force" algorithm to search for the NNs knn.fit =knn.reg(train = X, test = x0, y = y,k =5, algorithm ="brute")# record the prediction error of this run# the truth f(x_0) is 0 allerror[l] = (knn.fit$pred -0)^2 }# the prediction errormean(allerror)
[1] 0.9483551
Show the reproducible code
from sklearn.neighbors import KNeighborsRegressorp =10n =100# The target must have one row and p columns.x0 = np.zeros((1, p))# Number of simulations and storage for the squared errors.nsim =300allerror = np.full(nsim, np.nan)for l inrange(nsim): X = np.random.uniform(0, 1, size=(n, p)) y = X[:, 0] + X[:, 1] + X[:, 2] + np.random.normal(0, 0.5, n)# Use brute-force neighbor search, as in the R example. knn_fit = KNeighborsRegressor(n_neighbors=5, weights="uniform", algorithm="brute") knn_fit.fit(X, y) allerror[l] = (knn_fit.predict(x0)[0] -0)**2print(np.mean(allerror))
1.0438422123340028
Each entry of allerror is a squared error from one newly generated dataset; its average estimates the displayed expectation. At this corner, nearby training observations still have positive values in the first three coordinates, so their mean responses can be well above the target’s true mean of zero. The numerical average varies across runs.
Practice question
Use the same code to calculate the mean squared error at , another corner of the support. Here . What are the errors when , and ? The example below sets ; change to examine the other dimensions.
# Let's try a new package FNNlibrary(FNN) p =20 n =100# the target prediction point# be careful that this needs to be a 1xp matrix x0 =matrix(c(rep(1, 3), rep(0, p-3)), nrow =1, ncol = p)# number of simulations nsim =300# vector to store predicted values allerror =rep(NA, nsim)for (l in1:nsim) {# generate data X =matrix(runif(n*p), nrow = n, ncol = p) y = X[, 1] + X[, 2] + X[, 3] +rnorm(n, sd =0.5)# "5-nearest neighbor" regression using the FNN package# for this question, use the "brute force" algorithm to search for the NNs knn.fit =knn.reg(train = X, test = x0, y = y,k =5, algorithm ="brute")# record the prediction error of this run# the truth f(x0) is 3 allerror[l] = (knn.fit$pred -3)^2 }# the prediction errormean(allerror)
Show the reproducible code
from sklearn.neighbors import KNeighborsRegressorp =20n =100# The first three target coordinates are one; the others are zero.x0 = np.array([[1, 1, 1] + [0] * (p -3)])nsim =300allerror = np.full(nsim, np.nan)for l inrange(nsim): X = np.random.uniform(0, 1, size=(n, p)) y = X[:, 0] + X[:, 1] + X[:, 2] + np.random.normal(0, 0.5, n) knn_fit = KNeighborsRegressor(n_neighbors=5, weights="uniform", algorithm="brute") knn_fit.fit(X, y) allerror[l] = (knn_fit.predict(x0)[0] -3)**2print(np.mean(allerror))
Discussion
Why, then, did 3NN perform well in the handwritten digit example? The pixel values may have an approximately lower-dimensional structure: they do not vary as 256 unrelated measurements. Distances in the full pixel space can still identify similar digits, and the digit classes may be well separated along that structure. The test performance is consistent with this explanation; it does not prove that an equivalent lower-dimensional representation exists.
Dimension reduction is an important topic in statistical learning and machine learning. Methods such as sliced inverse regression (Li, 1991) and UMAP (McInnes et al., 2018) investigate lower-dimensional representations of data.
A rolled surface and a curved line illustrate lower-dimensional structure in three-dimensional space.
Image from Cayton (2005).
Return to the simulation above. What if all covariates were highly linearly dependent? Would that give an approximately lower-dimensional representation and potentially improve prediction accuracy? Can you use a simulation study to investigate this?
Footnotes
The R and Python functions can resolve tied class votes differently, so their test errors need not be exactly equal.↩︎