Expectation propagation: Probit
The Probit node: a binary observation explained by a real latent variable through the standard normal CDF. Use it for probit regression and binary classification, where each label y is linked to a latent score x, itself normal, by y ~ Probit(x) in an RxInfer model.
It is also this package's example of an expectation propagation node: a node with an algorithm of its own, whose rule towards in reads the message on in itself and returns a normal that matches the moments of the exact product. A node that needs the same can follow its declaration and rules.
Overview
Probit observes a binary out, or the probability of a 1, through Φ(in). The likelihood of in under a binary observation is not a normal, so a normal message on in times that likelihood is not normal either.
The node's own algorithm, ProbitEP, keeps the messages normal by expectation propagation (EP). EP computes the mean and variance of the exact product, called the tilted distribution, and returns the normal message that, multiplied by the message on in, has that mean and variance. The message on in that the rule reads is called the cavity.
The node also runs under DefaultAlgorithm. There, the message towards in is the exact likelihood as a log-density, for a model that approximates non-normal messages elsewhere.
Definition
With Φ the standard normal CDF and out ∈ {0, 1}, or out = p ∈ [0, 1] for a soft label,
\[p(\mathrm{out} \mid \mathrm{in}) = \Phi(\mathrm{in})^{\mathrm{out}} \, \bigl(1 - \Phi(\mathrm{in})\bigr)^{1 - \mathrm{out}}, \qquad \Phi(x) = \int_{-\infty}^{x} \mathcal{N}(t \mid 0, 1) \, \mathrm{d}t.\]
Integrated against a normal message N(in | μ, v), the probability of a 1 is Φ(μ / √(1 + v)), which is the message towards out.
Interfaces
using StandardMessagePassingRules, MessagePassingRulesBase, ExponentialFamily, BayesBase
MessagePassingRulesBase.nodespec(Probit)| interface | aliases | |
|---|---|---|
out | ||
in |
| default algorithm | ProbitEP(32) |
|---|---|
| static inputs | none |
| initial messages | in => ExponentialFamily.NormalMeanPrecision{Float64}(μ=0.0, w=100.0) |
| defined | nodes/probit.jl:78 |
| name | aliases | meaning | messages and marginals its rules take |
|---|---|---|---|
out | none | the binary output, or the probability of a 1 | PointMass or Bernoulli, with its value in [0, 1] |
in | none | the latent input | a univariate normal, or a PointMass towards out |
An observed label reaches Probit as a point mass, a distribution with all its mass on the observed value. The drawn declaration lists the initial message on in, which the Algorithm section explains.
Algorithm
ProbitEP(; p = 32) is the node's own algorithm, so a model that names none runs it. Its keyword p is the number of Gauss–Hermite points of the average energy (GaussHermiteCubature); the messages are closed-form and ignore it. The algorithm declares its dependencies, the inputs each rule takes:
MessagePassingRulesBase.dependencies_spec(Probit, ProbitEP())| free-energy partition | from the factorisation |
|---|
The rule towards in reads the message on in itself, the cavity. Before any message has arrived there, the node declares NormalMeanPrecision(0.0, 100.0) as the cavity's starting value, which the engine sets wherever the model sets none.
DefaultAlgorithm() must be named by the model. It declares no dependencies, so its rules follow the factorisation. They read messages when out and in share a cluster, and marginals under mean field. The rule towards in does not read its own edge.
Supported rules
MessagePassingRulesBase.rule_coverage(Probit)| ProbitEP | DefaultAlgorithm | |
|---|---|---|
| → out | ✓×2 | ✓×3 |
| → in | ✓ | ✓×2 |
| average energy | ✓ | ✓ |
| q(out, in) | ✓ |
Each cell counts the rules of Probit for a target under an algorithm. Under ProbitEP the rules are expectation propagation. The messages read the messages on both edges. The joint marginal q(out, in) exists for a point-mass out, and there is an average energy.
Under DefaultAlgorithm there are belief propagation rules, from m(in) towards out and from m(out) towards in. There are mean-field rules for point-mass marginals, from q(in) towards out and from q(out) towards in, and an average energy. There is no joint marginal rule.
Example
The expectation-propagation message towards in, from an observed 1 and a standard normal cavity:
@call_message_update_rule(
node = Probit, target = :in,
m = (out = PointMass(1.0), in = NormalMeanVariance(0.0, 1.0)),
)Result
| value | ExponentialFamily.NormalWeightedMeanPrecision{Float64}(xi=0.827634, w=0.466942) |
|---|---|
| type | ExponentialFamily.NormalWeightedMeanPrecision{Float64} |
| log scale | undefined: the message rule for Probit towards :in under ProbitEP declares no `logscale` |
Inputs
| edge | value |
|---|
Rule
| declared inputs | m[:out]::Union{BayesBase.PointMass, Distributions.Bernoulli}, m[:in]::Union{ExponentialFamily.UnivariateNormalDistributionsFamily{T}, ExponentialFamily.UnivariateGaussianDistributionsFamily{T}} where T |
|---|---|
| algorithm | ProbitEP(32) |
| log scale | none |
| checks | check_probability(args.m[:out]) |
| defined | probit/in.jl:2 |
| body | args->begin
(mpz, vpz, mz, vz) = tilted_moments(mean(args.m[:out]), args.m[:in])
NormalWeightedMeanPrecision(mpz / vpz - mz / vz, clamp(1 / vpz - 1 / vz, tiny, huge))
end |
Other rules for this target (2)
probit/in.jl:14| ✗ | algorithm MessagePassingRulesBase.DefaultAlgorithm |
| ✓ | m[:out]::Union{BayesBase.PointMass, Distributions.Bernoulli} got BayesBase.PointMass{Float64} |
| ✗ | m[:in]::ExponentialFamily.NormalMeanVariance{Float64} provided but not consumed |
probit/in.jl:19| ✗ | algorithm MessagePassingRulesBase.DefaultAlgorithm |
| ✗ | q[:out]::BayesBase.PointMass not provided |
| ✗ | m[:in]::ExponentialFamily.NormalMeanVariance{Float64} provided but not consumed |
| ✗ | m[:out]::BayesBase.PointMass{Float64} provided but not consumed |
The card draws the cavity as an input on the target's own edge. The message times the cavity has the moments of the tilted distribution, Φ(in) N(in | 0, 1) normalised: mean √(2 / π) / √2 = 1 / √π ≈ 0.564 and variance 1 - 1 / π ≈ 0.682:
message = getresult(@call_message_update_rule(
node = Probit, target = :in,
m = (out = PointMass(1.0), in = NormalMeanVariance(0.0, 1.0)),
))
mean_var(prod(GenericProd(), NormalMeanVariance(0.0, 1.0), message))(0.5641895835477563, 0.6816901138162094)The message towards out, and the message towards in under DefaultAlgorithm:
julia> using StandardMessagePassingRules, MessagePassingRulesBase, ExponentialFamily, BayesBase
julia> result = @call_message_update_rule(node = Probit, target = :out, m = (in = NormalMeanVariance(1.0, 0.5),));
julia> mean(getresult(result)) ≈ 0.7928919108787374 # Φ(1 / √(1 + 0.5))
true
julia> result = @call_message_update_rule(
node = Probit, target = :in, m = (out = PointMass(1.0), in = NormalMeanPrecision(0.0, 1.0)),
);
julia> getresult(result) isa NormalWeightedMeanPrecision
true
julia> result = @call_message_update_rule(
node = Probit, target = :in, algorithm = DefaultAlgorithm(), m = (out = PointMass(1.0),),
);
julia> getresult(result) isa ContinuousUnivariateLogPdf
trueLimitations
inis univariate; there are no multivariate rules.- The value on
outmust lie in[0, 1]; the expectation-propagation rules towardsinand the joint marginal check it and throw aRuleInputErrorotherwise. - The joint marginal
q(out, in)exists only underProbitEPand only for a point-massout. - Under
DefaultAlgorithm, the mean-field rules take point-mass marginals only (q(in)towardsout,q(out)towardsin), and the message towardsinis aContinuousUnivariateLogPdf, not a normal: something downstream must approximate it. - The average energy computes
⟨-log Φ⟩by Gauss–Hermite cubature: withProbitEP(p = …)points, 32 by default, and with 32 underDefaultAlgorithm, which has no keyword for it. It is exact to rounding whileq(in)is narrow, a variance up to about 1, and loses accuracy as it broadens: about1e-4relative at a variance of 25 and2e-3at 400, and more points help slowly. The messages do not use the cubature. - No rule declares a log scale: where the engine tracks log scales (
logscales = true), a message from this node carries anUndefinedLogScale.
API
The node, and the algorithm it runs by default.
StandardMessagePassingRules.Probit — Type
ProbitThe probit node, out ~ Bernoulli(Φ(in)) with Φ the standard normal CDF: a binary output through a real latent input. It is stochastic, with the interfaces
out: the output, observed as aPointMassat0or1, or aBernoulli; its value, the probability of a1, lies in[0, 1];in: the latent input, a univariate normal.
Its algorithm is ProbitEP, and it also has rules under DefaultAlgorithm. Under DefaultAlgorithm the message towards in is the exact likelihood, as a log-density, rather than a normal. Both algorithms have an average energy, computed by GaussHermiteCubature. No rule declares a log scale.
Under ProbitEP the rule towards in reads the message on in itself, so in a graph with a loop through in it needs one to start from: the node declares NormalMeanPrecision(0.0, 100.0), set at activation wherever the model sets no initial message.
Throws
RuleInputError, from the expectation-propagation rules towardsinand the joint marginal, when the value onoutlies outside[0, 1].
See also ProbitEP.
StandardMessagePassingRules.ProbitEP — Type
ProbitEP(p::Int)
ProbitEP(; p = 32)Probit's own algorithm, expectation propagation. A model that names no algorithm for the node runs this one.
- towards
out:Bernoulli(Φ(μ / √(1 + v)))from a normal messageN(μ, v)onin, orBernoulli(Φ(x))from a point mass atx; - towards
in: the normal whose product with the message onin(the cavity) matches the mean and variance of the tilted distributionp(out | in) ⋅ m(in), from the message onoutand the message onin; its precision is clamped to be positive, and the tilted variance to be at most the cavity's; - the joint marginal
q(out, in), for a point-massoutonly:outitself and the tilted distribution ofin, as aNormalMeanVariance; - the average energy
E_q[-log p(out | in)], fromq(out)and a normalq(in).
Keywords
p: the number of Gauss–Hermite points of the average energy, a positive integer. Default32, the number the average energy underDefaultAlgorithmuses too. The messages are computed in closed form and do not use it.
Examples
julia> using MessagePassingRulesBase, ExponentialFamily, BayesBase
julia> energy = @call_average_energy(
node = Probit, algorithm = ProbitEP(p = 64), q = (out = PointMass(1.0), in = NormalMeanVariance(0.0, 1.0)),
);
julia> getresult(energy) ≈ 1.0 # E[-log Φ(z)] for a standard normal z
trueSee also Probit, and GaussHermiteCubature for the cubature.