{"id":19120463,"url":"https://github.com/juliagaussianprocesses/parameterhandling.jl","last_synced_at":"2025-10-16T23:34:33.138Z","repository":{"id":39874747,"uuid":"289302752","full_name":"JuliaGaussianProcesses/ParameterHandling.jl","owner":"JuliaGaussianProcesses","description":"Foundational tooling for handling collections of parameters in models","archived":false,"fork":false,"pushed_at":"2024-02-26T16:03:53.000Z","size":373,"stargazers_count":73,"open_issues_count":19,"forks_count":11,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-02-20T22:37:00.264Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Julia","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JuliaGaussianProcesses.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2020-08-21T15:26:48.000Z","updated_at":"2024-12-19T13:49:11.000Z","dependencies_parsed_at":"2024-02-10T13:25:55.107Z","dependency_job_id":"696e589a-614b-4361-8e01-642b15c402ce","html_url":"https://github.com/JuliaGaussianProcesses/ParameterHandling.jl","commit_stats":null,"previous_names":["invenia/parameterhandling.jl"],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JuliaGaussianProcesses%2FParameterHandling.jl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JuliaGaussianProcesses%2FParameterHandling.jl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JuliaGaussianProcesses%2FParameterHandling.jl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JuliaGaussianProcesses%2FParameterHandling.jl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JuliaGaussianProcesses","download_url":"https://codeload.github.com/JuliaGaussianProcesses/ParameterHandling.jl/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239927821,"owners_count":19719835,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-09T05:14:11.967Z","updated_at":"2025-10-16T23:34:28.121Z","avatar_url":"https://github.com/JuliaGaussianProcesses.png","language":"Julia","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ParameterHandling\n\n[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaGaussianProcesses.github.io/ParameterHandling.jl/stable)\n[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaGaussianProcesses.github.io/ParameterHandling.jl/dev)\n[![CI](https://github.com/JuliaGaussianProcesses/ParameterHandling.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/JuliaGaussianProcesses/ParameterHandling.jl/actions/workflows/CI.yml)\n[![Codecov](https://codecov.io/gh/JuliaGaussianProcesses/ParameterHandling.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaGaussianProcesses/ParameterHandling.jl)\n[![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac)\n\nParameterHandling.jl is an experiment in handling constrained tunable parameters of models.\n\n\n\n\n\n# The Parameter Handling Problem\n\nConsider the following common situation: you have a function `build_model` that maps a\ncollection of parameters `θ` to a `model` of some kind:\n```julia\nmodel = build_model(θ)\n```\nThe `model` might, for example, be a function that maps some input `x` to some sort of\nprediction `y`:\n```julia\ny = model(x)\n```\nwhere `x` and `y` could essentially be anything that you like.\nYou might also wish to somehow \"learn\" or \"tune\" or \"infer\" the parameters `θ` by plugging\n`build_model` into some other function, lets call it `learn`, that tries out various\ndifferent parameter values in some clever way and determines which ones are good -- think\nloss minimisation / objective maximisation, (approximate) Bayesian inference, etc.\nWe'll not worry about exactly what procedure `learn` employs to try out a number of\ndifferent parameter values, but suppose that `learn` has the interface:\n```julia\nlearned_θ = learn(build_model, initial_θ)\n```\n\nSo far so good, but now consider how one actually goes about writing `build_model`.\nThere are more or less two things that must be written:\n\n1. `θ` must be in a format that `learn` knows how to handle. A popular approach is to\n    require that `θ` be a `Vector` of `Real` numbers -- or, rather, some concrete subtype of\n    `Real`.\n1. The code required to turn `θ` into `model` inside `build_model` mustn't be too onerous to\n\twrite, read, or modify.\n\nWhile the first point is fairly straightforward, the second point is a bit subtle, so it's\nworth dwelling on it a little.\n\nFor the sake of concreteness, let's suppose that we adopt the convention that `θ` is a\n`Vector{Float64}`. In the case of linear regression, we might assume that `θ` comprises\na length `D` \"weight vector\" `w`, and a scalar \"bias\" `b`. So the function to build the\nmodel might be something like\n\n```julia\nfunction build_model(θ::Vector{Float64})\n    return x -\u003e dot(θ[1:end-1], x) + θ[end]\nend\n```\n\nThe easiest way to see that this is a less than ideal solution is to consider what this\nfunction would look like if `θ` was, say, a `NamedTuple` with fields `w` and `b`:\n```julia\nfunction build_model(θ::NamedTuple)\n    return x -\u003e dot(θ.w, x) + θ.b\nend\n```\nThis version of the function is much easier to read -- moreover if you want to inspect the\nvalues of `w` and `b` at some other point in time, you don't need to know precisely how to\nchop up the vector.\n\nMoreover it seems probable that the latter approach is less\nbug-prone -- suppose for some reason one refactored the code so that the first element of\n`θ` became `b` and the last `D` elements `w`; any code that depended upon the original\nordering will now be incorrect and likely fail silently. The `NamedTuple` approach simply\ndoesn't have this issue.\n\nGranted, in this simple case it's not too much of a problem, but it's easy to find\nsituations in which things become considerably more difficult. For example, suppose that we\ninstead had pretty much any kind of neural network, Gaussian process, ODE, or really just\nany model with more than a couple of distinct parameters. From the perspective of\nwriting complicated models, implementing things in terms of a single vector of\nparameters that is _manually_ chopped up is an _extremely_ bad design choice. It simply\ndoesn't scale.\n\nHowever, a single vector of e.g. `Float64`s _is_ extremely convenient when writing general\npurpose optimisers / approximate inference routines --\n[Optim.jl](https://github.com/JuliaNLSolvers/Optim.jl) and\n[AdvancedHMC.jl](https://github.com/TuringLang/AdvancedHMC.jl) being two obvious examples.\n\n\n\n\n\n# The ParameterHandling.jl Approach\n\n`ParameterHandling.jl` aims to give you the best of both worlds by providing the tools\nrequired to automate the transformation between a \"structured\" representation (e.g. nested\n`NamedTuple` / `Dict` etc) and a \"flattened\" (e.g. `Vector{Float64}`) of your model\nparameters.\n\nThe function `flatten` eats a structured representation of some parameters, returning the\nflattened representation _and_ a function that converts the flattened thing back into its\nstructured representation.\n\n`flatten` is implemented recursively, with a _very_ small number of base-implementations\nthat don't themselves call `flatten`.\n\nYou should expect to occassionally have to extend `flatten` to handle your own types and, if\nyou wind up doing this for a function in `Base` that this package doesn't yet cover, a PR\nincluding that implementation will be very welcome.\n\nSee `test/parameters.jl` for a couple of examples that utilise `flatten` to do something\nsimilar to the task described above.\n\n\n\n\n\n# Dealing with Constrained Parameters\n\nIt is very common to need to handle constraints on parameters e.g. it may be necessary for a\nparticular scalar to always be positive. While `flatten` is great for changing between\nrepresentations of your parameters, it doesn't really have anything to say about this\nconstraint problem.\n\nFor this we introduce a collection of new `AbstractParameter` types (whether we really need\nthem to have some mutual supertype is unclear at present) that play nicely with `flatten`\nand allow one to specify that e.g. a particular scalar must remain positive, or should be\nfixed across iterations. See `src/parameters.jl` and `test/parameters.jl` for more examples.\n\nThe approach to implementing these types typically revolves around some kind of `Deferred` /\ndelayed computation. For example, a `Positive` parameter is represented by an\n\"unconstrained\" number, and a \"transform\" that maps from the entire real line to the\npositive half. The `value` of a `Positive` is given by the application of this transform to\nthe unconstrained number. `flatten`ing a `Positive` yields a length-1 vector containing the\n_unconstrained_ number, rather than the value represented by the `Positive` object. For\nexample\n\n```julia\njulia\u003e using ParameterHandling\n\njulia\u003e x_constrained = 1.0 # Specify constrained value.\n1.0\n\njulia\u003e x = positive(x_constrained) # Construct a number that should remain positive.\nParameterHandling.Positive{Float64, typeof(exp), Float64}(-1.490116130486996e-8, exp, 1.4901161193847656e-8)\n\njulia\u003e ParameterHandling.value(x) # Get the constrained value by applying the transform.\n1.0\n\njulia\u003e v, unflatten = flatten(x); # Supports the `flatten` interface.\n\njulia\u003e v\n1-element Vector{Float64}:\n -1.490116130486996e-8\n\njulia\u003e new_v = randn(1) # Pick a random new value.\n1-element Vector{Float64}:\n 2.3482666974328716\n\njulia\u003e ParameterHandling.value(unflatten(new_v)) # Obtain constrained value.\n10.467410816707215\n```\n\nWe also provide the utility function `value_flatten` which returns an unflattening function\nequivalent to `value(unflatten(v))`. The above could then be implemented as\n```julia\njulia\u003e v, unflatten = value_flatten(x);\n\njulia\u003e unflatten(v)\n1.0\n```\n\nIt is straightforward to implement your own parameters that interoperate with those already\nwritten by implementing `value` and `flatten` for them. You might want to do this if this\npackage doesn't currently support the functionality that you need.\n\n\n\n\n# A Worked Example\n\n\n\nWe use a model involving a Gaussian process (GP) -- you don't need to know anything about\nGaussian processes other than\n1. they are a class of probabilistic model which can be used for regression (amongst other things).\n2. they have some tunable parameters that are usually chosen by optimising a scalar objective function using an iterative\noptimisation algorithm -- typically a variant of gradient descent.\nThs is representative of a large number of models in ML / statistics / optimisation.\n\nThis example can be copy+pasted into a REPL session.\n\n```julia\n# Install some packages.\nusing Pkg\nPkg.add(\"ParameterHandling\")\nPkg.add(\"Optim\")\nPkg.add(\"Zygote\")\nPkg.add(\"AbstractGPs\")\n\nusing ParameterHandling # load up this package.\nusing Optim # generic optimisation\nusing Zygote # algorithmic differentiation\nusing AbstractGPs # package containing the models we'll be working with\n\n# Declare a NamedTuple containing an initial guess at parameters.\nraw_initial_params = (\n    k1 = (\n        var=positive(0.9),\n        precision=positive(1.0),\n    ),\n    k2 = (\n        var=positive(0.1),\n        precision=positive(0.3),\n    ),\n    noise_var=positive(0.2),\n)\n\n# Using ParameterHandling.value_flatten, we can obtain both a Vector{Float64} representation of\n# these parameters, and a mapping from that vector back to the original (unconstrained) parameter values.\nflat_initial_params, unflatten = ParameterHandling.value_flatten(raw_initial_params)\n\n# ParameterHandling.value strips out all of the Positive types in initial_params,\n# returning a plain named tuple of named tuples and Float64s.\njulia\u003e initial_params = ParameterHandling.value(raw_initial_params)\n(k1 = (var = 0.9, precision = 1.0), k2 = (var = 0.10000000000000002, precision = 0.30000000000000004), noise_var = 0.19999999999999998)\n\n# GP-specific functionality. Don't worry about the details, just\n# note the use of the structured representation of the parameters.\nfunction build_gp(params::NamedTuple)\n    k1 = params.k1.var * Matern52Kernel() ∘ ScaleTransform(params.k1.precision)\n    k2 = params.k2.var * SEKernel() ∘ ScaleTransform(params.k2.precision)\n    return GP(k1 + k2)\nend\n\n# Generate some synthetic training data.\n# Again, don't worry too much about the specifics here.\nconst x = range(-5.0, 5.0; length=100)\nconst y = rand(build_gp(initial_params)(x, initial_params.noise_var))\n\n# Specify an objective function in terms of x and y.\nfunction objective(params::NamedTuple)\n    f = build_gp(params)\n    return -logpdf(f(x, params.noise_var), y)\nend\n\n# Use Optim.jl to minimise the objective function w.r.t. the params.\n# The important thing here is to note that we're passing in the flat vector of parameters to\n# Optim, which is something that Optim knows how to work with, and using `unflatten` to convert\n# from this representation to the structured one that our objective function knows about\n# using `unflatten` -- we've used ParameterHandling to build a bridge between Optim and an\n# entirely unrelated package.\ntraining_results = Optim.optimize(\n    objective ∘ unflatten,\n    θ -\u003e only(Zygote.gradient(objective ∘ unflatten, θ)),\n    flat_initial_params,\n    BFGS(\n        alphaguess = Optim.LineSearches.InitialStatic(scaled=true),\n        linesearch = Optim.LineSearches.BackTracking(),\n    ),\n    Optim.Options(show_trace = true);\n    inplace=false,\n)\n\n# Extracting the final values of the parameters.\nfinal_params = unflatten(training_results.minimizer)\nf_trained = build_gp(final_params)\n```\n\nUsually you would go on to make some predictions on test data using `f_trained`, or\nsomething like that.\nFrom the perspective of ParameterHandling.jl, we've seen the interesting stuff though.\nIn particular, we've seen an example of how ParameterHandling.jl can be used to bridge the\ngap between the \"flat\" representation of parameters that `Optim` likes to work with, and the\n\"structured\" representation that it's convenient to write optimisation algorithms with.\n\n# Gotchas and Performance Tips\n\n1. `Integer`s typically don't take part in the kind of optimisation procedures that this package is designed to handle. Consequently, `flatten(::Integer)` produces an empty vector.\n2. `deferred` has some type-stability issues when used in conjunction with abstract types. For example, `flatten(deferred(Normal, 5.0, 4.0))` won't infer properly. A simple work around is to write a function `normal(args...) = Normal(args...)` and work with `deferred(normal, 5.0, 4.0)` instead.\n3. Let `x` be an `Array{\u003c:Real}`. If you wish to constrain each of its values to be positive, prefer `positive(x)` over `map(positive, x)` or `positive.(x)`. `positive(x)` has been implemented the associated `unflatten` function has good performance, particularly when interacting with `Zygote` (when `map(positive, x)` is extremely slow). The same thing applies to `bounded` values. Prefer `bounded(x, lb, ub)` to e.g. `bounded.(x, lb, ub)`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjuliagaussianprocesses%2Fparameterhandling.jl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjuliagaussianprocesses%2Fparameterhandling.jl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjuliagaussianprocesses%2Fparameterhandling.jl/lists"}