EatingDetection checkpoint
Ten leave-one-participant-out checkpoints for binary eating detection from the smart earring β green PPG + tri-axial accelerometer, no gyroscope, no microphone.
Trained on the 8-second windowed split of a 10-participant earring study. The study data is not public yet, so these weights are released ahead of it: the model, the evaluation protocol and the per-fold scores are all here, but the numbers below cannot be re-derived from raw data until the dataset is released.
Code: https://github.com/FengYe476/Eating_Detection/tree/Feng/eating_detection
Result
| macro-F1 | |
|---|---|
| Pooled over all held-out decisions | 0.9535 |
| Mean over the 10 participants | 0.9486 Β± 0.0253 |
Same ten models, two reductions: pooling concatenates every held-out decision and scores once (each window counts equally); the per-participant mean scores each fold then averages (each person counts equally).
| P2 | P3 | P4 | P5 | P7 | P8 | P9 | P10 | P11 | P12 |
|---|---|---|---|---|---|---|---|---|---|
| .944 | .947 | .947 | .947 | .960 | .996 | .913 | .901 | .964 | .966 |
checkpoint_scores.json holds the per-fold table, including the score stored
inside each checkpoint at training time β re-scoring reproduces all ten to four
decimals.
Files
fold_P<user>.pt β one per held-out participant. fold_P<u>.pt never saw
participant <u> during training, standardisation or checkpoint selection;
that is what makes its score subject-independent. For a participant outside this
cohort, any fold is equally valid.
Each file is a dict: state_dict, mu, sd (per-channel standardisation fitted
on that fold's training subjects), channels, window_samples, in_channels,
test_user, val_f1, test_macro_f1.
Model
CoincidenceTCN, 9,433 parameters. Input (N, 4, 400) β 8 s at 50 Hz,
channel order [ppg_green, acc_x, acc_y, acc_z]. Output: one logit per
window; eating if logit > 0.
import torch
from eating_detection.models.coincidence_tcn import CoincidenceTCN
d = torch.load("fold_P2.pt", map_location="cpu", weights_only=False)
m = CoincidenceTCN(d["in_channels"]); m.load_state_dict(d["state_dict"]); m.eval()
X = ... # (N, 4, 400) float32, raw window values
X = (X - d["mu"]) / d["sd"] # the fold's own statistics β do not refit
eating = m(torch.from_numpy(X.astype("float32"))) > 0
Evaluation protocol
10-fold leave-one-participant-out over P2, P3, P4, P5, P7, P8, P9, P10, P11, P12; both earrings merged as independent windows; 8 s windows on a 1 s stride. Negatives follow "Strategy-2": every eating window, every idle window within two minutes of eating, and far background idle subsampled to 1:1 with eating β so majority-class accuracy would be 0.62, not 0.9.
Reported as macro-F1 with the decision fixed at logit > 0; no per-participant
threshold selection and no per-participant calibration.
Reading these numbers honestly
- Negatives are not uniformly hard: false-positive rate is 0.138 on pauses inside a food block against 0.039 on rest between blocks. Hard-negative balanced accuracy is 0.90 β quote that for a deployment claim.
- P9 and P10 fail in opposite directions (under-detection and over-alarming) and do so reproducibly; the decision rule has no threshold selection.
- Run-to-run spread for this configuration is about 0.8 pp, and CUDA vs MPS alone moves it 0.3β0.4 pp. A single run does not separate two configurations that differ by ~1 pp.