Coverage for local_installation_linux/mumott/optimization/optimizers/lbfgs.py: 100%

42 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-08-11 23:08 +0000

1import logging 

2 

3from typing import Dict, Any 

4 

5from numpy import float64 

6 

7from scipy.optimize import minimize 

8 

9from mumott.core.hashing import list_to_hash 

10from mumott.optimization.loss_functions.base_loss_function import LossFunction 

11from .base_optimizer import Optimizer 

12 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17class LBFGS(Optimizer): 

18 """This Optimizer makes the :term:`LBFGS` algorithm from `scipy.optimize 

19 <https://docs.scipy.org/doc/scipy/reference/optimize.html>`_ 

20 available for usage with a :class:`LossFunction <module-mumott.optimization.loss_functions>`. 

21 

22 Parameters 

23 ---------- 

24 loss_function : LossFunction 

25 The :ref:`loss function <loss_functions>` to be minimized using this algorithm. 

26 kwargs : Dict[str, Any] 

27 Miscellaneous options. See notes for valid entries. 

28 

29 Notes 

30 ----- 

31 Valid entries in :attr:`kwargs` are 

32 

33 x0 

34 Initial guess for solution vector. Must be the same size as 

35 :attr:`residual_calculator.coefficients`. Defaults to :attr:`loss_function.initial_values`. 

36 bounds 

37 Used to set the ``bounds`` of the optimization method, see `scipy.optimize.minimize 

38 <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`_ 

39 documentation for details. Defaults to ``None``. 

40 maxiter 

41 Maximum number of iterations. Defaults to ``10``. 

42 disp 

43 Whether to display output from the optimizer. Defaults to ``False`` 

44 maxcor 

45 Maximum number of Hessian corrections to the Jacobian. Defaults to ``10``. 

46 iprint 

47 If ``disp`` is true, controls output with no output if ``iprint < 0``, 

48 convergence output only if ``iprint == 0``, iteration-wise output if 

49 ``0 < iprint <= 99``, and sub-iteration output if ``iprint > 99``. 

50 maxfun 

51 Maximum number of function evaluations, including line search evaluations. 

52 Defaults to ``20``. 

53 ftol 

54 Relative change tolerance for objective function. Changes to absolute change tolerance 

55 if objective function is ``< 1``, see `scipy.optimize.minimize 

56 <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`_ 

57 documentation, which may lead to excessively fast convergence. 

58 Defaults to ``1e-3``. 

59 gtol 

60 Convergence tolerance for gradient. Defaults to ``1e-5``. 

61 """ 

62 

63 def __init__(self, 

64 loss_function: LossFunction, 

65 **kwargs: Dict[str, Any]): 

66 super().__init__(loss_function, **kwargs) 

67 # This will later be used to reshape the flattened output. 

68 self._output_shape = None 

69 

70 def optimize(self) -> Dict: 

71 """ Executes the optimization using the options stored in this class 

72 instance. The optimization will continue until convergence, 

73 or until the maximum number of iterations (:attr:`maxiter`) is exceeded. 

74 

75 Returns 

76 ------- 

77 A ``dict`` of optimization results. See `scipy.optimize.OptimizeResult 

78 <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.OptimizeResult.html>`_ 

79 for details. The entry ``'x'``, which contains the result, will be reshaped using 

80 the shape of the gradient from :attr:`loss_function`. 

81 """ 

82 lbfgs_kwargs = dict(x0=self._loss_function.initial_values, 

83 bounds=None) 

84 misc_options = dict(maxiter=10, 

85 disp=False, 

86 maxcor=10, 

87 iprint=1, 

88 maxfun=20, 

89 ftol=1e-3, 

90 gtol=1e-5) 

91 

92 for k in lbfgs_kwargs: 

93 if k in dict(self): 

94 lbfgs_kwargs[k] = self[k] 

95 

96 for k in misc_options: 

97 if k in dict(self): 

98 misc_options[k] = self[k] 

99 

100 for k in dict(self): 

101 if k not in lbfgs_kwargs and k not in misc_options: 

102 logger.warning(f'Unknown option {k}, with value {self[k]}, has been ignored.') 

103 

104 if lbfgs_kwargs['x0'] is None: 

105 lbfgs_kwargs['x0'] = self._loss_function.initial_values 

106 lbfgs_kwargs['x0'] = lbfgs_kwargs['x0'].ravel() 

107 

108 with self._tqdm(misc_options['maxiter']) as progress: 

109 

110 def progress_callback(*args, **kwargs): 

111 progress.update(1) 

112 

113 def loss_function_wrapper(coefficients): 

114 d = self._loss_function.get_loss(coefficients, get_gradient=True) 

115 # Store gradient shape to reshape flattened output. 

116 if self._output_shape is None: 

117 self._output_shape = d['gradient'].shape 

118 # LBFGS needs float64 

119 return d['loss'], d['gradient'].ravel().astype(float64) 

120 

121 result = minimize(fun=loss_function_wrapper, callback=progress_callback, 

122 **lbfgs_kwargs, jac=True, method='L-BFGS-B', options=misc_options) 

123 result = dict(result) 

124 result['x'] = result['x'].reshape(self._output_shape) 

125 return dict(result) 

126 

127 def __hash__(self) -> int: 

128 to_hash = [self._options, hash(self._loss_function)] 

129 return int(list_to_hash(to_hash), 16)