Part IV — Modeling · Chapter 11

k-NN, Perceptron and MLP

~50 min read6 interactive widgets4 plates

In this chapter

  1. k-Nearest Neighbors: the lazy model
  2. Tuning k: what do you expect?
  3. k=1 without normalization: scale wins
  4. k=1 with min-max normalization
  5. k=10 with min-max normalization: the vote
  6. Plotting the accuracy — and a bug in the deck
  7. Perceptron: the binary classifier
  8. One Versus All: three yes/no questions
  9. Perceptron on Iris
  10. Perceptron: changing the seed
  11. Multi-layer perceptron: the nonlinear boundary
  12. Exercise: the wine dataset
  13. Check your understanding

1. k-Nearest Neighbors: the lazy model

The deck now opens the third model of the lecture: k-Nearest Neighbors. kNN is the archetype of instance-based learning — the category from Chapter 8's taxonomy where the model compares new data points to known ones instead of detecting a pattern in the training data:

The model dynamics rule

Chapter 9's rule applies here too: understand the dynamics, not only the result. For kNN the dynamics are governed by distance — and distance depends on the scale of the features. That is the subject of the next four sections.

2. Tuning k: what do you expect?

As with max_depth in Chapter 9, the deck stops before running anything and asks: what do you expect?

Expectation: every point takes the label of its single nearest neighbor. The boundary will be maximally detailed — jagged regions that hug the training points, with every isolated flower owning a private territory. Sensitive to noise: one atypical point creates a bubble around itself.

Expectation: each point consults a neighborhood of 10 and takes the majority label. The boundary becomes smoother — small islands disappear because no single neighbor can swing a vote of ten. The price: a genuinely isolated point may be overruled by its neighbors.

There is a second expectation the deck sets up quietly: normalization. The next two slides run the same model (k=1) twice — once on the raw petal features and once after min-max normalization — to show that the boundary changes purely because the distance function changed.

3. k=1 without normalization: scale wins

from sklearn.neighbors import KNeighborsClassifier
knn = plot_boundary(KNeighborsClassifier(n_neighbors=1), "knn_cplot", norm=False)

Without normalization, the two petal features live on different scales: petal length ranges up to ~6.9 cm, petal width only up to ~2.5 cm. Euclidean distance therefore measures almost only petal length — the width contributes little. The effect:

This is the first dynamics lesson of the chapter: kNN has no internal notion of feature importance — the scale of the features is the importance, silently encoded in the distance.

k=1 — RAW vs MIN-MAX NORMALIZED raw features vertical cuts: petal length dominates min-max normalized both features now shape the boundary same model, same data, same k — only the distance function changed. ● setosa ● versicolor ● virginica
Plate 11.1 — The deck's first k=1 comparison (reconstructed from the deck's boundary plots). Raw features: petal length dominates the distance, so the regions are mostly vertical strips. After min-max normalization each feature spans [0, 1] and both contribute, so the boundary tilts and splits differently.

4. k=1 with min-max normalization

knn = plot_boundary(KNeighborsClassifier(n_neighbors=1), "knn_cplot", norm=True)

Min-max normalization rescales each feature to the range [0, 1] before the distances are computed:

x' = (x - min(x)) / (max(x) - min(x))

Now both features span the same interval, and Euclidean distance weighs them equally. The k=1 boundary changes shape: regions that were pure vertical strips (Plate 11.1, left) become two-dimensional — the width of a flower finally counts in the nearest-neighbor ranking.

The deck's point is not that one boundary is "better" — it is that the model's behavior is a function of the preprocessing. Two runs with identical data, model and k produce different regions solely because the features were rescaled. When the exam asks "why does the boundary change?", the answer is normalization, not a different algorithm.

5. k=10 with min-max normalization: the vote

knn = plot_boundary(KNeighborsClassifier(n_neighbors=10), "knn_cplot", norm=True)

Keeping normalization and raising k to 10 shows the second dynamics: the majority vote smooths the boundary.

MIN-MAX — k=1 vs k=10 k = 1 every isolated flower owns a bubble k = 10 three smooth regions, bubbles gone higher k = larger neighborhoods = smoother regions, at the cost of following isolated but real structure.
Plate 11.2 — The deck's k=10 normalized plot, contrasted with k=1. Ten votes average out the private bubbles: the boundary now follows the clouds, not the points.

Widget — the kNN boundary, with real flowers

The widget below computes true k-NN decisions on the 150 real iris flowers (petal plane). Move k and toggle normalization to reproduce the deck's three plots — and the ones in between.

6. Plotting the accuracy — and a bug in the deck

As with the decision tree, the deck closes the kNN story with a training-vs-test accuracy plot over k = 1..9. But the loop as printed in the deck contains copy-paste errors — and the lecture's own disclaimer (§4 of Chapter 8) says it out loud: do not rely on external code without knowing what the code is doing. Here is exactly what the deck prints (slide 49):

# Prepare to store max_depth values and corresponding accuracies
k_s = range(1, 10)
train_accuracies, test_accuracies = [], []
for k in k_s: # Train decision trees with increasing max_depth
    knn = KNeighborsClassifier(n_neighbors=k)
    knn.fit(X_train, y_train)
    train_acc = accuracy_score(y_train, clf.predict(X_train)) # Compute accuracy for training set
    test_acc = accuracy_score(y_test, clf.predict(X_test))   # Compute accuracy for test set
    train_accuracies.append(train_acc)
    test_accuracies.append(test_acc)
# Plot accuracies
plt.plot(max_depths, train_accuracies, label="Training", marker='o')
plt.plot(max_depths, test_accuracies, label="Test", marker='o')
plt.xticks(max_depths)
plt.xlabel("k"); plt.ylabel("Accuracy"); plt.legend(); plt.grid(True)
Editorial note — a bug in the deck's kNN loop

Verified against the source deck (slide 49), the loop above cannot produce the curve it claims, for three independent reasons:

This is precisely the kind of bug the lecture warns about: a plausible-looking plot that, if never questioned, teaches the wrong lesson. The corrected loop measures what the section intends:

k_s = range(1, 10)
train_accuracies, test_accuracies = [], []
for k in k_s:
    knn = KNeighborsClassifier(n_neighbors=k)
    knn.fit(X_train, y_train)
    train_acc = accuracy_score(y_train, knn.predict(X_train))
    test_acc = accuracy_score(y_test, knn.predict(X_test))
    train_accuracies.append(train_acc)
    test_accuracies.append(test_acc)
plt.plot(k_s, train_accuracies, label="Training", marker='o')
plt.plot(k_s, test_accuracies, label="Test", marker='o')
plt.xticks(k_s)
plt.xlabel("k"); plt.ylabel("Accuracy"); plt.legend(); plt.grid(True)

The table below gives the real values of the corrected loop on the deck's setup (seed 42, test size 0.2), for the raw features exactly as the deck's split provides them, and after min-max normalization:

k123456789
Raw · Training1.00.96670.950.95830.96670.96670.9750.95830.9667
Raw · Test1.01.01.01.01.01.00.96671.01.0
Min-max · Training1.00.96670.95830.95830.95830.95830.96670.96670.9667
Min-max · Test1.01.01.01.01.01.01.01.01.0

Read honestly: on this clean dataset, kNN is excellent across all k — the test accuracy is 1.0 almost everywhere, and even k=7 (raw) only drops one flower. The differences between k values that the boundary plots make so visible (jagged vs smooth) barely move the score. The boundary picture, not the number, is where the model's dynamics live — exactly the lecture's rule.

Widget — the (corrected) accuracy vs k curves

Hover the markers for the exact values.

7. Perceptron: the binary classifier

The deck switches model family: from distances to weights. The perceptron is the historical starting point of neural networks (and the bridge to Chapter 7's networks):

The deck's question is immediate and sharp: "Perceptron is binary classifier. How can we use it in Iris?" Iris has three classes, and the perceptron speaks in two. The next slide gives the answer.

8. One Versus All: three yes/no questions

The strategy is One Versus All (OVA):

OVA provides a way to use binary classification for a series of yes or no predictions across multiple possible labels. Given a classification problem with N possible solutions, an OVA solution consists of N separate binary classifiers — one binary classifier for each possible outcome.

The deck's example is fruit recognition: four recognizers, each answering a different question — is this an apple? is this an orange? is this a banana? is this a grape? During training, each binary classifier learns its own yes/no question; at prediction time, the answers are combined: pick the prediction of the non-zero class which is the most certain, and use the argmax of these scores (the class index with the largest score) to predict a class.

ONE VERSUS ALL — N BINARY QUESTIONS, ONE ARGMAX setosa vs rest score s₋ versicolor vs rest score s₂ virginica vs rest score s₃ class = argmax(s₋, s₂, s₃) the most certain yes wins three perceptrons, each a straight line; the final prediction is the one with the largest score. scikit-learn applies OVA automatically for multiclass.
Plate 11.3 — The OVA scheme behind the deck's Iris question. Three binary recognizers (three straight boundaries), three scores, one argmax — this is how a binary model serves a three-class problem.

Widget — click a flower, hear the three recognizers

Real OVA perceptrons trained on the petal plane. Click anywhere: each recognizer reports its score, and the argmax picks the class.

9. Perceptron on Iris

from sklearn.linear_model import Perceptron
perceptron = plot_boundary(Perceptron(random_state=seed), "perceptron_cplot")

With OVA under the hood, the perceptron learns three straight boundaries on the petal plane. The result is the classic linear picture: three half-planes meeting in the middle, with the versicolor/virginica frontier as the fragile one — the two species overlap enough that no single line separates them perfectly.

Measured on the deck's setup (seed 42), the perceptron is honest about this weakness: training accuracy 0.675, test accuracy 0.6333 — far below the tree, the forest and the kNN, all of which reached 1.0 on the same split. The straight-line family simply cannot draw the curved boundary that the petal data needs. This is a feature, not a failure: it is the model's dynamics made visible, and it is why the next slide and the MLP exist.

10. Perceptron: changing the seed

perceptron = plot_boundary(Perceptron(random_state=1), "perceptron_cplot")

Only the seed changes — and the boundary moves. On this run the perceptron scores training 0.9167, test 0.9: a completely different model, from the same code.

Why the seed matters so much here

The perceptron's training is order-dependent: samples are presented in a shuffled sequence (controlled by random_state), and each mistake updates the weights. Different orders walk different paths to (possibly different) solutions — and when the data is not linearly separable, as versicolor vs virginica is, the final boundary depends on where the walk stopped. The decision tree had tie-breaking randomness (Chapter 9); the perceptron has learning randomness: the same hyperparameters, a different seed, a different model. Compare the two boundaries in the widget below by re-training.

Widget — re-train the perceptron

This widget trains real OVA perceptrons on the petal plane. Press the button to re-train with a different seed and watch the three straight boundaries (and the accuracy) change.

11. Multi-layer perceptron: the nonlinear boundary

from sklearn.neural_network import MLPClassifier
mlp = plot_boundary(MLPClassifier(hidden_layer_sizes=(10, 20), random_state=seed, max_iter=1000), "mlp_cplot")

The multi-layer perceptron (MLP) adds hidden layers between input and output — here two layers with 10 and 20 neurons — and each neuron applies a nonlinearity. That single change removes the straight-line limitation:

LINEAR vs NONLINEAR perceptron (any seed) three straight lines MLP, hidden (10, 20) curved boundaries follow the clouds same data, same seed, same family of models — the hidden layers are the difference between three lines and a frontier that bends around the flowers.
Plate 11.4 — Perceptron vs MLP boundaries (reconstructed from the deck's two plots). The MLP's hidden layers (10, 20) curve the frontier; the perceptron is limited to straight lines, which is why its test accuracy (0.6333) lags the MLP's (0.9333).

Widget — train a small MLP

This widget trains a real 2-8-3 network (two inputs, one hidden layer of 8 neurons, softmax output) on the petal plane by gradient descent. Watch the boundary become nonlinear as training proceeds.

12. Exercise: the wine dataset

The deck closes the lecture with the exercise that the exam's style anticipates:

  1. Load the wine dataset from sklearn;
  2. Train a decision tree;
  3. … try different configurations of hyperparameters;
  4. What is your best accuracy?
  5. What are the most relevant features?

The exercise is deliberately open — it asks you to run the whole workflow of Chapters 8-10 on a new dataset. The deck's rules apply: do not stop at the first good result; question the pipeline; be able to explain every line.

Reference values (seed 42, test size 0.2) — check your run against these

Wine: 178 samples, 13 features, 3 classes, split 142/36. A plain decision tree with random_state=42 reaches test accuracy 0.9444 (34/36) at depth 3-4. The most relevant features are flavanoids (0.42), color_intensity (0.39) and proline (0.17) — three features carry nearly all the decision power. Tuning max_depth (and optionally min_samples_leaf) does not improve on 0.9444 on this split; a random search over the forest, as in Chapter 10, will typically match or tie it. The honest lesson: on a small clean dataset the model choice moves the score less than the understanding of the features.

Check your understanding

Why is kNN an instance-based model, and what is its hyperparameter?

kNN does not extract a pattern from training: the model is the stored training set, and prediction compares new points to the stored ones by distance (the instance-based category of Chapter 8). Its hyperparameter is k, the number of nearest neighbors whose labels vote for the prediction.

What changes between the k=1 raw and k=1 min-max plots, and why?

Only the distance function changes. Petal length spans ~0-6.9 cm while petal width spans ~0-2.5 cm, so raw Euclidean distance is dominated by petal length and the boundary is mostly vertical strips. Min-max normalization rescales each feature to [0, 1], both features contribute equally to the distance, and the boundary becomes genuinely two-dimensional. Same model, same data, same k — different preprocessing, different behavior.

What does raising k from 1 to 10 do to the boundary, and what is the trade-off?

The majority vote of 10 neighbors smooths the boundary: private bubbles around isolated points disappear and regions follow the data clouds. The trade-off is that genuinely informative isolated structure is also smoothed away — with very large k the boundary stops following the data (the kNN analogue of underfitting).

Describe the bug in the deck's kNN accuracy loop and how to fix it.

The loop fits knn but evaluates clf — the decision tree from earlier, still in scope — so the "kNN" accuracies are the tree's and constant across k. The plotting lines also use max_depths (undefined here; a NameError) instead of k_s. Fix: call knn.predict(...) and plot with k_s/xticks(k_s). This is the deck's own warning made concrete: code that looks right but teaches the wrong lesson if never questioned.

How does One Versus All let a binary perceptron classify Iris's three classes?

OVA trains N binary classifiers for N classes, each answering one yes/no question (setosa vs rest, versicolor vs rest, virginica vs rest). At prediction time each recognizer outputs a score, and the final class is argmax of the scores — the most certain yes wins. scikit-learn applies OVA automatically for multiclass.

Why does changing the seed change the perceptron's boundary so much?

Perceptron training is order-dependent: samples are shuffled (controlled by random_state) and each mistake updates the weights, so different orders follow different paths. When classes are not linearly separable (versicolor vs virginica), the final line depends on where the walk stopped. On the deck's split: seed 42 → test 0.6333, seed 1 → test 0.9 — same code, different models.

What does the MLP add over the perceptron, and what do their accuracies say?

The MLP adds hidden layers (here 10 and 20 neurons) with nonlinearities, so its boundary is curved instead of straight. On the deck's setup: perceptron test 0.6333, MLP (10, 20, max_iter=1000) test 0.9333, training 0.9583. The MLP closes most of the gap to the tree/forest/kNN (1.0) but not all: even a flexible network does not perfectly capture the iris clouds.

Sketch the wine exercise: what would you load, train and report?

Load load_wine() (178 samples, 13 features, 3 classes), split with train_test_split(..., test_size=0.2, random_state=42), train a DecisionTreeClassifier, then vary hyperparameters (max_depth, min_samples_leaf; or a random forest search as in Chapter 10). Reference values: best test accuracy 0.9444 at depth 3-4; most relevant features flavanoids, color_intensity, proline (together ≈0.98 of the importance).