| 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 | |