Build the mental model
A fully-connected layer flattens an image into a long vector and connects every pixel to every neuron with its own independent weight, which throws away two facts a CNN can exploit: nearby pixels are spatially related (an edge is a local pattern, not a global one), and a useful pattern like an edge or curve can appear anywhere in the image and should be detected the same way regardless of position. A convolutional layer instead learns a small filter (kernel) — say 3x3 pixels — and slides it across the entire image, computing the same dot-product operation at every position. Because the filter's weights are shared across all positions, the same small set of parameters detects a pattern wherever it occurs, giving the network translation invariance and drastically fewer parameters to learn than a fully-connected layer over the same image. Stacking several conv layers lets early layers detect simple patterns (edges, colors) and later layers combine those into complex ones (shapes, textures, objects). Pooling layers (like max pooling) then downsample the feature map by keeping only the strongest activation in each small region, reducing spatial size and computation while preserving the most important detected features.
Connect it to a real scenario
The Tutorial Platform's duplicate/low-quality image detector needs to recognize visual patterns — blur, watermark artifacts, near-identical screenshots — no matter where they sit in the frame; a screenshot cropped slightly differently shouldn't fool the detector. A fully-connected network would need separate weights for every pixel position and would fail to recognize the same blur pattern shifted a few pixels over, while a CNN's shared filters detect it regardless of position. The team stacks a few Conv2d+ReLU+MaxPool blocks so early layers catch simple artifacts like edges and compression blocks, and later layers combine them into higher-level cues like 'this looks like a re-upload of an existing lesson image.'
Try the working example
import torch
import torch.nn as nn
conv = nn.Conv2d(in_channels=3, out_channels=8, kernel_size=3, padding=1)
pool = nn.MaxPool2d(kernel_size=2)
# Fake tiny RGB image: batch=1, channels=3, height=16, width=16
image = torch.randn(1, 3, 16, 16)
conv_out = conv(image)
print("After conv:", conv_out.shape)
pooled_out = pool(conv_out)
print("After pool:", pooled_out.shape)It prints `After conv: torch.Size([1, 8, 16, 16])` (padding kept the spatial size the same while expanding to 8 channels), then `After pool: torch.Size([1, 8, 8, 8])` (max pooling halved the height and width).5-minute try-it
Change `kernel_size` in the Conv2d to 5 with `padding=2` and confirm the output spatial size still stays 16x16.
One important caution
Forgetting to set `padding` and being surprised the spatial dimensions shrink after every conv layer — with no padding, a 3x3 kernel loses 2 pixels off each spatial dimension per layer, and after several layers the feature map can shrink to nothing.
Confusing `in_channels` with image width/height — `in_channels` must match the number of channels in the input (e.g. 3 for RGB), not a spatial dimension, and passing the wrong value raises a shape-mismatch error.
Wikipedia — Convolutional neural network — Deep Learning