๐ŸŽ“ A1Learner
๐Ÿ”

PyTorch What the Model Learned

A decision boundary is the line where a model's answer changes from one class to another. Asking the model about a grid of points draws that boundary as a map.

Build a grid of points

Two loops walk the x and y values. Each pair becomes one grid point.

grid_points = []
for y in [1.0, 0.0, -1.0]:
    for x in [-1.0, 0.0, 1.0]:
        grid_points.append([x, y])
grid = torch.tensor(grid_points)
print(grid.shape)
print(grid[0])

The model answers the grid

One forward pass answers every grid point at once. argmax turns the scores into class numbers.

with torch.no_grad():
    answers = model(grid).argmax(dim=1)

Print answers as a map

The flat answers are cut into rows of the grid's width. Each row prints as one line of characters.

answers = [0, 0, 1, 1, 0, 1]
width = 3
for row in range(2):
    line = "".join(str(answers[row * width + col])
                   for col in range(width))
    print(line)

Example

import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset

torch.manual_seed(0)

per_class = 60
centres = [torch.tensor([-3.0, -2.0]), torch.tensor([3.0, -2.0]),
           torch.tensor([0.0, 3.0])]
blobs = [torch.randn(per_class, 2) * 0.8 + centre for centre in centres]
points = torch.cat(blobs)
labels = torch.cat([torch.full((per_class,), i, dtype=torch.long)
                     for i in range(3)])

dataset = TensorDataset(points, labels)
loader = DataLoader(dataset, batch_size=32, shuffle=True)

torch.manual_seed(1)
model = nn.Sequential(
    nn.Linear(2, 16),
    nn.ReLU(),
    nn.Linear(16, 3),
)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)

for epoch in range(30):
    for batch_points, batch_labels in loader:
        scores = model(batch_points)
        loss = loss_fn(scores, batch_labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

model.eval()
width, height = 30, 15
grid_points = []
for row in range(height):
    y = 4.5 - row * (9.0 / (height - 1))
    for col in range(width):
        x = -5.5 + col * (11.0 / (width - 1))
        grid_points.append([x, y])
grid_tensor = torch.tensor(grid_points)

with torch.no_grad():
    answers = model(grid_tensor).argmax(dim=1).tolist()

print("What the model predicts at each grid point:")
for row in range(height):
    line = "".join(str(answers[row * width + col]) for col in range(width))
    print(line)

Key points

  • Use a grid of evenly spaced points to cover the whole data area
  • Use the trained model under no_grad to answer every grid point
  • Each map cell prints the class predicted at that spot
  • The boundary is the dividing line the model itself learned

In this example, the three-blob classifier trains exactly as in the earlier lesson. Then a grid of 30 by 15 points is built across the data area. The top row of the grid holds the highest y values. The trained model answers all 450 grid points in one pass under no_grad. Each answer prints as one character, row by row. Class 2 fills the top of the map. Class 0 holds the bottom left, and class 1 holds the bottom right. The edges between the regions are the decision boundaries.

Note

The map needs no plotting library, because characters carry the picture

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

What does a character in the boundary map stand for?