33
44import equinox as eqx
55import jax
6+ import jax .flatten_util as jfu
67import jax .lax as lax
78import jax .numpy as jnp
89import jax .tree_util as jtu
2122
2223
2324UpdateScalingFn : TypeAlias = Callable [
24- [FunctionInfo .EvalGradHessian | FunctionInfo .ResidualJac , PyTree ],
25- lx .AbstractLinearOperator ,
25+ [lx .AbstractLinearOperator , lx .DiagonalLinearOperator ], lx .DiagonalLinearOperator
2626]
2727
2828
@@ -38,26 +38,20 @@ def __call__(self, y: PyTree[Array]):
3838
3939
4040def max_diagonal_scaling_update (
41- f_info : FunctionInfo .EvalGradHessian | FunctionInfo .ResidualJac ,
42- scaling_operator : lx .AbstractLinearOperator ,
43- ) -> lx .AbstractLinearOperator :
41+ hessian : lx .AbstractLinearOperator , scaling_operator : lx .DiagonalLinearOperator
42+ ) -> lx .DiagonalLinearOperator :
4443 """Update the matrix `D` that controls the relative scaling of each
45- parameter based on the procedure described by More (1977). This takes `D`
46- on each iteration as the maximum diagonal of the hessian
47- so far encountered.
44+ parameter based on the procedure described by More (1977).
45+ This takes `D` on each iteration as the maximum diagonal of the
46+ hessian so far encountered.
4847 """ # noqa: E501
49- raise NotImplementedError
50- # assert lx.is_diagonal(scaling_operator)
51- # if isinstance(f_info, FunctionInfo.EvalGradHessian):
52- # hessian = f_info.hessian
53- # else:
54- # hessian = f_info.jac.transpose() @ f_info.jac
55- # hessian_diagonal = lx.TaggedLinearOperator(
56- # hessian @ lx.IdentityLinearOperator(hessian.in_structure()), lx.diagonal_tag
57- # )
58-
59-
60- def step_levenberg_damping (
48+ diagonal , unflatten_fn = jfu .ravel_pytree (scaling_operator .diagonal )
49+ return lx .DiagonalLinearOperator (
50+ unflatten_fn (jnp .maximum (lx .diagonal (hessian ), diagonal ))
51+ )
52+
53+
54+ def damped_newton_step_levenberg (
6155 step_size : Scalar ,
6256 f_info : FunctionInfo .EvalGradHessian | FunctionInfo .ResidualJac ,
6357 linear_solver : lx .AbstractLinearSolver ,
@@ -104,11 +98,11 @@ def step_levenberg_damping(
10498 return linear_sol .value , RESULTS .promote (linear_sol .result )
10599
106100
107- def step_scaling_operator_damping (
101+ def damped_newton_step_scaled (
108102 step_size : Scalar ,
109103 f_info : FunctionInfo .EvalGradHessian | FunctionInfo .ResidualJac ,
110- scaling_operator : lx .AbstractLinearOperator ,
111104 linear_solver : lx .AbstractLinearSolver ,
105+ scaling_operator : lx .DiagonalLinearOperator ,
112106 update_scaling_fn : UpdateScalingFn ,
113107) -> tuple [PyTree [Array ], RESULTS ]:
114108 """Compute a damped Newton step that keeps track of an operator
@@ -126,14 +120,14 @@ def step_scaling_operator_damping(
126120 grad = f_info .grad
127121 elif isinstance (f_info , FunctionInfo .ResidualJac ):
128122 jac , residual = f_info .jac , f_info .residual
129- hessian = jac . transpose () @ jac
130- grad = jac .transpose () @ residual
123+ hessian = lx . TaggedLinearOperator ( jac . T @ jac , lx . positive_semidefinite_tag )
124+ grad = jac .T . mv ( residual )
131125 else :
132126 raise ValueError (
133127 "Damped newton descent cannot be used with a solver that does not "
134128 "provide (approximate) Hessian information."
135129 )
136- scaling_operator = update_scaling_fn (f_info , scaling_operator )
130+ scaling_operator = update_scaling_fn (hessian , scaling_operator )
137131 operator = hessian + lm_param * scaling_operator
138132 if lx .is_positive_semidefinite (hessian ):
139133 operator = lx .TaggedLinearOperator (operator , lx .positive_semidefinite_tag )
@@ -191,7 +185,7 @@ def query(
191185 def step (
192186 self , step_size : Scalar , state : _DampedNewtonDescentState
193187 ) -> tuple [Y , RESULTS ]:
194- sol_value , result = step_levenberg_damping (
188+ sol_value , result = damped_newton_step_levenberg (
195189 step_size , state .f_info , self .linear_solver
196190 )
197191 y_diff = (- (sol_value ** ω )).ω
@@ -281,7 +275,7 @@ def step(
281275 scaled_step_size = state .newton_norm * step_size
282276
283277 def comparison_fn (lambda_i : Scalar , _ ):
284- step , _ = step_levenberg_damping (
278+ step , _ = damped_newton_step_levenberg (
285279 1 / lambda_i , state .f_info , self .linear_solver
286280 )
287281 return self .trust_region_norm (step ) - scaled_step_size
@@ -296,7 +290,7 @@ def reject_newton():
296290 max_steps = 32 ,
297291 throw = False ,
298292 ).value
299- y_diff , result = step_levenberg_damping (
293+ y_diff , result = damped_newton_step_levenberg (
300294 1 / lambda_out , state .f_info , self .linear_solver
301295 )
302296 return y_diff , result
@@ -325,7 +319,7 @@ def accept_newton():
325319
326320class _ScaledDampedNewtonDescentState (eqx .Module ):
327321 f_info : FunctionInfo .EvalGradHessian | FunctionInfo .ResidualJac
328- scaling_operator : lx .AbstractLinearOperator
322+ scaling_operator : lx .DiagonalLinearOperator
329323
330324
331325class ScaledDampedNewtonDescent (
@@ -366,12 +360,9 @@ def init(
366360 y : Y ,
367361 f_info_struct : FunctionInfo .EvalGradHessian | FunctionInfo .ResidualJac ,
368362 ) -> _ScaledDampedNewtonDescentState :
369- if isinstance (f_info_struct , FunctionInfo .ResidualJac ):
370- raise NotImplementedError
371- del y
372363 f_info_init = tree_full_like (f_info_struct , 0 , allow_static = True )
373- scaling_operator = - jnp . inf * lx .IdentityLinearOperator (
374- f_info_struct . hessian . in_structure ( )
364+ scaling_operator = lx .DiagonalLinearOperator (
365+ tree_full_like ( y , - jnp . inf , allow_static = True )
375366 )
376367 return _ScaledDampedNewtonDescentState (f_info_init , scaling_operator )
377368
@@ -387,11 +378,11 @@ def query(
387378 def step (
388379 self , step_size : Scalar , state : _ScaledDampedNewtonDescentState
389380 ) -> tuple [Y , RESULTS ]:
390- sol_value , result = step_scaling_operator_damping (
381+ sol_value , result = damped_newton_step_scaled (
391382 step_size ,
392383 state .f_info ,
393- state .scaling_operator ,
394384 self .linear_solver ,
385+ state .scaling_operator ,
395386 self .update_scaling_fn ,
396387 )
397388 y_diff = (- (sol_value ** ω )).ω
@@ -401,10 +392,11 @@ def step(
401392ScaledDampedNewtonDescent .__init__ .__doc__ = """**Arguments:**
402393
403394- `linear_solver`: The linear solver used to compute the Newton step.
404- - `update_scaling_fn`: A function with signature `fn(f_info, descent_state)`
405- that returns a `lineax.AbstractLinearOperator` of the same structure as
406- `f_info.hessian.in_structure()`. By default, this is the procedure used
407- in [`scipy.least_squares`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html)
395+ - `update_scaling_fn`: A function with signature `fn(hessian, scaling)`,
396+ where `hessian` is an `AbstractLinearOperator` and `scaling` is a
397+ `DiagonalLinearOperator` for the scaling operator on the kth iteration.
398+ By default, this is the procedure used in [`scipy.least_squares`]
399+ (https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html)
408400""" # noqa: E501
409401
410402
0 commit comments