Skip to content

Commit

Permalink
Added an example for template modelling. Also a bug occured for sampl…
Browse files Browse the repository at this point in the history
…ing in circuits. However this is not fixed yet.
  • Loading branch information
tomsch420 committed Feb 2, 2024
1 parent 3fce8bc commit 245ed46
Show file tree
Hide file tree
Showing 8 changed files with 3,939 additions and 19 deletions.
7 changes: 7 additions & 0 deletions doc/examples.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
========
Examples
========
.. nbgallery:: examples
examples/nyga_distribution
examples/joint_probability_trees
examples/truncated_gaussians
4 changes: 1 addition & 3 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ You can read more about the theory behind probabilistic models in these resource

distributions
probabilistic_circuits
examples/nyga_distribution
examples/joint_probability_trees
examples/truncated_gaussians
examples

.. bibliography::
:all:
Expand Down
3,793 changes: 3,793 additions & 0 deletions examples/template_modelling.ipynb

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/probabilistic_model/distributions/distributions.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ def to_json(self) -> Dict[str, Any]:
"variable": self.variable.to_json()
}

def plotly_layout(self) -> Dict[str, Any]:
"""
:return: The layout argument for plotly figures as dict
"""
return {
"title": f"{self.__class__.__name__}",
"xaxis": {"title": self.variable.name}
}


class ContinuousDistribution(UnivariateDistribution):
"""
Expand Down
31 changes: 23 additions & 8 deletions src/probabilistic_model/learning/nyga_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import plotly.graph_objects as go
import portion
from random_events.events import Event
from random_events.variables import Continuous
from random_events.variables import Continuous, Variable
from typing_extensions import Self

from ..probabilistic_circuit.distributions import ContinuousDistribution, DiracDeltaDistribution, UniformDistribution
Expand Down Expand Up @@ -250,6 +250,13 @@ def __init__(self, variable: Continuous, min_samples_per_quantile: Optional[int]
self.min_samples_per_quantile = min_samples_per_quantile
self.min_likelihood_improvement = min_likelihood_improvement

@property
def variables(self) -> Tuple[Variable, ...]:
if len(self.subcircuits) > 0:
return DeterministicSumUnit.variables.fget(self)
else:
return self._variables

@property
def domain(self) -> Event:
return ProbabilisticCircuitMixin.domain.fget(self)
Expand Down Expand Up @@ -383,28 +390,36 @@ def cdf_trace(self) -> go.Scatter:
uniform: UniformDistribution = subcircuit
lower_value = uniform.interval.lower
upper_value = uniform.interval.upper
x += [lower_value, upper_value, None]
y += [self.cdf(lower_value), self.cdf(upper_value), None]
x += [lower_value, upper_value]
y += [self.cdf(lower_value), self.cdf(upper_value)]
self.reset_result_of_current_query()

x.extend([self.domain[self.variable].upper, self.domain[self.variable].upper + domain_size * 0.05])
y.extend([1, 1])
return go.Scatter(x=x, y=y, mode='lines', name="Cumulative Distribution Function")

def mode_trace(self) -> Tuple[go.Scatter, float]:
modes, maximum_likelihood = self.mode()
xs = []
ys = []
for mode in modes[0][self.variable]:
xs.extend([mode.lower, mode.lower, mode.upper, mode.upper, None])
ys.extend([0, maximum_likelihood * 1.05, maximum_likelihood * 1.05, 0, None])

trace = go.Scatter(x=xs, y=ys, mode='lines+markers', name="Mode", fill="toself")
return trace, maximum_likelihood

def plot(self) -> List[go.Scatter]:
"""
Plot the distribution with PDF, CDF, Expectation and Mode.
"""
traces = [self.pdf_trace(), self.cdf_trace()]
mode, maximum_likelihood = self.mode()
mode_trace, maximum_likelihood = self.mode_trace()
self.reset_result_of_current_query()
mode = mode[0][self.variable]

expectation = self.expectation([self.variable])[self.variable]
traces.append(mode_trace)
self.reset_result_of_current_query()
traces.append(go.Scatter(x=[mode.lower, mode.lower, mode.upper, mode.upper, ],
y=[0, maximum_likelihood * 1.05, maximum_likelihood * 1.05, 0], mode='lines+markers',
name="Mode", fill="toself"))
traces.append(go.Scatter(x=[expectation, expectation], y=[0, maximum_likelihood * 1.05], mode='lines+markers',
name="Expectation"))
return traces
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ def cache_inference_result(func):
def wrapper(*args, **kwargs):

self: ProbabilisticCircuitMixin = args[0]

if not self.cache_result:
return func(*args, **kwargs)

if self.result_of_current_query is None:
self.result_of_current_query = func(*args, **kwargs)
return self.result_of_current_query
Expand Down Expand Up @@ -305,16 +303,15 @@ def sample(self, amount: int) -> Iterable:
"""
Sample from the sum node using the latent variable interpretation.
"""

weights, subcircuits = zip(*self.weighted_subcircuits)

# sample the latent variable
states = random.choices(list(range(len(weights))), weights=weights, k=amount)

# sample from the children
result = []
for index, subcircuit in enumerate(self.subcircuits):
result.extend(subcircuit.sample(states.count(index)))

return result

@cache_inference_result
Expand Down Expand Up @@ -603,19 +600,30 @@ def _conditional(self, event: EncodedEvent) -> Tuple[Self, float]:
@cache_inference_result
def sample(self, amount: int) -> List[List[Any]]:

# load on variables
variables = self.variables

# list for the samples content in the same order as self.variables
rearranged_sample = [[None] * len(variables)] * amount
rearranged_samples = [[None] * len(variables)] * amount

# for every subcircuit
for subcircuit in self.subcircuits:

# sample from the subcircuit
sample_subset = subcircuit.sample(amount)


# for each sample from the subcircuit
for sample_index in range(amount):

# for each variable and its index of the subcircuit
for child_variable_index, variable in enumerate(subcircuit.variables):
rearranged_sample[sample_index][variables.index(variable)] = sample_subset[sample_index][

# find the index of the variable in the variables of the product
rearranged_samples[sample_index][variables.index(variable)] = sample_subset[sample_index][
child_variable_index]

return rearranged_sample
return rearranged_samples

@cache_inference_result
def moment(self, order: OrderType, center: CenterType) -> MomentType:
Expand Down Expand Up @@ -672,6 +680,27 @@ def __copy__(self):
result.probabilistic_circuit.add_edge(result, copied_subcircuit)
return result

def is_decomposable(self) -> bool:
"""
Check if only this product unit is decomposable.
A product mode is decomposable iff all children have disjoint scopes.
:return: if this product unit is decomposable
"""
# for every child pair
for subcircuits_a, subcircuits_b in itertools.combinations(self.subcircuits, 2):

# form the intersection of the scopes
scope_intersection = set(subcircuits_a.variables) & set(subcircuits_b.variables)

# if this not empty, the product unit is not decomposable
if len(scope_intersection) > 0:
return False

# if every pairwise intersection is empty, the product unit is decomposable
return True


class ProbabilisticCircuit(ProbabilisticModel, nx.DiGraph, SubclassJSONSerializer):
"""
Expand Down Expand Up @@ -755,7 +784,7 @@ def marginal(self, variables: Iterable[Variable]) -> Optional[Self]:
root.reset_result_of_current_query()
return result.probabilistic_circuit

@graph_inference_caching_wrapper
# @graph_inference_caching_wrapper
def sample(self, amount: int) -> Iterable:
return self.root.sample(amount)

Expand All @@ -770,6 +799,17 @@ def domain(self) -> Event:
root.reset_result_of_current_query()
return result

def is_decomposable(self) -> bool:
"""
Check if the whole circuit is decomposed.
A circuit is decomposed if all its product units are decomposed.
:return: if the whole circuit is decomposed
"""
return all([subcircuit.is_decomposable() for subcircuit in self.leaves
if isinstance(subcircuit, DecomposableProductUnit)])

def __eq__(self, other: 'ProbabilisticCircuit'):
return self.root == other.root

Expand Down
14 changes: 14 additions & 0 deletions test/test_jpt/test_nyga_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,19 @@ def test_deep_mount(self):
self.assertEqual(len(n2.probabilistic_circuit.nodes), 3)


class NygaDistributionTestCase(unittest.TestCase):
x: Continuous = Continuous("x")
model: NygaDistribution

def setUp(self) -> None:
self.model = NygaDistribution(self.x)
self.model.add_subcircuit(UniformDistribution(self.x, portion.closed(-1.5, -0.5)), 0.5)
self.model.add_subcircuit(UniformDistribution(self.x, portion.closed(0.5, 1.5)), 0.5)

def test_plot(self):
fig = go.Figure(self.model.plot())
self.assertIsNotNone(fig)
# fig.show()

if __name__ == '__main__':
unittest.main()
44 changes: 44 additions & 0 deletions test/test_probabilistic_circuits/test_graph_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,5 +394,49 @@ def test_mount_with_interaction(self):
self.assertEqual(1, self.sum_unit_1.probability(Event()))


class MountedInferenceTestCase(unittest.TestCase, ShowMixin):

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

probabilities = np.array([[0, 1],
[1, 0]])
model: DeterministicSumUnit

def setUp(self):
model = DeterministicSumUnit()
model.add_subcircuit(UniformDistribution(self.x, portion.closed(-1.5, -0.5)), 0.5)
model.add_subcircuit(UniformDistribution(self.x, portion.closed(0.5, 1.5)), 0.5)
next_model = model.__copy__()
for leaf in next_model.leaves:
leaf._variables = (self.y,)

transition_model = MultinomialDistribution([model.latent_variable, next_model.latent_variable],
self.probabilities)
next_model.mount_with_interaction_terms(model, transition_model)
self.model = next_model

def test_setup(self):
self.assertEqual(self.model.variables, (self.x, self.y))
self.assertTrue(self.model.probabilistic_circuit.is_decomposable())

def test_sample_from_uniform(self):
for leaf in self.model.leaves:
samples = leaf.sample(2)
self.assertNotEqual(samples[0], samples[1])

@unittest.skip("Sampling multiple things with undirected cycles is weird")
def test_sample(self):
# self.show(self.model)
samples: List = self.model.probabilistic_circuit.sample(2)
self.assertEqual(len(samples), 2)
print(samples)
self.assertNotEqual(samples[0], samples[1])

def test_samples_in_sequence(self):
samples = self.model.probabilistic_circuit.sample(1) + self.model.probabilistic_circuit.sample(1)
self.assertEqual(len(samples), 2)
self.assertNotEqual(samples[0], samples[1])

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

0 comments on commit 245ed46

Please sign in to comment.