Skip to content

Commit bf4b8eb

Browse files
paldaydmbates
andauthored
optimization info/backend updates (#853)
* remove prfit!, place NLopt support in submodule * Placing NLopt support into a submodule will make it easier to move NLopt to an extension and promote PRIMA to the default backend. * Unfortunately, the profiling code still has been explicit assumptions around NLopt. * refactor profiling to be optimizer independent * update imports * docs * fix figure rendering --------- Co-authored-by: Douglas Bates <[email protected]>
1 parent e9a139b commit bf4b8eb

File tree

12 files changed

+188
-80
lines changed

12 files changed

+188
-80
lines changed

NEWS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
MixedModels v5.0.0 Release Notes
22
==============================
3-
- Optimization is now performed _without constraints_. In a post-fitting step, the the Cholesky factor is canonicalized to have non-negative diagonal elements. [#840]
4-
- The default optimizer has changed to NLopt's implementation of NEWUOA where possible. NLopt's implementation fails on 1-dimensional problems, so in the case A single, scalar random effect, BOBYQA is used instead. In the future, the default optimizer backend will likely change to PRIMA and NLopt support will be moved to an extension. Blocking this change in backend is an issue with PRIMA.jl when running in VSCode's built-in REPL on Linux. [#840]
3+
- Optimization is now performed _without constraints_. In a post-fitting step, the Cholesky factor is canonicalized to have non-negative diagonal elements. [#840]
4+
- The default optimizer has changed to NLopt's implementation of NEWUOA where possible. NLopt's implementation fails on 1-dimensional problems, so in the case of a single, scalar random effect, BOBYQA is used instead. In the future, the default optimizer backend will likely change to PRIMA and NLopt support will be moved to an extension. Blocking this change in backend is an issue with PRIMA.jl when running in VSCode's built-in REPL on Linux. [#840]
55
- [BREAKING] Support for constrained optimization has been completely removed, i.e. the field `lowerbd` has been removed from `OptSummary`.
6-
6+
- Internal code around the default optimizer has been restructured. In particular, the NLopt backend has been moved to a submodule, which will make it easier to move it to an extension if we promote another backend to the default. [#853]
7+
- Internal code around optimization in profiling has been restructuring so that fitting done during calls to `profile` respect the `backend` and `optimizer` settings. [#853]
8+
- The `prfit!` convenience function has been removed. [#853]
79

810
MixedModels v4.38.0 Release Notes
911
==============================

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ makedocs(;
1111
doctest=true,
1212
# pagesonly=true,
1313
# warnonly=true,
14+
# warnonly=[:cross_references],
1415
pages=[
1516
"index.md",
1617
"constructors.md",

docs/src/api.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,17 @@ stderrror!
7979
varest
8080
```
8181

82-
## Non-Exported Functions
82+
## Non-Exported Functions and Constants
8383

84-
Note that unless discussed elsewhere in the online documentation, non-exported functions should be considered implementation details.
84+
Note that unless discussed elsewhere in the online documentation, non-exported functions and constants should be considered implementation details.
8585

8686
```@autodocs
8787
Modules = [MixedModels]
8888
Public = false
8989
Order = [:function]
9090
Filter = f -> !startswith(string(f), "_")
9191
```
92+
93+
```@docs
94+
MixedModels.OPTIMIZATION_BACKENDS
95+
```

docs/src/optimization.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,16 +222,35 @@ Typically, the (1,1) block is the largest block in `A` and `L` and it has a spec
222222
`UniformBlockDiagonal`
223223
providing a compact representation and fast matrix multiplication or solutions of linear systems of equations.
224224

225-
### Modifying the optimization process
225+
## Modifying the optimization process
226226

227-
The `OptSummary` object contains both input and output fields for the optimizer.
227+
The [`OptSummary`](@ref) object contains both input and output fields for the optimizer.
228228
To modify the optimization process the input fields can be changed after constructing the model but before fitting it.
229+
In addition to various tolerances, which we will not discuss further here, users can specify the choice of `backend` (i.e., the non-linear optimization library used) and `optimizer` (i.e., the implementation of an algorithm provided by the backend).
230+
231+
The current default backend is [NLopt](https://github.com/JuliaOpt/NLopt.jl), which is a direct dependency of MixedModels.jl.
232+
A [PRIMA](https://github.com/libprima/PRIMA.jl/) backend is also provided as a package extension and thus only
233+
available when the PRIMA package is loaded.
234+
The list of currently loaded backends is available as [`MixedModels.OPTIMIZATION_BACKENDS`](@ref).
235+
For each individual backend, the list of available optimizers can be inspected with the function [`MixedModels.optimizers`](@ref).
236+
```@example Main
237+
MixedModels.optimizers(:nlopt)
238+
```
239+
Similarly, the list of applicable optimization parameters can be inspected with the function [`MixedModels.opt_params`](@ref).
240+
```@example Main
241+
MixedModels.opt_params(:nlopt)
242+
```
243+
244+
!!! note "Optimizer defaults subject to change"
245+
The choice of default backend and optimizer is subject to change without being considered a breaking change.
246+
If you want to guarantee a particular backend and optimizer, then you should
247+
explicitly load the associated backend's package (e.g. NLopt or PRIMA) and manually set the `optimizer` and `backend` fields.
229248

230249
Suppose, for example, that the user wishes to try a [Nelder-Mead](https://en.wikipedia.org/wiki/Nelder%E2%80%93Mead_method) optimization method instead of the default [`BOBYQA`](https://en.wikipedia.org/wiki/BOBYQA) (Bounded Optimization BY Quadratic Approximation) method.
231250
```@example Main
232251
fm2nm = LinearMixedModel(@formula(reaction ~ 1+days+(1+days|subj)), sleepstudy);
233252
fm2nm.optsum.optimizer = :LN_NELDERMEAD;
234-
fit!(fm2nm; thin=1)
253+
fit!(fm2nm; fitlog=true)
235254
fm2nm.optsum
236255
DisplayAs.Text(ans) # hide
237256
```
@@ -252,8 +271,6 @@ plot(convdf, x=:step, y=:objective, color=:algorithm, Geom.line)
252271

253272
Run time can be constrained with `maxfeval` and `maxtime`.
254273

255-
See the documentation for the [`NLopt`](https://github.com/JuliaOpt/NLopt.jl) package for details about the various settings.
256-
257274
### Convergence to singular covariance matrices
258275

259276
To ensure identifiability of $\Sigma_\theta=\sigma^2\Lambda_\theta \Lambda_\theta$, the elements of $\theta$ corresponding to diagonal elements of $\Lambda_\theta$ are constrained to be non-negative.

ext/MixedModelsPRIMAExt.jl

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ module MixedModelsPRIMAExt
22

33
using MixedModels
44
using MixedModels: Statistics
5-
using MixedModels: ProgressMeter, ProgressUnknown, objective!, _objective!
5+
using MixedModels.ProgressMeter: ProgressMeter, ProgressUnknown
6+
using MixedModels: objective!, _objective!, rectify!
67
using LinearAlgebra: PosDefException
78
using PRIMA: PRIMA
89

@@ -13,19 +14,16 @@ end
1314

1415
const PRIMABackend = Val{:prima}
1516

16-
function MixedModels.prfit!(m::LinearMixedModel;
17-
kwargs...)
18-
MixedModels.unfit!(m)
19-
m.optsum.optimizer = :bobyqa
20-
m.optsum.backend = :prima
21-
22-
return fit!(m; kwargs...)
17+
function _optimizer(o::Val{O}, f, x0::Vector, args...; kwargs...) where {O}
18+
x0 = copy(x0)
19+
info = _optimizer!(o, f, x0, args...; kwargs...)
20+
return x0, info
2321
end
2422

25-
prima_optimizer!(::Val{:bobyqa}, args...; kwargs...) = PRIMA.bobyqa!(args...; kwargs...)
26-
prima_optimizer!(::Val{:cobyla}, args...; kwargs...) = PRIMA.cobyla!(args...; kwargs...)
27-
prima_optimizer!(::Val{:lincoa}, args...; kwargs...) = PRIMA.lincoa!(args...; kwargs...)
28-
prima_optimizer!(::Val{:newuoa}, args...; kwargs...) = PRIMA.newuoa!(args...; kwargs...)
23+
_optimizer!(::Val{:bobyqa}, args...; kwargs...) = PRIMA.bobyqa!(args...; kwargs...)
24+
_optimizer!(::Val{:cobyla}, args...; kwargs...) = PRIMA.cobyla!(args...; kwargs...)
25+
_optimizer!(::Val{:lincoa}, args...; kwargs...) = PRIMA.lincoa!(args...; kwargs...)
26+
_optimizer!(::Val{:newuoa}, args...; kwargs...) = PRIMA.newuoa!(args...; kwargs...)
2927

3028
function MixedModels.optimize!(m::LinearMixedModel, ::PRIMABackend;
3129
progress::Bool=true, fitlog::Bool=false, kwargs...)
@@ -56,8 +54,7 @@ function MixedModels.optimize!(m::LinearMixedModel, ::PRIMABackend;
5654
end
5755

5856
maxfun = optsum.maxfeval > 0 ? optsum.maxfeval : 500 * length(optsum.initial)
59-
info = prima_optimizer!(Val(optsum.optimizer), obj, optsum.final;
60-
# xl=optsum.lowerbd,
57+
info = _optimizer!(Val(optsum.optimizer), obj, optsum.final;
6158
maxfun,
6259
optsum.rhoend, optsum.rhobeg)
6360
ProgressMeter.finish!(prog)
@@ -108,7 +105,7 @@ function MixedModels.optimize!(m::GeneralizedLinearMixedModel, ::PRIMABackend;
108105
end
109106
sc
110107
end
111-
info = prima_optimizer!(Val(optsum.optimizer), obj, optsum.final;
108+
info = _optimizer!(Val(optsum.optimizer), obj, optsum.final;
112109
xl=optsum.lowerbd, maxfun,
113110
optsum.rhoend, optsum.rhobeg,
114111
scale)
@@ -130,6 +127,34 @@ function _check_prima_return(info::PRIMA.Info)
130127
return nothing
131128
end
132129

133-
MixedModels.opt_params(::PRIMABackend) = (:rhobeg, :rhoend, :maxfeval)
130+
MixedModels.opt_params(::PRIMABackend) = [:rhobeg, :rhoend, :maxfeval]
131+
MixedModels.optimizers(::PRIMABackend) = [:bobyqa, :cobyla, :lincoa, :newuoa]
132+
133+
function MixedModels.profilevc(obj, optsum::OptSummary, ::PRIMABackend; kwargs...)
134+
maxfun = optsum.maxfeval > 0 ? optsum.maxfeval : 500 * length(optsum.initial)
135+
xmin, info = _optimizer(Val(optsum.optimizer), obj,
136+
copyto!(optsum.final, optsum.initial);
137+
maxfun,
138+
optsum.rhoend, optsum.rhobeg,
139+
scale=nothing) # will need to scale for GLMM
140+
_check_prima_return(info)
141+
fmin = info.fx
142+
return fmin, xmin
143+
end
144+
145+
function MixedModels.profileobj!(obj,
146+
m::LinearMixedModel{T}, θ::AbstractVector{T}, osj::OptSummary, ::PRIMABackend;
147+
kwargs...) where {T}
148+
maxfun = osj.maxfeval > 0 ? osj.maxfeval : 500 * length(osj.initial)
149+
xmin = copyto!(osj.final, osj.initial)
150+
info = _optimizer!(Val(osj.optimizer), obj, xmin;
151+
maxfun,
152+
osj.rhoend, osj.rhobeg,
153+
scale=nothing) # will need to scale for GLMM
154+
fmin = info.fx
155+
_check_prima_return(info)
156+
rectify!(m)
157+
return fmin
158+
end
134159

135160
end # module

src/MixedModels.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ using LinearAlgebra: ldiv!, lmul!, logdet, mul!, norm, normalize, normalize!, qr
2020
using LinearAlgebra: rank, rdiv!, rmul!, svd, tril!
2121
using Markdown: Markdown
2222
using MixedModelsDatasets: dataset, datasets
23-
using NLopt: NLopt, Opt
2423
using PooledArrays: PooledArrays, PooledArray
24+
using NLopt: NLopt
2525
using PrecompileTools: PrecompileTools, @setup_workload, @compile_workload
26-
using ProgressMeter: ProgressMeter, Progress, ProgressUnknown, finish!, next!
26+
using ProgressMeter: ProgressMeter, Progress, finish!, next!
2727
using Random: Random, AbstractRNG, randn!
2828
using SparseArrays: SparseArrays, SparseMatrixCSC, SparseVector, dropzeros!, nnz
2929
using SparseArrays: nonzeros, nzrange, rowvals, sparse
@@ -213,8 +213,9 @@ include("grouping.jl")
213213
include("mimeshow.jl")
214214
include("serialization.jl")
215215
include("profile/profile.jl")
216-
include("nlopt.jl")
217-
include("prima.jl")
216+
include("MixedModelsNLoptExt.jl")
217+
using .MixedModelsNLoptExt
218+
218219
include("derivatives.jl")
219220

220221
# aliases with non-unicode function names

src/nlopt.jl renamed to src/MixedModelsNLoptExt.jl

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
1-
push!(OPTIMIZATION_BACKENDS, :nlopt)
1+
module MixedModelsNLoptExt # not actually an extension at the moment
2+
3+
using ..MixedModels
4+
using ..MixedModels: objective!, _objective!, rectify!
5+
# are part of the package's dependencies and will not be part
6+
# of the extension's dependencies
7+
using ..MixedModels.ProgressMeter: ProgressMeter, ProgressUnknown
8+
9+
# stdlib
10+
using LinearAlgebra: PosDefException
11+
# will be a weakdep when this is moved to an extension
12+
using NLopt: NLopt, Opt
13+
14+
function __init__()
15+
push!(MixedModels.OPTIMIZATION_BACKENDS, :nlopt)
16+
return nothing
17+
end
218

319
const NLoptBackend = Val{:nlopt}
420

5-
function optimize!(m::LinearMixedModel, ::NLoptBackend;
21+
function MixedModels.optimize!(m::LinearMixedModel, ::NLoptBackend;
622
progress::Bool=true, fitlog::Bool=false,
723
kwargs...)
824
optsum = m.optsum
@@ -42,7 +58,7 @@ function optimize!(m::LinearMixedModel, ::NLoptBackend;
4258
return xmin, fmin
4359
end
4460

45-
function optimize!(m::GeneralizedLinearMixedModel, ::NLoptBackend;
61+
function MixedModels.optimize!(m::GeneralizedLinearMixedModel, ::NLoptBackend;
4662
progress::Bool=true, fitlog::Bool=false,
4763
fast::Bool=false, verbose::Bool=false, nAGQ=1,
4864
kwargs...)
@@ -121,6 +137,32 @@ function _check_nlopt_return(ret, failure_modes=_NLOPT_FAILURE_MODES)
121137
end
122138
end
123139

124-
function opt_params(::NLoptBackend)
125-
return (:ftol_rel, :ftol_abs, :xtol_rel, :xtol_abs, :initial_step, :maxfeval, :maxtime)
140+
function MixedModels.opt_params(::NLoptBackend)
141+
return [:ftol_rel, :ftol_abs, :xtol_rel, :xtol_abs, :initial_step, :maxfeval, :maxtime]
126142
end
143+
144+
function MixedModels.optimizers(::NLoptBackend)
145+
return [:LN_NEWUOA, :LN_BOBYQA, :LN_COBYLA, :LN_NELDERMEAD, :LN_PRAXIS]
146+
end
147+
148+
function MixedModels.profilevc(obj, optsum::OptSummary, ::NLoptBackend; kwargs...)
149+
opt = NLopt.Opt(optsum)
150+
NLopt.min_objective!(opt, obj)
151+
fmin, xmin, ret = NLopt.optimize!(opt, copyto!(optsum.final, optsum.initial))
152+
_check_nlopt_return(ret)
153+
154+
return fmin, xmin
155+
end
156+
157+
function MixedModels.profileobj!(obj,
158+
m::LinearMixedModel{T}, θ::AbstractVector{T}, osj::OptSummary, ::NLoptBackend;
159+
kwargs...) where {T}
160+
opt = NLopt.Opt(osj)
161+
NLopt.min_objective!(opt, obj)
162+
fmin, xmin, ret = NLopt.optimize(opt, copyto!(osj.final, osj.initial))
163+
_check_nlopt_return(ret)
164+
rectify!(m)
165+
return fmin
166+
end
167+
168+
end # module

src/optsummary.jl

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,21 @@ Summary of an optimization
2020
2121
## Choice of optimizer and backend
2222
* `optimizer`: the name of the optimizer used, as a `Symbol`
23-
* `backend`: the optimization library providing the optimizer, default is `NLoptBackend`.
23+
* `backend`: the optimization library providing the optimizer, stored as a symbol.
24+
The current default is `:nlopt`.
25+
26+
The current default backend is NLopt, which is a direct dependency of MixedModels.jl.
27+
A PRIMA backend is also provided as a package extension and thus only
28+
available when the library PRIMA is loaded.
29+
The list of currently loaded backends is available as [`MixedModels.OPTIMIZATION_BACKENDS`](@ref).
30+
For each individual backend, the list of available optimizers can be inspected with the function [`MixedModels.optimizers`](@ref).
31+
Similarly, the list of applicable optimization parameters can be inspected with the function [`MixedModels.opt_params`](@ref).
32+
33+
!!! note "Optimizer defaults subject to change"
34+
The choice of backend and optimizer is subject to change without being considered a breaking
35+
change. If you want to guarantee a particular backend and optimizer, then you should
36+
explicitly load the associated backend's package (e.g. NLopt or PRIMA) and manually
37+
set the `optimizer` and `backend` fields.
2438
2539
## Backend-specific fields
2640
* `ftol_rel`: as in NLopt, not used in PRIMA
@@ -185,3 +199,20 @@ Return a collection of the fields of [`OptSummary`](@ref) used by backend.
185199
they are used _after_ optimization and are thus shared across backends.
186200
"""
187201
function opt_params end
202+
203+
opt_params(s::Symbol) = opt_params(Val(s))
204+
205+
"""
206+
optimizers(::Val{backend})
207+
208+
Return a collection of the algorithms supported by the backend.
209+
210+
!!! note
211+
The names of the algorithms are not necessarily consistent across backends.
212+
For example, NLopt has `:LN_BOBYQA` and PRIMA has `:bobyqa` for Powell's
213+
BOBYQA algorithm. In other words, we have not yet abstracted over the
214+
backends' differing naming conventions for algorithms.
215+
"""
216+
function optimizers end
217+
218+
optimizers(s::Symbol) = optimizers(Val(s))

src/prima.jl

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)