PyTorch What the CNN Learned
What the CNN learned is stored in its kernels. Each first-layer kernel is a 3 by 3 pattern of weights, printed as a shade grid in this lesson.
The first layer's weight tensor
conv1.weight holds all 8 kernels in one tensor. The middle 1 is the single input channel.
model = ShapesCNN()
print(model.conv1.weight.shape)One kernel on its own
Two indices pick one kernel out of the tensor. The first index picks the filter, and the second picks the channel.
kernel = model.conv1.weight.detach()[0, 0]
print(kernel.shape)From numbers to shades
Each weight maps to one character by its place between the layer's smallest and largest weights. Equal weights print as equal characters.
shades = ".:-=+*#@"
level = int((value - low) / (high - low) * 7.99)
char = shades[level]Example
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
def draw_shape(label, g):
rows = torch.arange(16).view(16, 1).float()
cols = torch.arange(16).view(1, 16).float()
if label == 0:
centre = 8 + int(torch.randint(-2, 3, (1,), generator=g))
mask = (rows >= centre - 1) & (rows <= centre + 1) & (cols >= 0)
elif label == 1:
cr = 7.5 + int(torch.randint(-1, 2, (1,), generator=g))
cc = 7.5 + int(torch.randint(-1, 2, (1,), generator=g))
dist = torch.sqrt((rows - cr) ** 2 + (cols - cc) ** 2)
mask = (dist >= 4.5) & (dist <= 6.5)
else:
top = int(torch.randint(2, 7, (1,), generator=g))
left = int(torch.randint(2, 7, (1,), generator=g))
mask = (rows >= top) & (rows <= top + 7) & (cols >= left) & (cols <= left + 7)
return mask.float()
def make_shapes(count, seed):
g = torch.Generator().manual_seed(seed)
labels = torch.arange(count) % 3
labels = labels[torch.randperm(count, generator=g)]
images = [draw_shape(int(label), g) for label in labels]
return torch.stack(images).unsqueeze(1), labels
class ShapesCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 8, 3, padding=1)
self.pool = nn.MaxPool2d(2)
self.conv2 = nn.Conv2d(8, 16, 3, padding=1)
self.gap = nn.AdaptiveAvgPool2d(1)
self.head = nn.Linear(16, 3)
def forward(self, x):
x = self.pool(torch.relu(self.conv1(x)))
x = torch.relu(self.conv2(x))
x = self.gap(x)
return self.head(torch.flatten(x, 1))
train_x, train_y = make_shapes(240, seed=42)
val_x, val_y = make_shapes(60, seed=43)
loader = DataLoader(TensorDataset(train_x, train_y), batch_size=32,
shuffle=True, generator=torch.Generator().manual_seed(0))
torch.manual_seed(1)
model = ShapesCNN()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=4, gamma=0.5)
loss_fn = nn.CrossEntropyLoss()
def accuracy():
model.eval()
with torch.no_grad():
predictions = model(val_x).argmax(dim=1)
return float((predictions == val_y).float().mean())
for epoch in range(8):
model.train()
for xb, yb in loader:
optimizer.zero_grad()
loss = loss_fn(model(xb), yb)
loss.backward()
optimizer.step()
scheduler.step()
print(f"Validation accuracy: {accuracy():.3f}")
print("First-layer kernels (dark cells weigh most):")
shades = ".:-=+*#@"
kernels = model.conv1.weight.detach()
low = float(kernels.min())
high = float(kernels.max())
for index in range(kernels.shape[0]):
print(f"Kernel {index}:")
for row in kernels[index, 0].tolist():
line = ""
for value in row:
level = int((value - low) / (high - low) * 7.99)
line += shades[level]
print(line)Key points
- Kernel: a small grid of weights that one conv filter slides over the image
- conv1.weight: 8 kernels of shape (3, 3), one per filter on the single input channel
- In the gallery, the darkest cells hold the largest weights in the layer
- Kernel 5 weighs its top and bottom rows and mostly skips the middle row
- Kernel 6 weighs its middle column most and carries the layer's largest weights
In this example, the program trains the same CNN as the Training a CNN lesson. The data, seeds, and schedule match that lesson too. The validation accuracy prints first and reaches 1.000. The first layer's weight tensor then gives up its 8 kernels. Each kernel prints as three rows of shade characters. The scale is shared by the whole layer, so shades compare across kernels. Kernel 1 is dark in most cells, so its weights are high almost everywhere. Kernel 5 is dark on its top and bottom rows and pale in the middle. Kernel 6 carries several @ cells, the largest weights in the layer, down its middle column.
Note
The kernels come from training, not from a hand design, and a different seed would grow different kernels.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Python Playground
Exercise
What does one first-layer kernel hold?