OptimizationAdjoint for constrained optimization solution sensitivities #1444
OptimizationAdjoint for constrained optimization solution sensitivities #1444jClugstor wants to merge 29 commits into
Conversation
SummaryAdds a new OptimizationAdjoint sensitivity algorithm that computes opt_sol = solve(prob, NLopt.LD_SLSQP()) # solve once
dgdu!(out, u, _, _, _) = (out .= dG_dx) # cotangent of loss w.r.t. x*
dp = adjoint_sensitivities(opt_sol, nothing;
sensealg = OptimizationAdjoint(),
dgdu = dgdu!) |
The MathA simple way to say this is that this is basically the same thing as SteadyStateAdjoint, but just doing the implicit differentiation on the Nonlinear system that is made up of the gradient of the lagrangian and the constraint equations all set to zero. First of all though, we do need all of the KKT variables/ dual variables / lagrange multipliers, the variables that are multiplied by the constraint functions in the lagrangian that satisfy the KKT conditions. We get the optimal This is typically overdetermined (more x's than active constraints), so we solve via LinearSolve.QRFactorization — least-squares, which under LICQ recovers the exact multipliers. LICQ says that at the optimal point, the gradients of the active constraints are all linearly independent. QR decomposition finds the least squares solution, so it's finding but if we found a viable local minimum, the KKT theorem states that If any returned z_I is negative (KKT violation), the corresponding constraint was wrongly classified as active; drop it and redo the multiplier solve. The sign-check + redo handles boundary degeneracy that proximity-based active-set detection alone gets wrong. Now for the sensitivity, the idea is that a solution of the constrained optimization problem satisfies the Karush-Kuhn Tucker (KKT) conditions: Taking only the set of inequality constraints that are actually active, The solution of the optimization problem satisfies this nonlinear system of equations. Our end goal is to find Using the above, Now consider that we have some scalar cost function that will be a function of the solution of the optimization problem, But this term so that |
9c6375c to
b6aef6b
Compare
|
@ChrisRackauckas this is finally ready to be looked at |
| function gradient( | ||
| f, x::AbstractArray{<:Number}, | ||
| alg::AbstractOverloadingSensitivityAlgorithm | ||
| ) | ||
| return if alg_autodiff(alg) | ||
| ForwardDiff.gradient(unwrapped_f(f), x) | ||
| else | ||
| FiniteDiff.finite_difference_gradient(f, x, diff_type(alg)) | ||
| end | ||
| end |
There was a problem hiding this comment.
What do you mean? The finite difference grad should be using FDT, right?
There was a problem hiding this comment.
There's no reverse mode gradient methods here?
There was a problem hiding this comment.
Oh right, for context this only gets used for the gradient of the lagrangian. The actual VJP is performed by vecjacobian!.
If we want to be able to use reverse mode for the lagrangian grad I think the best way to do it would be to just use DifferentiationInterface and just add a field in the OptimizationAdjoint for the ADType. We only need to differentiate with respect to one variable so it should work fine.
That probably won't work for all combinations of AD backend for lagrangian gradient and autojacvec of course because it will be AD of AD, but should work for some combinations I think.
There was a problem hiding this comment.
Why not just require that the optimization has be initialized with a gradient function? Optimization.jl has a whole system for that already, and you just get an error that you need to do the standard Optimization AD setup if you want to do this.
There was a problem hiding this comment.
We could have Optimization.jl build the gradient of the lagrangian, lag_g? The problem I can see with that is that we have to differentiate lag_g with respect to p. So because of DI preparation it might be difficult to figure out how to push Duals through it, in the case of autojacvec = true.
There was a problem hiding this comment.
@ChrisRackauckas the problem with using the grad created for the OptimizationFunction is that it uses a DI preparation, which bakes in the types. So there's no way to put Duals in to it for ForwardDiff autojacvec without using some kind of diffcache, or having the grad reprepare every time.
| elseif !has_cons && opt_f.hess !== nothing | ||
| _opt_eval_mat(opt_f.hess, n_x, n_x, x_star, p, iip_val, has_p_val) | ||
| else | ||
| hessian(x -> L(x, p), x_star, sensealg) | ||
| end |
There was a problem hiding this comment.
It's probably better just to require there is a hess and grad function from the standard system, rather than doubling it up.
There was a problem hiding this comment.
Oh I see, those fields get populated anyways depending on the solver. Makes sense.
There was a problem hiding this comment.
First order constrained solvers don't populate the required second order pieces that we need, so for those you would always have to provide either lag_h or hess and cons_h. Is that ok?
There was a problem hiding this comment.
Can we add that via DI? What solvers do you need to support this?
There was a problem hiding this comment.
The main issue here is that in instantiate_function, it looks at which pieces the algorithm requires, and if it doesn't require that piece, it just doesn't build it, or store it in the cache, even if the user directly provided it.
e.g. here https://github.com/SciML/Optimization.jl/blob/ae548dc4213083be50afc9e35dd7e06eab3e0c12/lib/OptimizationBase/src/OptimizationDIExt.jl#L99
if the algorithm doesn't require the hessian even a user provided hessian won't be stored in the cache, so it can't be used in the adjoint.
I think the choices are:
-
Have Optimization.jl always build all of the derivatives and keep them in the OptimizationCache. This would allow sensitivity analysis for constrained problems that used single order constrained solvers even if the user didn't provide second order information.
-
Just keep the second order information in the cache if it's provided by a user, even if the solver doesn't use the information. This would mean that for first order solvers users would have to provide second order information to find the sensitivities.
-
Make an OptimizationBase extension in SciMLSensitivity, check if the cache has second order information, if it doesn't have it call
OptimizationBase.instantiate_functionto build it.
There was a problem hiding this comment.
What solvers do you need to support this?
I would think as many as possible. The calculation of the adjoint doesn't use the optimization solver at all, it only needs the function, first order, and second order information to find the derivative.
2883b16 to
ecc7541
Compare
|
Ok, so this works, but I basically have a custom gradient wrapper for the inner gradient of the lagrangian. I just take the objective function and constraints and create the lagrangian, and use AD to find the gradient, then pass that on to If we want to reuse the |
ecc7541 to
89820bd
Compare
Checklist
contributor guidelines, in particular the SciML Style Guide and
COLPRAC.
Additional context
Add any other context about the problem here.