Dralkh commited on
Commit
fab9fa8
·
verified ·
1 Parent(s): 4d181b5

Fix calorie<->mass / protein<->carb label swap: retrain on corrected labels; honest metrics (calorie MAE 86.2 kcal); remove invalid comparison charts

Browse files
Files changed (6) hide show
  1. README.md +46 -76
  2. calibration.json +3 -3
  3. caloriek.pt +2 -2
  4. comparison.png +0 -3
  5. config.json +9 -2
  6. loss_curve.png +0 -3
README.md CHANGED
@@ -24,7 +24,7 @@ model-index:
24
  type: Nutrition5k
25
  metrics:
26
  - type: mae
27
- value: 49.8
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
- [![Comparison on the locked Nutrition5k test set](https://huggingface.co/Dralkh/CalorieK/resolve/main/comparison.png)](https://huggingface.co/Dralkh/CalorieK/blob/main/comparison.png)
 
 
 
 
 
 
 
 
38
 
39
  ## Headline result — locked Nutrition5k test set (608 dishes, official test split, never seen)
40
 
41
- | Metric | CalorieK | CalorieCLIP (same 608 dishes) |
42
- | --- | --- | --- |
43
- | Calorie MAE | **49.8 kcal** (95% CI 34.4–78.0) | 181.2 kcal |
44
- | Median absolute error | **24.5 kcal** | 137.0 kcal |
45
- | Within ±50 kcal | **78.5%** | 21.4% |
46
- | Within ±100 kcal | **94.2%** | 40.6% |
47
- | Bias | **−6.6 kcal** | +134.8 kcal |
48
- | Calibrated 90% interval | **±105.9 kcal** (actual coverage 93.6%) | — |
49
- | Invalid predictions | 0 / 608 | 0 / 608 |
 
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 | Median AE | Bias |
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
- | MAE | **162.5 kcal** | 206.4 kcal |
97
- | Median error | **135.0 kcal** | 179.9 kcal |
98
- | Within ±100 kcal | **38.8%** | 25.6% |
99
- | Bias | **−8.1 kcal** | +98.3 kcal |
100
- | R² | **+0.24** | −0.08 |
101
 
102
- Findings, stated honestly:
 
 
103
 
104
- - CalorieK beats CalorieCLIP out-of-distribution on every metric, keeps **near-zero bias** on food it
105
- has never seen, and retains real signal (R² +0.24) where CalorieCLIP is worse than predicting the
106
- mean (R² −0.08).
107
- - But CalorieK degrades ~3× versus in-distribution (49.8 162.5 kcal MAE): it is substantially a
108
- specialist in Nutrition5k-style plated dishes. For wild-photo deployment, mixing diverse weakly
109
- labeled data into training is the indicated next step.
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 39.3 kcal).
121
- Training ran ~12 minutes on one RTX 4080 (16 GB).
122
  - **Calibration**: a conformal radius fitted on 86 held-out calibration dishes gives 90% prediction
123
- intervals of ±105.9 kcal, with 93.6% actual coverage on the locked test set (`calibration.json`).
124
-
125
- [![Training loss](https://huggingface.co/Dralkh/CalorieK/resolve/main/loss_curve.png)](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
- | [`mmathys/food-nutrients`](https://huggingface.co/datasets/mmathys/food-nutrients) (Nutrition5k overhead images) | 9,798 | Weighed plate totals |
132
- | [`pinkieseb/nutrition_dataset`](https://huggingface.co/datasets/pinkieseb/nutrition_dataset) (Nutrition5k video frames) | 9,936 | Weighed plate totals, ≤16 frames per dish |
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 ± 106 (90% interval), {mass_g:.0f} g, P{protein_g:.0f}/C{carb_g:.0f}/F{fat_g:.0f}")
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 (326) is far above MAE (50) — very high-calorie dishes are occasionally missed badly.
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{alkharji2026caloriek,
190
  title={CalorieK: Accurate Food Calorie Estimation from a Single RGB Image},
191
- author={Alkharji, Faisal},
192
- year={2026},
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": 105.9188232421875,
4
- "manifest_sha256": "6002cd3b3d33917f888573a49bee41fc23b26055a7c626408d6d71641e2b09d0",
5
- "checkpoint": "outputs/siglip2_b0/checkpoint_best.pt"
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:17ac63ec7034919aae7436ecf43ea08809a4c7b2fd59abaed44c40a0bd0ac344
3
- size 607962335
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0d5a1f26f5b1a27d1914a18f28f5218ff6df23ee05b0932212bc0c1dd61b053a
3
+ size 374774573
comparison.png DELETED

Git LFS Details

  • SHA256: a85e4401f3e763e7ec37df64c915b7913e055c07f9fc2d3a0ed47060a24a4405
  • Pointer size: 131 Bytes
  • Size of remote file: 123 kB
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": ["calories_kcal", "mass_g", "protein_g", "carbohydrate_g", "fat_g"],
 
 
 
 
 
 
8
  "checkpoint_file": "caloriek.pt",
9
  "calibration_file": "calibration.json",
10
- "interval_90_radius_kcal": 105.92
 
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

  • SHA256: 5aee4af983014bb2e734c341bf112762daab7ce877158eaf572b6b099f665c70
  • Pointer size: 131 Bytes
  • Size of remote file: 159 kB