-
Notifications
You must be signed in to change notification settings - Fork 432
Differentiating mvnormal #1554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Differentiating mvnormal #1554
Changes from 7 commits
789ad0a
24861ca
6e6c029
2bcc217
1e9571b
ac0995e
522c13e
6529a4a
4e4d982
d398140
661de16
3c5007c
1f18e67
b60147d
375cad8
c34a3ed
2d96680
f32c223
b225298
cf1242a
e2846e8
6bc85da
e303416
e21506a
fd272ae
8b7d451
53601fe
2c75061
6d88e4d
1f00a3b
8ebf419
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -253,7 +253,7 @@ Base.show(io::IO, d::MvNormal) = | |||||||||||||||||||||||||||||||||||||||
| length(d::MvNormal) = length(d.μ) | ||||||||||||||||||||||||||||||||||||||||
| mean(d::MvNormal) = d.μ | ||||||||||||||||||||||||||||||||||||||||
| params(d::MvNormal) = (d.μ, d.Σ) | ||||||||||||||||||||||||||||||||||||||||
| @inline partype(d::MvNormal{T}) where {T<:Real} = T | ||||||||||||||||||||||||||||||||||||||||
| @inline partype(::MvNormal{T}) where {T<:Real} = T | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| var(d::MvNormal) = diag(d.Σ) | ||||||||||||||||||||||||||||||||||||||||
| cov(d::MvNormal) = Matrix(d.Σ) | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -372,7 +372,7 @@ struct MvNormalStats <: SufficientStats | |||||||||||||||||||||||||||||||||||||||
| tw::Float64 # total sample weight | ||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function suffstats(D::Type{MvNormal}, x::AbstractMatrix{Float64}) | ||||||||||||||||||||||||||||||||||||||||
| function suffstats(::Type{MvNormal}, x::AbstractMatrix{Float64}) | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
| d = size(x, 1) | ||||||||||||||||||||||||||||||||||||||||
| n = size(x, 2) | ||||||||||||||||||||||||||||||||||||||||
| s = vec(sum(x, dims=2)) | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -382,7 +382,7 @@ function suffstats(D::Type{MvNormal}, x::AbstractMatrix{Float64}) | |||||||||||||||||||||||||||||||||||||||
| MvNormalStats(s, m, s2, Float64(n)) | ||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function suffstats(D::Type{MvNormal}, x::AbstractMatrix{Float64}, w::AbstractVector) | ||||||||||||||||||||||||||||||||||||||||
| function suffstats(::Type{MvNormal}, x::AbstractMatrix{Float64}, w::AbstractVector) | ||||||||||||||||||||||||||||||||||||||||
| d = size(x, 1) | ||||||||||||||||||||||||||||||||||||||||
| n = size(x, 2) | ||||||||||||||||||||||||||||||||||||||||
| length(w) == n || throw(DimensionMismatch("Inconsistent argument dimensions.")) | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -410,13 +410,13 @@ end | |||||||||||||||||||||||||||||||||||||||
| # each kind of covariance | ||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| fit_mle(D::Type{MvNormal}, ss::MvNormalStats) = fit_mle(FullNormal, ss) | ||||||||||||||||||||||||||||||||||||||||
| fit_mle(D::Type{MvNormal}, x::AbstractMatrix{Float64}) = fit_mle(FullNormal, x) | ||||||||||||||||||||||||||||||||||||||||
| fit_mle(D::Type{MvNormal}, x::AbstractMatrix{Float64}, w::AbstractArray{Float64}) = fit_mle(FullNormal, x, w) | ||||||||||||||||||||||||||||||||||||||||
| fit_mle(::Type{MvNormal}, ss::MvNormalStats) = fit_mle(FullNormal, ss) | ||||||||||||||||||||||||||||||||||||||||
| fit_mle(::Type{MvNormal}, x::AbstractMatrix{Float64}) = fit_mle(FullNormal, x) | ||||||||||||||||||||||||||||||||||||||||
| fit_mle(::Type{MvNormal}, x::AbstractMatrix{Float64}, w::AbstractArray{Float64}) = fit_mle(FullNormal, x, w) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| fit_mle(D::Type{FullNormal}, ss::MvNormalStats) = MvNormal(ss.m, ss.s2 * inv(ss.tw)) | ||||||||||||||||||||||||||||||||||||||||
| fit_mle(::Type{<:FullNormal}, ss::MvNormalStats) = MvNormal(ss.m, ss.s2 * inv(ss.tw)) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function fit_mle(D::Type{FullNormal}, x::AbstractMatrix{Float64}) | ||||||||||||||||||||||||||||||||||||||||
| function fit_mle(::Type{FullNormal}, x::AbstractMatrix{Float64}) | ||||||||||||||||||||||||||||||||||||||||
| n = size(x, 2) | ||||||||||||||||||||||||||||||||||||||||
| mu = vec(mean(x, dims=2)) | ||||||||||||||||||||||||||||||||||||||||
| z = x .- mu | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -425,7 +425,7 @@ function fit_mle(D::Type{FullNormal}, x::AbstractMatrix{Float64}) | |||||||||||||||||||||||||||||||||||||||
| MvNormal(mu, PDMat(C)) | ||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function fit_mle(D::Type{FullNormal}, x::AbstractMatrix{Float64}, w::AbstractVector) | ||||||||||||||||||||||||||||||||||||||||
| function fit_mle(::Type{<:FullNormal}, x::AbstractMatrix{Float64}, w::AbstractVector) | ||||||||||||||||||||||||||||||||||||||||
| m = size(x, 1) | ||||||||||||||||||||||||||||||||||||||||
| n = size(x, 2) | ||||||||||||||||||||||||||||||||||||||||
| length(w) == n || throw(DimensionMismatch("Inconsistent argument dimensions")) | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -445,7 +445,7 @@ function fit_mle(D::Type{FullNormal}, x::AbstractMatrix{Float64}, w::AbstractVec | |||||||||||||||||||||||||||||||||||||||
| MvNormal(mu, PDMat(C)) | ||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function fit_mle(D::Type{DiagNormal}, x::AbstractMatrix{Float64}) | ||||||||||||||||||||||||||||||||||||||||
| function fit_mle(::Type{DiagNormal}, x::AbstractMatrix{Float64}) | ||||||||||||||||||||||||||||||||||||||||
| m = size(x, 1) | ||||||||||||||||||||||||||||||||||||||||
| n = size(x, 2) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -460,7 +460,7 @@ function fit_mle(D::Type{DiagNormal}, x::AbstractMatrix{Float64}) | |||||||||||||||||||||||||||||||||||||||
| MvNormal(mu, PDiagMat(va)) | ||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function fit_mle(D::Type{DiagNormal}, x::AbstractMatrix{Float64}, w::AbstractVector) | ||||||||||||||||||||||||||||||||||||||||
| function fit_mle(::Type{<:DiagNormal}, x::AbstractMatrix{Float64}, w::AbstractVector) | ||||||||||||||||||||||||||||||||||||||||
| m = size(x, 1) | ||||||||||||||||||||||||||||||||||||||||
| n = size(x, 2) | ||||||||||||||||||||||||||||||||||||||||
| length(w) == n || throw(DimensionMismatch("Inconsistent argument dimensions")) | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -479,7 +479,7 @@ function fit_mle(D::Type{DiagNormal}, x::AbstractMatrix{Float64}, w::AbstractVec | |||||||||||||||||||||||||||||||||||||||
| MvNormal(mu, PDiagMat(va)) | ||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function fit_mle(D::Type{IsoNormal}, x::AbstractMatrix{Float64}) | ||||||||||||||||||||||||||||||||||||||||
| function fit_mle(::Type{IsoNormal}, x::AbstractMatrix{Float64}) | ||||||||||||||||||||||||||||||||||||||||
| m = size(x, 1) | ||||||||||||||||||||||||||||||||||||||||
| n = size(x, 2) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -495,7 +495,7 @@ function fit_mle(D::Type{IsoNormal}, x::AbstractMatrix{Float64}) | |||||||||||||||||||||||||||||||||||||||
| MvNormal(mu, ScalMat(m, va / (m * n))) | ||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function fit_mle(D::Type{IsoNormal}, x::AbstractMatrix{Float64}, w::AbstractVector) | ||||||||||||||||||||||||||||||||||||||||
| function fit_mle(::Type{<:IsoNormal}, x::AbstractMatrix{Float64}, w::AbstractVector) | ||||||||||||||||||||||||||||||||||||||||
| m = size(x, 1) | ||||||||||||||||||||||||||||||||||||||||
| n = size(x, 2) | ||||||||||||||||||||||||||||||||||||||||
| length(w) == n || throw(DimensionMismatch("Inconsistent argument dimensions")) | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -515,3 +515,87 @@ function fit_mle(D::Type{IsoNormal}, x::AbstractMatrix{Float64}, w::AbstractVect | |||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||
| MvNormal(mu, ScalMat(m, va / (m * sw))) | ||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ## Differentiation | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function ChainRulesCore.frule((_, Δd, Δx)::Tuple{Any,Any,Any}, ::typeof(_logpdf), d::AbstractMvNormal, x::AbstractVector) | ||||||||||||||||||||||||||||||||||||||||
| c0, Δc0 = ChainRulesCore.frule((ChainRulesCore.NoTangent(), Δd), mvnormal_c0, d) | ||||||||||||||||||||||||||||||||||||||||
| sq, Δsq = ChainRulesCore.frule((ChainRulesCore.NoTangent(), Δd, Δx), sqmahal, d, x) | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
| Δc0 = ChainRulesCore.unthunk(Δc0) | ||||||||||||||||||||||||||||||||||||||||
| Δsq = ChainRulesCore.unthunk(Δsq) | ||||||||||||||||||||||||||||||||||||||||
| return c0 - sq/2, Δc0 - Δsq/2 | ||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
| function ChainRulesCore.frule((_, Δd, Δx)::Tuple{Any,Any,Any}, ::typeof(_logpdf), d::AbstractMvNormal, x::AbstractVector) | |
| c0, Δc0 = ChainRulesCore.frule((ChainRulesCore.NoTangent(), Δd), mvnormal_c0, d) | |
| sq, Δsq = ChainRulesCore.frule((ChainRulesCore.NoTangent(), Δd, Δx), sqmahal, d, x) | |
| Δc0 = ChainRulesCore.unthunk(Δc0) | |
| Δsq = ChainRulesCore.unthunk(Δsq) | |
| return c0 - sq/2, Δc0 - Δsq/2 | |
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't cost us much to add this definition and lets us have derivatives built-in, we can also re-add specialized methods for some MvNormal if necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it can be quite problematic to define derivatives that overrule the AD system if they are not needed and e.g. to generic (as possibly the case here). I've ran into multiple issues of this kind with ChainRules, which then requires e.g. packages that otherwise would just work (without even knowing about ChainRules) to use ChainRules.@opt_out or define their own rules. The rule here will catch every AbstractMvNormal and AbstractVector which can be problematic e.g. similar to TuringLang/DistributionsAD.jl#180.
So I strongly recommend not adding rules that are not needed.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, this should probably be rrule_via_ad.
devmotion marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, it seems this is exactly what AD does if no rule is defined.
| function ChainRulesCore.rrule(::typeof(_logpdf), d::MvNormal, x::AbstractVector) | |
| c0, c0_pullback = ChainRulesCore.rrule(mvnormal_c0, d) | |
| sq, sq_pullback = ChainRulesCore.rrule(sqmahal, d, x) | |
| function logpdf_MvNormal_pullback(dy) | |
| dy = ChainRulesCore.unthunk(dy) | |
| (_, ∂d_c0) = c0_pullback(dy) | |
| ∂d_c0 = ChainRulesCore.unthunk(∂d_c0) | |
| (_, ∂d_sq, ∂x_sq) = sq_pullback(dy) | |
| ∂d_sq = ChainRulesCore.unthunk(∂d_sq) | |
| ∂x_sq = ChainRulesCore.unthunk(∂x_sq) | |
| backing = NamedTuple{(:μ, :Σ), Tuple{typeof(∂d_sq.μ), typeof(∂d_sq.Σ)}}(( | |
| (∂d_c0.μ - 0.5 * ∂d_sq.μ), | |
| (∂d_c0.Σ - 0.5 * ∂d_sq.Σ), | |
| )) | |
| ∂d = ChainRulesCore.Tangent{typeof(d), typeof(backing)}(backing) | |
| return ChainRulesCore.NoTangent(), ∂d, ∂x_sq / (-2) | |
| end | |
| return c0 - sq / 2, logpdf_MvNormal_pullback | |
| end |
matbesancon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
matbesancon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we avoid computing the inverse?
matbesancon marked this conversation as resolved.
Show resolved
Hide resolved
devmotion marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
matbesancon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| # Tests on Multivariate Normal distributions | ||
|
|
||
| import PDMats | ||
| import PDMats: ScalMat, PDiagMat, PDMat | ||
| if isdefined(PDMats, :PDSparseMat) | ||
| import PDMats: PDSparseMat | ||
|
|
@@ -9,6 +10,8 @@ using Distributions | |
| using LinearAlgebra, Random, Test | ||
| using SparseArrays | ||
| using FillArrays | ||
| using ChainRulesCore | ||
| using ChainRulesTestUtils | ||
|
|
||
| ###### General Testing | ||
|
|
||
|
|
@@ -302,3 +305,67 @@ end | |
| x = rand(d) | ||
| @test logpdf(d, x) ≈ logpdf(Normal(), x[1]) + logpdf(Normal(), x[2]) | ||
| end | ||
|
|
||
| @testset "MvNormal differentiation rules" begin | ||
| for n in (3, 10) | ||
| for _ in 1:10 | ||
| A = Symmetric(rand(n,n)) .+ 4 * Matrix(I, n, n) | ||
| @assert isposdef(A) | ||
| d = MvNormal(randn(n), A) | ||
| # make ΔΣ symmetric, such that Σ ± ΔΣ is PSD | ||
| t = 0.001 * ChainRulesTestUtils.rand_tangent(d) | ||
| t.Σ .+= t.Σ' | ||
| if eigmin(t.Σ) < 0 | ||
| while eigmin(d.Σ + t.Σ) < 0 | ||
| t.Σ .*= 0.8 | ||
| end | ||
| end | ||
| if eigmax(t.Σ) > 0 | ||
| while eigmin(d.Σ - t.Σ) < 0 | ||
| t.Σ .*= 0.8 | ||
| end | ||
| end | ||
| # mvnormal_c0 | ||
| (y, Δy) = @inferred ChainRulesCore.frule((ChainRulesCore.NoTangent(), t), Distributions.mvnormal_c0, d) | ||
| y_r, c0_pullback = @inferred ChainRulesCore.rrule(Distributions.mvnormal_c0, d) | ||
| @test y_r ≈ y | ||
| y2 = Distributions.mvnormal_c0(MvNormal(d.μ, d.Σ + t.Σ)) | ||
| @test unthunk(Δy) ≈ y2 - y atol= n * 1e-4 | ||
| y3 = Distributions.mvnormal_c0(MvNormal(d.μ, d.Σ - t.Σ)) | ||
| @test unthunk(Δy) ≈ y - y3 atol = n * 1e-4 | ||
| (_, ∇c0) = c0_pullback(1.0) | ||
| ∇c0 = ChainRulesCore.unthunk(∇c0) | ||
| @test dot(∇c0.Σ, t.Σ) ≈ y2 - y atol = n * 1e-4 | ||
| @test dot(∇c0.Σ, t.Σ) ≈ y - y3 atol = n * 1e-4 | ||
| # sqmahal | ||
| x = randn(n) | ||
| Δx = 0.0001 * randn(n) | ||
| (y, Δy) = @inferred ChainRulesCore.frule((ChainRulesCore.NoTangent(), t, Δx), sqmahal, d, x) | ||
| (yr, sqmahal_pullback) = @inferred ChainRulesCore.rrule(sqmahal, d, x) | ||
| (_, ∇s_d, ∇s_x) = @inferred sqmahal_pullback(1.0) | ||
| ∇s_d = ChainRulesCore.unthunk(∇s_d) | ||
| ∇s_x = ChainRulesCore.unthunk(∇s_x) | ||
| @test yr ≈ y | ||
| y2 = Distributions.sqmahal(MvNormal(d.μ + t.μ, d.Σ + t.Σ), x + Δx) | ||
| y3 = Distributions.sqmahal(MvNormal(d.μ - t.μ, d.Σ - t.Σ), x - Δx) | ||
| @test unthunk(Δy) ≈ y2 - y atol = n * 1e-4 | ||
| @test unthunk(Δy) ≈ y - y3 atol = n * 1e-4 | ||
| @test dot(∇s_d.Σ, t.Σ) + dot(∇s_d.μ, t.μ) + dot(∇s_x, Δx) ≈ y2 - y atol = n * 1e-4 | ||
| @test dot(∇s_d.Σ, t.Σ) + dot(∇s_d.μ, t.μ) + dot(∇s_x, Δx) ≈ y - y3 atol = n * 1e-4 | ||
| # _logpdf | ||
| (y, Δy) = @inferred ChainRulesCore.frule((ChainRulesCore.NoTangent(), t, Δx), Distributions._logpdf, d, x) | ||
| (yr, logpdf_MvNormal_pullback) = @inferred ChainRulesCore.rrule(Distributions._logpdf, d, x) | ||
| @test y ≈ yr | ||
| # inference broken | ||
| # (_, ∇s_d, ∇s_x) = @inferred logpdf_MvNormal_pullback(1.0) | ||
| (_, ∇s_d, ∇s_x) = logpdf_MvNormal_pullback(1.0) | ||
|
|
||
| y2 = Distributions._logpdf(MvNormal(d.μ + t.μ, d.Σ + t.Σ), x + Δx) | ||
| y3 = Distributions._logpdf(MvNormal(d.μ - t.μ, d.Σ - t.Σ), x - Δx) | ||
| @test unthunk(Δy) ≈ y - y3 atol = n * 1e-4 | ||
| @test unthunk(Δy) ≈ y2 - y atol = n * 1e-4 | ||
| @test dot(∇s_d.Σ, t.Σ) + dot(∇s_d.μ, t.μ) + dot(∇s_x, Δx) ≈ y2 - y atol = n * 1e-4 | ||
| @test dot(∇s_d.Σ, t.Σ) + dot(∇s_d.μ, t.μ) + dot(∇s_x, Δx) ≈ y - y3 atol = n * 1e-4 | ||
|
||
| end | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe move unrelated changes to a separate PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there were all relatively minor things (unused variables)