tbnpy.probability

Overview

A probability object quantifies one node of a Bayesian network. tbnpy recognises one by what it has, not by what it inherits from, so a class written for one model needs no tbnpy base class to work. This module makes the protocol explicit and adds three things on top of it:

  • Probability, an optional base class that makes an object readable when it is printed;

  • describe(), the same reading for any probability object, whether it inherits the base class or not;

  • check_model(), which samples a few rows through a whole model to find an object that breaks the protocol before an inference run does.

Cpt and every node compiled by tbnpy.compile inherit Probability. The three names are also available as tbnpy.Probability, tbnpy.describe and tbnpy.check_model.

The protocol

Member

Meaning

childs

List of child Variable. Every variable of a model is the child of exactly one object.

parents

List of parent Variable.

sample(n_sample=N)

For a root (no parents). Returns (Cs, logp).

sample(Cs_pars=...)

For an object with parents. Cs_pars is (N, n_parents), in the order of parents. Returns (Cs, logp).

log_prob(Cs)

Cs is (N, n_childs + n_parents): childs first, then parents. Returns (N,) log probabilities (or log densities).

device

Where the object’s tensors live ("cpu", "cuda" or a torch.device).

In sample’s return value, Cs is (N,) for a single child or (N, n_childs), and logp is (N,): the log probability of each sampled row given its parents. log_prob of the same rows must return the same numbers.

Discrete values

A discrete variable’s value is its state index into Variable.values, not its label. It may be held in any dtype that stores whole numbers exactly: inference builds Cs_pars with torch.stack(), which promotes a mix of integer and float columns to a common dtype. So a class receiving discrete parents should cast them with .long() before using them as indices, and look up labels with Variable.values:

A_values = torch.tensor(A.values, dtype=torch.float32)   # labels, by state index
mean = A_values[Cs_pars[:, 0].long()]

Reading an object

With the base class, typing an object’s name shows the distribution it defines, and print gives each variable’s domain:

>>> probs
{'A': Cpt(P(A): table of 2 rows), 'B': Cpt(P(B): table of 3 rows),
 'C': C(P(C | A, B): normal(loc=A + B, scale=0.6)), ...}

>>> print(probs['C'])
P(C | A, B)  [C]
  form    normal(loc=A + B, scale=0.6)
  child   C  continuous (-inf, inf)
  parent  A  discrete, 2 states: -0.3, 0.3
  parent  B  discrete, 3 states: -0.2, 0.0, 0.2

The form is what the class says it is. Override form() to return one line, and form_details() to add lines such as a table (as Cpt does). Both are optional:

from tbnpy.probability import Probability

class C(Probability):
    def __init__(self, childs, parents, sigma=0.6, device="cpu"):
        super().__init__(childs, parents, device)    # sets childs, parents, device
        self.sigma = sigma

    def form(self):
        return f"normal(loc=A + B, scale={self.sigma:g})"

    def sample(self, Cs_pars): ...
    def log_prob(self, Cs): ...

For an object that does not inherit the base class, tbnpy.describe(obj) prints the same reading (without the form, unless the object has a form() method).

Checking a model

>>> print(tbnpy.check_model(probs))
Model check: 6 probability objects, 6 variables, 8 sampled rows: no problems found

check_model() raises ModelError when it finds an error, listing every issue; pass raise_on_error=False to receive the ModelCheck instead. It reports:

  • Without sampling: a missing childs, parents, sample or log_prob; a variable that is the child of two objects; a parent that is no object’s child; a parent declared with a different domain from its owner’s (a warning); a cycle.

  • By sampling n_sample rows in topological order, as tbnpy.inference.sample() does: an object that cannot be deep-copied or whose sample raises; Cs or logp of the wrong shape; a discrete value that is not a state index or a continuous value outside its support; a logp that is NaN or infinite; and a log_prob that disagrees with sample’s logp. An object whose log_prob raises NotImplementedError gets a warning, since forward sampling does not need it.

The check uses its own seed and restores the random number generator’s state when it finishes, so running it never changes the results of a seeded run that follows. Inference does not run it for you: call it once after building a model.

API

class tbnpy.probability.Probability(childs, parents=None, device='cpu')[source]

Optional base class for probability objects.

Subclassing it is not required: tbnpy accepts any object that follows the protocol in this module’s docstring. What it adds is a reading:

  • repr(obj) gives one line, ClassName(P(C | A, B): form), so a probs dictionary prints as a list of distributions;

  • str(obj) (and print(obj)) gives describe()’s longer reading, with each variable’s domain.

Override form() to say what the distribution is, and form_details() to add lines such as a table, for both readings.

Parameters:
  • childs – Lists of Variable.

  • parents – Lists of Variable.

  • device – "cpu", "cuda" or a torch.device.

  • sets (Calling this __init__ is optional too; a subclass that)

  • childs

  • readings. (parents and device itself gets the same)

form() → str[source]

One line on the distribution’s form, e.g. "normal(loc=A + B, scale=0.6)".

form_details() → list[source]

Extra lines for describe(), such as a probability table.

tbnpy.probability.describe(obj) → str[source]

Several lines on any probability object: what it is and what it reads.

Works on any object with childs and parents. When the object defines form() and form_details() (as Probability subclasses may), their text is included.

tbnpy.probability.check_model(probs, n_sample=8, seed=0, raise_on_error=True) → ModelCheck[source]

Check that every object in a model follows the protocol, by using it.

The check has two parts. Without sampling, it confirms that each object has childs, parents, sample and log_prob; that every variable is the child of exactly one object; that every parent is some object’s child, with the same domain; and that the graph has no cycle. Then it forward-samples n_sample rows through the whole model, as tbnpy.inference.sample() does, and for each object confirms:

  • sample returns (Cs, logp) of the right shapes;

  • sampled values lie in their variables’ domains (state indices for discrete variables, the support for continuous ones);

  • logp is finite;

  • log_prob of the sampled rows equals logp.

The random number generator’s state is restored afterwards, so a check never changes the results of a seeded run that follows it.

Parameters:
  • probs – {node name: probability object}.

  • n_sample – Rows to sample. A few are enough to find a broken object.

  • seed – Seed for the check’s own sampling, so that it is reproducible.

  • raise_on_error – Raise ModelError when there are errors. With False, the ModelCheck is returned either way.

Returns:

print it to read the issues; .ok is True without errors.

Return type:

ModelCheck

class tbnpy.probability.ModelCheck(issues: list = <factory>, n_objects: int = 0, n_variables: int = 0, n_sample: int = 0)[source]

What check_model() found.

property ok: bool

No errors. Warnings are allowed.

class tbnpy.probability.ModelIssue(node: str, severity: str, issue: str, detail: str = '')[source]

One thing wrong ("error") or doubtful ("warning") in a model.

class tbnpy.probability.ModelError(check: ModelCheck)[source]

Raised by check_model() when a model has errors.

tbnpy.probability.is_probability(obj) → bool[source]

Whether obj has everything tbnpy calls on a probability object.

tbnpy.probability.statement(obj) → str[source]

The distribution an object defines, written P(C | A, B).

tbnpy.probability.domain(var) → str[source]

A variable’s domain in words: its states, or its support.