Skip to content

Commit d7f9223

Browse files
authored
replace fitlog with a column table, always populate it (#850)
* replace fitlog with a column table, always populate it * NEWS entry * address new and old fitlog formats in serialization * docs * remove fitlog from bootstrap * no thin
1 parent bf4b8eb commit d7f9223

15 files changed

+115
-90
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ MixedModels v5.0.0 Release Notes
33
- Optimization is now performed _without constraints_. In a post-fitting step, the Cholesky factor is canonicalized to have non-negative diagonal elements. [#840]
44
- 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+
- [BREAKING] A fitlog is always kept -- the deprecated keyword argument `thin` has been removed as has the `fitlog` keyword argument. [#850]
7+
- The fitlog is now stored as Tables.jl-compatible column table. [#850]
68
- 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]
79
- Internal code around optimization in profiling has been restructuring so that fitting done during calls to `profile` respect the `backend` and `optimizer` settings. [#853]
810
- The `prfit!` convenience function has been removed. [#853]

docs/src/optimization.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,8 @@ DisplayAs.Text(ans) # hide
190190
```
191191

192192
More detailed information about the intermediate steps of the nonlinear optimizer can be obtained the `fitlog` field.
193-
By default, `fitlog` is not populated, but passing the keyword argument `fitlog=true` to `fit!` and `refit!` will result in it being populated with the values obtained at each step of optimization:
194193

195194
```@example Main
196-
refit!(fm2; fitlog=true)
197195
first(fm2.optsum.fitlog, 5)
198196
DisplayAs.Text(ans) # hide
199197
```
@@ -250,7 +248,7 @@ Suppose, for example, that the user wishes to try a [Nelder-Mead](https://en.wik
250248
```@example Main
251249
fm2nm = LinearMixedModel(@formula(reaction ~ 1+days+(1+days|subj)), sleepstudy);
252250
fm2nm.optsum.optimizer = :LN_NELDERMEAD;
253-
fit!(fm2nm; fitlog=true)
251+
fit!(fm2nm)
254252
fm2nm.optsum
255253
DisplayAs.Text(ans) # hide
256254
```

ext/MixedModelsPRIMAExt.jl

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ _optimizer!(::Val{:lincoa}, args...; kwargs...) = PRIMA.lincoa!(args...; kwargs.
2626
_optimizer!(::Val{:newuoa}, args...; kwargs...) = PRIMA.newuoa!(args...; kwargs...)
2727

2828
function MixedModels.optimize!(m::LinearMixedModel, ::PRIMABackend;
29-
progress::Bool=true, fitlog::Bool=false, kwargs...)
29+
progress::Bool=true, kwargs...)
3030
optsum = m.optsum
3131
prog = ProgressUnknown(; desc="Minimizing", showspeed=true)
32-
fitlog && empty!(optsum.fitlog)
32+
empty!(optsum.fitlog)
3333

3434
function obj(x)
3535
val = if x == optsum.initial
@@ -49,15 +49,14 @@ function MixedModels.optimize!(m::LinearMixedModel, ::PRIMABackend;
4949
end
5050
end
5151
progress && ProgressMeter.next!(prog; showvalues=[(:objective, val)])
52-
fitlog && push!(optsum.fitlog, (copy(x), val))
52+
push!(optsum.fitlog, (; θ=copy(x), objective=val))
5353
return val
5454
end
5555

5656
maxfun = optsum.maxfeval > 0 ? optsum.maxfeval : 500 * length(optsum.initial)
5757
info = _optimizer!(Val(optsum.optimizer), obj, optsum.final;
5858
maxfun,
5959
optsum.rhoend, optsum.rhobeg)
60-
ProgressMeter.finish!(prog)
6160
optsum.feval = info.nf
6261
optsum.fmin = info.fx
6362
optsum.returnvalue = Symbol(info.status)
@@ -66,12 +65,12 @@ function MixedModels.optimize!(m::LinearMixedModel, ::PRIMABackend;
6665
end
6766

6867
function MixedModels.optimize!(m::GeneralizedLinearMixedModel, ::PRIMABackend;
69-
progress::Bool=true, fitlog::Bool=false,
68+
progress::Bool=true,
7069
fast::Bool=false, verbose::Bool=false, nAGQ=1,
7170
kwargs...)
7271
optsum = m.optsum
7372
prog = ProgressUnknown(; desc="Minimizing", showspeed=true)
74-
fitlog && empty!(opstum.fitlog)
73+
empty!(optsum.fitlog)
7574

7675
function obj(x)
7776
val = try
@@ -83,7 +82,7 @@ function MixedModels.optimize!(m::GeneralizedLinearMixedModel, ::PRIMABackend;
8382
x == optsum.initial && rethrow()
8483
m.optsum.finitial
8584
end
86-
fitlog && push!(optsum.fitlog, (copy(x), val))
85+
push!(optsum.fitlog, (; θ=copy(x), objective=val))
8786
verbose && println(round(val; digits=5), " ", x)
8887
progress && ProgressMeter.next!(prog; showvalues=[(:objective, val)])
8988
return val

src/MixedModelsNLoptExt.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ end
1919
const NLoptBackend = Val{:nlopt}
2020

2121
function MixedModels.optimize!(m::LinearMixedModel, ::NLoptBackend;
22-
progress::Bool=true, fitlog::Bool=false,
22+
progress::Bool=true,
2323
kwargs...)
2424
optsum = m.optsum
2525
prog = ProgressUnknown(; desc="Minimizing", showspeed=true)
26-
fitlog && empty!(optsum.fitlog)
26+
empty!(optsum.fitlog)
2727

2828
function obj(x, g)
2929
isempty(g) || throw(ArgumentError("g should be empty for this objective"))
@@ -44,7 +44,7 @@ function MixedModels.optimize!(m::LinearMixedModel, ::NLoptBackend;
4444
end
4545
end
4646
progress && ProgressMeter.next!(prog; showvalues=[(:objective, val)])
47-
fitlog && push!(optsum.fitlog, (copy(x), val))
47+
push!(optsum.fitlog, (; θ=copy(x), objective=val))
4848
return val
4949
end
5050

@@ -59,12 +59,12 @@ function MixedModels.optimize!(m::LinearMixedModel, ::NLoptBackend;
5959
end
6060

6161
function MixedModels.optimize!(m::GeneralizedLinearMixedModel, ::NLoptBackend;
62-
progress::Bool=true, fitlog::Bool=false,
62+
progress::Bool=true,
6363
fast::Bool=false, verbose::Bool=false, nAGQ=1,
6464
kwargs...)
6565
optsum = m.optsum
6666
prog = ProgressUnknown(; desc="Minimizing", showspeed=true)
67-
fitlog && empty!(optsum.fitlog)
67+
empty!(optsum.fitlog)
6868

6969
function obj(x, g)
7070
isempty(g) || throw(ArgumentError("g should be empty for this objective"))
@@ -77,7 +77,7 @@ function MixedModels.optimize!(m::GeneralizedLinearMixedModel, ::NLoptBackend;
7777
x == optsum.initial && rethrow()
7878
optsum.finitial
7979
end
80-
fitlog && push!(optsum.fitlog, (copy(x), val))
80+
push!(optsum.fitlog, (; θ=copy(x), objective=val))
8181
verbose && println(round(val; digits=5), " ", x)
8282
progress && ProgressMeter.next!(prog; showvalues=[(:objective, val)])
8383
return val

src/bootstrap.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ function parametricbootstrap(
254254
)
255255
samp = replicate(n; progress) do
256256
simulate!(rng, m; β, σ, θ)
257-
refit!(m; progress=false, fitlog=false)
257+
refit!(m; progress=false)
258258
(
259259
objective=ftype.(m.objective),
260260
σ=ismissing(m.σ) ? missing : ftype(m.σ),

src/generalizedlinearmixedmodel.jl

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ end
213213
"""
214214
fit!(m::GeneralizedLinearMixedModel; fast=false, nAGQ=1,
215215
verbose=false, progress=true,
216-
thin::Int=1,
217216
init_from_lmm=Set())
218217
219218
Optimize the objective function for `m`.
@@ -228,9 +227,6 @@ and it may not be shown at all for models that are optimized quickly.
228227
229228
If `verbose` is `true`, then both the intermediate results of both the nonlinear optimization and PIRLS are also displayed on standard output.
230229
231-
The `thin` argument is ignored: it had no impact on the final model fit and the logic around
232-
thinning the `fitlog` was needlessly complicated for a trivial performance gain.
233-
234230
By default, the starting values for model fitting are taken from a (non mixed,
235231
i.e. marginal ) GLM fit. Experience with larger datasets (many thousands of
236232
observations and/or hundreds of levels of the grouping variables) has suggested
@@ -256,8 +252,6 @@ function StatsAPI.fit!(
256252
fast::Bool=false,
257253
nAGQ::Integer=1,
258254
progress::Bool=true,
259-
thin::Int=typemax(Int),
260-
fitlog::Bool=false,
261255
init_from_lmm=Set(),
262256
backend::Symbol=m.optsum.backend,
263257
optimizer::Symbol=m.optsum.optimizer,
@@ -294,7 +288,7 @@ function StatsAPI.fit!(
294288
optsum.backend = backend
295289
optsum.optimizer = optimizer
296290

297-
xmin, fmin = optimize!(m; progress, fitlog, fast, verbose, nAGQ)
291+
xmin, fmin = optimize!(m; progress, fast, verbose, nAGQ)
298292

299293
θopt = length(xmin) == length(θ) ? xmin : view(xmin, (length(β) + 1):lastindex(xmin))
300294
rectify!(m.LMM) # flip signs of columns of m.λ elements with negative diagonal els
@@ -312,7 +306,7 @@ function StatsAPI.fit!(
312306
(fmin + optsum.ftol_zero_abs)
313307
fmin = zeroobj
314308
copyto!(xmin, xmin_)
315-
fitlog && push!(optsum.fitlog, (copy(xmin), fmin))
309+
push!(optsum.fitlog, (; θ=copy(xmin), objective=fmin))
316310
end
317311
end
318312

src/linearmixedmodel.jl

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -443,24 +443,17 @@ end
443443

444444
"""
445445
fit!(m::LinearMixedModel; progress::Bool=true, REML::Bool=m.optsum.REML,
446-
σ::Union{Real, Nothing}=m.optsum.sigma,
447-
thin::Int=typemax(Int),
448-
fitlog::Bool=true)
446+
σ::Union{Real, Nothing}=m.optsum.sigma)
449447
450448
Optimize the objective of a `LinearMixedModel`. When `progress` is `true` a
451449
`ProgressMeter.ProgressUnknown` display is shown during the optimization of the
452450
objective, if the optimization takes more than one second or so.
453-
454-
The `thin` argument is ignored: it had no impact on the final model fit and the logic around
455-
thinning the `fitlog` was needlessly complicated for a trivial performance gain.
456451
"""
457452
function StatsAPI.fit!(
458453
m::LinearMixedModel{T};
459454
progress::Bool=true,
460455
REML::Bool=m.optsum.REML,
461456
σ::Union{Real,Nothing}=m.optsum.sigma,
462-
thin::Int=typemax(Int),
463-
fitlog::Bool=false,
464457
backend::Symbol=m.optsum.backend,
465458
optimizer::Symbol=m.optsum.optimizer,
466459
) where {T}
@@ -495,7 +488,7 @@ function StatsAPI.fit!(
495488
optsum.finitial = objective!(m, optsum.initial)
496489
end
497490

498-
xmin, fmin = optimize!(m; progress, fitlog)
491+
xmin, fmin = optimize!(m; progress)
499492

500493
setθ!(m, xmin) # ensure that the parameters saved in m are xmin
501494
rectify!(m) # flip signs of columns of m.λ elements with negative diagonal els
@@ -512,7 +505,7 @@ function StatsAPI.fit!(
512505
if (zeroobj = objective!(m, xmin_)) (fmin + optsum.ftol_zero_abs)
513506
fmin = zeroobj
514507
copyto!(xmin, xmin_)
515-
fitlog && push!(optsum.fitlog, (copy(xmin), fmin))
508+
push!(optsum.fitlog, (; θ=copy(xmin), objective=fmin))
516509
end
517510
end
518511

src/optsummary.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Similarly, the list of applicable optimization parameters can be inspected with
4747
* `rhoend`: as in PRIMA, not used in NLopt
4848
4949
## MixedModels-specific fields, unrelated to the optimizer
50-
* `fitlog`: A vector of tuples of parameter and objectives values from steps in the optimization
50+
* `fitlog`: A Tables.jl-compatible table with columns `θ` and `objective`. The precise type is an implementation detail
5151
* `nAGQ`: number of adaptive Gauss-Hermite quadrature points in deviance evaluation for GLMMs
5252
* `REML`: use the REML criterion for LMM fits
5353
* `sigma`: a priori value for the residual standard deviation for LMM
@@ -90,7 +90,9 @@ Base.@kwdef mutable struct OptSummary{T<:AbstractFloat}
9090
rhoend::T = rhobeg / 1_000_000
9191

9292
# not SVector because we would need to parameterize on size (which breaks GLMM)
93-
fitlog::Vector{Tuple{Vector{T},T}} = Vector{Tuple{Vector{T},T}}()
93+
# we could parameterize more strictly, but this is already a mutable struct
94+
# and not isbits, so I don't think we're going to see any real advantage
95+
fitlog::Table = Table(; θ=Vector{Vector{T}}(), objective=T[])
9496
nAGQ::Int = 1
9597
REML::Bool = false
9698
sigma::Union{T,Nothing} = nothing
@@ -121,7 +123,7 @@ and `par` is a vector of parameter numbers.
121123
"""
122124
function Tables.columntable(s::OptSummary; stack::Bool=false)
123125
fitlog = s.fitlog
124-
val = (; iter=axes(fitlog, 1), objective=last.(fitlog), θ=first.(fitlog))
126+
val = (; iter=axes(fitlog, 1), objective=fitlog.objective, θ=fitlog.θ)
125127
stack || return val
126128
θ1 = first(val.θ)
127129
k = length(θ1)

src/profile/fixefpr.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ function betaprofile!(
5454
pr::FeProfile{T}, tc::TableColumns{T}, βⱼ::T, j::Integer, obj::T, neg::Bool
5555
) where {T}
5656
prm = pr.m
57-
refit!(prm, mul!(copyto!(prm.y, pr.y₀), pr.xⱼ, βⱼ, -1, 1); progress=false, fitlog=false)
57+
refit!(prm, mul!(copyto!(prm.y, pr.y₀), pr.xⱼ, βⱼ, -1, 1); progress=false)
5858
(; positions, v) = tc
5959
v[1] = (-1)^neg * sqrt(prm.objective - obj)
6060
getθ!(view(v, positions[]), prm)

src/profile/profile.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ Profiling starts at the parameter estimate and continues until reaching a parame
3333
value of ζ exceeds `threshold`.
3434
"""
3535
function profile(m::LinearMixedModel; threshold=4)
36-
isfitted(m) || refit!(m; progress=false, fitlog=false)
36+
isfitted(m) || refit!(m; progress=false)
37+
fitlog = copy(m.optsum.fitlog)
3738
final = copy(m.optsum.final)
3839
profile = try
3940
tc = TableColumns(m)
@@ -57,6 +58,7 @@ function profile(m::LinearMixedModel; threshold=4)
5758
copyto!(m.optsum.final, final)
5859
m.optsum.fmin = objective(m)
5960
m.optsum.sigma = nothing
61+
m.optsum.fitlog = fitlog
6062
end
6163
return profile
6264
end

0 commit comments

Comments
 (0)