Skip to content

Commit

Permalink
Integrated complete product algebra from random events.
Browse files Browse the repository at this point in the history
  • Loading branch information
tomsch420 committed Mar 18, 2024
1 parent 79986ac commit e8c0fcc
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 30 deletions.
2 changes: 1 addition & 1 deletion src/probabilistic_model/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.3.9"
__version__ = "4.0.1"
9 changes: 1 addition & 8 deletions src/probabilistic_model/bayesian_network/bayesian_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def interaction_term(self, node_latent_variable: Discrete, parent_latent_variabl
class BayesianNetwork(ProbabilisticModel, nx.DiGraph):
"""
Class for Bayesian Networks that are rooted, tree shaped and have univariate inner nodes.
This class cannot perform inference, but can be converted to a probabilistic circuit which can.
"""

def __init__(self):
Expand Down Expand Up @@ -161,14 +162,6 @@ def forward_pass(self, event: EncodedEvent):
for node in nx.bfs_tree(self, self.root):
node.forward_pass(event)

def _probability(self, event: EncodedEvent) -> float:
self.forward_pass(event)
result = 1.

for node in self.nodes:
result *= node.forward_probability
return result

def brute_force_joint_distribution(self) -> MultinomialDistribution:
"""
Compute the joint distribution of this bayes network variables by brute force.
Expand Down
17 changes: 10 additions & 7 deletions src/probabilistic_model/bayesian_network/distributions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import numpy as np
from matplotlib import pyplot as plt
from random_events.events import Event, EncodedEvent, VariableMap
from random_events.events import Event, EncodedEvent, VariableMap, ComplexEvent
from typing_extensions import Tuple, Dict, Iterable, List, Type, Union, Optional, Self

from .bayesian_network import BayesianNetworkMixin
Expand All @@ -23,7 +23,7 @@ class DiscreteDistribution(BayesianNetworkMixin, PCDiscreteDistribution):

forward_message: Optional[PCDiscreteDistribution]

def forward_pass(self, event: EncodedEvent):
def forward_pass(self, event: ComplexEvent):
self.forward_message, self.forward_probability = self._conditional(event)

def joint_distribution_with_parent(self) -> DeterministicSumUnit:
Expand Down Expand Up @@ -71,7 +71,7 @@ def _likelihood(self, event: Iterable) -> float:
node_event = tuple(event[1:])
return self.conditional_probability_distributions[parent_event]._likelihood(node_event)

def forward_pass(self, event: EncodedEvent):
def forward_pass(self, event: ComplexEvent):

# if the parent distribution is None, the forward message is None since it is an impossible event
if self.parent.forward_message is None:
Expand All @@ -86,17 +86,20 @@ def forward_pass(self, event: EncodedEvent):
forward_probability = 0

# for every parent state
for parent_state in event[self.parent.variable]:
for parent_state in event.marginal_event(self.parent.variables).simplify().events[0][self.parent.variable]:

# wrap the parent state
parent_state = (parent_state,)

# calculate the probability of said state
parent_state_probability = self.parent.forward_message.likelihood(parent_state)

event_for_parent = ComplexEvent([Event({self.parent.variable: parent_state})])
# construct the conditional distribution
conditional, current_probability = (self.conditional_probability_distributions[parent_state]
._conditional(event))
# conditional, current_probability = (self.conditional_probability_distributions[parent_state]
# ._conditional(event))

conditional, current_probability = self.conditional_probability_distributions[parent_state].conditional(event_for_parent)

# if the conditional is None, skip
if conditional is None:
Expand Down Expand Up @@ -205,7 +208,7 @@ def __init__(self, variables: Iterable[Variable]):
self.conditional_probability_distributions = dict()

def forward_pass(self, event: EncodedEvent):
forward_message, self.forward_probability = self.joint_distribution_with_parent()._conditional(event)
forward_message, self.forward_probability = self.joint_distribution_with_parent()._conditional_from_single_event(event)
self.forward_message = forward_message.marginal(self.variables)

def joint_distribution_with_parent(self) -> DeterministicSumUnit:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Iterable

from random_events.events import EncodedEvent, Event
from random_events.events import EncodedEvent, Event, ComplexEvent
from random_events.variables import Variable
from typing_extensions import Union, Tuple, Optional, Self

Expand Down Expand Up @@ -33,6 +33,11 @@ def variables(self, variables: Iterable[Variable]):
def __hash__(self):
return ProbabilisticCircuitMixin.__hash__(self)

@cache_inference_result
def _conditional_from_single_event(self, event: EncodedEvent) -> \
Tuple[Optional[Union['ContinuousDistribution', 'DiracDeltaDistribution', DeterministicSumUnit]], float]:
return super().conditional(event)

@cache_inference_result
def simplify(self) -> Self:
return self.__copy__()
Expand Down Expand Up @@ -85,11 +90,6 @@ def conditional_from_singleton(self, singleton: portion.Interval) -> \
conditional, probability = super().conditional_from_singleton(singleton)
return DiracDeltaDistribution(conditional.variable, conditional.location, conditional.density_cap), probability

@cache_inference_result
def _conditional(self, event: EncodedEvent) -> \
Tuple[Optional[Union['ContinuousDistribution', 'DiracDeltaDistribution', DeterministicSumUnit]], float]:
return super()._conditional(event)

@cache_inference_result
def marginal(self, variables: Iterable[Variable]) -> Optional[Self]:
return PMContinuousDistribution.marginal(self, variables)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,43 @@ def filter_variable_map_by_self(self, variable_map: VariableMap):
return variable_map.__class__(
{variable: value for variable, value in variable_map.items() if variable in variables})

def _conditional(self, event: ComplexEvent) -> Tuple[Optional[Self], float]:

# skip trivial case
if len(event.events) == 0:
return None, 0

# if the event is easy, don't create a proxy node
elif len(event.events) == 1:
return self._conditional_from_single_event(event.events[0])

# construct the proxy node
result = DeterministicSumUnit()
total_probability = 0

for event_ in event.events:
conditional, probability = self._conditional_from_single_event(event_)

# skip if impossible
if probability == 0:
continue

total_probability += probability
result.add_subcircuit(conditional, probability)

if total_probability == 0:
return None, 0

result.normalize()

return result, total_probability

def _conditional_from_single_event(self, event: EncodedEvent) -> Tuple[Optional[Self], float]:
"""
:return: the conditional circuit from a single, encoded event
"""
raise NotImplementedError

@property
def variables(self) -> Tuple[Variable, ...]:
variables = set([variable for distribution in self.leaves for variable in distribution.variables])
Expand Down Expand Up @@ -545,7 +582,7 @@ def _probability(self, event: EncodedEvent) -> float:
return result

@cache_inference_result
def _conditional(self, event: EncodedEvent) -> Tuple[Optional[Self], float]:
def _conditional_from_single_event(self, event: EncodedEvent) -> Tuple[Optional[Self], float]:

subcircuit_probabilities = []
conditional_subcircuits = []
Expand All @@ -554,7 +591,7 @@ def _conditional(self, event: EncodedEvent) -> Tuple[Optional[Self], float]:
result = self.empty_copy()

for weight, subcircuit in self.weighted_subcircuits:
conditional, subcircuit_probability = subcircuit._conditional(event)
conditional, subcircuit_probability = subcircuit._conditional_from_single_event(event)

if subcircuit_probability == 0:
continue
Expand Down Expand Up @@ -930,7 +967,7 @@ def _mode(self) -> Tuple[ComplexEvent, float]:
return mode, likelihood

@cache_inference_result
def _conditional(self, event: EncodedEvent) -> Tuple[Self, float]:
def _conditional_from_single_event(self, event: EncodedEvent) -> Tuple[Self, float]:
# initialize probability
probability = 1.

Expand All @@ -940,7 +977,7 @@ def _conditional(self, event: EncodedEvent) -> Tuple[Self, float]:
for subcircuit in self.subcircuits:

# get conditional child and probability in pre-order
conditional_subcircuit, conditional_probability = subcircuit._conditional(event)
conditional_subcircuit, conditional_probability = subcircuit._conditional_from_single_event(event)

# if any is 0, the whole probability is 0
if conditional_probability == 0:
Expand Down
6 changes: 2 additions & 4 deletions test/test_jpt/test_jpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,6 @@ def test_to_bayesian_network(self):
p_sepal_species.from_multinomial_distribution(self.species_sepal_interaction_term)
bayesian_network.add_node(p_sepal_species)
bayesian_network.add_edge(root, p_sepal_species)
self.assertEqual(bayesian_network.probability(Event()), 1.)

# mount the distributions of the sepal variables
p_sepal = ConditionalProbabilisticCircuit(self.model_sl_sw.variables)
Expand All @@ -489,12 +488,11 @@ def test_to_bayesian_network(self):
bayesian_network.add_edge(p_petal_species, p_petal)

# test some queries
self.assertEqual(bayesian_network.probability(Event()), 1.)
self.assertAlmostEqual(bayesian_network.as_probabilistic_circuit().probability(Event()), 1)

e_species_1 = Event({self.species: 0})
bn_p_species_1 = bayesian_network.probability(e_species_1)
self.assertAlmostEqual(bn_p_species_1, 1 / 3)
# bn_p_species_1 = bayesian_network.probability(e_species_1)
# self.assertAlmostEqual(bn_p_species_1, 1 / 3)
self.assertAlmostEqual(bayesian_network.as_probabilistic_circuit().probability(e_species_1), 1 / 3)

complex_event = Event({self.species: 0,
Expand Down
28 changes: 28 additions & 0 deletions test/test_probabilistic_circuits/test_graph_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,5 +561,33 @@ def test_plot_2d(self):
# go.Figure(traces, self.model.plotly_layout()).show()


class ComplexInferenceTestCase(unittest.TestCase):

x: Continuous = Continuous("x")
y: Continuous = Continuous("y")
model: ProbabilisticCircuit

e1: Event = Event({x: portion.closed(0, 1), y: portion.closedopen(0, 1)})
e2: Event = Event({x: portion.closed(1.5, 2), y: portion.closed(1.5, 2)})

event: ComplexEvent = e1 | e2

def setUp(self):
root = DecomposableProductUnit()
px = UniformDistribution(self.x, portion.closed(0, 2))
py = UniformDistribution(self.y, portion.closed(0, 3))
root.add_subcircuit(px)
root.add_subcircuit(py)
self.model = root.probabilistic_circuit

def test_complex_probability(self):
p = self.model.probability(self.event)
self.assertEqual(self.model.probability(self.e1) + self.model.probability(self.e2), p)

def test_complex_conditional(self):
conditional, probability = self.model.conditional(self.event)
self.assertAlmostEqual(conditional.probability(self.event), 1.)


if __name__ == '__main__':
unittest.main()

0 comments on commit e8c0fcc

Please sign in to comment.