Skip to content

Commit 9357164

Browse files
closed form optimal covariance
1 parent 47a03ec commit 9357164

3 files changed

Lines changed: 65 additions & 88 deletions

File tree

coupled_rejection_sampling/mvn.py

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,47 @@
11
import math
2-
from functools import partial, lru_cache
2+
from functools import partial
33

4-
import cvxpy as cp
5-
import jax.experimental.host_callback as hcb
64
import jax.numpy as jnp
75
import jax.random
86
import jax.scipy.linalg as jlinalg
97
import jax.scipy.stats as jstats
108
import numpy as np
11-
import scipy.linalg as linalg
129
from jax import numpy as jnp
13-
from jax._src.scipy.linalg import cho_solve
10+
from jax.scipy.linalg import cho_solve
1411

1512
from coupled_rejection_sampling.coupled_rejection_sampler import coupled_sampler
1613

1714
_LOG_2PI = math.log(2 * math.pi)
1815

1916

20-
@lru_cache
21-
def _get_cvxpy_pb(d):
22-
P_inv_param = cp.Parameter((d, d), PSD=True)
23-
Sig_inv_param = cp.Parameter((d, d), PSD=True)
24-
z_param = cp.Parameter((d,))
25-
26-
X = cp.Variable((d, d), PSD=True)
27-
28-
constraints = [X << P_inv_param, X << Sig_inv_param]
29-
objective = cp.log_det(X)
30-
problem = cp.Problem(cp.Maximize(objective), constraints)
31-
32-
return problem, X, P_inv_param, Sig_inv_param, z_param
33-
34-
35-
def get_optimal_covariance(m, chol_P, mu, chol_Sig, verbose=False):
17+
def get_optimal_covariance(chol_P, chol_Sig):
3618
"""
3719
Get the optimal covariance according to the objective defined in Section 3 of [1].
3820
21+
The notations roughly follow the ones in the article.
22+
3923
Parameters
4024
----------
41-
m: jnp.ndarray
42-
Mean of X
4325
chol_P: jnp.ndarray
4426
Square root of the covariance of X. Lower triangular.
45-
mu: jnp.ndarray
46-
Mean of Y
4727
chol_Sig: jnp.ndarray
4828
Square root of the covariance of Y. Lower triangular.
49-
verbose: bool, optional
50-
Is the solver verbose. Default is False.
5129
Returns
5230
-------
5331
chol_Q: jnp.ndarray
5432
Cholesky of the resulting dominating matrix.
5533
"""
56-
d = m.shape[0]
34+
d = chol_P.shape[0]
5735
if d == 1:
5836
return np.maximum(chol_P, chol_Sig)
59-
problem, X, P_inv_param, Sig_inv_param, z_param = _get_cvxpy_pb(d)
6037

61-
eye = np.eye(d)
62-
P_inv = linalg.cho_solve((chol_P, True), eye)
63-
Sig_inv = linalg.cho_solve((chol_Sig, True), eye)
38+
right_Y = jlinalg.solve_triangular(chol_P, chol_Sig, lower=True) # Y = RY.T RY
39+
w_Y, v_Y = jlinalg.eigh(right_Y.T @ right_Y)
40+
w_Y = jnp.minimum(w_Y, 1)
41+
i_w_Y = 1. / jnp.sqrt(w_Y)
6442

65-
z_param.value = m - mu
66-
P_inv_param.value = P_inv
67-
Sig_inv_param.value = Sig_inv
68-
69-
problem.solve(warm_start=True, verbose=verbose, qcp=True, solver=cp.MOSEK)
70-
71-
Q_inv = X.value
72-
chol_Q_inv = linalg.cholesky(Q_inv, lower=True)
73-
Q = linalg.cho_solve((chol_Q_inv, True), eye)
74-
chol_Q = np.linalg.cholesky(Q)
75-
return chol_Q
43+
left_Q = chol_Sig @ (v_Y * i_w_Y[None, :])
44+
return jlinalg.cholesky(left_Q @ left_Q.T, lower=True)
7645

7746

7847
def coupled_mvns(key, m, chol_P, mu, chol_Sig, N=1, chol_Q=None):
@@ -109,9 +78,7 @@ def coupled_mvns(key, m, chol_P, mu, chol_Sig, N=1, chol_Q=None):
10978
"""
11079

11180
if chol_Q is None:
112-
# This is gonna be slow, but there is no real JAX version...
113-
chol_Q = hcb.call(lambda args: get_optimal_covariance(*args), (m, chol_P, mu, chol_Sig), result_shape=chol_P)
114-
81+
chol_Q = get_optimal_covariance(chol_P, chol_Sig)
11582
log_det_chol_P = tril_log_det(chol_P)
11683
log_det_chol_Sig = tril_log_det(chol_Sig)
11784
log_det_chol_Q = tril_log_det(chol_Q)

examples/optimal_covariance.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
"""
22
This corresponds to the Application 1 of [1].
33
"""
4+
import time
45
from functools import partial
56

6-
import jax.experimental.host_callback as hcb
77
import jax.numpy as jnp
88
import jax.random
99
import matplotlib.pyplot as plt
1010
import numpy as np
11-
import tikzplotlib
1211
import tqdm.auto as tqdm
1312
from jax.scipy import linalg
1413

@@ -19,15 +18,15 @@
1918
save_path = "out/optimal.npz"
2019

2120
jax_key = jax.random.PRNGKey(42)
22-
D = 10
21+
D = 15
2322
P = jnp.diag(jnp.arange(1, D + 1))
2423

2524
chol_P = jnp.linalg.cholesky(P)
2625
m = mu = jnp.zeros((D,))
2726

2827
B = 200 # number of covariances
2928
M = 500 # number of RS experiments
30-
NS = [1, 8, 64, 512]
29+
NS = [4 ** k for k in range(6)]
3130

3231

3332
def rejection_experiment():
@@ -39,15 +38,15 @@ def get_chol_covs(k):
3938
orth, _ = linalg.qr(gauss)
4039
Sigma = orth @ P @ orth.T
4140
chol_Sigma = jnp.linalg.cholesky(Sigma)
42-
chol_Q = hcb.call(lambda args: get_optimal_covariance(*args), (m, chol_P, mu, chol_Sigma),
43-
result_shape=chol_P)
41+
chol_Q = get_optimal_covariance(chol_P, chol_Sigma)
4442
return chol_Sigma, chol_Q
4543

4644
res_shape = B, len(NS), 2
4745
res_mean_coupled = np.zeros(res_shape)
4846
res_var_coupled = np.zeros(res_shape)
4947
res_mean_trials = np.zeros(res_shape)
5048
res_var_trials = np.zeros(res_shape)
49+
res_runtime = np.zeros(res_shape)
5150

5251
res_coupling_bounds = np.zeros((B, 2))
5352
res_trials_bounds = np.zeros(res_shape)
@@ -79,24 +78,31 @@ def test_fun(op_key, chol_Sigma, chol_Q, N):
7978
res_trials_bounds[b, i, 0] = (1 + (n - 1) / trials_opt) / (n / trials_opt)
8079
res_trials_bounds[b, i, 1] = (1 + (n - 1) / trials_max) / (n / trials_max)
8180

81+
tic = time.time()
8282
(res_mean_coupled[b, i, 0], res_var_coupled[b, i, 0], res_mean_trials[b, i, 0],
8383
res_var_trials[b, i, 0]) = test_fun(rs_key, chol_Sigma, chol_Q, n)
84+
res_runtime[b, i, 0] = time.time() - tic
8485

86+
tic = time.time()
8587
(res_mean_coupled[b, i, 1], res_var_coupled[b, i, 1], res_mean_trials[b, i, 1],
8688
res_var_trials[b, i, 1]) = test_fun(rs_key, chol_Sigma, chol_Q_max, n)
87-
88-
return res_mean_coupled, res_var_coupled, res_mean_trials, res_var_trials, res_coupling_bounds, res_trials_bounds
89+
res_runtime[b, i, 1] = time.time() - tic
90+
return res_mean_coupled, res_var_coupled, res_mean_trials, res_var_trials, res_coupling_bounds, res_trials_bounds, res_runtime
8991

9092

9193
if RUN:
92-
(coupled_mean, coupled_var, n_trials_mean, n_trials_var, coupling_bounds, n_trials_bounds) = rejection_experiment()
94+
(coupled_mean, coupled_var, n_trials_mean, n_trials_var, coupling_bounds, n_trials_bounds,
95+
runtime) = rejection_experiment()
9396
np.savez(save_path, coupled_mean=coupled_mean, coupled_var=coupled_var,
9497
n_trials_mean=n_trials_mean, n_trials_var=n_trials_var, coupling_bounds=coupling_bounds,
95-
n_trials_bounds=n_trials_bounds)
98+
n_trials_bounds=n_trials_bounds, runtime=runtime)
9699

97100
if PLOT:
98101
data = np.load(save_path)
99-
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 6), sharex=True, sharey=True)
102+
fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(15, 7), sharex=True, sharey=True)
103+
104+
print(np.mean(data["runtime"][1:], 0))
105+
print(np.std(data["runtime"][1:], 0))
100106

101107
for i, n in enumerate(NS):
102108
ax = axes.flatten()[i]
@@ -108,16 +114,16 @@ def test_fun(op_key, chol_Sigma, chol_Q, N):
108114
ax.scatter(range(B), data["coupled_mean"][arg_sort, i, 1], label=f"Empirical MAX",
109115
color="tab:orange",
110116
alpha=0.75)
111-
112-
ax.plot(range(B), data["coupling_bounds"][arg_sort, 0], label="Optimised bound",
113-
color="tab:blue")
114-
ax.plot(range(B), data["coupling_bounds"][arg_sort, 1], label="MAX bound",
115-
color="tab:orange")
117+
twinx = ax.twinx()
118+
twinx.semilogy(range(B), data["coupling_bounds"][arg_sort, 0], label="Optimised bound",
119+
color="tab:blue")
120+
twinx.semilogy(range(B), data["coupling_bounds"][arg_sort, 1], label="MAX bound",
121+
color="tab:orange")
116122
axes[0, 0].legend(loc="upper left")
117-
# plt.show()
118-
tikzplotlib.save("out/gaussian_opt_coupling.tikz")
123+
plt.show()
124+
# tikzplotlib.save("out/gaussian_opt_coupling.tikz")
119125

120-
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 6), sharex=True, sharey=True)
126+
fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(15, 7), sharex=True, sharey=True)
121127
for i, n in enumerate(NS):
122128
ax = axes.flatten()[i]
123129
ax.set_title(f"$N={n}$")
@@ -131,6 +137,6 @@ def test_fun(op_key, chol_Sigma, chol_Q, N):
131137
alpha=0.75)
132138
ax.plot(range(B), 1 / data["n_trials_bounds"][arg_sort, i, 1], label="MAX bound", color="tab:orange")
133139
axes[0, 0].legend(loc="upper left")
134-
# plt.show()
140+
plt.show()
135141

136-
tikzplotlib.save("out/gaussian_opt_acceptance.tikz")
142+
# tikzplotlib.save("out/gaussian_opt_acceptance.tikz")

tests/test_mvn.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import cvxpy as cp
12
import jax.random
23
import numpy as np
34
import numpy.testing as np_test
@@ -17,6 +18,25 @@ def is_inverse_less(chol_A, chol_B):
1718
assert np.max(np.real(linalg.eigvals(A_inv - B_inv))) > -1e-7
1819

1920

21+
def cvxpy_get_optimal_cov(chol_P, chol_Sig):
22+
d = chol_P.shape[0]
23+
eye = np.eye(d)
24+
P_inv = linalg.cho_solve((chol_P, True), eye)
25+
Sig_inv = linalg.cho_solve((chol_Sig, True), eye)
26+
27+
X = cp.Variable((d, d), PSD=True)
28+
29+
constraints = [X << P_inv, X << Sig_inv]
30+
objective = cp.log_det(X)
31+
problem = cp.Problem(cp.Maximize(objective), constraints)
32+
problem.solve(warm_start=True, qcp=True, solver=cp.SCS)
33+
Q_inv = X.value
34+
chol_Q_inv = linalg.cholesky(Q_inv, lower=True)
35+
Q = linalg.cho_solve((chol_Q_inv, True), eye)
36+
chol_Q = np.linalg.cholesky(Q)
37+
return chol_Q
38+
39+
2040
def test_ordering():
2141
chol_P = np.array([
2242
[1.0, 0.0, 0.0],
@@ -25,15 +45,14 @@ def test_ordering():
2545
])
2646

2747
chol_Sig = np.array([
28-
[1.3, 0.0, 0.0],
48+
[0.2, 0.0, 0.0],
2949
[0.5, 0.9, 0.0],
30-
[0.1, 1.7, 0.2]
50+
[0.1, 1.7, 1.5]
3151
])
3252

33-
a = np.array([0., 1., -1.])
34-
b = np.array([1., -1., 0.])
35-
36-
chol_Q = get_optimal_covariance(a, chol_P, b, chol_Sig, verbose=True)
53+
chol_Q = get_optimal_covariance(chol_P, chol_Sig)
54+
cvxpy_chol_Q = cvxpy_get_optimal_cov(chol_P, chol_Sig)
55+
np.testing.assert_allclose(chol_Q, cvxpy_chol_Q, rtol=1e-4, atol=1e-5)
3756

3857
is_inverse_less(chol_P, chol_Q)
3958
is_inverse_less(chol_Sig, chol_Q)
@@ -108,22 +127,7 @@ def test_mvns_different_cov(d, M, mocker: MockerFixture):
108127
np_test.assert_allclose(np.cov(ys, rowvar=False), chol_Sigma @ chol_Sigma.T, atol=1e-2, rtol=1e-2)
109128

110129

111-
112130
def test_lower_bound():
113-
# A trivial test
114-
m = np.array([1.])
115-
mu = np.array([1.])
116-
Sig = np.array([[1.]])
117-
P = np.array([[1.]])
118-
Q = np.array([[1.]])
119-
120-
chol_P = linalg.cholesky(P, lower=True)
121-
chol_Sig = linalg.cholesky(Sig, lower=True)
122-
chol_Q = linalg.cholesky(Q, lower=True)
123-
K = coupled_mvns.lower_bound(m, chol_P, mu, chol_Sig, chol_Q)
124-
125-
assert K == pytest.approx(1., rel=1e-5, abs=1e-5)
126-
127131
# Test against reflection coupling
128132
m = np.array([1., 2.])
129133
mu = np.array([1.5, 1.1])

0 commit comments

Comments
 (0)