Computer vision, Jul 2025

Retinopathy classifier

Transfer learning on retinal photos to grade diabetic retinopathy into five stages, with an honest look at where the model fails.

Stack
  • Python
  • TensorFlow
  • Keras
  • EfficientNet-B0
  • scikit-learn
  • OpenCV
Links

The problem

Diabetic retinopathy is graded on a five-step scale from photos of the back of the eye. Catching it early prevents blindness, and graders are scarce.

I wanted to see how far a small pretrained model gets on a public dataset of 3,662 images, and to find out exactly where it breaks.

How it's built

Images are 224 × 224 fundus photos with a Gaussian filter already applied. EfficientNet-B0 with ImageNet weights is the backbone, its first 100 layers frozen, with a small classification head on top.

Training pipeline 3,662 fundus images to Augmentation (80% train). Augmentation to EfficientNet-B0. EfficientNet-B0 to Classifier head. Classifier head to Softmax 3,662 fundus images 224 × 224, filtered Augmentation Rotate up to 15° Shift, shear, zoom 10% Horizontal flip EfficientNet-B0 ImageNet weights First 100 layers frozen 4.2M parameters Classifier head Global average pool Dropout 0.3 Dense 128, ReLU Dropout 0.5 Softmax 5 severity stages 80% train
Training pipeline. 80% of images train the model; 20% are held out for validation.

Key decisions

Fine-tune a small pretrained backbone

With under 3,000 training images, training from scratch would overfit. EfficientNet-B0 has 4.2M parameters and already knows edges and textures. Freezing the early layers and fine-tuning the rest, with augmentation, a stepped learning rate and early stopping, kept training stable.

base_model = EfficientNetB0(input_shape=(224, 224, 3),
                            weights="imagenet", include_top=False)
for layer in base_model.layers[:100]:
    layer.trainable = False

model = Sequential([
    base_model,
    GlobalAveragePooling2D(),
    Dropout(0.3),
    Dense(128, activation="relu"),
    Dropout(0.5),
    Dense(num_classes, activation="softmax"),
])
From the training notebook

Report more than accuracy

Half the validation images show no retinopathy, so accuracy flatters a lazy model. For most of the first eight epochs validation accuracy sat at exactly 49.4%: the score you get by predicting No DR for every image. I tracked Cohen's kappa, per-class F1 and the full confusion matrix instead.

Check the metric, not just the model

The notebook reported a quadratic weighted kappa of 0.25, far below the unweighted 0.63. Keras numbers classes alphabetically, so the metric was treating No DR as sitting between Moderate and Proliferative. Recomputed in severity order from the same confusion matrix, it is 0.76.

severity = ["No_DR", "Mild", "Moderate", "Severe", "Proliferate_DR"]
rank = {train_data.class_indices[name]: i for i, name in enumerate(severity)}

qwk = cohen_kappa_score([rank[y] for y in y_true],
                        [rank[y] for y in y_pred],
                        weights="quadratic")  # 0.758
Mapping Keras class indices to severity order before scoring

Results

accuracy on 731 held-out images
76.5%
Cohen's kappa, unweighted / quadratic
0.63 / 0.76
of eyes with any retinopathy flagged as diseased
95.4%

As a yes-or-no screen for any retinopathy, the same predictions catch 95.4% of diseased eyes and clear 97.8% of healthy ones.

Grading the severe end is where it fails: 2 of 38 Severe cases are found, and Proliferative is never predicted at all. 33 of the 38 Severe cases come back as Moderate.

Where the predictions land Rows are the true stage, columns the prediction. Colour shows the share of each true stage.
Show the numbers as a table
Confusion matrix, counts of validation images
True stage Predicted No DRPredicted MildPredicted ModeratePredicted SeverePredicted Proliferative
No DR 3534400
Mild 4313720
Moderate 101017360
Severe 033320
Proliferative 384620
F1 score by stage The model is strong on the common stages and fails the rare, severe ones.
Show the numbers as a table
Per-stage precision, recall, F1 and number of validation images
Stage Precision Recall F1 Images
No DR 0.95 0.98 0.97 361
Mild 0.55 0.42 0.48 74
Moderate 0.59 0.87 0.70 199
Severe 0.17 0.05 0.08 38
Proliferative 0.00 0.00 0.00 59

Limits and next steps

  • The rare classes need more weight: focal loss or class weights, oversampling, and more Severe and Proliferative images.
  • Higher input resolution (380 px) should help, since the lesions that separate the top grades are small.
  • Not suitable for clinical use. It's a baseline to learn from.