Skip to content

Commit 29beb8b

Browse files
authored
[Updated] Likelihood-informed processors (#410)
* first port from PR 376 typos add pkgs encoder_kwargs in nice form remove dim criterion for now docstring API refinement add example builds encoder, but mcmc bug examples run, bugs for dimensionality and product order resolved noise injection reduced add some get_encoded_dim functions add consistency for both in-and-out dimensions for li, when using multiple distributions runs through with output reduction, logic simplified for now * rm linlinexp * truncate based on relative log^2(1+li) not li itself * add docs manifest * remove data-as-samples (make default behaviour), first tests * format * add encoder_kwargs_from test * typo * typo * logic typo * increase shaky tol * refactor private/public encode/decode functions * format * new docs manifest * more coverage * more coverage * add cases * rm trap rule switch for now * format * test pipeline gets better kwargs * recreate manifest for docs and julia 1.11.9 * add coverage * add apply_to for paired_data_container * typo * format * nicer table, and bugfix ref with deepcopy * format * docs update * refine example setup for LI 1:5 in 1:1 out * add ref in as comparison to true parameters in example * remove extra message * docs improvement * codecov * updates from review * codecov, and a bugfix for get_training_points * format
1 parent 6341ca2 commit 29beb8b

15 files changed

Lines changed: 1706 additions & 475 deletions

File tree

Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1919
LinearMaps = "7a12625a-238d-50fd-b39a-03d52299707e"
2020
LowRankApprox = "898213cb-b102-5a47-900c-97e73b919f73"
2121
MCMCChains = "c7f686f2-ff18-58e9-bc7b-31028e88f75d"
22+
Manifolds = "1cead3c2-87b3-11e9-0ccd-23c62b72b94e"
23+
Manopt = "0fc0a36d-df90-57f3-8f93-d78a9fc72bb5"
2224
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
2325
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
2426
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
@@ -47,6 +49,8 @@ KernelFunctions = "0.10.64, 0.11"
4749
LinearMaps = "3.11.4"
4850
LowRankApprox = "0.5.5"
4951
MCMCChains = "7"
52+
Manifolds = "0.11.19"
53+
Manopt = "0.5.34"
5054
Plots = "1.41.1"
5155
Printf = "1"
5256
ProgressBars = "1"

docs/Manifest.toml

Lines changed: 495 additions & 257 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/src/data_processing.md

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,72 @@ The framework works as follows
1212
An external API is also available using the `encode_data`, and `encode_structure_matrix` methods if needed to encode new data, or new covariance-type matrices.
1313

1414
## Defaults and recommendations
15-
### Default schedule (no explicit dimension reduction)
16-
When no schedule is provided, i.e.
15+
16+
### Recommended schedule: ("latest and greatest")
17+
18+
Given `ekp` and `prior` from an ensemble kalman process run (e.g., the "Calibrate" stage). Assume we have chosen our machine learning tool and obtained training data. Our recommendeded family to balance efficiency and reduce dimension is to use the `retain_var` and `retain_info`.
19+
1720
```julia
18-
emulator = Emulator(machine_learning_tool, input_output_pairs)
21+
retain_var = 0.999 # reduce dimension retaining 99.9% of variance (for PCA)
22+
retain_info = 0.999 # reduce dimension retaining 99.9% of information (for likelihood-informed)
23+
ekp_it = 5 # number of eki iteration (for likelihood-informed)
24+
25+
encoder_schedule = [
26+
(decorrelate_sample_cov(retain_var = retain_var), "in"),
27+
(decorrelate_structure_mat(retain_var = retain_var), "out"),
28+
(likelihood_informed(retain_info=retain_info, iters = 1:ekp_it), "in"),
29+
(likelihood_informed(retain_info=retain_info, iters = 1:1), "out"),
30+
]
1931
```
20-
The default schedule under-the-hood, is given by
32+
The motivation is that PCA is an efficient, but blunt, tool for reducing dimension from ((>100s) -> (100s)) dimension, then use likelihood-informed tools to hone down ((100s) -> (10s)) dimensions. `iters=1:X` is more performant in input than output dimension for `X>1` so we only do this here. This schedule is added to the emulator as follows
33+
```
34+
emulator = Emulator(
35+
machine_learning_tool,
36+
input_output_pairs;
37+
encoder_schedule = encoder_schedule,
38+
encoder_kwargs = encoder_kwargs_from(ekp, prior), # kwargs from "Calibrate" objects
39+
)
40+
```
41+
42+
## Recommended `encoder_kwargs`
43+
44+
The `EnsembleKalmanProcesses` infrastructure naturally provides information to build the data processors. We provide some utilities to extract these into `kwargs` of the correct format, the recommended (but not only) option is:
45+
2146
```julia
22-
schedule = (decorrelate_sample_cov(), "in_and_out")
47+
# `prior::ParameterDistribution`
48+
# `ekp::EnsembleKalmanProcess`
49+
encoder_kwargs = encoder_kwargs_from(ekp, prior)
2350
```
24-
If the user only provides the observational noise covariance,
51+
52+
### Default schedule (to whiten with no dimension reduction)
53+
When the user provides no `encoder_schedule`, the following is created
2554
```julia
2655
emulator = Emulator(
2756
machine_learning_tool,
2857
input_output_pairs;
29-
encoder_kwargs = (; obs_noise_cov = obs_noise_cov),
58+
encoder_kwargs = encoder_kwargs,
3059
)
3160
```
3261
The default schedule under-the-hood, is given by
3362
```julia
3463
schedule = [
3564
(decorrelate_sample_cov(), "in"),
36-
(decorrelate_structure_mat(), "out"), # uses obs_noise_cov
65+
(decorrelate_structure_mat(), "out"), # uses obs_noise_cov kwarg
66+
]
67+
```
68+
With no kwargs also provided, the following schedule under-the-hood, is given by
69+
```julia
70+
schedule = [
71+
(decorrelate_sample_cov(), "in_and_out"),
3772
]
3873
```
3974

4075
!!! note "Switch encoding off"
4176
To ensure that no encoding is happening, the user must pass in an empty schedule `encoder_schedule = []`
4277

43-
### Recommended: schedule for PCA dimension reduction
78+
## Other typical schedule: PCA dimension reduction
4479

45-
Our recommendeded family to balance efficiency and reduce dimension is to use the `retain_var` kwargs.
80+
Robust and blunt dimension reduction can be done with only PCA
4681
```julia
4782
retain_var_in = 0.99 # reduce dimension retaining 99% of input variance
4883
retain_var_out = 0.95 # reduce dimension retaining 95% of output variance
@@ -59,22 +94,21 @@ emulator = Emulator(
5994
encoder_kwargs = encoder_kwargs,
6095
)
6196
```
62-
## Recommended: Get `encoder_kwargs` from `ObservationSeries` etc.
97+
## kwargs from other objects:
6398

64-
To transition more smoothly from the `EnsembleKalmanProcesses` infrastructure, one can also get the kwargs in the desired format from any [`Observation` and `ObservationSeries`](https://clima.github.io/EnsembleKalmanProcesses.jl/dev/observations/) objects, as well as [`ParameterDistribution`](https://clima.github.io/EnsembleKalmanProcesses.jl/dev/parameter_distributions/) objects used in the `EnsembleKalmanProcesses.jl` package.
99+
One can also obtain individual kwarg sets from other objects, and can merge these as follows
65100
```julia
66101
# `prior::ParameterDistribution`
67-
input_kwargs = get_kwargs_from(prior)
68-
69102
# `observation_series::ObservationSeries`
103+
input_kwargs = get_kwargs_from(prior)
70104
output_kwargs = get_kwargs_from(observation_series)
71-
72105
encoder_kwargs = merge(input_kwargs, output_kwargs)
73106
```
107+
108+
Though not recommended, you can also build your own keyword arguments.
74109
!!! note "Observational noise notes"
75110
1. The `obs_noise_cov` object does not need to be a constructed `AbstractMatrix`, rather it can be any type of compactly stored matrix compatible with `EnsembleKalmanProcesses` (e.g., [here](https://clima.github.io/EnsembleKalmanProcesses.jl/dev/observations/)).
76-
2. When `ObservationSeries` contains multiple observations, it is assumed that the covariance of the noise of all observations are the same. (If they are not, only the first will be taken by default in `get_kwargs_from(...)`
77-
111+
2. When `ObservationSeries` contains multiple observations, it is assumed that the covariance of the noise of all observations are the same. (If they are not, only the first will be taken by default in `get_kwargs_from(...)`
78112

79113
## Building and interpreting encoder schedules
80114

@@ -145,7 +179,7 @@ The `DataContainerProcessor` encodes "input" data agnostic of the "output" data,
145179

146180
The `PairedDataContainerProcessor` encodes inputs (or outputs) using information of the both inputs and outputs in pairs
147181
- `CanonicalCorrelation` - constructed with `canonical_correlation()`, which performs [canonical correlation analysis](https://en.wikipedia.org/wiki/Canonical_correlation) to process the pairs. In effect this performs PCA on the cross-correlation from input and output samples.
148-
- [Coming soon] `LikelihoodInformed` - this will use the data or likelihood from the inverse problem at hand to build diagnostic matrices that are used to find informative directions for dimension reduction. In particular we build generalizations of current frameworks (e.g., [Cui, Zahm 2021](http://doi.org/10.1088/1361-6420/abeafb), [Baptista, Marzouk, Zahm 2022](https://arxiv.org/abs/2207.08670)) to use the latest EKP iterations.
182+
- `LikelihoodInformed`- constructed with `likelihood_informed()`- we use the data or likelihood from the inverse problem at hand to build diagnostic matrices that are used to find informative directions for dimension reduction. In particular we build generalizations of current frameworks (e.g., [Cui, Zahm 2021](http://doi.org/10.1088/1361-6420/abeafb), [Baptista, Marzouk, Zahm 2022](https://arxiv.org/abs/2207.08670)) to use the latest EKP iterations. We in fact generalize this framework here [Preprint Coming soon] for use in partially-informed distributions (e.g. EKI iterations)
149183

150184
This is an extensible framework, and so new data processors can be added to this library.
151185

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[deps]
2+
AdvancedMH = "5b7e9947-ddc0-4b3f-9b55-0d8042f74170"
3+
CalibrateEmulateSample = "95e48a1f-0bec-4818-9538-3db4340308e3"
4+
ChunkSplitters = "ae650224-84b6-46f8-82ea-d812ca08434e"
5+
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
6+
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
7+
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
8+
EnsembleKalmanProcesses = "aa8a2aa5-91d8-4396-bcef-d4f2ec43552d"
9+
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
10+
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
11+
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
12+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
13+
MCMCChains = "c7f686f2-ff18-58e9-bc7b-31028e88f75d"
14+
Manifolds = "1cead3c2-87b3-11e9-0ccd-23c62b72b94e"
15+
Manopt = "0fc0a36d-df90-57f3-8f93-d78a9fc72bb5"
16+
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
17+
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
18+
StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd"
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using Distributions
2+
using EnsembleKalmanProcesses
3+
using JLD2
4+
using LinearAlgebra
5+
using Random
6+
7+
8+
include("./models.jl") # defines dims, files, mod_type
9+
# select the forward map
10+
@info "Executing model type $(mod_type)"
11+
12+
function main()
13+
if mod_type == "linear"
14+
prior_cov, y, obs_noise_cov, model, true_parameter = lin(input_dim, output_dim, rng)
15+
else
16+
bad_model(mod_type, mod_types)
17+
end
18+
19+
prior_obj = ParameterDistribution(
20+
Parameterized(MvNormal(zeros(size(prior_cov, 1)), prior_cov)),
21+
fill(no_constraint(), size(prior_cov, 1)),
22+
"$(mod_type)_prior",
23+
)
24+
25+
n_ensemble = 80
26+
initial_ensemble = construct_initial_ensemble(rng, prior_obj, n_ensemble)
27+
ekp = EnsembleKalmanProcess(initial_ensemble, y, obs_noise_cov, TransformInversion(); rng)
28+
29+
max_iter = 20
30+
n_iters = 0
31+
for i in 1:max_iter
32+
n_iters += 1
33+
G_ens = hcat([forward_map(param, model) for param in eachcol(get_ϕ_final(prior_obj, ekp))]...)
34+
terminate = update_ensemble!(ekp, G_ens)
35+
if !isnothing(terminate)
36+
break
37+
end
38+
end
39+
@info "EKP iterations: $n_iters"
40+
@info "Loss over iterations: $(get_error(ekp))"
41+
@info "Timesteps: $(ekp.Δt)"
42+
@info "Checkpoints: $(get_algorithm_time(ekp))"
43+
αs = vec([0 get_algorithm_time(ekp)[1:end]...])
44+
ekp_samples = Dict=> (get_u(ekp, i), get_g(ekp, i)) for (i, α) in enumerate(αs[1:(end - 1)]))
45+
46+
# evaluate at final time
47+
G_ens = hcat([forward_map(param, model) for param in eachcol(get_ϕ_final(prior_obj, ekp))]...)
48+
ekp_samples[αs[end]] = (get_u(ekp, length(αs)), G_ens)
49+
50+
#! format: off
51+
save(
52+
joinpath(datadir,"ekp_$(mod_type).jld2"),
53+
"ekpobj", ekp,
54+
"ekp_samples", ekp_samples,
55+
"prior_obj", prior_obj,
56+
"y", y,
57+
"obs_noise_cov", obs_noise_cov,
58+
"model", model,
59+
"true_parameter", true_parameter,
60+
"alphas", αs,
61+
)
62+
#! format: on
63+
64+
end
65+
main()
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
using Distributions
2+
using EnsembleKalmanProcesses
3+
using JLD2
4+
using LinearAlgebra
5+
using Random
6+
using MCMCChains
7+
using AdvancedMH
8+
using DataFrames # table print at end
9+
# CES
10+
using CalibrateEmulateSample.Emulators
11+
using CalibrateEmulateSample.MarkovChainMonteCarlo
12+
using CalibrateEmulateSample.Utilities
13+
14+
15+
include("./models.jl")
16+
# select the forward map
17+
mod_types = ["linear"]
18+
mod_type = mod_types[1]
19+
@info "Executing model type $(mod_type)"
20+
21+
function main()
22+
loaded = load("datafiles/ekp_$(mod_type).jld2")
23+
ekpobj = loaded["ekpobj"]
24+
ekp_samples = loaded["ekp_samples"]
25+
prior = loaded["prior_obj"]
26+
obs_noise_cov = loaded["obs_noise_cov"]
27+
y = loaded["y"]
28+
model = loaded["model"]
29+
true_parameter = loaded["true_parameter"]
30+
αs = loaded["alphas"]
31+
mask = [1, length(αs)] # don't do all cases
32+
αs = αs[mask]
33+
34+
min_iter = 1
35+
max_iter = min(10, length(αs)) # number of EKP iterations to use data from is at most this
36+
training_points = [ekp_samples[α] for α in αs[min_iter:max_iter]]
37+
38+
encoder_schedule_ref = [(decorrelate_structure_mat(), "in_and_out")]
39+
40+
# chosen some truncations with similar dimension
41+
rvs_rinfos = [(0.995, 0.99995), (0.99, 0.9995), (0.98, 0.995), (0.95, 0.97), (0.9, 0.9)]
42+
43+
# as we have more input samples than out stored in ekp. we provide the extended set of outputs to be used in the likelihood informed processor
44+
final_samples_out = ekp_samples[αs[end]][2]
45+
encoder_kwargs = encoder_kwargs_from(ekpobj, prior; final_samples_out = final_samples_out)
46+
47+
flat_io_pairs = get_training_points(ekpobj, min_iter:max_iter, g_final = final_samples_out)
48+
49+
names = ["reference", "PCA-in-out", "PCA(mat)-in-out", ["LI-in(1:$(i))-out(1:1)" for i in mask]...]
50+
51+
52+
if mod_type == "linear"
53+
ni_scaling = 0.0 # noise injection when eval forward map (def. 1)
54+
else
55+
bad_model(mod_type, mod_types)
56+
end
57+
58+
all_errs = zeros(length(rvs_rinfos), length(names))
59+
reduced_dims = zeros(Int, length(rvs_rinfos), length(names)) # store reduced dims for final table
60+
61+
for (idx, rv_rinfo) in enumerate(rvs_rinfos)
62+
rv, rinfo = rv_rinfo
63+
encoder_schedule_decorrelate_samp = [(decorrelate_sample_cov(retain_var = rv), "in_and_out")]
64+
encoder_schedule_decorrelate = [(decorrelate_structure_mat(retain_var = rv), "in_and_out")]
65+
encoder_schedules_li = [
66+
[
67+
(decorrelate_structure_mat(), "in_and_out"),
68+
(likelihood_informed(retain_info = rinfo, iters = 1:i), "in"),
69+
(likelihood_informed(retain_info = rinfo, iters = 1:1), "out"),
70+
] for i in mask
71+
]
72+
73+
em_ref = forward_map_wrapper(
74+
param -> forward_map(param, model),
75+
prior,
76+
flat_io_pairs;
77+
encoder_schedule = deepcopy(encoder_schedule_ref),
78+
encoder_kwargs = deepcopy(encoder_kwargs),
79+
noise_injector_scaling = ni_scaling, # shouldnt be needed
80+
)
81+
82+
em_decorrelate_samp = forward_map_wrapper(
83+
param -> forward_map(param, model),
84+
prior,
85+
flat_io_pairs;
86+
encoder_schedule = deepcopy(encoder_schedule_decorrelate_samp),
87+
encoder_kwargs = deepcopy(encoder_kwargs),
88+
noise_injector_scaling = ni_scaling,
89+
)
90+
91+
em_decorrelate = forward_map_wrapper(
92+
param -> forward_map(param, model),
93+
prior,
94+
flat_io_pairs;
95+
encoder_schedule = deepcopy(encoder_schedule_decorrelate),
96+
encoder_kwargs = deepcopy(encoder_kwargs),
97+
noise_injector_scaling = ni_scaling,
98+
)
99+
100+
101+
ems_li = [
102+
forward_map_wrapper(
103+
param -> forward_map(param, model),
104+
prior,
105+
flat_io_pairs;
106+
encoder_schedule = deepcopy(encoder_schedule),
107+
encoder_kwargs = deepcopy(encoder_kwargs),
108+
noise_injector_scaling = ni_scaling,
109+
) for encoder_schedule in encoder_schedules_li
110+
]
111+
post_means = reshape(true_parameter, input_dim, 1)
112+
post_covs = []
113+
for (iidx, nn, em) in zip(1:length(names), names, vcat(em_ref, em_decorrelate_samp, em_decorrelate, ems_li...))
114+
115+
reduced_dims[idx, iidx] = get_encoded_dim(get_encoder_schedule(em), "in")
116+
println(" ")
117+
@info "Encoding name: $(nn)"
118+
E, _ = get_encoder_from_schedule(get_encoder_schedule(em), "in")
119+
120+
if isnothing(E)
121+
@info "No truncation"
122+
else
123+
ed = get_encoded_dim(get_encoder_schedule(em), "in")
124+
@info "Truncation criteria (var>$(rv), or information > $(rinfo))"
125+
@info "input dim reduced to: $(ed)"
126+
127+
end
128+
println(" ")
129+
u0 = rand(MvNormal(mean(prior), cov(prior)))
130+
mcmc = MCMCWrapper(RWMHSampling(), y, prior, em; init_params = u0)
131+
new_step = optimize_stepsize(mcmc; init_stepsize = 0.05, N = 2000, discard_initial = 0)
132+
133+
println("Begin MCMC - with step size ", new_step)
134+
mcmc = MCMCWrapper(RWMHSampling(), y, prior, em; init_params = u0)
135+
chain = MarkovChainMonteCarlo.sample(mcmc, 40_000; stepsize = new_step, discard_initial = 5_000)
136+
posterior = MarkovChainMonteCarlo.get_posterior(mcmc, chain)
137+
138+
post_mean = mean(posterior)
139+
post_cov = cov(posterior)
140+
141+
post_means = hcat(post_means, reshape(post_mean, input_dim, 1))
142+
push!(post_covs, post_cov)
143+
end
144+
145+
# post_means[:,1] = true parameter
146+
# post_means[:,2] = ref
147+
# post_means[:,3] = decor, LI etc.
148+
# so err cols are normalized diff to ref of (decor, LI etc.) (lower is better)
149+
all_errs[idx, :] = [norm(post_means[:, 1] - v) / norm(post_means[:, 1]) for v in eachcol(post_means[:, 2:end])]'
150+
end
151+
152+
df = DataFrame(all_errs, [Symbol(n) for n in names[1:end]])
153+
154+
# build dataframe
155+
pairs = [(reduced_dims[i, j], all_errs[i, j]) for i in axes(all_errs, 1), j in axes(all_errs, 2)]
156+
157+
df = DataFrame(pairs, Symbol.(names[1:end])) # columns = names
158+
df.truncation = rvs_rinfos # add row labels
159+
return df, all_errs, reduced_dims[:, 1:end]
160+
end
161+
162+
df, all_errs, red_dims = main()
163+
164+
@info "(reduced_dim, error) of posterior mean to whitened \"reference\" solution,\n when using the truncation ({for PCA}, {for LI})"
165+
select!(df, :reference, :)

0 commit comments

Comments
 (0)