KNN & Naive Bayes

core 12 minknnnaive-bayesclassificationlazy-learningtext-classificationbaselines

Builds on:Bayes' theoremBias-variance tradeoffFeature scaling and preprocessing

The idea

These two families are merged here on purpose: they are the two simplest classifiers that are still strong baselines, and each fits in one sentence. KNN says you are what your neighbors are. Naive Bayes says multiply the evidence and pretend the features never talk to each other. One defers all work to query time; the other compresses the data to counts and defers nothing. Interviewers love both because each has one big idea and one famous failure mode.

K-nearest neighbors: prediction by memory

KNN has no training step — it is lazy (instance-based) learning. The "model" is the stored training set. To classify a query xx, find the kk training points closest to it under some distance and take a majority vote:

y^(x)=mode⁡{ yi:i∈Nk(x) }\hat{y}(x) = \operatorname{mode}\{\, y_i : i \in N_k(x) \,\}

where Nk(x)N_k(x) is the index set of the kk nearest neighbors. Training costs O(1)O(1) (store the data); every query costs O(nd)O(nd) — a full scan of nn points in dd dimensions. Eager models pay once up front; KNN pays at every prediction, which is exactly backwards from what production usually wants.

kk is the bias-variance dial. At k=1k = 1 the classifier memorizes the training set — zero training error, a Voronoi-cell boundary around every point, and maximal variance: one mislabeled neighbor flips predictions in its whole cell. At k=nk = n every query receives the global majority class — maximal bias, zero variance, and the model has collapsed into "predict the prior". Tune kk by cross-validation (a common starting heuristic is k≈nk \approx \sqrt{n}), and use odd kk for binary problems to dodge ties.

Distance choice and scaling. Standard metrics:

Because distances add raw per-feature differences, scaling is mandatory: an income gap of 1000 contributes 10002=1061000^2 = 10^6 to squared Euclidean distance while a 25-year age gap contributes 252=62525^2 = 625 — the neighbor search becomes a one-feature model. Standardize (or min-max) before any distance computation.

The famous failure mode: the curse of dimensionality. As dd grows with nn fixed, pairwise distances concentrate — the ratio of the farthest to the nearest neighbor's distance tends toward 1, so "nearest" carries almost no information and the vote approaches a poll of random points. Each irrelevant feature adds pure noise to every distance. Mitigations: feature selection, PCA or learned embeddings, cosine on sparse data.

Query-cost mitigations: KD-trees and ball trees give roughly logarithmic queries but only help in low dimension (rule of thumb: dd below about 20). Beyond that, approximate nearest neighbors — LSH, HNSW graphs (the engines inside libraries like FAISS) — trade exactness for orders-of-magnitude speedups.

Naive Bayes: multiply the evidence

Start from Bayes' rule and drop the denominator, which is constant across classes:

P(y∣x)∝P(y) P(x∣y)P(y \mid x) \propto P(y)\, P(x \mid y)

Estimating the joint P(x∣y)P(x \mid y) over dd features is hopeless — the table is exponential in dd. The naive move assumes features are conditionally independent given the class:

P(x∣y)=∏j=1dP(xj∣y)⇒y^=arg⁡max⁡y[log⁡P(y)+∑jlog⁡P(xj∣y)]P(x \mid y) = \prod_{j=1}^{d} P(x_j \mid y) \qquad\Rightarrow\qquad \hat{y} = \arg\max_y \Big[ \log P(y) + \sum_j \log P(x_j \mid y) \Big]

That factorization turns one dd-dimensional estimation problem into dd one-dimensional ones — which is why NB trains in a single counting pass and shrugs at a 100,000-word vocabulary. Always work in log space: a product of hundreds of probabilities like 1/81/8 underflows double precision after a few hundred factors; sums of logs never do.

Variants — match the likelihood to the feature type:

Laplace smoothing. With counts, an unseen word-class pair gives P(w∣y)=0P(w \mid y) = 0, and one zero in a product vetoes the entire class no matter how strong the rest of the evidence is. Add-one smoothing fixes this:

P^(w∣y)=Nwy+αNy+αV,α=1\hat{P}(w \mid y) = \frac{N_{wy} + \alpha}{N_y + \alpha V}, \qquad \alpha = 1

where NwyN_{wy} is the count of word ww in class yy, NyN_y the total token count in class yy, and VV the vocabulary size. (α\alpha below 1 — Lidstone smoothing — often edges out add-one on real text.)

Calibration caveat. Correlated features get multiplied in as if independent, so NB's posteriors slam toward 0 and 1. The ranking and the argmax are usually fine; the probabilities are not. Calibrate (Platt scaling, isotonic regression) before anyone consumes them as probabilities.

Worked example

Multinomial NB spam filter. Four training documents, vocabulary V=3V = 3 words: free, win, meeting.

DocClassText
d1spam"free win"
d2spam"free free win"
d3ham"meeting"
d4ham"free meeting"

Priors: 2 spam, 2 ham out of 4 docs, so P(spam)=P(ham)=1/2P(\text{spam}) = P(\text{ham}) = 1/2. Token counts per class (spam has 55 tokens total, ham has 33):

WordCount in spamCount in ham
free31
win20
meeting02
total53

Laplace-smoothed likelihoods, denominators 5+3=85 + 3 = 8 (spam) and 3+3=63 + 3 = 6 (ham):

WordSmoothed prob. given spamSmoothed prob. given ham
free(3+1)/8=1/2(3+1)/8 = 1/2(1+1)/6=1/3(1+1)/6 = 1/3
win(2+1)/8=3/8(2+1)/8 = 3/8(0+1)/6=1/6(0+1)/6 = 1/6
meeting(0+1)/8=1/8(0+1)/8 = 1/8(2+1)/6=1/2(2+1)/6 = 1/2

Sanity check: each column sums to 1 (4+3+18\tfrac{4+3+1}{8} and 2+1+36\tfrac{2+1+3}{6}).

Classify "free win":

score(spam)=12⋅12⋅38=332,score(ham)=12⋅13⋅16=136.\begin{aligned} \text{score}(\text{spam}) &= \tfrac{1}{2}\cdot\tfrac{1}{2}\cdot\tfrac{3}{8} = \tfrac{3}{32}, \\ \text{score}(\text{ham}) &= \tfrac{1}{2}\cdot\tfrac{1}{3}\cdot\tfrac{1}{6} = \tfrac{1}{36}. \end{aligned}

Normalize over a common denominator of 288: 3/32=27/2883/32 = 27/288 and 1/36=8/2881/36 = 8/288, so

P(spam∣"free win")=2727+8=2735≈0.771P(\text{spam} \mid \text{"free win"}) = \frac{27}{27 + 8} = \frac{27}{35} \approx 0.771

Why the smoothing mattered: without it, P(win∣ham)=0/3=0P(\text{win} \mid \text{ham}) = 0/3 = 0, the ham score collapses to exactly zero, and the posterior becomes a certainty of 1.0 for spam — one never-seen word-class pair silently vetoes a class. With a realistic vocabulary this happens on virtually every document.

Log-space view: the posterior odds are (3/32)/(1/36)=27/8(3/32)\big/(1/36) = 27/8, and log⁡(27/8)≈1.216\log(27/8) \approx 1.216 — the same 27/3527/35 after the sigmoid. On real documents you are summing hundreds of terms like log⁡(1/8)\log(1/8); multiplying the raw probabilities instead would underflow double precision after a few hundred words.

Cheat sheet

KNN

Naive Bayes

Head to head

KNNNaive Bayes
Core assumptionSmoothness: nearby points share labelsConditional independence of features given class
Training costO(1)O(1) — store the data (O(nd)O(nd) memory)O(nd)O(nd) — one counting pass
Query costO(nd)O(nd) exact; sublinear with trees/ANNO(cd)O(cd) for cc classes
Feature scalingMandatoryEssentially irrelevant
Decision boundaryArbitrarily wiggly (Voronoi at k=1k = 1)Linear (multinomial, tied-variance Gaussian) or quadratic (general Gaussian)
High dimensionsDegrades — distances concentrateFine — dd separate 1-D estimates
Probability outputVote fraction, coarse for small kkOverconfident; ranking still good

Symptom to fix

SymptomDiagnosisFix
KNN predictions track a single featureUnscaled featuresStandardize / min-max before distances
KNN collapses after adding featuresDistance concentration (curse of dimensionality)Feature selection, PCA, embeddings, cosine on sparse data
KNN inference too slowO(nd)O(nd) scan per queryKD/ball tree (low dd), HNSW/LSH (high dd)
KNN always predicts majority classkk too large or class imbalanceSmaller kk, distance-weighted votes
NB scores a class as exactly impossibleZero count for a feature-class pairLaplace / additive smoothing
NB probabilities pinned at 0 or 1Correlated features double-countedExpected; calibrate (Platt / isotonic) if probabilities matter
NB scores underflow to zeroProduct of many small probabilitiesSum log-probabilities instead