JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Building a Real Convolutional Layer

Turn one kernel into a real convolutional layer with many channels, then master stride, padding, and the output-size formula.

From one kernel to a stack of filters

In guide 1 we slid a single kernel across an image and watched it light up wherever it found the pattern it was tuned to — say, a vertical edge. That one kernel produced one feature map: a grid of scores answering 'how strongly is my pattern present here?'. A real convolutional layer does nothing more mysterious than run many such kernels at once.

Picture a layer with, say, 16 different convolution kernels. Each slides independently over the same input and makes its own feature map. If every map is H × W, then stacking all 16 on top of one another gives a single 3D output of shape H × W × 16. That third dimension — the number of stacked maps — is what we call the output's feature channels. The rule to memorise: the number of output channels equals the number of kernels in the layer.

A convolutional layer runs many kernels in parallel; each kernel's feature map becomes one channel, and they stack into an H × W × C_out output volume.

An input image feeding several kernels, each producing a separate feature map, the maps stacking into a 3D output block whose depth equals the number of kernels.

Because each kernel has its own weights, each learns to detect a different pattern: one might fire on vertical edges, another on horizontal ones, a third on small bright blobs, a fourth on a particular colour transition. Think of the output volume as a deck of cards — each card is one feature map, and the whole deck is the layer's full report on the image: 'here is where I saw edges, here is where I saw blobs, …'.

Channels: depth in, depth out

At every spatial position the kernel lays its K × K × C_in box over the input, multiplies each weight by the input value beneath it — across all C_in channels — and adds the whole pile into a single number. So even though the box is 3D, it still collapses the depth and emits exactly one value per position. One kernel therefore still produces exactly one feature map, no matter how deep the input is. Analogy: each kernel is a small committee that reads every colour layer and then casts a single vote.

A single kernel spans all C_in input channels (K × K × C_in) and sums across the depth, collapsing the channels into one feature map.

A 3-channel input with a 3x3x3 kernel box overlapping all three channels at one position; the multiply-and-sum produces a single output value.

Let's make it concrete. Take an input of shape 32 × 32 × 3 (a small RGB image). A single 3 × 3 × 3 kernel run as a 'valid' convolution (no padding, stride 1) produces a 30 × 30 × 1 feature map — the 3 in the kernel matched the 3 in the input and got summed away, which is why the output depth is 1, not 3. Now stack sixteen such kernels in one convolutional layer: you get sixteen 30 × 30 maps, i.e. an output of 30 × 30 × 16. Crucially, every one of those 16 output feature channels looked at *all 3* input channels — there is no output channel that only ever saw red.

\text{params} = K \times K \times C_{\text{in}} \times C_{\text{out}} + C_{\text{out}}

The number of learnable weights in one convolutional layer.

Read the formula left to right. K × K is the spatial footprint of one kernel (9 weights for a 3×3). We multiply by C_in because the kernel reaches across every input channel (so a 3×3 kernel on a 3-channel input is really 3×3×3 = 27 weights). We multiply by C_out because the layer holds that many independent kernels. Finally we add C_out because each kernel gets one bias term. Plugging in K=3, C_in=3, C_out=16: params = 3×3×3×16 + 16 = 432 + 16 = 448. The headline is that this count does not depend on the image's height or width at all — a sharp contrast with the dense layer from guide 1, whose parameter count grew with the number of pixels. The same 448 weights process a 32×32 image or a 4000×3000 one.

Stride: stepping faster to shrink the map

So far our kernel has tiptoed one pixel at a time. The stride is simply how big a step the kernel takes between evaluations. Stride 1 means 'move one pixel, compute, repeat' — it visits every position. Stride 2 means 'move two pixels each time', skipping every other position.

With stride 1 the output is (almost) the same size as the input — dense and detailed. With stride 2 you evaluate the kernel at only about half as many positions along each axis, so each spatial dimension is roughly halved and the feature map comes out about a quarter of the area. Because the kernel's weights are still learned, a strided convolutional layer is a cheap, learnable downsampler: it shrinks the map and extracts features in one move.

Stride controls how far the kernel jumps between evaluations: stride 1 visits every position, stride 2 skips every other one.

A kernel sliding across a row at stride 1 landing on consecutive positions, versus at stride 2 landing on every other position, producing a shorter output.

Tiny example. Take a 7-wide input row and a kernel of width 3 (no padding). At stride 1 the kernel's left edge can sit at columns 0, 1, 2, 3, 4 — five placements, so the output is 5 wide. At stride 2 it lands only at columns 0, 2, 4 — three placements, so the output is 3 wide. Same kernel, same input; doubling the stride nearly halved the output.

Padding: protecting the borders

Notice that 'valid' convolution made our 32-wide input come out 30 wide, and the 7-wide row come out 5 wide. Stack many such layers and the map keeps shrinking — in a deep network you would simply run out of map. There is a second, subtler problem: a kernel centred on a corner pixel would hang off the edge, so corner and border pixels get visited far fewer times than central ones. Left untreated, information near the borders quietly fades away.

The fix is padding: before convolving, we wrap the input in a frame of extra pixels — usually zeros, hence zero-padding. With a one-pixel frame of zeros, the kernel can now centre itself directly on what used to be the edge pixels, because there are now 'virtual' pixels just outside for it to overlap. The zeros contribute nothing to the weighted sum, so they don't invent fake signal — they simply give the kernel somewhere to stand.

Zero-padding adds a frame of zeros so the kernel can centre on the edge pixels instead of falling off the border.

An input grid surrounded by a one-pixel frame of zeros, with a kernel centred on a former edge pixel, its corner now overlapping a padding zero.

Two padding modes have standard names. Valid means no padding at all: the kernel only sits where it fully fits, so the feature map shrinks. Same means 'pad just enough that the output keeps the same spatial size as the input' (at stride 1). For a kernel of size K, the padding per side that achieves this is P = (K − 1) / 2: a 3×3 needs P=1, a 5×5 needs P=2, a 7×7 needs P=3. Notice this is a whole number only when K is odd — which is exactly why kernel sizes 3, 5, and 7 are the everyday standard: they have a well-defined centre and a clean, symmetric pad.

The output-size formula

Everything above — kernel size, stride, padding — comes together in one formula that predicts the exact output size. This is the single most useful equation in this whole guide; learn it well enough to use on paper.

O = \left\lfloor \dfrac{W - K + 2P}{S} \right\rfloor + 1

Output size along one axis. The same formula applies independently to height (swap W for H).

Here O is the output width, W the input width, K the kernel size, P the padding added on each side, and S the stride. Read the pieces. We subtract K because the kernel needs K pixels of room to fit before its first placement even counts. We add 2P because padding adds P usable pixels on the left and P on the right, widening the field the kernel can roam. We divide by S because we only land on every S-th position, so the remaining placements scale down by S. We add 1 because the very first placement always counts — division alone counts the gaps between placements, not the placements themselves. And we take the floor ⌊·⌋ because a final step that would hang off the edge is simply not taken, so any fractional leftover is dropped. The identical formula governs the height: just replace W with H.

def conv_out(W, K, P, S):
    # Output size along one axis of a convolution.
    # W = input size, K = kernel size, P = padding per side, S = stride
    return (W - K + 2 * P) // S + 1   # // is floor division

# Three contrasting 3x3 layers on a 32-wide input:
conv_out(32, 3, 0, 1)   # valid             -> 30
conv_out(32, 3, 1, 1)   # same              -> 32
conv_out(32, 3, 1, 2)   # stride-2, padded  -> 16
The formula in code — note that // (floor division) is exactly the floor in the equation.

Walk through each on our 32-wide input with a 3×3 kernel. Valid (P=0, S=1): (32 − 3 + 0)/1 + 1 = 29 + 1 = 30 — the map shrinks by K−1 = 2, as we saw. Same (P=1, S=1): (32 − 3 + 2)/1 + 1 = 31 + 1 = 32 — padding exactly cancels the shrink, so the size is preserved (that's P = (K−1)/2 at work). Stride-2, padded (P=1, S=2): (32 − 3 + 2)/2 + 1 = 31/2 + 1 = 15.5 → floor to 15 → +1 = 16 — here the floor actually bites, dropping the half-step, and the 32-wide input is neatly halved to 16. Running the same arithmetic on the height turns a 32×32 input into 30×30, 32×32, and 16×16 respectively.

Why weight sharing makes this affordable

Let's tie this back to guide 1's worry about dense layers being too big. A dense (fully connected) layer gives every output its own private weight to every single input, so its parameter count explodes with image size. Weight sharing is the convolution's answer: the same small kernel is reused at every position, so we pay for the kernel once, not once per pixel.

Put numbers on it. A 3×3 convolutional layer mapping 64 input feature channels to 64 output channels has 3×3×64×64 ≈ 36,864 weights (plus 64 biases) — about 37k — and it works on an image of any size. Compare a dense layer: even just flattening a modest 32×32×64 feature map (that's 65,536 numbers) and connecting it to a mere 64 outputs needs 65,536 × 64 ≈ 4.2 million weights — and that number grows if the image gets bigger. The convolution is over a hundred times leaner and size-independent.

Weight sharing, combined with local connectivity (each output looks only at a small K×K neighbourhood), buys two things. First, trainability: a layer with thousands of parameters instead of millions is far easier to optimise and far less data-hungry. Second, a built-in prior — a baked-in assumption that useful visual features are local (edges and textures live in small neighbourhoods) and position-independent (an edge is an edge wherever it appears). Reusing one kernel everywhere encodes exactly that belief, which is usually true for images, and so it improves generalisation to new pictures.