E2E: Convolutional Neural Network - End-to-End Implementations
Implement E2E: Convolutional Neural Network in Python with NumPy. hard exercise with examples, hints, and instant feedback. No setup or signup required.
End-to-End CNN
What you'll build
A LeNet-style image classifier in NumPy: 2-D convolution, max pooling and flatten as standalone functions, then a CNN class that chains two conv blocks and two fully connected layers into logits.
Architecture (input (batch, C, H, W), e.g. MNIST (batch, 1, 28, 28))
| Layer | Operation | Output shape (28×28 input) |
|---|---|---|
| conv1 | conv2d(x, conv1_w (32, C, 3, 3), conv1_b) + ReLU | (batch, 32, 26, 26) |
| pool1 | maxpool2d(·, 2) | (batch, 32, 13, 13) |
| conv2 | conv2d(·, conv2_w (64, 32, 3, 3), conv2_b) + ReLU | (batch, 64, 11, 11) |
| pool2 | maxpool2d(·, 2) | (batch, 64, 5, 5) |
| flatten | flatten(·) | (batch, 1600) |
| fc1 | · @ fc1_w + fc1_b + ReLU | (batch, 128) |
| fc2 | · @ fc2_w + fc2_b | (batch, num_classes) logits (no softmax) |
General sizes: a 3×3 valid conv maps H → H − 2; a 2×2 pool maps H → H // 2 (an odd leftover row/column is dropped). So flat_size = 64 · h2 · w2 with h1 = (H − 2) // 2, h2 = (h1 − 2) // 2 (same for W).
Function contract
| Function | Behaviour | Returns |
|---|---|---|
conv2d(x, kernel, bias) | stride 1, no padding, cross-correlation (no kernel flip, like every DL framework): out[b, o, i, j] = Σ_{c,u,v} x[b, c, i+u, j+v] · kernel[o, c, u, v] + bias[o] | (batch, out_ch, H − kH + 1, W − kW + 1) |
maxpool2d(x, pool_size=2) | non-overlapping windows (stride = pool_size), max per window and channel | (batch, ch, H // p, W // p) |
flatten(x) | keep the batch axis, merge the rest | (batch, −1) |
CNN(input_shape, num_classes) | creates conv1_w/b, conv2_w/b (given, after np.random.seed(42)), then fc1_w (flat_size, 128), fc1_b, fc2_w (128, num_classes), fc2_b (weights N(0, 0.1²), zero biases) | |
CNN.forward(x) | the table above | logits (batch, num_classes) |
test_cnn() | a smoke test: build CNN((1, 28, 28), 10), run 4 random images | True if the logits have shape (4, 10) |
Constraints
NumPy only. Loops over output positions are acceptable for clarity, but try to vectorise conv2d (np.lib.stride_tricks.sliding_window_view + np.einsum): the naive 4-level loop is very slow in the browser.
Follow-up
- Parameter count of conv1 and fc1 for MNIST? (32·1·9 + 32 = 320 and 1600·128 + 128 = 204,928: the dense layer dominates.)
- How does im2col turn convolution into one matrix multiply?
Examples
Input: conv2d(np.arange(16.0).reshape(1, 1, 4, 4), np.array([[[[1.0, 0.0], [2.0, -1.0]]]]), np.array([0.5])) Output: [[[[3.5, 5.5, 7.5], [11.5, 13.5, 15.5], [19.5, 21.5, 23.5]]]]
With x[i, j] = 4i + j, each output is x[i, j] + 2·x[i+1, j] − x[i+1, j+1] + 0.5 = 8i + 2j + 3.5
Input: maxpool2d(np.array([[[[1, 3, 2, 0], [4, 2, 1, 5], [0, 1, 7, 6], [2, 8, 3, 1]]]])) Output: [[[[4.0, 5.0], [8.0, 7.0]]]]
The max of each non-overlapping 2×2 block
Input: CNN((1, 28, 28), 10).forward(np.random.randn(2, 1, 28, 28)).shape Output: (2, 10)
28 → 26 → 13 → 11 → 5, so fc1 sees 64·5·5 = 1600 features
Practice in Python
import numpy as np
def conv2d(x: np.ndarray, kernel: np.ndarray, bias: np.ndarray) -> np.ndarray:
"""
2D convolution (cross-correlation, no padding, stride=1).
Args:
x: Input (batch, in_channels, height, width)
kernel: Weights (out_channels, in_channels, kH, kW)
bias: Bias (out_channels,)
Returns:
output: (batch, out_channels, out_height, out_width)
"""
# Your code here
pass
def maxpool2d(x: np.ndarray, pool_size: int = 2) -> np.ndarray:
"""
2D max pooling with non-overlapping windows (stride = pool_size).
Args:
x: Input (batch, channels, height, width)
pool_size: Size of pooling window
Returns:
output: (batch, channels, height // pool_size, width // pool_size)
"""
# Your code here
pass
def flatten(x: np.ndarray) -> np.ndarray:
"""Flatten all dimensions except batch."""
# Your code here
pass
class CNN:
def __init__(self, input_shape: tuple, num_classes: int):
"""
Initialize CNN.
Args:
input_shape: (channels, height, width)
num_classes: Number of output classes
"""
np.random.seed(42)
in_channels, H, W = input_shape
# Conv layer 1: in_channels -> 32
self.conv1_w = np.random.randn(32, in_channels, 3, 3) * 0.1
self.conv1_b = np.zeros(32)
# Conv layer 2: 32 -> 64
self.conv2_w = np.random.randn(64, 32, 3, 3) * 0.1
self.conv2_b = np.zeros(64)
# Flattened size after conv(3x3) -> pool(2) -> conv(3x3) -> pool(2)
# Your code here
# Fully connected layers: fc1_w (flat_size, 128), fc1_b (128,),
# fc2_w (128, num_classes), fc2_b (num_classes,); weights * 0.1, biases zero
# Your code here
pass
def forward(self, x: np.ndarray) -> np.ndarray:
"""
Forward pass through CNN.
Args:
x: Input images (batch, channels, height, width)
Returns:
logits: (batch, num_classes), no softmax
"""
# Your code here
pass
def test_cnn():
"""Smoke test: CNN((1, 28, 28), 10) on 4 random images; return True if logits are (4, 10)."""
# Your code here
pass
The interactive editor loads with Python and NumPy. Run your implementation against 7 tests, or use the progressive hints.