├── requirements.txt
├── AR-Net_paper.pdf
├── .gitattributes
├── scripts
├── pre_commit.bash
├── pre_push.bash
├── install_hooks.bash
└── arnet_dev_setup
├── .gitignore
├── pyproject.toml
├── setup.py
├── LICENSE
├── arnet
├── __init__.py
├── utils.py
├── plotting.py
├── utils_data.py
├── ar_net_legacy.py
├── fastai_mods.py
├── create_ar_data.py
└── ar_net.py
├── v0_1
├── model.py
├── example.py
├── data_loader.py
├── utils.py
└── training.py
├── README.md
├── tests
├── test_unit.py
├── test_integration.py
└── test_legacy.py
└── example_notebooks
├── create_ar_data.ipynb
├── make_dataset.ipynb
├── 01_fit_arnet.ipynb
└── legacy_run_experiments.ipynb
/requirements.txt:
--------------------------------------------------------------------------------
1 | fastai>=2.1.4
2 | statsmodels>=0.12.1
3 | seaborn>=0.11.0
--------------------------------------------------------------------------------
/AR-Net_paper.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ourownstory/AR-Net/HEAD/AR-Net_paper.pdf
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Jupyter notebook language stats
2 |
3 | # For text count
4 | # *.ipynb text
5 |
6 | # To ignore it use below
7 | *.ipynb linguist-documentation
8 |
--------------------------------------------------------------------------------
/scripts/pre_commit.bash:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | files=$(git diff --staged --name-only --diff-filter=d -- "*.py")
6 | for file in $files; do
7 | black "$file"
8 | git add "$file"
9 | done
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __py*
2 | *.egg*
3 | *.idea*
4 | *.log
5 | **/*.pyc
6 | **/__pycache__/
7 | **/*.gz
8 | **/*.whl
9 | **/*egg-info/
10 | site/
11 | *.ipynb_checkpoints*
12 | ar_data/
13 | results/
14 | */models/*
15 |
--------------------------------------------------------------------------------
/scripts/pre_push.bash:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo "Running pre-push hook: unittests"
4 | if ! python3 -m unittest discover -s tests;
5 | then
6 | echo "Failed tests. Unittests must pass before push!"
7 | exit 1
8 | fi
--------------------------------------------------------------------------------
/scripts/install_hooks.bash:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo "Installing hooks..."
4 | GIT_DIR=$(git rev-parse --git-dir)
5 | # create symlink to our pre-commit and pre-push scripts
6 | ln -s ../../scripts/pre_commit.bash "$GIT_DIR"/hooks/pre-commit
7 | ln -s ../../scripts/pre_push.bash "$GIT_DIR"/hooks/pre-push
8 | # make the symlinks executable
9 | chmod a+rwx "$GIT_DIR"/hooks/pre-commit
10 | chmod a+rwx "$GIT_DIR"/hooks/pre-push
11 | echo "Done!"
12 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 120
3 | target-version = ['py38']
4 | include = '\.pyi?$'
5 | exclude = '''
6 |
7 | (
8 | /(
9 | \.eggs # exclude a few common directories in the
10 | | \.git # root of the project
11 | | \.hg
12 | | \.mypy_cache
13 | | \.tox
14 | | \.venv
15 | | _build
16 | | buck-out
17 | | build
18 | | dist
19 | | site
20 | | ar_data # exclude our project directories that are not current source code.
21 | | v0_1
22 | | v1_0
23 | | results
24 | )/
25 | | .gitignore
26 | )
27 | '''
--------------------------------------------------------------------------------
/scripts/arnet_dev_setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os
4 | import subprocess
5 |
6 |
7 | def install_hooks():
8 | dir_scripts = os.path.abspath(os.path.dirname(__file__))
9 | script_files = [
10 | "install_hooks.bash",
11 | "pre_commit.bash",
12 | "pre_push.bash",
13 | ]
14 | for script_f in script_files:
15 | file = os.path.join(dir_scripts, script_f)
16 | subprocess.check_call(["chmod", "a+rwx", file])
17 | subprocess.call(os.path.join(dir_scripts, "install_hooks.bash"), shell=True)
18 |
19 |
20 | if __name__ == "__main__":
21 | install_hooks()
22 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import setuptools
3 |
4 | dir_repo = os.path.abspath(os.path.dirname(__file__))
5 | # read the contents of REQUIREMENTS file
6 | with open(os.path.join(dir_repo, "requirements.txt"), "r") as f:
7 | requirements = f.read().splitlines()
8 | # read the contents of README file
9 | with open(os.path.join(dir_repo, "README.md"), encoding="utf-8") as f:
10 | readme = f.read()
11 |
12 | setuptools.setup(
13 | name="arnet",
14 | version="1.2.0",
15 | description="A simple auto-regressive Neural Network for time-series",
16 | author="Oskar Triebe",
17 | url="https://github.com/ourownstory/AR-Net",
18 | packages=setuptools.find_packages(),
19 | python_requires=">=3.8",
20 | install_requires=requirements,
21 | extras_require={
22 | "dev": ["black"],
23 | },
24 | # setup_requires=[""],
25 | scripts=["scripts/arnet_dev_setup"],
26 | long_description=readme,
27 | long_description_content_type="text/markdown",
28 | )
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Oskar Triebe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/arnet/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | log = logging.getLogger("ARNet")
4 | log.setLevel("INFO")
5 | # Create handlers
6 | c_handler = logging.StreamHandler()
7 | f_handler = logging.FileHandler("logs.log", "w+")
8 | # c_handler.setLevel("WARNING")
9 | # f_handler.setLevel("INFO")
10 | # Create formatters and add it to handlers
11 | c_format = logging.Formatter("%(levelname)s: %(name)s - %(funcName)s: %(message)s")
12 | c_handler.setFormatter(c_format)
13 | log.addHandler(c_handler)
14 |
15 | # uncomment for a log file
16 | # f_format = logging.Formatter("%(asctime)s; %(levelname)s; %(name)s; %(funcName)s; %(message)s")
17 | # f_handler.setFormatter(f_format)
18 | # log.addHandler(f_handler)
19 |
20 | # lazy imports ala fastai2 style (for nice print functionality)
21 | from fastai.basics import *
22 | from fastai.tabular.all import *
23 |
24 | from .ar_net import ARNet
25 |
26 | # from .ar_net_legacy import init_ar_learner
27 | from .utils_data import load_from_file, tabularize_univariate, estimate_noise, split_by_p_valid
28 | from .utils import pad_ar_params, nice_print_list, compute_sTPE, coeff_from_model
29 | from .plotting import plot_weights, plot_prediction_sample, plot_error_scatter
30 |
--------------------------------------------------------------------------------
/v0_1/model.py:
--------------------------------------------------------------------------------
1 | import torch.nn as nn
2 | import torch.nn.functional as F
3 |
4 |
5 | class DAR(nn.Module):
6 | '''
7 | A simple, general purpose, fully connected network
8 | '''
9 |
10 | def __init__(self, ar, num_layers=1, d_hidden=None):
11 | # Perform initialization of the pytorch superclass
12 | super().__init__()
13 | # Define network layer dimensions
14 | d_in, d_out = [ar, 1]
15 | self.ar = ar
16 | self.num_layers = num_layers
17 | if d_hidden is None and num_layers > 1:
18 | d_hidden = d_in
19 | if self.num_layers == 1:
20 | self.layer_1 = nn.Linear(d_in, d_out, bias=True)
21 | else:
22 | self.layer_1 = nn.Linear(d_in, d_hidden, bias=True)
23 | self.mid_layers = []
24 | for i in range(self.num_layers - 2):
25 | self.mid_layers.append(nn.Linear(d_hidden, d_hidden, bias=True))
26 | self.layer_out = nn.Linear(d_hidden, d_out, bias=True)
27 |
28 | def forward(self, x):
29 | '''
30 | This method defines the network layering and activation functions
31 | '''
32 | activation = F.relu
33 | x = self.layer_1(x)
34 | if self.num_layers > 1:
35 | x = activation(x)
36 | for layer in self.mid_layers:
37 | x = layer(x)
38 | x = activation(x)
39 | x = self.layer_out(x)
40 | return x
41 |
42 |
43 | def main():
44 | pass
45 |
46 |
47 | if __name__ == "__main__":
48 | main()
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/psf/black)
2 |
3 | # AR-Net
4 | A simple auto-regressive Neural Network for time-series ([link to paper](https://arxiv.org/abs/1911.12436)).
5 |
6 | ## Install
7 | After downloading the code repository (via `git clone`), change to the repository directory (`cd AR-Net`)
8 | and install arnet as python package with `pip install .`
9 |
10 | ## Use
11 | View the notebook [`example_notebooks/arnet.ipynb`](example_notebooks/01_fit_arnet.ipynb) for an example of how to use the model.
12 |
13 | ## Versions
14 | ### Current (1.2)
15 | The version 1.0 made the model easier to use with your own datasets and requires less hyperparameters
16 | for a simpler training procedure. It is built on the fastai library.
17 |
18 | Changes (1.1 -> 1.2):
19 | * simplified UI with ARNet as object
20 | * GPU support
21 | * robustified training
22 | * added test cases
23 | * updated example notebooks
24 |
25 | Changes (1.0 -> 1.1):
26 | * port [beta fastai2](https://github.com/fastai/fastai2) to it's current [stable release](https://github.com/fastai/fastai)
27 | * make install as pip package possible
28 | * add black code formatter (and git pre-commit hook)
29 | * add unittests (and git pre-push hook)
30 | * fix issues with new fastai api
31 | * remove old code fragments
32 |
33 | ### Pure PyTorch (0.1)
34 | Version 0.1 was based on Pytorch and you can still use it if you do not want to use fastai.
35 |
36 | See file [`v0_1/example.py`](v0_1/example.py) for how to use the v0.1 model.
37 |
38 | ### Now also part of NeuralProphet
39 | AR-Net is now part of a more comprehensive package [NeuralProphet](https://github.com/ourownstory/neural_prophet).
40 |
41 | I strongly recommend using it instead of the standalone version, unless you specifically want to use AR-Net,
42 | which may make sense if you need to model a highly-autoregressive time-series with sparse long-range dependencies.
43 |
--------------------------------------------------------------------------------
/arnet/utils.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import torch
3 |
4 |
5 | def pad_ar_params(ar_params, n_lags, n_forecasts=1):
6 | """ "
7 | pads ar_parameter lists to the length of n_lags
8 | ar_params: list of length n_forecasts with elements: lists of ar coeffs
9 | n_lags: length to which pad each of the ar coeffs
10 | """
11 | assert n_forecasts == len(ar_params)
12 | if n_forecasts != 1:
13 | if all([isinstance(ar_params[i], list) for i in range(n_forecasts)]):
14 | return [pad_ar_params([ar_params[i]], n_lags, 1)[0] for i in range(n_forecasts)]
15 | else:
16 | raise NotImplementedError("AR Coeff for each of the forecast targets are needed")
17 | return [ar_params[0] + [0.0] * (n_lags - len(ar_params[0]))]
18 |
19 |
20 | def nice_print_list(data):
21 | if all([isinstance(data[i], list) for i in range(len(data))]):
22 | return [nice_print_list(data[i]) for i in range(len(data))]
23 | return ["{:.3f}".format(x) for x in data]
24 | # return [["{:.2f}".format(x) for x in sublist] for sublist in data]
25 |
26 |
27 | def compute_sTPE(est, real):
28 | est, real = np.array(est), np.array(real)
29 | sum_abs_diff = np.sum(np.abs(est - real))
30 | sum_abs = np.sum(np.abs(est) + np.abs(real))
31 | return 100.0 * sum_abs_diff / (10e-9 + sum_abs)
32 |
33 |
34 | def coeff_from_model(model, reversed_weights=True):
35 | for layer in model.modules():
36 | if isinstance(layer, torch.nn.Linear):
37 | weights = [list(x[::-1] if reversed_weights else x) for x in layer.weight.detach().cpu().numpy()]
38 | return weights # note: preliminary exit of loop is a feature.
39 |
40 |
41 | def set_logger_level(logger, log_level=None, include_handlers=False):
42 | if log_level is None:
43 | logger.warning("Failed to set log_level to None.")
44 | elif log_level not in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", 10, 20, 30, 40, 50):
45 | logger.error(
46 | "Failed to set log_level to {}."
47 | "Please specify a valid log level from: "
48 | "'DEBUG', 'INFO', 'WARNING', 'ERROR' or 'CRITICAL'"
49 | "".format(log_level)
50 | )
51 | else:
52 | logger.setLevel(log_level)
53 | if include_handlers:
54 | for h in logger.handlers:
55 | h.setLevel(log_level)
56 | logger.debug("Set log level to {}".format(log_level))
57 |
--------------------------------------------------------------------------------
/arnet/plotting.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import matplotlib.pyplot as plt
3 | import seaborn as sns
4 | import os
5 |
6 |
7 | def plot_weights(ar_val, weights, ar=None, model_name="AR-Net", save=False, savedir="results", figsize=(10, 4)):
8 | if ar is not None:
9 | df = pd.DataFrame(
10 | zip(
11 | list(range(1, ar_val + 1)) * 2,
12 | ["AR-Process (True)"] * ar_val + [model_name] * ar_val,
13 | list(ar) + list(weights),
14 | ),
15 | columns=["AR-coefficient (lag number)", "model", "value (weight)"],
16 | )
17 | plt.figure(figsize=figsize)
18 | palette = {"Classic-AR": "C0", "AR-Net": "C1", "AR-Process (True)": "k"}
19 | sns.barplot(data=df, palette=palette, x="AR-coefficient (lag number)", hue="model", y="value (weight)")
20 | else:
21 | df = pd.DataFrame(
22 | zip(
23 | list(range(1, ar_val + 1)),
24 | [model_name] * ar_val,
25 | list(weights),
26 | ),
27 | columns=["AR-coefficient (lag number)", "model", "value (weight)"],
28 | )
29 | plt.figure(figsize=figsize)
30 | sns.barplot(data=df, x="AR-coefficient (lag number)", hue="model", y="value (weight)")
31 | if save:
32 | if not os.path.exists(savedir):
33 | os.makedirs(savedir)
34 | figname = "weights_{}_{}.png".format(ar_val, model_name)
35 | plt.savefig(os.path.join(savedir, figname), dpi=300, bbox_inches="tight")
36 | else:
37 | plt.show()
38 |
39 |
40 | def plot_prediction_sample(predicted, actual, model_name="AR-Net", save=False, savedir="results"):
41 | fig2 = plt.figure()
42 | fig2.set_size_inches(10, 6)
43 | plt.plot(actual)
44 | plt.plot(predicted)
45 | plt.legend(["Actual Time-Series", "{}-Prediction".format(model_name)])
46 | if save:
47 | if not os.path.exists(savedir):
48 | os.makedirs(savedir)
49 | figname = "prediction_{}.png".format(model_name)
50 | plt.savefig(os.path.join(savedir, figname), dpi=600, bbox_inches="tight")
51 | else:
52 | plt.show()
53 |
54 |
55 | def plot_error_scatter(predicted, actual, model_name="AR-Net", save=False, savedir="results"):
56 | # error = predicted - actual
57 | fig3 = plt.figure()
58 | fig3.set_size_inches(6, 6)
59 | plt.scatter(actual, predicted - actual, marker="o", s=10, alpha=0.3)
60 | plt.legend(["{}-Error".format(model_name)])
61 | if save:
62 | if not os.path.exists(savedir):
63 | os.makedirs(savedir)
64 | figname = "scatter_{}.png".format(model_name)
65 | plt.savefig(os.path.join(savedir, figname), dpi=600, bbox_inches="tight")
66 | else:
67 | plt.show()
68 |
--------------------------------------------------------------------------------
/tests/test_unit.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import unittest
4 | import os
5 | import pathlib
6 | import shutil
7 | import logging
8 | import random
9 | import pandas as pd
10 | import numpy as np
11 | import warnings
12 |
13 | warnings.filterwarnings("ignore", message=".*nonzero.*", category=UserWarning)
14 |
15 | import arnet
16 | from arnet.create_ar_data import generate_armaprocess_data
17 | from arnet.create_ar_data import save_to_file, load_from_file
18 |
19 | log = logging.getLogger("ARNet.test")
20 | log.setLevel("DEBUG")
21 | log.parent.setLevel("WARNING")
22 |
23 | DIR = pathlib.Path(__file__).parent.absolute()
24 | data_path = os.path.join(DIR, "ar_data")
25 | results_path = os.path.join(DIR, "results")
26 | EPOCHS = 2
27 |
28 |
29 | class UnitTests(unittest.TestCase):
30 | save = True
31 |
32 | def test_create_data_random(self):
33 | # option 1: Randomly generated AR parameters
34 | data_config = {
35 | "samples": 1000,
36 | "noise_std": 0.1,
37 | "ar_order": 3,
38 | "ma_order": 0,
39 | "params": None, # for randomly generated AR params
40 | }
41 | log.debug("{}".format(data_config))
42 |
43 | # Generate data
44 | series, data_config["ar_params"], data_config["ma_params"] = generate_armaprocess_data(**data_config)
45 |
46 | if self.save:
47 | del data_config["params"]
48 | data_name = save_to_file(data_path, series, data_config)
49 | # just to test:
50 | df, data_config2 = load_from_file(data_path, data_name, load_config=True)
51 | log.debug("loaded from saved files:")
52 | log.debug("{}".format(data_config2))
53 | log.debug("{}".format(df.head()))
54 |
55 | def test_create_data_manual(self):
56 | # option 1: Randomly generated AR parameters
57 | # option 2: Manually define AR parameters
58 | data_config = {
59 | "samples": 1000,
60 | "noise_std": 0.1,
61 | "params": ([0.2, 0.3, -0.5], []),
62 | # "params": ([0.2, 0, 0.3, 0, 0, 0, 0, 0, 0, -0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], []),
63 | }
64 | data_config["ar_order"] = int(sum(np.array(data_config["params"][0]) != 0.0))
65 | data_config["ma_order"] = int(sum(np.array(data_config["params"][1]) != 0.0))
66 | log.debug("{}".format(data_config))
67 |
68 | # Generate data
69 | series, data_config["ar_params"], data_config["ma_params"] = generate_armaprocess_data(**data_config)
70 |
71 | if self.save:
72 | del data_config["params"]
73 | data_name = save_to_file(data_path, series, data_config)
74 | # just to test:
75 | df, data_config2 = load_from_file(data_path, data_name, load_config=True)
76 | log.debug("loaded from saved files:")
77 | log.debug("{}".format(data_config2))
78 | log.debug("{}".format(df.head()))
79 |
--------------------------------------------------------------------------------
/arnet/utils_data.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import numpy as np
3 | import logging
4 |
5 | from arnet.create_ar_data import load_from_file
6 |
7 | log = logging.getLogger("ARNet")
8 |
9 |
10 | def estimate_noise(series):
11 | return float(np.mean(np.abs(series.iloc[:-1].values - series.iloc[1:].values)))
12 |
13 |
14 | def split_by_p_valid(valid_p, n_sample, verbose=False):
15 | split_idx = int(n_sample * (1 - valid_p))
16 | splits = [list(range(split_idx)), list(range(split_idx, n_sample))]
17 | if verbose:
18 | print("split on idx: ", split_idx)
19 | print("split sizes: ", [len(x) for x in splits])
20 | return splits
21 |
22 |
23 | def tabularize_univariate(series, n_lags, n_forecasts=1, nested_list=False):
24 | """
25 | Create a tabular dataset with ar_order lags for supervised forecasting
26 | Arguments:
27 | series: Sequence of observations as a Pandas DataFrame
28 | n_lags: Number of lag observations as input (X).
29 | n_forecasts: Number of observations as output (y).
30 | Returns:
31 | df: Pandas DataFrame of input lags and forecast values (as nested lists)
32 | shape (n_samples, 2).
33 | Cols: "x": list(n_lags)
34 | Cols: "y": list(n_forecasts)
35 | """
36 | n_samples = len(series) - n_lags + 1 - n_forecasts
37 |
38 | x = pd.DataFrame([series.iloc[i : i + n_lags, 0].values for i in range(n_samples)])
39 | y = pd.DataFrame([series.iloc[i + n_lags : i + n_lags + n_forecasts, 0].values for i in range(n_samples)])
40 | if nested_list:
41 | df = pd.concat([x.apply(list, axis=1), y.apply(list, axis=1)], axis=1)
42 | df.columns = ["x", "y"]
43 | else:
44 | df = pd.concat([x, y], axis=1)
45 | df.columns = ["x_{}".format(num) for num in list(range(len(x.columns)))] + [
46 | "y_{}".format(num) for num in list(range(len(y.columns)))
47 | ]
48 | return df
49 |
50 |
51 | def main():
52 | verbose = True
53 | data_path = "ar_data"
54 | data_name = "ar_3_ma_0_noise_0.100_len_10000"
55 |
56 | ## if created AR data with create_ar_data, we can use the helper function:
57 | df, data_config = load_from_file(data_path, data_name, load_config=True)
58 | n_lags = data_config["ar_order"]
59 |
60 | ## else we can manually load any file that stores a time series, for example:
61 | # df = pd.read_csv(os.path.join(data_path, data_name + '.csv'), header=None, index_col=False)
62 | # n_lags = 3
63 |
64 | if verbose:
65 | print(data_config)
66 | print(df.shape)
67 |
68 | ## create a tabularized dataset from time series
69 | df_tab = tabularize_univariate(
70 | df,
71 | n_lags=n_lags,
72 | n_forecasts=1,
73 | nested_list=False,
74 | )
75 |
76 | if verbose:
77 | print("tabularized df")
78 | print(df_tab.shape)
79 | # print(df_tab.columns)
80 | # if nested_list:
81 | # print("x_dim:", len(df_tab['x'][0]), "y_dim:", len(df_tab['y'][0]))
82 | print(df_tab.head())
83 |
84 |
85 | if __name__ == "__main__":
86 | main()
87 |
--------------------------------------------------------------------------------
/arnet/ar_net_legacy.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | import logging
3 |
4 | import fastai
5 |
6 | # lazy imports ala fastai2 style (for nice print functionality)
7 | from fastai.basics import *
8 | from fastai.tabular.all import *
9 |
10 | # explicit imports (for reference)
11 | # from fastai.data.core import DataLoaders
12 | # from fastai.metrics import mse, mae
13 | # from fastai.tabular.core import TabularPandas, TabDataLoader
14 | # from fastai.tabular.learner import tabular_learner
15 | # from fastai.data.transforms import Normalize
16 |
17 |
18 | ## import arnet
19 | from arnet.utils_data import load_from_file, tabularize_univariate, estimate_noise, split_by_p_valid
20 | from arnet.utils import pad_ar_params, nice_print_list, compute_sTPE, coeff_from_model
21 | from arnet import utils, plotting
22 | from arnet.fastai_mods import SparsifyAR, sTPE
23 |
24 | log = logging.getLogger("ARNet")
25 |
26 |
27 | def init_ar_learner(
28 | series,
29 | ar_order,
30 | n_forecasts=1,
31 | valid_p=0.1,
32 | sparsity=None,
33 | ar_params=None,
34 | train_bs=32,
35 | valid_bs=128,
36 | verbose=False,
37 | ):
38 | if sparsity is not None and sparsity == 1.0:
39 | sparsity = None
40 | df_all = tabularize_univariate(series, ar_order, n_forecasts)
41 | est_noise = estimate_noise(series)
42 |
43 | if verbose:
44 | print("tabularized df")
45 | print("df columns", list(df_all.columns))
46 | print("df shape", df_all.shape)
47 | # if nested_list: print("x_dim:", len(df_all['x'][0]), "y_dim:", len(df_all['y'][0]))
48 | # print("df head(3)", df_all.head(3))
49 | print("estimated noise of series", est_noise)
50 |
51 | ## split
52 | splits = split_by_p_valid(valid_p, len(df_all), verbose)
53 |
54 | cont_names = [col for col in list(df_all.columns) if "x_" == col[:2]]
55 | target_names = [col for col in list(df_all.columns) if "y_" == col[:2]]
56 |
57 | ## preprocess?
58 | # procs = [Normalize]
59 | procs = []
60 |
61 | tp = TabularPandas(df_all, procs=procs, cat_names=None, cont_names=cont_names, y_names=target_names, splits=splits)
62 | if verbose:
63 | print("cont var num", len(tp.cont_names), tp.cont_names)
64 | # print(tp.iloc[0:5])
65 |
66 | ### next: data loader, learner
67 | trn_dl = TabDataLoader(tp.train, bs=train_bs, shuffle=True, drop_last=True)
68 | val_dl = TabDataLoader(tp.valid, bs=valid_bs)
69 | dls = DataLoaders(trn_dl, val_dl)
70 |
71 | # if verbose:
72 | # print("showing batch")
73 | # print(dls.show_batch(show=False))
74 |
75 | callbacks = []
76 | if sparsity is not None:
77 | callbacks.append(SparsifyAR(sparsity, est_noise))
78 | if verbose:
79 | print("reg lam: ", callbacks[0].lam)
80 |
81 | metrics = [mae]
82 | if ar_params is not None:
83 | metrics.append(sTPE(ar_params, at_epoch_end=False))
84 |
85 | tm_config = {"use_bn": False, "bn_final": False, "bn_cont": False}
86 | learn = tabular_learner(
87 | dls,
88 | layers=[], # Note: None defaults to [200, 100]
89 | config=tm_config, # None calls tabular_config()
90 | n_out=len(target_names), # None calls get_c(dls)
91 | train_bn=False, # passed to Learner
92 | metrics=metrics, # passed on to TabularLearner, to parent Learner
93 | loss_func=mse,
94 | cbs=callbacks,
95 | )
96 | if verbose:
97 | print(learn.model)
98 | return learn
99 |
--------------------------------------------------------------------------------
/tests/test_integration.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import unittest
4 | import os
5 | import pathlib
6 | import shutil
7 | import logging
8 | import random
9 | import pandas as pd
10 | import warnings
11 |
12 | warnings.filterwarnings("ignore", message=".*nonzero.*", category=UserWarning)
13 | import arnet
14 |
15 | log = logging.getLogger("ARNet.test")
16 | log.setLevel("WARNING")
17 | log.parent.setLevel("WARNING")
18 |
19 | DIR = pathlib.Path(__file__).parent.absolute()
20 | data_path = os.path.join(DIR, "ar_data")
21 | results_path = os.path.join(DIR, "results_test")
22 | AR_FILE = "ar_3_ma_0_noise_0.100_len_1000"
23 | EPOCHS = 2
24 |
25 |
26 | class IntegrationTests(unittest.TestCase):
27 | plot = False
28 | save = False
29 |
30 | def test_random_data(self):
31 | df = pd.DataFrame({"x": [random.gauss(0.0, 1.0) for i in range(1000)]})
32 | m = arnet.ARNet(ar_order=3, n_epoch=3)
33 | m.tabularize(df)
34 | m.make_datasets()
35 | m.create_regularizer(sparsity=0.3)
36 | m.create_learner()
37 | m.find_lr(plot=False)
38 | m.fit_one_cycle(cycles=2, plot=False)
39 | log.info("coeff of random data: {}".format(m.coeff))
40 |
41 | def test_plot(self):
42 | if not os.path.exists(results_path):
43 | os.makedirs(results_path)
44 |
45 | df = pd.DataFrame({"x": [random.gauss(0.0, 1.0) for i in range(1000)]})
46 | m = arnet.ARNet(ar_order=3, n_epoch=3)
47 | m.fit(series=df)
48 | if self.plot:
49 | m.learn.recorder.plot_loss()
50 | m.plot_weights(save=True, savedir=results_path)
51 | m.plot_fitted_obs(num_obs=100, save=True, savedir=results_path)
52 | m.plot_errors(save=True, savedir=results_path)
53 |
54 | shutil.rmtree(results_path)
55 |
56 | def test_save_load(self):
57 | if not os.path.exists(results_path):
58 | os.makedirs(results_path)
59 |
60 | df = pd.DataFrame({"x": [random.gauss(0.0, 1.0) for i in range(1000)]})
61 | m = arnet.ARNet(ar_order=3, n_epoch=3)
62 | m.fit(series=df)
63 |
64 | # Optional:save and create inference learner
65 | sparsity = 1.0 if m.sparsity is None else m.sparsity
66 | model_name = "ar{}_sparse_{:.3f}_ahead_{}_epoch_{}.pkl".format(m.ar_order, sparsity, m.n_forecasts, m.n_epoch)
67 | m.save_model(results_path=results_path, model_name=model_name)
68 | # can be loaded like this
69 | m.load_model(results_path, model_name)
70 | # can unfreeze the model and fine_tune
71 | log.info("loaded coeff: {}".format(m.coeff))
72 |
73 | shutil.rmtree(results_path)
74 |
75 | def test_ar_data(self):
76 | df, data_config = arnet.load_from_file(data_path, AR_FILE, load_config=True, verbose=False)
77 | df = df[:1000]
78 |
79 | # Hyperparameters
80 | sparsity = 0.3
81 | ar_order = int(1 / sparsity * data_config["ar_order"]) # sparse AR: (for non-sparse, set sparsity to 1.0)
82 | ar_params = arnet.pad_ar_params([data_config["ar_params"]], ar_order, 1) # to compute stats
83 |
84 | # run
85 | m = arnet.ARNet(
86 | ar_order=ar_order,
87 | n_epoch=EPOCHS,
88 | sparsity=sparsity,
89 | ar_params=ar_params,
90 | )
91 | m.fit(series=df)
92 |
93 | # Look at Coeff
94 | log.info("ar params: {}".format(arnet.nice_print_list(ar_params)))
95 | log.info("model weights: {}".format(arnet.nice_print_list(m.coeff)))
96 |
--------------------------------------------------------------------------------
/v0_1/example.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from data_loader import load_data
3 | from training import run as run_training
4 |
5 | from v0_1_pure_pytorch import utils
6 |
7 |
8 | def load_config(verbose=False, random=True):
9 | # load specified settings
10 |
11 | #### Data settings ####
12 | data_config = {
13 | "type": 'AR',
14 | "ar_val": 3,
15 | "pad_to": 10, # set to >ar_val for sparse AR estimation
16 | "ar_params": None, # for randomly generated AR params
17 | "noise_std": 1.0,
18 | "test": 0.2,
19 | "n_samples": int(1.25e5), # for 1e5 train size
20 | }
21 |
22 | # OR manually define AR params:
23 | if not random:
24 | # data_config["ar_params"] = [0.2, 0.3, -0.5]
25 | # Alternative: sparse AR params:
26 | data_config["ar_params"] = [0.2, 0, 0.3, 0, 0, 0, 0, 0, 0, -0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
27 |
28 | # correct settings if manually set
29 | data_config["ar_val"] = sum(np.array(data_config["ar_params"]) != 0.0)
30 | data_config["pad_to"] = int(len(data_config["ar_params"]))
31 |
32 | #### Model settings ####
33 | model_config = {
34 | "ar": data_config["ar_val"], # for normal AR
35 | "ma": 0,
36 | "num_layers": 1,
37 | "d_hidden": None
38 | }
39 | if data_config["pad_to"] is not None and data_config["pad_to"] > data_config["ar_val"]:
40 | model_config["ar"] = data_config["pad_to"] # for sparse AR
41 |
42 | #### Train settings ####
43 | train_config = {
44 | "lr": 2e-4,
45 | "lr_decay": 0.9,
46 | "epochs": 10,
47 | "batch": 128,
48 | "est_sparsity": 1, # 0 = fully sparse, 1 = not sparse
49 | "lambda_delay": 10, # delays start of regularization by lambda_delay epochs
50 | }
51 | # For auto-regularization based on sparsity estimation:
52 | if data_config["pad_to"] is not None and data_config["pad_to"] > data_config["ar_val"]:
53 | train_config["est_sparsity"] = data_config["ar_val"] / (1.0 * data_config["pad_to"])
54 |
55 | # Note: find the right learning rate range with a learning rate range test
56 | # e.g. a LR range test on random AR data (with 5e5 data, batch 64, pad_to 100) led to
57 | # ---> min 5e-7, max 5e-4
58 |
59 | if verbose:
60 | print("data_config\n", data_config)
61 | print("model_config\n", model_config)
62 | print("train_config\n", train_config)
63 |
64 | return data_config, model_config, train_config
65 |
66 |
67 | def main(verbose=False, plot=False, save=False, random_ar_param=True):
68 | # load configuration dicts. Could be implemented to load from JSON instead.
69 | data_config, model_config, train_config = load_config(verbose, random_ar_param)
70 | # loads randomly generated data. Could be implemented to load a specific dataset instead.
71 | data = load_data(data_config, verbose, plot)
72 | # runs training and testing.
73 | results_dar, stats_dar = run_training(data, model_config, train_config, verbose)
74 |
75 | # optional printing
76 | if verbose:
77 | print(stats_dar)
78 |
79 | # optional plotting
80 | if plot:
81 | utils.plot_loss_curve(
82 | losses=results_dar["losses"],
83 | test_loss=results_dar["test_mse"],
84 | epoch_losses=results_dar["epoch_losses"],
85 | show=False,
86 | save=save
87 | )
88 | utils.plot_weights(
89 | model_config["ar"],
90 | results_dar["weights"],
91 | data["ar"],
92 | model_name="AR-Net",
93 | save=save
94 | )
95 | utils.plot_results(
96 | results_dar,
97 | model_name="AR-Net",
98 | save=save
99 | )
100 |
101 |
102 | if __name__ == "__main__":
103 | main(verbose=True, plot=True, save=True, random_ar_param=False)
104 |
--------------------------------------------------------------------------------
/arnet/fastai_mods.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import torch
3 | import torch.nn.functional as F
4 | import fastai
5 | from fastai.torch_core import flatten_check
6 | from fastai.metrics import mse, mae
7 | from fastai.basics import Callback
8 | from fastai.learner import Metric
9 |
10 | from arnet import utils
11 |
12 | log = logging.getLogger("ARNet.fastai_mods")
13 |
14 |
15 | def huber(inp, targ):
16 | """Huber error between `inp` and `targ`."""
17 | return F.smooth_l1_loss(*flatten_check(inp, targ))
18 |
19 |
20 | class SparsifyAR(Callback):
21 | """Callback that adds regularization of first linear layer according to AR-Net paper"""
22 |
23 | def __init__(
24 | self,
25 | est_sparsity,
26 | est_noise=1.0,
27 | reg_strength=0.01,
28 | start_pct=0.0,
29 | full_pct=0.5,
30 | c1=2.0,
31 | c2=2.0,
32 | **kwargs,
33 | ):
34 | super().__init__(**kwargs)
35 | self.lam_max = 0.0
36 | est_noise = 1.0 if est_noise is None else est_noise
37 | if est_sparsity is not None:
38 | assert 1 >= est_sparsity > 0
39 | self.lam_max = reg_strength * est_noise * ((1.0 + 1e-9) / (est_sparsity + 1e-9) - 1.0)
40 | self.start_pct = start_pct
41 | self.full_pct = full_pct
42 | # note: implementation in paper used c1 = 3.0, c2 = 3.0
43 | self.c1 = c1
44 | self.c2 = c2
45 | self.lam = None
46 |
47 | def after_loss(self):
48 | if not self.training:
49 | return
50 | if self.lam_max == 0.0 or self.lam == 0.0:
51 | return
52 | abs_weights = None
53 | for layer in self.learn.model.modules():
54 | if isinstance(layer, torch.nn.Linear):
55 | abs_weights = torch.abs(layer.weight)
56 | break
57 | if abs_weights is None:
58 | raise NotImplementedError("weight regualarization only implemented for model with Linear layer")
59 | reg = torch.div(2.0, 1.0 + torch.exp(-self.c1 * abs_weights.pow(1.0 / self.c2))) - 1.0
60 |
61 | progress_iter = (1.0 + self.learn.iter) / (1.0 * self.learn.n_iter)
62 | progress = (progress_iter + self.learn.epoch) / (1.0 * self.learn.n_epoch)
63 | progress = (progress - self.start_pct) / (self.full_pct - self.start_pct)
64 | if progress <= 0:
65 | self.lam = 0.0
66 | elif progress < 1:
67 | self.lam = self.lam_max * progress ** 2
68 | else:
69 | self.lam = self.lam_max
70 |
71 | self.learn.loss += self.lam * torch.mean(reg)
72 |
73 | _docs = dict(after_loss="Add regularization of first linear layer")
74 |
75 |
76 | class sTPE(Metric):
77 | """ "
78 | Symmetrical Total Percentage Error of learned weights compared to underlying AR coefficients.
79 | Computed as the average over snapshots at each batch.
80 | """
81 |
82 | def __init__(self, ar_params, at_epoch_end=False):
83 | self.ar_params = ar_params
84 | self.at_epoch_end = at_epoch_end
85 | self.total = 0.0
86 | self.count = 0
87 | self.sTPE = None
88 |
89 | def reset(self):
90 | self.total = 0.0
91 | self.count = 0
92 | self.sTPE = None
93 |
94 | def accumulate(self, learn):
95 | self.sTPE = fastai.torch_core.to_detach(
96 | utils.compute_sTPE(
97 | est=utils.coeff_from_model(model=learn.model, reversed_weights=True),
98 | real=self.ar_params,
99 | )
100 | )
101 | self.total += self.sTPE
102 | self.count += 1
103 |
104 | @property
105 | def value(self):
106 | if self.at_epoch_end:
107 | return self.sTPE
108 | return self.total / self.count if self.count != 0 else None
109 |
110 | @property
111 | def name(self):
112 | return "sTPE of AR coeff"
113 |
114 |
115 | def get_loss_func(loss_func):
116 | if type(loss_func) == str:
117 | if loss_func.lower() == "mse":
118 | loss_func = mse
119 | elif loss_func.lower() in ["huber", "smooth_l1", "smoothl1"]:
120 | loss_func = huber
121 | elif loss_func.lower() in ["mae", "l1"]:
122 | loss_func = mae
123 | else:
124 | log.error("loss {} not defined".format(loss_func))
125 | loss_func = None
126 | return loss_func
127 |
--------------------------------------------------------------------------------
/example_notebooks/create_ar_data.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import numpy as np\n",
10 | "from statsmodels.tsa.arima_process import ArmaProcess"
11 | ]
12 | },
13 | {
14 | "cell_type": "code",
15 | "execution_count": 2,
16 | "metadata": {},
17 | "outputs": [],
18 | "source": [
19 | "from arnet.create_ar_data import generate_armaprocess_data\n",
20 | "from arnet.create_ar_data import save_to_file, load_from_file"
21 | ]
22 | },
23 | {
24 | "cell_type": "code",
25 | "execution_count": 3,
26 | "metadata": {},
27 | "outputs": [],
28 | "source": [
29 | "#### Notebook settings ####\n",
30 | "random_ar_params = False\n",
31 | "save = True\n",
32 | "save_path = '../ar_data'"
33 | ]
34 | },
35 | {
36 | "cell_type": "code",
37 | "execution_count": 4,
38 | "metadata": {},
39 | "outputs": [],
40 | "source": [
41 | "#### Data settings ####\n",
42 | "# option 1: Randomly generated AR parameters\n",
43 | "data_config_random = {\n",
44 | " \"samples\": 10000,\n",
45 | " \"noise_std\": 0.1,\n",
46 | " \"ar_order\": 3,\n",
47 | " \"ma_order\": 0,\n",
48 | " \"params\": None, # for randomly generated AR params\n",
49 | "}\n",
50 | "\n",
51 | "# option 2: Manually define AR parameters\n",
52 | "data_config_manual = {\n",
53 | " \"samples\": 10000,\n",
54 | " \"noise_std\": 0.1,\n",
55 | " \"params\": ([0.2, 0.3, -0.5], []), \n",
56 | "# \"params\": ([0.2, 0, 0.3, 0, 0, 0, 0, 0, 0, -0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], []), \n",
57 | "}\n",
58 | "data_config_manual[\"ar_order\"] = int(sum(np.array(data_config_manual[\"params\"][0]) != 0.0))\n",
59 | "data_config_manual[\"ma_order\"] = int(sum(np.array(data_config_manual[\"params\"][1]) != 0.0))"
60 | ]
61 | },
62 | {
63 | "cell_type": "code",
64 | "execution_count": 5,
65 | "metadata": {},
66 | "outputs": [
67 | {
68 | "name": "stdout",
69 | "output_type": "stream",
70 | "text": [
71 | "{'samples': 10000, 'noise_std': 0.1, 'params': ([0.2, 0.3, -0.5], []), 'ar_order': 3, 'ma_order': 0}\n"
72 | ]
73 | }
74 | ],
75 | "source": [
76 | "## Select config\n",
77 | "data_config = data_config_random if random_ar_params else data_config_manual\n",
78 | "print(data_config)"
79 | ]
80 | },
81 | {
82 | "cell_type": "code",
83 | "execution_count": 6,
84 | "metadata": {},
85 | "outputs": [],
86 | "source": [
87 | "## Generate data\n",
88 | "series, data_config[\"ar_params\"], data_config[\"ma_params\"] = generate_armaprocess_data(**data_config)\n",
89 | "del data_config[\"params\"]"
90 | ]
91 | },
92 | {
93 | "cell_type": "code",
94 | "execution_count": 7,
95 | "metadata": {},
96 | "outputs": [
97 | {
98 | "name": "stdout",
99 | "output_type": "stream",
100 | "text": [
101 | "loaded from saved files:\n",
102 | "{'samples': 10000, 'noise_std': 0.1, 'ar_order': 3, 'ma_order': 0, 'ar_params': [0.2, 0.3, -0.5], 'ma_params': []}\n",
103 | " 0\n",
104 | "0 0.114610\n",
105 | "1 0.027092\n",
106 | "2 -0.015263\n",
107 | "3 -0.125719\n",
108 | "4 -0.279195\n"
109 | ]
110 | }
111 | ],
112 | "source": [
113 | "if save:\n",
114 | " data_name = save_to_file(save_path, series, data_config)\n",
115 | " \n",
116 | " # just to test:\n",
117 | " df, data_config2 = load_from_file(save_path, data_name, load_config=True)\n",
118 | " print(\"loaded from saved files:\")\n",
119 | " print(data_config2)\n",
120 | " print(df.head())"
121 | ]
122 | },
123 | {
124 | "cell_type": "code",
125 | "execution_count": null,
126 | "metadata": {},
127 | "outputs": [],
128 | "source": []
129 | }
130 | ],
131 | "metadata": {
132 | "kernelspec": {
133 | "display_name": "Python 3",
134 | "language": "python",
135 | "name": "python3"
136 | },
137 | "language_info": {
138 | "codemirror_mode": {
139 | "name": "ipython",
140 | "version": 3
141 | },
142 | "file_extension": ".py",
143 | "mimetype": "text/x-python",
144 | "name": "python",
145 | "nbconvert_exporter": "python",
146 | "pygments_lexer": "ipython3",
147 | "version": "3.8.6"
148 | }
149 | },
150 | "nbformat": 4,
151 | "nbformat_minor": 4
152 | }
153 |
--------------------------------------------------------------------------------
/arnet/create_ar_data.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import numpy as np
4 | import pandas as pd
5 | from statsmodels.tsa.arima_process import ArmaProcess
6 |
7 |
8 | def _get_config(noise_std=0.1, n_samples=10000, random_ar_params=True):
9 | # option 1: Randomly generated AR parameters
10 | data_config_random = {"noise_std": noise_std, "ar_order": 3, "ma_order": 0, "params": None, "samples": n_samples}
11 | # option 2: Manually define AR parameters
12 | data_config_manual = {"noise_std": noise_std, "params": ([0.2, 0.3, -0.5], [])}
13 | data_config_manual["ar_order"] = int(sum(np.array(data_config_manual["params"][0]) != 0.0))
14 | data_config_manual["ma_order"] = int(sum(np.array(data_config_manual["params"][1]) != 0.0))
15 | data_config_manual["samples"] = n_samples # + int(data_config_manual["ar_order"])
16 | return data_config_random if random_ar_params else data_config_manual
17 |
18 |
19 | def _generate_random_arparams(ar_order, ma_order, limit_abs_sum=True, maxiter=100):
20 | is_stationary = False
21 | iteration = 0
22 | while not is_stationary:
23 | iteration += 1
24 | # print("Iteration", iteration)
25 | if iteration > maxiter:
26 | raise RuntimeError("failed to find stationary coefficients")
27 | # Generate random parameters
28 | arparams = []
29 | maparams = []
30 | for i in range(ar_order):
31 | arparams.append(2 * np.random.random() - 1)
32 | for i in range(ma_order):
33 | maparams.append(2 * np.random.random() - 1)
34 | # print(arparams)
35 | arparams = np.array(arparams)
36 | maparams = np.array(maparams)
37 | if limit_abs_sum:
38 | ar_abssum = sum(np.abs(arparams))
39 | ma_abssum = sum(np.abs(maparams))
40 | if ar_abssum > 1:
41 | arparams = arparams / (ar_abssum + 10e-6)
42 | arparams = arparams * (0.5 + 0.5 * np.random.random())
43 | if ma_abssum > 1:
44 | maparams = maparams / (ma_abssum + 10e-6)
45 | maparams = maparams * (0.5 + 0.5 * np.random.random())
46 |
47 | arparams = arparams - np.mean(arparams)
48 | maparams = maparams - np.mean(maparams)
49 | arma_process = ArmaProcess.from_coeffs(arparams, maparams, nobs=100)
50 | is_stationary = arma_process.isstationary
51 | return arparams, maparams
52 |
53 |
54 | def generate_armaprocess_data(samples, ar_order, ma_order, noise_std, params=None):
55 | if params is not None:
56 | # use specified params (make sure to sum up to 1 or less)
57 | arparams, maparams = params
58 | else:
59 | # iterate to find random arparams that are stationary
60 | arparams, maparams = _generate_random_arparams(ar_order, ma_order)
61 | arma_process = ArmaProcess.from_coeffs(arparams, maparams, nobs=samples)
62 | # sample output from ARMA Process
63 | series = arma_process.generate_sample(samples, scale=noise_std)
64 | # make zero-mean:
65 | series = series - np.mean(series)
66 | return series, list(arparams), list(maparams)
67 |
68 |
69 | def save_to_file(save_path, series, data_config):
70 | if not os.path.exists(save_path):
71 | os.makedirs(save_path)
72 | file_data = "ar_{}_ma_{}_noise_{:.3f}_len_{}".format(
73 | data_config["ar_order"], data_config["ma_order"], data_config["noise_std"], data_config["samples"]
74 | )
75 | # data_config["ar_params"] = list(data_config["ar_params"])
76 | # data_config["ma_params"] = list(data_config["ma_params"])
77 | np.savetxt(os.path.join(save_path, file_data + ".csv"), series, delimiter=",")
78 | with open(os.path.join(save_path, "info_" + file_data + ".json"), "w") as f:
79 | json.dump(data_config, f)
80 | return file_data
81 |
82 |
83 | def load_from_file(data_path, data_name, load_config=True, verbose=False):
84 | df = pd.read_csv(os.path.join(data_path, data_name + ".csv"), header=None, index_col=False)
85 | if load_config:
86 | with open(os.path.join(data_path, "info_" + data_name + ".json"), "r") as f:
87 | data_config = json.load(f)
88 | else:
89 | data_config = None
90 | if verbose:
91 | print("loaded series from file")
92 | print("data_config", data_config)
93 | print(df.shape)
94 | print(df.head())
95 | return df, data_config
96 |
97 |
98 | def main():
99 | verbose = True
100 | random = False
101 | save = True
102 | save_path = "ar_data"
103 |
104 | data_config = _get_config(random_ar_params=random)
105 | if verbose:
106 | print(data_config)
107 |
108 | series, data_config["ar_params"], data_config["ma_params"] = generate_armaprocess_data(**data_config)
109 | del data_config["params"]
110 |
111 | if save:
112 | data_name = save_to_file(save_path, series, data_config)
113 |
114 | # just to test:
115 | df, data_config2 = load_from_file(save_path, data_name, load_config=True)
116 | if verbose:
117 | print("loaded from saved files:")
118 | print(data_config2)
119 | print(df.head())
120 |
121 |
122 | if __name__ == "__main__":
123 | main()
124 |
--------------------------------------------------------------------------------
/tests/test_legacy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import unittest
4 | import os
5 | import pathlib
6 | import shutil
7 | import logging
8 | import warnings
9 |
10 | warnings.filterwarnings("ignore", message=".*nonzero.*", category=UserWarning)
11 |
12 | import fastai
13 |
14 | ## lazy imports ala fastai2 style (needed for nice print functionality)
15 | from fastai.basics import *
16 | from fastai.tabular.all import *
17 |
18 | import arnet
19 | from arnet.ar_net_legacy import init_ar_learner
20 |
21 | log = logging.getLogger("ARNet.test_legacy")
22 | log.setLevel("WARNING")
23 | log.parent.setLevel("WARNING")
24 |
25 | DIR = pathlib.Path(__file__).parent.parent.absolute()
26 | data_path = os.path.join(DIR, "ar_data")
27 | results_path = os.path.join(data_path, "results_test")
28 | EPOCHS = 2
29 |
30 |
31 | class LegacyTests(unittest.TestCase):
32 | verbose = False
33 | plot = False
34 | save = False
35 |
36 | def test_legacy_ar(self):
37 | self.save = True
38 | if self.save:
39 | if not os.path.exists(results_path):
40 | os.makedirs(results_path)
41 | # Hyperparameters
42 | n_epoch = 3
43 | valid_p = 0.2
44 | n_forecasts = 1 # Note: if more than one, must have a list of ar_param for each forecast target.
45 | sparsity = 0.3 # guesstimate
46 | data_name = "ar_3_ma_0_noise_0.100_len_10000"
47 | df, data_config = arnet.load_from_file(data_path, data_name, load_config=True, verbose=self.verbose)
48 | df = df[:1000]
49 |
50 | # sparse AR: (for non-sparse, set sparsity to 1.0)
51 | ar_order = int(1 / sparsity * data_config["ar_order"])
52 | # to compute stats
53 | ar_params = arnet.pad_ar_params([data_config["ar_params"]], ar_order, n_forecasts)
54 |
55 | learn = init_ar_learner(
56 | series=df,
57 | ar_order=ar_order,
58 | n_forecasts=n_forecasts,
59 | valid_p=valid_p,
60 | sparsity=sparsity,
61 | ar_params=ar_params,
62 | verbose=self.verbose,
63 | )
64 |
65 | lr_at_min, _ = learn.lr_find(start_lr=1e-6, end_lr=1e2, num_it=400)
66 | log.info("lr at minimum: {}".format(lr_at_min))
67 |
68 | # Run Model
69 | # if you know the best learning rate:
70 | # learn.fit(n_epoch, 1e-2)
71 | # else use onecycle
72 | learn.fit_one_cycle(n_epoch=EPOCHS, lr_max=lr_at_min / 10)
73 |
74 | # Look at Coeff
75 | coeff = arnet.coeff_from_model(learn.model)
76 | log.info("ar params: {}".format(arnet.nice_print_list(ar_params)))
77 | log.info("model weights: {}".format(arnet.nice_print_list(coeff)))
78 | # should be [0.20, 0.30, -0.50, ...]
79 |
80 | preds, y = learn.get_preds()
81 | if self.plot or self.save:
82 | if self.plot:
83 | learn.recorder.plot_loss()
84 | arnet.plot_weights(
85 | ar_val=len(ar_params[0]), weights=coeff[0], ar=ar_params[0], save=not self.plot, savedir=results_path
86 | )
87 | arnet.plot_prediction_sample(preds[:100], y[:100], save=not self.plot, savedir=results_path)
88 | arnet.plot_error_scatter(preds, y, save=not self.plot, savedir=results_path)
89 |
90 | if self.save:
91 | # Optional:save and create inference learner
92 | learn.freeze()
93 | model_name = "ar{}_sparse_{:.3f}_ahead_{}_epoch_{}.pkl".format(ar_order, sparsity, n_forecasts, n_epoch)
94 | learn.export(fname=os.path.join(results_path, model_name))
95 | # can be loaded like this
96 | infer = load_learner(fname=os.path.join(results_path, model_name), cpu=True)
97 | # can unfreeze the model and fine_tune
98 | learn.unfreeze()
99 | learn.fit_one_cycle(1, lr_at_min / 100)
100 |
101 | coeff2 = arnet.coeff_from_model(learn.model)
102 | log.info("ar params: {}".format(arnet.nice_print_list(ar_params)))
103 | log.info("model weights: {}".format(arnet.nice_print_list(coeff)))
104 | log.info("model weights2: {}".format(arnet.nice_print_list(coeff2)))
105 |
106 | if self.plot or self.save:
107 | if self.plot:
108 | learn.recorder.plot_loss()
109 | arnet.plot_weights(
110 | ar_val=len(ar_params[0]), weights=coeff2[0], ar=ar_params[0], save=not self.plot, savedir=results_path
111 | )
112 | if self.save:
113 | shutil.rmtree(results_path)
114 |
115 | def test_legacy_random(self):
116 | df = pd.DataFrame({"x": [random.gauss(0.0, 1.0) for i in range(1000)]})
117 | learn = init_ar_learner(
118 | series=df,
119 | ar_order=3,
120 | n_forecasts=1,
121 | valid_p=0.1,
122 | sparsity=0.3,
123 | train_bs=32,
124 | valid_bs=1024,
125 | verbose=False,
126 | )
127 | # find Learning Rate
128 | lr_at_min, lr_steep = learn.lr_find(start_lr=1e-6, end_lr=1, num_it=1000, show_plot=self.plot)
129 | if self.plot:
130 | plt.show()
131 | print("lr at minimum: {}; steeptes lr: {}".format(lr_at_min, lr_steep))
132 | learn.fit_one_cycle(n_epoch=EPOCHS, lr_max=lr_at_min / 10)
133 | if self.plot:
134 | learn.recorder.plot_loss()
135 | plt.show()
136 | # record Coeff
137 | coeff = arnet.coeff_from_model(learn.model)
138 |
--------------------------------------------------------------------------------
/v0_1/data_loader.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import matplotlib.pyplot as plt
3 | from statsmodels.tsa.arima_process import ArmaProcess
4 | from torch.utils.data.dataset import Dataset
5 | import torch
6 | import copy
7 |
8 |
9 | def sample(y, offset, sample_inp_size, sample_out_size):
10 | Xin = np.arange(offset, offset + sample_inp_size)
11 | Xout = np.arange(sample_inp_size + offset, offset + sample_inp_size + sample_out_size)
12 | out = y[Xout]
13 | inp = y[Xin]
14 | return inp, out
15 |
16 |
17 | def create_dataset(series, n_samples=None, sample_inp_size=51, sample_out_size=1, test=None, verbose=False, plot=False):
18 | if n_samples is None:
19 | n_samples = len(series)
20 | data_inp = np.zeros((n_samples, sample_inp_size))
21 | data_out = np.zeros((n_samples, sample_out_size))
22 |
23 | for i in range(n_samples):
24 | sample_inp, sample_out = sample(series, i, sample_inp_size, sample_out_size)
25 | data_inp[i, :] = sample_inp
26 | data_out[i, :] = sample_out
27 | if test is not None:
28 | assert 0 < test < 1
29 | split = int(n_samples * (1 - test))
30 | train_inp, train_out = data_inp[:split], data_out[:split]
31 | test_inp, test_out = data_inp[split:], data_out[split:]
32 | series_train = series[:split]
33 | series_test = series[split:]
34 | else:
35 | train_inp, train_out = data_inp, data_out
36 | test_inp, test_out = data_inp, data_out
37 | series_train = series
38 | series_test = series
39 |
40 | dataset_train = LocalDataset(x=train_inp, y=train_out)
41 | dataset_test = LocalDataset(x=test_inp, y=test_out)
42 |
43 | if verbose:
44 | print("Train set size: ", dataset_train.length)
45 | print("Test set size: ", dataset_test.length)
46 |
47 | if plot:
48 | # Plot generated process.
49 | plt.plot(np.array(series)[:200])
50 | plt.show()
51 | return dataset_train, dataset_test, series_train, series_test
52 |
53 |
54 | class LocalDataset(Dataset):
55 | def __init__(self, x, y):
56 | x_dtype = torch.FloatTensor
57 | y_dtype = torch.FloatTensor # for MSE or L1 Loss
58 |
59 | self.length = x.shape[0]
60 |
61 | self.x_data = torch.from_numpy(x).type(x_dtype)
62 | self.y_data = torch.from_numpy(y).type(y_dtype)
63 |
64 | def __getitem__(self, index):
65 | return self.x_data[index], self.y_data[index]
66 |
67 | def __len__(self):
68 | return self.length
69 |
70 |
71 | def generate_armaprocess_data(samples, noise_std, random_order=None, params=None, limit_abs_sum=True):
72 | if params is not None:
73 | # use specified params, make sure to sum up to 1 or less
74 | arparams, maparams = params
75 | arma_process = ArmaProcess.from_coeffs(arparams, maparams, nobs=samples)
76 | else:
77 | is_stationary = False
78 | iteration = 0
79 | while not is_stationary:
80 | iteration += 1
81 | # print("Iteration", iteration)
82 | if iteration > 100:
83 | raise RuntimeError("failed to find stationary coefficients")
84 | # Generate random parameters
85 | arparams = []
86 | maparams = []
87 | ar_order, ma_order = random_order
88 | for i in range(ar_order):
89 | arparams.append(2 * np.random.random() - 1)
90 | for i in range(ma_order):
91 | maparams.append(2 * np.random.random() - 1)
92 |
93 | # print(arparams)
94 | arparams = np.array(arparams)
95 | maparams = np.array(maparams)
96 | if limit_abs_sum:
97 | ar_abssum = sum(np.abs(arparams))
98 | ma_abssum = sum(np.abs(maparams))
99 | if ar_abssum > 1:
100 | arparams = arparams / (ar_abssum + 10e-6)
101 | arparams = arparams * (0.5 + 0.5*np.random.random())
102 | if ma_abssum > 1:
103 | maparams = maparams / (ma_abssum + 10e-6)
104 | maparams = maparams * (0.5 + 0.5*np.random.random())
105 |
106 | arparams = arparams - np.mean(arparams)
107 | maparams = maparams - np.mean(maparams)
108 | arma_process = ArmaProcess.from_coeffs(arparams, maparams, nobs=samples)
109 | is_stationary = arma_process.isstationary
110 |
111 | # sample output from ARMA Process
112 | series = arma_process.generate_sample(samples, scale=noise_std)
113 | # make zero-mean:
114 | series = series - np.mean(series)
115 | return series, arparams, maparams
116 |
117 |
118 | def init_ar_dataset(n_samples, ar_val, ar_params=None, noise_std=1.0, plot=False, verbose=False, test=None, pad_to=None):
119 | # AR-Process
120 | if ar_params is not None:
121 | ar_val = len(ar_params)
122 | params = (ar_params, [])
123 | else:
124 | params = None
125 |
126 | if pad_to is None:
127 | inp_size = ar_val
128 | else:
129 | inp_size = pad_to
130 |
131 | series, ar, ma = generate_armaprocess_data(
132 | samples=n_samples+inp_size,
133 | noise_std=noise_std,
134 | random_order=(ar_val, 0),
135 | params=params,
136 | )
137 | # print("series mean", np.mean(series))
138 |
139 | if pad_to is not None:
140 | ar_pad = [0.0] * max(0, pad_to - ar_val)
141 | ar = list(ar) + ar_pad
142 |
143 | if verbose:
144 | print("AR params: ")
145 | print(ar)
146 |
147 | # Initialize data for DAR
148 | dataset_train, dataset_test, series_train, series_test = create_dataset(
149 | series=series,
150 | n_samples=n_samples,
151 | sample_inp_size=inp_size,
152 | sample_out_size=1,
153 | verbose=verbose,
154 | plot=plot,
155 | test=test,
156 | )
157 |
158 | return dataset_train, dataset_test, series_train, series_test, ar
159 |
160 |
161 | def load_data(data_config_in, verbose=False, plot=False):
162 | data_config = copy.deepcopy(data_config_in)
163 | data_type = data_config.pop("type")
164 | data = {
165 | "type": data_type
166 | }
167 | if data_type == 'AR':
168 | data["train"], data["test"], data["series_train"], data["series_test"], data["ar"] = init_ar_dataset(
169 | **data_config,
170 | verbose=verbose,
171 | plot=plot,
172 | )
173 | data["pad_to"] = data_config_in["pad_to"]
174 | else:
175 | raise NotImplementedError
176 | return data
177 |
--------------------------------------------------------------------------------
/v0_1/utils.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import matplotlib.pyplot as plt
3 | import numpy as np
4 | import seaborn as sns
5 | import os
6 |
7 |
8 | def compute_stats_ar(results, ar_params, verbose=False):
9 | weights = results["weights"]
10 | error = results["predicted"] - results["actual"]
11 | stats = {}
12 |
13 | abs_error = np.abs(weights - ar_params)
14 |
15 | symmetric_abs_coeff = np.abs(weights) + np.abs(ar_params)
16 | stats["sMAPE (AR-coefficients)"] = 100 * np.mean(abs_error / (10e-9 + symmetric_abs_coeff))
17 |
18 | sTPE = 100 * np.sum(abs_error) / (10e-9 + np.sum(symmetric_abs_coeff))
19 | stats["sTPE (AR-coefficients)"] = sTPE
20 |
21 | # predictions error
22 | stats["MSE"] = np.mean(error ** 2)
23 |
24 | if verbose:
25 | print("MSE: {}".format(stats["MSE"]))
26 | print("sMAPE (AR-coefficients): {:6.3f}".format(stats["sMAPE (AR-coefficients)"]))
27 | print("sTPE (AR-coefficients): {:6.3f}".format(stats["sTPE (AR-coefficients)"]))
28 | # print("Relative error: {:6.3f}".format(stats["TP (AR-coefficients)"]))
29 | # print("Mean relative error: {:6.3f}".format(mean_rel_error))
30 |
31 | print("AR params: ")
32 | print(ar_params)
33 |
34 | print("Weights: ")
35 | print(weights)
36 | return stats
37 |
38 |
39 | def plot_loss_curve(losses, test_loss=None, epoch_losses=None, show=False, save=False):
40 | fig = plt.figure()
41 | fig.set_size_inches(12, 6)
42 | ax = plt.axes()
43 | ax.set_xlabel("Iteration")
44 | ax.set_ylabel("Loss")
45 | x_loss = list(range(len(losses)))
46 | plt.plot(x_loss, losses, 'b', alpha=0.3)
47 | if epoch_losses is not None:
48 | iter_per_epoch = int(len(losses) / len(epoch_losses))
49 | epoch_ends = int(iter_per_epoch/2) + iter_per_epoch*np.arange(len(epoch_losses))
50 | plt.plot(epoch_ends, epoch_losses, 'b')
51 | if test_loss is not None:
52 | plt.hlines(test_loss, xmin=x_loss[0], xmax=x_loss[-1])
53 | if save:
54 | if not os.path.exists('results'):
55 | os.makedirs('results')
56 | figname = 'results/loss_DAR.png'
57 | plt.savefig(figname, dpi=600, bbox_inches='tight')
58 | plt.show()
59 | # plt.close()
60 |
61 |
62 | def plot_prediction_sample(predicted, actual, num_obs=100, model_name="AR-Net", save=False):
63 | fig2 = plt.figure()
64 | fig2.set_size_inches(10, 6)
65 | plt.plot(actual[0:num_obs])
66 | plt.plot(predicted[0:num_obs])
67 | plt.legend(["Actual Time-Series", "{}-Prediction".format(model_name)])
68 | if save:
69 | if not os.path.exists('results'):
70 | os.makedirs('results')
71 | figname = 'results/prediction_{}.png'.format(model_name)
72 | plt.savefig(figname, dpi=600, bbox_inches='tight')
73 | plt.show()
74 |
75 |
76 | def plot_error_scatter(predicted, actual, model_name="AR-Net", save=False):
77 | # error = predicted - actual
78 | fig3 = plt.figure()
79 | fig3.set_size_inches(6, 6)
80 | plt.scatter(actual, predicted - actual, marker='o', s=10, alpha=0.3)
81 | plt.legend(["{}-Error".format(model_name)])
82 | if save:
83 | if not os.path.exists('results'):
84 | os.makedirs('results')
85 | figname = 'results/scatter_{}.png'.format(model_name)
86 | plt.savefig(figname, dpi=600, bbox_inches='tight')
87 | plt.show()
88 |
89 |
90 | def plot_weights(ar_val, weights, ar, model_name="AR-Net", save=False):
91 | df = pd.DataFrame(
92 | zip(
93 | list(range(1, ar_val + 1)) * 2,
94 | ["AR-Process (True)"] * ar_val + [model_name] * ar_val,
95 | list(ar) + list(weights)
96 | ),
97 | columns=["AR-coefficient (lag number)", "model", "value (weight)"]
98 | )
99 | plt.figure(figsize=(10, 6))
100 | palette = {"Classic-AR": "C0", "AR-Net": "C1", "AR-Process (True)": "k"}
101 | sns.barplot(x="AR-coefficient (lag number)", hue="model", y="value (weight)", data=df)
102 | if save:
103 | if not os.path.exists('results'):
104 | os.makedirs('results')
105 | figname = 'results/weights_{}_{}.png'.format(ar_val, model_name, palette=palette)
106 | plt.savefig(figname, dpi=600, bbox_inches='tight')
107 |
108 | plt.show()
109 |
110 |
111 | def plot_results(results, model_name="MODEL", save=False):
112 | plot_prediction_sample(results["predicted"], results["actual"], num_obs=100, model_name=model_name, save=save)
113 | plot_error_scatter(results["predicted"], results["actual"], model_name=model_name, save=save)
114 |
115 |
116 | def jsonize(results):
117 | for key, value in results.items():
118 | if type(value) is list:
119 | if type(value[0]) is list:
120 | results[key] = [["{:8.5f}".format(xy) for xy in x] for x in value]
121 | else:
122 | results[key] = ["{:8.5f}".format(x) for x in value]
123 | else:
124 | results[key] = "{:8.5f}".format(value)
125 | return results
126 |
127 |
128 | def list_of_dicts_2_dict_of_lists(sources):
129 | keys = sources[0].keys()
130 | res = {}
131 | for key in keys:
132 | res[key] = [d[key] for d in sources]
133 | return res
134 |
135 |
136 | def list_of_dicts_2_dict_of_means(sources):
137 | keys = sources[0].keys()
138 | res = {}
139 | for key in keys:
140 | res[key] = np.mean([d[key] for d in sources])
141 | return res
142 |
143 |
144 | def list_of_dicts_2_dict_of_means_minmax(sources):
145 | keys = sources[0].keys()
146 | res = {}
147 | for key in keys:
148 | values = [d[key] for d in sources]
149 | res[key] = (np.mean(values), min(values), max(values))
150 | return res
151 |
152 |
153 | def get_json_filenames(values, subdir=None):
154 | ar_filename = get_json_filenames_type("AR", values, subdir)
155 | dar_filename = get_json_filenames_type("DAR", values, subdir)
156 | return ar_filename, dar_filename
157 |
158 |
159 | def get_json_filenames_type(model_type, values, subdir=None):
160 | filename = 'results/{}{}_{}.json'.format(
161 | subdir + "/" if subdir is not None else "",
162 | model_type,
163 | "-".join([str(x) for x in values])
164 | )
165 | return filename
166 |
167 |
168 | def intelligent_regularization(sparsity):
169 | if sparsity is not None:
170 | # best:
171 | # lam = 0.01 * (1.0 / sparsity - 1.0)
172 | lam = 0.02 * (1.0 / sparsity - 1.0)
173 | # lam = 0.05 * (1.0 / sparsity - 1.0)
174 |
175 | # alternatives
176 | # l1 = 0.02 * (np.log(2) / np.log(1 + sparsity) - 1.0)
177 | # l1 = 0.1 * (1.0 / np.sqrt(sparsity) - 1.0)
178 | else:
179 | lam = 0.0
180 | return lam
181 |
182 |
183 |
--------------------------------------------------------------------------------
/v0_1/training.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | import numpy as np
4 | import torch
5 | import torch.nn as nn
6 | from model import DAR
7 | from torch import optim
8 | from torch.utils.data import DataLoader
9 |
10 | from v0_1_pure_pytorch import utils
11 |
12 |
13 | # Architecture, batching etc of DARMA
14 | def train_batch(model, x, y, optimizer, loss_fn, lambda_value=None):
15 | # Run forward calculation
16 | y_predict = model.forward(x)
17 |
18 | # Compute loss.
19 | loss = loss_fn(y_predict, y)
20 |
21 | # regularize
22 | if lambda_value is not None:
23 | reg_loss = torch.zeros(1, dtype=torch.float, requires_grad=True)
24 | if model.num_layers == 1:
25 | abs_weights = torch.abs(model.layer_1.weight)
26 | # classic L1
27 | # reg = torch.mean(abs_weights)
28 |
29 | # sqrt - helps to protect some weights and bring others to zero,
30 | # but is still tough on larger weights
31 | # reg = torch.mean(torch.sqrt(abs_weights))
32 |
33 | # new, less hard on larger weights: (protects ~0.1-1.0)
34 | # reg = torch.div(2.0, 1.0 + torch.exp(-5.0*abs_weights.pow(0.4))) - 1.0
35 |
36 | # mid-way, more stable
37 | reg = torch.div(2.0, 1.0 + torch.exp(-3.0*abs_weights.pow(1.0/3.0))) - 1.0
38 |
39 | reg_loss = reg_loss + torch.mean(reg)
40 | else:
41 | # for weights in model.parameters():
42 | raise NotImplementedError("L1 Norm for deeper models not implemented")
43 |
44 | loss = loss + lambda_value * reg_loss
45 |
46 | optimizer.zero_grad()
47 |
48 | loss.backward(retain_graph=True)
49 |
50 | optimizer.step()
51 |
52 | return loss.data.item()
53 |
54 |
55 | def train(model, loader, loss_fn, lr, epochs, lr_decay, est_sparsity, lambda_delay=None, verbose=False):
56 |
57 | # Initialize the optimizer with above parameters
58 | optimizer = optim.Adam(model.parameters(), lr=lr)
59 | scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=1, gamma=lr_decay)
60 |
61 | losses = list()
62 | batch_index = 0
63 | epoch_losses = []
64 | avg_losses = []
65 | lambda_value = utils.intelligent_regularization(est_sparsity)
66 |
67 | for e in range(epochs):
68 | # slowly increase regularization until lambda_delay epoch
69 | if lambda_delay is not None and e < lambda_delay:
70 | l_factor = e / (1.0 * lambda_delay)
71 | # l_factor = (e / (1.0 * lambda_delay))**2
72 | else:
73 | l_factor = 1.0
74 |
75 | for x, y in loader:
76 | loss = train_batch(model=model, x=x, y=y, optimizer=optimizer,
77 | loss_fn=loss_fn, lambda_value=l_factor*lambda_value)
78 | epoch_losses.append(loss)
79 | batch_index += 1
80 | scheduler.step()
81 | losses.extend(epoch_losses)
82 | avg_loss = np.mean(epoch_losses)
83 | avg_losses.append(avg_loss)
84 | epoch_losses = []
85 | if verbose:
86 | print("{}. Epoch Avg Loss: {:10.2f}".format(e + 1, avg_loss))
87 | if verbose:
88 | print("Total Batches: ", batch_index)
89 |
90 | return losses, avg_losses
91 |
92 |
93 | def test_batch(model, x, y, loss_fn):
94 | # run forward calculation
95 | y_predict = model.forward(x)
96 | loss = loss_fn(y_predict, y)
97 |
98 | return y_predict, loss
99 |
100 |
101 | def test(model, loader, loss_fn):
102 | losses = list()
103 | y_vectors = list()
104 | y_predict_vectors = list()
105 |
106 | batch_index = 0
107 | for x, y in loader:
108 | y_predict, loss = test_batch(model=model, x=x, y=y, loss_fn=loss_fn)
109 |
110 | losses.append(loss.data.numpy())
111 | y_vectors.append(y.data.numpy())
112 | y_predict_vectors.append(y_predict.data.numpy())
113 |
114 | batch_index += 1
115 |
116 | losses = np.array(losses)
117 | y_predict_vector = np.concatenate(y_predict_vectors)
118 | mse = np.mean((y_predict_vector - np.concatenate(y_vectors)) ** 2)
119 |
120 | return y_predict_vector, losses, mse
121 |
122 |
123 | def run_train_test(dataset_train, dataset_test, model_config, train_config, verbose=False):
124 | data_loader_train = DataLoader(dataset=dataset_train, batch_size=train_config["batch"], shuffle=True)
125 | data_loader_test = DataLoader(dataset=dataset_test, batch_size=len(dataset_test), shuffle=False)
126 |
127 | if model_config["ma"] > 0:
128 | # TODO: implement DARMA
129 | raise NotImplementedError
130 | else:
131 | del model_config["ma"]
132 | model = DAR(
133 | **model_config
134 | )
135 |
136 | # Define the loss function
137 | loss_fn = nn.MSELoss() # mean squared error
138 |
139 | # Train and get the resulting loss per iteration
140 | del train_config["batch"]
141 | losses, avg_losses = train(
142 | model=model,
143 | loader=data_loader_train,
144 | loss_fn=loss_fn,
145 | **train_config,
146 | verbose=verbose,
147 | )
148 |
149 | # Test and get the resulting predicted y values
150 | y_predict, test_losses, test_mse = test(model=model, loader=data_loader_test, loss_fn=loss_fn)
151 |
152 | actual = np.concatenate(np.array(dataset_test.y_data))
153 | predicted = np.concatenate(y_predict)
154 | # weights_rereversed = np.array(model.layer_1.data)[0, ::-1]
155 | weights_rereversed = model.layer_1.weight.detach().numpy()[0, ::-1]
156 |
157 | return predicted, actual, np.array(losses), weights_rereversed, test_mse, avg_losses
158 |
159 |
160 | def run(data, model_config, train_config, verbose=False):
161 | if verbose:
162 | print("################ Model: AR-Net ################")
163 | start = time.time()
164 | predicted, actual, losses, weights, test_mse, epoch_losses = run_train_test(
165 | dataset_train=data["train"],
166 | dataset_test=data["test"],
167 | model_config=model_config,
168 | train_config=train_config,
169 | verbose=verbose,
170 | )
171 | end = time.time()
172 | duration = end - start
173 |
174 | if verbose:
175 | print("Time: {:8.4f}".format(duration))
176 | print("Final train epoch loss: {:10.2f}".format(epoch_losses[-1]))
177 | print("Test MSEs: {:10.2f}".format(test_mse))
178 |
179 | results = {}
180 | results["weights"] = weights
181 | results["predicted"] = predicted
182 | results["actual"] = actual
183 | results["test_mse"] = test_mse
184 | results["losses"] = losses
185 | results["epoch_losses"] = epoch_losses
186 | if data["type"] == 'AR':
187 | stats = utils.compute_stats_ar(results, ar_params=data["ar"], verbose=verbose)
188 | else:
189 | raise NotImplementedError
190 | stats["Time (s)"] = duration
191 |
192 | return results, stats
193 |
194 |
195 | def main():
196 | print("deprecated")
197 |
198 |
199 | if __name__ == "__main__":
200 | main()
201 |
--------------------------------------------------------------------------------
/example_notebooks/make_dataset.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import os\n",
10 | "import json\n",
11 | "import pandas as pd\n",
12 | "import numpy as np"
13 | ]
14 | },
15 | {
16 | "cell_type": "code",
17 | "execution_count": 2,
18 | "metadata": {},
19 | "outputs": [],
20 | "source": [
21 | "from arnet.create_ar_data import load_from_file\n",
22 | "from arnet.make_dataset import tabularize_univariate"
23 | ]
24 | },
25 | {
26 | "cell_type": "code",
27 | "execution_count": 3,
28 | "metadata": {},
29 | "outputs": [],
30 | "source": [
31 | "## Notebook settings\n",
32 | "data_path = '../ar_data'\n",
33 | "data_name = 'ar_3_ma_0_noise_0.100_len_10000'"
34 | ]
35 | },
36 | {
37 | "cell_type": "code",
38 | "execution_count": 4,
39 | "metadata": {},
40 | "outputs": [
41 | {
42 | "name": "stdout",
43 | "output_type": "stream",
44 | "text": [
45 | "{'samples': 10000, 'noise_std': 0.1, 'ar_order': 3, 'ma_order': 0, 'ar_params': [0.2, 0.3, -0.5], 'ma_params': []}\n"
46 | ]
47 | }
48 | ],
49 | "source": [
50 | "## Load data\n",
51 | "## if created AR data with create_ar_data, we can use the helper function:\n",
52 | "df, data_config = load_from_file(data_path, data_name, load_config=True)\n",
53 | "print(data_config)\n",
54 | "n_lags = data_config[\"ar_order\"]"
55 | ]
56 | },
57 | {
58 | "cell_type": "code",
59 | "execution_count": 5,
60 | "metadata": {},
61 | "outputs": [],
62 | "source": [
63 | "## Load data\n",
64 | "## else we can manually load any file that stores a time series, for example:\n",
65 | "# df = pd.read_csv(os.path.join(data_path, data_name + '.csv'), header=None, index_col=False)\n",
66 | "# n_lags = 3"
67 | ]
68 | },
69 | {
70 | "cell_type": "code",
71 | "execution_count": 6,
72 | "metadata": {},
73 | "outputs": [
74 | {
75 | "name": "stdout",
76 | "output_type": "stream",
77 | "text": [
78 | "(10000, 1)\n"
79 | ]
80 | },
81 | {
82 | "data": {
83 | "text/html": [
84 | "
\n",
85 | "\n",
98 | "
\n",
99 | " \n",
100 | " \n",
101 | " | \n",
102 | " 0 | \n",
103 | "
\n",
104 | " \n",
105 | " \n",
106 | " \n",
107 | " | 0 | \n",
108 | " 0.114610 | \n",
109 | "
\n",
110 | " \n",
111 | " | 1 | \n",
112 | " 0.027092 | \n",
113 | "
\n",
114 | " \n",
115 | " | 2 | \n",
116 | " -0.015263 | \n",
117 | "
\n",
118 | " \n",
119 | " | 3 | \n",
120 | " -0.125719 | \n",
121 | "
\n",
122 | " \n",
123 | " | 4 | \n",
124 | " -0.279195 | \n",
125 | "
\n",
126 | " \n",
127 | "
\n",
128 | "
"
129 | ],
130 | "text/plain": [
131 | " 0\n",
132 | "0 0.114610\n",
133 | "1 0.027092\n",
134 | "2 -0.015263\n",
135 | "3 -0.125719\n",
136 | "4 -0.279195"
137 | ]
138 | },
139 | "execution_count": 6,
140 | "metadata": {},
141 | "output_type": "execute_result"
142 | }
143 | ],
144 | "source": [
145 | "print(df.shape)\n",
146 | "df.head()"
147 | ]
148 | },
149 | {
150 | "cell_type": "code",
151 | "execution_count": 7,
152 | "metadata": {},
153 | "outputs": [],
154 | "source": [
155 | "## create a tabularized dataset from time series\n",
156 | "df_tab = tabularize_univariate(df, n_lags, n_forecasts=1)"
157 | ]
158 | },
159 | {
160 | "cell_type": "code",
161 | "execution_count": 8,
162 | "metadata": {},
163 | "outputs": [
164 | {
165 | "name": "stdout",
166 | "output_type": "stream",
167 | "text": [
168 | "(9997, 4)\n"
169 | ]
170 | },
171 | {
172 | "data": {
173 | "text/html": [
174 | "\n",
175 | "\n",
188 | "
\n",
189 | " \n",
190 | " \n",
191 | " | \n",
192 | " x_0 | \n",
193 | " x_1 | \n",
194 | " x_2 | \n",
195 | " y_0 | \n",
196 | "
\n",
197 | " \n",
198 | " \n",
199 | " \n",
200 | " | 0 | \n",
201 | " 0.114610 | \n",
202 | " 0.027092 | \n",
203 | " -0.015263 | \n",
204 | " -0.125719 | \n",
205 | "
\n",
206 | " \n",
207 | " | 1 | \n",
208 | " 0.027092 | \n",
209 | " -0.015263 | \n",
210 | " -0.125719 | \n",
211 | " -0.279195 | \n",
212 | "
\n",
213 | " \n",
214 | " | 2 | \n",
215 | " -0.015263 | \n",
216 | " -0.125719 | \n",
217 | " -0.279195 | \n",
218 | " -0.133535 | \n",
219 | "
\n",
220 | " \n",
221 | " | 3 | \n",
222 | " -0.125719 | \n",
223 | " -0.279195 | \n",
224 | " -0.133535 | \n",
225 | " -0.254239 | \n",
226 | "
\n",
227 | " \n",
228 | " | 4 | \n",
229 | " -0.279195 | \n",
230 | " -0.133535 | \n",
231 | " -0.254239 | \n",
232 | " 0.122992 | \n",
233 | "
\n",
234 | " \n",
235 | "
\n",
236 | "
"
237 | ],
238 | "text/plain": [
239 | " x_0 x_1 x_2 y_0\n",
240 | "0 0.114610 0.027092 -0.015263 -0.125719\n",
241 | "1 0.027092 -0.015263 -0.125719 -0.279195\n",
242 | "2 -0.015263 -0.125719 -0.279195 -0.133535\n",
243 | "3 -0.125719 -0.279195 -0.133535 -0.254239\n",
244 | "4 -0.279195 -0.133535 -0.254239 0.122992"
245 | ]
246 | },
247 | "execution_count": 8,
248 | "metadata": {},
249 | "output_type": "execute_result"
250 | }
251 | ],
252 | "source": [
253 | "print(df_tab.shape)\n",
254 | "df_tab.head()"
255 | ]
256 | },
257 | {
258 | "cell_type": "code",
259 | "execution_count": null,
260 | "metadata": {},
261 | "outputs": [],
262 | "source": []
263 | }
264 | ],
265 | "metadata": {
266 | "kernelspec": {
267 | "display_name": "Python 3",
268 | "language": "python",
269 | "name": "python3"
270 | },
271 | "language_info": {
272 | "codemirror_mode": {
273 | "name": "ipython",
274 | "version": 3
275 | },
276 | "file_extension": ".py",
277 | "mimetype": "text/x-python",
278 | "name": "python",
279 | "nbconvert_exporter": "python",
280 | "pygments_lexer": "ipython3",
281 | "version": "3.8.6"
282 | }
283 | },
284 | "nbformat": 4,
285 | "nbformat_minor": 4
286 | }
287 |
--------------------------------------------------------------------------------
/arnet/ar_net.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | import logging
3 | import os
4 | import pandas as pd
5 | import matplotlib.pyplot as plt
6 |
7 | import torch
8 | from fastai.data.core import DataLoaders
9 | from fastai.tabular.core import TabularPandas, TabDataLoader
10 | from fastai.tabular.learner import tabular_learner, TabularLearner
11 | from fastai.data.transforms import Normalize
12 | from fastai.learner import load_learner
13 | from fastai.distributed import ParallelTrainer
14 |
15 | from arnet import utils, utils_data, plotting
16 | from arnet.fastai_mods import SparsifyAR, huber, sTPE, get_loss_func
17 |
18 | log = logging.getLogger("ARNet")
19 |
20 |
21 | @dataclass
22 | class ARNet:
23 | ar_order: int
24 | sparsity: float = None
25 | n_forecasts: int = 1
26 | n_epoch: int = 20
27 | lr: float = None
28 | est_noise: float = None
29 | start_reg_pct: float = 0.0
30 | full_reg_pct: float = 0.5
31 | use_reg_noise: bool = False
32 | reg_c1: float = 2.0
33 | reg_c2: float = 2.0
34 | loss_func: str = "huber"
35 | train_bs: int = 32
36 | valid_bs: int = 1024
37 | valid_p: float = 0.1
38 | normalize: bool = False
39 | ar_params: list = None
40 | log_level: str = None
41 | callbacks: list = None
42 | metrics: list = None
43 | use_gpu: bool = False
44 | dls: DataLoaders = field(init=False, default=None)
45 | learn: TabularLearner = field(init=False, default=None)
46 | coeff: list = field(init=False, default=None)
47 | df: pd.DataFrame = field(init=False, default=None)
48 | regularizer: SparsifyAR = field(init=False, default=None)
49 |
50 | def __post_init__(self):
51 | if self.log_level is not None:
52 | utils.set_logger_level(log, self.log_level)
53 | self.loss_func = get_loss_func(self.loss_func)
54 | if self.use_gpu:
55 | if torch.cuda.is_available():
56 | self.device = torch.device("cuda")
57 | # torch.cuda.set_device(0)
58 | else:
59 | log.error("CUDA is not available. defaulting to CPU")
60 | self.device = torch.device("cpu")
61 | else:
62 | self.device = torch.device("cpu")
63 |
64 | def tabularize(self, series):
65 | if self.est_noise is None:
66 | self.est_noise = utils_data.estimate_noise(series)
67 | log.info("estimated noise of series: {}".format(self.est_noise))
68 | df_all = utils_data.tabularize_univariate(series, self.ar_order, self.n_forecasts)
69 | log.debug("tabularized df")
70 | log.debug("df columns: {}".format(list(df_all.columns)))
71 | log.debug("df shape: {}".format(df_all.shape))
72 | # log.debug("df head(3): {}".format(df_all.head(3)))
73 | self.df = df_all
74 | return self
75 |
76 | def make_datasets(
77 | self,
78 | series=None,
79 | valid_p=None,
80 | train_bs=None,
81 | valid_bs=None,
82 | normalize=None,
83 | ):
84 | if series is None:
85 | if self.df is None:
86 | raise ValueError("must pass a series.")
87 | else:
88 | self.tabularize(series)
89 | valid_p = self.valid_p if valid_p is None else valid_p
90 | train_bs = self.train_bs if train_bs is None else train_bs
91 | valid_bs = self.valid_bs if valid_bs is None else valid_bs
92 | normalize = self.normalize if normalize is None else normalize
93 |
94 | procs = []
95 | if normalize:
96 | procs.append(Normalize)
97 |
98 | df_all = self.df
99 | splits = utils_data.split_by_p_valid(valid_p, len(df_all))
100 | cont_names = [col for col in list(df_all.columns) if "x_" == col[:2]]
101 | target_names = [col for col in list(df_all.columns) if "y_" == col[:2]]
102 | tp = TabularPandas(
103 | df_all,
104 | procs=procs,
105 | cat_names=None,
106 | cont_names=cont_names,
107 | y_names=target_names,
108 | splits=splits,
109 | )
110 | log.debug("cont var num: {}, names: {}".format(len(tp.cont_names), tp.cont_names))
111 |
112 | trn_dl = TabDataLoader(tp.train, bs=train_bs, shuffle=True, drop_last=True, device=self.device)
113 | val_dl = TabDataLoader(tp.valid, bs=valid_bs, device=self.device)
114 | self.dls = DataLoaders(trn_dl, val_dl, device=self.device)
115 | log.debug("showing batch")
116 | log.debug("{}".format(self.dls.show_batch(show=False)))
117 | return self
118 |
119 | def create_regularizer(
120 | self,
121 | sparsity=None,
122 | start_reg_pct=None,
123 | full_reg_pct=None,
124 | est_noise=None,
125 | use_reg_noise=None,
126 | reg_c1=None,
127 | reg_c2=None,
128 | ):
129 | sparsity = self.sparsity if sparsity is None else sparsity
130 | start_reg_pct = self.start_reg_pct if start_reg_pct is None else start_reg_pct
131 | full_reg_pct = self.full_reg_pct if full_reg_pct is None else full_reg_pct
132 | est_noise = self.est_noise if est_noise is None else est_noise
133 | use_reg_noise = self.use_reg_noise if use_reg_noise is None else use_reg_noise
134 | reg_c1 = self.reg_c1 if reg_c1 is None else reg_c1
135 | reg_c2 = self.reg_c2 if reg_c2 is None else reg_c2
136 |
137 | self.regularizer = SparsifyAR(
138 | sparsity,
139 | est_noise=est_noise if use_reg_noise else None,
140 | start_pct=start_reg_pct,
141 | full_pct=full_reg_pct,
142 | c1=reg_c1,
143 | c2=reg_c2,
144 | )
145 | log.info("reg lam (max): {}".format(self.regularizer.lam_max))
146 | return self
147 |
148 | def create_learner(
149 | self,
150 | loss_func=None,
151 | metrics=None,
152 | ar_params=None,
153 | callbacks=None,
154 | ):
155 | loss_func = self.loss_func if loss_func is None else get_loss_func(loss_func)
156 | metrics = self.metrics if metrics is None else metrics
157 | ar_params = self.ar_params if ar_params is None else ar_params
158 | callbacks = self.callbacks if callbacks is None else callbacks
159 |
160 | if metrics is None:
161 | metrics = ["MSE", "MAE"]
162 | metrics = [get_loss_func(m) for m in metrics]
163 | if ar_params is not None:
164 | metrics.append(sTPE(ar_params, at_epoch_end=False))
165 |
166 | if callbacks is None:
167 | callbacks = []
168 | if self.sparsity is not None and self.regularizer is None:
169 | self.create_regularizer()
170 | if self.regularizer is not None:
171 | callbacks.append(self.regularizer)
172 |
173 | self.learn = tabular_learner(
174 | self.dls,
175 | layers=[], # Note: None defaults to [200, 100]
176 | config={"use_bn": False, "bn_final": False, "bn_cont": False},
177 | n_out=self.n_forecasts, # None calls get_c(dls)
178 | train_bn=False, # passed to Learner
179 | metrics=metrics, # passed to Learner
180 | loss_func=loss_func,
181 | cbs=callbacks,
182 | )
183 | log.debug("{}".format(self.learn.model))
184 | return self
185 |
186 | def find_lr(self, plot=True):
187 | if self.learn is None:
188 | raise ValueError("create learner first.")
189 | lr_at_min, lr_steep = self.learn.lr_find(start_lr=1e-6, end_lr=10, num_it=300, show_plot=plot)
190 | if plot:
191 | plt.show()
192 | log.debug("lr at minimum: {}; (steepest lr: {})".format(lr_at_min, lr_steep))
193 | lr = lr_at_min
194 | log.info("Optimal learning rate: {}".format(lr))
195 | self.lr = lr
196 | return self
197 |
198 | def fit_one_cycle(self, n_epoch=None, lr=None, cycles=1, plot=True):
199 | n_epoch = self.n_epoch if n_epoch is None else n_epoch
200 | lr = self.lr if lr is None else lr
201 |
202 | if lr is None:
203 | self.find_lr(plot=plot)
204 | lr = self.lr
205 | for i in range(0, cycles):
206 | self.learn.fit_one_cycle(n_epoch=n_epoch, lr_max=lr, div=25.0, div_final=10000.0, pct_start=0.25)
207 | lr = lr / 10
208 | if plot:
209 | self.learn.recorder.plot_loss(skip_start=20)
210 | if plot:
211 | plt.show()
212 | # record Coeff
213 | self.coeff = utils.coeff_from_model(self.learn.model)
214 | return self
215 |
216 | def fit(self, series, plot=False):
217 | self.make_datasets(series)
218 | self.create_learner()
219 | self.fit_one_cycle(plot=plot)
220 | return self
221 |
222 | def plot_weights(self, **kwargs):
223 | plotting.plot_weights(
224 | ar_val=self.ar_order,
225 | weights=self.coeff[0],
226 | ar=self.ar_params,
227 | **kwargs,
228 | )
229 |
230 | def plot_fitted_obs(self, num_obs=100, **kwargs):
231 | preds, y = self.learn.get_preds()
232 | if num_obs is not None:
233 | y = y[0:num_obs]
234 | preds = preds[0:num_obs]
235 | plotting.plot_prediction_sample(preds, y, **kwargs)
236 |
237 | def plot_errors(self, **kwargs):
238 | preds, y = self.learn.get_preds()
239 | plotting.plot_error_scatter(preds, y, **kwargs)
240 |
241 | def save_model(self, results_path="results", model_name=None):
242 | # self.learn.freeze()
243 | sparsity = 1.0 if self.sparsity is None else self.sparsity
244 | if model_name is None:
245 | model_name = "ar{}_sparse_{:.3f}_ahead_{}_epoch_{}.pkl".format(
246 | self.ar_order, sparsity, self.n_forecasts, self.n_epoch
247 | )
248 | self.learn.export(fname=os.path.join(results_path, model_name))
249 | return self
250 |
251 | def load_model(self, results_path="results", model_name=None, cpu=True):
252 | self.learn = load_learner(fname=os.path.join(results_path, model_name), cpu=cpu)
253 | # can unfreeze the model and fine_tune
254 | self.learn.unfreeze()
255 | return self
256 |
--------------------------------------------------------------------------------
/example_notebooks/01_fit_arnet.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import numpy as np\n",
10 | "import pandas as pd\n",
11 | "import os\n",
12 | "import torch\n",
13 | "import arnet\n",
14 | "from arnet import ARNet\n",
15 | "import matplotlib.pyplot as plt\n",
16 | "%matplotlib inline"
17 | ]
18 | },
19 | {
20 | "cell_type": "code",
21 | "execution_count": 2,
22 | "metadata": {},
23 | "outputs": [],
24 | "source": [
25 | "DIR = os.path.dirname(os.path.abspath(''))\n",
26 | "data_path = os.path.join(DIR, 'ar_data')\n",
27 | "name = 'ar_3_ma_0_noise_0.100_len_10000'\n",
28 | "df = pd.read_csv(os.path.join(data_path, name + '.csv'), header=None, index_col=False)"
29 | ]
30 | },
31 | {
32 | "cell_type": "code",
33 | "execution_count": 3,
34 | "metadata": {},
35 | "outputs": [
36 | {
37 | "name": "stderr",
38 | "output_type": "stream",
39 | "text": [
40 | "INFO: ARNet - tabularize: estimated noise of series: 0.12853080551811422\n",
41 | "INFO: ARNet - create_regularizer: reg lam (max): 0.009999999980000004\n"
42 | ]
43 | },
44 | {
45 | "data": {
46 | "text/html": [],
47 | "text/plain": [
48 | ""
49 | ]
50 | },
51 | "metadata": {},
52 | "output_type": "display_data"
53 | },
54 | {
55 | "data": {
56 | "image/png": "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\n",
57 | "text/plain": [
58 | ""
59 | ]
60 | },
61 | "metadata": {
62 | "needs_background": "light"
63 | },
64 | "output_type": "display_data"
65 | },
66 | {
67 | "name": "stderr",
68 | "output_type": "stream",
69 | "text": [
70 | "INFO: ARNet - find_lr: Optimal learning rate: 0.1369830012321472\n"
71 | ]
72 | },
73 | {
74 | "data": {
75 | "text/html": [
76 | "\n",
77 | " \n",
78 | " \n",
79 | " | epoch | \n",
80 | " train_loss | \n",
81 | " valid_loss | \n",
82 | " mse | \n",
83 | " mae | \n",
84 | " time | \n",
85 | "
\n",
86 | " \n",
87 | " \n",
88 | " \n",
89 | " | 0 | \n",
90 | " 0.010955 | \n",
91 | " 0.010912 | \n",
92 | " 0.010912 | \n",
93 | " 0.084099 | \n",
94 | " 00:01 | \n",
95 | "
\n",
96 | " \n",
97 | " | 1 | \n",
98 | " 0.012936 | \n",
99 | " 0.010850 | \n",
100 | " 0.010850 | \n",
101 | " 0.083003 | \n",
102 | " 00:01 | \n",
103 | "
\n",
104 | " \n",
105 | " | 2 | \n",
106 | " 0.013433 | \n",
107 | " 0.010741 | \n",
108 | " 0.010741 | \n",
109 | " 0.082137 | \n",
110 | " 00:01 | \n",
111 | "
\n",
112 | " \n",
113 | " | 3 | \n",
114 | " 0.014620 | \n",
115 | " 0.010933 | \n",
116 | " 0.010933 | \n",
117 | " 0.082773 | \n",
118 | " 00:01 | \n",
119 | "
\n",
120 | " \n",
121 | " | 4 | \n",
122 | " 0.015200 | \n",
123 | " 0.010846 | \n",
124 | " 0.010846 | \n",
125 | " 0.083539 | \n",
126 | " 00:01 | \n",
127 | "
\n",
128 | " \n",
129 | " | 5 | \n",
130 | " 0.014374 | \n",
131 | " 0.011287 | \n",
132 | " 0.011287 | \n",
133 | " 0.083522 | \n",
134 | " 00:01 | \n",
135 | "
\n",
136 | " \n",
137 | " | 6 | \n",
138 | " 0.013678 | \n",
139 | " 0.010433 | \n",
140 | " 0.010433 | \n",
141 | " 0.081839 | \n",
142 | " 00:01 | \n",
143 | "
\n",
144 | " \n",
145 | " | 7 | \n",
146 | " 0.013177 | \n",
147 | " 0.010253 | \n",
148 | " 0.010253 | \n",
149 | " 0.080798 | \n",
150 | " 00:01 | \n",
151 | "
\n",
152 | " \n",
153 | " | 8 | \n",
154 | " 0.012035 | \n",
155 | " 0.010105 | \n",
156 | " 0.010105 | \n",
157 | " 0.080134 | \n",
158 | " 00:01 | \n",
159 | "
\n",
160 | " \n",
161 | " | 9 | \n",
162 | " 0.011887 | \n",
163 | " 0.010102 | \n",
164 | " 0.010102 | \n",
165 | " 0.080020 | \n",
166 | " 00:01 | \n",
167 | "
\n",
168 | " \n",
169 | "
"
170 | ],
171 | "text/plain": [
172 | ""
173 | ]
174 | },
175 | "metadata": {},
176 | "output_type": "display_data"
177 | },
178 | {
179 | "data": {
180 | "image/png": "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\n",
181 | "text/plain": [
182 | ""
183 | ]
184 | },
185 | "metadata": {
186 | "needs_background": "light"
187 | },
188 | "output_type": "display_data"
189 | }
190 | ],
191 | "source": [
192 | "m = arnet.ARNet(\n",
193 | " ar_order=10,\n",
194 | " sparsity=0.5,\n",
195 | " n_epoch=10,\n",
196 | " loss_func=\"MSE\",\n",
197 | " valid_p=0.1,\n",
198 | " use_gpu=False,\n",
199 | ")\n",
200 | "m = m.fit(df, plot=True)"
201 | ]
202 | },
203 | {
204 | "cell_type": "code",
205 | "execution_count": 4,
206 | "metadata": {},
207 | "outputs": [
208 | {
209 | "data": {
210 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAmkAAAEKCAYAAABJ+cK7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAAhwUlEQVR4nO3de5xeZX33+8+XcIgotJAgIiEmj6IcSgw6IHLSchI3j4BbqlDloNLIFiyI1tLtsxVbt9BKaxHpQ9ngIx4IKlKJTz1BEDlUkATDMSioAUIRQlAOVdTAb/9xryTDOJMMmbln3TP5vF+ved3rdK/rtyaafFnrWteVqkKSJEm9ZYO2C5AkSdIfMqRJkiT1IEOaJElSDzKkSZIk9SBDmiRJUg8ypEmSJPWgVkNakoOT/DjJPUlOG2T/CUluS7IoyXVJdmqjTkmSpLGWtsZJSzIJ+AlwILAUuAk4qqru7HfM5lX1eLN8KPDeqjq4jXolSZLGUpt30nYH7qmqn1XV74BLgMP6H7AyoDWeDzjyriRJWi9s2GLb2wL391tfCrxm4EFJTgROBTYG9lvbSadOnVozZswYpRIlSZK6Z+HChY9U1VaD7WszpA1LVZ0LnJvkz4H/ARw78Jgkc4A5ANOnT2fBggVjW6QkSdI6SHLvUPvafNz5ALBdv/VpzbahXAIcPtiOqjq/qvqqqm+rrQYNo5IkSeNKmyHtJmD7JDOTbAwcCczrf0CS7futHgLcPYb1SZIktaa1x51VtSLJScB3gEnAZ6vqjiR/CyyoqnnASUkOAH4P/JJBHnVKkiRNRK32SauqbwLfHLDtI/2WTx7zoiRJ0oj8/ve/Z+nSpTz11FNtl9IzJk+ezLRp09hoo42G/Z2ef3FAkiSNL0uXLmWzzTZjxowZJGm7nNZVFcuXL2fp0qXMnDlz2N9zWihJkjSqnnrqKaZMmWJAayRhypQpz/nOoiFNkiSNOgPas63L78OQJkmS1IPskzZBvPqvPt92Cc/Jwk8e03YJkqT11IwZM1iwYAFTp04d0THd5p00SZKkHmRIkyRJPW/JkiXssMMOHHfccbz85S/n7W9/O1deeSV77bUX22+/PT/84Q959NFHOfzww5k1axZ77LEHt956KwDLly/noIMOYuedd+b444+nqlad94tf/CK77747s2fP5j3veQ9PP/10W5f4BwxpkiRpXLjnnnv4wAc+wF133cVdd93FxRdfzHXXXcdZZ53FJz7xCT760Y+y6667cuutt/KJT3yCY47pdK352Mc+xt57780dd9zBm9/8Zu677z4AFi9ezJe//GWuv/56Fi1axKRJk/jSl77U5iU+i33SJEnSuDBz5kx22WUXAHbeeWf2339/krDLLruwZMkS7r33Xr72ta8BsN9++7F8+XIef/xxrrnmGi677DIADjnkELbYYgsA5s+fz8KFC9ltt90A+M1vfsMLX/jCFq5scIY0SZI0LmyyySarljfYYINV6xtssAErVqx4TqP5Q2eQ2WOPPZYzzjhjVOscLT7ulCRJE8I+++yz6nHl1VdfzdSpU9l8883Zd999ufjiiwH41re+xS9/+UsA9t9/fy699FIefvhhAB599FHuvffedoofhHfSJEnShHD66afzrne9i1mzZrHpppty0UUXAfDRj36Uo446ip133pk999yT6dOnA7DTTjvx8Y9/nIMOOohnnnmGjTbaiHPPPZeXvOQlbV7GKun/hsNE0NfXVwsWLGi7jDHnOGmSpF6xePFidtxxx7bL6DmD/V6SLKyqvsGO93GnJElSDzKkSZIk9SBDmiRJUg8ypEmSJPUgQ5okSVIPMqRJkiT1oFbHSUtyMHA2MAm4oKrOHLD/VOB4YAWwDHhXVfXOKHOSJGmtRnuYqOEO4/T1r3+dN7/5zSxevJgddtiBJUuWsOOOO/KKV7yC3/3ud/T19XHhhRcOOlNBEk499VT+8R//EYCzzjqLJ598ktNPP33I9q6++mo23nhj9txzz3W6roFau5OWZBJwLvBGYCfgqCQ7DTjsR0BfVc0CLgX+YWyrlCRJ49XcuXPZe++9mTt37qptL33pS1m0aBG33XYbS5cu5Stf+cqg391kk0247LLLeOSRR4bd3tVXX81//Md/jLjuldp83Lk7cE9V/ayqfgdcAhzW/4Cq+l5V/bpZvQGYNsY1SpKkcejJJ5/kuuuu48ILL+SSSy75g/2TJk1i991354EHHhj0+xtuuCFz5szhU5/61B/sW7ZsGW95y1vYbbfd2G233bj++utZsmQJ5513Hp/61KeYPXs211577Yivoc3HndsC9/dbXwq8Zg3Hvxv4VlcrkiRJE8Lll1/OwQcfzMtf/nKmTJnCwoULmTJlyqr9Tz31FDfeeCNnn332kOc48cQT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\n",
211 | "text/plain": [
212 | ""
213 | ]
214 | },
215 | "metadata": {
216 | "needs_background": "light"
217 | },
218 | "output_type": "display_data"
219 | }
220 | ],
221 | "source": [
222 | "m.plot_weights()"
223 | ]
224 | }
225 | ],
226 | "metadata": {
227 | "kernelspec": {
228 | "display_name": "light-matter",
229 | "language": "python",
230 | "name": "light-matter"
231 | },
232 | "language_info": {
233 | "codemirror_mode": {
234 | "name": "ipython",
235 | "version": 3
236 | },
237 | "file_extension": ".py",
238 | "mimetype": "text/x-python",
239 | "name": "python",
240 | "nbconvert_exporter": "python",
241 | "pygments_lexer": "ipython3",
242 | "version": "3.8.6"
243 | }
244 | },
245 | "nbformat": 4,
246 | "nbformat_minor": 4
247 | }
248 |
--------------------------------------------------------------------------------
/example_notebooks/legacy_run_experiments.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import numpy as np\n",
10 | "import pandas as pd\n",
11 | "import os\n",
12 | "import matplotlib.pyplot as plt\n",
13 | "\n",
14 | "import arnet\n",
15 | "from arnet.ar_net_legacy import init_ar_learner\n",
16 | "\n",
17 | "%matplotlib inline"
18 | ]
19 | },
20 | {
21 | "cell_type": "code",
22 | "execution_count": 3,
23 | "metadata": {},
24 | "outputs": [],
25 | "source": [
26 | "# hyperparameters\n",
27 | "save = True\n",
28 | "sparsity = 0.5 # guesstimate\n",
29 | "ar_order = 10 # guesstimate\n",
30 | "\n",
31 | "n_epoch = 10\n",
32 | "valid_p = 0.1"
33 | ]
34 | },
35 | {
36 | "cell_type": "code",
37 | "execution_count": 2,
38 | "metadata": {},
39 | "outputs": [],
40 | "source": [
41 | "# data\n",
42 | "DIR = os.path.dirname(os.path.abspath(''))\n",
43 | "data_path = os.path.join(DIR, 'ar_data')\n",
44 | "results_path = os.path.join(DIR, 'results')\n",
45 | "models_path = os.path.join(DIR, 'models')\n",
46 | "if not os.path.exists(results_path): \n",
47 | " os.makedirs(results_path) \n",
48 | "if not os.path.exists(models_path): \n",
49 | " os.makedirs(models_path)\n",
50 | " \n",
51 | "data_names = ['ar_3_ma_0_noise_0.100_len_10000'] "
52 | ]
53 | },
54 | {
55 | "cell_type": "code",
56 | "execution_count": 4,
57 | "metadata": {},
58 | "outputs": [
59 | {
60 | "name": "stdout",
61 | "output_type": "stream",
62 | "text": [
63 | "fitting: ar_3_ma_0_noise_0.100_len_10000\n"
64 | ]
65 | },
66 | {
67 | "data": {
68 | "text/html": [],
69 | "text/plain": [
70 | ""
71 | ]
72 | },
73 | "metadata": {},
74 | "output_type": "display_data"
75 | },
76 | {
77 | "data": {
78 | "image/png": "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\n",
79 | "text/plain": [
80 | ""
81 | ]
82 | },
83 | "metadata": {
84 | "needs_background": "light"
85 | },
86 | "output_type": "display_data"
87 | },
88 | {
89 | "name": "stdout",
90 | "output_type": "stream",
91 | "text": [
92 | "lr at minimum: 0.05623413324356079; steeptes lr: 0.020417379215359688\n"
93 | ]
94 | },
95 | {
96 | "data": {
97 | "text/html": [
98 | "\n",
99 | " \n",
100 | " \n",
101 | " | epoch | \n",
102 | " train_loss | \n",
103 | " valid_loss | \n",
104 | " mae | \n",
105 | " time | \n",
106 | "
\n",
107 | " \n",
108 | " \n",
109 | " \n",
110 | " | 0 | \n",
111 | " 0.020248 | \n",
112 | " 0.013306 | \n",
113 | " 0.091418 | \n",
114 | " 00:01 | \n",
115 | "
\n",
116 | " \n",
117 | " | 1 | \n",
118 | " 0.010328 | \n",
119 | " 0.010245 | \n",
120 | " 0.080722 | \n",
121 | " 00:01 | \n",
122 | "
\n",
123 | " \n",
124 | " | 2 | \n",
125 | " 0.010258 | \n",
126 | " 0.010097 | \n",
127 | " 0.080104 | \n",
128 | " 00:01 | \n",
129 | "
\n",
130 | " \n",
131 | " | 3 | \n",
132 | " 0.010552 | \n",
133 | " 0.010240 | \n",
134 | " 0.080945 | \n",
135 | " 00:01 | \n",
136 | "
\n",
137 | " \n",
138 | " | 4 | \n",
139 | " 0.010190 | \n",
140 | " 0.010257 | \n",
141 | " 0.080975 | \n",
142 | " 00:01 | \n",
143 | "
\n",
144 | " \n",
145 | " | 5 | \n",
146 | " 0.010604 | \n",
147 | " 0.010100 | \n",
148 | " 0.080252 | \n",
149 | " 00:01 | \n",
150 | "
\n",
151 | " \n",
152 | " | 6 | \n",
153 | " 0.010374 | \n",
154 | " 0.010058 | \n",
155 | " 0.079831 | \n",
156 | " 00:01 | \n",
157 | "
\n",
158 | " \n",
159 | " | 7 | \n",
160 | " 0.010370 | \n",
161 | " 0.010076 | \n",
162 | " 0.079970 | \n",
163 | " 00:01 | \n",
164 | "
\n",
165 | " \n",
166 | " | 8 | \n",
167 | " 0.010277 | \n",
168 | " 0.010079 | \n",
169 | " 0.079972 | \n",
170 | " 00:01 | \n",
171 | "
\n",
172 | " \n",
173 | " | 9 | \n",
174 | " 0.010468 | \n",
175 | " 0.010070 | \n",
176 | " 0.079924 | \n",
177 | " 00:01 | \n",
178 | "
\n",
179 | " \n",
180 | "
"
181 | ],
182 | "text/plain": [
183 | ""
184 | ]
185 | },
186 | "metadata": {},
187 | "output_type": "display_data"
188 | },
189 | {
190 | "data": {
191 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXoAAAD4CAYAAADiry33AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAAnz0lEQVR4nO3deXxcdb3/8ddnJpN9a5a2abokXelKoaVUNlEECngFlFpQr6jcBz8XrvvPW69eLnK5V/TxUO/lCvITRQVF4BaRXimCCAiy2bSULnRL93RN0jTNnszM9/fHnKbTkL2TTmbyfj4eeeTMOd+Z+Xwzk/ec+Z7NnHOIiEjy8sW7ABERGVoKehGRJKegFxFJcgp6EZEkp6AXEUlyKfEuoKuioiJXVlYW7zJERBLKmjVrapxzxd0tG3ZBX1ZWRkVFRbzLEBFJKGa2p6dlGroREUlyCnoRkSTXr6A3syVmttXMKs1seTfL08zsMW/5m2ZWFrVsnpm9bmabzGyDmaXHsH4REelDn2P0ZuYH7gUuB6qA1Wa20jn3TlSzW4A659xUM7sR+B6wzMxSgF8Df++ce9vMCoGOmPdCREa0jo4OqqqqaG1tjXcpQy49PZ3x48cTCAT6fZ/+bIxdBFQ653YCmNmjwLVAdNBfC9zhTa8AfmxmBlwBrHfOvQ3gnKvtd2UiIv1UVVVFTk4OZWVlRKInOTnnqK2tpaqqivLy8n7frz9DN6XAvqjbVd68bts454JAPVAITAecmT1rZmvN7BvdPYGZ3WpmFWZWUV1d3e/iRUQAWltbKSwsTOqQBzAzCgsLB/zNZag3xqYAFwEf935fb2aXdW3knPupc26hc25hcXG3u4GKiPQq2UP+hMH0sz9Bvx+YEHV7vDev2zbeuHweUEtk7f9l51yNc64ZWAWcO+Aq++FgfQs/fG4ru2qahuLhRUQSVn+CfjUwzczKzSwVuBFY2aXNSuBmb/oG4AUXOdH9s8BcM8v0PgDey6lj+zFT09DOPS9UsuNI41A8vIhIj44dO8Z999034PtdffXVHDt2LPYFddFn0Htj7rcRCe3NwOPOuU1mdqeZfchr9nOg0Mwqga8Cy7371gE/JPJhsQ5Y65x7Oua9ADJSI11p7ggNxcOLiPSop6APBoO93m/VqlXk5+cPUVUn9esUCM65VUSGXaLn3R413Qos7eG+vyayi+WQykiNdKWlvfc/rIhIrC1fvpwdO3Ywf/58AoEA6enpjBo1ii1btrBt2zauu+469u3bR2trK1/60pe49dZbgZOnfGlsbOSqq67ioosu4rXXXqO0tJSnnnqKjIyMmNQ37M51M1gBf2QDRUdIl0YUGcm+87+beOfA8Zg+5qxxufzr383ucfndd9/Nxo0bWbduHS+99BLXXHMNGzdu7NwF8sEHH6SgoICWlhbOO+88PvKRj1BYWHjKY2zfvp3f/va3PPDAA3z0ox/liSee4BOf+ERM6k+aoPd5W6J1DVwRibdFixadsp/7Pffcw5NPPgnAvn372L59+7uCvry8nPnz5wOwYMECdu/eHbN6ki7ow8p5kRGttzXvMyUrK6tz+qWXXuL555/n9ddfJzMzk0svvbTb/eDT0tI6p/1+Py0tLTGrJ2lOaubzdi0NKelF5AzLycmhoaGh22X19fWMGjWKzMxMtmzZwhtvvHGGq0umNXrfiTV6Bb2InFmFhYVceOGFzJkzh4yMDMaMGdO5bMmSJdx///3MnDmTGTNmsHjx4jNeX/IEfecYfZwLEZER6ZFHHul2flpaGs8880y3y06MwxcVFbFx48bO+V//+tdjWlvSDd1ojV5E5FRJFPSRpA8p6EVETpF0Qa+cFxE5VRIFfeR3m06BICJyiiQK+kjS3/NCJT97ZWecqxERGT6SJuijT9F819Ob41eIiMgwk0RBf+rJ+HXglIgMV9nZ2QAcOHCAG264ods2l156KRUVFTF5vqQJegC/72TYT/nnVb20FBGJv3HjxrFixYohf56kCnqtxYtIPCxfvpx777238/Ydd9zBXXfdxWWXXca5557L3Llzeeqpp951v927dzNnzhwAWlpauPHGG5k5cybXX399TM91kzRHxnZnZ3Uj2ekpjM5Jj3cpInKmPLMcDm2I7WOOnQtX3d3j4mXLlvHlL3+ZL3zhCwA8/vjjPPvss3zxi18kNzeXmpoaFi9ezIc+9KEer/n6k5/8hMzMTDZv3sz69es599zYXXU1qYP+/T/4CwC7774mzpWISDI755xzOHLkCAcOHKC6uppRo0YxduxYvvKVr/Dyyy/j8/nYv38/hw8fZuzYsd0+xssvv8wXv/hFAObNm8e8efNiVl9SB/0Jy59Yz90fid0fTUSGsV7WvIfS0qVLWbFiBYcOHWLZsmX85je/obq6mjVr1hAIBCgrK+v29MRnQlKN0eekd/+59ejqfWe4EhEZaZYtW8ajjz7KihUrWLp0KfX19YwePZpAIMCLL77Inj17er3/JZdc0nlitI0bN7J+/fqY1ZZUQf/Wv1zOx8+fyH8umx/vUkRkhJk9ezYNDQ2UlpZSUlLCxz/+cSoqKpg7dy4PPfQQZ511Vq/3/9znPkdjYyMzZ87k9ttvZ8GCBTGrLamGblL8Pv79+rkcOPburdX1LR3kZQTiUJWIjBQbNpzcCFxUVMTrr7/ebbvGxkYgcnHwE6cnzsjI4NFHHx2SupJqjf6Eouy0d83bfDC2FwsWEUkUSRn0qSknu3X/JyJff+59sTJe5YiIxFVSBn20K2dHLun1yvaaOFciIkPJjZBzlA+mn0kf9NEHJ4yUN4LISJOenk5tbW3S/48756itrSU9fWAHgSbVxthoF08roj0YBuCaeSU8vf4g/7pyE3deOyfOlYlIrI0fP56qqiqqq6vjXcqQS09PZ/z48QO6T9IG/cO3nN85vau6CYBVGw4p6EWSUCAQoLy8PN5lDFtJP3QD8CNvv/rLzhod30JEROJgRAT9jLE5TCnOoqGtI96liIiccSMi6AHyMgLUtyjoRWTkGTFBHww7Xq2sJRgKx7sUEZEzql9Bb2ZLzGyrmVWa2fJulqeZ2WPe8jfNrMybX2ZmLWa2zvu5P8b199v6qnoApn7rGcK6QImIjCB9Br2Z+YF7gauAWcBNZjarS7NbgDrn3FTgR8D3opbtcM7N934+G6O6B+y6+eM6pzWEIyIjSX/W6BcBlc65nc65duBR4Nouba4FfuVNrwAus54uoxIn/3njOZ3TL2w5EsdKRETOrP4EfSkQfUL3Km9et22cc0GgHij0lpWb2Vtm9hczu7i7JzCzW82swswqhvKAh0mFmQD87K+7huw5RESGm6HeGHsQmOicOwf4KvCImeV2beSc+6lzbqFzbmFxcfGQFfPi1y4FImeybAuGhux5RESGk/4E/X5gQtTt8d68btuYWQqQB9Q659qcc7UAzrk1wA5g+ukWPVg+38nRpBnf/mO8yhAROaP6E/SrgWlmVm5mqcCNwMoubVYCN3vTNwAvOOecmRV7G3Mxs8nANGBnbEofnD9++eTo0cH6d1+gREQk2fQZ9N6Y+23As8Bm4HHn3CYzu9PMPuQ1+zlQaGaVRIZoTuyCeQmw3szWEdlI+1nn3NEY92FAzhqby4VTI5sPXtqa/CdAEhHp10nNnHOrgFVd5t0eNd0KLO3mfk8AT5xmjTF338cWcPadz9HUFox3KSIiQ27EHBkbLTcjhaxUP/u7ubasiEiyGZFBb2Y0tYf4xau7412KiMiQG5FBHy3Zr0gjIjJig35cXuRSXG/tOxbfQkREhtiIDfpffmYRAJVHGuNciYjI0BqxQT9hVOR0CDWNbXGuRERkaI3YoM9I9ZMe8FHX1B7vUkREhtSIDXqAwqw0ahX0IpLkRnTQj8oKcOS4hm5EJLn168jYZLVx//F4lyAiMuRG9Br91XPHAlDdoLV6EUleIzroP3H+JCByfnoRkWQ1ooN+9rg8ADbsr49zJSIiQ2dEB31eZoCJBZlsVNCLSBIb0UEPkevIPrPxULzLEBEZMiM+6M0ilxds7dA1ZEUkOY34oL985mhA57wRkeQ14oP+rJJcAH752u74FiIiMkRGfNCfV1YAwOHjrXGuRERkaIz4oAe4Zm4Je2qb412GiMiQUNADs8blsvdoMw2tHfEuRUQk5hT0wJTibEAHTolIclLQA2dPiBwh++Bfd8W5EhGR2FPQAyV5GQA8v/lInCsREYk9Bb2nOCcNgFDYxbkSEZHYUtB7vnTZNACONGg3SxFJLgp6T0FWKgC/eWNvnCsREYktBb3nwilFAOyubYpzJSIisaWg9+RlBpg2Opu2YDjepYiIxJSCPsrk4ix2VuvkZiKSXBT0UcqLstlR3aRTFotIUulX0JvZEjPbamaVZra8m+VpZvaYt/xNMyvrsnyimTWa2ddjVPeQmD8hH4AXt2h/ehFJHn0GvZn5gXuBq4BZwE1mNqtLs1uAOufcVOBHwPe6LP8h8Mzplzu0zp2UD8DnfrM2voWIiMRQf9boFwGVzrmdzrl24FHg2i5trgV+5U2vAC4z79JNZnYdsAvYFJOKh9DonPR4lyAiEnP9CfpSYF/U7SpvXrdtnHNBoB4oNLNs4J+A7/T2BGZ2q5lVmFlFdXV1f2sfEv/3yhkANLcH41qHiEisDPXG2DuAHznnet2VxTn3U+fcQufcwuLi4iEuqXcnToWwenddXOsQEYmVlH602Q9MiLo93pvXXZsqM0sB8oBa4HzgBjP7PpAPhM2s1Tn349MtfKi8Z3IhAGv31PHe6fH90BERiYX+BP1qYJqZlRMJ9BuBj3VpsxK4GXgduAF4wTnngItPNDCzO4DG4RzyACV5kXH6fUd1xSkRSQ59Br1zLmhmtwHPAn7gQefcJjO7E6hwzq0Efg48bGaVwFEiHwYJKcXvY1FZAVV1LfEuRUQkJvqzRo9zbhWwqsu826OmW4GlfTzGHYOoLy4mFGTyamVNvMsQEYkJHRnbjYkFmRxuaNURsiKSFBT03Zg6OhvnoPKIznsjIolPQd+NGWMjFwvfdrghzpWIiJw+BX03JhVmEfAb2w5rjV5EEp+CvhsBv48pxdls1xq9iCQBBX0Ppo/J4e2qeiKHA4iIJC4FfQ/mluZR09hGfUtHvEsRETktCvoejPWOkD3S0BbnSkRETo+CvgejvZObHT7eGudKREROj4K+B2NyvTX641qjF5HEpqDvwehcb42+QWv0IpLYFPQ9yExNISctRWv0IpLwFPS9GJOXrjF6EUl4CvpejMlN45CCXkQSnIK+F6X5GVQebtRBUyKS0BT0vZhZkktDW5DapvZ4lyIiMmgK+l6U5mcAcOCYrjYlIolLQd+LcQp6EUkCCvpenFij339MG2RFJHEp6HuRnxkgM9XPfl0oXEQSmIK+F2ZGSV46L249Eu9SREQGTUHfhxljc9hT20QwFI53KSIig6Kg78Ml04oJOzhYr3F6EUlMCvo+TCjIBGBfXXOcKxERGRwFfR8mjIoEfdVRbZAVkcSkoO9DSX46PtMavYgkLgV9HwJ+HyV5Gew7qqAXkcSkoO+HCQUZ7NO+9CKSoBT0/TB9TA7vHDius1iKSEJS0PfDmNx0WjpCVDfqalMikngU9P1wvKUDgB8+ty3OlYiIDFy/gt7MlpjZVjOrNLPl3SxPM7PHvOVvmlmZN3+Rma3zft42s+tjXP8Z8akLywD4y7bq+BYiIjIIfQa9mfmBe4GrgFnATWY2q0uzW4A659xU4EfA97z5G4GFzrn5wBLg/5lZSoxqP2NK8iJnsbx0RnGcKxERGbj+rNEvAiqdczudc+3Ao8C1XdpcC/zKm14BXGZm5pxrds4FvfnpQMJuzZw+Jpu6po54lyEiMmD9CfpSYF/U7SpvXrdtvGCvBwoBzOx8M9sEbAA+GxX8nczsVjOrMLOK6urhOTwyKjOVo826pKCIJJ4h3xjrnHvTOTcbOA/4ppmld9Pmp865hc65hcXFw3N4pCArlTpdO1ZEElB/gn4/MCHq9nhvXrdtvDH4PKA2uoFzbjPQCMwZbLHxNCorlTqt0YtIAupP0K8GpplZuZmlAjcCK7u0WQnc7E3fALzgnHPefVIAzGwScBawOyaVn2GjMgPUNXcQDifsZgYRGaH63APGORc0s9uAZwE/8KBzbpOZ3QlUOOdWAj8HHjazSuAokQ8DgIuA5WbWAYSBzzvnaoaiI0Nt/KhMQmHHnqPNlBdlxbscEZF+69eujs65VcCqLvNuj5puBZZ2c7+HgYdPs8ZhYWZJLgCVRxoV9CKSUHRkbD+Ny4tsQz5Yr5ObiUhiUdD3U1F2GgG/ceCYLikoIolFQd9PPp8xJjdda/QiknAU9AMwLj+D3bW6AImIJBYF/QDMKsll6yGdl15EEouCfgDKi7Jo7Qiz/UhjvEsREek3Bf0AnD0hH4A3dtb23lBEZBhR0A/A2ePzSA/42KNxehFJIAr6ATAzJhdls+1wQ7xLERHpNwX9AJ09IZ91+45pg6yIJAwF/QDNLMmhoTXI4eO6ULiIJAYF/QBNLsoGYGe19rwRkcSgoB+gycWRE5rtqGmKcyUiIv2joB+gsbnpZAT8WqMXkYShoB8gn88oL8piZ7XW6EUkMSjoB2FycRY7a7RGLyKJQUE/CJOLs6mqa6G1IxTvUkRE+qSgH4QpxVk4h46QFZGEoKAfBO1iKSKJREE/COXeLpY7tYuliCQABf0gZKelMCY3jR1aoxeRBKCgH6TJRdm8vK063mWIiPRJQT9I08dkU9PYzm4N34jIMKegH6Sr55YA8LfdR+NciYhI7xT0g7SwrIC0FB/bDunc9CIyvCnoB8nvM6YUZ/PmLq3Ri8jwpqA/DRMKMth6qIFQWBchEZHhS0F/Gt43YzTtoTAHjrXEuxQRkR4p6E9DeVHkwKld2vNGRIYxBf1pmDYmB4Ath47HuRIRkZ71K+jNbImZbTWzSjNb3s3yNDN7zFv+ppmVefMvN7M1ZrbB+/3+GNcfVwVZqRRkpbKrRic3E5Hhq8+gNzM/cC9wFTALuMnMZnVpdgtQ55ybCvwI+J43vwb4O+fcXOBm4OFYFT5cTC7KYn3VsXiXISLSo/6s0S8CKp1zO51z7cCjwLVd2lwL/MqbXgFcZmbmnHvLOXfAm78JyDCztFgUPlyUF2Wx6cBxjjS0xrsUEZFu9SfoS4F9UbervHndtnHOBYF6oLBLm48Aa51zbYMrdXi6cvZYADYd0Di9iAxPZ2RjrJnNJjKc8396WH6rmVWYWUV1dWKdKGzBpFEAbDmoI2RFZHjqT9DvByZE3R7vzeu2jZmlAHlArXd7PPAk8Enn3I7unsA591Pn3ELn3MLi4uKB9SDORmWlMqkwk3X76uJdiohIt/oT9KuBaWZWbmapwI3Ayi5tVhLZ2ApwA/CCc86ZWT7wNLDcOfdqjGoeduZPyOfNXUdxTkfIisjw02fQe2PutwHPApuBx51zm8zsTjP7kNfs50ChmVUCXwVO7IJ5GzAVuN3M1nk/o2Peizg7Z0I+x5o7qG5Mqs0PIpIkUvrTyDm3CljVZd7tUdOtwNJu7ncXcNdp1jjsnThwatuhRkbnpMe5GhGRU+nI2BiYU5pHwG+8vD2xNiSLyMigoI+BvIwAF08rZuW6AwRD4XiXIyJyCgV9jNx43gQOHW/lle018S5FROQUCvoYuXhaMT6Dt/Ydi3cpIiKnUNDHSEaqn+ljcnTeGxEZdhT0MTS3NI+XtlbTHtQ4vYgMHwr6GJo2JhuAb/9+Q5wrERE5SUEfQ8vOmwjA4xVVOkpWRIYNBX0M5WUE+MDMyIG/a/cei28xIiIeBX2M/WjZfFJ8xg+e2xrvUkREAAV9zOWkB/D5jNd21PIvv98Y73JERBT0Q+F3n7sAgEf+tpe2YCjO1YjISKegHwJzSvP4z2XzCYUd/1NRFe9yRGSEU9APkcWTI1dSfLxiXx8tRUSGloJ+iIzNS+fSGcU0tQXjXYqIjHAK+iF00dQidlQ3saumKd6liMgIpqAfQlfPLQHgf98+EOdKRGQkU9APoXH5GSwqK+CJtTpSVkTiR0E/xJYuHM+e2mZWaq1eROJEQT/EPnLueGaMyeFLj65jZ3VjvMsRkRFIQX+6Wupg1Tfg8KZuF/t8xr9dNwdAa/UiEhcK+tO1fw2s+SX85AL42QfgrV9D+6l72SwqL2De+Mi56kVEzjQF/ema+gH42ha48rvQehye+gL84Cx4+utw6OS5bj44r4R1+45x9X+9og2zInJGKehjIbMA3vN5+MKb8Ok/woyrYO1DcP+F8MBlsPZhPjxnFADvHDzOrQ+vIRjSVahE5Myw4bZ2uXDhQldRURHvMk5f81FY/xhU/AJqtkJaLg3Tr+ejFWex2U2iND+DP3/tvaQH/PGuVESSgJmtcc4t7HaZgn6IOQd734iM4296EkJtbPFP58HW9/KH0Ht48Z+vYUxueryrFJEEp6AfLpqPwvrHYc0voHoLDS6D34cuZNENX2X62RdgZvGuUEQSVG9BrzH6MymzABZ/Fj7/Bu03r+K58EKW+v/CjN9fzeEfXABrfgVt2tdeRGJLa/RxFAyFue+PFdS+9ms+7v8z031VkJoD85bCgk9BydnxLlFEEoSGboa51buPsvT+1/hE6WG+U7oa/ztPQrAVxp0DCz5N+8zrITWb362tYtqYHBZMGhXvkoeVcNjh82nYSwavprGNouy0eJdxWhT0w5xzjk//cnXnAVW5NHJz1psstT8zMbibRpfOU6ELeST0fja5copz0vj1LeczY2xOnCuPj2AozE9f2cmCiaN44JVdPL/5MABzS/PYfqSBK2aN5aJpRXx04QRCYYd/gB8Cx1s7yE0P9NnOOdfrdpWD9S3c9fRmPnVBGeeVFQyohhM6QmEC/pMjrM3tQZ7ZcIiLpxeRmx4Y1F5brR0hHnx1F9fMLeE3b+5lTmkem/bXs76qntd31vIf18/lY+dPHFS9AxUMhWkLhqlrbqe5PUSKz8hOS6E4Jy1m26x6e52cc1xzz1955+BxRuek8bdvfSAmz9mTo03t3LFyE7kZKWSnBWho7WD+hHyONLTxqQvKyEpLGfRjn3bQm9kS4L8AP/Az59zdXZanAQ8BC4BaYJlzbreZFQIrgPOAXzrnbuvruUZi0J/w70+/wwOv7Iqa4zjXtnOT/wU+6H+DDGtnfbic34bez8rQBfzbRxdzyfRifv/WfhrbgnzhfVMJ+H0EQ2H++4VKxuSmd/7DVh5pxDlHdUMbF0wt6ndN+442c/H3X+RPX7mEaWN6/2Bp7Qi9K3h21zTxoR//levPKeW88gLOGpvL1NHZncub2oK0doTw+4z8zNRuH/fI8Vbe/4O/0DjIi7ik+IyHPrOIqmMtfGPFeq6ZV8KPbzrnlH/+jfvrue7eVwmGXed9/vKN95ER8DMqM0BbMNzZt3DYccHdL3DoeCtLZo/ln646i9seWcsn3zOJ9501mlS/jw/88C/UNLZ3Pv4f/vEijja18+ymQ2w8cByc467r5jJ3fB7twTChsGNfXTNTirPx+4yG1g7m3vHcKf24Zm4JT2842Hk74DfW/MvlfX4oBUNhHqvYx0Ov7WHr4YZ+/c0mFmTy7Wtm0hFyzBufx4SCTCDy7fObv9vA6Jw0qupauGzmaL56+XRy0gOs3VtH5ZFGSvMzuLCb99ix5nYOH2+jpSPEdfe+2mcNW/5tCekBf69B3REK89DrezBgd20TZ4/P52v/8zY3LZrIVXPG8tLWah58NfI/lZ2WwiXTi9hxpIlx+em8vL2GUPjU/Lvv4+d2nl68L5Fv4q8zb3weD3xy4bv2nguGwnz79xv58gems2rDQe78wzt9PubG71xJ9iDD/rSC3sz8wDbgcqAKWA3c5Jx7J6rN54F5zrnPmtmNwPXOuWVmlgWcA8wB5ijo+3aovpV3Dtazbl89KT5jxZoqbr6gDF9rPZMP/oH3NjwNR96h0aWzMnQBr4dn0U4KQfwESaEDP0Hn925Hfjq85SeWpQRSuXzOeEblZvL4mkMcaAxRkp9FTnoK6QE/n790Cnes3MSB+tZTatv0nSt54JWd/HV7DR89bwJ1Te1895ktjB+VwaySXJ57J7JmPXtcLueVFfDL13Z36Z0jQIgPzi7ivIk5/GHdXrYfPEaAECkW4v6b5jI2J8DXH6ugtr6JK84q5JIp+dy9amOkjffjI4wRuYrX7JIc3jujmN01jbS2h9lT28iWQw0ca46ErOG835xye25pLtPG5HDhlAL21jbx4xcrO5fRpa0BWal+pozOJoSfI01hdh5t6/zbdv59O//uKZ2viS8lQFOHee0iy068Fq6HfSFK8zPYf6xlwO+dnf9xNWZQsaeOwqxUrr7nFcJhaO/nwXk3LZrA0oUT+NaTG9l88Pgpy756+XR++Kdt3d5v8eQC3tp7jLbgyef52uXTed9ZowGobmzj079Y3etz56Sn0NDa8wf5n75yCZOLs/nvF7azeHJh56U6b/jJa1TsqetX/3rzu89fwIfvew2AC6cW8tBnzn/XN8GD9S2857svAHDl7DE8u+nwKcu/++G53LQosmJ130uVfP+PW3t8PjOYNjqbMbnpvLK9pnP+soUT+N4N8wbVh9MN+vcAdzjnrvRufxPAOffdqDbPem1eN7MU4BBQ7LwHN7NPAQsV9DHgHFSt5tCL95O343/JsPa+79MPYWdeEJ364RCM+uA4uTyFID5SCJNC0PsdIoUgAYuEsZ9QZzhn+MNYOIgPHQ0cLeh87/pbt5PS+fcuyM0iMyODsKXQ0O6obmhnfEEWeZmpYD6ONnew+WADYQx3yg+E8eEA5/0OpKTg8xlTRudQkJ1OeiCFHTXNlI7KJDM1BcwHWOS3GXuONlOxp55g2BF2eI8csWBSHmv3HKMwK0BjWwcdIRf1Iene9aEa/YFpOLLTUpg+Jpv2YJiywkyvQgiGw5FwdY7n3znU+e2qLxb12wFZaX6a2kJAZGVgSnEW2WkpvLW3jvQUH/mZqWzYX8/c0jzyMwP4zEgP+Klv6eCNnbWdjztjbA6TCjOpa+5gd00zRxravB6eVFaUTX5mgLf2HiPFZ1w4tZiXt1cTdi7qLxExe1weEwsyvQ+QLt9QzKhpbCOtfDE57/3HfvX7XX+H0wz6G4Alzrl/8G7/PXB+dGib2UavTZV3e4fXpsa7/Sl6CXozuxW4FWDixIkL9uzZM7AejlDB5mO4+v0ECEE4yOFjjTy9bi+NLS38/Xml5KfBq9sOMaUwjZLsFAh30NTSyu/X7Gbn4XqmFqaR7g+zeFIuuWnGtgNHIdTBuj01TC9KIzcVphalk+4Ls2ZXNbXHm0ghRGGGkepzNLQ7Jo/Jp67Vsb2mhYuml5CWlsb2mlbqWh3zJxWRm5UJ/hTwBcCXwo6jbTz59mFS09K4dOY4zho3ipRAKjtq2/jvl/bQgZ+F5UV8ZFE5v1t3hD9vO8pNiyfzwfkTox7nxPCQ989i1uftbYcbyMtMZUxOGiFnfPbXa9jiDWM4jE9fUM4tF5V3e99gKMzzW6r58+bDBCzEoaPH+f6HZ1GU4WP93hre2VfL0nPG4HdBCAch1AGh9pPT4Q6q6xspzvBBuMObF6SxuYX6xiayUhytbW2MzfZz9HgTOw/XMbUwnfw0vMdoh3AIcJEPeue86TDN7UHC4TAH6ppoagti3rcdH2EyU/2MzUkjI2CR7w4uHPUY4c7HiH68yKfDyXbOhalpbCfkHAVZaaSmnPq3DwN1zR04B3mZqQT8RktHmCMN7Z1/W4D0VD9jc9MBw5l5c6Nfr3e/diEHwbCjIxSmqq7nbzhTirNI8bZjhB34exvb72Pc/3hrB/tPea5T8zEtxUdeRgo1je0UZ6dS4A03tgUj3yY7n4bIh0xOWgphHOainvpdmRt1e9oVsOS7DMawD/poWqNPHH1tjBzofYOhMFsONTB7XG7nsu7G/WPFOUdze4jMVH/CH6zWEQrT1Bbs3M4xXPZEGoo6TmTW2r11HKxv5YPzxsX08QF21TTxuV+vYcuhk9s0vvj+qfzjZdNO2Tge7Q/rD3DbI28BUPHtD5zxvXh6C/r+jPrvByZE3R7vzeuuTZU3dJNHZKOsJLHTCcfu7pvi9zGnNO+UeUN5LiAzO629HIaTgN93ysbs4RDyMDR1nHjvLJg0uD2Z+qO8KIs/fvkSfre2iqNN7fzDxZP7vM8H541jUVkBGMNuV83+vMtXA9PMrJxIoN8IfKxLm5XAzcDrwA3AC66vrwoiIsPch88dP6D2o4fpeav6DHrnXNDMbgOeJbJ75YPOuU1mdidQ4ZxbCfwceNjMKoGjRD4MADCz3UAukGpm1wFXRO+xIyIiQ6tf31udc6uAVV3m3R413Qos7eG+ZadRn4iInCad1ExEJMkp6EVEkpyCXkQkySnoRUSSnIJeRCTJKehFRJLcsDsfvZlVA4M92U0RUNNnq8STrP2C5O1bsvYLkrdvid6vSc654u4WDLugPx1mVtHTuR4SWbL2C5K3b8naL0jeviVrv0BDNyIiSU9BLyKS5JIt6H8a7wKGSLL2C5K3b8naL0jeviVrv5JrjF5ERN4t2dboRUSkCwW9iEiSS4qgN7MlZrbVzCrNbHm86xkMM9ttZhvMbJ2ZVXjzCszsT2a23fs9yptvZnaP19/1ZnZufKs/ycweNLMj3uUlT8wbcD/M7Gav/XYzuzkefemqh77dYWb7vddtnZldHbXsm17ftprZlVHzh9X71cwmmNmLZvaOmW0ysy958xP6deulXwn/mg2Ycy6hf4hcDGUHMBlIBd4GZsW7rkH0YzdQ1GXe94Hl3vRy4Hve9NXAM0SuQbwYeDPe9UfVfAlwLrBxsP0ACoCd3u9R3vSoYdq3O4Cvd9N2lvdeTAPKvfeofzi+X4ES4FxvOgfY5tWf0K9bL/1K+NdsoD/JsEa/CKh0zu10zrUDjwLXxrmmWLkW+JU3/Svguqj5D7mIN4B8MyuJQ33v4px7mchVxqINtB9XAn9yzh11ztUBfwKWDHnxfeihbz25FnjUOdfmnNsFVBJ5rw6796tz7qBzbq033QBsBkpJ8Netl371JGFes4FKhqAvBfZF3a6i9xdzuHLAc2a2xsxu9eaNcc4d9KYPAWO86UTr80D7kWj9u80bwnjwxPAGCdo3MysDzgHeJIlety79giR6zfojGYI+WVzknDsXuAr4gpldEr3QRb5bJvy+sMnSjyg/AaYA84GDwA/iWs1pMLNs4Angy86549HLEvl166ZfSfOa9VcyBP1+YELU7fHevITinNvv/T4CPEnk6+LhE0My3u8jXvNE6/NA+5Ew/XPOHXbOhZxzYeABIq8bJFjfzCxAJAx/45z7nTc74V+37vqVLK/ZQCRD0K8GpplZuZmlAjcCK+Nc04CYWZaZ5ZyYBq4ANhLpx4k9F24GnvKmVwKf9PZ+WAzUR33FHo4G2o9ngSvMbJT3tfoKb96w02XbyPVEXjeI9O1GM0szs3JgGvA3huH71cwM+Dmw2Tn3w6hFCf269dSvZHjNBizeW4Nj8UNkL4BtRLaMfyve9Qyi/slEtuS/DWw60QegEPgzsB14Hijw5htwr9ffDcDCePchqi+/JfJ1uIPIWOYtg+kH8BkiG8MqgU/Hu1+99O1hr/b1RP75S6Laf8vr21bgquH6fgUuIjIssx5Y5/1cneivWy/9SvjXbKA/OgWCiEiSS4ahGxER6YWCXkQkySnoRUSSnIJeRCTJKehFRJKcgl5EJMkp6EVEktz/B2JaCOc2gvQjAAAAAElFTkSuQmCC\n",
192 | "text/plain": [
193 | ""
194 | ]
195 | },
196 | "metadata": {
197 | "needs_background": "light"
198 | },
199 | "output_type": "display_data"
200 | }
201 | ],
202 | "source": [
203 | "# fit and collect AR coefficients\n",
204 | "coeff_list = []\n",
205 | "for name in data_names:\n",
206 | " print(\"fitting: {}\".format(name))\n",
207 | " df = pd.read_csv(os.path.join(data_path, name + '.csv'), header=None, index_col=False)\n",
208 | " # Init Model\n",
209 | " learn = init_ar_learner(\n",
210 | " series=df,\n",
211 | " ar_order=ar_order,\n",
212 | " n_forecasts=1,\n",
213 | " valid_p=valid_p,\n",
214 | " sparsity=sparsity,\n",
215 | " train_bs=32,\n",
216 | " valid_bs=1024,\n",
217 | " verbose=False,\n",
218 | " )\n",
219 | " # find Learning Rate\n",
220 | " lr_at_min, lr_steep = learn.lr_find(start_lr=1e-7, end_lr=100, num_it=100, show_plot=True)\n",
221 | " plt.show()\n",
222 | " print(\"lr at minimum: {}; steeptes lr: {}\".format(lr_at_min, lr_steep))\n",
223 | " lr_max = lr_at_min/10\n",
224 | "\n",
225 | " # Run Model\n",
226 | " learn.fit_one_cycle(n_epoch=n_epoch, lr_max=lr_max)\n",
227 | " learn.recorder.plot_loss()\n",
228 | " plt.show()\n",
229 | " # record Coeff\n",
230 | " coeff = arnet.coeff_from_model(learn.model) \n",
231 | " coeff_list.append({\"name\": name, \"coeff\": coeff[0]})\n",
232 | "df_coeff = pd.DataFrame(coeff_list)\n"
233 | ]
234 | },
235 | {
236 | "cell_type": "code",
237 | "execution_count": 5,
238 | "metadata": {},
239 | "outputs": [
240 | {
241 | "data": {
242 | "text/html": [
243 | "\n",
244 | "\n",
257 | "
\n",
258 | " \n",
259 | " \n",
260 | " | \n",
261 | " name | \n",
262 | " coeff | \n",
263 | "
\n",
264 | " \n",
265 | " \n",
266 | " \n",
267 | " | 0 | \n",
268 | " ar_3_ma_0_noise_0.100_len_10000 | \n",
269 | " [0.19604042, 0.2887145, -0.4777012, 0.0003959917, 1.7987082e-05, 3.091372e-05, 0.00032146755, -1.1136071e-05, 2.8788853e-07, -2.827955e-06] | \n",
270 | "
\n",
271 | " \n",
272 | "
\n",
273 | "
"
274 | ],
275 | "text/plain": [
276 | " name \\\n",
277 | "0 ar_3_ma_0_noise_0.100_len_10000 \n",
278 | "\n",
279 | " coeff \n",
280 | "0 [0.19604042, 0.2887145, -0.4777012, 0.0003959917, 1.7987082e-05, 3.091372e-05, 0.00032146755, -1.1136071e-05, 2.8788853e-07, -2.827955e-06] "
281 | ]
282 | },
283 | "execution_count": 5,
284 | "metadata": {},
285 | "output_type": "execute_result"
286 | }
287 | ],
288 | "source": [
289 | "df_coeff"
290 | ]
291 | },
292 | {
293 | "cell_type": "code",
294 | "execution_count": 6,
295 | "metadata": {},
296 | "outputs": [
297 | {
298 | "data": {
299 | "image/png": "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LVu2pLS0lH33rWsiA7YqM6LGZ+Fq9J3vfIdDDz2UP/zhD5x00kn8+te/JiIYO3Ys11133Tbv33fffSktLa3cTyfW6ve//faWSRsq7t+R7wBg8+bN7LrrrpWtl9Xt6PdUoa64KupSWFjIpk2bWLt2LZ06daq1jkCtx+vDy0KZmVneW7t2LR07dqRdu3YsWbKEuXPnpnVfRLBs2bLK7ccee4yDDjqoXrGsW7eOnXbaiV122YX33nuPJ56ocXYoACTx1a9+tXLM1NSpUxkxYkSt19dmyJAhzJgxg/LyctasWcPzzz/PoEGDtrrmzTffpHv37lx00UWMGDGCBQsWMHToUGbOnMnq1alh3R988AErV66ssYy9996bDh06MHfuXCKCu+++e7tiHThwIEuXLmX58uX861//Yvr06QwfPnyHv4MOHTrQrVs3HnzwQSD185s/f37a8WxLXXENHz6cqVOnAjBz5kyOPfZYJDF8+HCmT5/Oxo0bWb58OUuXLmXQoEG11r2+3JJmZmbbJRfTAw0bNozbb7+dgw8+mK985SsMHjw4rfsqWpLWrVtHRNC3b19+9atf1SuWvn370r9/fw466CD2228/jjjiiDqvv/766xk1ahQ/+clP6N+/P+PHj9/uMk8++WTmzJlD3759kcQNN9zAXnvtxYoVKyqveeCBB5g2bRqtWrVir7324sc//jG77bYb11xzDccffzybN2+mVatW3Hbbbey/f82rLP7yl7+snILjxBNP5MQTTwRS490Avve97/Huu+9SVFTEunXraNGiBTfddBOLFi2iQ4cO3HrrrZxwwgmUl5dz1lln0atXr3p9B/feey/nnXce11xzDZ9//jmjRo2ib9++2/39VTx08Mknn1BYWMiUKVM44YQTao1r/PjxnHHGGfTo0YPddtuN6dOnA6ku79NOO42ePXvSsmVLbrvttsqW3NrqXh/akSbCfFZUVBTFxcVfON7U50lrbPXzHHBmjcfixYs5+OCDcx2GWaNX078lSSURUVTT9e7uNDMzM8tD7u40M7Mm4eSTT2b58uVbHbv++usrZ4SvasKECZVjnSqceuqpXHnllY22/O1x6KGHbvWkJMC0adMqn2RtCJn+Dl577TXOOOOMrY61adOGl156aYdjzDV3d+Yxd3eaWT5wd6dZZri708zMMq6p/Q+9WUPbkX9DTtLMzKxO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\n",
300 | "text/plain": [
301 | ""
302 | ]
303 | },
304 | "metadata": {
305 | "needs_background": "light"
306 | },
307 | "output_type": "display_data"
308 | }
309 | ],
310 | "source": [
311 | "if save:\n",
312 | " df_coeff.to_csv(\n",
313 | " os.path.join(results_path, \"coeff_ar-{}_spar-{}.csv\".format(ar_order, sparsity)),\n",
314 | " index=False,\n",
315 | " )\n",
316 | " for index, row in df_coeff.iterrows():\n",
317 | " arnet.plot_weights(\n",
318 | " ar_val=ar_order,\n",
319 | " weights=row[\"coeff\"],\n",
320 | " model_name=row[\"name\"],\n",
321 | " save=True,\n",
322 | " savedir=results_path,\n",
323 | " figsize=(10,3),\n",
324 | " )"
325 | ]
326 | },
327 | {
328 | "cell_type": "code",
329 | "execution_count": null,
330 | "metadata": {},
331 | "outputs": [],
332 | "source": []
333 | },
334 | {
335 | "cell_type": "code",
336 | "execution_count": null,
337 | "metadata": {},
338 | "outputs": [],
339 | "source": []
340 | },
341 | {
342 | "cell_type": "code",
343 | "execution_count": null,
344 | "metadata": {},
345 | "outputs": [],
346 | "source": []
347 | },
348 | {
349 | "cell_type": "code",
350 | "execution_count": null,
351 | "metadata": {},
352 | "outputs": [],
353 | "source": []
354 | }
355 | ],
356 | "metadata": {
357 | "kernelspec": {
358 | "display_name": "light-matter",
359 | "language": "python",
360 | "name": "light-matter"
361 | },
362 | "language_info": {
363 | "codemirror_mode": {
364 | "name": "ipython",
365 | "version": 3
366 | },
367 | "file_extension": ".py",
368 | "mimetype": "text/x-python",
369 | "name": "python",
370 | "nbconvert_exporter": "python",
371 | "pygments_lexer": "ipython3",
372 | "version": "3.8.6"
373 | }
374 | },
375 | "nbformat": 4,
376 | "nbformat_minor": 4
377 | }
378 |
--------------------------------------------------------------------------------