import numpy as np
import matplotlib.pyplot as plt
plt.style.use("../figure_style.mplstyle")
rng = np.random.default_rng(11)
n_per_class, noise = 150, 0.15
X, y = [], []
for c in [0, 1]:
r = np.linspace(0.15, 1.0, n_per_class)
t = np.linspace(c * np.pi, c * np.pi + 3.0 * np.pi, n_per_class) + rng.normal(0, noise, n_per_class)
X.append(np.column_stack([r * np.sin(t), r * np.cos(t)]))
y.append(np.full(n_per_class, c))
X, y = np.vstack(X), np.concatenate(y)Demo: Learned Decision Regions on a Spiral
Two interleaved spirals cannot be separated by a straight line. We will use the example to see what a hidden ReLU layer adds, how its decision regions develop, and why width matters.
Question 1: Why Is This Data Difficult for a Linear Classifier?
Inspect the two classes before fitting. Any single line that separates one part of a spiral must cut through another part.
The two classes wind around each other. Predict where a straight boundary would fail.
fig, ax = plt.subplots(figsize=(7, 4.5))
ax.scatter(X[y == 0, 0], X[y == 0, 1], c=y[y == 0], cmap="RdBu", vmin=0, vmax=1, s=14, label="Class 0")
ax.scatter(X[y == 1, 0], X[y == 1, 1], c=y[y == 1], cmap="RdBu", vmin=0, vmax=1, s=14, marker="^", label="Class 1")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.legend(frameon=False)
plt.show()
Answer. The classes alternate as they wind around the origin, so no half-plane contains one class without also containing points from the other.
Question 2: How Far Does a Linear Boundary Get?
We first train Topic 10’s softmax classifier. Its scores are linear functions of the two coordinates, so its class boundary must be one line.
def softmax(Z):
Z = Z - Z.max(axis=1, keepdims=True)
E = np.exp(Z)
return E / E.sum(axis=1, keepdims=True)
Y = np.zeros((len(y), 2)) # one-hot labels
Y[np.arange(len(y)), y] = 1
rng_linear = np.random.default_rng(1)
W = rng_linear.normal(0, 0.1, size=(2, 2))
b = np.zeros(2)
for epoch in range(4000):
P = softmax(X @ W + b)
dZ = (P - Y) / len(X)
W -= 1.0 * (X.T @ dZ)
b -= 1.0 * dZ.sum(0)
linear_accuracy = np.mean(softmax(X @ W + b).argmax(axis=1) == y)
print(f"linear model, training accuracy: {linear_accuracy:.1%}")linear model, training accuracy: 66.3%
Answer. The linear model performs only modestly better than chance because its boundary cannot follow the repeated turns.
Question 4: How Does the Boundary Develop During Training?
The backgrounds below show the predicted probability of class \(1\) at four epochs. Compare the initially arbitrary regions with the final shape.
xx, yy = np.meshgrid(np.linspace(-1.3, 1.3, 200), np.linspace(-1.3, 1.3, 200))
grid = np.column_stack([xx.ravel(), yy.ravel()])
fig, axes = plt.subplots(1, 4, figsize=(9, 2.6))
for ax, epoch in zip(axes, snapshots):
probs = predict(snapshots[epoch], grid).reshape(xx.shape)
shown = ax.imshow(probs, extent=[-1.3, 1.3, -1.3, 1.3], origin="lower", cmap="RdBu", vmin=0, vmax=1)
ax.scatter(X[y == 0, 0], X[y == 0, 1], c=y[y == 0], cmap="RdBu", vmin=0, vmax=1, s=5)
ax.scatter(X[y == 1, 0], X[y == 1, 1], c=y[y == 1], cmap="RdBu", vmin=0, vmax=1, s=5, marker="^")
ax.set_xlabel(f"Epoch {epoch}")
ax.set_xticks([])
ax.set_yticks([])
fig.colorbar(shown, ax=axes, fraction=0.025, pad=0.02, label="Predicted probability of class 1")
plt.show()
Answer. Training rearranges the piecewise-linear regions until they trace the two arms of the spiral.
