From 0f9f1dadf07c4fb2887430ed30c8ed8372a6a3fc Mon Sep 17 00:00:00 2001 From: Bence Bagi Date: Mon, 29 Jul 2024 13:58:28 -0400 Subject: [PATCH 01/52] Add automatic batch and step size for softplus-Poisson GLMs optimized by SVRG --- src/nemos/glm.py | 32 ++++++++ src/nemos/solvers.py | 176 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 8688257f..f5d8b9c4 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -19,6 +19,10 @@ from .exceptions import NotFittedError from .pytrees import FeaturePytree from .regularizer import GroupLasso, Lasso, Regularizer, Ridge +from .solvers import ( + softplus_poisson_optimal_batch_and_stepsize, + softplus_poisson_optimal_stepsize, +) from .type_casting import jnp_asarray_if, support_pynapple from .typing import DESIGN_INPUT_TYPE @@ -887,6 +891,34 @@ def initialize_state( ) self.regularizer.mask = jnp.ones((1, data.shape[1])) + # optionally set auto stepsize and batch size if SVRG is used + if ( + "SVRG" in self.solver_name + and isinstance(self.observation_model, obs.PoissonObservations) + and self.observation_model.inverse_link_function == jax.nn.softplus + ): + batch_size = self.solver_kwargs.get("batch_size", None) + stepsize = self.solver_kwargs.get("stepsize", None) + # following jaxopt, stepsize <= 0 also means auto + if stepsize <= 0: + stepsize = None + + new_solver_kwargs = self.solver_kwargs.copy() + + # if both are None, determine them together + if batch_size is None and stepsize is None: + batch_size, stepsize = softplus_poisson_optimal_batch_and_stepsize( + data, y + ) + new_solver_kwargs["batch_size"] = batch_size + new_solver_kwargs["stepsize"] = stepsize + # if only batch size is given, we can still try to determine the optimal step size for it + elif batch_size is not None and stepsize is None: + stepsize = softplus_poisson_optimal_stepsize(data, y, batch_size) + new_solver_kwargs["stepsize"] = stepsize + + self.solver_kwargs = new_solver_kwargs + # set up the solver init/run/update attrs self.instantiate_solver() diff --git a/src/nemos/solvers.py b/src/nemos/solvers.py index 9ffbeab7..bee7d1d5 100644 --- a/src/nemos/solvers.py +++ b/src/nemos/solvers.py @@ -1,3 +1,4 @@ +import warnings from functools import partial from typing import Any, Callable, NamedTuple, Optional, Union @@ -748,3 +749,178 @@ def run( # substitute None for prox_lambda return self._run(init_params, init_state, None, *args) + + +def softplus_poisson_optimal_stepsize( + X: jnp.ndarray, y: jnp.ndarray, batch_size: int, n_power_iters: Optional[int] = None +): + """ + Calculate the optimal stepsize to use for SVRG with a GLM that uses + Poisson observations and softplus inverse link function. + + Parameters + ---------- + X : jnp.ndarray + Input data. + y : jnp.ndarray + Output data. + batch_size : int + Mini-batch size, i.e. number of data points sampled for + each inner update of SVRG. + n_power_iters: int, optional, default None + If None, build the XDX matrix (which has a shape of n_features x n_features) + and find its eigenvalues directly. + If an integer, it is the max number of iterations to run the power + iteration for when finding the largest eigenvalue. + + Returns + ------- + stepsize : scalar jax array + Optimal stepsize to use + """ + L_max, L = _softplus_poisson_L_max_and_L(jnp.array(X), jnp.array(y), n_power_iters) + + stepsize = _calc_alpha(batch_size, X.shape[0], L_max, L) + + return stepsize + + +# not using the previous one to avoid calculating L and L_max twice +def softplus_poisson_optimal_batch_and_stepsize( + X: jnp.ndarray, + y: jnp.ndarray, + n_power_iters: Optional[int] = None, + default_batch_size: int = 1, + default_stepsize: float = 1e-3, +): + """ + Calculate the optimal batch size and step size to use for SVRG with a GLM + that uses Poisson observations and softplus inverse link function. + + Parameters + ---------- + X : jnp.ndarray + Input data. + y : jnp.ndarray + Output data. + n_power_iters: int, optional, default None + If None, build the XDX matrix (which has a shape of n_features x n_features) + and find its eigenvalues directly. + If an integer, it is the max number of iterations to run the power + iteration for when finding the largest eigenvalue. + default_batch_size : int + Batch size to fall back on if the calculation fails. + default_stepsize: float + Step size to fall back on if the calculation fails. + + Returns + ------- + batch_size : int + Optimal batch size to use. + stepsize : scalar jax array + Optimal stepsize to use. + """ + L_max, L = _softplus_poisson_L_max_and_L(jnp.array(X), jnp.array(y), n_power_iters) + + batch_size = jnp.floor(_calc_b_hat(X.shape[0], L_max, L)) + + if not jnp.isfinite(batch_size): + batch_size = default_batch_size + stepsize = default_stepsize + + warnings.warn( + f"Could not determine batch and step size automatically. Falling back on the default values of {batch_size} and {default_stepsize}." + ) + else: + stepsize = _calc_alpha(batch_size, X.shape[0], L_max, L) + + return int(batch_size), stepsize + + +def _softplus_poisson_L_max_and_L( + X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None +): + L = _softplus_poisson_L(X, y, n_power_iters) + L_max = _softplus_poisson_L_max(X, y) + + return L_max, L + + +def _softplus_poisson_L_multiply(X, y, v): + N, _ = X.shape + + def body_fun(i, current_sum): + return current_sum + (0.17 * y[i] + 0.25) * jnp.outer(X[i, :], X[i, :]) @ v + + v_new = jax.lax.fori_loop(0, N, body_fun, v) + + return v_new / N + + +def _softplus_poisson_L_with_power_iteration(X, y, n_power_iters: int = 5): + # key is fixed to random.key(0) + _, d = X.shape + + # initialize to random d-dimensional vector + v = random.normal(jax.random.key(0), (d,)) + + # run the power iteration until convergence or the max steps + for _ in range(n_power_iters): + v_prev = v.copy() + v = _softplus_poisson_L_multiply(X, y, v) + + if jnp.allclose(v_prev, v): + break + + # calculate the eigenvalue + return jnp.linalg.norm(_softplus_poisson_L_multiply(X, y, v)) / jnp.linalg.norm(v) + + +def _softplus_poisson_XDX(X, y): + N, d = X.shape + + def body_fun(i, current_sum): + return current_sum + (0.17 * y[i] + 0.25) * jnp.outer(X[i, :], X[i, :]) + + # xi = jax.lax.dynamic_slice(X, (i, 0), (1, d)).reshape((d,)) + # yi = jax.lax.dynamic_slice(y, (i, 0), (1, 1)) + # return current_sum + (0.17 * yi + 0.25) * jnp.outer(xi, xi) + + # will be d x d + XDX = jax.lax.fori_loop(0, N, body_fun, jnp.zeros((d, d))) + + return XDX / N + + +def _softplus_poisson_L( + X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None +): + if n_power_iters is None: + # calculate XDX and its largest eigenvalue directly + return jnp.sort(jnp.linalg.eigvals(_softplus_poisson_XDX(X, y)).real)[-1] + else: + # use the power iteration to calculate the larget eigenvalue + return _softplus_poisson_L_with_power_iteration(X, y, n_power_iters) + + +def _softplus_poisson_L_max(X: jnp.ndarray, y: jnp.ndarray): + N, _ = X.shape + + def body_fun(i, current_max): + return jnp.maximum( + current_max, jnp.linalg.norm(X[i, :]) ** 2 * (0.17 * y[i] + 0.25) + ) + + L_max = jax.lax.fori_loop(0, N, body_fun, jnp.array([0.0])) + + return L_max[0] + + +def _calc_b_hat(N: int, L_max: float, L: float): + with jax.experimental.enable_x64(): + return jnp.sqrt(N / 2 * (3 * L_max - L) / (N * L - 3 * L_max)) + + +def _calc_alpha(b: int, N: int, L_max: float, L: float): + with jax.experimental.enable_x64(): + return 1 / 2 * b * (N - 1) / (3 * (N - b) * L_max + N * (b - 1) * L) From a88bc619f84e89773c42d03b54e1d402666f7bbf Mon Sep 17 00:00:00 2001 From: Bence Bagi Date: Thu, 8 Aug 2024 11:10:34 -0400 Subject: [PATCH 02/52] Add docstrings --- src/nemos/solvers.py | 139 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/nemos/solvers.py b/src/nemos/solvers.py index bee7d1d5..a54dd5f6 100644 --- a/src/nemos/solvers.py +++ b/src/nemos/solvers.py @@ -840,6 +840,26 @@ def softplus_poisson_optimal_batch_and_stepsize( def _softplus_poisson_L_max_and_L( X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None ): + """ + Calculate the smoothness constant and maximum smoothness constant for SVRG + assuming that the optimized function is the log-likelihood of a Poisson GLM + with a softplus inverse link function. + + Parameters + ---------- + X : + Input data. + y : + Output data. + n_power_iters : + If None, calculate X.T @ D @ X and its largest eigenvalue directly. + If an integer, the umber of power iterations to use to calculate the largest eigenvalue. + + Returns + ------- + L_max, L : + Maximum smoothness constant and smoothness constant. + """ L = _softplus_poisson_L(X, y, n_power_iters) L_max = _softplus_poisson_L_max(X, y) @@ -847,6 +867,23 @@ def _softplus_poisson_L_max_and_L( def _softplus_poisson_L_multiply(X, y, v): + """ + Perform the multiplication of v with X.T @ D @ X without forming the full X.T @ D @ X, + and iterating through the rows of X and y instead. + + Parameters + ---------- + X : + Input data. + y : + Output data. + v : + d-dimensionl vector. + + Returns + ------- + X.T @ D @ X @ v + """ N, _ = X.shape def body_fun(i, current_sum): @@ -858,6 +895,24 @@ def body_fun(i, current_sum): def _softplus_poisson_L_with_power_iteration(X, y, n_power_iters: int = 5): + """ + Instead of calculating X.T @ D @ X and its largest eigenvalue directly, + calculate it using the power method and by iterating through X and y, + forming a small product at a time. + + Parameters + ---------- + X : + Input data. + y : + Output data. + n_power_iters : + Number of power iterations. + + Returns + ------- + The largest eigenvalue of X.T @ D @ X + """ # key is fixed to random.key(0) _, d = X.shape @@ -877,6 +932,21 @@ def _softplus_poisson_L_with_power_iteration(X, y, n_power_iters: int = 5): def _softplus_poisson_XDX(X, y): + """ + Calculate the X.T @ D @ X matrix for use in calculating the smoothness constant L. + + Parameters + ---------- + X : + Input data. + y : + Output data. + + Returns + ------- + XDX : + d x d matrix + """ N, d = X.shape def body_fun(i, current_sum): @@ -895,6 +965,22 @@ def body_fun(i, current_sum): def _softplus_poisson_L( X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None ): + """ + Calculate the smoothness constant from data, assuming that the optimized + function is the log-likelihood of a Poisson GLM with a softplus inverse link function. + + Parameters + ---------- + X : + Input data. + y : + Output data. + + Returns + ------- + L : + Smoothness constant of f. + """ if n_power_iters is None: # calculate XDX and its largest eigenvalue directly return jnp.sort(jnp.linalg.eigvals(_softplus_poisson_XDX(X, y)).real)[-1] @@ -904,6 +990,23 @@ def _softplus_poisson_L( def _softplus_poisson_L_max(X: jnp.ndarray, y: jnp.ndarray): + """ + Calculate the maximum smoothness constant from data, assuming that + the optimized function is the log-likelihood of a Poisson GLM with + a softplus inverse link function. + + Parameters + ---------- + X : + Input data. + y : + Output data. + + Returns + ------- + L_max : + Maximum smoothness constant among f_{i}. + """ N, _ = X.shape def body_fun(i, current_max): @@ -917,10 +1020,46 @@ def body_fun(i, current_max): def _calc_b_hat(N: int, L_max: float, L: float): + """ + Calculate optimal batch size according to Sebbouh et al. 2019. + + Parameters + ---------- + N : + Overall number of data points. + L_max : + Maximum smoothness constant among f_{i}. + L : + Smoothness constant. + + Returns + ------- + b_hat : + Optimal batch size for the optimization. + """ with jax.experimental.enable_x64(): return jnp.sqrt(N / 2 * (3 * L_max - L) / (N * L - 3 * L_max)) def _calc_alpha(b: int, N: int, L_max: float, L: float): + """ + Calculate optimal step size according to Sebbouh et al. 2019. + + Parameters + ---------- + b : + Mini-batch size. + N : + Overall number of data points. + L_max : + Maximum smoothness constant among f_{i}. + L : + Smoothness constant. + + Returns + ------- + alpha : + Optimal step size for the optimization. + """ with jax.experimental.enable_x64(): return 1 / 2 * b * (N - 1) / (3 * (N - b) * L_max + N * (b - 1) * L) From a021cbbc3ad4c9a4a72cc044fdb0b3031a5429d8 Mon Sep 17 00:00:00 2001 From: Bence Bagi Date: Fri, 9 Aug 2024 14:23:12 -0400 Subject: [PATCH 03/52] Handle stepsize not being in solver_kwargs --- src/nemos/glm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index f5d8b9c4..e4c037ee 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -900,7 +900,7 @@ def initialize_state( batch_size = self.solver_kwargs.get("batch_size", None) stepsize = self.solver_kwargs.get("stepsize", None) # following jaxopt, stepsize <= 0 also means auto - if stepsize <= 0: + if stepsize is not None and stepsize <= 0: stepsize = None new_solver_kwargs = self.solver_kwargs.copy() From e606fc1e3811163412f6d0f191babc9bbd62e318 Mon Sep 17 00:00:00 2001 From: Bence Bagi Date: Fri, 9 Aug 2024 14:23:35 -0400 Subject: [PATCH 04/52] Add new way to calculate stepsize and also b_tilde --- src/nemos/solvers.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/nemos/solvers.py b/src/nemos/solvers.py index a54dd5f6..86da9e97 100644 --- a/src/nemos/solvers.py +++ b/src/nemos/solvers.py @@ -1041,7 +1041,7 @@ def _calc_b_hat(N: int, L_max: float, L: float): return jnp.sqrt(N / 2 * (3 * L_max - L) / (N * L - 3 * L_max)) -def _calc_alpha(b: int, N: int, L_max: float, L: float): +def _calc_alpha_old(b: int, N: int, L_max: float, L: float): """ Calculate optimal step size according to Sebbouh et al. 2019. @@ -1063,3 +1063,37 @@ def _calc_alpha(b: int, N: int, L_max: float, L: float): """ with jax.experimental.enable_x64(): return 1 / 2 * b * (N - 1) / (3 * (N - b) * L_max + N * (b - 1) * L) + + +def _calc_alpha(b: int, N: int, L_max: float, L: float): + """ + Calculate optimal step size. + + Parameters + ---------- + b : + Mini-batch size. + N : + Overall number of data points. + L_max : + Maximum smoothness constant among f_{i}. + L : + Smoothness constant. + + Returns + ------- + alpha : + Optimal step size for the optimization. + """ + with jax.experimental.enable_x64(): + L_b = L * N / b * (b - 1) / (N - 1) + L_max / b * (N - b) / (N - 1) + + return 1 / 4 / L_b + + +def _calc_b_tilde(L_max, L, N, mu): + with jax.experimental.enable_x64(): + numerator = (3 * L_max - L) * N + denominator = N * (N - 1) * mu - N * L + 3 * L_max + + return numerator / denominator From 95870f8af4f60230a1c636df34e12a934539c6a4 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 21 Aug 2024 19:01:44 +0200 Subject: [PATCH 05/52] started renaming vars --- src/nemos/solvers.py | 149 +++++++++++++++++++++++++++++++------------ 1 file changed, 108 insertions(+), 41 deletions(-) diff --git a/src/nemos/solvers.py b/src/nemos/solvers.py index 564092af..0b9153fc 100644 --- a/src/nemos/solvers.py +++ b/src/nemos/solvers.py @@ -1,6 +1,6 @@ import warnings -from functools import partial -from typing import Callable, NamedTuple, Optional, Union +from functools import partial, wraps +from typing import Callable, NamedTuple, Optional, Tuple, Union import jax import jax.flatten_util @@ -771,6 +771,17 @@ def run( return self._run(init_params, init_state, None, *args) +def _convert_to_float(func): + """Convert to float.""" + + @wraps + def wrapper(*args, **kwargs): + args, kwargs = jax.tree_util.tree_map(float, (args, kwargs)) + return func(*args, **kwargs) + + return wrapper + + def softplus_poisson_optimal_stepsize( X: jnp.ndarray, y: jnp.ndarray, batch_size: int, n_power_iters: Optional[int] = None ): @@ -800,7 +811,7 @@ def softplus_poisson_optimal_stepsize( """ L_max, L = _softplus_poisson_L_max_and_L(jnp.array(X), jnp.array(y), n_power_iters) - stepsize = _calc_alpha(batch_size, X.shape[0], L_max, L) + stepsize = _calculate_stepsize_saga(batch_size, X.shape[0], L_max, L) return stepsize @@ -840,9 +851,11 @@ def softplus_poisson_optimal_batch_and_stepsize( stepsize : scalar jax array Optimal stepsize to use. """ - L_max, L = _softplus_poisson_L_max_and_L(jnp.array(X), jnp.array(y), n_power_iters) + l_smooth_max, l_smooth = _softplus_poisson_L_max_and_L( + jnp.array(X), jnp.array(y), n_power_iters + ) - batch_size = jnp.floor(_calc_b_hat(X.shape[0], L_max, L)) + batch_size = int(jnp.floor(_calculate_batch_size_hat(X.shape[0], l_smooth_max, l_smooth))) if not jnp.isfinite(batch_size): batch_size = default_batch_size @@ -852,14 +865,14 @@ def softplus_poisson_optimal_batch_and_stepsize( f"Could not determine batch and step size automatically. Falling back on the default values of {batch_size} and {default_stepsize}." ) else: - stepsize = _calc_alpha(batch_size, X.shape[0], L_max, L) + stepsize = _calculate_stepsize_saga(batch_size, X.shape[0], l_smooth_max, l_smooth) return int(batch_size), stepsize def _softplus_poisson_L_max_and_L( X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None -): +) -> Tuple[float, float]: """ Calculate the smoothness constant and maximum smoothness constant for SVRG assuming that the optimized function is the log-likelihood of a Poisson GLM @@ -1039,81 +1052,135 @@ def body_fun(i, current_max): return L_max[0] -def _calc_b_hat(N: int, L_max: float, L: float): +@_convert_to_float +def _calculate_stepsize_svrg(batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float): """ - Calculate optimal batch size according to Sebbouh et al. 2019. + Calculate optimal step size for SVRG$^{[1]}$. Parameters ---------- - N : + batch_size : + Mini-batch size. + num_samples : Overall number of data points. - L_max : + l_smooth_max : Maximum smoothness constant among f_{i}. - L : + l_smooth : Smoothness constant. Returns ------- - b_hat : - Optimal batch size for the optimization. + : + Optimal step size for the optimization. + + References + ---------- + [1] Sebbouh, Othmane, et al. "Towards closing the gap between the theory and practice of SVRG." + Advances in neural information processing systems 32 (2019). """ - with jax.experimental.enable_x64(): - return jnp.sqrt(N / 2 * (3 * L_max - L) / (N * L - 3 * L_max)) + numerator = 0.5 * batch_size * (num_samples - 1) + denominator = (3 * (num_samples - batch_size) * l_smooth_max + num_samples * (batch_size - 1) * l_smooth) + return numerator / denominator -def _calc_alpha_old(b: int, N: int, L_max: float, L: float): +@_convert_to_float +def _calculate_stepsize_saga( + batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float +) -> float: """ - Calculate optimal step size according to Sebbouh et al. 2019. + Calculate optimal step size for SAGA. Parameters ---------- - b : + batch_size : Mini-batch size. - N : + num_samples : Overall number of data points. - L_max : + l_smooth_max : Maximum smoothness constant among f_{i}. - L : + l_smooth : Smoothness constant. Returns ------- - alpha : + : Optimal step size for the optimization. + + References + ---------- + [1] Gazagnadou, Nidham, Robert Gower, and Joseph Salmon. + "Optimal mini-batch and step sizes for saga." + International conference on machine learning. PMLR, 2019. """ - with jax.experimental.enable_x64(): - return 1 / 2 * b * (N - 1) / (3 * (N - b) * L_max + N * (b - 1) * L) + # convert any scalar (even if contained in jax.ndarray) to float + # this avoids issues with operation on different dtypes, that + # can be an issue on jax arrays. + batch_size, sample_size, l_smooth_max, l_smooth = map( + float, (batch_size, num_samples, l_smooth_max, l_smooth) + ) + + l_b = l_smooth * num_samples / batch_size * (batch_size - 1) / ( + num_samples - 1 + ) + l_smooth_max / batch_size * (num_samples - batch_size) / (num_samples - 1) + + return 0.25 / l_b -def _calc_alpha(b: int, N: int, L_max: float, L: float): +@_convert_to_float +def _calculate_batch_size_hat(num_samples: int, l_smooth_max: float, l_smooth: float): """ - Calculate optimal step size. + Calculate optimal batch size^{[1]}. Parameters ---------- - b : - Mini-batch size. - N : + num_samples : Overall number of data points. - L_max : + l_smooth_max : Maximum smoothness constant among f_{i}. - L : + l_smooth : Smoothness constant. Returns ------- - alpha : - Optimal step size for the optimization. + : + Optimal batch size for the optimization. + + References + ---------- + [1] Sebbouh, Othmane, et al. "Towards closing the gap between the theory and practice of SVRG." + Advances in neural information processing systems 32 (2019). """ - with jax.experimental.enable_x64(): - L_b = L * N / b * (b - 1) / (N - 1) + L_max / b * (N - b) / (N - 1) + numerator = num_samples / 2 * (3 * l_smooth_max - l_smooth) + denominator = num_samples * l_smooth - 3 * l_smooth_max + return jnp.sqrt(numerator / denominator) - return 1 / 4 / L_b +@_convert_to_float +def _calc_b_tilde(num_samples, l_smooth_max, l_smooth, mu): + """ + Calculate optimal batch size as in [1]. -def _calc_b_tilde(L_max, L, N, mu): - with jax.experimental.enable_x64(): - numerator = (3 * L_max - L) * N - denominator = N * (N - 1) * mu - N * L + 3 * L_max + Parameters + ---------- + num_samples : + Overall number of data points. + l_smooth_max : + Maximum smoothness constant among f_{i}. + l_smooth : + Smoothness constant. + mu : + Strong convexity constant. + + Returns + ------- + : + Optimal batch size for the optimization. + References + ---------- + [1] Sebbouh, Othmane, et al. "Towards closing the gap between the theory and practice of SVRG." + Advances in neural information processing systems 32 (2019). + """ + numerator = (3 * l_smooth_max - l_smooth) * num_samples + denominator = num_samples * (num_samples - 1) * mu - num_samples * l_smooth + 3 * l_smooth_max return numerator / denominator From 56618990012f3be9753bc21be6191a1fe8b9a47b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 22 Aug 2024 15:11:34 +0200 Subject: [PATCH 06/52] added ref to algorithm --- src/nemos/solvers.py | 93 +++++++++++++++----------------------------- 1 file changed, 31 insertions(+), 62 deletions(-) diff --git a/src/nemos/solvers.py b/src/nemos/solvers.py index 0b9153fc..95497af7 100644 --- a/src/nemos/solvers.py +++ b/src/nemos/solvers.py @@ -594,9 +594,10 @@ def _error(x, x_prev, stepsize): class SVRG(ProxSVRG): """ - SVRG solver + SVRG solver. - Equivalent to ProxSVRG with prox as the identity function and hyperparams_prox=None. + This solver implements "Algorithm 3" of [1]. Equivalent to ProxSVRG with prox as the identity + function and hyperparams_prox=None. Attributes ---------- @@ -783,11 +784,11 @@ def wrapper(*args, **kwargs): def softplus_poisson_optimal_stepsize( - X: jnp.ndarray, y: jnp.ndarray, batch_size: int, n_power_iters: Optional[int] = None + X: jnp.ndarray, y: jnp.ndarray, batch_size: int, n_power_iters: Optional[int] = 20 ): """ Calculate the optimal stepsize to use for SVRG with a GLM that uses - Poisson observations and softplus inverse link function. + Poisson observations and soft-plus inverse link function. Parameters ---------- @@ -809,9 +810,9 @@ def softplus_poisson_optimal_stepsize( stepsize : scalar jax array Optimal stepsize to use """ - L_max, L = _softplus_poisson_L_max_and_L(jnp.array(X), jnp.array(y), n_power_iters) + l_smooth_max, l_smooth = _softplus_poisson_L_max_and_L(jnp.array(X), jnp.array(y), n_power_iters) - stepsize = _calculate_stepsize_saga(batch_size, X.shape[0], L_max, L) + stepsize = _calculate_stepsize_svrg(batch_size, X.shape[0], l_smooth_max, l_smooth) return stepsize @@ -871,7 +872,7 @@ def softplus_poisson_optimal_batch_and_stepsize( def _softplus_poisson_L_max_and_L( - X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None + X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = 20 ) -> Tuple[float, float]: """ Calculate the smoothness constant and maximum smoothness constant for SVRG @@ -890,19 +891,20 @@ def _softplus_poisson_L_max_and_L( Returns ------- - L_max, L : + l_smooth_max, l_smooth : Maximum smoothness constant and smoothness constant. """ - L = _softplus_poisson_L(X, y, n_power_iters) - L_max = _softplus_poisson_L_max(X, y) + l_smooth = _softplus_poisson_L(X, y, n_power_iters) + l_smooth_max = _softplus_poisson_L_max(X, y) - return L_max, L + return l_smooth_max, l_smooth def _softplus_poisson_L_multiply(X, y, v): """ - Perform the multiplication of v with X.T @ D @ X without forming the full X.T @ D @ X, - and iterating through the rows of X and y instead. + Perform the multiplication of v with X.T @ D @ X without forming the full X.T @ D @ X. + + This assumes that X fits in memory. This estimate is based on calculating the hessian of the loss. Parameters ---------- @@ -911,23 +913,18 @@ def _softplus_poisson_L_multiply(X, y, v): y : Output data. v : - d-dimensionl vector. + d-dimensional vector. Returns ------- - X.T @ D @ X @ v + : + X.T @ D @ X @ v """ N, _ = X.shape - - def body_fun(i, current_sum): - return current_sum + (0.17 * y[i] + 0.25) * jnp.outer(X[i, :], X[i, :]) @ v - - v_new = jax.lax.fori_loop(0, N, body_fun, v) - - return v_new / N + return X.T.dot((0.17 * y + 0.25) * X.dot(v)) / N -def _softplus_poisson_L_with_power_iteration(X, y, n_power_iters: int = 5): +def _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters: int = 20): """ Instead of calculating X.T @ D @ X and its largest eigenvalue directly, calculate it using the power method and by iterating through X and y, @@ -949,50 +946,21 @@ def _softplus_poisson_L_with_power_iteration(X, y, n_power_iters: int = 5): # key is fixed to random.key(0) _, d = X.shape - # initialize to random d-dimensional vector - v = random.normal(jax.random.key(0), (d,)) + # initialize a random d-dimensional vector + v = jnp.ones((d, )) # run the power iteration until convergence or the max steps for _ in range(n_power_iters): v_prev = v.copy() v = _softplus_poisson_L_multiply(X, y, v) + v /= v.max() if jnp.allclose(v_prev, v): break # calculate the eigenvalue - return jnp.linalg.norm(_softplus_poisson_L_multiply(X, y, v)) / jnp.linalg.norm(v) - - -def _softplus_poisson_XDX(X, y): - """ - Calculate the X.T @ D @ X matrix for use in calculating the smoothness constant L. - - Parameters - ---------- - X : - Input data. - y : - Output data. - - Returns - ------- - XDX : - d x d matrix - """ - N, d = X.shape - - def body_fun(i, current_sum): - return current_sum + (0.17 * y[i] + 0.25) * jnp.outer(X[i, :], X[i, :]) - - # xi = jax.lax.dynamic_slice(X, (i, 0), (1, d)).reshape((d,)) - # yi = jax.lax.dynamic_slice(y, (i, 0), (1, 1)) - # return current_sum + (0.17 * yi + 0.25) * jnp.outer(xi, xi) - - # will be d x d - XDX = jax.lax.fori_loop(0, N, body_fun, jnp.zeros((d, d))) - - return XDX / N + v /= jnp.linalg.norm(v) + return _softplus_poisson_L_multiply(X, y, v).dot(v) def _softplus_poisson_L( @@ -1015,11 +983,12 @@ def _softplus_poisson_L( Smoothness constant of f. """ if n_power_iters is None: - # calculate XDX and its largest eigenvalue directly - return jnp.sort(jnp.linalg.eigvals(_softplus_poisson_XDX(X, y)).real)[-1] + # calculate XDX/n and its largest eigenvalue directly + XDX = X.T.dot((0.17 * y.reshape(y.shape[0], 1) + 0.25) * X) / y.shape[0] + return jnp.sort(jnp.linalg.eigvalsh(XDX))[-1] else: - # use the power iteration to calculate the larget eigenvalue - return _softplus_poisson_L_with_power_iteration(X, y, n_power_iters) + # use the power iteration to calculate the largest eigenvalue + return _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters) def _softplus_poisson_L_max(X: jnp.ndarray, y: jnp.ndarray): @@ -1047,7 +1016,7 @@ def body_fun(i, current_max): current_max, jnp.linalg.norm(X[i, :]) ** 2 * (0.17 * y[i] + 0.25) ) - L_max = jax.lax.fori_loop(0, N, body_fun, jnp.array([0.0])) + L_max = jax.lax.fori_loop(0, N, body_fun, jnp.array([-jnp.inf])) return L_max[0] From 481220dc1bf94edd91c1f317236a82c3943f2061 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 22 Aug 2024 17:49:07 +0200 Subject: [PATCH 07/52] renamed function and generalized lookup --- src/nemos/glm.py | 6 ++- src/nemos/solvers.py | 124 ++++++++++++++++++++++++++++++++----------- 2 files changed, 98 insertions(+), 32 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index e4c037ee..7b059941 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -20,7 +20,8 @@ from .pytrees import FeaturePytree from .regularizer import GroupLasso, Lasso, Regularizer, Ridge from .solvers import ( - softplus_poisson_optimal_batch_and_stepsize, + svrg_optimal_batch_and_stepsize, + _softplus_poisson_l_max_and_l, softplus_poisson_optimal_stepsize, ) from .type_casting import jnp_asarray_if, support_pynapple @@ -907,7 +908,8 @@ def initialize_state( # if both are None, determine them together if batch_size is None and stepsize is None: - batch_size, stepsize = softplus_poisson_optimal_batch_and_stepsize( + batch_size, stepsize = svrg_optimal_batch_and_stepsize( + _softplus_poisson_l_max_and_l, data, y ) new_solver_kwargs["batch_size"] = batch_size diff --git a/src/nemos/solvers.py b/src/nemos/solvers.py index 95497af7..950be008 100644 --- a/src/nemos/solvers.py +++ b/src/nemos/solvers.py @@ -1,6 +1,6 @@ import warnings from functools import partial, wraps -from typing import Callable, NamedTuple, Optional, Tuple, Union +from typing import Any, Callable, NamedTuple, Optional, Tuple, Union import jax import jax.flatten_util @@ -107,7 +107,7 @@ def __init__( self, fun: Callable, prox: Callable, - maxiter: int = 10_000, + maxiter: int = 1_000, key: Optional[KeyArrayLike] = None, stepsize: float = 1e-3, tol: float = 1e-3, @@ -640,7 +640,7 @@ class SVRG(ProxSVRG): def __init__( self, fun: Callable, - maxiter: int = 10_000, + maxiter: int = 1_000, key: Optional[KeyArrayLike] = None, stepsize: float = 1e-3, tol: float = 1e-3, @@ -775,7 +775,7 @@ def run( def _convert_to_float(func): """Convert to float.""" - @wraps + @wraps(func) def wrapper(*args, **kwargs): args, kwargs = jax.tree_util.tree_map(float, (args, kwargs)) return func(*args, **kwargs) @@ -810,7 +810,7 @@ def softplus_poisson_optimal_stepsize( stepsize : scalar jax array Optimal stepsize to use """ - l_smooth_max, l_smooth = _softplus_poisson_L_max_and_L(jnp.array(X), jnp.array(y), n_power_iters) + l_smooth_max, l_smooth = _softplus_poisson_l_max_and_l(jnp.array(X), jnp.array(y), n_power_iters) stepsize = _calculate_stepsize_svrg(batch_size, X.shape[0], l_smooth_max, l_smooth) @@ -818,12 +818,13 @@ def softplus_poisson_optimal_stepsize( # not using the previous one to avoid calculating L and L_max twice -def softplus_poisson_optimal_batch_and_stepsize( - X: jnp.ndarray, - y: jnp.ndarray, +def svrg_optimal_batch_and_stepsize( + compute_smoothness_constants: Callable, + *data: Any, n_power_iters: Optional[int] = None, default_batch_size: int = 1, default_stepsize: float = 1e-3, + strong_convexity: Optional[float] = None ): """ Calculate the optimal batch size and step size to use for SVRG with a GLM @@ -831,10 +832,10 @@ def softplus_poisson_optimal_batch_and_stepsize( Parameters ---------- - X : jnp.ndarray - Input data. - y : jnp.ndarray - Output data. + compute_smoothness_constants: + Function that computes l_smooth and l_smooth_max for the problem. + data : + The input data. For a GLM, X and y. n_power_iters: int, optional, default None If None, build the XDX matrix (which has a shape of n_features x n_features) and find its eigenvalues directly. @@ -851,12 +852,33 @@ def softplus_poisson_optimal_batch_and_stepsize( Optimal batch size to use. stepsize : scalar jax array Optimal stepsize to use. + + Examples + -------- + >>> import numpy as np + >>> from nemos.solvers import svrg_optimal_batch_and_stepsize as compute_opt_params + >>> from nemos.solvers import _softplus_poisson_l_max_and_l + >>> np.random.seed(123) + >>> X = np.random.normal(size=(500, 5)) + >>> y = np.random.poisson(np.exp(X.dot(np.ones(X.shape[1])))) + >>> batch_size, stepsize = compute_opt_params(_softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08) """ - l_smooth_max, l_smooth = _softplus_poisson_L_max_and_L( - jnp.array(X), jnp.array(y), n_power_iters - ) + data = jax.tree_util.tree_map(jnp.asarray, data) - batch_size = int(jnp.floor(_calculate_batch_size_hat(X.shape[0], l_smooth_max, l_smooth))) + num_samples = {dd.shape[0] for dd in jax.tree_util.tree_leaves(data)} + + if len(num_samples) != 1: + raise ValueError("Each array in data must have the same number of samples.") + num_samples = num_samples.pop() + + l_smooth_max, l_smooth = compute_smoothness_constants(*data, n_power_iters=n_power_iters) + + batch_size = _calculate_optimal_batch_size_svrg( + num_samples, + l_smooth_max, + l_smooth, + strong_convexity=strong_convexity + ) if not jnp.isfinite(batch_size): batch_size = default_batch_size @@ -866,13 +888,13 @@ def softplus_poisson_optimal_batch_and_stepsize( f"Could not determine batch and step size automatically. Falling back on the default values of {batch_size} and {default_stepsize}." ) else: - stepsize = _calculate_stepsize_saga(batch_size, X.shape[0], l_smooth_max, l_smooth) + stepsize = _calculate_stepsize_svrg(batch_size, num_samples, l_smooth_max, l_smooth) return int(batch_size), stepsize -def _softplus_poisson_L_max_and_L( - X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = 20 +def _softplus_poisson_l_max_and_l( + *data, n_power_iters: Optional[int] = 20 ) -> Tuple[float, float]: """ Calculate the smoothness constant and maximum smoothness constant for SVRG @@ -881,10 +903,8 @@ def _softplus_poisson_L_max_and_L( Parameters ---------- - X : - Input data. - y : - Output data. + data: + Tuple of X and y. n_power_iters : If None, calculate X.T @ D @ X and its largest eigenvalue directly. If an integer, the umber of power iterations to use to calculate the largest eigenvalue. @@ -894,9 +914,13 @@ def _softplus_poisson_L_max_and_L( l_smooth_max, l_smooth : Maximum smoothness constant and smoothness constant. """ + X, y = data + + # concatenate all data (if X is FeaturePytree) + X = jnp.hstack(jax.tree_util.tree_leaves(X)) + l_smooth = _softplus_poisson_L(X, y, n_power_iters) l_smooth_max = _softplus_poisson_L_max(X, y) - return l_smooth_max, l_smooth @@ -1095,10 +1119,50 @@ def _calculate_stepsize_saga( return 0.25 / l_b +def _calculate_optimal_batch_size_svrg(num_samples: int, l_smooth_max: float, l_smooth:float, strong_convexity: Optional[float] = None) -> int: + """ + Calculate the optimal batch size according to the table in [1]. + + Parameters + ---------- + num_samples: + The number of samples. + l_smooth_max: + The Lmax smoothness constant. + l_smooth: + The L smoothness constant. + strong_convexity: + The strong convexity constant. + + Returns + ------- + batch_size: + The batch size. + + """ + if strong_convexity is None: + batch_size = 1 # assume that N is large enough for mini-batching + else: + if num_samples >= 3 * l_smooth_max / strong_convexity: + batch_size = 1 + elif num_samples > l_smooth / strong_convexity: + b_tilde = _calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity) + if l_smooth_max < num_samples * l_smooth / 3: + b_hat = _calculate_b_hat(num_samples, l_smooth_max, l_smooth) + batch_size = int(jnp.floor(jnp.minimum(b_hat, b_tilde))) + else: + batch_size = b_tilde + else: + if l_smooth_max < num_samples * l_smooth / 3: + batch_size = int(jnp.floor(_calculate_b_hat(num_samples, l_smooth_max, l_smooth))) + else: + batch_size = num_samples + return batch_size + @_convert_to_float -def _calculate_batch_size_hat(num_samples: int, l_smooth_max: float, l_smooth: float): +def _calculate_b_hat(num_samples: int, l_smooth_max: float, l_smooth: float): """ - Calculate optimal batch size^{[1]}. + Helper function for calculating the optimal batch size^{[1]}. Parameters ---------- @@ -1125,9 +1189,9 @@ def _calculate_batch_size_hat(num_samples: int, l_smooth_max: float, l_smooth: f @_convert_to_float -def _calc_b_tilde(num_samples, l_smooth_max, l_smooth, mu): +def _calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity): """ - Calculate optimal batch size as in [1]. + Helper function to calculate the optimal batch size as in [1]. Parameters ---------- @@ -1137,7 +1201,7 @@ def _calc_b_tilde(num_samples, l_smooth_max, l_smooth, mu): Maximum smoothness constant among f_{i}. l_smooth : Smoothness constant. - mu : + strong_convexity : Strong convexity constant. Returns @@ -1151,5 +1215,5 @@ def _calc_b_tilde(num_samples, l_smooth_max, l_smooth, mu): Advances in neural information processing systems 32 (2019). """ numerator = (3 * l_smooth_max - l_smooth) * num_samples - denominator = num_samples * (num_samples - 1) * mu - num_samples * l_smooth + 3 * l_smooth_max + denominator = num_samples * (num_samples - 1) * strong_convexity - num_samples * l_smooth + 3 * l_smooth_max return numerator / denominator From 68c324067c4b5a776e557f85234aca3709887e4d Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 22 Aug 2024 18:19:52 +0200 Subject: [PATCH 08/52] added the table calculations --- src/nemos/solvers.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/nemos/solvers.py b/src/nemos/solvers.py index 950be008..96e3fd69 100644 --- a/src/nemos/solvers.py +++ b/src/nemos/solvers.py @@ -1105,12 +1105,6 @@ def _calculate_stepsize_saga( "Optimal mini-batch and step sizes for saga." International conference on machine learning. PMLR, 2019. """ - # convert any scalar (even if contained in jax.ndarray) to float - # this avoids issues with operation on different dtypes, that - # can be an issue on jax arrays. - batch_size, sample_size, l_smooth_max, l_smooth = map( - float, (batch_size, num_samples, l_smooth_max, l_smooth) - ) l_b = l_smooth * num_samples / batch_size * (batch_size - 1) / ( num_samples - 1 @@ -1121,7 +1115,7 @@ def _calculate_stepsize_saga( def _calculate_optimal_batch_size_svrg(num_samples: int, l_smooth_max: float, l_smooth:float, strong_convexity: Optional[float] = None) -> int: """ - Calculate the optimal batch size according to the table in [1]. + Calculate the optimal batch size according to "Table 1" in [1]. Parameters ---------- @@ -1141,8 +1135,13 @@ def _calculate_optimal_batch_size_svrg(num_samples: int, l_smooth_max: float, l_ """ if strong_convexity is None: - batch_size = 1 # assume that N is large enough for mini-batching + # Assume that num_sample is large enough for mini-batching. + # This is usually the case for neuroscience where num_sample + # is typically very large. + # If this assumption is not matched, convergence may be slow. + batch_size = 1 else: + # Compute optimal batch size according to Table 1. if num_samples >= 3 * l_smooth_max / strong_convexity: batch_size = 1 elif num_samples > l_smooth / strong_convexity: @@ -1151,18 +1150,19 @@ def _calculate_optimal_batch_size_svrg(num_samples: int, l_smooth_max: float, l_ b_hat = _calculate_b_hat(num_samples, l_smooth_max, l_smooth) batch_size = int(jnp.floor(jnp.minimum(b_hat, b_tilde))) else: - batch_size = b_tilde + batch_size = int(jnp.floor(b_tilde)) else: if l_smooth_max < num_samples * l_smooth / 3: batch_size = int(jnp.floor(_calculate_b_hat(num_samples, l_smooth_max, l_smooth))) else: - batch_size = num_samples + batch_size = int(num_samples) # reset this to int return batch_size + @_convert_to_float def _calculate_b_hat(num_samples: int, l_smooth_max: float, l_smooth: float): - """ - Helper function for calculating the optimal batch size^{[1]}. + r""" + Helper function for calculating $\hat{b}$ in "Table 1" of [1]. Parameters ---------- @@ -1190,8 +1190,8 @@ def _calculate_b_hat(num_samples: int, l_smooth_max: float, l_smooth: float): @_convert_to_float def _calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity): - """ - Helper function to calculate the optimal batch size as in [1]. + r""" + Helper function for calculating $\tilde{b}$ as in "Table 1" of [1]. Parameters ---------- From 730b789c49e302566290c977da3e9db4ce3dce3b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 23 Aug 2024 14:28:54 +0200 Subject: [PATCH 09/52] brought back maxiter to 10K. --- src/nemos/glm.py | 5 ++-- src/nemos/solvers.py | 59 ++++++++++++++++++++++++++++++-------------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 7b059941..2d690349 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -20,9 +20,9 @@ from .pytrees import FeaturePytree from .regularizer import GroupLasso, Lasso, Regularizer, Ridge from .solvers import ( - svrg_optimal_batch_and_stepsize, _softplus_poisson_l_max_and_l, softplus_poisson_optimal_stepsize, + svrg_optimal_batch_and_stepsize, ) from .type_casting import jnp_asarray_if, support_pynapple from .typing import DESIGN_INPUT_TYPE @@ -909,8 +909,7 @@ def initialize_state( # if both are None, determine them together if batch_size is None and stepsize is None: batch_size, stepsize = svrg_optimal_batch_and_stepsize( - _softplus_poisson_l_max_and_l, - data, y + _softplus_poisson_l_max_and_l, data, y ) new_solver_kwargs["batch_size"] = batch_size new_solver_kwargs["stepsize"] = stepsize diff --git a/src/nemos/solvers.py b/src/nemos/solvers.py index 96e3fd69..f2804c24 100644 --- a/src/nemos/solvers.py +++ b/src/nemos/solvers.py @@ -107,7 +107,7 @@ def __init__( self, fun: Callable, prox: Callable, - maxiter: int = 1_000, + maxiter: int = 10_000, key: Optional[KeyArrayLike] = None, stepsize: float = 1e-3, tol: float = 1e-3, @@ -640,7 +640,7 @@ class SVRG(ProxSVRG): def __init__( self, fun: Callable, - maxiter: int = 1_000, + maxiter: int = 10_000, key: Optional[KeyArrayLike] = None, stepsize: float = 1e-3, tol: float = 1e-3, @@ -810,7 +810,9 @@ def softplus_poisson_optimal_stepsize( stepsize : scalar jax array Optimal stepsize to use """ - l_smooth_max, l_smooth = _softplus_poisson_l_max_and_l(jnp.array(X), jnp.array(y), n_power_iters) + l_smooth_max, l_smooth = _softplus_poisson_l_max_and_l( + jnp.array(X), jnp.array(y), n_power_iters + ) stepsize = _calculate_stepsize_svrg(batch_size, X.shape[0], l_smooth_max, l_smooth) @@ -824,7 +826,7 @@ def svrg_optimal_batch_and_stepsize( n_power_iters: Optional[int] = None, default_batch_size: int = 1, default_stepsize: float = 1e-3, - strong_convexity: Optional[float] = None + strong_convexity: Optional[float] = None, ): """ Calculate the optimal batch size and step size to use for SVRG with a GLM @@ -871,13 +873,12 @@ def svrg_optimal_batch_and_stepsize( raise ValueError("Each array in data must have the same number of samples.") num_samples = num_samples.pop() - l_smooth_max, l_smooth = compute_smoothness_constants(*data, n_power_iters=n_power_iters) + l_smooth_max, l_smooth = compute_smoothness_constants( + *data, n_power_iters=n_power_iters + ) batch_size = _calculate_optimal_batch_size_svrg( - num_samples, - l_smooth_max, - l_smooth, - strong_convexity=strong_convexity + num_samples, l_smooth_max, l_smooth, strong_convexity=strong_convexity ) if not jnp.isfinite(batch_size): @@ -888,7 +889,9 @@ def svrg_optimal_batch_and_stepsize( f"Could not determine batch and step size automatically. Falling back on the default values of {batch_size} and {default_stepsize}." ) else: - stepsize = _calculate_stepsize_svrg(batch_size, num_samples, l_smooth_max, l_smooth) + stepsize = _calculate_stepsize_svrg( + batch_size, num_samples, l_smooth_max, l_smooth + ) return int(batch_size), stepsize @@ -971,7 +974,7 @@ def _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters: int = 2 _, d = X.shape # initialize a random d-dimensional vector - v = jnp.ones((d, )) + v = jnp.ones((d,)) # run the power iteration until convergence or the max steps for _ in range(n_power_iters): @@ -1046,7 +1049,9 @@ def body_fun(i, current_max): @_convert_to_float -def _calculate_stepsize_svrg(batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float): +def _calculate_stepsize_svrg( + batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float +): """ Calculate optimal step size for SVRG$^{[1]}$. @@ -1072,7 +1077,10 @@ def _calculate_stepsize_svrg(batch_size: int, num_samples: int, l_smooth_max: fl Advances in neural information processing systems 32 (2019). """ numerator = 0.5 * batch_size * (num_samples - 1) - denominator = (3 * (num_samples - batch_size) * l_smooth_max + num_samples * (batch_size - 1) * l_smooth) + denominator = ( + 3 * (num_samples - batch_size) * l_smooth_max + + num_samples * (batch_size - 1) * l_smooth + ) return numerator / denominator @@ -1107,13 +1115,18 @@ def _calculate_stepsize_saga( """ l_b = l_smooth * num_samples / batch_size * (batch_size - 1) / ( - num_samples - 1 - ) + l_smooth_max / batch_size * (num_samples - batch_size) / (num_samples - 1) + num_samples - 1 + ) + l_smooth_max / batch_size * (num_samples - batch_size) / (num_samples - 1) return 0.25 / l_b -def _calculate_optimal_batch_size_svrg(num_samples: int, l_smooth_max: float, l_smooth:float, strong_convexity: Optional[float] = None) -> int: +def _calculate_optimal_batch_size_svrg( + num_samples: int, + l_smooth_max: float, + l_smooth: float, + strong_convexity: Optional[float] = None, +) -> int: """ Calculate the optimal batch size according to "Table 1" in [1]. @@ -1145,7 +1158,9 @@ def _calculate_optimal_batch_size_svrg(num_samples: int, l_smooth_max: float, l_ if num_samples >= 3 * l_smooth_max / strong_convexity: batch_size = 1 elif num_samples > l_smooth / strong_convexity: - b_tilde = _calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity) + b_tilde = _calculate_b_tilde( + num_samples, l_smooth_max, l_smooth, strong_convexity + ) if l_smooth_max < num_samples * l_smooth / 3: b_hat = _calculate_b_hat(num_samples, l_smooth_max, l_smooth) batch_size = int(jnp.floor(jnp.minimum(b_hat, b_tilde))) @@ -1153,7 +1168,9 @@ def _calculate_optimal_batch_size_svrg(num_samples: int, l_smooth_max: float, l_ batch_size = int(jnp.floor(b_tilde)) else: if l_smooth_max < num_samples * l_smooth / 3: - batch_size = int(jnp.floor(_calculate_b_hat(num_samples, l_smooth_max, l_smooth))) + batch_size = int( + jnp.floor(_calculate_b_hat(num_samples, l_smooth_max, l_smooth)) + ) else: batch_size = int(num_samples) # reset this to int return batch_size @@ -1215,5 +1232,9 @@ def _calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity): Advances in neural information processing systems 32 (2019). """ numerator = (3 * l_smooth_max - l_smooth) * num_samples - denominator = num_samples * (num_samples - 1) * strong_convexity - num_samples * l_smooth + 3 * l_smooth_max + denominator = ( + num_samples * (num_samples - 1) * strong_convexity + - num_samples * l_smooth + + 3 * l_smooth_max + ) return numerator / denominator From d15ad8d0eaa1f8563b4ced371ca3132d000dc63d Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 23 Aug 2024 20:06:41 +0200 Subject: [PATCH 10/52] moved pieces around --- src/nemos/base_regressor.py | 29 +- src/nemos/glm.py | 117 +++-- src/nemos/solvers/__init__.py | 2 + src/nemos/{solvers.py => solvers/_svrg.py} | 473 +-------------------- src/nemos/solvers/_svrg_defaults.py | 454 ++++++++++++++++++++ tests/test_solvers.py | 2 +- 6 files changed, 572 insertions(+), 505 deletions(-) create mode 100644 src/nemos/solvers/__init__.py rename src/nemos/{solvers.py => solvers/_svrg.py} (67%) create mode 100644 src/nemos/solvers/_svrg_defaults.py diff --git a/src/nemos/base_regressor.py b/src/nemos/base_regressor.py index 5f651313..51859fd7 100644 --- a/src/nemos/base_regressor.py +++ b/src/nemos/base_regressor.py @@ -247,7 +247,7 @@ def _check_solver_kwargs(solver_class, solver_kwargs): f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for {solver_class.__name__}!" ) - def instantiate_solver(self, *args) -> BaseRegressor: + def instantiate_solver(self, *args, solver_kwargs: Optional[dict] = None) -> BaseRegressor: """ Instantiate the solver with the provided loss function. @@ -266,6 +266,9 @@ def instantiate_solver(self, *args) -> BaseRegressor: *args: Positional arguments for the jaxopt `solver.run` method, e.g. the regularizing strength for proximal gradient methods. + solver_kwargs: + Optional dictionary with the solver kwargs. + If nothing is provided, it defaults to self.solver_kwargs. Returns ------- @@ -290,8 +293,9 @@ def instantiate_solver(self, *args) -> BaseRegressor: else: loss = self._predict_and_compute_loss - # copy dictionary of kwargs to avoid modifying user settings - solver_kwargs = deepcopy(self.solver_kwargs) + if solver_kwargs is None: + # copy dictionary of kwargs to avoid modifying user settings + solver_kwargs = deepcopy(self.solver_kwargs) # check that the loss is Callable utils.assert_is_callable(loss, "loss") @@ -577,3 +581,22 @@ def _get_solver_class(solver_name: str): ) return solver_class + + def optimize_solver_params(self, X, y): + """ + Compute solver optimal defaults if available. + + Parameters + ---------- + X: + Input predictions. + y: + Output observations. + + Returns + ------- + : + A dictionary with the optimal defaults. + + """ + return self.solver_kwargs diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 2d690349..402f3146 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -19,9 +19,8 @@ from .exceptions import NotFittedError from .pytrees import FeaturePytree from .regularizer import GroupLasso, Lasso, Regularizer, Ridge -from .solvers import ( - _softplus_poisson_l_max_and_l, - softplus_poisson_optimal_stepsize, +from .solvers._svrg_defaults import ( + softplus_poisson_l_max_and_l, svrg_optimal_batch_and_stepsize, ) from .type_casting import jnp_asarray_if, support_pynapple @@ -30,6 +29,55 @@ ModelParams = Tuple[jnp.ndarray, jnp.ndarray] +_OPTIMAL_CONFIGURATIONS = { + "SVRG": [ + { + "required_params": { + "observation_model__inverse_link_function": lambda x: x == jax.nn.softplus, + "observation_model": lambda x: isinstance(x, obs.PoissonObservations), + "regularizer": lambda x: not isinstance(x, Ridge) + }, + "compute_l_smooth": softplus_poisson_l_max_and_l, + "compute_defaults": svrg_optimal_batch_and_stepsize, + "strong_convexity": None + }, + { + "required_params": { + "observation_model__inverse_link_function": lambda x: x == jax.nn.softplus, + "observation_model": lambda x: isinstance(x, obs.PoissonObservations), + "regularizer": lambda x: isinstance(x, Ridge) + }, + "compute_l_smooth": softplus_poisson_l_max_and_l, + "compute_defaults": svrg_optimal_batch_and_stepsize, + "strong_convexity": "regularizer_strength" + } + ], + "ProxSVRG": [ + { + "required_params": { + "observation_model__inverse_link_function": lambda x: x == jax.nn.softplus, + "observation_model": lambda x: isinstance(x, obs.PoissonObservations), + "regularizer": lambda x: not isinstance(x, Ridge) + }, + "compute_l_smooth": softplus_poisson_l_max_and_l, + "compute_defaults": svrg_optimal_batch_and_stepsize, + "strong_convexity": None + }, + { + "required_params": { + "observation_model__inverse_link_function": lambda x: x == jax.nn.softplus, + "observation_model": lambda x: isinstance(x, obs.PoissonObservations), + "regularizer": lambda x: isinstance(x, Ridge) + }, + "compute_l_smooth": softplus_poisson_l_max_and_l, + "compute_defaults": svrg_optimal_batch_and_stepsize, + "strong_convexity": "regularizer_strength" + } + + ], +} + + def cast_to_jax(func): """Cast argument to jax.""" @@ -892,36 +940,10 @@ def initialize_state( ) self.regularizer.mask = jnp.ones((1, data.shape[1])) - # optionally set auto stepsize and batch size if SVRG is used - if ( - "SVRG" in self.solver_name - and isinstance(self.observation_model, obs.PoissonObservations) - and self.observation_model.inverse_link_function == jax.nn.softplus - ): - batch_size = self.solver_kwargs.get("batch_size", None) - stepsize = self.solver_kwargs.get("stepsize", None) - # following jaxopt, stepsize <= 0 also means auto - if stepsize is not None and stepsize <= 0: - stepsize = None - - new_solver_kwargs = self.solver_kwargs.copy() - - # if both are None, determine them together - if batch_size is None and stepsize is None: - batch_size, stepsize = svrg_optimal_batch_and_stepsize( - _softplus_poisson_l_max_and_l, data, y - ) - new_solver_kwargs["batch_size"] = batch_size - new_solver_kwargs["stepsize"] = stepsize - # if only batch size is given, we can still try to determine the optimal step size for it - elif batch_size is not None and stepsize is None: - stepsize = softplus_poisson_optimal_stepsize(data, y, batch_size) - new_solver_kwargs["stepsize"] = stepsize - - self.solver_kwargs = new_solver_kwargs + opt_solver_kwargs = self.optimize_solver_params(data, y) # set up the solver init/run/update attrs - self.instantiate_solver() + self.instantiate_solver(solver_kwargs=opt_solver_kwargs) opt_state = self.solver_init_state(init_params, data, y) return opt_state @@ -1015,6 +1037,39 @@ def update( return opt_step + def optimize_solver_params(self, X, y): + """ + Compute solver optimal defaults if available. + + Returns + ------- + : + A dictionary with the optimal defaults. + """ + new_solver_kwargs = self.solver_kwargs.copy() + if self.solver_name in _OPTIMAL_CONFIGURATIONS: + configs = _OPTIMAL_CONFIGURATIONS[self.solver_name] + model_params = self.get_params() + for conf in configs: + + # check if config is known + known_config = all([check(model_params[key]) for key, check in conf["required_params"].items()]) + + if known_config: + + compute_defaults = conf["compute_defaults"] + strong_convexity = model_params[conf["strong_convexity"]] if conf["strong_convexity"] else None + + # grab the batch and step parameters if set by the user + batch_size = new_solver_kwargs["batch_size"] if "batch_size" in new_solver_kwargs else None + stepsize = new_solver_kwargs["stepsize"] if "stepsize" in new_solver_kwargs else None + + # compute the optimal when batch_size and/or stepsize are not provided and update the parameters. + new_params = compute_defaults(conf["compute_l_smooth"], X, y, batch_size=batch_size, stepsize=stepsize, strong_convexity=strong_convexity) + new_solver_kwargs.update(new_params) + + return new_solver_kwargs + class PopulationGLM(GLM): """ diff --git a/src/nemos/solvers/__init__.py b/src/nemos/solvers/__init__.py new file mode 100644 index 00000000..42d7ee7a --- /dev/null +++ b/src/nemos/solvers/__init__.py @@ -0,0 +1,2 @@ +from ._svrg import ProxSVRG, SVRG +from ._svrg_defaults import svrg_optimal_batch_and_stepsize, softplus_poisson_l_max_and_l \ No newline at end of file diff --git a/src/nemos/solvers.py b/src/nemos/solvers/_svrg.py similarity index 67% rename from src/nemos/solvers.py rename to src/nemos/solvers/_svrg.py index f2804c24..ef6bcac1 100644 --- a/src/nemos/solvers.py +++ b/src/nemos/solvers/_svrg.py @@ -1,6 +1,6 @@ import warnings from functools import partial, wraps -from typing import Any, Callable, NamedTuple, Optional, Tuple, Union +from typing import Callable, NamedTuple, Optional, Union import jax import jax.flatten_util @@ -10,8 +10,8 @@ from jaxopt._src import loop from jaxopt.prox import prox_none -from .tree_utils import tree_add_scalar_mul, tree_l2_norm, tree_slice, tree_sub -from .typing import KeyArrayLike, Pytree +from ..tree_utils import tree_add_scalar_mul, tree_l2_norm, tree_slice, tree_sub +from ..typing import KeyArrayLike, Pytree class SVRGState(NamedTuple): @@ -771,470 +771,3 @@ def run( # substitute None for hyperparams_prox return self._run(init_params, init_state, None, *args) - -def _convert_to_float(func): - """Convert to float.""" - - @wraps(func) - def wrapper(*args, **kwargs): - args, kwargs = jax.tree_util.tree_map(float, (args, kwargs)) - return func(*args, **kwargs) - - return wrapper - - -def softplus_poisson_optimal_stepsize( - X: jnp.ndarray, y: jnp.ndarray, batch_size: int, n_power_iters: Optional[int] = 20 -): - """ - Calculate the optimal stepsize to use for SVRG with a GLM that uses - Poisson observations and soft-plus inverse link function. - - Parameters - ---------- - X : jnp.ndarray - Input data. - y : jnp.ndarray - Output data. - batch_size : int - Mini-batch size, i.e. number of data points sampled for - each inner update of SVRG. - n_power_iters: int, optional, default None - If None, build the XDX matrix (which has a shape of n_features x n_features) - and find its eigenvalues directly. - If an integer, it is the max number of iterations to run the power - iteration for when finding the largest eigenvalue. - - Returns - ------- - stepsize : scalar jax array - Optimal stepsize to use - """ - l_smooth_max, l_smooth = _softplus_poisson_l_max_and_l( - jnp.array(X), jnp.array(y), n_power_iters - ) - - stepsize = _calculate_stepsize_svrg(batch_size, X.shape[0], l_smooth_max, l_smooth) - - return stepsize - - -# not using the previous one to avoid calculating L and L_max twice -def svrg_optimal_batch_and_stepsize( - compute_smoothness_constants: Callable, - *data: Any, - n_power_iters: Optional[int] = None, - default_batch_size: int = 1, - default_stepsize: float = 1e-3, - strong_convexity: Optional[float] = None, -): - """ - Calculate the optimal batch size and step size to use for SVRG with a GLM - that uses Poisson observations and softplus inverse link function. - - Parameters - ---------- - compute_smoothness_constants: - Function that computes l_smooth and l_smooth_max for the problem. - data : - The input data. For a GLM, X and y. - n_power_iters: int, optional, default None - If None, build the XDX matrix (which has a shape of n_features x n_features) - and find its eigenvalues directly. - If an integer, it is the max number of iterations to run the power - iteration for when finding the largest eigenvalue. - default_batch_size : int - Batch size to fall back on if the calculation fails. - default_stepsize: float - Step size to fall back on if the calculation fails. - - Returns - ------- - batch_size : int - Optimal batch size to use. - stepsize : scalar jax array - Optimal stepsize to use. - - Examples - -------- - >>> import numpy as np - >>> from nemos.solvers import svrg_optimal_batch_and_stepsize as compute_opt_params - >>> from nemos.solvers import _softplus_poisson_l_max_and_l - >>> np.random.seed(123) - >>> X = np.random.normal(size=(500, 5)) - >>> y = np.random.poisson(np.exp(X.dot(np.ones(X.shape[1])))) - >>> batch_size, stepsize = compute_opt_params(_softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08) - """ - data = jax.tree_util.tree_map(jnp.asarray, data) - - num_samples = {dd.shape[0] for dd in jax.tree_util.tree_leaves(data)} - - if len(num_samples) != 1: - raise ValueError("Each array in data must have the same number of samples.") - num_samples = num_samples.pop() - - l_smooth_max, l_smooth = compute_smoothness_constants( - *data, n_power_iters=n_power_iters - ) - - batch_size = _calculate_optimal_batch_size_svrg( - num_samples, l_smooth_max, l_smooth, strong_convexity=strong_convexity - ) - - if not jnp.isfinite(batch_size): - batch_size = default_batch_size - stepsize = default_stepsize - - warnings.warn( - f"Could not determine batch and step size automatically. Falling back on the default values of {batch_size} and {default_stepsize}." - ) - else: - stepsize = _calculate_stepsize_svrg( - batch_size, num_samples, l_smooth_max, l_smooth - ) - - return int(batch_size), stepsize - - -def _softplus_poisson_l_max_and_l( - *data, n_power_iters: Optional[int] = 20 -) -> Tuple[float, float]: - """ - Calculate the smoothness constant and maximum smoothness constant for SVRG - assuming that the optimized function is the log-likelihood of a Poisson GLM - with a softplus inverse link function. - - Parameters - ---------- - data: - Tuple of X and y. - n_power_iters : - If None, calculate X.T @ D @ X and its largest eigenvalue directly. - If an integer, the umber of power iterations to use to calculate the largest eigenvalue. - - Returns - ------- - l_smooth_max, l_smooth : - Maximum smoothness constant and smoothness constant. - """ - X, y = data - - # concatenate all data (if X is FeaturePytree) - X = jnp.hstack(jax.tree_util.tree_leaves(X)) - - l_smooth = _softplus_poisson_L(X, y, n_power_iters) - l_smooth_max = _softplus_poisson_L_max(X, y) - return l_smooth_max, l_smooth - - -def _softplus_poisson_L_multiply(X, y, v): - """ - Perform the multiplication of v with X.T @ D @ X without forming the full X.T @ D @ X. - - This assumes that X fits in memory. This estimate is based on calculating the hessian of the loss. - - Parameters - ---------- - X : - Input data. - y : - Output data. - v : - d-dimensional vector. - - Returns - ------- - : - X.T @ D @ X @ v - """ - N, _ = X.shape - return X.T.dot((0.17 * y + 0.25) * X.dot(v)) / N - - -def _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters: int = 20): - """ - Instead of calculating X.T @ D @ X and its largest eigenvalue directly, - calculate it using the power method and by iterating through X and y, - forming a small product at a time. - - Parameters - ---------- - X : - Input data. - y : - Output data. - n_power_iters : - Number of power iterations. - - Returns - ------- - The largest eigenvalue of X.T @ D @ X - """ - # key is fixed to random.key(0) - _, d = X.shape - - # initialize a random d-dimensional vector - v = jnp.ones((d,)) - - # run the power iteration until convergence or the max steps - for _ in range(n_power_iters): - v_prev = v.copy() - v = _softplus_poisson_L_multiply(X, y, v) - v /= v.max() - - if jnp.allclose(v_prev, v): - break - - # calculate the eigenvalue - v /= jnp.linalg.norm(v) - return _softplus_poisson_L_multiply(X, y, v).dot(v) - - -def _softplus_poisson_L( - X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None -): - """ - Calculate the smoothness constant from data, assuming that the optimized - function is the log-likelihood of a Poisson GLM with a softplus inverse link function. - - Parameters - ---------- - X : - Input data. - y : - Output data. - - Returns - ------- - L : - Smoothness constant of f. - """ - if n_power_iters is None: - # calculate XDX/n and its largest eigenvalue directly - XDX = X.T.dot((0.17 * y.reshape(y.shape[0], 1) + 0.25) * X) / y.shape[0] - return jnp.sort(jnp.linalg.eigvalsh(XDX))[-1] - else: - # use the power iteration to calculate the largest eigenvalue - return _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters) - - -def _softplus_poisson_L_max(X: jnp.ndarray, y: jnp.ndarray): - """ - Calculate the maximum smoothness constant from data, assuming that - the optimized function is the log-likelihood of a Poisson GLM with - a softplus inverse link function. - - Parameters - ---------- - X : - Input data. - y : - Output data. - - Returns - ------- - L_max : - Maximum smoothness constant among f_{i}. - """ - N, _ = X.shape - - def body_fun(i, current_max): - return jnp.maximum( - current_max, jnp.linalg.norm(X[i, :]) ** 2 * (0.17 * y[i] + 0.25) - ) - - L_max = jax.lax.fori_loop(0, N, body_fun, jnp.array([-jnp.inf])) - - return L_max[0] - - -@_convert_to_float -def _calculate_stepsize_svrg( - batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float -): - """ - Calculate optimal step size for SVRG$^{[1]}$. - - Parameters - ---------- - batch_size : - Mini-batch size. - num_samples : - Overall number of data points. - l_smooth_max : - Maximum smoothness constant among f_{i}. - l_smooth : - Smoothness constant. - - Returns - ------- - : - Optimal step size for the optimization. - - References - ---------- - [1] Sebbouh, Othmane, et al. "Towards closing the gap between the theory and practice of SVRG." - Advances in neural information processing systems 32 (2019). - """ - numerator = 0.5 * batch_size * (num_samples - 1) - denominator = ( - 3 * (num_samples - batch_size) * l_smooth_max - + num_samples * (batch_size - 1) * l_smooth - ) - return numerator / denominator - - -@_convert_to_float -def _calculate_stepsize_saga( - batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float -) -> float: - """ - Calculate optimal step size for SAGA. - - Parameters - ---------- - batch_size : - Mini-batch size. - num_samples : - Overall number of data points. - l_smooth_max : - Maximum smoothness constant among f_{i}. - l_smooth : - Smoothness constant. - - Returns - ------- - : - Optimal step size for the optimization. - - References - ---------- - [1] Gazagnadou, Nidham, Robert Gower, and Joseph Salmon. - "Optimal mini-batch and step sizes for saga." - International conference on machine learning. PMLR, 2019. - """ - - l_b = l_smooth * num_samples / batch_size * (batch_size - 1) / ( - num_samples - 1 - ) + l_smooth_max / batch_size * (num_samples - batch_size) / (num_samples - 1) - - return 0.25 / l_b - - -def _calculate_optimal_batch_size_svrg( - num_samples: int, - l_smooth_max: float, - l_smooth: float, - strong_convexity: Optional[float] = None, -) -> int: - """ - Calculate the optimal batch size according to "Table 1" in [1]. - - Parameters - ---------- - num_samples: - The number of samples. - l_smooth_max: - The Lmax smoothness constant. - l_smooth: - The L smoothness constant. - strong_convexity: - The strong convexity constant. - - Returns - ------- - batch_size: - The batch size. - - """ - if strong_convexity is None: - # Assume that num_sample is large enough for mini-batching. - # This is usually the case for neuroscience where num_sample - # is typically very large. - # If this assumption is not matched, convergence may be slow. - batch_size = 1 - else: - # Compute optimal batch size according to Table 1. - if num_samples >= 3 * l_smooth_max / strong_convexity: - batch_size = 1 - elif num_samples > l_smooth / strong_convexity: - b_tilde = _calculate_b_tilde( - num_samples, l_smooth_max, l_smooth, strong_convexity - ) - if l_smooth_max < num_samples * l_smooth / 3: - b_hat = _calculate_b_hat(num_samples, l_smooth_max, l_smooth) - batch_size = int(jnp.floor(jnp.minimum(b_hat, b_tilde))) - else: - batch_size = int(jnp.floor(b_tilde)) - else: - if l_smooth_max < num_samples * l_smooth / 3: - batch_size = int( - jnp.floor(_calculate_b_hat(num_samples, l_smooth_max, l_smooth)) - ) - else: - batch_size = int(num_samples) # reset this to int - return batch_size - - -@_convert_to_float -def _calculate_b_hat(num_samples: int, l_smooth_max: float, l_smooth: float): - r""" - Helper function for calculating $\hat{b}$ in "Table 1" of [1]. - - Parameters - ---------- - num_samples : - Overall number of data points. - l_smooth_max : - Maximum smoothness constant among f_{i}. - l_smooth : - Smoothness constant. - - Returns - ------- - : - Optimal batch size for the optimization. - - References - ---------- - [1] Sebbouh, Othmane, et al. "Towards closing the gap between the theory and practice of SVRG." - Advances in neural information processing systems 32 (2019). - """ - numerator = num_samples / 2 * (3 * l_smooth_max - l_smooth) - denominator = num_samples * l_smooth - 3 * l_smooth_max - return jnp.sqrt(numerator / denominator) - - -@_convert_to_float -def _calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity): - r""" - Helper function for calculating $\tilde{b}$ as in "Table 1" of [1]. - - Parameters - ---------- - num_samples : - Overall number of data points. - l_smooth_max : - Maximum smoothness constant among f_{i}. - l_smooth : - Smoothness constant. - strong_convexity : - Strong convexity constant. - - Returns - ------- - : - Optimal batch size for the optimization. - - References - ---------- - [1] Sebbouh, Othmane, et al. "Towards closing the gap between the theory and practice of SVRG." - Advances in neural information processing systems 32 (2019). - """ - numerator = (3 * l_smooth_max - l_smooth) * num_samples - denominator = ( - num_samples * (num_samples - 1) * strong_convexity - - num_samples * l_smooth - + 3 * l_smooth_max - ) - return numerator / denominator diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py new file mode 100644 index 00000000..4e2711e9 --- /dev/null +++ b/src/nemos/solvers/_svrg_defaults.py @@ -0,0 +1,454 @@ +"""Calculate theoretical optimal defaults if available.""" + +from functools import wraps +import warnings +from typing import Optional, Callable, Any, Tuple +import jax +import jax.numpy as jnp + + +def _convert_to_float(func): + """Convert to float.""" + + @wraps(func) + def wrapper(*args, **kwargs): + args, kwargs = jax.tree_util.tree_map(float, (args, kwargs)) + return func(*args, **kwargs) + + return wrapper + + +# not using the previous one to avoid calculating L and L_max twice +def svrg_optimal_batch_and_stepsize( + compute_smoothness_constants: Callable, + *data: Any, + batch_size: Optional[int] = None, + stepsize: Optional[float] = None, + n_power_iters: Optional[int] = None, + default_batch_size: int = 1, + default_stepsize: float = 1e-3, + strong_convexity: Optional[float] = None, +): + """ + Calculate the optimal batch size and step size to use for SVRG with a GLM + that uses Poisson observations and softplus inverse link function. + + Parameters + ---------- + compute_smoothness_constants: + Function that computes l_smooth and l_smooth_max for the problem. + data : + The input data. For a GLM, X and y. + n_power_iters: int, optional, default None + If None, build the XDX matrix (which has a shape of n_features x n_features) + and find its eigenvalues directly. + If an integer, it is the max number of iterations to run the power + iteration for when finding the largest eigenvalue. + batch_size: + The batch_size set by the user. + stepsize: + The stepsize set by the user. + default_batch_size : int + Batch size to fall back on if the calculation fails. + default_stepsize: float + Step size to fall back on if the calculation fails. + + Returns + ------- + batch_size : int + Optimal batch size to use. + stepsize : scalar jax array + Optimal stepsize to use. + + Examples + -------- + >>> import numpy as np + >>> from nemos.solvers import svrg_optimal_batch_and_stepsize as compute_opt_params + >>> from nemos.solvers import softplus_poisson_l_max_and_l + >>> np.random.seed(123) + >>> X = np.random.normal(size=(500, 5)) + >>> y = np.random.poisson(np.exp(X.dot(np.ones(X.shape[1])))) + >>> batch_size, stepsize = compute_opt_params(softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08) + """ + # if both parameters are set by the user then just return them + if batch_size is not None and stepsize is not None: + return {"batch_size": batch_size, "stepsize": stepsize} + + data = jax.tree_util.tree_map(jnp.asarray, data) + + num_samples = {dd.shape[0] for dd in jax.tree_util.tree_leaves(data)} + + if len(num_samples) != 1: + raise ValueError("Each array in data must have the same number of samples.") + num_samples = num_samples.pop() + + l_smooth_max, l_smooth = compute_smoothness_constants( + *data, n_power_iters=n_power_iters + ) + + if batch_size is None: + batch_size = _calculate_optimal_batch_size_svrg( + num_samples, l_smooth_max, l_smooth, strong_convexity=strong_convexity + ) + + if not jnp.isfinite(batch_size): + batch_size = default_batch_size + stepsize = default_stepsize + + warnings.warn( + f"Could not determine batch and step size automatically. Falling back on the default values of {batch_size} and {default_stepsize}." + ) + + if stepsize is None: + stepsize = _calculate_stepsize_svrg( + batch_size, num_samples, l_smooth_max, l_smooth + ) + + return {"batch_size": int(batch_size), "stepsize": stepsize} + + +def softplus_poisson_l_max_and_l( + *data, n_power_iters: Optional[int] = 20 +) -> Tuple[float, float]: + """ + Calculate the smoothness constant and maximum smoothness constant for SVRG + assuming that the optimized function is the log-likelihood of a Poisson GLM + with a softplus inverse link function. + + Parameters + ---------- + data: + Tuple of X and y. + n_power_iters : + If None, calculate X.T @ D @ X and its largest eigenvalue directly. + If an integer, the umber of power iterations to use to calculate the largest eigenvalue. + + Returns + ------- + l_smooth_max, l_smooth : + Maximum smoothness constant and smoothness constant. + """ + X, y = data + + # takes care of population glm (see bound found on overleaf) + y = jnp.max(y, axis=tuple(range(1, y.ndim))) + + # concatenate all data (if X is FeaturePytree) + X = jnp.hstack(jax.tree_util.tree_leaves(X)) + + l_smooth = _softplus_poisson_L(X, y, n_power_iters) + l_smooth_max = _softplus_poisson_L_max(X, y) + return l_smooth_max, l_smooth + + +def _softplus_poisson_L_multiply(X, y, v): + """ + Perform the multiplication of v with X.T @ D @ X without forming the full X.T @ D @ X. + + This assumes that X fits in memory. This estimate is based on calculating the hessian of the loss. + + Parameters + ---------- + X : + Input data. + y : + Output data. + v : + d-dimensional vector. + + Returns + ------- + : + X.T @ D @ X @ v + """ + N, _ = X.shape + return X.T.dot((0.17 * y + 0.25) * X.dot(v)) / N + + +def _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters: int = 20): + """ + Instead of calculating X.T @ D @ X and its largest eigenvalue directly, + calculate it using the power method and by iterating through X and y, + forming a small product at a time. + + Parameters + ---------- + X : + Input data. + y : + Output data. + n_power_iters : + Number of power iterations. + + Returns + ------- + The largest eigenvalue of X.T @ D @ X + """ + # key is fixed to random.key(0) + _, d = X.shape + + # initialize a random d-dimensional vector + v = jnp.ones((d,)) + + # run the power iteration until convergence or the max steps + for _ in range(n_power_iters): + v_prev = v.copy() + v = _softplus_poisson_L_multiply(X, y, v) + v /= v.max() + + if jnp.allclose(v_prev, v): + break + + # calculate the eigenvalue + v /= jnp.linalg.norm(v) + return _softplus_poisson_L_multiply(X, y, v).dot(v) + + +def _softplus_poisson_L( + X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None +): + """ + Calculate the smoothness constant from data, assuming that the optimized + function is the log-likelihood of a Poisson GLM with a softplus inverse link function. + + Parameters + ---------- + X : + Input data. + y : + Output data. + + Returns + ------- + L : + Smoothness constant of f. + """ + if n_power_iters is None: + # calculate XDX/n and its largest eigenvalue directly + XDX = X.T.dot((0.17 * y.reshape(y.shape[0], 1) + 0.25) * X) / y.shape[0] + return jnp.sort(jnp.linalg.eigvalsh(XDX))[-1] + else: + # use the power iteration to calculate the largest eigenvalue + return _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters) + + +def _softplus_poisson_L_max(X: jnp.ndarray, y: jnp.ndarray): + """ + Calculate the maximum smoothness constant from data, assuming that + the optimized function is the log-likelihood of a Poisson GLM with + a softplus inverse link function. + + Parameters + ---------- + X : + Input data. + y : + Output data. + + Returns + ------- + L_max : + Maximum smoothness constant among f_{i}. + """ + N, _ = X.shape + + def body_fun(i, current_max): + return jnp.maximum( + current_max, jnp.linalg.norm(X[i, :]) ** 2 * (0.17 * y[i] + 0.25) + ) + + L_max = jax.lax.fori_loop(0, N, body_fun, jnp.array([-jnp.inf])) + + return L_max[0] + + +@_convert_to_float +def _calculate_stepsize_svrg( + batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float +): + """ + Calculate optimal step size for SVRG$^{[1]}$. + + Parameters + ---------- + batch_size : + Mini-batch size. + num_samples : + Overall number of data points. + l_smooth_max : + Maximum smoothness constant among f_{i}. + l_smooth : + Smoothness constant. + + Returns + ------- + : + Optimal step size for the optimization. + + References + ---------- + [1] Sebbouh, Othmane, et al. "Towards closing the gap between the theory and practice of SVRG." + Advances in neural information processing systems 32 (2019). + """ + numerator = 0.5 * batch_size * (num_samples - 1) + denominator = ( + 3 * (num_samples - batch_size) * l_smooth_max + + num_samples * (batch_size - 1) * l_smooth + ) + return numerator / denominator + + +@_convert_to_float +def _calculate_stepsize_saga( + batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float +) -> float: + """ + Calculate optimal step size for SAGA. + + Parameters + ---------- + batch_size : + Mini-batch size. + num_samples : + Overall number of data points. + l_smooth_max : + Maximum smoothness constant among f_{i}. + l_smooth : + Smoothness constant. + + Returns + ------- + : + Optimal step size for the optimization. + + References + ---------- + [1] Gazagnadou, Nidham, Robert Gower, and Joseph Salmon. + "Optimal mini-batch and step sizes for saga." + International conference on machine learning. PMLR, 2019. + """ + + l_b = l_smooth * num_samples / batch_size * (batch_size - 1) / ( + num_samples - 1 + ) + l_smooth_max / batch_size * (num_samples - batch_size) / (num_samples - 1) + + return 0.25 / l_b + + +def _calculate_optimal_batch_size_svrg( + num_samples: int, + l_smooth_max: float, + l_smooth: float, + strong_convexity: Optional[float] = None, +) -> int: + """ + Calculate the optimal batch size according to "Table 1" in [1]. + + Parameters + ---------- + num_samples: + The number of samples. + l_smooth_max: + The Lmax smoothness constant. + l_smooth: + The L smoothness constant. + strong_convexity: + The strong convexity constant. + + Returns + ------- + batch_size: + The batch size. + + """ + if strong_convexity is None: + # Assume that num_sample is large enough for mini-batching. + # This is usually the case for neuroscience where num_sample + # is typically very large. + # If this assumption is not matched, convergence may be slow. + batch_size = 1 + else: + # Compute optimal batch size according to Table 1. + if num_samples >= 3 * l_smooth_max / strong_convexity: + batch_size = 1 + elif num_samples > l_smooth / strong_convexity: + b_tilde = _calculate_b_tilde( + num_samples, l_smooth_max, l_smooth, strong_convexity + ) + if l_smooth_max < num_samples * l_smooth / 3: + b_hat = _calculate_b_hat(num_samples, l_smooth_max, l_smooth) + batch_size = int(jnp.floor(jnp.minimum(b_hat, b_tilde))) + else: + batch_size = int(jnp.floor(b_tilde)) + else: + if l_smooth_max < num_samples * l_smooth / 3: + batch_size = int( + jnp.floor(_calculate_b_hat(num_samples, l_smooth_max, l_smooth)) + ) + else: + batch_size = int(num_samples) # reset this to int + return batch_size + + +@_convert_to_float +def _calculate_b_hat(num_samples: int, l_smooth_max: float, l_smooth: float): + r""" + Helper function for calculating $\hat{b}$ in "Table 1" of [1]. + + Parameters + ---------- + num_samples : + Overall number of data points. + l_smooth_max : + Maximum smoothness constant among f_{i}. + l_smooth : + Smoothness constant. + + Returns + ------- + : + Optimal batch size for the optimization. + + References + ---------- + [1] Sebbouh, Othmane, et al. "Towards closing the gap between the theory and practice of SVRG." + Advances in neural information processing systems 32 (2019). + """ + numerator = num_samples / 2 * (3 * l_smooth_max - l_smooth) + denominator = num_samples * l_smooth - 3 * l_smooth_max + return jnp.sqrt(numerator / denominator) + + +@_convert_to_float +def _calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity): + r""" + Helper function for calculating $\tilde{b}$ as in "Table 1" of [1]. + + Parameters + ---------- + num_samples : + Overall number of data points. + l_smooth_max : + Maximum smoothness constant among f_{i}. + l_smooth : + Smoothness constant. + strong_convexity : + Strong convexity constant. + + Returns + ------- + : + Optimal batch size for the optimization. + + References + ---------- + [1] Sebbouh, Othmane, et al. "Towards closing the gap between the theory and practice of SVRG." + Advances in neural information processing systems 32 (2019). + """ + numerator = (3 * l_smooth_max - l_smooth) * num_samples + denominator = ( + num_samples * (num_samples - 1) * strong_convexity + - num_samples * l_smooth + + 3 * l_smooth_max + ) + return numerator / denominator diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 72970397..973086c4 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -7,7 +7,7 @@ import pytest import nemos as nmo -from nemos.solvers import SVRG, ProxSVRG, SVRGState +from nemos.solvers._svrg import SVRG, ProxSVRG, SVRGState from nemos.tree_utils import pytree_map_and_reduce, tree_l2_norm, tree_slice, tree_sub From ab4dbfd2b6262736b8cfc36606d79ffe726f27da Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 10:32:22 +0200 Subject: [PATCH 11/52] improved doscrsrings and added test for config --- src/nemos/base_regressor.py | 4 +- src/nemos/glm.py | 107 ++++++++++++++++++++-------- src/nemos/solvers/__init__.py | 5 +- src/nemos/solvers/_svrg.py | 1 - src/nemos/solvers/_svrg_defaults.py | 24 ++++--- tests/test_glm.py | 40 +++++++++++ 6 files changed, 138 insertions(+), 43 deletions(-) diff --git a/src/nemos/base_regressor.py b/src/nemos/base_regressor.py index 51859fd7..1b87ae66 100644 --- a/src/nemos/base_regressor.py +++ b/src/nemos/base_regressor.py @@ -247,7 +247,9 @@ def _check_solver_kwargs(solver_class, solver_kwargs): f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for {solver_class.__name__}!" ) - def instantiate_solver(self, *args, solver_kwargs: Optional[dict] = None) -> BaseRegressor: + def instantiate_solver( + self, *args, solver_kwargs: Optional[dict] = None + ) -> BaseRegressor: """ Instantiate the solver with the provided loss function. diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 402f3146..7c86a129 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -33,47 +33,50 @@ "SVRG": [ { "required_params": { - "observation_model__inverse_link_function": lambda x: x == jax.nn.softplus, + "observation_model__inverse_link_function": lambda x: x + == jax.nn.softplus, "observation_model": lambda x: isinstance(x, obs.PoissonObservations), - "regularizer": lambda x: not isinstance(x, Ridge) + "regularizer": lambda x: not isinstance(x, Ridge), }, "compute_l_smooth": softplus_poisson_l_max_and_l, "compute_defaults": svrg_optimal_batch_and_stepsize, - "strong_convexity": None + "strong_convexity": None, }, { "required_params": { - "observation_model__inverse_link_function": lambda x: x == jax.nn.softplus, + "observation_model__inverse_link_function": lambda x: x + == jax.nn.softplus, "observation_model": lambda x: isinstance(x, obs.PoissonObservations), - "regularizer": lambda x: isinstance(x, Ridge) + "regularizer": lambda x: isinstance(x, Ridge), }, "compute_l_smooth": softplus_poisson_l_max_and_l, "compute_defaults": svrg_optimal_batch_and_stepsize, - "strong_convexity": "regularizer_strength" - } + "strong_convexity": "regularizer_strength", + }, ], "ProxSVRG": [ { "required_params": { - "observation_model__inverse_link_function": lambda x: x == jax.nn.softplus, + "observation_model__inverse_link_function": lambda x: x + == jax.nn.softplus, "observation_model": lambda x: isinstance(x, obs.PoissonObservations), - "regularizer": lambda x: not isinstance(x, Ridge) + "regularizer": lambda x: not isinstance(x, Ridge), }, "compute_l_smooth": softplus_poisson_l_max_and_l, "compute_defaults": svrg_optimal_batch_and_stepsize, - "strong_convexity": None + "strong_convexity": None, }, { "required_params": { - "observation_model__inverse_link_function": lambda x: x == jax.nn.softplus, + "observation_model__inverse_link_function": lambda x: x + == jax.nn.softplus, "observation_model": lambda x: isinstance(x, obs.PoissonObservations), - "regularizer": lambda x: isinstance(x, Ridge) + "regularizer": lambda x: isinstance(x, Ridge), }, "compute_l_smooth": softplus_poisson_l_max_and_l, "compute_defaults": svrg_optimal_batch_and_stepsize, - "strong_convexity": "regularizer_strength" - } - + "strong_convexity": "regularizer_strength", + }, ], } @@ -1039,35 +1042,81 @@ def update( def optimize_solver_params(self, X, y): """ - Compute solver optimal defaults if available. + Compute and update solver parameters with optimal defaults if available. + + This method checks the current solver configuration and, if an optimal + configuration is known for the given model parameters, computes the optimal + batch size, step size, and other hyperparameters to ensure faster convergence. + + Parameters + ---------- + X : array-like + Input data used to compute smoothness and strong convexity constants. + y : array-like + Target values used in conjunction with X for the same purpose. Returns ------- - : - A dictionary with the optimal defaults. + dict + A dictionary containing the solver parameters, updated with optimal defaults + where applicable. + + Notes + ----- + The method first looks up the known configurations for the solver in the + `_OPTIMAL_CONFIGURATIONS` dictionary. If a configuration matches the model's + parameters, it uses the associated functions to compute the optimal solver + parameters. If `batch_size` or `stepsize` is already provided by the user, + those values are used; otherwise, optimal defaults are computed. """ + + # Start with a copy of the existing solver parameters new_solver_kwargs = self.solver_kwargs.copy() + + # Check if the current solver has any known optimal configurations if self.solver_name in _OPTIMAL_CONFIGURATIONS: + # Retrieve the list of configurations for the solver configs = _OPTIMAL_CONFIGURATIONS[self.solver_name] + # Get the model parameters to check against the required conditions model_params = self.get_params() - for conf in configs: - # check if config is known - known_config = all([check(model_params[key]) for key, check in conf["required_params"].items()]) + # Iterate through each configuration + for conf in configs: + # Determine if the current model matches the configuration's requirements + known_config = all( + [ + check(model_params[key]) + for key, check in conf["required_params"].items() + ] + ) if known_config: - + # Extract the function to compute optimal defaults compute_defaults = conf["compute_defaults"] - strong_convexity = model_params[conf["strong_convexity"]] if conf["strong_convexity"] else None - # grab the batch and step parameters if set by the user - batch_size = new_solver_kwargs["batch_size"] if "batch_size" in new_solver_kwargs else None - stepsize = new_solver_kwargs["stepsize"] if "stepsize" in new_solver_kwargs else None + # Extract strong convexity if it is relevant for this configuration + strong_convexity = ( + model_params[conf["strong_convexity"]] + if conf["strong_convexity"] + else None + ) - # compute the optimal when batch_size and/or stepsize are not provided and update the parameters. - new_params = compute_defaults(conf["compute_l_smooth"], X, y, batch_size=batch_size, stepsize=stepsize, strong_convexity=strong_convexity) - new_solver_kwargs.update(new_params) + # Check if the user has provided batch size or stepsize, or else use None + batch_size = new_solver_kwargs.get("batch_size", None) + stepsize = new_solver_kwargs.get("stepsize", None) + + # Compute the optimal batch size and stepsize based on smoothness, strong convexity, etc. + new_params = compute_defaults( + conf["compute_l_smooth"], + X, + y, + batch_size=batch_size, + stepsize=stepsize, + strong_convexity=strong_convexity, + ) + # Update the solver parameters with the computed optimal values + new_solver_kwargs.update(new_params) return new_solver_kwargs diff --git a/src/nemos/solvers/__init__.py b/src/nemos/solvers/__init__.py index 42d7ee7a..ec59f177 100644 --- a/src/nemos/solvers/__init__.py +++ b/src/nemos/solvers/__init__.py @@ -1,2 +1,5 @@ from ._svrg import ProxSVRG, SVRG -from ._svrg_defaults import svrg_optimal_batch_and_stepsize, softplus_poisson_l_max_and_l \ No newline at end of file +from ._svrg_defaults import ( + svrg_optimal_batch_and_stepsize, + softplus_poisson_l_max_and_l, +) diff --git a/src/nemos/solvers/_svrg.py b/src/nemos/solvers/_svrg.py index ef6bcac1..da6059b4 100644 --- a/src/nemos/solvers/_svrg.py +++ b/src/nemos/solvers/_svrg.py @@ -770,4 +770,3 @@ def run( # substitute None for hyperparams_prox return self._run(init_params, init_state, None, *args) - diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 4e2711e9..04e19cf4 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -24,10 +24,10 @@ def svrg_optimal_batch_and_stepsize( *data: Any, batch_size: Optional[int] = None, stepsize: Optional[float] = None, + strong_convexity: Optional[float] = None, n_power_iters: Optional[int] = None, default_batch_size: int = 1, default_stepsize: float = 1e-3, - strong_convexity: Optional[float] = None, ): """ Calculate the optimal batch size and step size to use for SVRG with a GLM @@ -48,6 +48,9 @@ def svrg_optimal_batch_and_stepsize( The batch_size set by the user. stepsize: The stepsize set by the user. + strong_convexity: + The strong convexity constant. For penalized losses with an L2 component (Ridge, Elastic Net, etc.) + the convexity constant should be equal to the penalization strength. default_batch_size : int Batch size to fall back on if the calculation fails. default_stepsize: float @@ -93,10 +96,9 @@ def svrg_optimal_batch_and_stepsize( if not jnp.isfinite(batch_size): batch_size = default_batch_size - stepsize = default_stepsize - warnings.warn( - f"Could not determine batch and step size automatically. Falling back on the default values of {batch_size} and {default_stepsize}." + "Could not determine batch and step size automatically. " + f"Falling back on the default values of {batch_size} and {default_stepsize}." ) if stepsize is None: @@ -136,12 +138,12 @@ def softplus_poisson_l_max_and_l( # concatenate all data (if X is FeaturePytree) X = jnp.hstack(jax.tree_util.tree_leaves(X)) - l_smooth = _softplus_poisson_L(X, y, n_power_iters) - l_smooth_max = _softplus_poisson_L_max(X, y) + l_smooth = _softplus_poisson_l_smooth(X, y, n_power_iters) + l_smooth_max = _softplus_poisson_l_smooth_max(X, y) return l_smooth_max, l_smooth -def _softplus_poisson_L_multiply(X, y, v): +def _softplus_poisson_l_smooth_multiply(X, y, v): """ Perform the multiplication of v with X.T @ D @ X without forming the full X.T @ D @ X. @@ -193,7 +195,7 @@ def _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters: int = 2 # run the power iteration until convergence or the max steps for _ in range(n_power_iters): v_prev = v.copy() - v = _softplus_poisson_L_multiply(X, y, v) + v = _softplus_poisson_l_smooth_multiply(X, y, v) v /= v.max() if jnp.allclose(v_prev, v): @@ -201,10 +203,10 @@ def _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters: int = 2 # calculate the eigenvalue v /= jnp.linalg.norm(v) - return _softplus_poisson_L_multiply(X, y, v).dot(v) + return _softplus_poisson_l_smooth_multiply(X, y, v).dot(v) -def _softplus_poisson_L( +def _softplus_poisson_l_smooth( X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None ): """ @@ -232,7 +234,7 @@ def _softplus_poisson_L( return _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters) -def _softplus_poisson_L_max(X: jnp.ndarray, y: jnp.ndarray): +def _softplus_poisson_l_smooth_max(X: jnp.ndarray, y: jnp.ndarray): """ Calculate the maximum smoothness constant from data, assuming that the optimized function is the log-likelihood of a Poisson GLM with diff --git a/tests/test_glm.py b/tests/test_glm.py index c9afef1c..003b9cdf 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -3340,3 +3340,43 @@ def test_reg_strength_reset(self, reg): model = nmo.glm.GLM(regularizer=reg, regularizer_strength=1.0) model.regularizer = "UnRegularized" assert model.regularizer_strength is None + + +def test_optimal_config_all_required_keys_present(): + """Test that all required keys are present in each configuration.""" + required_keys = ["required_params", "compute_l_smooth", "compute_defaults", "strong_convexity"] + for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): + for config in configs: + for key in required_keys: + assert key in config, f"Configuration for solver '{solver}' is missing the required key: '{key}'." + + +def test_optimal_config_required_params_is_dict(): + """Test that 'required_params' is a dictionary.""" + for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): + for config in configs: + assert isinstance(config["required_params"], dict), f"'required_params' should be a dictionary in the configuration for solver '{solver}'." + + +def test_optimal_config_compute_l_smooth_is_callable(): + """Test that 'compute_l_smooth' is a callable function.""" + for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): + for config in configs: + assert callable(config["compute_l_smooth"]), f"'compute_l_smooth' should be callable in the configuration for solver '{solver}'." + + +def test_optimal_config_compute_defaults_is_callable(): + """Test that 'compute_defaults' is a callable function.""" + for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): + for config in configs: + assert callable(config["compute_defaults"]), f"'compute_defaults' should be callable in the configuration for solver '{solver}'." + + +def test_optimal_config_strong_convexity_is_valid_type(): + """Test that 'strong_convexity' is either None, a string, or callable.""" + for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): + for config in configs: + strong_convexity = config["strong_convexity"] + assert strong_convexity is None or isinstance(strong_convexity, (str, type(None))), \ + f"'strong_convexity' should be either None, a string, or callable in the configuration for solver '{solver}'." + From 886bdeb1d0cf2842ef76b6d94cb9b50d6568bca3 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 11:04:51 +0200 Subject: [PATCH 12/52] improved naming and docstrings --- src/nemos/glm.py | 10 +- src/nemos/solvers/__init__.py | 2 +- src/nemos/solvers/_svrg_defaults.py | 269 +++++++++++++++++----------- 3 files changed, 166 insertions(+), 115 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 7c86a129..b44bc27b 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -20,7 +20,7 @@ from .pytrees import FeaturePytree from .regularizer import GroupLasso, Lasso, Regularizer, Ridge from .solvers._svrg_defaults import ( - softplus_poisson_l_max_and_l, + glm_softplus_poisson_l_max_and_l, svrg_optimal_batch_and_stepsize, ) from .type_casting import jnp_asarray_if, support_pynapple @@ -38,7 +38,7 @@ "observation_model": lambda x: isinstance(x, obs.PoissonObservations), "regularizer": lambda x: not isinstance(x, Ridge), }, - "compute_l_smooth": softplus_poisson_l_max_and_l, + "compute_l_smooth": glm_softplus_poisson_l_max_and_l, "compute_defaults": svrg_optimal_batch_and_stepsize, "strong_convexity": None, }, @@ -49,7 +49,7 @@ "observation_model": lambda x: isinstance(x, obs.PoissonObservations), "regularizer": lambda x: isinstance(x, Ridge), }, - "compute_l_smooth": softplus_poisson_l_max_and_l, + "compute_l_smooth": glm_softplus_poisson_l_max_and_l, "compute_defaults": svrg_optimal_batch_and_stepsize, "strong_convexity": "regularizer_strength", }, @@ -62,7 +62,7 @@ "observation_model": lambda x: isinstance(x, obs.PoissonObservations), "regularizer": lambda x: not isinstance(x, Ridge), }, - "compute_l_smooth": softplus_poisson_l_max_and_l, + "compute_l_smooth": glm_softplus_poisson_l_max_and_l, "compute_defaults": svrg_optimal_batch_and_stepsize, "strong_convexity": None, }, @@ -73,7 +73,7 @@ "observation_model": lambda x: isinstance(x, obs.PoissonObservations), "regularizer": lambda x: isinstance(x, Ridge), }, - "compute_l_smooth": softplus_poisson_l_max_and_l, + "compute_l_smooth": glm_softplus_poisson_l_max_and_l, "compute_defaults": svrg_optimal_batch_and_stepsize, "strong_convexity": "regularizer_strength", }, diff --git a/src/nemos/solvers/__init__.py b/src/nemos/solvers/__init__.py index ec59f177..cc71bd63 100644 --- a/src/nemos/solvers/__init__.py +++ b/src/nemos/solvers/__init__.py @@ -1,5 +1,5 @@ from ._svrg import ProxSVRG, SVRG from ._svrg_defaults import ( svrg_optimal_batch_and_stepsize, - softplus_poisson_l_max_and_l, + glm_softplus_poisson_l_max_and_l, ) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 04e19cf4..14010e91 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -1,4 +1,4 @@ -"""Calculate theoretical optimal defaults if available.""" +"""Module for calculating theoretical optimal defaults for SVRG and GLM configurations.""" from functools import wraps import warnings @@ -8,7 +8,21 @@ def _convert_to_float(func): - """Convert to float.""" + """ + Decorator to convert all inputs to float before passing them to the function. + + Ensures that calculations within the function are performed with floating-point precision. + + Parameters + ---------- + func : + The function to be wrapped by the decorator. + + Returns + ------- + : + Wrapped function with inputs converted to floats. + """ @wraps(func) def wrapper(*args, **kwargs): @@ -30,77 +44,90 @@ def svrg_optimal_batch_and_stepsize( default_stepsize: float = 1e-3, ): """ - Calculate the optimal batch size and step size to use for SVRG with a GLM - that uses Poisson observations and softplus inverse link function. + Calculate the optimal batch size and step size for SVRG optimization in GLMs. + + This function computes the optimal batch size and step size parameters for SVRG, + based on the smoothness constants and strong convexity of the loss function. Parameters ---------- - compute_smoothness_constants: - Function that computes l_smooth and l_smooth_max for the problem. - data : - The input data. For a GLM, X and y. - n_power_iters: int, optional, default None - If None, build the XDX matrix (which has a shape of n_features x n_features) - and find its eigenvalues directly. - If an integer, it is the max number of iterations to run the power - iteration for when finding the largest eigenvalue. - batch_size: - The batch_size set by the user. - stepsize: - The stepsize set by the user. - strong_convexity: - The strong convexity constant. For penalized losses with an L2 component (Ridge, Elastic Net, etc.) - the convexity constant should be equal to the penalization strength. - default_batch_size : int - Batch size to fall back on if the calculation fails. - default_stepsize: float - Step size to fall back on if the calculation fails. + compute_smoothness_constants : Callable + Function that computes the smoothness constants `l_smooth` and `l_smooth_max` for the problem. + This is problem (loss function) specific. + data : Any + Input data, typically (X, y) for a GLM. + batch_size : Optional[int], default None + The batch size set by the user. If None, it will be calculated. + stepsize : Optional[float], default None + The step size set by the user. If None, it will be calculated. + strong_convexity : Optional[float], default None + The strong convexity constant. For L2-regularized losses, this should be the regularization strength. + n_power_iters : Optional[int], default None + Maximum number of iterations for the power method when finding the largest eigenvalue. + default_batch_size : int, default 1 + Default batch size to use if the optimal calculation fails. + default_stepsize : float, default 1e-3 + Default step size to use if the optimal calculation fails. Returns ------- - batch_size : int - Optimal batch size to use. - stepsize : scalar jax array - Optimal stepsize to use. + dict + Dictionary containing the optimal `batch_size` and `stepsize`. + + Raises + ------ + ValueError + If the data provided has inconsistent numbers of samples. + + Warnings + -------- + UserWarning + Warns the user if the calculation fails and defaults are used instead. Examples -------- >>> import numpy as np >>> from nemos.solvers import svrg_optimal_batch_and_stepsize as compute_opt_params - >>> from nemos.solvers import softplus_poisson_l_max_and_l + >>> from nemos.solvers import glm_softplus_poisson_l_max_and_l >>> np.random.seed(123) >>> X = np.random.normal(size=(500, 5)) >>> y = np.random.poisson(np.exp(X.dot(np.ones(X.shape[1])))) - >>> batch_size, stepsize = compute_opt_params(softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08) + >>> batch_size, stepsize = compute_opt_params(glm_softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08) """ - # if both parameters are set by the user then just return them + # If both parameters are set by the user, return them directly if batch_size is not None and stepsize is not None: return {"batch_size": batch_size, "stepsize": stepsize} + # Ensure data is converted to JAX arrays data = jax.tree_util.tree_map(jnp.asarray, data) + # Get the number of samples, ensuring consistency across all inputs num_samples = {dd.shape[0] for dd in jax.tree_util.tree_leaves(data)} - if len(num_samples) != 1: raise ValueError("Each array in data must have the same number of samples.") num_samples = num_samples.pop() + # Compute smoothness constants l_smooth_max, l_smooth = compute_smoothness_constants( *data, n_power_iters=n_power_iters ) + # Compute optimal batch size if not provided by the user if batch_size is None: batch_size = _calculate_optimal_batch_size_svrg( num_samples, l_smooth_max, l_smooth, strong_convexity=strong_convexity ) + # Fall back to defaults if batch size calculation fails if not jnp.isfinite(batch_size): batch_size = default_batch_size warnings.warn( "Could not determine batch and step size automatically. " - f"Falling back on the default values of {batch_size} and {default_stepsize}." + f"Falling back on the default values of {batch_size} and {default_stepsize}.", + UserWarning ) + # Compute optimal step size if not provided by the user if stepsize is None: stepsize = _calculate_stepsize_svrg( batch_size, num_samples, l_smooth_max, l_smooth @@ -109,26 +136,27 @@ def svrg_optimal_batch_and_stepsize( return {"batch_size": int(batch_size), "stepsize": stepsize} -def softplus_poisson_l_max_and_l( - *data, n_power_iters: Optional[int] = 20 +def glm_softplus_poisson_l_max_and_l( + *data: jnp.ndarray, n_power_iters: Optional[int] = 20 ) -> Tuple[float, float]: """ - Calculate the smoothness constant and maximum smoothness constant for SVRG - assuming that the optimized function is the log-likelihood of a Poisson GLM - with a softplus inverse link function. + Calculate smoothness constants for a Poisson GLM with a softplus inverse link function. + + Computes the smoothness constant (`l_smooth`) and the maximum smoothness constant + (`l_smooth_max`) for SVRG, given the data and the GLM structure. Parameters ---------- - data: - Tuple of X and y. + data : + Input data, typically (X, y). n_power_iters : - If None, calculate X.T @ D @ X and its largest eigenvalue directly. - If an integer, the umber of power iterations to use to calculate the largest eigenvalue. + Number of power iterations to use when finding the largest eigenvalue. If None, + the eigenvalue is calculated directly. Returns ------- - l_smooth_max, l_smooth : - Maximum smoothness constant and smoothness constant. + : + Maximum smoothness constant (`l_smooth_max`) and smoothness constant (`l_smooth`). """ X, y = data @@ -138,119 +166,129 @@ def softplus_poisson_l_max_and_l( # concatenate all data (if X is FeaturePytree) X = jnp.hstack(jax.tree_util.tree_leaves(X)) - l_smooth = _softplus_poisson_l_smooth(X, y, n_power_iters) - l_smooth_max = _softplus_poisson_l_smooth_max(X, y) + l_smooth = _glm_softplus_poisson_l_smooth(X, y, n_power_iters) + l_smooth_max = _glm_softplus_poisson_l_smooth_max(X, y) return l_smooth_max, l_smooth -def _softplus_poisson_l_smooth_multiply(X, y, v): +def _glm_softplus_poisson_l_smooth_multiply(X: jnp.ndarray, y: jnp.ndarray, v: jnp.ndarray): """ - Perform the multiplication of v with X.T @ D @ X without forming the full X.T @ D @ X. + Multiply vector `v` with the matrix X.T @ D @ X without forming it explicitly. - This assumes that X fits in memory. This estimate is based on calculating the hessian of the loss. + This method estimates the multiplication by calculating the Hessian of the loss. + It is efficient for situations where X can fit in memory. Parameters ---------- X : - Input data. + Input data matrix (N x d). y : - Output data. + Output data vector (N,). v : - d-dimensional vector. + Vector to be multiplied (d,). Returns ------- : - X.T @ D @ X @ v + Result of the multiplication (X.T @ D @ X) @ v. """ N, _ = X.shape return X.T.dot((0.17 * y + 0.25) * X.dot(v)) / N -def _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters: int = 20): +def _glm_softplus_poisson_l_smooth_with_power_iteration(X: jnp.ndarray, y: jnp.ndarray, n_power_iters: int = 20): """ - Instead of calculating X.T @ D @ X and its largest eigenvalue directly, - calculate it using the power method and by iterating through X and y, - forming a small product at a time. + Compute the largest eigenvalue of X.T @ D @ X using the power method. + + Instead of calculating the full matrix and its eigenvalue directly, this function + uses the power iteration method, which is more memory efficient and scales better + with large datasets. Parameters ---------- X : - Input data. + Input data matrix (N x d). y : - Output data. + Output data vector (N,). n_power_iters : - Number of power iterations. + Maximum number of power iterations to use. Returns ------- - The largest eigenvalue of X.T @ D @ X + : + The largest eigenvalue of X.T @ D @ X. """ - # key is fixed to random.key(0) _, d = X.shape - # initialize a random d-dimensional vector + # Initialize a random d-dimensional vector for power iteration v = jnp.ones((d,)) - # run the power iteration until convergence or the max steps + # Run power iteration to approximate the largest eigenvalue for _ in range(n_power_iters): v_prev = v.copy() - v = _softplus_poisson_l_smooth_multiply(X, y, v) + v = _glm_softplus_poisson_l_smooth_multiply(X, y, v) v /= v.max() + # Check for convergence if jnp.allclose(v_prev, v): break - # calculate the eigenvalue + # Final eigenvalue calculation v /= jnp.linalg.norm(v) - return _softplus_poisson_l_smooth_multiply(X, y, v).dot(v) + return _glm_softplus_poisson_l_smooth_multiply(X, y, v).dot(v) -def _softplus_poisson_l_smooth( - X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None -): +def _glm_softplus_poisson_l_smooth( + X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None +) -> jnp.ndarray: """ - Calculate the smoothness constant from data, assuming that the optimized - function is the log-likelihood of a Poisson GLM with a softplus inverse link function. + Calculate the smoothness constant `L` for a Poisson GLM with softplus inverse link. + + Depending on whether `n_power_iters` is provided, this function either computes + the largest eigenvalue directly or uses the power method. Parameters ---------- - X : - Input data. - y : - Output data. + X : jnp.ndarray + Input data matrix (N x d). + y : jnp.ndarray + Output data vector (N,). + n_power_iters : Optional[int], default None + Number of power iterations to use when finding the largest eigenvalue. If None, + the eigenvalue is calculated directly. Returns ------- - L : - Smoothness constant of f. + : + Smoothness constant `L`. """ if n_power_iters is None: - # calculate XDX/n and its largest eigenvalue directly + # Calculate the Hessian directly and find the largest eigenvalue XDX = X.T.dot((0.17 * y.reshape(y.shape[0], 1) + 0.25) * X) / y.shape[0] return jnp.sort(jnp.linalg.eigvalsh(XDX))[-1] else: - # use the power iteration to calculate the largest eigenvalue - return _softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters) + # Use power iteration to find the largest eigenvalue + return _glm_softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters) -def _softplus_poisson_l_smooth_max(X: jnp.ndarray, y: jnp.ndarray): +def _glm_softplus_poisson_l_smooth_max(X: jnp.ndarray, y: jnp.ndarray) -> jnp.ndarray: """ - Calculate the maximum smoothness constant from data, assuming that - the optimized function is the log-likelihood of a Poisson GLM with - a softplus inverse link function. + Calculate the maximum smoothness constant `L_max` for individual observations. + + This function estimates the maximum smoothness constant among the individual + components of the loss function. Parameters ---------- X : - Input data. + Input data matrix (N x d). y : - Output data. + Output data vector (N,). Returns ------- - L_max : - Maximum smoothness constant among f_{i}. + l_max : + Maximum smoothness constant `L_max`. """ N, _ = X.shape @@ -259,9 +297,9 @@ def body_fun(i, current_max): current_max, jnp.linalg.norm(X[i, :]) ** 2 * (0.17 * y[i] + 0.25) ) - L_max = jax.lax.fori_loop(0, N, body_fun, jnp.array([-jnp.inf])) + l_max = jax.lax.fori_loop(0, N, body_fun, jnp.array([-jnp.inf])) - return L_max[0] + return l_max[0] @_convert_to_float @@ -269,7 +307,7 @@ def _calculate_stepsize_svrg( batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float ): """ - Calculate optimal step size for SVRG$^{[1]}$. + Calculate optimal step size for SVRG according to [1]. Parameters ---------- @@ -305,7 +343,7 @@ def _calculate_stepsize_saga( batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float ) -> float: """ - Calculate optimal step size for SAGA. + Calculate optimal step size for SAGA according to [1]. Parameters ---------- @@ -343,25 +381,32 @@ def _calculate_optimal_batch_size_svrg( l_smooth: float, strong_convexity: Optional[float] = None, ) -> int: - """ - Calculate the optimal batch size according to "Table 1" in [1]. + r""" + Calculate the optimal batch size for SVRG based on theoretical guidelines. + + The batch size is computed according to the smoothness constants, strong convexity, + and number of samples, following the recommendations in Table 1 of [1]. Parameters ---------- num_samples: The number of samples. l_smooth_max: - The Lmax smoothness constant. + The $L\_{\text{max}}$ smoothness constant. l_smooth: - The L smoothness constant. + The $L$ smoothness constant. strong_convexity: The strong convexity constant. Returns ------- batch_size: - The batch size. + The optimal mini-batch size for SVRG. + References + ---------- + [1] Sebbouh, Othmane, et al. "Towards closing the gap between the theory and practice of SVRG." + Advances in neural information processing systems 32 (2019). """ if strong_convexity is None: # Assume that num_sample is large enough for mini-batching. @@ -395,21 +440,24 @@ def _calculate_optimal_batch_size_svrg( @_convert_to_float def _calculate_b_hat(num_samples: int, l_smooth_max: float, l_smooth: float): r""" - Helper function for calculating $\hat{b}$ in "Table 1" of [1]. + Calculate the optimal `b_hat` batch size parameter for SVRG. + + This is a helper function to compute the theoretical batch size $\hat{b}$, as detailed + in "Table 1" of [1]. Parameters ---------- num_samples : - Overall number of data points. + Total number of data points. l_smooth_max : - Maximum smoothness constant among f_{i}. + Maximum smoothness constant $L\_{\text{max}}$. l_smooth : - Smoothness constant. + Smoothness constant $L$. Returns ------- - : - Optimal batch size for the optimization. + float + Optimal batch size parameter `b_hat`. References ---------- @@ -424,23 +472,26 @@ def _calculate_b_hat(num_samples: int, l_smooth_max: float, l_smooth: float): @_convert_to_float def _calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity): r""" - Helper function for calculating $\tilde{b}$ as in "Table 1" of [1]. + Calculate the optimal $\tilde{b}$ batch size parameter for SVRG. + + This is a helper function to compute the theoretical batch size $\tilde{b}$, as detailed + in "Table 1" of [1]. Parameters ---------- num_samples : - Overall number of data points. + Total number of data points. l_smooth_max : - Maximum smoothness constant among f_{i}. + Maximum smoothness constant `L_max`. l_smooth : - Smoothness constant. + Smoothness constant `L`. strong_convexity : Strong convexity constant. Returns ------- : - Optimal batch size for the optimization. + Optimal batch size parameter `b_tilde`. References ---------- From d5baa02a0eb794c2892cfcc933b2a016c64ce512 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 12:06:06 +0200 Subject: [PATCH 13/52] started testing --- src/nemos/solvers/_svrg_defaults.py | 24 +++-- tests/test_svrg_defaults.py | 152 ++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 tests/test_svrg_defaults.py diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 14010e91..b19924f9 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -122,9 +122,9 @@ def svrg_optimal_batch_and_stepsize( if not jnp.isfinite(batch_size): batch_size = default_batch_size warnings.warn( - "Could not determine batch and step size automatically. " - f"Falling back on the default values of {batch_size} and {default_stepsize}.", - UserWarning + "Could not determine batch size automatically. " + f"Falling back on the default values of {default_batch_size}.", + UserWarning, ) # Compute optimal step size if not provided by the user @@ -132,7 +132,13 @@ def svrg_optimal_batch_and_stepsize( stepsize = _calculate_stepsize_svrg( batch_size, num_samples, l_smooth_max, l_smooth ) - + if stepsize < 0: + stepsize = default_stepsize + warnings.warn( + "Could not determine step size automatically. " + f"Falling back on the default value of {default_stepsize}.", + UserWarning, + ) return {"batch_size": int(batch_size), "stepsize": stepsize} @@ -171,7 +177,9 @@ def glm_softplus_poisson_l_max_and_l( return l_smooth_max, l_smooth -def _glm_softplus_poisson_l_smooth_multiply(X: jnp.ndarray, y: jnp.ndarray, v: jnp.ndarray): +def _glm_softplus_poisson_l_smooth_multiply( + X: jnp.ndarray, y: jnp.ndarray, v: jnp.ndarray +): """ Multiply vector `v` with the matrix X.T @ D @ X without forming it explicitly. @@ -196,7 +204,9 @@ def _glm_softplus_poisson_l_smooth_multiply(X: jnp.ndarray, y: jnp.ndarray, v: j return X.T.dot((0.17 * y + 0.25) * X.dot(v)) / N -def _glm_softplus_poisson_l_smooth_with_power_iteration(X: jnp.ndarray, y: jnp.ndarray, n_power_iters: int = 20): +def _glm_softplus_poisson_l_smooth_with_power_iteration( + X: jnp.ndarray, y: jnp.ndarray, n_power_iters: int = 20 +): """ Compute the largest eigenvalue of X.T @ D @ X using the power method. @@ -239,7 +249,7 @@ def _glm_softplus_poisson_l_smooth_with_power_iteration(X: jnp.ndarray, y: jnp.n def _glm_softplus_poisson_l_smooth( - X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None + X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None ) -> jnp.ndarray: """ Calculate the smoothness constant `L` for a Poisson GLM with softplus inverse link. diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py new file mode 100644 index 00000000..7c2a9603 --- /dev/null +++ b/tests/test_svrg_defaults.py @@ -0,0 +1,152 @@ +import pytest +import jax.numpy as jnp +from nemos.solvers import _svrg_defaults +from contextlib import nullcontext as does_not_raise + +# Define fixtures for X_sample and y_sample +@pytest.fixture +def X_sample(): + return jnp.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) + +@pytest.fixture +def y_sample(): + return jnp.array([1.0, 2.0, 3.0]) + +# Test _convert_to_float decorator +def test_convert_to_float_decorator(): + @ _svrg_defaults._convert_to_float + def sample_function(x): + return x + + result = sample_function(1) + assert isinstance(result, float) + +def test_svrg_optimal_batch_and_stepsize(X_sample, y_sample): + """Test calculation of optimal batch size and step size for SVRG.""" + result = _svrg_defaults.svrg_optimal_batch_and_stepsize( + _svrg_defaults.glm_softplus_poisson_l_max_and_l, X_sample, y_sample, strong_convexity=0.1 + ) + assert "batch_size" in result + assert "stepsize" in result + assert result["batch_size"] > 0 + assert result["stepsize"] > 0 + assert isinstance(result["batch_size"], int) + assert isinstance(result["stepsize"], float) + +def test_softplus_poisson_l_smooth_multiply(X_sample, y_sample): + """Test multiplication with X.T @ D @ X without forming the matrix.""" + v_sample = jnp.array([0.5, 1.0]) + result = _svrg_defaults._glm_softplus_poisson_l_smooth_multiply(X_sample, y_sample, v_sample) + diag_mat = jnp.diag(y_sample * 0.17 + 0.25) + expected_result = X_sample.T.dot(diag_mat).dot(X_sample.dot(v_sample)) / X_sample.shape[0] + assert jnp.allclose(result, expected_result) + +def test_softplus_poisson_l_smooth_with_power_iteration(X_sample, y_sample): + """Test the power iteration method for finding the largest eigenvalue.""" + result = _svrg_defaults._glm_softplus_poisson_l_smooth_with_power_iteration(X_sample, y_sample, n_power_iters=20) + # compute eigvals directly + diag_mat = jnp.diag(y_sample * 0.17 + 0.25) + XDX = X_sample.T.dot(diag_mat).dot(X_sample) / X_sample.shape[0] + eigmax = jnp.linalg.eigvalsh(XDX).max() + + assert result > 0 + assert jnp.allclose(eigmax, result) + +@pytest.mark.parametrize("batch_size", [1, 2, 10]) +@pytest.mark.parametrize("num_samples", [10, 12, 100, 500]) +@pytest.mark.parametrize("l_smooth_max", [0.1, 1., 10.]) +@pytest.mark.parametrize("l_smooth", [0.01, 0.05, 2.]) +def test_calculate_stepsize_svrg(batch_size, num_samples, l_smooth_max, l_smooth): + """Test calculation of the optimal step size for SVRG.""" + stepsize = _svrg_defaults._calculate_stepsize_svrg(batch_size, num_samples, l_smooth_max, l_smooth) + assert stepsize > 0 + assert isinstance(stepsize, float) + +@pytest.mark.parametrize("batch_size", [1, 2, 10]) +@pytest.mark.parametrize("num_samples", [10, 12, 100, 500]) +@pytest.mark.parametrize("l_smooth_max", [0.1, 1., 10.]) +@pytest.mark.parametrize("l_smooth", [0.01, 0.05, 2.]) +def test_calculate_stepsize_saga(batch_size, num_samples, l_smooth_max, l_smooth): + """Test calculation of the optimal step size for SAGA.""" + stepsize = _svrg_defaults._calculate_stepsize_saga(batch_size, num_samples, l_smooth_max, l_smooth) + assert stepsize > 0 + assert isinstance(stepsize, float) + +@pytest.mark.parametrize("num_samples", [12, 100, 500]) +@pytest.mark.parametrize("l_smooth_max", [0.1, 1., 10.]) +@pytest.mark.parametrize("l_smooth", [0.01, 0.05]) +@pytest.mark.parametrize("strong_convexity", [0.01, 1., 10.]) +def test_calculate_optimal_batch_size_svrg(num_samples, l_smooth_max, l_smooth, strong_convexity): + """Test calculation of the optimal batch size for SVRG.""" + batch_size = _svrg_defaults._calculate_optimal_batch_size_svrg(num_samples, l_smooth_max, l_smooth, strong_convexity) + assert batch_size > 0 + assert isinstance(batch_size, int) + +@pytest.mark.parametrize( + "num_samples, l_smooth_max, l_smooth, expected_b_hat", + [ + (100, 10.0, 2.0, 2.8697202), + ], +) +def test_calculate_b_hat(num_samples, l_smooth_max, l_smooth, expected_b_hat): + """Test calculation of b_hat for SVRG.""" + b_hat = _svrg_defaults._calculate_b_hat(num_samples, l_smooth_max, l_smooth) + assert jnp.isclose(b_hat, expected_b_hat) + +@pytest.mark.parametrize( + "num_samples, l_smooth_max, l_smooth, strong_convexity, expected_b_tilde", + [ + (100, 10.0, 2.0, 0.1, 3.4146341463414633), + ], +) +def test_calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity, expected_b_tilde): + """Test calculation of b_tilde for SVRG.""" + b_tilde = _svrg_defaults._calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity) + assert jnp.isclose(b_tilde, expected_b_tilde) + +@pytest.mark.parametrize( + "batch_size, stepsize, expected_batch_size, expected_stepsize", + [ + (32, 0.01, 32, 0.01), # Both batch_size and stepsize provided + (32, None, 32, None), # Only batch_size provided + (None, 0.01, None, 0.01), # Only stepsize provided + ] +) +def test_svrg_optimal_batch_and_stepsize_with_provided_defaults(batch_size, stepsize, expected_batch_size, + expected_stepsize, X_sample, y_sample): + """Test that provided defaults for batch_size and stepsize are returned as-is or computed correctly.""" + result = _svrg_defaults.svrg_optimal_batch_and_stepsize( + _svrg_defaults.glm_softplus_poisson_l_max_and_l, + X_sample, + y_sample, + batch_size=batch_size, + stepsize=stepsize + ) + if expected_batch_size is not None: + assert result["batch_size"] == expected_batch_size, "Provided batch_size should be returned as-is." + else: + assert "batch_size" in result and result["batch_size"] > 0, "Batch size should be computed since it was not provided." + if expected_stepsize is not None: + assert result["stepsize"] == expected_stepsize, "Provided stepsize should be returned as-is." + else: + assert "stepsize" in result and result["stepsize"] > 0, "Stepsize should be computed since it was not provided." + +@pytest.mark.parametrize( + "batch_size, stepsize, strong_convexity, expectation", + [ + (jnp.inf, None, 0.1, pytest.warns(UserWarning, match="Could not determine batch size automatically")), + (32, None, 0.1, pytest.warns(UserWarning, match="Could not determine step size automatically")), + (None, None, 0.1, does_not_raise()), + ] +) +def test_warnigns_svrg_optimal_batch_and_stepsize(batch_size, stepsize, strong_convexity, expectation, X_sample, y_sample): + """Test that warnings are correctly raised during SVRG optimization when appropriate.""" + with expectation: + _svrg_defaults.svrg_optimal_batch_and_stepsize( + _svrg_defaults.glm_softplus_poisson_l_max_and_l, + X_sample, + y_sample, + batch_size=batch_size, + stepsize=stepsize, + strong_convexity=strong_convexity + ) From deae6a1037e304256a6f7607ce05eb7b40871d48 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 12:08:16 +0200 Subject: [PATCH 14/52] changed naming --- tests/test_svrg_defaults.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index 7c2a9603..888c05d3 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -3,9 +3,9 @@ from nemos.solvers import _svrg_defaults from contextlib import nullcontext as does_not_raise -# Define fixtures for X_sample and y_sample + @pytest.fixture -def X_sample(): +def x_sample(): return jnp.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) @pytest.fixture @@ -21,10 +21,10 @@ def sample_function(x): result = sample_function(1) assert isinstance(result, float) -def test_svrg_optimal_batch_and_stepsize(X_sample, y_sample): +def test_svrg_optimal_batch_and_stepsize(x_sample, y_sample): """Test calculation of optimal batch size and step size for SVRG.""" result = _svrg_defaults.svrg_optimal_batch_and_stepsize( - _svrg_defaults.glm_softplus_poisson_l_max_and_l, X_sample, y_sample, strong_convexity=0.1 + _svrg_defaults.glm_softplus_poisson_l_max_and_l, x_sample, y_sample, strong_convexity=0.1 ) assert "batch_size" in result assert "stepsize" in result @@ -33,20 +33,20 @@ def test_svrg_optimal_batch_and_stepsize(X_sample, y_sample): assert isinstance(result["batch_size"], int) assert isinstance(result["stepsize"], float) -def test_softplus_poisson_l_smooth_multiply(X_sample, y_sample): +def test_softplus_poisson_l_smooth_multiply(x_sample, y_sample): """Test multiplication with X.T @ D @ X without forming the matrix.""" v_sample = jnp.array([0.5, 1.0]) - result = _svrg_defaults._glm_softplus_poisson_l_smooth_multiply(X_sample, y_sample, v_sample) + result = _svrg_defaults._glm_softplus_poisson_l_smooth_multiply(x_sample, y_sample, v_sample) diag_mat = jnp.diag(y_sample * 0.17 + 0.25) - expected_result = X_sample.T.dot(diag_mat).dot(X_sample.dot(v_sample)) / X_sample.shape[0] + expected_result = x_sample.T.dot(diag_mat).dot(x_sample.dot(v_sample)) / x_sample.shape[0] assert jnp.allclose(result, expected_result) -def test_softplus_poisson_l_smooth_with_power_iteration(X_sample, y_sample): +def test_softplus_poisson_l_smooth_with_power_iteration(x_sample, y_sample): """Test the power iteration method for finding the largest eigenvalue.""" - result = _svrg_defaults._glm_softplus_poisson_l_smooth_with_power_iteration(X_sample, y_sample, n_power_iters=20) + result = _svrg_defaults._glm_softplus_poisson_l_smooth_with_power_iteration(x_sample, y_sample, n_power_iters=20) # compute eigvals directly diag_mat = jnp.diag(y_sample * 0.17 + 0.25) - XDX = X_sample.T.dot(diag_mat).dot(X_sample) / X_sample.shape[0] + XDX = x_sample.T.dot(diag_mat).dot(x_sample) / x_sample.shape[0] eigmax = jnp.linalg.eigvalsh(XDX).max() assert result > 0 @@ -113,11 +113,11 @@ def test_calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity ] ) def test_svrg_optimal_batch_and_stepsize_with_provided_defaults(batch_size, stepsize, expected_batch_size, - expected_stepsize, X_sample, y_sample): + expected_stepsize, x_sample, y_sample): """Test that provided defaults for batch_size and stepsize are returned as-is or computed correctly.""" result = _svrg_defaults.svrg_optimal_batch_and_stepsize( _svrg_defaults.glm_softplus_poisson_l_max_and_l, - X_sample, + x_sample, y_sample, batch_size=batch_size, stepsize=stepsize @@ -139,12 +139,12 @@ def test_svrg_optimal_batch_and_stepsize_with_provided_defaults(batch_size, step (None, None, 0.1, does_not_raise()), ] ) -def test_warnigns_svrg_optimal_batch_and_stepsize(batch_size, stepsize, strong_convexity, expectation, X_sample, y_sample): +def test_warnigns_svrg_optimal_batch_and_stepsize(batch_size, stepsize, strong_convexity, expectation, x_sample, y_sample): """Test that warnings are correctly raised during SVRG optimization when appropriate.""" with expectation: _svrg_defaults.svrg_optimal_batch_and_stepsize( _svrg_defaults.glm_softplus_poisson_l_max_and_l, - X_sample, + x_sample, y_sample, batch_size=batch_size, stepsize=stepsize, From 496702afeefa7d6d3ddc7f8d65c5a1156913a540 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 12:33:06 +0200 Subject: [PATCH 15/52] linted --- tests/test_svrg_defaults.py | 119 ++++++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 31 deletions(-) diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index 888c05d3..8bddfc31 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -8,23 +8,28 @@ def x_sample(): return jnp.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) + @pytest.fixture def y_sample(): return jnp.array([1.0, 2.0, 3.0]) -# Test _convert_to_float decorator + def test_convert_to_float_decorator(): - @ _svrg_defaults._convert_to_float + @_svrg_defaults._convert_to_float def sample_function(x): return x result = sample_function(1) assert isinstance(result, float) + def test_svrg_optimal_batch_and_stepsize(x_sample, y_sample): """Test calculation of optimal batch size and step size for SVRG.""" result = _svrg_defaults.svrg_optimal_batch_and_stepsize( - _svrg_defaults.glm_softplus_poisson_l_max_and_l, x_sample, y_sample, strong_convexity=0.1 + _svrg_defaults.glm_softplus_poisson_l_max_and_l, + x_sample, + y_sample, + strong_convexity=0.1, ) assert "batch_size" in result assert "stepsize" in result @@ -33,17 +38,25 @@ def test_svrg_optimal_batch_and_stepsize(x_sample, y_sample): assert isinstance(result["batch_size"], int) assert isinstance(result["stepsize"], float) + def test_softplus_poisson_l_smooth_multiply(x_sample, y_sample): """Test multiplication with X.T @ D @ X without forming the matrix.""" v_sample = jnp.array([0.5, 1.0]) - result = _svrg_defaults._glm_softplus_poisson_l_smooth_multiply(x_sample, y_sample, v_sample) + result = _svrg_defaults._glm_softplus_poisson_l_smooth_multiply( + x_sample, y_sample, v_sample + ) diag_mat = jnp.diag(y_sample * 0.17 + 0.25) - expected_result = x_sample.T.dot(diag_mat).dot(x_sample.dot(v_sample)) / x_sample.shape[0] + expected_result = ( + x_sample.T.dot(diag_mat).dot(x_sample.dot(v_sample)) / x_sample.shape[0] + ) assert jnp.allclose(result, expected_result) + def test_softplus_poisson_l_smooth_with_power_iteration(x_sample, y_sample): """Test the power iteration method for finding the largest eigenvalue.""" - result = _svrg_defaults._glm_softplus_poisson_l_smooth_with_power_iteration(x_sample, y_sample, n_power_iters=20) + result = _svrg_defaults._glm_softplus_poisson_l_smooth_with_power_iteration( + x_sample, y_sample, n_power_iters=20 + ) # compute eigvals directly diag_mat = jnp.diag(y_sample * 0.17 + 0.25) XDX = x_sample.T.dot(diag_mat).dot(x_sample) / x_sample.shape[0] @@ -52,36 +65,48 @@ def test_softplus_poisson_l_smooth_with_power_iteration(x_sample, y_sample): assert result > 0 assert jnp.allclose(eigmax, result) + @pytest.mark.parametrize("batch_size", [1, 2, 10]) @pytest.mark.parametrize("num_samples", [10, 12, 100, 500]) -@pytest.mark.parametrize("l_smooth_max", [0.1, 1., 10.]) -@pytest.mark.parametrize("l_smooth", [0.01, 0.05, 2.]) +@pytest.mark.parametrize("l_smooth_max", [0.1, 1.0, 10.0]) +@pytest.mark.parametrize("l_smooth", [0.01, 0.05, 2.0]) def test_calculate_stepsize_svrg(batch_size, num_samples, l_smooth_max, l_smooth): """Test calculation of the optimal step size for SVRG.""" - stepsize = _svrg_defaults._calculate_stepsize_svrg(batch_size, num_samples, l_smooth_max, l_smooth) + stepsize = _svrg_defaults._calculate_stepsize_svrg( + batch_size, num_samples, l_smooth_max, l_smooth + ) assert stepsize > 0 assert isinstance(stepsize, float) + @pytest.mark.parametrize("batch_size", [1, 2, 10]) @pytest.mark.parametrize("num_samples", [10, 12, 100, 500]) -@pytest.mark.parametrize("l_smooth_max", [0.1, 1., 10.]) -@pytest.mark.parametrize("l_smooth", [0.01, 0.05, 2.]) +@pytest.mark.parametrize("l_smooth_max", [0.1, 1.0, 10.0]) +@pytest.mark.parametrize("l_smooth", [0.01, 0.05, 2.0]) def test_calculate_stepsize_saga(batch_size, num_samples, l_smooth_max, l_smooth): """Test calculation of the optimal step size for SAGA.""" - stepsize = _svrg_defaults._calculate_stepsize_saga(batch_size, num_samples, l_smooth_max, l_smooth) + stepsize = _svrg_defaults._calculate_stepsize_saga( + batch_size, num_samples, l_smooth_max, l_smooth + ) assert stepsize > 0 assert isinstance(stepsize, float) + @pytest.mark.parametrize("num_samples", [12, 100, 500]) -@pytest.mark.parametrize("l_smooth_max", [0.1, 1., 10.]) +@pytest.mark.parametrize("l_smooth_max", [0.1, 1.0, 10.0]) @pytest.mark.parametrize("l_smooth", [0.01, 0.05]) -@pytest.mark.parametrize("strong_convexity", [0.01, 1., 10.]) -def test_calculate_optimal_batch_size_svrg(num_samples, l_smooth_max, l_smooth, strong_convexity): +@pytest.mark.parametrize("strong_convexity", [0.01, 1.0, 10.0]) +def test_calculate_optimal_batch_size_svrg( + num_samples, l_smooth_max, l_smooth, strong_convexity +): """Test calculation of the optimal batch size for SVRG.""" - batch_size = _svrg_defaults._calculate_optimal_batch_size_svrg(num_samples, l_smooth_max, l_smooth, strong_convexity) + batch_size = _svrg_defaults._calculate_optimal_batch_size_svrg( + num_samples, l_smooth_max, l_smooth, strong_convexity + ) assert batch_size > 0 assert isinstance(batch_size, int) + @pytest.mark.parametrize( "num_samples, l_smooth_max, l_smooth, expected_b_hat", [ @@ -93,53 +118,85 @@ def test_calculate_b_hat(num_samples, l_smooth_max, l_smooth, expected_b_hat): b_hat = _svrg_defaults._calculate_b_hat(num_samples, l_smooth_max, l_smooth) assert jnp.isclose(b_hat, expected_b_hat) + @pytest.mark.parametrize( "num_samples, l_smooth_max, l_smooth, strong_convexity, expected_b_tilde", [ (100, 10.0, 2.0, 0.1, 3.4146341463414633), ], ) -def test_calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity, expected_b_tilde): +def test_calculate_b_tilde( + num_samples, l_smooth_max, l_smooth, strong_convexity, expected_b_tilde +): """Test calculation of b_tilde for SVRG.""" - b_tilde = _svrg_defaults._calculate_b_tilde(num_samples, l_smooth_max, l_smooth, strong_convexity) + b_tilde = _svrg_defaults._calculate_b_tilde( + num_samples, l_smooth_max, l_smooth, strong_convexity + ) assert jnp.isclose(b_tilde, expected_b_tilde) + @pytest.mark.parametrize( "batch_size, stepsize, expected_batch_size, expected_stepsize", [ (32, 0.01, 32, 0.01), # Both batch_size and stepsize provided (32, None, 32, None), # Only batch_size provided (None, 0.01, None, 0.01), # Only stepsize provided - ] + ], ) -def test_svrg_optimal_batch_and_stepsize_with_provided_defaults(batch_size, stepsize, expected_batch_size, - expected_stepsize, x_sample, y_sample): +def test_svrg_optimal_batch_and_stepsize_with_provided_defaults( + batch_size, stepsize, expected_batch_size, expected_stepsize, x_sample, y_sample +): """Test that provided defaults for batch_size and stepsize are returned as-is or computed correctly.""" result = _svrg_defaults.svrg_optimal_batch_and_stepsize( _svrg_defaults.glm_softplus_poisson_l_max_and_l, x_sample, y_sample, batch_size=batch_size, - stepsize=stepsize + stepsize=stepsize, ) if expected_batch_size is not None: - assert result["batch_size"] == expected_batch_size, "Provided batch_size should be returned as-is." + assert ( + result["batch_size"] == expected_batch_size + ), "Provided batch_size should be returned as-is." else: - assert "batch_size" in result and result["batch_size"] > 0, "Batch size should be computed since it was not provided." + assert ( + "batch_size" in result and result["batch_size"] > 0 + ), "Batch size should be computed since it was not provided." if expected_stepsize is not None: - assert result["stepsize"] == expected_stepsize, "Provided stepsize should be returned as-is." + assert ( + result["stepsize"] == expected_stepsize + ), "Provided stepsize should be returned as-is." else: - assert "stepsize" in result and result["stepsize"] > 0, "Stepsize should be computed since it was not provided." + assert ( + "stepsize" in result and result["stepsize"] > 0 + ), "Stepsize should be computed since it was not provided." + @pytest.mark.parametrize( "batch_size, stepsize, strong_convexity, expectation", [ - (jnp.inf, None, 0.1, pytest.warns(UserWarning, match="Could not determine batch size automatically")), - (32, None, 0.1, pytest.warns(UserWarning, match="Could not determine step size automatically")), + ( + jnp.inf, + None, + 0.1, + pytest.warns( + UserWarning, match="Could not determine batch size automatically" + ), + ), + ( + 32, + None, + 0.1, + pytest.warns( + UserWarning, match="Could not determine step size automatically" + ), + ), (None, None, 0.1, does_not_raise()), - ] + ], ) -def test_warnigns_svrg_optimal_batch_and_stepsize(batch_size, stepsize, strong_convexity, expectation, x_sample, y_sample): +def test_warnigns_svrg_optimal_batch_and_stepsize( + batch_size, stepsize, strong_convexity, expectation, x_sample, y_sample +): """Test that warnings are correctly raised during SVRG optimization when appropriate.""" with expectation: _svrg_defaults.svrg_optimal_batch_and_stepsize( @@ -148,5 +205,5 @@ def test_warnigns_svrg_optimal_batch_and_stepsize(batch_size, stepsize, strong_c y_sample, batch_size=batch_size, stepsize=stepsize, - strong_convexity=strong_convexity + strong_convexity=strong_convexity, ) From e084844c4cd2085cfbc3381cd14e6c24b1ca6427 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 13:04:14 +0200 Subject: [PATCH 16/52] added two missed lines for cov --- src/nemos/solvers/_svrg_defaults.py | 7 ++++--- tests/test_svrg_defaults.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index b19924f9..4b6d9db3 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -94,9 +94,6 @@ def svrg_optimal_batch_and_stepsize( >>> y = np.random.poisson(np.exp(X.dot(np.ones(X.shape[1])))) >>> batch_size, stepsize = compute_opt_params(glm_softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08) """ - # If both parameters are set by the user, return them directly - if batch_size is not None and stepsize is not None: - return {"batch_size": batch_size, "stepsize": stepsize} # Ensure data is converted to JAX arrays data = jax.tree_util.tree_map(jnp.asarray, data) @@ -107,6 +104,10 @@ def svrg_optimal_batch_and_stepsize( raise ValueError("Each array in data must have the same number of samples.") num_samples = num_samples.pop() + # If both parameters are set by the user, return them directly + if batch_size is not None and stepsize is not None: + return {"batch_size": batch_size, "stepsize": stepsize} + # Compute smoothness constants l_smooth_max, l_smooth = compute_smoothness_constants( *data, n_power_iters=n_power_iters diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index 8bddfc31..fe7e636d 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -207,3 +207,30 @@ def test_warnigns_svrg_optimal_batch_and_stepsize( stepsize=stepsize, strong_convexity=strong_convexity, ) + + +@pytest.mark.parametrize("n_power_iter", [None, 1, 10]) +def test_glm_softplus_poisson_l_smooth_power_iter(x_sample, y_sample, n_power_iter): + _svrg_defaults._glm_softplus_poisson_l_smooth(x_sample, y_sample, n_power_iter) + + +@pytest.mark.parametrize( + "delta_num_sample, expectation", + [ + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Each array in data must have the same number")) + ] + ) +def test_svrg_optimal_batch_and_stepsize_num_samples(x_sample, y_sample, delta_num_sample, expectation): + y_sample = y_sample[delta_num_sample:] + with expectation: + _svrg_defaults.svrg_optimal_batch_and_stepsize( + _svrg_defaults.glm_softplus_poisson_l_max_and_l, + x_sample, + y_sample, + batch_size=1, + stepsize=0.1, + strong_convexity=0.1, + ) + + From 0d36aaf00110b909971f94aa4daf503f343fd7ad Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 14:02:42 +0200 Subject: [PATCH 17/52] added test all table cases --- tests/test_svrg_defaults.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index fe7e636d..a8298f91 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -233,4 +233,30 @@ def test_svrg_optimal_batch_and_stepsize_num_samples(x_sample, y_sample, delta_n strong_convexity=0.1, ) - +@pytest.mark.parametrize( + "num_samples, l_smooth_max, l_smooth, strong_convexity, expected_batch_size", + [ + # Case 1: strong_convexity is None + (100, 10.0, 2.0, None, 1), # strong_convexity is None, should return batch_size = 1 + # Case 2: num_samples >= 3 * l_smooth_max / strong_convexity + (100, 10.0, 2.0, 0.8, 1), # num_samples >= 3 * l_smooth_max / strong_convexity, should return batch_size = 1 + # Case 3: num_samples > l_smooth / strong_convexity + (100, 10.0, 2.0, 0.1, 2), # num_samples > l_smooth / strong_convexity, and b_tilde is the minimum + # Case 4: l_smooth_max < num_samples * l_smooth / 3 and b_hat < b_tilde + (100, 5.0, 0.2, 0.1, 1), # l_smooth_max < num_samples * l_smooth / 3, use minimum(b_hat, b_tilde) + # Case 5: l_smooth_max >= num_samples * l_smooth / 3 + (100, 10.0, 0.2, 0.01, 27), # l_smooth_max >= num_samples * l_smooth / 3, batch_size = num_samples + # Case 6: l_smooth_max >= num_samples * l_smooth / 3, but falls back to b_tilde + (100, 5.0, 0.05, 0.1, 1), # l_smooth_max >= num_samples * l_smooth / 3, but falls back to b_tilde + # Case 7: l_smooth_max < num_samples * l_smooth / 3 + (100, 5.0, 0.5, 0.005, 4), + # Case 8: l_smooth_max > num_samples * l_smooth / 3 + (100, 18.0, 0.5, 0.005, 100), + ] +) +def test_calculate_optimal_batch_size_svrg(num_samples, l_smooth_max, l_smooth, strong_convexity, expected_batch_size): + """Test the calculation of the optimal batch size for SVRG.""" + batch_size = _svrg_defaults._calculate_optimal_batch_size_svrg( + num_samples, l_smooth_max, l_smooth, strong_convexity + ) + assert batch_size == expected_batch_size, f"Expected batch_size {expected_batch_size}, got {batch_size}" \ No newline at end of file From aada505145749ae4e81ea665120c2ee595901019 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 14:31:44 +0200 Subject: [PATCH 18/52] linted --- src/nemos/solvers/__init__.py | 4 ++-- src/nemos/solvers/_svrg.py | 3 +-- src/nemos/solvers/_svrg_defaults.py | 5 +++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/nemos/solvers/__init__.py b/src/nemos/solvers/__init__.py index cc71bd63..24943785 100644 --- a/src/nemos/solvers/__init__.py +++ b/src/nemos/solvers/__init__.py @@ -1,5 +1,5 @@ -from ._svrg import ProxSVRG, SVRG +from ._svrg import SVRG, ProxSVRG from ._svrg_defaults import ( - svrg_optimal_batch_and_stepsize, glm_softplus_poisson_l_max_and_l, + svrg_optimal_batch_and_stepsize, ) diff --git a/src/nemos/solvers/_svrg.py b/src/nemos/solvers/_svrg.py index da6059b4..e8fac968 100644 --- a/src/nemos/solvers/_svrg.py +++ b/src/nemos/solvers/_svrg.py @@ -1,5 +1,4 @@ -import warnings -from functools import partial, wraps +from functools import partial from typing import Callable, NamedTuple, Optional, Union import jax diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 4b6d9db3..682837f8 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -1,8 +1,9 @@ """Module for calculating theoretical optimal defaults for SVRG and GLM configurations.""" -from functools import wraps import warnings -from typing import Optional, Callable, Any, Tuple +from functools import wraps +from typing import Any, Callable, Optional, Tuple + import jax import jax.numpy as jnp From e9028a8d416977358a223b5d0791069733998e2b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 14:33:03 +0200 Subject: [PATCH 19/52] linted --- tests/test_basis.py | 2 +- tests/test_svrg_defaults.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_basis.py b/tests/test_basis.py index 6e81d142..20622dd2 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -4510,6 +4510,7 @@ def test_transformerbasis_exponentiation(basis_cls, exponent: int, error_type, e assert isinstance(trans_bas_exp, basis.TransformerBasis) assert isinstance(trans_bas_exp._basis, basis.MultiplicativeBasis) + @pytest.mark.parametrize( "basis_cls", [ @@ -4526,7 +4527,6 @@ def test_transformerbasis_dir(basis_cls): assert attr_name in dir(trans_bas) - @pytest.mark.parametrize( "basis_cls", [ diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index a8298f91..eed20f60 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -254,7 +254,7 @@ def test_svrg_optimal_batch_and_stepsize_num_samples(x_sample, y_sample, delta_n (100, 18.0, 0.5, 0.005, 100), ] ) -def test_calculate_optimal_batch_size_svrg(num_samples, l_smooth_max, l_smooth, strong_convexity, expected_batch_size): +def test_calculate_optimal_batch_size_svrg_all_config(num_samples, l_smooth_max, l_smooth, strong_convexity, expected_batch_size): """Test the calculation of the optimal batch size for SVRG.""" batch_size = _svrg_defaults._calculate_optimal_batch_size_svrg( num_samples, l_smooth_max, l_smooth, strong_convexity From b8801b509c9437a62ba8f5ec307a684f02a14153 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 16:59:00 +0200 Subject: [PATCH 20/52] added glm tests --- tests/test_glm.py | 181 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/tests/test_glm.py b/tests/test_glm.py index 003b9cdf..b690b8ae 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1,5 +1,6 @@ import warnings from contextlib import nullcontext as does_not_raise +import inspect from typing import Callable import jax @@ -1652,6 +1653,96 @@ def test_reg_strength_reset(self, reg): model.regularizer = "UnRegularized" assert model.regularizer_strength is None + @pytest.mark.parametrize( + "solver_name, reg", + [ + ("SVRG", "Ridge"), + ("SVRG", "UnRegularized"), + ("ProxSVRG", "Ridge"), + ("ProxSVRG", "UnRegularized"), + ("ProxSVRG", "Lasso"), + ("ProxSVRG", "GroupLasso") + ] + ) + @pytest.mark.parametrize( + "obs", + [ + nmo.observation_models.PoissonObservations(inverse_link_function=jax.nn.softplus) + ] + ) + @pytest.mark.parametrize("batch_size", [None, 1, 10]) + @pytest.mark.parametrize("stepsize", [None, 0.01]) + def test_glm_optimal_config_set(self, solver_name, batch_size, stepsize, reg, obs, poissonGLM_model_instantiation): + X, y, _, true_params, _ = poissonGLM_model_instantiation + model = nmo.glm.GLM( + solver_name=solver_name, + solver_kwargs=dict(batch_size=batch_size, stepsize=stepsize), + observation_model=obs, + regularizer=reg + ) + opt_state = model.initialize_state(X, y, true_params) + solver = inspect.getclosurevars(model._solver_run).nonlocals["solver"] + + if stepsize is not None: + assert opt_state.stepsize == stepsize + assert solver.stepsize == stepsize + else: + assert opt_state.stepsize > 0 + assert isinstance(opt_state.stepsize, float) + model.fit(X, y) + + if batch_size is not None: + assert solver.batch_size == batch_size + else: + assert isinstance(solver.batch_size, int) + assert solver.batch_size > 0 + model.fit(X, y) + + @pytest.mark.parametrize( + "solver_name, reg", + [ + ("SVRG", "Ridge"), + ("SVRG", "UnRegularized"), + ("ProxSVRG", "Ridge"), + ("ProxSVRG", "UnRegularized"), + ("ProxSVRG", "Lasso"), + ] + ) + @pytest.mark.parametrize( + "obs", + [ + nmo.observation_models.PoissonObservations(inverse_link_function=jax.nn.softplus) + ] + ) + @pytest.mark.parametrize("batch_size", [None, 1, 10]) + @pytest.mark.parametrize("stepsize", [None, 0.01]) + def test_glm_optimal_config_set_pytree(self, solver_name, batch_size, stepsize, reg, obs, poissonGLM_model_instantiation_pytree): + X, y, _, true_params, _ = poissonGLM_model_instantiation_pytree + model = nmo.glm.GLM( + solver_name=solver_name, + solver_kwargs=dict(batch_size=batch_size, stepsize=stepsize), + observation_model=obs, + regularizer=reg + ) + opt_state = model.initialize_state(X, y, true_params) + solver = inspect.getclosurevars(model._solver_run).nonlocals["solver"] + + if stepsize is not None: + assert opt_state.stepsize == stepsize + assert solver.stepsize == stepsize + else: + assert opt_state.stepsize > 0 + assert isinstance(opt_state.stepsize, float) + model.fit(X, y) + + if batch_size is not None: + assert solver.batch_size == batch_size + else: + assert isinstance(solver.batch_size, int) + assert solver.batch_size > 0 + model.fit(X, y) + + class TestPopulationGLM: """ @@ -3341,6 +3432,96 @@ def test_reg_strength_reset(self, reg): model.regularizer = "UnRegularized" assert model.regularizer_strength is None + @pytest.mark.parametrize( + "solver_name, reg", + [ + ("SVRG", "Ridge"), + ("SVRG", "UnRegularized"), + ("ProxSVRG", "Ridge"), + ("ProxSVRG", "UnRegularized"), + ("ProxSVRG", "Lasso"), + ("ProxSVRG", "GroupLasso") + ] + ) + @pytest.mark.parametrize( + "obs", + [ + nmo.observation_models.PoissonObservations(inverse_link_function=jax.nn.softplus) + ] + ) + @pytest.mark.parametrize("batch_size", [None, 1, 10]) + @pytest.mark.parametrize("stepsize", [None, 0.01]) + def test_glm_optimal_config_set(self, solver_name, batch_size, stepsize, reg, obs, poisson_population_GLM_model): + X, y, _, true_params, _ = poisson_population_GLM_model + model = nmo.glm.PopulationGLM( + solver_name=solver_name, + solver_kwargs=dict(batch_size=batch_size, stepsize=stepsize), + observation_model=obs, + regularizer=reg + ) + opt_state = model.initialize_state(X, y, true_params) + solver = inspect.getclosurevars(model._solver_run).nonlocals["solver"] + + if stepsize is not None: + assert opt_state.stepsize == stepsize + assert solver.stepsize == stepsize + else: + assert opt_state.stepsize > 0 + assert isinstance(opt_state.stepsize, float) + model.fit(X, y) + + if batch_size is not None: + assert solver.batch_size == batch_size + else: + assert isinstance(solver.batch_size, int) + assert solver.batch_size > 0 + model.fit(X, y) + + @pytest.mark.parametrize( + "solver_name, reg", + [ + ("SVRG", "Ridge"), + ("SVRG", "UnRegularized"), + ("ProxSVRG", "Ridge"), + ("ProxSVRG", "UnRegularized"), + ("ProxSVRG", "Lasso"), + ] + ) + @pytest.mark.parametrize( + "obs", + [ + nmo.observation_models.PoissonObservations(inverse_link_function=jax.nn.softplus) + ] + ) + @pytest.mark.parametrize("batch_size", [None, 1, 10]) + @pytest.mark.parametrize("stepsize", [None, 0.01]) + def test_glm_optimal_config_set_pytree(self, solver_name, batch_size, stepsize, reg, obs, + poisson_population_GLM_model_pytree): + X, y, _, true_params, _ = poisson_population_GLM_model_pytree + model = nmo.glm.PopulationGLM( + solver_name=solver_name, + solver_kwargs=dict(batch_size=batch_size, stepsize=stepsize), + observation_model=obs, + regularizer=reg + ) + opt_state = model.initialize_state(X, y, true_params) + solver = inspect.getclosurevars(model._solver_run).nonlocals["solver"] + + if stepsize is not None: + assert opt_state.stepsize == stepsize + assert solver.stepsize == stepsize + else: + assert opt_state.stepsize > 0 + assert isinstance(opt_state.stepsize, float) + model.fit(X, y) + + if batch_size is not None: + assert solver.batch_size == batch_size + else: + assert isinstance(solver.batch_size, int) + assert solver.batch_size > 0 + model.fit(X, y) + def test_optimal_config_all_required_keys_present(): """Test that all required keys are present in each configuration.""" From 7e8d57687ed673ce32cf77cfd42317ef62626157 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 16:59:40 +0200 Subject: [PATCH 21/52] linted --- tests/test_glm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index b690b8ae..968a5d03 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1,6 +1,6 @@ +import inspect import warnings from contextlib import nullcontext as does_not_raise -import inspect from typing import Callable import jax From 06b9f53ad027ddd22beb2520c9c5b35735adde0c Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 26 Aug 2024 17:55:21 +0200 Subject: [PATCH 22/52] improved glm docstrings --- src/nemos/glm.py | 66 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index b44bc27b..4f380900 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -111,10 +111,35 @@ class GLM(BaseRegressor): | Regularizer | Default Solver | Available Solvers | | ------------- | ---------------- | ----------------------------------------------------------- | - | UnRegularized | GradientDescent | GradientDescent, BFGS, LBFGS, NonlinearCG, ProximalGradient | - | Ridge | GradientDescent | GradientDescent, BFGS, LBFGS, NonlinearCG, ProximalGradient | - | Lasso | ProximalGradient | ProximalGradient | - | GroupLasso | ProximalGradient | ProximalGradient | + | UnRegularized | GradientDescent | GradientDescent, BFGS, LBFGS, NonlinearCG, SVRG, ProximalGradient, ProxSVRG | + | Ridge | GradientDescent | GradientDescent, BFGS, LBFGS, NonlinearCG, SVRG, ProximalGradient, ProxSVRG | + | Lasso | ProximalGradient | ProximalGradient, ProxSVRG | + | GroupLasso | ProximalGradient | ProximalGradient, , ProxSVRG | + + + **Fitting Large Models** + + For very large models, you may consider using the Stochastic Variance Reduced Gradient + ([SVRG](../solvers/_svrg/#nemos.solvers._svrg.SVRG)) or its proximal variant + ([ProxSVRG](../solvers/_svrg/#nemos.solvers._svrg.ProxSVRG)) solver, + which take advantage of batched computation. + + The performance of the SVRG solver depends critically on the choice of `batch_size` and `stepsize` + hyperparameters. These parameters control the size of the mini-batches used for gradient computations + and the step size for each iteration, respectively. Improper selection of these parameters can lead to slow + convergence or even divergence of the optimization process. + + To assist with this, for certain GLM configurations, we provide recommended `batch_size` and `stepsize` + values that are theoretically guaranteed to ensure fast convergence. + + Below is a list of the configurations for which we can provide guaranteed hyperparameters: + + | GLM / PopulationGLM Configuration | Stepsize | Batch Size | + | --------------------------------- | :------: | :---------: | + | Poisson + soft-plus + UnRegularized | ✅ | ❌ | + | Poisson + soft-plus + Ridge | ✅ | ✅ | + | Poisson + soft-plus + Lasso | ✅ | ❌ | + | Poisson + soft-plus + GroupLasso | ✅ | ❌ | Parameters ---------- @@ -1134,10 +1159,35 @@ class PopulationGLM(GLM): | Regularizer | Default Solver | Available Solvers | | ------------- | ---------------- | ----------------------------------------------------------- | - | UnRegularized | GradientDescent | GradientDescent, BFGS, LBFGS, NonlinearCG, ProximalGradient | - | Ridge | GradientDescent | GradientDescent, BFGS, LBFGS, NonlinearCG, ProximalGradient | - | Lasso | ProximalGradient | ProximalGradient | - | GroupLasso | ProximalGradient | ProximalGradient | + | UnRegularized | GradientDescent | GradientDescent, BFGS, LBFGS, NonlinearCG, SVRG, ProximalGradient, ProxSVRG | + | Ridge | GradientDescent | GradientDescent, BFGS, LBFGS, NonlinearCG, SVRG, ProximalGradient, ProxSVRG | + | Lasso | ProximalGradient | ProximalGradient, ProxSVRG | + | GroupLasso | ProximalGradient | ProximalGradient, ProxSVRG | + + + **Fitting Large Models** + + For very large models, you may consider using the Stochastic Variance Reduced Gradient + ([SVRG](../solvers/_svrg/#nemos.solvers._svrg.SVRG)) or its proximal variant + ([ProxSVRG](../solvers/_svrg/#nemos.solvers._svrg.ProxSVRG)) solver, + which take advantage of batched computation. + + The performance of the SVRG solver depends critically on the choice of `batch_size` and `stepsize` + hyperparameters. These parameters control the size of the mini-batches used for gradient computations + and the step size for each iteration, respectively. Improper selection of these parameters can lead to slow + convergence or even divergence of the optimization process. + + To assist with this, for certain GLM configurations, we provide recommended `batch_size` and `stepsize` + values that are theoretically guaranteed to ensure fast convergence. + + Below is a list of the configurations for which we can provide guaranteed hyperparameters: + + | GLM / PopulationGLM Configuration | Stepsize | Batch Size | + | --------------------------------- | :------: | :---------: | + | Poisson + soft-plus + UnRegularized | ✅ | ❌ | + | Poisson + soft-plus + Ridge | ✅ | ✅ | + | Poisson + soft-plus + Lasso | ✅ | ❌ | + | Poisson + soft-plus + GroupLasso | ✅ | ❌ | Parameters ---------- From cae93acc63af8f20146e7422bcdc90692acd0c2f Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 27 Aug 2024 10:58:39 +0200 Subject: [PATCH 23/52] removed args from docstrings --- src/nemos/basis.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/nemos/basis.py b/src/nemos/basis.py index f8608fb4..2cc48f95 100644 --- a/src/nemos/basis.py +++ b/src/nemos/basis.py @@ -464,9 +464,6 @@ class Basis(Base, abc.ABC): The bounds for the basis domain in `mode="eval"`. The default `bounds[0]` and `bounds[1]` are the minimum and the maximum of the samples provided when evaluating the basis. If a sample is outside the bonuds, the basis will return NaN. - *args : - Only used in "conv" mode. Additional positional arguments that are passed to - `nemos.convolve.create_convolutional_predictor` **kwargs : Only used in "conv" mode. Additional keyword arguments that are passed to `nemos.convolve.create_convolutional_predictor` From 35622bdad87cd43b665f340cfc51bb125a13d9a6 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 1 Oct 2024 15:05:29 -0400 Subject: [PATCH 24/52] added billy's comments --- src/nemos/glm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 0a5c3be5..f5aed678 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -28,7 +28,7 @@ ModelParams = Tuple[jnp.ndarray, jnp.ndarray] - +# make this a helper func in the _OPTIMAL_CONFIGURATIONS = { "SVRG": [ { @@ -38,8 +38,8 @@ "observation_model": lambda x: isinstance(x, obs.PoissonObservations), "regularizer": lambda x: not isinstance(x, Ridge), }, - "compute_l_smooth": glm_softplus_poisson_l_max_and_l, - "compute_defaults": svrg_optimal_batch_and_stepsize, + "compute_l_smooth": glm_softplus_poisson_l_max_and_l, # rename with the shared part at the beginning + "compute_defaults": svrg_optimal_batch_and_stepsize, # rename with the shared part at the beginning "strong_convexity": None, }, { From 8f33e7f0665041bbef43b812646407af636592c4 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 8 Oct 2024 12:58:17 -0400 Subject: [PATCH 25/52] added tests for auto-stepsize --- src/nemos/base_regressor.py | 4 +- src/nemos/glm.py | 120 ++----- src/nemos/solvers/_compute_defaults.py | 71 ++++ src/nemos/solvers/_svrg_defaults.py | 1 + tests/test_glm.py | 478 ++++++++++++++++++++++--- 5 files changed, 518 insertions(+), 156 deletions(-) create mode 100644 src/nemos/solvers/_compute_defaults.py diff --git a/src/nemos/base_regressor.py b/src/nemos/base_regressor.py index ac0c528f..2a97e3e0 100644 --- a/src/nemos/base_regressor.py +++ b/src/nemos/base_regressor.py @@ -228,7 +228,7 @@ def solver_name(self, solver_name: str): if solver_name not in self._regularizer.allowed_solvers: raise ValueError( f"The solver: {solver_name} is not allowed for " - f"{self._regularizer.__class__.__name__} regularizaration. Allowed solvers are " + f"{self._regularizer.__class__.__name__} regularization. Allowed solvers are " f"{self._regularizer.allowed_solvers}." ) self._solver_name = solver_name @@ -304,7 +304,7 @@ def instantiate_solver( if self.solver_name not in self.regularizer.allowed_solvers: raise ValueError( f"The solver: {self.solver_name} is not allowed for " - f"{self._regularizer.__class__.__name__} regularizaration. Allowed solvers are " + f"{self._regularizer.__class__.__name__} regularization. Allowed solvers are " f"{self._regularizer.allowed_solvers}." ) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 6d0a494a..070e505c 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -19,67 +19,12 @@ from .exceptions import NotFittedError from .pytrees import FeaturePytree from .regularizer import GroupLasso, Lasso, Regularizer, Ridge -from .solvers._svrg_defaults import ( - glm_softplus_poisson_l_max_and_l, - svrg_optimal_batch_and_stepsize, -) +from .solvers._compute_defaults import glm_compute_optimal_stepsize_configs from .type_casting import jnp_asarray_if, support_pynapple from .typing import DESIGN_INPUT_TYPE ModelParams = Tuple[jnp.ndarray, jnp.ndarray] -# make this a helper func in the -_OPTIMAL_CONFIGURATIONS = { - "SVRG": [ - { - "required_params": { - "observation_model__inverse_link_function": lambda x: x - == jax.nn.softplus, - "observation_model": lambda x: isinstance(x, obs.PoissonObservations), - "regularizer": lambda x: not isinstance(x, Ridge), - }, - "compute_l_smooth": glm_softplus_poisson_l_max_and_l, # rename with the shared part at the beginning - "compute_defaults": svrg_optimal_batch_and_stepsize, # rename with the shared part at the beginning - "strong_convexity": None, - }, - { - "required_params": { - "observation_model__inverse_link_function": lambda x: x - == jax.nn.softplus, - "observation_model": lambda x: isinstance(x, obs.PoissonObservations), - "regularizer": lambda x: isinstance(x, Ridge), - }, - "compute_l_smooth": glm_softplus_poisson_l_max_and_l, - "compute_defaults": svrg_optimal_batch_and_stepsize, - "strong_convexity": "regularizer_strength", - }, - ], - "ProxSVRG": [ - { - "required_params": { - "observation_model__inverse_link_function": lambda x: x - == jax.nn.softplus, - "observation_model": lambda x: isinstance(x, obs.PoissonObservations), - "regularizer": lambda x: not isinstance(x, Ridge), - }, - "compute_l_smooth": glm_softplus_poisson_l_max_and_l, - "compute_defaults": svrg_optimal_batch_and_stepsize, - "strong_convexity": None, - }, - { - "required_params": { - "observation_model__inverse_link_function": lambda x: x - == jax.nn.softplus, - "observation_model": lambda x: isinstance(x, obs.PoissonObservations), - "regularizer": lambda x: isinstance(x, Ridge), - }, - "compute_l_smooth": glm_softplus_poisson_l_max_and_l, - "compute_defaults": svrg_optimal_batch_and_stepsize, - "strong_convexity": "regularizer_strength", - }, - ], -} - def cast_to_jax(func): """Cast argument to jax.""" @@ -1104,50 +1049,27 @@ def optimize_solver_params(self, X, y): # Start with a copy of the existing solver parameters new_solver_kwargs = self.solver_kwargs.copy() - # Check if the current solver has any known optimal configurations - if self.solver_name in _OPTIMAL_CONFIGURATIONS: - # Retrieve the list of configurations for the solver - configs = _OPTIMAL_CONFIGURATIONS[self.solver_name] - # Get the model parameters to check against the required conditions - model_params = self.get_params() - - # Iterate through each configuration - for conf in configs: - # Determine if the current model matches the configuration's requirements - known_config = all( - [ - check(model_params[key]) - for key, check in conf["required_params"].items() - ] - ) - - if known_config: - # Extract the function to compute optimal defaults - compute_defaults = conf["compute_defaults"] - - # Extract strong convexity if it is relevant for this configuration - strong_convexity = ( - model_params[conf["strong_convexity"]] - if conf["strong_convexity"] - else None - ) - - # Check if the user has provided batch size or stepsize, or else use None - batch_size = new_solver_kwargs.get("batch_size", None) - stepsize = new_solver_kwargs.get("stepsize", None) - - # Compute the optimal batch size and stepsize based on smoothness, strong convexity, etc. - new_params = compute_defaults( - conf["compute_l_smooth"], - X, - y, - batch_size=batch_size, - stepsize=stepsize, - strong_convexity=strong_convexity, - ) + # get the model specific configs + compute_defaults, compute_l_smooth, strong_convexity = ( + glm_compute_optimal_stepsize_configs(self) + ) + if compute_defaults and compute_l_smooth: + # Check if the user has provided batch size or stepsize, or else use None + batch_size = new_solver_kwargs.get("batch_size", None) + stepsize = new_solver_kwargs.get("stepsize", None) + + # Compute the optimal batch size and stepsize based on smoothness, strong convexity, etc. + new_params = compute_defaults( + compute_l_smooth, + X, + y, + batch_size=batch_size, + stepsize=stepsize, + strong_convexity=strong_convexity, + ) - # Update the solver parameters with the computed optimal values - new_solver_kwargs.update(new_params) + # Update the solver parameters with the computed optimal values + new_solver_kwargs.update(new_params) return new_solver_kwargs diff --git a/src/nemos/solvers/_compute_defaults.py b/src/nemos/solvers/_compute_defaults.py new file mode 100644 index 00000000..5bf94fca --- /dev/null +++ b/src/nemos/solvers/_compute_defaults.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, Optional, Tuple, Union + +import jax + +from ..observation_models import PoissonObservations +from ..regularizer import Ridge +from ._svrg_defaults import ( + glm_softplus_poisson_l_max_and_l, + svrg_optimal_batch_and_stepsize, +) + +if TYPE_CHECKING: + from ..glm import GLM, PopulationGLM + + +def glm_compute_optimal_stepsize_configs( + model: Union[GLM, PopulationGLM] +) -> Tuple[Optional[Callable], Optional[Callable], Optional[float]]: + """ + Compute configuration functions for optimal step size selection based on the model. + + This function returns a tuple of three elements that are used for configuring the + optimal step size and batch size for stochastic variance reduced gradient (SVRG and + ProxSVRG) algorithms. If the model is configured with specific solver names, + the appropriate computation functions are returned. Additionally, it determines the + smoothness and strong convexity constants based on the model's observation and regularizer. + + Parameters + ---------- + model : + The generalized linear model object for which the optimal step size and batch + configuration need to be computed. The model should have attributes like + `solver_name`, `observation_model`, and `regularizer`. + + Returns + ------- + compute_optimal_params : + A function to compute the optimal batch size and step size if the model + is configured with the SVRG or ProxSVRG solver, None otherwise. + + compute_smoothness : + A function to compute the smoothness constant of the loss function if the + observation model uses a softplus inverse link function and is a Poisson + observation model, None otherwise. + + strong_convexity : + The strong convexity constant of the loss function if the model has a + Ridge regularizer. If the model does not have a Ridge regularizer, this + value will be None. + + """ + # initialize funcs and strong convexity constant + compute_optimal_params = None + compute_smoothness = None + strong_convexity = ( + None if not isinstance(model.regularizer, Ridge) else model.regularizer_strength + ) + + # look-up table for selecting the optimal step and batch + if model.solver_name in ("SVRG", "ProxSVRG"): + compute_optimal_params = svrg_optimal_batch_and_stepsize + + # get the smoothness parameter compute function + if model.observation_model.inverse_link_function is jax.nn.softplus and isinstance( + model.observation_model, PoissonObservations + ): + compute_smoothness = glm_softplus_poisson_l_max_and_l + + return compute_optimal_params, compute_smoothness, strong_convexity diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 682837f8..50136606 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -1,5 +1,6 @@ """Module for calculating theoretical optimal defaults for SVRG and GLM configurations.""" +import copy import warnings from functools import wraps from typing import Any, Callable, Optional, Tuple diff --git a/tests/test_glm.py b/tests/test_glm.py index 59f8ccfb..8ab67adf 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1794,7 +1794,7 @@ def test_reg_set_params_reg_str_only(self, params, warns, reg): ) @pytest.mark.parametrize("batch_size", [None, 1, 10]) @pytest.mark.parametrize("stepsize", [None, 0.01]) - def test_glm_optimal_config_set( + def test_glm_optimal_config_set_initial_state( self, solver_name, batch_size, @@ -1848,7 +1848,7 @@ def test_glm_optimal_config_set( ) @pytest.mark.parametrize("batch_size", [None, 1, 10]) @pytest.mark.parametrize("stepsize", [None, 0.01]) - def test_glm_optimal_config_set_pytree( + def test_glm_optimal_config_set_initial_state_pytree( self, solver_name, batch_size, @@ -1882,6 +1882,178 @@ def test_glm_optimal_config_set_pytree( assert solver.batch_size > 0 model.fit(X, y) + @pytest.mark.parametrize("batch_size", [None, 1, 10]) + @pytest.mark.parametrize("stepsize", [None, 0.01]) + @pytest.mark.parametrize( + "regularizer", ["UnRegularized", "Ridge", "Lasso", "GroupLasso"] + ) + @pytest.mark.parametrize( + "solver_name, has_dafaults", + [ + ("GradientDescent", False), + ("LBFGS", False), + ("ProximalGradient", False), + ("SVRG", True), + ("ProxSVRG", True), + ], + ) + @pytest.mark.parametrize( + "inv_link, link_has_defaults", [(jax.nn.softplus, True), (jax.numpy.exp, False)] + ) + @pytest.mark.parametrize( + "observation_model, obs_has_defaults", + [ + (nmo.observation_models.PoissonObservations, True), + (nmo.observation_models.GammaObservations, False), + ], + ) + def test_optimize_solver_params( + self, + batch_size, + stepsize, + regularizer, + solver_name, + inv_link, + observation_model, + has_dafaults, + link_has_defaults, + obs_has_defaults, + poissonGLM_model_instantiation, + ): + """Test the behavior of `optimize_solver_params` for different solver, regularizer, and observation model configurations.""" + obs = observation_model(inverse_link_function=inv_link) + X, y, _, _, _ = poissonGLM_model_instantiation + solver_kwargs = dict(stepsize=stepsize, batch_size=batch_size) + # use glm static methods to check if the solver is batchable + # if not pop the batch_size kwarg + try: + slv_class = nmo.glm.GLM._get_solver_class(solver_name) + nmo.glm.GLM._check_solver_kwargs(slv_class, solver_kwargs) + except NameError: + solver_kwargs.pop("batch_size") + + # if the regularizer is not allowed for the solver type, return + try: + model = nmo.glm.GLM( + regularizer=regularizer, + solver_name=solver_name, + observation_model=obs, + solver_kwargs=solver_kwargs, + ) + except ValueError as e: + if not str(e).startswith( + rf"The solver: {solver_name} is not allowed for {regularizer} regularization" + ): + raise e + return + + kwargs = model.optimize_solver_params(X, y) + if isinstance(batch_size, int) and "batch_size" in solver_kwargs: + # if batch size was provided, then it should be returned unchanged + assert batch_size == kwargs["batch_size"] + elif has_dafaults and link_has_defaults and obs_has_defaults: + # if defaults are available, a batch size is computed + assert isinstance(kwargs["batch_size"], int) and kwargs["batch_size"] > 0 + elif "batch_size" in solver_kwargs: + # return None otherwise + assert isinstance(kwargs["batch_size"], type(None)) + + if isinstance(stepsize, float): + # if stepsize was provided, then it should be returned unchanged + assert stepsize == kwargs["stepsize"] + elif has_dafaults and link_has_defaults and obs_has_defaults: + # if defaults are available, compute a value + assert isinstance(kwargs["stepsize"], float) and kwargs["stepsize"] > 0 + else: + # return None otherwise + assert isinstance(kwargs["stepsize"], type(None)) + + @pytest.mark.parametrize("batch_size", [None, 1, 10]) + @pytest.mark.parametrize("stepsize", [None, 0.01]) + @pytest.mark.parametrize( + "regularizer", ["UnRegularized", "Ridge", "Lasso", "GroupLasso"] + ) + @pytest.mark.parametrize( + "solver_name, has_dafaults", + [ + ("GradientDescent", False), + ("LBFGS", False), + ("ProximalGradient", False), + ("SVRG", True), + ("ProxSVRG", True), + ], + ) + @pytest.mark.parametrize( + "inv_link, link_has_defaults", [(jax.nn.softplus, True), (jax.numpy.exp, False)] + ) + @pytest.mark.parametrize( + "observation_model, obs_has_defaults", + [ + (nmo.observation_models.PoissonObservations, True), + (nmo.observation_models.GammaObservations, False), + ], + ) + def test_optimize_solver_params_pytree( + self, + batch_size, + stepsize, + regularizer, + solver_name, + inv_link, + observation_model, + has_dafaults, + link_has_defaults, + obs_has_defaults, + poissonGLM_model_instantiation_pytree, + ): + """Test the behavior of `optimize_solver_params` for different solver, regularizer, and observation model configurations.""" + obs = observation_model(inverse_link_function=inv_link) + X, y, _, _, _ = poissonGLM_model_instantiation_pytree + solver_kwargs = dict(stepsize=stepsize, batch_size=batch_size) + # use glm static methods to check if the solver is batchable + # if not pop the batch_size kwarg + try: + slv_class = nmo.glm.GLM._get_solver_class(solver_name) + nmo.glm.GLM._check_solver_kwargs(slv_class, solver_kwargs) + except NameError: + solver_kwargs.pop("batch_size") + + # if the regularizer is not allowed for the solver type, return + try: + model = nmo.glm.GLM( + regularizer=regularizer, + solver_name=solver_name, + observation_model=obs, + solver_kwargs=solver_kwargs, + ) + except ValueError as e: + if not str(e).startswith( + rf"The solver: {solver_name} is not allowed for {regularizer} regularization" + ): + raise e + return + + kwargs = model.optimize_solver_params(X, y) + if isinstance(batch_size, int) and "batch_size" in solver_kwargs: + # if batch size was provided, then it should be returned unchanged + assert batch_size == kwargs["batch_size"] + elif has_dafaults and link_has_defaults and obs_has_defaults: + # if defaults are available, a batch size is computed + assert isinstance(kwargs["batch_size"], int) and kwargs["batch_size"] > 0 + elif "batch_size" in solver_kwargs: + # return None otherwise + assert isinstance(kwargs["batch_size"], type(None)) + + if isinstance(stepsize, float): + # if stepsize was provided, then it should be returned unchanged + assert stepsize == kwargs["stepsize"] + elif has_dafaults and link_has_defaults and obs_has_defaults: + # if defaults are available, compute a value + assert isinstance(kwargs["stepsize"], float) and kwargs["stepsize"] > 0 + else: + # return None otherwise + assert isinstance(kwargs["stepsize"], type(None)) + class TestPopulationGLM: """ @@ -3610,7 +3782,7 @@ def test_reg_strength_reset(self, reg): ) @pytest.mark.parametrize("batch_size", [None, 1, 10]) @pytest.mark.parametrize("stepsize", [None, 0.01]) - def test_glm_optimal_config_set( + def test_glm_optimal_config_set_initial_state( self, solver_name, batch_size, stepsize, reg, obs, poisson_population_GLM_model ): X, y, _, true_params, _ = poisson_population_GLM_model @@ -3658,7 +3830,7 @@ def test_glm_optimal_config_set( ) @pytest.mark.parametrize("batch_size", [None, 1, 10]) @pytest.mark.parametrize("stepsize", [None, 0.01]) - def test_glm_optimal_config_set_pytree( + def test_glm_optimal_config_set_initial_state_pytree( self, solver_name, batch_size, @@ -3692,57 +3864,6 @@ def test_glm_optimal_config_set_pytree( assert solver.batch_size > 0 model.fit(X, y) - def test_optimal_config_all_required_keys_present(): - """Test that all required keys are present in each configuration.""" - required_keys = [ - "required_params", - "compute_l_smooth", - "compute_defaults", - "strong_convexity", - ] - for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): - for config in configs: - for key in required_keys: - assert ( - key in config - ), f"Configuration for solver '{solver}' is missing the required key: '{key}'." - - def test_optimal_config_required_params_is_dict(): - """Test that 'required_params' is a dictionary.""" - for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): - for config in configs: - assert isinstance( - config["required_params"], dict - ), f"'required_params' should be a dictionary in the configuration for solver '{solver}'." - - def test_optimal_config_compute_l_smooth_is_callable(): - """Test that 'compute_l_smooth' is a callable function.""" - for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): - for config in configs: - assert callable( - config["compute_l_smooth"] - ), f"'compute_l_smooth' should be callable in the configuration for solver '{solver}'." - - def test_optimal_config_compute_defaults_is_callable(): - """Test that 'compute_defaults' is a callable function.""" - for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): - for config in configs: - assert callable( - config["compute_defaults"] - ), f"'compute_defaults' should be callable in the configuration for solver '{solver}'." - - def test_optimal_config_strong_convexity_is_valid_type(): - """Test that 'strong_convexity' is either None, a string, or callable.""" - for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): - for config in configs: - strong_convexity = config["strong_convexity"] - assert strong_convexity is None or isinstance( - strong_convexity, (str, type(None)) - ), ( - f"'strong_convexity' should be either None, a string, or callable in the " - f"configuration for solver '{solver}'." - ) - @pytest.mark.parametrize( "params, warns", [ @@ -3836,3 +3957,250 @@ def test_reg_set_params_reg_str_only(self, params, warns, reg): model = nmo.glm.PopulationGLM(regularizer=reg, regularizer_strength=1) with warns: model.set_params(**params) + + @pytest.mark.parametrize("batch_size", [None, 1, 10]) + @pytest.mark.parametrize("stepsize", [None, 0.01]) + @pytest.mark.parametrize( + "regularizer", ["UnRegularized", "Ridge", "Lasso", "GroupLasso"] + ) + @pytest.mark.parametrize( + "solver_name, has_dafaults", + [ + ("GradientDescent", False), + ("LBFGS", False), + ("ProximalGradient", False), + ("SVRG", True), + ("ProxSVRG", True), + ], + ) + @pytest.mark.parametrize( + "inv_link, link_has_defaults", [(jax.nn.softplus, True), (jax.numpy.exp, False)] + ) + @pytest.mark.parametrize( + "observation_model, obs_has_defaults", + [ + (nmo.observation_models.PoissonObservations, True), + (nmo.observation_models.GammaObservations, False), + ], + ) + def test_optimize_solver_params( + self, + batch_size, + stepsize, + regularizer, + solver_name, + inv_link, + observation_model, + has_dafaults, + link_has_defaults, + obs_has_defaults, + poisson_population_GLM_model, + ): + """Test the behavior of `optimize_solver_params` for different solver, regularizer, and observation model configurations.""" + obs = observation_model(inverse_link_function=inv_link) + X, y, _, _, _ = poisson_population_GLM_model + solver_kwargs = dict(stepsize=stepsize, batch_size=batch_size) + # use glm static methods to check if the solver is batchable + # if not pop the batch_size kwarg + try: + slv_class = nmo.glm.PopulationGLM._get_solver_class(solver_name) + nmo.glm.PopulationGLM._check_solver_kwargs(slv_class, solver_kwargs) + except NameError: + solver_kwargs.pop("batch_size") + + # if the regularizer is not allowed for the solver type, return + try: + model = nmo.glm.PopulationGLM( + regularizer=regularizer, + solver_name=solver_name, + observation_model=obs, + solver_kwargs=solver_kwargs, + ) + except ValueError as e: + if not str(e).startswith( + rf"The solver: {solver_name} is not allowed for {regularizer} regularization" + ): + raise e + return + + kwargs = model.optimize_solver_params(X, y) + if isinstance(batch_size, int) and "batch_size" in solver_kwargs: + # if batch size was provided, then it should be returned unchanged + assert batch_size == kwargs["batch_size"] + elif has_dafaults and link_has_defaults and obs_has_defaults: + # if defaults are available, a batch size is computed + assert isinstance(kwargs["batch_size"], int) and kwargs["batch_size"] > 0 + elif "batch_size" in solver_kwargs: + # return None otherwise + assert isinstance(kwargs["batch_size"], type(None)) + + if isinstance(stepsize, float): + # if stepsize was provided, then it should be returned unchanged + assert stepsize == kwargs["stepsize"] + elif has_dafaults and link_has_defaults and obs_has_defaults: + # if defaults are available, compute a value + assert isinstance(kwargs["stepsize"], float) and kwargs["stepsize"] > 0 + else: + # return None otherwise + assert isinstance(kwargs["stepsize"], type(None)) + + @pytest.mark.parametrize("batch_size", [None, 1, 10]) + @pytest.mark.parametrize("stepsize", [None, 0.01]) + @pytest.mark.parametrize( + "regularizer", ["UnRegularized", "Ridge", "Lasso", "GroupLasso"] + ) + @pytest.mark.parametrize( + "solver_name, has_dafaults", + [ + ("GradientDescent", False), + ("LBFGS", False), + ("ProximalGradient", False), + ("SVRG", True), + ("ProxSVRG", True), + ], + ) + @pytest.mark.parametrize( + "inv_link, link_has_defaults", [(jax.nn.softplus, True), (jax.numpy.exp, False)] + ) + @pytest.mark.parametrize( + "observation_model, obs_has_defaults", + [ + (nmo.observation_models.PoissonObservations, True), + (nmo.observation_models.GammaObservations, False), + ], + ) + def test_optimize_solver_params_pytree( + self, + batch_size, + stepsize, + regularizer, + solver_name, + inv_link, + observation_model, + has_dafaults, + link_has_defaults, + obs_has_defaults, + poisson_population_GLM_model_pytree, + ): + """Test the behavior of `optimize_solver_params` for different solver, regularizer, and observation model configurations.""" + obs = observation_model(inverse_link_function=inv_link) + X, y, _, _, _ = poisson_population_GLM_model_pytree + solver_kwargs = dict(stepsize=stepsize, batch_size=batch_size) + # use glm static methods to check if the solver is batchable + # if not pop the batch_size kwarg + try: + slv_class = nmo.glm.PopulationGLM._get_solver_class(solver_name) + nmo.glm.PopulationGLM._check_solver_kwargs(slv_class, solver_kwargs) + except NameError: + solver_kwargs.pop("batch_size") + + # if the regularizer is not allowed for the solver type, return + try: + model = nmo.glm.PopulationGLM( + regularizer=regularizer, + solver_name=solver_name, + observation_model=obs, + solver_kwargs=solver_kwargs, + ) + except ValueError as e: + if not str(e).startswith( + rf"The solver: {solver_name} is not allowed for {regularizer} regularization" + ): + raise e + return + + kwargs = model.optimize_solver_params(X, y) + if isinstance(batch_size, int) and "batch_size" in solver_kwargs: + # if batch size was provided, then it should be returned unchanged + assert batch_size == kwargs["batch_size"] + elif has_dafaults and link_has_defaults and obs_has_defaults: + # if defaults are available, a batch size is computed + assert isinstance(kwargs["batch_size"], int) and kwargs["batch_size"] > 0 + elif "batch_size" in solver_kwargs: + # return None otherwise + assert isinstance(kwargs["batch_size"], type(None)) + + if isinstance(stepsize, float): + # if stepsize was provided, then it should be returned unchanged + assert stepsize == kwargs["stepsize"] + elif has_dafaults and link_has_defaults and obs_has_defaults: + # if defaults are available, compute a value + assert isinstance(kwargs["stepsize"], float) and kwargs["stepsize"] > 0 + else: + # return None otherwise + assert isinstance(kwargs["stepsize"], type(None)) + + +def test_optimal_config_all_required_keys_present(): + """Test that all required keys are present in each configuration.""" + required_keys = [ + "required_params", + "compute_l_smooth", + "compute_defaults", + "strong_convexity", + ] + for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): + for config in configs: + for key in required_keys: + assert ( + key in config + ), f"Configuration for solver '{solver}' is missing the required key: '{key}'." + + +@pytest.mark.parametrize( + "regularizer, expected_type_convexity", + [ + ("UnRegularized", type(None)), + ("Lasso", type(None)), + ("GroupLasso", type(None)), + ("Ridge", float), + ], +) +@pytest.mark.parametrize( + "solver_name, expected_type_solver", + [ + ("GradientDescent", type(None)), + ("ProximalGradient", type(None)), + ("LBFGS", type(None)), + ("SVRG", Callable), + ("ProxSVRG", Callable), + ], +) +@pytest.mark.parametrize( + "inv_link_func, expected_type_link", + [(jax.nn.softplus, Callable), (jax.numpy.exp, type(None))], +) +def test_optimal_config_outputs( + regularizer, + solver_name, + inv_link_func, + expected_type_convexity, + expected_type_link, + expected_type_solver, +): + """Test that 'required_params' is a dictionary.""" + obs = nmo.observation_models.PoissonObservations( + inverse_link_function=inv_link_func + ) + + # if the regularizer is not allowed for the solver type, return + try: + model = nmo.glm.GLM( + regularizer=regularizer, solver_name=solver_name, observation_model=obs + ) + except ValueError as e: + if not str(e).startswith( + rf"The solver: {solver_name} is not allowed for {regularizer} regularization" + ): + raise e + return + + # if there is no callable for the model specs, then convexity should be None + func1, func2, convexity = ( + nmo.solvers._compute_defaults.glm_compute_optimal_stepsize_configs(model) + ) + assert isinstance(func1, expected_type_solver) + assert isinstance(func2, expected_type_link) + assert isinstance( + convexity, expected_type_convexity + ), f"convexity type: {type(convexity)}, expected type: {expected_type_convexity}" From 028952d969c20f912f306deaed57f52e3e03977b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 8 Oct 2024 12:59:03 -0400 Subject: [PATCH 26/52] removed unused import --- src/nemos/solvers/_svrg_defaults.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 50136606..682837f8 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -1,6 +1,5 @@ """Module for calculating theoretical optimal defaults for SVRG and GLM configurations.""" -import copy import warnings from functools import wraps from typing import Any, Callable, Optional, Tuple From 7d766eb894df76335231b0f34327be4bf0f0d979 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 8 Oct 2024 14:02:16 -0400 Subject: [PATCH 27/52] fixed warns in tests --- src/nemos/solvers/_compute_defaults.py | 2 +- tests/test_glm.py | 35 +++++++++++++------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/nemos/solvers/_compute_defaults.py b/src/nemos/solvers/_compute_defaults.py index 5bf94fca..f3e8fcbd 100644 --- a/src/nemos/solvers/_compute_defaults.py +++ b/src/nemos/solvers/_compute_defaults.py @@ -22,7 +22,7 @@ def glm_compute_optimal_stepsize_configs( Compute configuration functions for optimal step size selection based on the model. This function returns a tuple of three elements that are used for configuring the - optimal step size and batch size for stochastic variance reduced gradient (SVRG and + optimal step size and batch size for variance reduced gradient (SVRG and ProxSVRG) algorithms. If the model is configured with specific solver names, the appropriate computation functions are returned. Additionally, it determines the smoothness and strong convexity constants based on the model's observation and regularizer. diff --git a/tests/test_glm.py b/tests/test_glm.py index 8ab67adf..daca2cfd 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1804,11 +1804,14 @@ def test_glm_optimal_config_set_initial_state( poissonGLM_model_instantiation, ): X, y, _, true_params, _ = poissonGLM_model_instantiation + if reg == "GroupLasso": + reg = nmo.regularizer.GroupLasso(mask=jnp.ones((1, X.shape[1]))) model = nmo.glm.GLM( solver_name=solver_name, solver_kwargs=dict(batch_size=batch_size, stepsize=stepsize), observation_model=obs, regularizer=reg, + regularizer_strength=None if reg == "UnRegularized" else 1.0, ) opt_state = model.initialize_state(X, y, true_params) solver = inspect.getclosurevars(model._solver_run).nonlocals["solver"] @@ -1863,6 +1866,7 @@ def test_glm_optimal_config_set_initial_state_pytree( solver_kwargs=dict(batch_size=batch_size, stepsize=stepsize), observation_model=obs, regularizer=reg, + regularizer_strength=None if reg == "UnRegularized" else 1.0, ) opt_state = model.initialize_state(X, y, true_params) solver = inspect.getclosurevars(model._solver_run).nonlocals["solver"] @@ -1939,6 +1943,7 @@ def test_optimize_solver_params( solver_name=solver_name, observation_model=obs, solver_kwargs=solver_kwargs, + regularizer_strength=None if regularizer == "UnRegularized" else 1.0, ) except ValueError as e: if not str(e).startswith( @@ -2025,6 +2030,7 @@ def test_optimize_solver_params_pytree( solver_name=solver_name, observation_model=obs, solver_kwargs=solver_kwargs, + regularizer_strength=None if regularizer == "UnRegularized" else 1.0, ) except ValueError as e: if not str(e).startswith( @@ -3786,11 +3792,16 @@ def test_glm_optimal_config_set_initial_state( self, solver_name, batch_size, stepsize, reg, obs, poisson_population_GLM_model ): X, y, _, true_params, _ = poisson_population_GLM_model + + if reg == "GroupLasso": + reg = nmo.regularizer.GroupLasso(mask=jnp.ones((1, X.shape[1]))) + model = nmo.glm.PopulationGLM( solver_name=solver_name, solver_kwargs=dict(batch_size=batch_size, stepsize=stepsize), observation_model=obs, regularizer=reg, + regularizer_strength=None if reg == "UnRegularized" else 1.0, ) opt_state = model.initialize_state(X, y, true_params) solver = inspect.getclosurevars(model._solver_run).nonlocals["solver"] @@ -3845,6 +3856,7 @@ def test_glm_optimal_config_set_initial_state_pytree( solver_kwargs=dict(batch_size=batch_size, stepsize=stepsize), observation_model=obs, regularizer=reg, + regularizer_strength=None if reg == "UnRegularized" else 1.0, ) opt_state = model.initialize_state(X, y, true_params) solver = inspect.getclosurevars(model._solver_run).nonlocals["solver"] @@ -4015,6 +4027,7 @@ def test_optimize_solver_params( solver_name=solver_name, observation_model=obs, solver_kwargs=solver_kwargs, + regularizer_strength=None if regularizer == "UnRegularized" else 1.0, ) except ValueError as e: if not str(e).startswith( @@ -4101,6 +4114,7 @@ def test_optimize_solver_params_pytree( solver_name=solver_name, observation_model=obs, solver_kwargs=solver_kwargs, + regularizer_strength=None if regularizer == "UnRegularized" else 1.0, ) except ValueError as e: if not str(e).startswith( @@ -4131,22 +4145,6 @@ def test_optimize_solver_params_pytree( assert isinstance(kwargs["stepsize"], type(None)) -def test_optimal_config_all_required_keys_present(): - """Test that all required keys are present in each configuration.""" - required_keys = [ - "required_params", - "compute_l_smooth", - "compute_defaults", - "strong_convexity", - ] - for solver, configs in nmo.glm._OPTIMAL_CONFIGURATIONS.items(): - for config in configs: - for key in required_keys: - assert ( - key in config - ), f"Configuration for solver '{solver}' is missing the required key: '{key}'." - - @pytest.mark.parametrize( "regularizer, expected_type_convexity", [ @@ -4186,7 +4184,10 @@ def test_optimal_config_outputs( # if the regularizer is not allowed for the solver type, return try: model = nmo.glm.GLM( - regularizer=regularizer, solver_name=solver_name, observation_model=obs + regularizer=regularizer, + solver_name=solver_name, + observation_model=obs, + regularizer_strength=None if regularizer == "UnRegularized" else 1.0, ) except ValueError as e: if not str(e).startswith( From 93a703318592793420dae3ae298909a5d3fd3674 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 8 Oct 2024 14:23:48 -0400 Subject: [PATCH 28/52] fix warn svrg default --- tests/test_svrg_defaults.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index eed20f60..5766d46c 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -138,15 +138,17 @@ def test_calculate_b_tilde( @pytest.mark.parametrize( "batch_size, stepsize, expected_batch_size, expected_stepsize", [ - (32, 0.01, 32, 0.01), # Both batch_size and stepsize provided + # (32, 0.01, 32, 0.01), # Both batch_size and stepsize provided (32, None, 32, None), # Only batch_size provided - (None, 0.01, None, 0.01), # Only stepsize provided + # (None, 0.01, None, 0.01), # Only stepsize provided ], ) def test_svrg_optimal_batch_and_stepsize_with_provided_defaults( batch_size, stepsize, expected_batch_size, expected_stepsize, x_sample, y_sample ): """Test that provided defaults for batch_size and stepsize are returned as-is or computed correctly.""" + x_sample = jnp.tile(x_sample, 33).reshape(-1, x_sample.shape[-1]) + y_sample = jnp.tile(y_sample, 33) result = _svrg_defaults.svrg_optimal_batch_and_stepsize( _svrg_defaults.glm_softplus_poisson_l_max_and_l, x_sample, From 3e55ce7e03efcf1d6d781f8d962afe36beb866a0 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 8 Oct 2024 14:59:53 -0400 Subject: [PATCH 29/52] moved the methods around for re-usability --- src/nemos/base_regressor.py | 52 +++++++++++++++++++++++++++------ src/nemos/glm.py | 57 ++----------------------------------- tests/test_svrg_defaults.py | 4 +-- 3 files changed, 48 insertions(+), 65 deletions(-) diff --git a/src/nemos/base_regressor.py b/src/nemos/base_regressor.py index 2a97e3e0..c60da8da 100644 --- a/src/nemos/base_regressor.py +++ b/src/nemos/base_regressor.py @@ -6,6 +6,7 @@ import abc import inspect import warnings +from abc import abstractmethod from copy import deepcopy from typing import Any, Dict, NamedTuple, Optional, Tuple, Union @@ -607,21 +608,56 @@ def _get_solver_class(solver_name: str): return solver_class - def optimize_solver_params(self, X, y): + def optimize_solver_params(self, X: DESIGN_INPUT_TYPE, y: jnp.ndarray) -> dict: """ - Compute solver optimal defaults if available. + Compute and update solver parameters with optimal defaults if available. + + This method checks the current solver configuration and, if an optimal + configuration is known for the given model parameters, computes the optimal + batch size, step size, and other hyperparameters to ensure faster convergence. Parameters ---------- - X: - Input predictions. - y: - Output observations. + X : + Input data used to compute smoothness and strong convexity constants. + y : + Target values used in conjunction with X for the same purpose. Returns ------- : - A dictionary with the optimal defaults. + A dictionary containing the solver parameters, updated with optimal defaults + where applicable. """ - return self.solver_kwargs + # Start with a copy of the existing solver parameters + new_solver_kwargs = self.solver_kwargs.copy() + + # get the model specific configs + compute_defaults, compute_l_smooth, strong_convexity = ( + self.get_optimal_solver_params_config() + ) + if compute_defaults and compute_l_smooth: + # Check if the user has provided batch size or stepsize, or else use None + batch_size = new_solver_kwargs.get("batch_size", None) + stepsize = new_solver_kwargs.get("stepsize", None) + + # Compute the optimal batch size and stepsize based on smoothness, strong convexity, etc. + new_params = compute_defaults( + compute_l_smooth, + X, + y, + batch_size=batch_size, + stepsize=stepsize, + strong_convexity=strong_convexity, + ) + + # Update the solver parameters with the computed optimal values + new_solver_kwargs.update(new_params) + + return new_solver_kwargs + + @abstractmethod + def get_optimal_solver_params_config(self): + """Return the functions for computing default step and batch size for the solver.""" + pass diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 070e505c..c4de380c 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -1016,61 +1016,8 @@ def update( return opt_step - def optimize_solver_params(self, X, y): - """ - Compute and update solver parameters with optimal defaults if available. - - This method checks the current solver configuration and, if an optimal - configuration is known for the given model parameters, computes the optimal - batch size, step size, and other hyperparameters to ensure faster convergence. - - Parameters - ---------- - X : array-like - Input data used to compute smoothness and strong convexity constants. - y : array-like - Target values used in conjunction with X for the same purpose. - - Returns - ------- - dict - A dictionary containing the solver parameters, updated with optimal defaults - where applicable. - - Notes - ----- - The method first looks up the known configurations for the solver in the - `_OPTIMAL_CONFIGURATIONS` dictionary. If a configuration matches the model's - parameters, it uses the associated functions to compute the optimal solver - parameters. If `batch_size` or `stepsize` is already provided by the user, - those values are used; otherwise, optimal defaults are computed. - """ - - # Start with a copy of the existing solver parameters - new_solver_kwargs = self.solver_kwargs.copy() - - # get the model specific configs - compute_defaults, compute_l_smooth, strong_convexity = ( - glm_compute_optimal_stepsize_configs(self) - ) - if compute_defaults and compute_l_smooth: - # Check if the user has provided batch size or stepsize, or else use None - batch_size = new_solver_kwargs.get("batch_size", None) - stepsize = new_solver_kwargs.get("stepsize", None) - - # Compute the optimal batch size and stepsize based on smoothness, strong convexity, etc. - new_params = compute_defaults( - compute_l_smooth, - X, - y, - batch_size=batch_size, - stepsize=stepsize, - strong_convexity=strong_convexity, - ) - - # Update the solver parameters with the computed optimal values - new_solver_kwargs.update(new_params) - return new_solver_kwargs + def get_optimal_solver_params_config(self): + return glm_compute_optimal_stepsize_configs(self) class PopulationGLM(GLM): diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index 5766d46c..e2a9938d 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -138,9 +138,9 @@ def test_calculate_b_tilde( @pytest.mark.parametrize( "batch_size, stepsize, expected_batch_size, expected_stepsize", [ - # (32, 0.01, 32, 0.01), # Both batch_size and stepsize provided + (32, 0.01, 32, 0.01), # Both batch_size and stepsize provided (32, None, 32, None), # Only batch_size provided - # (None, 0.01, None, 0.01), # Only stepsize provided + (None, 0.01, None, 0.01), # Only stepsize provided ], ) def test_svrg_optimal_batch_and_stepsize_with_provided_defaults( From 4fdcf3245abf87018e245319c16674fcd151b3f9 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 8 Oct 2024 15:03:26 -0400 Subject: [PATCH 30/52] fixed mockregressor --- src/nemos/glm.py | 1 + tests/conftest.py | 3 +++ tests/test_base_class.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index c4de380c..c38c2216 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -1017,6 +1017,7 @@ def update( return opt_step def get_optimal_solver_params_config(self): + """Return the functions for computing default step and batch size for the solver.""" return glm_compute_optimal_stepsize_configs(self) diff --git a/tests/conftest.py b/tests/conftest.py index 8a36fadf..a235f7b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -82,6 +82,9 @@ def initialize_params(self, *args, **kwargs): def _predict_and_compute_loss(self, params, X, y): pass + def get_optimal_solver_params_config(self): + return None, None, None + class MockRegressorNested(MockRegressor): def __init__(self, other_param: int, std_param: int = 0): diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 4af81c30..1f89f7d6 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -24,6 +24,8 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: def score(self, X, y, score_type="pseudo-r2-McFadden"): pass + def get_optimal_solver_params_config(self): + return None, None, None class BadEstimator(Base): def __init__(self, param1, *args): From 30934cacf1b5ea15fd291206df26a57b52b5d4d1 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 9 Oct 2024 10:43:18 -0400 Subject: [PATCH 31/52] fix comment --- src/nemos/solvers/_svrg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nemos/solvers/_svrg.py b/src/nemos/solvers/_svrg.py index c381ba32..8f47b8e6 100644 --- a/src/nemos/solvers/_svrg.py +++ b/src/nemos/solvers/_svrg.py @@ -207,7 +207,7 @@ def _inner_loop_param_update_step( # gradient of f_{i_k} at x_{k} in the pseudocode of Gower et al. 2020 minibatch_grad_at_current_params = self.loss_gradient(params, *args) # gradient on batch_{i_k} evaluated at the anchor point - # gradient of f_{i_k} at x_{x} in the pseudocode of Gower et al. 2020 + # gradient of f_{i_k} at x_{k} in the pseudocode of Gower et al. 2020 minibatch_grad_at_reference_point = self.loss_gradient(reference_point, *args) # SVRG gradient estimate From b3f801f219130fd1c88fa040e2fcc87d81850552 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 16 Oct 2024 10:29:13 -0400 Subject: [PATCH 32/52] batched multiply --- src/nemos/solvers/_svrg_defaults.py | 21 ++++++++++++++++----- tests/test_svrg_defaults.py | 5 +++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 682837f8..58b1cb23 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -180,7 +180,7 @@ def glm_softplus_poisson_l_max_and_l( def _glm_softplus_poisson_l_smooth_multiply( - X: jnp.ndarray, y: jnp.ndarray, v: jnp.ndarray + X: jnp.ndarray, y: jnp.ndarray, v: jnp.ndarray, batch_size: int ): """ Multiply vector `v` with the matrix X.T @ D @ X without forming it explicitly. @@ -202,12 +202,17 @@ def _glm_softplus_poisson_l_smooth_multiply( : Result of the multiplication (X.T @ D @ X) @ v. """ - N, _ = X.shape - return X.T.dot((0.17 * y + 0.25) * X.dot(v)) / N + N, K = X.shape + out = jnp.zeros((K,)) + for i in range(0, N, batch_size): + xb, yb = X[i:i + batch_size], y[i:i + batch_size] + out = out + xb.T.dot((0.17 * yb + 0.25) * xb.dot(v)) + out = out / N + return out def _glm_softplus_poisson_l_smooth_with_power_iteration( - X: jnp.ndarray, y: jnp.ndarray, n_power_iters: int = 20 + X: jnp.ndarray, y: jnp.ndarray, n_power_iters: int = 20, batch_size: Optional[int] = None ): """ Compute the largest eigenvalue of X.T @ D @ X using the power method. @@ -224,12 +229,18 @@ def _glm_softplus_poisson_l_smooth_with_power_iteration( Output data vector (N,). n_power_iters : Maximum number of power iterations to use. + batch_size : + The batch size, if user provides one. Returns ------- : The largest eigenvalue of X.T @ D @ X. """ + + if batch_size is None: + batch_size = X.shape[0] + _, d = X.shape # Initialize a random d-dimensional vector for power iteration @@ -238,7 +249,7 @@ def _glm_softplus_poisson_l_smooth_with_power_iteration( # Run power iteration to approximate the largest eigenvalue for _ in range(n_power_iters): v_prev = v.copy() - v = _glm_softplus_poisson_l_smooth_multiply(X, y, v) + v = _glm_softplus_poisson_l_smooth_multiply(X, y, v, batch_size) v /= v.max() # Check for convergence diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index e2a9938d..59e6536c 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -39,11 +39,12 @@ def test_svrg_optimal_batch_and_stepsize(x_sample, y_sample): assert isinstance(result["stepsize"], float) -def test_softplus_poisson_l_smooth_multiply(x_sample, y_sample): +@pytest.mark.parametrize("batch_size", [1, 2, 3]) +def test_softplus_poisson_l_smooth_multiply(x_sample, y_sample, batch_size): """Test multiplication with X.T @ D @ X without forming the matrix.""" v_sample = jnp.array([0.5, 1.0]) result = _svrg_defaults._glm_softplus_poisson_l_smooth_multiply( - x_sample, y_sample, v_sample + x_sample, y_sample, v_sample, batch_size ) diag_mat = jnp.diag(y_sample * 0.17 + 0.25) expected_result = ( From 26499a76799dbcc4efbd9daf7a61b4a1a263f099 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 16 Oct 2024 10:38:54 -0400 Subject: [PATCH 33/52] changed typing --- src/nemos/solvers/_svrg_defaults.py | 36 +++++++++++++++-------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 58b1cb23..cedf9072 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -6,6 +6,7 @@ import jax import jax.numpy as jnp +from numpy.typing import NDArray def _convert_to_float(func): @@ -52,22 +53,22 @@ def svrg_optimal_batch_and_stepsize( Parameters ---------- - compute_smoothness_constants : Callable + compute_smoothness_constants : Function that computes the smoothness constants `l_smooth` and `l_smooth_max` for the problem. This is problem (loss function) specific. - data : Any + data : Input data, typically (X, y) for a GLM. - batch_size : Optional[int], default None + batch_size : The batch size set by the user. If None, it will be calculated. - stepsize : Optional[float], default None + stepsize : The step size set by the user. If None, it will be calculated. - strong_convexity : Optional[float], default None + strong_convexity : The strong convexity constant. For L2-regularized losses, this should be the regularization strength. - n_power_iters : Optional[int], default None + n_power_iters : Maximum number of iterations for the power method when finding the largest eigenvalue. - default_batch_size : int, default 1 + default_batch_size : Default batch size to use if the optimal calculation fails. - default_stepsize : float, default 1e-3 + default_stepsize : Default step size to use if the optimal calculation fails. Returns @@ -145,8 +146,8 @@ def svrg_optimal_batch_and_stepsize( def glm_softplus_poisson_l_max_and_l( - *data: jnp.ndarray, n_power_iters: Optional[int] = 20 -) -> Tuple[float, float]: + *data: NDArray, n_power_iters: Optional[int] = 20 +) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Calculate smoothness constants for a Poisson GLM with a softplus inverse link function. @@ -180,13 +181,14 @@ def glm_softplus_poisson_l_max_and_l( def _glm_softplus_poisson_l_smooth_multiply( - X: jnp.ndarray, y: jnp.ndarray, v: jnp.ndarray, batch_size: int + X: NDArray, y: NDArray, v: NDArray, batch_size: int ): """ Multiply vector `v` with the matrix X.T @ D @ X without forming it explicitly. This method estimates the multiplication by calculating the Hessian of the loss. It is efficient for situations where X can fit in memory. + If batch_size is provided, the computation will be done by slicing the array. Parameters ---------- @@ -212,7 +214,7 @@ def _glm_softplus_poisson_l_smooth_multiply( def _glm_softplus_poisson_l_smooth_with_power_iteration( - X: jnp.ndarray, y: jnp.ndarray, n_power_iters: int = 20, batch_size: Optional[int] = None + X: NDArray, y: NDArray, n_power_iters: int = 20, batch_size: Optional[int] = None ): """ Compute the largest eigenvalue of X.T @ D @ X using the power method. @@ -262,7 +264,7 @@ def _glm_softplus_poisson_l_smooth_with_power_iteration( def _glm_softplus_poisson_l_smooth( - X: jnp.ndarray, y: jnp.ndarray, n_power_iters: Optional[int] = None + X: NDArray, y: NDArray, n_power_iters: Optional[int] = None ) -> jnp.ndarray: """ Calculate the smoothness constant `L` for a Poisson GLM with softplus inverse link. @@ -272,11 +274,11 @@ def _glm_softplus_poisson_l_smooth( Parameters ---------- - X : jnp.ndarray + X : Input data matrix (N x d). - y : jnp.ndarray + y : Output data vector (N,). - n_power_iters : Optional[int], default None + n_power_iters : Number of power iterations to use when finding the largest eigenvalue. If None, the eigenvalue is calculated directly. @@ -294,7 +296,7 @@ def _glm_softplus_poisson_l_smooth( return _glm_softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters) -def _glm_softplus_poisson_l_smooth_max(X: jnp.ndarray, y: jnp.ndarray) -> jnp.ndarray: +def _glm_softplus_poisson_l_smooth_max(X: NDArray, y: NDArray) -> NDArray: """ Calculate the maximum smoothness constant `L_max` for individual observations. From 05f4f3645fe983b18288a4dc6a33f552b5cfc3ed Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 16 Oct 2024 11:56:55 -0400 Subject: [PATCH 34/52] fixed tests --- src/nemos/solvers/_svrg_defaults.py | 6 +++--- tests/test_svrg_defaults.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index cedf9072..99fc3462 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -97,8 +97,8 @@ def svrg_optimal_batch_and_stepsize( >>> batch_size, stepsize = compute_opt_params(glm_softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08) """ - # Ensure data is converted to JAX arrays - data = jax.tree_util.tree_map(jnp.asarray, data) + # # Ensure data is converted to JAX arrays + # data = jax.tree_util.tree_map(jnp.asarray, data) # Get the number of samples, ensuring consistency across all inputs num_samples = {dd.shape[0] for dd in jax.tree_util.tree_leaves(data)} @@ -260,7 +260,7 @@ def _glm_softplus_poisson_l_smooth_with_power_iteration( # Final eigenvalue calculation v /= jnp.linalg.norm(v) - return _glm_softplus_poisson_l_smooth_multiply(X, y, v).dot(v) + return _glm_softplus_poisson_l_smooth_multiply(X, y, v, batch_size).dot(v) def _glm_softplus_poisson_l_smooth( diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index 59e6536c..d0ad7b06 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -56,7 +56,7 @@ def test_softplus_poisson_l_smooth_multiply(x_sample, y_sample, batch_size): def test_softplus_poisson_l_smooth_with_power_iteration(x_sample, y_sample): """Test the power iteration method for finding the largest eigenvalue.""" result = _svrg_defaults._glm_softplus_poisson_l_smooth_with_power_iteration( - x_sample, y_sample, n_power_iters=20 + x_sample, y_sample, n_power_iters=20, batch_size=x_sample.shape[0] ) # compute eigvals directly diag_mat = jnp.diag(y_sample * 0.17 + 0.25) From d8e821698bdd2a564ac7423896bc32568f7e140b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 16 Oct 2024 12:23:46 -0400 Subject: [PATCH 35/52] add batched compute of lmax --- src/nemos/solvers/_svrg_defaults.py | 42 ++++++++++++++++------------- tests/test_svrg_defaults.py | 8 ------ 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 99fc3462..d061ade3 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -8,6 +8,8 @@ import jax.numpy as jnp from numpy.typing import NDArray +from .. import pytrees + def _convert_to_float(func): """ @@ -94,12 +96,8 @@ def svrg_optimal_batch_and_stepsize( >>> np.random.seed(123) >>> X = np.random.normal(size=(500, 5)) >>> y = np.random.poisson(np.exp(X.dot(np.ones(X.shape[1])))) - >>> batch_size, stepsize = compute_opt_params(glm_softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08) + >>> batch_size, stepsize = compute_opt_params(glm_softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08, batch_size=1) """ - - # # Ensure data is converted to JAX arrays - # data = jax.tree_util.tree_map(jnp.asarray, data) - # Get the number of samples, ensuring consistency across all inputs num_samples = {dd.shape[0] for dd in jax.tree_util.tree_leaves(data)} if len(num_samples) != 1: @@ -112,7 +110,7 @@ def svrg_optimal_batch_and_stepsize( # Compute smoothness constants l_smooth_max, l_smooth = compute_smoothness_constants( - *data, n_power_iters=n_power_iters + *data, n_power_iters=n_power_iters, batch_size=batch_size ) # Compute optimal batch size if not provided by the user @@ -146,7 +144,7 @@ def svrg_optimal_batch_and_stepsize( def glm_softplus_poisson_l_max_and_l( - *data: NDArray, n_power_iters: Optional[int] = 20 + *data: NDArray, n_power_iters: Optional[int] = 20, batch_size: Optional[int] = None ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Calculate smoothness constants for a Poisson GLM with a softplus inverse link function. @@ -161,6 +159,8 @@ def glm_softplus_poisson_l_max_and_l( n_power_iters : Number of power iterations to use when finding the largest eigenvalue. If None, the eigenvalue is calculated directly. + batch_size: + The batch size set by the user. If None, it will be n_samples. Returns ------- @@ -169,14 +169,18 @@ def glm_softplus_poisson_l_max_and_l( """ X, y = data + if batch_size is None: + batch_size = y.shape[0] + # takes care of population glm (see bound found on overleaf) y = jnp.max(y, axis=tuple(range(1, y.ndim))) # concatenate all data (if X is FeaturePytree) - X = jnp.hstack(jax.tree_util.tree_leaves(X)) + if isinstance(X, (pytrees.FeaturePytree, dict)): + X = jnp.hstack(jax.tree_util.tree_leaves(X)) - l_smooth = _glm_softplus_poisson_l_smooth(X, y, n_power_iters) - l_smooth_max = _glm_softplus_poisson_l_smooth_max(X, y) + l_smooth = _glm_softplus_poisson_l_smooth(X, y, n_power_iters=n_power_iters, batch_size=batch_size) + l_smooth_max = _glm_softplus_poisson_l_smooth_max(X, y, batch_size=batch_size) return l_smooth_max, l_smooth @@ -264,7 +268,7 @@ def _glm_softplus_poisson_l_smooth_with_power_iteration( def _glm_softplus_poisson_l_smooth( - X: NDArray, y: NDArray, n_power_iters: Optional[int] = None + X: NDArray, y: NDArray, batch_size: int, n_power_iters: Optional[int] = None ) -> jnp.ndarray: """ Calculate the smoothness constant `L` for a Poisson GLM with softplus inverse link. @@ -278,6 +282,8 @@ def _glm_softplus_poisson_l_smooth( Input data matrix (N x d). y : Output data vector (N,). + batch_size : + The batch size, if user provides one. n_power_iters : Number of power iterations to use when finding the largest eigenvalue. If None, the eigenvalue is calculated directly. @@ -293,10 +299,10 @@ def _glm_softplus_poisson_l_smooth( return jnp.sort(jnp.linalg.eigvalsh(XDX))[-1] else: # Use power iteration to find the largest eigenvalue - return _glm_softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters) + return _glm_softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters, batch_size=batch_size) -def _glm_softplus_poisson_l_smooth_max(X: NDArray, y: NDArray) -> NDArray: +def _glm_softplus_poisson_l_smooth_max(X: NDArray, y: NDArray, batch_size: int) -> NDArray: """ Calculate the maximum smoothness constant `L_max` for individual observations. @@ -317,12 +323,10 @@ def _glm_softplus_poisson_l_smooth_max(X: NDArray, y: NDArray) -> NDArray: """ N, _ = X.shape - def body_fun(i, current_max): - return jnp.maximum( - current_max, jnp.linalg.norm(X[i, :]) ** 2 * (0.17 * y[i] + 0.25) - ) - - l_max = jax.lax.fori_loop(0, N, body_fun, jnp.array([-jnp.inf])) + l_max = jnp.array([0]) + for nb in range(0, N, batch_size): + xb, yb = X[nb:nb + batch_size], y[nb:nb + batch_size] + l_max = jnp.maximum(l_max, jnp.max(jnp.linalg.norm(xb, axis=1) ** 2 * (0.17 * yb + 0.25))) return l_max[0] diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index d0ad7b06..9ec4db8a 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -178,14 +178,6 @@ def test_svrg_optimal_batch_and_stepsize_with_provided_defaults( @pytest.mark.parametrize( "batch_size, stepsize, strong_convexity, expectation", [ - ( - jnp.inf, - None, - 0.1, - pytest.warns( - UserWarning, match="Could not determine batch size automatically" - ), - ), ( 32, None, From b10c192900ed3a9a6f4224d602142103b656d3a4 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 16 Oct 2024 14:55:22 -0400 Subject: [PATCH 36/52] added comment --- src/nemos/solvers/_svrg_defaults.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index d061ade3..c21ac584 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -315,6 +315,8 @@ def _glm_softplus_poisson_l_smooth_max(X: NDArray, y: NDArray, batch_size: int) Input data matrix (N x d). y : Output data vector (N,). + batch_size: + The batch size. Returns ------- From e09623b4421e891fc61c6c4c2a7a7b697f78cdea Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 21 Oct 2024 16:47:33 -0400 Subject: [PATCH 37/52] linted --- src/nemos/solvers/_svrg_defaults.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index c21ac584..c04bfcfa 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -179,7 +179,9 @@ def glm_softplus_poisson_l_max_and_l( if isinstance(X, (pytrees.FeaturePytree, dict)): X = jnp.hstack(jax.tree_util.tree_leaves(X)) - l_smooth = _glm_softplus_poisson_l_smooth(X, y, n_power_iters=n_power_iters, batch_size=batch_size) + l_smooth = _glm_softplus_poisson_l_smooth( + X, y, n_power_iters=n_power_iters, batch_size=batch_size + ) l_smooth_max = _glm_softplus_poisson_l_smooth_max(X, y, batch_size=batch_size) return l_smooth_max, l_smooth @@ -211,7 +213,7 @@ def _glm_softplus_poisson_l_smooth_multiply( N, K = X.shape out = jnp.zeros((K,)) for i in range(0, N, batch_size): - xb, yb = X[i:i + batch_size], y[i:i + batch_size] + xb, yb = X[i : i + batch_size], y[i : i + batch_size] out = out + xb.T.dot((0.17 * yb + 0.25) * xb.dot(v)) out = out / N return out @@ -299,10 +301,14 @@ def _glm_softplus_poisson_l_smooth( return jnp.sort(jnp.linalg.eigvalsh(XDX))[-1] else: # Use power iteration to find the largest eigenvalue - return _glm_softplus_poisson_l_smooth_with_power_iteration(X, y, n_power_iters, batch_size=batch_size) + return _glm_softplus_poisson_l_smooth_with_power_iteration( + X, y, n_power_iters, batch_size=batch_size + ) -def _glm_softplus_poisson_l_smooth_max(X: NDArray, y: NDArray, batch_size: int) -> NDArray: +def _glm_softplus_poisson_l_smooth_max( + X: NDArray, y: NDArray, batch_size: int +) -> NDArray: """ Calculate the maximum smoothness constant `L_max` for individual observations. @@ -327,8 +333,10 @@ def _glm_softplus_poisson_l_smooth_max(X: NDArray, y: NDArray, batch_size: int) l_max = jnp.array([0]) for nb in range(0, N, batch_size): - xb, yb = X[nb:nb + batch_size], y[nb:nb + batch_size] - l_max = jnp.maximum(l_max, jnp.max(jnp.linalg.norm(xb, axis=1) ** 2 * (0.17 * yb + 0.25))) + xb, yb = X[nb : nb + batch_size], y[nb : nb + batch_size] + l_max = jnp.maximum( + l_max, jnp.max(jnp.linalg.norm(xb, axis=1) ** 2 * (0.17 * yb + 0.25)) + ) return l_max[0] From c220a1de13581b2d03c5f1ff3f40cbfd27424a13 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 21 Oct 2024 16:55:24 -0400 Subject: [PATCH 38/52] linted --- src/nemos/solvers/_svrg_defaults.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index c04bfcfa..fd1c737d 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -96,7 +96,9 @@ def svrg_optimal_batch_and_stepsize( >>> np.random.seed(123) >>> X = np.random.normal(size=(500, 5)) >>> y = np.random.poisson(np.exp(X.dot(np.ones(X.shape[1])))) - >>> batch_size, stepsize = compute_opt_params(glm_softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08, batch_size=1) + >>> batch_size, stepsize = compute_opt_params( + ... glm_softplus_poisson_l_max_and_l, X, y, strong_convexity=0.08, batch_size=1 + ... ) """ # Get the number of samples, ensuring consistency across all inputs num_samples = {dd.shape[0] for dd in jax.tree_util.tree_leaves(data)} From 4c9553d054c4fedf90f865d323fdeb5903d40fee Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 21 Oct 2024 17:17:28 -0400 Subject: [PATCH 39/52] modified svrg error to match GradientDescent and ProximalGradient from jaxopt --- src/nemos/solvers/_svrg.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/nemos/solvers/_svrg.py b/src/nemos/solvers/_svrg.py index 8f47b8e6..b139ada2 100644 --- a/src/nemos/solvers/_svrg.py +++ b/src/nemos/solvers/_svrg.py @@ -575,7 +575,7 @@ def inner_loop_body(_, carry): @staticmethod def _error(x, x_prev, stepsize): """ - Calculate the magnitude of the update relative to the parameters. + Calculate the magnitude of the update relative to the stepsize. Used for terminating the algorithm if a certain tolerance is reached. Params @@ -589,8 +589,7 @@ def _error(x, x_prev, stepsize): ------- Scaled update magnitude. """ - # stepsize is an argument to be consistent with jaxopt - return tree_l2_norm(tree_sub(x, x_prev)) / tree_l2_norm(x_prev) + return tree_l2_norm(tree_sub(x, x_prev)) / stepsize class SVRG(ProxSVRG): From 009561e3dcc1a4df207baccabe13b14dfe415b12 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Oct 2024 10:57:09 -0400 Subject: [PATCH 40/52] added pdf --- docs/assets/poisson_model_calc_stepsize.pdf | Bin 0 -> 237440 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/assets/poisson_model_calc_stepsize.pdf diff --git a/docs/assets/poisson_model_calc_stepsize.pdf b/docs/assets/poisson_model_calc_stepsize.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4ec9930b3fbb0543153416ce6feffd3a15ba38ae GIT binary patch literal 237440 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

G!DZ6+t>qsNnRSPwPBRU_552T>^1 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

EJeueD@tL6sIk-enQSA)E?`ZdWyDcP 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

VR5Hy?KJ`M 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


11`o+{@0-F-6a0>$&%@ogB?E`23O2fI;_+u%s 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
C0r)1!EBi0nz48{BXpgDreDeND_wMoS3pb}nes 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

+?qCvNm9Zvc~o! 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 XdiIX4KfMi-fu04Dm{>?g81jDr$gu!8 literal 0 HcmV?d00001 From 60b2f2763398ac7467dd7be0a9f21dab4f7198a6 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Oct 2024 11:08:28 -0400 Subject: [PATCH 41/52] improved error message and provided solution --- src/nemos/solvers/_compute_defaults.py | 3 +-- src/nemos/solvers/_svrg_defaults.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/nemos/solvers/_compute_defaults.py b/src/nemos/solvers/_compute_defaults.py index f3e8fcbd..0d110549 100644 --- a/src/nemos/solvers/_compute_defaults.py +++ b/src/nemos/solvers/_compute_defaults.py @@ -31,8 +31,7 @@ def glm_compute_optimal_stepsize_configs( ---------- model : The generalized linear model object for which the optimal step size and batch - configuration need to be computed. The model should have attributes like - `solver_name`, `observation_model`, and `regularizer`. + configuration need to be computed. Returns ------- diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index fd1c737d..9a7dfc65 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -36,7 +36,6 @@ def wrapper(*args, **kwargs): return wrapper -# not using the previous one to avoid calculating L and L_max twice def svrg_optimal_batch_and_stepsize( compute_smoothness_constants: Callable, *data: Any, @@ -298,9 +297,15 @@ def _glm_softplus_poisson_l_smooth( Smoothness constant `L`. """ if n_power_iters is None: - # Calculate the Hessian directly and find the largest eigenvalue - XDX = X.T.dot((0.17 * y.reshape(y.shape[0], 1) + 0.25) * X) / y.shape[0] - return jnp.sort(jnp.linalg.eigvalsh(XDX))[-1] + try: + # Calculate the Hessian directly and find the largest eigenvalue + XDX = X.T.dot((0.17 * y.reshape(y.shape[0], 1) + 0.25) * X) / y.shape[0] + return jnp.sort(jnp.linalg.eigvalsh(XDX))[-1] + except RuntimeError as e: + raise RuntimeError( + f"Failed to calculate the largest eigenvalue: {e}. " + "Consider using the power method by setting the `n_power_iters` parameter." + ) else: # Use power iteration to find the largest eigenvalue return _glm_softplus_poisson_l_smooth_with_power_iteration( From f0e133e8d74fc2ab95bf49c3dcbcc0bbe0a4a7b6 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Oct 2024 11:12:07 -0400 Subject: [PATCH 42/52] change default to power iteration --- src/nemos/solvers/_svrg_defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 9a7dfc65..f8ca382b 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -271,7 +271,7 @@ def _glm_softplus_poisson_l_smooth_with_power_iteration( def _glm_softplus_poisson_l_smooth( - X: NDArray, y: NDArray, batch_size: int, n_power_iters: Optional[int] = None + X: NDArray, y: NDArray, batch_size: int, n_power_iters: Optional[int] = 20 ) -> jnp.ndarray: """ Calculate the smoothness constant `L` for a Poisson GLM with softplus inverse link. From 7fc7e665adbd91b68a0240e3ad90d1ff272fab4d Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Oct 2024 11:12:32 -0400 Subject: [PATCH 43/52] saga removed --- src/nemos/solvers/_svrg_defaults.py | 37 ----------------------------- 1 file changed, 37 deletions(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index f8ca382b..78773892 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -384,43 +384,6 @@ def _calculate_stepsize_svrg( return numerator / denominator -@_convert_to_float -def _calculate_stepsize_saga( - batch_size: int, num_samples: int, l_smooth_max: float, l_smooth: float -) -> float: - """ - Calculate optimal step size for SAGA according to [1]. - - Parameters - ---------- - batch_size : - Mini-batch size. - num_samples : - Overall number of data points. - l_smooth_max : - Maximum smoothness constant among f_{i}. - l_smooth : - Smoothness constant. - - Returns - ------- - : - Optimal step size for the optimization. - - References - ---------- - [1] Gazagnadou, Nidham, Robert Gower, and Joseph Salmon. - "Optimal mini-batch and step sizes for saga." - International conference on machine learning. PMLR, 2019. - """ - - l_b = l_smooth * num_samples / batch_size * (batch_size - 1) / ( - num_samples - 1 - ) + l_smooth_max / batch_size * (num_samples - batch_size) / (num_samples - 1) - - return 0.25 / l_b - - def _calculate_optimal_batch_size_svrg( num_samples: int, l_smooth_max: float, From c0103aafb2edad4f0b8895f75a1c781119156f66 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Oct 2024 11:19:11 -0400 Subject: [PATCH 44/52] removed model.fit --- tests/test_glm.py | 8 -------- tests/test_svrg_defaults.py | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index daca2cfd..7f3ad62e 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1822,14 +1822,12 @@ def test_glm_optimal_config_set_initial_state( else: assert opt_state.stepsize > 0 assert isinstance(opt_state.stepsize, float) - model.fit(X, y) if batch_size is not None: assert solver.batch_size == batch_size else: assert isinstance(solver.batch_size, int) assert solver.batch_size > 0 - model.fit(X, y) @pytest.mark.parametrize( "solver_name, reg", @@ -1877,14 +1875,12 @@ def test_glm_optimal_config_set_initial_state_pytree( else: assert opt_state.stepsize > 0 assert isinstance(opt_state.stepsize, float) - model.fit(X, y) if batch_size is not None: assert solver.batch_size == batch_size else: assert isinstance(solver.batch_size, int) assert solver.batch_size > 0 - model.fit(X, y) @pytest.mark.parametrize("batch_size", [None, 1, 10]) @pytest.mark.parametrize("stepsize", [None, 0.01]) @@ -3812,14 +3808,12 @@ def test_glm_optimal_config_set_initial_state( else: assert opt_state.stepsize > 0 assert isinstance(opt_state.stepsize, float) - model.fit(X, y) if batch_size is not None: assert solver.batch_size == batch_size else: assert isinstance(solver.batch_size, int) assert solver.batch_size > 0 - model.fit(X, y) @pytest.mark.parametrize( "solver_name, reg", @@ -3867,14 +3861,12 @@ def test_glm_optimal_config_set_initial_state_pytree( else: assert opt_state.stepsize > 0 assert isinstance(opt_state.stepsize, float) - model.fit(X, y) if batch_size is not None: assert solver.batch_size == batch_size else: assert isinstance(solver.batch_size, int) assert solver.batch_size > 0 - model.fit(X, y) @pytest.mark.parametrize( "params, warns", diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index 9ec4db8a..58f82a68 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -124,6 +124,7 @@ def test_calculate_b_hat(num_samples, l_smooth_max, l_smooth, expected_b_hat): "num_samples, l_smooth_max, l_smooth, strong_convexity, expected_b_tilde", [ (100, 10.0, 2.0, 0.1, 3.4146341463414633), + (121, 11.0, 1.0, 0.4, 0.6769230769230770), ], ) def test_calculate_b_tilde( From 78cb8edc984e2cf9ef5bc684225f3de6a3c43c50 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Oct 2024 11:22:28 -0400 Subject: [PATCH 45/52] removed test saga --- tests/test_svrg_defaults.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index 58f82a68..044f77ea 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -80,19 +80,6 @@ def test_calculate_stepsize_svrg(batch_size, num_samples, l_smooth_max, l_smooth assert isinstance(stepsize, float) -@pytest.mark.parametrize("batch_size", [1, 2, 10]) -@pytest.mark.parametrize("num_samples", [10, 12, 100, 500]) -@pytest.mark.parametrize("l_smooth_max", [0.1, 1.0, 10.0]) -@pytest.mark.parametrize("l_smooth", [0.01, 0.05, 2.0]) -def test_calculate_stepsize_saga(batch_size, num_samples, l_smooth_max, l_smooth): - """Test calculation of the optimal step size for SAGA.""" - stepsize = _svrg_defaults._calculate_stepsize_saga( - batch_size, num_samples, l_smooth_max, l_smooth - ) - assert stepsize > 0 - assert isinstance(stepsize, float) - - @pytest.mark.parametrize("num_samples", [12, 100, 500]) @pytest.mark.parametrize("l_smooth_max", [0.1, 1.0, 10.0]) @pytest.mark.parametrize("l_smooth", [0.01, 0.05]) From 37f6b11c7c275d4c3b0976248354a457adb5b88c Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 28 Oct 2024 10:07:06 -0400 Subject: [PATCH 46/52] added extra example --- tests/test_svrg_defaults.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index 044f77ea..7b5ce7eb 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -99,6 +99,7 @@ def test_calculate_optimal_batch_size_svrg( "num_samples, l_smooth_max, l_smooth, expected_b_hat", [ (100, 10.0, 2.0, 2.8697202), + (121, 11.0, 1.0, 4.690416), ], ) def test_calculate_b_hat(num_samples, l_smooth_max, l_smooth, expected_b_hat): From 07188193b3716c1f55f6c16a5a96f0c9cd6bab7b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 28 Oct 2024 10:11:02 -0400 Subject: [PATCH 47/52] added warning --- src/nemos/solvers/_svrg_defaults.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 78773892..ead33a28 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -298,6 +298,9 @@ def _glm_softplus_poisson_l_smooth( """ if n_power_iters is None: try: + warnings.warn("Direct computation of the eigenvalues requires a substantial amount of resources. " + "Please, consider using the power method by setting the `n_power_iters` parameter " + "(default behavior).", UserWarning) # Calculate the Hessian directly and find the largest eigenvalue XDX = X.T.dot((0.17 * y.reshape(y.shape[0], 1) + 0.25) * X) / y.shape[0] return jnp.sort(jnp.linalg.eigvalsh(XDX))[-1] From 6ecf1a195a3a8d68add783b05530ca786616878f Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 28 Oct 2024 10:13:51 -0400 Subject: [PATCH 48/52] changed descr of svrg usage --- src/nemos/glm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 9e643e04..eed82912 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -67,17 +67,17 @@ class GLM(BaseRegressor): For very large models, you may consider using the Stochastic Variance Reduced Gradient ([SVRG](../solvers/_svrg/#nemos.solvers._svrg.SVRG)) or its proximal variant ([ProxSVRG](../solvers/_svrg/#nemos.solvers._svrg.ProxSVRG)) solver, - which take advantage of batched computation. + which take advantage of batched computation, passing the solver name at model initialization. The performance of the SVRG solver depends critically on the choice of `batch_size` and `stepsize` hyperparameters. These parameters control the size of the mini-batches used for gradient computations and the step size for each iteration, respectively. Improper selection of these parameters can lead to slow convergence or even divergence of the optimization process. - To assist with this, for certain GLM configurations, we provide recommended `batch_size` and `stepsize` + To assist with this, for certain GLM configurations, we provide `batch_size` and `stepsize` default values that are theoretically guaranteed to ensure fast convergence. - Below is a list of the configurations for which we can provide guaranteed hyperparameters: + Below is a list of the configurations for which we can provide guaranteed default hyperparameters: | GLM / PopulationGLM Configuration | Stepsize | Batch Size | | --------------------------------- | :------: | :---------: | From 6bb16b1dae807de4c4121c760378d9cfb01775c6 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 28 Oct 2024 10:18:32 -0400 Subject: [PATCH 49/52] linted --- src/nemos/solvers/_svrg_defaults.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index ead33a28..00fb3bb4 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -298,9 +298,12 @@ def _glm_softplus_poisson_l_smooth( """ if n_power_iters is None: try: - warnings.warn("Direct computation of the eigenvalues requires a substantial amount of resources. " - "Please, consider using the power method by setting the `n_power_iters` parameter " - "(default behavior).", UserWarning) + warnings.warn( + "Direct computation of the eigenvalues requires a substantial amount of resources. " + "Please, consider using the power method by setting the `n_power_iters` parameter " + "(default behavior).", + UserWarning, + ) # Calculate the Hessian directly and find the largest eigenvalue XDX = X.T.dot((0.17 * y.reshape(y.shape[0], 1) + 0.25) * X) / y.shape[0] return jnp.sort(jnp.linalg.eigvalsh(XDX))[-1] From d901ebbfc2be21d8dbbb60c77b01cd125104a6c5 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 28 Oct 2024 10:26:08 -0400 Subject: [PATCH 50/52] improved docstrings --- src/nemos/glm.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index eed82912..87164700 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -59,7 +59,7 @@ class GLM(BaseRegressor): | UnRegularized | GradientDescent | GradientDescent, BFGS, LBFGS, NonlinearCG, SVRG, ProximalGradient, ProxSVRG | | Ridge | GradientDescent | GradientDescent, BFGS, LBFGS, NonlinearCG, SVRG, ProximalGradient, ProxSVRG | | Lasso | ProximalGradient | ProximalGradient, ProxSVRG | - | GroupLasso | ProximalGradient | ProximalGradient, , ProxSVRG | + | GroupLasso | ProximalGradient | ProximalGradient, ProxSVRG | **Fitting Large Models** @@ -67,7 +67,8 @@ class GLM(BaseRegressor): For very large models, you may consider using the Stochastic Variance Reduced Gradient ([SVRG](../solvers/_svrg/#nemos.solvers._svrg.SVRG)) or its proximal variant ([ProxSVRG](../solvers/_svrg/#nemos.solvers._svrg.ProxSVRG)) solver, - which take advantage of batched computation, passing the solver name at model initialization. + which take advantage of batched computation. You can change the solver by passing + `"SVRG"` as `solver_name` at model initialization. The performance of the SVRG solver depends critically on the choice of `batch_size` and `stepsize` hyperparameters. These parameters control the size of the mini-batches used for gradient computations @@ -1034,14 +1035,15 @@ class PopulationGLM(GLM): For very large models, you may consider using the Stochastic Variance Reduced Gradient ([SVRG](../solvers/_svrg/#nemos.solvers._svrg.SVRG)) or its proximal variant ([ProxSVRG](../solvers/_svrg/#nemos.solvers._svrg.ProxSVRG)) solver, - which take advantage of batched computation. + which take advantage of batched computation. You can change the solver by passing + `"SVRG"` or `"ProxSVRG"` as `solver_name` at model initialization. The performance of the SVRG solver depends critically on the choice of `batch_size` and `stepsize` hyperparameters. These parameters control the size of the mini-batches used for gradient computations and the step size for each iteration, respectively. Improper selection of these parameters can lead to slow convergence or even divergence of the optimization process. - To assist with this, for certain GLM configurations, we provide recommended `batch_size` and `stepsize` + To assist with this, for certain GLM configurations, we provide `batch_size` and `stepsize` default values that are theoretically guaranteed to ensure fast convergence. Below is a list of the configurations for which we can provide guaranteed hyperparameters: From 01bc153a59b2516fd44829f4fc58ffa881356476 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 28 Oct 2024 10:52:37 -0400 Subject: [PATCH 51/52] added expectation in test --- src/nemos/solvers/_svrg_defaults.py | 5 +++-- tests/test_svrg_defaults.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index 00fb3bb4..a3d1f7ee 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -42,7 +42,7 @@ def svrg_optimal_batch_and_stepsize( batch_size: Optional[int] = None, stepsize: Optional[float] = None, strong_convexity: Optional[float] = None, - n_power_iters: Optional[int] = None, + n_power_iters: Optional[int] = 20, default_batch_size: int = 1, default_stepsize: float = 1e-3, ): @@ -66,7 +66,8 @@ def svrg_optimal_batch_and_stepsize( strong_convexity : The strong convexity constant. For L2-regularized losses, this should be the regularization strength. n_power_iters : - Maximum number of iterations for the power method when finding the largest eigenvalue. + Maximum number of iterations for the power method when finding the largest eigenvalue. If set to `None`, + the lead eigenvalue is computed directly (no power method). default_batch_size : Default batch size to use if the optimal calculation fails. default_stepsize : diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index 7b5ce7eb..1c24c80d 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -1,5 +1,6 @@ import pytest import jax.numpy as jnp + from nemos.solvers import _svrg_defaults from contextlib import nullcontext as does_not_raise @@ -193,9 +194,17 @@ def test_warnigns_svrg_optimal_batch_and_stepsize( ) -@pytest.mark.parametrize("n_power_iter", [None, 1, 10]) -def test_glm_softplus_poisson_l_smooth_power_iter(x_sample, y_sample, n_power_iter): - _svrg_defaults._glm_softplus_poisson_l_smooth(x_sample, y_sample, n_power_iter) +@pytest.mark.parametrize( + "n_power_iter, expectation", + [ + (None, pytest.warns(UserWarning, match="Direct computation of the eigenvalues")), + (1, does_not_raise()), + (10, does_not_raise()) + ] +) +def test_glm_softplus_poisson_l_smooth_power_iter(x_sample, y_sample, n_power_iter, expectation): + with expectation: + _svrg_defaults._glm_softplus_poisson_l_smooth(x_sample, y_sample, batch_size=1, n_power_iters=n_power_iter) @pytest.mark.parametrize( From 9dc8e6e8008e9c7b74f1c5dd1528f1ac1ec43fc8 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 28 Oct 2024 12:20:54 -0400 Subject: [PATCH 52/52] added typeerror and valueerror, as well as tests --- src/nemos/solvers/_svrg_defaults.py | 14 ++++++++++++++ tests/test_svrg_defaults.py | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/nemos/solvers/_svrg_defaults.py b/src/nemos/solvers/_svrg_defaults.py index a3d1f7ee..a12d1098 100644 --- a/src/nemos/solvers/_svrg_defaults.py +++ b/src/nemos/solvers/_svrg_defaults.py @@ -296,7 +296,19 @@ def _glm_softplus_poisson_l_smooth( ------- : Smoothness constant `L`. + + Raises + ------ + TypeError: + If `n_power_iters` is not an integer or None. + RuntimeError: + If the eigienvalue computation does not converge. + ValueError: + If a negative integer is provided. """ + if n_power_iters and not isinstance(n_power_iters, int): + raise TypeError("`n_power_iters` must be an integer or None.") + if n_power_iters is None: try: warnings.warn( @@ -314,6 +326,8 @@ def _glm_softplus_poisson_l_smooth( "Consider using the power method by setting the `n_power_iters` parameter." ) else: + if n_power_iters <= 0: + raise ValueError("`n_power_iters` must be positive.") # Use power iteration to find the largest eigenvalue return _glm_softplus_poisson_l_smooth_with_power_iteration( X, y, n_power_iters, batch_size=batch_size diff --git a/tests/test_svrg_defaults.py b/tests/test_svrg_defaults.py index 1c24c80d..ff6a117e 100644 --- a/tests/test_svrg_defaults.py +++ b/tests/test_svrg_defaults.py @@ -199,7 +199,10 @@ def test_warnigns_svrg_optimal_batch_and_stepsize( [ (None, pytest.warns(UserWarning, match="Direct computation of the eigenvalues")), (1, does_not_raise()), - (10, does_not_raise()) + (10, does_not_raise()), + ("a", pytest.raises(TypeError, match="`n_power_iters` must be an integer or None")), + (0.5, pytest.raises(TypeError, match="`n_power_iters` must be an integer or None")), + (-1, pytest.raises(ValueError, match="`n_power_iters` must be positive")) ] ) def test_glm_softplus_poisson_l_smooth_power_iter(x_sample, y_sample, n_power_iter, expectation):