Add unified multi-task model (single checkpoint, 7 domains)
Browse files- README.md +120 -70
- config_weather.json +18 -0
- data.py +1 -4
- model.py +38 -0
- model_weather.pt +3 -0
- predict.py +77 -8
- train_unified.py +187 -0
- unified_config.json +74 -0
- unified_model.pt +3 -0
README.md
CHANGED
|
@@ -1,89 +1,139 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
-
|
| 8 |
-
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
-
|
| 13 |
-
|
| 14 |
-
- mae
|
| 15 |
-
- mape
|
| 16 |
-
pipeline_tag: time-series-forecasting
|
| 17 |
-
---
|
| 18 |
-
|
| 19 |
-
# Future Prediction Models (Multi-Domain LSTM)
|
| 20 |
-
|
| 21 |
-
An end-to-end multi-domain forecasting system. For each of **7 topics** it automatically fetches a real dataset, trains a 2-layer LSTM, evaluates on a held-out test set (no leakage), and generates ChatGPT-style future predictions.
|
| 22 |
-
|
| 23 |
-
## Topics & validated accuracy
|
| 24 |
-
|
| 25 |
-
| Topic | Asset | Source | H1 MAPE | vs naive |
|
| 26 |
-
|---|---|---|---|---|
|
| 27 |
-
| AI | NVIDIA daily close | Yahoo Finance | 1.86% | ~naive |
|
| 28 |
-
| Economy | S&P 500 daily close | Yahoo Finance | 0.64% | beats naive 1.8% |
|
| 29 |
-
| Energy | WTI crude oil close | Yahoo Finance | 2.83% | ~naive |
|
| 30 |
-
| Finance | Bitcoin BTC-USD | Yahoo Finance | 1.52% | ~naive |
|
| 31 |
-
| Programming | `react` npm downloads | npm registry | 7.12% | **beats naive 77.5%** |
|
| 32 |
-
| Sports | ATP world #1 Elo | Hugging Face tennis | 0.06% | ~naive |
|
| 33 |
-
| Weather | Daily mean temperature | Open-Meteo | 0.18% | beats naive 1.9% |
|
| 34 |
-
|
| 35 |
-
> Markets behave near a random walk, so 100% accuracy is impossible — these are honest, validated numbers.
|
| 36 |
-
|
| 37 |
-
## Quick start
|
| 38 |
-
|
| 39 |
-
```bash
|
| 40 |
-
pip install -r requirements.txt
|
| 41 |
-
python data.py --topic finance --refresh # fetch real data
|
| 42 |
-
python train.py --topic finance # train the LSTM
|
| 43 |
-
python ask.py "what will bitcoin do next week?"
|
| 44 |
-
python ask.py "compare all topics"
|
| 45 |
```
|
| 46 |
|
| 47 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
```
|
| 50 |
user question -> intent detection (response.py)
|
| 51 |
-> prediction_engine.py (structured metrics, prints nothing)
|
| 52 |
-
-> response_formatter.py (natural summary + Markdown)
|
| 53 |
-
-> ask.py (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
```
|
| 55 |
|
| 56 |
-
|
| 57 |
-
- `net_change_pct` (first → last forecast along the path)
|
| 58 |
-
- `forecast_range_pct` (max − min of the forecast path)
|
| 59 |
-
- `current_to_forecast_pct` (final forecast vs latest observed value)
|
| 60 |
-
- `direction` (rising / falling / sideways, configurable threshold)
|
| 61 |
|
| 62 |
-
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
|
| 66 |
-
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
## Weather for any city
|
| 73 |
|
| 74 |
-
```
|
| 75 |
-
|
| 76 |
-
python data.py --topic weather --refresh
|
| 77 |
-
python train.py --topic weather
|
| 78 |
-
python ask.py "predict weather in chennai tomorrow"
|
| 79 |
```
|
| 80 |
|
| 81 |
-
##
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
-
|
| 88 |
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Future Prediction Models (Global: 7 topics)
|
| 2 |
+
|
| 3 |
+
An end-to-end multi-domain AI forecasting system. Real datasets, two model modes, ChatGPT-style predictions.
|
| 4 |
+
|
| 5 |
+
## Models: separate vs unified
|
| 6 |
+
|
| 7 |
+
- **Separate** (`model_<topic>.pt`) — 7 dedicated 2-layer LSTMs, one per topic
|
| 8 |
+
- **Unified** (`unified_model.pt`) — ONE combined model for all 7 domains: shared LSTM backbone + per-domain embedding + per-domain heads, trained jointly on 31,735 windows from every topic
|
| 9 |
+
|
| 10 |
+
```powershell
|
| 11 |
+
python train_unified.py # train the single combined model
|
| 12 |
+
python predict.py --topic finance --mode unified # query it for any topic
|
| 13 |
+
python ask.py "compare all topics" # chat uses unified automatically when available
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
```
|
| 15 |
|
| 16 |
+
## Topics & datasets (all real, fetched automatically)
|
| 17 |
+
|
| 18 |
+
| Topic | Asset | Source | Points |
|
| 19 |
+
|---|---|---|---|
|
| 20 |
+
| **AI** | NVIDIA daily close | Yahoo Finance | 6,933 |
|
| 21 |
+
| **Programming** | Daily `react` npm downloads | npm registry API | 547 |
|
| 22 |
+
| **Finance** | Bitcoin BTC-USD close | Yahoo Finance (Hugging Face fallback) | 4,247 |
|
| 23 |
+
| **Sports** | ATP world #1 Elo rating | Hugging Face tennis (93,028 matches, Elo computed from results) | 10,944 |
|
| 24 |
+
| **Weather** | Daily mean temperature (any city) | Open-Meteo archive | 4,248 |
|
| 25 |
+
| **Economy** | S&P 500 daily close | Yahoo Finance | 6,694 |
|
| 26 |
+
| **Energy** | WTI crude oil close | Yahoo Finance | 6,524 |
|
| 27 |
+
|
| 28 |
+
- Weather city is configurable: set env vars `WEATHER_NAME` / `WEATHER_LAT` / `WEATHER_LON`, or add cities to `weather_cities.py` (Chennai, Mumbai, Delhi, London, Tokyo, ... included).
|
| 29 |
+
- Every topic falls back gracefully: Hugging Face search -> API -> synthetic data if fully offline.
|
| 30 |
+
|
| 31 |
+
## Validated accuracy (held-out test set, no data leakage)
|
| 32 |
+
|
| 33 |
+
Separate per-topic models:
|
| 34 |
+
|
| 35 |
+
| Topic | MAE | H1 MAPE | vs naive baseline |
|
| 36 |
+
|---|---|---|---|
|
| 37 |
+
| AI (NVIDIA) | $6.73 | 1.86% | ~naive |
|
| 38 |
+
| Economy (S&P 500) | $85.51 | 0.64% | beats naive by 1.8% |
|
| 39 |
+
| Energy (WTI) | $4.78 | 2.83% | ~naive |
|
| 40 |
+
| Finance (Bitcoin) | $1,982.95 | 1.52% | ~naive |
|
| 41 |
+
| Programming (react) | 1.17M downloads | 7.12% | **beats naive by 77.5%** |
|
| 42 |
+
| Sports (ATP #1 Elo) | 3.40 Elo | 0.06% | ~naive |
|
| 43 |
+
| Weather (Chennai temp) | 0.99 K | 0.18% | beats naive by 1.9% |
|
| 44 |
+
|
| 45 |
+
Unified single model (trained jointly, one checkpoint for everything):
|
| 46 |
+
|
| 47 |
+
| Topic | MAE | H1 MAPE |
|
| 48 |
+
|---|---|---|
|
| 49 |
+
| AI | $3.40 | 2.36% |
|
| 50 |
+
| Economy | $72.21 | 0.75% |
|
| 51 |
+
| Energy | $2.93 | 1.90% |
|
| 52 |
+
| Finance | $2,863.45 | 1.74% |
|
| 53 |
+
| Programming | 1.14M downloads | 5.42% |
|
| 54 |
+
| Sports | 2.75 Elo | 0.04% |
|
| 55 |
+
| Weather | 0.94 K | 0.18% |
|
| 56 |
+
|
| 57 |
+
> Markets behave near a random walk, so 100% accuracy is impossible — these are honest, validated numbers. No model can guarantee the future.
|
| 58 |
+
|
| 59 |
+
## Architecture (normal mode shows only the final response)
|
| 60 |
|
| 61 |
```
|
| 62 |
user question -> intent detection (response.py)
|
| 63 |
-> prediction_engine.py (structured metrics, prints nothing)
|
| 64 |
+
-> response_formatter.py (natural summary + Markdown, no internal details)
|
| 65 |
+
-> ask.py (chat_output, stdout)
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
- `ask.py` — chat CLI; only the final assistant response reaches the user
|
| 69 |
+
- `response.py` — intent detection + orchestration (topic, location, horizon, style)
|
| 70 |
+
- `prediction_engine.py` — runs the LSTM, returns distinct metrics:
|
| 71 |
+
`net_change_pct` (path change), `forecast_range_pct` (path min-max range),
|
| 72 |
+
`current_to_forecast_pct` (vs latest observed value), `direction`
|
| 73 |
+
(rising/falling/sideways, configurable threshold), `primary_forecast`
|
| 74 |
+
(end-of-horizon value)
|
| 75 |
+
- `response_formatter.py` — presents metrics naturally; opens with a one-sentence
|
| 76 |
+
ChatGPT-style summary; never invents confidence, probability, or values
|
| 77 |
+
- `debug_logger.py` — internal logs go to stderr only when `FORECAST_DEBUG=1` (or `--debug`)
|
| 78 |
+
- `data.py` — multi-topic data pipeline (fetchers + caching to `data/<topic>.csv`)
|
| 79 |
+
- `weather_cities.py` — city registry for location-specific weather models
|
| 80 |
+
- `model.py` / `train.py` — 2-layer LSTM, Huber loss, AdamW, early stopping, temporal split
|
| 81 |
+
- `predict.py` — per-topic model loader, rollout forecast, chart + text report
|
| 82 |
+
- `multi.py` — trains + evaluates all topics; quiet in normal mode
|
| 83 |
+
|
| 84 |
+
## Ask it anything (ChatGPT-style)
|
| 85 |
+
|
| 86 |
+
```powershell
|
| 87 |
+
.\.venv\Scripts\python.exe ask.py # interactive chat
|
| 88 |
+
.\.venv\Scripts\python.exe ask.py "what will bitcoin do next week?"
|
| 89 |
+
.\.venv\Scripts\python.exe ask.py "compare all topics"
|
| 90 |
+
.\.venv\Scripts\python.exe ask.py --detailed "predict programming for 14 days"
|
| 91 |
+
.\.venv\Scripts\python.exe ask.py --debug "predict ai for 3 days" # internal logs on stderr
|
| 92 |
```
|
| 93 |
|
| 94 |
+
Example response:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
```
|
| 97 |
+
In short, the model expects Bitcoin to stay roughly stable next week, hovering near $63,228.
|
| 98 |
|
| 99 |
+
Bitcoin Forecast
|
| 100 |
|
| 101 |
+
August 18-24, 2026
|
| 102 |
+
|
| 103 |
+
The model forecasts relatively sideways movement for Bitcoin next week, with an
|
| 104 |
+
estimated level around **$63,228**.
|
| 105 |
+
|
| 106 |
+
Outlook: Sideways
|
| 107 |
+
Predicted level (2026-08-24): ~$63,228
|
| 108 |
+
Expected change: <0.01%
|
| 109 |
+
Forecast range: <0.01%
|
| 110 |
+
From latest observed value ($63,229, 2026-08-17): <0.01%
|
| 111 |
+
|
| 112 |
+
This is a model-generated forecast, and actual market behavior may differ.
|
| 113 |
+
```
|
| 114 |
|
| 115 |
## Weather for any city
|
| 116 |
|
| 117 |
+
```powershell
|
| 118 |
+
$env:WEATHER_NAME='Chennai, India'; $env:WEATHER_LAT='13.0827'; $env:WEATHER_LON='80.2707'
|
| 119 |
+
.\.venv\Scripts\python.exe data.py --topic weather --refresh
|
| 120 |
+
.\.venv\Scripts\python.exe train.py --topic weather --epochs 150
|
| 121 |
+
.\.venv\Scripts\python.exe ask.py "predict weather in chennai tomorrow"
|
| 122 |
```
|
| 123 |
|
| 124 |
+
## Train / refresh
|
| 125 |
|
| 126 |
+
```powershell
|
| 127 |
+
.\run_all.ps1 # everything (default topics)
|
| 128 |
+
.\.venv\Scripts\python.exe multi.py # all topics
|
| 129 |
+
.\.venv\Scripts\python.exe train.py --topic sports
|
| 130 |
+
.\.venv\Scripts\python.exe predict.py --topic ai --days 14
|
| 131 |
+
```
|
| 132 |
|
| 133 |
+
Outputs: `model_<topic>.pt`, `config_<topic>.json`, `prediction_<topic>.png`, `prediction_<topic>.txt`, `results.json`.
|
| 134 |
|
| 135 |
+
## Add a new domain
|
| 136 |
+
|
| 137 |
+
1. Add an entry to `TOPICS` in `data.py` (label, unit, asset name)
|
| 138 |
+
2. Add a fetcher returning `{source, series}` (date + value columns) to `FETCHERS`
|
| 139 |
+
3. Run `python train.py --topic <name>` — everything else is automatic
|
config_weather.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"topic": "weather",
|
| 3 |
+
"topic_label": "Weather",
|
| 4 |
+
"unit": "K",
|
| 5 |
+
"source": "open-meteo:Chennai, India",
|
| 6 |
+
"lookback": 60,
|
| 7 |
+
"horizon_steps": 7,
|
| 8 |
+
"horizon_days": 7,
|
| 9 |
+
"hidden_size": 64,
|
| 10 |
+
"num_layers": 2,
|
| 11 |
+
"dropout": 0.1,
|
| 12 |
+
"val_mae": 0.9925612108358967,
|
| 13 |
+
"val_rel_mae": 0.0032603859435766935,
|
| 14 |
+
"val_h1_mape": 0.0018425700744697428,
|
| 15 |
+
"naive_baseline_mae": 1.0116436048493131,
|
| 16 |
+
"last_date": "2026-08-18",
|
| 17 |
+
"last_value": 303.25
|
| 18 |
+
}
|
data.py
CHANGED
|
@@ -337,10 +337,7 @@ def set_weather_location(name: str, lat, lon):
|
|
| 337 |
|
| 338 |
|
| 339 |
def artifact_suffix(topic: str) -> str:
|
| 340 |
-
"""
|
| 341 |
-
same topic keep separate artifacts."""
|
| 342 |
-
if topic == "weather":
|
| 343 |
-
return f"_{weather_slug()}"
|
| 344 |
return ""
|
| 345 |
|
| 346 |
|
|
|
|
| 337 |
|
| 338 |
|
| 339 |
def artifact_suffix(topic: str) -> str:
|
| 340 |
+
"""Kept for compatibility; artifacts use plain `model_<topic>.pt` names."""
|
|
|
|
|
|
|
|
|
|
| 341 |
return ""
|
| 342 |
|
| 343 |
|
model.py
CHANGED
|
@@ -2,6 +2,44 @@ import torch
|
|
| 2 |
import torch.nn as nn
|
| 3 |
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
class LSTMForecaster(nn.Module):
|
| 6 |
def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.1, horizon=7):
|
| 7 |
super().__init__()
|
|
|
|
| 2 |
import torch.nn as nn
|
| 3 |
|
| 4 |
|
| 5 |
+
class UnifiedForecaster(nn.Module):
|
| 6 |
+
"""Single multi-task model for all domains: one shared LSTM backbone,
|
| 7 |
+
a per-domain embedding, and a per-domain output head."""
|
| 8 |
+
|
| 9 |
+
def __init__(self, num_domains, input_size=1, hidden_size=128, num_layers=2,
|
| 10 |
+
dropout=0.1, horizon=7, embed_dim=16):
|
| 11 |
+
super().__init__()
|
| 12 |
+
self.horizon = horizon
|
| 13 |
+
self.num_domains = num_domains
|
| 14 |
+
self.domain_embed = nn.Embedding(num_domains, embed_dim)
|
| 15 |
+
self.lstm = nn.LSTM(
|
| 16 |
+
input_size=input_size + embed_dim,
|
| 17 |
+
hidden_size=hidden_size,
|
| 18 |
+
num_layers=num_layers,
|
| 19 |
+
batch_first=True,
|
| 20 |
+
dropout=dropout if num_layers > 1 else 0.0,
|
| 21 |
+
)
|
| 22 |
+
self.heads = nn.ModuleList([
|
| 23 |
+
nn.Sequential(
|
| 24 |
+
nn.Linear(hidden_size, hidden_size // 2),
|
| 25 |
+
nn.ReLU(),
|
| 26 |
+
nn.Dropout(dropout),
|
| 27 |
+
nn.Linear(hidden_size // 2, horizon),
|
| 28 |
+
)
|
| 29 |
+
for _ in range(num_domains)
|
| 30 |
+
])
|
| 31 |
+
|
| 32 |
+
def forward(self, x, domain_ids):
|
| 33 |
+
emb = self.domain_embed(domain_ids).unsqueeze(1).expand(-1, x.size(1), -1)
|
| 34 |
+
out, _ = self.lstm(torch.cat([x, emb], dim=-1))
|
| 35 |
+
last = out[:, -1, :]
|
| 36 |
+
preds = torch.empty(x.size(0), self.horizon, device=x.device, dtype=last.dtype)
|
| 37 |
+
for d in torch.unique(domain_ids):
|
| 38 |
+
mask = domain_ids == d
|
| 39 |
+
preds[mask] = self.heads[int(d.item())](last[mask])
|
| 40 |
+
return preds
|
| 41 |
+
|
| 42 |
+
|
| 43 |
class LSTMForecaster(nn.Module):
|
| 44 |
def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.1, horizon=7):
|
| 45 |
super().__init__()
|
model_weather.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1c99536594bd56b91868633503b129d30d94eec7ac6eeab0e9c50569663e1b5b
|
| 3 |
+
size 224939
|
predict.py
CHANGED
|
@@ -10,9 +10,11 @@ matplotlib.use("Agg")
|
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
|
| 12 |
import data
|
| 13 |
-
from model import LSTMForecaster
|
| 14 |
|
| 15 |
BASE_DIR = Path(__file__).resolve().parent
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
def write_text_report(rows, cfg, topic):
|
|
@@ -52,13 +54,68 @@ def write_text_report(rows, cfg, topic):
|
|
| 52 |
return txt_path
|
| 53 |
|
| 54 |
|
| 55 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
suffix = data.artifact_suffix(topic)
|
| 57 |
model_path = BASE_DIR / f"model_{topic}{suffix}.pt"
|
| 58 |
config_path = BASE_DIR / f"config_{topic}{suffix}.json"
|
| 59 |
if not model_path.exists() or not config_path.exists():
|
| 60 |
raise FileNotFoundError(f"Model for topic '{topic}' not trained yet. Run: python train.py --topic {topic}")
|
| 61 |
cfg = json.loads(config_path.read_text())
|
|
|
|
| 62 |
model = LSTMForecaster(
|
| 63 |
hidden_size=cfg["hidden_size"],
|
| 64 |
num_layers=cfg["num_layers"],
|
|
@@ -70,8 +127,19 @@ def load_artifacts(topic, device="cpu"):
|
|
| 70 |
return model, cfg
|
| 71 |
|
| 72 |
|
| 73 |
-
def
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
series = pd.read_csv(BASE_DIR / "data" / f"{topic}.csv")
|
| 76 |
vals = series["value"].to_numpy(dtype=np.float64)
|
| 77 |
lb = cfg["lookback"]
|
|
@@ -85,9 +153,7 @@ def predict(topic, days=None, device="cpu"):
|
|
| 85 |
last_date = pd.Timestamp(cfg["last_date"])
|
| 86 |
rows = []
|
| 87 |
for step in range(n_steps):
|
| 88 |
-
|
| 89 |
-
with torch.no_grad():
|
| 90 |
-
pred_rel = model(x).cpu().numpy().flatten()
|
| 91 |
cur_val = last_val * (1.0 + pred_rel[0])
|
| 92 |
rel = np.concatenate([rel, pred_rel])
|
| 93 |
rows.append({
|
|
@@ -124,11 +190,14 @@ def main():
|
|
| 124 |
parser = argparse.ArgumentParser()
|
| 125 |
parser.add_argument("--topic", default="finance", choices=list(data.TOPICS))
|
| 126 |
parser.add_argument("--days", type=float, default=None, help="how many days ahead to predict")
|
|
|
|
|
|
|
| 127 |
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 128 |
args = parser.parse_args()
|
| 129 |
-
rows, cfg, chart_path, txt_path = predict(args.topic, days=args.days, device=args.device)
|
| 130 |
unit = cfg.get("unit", "")
|
| 131 |
print(f"Topic : {cfg.get('topic_label', args.topic)} - {cfg['source']}")
|
|
|
|
| 132 |
print(f"Last known : {cfg['last_date']} = {cfg['last_value']:,.2f} {unit}")
|
| 133 |
print(f"Validated on : MAE={cfg['val_mae']:,.2f}, rel MAE={cfg['val_rel_mae'] * 100:.2f}%, "
|
| 134 |
f"H1 MAPE={cfg['val_h1_mape'] * 100:.2f}% (naive baseline MAE={cfg['naive_baseline_mae']:,.2f})")
|
|
|
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
|
| 12 |
import data
|
| 13 |
+
from model import LSTMForecaster, UnifiedForecaster
|
| 14 |
|
| 15 |
BASE_DIR = Path(__file__).resolve().parent
|
| 16 |
+
UNIFIED_MODEL = BASE_DIR / "unified_model.pt"
|
| 17 |
+
UNIFIED_CONFIG = BASE_DIR / "unified_config.json"
|
| 18 |
|
| 19 |
|
| 20 |
def write_text_report(rows, cfg, topic):
|
|
|
|
| 54 |
return txt_path
|
| 55 |
|
| 56 |
|
| 57 |
+
def load_unified(device="cpu"):
|
| 58 |
+
"""Load the single combined model that serves all topics."""
|
| 59 |
+
if not UNIFIED_MODEL.exists() or not UNIFIED_CONFIG.exists():
|
| 60 |
+
raise FileNotFoundError("Unified model not trained yet. Run: python train_unified.py")
|
| 61 |
+
ucfg = json.loads(UNIFIED_CONFIG.read_text())
|
| 62 |
+
model = UnifiedForecaster(
|
| 63 |
+
num_domains=ucfg["num_domains"],
|
| 64 |
+
hidden_size=ucfg["hidden_size"],
|
| 65 |
+
num_layers=ucfg["num_layers"],
|
| 66 |
+
dropout=ucfg["dropout"],
|
| 67 |
+
horizon=ucfg["horizon_steps"],
|
| 68 |
+
embed_dim=ucfg["embed_dim"],
|
| 69 |
+
).to(device)
|
| 70 |
+
model.load_state_dict(torch.load(UNIFIED_MODEL, map_location=device))
|
| 71 |
+
model.eval()
|
| 72 |
+
return model, ucfg
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def load_topic_cfg(topic):
|
| 76 |
+
config_path = BASE_DIR / f"config_{topic}{data.artifact_suffix(topic)}.json"
|
| 77 |
+
if config_path.exists():
|
| 78 |
+
return json.loads(config_path.read_text())
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def load_artifacts(topic, device="cpu", mode="auto"):
|
| 83 |
+
"""mode: 'unified' -> single combined model; 'separate' -> per-topic LSTM;
|
| 84 |
+
'auto' -> unified when available, else per-topic."""
|
| 85 |
+
if mode in ("unified", "auto") and UNIFIED_MODEL.exists():
|
| 86 |
+
model, ucfg = load_unified(device)
|
| 87 |
+
cfg = load_topic_cfg(topic) or {}
|
| 88 |
+
topic_metrics = ucfg.get("metrics_per_topic", {}).get(topic, {})
|
| 89 |
+
meta = json.loads((BASE_DIR / "data" / f"{topic}.meta.json").read_text()) if (
|
| 90 |
+
BASE_DIR / "data" / f"{topic}.meta.json").exists() else {}
|
| 91 |
+
merged = {
|
| 92 |
+
"topic": topic,
|
| 93 |
+
"topic_label": data.TOPICS.get(topic, {}).get("label", topic),
|
| 94 |
+
"unit": data.TOPICS.get(topic, {}).get("unit", cfg.get("unit", "")),
|
| 95 |
+
"source": cfg.get("source", meta.get("source", "unified")),
|
| 96 |
+
"lookback": ucfg["lookback"],
|
| 97 |
+
"horizon_steps": ucfg["horizon_steps"],
|
| 98 |
+
"horizon_days": ucfg.get("horizon_days", 7),
|
| 99 |
+
"hidden_size": ucfg["hidden_size"],
|
| 100 |
+
"num_layers": ucfg["num_layers"],
|
| 101 |
+
"dropout": ucfg["dropout"],
|
| 102 |
+
"val_mae": topic_metrics.get("mae", cfg.get("val_mae", float("nan"))),
|
| 103 |
+
"val_rel_mae": topic_metrics.get("rel_mae", cfg.get("val_rel_mae", float("nan"))),
|
| 104 |
+
"val_h1_mape": topic_metrics.get("h1_mape", cfg.get("val_h1_mape", float("nan"))),
|
| 105 |
+
"naive_baseline_mae": topic_metrics.get("naive_mae", cfg.get("naive_baseline_mae", float("nan"))),
|
| 106 |
+
"last_date": cfg.get("last_date", meta.get("end", "")),
|
| 107 |
+
"last_value": cfg.get("last_value", float("nan")),
|
| 108 |
+
"engine": "unified",
|
| 109 |
+
}
|
| 110 |
+
return model, merged
|
| 111 |
+
|
| 112 |
suffix = data.artifact_suffix(topic)
|
| 113 |
model_path = BASE_DIR / f"model_{topic}{suffix}.pt"
|
| 114 |
config_path = BASE_DIR / f"config_{topic}{suffix}.json"
|
| 115 |
if not model_path.exists() or not config_path.exists():
|
| 116 |
raise FileNotFoundError(f"Model for topic '{topic}' not trained yet. Run: python train.py --topic {topic}")
|
| 117 |
cfg = json.loads(config_path.read_text())
|
| 118 |
+
cfg["engine"] = "separate"
|
| 119 |
model = LSTMForecaster(
|
| 120 |
hidden_size=cfg["hidden_size"],
|
| 121 |
num_layers=cfg["num_layers"],
|
|
|
|
| 127 |
return model, cfg
|
| 128 |
|
| 129 |
|
| 130 |
+
def _run_step(model, rel, lb, topic, engine, device):
|
| 131 |
+
x = torch.from_numpy(rel[-lb:].astype(np.float32)).unsqueeze(0).unsqueeze(-1).to(device)
|
| 132 |
+
with torch.no_grad():
|
| 133 |
+
if engine == "unified":
|
| 134 |
+
ucfg = json.loads(UNIFIED_CONFIG.read_text())
|
| 135 |
+
did = torch.tensor([ucfg["domain_ids"][topic]], dtype=torch.int64, device=device)
|
| 136 |
+
return model(x, did).cpu().numpy().flatten()
|
| 137 |
+
return model(x).cpu().numpy().flatten()
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def predict(topic, days=None, device="cpu", mode="auto"):
|
| 141 |
+
model, cfg = load_artifacts(topic, device, mode=mode)
|
| 142 |
+
engine = cfg.get("engine", "separate")
|
| 143 |
series = pd.read_csv(BASE_DIR / "data" / f"{topic}.csv")
|
| 144 |
vals = series["value"].to_numpy(dtype=np.float64)
|
| 145 |
lb = cfg["lookback"]
|
|
|
|
| 153 |
last_date = pd.Timestamp(cfg["last_date"])
|
| 154 |
rows = []
|
| 155 |
for step in range(n_steps):
|
| 156 |
+
pred_rel = _run_step(model, rel, lb, topic, engine, device)
|
|
|
|
|
|
|
| 157 |
cur_val = last_val * (1.0 + pred_rel[0])
|
| 158 |
rel = np.concatenate([rel, pred_rel])
|
| 159 |
rows.append({
|
|
|
|
| 190 |
parser = argparse.ArgumentParser()
|
| 191 |
parser.add_argument("--topic", default="finance", choices=list(data.TOPICS))
|
| 192 |
parser.add_argument("--days", type=float, default=None, help="how many days ahead to predict")
|
| 193 |
+
parser.add_argument("--mode", default="auto", choices=["auto", "unified", "separate"],
|
| 194 |
+
help="unified = the single combined model; separate = per-topic LSTMs")
|
| 195 |
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 196 |
args = parser.parse_args()
|
| 197 |
+
rows, cfg, chart_path, txt_path = predict(args.topic, days=args.days, device=args.device, mode=args.mode)
|
| 198 |
unit = cfg.get("unit", "")
|
| 199 |
print(f"Topic : {cfg.get('topic_label', args.topic)} - {cfg['source']}")
|
| 200 |
+
print(f"Engine : {cfg.get('engine', 'separate')} model")
|
| 201 |
print(f"Last known : {cfg['last_date']} = {cfg['last_value']:,.2f} {unit}")
|
| 202 |
print(f"Validated on : MAE={cfg['val_mae']:,.2f}, rel MAE={cfg['val_rel_mae'] * 100:.2f}%, "
|
| 203 |
f"H1 MAPE={cfg['val_h1_mape'] * 100:.2f}% (naive baseline MAE={cfg['naive_baseline_mae']:,.2f})")
|
train_unified.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train ONE model for all domains (UnifiedForecaster).
|
| 2 |
+
|
| 3 |
+
Loads every topic's dataset, builds normalized windows tagged with a domain
|
| 4 |
+
id, and trains a shared LSTM backbone + per-domain heads jointly. Saves a
|
| 5 |
+
single checkpoint used by predict.py (mode=unified).
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import argparse
|
| 9 |
+
import json
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn as nn
|
| 16 |
+
from torch.utils.data import DataLoader, TensorDataset
|
| 17 |
+
|
| 18 |
+
import data
|
| 19 |
+
from model import UnifiedForecaster
|
| 20 |
+
|
| 21 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 22 |
+
UNIFIED_MODEL = BASE_DIR / "unified_model.pt"
|
| 23 |
+
UNIFIED_CONFIG = BASE_DIR / "unified_config.json"
|
| 24 |
+
|
| 25 |
+
DOMAIN_ORDER = ["ai", "programming", "finance", "sports", "weather", "economy", "energy"]
|
| 26 |
+
DOMAIN_IDS = {name: i for i, name in enumerate(DOMAIN_ORDER)}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def build_windows(values, lookback, horizon):
|
| 30 |
+
xs, ys, bases = [], [], []
|
| 31 |
+
for i in range(lookback, len(values) - horizon):
|
| 32 |
+
base = values[i - 1]
|
| 33 |
+
if not np.isfinite(base) or base <= 0:
|
| 34 |
+
continue
|
| 35 |
+
xs.append(values[i - lookback:i] / base - 1.0)
|
| 36 |
+
ys.append(values[i:i + horizon] / base - 1.0)
|
| 37 |
+
bases.append(base)
|
| 38 |
+
return (np.array(xs, dtype=np.float32), np.array(ys, dtype=np.float32), np.array(bases, dtype=np.float64))
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def collect_all(topics, lookback, horizon, test_frac=0.2):
|
| 42 |
+
train_x, train_y, train_d = [], [], []
|
| 43 |
+
test_sets = {}
|
| 44 |
+
for topic in topics:
|
| 45 |
+
series, source = data.load(topic=topic, refresh=False)
|
| 46 |
+
vals = series["value"].to_numpy(dtype=np.float64)
|
| 47 |
+
x, y, b = build_windows(vals, lookback, horizon)
|
| 48 |
+
if len(x) == 0:
|
| 49 |
+
continue
|
| 50 |
+
n_test = max(horizon, int(len(x) * test_frac))
|
| 51 |
+
n_train = len(x) - n_test
|
| 52 |
+
did = DOMAIN_IDS[topic]
|
| 53 |
+
train_x.append(x[:n_train])
|
| 54 |
+
train_y.append(y[:n_train])
|
| 55 |
+
train_d.append(np.full(n_train, did, dtype=np.int64))
|
| 56 |
+
test_sets[topic] = (x[n_train:], y[n_train:], b[n_train:])
|
| 57 |
+
print(f"[unified] {topic:12s} train={n_train:6d} test={n_test:5d} ({source})")
|
| 58 |
+
X = np.concatenate(train_x)
|
| 59 |
+
Y = np.concatenate(train_y)
|
| 60 |
+
D = np.concatenate(train_d)
|
| 61 |
+
return X, Y, D, test_sets
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def evaluate_unified(model, loader, device):
|
| 65 |
+
model.eval()
|
| 66 |
+
preds, actuals, bases = [], [], []
|
| 67 |
+
with torch.no_grad():
|
| 68 |
+
for xb, yb, bb, db in loader:
|
| 69 |
+
preds.append(model(xb.to(device), db.to(device)).cpu().numpy())
|
| 70 |
+
actuals.append(yb.numpy())
|
| 71 |
+
bases.append(bb.numpy())
|
| 72 |
+
preds = np.concatenate(preds)
|
| 73 |
+
actuals = np.concatenate(actuals)
|
| 74 |
+
bases = np.concatenate(bases)[:, None]
|
| 75 |
+
pred_usd = bases * (1.0 + preds)
|
| 76 |
+
act_usd = bases * (1.0 + actuals)
|
| 77 |
+
mae = float(np.mean(np.abs(pred_usd - act_usd)))
|
| 78 |
+
rel_mae = float(np.mean(np.abs(preds - actuals)))
|
| 79 |
+
denom = np.where(act_usd[:, 0] == 0, 1e-9, act_usd[:, 0])
|
| 80 |
+
h1_mape = float(np.mean(np.abs((act_usd[:, 0] - pred_usd[:, 0]) / denom)))
|
| 81 |
+
naive_mae = float(np.mean(np.abs(act_usd - bases)))
|
| 82 |
+
return mae, rel_mae, h1_mape, naive_mae
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def train(topics=None, lookback=60, horizon=7, epochs=40, batch_size=256, lr=1e-3,
|
| 86 |
+
device="cpu", seed=0, hidden_size=128, num_layers=2, dropout=0.1, embed_dim=16,
|
| 87 |
+
patience=10):
|
| 88 |
+
torch.manual_seed(seed)
|
| 89 |
+
np.random.seed(seed)
|
| 90 |
+
topics = topics or DOMAIN_ORDER
|
| 91 |
+
X, Y, D, test_sets = collect_all(topics, lookback, horizon)
|
| 92 |
+
n_train = len(X)
|
| 93 |
+
if n_train < 100:
|
| 94 |
+
raise SystemExit("[unified] not enough training windows; run data.py --refresh for each topic first")
|
| 95 |
+
|
| 96 |
+
train_ds = TensorDataset(torch.from_numpy(X).unsqueeze(-1), torch.from_numpy(Y), torch.from_numpy(D))
|
| 97 |
+
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)
|
| 98 |
+
|
| 99 |
+
test_by_topic = {}
|
| 100 |
+
for topic, (tx, ty, tb) in test_sets.items():
|
| 101 |
+
test_by_topic[topic] = DataLoader(
|
| 102 |
+
TensorDataset(torch.from_numpy(tx).unsqueeze(-1), torch.from_numpy(ty),
|
| 103 |
+
torch.from_numpy(tb),
|
| 104 |
+
torch.full((len(tx),), DOMAIN_IDS[topic], dtype=torch.int64)),
|
| 105 |
+
batch_size=512)
|
| 106 |
+
|
| 107 |
+
model = UnifiedForecaster(num_domains=len(DOMAIN_ORDER), hidden_size=hidden_size,
|
| 108 |
+
num_layers=num_layers, dropout=dropout, horizon=horizon,
|
| 109 |
+
embed_dim=embed_dim).to(device)
|
| 110 |
+
opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-5)
|
| 111 |
+
sched = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, patience=4, factor=0.5)
|
| 112 |
+
crit = nn.HuberLoss()
|
| 113 |
+
|
| 114 |
+
best_rel, best_state, seen = float("inf"), None, 0
|
| 115 |
+
for epoch in range(1, epochs + 1):
|
| 116 |
+
model.train()
|
| 117 |
+
total = 0.0
|
| 118 |
+
for xb, yb, db in train_loader:
|
| 119 |
+
opt.zero_grad()
|
| 120 |
+
loss = crit(model(xb.to(device), db.to(device)), yb.to(device))
|
| 121 |
+
loss.backward()
|
| 122 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 123 |
+
opt.step()
|
| 124 |
+
total += float(loss.item()) * len(xb)
|
| 125 |
+
|
| 126 |
+
rels, h1s = [], []
|
| 127 |
+
for topic, loader in test_by_topic.items():
|
| 128 |
+
mae, rel_mae, h1_mape, naive = evaluate_unified(model, loader, device)
|
| 129 |
+
rels.append(rel_mae)
|
| 130 |
+
h1s.append(h1_mape)
|
| 131 |
+
val_rel = float(np.mean(rels))
|
| 132 |
+
val_h1 = float(np.mean(h1s))
|
| 133 |
+
sched.step(val_rel)
|
| 134 |
+
print(f"epoch {epoch:3d}/{epochs} loss={total / n_train:.5f} "
|
| 135 |
+
f"val_relMAE={val_rel * 100:.2f}% avg_H1_MAPE={val_h1 * 100:.2f}%")
|
| 136 |
+
if val_rel < best_rel - 1e-6:
|
| 137 |
+
best_rel, seen = val_rel, 0
|
| 138 |
+
best_state = {k: v.detach().cpu().clone() for k, v in model.state_dict().items()}
|
| 139 |
+
else:
|
| 140 |
+
seen += 1
|
| 141 |
+
if seen >= patience:
|
| 142 |
+
print("[unified] early stopping")
|
| 143 |
+
break
|
| 144 |
+
|
| 145 |
+
if best_state is not None:
|
| 146 |
+
model.load_state_dict(best_state)
|
| 147 |
+
|
| 148 |
+
per_topic = {}
|
| 149 |
+
for topic, loader in test_by_topic.items():
|
| 150 |
+
mae, rel_mae, h1_mape, naive = evaluate_unified(model, loader, device)
|
| 151 |
+
per_topic[topic] = {"mae": mae, "rel_mae": rel_mae, "h1_mape": h1_mape, "naive_mae": naive}
|
| 152 |
+
print(f"[unified] {topic:12s} MAE={mae:,.2f} relMAE={rel_mae * 100:.2f}% "
|
| 153 |
+
f"H1_MAPE={h1_mape * 100:.2f}% naive={naive:,.2f}")
|
| 154 |
+
|
| 155 |
+
torch.save(model.state_dict(), UNIFIED_MODEL)
|
| 156 |
+
cfg = {
|
| 157 |
+
"mode": "unified",
|
| 158 |
+
"num_domains": len(DOMAIN_ORDER),
|
| 159 |
+
"domain_order": DOMAIN_ORDER,
|
| 160 |
+
"domain_ids": DOMAIN_IDS,
|
| 161 |
+
"lookback": lookback,
|
| 162 |
+
"horizon_steps": horizon,
|
| 163 |
+
"horizon_days": horizon,
|
| 164 |
+
"hidden_size": hidden_size,
|
| 165 |
+
"num_layers": num_layers,
|
| 166 |
+
"dropout": dropout,
|
| 167 |
+
"embed_dim": embed_dim,
|
| 168 |
+
"n_train_windows": int(n_train),
|
| 169 |
+
"metrics_per_topic": per_topic,
|
| 170 |
+
}
|
| 171 |
+
UNIFIED_CONFIG.write_text(json.dumps(cfg, indent=2))
|
| 172 |
+
print(f"\n[unified] saved single model -> {UNIFIED_MODEL.name}, config -> {UNIFIED_CONFIG.name}")
|
| 173 |
+
return model, cfg
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def main():
|
| 177 |
+
parser = argparse.ArgumentParser()
|
| 178 |
+
parser.add_argument("--epochs", type=int, default=40)
|
| 179 |
+
parser.add_argument("--horizon", type=int, default=7)
|
| 180 |
+
parser.add_argument("--lookback", type=int, default=60)
|
| 181 |
+
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 182 |
+
args = parser.parse_args()
|
| 183 |
+
train(epochs=args.epochs, horizon=args.horizon, lookback=args.lookback, device=args.device)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
if __name__ == "__main__":
|
| 187 |
+
main()
|
unified_config.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mode": "unified",
|
| 3 |
+
"num_domains": 7,
|
| 4 |
+
"domain_order": [
|
| 5 |
+
"ai",
|
| 6 |
+
"programming",
|
| 7 |
+
"finance",
|
| 8 |
+
"sports",
|
| 9 |
+
"weather",
|
| 10 |
+
"economy",
|
| 11 |
+
"energy"
|
| 12 |
+
],
|
| 13 |
+
"domain_ids": {
|
| 14 |
+
"ai": 0,
|
| 15 |
+
"programming": 1,
|
| 16 |
+
"finance": 2,
|
| 17 |
+
"sports": 3,
|
| 18 |
+
"weather": 4,
|
| 19 |
+
"economy": 5,
|
| 20 |
+
"energy": 6
|
| 21 |
+
},
|
| 22 |
+
"lookback": 60,
|
| 23 |
+
"horizon_steps": 7,
|
| 24 |
+
"horizon_days": 7,
|
| 25 |
+
"hidden_size": 128,
|
| 26 |
+
"num_layers": 2,
|
| 27 |
+
"dropout": 0.1,
|
| 28 |
+
"embed_dim": 16,
|
| 29 |
+
"n_train_windows": 31735,
|
| 30 |
+
"metrics_per_topic": {
|
| 31 |
+
"ai": {
|
| 32 |
+
"mae": 3.4010714403826725,
|
| 33 |
+
"rel_mae": 0.046488404273986816,
|
| 34 |
+
"h1_mape": 0.023641787109347494,
|
| 35 |
+
"naive_mae": 3.4285758835207747
|
| 36 |
+
},
|
| 37 |
+
"programming": {
|
| 38 |
+
"mae": 1135964.0941619105,
|
| 39 |
+
"rel_mae": 0.058158028870821,
|
| 40 |
+
"h1_mape": 0.054231874000870206,
|
| 41 |
+
"naive_mae": 5238840.46007266
|
| 42 |
+
},
|
| 43 |
+
"finance": {
|
| 44 |
+
"mae": 2863.447612711464,
|
| 45 |
+
"rel_mae": 0.03436192125082016,
|
| 46 |
+
"h1_mape": 0.017374594652497116,
|
| 47 |
+
"naive_mae": 2704.601283383124
|
| 48 |
+
},
|
| 49 |
+
"sports": {
|
| 50 |
+
"mae": 2.7536647755605217,
|
| 51 |
+
"rel_mae": 0.0012617451138794422,
|
| 52 |
+
"h1_mape": 0.0003771134174271423,
|
| 53 |
+
"naive_mae": 2.502011465506802
|
| 54 |
+
},
|
| 55 |
+
"weather": {
|
| 56 |
+
"mae": 0.9439951772615057,
|
| 57 |
+
"rel_mae": 0.003122166031971574,
|
| 58 |
+
"h1_mape": 0.0017910992340668128,
|
| 59 |
+
"naive_mae": 0.9435578352786341
|
| 60 |
+
},
|
| 61 |
+
"economy": {
|
| 62 |
+
"mae": 72.2052313993689,
|
| 63 |
+
"rel_mae": 0.014413672499358654,
|
| 64 |
+
"h1_mape": 0.007524061656098242,
|
| 65 |
+
"naive_mae": 73.12155254020708
|
| 66 |
+
},
|
| 67 |
+
"energy": {
|
| 68 |
+
"mae": 2.9286401717398816,
|
| 69 |
+
"rel_mae": 0.03628615289926529,
|
| 70 |
+
"h1_mape": 0.018979403445247508,
|
| 71 |
+
"naive_mae": 2.9195108923456288
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
}
|
unified_model.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6eadd67d78c1019664062377451681c04815c0878c32e95de3532b216802a9ec
|
| 3 |
+
size 1086390
|