EP09_01 — 🟢 Manual 2D Convolution (Forward Pass of a Learned Layer)
9.10.1 EP09_01 🟢 Manual 2D Convolution (Forward Pass of a Learned Layer)
PyTorch, presented in this chapter, executes nn.Conv2d(x) in a single call — but behind it is merely the cross-correlation between a (already trained) kernel and a neighborhood of the input, followed by the sum of a bias and an activation, exactly as formalized in the “Convolutional Layer” Section. The essential difference compared to the fixed-kernel convolution of Chapter 3 is that, here, the kernel and bias values already come ready (as if they had been learned by gradient), and it is up to you to manually reproduce the forward pass that the framework executes internally.
Before training a real CNN, you have been tasked with implementing this forward pass from scratch, for a single convolutional layer with a single input channel and a single output filter, including support for arbitrary padding and stride.
9.10.1.1 📋 Implementation Guidelines
Input: Read the dimensions \(H \times W\) of the input feature map and, subsequently, its \(H \times W\) real values.
Kernel and bias: Read the dimensions \(k_h \times k_w\) of the (already trained) kernel, its real values, and the bias \(b\) (real, scalar).
Hyperparameters: Read the padding \(p\) (integer, number of zeros added to each border) and the stride \(s\) (integer, sliding step).
Padding: Add \(p\) zeros to each of the four borders of the input map before the correlation.
Cross-correlation: For each output position \((i, j)\), compute \[
z(i,j) = b + \sum_{u=0}^{k_h-1} \sum_{v=0}^{k_w-1} K(u,v) \cdot X_{pad}(i \cdot s + u,\; j \cdot s + v),
\] scanning the input without flipping the kernel (the convention of deep learning frameworks, different from classical mathematical convolution).
Activation: Apply ReLU to each value: \(a(i,j) = \max(0, z(i,j))\).
Output: Print \(O_h\) and \(O_w\) on the first line, followed by \(O_h\) lines with \(O_w\) real values each (the output feature map, already with ReLU applied), formatted with 4 decimal places.
9.10.1.2 📌 Computational Restrictions
One input channel, one output filter: there is no need to handle multiple channels or multiple filters in this simplified version.
No kernel flipping: implement cross-correlation, not the classical mathematical convolution with a flipped kernel — this is the operation that PyTorch (and most frameworks) calls “convolution”.
Zero padding: the \(p\) pixels added to each border are always \(0\).
Formatting: all output values must have exactly 4 decimal places, even when the value is an integer (e.g., 2.0000).
9.10.1.3 🧠 Theoretical Foundation
Element
Role in the convolutional layer
Kernel \(K\)
Parameters learned by gradient, analogous to the coefficients of a fixed filter from Chapter 3, but adjusted by backpropagation
Bias \(b\)
Learned offset, added after the correlation — allows the neuron to “fire” even with null input
Padding
Controls the spatial output dimension and prevents the loss of information at the borders at each layer
Stride
Controls the sliding step; values \(> 1\) reduce spatial resolution, as a form of downsampling embedded within the convolution itself
ReLU
Introduces non-linearity after the linear combination, exactly as in the “Activation Function” Section
9.10.1.4 🧩 morph.py Methods That May Help
mm.readImg(h, w, dtype='float') — directly reads an \(h \times w\) matrix of real values from standard input, saving manual parsing of the feature map and the kernel.
mm.correlacao0(f, kernel, bias) — implements the same cross-correlation sum + bias that you will compute by hand, but without support for padding or stride, and converts the result to uint8 (truncating negative and decimal values). It may serve as a conceptual reference or to check the simplest case (\(p=0\), \(s=1\)), but it does not replace your complete implementation — which must preserve sign, decimal places, padding, stride, and ReLU.
9.10.1.5 📦 Input and Output Specification (VPL)
Input:
Line 1: Integers \(H\) and \(W\).
Next \(H\) lines: \(W\) real values each (input map).
Next line: Integers \(k_h\) and \(k_w\).
Next \(k_h\) lines: \(k_w\) real values each (kernel).
Next line: Real \(b\) (bias).
Next line: Integers \(p\) and \(s\).
Output:
Line 1: Integers \(O_h\) and \(O_w\).
Next \(O_h\) lines: \(O_w\) real values each, with 4 decimal places.
9.10.1.6 📌 Examples
Input
Output
Observation
3 3
1 2 0
0 1 2
1 0 1
2 2
1 1
1 1
-2
0 1
2 2
2.0000 3.0000
0.0000 2.0000
Padding 0, stride 1: \(2\times2\) output without padding.
3 3
1 2 0
0 1 2
1 0 1
2 2
1 0
0 1
0
1 2
2 2
1.0000 0.0000
1.0000 2.0000
Padding 1, stride 2: input padded with zeros before the correlation.
Fixed 4×4 input, fixed 2×2 kernel (highlighted in blue) — adjust padding (p), stride (s) and bias (b), exactly the parameters EP09_01 asks for as input, and see how they change the output size and values.
Padding (p)
Stride (s)
Bias (b)
(0,0)
Padded input X (with padding)
original padding (0) current window
Kernel K (2×2)
Output Y = ReLU(X⊛K + b)
💡 Each slider position reveals one cell of the output matrix. Go through all positions to complete the output map. Changing p, s, or b resets the exploration because the output map changes in size and/or values.
Figure 9.43: EP09_01 Simulator: Manual 2D Convolution (cross-correlation + bias + ReLU, with adjustable padding and stride)
%%writefile EP09_01.py# Python code
Overwriting EP09_01.py
TestSuite("EP09_01.py").run()
✔️ EP09_01.cases already exists in casos/
📋 3 case(s) loaded from casos/EP09_01.cases
🔍 Testing Python: EP09_01.py
⚠️ EP09_01.py: Empty file (fewer than 3 lines). Tests skipped.