Table of Contents
- Course Introduction
- Preparing Image Data for Deep Learning
- 1.1 Detect pneumonia from chest X-rays
- 1.2 Organization of Train, Validation and Test sets
- 1.3 Image Preprocessing Essentials
- 1.4 Data quality visualization and validation
- Module 2: Construction and training of the CNN model
- 2.1 Training pipeline and preprocessing
- 2.2 Construction of a CNN from scratch
- 2.3 Exploitation of pre-trained models with Transfer Learning
- 2.4 Model performance evaluation
1. Course Introduction
Every day, emergencies around the world face a critical challenge. A child arrives with a persistent cough and fever. An elderly patient has difficulty breathing. The clock is ticking, and doctors need to know: is it pneumonia?
The traditional approach is to have a radiologist review chest X-rays, looking for telltale signs: cloudy areas in the lungs, fluid buildup, and patterns of inflammation. But here’s the thing: There are more than 450 million cases of pneumonia worldwide each year. In many hospitals, especially in underserved areas, there simply aren’t enough radiologists. Even in well-staffed facilities, sheer volume can create dangerous backlogs. A late diagnosis not only means a longer wait, it can literally mean the difference between life and death.
But what if we could give doctors a powerful ally? What if artificial intelligence could analyze these x-rays in seconds, flagging potential cases of pneumonia for immediate review? This is exactly what we will build together in this course.
Course outline
-
Module 1: We focus on data. You learn how to prepare and preprocess image datasets, covering everything from normalization and resizing to data augmentation. We’ll also look at how to properly split your data into train, validation, and test sets to ensure you’re not cheating with the model.
-
Module 2: We move on to architecture. We will build Convolutional Neural Networks (CNNs) from scratch. We’re not going to stop there. I’ll also show you how to harness the power of transfer learning using pre-trained models like ResNet. Finally, we will learn how to evaluate the performance of our model using confusion matrices and accuracy metrics to ensure that our predictions are sufficiently reliable for a medical context.
2. Preparing Image Data for Deep Learning
1.1 Detect pneumonia from chest x-rays
Now that we understand the problem we’re going to solve, let’s talk about the data we’ll be working with and what makes an image dataset suitable for deep learning.
The dataset
We will use a dataset publicly available on Kaggle which contains 5,856 chest x-rays from Guangzhou Women and Children’s Medical Center. This is real medical data: pediatric chest X-rays of patients ages 1 to 5, carefully annotated by expert doctors to identify cases of pneumonia versus normal, healthy lungs.
The dataset is divided into training and testing sets. Before continuing, it is important to acknowledge the researchers who have made this data available under a Creative Commons license. Open datasets like this are invaluable for advancing research and education in medical AI.
How computers see images
To you and me, an x-ray is a picture of the lungs, but to a computer, it’s just a grid of numbers. Imagine a chessboard, but instead of black and white squares, each square has a number representing the brightness of that tiny dot. We call these boxes pixels. An X-ray image can be 1,024 pixels wide and 1,024 pixels high, or more than a million small numbers for the computer to process. It is this transformation of a visual image into a digital grid that makes deep learning possible.
1.2 Organization of the Train, Validation and Test sets
Why three separate sets?
Imagine you are studying for an exam. If you only practice the exact same questions that will appear on the test, you might memorize the answers without really understanding the material — this is called overfitting in machine learning. We need our model to generalize, that is, to work on x-rays it has never seen before, and not just memorize the training images.
That’s why we set aside some data that the model never uses to train, so that we can honestly assess whether it has learned to recognize pneumonia or is just memorizing specific images.
| Together | Proportion | Role |
|---|---|---|
| Training set | 70-80% | What your model learns. The model sees these images repeatedly during training, adjusting its internal parameters to recognize patterns that distinguish pneumonia from normal lungs. |
| Validation set | 10-15% | Your reality check during training. After each round of training, you test the model on these unseen images to see if it actually learns generalizable patterns or memorizes the training data. This helps you adjust hyperparameters and decide when to stop training. |
| Test set | 10-15% | Your final exam. You only use it once at the very end to get an honest assessment of how your model will perform in the real world. If you continue to adjust your model based on test set performance, it ceases to be a true measure of generalization. |
Downloading and exploring the dataset
The dataset is available on Kaggle. To upload it directly to Google Colab:
import kagglehub
# Téléchargement de la dernière version
path = kagglehub.dataset_download("tolgadincer/labeled-chest-xray-images")
print("Path to dataset files:", path)
Once downloaded, let’s import the dependencies and explore the structure:
import os
import glob
import numpy as np
from sklearn.model_selection import train_test_split
import pandas as pd
# Lister le contenu du dataset
contents = os.listdir(path)
print("Dataset contents:", contents)
# Les données réelles sont dans le dossier chest_xray
chest_xray_path = os.path.join(path, 'chest_xray')
print(os.listdir(chest_xray_path))
train_dir = os.path.join(chest_xray_path, 'train')
test_dir = os.path.join(chest_xray_path, 'test')
# Vérification de nos catégories
print("Training categories:", os.listdir(train_dir))
print("Testing categories", os.listdir(test_dir))
This reveals two folders: NORMAL and PNEUMONIA.
Counting frames and creating DataFrames
# Récupération de tous les chemins d'images avec glob
train_normal = glob.glob(train_dir + "/NORMAL/*.jpeg")
train_pneumonia = glob.glob(train_dir + "/PNEUMONIA/*.jpeg")
test_normal = glob.glob(test_dir + "/NORMAL/*.jpeg")
test_pneumonia = glob.glob(test_dir + "/PNEUMONIA/*.jpeg")
print(f"\nTraining set:")
print(f" Normal: {len(train_normal)} images")
print(f" Pneumonia: {len(train_pneumonia)} images")
print(f"\nTest set:")
print(f" Normal: {len(test_normal)} images")
print(f" Pneumonia: {len(test_pneumonia)} images")
What immediately stands out is that in training we have about three times as many pneumonia cases as normal cases. This imbalance is something we need to take into account when training our model.
Now let’s create DataFrames to organize this data. A DataFrame gives us a nice tabular structure, where each line represents an image with its path and label:
# Création du DataFrame d'entraînement
train_list = train_normal + train_pneumonia
train_labels = ['Normal'] * len(train_normal) + ['Pneumonia'] * len(train_pneumonia)
df_train = pd.DataFrame({
'image': train_list,
'class': train_labels
})
# Création du DataFrame de test
test_list = test_normal + test_pneumonia
test_labels = ['Normal'] * len(test_normal) + ['Pneumonia'] * len(test_pneumonia)
df_test = pd.DataFrame({
'image': test_list,
'class': test_labels
})
# Affichage des premières lignes
df_train.head()
Creating validation set with stratification
# Diviser les données d'entraînement en sets train et validation
train_df, val_df = train_test_split(
df_train,
test_size=0.2,
random_state=7,
stratify=df_train["class"]
)
print("Split complete!")
print(f"Training set: {len(train_df)} images")
print(f"Validation set: {len(val_df)} images")
We use 80% of df_train to create the training set and 20% to create the validation set. stratification based on the classes of the original DataFrame ensures that the proportions of each class are maintained in both sets. Also note that train_test_split shuffles the dataset by default, so we won’t just have normal or pneumonia cases on one side of the dataset.
1.3 Image Preprocessing Essentials
Now that we’ve uploaded our dataset and organized it into the appropriate train, validation, and test divisions, before we can feed these chest x-rays into a neural network, we need to understand the essential preprocessing steps that prepare the raw images for deep learning.
There are three fundamental preprocessing techniques that you will use in virtually every computer vision project:
- Resizing images to uniform dimensions
- Normalization of pixel values to an optimal range
- Augmentation of your data to increase diversity
Resizing
If you look at our pneumonia dataset, it contains x-ray images of varying dimensions: some are 1654 x 1275 pixels, while others are 1460 x 951 pixels. But here is the fundamental constraint: neural networks require inputs of fixed size. The first layer of your network has a specific number of neurons, for example 224 x 224 x 3 for RGB images.
Beyond this technical requirement, there are also practical advantages: smaller images mean faster training and less memory usage.
The most common target dimensions are: – 224 × 224 pixels – 256 × 256 pixels – 299 × 299 pixels – 512 × 512 pixels
Normalization
Recall that each pixel in a grayscale image has a value between 0 and 255, where 0 represents black and 255 represents white. These integer values are how images are naturally stored. But neural networks don’t work well with these raw values — they learn better when the inputs are scaled to a smaller, consistent range, typically between 0 and 1 (or sometimes between -1 and 1).
Large values like 255 can cause numerical instability during training, leading to exploding gradients or other optimization issues.
Normalization improves training in several ways:
- Gradient stability: During backpropagation, the gradients flowing through your network remain within a reasonable range, making the optimization process smoother and more reliable.
- Using standard learning rates: If your input values are all over the place, you have to constantly adjust your learning rate, which is tedious and error-prone.
The most common approach is simple: divide all pixel values by 255. This scales your range from 0 to 255 to 0 to 1.
Data Augmentation
Data augmentation is a technique to alter existing data to create more for the model training process, through different processing methods such as rotations, shifts, flips, etc.
Even though we have almost 6,000 chest x-rays, that’s actually relatively few for deep learning. Modern CNNs have millions of parameters and are data hungry. Without enough diverse training examples, your model overfits, which means poor performance on new, unseen images.
Common augmentation techniques include:
- Rotation
- Horizontal flip
- Zoom
- Shifting
- Brightness adjustments
The key is that these transformations preserve diagnostic content while introducing variation. For example, rotating a chest X-ray 10 degrees doesn’t change whether pneumonia is present — it’s still the same lung condition, just viewed from a slightly different angle.
Important: We only increase the training set. Validation and test sets are never increased. We want to evaluate the model on clean, unaltered images to get an honest assessment of performance.
1.4 Data quality visualization and validation
Before we move on to building our model, let’s quickly visualize our data to check that everything is ready. We look at class distributions and sample images to detect any potential issues early. This validation step only takes a few minutes, but can save you hours of debugging later.
By visualizing class distributions and example images, we can spot imbalances, quality issues, or mislabeled examples.
Visualizing class distributions
import matplotlib.pyplot as plt
import seaborn as sns
from PIL import Image
plt.figure(figsize=(15, 4))
# Set d'entraînement
plt.subplot(1, 3, 1)
sns.countplot(x="class", data=train_df, palette="mako")
plt.title('Training Set', fontsize=12, fontweight='bold')
plt.ylabel('Count')
# Set de validation
plt.subplot(1, 3, 2)
sns.countplot(x='class', data=val_df, palette='mako')
plt.title('Validation Set', fontsize=12, fontweight='bold')
plt.ylabel('Count')
# Set de test
plt.subplot(1, 3, 3)
sns.countplot(x='class', data=df_test, palette='mako')
plt.title('Test Set', fontsize=12, fontweight='bold')
plt.ylabel('Count')
plt.tight_layout()
plt.show()
The training and validation sets maintain similar proportions through stratification, while the test set has a slightly different distribution, which is correct.
Viewing sample images
# Affichage d'exemples d'images des deux classes
fig, axes = plt.subplots(2, 6, figsize=(15, 6))
# Récupération des exemples
normal_samples = train_df[train_df['class'] == 'Normal'].sample(6, random_state=7)
pneumonia_samples = train_df[train_df['class'] == 'Pneumonia'].sample(6, random_state=7)
# Affichage des radiographies normales (ligne du haut)
for idx, (i, row) in enumerate(normal_samples.iterrows()):
img = Image.open(row['image'])
axes[0, idx].imshow(img, cmap='gray')
axes[0, idx].set_title('Normal', fontsize=10)
axes[0, idx].axis('off')
# Affichage des radiographies avec pneumonie (ligne du bas)
for idx, (i, row) in enumerate(pneumonia_samples.iterrows()):
img = Image.open(row['image'])
axes[1, idx].imshow(img, cmap='gray')
axes[1, idx].set_title('Pneumonia', fontsize=10)
axes[1, idx].axis('off')
plt.suptitle('Sample Chest X-Rays: Normal (Top) vs Pneumonia (Bottom)',
fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
Note the visual differences: Normal x-rays show light and dark lung fields, while pneumonia x-rays show whitish opacities where fluid or consolidation has occurred. These are the patterns that our CNN will learn to recognize.
Checking image properties
# Vérification des dimensions des images
sample_imgs = train_df.sample(50, random_state=42)
dimensions = []
for img_path in sample_imgs['image']:
img = Image.open(img_path)
dimensions.append(img.size)
dimensions = np.array(dimensions)
print("\nImage Properties:")
print(f"Dimensions vary from {dimensions.min(axis=0)} to {dimensions.max(axis=0)}")
print(f"Image mode: {Image.open(train_df.iloc[0]['image']).mode} (Grayscale)")
print(f"Total unique dimensions: {len(set(map(tuple, dimensions)))}")
Data quality checklist
To conclude this module, here is a data quality verification checklist:
- ✅ Dataset divided into train, validation and test sets
- ✅ Examined class distributions — small manageable imbalance
- ✅ Example images showing clear visual differences between classes
- ✅ Grayscale images with variable dimensions
- ✅ No obvious quality issues or corrupted files
3. CNN model construction and training
2.1 Training pipeline and preprocessing
We have prepared our chest x-ray data, and now it is time to build and train the models that will learn to detect pneumonia. Before we design the model itself, let’s build a training pipeline that will feed the data into our model.
ImageDataGenerator
The most important tool for this is ImageDataGenerator. It is a powerful Keras tool that handles all our preprocessing automatically and efficiently. Instead of manually resizing and normalizing thousands of images and storing them on disk, ImageDataGenerator processes images on the fly as they are loaded during training.
from tensorflow.keras.preprocessing.image import ImageDataGenerator
import tensorflow as tf
# Définition des paramètres
IMG_SIZE = 224 # Taille cible pour toutes les images
BATCH_SIZE = 32 # Nombre d'images par batch d'entraînement
Instead of feeding images one by one into our model, we group them into batches. A BATCH_SIZE of 32 means we process 32 images together before updating the model weights — this is more computationally efficient because GPUs are designed to process multiple images in parallel. This is a setting that you can also adjust.
Creating ImageDataGenerators
We need two generators: one for training with augmentation, and one for validation and testing without augmentation.
# Générateur de données d'entraînement avec augmentation
train_datagen = ImageDataGenerator(
rescale=1./255, # Normaliser les valeurs de pixels vers 0-1
zoom_range=0.1, # Zoom aléatoire jusqu'à 10%
width_shift_range=0.1, # Décalage horizontal aléatoire jusqu'à 10%
height_shift_range=0.1 # Décalage vertical aléatoire jusqu'à 10%
)
# Générateur de données validation/test - seulement le rescaling, sans augmentation
val_datagen = ImageDataGenerator(
rescale=1./255 # Seulement normaliser, sans augmentation
)
The rescale parameter divides each pixel by 255, converting our 0-255 range to 0-1 (the normalization). For the training generator, we add augmentation: zoom_range randomly zooms up to 10%, width_shift_range shifts images horizontally up to 10%, and height_shift_range does the same vertically. These operations create variations that help prevent overfitting.
Note: We are not using rotation here. Although some rotations might work for chest x-rays, we want our increases to be modest and anatomically reasonable.
Connecting generators to DataFrames
# Création du générateur de données d'entraînement
train_generator = train_datagen.flow_from_dataframe(
train_df,
x_col='image', # Colonne contenant les chemins d'images
y_col='class', # Colonne contenant les labels
target_size=(IMG_SIZE, IMG_SIZE), # Redimensionner toutes les images à 224x224
batch_size=BATCH_SIZE,
class_mode='binary', # Classification binaire (Normal vs Pneumonia)
seed=7
)
print("Training generator created!")
The flow_from_dataframe method reads image paths from our DataFrame, loads the images, resizes them to 224×224, applies our preprocessing transformations, and batches them — all of this happens automatically during training.
The class_mode='binary' means that the method will automatically convert all of our Normal and Pneumonia labels to 0 and 1.
# Création du générateur de données de validation
val_generator = val_datagen.flow_from_dataframe(
val_df,
x_col='image',
y_col='class',
target_size=(IMG_SIZE, IMG_SIZE),
batch_size=BATCH_SIZE,
class_mode='binary',
seed=7
)
print("Validation generator created!")
# Création du générateur de données de test
test_generator = val_datagen.flow_from_dataframe(
df_test,
x_col='image',
y_col='class',
target_size=(IMG_SIZE, IMG_SIZE),
batch_size=1, # Batch size de 1 pour l'évaluation sur le test
class_mode='binary',
shuffle=False, # Ne pas mélanger les données de test
seed=7
)
print("Test generator created!")
For the test generator, there are two differences: a batch_size of 1 (common for evaluation on the test) and shuffle=False to keep the test data in its original order, which can be useful for analyzing specific predictions later.
Checking the pipeline
# Récupération d'un batch d'exemple pour vérification
sample_batch, sample_labels = next(train_generator)
print("\nSample batch information:")
print(f"Batch shape: {sample_batch.shape}") # Devrait être (32, 224, 224, 3)
print(f"Pixel value range: {sample_batch.min()} to {sample_batch.max()}")
print(f"Labels shape: {sample_labels.shape}")
print(f"First few labels: {sample_labels[:5]}")
The batch.shape shows us the dimensions. We expect 32 images, each 224 × 224 pixels, with 3 color channels. The range of pixel values should be between 0 and 1, confirming that our normalization worked. And the labels should be an array of 0s and 1s representing our two classes.
Note: Even though our x-rays are in grayscale,
ImageDataGeneratorconverts grayscale images to RGB format (3 channels) by default. This is actually correct because many pre-trained models expect 3-channel RGB input.
2.2 Construction of a CNN from scratch
Now that our data pipeline is ready, it’s time to build our first Convolutional Neural Network from scratch. We will build a simple CNN with two convolutional blocks to establish a baseline.
Each block will have:
- A convolutional layer to detect features
- Max pooling for dimension reduction
- Dropout to prevent overfitting
After the convolutional blocks, we will flatten the output and add a dense layer for classification.
Why such a simple architecture? It is a deliberately simple architecture. We want to see what a basic CNN can accomplish before exploring more sophisticated approaches like transfer learning.
Imports required
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, Input
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
Model construction
# Construction du modèle
model = Sequential()
# Ajout de la couche d'entrée
model.add(Input(shape=(IMG_SIZE, IMG_SIZE, 3)))
# Premier bloc convolutif
model.add(Conv2D(8, (3, 3), activation='relu'))
model.add(MaxPooling2D(2, 2))
model.add(Dropout(0.2))
# Deuxième bloc convolutif
model.add(Conv2D(16, (3, 3), activation='relu'))
model.add(MaxPooling2D(2, 2))
model.add(Dropout(0.2))
# Couches de classification (Flatten et Dense)
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
print("Model created successfully!")
Architecture explanation:
- The Sequential API allows layers to be stacked one after the other.
- The Input` layer specifies that we will receive RGB images of 224 × 224 pixels.
- The first convolutional block: 8 filters of 3 × 3 pixels with ReLU activation, then
MaxPooling2D(2,2)which reduces the dimensions by half, andDropout(0.2)which randomly drops 20% of the connections during training to prevent overfitting. - The second block doubles the number of filters to 16, allowing the network to detect more complex combinations of the simple features from the first block.
Flattenconverts our 3D feature maps into 1D vector.- Dense(32) layer passes through 32 neurons with ReLU activation.
- The output layer
Dense(1, sigmoid)produces a single value between 0 and 1 representing the probability of pneumonia.
# Affichage de l'architecture du modèle
model.summary()
This summary shows each layer, its output shape, and the number of parameters. With less than 3 million parameters, this model has a relatively small number of parameters compared to deeper and more complex networks.
Model compilation
# Compilation du modèle
model.compile(
loss='binary_crossentropy',
optimizer=Adam(learning_rate=3e-5),
metrics=['binary_accuracy']
)
print("Model compiled!")
binary_crossentropy: standard loss function for binary classification.- Adam optimizer with a learning rate of 0.00003: helps the model learn carefully without overshooting.
binary_accuracy: Metric to track precision.
Configuring callbacks
# Configuration des callbacks
early_stopping = EarlyStopping(
monitor='val_loss',
patience=5,
restore_best_weights=True
)
reduce_lr = ReduceLROnPlateau(
monitor='val_loss',
factor=0.2,
patience=2,
min_lr=1e-7,
verbose=1
)
Callbacks are functions applied to different parts of the training procedure, allowing it to be controlled and monitored:
EarlyStopping: monitors val_loss and stops training if it does not improve for 5 consecutive epochs, then restores the weights of the best epoch.ReduceLROnPlateau: reduces the learning rate by 80% if the val_loss plateaus for 2 epochs, helping the model refine its weights when learning slows down.
Model training
# Entraînement du modèle
print("Starting training...")
history = model.fit(
train_generator,
epochs=50,
validation_data=val_generator,
callbacks=[early_stopping, reduce_lr],
verbose=1
)
print("\nTraining complete!")
# Sauvegarder le modèle avec un nom descriptif pour la comparaison ultérieure
baseline_model = model
epochs=50: the model can potentially see the entire training dataset 50 times (butEarlyStoppingwill stop before then if necessary).- The
validation_dataparameter allows you to monitor whether the model overfits or learns well after each epoch.
Visualization of learning curves
# Tracer l'historique d'entraînement
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Tracer la loss
axes[0].plot(history.history['loss'], label='Training Loss')
axes[0].plot(history.history['val_loss'], label='Validation Loss')
axes[0].set_title('Model Loss Over Epochs', fontsize=14, fontweight='bold')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Loss')
axes[0].legend()
axes[0].grid(True)
# Tracer la précision
axes[1].plot(history.history['binary_accuracy'], label='Training Accuracy')
axes[1].plot(history.history['val_binary_accuracy'], label='Validation Accuracy')
axes[1].set_title('Model Accuracy Over Epochs', fontsize=14, fontweight='bold')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Accuracy')
axes[1].legend()
axes[1].grid(True)
plt.tight_layout()
plt.show()
Learning curve analysis:
These curves reveal a clear overfitting problem. First look at the accuracy graph:
- Validation accuracy peaks at 93% around epoch 2 (even higher than training accuracy at this point), then steadily declines to around 77% at the end.
- Meanwhile, training accuracy continues to climb up to 89%.
- This growing gap is a classic sign of overfitting.
The loss graph confirms this: after epoch 5, the training loss continues to decline, while the validation loss rises from 0.27 to almost 0.50. The model memorizes training data rather than learning generalizable features.
This tells us that our simple CNN is too limited. With only 8 and 16 filters on two convolutional blocks, it found some basic patterns early, but lacked the ability to learn the sophisticated features needed for robust pneumonia detection.
Evaluation on the validation set
# Évaluer sur les données de validation
val_loss, val_accuracy = model.evaluate(val_generator, verbose=0)
print(f"\nValidation Results:")
print(f" Loss: {val_loss:.4f}")
print(f" Accuracy: {val_accuracy:.4f} ({val_accuracy*100:.2f}%)")
The accuracy is approximately 85.58%. This is what our simple CNN achieved, proving that the concept works. But what if we could do significantly better without having to design increasingly complex architectures from scratch?
2.3 Exploitation of pre-trained models with Transfer Learning
Our baseline CNN achieved around 85% accuracy, which is pretty decent, but shows signs of overfitting. To address this, let’s explore transfer learning, a powerful technique that allows us to leverage models trained on millions of images.
Concept of Transfer Learning
Transfer learning is based on a simple but powerful idea: features learned from one task can be useful for another task.
Models like ResNet were trained on ImageNet, a dataset with over 40 million images divided into thousands of categories. During this workout:
- Early layers learn to detect universal features like edges, textures and shapes.
- Middle layers learn to detect more complex patterns, such as corners, circles and parts of objects.
These features are not specific to ImageNet — they are useful for any image recognition task, including medical imaging. Instead of training all these feature detectors from scratch, we can start with a model that already knows them. We just have to adapt the final layers to our specific task: distinguishing pneumonia from normal chest x-rays.
Pre-trained architectures available
Here are some examples of pre-trained model architectures:
- VGG19: 19-layer deep architecture
- Inception-v3: architecture with parallel Inception modules
- Xception: improved version of Inception with depth-separable convolutions
- ResNet-50 / ResNet152V2: residual networks with jump connections
We will use ResNet152V2, a deep convolutional network with 152 layers.
Imports
from tensorflow.keras.applications import ResNet152V2
from tensorflow.keras.models import Model
from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout, Input
Loading base model
base_model = ResNet152V2(
weights='imagenet',
include_top=False,
input_shape=(IMG_SIZE, IMG_SIZE, 3)
)
base_model.trainable = False
print(f"Loaded ResNet152V2 with {len(base_model.layers)} layers")
weights='imagenet': we use the weights learned during training on ImageNet.include_top=False: The original ResNet classification head was designed to classify 1000 ImageNet categories. We don’t need it — we’ll add our own header for the binary pneumonia classification.base_model.trainable = False: This freezes all ResNet weights so that they are not modified during training. We use ResNet purely as a feature extractor.
Custom Head Build
inputs = Input(shape=(IMG_SIZE, IMG_SIZE, 3))
x = base_model(inputs, training=False)
x = GlobalAveragePooling2D()(x)
x = Dense(128, activation='relu')(x)
x = Dropout(0.3)(x)
outputs = Dense(1, activation='sigmoid')(x)
model = Model(inputs=inputs, outputs=outputs)
print("\nTransfer learning model created!")
Architecture explanation:
Note that we are using the Keras Functional API here (as opposed to the Sequential API used for our custom CNN). For transfer learning, we need the working API because we are composing a pre-trained model with our custom layers.
GlobalAveragePooling2D: UnlikeMaxPoolingwhich gradually reduces the dimensions by taking the maximum value in small windows,GlobalAveragePoolingcompletely collapses the spatial dimensions in a single step by averaging the entire feature map. This helps preserve important features while being much more efficient in terms of settings.Dense(128, relu): 128 neurons with ReLU activation.Dropout(0.3): regularization to prevent overfitting.Dense(1, sigmoid): binary output for pneumonia/normal classification.
model.summary()
Note the huge number of parameters (over 58 million), but the vast majority are frozen (marked as untrainable). We only train the final layers we added.
Compilation and training
# Compilation du modèle
model.compile(
loss='binary_crossentropy',
optimizer=Adam(learning_rate=1e-4), # Learning rate légèrement plus élevé qu'avant
metrics=['binary_accuracy']
)
print("Model compiled and ready to train!")
We use a learning rate of 0.0001, slightly higher than for our custom CNN. This is safe because we only train a small number of parameters in the head.
# Configuration des callbacks
early_stopping = EarlyStopping(
monitor='val_loss',
patience=5,
restore_best_weights=True
)
reduce_lr = ReduceLROnPlateau(
monitor='val_loss',
factor=0.2,
patience=2,
min_lr=1e-7,
verbose=1
)
# Entraînement du modèle
print("Starting transfer learning training...")
history = model.fit(
train_generator,
epochs=50,
validation_data=val_generator,
callbacks=[early_stopping, reduce_lr],
verbose=1
)
print("\nTraining complete!")
# Sauvegarder avec un nom descriptif
transfer_model = model
As you watch the training progress, you should notice something interesting: the validation accuracy starts very high, while it took a while for our custom CNN to reach that level. For what ? Because feature extraction is already sophisticated, new head layers can learn quickly.
Visualization of Transfer Learning learning curves
# Tracer l'historique d'entraînement
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Tracer la loss
axes[0].plot(history.history['loss'], label='Training Loss')
axes[0].plot(history.history['val_loss'], label='Validation Loss')
axes[0].set_title('Transfer Learning - Loss Over Epochs', fontsize=14, fontweight='bold')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Loss')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Tracer la précision
axes[1].plot(history.history['binary_accuracy'], label='Training Accuracy')
axes[1].plot(history.history['val_binary_accuracy'], label='Validation Accuracy')
axes[1].set_title('Transfer Learning - Accuracy Over Epochs', fontsize=14, fontweight='bold')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Accuracy')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Look at these curves — they are much smoother than our baseline CNN. The training and validation metrics improve together without the overfitting observed previously. Pre-trained features give the model a solid foundation to build on.
Evaluation on validation
# Évaluer sur les données de validation
val_loss, val_accuracy = transfer_model.evaluate(val_generator, verbose=0)
print(f"\nTransfer Learning Results:")
print(f" Loss: {val_loss:.4f}")
print(f" Accuracy: {val_accuracy:.4f} ({val_accuracy*100:.2f}%)")
Accuracy is now 96.56%, a big improvement over our custom CNN.
2.4 Model performance evaluation
We have trained the model and tested its performance on the validation set, but we have not yet evaluated its performance on our test set. Let’s evaluate both models on the test set to see the real difference.
Evaluation on the test set
from sklearn.metrics import confusion_matrix, classification_report
import seaborn as sns
# Évaluation des deux modèles
baseline_loss, baseline_acc = baseline_model.evaluate(test_generator, verbose=0)
transfer_loss, transfer_acc = transfer_model.evaluate(test_generator, verbose=0)
print(f"Baseline CNN Test Accuracy: {baseline_acc*100:.2f}%")
print(f"Transfer Learning Test Accuracy: {transfer_acc*100:.2f}%")
The difference is even more marked here: almost 18% difference in accuracy!
Generating predictions
# Récupération des prédictions
test_generator.reset()
baseline_preds = (baseline_model.predict(test_generator, verbose=0) > 0.5).astype(int).flatten()
test_generator.reset()
transfer_preds = (transfer_model.predict(test_generator, verbose=0) > 0.5).astype(int).flatten()
# Récupération des vrais labels
test_generator.reset()
true_labels = test_generator.classes
# Création des matrices de confusion
baseline_cm = confusion_matrix(true_labels, baseline_preds)
transfer_cm = confusion_matrix(true_labels, transfer_preds)
- The
predictmethod gives probabilities for each image (a value between 0 and 1). - We convert them to class predictions by checking if they are greater than 0.5 — if the probability is above 0.5 we predict pneumonia, otherwise normal.
- The
confusion_matrixcompares the true labels with the predicted labels and counts how many fall into each category: true positives, true negatives, false positives and false negatives.
Visualization of confusion matrices
# Visualisation des matrices de confusion
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
sns.heatmap(baseline_cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['Normal', 'Pneumonia'],
yticklabels=['Normal', 'Pneumonia'], ax=axes[0])
axes[0].set_title('Baseline CNN', fontsize=14, fontweight='bold')
axes[0].set_ylabel('True Label')
axes[0].set_xlabel('Predicted Label')
sns.heatmap(transfer_cm, annot=True, fmt='d', cmap='Greens',
xticklabels=['Normal', 'Pneumonia'],
yticklabels=['Normal', 'Pneumonia'], ax=axes[1])
axes[1].set_title('Transfer Learning', fontsize=14, fontweight='bold')
axes[1].set_ylabel('True Label')
axes[1].set_xlabel('Predicted Label')
plt.tight_layout()
plt.show()
Detailed classification reports
# Impression des rapports de classification
print("\n=== Baseline CNN ===")
print(classification_report(true_labels, baseline_preds,
target_names=['Normal', 'Pneumonia']))
print("\n=== Transfer Learning ===")
print(classification_report(true_labels, transfer_preds,
target_names=['Normal', 'Pneumonia']))
Analysis of confusion matrices
Look closely at the confusion matrices:
The baseline CNN has a major problem: it predicted pneumonia 183 times when the x-rays were actually normal. These are 183 false positives — unnecessary worry and follow-up tests for healthy patients.
The transfer learning model is much better balanced: only 67 false positives, a massive reduction. It also maintains a low false negative rate, meaning we still capture almost all cases of pneumonia.
Why accuracy alone is not enough for medical data
Although precision is normally a good indicator, this dataset is different. We have a medical dataset that almost always has some imbalance, so other things need to be considered:
| Criterion | CNN Baseline | Transfer Learning |
|---|---|---|
| Accuracy on the test set | ~78% | ~96% |
| False positives (unnecessary alarms) | 183 | 67 |
| Stability of learning curves | Erratic | Smooth and stable |
| Reliability of recall (detection of real cases) | Good | Very good |
What matters beyond the 18% accuracy improvement:
- False positives: 183 false alarms vs. 67 for transfer learning — a massive reduction in unnecessary patient worry and follow-up exams.
- Stability: look at the training curves. The transfer learning was smooth and stable, the baseline was erratic.
- Reliability: transfer learning maintains high recall while drastically reducing false alarms.
- Confidence: For medical deployment, we need consistent and balanced predictions.
4. Conclusion
Congratulations! You have built an image recognition model, from data preparation to model evaluation. This course covered:
- Data preparation: downloading, organization into three sets (train/validation/test), resizing, normalization and augmentation.
- Construction of a CNN from scratch: architecture with two convolutional blocks, compilation, callbacks and training with visualization of learning curves.
- Transfer learning with ResNet152V2: exploitation of a pre-trained model on ImageNet as a feature extractor, adding only a personalized head.
- The complete assessment: confusion matrices, classification reports and false positive/negative analysis in a medical context.
The techniques learned in this course are directly applicable to any computer vision project, whether medical diagnostics, industrial quality control, face recognition, or image classification in any domain.
Search Terms
image · recognition · model · deep · neural · networks · machine · data · science · validation · evaluation · visualization · construction · set · transfer · checking · cnn · compilation · confusion · curves · dataframes · dataset · images · imports