Sharris commited on
Commit
de3c81a
·
verified ·
1 Parent(s): 33a6507

Upload folder using huggingface_hub

Browse files
.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
- license: mit
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()