Skip to content

Commit 8170c84

Browse files
authored
Merge pull request #134 from ihincks/upgrade-vectorized-risk
Rewrote bayes_risk to allow multiple expparams
2 parents a7b12a5 + b9bd357 commit 8170c84

File tree

3 files changed

+252
-60
lines changed

3 files changed

+252
-60
lines changed

src/qinfer/derived_models.py

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
'DerivedModel',
4747
'PoisonedModel',
4848
'BinomialModel',
49+
'DifferentiableBinomialModel',
4950
'GaussianHyperparameterizedModel',
5051
'MultinomialModel',
5152
'MLEModel',

src/qinfer/smc.py

+86-60
Original file line numberDiff line numberDiff line change
@@ -552,88 +552,115 @@ def resample(self):
552552

553553
def bayes_risk(self, expparams):
554554
r"""
555-
Calculates the Bayes risk for a hypothetical experiment, assuming the
555+
Calculates the Bayes risk for hypothetical experiments, assuming the
556556
quadratic loss function defined by the current model's scale matrix
557557
(see :attr:`qinfer.abstract_model.Simulatable.Q`).
558558
559-
:param expparams: The experiment at which to compute the Bayes risk.
559+
:param expparams: The experiments at which to compute the risk.
560560
:type expparams: :class:`~numpy.ndarray` of dtype given by the current
561561
model's :attr:`~qinfer.abstract_model.Simulatable.expparams_dtype` property,
562562
and of shape ``(1,)``
563563
564-
:return float: The Bayes risk for the current posterior distribution
565-
of the hypothetical experiment ``expparams``.
564+
:return np.ndarray: The Bayes risk for the current posterior distribution
565+
at each hypothetical experiment in ``expparams``, therefore
566+
has shape ``(expparams.size,)``
566567
"""
567-
# This subroutine computes the bayes risk for a hypothetical experiment
568-
# defined by expparams.
569-
570-
# Assume expparams is a single experiment
571-
572-
# expparams =
573-
# Q = np array(Nmodelparams), which contains the diagonal part of the
574-
# rescaling matrix. Non-diagonal could also be considered, but
575-
# for the moment this is not implemented.
576-
nout = self.model.n_outcomes(expparams) # This is a vector so this won't work
577-
w, N = self.hypothetical_update(np.arange(nout), expparams, return_normalization=True)
578-
w = w[:, 0, :] # Fix w.shape == (n_outcomes, n_particles).
579-
N = N[:, :, 0] # Fix L.shape == (n_outcomes, n_particles).
580-
581-
xs = self.particle_locations.transpose([1, 0]) # shape (n_mp, n_particles).
582-
583-
# In the following, we will use the subscript convention that
584-
# "o" refers to an outcome, "p" to a particle, and
585-
# "i" to a model parameter.
586-
# Thus, mu[o,i] is the sum over all particles of w[o,p] * x[i,p].
587-
588-
mu = np.transpose(np.tensordot(w,xs,axes=(1,1)))
589-
var = (
590-
# This sum is a reduction over the particle index and thus
591-
# represents an expectation value over the diagonal of the
592-
# outer product $x . x^T$.
593-
594-
np.transpose(np.tensordot(w,xs**2,axes=(1,1)))
595-
# We finish by subracting from the above expectation value
596-
# the diagonal of the outer product $mu . mu^T$.
597-
- mu**2).T
598568

569+
# for models whose outcome number changes with experiment, we
570+
# take the easy way out and for-loop over experiments
571+
n_eps = expparams.size
572+
if n_eps > 1 and not self.model.is_n_outcomes_constant:
573+
risk = np.empty(n_eps)
574+
for idx in range(n_eps):
575+
risk[idx] = self.bayes_risk(expparams[idx, np.newaxis])
576+
return risk
577+
578+
# outcomes for the first experiment
579+
os = self.model.domain(expparams[0,np.newaxis])[0].values
580+
581+
# compute the hypothetical weights, likelihoods and normalizations for
582+
# every possible outcome and expparam
583+
# the likelihood over outcomes should sum to 1, so don't compute for last outcome
584+
w_hyp, L, N = self.hypothetical_update(
585+
os[:-1],
586+
expparams,
587+
return_normalization=True,
588+
return_likelihood=True
589+
)
590+
w_hyp_last_outcome = (1 - L.sum(axis=0)) * self.particle_weights[np.newaxis, :]
591+
N = np.concatenate([N[:,:,0], np.sum(w_hyp_last_outcome[np.newaxis,:,:], axis=2)], axis=0)
592+
w_hyp_last_outcome = w_hyp_last_outcome / N[-1,:,np.newaxis]
593+
w_hyp = np.concatenate([w_hyp, w_hyp_last_outcome[np.newaxis,:,:]], axis=0)
594+
# w_hyp.shape == (n_out, n_eps, n_particles)
595+
# N.shape == (n_out, n_eps)
596+
597+
# compute the hypothetical means and variances given outcomes and exparams
598+
# mu_hyp.shape == (n_out, n_eps, n_models)
599+
# var_hyp.shape == (n_out, n_eps)
600+
mu_hyp = np.dot(w_hyp, self.particle_locations)
601+
var_hyp = np.sum(
602+
w_hyp *
603+
np.sum(self.model.Q * (
604+
self.particle_locations[np.newaxis,np.newaxis,:,:] -
605+
mu_hyp[:,:,np.newaxis,:]
606+
) ** 2, axis=3),
607+
axis=2
608+
)
599609

600-
rescale_var = np.sum(self.model.Q * var, axis=1)
601-
# Q has shape (n_mp,), therefore rescale_var has shape (n_outcomes,).
602-
tot_norm = np.sum(N, axis=1)
603-
return np.dot(tot_norm.T, rescale_var)
610+
# the risk of a given expparam can be calculated as the mean posterior
611+
# variance weighted over all possible outcomes
612+
return np.sum(N * var_hyp, axis=0)
604613

605614
def expected_information_gain(self, expparams):
606615
r"""
607-
Calculates the expected information gain for a hypothetical experiment.
616+
Calculates the expected information gain for each hypothetical experiment.
608617
609-
:param expparams: The experiment at which to compute expected
618+
:param expparams: The experiments at which to compute expected
610619
information gain.
611620
:type expparams: :class:`~numpy.ndarray` of dtype given by the current
612621
model's :attr:`~qinfer.abstract_model.Simulatable.expparams_dtype` property,
613-
and of shape ``(1,)``
622+
and of shape ``(n,)``
614623
615-
:return float: The Bayes risk for the current posterior distribution
616-
of the hypothetical experiment ``expparams``.
624+
:return float: The expected information gain for each
625+
hypothetical experiment in ``expparams``.
617626
"""
618-
619-
nout = self.model.n_outcomes(expparams)
620-
w, N = self.hypothetical_update(np.arange(nout), expparams, return_normalization=True)
621-
w = w[:, 0, :] # Fix w.shape == (n_outcomes, n_particles).
622-
N = N[:, :, 0] # Fix N.shape == (n_outcomes, n_particles).
623-
624627
# This is a special case of the KL divergence estimator (see below),
625628
# in which the other distribution is guaranteed to share support.
626-
#
627-
# KLD[idx_outcome] = Sum over particles(self * log(self / other[idx_outcome])
628-
# Est. KLD = E[KLD[idx_outcome] | outcomes].
629+
630+
# for models whose outcome number changes with experiment, we
631+
# take the easy way out and for-loop over experiments
632+
n_eps = expparams.size
633+
if n_eps > 1 and not self.model.is_n_outcomes_constant:
634+
risk = np.empty(n_eps)
635+
for idx in range(n_eps):
636+
risk[idx] = self.expected_information_gain(expparams[idx, np.newaxis])
637+
return risk
638+
639+
# number of outcomes for the first experiment
640+
os = self.model.domain(expparams[0,np.newaxis])[0].values
641+
642+
# compute the hypothetical weights, likelihoods and normalizations for
643+
# every possible outcome and expparam
644+
# the likelihood over outcomes should sum to 1, so don't compute for last outcome
645+
w_hyp, L, N = self.hypothetical_update(
646+
os[:-1],
647+
expparams,
648+
return_normalization=True,
649+
return_likelihood=True
650+
)
651+
w_hyp_last_outcome = (1 - L.sum(axis=0)) * self.particle_weights[np.newaxis, :]
652+
N = np.concatenate([N[:,:,0], np.sum(w_hyp_last_outcome[np.newaxis,:,:], axis=2)], axis=0)
653+
w_hyp_last_outcome = w_hyp_last_outcome / N[-1,:,np.newaxis]
654+
w_hyp = np.concatenate([w_hyp, w_hyp_last_outcome[np.newaxis,:,:]], axis=0)
655+
# w_hyp.shape == (n_out, n_eps, n_particles)
656+
# N.shape == (n_out, n_eps)
629657

630-
KLD = np.sum(
631-
w * np.log(w / self.particle_weights ),
632-
axis=1 # Sum over particles.
633-
)
658+
# compute the Kullback-Liebler divergence for every experiment and possible outcome
659+
# KLD.shape == (n_out, n_eps)
660+
KLD = np.sum(w_hyp * np.log(w_hyp / self.particle_weights), axis=2)
634661

635-
tot_norm = np.sum(N, axis=1)
636-
return np.dot(tot_norm, KLD)
662+
# return the expected KLD (ie expected info gain) for every experiment
663+
return np.sum(N * KLD, axis=0)
637664

638665
## MISC METHODS ###########################################################
639666

@@ -1212,4 +1239,3 @@ def update(self, outcome, expparams,check_for_resample=True):
12121239

12131240
# We now can update as normal.
12141241
SMCUpdater.update(self, outcome, expparams,check_for_resample=check_for_resample)
1215-

src/qinfer/tests/test_metrics.py

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
##
4+
# test_metrics.py: Tests various metrics like risk and information gain.
5+
##
6+
# © 2014 Chris Ferrie ([email protected]) and
7+
# Christopher E. Granade ([email protected])
8+
#
9+
# This file is a part of the Qinfer project.
10+
# Licensed under the AGPL version 3.
11+
##
12+
# This program is free software: you can redistribute it and/or modify
13+
# it under the terms of the GNU Affero General Public License as published by
14+
# the Free Software Foundation, either version 3 of the License, or
15+
# (at your option) any later version.
16+
#
17+
# This program is distributed in the hope that it will be useful,
18+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
# GNU Affero General Public License for more details.
21+
#
22+
# You should have received a copy of the GNU Affero General Public License
23+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
##
25+
26+
## FEATURES ###################################################################
27+
28+
from __future__ import division # Ensures that a/b is always a float.
29+
from __future__ import absolute_import
30+
## IMPORTS ####################################################################
31+
32+
import numpy as np
33+
from numpy.testing import assert_equal, assert_almost_equal, assert_array_less,assert_approx_equal
34+
35+
from qinfer.tests.base_test import DerandomizedTestCase
36+
from qinfer import (BinomialModel,CoinModel,BetaDistribution,DifferentiableBinomialModel)
37+
38+
from qinfer.smc import SMCUpdater,SMCUpdaterBCRB
39+
40+
class TestBayesRisk(DerandomizedTestCase):
41+
# Test the implementation of numerical Bayes Risk by comparing to
42+
# numbers which were derived by doing analytic/numeric
43+
# integrals of simple models in Mathematica. This test trusts that
44+
# these calculations were done correctly.
45+
46+
ALPHA = 1.
47+
BETA = 3.
48+
PRIOR_BETA = BetaDistribution(alpha=ALPHA, beta=BETA)
49+
N_PARTICLES = 10000
50+
NMEAS_EXPPARAMS = np.arange(1, 11, dtype=int)
51+
52+
def setUp(self):
53+
54+
super(TestBayesRisk,self).setUp()
55+
56+
# Set up relevant models.
57+
self.coin_model = CoinModel()
58+
self.binomial_model = BinomialModel(self.coin_model)
59+
60+
# Set up updaters for these models using particle approximations
61+
# of conjugate priors
62+
self.updater_binomial = SMCUpdater(self.binomial_model,
63+
TestBayesRisk.N_PARTICLES,TestBayesRisk.PRIOR_BETA)
64+
65+
def test_finite_outcomes_risk(self):
66+
# The binomial model has a finite number of outcomes. Test the
67+
# risk calculation in this case.
68+
69+
expparams = self.NMEAS_EXPPARAMS.astype(self.binomial_model.expparams_dtype)
70+
71+
# estimate the risk
72+
est_risk = self.updater_binomial.bayes_risk(expparams)
73+
74+
# compute exact risk
75+
a, b = TestBayesRisk.ALPHA, TestBayesRisk.BETA
76+
exact_risk = a * b / ((a + b) * (a + b + 1) * (a + b + expparams['n_meas']))
77+
78+
# see if they roughly match
79+
assert_almost_equal(est_risk, exact_risk, decimal=3)
80+
81+
class TestInformationGain(DerandomizedTestCase):
82+
# Test the implementation of numerical information gain by comparing to
83+
# numbers which were derived by doing analytic/numeric
84+
# integrals of simple models (binomialm, poisson, and gaussian) in
85+
# Mathematica. This test trusts that these calculations
86+
# were done correctly.
87+
88+
ALPHA = 1
89+
BETA = 3
90+
PRIOR_BETA = BetaDistribution(alpha=ALPHA, beta=BETA)
91+
N_PARTICLES = 10000
92+
# Calculated in Mathematica, IG for the binomial model and the given expparams
93+
NMEAS_EXPPARAMS = np.arange(1, 11, dtype=int)
94+
BINOM_IG = np.array([0.104002,0.189223,0.261496,0.324283,0.379815,0.429613,0.474764,0.516069,0.554138,0.589446])
95+
96+
def setUp(self):
97+
98+
super(TestInformationGain,self).setUp()
99+
100+
# Set up relevant models.
101+
self.coin_model = CoinModel()
102+
self.binomial_model = BinomialModel(self.coin_model)
103+
104+
# Set up updaters for these models using particle approximations
105+
# of conjugate priors
106+
self.updater_binomial = SMCUpdater(self.binomial_model,
107+
TestInformationGain.N_PARTICLES,TestInformationGain.PRIOR_BETA)
108+
109+
110+
def test_finite_outcomes_ig(self):
111+
# The binomial model has a finite number of outcomes. Test the
112+
# ig calculation in this case.
113+
114+
expparams = self.NMEAS_EXPPARAMS.astype(self.binomial_model.expparams_dtype)
115+
116+
# estimate the information gain
117+
est_ig = self.updater_binomial.expected_information_gain(expparams)
118+
119+
# see if they roughly match
120+
assert_almost_equal(est_ig, TestInformationGain.BINOM_IG, decimal=2)
121+
122+
class TestFisherInformation(DerandomizedTestCase):
123+
# Test the implementation of numerical Fisher Information by comparing to
124+
# numbers which were derived by doing analytic/numeric
125+
# integrals of simple models (binomialm, poisson, and gaussian) in
126+
# Mathematica. This test trusts that these calculations
127+
# were done correctly.
128+
129+
ALPHA = 1
130+
BETA = 3
131+
PRIOR_BETA = BetaDistribution(alpha=ALPHA, beta=BETA)
132+
N_PARTICLES = 10000
133+
134+
BIN_FI_MODELPARAMS = np.linspace(0.01,0.99,5)
135+
NMEAS_EXPPARAMS = np.arange(1, 11, dtype=int)
136+
137+
def setUp(self):
138+
139+
super(TestFisherInformation,self).setUp()
140+
141+
# Set up relevant models.
142+
self.coin_model = CoinModel()
143+
self.binomial_model = DifferentiableBinomialModel(self.coin_model)
144+
145+
# Set up updaters for these models using particle approximations
146+
# of conjugate priors
147+
self.updater_binomial = SMCUpdater(self.binomial_model,
148+
TestFisherInformation.N_PARTICLES,TestFisherInformation.PRIOR_BETA)
149+
150+
151+
def test_finite_outcomes_fi(self):
152+
# The binomial model has a finite number of outcomes. Test the
153+
# ig calculation in this case.
154+
155+
expparams = self.NMEAS_EXPPARAMS.astype(self.binomial_model.expparams_dtype)
156+
p = TestFisherInformation.BIN_FI_MODELPARAMS
157+
# estimate the information gain
158+
est_fi = self.binomial_model.fisher_information(TestFisherInformation.BIN_FI_MODELPARAMS,expparams)[0,0]
159+
p = p[:,np.newaxis]
160+
n = expparams.astype(np.float32)[np.newaxis,:]
161+
162+
exact_fi = n/((1-p)*p)
163+
# see if they roughly match
164+
165+
assert_almost_equal(est_fi,exact_fi, decimal=3)

0 commit comments

Comments
 (0)