Majid Al-RaimiPadding, stride, receptive field and grouped convolution

COE 592Lecture 02Part 03

Padding, stride, receptive field and grouped convolution

How padding and stride control the output size, how the receptive field grows with depth, and how grouped and depthwise convolution cut parameters.

Concepts
4
Slides
16-20
Reading
24 min
Understood
0/4 concepts

Why this part matters

Part 02 gave you a convolution that slides one filter over one input. Every embedded CNN you will profile or design in this course, from MobileNet to the TinyML backbones in your project, is built by turning four extra knobs on that layer: padding, stride, depth and grouping.

Those knobs decide the three numbers an embedded engineer lives by. The output size sets how much activation memory a layer needs. The receptive field sets how many layers it takes before one output can see the whole image. The parameter count sets how much flash the weights occupy and, with the output size, how many multiply-accumulates each inference costs. The exam asks for all three directly, and the storage and MAC budgets in your research project come out of the same formulas.

By the end you can

  1. Compute the output size of any Conv2D from h_i, k, p and s, including the floor, and pick p for a same-size output.
  2. Explain what each padding mode puts in the border and why none of them adds a parameter.
  3. Compute the receptive field after L layers, with and without stride, and explain why networks downsample.
  4. Count weights and multiply-accumulates for standard, grouped and depthwise convolutions, and check that g divides c_i and c_o.
  5. Argue when a depthwise separable block is the right trade for an embedded device and what it gives up.

Take the 5 x 5 input and 3 x 3 Kernel (filter) from Part 02. The kernel can only sit where it fits entirely inside the image, so it has three horizontal and three vertical positions and the output Feature map is 3 x 3. Stack a second such layer and you get 1 x 1. A third layer cannot run at all. Every layer eats two rows and two columns, and Goodfellow, Bengio and Courville put it plainly: without padding the representation shrinks by one pixel less than the kernel width at every layer.

Padding is the fix. Add a ring of p extra cells around the input before sliding the kernel. With p = 1 the 5 x 5 image becomes 7 x 7, the kernel now has five positions in each direction, and the output is 5 x 5 again. The same 27 weights per filter (3 x 3 over three input channels) do the work; the only thing that changed is where the kernel is allowed to stand.

ho=hi+2pkh+1(s=1)h_o = h_i + 2p - k_h + 1 \qquad (s = 1)
Output height with p cells of padding on each side, stride 1

Read the formula as a story. The padded input is h_i + 2p rows tall. A window of height k_h can start on any row except the last k_h - 1, which leaves h_i + 2p - k_h + 1 starting rows, one per output row. Set h_o = h_i and the equation solves to p = (k - 1)/2, which is a whole number only for odd kernels. That is why 3 x 3, 5 x 5 and 7 x 7 dominate practice: Zhang et al. note that an odd kernel lets the same amount of padding go on both sides and keeps the output pixel centred on its window. Dumoulin and Visin call this half padding, and the deep learning book calls it same convolution, after the MATLAB option of that name.

Worked example

Same, valid and a strided stem

  1. CIFAR-10 input, no padding

    32 x 32 image, k = 5, p = 0: 32 - 5 + 1 = 28. The map shrinks by four.
  2. Same padding for k = 5

    p = (5 - 1)/2 = 2: 32 + 4 - 5 + 1 = 32. The map keeps its size.
  3. The slide 16 example

    h_i = 5, k = 3, p = 1: 5 + 2 - 3 + 1 = 5.
  4. Parameters in every case

    Identical. A 3 x 3 filter over three input channels has 27 weights whether p is 0, 1 or 10. Padding changes the output size and therefore the multiply-accumulates, and nothing else.

What goes in the ring

The formula does not care what the new cells contain, but the values do affect what the network learns near the border. The slide pads a 3 x 3 image holding 1 to 9 by p = 2 on every side, which is a good specimen because 2 is the largest reflection padding a 3 x 3 image can take. Zero padding writes zeros, and PyTorch uses it unless told otherwise. Reflection padding mirrors the image across its edge without repeating the edge pixel, so the row above 1 2 3 reads 4 5 6 and the row above that reads 7 8 9. Replication padding stretches each edge pixel outward, so the corner 1 fills the whole top-left 3 x 3 block. Constant padding is zero padding with a value of your choice.

The same 3 x 3 image padded by 2. Reflection reaches back into the interior, replication clamps to the edge, zero has no source at all.
ModeRuleRow 0PyTorch
ZeroFill the border with 00 0 0 0 0 0 0padding_mode='zeros' (default)
ReflectionMirror across the edge; the edge value itself is not repeated9 8 7 8 9 8 7ReflectionPad2d, padding_mode='reflect'
ReplicationRepeat the nearest edge value outward1 1 1 2 3 3 3ReplicationPad2d, padding_mode='replicate'
ConstantFill the border with one chosen value vv v v v v v vConstantPad2d(padding, value)
Padding modes on the slide 16 grid (row 0 of the 7 x 7 result)

The index rule behind the visual is short. For a padded coordinate j in 0 to 6, subtract p to get an offset into the original image. Replication clamps that offset into 0 to 2. Reflection folds it: -1 becomes 1, -2 becomes 2, 3 becomes 1 and 4 becomes 0. Fold both the row and the column offset and you have the source pixel. The PyTorch documentation for ReflectionPad2d walks through exactly this example with the values 0 to 8. PyTorch also offers a 'circular' mode that wraps around to the opposite edge; the slide leaves it out.

Recall

Input 5 x 5, kernel 3 x 3, stride 1. What padding keeps the output 5 x 5, and how many parameters does that padding add?

p = 1, because 5 + 2 - 3 + 1 = 5. It adds zero parameters. The filter still has c_i x 3 x 3 weights.

One output pixel of a 3 x 3 Convolution layer is a weighted sum of a 3 x 3 patch of its input. That patch is its Receptive field. Now feed that layer's output into a second 3 x 3 layer. One pixel of the second layer reads a 3 x 3 block of first-layer outputs, and each of those was itself computed from a 3 x 3 patch. The patches of neighbouring pixels overlap and slide by one cell, so the union of all nine is a 5 x 5 region of the original image. A third layer makes it 7 x 7.

Read the slide 17 figure from right to left: the single output cell traces back to 3 x 3, then 5 x 5, then 7 x 7 of the input.

Each stride-1 layer adds k - 1 to the side of the region, because the outermost cells of the new window reach (k - 1)/2 further in each direction. Start from a single pixel and add k - 1 once per layer.

RFL=L(k1)+1(every stride=1)RF_L = L\,(k - 1) + 1 \qquad (\text{every stride} = 1)
Receptive field after L stride-1 layers of size k

With k = 3 this gives 3, 5, 7, 9, 11 for L = 1 to 5. The deep learning book's figure 9.4 makes the same point and adds the hint that matters next: the effect increases if the network includes strided convolution or pooling.

The problem with large images

A classifier must eventually let one output depend on the whole picture. For a 224 x 224 image and stride-1 3 x 3 layers, solve 2L + 1 >= 224: L = 112 layers before a single output pixel sees everything. Each of those layers works at full resolution, so the activation memory and the multiply-accumulates are enormous. The slide states the problem and the cure in one breath: downsample inside the network.

Stride is the cheapest way to do it. Instead of moving the kernel one cell at a time, move it s cells. The number of window positions drops by about a factor of s in each direction, and so does the output size. Goodfellow, Bengio and Courville describe stride s as mathematically equivalent to a stride-1 convolution followed by keeping every s-th output, only without wasting work on the outputs you would throw away.

ho=hi+2pkhs+1h_o = \left\lfloor \frac{h_i + 2p - k_h}{s} \right\rfloor + 1
Output height with padding p and stride s

The floor is not decoration. The padded input has h_i + 2p - k_h rows on which a window can start after the first, and the kernel visits every s-th of them. If that span is not a multiple of s, the last window would run past the edge and is dropped. Try h_i = 8, k = 3, p = 0, s = 2: the slide's version gives 3.5, which is not a size; the true answer is floor(5/2) + 1 = 3. Dumoulin and Visin state the relationship with the floor, and the PyTorch Conv2d documentation computes the output shape the same way.

SimulatorConv2D shape and cost calculator
Output size5 x 5x 8floor((h_i + 2p - k)/s) + 1
Weights216valuesc_o x (c_i/g) x k x k, 864 B at fp32
Parameters224valuesweights + 8 biases
Multiply-accumulates5,400MACsweights x h_o x w_o, one image
Per output channel3input channels readevery filter reads all of c_i
Uncovered edge0 + 0rows + colspadded input the floor never reads

Change p or s and watch the output size and the MACs move while the weight count stays still. Only c_i, c_o, k and g touch the parameter count.

How stride changes the receptive field rule

Stride does more than shrink maps. Once a layer has stride 2, two neighbouring outputs of that layer sit two input pixels apart, so every layer after it takes steps of two in input coordinates. Its 3 x 3 window no longer adds 2 pixels to the receptive field; it adds 2 x 2 = 4. The slide 18 top row shows the payoff: a stride-2 first layer followed by one stride-1 layer already reaches 7, where stride 1 needed three layers.

Stride 2 in the first layer: the three dashed windows start two cells apart, so two 3 x 3 layers already cover 7 x 7 of the input.

Araujo, Norris and Sim give the general closed form. Call the product of the strides before layer l the jump (some authors say effective stride). Each layer adds (k_l - 1) times its jump.

RFL=1+l=1L(kl1)i=1l1siRF_L = 1 + \sum_{l=1}^{L} (k_l - 1) \prod_{i=1}^{l-1} s_i
Receptive field with arbitrary kernels and strides (Araujo, Norris and Sim, 2019)

Worked example

Slide 18, top row, and one step further

  1. Layer 1: k = 3, s = 2

    Jump before it is 1, so RF = 1 + 2 x 1 = 3. Jump after it is 2.
  2. Layer 2: k = 3, s = 1

    RF = 3 + 2 x 2 = 7. Two layers reach what three stride-1 layers reached.
  3. Add a third stride-1 layer

    Jump is still 2: RF = 7 + 2 x 2 = 11.
  4. Or make layers 1 and 2 both stride 2

    Jumps 1, 2, 4: RF = 1 + 2 + 4 + 8 = 15 after three 3 x 3 layers.
  5. The shortcut only holds for stride 1

    L(k - 1) + 1 would predict 7 for all three-layer cases. The general formula gives 7, 11 or 15 depending on where the strides sit.
LayersAll stride 1First layer stride 2Every layer stride 2
1333
2577
371115
491531
Receptive field after L layers of 3 x 3
SimulatorReceptive field stacker
layerkernelstrideRFjump
1
31
2
72
3
112
General receptive field11pixelsRF_l = RF_(l-1) + (k - 1) x jump
Stride-1 shortcut7pixelssum of (k - 1) plus 1: wrong here because a stride before the last layer exceeds 1

The jump column is the jump before each layer: the product of the strides of the layers above it. The layer adds (k - 1) times that jump. A stride of 2 in layer 1 doubles how much every later layer adds, which is why the general value pulls away from L(k - 1) + 1.

Real networks live at the strided end of that table. Araujo, Norris and Sim compute receptive fields of 195 for AlexNet, 212 for VGG-16, 483 for ResNet-50 and 1,311 for Inception-v3, all with a total downsampling of 32, and remark that the receptive field usually covers the entire input image. The stride in the ResNet stem you computed in the calculator is the first of those doublings: 224 to 112 in one layer.

Quick check

A 32 x 32 input passes through a 5 x 5 kernel with padding 2 and stride 2. What is the output height?

Quick check

Three 3 x 3 convolutions have strides 2, 1 and 1 in that order. What is the receptive field of one output pixel?

Recall

Write the output size formula with stride and say why the floor is needed.

h_o = floor((h_i + 2p - k_h)/s) + 1. When the span h_i + 2p - k_h is not a multiple of s, the last window would overrun the edge and is dropped: 8 rows, k = 3, s = 2 gives 3, not 3.5.

Recall

Three 3 x 3 layers with strides 2, 2, 1: what is the receptive field?

Jumps 1, 2, 4, so 1 + 2 x 1 + 2 x 2 + 2 x 4 = 15.

Padding and stride change the shape of the computation. The last two concepts change its wiring. Take a layer with c_i = c_o = 64 channels and 3 x 3 kernels. Each of the 64 filters reads all 64 input channels, so the layer holds 64 x 64 x 9 = 36,864 weights. Now split the channels into two halves and run two separate convolutions, each mapping 32 inputs to 32 outputs. Each half holds 32 x 32 x 9 = 9,216 weights, so the pair holds 18,432: the same output shape at half the cost.

That is a Grouped convolution with g = 2, or in the slide's words a group of narrower convolutions. In general, split c_i into g blocks of c_i/g channels and c_o into g blocks of c_o/g. Output block j reads only input block j. The slide writes the new Weight shape as (g · c_o/g, c_i/g, k_h, k_w), and the first factor simplifies to c_o: there are still c_o filters, each just reads c_i/g channels instead of all c_i. The Bias stays one per output channel.

W:(co,  cig,  kh,  kw),#weights=cocigkhkw,MACs=#weightshowo\mathbf{W} : \left(c_o,\; \tfrac{c_i}{g},\; k_h,\; k_w\right), \qquad \#\text{weights} = c_o \cdot \tfrac{c_i}{g} \cdot k_h k_w, \qquad \text{MACs} = \#\text{weights} \cdot h_o w_o
Grouped convolution: g times fewer weights and MACs, same output shape
Slide 19 redrawn: with g = 2 the full block of crossing lines splits into two half-width blocks, and the faint grey lines are the connections that no longer exist.
Groupsc_o x (c_i/g) x k x kWeightsSaving
g = 164 x 64 x 936,8641x
g = 264 x 32 x 918,4322x
g = 464 x 16 x 99,2164x
g = 864 x 8 x 94,6088x
g = 6464 x 1 x 957664x
c_i = c_o = 64, k = 3, weights only (add 64 biases to every row)
SimulatorGrouped to depthwise, 64 channels in and out

Each drawn lane stands for 8 real channels. 32 of 64 lane pairs stay connected.

0full bar = standard 36,864 weights
Grouped 3 x 318,432weights64 x (64/2) x 9, 32 channels per group
Pointwise 1 x 10weightsoff, no cross-channel mixing
Weights vs standard2.0xfewer18,432 of 36,864 weights

The divisibility rule

The split only works if both channel counts divide evenly. PyTorch says so directly: in_channels and out_channels must both be divisible by groups. With c_i = 64 and c_o = 96, g = 4 is fine (16 inputs and 24 outputs per group) but g = 5 is not, because 64/5 is not an integer. Output channel j belongs to group floor(j / (c_o/g)) and reads only the input channels of that group. The calculator earlier in this part flags any pair that fails the rule.

The idea is older than its name. Krizhevsky, Sutskever and Hinton trained AlexNet on two GTX 580 GPUs with 3 GB each, so they put half of the kernels on each GPU and let the kernels of layer 4 take input only from the kernel maps of layer 3 that lived on the same GPU. That is g = 2, chosen for memory rather than elegance. ResNeXt (Xie et al., 2017) turned the group count into a design axis they named cardinality and found that raising it was more effective than going deeper or wider at the same parameter budget.

That last clause is the cost. Inside a grouped layer, no information crosses a group boundary; the faint grey lines in the visual are exactly the missing connections. Networks that use grouped layers alternate them with something that mixes channels again, most often a 1 x 1 convolution. The next concept pushes g to its limit and meets that need head on.

Quick check

A layer has c_i = c_o = 128, k = 3, g = 8 and no bias. How many weights does it hold?

Recall

c_i = c_o = 64, k = 3. How many weights for g = 1, g = 4 and depthwise (g = 64)?

36,864; 9,216; 576. Each is 64 x (64/g) x 9.

Push the group count as far as it goes: g = c_i = c_o. Every group is now one Channel in and one channel out, so each channel gets its own k x k filter and nothing else. For 64 channels and 3 x 3 kernels that is 64 x 9 = 576 weights, 64 times fewer than the 36,864 of the standard layer. The slide 20 diagram says it in one picture: eight straight arrows from input strip to output strip, and not one of them crosses.

This is a Depthwise convolution. The slide writes its Weight tensor as (c, k_h, k_w), one k_h x k_w filter per channel. Frameworks keep the four-dimensional layout: PyTorch stores (out_channels, in_channels/groups, kH, kW), which with groups = in_channels is (c, 1, k_h, k_w), the same numbers with a singleton axis. PyTorch also allows out_channels = K · in_channels with groups = in_channels, a depthwise layer with depth multiplier K; the slide's g = c_i = c_o is the case K = 1.

g=ci=co=c:W:(c,1,kh,kw),#weights=ckhkwg = c_i = c_o = c: \quad \mathbf{W} : (c, 1, k_h, k_w), \qquad \#\text{weights} = c \cdot k_h k_w
Depthwise convolution: one filter per channel

Is this reduction in the number of weights really good?

The slide asks the question in red and leaves it open. The honest answer has two halves. For storage and arithmetic, yes: 576 weights fit in 2.3 KB of fp32 flash, and the multiply-accumulates fall by the same factor of 64. For what the layer can learn, no. A depthwise layer filters each channel in isolation. Sandler et al. describe it as lightweight filtering that applies a single convolutional filter per input channel, and Howard et al. point out that it does not combine input channels, so it cannot build a feature that depends on two channels at once. A standard layer does that at every position; a depthwise layer never does.

The repair is a 1 x 1 convolution, called a pointwise convolution, placed right after it. With k = 1 it looks at one position and mixes all c_i channels into c_o outputs: W of shape (c_o, c_i, 1, 1), c_o · c_i weights, and in Sandler et al.'s words it is responsible for building new features through computing linear combinations of the input channels. The depthwise layer handles space, the pointwise layer handles channels, and the pair is called a depthwise separable convolution. It is the building block of MobileNet and, wrapped in an expand and project pair of 1 x 1 layers, of MobileNetV2.

Phase one: eight straight lanes, one 3 x 3 filter each, no crossing. Phase two: the 1 x 1 pointwise fan reconnects every channel to every output.
LayerWeightsBiasesWeights + biasesFewer weights than standard
Standard 3 x 3 (g = 1)36,8646436,9281x
Grouped, g = 218,4326418,4962x
Grouped, g = 49,216649,2804x
Depthwise, g = 645766464064x
Depthwise + 1 x 1 pointwise576 + 4,096 = 4,6721284,8007.9x
c_i = c_o = 64, k = 3, one layer versus the alternatives

The last row is the one to remember. Howard et al. derive the cost ratio of a separable block to a standard layer as 1/N + 1/D_K², where N is the output channel count and D_K the kernel size. With N = 64 and D_K = 3 that is 1/64 + 1/9 ≈ 0.127, so the block is about 7.9 times cheaper, matching the table. For wide layers the 1/N term fades and the saving approaches D_K² = 9; Howard et al. report 8 to 9 times less computation for 3 x 3 kernels at a small accuracy cost.

Multiply-accumulates on a 56 x 56 map (h_o · w_o = 3,136)

Standard 3 x 3, 64 to 64
36,864 x 3,136 = 115,605,504 (about 115.6 M)
Depthwise 3 x 3 plus 1 x 1 pointwise
4,672 x 3,136 = 14,651,392 (about 14.7 M)
Ratio
about 7.9x

What this means on an embedded device

  • Fewer weights means less flash and less weight traffic from memory, which on a microcontroller is often the whole budget.
  • Fewer multiply-accumulates means lower latency and lower energy per inference on the same core.
  • The depthwise layer does only multiply-accumulates per output value, against c_i · k² for a standard layer, yet still reads a k x k window per output, so it moves a lot of activation data for little arithmetic and on many accelerators is bound by memory bandwidth rather than compute. Treat the parameter saving as an upper bound on the speedup, not a promise.
  • Accuracy drops a little relative to a standard layer of the same shape, which is the price of removing the cross-channel terms.

Quick check

Why does MobileNet follow each depthwise convolution with a 1 x 1 pointwise convolution?

Recall

Why is a depthwise convolution alone a weak layer on an embedded device, and what fixes it?

It never mixes information across channels, so it cannot form features that combine channels. A 1 x 1 pointwise convolution after it mixes the channels at c_o · c_i weights, giving the depthwise separable block that MobileNets use.

Recall

c_i = c_o = 64, k = 3. How many weights does a depthwise 3 x 3 plus 1 x 1 pointwise block hold, and how does it compare with the standard layer?

64 x 9 = 576 depthwise plus 64 x 64 = 4,096 pointwise gives 4,672, about 7.9 times fewer than the 36,864 of the standard layer.

Recap

If you remember nothing else

  • h_o = floor((h_i + 2p - k_h)/s) + 1. With s = 1 and p = (k - 1)/2 the output keeps the input size.
  • Zero, reflection, replication and constant padding change only the border cells. W is unchanged.
  • One 3 x 3 layer sees 3 x 3. Each stride-1 layer adds k - 1, so L layers give L(k - 1) + 1.
  • With stride, RF_L = RF_(L-1) + (k - 1) times the product of the earlier strides. Stride 2 in layer 1 gives 7 after two 3 x 3 layers.
  • Stride and padding change activations and MACs, never the parameter count.
  • Grouped convolution: W is (c_o, c_i/g, k_h, k_w), g times fewer weights, and c_i and c_o must both be divisible by g.
  • Depthwise convolution is g = c_i = c_o: one k x k filter per channel and no mixing across channels.
  • 64 to 64 channels, 3 x 3: 36,864 weights standard, 576 depthwise, 4,672 depthwise plus 1 x 1, about 7.9x fewer.
  • The 1 x 1 pointwise convolution restores cross-channel mixing. MobileNets are built on this pair.

Sources