Source code for decent_bench.centralized_algorithms
from typing import TYPE_CHECKING
import numpy as np
from numpy import float64
from numpy import linalg as la
from numpy.typing import NDArray
if TYPE_CHECKING:
from decent_bench.cost_functions import CostFunction
[docs]
def gradient_descent(
cost_function: "CostFunction",
x0: NDArray[float64] | None,
*,
step_size: float,
max_iter: int,
stop_tol: float | None,
max_tol: float | None,
) -> NDArray[float64]:
"""
Find the x that minimizes the cost function using gradient descent.
Args:
cost_function: cost function to minimize
x0: initial guess, defaults to ``np.zeros()`` if ``None`` is provided
step_size: scaling factor for each update
max_iter: maximum number of iterations to run
stop_tol: early stopping criteria - stop if ``norm(x_new - x) <= stop_tol``
max_tol: maximum tolerated ``norm(x_new - x)`` at the end
Raises:
RuntimeError: if ``norm(x_new - x) > max_tol`` at the end
Returns:
x that minimizes the cost function.
"""
delta = np.inf
x = x0 if x0 is not None else np.zeros(cost_function.domain_shape)
for _ in range(max_iter):
x_new = x - step_size * cost_function.gradient(x)
delta = float(la.norm(x_new - x))
x = x_new
if stop_tol is not None and delta <= stop_tol:
break
if max_tol is not None and delta > max_tol:
raise RuntimeError(
f"Gradient descent failed to reach convergence within {max_iter} iterations with step size {step_size}."
f"Max delta acceptable: {max_tol}."
f"Actual delta: {delta}."
)
return x
[docs]
def accelerated_gradient_descent(
cost_function: "CostFunction",
x0: NDArray[float64] | None,
*,
max_iter: int,
stop_tol: float | None,
max_tol: float | None,
) -> NDArray[float64]:
r"""
Find the x that minimizes the cost function using accelerated gradient descent.
Args:
cost_function: cost function to minimize
x0: initial guess, defaults to ``np.zeros()`` if ``None`` is provided
max_iter: maximum number of iterations to run
stop_tol: early stopping criteria - stop if ``norm(x_new - x) <= stop_tol``
max_tol: maximum tolerated ``norm(x_new - x)`` at the end
Raises:
RuntimeError: if ``norm(x_new - x) > max_tol`` at the end
ValueError: if ``cost_function.m_smooth < 0``, ``cost_function.m_cvx < 0``, or cost function is affine
Returns:
x that minimizes the cost function.
"""
if x0 is not None and x0.shape != cost_function.domain_shape:
raise ValueError("x0 and cost function domain must have same shape")
if cost_function.m_smooth == 0:
raise ValueError("Function must not be affine")
if cost_function.m_smooth < 0:
raise ValueError("m_smooth must not be negative")
if cost_function.m_cvx < 0:
raise ValueError("m_cvx must not be negative")
if cost_function.m_smooth == np.inf:
raise NotImplementedError("Support for non-L-smoothness is not implemented yet")
if np.isnan(cost_function.m_smooth):
raise NotImplementedError("Support for non-global differentiability is not implemented yet")
if np.isnan(cost_function.m_cvx):
raise NotImplementedError("Support for non-convexity is not implemented yet")
x0 = x0 if x0 is not None else np.zeros(cost_function.domain_shape)
x = x0
y = x0
c = (np.sqrt(cost_function.m_smooth) - np.sqrt(cost_function.m_cvx)) / (
np.sqrt(cost_function.m_smooth) + np.sqrt(cost_function.m_cvx)
)
delta = np.inf
for k in range(1, max_iter + 1):
x_new = y - cost_function.gradient(y) / cost_function.m_smooth
delta = float(la.norm(x_new - x))
beta = c if cost_function.m_cvx > 0 else (k - 1) / (k + 2)
y_new = x_new + beta * (x_new - x)
x, y = x_new, y_new
if stop_tol is not None and delta <= stop_tol:
break
if max_tol is not None and delta > max_tol:
raise RuntimeError(
f"Accelerated gradient descent failed to reach convergence within {max_iter} iterations."
f"Max delta acceptable: {max_tol}."
f"Actual delta: {delta}."
)
return x
[docs]
def proximal_solver(cost_function: "CostFunction", y: NDArray[float64], rho: float) -> NDArray[float64]:
"""
Find the proximal at y using accelerated gradient descent.
This is the solution to the proximal operator defined as:
.. include:: snippets/proximal_operator.rst
Raises:
ValueError: if *cost_function*'s domain and *y* don't have the same shape, or if *rho* is not greater than 0
"""
if cost_function.domain_shape != y.shape:
raise ValueError("Cost function domain and y need to have the same shape")
if rho <= 0:
raise ValueError("Penalty term `rho` must be greater than 0")
from decent_bench.cost_functions import QuadraticCost # noqa: PLC0415
proximal_cost = QuadraticCost(A=np.eye(len(y)) / rho, b=-y / rho, c=y.dot(y) / (2 * rho)) + cost_function
return accelerated_gradient_descent(proximal_cost, y, max_iter=100, stop_tol=1e-10, max_tol=None)