A foerster model is a spindel spin
that calls three effects: sample, observe and factor (and, to record
what it computes, deterministic). Everything else is ordinary Clojure.
(require '[org.replikativ.foerster.effects :refer [sample observe factor]]
'[org.replikativ.foerster.dist :as dist]
'[org.replikativ.spindel.spin.cps :refer [spin]])
(defn model []
(spin
(let [mu (sample (dist/normal 0.0 10.0) :id :mu)]
(observe (dist/normal mu 1.0) 1.3)
(factor (if (pos? mu) 0.0 -1.0))
mu)))
The value of the spin — here mu — is what the posterior is over.
| Effect | Meaning |
|---|---|
(sample d & opts) | a latent value drawn from distribution d |
(observe d v & opts) | the data v, scored under d (log d(v) enters the weight); evaluates to v |
(factor w) | multiplies the weight by exp(w): a score that is not the density of a value (a soft constraint, a reward, a likelihood computed elsewhere) |
(factor w :barrier true) | the same, and SMC resamples there as at an observation (scored steps; see algorithms) |
(deterministic v :id a) | records v, a value computed from the choices, in the trace under a; no randomness, no weight; evaluates to v |
A deterministic site makes a derived quantity part of every particle's
trace, so its posterior can be read without returning it from the model:
(m/measure-stats measure #(m/site-value % :bmi)) (see
posteriors). Traces that carry what a model computed are also
the training data of learned proposals (foerster.learn).
Options of sample and observe:
| Option | Meaning |
|---|---|
:id | the site's name, its address (see below) |
:init v | the first state of a Markov chain starts the site at v (a block that cannot be drawn from needs it) |
:proposal q | sample only: under inference a fresh value is drawn from the distribution q instead of d, and the weight takes log d(v) − log q(v) |
:stream true | the site's value arrives from outside, pushed by smc/stream |
A :proposal is a guide the program names: a distribution it computes, from
its data for instance, which is how an amortized guide plugs in. The target
stays the program's posterior; a good guide only raises the effective sample
size. It applies to fresh draws: a replayed or kept value, a Markov chain's
move, a constraint, :init and a policy's :draw take precedence.
(sample (dist/normal 0.0 1.0) :id :mu :proposal (dist/normal 0.8 0.6))
To draw from the prior, run a model under gfi/simulate (see the
programmable inference notebook)
or importance sampling, which also record the trace. A model run with no
inference at all — deref'd in a bare world — simulates forward too: every
site draws afresh, observations add to the world's [:inference :log-weight],
and nothing is recorded.
Distributions are in foerster.dist.
Every site has an address, the name under which its value is recorded in
the trace, constrained (gfi/generate), intervened on or proposed. The rules
below hold under inference (every algorithm, gfi, counterfactual) and in
a bare forward run alike.
:id is addressed by it. A vector id is a hierarchical name:
:id [:step 3 :x].:id, the address is structural: the site's source location,
how often it was reached in the current run of its spin, and the
surrounding scope. It does not depend on which other sites ran before, so
a site keeps its address when control flow upstream changes — which MCMC
over programs with branches needs.with-scope (spindel engine.addressing) prefixes the names of the sites
inside it: in a loop, (with-scope [:step i] (sample d :id :x)) addresses
[:step 0 :x], [:step 1 :x], … — Gen's :step => i => :x.An :id must be unique within a run: a site reached twice under one name
is an error ("Duplicate site address"). In a loop, use a vector id that
includes the loop index, :id [:y i], or with-scope.
Name the sites you will refer to — to constrain, intervene on or read from a trace. Naming matters for reproducibility too: an unnamed site's address includes the identity of its spin, and a spin created again in the same world gets a new one, so its random stream changes (see reproducibility).
org.replikativ.foerster.process keeps state in the particle's world, so
it forks with the particle (Anglican's mem and Chinese restaurant
process):
(let [mean-of (process/mem (fn [k] (spin (sample (dist/normal 0 10) :id [:mean k]))))]
(spin
(let [k (await (process/crp-draw :clusters 1.0 :id [:z i])) ; a table index
mu (await (mean-of k))] ; drawn once per table
(observe (dist/normal mu 1.0) y :id [:y i]))))
mem computes each argument list once per world: later calls, in the same
particle and in particles forked after it, return the first value. Name the
sites inside a memoized function by its arguments. crp-draw seats a
customer at an existing table ∝ its customers or a new one ∝ α; it is an
ordinary sample site over finitely many tables, so exact enumeration
handles it too.
(infer/conditional model opts) runs an inner inference (any infer/infer
method) in fresh worlds and resolves its posterior over the inner
program's value as a distribution, which the outer program samples from or
observes against — reasoning about another agent's reasoning, as Anglican's
conditional did:
(let [their-belief (await (infer/conditional (inner-model evidence) {:method :enumerate}))
x (sample their-belief :id :their-guess)]
…)
Under exact enumeration the inner distribution is exact, and so the whole
nested model can be enumerated exactly; otherwise it is the inner measure's
weighted atoms. Wrap it in process/mem when the same question recurs.
Operations that act on a set of sites — gfi/regenerate, gfi/mh,
interventions — take a set of addresses or a spindel selector
(org.replikativ.spindel.select): (select/id :x), (select/path [:step :* :x]),
(select/prefix [:step]), (select/site :inference/choose), combined with
union, intersection and complement*.
A spin body is transformed into continuation-passing style, so that an
inference algorithm can suspend it at a site, fork the world it runs in, and
resume it — any number of times. The transformation only sees code that is
lexically inside the spin:
Call sites directly in the spin body, not inside functions passed to
map, filter, reduce, or in for/doseq. The macro cannot see into
them. Loop with loop/recur:
;; wrong: the observes are inside a closure
(spin (doseq [y ys] (observe (dist/normal mu 1.0) y)))
;; right
(spin (loop [[y & more] ys]
(when y (observe (dist/normal mu 1.0) y) (recur more))))
Use await for other spins and track for signals, never @ inside a
spin body (spindel's rules apply unchanged).
A model's randomness must come from its sites (or foerster.random), not
from rand: only site draws are recorded, keyed and reproducible.
A model can call functions that build spins — a sub-model is a function
returning a spin, awaited from the parent:
(defn noisy [mu] (spin (sample (dist/normal mu 1.0))))
(spin (let [x (await (noisy 0.0))] …))
foerster's operations come in two shapes. infer/infer, infer/predictive
and the models are spins; the trace operations (gfi/simulate,
gfi/update, involutive/step, smc/stream, …) are CPS operations,
functions of resolve and reject. await takes both, so inside a spin
they compose the same way. At the REPL (JVM), deref a spin and wrap a CPS
operation in one:
@(infer/infer (model) {:method :smc})
(sp/with-context world @(spin (await (gfi/simulate (model)))))
(intervene! address value) sets a site's value for the rest of the run,
Pearl's do-operator applied by the program itself; the site draws and scores
nothing. From outside a model, pass interventions to a trace policy instead
(see the counterfactuals notebook).
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |