Label-distribution learning for estimating the aggregate human rating of facial attractiveness from photographs.
The model predicts, for an aligned face crop, the distribution of scores that a defined population of human raters would assign. The point estimate is the expectation of that distribution; the spread is reported as first-class output. The target is a statistical aggregate of subjective preferences from a specific rater population — not an objective property of a person. See ETHICS.md for scope and prohibited uses.
The released checkpoint is trained on SCUT-FBP5500 (1–5 scale, 60 raters/image, 5500 images). The architecture and pipeline support any 1–N_BINS rating scale; N_BINS is set in src/fbp/__init__.py.
- Distributional regression: a backbone (ViT-B/16 or ResNet-50) with an
N_BINS-bin distribution head (5 for the released SCUT-FBP5500 checkpoint), trained with KL divergence against the empirical rating histogram plus a Huber penalty on the expectation. Strictly dominates scalar regression andN_BINS-class classification on this task (preserves ordinality, models rater disagreement, yields calibrated uncertainty). - Rating normalization: per-rater z-scoring, trimmed aggregation, percentile-anchored rescaling to [1,
N_BINS]. - Fairness audit: every evaluation reports metrics per demographic subgroup and per photo condition, with hard gates on inter-group MAE gaps.
- Adversarial debiasing (optional): gradient-reversal probe that penalizes demographic information in the penultimate features.
- Interpretability: Grad-CAM (CNN and ViT via token reshaping) and model-agnostic occlusion sensitivity.
pip install -e ".[dev]"Face detection/alignment for raw photos additionally requires pip install -e ".[align]".
Two input CSVs, produced however you collect data:
ratings.csv — one row per individual rating:
| column | type | |
|---|---|---|
image_id |
str | |
rater_id |
str | each rater needs ≥ 50 ratings for z-scoring |
score |
int | 1–N_BINS (1–5 for SCUT-FBP5500) |
images.csv — one row per image:
| column | type | |
|---|---|---|
image_id |
str | |
image |
str | path relative to the image root, aligned 256×256 crop |
subject_id |
str | used for subject-disjoint splits |
| subgroup columns | str | e.g. gender, age_bracket, ethnicity, photo_source |
Build the training manifest (normalized means, rating histograms, subject-disjoint splits):
python scripts/prepare_data.py --ratings ratings.csv --images images.csv --out data/manifestThe released checkpoint is trained on SCUT-FBP5500
(Liang et al., 2018, arXiv:1801.06345), which ships its own 60-rater-per-image distributions
and an official 3300/2200 train/test split. scripts/prepare_scut_fbp5500.py converts the
dataset's All_Ratings.xlsx and split files directly into manifests (no prepare_data.py
step needed, since the rater-level data is already in long form).
Download SCUT-FBP5500_v2.1.zip from the official release
and extract it under data/raw/SCUT-FBP5500/ (gitignored), then symlink the image folder to
image_root and build the manifests:
unzip SCUT-FBP5500_v2.1.zip -d data/raw/SCUT-FBP5500
ln -s raw/SCUT-FBP5500/SCUT-FBP5500_v2/Images data/images
python scripts/prepare_scut_fbp5500.py \
--ratings-xlsx "data/raw/SCUT-FBP5500/SCUT-FBP5500_v2/All_Ratings.xlsx" \
--split-dir "data/raw/SCUT-FBP5500/SCUT-FBP5500_v2/train_test_files/split_of_60%training and 40%testing" \
--out data/manifestpython scripts/train.py --config configs/scut_fbp5500.yamlconfigs/vit_b16.yaml and configs/resnet50.yaml are reference configs for the
ViT-B/16 and ResNet-50 backbones described in docs/DESIGN.md; they
expect manifests built with prepare_data.py.
Checkpoints, config snapshot, and per-epoch metrics (global and per subgroup) are written to the run directory.
python scripts/evaluate.py --checkpoint runs/scut_fbp5500/best.pt --manifest data/manifest/test.csv \
--image-root data/imagesReports MAE, RMSE, Pearson r, Spearman ρ, R², uncertainty calibration, and the subgroup table. A run fails the audit if any subgroup MAE gap exceeds the configured threshold.
python scripts/predict.py --checkpoint runs/scut_fbp5500/best.pt --image face.jpgOutput is {score, uncertainty}. Uncertainty is the predicted rater disagreement (std of the predicted distribution); a score without its uncertainty is not a valid output of this system.
web/ is a static page that runs the model client-side with ONNX Runtime Web — images never leave the browser. Enable Pages (Settings → Pages → GitHub Actions); .github/workflows/pages.yml deploys web/ on push.
python scripts/export_onnx.py --checkpoint runs/scut_fbp5500/best.pt --out web/model.onnxWithout --checkpoint the export uses untrained weights and the page labels the scores as noise. The committed web/model.onnx is already exported from runs/scut_fbp5500/best.pt (trained: true in web/model_meta.json; metrics in docs/MODEL_CARD.md). Local preview: python -m http.server -d web.
src/fbp/
data/ dataset, transforms, rating normalization, alignment
models/ backbone factory, distribution head
engine/ trainer, evaluation loop
interpret/ grad-cam, occlusion sensitivity
losses.py KL + Huber distributional loss, gradient-reversal probe
metrics.py regression metrics, uncertainty calibration
fairness.py subgroup report and gap gates
scripts/ prepare_data, prepare_scut_fbp5500, train, evaluate, predict, export_onnx
configs/ YAML experiment configs
web/ static demo page (ONNX Runtime Web, deployed to GitHub Pages)
docs/DESIGN.md full methodology: dataset design, annotation protocol,
architecture comparison, bias analysis, validation
docs/MODEL_CARD.md model card for the released checkpoint (data, metrics, fairness audit)
pytestThe prediction ceiling is the reliability of the human consensus itself (split-half, Spearman-Brown corrected). Cross-cultural transfer degrades correlations by 0.2–0.3. Roughly 30–50% of rating variance is intra-identity — the model scores a photograph, not a person. Full analysis in docs/DESIGN.md.
MIT for the code. Use of any trained model is additionally constrained by ETHICS.md.