$ cat slop/probabilistic-programming.md

Probabilistic Programming: Code That Reasons with Uncertainty

slop probabilistic programming

⚠️ Slop Warning

This blog post is AI generated. I like to read about interesting topics and have them on my blog, if the AI lied to me so be it.

This post was generated by: Kimi K2 Thinking

The problem isn’t that your model is wrong. It’s that your programming language can’t express what you actually mean.

You’ve built a fraud detection system. It returns a score: 0.73. Your product manager asks, “How sure are we?” You mumble something about confidence intervals and bootstrap sampling, then spend three days implementing a hack that sort of works but breaks when the data distribution shifts. Again.

This is the central failure of deterministic programming. We’re forced to collapse uncertainty—a distribution over plausible worlds—into a single number, then bolt on error handling as an afterthought. Probabilistic programming flips this. It treats random variables as first-class citizens and automates the mathematics of reasoning under uncertainty. You write code that generates data; the language automatically inverts that code to infer the hidden causes.

This isn’t another machine learning framework. It’s a different paradigm entirely. And if you’ve ever cursed at a threshold that feels arbitrary, or wondered why your “99% accurate” model fails catastrophically at deployment, this is for you.

What Is Probabilistic Programming? (The Two-Line Magic)

Here’s the pattern that shows up in every probabilistic program:

# 1. Specify your model: how could this data have been generated?
def my_model(data):
    # ... define random variables, priors, likelihood ...

# 2. Condition on observations and invert
posterior = infer(my_model, observed_data=data)

That’s it. The first block is a generative model—a simulation of reality. The second block performs Bayesian inference, returning not a point estimate but a distribution over all possible parameter values, weighted by how well they explain your data.

Let’s decode the jargon before it becomes irritating:

  • Random Variable: Not a sample, but a distribution object. When you write x = Normal(0, 1), you’re not drawing a number. You’re declaring “x is whatever process produces numbers centered at 0 with spread 1.”
  • Conditioning: The observe statement. It’s how you say “given that I definitely saw this data, what does that imply about my parameters?” This is where the magic happens—the language applies Bayes’ theorem for you.
  • Posterior: The answer. Not w = 1.5, but “w is most likely around 1.5, probably between 1.2 and 1.8, with a slight right skew because we don’t have much data.”

You already know Bayes’ theorem: posterior ∝ likelihood × prior. Probabilistic programming languages (PPLs) let you specify the right-hand side naturally in code; they solve for the left.

A Concrete Example: Bayesian Linear Regression That Knows What It Doesn’t Know

Traditional linear regression gives you a best-fit line. Bayesian linear regression gives you a distribution over lines, which is much more useful when you’re extrapolating into regions where you have no data.

Here’s the complete model in NumPyro, which runs on JAX and is delightfully fast:

import numpyro
import numpyro.distributions as dist
from numpyro.infer import MCMC, NUTS
import jax
import jax.numpy as jnp

def model(X, y=None):
    # Priors: what we believe before seeing data
    w = numpyro.sample("weight", dist.Normal(0, 10))  # Probably small-ish
    b = numpyro.sample("bias", dist.Normal(0, 10))
    sigma = numpyro.sample("noise", dist.Exponential(1))  # Positive only
    
    # Likelihood: how data is generated
    mu = X * w + b
    numpyro.sample("obs", dist.Normal(mu, sigma), obs=y)

# Run inference
mcmc = MCMC(NUTS(model), num_warmup=500, num_samples=1000)
mcmc.run(jax.random.PRNGKey(0), X_train, y_train)
samples = mcmc.get_samples()

What this gets you that sklearn doesn’t:

Plot 100 lines sampled from the posterior. Where your training data is dense, the lines cluster tightly—high confidence. Where data is sparse, they fan out, forming a natural uncertainty band that widens automatically. The model knows what it doesn’t know. No ad-hoc confidence intervals. No cross-validation hackery. Just the logical consequence of your assumptions.

This is the first superpower: uncertainty quantification for free. In high-stakes domains—medical diagnosis, autonomous vehicles, financial risk—you don’t just need a prediction. You need a credible interval. PPLs give it to you natively.

Three Superpowers (And Why They Matter)

1. Data Efficiency Through Priors

With five data points, a traditional neural network memorizes noise. A Bayesian model with an informed prior—“these parameters are probably small”—produces reasonable uncertainty bands. With five million points, the data overwhelms the prior. This is principled regularization, not the voodoo of dropout rates.

2. Model Composition Without Pain

Want to share statistical strength across 10,000 ad campaigns with varying data? Traditional ML requires custom architectures. In a PPL, you write:

def hierarchical_model(campaign_data):
    # Global parameters
    global_mean = numpyro.sample("global_mean", dist.Normal(0, 1))
    
    # Each campaign gets its own offset from global
    with numpyro.plate("campaigns", num_campaigns):
        local_offset = numpyro.sample("local_offset", dist.Normal(0, 0.1))
    
    # ... rest of model

The plate construct automatically handles the bookkeeping. You’re building complex statistical models like Lego, not hand-deriving gradients.

3. Causal Reasoning (When You Get Ambitious)

PPLs shine when you need to answer “what if?” questions. Because your model is a simulator, you can intervene on variables—force a value and see how predictions change—without retraining. This is the foundation of causal inference. Most ML models can’t do this; they only know correlation.

How It Works: The Inference Engine (And Why You Shouldn’t Fear It)

You might be thinking: “This looks like magic. How do I trust it?”

The separation of concerns is clean. You focus on modeling reality; the PPL handles the calculus. But understanding the engine helps you diagnose when things go wrong.

There are two strategies:

Variational Inference (VI) approximates the posterior with a simpler distribution. It’s fast—think stochastic gradient descent—but approximate. Use it for prototyping or when speed matters.

Markov Chain Monte Carlo (MCMC) is the gold standard. It works by taking a random walk through parameter space, spending more time in high-probability regions. The result is a set of samples that exactly represents the posterior (in the limit).

NUTS: The Algorithm You Keep Seeing

Every time you see NUTS(model) in code, you’re invoking the No-U-Turn Sampler—the algorithm that makes MCMC practical. Here’s why it dominates.

Traditional MCMC requires a step-size parameter. Too small, and you crawl. Too large, and you reject every proposal. NUTS automatically finds the optimal step-size and trajectory length by detecting when a path would “make a U-turn” and waste computation.

The intuition: NUTS uses gradient information (via autodiff) to zoom through high-probability regions like a guided missile, rather than blindly stumbling. It builds a binary tree of possible trajectories and stops when adding more steps would be redundant.

For you, this means rarely thinking about sampler settings. If NUTS struggles—divergences, low effective sample size—your model is probably misspecified. That’s diagnostic gold. The algorithm fails gracefully, telling you “your priors are too vague” or “your parameters are non-identifiable.”

This is the opposite of deep learning, where training instability is solved by tuning hyperparameters until the loss stops exploding. NUTS forces you to think about your model’s structure, which is where the real problem usually lives.

The Practical Landscape: What to Use and When

Let’s cut through the noise. Here are the three frameworks worth your time, with unambiguous opinions:

NumPyro/Pyro: Use this if you’re in the ML research world. It’s built on JAX, so you get GPU acceleration and composable transformations. The downside? JAX’s functional style can feel alien if you’re coming from PyTorch. But for complex models, nothing else is as fast or flexible.

Stan: The industry workhorse. It has the best documentation, a mature ecosystem, and interfaces for R, Python, Julia, and more. It’s slower than NumPyro for massive models because it compiles to C++ and can’t leverage GPUs as easily. But if you’re a data scientist who needs to ship a hierarchical model to production next week, start here. The community is huge, and most questions have been answered.

Turing.jl: For the Julia faithful. It’s elegant, fast, and integrates beautifully with the Julia scientific stack. If you’re already using Julia for differential equations or optimization, this is a no-brainer. Otherwise, the language barrier isn’t worth it for most teams.

When to reach for a PPL:

  • Small-to-medium data (< 1M observations) where uncertainty matters
  • Hierarchical structure (patients within hospitals within regions)
  • Physics-based simulators you need to invert (e.g., “what parameters make this simulation match reality?”)

When not to:

  • Standard deep learning at ImageNet scale (use PyTorch)
  • Latency-critical production APIs (inference is too slow unless you use VI)
  • When you just need a point prediction and don’t care about confidence

Performance tip: Vectorize ruthlessly. PPLs punish Python loops. Use plate constructs and batch operations. If your model is slow, you’re probably doing something imperative that could be declarative.

Getting Started: Your First Model in 15 Minutes

Don’t read another tutorial. Install NumPyro and run the linear regression example above on your own data. The key is to start with a problem where you actually need uncertainty.

Good first projects:

  • A/B testing: Get a full posterior over conversion rates, not just a p-value. See how often variant B is truly better.
  • Noisy sensor fusion: You have three temperature sensors with different reliabilities. Fuse them into a single credible estimate.
  • Time series with changepoints: Model a trend that might have shifted at an unknown point in time.

The pattern is always the same: simulate how the data could be generated, then invert. The hardest part is unlearning the deterministic mindset.

Conclusion: Uncertainty Is Not a Bug

Most of our systems treat uncertainty as an edge case to be handled with try/catch blocks and fallback logic. Probabilistic programming treats it as the central logic.

The paradigm shift is from optimizing parameters to reasoning about distributions. This feels slower at first because it is. But you’re asking a harder question: not “what’s the best single answer?” but “what do I believe about the world, and how confident am I?”

As our software interacts with messy reality—human behavior, sensor noise, emergent complexity—this isn’t optional. It’s the only honest way to write code that knows its limits.

Copy the 20-line example. Tweak it. Ask: “What problem do I have where a distribution is better than a point?” Run it. The first time you see a model say “I’m 95% sure the answer is in this interval, and here’s why,” you’ll wonder how you ever trusted a single number.