Skip to content

Commit 5cbe05b

Browse files
odunbarph-kev
andauthored
helper and docs for minibatching (#438)
* docs for minibatcher tools * add minibatch helpers into encoder_kwargs and get_training_points * improve name * fix test failures, and improve layout. Add dosctring * add note about noise sensitivity * Update docs/src/calibrate.md Co-authored-by: Kevin Phan <98072684+ph-kev@users.noreply.github.com> * Update docs/src/emulate.md Co-authored-by: Kevin Phan <98072684+ph-kev@users.noreply.github.com> * resolve comments * clarify n,N etc. * try adding Module * qualify module even more? * pdc --------- Co-authored-by: Kevin Phan <98072684+ph-kev@users.noreply.github.com>
1 parent 1df3acb commit 5cbe05b

4 files changed

Lines changed: 308 additions & 31 deletions

File tree

docs/src/calibrate.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,22 @@ Documentation on how to construct an EnsembleKalmanProcess from the computer mod
1919

2020
One draw of our approach is that it does not require the forward map to be written in Julia. To aid construction of such a workflow, EnsembleKalmanProcesses.jl provides a documented example of a BASH workflow for the [sinusoid problem](https://clima.github.io/EnsembleKalmanProcesses.jl/dev/examples/sinusoid_example_toml/), with source code [here](https://github.com/CliMA/EnsembleKalmanProcesses.jl/tree/main/examples/SinusoidInterface). The forward map interacts with the calibration tools (EKP) only though TOML file reading an writing, and thus can be written in any language; for example, to be used with [slurm HPC scripts](https://clima.github.io/EnsembleKalmanProcesses.jl/dev/examples/ClimateMachine_example/), with source code [here](https://github.com/CliMA/EnsembleKalmanProcesses.jl/tree/main/examples/ClimateMachine).
2121

22+
## Extracting training data after calibration
23+
24+
Two helper functions from `CalibrateEmulateSample.Utilities` simplify passing the calibration results to the emulator.
25+
26+
[`get_training_points(ekp, n; g_final = nothing)`](@ref CalibrateEmulateSample.Utilities.get_training_points) collects the stored parameter ensembles and forward-model outputs into a [`PairedDataContainer`](https://clima.github.io/EnsembleKalmanProcesses.jl/dev/internal_data_representation/) ready for emulator training. The argument `n` can be an integer (which uses iterations `1:n`) or an index vector. An optional keyword `g_final` accepts the forward-model outputs at the final, not-yet-stored parameter ensemble.
27+
28+
```julia
29+
using CalibrateEmulateSample.Utilities
30+
input_output_pairs = get_training_points(ekp, 5) # first 5 iterations
31+
input_output_pairs = get_training_points(ekp, 1:2:9) # odd iterations
32+
input_output_pairs = get_training_points(ekp, 5; g_final = G.(get_ϕ_final(prior, ekp))) # an additional iteration pairing (u_final, g_final)
33+
```
34+
35+
[`encoder_kwargs_from(ekp, prior)`](@ref CalibrateEmulateSample.Utilities.encoder_kwargs_from) collects additional information from the calibration — the prior covariance, the observational noise covariance, and the sequence of parameter and output sample distributions across EKP iterations — into a named tuple that can be passed directly as the `encoder_kwargs` argument of the `Emulator`. This enables data-adaptive preprocessing such as likelihood-informed subspace reduction.
36+
37+
```julia
38+
encoder_kwargs = encoder_kwargs_from(ekp, prior)
39+
```
40+

docs/src/emulate.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ First, obtain data in a `PairedDataContainer`, for example, get this from an `En
1111
using CalibrateEmulateSample.Utilities
1212
input_output_pairs = Utilities.get_training_points(ekpobj, 5) # use first 5 iterations as data
1313
```
14+
15+
!!! note "Minibatched calibration"
16+
The `EnsembleKalmanProcess` can be built with an `ObservationSeries` of `N` samples and a minibatcher with `n` batches. In this case, each iteration fits the ensemble to one of the batches ``B_1, \ldots, B_n`` (``n \leq N``) that form a disjoint partition of statistically similar observations ``y_1, \ldots, y_N \sim \rho``. The batching can aid the calibration stage but does not affect the emulate-sample stages.
17+
18+
Internally `get_training_points` and `encoder_kwargs_from` reduce outputs/noise from the `EnsembleKalmanProcess` to their per-observation components, thus the emulator is trained on ``(\theta, \mathcal{G}(\theta))`` pairs representative of the distribution ``\rho``. In the Sampling stage the full observation set ``y_1, \ldots, y_N \sim \rho`` is used and batch structure is ignored.
19+
20+
Warning: using training data with several `(identical-input)->(different-output)` pairs, will induce more sensitivity on the `obs_noise_cov`. It is recommended to ensure it is well estimated (or well-regularized with white noise) to help training; one can also use the `noise_learn=true` flag but this is known to cause slow-downs in training.
21+
1422
Wrapping a predefined machine learning tool, e.g. a Gaussian process `gauss_proc`, the `Emulator` can then be built:
1523

1624
```julia

src/Utilities.jl

Lines changed: 163 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ using TSVD
1717
import LinearAlgebra: norm
1818

1919
export get_training_points
20+
export get_flat_pairs
2021
export create_compact_linear_map
2122
export PairedDataContainerProcessor, DataContainerProcessor
2223
export create_encoder_schedule,
@@ -54,6 +55,26 @@ Extract and flatten the training data from an `EnsembleKalmanProcess` into a
5455
- `train_iterations`: integer `n` (uses iterations `1:n`) or an index vector such as `3:2:9`.
5556
- `g_final` (keyword, default `nothing`): optional `AbstractMatrix` of forward-model outputs
5657
for the final parameter ensemble (not yet stored in `ekp`), sized as `get_g(ekp, 1)`.
58+
59+
To ensure consistency in `g_final`, we recommend that, given the `ekp::EnsembleKalmanProcess`
60+
and `prior::ParameterDistribution` used to generate it, the user evaluates the forward map `G`
61+
on the parameters returned by `get_ϕ_final`:
62+
63+
```julia
64+
params = get_ϕ_final(prior, ekp)
65+
g_final = reduce(hcat, [G(param) for param in eachcol(params)])
66+
```
67+
68+
If `ekp` is using minibatching, `G` must be evaluated on the same minibatch that `ekp` is
69+
currently on:
70+
71+
```julia
72+
params = get_ϕ_final(prior, ekp)
73+
batch_final = get_current_minibatch(ekp)
74+
g_final = reduce(hcat, [G(param, batch_final) for param in eachcol(params)])
75+
```
76+
77+
See `EnsembleKalmanProcesses.Observations` for more on minibatching and `ObservationSeries`.
5778
"""
5879
function get_training_points(
5980
ekp::EKP.EnsembleKalmanProcess{FT, IT, P},
@@ -78,36 +99,15 @@ function get_training_points(
7899
)
79100
end
80101

81-
u_tp = []
82-
g_tp = []
83-
102+
os = get_observation_series(ekp)
84103
g_len = length(get_g(ekp))
85-
g_full = [get_g(ekp, i) for i in iter_range[iter_range .<= g_len]]
86-
if !isnothing(g_final)
87-
if (isa(g_final, AbstractMatrix)) # add the matrix
88-
if !(size(g_full[1]) == size(g_final))
89-
throw(ArgumentError("Expected `g_final` size: $(size(g_full[1])), received $(size(g_final))."))
90-
end
91-
push!(g_full, g_final)
92-
else
93-
throw(
94-
ArgumentError(
95-
"Expected `g_final` to be type `<:AbstractMatrix`, of size: $(size(g_full[1])), received $(typeof(g_final)).",
96-
),
97-
)
98-
end
99-
end
100-
101-
for (idx, ir) in enumerate(iter_range)
102-
push!(u_tp, get_u(ekp, ir)) #N_parameters x N_ens
103-
push!(g_tp, g_full[idx]) #N_data x N_ens
104-
end
105-
u_tp = reduce(hcat, u_tp) # N_parameters x (N_ek_it x N_ensemble)]
106-
g_tp = reduce(hcat, g_tp) # N_data x (N_ek_it x N_ensemble)
104+
paired_range = [ir for ir in iter_range if ir <= g_len]
107105

108-
training_points = PairedDataContainer(u_tp, g_tp, data_are_columns = true)
106+
# get_flat_pairs handles any batch size, including 1 (no-op for batch_size=1)
107+
# final_samples_out appends the final ensemble output in per-element or batched form
108+
u_tp, g_tp = get_flat_pairs(ekp, paired_range; final_samples_out = g_final)
109109

110-
return training_points
110+
return PairedDataContainer(u_tp, g_tp, data_are_columns = true)
111111
end
112112

113113
# Using Observation Objects:
@@ -162,6 +162,121 @@ end
162162
"""
163163
$(TYPEDSIGNATURES)
164164
165+
Flatten the paired parameter and forward-model output ensembles from a minibatched
166+
`EnsembleKalmanProcess` into individual `(parameter, single-observation)` column pairs.
167+
168+
In each stored EKP iteration the output matrix `g` has `batch_size × gdim` rows, where
169+
`gdim` is the single-observation dimension. This function splits each `g` into
170+
`batch_size` blocks of `gdim` rows and replicates the corresponding parameter ensemble
171+
so that each batch element becomes an independent `(u, g)` training pair. For
172+
non-minibatched runs (`batch_size = 1`) the function is a no-op.
173+
174+
# Arguments
175+
176+
- `ekp`: `EnsembleKalmanProcess` holding the parameter ensembles and forward-model outputs.
177+
- `iter_range`: iterations to include — an integer `n` is interpreted as `1:n`
178+
(clamped to the number of stored outputs); a vector selects specific iterations,
179+
which must be a subset of `1:length(get_g(ekp))`.
180+
- `include_dt` (keyword, default `false`): when `true`, also return `dt_flat`, a
181+
`Vector{Float64}` of per-pair algorithm times (`0` for iteration 1, then
182+
`get_algorithm_time(ekp)[ir - 1]` repeated `batch_size` times per iteration).
183+
- `final_samples_out` (keyword, default `nothing`): optional `AbstractMatrix` of
184+
forward-model outputs for the final parameter ensemble `get_u_final(ekp)` (not yet
185+
stored in `ekp`). Must be stacked as `[g_b1; g_b2; …]` matching the minibatch
186+
structure of the next observation-series cycle.
187+
188+
# Returns
189+
190+
- `(u_flat, g_flat)` — matrices of size `dim_par × (Σ_k bs_k × N_ens)` and
191+
`gdim × (Σ_k bs_k × N_ens)`, with parameters repeated once per batch element.
192+
- `(u_flat, g_flat, dt_flat)` — as above, plus the algorithm-time vector, when
193+
`include_dt = true`.
194+
"""
195+
function get_flat_pairs(
196+
ekp::EKP.EnsembleKalmanProcess{FT, IT, P},
197+
iter_range::Union{IT, AbstractVector{IT}};
198+
include_dt::Bool = false,
199+
final_samples_out = nothing,
200+
) where {FT, IT, P}
201+
if !isa(iter_range, AbstractVector)
202+
iter_range = 1:min(iter_range, length(get_g(ekp)))
203+
end
204+
!isnothing(final_samples_out) &&
205+
!isa(final_samples_out, AbstractMatrix) &&
206+
throw(
207+
ArgumentError(
208+
"Expected `final_samples_out` to be an `AbstractMatrix`, received $(typeof(final_samples_out)).",
209+
),
210+
)
211+
212+
os = get_observation_series(ekp)
213+
g_len = length(get_g(ekp))
214+
alg_time = include_dt ? get_algorithm_time(ekp) : nothing
215+
216+
u_out = []
217+
g_out = []
218+
dt_out = include_dt ? Float64[] : nothing
219+
220+
# if a final output is provided, treat it as one extra iteration beyond g_len
221+
full_range = isnothing(final_samples_out) ? iter_range : [collect(iter_range); g_len + 1]
222+
223+
for ir in full_range
224+
if ir > g_len
225+
# extra iteration: use the caller-supplied output instead of get_g
226+
isnothing(final_samples_out) && throw(
227+
ArgumentError(
228+
"iter_range contains iteration $ir, but EKP only stores $g_len " *
229+
"outputs. iter_range must be a subset of 1:length(get_g(ekp)).",
230+
),
231+
)
232+
gg = final_samples_out
233+
else
234+
gg = get_g(ekp, ir)
235+
end
236+
uu = get_u(ekp, ir)
237+
batch = get_minibatch(os, ir)
238+
bs = length(batch)
239+
240+
ndim = size(gg, 1)
241+
ndim % bs == 0 || throw(
242+
ArgumentError(
243+
"At iteration $ir: output dimension ($ndim) is not divisible by batch " *
244+
"size ($bs). Ensure outputs are stacked as [g_b1; g_b2; …] for each " *
245+
"batch element.",
246+
),
247+
)
248+
gdim = ndim ÷ bs
249+
if ir > g_len && !isempty(g_out) && gdim != size(g_out[1], 1)
250+
throw(
251+
ArgumentError(
252+
"final_samples_out has per-element output dimension $gdim, " *
253+
"but existing outputs have dimension $(size(g_out[1], 1)).",
254+
),
255+
)
256+
end
257+
258+
for b in 1:bs
259+
idx = (gdim * (b - 1) + 1):(gdim * b)
260+
push!(u_out, uu)
261+
push!(g_out, gg[idx, :])
262+
end
263+
264+
if include_dt
265+
di = (ir == 1) ? zero(eltype(alg_time)) : alg_time[ir - 1]
266+
append!(dt_out, fill(di, bs))
267+
end
268+
end
269+
270+
if include_dt
271+
return reduce(hcat, u_out), reduce(hcat, g_out), dt_out
272+
else
273+
return reduce(hcat, u_out), reduce(hcat, g_out)
274+
end
275+
end
276+
277+
"""
278+
$(TYPEDSIGNATURES)
279+
165280
Extracts the relevant encoder kwargs from a vector triple (samples_in, samples_out, dt). Samples describe an ordered sequence of distributions in input and output space, each indexed with a temperature, or algorithm time, `dt`.
166281
167282
Contains
@@ -216,19 +331,36 @@ function encoder_kwargs_from(
216331
final_samples_out = nothing,
217332
) where {PD <: ParameterDistribution, EKP <: EnsembleKalmanProcess}
218333
observation_series = isnothing(observation_series) ? get_observation_series(ekp) : observation_series
334+
auto_flatten_io = isnothing(samples_in) && isnothing(samples_out)
219335
samples_in = isnothing(samples_in) ? get_u(ekp) : samples_in
220336
samples_out = isnothing(samples_out) ? get_g(ekp) : samples_out
221337
dt = isnothing(dt) ? get_algorithm_time(ekp) : dt
222-
# a common setting is you have one-less samples_out than (samples_in, dt) so we extend g
223-
if !isnothing(final_samples_out)
224-
if (isa(final_samples_out, AbstractMatrix)) # add one matrix
338+
339+
orig_g_len = length(get_g(ekp))
340+
is_minibatched =
341+
auto_flatten_io &&
342+
orig_g_len > 0 &&
343+
any(length(get_minibatch(observation_series, ir)) > 1 for ir in 1:orig_g_len)
344+
345+
if is_minibatched
346+
# delegate all splitting (including final_samples_out) to get_flat_pairs
347+
u_flat, g_flat, flat_dt =
348+
get_flat_pairs(ekp, 1:orig_g_len; include_dt = true, final_samples_out = final_samples_out)
349+
n_ens_per = size(get_u(ekp, 1), 2)
350+
n_pairs = size(u_flat, 2) ÷ n_ens_per
351+
samples_in = [u_flat[:, ((k - 1) * n_ens_per + 1):(k * n_ens_per)] for k in 1:n_pairs]
352+
samples_out = [g_flat[:, ((k - 1) * n_ens_per + 1):(k * n_ens_per)] for k in 1:n_pairs]
353+
dt = flat_dt
354+
elseif !isnothing(final_samples_out)
355+
# non-minibatch path: append final_samples_out directly
356+
if isa(final_samples_out, AbstractMatrix)
225357
if length(samples_out) > 0
226358
size(samples_out[1]) == size(final_samples_out) || error(
227359
"size of final_samples_out ($(size(final_samples_out))) does not match existing samples_out entries ($(size(samples_out[1])))",
228360
)
229361
end
230362
push!(samples_out, final_samples_out)
231-
else # add a vector of matrices
363+
else
232364
if length(samples_out) > 0
233365
all(size(samples_out[1]) == size(so) for so in final_samples_out) || error(
234366
"not all final_samples_out entries have size $(size(samples_out[1])) to match existing samples_out",

0 commit comments

Comments
 (0)