Skip to content

Commit 15c7601

Browse files
committed
added ISE and NISE metrics for 2D densities
1 parent 214366b commit 15c7601

File tree

3 files changed

+165
-1
lines changed

3 files changed

+165
-1
lines changed

pyest/gm/gm.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
'v_eval_mvnpdfchol',
3232
'integral_gauss_product',
3333
'integral_gauss_product_chol',
34+
'integral_squared_gm',
3435
'marginal_2d',
3536
'marginal_nd',
3637
'comp_bounds',
@@ -463,6 +464,24 @@ def integral_gauss_product(m1, P1, m2, P2, allow_singular=False):
463464
return multivariate_normal.pdf(m1, m2, P1 + P2, allow_singular=allow_singular)
464465

465466

467+
def integral_squared_gm(p):
468+
''' compute integral of squared Gaussian mixture
469+
470+
Parameters
471+
----------
472+
p : GaussianMixture
473+
Gaussian mixture
474+
475+
Returns
476+
-------
477+
integral : float
478+
integral of the squared Gaussian mixture
479+
'''
480+
return np.sum([
481+
wi*wj*eval_mvnpdf(mi, mj, Pi + Pj) for (wi, mi, Pi) in p for (wj, mj, Pj) in p
482+
])
483+
484+
466485
def marginal_2d(m, P, dimensions=[0, 1]):
467486
""" compute 2D marginal of GM"""
468487
return marginal_nd(m, P, dimensions)

pyest/metrics.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22
import pyest.gm as pygm
33
from scipy.linalg import solve_triangular
4+
from scipy.integrate import dblquad
45

56

67
def l2_dist(p1, p2):
@@ -87,4 +88,106 @@ def madem(m, S, m_ref):
8788
"""
8889
return np.linalg.norm(
8990
solve_triangular(S, m - m_ref, lower=True)
90-
)
91+
)
92+
93+
94+
def integral_squared_error_2d(p1, p2, a, b, c, d, epsabs=1.49e-2, epsrel=1.49e-2):
95+
''' compute integral squared error between two 2D densities
96+
97+
Parameters
98+
----------
99+
p1: callable
100+
first density, p1([x,y])
101+
p2: callable
102+
second density, p2([x,y])
103+
a, b : float
104+
The limits of integration in x: a<b
105+
c, d : float
106+
The limits of integration in y: c<d
107+
epsabs : float, optional
108+
absolute error tolerance for numerical integration
109+
epsrel : float, optional
110+
relative error tolerance for numerical integration
111+
112+
113+
Returns
114+
-------
115+
ise: foat
116+
kld
117+
int_error:
118+
numerical integration estimated error
119+
120+
See Also
121+
--------
122+
normalized_integral_squared_error_2d : compute normalized integral squared
123+
error between two 2D densities
124+
l2_dist : compute L2 distance (ISE) between two Gaussian mixtures
125+
126+
Notes
127+
-----
128+
This function is intended for use with generic callable densities and
129+
makes no assumptions about the form of the densities. If both p1 and p2
130+
are Gaussian mixtures, use l2_dist instead, which is exact and more
131+
efficient.
132+
'''
133+
integrand_fun = lambda y,x: (p1([x,y])-p2([x,y]))**2
134+
return dblquad(integrand_fun, a, b, c, d, epsabs=epsabs, epsrel=epsrel)
135+
136+
137+
def normalized_integral_squared_error_2d(p1, p2, a, b, c, d, epsabs=1.49e-2, epsrel=1.49e-2):
138+
''' compute normalized integral squared error between two 2D, densities
139+
140+
Parameters
141+
----------
142+
p1: callable
143+
first density
144+
p2: callable
145+
second density
146+
a, b : float
147+
The limits of integration in x: a<b
148+
c, d : float
149+
The limits of integration in y: c<d
150+
epsabs : float, optional
151+
absolute error tolerance for numerical integration
152+
epsrel : float, optional
153+
relative error tolerance for numerical integration
154+
155+
Returns
156+
-------
157+
nise: float
158+
normalized integral squared error
159+
ise:
160+
integral squared error
161+
err:
162+
numerical integration estimated error in ISE computation
163+
164+
See Also
165+
--------
166+
integral_squared_error_2d : compute integral squared error between two
167+
2D densities
168+
l2_dist : compute L2 distance (ISE) between two Gaussian mixtures
169+
170+
Notes
171+
-----
172+
This function is intended for use with generic callable densities and
173+
makes no assumptions about the form of the densities. If both p1 and p2
174+
are Gaussian mixtures, use metrics.l2_dist and gm.integral_squared_gm
175+
instead for the numerator and denominator terms separately, which is
176+
exact and more efficient.
177+
178+
'''
179+
ise, err = integral_squared_error_2d(p1, p2, a, b, c, d, epsabs=epsabs, epsrel=epsrel)
180+
# if p1 is a GaussianMixtureRv, use l2_dist
181+
if isinstance(p1, pygm.GaussianMixture):
182+
int_p1_sq = pygm.integral_squared_gm(p1)
183+
else:
184+
p1_sq_integrand_fun = lambda y,x: p1([x,y])**2
185+
int_p1_sq = dblquad(p1_sq_integrand_fun, a, b, c, d, epsabs=epsabs, epsrel=epsrel)[0]
186+
if isinstance(p2, pygm.GaussianMixture):
187+
int_p2_sq = pygm.integral_squared_gm(p2)
188+
else:
189+
p2_sq_integrand_fun = lambda y,x: p2([x,y])**2
190+
int_p2_sq = dblquad(p2_sq_integrand_fun, a, b, c, d, epsabs=epsabs, epsrel=epsrel)[0]
191+
nise = ise/(int_p1_sq + int_p2_sq)
192+
193+
return nise, ise, err

tests/test_metrics.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,48 @@ def test_l2_dist():
2222
npt.assert_approx_equal(gm.l2_dist(gm1, gm3), des_l2, significant=9)
2323

2424

25+
def test_integral_squared_error_2d():
26+
""" test computation of integral squared error between 2D densities """
27+
28+
# test identical mixtures
29+
p1 = gm.defaults.default_gm().marginal_2d([0, 1])
30+
p2 = gm.defaults.default_gm().marginal_2d([0, 1])
31+
lb, ub = gm.bounds(p1.m, p1.P, sigma_mult=3)
32+
33+
ise, int_err = metrics.integral_squared_error_2d(p1, p2, lb[0], ub[0], lb[1], ub[1])
34+
assert abs(ise) < 1e-10
35+
36+
# now, create two mixtures with disjoint supports. The ISE in this case
37+
# should trend toward the sum of the individual integrals of the squared densities.
38+
shift = ub - lb
39+
p2 = gm.defaults.default_gm(mean_shift=np.array([shift[0], shift[1], 0, 0])).marginal_2d([0, 1])
40+
ise, int_err = metrics.integral_squared_error_2d(p1, p2, lb[0], ub[0] + shift[0], lb[1], ub[1] + shift[1])
41+
int_p1_sq = gm.integral_squared_gm(p1)
42+
int_p2_sq = gm.integral_squared_gm(p2)
43+
des_ise = int_p1_sq + int_p2_sq
44+
assert abs(ise - des_ise) < 1e-5
45+
46+
47+
def test_normalized_integral_squared_error_2d():
48+
""" test computation of NISE between 2D densities """
49+
50+
# test identical mixtures
51+
p1 = gm.defaults.default_gm().marginal_2d([0, 1])
52+
p2 = gm.defaults.default_gm().marginal_2d([0, 1])
53+
lb, ub = gm.bounds(p1.m, p1.P, sigma_mult=3)
54+
55+
nise, ise, int_err = metrics.normalized_integral_squared_error_2d(p1, p2, lb[0], ub[0], lb[1], ub[1])
56+
assert abs(nise) < 1e-10
57+
58+
# now, create two mixtures with disjoint supports. The NISE in this case
59+
# should trend toward 1
60+
shift = ub - lb
61+
p2 = gm.defaults.default_gm(mean_shift=np.array([shift[0], shift[1], 0, 0])).marginal_2d([0, 1])
62+
nise, ise, int_err = metrics.normalized_integral_squared_error_2d(p1, p2, lb[0], ub[0] + shift[0], lb[1], ub[1] + shift[1])
63+
des_nise = 1
64+
assert abs(nise - des_nise) < 1e-4
65+
66+
2567

2668
if __name__ == '__main__':
2769
pytest.main([__file__])

0 commit comments

Comments
 (0)