C.W.K.
Stream
Lesson 06 of 08 · published

Convolutional Layers — Conv2d and Friends

~14 min · conv2d, cnn, channels, padding

Level 0Tensor Curious
0 XP0/62 lessons0/13 achievements
0/120 XP to next level120 XP to go0% complete

Conv layers, in PyTorch's NCHW universe

PyTorch represents image data as (N, C, H, W): batch, channels, height, width. (The other major convention is NHWC, used by TensorFlow and CoreML — be ready to permute when you cross frameworks.)

nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0) learns out_channels filters of shape (kernel_size, kernel_size, in_channels), slides each over the input, and produces out_channels feature maps. The output spatial size follows the standard formula: out = (in + 2*padding - kernel) / stride + 1.

The variants you'll actually use

  • Conv2d — 2D conv. Default for images.
  • Conv1d — 1D conv. Useful for sequences (audio waveforms, character-level text, time series).
  • ConvTranspose2d — "deconvolution" for upsampling. The decoder of a U-Net, the generator of a DCGAN.
  • Depthwise separable convs — built by combining Conv2d(groups=in_channels) + Conv2d(1x1). The trick that makes MobileNet / EfficientNet efficient.

The padding shorthand

PyTorch added padding='same' as a string in 1.10+, which keeps spatial dims unchanged for stride=1. It's the convenient version of "compute the right padding by hand" that everyone used to write. Use it when you don't have a specific reason to drop spatial dims.

Code

Conv2d basics·python
import torch
import torch.nn as nn

# 3 RGB channels in, 16 feature maps out, 3x3 kernel
conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3,
                 stride=1, padding=1)

x = torch.randn(8, 3, 32, 32)   # batch=8, RGB, 32x32 image
y = conv(x)
print(y.shape)                   # torch.Size([8, 16, 32, 32]) — same spatial
print(conv.weight.shape)         # torch.Size([16, 3, 3, 3])  — out, in, kH, kW
print(conv.bias.shape)           # torch.Size([16])

# stride=2 halves spatial dims
conv_down = nn.Conv2d(3, 16, 3, stride=2, padding=1)
print(conv_down(x).shape)        # torch.Size([8, 16, 16, 16])
A simple CNN — the canonical pattern·python
import torch
import torch.nn as nn

class SimpleCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(2),                 # 32x32 → 16x16

            nn.Conv2d(32, 64, 3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2),                 # 16x16 → 8x8

            nn.Conv2d(64, 128, 3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.AdaptiveAvgPool2d(1),         # global avg pool → 1x1
        )
        self.classifier = nn.Linear(128, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = x.flatten(1)                     # (B, 128, 1, 1) → (B, 128)
        return self.classifier(x)

model = SimpleCNN()
print(model(torch.randn(4, 3, 32, 32)).shape)   # torch.Size([4, 10])
Depthwise separable conv — the MobileNet trick·python
import torch.nn as nn

class DepthwiseSeparable(nn.Module):
    """Replace a regular conv with depthwise + pointwise — far fewer params."""
    def __init__(self, in_ch, out_ch, kernel=3):
        super().__init__()
        # Depthwise: each input channel gets its own kernel
        self.depthwise = nn.Conv2d(in_ch, in_ch, kernel,
                                    padding=kernel // 2, groups=in_ch)
        # Pointwise: 1x1 conv to mix channels
        self.pointwise = nn.Conv2d(in_ch, out_ch, kernel_size=1)

    def forward(self, x):
        return self.pointwise(self.depthwise(x))

# Compare param counts
regular = nn.Conv2d(64, 128, 3, padding=1)
sep = DepthwiseSeparable(64, 128, 3)

regular_params = sum(p.numel() for p in regular.parameters())
sep_params = sum(p.numel() for p in sep.parameters())
print(f"Regular: {regular_params:,}")    # 73,856
print(f"Separable: {sep_params:,}")       # 8,896 — about 8x fewer

External links

Exercise

Build a small ResNet-style block: Conv2d(64, 64, 3, padding=1) → BatchNorm → ReLU → Conv2d(64, 64, 3, padding=1) → BatchNorm, then add the input back (skip connection) before a final ReLU. Verify it preserves spatial dims and channel count.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.