Optimal Experiment Design for WebPPL models
- WebPPL (v0.80+)
In OED, models are a conditional distribution on possible responses y
for a
given experiment x
. In WebPPL, we implement this as a function that takes two
arguments, an experiment x
and a response y
, and returns the log-likelihood
of the response given the experiment.
For OED to work correctly, the sum of
the model's probabilities of the responses for each experiment should equal 1!
This is often ensured by deferring to the scoring functions of builtin distributions
(e.g. Binomial
). Another possibility, if the response space is not terribly
large, is to compute the probability tables beforehand and ensure the results
are sensible.
A strategy for modeling independent participants in experimental scenarios is
to first define a generative model for the (probabilistic) behavior of one
individual. Specifically, write a function that accepts an experiment x
and
samples from the space of responses y
given their likelihoods considering
x
. Then it should be possible to create a linking function that aggregates
multiple independent participants and describes the behavior of an experimental
group as a more general distribution (e.g. Binomial
). This is demonstrated
in examples/coin.wppl
with the groupify
linking function.
Once you have defined your models, you should assign them a name via the
Model
constructor. For example:
// Imagine the possible responses to an experiment are the numbers {0, 1,
// ... 20}. Thus "null" model weights each response equally, irrespective
// of the experiment.
var nullGroup = Model('nullGroup', function(x, y) {
return randomInteger({n: 21}).score(y);
});
All Model
does is return the same function after defining its name
property
(you could also do this with Object.defineProperty
). In cases where you are
programmatically creating multiple models as anonymous functions, your models
may not have useful names. Using Model
is necessary for OED to work
correctly, and ensures that OED (and you!) can tell your models apart.
Given that your definition of the model space in OED
usually consists of a
draw from an array of models, the shorthand function Models
is available to
declare models in an object and collapse them into an array, using the keys of
the object as names:
var models = Models({
m1: function(x, y) { ... },
m2: function(x, y) { ... },
...
}); // => Returns an array of functions, named 'm1', 'm2', etc.
OED
accepts an args
argument with 3 required properties: the model sampling
function M
, the experiment sampling function X
, and the response sampling
function Y
.
M
and X
should take no arguments, but the response prior Y
can
(optionally) be a function of a sampled experiment x
, if you have different
dependent measures in the experiment space (see Multiple dependent
measures). M
, X
, and Y
should, when called,
return a sampled item from the corresponding space. Usually, your prior on M
,
X
, and Y
will be uniform: as a result, your sampling function should
produce each model/experiment/response with equal probability (e.g. via
uniformDraw
or flip
).
New: instead of specifying a model sampling function e.g.
M
, you can specify a model prior,mPrior
, which will overrideM
. You should provide a model prior that has been given to you previously viaupdatePosterior
or a similar function.
In other cases, the search spaces are not enumerable. Thus OED
allows
performing inference on the spaces with the inference techniques built into
WebPPL. You can specify any or all of the inference methods with the optional
infer
property of the args
object, itself an object mapping keys to
inference functions. If a specific inference method is not specified, OED
will default to Enumerate
.
There is one other option: usePredictiveY
. This weights the Y
prior by the
predictions of the promising models in the model space. This is a good
option for when you believe the model space contains the true model, as it will
give experiment suggestions that hone in more quickly to the true model.
These options are displayed in the following example:
// Assume a trivial experiment scenario where the numbers 0-20 are experiments,
// and the same numbers are also responses.
OED({
M: function() { uniformDraw([m1, m2, ...]) }, // Sampling from model prior
mPrior: Marginal({ ... }), // Optional: specify a concrete model prior
X: function() { randomInteger(21) }, // Sampling from experiment prior
Y: function(x) { randomInteger(21) }, // Sampling from response prior.
infer: {
M1: Enumerate, // Inference for model prior
M2: Enumerate, // Inference for model posterior
X: Enumerate, // Inference for experiments prior
// Inference for response prior (as an example, using MCMC)
Y: function(thunk) {
Infer({method: 'MCMC', samples: 5000}, thunk)
}
},
usePredictiveY: true // Use the model predictive prior on responses?
});
OED
returns a marginal distribution on experiments. However, the probabilities
of the experiments are meaningless. What you care about is the support of the
distribution, which contains the EIG values.
var eigDist = OED({ ... }).support();
In particular, each element of the
support of the EIG
distribution is an object with two properties:
- x:
experiment
. A specific experiment (sampled from theX
function). - EIG:
number
. The expected information gain from the experiment, defined by the expected Kullback-Leibler divergence (KL-divergence) between the model posterior and the model prior, conditioning on all possible resultsy
for the experimentx
.
If you are interested in the best experiment, use the utility function
getBestExpt
, described later.
Note that
OED
is an alias forEIG
.
If your experiment space contains qualitatively different experiments (i.e.
dependent measures [DMs]), you can update your response space for each experiment by
having Y
accept a sampled experiment. In order to have Y
behave differently
for different DMs x
, X
should return an experiment with some kind of
distinguishing attribute (e.g. an object with a type
property). As an
example, consider two kinds of experiments you could run, a "categorical"
experiment for which participants answer yes/no, and a "continuous" experiment
where participants return a number in the interval [0, 1]:
var X = function() {
var type = uniformDraw(['categorical', 'continuous']);
// Sample a categorical or continuous experiment
var data = (type === 'categorical') ?
sampleCategorical() : sampleContinuous();
return {
type: type,
data: data
};
};
var Y = function(x) {
if (x.type === 'categorical') {
// e.g. a "true or false" response
return flip();
} else {
// e.g. a response on the interval [0, 1]
return sample(Uniform({a: 0, b: 1}));
}
};
Similarly, your model functions should return different scores depending on the experiment type:
var M = function(x, y) {
if (x.type === 'categorical') {
// Score y response assuming y is either true or false
} else {
// Score y response assuming y is a number in [0, 1]
}
};
This allows EIG to be calculated independently using the different samples.
If you do not have multiple dependent measures, then you may simply ignore
the single argument x
of Y
, or, since JavaScript is not very strict, omit
the parameter altogether.
To calculate the actual information gain from an experiment, use AIG
. It
accepts a subset of the arguments supplied to OED
:
AIG({
M: function() { uniformDraw([m1, m2, ...]) },
mPrior: Marginal({ ... }), // Optional
x: 15, // LITTLE x: the experiment tested
y: 20, // LITTLE y: the observed response,
infer: {
// Since x and y are given, no infer.X, infer.Y
M1: Enumerate,
M2: Enumerate
}
// No usePredictiveY, since y is given.
});
AIG
returns a single number, which is the information gain defined by the
actual KL-divergence
between the model posterior and the model prior after conditioning on the given
experiment and observed response.
If you wish to obtain the actual updated model posterior from a specific
experiment and observed response, use updatePosterior
rather than AIG, which
accepts the same arguments as AIG
.
updatePosterior
returns an object with two properties:
- mPosterior:
Marginal({ ... })
. The updated model distribution. - AIG:
number
. AIG observed from the experiment, as above.
Notice that AIG
just calls updatePosterior
and returns only its AIG
property. Thus, if you plan on using both AIG
and updatePosterior
, use only
updatePosterior
to save computation time.
getBestExpt
accepts an array of experiments with x
and EIG
properties,
such as one returned from the support of an OED
call.
var bestExpt = getBestExpt(eigDist.support());
The best experiment, i.e. the one with the highest EIG
, breaking ties
arbitrarily.
examples/coin.wppl
is a good, approachable introduction to the methods
defined in this package. It sets up the sequence prediction problem introduced
in the original OED paper. To get the best experiment for 3-way comparison,
uncomment the EIG
call in the last lines of the example.
webppl examples/coin.wppl --require .