Upload folder using huggingface_hub
Browse files- .gitignore +30 -0
- README.md +168 -3
- app.py +74 -0
- convert_model.py +27 -0
- inference_log.py +47 -0
- inference_log.txt +12 -0
- load_predict_log.txt +7 -0
- predict.py +88 -0
- requirements.txt +6 -0
- run_load_and_predict.py +91 -0
- space_requirements.txt +5 -0
- test_predict.py +27 -0
- train.py +273 -0
.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.vscode/
|
| 2 |
+
.vs/
|
| 3 |
+
.venv/
|
| 4 |
+
venv/
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.py[cod]
|
| 7 |
+
*.egg-info/
|
| 8 |
+
dist/
|
| 9 |
+
build/
|
| 10 |
+
*.h5
|
| 11 |
+
*.keras
|
| 12 |
+
saved_model_*/
|
| 13 |
+
*.pb
|
| 14 |
+
checkpoints/
|
| 15 |
+
*.ckpt
|
| 16 |
+
data/
|
| 17 |
+
data/**
|
| 18 |
+
UTKFace/
|
| 19 |
+
models/
|
| 20 |
+
models/**
|
| 21 |
+
*.log
|
| 22 |
+
*.npy
|
| 23 |
+
*.npz
|
| 24 |
+
*.tflite
|
| 25 |
+
*.zip
|
| 26 |
+
*.tar.gz
|
| 27 |
+
.env
|
| 28 |
+
.env.*
|
| 29 |
+
.DS_Store
|
| 30 |
+
.ipynb_checkpoints/
|
README.md
CHANGED
|
@@ -1,3 +1,168 @@
|
|
| 1 |
-
---
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language: en
|
| 3 |
+
license: mit
|
| 4 |
+
tags: ["image-regression", "tensorflow", "mobilenetv2", "utkface", "age-estimation"]
|
| 5 |
+
datasets: ["UTKFace"]
|
| 6 |
+
metrics: ["mean_absolute_error"]
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
# UTKFace Age Regression — Model Card
|
| 10 |
+
|
| 11 |
+
This repository contains code to train a TensorFlow / Keras regression model that estimates a person's age from a face image using the UTKFace dataset. The model uses a MobileNetV2 backbone and a small regression head on top.
|
| 12 |
+
|
| 13 |
+
## Summary
|
| 14 |
+
|
| 15 |
+
- **Model type**: Image regression (single-output continuous)
|
| 16 |
+
- **Backbone**: MobileNetV2 (ImageNet pre-trained)
|
| 17 |
+
- **Task**: Age estimation (years)
|
| 18 |
+
- **Dataset**: UTKFace (public dataset; filenames encode age)
|
| 19 |
+
- **Reported metric**: Mean Absolute Error (MAE) — see Evaluation section for how to compute and report MAE for your runs
|
| 20 |
+
|
| 21 |
+
## Model details
|
| 22 |
+
|
| 23 |
+
- **Input**: RGB face image (recommended size: 224×224)
|
| 24 |
+
- **Output**: Single scalar value — predicted age in years
|
| 25 |
+
- **Preprocessing**: MobileNetV2 preprocessing (scales inputs to [-1, 1])
|
| 26 |
+
- **Loss**: Mean Squared Error (MSE) used during training
|
| 27 |
+
- **Metric for reporting**: Mean Absolute Error (MAE)
|
| 28 |
+
|
| 29 |
+
## Intended uses
|
| 30 |
+
|
| 31 |
+
- Research and educational purposes for learning about image regression and age estimation
|
| 32 |
+
- Prototyping demo applications that predict approximate age ranges from face crops
|
| 33 |
+
|
| 34 |
+
## Out-of-scope / Limitations
|
| 35 |
+
|
| 36 |
+
- This model provides an estimate of age; it's not a substitute for official identification
|
| 37 |
+
- Models trained on UTKFace carry dataset biases (race, gender, age distribution). They may underperform on underrepresented groups.
|
| 38 |
+
- Do not use this model for high-stakes decision making (employment, legal, medical, etc.)
|
| 39 |
+
|
| 40 |
+
## Dataset
|
| 41 |
+
|
| 42 |
+
**UTKFace**
|
| 43 |
+
|
| 44 |
+
- **Source**: https://susanqq.github.io/UTKFace/
|
| 45 |
+
- **Format**: Filenames encode metadata as `<age>_<gender>_<race>_<date&time>.jpg`.
|
| 46 |
+
- **Usage**: The training scripts in this repo extract the age from the filename (the integer before the first underscore).
|
| 47 |
+
- **Note**: Respect the dataset's license and authors when redistributing or publishing results.
|
| 48 |
+
|
| 49 |
+
## Training details
|
| 50 |
+
|
| 51 |
+
- **Framework**: TensorFlow / Keras
|
| 52 |
+
- **Backbone**: MobileNetV2 pretrained on ImageNet
|
| 53 |
+
- **Head**: GlobalAveragePooling2D -> Dense(128, relu) -> Dense(1, linear)
|
| 54 |
+
- **Recommended input size**: 224×224 (configurable via command-line args in `train.py`)
|
| 55 |
+
- **Batch size**: configurable (default set in `train.py`)
|
| 56 |
+
- **Optimizer**: Adam (default), learning rate and scheduler configurable in `train.py`
|
| 57 |
+
- **Loss**: Mean Squared Error (MSE)
|
| 58 |
+
- **Metric**: Mean Absolute Error (MAE) reported on validation/test sets
|
| 59 |
+
- **Augmentations**: Basic augmentations recommended (flip, random crop/brightness) for better robustness
|
| 60 |
+
|
| 61 |
+
## Reproducibility / Example training command
|
| 62 |
+
|
| 63 |
+
1. **Prepare UTKFace dataset**
|
| 64 |
+
- Download and extract UTKFace images into `data/UTKFace/` or pass `--dataset_dir` to the training script.
|
| 65 |
+
2. **Install dependencies**
|
| 66 |
+
- `python -m pip install -r requirements.txt`
|
| 67 |
+
3. **Train**
|
| 68 |
+
- `python train.py --dataset_dir data/UTKFace --epochs 30 --batch_size 32 --img_size 224 --output_dir saved_model`
|
| 69 |
+
|
| 70 |
+
The `train.py` script builds a tf.data pipeline, extracts ages from filenames, constructs a MobileNetV2-based model, and saves the trained model to the `--output_dir`.
|
| 71 |
+
|
| 72 |
+
## Evaluation and metrics (MAE)
|
| 73 |
+
|
| 74 |
+
Mean Absolute Error (MAE) gives an intuitive measure of average error in predicted age (in years):
|
| 75 |
+
|
| 76 |
+
```
|
| 77 |
+
MAE = mean(|y_true - y_pred|)
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
Compute MAE in Python (example):
|
| 81 |
+
|
| 82 |
+
```python
|
| 83 |
+
import numpy as np
|
| 84 |
+
mae = np.mean(np.abs(y_true - y_pred))
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Example: the training script prints per-epoch validation MAE. To reproduce test MAE after training, run the provided evaluation routine or:
|
| 88 |
+
|
| 89 |
+
```python
|
| 90 |
+
from tensorflow import keras
|
| 91 |
+
import numpy as np
|
| 92 |
+
model = keras.models.load_model('saved_model')
|
| 93 |
+
# prepare test_images, test_labels arrays
|
| 94 |
+
preds = model.predict(test_images).squeeze()
|
| 95 |
+
mae = float(np.mean(np.abs(test_labels - preds)))
|
| 96 |
+
print('Test MAE (years):', mae)
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
Note: Exact MAE depends on preprocessing, train/validation split, augmentations, and hyperparameters. Report MAE alongside the exact training configuration for reproducibility.
|
| 100 |
+
|
| 101 |
+
## Usage — Quick examples
|
| 102 |
+
|
| 103 |
+
**Python (local SavedModel)**
|
| 104 |
+
|
| 105 |
+
```python
|
| 106 |
+
import tensorflow as tf
|
| 107 |
+
import numpy as np
|
| 108 |
+
from PIL import Image
|
| 109 |
+
from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
|
| 110 |
+
|
| 111 |
+
model = tf.keras.models.load_model('saved_model') # path to a SavedModel directory
|
| 112 |
+
img = Image.open('path/to/face.jpg').convert('RGB').resize((224, 224))
|
| 113 |
+
arr = np.array(img, dtype=np.float32)
|
| 114 |
+
arr = preprocess_input(arr)
|
| 115 |
+
pred = model.predict(np.expand_dims(arr, 0))[0, 0]
|
| 116 |
+
print('Predicted age (years):', float(pred))
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
**Command-line (using predict.py)**
|
| 120 |
+
|
| 121 |
+
```
|
| 122 |
+
python predict.py --model_dir saved_model --image path/to/face.jpg
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
**Loading from Hugging Face Hub**
|
| 126 |
+
|
| 127 |
+
If you upload your saved model to the Hugging Face Hub, Consumers can download it using the `huggingface_hub` package. For example, in a Space, set the environment variable `HF_MODEL_ID` to the model repository (e.g. `username/my-age-model`) and the Gradio app supplied in this repo will attempt to download and use it.
|
| 128 |
+
|
| 129 |
+
**Gradio demo / Hugging Face Space**
|
| 130 |
+
|
| 131 |
+
A simple Gradio app is provided in `app.py` that:
|
| 132 |
+
|
| 133 |
+
- accepts an input face image
|
| 134 |
+
- preprocesses it (224×224 + MobileNetV2 preprocess)
|
| 135 |
+
- returns the predicted age (years) and the model's raw output
|
| 136 |
+
|
| 137 |
+
**How to host as a Space**
|
| 138 |
+
|
| 139 |
+
1. Create a new Space on Hugging Face and select "Gradio" as the SDK.
|
| 140 |
+
2. Push this repository to the Space (include `app.py`, your `saved_model/` directory or set `HF_MODEL_ID` to your model on the Hub).
|
| 141 |
+
3. Make sure `requirements.txt` includes `gradio` and `huggingface_hub` (the repository `requirements.txt` in this project may be extended with these packages for the Space).
|
| 142 |
+
|
| 143 |
+
## Files in this repository
|
| 144 |
+
|
| 145 |
+
- `train.py` — training script
|
| 146 |
+
- `predict.py` — single-image prediction helper
|
| 147 |
+
- `convert_model.py` — conversion helpers
|
| 148 |
+
- `inference_log.py`, `inference_log.txt`, `load_predict_log.txt` — logging and CLI helpers for inference (dev)
|
| 149 |
+
- `app.py` — (added) Gradio demo app for live predictions
|
| 150 |
+
- `requirements.txt` — Python dependencies (extend for Spaces with `gradio` and `huggingface_hub`)
|
| 151 |
+
|
| 152 |
+
## Security, biases and ethical considerations
|
| 153 |
+
|
| 154 |
+
- Age estimation models can reflect and amplify biases in the training data (race and gender imbalance, age distribution). Evaluate fairness across demographic slices before using widely.
|
| 155 |
+
- Avoid using the model in high-risk contexts where inaccurate age estimates could cause harm.
|
| 156 |
+
|
| 157 |
+
## How to cite / license
|
| 158 |
+
|
| 159 |
+
- UTKFace authors and dataset should be cited if you publish results.
|
| 160 |
+
- This repository is provided under the MIT license (see LICENSE file if present).
|
| 161 |
+
|
| 162 |
+
## Contact and credits
|
| 163 |
+
|
| 164 |
+
**Maintainer**: Stealth Labs Ltd.
|
| 165 |
+
|
| 166 |
+
**Acknowledgements**
|
| 167 |
+
|
| 168 |
+
Thanks to the UTKFace dataset authors for the publicly available images used in training and experimentation.
|
app.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import tensorflow as tf
|
| 5 |
+
from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
|
| 6 |
+
import gradio as gr
|
| 7 |
+
|
| 8 |
+
# Try to load a local SavedModel first, otherwise try to download from Hugging Face Hub
|
| 9 |
+
MODEL_DIR = "saved_model"
|
| 10 |
+
model = None
|
| 11 |
+
|
| 12 |
+
if os.path.isdir(MODEL_DIR):
|
| 13 |
+
try:
|
| 14 |
+
model = tf.keras.models.load_model(MODEL_DIR)
|
| 15 |
+
print(f"Loaded model from local path: {MODEL_DIR}")
|
| 16 |
+
except Exception as e:
|
| 17 |
+
print(f"Failed to load local model: {e}")
|
| 18 |
+
|
| 19 |
+
if model is None:
|
| 20 |
+
# If HF_MODEL_ID is set, attempt to download model files from the Hub
|
| 21 |
+
HF_MODEL_ID = os.environ.get("HF_MODEL_ID")
|
| 22 |
+
if HF_MODEL_ID:
|
| 23 |
+
try:
|
| 24 |
+
from huggingface_hub import snapshot_download
|
| 25 |
+
repo_dir = snapshot_download(repo_id=HF_MODEL_ID)
|
| 26 |
+
# Expecting a SavedModel dir inside the repo; try to load from repo root
|
| 27 |
+
model = tf.keras.models.load_model(repo_dir)
|
| 28 |
+
print(f"Loaded model from HF Hub repo: {HF_MODEL_ID}")
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"Failed to load model from HF Hub ({HF_MODEL_ID}): {e}")
|
| 31 |
+
|
| 32 |
+
if model is None:
|
| 33 |
+
raise RuntimeError(
|
| 34 |
+
"No model found. Place a SavedModel in './saved_model' or set HF_MODEL_ID env var to a Hugging Face model repo containing a SavedModel."
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
INPUT_SIZE = (224, 224)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def predict_age(image: Image.Image):
|
| 41 |
+
if image.mode != 'RGB':
|
| 42 |
+
image = image.convert('RGB')
|
| 43 |
+
image = image.resize(INPUT_SIZE)
|
| 44 |
+
arr = np.array(image).astype(np.float32)
|
| 45 |
+
arr = preprocess_input(arr)
|
| 46 |
+
arr = np.expand_dims(arr, 0)
|
| 47 |
+
|
| 48 |
+
pred = model.predict(arr)[0]
|
| 49 |
+
# Ensure scalar
|
| 50 |
+
if hasattr(pred, '__len__'):
|
| 51 |
+
pred = float(np.asarray(pred).squeeze())
|
| 52 |
+
else:
|
| 53 |
+
pred = float(pred)
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
"predicted_age": round(pred, 2),
|
| 57 |
+
"raw_output": float(pred)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
demo = gr.Interface(
|
| 62 |
+
fn=predict_age,
|
| 63 |
+
inputs=gr.Image(type='pil', label='Face image (crop to face for best results)'),
|
| 64 |
+
outputs=[
|
| 65 |
+
gr.Number(label='Predicted age (years)'),
|
| 66 |
+
gr.Number(label='Raw model output')
|
| 67 |
+
],
|
| 68 |
+
examples=[],
|
| 69 |
+
title='UTKFace Age Estimator',
|
| 70 |
+
description='Upload a cropped face image and the model will predict age in years. For Spaces, set the HF_MODEL_ID environment variable to your Hugging Face model repo if you want the app to download a SavedModel from the Hub.'
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
if __name__ == '__main__':
|
| 74 |
+
demo.launch(server_name='0.0.0.0', server_port=int(os.environ.get('PORT', 7860)))
|
convert_model.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tensorflow as tf
|
| 2 |
+
|
| 3 |
+
print('loading best_model.h5...')
|
| 4 |
+
try:
|
| 5 |
+
# Load without compiling to avoid deserializing legacy training configs/metrics
|
| 6 |
+
m = tf.keras.models.load_model('best_model.h5', compile=False)
|
| 7 |
+
except Exception as e:
|
| 8 |
+
print('Failed to load best_model.h5:', e)
|
| 9 |
+
raise
|
| 10 |
+
|
| 11 |
+
# Try to export to the TF SavedModel format first
|
| 12 |
+
try:
|
| 13 |
+
m.export('saved_model_age_regressor')
|
| 14 |
+
print('Exported SavedModel to ./saved_model_age_regressor')
|
| 15 |
+
except Exception as e:
|
| 16 |
+
print('Export to SavedModel failed:', e)
|
| 17 |
+
# Fallback: save as Keras native single-file and HDF5 for compatibility
|
| 18 |
+
try:
|
| 19 |
+
m.save('saved_model_age_regressor.keras')
|
| 20 |
+
print('Saved Keras model to ./saved_model_age_regressor.keras')
|
| 21 |
+
except Exception as e2:
|
| 22 |
+
print('Saving Keras native format failed:', e2)
|
| 23 |
+
try:
|
| 24 |
+
m.save('final_model.h5')
|
| 25 |
+
print('Saved HDF5 model to ./final_model.h5')
|
| 26 |
+
except Exception as e3:
|
| 27 |
+
print('Saving HDF5 format failed:', e3)
|
inference_log.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import traceback
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
log_path = Path('inference_log.txt')
|
| 5 |
+
with log_path.open('w', encoding='utf-8') as f:
|
| 6 |
+
def log(*args, **kwargs):
|
| 7 |
+
print(*args, file=f, **kwargs)
|
| 8 |
+
f.flush()
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
log('Starting inference log')
|
| 12 |
+
import tensorflow as tf
|
| 13 |
+
import numpy as np
|
| 14 |
+
from PIL import Image
|
| 15 |
+
|
| 16 |
+
model_path = 'saved_model_age_regressor'
|
| 17 |
+
img_path = Path('data/UTKFace/53_1_1_20170110122449716.jpg.chip.jpg')
|
| 18 |
+
|
| 19 |
+
log('Model path:', model_path)
|
| 20 |
+
log('Image path:', str(img_path))
|
| 21 |
+
|
| 22 |
+
log('Attempting to load model with compile=False...')
|
| 23 |
+
m = tf.keras.models.load_model(model_path, compile=False)
|
| 24 |
+
log('Loaded model type:', type(m))
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
m.summary(print_fn=lambda *a, **k: log(*a, **k))
|
| 28 |
+
except Exception as e:
|
| 29 |
+
log('model.summary failed:', e)
|
| 30 |
+
|
| 31 |
+
img = Image.open(img_path).convert('RGB').resize((224,224))
|
| 32 |
+
arr = np.array(img, dtype=np.float32)/255.0
|
| 33 |
+
x = np.expand_dims(arr, 0)
|
| 34 |
+
log('Input shape:', x.shape)
|
| 35 |
+
|
| 36 |
+
log('Running predict...')
|
| 37 |
+
pred = m.predict(x)
|
| 38 |
+
log('Raw prediction output:', pred, 'shape:', getattr(pred, 'shape', None))
|
| 39 |
+
try:
|
| 40 |
+
log('Predicted age:', float(pred.flatten()[0]))
|
| 41 |
+
except Exception as e:
|
| 42 |
+
log('Error converting prediction to float:', e)
|
| 43 |
+
|
| 44 |
+
log('Inference finished successfully')
|
| 45 |
+
except Exception:
|
| 46 |
+
traceback.print_exc(file=f)
|
| 47 |
+
log('Inference script caught exception')
|
inference_log.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Starting inference log
|
| 2 |
+
Model path: saved_model_age_regressor
|
| 3 |
+
Image path: data\UTKFace\53_1_1_20170110122449716.jpg.chip.jpg
|
| 4 |
+
Attempting to load model with compile=False...
|
| 5 |
+
Traceback (most recent call last):
|
| 6 |
+
File "C:\Users\SammyHarris\Downloads\Age-classification-model\inference_log.py", line 23, in <module>
|
| 7 |
+
m = tf.keras.models.load_model(model_path, compile=False)
|
| 8 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 9 |
+
File "C:\Users\SammyHarris\Downloads\Age-classification-model\.venv\Lib\site-packages\keras\src\saving\saving_api.py", line 209, in load_model
|
| 10 |
+
raise ValueError(
|
| 11 |
+
ValueError: File format not supported: filepath=saved_model_age_regressor. Keras 3 only supports V3 `.keras` files and legacy H5 format files (`.h5` extension). Note that the legacy SavedModel format is not supported by `load_model()` in Keras 3. In order to reload a TensorFlow SavedModel as an inference-only layer in Keras 3, use `keras.layers.TFSMLayer(saved_model_age_regressor, call_endpoint='serving_default')` (note that your `call_endpoint` might have a different name).
|
| 12 |
+
Inference script caught exception
|
load_predict_log.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Starting model load & predict test
|
| 2 |
+
Image path: data\UTKFace\53_1_1_20170110122449716.jpg.chip.jpg
|
| 3 |
+
HDF5/.keras not found; attempting to wrap TF SavedModel using TFSMLayer...
|
| 4 |
+
Wrapper model created; running predict...
|
| 5 |
+
Prediction returned a dict with keys: ['output_0']
|
| 6 |
+
Output 'output_0': shape=(1, 1) values=[28.5549259185791]
|
| 7 |
+
Finished load & predict test
|
predict.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Load a trained age regression model and run a prediction on a single image.
|
| 3 |
+
|
| 4 |
+
Usage: python predict.py --model_path saved_model_age_regressor --image_path some_image.jpg
|
| 5 |
+
"""
|
| 6 |
+
import argparse
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
from PIL import Image
|
| 11 |
+
import tensorflow as tf
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def parse_args():
|
| 15 |
+
parser = argparse.ArgumentParser()
|
| 16 |
+
parser.add_argument('--model_path', type=str, default='saved_model_age_regressor')
|
| 17 |
+
parser.add_argument('--image_path', type=str, required=True)
|
| 18 |
+
parser.add_argument('--img_size', type=int, default=224)
|
| 19 |
+
parser.add_argument('--output_key', type=str, default=None,
|
| 20 |
+
help='If the model returns a dict, select this key for the numeric prediction. If omitted the first numeric output will be used.')
|
| 21 |
+
return parser.parse_args()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def load_image(path, img_size):
|
| 25 |
+
img = Image.open(path).convert('RGB')
|
| 26 |
+
img = img.resize((img_size, img_size))
|
| 27 |
+
arr = np.array(img, dtype=np.float32) / 255.0
|
| 28 |
+
return arr
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main():
|
| 32 |
+
args = parse_args()
|
| 33 |
+
model_path = Path(args.model_path)
|
| 34 |
+
# Load Keras .h5/.keras files directly, and attempt Keras load for directories first.
|
| 35 |
+
if model_path.is_file() and model_path.suffix.lower() in ('.h5', '.keras'):
|
| 36 |
+
model = tf.keras.models.load_model(str(model_path), compile=False)
|
| 37 |
+
print(f"Loaded Keras model file: {model_path}")
|
| 38 |
+
elif model_path.is_dir():
|
| 39 |
+
# Some SavedModel directories are not loadable with tf.keras.load_model in Keras 3;
|
| 40 |
+
# try load_model first (covers .keras saved dirs), otherwise wrap with TFSMLayer.
|
| 41 |
+
try:
|
| 42 |
+
model = tf.keras.models.load_model(str(model_path), compile=False)
|
| 43 |
+
print(f"Loaded Keras-compatible model from directory: {model_path}")
|
| 44 |
+
except Exception:
|
| 45 |
+
# Wrap the SavedModel with a TFSMLayer for inference compatibility in Keras.
|
| 46 |
+
try:
|
| 47 |
+
tf_layer = tf.keras.layers.TFSMLayer(str(model_path), call_endpoint='serving_default')
|
| 48 |
+
model = tf.keras.Sequential([
|
| 49 |
+
tf.keras.Input(shape=(args.img_size, args.img_size, 3)),
|
| 50 |
+
tf_layer,
|
| 51 |
+
])
|
| 52 |
+
print(f"Wrapped TensorFlow SavedModel at {model_path} with TFSMLayer (serving_default).")
|
| 53 |
+
except Exception as e:
|
| 54 |
+
raise RuntimeError(f"Failed to load or wrap SavedModel directory '{model_path}': {e}")
|
| 55 |
+
else:
|
| 56 |
+
# Unknown path type: try load_model and allow it to raise a helpful exception.
|
| 57 |
+
model = tf.keras.models.load_model(str(model_path), compile=False)
|
| 58 |
+
print(f"Loaded model from path: {model_path}")
|
| 59 |
+
image_path = Path(args.image_path)
|
| 60 |
+
if not image_path.exists():
|
| 61 |
+
raise FileNotFoundError(f"Image not found: {image_path}")
|
| 62 |
+
x = load_image(image_path, args.img_size)
|
| 63 |
+
x = np.expand_dims(x, axis=0)
|
| 64 |
+
pred = model.predict(x)
|
| 65 |
+
|
| 66 |
+
# If the model returns a dict (typical for a wrapped SavedModel serving signature),
|
| 67 |
+
# select the requested output key or fall back to the first available numeric output.
|
| 68 |
+
if isinstance(pred, dict):
|
| 69 |
+
if args.output_key:
|
| 70 |
+
if args.output_key not in pred:
|
| 71 |
+
raise KeyError(f"Requested output key '{args.output_key}' not found. Available keys: {list(pred.keys())}")
|
| 72 |
+
chosen = pred[args.output_key]
|
| 73 |
+
else:
|
| 74 |
+
first_key = next(iter(pred.keys()))
|
| 75 |
+
print(f"No --output_key provided; using first output key: '{first_key}'")
|
| 76 |
+
chosen = pred[first_key]
|
| 77 |
+
arr = np.asarray(chosen)
|
| 78 |
+
else:
|
| 79 |
+
arr = np.asarray(pred)
|
| 80 |
+
|
| 81 |
+
if arr.size == 0:
|
| 82 |
+
raise ValueError("Model returned an empty prediction.")
|
| 83 |
+
age_pred = float(arr.flatten()[0])
|
| 84 |
+
print(f"Predicted age: {age_pred:.2f} years")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
if __name__ == '__main__':
|
| 88 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tensorflow>=2.10.0
|
| 2 |
+
numpy
|
| 3 |
+
pillow
|
| 4 |
+
matplotlib
|
| 5 |
+
requests
|
| 6 |
+
tqdm
|
run_load_and_predict.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import traceback
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
log_path = Path('load_predict_log.txt')
|
| 5 |
+
with log_path.open('w', encoding='utf-8') as f:
|
| 6 |
+
def log(*args, **kwargs):
|
| 7 |
+
print(*args, file=f, **kwargs)
|
| 8 |
+
f.flush()
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
log('Starting model load & predict test')
|
| 12 |
+
import tensorflow as tf
|
| 13 |
+
import numpy as np
|
| 14 |
+
from PIL import Image
|
| 15 |
+
import os
|
| 16 |
+
|
| 17 |
+
img_path = Path('data/UTKFace/53_1_1_20170110122449716.jpg.chip.jpg')
|
| 18 |
+
log('Image path:', str(img_path))
|
| 19 |
+
|
| 20 |
+
# Try HDF5 / .h5 first
|
| 21 |
+
h5_path = Path('final_model.h5')
|
| 22 |
+
keras_path = Path('saved_model_age_regressor.keras')
|
| 23 |
+
saved_model_dir = Path('saved_model_age_regressor')
|
| 24 |
+
|
| 25 |
+
if h5_path.exists():
|
| 26 |
+
try:
|
| 27 |
+
log('Attempting to load HDF5 model:', str(h5_path))
|
| 28 |
+
m = tf.keras.models.load_model(str(h5_path), compile=False)
|
| 29 |
+
log('Loaded HDF5 model:', type(m))
|
| 30 |
+
img = Image.open(img_path).convert('RGB').resize((224,224))
|
| 31 |
+
x = np.expand_dims(np.array(img, dtype=np.float32)/255.0, 0)
|
| 32 |
+
log('Running predict on HDF5 model...')
|
| 33 |
+
pred = m.predict(x)
|
| 34 |
+
log('Prediction result (HDF5):', pred.tolist())
|
| 35 |
+
except Exception:
|
| 36 |
+
log('Exception while loading/predicting from HDF5:')
|
| 37 |
+
traceback.print_exc(file=f)
|
| 38 |
+
elif keras_path.exists():
|
| 39 |
+
try:
|
| 40 |
+
log('Attempting to load Keras native file:', str(keras_path))
|
| 41 |
+
m = tf.keras.models.load_model(str(keras_path), compile=False)
|
| 42 |
+
log('Loaded Keras native model:', type(m))
|
| 43 |
+
img = Image.open(img_path).convert('RGB').resize((224,224))
|
| 44 |
+
x = np.expand_dims(np.array(img, dtype=np.float32)/255.0, 0)
|
| 45 |
+
pred = m.predict(x)
|
| 46 |
+
log('Prediction result (KERAS):', pred.tolist())
|
| 47 |
+
except Exception:
|
| 48 |
+
log('Exception while loading/predicting from Keras file:')
|
| 49 |
+
traceback.print_exc(file=f)
|
| 50 |
+
elif saved_model_dir.exists():
|
| 51 |
+
try:
|
| 52 |
+
log('HDF5/.keras not found; attempting to wrap TF SavedModel using TFSMLayer...')
|
| 53 |
+
try:
|
| 54 |
+
from keras.layers import TFSMLayer
|
| 55 |
+
except Exception as e:
|
| 56 |
+
log('TFSMLayer import failed:', e)
|
| 57 |
+
raise
|
| 58 |
+
# Build wrapper model
|
| 59 |
+
inputs = tf.keras.Input(shape=(224,224,3))
|
| 60 |
+
tfsml = TFSMLayer(str(saved_model_dir), call_endpoint='serving_default')
|
| 61 |
+
outputs = tfsml(inputs)
|
| 62 |
+
wrapper = tf.keras.Model(inputs, outputs)
|
| 63 |
+
log('Wrapper model created; running predict...')
|
| 64 |
+
img = Image.open(img_path).convert('RGB').resize((224,224))
|
| 65 |
+
x = np.expand_dims(np.array(img, dtype=np.float32)/255.0, 0)
|
| 66 |
+
pred = wrapper.predict(x)
|
| 67 |
+
# The SavedModel serving signature can return a dict mapping names->arrays
|
| 68 |
+
if isinstance(pred, dict):
|
| 69 |
+
log('Prediction returned a dict with keys:', list(pred.keys()))
|
| 70 |
+
import numpy as _np
|
| 71 |
+
for k, v in pred.items():
|
| 72 |
+
try:
|
| 73 |
+
arr = _np.array(v)
|
| 74 |
+
log(f"Output '{k}': shape={arr.shape} values={arr.flatten()[:10].tolist()}")
|
| 75 |
+
except Exception as _e:
|
| 76 |
+
log(f"Could not convert output '{k}' to numpy array:", _e)
|
| 77 |
+
else:
|
| 78 |
+
try:
|
| 79 |
+
log('Prediction result (wrapped SavedModel):', pred.tolist())
|
| 80 |
+
except Exception:
|
| 81 |
+
log('Prediction result (wrapped SavedModel) type:', type(pred))
|
| 82 |
+
except Exception:
|
| 83 |
+
log('Exception while wrapping/using SavedModel:')
|
| 84 |
+
traceback.print_exc(file=f)
|
| 85 |
+
else:
|
| 86 |
+
log('No model file found: looked for final_model.h5, saved_model_age_regressor.keras, or saved_model_age_regressor/')
|
| 87 |
+
|
| 88 |
+
log('Finished load & predict test')
|
| 89 |
+
except Exception:
|
| 90 |
+
traceback.print_exc(file=f)
|
| 91 |
+
log('Top-level exception')
|
space_requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
huggingface_hub
|
| 3 |
+
tensorflow>=2.10.0
|
| 4 |
+
pillow
|
| 5 |
+
numpy
|
test_predict.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tensorflow as tf
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import numpy as np
|
| 4 |
+
from PIL import Image
|
| 5 |
+
|
| 6 |
+
model_path = 'saved_model_age_regressor'
|
| 7 |
+
img_path = Path('data/UTKFace/53_1_1_20170110122449716.jpg.chip.jpg')
|
| 8 |
+
print('Model path:', model_path, flush=True)
|
| 9 |
+
print('Image path:', img_path, flush=True)
|
| 10 |
+
|
| 11 |
+
m = tf.keras.models.load_model(model_path, compile=False)
|
| 12 |
+
print('Loaded model type:', type(m), flush=True)
|
| 13 |
+
try:
|
| 14 |
+
m.summary()
|
| 15 |
+
except Exception as e:
|
| 16 |
+
print('model.summary failed:', e, flush=True)
|
| 17 |
+
|
| 18 |
+
img = Image.open(img_path).convert('RGB').resize((224,224))
|
| 19 |
+
arr = np.array(img, dtype=np.float32)/255.0
|
| 20 |
+
x = np.expand_dims(arr, 0)
|
| 21 |
+
print('Input shape:', x.shape, flush=True)
|
| 22 |
+
pred = m.predict(x)
|
| 23 |
+
print('Raw prediction output:', pred, 'shape:', getattr(pred, 'shape', None), flush=True)
|
| 24 |
+
try:
|
| 25 |
+
print('Predicted age:', float(pred.flatten()[0]), flush=True)
|
| 26 |
+
except Exception as e:
|
| 27 |
+
print('Error converting prediction to float:', e, flush=True)
|
train.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Train a TensorFlow regression model to predict age from face images (UTKFace dataset).
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
- Put UTKFace images into a folder, e.g. data/UTKFace/
|
| 6 |
+
- python train.py --dataset_dir data/UTKFace --epochs 30 --batch_size 32
|
| 7 |
+
|
| 8 |
+
The script extracts the age from the filename (before the first underscore).
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import argparse
|
| 13 |
+
import random
|
| 14 |
+
import math
|
| 15 |
+
import zipfile
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
from tqdm import tqdm
|
| 20 |
+
import requests
|
| 21 |
+
|
| 22 |
+
import tensorflow as tf
|
| 23 |
+
from tensorflow import keras
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def parse_args():
|
| 27 |
+
parser = argparse.ArgumentParser(description="Train an age regression model on UTKFace images")
|
| 28 |
+
parser.add_argument("--dataset_dir", type=str, default="data/UTKFace", help="Path to folder containing UTKFace images")
|
| 29 |
+
parser.add_argument("--img_size", type=int, default=224, help="Image size (square)")
|
| 30 |
+
parser.add_argument("--batch_size", type=int, default=32)
|
| 31 |
+
parser.add_argument("--epochs", type=int, default=30)
|
| 32 |
+
parser.add_argument("--val_split", type=float, default=0.12, help="Fraction to reserve for validation")
|
| 33 |
+
parser.add_argument("--learning_rate", type=float, default=1e-4)
|
| 34 |
+
parser.add_argument("--auto_download", type=lambda x: (str(x).lower() in ("true", "1", "yes")), default=False,
|
| 35 |
+
help="Whether to attempt to download UTKFace archive automatically if dataset folder is missing")
|
| 36 |
+
parser.add_argument("--fine_tune", type=lambda x: (str(x).lower() in ("true", "1", "yes")), default=False,
|
| 37 |
+
help="Whether to unfreeze part of the backbone for fine-tuning")
|
| 38 |
+
args = parser.parse_args()
|
| 39 |
+
return args
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def attempt_download_utkface(dest_dir: Path):
|
| 43 |
+
"""Attempt to download a ZIP archive of the UTKFace repository and extract it.
|
| 44 |
+
|
| 45 |
+
This may fail if the remote hosting changes. The function attempts a best-effort download
|
| 46 |
+
from the repository URL commonly used to host UTKFace on GitHub.
|
| 47 |
+
"""
|
| 48 |
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
| 49 |
+
github_zip = "https://github.com/susanqq/UTKFace/archive/refs/heads/master.zip"
|
| 50 |
+
tmp_zip = dest_dir / "utkface_master.zip"
|
| 51 |
+
print(f"Attempting to download UTKFace from {github_zip} ...")
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
with requests.get(github_zip, stream=True, timeout=30) as r:
|
| 55 |
+
r.raise_for_status()
|
| 56 |
+
total = int(r.headers.get('content-length', 0))
|
| 57 |
+
with open(tmp_zip, 'wb') as f:
|
| 58 |
+
for chunk in r.iter_content(chunk_size=8192):
|
| 59 |
+
if chunk:
|
| 60 |
+
f.write(chunk)
|
| 61 |
+
|
| 62 |
+
print("Download complete. Extracting archive...")
|
| 63 |
+
with zipfile.ZipFile(tmp_zip, 'r') as z:
|
| 64 |
+
z.extractall(dest_dir)
|
| 65 |
+
|
| 66 |
+
# Move images into dest_dir if they're inside a top-level folder
|
| 67 |
+
extracted_root = None
|
| 68 |
+
for name in os.listdir(dest_dir):
|
| 69 |
+
if name.lower().startswith('utkface') and os.path.isdir(dest_dir / name):
|
| 70 |
+
extracted_root = dest_dir / name
|
| 71 |
+
break
|
| 72 |
+
if extracted_root:
|
| 73 |
+
images = list(extracted_root.rglob('*.jpg')) + list(extracted_root.rglob('*.png'))
|
| 74 |
+
for p in images:
|
| 75 |
+
target = dest_dir / p.name
|
| 76 |
+
try:
|
| 77 |
+
os.replace(p, target)
|
| 78 |
+
except Exception:
|
| 79 |
+
pass
|
| 80 |
+
# clean up
|
| 81 |
+
try:
|
| 82 |
+
os.remove(tmp_zip)
|
| 83 |
+
except Exception:
|
| 84 |
+
pass
|
| 85 |
+
print("UTKFace images should now be in:", dest_dir)
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print("Automatic download failed:", e)
|
| 88 |
+
print("Please download the UTKFace archive manually and place images in the dataset directory.")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def collect_image_paths_and_labels(dataset_dir: Path):
|
| 92 |
+
# UTKFace filenames: <age>_<gender>_<race>_<date&time>.jpg
|
| 93 |
+
img_paths = []
|
| 94 |
+
labels = []
|
| 95 |
+
supported_ext = ('.jpg', '.jpeg', '.png')
|
| 96 |
+
for p in dataset_dir.iterdir():
|
| 97 |
+
if p.is_file() and p.suffix.lower() in supported_ext:
|
| 98 |
+
# parse age
|
| 99 |
+
parts = p.name.split('_')
|
| 100 |
+
try:
|
| 101 |
+
age = int(parts[0])
|
| 102 |
+
except Exception:
|
| 103 |
+
continue
|
| 104 |
+
img_paths.append(str(p))
|
| 105 |
+
labels.append(age)
|
| 106 |
+
return img_paths, labels
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def make_dataset(paths, labels, img_size, batch_size, is_training=True):
|
| 110 |
+
paths = tf.convert_to_tensor(paths)
|
| 111 |
+
labels = tf.convert_to_tensor(labels, dtype=tf.float32)
|
| 112 |
+
|
| 113 |
+
ds = tf.data.Dataset.from_tensor_slices((paths, labels))
|
| 114 |
+
if is_training:
|
| 115 |
+
ds = ds.shuffle(10000, reshuffle_each_iteration=True)
|
| 116 |
+
|
| 117 |
+
def _load_image(path, label):
|
| 118 |
+
img = tf.io.read_file(path)
|
| 119 |
+
img = tf.image.decode_jpeg(img, channels=3)
|
| 120 |
+
img = tf.image.resize(img, [img_size, img_size])
|
| 121 |
+
img = img / 255.0 # normalize to [0,1]
|
| 122 |
+
if is_training:
|
| 123 |
+
img = data_augmentation(img)
|
| 124 |
+
return img, label
|
| 125 |
+
|
| 126 |
+
ds = ds.map(_load_image, num_parallel_calls=tf.data.AUTOTUNE)
|
| 127 |
+
ds = ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)
|
| 128 |
+
return ds
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def data_augmentation(image):
|
| 132 |
+
# Simple augmentation pipeline
|
| 133 |
+
image = tf.image.random_flip_left_right(image)
|
| 134 |
+
image = tf.image.random_brightness(image, max_delta=0.08)
|
| 135 |
+
image = tf.image.random_contrast(image, 0.9, 1.1)
|
| 136 |
+
# random zoom by central crop/resizing
|
| 137 |
+
if tf.random.uniform(()) > 0.6:
|
| 138 |
+
crop_frac = tf.random.uniform((), 0.8, 1.0)
|
| 139 |
+
shape = tf.shape(image)
|
| 140 |
+
crop_h = tf.cast(tf.cast(shape[0], tf.float32) * crop_frac, tf.int32)
|
| 141 |
+
crop_w = tf.cast(tf.cast(shape[1], tf.float32) * crop_frac, tf.int32)
|
| 142 |
+
image = tf.image.random_crop(image, size=[crop_h, crop_w, 3])
|
| 143 |
+
image = tf.image.resize(image, [shape[0], shape[1]])
|
| 144 |
+
return image
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def build_model(img_size, fine_tune=False):
|
| 148 |
+
inputs = keras.Input(shape=(img_size, img_size, 3))
|
| 149 |
+
base = keras.applications.MobileNetV2(include_top=False, input_tensor=inputs, weights='imagenet')
|
| 150 |
+
base.trainable = False
|
| 151 |
+
|
| 152 |
+
x = base.output
|
| 153 |
+
x = keras.layers.GlobalAveragePooling2D()(x)
|
| 154 |
+
x = keras.layers.Dropout(0.2)(x)
|
| 155 |
+
x = keras.layers.Dense(128, activation='relu')(x)
|
| 156 |
+
x = keras.layers.Dense(64, activation='relu')(x)
|
| 157 |
+
outputs = keras.layers.Dense(1, name='age')(x) # regression output
|
| 158 |
+
|
| 159 |
+
model = keras.Model(inputs=inputs, outputs=outputs)
|
| 160 |
+
|
| 161 |
+
if fine_tune:
|
| 162 |
+
# Unfreeze last blocks for fine-tuning
|
| 163 |
+
base.trainable = True
|
| 164 |
+
# Freeze earlier layers
|
| 165 |
+
for layer in base.layers[:-30]:
|
| 166 |
+
layer.trainable = False
|
| 167 |
+
|
| 168 |
+
return model
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def main():
|
| 172 |
+
args = parse_args()
|
| 173 |
+
dataset_dir = Path(args.dataset_dir)
|
| 174 |
+
|
| 175 |
+
if (not dataset_dir.exists() or not any(dataset_dir.iterdir())) and args.auto_download:
|
| 176 |
+
attempt_download_utkface(dataset_dir)
|
| 177 |
+
|
| 178 |
+
if not dataset_dir.exists() or not any(dataset_dir.iterdir()):
|
| 179 |
+
raise RuntimeError(f"No images found in {dataset_dir}. Place UTKFace images there or use --auto_download True to attempt download.")
|
| 180 |
+
|
| 181 |
+
paths, labels = collect_image_paths_and_labels(dataset_dir)
|
| 182 |
+
if len(paths) == 0:
|
| 183 |
+
raise RuntimeError("No valid UTKFace images found in dataset directory. Ensure the files follow the naming convention '<age>_...'.")
|
| 184 |
+
|
| 185 |
+
# Convert to numpy lists
|
| 186 |
+
paths = np.array(paths)
|
| 187 |
+
labels = np.array(labels, dtype=np.float32)
|
| 188 |
+
|
| 189 |
+
# Shuffle and split
|
| 190 |
+
indices = np.arange(len(paths))
|
| 191 |
+
np.random.shuffle(indices)
|
| 192 |
+
paths = paths[indices]
|
| 193 |
+
labels = labels[indices]
|
| 194 |
+
|
| 195 |
+
n_val = max(1, int(len(paths) * args.val_split))
|
| 196 |
+
val_paths = paths[:n_val].tolist()
|
| 197 |
+
val_labels = labels[:n_val].tolist()
|
| 198 |
+
train_paths = paths[n_val:].tolist()
|
| 199 |
+
train_labels = labels[n_val:].tolist()
|
| 200 |
+
|
| 201 |
+
print(f"Found {len(train_paths)} training images and {len(val_paths)} validation images.")
|
| 202 |
+
|
| 203 |
+
train_ds = make_dataset(train_paths, train_labels, args.img_size, args.batch_size, is_training=True)
|
| 204 |
+
val_ds = make_dataset(val_paths, val_labels, args.img_size, args.batch_size, is_training=False)
|
| 205 |
+
|
| 206 |
+
model = build_model(args.img_size, fine_tune=args.fine_tune)
|
| 207 |
+
model.compile(optimizer=keras.optimizers.Adam(learning_rate=args.learning_rate),
|
| 208 |
+
loss='mse',
|
| 209 |
+
metrics=[keras.metrics.MeanAbsoluteError(name='mae')])
|
| 210 |
+
|
| 211 |
+
model.summary()
|
| 212 |
+
|
| 213 |
+
callbacks = [
|
| 214 |
+
keras.callbacks.ModelCheckpoint('best_model.h5', save_best_only=True, monitor='val_loss'),
|
| 215 |
+
keras.callbacks.EarlyStopping(monitor='val_loss', patience=8, restore_best_weights=True),
|
| 216 |
+
keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=4, min_lr=1e-7)
|
| 217 |
+
]
|
| 218 |
+
|
| 219 |
+
history = model.fit(train_ds, validation_data=val_ds, epochs=args.epochs, callbacks=callbacks)
|
| 220 |
+
|
| 221 |
+
# Evaluate
|
| 222 |
+
print("Evaluating on validation set:")
|
| 223 |
+
eval_res = model.evaluate(val_ds)
|
| 224 |
+
print(dict(zip(model.metrics_names, eval_res)))
|
| 225 |
+
|
| 226 |
+
# Save in both SavedModel (preferred) and Keras formats for compatibility
|
| 227 |
+
try:
|
| 228 |
+
# Preferred: export to SavedModel directory for TFServing/TFLite
|
| 229 |
+
model.export('saved_model_age_regressor')
|
| 230 |
+
print('Exported SavedModel to ./saved_model_age_regressor')
|
| 231 |
+
except Exception as e:
|
| 232 |
+
print('SavedModel export failed:', e)
|
| 233 |
+
# Fallback: save as Keras native single-file (.keras)
|
| 234 |
+
try:
|
| 235 |
+
model.save('saved_model_age_regressor.keras')
|
| 236 |
+
print('Saved Keras model to ./saved_model_age_regressor.keras')
|
| 237 |
+
except Exception as e2:
|
| 238 |
+
print('Keras native save failed:', e2)
|
| 239 |
+
# Also save an HDF5 copy for backward compatibility with tools that require .h5
|
| 240 |
+
try:
|
| 241 |
+
model.save('final_model.h5')
|
| 242 |
+
print('Saved HDF5 model to ./final_model.h5')
|
| 243 |
+
except Exception as e3:
|
| 244 |
+
print('HDF5 save failed:', e3)
|
| 245 |
+
|
| 246 |
+
# Show a few sample predictions
|
| 247 |
+
sample_paths = val_paths[:12]
|
| 248 |
+
sample_labels = val_labels[:12]
|
| 249 |
+
|
| 250 |
+
sample_ds = make_dataset(sample_paths, sample_labels, args.img_size, batch_size=12, is_training=False)
|
| 251 |
+
imgs, labs = next(iter(sample_ds))
|
| 252 |
+
preds = model.predict(imgs).flatten()
|
| 253 |
+
|
| 254 |
+
try:
|
| 255 |
+
import matplotlib.pyplot as plt
|
| 256 |
+
n = len(preds)
|
| 257 |
+
cols = 4
|
| 258 |
+
rows = math.ceil(n / cols)
|
| 259 |
+
plt.figure(figsize=(cols * 3, rows * 3))
|
| 260 |
+
for i in range(n):
|
| 261 |
+
ax = plt.subplot(rows, cols, i + 1)
|
| 262 |
+
img = imgs[i].numpy()
|
| 263 |
+
plt.imshow(img)
|
| 264 |
+
plt.axis('off')
|
| 265 |
+
plt.title(f"True: {int(labs[i])}\nPred: {preds[i]:.1f}")
|
| 266 |
+
plt.tight_layout()
|
| 267 |
+
plt.show()
|
| 268 |
+
except Exception:
|
| 269 |
+
print("Matplotlib not available or running headless; skipping sample visualization.")
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
if __name__ == '__main__':
|
| 273 |
+
main()
|