Optimizable parameters


#1

TLDR: I want to use Parameters.jl and convert a fraction of its fields to a vector and back. I can convert to the vector, but I don’t know how to convert back - please help

Here is my MWE Para type

using Parameters
@with_kw struct Para{T}
    a::T = 1.0
    b::T = 2.0
    c::T = 3.0
end

with three parameters (a, b, and c). Some examples of concrete Paras:

julia> p1 = Para()
Para{Float64}
  a: Float64 1.0
  b: Float64 2.0
  c: Float64 3.0

julia> p2 = Para(a = 4.0, c = 5.0)
Para{Float64}
  a: Float64 4.0
  b: Float64 2.0
  c: Float64 5.0

Note that the Para constructor nicely works by supplying it assignments (like a = 4.0 - Thanks to Parameters.jl! ).

I want to optimize only a and b. For that, I want to create a vector of the values in a and b for a given Para. So I create a tuple of the symbol names

optimizable_Para = (:a, :b)

which lists the “optimizable” parameters. To create the vector of the optimizable parameters, I have the function

function convert_Para_to_Vector(p::Para)
    [getfield(p, s) for s in optimizable_Para]
end

Some examples of conversion from Para to Vector:

julia> x1 = convert_Para_to_Vector(p1)
2-element Array{Float64,1}:
 1.0
 2.0

julia> x2 = convert_Para_to_Vector(p2)
2-element Array{Float64,1}:
 4.0
 2.0

Now I want to do the opposite, i.e., create a Para from a Vector (and assign the default values to the parameters not listed in optimizable_Para, the non-optimizable parameters).
Thus I am looking for a function convert_Vector_to_Para that achieves

function convert_Vector_to_Para(x)
    Para(a = x[1], b = x[2])
end

But, instead of explicitly writing the assignments (a = x[1], b = x[2]), I would like to use the symbols in oPara. So that if I change oPara in the future, I don’t have to rewrite convert_Vector_to_Para. I have not been able to figure how to do that so far… Please help?

My guess is I need a macro, but the closest I have been is via a function which does not work:

function convert_Vector_to_Para(x)
    eval(Expr(:call, :Para, [Expr(:(=), oPara[i], x[i]) for i in 1:length(optimizable_Para)]...))
end

#2
function Para(obj::AbstractVector,
              overwrite::AbstractVector{<:Symbol})
    fn = fieldnames(Para)
    default = Para()
    T = eltype(obj)
    return Para(NamedTuple{fn}(
                    name in overwrite ?
                        obj[idx] :
                        T(getfield(default, name)) for
                    (idx, name) in enumerate(fn))...)
end

#3

Flatten.jl was made entirely to do this! Even for nested structs.

Like:

using Flatten
import Flatten: flattenable

@flattenable @with_kw struct Para{T}
    a::T = 1.0 | true
    b::T = 2.0 | true
    c::T = 3.0 | false
end

julia> para = Para()
Para{Float64}
  a: Float64 1.0
  b: Float64 2.0
  c: Float64 3.0


julia> fieldnameflatten(para)
(:a, :b)

julia> data = flatten(Vector,para)
2-element Array{Float64,1}:
 1.0
 2.0

julia> data[2] = 5.0
5.0

julia> Flatten.reconstruct(para, data)
Para{Float64}
  a: Float64 1.0
  b: Float64 5.0
  c: Float64 3.0

It becomes increasingly useful the larger and more complicated the struct, and it generates pretty fast code.

To use the fieldnames in the array, make an AxisArray

data = flatten(Vector,para)
names = fieldnameflatten(Vector, para)
a = AxisArray(data, Axis{:parameters}(names))                                                                                                                                                                                             

julia> a[:b]                                                                                                               
2.0                

#4

Make it a subtype of FieldVector from StaticArrays.jl


#5

That’s the simplest way, but he needs to exclude fields from the vector and rebuild ignoring those excluded fields


#6

Thank you very much, this is even better than what I was looking for! I’ll try it right away!


#7

@Raf It seems Flatten.jl (and Tags.jl) are not in the reigistry. Is that normal?


#8

That’s in the process of happening right now! except some people thought Tags.jl wasn’t a clear enough name (after I already renamed it from MetaFields.jl), I’m considering FieldTags.jl or FieldMetadata.jl, if you have any opinions now is the time!

So for now use add "https://github.com/rafaqz/Tags.jl" and add "https://github.com/rafaqz/Flatten.jl" but also expect some changes in the coming month.

Any feedback or feature requests you have would be really useful too, I’m not sure how people will actually use these packages.


#9

I’m considering FieldTags.jl or FieldMetadata.jl, if you have any opinions now is the time!

My uneducated vote would go to FieldMetadata.jl. It is longer but I think it is more descriptive. I’ll got there to ask things about it :slight_smile: