import json import numpy as np import pandas as pd import torch import torch.nn as nn import joblib from typing import Optional, Dict, Any from huggingface_hub import hf_hub_download class LSTMClassifier(nn.Module): def __init__(self, input_size: int, hidden_size: int = 64, num_layers: int = 1, dropout: float = 0.0): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0.0, bidirectional=False ) self.head = nn.Linear(hidden_size, 2) def forward(self, x): _, (h_n, _) = self.lstm(x) last_h = h_n[-1] return self.head(last_h) def load_model_and_scaler(repo_id: str, revision: Optional[str] = None, device: Optional[str] = None): cfg_path = hf_hub_download(repo_id, "config.json", revision=revision) scaler_path = hf_hub_download(repo_id, "scaler.joblib", revision=revision) with open(cfg_path, "r", encoding="utf-8") as f: cfg = json.load(f) device = device or ("cuda" if torch.cuda.is_available() else "cpu") model = LSTMClassifier( input_size=int(cfg["input_size"]), hidden_size=int(cfg["hidden_size"]), num_layers=int(cfg["num_layers"]), dropout=float(cfg["dropout"]), ).to(device) weights_name = cfg.get("weights_file", "model.safetensors") weights_path = hf_hub_download(repo_id, weights_name, revision=revision) if weights_name.endswith(".safetensors"): from safetensors.torch import load_file state = load_file(weights_path) model.load_state_dict({k: v for k, v in state.items()}, strict=True) else: state = torch.load(weights_path, map_location="cpu") model.load_state_dict(state, strict=True) model.eval() scaler = joblib.load(scaler_path) return model, scaler, cfg def predict_df(df: pd.DataFrame, model: nn.Module, scaler, cfg: Dict[str, Any]) -> np.ndarray: from numpy.lib.stride_tricks import sliding_window_view feature_cols = cfg["feature_cols"] W = int(cfg["window_size"]) stride = int(cfg.get("stride", 1)) X = df[feature_cols].to_numpy(np.float32) if len(X) < W: return np.empty((0,), dtype=np.int64) Xw = sliding_window_view(X, window_shape=(W, X.shape[1])).squeeze(1) Xw = Xw[::stride] F = Xw.shape[2] Xw_scaled = scaler.transform(Xw.reshape(-1, F)).reshape(Xw.shape).astype(np.float32) device = next(model.parameters()).device with torch.no_grad(): xb = torch.tensor(Xw_scaled, device=device) logits = model(xb) y_pred = torch.argmax(logits, dim=1).detach().cpu().numpy() return y_pred