Majid Al-RaimiConvolution layers and their tensor shapes

COE 592Lecture 02Part 02

Convolution layers and their tensor shapes

Local receptive fields and weight sharing, from 1D to 2D convolution, the no-padding output size and a fully worked two-channel example.

Concepts
4
Slides
8-15
Reading
24 min
Understood
0/4 concepts

Why this part matters

Every efficiency technique in this course is an operation on the tensors of a convolution layer. Lecture 3 counts parameters and MACs from these shapes, pruning in lecture 4 removes entries of W, quantization shrinks the bits of W and Y, and the depthwise and grouped convolutions later in this lecture change the shape of W itself. Every model-size or MAC estimate for an embedded target starts from that shape and from the formula for the output height h_o.

Part 01 gave you the fully connected layer, whose weight matrix (c_o, c_i) connects every input to every output. This part crosses out that matrix and rebuilds it as a convolution: a small filter that sees only a local patch and is reused at every position. You will learn to read the four Conv2D tensors, to predict how much the feature map shrinks, to compute one output value by hand from a two-channel input, and to count parameters. These four skills are the most reliable exam material in the lecture, and they are the same arithmetic you will do when you size a model for a microcontroller or read a MobileNet table.

By the end you can

  1. Explain, with the receptive field and weight sharing, why one filter is reused at every position and why the parameter count does not depend on image size.
  2. Write the shapes of X, Y, W and b for any Conv2D from its c_i, c_o, k_h, k_w and input size, in PyTorch order.
  3. Compute the output size without padding at stride 1, and with the general stride formula that slide 15 uses ahead of slide 18.
  4. Compute one output value by hand from a multi-channel input, summing per-channel contributions and adding the bias.
  5. Count a Conv2D's parameters as c_o c_i k_h k_w + c_o and check it against the 1216 of the ConvNetJS demo.

Take a CIFAR-10 image: 32 x 32 pixels in three colour channels, so 3072 numbers. A single neuron of the Fully connected layer from Part 01 has one weight for each of those 3072 inputs, and CS231n uses exactly this count to show why the fully connected design does not scale to images. A neuron in a Convolution layer does something far more modest. It looks at a 5 x 5 patch of the image across all three channels, so 5 x 5 x 3 = 75 numbers, and ignores everything else. That patch is the neuron's Receptive field, and the slide states the whole idea in one sentence: the output neuron is connected to input neurons in the receptive field.

That sentence hides two separate decisions, and Goodfellow, Bengio and Courville name them in section 9.2 of Deep Learning. The first is sparse interactions. With m inputs and n outputs, a fully connected layer stores m x n weights. If each output is allowed to touch only k inputs, the layer stores k x n. On the CIFAR image that is 75 per output instead of 3072.

m×nfully connected    k×nsparse    ksparse and shared\underbrace{m \times n}_{\text{fully connected}} \;\longrightarrow\; \underbrace{k \times n}_{\text{sparse}} \;\longrightarrow\; \underbrace{k}_{\text{sparse and shared}}
Weights stored for one output channel, as the two convolution ideas are applied in turn

The second decision is Weight sharing. Sparse connectivity alone would still give every output position its own private set of 75 weights. Convolution goes further and uses the same 75 numbers at every position. Goodfellow and colleagues put it plainly: rather than learning a separate set of parameters for every location, we learn only one set, so the storage of one filter drops to k parameters. They also give the synonym you will meet in older papers, tied weights. Slide 8 labels its figure Weight Sharing, and slides 12 and 13 show it in motion: the same green, yellow and cyan filters are applied wherever the red window sits on the blue input.

One single-channel 5x5 stamp, pressed at four positions. The position count grows, the parameter count stays at 25.

What the saving looks like in a real first layer

CS231n works through AlexNet's first layer to show the size of the effect, and the numbers are worth holding in memory for the exam. The input is 227 x 227 x 3 (the AlexNet paper says 224 x 224; CS231n uses 227 so that (227 - 11)/4 + 1 = 55 comes out exact), the filters are 11 x 11 x 3, and the output is 55 x 55 x 96.

Worked example

AlexNet conv1 with and without weight sharing

  1. Count the output neurons

    55 x 55 x 96 = 290,400 outputs, one per position per output Channel.
  2. Weights per neuron with a local receptive field only

    11 x 11 x 3 = 363 weights plus one Bias, so 364 parameters each.
  3. No sharing

    290,400 x 364 = 105,705,600 parameters for one layer.
  4. With sharing

    One filter per output channel: 96 x 363 = 34,848 weights plus 96 biases, so 34,944.
  5. Reduction

    About 3025x fewer parameters, and 3025 = 55 x 55 is exactly the number of positions the filter is reused at.

Crossing out the fully connected shapes

The slide introduces the shapes by literally striking through the fully connected ones. In Part 01 the input was X (n, c_i): n samples in the batch (the Batch size), each a vector of c_i features. A 1D convolution adds a spatial axis of width w_i, so X becomes (n, c_i, w_i) and Y becomes (n, c_o, w_o). The weight matrix (c_o, c_i) gains a kernel width k_w and becomes (c_o, c_i, k_w). The bias does not change at all: still one number per output channel, (c_o,). The table already shows the 2D column, which adds a height axis in the same way; the next concept explains each of its axes.

TensorFully connected1D convolution2D convolution
Input X(n, c_i)(n, c_i, w_i)(n, c_i, h_i, w_i)
Output Y(n, c_o)(n, c_o, w_o)(n, c_o, h_o, w_o)
Weights W(c_o, c_i)(c_o, c_i, k_w)(c_o, c_i, k_h, k_w)
Bias b(c_o,)(c_o,)(c_o,)
Tensor shapes as the fully connected layer becomes a convolution

Recall

A 1D convolution has 8 input channels, 32 output channels and kernel width 5. Write the shapes of W and b, and say what happens to them if the input width doubles.

W is (32, 8, 5) and b is (32,). Doubling w_i changes nothing in either shape, because of weight sharing; only X, Y and the compute grow.

Quick check

Why does a convolution layer on a 32x32x3 image need far fewer parameters than a fully connected layer?

Reading the four Conv2D tensors: X, Y, W and b

Start with a layer you could type into PyTorch today: nn.Conv2d(3, 16, kernel_size=5), applied to a batch of 8 RGB images of 32 x 32. Four tensors take part, and every axis of each one has a name.

nn.Conv2d(3, 16, kernel_size=5) on 8 images of 32x32, no padding

Input X
(8, 3, 32, 32): batch, input channels, height, width
Weights W
(16, 3, 5, 5): output channels, input channels, kernel height, kernel width
Bias b
(16,): one scalar per output channel
Output Y
(8, 16, 28, 28): batch, output channels, then 32 - 5 + 1 = 28 each way

The general rule is the 2D column that the previous concept left open. The input X is (n, c_i, h_i, w_i), the output Y is (n, c_o, h_o, w_o), the weights W are (c_o, c_i, k_h, k_w) and the bias b is (c_o,). The PyTorch documentation for torch.nn.Conv2d confirms this exact ordering: the weight is stored as (out_channels, in_channels / groups, kernel_size[0], kernel_size[1]) and the bias as (out_channels). The groups divisor is 1 by default; it becomes important in Part 03 when grouped and depthwise convolutions arrive.

filters=co,volume of one filter=cikhkw\text{filters} = c_o, \qquad \text{volume of one filter} = c_i \cdot k_h \cdot k_w
Reading W as c_o filters, each c_i deep and k_h by k_w wide

A filter is a block, not a square

Slides 9 to 11 make one argument in three frames. The first frame draws a single filter as a green block with edges labelled k_h, k_w and c_i, sitting next to the red window on the input. The block is as deep as the input has channels. When it is applied at one position, it multiplies all c_i x k_h x k_w input values under the window by its own c_i x k_h x k_w weights, sums everything into a single number, and drops that number into one output channel. CS231n states the rule as a sentence to memorise: the extent of the connectivity along the depth axis is always equal to the depth of the input volume.

The second frame adds a yellow filter. It has the same depth c_i, sees the same window, and drops its number into a second output channel. The third frame shows the full stack of c_o filters with a bracket labelled c_o, and the output is now c_o channels deep. That bracket is literally the first axis of W. Choosing c_o is a design decision made by whoever builds the network; it is not derived from c_i.

Each filter spans all c_i input slabs and lights exactly one output channel. Three filters, three channels.

Bias and batch, the two easy axes

The Bias is added once per output channel, after the whole c_i-deep dot product, which is why its shape is (c_o,) exactly as in the fully connected layer of Part 01. The Batch size n appears in X and Y but never in W or b: the same weights process every sample in the batch. If you are ever unsure of a shape, check that n is absent from the parameters and that c_o leads W.

Recall

Write the shapes of X, W, b and Y for nn.Conv2d(3, 16, kernel_size=5) applied to a batch of 8 images of 32x32 with no padding.

X (8, 3, 32, 32), W (16, 3, 5, 5), b (16,), Y (8, 16, 28, 28) since 32 - 5 + 1 = 28.

Recall

A Conv2D has 64 input channels and 128 filters of 3x3. How many output channels does it produce, and what is the shape of W?

128 output channels, one per filter. W is (128, 64, 3, 3), which holds 73,728 weights, plus 128 biases for 73,856 parameters in total.

Quick check

A Conv2D layer has c_i = 3, c_o = 16 and 5x5 kernels. Which is the shape of its weight tensor W?

Slide 14 gives the smallest example that shows the shrink. The input is 4 x 4, the Kernel (filter) is 3 x 3, there is no padding and the window moves one cell at a time. Along the top row the window can start at column 0 or column 1. Starting at column 2 would need columns 2, 3 and 4, and there is no column 4. Two starting positions per axis means the output is 2 x 2.

Worked example

Counting the valid positions on slide 14

  1. Last valid start index

    The window of height k_h = 3 starting at row r covers rows r to r + 2. It fits while r + 2 <= 3, so the last start is r = h_i - k_h = 1.
  2. Count the starts

    Rows 0 and 1, which is h_i - k_h + 1 = 4 - 3 + 1 = 2 positions.
  3. Result

    h_o = w_o = 2. The feature map is 2 x 2, smaller than the input by k - 1 = 2 along each axis (one cell lost at each border).
ho=hikh+1,wo=wikw+1h_o = h_i - k_h + 1, \qquad w_o = w_i - k_w + 1
No padding, stride 1
A 3x3 window on a 4x4 grid stops at four places, and each place fills one output cell. The dashed fifth window overhangs and never counts.

Slides 12 and 13 are two frames of the same motion, taken from the MIT 6.5940 lecture that this section of the deck reproduces. The red window moves one step at a time along a spatial dimension, and at every stop the same green, yellow and cyan filters are applied, which is why slide 12 carries the label Weight Sharing. Nothing about the filters changes as the window moves; this is where Weight sharing becomes visible rather than stated. Each stop adds one short stack of c_o values to the output, one value per filter, so the output has exactly one spatial cell per valid window position. The count of valid positions is the whole story of the output size.

The general formula, one part early

Slide 15 asks you to use a Stride of 2 before slide 18 has defined stride, so you need the general rule now. With padding p on every border and stride s, Dumoulin and Visin's relationship 6 gives the output size, and the PyTorch documentation states the same formula with a dilation term that is 1 throughout this course.

ho=hi+2pkhs+1h_o = \left\lfloor \frac{h_i + 2p - k_h}{s} \right\rfloor + 1
General output size; set p = 0 and s = 1 to recover h_i - k_h + 1
h_ikspArithmetich_o
4310(4 - 3)/1 + 12
4220(4 - 2)/2 + 12
32510(32 - 5)/1 + 128
32512(32 + 4 - 5)/1 + 132
224723floor((224 + 6 - 7)/2) + 1112
Output height for several configurations

Two rows deserve a second look. The 32, 5, 1, 2 row is the ConvNetJS demo on slide 25, which uses pad 2 precisely so that its 5 x 5 convolution keeps the 32 x 32 size, giving the 32 x 32 x 16 output printed there. The 224, 7, 2, 3 row is the first layer of a ResNet, which halves the spatial size in one step. Both are the same formula.

Recall

A 32x32 input meets a 5x5 kernel at stride 1. What is the output size with no padding, and with padding 2?

No padding: 32 - 5 + 1 = 28, so 28 x 28. Padding 2: (32 + 4 - 5)/1 + 1 = 32, so 32 x 32, the same as the input.

Quick check

An input of 4x4 is convolved with a 3x3 kernel, stride 1, no padding. What is the output spatial size?

Slide 15 is the exam question in miniature: two input channels of 4 x 4, one output channel, a 2 x 2 filter, Stride 2, no padding and no Bias. Input channel 1 has every row equal to 1 2 3 4; input channel 2 has every row equal to 5 6 7 8. The filter's first slab is [[2, 1], [1, 2]] and its second slab is [[1, 2], [2, 1]]. In the shapes of the previous concepts, X is (1, 2, 4, 4) and W is (1, 2, 2, 2).

Y[o,i,j]=b[o]+c=0ci1u=0kh1v=0kw1W[o,c,u,v]X[c,  si+u,  sj+v]Y[o, i, j] = b[o] + \sum_{c=0}^{c_i - 1} \sum_{u=0}^{k_h - 1} \sum_{v=0}^{k_w - 1} W[o, c, u, v] \cdot X[c,\; s i + u,\; s j + v]
One output value: every channel, every kernel cell, summed into a single number, then the bias

The formula says what the filter-is-a-block idea implied: the sum over c folds all input channels into one number. There is no separate output per input channel. With that in mind the four values of the slide are four applications of the same recipe. Strictly, this is cross-correlation: mathematical convolution flips the kernel first, and this formula does not. Deep learning libraries, PyTorch and this deck all call the unflipped version convolution (Goodfellow et al., section 9.1), and this course follows that convention.

Worked example

Slide 15, worked from the shapes down

  1. Output size

    Stride 2 with no padding: (4 - 2)/2 + 1 = 2, so the output is 2 x 2 and the window visits four positions, starting at rows and columns 0 and 2.
  2. Top-left value, window at rows 0 to 1, columns 0 to 1

    Per-channel contributions at position (0, 0)

    Channel 1
    2·1 + 1·2 + 1·1 + 2·2 = 9
    Channel 2
    1·5 + 2·6 + 2·5 + 1·6 = 33
    Bias
    0
    Sum
    9 + 33 = 42
  3. Top-right value, window at rows 0 to 1, columns 2 to 3

    Per-channel contributions at position (0, 1)

    Channel 1
    2·3 + 1·4 + 1·3 + 2·4 = 21
    Channel 2
    1·7 + 2·8 + 2·7 + 1·8 = 45
    Bias
    0
    Sum
    21 + 45 = 66
  4. Bottom row

    Every row of both inputs is identical, so the windows at rows 2 to 3 see the same numbers and give 42 and 66 again.
  5. Output feature map

    [[42, 66], [42, 66]], exactly as printed on the slide.

Now do it with your hands. The stepper below holds the slide 15 numbers, highlights the current window on both input channels, lists the products channel by channel and fills the output one cell at a time. Switch the stride to 1 to see the 3 x 3 output, then change a few cells and predict the result before you press Next.

SimulatorSlide 15, one window at a time
h_o = (4 - 2) / 2 + 1 = 2
Input channel 1
Input channel 2
Filter channel 1
Filter channel 2
Output 2×2
·
·
·
·
This position
press Next to place the window
0 / 4

Every input and filter cell is editable, negative weights included; a blank cell counts as 0. Rows and columns are numbered from 0, as in the formula above. Each channel of the filter multiplies the matching input channel inside the highlighted window, the two channel sums are added into one value, and the bias is added last.Channel 1 and channel 2 keep their colours throughout.

Stride 2 (the slide)Stride 1
Output size(4 - 2)/2 + 1 = 2, so 2x2(4 - 2)/1 + 1 = 3, so 3x3
Channel 1 partials, one row9, 219, 15, 21
Channel 2 partials, one row33, 4533, 39, 45
One output row42, 6642, 54, 66
Windows visited49
Same data, two strides

The stride-1 column shows what the middle window adds. At columns 1 to 2, channel 1 gives 2·2 + 1·3 + 1·2 + 2·3 = 15 and channel 2 gives 1·6 + 2·7 + 2·6 + 1·7 = 39, so 54 sits between 42 and 66. Stride 2 skips that window entirely, which is why the output is smaller and the compute drops from 9 windows to 4. On a large input the compute falls to about a quarter of the stride-1 cost, since both axes are halved.

Recall

In the slide 15 example, what are the per-channel contributions to the top-right output value?

Channel 1: 2·3 + 1·4 + 1·3 + 2·4 = 21. Channel 2: 1·7 + 2·8 + 2·7 + 1·8 = 45. Total 66.

Recall

Same data with stride 1: what is the output size, and what is the middle column?

(4 - 2)/1 + 1 = 3, so 3 x 3. Every middle-column value is 54, which is 15 from channel 1 plus 39 from channel 2.

Quick check

In the slide 15 example (two input channels, one filter with a 2x2 slab per input channel, stride 2, no bias), what is the top-left output value?

Counting parameters

The shapes of W and b give the parameter count directly, and that count is what decides whether a model fits in a microcontroller's flash. The weights number c_o x c_i x k_h x k_w, the size of W, and there is one bias per output channel.

#params=cocikhkw+co\#\text{params} = c_o \cdot c_i \cdot k_h \cdot k_w + c_o
Weights from the shape of W, plus one bias per output channel

Slide 25 later in this lecture shows the ConvNetJS CIFAR-10 demo, whose first convolution has 16 filters of 5 x 5 over the 3 colour channels. The demo prints 16 x 5 x 5 x 3 + 16 = 1216 parameters, and the formula reproduces it: 1200 weights and 16 biases. Use that number as a retrieval hook; if your formula does not give 1216 for that layer, you have forgotten the biases or swapped a factor.

LayerArithmeticParametersDepends on h_i, w_i?
Conv2D, 16 filters of 5x5, 3 to 16 channels16 x 3 x 5 x 5 + 161216No
Fully connected, 32x32x3 = 3072 inputs to 16 outputs3072 x 16 + 1649,168Yes
AlexNet conv1, 96 filters of 11x11, 3 to 96 channels96 x 3 x 11 x 11 + 9634,944No
Parameter counts for three layers (the first two on a 32x32x3 input)
ChangeArithmeticParameters
Input 64x64x3, conv16 x 3 x 5 x 5 + 161216
Input 64x64x3, fully connected12288 x 16 + 16196,624
Kernels 3x316 x 3 x 3 x 3 + 16448
Filters 3232 x 3 x 5 x 5 + 322432
What moves the Conv2D parameter count

Only the filter geometry and the channel counts move the number.

Recall

How many parameters does nn.Conv2d(3, 16, kernel_size=5) have, and what changes if the images become 64x64?

16 x 3 x 5 x 5 + 16 = 1216. Nothing changes with the image size; the parameter count does not depend on h_i or w_i because of weight sharing.

Quick check

A Conv2D with 16 filters of 5x5x3 plus biases (the ConvNetJS first layer) has how many parameters?

Recap

If you remember nothing else

  • A conv output neuron is connected only to the input neurons in its receptive field, and the same filter is reused at every position (weight sharing).
  • Shapes: X (n, c_i, h_i, w_i), Y (n, c_o, h_o, w_o), W (c_o, c_i, k_h, k_w), b (c_o,). The batch size n never appears in W or b.
  • A filter is not 2D: it is c_i deep and produces exactly one output channel, so c_o filters give c_o channels regardless of c_i.
  • Without padding at stride 1, h_o = h_i - k_h + 1: a 4x4 input with a 3x3 kernel gives 2x2; in general h_o = floor((h_i + 2p - k_h)/s) + 1.
  • Slide 15: output size (4 - 2)/2 + 1 = 2; top-left 9 + 33 = 42, top-right 21 + 45 = 66; output [[42, 66], [42, 66]]; bias is ignored on the slide.
  • Parameters = c_o c_i k_h k_w + c_o; 16 filters of 5x5x3 plus biases give 1216; the count is independent of h_i and w_i, unlike an FC layer's 3072 x 16 + 16 = 49,168.

Sources