This post marks the end of Module 2! You’ve learned linear models, loss functions, optimization, and both binary and multiclass classification. Now let’s bring it all together in a mini-project where you’ll:
These hands-on skills are exactly what’s needed to prepare for truly RL-style decision boundaries and the transition to neural networks!
Given a model that takes a point and predicts a class:
For neural networks and logistic regression, this can produce lines (linear), curves (nonlinear), or more complex boundaries (deep nets).
The accuracy is:
We visualize the decision boundary by “coloring in” the 2D space by the model’s prediction.
In this project, you’ll see the direct link between mathematics and code:
Ultimately, this hands-on project distills both the geometry and mechanics of classification in machine learning, which will serve you well in both RL state/action problems and deep net applications.
We’ll generate a “two moons” dataset—a classic nonlinear classification task.
import torch
import numpy as np
import matplotlib.pyplot as plt
def make_moons(n_samples=200, noise=0.1):
# Generate two interleaving half circles ("moons"), similar to sklearn.datasets.make_moons
n = n_samples // 2
theta = np.pi * np.random.rand(n)
x0 = np.stack([np.cos(theta), np.sin(theta)], axis=1)
x1 = np.stack([1 - np.cos(theta), 1 - np.sin(theta)], axis=1) + np.array([0.6, -0.4])
X = np.vstack([x0, x1])
X += noise * np.random.randn(*X.shape)
y = np.hstack([np.zeros(n), np.ones(n)])
return torch.tensor(X, dtype=torch.float32), torch.tensor(y, dtype=torch.long)
torch.manual_seed(42)
np.random.seed(42)
X, y = make_moons(200, noise=0.2)
plt.scatter(X[y==0,0], X[y==0,1], c='r', label='Class 0', alpha=0.6)
plt.scatter(X[y==1,0], X[y==1,1], c='b', label='Class 1', alpha=0.6)
plt.legend(); plt.xlabel("x1"); plt.ylabel("x2"); plt.title("Two Moons Data"); plt.show()
Let’s use a small neural network (can use logistic regression if desired).
import torch.nn as nn
model = nn.Sequential(
nn.Linear(2, 10),
nn.ReLU(),
nn.Linear(10, 2) # 2 outputs = logits for 2 classes
)
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
loss_fn = nn.CrossEntropyLoss()
# Training loop
losses = []
for epoch in range(200):
logits = model(X)
loss = loss_fn(logits, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
losses.append(loss.item())
if epoch % 40 == 0 or epoch == 199:
print(f"Epoch {epoch}: loss = {loss.item():.3f}")
plt.plot(losses)
plt.title("Loss During Training")
plt.xlabel("Epoch"); plt.ylabel("Cross-Entropy Loss"); plt.grid(True); plt.show()
We color the background according to the predicted class.
with torch.no_grad():
# Create a grid of points covering the data
xx, yy = np.meshgrid(np.linspace(X[:,0].min()-0.2, X[:,0].max()+0.2, 200),
np.linspace(X[:,1].min()-0.2, X[:,1].max()+0.2, 200))
grid_points = np.c_[xx.ravel(), yy.ravel()]
grid_tensor = torch.tensor(grid_points, dtype=torch.float32)
logits_grid = model(grid_tensor)
probas = torch.softmax(logits_grid, dim=1)
preds = probas.argmax(dim=1).cpu().numpy().reshape(xx.shape)
plt.contourf(xx, yy, preds, alpha=0.2, cmap="coolwarm", levels=2)
plt.scatter(X[y==0,0], X[y==0,1], c='r', label='Class 0', alpha=0.7)
plt.scatter(X[y==1,0], X[y==1,1], c='b', label='Class 1', alpha=0.7)
plt.title("Learned Decision Boundary")
plt.xlabel("x1"); plt.ylabel("x2"); plt.legend(); plt.show()
with torch.no_grad():
logits = model(X)
predictions = logits.argmax(dim=1)
accuracy = (predictions == y).float().mean().item()
misclassified = (predictions != y)
print(f"Accuracy: {accuracy*100:.2f}%")
plt.scatter(X[y==0,0], X[y==0,1], c='r', label='Class 0', alpha=0.7)
plt.scatter(X[y==1,0], X[y==1,1], c='b', label='Class 1', alpha=0.7)
plt.scatter(X[misclassified,0], X[misclassified,1],
facecolors='none', edgecolors='k', linewidths=2, s=90, label='Misclassified')
plt.xlabel("x1"); plt.ylabel("x2")
plt.legend(); plt.title("Classification Results (Misclassifications circled)")
plt.show()
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
# EXERCISE 1
def make_circles(n_samples=200, noise=0.05, factor=0.6):
n = n_samples // 2
theta = 2 * np.pi * np.random.rand(n)
outer = np.stack([np.cos(theta), np.sin(theta)], axis=1)
inner = np.stack([factor * np.cos(theta), factor * np.sin(theta)], axis=1)
X = np.vstack([outer, inner])
X += noise * np.random.randn(*X.shape)
y = np.hstack([np.zeros(n), np.ones(n)])
return torch.tensor(X, dtype=torch.float32), torch.tensor(y, dtype=torch.long)
torch.manual_seed(1); np.random.seed(1)
X, y = make_circles(200, noise=0.2)
plt.scatter(X[y==0,0], X[y==0,1], c='cyan', label='Class 0')
plt.scatter(X[y==1,0], X[y==1,1], c='magenta', label='Class 1')
plt.legend(); plt.xlabel("x1"); plt.ylabel("x2"); plt.title("Circle Data"); plt.show()
# EXERCISE 2
model = nn.Sequential(
nn.Linear(2, 12), nn.ReLU(),
nn.Linear(12, 2)
)
optimizer = torch.optim.Adam(model.parameters(), lr=0.07)
loss_fn = nn.CrossEntropyLoss()
losses = []
for epoch in range(250):
logits = model(X)
loss = loss_fn(logits, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
losses.append(loss.item())
plt.plot(losses)
plt.title("Training Loss"); plt.xlabel("Epoch"); plt.ylabel("Loss"); plt.grid(); plt.show()
# EXERCISE 3
with torch.no_grad():
xx, yy = np.meshgrid(np.linspace(X[:,0].min()-0.2, X[:,0].max()+0.2, 200),
np.linspace(X[:,1].min()-0.2, X[:,1].max()+0.2, 200))
grid_points = np.c_[xx.ravel(), yy.ravel()]
logits_grid = model(torch.tensor(grid_points, dtype=torch.float32))
preds_grid = logits_grid.argmax(dim=1).cpu().numpy().reshape(xx.shape)
plt.contourf(xx, yy, preds_grid, alpha=0.2, cmap="coolwarm", levels=2)
plt.scatter(X[y==0,0], X[y==0,1], c='cyan', label='Class 0', alpha=0.8)
plt.scatter(X[y==1,0], X[y==1,1], c='magenta', label='Class 1', alpha=0.8)
plt.xlabel("x1"); plt.ylabel("x2"); plt.title("Learned Boundary"); plt.legend(); plt.show()
# EXERCISE 4
with torch.no_grad():
logits = model(X)
predictions = logits.argmax(dim=1)
accuracy = (predictions == y).float().mean().item()
misclassified = (predictions != y)
print(f"Accuracy: {accuracy*100:.2f}%")
plt.scatter(X[y==0,0], X[y==0,1], c='cyan', label='Class 0', alpha=0.8)
plt.scatter(X[y==1,0], X[y==1,1], c='magenta', label='Class 1', alpha=0.8)
plt.scatter(X[misclassified,0], X[misclassified,1], facecolors='none', edgecolors='k', linewidths=2, s=90, label='Misclassified')
plt.xlabel("x1"); plt.ylabel("x2"); plt.title("Classification with Misclassifications"); plt.legend(); plt.show()
Congratulations, you’ve built and visualized your own machine learning classifier! You’ve learned to:
Up next: We step into the world of neural network architectures—moving from shallow to deep, and extending your classifier toolbox for RL and general ML alike.
Experiment with your own datasets and models—true understanding comes from hands-on hacking. See you in Module 3!