Spaces:
Running
Running
Download predictor.py from Kurtobe/kronos-filter: direct link, hf CLI and curl.
- Browser
- Download file 9.48 kB
-
https://huggingface.co/spaces/Kurtobe/kronos-filter/resolve/main/predictor.py
- Command line
-
hf download hf://spaces/Kurtobe/kronos-filter/predictor.py
-
curl -L -o predictor.py https://huggingface.co/spaces/Kurtobe/kronos-filter/resolve/main/predictor.py
9.48 kB
| """ | |
| predictor.py — Kronos inference that retains ALL sampled paths. | |
| Same as the local version but imports Kronos from the `Kronos/` subfolder | |
| that the Space's pre_build step clones from GitHub. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| import logging | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| # Kronos model code lives at ./model/ in this Space (copied from upstream) | |
| SPACE_ROOT = Path(__file__).resolve().parent | |
| if str(SPACE_ROOT) not in sys.path: | |
| sys.path.insert(0, str(SPACE_ROOT)) | |
| from model import Kronos, KronosTokenizer, KronosPredictor # noqa: E402 | |
| from model.kronos import sample_from_logits, calc_time_stamps # noqa: E402 | |
| logger = logging.getLogger(__name__) | |
| MODEL_IDS = { | |
| "mini": ("NeoQuasar/Kronos-Tokenizer-2k", "NeoQuasar/Kronos-mini", 2048), | |
| "small": ("NeoQuasar/Kronos-Tokenizer-base", "NeoQuasar/Kronos-small", 512), | |
| "base": ("NeoQuasar/Kronos-Tokenizer-base", "NeoQuasar/Kronos-base", 512), | |
| } | |
| def _auto_regressive_inference_keep_paths( | |
| tokenizer, model, | |
| x, x_stamp, y_stamp, | |
| max_context: int, pred_len: int, | |
| clip: float = 5.0, | |
| T: float = 1.0, | |
| top_k: int = 0, | |
| top_p: float = 0.9, | |
| sample_count: int = 5, | |
| verbose: bool = False, | |
| ): | |
| """Copy of Kronos's auto_regressive_inference with averaging removed.""" | |
| try: | |
| from tqdm import trange | |
| except ImportError: | |
| trange = range | |
| with torch.no_grad(): | |
| x = torch.clip(x, -clip, clip) | |
| device = x.device | |
| x = x.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x.size(1), x.size(2)).to(device) | |
| x_stamp = x_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x_stamp.size(1), x_stamp.size(2)).to(device) | |
| y_stamp = y_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, y_stamp.size(1), y_stamp.size(2)).to(device) | |
| x_token = tokenizer.encode(x, half=True) | |
| initial_seq_len = x.size(1) | |
| batch_size = x_token[0].size(0) | |
| total_seq_len = initial_seq_len + pred_len | |
| full_stamp = torch.cat([x_stamp, y_stamp], dim=1) | |
| generated_pre = x_token[0].new_empty(batch_size, pred_len) | |
| generated_post = x_token[1].new_empty(batch_size, pred_len) | |
| pre_buffer = x_token[0].new_zeros(batch_size, max_context) | |
| post_buffer = x_token[1].new_zeros(batch_size, max_context) | |
| buffer_len = min(initial_seq_len, max_context) | |
| if buffer_len > 0: | |
| start_idx = max(0, initial_seq_len - max_context) | |
| pre_buffer[:, :buffer_len] = x_token[0][:, start_idx:start_idx + buffer_len] | |
| post_buffer[:, :buffer_len] = x_token[1][:, start_idx:start_idx + buffer_len] | |
| ran = trange if verbose else range | |
| for i in ran(pred_len): | |
| current_seq_len = initial_seq_len + i | |
| window_len = min(current_seq_len, max_context) | |
| if current_seq_len <= max_context: | |
| input_tokens = [ | |
| pre_buffer[:, :window_len], | |
| post_buffer[:, :window_len], | |
| ] | |
| else: | |
| input_tokens = [pre_buffer, post_buffer] | |
| context_end = current_seq_len | |
| context_start = max(0, context_end - max_context) | |
| current_stamp = full_stamp[:, context_start:context_end, :].contiguous() | |
| s1_logits, context = model.decode_s1(input_tokens[0], input_tokens[1], current_stamp) | |
| s1_logits = s1_logits[:, -1, :] | |
| sample_pre = sample_from_logits(s1_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True) | |
| s2_logits = model.decode_s2(context, sample_pre) | |
| s2_logits = s2_logits[:, -1, :] | |
| sample_post = sample_from_logits(s2_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True) | |
| generated_pre[:, i] = sample_pre.squeeze(-1) | |
| generated_post[:, i] = sample_post.squeeze(-1) | |
| if current_seq_len < max_context: | |
| pre_buffer[:, current_seq_len] = sample_pre.squeeze(-1) | |
| post_buffer[:, current_seq_len] = sample_post.squeeze(-1) | |
| else: | |
| pre_buffer.copy_(torch.roll(pre_buffer, shifts=-1, dims=1)) | |
| post_buffer.copy_(torch.roll(post_buffer, shifts=-1, dims=1)) | |
| pre_buffer[:, -1] = sample_pre.squeeze(-1) | |
| post_buffer[:, -1] = sample_post.squeeze(-1) | |
| full_pre = torch.cat([x_token[0], generated_pre], dim=1) | |
| full_post = torch.cat([x_token[1], generated_post], dim=1) | |
| context_start = max(0, total_seq_len - max_context) | |
| input_tokens = [ | |
| full_pre[:, context_start:total_seq_len].contiguous(), | |
| full_post[:, context_start:total_seq_len].contiguous(), | |
| ] | |
| z = tokenizer.decode(input_tokens, half=True) | |
| z = z.reshape(-1, sample_count, z.size(1), z.size(2)) | |
| # NOT averaging; keep all paths | |
| preds = z.cpu().numpy() | |
| return preds | |
| class KronosPathsPredictor: | |
| def __init__(self, model_size: str = "small", device: str | None = None): | |
| if model_size not in MODEL_IDS: | |
| raise ValueError(f"model_size must be one of {list(MODEL_IDS.keys())}") | |
| tokenizer_id, model_id, max_context = MODEL_IDS[model_size] | |
| self.max_context = max_context | |
| self.model_size = model_size | |
| self.model_id = model_id | |
| if device is None: | |
| if torch.cuda.is_available(): | |
| device = "cuda:0" | |
| elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): | |
| device = "mps" | |
| else: | |
| device = "cpu" | |
| self.device = device | |
| logger.info(f"Loading tokenizer: {tokenizer_id}") | |
| self.tokenizer = KronosTokenizer.from_pretrained(tokenizer_id) | |
| logger.info(f"Loading model: {model_id}") | |
| self.model = Kronos.from_pretrained(model_id) | |
| self.tokenizer = self.tokenizer.to(self.device) | |
| self.model = self.model.to(self.device) | |
| self.price_cols = ["open", "high", "low", "close"] | |
| self.vol_col = "volume" | |
| self.amt_col = "amount" | |
| self.clip = 5.0 | |
| def _prepare_one(self, df: pd.DataFrame, x_timestamp: pd.Series, y_timestamp: pd.Series): | |
| df = df.copy() | |
| if self.vol_col not in df.columns: | |
| df[self.vol_col] = 0.0 | |
| df[self.amt_col] = 0.0 | |
| if self.amt_col not in df.columns: | |
| df[self.amt_col] = df[self.vol_col] * df[self.price_cols].mean(axis=1) | |
| if df[self.price_cols + [self.vol_col, self.amt_col]].isnull().values.any(): | |
| raise ValueError("DataFrame contains NaN in price/volume columns") | |
| x_time = calc_time_stamps(x_timestamp) | |
| y_time = calc_time_stamps(y_timestamp) | |
| x = df[self.price_cols + [self.vol_col, self.amt_col]].values.astype(np.float32) | |
| x_stamp = x_time.values.astype(np.float32) | |
| y_stamp = y_time.values.astype(np.float32) | |
| x_mean = np.mean(x, axis=0) | |
| x_std = np.std(x, axis=0) | |
| x_norm = (x - x_mean) / (x_std + 1e-5) | |
| x_norm = np.clip(x_norm, -self.clip, self.clip) | |
| return x_norm, x_stamp, y_stamp, x_mean, x_std | |
| def predict_paths( | |
| self, | |
| df_list, x_timestamp_list, y_timestamp_list, | |
| pred_len: int, | |
| T: float = 1.0, top_k: int = 0, top_p: float = 0.9, | |
| sample_count: int = 30, | |
| verbose: bool = False, | |
| seed: int | None = None, | |
| ): | |
| if seed is not None: | |
| torch.manual_seed(seed) | |
| np.random.seed(seed) | |
| if not (len(df_list) == len(x_timestamp_list) == len(y_timestamp_list)): | |
| raise ValueError("df_list / x_timestamp_list / y_timestamp_list must be same length") | |
| seq_lens = [len(df) for df in df_list] | |
| y_lens = [len(ts) for ts in y_timestamp_list] | |
| if len(set(seq_lens)) > 1: | |
| raise ValueError(f"All dfs must have same historical length, got {seq_lens}") | |
| if len(set(y_lens)) > 1: | |
| raise ValueError(f"All y_timestamps must have same length, got {y_lens}") | |
| x_batch, x_stamp_batch, y_stamp_batch, means, stds = [], [], [], [], [] | |
| for df, x_ts, y_ts in zip(df_list, x_timestamp_list, y_timestamp_list): | |
| x_norm, x_stamp, y_stamp, m, s = self._prepare_one(df, x_ts, y_ts) | |
| x_batch.append(x_norm); x_stamp_batch.append(x_stamp); y_stamp_batch.append(y_stamp) | |
| means.append(m); stds.append(s) | |
| x_batch = np.stack(x_batch, axis=0) | |
| x_stamp_batch = np.stack(x_stamp_batch, axis=0) | |
| y_stamp_batch = np.stack(y_stamp_batch, axis=0) | |
| x_tensor = torch.from_numpy(x_batch.astype(np.float32)).to(self.device) | |
| x_stamp_tensor = torch.from_numpy(x_stamp_batch.astype(np.float32)).to(self.device) | |
| y_stamp_tensor = torch.from_numpy(y_stamp_batch.astype(np.float32)).to(self.device) | |
| preds_all = _auto_regressive_inference_keep_paths( | |
| self.tokenizer, self.model, | |
| x_tensor, x_stamp_tensor, y_stamp_tensor, | |
| self.max_context, pred_len, | |
| self.clip, T, top_k, top_p, sample_count, verbose, | |
| ) | |
| results = [] | |
| for i, (m, s) in enumerate(zip(means, stds)): | |
| pred = preds_all[i] | |
| pred = pred[:, -pred_len:, :] | |
| pred = pred * (s + 1e-5) + m | |
| results.append(pred.astype(np.float32)) | |
| return results | |