diff --git a/docs/source/_ext/model_overview.py b/docs/source/_ext/model_overview.py new file mode 100644 index 000000000..b377e3555 --- /dev/null +++ b/docs/source/_ext/model_overview.py @@ -0,0 +1,180 @@ +""" +Sphinx extension: Auto-generate pytorch_forecasting model overview. + +This writes/overwrites docs/source/models.rst during the build, +listing all registry models with tags and links to API docs. +""" + +from __future__ import annotations + +import os + + +def _safe_import_all_objects(): + try: + # prefer public registry interface + from pytorch_forecasting._registry import all_objects # type: ignore + + return all_objects, None + except Exception as e: # pragma: no cover - defensive + return None, e + + +def _render_lines() -> list[str]: + all_objects, err = _safe_import_all_objects() + + lines: list[str] = [] + lines.append("Models") + lines.append("======") + lines.append("") + lines.append("(This page is auto-generated from the registry at build time.)") + lines.append("Do not edit manually.") + lines.append("") + + if all_objects is None: + lines.extend( + [ + ".. note::", + " Failed to import registry for model overview.", + f" Build-time error: ``{err}``", + "", + ] + ) + return lines + + try: + df = all_objects( + object_types=["forecaster_pytorch_v1", "forecaster_pytorch_v2"], + as_dataframe=True, + return_tags=[ + "object_type", + "info:name", + "authors", + "python_dependencies", + ], + return_names=True, + ) + except Exception as e: # pragma: no cover - defensive + lines.extend( + [ + ".. note::", + f" Registry query failed: ``{e}``", + "", + ] + ) + return lines + + if df is None or len(df) == 0: + lines.extend([".. note::", " No models found in registry.", ""]) + return lines + + # header + lines.append(".. list-table:: Available forecasting models") + lines.append(" :header-rows: 1") + lines.append(" :widths: 30 15 20 20 15") + lines.append("") + header_cols = [ + "Class Name", + "Estimator Type", + "Authors", + "Maintainers", + "Dependencies", + ] + lines.append(" * - " + "\n - ".join(header_cols)) + + # rows + for _, row in df.sort_values("names").iterrows(): + pkg_cls = row["objects"] + try: + model_cls = pkg_cls.get_model_cls() + qualname = f"{model_cls.__module__}.{model_cls.__name__}" + except Exception: + qualname = f"{pkg_cls.__module__}.{pkg_cls.__name__}" + + # Get object type (forecaster_pytorch_v1 or forecaster_pytorch_v2) + object_type = row.get("object_type", "") + if object_type == "forecaster_pytorch_v1": + estimator_type = "forecaster_v1" + elif object_type == "forecaster_pytorch_v2": + estimator_type = "forecaster_v2" + else: + estimator_type = object_type + + # Get authors from tags + authors = row.get("authors", []) + if isinstance(authors, list) and authors: + authors_str = ", ".join(authors) + else: + authors_str = "pytorch-forecasting developers" + + # No maintainers tag exists, so use authors as maintainers + maintainers_str = authors_str + + # Get dependencies from tags + dependencies = row.get("python_dependencies", []) + if isinstance(dependencies, list) and dependencies: + dependencies_str = ", ".join(dependencies) + else: + dependencies_str = "None" + + row_cells = [ + f":py:class:`~{qualname}`", + estimator_type, + authors_str, + maintainers_str, + dependencies_str, + ] + lines.append(" * - " + "\n - ".join(row_cells)) + + lines.append("") + return lines + + +def _is_safe_mode() -> bool: + """Return True if model overview generation is explicitly disabled. + + By default, generation runs in all environments. Set PF_SKIP_MODEL_OVERVIEW=1 to disable. + """ + if os.environ.get("PF_SKIP_MODEL_OVERVIEW", "").lower() in {"1", "true", "yes"}: + return True + return False + + +def _write_models_rst(app) -> None: + # confdir is docs/source + out_file = os.path.join(app.confdir, "models.rst") + try: + if _is_safe_mode(): + # minimal page on hosted builders to avoid heavy optional deps + lines = [ + "Models", + "======", + "", + "(Model overview generation is disabled in this build environment.)", + "Use a local build to view the full, registry-driven table.", + "", + ] + else: + lines = _render_lines() + except Exception as exc: # pragma: no cover - defensive + lines = [ + "Models", + "======", + "", + "(Model overview could not be generated due to a build-time error.)", + f"Error: ``{exc}``", + "", + ] + os.makedirs(os.path.dirname(out_file), exist_ok=True) + with open(out_file, "w", encoding="utf-8") as f: + f.write("\n".join(lines)) + + +def setup(app): + # generate as early as possible so Sphinx sees the written file during source discovery + app.connect("config-inited", _write_models_rst) + return { + "version": "1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/source/conf.py b/docs/source/conf.py index 0e58692e1..91d839648 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,11 +18,18 @@ from sphinx.application import Sphinx from sphinx.ext.autosummary import Autosummary from sphinx.pycode import ModuleAnalyzer +from sphinx.util import logging as sphinx_logging SOURCE_PATH = Path(os.path.dirname(__file__)) # noqa # docs source PROJECT_PATH = SOURCE_PATH.joinpath("../..") # noqa # project root sys.path.insert(0, str(PROJECT_PATH)) # noqa +sys.path.insert(0, os.path.abspath("../..")) + +# make the local _ext folder importable +_EXT_PATH = SOURCE_PATH.joinpath("_ext") +if str(_EXT_PATH) not in sys.path: + sys.path.insert(0, str(_EXT_PATH)) import pytorch_forecasting # isort:skip @@ -118,6 +125,9 @@ class ModuleAutoSummary(Autosummary): def get_items(self, names): new_names = [] for name in names: + # Skip if module doesn't exist in sys.modules + if name not in sys.modules: + continue mod = sys.modules[name] mod_items = getattr(mod, "__all__", mod.__dict__) for t in mod_items: @@ -137,6 +147,15 @@ def setup(app: Sphinx): app.connect("autodoc-skip-member", skip) app.add_directive("moduleautosummary", ModuleAutoSummary) app.add_js_file("https://buttons.github.io/buttons.js", **{"async": "async"}) + # load custom model overview generator if available + try: + if "model_overview" not in extensions: + extensions.append("model_overview") + except Exception as exc: + # avoid hard-failing docs builds; make the reason visible in Sphinx logs + sphinx_logging.getLogger(__name__).warning( + "model_overview extension not loaded: %s", exc + ) # extension configuration @@ -190,3 +209,6 @@ def setup(app: Sphinx): nbsphinx_execute = "never" # always nbsphinx_allow_errors = False # False nbsphinx_timeout = 600 # seconds + + +# (model overview generation moved to docs/source/_ext/model_overview.py) diff --git a/docs/source/models.rst b/docs/source/models.rst index cd9048b0a..d21db463c 100644 --- a/docs/source/models.rst +++ b/docs/source/models.rst @@ -1,166 +1,4 @@ Models ====== -.. _models: - -.. currentmodule:: pytorch_forecasting - -Model parameters very much depend on the dataset for which they are destined. - -PyTorch Forecasting provides a ``.from_dataset()`` method for each model that -takes a :py:class:`~data.timeseries.TimeSeriesDataSet` and additional parameters -that cannot directy derived from the dataset such as, e.g. ``learning_rate`` or ``hidden_size``. - -To tune models, `optuna `_ can be used. For example, tuning of the -:py:class:`~models.temporal_fusion_transformer.TemporalFusionTransformer` -is implemented by :py:func:`~models.temporal_fusion_transformer.tuning.optimize_hyperparameters` - -Selecting an architecture --------------------------- - -Criteria for selecting an architecture depend heavily on the use-case. There are multiple selection criteria -and you should take into account. Here is an overview over the pros and cons of the implemented models: - -.. csv-table:: Model comparison - :header: "Name", "Covariates", "Multiple targets", "Regression", "Classification", "Probabilistic", "Uncertainty", "Interactions between series", "Flexible history length", "Cold-start", "Required computational resources (1-5, 5=most)" - - :py:class:`~pytorch_forecasting.models.rnn.RecurrentNetwork`, "x", "x", "x", "", "", "", "", "x", "", 2 - :py:class:`~pytorch_forecasting.models.mlp.DecoderMLP`, "x", "x", "x", "x", "", "x", "", "x", "x", 1 - :py:class:`~pytorch_forecasting.models.nbeats.NBeats`, "", "", "x", "", "", "", "", "", "", 1 - :py:class:`~pytorch_forecasting.models.nhits.NHiTS`, "x", "x", "x", "", "", "", "", "", "", 1 - :py:class:`~pytorch_forecasting.models.deepar.DeepAR`, "x", "x", "x", "", "x", "x", "x [#deepvar]_ ", "x", "", 3 - :py:class:`~pytorch_forecasting.models.temporal_fusion_transformer.TemporalFusionTransformer`, "x", "x", "x", "x", "", "x", "", "x", "x", 4 - :py:class:`~pytorch_forecasting.models.tide.TiDEModel`, "x", "x", "x", "", "", "", "", "x", "", 3 - :py:class:`~pytorch_forecasting.models.xlstm.xLSTMTime`, "x", "x", "x", "", "", "", "", "x", "", 3 - -.. [#deepvar] Accounting for correlations using a multivariate loss function which converts the network into a DeepVAR model. - -Size and type of available data -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -One should particularly consider five criteria. - -Availability of covariates -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. _model-covariates: - -If you have covariates, that is variables in addition to the target variable itself that hold information -about the target, then your case will benefit from a model that can accomodate covariates. A model that -cannot use covariates is :py:class:`~pytorch_forecasting.models.nbeats.NBeats`. - -Length of timeseries -^^^^^^^^^^^^^^^^^^^^^^ - -The length of time series has a significant impact on which model will work well. Unfortunately, -most models are created and tested on very long timeseries while in practice short or a mix of short and long -timeseries are often encountered. A model that can leverage covariates well such as the -:py:class:`~pytorch_forecasting.models.temporal_fusion_transformer.TemporalFusionTransformer` -will typically perform better than other models on short timeseries. It is a significant step -from short timeseries to making cold-start predictions soley based on static covariates, i.e. -making predictions without observed history. For example, -this is only supported by the -:py:class:`~pytorch_forecasting.models.temporal_fusion_transformer.TemporalFusionTransformer` -but does not work tremendously well. - - -Number of timeseries and their relation to each other -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If your time series are related to each other (e.g. all sales of products of the same company), -a model that can learn relations between the timeseries can improve accuracy. -Not that only :ref:`models that can process covariates ` can -learn relationships between different timeseries. -If the timeseries denote different entities or exhibit very similar patterns accross the board, -a model such as :py:class:`~pytorch_forecasting.models.nbeats.NBeats` will not work as well. - -If you have only one or very few timeseries, -they should be very long in order for a deep learning approach to work well. Consider also -more traditional approaches. - -Type of prediction task -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Not every can do regression, classification or handle multiple targets. Some are exclusively -geared towards a single task. For example, :py:class:`~pytorch_forecasting.models.nbeats.NBeats` -can only be used for regression on a single target without covariates while the -:py:class:`~pytorch_forecasting.models.temporal_fusion_transformer.TemporalFusionTransformer` supports -multiple targets and even hetrogeneous targets where some are continuous variables and others categorical, -i.e. regression and classification at the same time. :py:class:`~pytorch_forecasting.models.deepar.DeepAR` -can handle multiple targets but only works for regression tasks. - -For long forecast horizon forecasts, :py:class:`~pytorch_forecasting.models.nhits.NHiTS` is an excellent choice -as it uses interpolation capabilities. - -Supporting uncertainty -~~~~~~~~~~~~~~~~~~~~~~~ - -Not all models support uncertainty estimation. Those that do, might do so in different fashions. -Non-parameteric models provide forecasts that are not bound to a given distribution -while parametric models assume that the data follows a specific distribution. - -The parametric models will be a better choice if you -know how your data (and potentially error) is distributed. However, if you are missing this information or -cannot make an educated guess that matches reality rather well, the model's uncertainty estimates will -be adversely impacted. In this case, a non-parameteric model will do much better. - -:py:class:`~pytorch_forecasting.models.deepar.DeepAR` is an example for a parameteric model while -the :py:class:`~pytorch_forecasting.models.temporal_fusion_transformer.TemporalFusionTransformer` -can output quantile forecasts that can fit any distribution. -Models based on normalizing flows marry the two worlds by providing a non-parameteric estimate -of a full probability distribution. PyTorch Forecasting currently does not provide -support for these but -`Pyro, a package for probabilistic programming `_ does -if you believe that your problem is uniquely suited to this solution. - -Computational requirements -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some models have simpler architectures and less parameters than others which can -lead to significantly different training times. However, this not a general rule as demonstrated -by Zhuohan et al. in `Train Large, Then Compress: Rethinking Model Size for Efficient Training and Inference of Transformers -`_. Because the data for a sample for timeseries models is often far samller than it -is for computer vision or language tasks, GPUs are often underused and increasing the width of models can be an effective way -to fully use a GPU. This can increase the speed of training while also improving accuracy. -The other path to pushing utilization of a GPU up is increasing the batch size. -However, increasing the batch size can adversly affect the generalization abilities of a trained network. -Also, take into account that often computational resources are mainly necessary for inference/prediction. The upfront task of training -a models will require developer time (also expensive!) but might be only a small part of the total compuational costs over -the lifetime of a model. - -The :py:class:`~pytorch_forecasting.models.temporal_fusion_transformer.TemporalFusionTransformer` is -a rather large model but might benefit from being trained with. -For example, :py:class:`~pytorch_forecasting.models.nbeats.NBeats` or :py:class:`~pytorch_forecasting.models.nhits.NHiTS` are -efficient models. -Autoregressive models such as :py:class:`~pytorch_forecasting.models.deepar.DeepAR` might be quick to train -but might be slow at inference time (in case of :py:class:`~pytorch_forecasting.models.deepar.DeepAR` this is -driven by sampling results probabilistically multiple times, effectively increasing the computational burden linearly with the -number of samples. - - -Implementing new architectures -------------------------------- - -Please see the :ref:`Using custom data and implementing custom models ` tutorial on how implement basic and more advanced models. - -Every model should inherit from a base model in :py:mod:`~pytorch_forecasting.models.base_model`. - -.. autoclass:: pytorch_forecasting.models.base_model.BaseModel - :noindex: - :members: __init__ - - - -Details and available models -------------------------------- - -See the API documentation for further details on available models: - -.. currentmodule:: pytorch_forecasting - -.. moduleautosummary:: - :toctree: api/ - :template: custom-module-template.rst - :recursive: - - pytorch_forecasting.models +*(This file is overwritten during the docs build by the `model_overview` extension. Do not edit manually.)*