PyTorch Conv2d Layer
nn.Conv2d is the core building block of image processing networks in PyTorch. It applies a set of learnable filters to an input image or feature map and produces a set of output feature maps. Understanding its parameters removes most of the confusion when debugging CNN shape errors.
Conv2d Signature
nn.Conv2d(
in_channels, # number of input channels
out_channels, # number of filters (= number of output channels)
kernel_size, # filter size: integer or (height, width) tuple
stride=1, # step size when sliding the filter
padding=0, # pixels added around the border before convolution
bias=True # include a learnable bias term
)Channels Explained
Grayscale image: in_channels = 1 (one intensity value per pixel) RGB color image: in_channels = 3 (red, green, blue per pixel) After a Conv2d with out_channels=32: the next layer sees 32 channels Input shape: [batch, in_channels, height, width] Output shape: [batch, out_channels, new_height, new_width]
How the Filter Slides
Input feature map (5×5, 1 channel): ┌───────────────┐ │ 1 2 3 4 5│ │ 6 7 8 9 0│ │ 1 2 3 4 5│ │ 6 7 8 9 0│ │ 1 2 3 4 5│ └───────────────┘ 3×3 filter slides with stride=1, no padding: Position (0,0): covers rows 0-2, cols 0-2 → one output value Position (0,1): covers rows 0-2, cols 1-3 → one output value ... Output size: (5-3)/1 + 1 = 3 → output is 3×3
Output Size Formula
output_size = floor((input_size + 2×padding - kernel_size) / stride) + 1
Example:
input_size = 28, kernel_size = 3, padding = 1, stride = 1
output = (28 + 2×1 - 3) / 1 + 1 = 28 ← same size as input ("same" padding)
input_size = 28, kernel_size = 3, padding = 0, stride = 1
output = (28 - 3) / 1 + 1 = 26 ← shrinks by 2
Common Conv2d Configurations
import torch.nn as nn
# Keep spatial size the same (padding = kernel_size // 2 for odd kernels)
nn.Conv2d(3, 64, kernel_size=3, padding=1) # 3→64 ch, size unchanged
# Halve spatial size
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1)
# Large receptive field
nn.Conv2d(3, 32, kernel_size=7, stride=2, padding=3) # used in ResNet first layer
# 1×1 convolution — mix channels, no spatial processing
nn.Conv2d(256, 64, kernel_size=1)
Multiple Filters — Multiple Output Channels
When out_channels=32, PyTorch learns 32 separate filters. Each filter slides across the entire input and produces one output feature map. The output has 32 channels — one per filter.
in_channels=3 (RGB), out_channels=32, kernel_size=3 Each of the 32 filters has shape: [3, 3, 3] ↑ depth (matches in_channels=3) ↑ height 3 ↑ width 3 Total parameters: 32 × (3×3×3) + 32 = 896 weights
Counting Conv2d Parameters
layer = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, bias=True)
# Weights: out_channels × in_channels × kH × kW
# Bias: out_channels
total = 32 * 3 * 3 * 3 + 32 # = 896
print(sum(p.numel() for p in layer.parameters())) # 896Padding Modes
padding=0 (default): No border added. Output shrinks. padding=1 (for 3×3 kernel): One row/column of zeros added on all sides. Output same size as input. padding='same' (PyTorch 1.9+, stride=1 only): Automatically computes padding to keep spatial size unchanged. padding_mode='reflect': Pads with reflected values instead of zeros. Reduces border artifacts in image generation tasks.
Stride — Controlling Step Size
Stride controls how many pixels the filter moves each step. Stride 1 means move one pixel at a time. Stride 2 means skip every other position — halving the output size. Using stride=2 instead of MaxPool2d is common in modern architectures.
import torch
import torch.nn as nn
x = torch.rand(1, 3, 32, 32) # 1 image, 3 channels, 32×32
conv_s1 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1)
conv_s2 = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1)
print(conv_s1(x).shape) # torch.Size([1, 16, 32, 32]) ← same size
print(conv_s2(x).shape) # torch.Size([1, 16, 16, 16]) ← halvedDilated Convolutions
Dilated convolutions insert gaps between filter elements. A 3×3 filter with dilation=2 covers the same area as a 5×5 filter but uses only 9 weights. This expands the receptive field without increasing parameters.
nn.Conv2d(32, 32, kernel_size=3, padding=2, dilation=2)
Full Conv Block Example
conv_block = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, padding=1), # 3→32 ch, same size
nn.BatchNorm2d(32),
nn.ReLU(),
nn.Conv2d(32, 32, kernel_size=3, padding=1), # 32→32 ch, same size
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2, 2) # halve spatial dims
)Summary
nn.Conv2d takes in_channels, out_channels, and kernel_size as its three key parameters. The filter slides across the input with a given stride. Padding controls whether the output is smaller, equal, or (with asymmetric padding) larger than the input. The output size formula is (input + 2×padding - kernel) / stride + 1. Use padding=kernel_size//2 to keep the spatial size the same with stride=1. Multiple out_channels mean multiple filters — each learning to detect a different pattern. Every CNN layer builds on the feature maps produced by the layer before it.
