Bayesian model merging for stochastic context-free grammar induction

This article describes a weekend project, with a fuller account in the accompanying paper.

Grammar induction requires finding both the productions and their probabilities. Once the productions are fixed, expectation maximisation estimates their probabilities by repeating the inside and outside recursions until the corpus likelihood stops improving. Finding the discrete structure is harder, since the possible sets of productions form a combinatorial space that grows quickly with the number of nonterminals. Stolcke and Omohundro approach this problem in their 1994 paper, Inducing Probabilistic Grammars by Bayesian Model Merging, by beginning with a grammar that memorises the corpus and applying local merge and chunk operations. The implementation here follows their treatment of stochastic context-free grammars in Section 3.3.

Grammar representation and data incorporation

Let a grammar consist of productions with a nonterminal on the left and a sequence of symbols on the right. Each production stores a usage count from which its probability can be obtained. For every observed token aa, introduce a nonterminal TaT_a with the single production Ta→aT_a \to a, so that terminals occur only in these lexical productions and every structural right-hand side consists of nonterminals. This follows the representation introduced in Section 3.3 of the paper and allows the two editing operators to act uniformly on structural productions. The OCaml representation is:

type symbol = Term of string | Nonterm of string

type production = {
  lhs : string;
  rhs : symbol list;
  count : float;
}

type t = { start : string; productions : production list }

Incorporating the data means folding over the corpus and adding a start production for each distinct sample. Its right-hand side is the corresponding sequence of terminal nonterminals, and its count is the observed frequency of the sample. For {ab,aabb,aaabbb}\{ab, aabb, aaabbb\}, this gives the grammar at the top of Figure 2 in the paper. A one-token sample gives a unit production, which must survive normalisation when it does not belong to a cycle.

let initial_grammar ?(start = "S") samples =
  let term_nts = Hashtbl.create 64 in
  let prods = ref [] in
  List.iter
    (fun (tokens, count) ->
      let rhs =
        List.map
          (fun tok ->
            match Hashtbl.find_opt term_nts tok with
            | Some nt -> Nonterm nt
            | None ->
                let nt = "T_" ^ tok in
                Hashtbl.replace term_nts tok nt;
                prods := { lhs = nt; rhs = [ Term tok ]; count = 1.0 } :: !prods;
                Nonterm nt)
          tokens
      in
      prods := { lhs = start; rhs; count } :: !prods)
    samples;
  make ~start (List.rev !prods)

The Bayesian score

Bayes' rule gives the posterior over models. The paper factors the model prior into a prior over discrete structure and a conditional prior over continuous parameters:

P(M∣X)  ∝  P(M) P(X∣M),P(M)=P(MS) P(θM∣MS).\begin{gathered} P(M \mid X) \;\propto\; P(M)\,P(X \mid M), \\ P(M) = P(M_S)\,P(\theta_M \mid M_S). \end{gathered}

The search chooses a structure by maximising P(MS∣X)P(M_S \mid X). This posterior is proportional to the structural prior multiplied by the marginal likelihood, with the production probabilities integrated out:

P(X∣MS)=∫P(θM∣MS) P(X∣MS,θM) dθM.P(X \mid M_S) = \int P(\theta_M \mid M_S)\,P(X \mid M_S, \theta_M)\,d\theta_M.

Structural prior

The structural prior penalises the description length of a grammar. The paper specifies a cost of log⁡2N\log_2 N bits for each occurrence of a nonterminal in a right-hand side, where NN is the number of nonterminals. The implementation supplies the remaining encoding choices, charging terminal occurrences against the size of the terminal alphabet Σ\Sigma:

ℓ(MS)=log⁡2∣N∣+log⁡2∣Σ∣+∑A∈N[log⁡2∣PA∣+∑p∈PA(log⁡2∣rhs(p)∣+∑x∈rhs(p)b(x))],\begin{aligned} \ell(M_S) &= \log_2 |\mathcal{N}| + \log_2 |\Sigma| \\ &\quad + \sum_{A \in \mathcal{N}} \Bigl[ \log_2 |P_A| + \sum_{p \in P_A} \Bigl( \log_2 |\mathrm{rhs}(p)| + \sum_{x \in \mathrm{rhs}(p)} b(x) \Bigr) \Bigr], \end{aligned} b(x)={log⁡2∣N∣if x∈N,log⁡2∣Σ∣if x∈Σ.b(x) = \begin{cases} \log_2 |\mathcal{N}| & \text{if } x \in \mathcal{N}, \\ \log_2 |\Sigma| & \text{if } x \in \Sigma. \end{cases}

The first two terms describe the sizes of the nonterminal and terminal alphabets. Within the sum, the encoding records the number of productions for each nonterminal, the length of each right-hand side and the symbols in that sequence, using the alphabet to which each symbol belongs. Converting to nats gives the log prior −w ℓ(MS)ln⁡2-w\,\ell(M_S)\ln 2, where the weight ww defaults to one. Extra productions and longer right-hand sides therefore carry an explicit description cost which favours smaller grammars.

Parameter prior and the marginal likelihood

The parameters are the production probabilities, with an independent symmetric Dirichlet prior for each left-hand side. Integrating a production-count likelihood against this prior gives the following Dirichlet integral for kk productions with counts nin_i:

log⁡P(n∣α)=log⁡Γ(kα)−log⁡Γ(n+kα)+∑i=1k[log⁡Γ(ni+α)−log⁡Γ(α)],\log P(\mathbf{n} \mid \alpha) = \log\Gamma(k\alpha) - \log\Gamma(n + k\alpha) + \sum_{i=1}^{k}\bigl[\log\Gamma(n_i + \alpha) - \log\Gamma(\alpha)\bigr],

Here Γ\Gamma is the gamma function, n=∑inin = \sum_i n_i, and α\alpha is the concentration assigned to each production, giving kαk\alpha virtual samples in total. The implementation evaluates log⁡Γ\log\Gamma using a Lanczos approximation. It also needs the digamma function below, which it evaluates using an asymptotic expansion and the recurrence ψ(x+1)=ψ(x)+1/x\psi(x+1) = \psi(x) + 1/x.

Ambiguity and the variational bound

When each corpus string has one parse, the likelihood factors over production counts and this integral gives the exact evidence. For an ambiguous string, the likelihood sums over parses, so there is no single set of counts from which to obtain the evidence by multiplying these per-production terms. The implementation provides two scores for this case. The first computes the exact inside likelihood at fitted parameters and adds a Dirichlet Occam correction, giving an approximation that is exact on unambiguous corpora. The second uses a mean-field variational lower bound:

log⁡P(X∣M)  ≥  log⁡Z(θˉ)−KL(q(θ) ∥ p(θ)),θˉi=exp⁡(ψ(βi)−ψ(B)),βi=α+E[ni].\log P(X \mid M) \;\ge\; \log Z(\bar\theta) - \mathrm{KL}\bigl(q(\theta) \,\|\, p(\theta)\bigr), \qquad \bar\theta_i = \exp\bigl(\psi(\beta_i) - \psi(B)\bigr), \qquad \beta_i = \alpha + \mathbb{E}[n_i].

The variational distribution q(θ)q(\theta) is Dirichlet with pseudocounts βi\beta_i, and B=∑iβiB = \sum_i \beta_i. Its geometric mean parameters θˉi\bar\theta_i act as unnormalised weights for the inside computation, whose value is ZZ. With the parse distribution chosen consistently with these weights, its entropy combines with the expected log likelihood to give log⁡Z\log Z. The bound then subtracts the KL divergence between the variational parameter distribution and the prior:

KL(q ∥ p)=log⁡Γ(B)∏iΓ(βi)−log⁡Γ(kα)∏iΓ(α)+∑i(βi−α)(ψ(βi)−ψ(B)).\mathrm{KL}\bigl(q \,\|\, p\bigr) = \log\frac{\Gamma(B)}{\prod_i \Gamma(\beta_i)} - \log\frac{\Gamma(k\alpha)}{\prod_i \Gamma(\alpha)} + \sum_i (\beta_i - \alpha)\bigl(\psi(\beta_i) - \psi(B)\bigr).

Each variational iteration computes the geometric mean weights, obtains expected production counts with inside and outside, and adds those counts to the prior pseudocounts. The following sketch shows the updates:

for _ = 1 to em_iters do
  set_theta_bar ();
  let acc = expected_counts corpus in
  for i = 0 to np - 1 do beta.(i) <- alpha +. acc.(i) done;
  kl := dirichlet_kl beta
done;
elbo := log_z theta_bar () -. !kl

The scores have the same form at their respective fixed points, but this does not make them equal in general. At an expectation maximisation fixed point, the first score is the Dirichlet integral of the expected production counts plus the entropy of the posterior over parses. The variational bound has that form at its own fixed point, using counts and a parse posterior obtained from θˉ\bar\theta instead of maximum likelihood parameters. They coincide when every parse of a given string has the same production-count vector, including unambiguous corpora and grammars such as S→SS∣aS \to SS \mid a. On S→SS∣SSS∣a∣b∣abS \to SS \mid SSS \mid a \mid b \mid ab, the differences are 0.014 nats at α=0.1\alpha = 0.1 and 1.42 nats at α=2\alpha = 2, with opposite signs. The first score is consequently not a bound on the marginal likelihood. An earlier version of this article reported agreement to machine precision on every tested grammar, which described those tests but did not establish agreement in general.

Parsing and parameter estimation

Since right-hand sides are arbitrary sequences rather than Chomsky normal form pairs, parsing uses a generalised inside recursion over spans. Memoisation keys consist of a nonterminal and the two span endpoints, and each production enumerates the ways to divide its span among the symbols on the right. There are no epsilon productions, so every symbol consumes at least one token and a production with several symbols recurses on strictly smaller spans. Unit productions can retain the same span, which is why normalisation must remove unit cycles before parsing. The sketch below uses ordinary probabilities, while the implementation stores inside and outside quantities as logarithms and combines alternatives using a stable log sum:

let rec inside_nt nt i j =
  if j <= i then 0.0
  else match Hashtbl.find_opt imemo (nt, i, j) with
    | Some v -> v
    | None ->
        let v = List.fold_left (fun acc pi ->
          acc +. probs.(pi) *. seq_inside prods.(pi).rhs i j) 0.0 (by_lhs nt) in
        Hashtbl.replace imemo (nt, i, j) v; v

and seq_inside rhs i j =
  match rhs with
  | [] -> if i = j then 1.0 else 0.0
  | Term a :: rest ->
      if i < n && String.equal w.(i) a then seq_inside rest (i + 1) j else 0.0
  | Nonterm x :: rest ->
      let acc = ref 0.0 in
      for k = i + 1 to j - List.length rest do
        acc := !acc +. inside_nt x i k *. seq_inside rest k j
      done;
      !acc

The inside value I(A,i,j)I(A,i,j) sums the probabilities of all parses in which AA derives wi+1⋯wjw_{i+1}\cdots w_j. Thus I(S,0,n)I(S,0,n) gives the string probability including ambiguous derivations. A Viterbi variant selects the best parse for inspection, while combining the inside and outside passes gives the expected count of each production:

E[np]=1P(X)∑i≤jO(A,i,j) θA→γ∏k=1mI(γk,⋅,⋅),\mathbb{E}[n_p] = \frac{1}{P(X)} \sum_{i \le j} O(A,i,j)\,\theta_{A \to \gamma} \prod_{k=1}^{m} I(\gamma_k, \cdot, \cdot),

Here O(A,i,j)O(A,i,j) is the outside probability for AA over the same interval. Expectation maximisation normalises the expected counts separately for each left-hand side, and the resulting production probabilities supply the likelihood and Occam correction.

Operators

The search edits a grammar by choosing either a pair of nonterminals to merge or a sequence of symbols to chunk. Merging replaces both nonterminals with one survivor carrying the union of their productions, which can enlarge the language and shorten its description. If either nonterminal is the start symbol, that name survives. Otherwise the lexicographically smaller name survives, so reversing the input pair gives the same result:

let apply_merge g a b =
  let survivor, removed =
    if String.equal a g.start then (a, b)
    else if String.equal b g.start then (b, a)
    else if String.compare a b <= 0 then (a, b) else (b, a)
  in
  let map = function
    | Nonterm x when String.equal x removed -> Nonterm survivor
    | s -> s
  in
  let prods = List.map (fun p ->
    let lhs = if String.equal p.lhs removed then survivor else p.lhs in
    { p with lhs; rhs = List.map map p.rhs }) g.productions
  in
  copy_with g prods

Chunking introduces a fresh nonterminal for a contiguous sequence of nonterminals. Its production expands to the sequence it replaces, so the operation preserves the generated language, although adding the nonterminal and production usually lowers the score initially. Later merges may recover that cost, requiring the search to look beyond the immediate result. The paper uses beam search for this purpose. It does not specify how to replace repeated occurrences within one right-hand side, so the implementation supports both replacing every nonoverlapping occurrence and replacing only the leftmost occurrence:

let apply_chunk ?(occurrence = All_occurrences) g seq =
  let name = fresh_name g "X" in
  let replace = match occurrence with
    | All_occurrences -> replace_all
    | First_occurrence -> replace_first
  in
  let total = ref 0 in
  let prods = List.map (fun p ->
    let rhs, n = replace seq name p.rhs in
    total := !total + n; { p with rhs }) g.productions
  in
  copy_with g (prods @ [ { lhs = name; rhs = seq;
                           count = float_of_int (max 1 !total) } ])

Normalisation follows each edit. It combines duplicate productions and sums their counts, removes self loops and unit cycles, and drops nonproductive or unreachable nonterminals. It also removes a production when the left-hand side can derive its right-hand side using the other productions. This last check uses a complete CYK-style decision procedure over sentential forms. It reproduces the deletions in Figure 2 of the paper, including removing S→AXBS \to AXB once S→ASBS \to ASB and X→ABX \to AB make it derivable.

Search

A chunk may need several subsequent merges before the posterior improves, so accepting only immediate improvements can stop the search before it reaches a useful grammar. The implementation offers the paper's beam search and a best-first frontier. Both retain candidate grammars, score them by the posterior and expand promising candidates, stopping after a fixed number of expansions without improvement. Each proposal is logged with its score and parent, and each accepted step produces Graphviz diagrams of the grammar before and after the edit. In the running example, chunking reduces the score from −72.35-72.35 to −74.23-74.23, before merging raises it to −62.51-62.51. Retaining the intermediate grammar allows the search to reach that improvement.

Figure 1. The two accepted steps on the running example, with the top panel showing the grammar after chunk (T_a T_b), where the score falls, and the bottom panel after merge S,X, which produces the recursive production S→TaSTbS \to T_a S T_b. Productions and nonterminals changed by each step are drawn with a heavier outline.

Experiments

The experiment suite uses the eight test grammars from Cook et al. listed in Table 1 of the paper, including the palindrome language wcwRwcw^R with a centre marker. Each is recovered up to a finite comparison length. The suite does not include wwRww^R without a marker, which Cook et al. report as beyond their method and which Stolcke and Omohundro infer with beam search. It also omits the relative clause grammar at the end of Section 3.3. For the running example, the implementation reaches S→TaTb∣TaSTbS \to T_a T_b \mid T_a S T_b, generating anbna^n b^n, in two accepted steps. Figure 2 in the paper takes four because normalisation here removes S→TaXTbS \to T_a X T_b as soon as the merge makes that production derivable.

Section 4 explains why absolute frequencies affect generalisation. Multiplying all corpus counts by a constant multiplies the likelihood cost of a generalisation by that constant, while leaving its saving in the structural prior fixed. With the three strings in the ratio six to three to one, the implementation generalises to anbna^n b^n at every tested size up to 200 samples, although the posterior gain decreases as the corpus grows. From 300 samples onwards it accepts no edit and retains the memorising grammar. Counts of 1717, 1717 and 1616 also prevent generalisation, despite totalling the same 50 samples as 3030, 1515 and 55. In that case the distribution matters: the recursive grammar assigns a geometric distribution to string lengths, fitting decaying frequencies better than nearly uniform ones. The corresponding log likelihoods are −68.62-68.62 and −47.74-47.74.

The table gives the results for each target using a fixed seed, beam width three, strings of at most seven symbols and prior concentration 0.1. Its last column records whether the induced grammar is recursive, as required by the nested and embedding targets.

TargetNontermsProdsInitialFinalHeld outExactRecursive
parens35−124.81−100.91−0.438yesyes
a2n23−60.67−54.43−0.290yesyes
abn34−68.02−58.54−0.290yesyes
anbn34−76.14−68.73−0.336yesyes
palindrome35−198.88−120.80−0.717yesyes
addition47−203.57−134.30−1.120yesyes
shape57−251.23−140.65−0.646yesyes
basic_english1122−470.39−286.80−0.986yesno
nested_parens35−267.46−149.87−0.794yesyes
nested_anbn34−93.67−68.16−0.481yesyes
expr48−424.16−223.34−1.291yesyes

The induced languages match their targets up to the comparison length, and the finite English grammar remains nonrecursive. For a2na^{2n}, (ab)n(ab)^n, anbna^n b^n and anc bna^n c\, b^n, only three or four strings have length at most seven, and all occur among the 50 training samples. Their held-out likelihoods and finite language comparisons therefore establish fit within that range; the induced recursion supplies the evidence of generalisation. The arithmetic target has another limitation: the search merges ++ and ∗* into one operator class and collapses the three precedence levels into a single nonterminal. This produces the right strings with an ambiguous grammar that represents no precedence. The runs are deterministic given the seed and beam width.

Reproduction notes

The paper leaves some choices unspecified. The implementation supplies a description-length encoding around the stated log⁡N\log N cost per nonterminal occurrence and exposes a prior weight because the paper writes exp⁡(−ℓ)\exp(-\ell) while measuring ℓ\ell in bits. It also exposes the unspecified Dirichlet concentration and supports both interpretations of chunking repeated occurrences through a flag. The accompanying report records these choices and the complete derivability check used in place of the earlier bounded pruning search.

Demo

Induced grammar

  
Accepted transformations

    
Posterior