Skip to content

Commit 48a3dfc

Browse files
Gaussian tails experiment (#4)
* c(x) and right solver implmemented * left exp exp solver * fixed equation solvers and implemented various other parts * tp and tq sampling * Gamma_hat and other yet untested parts added * p and q sampling * Converted while loops to while_loops * First version that appears to work * Unit tests and fixes * Experiment draft * coupled truncated exponentials in functional paradigm + logspace * Made gauss tails residual using quick solver. * I think this works just fine. Let's just design a bunch of unittest, document and merge. * Make experiment use the "new" code. * Visibly some issue with the second marginal of the exponential coupling. * Finished refactoring and changed experiment * Finalise Gaussian tails experiment. * Some fixes on sampling tails + better test using p-value. * truncnorm was failing. * Fixed the Gaussian tails. * Started the reflected max example * Reflected mh (#3) * need to commit before holidays * This should be the reflected mh implemented. * Finalised MMALA example * add some replication * Update README.md Co-authored-by: Simo Särkkä <simo.sarkka@aalto.fi>
1 parent 3f7de84 commit 48a3dfc

5 files changed

Lines changed: 403 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Otherwise, simply download the repository and run `pip .`.
1414

1515
### Example
1616

17-
See the `examples` and `tests` folders.
17+
See the `examples` and `tests` folders. The examples implement the paper illustrations, while the tests test single functions behaviours and are a bit more low level.
1818

1919

2020
### References
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# Coupled sampling from 2 different tails x > mu and x > eta of N(x | 0,1) Gaussian
2+
# by using a maximal coupling of translated exponential proposals hatp(x) = alpha exp(-alpha (x - mu))
3+
# and hatq(x) = beta exp(-beta (x - eta)) similarly to Robert (1995), but with the coupling.
4+
#
5+
# Robert, C. P. (1995). Simulation of truncated normal variables. Statistics and Computing, Volume 5, pages 121–125.
6+
import chex as chex
7+
import jax.lax
8+
import jax.numpy as jnp
9+
import jax.random
10+
import tensorflow_probability.substrates.jax as tfp
11+
from jax.experimental.host_callback import id_print
12+
13+
from coupled_rejection_sampling.utils import logsubexp, log1mexp
14+
15+
16+
@jax.jit
17+
def coupled_gaussian_tails(key: chex.PRNGKey,
18+
mu: chex.Numeric, eta: chex.Numeric):
19+
"""
20+
A coupled version of Robert's truncated normal sampling algorithm.
21+
We want to sample from 2 different tails x > mu and x > eta of a N(x | 0,1) Gaussian
22+
23+
Parameters
24+
----------
25+
key: jnp.ndarray
26+
JAX random key
27+
mu, eta: float
28+
The tails of the Gaussians we want to sample from
29+
30+
Returns
31+
-------
32+
X: chex.Numeric
33+
The sample from x > mu
34+
Y: chex.Numeric
35+
The sample from x > eta
36+
is_coupled: jnp.ndarray
37+
Coupling flag
38+
"""
39+
alpha_mu = get_alpha(mu)
40+
alpha_eta = get_alpha(eta)
41+
42+
p = lambda k: _robert_sampler(k, mu, alpha_mu)
43+
q = lambda k: _robert_sampler(k, eta, alpha_eta)
44+
45+
log_w_p = lambda x: -0.5 * (x - alpha_mu) ** 2
46+
log_w_q = lambda x: -0.5 * (x - alpha_eta) ** 2
47+
48+
def cond(carry):
49+
accept_X, accept_Y, *_ = carry
50+
return ~accept_X & ~accept_Y
51+
52+
def body(carry):
53+
*_, i, curr_key = carry
54+
next_key, sample_key, accept_key = jax.random.split(curr_key, 3)
55+
X_hat, Y_hat, are_coupled = coupled_exponentials(sample_key, mu, alpha_mu, eta, alpha_eta)
56+
log_w_X = log_w_p(X_hat)
57+
log_w_Y = log_w_q(Y_hat)
58+
59+
log_u = jnp.log(jax.random.uniform(accept_key))
60+
accept_X = log_u < log_w_X
61+
accept_Y = log_u < log_w_Y
62+
63+
return accept_X, accept_Y, X_hat, Y_hat, are_coupled, i + 1, next_key
64+
65+
# initialisation
66+
residual_key, loop_key = jax.random.split(key)
67+
68+
output = jax.lax.while_loop(cond,
69+
lambda carry: body(carry),
70+
(False, False, 0., 0., False, 0, loop_key))
71+
72+
is_X_accepted, is_Y_accepted, X, Y, is_coupled, n_trials, _ = output
73+
74+
X = jax.lax.cond(is_X_accepted, lambda _: X, p, residual_key)
75+
Y = jax.lax.cond(is_Y_accepted, lambda _: Y, q, residual_key)
76+
77+
is_coupled = is_coupled & is_X_accepted & is_Y_accepted
78+
79+
return X, Y, is_coupled
80+
81+
82+
@jax.jit
83+
def coupled_exponentials(key:chex.PRNGKey, mu:chex.Numeric, alpha_mu:chex.Numeric, eta:chex.Numeric, alpha_eta:chex.Numeric):
84+
"""
85+
Sampling from a maximal coupling of shifted exponentials.
86+
p(x) = exp(-alpha (x - m)) / m for x >= m, 0 otherwise
87+
88+
It assumes that eta > mu and alpha_eta > alpha_mu
89+
90+
Parameters
91+
----------
92+
key: chex.PRNGKey
93+
JAX random key
94+
mu, eta: chex.Numeric
95+
The shift of the exponentials
96+
alpha_mu, alpha_eta: chex.Numeric
97+
The rate parameters
98+
99+
Returns
100+
-------
101+
X: chex.Numeric
102+
The sample from x > mu
103+
Y: chex.Numeric
104+
The sample from x > eta
105+
is_coupled: chex.Numeric
106+
Coupling flag
107+
"""
108+
109+
gamma = get_gamma(mu, eta, alpha_mu, alpha_eta)
110+
111+
eta_mu = -alpha_mu * (eta - mu)
112+
gamma_mu = -alpha_mu * (gamma - mu)
113+
gamma_eta = -alpha_eta * (gamma - eta)
114+
115+
log_max_coupling_proba = logsubexp(eta_mu, jnp.logaddexp(gamma_mu, gamma_eta))
116+
117+
subkey1, subkey2 = jax.random.split(key)
118+
119+
log_u = jnp.log(jax.random.uniform(subkey1, shape=()))
120+
are_coupled = (log_u <= log_max_coupling_proba)
121+
122+
def if_coupled(k):
123+
x = _sampled_from_coupled_exponentials(k, mu, eta, alpha_mu, alpha_eta, eta_mu, gamma_mu, gamma_eta, gamma)
124+
return x, x
125+
126+
def otherwise(k):
127+
x = _sample_from_first_marginal(k, mu, eta, alpha_mu, alpha_eta, eta_mu, gamma_mu, gamma_eta, gamma)
128+
y = _sample_from_second_marginal(k, mu, eta, alpha_mu, alpha_eta, eta_mu, gamma_mu, gamma_eta, gamma)
129+
return x, y
130+
131+
x_out, y_out = jax.lax.cond(are_coupled, if_coupled, otherwise, subkey2)
132+
return x_out, y_out, are_coupled
133+
134+
135+
def _sampled_from_coupled_exponentials(key, mu, _eta, alpha_mu, alpha_eta, eta_mu, gamma_mu, gamma_eta, gamma):
136+
def C1_inv(log_u):
137+
return mu - logsubexp(eta_mu, log_u + logsubexp(eta_mu, gamma_mu)) / alpha_mu
138+
139+
def C2_inv(log_u):
140+
return gamma - log_u / alpha_eta
141+
142+
log_p1 = logsubexp(eta_mu, gamma_mu)
143+
log_p2 = gamma_eta
144+
log_p = log_p1 - jnp.logaddexp(log_p1, log_p2)
145+
146+
log_u1, log_u2 = jnp.log(jax.random.uniform(key, shape=(2,)))
147+
148+
res = jax.lax.cond(log_u1 < log_p, C1_inv, C2_inv, log_u2)
149+
return res
150+
151+
152+
def _sample_from_first_marginal(key, mu, eta, alpha_mu, alpha_eta, eta_mu, gamma_mu, gamma_eta, gamma):
153+
key1, key2 = jax.random.split(key, 2)
154+
log_u1 = jnp.log(jax.random.uniform(key1))
155+
156+
log_p1 = logsubexp(gamma_mu, gamma_eta) # This has the same value as $\log(\tilde{Z})$
157+
log_p2 = log1mexp(eta_mu)
158+
log_p = log_p1 - jnp.logaddexp(log_p1, log_p2)
159+
160+
def _sample_from_tail(log_u):
161+
return mu - log1mexp(log_u + log1mexp(eta_mu)) / alpha_mu
162+
163+
def _sample_from_overlap(log_u):
164+
165+
def log_f(x):
166+
return logsubexp(-alpha_mu * (x - mu), -alpha_eta * (x - eta)) - log_p1 - log_u
167+
168+
# upper bound for the solution is given by a lower bounding of the density
169+
def upper_bound_loop(carry):
170+
171+
curr_upper_bound, _ = carry
172+
curr_upper_bound = 1.5 * curr_upper_bound
173+
obj = log_f(curr_upper_bound)
174+
return curr_upper_bound, obj >= 0
175+
176+
upper_bound, _ = jax.lax.while_loop(lambda carry: carry[-1], upper_bound_loop, (gamma, True))
177+
178+
res, objective_at_estimated_root, *_ = tfp.math.find_root_chandrupatla(log_f, gamma, upper_bound,
179+
position_tolerance=1e-6,
180+
value_tolerance=1e-6)
181+
182+
return res
183+
184+
return jax.lax.cond(log_u1 < log_p, _sample_from_overlap, _sample_from_tail, jnp.log(jax.random.uniform(key2)))
185+
186+
187+
def _sample_from_second_marginal(key, mu, eta, alpha_mu, alpha_eta, eta_mu, gamma_mu, gamma_eta, gamma):
188+
log_Zq_1 = jnp.logaddexp(0, gamma_mu) # log(1 + exp(gamma_mu))
189+
log_Zq_2 = jnp.logaddexp(eta_mu, gamma_eta)
190+
log_Zq = logsubexp(log_Zq_1, log_Zq_2)
191+
log_u = jnp.log(jax.random.uniform(key))
192+
193+
def log_f(x):
194+
res_1 = jnp.logaddexp(0, -alpha_mu * (x - mu)) # log(1 + exp(...))
195+
res_2 = jnp.logaddexp(eta_mu, -alpha_eta * (x - eta))
196+
res = logsubexp(res_1, res_2)
197+
res = res - log_Zq - log_u
198+
return res
199+
200+
out, objective_at_estimated_root, *_ = tfp.math.find_root_chandrupatla(log_f, eta, gamma, position_tolerance=1e-6,
201+
value_tolerance=1e-6)
202+
203+
return out
204+
205+
206+
def _robert_sampler(key, mu, alpha):
207+
def body(carry):
208+
curr_k, *_ = carry
209+
curr_k, subkey = jax.random.split(curr_k, 2)
210+
211+
u1, u2 = jax.random.uniform(subkey, shape=(2,))
212+
213+
x = mu - jnp.log(1 - u1) / alpha
214+
accepted = u2 <= jnp.exp(-0.5 * (x - alpha) ** 2)
215+
216+
return curr_k, x, accepted
217+
218+
_, x_out, _ = jax.lax.while_loop(lambda carry: ~carry[-1], body, (key, 0., False))
219+
return x_out
220+
221+
222+
def get_alpha(mu):
223+
""" Compute the optimal alpha as per Robert (1995) """
224+
return 0.5 * (mu + jnp.sqrt(mu ** 2 + 4))
225+
226+
227+
def get_gamma(mu, eta, alpha, beta):
228+
""" Threshold when hatp(x) = hatq(x) """
229+
return (jnp.log(beta) - jnp.log(alpha) + beta * eta - alpha * mu) / (beta - alpha)
230+
231+
232+
def texp_logpdf(x, mu, alpha):
233+
""" Translated exponential density """
234+
return jnp.where(x < mu, -jnp.inf, jnp.log(alpha) - alpha * (x - mu))

examples/gaussian_tail_coupling.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Example of coupling of tails of N(0,1) Gaussian distributions.
3+
"""
4+
import time
5+
6+
import jax.lax
7+
import jax.random
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
import scipy.integrate
11+
import tikzplotlib
12+
import tqdm
13+
from scipy.stats import truncnorm
14+
import seaborn as sns
15+
from coupled_rejection_sampling.gauss_tails import coupled_gaussian_tails
16+
17+
RUN = False
18+
PLOT = True
19+
20+
save_path = "out/gauss_tails.npz"
21+
22+
23+
if RUN:
24+
JAX_KEY = jax.random.PRNGKey(0)
25+
26+
# Tails >5 is considered "hard" and the closer these are, the worse Thorisson's method is
27+
mu = 6.
28+
29+
M = 100_000 # Samples to draw
30+
DELTAS = np.linspace(1e-6, 0.5, num=200)
31+
32+
# Compute maximal coupling probability (note that this can be inaccurate)
33+
p = lambda x: truncnorm.pdf(x, mu, np.inf)
34+
35+
def experiment():
36+
pxy_list = np.empty((len(DELTAS), 2))
37+
x_samples = np.empty((len(DELTAS), M))
38+
y_samples = np.empty((len(DELTAS), M))
39+
runtimes = np.empty((len(DELTAS),))
40+
41+
keys = jax.random.split(JAX_KEY, M)
42+
sampler = jax.jit(jax.vmap(coupled_gaussian_tails, in_axes=[0, None, None]))
43+
# compilation run
44+
*_, acc = sampler(keys, mu, mu + DELTAS[0])
45+
acc.block_until_ready()
46+
47+
for n in tqdm.trange(len(DELTAS)):
48+
delta = DELTAS[n]
49+
eta = mu + delta
50+
51+
q = lambda x: truncnorm.pdf(x, eta, np.inf)
52+
mpq = lambda x: np.minimum(p(x), q(x))
53+
true_pxy = scipy.integrate.quad(mpq, 0, np.inf)[0]
54+
55+
tic = time.time()
56+
x_samples[n], y_samples[n], acc = sampler(keys, mu, eta)
57+
acc.block_until_ready()
58+
toc = time.time()
59+
runtimes[n] = toc - tic
60+
pxy = np.mean(acc)
61+
pxy_list[n] = pxy, true_pxy
62+
63+
return x_samples, y_samples, pxy_list, runtimes
64+
65+
66+
x_samples, y_samples, pxy_list, runtimes = experiment()
67+
np.savez(save_path, x_samples=x_samples, y_samples=y_samples, pxy_list=pxy_list, M=M, etas=mu + DELTAS, mu=mu, runtimes=runtimes)
68+
69+
if PLOT:
70+
data = np.load(save_path)
71+
72+
x_samples = data["x_samples"]
73+
y_samples = data["y_samples"]
74+
pxy_list = data["pxy_list"]
75+
M = data["M"]
76+
77+
mu = data["mu"]
78+
etas = data["etas"]
79+
runtimes = data["runtimes"]
80+
xs = np.linspace(mu, 10, 1000)
81+
82+
fig = plt.figure(figsize=(10, 10))
83+
g = sns.jointplot(x=x_samples[10], y=y_samples[10], s=10)
84+
85+
plt.show()
86+
87+
fig, ax = plt.subplots()
88+
plt.title("Coupling probability as function of $\eta$")
89+
ax.semilogy(etas, pxy_list[:, 0], label='Actual coupling probability', linestyle="-", color="k")
90+
ax.semilogy(etas, pxy_list[:, 1], label='True coupling probability', linestyle="--", color="k")
91+
plt.xlabel("$\eta$")
92+
ax.legend()
93+
# plt.show()
94+
tikzplotlib.save("out/gaussian_tails_coupling.tikz")
95+
96+
97+
fig, ax = plt.subplots()
98+
plt.title("Run time as function of $\eta$")
99+
ax.plot(etas, runtimes, linestyle="-", color="k")
100+
plt.xlabel("$\eta$")
101+
plt.ylabel("Run time (s)")
102+
ax.legend()
103+
tikzplotlib.save("out/gaussian_tails_runtime.tikz")

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
chex
12
cvxpy
23
jax[cpu]
34
jupyter
@@ -9,5 +10,6 @@ pytest
910
pytest-mock
1011
scipy
1112
seaborn
13+
tensorflow_probability
1214
tikzplotlib
1315
tqdm

0 commit comments

Comments
 (0)