Examples
We start with a simple model for Markov chains:
package blang.validation.internals.fixtures
model MarkovChain {
param Simplex initialDistribution
param TransitionMatrix transitionProbabilities
random List<IntVar> chain
laws {
// Initial distribution:
chain.get(0) | initialDistribution ~ Categorical(initialDistribution)
// Transitions:
for (int step : 1 ..< chain.size) {
chain.get(step) |
IntVar previous = chain.get(step - 1),
transitionProbabilities
~ Categorical(
if (previous >= 0 && previous < transitionProbabilities.nRows)
transitionProbabilities.row(previous)
else
transitionProbabilities.row(0)
)
}
}
}
Notice that we used the pre-computation construct, namely
IntVar previous = chain.get(step - 1)
Accessing an array is not so much expensive, so you may wonder why we bothered pre-computing this.
It turns out there is actually an important speed gain to be made, of the order of the length of the chain. Why?
To understand, we need to outline how the blang inference engines work under the hood.
Most of these engines exploit cases where conditional distributions only depend on subsets of the variables.
To do so, blang inspects the model constituents (for example the Categorical
constituents) to
infer what is the scope of the constituent. A scope is simply the subset of the variables
available at a given location of the code (e.g. the code in one function cannot access the local variables
declared in another function, they are out of scope). So coming back to the Markov chain example,
this means that by passing in the precomputed chain.get(step - 1)
rather than all the latent
variables, we make it possible for blang engines to infer that each time step in the HMM only have
interdependence with the previous and next state rather than all states.
In graphical model parlance, this means sparsity patterns in the graphical model are inferred.
Let us look now how we can use a Markov chain as a building block for an HMM:
package blang.validation.internals.fixtures
model DynamicNormalMixture {
param int nLatentStates
random List<RealVar> observations
random List<IntVar> states ?: latentIntList(observations.size)
random DenseSimplex initialDistribution ?: latentSimplex(nLatentStates)
random DenseTransitionMatrix transitionProbabilities ?: latentTransitionMatrix(nLatentStates)
random List<RealVar> means ?: latentRealList(nLatentStates), variances ?: latentRealList(nLatentStates)
param Matrix concentrations ?: ones(nLatentStates).readOnlyView
laws {
// Priors on initial and transition probabilities
initialDistribution | concentrations ~ Dirichlet(concentrations)
for (int latentStateIdx : 0 ..< means.size) {
transitionProbabilities.row(latentStateIdx) | concentrations ~ Dirichlet(concentrations)
}
// Priors on means and variances
for (int latentStateIdx : 0 ..< means.size) {
means.get(latentStateIdx) ~ Normal(0.0, 1.0)
variances.get(latentStateIdx) ~ Gamma(1.0, 1.0)
}
states | initialDistribution, transitionProbabilities
~ MarkovChain(initialDistribution, transitionProbabilities)
// Gaussian emissions
for (int obsIdx : 0 ..< observations.size) {
observations.get(obsIdx) |
means,
variances,
IntVar curIndic = states.get(obsIdx)
~ Normal(means.get(curIndic), variances.get(curIndic))
}
}
}
Undirected graphical models (AKA Markov random fields) are supported: here is
for example how a square Ising model is implemented:
package blang.validation.internals.fixtures
import briefj.collections.UnorderedPair
import static blang.validation.internals.fixtures.Functions.squareIsingEdges
model Ising {
param Double moment ?: 0.0
param Double beta ?: log(1 + sqrt(2.0)) / 2.0 // critical point
param Integer N ?: 5
random List<IntVar> vertices ?: latentIntList(N*N)
laws {
// Pairwise potentials
for (UnorderedPair<Integer, Integer> pair : squareIsingEdges(N)) {
| IntVar first = vertices.get(pair.getFirst),
IntVar second = vertices.get(pair.getSecond),
beta
~ LogPotential(
if ((first < 0 || first > 1 || second < 0 || second > 1))
return NEGATIVE_INFINITY
else
return beta*(2*first-1)*(2*second-1))
}
// Node potentials
for (IntVar vertex : vertices) {
vertex | moment ~ Bernoulli(logistic(-2.0*moment))
}
}
}
First, we create a custom data type:
package blang.types
import blang.core.RealVar
import blang.core.IntVar
import blang.types.StaticUtils
class SpikedRealVar implements RealVar {
public val IntVar selected = StaticUtils::latentInt
public val RealVar continuousPart = StaticUtils::latentReal
override doubleValue() {
if (selected.intValue < 0 || selected.intValue > 1)
StaticUtils::invalidParameter()
if (selected.intValue == 0) return 0.0
else return continuousPart.doubleValue
}
override toString() { "" + doubleValue }
}
We can now define a distribution for this type:
package blang.validation.internals.fixtures
model SpikeAndSlab {
random List<SpikedRealVar> variables
param RealVar zeroProbability
param RealDistribution nonZeroLogDensity
laws {
for (int index : 0 ..< variables.size) {
logf(zeroProbability, nonZeroLogDensity, RealVar variable = variables.get(index)) {
if (zeroProbability < 0.0 || zeroProbability > 1.0) return NEGATIVE_INFINITY
if (variable == 0.0) {
log(zeroProbability)
} else {
log(1.0 - zeroProbability) + nonZeroLogDensity.logDensity(variable)
}
}
logf(SpikedRealVar variable = variables.get(index)) {
if (variable.selected.isBool) return 0.0
else return NEGATIVE_INFINITY
}
variables.get(index) is Constrained
}
}
generate(rand) {
for (SpikedRealVar variable : variables) {
(variable.selected as WritableIntVar).set(Generators::bernoulli(rand, zeroProbability).asInt)
(variable.continuousPart as WritableRealVar).set(nonZeroLogDensity.sample(rand))
}
}
}
Afterwards, it is easy to incorporate Spike and Slab priors in more complicated models, for example a
naive GLM here:
package blang.validation.internals.fixtures
model SpikedGLM {
param Matrix designMatrix // n by p
random List<IntVar> output // size n
random List<SpikedRealVar> coefficients ?: {
val p = designMatrix.nCols
return new ArrayList(p) => [
for (int i : 0 ..< p)
add(new SpikedRealVar)
]
}
random RealVar zeroProbability ?: latentReal
laws {
zeroProbability ~ Beta(1,1)
coefficients | zeroProbability ~
SpikeAndSlab(zeroProbability, Normal::distribution(0, 1))
for (int index : 0 ..< output.size) {
output.get(index) | coefficients,
Matrix predictors = designMatrix.row(index)
~ Bernoulli(logistic({
var sum = 0.0
for (i : 0 ..< coefficients.size)
sum += coefficients.get(i).doubleValue * predictors.get(i)
sum
}))
}
}
}