Convolutional Neural Networks¶
CNNs are the backbone of computer vision and appear in interviews for roles involving images, video, medical imaging, and any domain where spatial structure matters. Interviewers probe whether you understand what CNNs are actually computing — not just which architecture to import.
Q1: What does a convolution operation do?¶
Show answer
A convolution operation slides a small learnable filter (kernel) across the input, computing the dot product between the filter and each local region of the input it overlaps. The result is a feature map that shows where and how strongly each pattern (that the filter detects) is present in the input.
Key parameters: - Filter size (kernel size): e.g. 3×3 or 5×5. Controls how large a neighbourhood each output element sees. - Stride: how many pixels the filter moves between positions. Stride 1 → dense output. Stride 2 → halves spatial dimensions. - Padding: adding zeros around the input border. "Same" padding preserves spatial dimensions; "valid" padding shrinks them.
import tensorflow as tf
# Single conv layer: 32 filters, each 3x3
conv = tf.keras.layers.Conv2D(
filters=32,
kernel_size=(3, 3),
strides=(1, 1),
padding='same', # output same spatial size as input
activation='relu'
)
# Input: batch of images, shape (batch, height, width, channels)
x = tf.random.normal((8, 28, 28, 1)) # 8 grayscale 28x28 images
out = conv(x)
print(out.shape) # (8, 28, 28, 32) — 32 feature maps, same spatial size
Each of the 32 filters learns to detect a different pattern. A 3×3 filter on a single-channel image has 3×3×1 + 1 = 10 parameters. On a 3-channel RGB image: 3×3×3 + 1 = 28 parameters.
Q2: Why use convolutions instead of fully connected layers for images?¶
Show answer
Two properties of convolutions make them far better suited to images than fully connected layers:
1. Parameter sharing: A fully connected layer connecting a 224×224 RGB image (150,528 inputs) to 1,000 neurons would have 150,528,000 parameters — just in the first layer. A convolutional layer with 64 filters of size 3×3 has 64 × (3×3×3 + 1) = 1,792 parameters. The same filter is applied at every spatial location — this is parameter sharing. It works because the same edge detector is useful everywhere in an image, not just in one corner.
2. Translation equivariance: If the input shifts, the feature map shifts by the same amount but the detected features are the same. A cat in the left corner and a cat in the right corner should produce similar filter responses — fully connected layers don't have this property (a different set of weights fires for each spatial position).
# Fully connected — no spatial awareness, enormous parameter count
fc_layer = tf.keras.layers.Dense(1000) # 150,528 * 1000 = 150M params on a 224x224 image
# Convolutional — spatially aware, vastly fewer parameters
conv_layer = tf.keras.layers.Conv2D(64, (3, 3), padding='same')
# 3*3*3*64 + 64 = 1,792 params
The consequence: CNNs are data-efficient for images. They generalise from seeing a pattern at one location to recognising it everywhere.
Q3: What is a receptive field?¶
Show answer
The receptive field of a neuron is the region of the original input image that influences its output value.
- A single 3×3 conv layer: each output neuron sees a 3×3 region of the input
- Two stacked 3×3 conv layers: each output neuron sees a 5×5 region (the 3×3 output of layer 2 each came from a 3×3 area of layer 1's output, which each saw a 3×3 area of the input → 3 + 2 = 5)
- Three stacked 3×3 conv layers: 7×7 receptive field
Why receptive field matters: - Early layers detect local patterns (edges, corners) — small receptive field - Deeper layers must integrate information over larger regions to detect objects — they need larger receptive fields - Depth is how CNNs build up from local to global representations
Growing receptive field without depth: - Use larger kernels (5×5, 7×7) — but more parameters per layer - Use dilated (atrous) convolutions — insert gaps in the filter to skip input positions, expanding the receptive field without adding parameters
# Dilated convolution — receptive field grows without depth or large kernels
dilated_conv = tf.keras.layers.Conv2D(
filters=64,
kernel_size=(3, 3),
dilation_rate=2, # skip 1 input position between each filter element
padding='same'
)
# A 3x3 filter with dilation=2 has the same receptive field as a 5x5 filter
# but with only 9 learnable weights instead of 25
Q4: What is pooling and why is it used?¶
Show answer
Pooling reduces the spatial dimensions of feature maps by summarising regions of the input.
Max pooling: takes the maximum value in each pooling window. Standard choice. Average pooling: takes the mean value. Used in specific architectures (e.g., global average pooling at the network's end).
# Max pooling: 2x2 window, stride 2 → halves spatial dimensions
pool = tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2, 2))
x = tf.random.normal((8, 28, 28, 32))
out = pool(x)
print(out.shape) # (8, 14, 14, 32) — spatial dims halved
# Global average pooling: collapses each feature map to a single number
gap = tf.keras.layers.GlobalAveragePooling2D()
out = gap(x)
print(out.shape) # (8, 32)
Why use pooling: - Reduces spatial resolution → fewer parameters in subsequent layers - Introduces a degree of translation invariance — a pattern shifted by 1–2 pixels produces the same max-pool output - Expands the effective receptive field
Why global average pooling at the end? Instead of flattening a feature map and passing through large fully connected layers, global average pooling computes the mean of each feature map. This dramatically reduces parameters and acts as a regulariser. Used in ResNet, Inception, and most modern architectures.
Max vs Average: max pooling retains the presence of the most activated feature — useful when you care whether a feature is present. Average pooling summarises the overall activation strength — more informative when you care about how much a feature dominates the region.
Q5: What is the standard CNN architecture pattern?¶
Show answer
The canonical CNN architecture follows this progression:
Input
→ [Conv → BatchNorm → ReLU → Pool] × N (feature extraction)
→ Flatten or GlobalAveragePooling
→ [Dense → ReLU → Dropout] × M (classification head)
→ Dense → Softmax (output)
import tensorflow as tf
model = tf.keras.Sequential([
# Block 1
tf.keras.layers.Conv2D(32, (3, 3), padding='same', input_shape=(28, 28, 1)),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('relu'),
tf.keras.layers.MaxPooling2D((2, 2)),
# Block 2
tf.keras.layers.Conv2D(64, (3, 3), padding='same'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('relu'),
tf.keras.layers.MaxPooling2D((2, 2)),
# Block 3
tf.keras.layers.Conv2D(128, (3, 3), padding='same'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('relu'),
# Classification head
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(10, activation='softmax')
])
model.summary()
Typical design decisions: - Double the number of filters at each pooling step (32 → 64 → 128 → 256) - Pooling or strided convolutions halve spatial dimensions at each stage - Use global average pooling rather than flattening to reduce parameters and improve generalisation
Q6: What did LeNet, VGG, and ResNet each introduce?¶
Show answer
LeNet (1989, LeCun): The first successful deep CNN. Designed for handwritten digit recognition (MNIST). Established the pattern: convolutional layers for feature extraction → pooling → fully connected layers. Used sigmoid/tanh activations and average pooling. Architecturally simple by modern standards but proved the concept.
VGG (2014, Simonyan & Zisserman): Demonstrated that depth using only small 3×3 filters dramatically outperforms wider, shallower networks with larger filters. Two stacked 3×3 conv layers have the same receptive field as one 5×5 layer but fewer parameters and an extra non-linearity. VGG-16 (16 weight layers) and VGG-19 became standard transfer learning baselines. Limitation: ~138M parameters — very large, slow to train.
ResNet (2015, He et al.): Solved the degradation problem: very deep networks (>20 layers) trained with standard backprop were harder to optimise than shallower ones, even with BatchNorm. ResNet introduced residual (skip) connections — adding the input of a block directly to its output — enabling networks with 50, 101, even 152 layers to train successfully. Residual connections provide a gradient highway: gradients can flow directly through skip connections, bypassing potentially problematic transformations.
import tensorflow as tf
# Residual block implementation
def residual_block(x, filters, stride=1):
shortcut = x
x = tf.keras.layers.Conv2D(filters, (3, 3), strides=stride, padding='same')(x)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.Activation('relu')(x)
x = tf.keras.layers.Conv2D(filters, (3, 3), padding='same')(x)
x = tf.keras.layers.BatchNormalization()(x)
# If dimensions changed, project shortcut to match
if stride != 1 or shortcut.shape[-1] != filters:
shortcut = tf.keras.layers.Conv2D(filters, (1, 1), strides=stride)(shortcut)
x = tf.keras.layers.Add()([x, shortcut])
x = tf.keras.layers.Activation('relu')(x)
return x
Q7: Why do residual connections work?¶
Show answer
Without residual connections, gradients must flow through every layer in sequence. In a deep network, these gradients are multiplied repeatedly — they can vanish (become negligibly small) long before reaching early layers, which then receive no useful update signal.
A residual connection adds the input directly to the output of a block:
whereF(x) is the transformation applied by the block.
Why this helps:
- Gradient can flow directly through the identity (addition) operation without being modified
- The gradient of the loss with respect to an early layer's input becomes:
∂L/∂x = ∂L/∂output · (∂F/∂x + 1)
- The +1 term ensures the gradient is never smaller than ∂L/∂output — no matter how many layers are stacked, the gradient highway remains open
What the block learns: instead of learning a full transformation H(x), the block learns the residual F(x) = H(x) - x. If the optimal transformation is close to the identity, the block can learn F(x) ≈ 0 — an easier target than learning the identity mapping from scratch.
Effect in practice: ResNets can be trained with hundreds of layers without degradation. ResNet-50 and ResNet-152 are still widely used as backbone networks.
Q8: How does transfer learning work specifically with CNNs?¶
Show answer
CNNs trained on ImageNet develop a hierarchy of features: - Early layers: edges, colour gradients, simple textures - Middle layers: patterns, shapes, object parts - Late layers: class-specific features (dog ears, car wheels)
Transfer learning reuses this hierarchy for a new task.
import tensorflow as tf
base_model = tf.keras.applications.EfficientNetB0(
weights='imagenet',
include_top=False,
input_shape=(224, 224, 3)
)
# Freeze all but the last 20 layers
base_model.trainable = True
for layer in base_model.layers[:-20]:
layer.trainable = False
inputs = tf.keras.Input(shape=(224, 224, 3))
x = base_model(inputs, training=False)
x = tf.keras.layers.GlobalAveragePooling2D()(x)
x = tf.keras.layers.Dropout(0.3)(x)
outputs = tf.keras.layers.Dense(num_classes, activation='softmax')(x)
model = tf.keras.Model(inputs, outputs)
model.compile(
optimizer=tf.keras.optimizers.Adam(1e-4),
loss='categorical_crossentropy',
metrics=['accuracy']
)
Decision guide:
| Your dataset size | Similarity to ImageNet | Strategy |
|---|---|---|
| Small | High | Freeze all base layers, train only head |
| Small | Low | Freeze early layers, fine-tune last few |
| Large | High | Fine-tune the full network with low LR |
| Large | Low | Consider training from scratch or fine-tune all |
Why freeze early layers? Early layer features (edges, textures) are universal. Fine-tuning them on a small dataset will overwrite useful generic features with noisy task-specific ones.
Q9: What data augmentation techniques are used for images?¶
Show answer
Data augmentation creates new training examples by applying transformations that preserve the label but change the pixel values. This artificially expands the training set and improves generalisation.
import tensorflow as tf
# Keras built-in augmentation layers (applied during training only)
data_augmentation = tf.keras.Sequential([
tf.keras.layers.RandomFlip('horizontal'), # random left-right flip
tf.keras.layers.RandomRotation(0.1), # ±10% of 360° rotation
tf.keras.layers.RandomZoom(0.1), # ±10% zoom in/out
tf.keras.layers.RandomTranslation(0.1, 0.1), # ±10% translation
tf.keras.layers.RandomContrast(0.2), # contrast jitter
])
# Apply as first layer in model (runs on GPU)
model = tf.keras.Sequential([
data_augmentation,
tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),
# ... rest of model
])
Common augmentation strategies by domain: - Natural images: horizontal flip, crop, colour jitter, Gaussian noise - Medical imaging: careful with flips (anatomical orientation matters), prefer intensity variations - Satellite images: all rotations valid (no canonical "up"), flips in all directions
Advanced techniques: - CutOut: randomly mask rectangular regions of the input → forces learning from all parts of the image - MixUp: linearly interpolate two images and their labels → smoother decision boundaries - RandAugment: automatically search over a policy of augmentation types and magnitudes
Augmentation is a form of regularisation: it prevents the network from memorising specific training examples and forces it to learn features that are robust to reasonable variations.
Q10: What do different layers of a CNN actually learn?¶
Show answer
The features learned at each depth level have been studied by visualising which inputs maximally activate each filter (DeepDream, activation maximisation, feature visualisation).
Layer 1 (immediately after input): - Oriented edge detectors (Gabor-like filters) - Colour blobs - Very local, primitive features — similar across all CNN architectures
Layers 2–3: - Textures and patterns (grids, repeating structures) - Combinations of edge orientations - Corners and junctions
Layers 4–6: - Object parts: eyes, wheels, windows, snouts - Increasingly specific to the training domain
Final conv layers: - Whole objects or class-discriminative features - Highly abstract, highly task-specific
This is why early layers transfer well across tasks (they are generic) while late layers are task-specific and benefit more from fine-tuning on the target domain.
Q11: When should you NOT use a CNN?¶
Show answer
CNNs are excellent when the data has spatial or local structure that is translation-equivariant. They are a poor choice when:
Tabular data: Columns in a tabular dataset have no spatial relationship — column 3 and column 7 are not "neighbours" in any meaningful sense. Applying convolutions imposes a false spatial structure. Tree-based models (XGBoost, LightGBM) and MLPs are more appropriate.
Sequential data without spatial structure: Text, time series, and audio have temporal structure but not spatial structure in the image sense. While 1D convolutions are used for text and time series, Transformers and RNNs are often more effective because they model long-range dependencies better.
Very small datasets: CNNs with many layers have large parameter counts. Without transfer learning, they overfit severely on small datasets. Use a pre-trained model or a simpler model.
Graphs and irregular data: Standard CNNs assume regular grid structure (images). For irregular data like molecular graphs or social networks, Graph Neural Networks (GNNs) are appropriate.
Q12: How do you count parameters in a convolutional layer?¶
Show answer
For a Conv2D layer:
import tensorflow as tf
# Example: 64 filters of size 3x3, input has 32 channels
conv = tf.keras.layers.Conv2D(64, (3, 3), input_shape=(28, 28, 32))
# Parameters: (3 * 3 * 32 * 64) + 64 = 18,432 + 64 = 18,496
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, (3, 3), padding='same', input_shape=(28, 28, 1)),
tf.keras.layers.Conv2D(64, (3, 3), padding='same'),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(128),
tf.keras.layers.Dense(10)
])
model.summary()
# Layer 1: (3*3*1*32) + 32 = 320 params
# Layer 2: (3*3*32*64) + 64 = 18,496 params
# Flatten output: 28*28*64 = 50,176 units
# Dense 1: (50176*128) + 128 = 6,422,656 params — the bottleneck!
# Dense 2: (128*10) + 10 = 1,290 params
Key insight: the fully connected layers after flattening contain the vast majority of parameters. This is why global average pooling is preferred — it collapses each feature map to a single number, eliminating the costly flatten → dense transition and drastically reducing parameter count.