-
Notifications
You must be signed in to change notification settings - Fork 6
Aleatoric Uncertainty with noise parameter sigma #284
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: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -25,6 +25,25 @@ struct ExtraLoss <: LossSpec | |
| f::Union{Function, Nothing} | ||
| end | ||
|
|
||
| """ | ||
| ParamLoss(f) | ||
|
|
||
| Wrapper for a *full-context* training loss with signature | ||
| `f(ŷ, y, y_nan, ps, targets, parameters)`. Unlike a classic custom loss | ||
| `f(ŷ_masked, y_masked)`, it receives the full (unmasked) predictions `ŷ`, targets | ||
| `y`, NaN masks `y_nan`, the raw parameters `ps`, the `targets` names, and the model | ||
| `parameters` (i.e. `ŷ.parameters`: NN-predicted, global and fixed values), and must | ||
| return a scalar loss. | ||
|
|
||
| Users normally do not construct this directly: passing a function with this | ||
| 6-argument signature to `training_loss` auto-detects it (see [`_accepts_params`](@ref)). | ||
| It is the natural choice when the loss needs a learned quantity such as a noise | ||
| scale `parameters.sigma`, e.g. a Gaussian negative log-likelihood. | ||
| """ | ||
| struct ParamLoss <: LossSpec | ||
| f::Function | ||
| end | ||
|
|
||
| """ | ||
| PerTarget(losses) | ||
|
|
||
|
|
@@ -111,9 +130,21 @@ end | |
|
|
||
|
|
||
| _to_loss_spec(s::Symbol) = SymbolicLoss(s) | ||
| _to_loss_spec(f::Function) = FunctionLoss(f) | ||
| _to_loss_spec(f::Function) = _accepts_params(f) ? ParamLoss(f) : FunctionLoss(f) | ||
| _to_loss_spec(ls::LossSpec) = ls | ||
|
|
||
| """ | ||
| _accepts_params(f) -> Bool | ||
|
|
||
| Auto-detect whether a custom loss `f` uses the full-context signature | ||
| `f(ŷ, y, y_nan, ps, targets, parameters)` (6 positional args) instead of the | ||
| classic masked signature `f(ŷ_masked, y_masked)` (2 positional args). Returns | ||
| `true` only when `f` has a 6-argument method and no 2-argument method, so existing | ||
| 2-arg losses keep their behavior. Functions that are ambiguous (e.g. varargs | ||
| matching both arities) fall back to the classic 2-arg path. | ||
| """ | ||
| _accepts_params(f) = hasmethod(f, NTuple{6, Any}) && !hasmethod(f, NTuple{2, Any}) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using A more robust approach that supports type-annotated arguments is to inspect the methods of _accepts_params(f) = any(m -> length(Base.unwrap_unionall(m.sig).parameters) == 7, methods(f)) &&
!any(m -> length(Base.unwrap_unionall(m.sig).parameters) == 3, methods(f))_accepts_params(f) = any(m -> length(Base.unwrap_unionall(m.sig).parameters) == 7, methods(f)) && !any(m -> length(Base.unwrap_unionall(m.sig).parameters) == 3, methods(f)) |
||
|
|
||
| _to_loss_spec(t::Tuple{<:Function, <:Tuple}) = ParameterizedLoss(t[1], t[2]) | ||
| _to_loss_spec(t::Tuple{<:Function, <:NamedTuple}) = ParameterizedLoss(t[1], (), t[2]) | ||
| _to_loss_spec(t::Tuple{<:Function, <:Tuple, <:NamedTuple}) = ParameterizedLoss(t[1], t[2], t[3]) | ||
|
|
@@ -137,6 +168,7 @@ loss_spec(ls::ParameterizedLoss) = | |
| (ls.f, ls.args, ls.kwargs) | ||
|
|
||
| loss_spec(el::ExtraLoss) = el.f | ||
| loss_spec(pl::ParamLoss) = pl.f | ||
| loss_spec(pt::PerTarget) = PerTarget(map(loss_spec, pt.losses)) | ||
|
|
||
| loss_types(logging::LoggingLoss) = map(loss_spec, logging.loss_types) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -437,10 +437,17 @@ function (m::HybridModel)(ds_k::Tuple, ps, st) | |
| forcing_data = ds_k[2] | ||
| all_kwargs = merge(forcing_data, all_params) | ||
|
|
||
| # 6) Apply mechanistic model | ||
| y_pred = m.mechanistic_model(; all_kwargs...) | ||
|
|
||
| out = (; y_pred..., parameters = all_params, out_extra...) | ||
| # 6) Apply mechanistic model. Only forward the kwargs it actually declares, so | ||
| # "loss-only" parameters (e.g. a learned noise scale used only in the loss) | ||
| # can be defined without the mechanistic model having to accept them. They | ||
| # still live in `all_params` and are exposed below under `parameters`. | ||
| y_pred = m.mechanistic_model(; _mechanistic_kwargs(m.mechanistic_model, all_kwargs)...) | ||
|
|
||
| # Parameters the mechanistic model does not consume (e.g. loss-only ones such as | ||
| # a learned noise scale) are surfaced at the top level so they can be monitored | ||
| # and plotted, in addition to always being available under `parameters`. | ||
| extra_params = _extra_params(m.mechanistic_model, all_params) | ||
| out = (; y_pred..., extra_params..., parameters = all_params, out_extra...) | ||
| st_new = (; st_new_nns..., fixed = st.fixed) | ||
|
|
||
| return out, st_new | ||
|
|
@@ -451,6 +458,52 @@ function (m::HybridModel)(ds_k, ps, st) | |
| return m(Tuple(ds_k), ps, st) | ||
| end | ||
|
|
||
| """ | ||
| _mechanistic_kwargs(f, all_kwargs::NamedTuple) | ||
|
|
||
| Select from `all_kwargs` only the keyword arguments the mechanistic model `f` | ||
| declares, so parameters used solely by the loss (e.g. a learned noise scale) do | ||
| not need to be accepted by `f`. Falls back to passing everything when `f` slurps | ||
| `kwargs...` or its keyword signature cannot be introspected. | ||
| """ | ||
| function _mechanistic_kwargs(f, all_kwargs::NamedTuple) | ||
| keep = ChainRulesCore.ignore_derivatives() do | ||
| _accepted_kwarg_names(f, keys(all_kwargs)) | ||
| end | ||
| keep === nothing && return all_kwargs | ||
| return NamedTuple{keep}(map(k -> all_kwargs[k], keep)) | ||
| end | ||
|
|
||
| """ | ||
| _extra_params(f, all_params::NamedTuple) | ||
|
|
||
| The parameters not consumed by the mechanistic model `f` (e.g. loss-only ones such | ||
| as a learned noise scale). They are surfaced at the top level of the model output | ||
| so they can be monitored/plotted, in addition to always being available under | ||
| `parameters`. Returns an empty `NamedTuple` when `f` consumes everything. | ||
| """ | ||
| function _extra_params(f, all_params::NamedTuple) | ||
| keep = ChainRulesCore.ignore_derivatives() do | ||
| acc = _accepted_kwarg_names(f, keys(all_params)) | ||
| acc === nothing ? () : Tuple(k for k in keys(all_params) if !(k in acc)) | ||
| end | ||
| return NamedTuple{keep}(map(k -> all_params[k], keep)) | ||
| end | ||
|
|
||
| # Returns the tuple of `all_kwargs` names accepted by `f`, or `nothing` to signal | ||
| # "pass everything" (the model slurps `kwargs...`, or has no introspectable kwargs). | ||
| function _accepted_kwarg_names(f, available::Tuple) | ||
| names = Symbol[] | ||
| for mth in methods(f) | ||
| for d in Base.kwarg_decl(mth) | ||
| endswith(string(d), "...") && return nothing # slurps kwargs → keep all | ||
| push!(names, d) | ||
| end | ||
|
Comment on lines
+498
to
+501
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Julia, converting a for d in Base.kwarg_decl(mth)
endswith(string(d), "...") && return nothing # slurps kwargs → keep all
push!(names, d)
end |
||
| end | ||
| isempty(names) && return nothing | ||
| return Tuple(k for k in available if k in names) | ||
| end | ||
|
|
||
| function (m::HybridModel)(df::DataFrame, ps, st) | ||
| @warn "Only makes sense in test mode, not training!" | ||
|
|
||
|
|
||
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.
Using
get(ŷ, :parameters, (;))can throw aMethodErrorifŷis not a standardNamedTuple(for example, if it is aComponentArrayor another custom container that does not supportgetwith aSymbolkey).Using
haspropertyand property access is much more robust and widely supported across different container types (includingComponentArray):