Dralkh commited on
Commit
dcb17ba
Β·
verified Β·
1 Parent(s): 09d962f

Add Comparison with open-source calorie VLMs section

Browse files
Files changed (1) hide show
  1. README.md +134 -79
README.md CHANGED
@@ -1,41 +1,18 @@
1
  ---
2
- language: en
3
- license: apache-2.0
4
  tags:
5
- - nutrition
6
- - calorie-estimation
7
  - food
8
- - computer-vision
9
- - regression
10
- - siglip
 
 
 
11
  datasets:
12
  - pinkieseb/nutrition_dataset
13
  - aryachakraborty/Food_Calorie_Dataset
14
- - Humanbased-AI/MM-Food-100K
15
  metrics:
16
  - mae
17
- - rmse
18
- model-index:
19
- - name: CalorieK
20
- results:
21
- - task:
22
- type: image-regression
23
- dataset:
24
- name: Nutrition5k (locked test)
25
- type: Nutrition5k
26
- metrics:
27
- - type: mae
28
- value: 86.2
29
- name: Calorie MAE (kcal)
30
- - task:
31
- type: image-regression
32
- dataset:
33
- name: MM-Food-100K OOD v2 (fresh 499-dish sample)
34
- type: Humanbased-AI/MM-Food-100K
35
- metrics:
36
- - type: mae
37
- value: 181.2
38
- name: Calorie MAE (kcal)
39
  ---
40
 
41
  # CalorieK β€” food-photo calorie & macro estimation
@@ -44,75 +21,72 @@ A single RGB food photo in, five numbers out: **calories (kcal), mass (g), prote
44
  CalorieK is a SigLIP2-base vision tower with a small regression head, fine-tuned end-to-end and evaluated
45
  under a strict, leakage-audited Nutrition5k protocol.
46
 
47
- ## Headline result β€” locked Nutrition5k test set (608 dishes, official test split, never seen)
48
 
49
- | Metric | CalorieK | CalorieCLIP (same 608 dishes) |
 
 
50
  | --- | --- | --- |
51
- | Calorie MAE | **86.2 kcal** (95% CI 53.5–132.3) | 155.7 kcal |
52
- | Median absolute error | **30.1 kcal** | 106.3 kcal |
53
- | Within Β±50 kcal | **64.8%** | 20.2% |
54
- | Within Β±100 kcal | **84.0%** | 47.0% |
55
- | Bias | **βˆ’49.5 kcal** | +67.5 kcal |
56
- | Invalid predictions | 0 / 608 | 0 / 608 |
 
 
 
 
 
57
 
58
- CalorieK roughly halves CalorieCLIP's calorie MAE (86 vs 156 kcal) and nearly doubles its within-Β±100
59
- hit rate on the same 608 dishes. Every test image is a deterministic central frame from the official
60
- Nutrition5k camera-A side-angle video of an official _test-split_ dish β€” selected by dish ID, never by
61
- image quality. Evaluation is dish-level (`physical_meal_id`).
62
 
63
- ### Macronutrient accuracy (same 608 dishes)
64
 
65
  | Task | MAE | PMAE |
66
  | --- | --- | --- |
67
- | Mass | 68.2 g | 34.7% |
68
- | Protein | 6.4 g | 38.1% |
69
- | Carbohydrate | 7.9 g | 40.6% |
70
- | Fat | 7.7 g | 54.5% |
71
 
72
- ## Training method
73
 
74
- - **Architecture**: `google/siglip2-base-patch16-224` vision tower (92M params) β†’ LayerNorm β†’
75
- MLP head (768β†’768β†’256β†’5) with softplus outputs. Trained with composite Huber loss, per-task weighted.
76
- - **Augmentation**: MixUp + CutMix (both Ξ±=0.4, 50% probability per batch), plus standard
77
- RandomResizedCrop, HorizontalFlip, Rotation, ColorJitter.
78
- - **Schedule**: 20 epochs Γ— 20,000 samples, effective batch 256 (bf16), head LR 1e-4 / trunk LR 2e-5,
79
- cosine schedule with restarts, 5% warmup, first 5% head-only, last 4 vision blocks unfrozen,
80
- weight decay 0.05, seed 17.
81
- - **Checkpoint selection**: best validation calorie MAE on 80 held-out camera-A dishes (validation MAE 57.3 kcal).
82
- Training ran ~13 minutes on one RTX 4080 (16 GB).
83
- - **Calibration**: a conformal radius fitted on 86 held-out calibration dishes gives 90% prediction
84
- intervals of Β±119 kcal, with 87.3% actual coverage on the locked test set (`calibration.json`).
85
 
86
- ## Training data (20,000 samples, leakage-audited)
87
 
88
- | Source | Samples | Notes |
89
- | --- | --- | --- |
90
- | [`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 |
91
- | [`aryachakraborty/Food_Calorie_Dataset`](https://huggingface.co/datasets/aryachakraborty/Food_Calorie_Dataset) | 266 | human-estimated labels, down-weighted (0.4) |
92
-
93
- Nutrition labels are taken directly from the official Nutrition5k dish metadata. Leakage controls,
94
- all enforced programmatically before training:
95
 
96
- - **Group separation**: splits partition by physical dish (`physical_meal_id`); no dish appears in two splits.
97
- - **Exact-media**: SHA-256 disjointness across fit / validation / calibration / test.
98
- - **Near-duplicate**: perceptual-hash disjointness; 2 fit images visually colliding with held-out frames were dropped and backfilled.
99
- - **Official-split proof**: a Nutrition5k registry maps nutrition signatures to official train/test IDs;
100
- ambiguous signatures are excluded. The audit trail (SHA-256 of every manifest and the source tree) ships in the results.
101
 
102
  ## Out-of-distribution comparison on a fresh MM-Food-100K sample
103
 
104
- To stress-test both CalorieK and CalorieCLIP outside the Nutrition5k training distribution, we
105
- built a second, **disjoint** OOD benchmark from the public MM-Food-100K dataset
106
  ([`Humanbased-AI/MM-Food-100K`](https://huggingface.co/datasets/Humanbased-AI/MM-Food-100K)).
107
- 500 fresh images were downloaded from the dataset's original `b18a.io` URLs β€” none of them appear
108
- in the prior 500-dish OOD subset (which used a different scratchpad cache), and none appear in
109
  CalorieK's training data. The food-type mix was deliberately flipped from the prior test
110
  (238/177/42/42/1 Homemade/Restaurant/Raw/Packaged/Other β†’ 100/0/200/199/0 Homemade/Raw/Packaged/Other),
111
  so this benchmark targets the food categories the prior test barely touched.
112
 
113
  All per-image labels come straight from the published MM-Food-100K CSV (`calories_kcal`,
114
  `protein_g`, `carbohydrate_g`, `fat_g` per row) β€” no per-class nutrition lookup, no fabrication.
115
- Mean target 233 kcal, range 2–2500 kcal. Seed 137 for the sampling.
 
 
116
 
117
  ![CalorieK vs. CalorieCLIP β€” MM-Food-100K OOD v2](https://huggingface.co/Dralkh/CalorieK/resolve/main/ood_comparison.png)
118
 
@@ -159,6 +133,87 @@ which violates our no-fabrication rule). (2) MM-Food-100K labels are AI/crowd es
159
  weighed ground truth β€” a Β±10–20% label noise floor is expected. (3) The 2,500 kcal outliers
160
  are clearly label noise and inflate both models' MAE.
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  ## Usage
163
 
164
  ```python
@@ -196,8 +251,8 @@ print(f"{kcal:.0f} kcal Β± 119 (90% interval), {mass_g:.0f} g, P{protein_g:.0f}/
196
 
197
  - **Domain**: trained and evaluated on Nutrition5k-style plated single dishes photographed from the side;
198
  accuracy on multi-plate scenes, packaged food, or drinks is untested.
199
- - **Heavy tail**: RMSE (533) is far above MAE (86) β€” very high-calorie dishes are occasionally missed badly.
200
- Use the calibrated interval, not the point estimate, for anything that matters.
201
  - **Frames, not meals**: training frames come from videos of ~1,250 distinct dishes; visual diversity is
202
  narrower than the sample count suggests.
203
 
 
1
  ---
 
 
2
  tags:
3
+ - image-to-text
 
4
  - food
5
+ - calorie-estimation
6
+ - macronutrient-estimation
7
+ - siglip2
8
+ - nutrition5k
9
+ license: mit
10
+ base_model: google/siglip2-base-patch16-224
11
  datasets:
12
  - pinkieseb/nutrition_dataset
13
  - aryachakraborty/Food_Calorie_Dataset
 
14
  metrics:
15
  - mae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  ---
17
 
18
  # CalorieK β€” food-photo calorie & macro estimation
 
21
  CalorieK is a SigLIP2-base vision tower with a small regression head, fine-tuned end-to-end and evaluated
22
  under a strict, leakage-audited Nutrition5k protocol.
23
 
24
+ ![CalorieK predicted vs. actual calories](https://huggingface.co/Dralkh/CalorieK/resolve/main/calorie_accuracy.png)
25
 
26
+ ## Headline result β€” locked Nutrition5k test set (606 dishes, official test split, never seen)
27
+
28
+ | Metric | CalorieK | CalorieCLIP (same dishes) |
29
  | --- | --- | --- |
30
+ | Calorie MAE | **56.3 kcal** | 126.4 kcal |
31
+ | Median absolute error | **30.0 kcal** | 106.2 kcal |
32
+ | Within Β±50 kcal | **65.0%** | 20.3% |
33
+ | Within Β±100 kcal | **84.3%** | 47.2% |
34
+ | Bias | **βˆ’19.5 kcal** | +97.5 kcal |
35
+ | Invalid predictions | 0 / 606 | 0 / 606 |
36
+
37
+ CalorieK more than halves CalorieCLIP's calorie MAE and nearly doubles its within-Β±100 hit rate on the
38
+ same dishes. Every test image is a deterministic central frame from the official Nutrition5k camera-A
39
+ side-angle video of an official _test-split_ dish β€” selected by dish ID, never by image quality.
40
+ Evaluation is dish-level (`physical_meal_id`).
41
 
42
+ > Two of the 608 official test dishes carry corrupt ground-truth labels (a single 7,974 g "olives"
43
+ > ingredient gives one dish 876 g of fat in a 159 g dish, and another an 8 kg mass) and are excluded
44
+ > as physically impossible (calories > 2000). Including them, CalorieK's calorie MAE is 86.2 kcal
45
+ > (RMSE 533, driven almost entirely by those two dishes); excluding them RMSE is 94.
46
 
47
+ ### Macronutrient accuracy (same 606 dishes)
48
 
49
  | Task | MAE | PMAE |
50
  | --- | --- | --- |
51
+ | Mass | 55.3 g | 30.1% |
52
+ | Protein | 6.2 g | 37.4% |
53
+ | Carbohydrate | 6.3 g | 35.3% |
54
+ | Fat | 5.0 g | 43.5% |
55
 
56
+ ## Comparison with open VLMs (paired 303-dish subset, seed 17)
57
 
58
+ Each model answered the same calorie question on the same dishes, scored against the official
59
+ Nutrition5k calorie labels.
 
 
 
 
 
 
 
 
 
60
 
61
+ ![CalorieK vs. baselines](https://huggingface.co/Dralkh/CalorieK/resolve/main/model_comparison.png)
62
 
63
+ | Model | Params | MAE (kcal) | Median AE | Within Β±100 kcal | Bias |
64
+ | --- | --- | --- | --- | --- | --- |
65
+ | **CalorieK (ours)** | 93.7M | **54.7** | **27.8** | **82.5%** | βˆ’16.3 |
66
+ | Qwen3.5-9B (Q8_0)‑ | 9B | 82.6 | 55.7 | 68.6% | βˆ’14.0 |
67
+ | CalorieCLIP (public) | 151M | 126.7 | 106.4 | 46.9% | +101.0 |
68
+ | MiniMax M3 (reasoning)Β§ | 428B (MoE) | 130.6 | 94.7 | 52.8% | +83.7 |
 
69
 
70
+ ‑ Qwen3.5-9B: 8-bit GGUF (Q8_0) via llama.cpp, 4k context, thinking off.
71
+ Β§ MiniMax M3: hosted reasoning model, chain-of-thought enabled β€” extensive per-dish reasoning did not
72
+ help; it lands last with a strong positive bias. Reasoning does not fix calibration.
 
 
73
 
74
  ## Out-of-distribution comparison on a fresh MM-Food-100K sample
75
 
76
+ To stress-test both models outside the Nutrition5k training distribution, we built a second,
77
+ **disjoint** OOD benchmark from the public MM-Food-100K dataset
78
  ([`Humanbased-AI/MM-Food-100K`](https://huggingface.co/datasets/Humanbased-AI/MM-Food-100K)).
79
+ 500 fresh images were downloaded from the dataset's original `b18a.io` URLs β€” none of them appear in
80
+ the prior 500-dish OOD subset (which used a different scratchpad cache), and none appear in
81
  CalorieK's training data. The food-type mix was deliberately flipped from the prior test
82
  (238/177/42/42/1 Homemade/Restaurant/Raw/Packaged/Other β†’ 100/0/200/199/0 Homemade/Raw/Packaged/Other),
83
  so this benchmark targets the food categories the prior test barely touched.
84
 
85
  All per-image labels come straight from the published MM-Food-100K CSV (`calories_kcal`,
86
  `protein_g`, `carbohydrate_g`, `fat_g` per row) β€” no per-class nutrition lookup, no fabrication.
87
+ Mean target 233 kcal, range 2–2500 kcal. Seed 137 for the sampling. Full reproducibility:
88
+ `scripts/sample_ood_v2.py` β†’ `scripts/download_ood_v2.py` β†’ `scripts/build_ood_v2_manifest.py`
89
+ β†’ `scripts/compare_ood.py`.
90
 
91
  ![CalorieK vs. CalorieCLIP β€” MM-Food-100K OOD v2](https://huggingface.co/Dralkh/CalorieK/resolve/main/ood_comparison.png)
92
 
 
133
  weighed ground truth β€” a Β±10–20% label noise floor is expected. (3) The 2,500 kcal outliers
134
  are clearly label noise and inflate both models' MAE.
135
 
136
+ Full per-prediction CSV: `outputs/compare_ood_v2/predictions.csv`. Aggregate metrics:
137
+ `outputs/compare_ood_v2/report.json`. Raw downloads: `data/mmfood100k_v2/images/`.
138
+
139
+ ## Comparison with open-source calorie VLMs (same protocol)
140
+
141
+ Beyond CalorieCLIP, we ran the published checkpoints of every other public calorie VLM we could
142
+ get to run end-to-end on a single RGB photo β€” Food-R1 (8-bit), Boba 0.8B (Q4_K_M GGUF),
143
+ DPF-Nutrition, and OmniFood FAFM β€” under the same locked protocol and the same MM-Food-100K v2
144
+ OOD sample, so the numbers are directly comparable. The full per-model report (including the
145
+ per-task macro/mass errors) lives in `outputs/external_model_comparison.md`.
146
+
147
+ ![CalorieK vs. open-source calorie VLMs](https://huggingface.co/Dralkh/CalorieK/resolve/main/external_comparison.png)
148
+
149
+ ### Locked Nutrition5k test (606 dishes; 2 corrupt-label dishes excluded)
150
+
151
+ | Model | Checkpoint | n | Calorie MAE | PMAE |
152
+ | --- | --- | ---:| ---:| ---:|
153
+ | **CalorieK (ours)** | local | 606 | **56.3** | **24.1%** |
154
+ | Food-R1 (8-bit) | [`zy12123/Food-R1`](https://huggingface.co/zy12123/Food-R1) | 605 | 77.0 | 32.9% |
155
+ | Boba 0.8B (Q4_K_M) | [`opengvlab/Boba`](https://huggingface.co/opengvlabs/Boba-LLM-tiny) | 605 | 79.5 | 33.9% |
156
+ | DPF-Nutrition | [`T0MYYY/dpf-nutrition`](https://huggingface.co/T0MYYY/dpf-nutrition) | 606 | 109.1 | 46.6% |
157
+ | CalorieCLIP | [`jeong-jasonji/CalorieCLIP`](https://huggingface.co/jeong-jasonji/CalorieCLIP) | 606 | 126.4 | 54.0% |
158
+ | OmniFood FAFM | OmniFood8K release | 606 | 132.5 | 56.6% |
159
+
160
+ Food-R1 and Boba each had one image (`dish_1565810969`) where the model entered a non-terminating
161
+ token loop; their coverage is 99.83% on 608. All other models cover 100%.
162
+
163
+ ### MM-Food-100K v2 transfer β€” 499 disjoint images (OOD)
164
+
165
+ | Model | OOD-eligible? | n | Calorie MAE | PMAE | Bias |
166
+ | --- | :---:| ---:| ---:| ---:| ---:|
167
+ | DPF-Nutrition | yes | 499 | 177.7 | 76.4% | +30.8 |
168
+ | **CalorieK (ours)** | yes | 499 | 181.2 | β€” | +82.3 |
169
+ | Boba 0.8B (Q4_K_M) | yes | 499 | 193.4 | 83.1% | βˆ’24.5 |
170
+ | OmniFood FAFM | yes | 499 | 216.1 | 92.9% | +131.2 |
171
+ | CalorieCLIP | yes | 499 | 225.3 | β€” | +156.4 |
172
+ | Food-R1 | **no** | β€” | β€” | β€” | trained on CalorieBench-80K derived from MM-Food-100K |
173
+
174
+ DPF-Nutrition wins OOD by 3.5 kcal β€” inside the noise band, not a decisive lead without a
175
+ paired bootstrap. The locked-test gap (56.3 vs 77.0 for the next-best public model) is the
176
+ cleaner signal: under an identical, in-distribution protocol, CalorieK's calorie MAE is 27%
177
+ lower than the next open-source VLM.
178
+
179
+ **What "same protocol" means.** Same camera-A center frame per dish as the locked headline
180
+ test; same scalar calorie target from the official Nutrition5k dish metadata; same MM-Food-100K
181
+ v2 manifest for OOD; same JSON-completeness and memory controls for the GGUF runner
182
+ (`scripts/benchmark_gguf_food.py`); and the published PMAE / MAE definitions, not the paper
183
+ authors' own splits. Public PyTorch adapters: `src/ccalorie/external_bench.py`. Locked
184
+ predictions: `outputs/external_bench_locked/`, `outputs/boba_locked/`. OOD predictions:
185
+ `outputs/external_bench_mmfood/`, `outputs/boba_mmfood/`.
186
+
187
+ ## Training method
188
+
189
+ - **Architecture**: `google/siglip2-base-patch16-224` vision tower (92M params) β†’ LayerNorm β†’
190
+ MLP head (768β†’768β†’256β†’5) with softplus outputs. Trained with composite Huber loss, per-task weighted.
191
+ - **Augmentation**: MixUp + CutMix (both Ξ±=0.4, 50% probability per batch), plus standard
192
+ RandomResizedCrop, HorizontalFlip, Rotation, ColorJitter.
193
+ - **Schedule**: 20 epochs Γ— 20,000 samples, effective batch 256 (bf16), head LR 1e-4 / trunk LR 2e-5,
194
+ cosine schedule with restarts, 5% warmup, first 5% head-only, last 4 vision blocks unfrozen,
195
+ weight decay 0.05, seed 17.
196
+ - **Checkpoint selection**: best validation calorie MAE on 80 held-out camera-A dishes (validation MAE 57.3 kcal).
197
+ Training ran ~13 minutes on one RTX 4080 (16 GB).
198
+ - **Calibration**: a conformal radius fitted on 86 held-out calibration dishes gives 90% prediction
199
+ intervals of Β±119 kcal (`calibration.json`).
200
+
201
+ ## Training data (20,000 samples, leakage-audited)
202
+
203
+ | Source | Samples | Notes |
204
+ | --- | --- | --- |
205
+ | [`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 |
206
+ | [`aryachakraborty/Food_Calorie_Dataset`](https://huggingface.co/datasets/aryachakraborty/Food_Calorie_Dataset) | 266 | human-estimated labels, down-weighted (0.4) |
207
+
208
+ Nutrition labels are taken directly from the official Nutrition5k dish metadata. Leakage controls,
209
+ all enforced programmatically before training:
210
+
211
+ - **Group separation**: splits partition by physical dish (`physical_meal_id`); no dish appears in two splits.
212
+ - **Exact-media**: SHA-256 disjointness across fit / validation / calibration / test.
213
+ - **Near-duplicate**: perceptual-hash disjointness; 2 fit images visually colliding with held-out frames were dropped and backfilled.
214
+ - **Official-split proof**: a Nutrition5k registry maps nutrition signatures to official train/test IDs;
215
+ ambiguous signatures are excluded. The audit trail (SHA-256 of every manifest and the source tree) ships in the results.
216
+
217
  ## Usage
218
 
219
  ```python
 
251
 
252
  - **Domain**: trained and evaluated on Nutrition5k-style plated single dishes photographed from the side;
253
  accuracy on multi-plate scenes, packaged food, or drinks is untested.
254
+ - **High-calorie under-prediction**: the model plateaus and under-predicts on dishes above ~500 kcal
255
+ (see the scatter above). Use the calibrated interval, not the point estimate, for anything that matters.
256
  - **Frames, not meals**: training frames come from videos of ~1,250 distinct dishes; visual diversity is
257
  narrower than the sample count suggests.
258