Implementation exercises for convolutional networks — from manually computing convolution and output shapes, to building and training a real image classifier in PyTorch.
🟢 Problem 1: Compute convolution output size by hand, then verify in code
Task: For a 28×28 input, a 5×5 kernel, stride 2, and padding 1, compute the output spatial dimension by hand using the formula, then verify with PyTorch.
\[ O = \left\lfloor \frac{28 - 5 + 2(1)}{2} \right\rfloor + 1 = \left\lfloor \frac{25}{2} \right\rfloor + 1 = 12 + 1 = 13 \]import torch
import torch.nn as nn
conv = nn.Conv2d(in_channels=1, out_channels=8, kernel_size=5, stride=2, padding=1)
x = torch.randn(1, 1, 28, 28)
output = conv(x)
print(output.shape) # should confirm: torch.Size([1, 8, 13, 13])
🟡 Problem 2: Implement a 2D convolution manually with NumPy (no PyTorch)
Task: Implement a single-channel, single-kernel, stride-1, no-padding convolution using nested loops — then verify it matches PyTorch's output on the same input and kernel.
def conv2d_manual(image, kernel):
ih, iw = image.shape
kh, kw = kernel.shape
oh, ow = ih - kh + 1, iw - kw + 1
output = np.zeros((oh, ow))
for i in range(oh):
for j in range(ow):
region = image[i:i+kh, j:j+kw]
output[i, j] = np.sum(region * kernel)
return output
image = np.random.randn(6, 6)
kernel = np.array([[1, 0, -1], [1, 0, -1], [1, 0, -1]]) # a vertical edge detector
result = conv2d_manual(image, kernel)
print(result.shape) # (4, 4)
# Verify against PyTorch
import torch.nn.functional as F
torch_result = F.conv2d(
torch.tensor(image).unsqueeze(0).unsqueeze(0),
torch.tensor(kernel).unsqueeze(0).unsqueeze(0)
).squeeze().numpy()
print(np.allclose(result, torch_result, atol=1e-6)) # should print True
Hint if stuck: The output size formula with no padding and stride 1 simplifies to \(O = I - K + 1\) — use this to double-check your loop bounds.
🟡 Problem 3: Build and train a small CNN on a toy image dataset
Task: Build a CNN with 2 convolutional layers, max pooling, and a final classification head for a small dataset (e.g. a subset of MNIST or CIFAR-10), and train it for a few epochs.
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, 3, padding=1)
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.fc = nn.Linear(32 * 7 * 7, num_classes)
def forward(self, x):
x = self.pool(torch.relu(self.conv1(x))) # 28x28 -> 14x14
x = self.pool(torch.relu(self.conv2(x))) # 14x14 -> 7x7
x = x.view(x.size(0), -1)
return self.fc(x)
model = SimpleCNN()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
loss_fn = nn.CrossEntropyLoss()
for epoch in range(3):
for x_batch, y_batch in train_loader:
optimizer.zero_grad()
loss = loss_fn(model(x_batch), y_batch)
loss.backward()
optimizer.step()
print(f"Epoch {epoch}: loss={loss.item():.4f}")
Hint if stuck: If you get a shape mismatch in the final Linear layer, print x.shape right before x.view(...) to see the actual flattened size, and adjust the Linear layer's input dimension to match.
🔴 Problem 4: Visualize what a trained CNN's first-layer filters actually learned
Task: After training the model from Problem 3, extract and visualize the first convolutional layer's learned kernels as small images.
import matplotlib.pyplot as plt
filters = model.conv1.weight.data.cpu().numpy() # shape: (16, 1, 3, 3)
fig, axes = plt.subplots(4, 4, figsize=(6, 6))
for i, ax in enumerate(axes.flat):
ax.imshow(filters[i, 0], cmap='gray')
ax.axis('off')
plt.suptitle("Learned first-layer convolutional filters")
plt.show()
# Many filters should resemble simple edge/gradient detectors, similar to
# classical hand-designed filters -- the network learned these on its own