NLag commited on
Commit ·
7e61c38
1
Parent(s): 3a5272a
pioritize BiLSTM model
Browse files- README.md +5 -5
- docs/exercise-router-training-report.md +35 -9
- docs/huggingface-router-model-card.md +5 -5
- docs/huggingface-router-release.md +6 -6
- pyproject.toml +1 -1
- scripts/exercise_router_modal.py +2 -4
- src/pozify/ml/exercise_router_evaluation.py +1 -1
- src/pozify/ml/exercise_router_inference.py +2 -2
- tests/test_exercise_classifier.py +4 -5
- uv.lock +2 -2
README.md
CHANGED
|
@@ -250,8 +250,8 @@ compact BiLSTM over 30-frame feature tensors on a Modal A10 GPU and writes `temp
|
|
| 250 |
73 hidden units, 0.2174 dropout, 0.0004 learning rate, batch size 54, and 73 epochs
|
| 251 |
(https://arxiv.org/abs/2411.11548). Evaluation scores every available trained artifact, writes
|
| 252 |
per-model metrics into `evaluation.json`, and records the active artifact in `router_selection.json`.
|
| 253 |
-
|
| 254 |
-
|
| 255 |
|
| 256 |
Download the selected artifact and its selection file after evaluation, then place them under:
|
| 257 |
|
|
@@ -259,9 +259,9 @@ Download the selected artifact and its selection file after evaluation, then pla
|
|
| 259 |
models/exercise_router/active/
|
| 260 |
```
|
| 261 |
|
| 262 |
-
For the
|
| 263 |
-
|
| 264 |
-
|
| 265 |
|
| 266 |
To publish and load the router from Hugging Face, use the setup notes in
|
| 267 |
[docs/huggingface-router-release.md](docs/huggingface-router-release.md). The draft model card is in
|
|
|
|
| 250 |
73 hidden units, 0.2174 dropout, 0.0004 learning rate, batch size 54, and 73 epochs
|
| 251 |
(https://arxiv.org/abs/2411.11548). Evaluation scores every available trained artifact, writes
|
| 252 |
per-model metrics into `evaluation.json`, and records the active artifact in `router_selection.json`.
|
| 253 |
+
The current selection policy prefers the BiLSTM temporal model when it is available; the baseline is
|
| 254 |
+
kept as a fallback/reference artifact.
|
| 255 |
|
| 256 |
Download the selected artifact and its selection file after evaluation, then place them under:
|
| 257 |
|
|
|
|
| 259 |
models/exercise_router/active/
|
| 260 |
```
|
| 261 |
|
| 262 |
+
For the active BiLSTM router this directory should contain `temporal.pt` and
|
| 263 |
+
`router_selection.json`. Keep `router.joblib` only when you want the baseline artifact available for
|
| 264 |
+
comparison or fallback.
|
| 265 |
|
| 266 |
To publish and load the router from Hugging Face, use the setup notes in
|
| 267 |
[docs/huggingface-router-release.md](docs/huggingface-router-release.md). The draft model card is in
|
docs/exercise-router-training-report.md
CHANGED
|
@@ -5,16 +5,16 @@ Generated after the Modal training run on June 11, 2026.
|
|
| 5 |
## Summary
|
| 6 |
|
| 7 |
The exercise router was trained and evaluated for `squat`, `push_up`, `shoulder_press`, and
|
| 8 |
-
`unknown`. The final active artifact is the scikit-learn baseline
|
| 9 |
-
|
| 10 |
|
| 11 |
| Field | Value |
|
| 12 |
| --- | --- |
|
| 13 |
-
| Selected model | `
|
| 14 |
-
| Selected artifact | `
|
| 15 |
-
| Selection rule |
|
| 16 |
-
| Local active path | `models/exercise_router/active/
|
| 17 |
-
|
|
| 18 |
|
| 19 |
## Data
|
| 20 |
|
|
@@ -48,6 +48,29 @@ The BiLSTM hyperparameters follow the Riccio exercise-classification paper:
|
|
| 48 |
| Batch size | 54 |
|
| 49 |
| Final training loss | 0.0003 |
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
## Training Metrics
|
| 52 |
|
| 53 |
| Model | Validation accuracy | Unknown rejection rate |
|
|
@@ -58,6 +81,8 @@ The BiLSTM hyperparameters follow the Riccio exercise-classification paper:
|
|
| 58 |
## Selection Evaluation
|
| 59 |
|
| 60 |
The final evaluation scored every available trained artifact on the cached router windows.
|
|
|
|
|
|
|
| 61 |
|
| 62 |
| Model | Artifact | Accuracy | Unknown rejection rate |
|
| 63 |
| --- | --- | ---: | ---: |
|
|
@@ -112,12 +137,13 @@ uv run modal run scripts/exercise_router_modal.py --stage train-temporal
|
|
| 112 |
uv run modal run scripts/exercise_router_modal.py --stage evaluate
|
| 113 |
```
|
| 114 |
|
| 115 |
-
Download the
|
|
|
|
| 116 |
|
| 117 |
```bash
|
| 118 |
-
uv run modal volume get --force pozify-router-models /router.joblib models/exercise_router/active/router.joblib
|
| 119 |
uv run modal volume get --force pozify-router-models /temporal.pt models/exercise_router/active/temporal.pt
|
| 120 |
uv run modal volume get --force pozify-router-models /router_selection.json models/exercise_router/active/router_selection.json
|
|
|
|
| 121 |
```
|
| 122 |
|
| 123 |
## Notes
|
|
|
|
| 5 |
## Summary
|
| 6 |
|
| 7 |
The exercise router was trained and evaluated for `squat`, `push_up`, `shoulder_press`, and
|
| 8 |
+
`unknown`. The final active artifact is the BiLSTM temporal model. The scikit-learn baseline is
|
| 9 |
+
retained as a reference and fallback artifact.
|
| 10 |
|
| 11 |
| Field | Value |
|
| 12 |
| --- | --- |
|
| 13 |
+
| Selected model | `temporal.pt` |
|
| 14 |
+
| Selected artifact | `temporal.pt` |
|
| 15 |
+
| Selection rule | Prefer BiLSTM temporal when available; baseline falls back when temporal is missing |
|
| 16 |
+
| Local active path | `models/exercise_router/active/temporal.pt` |
|
| 17 |
+
| Baseline artifact path | `models/exercise_router/active/router.joblib` |
|
| 18 |
|
| 19 |
## Data
|
| 20 |
|
|
|
|
| 48 |
| Batch size | 54 |
|
| 49 |
| Final training loss | 0.0003 |
|
| 50 |
|
| 51 |
+
## Model Complexity
|
| 52 |
+
|
| 53 |
+
Counts were checked from the local trained artifacts with `uv run --extra train`.
|
| 54 |
+
|
| 55 |
+
| Model | Count type | Value |
|
| 56 |
+
| --- | --- | ---: |
|
| 57 |
+
| Baseline | Neural-network-style trainable parameters | 0 |
|
| 58 |
+
| Baseline | Input features | 2,574 |
|
| 59 |
+
| Baseline | Trees | 800 |
|
| 60 |
+
| Baseline | Total tree nodes | 20,778 |
|
| 61 |
+
| Baseline | Split nodes | 9,989 |
|
| 62 |
+
| Baseline | Leaves | 10,789 |
|
| 63 |
+
| Baseline | Approximate learned scalar state | 35,915 |
|
| 64 |
+
| BiLSTM temporal | Trainable parameters | 294,924 |
|
| 65 |
+
| BiLSTM temporal | Input features per frame | 429 |
|
| 66 |
+
| BiLSTM temporal | Hidden units | 73 |
|
| 67 |
+
| BiLSTM temporal | Layers | 1 |
|
| 68 |
+
| BiLSTM temporal | Output classes | 4 |
|
| 69 |
+
|
| 70 |
+
The baseline is a tree-based `HistGradientBoostingClassifier`, so it does not have trainable tensor
|
| 71 |
+
parameters in the same sense as a neural network. The BiLSTM parameter count includes both LSTM
|
| 72 |
+
directions and the linear classification head.
|
| 73 |
+
|
| 74 |
## Training Metrics
|
| 75 |
|
| 76 |
| Model | Validation accuracy | Unknown rejection rate |
|
|
|
|
| 81 |
## Selection Evaluation
|
| 82 |
|
| 83 |
The final evaluation scored every available trained artifact on the cached router windows.
|
| 84 |
+
The baseline scored slightly higher on this cache, but the active router is BiLSTM so routing uses
|
| 85 |
+
the temporal pose-window sequence directly.
|
| 86 |
|
| 87 |
| Model | Artifact | Accuracy | Unknown rejection rate |
|
| 88 |
| --- | --- | ---: | ---: |
|
|
|
|
| 137 |
uv run modal run scripts/exercise_router_modal.py --stage evaluate
|
| 138 |
```
|
| 139 |
|
| 140 |
+
Download the active artifact and selection file after evaluation. Download `router.joblib` too when
|
| 141 |
+
you want to keep the baseline for comparison or fallback:
|
| 142 |
|
| 143 |
```bash
|
|
|
|
| 144 |
uv run modal volume get --force pozify-router-models /temporal.pt models/exercise_router/active/temporal.pt
|
| 145 |
uv run modal volume get --force pozify-router-models /router_selection.json models/exercise_router/active/router_selection.json
|
| 146 |
+
uv run modal volume get --force pozify-router-models /router.joblib models/exercise_router/active/router.joblib
|
| 147 |
```
|
| 148 |
|
| 149 |
## Notes
|
docs/huggingface-router-model-card.md
CHANGED
|
@@ -23,17 +23,17 @@ Current selected artifact:
|
|
| 23 |
|
| 24 |
```json
|
| 25 |
{
|
| 26 |
-
"selected_model": "
|
| 27 |
-
"selected_artifact": "
|
| 28 |
-
"reason": "
|
| 29 |
}
|
| 30 |
```
|
| 31 |
|
| 32 |
Artifacts:
|
| 33 |
|
| 34 |
-
- `
|
| 35 |
- `router_selection.json`: active artifact selector used by Pozify runtime loading.
|
| 36 |
-
- `
|
| 37 |
- `training_report.md`: training and evaluation metrics.
|
| 38 |
|
| 39 |
## Intended Use
|
|
|
|
| 23 |
|
| 24 |
```json
|
| 25 |
{
|
| 26 |
+
"selected_model": "temporal.pt",
|
| 27 |
+
"selected_artifact": "temporal.pt",
|
| 28 |
+
"reason": "prefer BiLSTM temporal when available; baseline falls back when temporal is missing"
|
| 29 |
}
|
| 30 |
```
|
| 31 |
|
| 32 |
Artifacts:
|
| 33 |
|
| 34 |
+
- `temporal.pt`: selected PyTorch BiLSTM temporal model trained over 30-frame feature tensors.
|
| 35 |
- `router_selection.json`: active artifact selector used by Pozify runtime loading.
|
| 36 |
+
- `router.joblib`: scikit-learn baseline artifact kept for comparison and fallback.
|
| 37 |
- `training_report.md`: training and evaluation metrics.
|
| 38 |
|
| 39 |
## Intended Use
|
docs/huggingface-router-release.md
CHANGED
|
@@ -93,15 +93,15 @@ export POZIFY_ROUTER_DISABLE_HF=1
|
|
| 93 |
|
| 94 |
## Expected Artifact Selection
|
| 95 |
|
| 96 |
-
The current
|
| 97 |
|
| 98 |
```json
|
| 99 |
{
|
| 100 |
-
"selected_model": "
|
| 101 |
-
"selected_artifact": "
|
| 102 |
-
"reason": "
|
| 103 |
}
|
| 104 |
```
|
| 105 |
|
| 106 |
-
The
|
| 107 |
-
|
|
|
|
| 93 |
|
| 94 |
## Expected Artifact Selection
|
| 95 |
|
| 96 |
+
The current active router selects the BiLSTM temporal artifact:
|
| 97 |
|
| 98 |
```json
|
| 99 |
{
|
| 100 |
+
"selected_model": "temporal.pt",
|
| 101 |
+
"selected_artifact": "temporal.pt",
|
| 102 |
+
"reason": "prefer BiLSTM temporal when available; baseline falls back when temporal is missing"
|
| 103 |
}
|
| 104 |
```
|
| 105 |
|
| 106 |
+
The baseline artifact is still uploaded for comparison and fallback, but runtime routing uses the
|
| 107 |
+
BiLSTM when `router_selection.json` points at `temporal.pt`.
|
pyproject.toml
CHANGED
|
@@ -16,6 +16,7 @@ dependencies = [
|
|
| 16 |
# HF Spaces installs gradio[mcp,oauth], which requires pydantic<=2.12.5.
|
| 17 |
"pydantic>=2.11.10,<=2.12.5",
|
| 18 |
"scikit-learn>=1.4.0",
|
|
|
|
| 19 |
]
|
| 20 |
|
| 21 |
[project.optional-dependencies]
|
|
@@ -24,7 +25,6 @@ dev = [
|
|
| 24 |
]
|
| 25 |
train = [
|
| 26 |
"datasets>=2.20.0",
|
| 27 |
-
"torch>=2.2.0",
|
| 28 |
]
|
| 29 |
|
| 30 |
[tool.uv]
|
|
|
|
| 16 |
# HF Spaces installs gradio[mcp,oauth], which requires pydantic<=2.12.5.
|
| 17 |
"pydantic>=2.11.10,<=2.12.5",
|
| 18 |
"scikit-learn>=1.4.0",
|
| 19 |
+
"torch>=2.2.0",
|
| 20 |
]
|
| 21 |
|
| 22 |
[project.optional-dependencies]
|
|
|
|
| 25 |
]
|
| 26 |
train = [
|
| 27 |
"datasets>=2.20.0",
|
|
|
|
| 28 |
]
|
| 29 |
|
| 30 |
[tool.uv]
|
scripts/exercise_router_modal.py
CHANGED
|
@@ -546,7 +546,7 @@ def evaluate() -> dict[str, Any]:
|
|
| 546 |
{
|
| 547 |
"name": "temporal",
|
| 548 |
"source_artifact": temporal_path,
|
| 549 |
-
"selected_artifact": "
|
| 550 |
**temporal_evaluation,
|
| 551 |
}
|
| 552 |
)
|
|
@@ -560,12 +560,10 @@ def evaluate() -> dict[str, Any]:
|
|
| 560 |
selected = select_router_candidate(candidates)
|
| 561 |
if selected["selected_artifact"] == "router.joblib":
|
| 562 |
shutil.copyfile(selected["source_artifact"], MODEL_ROOT / "router.joblib")
|
| 563 |
-
else:
|
| 564 |
-
shutil.copyfile(selected["source_artifact"], MODEL_ROOT / "router.pt")
|
| 565 |
selection = {
|
| 566 |
"selected_model": f"{selected['name']}.{ 'joblib' if selected['name'] == 'baseline' else 'pt' }",
|
| 567 |
"selected_artifact": selected["selected_artifact"],
|
| 568 |
-
"reason": "
|
| 569 |
}
|
| 570 |
_write_json(MODEL_ROOT / "router_selection.json", selection)
|
| 571 |
result = {
|
|
|
|
| 546 |
{
|
| 547 |
"name": "temporal",
|
| 548 |
"source_artifact": temporal_path,
|
| 549 |
+
"selected_artifact": "temporal.pt",
|
| 550 |
**temporal_evaluation,
|
| 551 |
}
|
| 552 |
)
|
|
|
|
| 560 |
selected = select_router_candidate(candidates)
|
| 561 |
if selected["selected_artifact"] == "router.joblib":
|
| 562 |
shutil.copyfile(selected["source_artifact"], MODEL_ROOT / "router.joblib")
|
|
|
|
|
|
|
| 563 |
selection = {
|
| 564 |
"selected_model": f"{selected['name']}.{ 'joblib' if selected['name'] == 'baseline' else 'pt' }",
|
| 565 |
"selected_artifact": selected["selected_artifact"],
|
| 566 |
+
"reason": "prefer BiLSTM temporal when available; baseline falls back when temporal is missing",
|
| 567 |
}
|
| 568 |
_write_json(MODEL_ROOT / "router_selection.json", selection)
|
| 569 |
result = {
|
src/pozify/ml/exercise_router_evaluation.py
CHANGED
|
@@ -72,7 +72,7 @@ def select_router_candidate(candidates: Sequence[Mapping[str, Any]]) -> Mapping[
|
|
| 72 |
|
| 73 |
def router_candidate_sort_key(candidate: Mapping[str, Any]) -> tuple[float, float, int]:
|
| 74 |
return (
|
|
|
|
| 75 |
float(candidate.get("accuracy", 0.0)),
|
| 76 |
float(candidate.get("unknown_rejection_rate", 0.0)),
|
| 77 |
-
1 if candidate.get("name") == "baseline" else 0,
|
| 78 |
)
|
|
|
|
| 72 |
|
| 73 |
def router_candidate_sort_key(candidate: Mapping[str, Any]) -> tuple[float, float, int]:
|
| 74 |
return (
|
| 75 |
+
1 if candidate.get("name") == "temporal" else 0,
|
| 76 |
float(candidate.get("accuracy", 0.0)),
|
| 77 |
float(candidate.get("unknown_rejection_rate", 0.0)),
|
|
|
|
| 78 |
)
|
src/pozify/ml/exercise_router_inference.py
CHANGED
|
@@ -18,11 +18,11 @@ HF_REPO_ID_ENV = "POZIFY_ROUTER_HF_REPO_ID"
|
|
| 18 |
HF_REVISION_ENV = "POZIFY_ROUTER_HF_REVISION"
|
| 19 |
HF_DISABLE_ENV = "POZIFY_ROUTER_DISABLE_HF"
|
| 20 |
MODEL_FILENAMES = (
|
| 21 |
-
"router.joblib",
|
| 22 |
"router.pt",
|
|
|
|
|
|
|
| 23 |
"model.joblib",
|
| 24 |
"baseline.joblib",
|
| 25 |
-
"temporal.pt",
|
| 26 |
"exercise_router.joblib",
|
| 27 |
)
|
| 28 |
MIN_FINAL_CONFIDENCE = 0.65
|
|
|
|
| 18 |
HF_REVISION_ENV = "POZIFY_ROUTER_HF_REVISION"
|
| 19 |
HF_DISABLE_ENV = "POZIFY_ROUTER_DISABLE_HF"
|
| 20 |
MODEL_FILENAMES = (
|
|
|
|
| 21 |
"router.pt",
|
| 22 |
+
"temporal.pt",
|
| 23 |
+
"router.joblib",
|
| 24 |
"model.joblib",
|
| 25 |
"baseline.joblib",
|
|
|
|
| 26 |
"exercise_router.joblib",
|
| 27 |
)
|
| 28 |
MIN_FINAL_CONFIDENCE = 0.65
|
tests/test_exercise_classifier.py
CHANGED
|
@@ -334,17 +334,16 @@ class ExerciseRouterEvaluationTests(unittest.TestCase):
|
|
| 334 |
self.assertEqual(evaluation.confusion_matrix["shoulder_press"]["unknown"], 1)
|
| 335 |
self.assertEqual(evaluation.unknown_rejection_rate, 1.0)
|
| 336 |
|
| 337 |
-
def
|
| 338 |
baseline = {"name": "baseline", "accuracy": 0.91, "unknown_rejection_rate": 0.8}
|
| 339 |
-
temporal = {"name": "temporal", "accuracy": 0.
|
| 340 |
|
| 341 |
self.assertEqual(select_router_candidate([baseline, temporal]), temporal)
|
| 342 |
|
| 343 |
-
def
|
| 344 |
baseline = {"name": "baseline", "accuracy": 0.91, "unknown_rejection_rate": 0.8}
|
| 345 |
-
temporal = {"name": "temporal", "accuracy": 0.91, "unknown_rejection_rate": 0.8}
|
| 346 |
|
| 347 |
-
self.assertEqual(select_router_candidate([baseline
|
| 348 |
|
| 349 |
|
| 350 |
if __name__ == "__main__":
|
|
|
|
| 334 |
self.assertEqual(evaluation.confusion_matrix["shoulder_press"]["unknown"], 1)
|
| 335 |
self.assertEqual(evaluation.unknown_rejection_rate, 1.0)
|
| 336 |
|
| 337 |
+
def test_prefers_temporal_when_available(self) -> None:
|
| 338 |
baseline = {"name": "baseline", "accuracy": 0.91, "unknown_rejection_rate": 0.8}
|
| 339 |
+
temporal = {"name": "temporal", "accuracy": 0.90, "unknown_rejection_rate": 0.7}
|
| 340 |
|
| 341 |
self.assertEqual(select_router_candidate([baseline, temporal]), temporal)
|
| 342 |
|
| 343 |
+
def test_selects_baseline_when_temporal_is_missing(self) -> None:
|
| 344 |
baseline = {"name": "baseline", "accuracy": 0.91, "unknown_rejection_rate": 0.8}
|
|
|
|
| 345 |
|
| 346 |
+
self.assertEqual(select_router_candidate([baseline]), baseline)
|
| 347 |
|
| 348 |
|
| 349 |
if __name__ == "__main__":
|
uv.lock
CHANGED
|
@@ -2191,6 +2191,7 @@ dependencies = [
|
|
| 2191 |
{ name = "opencv-python-headless" },
|
| 2192 |
{ name = "pydantic" },
|
| 2193 |
{ name = "scikit-learn" },
|
|
|
|
| 2194 |
]
|
| 2195 |
|
| 2196 |
[package.optional-dependencies]
|
|
@@ -2199,7 +2200,6 @@ dev = [
|
|
| 2199 |
]
|
| 2200 |
train = [
|
| 2201 |
{ name = "datasets" },
|
| 2202 |
-
{ name = "torch" },
|
| 2203 |
]
|
| 2204 |
|
| 2205 |
[package.metadata]
|
|
@@ -2216,7 +2216,7 @@ requires-dist = [
|
|
| 2216 |
{ name = "pydantic", specifier = ">=2.11.10,<=2.12.5" },
|
| 2217 |
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.0" },
|
| 2218 |
{ name = "scikit-learn", specifier = ">=1.4.0" },
|
| 2219 |
-
{ name = "torch",
|
| 2220 |
]
|
| 2221 |
provides-extras = ["dev", "train"]
|
| 2222 |
|
|
|
|
| 2191 |
{ name = "opencv-python-headless" },
|
| 2192 |
{ name = "pydantic" },
|
| 2193 |
{ name = "scikit-learn" },
|
| 2194 |
+
{ name = "torch" },
|
| 2195 |
]
|
| 2196 |
|
| 2197 |
[package.optional-dependencies]
|
|
|
|
| 2200 |
]
|
| 2201 |
train = [
|
| 2202 |
{ name = "datasets" },
|
|
|
|
| 2203 |
]
|
| 2204 |
|
| 2205 |
[package.metadata]
|
|
|
|
| 2216 |
{ name = "pydantic", specifier = ">=2.11.10,<=2.12.5" },
|
| 2217 |
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.0" },
|
| 2218 |
{ name = "scikit-learn", specifier = ">=1.4.0" },
|
| 2219 |
+
{ name = "torch", specifier = ">=2.2.0" },
|
| 2220 |
]
|
| 2221 |
provides-extras = ["dev", "train"]
|
| 2222 |
|