Intermediate

Category 2: CNNs

The CNN category tests your ability to build convolutional neural networks for image classification. You must handle image preprocessing, build CNN architectures, apply transfer learning with pre-trained models, and use data augmentation to improve accuracy.

What the Exam Tests

You will receive image datasets (often loaded via ImageDataGenerator or tf.keras.utils.image_dataset_from_directory) and must build models that classify images above a target accuracy. Tasks range from simple binary classification (cats vs dogs) to multi-class problems with many categories.

💡
Exam tip: If a CNN from scratch is not reaching the accuracy threshold, switch to transfer learning immediately. Using a pre-trained MobileNetV2 or InceptionV3 base with frozen weights and a custom classification head is the fastest path to high accuracy on the exam.

Practice Model 1: CNN from Scratch

Build a convolutional neural network from scratch for image classification. This is the foundational pattern for all CNN exam tasks.

import tensorflow as tf

# ---- Load a dataset (Fashion MNIST as exam-like example) ----
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.fashion_mnist.load_data()

# ---- Preprocess ----
# Normalize pixel values to [0, 1]
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0

# Add channel dimension: (28, 28) -> (28, 28, 1)
x_train = x_train[..., tf.newaxis]
x_test = x_test[..., tf.newaxis]

print(f"Training shape: {x_train.shape}")   # (60000, 28, 28, 1)
print(f"Classes: {len(set(y_train))}")       # 10

# ---- Build CNN ----
model = tf.keras.Sequential([
    # Block 1
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
    tf.keras.layers.MaxPooling2D((2, 2)),

    # Block 2
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),

    # Block 3
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),

    # Classification head
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(10, activation='softmax')
])

model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# ---- Train ----
history = model.fit(
    x_train, y_train,
    validation_data=(x_test, y_test),
    epochs=20,
    batch_size=64,
    callbacks=[
        tf.keras.callbacks.EarlyStopping(
            monitor='val_accuracy', patience=3,
            restore_best_weights=True
        )
    ]
)

model.save('fashion_cnn.h5')

Practice Model 2: Transfer Learning

Transfer learning is the most powerful technique for the exam. Use a pre-trained model as a feature extractor and train only the classification head.

import tensorflow as tf

# ---- Transfer Learning with MobileNetV2 ----
# This pattern works for ANY image classification exam task

IMG_SIZE = 160  # MobileNetV2 expects at least 96x96
BATCH_SIZE = 32
NUM_CLASSES = 5  # Adjust based on exam task

# ---- Data augmentation (critical for small datasets) ----
data_augmentation = tf.keras.Sequential([
    tf.keras.layers.RandomFlip("horizontal"),
    tf.keras.layers.RandomRotation(0.2),
    tf.keras.layers.RandomZoom(0.2),
])

# ---- Load pre-trained base model ----
base_model = tf.keras.applications.MobileNetV2(
    input_shape=(IMG_SIZE, IMG_SIZE, 3),
    include_top=False,        # Remove classification head
    weights='imagenet'        # Pre-trained on ImageNet
)

# FREEZE the base model - do not train these weights
base_model.trainable = False

# ---- Build the complete model ----
model = tf.keras.Sequential([
    # Preprocessing
    tf.keras.layers.Resizing(IMG_SIZE, IMG_SIZE),
    tf.keras.layers.Rescaling(1./255),
    data_augmentation,

    # Pre-trained feature extractor
    base_model,

    # Custom classification head
    tf.keras.layers.GlobalAveragePooling2D(),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(NUM_CLASSES, activation='softmax')
])

model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# ---- Train (only the classification head trains) ----
# Replace x_train/y_train with your exam dataset
# history = model.fit(
#     train_dataset,
#     validation_data=val_dataset,
#     epochs=10,
#     callbacks=[
#         tf.keras.callbacks.EarlyStopping(
#             monitor='val_accuracy', patience=3,
#             restore_best_weights=True
#         )
#     ]
# )

# ---- Optional: Fine-tune top layers of base model ----
# If accuracy is still too low, unfreeze the last 20 layers
base_model.trainable = True
for layer in base_model.layers[:-20]:
    layer.trainable = False

# Re-compile with lower learning rate for fine-tuning
model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# Continue training (fine-tune)
# history_fine = model.fit(
#     train_dataset,
#     validation_data=val_dataset,
#     epochs=10
# )

model.save('transfer_learning_cnn.h5')

Practice Model 3: ImageDataGenerator Pattern

The exam often provides data in directory format. This pattern loads images from directories with augmentation.

import tensorflow as tf

# ---- Loading images from directories ----
# Exam datasets are often structured as:
# data/
#   train/
#     class_a/  (images)
#     class_b/  (images)
#   validation/
#     class_a/  (images)
#     class_b/  (images)

IMG_SIZE = (150, 150)
BATCH_SIZE = 32

# Method 1: ImageDataGenerator (older but still valid for exam)
train_datagen = tf.keras.preprocessing.image.ImageDataGenerator(
    rescale=1./255,
    rotation_range=40,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,
    fill_mode='nearest'
)

val_datagen = tf.keras.preprocessing.image.ImageDataGenerator(
    rescale=1./255  # Only rescale for validation - NO augmentation
)

# train_generator = train_datagen.flow_from_directory(
#     'data/train',
#     target_size=IMG_SIZE,
#     batch_size=BATCH_SIZE,
#     class_mode='categorical'  # or 'binary' for 2 classes
# )

# val_generator = val_datagen.flow_from_directory(
#     'data/validation',
#     target_size=IMG_SIZE,
#     batch_size=BATCH_SIZE,
#     class_mode='categorical'
# )

# Method 2: tf.keras.utils (newer, preferred)
# train_ds = tf.keras.utils.image_dataset_from_directory(
#     'data/train',
#     image_size=IMG_SIZE,
#     batch_size=BATCH_SIZE
# )

# ---- CNN for directory-loaded data ----
model = tf.keras.Sequential([
    tf.keras.layers.Conv2D(32, (3,3), activation='relu', input_shape=(150, 150, 3)),
    tf.keras.layers.MaxPooling2D(2, 2),
    tf.keras.layers.Conv2D(64, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2, 2),
    tf.keras.layers.Conv2D(128, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2, 2),
    tf.keras.layers.Conv2D(128, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2, 2),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(512, activation='relu'),
    tf.keras.layers.Dense(5, activation='softmax')  # Adjust num classes
])

model.compile(
    optimizer='adam',
    loss='categorical_crossentropy',  # categorical for one-hot from generator
    metrics=['accuracy']
)

# history = model.fit(
#     train_generator,
#     validation_data=val_generator,
#     epochs=25,
#     callbacks=[tf.keras.callbacks.EarlyStopping(patience=3)]
# )

model.save('image_dir_cnn.h5')

Key Takeaways

💡
  • Always normalize pixel values to [0, 1] by dividing by 255 — raw pixel values will not train well
  • Use Conv2D → MaxPooling2D blocks, then Flatten → Dense for the classification head
  • Apply data augmentation only to training data, never to validation or test data
  • Transfer learning with MobileNetV2 is the fastest path to high accuracy on exam tasks
  • Freeze the base model first, train the head, then optionally fine-tune top layers with a low learning rate
  • Match class_mode in ImageDataGenerator to your loss function: 'binary' with binary_crossentropy, 'categorical' with categorical_crossentropy