Convolutional Neural Networks (CNNs): How Image Recognition Works
โก Quick Answer
CNNs learn to see by sharing weights across space. Here's the math behind convolution, pooling, and why ResNets can train 100+ layers without vanishing gradients.
Get more content like this on Telegram!
Daily AI tips, notes & resources โ free
Advertisement
Convolutional Neural Networks (CNNs): How Image Recognition Works
A convolutional neural network (CNN) is a neural network that hard-codes two assumptions about images directly into its architecture: nearby pixels are related, and the same visual pattern means the same thing no matter where it sits in the frame. Most explanations stop at "CNNs use sliding filters" and move on โ that skips the actual insight.
Think of a CNN as a detective who only needs to learn what a fingerprint looks like once, then can spot that same fingerprint anywhere on the page. A regular network would have to relearn it separately for every possible location.
Those two priors โ locality and translation invariance โ are why CNNs need far fewer parameters than a generic network and why they became the default architecture for vision.
Why Fully-Connected Networks Fail for Images
A fully-connected layer treats every pixel as unrelated to every other pixel, which throws away the one fact you know for certain about images: neighboring pixels belong together.
A 224ร224 RGB image has 224 ร 224 ร 3 = 150,528 input values. A single fully-connected hidden layer with 4,096 neurons would need 150,528 ร 4,096 โ 617 million parameters โ just for the first layer. AlexNet (Krizhevsky et al., 2012), the network that reset the field of computer vision, has only 60 million total parameters, and most of that budget went elsewhere.
Beyond the parameter count, fully-connected layers ignore spatial structure entirely. Every input pixel connects to every neuron with an independent weight. The network has no built-in way to notice that adjacent pixels form edges, or that an eye looks the same in the top-left of an image as in the bottom-right โ it has to learn each case from scratch, separately, at a staggering parameter cost.
Convolutional layers solve both problems at once.
The Convolution Operation
A convolution slides a small weight matrix โ the kernel, or filter โ across the input and computes a dot product at every position, the way a stamp presses the same design onto every page of a notebook. If the input is a 2D feature map I and the kernel is K of size kรk:
(I * K)[i, j] = ฮฃโ ฮฃโ I[i+m, j+n] ยท K[m, n]
The output is called a feature map or activation map. Each value answers one question: how strongly does this kernel's pattern appear at this location?
The key mechanism is weight sharing: the same kernel weights K are applied at every position. A filter that detects a horizontal edge uses the same nine numbers, for a 3ร3 kernel, whether it looks at position (0,0) or position (100,100). This reduces parameters from H ร W ร k ร k, the fully-connected cost, down to just k ร k.
A single convolutional layer applies multiple filters, each learning to detect a different pattern โ one for edges, one for curves, one for a particular texture. With 64 filters of size 3ร3ร3 applied to an RGB image:
Parameters = 64 ร (3 ร 3 ร 3) + 64 (bias) = 1,792
Compare that to the millions a fully-connected layer would need.
Convolution in PyTorch
import torch
import torch.nn as nn
import torch.nn.functional as F
# A single convolutional layer
conv = nn.Conv2d(
in_channels=3, # RGB input
out_channels=64, # 64 different filters
kernel_size=3, # 3x3 kernels
padding=1, # 'same' padding โ output same spatial size as input
stride=1
)
# Manual convolution to see the math
x = torch.randn(1, 3, 32, 32) # batch_size=1, channels=3, 32x32
output = conv(x)
print(f"Input shape: {x.shape}") # [1, 3, 32, 32]
print(f"Output shape: {output.shape}") # [1, 64, 32, 32]
# Visualizing what a learned filter looks like
# After training, conv.weight[0] is the first filter
# Shape: [3, 3, 3] โ 3 input channels, 3x3 spatial
Pooling: Downsampling with Purpose
Pooling is a layer that shrinks a feature map's spatial size by summarizing each local neighborhood into a single value โ the CNN equivalent of zooming out to see the bigger picture instead of every brick in the wall. Max pooling takes the maximum value in each local window:
pool = nn.MaxPool2d(kernel_size=2, stride=2)
# Halves spatial dimensions: [1, 64, 32, 32] โ [1, 64, 16, 16]
Max pooling wins over the alternatives for two reasons: it tolerates small shifts โ if a feature moves slightly within the pooling window, the maximum still fires โ and it cuts computation, since every later layer processes fewer spatial positions.
Global average pooling (GAP) averages across the entire spatial dimension, collapsing a feature map to a single value per channel. GAP replaced fully-connected layers in modern architectures, cutting millions of parameters to zero while improving generalization โ proof that fewer parameters can mean a better model, not a weaker one.
Building a CNN from Scratch
class ConvNet(nn.Module):
"""
A small CNN for CIFAR-10 (32x32 RGB, 10 classes).
Architecture: 3 conv blocks + global average pooling + classifier
"""
def __init__(self):
super().__init__()
# Block 1: 3 โ 64 channels, 32x32 โ 16x16
self.block1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2) # 32x32 โ 16x16
)
# Block 2: 64 โ 128 channels, 16x16 โ 8x8
self.block2 = nn.Sequential(
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2) # 16x16 โ 8x8
)
# Block 3: 128 โ 256 channels, 8x8 โ 4x4
self.block3 = nn.Sequential(
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2) # 8x8 โ 4x4
)
# Global average pooling: 4x4 โ 1x1
self.gap = nn.AdaptiveAvgPool2d(1)
# Classifier
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Dropout(0.5),
nn.Linear(256, 10)
)
def forward(self, x):
x = self.block1(x)
x = self.block2(x)
x = self.block3(x)
x = self.gap(x)
return self.classifier(x)
model = ConvNet()
print(f"Parameters: {sum(p.numel() for p in model.parameters()):,}")
# ~1.1M parameters โ lean but effective
Architecture Evolution
Each architecture introduced an idea that became standard practice:
- AlexNet popularized ReLU: it swapped tanh for ReLU activations, added dropout regularization, and proved GPU training could scale.
- VGGNet chose simplicity: deep stacks of plain 3ร3 convolutions beat hand-tuned exotic kernel shapes.
- GoogLeNet added multi-scale vision: Inception modules process the input at several receptive-field sizes simultaneously.
- ResNet solved depth: skip connections are the single most important CNN innovation, discussed next.
- EfficientNet scaled systematically: compound scaling grows width, depth, and resolution together instead of tuning one at a time.
Residual Networks: Solving the Depth Problem
A residual network (ResNet) is a CNN that adds a shortcut path around each block, letting the block learn only the correction to its input rather than the whole transformation from scratch.
Before ResNets, deeper networks were paradoxically worse than shallower ones โ not from overfitting, but from optimization difficulty. Gradients vanished as they backpropagated through many layers. A 56-layer network trained worse than a 20-layer network on the training set itself (He et al., 2016), which ruled out overfitting as the culprit.
The residual block solves this elegantly:
class ResidualBlock(nn.Module):
"""
Basic ResNet residual block (He et al., 2016).
F(x) + x: the block learns the residual, not the full transformation.
"""
def __init__(self, channels):
super().__init__()
self.conv1 = nn.Conv2d(channels, channels, 3, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(channels)
self.conv2 = nn.Conv2d(channels, channels, 3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(channels)
def forward(self, x):
identity = x # skip connection
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out)) # no activation yet
out = out + identity # add skip connection
out = F.relu(out) # activate after addition
return out
The skip connection does two things. At initialization, F(x) is near zero, so F(x) + x โ x โ the block starts as an identity mapping and only has to learn small deviations from it, which is far easier than learning a full transformation from scratch. During backprop, the gradient flows through two paths at once: through the residual branch F(x) and directly through the skip connection. Even if the residual branch's gradient vanishes, the skip connection keeps information flowing โ like a highway bypass that stays open even when the local streets are jammed.
ImageNet Benchmark Results
ImageNet is the standard benchmark for image classification: 1.2 million training images across 1,000 classes. Top-1 accuracy is the fraction of images where the model's top prediction is correct.
| Model | Year | Top-1 Accuracy | Parameters | GFLOPs |
|---|---|---|---|---|
| AlexNet | 2012 | 56.5% | 60M | 0.7 |
| VGG-16 | 2014 | 71.6% | 138M | 15.3 |
| GoogLeNet | 2014 | 69.8% | 6.8M | 1.4 |
| ResNet-50 | 2016 | 76.1% | 25.6M | 4.1 |
| ResNet-152 | 2016 | 77.8% | 60.2M | 11.3 |
| EfficientNet-B4 | 2019 | 82.9% | 19M | 4.2 |
| EfficientNet-B7 | 2019 | 84.3% | 66M | 37.0 |
| ViT-L/16 | 2021 | 87.1% | 307M | 190.7 |
| ConvNeXt-L | 2022 | 87.5% | 198M | 34.4 |
EfficientNet-B4 beats ResNet-152 on accuracy with a third of the parameters โ proof that architecture design matters as much as raw scale. The FLOP count matters for deployment specifically: a model that needs 37 GFLOPs cannot run in real time on a mobile device, no matter how accurate it is.
Receptive Field: How Much Does Each Neuron See?
The receptive field is the region of the original input that influences a single neuron's output โ and in a deep network it is much larger than that neuron's own kernel size suggests.
Think of it like standing progressively farther back from a mosaic: each step back lets your eye take in more tiles at once, even though your vision hasn't physically changed.
For a stack of n layers with kernel size k and no pooling:
Receptive field = 1 + n ร (k - 1)
With pooling (stride 2) every p layers:
Effective receptive field grows as ~2^(n/p)
This is why networks go deep: each layer compounds the previous one, letting final layers integrate information from the entire image while early layers stay focused on fine-grained local patterns.
After five layers of 3ร3 convolutions with 2ร2 max pooling every two layers, a neuron can "see" an area of roughly 64ร64 pixels โ far beyond the 3ร3 patch its own weights cover.
Data Augmentation: Making the Most of Limited Data
Data augmentation is the practice of generating modified copies of your training images โ flipped, cropped, recolored โ so a limited dataset behaves like a larger, more diverse one. CNNs are hungry for data, and augmentation is the cheapest way to feed that hunger:
from torchvision import transforms
train_transform = transforms.Compose([
transforms.RandomCrop(32, padding=4), # random crop with padding
transforms.RandomHorizontalFlip(p=0.5), # horizontal flip
transforms.ColorJitter(
brightness=0.2, contrast=0.2,
saturation=0.2, hue=0.1
),
transforms.RandomRotation(degrees=15),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406], # ImageNet statistics
std=[0.229, 0.224, 0.225]
),
])
# Validation โ no augmentation, only normalization
val_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
Each transform exploits a known invariance: a dog is still a dog when flipped horizontally, so horizontal flip is safe; lighting shouldn't change object identity, so color jitter teaches that; scale and position shouldn't matter either, so random crops teach that too.
Using a Pre-Trained ResNet
Transfer learning means starting from a backbone already trained on ImageNet instead of training from scratch โ almost always the right default, since training ImageNet-scale models from zero requires hundreds of GPU-hours and millions of images.
import torchvision.models as models
# Load pre-trained ResNet-50
backbone = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
# Freeze early layers โ they have learned general features
for name, param in backbone.named_parameters():
if 'layer4' not in name and 'fc' not in name:
param.requires_grad = False
# Replace the classification head for your number of classes
num_classes = 5 # your task
backbone.fc = nn.Sequential(
nn.Dropout(0.3),
nn.Linear(backbone.fc.in_features, num_classes)
)
# Only the final layers are trainable
trainable = sum(p.numel() for p in backbone.parameters() if p.requires_grad)
total = sum(p.numel() for p in backbone.parameters())
print(f"Trainable: {trainable:,} / {total:,} ({100*trainable/total:.1f}%)")
Freezing the early layers keeps the features that transfer well and only spends training budget on the layers specific to your task. For more detail on fine-tuning strategies, see the Transfer Learning Explained article.
Practical Training Tips
A few habits separate models that train cleanly from ones that stall:
- Learning rate warmup avoids early destructive updates: start with a small learning rate โ about a tenth of the target โ for the first few epochs, then ramp up linearly, since weights are still randomly initialized and large steps can wreck them.
- Cosine annealing finds flatter, more generalizable minima: decay the learning rate along a cosine curve instead of a step schedule.
scheduler = torch.optim.lr_scheduler.OneCycleLR(
optimizer,
max_lr=0.01,
epochs=50,
steps_per_epoch=len(train_loader),
pct_start=0.3, # 30% of training for warmup
)
- Mixed precision training cuts memory and speeds up training: run the forward and backward pass in float16 and keep parameter updates in float32, which nearly halves memory use and often speeds up training 2-3x on modern GPUs.
scaler = torch.cuda.amp.GradScaler()
with torch.cuda.amp.autocast():
output = model(input)
loss = criterion(output, target)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
From CNNs to Transformers
CNNs dominated computer vision from AlexNet in 2012 through roughly 2020. Then Vision Transformers (ViT) (Dosovitskiy et al., 2021) showed that transformers could match or beat CNNs on ImageNet โ but only when trained on enough data.
The competition is unresolved: ConvNeXt (Liu et al., 2022) showed that modernizing CNNs with transformer-inspired design choices closes most of the accuracy gap. Both architectures remain relevant, and hybrid designs combining both are increasingly common in production systems.
For sequence problems โ text, audio, time series โ CNNs play a secondary role. That territory belongs to recurrent networks and transformers, covered in LSTM vs Transformer.
Test your CNN knowledge with the Deep Learning Quiz, and for more context on how these architectures fit together, the ML Algorithms Quiz covers the broader landscape.
The Machine Learning course includes hands-on CNN projects, and LLM concepts notes explain how visual features connect to language model representations.
Advertisement
๐ฌ DiscussionPowered by GitHub Discussions
Frequently Asked Questions

Software Testing Expert & Prompt Engineering
Ensures every release is bug-free through rigorous testing, and crafts high-precision prompts that power our AI-driven workflows. Abdullah Al Arman Emon leads QA and prompt engineering across AiTechWorlds.
Not sure yet? Ask AI about this article
Get an instant, unbiased AI summary of โConvolutional Neural Networks (CNNs): How Image Recognition Worksโ.
Advertisement
Related Articles
Deep Learning Explained: Neural Networks from Zero to Understanding
Most tutorials teach you the API. This guide teaches you what's actually happening inside a neural network โ forward pass, backprop, and why depth matters.
LSTM vs Transformer: The Evolution of Sequence Learning in AI
LSTMs ruled NLP for a decade. Transformers replaced them in three years. This is the technical story of why โ and what each architecture actually computes.
Building Your First Deep Learning Model with PyTorch: Practical Guide
Learn to build deep learning models with PyTorch from scratch. Covers tensors, neural networks, training loops, and your first image classifier โ hands-on for real beginners.
Transfer Learning Explained: Fine-Tune Pre-Trained Models in 30 Minutes
Transfer learning lets you use ResNet, BERT, and ViT weights trained on millions of examples for your own dataset. Fine-tune in 30 minutes with real code and benchmark comparisons.