Updates to minimize.least_squares. Fixes #1585

- Residual callable is vectorized for easy multithreading by the user. Internally all vectors are now explicitly column vectors.
- Removed central findiff option, it wasn't applicable at the bounds anyway and just complicated the code.
- Added optional user-provided norm function for non-quadratic (robust) norms.
- Added an iter_callback callable for user convenience.
- Added option to internally check user-provided Jacobian and norm against finite differences.
- Updated the notebook accordingly.

PiperOrigin-RevId: 631205889
Change-Id: I3b9f8893756e329640de464e6f2e26a39e7dbd9e
This commit is contained in:
Yuval Tassa
2024-05-06 15:34:56 -07:00
committed by Copybara-Service
parent 93b57c1421
commit 34e537e557
3 changed files with 1223 additions and 803 deletions
+827 -635
View File
File diff suppressed because it is too large Load Diff
+238 -116
View File
@@ -14,6 +14,7 @@
# ==============================================================================
"""Nonlinear Least Squares minimization with box bounds."""
import abc
import dataclasses
import enum
import time
@@ -32,14 +33,14 @@ class Verbosity(enum.Enum):
class Status(enum.Enum):
FACTORIZATION_FAILED = enum.auto()
NO_IMPORVEMENT = enum.auto()
NO_IMPROVEMENT = enum.auto()
MAX_ITER = enum.auto()
DX_TOL = enum.auto()
_STATUS_MESSAGE = {
Status.FACTORIZATION_FAILED: 'factorization failed.',
Status.NO_IMPORVEMENT: 'insufficient reduction.',
Status.NO_IMPROVEMENT: 'insufficient reduction.',
Status.MAX_ITER: 'maximum iterations reached.',
Status.DX_TOL: 'norm(dx) < tol.',
}
@@ -68,90 +69,70 @@ class IterLog:
step: Optional[np.ndarray] = None
def jacobian_fd(
residual: Callable[[np.ndarray], np.ndarray],
x: np.ndarray,
r: np.ndarray,
eps: np.float64,
central: bool,
n_res: int,
bounds: Optional[List[np.ndarray]] = None,
):
"""Finite-difference Jacobian of a residual function.
class Norm(abc.ABC):
"""Abstract interface for norm functions, measuring the magnitude of vectors.
Args:
residual: function that returns the residual for a given point.
x: point at which to evaluate the Jacobian.
r: residual at x.
eps: finite-difference step size.
central: whether to use central differences.
n_res: number or residual evaluations so far.
bounds: optional pair of lower and upper bounds.
Key Concepts:
Returns:
jac: Jacobian of the residual at x.
n_res: updated number of residual evaluations.
* Norm Value: The value of the norm for a given input vector.
* Gradient and Hessian: The gradient (first derivative) and Hessian (second
derivative) of the norm function with respect to the input vector.
Subclasses Must Implement:
* `value(self, r: np.ndarray)`: Computes and returns the norm value for the
input vector `r`.
* `grad_hess(self, r: np.ndarray, proj: np.ndarray)`: Computes and returns
both the gradient and Hessian of the norm at `r`, projected onto `proj`.
The reason we ask the user to perform the projection themselves is that
norm Hessians are often large and sparse, and the "sandwich" projection
operator `proj.T @ hess @ proj` can be computed efficiently by taking the
specific norm structure into account.
"""
nx = x.size
nr = r.size
jac = np.zeros((nr, nx))
xh = x.copy()
if bounds is None:
# No bounds, simple forward or central differencing.
for i in range(nx):
xh[i] = x[i] + eps
rp = residual(xh)
if central:
xh[i] = x[i] - eps
rm = residual(xh)
jac[:, i] = (rp - rm) / (2*eps)
else:
jac[:, i] = (rp - r) / eps
xh[i] = x[i]
n_res += 2*nx if central else nx
else:
lower, upper = bounds
midpoint = 0.5 * (upper - lower)
for i in range(nx):
# Scale eps, don't cross bounds.
eps_i = eps * (upper[i] - lower[i])
if central:
# Use central differencing if away from bounds.
if x[i] - eps_i < lower[i]:
# Near lower bound, use forward.
xh[i] = x[i] + eps_i
rp = residual(xh)
jac[:, i] = (rp - r) / eps_i
n_res += 1
elif x[i] + eps_i > upper[i]:
# Near upper bound, use backward.
xh[i] = x[i] - eps_i
rm = residual(xh)
jac[:, i] = (r - rm) / eps_i
n_res += 1
else:
# Use central.
xh[i] = x[i] + eps_i
rp = residual(xh)
xh[i] = x[i] - eps_i
rm = residual(xh)
jac[:, i] = (rp - rm) / (2*eps_i)
n_res += 2
else:
# Below midpoint use forward differencing, otherwise backward.
if x[i] < midpoint[i]:
xh[i] = x[i] + eps_i
rp = residual(xh)
jac[:, i] = (rp - r) / eps_i
else:
xh[i] = x[i] - eps_i
rm = residual(xh)
jac[:, i] = (r - rm) / eps_i
n_res += 1
# Reset.
xh[i] = x[i]
return jac, n_res
@abc.abstractmethod
def value(self, r: np.ndarray) -> np.float64:
"""Returns the value of the norm at the input vector `y = norm(r)`."""
pass
@abc.abstractmethod
def grad_hess(self, r: np.ndarray, proj: np.ndarray):
"""Computes the projected gradient and Hessian of the norm at `r`.
Args:
r: A NumPy column vector (nr x 1).
proj: A pre-computed projection matrix (nr x nx).
Returns:
A tuple containing:
* Projected gradient: proj.T @ (d_norm/d_r).
* Projected Hessian: proj.T @ (d^2_norm/d_r^2) @ proj.
"""
pass
class Quadratic(Norm):
"""Implementation of the quadratic norm."""
def value(self, r: np.ndarray):
"""Returns the quadratic norm of `r`."""
return 0.5 * (r.T @ r).item()
def grad_hess(self, r: np.ndarray, proj: np.ndarray):
"""Computes the projected gradient and Hessian of the quadratic norm at `r`.
Args:
r: A NumPy column vector (nr x 1).
proj: A pre-computed projection matrix (nr x nx).
Returns:
A tuple containing:
* Projected gradient: `proj.T @ r`.
* Projected Hessian: `proj.T @ proj`.
"""
grad = proj.T @ r
hess = proj.T @ proj # Notionally proj.T @ np.eye(r.size) @ proj
return grad, hess
def least_squares(
@@ -159,33 +140,38 @@ def least_squares(
residual: Callable[[np.ndarray], np.ndarray],
bounds: Optional[List[np.ndarray]] = None,
jacobian: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None,
norm: Norm = Quadratic(),
eps: float = 1e-6,
central: bool = False,
mu_min: float = 1e-6,
mu_max: float = 1e8,
mu_factor: float = 10.0**0.1,
tol: float = 1e-7,
tol: float = 1e-6,
max_iter: int = 100,
verbose: Union[Verbosity, int] = Verbosity.ITER,
output: Optional[TextIO] = None,
iter_callback: Optional[Callable[[List[IterLog]], None]] = None,
check_derivatives: bool = False,
) -> Tuple[np.ndarray, List[IterLog]]:
"""Nonlinear Least Squares minimization with box bounds.
Args:
x0: initial guess
residual: function that returns the residual for a given point x.
bounds: optional pair of lower and upper bounds on the solution.
jacobian: optional function that returns Jacobian of the residual at a given
x0: Initial guess
residual: Vectorized function returning the residual for 1 or more points.
bounds: Optional pair of lower and upper bounds on the solution.
jacobian: Optional function that returns Jacobian of the residual at a given
point and residual. If not given, `residual` will be finite-differenced.
eps: perurbation used for automatic finite-differencing.
central: whether to use central differences.
mu_min: minimum value of the regularizer.
mu_max: maximum value of the regularizer.
mu_factor: factor increasing or decreasing the regularizer.
tol: termination tolerance on the step size.
max_iter: maximum number of iterations.
verbose: verbosity level.
output: optional file or StringIO to which to print messages.
norm: Norm object returning norm scalar or its projected gradient and
Hessian. See Norm class for detailed documentation.
eps: Perurbation used for automatic finite-differencing.
mu_min: Minimum value of the regularizer.
mu_max: Maximum value of the regularizer.
mu_factor: Factor for increasing or decreasing the regularizer.
tol: Termination tolerance on the step size.
max_iter: Maximum number of iterations.
verbose: Verbosity level.
output: Optional file or StringIO to which to print messages.
iter_callback: Optional iteration callback, takes trace argument.
check_derivatives: Compare user-defined Jacobian and norm against fin-diff.
Returns:
x: best solution found
@@ -202,10 +188,10 @@ def least_squares(
# Initialize locals.
status = Status.MAX_ITER
i = 0
x = x0.astype(np.float64)
n = x.size
xnew = np.zeros((n,))
dx = np.zeros((n,))
n = x0.size
x = x0.astype(np.float64).reshape((n, 1))
xnew = np.zeros((n, 1))
dx = np.zeros((n, 1))
scratch = np.zeros((n, n + 7))
eps = np.float64(eps)
mu = np.float64(0.0) # Optimistically start with no regularization.
@@ -234,6 +220,8 @@ def least_squares(
n_reduc = 0 # Reset n_reduc.
return mu, n_reduc
# Make local copy of bounds to avoid reshaping user input.
bounds = None if bounds is None else bounds.copy()
if bounds is not None:
# Checks bounds.
if len(bounds) != 2:
@@ -244,7 +232,10 @@ def least_squares(
raise ValueError('bounds must be finite.')
if not np.all(bounds[0] < bounds[1]):
raise ValueError('bounds[0] must be smaller than bounds[1].')
# Clip.
# Reshape and clip.
bounds[0] = bounds[0].reshape(n, 1)
bounds[1] = bounds[1].reshape(n, 1)
np.clip(x, bounds[0], bounds[1], out=x)
# Check for NaNs.
@@ -267,21 +258,28 @@ def least_squares(
break
# Get objective y.
y = 0.5 * r.dot(r)
y = norm.value(r)
# Get Jacobian jac.
t_start = time.time()
if jacobian is None:
jac, n_res = jacobian_fd(residual, x, r, eps, central, n_res, bounds)
jac, n_res = jacobian_fd(residual, x, r, eps, n_res, bounds)
t_res += time.time() - t_start
else:
jac = jacobian(x, r)
t_jac += time.time() - t_start
n_jac += 1
# Check user-provided Jacobian
if i == 0 and check_derivatives:
n_res = check_jacobian(residual, x, r, jac, eps, n_res, bounds, output)
# Check user-provided norm
if i == 0 and check_derivatives and not isinstance(norm, Quadratic):
check_norm(r, norm, eps, output)
# Get gradient, Gauss-Newton Hessian.
grad = jac.T @ r
hess = jac.T @ jac
grad, hess = norm.grad_hess(r, jac)
# Bounds relative to x
dlower = None if bounds is None else bounds[0] - x
@@ -316,13 +314,13 @@ def least_squares(
n_res += 1
# New objective, evaluate reduction.
ynew = 0.5 * rnew.dot(rnew)
ynew = norm.value(rnew)
reduction = y - ynew
armijo = reduction + armijo_c1*grad.dot(dx)
armijo = reduction + armijo_c1 * (grad.T @ dx).item()
if armijo < 0:
if mu >= mu_max:
status = Status.NO_IMPORVEMENT
status = Status.NO_IMPROVEMENT
break
mu, n_reduc = increase_mu(mu)
@@ -330,7 +328,7 @@ def least_squares(
break
# Compute reduction ratio.
expected_reduction = -(grad.dot(dx) + 0.5 * dx.T @ hess @ dx)
expected_reduction = -(grad.T @ dx + 0.5 * dx.T @ hess @ dx).item()
reduction_ratio = 0.0
if expected_reduction <= 0:
if verbose > Verbosity.SILENT.value:
@@ -352,11 +350,13 @@ def least_squares(
)
print(message, file=output)
# Append log to trace.
# Append log to trace, call iter_callback.
log = IterLog(candidate=x, objective=y, reduction=reduction, regularizer=mu)
if verbose >= Verbosity.FULLITER.value:
log = dataclasses.replace(log, residual=r, jacobian=jac, step=dx)
trace.append(log)
if iter_callback is not None:
iter_callback(trace)
# Check for success.
if dx_norm < tol:
@@ -373,12 +373,14 @@ def least_squares(
x = xnew
r = rnew
# Append final log to trace.
# Note: unlike other iter logs, this is at the end point.
yfinal = 0.5 * r.dot(r)
red = np.float64(0.0)
# Append final log to trace, call iter_callback.
# Note: unlike other iter logs, values are computed at the end point.
yfinal = norm.value(r)
red = np.float64(0.0) # No reduction sice we didn't take a step.
log = IterLog(candidate=x, objective=yfinal, reduction=red, regularizer=mu)
trace.append(log)
if iter_callback is not None:
iter_callback(trace)
# Print final diagnostics.
if verbose > Verbosity.SILENT.value:
@@ -401,4 +403,124 @@ def least_squares(
message += f' Jacobian {jac_percent:<.1f}%'
print(message, file=output)
return x, trace
return x.reshape(x0.shape), trace
def jacobian_fd(
residual: Callable[[np.ndarray], np.ndarray],
x: np.ndarray,
r: np.ndarray,
eps: np.float64,
n_res: int,
bounds: Optional[List[np.ndarray]] = None,
) -> Tuple[np.ndarray, int]:
"""Finite-difference Jacobian of a residual function.
Args:
residual: vectorized function that returns the residual of a vector array.
x: point at which to evaluate the Jacobian.
r: residual at x.
eps: finite-difference step size.
n_res: number or residual evaluations so far.
bounds: optional pair of lower and upper bounds.
Returns:
jac: Jacobian of the residual at x.
n_res: updated number of residual evaluations (add x.size).
"""
n = x.size
if bounds is None:
eps_vec = eps * np.ones(n)
else:
mid = 0.5 * (bounds[1] - bounds[0])
eps_vec = np.where(x > mid, -eps, eps).flatten()
xh = x + np.diag(eps_vec)
rh = residual(xh)
jac = (rh - r) / eps_vec
return jac, n_res+n
def check_jacobian(
residual: Callable[[np.ndarray], np.ndarray],
x: np.ndarray,
r: np.ndarray,
jac: np.ndarray,
eps: np.float64,
n_res: int,
bounds: Optional[List[np.ndarray]] = None,
output: Optional[TextIO] = None,
name: Optional[str] = 'Jacobian',
) -> int:
"""Check user-provided Jacobian against internal finite-differencing.
Args:
residual: vectorized function that returns the residual of a vector array.
x: point at which the r and jac were evaluated.
r: residual at x.
jac: Jacobian at x.
eps: finite-difference step size.
n_res: number or residual evaluations so far.
bounds: optional pair of lower and upper bounds.
output: Optional file or StringIO to which to print messages.
name: Optional name of the function being tested.
Returns:
n_res: updated number of residual evaluations.
"""
jac_fd, n_res = jacobian_fd(residual, x, r, eps, n_res, bounds)
denom = np.abs(jac).sum() + np.abs(jac_fd).sum() + 1e-8
rel_diff = np.abs(jac - jac_fd) / denom
if np.any(rel_diff > 1e-5):
raise ValueError(f'User-provided {name} does not match finite-differences '
'to a relative tolerance of 1e-5.')
print(f'User-provided {name} matches finite-differences.', file=output)
return n_res
def check_norm(
r: np.ndarray,
norm: Norm,
eps: np.float64,
output: Optional[TextIO] = None,
):
"""Check user-provided norm against internal finite-differencing.
Args:
r: residual vector.
norm: Norm function returning either the norm scalar or its gradient
and Gauss-Newton Hessian.
eps: finite-difference step size.
output: Optional file or StringIO to which to print messages.
"""
# Get norm(r) value and 1st, 2nd derivatives.
n = np.atleast_2d(norm.value(r)) # norm value as 1x1 array.
eye = np.eye(r.size) # Identity projection.
n_g, n_h = norm.grad_hess(r, eye) # Gradient and Hessian.
# Check that Hessian is symmetric.
if not np.allclose(n_h, n_h.T):
raise ValueError('User-provided norm Hessian is not symmetric.')
# Check that Hessian is positive-definite.
if np.any(np.linalg.eigvals(n_h) < 0):
h_min = np.min(np.linalg.eigvals(n_h))
raise ValueError('User-provided norm Hessian is not positive definite. '
f'Minimum eigenvalue is {h_min:<.4g}')
# Local function returning norm values (vectorized).
def norm_vec(v):
norms = [np.atleast_2d(norm.value(v[:, i:i+1])) for i in range(v.shape[1])]
return np.hstack(norms)
# Check the norm gradient.
check_jacobian(norm_vec, r, n, n_g.T, eps, 0, None, output, 'norm gradient')
# Local function returning norm gradients (vectorized).
def grad_vec(v):
gradients = [norm.grad_hess(v[:, i:i+1], eye)[0] for i in range(v.shape[1])]
return np.hstack(gradients)
# Check the norm Hessian.
check_jacobian(grad_vec, r, n_g, n_h, eps, 0, None, output, 'norm Hessian')
+158 -52
View File
@@ -15,7 +15,6 @@
"""Tests for minimize.py."""
import io
from typing import Tuple
from absl.testing import absltest
from mujoco import minimize
@@ -25,20 +24,19 @@ import numpy as np
class MinimizeTest(absltest.TestCase):
def test_basic(self) -> None:
def residual(x: np.ndarray) -> np.ndarray:
return np.array([1 - x[0], 10 * (x[1] - x[0] ** 2)], dtype=np.float64)
def residual(x):
return np.stack([1 - x[0, :], 10 * (x[1, :] - x[0, :] ** 2)])
for central in [False, True]:
out = io.StringIO()
x0 = np.array((0.0, 0.0))
x, _ = minimize.least_squares(x0, residual, output=out, central=central)
expected_x = np.array((1.0, 1.0))
np.testing.assert_array_almost_equal(x, expected_x)
self.assertContainsSubsequence(out.getvalue(), 'norm(dx) < tol')
out = io.StringIO()
x0 = np.array((0.0, 0.0))
x, _ = minimize.least_squares(x0, residual, output=out)
expected_x = np.array((1.0, 1.0))
np.testing.assert_array_almost_equal(x, expected_x)
self.assertContainsSubsequence(out.getvalue(), 'norm(dx) < tol')
def test_start_at_minimum(self) -> None:
def residual(x: np.ndarray) -> np.ndarray:
return np.array([1 - x[0], 10 * (x[1] - x[0] ** 2)])
def residual(x):
return np.stack([1 - x[0, :], 10 * (x[1, :] - x[0, :] ** 2)])
out = io.StringIO()
x0 = np.array((1.0, 1.0))
@@ -49,33 +47,36 @@ class MinimizeTest(absltest.TestCase):
self.assertContainsSubsequence(out.getvalue(), 'exact minimum found')
def test_jac_callback(self) -> None:
def residual(x: np.ndarray) -> np.ndarray:
return np.array([1 - x[0], 10 * (x[1] - x[0] ** 2)])
def residual(x):
return np.stack([1 - x[0, :], 10 * (x[1, :] - x[0, :] ** 2)])
def jacobian(x: np.ndarray, r: np.ndarray) -> Tuple[float, np.ndarray]:
def jacobian(x, r):
del r # Unused.
return np.array([[-1, 0], [-20 * x[0], 10]])
return np.array([[-1, 0], [-20 * x[0, 0], 10]])
x0 = np.array((0.0, 0.0))
out = io.StringIO()
x, _ = minimize.least_squares(x0, residual, jacobian=jacobian, output=out)
x, _ = minimize.least_squares(x0, residual, jacobian=jacobian, output=out,
check_derivatives=True)
expected_x = np.array((1.0, 1.0))
np.testing.assert_array_almost_equal(x, expected_x)
self.assertContainsSubsequence(out.getvalue(), 'norm(dx) < tol')
self.assertContainsSubsequence(out.getvalue(), 'Jacobian matches')
# Try with bad Jacobian, expect no improvement.
def jac_bad1(x: np.ndarray, r: np.ndarray) -> Tuple[float, np.ndarray]:
return -jacobian(x, r)
out1 = io.StringIO()
minimize.least_squares(x0, residual, jacobian=jac_bad1, output=out1)
self.assertContainsSubsequence(out1.getvalue(), 'insufficient reduction')
# Try with bad Jacobian, ask least_squares to check it.
def bad_jacobian(x, r):
del r # Unused.
return np.array([[-1, 0], [-20 * x[0, 0], 15]])
with self.assertRaisesRegex(ValueError, r'\bJacobian does not match\b'):
minimize.least_squares(x0, residual, jacobian=bad_jacobian, output=out,
check_derivatives=True)
def test_max_iter(self) -> None:
dim = 20 # High-D Rosenbrock
def residual(x: np.ndarray) -> np.ndarray:
res0 = [1 - x[i] for i in range(dim - 1)]
res1 = [10 * (x[i] - x[i + 1] ** 2) for i in range(dim - 1)]
def residual(x):
res0 = [1 - x[i, :] for i in range(dim - 1)]
res1 = [10 * (x[i, :] - x[i + 1, :] ** 2) for i in range(dim - 1)]
return np.asarray(res0 + res1)
# Fail to reach minimum after 20 iterations.
@@ -90,8 +91,8 @@ class MinimizeTest(absltest.TestCase):
np.testing.assert_array_almost_equal(x, expected_x)
def test_bounds(self) -> None:
def residual(x: np.ndarray) -> np.ndarray:
return np.array([1 - x[0], 10 * (x[1] - x[0] ** 2)])
def residual(x):
return np.stack([1 - x[0, :], 10 * (x[1, :] - x[0, :] ** 2)])
out = io.StringIO()
x0 = np.array((0.0, 0.0))
@@ -108,32 +109,28 @@ class MinimizeTest(absltest.TestCase):
self.assertContainsSubsequence(out.getvalue(), 'norm(dx) < tol')
# Test different bounds conditions.
verbose = minimize.Verbosity.FULLITER
for central in [False, True]:
for bounds in bounds_types.values():
out = io.StringIO()
x, trace = minimize.least_squares(
x0,
residual,
bounds=bounds,
output=out,
central=central,
verbose=verbose,
)
self.assertContainsSubsequence(out.getvalue(), ' < tol')
grad = trace[-2].jacobian.T @ trace[-2].residual
# If x_i is on the boundary, gradient points out, otherwise it is 0.
for i, xi in enumerate(x):
if xi == bounds[0][i]:
self.assertGreater(grad[i], 0)
elif xi == bounds[1][i]:
self.assertLess(grad[i], 0)
else:
self.assertAlmostEqual(grad[i], 0, places=4)
for bounds in bounds_types.values():
out = io.StringIO()
x, trace = minimize.least_squares(
x0,
residual,
bounds=bounds,
output=out,
verbose=minimize.Verbosity.FULLITER,
)
self.assertContainsSubsequence(out.getvalue(), ' < tol')
grad = trace[-2].jacobian.T @ trace[-2].residual
# If x_i is on the boundary, gradient points out, otherwise it is 0.
for i, xi in enumerate(x):
if xi == bounds[0][i]:
self.assertGreater(grad[i], 0)
elif xi == bounds[1][i]:
self.assertLess(grad[i], 0)
else:
self.assertAlmostEqual(grad[i].item(), 0, places=4)
def test_bad_bounds(self) -> None:
def residual(x: np.ndarray) -> np.ndarray:
def residual(x):
return np.array([1 - x[0], 10 * (x[1] - x[0] ** 2)])
out = io.StringIO()
@@ -150,5 +147,114 @@ class MinimizeTest(absltest.TestCase):
with self.assertRaises(ValueError):
minimize.least_squares(x0, residual, bounds=bounds, output=out)
def test_iter_callback(self) -> None:
def residual(x):
return np.stack([1 - x[0, :], 10 * (x[1, :] - x[0, :] ** 2)])
out = io.StringIO()
def iter_callback(trace):
print(f'Hello iteration {len(trace)}!', file=out)
x0 = np.array((0.0, 0.0))
x, _ = minimize.least_squares(x0, residual, output=out,
iter_callback=iter_callback)
expected_x = np.array((1.0, 1.0))
np.testing.assert_array_almost_equal(x, expected_x)
self.assertContainsSubsequence(out.getvalue(), 'Hello iteration 3!')
def test_norm(self) -> None:
def residual(x):
return np.stack([1 - x[0, :], 10 * (x[1, :] - x[0, :] ** 2)])
p = 0.01 # Smoothing radius for smooth-L2 norm.
class SmoothL2(minimize.Norm):
def value(self, r):
return np.sqrt((r.T @ r).item() + p*p) - p
def grad_hess(self, r, proj):
s = np.sqrt((r.T @ r).item() + p*p)
y_r = r / s
grad = proj.T @ y_r
y_rr = (np.eye(r.size) - y_r @ y_r.T) / s
hess = proj.T @ y_rr @ proj
return grad, hess
out = io.StringIO()
x0 = np.array((0.0, 0.0))
x, _ = minimize.least_squares(x0, residual, norm=SmoothL2(), output=out,
check_derivatives=True)
expected_x = np.array((1.0, 1.0))
np.testing.assert_array_almost_equal(x, expected_x)
self.assertContainsSubsequence(out.getvalue(), 'norm(dx) < tol')
self.assertContainsSubsequence(out.getvalue(),
'User-provided norm gradient matches')
self.assertContainsSubsequence(out.getvalue(),
'User-provided norm Hessian matches')
class SmoothL2BadGrad(minimize.Norm):
def value(self, r):
return np.sqrt((r.T @ r).item() + p*p) - p
def grad_hess(self, r, proj):
s = np.sqrt((r.T @ r).item() + p*p)
y_r = r / s
grad = proj.T @ (y_r + 0.001) # 0.001 is erronous.
y_rr = (np.eye(r.size) - y_r @ y_r.T) / s
hess = proj.T @ y_rr @ proj
return grad, hess
with self.assertRaisesRegex(ValueError, r'\bgradient does not match\b'):
minimize.least_squares(x0, residual, norm=SmoothL2BadGrad(), output=out,
check_derivatives=True)
class SmoothL2BadHess(minimize.Norm):
def value(self, r):
return np.sqrt((r.T @ r).item() + p*p) - p
def grad_hess(self, r, proj):
s = np.sqrt((r.T @ r).item() + p*p)
y_r = r / s
grad = proj.T @ y_r
y_rr = (1.001 * np.eye(r.size) - y_r @ y_r.T) / s # 1.001 is erronous.
hess = proj.T @ y_rr @ proj
return grad, hess
with self.assertRaisesRegex(ValueError, r'\bHessian does not match\b'):
minimize.least_squares(x0, residual, norm=SmoothL2BadHess(), output=out,
check_derivatives=True)
class SmoothL2AsymHess(minimize.Norm):
def value(self, r):
return np.sqrt((r.T @ r).item() + p*p) - p
def grad_hess(self, r, proj):
s = np.sqrt((r.T @ r).item() + p*p)
y_r = r / s
grad = proj.T @ y_r
y_rr = (np.eye(r.size) - (y_r + 0.0001) @ y_r.T) / s
hess = proj.T @ y_rr @ proj
return grad, hess
with self.assertRaisesRegex(ValueError, r'\bnot symmetric\b'):
minimize.least_squares(x0, residual, norm=SmoothL2AsymHess(), output=out,
check_derivatives=True)
class SmoothL2NegHess(minimize.Norm):
def value(self, r):
return np.sqrt((r.T @ r).item() + p*p) - p
def grad_hess(self, r, proj):
s = np.sqrt((r.T @ r).item() + p*p)
y_r = r / s
grad = proj.T @ y_r
y_rr = -(np.eye(r.size) - y_r @ y_r.T) / s # Negative-definite.
hess = proj.T @ y_rr @ proj
return grad, hess
with self.assertRaisesRegex(ValueError, r'\bnot positive definite\b'):
minimize.least_squares(x0, residual, norm=SmoothL2NegHess(), output=out,
check_derivatives=True)
if __name__ == '__main__':
absltest.main()