Fix calorie<->mass / protein<->carb label swap: retrain on corrected labels; honest metrics (calorie MAE 86.2 kcal); remove invalid comparison charts
Browse files- README.md +46 -76
- calibration.json +3 -3
- caloriek.pt +2 -2
- comparison.png +0 -3
- config.json +9 -2
- loss_curve.png +0 -3
README.md
CHANGED
|
@@ -24,7 +24,7 @@ model-index:
|
|
| 24 |
type: Nutrition5k
|
| 25 |
metrics:
|
| 26 |
- type: mae
|
| 27 |
-
value:
|
| 28 |
name: Calorie MAE (kcal)
|
| 29 |
---
|
| 30 |
|
|
@@ -34,79 +34,52 @@ A single RGB food photo in, five numbers out: **calories (kcal), mass (g), prote
|
|
| 34 |
CalorieK is a SigLIP2-base vision tower with a small regression head, fine-tuned end-to-end and evaluated
|
| 35 |
under a strict, leakage-audited Nutrition5k protocol.
|
| 36 |
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
## Headline result — locked Nutrition5k test set (608 dishes, official test split, never seen)
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
| 44 |
-
|
|
| 45 |
-
|
|
| 46 |
-
|
|
| 47 |
-
|
|
| 48 |
-
|
|
| 49 |
-
|
|
|
|
|
| 50 |
|
| 51 |
Every test image is a deterministic central frame from the official Nutrition5k camera-A side-angle
|
| 52 |
video of an official _test-split_ dish — selected by dish ID, never by image quality. Evaluation is
|
| 53 |
dish-level (`physical_meal_id`).
|
| 54 |
|
| 55 |
-
### Macronutrient accuracy (same 608 dishes)
|
| 56 |
-
|
| 57 |
-
| Task | MAE |
|
| 58 |
-
| --- | --- | --- | --- |
|
| 59 |
-
| Mass | **90.4 g** | 34.5 g | −47.3 g |
|
| 60 |
-
| Protein | **7.8 g** | 3.9 g | −1.9 g |
|
| 61 |
-
| Carbohydrate | **5.8 g** | 2.8 g | −1.2 g |
|
| 62 |
-
| Fat | **7.2 g** | 2.2 g | −3.1 g |
|
| 63 |
-
|
| 64 |
-
## Comparison with open VLMs (paired 304-dish subset, seed 17)
|
| 65 |
-
|
| 66 |
-
Each model answered the same calorie question on the same dishes
|
| 67 |
-
(prompt: _"Estimate the total calories in this food image. Return exactly one integer number in kcal…"_).
|
| 68 |
-
|
| 69 |
-
| Model | n | MAE (kcal) | Median AE | Within ±100 kcal | Bias |
|
| 70 |
-
| --- | --- | --- | --- | --- | --- |
|
| 71 |
-
| **CalorieK (ours)** | 304 | **62.7** | **24.0** | **94.1%** | −11.0 |
|
| 72 |
-
| Qwen3.5-2B | 304 | 116.4 | 65.0 | 69.4% | −48.1 |
|
| 73 |
-
| Qwen3.5-9B (Q8_0, thinking off) ‡ | 304 | 117.2 | 64.5 | 65.8% | +6.1 |
|
| 74 |
-
| MiniCPM-V 4.6 (1.3B) | 304 | 163.3 | 97.0 | 52.6% | +75.5 |
|
| 75 |
-
| Gemma-4-12B † | 100 | 176.8 | 90.5 | 58.0% | −43.3 |
|
| 76 |
-
| MiniMax M3 (hosted, thinking) | 304 | 193.5 | 120.5 | 44.7% | +103.7 |
|
| 77 |
-
| CalorieCLIP | 304 | 196.2 | 133.2 | 42.1% | +121.1 |
|
| 78 |
-
|
| 79 |
-
† Gemma-4-12B was scored on a seeded 100-dish paired subset of the same 304 dishes (compute budget);
|
| 80 |
-
all parse rates were 100%.
|
| 81 |
-
‡ Qwen3.5-9B ran as an 8-bit GGUF (Q8_0) via llama.cpp with 4k context and thinking disabled.
|
| 82 |
-
MiniMax M3 (a hosted reasoning model, chain-of-thought enabled) reinforces the point from the other
|
| 83 |
-
direction: extensive per-dish reasoning did not help — it lands second-to-last with a strong +104 kcal
|
| 84 |
-
systematic overestimate. Reasoning does not fix calibration.
|
| 85 |
-
|
| 86 |
-
## Out-of-distribution evaluation (MM-Food-100K, 500 wild food photos)
|
| 87 |
-
|
| 88 |
-
To measure generalization beyond Nutrition5k plating, both regression models were scored on a seeded
|
| 89 |
-
500-image sample of [MM-Food-100K](https://huggingface.co/datasets/Codatta/MM-Food-100K)
|
| 90 |
-
(restaurant/home photos; `food_prob ≥ 0.9`). **Caveat: these labels are crowd/AI estimates, not weighed**
|
| 91 |
-
**measurements** — absolute errors include label noise no model can beat, so the relative comparison is
|
| 92 |
-
the meaningful signal.
|
| 93 |
-
|
| 94 |
-
| | CalorieK | CalorieCLIP |
|
| 95 |
| --- | --- | --- |
|
| 96 |
-
|
|
| 97 |
-
|
|
| 98 |
-
|
|
| 99 |
-
|
|
| 100 |
-
| R² | **+0.24** | −0.08 |
|
| 101 |
|
| 102 |
-
|
|
|
|
|
|
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
|
| 111 |
## Training method (improved)
|
| 112 |
|
|
@@ -117,20 +90,17 @@ RandomResizedCrop, HorizontalFlip, Rotation, ColorJitter.
|
|
| 117 |
- **Schedule**: 20 epochs × 20,000 samples, effective batch 256 (bf16), head LR 1e-4 / trunk LR 2e-5,
|
| 118 |
cosine schedule with restarts, 5% warmup, first 5% head-only, last 4 vision blocks unfrozen,
|
| 119 |
weight decay 0.05, seed 17.
|
| 120 |
-
- **Checkpoint selection**: best validation MAE on 80 held-out camera-A dishes (validation MAE
|
| 121 |
-
Training ran ~
|
| 122 |
- **Calibration**: a conformal radius fitted on 86 held-out calibration dishes gives 90% prediction
|
| 123 |
-
intervals of ±
|
| 124 |
-
|
| 125 |
-
[](https://huggingface.co/Dralkh/CalorieK/blob/main/loss_curve.png)
|
| 126 |
|
| 127 |
## Training data (20,000 samples, leakage-audited)
|
| 128 |
|
| 129 |
| Source | Samples | Notes |
|
| 130 |
| --- | --- | --- |
|
| 131 |
-
| [`
|
| 132 |
-
| [`
|
| 133 |
-
| [`aryachakraborty/Food_Calorie_Dataset`](https://huggingface.co/datasets/aryachakraborty/Food_Calorie_Dataset) | 266 | Human-estimated labels, down-weighted (0.4) |
|
| 134 |
|
| 135 |
Leakage controls, all enforced programmatically before training:
|
| 136 |
|
|
@@ -170,14 +140,14 @@ with torch.inference_mode():
|
|
| 170 |
out = vision(pixel_values=pixels)
|
| 171 |
feats = out.pooler_output if out.pooler_output is not None else out.last_hidden_state[:, 0]
|
| 172 |
kcal, mass_g, protein_g, carb_g, fat_g = head(feats.float())[0].tolist()
|
| 173 |
-
print(f"{kcal:.0f} kcal ±
|
| 174 |
```
|
| 175 |
|
| 176 |
## Limitations
|
| 177 |
|
| 178 |
- **Domain**: trained and evaluated on Nutrition5k-style plated single dishes photographed from the side;
|
| 179 |
accuracy on multi-plate scenes, packaged food, or drinks is untested.
|
| 180 |
-
- **Heavy tail**: RMSE (
|
| 181 |
Use the calibrated interval, not the point estimate, for anything that matters.
|
| 182 |
- **Frames, not meals**: training frames come from videos of ~1,250 distinct dishes; visual diversity is
|
| 183 |
narrower than the sample count suggests.
|
|
@@ -186,10 +156,10 @@ narrower than the sample count suggests.
|
|
| 186 |
## Citation
|
| 187 |
|
| 188 |
```bibtex
|
| 189 |
-
@misc{
|
| 190 |
title={CalorieK: Accurate Food Calorie Estimation from a Single RGB Image},
|
| 191 |
-
author={
|
| 192 |
-
year={
|
| 193 |
url={https://huggingface.co/Dralkh/CalorieK}
|
| 194 |
}
|
| 195 |
```
|
|
|
|
| 24 |
type: Nutrition5k
|
| 25 |
metrics:
|
| 26 |
- type: mae
|
| 27 |
+
value: 86.2
|
| 28 |
name: Calorie MAE (kcal)
|
| 29 |
---
|
| 30 |
|
|
|
|
| 34 |
CalorieK is a SigLIP2-base vision tower with a small regression head, fine-tuned end-to-end and evaluated
|
| 35 |
under a strict, leakage-audited Nutrition5k protocol.
|
| 36 |
|
| 37 |
+
> **⚠️ Correction (2026-07-13).** Earlier versions of this card reported a
|
| 38 |
+
> "49.8 kcal calorie MAE." A label-mapping bug (`n5k_registry.py` parsed the
|
| 39 |
+
> Nutrition5k CSV columns in the wrong order) had swapped **calories↔mass** and
|
| 40 |
+
> **protein↔carbohydrate** throughout training and evaluation — so the published
|
| 41 |
+
> "calorie" figure was really a **mass** error in grams. The bug is fixed, the
|
| 42 |
+
> labels canonicalized against the official CSV, and a model retrained on the
|
| 43 |
+
> corrected labels. Honest numbers are below. The VLM/CalorieCLIP/OOD comparison
|
| 44 |
+
> tables further down were scored against the swapped labels and are being
|
| 45 |
+
> re-evaluated — treat them as invalid until updated.
|
| 46 |
|
| 47 |
## Headline result — locked Nutrition5k test set (608 dishes, official test split, never seen)
|
| 48 |
|
| 49 |
+
Corrected model (`siglip2-base-patch16-224`, retrained on canonicalized labels), dish-level (`physical_meal_id`):
|
| 50 |
+
|
| 51 |
+
| Metric | CalorieK (corrected) |
|
| 52 |
+
| --- | --- |
|
| 53 |
+
| Calorie MAE | **86.2 kcal** (95% CI 53.5–132.3) |
|
| 54 |
+
| Median absolute error | 30.1 kcal |
|
| 55 |
+
| Within ±50 kcal | 64.8% |
|
| 56 |
+
| Within ±100 kcal | 84.0% |
|
| 57 |
+
| Bias | −49.5 kcal (systematic under-prediction) |
|
| 58 |
+
| Invalid predictions | 0 / 608 |
|
| 59 |
|
| 60 |
Every test image is a deterministic central frame from the official Nutrition5k camera-A side-angle
|
| 61 |
video of an official _test-split_ dish — selected by dish ID, never by image quality. Evaluation is
|
| 62 |
dish-level (`physical_meal_id`).
|
| 63 |
|
| 64 |
+
### Macronutrient accuracy (same 608 dishes, corrected labels)
|
| 65 |
+
|
| 66 |
+
| Task | MAE | PMAE |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
| --- | --- | --- |
|
| 68 |
+
| Mass | **68.2 g** | 34.7% |
|
| 69 |
+
| Protein | **6.4 g** | 38.1% |
|
| 70 |
+
| Carbohydrate | **7.9 g** | 40.6% |
|
| 71 |
+
| Fat | **7.7 g** | 54.5% |
|
|
|
|
| 72 |
|
| 73 |
+
These are the true, correctly-labeled numbers. Calories (86 kcal, 33% PMAE) and
|
| 74 |
+
fat (54% PMAE) are the weakest and are the focus of the ongoing architecture work
|
| 75 |
+
(portion decomposition, ingredient-semantic fusion, depth fusion).
|
| 76 |
|
| 77 |
+
## Comparisons (under re-evaluation)
|
| 78 |
+
|
| 79 |
+
The previous open-VLM, CalorieCLIP, and out-of-distribution (MM-Food-100K)
|
| 80 |
+
comparison tables were computed against the swapped labels and are therefore
|
| 81 |
+
invalid. They have been removed and will be republished once re-run against the
|
| 82 |
+
corrected labels and the improved model.
|
| 83 |
|
| 84 |
## Training method (improved)
|
| 85 |
|
|
|
|
| 90 |
- **Schedule**: 20 epochs × 20,000 samples, effective batch 256 (bf16), head LR 1e-4 / trunk LR 2e-5,
|
| 91 |
cosine schedule with restarts, 5% warmup, first 5% head-only, last 4 vision blocks unfrozen,
|
| 92 |
weight decay 0.05, seed 17.
|
| 93 |
+
- **Checkpoint selection**: best validation calorie MAE on 80 held-out camera-A dishes (validation MAE 57.3 kcal).
|
| 94 |
+
Training ran ~13 minutes on one RTX 4080 (16 GB).
|
| 95 |
- **Calibration**: a conformal radius fitted on 86 held-out calibration dishes gives 90% prediction
|
| 96 |
+
intervals of ±119 kcal, with 87.3% actual coverage on the locked test set (`calibration.json`).
|
|
|
|
|
|
|
| 97 |
|
| 98 |
## Training data (20,000 samples, leakage-audited)
|
| 99 |
|
| 100 |
| Source | Samples | Notes |
|
| 101 |
| --- | --- | --- |
|
| 102 |
+
| [`pinkieseb/nutrition_dataset`](https://huggingface.co/datasets/pinkieseb/nutrition_dataset) (Nutrition5k video frames) | 19,734 | only dishes provably in the **official Nutrition5k train split** (matched by exact nutrition signature), ≤16 frames per dish |
|
| 103 |
+
| [`aryachakraborty/Food_Calorie_Dataset`](https://huggingface.co/datasets/aryachakraborty/Food_Calorie_Dataset) | 266 | human-estimated labels, down-weighted (0.4) |
|
|
|
|
| 104 |
|
| 105 |
Leakage controls, all enforced programmatically before training:
|
| 106 |
|
|
|
|
| 140 |
out = vision(pixel_values=pixels)
|
| 141 |
feats = out.pooler_output if out.pooler_output is not None else out.last_hidden_state[:, 0]
|
| 142 |
kcal, mass_g, protein_g, carb_g, fat_g = head(feats.float())[0].tolist()
|
| 143 |
+
print(f"{kcal:.0f} kcal ± 119 (90% interval), {mass_g:.0f} g, P{protein_g:.0f}/C{carb_g:.0f}/F{fat_g:.0f}")
|
| 144 |
```
|
| 145 |
|
| 146 |
## Limitations
|
| 147 |
|
| 148 |
- **Domain**: trained and evaluated on Nutrition5k-style plated single dishes photographed from the side;
|
| 149 |
accuracy on multi-plate scenes, packaged food, or drinks is untested.
|
| 150 |
+
- **Heavy tail**: RMSE (533) is far above MAE (86) — very high-calorie dishes are occasionally missed badly.
|
| 151 |
Use the calibrated interval, not the point estimate, for anything that matters.
|
| 152 |
- **Frames, not meals**: training frames come from videos of ~1,250 distinct dishes; visual diversity is
|
| 153 |
narrower than the sample count suggests.
|
|
|
|
| 156 |
## Citation
|
| 157 |
|
| 158 |
```bibtex
|
| 159 |
+
@misc{caloriek2025,
|
| 160 |
title={CalorieK: Accurate Food Calorie Estimation from a Single RGB Image},
|
| 161 |
+
author={Dralkh},
|
| 162 |
+
year={2025},
|
| 163 |
url={https://huggingface.co/Dralkh/CalorieK}
|
| 164 |
}
|
| 165 |
```
|
calibration.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"coverage": 0.9,
|
| 3 |
-
"radius_kcal":
|
| 4 |
-
"manifest_sha256": "
|
| 5 |
-
"checkpoint": "outputs/
|
| 6 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"coverage": 0.9,
|
| 3 |
+
"radius_kcal": 118.95077547851565,
|
| 4 |
+
"manifest_sha256": "1678ff2a4c82da6bb6e9acd8842a9b20c09624d11d692f8db10e5fa67423207a",
|
| 5 |
+
"checkpoint": "outputs/siglip2_corrected/checkpoint_best.pt"
|
| 6 |
}
|
caloriek.pt
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0d5a1f26f5b1a27d1914a18f28f5218ff6df23ee05b0932212bc0c1dd61b053a
|
| 3 |
+
size 374774573
|
comparison.png
DELETED
Git LFS Details
|
config.json
CHANGED
|
@@ -4,8 +4,15 @@
|
|
| 4 |
"base_model": "google/siglip2-base-patch16-224",
|
| 5 |
"image_size": 224,
|
| 6 |
"hidden_size": 768,
|
| 7 |
-
"tasks": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"checkpoint_file": "caloriek.pt",
|
| 9 |
"calibration_file": "calibration.json",
|
| 10 |
-
"interval_90_radius_kcal":
|
|
|
|
| 11 |
}
|
|
|
|
| 4 |
"base_model": "google/siglip2-base-patch16-224",
|
| 5 |
"image_size": 224,
|
| 6 |
"hidden_size": 768,
|
| 7 |
+
"tasks": [
|
| 8 |
+
"calories_kcal",
|
| 9 |
+
"mass_g",
|
| 10 |
+
"protein_g",
|
| 11 |
+
"carbohydrate_g",
|
| 12 |
+
"fat_g"
|
| 13 |
+
],
|
| 14 |
"checkpoint_file": "caloriek.pt",
|
| 15 |
"calibration_file": "calibration.json",
|
| 16 |
+
"interval_90_radius_kcal": 118.95,
|
| 17 |
+
"note": "labels canonicalized from official Nutrition5k CSV 2026-07-13; retrained (see README correction)"
|
| 18 |
}
|
loss_curve.png
DELETED
Git LFS Details
|