Source code for cil.optimisation.utilities.StepSizeMethods
# Copyright 2024 United Kingdom Research and Innovation
# Copyright 2024 The University of Manchester
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Authors:
# - CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt
from abc import ABC, abstractmethod
import numpy
[docs]
class StepSizeRule(ABC):
"""
Abstract base class for a step size rule. The abstract method, `get_step_size` takes in an algorithm and thus can access all parts of the algorithm (e.g. current iterate, current gradient, objective functions etc) and from this should return a float as a step size.
"""
def __init__(self):
'''Initialises the step size rule
'''
pass
[docs]
@abstractmethod
def get_step_size(self, algorithm):
"""
Returns
--------
the calculated step size:float
"""
pass
[docs]
class ConstantStepSize(StepSizeRule):
"""
Step-size rule that always returns a constant step-size.
Parameters
----------
step_size: float
The step-size to be returned with each call.
"""
def __init__(self, step_size):
'''Initialises the constant step size rule
Parameters:
-------------
step_size : float, the constant step size
'''
self.step_size = step_size
[docs]
def get_step_size(self, algorithm):
"""
Returns
--------
the calculated step size:float
"""
return self.step_size
[docs]
class ArmijoStepSizeRule(StepSizeRule):
"""
Applies the Armijo rule to calculate the step size (step_size).
The Armijo rule runs a while loop to find the appropriate step_size by starting from a very large number (`alpha`). The step_size is found by reducing the step size (by a factor `beta`) in an iterative way until a certain criterion is met. To avoid infinite loops, we add a maximum number of times (`max_iterations`) the while loop is run.
Parameters
----------
alpha: float, optional, default=1e6
The starting point for the step size iterations
beta: float between 0 and 1, optional, default=0.5
The amount the step_size is reduced if the criterion is not met
max_iterations: integer, optional, default is numpy.ceil (2 * numpy.log10(alpha) / numpy.log10(2))
The maximum number of iterations to find a suitable step size
Reference
---------
Algorithm 3.1 (Numerical Optimization, Nocedal, Wright) (https://www.math.uci.edu/~qnie/Publications/NumericalOptimization.pdf)
https://projecteuclid.org/download/pdf_1/euclid.pjm/1102995080
"""
def __init__(self, alpha=1e6, beta=0.5, max_iterations=None):
'''Initialises the step size rule
'''
self.alpha_orig = alpha
if self.alpha_orig is None: # Can be removed when alpha and beta are deprecated in GD
self.alpha_orig = 1e6
self.beta = beta
if self.beta is None: # Can be removed when alpha and beta are deprecated in GD
self.beta = 0.5
self.max_iterations = max_iterations
if self.max_iterations is None:
self.max_iterations = numpy.ceil(2 * numpy.log10(self.alpha_orig) / numpy.log10(2))
[docs]
def get_step_size(self, algorithm):
"""
Applies the Armijo rule to calculate the step size (`step_size`)
Returns
--------
the calculated step size:float
"""
k = 0
self.alpha = self.alpha_orig
f_x = algorithm.objective_function(algorithm.solution)
self.x_armijo = algorithm.solution.copy()
while k < self.max_iterations:
algorithm.gradient_update.multiply(self.alpha, out=self.x_armijo)
algorithm.solution.subtract(self.x_armijo, out=self.x_armijo)
f_x_a = algorithm.objective_function(self.x_armijo)
sqnorm = algorithm.gradient_update.squared_norm()
if f_x_a - f_x <= - (self.alpha/2.) * sqnorm:
break
k += 1.
self.alpha *= self.beta
if k == self.max_iterations:
raise ValueError(
'Could not find a proper step_size in {} loops. Consider increasing alpha or max_iterations.'.format(self.max_iterations))
return self.alpha