Shortcuts

pypose.optim.LevenbergMarquardt

class pypose.optim.LevenbergMarquardt(model, solver=None, strategy=None, kernel=None, corrector=None, weight=None, reject=16, min=1e-06, max=1e+32, vectorize=True, sparse=False)[source]

The Levenberg-Marquardt (LM) algorithm solving non-linear least squares problems. It is also known as the damped least squares (DLS) method. This implementation is for optimizing the model parameters to approximate the target, which can be a Tensor/LieTensor or a tuple of Tensors/LieTensors.

\[\bm{\theta}^* = \arg\min_{\bm{\theta}} \sum_i \rho\left((\bm{f}(\bm{\theta},\bm{x}_i)-\bm{y}_i)^T \mathbf{W}_i (\bm{f}(\bm{\theta},\bm{x}_i)-\bm{y}_i)\right), \]

where \(\bm{f}()\) is the model, \(\bm{\theta}\) is the parameters to be optimized, \(\bm{x}\) is the model input, \(\mathbf{W}_i\) is a weighted square matrix (positive definite), and \(\rho\) is a robust kernel function to reduce the effect of outliers. \(\rho(x) = x\) is used by default.

\[\begin{aligned} &\rule{113mm}{0.4pt} \\ &\textbf{input}: \lambda~\text{(damping)}, \bm{\theta}_0~\text{(params)}, \bm{f}~\text{(model)}, \bm{x}~(\text{input}), \bm{y}~(\text{target}) \\ &\hspace{12mm} \rho~(\text{kernel}), \epsilon_{s}~(\text{min}), \epsilon_{l}~(\text{max}) \\ &\rule{113mm}{0.4pt} \\ &\textbf{for} \: t=1 \: \textbf{to} \: \ldots \: \textbf{do} \\ &\hspace{5mm} \mathbf{J} \leftarrow {\dfrac {\partial \bm{f}} {\partial \bm{\theta}_{t-1}}} \\ &\hspace{5mm} \mathbf{A} \leftarrow (\mathbf{J}^T \mathbf{W} \mathbf{J}) .\mathrm{diagnal\_clamp(\epsilon_{s}, \epsilon_{l})} \\ &\hspace{5mm} \mathbf{R} = \bm{f(\bm{\theta}_{t-1}, \bm{x})}-\bm{y} \\ &\hspace{5mm} \mathbf{R}, \mathbf{J}=\mathrm{corrector}(\rho, \mathbf{R}, \mathbf{J})\\ &\hspace{5mm} \textbf{while}~\text{first iteration}~\textbf{or}~ \text{loss not decreasing} \\ &\hspace{10mm} \mathbf{A} \leftarrow \mathbf{A} + \lambda \mathrm{diag}(\mathbf{A}) \\ &\hspace{10mm} \bm{\delta} = \mathrm{solver}(\mathbf{A}, -\mathbf{J}^T \mathbf{W} \mathbf{R}) \\ &\hspace{10mm} \lambda \leftarrow \mathrm{strategy}(\lambda,\text{model information})\\ &\hspace{10mm} \bm{\theta}_t \leftarrow \bm{\theta}_{t-1} + \bm{\delta} \\ &\hspace{10mm} \textbf{if}~\text{loss not decreasing}~\textbf{and}~ \text{maximum reject step not reached} \\ &\hspace{15mm} \bm{\theta}_t \leftarrow \bm{\theta}_{t-1} - \bm{\delta} ~(\text{reject step}) \\ &\rule{113mm}{0.4pt} \\[-1.ex] &\bf{return} \: \theta_t \\[-1.ex] &\rule{113mm}{0.4pt} \\[-1.ex] \end{aligned} \]
Parameters:
  • model (nn.Module) – a module containing learnable parameters.

  • solver (nn.Module, optional) – a linear solver. If None, solver.Cholesky() is used. Default: None.

  • strategy (object, optional) – strategy for adjusting the damping factor. If None, the strategy.TrustRegion() will be used. Defult: None.

  • kernel (nn.Module, or list, optional) – the robust kernel function. If a list, the element must be nn.Module or None and the length must be 1 or the number of residuals. Default: None.

  • corrector – (nn.Module, or list, optional): the Jacobian and model residual corrector to fit the kernel function. If a list, the element must be nn.Module or None and the length must be 1 or the number of residuals. If a kernel is given but a corrector is not specified, auto correction is used. Auto correction can be unstable when the robust model has indefinite Hessian. Default: None.

  • weight (Tensor, or list, optional) – the square positive definite matrix defining the weight of model residual. If a list, the element must be Tensor and the length must be equal to the number of residuals. The corresponding residual and weight should be broadcastable. For example, if the shape of a residual is B*M*N*R, the shape of its weight can be R*R, N*R*R, M*N*R*R or B*M*N*R*R. Use this only when all inputs shared the same weight matrices. This is ignored when weight is given when calling step() or optimize() method. Default: None.

  • reject (integer, optional) – the maximum number of rejecting unsuccessfull steps. Default: 16.

  • min (float, optional) – the lower-bound of the Hessian diagonal. Default: 1e-6.

  • max (float, optional) – the upper-bound of the Hessian diagonal. Default: 1e32.

  • vectorize (bool, optional) – the method of computing Jacobian. If True, the gradient of each scalar in output with respect to the model parameters will be computed in parallel with "reverse-mode". More details go to pypose.optim.functional.modjac(). Default: True.

  • sparse (bool, optional) – if True, use the sparse LM path based on sparse Jacobians and sparse normal equations. This mode requires the optional sparse backend bae and is intended to be used with sparse linear solvers such as solver.CG(). Default: False.

Available solvers: solver.PINV(); solver.LSTSQ(); solver.Cholesky(); solver.CG(); solver.PCG().

Available kernels: kernel.Huber(); kernel.PseudoHuber(); kernel.Cauchy().

Available correctors: corrector.FastTriggs(), corrector.Triggs().

Available strategies: strategy.Constant(); strategy.Adaptive(); strategy.TrustRegion();

Note

Setting sparse=True enables the sparse Jacobian / sparse LM backend. It should be used when the underlying optimization problem exhibits a large, structured sparse Jacobian, where each residual depends only on a small subset of parameters. Please cite the following paper implementing the sparse LM backend:

Check a full and clean runable example with sparse=True for bundle adjustment.

Warning

The output of model \(\bm{f}(\bm{\theta},\bm{x}_i)\) and target \(\bm{y}_i\) can be any shape, while their last dimension \(d\) is always taken as the dimension of model residual, whose inner product will be input to the kernel function. This is useful for residuals like re-projection error, whose last dimension is 2.

Note that auto correction is equivalent to the method of ‘square-rooting the kernel’ mentioned in Section 3.3 of the following paper. It replaces the \(d\)-dimensional residual with a one-dimensional one, which loses residual-level structural information.

Therefore, the users need to keep the last dimension of model output and target to 1, even if the model residual is a scalar. If the model output only has one dimension, the model Jacobian will be a row vector, instead of a matrix, which loses sample-level structural information, although computing Jacobian vector is faster.

step(input, target=None, weight=None)[source]

Performs a single optimization step.

Parameters:
  • input (Tensor/LieTensor, tuple or a dict of Tensors/LieTensors) – the input to the model.

  • target (Tensor/LieTensor) – the model target to optimize. If not given, the squared model output is minimized. Defaults: None.

  • weight (Tensor, or list, optional) – the square positive definite matrix defining the weight of model residual. If a list, the element must be Tensor and the length must be equal to the number of residuals. This argument is currently not supported when sparse=True. Default: None.

Returns:

the minimized model loss.

Return type:

Tensor

Note

The (non-negative) damping factor \(\lambda\) can be adjusted at each iteration. If the residual reduces rapidly, a smaller value can be used, bringing the algorithm closer to the Gauss-Newton algorithm, whereas if an iteration gives insufficient residual reduction, \(\lambda\) can be increased, giving a step closer to the gradient descent direction.

See more details of Levenberg-Marquardt (LM) algorithm on Wikipedia.

Note

Different from PyTorch optimizers like SGD, where the model error has to be a scalar, the output of model \(\bm{f}\) can be a Tensor/LieTensor or a tuple of Tensors/LieTensors.

Note

When sparse=True, only a single residual tensor is currently supported. If the model returns multiple residuals, only the first one is used.

Example

Optimizing a simple module to approximate pose inversion.

>>> class PoseInv(nn.Module):
...     def __init__(self, *dim):
...         super().__init__()
...         self.pose = pp.Parameter(pp.randn_se3(*dim))
...
...     def forward(self, input):
...         # the last dimension of the output is 6,
...         # which will be the residual dimension.
...         return (self.pose.Exp() @ input).Log()
...
>>> posinv = PoseInv(2, 2)
>>> input = pp.randn_SE3(2, 2)
>>> strategy = pp.optim.strategy.Adaptive(damping=1e-6)
>>> optimizer = pp.optim.LM(posinv, strategy=strategy)
...
>>> for idx in range(10):
...     loss = optimizer.step(input)
...     print('Pose Inversion loss %.7f @ %d it'%(loss, idx))
...     if loss < 1e-5:
...         print('Early Stopping with loss:', loss.item())
...         break
...
Pose Inversion error: 1.6600330 @ 0 it
Pose Inversion error: 0.1296970 @ 1 it
Pose Inversion error: 0.0008593 @ 2 it
Pose Inversion error: 0.0000004 @ 3 it
Early Stopping with error: 4.443569991963159e-07

Solving bundle adjustment by minimizing reprojection error with sparse LM. (requires the optional sparse backend bae and CUDA).

Here, psjac marks the reprojection residual so the sparse backend can assemble sparse Jacobians for sparse LM. Use it on factorwise residual functions that take batch inputs and return one residual block per batch item. When you call the function normally, it behaves the same as before; the decorator only helps the sparse backend build sparse Jacobians. In the example, both camera poses and 3D points are optimized to minimize the reprojection error.

>>> import torch
>>> import pypose as pp
>>> from torch import nn
>>> from pypose.optim import LM
>>> from pypose.optim.solver import PCG
>>> from pypose.optim.strategy import TrustRegion
>>> from pypose.optim.scheduler import StopOnPlateau
>>> from pypose.autograd.function import psjac
>>>
>>> class ReprojErr(nn.Module):
...     def __init__(self, poses, points):
...         super().__init__()
...         # sjac: enable tracing sparse Jacobian
...         self.poses = pp.Parameter(poses, sjac=True)
...         self.points = pp.Parameter(points, sjac=True)
...
...     @psjac  # parallelize assembly of sparse Jacobian
...     def project(poses, points):
...         points = poses.Act(points)
...         return - points[..., :2] / points[..., [2]]
...
...     def forward(self, pixels, cidx, pidx):
...         poses = self.poses[cidx]
...         points = self.points[pidx]
...         return ReprojErr.project(poses, points) - pixels
...
>>> torch.set_default_device("cuda")
>>> npts, poses = 8, pp.randn_SE3(1)
>>> points = torch.randn(npts, 3)
>>> points[:, 2] += 4  # positive depth
>>> cidx = torch.zeros(npts, dtype=torch.long)
>>> pidx = torch.arange(npts)
>>> pixels = torch.randn(npts, 2)
>>> inputs = (pixels, cidx, pidx)
>>>
>>> model = ReprojErr(poses, points)
>>> strategy = TrustRegion(up=2.0, down=0.5**4)
>>> solver = PCG(tol=1e-4, maxiter=250)
>>> optimizer = LM(model, solver, strategy, sparse=True)
>>> scheduler = StopOnPlateau(optimizer, steps=5, verbose=True)
>>>
>>> while scheduler.continual():
...     loss = optimizer.step(inputs)
...     scheduler.step(loss)
StopOnPlateau on step 1 Loss 4.467577e+01 --> Loss 7.737489e+00.
StopOnPlateau on step 2 Loss 7.737489e+00 --> Loss 2.681350e+00.
StopOnPlateau on step 3 Loss 2.681350e+00 --> Loss 1.192892e+00.
StopOnPlateau on step 4 Loss 1.192892e+00 --> Loss 5.546592e-01.
StopOnPlateau on step 5 Loss 5.546592e-01 --> Loss 5.288146e-01.
StopOnPlateau: Maximum steps reached, Quitting..
update_parameter(params, step)[source]

params will be updated by calling this function

Docs

Access documentation for PyPose

View Docs

Tutorials

Get started with tutorials and examples

View Tutorials

Get Started

Find resources and how to start using pypose

View Resources