Skip to content

Commit 64913c1

Browse files
Implement scaling update as maximum hessian diagonal encountered so far
1 parent ec00885 commit 64913c1

1 file changed

Lines changed: 32 additions & 40 deletions

File tree

optimistix/_solver/levenberg_marquardt.py

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import equinox as eqx
55
import jax
6+
import jax.flatten_util as jfu
67
import jax.lax as lax
78
import jax.numpy as jnp
89
import jax.tree_util as jtu
@@ -21,8 +22,7 @@
2122

2223

2324
UpdateScalingFn: 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

4040
def 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

326320
class _ScaledDampedNewtonDescentState(eqx.Module):
327321
f_info: FunctionInfo.EvalGradHessian | FunctionInfo.ResidualJac
328-
scaling_operator: lx.AbstractLinearOperator
322+
scaling_operator: lx.DiagonalLinearOperator
329323

330324

331325
class 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(
401392
ScaledDampedNewtonDescent.__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

Comments
 (0)