A deployed deep learning app that classifies dermoscopic skin lesion images into 7 diagnostic categories, with Grad-CAM interpretability so predictions aren't a black box.
Live app: [add your Streamlit Community Cloud link here after Day 10 deployment]
⚠️ Educational project only. Not a medical diagnostic tool. Predictions from this app should never be used to make real health decisions — consult a dermatologist for any actual skin concern.
This project fine-tunes a pretrained MobileNetV2 on the HAM10000 dataset (~10,000 dermoscopic images across 7 lesion types) and wraps it in a Streamlit app with:
- Predicted class + per-class confidence scores
- Grad-CAM heatmap visualization showing which image regions drove the prediction
- A low-confidence flag that defers to "consult a professional" rather than asserting a guess
Built end-to-end on a CPU-only machine — no GPU required for training or inference.
HAM10000 — 7 classes:
| Code | Meaning |
|---|---|
| akiec | Actinic keratosis / intraepithelial carcinoma |
| bcc | Basal cell carcinoma |
| bkl | Benign keratosis |
| df | Dermatofibroma |
| mel | Melanoma |
| nv | Melanocytic nevus (benign mole) |
| vasc | Vascular lesion |
Class imbalance: nv makes up roughly two-thirds of the dataset; several classes (e.g. df) have under 150 samples. This shaped the entire training approach — see Methodology below.
- Model: MobileNetV2, pretrained on ImageNet. Backbone frozen except the last inverted-residual block, which was fine-tuned alongside a newly trained classifier head. This keeps training CPU-feasible (~2–5 min/epoch) while still adapting the model to dermoscopy-specific features.
- Input size: 160x160, normalized with ImageNet mean/std.
- Class imbalance handling: weighted cross-entropy loss (inverse class frequency). A
WeightedRandomSamplervariant was also tested (seesrc/train_v2.py); results were compared before selecting the final model. - Split: stratified 70/15/15 train/val/test, so class proportions are preserved across all three sets.
- Evaluation metric: macro-F1 and balanced accuracy, not plain accuracy — with
nvat ~67% of the data, plain accuracy would reward a model that mostly just predicts the majority class.
| Metric | Value |
|---|---|
| Balanced accuracy | 0.69 |
| Macro-F1 | 0.51 |
| Melanoma (mel) recall | 0.63 |
Full per-class report and confusion matrix are in checkpoints/ (generated by src/evaluate_test.py).
Why these numbers, in plain terms: the model trades precision for recall on rare/high-stakes classes — it flags more false positives on conditions like melanoma and basal cell carcinoma in exchange for catching more true positives. For a screening-style tool, this is arguably the safer direction to err: a false alarm sends someone to a dermatologist unnecessarily, while a missed melanoma does not.
Grad-CAM was used to visualize which regions of each image most influenced the model's prediction (see src/gradcam_utils.py, src/test_gradcam.py).
- In most cases, the heatmap correctly focused on the lesion itself rather than surrounding skin.
- One documented limitation: on at least one misclassified
nv(benign mole) sample, the heatmap showed partial attention on a non-lesion artifact (a skin-marking tick visible in the original dermoscopic image) alongside the genuine lesion region. This is a known issue in dermoscopy ML more broadly — datasets like HAM10000 contain rulers, ink marks, and hair that models can partially key on if not explicitly preprocessed away (e.g. via hair/artifact inpainting, which was out of scope for this project).
This kind of self-critical interpretability check — rather than only reporting a headline accuracy number — was a deliberate part of the project, not an afterthought.
skin-lesion-classifier/
├── app.py # Streamlit app (entry point)
├── requirements.txt
├── checkpoints/ # trained model weights (not tracked in git — see below)
├── data/ # HAM10000 dataset (not tracked in git — see below)
├── notebooks/ # exploratory notebooks, one per project day
└── src/
├── dataset.py # data loading, splits, transforms
├── model.py # MobileNetV2 transfer learning setup
├── evaluate.py # class weights, weighted loss, evaluate()
├── train.py # training loop (v1 — weighted loss only)
├── train_v2.py # training loop (v2 — weighted sampler variant)
├── evaluate_test.py # test-set evaluation + confusion matrix
├── gradcam_utils.py # Grad-CAM generation utilities
└── test_gradcam.py # Grad-CAM sanity-check script
git clone <your-repo-url>
cd skin-lesion-classifier
python -m venv venv
venv\Scripts\activate # Windows; use source venv/bin/activate on Mac/Linux
pip install -r requirements.txtData: download HAM10000 from Kaggle and place it in data/ (see structure above). Not included in this repo due to size.
Model weights: trained weights are hosted separately (not committed to git due to file size) — see download instructions below.
Run the app:
streamlit run app.py- Trained on CPU with a frozen backbone; a fully fine-tuned model on GPU would likely improve macro-F1 further.
- Precision on rare classes is low (more false positives) — a deliberate trade-off, but worth knowing before drawing conclusions from any single prediction.
- Grad-CAM analysis surfaced at least one case of partial attention on non-lesion artifacts (see Interpretability above).
- This is a portfolio/demo project, not validated for clinical use in any capacity.
- Dataset: Tschandl, P., Rosendahl, C. & Kittler, H. The HAM10000 dataset. Sci Data 5, 180161 (2018).
- Base architecture: MobileNetV2 (Sandler et al., 2018), pretrained weights via torchvision.