Liking cljdoc? Tell your friends :D

Language

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.

Sites

EffectMeaning
(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:

OptionMeaning
:idthe site's name, its address (see below)
:init vthe first state of a Markov chain starts the site at v (a block that cannot be drawn from needs it)
:proposal qsample 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 truethe 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.

Addresses

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.

  • A site with :id is addressed by it. A vector id is a hierarchical name: :id [:step 3 :x].
  • Without :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).

Memoization and random processes

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.

Nested inference

(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.

Selectors

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*.

The rules of the spin macro

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))] …))

Interventions from inside

(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

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close