Improvements to minimize.least_squares.
All changes make functionality more similar to SciPy least squares: - Use adaptive findiff epsilon. - Make termination on step size relative to norm(x). - Add termination on gradient norm. - Make default tolerances like SciPy's. PiperOrigin-RevId: 745221614 Change-Id: Iee93256651fca8154c97fa3bdaa9c67ede28e573
This commit is contained in:
committed by
Copybara-Service
parent
96dda6ea75
commit
55e3ca3acf
+39
-12
@@ -36,6 +36,7 @@ class Status(enum.Enum):
|
|||||||
NO_IMPROVEMENT = enum.auto()
|
NO_IMPROVEMENT = enum.auto()
|
||||||
MAX_ITER = enum.auto()
|
MAX_ITER = enum.auto()
|
||||||
DX_TOL = enum.auto()
|
DX_TOL = enum.auto()
|
||||||
|
G_TOL = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
_STATUS_MESSAGE = {
|
_STATUS_MESSAGE = {
|
||||||
@@ -43,6 +44,7 @@ _STATUS_MESSAGE = {
|
|||||||
Status.NO_IMPROVEMENT: 'insufficient reduction.',
|
Status.NO_IMPROVEMENT: 'insufficient reduction.',
|
||||||
Status.MAX_ITER: 'maximum iterations reached.',
|
Status.MAX_ITER: 'maximum iterations reached.',
|
||||||
Status.DX_TOL: 'norm(dx) < tol.',
|
Status.DX_TOL: 'norm(dx) < tol.',
|
||||||
|
Status.G_TOL: 'norm(gradient) < tol.',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +59,7 @@ class IterLog:
|
|||||||
regularizer: Value of the regularizer used for this iteration.
|
regularizer: Value of the regularizer used for this iteration.
|
||||||
residual: Optional value of the residual at the candidate.
|
residual: Optional value of the residual at the candidate.
|
||||||
jacobian: Optional value of the Jacobian at the candidate.
|
jacobian: Optional value of the Jacobian at the candidate.
|
||||||
|
grad: Optional value of the gradient at the candidate.
|
||||||
step: Optional change in decision variable during this iteration.
|
step: Optional change in decision variable during this iteration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -66,6 +69,7 @@ class IterLog:
|
|||||||
regularizer: np.float64
|
regularizer: np.float64
|
||||||
residual: Optional[np.ndarray] = None
|
residual: Optional[np.ndarray] = None
|
||||||
jacobian: Optional[np.ndarray] = None
|
jacobian: Optional[np.ndarray] = None
|
||||||
|
grad: Optional[np.ndarray] = None
|
||||||
step: Optional[np.ndarray] = None
|
step: Optional[np.ndarray] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -141,11 +145,12 @@ def least_squares(
|
|||||||
bounds: Optional[Sequence[np.ndarray]] = None,
|
bounds: Optional[Sequence[np.ndarray]] = None,
|
||||||
jacobian: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None,
|
jacobian: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None,
|
||||||
norm: Norm = Quadratic(),
|
norm: Norm = Quadratic(),
|
||||||
eps: float = 1e-6,
|
eps: float = np.finfo(np.float64).eps ** 0.5,
|
||||||
mu_min: float = 1e-6,
|
mu_min: float = 1e-6,
|
||||||
mu_max: float = 1e8,
|
mu_max: float = 1e8,
|
||||||
mu_factor: float = 10.0**0.1,
|
mu_factor: float = 10.0 ** 0.1,
|
||||||
tol: float = 1e-6,
|
xtol: float = 1e-8,
|
||||||
|
gtol: float = 1e-8,
|
||||||
max_iter: int = 100,
|
max_iter: int = 100,
|
||||||
verbose: Union[Verbosity, int] = Verbosity.ITER,
|
verbose: Union[Verbosity, int] = Verbosity.ITER,
|
||||||
output: Optional[TextIO] = None,
|
output: Optional[TextIO] = None,
|
||||||
@@ -166,7 +171,8 @@ def least_squares(
|
|||||||
mu_min: Minimum value of the regularizer.
|
mu_min: Minimum value of the regularizer.
|
||||||
mu_max: Maximum value of the regularizer.
|
mu_max: Maximum value of the regularizer.
|
||||||
mu_factor: Factor for increasing or decreasing the regularizer.
|
mu_factor: Factor for increasing or decreasing the regularizer.
|
||||||
tol: Termination tolerance on the step size.
|
xtol: Termination tolerance on relative step size.
|
||||||
|
gtol: Termination tolerance on gradient norm.
|
||||||
max_iter: Maximum number of iterations.
|
max_iter: Maximum number of iterations.
|
||||||
verbose: Verbosity level.
|
verbose: Verbosity level.
|
||||||
output: Optional file or StringIO to which to print messages.
|
output: Optional file or StringIO to which to print messages.
|
||||||
@@ -281,6 +287,23 @@ def least_squares(
|
|||||||
# Get gradient, Gauss-Newton Hessian.
|
# Get gradient, Gauss-Newton Hessian.
|
||||||
grad, hess = norm.grad_hess(r, jac)
|
grad, hess = norm.grad_hess(r, jac)
|
||||||
|
|
||||||
|
# Get free (unclamped) gradient.
|
||||||
|
if bounds is None:
|
||||||
|
grad_free = grad
|
||||||
|
else:
|
||||||
|
clamped_lower = (x == bounds[0]) & (grad > 0)
|
||||||
|
clamped_upper = (x == bounds[1]) & (grad < 0)
|
||||||
|
clamped = clamped_lower | clamped_upper
|
||||||
|
grad_free = grad[~clamped]
|
||||||
|
|
||||||
|
# Check termination condition on gradient norm.
|
||||||
|
g_norm = np.linalg.norm(grad_free)
|
||||||
|
if g_norm <= gtol:
|
||||||
|
status = Status.G_TOL
|
||||||
|
if g_norm == 0:
|
||||||
|
print('Zero gradient norm: exact minimum found?', file=output)
|
||||||
|
break
|
||||||
|
|
||||||
# Bounds relative to x
|
# Bounds relative to x
|
||||||
dlower = None if bounds is None else bounds[0] - x
|
dlower = None if bounds is None else bounds[0] - x
|
||||||
dupper = None if bounds is None else bounds[1] - x
|
dupper = None if bounds is None else bounds[1] - x
|
||||||
@@ -353,13 +376,15 @@ def least_squares(
|
|||||||
# Append log to trace, call iter_callback.
|
# Append log to trace, call iter_callback.
|
||||||
log = IterLog(candidate=x, objective=y, reduction=reduction, regularizer=mu)
|
log = IterLog(candidate=x, objective=y, reduction=reduction, regularizer=mu)
|
||||||
if verbose >= Verbosity.FULLITER.value:
|
if verbose >= Verbosity.FULLITER.value:
|
||||||
log = dataclasses.replace(log, residual=r, jacobian=jac, step=dx)
|
log = dataclasses.replace(
|
||||||
|
log, residual=r, jacobian=jac, grad=grad, step=dx
|
||||||
|
)
|
||||||
trace.append(log)
|
trace.append(log)
|
||||||
if iter_callback is not None:
|
if iter_callback is not None:
|
||||||
iter_callback(trace)
|
iter_callback(trace)
|
||||||
|
|
||||||
# Check for success.
|
# Check termination condition on step norm.
|
||||||
if dx_norm < tol:
|
if dx_norm < xtol * (xtol + np.linalg.norm(x)):
|
||||||
status = Status.DX_TOL
|
status = Status.DX_TOL
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -376,7 +401,7 @@ def least_squares(
|
|||||||
# Append final log to trace, call iter_callback.
|
# Append final log to trace, call iter_callback.
|
||||||
# Note: unlike other iter logs, values are computed at the end point.
|
# Note: unlike other iter logs, values are computed at the end point.
|
||||||
yfinal = norm.value(r)
|
yfinal = norm.value(r)
|
||||||
red = np.float64(0.0) # No reduction sice we didn't take a step.
|
red = np.float64(0.0) # No reduction since we didn't take a step.
|
||||||
log = IterLog(candidate=x, objective=yfinal, reduction=red, regularizer=mu)
|
log = IterLog(candidate=x, objective=yfinal, reduction=red, regularizer=mu)
|
||||||
trace.append(log)
|
trace.append(log)
|
||||||
if iter_callback is not None:
|
if iter_callback is not None:
|
||||||
@@ -430,13 +455,15 @@ def jacobian_fd(
|
|||||||
"""
|
"""
|
||||||
n = x.size
|
n = x.size
|
||||||
if bounds is None:
|
if bounds is None:
|
||||||
eps_vec = eps * np.ones(n)
|
eps_vec = eps * np.ones((n, 1))
|
||||||
else:
|
else:
|
||||||
mid = 0.5 * (bounds[1] - bounds[0])
|
mid = 0.5 * (bounds[1] - bounds[0])
|
||||||
eps_vec = np.where(x > mid, -eps, eps).flatten()
|
eps_vec = np.where(x > mid, -eps, eps)
|
||||||
xh = x + np.diag(eps_vec)
|
eps_vec *= np.maximum(1.0, np.abs(x))
|
||||||
|
eps_vec = (eps_vec + x) - x
|
||||||
|
xh = x + np.diag(eps_vec.flatten())
|
||||||
rh = residual(xh)
|
rh = residual(xh)
|
||||||
jac = (rh - r) / eps_vec
|
jac = (rh - r) / eps_vec.T
|
||||||
return jac, n_res + n
|
return jac, n_res + n
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
"""Tests for minimize.py."""
|
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ class MinimizeTest(absltest.TestCase):
|
|||||||
x, _ = minimize.least_squares(x0, residual, output=out)
|
x, _ = minimize.least_squares(x0, residual, output=out)
|
||||||
expected_x = np.array((1.0, 1.0))
|
expected_x = np.array((1.0, 1.0))
|
||||||
np.testing.assert_array_almost_equal(x, expected_x)
|
np.testing.assert_array_almost_equal(x, expected_x)
|
||||||
self.assertIn('norm(dx) < tol', out.getvalue())
|
self.assertIn('norm(gradient) < tol', out.getvalue())
|
||||||
|
|
||||||
def test_start_at_minimum(self) -> None:
|
def test_start_at_minimum(self) -> None:
|
||||||
def residual(x):
|
def residual(x):
|
||||||
@@ -43,7 +42,7 @@ class MinimizeTest(absltest.TestCase):
|
|||||||
x, _ = minimize.least_squares(x0, residual, output=out)
|
x, _ = minimize.least_squares(x0, residual, output=out)
|
||||||
expected_x = np.array((1.0, 1.0))
|
expected_x = np.array((1.0, 1.0))
|
||||||
np.testing.assert_array_almost_equal(x, expected_x)
|
np.testing.assert_array_almost_equal(x, expected_x)
|
||||||
self.assertIn('norm(dx) < tol', out.getvalue())
|
self.assertIn('norm(gradient) < tol', out.getvalue())
|
||||||
self.assertIn('exact minimum found', out.getvalue())
|
self.assertIn('exact minimum found', out.getvalue())
|
||||||
|
|
||||||
def test_jac_callback(self) -> None:
|
def test_jac_callback(self) -> None:
|
||||||
@@ -61,7 +60,7 @@ class MinimizeTest(absltest.TestCase):
|
|||||||
)
|
)
|
||||||
expected_x = np.array((1.0, 1.0))
|
expected_x = np.array((1.0, 1.0))
|
||||||
np.testing.assert_array_almost_equal(x, expected_x)
|
np.testing.assert_array_almost_equal(x, expected_x)
|
||||||
self.assertIn('norm(dx) < tol', out.getvalue())
|
self.assertIn('norm(gradient) < tol', out.getvalue())
|
||||||
self.assertIn('Jacobian matches', out.getvalue())
|
self.assertIn('Jacobian matches', out.getvalue())
|
||||||
|
|
||||||
# Try with bad Jacobian, ask least_squares to check it.
|
# Try with bad Jacobian, ask least_squares to check it.
|
||||||
@@ -116,7 +115,7 @@ class MinimizeTest(absltest.TestCase):
|
|||||||
x0, residual, bounds=bounds_types['inbounds'], output=out
|
x0, residual, bounds=bounds_types['inbounds'], output=out
|
||||||
)
|
)
|
||||||
np.testing.assert_array_almost_equal(x, expected_x)
|
np.testing.assert_array_almost_equal(x, expected_x)
|
||||||
self.assertIn('norm(dx) < tol', out.getvalue())
|
self.assertIn('norm(gradient) < tol', out.getvalue())
|
||||||
|
|
||||||
# Test different bounds conditions.
|
# Test different bounds conditions.
|
||||||
for bounds in bounds_types.values():
|
for bounds in bounds_types.values():
|
||||||
|
|||||||
Reference in New Issue
Block a user