Skip to content

Commit e9a139b

Browse files
dmbatespalday
andauthored
[WIP] Switch to unconstrained optimizers - step 1 (#840)
* Change lower bounds to -Inf, repair tests * run JuliaFormatter on src dir * Comparison values from AppleAccelerate * [ci skip] allow PRIMA::newuoa optimizer * [ci skip] don't pass lower bounds to optimizer * Initial comparison of optimizers * [ci skip] Adjust test targets and tolerances * [ci skip] Adjust tolerance on test for x86_64, Linux, OpenBLAS * Force ci * Loosen tolerances (again) * yet another tolerance adjustment in a test * format * Coverage and formatting * format * Add missing import * Default optimizer to :LN_NEWUOA. Add timingtable function. * Account for re-ordering of models. * Default optimizer to :LN_NEWUOA, adjust tests, add timingtable * Remove test of nonsensical model. * Add atol to first(std(fmnc)) test. * Add tolerance in prima test. * More tolerance additions * bump version to 5.0.0-DEV * NEWS update --------- Co-authored-by: Phillip Alday <[email protected]>
1 parent 42950ae commit e9a139b

38 files changed

+1397
-849
lines changed

NEWS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
MixedModels v5.0.0 Release Notes
2+
==============================
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]
5+
- [BREAKING] Support for constrained optimization has been completely removed, i.e. the field `lowerbd` has been removed from `OptSummary`.
6+
7+
18
MixedModels v4.38.0 Release Notes
29
==============================
310
- Experimental support for evaluating `FiniteDiff.finite_difference_gradient` and `FiniteDiff.finite_difference_hessian of the objective of a fitted `LinearMixedModel`. [#842]
@@ -655,5 +662,6 @@ Package dependencies
655662
[#828]: https://github.com/JuliaStats/MixedModels.jl/issues/828
656663
[#829]: https://github.com/JuliaStats/MixedModels.jl/issues/829
657664
[#836]: https://github.com/JuliaStats/MixedModels.jl/issues/836
665+
[#840]: https://github.com/JuliaStats/MixedModels.jl/issues/840
658666
[#841]: https://github.com/JuliaStats/MixedModels.jl/issues/841
659667
[#842]: https://github.com/JuliaStats/MixedModels.jl/issues/842

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "MixedModels"
22
uuid = "ff71e718-51f3-5ec2-a782-8ffcbfa3c316"
33
author = ["Phillip Alday <[email protected]>", "Douglas Bates <[email protected]>"]
4-
version = "4.38.0"
4+
version = "5.0.0-DEV"
55

66
[deps]
77
Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45"

ext/MixedModelsForwardDiffExt.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ using MixedModels: AbstractReMat,
99
kp1choose2,
1010
LD,
1111
lmulΛ!,
12+
rankUpdate!,
1213
rmulΛ!,
1314
ssqdenom,
1415
UniformBlockDiagonal

ext/MixedModelsPRIMAExt.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ end
2525
prima_optimizer!(::Val{:bobyqa}, args...; kwargs...) = PRIMA.bobyqa!(args...; kwargs...)
2626
prima_optimizer!(::Val{:cobyla}, args...; kwargs...) = PRIMA.cobyla!(args...; kwargs...)
2727
prima_optimizer!(::Val{:lincoa}, args...; kwargs...) = PRIMA.lincoa!(args...; kwargs...)
28+
prima_optimizer!(::Val{:newuoa}, args...; kwargs...) = PRIMA.newuoa!(args...; kwargs...)
2829

2930
function MixedModels.optimize!(m::LinearMixedModel, ::PRIMABackend;
3031
progress::Bool=true, fitlog::Bool=false, kwargs...)
@@ -56,7 +57,8 @@ function MixedModels.optimize!(m::LinearMixedModel, ::PRIMABackend;
5657

5758
maxfun = optsum.maxfeval > 0 ? optsum.maxfeval : 500 * length(optsum.initial)
5859
info = prima_optimizer!(Val(optsum.optimizer), obj, optsum.final;
59-
xl=optsum.lowerbd, maxfun,
60+
# xl=optsum.lowerbd,
61+
maxfun,
6062
optsum.rhoend, optsum.rhobeg)
6163
ProgressMeter.finish!(prog)
6264
optsum.feval = info.nf

src/generalizedlinearmixedmodel.jl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ function StatsAPI.fit!(
296296

297297
xmin, fmin = optimize!(m; progress, fitlog, fast, verbose, nAGQ)
298298

299+
θopt = length(xmin) == length(θ) ? xmin : view(xmin, (length(β) + 1):lastindex(xmin))
300+
rectify!(m.LMM) # flip signs of columns of m.λ elements with negative diagonal els
301+
getθ!(θopt, m) # use the rectified values in xmin
302+
299303
## check if very small parameter values bounded below by zero can be set to zero
300304
xmin_ = copy(xmin)
301305
for i in eachindex(xmin_)
@@ -789,10 +793,10 @@ function unfit!(model::GeneralizedLinearMixedModel{T}) where {T}
789793
optsum = model.LMM.optsum
790794
# we need to reset optsum so that it
791795
# plays nice with the modifications fit!() does
792-
optsum.lowerbd = mapfoldl(lowerbd, vcat, reterms)
796+
optsum.lowerbd = mapfoldl(lowerbd, vcat, reterms) # probably don't need this anymore - now trivial with all elements = -Inf
793797
# for variances (bounded at zero), we have ones, while
794798
# for everything else (bounded at -Inf), we have zeros
795-
optsum.initial = map(T iszero, optsum.lowerbd)
799+
optsum.initial = map(x -> T(x[2] == x[3]), model.LMM.parmap)
796800
optsum.final = copy(optsum.initial)
797801
optsum.xtol_abs = fill!(copy(optsum.initial), 1.0e-10)
798802
optsum.initial_step = T[]

src/linearmixedmodel.jl

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -497,11 +497,14 @@ function StatsAPI.fit!(
497497

498498
xmin, fmin = optimize!(m; progress, fitlog)
499499

500+
setθ!(m, xmin) # ensure that the parameters saved in m are xmin
501+
rectify!(m) # flip signs of columns of m.λ elements with negative diagonal els
502+
getθ!(xmin, m) # use the rectified values as xmin
503+
500504
## check if small non-negative parameter values can be set to zero
501505
xmin_ = copy(xmin)
502-
lb = optsum.lowerbd
503-
for i in eachindex(xmin_)
504-
if iszero(lb[i]) && zero(T) < xmin_[i] < optsum.xtol_zero_abs
506+
for (i, pm) in enumerate(m.parmap)
507+
if pm[2] == pm[3] && zero(T) < xmin_[i] < optsum.xtol_zero_abs
505508
xmin_[i] = zero(T)
506509
end
507510
end
@@ -681,6 +684,19 @@ function Base.getproperty(m::LinearMixedModel{T}, s::Symbol) where {T}
681684
end
682685
end
683686

687+
"""
688+
_set_init(m::LinearMixedModel)
689+
690+
Set each element of m.optsum.initial to 1.0 for diagonal and 0.0 for off-diagonal
691+
"""
692+
function _set_init!(m::LinearMixedModel)
693+
init = m.optsum.initial
694+
for (i, pm) in enumerate(m.parmap)
695+
init[i] = pm[2] == pm[3]
696+
end
697+
return m
698+
end
699+
684700
StatsAPI.islinear(m::LinearMixedModel) = true
685701

686702
"""
@@ -927,6 +943,34 @@ end
927943

928944
LinearAlgebra.rank(m::LinearMixedModel) = m.feterm.rank
929945

946+
"""
947+
rectify!(m::LinearMixedModel)
948+
949+
For each element of m.λ check for negative values on the diagonal and flip the signs of the entire column when any are present.
950+
951+
This provides a canonical converged value of θ. We use unconstrained optimization followed by this reassignment to avoid the
952+
hassle of constrained optimization.
953+
"""
954+
function rectify!(m::LinearMixedModel)
955+
rectify!.(m.λ)
956+
return m
957+
end
958+
959+
function rectify!::LowerTriangular)
960+
for (j, c) in enumerate(eachcol.data))
961+
if c[j] < 0
962+
c .*= -1
963+
end
964+
end
965+
return λ
966+
end
967+
968+
function rectify!::Diagonal)
969+
d = λ.diag
970+
map!(abs, d, d)
971+
return λ
972+
end
973+
930974
"""
931975
rePCA(m::LinearMixedModel; corr::Bool=true)
932976
@@ -939,7 +983,7 @@ principal component, the first two principal components, etc. The last element
939983
always 1.0 representing the complete proportion of the variance.
940984
"""
941985
function rePCA(m::LinearMixedModel; corr::Bool=true)
942-
pca = PCA.(m.reterms, corr=corr)
986+
pca = PCA.(m.reterms; corr=corr)
943987
return NamedTuple{_unique_fnames(m)}(getproperty.(pca, :cumvar))
944988
end
945989

@@ -951,7 +995,7 @@ covariance matrices or correlation matrices when `corr` is `true`.
951995
"""
952996

953997
function PCA(m::LinearMixedModel; corr::Bool=true)
954-
return NamedTuple{_unique_fnames(m)}(PCA.(m.reterms, corr=corr))
998+
return NamedTuple{_unique_fnames(m)}(PCA.(m.reterms; corr=corr))
955999
end
9561000

9571001
"""
@@ -1256,9 +1300,8 @@ function unfit!(model::LinearMixedModel{T}) where {T}
12561300
optsum = model.optsum
12571301
optsum.feval = -1
12581302
optsum.initial_step = T[]
1259-
# for variances (bounded at zero), we have ones, while
1260-
# for everything else (bounded at -Inf), we have zeros
1261-
map!(T iszero, optsum.initial, optsum.lowerbd)
1303+
# initialize elements on the diagonal of Λ to one(T), off-diagonals to zero(T)
1304+
_set_init!(model)
12621305
copyto!(optsum.final, optsum.initial)
12631306
reevaluateAend!(model)
12641307

src/mixedmodel.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,11 @@ Equality comparisons are used b/c small non-negative θ values are replaced by 0
6868
For `GeneralizedLinearMixedModel`, the entire parameter vector (including
6969
β in the case `fast=false`) must be specified if the default is not used.
7070
"""
71-
function issingular(m::MixedModel, θ=m.θ; atol::Real=0, rtol::Real=atol > 0 ? 0 : eps())
72-
return _issingular(m.lowerbd, θ; atol, rtol)
71+
function issingular(
72+
m::MixedModel{T}, θ=m.θ; atol::Real=0, rtol::Real=atol > 0 ? 0 : eps()
73+
) where {T}
74+
lb = [(pm[2] == pm[3]) ? zero(T) : T(-Inf) for pm in m.parmap]
75+
return _issingular(lb, θ; atol, rtol)
7376
end
7477

7578
function _issingular(v, w; atol, rtol)

src/nlopt.jl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,19 @@ end
8282

8383
function NLopt.Opt(optsum::OptSummary)
8484
lb = optsum.lowerbd
85+
n = length(lb)
8586

86-
opt = NLopt.Opt(optsum.optimizer, length(lb))
87+
if optsum.optimizer == :LN_NEWUOA && isone(n) # :LN_NEWUOA doesn't allow n == 1
88+
optsum.optimizer = :LN_BOBYQA
89+
end
90+
opt = NLopt.Opt(optsum.optimizer, n)
8791
NLopt.ftol_rel!(opt, optsum.ftol_rel) # relative criterion on objective
8892
NLopt.ftol_abs!(opt, optsum.ftol_abs) # absolute criterion on objective
8993
NLopt.xtol_rel!(opt, optsum.xtol_rel) # relative criterion on parameter values
90-
if length(optsum.xtol_abs) == length(lb) # not true for fast=false optimization in GLMM
94+
if length(optsum.xtol_abs) == n # not true for fast=false optimization in GLMM
9195
NLopt.xtol_abs!(opt, optsum.xtol_abs) # absolute criterion on parameter values
9296
end
93-
NLopt.lower_bounds!(opt, lb)
97+
# NLopt.lower_bounds!(opt, lb) # use unconstrained optimization even for :LN_BOBYQA
9498
NLopt.maxeval!(opt, optsum.maxfeval)
9599
NLopt.maxtime!(opt, optsum.maxtime)
96100
if isempty(optsum.initial_step)

src/optsummary.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Base.@kwdef mutable struct OptSummary{T<:AbstractFloat}
6060
ftol_zero_abs::T = eltype(initial)(1.e-5)
6161
maxfeval::Int = -1
6262

63-
optimizer::Symbol = :LN_BOBYQA
63+
optimizer::Symbol = :LN_NEWUOA # switched to :LN_BOBYQA for one-dimensional optimizations
6464
backend::Symbol = :nlopt
6565

6666
# the @kwdef macro isn't quite smart enough for us to use the type parameter
@@ -85,7 +85,7 @@ end
8585
function OptSummary(
8686
initial::Vector{T},
8787
lowerbd::Vector{S},
88-
optimizer::Symbol=:LN_BOBYQA; kwargs...,
88+
optimizer::Symbol=:LN_NEWUOA; kwargs...,
8989
) where {T<:AbstractFloat,S<:AbstractFloat}
9090
TS = promote_type(T, S)
9191
return OptSummary{TS}(; initial, lowerbd, optimizer, kwargs...)

src/pca.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ function Base.show(
8181
# only display the lower triangle of symmetric matrix
8282
if pca.rnames !== missing
8383
n = length(pca.rnames)
84-
cv = string.(round.(pca.covcor, digits=ndigitsmat))
84+
cv = string.(round.(pca.covcor; digits=ndigitsmat))
8585
dotpad = lpad(".", div(maximum(length, cv), 2))
8686
for i in 1:n, j in (i + 1):n
8787
cv[i, j] = dotpad
@@ -97,7 +97,7 @@ function Base.show(
9797
# if there are no names, then we cheat and use the print method
9898
# for LowerTriangular, which automatically covers the . in the
9999
# upper triangle
100-
printmat = round.(LowerTriangular(pca.covcor), digits=ndigitsmat)
100+
printmat = round.(LowerTriangular(pca.covcor); digits=ndigitsmat)
101101
end
102102

103103
Base.print_matrix(io, printmat)
@@ -106,21 +106,21 @@ function Base.show(
106106
if stddevs
107107
println(io, "\nStandard deviations:")
108108
sv = pca.sv
109-
show(io, round.(sv.S, digits=ndigitsvec))
109+
show(io, round.(sv.S; digits=ndigitsvec))
110110
println(io)
111111
end
112112
if variances
113113
println(io, "\nVariances:")
114114
vv = abs2.(sv.S)
115-
show(io, round.(vv, digits=ndigitsvec))
115+
show(io, round.(vv; digits=ndigitsvec))
116116
println(io)
117117
end
118118
println(io, "\nNormalized cumulative variances:")
119-
show(io, round.(pca.cumvar, digits=ndigitscum))
119+
show(io, round.(pca.cumvar; digits=ndigitscum))
120120
println(io)
121121
if loadings
122122
println(io, "\nComponent loadings")
123-
printmat = round.(pca.loadings, digits=ndigitsmat)
123+
printmat = round.(pca.loadings; digits=ndigitsmat)
124124
if pca.rnames !== missing
125125
pclabs = [Text(""); Text.("PC$i" for i in 1:length(pca.rnames))]
126126
pclabs = reshape(pclabs, 1, :)

0 commit comments

Comments
 (0)