Skip to content

Commit 5902a21

Browse files
authored
Merge branch 'breaking' into mhauru/dppl-0.35
2 parents d473012 + 5c440ce commit 5902a21

File tree

7 files changed

+122
-20
lines changed

7 files changed

+122
-20
lines changed

.github/workflows/Tests.yml

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ concurrency:
1414
jobs:
1515
test:
1616
# Use matrix.test.name here to avoid it taking up the entire window width
17-
name: test ${{matrix.test.name}} (${{ matrix.runner.os }}, ${{ matrix.runner.version }}, ${{ matrix.runner.arch }}, ${{ matrix.runner.num_threads }})
17+
name: test ${{matrix.test.name}} (${{ matrix.runner.version }}, ${{ matrix.runner.os }}, ${{ matrix.runner.num_threads }})
1818
runs-on: ${{ matrix.runner.os }}
1919
continue-on-error: ${{ matrix.runner.version == 'pre' }}
2020

@@ -40,49 +40,36 @@ jobs:
4040
# Default
4141
- version: '1'
4242
os: ubuntu-latest
43-
arch: x64
44-
num_threads: 1
45-
# x86
46-
- version: '1'
47-
os: ubuntu-latest
48-
arch: x86
4943
num_threads: 1
5044
# Multithreaded
5145
- version: '1'
5246
os: ubuntu-latest
53-
arch: x64
5447
num_threads: 2
5548
# Windows
5649
- version: '1'
5750
os: windows-latest
58-
arch: x64
5951
num_threads: 1
6052
# macOS
6153
- version: '1'
6254
os: macos-latest
63-
arch: aarch64
6455
num_threads: 1
6556
# Minimum supported Julia version
6657
- version: 'min'
6758
os: ubuntu-latest
68-
arch: x64
6959
num_threads: 1
7060
# Minimum supported Julia version, multithreaded
7161
- version: 'min'
7262
os: ubuntu-latest
73-
arch: x64
7463
num_threads: 2
7564
# Pre-release Julia version
7665
- version: 'pre'
7766
os: ubuntu-latest
78-
arch: x64
7967
num_threads: 1
8068

8169
steps:
8270
- name: Print matrix variables
8371
run: |
8472
echo "OS: ${{ matrix.runner.os }}"
85-
echo "Architecture: ${{ matrix.runner.arch }}"
8673
echo "Julia version: ${{ matrix.runner.version }}"
8774
echo "Number of threads: ${{ matrix.runner.num_threads }}"
8875
echo "Test arguments: ${{ matrix.test.args }}"
@@ -93,7 +80,6 @@ jobs:
9380
- uses: julia-actions/setup-julia@v2
9481
with:
9582
version: '${{ matrix.runner.version }}'
96-
arch: ${{ matrix.runner.arch }}
9783
- uses: julia-actions/cache@v2
9884
- uses: julia-actions/julia-buildpkg@v1
9985
# TODO: Use julia-actions/julia-runtest when test_args are supported

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ Bijectors = "76274a88-744f-5084-9051-94815aaf08c4"
33
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
44
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
55
DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656"
6+
DynamicPPL = "366bfd00-2699-11ea-058f-f148b4cae6d8"
67
Turing = "fce5fe82-541a-59a6-adf8-730c64b5f9a0"

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ using Turing
44
# from those packages.
55
using Distributions
66
using Bijectors
7+
using DynamicPPL
78

89
using DocumenterInterLinks
910

docs/src/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ Distributions.BernoulliLogit
127127
### Predictions
128128

129129
```@docs
130-
predict
130+
DynamicPPL.predict
131131
```
132132

133133
### Querying model probabilities and quantities

src/optimisation/Optimisation.jl

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ using Printf: Printf
1515
using ForwardDiff: ForwardDiff
1616
using StatsAPI: StatsAPI
1717
using Statistics: Statistics
18+
using LinearAlgebra: LinearAlgebra
1819

1920
export maximum_a_posteriori, maximum_likelihood
2021
# The MAP and MLE exports are only needed for the Optim.jl interface.
@@ -235,11 +236,61 @@ end
235236

236237
# Various StatsBase methods for ModeResult
237238

238-
function StatsBase.coeftable(m::ModeResult; level::Real=0.95)
239+
"""
240+
StatsBase.coeftable(m::ModeResult; level::Real=0.95, numerrors_warnonly::Bool=true)
241+
242+
243+
Return a table with coefficients and related statistics of the model. level determines the
244+
level for confidence intervals (by default, 95%).
245+
246+
In case the `numerrors_warnonly` argument is true (the default) numerical errors encountered
247+
during the computation of the standard errors will be caught and reported in an extra
248+
"Error notes" column.
249+
"""
250+
function StatsBase.coeftable(m::ModeResult; level::Real=0.95, numerrors_warnonly::Bool=true)
239251
# Get columns for coeftable.
240252
terms = string.(StatsBase.coefnames(m))
241253
estimates = m.values.array[:, 1]
242-
stderrors = StatsBase.stderror(m)
254+
# If numerrors_warnonly is true, and if either the information matrix is singular or has
255+
# negative entries on its diagonal, then `notes` will be a list of strings for each
256+
# value in `m.values`, explaining why the standard error is NaN.
257+
notes = nothing
258+
local stderrors
259+
if numerrors_warnonly
260+
infmat = StatsBase.informationmatrix(m)
261+
local vcov
262+
try
263+
vcov = inv(infmat)
264+
catch e
265+
if isa(e, LinearAlgebra.SingularException)
266+
stderrors = fill(NaN, length(m.values))
267+
notes = fill("Information matrix is singular", length(m.values))
268+
else
269+
rethrow(e)
270+
end
271+
else
272+
vars = LinearAlgebra.diag(vcov)
273+
stderrors = eltype(vars)[]
274+
if any(x -> x < 0, vars)
275+
notes = []
276+
end
277+
for var in vars
278+
if var >= 0
279+
push!(stderrors, sqrt(var))
280+
if notes !== nothing
281+
push!(notes, "")
282+
end
283+
else
284+
push!(stderrors, NaN)
285+
if notes !== nothing
286+
push!(notes, "Negative variance")
287+
end
288+
end
289+
end
290+
end
291+
else
292+
stderrors = StatsBase.stderror(m)
293+
end
243294
zscore = estimates ./ stderrors
244295
p = map(z -> StatsAPI.pvalue(Distributions.Normal(), z; tail=:both), zscore)
245296

@@ -251,7 +302,7 @@ function StatsBase.coeftable(m::ModeResult; level::Real=0.95)
251302
level_ = 100 * level
252303
level_percentage = isinteger(level_) ? Int(level_) : level_
253304

254-
cols = [estimates, stderrors, zscore, p, ci_low, ci_high]
305+
cols = Vector[estimates, stderrors, zscore, p, ci_low, ci_high]
255306
colnms = [
256307
"Coef.",
257308
"Std. Error",
@@ -260,6 +311,10 @@ function StatsBase.coeftable(m::ModeResult; level::Real=0.95)
260311
"Lower $(level_percentage)%",
261312
"Upper $(level_percentage)%",
262313
]
314+
if notes !== nothing
315+
push!(cols, notes)
316+
push!(colnms, "Error notes")
317+
end
263318
return StatsBase.CoefTable(cols, colnms, terms)
264319
end
265320

test/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ LinearAlgebra = "1"
5959
LogDensityProblems = "2"
6060
LogDensityProblemsAD = "1.4"
6161
MCMCChains = "5, 6"
62-
Mooncake = "0.4.61"
62+
Mooncake = "0.4.95"
6363
NamedArrays = "0.9.4, 0.10"
6464
Optim = "1"
6565
Optimization = "3, 4"

test/optimisation/Optimisation.jl

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,65 @@ using Turing
627627
maximum_likelihood(m; adtype=adbackend)
628628
maximum_a_posteriori(m; adtype=adbackend)
629629
end
630+
631+
@testset "Collinear coeftable" begin
632+
xs = [-1.0, 0.0, 1.0]
633+
ys = [0.0, 0.0, 0.0]
634+
635+
@model function collinear(x, y)
636+
a ~ Normal(0, 1)
637+
b ~ Normal(0, 1)
638+
return y ~ MvNormal(a .* x .+ b .* x, 1)
639+
end
640+
641+
model = collinear(xs, ys)
642+
mle_estimate = Turing.Optimisation.estimate_mode(model, MLE())
643+
tab = coeftable(mle_estimate)
644+
@assert isnan(tab.cols[2][1])
645+
@assert tab.colnms[end] == "Error notes"
646+
@assert occursin("singular", tab.cols[end][1])
647+
end
648+
649+
@testset "Negative variance" begin
650+
# A model for which the likelihood has a saddle point at x=0, y=0.
651+
# Creating an optimisation result for this model at the x=0, y=0 results in negative
652+
# variance for one of the variables, because the variance is calculated as the
653+
# diagonal of the inverse of the Hessian.
654+
@model function saddle_model()
655+
x ~ Normal(0, 1)
656+
y ~ Normal(x, 1)
657+
Turing.@addlogprob! x^2 - y^2
658+
return nothing
659+
end
660+
m = saddle_model()
661+
ctx = Turing.Optimisation.OptimizationContext(DynamicPPL.LikelihoodContext())
662+
optim_ld = Turing.Optimisation.OptimLogDensity(m, ctx)
663+
vals = Turing.Optimisation.NamedArrays.NamedArray([0.0, 0.0])
664+
m = Turing.Optimisation.ModeResult(vals, nothing, 0.0, optim_ld)
665+
ct = coeftable(m)
666+
@assert isnan(ct.cols[2][1])
667+
@assert ct.colnms[end] == "Error notes"
668+
@assert occursin("Negative variance", ct.cols[end][1])
669+
end
670+
671+
@testset "Same coeftable with/without numerrors_warnonly" begin
672+
xs = [0.0, 1.0, 2.0]
673+
674+
@model function extranormal(x)
675+
mean ~ Normal(0, 1)
676+
return x ~ Normal(mean, 1)
677+
end
678+
679+
model = extranormal(xs)
680+
mle_estimate = Turing.Optimisation.estimate_mode(model, MLE())
681+
warnonly_coeftable = coeftable(mle_estimate; numerrors_warnonly=true)
682+
no_warnonly_coeftable = coeftable(mle_estimate; numerrors_warnonly=false)
683+
@assert warnonly_coeftable.cols == no_warnonly_coeftable.cols
684+
@assert warnonly_coeftable.colnms == no_warnonly_coeftable.colnms
685+
@assert warnonly_coeftable.rownms == no_warnonly_coeftable.rownms
686+
@assert warnonly_coeftable.pvalcol == no_warnonly_coeftable.pvalcol
687+
@assert warnonly_coeftable.teststatcol == no_warnonly_coeftable.teststatcol
688+
end
630689
end
631690

632691
end

0 commit comments

Comments
 (0)