Module F · Lecture 21

Self-Organizing Maps & K-Means Clustering

Neurons that compete instead of cooperate: from a single winner-take-all update rule to Kohonen's topology-preserving map and the ubiquitous k-means algorithm.

⏱ ~80 min 🧩 Builds on: Lectures 3, 7 🎯 CO5
🧭 Why we're learning this now

Stop and notice something about every single model built so far, from the perceptron through Transformers: every one of them was trained against a known correct answer — a label. That covers a lot of ground, but most data in the real world has no labels at all. This lecture is the first of two on a different question entirely: what can a network learn when nobody tells it the right answer?

  • Define competitive learning and derive the winner-take-all update rule from first principles.
  • Explain how a Self-Organizing Map extends competitive learning with a neighborhood function to preserve topology.
  • Compute Best Matching Units, neighborhood function values, and weight updates for a small SOM by hand.
  • Describe how a SOM performs dimensionality reduction, and list real-world applications of SOMs.
  • Implement k-means clustering from its assign/update principles and connect the assignment step to competitive learning's nearest-neighbor rule.
  • Compare k-means and SOM as, respectively, topologically-unconstrained and topologically-constrained prototype learners.

1. Why Unsupervised Learning?

Every model built so far in this course — from the perceptron in Lecture 3 to the Transformer in Lecture 20 — was trained with labels: an input \(x\) paired with a target \(y\), and a loss function that measured how far the network's prediction strayed from that target. Most data in the world doesn't come with labels attached. Raw sensor logs, unannotated images, customer transaction histories, gene expression matrices — someone would have to hand-label all of it before supervised learning could touch it.

Unsupervised learning asks a different question: given only \(x\), with no \(y\) at all, can a model discover useful structure on its own — groups of similar points, a compact representation, or a map of how the data is organized? This lecture builds up unsupervised learning from a single, elegant mechanism — neurons competing for the right to respond to an input — through two of its most important instantiations: the Self-Organizing Map and k-means clustering.

2. Competitive Learning

Definition — Competitive Learning

Competitive learning is an unsupervised learning rule in which neurons compete to respond to an input; only the winning neuron updates its weights. This is called winner-takes-all.

Let the input be \(x\in\mathbb{R}^d\) and let each neuron \(i\) hold a weight vector \(w_i\in\mathbb{R}^d\). The winner is the neuron whose weight vector is closest to the input by Euclidean distance:

$$i^{*}=\arg\min_i \lVert x-w_i\rVert$$

Only the winning neuron's weights move — every other neuron is left untouched. With learning rate \(\eta(t)\):

$$w_{i^{*}}(t+1)=w_{i^{*}}(t)+\eta(t)\big(x-w_{i^{*}}(t)\big) \qquad\text{(all other } w_i \text{ unchanged)}$$

The update simply nudges the winner's weight vector a fraction \(\eta(t)\) of the way toward the input. Over many inputs, each neuron drifts toward the center of the region of input space it keeps winning — it becomes a prototype for that region.

Prototype learning

Each neuron's weight vector converges to a prototype — a representative example of the cluster of inputs it wins. A closely related supervised model, Learning Vector Quantization (LVQ), uses the same nearest-prototype mechanism but nudges the winning prototype toward the input when the label matches and away from it when the label doesn't.

Feature specialization

As training repeats over many inputs, different neurons specialize to different regions of the input space — e.g. in image data, distinct neurons might come to represent edge-like patterns while others come to represent corner-like patterns, purely from repeated winner-take-all competition, with no labels telling them "this is an edge."

"Each cluster has a prototype, and each prototype defines a cluster."

Competitive learning is the mechanism underneath: clustering (it behaves like an online, incremental version of k-means — the explicit bridge we'll make precise in Part C), prototype learning, feature specialization, dimensionality reduction & visualization — the foundation the Self-Organizing Map builds on — and general pattern discovery.

3. Worked Example: Winner-Take-All

Input dimension 2, three neurons, learning rate \(\eta=0.5\). Initial weights: \(N_1=(0.2,0.6)\), \(N_2=(0.8,0.4)\), \(N_3=(0.5,0.9)\). Input \(x=(0.1,0.5)\).

Initial positions of the three neurons (prototypes) and the input point in 2D weight space. The winner is whichever neuron sits geometrically closest to the input.
Step 1 — Euclidean distance from x to every neuron

Each distance is: square the difference in each coordinate, add the squares, take the square root. Written out in full for all three neurons:

$$d_1=\sqrt{(0.1-0.2)^2+(0.5-0.6)^2}=\sqrt{(-0.1)^2+(-0.1)^2}=\sqrt{0.01+0.01}=\sqrt{0.02}\approx\mathbf{0.141}$$

$$d_2=\sqrt{(0.1-0.8)^2+(0.5-0.4)^2}=\sqrt{(-0.7)^2+(0.1)^2}=\sqrt{0.49+0.01}=\sqrt{0.50}\approx\mathbf{0.707}$$

$$d_3=\sqrt{(0.1-0.5)^2+(0.5-0.9)^2}=\sqrt{(-0.4)^2+(-0.4)^2}=\sqrt{0.16+0.16}=\sqrt{0.32}\approx\mathbf{0.566}$$

Step 2 — pick the winner

Smallest distance wins: \(d_1=0.141\) is smaller than both \(d_2=0.707\) and \(d_3=0.566\), so N1 is the winner. N2 and N3 lose the competition — under winner-takes-all, they will not be updated at all this round.

Step 3 — update only the winner

Apply the update rule \(w_{1}(t{+}1)=w_1(t)+\eta(x-w_1(t))\) coordinate by coordinate:

$$w_1^{new}=(0.2,0.6)+0.5\big[(0.1,0.5)-(0.2,0.6)\big]=(0.2,0.6)+0.5\times(-0.1,-0.1)=(0.2-0.05,\ 0.6-0.05)=\mathbf{(0.15,\ 0.55)}$$

N2 stays exactly \((0.8,0.4)\) and N3 stays exactly \((0.5,0.9)\) — unchanged, because they didn't win.

You can replay the same three steps interactively below — useful for testing yourself before moving on:

4. Self-Organizing Maps (SOM)

Competitive learning finds prototypes, but the prototypes it finds have no relationship to one another — neuron 7's prototype could be nothing like neuron 8's. The Self-Organizing Map (Kohonen, 1982) fixes this by arranging the output neurons on a low-dimensional grid — usually 2D — and forcing neighboring grid neurons to learn similar prototypes. It projects high-dimensional input onto that grid while preserving topology: inputs that are close in the original space end up mapped to neurons that are close on the grid. This topology preservation is exactly what distinguishes a SOM from plain competitive learning.

The output layer is a grid of neurons — e.g. \(m\times n\) — where each neuron \(i\) has both a weight vector \(w_i\in\mathbb{R}^d\) (as before) and a fixed grid position \(r_i\) that never changes during training. Training combines two ideas: competition (find the winner, exactly as in Part 2) with neighborhood cooperation (also update the winner's neighbors, scaled by how far they sit from the winner on the grid).

Best Matching Unit (BMU)

Identical rule to competitive learning's winner: \(i^{*}=\arg\min_i \lVert x-w_i\rVert\) — the neuron whose weight vector is closest to the input.

What's new is the Gaussian neighborhood function, which measures how "close" neuron \(i\) is to the BMU on the grid (not in weight space):

$$h_{i,i^{*}}(t)=\exp\!\left(-\frac{\lVert r_i-r_{i^{*}}\rVert^2}{2\sigma^2(t)}\right)$$

Every neuron in the map is updated — not just the winner — scaled by this neighborhood value:

$$w_i(t+1)=w_i(t)+\eta(t)\cdot h_{i,i^{*}}(t)\cdot\big(x-w_i(t)\big)$$

Two limits make this rule click. When \(i=i^{*}\), the grid distance is 0, so \(h_{i,i^{*}}=1\) — the winner gets the full update, exactly as in ordinary competitive learning. As the grid distance from the winner grows, \(h_{i,i^{*}}\to 0\) — distant neurons barely move at all. Neurons that sit close to the winner on the grid get a partial nudge toward the same input. Repeat this over many inputs, and neighboring grid neurons are continually pulled toward similar regions of input space — which is precisely what makes the map preserve topology.

5. Worked Example: BMU & Neighborhood Update

A 2×2 grid with fixed positions \(r_{11}=(0,0),\ r_{12}=(0,1),\ r_{21}=(1,0),\ r_{22}=(1,1)\). Input \(x=(0.2,0.8)\), \(\eta=0.5\), \(\sigma=1.0\).

🔢 Initial weights

N11 = (0.1, 0.7)   N12 = (0.8, 0.2)   N21 = (0.3, 0.9)   N22 = (0.7, 0.5)

Step 1 — distance from x to every neuron (in weight space)

$$d_{11}=\sqrt{(0.2-0.1)^2+(0.8-0.7)^2}=\sqrt{0.1^2+0.1^2}=\sqrt{0.01+0.01}=\sqrt{0.02}\approx\mathbf{0.141}$$

$$d_{12}=\sqrt{(0.2-0.8)^2+(0.8-0.2)^2}=\sqrt{(-0.6)^2+0.6^2}=\sqrt{0.36+0.36}=\sqrt{0.72}\approx\mathbf{0.849}$$

$$d_{21}=\sqrt{(0.2-0.3)^2+(0.8-0.9)^2}=\sqrt{(-0.1)^2+(-0.1)^2}=\sqrt{0.01+0.01}=\sqrt{0.02}\approx\mathbf{0.141}$$

$$d_{22}=\sqrt{(0.2-0.7)^2+(0.8-0.5)^2}=\sqrt{(-0.5)^2+0.3^2}=\sqrt{0.25+0.09}=\sqrt{0.34}\approx\mathbf{0.583}$$

Step 2 — identify the Best Matching Unit

\(d_{11}\) and \(d_{21}\) are both exactly \(\sqrt{0.02}\approx0.141\) — a genuine tie for closest neuron. We break the tie in favor of N11 (matching the source example), so BMU = N11. A real implementation needs an explicit tie-breaking rule (lowest index, or random choice).

Step 3 — Gaussian neighborhood function for every neuron

The neighborhood function uses grid distance \(\lVert r_i-r_{i^{*}}\rVert\) — not the weight-space distance from Step 1 — measured against the BMU's fixed grid position \(r_{11}=(0,0)\). With \(\sigma=1.0\), the denominator \(2\sigma^2=2\) throughout:

grid dist(N11, N11) \(=\sqrt{(0-0)^2+(0-0)^2}=0\)   →   $$h_{11}=\exp\!\left(-\frac{0^2}{2}\right)=\exp(0)=\mathbf{1.000}$$

grid dist(N12, N11) \(=\sqrt{(0-0)^2+(1-0)^2}=1\)   →   $$h_{12}=\exp\!\left(-\frac{1^2}{2}\right)=\exp(-0.5)\approx\mathbf{0.6065}$$

grid dist(N21, N11) \(=\sqrt{(1-0)^2+(0-0)^2}=1\)   →   $$h_{21}=\exp\!\left(-\frac{1^2}{2}\right)=\exp(-0.5)\approx\mathbf{0.6065}$$

grid dist(N22, N11) \(=\sqrt{(1-0)^2+(1-0)^2}=\sqrt2\)   →   $$h_{22}=\exp\!\left(-\frac{(\sqrt2)^2}{2}\right)=\exp\!\left(-\frac{2}{2}\right)=\exp(-1)\approx\mathbf{0.3679}$$

Step 4 — update the winner (full update, h11 = 1.000)

$$w_{11}^{new}=(0.1,0.7)+0.5(1.0)\big[(0.2,0.8)-(0.1,0.7)\big]=(0.1,0.7)+0.5\times(0.1,0.1)=\mathbf{(0.15,\ 0.75)}$$

Step 5 — update every neighbor (partial updates, scaled by h)

$$w_{12}^{new}=(0.8,0.2)+0.5(0.6065)\big[(0.2,0.8)-(0.8,0.2)\big]=(0.8,0.2)+0.30325\times(-0.6,0.6)\approx\mathbf{(0.618,\ 0.382)}$$

$$w_{21}^{new}=(0.3,0.9)+0.5(0.6065)\big[(0.2,0.8)-(0.3,0.9)\big]=(0.3,0.9)+0.30325\times(-0.1,-0.1)\approx\mathbf{(0.270,\ 0.870)}$$

$$w_{22}^{new}=(0.7,0.5)+0.5(0.3679)\big[(0.2,0.8)-(0.7,0.5)\big]=(0.7,0.5)+0.18395\times(-0.5,0.3)\approx\mathbf{(0.608,\ 0.555)}$$

Every neighbor moved toward x too, but by less than the winner — exactly the cooperation that pulls the whole neighborhood toward similar inputs over time.

You can replay the same five steps interactively below — useful for testing yourself before moving on:

The neighborhood values from this step form a small, clean picture of how influence fades with grid distance:

Neighborhood function \(h_{i,i^{*}}\) for each of the 4 grid neurons, with N11 as BMU. Full strength (1.000) at the winner, 0.6065 one grid-step away, 0.3679 at the diagonal (grid-distance \(\sqrt2\)).

6. SOM as Dimensionality Reduction

Because every input is ultimately reduced to "which grid cell won," a SOM doubles as a dimensionality-reduction technique: a high-dimensional input is summarized by a low-dimensional (often 2D) grid coordinate that a human can inspect on a map. Consider a SOM with input dimension 3 and a 2×2 grid:

🔢 Neuron weights

N11 at (0,0): [0.2, 0.7, 0.4]  |  N12 at (0,1): [0.9, 0.3, 0.6]
N21 at (1,0): [0.3, 0.8, 0.9]  |  N22 at (1,1): [0.5, 0.5, 0.5]

For input \(x=[0.8,0.2,0.5]\), computing all four distances shows N12 is closest, so the BMU is (0,1) — the SOM's 2D "output" for this 3D input is simply the coordinate (0, 1). Running the same computation over several inputs produces a mapping table:

Input vectorBMU grid coordinate
[0.8, 0.2, 0.5](0, 1)
[0.9, 0.1, 0.6](0, 1)
[0.3, 0.8, 0.9](1, 0)
[0.2, 0.7, 0.4](0, 0)
[0.5, 0.5, 0.5](1, 1)
✅ Topology preservation, made concrete

The first two 3D input vectors, [0.8, 0.2, 0.5] and [0.9, 0.1, 0.6], are numerically close to each other — and they map to the exact same grid cell (0, 1). This is topology preservation in action: similar high-dimensional inputs land on the same or neighboring low-dimensional grid cells. It's exactly why SOMs are used to visualize high-dimensional data — customer records, gene expression profiles, sensor readings — on a 2D map a human can actually read.

Applications of SOMs: clustering & prototype discovery (customer segmentation, text topic discovery, bioinformatics), dimensionality reduction (as preprocessing for other ML models), anomaly detection (inputs that land far from every learned prototype), robotics & control (building sensor and motor maps), and industrial/IoT settings (fault detection, equipment health monitoring).

T. Kohonen, "The Self-Organizing Map," Proc. IEEE, 1990.

7. K-Means Clustering

Note on sourcing

K-means is not covered in the local course slides. What follows is standard, well-established textbook material (Goodfellow/Bengio/Courville-style treatment) — kept intentionally brief relative to the instructor's own richly-worked SOM material above.

K-means is the simplest widely-used clustering algorithm, and it shares its core mechanism with competitive learning:

  1. Choose the number of clusters \(k\) and initialize \(k\) centroids (e.g. randomly, or with the k-means++ heuristic).
  2. Assign every data point to its nearest centroid by Euclidean distance — exactly the same winner-takes-all, nearest-neighbor idea as the competitive-learning winner rule in Part 2.
  3. Update each centroid to the mean of all points currently assigned to it.
  4. Repeat steps 2–3 until centroids stop moving (convergence) or a maximum iteration count is reached.

K-means minimizes the within-cluster sum of squares:

$$J=\sum_{i=1}^{k}\sum_{x\in C_i}\lVert x-\mu_i\rVert^2$$
The key difference from competitive learning

Competitive learning's update is incremental: one input nudges the winning prototype by a fraction \(\eta\) of the distance to it. K-means's update is a batch recomputation: the centroid jumps directly to the exact mean of its entire assigned cluster, all at once, after every point has been (re-)assigned.

8. Worked Example: K-Means by Hand

Since the local slides don't include a k-means example, here is a small, self-contained one with \(k=2\) and six 2D points, run for two full iterations.

🔢 Data & initial centroids

Points: P1(1,1), P2(1.5,2), P3(3,1), P4(5,7), P5(3.5,5), P6(4.5,5).
Initial centroids (chosen as two of the data points): \(\mu_1=(1,1)\), \(\mu_2=(5,7)\).

The six data points and the two initial centroids, before any assignment.
Iteration 1, Step 1 — assign every point to its nearest centroid

Same Euclidean-distance computation as the competitive-learning example above: square each coordinate difference, add, take the square root. Worked in full for the first two points:

$$d(P_1,\mu_1)=\sqrt{(1-1)^2+(1-1)^2}=\sqrt{0+0}=\mathbf{0.000}\qquad d(P_1,\mu_2)=\sqrt{(1-5)^2+(1-7)^2}=\sqrt{16+36}=\sqrt{52}\approx\mathbf{7.211}$$

$$d(P_2,\mu_1)=\sqrt{(1.5-1)^2+(2-1)^2}=\sqrt{0.25+1}=\sqrt{1.25}\approx\mathbf{1.118}\qquad d(P_2,\mu_2)=\sqrt{(1.5-5)^2+(2-7)^2}=\sqrt{12.25+25}=\sqrt{37.25}\approx\mathbf{6.103}$$

Since \(d(P_1,\mu_1), P1 is assigned to cluster 1; likewise P2. The same squaring/adding/square-rooting pattern applied to every remaining point gives:

Pointd(·, μ1)d(·, μ2)Assigned cluster
P1 (1, 1)0.0007.2111
P2 (1.5, 2)1.1186.1031
P3 (3, 1)2.0006.3251
P4 (5, 7)7.2110.0002
P5 (3.5, 5)4.7172.5002
P6 (4.5, 5)5.3152.0622

Cluster 1 = {P1, P2, P3}, cluster 2 = {P4, P5, P6}.

Iteration 1, Step 2 — recompute each centroid as the mean of its cluster

Average the \(x\)-coordinates and the \(y\)-coordinates of each cluster's members separately:

$$\mu_1^{new}=\text{mean}\big[(1,1),(1.5,2),(3,1)\big]=\left(\frac{1+1.5+3}{3},\ \frac{1+2+1}{3}\right)=\left(\frac{5.5}{3},\ \frac{4}{3}\right)\approx\mathbf{(1.833,\ 1.333)}$$

$$\mu_2^{new}=\text{mean}\big[(5,7),(3.5,5),(4.5,5)\big]=\left(\frac{5+3.5+4.5}{3},\ \frac{7+5+5}{3}\right)=\left(\frac{13}{3},\ \frac{17}{3}\right)\approx\mathbf{(4.333,\ 5.667)}$$

Iteration 2 — re-assign with the updated centroids

Repeat the exact same distance computation against the new centroids \(\mu_1=(1.833,1.333)\), \(\mu_2=(4.333,5.667)\). Worked in full for P3, since it's the closest call:

$$d(P_3,\mu_1)=\sqrt{(3-1.833)^2+(1-1.333)^2}=\sqrt{1.167^2+(-0.333)^2}=\sqrt{1.362+0.111}=\sqrt{1.473}\approx\mathbf{1.214}$$

$$d(P_3,\mu_2)=\sqrt{(3-4.333)^2+(1-5.667)^2}=\sqrt{(-1.333)^2+(-4.667)^2}=\sqrt{1.777+21.780}=\sqrt{23.557}\approx\mathbf{4.853}$$

P3 is still much closer to \(\mu_1\), so it stays in cluster 1. The same computation for every point gives:

Pointd(·, μ1)d(·, μ2)Assigned cluster
P1 (1, 1)0.8985.7351
P2 (1.5, 2)0.7454.6341
P3 (3, 1)1.2144.8531
P4 (5, 7)6.4911.4912
P5 (3.5, 5)4.0281.0672
P6 (4.5, 5)4.5340.6872

Every point lands in the same cluster as iteration 1. Since assignments didn't change, recomputing the centroids again would just reproduce the same means — the algorithm has converged: \(\mu_1\approx(1.833,1.333)\), \(\mu_2\approx(4.333,5.667)\).

Final objective value J

Squaring and summing every point's distance to its own (final) centroid, \(J=\sum_i\lVert x_i-\mu_{c(i)}\rVert^2\):

$$J = 0.898^2+0.745^2+1.214^2+1.491^2+1.067^2+0.687^2 \approx 0.806+0.556+1.473+2.222+1.139+0.472 \approx \mathbf{6.667}$$

You can replay the same iterations interactively below — useful for testing yourself before moving on:

9. K-Means vs. SOM

Two prototype learners, one key difference

Shared ground: both are unsupervised, both represent data with a small set of prototypes, and both assign an input to its prototype by nearest-neighbor (winner-takes-all) matching. Where they diverge: in k-means, clusters have no spatial or topological structure among them — each centroid updates independently of every other, so cluster 3 could be adjacent to cluster 7 in the data or a world apart, with no way to tell from the algorithm itself. A SOM's grid topology means nearby map units are forced to represent similar data through the neighborhood function. In this sense, a SOM can be viewed as a "topologically-constrained k-means" — k-means with an added neighborhood-cooperation term that organizes the prototypes into a map.

Summary

Key takeaways
  • Competitive learning: nearest-prototype neuron wins, only the winner updates — the seed idea behind every algorithm in this lecture.
  • SOM = competitive learning + a Gaussian neighborhood function over a fixed grid, so nearby map units learn similar prototypes and topology is preserved.
  • A SOM's Best Matching Unit coordinate is itself a dimensionality-reduced summary of a high-dimensional input — the basis for using SOMs in visualization.
  • K-means shares the nearest-neighbor assignment step with competitive learning but replaces the incremental update with an exact batch mean, and has no notion of neighborhood between clusters.

10. Code: Competitive Learning & K-Means From Scratch

The NumPy code below reproduces the Section 3 winner-take-all update exactly, then implements k-means from scratch on a small synthetic 2D dataset, printing centroids after every iteration.

lecture-21-som-kmeans.py
import numpy as np

# ---------------------------------------------------------------
# Part A: Competitive learning — reproduces the worked example
# ---------------------------------------------------------------
neurons = np.array([[0.2, 0.6], [0.8, 0.4], [0.5, 0.9]])  # N1, N2, N3
x = np.array([0.1, 0.5])
eta = 0.5

dists = np.linalg.norm(neurons - x, axis=1)
winner = np.argmin(dists)
print("Distances:", np.round(dists, 3), " Winner: N", winner + 1)

neurons[winner] = neurons[winner] + eta * (x - neurons[winner])
print("Updated winner weights:", np.round(neurons[winner], 3))
print("Other neurons unchanged:", np.round(neurons, 3))

# ---------------------------------------------------------------
# Part C: K-means from scratch — small synthetic dataset, k=2
# ---------------------------------------------------------------
def kmeans(X, centroids, n_iter=3):
    centroids = centroids.copy()
    for it in range(n_iter):
        dists = np.linalg.norm(X[:, None, :] - centroids[None, :, :], axis=2)
        assign = np.argmin(dists, axis=1)
        new_centroids = np.array([
            X[assign == k].mean(axis=0) if np.any(assign == k) else centroids[k]
            for k in range(len(centroids))
        ])
        print(f"Iter {it+1}: assignments={assign.tolist()}  "
              f"centroids={np.round(new_centroids, 3).tolist()}")
        if np.allclose(new_centroids, centroids):
            print("Converged.")
            centroids = new_centroids
            break
        centroids = new_centroids
    return centroids, assign

X = np.array([[1, 1], [1.5, 2], [3, 1], [5, 7], [3.5, 5], [4.5, 5]])
init_centroids = np.array([[1, 1], [5, 7]])
final_centroids, final_assign = kmeans(X, init_centroids, n_iter=3)
print("Final centroids:", np.round(final_centroids, 3))

J = sum(np.linalg.norm(X[i] - final_centroids[final_assign[i]]) ** 2 for i in range(len(X)))
print("Within-cluster sum of squares J =", round(J, 3))

⬇ Download lecture-21-som-kmeans.py   More resources for this lecture →