# License: BSD 3 clause
import numpy as np
from numpy.linalg import norm
from .base import SolverFirstOrder
from .base.utils import relative_distance
[docs]class GD(SolverFirstOrder):
"""Proximal gradient descent
For the minimization of objectives of the form
.. math::
f(w) + g(w),
where :math:`f` has a smooth gradient and :math:`g` is prox-capable.
Function :math:`f` corresponds to the ``model.loss`` method of the model
(passed with ``set_model`` to the solver) and :math:`g` corresponds to
the ``prox.value`` method of the prox (passed with the ``set_prox`` method).
One iteration of :class:`GD <tick.solver.GD>` is as follows:
.. math::
w \\gets \\mathrm{prox}_{\\eta g} \\big(w - \\eta \\nabla f(w) \\big),
where :math:`\\nabla f(w)` is the gradient of :math:`f` given by the
``model.grad`` method and :math:`\\mathrm{prox}_{\\eta g}` is given by the
``prox.call`` method. The step-size :math:`\\eta` can be tuned with
``step``. The iterations stop whenever tolerance ``tol`` is achieved, or
after ``max_iter`` iterations. The obtained solution :math:`w` is returned
by the ``solve`` method, and is also stored in the ``solution`` attribute
of the solver.
Parameters
----------
step : `float`, default=None
Step-size parameter, the most important parameter of the solver.
Whenever possible, this can be automatically tuned as
``step = 1 / model.get_lip_best()``. If ``linesearch=True``, this is
the first step-size to be used in the linesearch (that should be taken
as too large).
tol : `float`, default=1e-10
The tolerance of the solver (iterations stop when the stopping
criterion is below it)
max_iter : `int`, default=100
Maximum number of iterations of the solver.
linesearch : `bool`, default=True
If `True`, use backtracking linesearch to tune the step automatically.
verbose : `bool`, default=True
If `True`, solver verboses history, otherwise nothing is displayed,
but history is recorded anyway
print_every : `int`, default=10
Print history information every time the iteration number is a
multiple of ``print_every``. Used only is ``verbose`` is True
record_every : `int`, default=1
Save history information every time the iteration number is a
multiple of ``record_every``
linesearch_step_increase : `float`, default=2.
Factor of step increase when using linesearch
linesearch_step_decrease : `float`, default=0.5
Factor of step decrease when using linesearch
Attributes
----------
model : `Model`
The model used by the solver, passed with the ``set_model`` method
prox : `Prox`
Proximal operator used by the solver, passed with the ``set_prox``
method
solution : `numpy.array`, shape=(n_coeffs,)
Minimizer found by the solver
history : `dict`-like
A dict-type of object that contains history of the solver along
iterations. It should be accessed using the ``get_history`` method
time_start : `str`
Start date of the call to ``solve()``
time_elapsed : `float`
Duration of the call to ``solve()``, in seconds
time_end : `str`
End date of the call to ``solve()``
References
----------
* A. Beck and M. Teboulle, A fast iterative shrinkage-thresholding
algorithm for linear inverse problems,
*SIAM journal on imaging sciences*, 2009
"""
[docs] def __init__(self, step: float = None, tol: float = 0.,
max_iter: int = 100, linesearch: bool = True,
linesearch_step_increase: float = 2.,
linesearch_step_decrease: float = 0.5, verbose: bool = True,
print_every: int = 10, record_every: int = 1):
SolverFirstOrder.__init__(self, step=step, tol=tol, max_iter=max_iter,
verbose=verbose, print_every=print_every,
record_every=record_every)
self.linesearch = linesearch
self.linesearch_step_increase = linesearch_step_increase
self.linesearch_step_decrease = linesearch_step_decrease
def _initialize_values(self, x0=None, step=None):
if step is None:
if self.step is None:
if self.linesearch:
# If we use linesearch, then we can choose a large
# initial step
step = 1e9
else:
raise ValueError("No step specified.")
step, obj, x, prev_x, x_new = \
SolverFirstOrder._initialize_values(self, x0, step,
n_empty_vectors=2)
return x, prev_x, x_new, step, obj
def _gradient_step(self, x, x_new, step):
if self.linesearch:
step *= self.linesearch_step_increase
loss_x, grad_x = self.model.loss_and_grad(x)
obj_x = self.objective(x, loss=loss_x)
while True:
x_new[:] = self.prox.call(x - step * grad_x, step)
obj_x_new = self.objective(x_new)
envelope = obj_x + np.sum(grad_x * (x_new - x),
axis=None) \
+ 1. / (2 * step) * norm(x_new - x) ** 2
test = (obj_x_new <= envelope)
if test:
break
step *= self.linesearch_step_decrease
if step == 0:
break
else:
grad_x = self.model.grad(x)
x_new[:] = self.prox.call(x - step * grad_x, step)
obj_x_new = self.objective(x_new)
x[:] = x_new
return x, step, obj_x_new
def _solve(self, x0: np.ndarray = None, step: float = None):
minimizer, prev_minimizer, x_new, step, obj = self._initialize_values(
x0, step)
for n_iter in range(self.max_iter):
# We will record on this iteration and we must be ready
if self._should_record_iter(n_iter):
prev_minimizer[:] = minimizer
prev_obj = self.objective(prev_minimizer)
minimizer, step, obj = self._gradient_step(minimizer, x_new, step)
if step == 0:
print('Step equals 0... at %i' % n_iter)
break
# Let's record metrics
if self._should_record_iter(n_iter):
rel_delta = relative_distance(minimizer, prev_minimizer)
obj = self.objective(minimizer)
rel_obj = abs(obj - prev_obj) / abs(prev_obj)
converged = rel_obj < self.tol
# If converged, we stop the loop and record the last step
# in history
self._handle_history(n_iter + 1, force=converged, obj=obj,
x=minimizer.copy(), rel_delta=rel_delta,
step=step, rel_obj=rel_obj)
if converged:
break
self._set("solution", minimizer)
return minimizer