Source code for gpflux.helpers

#
# Copyright (c) 2021 The GPflux Contributors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
r"""
This module contains helper functions for constructing :class:`~gpflow.kernels.MultioutputKernel`,
:class:`~gpflow.inducing_variables.MultioutputInducingVariables`,
:class:`~gpflow.mean_functions.MeanFunction`, and :class:`~gpflux.layers.GPLayer` objects.
"""

import inspect
import warnings
from dataclasses import fields
from typing import Any, List, Optional, Type, TypeVar, Union

import numpy as np

import gpflow
from gpflow import default_float
from gpflow.inducing_variables import (
    InducingPoints,
    SeparateIndependentInducingVariables,
    SharedIndependentInducingVariables,
)
from gpflow.kernels import SeparateIndependent, SharedIndependent
from gpflow.utilities import deepcopy

from gpflux.layers.gp_layer import GPLayer


[docs]def construct_basic_kernel( kernels: Union[gpflow.kernels.Kernel, List[gpflow.kernels.Kernel]], output_dim: Optional[int] = None, share_hyperparams: bool = False, ) -> gpflow.kernels.MultioutputKernel: r""" Construct a :class:`~gpflow.kernels.MultioutputKernel` to use in :class:`GPLayer`\ s. :param kernels: A single kernel or list of :class:`~gpflow.kernels.Kernel`\ s. - When a single kernel is passed, the same kernel is used for all outputs. Depending on ``share_hyperparams``, the hyperparameters will be shared across outputs. You must also specify ``output_dim``. - When a list of kernels is passed, each kernel in the list is used on a separate output dimension and a :class:`gpflow.kernels.SeparateIndependent` is returned. :param output_dim: The number of outputs. This is equal to the number of latent GPs in the :class:`GPLayer`. When only a single kernel is specified for ``kernels``, you must also specify ``output_dim``. When a list of kernels is specified for ``kernels``, we assume that ``len(kernels) == output_dim``, and ``output_dim`` is not required. :param share_hyperparams: If `True`, use the type of kernel and the same hyperparameters (variance and lengthscales) for the different outputs. Otherwise, the same type of kernel (Squared-Exponential, Matern12, and so on) is used for the different outputs, but the kernel can have different hyperparameter values for each. """ if isinstance(kernels, list): mo_kern = SeparateIndependent(kernels) elif not share_hyperparams: copies = [deepcopy(kernels) for _ in range(output_dim)] mo_kern = SeparateIndependent(copies) else: mo_kern = SharedIndependent(kernels, output_dim) return mo_kern
[docs]def construct_basic_inducing_variables( num_inducing: Union[int, List[int]], input_dim: int, output_dim: Optional[int] = None, share_variables: bool = False, z_init: Optional[np.ndarray] = None, ) -> gpflow.inducing_variables.MultioutputInducingVariables: r""" Construct a compatible :class:`~gpflow.inducing_variables.MultioutputInducingVariables` to use in :class:`GPLayer`\ s. :param num_inducing: The total number of inducing variables, ``M``. This parameter can be freely chosen by the user. General advice is to set it as high as possible, but smaller than the number of datapoints. The computational complexity of the layer is cubic in ``M``. If a list is passed, each element in the list specifies the number of inducing variables to use for each ``output_dim``. :param input_dim: The dimensionality of the input data (or features) ``X``. Typically, this corresponds to ``X.shape[-1]``. For :class:`~gpflow.inducing_variables.InducingPoints`, this specifies the dimensionality of ``Z``. :param output_dim: The dimensionality of the outputs (or targets) ``Y``. Typically, this corresponds to ``Y.shape[-1]`` or the number of latent GPs. The parameter is used to determine the number of inducing variable sets to create when a different set is used for each output. The parameter is redundant when ``num_inducing`` is a list, because the code assumes that ``len(num_inducing) == output_dim``. :param share_variables: If `True`, use the same inducing variables for different outputs. Otherwise, create a different set for each output. Set this parameter to `False` when ``num_inducing`` is a list, because otherwise the two arguments contradict each other. If you set this parameter to `True`, you must also specify ``output_dim``, because that is used to determine the number of inducing variable sets to create. :param z_init: Raw values to use to initialise :class:`gpflow.inducing_variables.InducingPoints`. If `None` (the default), values will be initialised from ``N(0, 1)``. The shape of ``z_init`` depends on the other input arguments. If a single set of inducing points is used for all outputs (that is, if ``share_variables`` is `True`), ``z_init`` should be rank two, with the dimensions ``[M, input_dim]``. If a different set of inducing points is used for the outputs (ithat is, if ``num_inducing`` is a list, or if ``share_variables`` is `False`), ``z_init`` should be a rank three tensor with the dimensions ``[output_dim, M, input_dim]``. """ if z_init is None: warnings.warn( "No `z_init` has been specified in `construct_basic_inducing_variables`. " "Default initialization using random normal N(0, 1) will be used." ) z_init_is_given = z_init is not None if isinstance(num_inducing, list): if output_dim is not None: # TODO: the following assert may clash with MixedMultiOutputFeatures # where the number of independent GPs can differ from the output # dimension assert output_dim == len(num_inducing) # pragma: no cover assert share_variables is False inducing_variables = [] for i, num_ind_var in enumerate(num_inducing): if z_init_is_given: assert len(z_init[i]) == num_ind_var z_init_i = z_init[i] else: z_init_i = np.random.randn(num_ind_var, input_dim).astype(dtype=default_float()) assert z_init_i.shape == (num_ind_var, input_dim) inducing_variables.append(InducingPoints(z_init_i)) return SeparateIndependentInducingVariables(inducing_variables) elif not share_variables: inducing_variables = [] for o in range(output_dim): if z_init_is_given: if z_init.shape != (output_dim, num_inducing, input_dim): raise ValueError( "When not sharing variables, z_init must have shape" "[output_dim, num_inducing, input_dim]" ) z_init_o = z_init[o] else: z_init_o = np.random.randn(num_inducing, input_dim).astype(dtype=default_float()) inducing_variables.append(InducingPoints(z_init_o)) return SeparateIndependentInducingVariables(inducing_variables) else: # TODO: should we assert output_dim is None ? z_init = ( z_init if z_init_is_given else np.random.randn(num_inducing, input_dim).astype(dtype=default_float()) ) shared_ip = InducingPoints(z_init) return SharedIndependentInducingVariables(shared_ip)
[docs]def construct_mean_function( X: np.ndarray, D_in: int, D_out: int ) -> gpflow.mean_functions.MeanFunction: """ Return :class:`gpflow.mean_functions.Identity` when ``D_in`` and ``D_out`` are equal. Otherwise, use the principal components of the inputs matrix ``X`` to build a :class:`~gpflow.mean_functions.Linear` mean function. .. note:: The returned mean function is set to be untrainable. To change this, use :meth:`gpflow.set_trainable`. :param X: A data array with the shape ``[N, D_in]`` used to determine the principal components to use to create a :class:`~gpflow.mean_functions.Linear` mean function when ``D_in != D_out``. :param D_in: The dimensionality of the input data (or features) ``X``. Typically, this corresponds to ``X.shape[-1]``. :param D_out: The dimensionality of the outputs (or targets) ``Y``. Typically, this corresponds to ``Y.shape[-1]`` or the number of latent GPs in the layer. """ assert X.shape[-1] == D_in if D_in == D_out: mean_function = gpflow.mean_functions.Identity() else: if D_in > D_out: _, _, V = np.linalg.svd(X, full_matrices=False) W = V[:D_out, :].T else: W = np.concatenate([np.eye(D_in), np.zeros((D_in, D_out - D_in))], axis=1) assert W.shape == (D_in, D_out) mean_function = gpflow.mean_functions.Linear(W) gpflow.set_trainable(mean_function, False) return mean_function
[docs]def construct_gp_layer( num_data: int, num_inducing: int, input_dim: int, output_dim: int, kernel_class: Type[gpflow.kernels.Stationary] = gpflow.kernels.SquaredExponential, z_init: Optional[np.ndarray] = None, name: Optional[str] = None, ) -> GPLayer: """ Builds a vanilla GP layer with a single kernel shared among all outputs, shared inducing point variables and zero mean function. :param num_data: total number of datapoints in the dataset, *N*. Typically corresponds to ``X.shape[0] == len(X)``. :param num_inducing: total number of inducing variables, *M*. This parameter can be freely chosen by the user. General advice is to pick it as high as possible, but smaller than *N*. The computational complexity of the layer is cubic in *M*. :param input_dim: dimensionality of the input data (or features) X. Typically, this corresponds to ``X.shape[-1]``. :param output_dim: The dimensionality of the outputs (or targets) ``Y``. Typically, this corresponds to ``Y.shape[-1]``. :param kernel_class: The kernel class used by the layer. This can be as simple as :class:`gpflow.kernels.SquaredExponential`, or more complex, for example, ``lambda **_: gpflow.kernels.Linear() + gpflow.kernels.Periodic()``. It will be passed a ``lengthscales`` keyword argument. :param z_init: The initial value for the inducing variable inputs. :param name: The name for the GP layer. """ lengthscale = float(input_dim) ** 0.5 base_kernel = kernel_class(lengthscales=np.full(input_dim, lengthscale)) kernel = construct_basic_kernel(base_kernel, output_dim=output_dim, share_hyperparams=True) inducing_variable = construct_basic_inducing_variables( num_inducing, input_dim, output_dim=output_dim, share_variables=True, z_init=z_init, ) gp_layer = GPLayer( kernel=kernel, inducing_variable=inducing_variable, num_data=num_data, mean_function=gpflow.mean_functions.Zero(), name=name, ) return gp_layer
T = TypeVar("T") # HACK to get mypy to pass, should be (dataclass: Type[T], ...) -> T: # mypy said: gpflux/helpers.py:271: error: Argument 1 to "fields" has incompatible type "Type[T]"; # expected "Union[DataclassInstance, Type[DataclassInstance]]" [arg-type]
[docs]def make_dataclass_from_class(dataclass: Any, instance: object, **updates: object) -> Any: """ Take a regular object ``instance`` with a superset of fields for a :class:`dataclasses.dataclass` (``@dataclass``-decorated class), and return an instance of the dataclass. The ``instance`` has all of the dataclass's fields but might also have more. ``key=value`` keyword arguments supersede the fields in ``instance``. """ dataclass_keys = [f.name for f in fields(dataclass)] field_dict = {k: v for k, v in inspect.getmembers(instance) if k in dataclass_keys} field_dict.update(updates) return dataclass(**field_dict) # type: ignore
[docs]def xavier_initialization_numpy(input_dim: int, output_dim: int) -> np.ndarray: r""" Generate initial weights for a neural network layer with the given input and output dimensionality using the Xavier Glorot normal initialiser. From: Glorot, Xavier, and Yoshua Bengio. "Understanding the difficulty of training deep feedforward neural networks." Proceedings of the thirteenth international conference on artificial intelligence and statistics. JMLR Workshop and Conference Proceedings, 2010. Draw samples from a normal distribution centred on :math:`0` with standard deviation :math:`\sqrt(2 / (\text{input_dim} + \text{output_dim}))`. """ return np.random.randn(input_dim, output_dim) * (2.0 / (input_dim + output_dim)) ** 0.5