AI-320 · Lesson 4 of 12 · 30 min

Learning graphical models, loopy inference and causality

Lesson 3 took the graph and its conditional distributions as given and asked how to compute probabilities from them. In practice both usually come from data. This lesson covers learning the parameters of a graphical model, learning its structure, approximate inference in graphs with loops where exact methods are too expensive, conditional random fields for labeling sequences and images, and causal graphical models, which answer a different kind of question: not what we expect to see if we observe something, but what would happen if we intervened to change it.

Learning parameters from complete data

Suppose the structure of a Bayesian network is known and every variable is observed in every training case. The log-likelihood of the data then splits into a separate term for each node:

\[\log p(\mathcal{D} \mid \boldsymbol\theta) = \sum_{i=1}^{n}\sum_{j} \log p(x_j^{(i)} \mid \mathrm{pa}(x_j)^{(i)}, \boldsymbol\theta_j)\]

Each node's parameters \(\boldsymbol\theta_j\) appear in only one term, so they can be estimated independently. This decomposability is one of the great practical strengths of Bayesian networks. For discrete variables, the maximum likelihood estimate of each conditional probability table entry is a ratio of counts:

\[\hat p(x_j = k \mid \mathrm{pa}(x_j) = c) = \frac{N(x_j = k, \mathrm{pa} = c)}{N(\mathrm{pa} = c)}\]

Rare parent configurations give unreliable or undefined ratios (zero divided by zero, or a probability of exactly zero for an outcome merely not yet seen). The standard remedy is a Dirichlet prior on each row of the table (Lesson 1), which adds pseudo-counts \(\alpha\) to every cell: \(\hat p = (N_{kc} + \alpha)/(N_c + K\alpha)\). With \(\alpha = 1\) this is Laplace or add-one smoothing.

Worked problem

Smoothing a sparse table

A node with three states has a parent configuration seen only 4 times, with counts (3, 1, 0). Give the maximum likelihood and add-one estimates.

Show solution
  1. Maximum likelihood: \((3/4, 1/4, 0) = (0.75, 0.25, 0)\).
  2. Add-one: \(((3 + 1)/7, (1 + 1)/7, (0 + 1)/7) = (0.571, 0.286, 0.143)\).

Answer: The smoothed estimate never assigns zero probability, which matters because a single zero in a product of probabilities rules out every explanation involving that value, however strong the other evidence.

For continuous nodes, each conditional is a regression: a linear Gaussian node \(x_j \mid \mathrm{pa} \sim \mathcal{N}(\mathbf{w}^\top\mathrm{pa} + b, \sigma^2)\) is fitted by least squares. When some variables are missing in some cases, or are never observed (latent variables), the likelihood no longer decomposes, and the EM algorithm of Lesson 5 is the standard tool: it fills in expected counts using inference and then re-estimates the tables as if the data were complete.

Learning structure

Learning which edges exist is harder. The number of possible DAGs grows super-exponentially (25 for three nodes, 29,000 for five, more than \(10^{18}\) for ten), so structure learning needs both a criterion and a search strategy. There are two broad approaches.

Score-based methods assign each candidate structure a score that rewards fit and penalizes complexity, then search for a high-scoring structure. The likelihood alone always favors adding edges, because a more flexible model never fits worse, so a penalty is essential. The Bayesian information criterion (BIC) approximates the log marginal likelihood (Lesson 1):

\[\mathrm{BIC} = \log p(\mathcal{D} \mid \hat{\boldsymbol\theta}, G) - \frac{k}{2}\log n\]

where \(k\) is the number of free parameters. Because the score decomposes over nodes, local search is efficient: start from an empty or guessed graph and repeatedly add, delete or reverse the single edge that most improves the score, keeping the graph acyclic, until no change helps. Restarts or tabu lists help escape local optima.

Scoring candidate structures by BIC on simulated data where smoking affects cancer only through tar, and estimating a smoothed table.
import numpy as np

rng = np.random.default_rng(11)
n = 2000
# True structure: smoking (S) -> tar (T) -> cancer (C); S has no direct edge to C
S = rng.random(n) < 0.3
T = rng.random(n) < np.where(S, 0.8, 0.1)
C = rng.random(n) < np.where(T, 0.25, 0.03)

def loglik_cpt(child, parents):
    """Maximum likelihood log-likelihood of a binary child given binary parents, with counts."""
    ll, k = 0.0, 0
    keys = np.zeros(n, dtype=int)
    for p in parents:
        keys = keys * 2 + p
    for key in np.unique(keys):
        m = keys == key
        c1, c0 = child[m].sum(), (~child[m]).sum()
        for c in (c1, c0):
            if c > 0:
                ll += c * np.log(c / (c1 + c0))
        k += 1                                        # one free parameter per parent configuration
    return ll, k

def bic(structure):
    total_ll, total_k = 0.0, 0
    for child, parents in structure:
        ll, k = loglik_cpt(child, parents)
        total_ll += ll; total_k += k
    return total_ll - 0.5 * total_k * np.log(n), total_ll, total_k

candidates = {
    "S -> T -> C (true)": [(S, []), (T, [S]), (C, [T])],
    "S -> T -> C plus S -> C": [(S, []), (T, [S]), (C, [T, S])],
    "S -> C, no tar edge": [(S, []), (T, []), (C, [S])],
    "all independent": [(S, []), (T, []), (C, [])],
}
for name, st in candidates.items():
    b, ll, k = bic(st)
    print(f"{name:26s} log-lik {ll:9.1f}  params {k}  BIC {b:9.1f}")

# Learned conditional probability table with add-one (Dirichlet) smoothing
for t in (True, False):
    m = T == t
    print(f"P(C | T={int(t)}) = {(C[m].sum() + 1) / (m.sum() + 2):.3f}  from {m.sum()} cases")
Output
S -> T -> C (true)         log-lik   -2542.8  params 5  BIC   -2561.8
S -> T -> C plus S -> C    log-lik   -2542.4  params 7  BIC   -2569.0
S -> C, no tar edge        log-lik   -3088.5  params 4  BIC   -3103.7
all independent            log-lik   -3138.0  params 3  BIC   -3149.4
P(C | T=1) = 0.248  from 636 cases
P(C | T=0) = 0.030  from 1364 cases

The extra edge from smoking to cancer improves the log-likelihood by less than one unit but costs two parameters, so BIC rejects it, correctly, because in this simulation smoking acts only through tar. Removing the tar edge destroys the fit. The learned conditional probabilities are close to the true 0.25 and 0.03.

Constraint-based methods instead test conditional independences in the data and build a graph consistent with them. The PC algorithm starts with a complete undirected graph, removes the edge between two variables whenever some set of other variables makes them conditionally independent, and then orients edges using v-structures (colliders, Lesson 3): if A and C are both adjacent to B but not to each other, and B was not in the set that separated them, the pattern must be A → B ← C.

Both approaches face a fundamental limit. Different DAGs can encode exactly the same conditional independences; A → B → C, A ← B ← C and A ← B → C are all equivalent. Such Markov-equivalent graphs fit any observational data equally well, so data alone can identify only an equivalence class, in which some edge directions remain undetermined. Fixing them requires background knowledge (time order, physics) or experiments, which leads to the causal questions at the end of this lesson.

Inference in graphs with loops

Variable elimination and message passing are exact and efficient on chains and trees (Lesson 3). On graphs with loops, exact inference is possible with the junction tree algorithm, which clusters variables into a tree of cliques and passes messages between the clusters. Its cost is exponential in the size of the largest cluster, measured by the graph's treewidth. For a grid of pixels, the treewidth equals the shorter side of the grid, so exact inference on even a 100 × 100 image would require tables of \(2^{100}\) entries. Exact inference in general graphical models is NP-hard, and approximation is unavoidable.

Loopy belief propagation simply runs the sum-product message updates on the loopy graph anyway, iterating until the messages stop changing. Each node combines the messages from its neighbors with its own evidence to form a belief, and sends each neighbor a message based on everything except what that neighbor told it. There is no guarantee of convergence or correctness, because messages can circulate around loops and count the same evidence repeatedly, but in many problems it works remarkably well. The decoding of turbo codes and LDPC codes, used in every mobile phone and Wi-Fi connection, is loopy belief propagation.

A classic test is image denoising with an Ising model. Each pixel \(x_i \in \{-1, +1\}\) is linked to its four neighbors with a coupling that rewards agreement, and to its noisy observation \(y_i\):

\[p(\mathbf{x} \mid \mathbf{y}) \propto \exp\!\left(J\sum_{(i,j)} x_i x_j + h\sum_i x_i y_i\right)\]

For binary variables, messages can be represented as single numbers (log-odds), and the sum-product update takes the compact form \(m_{j \to i} = \tanh^{-1}\!\left(\tanh J \tanh\left(h y_j + \sum_{k \neq i} m_{k \to j}\right)\right)\).

Loopy belief propagation on an Ising model removes most of the noise from a 40 × 40 binary image with 20% of pixels flipped.
import numpy as np

rng = np.random.default_rng(1)
H, W = 40, 40
yy, xx = np.mgrid[0:H, 0:W]
clean = np.where((xx - 20) ** 2 + (yy - 20) ** 2 < 120, 1, -1)       # a disc of +1 on a -1 background
flip = rng.random((H, W)) < 0.2
noisy = np.where(flip, -clean, clean)

J, h = 0.9, 0.7      # coupling between neighbours and strength of the evidence (Ising model)
# messages m[d][i, j] = message into pixel (i, j) from its neighbour in direction d, as a log-odds
msgs = {d: np.zeros((H, W)) for d in "udlr"}
def shift(a, d):
    out = np.zeros_like(a)
    if d == "u": out[1:, :] = a[:-1, :]          # value of the pixel above
    if d == "d": out[:-1, :] = a[1:, :]
    if d == "l": out[:, 1:] = a[:, :-1]
    if d == "r": out[:, :-1] = a[:, 1:]
    return out
opposite = {"u": "d", "d": "u", "l": "r", "r": "l"}
for it in range(15):
    belief = h * noisy + sum(msgs.values())
    new = {}
    for d in "udlr":
        # the neighbour in direction d sends what it believes, excluding what this pixel told it
        nb_belief = shift(belief, d)
        nb_from_me = shift(msgs[opposite[d]], d)
        x = nb_belief - nb_from_me
        new[d] = np.arctanh(np.tanh(J) * np.tanh(x))
        if d == "u": new[d][0, :] = 0
        if d == "d": new[d][-1, :] = 0
        if d == "l": new[d][:, 0] = 0
        if d == "r": new[d][:, -1] = 0
    msgs = new
    if it in (0, 2, 14):
        est = np.where(h * noisy + sum(msgs.values()) > 0, 1, -1)
        print(f"iteration {it + 1:2d}: pixel error {np.mean(est != clean):.3f}")
print(f"noisy image pixel error {np.mean(noisy != clean):.3f}")
Output
iteration  1: pixel error 0.075
iteration  3: pixel error 0.011
iteration 15: pixel error 0.006
noisy image pixel error 0.190
Three 40 by 40 binary images side by side. The first is a clean white disc on a black background. The second is the same image with about one fifth of the pixels flipped, scattering white and black speckles everywhere. The third is the result of loopy belief propagation, a clean disc with only a few wrong pixels near its edge.
Denoising with loopy belief propagation: clean image, noisy observation (19% errors), and the result after 15 iterations (under 1% errors).

The neighbors' agreement outweighs isolated wrong observations, while pixels along the true boundary, where neighbors genuinely disagree, are where the remaining errors sit. When loopy belief propagation fails to converge, damping (averaging each new message with the old one) usually helps. The alternatives are sampling methods (Gibbs sampling, Lesson 7) and variational methods (Lesson 9); loopy belief propagation can itself be understood as minimizing an approximate free energy, linking it to variational inference.

Conditional random fields

Generative models such as HMMs and naive Bayes model the joint distribution \(p(\mathbf{x}, \mathbf{y})\) of inputs and labels. When the goal is only to predict labels from inputs, it is often better to model \(p(\mathbf{y} \mid \mathbf{x})\) directly, which frees the model from describing how inputs are generated and allows rich, overlapping input features. A conditional random field (CRF) is an undirected graphical model over the labels, conditioned on the input:

\[p(\mathbf{y} \mid \mathbf{x}) = \frac{1}{Z(\mathbf{x})}\exp\!\left(\sum_t \mathbf{w}^\top \mathbf{f}(y_{t-1}, y_t, \mathbf{x}, t)\right)\]

In a linear-chain CRF for sequence labeling (part-of-speech tags, named entities, or segments of a sensor signal), the features \(\mathbf{f}\) can look at any part of the input (the current word, its suffix, whether it is capitalized, the next word) together with adjacent label pairs. Inference uses the same forward-backward and Viterbi algorithms as an HMM, because the label graph is still a chain. Training maximizes the conditional log-likelihood, which is concave in \(\mathbf{w}\); its gradient is the observed feature counts minus the expected counts under the model, computed by forward-backward.

A CRF relates to an HMM as logistic regression relates to naive Bayes: the discriminative partner of a generative model. Today, neural sequence models often compute the input features, with a CRF layer on top to enforce consistent label sequences (for example, that an "inside a name" tag cannot follow an "outside" tag). Grid-shaped CRFs serve the same role in image segmentation, encouraging neighboring pixels to take the same label unless the image shows an edge.

Causal graphical models

A Bayesian network describes how variables are associated. A causal graphical model makes a stronger claim: each edge represents a direct causal influence, so the graph also predicts what happens under interventions. The distinction is captured by the do-operator. \(p(y \mid x)\) is the distribution of \(Y\) among cases where \(X\) happened to equal \(x\); \(p(y \mid \mathrm{do}(x))\) is the distribution of \(Y\) if we set \(X\) to \(x\) by intervention. In the graph, an intervention deletes all arrows into \(X\), because its value is no longer determined by its usual causes.

The two differ when a confounder affects both \(X\) and \(Y\). A standard example: doctors give a treatment more often to severely ill patients, and severe illness lowers recovery. Treated patients may then recover no more often than untreated ones, even though the treatment helps, because the treated group is sicker. This is Simpson's paradox: an association that reverses or disappears when data are split by a third variable.

If the confounders \(Z\) are observed and satisfy the back-door criterion (they block every path from \(X\) to \(Y\) that starts with an arrow into \(X\), and contain no descendant of \(X\)), the causal effect can be computed from observational data by adjustment:

\[p(y \mid \mathrm{do}(x)) = \sum_z p(y \mid x, z)\,p(z)\]

Compare this with ordinary conditioning, \(p(y \mid x) = \sum_z p(y \mid x, z)\,p(z \mid x)\): the adjustment weights each stratum by how common it is overall, not by how common it is among the treated.

A confounded treatment: the naive comparison shows no benefit, while back-door adjustment and a randomized experiment both recover the true effect of +0.20.
import numpy as np

rng = np.random.default_rng(5)
n = 200000
# Severity Z confounds treatment X and recovery Y: severe patients get treated more and recover less
Z = rng.random(n) < 0.4
X = rng.random(n) < np.where(Z, 0.8, 0.2)
Y = rng.random(n) < 0.5 + 0.2 * X - 0.35 * Z          # treatment truly adds 0.20 to recovery

obs = Y[X].mean() - Y[~X].mean()
print(f"observational difference P(Y|X=1) - P(Y|X=0): {obs:+.3f}")

# Back-door adjustment: average the within-stratum effect over the distribution of Z
adj = 0.0
for z in (True, False):
    m = Z == z
    effect = Y[m & X].mean() - Y[m & ~X].mean()
    adj += effect * m.mean()
    print(f"  within Z={int(z)}: treated {Y[m & X].mean():.3f}, untreated {Y[m & ~X].mean():.3f}")
print(f"adjusted estimate of P(Y|do(X=1)) - P(Y|do(X=0)): {adj:+.3f}")

# A randomized experiment cuts the arrow Z -> X
Xr = rng.random(n) < 0.5
Yr = rng.random(n) < 0.5 + 0.2 * Xr - 0.35 * Z
print(f"randomized experiment difference: {Yr[Xr].mean() - Yr[~Xr].mean():+.3f}")
Output
observational difference P(Y|X=1) - P(Y|X=0): -0.009
  within Z=1: treated 0.345, untreated 0.149
  within Z=0: treated 0.703, untreated 0.501
adjusted estimate of P(Y|do(X=1)) - P(Y|do(X=0)): +0.199
randomized experiment difference: +0.203

Randomization works because assigning treatment by coin flip deletes the arrow from severity to treatment, exactly what the do-operator describes. When randomization is impossible, adjustment is only as good as the assumption that every confounder has been measured, and that assumption cannot be tested from the data. Adjusting for the wrong variables can also create bias: conditioning on a collider (a common effect) or on a mediator (a step on the causal path) distorts the estimate. Drawing the causal graph forces these assumptions into the open.

Causal reasoning matters wherever models inform decisions. A model predicting hospital readmission from treatment records learns associations, including confounded ones; using it to decide who should receive a treatment asks a causal question it was never built to answer. Uplift modeling in marketing, policy evaluation, and fairness analysis all require causal rather than merely predictive models.

Common mistakes

  • MistakeEstimating conditional probability tables by raw counts when some configurations are rare.

    InsteadAdd Dirichlet pseudo-counts so that no probability is exactly zero.

  • MistakeChoosing the structure with the highest likelihood.

    InsteadPenalize complexity with BIC or a marginal likelihood; likelihood always prefers more edges.

  • MistakeReading edge directions from a structure learned on observational data as causal.

    InsteadData identify only a Markov equivalence class; directions need background knowledge or experiments.

  • MistakeTrusting loopy belief propagation without checking convergence.

    InsteadMonitor message changes, use damping, and compare with sampling on small cases.

  • MistakeTreating \(p(y \mid x)\) as the effect of setting \(x\).

    InsteadUse \(p(y \mid \mathrm{do}(x))\), computed by adjustment for confounders or estimated from randomized experiments.

Summary and key terms

  • With complete data, a Bayesian network's likelihood decomposes by node; tables are estimated by smoothed counts.
  • Structure learning uses penalized scores with local search, or conditional independence tests; data identify only a Markov equivalence class.
  • Exact inference costs grow exponentially with treewidth; loopy belief propagation is a fast, usually accurate approximation.
  • Conditional random fields model labels given inputs, with rich features and chain or grid structure.
  • Causal models distinguish observing from intervening; the do-operator removes arrows into the intervened variable.
  • Back-door adjustment or randomization recovers causal effects when confounders would otherwise mislead.
Decomposability
The splitting of the likelihood or score into independent terms per node.
Laplace smoothing
Adding one pseudo-count to every cell of a count table.
BIC
Log-likelihood minus half the number of parameters times \(\log n\).
Markov equivalence
Different DAGs implying the same conditional independences.
Treewidth
A measure of how far a graph is from a tree, governing exact inference cost.
Loopy belief propagation
Sum-product message passing applied iteratively to graphs with cycles.
Conditional random field
An undirected model of labels conditioned on inputs.
do-operator
Notation for setting a variable by intervention.
Confounder
A common cause of treatment and outcome.
Back-door adjustment
Averaging stratum-specific effects over the confounder distribution.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Counts (5, 0) for a binary node in one parent configuration: add-one estimate?

    Show answer

    (6/7, 1/7) = (0.857, 0.143).

  2. Why can each node's table be estimated separately with complete data?

    Show answer

    The log-likelihood is a sum of per-node terms with separate parameters.

  3. A model gains 3 in log-likelihood by adding 4 parameters with \(n = 1000\). Does BIC accept it?

    Show answer

    No: the penalty is \(2\log 1000 = 13.8\).

  4. Are A → B → C and A ← B → C distinguishable from observational data?

    Show answer

    No, they are Markov equivalent.

  5. Which pattern can be oriented from data alone?

    Show answer

    A v-structure (collider) A → B ← C with A and C non-adjacent.

  6. Why is exact inference on a large pixel grid infeasible?

    Show answer

    Its treewidth equals the grid side, so tables are exponentially large.

  7. One remedy when loopy BP oscillates?

    Show answer

    Damping the messages.

  8. What is the main advantage of a CRF over an HMM for labeling?

    Show answer

    It can use rich, overlapping input features without modeling the inputs.

  9. Strata: effect +0.1 in a group with \(p(z) = 0.3\) and +0.3 with \(p(z) = 0.7\). Adjusted effect?

    Show answer

    0.03 + 0.21 = +0.24.

  10. Why does randomization remove confounding?

    Show answer

    It deletes the arrows from the confounders into the treatment.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz