sktime.forecasting.pykan_forecaster 源代码

# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
"""Dummy forecasters."""

__author__ = ["benheid"]

import pandas as pd

from sktime.forecasting.base import BaseForecaster
from sktime.split import temporal_train_test_split
from sktime.utils.dependencies import _check_soft_dependencies

if _check_soft_dependencies(["pykan", "torch"], severity="none"):
    import torch
    from torch.utils.data import Dataset
else:

    class Dataset:
        """Dummy class if torch is unavailable."""


if _check_soft_dependencies(["pykan", "torch"], severity="none"):
    from kan import KAN


[文档]class PyKANForecaster(BaseForecaster): """ PyKANForecaster uses Kolmogorov Arnold Network [1] to forecast time series data. This forecaster uses the pykan library to create a KAN model to forecast time series data. The model is trained on the training data and then used to forecast the future values of the time series. **Note** This forecaster is experimental and the used library for implementing the KANs may be exchanged in the future to a more stable and efficient library. Parameters ---------- hidden_layers : tuple, optional (default=(1, 1)) The number of hidden layers in the network. input_layer_size : int, optional (default=2) The size of the input layer. k : int, optional (default=3) The number of nearest neighbors to consider. grids : np.array, optional (default=np.array([2, 3])) The grid sizes to use in the model. model_params : dict, optional (default={"k": 2}) The parameters to pass to the model. See pykan documentation for more details. fit_params : dict, optional (default={"steps": 1}) The parameters to pass to the fit method. See pykan documentation for more details. val_size : float, optional (default=0.5) The size of the validation set to use in the training. device : str, optional (default="cpu") The device to use for training the model. References ---------- .. [1] Liu, Ziming, et al. "KAN: Kolmogorov-Arnold Networks." arXiv preprint arXiv:2404.19756 (2024). """ _tags = { # packaging info # -------------- "authors": ["benheid"], "maintainers": ["benheid"], "python_dependencies": ["pykan", "torch"], # estimator type # -------------- "y_inner_mtype": "pd.Series", "X_inner_mtype": "pd.DataFrame", "scitype:y": "univariate", "ignores-exogeneous-X": False, "requires-fh-in-fit": True, "X-y-must-have-same-index": True, "enforce_index_type": None, "handles-missing-data": False, "capability:pred_int": False, "capability:pred_int:insample": False, "capability:insample": False, } def __init__( self, hidden_layers=(1, 1), input_layer_size=2, grids=None, model_params=None, fit_params=None, val_size=0.5, device="cpu", ): self.hidden_layers = hidden_layers self.input_layer_size = input_layer_size self.grids = grids self._grids = grids if grids is not None else [2, 3] self.val_size = val_size self.model_params = model_params self._model_params = model_params if model_params is not None else {"k": 2} self.fit_params = fit_params self._fit_params = fit_params if fit_params is not None else {"steps": 1} self.device = device super().__init__() def _fit(self, y, X, fh): """Fit forecaster to training data. private _fit containing the core logic, called from fit Writes to self: Sets fitted model attributes ending in "_". Parameters ---------- y : sktime time series object guaranteed to be of an mtype in self.get_tag("y_inner_mtype") Time series to which to fit the forecaster. if self.get_tag("scitype:y")=="univariate": guaranteed to have a single column/variable if self.get_tag("scitype:y")=="multivariate": guaranteed to have 2 or more columns if self.get_tag("scitype:y")=="both": no restrictions apply fh : guaranteed to be ForecastingHorizon or None, optional (default=None) The forecasting horizon with the steps ahead to to predict. guaranteed to be passed in _predict X : sktime time series object, optional (default=None) guaranteed to be of an mtype in self.get_tag("X_inner_mtype") Exogeneous time series to fit to. Returns ------- self : reference to self """ output_size = max(fh.to_relative(self.cutoff)._values) if X is not None: y_train, y_test, X_train, X_test = temporal_train_test_split( y, X=X, test_size=(self.val_size) ) ds_train = PyTorchTrainDataset( y_train, self.input_layer_size, output_size, X=X_train ) ds_test = PyTorchTrainDataset( y_test, self.input_layer_size, output_size, X=X_test ) input_layer_size = self.input_layer_size + X_train.shape[1] * output_size else: y_train, y_test = temporal_train_test_split(y, test_size=(self.val_size)) ds_train = PyTorchTrainDataset(y_train, self.input_layer_size, output_size) ds_test = PyTorchTrainDataset(y_test, self.input_layer_size, output_size) input_layer_size = self.input_layer_size train_input = [ds_train[i][0] for i in range(len(ds_train))] train_target = [ds_train[i][1] for i in range(len(ds_train))] test_input = [ds_test[i][0] for i in range(len(ds_test))] test_target = [ds_test[i][1] for i in range(len(ds_test))] # no fitting, we already know the forecast values ds_new = { "train_input": torch.stack(train_input).type(torch.float32).to(self.device), "train_label": torch.stack(train_target) .type(torch.float32) .to(self.device), "test_input": torch.stack(test_input).type(torch.float32).to(self.device), "test_label": torch.stack(test_target).type(torch.float32).to(self.device), } self.train_losses = [] self.test_losses = [] self._layer_sizes = [input_layer_size, *self.hidden_layers, output_size] for i in range(len(self._grids)): if i == 0: model = KAN( width=self._layer_sizes, grid=self._grids[i], device=self.device, **self._model_params, ) if i != 0: model = KAN( width=self._layer_sizes, grid=self._grids[i], device=self.device, **self._model_params, ).initialize_from_another_model(model, ds_new["train_input"]) results = model.fit(ds_new, **self._fit_params) if len(self.test_losses) == 0 or results["test_loss"][-1] < min( self.test_losses ): self._state_dict = model.state_dict() self._best_grid = self._grids[i] self.train_losses += results["train_loss"] self.test_losses += results["test_loss"] # self.state_dict = best_model.state_dict() return self def _predict(self, fh, X): """Forecast time series at future horizon. private _predict containing the core logic, called from predict State required: Requires state to be "fitted". Accesses in self: Fitted model attributes ending in "_" self.cutoff Parameters ---------- fh : guaranteed to be ForecastingHorizon or None, optional (default=None) The forecasting horizon with the steps ahead to to predict. X : sktime time series object, optional (default=None) guaranteed to be of an mtype in self.get_tag("X_inner_mtype") Exogeneous time series for the forecast Returns ------- y_pred : sktime time series object should be of the same type as seen in _fit, as in "y_inner_mtype" tag Point predictions """ model = KAN(width=self._layer_sizes, grid=self._best_grid, **self._model_params) model.load_state_dict(self._state_dict) if X is not None: input_ = torch.cat( [ torch.from_numpy(self._y.values[-self.input_layer_size :]).reshape( (1, -1) ), torch.from_numpy(X.values).reshape((1, -1)), ], dim=-1, ).type(torch.float32) else: input_ = ( torch.from_numpy(self._y.values[-self.input_layer_size :]) .reshape((1, -1)) .type(torch.float32) ) prediction = model(input_).detach().numpy().reshape((-1,)) index = list(fh.to_absolute(self.cutoff)) return pd.Series( prediction[fh.to_relative(self._cutoff) - 1], index=index, name=self._y.name )
[文档] @classmethod def get_test_params(cls, parameter_set="default"): """Return testing parameter settings for the estimator. Parameters ---------- parameter_set : str, default="default" Name of the set of test parameters to return, for use in tests. If no special parameters are defined for a value, will return `"default"` set. There are currently no reserved values for forecasters. Returns ------- params : dict or list of dict, default = {} Parameters to create testing instances of the class Each dict are parameters to construct an "interesting" test instance, i.e., `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance. `create_test_instance` uses the first (or only) dictionary in `params` """ params = [ {}, # default parameters { "grids": [2, 3], "model_params": {"k": 2}, "fit_params": {"steps": 1}, "input_layer_size": 2, "hidden_layers": (1,), }, { "input_layer_size": 2, "grids": [3], "model_params": {"k": 2}, "fit_params": {"steps": 1}, "hidden_layers": (1, 1), }, ] return params
class PyTorchTrainDataset(Dataset): """Dataset for use in sktime deep learning forecasters.""" def __init__(self, y, seq_len, fh=None, X=None): self.y = y.values self.X = X.values if X is not None else X self.seq_len = seq_len self.fh = fh def __len__(self): """Return length of dataset.""" return max(len(self.y) - self.seq_len - self.fh + 1, 0) def __getitem__(self, i): """Return data point.""" from torch import from_numpy, tensor hist_y = tensor(self.y[i : i + self.seq_len]).type(torch.float32) if self.X is not None: exog_data = ( tensor(self.X[i + self.seq_len : i + self.seq_len + self.fh]) .type(torch.float32) .flatten() ) else: exog_data = tensor([]) return ( torch.cat([hist_y, exog_data]), from_numpy(self.y[i + self.seq_len : i + self.seq_len + self.fh]).type( torch.float32 ), )