Module 7 of 12
Chapter 06 • Deep Architectures

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:

W_out = ⌊ (W_in - K + 2*P) / S ⌋ + 1
LayerConfigurationInput ShapeOutput 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])
Next Chapter

Sequential Models & LSTMs

Continue to the next hands-on deep learning guide in this PyTorch series.

Continue Learning →

Explore Related Tools

Hand-picked utilities and calculators related to this tool.

Related Tools