ForwardDiff.jl icon indicating copy to clipboard operation
ForwardDiff.jl copied to clipboard

Nested jacobian! calls

Open Magalame opened this issue 6 years ago • 5 comments

Hi!

I'm having some troubles using nested inplace jacobian:

using ForwardDiff
using LinearAlgebra
#some data
x0 = randn(50000);
p0 = randn(2);

#pre allocate outputs
out = similar(x0);
outjac = Array{Float64}(undef, 50000, 2); 
outjac2 = Array{Float64}(undef,100000, 2)

#'regular' and in place function
f(p) =  @. p[1] * exp(-x0 * p[2]);
f!(out, p) =  @. out = p[1] * exp(-x0 * p[2]);

ForwardDiff.jacobian!(outjac2,(outjacin,p) -> (outjacin .= ForwardDiff.jacobian(f, p)), outjac, p0) #works like a charm
ForwardDiff.jacobian!(outjac2,(outjacin,p) -> ForwardDiff.jacobian!(outjacin, f!, out, p), outjac, p0); # breaks

It throws MethodError: no method matching Float64(::ForwardDiff.Dual{ForwardDiff.Tag{getfield(Main, Symbol("##77#78")),Float64},Float64,2})

Both inner functions work and produce the same result:

g1 = (outjacin,p) -> ForwardDiff.jacobian!(outjacin, f!, out, p)
g1(outjac,p0)
g2 = (outjacin,p) -> (outjacin .= ForwardDiff.jacobian(f, p))
g2(outjac,p0)

Thanks for the help!

Magalame avatar Feb 23 '19 05:02 Magalame

A similar problem:

jac1 = p -> ForwardDiff.jacobian(f, p)
jac2 = p -> ForwardDiff.jacobian(f!,out, p)

jac1(p0) == jac2(p0)
> true

ForwardDiff.jacobian(jac1, p0) #works
ForwardDiff.jacobian(jac2, p0) #crashes

outputs

MethodError: no method matching Float64(::ForwardDiff.Dual{ForwardDiff.Tag{getfield(Main, Symbol("##130#131")),Float64},Float64,2})

Magalame avatar Feb 24 '19 20:02 Magalame

https://discourse.julialang.org/t/nested-forwarddiff-jacobian-calls-with-inplace-function/21232/2

Magalame avatar Mar 09 '19 02:03 Magalame

https://discourse.julialang.org/t/optim-gradient-and-hessian-of-constraints/31493/3 here i wrote a code that does a jacobian of a jacobian of an inplace function for use in Optim, maybe that can help

longemen3000 avatar Dec 12 '19 17:12 longemen3000

I have recently come across this and here is my take on the issue, very much inspired by how hessian! is implemented. We take care to define duals for the nested jacobian calls. Due to my use-case, I have coded vector_hessian! to mutate a DiffResult. As all target arrays are contained in result already, I deviate from the standard syntax a bit: To indicate an in-place function with signature f!(y, x), set the kwarg f_is_in_place=true. Additionally, I'd like my Hessians in “tensor-format”, so there are some non-allocating transformations in case that ndims(DiffResults.hessian(result))==3 :)


import ForwardDiff.DiffResults
import ForwardDiff: jacobian, jacobian!, JacobianConfig, DiffResult, MutableDiffResult,
    Dual, Chunk, Tag, value, checktag

struct VectorHessianConfig{T, V, N, DI, DO}
    inner_config::JacobianConfig{T, Dual{T, V, N}, N, DI}
    outer_config::JacobianConfig{T, V, N, DO}
    ydual::Vector{Dual{T,V,N}}
end

function VectorHessianConfig(
    f::F, result::DiffResult, x::AbstractArray{V}, chunk::Chunk = Chunk(x), tag = Tag(f, V)
) where {F, V}
    Dy = DiffResults.jacobian(result)
    nout = size(Dy, 1)

    outer_config = JacobianConfig((f, jacobian), Dy, x, chunk, tag)
    xdual = outer_config.duals[2]
    ydual = zeros(eltype(xdual), nout)
    inner_config = JacobianConfig(f, ydual, xdual, chunk, tag)
    return VectorHessianConfig(inner_config, outer_config, ydual)
end

mutable struct InnerJacobianForHess!{R,C,F}
    result::R
    cfg::C
    f::F
    f_is_in_place::Bool
end

function (g::InnerJacobianForHess!)(Dydual, xdual)
    inner_result = DiffResult(g.cfg.ydual, Dydual)
    
    if g.f_is_in_place
        jacobian!(inner_result, g.f, g.cfg.ydual, xdual, g.cfg.inner_config, Val(false))
    else
        jacobian!(inner_result, g.f, xdual, g.cfg.inner_config, Val(false))
    end
    # jacobian!(inner_result, g.f, xdual, g.cfg.inner_config, Val{false}(); f_is_in_place=g.f_is_in_place)
    g.result = DiffResults.value!(g.result, value.(DiffResults.value(inner_result)))
    return nothing
end

function vector_hessian!(
    result::MutableDiffResult{O}, 
    f::F, 
    x::AbstractArray, 
    cfg::VectorHessianConfig{T} = VectorHessianConfig(f, result, x), ::Val{CHK}=Val{true}();
    f_is_in_place::Bool=false,
) where {O,F,T,CHK}
    @assert O >= 2 "Target `result` must contain arrays for second-order derivatives."
    CHK && checktag(T, f, x)
    ∇f! = InnerJacobianForHess!(result, cfg, f, f_is_in_place)

    Dy = DiffResults.jacobian(result)
    nout, nvars = size(Dy)
    H = DiffResults.hessian(result)
    h = if ndims(H) == 3
        #=
        Assume, we want to store the Hessians in a 3D array `H`, such that `H[:,:,k]` 
        is the `k`-th Hessian matrix, i.e., `H[i,j,k]` is ∂/∂ᵢ∂ⱼ Hₖ.
        Given the Jacobian 
        ```
        J = [
            ∂₁f₁ ∂₂f₁  …  ∂ₙf₁ ;
            ∂₁f₂ ∂₂f₂  …  ∂ₙf₂ ;
             ⋮   ⋮    ⋱  ⋮  
            ∂₁fₘ ∂₂fₘ …  ∂ₙfₘ
        ]
        ```
        the function `ForwardDiff.jacobian` interprets the colums as seperate outputs
        and stacks their respective Jacobians vertically.
        Consider `J[:,i] = [∂ᵢf₁, …, ∂ᵢfₘ]`. 
        Its Jacobian is
        ```
        ∇(J[:,i]) = [
            (∇∂ᵢf₁)ᵀ;
            (∇∂ᵢf₂)ᵀ;
                ⋮
            (∇∂ᵢfₘ)ᵀ;
        ] = [
            ∂₁∂ᵢf₁ ∂₂∂ᵢf₁  …  ∂ₙ∂ᵢf₁ ;
            ∂₁∂ᵢf₂ ∂₂∂ᵢf₂  …  ∂ₙ∂ᵢf₂ ;
              ⋮     ⋮     ⋱    ⋮  
            ∂₁∂ᵢfₘ ∂₂∂ᵢfₘ …  ∂ₙ∂ᵢfₘ ;
        ] 
        ```
        Thus, the `i`-th block of the stacked Hessian contains as its rows the 
        `i`-th columns of all function Hessians.
        To get `H` from `∇J`, we can do 
        `H = permutedims(reshape(transpose(reshape(∇J, m, n*n)), n, n, m), (2,1,3))`
        But if `H` is provided as a pre-allocated buffer, we need the inverse operation:
        `∇J = reshape(transpose(reshape(permutedims(H, (2,1,3)), n*n, m)), m*n, n)`
        In case of symmetric Hessians, we could forgo the `permutedims` operation...
        =#
        reshape(transpose(reshape(PermutedDimsArray(H, (2,1,3)), nvars*nvars, nout)), nout*nvars, nvars)
    else
        H
    end
    jacobian!(h, ∇f!, Dy, x, cfg.outer_config, Val{false}())
    return nothing
end

https://github.com/JuliaDiff/ForwardDiff.jl/issues/360 might be related?

manuelbb-upb avatar Jul 01 '23 11:07 manuelbb-upb

ok, I actually had to change the signatures a bit, to make it work in a fresh REPL:


import ForwardDiff.DiffResults
import ForwardDiff: jacobian, jacobian!, JacobianConfig, DiffResult, MutableDiffResult,
    Dual, Chunk, Tag, value, checktag

struct VectorHessianConfig{T, V, N, DI, DO}
    inner_config::JacobianConfig{T, Dual{T, V, N}, N, DI}
    outer_config::JacobianConfig{T, V, N, DO}
    ydual::Vector{Dual{T,V,N}}
end

function VectorHessianConfig(
    f::F, result::DiffResult, x::AbstractArray{V}, f_is_in_place::Bool=false, chunk::Chunk=Chunk(x), tag=Tag(f, V)
) where {F, V}
    Dy = DiffResults.jacobian(result)
    outer_config = JacobianConfig((f, jacobian), Dy, x, chunk, tag)
    xdual = outer_config.duals[2]
    nout = size(Dy, 1)
    ydual = zeros(eltype(xdual), nout)
    inner_config = if f_is_in_place
        JacobianConfig(f, ydual, xdual, chunk, tag)
    else
        JacobianConfig(f, xdual, chunk, tag)
    end
    return VectorHessianConfig(inner_config, outer_config, ydual)
end

mutable struct InnerJacobianForHess!{R,C,F}
    result::R
    cfg::C
    f::F
    f_is_in_place::Bool
end

function (g::InnerJacobianForHess!)(Dydual, xdual)
    inner_result = DiffResult(g.cfg.ydual, Dydual)
    if g.f_is_in_place
        jacobian!(inner_result, g.f, g.cfg.ydual, xdual, g.cfg.inner_config, Val(false))
    else
        jacobian!(inner_result, g.f, xdual, g.cfg.inner_config, Val(false))
    end
    g.result = DiffResults.value!(g.result, value.(DiffResults.value(inner_result)))
    return nothing
end

function vector_hessian!(
    result::MutableDiffResult{O}, 
    f::F, 
    x::AbstractArray,
    f_is_in_place::Bool=false,
    cfg::VectorHessianConfig{T}=VectorHessianConfig(f, result, x, f_is_in_place), 
    ::Val{CHK}=Val{true}();
) where {O,F,T,CHK}
    @assert O >= 2 "Target `result` must contain arrays for second-order derivatives."
    CHK && checktag(T, f, x)
    ∇f! = InnerJacobianForHess!(result, cfg, f, f_is_in_place)

    Dy = DiffResults.jacobian(result)
    nout, nvars = size(Dy)
    H = DiffResults.hessian(result)
    h = if ndims(H) == 3
        reshape(transpose(reshape(PermutedDimsArray(H, (2,1,3)), nvars*nvars, nout)), nout*nvars, nvars)
    else
        H
    end
    jacobian!(h, ∇f!, Dy, x, cfg.outer_config, Val{false}())
    return nothing
end

manuelbb-upb avatar Jul 03 '23 12:07 manuelbb-upb