- Compute a 2-D convolution by hand and explain why libraries implement cross-correlation
- Apply the output-size and receptive-field formulas to any stack of convolution and pooling layers
- Count the parameters and multiply-accumulates of convolutional layers and compare them with fully connected layers
- Explain the design ideas behind LeNet, AlexNet, VGG, ResNet and EfficientNet
- Implement transfer learning with a pretrained torchvision backbone and appropriate data augmentation
- Compare CNNs with vision transformers and choose between them for a given data and compute budget
Before this: Neural Networks and Backpropagation, Training Deep Networks in Practice
Why convolutional networks matter
Convolutional neural networks (CNNs) are the architecture that made deep learning work for images. They power photo search, medical image triage, defect detection on factory lines, document scanning, and the perception stacks of robots and cars. Even in a period dominated by transformers, convolutions remain the default when you need a fast, data-efficient vision model, and they live on inside many hybrid and generative architectures. This lesson takes you from the convolution operation itself to fine-tuning a pretrained backbone, and closes with a careful comparison against vision transformers.
Intuition: a small stencil slid across the image
Picture a 3×3 cardboard stencil with nine numbers written on it. You lay it on the top-left corner of a grayscale image, multiply each stencil number by the pixel underneath, add the nine products, and write the sum in a new grid. Then you slide the stencil one pixel right and repeat, row by row, until you have covered the image. The new grid is a feature map: it is bright wherever the image locally "looks like" the stencil's pattern. A stencil with positive numbers on its left column and negative numbers on its right lights up at vertical edges.
A CNN learns the numbers on hundreds of such stencils (called kernels or filters) by gradient descent, stacks them in layers, and lets deeper layers combine edges into textures, textures into parts, and parts into objects. Two built-in assumptions make this efficient:
- Locality: useful patterns are built from nearby pixels, so each output only looks at a small neighbourhood.
- Weight sharing / translation equivariance: the same detector is useful everywhere, so one kernel is reused at every position. If the input shifts, the feature map shifts with it.
These assumptions are called the CNN's inductive bias. They are why a CNN with a few million parameters can learn from tens of thousands of images, where a fully connected network would need billions of weights.
The convolution operation, formally
Let the input be a tensor (channels, height, width) and let the layer have kernels, each of shape . The output feature map for kernel at position is
where is the kernel weight tensor, is a per-output-channel bias, and is the stride (how many pixels the kernel jumps between positions). Note that each kernel spans all input channels: a 3×3 kernel on an RGB image really has weights.
Strictly, this is cross-correlation; true mathematical convolution flips the kernel first. Deep learning libraries (PyTorch's conv2d included) implement cross-correlation and call it convolution. Because the kernel is learned, the flip does not matter.
Worked example: one kernel, every step
Input (4×4, one channel) and a vertical-edge kernel (3×3), stride 1, no padding:
Because has in its left column and in its right column, each output equals (sum of the left column of the patch) minus (sum of the right column).
- Top-left, rows 0–2, columns 0–2: left column , right column , output .
- Top-right, rows 0–2, columns 1–3: left column , right column , output .
- Bottom-left, rows 1–3, columns 0–2: left , right , output .
- Bottom-right, rows 1–3, columns 1–3: left , right , output .
A 4×4 input and 3×3 kernel give a 2×2 output: the kernel fits in only two positions along each axis.
Output size, padding, stride, dilation
Padding adds rows/columns of (usually) zeros around the border so the kernel can be centred on edge pixels. For input width , kernel , padding , stride :
The same formula applies to height. Two cases to memorise: with stride 1, "same" padding for odd keeps the size unchanged (3×3 with , 5×5 with ); with and (patchify), the size divides by .
Worked example — the classic ResNet "stem" on a 224×224 image: a 7×7 convolution with stride 2 and padding 3 gives . A following 3×3 max-pool with stride 2 and padding 1 gives .
Dilation spaces the kernel taps pixels apart, giving an effective kernel size with no extra weights; substitute for in the formula. Dilated convolutions are common in segmentation and audio models where a large context is needed at full resolution.
Pooling
Pooling downsamples each channel independently with a fixed (non-learned) function. Max pooling with a 2×2 window and stride 2 keeps the largest value in each window, halving height and width and giving a little local translation invariance. Average pooling takes the mean. Global average pooling averages each entire channel to a single number, turning a map into a length- vector; modern classifiers use it in place of large fully connected layers, which removes most of their parameters and lets the network accept varied input sizes. Many recent designs replace max pooling with strided convolutions, letting the network learn how to downsample.
Parameter and compute counting
A convolutional layer has
and costs about multiply-accumulates (MACs). Crucially, the parameter count does not depend on image size.
Worked example: a 3×3 convolution from 64 to 128 channels has parameters. At a 56×56 output it costs MACs. A fully connected layer mapping the same inputs to outputs would need about weights — roughly a million times more. That gap is the practical payoff of locality and weight sharing.
Receptive field
The receptive field of a unit is the region of the input that can influence it. For a stack of layers with kernel sizes and strides , the receptive field after layer is
The product of earlier strides (the "jump") is how many input pixels one step in layer 's input corresponds to. Continuing the ResNet stem: the 7×7 stride-2 conv gives ; the 3×3 stride-2 pool adds , giving ; a following 3×3 stride-1 conv adds , giving .
Two consequences matter in practice. First, stacking two 3×3 convolutions gives a 5×5 receptive field with weights versus for one 5×5 layer (with channels in and out), plus an extra nonlinearity; three 3×3 layers match a 7×7 at versus . This is the VGG design argument. Second, the effective receptive field — where gradients are actually large — is concentrated near the centre and smaller than the theoretical one, which is why downsampling and global pooling are needed to see whole objects.
Building blocks of a modern CNN
A typical stage is: convolution → batch normalization (normalizes each channel using batch statistics, then applies a learned scale and shift) → ReLU, repeated, followed by downsampling that halves resolution while doubling channels. The network ends with global average pooling and a linear classifier.
The code below checks the worked example against PyTorch and builds a small CIFAR-sized CNN.
import numpy as np, torch, torch.nn as nn, torch.nn.functional as F
X = np.array([[1,2,0,1],[0,1,3,1],[2,1,0,0],[1,0,1,2]], dtype=np.float32)
K = np.array([[1,0,-1],[1,0,-1],[1,0,-1]], dtype=np.float32)
def conv2d_valid(x, k, stride=1): # single channel, no padding
kh, kw = k.shape
oh, ow = (x.shape[0]-kh)//stride + 1, (x.shape[1]-kw)//stride + 1
out = np.zeros((oh, ow), dtype=x.dtype)
for i in range(oh):
for j in range(ow):
patch = x[i*stride:i*stride+kh, j*stride:j*stride+kw]
out[i, j] = np.sum(patch * k) # cross-correlation (no flip)
return out
print(conv2d_valid(X, K)) # [[0,2],[-1,-1]]
print(F.conv2d(torch.from_numpy(X)[None, None], torch.from_numpy(K)[None, None])[0, 0])
class SmallCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),
nn.Conv2d(32, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),
nn.MaxPool2d(2), # 32x32 -> 16x16
nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(2)) # 16x16 -> 8x8
self.head = nn.Sequential(nn.AdaptiveAvgPool2d(1), nn.Flatten(),
nn.Linear(64, num_classes))
def forward(self, x):
return self.head(self.features(x))
model = SmallCNN()
print(sum(p.numel() for p in model.parameters())) # 29,546
print(model(torch.randn(8, 3, 32, 32)).shape) # torch.Size([8, 10])
PyTorch tensors for images are laid out as (batch, channels, height, width), and convolution weights as (out_channels, in_channels, kH, kW) — hence the [None, None] to add batch and channel axes. The 29,546 parameters break down as 896 + 9,248 + 18,496 for the three convolutions, 64 + 64 + 128 for the batch-norm scales and shifts, and 650 for the classifier.
Classic architectures and what each one taught
| Model | Year | Key idea | Lesson for practitioners |
|---|---|---|---|
| LeNet-5 (LeCun et al.) | 1998 | Conv → pool → conv → pool → fully connected, trained end to end on digits | The basic recipe; worked on small grayscale images |
| AlexNet (Krizhevsky, Sutskever, Hinton) | 2012 | 5 conv + 3 fully connected layers, about 60M parameters, ReLU, dropout, GPU training | Scale + GPUs + data won ImageNet decisively and started the deep learning boom |
| VGG (Simonyan & Zisserman) | 2014 | Only 3×3 convolutions, 16–19 weight layers | Small kernels stacked deep beat large kernels; simple but heavy (VGG-16 is about 138M parameters, mostly in its fully connected layers) |
| ResNet (He, Zhang, Ren, Sun) | 2015 | Residual connections , up to 152 layers | Identity shortcuts make very deep nets trainable; ResNet-152 used about 11.3 GFLOPs versus 19.6 for VGG-19 and won ILSVRC 2015 |
| EfficientNet (Tan & Le) | 2019 | Compound scaling of depth, width and resolution together (, , ) | Scale all three dimensions in balance; B7 reached 84.3% ImageNet top-1 with about 66M parameters |
Why residuals work. In a residual block the layers learn a correction rather than a full mapping, and the gradient has a direct path: . Even if is small, the identity term carries signal backward through dozens of blocks. A bottleneck block (1×1 conv to reduce channels, 3×3 conv, 1×1 conv to expand) keeps the cost of deep networks manageable; ResNet-50 and deeper use it.
1×1 convolutions mix channels at each pixel without looking at neighbours. They are cheap channel-wise linear layers used to shrink or expand channel counts. Depthwise-separable convolutions (a per-channel conv followed by a 1×1 conv) cut parameters from to and are the basis of mobile architectures, including EfficientNet's building blocks.
Transfer learning with a pretrained backbone
Training a CNN from scratch needs a large labelled dataset. In practice you almost always start from a backbone pretrained on a large dataset such as ImageNet, because its early layers already detect generic edges, colours and textures. There are two standard regimes:
- Feature extraction (linear probing): freeze the backbone, replace the final classifier, train only the new head. Fast, needs little data, hard to overfit.
- Fine-tuning: unfreeze some or all of the backbone and train with a small learning rate (often 10× smaller than for the head), so the pretrained features adapt without being destroyed.
import torch, torch.nn as nn
from torchvision import models, transforms
weights = models.ResNet18_Weights.DEFAULT
model = models.resnet18(weights=weights)
for p in model.parameters():
p.requires_grad = False # stage 1: freeze backbone
model.fc = nn.Linear(model.fc.in_features, 5) # new trainable head, 5 classes
train_tf = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.6, 1.0)),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(0.2, 0.2, 0.2),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
eval_tf = weights.transforms() # preprocessing matching pretraining
head_opt = torch.optim.AdamW(model.fc.parameters(), lr=1e-3)
# stage 2: unfreeze the last stage and fine-tune with discriminative learning rates
for p in model.layer4.parameters():
p.requires_grad = True
ft_opt = torch.optim.AdamW([
{"params": model.layer4.parameters(), "lr": 1e-4},
{"params": model.fc.parameters(), "lr": 1e-3},
], weight_decay=0.01)
The Normalize statistics are the ImageNet channel means and standard deviations the backbone was trained with; using different preprocessing at inference than at pretraining silently costs accuracy. The weights enum API shown here is the current torchvision interface (older code used pretrained=True).
Data augmentation
Data augmentation applies random, label-preserving transformations to training images so the model sees a different version of each image every epoch. It is a regularizer that encodes invariances you believe in. Standard choices: random resized crops, horizontal flips, small rotations, colour jitter; stronger recipes add RandAugment-style random policies, Mixup (blend two images and their labels) and CutMix (paste a patch from one image into another and mix labels in proportion to area). Augment only the training set, and choose transformations that truly preserve the label: horizontal flips are fine for cats, wrong for reading text, and vertical flips are usually wrong for street scenes but fine for satellite imagery.
CNNs versus vision transformers
A vision transformer (ViT) (Dosovitskiy et al., 2020) cuts an image into fixed patches — 16×16 pixels for ViT-B/16, so a 224×224 image becomes tokens — linearly embeds each patch (equivalent to a convolution with kernel size and stride both 16), adds position embeddings, and runs a standard transformer encoder (see the transformers lesson). Every patch can attend to every other patch from the first layer, so the receptive field is global immediately.
| Aspect | CNN | Vision transformer |
|---|---|---|
| Inductive bias | Strong: locality, translation equivariance | Weak: must learn spatial structure from data |
| Data needs | Good from thousands of images, especially with pretraining | The original ViT trailed comparable ResNets when trained on ImageNet alone and excelled only after pretraining on much larger sets (ImageNet-21k, JFT-300M) |
| Compute vs resolution | Linear in pixel count | Self-attention is quadratic in the number of patches |
| Global context | Built up gradually with depth | Available in every layer |
| Scaling with data | Saturates earlier | Keeps improving with more data and compute |
In practice the gap has narrowed from both sides: transformers borrowed hierarchical, local-window designs, and modernized CNNs adopted transformer training recipes. As a rule of thumb: for small data, edge devices or high resolution, start with a CNN or a hybrid; when a large pretrained ViT backbone is available for your domain, fine-tuning it is often the strongest option.
Common pitfalls
- Wrong preprocessing for a pretrained model: mismatched resize, crop or normalization statistics; or feeding BGR images (OpenCV's default) to an RGB-trained model.
- Forgetting
model.eval()at inference: batch-norm then uses the current batch's statistics and dropout stays active, giving noisy, batch-dependent predictions. - Fine-tuning with a high learning rate: wipes out pretrained features in a few hundred steps. Use a low rate for the backbone and warm up.
- Tiny batch sizes with batch norm: statistics become noisy. Freeze BN statistics or use GroupNorm.
- Augmentations that change the label: flipping digits, rotating text, colour-jittering tasks where colour is the signal.
- Shape bugs: channels-last arrays fed to a channels-first API, or miscomputed output sizes. Compute sizes with the formula, or print them after each layer.
- Leakage across splits: near-duplicate images (frames from the same video, the same patient) in both train and test make results look far better than they are.
In practice
- Start with a pretrained backbone from a maintained library (torchvision, timm, Hugging Face) rather than a hand-built architecture. ResNet-50 and EfficientNet-class models remain strong, cheap baselines; try a pretrained ViT or modern hybrid if latency allows.
- Train in two stages — head only, then partial or full unfreezing with discriminative learning rates — and track validation metrics per stage.
- Use mixed precision, channels-last memory format on GPUs, and a learning-rate schedule with warmup and cosine decay.
- Budget compute with the MAC formula; resolution is usually the most expensive knob because cost scales with .
- For deployment, export to ONNX or a runtime such as TensorRT or Core ML, quantize to INT8 where accuracy allows, and benchmark latency on the target device, not your workstation.
- The capstone capstone-image-classifier walks through this pipeline end to end.
Key takeaways
- A convolution slides a small learned kernel over the input, so its parameter count is independent of image size and the same detector is reused everywhere.
- Output width is floor((W - k + 2P)/S) + 1, and receptive field grows by (k - 1) times the product of earlier strides at each layer.
- Stacks of 3x3 convolutions reach large receptive fields with fewer parameters and more nonlinearities than single large kernels.
- Residual connections give gradients an identity path, which is what made networks with 100+ layers trainable.
- In practice you fine-tune a pretrained backbone with matching preprocessing, low backbone learning rates and label-preserving augmentation.
- CNNs carry strong locality bias and need less data; vision transformers have global attention from layer one and scale better with large pretraining data.
Terms in this lesson
- Kernel (filter)
- A small learned weight tensor spanning all input channels that is slid across the input to produce one output feature map.
- Stride
- The number of input positions the kernel moves between successive outputs; a stride of 2 roughly halves each spatial dimension.
- Padding
- Extra border values (usually zeros) added around the input so kernels can be applied at the edges and output size can be controlled.
- Receptive field
- The region of the input image that can influence a given unit in a deeper layer.
- Pooling
- A fixed, non-learned downsampling operation, such as taking the maximum or average over each local window of a feature map.
- Residual connection
- A shortcut that adds a block's input to its output, y = x + F(x), so the block learns a correction and gradients flow through the identity path.
- Transfer learning
- Reusing a network pretrained on a large dataset as the starting point for a new task, either by training only a new head or by fine-tuning some layers.
- Data augmentation
- Applying random label-preserving transformations to training inputs to expose the model to more variation and reduce overfitting.
- Inductive bias
- Assumptions built into a model's architecture, such as locality and weight sharing in CNNs, that shape what it can learn easily from limited data.
Check your understanding
- 1.
A 32x32 input passes through a 5x5 convolution with stride 1 and no padding, then a 2x2 max-pool with stride 2. What is the output height and width?
- 2.
How many parameters (weights plus biases) does a 3x3 convolution from 3 input channels to 16 output channels have?
- 3.
Why did VGG replace large kernels with stacks of 3x3 convolutions?
- 4.
You have 1,500 labelled photos across 4 classes and a modest GPU. Which approach is most likely to give the best accuracy quickly?
- 5.
What is the main reason residual connections let very deep networks train?
Exercises
Work each problem before opening the solution.
- Exercise 1Core
A 128x128x3 image passes through: (1) Conv 3->32, 3x3, stride 1, padding 1; (2) MaxPool 2x2, stride 2; (3) Conv 32->64, 5x5, stride 2, padding 2; (4) Conv 64->64, 3x3, stride 1, padding 0.
For each layer give the output shape, the parameter count, and the receptive field (in input pixels) of one output unit.
Show solution
Use , params , and where is the product of earlier strides.
Layer Output Params Receptive field Jump after Conv 3x3 s1 p1 (128-3+2)/1+1 = 128 -> 128x128x32 333*32+32 = 896 1 + 2*1 = 3 1 MaxPool 2 s2 (128-2)/2+1 = 64 -> 64x64x32 0 3 + 1*1 = 4 2 Conv 5x5 s2 p2 floor((64-5+4)/2)+1 = floor(31.5)+1 = 32 -> 32x32x64 5532*64+64 = 51,264 4 + 4*2 = 12 4 Conv 3x3 s1 p0 (32-3)/1+1 = 30 -> 30x30x64 3364*64+64 = 36,928 12 + 2*4 = 20 4 Total parameters: 896 + 51,264 + 36,928 = 89,088. Each final unit sees a 20x20 patch of the input.
- Exercise 2Core
Implement
conv2d(x, w, b, stride=1, padding=0)in NumPy for multi-channel inputxof shape (C_in, H, W), weightswof shape (C_out, C_in, k, k) and biasbof shape (C_out,). Verify it againsttorch.nn.functional.conv2don random data.Show solution
import numpy as np, torch, torch.nn.functional as F def conv2d(x, w, b, stride=1, padding=0): C_in, H, W = x.shape C_out, _, k, _ = w.shape xp = np.pad(x, ((0, 0), (padding, padding), (padding, padding))) H_out = (H - k + 2 * padding) // stride + 1 W_out = (W - k + 2 * padding) // stride + 1 out = np.zeros((C_out, H_out, W_out), dtype=x.dtype) for i in range(H_out): for j in range(W_out): patch = xp[:, i*stride:i*stride+k, j*stride:j*stride+k] # (C_in, k, k) # contract over C_in, k, k for every output channel at once out[:, i, j] = np.tensordot(w, patch, axes=([1, 2, 3], [0, 1, 2])) + b return out rng = np.random.default_rng(0) x = rng.standard_normal((3, 9, 9)).astype(np.float32) w = rng.standard_normal((4, 3, 3, 3)).astype(np.float32) b = rng.standard_normal(4).astype(np.float32) mine = conv2d(x, w, b, stride=2, padding=1) ref = F.conv2d(torch.from_numpy(x)[None], torch.from_numpy(w), torch.from_numpy(b), stride=2, padding=1)[0].numpy() print(mine.shape, np.allclose(mine, ref, atol=1e-5)) # (4, 5, 5) Truenp.padwith a 3-tuple of pad widths pads only the spatial axes. The output size is (9 - 3 + 2)/2 + 1 = 5. Thetensordotsums over input channels and both kernel axes, which is exactly the formula . - Exercise 3Challenge
With C = 256 channels in and out (ignore biases), compare (a) one 7x7 convolution, (b) three stacked 3x3 convolutions and (c) a depthwise-separable 7x7 (depthwise 7x7 then pointwise 1x1). Give parameter counts, receptive fields, and the number of nonlinearities if each conv is followed by ReLU. Which would you choose for a mobile model and why?
Show solution
.
- (a) One 7x7: weights; receptive field 7; 1 ReLU.
- (b) Three 3x3: weights; receptive field ; 3 ReLUs.
- (c) Depthwise-separable: depthwise plus pointwise , total weights; receptive field 7; 1 or 2 ReLUs depending on placement.
Compute scales the same way per output pixel. For a mobile model, (c) is the clear choice: about 41x fewer weights than (a) and about 23x fewer than (b) for the same receptive field. The trade-off is that the depthwise step does not mix channels, so separable layers are somewhat less expressive per layer and are run in efficient blocks (as in MobileNet and EfficientNet). Option (b) is the classic VGG-style compromise when compute is less constrained.
- Exercise 4Challenge
You must classify chest X-rays into 3 classes and have 2,000 labelled images from 400 patients. Write a training plan and the core PyTorch training loop for fine-tuning a pretrained ResNet-18, covering: data split, augmentation choices (and which to avoid), the two-stage freezing schedule, batch-norm handling, and evaluation.
Show solution
Plan.
- Split by patient, not by image (e.g. 280/60/60 patients for train/val/test) so images of one patient never appear in two splits.
- Augmentation: small random resized crops (scale 0.8-1.0), small rotations (+/-10 degrees), brightness/contrast jitter. Avoid horizontal flips if laterality matters (heart position, text markers) and avoid hue jitter (X-rays are grayscale; replicate the single channel to 3 channels to match the backbone).
- Stage 1: freeze the backbone, train a new 3-way head for a few epochs at lr 1e-3. Stage 2: unfreeze
layer3andlayer4, lr 1e-4 for them and 1e-3 for the head, with cosine decay. - Batch norm: with small batches keep frozen layers in eval mode so their running statistics are not overwritten.
- Evaluate with per-class recall, macro-F1 and a confusion matrix on the patient-held-out test set, once, at the end.
import torch, torch.nn as nn from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) model.fc = nn.Linear(model.fc.in_features, 3) def set_stage(model, stage): for name, p in model.named_parameters(): trainable = name.startswith("fc") or (stage == 2 and name.startswith(("layer3", "layer4"))) p.requires_grad = trainable def train_epoch(model, loader, opt, device): model.train() for m in model.modules(): # keep BN stats fixed in frozen parts if isinstance(m, nn.BatchNorm2d) and not next(m.parameters()).requires_grad: m.eval() loss_fn = nn.CrossEntropyLoss() for x, y in loader: x, y = x.to(device), y.to(device) loss = loss_fn(model(x), y) opt.zero_grad(); loss.backward(); opt.step() @torch.no_grad() def evaluate(model, loader, device): model.eval(); preds, labels = [], [] for x, y in loader: preds.append(model(x.to(device)).argmax(1).cpu()); labels.append(y) return torch.cat(preds), torch.cat(labels) set_stage(model, 1) opt1 = torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr=1e-3) # ... run train_epoch for a few epochs with opt1, then: set_stage(model, 2) opt2 = torch.optim.AdamW([ {"params": [p for n, p in model.named_parameters() if n.startswith(("layer3", "layer4"))], "lr": 1e-4}, {"params": model.fc.parameters(), "lr": 1e-3}], weight_decay=0.01)The loaders (not shown) apply the training transforms only to the training set and the pretrained model's resize/normalize transforms to validation and test. Select the checkpoint on validation macro-F1, then report test metrics once.
Further reading
- paperKrizhevsky, Sutskever & Hinton, ImageNet Classification with Deep Convolutional Neural Networks (2012)
- paperSimonyan & Zisserman, Very Deep Convolutional Networks for Large-Scale Image Recognition (2014)
- paperHe, Zhang, Ren & Sun, Deep Residual Learning for Image Recognition (2015)
- paperTan & Le, EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks (2019)
- paperDosovitskiy et al., An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale (2020)
- bookZhang, Lipton, Li & Smola, Dive into Deep Learning, Ch. Convolutional Neural Networks
Go further
Sources
Fact-checked and last reviewed 2026-11-05