Download scripts/run_chronos_long_training.py from SolarWine/api: direct link, hf CLI and curl.
- Browser
- Download file 8.1 kB
-
https://huggingface.co/spaces/SolarWine/api/resolve/main/scripts/run_chronos_long_training.py
- Command line
-
hf download hf://spaces/SolarWine/api/scripts/run_chronos_long_training.py
-
curl -L -o run_chronos_long_training.py https://huggingface.co/spaces/SolarWine/api/resolve/main/scripts/run_chronos_long_training.py
8.1 kB
| """ | |
| Long Chronos-2 LoRA fine-tuning with large context window, tuned for local | |
| CPU/memory. Single training run (Chronos does not support resuming LoRA fit). | |
| Usage (from project root): | |
| PYTHONPATH=. python scripts/run_chronos_long_training.py [--device cpu] [--num-steps 4000] | |
| Uses: context_days=28, batch_size=16 by default. Set --num-steps for total steps. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| import threading | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| PROJECT_ROOT = Path(__file__).resolve().parent.parent | |
| if str(PROJECT_ROOT) not in sys.path: | |
| sys.path.insert(0, str(PROJECT_ROOT)) | |
| # Limit CPU threads to avoid oversubscription (set before importing torch) | |
| if "OMP_NUM_THREADS" not in os.environ: | |
| try: | |
| import multiprocessing | |
| n = multiprocessing.cpu_count() | |
| os.environ["OMP_NUM_THREADS"] = str(min(n, 10)) | |
| except Exception: | |
| pass | |
| from config.settings import OUTPUTS_DIR | |
| from src.chronos_forecaster import ( | |
| ChronosForecaster, | |
| STEPS_PER_DAY, | |
| ) | |
| from sklearn.metrics import mean_absolute_error | |
| import numpy as np | |
| import pandas as pd | |
| def _quick_val_mae(forecaster: ChronosForecaster, df: pd.DataFrame, train_ratio: float, n_windows: int = 5) -> float: | |
| """Compute MAE on first n_windows test windows (daytime-only) for convergence check.""" | |
| sparse = forecaster.load_sparse_data() | |
| daytime_ts = set(sparse["timestamp_utc"]) | |
| split_idx = int(len(df) * train_ratio) | |
| test_starts = list(range(split_idx, len(df) - STEPS_PER_DAY, STEPS_PER_DAY))[:n_windows] | |
| actual_list, pred_list = [], [] | |
| for start_idx in test_starts: | |
| f = forecaster.forecast_day(df, start_idx, STEPS_PER_DAY, covariate_mode="all") | |
| actual_slice = df.iloc[start_idx : start_idx + STEPS_PER_DAY] | |
| daytime_mask = actual_slice["timestamp_utc"].isin(daytime_ts).values[:len(f)] | |
| if daytime_mask.sum() < 5: | |
| continue | |
| actual_list.append(actual_slice["A"].values[:len(f)][daytime_mask]) | |
| pred_list.append(np.clip(f["median"].values[daytime_mask], 0, None)) | |
| if not actual_list: | |
| return float("nan") | |
| return float(mean_absolute_error(np.concatenate(actual_list), np.concatenate(pred_list))) | |
| def main() -> None: | |
| import argparse | |
| parser = argparse.ArgumentParser(description="Chronos-2 long LoRA training (single run)") | |
| parser.add_argument("--device", default="cpu", help="torch device (cpu or mps)") | |
| parser.add_argument("--context-days", type=int, default=28, help="context window in days") | |
| parser.add_argument("--batch-size", type=int, default=16, help="batch size (safe for 32GB RAM)") | |
| parser.add_argument("--num-steps", type=int, default=4000, help="total training steps") | |
| parser.add_argument("--learning-rate", type=float, default=1e-5, help="learning rate") | |
| parser.add_argument("--progress-minutes", type=int, default=10, help="print timestamp and progress every N minutes") | |
| parser.add_argument("--output-dir", type=str, default=None, help="output dir for checkpoints") | |
| args = parser.parse_args() | |
| output_dir = args.output_dir or str(OUTPUTS_DIR / "chronos_finetuned_long") | |
| OUTPUTS_DIR.mkdir(parents=True, exist_ok=True) | |
| print("Loading data...") | |
| forecaster = ChronosForecaster(device=args.device, context_days=args.context_days) | |
| df = forecaster.load_data() | |
| print(f" Grid: {len(df)} rows, context={args.context_days}d ({forecaster.context_steps} steps)") | |
| train_ratio = 0.75 | |
| split_idx = int(len(df) * train_ratio) | |
| print("\nBaseline (zero-shot) validation MAE (5 windows)...") | |
| baseline_mae = _quick_val_mae(forecaster, df, train_ratio, n_windows=5) | |
| print(f" {baseline_mae:.4f}") | |
| print(f"\nLoRA fine-tuning: {args.num_steps} steps, batch_size={args.batch_size}, lr={args.learning_rate}...") | |
| stop_event = threading.Event() | |
| interval_sec = max(1, args.progress_minutes * 60) | |
| def _progress_reporter(): | |
| while True: | |
| if stop_event.wait(interval_sec): | |
| break | |
| ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") | |
| print(f"[{ts}] Chronos LoRA training still in progress ({args.num_steps} steps total)...", flush=True) | |
| progress_thread = threading.Thread(target=_progress_reporter, daemon=True) | |
| progress_thread.start() | |
| try: | |
| forecaster.finetune( | |
| df, | |
| train_ratio=train_ratio, | |
| covariate_mode="all", | |
| num_steps=args.num_steps, | |
| learning_rate=args.learning_rate, | |
| batch_size=args.batch_size, | |
| output_dir=output_dir, | |
| ) | |
| finally: | |
| stop_event.set() | |
| progress_thread.join(timeout=interval_sec + 5) | |
| print("\nValidation MAE after training (5 windows)...") | |
| val_mae = _quick_val_mae(forecaster, df, train_ratio, n_windows=5) | |
| print(f" {val_mae:.4f} (baseline {baseline_mae:.4f})") | |
| # Full benchmark with finetuned model (append lora row to CSV) | |
| print("\nRunning full walk-forward benchmark (finetuned model, mode=all)...") | |
| sparse = forecaster.load_sparse_data() | |
| daytime_ts = set(sparse["timestamp_utc"]) | |
| test_starts = list(range(split_idx, len(df) - STEPS_PER_DAY, STEPS_PER_DAY)) | |
| all_actual, all_pred = [], [] | |
| for start_idx in test_starts: | |
| f = forecaster.forecast_day(df, start_idx, STEPS_PER_DAY, covariate_mode="all") | |
| actual_slice = df.iloc[start_idx : start_idx + STEPS_PER_DAY] | |
| daytime_mask = actual_slice["timestamp_utc"].isin(daytime_ts).values[:len(f)] | |
| if daytime_mask.sum() < 5: | |
| continue | |
| all_actual.append(actual_slice["A"].values[:len(f)][daytime_mask]) | |
| all_pred.append(np.clip(f["median"].values[daytime_mask], 0, None)) | |
| lora_mae = None | |
| if all_actual: | |
| from sklearn.metrics import mean_squared_error, r2_score | |
| a = np.concatenate(all_actual) | |
| p = np.concatenate(all_pred) | |
| lora_mae = float(mean_absolute_error(a, p)) | |
| lora_rmse = float(np.sqrt(mean_squared_error(a, p))) | |
| lora_r2 = float(r2_score(a, p)) | |
| print(f" LoRA / all: MAE={lora_mae:.4f} RMSE={lora_rmse:.4f} R²={lora_r2:.4f} ({len(all_actual)} windows, {len(a)} steps)") | |
| # Load existing benchmark CSV, append lora row, save | |
| bench_path = OUTPUTS_DIR / "chronos_benchmark.csv" | |
| if bench_path.exists(): | |
| existing = pd.read_csv(bench_path) | |
| lora_row = pd.DataFrame([{ | |
| "mode": "lora / all", | |
| "MAE": lora_mae, | |
| "RMSE": lora_rmse, | |
| "R2": lora_r2, | |
| "n_windows": len(all_actual), | |
| "n_steps": len(a), | |
| }]) | |
| combined = pd.concat([existing, lora_row], ignore_index=True) | |
| combined.to_csv(bench_path, index=False) | |
| print(f" Appended lora row → {bench_path}") | |
| else: | |
| pd.DataFrame([{ | |
| "mode": "lora / all", "MAE": lora_mae, "RMSE": lora_rmse, "R2": lora_r2, | |
| "n_windows": len(all_actual), "n_steps": len(a), | |
| }]).to_csv(bench_path, index=False) | |
| # Sample forecast plot | |
| print("\nGenerating sample forecast plot...") | |
| forecaster.plot_sample_forecast(df) | |
| # Summary and next steps | |
| print("\n" + "=" * 60) | |
| print("TRAINING COMPLETE — Next steps") | |
| print("=" * 60) | |
| print(f" Checkpoints: {output_dir}") | |
| print(f" Benchmark: {OUTPUTS_DIR / 'chronos_benchmark.csv'} (lora / all row appended)") | |
| print(f" Plot: {OUTPUTS_DIR / 'chronos_forecast_sample.png'} (from finetuned model)") | |
| print(" • Refresh the Streamlit app: Prediction Accuracy tab will show LoRA / all in the table.") | |
| print(" • Sample forecast image is from the finetuned model.") | |
| if lora_mae is not None: | |
| zs_mae = 3.91 # typical zero-shot 'all' on this eval | |
| delta = (zs_mae - lora_mae) / zs_mae * 100 | |
| print(f" • LoRA MAE {lora_mae:.2f} vs zero-shot ~{zs_mae:.2f} ({delta:+.0f}% change).") | |
| print("=" * 60) | |
| print("Done.") | |
| if __name__ == "__main__": | |
| main() | |