Add inference code, config, and technical report
Browse files- config/folds.json +1057 -0
- reports/phase2_technical_report.md +195 -0
- requirements.txt +36 -0
- solution.py +198 -0
- src/__init__.py +1 -0
- src/data/__init__.py +30 -0
- src/data/causal_features.py +117 -0
- src/data/dataset.py +119 -0
- src/data/protocol.py +229 -0
- src/evaluation/__init__.py +3 -0
- src/evaluation/stepwise.py +107 -0
- src/models/__init__.py +32 -0
- src/models/baselines.py +103 -0
- src/models/ensemble_predictor.py +49 -0
- src/models/sequence_inference.py +73 -0
- src/models/sequence_models.py +127 -0
- src/models/tcn.py +115 -0
- src/training/__init__.py +18 -0
- src/training/baseline_training.py +68 -0
- src/training/sequence_trainer.py +199 -0
- src/utils/__init__.py +25 -0
- src/utils/hardware.py +279 -0
- src/utils/metrics.py +59 -0
- src/utils/reproducibility.py +56 -0
config/folds.json
ADDED
|
@@ -0,0 +1,1057 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dataset_sha256": "79c14580e40baa6c0d08faf84d3b754e92be28aaf80fc6be4a3b341ea35a0544",
|
| 3 |
+
"final_holdout_fold": 4,
|
| 4 |
+
"final_holdout_seq_ids": [
|
| 5 |
+
1,
|
| 6 |
+
13,
|
| 7 |
+
20,
|
| 8 |
+
21,
|
| 9 |
+
34,
|
| 10 |
+
43,
|
| 11 |
+
48,
|
| 12 |
+
49,
|
| 13 |
+
50,
|
| 14 |
+
52,
|
| 15 |
+
53,
|
| 16 |
+
54,
|
| 17 |
+
58,
|
| 18 |
+
71,
|
| 19 |
+
80,
|
| 20 |
+
87,
|
| 21 |
+
88,
|
| 22 |
+
91,
|
| 23 |
+
99,
|
| 24 |
+
102,
|
| 25 |
+
105,
|
| 26 |
+
106,
|
| 27 |
+
121,
|
| 28 |
+
130,
|
| 29 |
+
134,
|
| 30 |
+
149,
|
| 31 |
+
151,
|
| 32 |
+
160,
|
| 33 |
+
161,
|
| 34 |
+
166,
|
| 35 |
+
169,
|
| 36 |
+
174,
|
| 37 |
+
187,
|
| 38 |
+
188,
|
| 39 |
+
189,
|
| 40 |
+
190,
|
| 41 |
+
191,
|
| 42 |
+
201,
|
| 43 |
+
205,
|
| 44 |
+
214,
|
| 45 |
+
217,
|
| 46 |
+
235,
|
| 47 |
+
241,
|
| 48 |
+
243,
|
| 49 |
+
251,
|
| 50 |
+
252,
|
| 51 |
+
257,
|
| 52 |
+
259,
|
| 53 |
+
263,
|
| 54 |
+
264,
|
| 55 |
+
269,
|
| 56 |
+
270,
|
| 57 |
+
273,
|
| 58 |
+
276,
|
| 59 |
+
293,
|
| 60 |
+
303,
|
| 61 |
+
306,
|
| 62 |
+
308,
|
| 63 |
+
309,
|
| 64 |
+
313,
|
| 65 |
+
315,
|
| 66 |
+
319,
|
| 67 |
+
328,
|
| 68 |
+
330,
|
| 69 |
+
339,
|
| 70 |
+
343,
|
| 71 |
+
344,
|
| 72 |
+
345,
|
| 73 |
+
350,
|
| 74 |
+
359,
|
| 75 |
+
363,
|
| 76 |
+
366,
|
| 77 |
+
372,
|
| 78 |
+
385,
|
| 79 |
+
387,
|
| 80 |
+
389,
|
| 81 |
+
401,
|
| 82 |
+
413,
|
| 83 |
+
419,
|
| 84 |
+
427,
|
| 85 |
+
430,
|
| 86 |
+
435,
|
| 87 |
+
443,
|
| 88 |
+
445,
|
| 89 |
+
455,
|
| 90 |
+
458,
|
| 91 |
+
459,
|
| 92 |
+
463,
|
| 93 |
+
466,
|
| 94 |
+
470,
|
| 95 |
+
471,
|
| 96 |
+
472,
|
| 97 |
+
474,
|
| 98 |
+
475,
|
| 99 |
+
488,
|
| 100 |
+
490,
|
| 101 |
+
491,
|
| 102 |
+
498,
|
| 103 |
+
500,
|
| 104 |
+
506,
|
| 105 |
+
510,
|
| 106 |
+
514,
|
| 107 |
+
516
|
| 108 |
+
],
|
| 109 |
+
"folds": {
|
| 110 |
+
"0": [
|
| 111 |
+
0,
|
| 112 |
+
2,
|
| 113 |
+
9,
|
| 114 |
+
10,
|
| 115 |
+
11,
|
| 116 |
+
18,
|
| 117 |
+
22,
|
| 118 |
+
30,
|
| 119 |
+
33,
|
| 120 |
+
39,
|
| 121 |
+
46,
|
| 122 |
+
55,
|
| 123 |
+
63,
|
| 124 |
+
68,
|
| 125 |
+
69,
|
| 126 |
+
70,
|
| 127 |
+
72,
|
| 128 |
+
73,
|
| 129 |
+
76,
|
| 130 |
+
77,
|
| 131 |
+
78,
|
| 132 |
+
79,
|
| 133 |
+
82,
|
| 134 |
+
84,
|
| 135 |
+
90,
|
| 136 |
+
93,
|
| 137 |
+
101,
|
| 138 |
+
104,
|
| 139 |
+
124,
|
| 140 |
+
131,
|
| 141 |
+
132,
|
| 142 |
+
137,
|
| 143 |
+
140,
|
| 144 |
+
148,
|
| 145 |
+
153,
|
| 146 |
+
155,
|
| 147 |
+
167,
|
| 148 |
+
172,
|
| 149 |
+
176,
|
| 150 |
+
177,
|
| 151 |
+
184,
|
| 152 |
+
194,
|
| 153 |
+
195,
|
| 154 |
+
203,
|
| 155 |
+
204,
|
| 156 |
+
208,
|
| 157 |
+
209,
|
| 158 |
+
220,
|
| 159 |
+
222,
|
| 160 |
+
225,
|
| 161 |
+
227,
|
| 162 |
+
231,
|
| 163 |
+
237,
|
| 164 |
+
238,
|
| 165 |
+
244,
|
| 166 |
+
247,
|
| 167 |
+
249,
|
| 168 |
+
250,
|
| 169 |
+
253,
|
| 170 |
+
275,
|
| 171 |
+
277,
|
| 172 |
+
281,
|
| 173 |
+
287,
|
| 174 |
+
289,
|
| 175 |
+
296,
|
| 176 |
+
298,
|
| 177 |
+
304,
|
| 178 |
+
317,
|
| 179 |
+
318,
|
| 180 |
+
320,
|
| 181 |
+
323,
|
| 182 |
+
324,
|
| 183 |
+
325,
|
| 184 |
+
331,
|
| 185 |
+
341,
|
| 186 |
+
342,
|
| 187 |
+
353,
|
| 188 |
+
357,
|
| 189 |
+
362,
|
| 190 |
+
382,
|
| 191 |
+
398,
|
| 192 |
+
400,
|
| 193 |
+
403,
|
| 194 |
+
405,
|
| 195 |
+
426,
|
| 196 |
+
429,
|
| 197 |
+
431,
|
| 198 |
+
441,
|
| 199 |
+
447,
|
| 200 |
+
451,
|
| 201 |
+
454,
|
| 202 |
+
465,
|
| 203 |
+
469,
|
| 204 |
+
477,
|
| 205 |
+
487,
|
| 206 |
+
489,
|
| 207 |
+
501,
|
| 208 |
+
503,
|
| 209 |
+
504,
|
| 210 |
+
507,
|
| 211 |
+
508,
|
| 212 |
+
509,
|
| 213 |
+
512,
|
| 214 |
+
513
|
| 215 |
+
],
|
| 216 |
+
"1": [
|
| 217 |
+
3,
|
| 218 |
+
5,
|
| 219 |
+
7,
|
| 220 |
+
15,
|
| 221 |
+
16,
|
| 222 |
+
17,
|
| 223 |
+
19,
|
| 224 |
+
23,
|
| 225 |
+
24,
|
| 226 |
+
25,
|
| 227 |
+
26,
|
| 228 |
+
29,
|
| 229 |
+
31,
|
| 230 |
+
37,
|
| 231 |
+
42,
|
| 232 |
+
45,
|
| 233 |
+
56,
|
| 234 |
+
57,
|
| 235 |
+
60,
|
| 236 |
+
66,
|
| 237 |
+
67,
|
| 238 |
+
75,
|
| 239 |
+
86,
|
| 240 |
+
94,
|
| 241 |
+
108,
|
| 242 |
+
110,
|
| 243 |
+
113,
|
| 244 |
+
114,
|
| 245 |
+
116,
|
| 246 |
+
117,
|
| 247 |
+
118,
|
| 248 |
+
126,
|
| 249 |
+
141,
|
| 250 |
+
144,
|
| 251 |
+
154,
|
| 252 |
+
157,
|
| 253 |
+
168,
|
| 254 |
+
173,
|
| 255 |
+
175,
|
| 256 |
+
180,
|
| 257 |
+
181,
|
| 258 |
+
182,
|
| 259 |
+
185,
|
| 260 |
+
192,
|
| 261 |
+
210,
|
| 262 |
+
211,
|
| 263 |
+
218,
|
| 264 |
+
228,
|
| 265 |
+
234,
|
| 266 |
+
255,
|
| 267 |
+
265,
|
| 268 |
+
266,
|
| 269 |
+
268,
|
| 270 |
+
271,
|
| 271 |
+
272,
|
| 272 |
+
274,
|
| 273 |
+
278,
|
| 274 |
+
284,
|
| 275 |
+
286,
|
| 276 |
+
299,
|
| 277 |
+
302,
|
| 278 |
+
311,
|
| 279 |
+
312,
|
| 280 |
+
321,
|
| 281 |
+
329,
|
| 282 |
+
332,
|
| 283 |
+
334,
|
| 284 |
+
340,
|
| 285 |
+
349,
|
| 286 |
+
352,
|
| 287 |
+
356,
|
| 288 |
+
367,
|
| 289 |
+
368,
|
| 290 |
+
373,
|
| 291 |
+
376,
|
| 292 |
+
380,
|
| 293 |
+
384,
|
| 294 |
+
391,
|
| 295 |
+
393,
|
| 296 |
+
396,
|
| 297 |
+
397,
|
| 298 |
+
408,
|
| 299 |
+
409,
|
| 300 |
+
412,
|
| 301 |
+
416,
|
| 302 |
+
420,
|
| 303 |
+
421,
|
| 304 |
+
422,
|
| 305 |
+
433,
|
| 306 |
+
438,
|
| 307 |
+
446,
|
| 308 |
+
452,
|
| 309 |
+
456,
|
| 310 |
+
457,
|
| 311 |
+
467,
|
| 312 |
+
468,
|
| 313 |
+
479,
|
| 314 |
+
483,
|
| 315 |
+
485,
|
| 316 |
+
496,
|
| 317 |
+
497,
|
| 318 |
+
505,
|
| 319 |
+
511,
|
| 320 |
+
515
|
| 321 |
+
],
|
| 322 |
+
"2": [
|
| 323 |
+
6,
|
| 324 |
+
36,
|
| 325 |
+
38,
|
| 326 |
+
59,
|
| 327 |
+
74,
|
| 328 |
+
81,
|
| 329 |
+
83,
|
| 330 |
+
89,
|
| 331 |
+
92,
|
| 332 |
+
96,
|
| 333 |
+
97,
|
| 334 |
+
103,
|
| 335 |
+
109,
|
| 336 |
+
111,
|
| 337 |
+
112,
|
| 338 |
+
119,
|
| 339 |
+
122,
|
| 340 |
+
123,
|
| 341 |
+
125,
|
| 342 |
+
129,
|
| 343 |
+
139,
|
| 344 |
+
143,
|
| 345 |
+
145,
|
| 346 |
+
146,
|
| 347 |
+
147,
|
| 348 |
+
150,
|
| 349 |
+
152,
|
| 350 |
+
158,
|
| 351 |
+
163,
|
| 352 |
+
165,
|
| 353 |
+
179,
|
| 354 |
+
183,
|
| 355 |
+
193,
|
| 356 |
+
196,
|
| 357 |
+
198,
|
| 358 |
+
199,
|
| 359 |
+
202,
|
| 360 |
+
219,
|
| 361 |
+
223,
|
| 362 |
+
229,
|
| 363 |
+
233,
|
| 364 |
+
239,
|
| 365 |
+
245,
|
| 366 |
+
246,
|
| 367 |
+
248,
|
| 368 |
+
261,
|
| 369 |
+
262,
|
| 370 |
+
280,
|
| 371 |
+
290,
|
| 372 |
+
291,
|
| 373 |
+
294,
|
| 374 |
+
297,
|
| 375 |
+
301,
|
| 376 |
+
305,
|
| 377 |
+
307,
|
| 378 |
+
310,
|
| 379 |
+
314,
|
| 380 |
+
316,
|
| 381 |
+
322,
|
| 382 |
+
335,
|
| 383 |
+
336,
|
| 384 |
+
338,
|
| 385 |
+
346,
|
| 386 |
+
347,
|
| 387 |
+
348,
|
| 388 |
+
354,
|
| 389 |
+
360,
|
| 390 |
+
361,
|
| 391 |
+
364,
|
| 392 |
+
365,
|
| 393 |
+
370,
|
| 394 |
+
371,
|
| 395 |
+
375,
|
| 396 |
+
377,
|
| 397 |
+
386,
|
| 398 |
+
388,
|
| 399 |
+
392,
|
| 400 |
+
394,
|
| 401 |
+
402,
|
| 402 |
+
404,
|
| 403 |
+
406,
|
| 404 |
+
407,
|
| 405 |
+
411,
|
| 406 |
+
417,
|
| 407 |
+
418,
|
| 408 |
+
428,
|
| 409 |
+
432,
|
| 410 |
+
440,
|
| 411 |
+
442,
|
| 412 |
+
450,
|
| 413 |
+
453,
|
| 414 |
+
461,
|
| 415 |
+
464,
|
| 416 |
+
473,
|
| 417 |
+
481,
|
| 418 |
+
482,
|
| 419 |
+
484,
|
| 420 |
+
486,
|
| 421 |
+
492,
|
| 422 |
+
493,
|
| 423 |
+
495,
|
| 424 |
+
499,
|
| 425 |
+
502
|
| 426 |
+
],
|
| 427 |
+
"3": [
|
| 428 |
+
4,
|
| 429 |
+
8,
|
| 430 |
+
12,
|
| 431 |
+
14,
|
| 432 |
+
27,
|
| 433 |
+
28,
|
| 434 |
+
32,
|
| 435 |
+
35,
|
| 436 |
+
40,
|
| 437 |
+
41,
|
| 438 |
+
44,
|
| 439 |
+
47,
|
| 440 |
+
51,
|
| 441 |
+
61,
|
| 442 |
+
62,
|
| 443 |
+
64,
|
| 444 |
+
65,
|
| 445 |
+
85,
|
| 446 |
+
95,
|
| 447 |
+
98,
|
| 448 |
+
100,
|
| 449 |
+
107,
|
| 450 |
+
115,
|
| 451 |
+
120,
|
| 452 |
+
127,
|
| 453 |
+
128,
|
| 454 |
+
133,
|
| 455 |
+
135,
|
| 456 |
+
136,
|
| 457 |
+
138,
|
| 458 |
+
142,
|
| 459 |
+
156,
|
| 460 |
+
159,
|
| 461 |
+
162,
|
| 462 |
+
164,
|
| 463 |
+
170,
|
| 464 |
+
171,
|
| 465 |
+
178,
|
| 466 |
+
186,
|
| 467 |
+
197,
|
| 468 |
+
200,
|
| 469 |
+
206,
|
| 470 |
+
207,
|
| 471 |
+
212,
|
| 472 |
+
213,
|
| 473 |
+
215,
|
| 474 |
+
216,
|
| 475 |
+
221,
|
| 476 |
+
224,
|
| 477 |
+
226,
|
| 478 |
+
230,
|
| 479 |
+
232,
|
| 480 |
+
236,
|
| 481 |
+
240,
|
| 482 |
+
242,
|
| 483 |
+
254,
|
| 484 |
+
256,
|
| 485 |
+
258,
|
| 486 |
+
260,
|
| 487 |
+
267,
|
| 488 |
+
279,
|
| 489 |
+
282,
|
| 490 |
+
283,
|
| 491 |
+
285,
|
| 492 |
+
288,
|
| 493 |
+
292,
|
| 494 |
+
295,
|
| 495 |
+
300,
|
| 496 |
+
326,
|
| 497 |
+
327,
|
| 498 |
+
333,
|
| 499 |
+
337,
|
| 500 |
+
351,
|
| 501 |
+
355,
|
| 502 |
+
358,
|
| 503 |
+
369,
|
| 504 |
+
374,
|
| 505 |
+
378,
|
| 506 |
+
379,
|
| 507 |
+
381,
|
| 508 |
+
383,
|
| 509 |
+
390,
|
| 510 |
+
395,
|
| 511 |
+
399,
|
| 512 |
+
410,
|
| 513 |
+
414,
|
| 514 |
+
415,
|
| 515 |
+
423,
|
| 516 |
+
424,
|
| 517 |
+
425,
|
| 518 |
+
434,
|
| 519 |
+
436,
|
| 520 |
+
437,
|
| 521 |
+
439,
|
| 522 |
+
444,
|
| 523 |
+
448,
|
| 524 |
+
449,
|
| 525 |
+
460,
|
| 526 |
+
462,
|
| 527 |
+
476,
|
| 528 |
+
478,
|
| 529 |
+
480,
|
| 530 |
+
494
|
| 531 |
+
],
|
| 532 |
+
"4": [
|
| 533 |
+
1,
|
| 534 |
+
13,
|
| 535 |
+
20,
|
| 536 |
+
21,
|
| 537 |
+
34,
|
| 538 |
+
43,
|
| 539 |
+
48,
|
| 540 |
+
49,
|
| 541 |
+
50,
|
| 542 |
+
52,
|
| 543 |
+
53,
|
| 544 |
+
54,
|
| 545 |
+
58,
|
| 546 |
+
71,
|
| 547 |
+
80,
|
| 548 |
+
87,
|
| 549 |
+
88,
|
| 550 |
+
91,
|
| 551 |
+
99,
|
| 552 |
+
102,
|
| 553 |
+
105,
|
| 554 |
+
106,
|
| 555 |
+
121,
|
| 556 |
+
130,
|
| 557 |
+
134,
|
| 558 |
+
149,
|
| 559 |
+
151,
|
| 560 |
+
160,
|
| 561 |
+
161,
|
| 562 |
+
166,
|
| 563 |
+
169,
|
| 564 |
+
174,
|
| 565 |
+
187,
|
| 566 |
+
188,
|
| 567 |
+
189,
|
| 568 |
+
190,
|
| 569 |
+
191,
|
| 570 |
+
201,
|
| 571 |
+
205,
|
| 572 |
+
214,
|
| 573 |
+
217,
|
| 574 |
+
235,
|
| 575 |
+
241,
|
| 576 |
+
243,
|
| 577 |
+
251,
|
| 578 |
+
252,
|
| 579 |
+
257,
|
| 580 |
+
259,
|
| 581 |
+
263,
|
| 582 |
+
264,
|
| 583 |
+
269,
|
| 584 |
+
270,
|
| 585 |
+
273,
|
| 586 |
+
276,
|
| 587 |
+
293,
|
| 588 |
+
303,
|
| 589 |
+
306,
|
| 590 |
+
308,
|
| 591 |
+
309,
|
| 592 |
+
313,
|
| 593 |
+
315,
|
| 594 |
+
319,
|
| 595 |
+
328,
|
| 596 |
+
330,
|
| 597 |
+
339,
|
| 598 |
+
343,
|
| 599 |
+
344,
|
| 600 |
+
345,
|
| 601 |
+
350,
|
| 602 |
+
359,
|
| 603 |
+
363,
|
| 604 |
+
366,
|
| 605 |
+
372,
|
| 606 |
+
385,
|
| 607 |
+
387,
|
| 608 |
+
389,
|
| 609 |
+
401,
|
| 610 |
+
413,
|
| 611 |
+
419,
|
| 612 |
+
427,
|
| 613 |
+
430,
|
| 614 |
+
435,
|
| 615 |
+
443,
|
| 616 |
+
445,
|
| 617 |
+
455,
|
| 618 |
+
458,
|
| 619 |
+
459,
|
| 620 |
+
463,
|
| 621 |
+
466,
|
| 622 |
+
470,
|
| 623 |
+
471,
|
| 624 |
+
472,
|
| 625 |
+
474,
|
| 626 |
+
475,
|
| 627 |
+
488,
|
| 628 |
+
490,
|
| 629 |
+
491,
|
| 630 |
+
498,
|
| 631 |
+
500,
|
| 632 |
+
506,
|
| 633 |
+
510,
|
| 634 |
+
514,
|
| 635 |
+
516
|
| 636 |
+
]
|
| 637 |
+
},
|
| 638 |
+
"git_commit": "cf46f28c2a9df775367b428eedd3efd5a2f68399",
|
| 639 |
+
"n_folds": 5,
|
| 640 |
+
"seed": 42,
|
| 641 |
+
"train_dev_seq_ids": [
|
| 642 |
+
0,
|
| 643 |
+
2,
|
| 644 |
+
3,
|
| 645 |
+
4,
|
| 646 |
+
5,
|
| 647 |
+
6,
|
| 648 |
+
7,
|
| 649 |
+
8,
|
| 650 |
+
9,
|
| 651 |
+
10,
|
| 652 |
+
11,
|
| 653 |
+
12,
|
| 654 |
+
14,
|
| 655 |
+
15,
|
| 656 |
+
16,
|
| 657 |
+
17,
|
| 658 |
+
18,
|
| 659 |
+
19,
|
| 660 |
+
22,
|
| 661 |
+
23,
|
| 662 |
+
24,
|
| 663 |
+
25,
|
| 664 |
+
26,
|
| 665 |
+
27,
|
| 666 |
+
28,
|
| 667 |
+
29,
|
| 668 |
+
30,
|
| 669 |
+
31,
|
| 670 |
+
32,
|
| 671 |
+
33,
|
| 672 |
+
35,
|
| 673 |
+
36,
|
| 674 |
+
37,
|
| 675 |
+
38,
|
| 676 |
+
39,
|
| 677 |
+
40,
|
| 678 |
+
41,
|
| 679 |
+
42,
|
| 680 |
+
44,
|
| 681 |
+
45,
|
| 682 |
+
46,
|
| 683 |
+
47,
|
| 684 |
+
51,
|
| 685 |
+
55,
|
| 686 |
+
56,
|
| 687 |
+
57,
|
| 688 |
+
59,
|
| 689 |
+
60,
|
| 690 |
+
61,
|
| 691 |
+
62,
|
| 692 |
+
63,
|
| 693 |
+
64,
|
| 694 |
+
65,
|
| 695 |
+
66,
|
| 696 |
+
67,
|
| 697 |
+
68,
|
| 698 |
+
69,
|
| 699 |
+
70,
|
| 700 |
+
72,
|
| 701 |
+
73,
|
| 702 |
+
74,
|
| 703 |
+
75,
|
| 704 |
+
76,
|
| 705 |
+
77,
|
| 706 |
+
78,
|
| 707 |
+
79,
|
| 708 |
+
81,
|
| 709 |
+
82,
|
| 710 |
+
83,
|
| 711 |
+
84,
|
| 712 |
+
85,
|
| 713 |
+
86,
|
| 714 |
+
89,
|
| 715 |
+
90,
|
| 716 |
+
92,
|
| 717 |
+
93,
|
| 718 |
+
94,
|
| 719 |
+
95,
|
| 720 |
+
96,
|
| 721 |
+
97,
|
| 722 |
+
98,
|
| 723 |
+
100,
|
| 724 |
+
101,
|
| 725 |
+
103,
|
| 726 |
+
104,
|
| 727 |
+
107,
|
| 728 |
+
108,
|
| 729 |
+
109,
|
| 730 |
+
110,
|
| 731 |
+
111,
|
| 732 |
+
112,
|
| 733 |
+
113,
|
| 734 |
+
114,
|
| 735 |
+
115,
|
| 736 |
+
116,
|
| 737 |
+
117,
|
| 738 |
+
118,
|
| 739 |
+
119,
|
| 740 |
+
120,
|
| 741 |
+
122,
|
| 742 |
+
123,
|
| 743 |
+
124,
|
| 744 |
+
125,
|
| 745 |
+
126,
|
| 746 |
+
127,
|
| 747 |
+
128,
|
| 748 |
+
129,
|
| 749 |
+
131,
|
| 750 |
+
132,
|
| 751 |
+
133,
|
| 752 |
+
135,
|
| 753 |
+
136,
|
| 754 |
+
137,
|
| 755 |
+
138,
|
| 756 |
+
139,
|
| 757 |
+
140,
|
| 758 |
+
141,
|
| 759 |
+
142,
|
| 760 |
+
143,
|
| 761 |
+
144,
|
| 762 |
+
145,
|
| 763 |
+
146,
|
| 764 |
+
147,
|
| 765 |
+
148,
|
| 766 |
+
150,
|
| 767 |
+
152,
|
| 768 |
+
153,
|
| 769 |
+
154,
|
| 770 |
+
155,
|
| 771 |
+
156,
|
| 772 |
+
157,
|
| 773 |
+
158,
|
| 774 |
+
159,
|
| 775 |
+
162,
|
| 776 |
+
163,
|
| 777 |
+
164,
|
| 778 |
+
165,
|
| 779 |
+
167,
|
| 780 |
+
168,
|
| 781 |
+
170,
|
| 782 |
+
171,
|
| 783 |
+
172,
|
| 784 |
+
173,
|
| 785 |
+
175,
|
| 786 |
+
176,
|
| 787 |
+
177,
|
| 788 |
+
178,
|
| 789 |
+
179,
|
| 790 |
+
180,
|
| 791 |
+
181,
|
| 792 |
+
182,
|
| 793 |
+
183,
|
| 794 |
+
184,
|
| 795 |
+
185,
|
| 796 |
+
186,
|
| 797 |
+
192,
|
| 798 |
+
193,
|
| 799 |
+
194,
|
| 800 |
+
195,
|
| 801 |
+
196,
|
| 802 |
+
197,
|
| 803 |
+
198,
|
| 804 |
+
199,
|
| 805 |
+
200,
|
| 806 |
+
202,
|
| 807 |
+
203,
|
| 808 |
+
204,
|
| 809 |
+
206,
|
| 810 |
+
207,
|
| 811 |
+
208,
|
| 812 |
+
209,
|
| 813 |
+
210,
|
| 814 |
+
211,
|
| 815 |
+
212,
|
| 816 |
+
213,
|
| 817 |
+
215,
|
| 818 |
+
216,
|
| 819 |
+
218,
|
| 820 |
+
219,
|
| 821 |
+
220,
|
| 822 |
+
221,
|
| 823 |
+
222,
|
| 824 |
+
223,
|
| 825 |
+
224,
|
| 826 |
+
225,
|
| 827 |
+
226,
|
| 828 |
+
227,
|
| 829 |
+
228,
|
| 830 |
+
229,
|
| 831 |
+
230,
|
| 832 |
+
231,
|
| 833 |
+
232,
|
| 834 |
+
233,
|
| 835 |
+
234,
|
| 836 |
+
236,
|
| 837 |
+
237,
|
| 838 |
+
238,
|
| 839 |
+
239,
|
| 840 |
+
240,
|
| 841 |
+
242,
|
| 842 |
+
244,
|
| 843 |
+
245,
|
| 844 |
+
246,
|
| 845 |
+
247,
|
| 846 |
+
248,
|
| 847 |
+
249,
|
| 848 |
+
250,
|
| 849 |
+
253,
|
| 850 |
+
254,
|
| 851 |
+
255,
|
| 852 |
+
256,
|
| 853 |
+
258,
|
| 854 |
+
260,
|
| 855 |
+
261,
|
| 856 |
+
262,
|
| 857 |
+
265,
|
| 858 |
+
266,
|
| 859 |
+
267,
|
| 860 |
+
268,
|
| 861 |
+
271,
|
| 862 |
+
272,
|
| 863 |
+
274,
|
| 864 |
+
275,
|
| 865 |
+
277,
|
| 866 |
+
278,
|
| 867 |
+
279,
|
| 868 |
+
280,
|
| 869 |
+
281,
|
| 870 |
+
282,
|
| 871 |
+
283,
|
| 872 |
+
284,
|
| 873 |
+
285,
|
| 874 |
+
286,
|
| 875 |
+
287,
|
| 876 |
+
288,
|
| 877 |
+
289,
|
| 878 |
+
290,
|
| 879 |
+
291,
|
| 880 |
+
292,
|
| 881 |
+
294,
|
| 882 |
+
295,
|
| 883 |
+
296,
|
| 884 |
+
297,
|
| 885 |
+
298,
|
| 886 |
+
299,
|
| 887 |
+
300,
|
| 888 |
+
301,
|
| 889 |
+
302,
|
| 890 |
+
304,
|
| 891 |
+
305,
|
| 892 |
+
307,
|
| 893 |
+
310,
|
| 894 |
+
311,
|
| 895 |
+
312,
|
| 896 |
+
314,
|
| 897 |
+
316,
|
| 898 |
+
317,
|
| 899 |
+
318,
|
| 900 |
+
320,
|
| 901 |
+
321,
|
| 902 |
+
322,
|
| 903 |
+
323,
|
| 904 |
+
324,
|
| 905 |
+
325,
|
| 906 |
+
326,
|
| 907 |
+
327,
|
| 908 |
+
329,
|
| 909 |
+
331,
|
| 910 |
+
332,
|
| 911 |
+
333,
|
| 912 |
+
334,
|
| 913 |
+
335,
|
| 914 |
+
336,
|
| 915 |
+
337,
|
| 916 |
+
338,
|
| 917 |
+
340,
|
| 918 |
+
341,
|
| 919 |
+
342,
|
| 920 |
+
346,
|
| 921 |
+
347,
|
| 922 |
+
348,
|
| 923 |
+
349,
|
| 924 |
+
351,
|
| 925 |
+
352,
|
| 926 |
+
353,
|
| 927 |
+
354,
|
| 928 |
+
355,
|
| 929 |
+
356,
|
| 930 |
+
357,
|
| 931 |
+
358,
|
| 932 |
+
360,
|
| 933 |
+
361,
|
| 934 |
+
362,
|
| 935 |
+
364,
|
| 936 |
+
365,
|
| 937 |
+
367,
|
| 938 |
+
368,
|
| 939 |
+
369,
|
| 940 |
+
370,
|
| 941 |
+
371,
|
| 942 |
+
373,
|
| 943 |
+
374,
|
| 944 |
+
375,
|
| 945 |
+
376,
|
| 946 |
+
377,
|
| 947 |
+
378,
|
| 948 |
+
379,
|
| 949 |
+
380,
|
| 950 |
+
381,
|
| 951 |
+
382,
|
| 952 |
+
383,
|
| 953 |
+
384,
|
| 954 |
+
386,
|
| 955 |
+
388,
|
| 956 |
+
390,
|
| 957 |
+
391,
|
| 958 |
+
392,
|
| 959 |
+
393,
|
| 960 |
+
394,
|
| 961 |
+
395,
|
| 962 |
+
396,
|
| 963 |
+
397,
|
| 964 |
+
398,
|
| 965 |
+
399,
|
| 966 |
+
400,
|
| 967 |
+
402,
|
| 968 |
+
403,
|
| 969 |
+
404,
|
| 970 |
+
405,
|
| 971 |
+
406,
|
| 972 |
+
407,
|
| 973 |
+
408,
|
| 974 |
+
409,
|
| 975 |
+
410,
|
| 976 |
+
411,
|
| 977 |
+
412,
|
| 978 |
+
414,
|
| 979 |
+
415,
|
| 980 |
+
416,
|
| 981 |
+
417,
|
| 982 |
+
418,
|
| 983 |
+
420,
|
| 984 |
+
421,
|
| 985 |
+
422,
|
| 986 |
+
423,
|
| 987 |
+
424,
|
| 988 |
+
425,
|
| 989 |
+
426,
|
| 990 |
+
428,
|
| 991 |
+
429,
|
| 992 |
+
431,
|
| 993 |
+
432,
|
| 994 |
+
433,
|
| 995 |
+
434,
|
| 996 |
+
436,
|
| 997 |
+
437,
|
| 998 |
+
438,
|
| 999 |
+
439,
|
| 1000 |
+
440,
|
| 1001 |
+
441,
|
| 1002 |
+
442,
|
| 1003 |
+
444,
|
| 1004 |
+
446,
|
| 1005 |
+
447,
|
| 1006 |
+
448,
|
| 1007 |
+
449,
|
| 1008 |
+
450,
|
| 1009 |
+
451,
|
| 1010 |
+
452,
|
| 1011 |
+
453,
|
| 1012 |
+
454,
|
| 1013 |
+
456,
|
| 1014 |
+
457,
|
| 1015 |
+
460,
|
| 1016 |
+
461,
|
| 1017 |
+
462,
|
| 1018 |
+
464,
|
| 1019 |
+
465,
|
| 1020 |
+
467,
|
| 1021 |
+
468,
|
| 1022 |
+
469,
|
| 1023 |
+
473,
|
| 1024 |
+
476,
|
| 1025 |
+
477,
|
| 1026 |
+
478,
|
| 1027 |
+
479,
|
| 1028 |
+
480,
|
| 1029 |
+
481,
|
| 1030 |
+
482,
|
| 1031 |
+
483,
|
| 1032 |
+
484,
|
| 1033 |
+
485,
|
| 1034 |
+
486,
|
| 1035 |
+
487,
|
| 1036 |
+
489,
|
| 1037 |
+
492,
|
| 1038 |
+
493,
|
| 1039 |
+
494,
|
| 1040 |
+
495,
|
| 1041 |
+
496,
|
| 1042 |
+
497,
|
| 1043 |
+
499,
|
| 1044 |
+
501,
|
| 1045 |
+
502,
|
| 1046 |
+
503,
|
| 1047 |
+
504,
|
| 1048 |
+
505,
|
| 1049 |
+
507,
|
| 1050 |
+
508,
|
| 1051 |
+
509,
|
| 1052 |
+
511,
|
| 1053 |
+
512,
|
| 1054 |
+
513,
|
| 1055 |
+
515
|
| 1056 |
+
]
|
| 1057 |
+
}
|
reports/phase2_technical_report.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Wunder Fund RNN Challenge — Technical Report
|
| 2 |
+
|
| 3 |
+
**Task.** Predict the next 32-dimensional market state from a sequence of prior
|
| 4 |
+
states, scored online (`PredictionModel.predict` one row at a time) by mean
|
| 5 |
+
per-feature R². Inference budget: 1 CPU core, 16 GB RAM, no GPU, 60 minutes,
|
| 6 |
+
deterministic. The competition is finished; this is a portfolio reconstruction
|
| 7 |
+
whose target is the winner's `0.3964` R².
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. Executive summary
|
| 12 |
+
|
| 13 |
+
The solution is a **causal unidirectional GRU** that predicts the next state at
|
| 14 |
+
every step, carrying its hidden state across the sequence so each online
|
| 15 |
+
prediction is O(1). It is trained on full 1000-step sequences with
|
| 16 |
+
back-prop-through-time and a loss masked to the scored steps (current steps
|
| 17 |
+
100–998 → targets 101–999). A multi-seed / multi-architecture ensemble is
|
| 18 |
+
averaged per feature.
|
| 19 |
+
|
| 20 |
+
Headline result (leaderboard-comparable, 5-fold grouped CV by `seq_ix`):
|
| 21 |
+
|
| 22 |
+
| Model | Dev CV mean R² | Notes |
|
| 23 |
+
| --- | ---: | --- |
|
| 24 |
+
| Persistence (predict current) | −0.37 | sanity baseline |
|
| 25 |
+
| EWMA α=0.10 | 0.217 | best stateful linear baseline |
|
| 26 |
+
| Ridge causal (640 feats) | 0.327 | at the linear ceiling |
|
| 27 |
+
| **Single causal GRU (d256)** | **0.389** | per-fold 0.391 / 0.394 / 0.407 / 0.363 |
|
| 28 |
+
| **GRU ensemble (multi-seed/arch)** | _finalized below_ | target ≥ 0.396 |
|
| 29 |
+
|
| 30 |
+
The single GRU already lands in the competition's top-10 band (0.386–0.392).
|
| 31 |
+
Inference costs ≈ **0.7 ms/prediction** single-threaded, ~1 min for a full
|
| 32 |
+
holdout, far inside the 60-minute budget.
|
| 33 |
+
|
| 34 |
+
---
|
| 35 |
+
|
| 36 |
+
## 2. Data analysis (what the data dictated)
|
| 37 |
+
|
| 38 |
+
Measured with `scripts/analyze_data.py` on `data/raw/train.parquet`
|
| 39 |
+
(517 sequences × 1000 steps × 32 features, columns named `"0"`…`"31"`):
|
| 40 |
+
|
| 41 |
+
- **Pre-whitened features.** Global mean ≈ 0, std ≈ 1, per-feature std ∈
|
| 42 |
+
[0.985, 1.011], excess kurtosis ≈ 0, hard-clipped at ±5.199. A
|
| 43 |
+
Gaussianizing/quantile transform was applied → no heavy tails, MSE is a
|
| 44 |
+
well-behaved objective, and no bespoke normalization is needed.
|
| 45 |
+
- **Mean-reverting, not a random walk.** Persistence R² ≈ −0.37 and
|
| 46 |
+
`var(next−current)/var(next) = 1.39` (differencing makes it *worse*), so the
|
| 47 |
+
model predicts the **level**, not a delta. Lag-1 autocorrelation ≈ 0.14.
|
| 48 |
+
- **The linear ceiling is ≈ 0.32.** VAR(p) Ridge saturates: p=1→0.23, p=2→0.28,
|
| 49 |
+
p=4→0.30, p=8→0.31, p=16→0.32. The Ridge causal baseline (0.327) is already
|
| 50 |
+
at this ceiling.
|
| 51 |
+
- **Nonlinearity is the lever (~+0.03).** Histogram gradient boosting on VAR(8)
|
| 52 |
+
features beats Ridge on every feature by +0.015…+0.055, with the largest
|
| 53 |
+
gains on the linearly-hardest features.
|
| 54 |
+
|
| 55 |
+
Conclusion: linear models top out near 0.32; the ~0.07 gap to the winner is
|
| 56 |
+
**nonlinear temporal structure**, which a sequence model is built to capture.
|
| 57 |
+
This is why the primary model is a GRU rather than more feature engineering.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## 3. Architecture
|
| 62 |
+
|
| 63 |
+
`src/models/sequence_models.py::CausalGRUForecaster`
|
| 64 |
+
|
| 65 |
+
```
|
| 66 |
+
state_t (32) ──► Linear(32→d) ─► LayerNorm ─► GRU(d, n_layers, unidirectional)
|
| 67 |
+
│ h_t
|
| 68 |
+
▼
|
| 69 |
+
LayerNorm ─► Linear(d→d) ─► GELU ─► Dropout ─► Linear(d→32) ─► pred of state_{t+1}
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
- **Unidirectional & causal** — uses only past/current states, matching the
|
| 73 |
+
online task. A bidirectional model would leak the future and is invalid here.
|
| 74 |
+
- **Stateful O(1) inference** — `forward(x, h0)` and a one-step `step(state, h)`
|
| 75 |
+
share the same recurrence, so the batched training forward and the row-by-row
|
| 76 |
+
online replay are numerically identical (verified to the digit and in
|
| 77 |
+
`tests/test_sequence_model.py`).
|
| 78 |
+
- Default size d_model=256, 2 layers (~0.87 M params); diversity members use
|
| 79 |
+
d_model=384 and a 3-layer variant.
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## 4. Training strategy
|
| 84 |
+
|
| 85 |
+
`src/training/sequence_trainer.py`
|
| 86 |
+
|
| 87 |
+
- **Full-sequence BPTT.** Each 1000-step sequence is processed end-to-end; the
|
| 88 |
+
loss is a **masked MSE** over the scored steps only (`need_prediction`,
|
| 89 |
+
i.e. current steps 100–998 against the next state). Because targets are
|
| 90 |
+
unit-variance, MSE ≈ (1 − R²), so the objective directly tracks the metric.
|
| 91 |
+
- **Optimizer/schedule.** AdamW (lr 1.5e-3, weight decay 1e-4), OneCycle cosine
|
| 92 |
+
schedule with 10% warmup, gradient clipping at 1.0.
|
| 93 |
+
- **Early stopping** on batched validation R² (identical to the stepwise score),
|
| 94 |
+
patience 14, typically converging by epoch 55–65.
|
| 95 |
+
- **Reproducibility.** Global seeds for Python/NumPy/PyTorch; deterministic
|
| 96 |
+
algorithms enabled; folds are fixed in `config/folds.json`.
|
| 97 |
+
- **Hardware.** Trained on an NVIDIA RTX 3050 Laptop (4 GB) at ~1.8 s/epoch
|
| 98 |
+
(~2 min/run); a CPU-only path (Ryzen 7 5800H, ~24 s/epoch) reproduces the
|
| 99 |
+
same scores within ±0.0003. Submission inference is CPU-only by design.
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## 5. Validation protocol
|
| 104 |
+
|
| 105 |
+
`src/evaluation/stepwise.py` + `config/folds.json`
|
| 106 |
+
|
| 107 |
+
- **Sequence-grouped** 5-fold split by `seq_ix` (seed 42); fold 4 is an
|
| 108 |
+
untouched final holdout, folds 0–3 are development. No row from a sequence
|
| 109 |
+
ever appears in both train and validation.
|
| 110 |
+
- The **stepwise scorer** replays `PredictionModel.predict` row by row and never
|
| 111 |
+
scores across `seq_ix` boundaries, exactly mirroring the official scorer.
|
| 112 |
+
- The **headline metric is the dev CV mean** across folds 0–3, not a single
|
| 113 |
+
fold: individual 103-sequence folds are high-variance (single-GRU range
|
| 114 |
+
0.363–0.407), whereas the hidden test (~517 sequences) averages over regimes.
|
| 115 |
+
Fold 4 is reported separately as a conservative point estimate (it is the
|
| 116 |
+
hardest fold; every dev fold scores higher).
|
| 117 |
+
|
| 118 |
+
---
|
| 119 |
+
|
| 120 |
+
## 6. Results
|
| 121 |
+
|
| 122 |
+
Cross-validated R² (sequence-grouped folds 0–3), simple-average ensembles:
|
| 123 |
+
|
| 124 |
+
| Ensemble | Members | CV mean R² |
|
| 125 |
+
| --- | ---: | ---: |
|
| 126 |
+
| Single GRU d256 (avg of 3 seeds) | 1 | 0.389 |
|
| 127 |
+
| 3× GRU d256 | 3 | 0.3915 |
|
| 128 |
+
| **3× GRU d256 + 2× GRU d384 (packaged)** | **5** | **0.3922** |
|
| 129 |
+
| + 2× LSTM | 7 | 0.3925 |
|
| 130 |
+
|
| 131 |
+
- Per-feature non-negative stacking matched simple averaging (0.3921 vs 0.3922)
|
| 132 |
+
— expected for a same-family ensemble, so the packaged model averages equally.
|
| 133 |
+
- **Untouched fold-4 holdout** of the 5-member ensemble (fold models that never
|
| 134 |
+
saw fold 4): **0.3498** — the conservative single-fold figure; fold 4 is the
|
| 135 |
+
hardest fold (all dev folds score higher), which is why the CV mean is the
|
| 136 |
+
headline.
|
| 137 |
+
- **Single-core inference:** 4.17 ms/prediction for the 5-member ensemble →
|
| 138 |
+
≈ **32 minutes** for the full ~517-sequence test on one CPU core (limit: 60).
|
| 139 |
+
- **Deterministic:** identical predictions across repeated runs (max abs diff 0).
|
| 140 |
+
|
| 141 |
+
For reference, the competition's public leaderboard #1 was 0.3920 and the finals
|
| 142 |
+
winner 0.3964; the cross-validated 0.3922 matches the public top.
|
| 143 |
+
|
| 144 |
+
### Key boosters (most score per unit effort)
|
| 145 |
+
1. **Causal GRU over linear/tree models** — moved 0.327 → 0.389 CV (+0.062), the
|
| 146 |
+
single biggest jump; it captures the nonlinear temporal structure linear
|
| 147 |
+
models cannot.
|
| 148 |
+
2. **Predicting the level on whitened inputs** — differencing or persistence is
|
| 149 |
+
actively harmful here (negative R²); letting the GRU learn the mean-reverting
|
| 150 |
+
map directly is the correct framing.
|
| 151 |
+
3. **Full-sequence stateful modeling** — carrying hidden state from step 0
|
| 152 |
+
matches train and inference exactly and makes online inference O(1).
|
| 153 |
+
4. **Multi-seed + width-diversity ensembling** — +0.003 over a single GRU
|
| 154 |
+
(0.389 → 0.3922); the largest ensemble gains came from combining d256 and
|
| 155 |
+
d384 seeds rather than adding more of the same.
|
| 156 |
+
|
| 157 |
+
## 7. What didn't work / didn't help
|
| 158 |
+
- **More training data had little effect** (310 → 352 sequences: +0.003 on the
|
| 159 |
+
holdout). The dev→holdout gap is fold *difficulty*, not data quantity.
|
| 160 |
+
- **Bagging same-config fold models** barely moved the holdout — correlated
|
| 161 |
+
members; diversity (seeds/architectures) is what helps.
|
| 162 |
+
- **Heavy feature engineering / GBDT as the primary model** — capped near the
|
| 163 |
+
linear-plus-small-nonlinear ceiling (~0.34); kept only as an optional
|
| 164 |
+
diversity member, not the backbone.
|
| 165 |
+
- **Differencing / momentum targets** — worse than predicting the level.
|
| 166 |
+
- **Causal TCN (dilated convolutions)** — trained as a decorrelated member but
|
| 167 |
+
scored lower (≈0.37–0.39, fold-dependent) and was far slower at inference (a
|
| 168 |
+
~250-step receptive field re-run each step). Doubly disqualifying, dropped.
|
| 169 |
+
(A real bug was caught here first: a time-mixing GroupNorm leaked the future;
|
| 170 |
+
fixed with a per-position LayerNorm — see `tests`/the equivalence check.)
|
| 171 |
+
- **GBDT (HistGradientBoosting) as a stacking member** — strongly decorrelated
|
| 172 |
+
(≈0.37) but added only ~+0.0002 to the ensemble while being slow to train OOF;
|
| 173 |
+
not worth the inference complexity. Kept the ensemble pure-GRU.
|
| 174 |
+
- **Adding more ensemble members past 5** — LSTM seeds lifted CV by only +0.0003
|
| 175 |
+
while increasing single-core inference cost; capped at 5 for budget headroom.
|
| 176 |
+
- The earlier repo's "CMDMamba ensemble" was aspirational (no runnable
|
| 177 |
+
artifacts) and based on an unverifiable reference; it was discarded in favor
|
| 178 |
+
of a model justified by the measured data structure.
|
| 179 |
+
|
| 180 |
+
## 8. Reproduction
|
| 181 |
+
|
| 182 |
+
```bash
|
| 183 |
+
# environment (CPU submission-faithful)
|
| 184 |
+
py -3.11 -m venv .venv && .venv/Scripts/python -m pip install -r requirements.txt
|
| 185 |
+
# optional GPU training env
|
| 186 |
+
py -3.11 -m venv .venv-gpu && .venv-gpu/Scripts/python -m pip install -r requirements-train-gpu.txt
|
| 187 |
+
|
| 188 |
+
python scripts/analyze_data.py # data diagnostics
|
| 189 |
+
python scripts/create_folds.py --output config/folds.json
|
| 190 |
+
python scripts/train_sequence.py --run-name gru --dev-fold 0 --device cuda # one fold
|
| 191 |
+
python scripts/cv_summary.py --runs <run...> # ensemble CV mean
|
| 192 |
+
python scripts/evaluate_ensemble.py --split holdout --gru <ckpt...> # holdout
|
| 193 |
+
python scripts/create_submission.py --output submissions/submission.zip
|
| 194 |
+
python -m pytest tests -q
|
| 195 |
+
```
|
requirements.txt
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Wunder Fund RNN Challenge - Dependencies
|
| 2 |
+
|
| 3 |
+
# Core
|
| 4 |
+
numpy==1.26.4
|
| 5 |
+
pandas==2.2.3
|
| 6 |
+
pyarrow==18.1.0
|
| 7 |
+
scikit-learn==1.5.2
|
| 8 |
+
scipy==1.14.1
|
| 9 |
+
joblib==1.4.2
|
| 10 |
+
tqdm==4.66.5
|
| 11 |
+
|
| 12 |
+
# Deep learning (CPU)
|
| 13 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 14 |
+
torch==2.9.1
|
| 15 |
+
einops==0.8.0
|
| 16 |
+
|
| 17 |
+
# Tree-based models
|
| 18 |
+
lightgbm==4.3.0
|
| 19 |
+
xgboost==2.1.2
|
| 20 |
+
|
| 21 |
+
# ONNX optimization
|
| 22 |
+
onnx==1.17.0
|
| 23 |
+
onnxruntime==1.20.1
|
| 24 |
+
|
| 25 |
+
# Configuration
|
| 26 |
+
pyyaml==6.0.2
|
| 27 |
+
|
| 28 |
+
# Experiment tracking (optional)
|
| 29 |
+
wandb==0.18.7
|
| 30 |
+
|
| 31 |
+
# Hyperparameter optimization (optional)
|
| 32 |
+
optuna==4.1.0
|
| 33 |
+
|
| 34 |
+
# Testing
|
| 35 |
+
pytest==8.3.4
|
| 36 |
+
pytest-cov==6.0.0
|
solution.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""solution.py — Competition submission for the Wunder Fund RNN Challenge.
|
| 2 |
+
|
| 3 |
+
Root entry point for the scorer. Defines PredictionModel with a predict()
|
| 4 |
+
method that receives DataPoint objects one at a time and predicts the *next*
|
| 5 |
+
state when need_prediction is True.
|
| 6 |
+
|
| 7 |
+
Primary backend: an ensemble of causal GRU forecasters (carrying hidden state
|
| 8 |
+
across steps for O(1)/prediction online inference), optionally blended per
|
| 9 |
+
feature with tabular models. Falls back to joblib tabular models, then a
|
| 10 |
+
momentum baseline (only when ALLOW_FALLBACK=1).
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
|
| 15 |
+
# Windows: torch and numpy/sklearn each ship an OpenMP runtime; importing torch
|
| 16 |
+
# before the MKL-backed stack avoids a c10.dll init failure. Also keep the
|
| 17 |
+
# submission single-core friendly by default (the scorer grants 1 CPU).
|
| 18 |
+
os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
|
| 19 |
+
_THREADS = os.environ.get("WUNDER_THREADS", "1")
|
| 20 |
+
os.environ.setdefault("OMP_NUM_THREADS", _THREADS)
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
import torch
|
| 24 |
+
torch.set_num_threads(max(1, int(_THREADS)))
|
| 25 |
+
_HAS_TORCH = True
|
| 26 |
+
except Exception:
|
| 27 |
+
_HAS_TORCH = False
|
| 28 |
+
|
| 29 |
+
import json
|
| 30 |
+
from pathlib import Path
|
| 31 |
+
from typing import Optional
|
| 32 |
+
|
| 33 |
+
import numpy as np
|
| 34 |
+
|
| 35 |
+
_ROOT = Path(__file__).resolve().parent
|
| 36 |
+
import sys
|
| 37 |
+
if str(_ROOT) not in sys.path:
|
| 38 |
+
sys.path.insert(0, str(_ROOT))
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class DataPoint:
|
| 42 |
+
"""Mirror of competition_package.utils.DataPoint for standalone use."""
|
| 43 |
+
|
| 44 |
+
def __init__(self, seq_ix, step_in_seq, need_prediction, state):
|
| 45 |
+
self.seq_ix = seq_ix
|
| 46 |
+
self.step_in_seq = step_in_seq
|
| 47 |
+
self.need_prediction = need_prediction
|
| 48 |
+
self.state = state
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class PredictionModel:
|
| 52 |
+
"""Competition PredictionModel. Loads the best available backend."""
|
| 53 |
+
|
| 54 |
+
def __init__(self):
|
| 55 |
+
self.n_features = 32
|
| 56 |
+
self.backend = "fallback"
|
| 57 |
+
self.predictor = None # object exposing predict(data_point)
|
| 58 |
+
self._current_seq = None
|
| 59 |
+
self._prev_state = None
|
| 60 |
+
self._cur_state = None
|
| 61 |
+
|
| 62 |
+
self._try_load_gru()
|
| 63 |
+
if self.predictor is None:
|
| 64 |
+
self._try_load_onnx()
|
| 65 |
+
if self.predictor is None:
|
| 66 |
+
self._try_load_tabular()
|
| 67 |
+
if self.predictor is None:
|
| 68 |
+
self._try_load_pytorch()
|
| 69 |
+
if self.predictor is None and os.environ.get("ALLOW_FALLBACK") != "1":
|
| 70 |
+
raise RuntimeError(
|
| 71 |
+
"No trained model artifacts found. Set ALLOW_FALLBACK=1 only for "
|
| 72 |
+
"local smoke tests; real submissions must package models."
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# ------------------------------------------------------------------
|
| 76 |
+
# Backends
|
| 77 |
+
# ------------------------------------------------------------------
|
| 78 |
+
def _try_load_gru(self):
|
| 79 |
+
"""Primary: GRU ensemble (+ optional tabular members) under models/submission."""
|
| 80 |
+
if not _HAS_TORCH:
|
| 81 |
+
return
|
| 82 |
+
try:
|
| 83 |
+
from src.models.ensemble_predictor import EnsemblePredictionModel
|
| 84 |
+
from src.models.sequence_inference import (
|
| 85 |
+
GRUStatefulPredictionModel,
|
| 86 |
+
load_gru_checkpoint,
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
sub = _ROOT / "models" / "submission"
|
| 90 |
+
gru_paths = sorted((sub / "gru").glob("*.pt")) if (sub / "gru").exists() else []
|
| 91 |
+
if not gru_paths:
|
| 92 |
+
return
|
| 93 |
+
|
| 94 |
+
members, names = [], []
|
| 95 |
+
for p in gru_paths:
|
| 96 |
+
model = load_gru_checkpoint(str(p))
|
| 97 |
+
members.append(GRUStatefulPredictionModel(model))
|
| 98 |
+
names.append(p.stem)
|
| 99 |
+
|
| 100 |
+
# optional tabular members
|
| 101 |
+
tab_dir = sub / "tabular"
|
| 102 |
+
if tab_dir.exists():
|
| 103 |
+
import joblib
|
| 104 |
+
from src.models.baselines import TabularStatefulPredictionModel
|
| 105 |
+
for jb in sorted(tab_dir.glob("*/model.joblib")):
|
| 106 |
+
manifest = jb.parent / "model_manifest.json"
|
| 107 |
+
schema = "compact_v1"
|
| 108 |
+
if manifest.exists():
|
| 109 |
+
try:
|
| 110 |
+
schema = json.loads(manifest.read_text()).get("feature_schema", schema)
|
| 111 |
+
except Exception:
|
| 112 |
+
pass
|
| 113 |
+
members.append(TabularStatefulPredictionModel(joblib.load(str(jb)), feature_schema=schema))
|
| 114 |
+
names.append(jb.parent.name)
|
| 115 |
+
|
| 116 |
+
weights = None
|
| 117 |
+
wpath = sub / "blend_weights.json"
|
| 118 |
+
if wpath.exists():
|
| 119 |
+
try:
|
| 120 |
+
payload = json.loads(wpath.read_text())
|
| 121 |
+
w = np.asarray(payload["per_feature_weights"], dtype=np.float32)
|
| 122 |
+
if w.shape == (self.n_features, len(members)):
|
| 123 |
+
weights = w
|
| 124 |
+
except Exception:
|
| 125 |
+
weights = None
|
| 126 |
+
|
| 127 |
+
self.predictor = EnsemblePredictionModel(members, weights=weights)
|
| 128 |
+
self.backend = "gru_ensemble"
|
| 129 |
+
except Exception:
|
| 130 |
+
self.predictor = None
|
| 131 |
+
|
| 132 |
+
def _try_load_onnx(self):
|
| 133 |
+
"""ONNX ensemble (kept for completeness; not the primary path)."""
|
| 134 |
+
return
|
| 135 |
+
|
| 136 |
+
def _try_load_tabular(self):
|
| 137 |
+
"""Joblib tabular models discovered under models/ (legacy/baseline path)."""
|
| 138 |
+
try:
|
| 139 |
+
import joblib
|
| 140 |
+
from src.models.baselines import TabularStatefulPredictionModel
|
| 141 |
+
from src.models.ensemble_predictor import EnsemblePredictionModel
|
| 142 |
+
|
| 143 |
+
model_dir = _ROOT / "models"
|
| 144 |
+
joblib_files = sorted(model_dir.glob("phase2_tabular/**/model.joblib"))
|
| 145 |
+
if not joblib_files:
|
| 146 |
+
return
|
| 147 |
+
members = []
|
| 148 |
+
for path in joblib_files:
|
| 149 |
+
manifest = path.parent / "model_manifest.json"
|
| 150 |
+
schema = "compact_v1"
|
| 151 |
+
if manifest.exists():
|
| 152 |
+
try:
|
| 153 |
+
schema = json.loads(manifest.read_text()).get("feature_schema", schema)
|
| 154 |
+
except Exception:
|
| 155 |
+
pass
|
| 156 |
+
members.append(TabularStatefulPredictionModel(joblib.load(str(path)), feature_schema=schema))
|
| 157 |
+
if members:
|
| 158 |
+
self.predictor = EnsemblePredictionModel(members)
|
| 159 |
+
self.backend = "tabular"
|
| 160 |
+
except Exception:
|
| 161 |
+
self.predictor = None
|
| 162 |
+
|
| 163 |
+
def _try_load_pytorch(self):
|
| 164 |
+
"""Legacy PyTorch checkpoint path (kept for test compatibility)."""
|
| 165 |
+
return
|
| 166 |
+
|
| 167 |
+
# ------------------------------------------------------------------
|
| 168 |
+
# Predict
|
| 169 |
+
# ------------------------------------------------------------------
|
| 170 |
+
def predict(self, data_point) -> Optional[np.ndarray]:
|
| 171 |
+
if self.predictor is not None:
|
| 172 |
+
return self.predictor.predict(data_point)
|
| 173 |
+
return self._predict_fallback(data_point)
|
| 174 |
+
|
| 175 |
+
def _predict_fallback(self, data_point) -> Optional[np.ndarray]:
|
| 176 |
+
"""Momentum baseline; only reachable with ALLOW_FALLBACK=1."""
|
| 177 |
+
if self._current_seq != data_point.seq_ix:
|
| 178 |
+
self._current_seq = data_point.seq_ix
|
| 179 |
+
self._prev_state = None
|
| 180 |
+
self._cur_state = None
|
| 181 |
+
self._prev_state = self._cur_state
|
| 182 |
+
self._cur_state = np.asarray(data_point.state, dtype=np.float32)
|
| 183 |
+
if not data_point.need_prediction:
|
| 184 |
+
return None
|
| 185 |
+
if self._prev_state is None:
|
| 186 |
+
return self._cur_state.astype(np.float32, copy=True)
|
| 187 |
+
return (2.0 * self._cur_state - self._prev_state).astype(np.float32)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
if __name__ == "__main__":
|
| 191 |
+
model = PredictionModel()
|
| 192 |
+
print(f"Backend: {model.backend}")
|
| 193 |
+
for step in range(200):
|
| 194 |
+
dp = DataPoint(0, step, step >= 100, np.random.randn(32).astype(np.float32))
|
| 195 |
+
pred = model.predict(dp)
|
| 196 |
+
if pred is not None and step == 100:
|
| 197 |
+
print(f"step {step}: shape={pred.shape} dtype={pred.dtype} finite={np.all(np.isfinite(pred))}")
|
| 198 |
+
print("Local test passed!")
|
src/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Wunder Fund RNN Challenge - Source Package
|
src/data/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Data package exports (lazy loading for the Torch-dependent dataset)."""
|
| 2 |
+
|
| 3 |
+
import importlib
|
| 4 |
+
|
| 5 |
+
from .protocol import create_dataset_manifest, create_sequence_folds, resolve_dataset_path, validate_wunder_dataframe
|
| 6 |
+
from .causal_features import (
|
| 7 |
+
build_causal_tabular_features,
|
| 8 |
+
feature_schema_hash,
|
| 9 |
+
feature_schema_payload,
|
| 10 |
+
feature_width,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
'CausalStepDataset',
|
| 15 |
+
'create_causal_dataloaders',
|
| 16 |
+
'create_dataset_manifest',
|
| 17 |
+
'create_sequence_folds',
|
| 18 |
+
'resolve_dataset_path',
|
| 19 |
+
'validate_wunder_dataframe',
|
| 20 |
+
'build_causal_tabular_features',
|
| 21 |
+
'feature_schema_hash',
|
| 22 |
+
'feature_schema_payload',
|
| 23 |
+
'feature_width',
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def __getattr__(name):
|
| 28 |
+
if name in {'CausalStepDataset', 'create_causal_dataloaders'}:
|
| 29 |
+
return getattr(importlib.import_module(".dataset", __name__), name)
|
| 30 |
+
raise AttributeError(f"module 'src.data' has no attribute {name!r}")
|
src/data/causal_features.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Causal online feature builders shared by Phase 2 tabular baselines."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
import json
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
COMPACT_V1_WINDOWS = (3, 5, 10, 20, 50, 100)
|
| 12 |
+
EXPANDED_V2_LAGS = (1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610)
|
| 13 |
+
EXPANDED_V2_EWMA_ALPHAS = (0.02, 0.05, 0.10, 0.20, 0.40)
|
| 14 |
+
EXPANDED_V2_WINDOWS = (3, 5, 10, 20, 50, 100, 200, 500)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _validate_history(history: np.ndarray) -> np.ndarray:
|
| 18 |
+
history = np.asarray(history, dtype=np.float32)
|
| 19 |
+
if history.ndim != 2 or history.shape[1] != 32:
|
| 20 |
+
raise ValueError(f"Expected history shape (t, 32), got {history.shape}")
|
| 21 |
+
if len(history) == 0:
|
| 22 |
+
raise ValueError("history must contain at least one state")
|
| 23 |
+
return history
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _ewma(history: np.ndarray, alpha: float) -> np.ndarray:
|
| 27 |
+
state = history[0].astype(np.float32, copy=True)
|
| 28 |
+
for row in history[1:]:
|
| 29 |
+
state = alpha * row + (1.0 - alpha) * state
|
| 30 |
+
return state.astype(np.float32)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def build_compact_v1_features(history: np.ndarray) -> np.ndarray:
|
| 34 |
+
"""Build the original compact raw-32 causal feature vector."""
|
| 35 |
+
history = _validate_history(history)
|
| 36 |
+
current = history[-1]
|
| 37 |
+
prev = history[-2] if len(history) >= 2 else np.zeros_like(current)
|
| 38 |
+
delta1 = current - prev
|
| 39 |
+
|
| 40 |
+
features = [current, delta1]
|
| 41 |
+
for window in COMPACT_V1_WINDOWS:
|
| 42 |
+
chunk = history[-window:]
|
| 43 |
+
mean = chunk.mean(axis=0)
|
| 44 |
+
features.extend([mean, chunk.std(axis=0), current - mean])
|
| 45 |
+
return np.concatenate(features).astype(np.float32)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def build_expanded_v2_features(history: np.ndarray) -> np.ndarray:
|
| 49 |
+
"""Build the Phase 2 max-safe expanded causal feature vector."""
|
| 50 |
+
history = _validate_history(history)
|
| 51 |
+
current = history[-1]
|
| 52 |
+
features = [current]
|
| 53 |
+
|
| 54 |
+
for lag in EXPANDED_V2_LAGS:
|
| 55 |
+
lagged = history[-1 - lag] if len(history) > lag else history[0]
|
| 56 |
+
features.extend([lagged, current - lagged])
|
| 57 |
+
|
| 58 |
+
for alpha in EXPANDED_V2_EWMA_ALPHAS:
|
| 59 |
+
smoothed = _ewma(history, alpha)
|
| 60 |
+
features.extend([smoothed, current - smoothed])
|
| 61 |
+
|
| 62 |
+
for window in EXPANDED_V2_WINDOWS:
|
| 63 |
+
chunk = history[-window:]
|
| 64 |
+
mean = chunk.mean(axis=0)
|
| 65 |
+
features.extend(
|
| 66 |
+
[
|
| 67 |
+
mean,
|
| 68 |
+
chunk.std(axis=0),
|
| 69 |
+
chunk.min(axis=0),
|
| 70 |
+
chunk.max(axis=0),
|
| 71 |
+
current - mean,
|
| 72 |
+
]
|
| 73 |
+
)
|
| 74 |
+
return np.concatenate(features).astype(np.float32)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def build_causal_tabular_features(history: np.ndarray, schema: str = "compact_v1") -> np.ndarray:
|
| 78 |
+
if schema == "compact_v1":
|
| 79 |
+
return build_compact_v1_features(history)
|
| 80 |
+
if schema == "expanded_v2":
|
| 81 |
+
return build_expanded_v2_features(history)
|
| 82 |
+
raise ValueError(f"Unknown feature schema: {schema}")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def feature_width(schema: str) -> int:
|
| 86 |
+
return int(build_causal_tabular_features(np.zeros((1, 32), dtype=np.float32), schema).shape[0])
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def infer_feature_schema_from_width(width: int) -> str:
|
| 90 |
+
width = int(width)
|
| 91 |
+
for schema in ("compact_v1", "expanded_v2"):
|
| 92 |
+
if feature_width(schema) == width:
|
| 93 |
+
return schema
|
| 94 |
+
raise ValueError(f"No known causal feature schema has width {width}")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def feature_schema_payload(schema: str) -> dict:
|
| 98 |
+
if schema == "compact_v1":
|
| 99 |
+
return {
|
| 100 |
+
"schema": schema,
|
| 101 |
+
"windows": list(COMPACT_V1_WINDOWS),
|
| 102 |
+
"width": feature_width(schema),
|
| 103 |
+
}
|
| 104 |
+
if schema == "expanded_v2":
|
| 105 |
+
return {
|
| 106 |
+
"schema": schema,
|
| 107 |
+
"lags": list(EXPANDED_V2_LAGS),
|
| 108 |
+
"ewma_alphas": list(EXPANDED_V2_EWMA_ALPHAS),
|
| 109 |
+
"windows": list(EXPANDED_V2_WINDOWS),
|
| 110 |
+
"width": feature_width(schema),
|
| 111 |
+
}
|
| 112 |
+
raise ValueError(f"Unknown feature schema: {schema}")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def feature_schema_hash(schema: str) -> str:
|
| 116 |
+
payload = json.dumps(feature_schema_payload(schema), sort_keys=True).encode("utf-8")
|
| 117 |
+
return hashlib.sha256(payload).hexdigest()
|
src/data/dataset.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Causal step dataset for the Wunder Fund RNN Challenge.
|
| 3 |
+
|
| 4 |
+
Provides CausalStepDataset (left-padded lookback windows for the official
|
| 5 |
+
stepwise task) and a deterministic train/val loader factory. Targets are
|
| 6 |
+
aligned so that the example at step t predicts state t+1, and only scored steps
|
| 7 |
+
(need_prediction=True, with an available next state) are emitted.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
from torch.utils.data import Dataset, DataLoader
|
| 13 |
+
from typing import Optional, List, Tuple
|
| 14 |
+
|
| 15 |
+
from src.data.protocol import get_feature_columns, load_wunder_dataframe
|
| 16 |
+
from src.utils.reproducibility import make_torch_generator, seed_worker
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class CausalStepDataset(Dataset):
|
| 20 |
+
"""
|
| 21 |
+
Causal online dataset for the official stepwise task.
|
| 22 |
+
|
| 23 |
+
Each sample contains only states up to and including step t and predicts
|
| 24 |
+
state t+1. Samples are emitted only where need_prediction=True and t+1 is
|
| 25 |
+
available in the local parquet file. The hidden competition may request a
|
| 26 |
+
final step prediction whose target is unavailable locally; this dataset
|
| 27 |
+
deliberately excludes that row from training and local validation.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(
|
| 31 |
+
self,
|
| 32 |
+
parquet_path: str,
|
| 33 |
+
seq_ids: Optional[List[int]] = None,
|
| 34 |
+
lookback: int = 128,
|
| 35 |
+
):
|
| 36 |
+
self.lookback = int(lookback)
|
| 37 |
+
if self.lookback <= 0:
|
| 38 |
+
raise ValueError("lookback must be positive")
|
| 39 |
+
|
| 40 |
+
df = load_wunder_dataframe(parquet_path, seq_ids=seq_ids)
|
| 41 |
+
self.feature_cols = get_feature_columns(df)
|
| 42 |
+
self.n_features = len(self.feature_cols)
|
| 43 |
+
self.samples = []
|
| 44 |
+
|
| 45 |
+
for seq_ix, seq_df in df.groupby("seq_ix", sort=True):
|
| 46 |
+
states = seq_df[self.feature_cols].to_numpy(dtype=np.float32)
|
| 47 |
+
need_prediction = seq_df["need_prediction"].to_numpy(dtype=bool)
|
| 48 |
+
steps = seq_df["step_in_seq"].to_numpy(dtype=np.int64)
|
| 49 |
+
for pos, step in enumerate(steps):
|
| 50 |
+
if not need_prediction[pos]:
|
| 51 |
+
continue
|
| 52 |
+
target_pos = pos + 1
|
| 53 |
+
if target_pos >= len(states):
|
| 54 |
+
continue
|
| 55 |
+
self.samples.append(
|
| 56 |
+
{
|
| 57 |
+
"seq_ix": int(seq_ix),
|
| 58 |
+
"step_in_seq": int(step),
|
| 59 |
+
"states": states,
|
| 60 |
+
"position": int(pos),
|
| 61 |
+
"target": states[target_pos],
|
| 62 |
+
}
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
def __len__(self) -> int:
|
| 66 |
+
return len(self.samples)
|
| 67 |
+
|
| 68 |
+
def __getitem__(self, idx: int) -> dict:
|
| 69 |
+
sample = self.samples[idx]
|
| 70 |
+
states = sample["states"]
|
| 71 |
+
pos = sample["position"]
|
| 72 |
+
start = max(0, pos + 1 - self.lookback)
|
| 73 |
+
history = states[start : pos + 1]
|
| 74 |
+
|
| 75 |
+
window = np.zeros((self.lookback, self.n_features), dtype=np.float32)
|
| 76 |
+
mask = np.zeros((self.lookback,), dtype=np.float32)
|
| 77 |
+
window[-len(history) :] = history
|
| 78 |
+
mask[-len(history) :] = 1.0
|
| 79 |
+
|
| 80 |
+
return {
|
| 81 |
+
"seq_ix": torch.tensor(sample["seq_ix"], dtype=torch.long),
|
| 82 |
+
"step_in_seq": torch.tensor(sample["step_in_seq"], dtype=torch.long),
|
| 83 |
+
"history": torch.tensor(window, dtype=torch.float32),
|
| 84 |
+
"history_mask": torch.tensor(mask, dtype=torch.float32),
|
| 85 |
+
"target": torch.tensor(sample["target"].astype(np.float32, copy=False), dtype=torch.float32),
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def create_causal_dataloaders(
|
| 90 |
+
parquet_path: str,
|
| 91 |
+
train_seq_ids: list[int],
|
| 92 |
+
val_seq_ids: list[int],
|
| 93 |
+
*,
|
| 94 |
+
lookback: int = 128,
|
| 95 |
+
batch_size: int = 256,
|
| 96 |
+
num_workers: int = 0,
|
| 97 |
+
seed: int = 42,
|
| 98 |
+
) -> Tuple[DataLoader, DataLoader]:
|
| 99 |
+
"""Create deterministic train/validation loaders for causal step examples."""
|
| 100 |
+
train_dataset = CausalStepDataset(parquet_path, seq_ids=train_seq_ids, lookback=lookback)
|
| 101 |
+
val_dataset = CausalStepDataset(parquet_path, seq_ids=val_seq_ids, lookback=lookback)
|
| 102 |
+
|
| 103 |
+
train_loader = DataLoader(
|
| 104 |
+
train_dataset,
|
| 105 |
+
batch_size=batch_size,
|
| 106 |
+
shuffle=True,
|
| 107 |
+
num_workers=num_workers,
|
| 108 |
+
pin_memory=False,
|
| 109 |
+
worker_init_fn=seed_worker,
|
| 110 |
+
generator=make_torch_generator(seed),
|
| 111 |
+
)
|
| 112 |
+
val_loader = DataLoader(
|
| 113 |
+
val_dataset,
|
| 114 |
+
batch_size=batch_size,
|
| 115 |
+
shuffle=False,
|
| 116 |
+
num_workers=num_workers,
|
| 117 |
+
pin_memory=False,
|
| 118 |
+
)
|
| 119 |
+
return train_loader, val_loader
|
src/data/protocol.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Official-protocol checks, dataset manifests, and locked sequence folds."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
import json
|
| 7 |
+
import subprocess
|
| 8 |
+
from dataclasses import asdict, dataclass
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Iterable, Optional
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
REQUIRED_META_COLUMNS = ["seq_ix", "step_in_seq", "need_prediction"]
|
| 17 |
+
EXPECTED_N_FEATURES = 32
|
| 18 |
+
EXPECTED_SEQUENCE_LENGTH = 1000
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass(frozen=True)
|
| 22 |
+
class DatasetManifest:
|
| 23 |
+
path: str
|
| 24 |
+
sha256: str
|
| 25 |
+
size_bytes: int
|
| 26 |
+
row_count: int
|
| 27 |
+
sequence_count: int
|
| 28 |
+
sequence_length: int
|
| 29 |
+
feature_columns: list[str]
|
| 30 |
+
need_prediction_counts_by_step: dict[str, int]
|
| 31 |
+
seq_ix_min: int
|
| 32 |
+
seq_ix_max: int
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass(frozen=True)
|
| 36 |
+
class FoldManifest:
|
| 37 |
+
seed: int
|
| 38 |
+
n_folds: int
|
| 39 |
+
final_holdout_fold: int
|
| 40 |
+
folds: dict[str, list[int]]
|
| 41 |
+
train_dev_seq_ids: list[int]
|
| 42 |
+
final_holdout_seq_ids: list[int]
|
| 43 |
+
dataset_sha256: Optional[str] = None
|
| 44 |
+
git_commit: Optional[str] = None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def resolve_dataset_path(preferred: str | Path = "data/raw/train.parquet") -> Path:
|
| 48 |
+
"""Find the canonical raw train parquet without mutating the workspace."""
|
| 49 |
+
candidates = [
|
| 50 |
+
Path(preferred),
|
| 51 |
+
Path("data/raw/train.parquet"),
|
| 52 |
+
Path("data/train.parquet"),
|
| 53 |
+
Path("competition_package/datasets/train.parquet"),
|
| 54 |
+
Path("wnn_starterpack/competition_package/datasets/train.parquet"),
|
| 55 |
+
Path("datasets/train.parquet"),
|
| 56 |
+
Path("train.parquet"),
|
| 57 |
+
]
|
| 58 |
+
for candidate in candidates:
|
| 59 |
+
if candidate.exists():
|
| 60 |
+
return candidate
|
| 61 |
+
raise FileNotFoundError(
|
| 62 |
+
"No train parquet found. Expected data/raw/train.parquet, "
|
| 63 |
+
"data/train.parquet, or competition_package/datasets/train.parquet."
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def sha256_file(path: str | Path, chunk_size: int = 1024 * 1024) -> str:
|
| 68 |
+
digest = hashlib.sha256()
|
| 69 |
+
with Path(path).open("rb") as f:
|
| 70 |
+
for chunk in iter(lambda: f.read(chunk_size), b""):
|
| 71 |
+
digest.update(chunk)
|
| 72 |
+
return digest.hexdigest()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def get_feature_columns(df: pd.DataFrame) -> list[str]:
|
| 76 |
+
"""Return feature columns in scorer/parquet order, never lexicographic sort."""
|
| 77 |
+
return list(df.columns[len(REQUIRED_META_COLUMNS) :])
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def validate_wunder_dataframe(
|
| 81 |
+
df: pd.DataFrame,
|
| 82 |
+
*,
|
| 83 |
+
expected_n_features: int = EXPECTED_N_FEATURES,
|
| 84 |
+
expected_sequence_length: int = EXPECTED_SEQUENCE_LENGTH,
|
| 85 |
+
) -> list[str]:
|
| 86 |
+
"""Validate the official data contract and return feature columns."""
|
| 87 |
+
if list(df.columns[:3]) != REQUIRED_META_COLUMNS:
|
| 88 |
+
raise ValueError(
|
| 89 |
+
"First columns must be seq_ix, step_in_seq, need_prediction; "
|
| 90 |
+
f"got {list(df.columns[:3])}"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
feature_cols = get_feature_columns(df)
|
| 94 |
+
if len(feature_cols) != expected_n_features:
|
| 95 |
+
raise ValueError(
|
| 96 |
+
f"Expected {expected_n_features} feature columns, got {len(feature_cols)}"
|
| 97 |
+
)
|
| 98 |
+
if df[REQUIRED_META_COLUMNS].isna().any().any():
|
| 99 |
+
raise ValueError("Metadata columns contain missing values")
|
| 100 |
+
|
| 101 |
+
lengths = df.groupby("seq_ix", sort=True).size()
|
| 102 |
+
bad_lengths = lengths[lengths != expected_sequence_length]
|
| 103 |
+
if not bad_lengths.empty:
|
| 104 |
+
raise ValueError(
|
| 105 |
+
"Every sequence must have exactly "
|
| 106 |
+
f"{expected_sequence_length} rows; bad seq_ix values: "
|
| 107 |
+
f"{bad_lengths.index.tolist()[:10]}"
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
expected_steps = np.arange(expected_sequence_length)
|
| 111 |
+
for seq_ix, seq_df in df.groupby("seq_ix", sort=False):
|
| 112 |
+
steps = seq_df["step_in_seq"].to_numpy()
|
| 113 |
+
if not np.array_equal(steps, expected_steps):
|
| 114 |
+
raise ValueError(
|
| 115 |
+
f"Sequence {seq_ix} must be ordered with steps 0.."
|
| 116 |
+
f"{expected_sequence_length - 1}"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
if df[feature_cols].isna().any().any():
|
| 120 |
+
raise ValueError("Feature columns contain missing values")
|
| 121 |
+
return feature_cols
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def load_wunder_dataframe(path: str | Path, seq_ids: Optional[Iterable[int]] = None) -> pd.DataFrame:
|
| 125 |
+
"""Read and validate the Wunder parquet file."""
|
| 126 |
+
df = pd.read_parquet(path)
|
| 127 |
+
if seq_ids is not None:
|
| 128 |
+
seq_set = set(int(seq_id) for seq_id in seq_ids)
|
| 129 |
+
df = df[df["seq_ix"].isin(seq_set)].copy()
|
| 130 |
+
df = df.sort_values(["seq_ix", "step_in_seq"]).reset_index(drop=True)
|
| 131 |
+
validate_wunder_dataframe(df)
|
| 132 |
+
return df
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def create_dataset_manifest(path: str | Path) -> DatasetManifest:
|
| 136 |
+
"""Build a reproducibility manifest for a validated parquet file."""
|
| 137 |
+
path = Path(path)
|
| 138 |
+
df = load_wunder_dataframe(path)
|
| 139 |
+
feature_cols = get_feature_columns(df)
|
| 140 |
+
need_counts = (
|
| 141 |
+
df.groupby("step_in_seq")["need_prediction"]
|
| 142 |
+
.sum()
|
| 143 |
+
.astype(int)
|
| 144 |
+
.to_dict()
|
| 145 |
+
)
|
| 146 |
+
seq_ids = sorted(int(x) for x in df["seq_ix"].unique())
|
| 147 |
+
return DatasetManifest(
|
| 148 |
+
path=str(path),
|
| 149 |
+
sha256=sha256_file(path),
|
| 150 |
+
size_bytes=path.stat().st_size,
|
| 151 |
+
row_count=int(len(df)),
|
| 152 |
+
sequence_count=len(seq_ids),
|
| 153 |
+
sequence_length=int(df.groupby("seq_ix").size().iloc[0]),
|
| 154 |
+
feature_columns=feature_cols,
|
| 155 |
+
need_prediction_counts_by_step={str(k): int(v) for k, v in need_counts.items()},
|
| 156 |
+
seq_ix_min=min(seq_ids),
|
| 157 |
+
seq_ix_max=max(seq_ids),
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def write_json(path: str | Path, payload: object) -> None:
|
| 162 |
+
"""Write dataclasses/dicts as stable JSON."""
|
| 163 |
+
path = Path(path)
|
| 164 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 165 |
+
if hasattr(payload, "__dataclass_fields__"):
|
| 166 |
+
payload = asdict(payload)
|
| 167 |
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def read_json(path: str | Path) -> dict:
|
| 171 |
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def current_git_commit() -> Optional[str]:
|
| 175 |
+
try:
|
| 176 |
+
return subprocess.check_output(
|
| 177 |
+
["git", "rev-parse", "HEAD"],
|
| 178 |
+
text=True,
|
| 179 |
+
stderr=subprocess.DEVNULL,
|
| 180 |
+
).strip()
|
| 181 |
+
except Exception:
|
| 182 |
+
return None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def create_sequence_folds(
|
| 186 |
+
seq_ids: Iterable[int],
|
| 187 |
+
*,
|
| 188 |
+
n_folds: int = 5,
|
| 189 |
+
seed: int = 42,
|
| 190 |
+
final_holdout_fold: int = 4,
|
| 191 |
+
dataset_sha256: Optional[str] = None,
|
| 192 |
+
) -> FoldManifest:
|
| 193 |
+
"""Create deterministic folds by seq_ix with one untouched final holdout fold."""
|
| 194 |
+
seq_ids = np.array(sorted(int(x) for x in seq_ids), dtype=int)
|
| 195 |
+
if len(seq_ids) < n_folds:
|
| 196 |
+
raise ValueError("Need at least as many sequences as folds")
|
| 197 |
+
if final_holdout_fold < 0 or final_holdout_fold >= n_folds:
|
| 198 |
+
raise ValueError("final_holdout_fold must be in [0, n_folds)")
|
| 199 |
+
|
| 200 |
+
rng = np.random.RandomState(seed)
|
| 201 |
+
shuffled = seq_ids.copy()
|
| 202 |
+
rng.shuffle(shuffled)
|
| 203 |
+
split_ids = np.array_split(shuffled, n_folds)
|
| 204 |
+
folds = {
|
| 205 |
+
str(i): sorted(int(x) for x in split_ids[i].tolist())
|
| 206 |
+
for i in range(n_folds)
|
| 207 |
+
}
|
| 208 |
+
final_holdout = folds[str(final_holdout_fold)]
|
| 209 |
+
train_dev = sorted(
|
| 210 |
+
int(seq_id)
|
| 211 |
+
for fold_id, fold_seq_ids in folds.items()
|
| 212 |
+
if int(fold_id) != final_holdout_fold
|
| 213 |
+
for seq_id in fold_seq_ids
|
| 214 |
+
)
|
| 215 |
+
return FoldManifest(
|
| 216 |
+
seed=seed,
|
| 217 |
+
n_folds=n_folds,
|
| 218 |
+
final_holdout_fold=final_holdout_fold,
|
| 219 |
+
folds=folds,
|
| 220 |
+
train_dev_seq_ids=train_dev,
|
| 221 |
+
final_holdout_seq_ids=final_holdout,
|
| 222 |
+
dataset_sha256=dataset_sha256,
|
| 223 |
+
git_commit=current_git_commit(),
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def load_sequence_folds(path: str | Path) -> FoldManifest:
|
| 228 |
+
payload = read_json(path)
|
| 229 |
+
return FoldManifest(**payload)
|
src/evaluation/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .stepwise import StepwiseScoreResult, StepwiseScorer
|
| 2 |
+
|
| 3 |
+
__all__ = ["StepwiseScoreResult", "StepwiseScorer"]
|
src/evaluation/stepwise.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Official-style row-by-row evaluation without sequence-boundary leakage."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pandas as pd
|
| 10 |
+
|
| 11 |
+
from competition_package.utils import DataPoint
|
| 12 |
+
from src.data.protocol import get_feature_columns, validate_wunder_dataframe
|
| 13 |
+
from src.utils.metrics import compute_r2_per_feature, compute_r2_score
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass(frozen=True)
|
| 17 |
+
class StepwiseScoreResult:
|
| 18 |
+
mean_r2: float
|
| 19 |
+
r2_per_feature: dict[str, float]
|
| 20 |
+
n_predictions_scored: int
|
| 21 |
+
n_predictions_requested: int
|
| 22 |
+
n_predictions_dropped_final_step: int
|
| 23 |
+
feature_columns: list[str]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class StepwiseScorer:
|
| 27 |
+
"""Replay held-out sequences through the competition `predict` API."""
|
| 28 |
+
|
| 29 |
+
def __init__(self, dataframe: pd.DataFrame):
|
| 30 |
+
self.dataset = dataframe.sort_values(["seq_ix", "step_in_seq"]).reset_index(drop=True)
|
| 31 |
+
self.feature_columns = validate_wunder_dataframe(self.dataset)
|
| 32 |
+
self.dim = len(self.feature_columns)
|
| 33 |
+
|
| 34 |
+
@classmethod
|
| 35 |
+
def from_parquet(cls, path: str, seq_ids: Optional[list[int]] = None) -> "StepwiseScorer":
|
| 36 |
+
df = pd.read_parquet(path)
|
| 37 |
+
if seq_ids is not None:
|
| 38 |
+
df = df[df["seq_ix"].isin(set(seq_ids))].copy()
|
| 39 |
+
return cls(df)
|
| 40 |
+
|
| 41 |
+
def score(self, model) -> StepwiseScoreResult:
|
| 42 |
+
predictions = []
|
| 43 |
+
targets = []
|
| 44 |
+
pending_prediction = None
|
| 45 |
+
pending_seq_ix = None
|
| 46 |
+
requested = 0
|
| 47 |
+
dropped_final = 0
|
| 48 |
+
|
| 49 |
+
for _, seq_df in self.dataset.groupby("seq_ix", sort=True):
|
| 50 |
+
pending_prediction = None
|
| 51 |
+
pending_seq_ix = None
|
| 52 |
+
|
| 53 |
+
seq_ix_values = seq_df["seq_ix"].to_numpy()
|
| 54 |
+
step_values = seq_df["step_in_seq"].to_numpy()
|
| 55 |
+
need_values = seq_df["need_prediction"].to_numpy()
|
| 56 |
+
state_values = seq_df[self.feature_columns].to_numpy(dtype=np.float32)
|
| 57 |
+
|
| 58 |
+
for pos in range(len(seq_df)):
|
| 59 |
+
seq_ix = int(seq_ix_values[pos])
|
| 60 |
+
step_in_seq = int(step_values[pos])
|
| 61 |
+
need_prediction = bool(need_values[pos])
|
| 62 |
+
state = state_values[pos]
|
| 63 |
+
|
| 64 |
+
if pending_prediction is not None:
|
| 65 |
+
if pending_seq_ix != seq_ix:
|
| 66 |
+
raise AssertionError("Internal scorer leaked across sequences")
|
| 67 |
+
predictions.append(pending_prediction)
|
| 68 |
+
targets.append(state)
|
| 69 |
+
|
| 70 |
+
data_point = DataPoint(seq_ix, step_in_seq, need_prediction, state)
|
| 71 |
+
next_prediction = model.predict(data_point)
|
| 72 |
+
self._check_prediction(data_point, next_prediction)
|
| 73 |
+
if need_prediction:
|
| 74 |
+
requested += 1
|
| 75 |
+
pending_prediction = next_prediction
|
| 76 |
+
pending_seq_ix = seq_ix
|
| 77 |
+
|
| 78 |
+
if pending_prediction is not None:
|
| 79 |
+
dropped_final += 1
|
| 80 |
+
|
| 81 |
+
if not predictions:
|
| 82 |
+
raise ValueError("No predictions were scored")
|
| 83 |
+
|
| 84 |
+
y_pred = np.asarray(predictions, dtype=np.float64)
|
| 85 |
+
y_true = np.asarray(targets, dtype=np.float64)
|
| 86 |
+
return StepwiseScoreResult(
|
| 87 |
+
mean_r2=compute_r2_score(y_true, y_pred),
|
| 88 |
+
r2_per_feature=compute_r2_per_feature(y_true, y_pred, self.feature_columns),
|
| 89 |
+
n_predictions_scored=len(predictions),
|
| 90 |
+
n_predictions_requested=requested,
|
| 91 |
+
n_predictions_dropped_final_step=dropped_final,
|
| 92 |
+
feature_columns=self.feature_columns,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
def _check_prediction(self, data_point: DataPoint, prediction: Optional[np.ndarray]) -> None:
|
| 96 |
+
if not data_point.need_prediction:
|
| 97 |
+
if prediction is not None:
|
| 98 |
+
raise ValueError(f"Prediction is not needed for {data_point}")
|
| 99 |
+
return
|
| 100 |
+
if prediction is None:
|
| 101 |
+
raise ValueError(f"Prediction is required for {data_point}")
|
| 102 |
+
if not isinstance(prediction, np.ndarray):
|
| 103 |
+
raise ValueError(f"Prediction must be np.ndarray, got {type(prediction)}")
|
| 104 |
+
if prediction.shape != (self.dim,):
|
| 105 |
+
raise ValueError(f"Prediction has wrong shape: {prediction.shape} != {(self.dim,)}")
|
| 106 |
+
if not np.isfinite(prediction).all():
|
| 107 |
+
raise ValueError("Prediction contains NaN or Inf")
|
src/models/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model package exports (lazy to keep torch optional for tabular-only use)."""
|
| 2 |
+
|
| 3 |
+
import importlib
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
'PersistencePredictionModel',
|
| 7 |
+
'MomentumPredictionModel',
|
| 8 |
+
'EWMAPredictionModel',
|
| 9 |
+
'TabularStatefulPredictionModel',
|
| 10 |
+
'build_causal_tabular_features',
|
| 11 |
+
'CausalGRUForecaster',
|
| 12 |
+
'CausalTCN',
|
| 13 |
+
'EnsemblePredictionModel',
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def __getattr__(name):
|
| 18 |
+
if name in {
|
| 19 |
+
'PersistencePredictionModel',
|
| 20 |
+
'MomentumPredictionModel',
|
| 21 |
+
'EWMAPredictionModel',
|
| 22 |
+
'TabularStatefulPredictionModel',
|
| 23 |
+
'build_causal_tabular_features',
|
| 24 |
+
}:
|
| 25 |
+
return getattr(importlib.import_module(".baselines", __name__), name)
|
| 26 |
+
if name == 'CausalGRUForecaster':
|
| 27 |
+
return getattr(importlib.import_module(".sequence_models", __name__), name)
|
| 28 |
+
if name == 'CausalTCN':
|
| 29 |
+
return getattr(importlib.import_module(".tcn", __name__), name)
|
| 30 |
+
if name == 'EnsemblePredictionModel':
|
| 31 |
+
return getattr(importlib.import_module(".ensemble_predictor", __name__), name)
|
| 32 |
+
raise AttributeError(f"module 'src.models' has no attribute {name!r}")
|
src/models/baselines.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Causal baseline predictors for the Wunder stepwise API."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from src.data.causal_features import build_causal_tabular_features
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class PersistencePredictionModel:
|
| 13 |
+
"""Predict the current state as the next state."""
|
| 14 |
+
|
| 15 |
+
def __init__(self, n_features: int = 32):
|
| 16 |
+
self.n_features = n_features
|
| 17 |
+
self.current_seq: Optional[int] = None
|
| 18 |
+
self.last_state: Optional[np.ndarray] = None
|
| 19 |
+
|
| 20 |
+
def predict(self, data_point):
|
| 21 |
+
if self.current_seq != data_point.seq_ix:
|
| 22 |
+
self.current_seq = data_point.seq_ix
|
| 23 |
+
self.last_state = None
|
| 24 |
+
self.last_state = data_point.state.astype(np.float32, copy=True)
|
| 25 |
+
if not data_point.need_prediction:
|
| 26 |
+
return None
|
| 27 |
+
return self.last_state.astype(np.float32, copy=True)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class MomentumPredictionModel:
|
| 31 |
+
"""Predict next state with a first-difference momentum term."""
|
| 32 |
+
|
| 33 |
+
def __init__(self, alpha: float = 1.0, n_features: int = 32):
|
| 34 |
+
self.alpha = float(alpha)
|
| 35 |
+
self.n_features = n_features
|
| 36 |
+
self.current_seq: Optional[int] = None
|
| 37 |
+
self.previous_state: Optional[np.ndarray] = None
|
| 38 |
+
self.current_state: Optional[np.ndarray] = None
|
| 39 |
+
|
| 40 |
+
def predict(self, data_point):
|
| 41 |
+
if self.current_seq != data_point.seq_ix:
|
| 42 |
+
self.current_seq = data_point.seq_ix
|
| 43 |
+
self.previous_state = None
|
| 44 |
+
self.current_state = None
|
| 45 |
+
|
| 46 |
+
self.previous_state = self.current_state
|
| 47 |
+
self.current_state = data_point.state.astype(np.float32, copy=True)
|
| 48 |
+
if not data_point.need_prediction:
|
| 49 |
+
return None
|
| 50 |
+
if self.previous_state is None:
|
| 51 |
+
return self.current_state.astype(np.float32, copy=True)
|
| 52 |
+
pred = self.current_state + self.alpha * (self.current_state - self.previous_state)
|
| 53 |
+
return pred.astype(np.float32)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class EWMAPredictionModel:
|
| 57 |
+
"""Predict with an exponentially weighted moving average state."""
|
| 58 |
+
|
| 59 |
+
def __init__(self, alpha: float = 0.2, n_features: int = 32):
|
| 60 |
+
if not 0.0 < alpha <= 1.0:
|
| 61 |
+
raise ValueError("alpha must be in (0, 1]")
|
| 62 |
+
self.alpha = float(alpha)
|
| 63 |
+
self.n_features = n_features
|
| 64 |
+
self.current_seq: Optional[int] = None
|
| 65 |
+
self.ewma_state: Optional[np.ndarray] = None
|
| 66 |
+
|
| 67 |
+
def predict(self, data_point):
|
| 68 |
+
if self.current_seq != data_point.seq_ix:
|
| 69 |
+
self.current_seq = data_point.seq_ix
|
| 70 |
+
self.ewma_state = None
|
| 71 |
+
state = data_point.state.astype(np.float32, copy=False)
|
| 72 |
+
if self.ewma_state is None:
|
| 73 |
+
self.ewma_state = state.copy()
|
| 74 |
+
else:
|
| 75 |
+
self.ewma_state = self.alpha * state + (1.0 - self.alpha) * self.ewma_state
|
| 76 |
+
if not data_point.need_prediction:
|
| 77 |
+
return None
|
| 78 |
+
return self.ewma_state.astype(np.float32, copy=True)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class TabularStatefulPredictionModel:
|
| 82 |
+
"""Stateful wrapper for sklearn-style multi-output tabular regressors."""
|
| 83 |
+
|
| 84 |
+
def __init__(self, estimator, n_features: int = 32, feature_schema: str = "compact_v1"):
|
| 85 |
+
self.estimator = estimator
|
| 86 |
+
self.n_features = n_features
|
| 87 |
+
self.feature_schema = feature_schema
|
| 88 |
+
self.current_seq: Optional[int] = None
|
| 89 |
+
self.history: list[np.ndarray] = []
|
| 90 |
+
|
| 91 |
+
def predict(self, data_point):
|
| 92 |
+
if self.current_seq != data_point.seq_ix:
|
| 93 |
+
self.current_seq = data_point.seq_ix
|
| 94 |
+
self.history = []
|
| 95 |
+
self.history.append(data_point.state.astype(np.float32, copy=True))
|
| 96 |
+
if not data_point.need_prediction:
|
| 97 |
+
return None
|
| 98 |
+
features = build_causal_tabular_features(
|
| 99 |
+
np.asarray(self.history, dtype=np.float32),
|
| 100 |
+
schema=self.feature_schema,
|
| 101 |
+
)
|
| 102 |
+
pred = self.estimator.predict(features.reshape(1, -1))[0]
|
| 103 |
+
return np.asarray(pred, dtype=np.float32)
|
src/models/ensemble_predictor.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Uniform ensemble PredictionModel for the Wunder stepwise task.
|
| 2 |
+
|
| 3 |
+
Both GRUStatefulPredictionModel and TabularStatefulPredictionModel expose the
|
| 4 |
+
same contract: ``predict(data_point)`` advances per-sequence internal state and
|
| 5 |
+
returns None during warm-up or an (n_features,) array when a prediction is
|
| 6 |
+
needed. An ensemble is therefore just a list of member predictors plus an
|
| 7 |
+
optional per-feature weight matrix. The same class powers local holdout
|
| 8 |
+
evaluation and the packaged solution.py.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
from typing import Optional
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class EnsemblePredictionModel:
|
| 19 |
+
def __init__(self, members: list, weights: Optional[np.ndarray] = None, n_features: int = 32):
|
| 20 |
+
"""Args:
|
| 21 |
+
members: objects exposing predict(data_point) -> Optional[np.ndarray].
|
| 22 |
+
weights: optional (n_features, n_members) blend weights. If None, the
|
| 23 |
+
members are averaged equally.
|
| 24 |
+
"""
|
| 25 |
+
if not members:
|
| 26 |
+
raise ValueError("EnsemblePredictionModel needs at least one member")
|
| 27 |
+
self.members = members
|
| 28 |
+
self.n_features = n_features
|
| 29 |
+
self.weights = None
|
| 30 |
+
if weights is not None:
|
| 31 |
+
weights = np.asarray(weights, dtype=np.float32)
|
| 32 |
+
if weights.shape != (n_features, len(members)):
|
| 33 |
+
raise ValueError(
|
| 34 |
+
f"weights shape {weights.shape} != {(n_features, len(members))}"
|
| 35 |
+
)
|
| 36 |
+
self.weights = weights
|
| 37 |
+
|
| 38 |
+
def predict(self, data_point) -> Optional[np.ndarray]:
|
| 39 |
+
# Always advance every member's state, even during warm-up.
|
| 40 |
+
member_preds = [m.predict(data_point) for m in self.members]
|
| 41 |
+
if not data_point.need_prediction:
|
| 42 |
+
return None
|
| 43 |
+
stacked = np.stack([np.asarray(p, dtype=np.float32) for p in member_preds], axis=0) # (M, F)
|
| 44 |
+
if self.weights is not None:
|
| 45 |
+
# out[f] = sum_m stacked[m, f] * weights[f, m]
|
| 46 |
+
out = np.einsum("mf,fm->f", stacked, self.weights)
|
| 47 |
+
else:
|
| 48 |
+
out = stacked.mean(axis=0)
|
| 49 |
+
return out.astype(np.float32)
|
src/models/sequence_inference.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Online stateful inference wrapper for the causal GRU.
|
| 2 |
+
|
| 3 |
+
Carries the GRU hidden state across steps so each prediction is O(1) work,
|
| 4 |
+
which is what makes the model fit the 1-core / 60-minute submission budget.
|
| 5 |
+
Used by both local stepwise evaluation and the packaged solution.py.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
from src.models.sequence_models import CausalGRUForecaster
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class GRUStatefulPredictionModel:
|
| 20 |
+
"""Wrap a CausalGRUForecaster as a competition PredictionModel."""
|
| 21 |
+
|
| 22 |
+
def __init__(self, model: CausalGRUForecaster, n_features: int = 32):
|
| 23 |
+
self.model = model.eval()
|
| 24 |
+
self.n_features = n_features
|
| 25 |
+
self.current_seq: Optional[int] = None
|
| 26 |
+
self.h: Optional[torch.Tensor] = None
|
| 27 |
+
|
| 28 |
+
def reset(self):
|
| 29 |
+
self.current_seq = None
|
| 30 |
+
self.h = None
|
| 31 |
+
|
| 32 |
+
@torch.no_grad()
|
| 33 |
+
def predict(self, data_point):
|
| 34 |
+
if self.current_seq != data_point.seq_ix:
|
| 35 |
+
self.current_seq = data_point.seq_ix
|
| 36 |
+
self.h = None
|
| 37 |
+
state = torch.from_numpy(np.asarray(data_point.state, dtype=np.float32))
|
| 38 |
+
pred, self.h = self.model.step(state, self.h)
|
| 39 |
+
if not data_point.need_prediction:
|
| 40 |
+
return None
|
| 41 |
+
return pred.detach().numpy().astype(np.float32)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def build_model_from_config(params: dict, model_type: str = "CausalGRUForecaster"):
|
| 45 |
+
if model_type == "CausalTCN":
|
| 46 |
+
from src.models.tcn import CausalTCN
|
| 47 |
+
return CausalTCN(
|
| 48 |
+
n_features=params.get("n_features", 32),
|
| 49 |
+
d_model=params.get("d_model", 192),
|
| 50 |
+
n_layers=params.get("n_layers", 6),
|
| 51 |
+
kernel_size=params.get("kernel_size", 3),
|
| 52 |
+
dropout=params.get("dropout", 0.1),
|
| 53 |
+
head_hidden=params.get("head_hidden"),
|
| 54 |
+
)
|
| 55 |
+
return CausalGRUForecaster(
|
| 56 |
+
n_features=params.get("n_features", 32),
|
| 57 |
+
d_model=params.get("d_model", 256),
|
| 58 |
+
n_layers=params.get("n_layers", 2),
|
| 59 |
+
dropout=params.get("dropout", 0.1),
|
| 60 |
+
head_hidden=params.get("head_hidden"),
|
| 61 |
+
rnn_type=params.get("rnn_type", "gru"),
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def load_gru_checkpoint(path: str | Path, map_location: str = "cpu"):
|
| 66 |
+
"""Reconstruct a forecaster (GRU/LSTM/TCN) from a saved checkpoint."""
|
| 67 |
+
ckpt = torch.load(str(path), map_location=map_location, weights_only=False)
|
| 68 |
+
model_cfg = ckpt.get("config", {}).get("model", {})
|
| 69 |
+
params = model_cfg.get("params", {})
|
| 70 |
+
model = build_model_from_config(params, model_cfg.get("type", "CausalGRUForecaster"))
|
| 71 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 72 |
+
model.eval()
|
| 73 |
+
return model
|
src/models/sequence_models.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Causal sequence models for the Wunder stepwise next-state task.
|
| 2 |
+
|
| 3 |
+
Design constraints derived from the data analysis (see memory / report):
|
| 4 |
+
- Features are pre-whitened (~N(0,1), no heavy tails) so no internal
|
| 5 |
+
normalization is required; the model predicts the next *level* directly.
|
| 6 |
+
- Inference is online, one row at a time, on a single CPU core within a
|
| 7 |
+
60-minute budget. A unidirectional GRU carries hidden state across steps,
|
| 8 |
+
giving O(1) work per prediction.
|
| 9 |
+
- Training uses full 1000-step sequences with back-prop-through-time and a
|
| 10 |
+
loss masked to the scored steps (100..998). Because a GRU is a pure
|
| 11 |
+
recurrence, the full-sequence forward and the stepwise stateful forward
|
| 12 |
+
produce identical outputs — so what we train is exactly what we score.
|
| 13 |
+
|
| 14 |
+
`forward(x, h0)` returns per-step predictions of the *next* state:
|
| 15 |
+
preds[:, t] predicts x[:, t + 1].
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
from typing import Optional, Tuple
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
import torch.nn as nn
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class CausalGRUForecaster(nn.Module):
|
| 27 |
+
"""Unidirectional GRU that predicts the next state at every step.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
n_features: input/output width (32).
|
| 31 |
+
d_model: GRU hidden size and embedding width.
|
| 32 |
+
n_layers: stacked GRU layers.
|
| 33 |
+
dropout: dropout between GRU layers and inside the head.
|
| 34 |
+
head_hidden: width of the MLP head hidden layer (defaults to d_model).
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
n_features: int = 32,
|
| 40 |
+
d_model: int = 256,
|
| 41 |
+
n_layers: int = 2,
|
| 42 |
+
dropout: float = 0.1,
|
| 43 |
+
head_hidden: Optional[int] = None,
|
| 44 |
+
rnn_type: str = "gru",
|
| 45 |
+
):
|
| 46 |
+
super().__init__()
|
| 47 |
+
self.n_features = int(n_features)
|
| 48 |
+
self.d_model = int(d_model)
|
| 49 |
+
self.n_layers = int(n_layers)
|
| 50 |
+
self.rnn_type = str(rnn_type).lower()
|
| 51 |
+
head_hidden = int(head_hidden) if head_hidden else self.d_model
|
| 52 |
+
|
| 53 |
+
# Per-step input embedding (applied identically in batch and stepwise modes).
|
| 54 |
+
self.input_proj = nn.Linear(n_features, d_model)
|
| 55 |
+
self.input_norm = nn.LayerNorm(d_model)
|
| 56 |
+
|
| 57 |
+
rnn_cls = nn.LSTM if self.rnn_type == "lstm" else nn.GRU
|
| 58 |
+
self.gru = rnn_cls(
|
| 59 |
+
input_size=d_model,
|
| 60 |
+
hidden_size=d_model,
|
| 61 |
+
num_layers=n_layers,
|
| 62 |
+
batch_first=True,
|
| 63 |
+
dropout=dropout if n_layers > 1 else 0.0,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
self.head = nn.Sequential(
|
| 67 |
+
nn.LayerNorm(d_model),
|
| 68 |
+
nn.Linear(d_model, head_hidden),
|
| 69 |
+
nn.GELU(),
|
| 70 |
+
nn.Dropout(dropout),
|
| 71 |
+
nn.Linear(head_hidden, n_features),
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
def forward(
|
| 75 |
+
self,
|
| 76 |
+
x: torch.Tensor,
|
| 77 |
+
h0: Optional[torch.Tensor] = None,
|
| 78 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 79 |
+
"""Args:
|
| 80 |
+
x: (batch, seq_len, n_features)
|
| 81 |
+
h0: optional initial hidden state (n_layers, batch, d_model)
|
| 82 |
+
Returns:
|
| 83 |
+
preds: (batch, seq_len, n_features) where preds[:, t] predicts x[:, t+1]
|
| 84 |
+
h_n: final hidden state (n_layers, batch, d_model)
|
| 85 |
+
"""
|
| 86 |
+
z = self.input_norm(self.input_proj(x))
|
| 87 |
+
out, h_n = self.gru(z, h0)
|
| 88 |
+
preds = self.head(out)
|
| 89 |
+
return preds, h_n
|
| 90 |
+
|
| 91 |
+
@torch.no_grad()
|
| 92 |
+
def step(
|
| 93 |
+
self,
|
| 94 |
+
state: torch.Tensor,
|
| 95 |
+
h: Optional[torch.Tensor] = None,
|
| 96 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 97 |
+
"""Single online step.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
state: (n_features,) or (1, n_features) current state.
|
| 101 |
+
h: previous hidden state (n_layers, 1, d_model) or None.
|
| 102 |
+
Returns:
|
| 103 |
+
pred: (n_features,) prediction of the next state.
|
| 104 |
+
h_new: updated hidden state.
|
| 105 |
+
"""
|
| 106 |
+
if state.dim() == 1:
|
| 107 |
+
state = state.view(1, 1, -1)
|
| 108 |
+
elif state.dim() == 2:
|
| 109 |
+
state = state.unsqueeze(1)
|
| 110 |
+
preds, h_new = self.forward(state, h)
|
| 111 |
+
return preds[0, -1], h_new
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def build_gru(config: dict) -> CausalGRUForecaster:
|
| 115 |
+
"""Construct a CausalGRUForecaster from a plain dict config."""
|
| 116 |
+
return CausalGRUForecaster(
|
| 117 |
+
n_features=config.get("n_features", 32),
|
| 118 |
+
d_model=config.get("d_model", 256),
|
| 119 |
+
n_layers=config.get("n_layers", 2),
|
| 120 |
+
dropout=config.get("dropout", 0.1),
|
| 121 |
+
head_hidden=config.get("head_hidden"),
|
| 122 |
+
rnn_type=config.get("rnn_type", "gru"),
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def count_parameters(model: nn.Module) -> int:
|
| 127 |
+
return sum(p.numel() for p in model.parameters() if p.requires_grad)
|
src/models/tcn.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Causal Temporal Convolutional Network for next-state prediction.
|
| 2 |
+
|
| 3 |
+
A different inductive bias from the RNNs (dilated causal convolutions instead of
|
| 4 |
+
a recurrence), which decorrelates its errors from the GRU/LSTM members and so
|
| 5 |
+
adds more in an ensemble than another RNN seed.
|
| 6 |
+
|
| 7 |
+
Exposes the same interface as CausalGRUForecaster so it is a drop-in for the
|
| 8 |
+
trainer, the stateful inference wrapper, and the ensembler:
|
| 9 |
+
forward(x, h0=None) -> (preds, None) where preds[:, t] predicts x[:, t+1]
|
| 10 |
+
step(state, h) -> (pred, buffer) buffer holds the last receptive-field states
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
from typing import Optional, Tuple
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn as nn
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class _ChannelLayerNorm(nn.Module):
|
| 22 |
+
"""LayerNorm over the channel dim of a (B, C, T) tensor, independent per
|
| 23 |
+
timestep — causal, unlike GroupNorm/BatchNorm which mix statistics across T."""
|
| 24 |
+
|
| 25 |
+
def __init__(self, d_model: int):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.norm = nn.LayerNorm(d_model)
|
| 28 |
+
|
| 29 |
+
def forward(self, x): # (B, C, T)
|
| 30 |
+
return self.norm(x.transpose(1, 2)).transpose(1, 2)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class _CausalConvBlock(nn.Module):
|
| 34 |
+
def __init__(self, d_model: int, kernel_size: int, dilation: int, dropout: float):
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.pad = (kernel_size - 1) * dilation
|
| 37 |
+
self.conv1 = nn.Conv1d(d_model, d_model, kernel_size, dilation=dilation, padding=self.pad)
|
| 38 |
+
self.conv2 = nn.Conv1d(d_model, d_model, kernel_size, dilation=dilation, padding=self.pad)
|
| 39 |
+
self.norm1 = _ChannelLayerNorm(d_model)
|
| 40 |
+
self.norm2 = _ChannelLayerNorm(d_model)
|
| 41 |
+
self.act = nn.GELU()
|
| 42 |
+
self.drop = nn.Dropout(dropout)
|
| 43 |
+
|
| 44 |
+
def forward(self, x): # x: (B, d, T)
|
| 45 |
+
T = x.shape[-1]
|
| 46 |
+
h = self.conv1(x)[..., :T] # trim right (future) padding -> causal
|
| 47 |
+
h = self.drop(self.act(self.norm1(h)))
|
| 48 |
+
h = self.conv2(h)[..., :T]
|
| 49 |
+
h = self.drop(self.act(self.norm2(h)))
|
| 50 |
+
return x + h
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class CausalTCN(nn.Module):
|
| 54 |
+
def __init__(
|
| 55 |
+
self,
|
| 56 |
+
n_features: int = 32,
|
| 57 |
+
d_model: int = 192,
|
| 58 |
+
n_layers: int = 6,
|
| 59 |
+
kernel_size: int = 3,
|
| 60 |
+
dropout: float = 0.1,
|
| 61 |
+
head_hidden: Optional[int] = None,
|
| 62 |
+
):
|
| 63 |
+
super().__init__()
|
| 64 |
+
self.n_features = int(n_features)
|
| 65 |
+
self.d_model = int(d_model)
|
| 66 |
+
self.n_layers = int(n_layers)
|
| 67 |
+
self.kernel_size = int(kernel_size)
|
| 68 |
+
head_hidden = int(head_hidden) if head_hidden else d_model
|
| 69 |
+
|
| 70 |
+
self.input_proj = nn.Linear(n_features, d_model)
|
| 71 |
+
self.input_norm = nn.LayerNorm(d_model)
|
| 72 |
+
self.blocks = nn.ModuleList([
|
| 73 |
+
_CausalConvBlock(d_model, kernel_size, dilation=2 ** i, dropout=dropout)
|
| 74 |
+
for i in range(n_layers)
|
| 75 |
+
])
|
| 76 |
+
self.head = nn.Sequential(
|
| 77 |
+
nn.LayerNorm(d_model),
|
| 78 |
+
nn.Linear(d_model, head_hidden),
|
| 79 |
+
nn.GELU(),
|
| 80 |
+
nn.Dropout(dropout),
|
| 81 |
+
nn.Linear(head_hidden, n_features),
|
| 82 |
+
)
|
| 83 |
+
# receptive field for buffered inference
|
| 84 |
+
self.receptive_field = 1 + 2 * sum((kernel_size - 1) * (2 ** i) for i in range(n_layers))
|
| 85 |
+
|
| 86 |
+
def forward(self, x: torch.Tensor, h0=None) -> Tuple[torch.Tensor, None]:
|
| 87 |
+
z = self.input_norm(self.input_proj(x)) # (B, T, d)
|
| 88 |
+
z = z.transpose(1, 2) # (B, d, T)
|
| 89 |
+
for block in self.blocks:
|
| 90 |
+
z = block(z)
|
| 91 |
+
z = z.transpose(1, 2) # (B, T, d)
|
| 92 |
+
return self.head(z), None
|
| 93 |
+
|
| 94 |
+
@torch.no_grad()
|
| 95 |
+
def step(self, state: torch.Tensor, h: Optional[torch.Tensor] = None):
|
| 96 |
+
"""h is the running buffer of recent states (1, L, F)."""
|
| 97 |
+
if state.dim() == 1:
|
| 98 |
+
state = state.view(1, 1, -1)
|
| 99 |
+
elif state.dim() == 2:
|
| 100 |
+
state = state.unsqueeze(1)
|
| 101 |
+
buf = state if h is None else torch.cat([h, state], dim=1)
|
| 102 |
+
buf = buf[:, -self.receptive_field:]
|
| 103 |
+
preds, _ = self.forward(buf)
|
| 104 |
+
return preds[0, -1], buf
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def build_tcn(config: dict) -> CausalTCN:
|
| 108 |
+
return CausalTCN(
|
| 109 |
+
n_features=config.get("n_features", 32),
|
| 110 |
+
d_model=config.get("d_model", 192),
|
| 111 |
+
n_layers=config.get("n_layers", 6),
|
| 112 |
+
kernel_size=config.get("kernel_size", 3),
|
| 113 |
+
dropout=config.get("dropout", 0.1),
|
| 114 |
+
head_hidden=config.get("head_hidden"),
|
| 115 |
+
)
|
src/training/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training package exports (lazy loading for the Torch-dependent trainer)."""
|
| 2 |
+
|
| 3 |
+
import importlib
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
'build_tabular_supervised_matrix',
|
| 7 |
+
'create_tabular_estimator',
|
| 8 |
+
'TrainConfig',
|
| 9 |
+
'train_sequence_model',
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def __getattr__(name):
|
| 14 |
+
if name in {'build_tabular_supervised_matrix', 'create_tabular_estimator'}:
|
| 15 |
+
return getattr(importlib.import_module(".baseline_training", __name__), name)
|
| 16 |
+
if name in {'TrainConfig', 'train_sequence_model'}:
|
| 17 |
+
return getattr(importlib.import_module(".sequence_trainer", __name__), name)
|
| 18 |
+
raise AttributeError(f"module 'src.training' has no attribute {name!r}")
|
src/training/baseline_training.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training utilities for Phase 2 causal tabular baselines."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import pandas as pd
|
| 7 |
+
|
| 8 |
+
from src.data.protocol import get_feature_columns, load_wunder_dataframe
|
| 9 |
+
from src.models.baselines import build_causal_tabular_features
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def build_tabular_supervised_matrix(
|
| 13 |
+
parquet_path: str,
|
| 14 |
+
seq_ids: list[int],
|
| 15 |
+
*,
|
| 16 |
+
feature_schema: str = "compact_v1",
|
| 17 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 18 |
+
"""Build causal tabular features and next-state targets for selected sequences."""
|
| 19 |
+
df = load_wunder_dataframe(parquet_path, seq_ids=seq_ids)
|
| 20 |
+
feature_cols = get_feature_columns(df)
|
| 21 |
+
rows = []
|
| 22 |
+
targets = []
|
| 23 |
+
for _, seq_df in df.groupby("seq_ix", sort=True):
|
| 24 |
+
states = seq_df[feature_cols].to_numpy(dtype=np.float32)
|
| 25 |
+
need_prediction = seq_df["need_prediction"].to_numpy(dtype=bool)
|
| 26 |
+
for pos in range(len(states) - 1):
|
| 27 |
+
if not need_prediction[pos]:
|
| 28 |
+
continue
|
| 29 |
+
rows.append(build_causal_tabular_features(states[: pos + 1], schema=feature_schema))
|
| 30 |
+
targets.append(states[pos + 1])
|
| 31 |
+
if not rows:
|
| 32 |
+
raise ValueError("No supervised rows were created")
|
| 33 |
+
return np.vstack(rows).astype(np.float32), np.vstack(targets).astype(np.float32)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def create_tabular_estimator(model_type: str, seed: int = 42, n_jobs: int = 1):
|
| 37 |
+
"""Create a deterministic sklearn-compatible multi-output regressor."""
|
| 38 |
+
model_type = model_type.lower()
|
| 39 |
+
if model_type == "ridge":
|
| 40 |
+
from sklearn.linear_model import Ridge
|
| 41 |
+
|
| 42 |
+
return Ridge(alpha=1.0, random_state=seed)
|
| 43 |
+
if model_type == "ridge_cv":
|
| 44 |
+
from sklearn.linear_model import RidgeCV
|
| 45 |
+
from sklearn.multioutput import MultiOutputRegressor
|
| 46 |
+
|
| 47 |
+
base = RidgeCV(alphas=np.logspace(-4, 4, 17))
|
| 48 |
+
return MultiOutputRegressor(base, n_jobs=max(1, int(n_jobs)))
|
| 49 |
+
if model_type == "elasticnet":
|
| 50 |
+
from sklearn.linear_model import MultiTaskElasticNet
|
| 51 |
+
|
| 52 |
+
return MultiTaskElasticNet(alpha=0.0005, l1_ratio=0.05, random_state=seed, max_iter=5000)
|
| 53 |
+
if model_type == "lightgbm":
|
| 54 |
+
import lightgbm as lgb
|
| 55 |
+
from sklearn.multioutput import MultiOutputRegressor
|
| 56 |
+
|
| 57 |
+
base = lgb.LGBMRegressor(
|
| 58 |
+
n_estimators=300,
|
| 59 |
+
learning_rate=0.03,
|
| 60 |
+
num_leaves=31,
|
| 61 |
+
subsample=0.9,
|
| 62 |
+
colsample_bytree=0.9,
|
| 63 |
+
random_state=seed,
|
| 64 |
+
n_jobs=1,
|
| 65 |
+
verbose=-1,
|
| 66 |
+
)
|
| 67 |
+
return MultiOutputRegressor(base, n_jobs=max(1, int(n_jobs)))
|
| 68 |
+
raise ValueError(f"Unknown tabular model type: {model_type}")
|
src/training/sequence_trainer.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Full-sequence BPTT trainer for the causal GRU next-state model.
|
| 2 |
+
|
| 3 |
+
Trains on whole 1000-step sequences, computing a masked MSE loss only on the
|
| 4 |
+
scored steps (need_prediction True, i.e. current steps 100..998 -> targets
|
| 5 |
+
101..999). Validation R2 is computed with a batched full-sequence forward,
|
| 6 |
+
which is numerically identical to the stepwise stateful replay because a GRU
|
| 7 |
+
is a pure recurrence.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import time
|
| 13 |
+
from dataclasses import dataclass, field
|
| 14 |
+
from typing import Optional
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn as nn
|
| 19 |
+
|
| 20 |
+
from src.data.protocol import get_feature_columns, load_wunder_dataframe
|
| 21 |
+
from src.models.sequence_models import CausalGRUForecaster, count_parameters
|
| 22 |
+
from src.models.tcn import CausalTCN
|
| 23 |
+
from src.utils.metrics import compute_r2_per_feature, compute_r2_score
|
| 24 |
+
from src.utils.reproducibility import set_global_seed
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def load_full_sequences(parquet_path: str, seq_ids: list[int]):
|
| 28 |
+
"""Return (states, need, seq_ids) as dense arrays.
|
| 29 |
+
|
| 30 |
+
states: (N, 1000, 32) float32
|
| 31 |
+
need: (N, 1000) bool (need_prediction flag per step)
|
| 32 |
+
"""
|
| 33 |
+
df = load_wunder_dataframe(parquet_path, seq_ids=seq_ids)
|
| 34 |
+
cols = get_feature_columns(df)
|
| 35 |
+
states_list, need_list, ids = [], [], []
|
| 36 |
+
for sid, g in df.sort_values(["seq_ix", "step_in_seq"]).groupby("seq_ix", sort=True):
|
| 37 |
+
states_list.append(g[cols].to_numpy(dtype=np.float32))
|
| 38 |
+
need_list.append(g["need_prediction"].to_numpy(dtype=bool))
|
| 39 |
+
ids.append(int(sid))
|
| 40 |
+
states = np.stack(states_list).astype(np.float32)
|
| 41 |
+
need = np.stack(need_list)
|
| 42 |
+
return states, need, ids
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@dataclass
|
| 46 |
+
class TrainConfig:
|
| 47 |
+
d_model: int = 256
|
| 48 |
+
n_layers: int = 2
|
| 49 |
+
dropout: float = 0.1
|
| 50 |
+
head_hidden: Optional[int] = None
|
| 51 |
+
epochs: int = 40
|
| 52 |
+
batch_size: int = 32
|
| 53 |
+
lr: float = 1.0e-3
|
| 54 |
+
weight_decay: float = 1.0e-4
|
| 55 |
+
grad_clip: float = 1.0
|
| 56 |
+
warmup_frac: float = 0.1
|
| 57 |
+
seed: int = 42
|
| 58 |
+
threads: int = 8
|
| 59 |
+
patience: int = 12
|
| 60 |
+
n_features: int = 32
|
| 61 |
+
device: str = "cpu"
|
| 62 |
+
rnn_type: str = "gru"
|
| 63 |
+
arch: str = "rnn" # "rnn" or "tcn"
|
| 64 |
+
kernel_size: int = 3 # tcn only
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _masked_mse(preds: torch.Tensor, states: torch.Tensor, need: torch.Tensor) -> torch.Tensor:
|
| 68 |
+
# preds[:, t] predicts states[:, t+1]
|
| 69 |
+
pred = preds[:, :-1, :]
|
| 70 |
+
target = states[:, 1:, :]
|
| 71 |
+
mask = need[:, :-1] # (B, T-1)
|
| 72 |
+
diff2 = (pred - target) ** 2 # (B, T-1, F)
|
| 73 |
+
m = mask.unsqueeze(-1).to(diff2.dtype)
|
| 74 |
+
return (diff2 * m).sum() / (m.sum() * preds.shape[-1])
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@torch.no_grad()
|
| 78 |
+
def _eval_r2(model, states_t, need_t, feature_cols, batch_size=64):
|
| 79 |
+
model.eval()
|
| 80 |
+
preds_all, tgts_all = [], []
|
| 81 |
+
n = states_t.shape[0]
|
| 82 |
+
for i in range(0, n, batch_size):
|
| 83 |
+
sb = states_t[i : i + batch_size]
|
| 84 |
+
nb = need_t[i : i + batch_size]
|
| 85 |
+
preds, _ = model(sb)
|
| 86 |
+
pred = preds[:, :-1, :]
|
| 87 |
+
target = sb[:, 1:, :]
|
| 88 |
+
mask = nb[:, :-1]
|
| 89 |
+
preds_all.append(pred[mask].cpu().numpy())
|
| 90 |
+
tgts_all.append(target[mask].cpu().numpy())
|
| 91 |
+
y_pred = np.concatenate(preds_all).astype(np.float64)
|
| 92 |
+
y_true = np.concatenate(tgts_all).astype(np.float64)
|
| 93 |
+
mean_r2 = compute_r2_score(y_true, y_pred)
|
| 94 |
+
per_feat = compute_r2_per_feature(y_true, y_pred, feature_cols)
|
| 95 |
+
return mean_r2, per_feat, y_true, y_pred
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def train_sequence_model(
|
| 99 |
+
data_path: str,
|
| 100 |
+
train_ids: list[int],
|
| 101 |
+
val_ids: list[int],
|
| 102 |
+
cfg: TrainConfig,
|
| 103 |
+
feature_cols: Optional[list[str]] = None,
|
| 104 |
+
log_every: int = 1,
|
| 105 |
+
verbose: bool = True,
|
| 106 |
+
):
|
| 107 |
+
set_global_seed(cfg.seed, deterministic_torch=True, seed_torch=True)
|
| 108 |
+
torch.set_num_threads(int(cfg.threads))
|
| 109 |
+
device = torch.device(cfg.device if (cfg.device != "cuda" or torch.cuda.is_available()) else "cpu")
|
| 110 |
+
if str(device) != cfg.device:
|
| 111 |
+
print(f"requested device '{cfg.device}' unavailable; using {device}")
|
| 112 |
+
|
| 113 |
+
s_tr, n_tr, _ = load_full_sequences(data_path, train_ids)
|
| 114 |
+
s_va, n_va, va_ids = load_full_sequences(data_path, val_ids)
|
| 115 |
+
if feature_cols is None:
|
| 116 |
+
feature_cols = [str(i) for i in range(cfg.n_features)]
|
| 117 |
+
|
| 118 |
+
states_tr = torch.from_numpy(s_tr).to(device)
|
| 119 |
+
need_tr = torch.from_numpy(n_tr).to(device)
|
| 120 |
+
states_va = torch.from_numpy(s_va).to(device)
|
| 121 |
+
need_va = torch.from_numpy(n_va).to(device)
|
| 122 |
+
|
| 123 |
+
if cfg.arch == "tcn":
|
| 124 |
+
model = CausalTCN(
|
| 125 |
+
n_features=cfg.n_features, d_model=cfg.d_model, n_layers=cfg.n_layers,
|
| 126 |
+
kernel_size=cfg.kernel_size, dropout=cfg.dropout, head_hidden=cfg.head_hidden,
|
| 127 |
+
).to(device)
|
| 128 |
+
else:
|
| 129 |
+
model = CausalGRUForecaster(
|
| 130 |
+
n_features=cfg.n_features, d_model=cfg.d_model, n_layers=cfg.n_layers,
|
| 131 |
+
dropout=cfg.dropout, head_hidden=cfg.head_hidden, rnn_type=cfg.rnn_type,
|
| 132 |
+
).to(device)
|
| 133 |
+
if verbose:
|
| 134 |
+
print(f"model params: {count_parameters(model):,} train_seqs={len(train_ids)} val_seqs={len(val_ids)}")
|
| 135 |
+
|
| 136 |
+
opt = torch.optim.AdamW(model.parameters(), lr=cfg.lr, weight_decay=cfg.weight_decay)
|
| 137 |
+
n_batches = max(1, (states_tr.shape[0] + cfg.batch_size - 1) // cfg.batch_size)
|
| 138 |
+
total_steps = cfg.epochs * n_batches
|
| 139 |
+
sched = torch.optim.lr_scheduler.OneCycleLR(
|
| 140 |
+
opt, max_lr=cfg.lr, total_steps=total_steps,
|
| 141 |
+
pct_start=cfg.warmup_frac, anneal_strategy="cos",
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
g = torch.Generator()
|
| 145 |
+
g.manual_seed(cfg.seed)
|
| 146 |
+
best_r2 = -1e9
|
| 147 |
+
best_state = None
|
| 148 |
+
best_per_feat = None
|
| 149 |
+
best_oof = None
|
| 150 |
+
history = []
|
| 151 |
+
bad_epochs = 0
|
| 152 |
+
|
| 153 |
+
for epoch in range(cfg.epochs):
|
| 154 |
+
model.train()
|
| 155 |
+
perm = torch.randperm(states_tr.shape[0], generator=g)
|
| 156 |
+
ep_loss = 0.0
|
| 157 |
+
t0 = time.perf_counter()
|
| 158 |
+
for bi in range(n_batches):
|
| 159 |
+
idx = perm[bi * cfg.batch_size : (bi + 1) * cfg.batch_size]
|
| 160 |
+
sb = states_tr[idx]
|
| 161 |
+
nb = need_tr[idx]
|
| 162 |
+
opt.zero_grad(set_to_none=True)
|
| 163 |
+
preds, _ = model(sb)
|
| 164 |
+
loss = _masked_mse(preds, sb, nb)
|
| 165 |
+
loss.backward()
|
| 166 |
+
nn.utils.clip_grad_norm_(model.parameters(), cfg.grad_clip)
|
| 167 |
+
opt.step()
|
| 168 |
+
sched.step()
|
| 169 |
+
ep_loss += float(loss.detach())
|
| 170 |
+
ep_loss /= n_batches
|
| 171 |
+
|
| 172 |
+
val_r2, per_feat, y_true, y_pred = _eval_r2(model, states_va, need_va, feature_cols)
|
| 173 |
+
dt = time.perf_counter() - t0
|
| 174 |
+
history.append({"epoch": epoch, "train_loss": ep_loss, "val_r2": val_r2, "sec": dt})
|
| 175 |
+
if verbose and (epoch % log_every == 0):
|
| 176 |
+
print(f"epoch {epoch:3d} loss={ep_loss:.5f} val_R2={val_r2:.5f} lr={sched.get_last_lr()[0]:.2e} {dt:.1f}s")
|
| 177 |
+
|
| 178 |
+
if val_r2 > best_r2:
|
| 179 |
+
best_r2 = val_r2
|
| 180 |
+
best_state = {k: v.detach().cpu().clone() for k, v in model.state_dict().items()}
|
| 181 |
+
best_per_feat = per_feat
|
| 182 |
+
best_oof = (y_true, y_pred)
|
| 183 |
+
bad_epochs = 0
|
| 184 |
+
else:
|
| 185 |
+
bad_epochs += 1
|
| 186 |
+
if bad_epochs >= cfg.patience:
|
| 187 |
+
if verbose:
|
| 188 |
+
print(f"early stop at epoch {epoch} (best val_R2={best_r2:.5f})")
|
| 189 |
+
break
|
| 190 |
+
|
| 191 |
+
return {
|
| 192 |
+
"best_val_r2": best_r2,
|
| 193 |
+
"best_per_feature": best_per_feat,
|
| 194 |
+
"best_state_dict": best_state,
|
| 195 |
+
"history": history,
|
| 196 |
+
"oof": best_oof,
|
| 197 |
+
"val_seq_ids": va_ids,
|
| 198 |
+
"feature_cols": feature_cols,
|
| 199 |
+
}
|
src/utils/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .metrics import compute_manual_r2_score, compute_r2_per_feature, compute_r2_score
|
| 2 |
+
from .reproducibility import make_torch_generator, seed_worker, set_global_seed
|
| 3 |
+
from .hardware import (
|
| 4 |
+
HardwareProfile,
|
| 5 |
+
MaxSafeConfig,
|
| 6 |
+
ResourceLimitError,
|
| 7 |
+
assert_within_max_safe,
|
| 8 |
+
build_hardware_profile,
|
| 9 |
+
calibrate_batch_size,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
'compute_r2_score',
|
| 14 |
+
'compute_r2_per_feature',
|
| 15 |
+
'compute_manual_r2_score',
|
| 16 |
+
'set_global_seed',
|
| 17 |
+
'seed_worker',
|
| 18 |
+
'make_torch_generator',
|
| 19 |
+
'HardwareProfile',
|
| 20 |
+
'MaxSafeConfig',
|
| 21 |
+
'ResourceLimitError',
|
| 22 |
+
'build_hardware_profile',
|
| 23 |
+
'assert_within_max_safe',
|
| 24 |
+
'calibrate_batch_size',
|
| 25 |
+
]
|
src/utils/hardware.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hardware profiling and max-safe guard helpers for local-only experiments."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import ctypes
|
| 6 |
+
import contextlib
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import platform
|
| 10 |
+
import shutil
|
| 11 |
+
import subprocess
|
| 12 |
+
import tempfile
|
| 13 |
+
from dataclasses import asdict, dataclass
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Callable, Iterable, Optional
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
BYTES_PER_GB = 1024**3
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass(frozen=True)
|
| 22 |
+
class MemoryInfo:
|
| 23 |
+
total_gb: float
|
| 24 |
+
available_gb: Optional[float]
|
| 25 |
+
used_fraction: Optional[float]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass(frozen=True)
|
| 29 |
+
class GpuInfo:
|
| 30 |
+
name: str
|
| 31 |
+
memory_total_mb: int
|
| 32 |
+
memory_used_mb: Optional[int]
|
| 33 |
+
temperature_c: Optional[int]
|
| 34 |
+
driver_version: Optional[str]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass(frozen=True)
|
| 38 |
+
class TorchInfo:
|
| 39 |
+
installed: bool
|
| 40 |
+
version: Optional[str]
|
| 41 |
+
cuda_available: bool
|
| 42 |
+
cuda_version: Optional[str]
|
| 43 |
+
device_name: Optional[str]
|
| 44 |
+
device_memory_gb: Optional[float]
|
| 45 |
+
error: Optional[str] = None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass(frozen=True)
|
| 49 |
+
class MaxSafeConfig:
|
| 50 |
+
cpu_workers: int
|
| 51 |
+
dataloader_workers: int
|
| 52 |
+
max_dataloader_workers: int
|
| 53 |
+
ram_limit_fraction: float
|
| 54 |
+
ram_target_gb: float
|
| 55 |
+
gpu_vram_fraction: float
|
| 56 |
+
gpu_target_vram_gb: Optional[float]
|
| 57 |
+
gpu_pause_temp_c: int
|
| 58 |
+
gpu_abort_temp_c: int
|
| 59 |
+
mixed_precision: bool
|
| 60 |
+
gradient_accumulation: bool
|
| 61 |
+
batch_candidates: list[int]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass(frozen=True)
|
| 65 |
+
class HardwareProfile:
|
| 66 |
+
profile_name: str
|
| 67 |
+
platform: str
|
| 68 |
+
python: str
|
| 69 |
+
cpu_name: str
|
| 70 |
+
logical_cpu_count: int
|
| 71 |
+
memory: MemoryInfo
|
| 72 |
+
gpus: list[GpuInfo]
|
| 73 |
+
torch: TorchInfo
|
| 74 |
+
max_safe: MaxSafeConfig
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class ResourceLimitError(RuntimeError):
|
| 78 |
+
"""Raised when a max-safe guard would abort a run."""
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _round_gb(value_bytes: int) -> float:
|
| 82 |
+
return round(value_bytes / BYTES_PER_GB, 2)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def get_memory_info() -> MemoryInfo:
|
| 86 |
+
"""Return physical memory using stdlib-only platform APIs."""
|
| 87 |
+
if platform.system().lower() == "windows":
|
| 88 |
+
class MEMORYSTATUSEX(ctypes.Structure):
|
| 89 |
+
_fields_ = [
|
| 90 |
+
("dwLength", ctypes.c_ulong),
|
| 91 |
+
("dwMemoryLoad", ctypes.c_ulong),
|
| 92 |
+
("ullTotalPhys", ctypes.c_ulonglong),
|
| 93 |
+
("ullAvailPhys", ctypes.c_ulonglong),
|
| 94 |
+
("ullTotalPageFile", ctypes.c_ulonglong),
|
| 95 |
+
("ullAvailPageFile", ctypes.c_ulonglong),
|
| 96 |
+
("ullTotalVirtual", ctypes.c_ulonglong),
|
| 97 |
+
("ullAvailVirtual", ctypes.c_ulonglong),
|
| 98 |
+
("sullAvailExtendedVirtual", ctypes.c_ulonglong),
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
status = MEMORYSTATUSEX()
|
| 102 |
+
status.dwLength = ctypes.sizeof(MEMORYSTATUSEX)
|
| 103 |
+
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status))
|
| 104 |
+
used_fraction = 1.0 - (status.ullAvailPhys / status.ullTotalPhys)
|
| 105 |
+
return MemoryInfo(
|
| 106 |
+
total_gb=_round_gb(status.ullTotalPhys),
|
| 107 |
+
available_gb=_round_gb(status.ullAvailPhys),
|
| 108 |
+
used_fraction=round(used_fraction, 4),
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
if hasattr(os, "sysconf") and "SC_PAGE_SIZE" in os.sysconf_names:
|
| 112 |
+
page_size = os.sysconf("SC_PAGE_SIZE")
|
| 113 |
+
pages = os.sysconf("SC_PHYS_PAGES")
|
| 114 |
+
return MemoryInfo(total_gb=_round_gb(page_size * pages), available_gb=None, used_fraction=None)
|
| 115 |
+
|
| 116 |
+
return MemoryInfo(total_gb=0.0, available_gb=None, used_fraction=None)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def get_nvidia_gpus() -> list[GpuInfo]:
|
| 120 |
+
"""Read NVIDIA GPU status through nvidia-smi when available."""
|
| 121 |
+
if not shutil.which("nvidia-smi"):
|
| 122 |
+
return []
|
| 123 |
+
cmd = [
|
| 124 |
+
"nvidia-smi",
|
| 125 |
+
"--query-gpu=name,memory.total,memory.used,temperature.gpu,driver_version",
|
| 126 |
+
"--format=csv,noheader,nounits",
|
| 127 |
+
]
|
| 128 |
+
try:
|
| 129 |
+
raw = subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL, timeout=10)
|
| 130 |
+
except Exception:
|
| 131 |
+
return []
|
| 132 |
+
|
| 133 |
+
gpus = []
|
| 134 |
+
for line in raw.splitlines():
|
| 135 |
+
parts = [part.strip() for part in line.split(",")]
|
| 136 |
+
if len(parts) < 5:
|
| 137 |
+
continue
|
| 138 |
+
name, total_mb, used_mb, temp_c, driver = parts[:5]
|
| 139 |
+
gpus.append(
|
| 140 |
+
GpuInfo(
|
| 141 |
+
name=name,
|
| 142 |
+
memory_total_mb=int(float(total_mb)),
|
| 143 |
+
memory_used_mb=int(float(used_mb)),
|
| 144 |
+
temperature_c=int(float(temp_c)),
|
| 145 |
+
driver_version=driver,
|
| 146 |
+
)
|
| 147 |
+
)
|
| 148 |
+
return gpus
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def get_torch_info() -> TorchInfo:
|
| 152 |
+
"""Return PyTorch/CUDA availability without making Torch mandatory."""
|
| 153 |
+
try:
|
| 154 |
+
with open(os.devnull, "w", encoding="utf-8") as devnull, contextlib.redirect_stderr(devnull):
|
| 155 |
+
import torch
|
| 156 |
+
|
| 157 |
+
cuda_available = bool(torch.cuda.is_available())
|
| 158 |
+
device_name = None
|
| 159 |
+
device_memory_gb = None
|
| 160 |
+
if cuda_available:
|
| 161 |
+
props = torch.cuda.get_device_properties(0)
|
| 162 |
+
device_name = torch.cuda.get_device_name(0)
|
| 163 |
+
device_memory_gb = round(props.total_memory / BYTES_PER_GB, 2)
|
| 164 |
+
return TorchInfo(
|
| 165 |
+
installed=True,
|
| 166 |
+
version=str(torch.__version__),
|
| 167 |
+
cuda_available=cuda_available,
|
| 168 |
+
cuda_version=getattr(torch.version, "cuda", None),
|
| 169 |
+
device_name=device_name,
|
| 170 |
+
device_memory_gb=device_memory_gb,
|
| 171 |
+
)
|
| 172 |
+
except Exception as exc:
|
| 173 |
+
return TorchInfo(
|
| 174 |
+
installed=False,
|
| 175 |
+
version=None,
|
| 176 |
+
cuda_available=False,
|
| 177 |
+
cuda_version=None,
|
| 178 |
+
device_name=None,
|
| 179 |
+
device_memory_gb=None,
|
| 180 |
+
error=repr(exc),
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def build_max_safe_config(memory: MemoryInfo, gpus: list[GpuInfo]) -> MaxSafeConfig:
|
| 185 |
+
"""Choose conservative max-safe defaults for this laptop class."""
|
| 186 |
+
logical = os.cpu_count() or 1
|
| 187 |
+
cpu_workers = max(1, min(12, logical - 4 if logical > 4 else logical))
|
| 188 |
+
gpu_target_vram_gb = None
|
| 189 |
+
if gpus:
|
| 190 |
+
gpu_target_vram_gb = round((gpus[0].memory_total_mb / 1024) * 0.85, 2)
|
| 191 |
+
return MaxSafeConfig(
|
| 192 |
+
cpu_workers=cpu_workers,
|
| 193 |
+
dataloader_workers=2,
|
| 194 |
+
max_dataloader_workers=4,
|
| 195 |
+
ram_limit_fraction=0.90,
|
| 196 |
+
ram_target_gb=min(13.0, round(memory.total_gb * 0.82, 2)) if memory.total_gb else 13.0,
|
| 197 |
+
gpu_vram_fraction=0.85,
|
| 198 |
+
gpu_target_vram_gb=gpu_target_vram_gb,
|
| 199 |
+
gpu_pause_temp_c=82,
|
| 200 |
+
gpu_abort_temp_c=87,
|
| 201 |
+
mixed_precision=True,
|
| 202 |
+
gradient_accumulation=True,
|
| 203 |
+
batch_candidates=[512, 256, 128, 64, 32, 16, 8],
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def build_hardware_profile(profile_name: str = "max_safe") -> HardwareProfile:
|
| 208 |
+
memory = get_memory_info()
|
| 209 |
+
gpus = get_nvidia_gpus()
|
| 210 |
+
return HardwareProfile(
|
| 211 |
+
profile_name=profile_name,
|
| 212 |
+
platform=platform.platform(),
|
| 213 |
+
python=platform.python_version(),
|
| 214 |
+
cpu_name=platform.processor() or platform.machine(),
|
| 215 |
+
logical_cpu_count=os.cpu_count() or 1,
|
| 216 |
+
memory=memory,
|
| 217 |
+
gpus=gpus,
|
| 218 |
+
torch=get_torch_info(),
|
| 219 |
+
max_safe=build_max_safe_config(memory, gpus),
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def assert_within_max_safe(profile: HardwareProfile) -> None:
|
| 224 |
+
"""Abort before a run if current memory/temperature already violates guardrails."""
|
| 225 |
+
if profile.memory.used_fraction is not None and profile.memory.used_fraction >= profile.max_safe.ram_limit_fraction:
|
| 226 |
+
raise ResourceLimitError(
|
| 227 |
+
f"System RAM usage {profile.memory.used_fraction:.1%} exceeds "
|
| 228 |
+
f"{profile.max_safe.ram_limit_fraction:.0%} guard"
|
| 229 |
+
)
|
| 230 |
+
if profile.gpus:
|
| 231 |
+
gpu = profile.gpus[0]
|
| 232 |
+
if gpu.temperature_c is not None and gpu.temperature_c >= profile.max_safe.gpu_abort_temp_c:
|
| 233 |
+
raise ResourceLimitError(
|
| 234 |
+
f"GPU temperature {gpu.temperature_c}C exceeds "
|
| 235 |
+
f"{profile.max_safe.gpu_abort_temp_c}C abort guard"
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def calibrate_batch_size(
|
| 240 |
+
trial_fn: Callable[[int], None],
|
| 241 |
+
candidates: Iterable[int],
|
| 242 |
+
*,
|
| 243 |
+
repeats: int = 2,
|
| 244 |
+
) -> Optional[int]:
|
| 245 |
+
"""
|
| 246 |
+
Choose the largest candidate batch that survives repeated trial calls.
|
| 247 |
+
|
| 248 |
+
`trial_fn` should raise RuntimeError for OOM-like failures. This helper is
|
| 249 |
+
intentionally framework-agnostic so tests can simulate OOM without Torch.
|
| 250 |
+
"""
|
| 251 |
+
for batch_size in candidates:
|
| 252 |
+
try:
|
| 253 |
+
for _ in range(repeats):
|
| 254 |
+
trial_fn(int(batch_size))
|
| 255 |
+
return int(batch_size)
|
| 256 |
+
except RuntimeError:
|
| 257 |
+
continue
|
| 258 |
+
return None
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def atomic_write_json(path: str | Path, payload: object) -> None:
|
| 262 |
+
"""Write JSON atomically so interrupted runs do not corrupt manifests."""
|
| 263 |
+
path = Path(path)
|
| 264 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 265 |
+
if hasattr(payload, "__dataclass_fields__"):
|
| 266 |
+
payload = asdict(payload)
|
| 267 |
+
with tempfile.NamedTemporaryFile("w", delete=False, dir=str(path.parent), encoding="utf-8") as tmp:
|
| 268 |
+
json.dump(payload, tmp, indent=2, sort_keys=True)
|
| 269 |
+
tmp.write("\n")
|
| 270 |
+
tmp_path = Path(tmp.name)
|
| 271 |
+
tmp_path.replace(path)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def dataclass_to_dict(value: object) -> dict:
|
| 275 |
+
if hasattr(value, "__dataclass_fields__"):
|
| 276 |
+
return asdict(value)
|
| 277 |
+
if isinstance(value, dict):
|
| 278 |
+
return value
|
| 279 |
+
raise TypeError(f"Unsupported JSON payload: {type(value)}")
|
src/utils/metrics.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""R2 metrics for the Wunder Fund RNN Challenge.
|
| 2 |
+
|
| 3 |
+
The authoritative local metric mirrors ``competition_package.utils``:
|
| 4 |
+
``sklearn.metrics.r2_score`` is computed independently per feature and then
|
| 5 |
+
averaged.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
from sklearn.metrics import r2_score
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _validate_shapes(y_true: np.ndarray, y_pred: np.ndarray) -> None:
|
| 15 |
+
if y_true.shape != y_pred.shape:
|
| 16 |
+
raise ValueError(
|
| 17 |
+
f"Shape mismatch: y_true {y_true.shape} vs y_pred {y_pred.shape}"
|
| 18 |
+
)
|
| 19 |
+
if y_true.ndim != 2:
|
| 20 |
+
raise ValueError(f"Expected 2D arrays, got y_true.ndim={y_true.ndim}")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def compute_r2_score(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
| 24 |
+
"""Compute mean per-feature R2 using sklearn, matching the starter pack."""
|
| 25 |
+
return float(np.mean(list(compute_r2_per_feature(y_true, y_pred).values())))
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def compute_r2_per_feature(
|
| 29 |
+
y_true: np.ndarray,
|
| 30 |
+
y_pred: np.ndarray,
|
| 31 |
+
feature_names: list[str] | None = None,
|
| 32 |
+
) -> dict[str, float]:
|
| 33 |
+
"""Compute sklearn R2 independently for each output feature."""
|
| 34 |
+
_validate_shapes(y_true, y_pred)
|
| 35 |
+
n_features = y_true.shape[1]
|
| 36 |
+
if feature_names is None:
|
| 37 |
+
feature_names = [f"feature_{i}" for i in range(n_features)]
|
| 38 |
+
if len(feature_names) != n_features:
|
| 39 |
+
raise ValueError("feature_names length must match prediction width")
|
| 40 |
+
|
| 41 |
+
return {
|
| 42 |
+
feature_names[i]: float(r2_score(y_true[:, i], y_pred[:, i]))
|
| 43 |
+
for i in range(n_features)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def compute_manual_r2_score(
|
| 48 |
+
y_true: np.ndarray,
|
| 49 |
+
y_pred: np.ndarray,
|
| 50 |
+
epsilon: float = 1e-8,
|
| 51 |
+
) -> float:
|
| 52 |
+
"""Legacy epsilon-stabilized R2 helper for diagnostics only."""
|
| 53 |
+
_validate_shapes(y_true, y_pred)
|
| 54 |
+
scores = []
|
| 55 |
+
for i in range(y_true.shape[1]):
|
| 56 |
+
ss_res = np.sum((y_true[:, i] - y_pred[:, i]) ** 2)
|
| 57 |
+
ss_tot = np.sum((y_true[:, i] - y_true[:, i].mean()) ** 2)
|
| 58 |
+
scores.append(1 - (ss_res / (ss_tot + epsilon)))
|
| 59 |
+
return float(np.mean(scores))
|
src/utils/reproducibility.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic runtime helpers for Phase 2 experiments."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import random
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def set_global_seed(seed: int, deterministic_torch: bool = True, seed_torch: bool = True) -> None:
|
| 13 |
+
"""Seed Python, NumPy, and PyTorch when available."""
|
| 14 |
+
os.environ["PYTHONHASHSEED"] = str(seed)
|
| 15 |
+
random.seed(seed)
|
| 16 |
+
np.random.seed(seed)
|
| 17 |
+
|
| 18 |
+
if not seed_torch:
|
| 19 |
+
return
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
import torch
|
| 23 |
+
|
| 24 |
+
torch.manual_seed(seed)
|
| 25 |
+
if torch.cuda.is_available():
|
| 26 |
+
torch.cuda.manual_seed_all(seed)
|
| 27 |
+
if deterministic_torch:
|
| 28 |
+
torch.use_deterministic_algorithms(True, warn_only=True)
|
| 29 |
+
torch.backends.cudnn.benchmark = False
|
| 30 |
+
torch.backends.cudnn.deterministic = True
|
| 31 |
+
except ImportError:
|
| 32 |
+
return
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def seed_worker(worker_id: int) -> None:
|
| 36 |
+
"""Seed DataLoader workers from PyTorch's worker seed."""
|
| 37 |
+
try:
|
| 38 |
+
import torch
|
| 39 |
+
|
| 40 |
+
worker_seed = torch.initial_seed() % 2**32
|
| 41 |
+
except ImportError:
|
| 42 |
+
worker_seed = worker_id
|
| 43 |
+
np.random.seed(worker_seed)
|
| 44 |
+
random.seed(worker_seed)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def make_torch_generator(seed: int) -> Optional[object]:
|
| 48 |
+
"""Return a seeded torch.Generator, or None if torch is unavailable."""
|
| 49 |
+
try:
|
| 50 |
+
import torch
|
| 51 |
+
|
| 52 |
+
generator = torch.Generator()
|
| 53 |
+
generator.manual_seed(seed)
|
| 54 |
+
return generator
|
| 55 |
+
except ImportError:
|
| 56 |
+
return None
|