Introduction to Probabilistic Programming: continued

models & model-based reasoning · conditioning and the inference problem · sample & observe · a first probabilistic program and its evaluator

Javier Burroni

2026-06-19

Today

Learning Objectives

  1. Explain what a generative model is and why the interesting questions run it backwards (inference).
  2. State Bayes’ rule, and explain why the central computational problem is computing expectations under the posterior (the queries we actually ask).
  3. Define a probabilistic program: an ordinary program + sample + observe. Conditioning is the evaluator’s job, not the program’s.
  4. State the question of this course: how to implement such a language inside Python.

Recap: the pieces of Bayes, with \(y\) observed

One latent \(x\), one observation \(y\). Bayes’ rule names each factor:

quantity formula in words
Prior \(p(x)\) belief about \(x\) before data
Likelihood \(p(y \given x)\) how \(y\) is generated from a given \(x\)
Joint \(p(x, y) = p(x)\,p(y \given x)\) the model, read forward as a simulator
Evidence \(p(y) = \int p(x, y)\,dx\) marginal probability of the data, the normalizer
Posterior \(p(x \given y) = \dfrac{p(x)\,p(y \given x)}{p(y)}\) belief about \(x\) after seeing \(y\)
Posterior predictive \(p(\tilde{y} \given y) = \int p(\tilde{y} \given x)\,p(x \given y)\,dx\) distribution of a new observation \(\tilde{y}\)


No observation, no names: a generative model alone is just a joint \(p(x, y)\).

Question 2

Our second model, the course “hello world”. One unknown quantity \(\mu\), one noisy measurement \(y\):

\[\mu \sim \mathcal{N}(0, 1) \qquad y \sim \mathcal{N}(\mu, 1)\]

In your notebook: write a Python function model() that simulates one \((\mu, y)\) pair, then use it to estimate \(p(y > 2)\). Hint: random.gauss(m, s).

def model():
    mu = random.gauss(0, 1)
    y  = random.gauss(mu, 1)
    return mu, y

pairs = [model() for _ in range(100_000)]
print(sum(y > 2 for _, y in pairs) / len(pairs))   # ≈ 0.079

Forward questions: easy. The whole course is the other one: given \(y = 2.3\), what is \(\mu\)?

Section 2: Conditioning: Easy to Write, Hard to Query

Question 3

Suppose we can generate many forward simulations from the captcha model:

  1. sample a candidate text \(x\) from the prior;
  2. generate an image \(y\) from the image model given \(x\).

Now we are given a fixed observed image \(y_{\text{obs}}\).

What extra information would be needed to turn these forward simulations into information about the posterior \(p(x \given y_{\text{obs}})\)?

A) Nothing extra: enough forward samples make the histogram of \(x\) approximate \(p(x \given y_{\text{obs}})\).

B) The likelihood \(p(y_{\text{obs}} \given x)\): how compatible the observed image is with each text \(x\).

C) Only the prior \(p(x)\): it tells us which texts are more plausible candidates.

D) Only the evidence \(p(y_{\text{obs}})\): it supplies the missing normalizing constant.

Vote: https://pe.app/ppls

What is the likelihood here?

For each candidate text \(x\), the model renders a blurry image \(\operatorname{render}(x)\) and compares it with the observed image \(y_{\text{obs}}\).

The likelihood is higher when the squared pixel error is smaller:

\[ y_{\text{obs}} \sim \operatorname{Normal}(\operatorname{render}(x), \sigma), \qquad \log p(y_{\text{obs}} \given x) = \text{constant} - \frac{1}{2\sigma^2} \|y_{\text{obs}}-\operatorname{render}(x)\|^2. \]

Likelihood is a soft pixel-level match between the rendered candidate and the observed degraded text.

Conditioning changes the distribution

\[p(x \given y) \;=\; \frac{p(y \given x)\,p(x)}{p(y)}\]

  • Run the program forward and the latents follow the prior \(p(x)\).
  • Conditioning on \(y\) reshapes them into the posterior, a different distribution: the executions that stay plausible are no longer the ones the program produces on its own.
  • A query is an expectation under that posterior, and forward averages estimate the wrong one.

The complicating crux

Bayes’ rule gives the posterior in one line. What we actually want are expectations under it, like \(P(x > 0.7 \given y)\) or \(\mathbb{E}[x \given y]\). Computing those is the hard problem the rest of the course attacks.

The lucky escape: conjugacy

In rare cases prior and likelihood are conjugate: the posterior stays in the same family and is available in closed form.
The running coin’s \(\text{Beta}(1,1)\) prior with one observed flip gives \(\text{Beta}(2,1)\), density \(2x\).

Full derivation in the Appendix. Conjugacy is the exception: table of conjugate distributions.

Samples are a usable answer

Outside conjugacy there is no formula for \(p(x \given y)\), yet samples drawn from it still answer the queries:

\[\frac{1}{L} \sum_{\ell=1}^{L} f\!\left(x^{(\ell)}\right) \;\xrightarrow{\;L \to \infty\;}\; \int f(x)\, p(x \given y)\, dx, \qquad x^{(\ell)} \sim p(x \given y)\]

  • \(f = \mathbb{I}(x > 0.7)\): tail probability. \(f = x\): posterior mean. Same samples, any \(f\), chosen after sampling.
  • In general we get neither an analytic posterior nor exact samples, but sampling can be approximated consistently.

The goal of this course’s algorithms

Characterize \(p(x \given y)\) by (weighted) samples. Every evaluator we build outputs samples, not formulas.

Section 3: Probabilistic Programs = Programs + sample + observe

Question 4

Back to Q1’s coin, but now we insist on conditioning, using plain Python:

def flip_conditioned():
    while True:
        x = random.random()              # x ~ Beta(1, 1)
        y = int(random.random() < x)     # y ~ Bernoulli(x)
        if y == 1:
            return x                     # keep only runs where y == 1

The returned values of x are distributed according to…

A) the posterior \(p(x \given y{=}1)\).

B) the prior \(\text{Beta}(1,1)\) unchanged: \(x\) is drawn before the if, so keeping or dropping whole runs cannot reshape it.

C) a biased version of the posterior, because discarding rejected runs distorts the original simulation.

D) almost all mass at \(x = 1\): you kept only heads, and \(x = 1\) is the bias that makes heads certain.

Vote: https://pe.app/ppls

You already wrote an inference algorithm

  • That loop is rejection sampling: simulate the joint, keep runs that match the data.
  • Acceptance probability given \(x\) is \(p(y{=}1 \given x)\). Kept \(x\)’s follow the posterior exactly. Histogram them against \(\text{Beta}(2,1)\): density \(2x\).
  • Conditioning by if: no algebra, no integral, just the simulator plus a filter.

Question 5

Apply the same trick to Q2’s hello world, conditioning on \(y = 2.3\) (treat floats as ideal real numbers):

def gaussian_conditioned():
    while True:
        mu = random.gauss(0, 1)
        y  = random.gauss(mu, 1)
        if y == 2.3: return mu

What happens when we call it?

A) As in Q4: it returns exact posterior samples, just somewhat more slowly.

B) It returns samples from the prior over x, because the condition y == 2.3 does not change how x was sampled.

C) It almost surely never returns, because under the continuous model the event y = 2.3 has probability zero.

D) It returns samples from the likelihood p(y = 2.3 | x), because the loop filters executions using the observed value.

Vote: https://pe.app/ppls

The zero-probability wall → observe

  • For continuous data, matching is hopeless: we must score runs against the data, not filter them.
  • Scoring needs language support: something must mark where the data enters.

What is a probabilistic program? (Gordon et al., 2014)

“Ordinary programs with two added constructs: the ability to draw values at random from distributions, and the ability to condition values of variables via observations.”

  • (sample d): draw a value from distribution \(d\) and move on.
  • (observe d v): declare that the value \(v\) was observed from \(d\).
  • A random library makes a simulation language. observe plus an evaluator that implements conditioning makes a probabilistic programming language.

A first probabilistic program (Program 1.1)

(let [prior (beta 1 1)
      x (sample prior)
      likelihood (bernoulli x)
      y 1]
  (observe likelihood y)
  x)

 

\[ \begin{aligned} x &\sim \text{Beta}(1,1) \\ y &\sim \text{Bernoulli}(x), \quad y = 1 \end{aligned} \] return \(x\), i.e. ask for \(p(x \given y{=}1)\)

  • Lisp-style s-expressions: the mini-language we will build.
  • The program denotes the joint and, via observe + its return value, the inference problem.

Question 6

Two programs differing in one construct:

;; Program 🚲
(let [x (sample (beta 1 1))]
  (observe (bernoulli x) 1)
  x)

 

;; Program 🚃
(let [x (sample (beta 1 1))
      y (sample (bernoulli x))]
  x)

How do the denoted distributions of the return value differ?

A) They are identical: both introduce a Bernoulli variable connected to \(x\).

B) Program 🚃 also conditions on \(y\), because sampling \(y\) creates the same evidence variable that observe uses.

C) 🚲 returns \(p(x,y=1)\); 🚃 returns \(p(x,y)\).

D) 🚲 denotes the posterior \(p(x \given y{=}1)\); 🚃 denotes the prior \(p(x)\).

Vote: https://pe.app/ppls

What a probabilistic program means

  • It simultaneously denotes:
    1. a joint \(p(x, y)\), read it as a simulator;
    2. a conditional: observe marks which variables carry data; the return value names the posterior you want.
  • Nothing in the program text computes that conditional. Conditioning is what inference does, the evaluator’s job.

sample ≠ observe

Swapping one for the other changes the inference problem, not the style: Q6’s two programs share a joint but denote different distributions.

Section 4: The Road Ahead

Denotation \(\neq\) one execution

The program denotes:

\[ p(x \given y{=}1) \]

But one execution only produces one sampled candidate for \(x\).

At the observe, the evaluator does not draw \(y\).

It must instead ask:

\[ \text{How likely is the observed value } y=1 \text{ under this sampled } x? \]

Important

The evaluator records that compatibility in the execution’s score.

Operationally:

  • sample draws values;
  • observe draws nothing;
  • observe updates the score.

Density, not probability

For the Gaussian model, rejection tried to draw a fresh \(y'\) and test:

\[ y' = 2.3 \]

That event has probability \(0\), so the loop almost surely never returns.

observe asks a different question. For a sampled \(\mu\), it evaluates the density of the fixed observed value:

\[ w \;=\; p(2.3 \given \mu) \;=\; \frac{1}{\sqrt{2\pi}} e^{-(2.3-\mu)^2/2}. \]

  • This is not the probability of the point \(2.3\).
  • It is a score: how well this sampled \(\mu\) explains the observation.
  • Every run returns; runs with better explanations receive larger weights.

Match vs score

Rejection tests equality against a probability-zero event.
observe evaluates a density and records it in the score.

The question of this course

We can describe the evaluator in words. How do you implement such a language, inside Python?

stage the PPL is… inference gets…
1 an interpreted s-expression mini-language a recursive eval threading weights \(\sigma\)
2 the same language on an abstract machine pause at sample/observe; fork = deepcopy
3 Python itself + sample/observe calls effect handlers over PyTorch, “Pyro in 50 lines”

Same observe, three embeddings: the spine of the course.

Summary

What inference computes

Conditioning yields a new distribution \(p(x \given y) = p(x,y)/p(y)\), not the prior the program samples forward. Inference means computing expectations under it, such as tail probabilities or posterior means; the general tool is (weighted) samples read off the unnormalized joint \(g\).

sample + observe

Probabilistic program = ordinary program + sample (draw) + observe (score data; draws nothing). Conditioning is the evaluator’s job.

Questions: Answer Key (1/2)

Solutions

  • Q3: B (the likelihood \(p(y_{\text{obs}} \given x)\)). Forward samples give prior candidates; the likelihood tells us which candidates are compatible with the observed image. A ignores conditioning, C ignores the image, D gives only the normalizer.
  • Q4: A (exactly the posterior \(\text{Beta}(2,1)\)). The loop is rejection sampling: the acceptance probability given \(x\) is \(p(y{=}1\mid x)\), so kept \(x\)’s are exactly posterior-distributed.
  • Q5: C (it almost surely never returns). For continuous \(y\), \(P(y{=}2.3)=0\), so the exact-match loop never terminates: the zero-probability wall.

Questions: Answer Key (2/2)

Solutions

  • Q6: D (Program A denotes the posterior \(p(x\mid y{=}1)\), Program 🚃 the prior \(p(x)\)). observe conditions; an unused sample draw conditions nothing.

Appendix

Conjugacy: the Beta-Bernoulli model

The coin’s prior is a Beta density on \(x \in [0,1]\), normalized by the Beta function \(B\):

\[ \begin{aligned} \text{Beta}(x;\, a, b) &= \frac{x^{a-1}(1-x)^{b-1}}{B(a,b)}, \\ B(a,b) &= \int_0^1 x^{a-1}(1-x)^{b-1}\,dx = \frac{\Gamma(a)\,\Gamma(b)}{\Gamma(a+b)}. \end{aligned} \]

With prior \(\text{Beta}(x;\alpha,\beta)\) and one observed flip, algebra returns a posterior in the same family:

\[p(x \given y) \;=\; \text{Beta}(x;\; \alpha + y,\; \beta - y + 1)\]

  • At integer arguments \(\Gamma(n) = (n-1)!\), so the constant is exact and easy.
  • Uniform prior \(\text{Beta}(x;1,1) = 1\), observe heads: posterior \(\text{Beta}(x;2,1)\), density \(2x\). Prior and likelihood in a known family: conjugacy.

Do not get used to this: Conjugacy is the exception.

For a renderer, a tree simulator, or your Q2 model() with a max() inside, no algebra applies.

Deriving \(p(x \given y)\)

Prior \(p(x) = x^{\alpha-1}(1-x)^{\beta-1}/B(\alpha,\beta)\) and one Bernoulli observation \(p(y \given x) = x^{y}(1-x)^{1-y}\). Their product collects the exponents:

\[p(x,y) \;=\; p(x)\,p(y \given x) \;=\; \frac{x^{(\alpha+y)-1}\,(1-x)^{(\beta-y+1)-1}}{B(\alpha,\beta)}\]

Integrate over \(x\) for the evidence; the integral is again a Beta function:

\[p(y) \;=\; \int_0^1 p(x,y)\,dx \;=\; \frac{B(\alpha+y,\; \beta-y+1)}{B(\alpha,\beta)}\]

Divide; the constant \(B(\alpha,\beta)\) cancels and a normalized Beta density remains:

\[p(x \given y) \;=\; \frac{p(x,y)}{p(y)} \;=\; \frac{x^{(\alpha+y)-1}\,(1-x)^{(\beta-y+1)-1}}{B(\alpha+y,\; \beta-y+1)} \;=\; \text{Beta}(x;\; \alpha+y,\; \beta-y+1)\]

Running coin

\(\alpha=\beta=1\), observe \(y=1\): \(B(2,1)=\tfrac12\), so \(p(x \given y) = x/\tfrac12 = 2x\), the \(\text{Beta}(2,1)\) our samplers reproduce.