Computer Vision & Convolutional Networks (CNNs)
Convolutional Neural Networks revolutionize computer vision through spatial weight sharing and translation equivariance. In PyTorch, vision pipelines adhere to the canonical [N, C, H, W] memory layout. In this chapter, master spatial dimensionality formulas, 2D convolutions, pooling layers, and modern Residual Connections (ResNet).
1. The PyTorch Image Tensor Convention: [N, C, H, W]
Unlike frameworks such as TensorFlow or OpenCV that store images as "channels-last" ([Height, Width, Channels]), PyTorch uses the "channels-first" standard:
N
Batch Size (e.g. 32)C
Channels (3 RGB / 1 Gray)H
Height (e.g. 224 px)W
Width (e.g. 224 px)import torch
# Converting a standard HWC image (NumPy / PIL) to PyTorch NCHW tensor:
# Standard NumPy image shape: (Height=224, Width=224, Channels=3)
import numpy as np
raw_image = np.random.randint(0, 256, size=(224, 224, 3), dtype=np.uint8)
# 1. Convert to float tensor and scale to [0.0, 1.0]
tensor_img = torch.from_numpy(raw_image).float() / 255.0
# 2. Permute dimensions: (H, W, C) -> (C, H, W)
tensor_img = tensor_img.permute(2, 0, 1)
# 3. Add batch dimension: (C, H, W) -> (1, C, H, W)
tensor_batch = tensor_img.unsqueeze(0)
print(f"PyTorch Batch Shape: {tensor_batch.shape}") # torch.Size([1, 3, 224, 224])2. Convolutional Math & Output Geometry
When passing a feature map through nn.Conv2d with input dimension W_in, kernel size K, padding P, and stride S, the output dimension W_out is calculated via:
| Layer | Configuration | Input Shape | Output Shape |
|---|---|---|---|
nn.Conv2d(3, 64, kernel_size=3, padding=1) | Stride 1, "Same" Padding | [B, 3, 224, 224] | [B, 64, 224, 224] (Spatial preserved) |
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1) | Stride 2 (Strided Downsampling) | [B, 64, 224, 224] | [B, 128, 112, 112] (Halved spatial) |
nn.MaxPool2d(kernel_size=2, stride=2) | 2x2 Pooling | [B, 128, 112, 112] | [B, 128, 56, 56] |
nn.AdaptiveAvgPool2d((1, 1)) | Global Average Pooling | [B, 512, 7, 7] | [B, 512, 1, 1] (Resolution independent) |
3. Building a Residual Block (ResNet Skip Connection)
In plain deep networks, stacking 20+ layers causes training degradation because gradients vanish as they backpropagate through many non-linear matrix products. He et al. (2015) introduced Residual Skip Connections:
Output = F(x) + x (where F(x) is the residual mapping)If the identity mapping is optimal, the optimizer easily drives weights toward zero without hurting representation.
import torch
import torch.nn as nn
import torch.nn.functional as F
class ResidualBlock(nn.Module):
"""
Standard ResNet basic residual block with 3x3 convolutions,
Batch Normalization, and an optional 1x1 projection shortcut.
"""
def __init__(self, in_channels: int, out_channels: int, stride: int = 1):
super().__init__()
# Conv 1: optionally downsamples spatial dimension
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
# Conv 2: keeps spatial dimension same
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# Shortcut connection
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
# Match channel and spatial dimensions using a 1x1 conv
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Save residual identity
residual = self.shortcut(x)
# Main branch
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
# Add skip connection
out += residual
return F.relu(out)4. Assembling a Modern ResNet Image Classifier
By combining a stem convolutional layer, stacked residual stages, and AdaptiveAvgPool2d, we construct a robust image classifier capable of processing arbitrary input resolutions:
class MiniResNet(nn.Module):
def __init__(self, num_classes: int = 10):
super().__init__()
# Stem: Initial 7x7 conv to extract low-level edges
self.stem = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True)
)
# Residual Stages
self.stage1 = ResidualBlock(32, 64, stride=2) # [B, 64, H/2, W/2]
self.stage2 = ResidualBlock(64, 128, stride=2) # [B, 128, H/4, W/4]
# Global Average Pooling collapses (H, W) -> (1, 1) regardless of image size
self.gap = nn.AdaptiveAvgPool2d((1, 1))
# Classification Head
self.classifier = nn.Linear(128, num_classes)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.stem(x)
x = self.stage1(x)
x = self.stage2(x)
x = self.gap(x)
x = torch.flatten(x, 1) # Flatten [B, 128, 1, 1] -> [B, 128]
logits = self.classifier(x)
return logits
# Verification
model = MiniResNet(num_classes=10)
dummy_batch = torch.randn(8, 3, 64, 64)
outputs = model(dummy_batch)
print(f"Output Logits Shape: {outputs.shape}") # torch.Size([8, 10])Sequential Models & LSTMs
Continue to the next hands-on deep learning guide in this PyTorch series.
Explore Related Tools
Hand-picked utilities and calculators related to this tool.
