fastdrs 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastdrs/__init__.py +2 -0
- fastdrs/data.py +113 -0
- fastdrs/deployment/__init__.py +0 -0
- fastdrs/deployment/litert.py +1 -0
- fastdrs/evaluation.py +111 -0
- fastdrs/models.py +110 -0
- fastdrs/preprocessing.py +88 -0
- fastdrs/training.py +169 -0
- fastdrs-0.1.0.dist-info/METADATA +776 -0
- fastdrs-0.1.0.dist-info/RECORD +11 -0
- fastdrs-0.1.0.dist-info/WHEEL +4 -0
fastdrs/__init__.py
ADDED
fastdrs/data.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data loading and dataset handling for Diabetic Retinopathy screening.
|
|
3
|
+
|
|
4
|
+
Supports Hugging Face datasets (e.g. `ctmedtech/EYEPACS`) as well as standard PyTorch datasets,
|
|
5
|
+
with reproducible train/validation/test splitting and DataLoader construction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Tuple, Dict, Any, Optional
|
|
9
|
+
import torch
|
|
10
|
+
from torch.utils.data import Dataset, DataLoader, Subset
|
|
11
|
+
from sklearn.model_selection import train_test_split
|
|
12
|
+
from datasets import load_dataset
|
|
13
|
+
from PIL import Image
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from fastdrs.preprocessing import get_transforms
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EyepacsDataset(Dataset):
|
|
20
|
+
"""
|
|
21
|
+
PyTorch Wrapper for Hugging Face EYEPACS dataset or image list.
|
|
22
|
+
"""
|
|
23
|
+
def __init__(self, hf_dataset, transform=None):
|
|
24
|
+
self.dataset = hf_dataset
|
|
25
|
+
self.transform = transform
|
|
26
|
+
|
|
27
|
+
def __len__(self) -> int:
|
|
28
|
+
return len(self.dataset)
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
|
|
31
|
+
item = self.dataset[int(idx)]
|
|
32
|
+
image = item["image"]
|
|
33
|
+
|
|
34
|
+
# Ensure image is PIL Image in RGB mode
|
|
35
|
+
if not isinstance(image, Image.Image):
|
|
36
|
+
image = Image.fromarray(np.uint8(image))
|
|
37
|
+
if image.mode != "RGB":
|
|
38
|
+
image = image.convert("RGB")
|
|
39
|
+
|
|
40
|
+
label = item.get("label", item.get("dr_level", item.get("level", 0)))
|
|
41
|
+
|
|
42
|
+
if self.transform is not None:
|
|
43
|
+
image = self.transform(image)
|
|
44
|
+
|
|
45
|
+
return image, int(label)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_dataloaders(
|
|
49
|
+
dataset_name: str = "sngsfydy/aptos",
|
|
50
|
+
batch_size: int = 32,
|
|
51
|
+
img_size: int = 224,
|
|
52
|
+
val_split: float = 0.15,
|
|
53
|
+
test_split: float = 0.15,
|
|
54
|
+
seed: int = 42,
|
|
55
|
+
num_workers: int = 2,
|
|
56
|
+
use_ben_graham: bool = False
|
|
57
|
+
) -> Tuple[DataLoader, DataLoader, DataLoader, Dict[str, Any]]:
|
|
58
|
+
"""
|
|
59
|
+
Loads dataset, splits into train/val/test, applies transforms, and returns PyTorch DataLoaders.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
train_loader, val_loader, test_loader, dataset_info metadata dict
|
|
63
|
+
"""
|
|
64
|
+
# Load raw dataset from Hugging Face
|
|
65
|
+
raw_dataset = load_dataset(dataset_name, split="train")
|
|
66
|
+
total_size = len(raw_dataset)
|
|
67
|
+
indices = np.arange(total_size)
|
|
68
|
+
|
|
69
|
+
# Get labels if available for stratified splitting
|
|
70
|
+
try:
|
|
71
|
+
labels = [raw_dataset[i].get("label", raw_dataset[i].get("dr_level", 0)) for i in range(min(total_size, 1000))]
|
|
72
|
+
stratify = None # Avoid full scan if dataset is large, or use stratified split on subset
|
|
73
|
+
except Exception:
|
|
74
|
+
labels = None
|
|
75
|
+
|
|
76
|
+
# Train / Val / Test Split
|
|
77
|
+
train_val_idx, test_idx = train_test_split(
|
|
78
|
+
indices, test_size=test_split, random_state=seed, shuffle=True
|
|
79
|
+
)
|
|
80
|
+
relative_val_size = val_split / (1.0 - test_split)
|
|
81
|
+
train_idx, val_idx = train_test_split(
|
|
82
|
+
train_val_idx, test_size=relative_val_size, random_state=seed, shuffle=True
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
train_idx = [int(i) for i in train_idx]
|
|
86
|
+
val_idx = [int(i) for i in val_idx]
|
|
87
|
+
test_idx = [int(i) for i in test_idx]
|
|
88
|
+
|
|
89
|
+
train_transform = get_transforms(img_size=img_size, is_train=True, use_ben_graham=use_ben_graham)
|
|
90
|
+
eval_transform = get_transforms(img_size=img_size, is_train=False, use_ben_graham=use_ben_graham)
|
|
91
|
+
|
|
92
|
+
full_train_ds = EyepacsDataset(raw_dataset, transform=train_transform)
|
|
93
|
+
full_eval_ds = EyepacsDataset(raw_dataset, transform=eval_transform)
|
|
94
|
+
|
|
95
|
+
train_ds = Subset(full_train_ds, train_idx)
|
|
96
|
+
val_ds = Subset(full_eval_ds, val_idx)
|
|
97
|
+
test_ds = Subset(full_eval_ds, test_idx)
|
|
98
|
+
|
|
99
|
+
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True)
|
|
100
|
+
val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)
|
|
101
|
+
test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)
|
|
102
|
+
|
|
103
|
+
info = {
|
|
104
|
+
"dataset_name": dataset_name,
|
|
105
|
+
"total_samples": total_size,
|
|
106
|
+
"train_samples": len(train_ds),
|
|
107
|
+
"val_samples": len(val_ds),
|
|
108
|
+
"test_samples": len(test_ds),
|
|
109
|
+
"batch_size": batch_size,
|
|
110
|
+
"img_size": img_size
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return train_loader, val_loader, test_loader, info
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.py
|
fastdrs/evaluation.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Evaluation and benchmarking module for Diabetic Retinopathy screening models.
|
|
3
|
+
|
|
4
|
+
Calculates clinical metrics:
|
|
5
|
+
- Accuracy, Precision, Sensitivity/Recall, Specificity, F1 Score
|
|
6
|
+
- Per-class classification report and Confusion Matrix
|
|
7
|
+
- Inference latency and deployment feasibility metrics
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Dict, Any, List, Tuple, Optional
|
|
11
|
+
import time
|
|
12
|
+
import torch
|
|
13
|
+
import torch.nn as nn
|
|
14
|
+
import numpy as np
|
|
15
|
+
from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix
|
|
16
|
+
|
|
17
|
+
from fastdrs.models import get_model_info
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def compute_specificity(cm: np.ndarray) -> np.ndarray:
|
|
21
|
+
"""
|
|
22
|
+
Computes per-class specificity from confusion matrix.
|
|
23
|
+
Specificity = TN / (TN + FP)
|
|
24
|
+
"""
|
|
25
|
+
num_classes = cm.shape[0]
|
|
26
|
+
specificities = []
|
|
27
|
+
total = np.sum(cm)
|
|
28
|
+
for i in range(num_classes):
|
|
29
|
+
tp = cm[i, i]
|
|
30
|
+
fp = np.sum(cm[:, i]) - tp
|
|
31
|
+
fn = np.sum(cm[i, :]) - tp
|
|
32
|
+
tn = total - (tp + fp + fn)
|
|
33
|
+
spec = tn / (tn + fp) if (tn + fp) > 0 else 0.0
|
|
34
|
+
specificities.append(spec)
|
|
35
|
+
return np.array(specificities)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def evaluate_model(
|
|
39
|
+
model: nn.Module,
|
|
40
|
+
dataloader: torch.utils.data.DataLoader,
|
|
41
|
+
device: torch.device,
|
|
42
|
+
class_names: Optional[List[str]] = None
|
|
43
|
+
) -> Dict[str, Any]:
|
|
44
|
+
"""
|
|
45
|
+
Evaluates a model on a dataloader, measuring clinical performance and inference latency.
|
|
46
|
+
"""
|
|
47
|
+
if class_names is None:
|
|
48
|
+
class_names = ["0 - No DR", "1 - Mild", "2 - Moderate", "3 - Severe", "4 - Proliferative"]
|
|
49
|
+
|
|
50
|
+
model.to(device)
|
|
51
|
+
model.eval()
|
|
52
|
+
|
|
53
|
+
all_preds = []
|
|
54
|
+
all_targets = []
|
|
55
|
+
inference_times = []
|
|
56
|
+
|
|
57
|
+
with torch.no_grad():
|
|
58
|
+
for inputs, targets in dataloader:
|
|
59
|
+
inputs = inputs.to(device)
|
|
60
|
+
|
|
61
|
+
start_time = time.perf_counter()
|
|
62
|
+
outputs = model(inputs)
|
|
63
|
+
if device.type == "cuda":
|
|
64
|
+
torch.cuda.synchronize()
|
|
65
|
+
end_time = time.perf_counter()
|
|
66
|
+
|
|
67
|
+
batch_latency = (end_time - start_time) / inputs.size(0)
|
|
68
|
+
inference_times.append(batch_latency)
|
|
69
|
+
|
|
70
|
+
preds = torch.argmax(outputs, dim=1).cpu().numpy()
|
|
71
|
+
all_preds.extend(preds)
|
|
72
|
+
all_targets.extend(targets.numpy())
|
|
73
|
+
|
|
74
|
+
all_preds = np.array(all_preds)
|
|
75
|
+
all_targets = np.array(all_targets)
|
|
76
|
+
|
|
77
|
+
acc = accuracy_score(all_targets, all_preds)
|
|
78
|
+
precision, recall, f1, _ = precision_recall_fscore_support(all_targets, all_preds, average='macro', zero_division=0)
|
|
79
|
+
_, _, f1_weighted, _ = precision_recall_fscore_support(all_targets, all_preds, average='weighted', zero_division=0)
|
|
80
|
+
|
|
81
|
+
cm = confusion_matrix(all_targets, all_preds, labels=list(range(len(class_names))))
|
|
82
|
+
per_class_spec = compute_specificity(cm)
|
|
83
|
+
per_class_prec, per_class_rec, per_class_f1, _ = precision_recall_fscore_support(
|
|
84
|
+
all_targets, all_preds, average=None, labels=list(range(len(class_names))), zero_division=0
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
avg_latency_ms = float(np.mean(inference_times) * 1000.0)
|
|
88
|
+
model_info = get_model_info(model)
|
|
89
|
+
|
|
90
|
+
results = {
|
|
91
|
+
"accuracy": round(float(acc), 4),
|
|
92
|
+
"precision_macro": round(float(precision), 4),
|
|
93
|
+
"sensitivity_macro": round(float(recall), 4),
|
|
94
|
+
"specificity_macro": round(float(np.mean(per_class_spec)), 4),
|
|
95
|
+
"f1_macro": round(float(f1), 4),
|
|
96
|
+
"f1_weighted": round(float(f1_weighted), 4),
|
|
97
|
+
"inference_latency_ms": round(avg_latency_ms, 2),
|
|
98
|
+
"model_size_mb": model_info["model_size_mb"],
|
|
99
|
+
"total_parameters": model_info["total_parameters"],
|
|
100
|
+
"confusion_matrix": cm.tolist(),
|
|
101
|
+
"per_class": {
|
|
102
|
+
class_names[i]: {
|
|
103
|
+
"precision": round(float(per_class_prec[i]), 4),
|
|
104
|
+
"sensitivity_recall": round(float(per_class_rec[i]), 4),
|
|
105
|
+
"specificity": round(float(per_class_spec[i]), 4),
|
|
106
|
+
"f1": round(float(per_class_f1[i]), 4),
|
|
107
|
+
} for i in range(len(class_names))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return results
|
fastdrs/models.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model architecture factory for Diabetic Retinopathy classification.
|
|
3
|
+
|
|
4
|
+
Supports building and benchmarking multiple vision architectures:
|
|
5
|
+
- ResNet (e.g. resnet50, resnet18)
|
|
6
|
+
- DenseNet (e.g. densenet121)
|
|
7
|
+
- ConvNeXt (e.g. convnext_tiny)
|
|
8
|
+
- Swin Transformer (e.g. swin_t)
|
|
9
|
+
- MobileNetV2 (efficient baseline)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Tuple, Dict, Any
|
|
13
|
+
import torch
|
|
14
|
+
import torch.nn as nn
|
|
15
|
+
import torchvision.models as models
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_model(
|
|
19
|
+
architecture: str = "resnet50",
|
|
20
|
+
num_classes: int = 5,
|
|
21
|
+
pretrained: bool = True,
|
|
22
|
+
dropout_rate: float = 0.2
|
|
23
|
+
) -> nn.Module:
|
|
24
|
+
"""
|
|
25
|
+
Constructs a vision model with customized classification head.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
architecture: Choice of 'resnet50', 'resnet18', 'densenet121', 'convnext_tiny', 'swin_t', 'mobilenet_v2'.
|
|
29
|
+
num_classes: Number of output severity classes (default: 5).
|
|
30
|
+
pretrained: Whether to use ImageNet pre-trained weights.
|
|
31
|
+
dropout_rate: Dropout rate before classification layer.
|
|
32
|
+
"""
|
|
33
|
+
arch = architecture.lower()
|
|
34
|
+
|
|
35
|
+
if arch == "resnet50":
|
|
36
|
+
weights = models.ResNet50_Weights.DEFAULT if pretrained else None
|
|
37
|
+
model = models.resnet50(weights=weights)
|
|
38
|
+
in_features = model.fc.in_features
|
|
39
|
+
model.fc = nn.Sequential(
|
|
40
|
+
nn.Dropout(p=dropout_rate),
|
|
41
|
+
nn.Linear(in_features, num_classes)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
elif arch == "resnet18":
|
|
45
|
+
weights = models.ResNet18_Weights.DEFAULT if pretrained else None
|
|
46
|
+
model = models.resnet18(weights=weights)
|
|
47
|
+
in_features = model.fc.in_features
|
|
48
|
+
model.fc = nn.Sequential(
|
|
49
|
+
nn.Dropout(p=dropout_rate),
|
|
50
|
+
nn.Linear(in_features, num_classes)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
elif arch == "densenet121":
|
|
54
|
+
weights = models.DenseNet121_Weights.DEFAULT if pretrained else None
|
|
55
|
+
model = models.densenet121(weights=weights)
|
|
56
|
+
in_features = model.classifier.in_features
|
|
57
|
+
model.classifier = nn.Sequential(
|
|
58
|
+
nn.Dropout(p=dropout_rate),
|
|
59
|
+
nn.Linear(in_features, num_classes)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
elif arch in ("convnext_tiny", "convnext"):
|
|
63
|
+
weights = models.ConvNeXt_Tiny_Weights.DEFAULT if pretrained else None
|
|
64
|
+
model = models.convnext_tiny(weights=weights)
|
|
65
|
+
in_features = model.classifier[2].in_features
|
|
66
|
+
model.classifier[2] = nn.Sequential(
|
|
67
|
+
nn.Dropout(p=dropout_rate),
|
|
68
|
+
nn.Linear(in_features, num_classes)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
elif arch in ("swin_t", "swin"):
|
|
72
|
+
weights = models.Swin_T_Weights.DEFAULT if pretrained else None
|
|
73
|
+
model = models.swin_t(weights=weights)
|
|
74
|
+
in_features = model.head.in_features
|
|
75
|
+
model.head = nn.Sequential(
|
|
76
|
+
nn.Dropout(p=dropout_rate),
|
|
77
|
+
nn.Linear(in_features, num_classes)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
elif arch in ("mobilenet_v2", "mobilenet"):
|
|
81
|
+
weights = models.MobileNet_V2_Weights.DEFAULT if pretrained else None
|
|
82
|
+
model = models.mobilenet_v2(weights=weights)
|
|
83
|
+
in_features = model.classifier[1].in_features
|
|
84
|
+
model.classifier = nn.Sequential(
|
|
85
|
+
nn.Dropout(p=dropout_rate),
|
|
86
|
+
nn.Linear(in_features, num_classes)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError(f"Unsupported architecture '{architecture}'. "
|
|
91
|
+
f"Supported: resnet50, resnet18, densenet121, convnext_tiny, swin_t, mobilenet_v2")
|
|
92
|
+
|
|
93
|
+
return model
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_model_info(model: nn.Module) -> Dict[str, Any]:
|
|
97
|
+
"""
|
|
98
|
+
Calculates parameter count and estimated model memory size in MB.
|
|
99
|
+
"""
|
|
100
|
+
total_params = sum(p.numel() for p in model.parameters())
|
|
101
|
+
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
|
102
|
+
|
|
103
|
+
# Estimate size assuming float32 (4 bytes per param)
|
|
104
|
+
param_size_mb = (total_params * 4) / (1024 * 1024)
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
"total_parameters": total_params,
|
|
108
|
+
"trainable_parameters": trainable_params,
|
|
109
|
+
"model_size_mb": round(param_size_mb, 2)
|
|
110
|
+
}
|
fastdrs/preprocessing.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Preprocessing module for Diabetic Retinopathy retinal fundus images.
|
|
3
|
+
|
|
4
|
+
Provides standard image transformations, augmentations, and specialized
|
|
5
|
+
preprocessing routines (e.g., circular cropping, Ben Graham color processing)
|
|
6
|
+
tailored for fundus image classification.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Tuple, Optional, Callable
|
|
10
|
+
import torch
|
|
11
|
+
import torchvision.transforms as T
|
|
12
|
+
from PIL import Image
|
|
13
|
+
import numpy as np
|
|
14
|
+
import cv2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def crop_image_from_gray(img: np.ndarray, tol: int = 7) -> np.ndarray:
|
|
18
|
+
"""
|
|
19
|
+
Crops black boundaries from fundus images.
|
|
20
|
+
"""
|
|
21
|
+
if img.ndim == 2:
|
|
22
|
+
mask = img > tol
|
|
23
|
+
return img[np.ix_(mask.any(1), mask.any(0))]
|
|
24
|
+
elif img.ndim == 3:
|
|
25
|
+
gray_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
|
26
|
+
mask = gray_img > tol
|
|
27
|
+
|
|
28
|
+
check_start = img[:, :, 0][np.ix_(mask.any(1), mask.any(0))]
|
|
29
|
+
if check_start.size == 0:
|
|
30
|
+
return img
|
|
31
|
+
else:
|
|
32
|
+
img1 = img[:, :, 0][np.ix_(mask.any(1), mask.any(0))]
|
|
33
|
+
img2 = img[:, :, 1][np.ix_(mask.any(1), mask.any(0))]
|
|
34
|
+
img3 = img[:, :, 2][np.ix_(mask.any(1), mask.any(0))]
|
|
35
|
+
return np.stack([img1, img2, img3], axis=-1)
|
|
36
|
+
return img
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ben_graham_preprocessing(img_pil: Image.Image, img_size: int = 224, sigma_x: int = 10) -> Image.Image:
|
|
40
|
+
"""
|
|
41
|
+
Applies Ben Graham's method for enhancing retinal fundus features (subtract local average color).
|
|
42
|
+
"""
|
|
43
|
+
img = np.array(img_pil)
|
|
44
|
+
img = crop_image_from_gray(img)
|
|
45
|
+
img = cv2.resize(img, (img_size, img_size))
|
|
46
|
+
img = cv2.addWeighted(img, 4, cv2.GaussianBlur(img, (0, 0), sigma_x), -4, 128)
|
|
47
|
+
return Image.fromarray(img)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_transforms(
|
|
51
|
+
img_size: int = 224,
|
|
52
|
+
is_train: bool = True,
|
|
53
|
+
mean: Tuple[float, float, float] = (0.485, 0.456, 0.406),
|
|
54
|
+
std: Tuple[float, float, float] = (0.229, 0.224, 0.225),
|
|
55
|
+
use_ben_graham: bool = False
|
|
56
|
+
) -> T.Compose:
|
|
57
|
+
"""
|
|
58
|
+
Returns torchvision transforms for training or evaluation.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
img_size: Target image height and width.
|
|
62
|
+
is_train: Whether to include data augmentation transforms.
|
|
63
|
+
mean: Normalization channel means.
|
|
64
|
+
std: Normalization channel standard deviations.
|
|
65
|
+
use_ben_graham: Whether to apply Ben Graham preprocessing.
|
|
66
|
+
"""
|
|
67
|
+
transforms_list = []
|
|
68
|
+
|
|
69
|
+
if use_ben_graham:
|
|
70
|
+
transforms_list.append(T.Lambda(lambda img: ben_graham_preprocessing(img, img_size=img_size)))
|
|
71
|
+
else:
|
|
72
|
+
transforms_list.append(T.Resize((img_size, img_size)))
|
|
73
|
+
|
|
74
|
+
if is_train:
|
|
75
|
+
transforms_list.extend([
|
|
76
|
+
T.RandomHorizontalFlip(p=0.5),
|
|
77
|
+
T.RandomVerticalFlip(p=0.5),
|
|
78
|
+
T.RandomRotation(degrees=30),
|
|
79
|
+
T.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
|
|
80
|
+
T.RandomAffine(degrees=0, translate=(0.05, 0.05), scale=(0.95, 1.05))
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
transforms_list.extend([
|
|
84
|
+
T.ToTensor(),
|
|
85
|
+
T.Normalize(mean=mean, std=std)
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
return T.Compose(transforms_list)
|
fastdrs/training.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Training script and loop for Diabetic Retinopathy classification models.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- PyTorch modular training loop with validation
|
|
6
|
+
- Support for class-weighted loss (for imbalanced DR classes)
|
|
7
|
+
- Learning rate schedulers (CosineAnnealing / ReduceLROnPlateau)
|
|
8
|
+
- Best model checkpointing
|
|
9
|
+
- Weights & Biases (wandb) logging integration
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Dict, Any, Optional, Tuple
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
import torch
|
|
16
|
+
import torch.nn as nn
|
|
17
|
+
import torch.optim as optim
|
|
18
|
+
from torch.optim.lr_scheduler import CosineAnnealingLR, ReduceLROnPlateau
|
|
19
|
+
|
|
20
|
+
from fastdrs.data import create_dataloaders
|
|
21
|
+
from fastdrs.models import build_model, get_model_info
|
|
22
|
+
from fastdrs.evaluation import evaluate_model
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def train_epoch(
|
|
26
|
+
model: nn.Module,
|
|
27
|
+
dataloader: torch.utils.data.DataLoader,
|
|
28
|
+
criterion: nn.Module,
|
|
29
|
+
optimizer: optim.Optimizer,
|
|
30
|
+
device: torch.device
|
|
31
|
+
) -> Tuple[float, float]:
|
|
32
|
+
model.train()
|
|
33
|
+
running_loss = 0.0
|
|
34
|
+
correct = 0
|
|
35
|
+
total = 0
|
|
36
|
+
|
|
37
|
+
for inputs, targets in dataloader:
|
|
38
|
+
inputs, targets = inputs.to(device), targets.to(device)
|
|
39
|
+
|
|
40
|
+
optimizer.zero_grad()
|
|
41
|
+
outputs = model(inputs)
|
|
42
|
+
loss = criterion(outputs, targets)
|
|
43
|
+
loss.backward()
|
|
44
|
+
optimizer.step()
|
|
45
|
+
|
|
46
|
+
running_loss += loss.item() * inputs.size(0)
|
|
47
|
+
preds = torch.argmax(outputs, dim=1)
|
|
48
|
+
correct += (preds == targets).sum().item()
|
|
49
|
+
total += inputs.size(0)
|
|
50
|
+
|
|
51
|
+
epoch_loss = running_loss / total
|
|
52
|
+
epoch_acc = correct / total
|
|
53
|
+
return epoch_loss, epoch_acc
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_epoch(
|
|
57
|
+
model: nn.Module,
|
|
58
|
+
dataloader: torch.utils.data.DataLoader,
|
|
59
|
+
criterion: nn.Module,
|
|
60
|
+
device: torch.device
|
|
61
|
+
) -> Tuple[float, float]:
|
|
62
|
+
model.eval()
|
|
63
|
+
running_loss = 0.0
|
|
64
|
+
correct = 0
|
|
65
|
+
total = 0
|
|
66
|
+
|
|
67
|
+
with torch.no_grad():
|
|
68
|
+
for inputs, targets in dataloader:
|
|
69
|
+
inputs, targets = inputs.to(device), targets.to(device)
|
|
70
|
+
outputs = model(inputs)
|
|
71
|
+
loss = criterion(outputs, targets)
|
|
72
|
+
|
|
73
|
+
running_loss += loss.item() * inputs.size(0)
|
|
74
|
+
preds = torch.argmax(outputs, dim=1)
|
|
75
|
+
correct += (preds == targets).sum().item()
|
|
76
|
+
total += inputs.size(0)
|
|
77
|
+
|
|
78
|
+
epoch_loss = running_loss / total
|
|
79
|
+
epoch_acc = correct / total
|
|
80
|
+
return epoch_loss, epoch_acc
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def train_model(
|
|
84
|
+
architecture: str = "resnet50",
|
|
85
|
+
dataset_name: str = "sngsfydy/aptos",
|
|
86
|
+
epochs: int = 10,
|
|
87
|
+
batch_size: int = 32,
|
|
88
|
+
lr: float = 1e-4,
|
|
89
|
+
weight_decay: float = 1e-4,
|
|
90
|
+
img_size: int = 224,
|
|
91
|
+
save_dir: str = "models",
|
|
92
|
+
use_wandb: bool = False,
|
|
93
|
+
device: str = "cuda" if torch.cuda.is_available() else "cpu"
|
|
94
|
+
) -> Dict[str, Any]:
|
|
95
|
+
"""
|
|
96
|
+
Main entry point for training a DR classification model.
|
|
97
|
+
"""
|
|
98
|
+
os.makedirs(save_dir, exist_ok=True)
|
|
99
|
+
device_obj = torch.device(device)
|
|
100
|
+
|
|
101
|
+
print(f"--- Training {architecture} on {device} ---")
|
|
102
|
+
|
|
103
|
+
# 1. DataLoaders
|
|
104
|
+
train_loader, val_loader, test_loader, ds_info = create_dataloaders(
|
|
105
|
+
dataset_name=dataset_name,
|
|
106
|
+
batch_size=batch_size,
|
|
107
|
+
img_size=img_size
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# 2. Build Model
|
|
111
|
+
model = build_model(architecture=architecture, num_classes=5, pretrained=True)
|
|
112
|
+
model.to(device_obj)
|
|
113
|
+
|
|
114
|
+
model_info = get_model_info(model)
|
|
115
|
+
print(f"Model parameters: {model_info['total_parameters']:,} ({model_info['model_size_mb']} MB)")
|
|
116
|
+
|
|
117
|
+
# 3. Optimization Setup
|
|
118
|
+
criterion = nn.CrossEntropyLoss()
|
|
119
|
+
optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)
|
|
120
|
+
scheduler = CosineAnnealingLR(optimizer, T_max=epochs)
|
|
121
|
+
|
|
122
|
+
best_val_loss = float("inf")
|
|
123
|
+
best_checkpoint_path = os.path.join(save_dir, f"{architecture}_best.pth")
|
|
124
|
+
|
|
125
|
+
history = {"train_loss": [], "train_acc": [], "val_loss": [], "val_acc": []}
|
|
126
|
+
|
|
127
|
+
# 4. Training Loop
|
|
128
|
+
for epoch in range(1, epochs + 1):
|
|
129
|
+
t0 = time.time()
|
|
130
|
+
train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, device_obj)
|
|
131
|
+
val_loss, val_acc = validate_epoch(model, val_loader, criterion, device_obj)
|
|
132
|
+
scheduler.step()
|
|
133
|
+
elapsed = time.time() - t0
|
|
134
|
+
|
|
135
|
+
history["train_loss"].append(train_loss)
|
|
136
|
+
history["train_acc"].append(train_acc)
|
|
137
|
+
history["val_loss"].append(val_loss)
|
|
138
|
+
history["val_acc"].append(val_acc)
|
|
139
|
+
|
|
140
|
+
print(f"Epoch {epoch:02d}/{epochs:02d} [{elapsed:.1f}s] | "
|
|
141
|
+
f"Train Loss: {train_loss:.4f}, Acc: {train_acc:.4f} | "
|
|
142
|
+
f"Val Loss: {val_loss:.4f}, Acc: {val_acc:.4f}")
|
|
143
|
+
|
|
144
|
+
if val_loss < best_val_loss:
|
|
145
|
+
best_val_loss = val_loss
|
|
146
|
+
torch.save(model.state_dict(), best_checkpoint_path)
|
|
147
|
+
print(f" Saved new best model to {best_checkpoint_path}")
|
|
148
|
+
|
|
149
|
+
# 5. Final Evaluation on Test Set using Best Weights
|
|
150
|
+
if os.path.exists(best_checkpoint_path):
|
|
151
|
+
model.load_state_dict(torch.load(best_checkpoint_path, map_location=device_obj))
|
|
152
|
+
|
|
153
|
+
print("\n--- Final Test Set Evaluation ---")
|
|
154
|
+
test_results = evaluate_model(model, test_loader, device_obj)
|
|
155
|
+
print(f"Test Accuracy: {test_results['accuracy']}")
|
|
156
|
+
print(f"Test Macro F1: {test_results['f1_macro']}")
|
|
157
|
+
print(f"Test Macro Sensitivity: {test_results['sensitivity_macro']}")
|
|
158
|
+
print(f"Test Latency: {test_results['inference_latency_ms']} ms/sample")
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
"architecture": architecture,
|
|
162
|
+
"history": history,
|
|
163
|
+
"test_results": test_results,
|
|
164
|
+
"best_checkpoint": best_checkpoint_path
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
train_model(architecture="resnet50", epochs=5)
|
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastdrs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Diabetic retinopathy screening toolkit
|
|
5
|
+
Author: Michael Mukiibi
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: pillow
|
|
10
|
+
Provides-Extra: export
|
|
11
|
+
Provides-Extra: training
|
|
12
|
+
Requires-Dist: datasets; extra == 'training'
|
|
13
|
+
Requires-Dist: opencv-python; extra == 'training'
|
|
14
|
+
Requires-Dist: scikit-learn; extra == 'training'
|
|
15
|
+
Requires-Dist: torch; extra == 'training'
|
|
16
|
+
Requires-Dist: torchvision; extra == 'training'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# AI-Assisted Diabetic Retinopathy Screening & Triage
|
|
20
|
+
|
|
21
|
+
> **Marconi Research & Innovations Lab — Internship Capstone Project**
|
|
22
|
+
|
|
23
|
+
An economically motivated computer-vision system for **AI-assisted diabetic retinopathy screening and clinical triage**, designed with resource-constrained healthcare environments in mind.
|
|
24
|
+
|
|
25
|
+
The project investigates whether modern deep-learning vision architectures can classify diabetic retinopathy severity from retinal fundus photographs while balancing **clinical sensitivity, predictive performance, model size, and inference latency** for practical deployment.
|
|
26
|
+
|
|
27
|
+
> **Important:** This project is a research and engineering prototype. It is **not a medical device and does not provide an autonomous clinical diagnosis**. Predictions are intended to demonstrate AI-assisted screening and referral prioritization.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. Problem
|
|
32
|
+
|
|
33
|
+
Diabetic retinopathy (DR) is a major complication of diabetes and an important cause of preventable vision loss. Early identification can enable timely clinical assessment and treatment.
|
|
34
|
+
|
|
35
|
+
In resource-constrained healthcare environments, however, screening can be limited by:
|
|
36
|
+
|
|
37
|
+
* Shortage of ophthalmologists and trained specialists
|
|
38
|
+
* Geographic barriers to specialist care
|
|
39
|
+
* Cost of specialist examinations
|
|
40
|
+
* Increasing numbers of patients requiring screening
|
|
41
|
+
* Limited access to regular retinal imaging assessment
|
|
42
|
+
|
|
43
|
+
This creates an opportunity for **AI-assisted screening and triage**.
|
|
44
|
+
|
|
45
|
+
Rather than replacing clinicians, an AI system can potentially help healthcare workers identify images that require **higher-priority specialist review**, allowing limited clinical resources to be allocated more efficiently.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Proposed Solution
|
|
50
|
+
|
|
51
|
+
This project develops a computer-vision pipeline that analyzes retinal fundus photographs and estimates the severity of diabetic retinopathy.
|
|
52
|
+
|
|
53
|
+
The system investigates multiple modern vision architectures and evaluates them not only by predictive performance, but also by their suitability for deployment.
|
|
54
|
+
|
|
55
|
+
### Core workflow
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
Retinal Fundus Image
|
|
59
|
+
│
|
|
60
|
+
▼
|
|
61
|
+
EDA
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
Preprocessing
|
|
65
|
+
│
|
|
66
|
+
▼
|
|
67
|
+
Model Fine-Tuning
|
|
68
|
+
│
|
|
69
|
+
├── ResNet
|
|
70
|
+
├── DenseNet
|
|
71
|
+
├── ConvNeXt
|
|
72
|
+
└── Swin Transformer
|
|
73
|
+
│
|
|
74
|
+
▼
|
|
75
|
+
Model Evaluation
|
|
76
|
+
│
|
|
77
|
+
├── Accuracy
|
|
78
|
+
├── Precision
|
|
79
|
+
├── Recall / Sensitivity
|
|
80
|
+
├── Specificity
|
|
81
|
+
└── F1 Score
|
|
82
|
+
│
|
|
83
|
+
▼
|
|
84
|
+
Deployment Analysis
|
|
85
|
+
│
|
|
86
|
+
├── Model Size
|
|
87
|
+
├── Parameter Count
|
|
88
|
+
└── Inference Latency
|
|
89
|
+
│
|
|
90
|
+
▼
|
|
91
|
+
Final Model
|
|
92
|
+
│
|
|
93
|
+
▼
|
|
94
|
+
Android Application / API
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 3. Capstone Research Question
|
|
100
|
+
|
|
101
|
+
> **Can modern computer-vision architectures provide sufficiently sensitive diabetic-retinopathy screening while remaining computationally efficient enough for deployment in resource-constrained healthcare environments?**
|
|
102
|
+
|
|
103
|
+
The project therefore treats model selection as a multi-objective engineering problem:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
Clinical Performance
|
|
107
|
+
+
|
|
108
|
+
Computational Efficiency
|
|
109
|
+
+
|
|
110
|
+
Deployment Feasibility
|
|
111
|
+
=
|
|
112
|
+
Practical Model
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 4. Objectives
|
|
118
|
+
|
|
119
|
+
### Primary objective
|
|
120
|
+
|
|
121
|
+
Develop and evaluate a deployable AI-assisted diabetic-retinopathy screening prototype using retinal fundus photographs.
|
|
122
|
+
|
|
123
|
+
### Specific objectives
|
|
124
|
+
|
|
125
|
+
1. Perform exploratory data analysis on the APTOS 2019 dataset.
|
|
126
|
+
2. Develop a standardized image preprocessing and training pipeline.
|
|
127
|
+
3. Fine-tune modern computer-vision architectures for diabetic-retinopathy classification.
|
|
128
|
+
4. Compare ResNet, DenseNet, ConvNeXt, and Swin Transformer architectures.
|
|
129
|
+
5. Evaluate models using clinically relevant classification metrics.
|
|
130
|
+
6. Investigate the relationship between sensitivity, model size, and inference latency.
|
|
131
|
+
7. Select a final model based on predictive performance and deployment feasibility.
|
|
132
|
+
8. Integrate the trained model into an **Android application or inference API**.
|
|
133
|
+
9. Demonstrate an end-to-end screening workflow.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 5. Dataset
|
|
138
|
+
|
|
139
|
+
### APTOS 2019 Blindness Detection
|
|
140
|
+
|
|
141
|
+
The primary dataset is the **APTOS 2019 Blindness Detection** dataset.
|
|
142
|
+
|
|
143
|
+
It contains approximately **3,662 labeled retinal fundus photographs** across five diabetic-retinopathy severity grades.
|
|
144
|
+
|
|
145
|
+
### Classes
|
|
146
|
+
|
|
147
|
+
| Label | Severity |
|
|
148
|
+
| ----: | ---------------------------------- |
|
|
149
|
+
| 0 | No diabetic retinopathy |
|
|
150
|
+
| 1 | Mild |
|
|
151
|
+
| 2 | Moderate |
|
|
152
|
+
| 3 | Severe |
|
|
153
|
+
| 4 | Proliferative diabetic retinopathy |
|
|
154
|
+
|
|
155
|
+
The dataset was originally released through the APTOS 2019 Blindness Detection Kaggle competition.
|
|
156
|
+
|
|
157
|
+
Dataset source:
|
|
158
|
+
|
|
159
|
+
[https://www.kaggle.com/competitions/aptos2019-blindness-detection](https://www.kaggle.com/competitions/aptos2019-blindness-detection)
|
|
160
|
+
|
|
161
|
+
### Data considerations
|
|
162
|
+
|
|
163
|
+
The project will investigate:
|
|
164
|
+
|
|
165
|
+
* Class distribution
|
|
166
|
+
* Image dimensions
|
|
167
|
+
* Image quality
|
|
168
|
+
* Color/channel characteristics
|
|
169
|
+
* Class imbalance
|
|
170
|
+
* Training/validation/test splitting
|
|
171
|
+
* Appropriate augmentation
|
|
172
|
+
* Image normalization
|
|
173
|
+
|
|
174
|
+
Because the dataset is imbalanced, **accuracy will not be treated as the sole measure of model quality**.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 6. Evaluation Strategy
|
|
179
|
+
|
|
180
|
+
The project evaluates models using multiple metrics.
|
|
181
|
+
|
|
182
|
+
### Accuracy
|
|
183
|
+
|
|
184
|
+
Measures the proportion of correctly classified samples.
|
|
185
|
+
|
|
186
|
+
### Precision
|
|
187
|
+
|
|
188
|
+
Measures how many predicted positive cases are actually positive.
|
|
189
|
+
|
|
190
|
+
### Recall / Sensitivity
|
|
191
|
+
|
|
192
|
+
Measures the proportion of relevant positive cases detected by the model.
|
|
193
|
+
|
|
194
|
+
For a screening application, sensitivity is particularly important because missed cases may require further clinical assessment.
|
|
195
|
+
|
|
196
|
+
### Specificity
|
|
197
|
+
|
|
198
|
+
Measures the proportion of negative cases correctly identified.
|
|
199
|
+
|
|
200
|
+
### F1 Score
|
|
201
|
+
|
|
202
|
+
Balances precision and recall.
|
|
203
|
+
|
|
204
|
+
### Confusion Matrix
|
|
205
|
+
|
|
206
|
+
Used to examine errors across the five diabetic-retinopathy severity classes.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 7. Model Benchmarking
|
|
211
|
+
|
|
212
|
+
The project will investigate four major vision architectures.
|
|
213
|
+
|
|
214
|
+
### ResNet
|
|
215
|
+
|
|
216
|
+
Uses residual/skip connections to facilitate the training of deep convolutional networks.
|
|
217
|
+
|
|
218
|
+
### DenseNet
|
|
219
|
+
|
|
220
|
+
Uses dense connectivity and feature reuse between layers.
|
|
221
|
+
|
|
222
|
+
### ConvNeXt
|
|
223
|
+
|
|
224
|
+
A modern convolutional architecture incorporating design principles developed alongside the rise of vision transformers.
|
|
225
|
+
|
|
226
|
+
### Swin Transformer
|
|
227
|
+
|
|
228
|
+
Uses hierarchical vision-transformer representations with shifted-window attention.
|
|
229
|
+
|
|
230
|
+
The objective is not simply to identify the model with the highest accuracy.
|
|
231
|
+
|
|
232
|
+
The benchmark will investigate:
|
|
233
|
+
|
|
234
|
+
> **Predictive performance vs. computational cost vs. deployment feasibility.**
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 8. Experimental Tracking
|
|
239
|
+
|
|
240
|
+
Experiments will be tracked using **Weights & Biases (W&B)**.
|
|
241
|
+
|
|
242
|
+
Potentially tracked parameters include:
|
|
243
|
+
|
|
244
|
+
* Model architecture
|
|
245
|
+
* Learning rate
|
|
246
|
+
* Batch size
|
|
247
|
+
* Number of epochs
|
|
248
|
+
* Training loss
|
|
249
|
+
* Validation loss
|
|
250
|
+
* Accuracy
|
|
251
|
+
* Precision
|
|
252
|
+
* Recall
|
|
253
|
+
* Sensitivity
|
|
254
|
+
* Specificity
|
|
255
|
+
* F1 score
|
|
256
|
+
* Training duration
|
|
257
|
+
|
|
258
|
+
This enables systematic comparison between experiments instead of relying on isolated notebook outputs.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 9. Training Framework & Quickstart
|
|
263
|
+
|
|
264
|
+
The project builds on technologies explored during the Marconi internship:
|
|
265
|
+
|
|
266
|
+
* Python
|
|
267
|
+
* PyTorch
|
|
268
|
+
* Hugging Face Datasets
|
|
269
|
+
* scikit-learn
|
|
270
|
+
* Weights & Biases (W&B)
|
|
271
|
+
|
|
272
|
+
### Running the Training Pipeline (`train.py`)
|
|
273
|
+
|
|
274
|
+
A unified, single-execution training script (`train.py`) is provided for local execution and experiment tracking.
|
|
275
|
+
|
|
276
|
+
#### 1. Setup Dependencies
|
|
277
|
+
```bash
|
|
278
|
+
pip install -r requirements.txt
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### 2. Single-Command Training Examples
|
|
282
|
+
|
|
283
|
+
* **Default Run (ResNet-50 with W&B logging)**:
|
|
284
|
+
```bash
|
|
285
|
+
python train.py
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
* **Architecture Benchmarking (DenseNet-121 with Ben Graham Preprocessing)**:
|
|
289
|
+
```bash
|
|
290
|
+
python train.py --arch densenet121 --epochs 15 --batch-size 16 --ben-graham
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
* **Modern Vision Transformers (Swin Transformer / ConvNeXt)**:
|
|
294
|
+
```bash
|
|
295
|
+
python train.py --arch swin_t --epochs 10 --batch-size 16
|
|
296
|
+
python train.py --arch convnext_tiny --epochs 10 --batch-size 16
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
* **Local Offline Run (without Weights & Biases)**:
|
|
300
|
+
```bash
|
|
301
|
+
python train.py --arch mobilenet_v2 --epochs 5 --no-wandb
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### 3. Command-Line Options
|
|
305
|
+
|
|
306
|
+
| Argument | Default | Description |
|
|
307
|
+
| :--- | :--- | :--- |
|
|
308
|
+
| `--arch`, `-a` | `resnet50` | Architecture (`resnet50`, `resnet18`, `densenet121`, `convnext_tiny`, `swin_t`, `mobilenet_v2`) |
|
|
309
|
+
| `--dataset`, `-d` | `sngsfydy/aptos` | Hugging Face dataset identifier |
|
|
310
|
+
| `--epochs`, `-e` | `10` | Number of training epochs |
|
|
311
|
+
| `--batch-size`, `-b` | `16` | Batch size per step |
|
|
312
|
+
| `--lr` | `1e-4` | Learning rate |
|
|
313
|
+
| `--ben-graham` | `False` | Apply Ben Graham circular crop & local color subtraction |
|
|
314
|
+
| `--device` | `auto` | Execution device (`auto`, `cuda`, `cpu`, `mps`) |
|
|
315
|
+
| `--wandb-project` | `diabetic-retinopathy-screening` | W&B project name |
|
|
316
|
+
| `--no-wandb` | `False` | Disable W&B online tracking |
|
|
317
|
+
| `--save-dir` | `models` | Directory for model checkpoints |
|
|
318
|
+
| `--results-dir` | `results` | Directory for benchmark metric CSV tables |
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 10. Deployment
|
|
323
|
+
|
|
324
|
+
A trained model is not considered the final deliverable by itself.
|
|
325
|
+
|
|
326
|
+
The selected model will be integrated into a usable inference system.
|
|
327
|
+
|
|
328
|
+
Two deployment paths are being considered.
|
|
329
|
+
|
|
330
|
+
### Option A — Android
|
|
331
|
+
|
|
332
|
+
```text
|
|
333
|
+
Android Application
|
|
334
|
+
│
|
|
335
|
+
▼
|
|
336
|
+
Fundus Image
|
|
337
|
+
│
|
|
338
|
+
▼
|
|
339
|
+
Preprocessing
|
|
340
|
+
│
|
|
341
|
+
▼
|
|
342
|
+
ML Model
|
|
343
|
+
│
|
|
344
|
+
▼
|
|
345
|
+
Prediction
|
|
346
|
+
│
|
|
347
|
+
▼
|
|
348
|
+
Severity / Referral Risk
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Potential technologies:
|
|
352
|
+
|
|
353
|
+
* Kotlin
|
|
354
|
+
* Jetpack Compose
|
|
355
|
+
* ONNX Runtime
|
|
356
|
+
* TensorFlow Lite
|
|
357
|
+
|
|
358
|
+
### Option B — API
|
|
359
|
+
|
|
360
|
+
```text
|
|
361
|
+
Client
|
|
362
|
+
│
|
|
363
|
+
▼
|
|
364
|
+
HTTP POST /predict
|
|
365
|
+
│
|
|
366
|
+
▼
|
|
367
|
+
FastAPI
|
|
368
|
+
│
|
|
369
|
+
▼
|
|
370
|
+
Preprocessing
|
|
371
|
+
│
|
|
372
|
+
▼
|
|
373
|
+
Trained Model
|
|
374
|
+
│
|
|
375
|
+
▼
|
|
376
|
+
Prediction
|
|
377
|
+
│
|
|
378
|
+
▼
|
|
379
|
+
JSON Response
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Potential API stack:
|
|
383
|
+
|
|
384
|
+
* Python
|
|
385
|
+
* FastAPI
|
|
386
|
+
* Uvicorn
|
|
387
|
+
* PyTorch / Hugging Face
|
|
388
|
+
|
|
389
|
+
The final deployment path will be selected after evaluating the trained model's framework, size, conversion requirements, and inference performance.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## 11. Deployment-Oriented Model Selection
|
|
394
|
+
|
|
395
|
+
The final model will not necessarily be the model with the highest raw accuracy.
|
|
396
|
+
|
|
397
|
+
A model-selection framework will consider:
|
|
398
|
+
|
|
399
|
+
| Criterion | Importance |
|
|
400
|
+
| ------------------------ | ----------- |
|
|
401
|
+
| Sensitivity | High |
|
|
402
|
+
| Specificity | High |
|
|
403
|
+
| F1 score | High |
|
|
404
|
+
| Generalization | High |
|
|
405
|
+
| Model size | Medium–High |
|
|
406
|
+
| Parameter count | Medium |
|
|
407
|
+
| Inference latency | Medium–High |
|
|
408
|
+
| Deployment compatibility | High |
|
|
409
|
+
|
|
410
|
+
The goal is to identify a model that provides an appropriate balance between **screening performance and practical deployment requirements**.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## 12. Economic Rationale
|
|
415
|
+
|
|
416
|
+
The project is motivated by an operational healthcare problem rather than model development for its own sake.
|
|
417
|
+
|
|
418
|
+
A potential AI-assisted screening system could help:
|
|
419
|
+
|
|
420
|
+
* Extend screening capacity
|
|
421
|
+
* Prioritize patients for specialist review
|
|
422
|
+
* Reduce unnecessary specialist referrals
|
|
423
|
+
* Support healthcare workers without specialist expertise
|
|
424
|
+
* Reduce geographic barriers to initial screening
|
|
425
|
+
* Enable lower-cost preliminary assessment
|
|
426
|
+
|
|
427
|
+
The economic hypothesis is:
|
|
428
|
+
|
|
429
|
+
> **If AI can reliably identify higher-risk retinal images for specialist review, limited ophthalmology resources can potentially be allocated more efficiently.**
|
|
430
|
+
|
|
431
|
+
This project does **not** attempt to quantify the real-world cost savings without clinical and operational validation. Economic impact remains a hypothesis requiring field evaluation.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## 13. System Architecture
|
|
436
|
+
|
|
437
|
+
The target system is:
|
|
438
|
+
|
|
439
|
+
```text
|
|
440
|
+
┌─────────────────┐
|
|
441
|
+
│ Fundus Image │
|
|
442
|
+
└────────┬────────┘
|
|
443
|
+
│
|
|
444
|
+
▼
|
|
445
|
+
┌─────────────────┐
|
|
446
|
+
│ Preprocessing │
|
|
447
|
+
└────────┬────────┘
|
|
448
|
+
│
|
|
449
|
+
▼
|
|
450
|
+
┌─────────────────┐
|
|
451
|
+
│ Trained Model │
|
|
452
|
+
└────────┬────────┘
|
|
453
|
+
│
|
|
454
|
+
▼
|
|
455
|
+
┌─────────────────┐
|
|
456
|
+
│ Prediction │
|
|
457
|
+
└────────┬────────┘
|
|
458
|
+
│
|
|
459
|
+
┌─────────────┴─────────────┐
|
|
460
|
+
▼ ▼
|
|
461
|
+
Severity Estimate Confidence
|
|
462
|
+
│
|
|
463
|
+
▼
|
|
464
|
+
Screening / Triage
|
|
465
|
+
Recommendation
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## 14. Project Structure
|
|
471
|
+
|
|
472
|
+
The final repository is expected to evolve toward:
|
|
473
|
+
|
|
474
|
+
```text
|
|
475
|
+
marconi-diabetic-retinopathy/
|
|
476
|
+
│
|
|
477
|
+
├── README.md
|
|
478
|
+
├── requirements.txt
|
|
479
|
+
├── train.py # Single-command training script (HF & W&B)
|
|
480
|
+
│
|
|
481
|
+
├── data/
|
|
482
|
+
│ └── README.md
|
|
483
|
+
│
|
|
484
|
+
├── notebooks/
|
|
485
|
+
│ ├── 01_eda.ipynb
|
|
486
|
+
│ ├── 02_baseline.ipynb
|
|
487
|
+
│ ├── 03_model_comparison.ipynb
|
|
488
|
+
│ └── 04_final_model.ipynb
|
|
489
|
+
│
|
|
490
|
+
├── src/
|
|
491
|
+
│ ├── __init__.py
|
|
492
|
+
│ ├── data.py
|
|
493
|
+
│ ├── preprocessing.py
|
|
494
|
+
│ ├── models.py
|
|
495
|
+
│ ├── train.py
|
|
496
|
+
│ └── evaluate.py
|
|
497
|
+
│
|
|
498
|
+
├── models/
|
|
499
|
+
│ └── README.md
|
|
500
|
+
│
|
|
501
|
+
├── results/
|
|
502
|
+
│ ├── metrics.csv
|
|
503
|
+
│ ├── model_comparison.csv
|
|
504
|
+
│ └── figures/
|
|
505
|
+
│
|
|
506
|
+
├── deployment/
|
|
507
|
+
│ ├── api/
|
|
508
|
+
│ └── android/
|
|
509
|
+
│
|
|
510
|
+
└── presentation/
|
|
511
|
+
└── README.md
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
The exact structure will be adapted from the existing Marconi repositories rather than unnecessarily rebuilding working code.
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## 15. Existing Marconi Work
|
|
519
|
+
|
|
520
|
+
This project builds upon work completed during the internship.
|
|
521
|
+
|
|
522
|
+
### Deep-learning foundations
|
|
523
|
+
|
|
524
|
+
* Neural-network mechanics
|
|
525
|
+
* Forward propagation
|
|
526
|
+
* Backpropagation
|
|
527
|
+
* Gradient descent
|
|
528
|
+
* Keras
|
|
529
|
+
* PyTorch
|
|
530
|
+
* MNIST classification
|
|
531
|
+
|
|
532
|
+
### Transfer learning
|
|
533
|
+
|
|
534
|
+
* MobileNetV2
|
|
535
|
+
* Custom PyTorch datasets
|
|
536
|
+
* DataLoader pipelines
|
|
537
|
+
* Image transforms
|
|
538
|
+
|
|
539
|
+
### Architecture benchmarking
|
|
540
|
+
|
|
541
|
+
Existing repositories:
|
|
542
|
+
|
|
543
|
+
**ResNet vs DenseNet**
|
|
544
|
+
|
|
545
|
+
[https://github.com/MichaelMukiibi/resnet-v-densenet.git](https://github.com/MichaelMukiibi/resnet-v-densenet.git)
|
|
546
|
+
|
|
547
|
+
**ConvNeXt vs Swin**
|
|
548
|
+
|
|
549
|
+
[https://github.com/MichaelMukiibi/convnext-v-swin.git](https://github.com/MichaelMukiibi/convnext-v-swin.git)
|
|
550
|
+
|
|
551
|
+
### Hugging Face vision pipeline
|
|
552
|
+
|
|
553
|
+
Existing repository:
|
|
554
|
+
|
|
555
|
+
**Drone Landing Safety**
|
|
556
|
+
|
|
557
|
+
[https://github.com/MichaelMukiibi/drone-landing-safety.git](https://github.com/MichaelMukiibi/drone-landing-safety.git)
|
|
558
|
+
|
|
559
|
+
### Additional repositories
|
|
560
|
+
|
|
561
|
+
**Omniglot**
|
|
562
|
+
|
|
563
|
+
[https://github.com/MichaelMukiibi/omniglot.git](https://github.com/MichaelMukiibi/omniglot.git)
|
|
564
|
+
|
|
565
|
+
**Electricity Consumption**
|
|
566
|
+
|
|
567
|
+
[https://github.com/MichaelMukiibi/electricity-consumption.git](https://github.com/MichaelMukiibi/electricity-consumption.git)
|
|
568
|
+
|
|
569
|
+
The first development task is to inspect these repositories and reuse the most mature training/evaluation infrastructure.
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## 16. Immediate Execution Plan
|
|
574
|
+
|
|
575
|
+
### Phase 1 — Repository inspection
|
|
576
|
+
|
|
577
|
+
Inspect:
|
|
578
|
+
|
|
579
|
+
1. `convnext-v-swin`
|
|
580
|
+
2. `resnet-v-densenet`
|
|
581
|
+
3. `drone-landing-safety`
|
|
582
|
+
|
|
583
|
+
Identify:
|
|
584
|
+
|
|
585
|
+
* Training scripts
|
|
586
|
+
* Dataset loaders
|
|
587
|
+
* Hugging Face integration
|
|
588
|
+
* W&B integration
|
|
589
|
+
* Evaluation functions
|
|
590
|
+
* Existing checkpoints
|
|
591
|
+
* Preprocessing functions
|
|
592
|
+
* Existing plots
|
|
593
|
+
|
|
594
|
+
### Phase 2 — APTOS integration
|
|
595
|
+
|
|
596
|
+
Adapt the strongest existing pipeline to APTOS.
|
|
597
|
+
|
|
598
|
+
Perform:
|
|
599
|
+
|
|
600
|
+
* Dataset download
|
|
601
|
+
* Data validation
|
|
602
|
+
* EDA
|
|
603
|
+
* Train/validation split
|
|
604
|
+
* Preprocessing
|
|
605
|
+
* Baseline training
|
|
606
|
+
|
|
607
|
+
### Phase 3 — Model comparison
|
|
608
|
+
|
|
609
|
+
Train feasible candidates:
|
|
610
|
+
|
|
611
|
+
* ResNet
|
|
612
|
+
* DenseNet
|
|
613
|
+
* ConvNeXt
|
|
614
|
+
* Swin
|
|
615
|
+
|
|
616
|
+
Do not attempt unnecessarily large experiments given the closeout deadline.
|
|
617
|
+
|
|
618
|
+
### Phase 4 — Evaluation
|
|
619
|
+
|
|
620
|
+
Generate:
|
|
621
|
+
|
|
622
|
+
* Confusion matrices
|
|
623
|
+
* Accuracy
|
|
624
|
+
* Precision
|
|
625
|
+
* Recall
|
|
626
|
+
* Sensitivity
|
|
627
|
+
* Specificity
|
|
628
|
+
* F1
|
|
629
|
+
* Model size
|
|
630
|
+
* Inference latency
|
|
631
|
+
|
|
632
|
+
### Phase 5 — Final model
|
|
633
|
+
|
|
634
|
+
Select a model using:
|
|
635
|
+
|
|
636
|
+
```text
|
|
637
|
+
Clinical relevance
|
|
638
|
+
+
|
|
639
|
+
Predictive performance
|
|
640
|
+
+
|
|
641
|
+
Computational efficiency
|
|
642
|
+
+
|
|
643
|
+
Deployment feasibility
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Phase 6 — Deployment
|
|
647
|
+
|
|
648
|
+
Implement either:
|
|
649
|
+
|
|
650
|
+
* Android inference
|
|
651
|
+
|
|
652
|
+
or
|
|
653
|
+
|
|
654
|
+
* FastAPI inference service
|
|
655
|
+
|
|
656
|
+
### Phase 7 — Closeout
|
|
657
|
+
|
|
658
|
+
Prepare:
|
|
659
|
+
|
|
660
|
+
* Working demo
|
|
661
|
+
* Final repository
|
|
662
|
+
* Results
|
|
663
|
+
* Presentation
|
|
664
|
+
* Technical Q&A preparation
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## 17. Current Status
|
|
669
|
+
|
|
670
|
+
### Completed
|
|
671
|
+
|
|
672
|
+
* Deep-learning foundation training
|
|
673
|
+
* PyTorch training
|
|
674
|
+
* Transfer learning
|
|
675
|
+
* Medical ML evaluation concepts
|
|
676
|
+
* ResNet/DenseNet research
|
|
677
|
+
* ConvNeXt/Swin research
|
|
678
|
+
* W&B experiment tracking
|
|
679
|
+
* Hugging Face vision workflows
|
|
680
|
+
* EDA workflow development
|
|
681
|
+
|
|
682
|
+
### In progress
|
|
683
|
+
|
|
684
|
+
* Selecting the final economically meaningful capstone
|
|
685
|
+
* Adapting existing vision pipelines to APTOS 2019
|
|
686
|
+
* Model benchmarking
|
|
687
|
+
* Final model selection
|
|
688
|
+
* Deployment
|
|
689
|
+
|
|
690
|
+
### Pending
|
|
691
|
+
|
|
692
|
+
* Final trained APTOS model
|
|
693
|
+
* Final evaluation results
|
|
694
|
+
* Deployment implementation
|
|
695
|
+
* End-to-end demo
|
|
696
|
+
* Closeout presentation
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
## 18. Limitations
|
|
701
|
+
|
|
702
|
+
This prototype has important limitations.
|
|
703
|
+
|
|
704
|
+
* APTOS is a relatively small dataset compared with large-scale medical imaging datasets.
|
|
705
|
+
* Dataset distribution may not represent the target population.
|
|
706
|
+
* Dataset labels may contain uncertainty.
|
|
707
|
+
* Image quality can vary substantially.
|
|
708
|
+
* Model performance does not automatically translate to clinical effectiveness.
|
|
709
|
+
* External validation is required.
|
|
710
|
+
* Clinical workflow integration has not been established.
|
|
711
|
+
* Regulatory approval would be required for clinical deployment.
|
|
712
|
+
* Economic impact has not yet been empirically demonstrated.
|
|
713
|
+
|
|
714
|
+
Therefore, the system should be presented as an **AI-assisted research prototype**, not as an autonomous diagnostic product.
|
|
715
|
+
|
|
716
|
+
---
|
|
717
|
+
|
|
718
|
+
## 19. Future Work
|
|
719
|
+
|
|
720
|
+
Potential future development includes:
|
|
721
|
+
|
|
722
|
+
* External validation on additional retinal datasets
|
|
723
|
+
* Calibration of model confidence
|
|
724
|
+
* Explainability methods such as Grad-CAM
|
|
725
|
+
* Model quantization
|
|
726
|
+
* ONNX/TensorFlow Lite conversion
|
|
727
|
+
* On-device inference
|
|
728
|
+
* Offline-first deployment
|
|
729
|
+
* Prospective clinical validation
|
|
730
|
+
* Integration with existing health-information systems
|
|
731
|
+
* Human-in-the-loop referral workflows
|
|
732
|
+
* Health-economic evaluation
|
|
733
|
+
* Field testing in resource-constrained healthcare settings
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## 20. Expected Capstone Outcome
|
|
738
|
+
|
|
739
|
+
The final deliverable should demonstrate the complete ML lifecycle:
|
|
740
|
+
|
|
741
|
+
```text
|
|
742
|
+
REAL-WORLD PROBLEM
|
|
743
|
+
↓
|
|
744
|
+
DATA
|
|
745
|
+
↓
|
|
746
|
+
EDA
|
|
747
|
+
↓
|
|
748
|
+
MODEL DEVELOPMENT
|
|
749
|
+
↓
|
|
750
|
+
EXPERIMENT TRACKING
|
|
751
|
+
↓
|
|
752
|
+
ARCHITECTURE BENCHMARKING
|
|
753
|
+
↓
|
|
754
|
+
CLINICALLY RELEVANT EVALUATION
|
|
755
|
+
↓
|
|
756
|
+
MODEL SELECTION
|
|
757
|
+
↓
|
|
758
|
+
DEPLOYMENT
|
|
759
|
+
↓
|
|
760
|
+
WORKING DEMONSTRATION
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
The central objective is not merely to achieve a high classification score.
|
|
764
|
+
|
|
765
|
+
The project aims to demonstrate that the intern can take a meaningful problem from **research question → machine-learning experimentation → engineering implementation → deployable prototype**.
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
## 21. Project Status
|
|
770
|
+
|
|
771
|
+
**Status:** Active development
|
|
772
|
+
**Intern:** MUKIIBI MICHAEL KIRINNYA
|
|
773
|
+
**Capstone:** AI-Assisted Diabetic Retinopathy Screening & Triage
|
|
774
|
+
**Dataset:** APTOS 2019 Blindness Detection
|
|
775
|
+
**Primary ML Task:** Five-class diabetic-retinopathy severity classification
|
|
776
|
+
**Deployment Requirement:** Android application **or** inference API
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
fastdrs/__init__.py,sha256=gmXsoS_boRaNYtP4QD08ygvVkSwdI_dDd-gTC8pGedo,30
|
|
2
|
+
fastdrs/data.py,sha256=QmlE9o2if4kG1IRM99bihDvnnA5eSofE1pbKhPRY1Yk,4149
|
|
3
|
+
fastdrs/evaluation.py,sha256=y3OvOJYvhjUV2MCWwzHtigXpNYKbLuntY9jycnBNeZo,3955
|
|
4
|
+
fastdrs/models.py,sha256=n9o15ihvoTOjeUjPcok9APp6G8vFELGeZZl9aB7PIXA,3905
|
|
5
|
+
fastdrs/preprocessing.py,sha256=gR3PDxUrbYeb7oyyifBK7xv15ZQuchdMsmeG7Z915Sw,2980
|
|
6
|
+
fastdrs/training.py,sha256=zoNCjV896v6wzIRlSoLKWRhxhZ2UcBix1Iz1iBR1LEs,5487
|
|
7
|
+
fastdrs/deployment/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
fastdrs/deployment/litert.py,sha256=inAsJXL5qgVXnWRcEhtFEEc1Lp8Q4-MW2JLZGPCRUEE,3
|
|
9
|
+
fastdrs-0.1.0.dist-info/METADATA,sha256=8T1q4QjKUt7f6P4LJr-FzKsL2JtvJAF_IYZBHJzooK4,20054
|
|
10
|
+
fastdrs-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
11
|
+
fastdrs-0.1.0.dist-info/RECORD,,
|