← All tutorials

On this page

1 Receptive field 2 Dilated convolution 3 Causal convolutions and TCNs 4 Receptive field problems and fixes 5 References

Receptive Fields, Dilation and Causal Convolutions

Every output of a CNN sees only a limited window of its input, its receptive field. Stacking layers and striding widen that window, spreading a kernel’s taps apart (dilation) widens it with no extra weights, and for a live signal the window must reach only into the past.

This page builds on CNN Basics: Convolution, Stride and Pooling, which slides a kernel over the input with a stride and keeps one value per patch with pooling.

1 measurethe window2 widen itwith dilation3 make itcausal
Figure 1. Measure the window, widen it, then make it look only into the past.

1 Receptive field

A neuron's receptive field (RF) is the patch of input pixels that can change its value; this page also calls it the window. Early layers see edges; deep layers can see whole objects, but only if the window is large enough. Section 1.3 and Section 1.5 use two well-known image networks. VGG-16 is a deep stack of 3×3 convolutions, 13 of them in blocks of 2, 2, 3, 3 and 3 with pooling between blocks (VGG, in Classic CNN Architectures). ResNet-50 is 16 bottleneck blocks with shortcuts, and each block’s two 1×1 layers mix channels without widening the window (ResNet, in Classic CNN Architectures).

One rule, applied layer by layer from the input up, gives the window of any network. It tracks two numbers: RF, and the jump J, how far apart two neighboring outputs sit, in input pixels. Both start at 1. For a layer with kernel k and stride s (the dilation d, the spacing between a kernel's taps, is 1 until Section 2):

How much each layer widens the window one output sees, applied once per layer from the input up.
RFnew = RFold + growth
Each layer adds its growth to the window RF and never shrinks it, so the window can only widen with depth. Layers with a larger growth widen it faster.
growth = (k − 1) · d · Jold
A kernel's first and last taps sit k − 1 gaps apart. Each gap skips d outputs of the layer below, and neighboring outputs below sit Jold input pixels apart, so the product is how many extra input pixels the kernel reaches. A 1×1 kernel has no gap and adds nothing; the same kernel adds more after a stride, because Jold is larger there.
Jnew = Jold · s
The jump J changes only at a stride s: keeping one output in s puts neighbors s times farther apart in the input. Because J multiplies every later growth, an early stride makes every later layer widen the window faster. Padding changes neither number; same padding only keeps the map from shrinking, so more layers can stack.
input layer 1: RF = 3 layer 2: RF = 5 layer 3: RF = 7
Figure 2. Each stacked 3×3 layer widens the window by 2: 3, 5, then 7.

1.1 Stacking small kernels

Two stacked 3×3 layers have a receptive field of 5, the same as one 5×5 layer, with 18 weights per channel pair instead of 25 and an extra ReLU between them.

one 5×525 weightsReLUtwo 3×3s18 weights
Figure 3. Same 5-wide reach (one row shown); the stack needs fewer weights.

1.2 Stride and the jump

A pool with stride 2 barely widens the window itself, but it doubles J, so every later layer grows twice as fast. In the first block of a typical CIFAR model (CIFAR is a set of 32×32 color photos), the 3×3 after the pool adds twice what the same 3×3 added before it (Figure 4).

window RFJconv 3×3+231conv 3×3+251pool, stride 2+162conv 3×3+4102before the layeradded by the layer
Figure 4. A CIFAR block; after the stride-2 pool the last 3×3 adds 4, not 2.

1.3 Receptive fields of real networks

Pools and strides keep multiplying J, so real networks reach hundreds of pixels.

NetworkLayers that grow the windowRF
LeNet-styleconv 5×5, pool 2, conv 5×5, pool 216
VGG-1613 convs 3×3 in blocks of 2, 2, 3, 3, 3, a stride-2 pool after each block212
ResNet-50stem 7×7 stride 2, pool 3×3 stride 2, 16 blocks each with one 3×3 (stride 2 at three stage starts)427

ResNet-50's number depends on where each stage puts its stride. With the stride on the 3×3, as in torchvision, the recurrence gives 427; other layouts give 483, the value Araujo et al. (2019) report.

Figure 5. The ▲ marks the window’s reach after each layer.

1.4 Effective receptive field

Pixels near the window's center reach the neuron through many paths and edge pixels through few, so influence falls off like a bell (Luo et al. 2016). A rough model treats influence as a bell over the distance from the center.

An illustrative bell of influence inside the receptive field.
influence = exp(−r22σ2)
Influence is 1 at the window's center and falls toward 0 with the distance r from the center. Squaring r keeps the drop slow near the center and makes it steep further out, and the width σ (sigma) sets the scale: doubling σ lets pixels twice as far out keep the same influence.
σ = RF5
The width σ is tied to the window RF, so a larger window has a wider bell. One fifth is this page's choice, not a measured value: the effective receptive field paper shows the bell shape, and the real width depends on the network and its training. With this width the bell stays above half its peak over the middle 47% of the window; that central part is the effective receptive field, the pixels that actually move the output.

1.5 Receptive field and object size

Whether a network sees an object whole comes down to one comparison: the object's width against the effective receptive field, not only against the whole receptive field. The function below makes that check for an object obj pixels wide, against the windows of LeNet (16), VGG-16 (212) and ResNet-50 (427):

# share of RF above half the peak
ERF = 0.471

def verdict(rf, obj):
    # rf, obj: widths in pixels
    if obj <= ERF * rf:
        return "fully seen"
    if obj <= rf:
        return "partly seen"
    return "not covered"

for rf in (16, 212, 427):
    print(verdict(rf, obj=150))
  1. 1The share of the receptive field inside the effective receptive field, the shaded band in Figure 6.
  2. 2An object inside that band is fully seen.
  3. 3An object wider than the band but inside the receptive field is partly seen; a wider one is not covered.
  4. 4Checks a 150 pixel object against the three windows.
effective receptive field object window

Figure 6. The verdict flips as orange outgrows the band, then the window.

Overlapping windows matter in self-supervised learning too (SSL failure modes).


2 Dilated convolution

Spread the kernel's taps apart, raising the dilation d, and one kernel covers more input with the same weights (Yu and Koltun 2016):

How wide a dilated kernel reaches.
span = d · (k − 1) + 1
A dilated kernel still has k taps and k − 1 gaps, but each gap now steps d inputs instead of 1, so the reach is d times the gaps, and the + 1 counts the first tap. Raising d widens the span with the same k weights: a 3-tap kernel spans 5 inputs at d = 2 and 9 at d = 4. d = 1 is an ordinary kernel and PyTorch’s default; a very large d leaves wide holes between taps and, near the border, reads mostly padding. In the receptive field recurrence its growth is (span − 1) · J.
012345678 taptaptap
Figure 7. With dilation 4, the 3 blue taps span 9 inputs with the same 3 weights.

Stacking the same dilation repeats the same holes, so the top output never sees some inputs (the gridding problem, Wang et al. 2018). Mixing rates such as 1, 2, 4 fills them for a 3-tap kernel. A stride before dilated layers spreads the taps further and makes the holes worse, so a plain convolution every few layers restores full coverage.

rates 2, 2, 2 see 7 of 13 inputsrates 1, 2, 4 see all 15rates 4, 4 see 5 of 17
Figure 8. Dark inputs reach the top output; grey ones are holes it never sees.

A dilated first layer skips raw samples before anything has summarized them, so dilation usually starts after a few plain layers. Running several dilated convolutions side by side at different rates and joining their outputs sees fine and coarse context at once; DeepLab calls this Atrous Spatial Pyramid Pooling, or ASPP (Chen et al. 2018, rates 6, 12, 18 and 24; atrous means dilated). The ASPP-style block below runs B branches on a map x with C channels. The positional arguments of Conv2d are in, out, kernel, stride, padding and dilation. Each branch costs as much as a plain 3×3, so the block runs about B times the compute of one layer.

import torch
from torch.nn import Conv2d
from torch.nn import ModuleList

C, rates = 64, (1, 2, 4)
branches = ModuleList()
for d in rates:
    # padding d keeps the size
    b = Conv2d(C, C, 3, 1, d, d)
    branches.append(b)
B = len(rates)
fuse = Conv2d(B * C, C, 1)

x = torch.randn(1, C, 32, 32)
outs = [b(x) for b in branches]
y = fuse(torch.cat(outs, dim=1))
# y: [1, C, 32, 32]
  1. 1One 3×3 branch per rate d, padded by d so the map keeps its size.
  2. 2A 1×1 that fuses the B · C joined channels back to C; a 1×1 kernel mixes channels only (1×1 convolution, in Classic CNN Architectures).
  3. 3Runs every branch on x, joins their channels and fuses them.

2.1 A receptive field for 300 ms of sensor data

At 1,000 samples per second a 300 ms event spans 300 samples, so the last layer’s window must reach at least 300. The function below runs the recurrence of Section 1 over (k, s, d) tuples, with rf and jump as RF and J, and scores the seven-layer design of Figure 9 and a copy whose last dilation doubles once more:

def receptive_field(layers):
    rf, jump = 1, 1  # RF and J
    for k, s, d in layers:
        rf += (k - 1) * d * jump
        jump *= s
    return rf

plain = [(7, 1, 1), (5, 2, 1)]
dils = (2, 4, 8, 16, 16)
stack = [(5, 1, d) for d in dils]
design = plain + stack
print(receptive_field(design))
# 379
wider = design[:-1] + [(5, 1, 32)]
print(receptive_field(wider))
# 507
  1. 1The receptive field recurrence: each layer adds (k − 1) · d · J, and J multiplies by s.
  2. 2The design: a kernel-7 layer, a stride-2 layer, then five dilated layers.
  3. 3The window reaches 379 samples, past the 300 target, the last point of Figure 9.
  4. 4Raising the last rate to 32 reaches 507.
0100200300400500RF in samples① 300 target7L1111L2127L3259L44123L58251L616L716② 379③ 507d
Figure 9. ① target; ② the design ends at 379; ③ last dilation 32 reaches 507.

3 Causal convolutions and TCNs

When the 300 ms detector of Section 2.1 runs live, at each new sample it must decide using only past samples, but a kernel with centered padding reads half its reach from the future. A Conv1D slides a kernel along one axis, time. In a causal task the output at time t cannot use later inputs, so the padding goes on the left only:

Causal padding puts zeros on the left only, enough for the kernel's reach into the past.
left pad = (k − 1) · d
A kernel with k taps and dilation d reaches (k − 1) · d steps into the past: k − 1 gaps of d steps each. Padding exactly that many zeros on the left, and none on the right, lines the newest tap up with the current time step, so each prediction depends only on data up to its own time. Centered padding would put half the reach into the future.

A causal convolution in PyTorch is a plain nn.Conv1d that pads (k − 1) · d zeros on the left, so the output keeps the input's length and never sees a later step.

import torch
import torch.nn as nn
import torch.nn.functional as F

class CausalConv1d(nn.Conv1d):
    """Looks back only"""
    def forward(self, x):
        # x: [B, C, T]
        k = self.kernel_size[0]
        d = self.dilation[0]
        pad = (k - 1) * d
        # pad the past side only
        x = F.pad(x, (pad, 0))
        return super().forward(x)
  1. 1Computes the left padding (k − 1) · d of the card.
  2. 2Pads zeros on the past side of the time axis only.
  3. 3Runs the ordinary convolution on the padded input.

A model trained with ordinary centered padding peeks at the future and scores well offline, then fails live. Doubling the dilation layer by layer grows the window exponentially, as in Section 2. A temporal convolutional network (TCN) stacks such layers:

Receptive field of a causal stack whose dilation doubles at each of L levels.
RF = 1 + (k − 1)(2L − 1)
With one causal convolution per level, level l has dilation 2l and adds (k − 1) · 2l steps. The dilations 1, 2, 4 and so on add up to 2L − 1, so each extra level about doubles the window while adding only k weights per channel pair. With k = 3 and L = 8 one output sees 511 steps. A TCN block from the TCN paper (Bai et al. 2018) has two convolutions per level, which doubles the growth to 1,021 steps.
inputdilation 1dilation 2dilation 4nowpast
Figure 10. Kernel 3, dilation doubling per layer; the newest output sees only the past.

If the dilation grows too fast, some time steps fall into holes the output never sees, the gridding problem (Figure 8); repeating a cycle such as 1, 2, 4, 1, 2, 4 avoids it. Deep stacks also need a shortcut so gradients reach the early layers: each level adds its input back to its output, the residual shortcut ResNet introduced (ResNet, in Classic CNN Architectures). One TCN level in PyTorch takes a ready-built CausalConv1d with dilation d. Dropout zeroes each value with probability p while training and keeps every value at test time (Regularization). Raising p regularizes harder, p = 0 turns it off and near 1 almost nothing survives; the TCN code uses 0.2. The authors’ code is at github.com/locuslab/TCN; stacking levels with d = 1, 2, 4 and so on gives the card's window:

class TCNLevel(nn.Module):
    """y = relu(x + branch)"""
    def __init__(self, conv):
        super().__init__()
        self.conv = conv
        drop = nn.Dropout(0.2)
        self.drop = drop

    def forward(self, x):
        # x and y: [B, c, T]
        h = self.conv(x)
        h = torch.relu(h)
        h = self.drop(h)  # branch
        return torch.relu(x + h)

c, k, d = 16, 3, 4
kw = dict(dilation=d)
conv = CausalConv1d(c, c, k, **kw)
level = TCNLevel(conv)
  1. 1The branch, made of the causal convolution, ReLU and dropout.
  2. 2Adds the input back, then ReLU.
  3. 3Builds one level with dilation 4.

For multi-sensor data such as EEG (recordings of brain waves), treat the sensors as the height of a sensors-by-time grid and apply a Conv2D whose kernel spans all sensors and a time window; for live use the causal padding then goes on the left of the time axis only. The Conformer mixes convolutions with attention for speech (The Conformer Block).

A convolution sees a fixed window. Recurrent networks (RNN Basics) carry a running state through time, and attention (Attention) lets every position see every other one in a single layer.


4 Receptive field problems and fixes

Most of these failures come from a receptive field that is smaller than the object, an effective receptive field smaller than the object, dilation gaps that skip inputs, or padding that lets a live model see the future.

SymptomLikely causeFix
Large objects are missedReceptive field smaller than the objectPool or stride earlier, or add dilation
Large objects are detected weakly or inconsistentlyObject extends past the effective receptive fieldKeep objects under half the receptive field
Fine detail vanishes after dilated layersDilated taps skip inputs (gridding)Mix rates such as 1, 2, 4
Offline scores beat live onesCentered padding saw the futureCausal, left-only padding

Which networks stacked these layers and won ImageNet? Classic CNN Architectures: AlexNet to ResNet answers that.


5 References

  1. Luo, W., et al. (2016). Understanding the Effective Receptive Field in Deep Convolutional Neural Networks. NeurIPS. arXiv:1701.04128.
  2. Araujo, A., et al. (2019). Computing Receptive Fields of Convolutional Neural Networks. Distill.
  3. Yu, F., and Koltun, V. (2016). Multi-Scale Context Aggregation by Dilated Convolutions. ICLR.
  4. Wang, P., et al. (2018). Understanding Convolution for Semantic Segmentation. WACV. arXiv:1702.08502.
  5. Chen, L.-C., et al. (2018). DeepLab: Semantic Image Segmentation with Deep Convolutional Nets, Atrous Convolution, and Fully Connected CRFs. IEEE TPAMI. arXiv:1606.00915.
  6. Bai, S., et al. (2018). An Empirical Evaluation of Generic Convolutional and Recurrent Networks for Sequence Modeling. arXiv:1803.01271.