| """Competition submission entrypoint — streaming global-norm GRU ensemble (CPU/ONNX). |
| |
| Reads ensemble_config.json (next to this file): |
| { |
| "global_mean": [...32...], "global_std": [...32...], |
| "models": [{"onnx": "m1.onnx", "scale": [s0, s1]}, ...] |
| } |
| Each model is a stateful one-step GRU exported by export_gru_onnx.py. Predictions |
| are scale-normalized per model and averaged (weighted-Pearson is shift/scale |
| invariant, so only relative scale matters). Recurrent state resets per sequence. |
| """ |
| from __future__ import annotations |
|
|
| import json |
| import os |
| from pathlib import Path |
| from typing import List, Optional |
|
|
| import numpy as np |
|
|
| os.environ.setdefault("OMP_NUM_THREADS", "1") |
| os.environ.setdefault("MKL_NUM_THREADS", "1") |
| os.environ.setdefault("OPENBLAS_NUM_THREADS", "1") |
|
|
| import onnxruntime as ort |
|
|
| from utils import DataPoint |
|
|
| RAW_FEATURE_SIZE = 32 |
| |
| |
| |
| ROLLING_WINDOWS = (5, 10, 20, 40) |
|
|
|
|
| class _OnlineGlobalFeatures: |
| def __init__(self, mean, std): |
| self.mean = np.asarray(mean, dtype=np.float32) |
| self.std = np.asarray(std, dtype=np.float32) |
| self.maxw = max(ROLLING_WINDOWS) |
| self.reset() |
|
|
| def reset(self): |
| self._hist: List[np.ndarray] = [] |
| self._prev: Optional[np.ndarray] = None |
|
|
| def update(self, raw_state): |
| x = np.asarray(raw_state, dtype=np.float32).reshape(-1) |
| norm = (x - self.mean) / self.std |
| if self._prev is None: |
| lag1 = np.zeros(RAW_FEATURE_SIZE, dtype=np.float32) |
| delta = np.zeros(RAW_FEATURE_SIZE, dtype=np.float32) |
| else: |
| lag1 = self._prev |
| delta = norm - self._prev |
| self._hist.append(norm) |
| if len(self._hist) > self.maxw: |
| self._hist = self._hist[-self.maxw:] |
| parts = [norm, lag1, delta] |
| hist = np.asarray(self._hist, dtype=np.float32) |
| for W in ROLLING_WINDOWS: |
| parts.append(hist[-W:].mean(axis=0).astype(np.float32)) |
| self._prev = norm |
| return np.concatenate(parts).astype(np.float32) |
|
|
|
|
| class _Model: |
| def __init__(self, onnx_path: Path): |
| so = ort.SessionOptions() |
| so.intra_op_num_threads = 1 |
| so.inter_op_num_threads = 1 |
| so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL |
| self.sess = ort.InferenceSession(str(onnx_path), so, providers=["CPUExecutionProvider"]) |
| ins = self.sess.get_inputs() |
| self.in_feat = ins[0].name |
| self.in_h = ins[1].name |
| self.in_size = int(ins[0].shape[2]) |
| hshape = ins[1].shape |
| self.L = int(hshape[0]); self.H = int(hshape[2]) |
| self.out_names = [o.name for o in self.sess.get_outputs()] |
| self.reset() |
|
|
| def reset(self): |
| self.h = np.zeros((self.L, 1, self.H), dtype=np.float32) |
|
|
| def step(self, feat_1x1xF: np.ndarray) -> np.ndarray: |
| x = feat_1x1xF[:, :, : self.in_size] |
| pred, h1 = self.sess.run(self.out_names, {self.in_feat: x, self.in_h: self.h}) |
| self.h = h1.astype(np.float32) |
| return pred[0].astype(np.float32) |
|
|
|
|
| class PredictionModel: |
| def __init__(self, config_path: Optional[str] = None): |
| base = Path(__file__).resolve().parent |
| cfg = json.loads((base / (config_path or "ensemble_config.json")).read_text()) |
| self.feats = _OnlineGlobalFeatures(cfg["global_mean"], cfg["global_std"]) |
| self.models = [_Model(base / m["onnx"]) for m in cfg["models"]] |
| |
| self.weights = np.asarray([m["weight"] for m in cfg["models"]], dtype=np.float32) |
| self.current_seq_ix: Optional[int] = None |
|
|
| def _reset(self): |
| self.feats.reset() |
| for m in self.models: |
| m.reset() |
|
|
| def predict(self, data_point: DataPoint): |
| if self.current_seq_ix != data_point.seq_ix: |
| self._reset() |
| self.current_seq_ix = data_point.seq_ix |
| feat = self.feats.update(data_point.state) |
| x = feat.reshape(1, 1, -1).astype(np.float32) |
| |
| acc = np.zeros(2, dtype=np.float32) |
| for i, m in enumerate(self.models): |
| acc += self.weights[i] * m.step(x) |
| if not data_point.need_prediction: |
| return None |
| return np.clip(acc, -6.0, 6.0).astype(np.float32) |
|
|
|
|
| if __name__ == "__main__": |
| from utils import ScorerStepByStep |
| data_path = Path(__file__).resolve().parent / "data" / "valid.parquet" |
| model = PredictionModel() |
| scorer = ScorerStepByStep(str(data_path)) |
| print(scorer.score(model)) |
|
|