Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2844fdd
Add LASSO without structure to LinearRegression notebook
Blunde1 Dec 27, 2023
b0552e0
Transport observation samples, not deterministic responses
Blunde1 Jan 3, 2024
36c1f5c
Add specified observation noise, not just standard normal
Blunde1 Jan 3, 2024
c71b6c6
Run black
Blunde1 Jan 3, 2024
6f130a6
Run ruff
Blunde1 Jan 3, 2024
40b1e0e
Copy linear_l1_regression from graphite-maps
Blunde1 Jan 3, 2024
d2810b1
Rename parameters in linear_l1_regression for easier understanding
Blunde1 Jan 3, 2024
6a3b199
add sklearn and tqdm to docs
Blunde1 Jan 3, 2024
1474d54
Tidy up text in notebook
Blunde1 Jan 5, 2024
6310ad2
First draft and most important thoughts on objective in kalman-type m…
Blunde1 Jan 12, 2024
3fbc8eb
Strong opinions on language for perturbations added
Blunde1 Jan 15, 2024
7da4ea2
Be more consistent in mathematical notation
Blunde1 Jan 15, 2024
2f5ce09
Strong opinions on nll vs ls objectives even when equivalent estimators
Blunde1 Jan 15, 2024
b3c446a
TLDR and more strong opinions
Blunde1 Jan 17, 2024
e68a30c
Add explicit how to evaluate different methods
Blunde1 Jan 17, 2024
9614d63
Test to see if github renders block-level math
Blunde1 Jan 17, 2024
f042a0c
add newlines to make github recognize block-math format
Blunde1 Jan 17, 2024
a35aa19
Format equations in minimize model information-loss
Blunde1 Jan 17, 2024
a5a89b3
Use ^\ast instead of ^*
Blunde1 Jan 17, 2024
44fd48d
Brackets to clearly separate expressions
Blunde1 Jan 17, 2024
5d7ee69
Replace dollarsigns with mathblocks some places
Blunde1 Jan 17, 2024
50a4ced
Use github formatting for inline math
Blunde1 Jan 17, 2024
941c529
Comments on lasso without structure
Blunde1 Jan 17, 2024
5110a98
Comments on human understandable for enif
Blunde1 Jan 17, 2024
6b159e7
Make sure we condition on the correct d
Blunde1 Jan 17, 2024
a47ff3d
Be very clear on evaluation on test-data
Blunde1 Jan 17, 2024
d408c49
Fix typo
Blunde1 Jan 18, 2024
bc86d68
follows --> follow
Blunde1 Jan 22, 2024
e3142da
is -> are
Blunde1 Jan 22, 2024
3b99956
Fix typo "empirical"
Blunde1 Jan 22, 2024
d365049
Fix typo works->work
Blunde1 Jan 22, 2024
5775872
Fix typo yields->yield
Blunde1 Jan 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 137 additions & 5 deletions docs/source/LinearRegression.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.15.2
# jupytext_version: 1.16.0
# kernelspec:
# display_name: Python 3 (ipykernel)
# language: python
Expand Down Expand Up @@ -34,6 +34,9 @@
# %%
import numpy as np
from matplotlib import pyplot as plt
from sklearn.linear_model import LassoCV
from sklearn.preprocessing import StandardScaler
from tqdm import tqdm

from iterative_ensemble_smoother import ESMDA

Expand All @@ -49,9 +52,10 @@

# %%
num_parameters = 25
num_observations = 100
num_ensemble = 30
num_observations = 50
num_ensemble = 100
prior_std = 1
obs_sd = 1.0

# %%
rng = np.random.default_rng(42)
Expand All @@ -67,14 +71,14 @@ def g(X):

# Create observations: obs = g(x) + N(0, 1)
x_true = np.linspace(-1, 1, num=num_parameters)
observation_noise = rng.standard_normal(size=num_observations)
observation_noise = obs_sd * rng.standard_normal(size=num_observations)
observations = g(x_true) + observation_noise

# Initial ensemble X ~ N(0, prior_std) and diagonal covariance with ones
X = rng.normal(size=(num_parameters, num_ensemble)) * prior_std

# Covariance matches the noise added to observations above
covariance = np.ones(num_observations)
covariance = np.ones(num_observations) * obs_sd**2

# %% [markdown]
# ## Solve the maximum likelihood problem
Expand Down Expand Up @@ -170,3 +174,131 @@ def g(X):
plt.grid(True, ls="--", zorder=0, alpha=0.33)
plt.legend()
plt.show()


# %% [markdown]
# ## Solve using LASSO without structure
#
# The Kalman gain is possible to estimate through multiple linear regression
# $d$ onto $x$.
# This view has some implications.
# - Modern linear regression routines (LASSO, RIDGE, and others) can be used
# to solve for $K$. This is particularly good for e.g. $p>>n$ problems,
# typical for ensemble methods.
# - We lose the ability to specify the independence of randomness from
# $x$ and $\epsilon$ into $d$.
# - We also lose the ability to specify structure in the prior through the
# covariance.
#
# Below we showcase how the LASSO algorithm can be used multiple times to
# solve for the Kalman gain $K$.


# %%
def linear_l1_regression(D, X):
"""
Performs LASSO regression for each response in X against predictors in D,
constructing a sparse matrix of regression coefficients.

The function scales features in D using standard scaling before applying
LASSO, then re-scales the coefficients to the original scale of D. This
extracts the effect of each feature in D on each response in X, ignoring
intercepts and constant terms.

Parameters
----------
D : np.ndarray
2D array of predictors with shape (n, p).
X : np.ndarray
2D array of responses with shape (n, m).

Returns
-------
H : np.ndarray
2D array of responses with shape (m, p) with re-scaled LASSO
regression coefficients for each response in X.

Raises
------
AssertionError
If the number of samples in D and X do not match, or if the shape of
H is not (m, p).
"""
n, p = D.shape # p: number of features
n_y, m = X.shape # m: number of y responses

# Assert that the first dimension of D and X are the same
assert n == n_y, "Number of samples in D and X must be the same"

scaler_d = StandardScaler()
D_scaled = scaler_d.fit_transform(D)

scaler_x = StandardScaler()
X_scaled = scaler_x.fit_transform(X)

# Loop over features
H = np.zeros((m, p))
for j in tqdm(range(m), desc="Learning sparse linear map for each response"):
x_j = X_scaled[:, j]

# Learn individual regularization and fit
eps = 1e-3
max_iter = 10000
model_cv = LassoCV(cv=10, fit_intercept=False, max_iter=max_iter, eps=eps)
model_cv.fit(D_scaled, x_j)

# Extract coefficients
for non_zero_ind in model_cv.coef_.nonzero()[0]:
H[j, non_zero_ind] = (
scaler_x.scale_[j]
* model_cv.coef_[non_zero_ind]
/ scaler_d.scale_[non_zero_ind]
)

# Assert shape of H_sparse
assert H.shape == (m, p), "Shape of H_sparse must be (m, p)"

return H


# %%
# Learn Kalman gain
X_prior = np.copy(X)
Y = g(X_prior)
D = Y + obs_sd * rng.standard_normal(size=Y.shape)
K = linear_l1_regression(D=D.T, X=X_prior.T)

# %%
# Use Kalman gain in update equation
X_posterior = X_prior + K @ (observations - D.T).T

# %%
plt.figure(figsize=(8, 3))
plt.scatter(np.arange(len(x_true)), x_true, label="True parameter values")
plt.scatter(np.arange(len(x_true)), x_ml, label="ML estimate (no prior)")
plt.scatter(
np.arange(len(x_true)), np.mean(X_posterior, axis=1), label="Posterior mean"
)

# Loop over every ensemble member and plot it
for j in range(num_ensemble):
# Jitter along the x-axis a little bit
x_jitter = np.arange(len(x_true)) + rng.normal(loc=0, scale=0.1, size=len(x_true))

# Plot this ensemble member
plt.scatter(
x_jitter,
X_posterior[:, j],
label=("Posterior values" if j == 0 else None),
color="black",
alpha=0.2,
s=5,
zorder=0,
)
plt.xlabel("Parameter index")
plt.ylabel("Parameter value")
plt.grid(True, ls="--", zorder=0, alpha=0.33)
plt.legend()
plt.show()

# %%
Loading