NLag commited on
Commit
7e61c38
·
1 Parent(s): 3a5272a

pioritize BiLSTM model

Browse files
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
- Baseline wins ties to keep runtime inference lightweight unless the temporal model clearly improves
254
- the metrics.
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 baseline this directory should contain `router.joblib`; for the temporal model it should
263
- contain `router.pt`. In both cases, include `router_selection.json` when present so the local loader
264
- can choose the intended active artifact even if multiple artifacts exist.
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 because it scored slightly higher
9
- than the BiLSTM temporal model in the artifact selection evaluation.
10
 
11
  | Field | Value |
12
  | --- | --- |
13
- | Selected model | `baseline.joblib` |
14
- | Selected artifact | `router.joblib` |
15
- | Selection rule | Highest accuracy, then unknown rejection rate; baseline wins ties |
16
- | Local active path | `models/exercise_router/active/router.joblib` |
17
- | Temporal artifact path | `models/exercise_router/active/temporal.pt` |
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 selected artifacts after evaluation:
 
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": "baseline.joblib",
27
- "selected_artifact": "router.joblib",
28
- "reason": "highest accuracy, then unknown rejection rate; baseline wins ties"
29
  }
30
  ```
31
 
32
  Artifacts:
33
 
34
- - `router.joblib`: selected scikit-learn baseline artifact.
35
  - `router_selection.json`: active artifact selector used by Pozify runtime loading.
36
- - `temporal.pt`: PyTorch BiLSTM temporal model trained over 30-frame feature tensors.
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 trained artifacts select the baseline:
97
 
98
  ```json
99
  {
100
- "selected_model": "baseline.joblib",
101
- "selected_artifact": "router.joblib",
102
- "reason": "highest accuracy, then unknown rejection rate; baseline wins ties"
103
  }
104
  ```
105
 
106
- The BiLSTM temporal artifact is uploaded for reproducibility and future selection, but it is not the
107
- active artifact because the baseline currently scores slightly higher.
 
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": "router.pt",
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": "highest accuracy, then unknown rejection rate; baseline wins ties",
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 test_selects_temporal_only_when_metrics_win(self) -> None:
338
  baseline = {"name": "baseline", "accuracy": 0.91, "unknown_rejection_rate": 0.8}
339
- temporal = {"name": "temporal", "accuracy": 0.92, "unknown_rejection_rate": 0.7}
340
 
341
  self.assertEqual(select_router_candidate([baseline, temporal]), temporal)
342
 
343
- def test_selects_baseline_on_metric_tie(self) -> None:
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, temporal]), 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", marker = "extra == 'train'", specifier = ">=2.2.0" },
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