From a3ab53d8571bd83e43dc6f4ee596e4d84db56e65 Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 18 Sep 2024 09:44:34 -0400 Subject: [PATCH] FEAT: Add RMoK (#1148) * WIP - Add reversible mixture of kan * WIP - Allows import of RMoK * AutoRMoK, add it to doc, add parameters * Fix tests * Get default config of AutoRMoK * FIX: timemixer shapes mismatch and doc update (#1138) * Use math.ceil to prevent shape mismatch * Show exog support for KAN in doc * FEAT: TimeLLM is faster and supports more LLMs (#1139) * Fix issue #950: Reduce TimeLLM setup time for training * Restore changes on the examples * Revert changes to nbs/models.ipynb, nbs/models.softs.ipynb and neuralforecast/_modidx.py * Revert changes to nbs/models.ipynb, nbs/models.softs.ipynb and neuralforecast/_modidx.py * Refactor code to dynamically load models with AutoModel, AutoTokenizer, and AutoConfig - Updated load_model_and_tokenizer function to use AutoModel, AutoTokenizer, and AutoConfig for flexible model loading. - Included default model(gpt2) for cases where the specified model fails to load. - Kept llm, llm_config, and llm_tokenizer arguments to minimize changes. - Changed llm from storing pretrained weights to accepting pretrained model path to reduce necessary modifications. This update enhances the flexibility and reliability of model loading based on received feedback while minimizing necessary changes. * Refactor code to dynamically load models with AutoModel, AutoTokenizer, and AutoConfig - Updated load_model_and_tokenizer function to use AutoModel, AutoTokenizer, and AutoConfig for flexible model loading. - Included default model(gpt2) for cases where the specified model fails to load. - Kept llm, llm_config, and llm_tokenizer arguments to minimize changes. - Changed llm from storing pretrained weights to accepting pretrained model path to reduce necessary modifications. This update enhances the flexibility and reliability of model loading based on received feedback while minimizing necessary changes. * clear output * modify test code * Optimize model loading and add deprecation warning - Simplify model loading logic - Add constant for default model name - Improve error handling for model loading - Add success messages for model loading - Implement deprecation warning for 'llm_config' and 'llm_tokenizer' parameters - Update print messages for clarity - Remove redundant code This commit improves code readability, maintainability, and user experience by providing clearer feedback and warnings about deprecated parameters. * Resolved conflict in nbs/models.timellm.ipynb --------- Co-authored-by: ive2go Co-authored-by: Olivier Sprangers <45119856+elephaint@users.noreply.github.com> * Consistency with math.ceil --------- Co-authored-by: Olivier Sprangers <45119856+elephaint@users.noreply.github.com> Co-authored-by: ive2go * Add image, docstring, fix typo in comment --------- Co-authored-by: Olivier Sprangers <45119856+elephaint@users.noreply.github.com> Co-authored-by: ive2go --- nbs/docs/capabilities/01_overview.ipynb | 1 + nbs/imgs_models/rmok.png | Bin 0 -> 123789 bytes nbs/models.ipynb | 167 +++++- nbs/models.rmok.ipynb | 650 ++++++++++++++++++++++++ neuralforecast/_modidx.py | 42 ++ neuralforecast/auto.py | 123 ++++- neuralforecast/models/__init__.py | 3 +- neuralforecast/models/rmok.py | 473 +++++++++++++++++ 8 files changed, 1438 insertions(+), 21 deletions(-) create mode 100644 nbs/imgs_models/rmok.png create mode 100644 nbs/models.rmok.ipynb create mode 100644 neuralforecast/models/rmok.py diff --git a/nbs/docs/capabilities/01_overview.ipynb b/nbs/docs/capabilities/01_overview.ipynb index 12fe0f29e..11b964a7f 100644 --- a/nbs/docs/capabilities/01_overview.ipynb +++ b/nbs/docs/capabilities/01_overview.ipynb @@ -34,6 +34,7 @@ "|`NHITS` | `AutoNHITS` | MLP | Univariate | Direct | F/H/S | \n", "|`NLinear` | `AutoNLinear` | MLP | Univariate | Direct | - | \n", "|`PatchTST` | `AutoPatchTST` | Transformer | Univariate | Direct | - | \n", + "|`RMoK` | `AutoRMoK` | KAN | Multivariate | Direct | - |\n", "|`RNN` | `AutoRNN` | RNN | Univariate | Recursive | F/H/S | \n", "|`SOFTS` | `AutoSOFTS` | MLP | Multivariate | Direct | - | \n", "|`StemGNN` | `AutoStemGNN` | GNN | Multivariate | Direct | - | \n", diff --git a/nbs/imgs_models/rmok.png b/nbs/imgs_models/rmok.png new file mode 100644 index 0000000000000000000000000000000000000000..11c7cf4dbdccff578a93510e3e1bfc3df30ab48f GIT binary patch literal 123789 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 literal 0 HcmV?d00001 diff --git a/nbs/models.ipynb b/nbs/models.ipynb index f693134cb..018525399 100644 --- a/nbs/models.ipynb +++ b/nbs/models.ipynb @@ -66,6 +66,7 @@ "from neuralforecast.models.itransformer import iTransformer\n", "\n", "from neuralforecast.models.kan import KAN\n", + "from neuralforecast.models.rmok import RMoK\n", "\n", "from neuralforecast.models.stemgnn import StemGNN\n", "from neuralforecast.models.hint import HINT\n", @@ -2300,6 +2301,14 @@ "model.fit(dataset=dataset)" ] }, + { + "cell_type": "markdown", + "id": "8efc1692", + "metadata": {}, + "source": [ + "## C. KAN-Based" + ] + }, { "cell_type": "code", "execution_count": null, @@ -2444,7 +2453,7 @@ "id": "fd705a56", "metadata": {}, "source": [ - "## C. Transformer-Based" + "## D. Transformer-Based" ] }, { @@ -3427,7 +3436,7 @@ "id": "57d6cb1f", "metadata": {}, "source": [ - "## D. CNN Based" + "## E. CNN Based" ] }, { @@ -3573,7 +3582,7 @@ "id": "e6fd22c7", "metadata": {}, "source": [ - "## E. Multivariate" + "## F. Multivariate" ] }, { @@ -4680,6 +4689,158 @@ "model.fit(dataset=dataset)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab15c4b6", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "class AutoRMoK(BaseAuto):\n", + "\n", + " default_config = {\n", + " \"input_size_multiplier\": [1, 2, 3, 4, 5],\n", + " \"h\": None,\n", + " \"n_series\": None,\n", + " \"taylor_order\": tune.choice([3, 4, 5]),\n", + " \"jacobi_degree\": tune.choice([4, 5, 6]),\n", + " \"wavelet_function\": tune.choice(['mexican_hat', 'morlet', 'dog', 'meyer', 'shannon']),\n", + " \"learning_rate\": tune.loguniform(1e-4, 1e-1),\n", + " \"scaler_type\": tune.choice([None, 'robust', 'standard', 'identity']),\n", + " \"max_steps\": tune.choice([500, 1000, 2000]),\n", + " \"batch_size\": tune.choice([32, 64, 128, 256]),\n", + " \"loss\": None,\n", + " \"random_seed\": tune.randint(1, 20),\n", + " }\n", + "\n", + " def __init__(self,\n", + " h,\n", + " n_series,\n", + " loss=MAE(),\n", + " valid_loss=None,\n", + " config=None, \n", + " search_alg=BasicVariantGenerator(random_state=1),\n", + " num_samples=10,\n", + " refit_with_val=False,\n", + " cpus=cpu_count(),\n", + " gpus=torch.cuda.device_count(),\n", + " verbose=False,\n", + " alias=None,\n", + " backend='ray',\n", + " callbacks=None):\n", + " \n", + " # Define search space, input/output sizes\n", + " if config is None:\n", + " config = self.get_default_config(h=h, backend=backend, n_series=n_series) \n", + "\n", + " # Always use n_series from parameters, raise exception with Optuna because we can't enforce it\n", + " if backend == 'ray':\n", + " config['n_series'] = n_series\n", + " elif backend == 'optuna':\n", + " mock_trial = MockTrial()\n", + " if ('n_series' in config(mock_trial) and config(mock_trial)['n_series'] != n_series) or ('n_series' not in config(mock_trial)):\n", + " raise Exception(f\"config needs 'n_series': {n_series}\") \n", + "\n", + " super(AutoRMoK, self).__init__(\n", + " cls_model=RMoK, \n", + " h=h,\n", + " loss=loss,\n", + " valid_loss=valid_loss,\n", + " config=config,\n", + " search_alg=search_alg,\n", + " num_samples=num_samples, \n", + " refit_with_val=refit_with_val,\n", + " cpus=cpus,\n", + " gpus=gpus,\n", + " verbose=verbose,\n", + " alias=alias,\n", + " backend=backend,\n", + " callbacks=callbacks, \n", + " )\n", + "\n", + " @classmethod\n", + " def get_default_config(cls, h, backend, n_series):\n", + " config = cls.default_config.copy() \n", + " config['input_size'] = tune.choice([h * x \\\n", + " for x in config[\"input_size_multiplier\"]])\n", + "\n", + " # Rolling windows with step_size=1 or step_size=h\n", + " # See `BaseWindows` and `BaseRNN`'s create_windows\n", + " config['step_size'] = tune.choice([1, h])\n", + " del config[\"input_size_multiplier\"]\n", + " if backend == 'optuna':\n", + " # Always use n_series from parameters\n", + " config['n_series'] = n_series\n", + " config = cls._ray_config_to_optuna(config) \n", + "\n", + " return config " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "111d8d3b", + "metadata": {}, + "outputs": [], + "source": [ + "show_doc(AutoRMoK, title_level=3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2073d4aa", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture\n", + "# Use your own config or AutoRMoK.default_config\n", + "config = dict(max_steps=1, val_check_steps=1, input_size=12, learning_rate=1e-2)\n", + "model = AutoRMoK(h=12, n_series=1, config=config, num_samples=1, cpus=1)\n", + "\n", + "# Fit and predict\n", + "model.fit(dataset=dataset)\n", + "y_hat = model.predict(dataset=dataset)\n", + "\n", + "# Optuna\n", + "model = AutoRMoK(h=12, n_series=1, config=None, backend='optuna')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebe2c500", + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "# Check Optuna\n", + "assert model.config(MockTrial())['h'] == 12\n", + "\n", + "# Unit test to test that Auto* model contains all required arguments from BaseAuto\n", + "test_args(AutoRMoK, exclude_args=['cls_model']) \n", + "\n", + "# Unit test for situation: Optuna with updated default config\n", + "my_config = AutoRMoK.get_default_config(h=12, n_series=1, backend='optuna')\n", + "def my_config_new(trial):\n", + " config = {**my_config(trial)}\n", + " config.update({'max_steps': 1, 'val_check_steps': 1, 'input_size': 12, 'learning_rate': 1e-1})\n", + " return config\n", + "\n", + "model = AutoRMoK(h=12, n_series=1, config=my_config_new, backend='optuna', num_samples=1, cpus=1)\n", + "model.fit(dataset=dataset)\n", + "\n", + "# Unit test for situation: Ray with updated default config\n", + "my_config = AutoRMoK.get_default_config(h=12, n_series=1, backend='ray')\n", + "my_config['max_steps'] = 1\n", + "my_config['val_check_steps'] = 1\n", + "my_config['input_size'] = 12\n", + "my_config['learning_rate'] = 1e-1\n", + "model = AutoRMoK(h=12, n_series=1, config=my_config, backend='ray', num_samples=1, cpus=1)\n", + "model.fit(dataset=dataset)" + ] + }, { "attachments": {}, "cell_type": "markdown", diff --git a/nbs/models.rmok.ipynb b/nbs/models.rmok.ipynb new file mode 100644 index 000000000..08fee40c0 --- /dev/null +++ b/nbs/models.rmok.ipynb @@ -0,0 +1,650 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "%set_env PYTORCH_ENABLE_MPS_FALLBACK=1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp models.rmok" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Reversible Mixture of KAN - RMoK\n", + "The Reversible Mixture of KAN (RMoK) is a KAN-based model for time series forecasting which uses a mixture-of-experts structure to assign variables to different KAN experts, such as WaveKAN, TaylorKAN and JacobiKAN.\n", + "\n", + "**Reference**\n", + "- [Xiao Han, Xinfeng Zhang, Yiling Wu, Zhenduo Zhang, Zhe Wu.\"KAN4TSF: Are KAN and KAN-based models Effective for Time Series Forecasting?\"](https://arxiv.org/abs/2408.11306)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Figure 1. Architecture of RMoK.](imgs_models/rmok.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "from fastcore.test import test_eq\n", + "from nbdev.showdoc import show_doc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "import math\n", + "\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.nn.functional as F\n", + "\n", + "from neuralforecast.losses.pytorch import MAE\n", + "from neuralforecast.common._base_multivariate import BaseMultivariate" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Auxiliary functions\n", + "### 1.1 WaveKAN" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class WaveKANLayer(nn.Module):\n", + " '''This is a sample code for the simulations of the paper:\n", + " Bozorgasl, Zavareh and Chen, Hao, Wav-KAN: Wavelet Kolmogorov-Arnold Networks (May, 2024)\n", + "\n", + " https://arxiv.org/abs/2405.12832\n", + " and also available at:\n", + " https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4835325\n", + " We used efficient KAN notation and some part of the code:+\n", + "\n", + " '''\n", + "\n", + " def __init__(self, in_features, out_features, wavelet_type='mexican_hat', with_bn=True, device=\"cpu\"):\n", + " super(WaveKANLayer, self).__init__()\n", + " self.in_features = in_features\n", + " self.out_features = out_features\n", + " self.wavelet_type = wavelet_type\n", + " self.with_bn = with_bn\n", + "\n", + " # Parameters for wavelet transformation\n", + " self.scale = nn.Parameter(torch.ones(out_features, in_features))\n", + " self.translation = nn.Parameter(torch.zeros(out_features, in_features))\n", + "\n", + " # self.weight1 is not used; you may use it for weighting base activation and adding it like Spl-KAN paper\n", + " self.weight1 = nn.Parameter(torch.Tensor(out_features, in_features))\n", + " self.wavelet_weights = nn.Parameter(torch.Tensor(out_features, in_features))\n", + "\n", + " nn.init.kaiming_uniform_(self.wavelet_weights, a=math.sqrt(5))\n", + " nn.init.kaiming_uniform_(self.weight1, a=math.sqrt(5))\n", + "\n", + " # Base activation function #not used for this experiment\n", + " self.base_activation = nn.SiLU()\n", + "\n", + " # Batch normalization\n", + " if self.with_bn:\n", + " self.bn = nn.BatchNorm1d(out_features)\n", + "\n", + " def wavelet_transform(self, x):\n", + " if x.dim() == 2:\n", + " x_expanded = x.unsqueeze(1)\n", + " else:\n", + " x_expanded = x\n", + "\n", + " translation_expanded = self.translation.unsqueeze(0).expand(x.size(0), -1, -1)\n", + " scale_expanded = self.scale.unsqueeze(0).expand(x.size(0), -1, -1)\n", + " x_scaled = (x_expanded - translation_expanded) / scale_expanded\n", + "\n", + " # Implementation of different wavelet types\n", + " if self.wavelet_type == 'mexican_hat':\n", + " term1 = ((x_scaled ** 2) - 1)\n", + " term2 = torch.exp(-0.5 * x_scaled ** 2)\n", + " wavelet = (2 / (math.sqrt(3) * math.pi ** 0.25)) * term1 * term2\n", + " wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as(wavelet)\n", + " wavelet_output = wavelet_weighted.sum(dim=2)\n", + " elif self.wavelet_type == 'morlet':\n", + " omega0 = 5.0 # Central frequency\n", + " real = torch.cos(omega0 * x_scaled)\n", + " envelope = torch.exp(-0.5 * x_scaled ** 2)\n", + " wavelet = envelope * real\n", + " wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as(wavelet)\n", + " wavelet_output = wavelet_weighted.sum(dim=2)\n", + "\n", + " elif self.wavelet_type == 'dog':\n", + " # Implementing Derivative of Gaussian Wavelet\n", + " dog = -x_scaled * torch.exp(-0.5 * x_scaled ** 2)\n", + " wavelet = dog\n", + " wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as(wavelet)\n", + " wavelet_output = wavelet_weighted.sum(dim=2)\n", + " elif self.wavelet_type == 'meyer':\n", + " # Implement Meyer Wavelet here\n", + " # Constants for the Meyer wavelet transition boundaries\n", + " v = torch.abs(x_scaled)\n", + " pi = math.pi\n", + "\n", + " def meyer_aux(v):\n", + " return torch.where(v <= 1 / 2, torch.ones_like(v),\n", + " torch.where(v >= 1, torch.zeros_like(v), torch.cos(pi / 2 * nu(2 * v - 1))))\n", + "\n", + " def nu(t):\n", + " return t ** 4 * (35 - 84 * t + 70 * t ** 2 - 20 * t ** 3)\n", + "\n", + " # Meyer wavelet calculation using the auxiliary function\n", + " wavelet = torch.sin(pi * v) * meyer_aux(v)\n", + " wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as(wavelet)\n", + " wavelet_output = wavelet_weighted.sum(dim=2)\n", + " elif self.wavelet_type == 'shannon':\n", + " # Windowing the sinc function to limit its support\n", + " pi = math.pi\n", + " sinc = torch.sinc(x_scaled / pi) # sinc(x) = sin(pi*x) / (pi*x)\n", + "\n", + " # Applying a Hamming window to limit the infinite support of the sinc function\n", + " window = torch.hamming_window(x_scaled.size(-1), periodic=False, dtype=x_scaled.dtype,\n", + " device=x_scaled.device)\n", + " # Shannon wavelet is the product of the sinc function and the window\n", + " wavelet = sinc * window\n", + " wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as(wavelet)\n", + " wavelet_output = wavelet_weighted.sum(dim=2)\n", + " # You can try many more wavelet types ...\n", + " else:\n", + " raise ValueError(\"Unsupported wavelet type\")\n", + "\n", + " return wavelet_output\n", + "\n", + " def forward(self, x):\n", + " wavelet_output = self.wavelet_transform(x)\n", + " # You may like test the cases like Spl-KAN\n", + " # wav_output = F.linear(wavelet_output, self.weight)\n", + " # base_output = F.linear(self.base_activation(x), self.weight1)\n", + "\n", + " # base_output = F.linear(x, self.weight1)\n", + " combined_output = wavelet_output # + base_output\n", + "\n", + " # Apply batch normalization\n", + " if self.with_bn:\n", + " return self.bn(combined_output)\n", + " else:\n", + " return combined_output" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.2 TaylorKAN" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class TaylorKANLayer(nn.Module):\n", + " \"\"\"\n", + " https://github.com/Muyuzhierchengse/TaylorKAN/\n", + " \"\"\"\n", + "\n", + " def __init__(self, input_dim, out_dim, order, addbias=True):\n", + " super(TaylorKANLayer, self).__init__()\n", + " self.input_dim = input_dim\n", + " self.out_dim = out_dim\n", + " self.order = order\n", + " self.addbias = addbias\n", + "\n", + " self.coeffs = nn.Parameter(torch.randn(out_dim, input_dim, order) * 0.01)\n", + " if self.addbias:\n", + " self.bias = nn.Parameter(torch.zeros(1, out_dim))\n", + "\n", + " def forward(self, x):\n", + " shape = x.shape\n", + " outshape = shape[0:-1] + (self.out_dim,)\n", + " x = torch.reshape(x, (-1, self.input_dim))\n", + " x_expanded = x.unsqueeze(1).expand(-1, self.out_dim, -1)\n", + "\n", + " y = torch.zeros((x.shape[0], self.out_dim), device=x.device)\n", + "\n", + " for i in range(self.order):\n", + " term = (x_expanded ** i) * self.coeffs[:, :, i]\n", + " y += term.sum(dim=-1)\n", + "\n", + " if self.addbias:\n", + " y += self.bias\n", + "\n", + " y = torch.reshape(y, outshape)\n", + " return y" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.3. JacobiKAN" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class JacobiKANLayer(nn.Module):\n", + " \"\"\"\n", + " https://github.com/SpaceLearner/JacobiKAN/blob/main/JacobiKANLayer.py\n", + " \"\"\"\n", + "\n", + " def __init__(self, input_dim, output_dim, degree, a=1.0, b=1.0):\n", + " super(JacobiKANLayer, self).__init__()\n", + " self.inputdim = input_dim\n", + " self.outdim = output_dim\n", + " self.a = a\n", + " self.b = b\n", + " self.degree = degree\n", + "\n", + " self.jacobi_coeffs = nn.Parameter(torch.empty(input_dim, output_dim, degree + 1))\n", + "\n", + " nn.init.normal_(self.jacobi_coeffs, mean=0.0, std=1 / (input_dim * (degree + 1)))\n", + "\n", + " def forward(self, x):\n", + " x = torch.reshape(x, (-1, self.inputdim)) # shape = (batch_size, inputdim)\n", + " # Since Jacobian polynomial is defined in [-1, 1]\n", + " # We need to normalize x to [-1, 1] using tanh\n", + " x = torch.tanh(x)\n", + " # Initialize Jacobian polynomial tensors\n", + " jacobi = torch.ones(x.shape[0], self.inputdim, self.degree + 1, device=x.device)\n", + " if self.degree > 0: ## degree = 0: jacobi[:, :, 0] = 1 (already initialized) ; degree = 1: jacobi[:, :, 1] = x ; d\n", + " jacobi[:, :, 1] = ((self.a - self.b) + (self.a + self.b + 2) * x) / 2\n", + " for i in range(2, self.degree + 1):\n", + " theta_k = (2 * i + self.a + self.b) * (2 * i + self.a + self.b - 1) / (2 * i * (i + self.a + self.b))\n", + " theta_k1 = (2 * i + self.a + self.b - 1) * (self.a * self.a - self.b * self.b) / (\n", + " 2 * i * (i + self.a + self.b) * (2 * i + self.a + self.b - 2))\n", + " theta_k2 = (i + self.a - 1) * (i + self.b - 1) * (2 * i + self.a + self.b) / (\n", + " i * (i + self.a + self.b) * (2 * i + self.a + self.b - 2))\n", + " jacobi[:, :, i] = (theta_k * x + theta_k1) * jacobi[:, :, i - 1].clone() - theta_k2 * jacobi[:, :,\n", + " i - 2].clone() # 2 * x * jacobi[:, :, i - 1].clone() - jacobi[:, :, i - 2].clone()\n", + " # Compute the Jacobian interpolation\n", + " y = torch.einsum('bid,iod->bo', jacobi, self.jacobi_coeffs) # shape = (batch_size, outdim)\n", + " y = y.view(-1, self.outdim)\n", + " return y" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.4 RevIN" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class RevIN(nn.Module):\n", + " def __init__(self, num_features: int, eps=1e-5, affine=True):\n", + " \"\"\"\n", + " :param num_features: the number of features or channels\n", + " :param eps: a value added for numerical stability\n", + " :param affine: if True, RevIN has learnable affine parameters\n", + " \"\"\"\n", + " super(RevIN, self).__init__()\n", + "\n", + " self.num_features = num_features\n", + " self.eps = eps\n", + " self.affine = affine\n", + "\n", + " if self.affine:\n", + " self._init_params()\n", + "\n", + " def forward(self, x, mode: str):\n", + " if mode == 'norm':\n", + " self._get_statistics(x)\n", + " x = self._normalize(x)\n", + "\n", + " elif mode == 'denorm':\n", + " x = self._denormalize(x)\n", + "\n", + " else:\n", + " raise NotImplementedError\n", + "\n", + " return x\n", + "\n", + " def _init_params(self):\n", + " # initialize RevIN params: (C,)\n", + " self.affine_weight = nn.Parameter(torch.ones(self.num_features))\n", + " self.affine_bias = nn.Parameter(torch.zeros(self.num_features))\n", + "\n", + " def _get_statistics(self, x):\n", + " dim2reduce = tuple(range(1, x.ndim - 1))\n", + " self.mean = torch.mean(x, dim=dim2reduce, keepdim=True).detach()\n", + " self.stdev = torch.sqrt(torch.var(x, dim=dim2reduce, keepdim=True, unbiased=False) + self.eps).detach()\n", + "\n", + " def _normalize(self, x):\n", + " x = x - self.mean\n", + " x = x / self.stdev\n", + " if self.affine:\n", + " x = x * self.affine_weight\n", + " x = x + self.affine_bias\n", + "\n", + " return x\n", + "\n", + " def _denormalize(self, x):\n", + " if self.affine:\n", + " x = x - self.affine_bias\n", + " x = x / (self.affine_weight + self.eps * self.eps)\n", + " x = x * self.stdev\n", + " x = x + self.mean\n", + "\n", + " return x" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class RMoK(BaseMultivariate):\n", + " \"\"\" Reversible Mixture of KAN\n", + " **Parameters**
\n", + " `h`: int, Forecast horizon.
\n", + " `input_size`: int, autorregresive inputs size, y=[1,2,3,4] input_size=2 -> y_[t-2:t]=[1,2].
\n", + " `n_series`: int, number of time-series.
\n", + " `futr_exog_list`: str list, future exogenous columns.
\n", + " `hist_exog_list`: str list, historic exogenous columns.
\n", + " `stat_exog_list`: str list, static exogenous columns.
\n", + " `taylor_order`: int, order of the Taylor polynomial.
\n", + " `jacobi_degree`: int, degree of the Jacobi polynomial.
\n", + " `wavelet_function`: str, wavelet function to use in the WaveKAN. Choose from [\"mexican_hat\", \"morlet\", \"dog\", \"meyer\", \"shannon\"]
\n", + " `dropout`: float, dropout rate.
\n", + " `revin_affine`: bool=False, bool to use affine in RevIn.
\n", + " `loss`: PyTorch module, instantiated train loss class from [losses collection](https://nixtla.github.io/neuralforecast/losses.pytorch.html).
\n", + " `valid_loss`: PyTorch module=`loss`, instantiated valid loss class from [losses collection](https://nixtla.github.io/neuralforecast/losses.pytorch.html).
\n", + " `max_steps`: int=1000, maximum number of training steps.
\n", + " `learning_rate`: float=1e-3, Learning rate between (0, 1).
\n", + " `num_lr_decays`: int=-1, Number of learning rate decays, evenly distributed across max_steps.
\n", + " `early_stop_patience_steps`: int=-1, Number of validation iterations before early stopping.
\n", + " `val_check_steps`: int=100, Number of training steps between every validation loss check.
\n", + " `batch_size`: int=32, number of different series in each batch.
\n", + " `step_size`: int=1, step size between each window of temporal data.
\n", + " `scaler_type`: str='identity', type of scaler for temporal inputs normalization see [temporal scalers](https://nixtla.github.io/neuralforecast/common.scalers.html).
\n", + " `random_seed`: int=1, random_seed for pytorch initializer and numpy generators.
\n", + " `num_workers_loader`: int=os.cpu_count(), workers to be used by `TimeSeriesDataLoader`.
\n", + " `drop_last_loader`: bool=False, if True `TimeSeriesDataLoader` drops last non-full batch.
\n", + " `alias`: str, optional, Custom name of the model.
\n", + " `optimizer`: Subclass of 'torch.optim.Optimizer', optional, user specified optimizer instead of the default choice (Adam).
\n", + " `optimizer_kwargs`: dict, optional, list of parameters used by the user specified `optimizer`.
\n", + " `lr_scheduler`: Subclass of 'torch.optim.lr_scheduler.LRScheduler', optional, user specified lr_scheduler instead of the default choice (StepLR).
\n", + " `lr_scheduler_kwargs`: dict, optional, list of parameters used by the user specified `lr_scheduler`.
\n", + " `**trainer_kwargs`: int, keyword trainer arguments inherited from [PyTorch Lighning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer).
\n", + "\n", + " Reference
\n", + " [Xiao Han, Xinfeng Zhang, Yiling Wu, Zhenduo Zhang, Zhe Wu.\"KAN4TSF: Are KAN and KAN-based models Effective for Time Series Forecasting?\"](https://arxiv.org/abs/2408.11306)\n", + " \"\"\"\n", + "\n", + " # Class attributes\n", + " SAMPLING_TYPE = 'multivariate'\n", + " EXOGENOUS_FUTR = False\n", + " EXOGENOUS_HIST = False\n", + " EXOGENOUS_STAT = False\n", + "\n", + " def __init__(self,\n", + " h,\n", + " input_size,\n", + " n_series,\n", + " futr_exog_list = None,\n", + " hist_exog_list = None,\n", + " stat_exog_list = None,\n", + " taylor_order: int = 3,\n", + " jacobi_degree: int = 6,\n", + " wavelet_function: str = 'mexican_hat',\n", + " dropout: float = 0.1,\n", + " revine_affine: bool = True,\n", + " loss = MAE(),\n", + " valid_loss = None,\n", + " max_steps: int = 1000,\n", + " learning_rate: float = 1e-3,\n", + " num_lr_decays: int = -1,\n", + " early_stop_patience_steps: int =-1,\n", + " val_check_steps: int = 100,\n", + " batch_size: int = 32,\n", + " step_size: int = 1,\n", + " scaler_type: str = 'identity',\n", + " random_seed: int = 1,\n", + " num_workers_loader: int = 0,\n", + " drop_last_loader: bool = False,\n", + " optimizer = None,\n", + " optimizer_kwargs = None,\n", + " lr_scheduler = None,\n", + " lr_scheduler_kwargs = None, \n", + " **trainer_kwargs):\n", + " \n", + " super(RMoK, self).__init__(h=h,\n", + " input_size=input_size,\n", + " n_series=n_series,\n", + " stat_exog_list = None,\n", + " futr_exog_list = None,\n", + " hist_exog_list = None,\n", + " loss=loss,\n", + " valid_loss=valid_loss,\n", + " max_steps=max_steps,\n", + " learning_rate=learning_rate,\n", + " num_lr_decays=num_lr_decays,\n", + " early_stop_patience_steps=early_stop_patience_steps,\n", + " val_check_steps=val_check_steps,\n", + " batch_size=batch_size,\n", + " step_size=step_size,\n", + " scaler_type=scaler_type,\n", + " random_seed=random_seed,\n", + " num_workers_loader=num_workers_loader,\n", + " drop_last_loader=drop_last_loader,\n", + " optimizer=optimizer,\n", + " optimizer_kwargs=optimizer_kwargs,\n", + " lr_scheduler=lr_scheduler,\n", + " lr_scheduler_kwargs=lr_scheduler_kwargs,\n", + " **trainer_kwargs)\n", + " \n", + " self.input_size = input_size\n", + " self.h = h\n", + " self.n_series = n_series\n", + " self.dropout = nn.Dropout(dropout)\n", + " self.revin_affine = revine_affine\n", + "\n", + " self.taylor_order = taylor_order\n", + " self.jacobi_degree = jacobi_degree\n", + " self.wavelet_function = wavelet_function\n", + "\n", + " self.experts = nn.ModuleList([\n", + " TaylorKANLayer(self.input_size, self.h, order=self.taylor_order, addbias=True),\n", + " JacobiKANLayer(self.input_size, self.h, degree=self.jacobi_degree),\n", + " WaveKANLayer(self.input_size, self.h, wavelet_type=self.wavelet_function),\n", + " nn.Linear(self.input_size, self.h),\n", + " ])\n", + " \n", + " self.num_experts = len(self.experts)\n", + " self.gate = nn.Linear(self.input_size, self.num_experts)\n", + " self.softmax = nn.Softmax(dim=-1)\n", + " self.rev = RevIN(self.n_series, affine=self.revin_affine)\n", + "\n", + " def forward(self, windows_batch):\n", + " insample_y = windows_batch['insample_y']\n", + " B, L, N = insample_y.shape\n", + " x = self.rev(insample_y, 'norm') if self.rev else insample_y\n", + " x = self.dropout(x).transpose(1, 2).reshape(B * N, L)\n", + "\n", + " score = F.softmax(self.gate(x), dim=-1)\n", + " expert_outputs = torch.stack([self.experts[i](x) for i in range(self.num_experts)], dim=-1)\n", + "\n", + " y_pred = torch.einsum(\"BLE,BE->BL\", expert_outputs, score).reshape(B, N, -1).permute(0, 2, 1)\n", + " y_pred = self.rev(y_pred, 'denorm')\n", + " y_pred = self.loss.domain_map(y_pred)\n", + "\n", + " # domain_map might have squeezed the last dimension in case n_series == 1\n", + " if y_pred.ndim == 2:\n", + " return y_pred.unsqueeze(-1)\n", + " else:\n", + " return y_pred" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show_doc(RMoK)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show_doc(RMoK.fit, name='RMoK.fit')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show_doc(RMoK.predict, name='RMoK.predict')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Usage example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| eval: false\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from neuralforecast import NeuralForecast\n", + "from neuralforecast.models import RMoK\n", + "from neuralforecast.utils import AirPassengersPanel, AirPassengersStatic\n", + "from neuralforecast.losses.pytorch import MSE\n", + "\n", + "Y_train_df = AirPassengersPanel[AirPassengersPanel.ds=AirPassengersPanel['ds'].values[-12]].reset_index(drop=True) # 12 test\n", + "\n", + "model = RMoK(h=12,\n", + " input_size=24,\n", + " n_series=2,\n", + " taylor_order=3,\n", + " jacobi_degree=6,\n", + " wavelet_function='mexican_hat',\n", + " dropout=0.1,\n", + " revine_affine=True,\n", + " loss=MSE(),\n", + " valid_loss=MAE(),\n", + " early_stop_patience_steps=3,\n", + " batch_size=32)\n", + "\n", + "fcst = NeuralForecast(models=[model], freq='M')\n", + "fcst.fit(df=Y_train_df, static_df=AirPassengersStatic, val_size=12)\n", + "forecasts = fcst.predict(futr_df=Y_test_df)\n", + "\n", + "# Plot predictions\n", + "fig, ax = plt.subplots(1, 1, figsize = (20, 7))\n", + "Y_hat_df = forecasts.reset_index(drop=False).drop(columns=['unique_id','ds'])\n", + "plot_df = pd.concat([Y_test_df, Y_hat_df], axis=1)\n", + "plot_df = pd.concat([Y_train_df, plot_df])\n", + "\n", + "plot_df = plot_df[plot_df.unique_id=='Airline1'].drop('unique_id', axis=1)\n", + "plt.plot(plot_df['ds'], plot_df['y'], c='black', label='True')\n", + "plt.plot(plot_df['ds'], plot_df['RMoK'], c='blue', label='Forecast')\n", + "ax.set_title('AirPassengers Forecast', fontsize=22)\n", + "ax.set_ylabel('Monthly Passengers', fontsize=20)\n", + "ax.set_xlabel('Year', fontsize=20)\n", + "ax.legend(prop={'size': 15})\n", + "ax.grid()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/neuralforecast/_modidx.py b/neuralforecast/_modidx.py index a58491846..add079b58 100644 --- a/neuralforecast/_modidx.py +++ b/neuralforecast/_modidx.py @@ -96,6 +96,10 @@ 'neuralforecast/auto.py'), 'neuralforecast.auto.AutoPatchTST.get_default_config': ( 'models.html#autopatchtst.get_default_config', 'neuralforecast/auto.py'), + 'neuralforecast.auto.AutoRMoK': ('models.html#autormok', 'neuralforecast/auto.py'), + 'neuralforecast.auto.AutoRMoK.__init__': ('models.html#autormok.__init__', 'neuralforecast/auto.py'), + 'neuralforecast.auto.AutoRMoK.get_default_config': ( 'models.html#autormok.get_default_config', + 'neuralforecast/auto.py'), 'neuralforecast.auto.AutoRNN': ('models.html#autornn', 'neuralforecast/auto.py'), 'neuralforecast.auto.AutoRNN.__init__': ('models.html#autornn.__init__', 'neuralforecast/auto.py'), 'neuralforecast.auto.AutoRNN.get_default_config': ( 'models.html#autornn.get_default_config', @@ -1037,6 +1041,44 @@ 'neuralforecast/models/patchtst.py'), 'neuralforecast.models.patchtst.positional_encoding': ( 'models.patchtst.html#positional_encoding', 'neuralforecast/models/patchtst.py')}, + 'neuralforecast.models.rmok': { 'neuralforecast.models.rmok.JacobiKANLayer': ( 'models.rmok.html#jacobikanlayer', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.JacobiKANLayer.__init__': ( 'models.rmok.html#jacobikanlayer.__init__', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.JacobiKANLayer.forward': ( 'models.rmok.html#jacobikanlayer.forward', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.RMoK': ('models.rmok.html#rmok', 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.RMoK.__init__': ( 'models.rmok.html#rmok.__init__', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.RMoK.forward': ( 'models.rmok.html#rmok.forward', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.RevIN': ('models.rmok.html#revin', 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.RevIN.__init__': ( 'models.rmok.html#revin.__init__', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.RevIN._denormalize': ( 'models.rmok.html#revin._denormalize', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.RevIN._get_statistics': ( 'models.rmok.html#revin._get_statistics', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.RevIN._init_params': ( 'models.rmok.html#revin._init_params', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.RevIN._normalize': ( 'models.rmok.html#revin._normalize', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.RevIN.forward': ( 'models.rmok.html#revin.forward', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.TaylorKANLayer': ( 'models.rmok.html#taylorkanlayer', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.TaylorKANLayer.__init__': ( 'models.rmok.html#taylorkanlayer.__init__', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.TaylorKANLayer.forward': ( 'models.rmok.html#taylorkanlayer.forward', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.WaveKANLayer': ( 'models.rmok.html#wavekanlayer', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.WaveKANLayer.__init__': ( 'models.rmok.html#wavekanlayer.__init__', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.WaveKANLayer.forward': ( 'models.rmok.html#wavekanlayer.forward', + 'neuralforecast/models/rmok.py'), + 'neuralforecast.models.rmok.WaveKANLayer.wavelet_transform': ( 'models.rmok.html#wavekanlayer.wavelet_transform', + 'neuralforecast/models/rmok.py')}, 'neuralforecast.models.rnn': { 'neuralforecast.models.rnn.RNN': ('models.rnn.html#rnn', 'neuralforecast/models/rnn.py'), 'neuralforecast.models.rnn.RNN.__init__': ( 'models.rnn.html#rnn.__init__', 'neuralforecast/models/rnn.py'), diff --git a/neuralforecast/auto.py b/neuralforecast/auto.py index c69b406b1..b3c85892a 100644 --- a/neuralforecast/auto.py +++ b/neuralforecast/auto.py @@ -5,7 +5,7 @@ 'AutoNBEATSx', 'AutoNHITS', 'AutoDLinear', 'AutoNLinear', 'AutoTiDE', 'AutoDeepNPTS', 'AutoKAN', 'AutoTFT', 'AutoVanillaTransformer', 'AutoInformer', 'AutoAutoformer', 'AutoFEDformer', 'AutoPatchTST', 'AutoiTransformer', 'AutoTimesNet', 'AutoStemGNN', 'AutoHINT', 'AutoTSMixer', 'AutoTSMixerx', - 'AutoMLPMultivariate', 'AutoSOFTS', 'AutoTimeMixer'] + 'AutoMLPMultivariate', 'AutoSOFTS', 'AutoTimeMixer', 'AutoRMoK'] # %% ../nbs/models.ipynb 2 from os import cpu_count @@ -44,6 +44,7 @@ from .models.itransformer import iTransformer from .models.kan import KAN +from .models.rmok import RMoK from .models.stemgnn import StemGNN from .models.hint import HINT @@ -1108,7 +1109,7 @@ def get_default_config(cls, h, backend, n_series=None): return config -# %% ../nbs/models.ipynb 74 +# %% ../nbs/models.ipynb 75 class AutoKAN(BaseAuto): default_config = { @@ -1177,7 +1178,7 @@ def get_default_config(cls, h, backend, n_series=None): return config -# %% ../nbs/models.ipynb 79 +# %% ../nbs/models.ipynb 80 class AutoTFT(BaseAuto): default_config = { @@ -1245,7 +1246,7 @@ def get_default_config(cls, h, backend, n_series=None): return config -# %% ../nbs/models.ipynb 83 +# %% ../nbs/models.ipynb 84 class AutoVanillaTransformer(BaseAuto): default_config = { @@ -1313,7 +1314,7 @@ def get_default_config(cls, h, backend, n_series=None): return config -# %% ../nbs/models.ipynb 87 +# %% ../nbs/models.ipynb 88 class AutoInformer(BaseAuto): default_config = { @@ -1381,7 +1382,7 @@ def get_default_config(cls, h, backend, n_series=None): return config -# %% ../nbs/models.ipynb 91 +# %% ../nbs/models.ipynb 92 class AutoAutoformer(BaseAuto): default_config = { @@ -1449,7 +1450,7 @@ def get_default_config(cls, h, backend, n_series=None): return config -# %% ../nbs/models.ipynb 95 +# %% ../nbs/models.ipynb 96 class AutoFEDformer(BaseAuto): default_config = { @@ -1516,7 +1517,7 @@ def get_default_config(cls, h, backend, n_series=None): return config -# %% ../nbs/models.ipynb 99 +# %% ../nbs/models.ipynb 100 class AutoPatchTST(BaseAuto): default_config = { @@ -1586,7 +1587,7 @@ def get_default_config(cls, h, backend, n_series=None): return config -# %% ../nbs/models.ipynb 103 +# %% ../nbs/models.ipynb 104 class AutoiTransformer(BaseAuto): default_config = { @@ -1671,7 +1672,7 @@ def get_default_config(cls, h, backend, n_series): return config -# %% ../nbs/models.ipynb 108 +# %% ../nbs/models.ipynb 109 class AutoTimesNet(BaseAuto): default_config = { @@ -1739,7 +1740,7 @@ def get_default_config(cls, h, backend, n_series=None): return config -# %% ../nbs/models.ipynb 113 +# %% ../nbs/models.ipynb 114 class AutoStemGNN(BaseAuto): default_config = { @@ -1824,7 +1825,7 @@ def get_default_config(cls, h, backend, n_series): return config -# %% ../nbs/models.ipynb 117 +# %% ../nbs/models.ipynb 118 class AutoHINT(BaseAuto): def __init__( @@ -1896,7 +1897,7 @@ def _fit_model( def get_default_config(cls, h, backend, n_series=None): raise Exception("AutoHINT has no default configuration.") -# %% ../nbs/models.ipynb 122 +# %% ../nbs/models.ipynb 123 class AutoTSMixer(BaseAuto): default_config = { @@ -1982,7 +1983,7 @@ def get_default_config(cls, h, backend, n_series): return config -# %% ../nbs/models.ipynb 126 +# %% ../nbs/models.ipynb 127 class AutoTSMixerx(BaseAuto): default_config = { @@ -2068,7 +2069,7 @@ def get_default_config(cls, h, backend, n_series): return config -# %% ../nbs/models.ipynb 130 +# %% ../nbs/models.ipynb 131 class AutoMLPMultivariate(BaseAuto): default_config = { @@ -2153,7 +2154,7 @@ def get_default_config(cls, h, backend, n_series): return config -# %% ../nbs/models.ipynb 134 +# %% ../nbs/models.ipynb 135 class AutoSOFTS(BaseAuto): default_config = { @@ -2238,7 +2239,7 @@ def get_default_config(cls, h, backend, n_series): return config -# %% ../nbs/models.ipynb 138 +# %% ../nbs/models.ipynb 139 class AutoTimeMixer(BaseAuto): default_config = { @@ -2323,3 +2324,91 @@ def get_default_config(cls, h, backend, n_series): config = cls._ray_config_to_optuna(config) return config + +# %% ../nbs/models.ipynb 143 +class AutoRMoK(BaseAuto): + + default_config = { + "input_size_multiplier": [1, 2, 3, 4, 5], + "h": None, + "n_series": None, + "taylor_order": tune.choice([3, 4, 5]), + "jacobi_degree": tune.choice([4, 5, 6]), + "wavelet_function": tune.choice( + ["mexican_hat", "morlet", "dog", "meyer", "shannon"] + ), + "learning_rate": tune.loguniform(1e-4, 1e-1), + "scaler_type": tune.choice([None, "robust", "standard", "identity"]), + "max_steps": tune.choice([500, 1000, 2000]), + "batch_size": tune.choice([32, 64, 128, 256]), + "loss": None, + "random_seed": tune.randint(1, 20), + } + + def __init__( + self, + h, + n_series, + loss=MAE(), + valid_loss=None, + config=None, + search_alg=BasicVariantGenerator(random_state=1), + num_samples=10, + refit_with_val=False, + cpus=cpu_count(), + gpus=torch.cuda.device_count(), + verbose=False, + alias=None, + backend="ray", + callbacks=None, + ): + + # Define search space, input/output sizes + if config is None: + config = self.get_default_config(h=h, backend=backend, n_series=n_series) + + # Always use n_series from parameters, raise exception with Optuna because we can't enforce it + if backend == "ray": + config["n_series"] = n_series + elif backend == "optuna": + mock_trial = MockTrial() + if ( + "n_series" in config(mock_trial) + and config(mock_trial)["n_series"] != n_series + ) or ("n_series" not in config(mock_trial)): + raise Exception(f"config needs 'n_series': {n_series}") + + super(AutoRMoK, self).__init__( + cls_model=RMoK, + h=h, + loss=loss, + valid_loss=valid_loss, + config=config, + search_alg=search_alg, + num_samples=num_samples, + refit_with_val=refit_with_val, + cpus=cpus, + gpus=gpus, + verbose=verbose, + alias=alias, + backend=backend, + callbacks=callbacks, + ) + + @classmethod + def get_default_config(cls, h, backend, n_series): + config = cls.default_config.copy() + config["input_size"] = tune.choice( + [h * x for x in config["input_size_multiplier"]] + ) + + # Rolling windows with step_size=1 or step_size=h + # See `BaseWindows` and `BaseRNN`'s create_windows + config["step_size"] = tune.choice([1, h]) + del config["input_size_multiplier"] + if backend == "optuna": + # Always use n_series from parameters + config["n_series"] = n_series + config = cls._ray_config_to_optuna(config) + + return config diff --git a/neuralforecast/models/__init__.py b/neuralforecast/models/__init__.py index ee404e31c..414689631 100644 --- a/neuralforecast/models/__init__.py +++ b/neuralforecast/models/__init__.py @@ -2,7 +2,7 @@ 'MLP', 'NHITS', 'NBEATS', 'NBEATSx', 'DLinear', 'NLinear', 'TFT', 'VanillaTransformer', 'Informer', 'Autoformer', 'PatchTST', 'FEDformer', 'StemGNN', 'HINT', 'TimesNet', 'TimeLLM', 'TSMixer', 'TSMixerx', 'MLPMultivariate', - 'iTransformer', 'BiTCN', 'TiDE', 'DeepNPTS', 'SOFTS', 'TimeMixer', 'KAN' + 'iTransformer', 'BiTCN', 'TiDE', 'DeepNPTS', 'SOFTS', 'TimeMixer', 'KAN', 'RMoK', ] from .rnn import RNN @@ -37,3 +37,4 @@ from .softs import SOFTS from .timemixer import TimeMixer from .kan import KAN +from .rmok import RMoK diff --git a/neuralforecast/models/rmok.py b/neuralforecast/models/rmok.py new file mode 100644 index 000000000..c83585e1b --- /dev/null +++ b/neuralforecast/models/rmok.py @@ -0,0 +1,473 @@ +# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/models.rmok.ipynb. + +# %% auto 0 +__all__ = ['WaveKANLayer', 'TaylorKANLayer', 'JacobiKANLayer', 'RevIN', 'RMoK'] + +# %% ../../nbs/models.rmok.ipynb 6 +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from ..losses.pytorch import MAE +from ..common._base_multivariate import BaseMultivariate + +# %% ../../nbs/models.rmok.ipynb 8 +class WaveKANLayer(nn.Module): + """This is a sample code for the simulations of the paper: + Bozorgasl, Zavareh and Chen, Hao, Wav-KAN: Wavelet Kolmogorov-Arnold Networks (May, 2024) + + https://arxiv.org/abs/2405.12832 + and also available at: + https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4835325 + We used efficient KAN notation and some part of the code:+ + + """ + + def __init__( + self, + in_features, + out_features, + wavelet_type="mexican_hat", + with_bn=True, + device="cpu", + ): + super(WaveKANLayer, self).__init__() + self.in_features = in_features + self.out_features = out_features + self.wavelet_type = wavelet_type + self.with_bn = with_bn + + # Parameters for wavelet transformation + self.scale = nn.Parameter(torch.ones(out_features, in_features)) + self.translation = nn.Parameter(torch.zeros(out_features, in_features)) + + # self.weight1 is not used; you may use it for weighting base activation and adding it like Spl-KAN paper + self.weight1 = nn.Parameter(torch.Tensor(out_features, in_features)) + self.wavelet_weights = nn.Parameter(torch.Tensor(out_features, in_features)) + + nn.init.kaiming_uniform_(self.wavelet_weights, a=math.sqrt(5)) + nn.init.kaiming_uniform_(self.weight1, a=math.sqrt(5)) + + # Base activation function #not used for this experiment + self.base_activation = nn.SiLU() + + # Batch normalization + if self.with_bn: + self.bn = nn.BatchNorm1d(out_features) + + def wavelet_transform(self, x): + if x.dim() == 2: + x_expanded = x.unsqueeze(1) + else: + x_expanded = x + + translation_expanded = self.translation.unsqueeze(0).expand(x.size(0), -1, -1) + scale_expanded = self.scale.unsqueeze(0).expand(x.size(0), -1, -1) + x_scaled = (x_expanded - translation_expanded) / scale_expanded + + # Implementation of different wavelet types + if self.wavelet_type == "mexican_hat": + term1 = (x_scaled**2) - 1 + term2 = torch.exp(-0.5 * x_scaled**2) + wavelet = (2 / (math.sqrt(3) * math.pi**0.25)) * term1 * term2 + wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as( + wavelet + ) + wavelet_output = wavelet_weighted.sum(dim=2) + elif self.wavelet_type == "morlet": + omega0 = 5.0 # Central frequency + real = torch.cos(omega0 * x_scaled) + envelope = torch.exp(-0.5 * x_scaled**2) + wavelet = envelope * real + wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as( + wavelet + ) + wavelet_output = wavelet_weighted.sum(dim=2) + + elif self.wavelet_type == "dog": + # Implementing Derivative of Gaussian Wavelet + dog = -x_scaled * torch.exp(-0.5 * x_scaled**2) + wavelet = dog + wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as( + wavelet + ) + wavelet_output = wavelet_weighted.sum(dim=2) + elif self.wavelet_type == "meyer": + # Implement Meyer Wavelet here + # Constants for the Meyer wavelet transition boundaries + v = torch.abs(x_scaled) + pi = math.pi + + def meyer_aux(v): + return torch.where( + v <= 1 / 2, + torch.ones_like(v), + torch.where( + v >= 1, torch.zeros_like(v), torch.cos(pi / 2 * nu(2 * v - 1)) + ), + ) + + def nu(t): + return t**4 * (35 - 84 * t + 70 * t**2 - 20 * t**3) + + # Meyer wavelet calculation using the auxiliary function + wavelet = torch.sin(pi * v) * meyer_aux(v) + wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as( + wavelet + ) + wavelet_output = wavelet_weighted.sum(dim=2) + elif self.wavelet_type == "shannon": + # Windowing the sinc function to limit its support + pi = math.pi + sinc = torch.sinc(x_scaled / pi) # sinc(x) = sin(pi*x) / (pi*x) + + # Applying a Hamming window to limit the infinite support of the sinc function + window = torch.hamming_window( + x_scaled.size(-1), + periodic=False, + dtype=x_scaled.dtype, + device=x_scaled.device, + ) + # Shannon wavelet is the product of the sinc function and the window + wavelet = sinc * window + wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as( + wavelet + ) + wavelet_output = wavelet_weighted.sum(dim=2) + # You can try many more wavelet types ... + else: + raise ValueError("Unsupported wavelet type") + + return wavelet_output + + def forward(self, x): + wavelet_output = self.wavelet_transform(x) + # You may like test the cases like Spl-KAN + # wav_output = F.linear(wavelet_output, self.weight) + # base_output = F.linear(self.base_activation(x), self.weight1) + + # base_output = F.linear(x, self.weight1) + combined_output = wavelet_output # + base_output + + # Apply batch normalization + if self.with_bn: + return self.bn(combined_output) + else: + return combined_output + +# %% ../../nbs/models.rmok.ipynb 10 +class TaylorKANLayer(nn.Module): + """ + https://github.com/Muyuzhierchengse/TaylorKAN/ + """ + + def __init__(self, input_dim, out_dim, order, addbias=True): + super(TaylorKANLayer, self).__init__() + self.input_dim = input_dim + self.out_dim = out_dim + self.order = order + self.addbias = addbias + + self.coeffs = nn.Parameter(torch.randn(out_dim, input_dim, order) * 0.01) + if self.addbias: + self.bias = nn.Parameter(torch.zeros(1, out_dim)) + + def forward(self, x): + shape = x.shape + outshape = shape[0:-1] + (self.out_dim,) + x = torch.reshape(x, (-1, self.input_dim)) + x_expanded = x.unsqueeze(1).expand(-1, self.out_dim, -1) + + y = torch.zeros((x.shape[0], self.out_dim), device=x.device) + + for i in range(self.order): + term = (x_expanded**i) * self.coeffs[:, :, i] + y += term.sum(dim=-1) + + if self.addbias: + y += self.bias + + y = torch.reshape(y, outshape) + return y + +# %% ../../nbs/models.rmok.ipynb 12 +class JacobiKANLayer(nn.Module): + """ + https://github.com/SpaceLearner/JacobiKAN/blob/main/JacobiKANLayer.py + """ + + def __init__(self, input_dim, output_dim, degree, a=1.0, b=1.0): + super(JacobiKANLayer, self).__init__() + self.inputdim = input_dim + self.outdim = output_dim + self.a = a + self.b = b + self.degree = degree + + self.jacobi_coeffs = nn.Parameter( + torch.empty(input_dim, output_dim, degree + 1) + ) + + nn.init.normal_( + self.jacobi_coeffs, mean=0.0, std=1 / (input_dim * (degree + 1)) + ) + + def forward(self, x): + x = torch.reshape(x, (-1, self.inputdim)) # shape = (batch_size, inputdim) + # Since Jacobian polynomial is defined in [-1, 1] + # We need to normalize x to [-1, 1] using tanh + x = torch.tanh(x) + # Initialize Jacobian polynomial tensors + jacobi = torch.ones(x.shape[0], self.inputdim, self.degree + 1, device=x.device) + if ( + self.degree > 0 + ): ## degree = 0: jacobi[:, :, 0] = 1 (already initialized) ; degree = 1: jacobi[:, :, 1] = x ; d + jacobi[:, :, 1] = ((self.a - self.b) + (self.a + self.b + 2) * x) / 2 + for i in range(2, self.degree + 1): + theta_k = ( + (2 * i + self.a + self.b) + * (2 * i + self.a + self.b - 1) + / (2 * i * (i + self.a + self.b)) + ) + theta_k1 = ( + (2 * i + self.a + self.b - 1) + * (self.a * self.a - self.b * self.b) + / (2 * i * (i + self.a + self.b) * (2 * i + self.a + self.b - 2)) + ) + theta_k2 = ( + (i + self.a - 1) + * (i + self.b - 1) + * (2 * i + self.a + self.b) + / (i * (i + self.a + self.b) * (2 * i + self.a + self.b - 2)) + ) + jacobi[:, :, i] = (theta_k * x + theta_k1) * jacobi[ + :, :, i - 1 + ].clone() - theta_k2 * jacobi[ + :, :, i - 2 + ].clone() # 2 * x * jacobi[:, :, i - 1].clone() - jacobi[:, :, i - 2].clone() + # Compute the Jacobian interpolation + y = torch.einsum( + "bid,iod->bo", jacobi, self.jacobi_coeffs + ) # shape = (batch_size, outdim) + y = y.view(-1, self.outdim) + return y + +# %% ../../nbs/models.rmok.ipynb 14 +class RevIN(nn.Module): + def __init__(self, num_features: int, eps=1e-5, affine=True): + """ + :param num_features: the number of features or channels + :param eps: a value added for numerical stability + :param affine: if True, RevIN has learnable affine parameters + """ + super(RevIN, self).__init__() + + self.num_features = num_features + self.eps = eps + self.affine = affine + + if self.affine: + self._init_params() + + def forward(self, x, mode: str): + if mode == "norm": + self._get_statistics(x) + x = self._normalize(x) + + elif mode == "denorm": + x = self._denormalize(x) + + else: + raise NotImplementedError + + return x + + def _init_params(self): + # initialize RevIN params: (C,) + self.affine_weight = nn.Parameter(torch.ones(self.num_features)) + self.affine_bias = nn.Parameter(torch.zeros(self.num_features)) + + def _get_statistics(self, x): + dim2reduce = tuple(range(1, x.ndim - 1)) + self.mean = torch.mean(x, dim=dim2reduce, keepdim=True).detach() + self.stdev = torch.sqrt( + torch.var(x, dim=dim2reduce, keepdim=True, unbiased=False) + self.eps + ).detach() + + def _normalize(self, x): + x = x - self.mean + x = x / self.stdev + if self.affine: + x = x * self.affine_weight + x = x + self.affine_bias + + return x + + def _denormalize(self, x): + if self.affine: + x = x - self.affine_bias + x = x / (self.affine_weight + self.eps * self.eps) + x = x * self.stdev + x = x + self.mean + + return x + +# %% ../../nbs/models.rmok.ipynb 16 +class RMoK(BaseMultivariate): + """Reversible Mixture of KAN + **Parameters**
+ `h`: int, Forecast horizon.
+ `input_size`: int, autorregresive inputs size, y=[1,2,3,4] input_size=2 -> y_[t-2:t]=[1,2].
+ `n_series`: int, number of time-series.
+ `futr_exog_list`: str list, future exogenous columns.
+ `hist_exog_list`: str list, historic exogenous columns.
+ `stat_exog_list`: str list, static exogenous columns.
+ `taylor_order`: int, order of the Taylor polynomial.
+ `jacobi_degree`: int, degree of the Jacobi polynomial.
+ `wavelet_function`: str, wavelet function to use in the WaveKAN. Choose from ["mexican_hat", "morlet", "dog", "meyer", "shannon"]
+ `dropout`: float, dropout rate.
+ `revin_affine`: bool=False, bool to use affine in RevIn.
+ `loss`: PyTorch module, instantiated train loss class from [losses collection](https://nixtla.github.io/neuralforecast/losses.pytorch.html).
+ `valid_loss`: PyTorch module=`loss`, instantiated valid loss class from [losses collection](https://nixtla.github.io/neuralforecast/losses.pytorch.html).
+ `max_steps`: int=1000, maximum number of training steps.
+ `learning_rate`: float=1e-3, Learning rate between (0, 1).
+ `num_lr_decays`: int=-1, Number of learning rate decays, evenly distributed across max_steps.
+ `early_stop_patience_steps`: int=-1, Number of validation iterations before early stopping.
+ `val_check_steps`: int=100, Number of training steps between every validation loss check.
+ `batch_size`: int=32, number of different series in each batch.
+ `step_size`: int=1, step size between each window of temporal data.
+ `scaler_type`: str='identity', type of scaler for temporal inputs normalization see [temporal scalers](https://nixtla.github.io/neuralforecast/common.scalers.html).
+ `random_seed`: int=1, random_seed for pytorch initializer and numpy generators.
+ `num_workers_loader`: int=os.cpu_count(), workers to be used by `TimeSeriesDataLoader`.
+ `drop_last_loader`: bool=False, if True `TimeSeriesDataLoader` drops last non-full batch.
+ `alias`: str, optional, Custom name of the model.
+ `optimizer`: Subclass of 'torch.optim.Optimizer', optional, user specified optimizer instead of the default choice (Adam).
+ `optimizer_kwargs`: dict, optional, list of parameters used by the user specified `optimizer`.
+ `lr_scheduler`: Subclass of 'torch.optim.lr_scheduler.LRScheduler', optional, user specified lr_scheduler instead of the default choice (StepLR).
+ `lr_scheduler_kwargs`: dict, optional, list of parameters used by the user specified `lr_scheduler`.
+ `**trainer_kwargs`: int, keyword trainer arguments inherited from [PyTorch Lighning's trainer](https://pytorch-lightning.readthedocs.io/en/stable/api/pytorch_lightning.trainer.trainer.Trainer.html?highlight=trainer).
+ + Reference
+ [Xiao Han, Xinfeng Zhang, Yiling Wu, Zhenduo Zhang, Zhe Wu."KAN4TSF: Are KAN and KAN-based models Effective for Time Series Forecasting?"](https://arxiv.org/abs/2408.11306) + """ + + # Class attributes + SAMPLING_TYPE = "multivariate" + EXOGENOUS_FUTR = False + EXOGENOUS_HIST = False + EXOGENOUS_STAT = False + + def __init__( + self, + h, + input_size, + n_series, + futr_exog_list=None, + hist_exog_list=None, + stat_exog_list=None, + taylor_order: int = 3, + jacobi_degree: int = 6, + wavelet_function: str = "mexican_hat", + dropout: float = 0.1, + revine_affine: bool = True, + loss=MAE(), + valid_loss=None, + max_steps: int = 1000, + learning_rate: float = 1e-3, + num_lr_decays: int = -1, + early_stop_patience_steps: int = -1, + val_check_steps: int = 100, + batch_size: int = 32, + step_size: int = 1, + scaler_type: str = "identity", + random_seed: int = 1, + num_workers_loader: int = 0, + drop_last_loader: bool = False, + optimizer=None, + optimizer_kwargs=None, + lr_scheduler=None, + lr_scheduler_kwargs=None, + **trainer_kwargs + ): + + super(RMoK, self).__init__( + h=h, + input_size=input_size, + n_series=n_series, + stat_exog_list=None, + futr_exog_list=None, + hist_exog_list=None, + loss=loss, + valid_loss=valid_loss, + max_steps=max_steps, + learning_rate=learning_rate, + num_lr_decays=num_lr_decays, + early_stop_patience_steps=early_stop_patience_steps, + val_check_steps=val_check_steps, + batch_size=batch_size, + step_size=step_size, + scaler_type=scaler_type, + random_seed=random_seed, + num_workers_loader=num_workers_loader, + drop_last_loader=drop_last_loader, + optimizer=optimizer, + optimizer_kwargs=optimizer_kwargs, + lr_scheduler=lr_scheduler, + lr_scheduler_kwargs=lr_scheduler_kwargs, + **trainer_kwargs + ) + + self.input_size = input_size + self.h = h + self.n_series = n_series + self.dropout = nn.Dropout(dropout) + self.revin_affine = revine_affine + + self.taylor_order = taylor_order + self.jacobi_degree = jacobi_degree + self.wavelet_function = wavelet_function + + self.experts = nn.ModuleList( + [ + TaylorKANLayer( + self.input_size, self.h, order=self.taylor_order, addbias=True + ), + JacobiKANLayer(self.input_size, self.h, degree=self.jacobi_degree), + WaveKANLayer( + self.input_size, self.h, wavelet_type=self.wavelet_function + ), + nn.Linear(self.input_size, self.h), + ] + ) + + self.num_experts = len(self.experts) + self.gate = nn.Linear(self.input_size, self.num_experts) + self.softmax = nn.Softmax(dim=-1) + self.rev = RevIN(self.n_series, affine=self.revin_affine) + + def forward(self, windows_batch): + insample_y = windows_batch["insample_y"] + B, L, N = insample_y.shape + x = self.rev(insample_y, "norm") if self.rev else insample_y + x = self.dropout(x).transpose(1, 2).reshape(B * N, L) + + score = F.softmax(self.gate(x), dim=-1) + expert_outputs = torch.stack( + [self.experts[i](x) for i in range(self.num_experts)], dim=-1 + ) + + y_pred = ( + torch.einsum("BLE,BE->BL", expert_outputs, score) + .reshape(B, N, -1) + .permute(0, 2, 1) + ) + y_pred = self.rev(y_pred, "denorm") + y_pred = self.loss.domain_map(y_pred) + + # domain_map might have squeezed the last dimension in case n_series == 1 + if y_pred.ndim == 2: + return y_pred.unsqueeze(-1) + else: + return y_pred