CNN Architectures and Transfer Learning
Size convolutions, build LeNet to ResNet, train a classifier and fine-tune a pretrained backbone. PyTorch and NumPy.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
Formulas#
- Output size
- H_{out} = \lfloor (H - K + 2P) / S \rfloor + 1
- Same padding
- P = (K - 1) / 2 for odd K and S = 1.
- Conv parameters
C_out * C_in * K * K + C_out. The last term is the biases.- Receptive field
- 1 + L (K - 1) for L stacked convs at stride 1.
- Two 3x3 or one 5x5
- same 5x5 view. 18C^2 against 25C^2 parameters.
- Residual block
- y = F(x) + x. The block learns identity with F(x) = 0.
- Cross-entropy
- -z_y + \log \sum_j \exp(z_j) on raw logits.
- Layer-wise LR decay
- \mathrm{lr}_k = \mathrm{base} \cdot \mathrm{decay}^{L - k}
Four families#
- LeNet-5 (1998)
- conv, avg pool, dense, tanh. 60,000 parameters.
- AlexNet (2012)
- ReLU, dropout, 5 conv and 3 dense layers. 60M parameters.
- VGG (2014)
- only 3x3 convs, 16 or 19 layers. 138M parameters.
- Inception (2014)
- 1x1, 3x3, 5x5 and pool in parallel, then concatenate.
- ResNet (2015)
- identity skip connections. 100+ layers train.
- BasicBlock
- two 3x3 convs with a skip. ResNet-18 and ResNet-34.
- Bottleneck
- 1x1 down, 3x3, 1x1 up. ResNet-50, -101, -152.
- ResNet-18 or VGG-16
- 11.7M or 138M parameters. Top-1 69.8% or 71.6%.
TinyResNet for 32x32 input#
ResNet BasicBlock#
self.conv1 = nn.Conv2d(in_c, out_c, kernel_size=3, stride=stride, padding=1, bias=False)BN beta holds the biasself.conv2 = nn.Conv2d(out_c, out_c, kernel_size=3, stride=1, padding=1, bias=False)if stride != 1 or in_c != out_c: self.shortcut = nn.Sequential( nn.Conv2d(in_c, out_c, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_c))1x1 conv matches the shapeelse: self.shortcut = nn.Identity()no-op skipout = F.relu(self.bn1(self.conv1(x)))out = self.bn2(self.conv2(out))no ReLU before the addout = out + self.shortcut(x)F(x) + xreturn F.relu(out)Tasks in code#
| Task | Code |
|---|---|
| Output size of one conv | output_size(h_in, |
| Receptive field of a stack | receptive_field(layers) |
| Conv as one matmul | conv2d_im2col(x, |
| Apply a hand kernel | apply_kernel(x[0], |
| Mix a batch | mixup_batch(x, |
| Loss on soft labels | soft_cross_entropy(logits, |
| Frozen backbone, new head | make_feature_extractor(num_classes= |
| Keep BN running stats | freeze_bn_stats(model) |
Pick a transfer recipe#
- < 1k images, close to ImageNet
- freeze the backbone. Train the head only.
- 1k-10k, close
- freeze the first 2-3 stages. Fine-tune the rest.
- 10k-100k, any domain
- fine-tune end-to-end with discriminative LR.
- 100k+, far domain
- fine-tune everything. Consider training from scratch.
- BN, >= 5k examples
- keep BN in train mode. It updates its running statistics.
- BN, small dataset
- freeze BN in eval mode. Keep the ImageNet statistics.
- Tiny batch per GPU
- replace BN with GroupNorm.
- Head to replace
backbone.fc(ResNet),backbone.classifier[1](EfficientNet, MobileNet).
Conv layer shapes#
conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1, padding=1)tuple(conv.weight.shape)(C_out, C_in, K, K)sum(p.numel() for p in conv.parameters())64 * 3 * 3 * 3 + 64 = 1,792y = conv(torch.randn(8, 3, 224, 224))padding=0 gives 222x222One training epoch#
Fine-tune with discriminative LR#
model = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)model.fc = nn.Linear(model.fc.in_features, 10)new headgroups = discriminative_param_groups(model, base_lr=1e-3, decay=0.3)optimizer = SGD(groups, momentum=0.9, weight_decay=1e-4, nesterov=True)scheduler = CosineAnnealingLR(optimizer, T_max=epochs)for epoch in range(epochs): model.train() if freeze_bn: freeze_bn_stats(model)after model.train()start, unfreeze = progressive_unfreeze_schedule(model)start()only fc trainsunfreeze(epoch)layer4 first, then layer3Training defaults#
| Setting | Default | Note |
|---|---|---|
SGD lr | 0.1 | TinyResNet from scratch, cosine over 10 epochs |
momentum | 0.9 | With nesterov=True |
weight_decay | 5e-4 | Fine-tune uses 1e-4 |
label_smoothing | 0.1 | Target 1-eps for the true class |
Mixup alpha | 0.2 | lambda from Beta(a, a) |
base_lr | 1e-3 | The fc group. Earlier stages get less |
decay | 0.3 | conv1 gets about 0.00243 times base_lr |
batch_size | 128 | Val loader uses 256 |
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| Wrong gradient, flat curve | Softmax before the loss | Pass raw logits to CrossEntropyLoss |
| Looks like an unstable LR | Gradients accumulate | Call optimizer.zero_grad() each step |
| About 1% accuracy lost | ImageNet mean and std on CIFAR-10 | Compute stats on your train set |
| A "6" becomes a "9" | Augment changes the label | Use smaller rotation ranges |
| Fine-tuned below pretrained-only | LR or BN bug | Use discriminative LR, check BN mode |
| 5-15% accuracy lost silently | BN stats from another distribution | Freeze BN stats or use GroupNorm |
| All images get one class | LR destroys pretrained features | Lower the backbone LR |
| Deep plain net trains worse | Degradation past ~20 layers | Add residual connections |
Questions#
- Why 3x3 kernels?
- It is the smallest odd kernel with a centre.
- How to downsample without a pool?
- Use a conv with
stride=2. It halves H and W. - Why does mixup raise train loss?
- The targets are soft. Test accuracy goes up.
- Which two numbers for a transfer run?
- Pretrained-only is the floor. Fine-tuned is the ceiling.
- Why a smaller LR for early layers?
- They hold generic edge and texture features.
- When to rebuild the optimizer?
- Each time the set of trainable parameters changes.