--- 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.