CNNs — Shopify Product Image Classification
Filters, feature maps, pooling, and how CNNs learn to recognise objects at any position in an image. Built from scratch then scaled with transfer learning.
A 224×224 image has 150,528 pixels. An MLP treating each pixel as a separate input needs millions of parameters just for the first layer — and still cannot recognise a shirt if it appears in a different corner of the image.
Shopify lists millions of fashion products. Each listing needs a category tag — kurta, saree, jeans, sneakers. An MLP flattens the image to a vector of 150,528 numbers and connects every pixel to every neuron. A first hidden layer of 512 neurons needs 77 million weights. It trains on images of shirts centred in frame, then fails on shirts shifted slightly to the left — because it memorised pixel positions, not the concept of "shirt."
CNNs solve both problems with one idea: instead of connecting every pixel to every neuron, slide a small filter (typically 3×3 pixels) across the entire image. The same filter weights are reused at every position — this is weight sharing. A filter that detects a vertical edge detects it whether the edge is in the top-left or bottom-right corner. This gives CNNs two critical properties: far fewer parameters, and translation invariance.
Imagine inspecting a large fabric roll for defects. You would not look at the entire roll simultaneously — you would use a small magnifying glass and slide it across, looking for the same pattern (a tear, a stain) at every position. You use the same visual skill (the same filter weights) everywhere.
A CNN's convolutional layer is that magnifying glass — a small filter sliding across the image, applying the same weights at every position. Early layers detect edges and textures. Middle layers detect shapes. Deep layers detect objects. The hierarchy of features is learned automatically from labelled images.
Convolution — sliding a filter across an image
A convolution takes a filter (a small matrix of learnable weights, e.g. 3×3) and slides it across the input image. At each position the filter is placed, the element-wise product between the filter and the overlapping image patch is computed and summed. The result — one number per position — forms the feature map. Multiple filters produce multiple feature maps, one per filter.
Pooling, flattening, and the full CNN pipeline
A complete CNN stacks three types of layers. Convolutional layers detect features — edges, textures, shapes — and produce feature maps. Pooling layers reduce spatial dimensions — typically MaxPool2d(2,2) halves H and W, keeping the most prominent feature in each 2×2 region. This builds translation invariance and reduces computation. Fully connected layers at the end combine all detected features to make the final classification decision.
Full training loop — data augmentation, class weighting, early stopping
Training a CNN from scratch on images requires one additional technique not needed for tabular data: data augmentation. Images of the same product can be flipped, rotated, cropped, or colour-jittered without changing their category. Applying random transformations during training artificially multiplies the dataset size and teaches the network that these variations should produce the same prediction. Without augmentation, CNNs overfit rapidly on small datasets.
Transfer learning — take ResNet50 pretrained on ImageNet, fine-tune the head
Training a CNN from scratch requires hundreds of thousands of labelled images and days of GPU compute. Shopify does not do this. Nobody does this for product classification. Instead, they use a model pretrained on ImageNet — a dataset of 1.2 million images across 1,000 categories. That model has already learned to detect edges, textures, shapes, and objects. Replace only the final classification layer with one that outputs your 6 categories, then fine-tune. This is transfer learning — and it produces better results with 1,000 images than training from scratch with 100,000.
Freeze all pretrained layers. Train only the new classification head. Fast — only a small number of parameters update. Best when your dataset is small (<1,000 images) and similar to ImageNet.
Freeze early layers (edge/texture detectors — universal). Unfreeze later layers (task-specific features). Train head + later layers with a small lr. Best balance of speed and accuracy.
Unfreeze all layers. Train entire network with a very small lr (1e-5). Early layers need tiny updates — they are already good. Risk of catastrophic forgetting if lr is too high.
Every common CNN mistake — explained and fixed
Shipping a CNN to a phone is a different engineering problem than shipping one to a GPU server
Every backbone comparison earlier in this module ranked architectures by accuracy. In production that ranking gets a second axis: does the model actually fit the device it needs to run on. A model that scores highest on a validation set is worthless if it is 98MB and takes 340 milliseconds per frame on the hardware that has to run it.
A retail team built a model to detect out-of-stock shelves from in-store camera frames. The first version, a fine-tuned ResNet50, hit 94 percent accuracy in validation. On the actual in-store hardware — a low-power ARM-based camera unit, not a GPU — it took 340 milliseconds per frame and the 98MB checkpoint barely fit in available memory alongside the camera's own firmware. It could not ship as built.
The team switched the backbone to MobileNetV3-Small, applied post-training int8 quantisation, and pruned the classification head. The final model was 4.2MB and ran in 12 milliseconds per frame on the same hardware. Accuracy dropped from 94 percent to 91 percent — a trade the team accepted without much debate, since the alternative was a model the device could not run at all.
Five things people get wrong about CNNs
Convolution itself is translation equivariant, not invariant — shift the input image and the resulting feature map shifts by the same amount rather than staying fixed. What gives a CNN its practical robustness to small shifts is pooling and downsampling collapsing spatial resolution layer by layer, which only approximates invariance, and imperfectly: research on shift invariance (Zhang, 2019) showed standard strided pooling can alias and actually break invariance in modern architectures, which is why anti-aliased pooling variants exist. True invariance is something you approximate through architecture choices, not a guarantee convolution hands you for free.
Stacking two 3×3 convolutional layers gives the exact same receptive field as one 5×5 layer, and three 3×3 layers match one 7×7 layer — but with fewer total parameters (3×(3×3)=27 vs 7×7=49 weights per channel pair) and, critically, two or three ReLU non-linearities inserted between them instead of one. This was the specific insight behind VGGNet: depth with small filters is strictly more expressive per parameter than shallow networks with large filters, which is why virtually every modern CNN backbone uses stacks of 3×3 (or even 1×1) convolutions instead of large kernels.
CNN filters start as small random matrices — there is no hand-engineering involved at all. What's genuinely striking is that after training on real images, the very first convolutional layer's filters frequently converge to something that looks like edge detectors and colour-opponent blobs — visually similar to, but not copied from, classical filters like Sobel — purely because gradient descent discovers these are the most useful low-level features for the task. Deeper layers converge to increasingly abstract, task-specific patterns with no classical analogue at all: textures, object parts, and eventually whole-object detectors a human would never hand-design.
The features learned by early and middle convolutional layers — edges, corners, textures, colour gradients, simple shapes — are generic visual primitives, not specific to any ImageNet category, and they transfer surprisingly well even to domains that look nothing like natural photos: medical imaging, satellite imagery, and industrial defect detection all see real gains from ImageNet-pretrained backbones. What actually needs to match your domain is not the visual similarity of the images but your fine-tuning strategy — the more your domain differs from natural photos, the more of the later, task-specific layers you typically need to unfreeze and retrain rather than just replacing the final classification head.
MaxPool is one common way to reduce spatial dimensions, but it is not a structural requirement — many strong modern architectures downsample using a strided convolution instead, letting the same layer that extracts features also handle the resolution reduction, or dispense with pooling until a single global-average-pool at the very end. The "Striving for Simplicity" line of research demonstrated that replacing all pooling with strided convolutions can match or beat traditional pooling-based architectures, because pooling inherently discards spatial information (only the max or average survives) that a learned strided convolution can partially preserve.
CNNs — 5 questions interviewers actually ask
Two properties, both baked into the convolution operation itself. First, local connectivity — each output value only depends on a small local patch of the input (e.g. 3×3), not the entire image, unlike a fully connected layer where every input connects to every output. Second, and more importantly, weight sharing — the exact same filter weights are reused at every spatial position across the image rather than learning a separate set of weights per position. A single 3×3 filter over a 3-channel image has only 27 weights plus bias regardless of whether the image is 32×32 or 1024×1024, because that same filter simply slides across however many positions the image contains. This is what makes CNNs both parameter-efficient and translation-equivariant.
Output size = floor((H + 2×padding − kernel_size) / stride) + 1. With "valid" padding (padding=0), the output shrinks every layer — a 3×3 filter with stride 1 on a 32×32 input gives 30×30 — which compounds across many layers and eventually leaves too little spatial resolution to keep stacking. With "same" padding (padding=(kernel_size−1)/2 for stride 1, e.g. padding=1 for a 3×3 filter), the output stays the same spatial size as the input, which is what lets you build very deep feature extractors without spatial dimensions collapsing prematurely. In practice: use "same" padding through the feature-extraction backbone so depth is a free architectural choice, and use pooling or strided convolutions as the deliberate, controlled points where you reduce spatial resolution.
With only 500 images I would start with feature extraction — freeze the entire pretrained backbone and train only a new classification head on top. With so little data, unfreezing and updating millions of backbone parameters risks catastrophic forgetting of useful pretrained features and overfitting almost immediately, since the backbone has far more capacity than the dataset can constrain. If feature extraction plateaus below the accuracy I need, I'd move to partial fine-tuning — unfreeze only the last block or two (which hold more task-specific, less universal features) with a very small learning rate. Full fine-tuning is usually reserved for datasets in the tens of thousands of images or more, or when the target domain is visually very different from natural photos.
CNNs are data-hungry and prone to overfitting on the exact pixel patterns of a small training set — augmentation artificially expands the effective dataset and teaches the network that these transformations shouldn't change the predicted label, directly reinforcing translation and rotation robustness the raw architecture doesn't guarantee for free. Applying augmentation at validation or test time is a real bug: it makes your evaluation numbers noisy and non-reproducible, since accuracy varies run to run based on random transforms, and more subtly it means you're evaluating on a shifted data distribution rather than genuinely representative held-out data — you can end up over- or under-estimating true generalisation performance.
MaxPool2d(kernel_size, stride) reduces spatial size by a fixed ratio determined by its kernel and stride — 2×2 with stride 2 always halves H and W — keeping the strongest activation in each local window. AdaptiveAvgPool2d((h, w)) instead is told the exact output size you want and figures out the pooling window itself to hit that target regardless of the input's spatial size — this is what lets the same architecture accept variable input image sizes and still produce a fixed-size feature vector for the classifier head, since a plain Flatten would produce a different-length vector for every different input resolution and break the fixed-size Linear layer that follows it. This is exactly why architectures like ResNet end their feature extractor with AdaptiveAvgPool2d((1,1)) rather than relying on a fixed input size.
You can classify images. Next: model sequences — text, time series, audio.
CNNs exploit spatial structure in images. But many real-world problems involve sequences — a sentence is a sequence of words, a stock price is a sequence of daily values, a user session is a sequence of actions. Sequences have temporal structure: what came earlier affects what comes later. CNNs treat every position independently and cannot model this dependency. Module 47 covers RNNs and LSTMs — architectures designed specifically to process sequences by maintaining a hidden state that carries information forward across time steps.
Hidden states, vanishing gradients across time, and how LSTMs use gates to selectively remember and forget.
🎯 Key Takeaways
- ✓CNNs solve two fundamental problems with MLPs on images: too many parameters (a 224×224 image needs 150k inputs × hidden units) and no spatial invariance (an MLP memorises pixel positions, not visual patterns). Convolutional filters slide across the image reusing the same weights everywhere — weight sharing dramatically reduces parameters and gives translation invariance.
- ✓A convolutional layer applies multiple small filters (typically 3×3) across the input, producing one feature map per filter. Output size = (H + 2×padding − kernel) / stride + 1. padding=1 with a 3×3 filter keeps spatial dimensions unchanged. MaxPool2d(2,2) halves H and W, keeping the strongest activation in each 2×2 region.
- ✓A complete CNN stacks: Conv+BN+ReLU blocks (feature extraction) → MaxPool (spatial reduction) → AdaptiveAvgPool (fixed output size) → Flatten → FC layers (classification). BatchNorm2d is placed after Conv2d and before ReLU for stable training.
- ✓Data augmentation is essential for CNN training — random flips, rotations, colour jitter, and random erasing artificially expand the dataset and teach the network that these variations do not change the category. Apply augmentation only during training, never at validation or test time.
- ✓Transfer learning is how all production image classifiers are built. Take a model pretrained on ImageNet, replace the final FC layer with one matching your number of classes, and fine-tune. Use differential learning rates: very small (1e-5) for pretrained backbone layers, normal (1e-3) for the new head. Always apply ImageNet normalisation when using pretrained models.
- ✓The four CNN gotchas: input must be (batch, channels, H, W) — use unsqueeze(0) for single images. Always normalise pixel values to 0–1 before training. When using pretrained models always apply ImageNet mean/std normalisation. Reduce batch size or use gradient accumulation when hitting CUDA OOM errors.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.