modelmark 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modelmark/__init__.py +3 -0
- modelmark/__main__.py +5 -0
- modelmark/common/dataset.py +116 -0
- modelmark/common/downloader.py +88 -0
- modelmark/common/loader.py +63 -0
- modelmark/common/logger.py +34 -0
- modelmark/common/parser.py +134 -0
- modelmark/common/report.py +734 -0
- modelmark/common/tester.py +162 -0
- modelmark/common/utils.py +105 -0
- modelmark/config.py +97 -0
- modelmark/constants.py +34 -0
- modelmark/modelmark.py +206 -0
- modelmark/models/conv.py +75 -0
- modelmark/models/gru.py +45 -0
- modelmark/models/linear.py +65 -0
- modelmark/models/lstm.py +43 -0
- modelmark/models/transformer.py +86 -0
- modelmark-0.1.0.dist-info/METADATA +109 -0
- modelmark-0.1.0.dist-info/RECORD +24 -0
- modelmark-0.1.0.dist-info/WHEEL +5 -0
- modelmark-0.1.0.dist-info/entry_points.txt +2 -0
- modelmark-0.1.0.dist-info/licenses/LICENSE +21 -0
- modelmark-0.1.0.dist-info/top_level.txt +1 -0
modelmark/__init__.py
ADDED
modelmark/__main__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
from torch.utils.data import Dataset
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from modelmark.common.utils import load_config
|
|
11
|
+
config = load_config()
|
|
12
|
+
|
|
13
|
+
class CausalDataset(Dataset):
|
|
14
|
+
"""
|
|
15
|
+
Sliding-window dataset for ETT.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
x: [seq_len, num_features]
|
|
19
|
+
y: [pred_len, 1]
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
data_config: dict,
|
|
25
|
+
seq_len: int,
|
|
26
|
+
pred_len: int,
|
|
27
|
+
split: str = "train",
|
|
28
|
+
mean: np.ndarray | None = None,
|
|
29
|
+
std: np.ndarray | None = None,
|
|
30
|
+
):
|
|
31
|
+
super().__init__()
|
|
32
|
+
|
|
33
|
+
self.seq_len = seq_len
|
|
34
|
+
self.pred_len = pred_len
|
|
35
|
+
|
|
36
|
+
# Extract config
|
|
37
|
+
data_path = config.data_path + data_config["path"]
|
|
38
|
+
features = copy.deepcopy(data_config["input_features"])
|
|
39
|
+
targets = data_config["output_features"]
|
|
40
|
+
features.extend([item for item in targets if item not in features])
|
|
41
|
+
train_ratio = data_config["train_ratio"]
|
|
42
|
+
val_ratio = data_config["val_ratio"]
|
|
43
|
+
|
|
44
|
+
# Load, extract, convert
|
|
45
|
+
df = pd.read_csv(data_path)
|
|
46
|
+
|
|
47
|
+
df = df[features]
|
|
48
|
+
data = df.values.astype(np.float32)
|
|
49
|
+
self.input_indices = torch.tensor(df.columns.get_indexer(data_config["input_features"]), dtype=torch.int)
|
|
50
|
+
self.output_indices = torch.tensor(df.columns.get_indexer(data_config["output_features"]), dtype=torch.int)
|
|
51
|
+
|
|
52
|
+
n = len(data)
|
|
53
|
+
|
|
54
|
+
train_end = int(n * train_ratio)
|
|
55
|
+
val_end = int(n * (train_ratio + val_ratio))
|
|
56
|
+
|
|
57
|
+
if split == "train":
|
|
58
|
+
start = 0
|
|
59
|
+
end = train_end
|
|
60
|
+
elif split == "val":
|
|
61
|
+
start = train_end
|
|
62
|
+
end = val_end
|
|
63
|
+
elif split == "test":
|
|
64
|
+
start = val_end
|
|
65
|
+
end = n
|
|
66
|
+
else:
|
|
67
|
+
raise ValueError(f"Unknown split: {split}")
|
|
68
|
+
|
|
69
|
+
# Save scaling information
|
|
70
|
+
if split == "train":
|
|
71
|
+
self.mean = data[start:train_end].mean(axis=0)
|
|
72
|
+
self.std = data[start:train_end].std(axis=0)
|
|
73
|
+
|
|
74
|
+
# Prevent division by zero
|
|
75
|
+
self.std[self.std < 1e-8] = 1.0
|
|
76
|
+
else:
|
|
77
|
+
if mean is None or std is None:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"Validation/test datasets need training mean and std."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self.mean = mean
|
|
83
|
+
self.std = std
|
|
84
|
+
|
|
85
|
+
# Scale using TRAIN statistics
|
|
86
|
+
data = (data - self.mean) / self.std
|
|
87
|
+
|
|
88
|
+
# Keep only the split region
|
|
89
|
+
self.data = data[start:end]
|
|
90
|
+
|
|
91
|
+
# Number of valid windows
|
|
92
|
+
self.length = len(self.data) - seq_len - pred_len + 1
|
|
93
|
+
|
|
94
|
+
if self.length <= 0:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Split '{split}' is too short for "
|
|
97
|
+
f"seq_len={seq_len}, pred_len={pred_len}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def __len__(self):
|
|
101
|
+
return self.length
|
|
102
|
+
|
|
103
|
+
def __getitem__(self, idx):
|
|
104
|
+
x_start = idx
|
|
105
|
+
x_end = x_start + self.seq_len
|
|
106
|
+
|
|
107
|
+
y_start = x_end
|
|
108
|
+
y_end = y_start + self.pred_len
|
|
109
|
+
|
|
110
|
+
x = self.data[x_start:x_end, self.input_indices]
|
|
111
|
+
y = self.data[y_start:y_end, self.output_indices]
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
torch.from_numpy(x),
|
|
115
|
+
torch.from_numpy(y),
|
|
116
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Download time-series datasets to a local directory.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- ETT (ETTh1, ETTh2, ETTm1, ETTm2) — from the ETDataset GitHub repo
|
|
5
|
+
- Air Quality (UCI Repository)
|
|
6
|
+
- Weather (Max Planck Institute / Jena climate dataset)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import shutil
|
|
11
|
+
import urllib.request
|
|
12
|
+
import zipfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
DATASETS = {
|
|
21
|
+
"ett": {
|
|
22
|
+
"dir": "data/ett",
|
|
23
|
+
"kind": "multi_csv",
|
|
24
|
+
"base_url": "https://raw.githubusercontent.com/zhouhaoyi/ETDataset/main/ETT-small",
|
|
25
|
+
"files": ["ETTh1", "ETTh2", "ETTm1", "ETTm2"],
|
|
26
|
+
},
|
|
27
|
+
"air_quality": {
|
|
28
|
+
"dir": "data/air_quality",
|
|
29
|
+
"kind": "zip",
|
|
30
|
+
"url": "https://archive.ics.uci.edu/static/public/360/air+quality.zip",
|
|
31
|
+
"zip_name": "AirQualityUCI.zip",
|
|
32
|
+
},
|
|
33
|
+
"weather": {
|
|
34
|
+
"dir": "data/weather",
|
|
35
|
+
"kind": "zip",
|
|
36
|
+
"url": "https://storage.googleapis.com/tensorflow/tf-keras-datasets/jena_climate_2009_2016.csv.zip",
|
|
37
|
+
"zip_name": "jena_climate_2009_2016.csv.zip",
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def download_ett(cfg: dict, out_dir: Path) -> None:
|
|
43
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
for name in cfg["files"]:
|
|
45
|
+
url = f"{cfg['base_url']}/{name}.csv"
|
|
46
|
+
path = out_dir / f"{name}.csv"
|
|
47
|
+
if path.exists():
|
|
48
|
+
logger.info(f"{name}.csv already exists, skipping.")
|
|
49
|
+
continue
|
|
50
|
+
logger.info(f"Downloading {name}...")
|
|
51
|
+
console.print(f"Downloading {name}...")
|
|
52
|
+
urllib.request.urlretrieve(url, path)
|
|
53
|
+
logger.info(f"Saved to {path}")
|
|
54
|
+
console.print(f"Saved to {path}", style="green")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def download_zip_dataset(cfg: dict, out_dir: Path) -> None:
|
|
58
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
zip_path = out_dir / cfg["zip_name"]
|
|
60
|
+
|
|
61
|
+
if not zip_path.exists():
|
|
62
|
+
logger.info(f"Downloading {cfg['zip_name']}...")
|
|
63
|
+
console.print(f"Downloading {cfg['zip_name']}...")
|
|
64
|
+
urllib.request.urlretrieve(cfg["url"], zip_path)
|
|
65
|
+
logger.info(f"Saved to {zip_path}")
|
|
66
|
+
console.print(f"Saved to {zip_path}", style="green")
|
|
67
|
+
else:
|
|
68
|
+
logger.info(f"{cfg['zip_name']} already exists, skipping download.")
|
|
69
|
+
console.print(f"{cfg['zip_name']} already exists, skipping download.", style="yellow")
|
|
70
|
+
|
|
71
|
+
logger.info(f"Extracting {zip_path.name}...")
|
|
72
|
+
console.print(f"Extracting {zip_path.name}...")
|
|
73
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
74
|
+
zf.extractall(out_dir)
|
|
75
|
+
console.print(f"Extracted to {out_dir}", color="green")
|
|
76
|
+
|
|
77
|
+
def download_dataset(name: str) -> None:
|
|
78
|
+
if name not in DATASETS:
|
|
79
|
+
valid = ", ".join(DATASETS)
|
|
80
|
+
raise ValueError(f"Unknown dataset '{name}'. Choose from: {valid}")
|
|
81
|
+
|
|
82
|
+
cfg = DATASETS[name]
|
|
83
|
+
out_dir = Path(cfg["dir"])
|
|
84
|
+
|
|
85
|
+
if cfg["kind"] == "multi_csv":
|
|
86
|
+
download_ett(cfg, out_dir)
|
|
87
|
+
elif cfg["kind"] == "zip":
|
|
88
|
+
download_zip_dataset(cfg, out_dir)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from modelmark.common.dataset import CausalDataset
|
|
4
|
+
from torch.utils.data import DataLoader
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
from modelmark.common.utils import load_config
|
|
9
|
+
config = load_config()
|
|
10
|
+
|
|
11
|
+
class Loader:
|
|
12
|
+
|
|
13
|
+
def __init__(self, file_config : dict, context_size : int):
|
|
14
|
+
|
|
15
|
+
self.get_dataloaders(file_config, context_size)
|
|
16
|
+
logger.debug(f"Dataset len stats: train={len(self.train_loader)} val={len(self.val_loader)} test={len(self.test_loader)}")
|
|
17
|
+
|
|
18
|
+
def get_dataloaders(self, file_config : dict, context_size : int) -> tuple[DataLoader, DataLoader, DataLoader]:
|
|
19
|
+
"""Load the data, pack to datasets, create the loaders and return them"""
|
|
20
|
+
|
|
21
|
+
train_ds = CausalDataset(data_config = file_config,
|
|
22
|
+
seq_len = context_size,
|
|
23
|
+
pred_len = context_size,
|
|
24
|
+
split = "train")
|
|
25
|
+
|
|
26
|
+
val_ds = CausalDataset(data_config = file_config,
|
|
27
|
+
seq_len = context_size,
|
|
28
|
+
pred_len = context_size,
|
|
29
|
+
split = "val",
|
|
30
|
+
mean = train_ds.mean,
|
|
31
|
+
std = train_ds.std)
|
|
32
|
+
|
|
33
|
+
test_ds = CausalDataset(data_config = file_config,
|
|
34
|
+
seq_len = context_size,
|
|
35
|
+
pred_len = context_size,
|
|
36
|
+
split = "test",
|
|
37
|
+
mean = train_ds.mean,
|
|
38
|
+
std = train_ds.std)
|
|
39
|
+
|
|
40
|
+
self.train_loader = DataLoader(
|
|
41
|
+
train_ds,
|
|
42
|
+
batch_size = config.test_config["optim"]["batch_size"],
|
|
43
|
+
shuffle = True,
|
|
44
|
+
num_workers = 0,
|
|
45
|
+
pin_memory = True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self.val_loader = DataLoader(
|
|
49
|
+
val_ds,
|
|
50
|
+
batch_size = config.test_config["optim"]["batch_size"],
|
|
51
|
+
shuffle = False,
|
|
52
|
+
num_workers = 0,
|
|
53
|
+
pin_memory = True,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self.test_loader = DataLoader(
|
|
57
|
+
test_ds,
|
|
58
|
+
batch_size = config.test_config["optim"]["batch_size"],
|
|
59
|
+
shuffle = False,
|
|
60
|
+
num_workers = 0,
|
|
61
|
+
pin_memory = True,
|
|
62
|
+
)
|
|
63
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
def setup_logging(log_format: Optional[str] = None) -> None:
|
|
6
|
+
"""
|
|
7
|
+
Configure logging for the entire application.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
log_format: Optional custom format string. If None, a default format is used.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
if log_format is None:
|
|
14
|
+
log_format = "%(asctime)s [%(levelname)s] [%(name)s] - %(message)s"
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(
|
|
17
|
+
level = logging.DEBUG,
|
|
18
|
+
filemode = 'w',
|
|
19
|
+
filename='modelmark_files/modelmark.log',
|
|
20
|
+
format = log_format,
|
|
21
|
+
datefmt = '%H:%M:%S',
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def get_logger(name: str) -> logging.Logger:
|
|
25
|
+
"""
|
|
26
|
+
Convenience function to get a logger for a module.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
name: Usually __name__.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A configured logger instance.
|
|
33
|
+
"""
|
|
34
|
+
return logging.getLogger(name)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from modelmark.common.downloader import download_dataset
|
|
7
|
+
import modelmark.constants as constants
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
class Parser:
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Parse the arguments"""
|
|
18
|
+
|
|
19
|
+
# Create parser
|
|
20
|
+
self.parser = argparse.ArgumentParser(
|
|
21
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
22
|
+
description = constants.PARSER_DESC
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Add arguments
|
|
26
|
+
self.parser.add_argument(
|
|
27
|
+
"-t", "--task",
|
|
28
|
+
type = str,
|
|
29
|
+
default = "help",
|
|
30
|
+
help = constants.PARSER_HELP
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Run without args / help request - print help
|
|
34
|
+
if len(sys.argv) == 1:
|
|
35
|
+
self.parser.print_help()
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
# Parse argument values
|
|
39
|
+
self.args = self.parser.parse_args()
|
|
40
|
+
|
|
41
|
+
# Setup other
|
|
42
|
+
pd.set_option('display.colheader_justify', 'center')
|
|
43
|
+
|
|
44
|
+
def run(self):
|
|
45
|
+
"""Select the task and run"""
|
|
46
|
+
|
|
47
|
+
# Config initialization #
|
|
48
|
+
if self.args.task == "init":
|
|
49
|
+
self.init_config()
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
# Dataset download #
|
|
53
|
+
if self.args.task == "load":
|
|
54
|
+
download_dataset("ett")
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
# Run the test #
|
|
58
|
+
if self.args.task == "run":
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# Clean the config folder #
|
|
62
|
+
if self.args.task == "clear":
|
|
63
|
+
self.clear_config()
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
# Reset the modelmark config #
|
|
67
|
+
if self.args.task == "reset":
|
|
68
|
+
self.reset_config()
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
# Print the modelmark usage #
|
|
72
|
+
if self.args.task == "help":
|
|
73
|
+
self.parser.print_help()
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
# Check if task is correct #
|
|
77
|
+
if self.args.task != "run":
|
|
78
|
+
logger.error(f"Unknown task: {self.args.task}, run 'modelmark' for more info")
|
|
79
|
+
console.print(f"Unknown task: {self.args.task}, run 'modelmark' for more info", style="red")
|
|
80
|
+
return 1
|
|
81
|
+
|
|
82
|
+
# Otherwise return error
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
def init_config(self):
|
|
86
|
+
"Copy the config example file to the user dir"
|
|
87
|
+
|
|
88
|
+
user_config_dir = constants.USER_CONFIG_DIR
|
|
89
|
+
user_models_dir = constants.USER_MODELS_DIR
|
|
90
|
+
|
|
91
|
+
package_config = constants.PACKAGE_CONFIG_PATH
|
|
92
|
+
user_config = constants.USER_CONFIG_PATH
|
|
93
|
+
package_model = constants.PACKAGE_MODEL_PATH
|
|
94
|
+
user_model = constants.USER_MODEL_PATH
|
|
95
|
+
|
|
96
|
+
# Make the dirs
|
|
97
|
+
user_config_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
user_models_dir.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
if not user_config.exists():
|
|
101
|
+
shutil.copy(package_config, user_config)
|
|
102
|
+
|
|
103
|
+
logger.info(f"Customizable config created at: {user_config}")
|
|
104
|
+
console.print(f"Customizable config created at: {user_config}")
|
|
105
|
+
|
|
106
|
+
shutil.copy(package_model, user_model)
|
|
107
|
+
|
|
108
|
+
logger.info(f"Model example file created at: {user_model}")
|
|
109
|
+
console.print(f"Model example file created at: {user_model}")
|
|
110
|
+
|
|
111
|
+
console.print(f"Initialization complete.", style="green")
|
|
112
|
+
else:
|
|
113
|
+
|
|
114
|
+
console.print(f"config.py already exists", style="yellow")
|
|
115
|
+
self.parser.print_help()
|
|
116
|
+
|
|
117
|
+
def clear_config(self):
|
|
118
|
+
"""Remove the user config dir"""
|
|
119
|
+
user_config_path = constants.USER_CONFIG_DIR
|
|
120
|
+
user_example_model_path = constants.USER_MODEL_PATH
|
|
121
|
+
|
|
122
|
+
if user_config_path.exists():
|
|
123
|
+
shutil.rmtree(user_config_path)
|
|
124
|
+
logger.info(f"Folder {user_config_path} removed.")
|
|
125
|
+
console.print(f"Folder {user_config_path} removed.")
|
|
126
|
+
if user_example_model_path.exists():
|
|
127
|
+
user_example_model_path.unlink(missing_ok=True)
|
|
128
|
+
logger.info(f"File {user_example_model_path} removed.")
|
|
129
|
+
console.print(f"File {user_example_model_path} removed.")
|
|
130
|
+
|
|
131
|
+
def reset_config(self):
|
|
132
|
+
self.clear_config()
|
|
133
|
+
self.init_config()
|
|
134
|
+
|