MA29's picture
Add measured ensemble test accuracy (0.9910)
41bf89a verified
|
Raw History Blame Contribute Delete
4.43 kB
---
license: mit
library_name: keras
pipeline_tag: image-classification
tags:
- image-classification
- astronomy
- tensorflow
- keras
- transfer-learning
- ensemble
---
# Astro Image Classifier
An ensemble of two transfer-learned CNN branches that classifies astronomical
images into 11 classes.
- **Demo:** https://huggingface.co/spaces/MA29/astro-image-classifier
- **Code:** https://github.com/Majd1029/Astro-Image-Classifier
## Architecture
```
vgg_ensemble_input [None,224,224,3] -> vgg_branch_model -> softmax(11)
densenet_ensemble_input [None,224,224,3] -> densenet_branch_model -> softmax(11)
-> Average
```
- VGG19 and DenseNet201 backbones, ImageNet-initialised and partially unfrozen
- Custom heads: global average pooling, dense + batch norm, dropout
- The VGG branch includes a data-augmentation block, inactive at inference
- 40,056,982 parameters
- Saved with Keras 3.10; optimizer state stripped, so this file is inference-only
## Classes
`black_hole`, `earth`, `galaxy`, `jupiter`, `mars`, `mercury`, `neptune`,
`pluto`, `saturn`, `uranus`, `venus`
## Usage
**Both branches preprocess internally** — the densenet branch through a
`Lambda(preprocess_input)`, the vgg branch through a channel-swap and
mean-subtraction chain. Feed **raw 0-255 RGB**. Applying
`vgg19.preprocess_input` or `densenet.preprocess_input` beforehand preprocesses
twice and degrades predictions badly on some classes.
`custom_objects` is required: the densenet Lambda is serialised under the name
`preprocess_input`, and Keras resolves it by that name at load time.
```python
import numpy as np, tensorflow as tf, keras
from huggingface_hub import hf_hub_download
from PIL import Image
from tensorflow.keras.applications.densenet import preprocess_input
keras.mixed_precision.set_global_policy("float32") # saved under mixed_float16
path = hf_hub_download("MA29/astro-image-classifier", "ensemble_model.keras")
model = tf.keras.models.load_model(
path, custom_objects={"preprocess_input": preprocess_input}, compile=False
)
CLASSES = ["black_hole", "earth", "galaxy", "jupiter", "mars", "mercury",
"neptune", "pluto", "saturn", "uranus", "venus"]
img = Image.open("example.jpg").convert("RGB").resize((224, 224))
x = np.expand_dims(np.array(img, dtype="float32"), 0) # raw 0-255
names = [t.name.split(":")[0].split("/")[0] for t in model.inputs]
probs = model.predict({n: x for n in names}, verbose=0)[0]
print(CLASSES[int(np.argmax(probs))], probs.max())
```
## Evaluation
Held-out test split: 446 images, reconstructed from the training notebook's
`image_dataset_from_directory(validation_split=0.3, seed=42)` then
`temp_ds.skip(14)`. Per-class supports match the notebook's own reports.
| Model | Test accuracy |
|---|---|
| DenseNet201 branch alone | 0.9888 |
| VGG19 branch alone | 0.9888 |
| **Ensemble (this model)** | **0.9910** (442/446) |
Macro F1 0.990, weighted F1 0.991, mean confidence 98.4%.
| class | precision | recall | f1 | support |
|---|---|---|---|---|
| black_hole | 1.000 | 0.952 | 0.976 | 21 |
| earth | 1.000 | 1.000 | 1.000 | 35 |
| galaxy | 0.962 | 1.000 | 0.980 | 25 |
| jupiter | 1.000 | 1.000 | 1.000 | 39 |
| mars | 0.978 | 0.957 | 0.967 | 46 |
| mercury | 1.000 | 1.000 | 1.000 | 50 |
| neptune | 1.000 | 1.000 | 1.000 | 52 |
| pluto | 1.000 | 1.000 | 1.000 | 37 |
| saturn | 1.000 | 1.000 | 1.000 | 41 |
| uranus | 1.000 | 1.000 | 1.000 | 48 |
| venus | 0.962 | 0.981 | 0.971 | 52 |
All four errors: three venus/mars confusions, and one black_hole predicted as
galaxy at 100% confidence — worth knowing that high confidence is not a
reliable correctness signal here.
Note the test split is drawn from the same curated dataset as training, so this
figure reflects in-distribution performance only.
## Limitations
- Trained on a curated, fairly clean dataset of planet and deep-sky imagery; it
has not been evaluated on telescope captures, noisy frames or unusual crops.
- Closed-world over 11 classes — every input is forced into one of them. There
is no "none of the above", so out-of-distribution images get confident and
meaningless labels.
- Several classes are visually similar under poor lighting or low resolution;
treat single-image predictions as indicative.