From 1826222ed133b33f05fe0290fade25c2bde20729 Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Mon, 11 Dec 2017 19:13:32 -0800 Subject: discrete factor with minimal methods --- beliefs/factors/discrete_factor.py | 121 +++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 beliefs/factors/discrete_factor.py diff --git a/beliefs/factors/discrete_factor.py b/beliefs/factors/discrete_factor.py new file mode 100644 index 0000000..da8e6bf --- /dev/null +++ b/beliefs/factors/discrete_factor.py @@ -0,0 +1,121 @@ +import copy +import numpy as np + + +class DiscreteFactor: + + def __init__(self, variables, cardinality, values=None, state_names=None): + """ + Args + variables: list, + variables in the scope of the factor + cardinality: list, + cardinalities of each variable, where len(cardinality)=len(variables) + values: list, + row vector of values of variables with ordering such that right-most variables + defined in `variables` cycle through their values the fastest + state_names: dictionary, + mapping variables to their states, of format {label_name: ['state1', 'state2']} + """ + self.variables = list(variables) + self.cardinality = cardinality + if values is None: + self._values = None + else: + self._values = np.array(values).reshape(self.cardinality) + self.state_names = state_names + + def __mul__(self, other): + return self.product(other) + + @property + def values(self): + return self._values + + def update_values(self, new_values): + """We make this available because _values is allowed to be None on init""" + self._values = np.array(new_values).reshape(self.cardinality) + + def get_value_for_state_vector(self, dict_of_states): + """ + Return the value for a dictionary of variable states. + + Args + dict_of_states: dictionary, + of format {label_name1: 'state1', label_name2: 'True'} + Returns + probability, a float, the factor value for a specific combination of variable states + """ + assert sorted(dict_of_states.keys()) == sorted(self.variables), \ + "The keys for the dictionary of states must match the variables in factor scope." + state_coordinates = [] + for var in self.variables: + var_state = dict_of_states[var] + idx_in_var_axis = self.state_names[var].index(var_state) + state_coordinates.append(idx_in_var_axis) + return self.values[tuple(state_coordinates)] + + def add_new_variables_from_other_factor(self, other): + """Add new variables to the factor.""" + extra_vars = set(other.variables) - set(self.variables) + # if all of these variables already exist there is nothing to do + if len(extra_vars) == 0: + return + # otherwise, extend the values array + slice_ = [slice(None)] * len(self.variables) + slice_.extend([np.newaxis] * len(extra_vars)) + self._values = self._values[slice_] + self.variables.extend(extra_vars) + + new_card_var = other.get_cardinality(extra_vars) + self.cardinality.extend([new_card_var[var] for var in extra_vars]) + return + + def get_cardinality(self, variables): + return {var: self.cardinality[self.variables.index(var)] for var in variables} + + def product(self, other): + left = copy.deepcopy(self) + + if isinstance(other, (int, float)): + # TODO: handle case of multiplication by constant + pass + else: + # assert right is a class or subclass of DiscreteFactor + # that has attributes: variables, values; method: get_cardinality + right = copy.deepcopy(other) + left.add_new_variables_from_other_factor(right) + right.add_new_variables_from_other_factor(left) + + # reorder variables in right factor to match order in left + source_axes = list(range(right.values.ndim)) + destination_axes = [right.variables.index(var) for var in left.variables] + right.variables = [right.variables[idx] for idx in destination_axes] + + # rearrange values in right factor to correspond to the reordered variables + right._values = np.moveaxis(right.values, source_axes, destination_axes) + left._values = left.values * right.values + return left + + def marginalize(self, vars): + """ + Args + vars: list, + variables over which to marginalize the factor + Returns + DiscreteFactor + """ + phi = copy.deepcopy(self) + + var_indexes = [] + for var in vars: + if var not in phi.variables: + raise ValueError('{} not in scope'.format(var)) + else: + var_indexes.append(self.variables.index(var)) + + index_to_keep = sorted(set(range(len(self.variables))) - set(var_indexes)) + phi.variables = [self.variables[index] for index in index_to_keep] + phi.cardinality = [self.cardinality[index] for index in index_to_keep] + phi._values = np.sum(phi.values, axis=tuple(var_indexes)) + return phi -- cgit v1.2.3 From f6ab3e7b918396dee70dc4ff2dc3a1341aaeb97b Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Tue, 12 Dec 2017 14:12:51 -0800 Subject: TabularCPD inherits from DiscreteFactor --- beliefs/factors/cpd.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/beliefs/factors/cpd.py b/beliefs/factors/cpd.py index a286aaa..9e7191f 100644 --- a/beliefs/factors/cpd.py +++ b/beliefs/factors/cpd.py @@ -1,15 +1,14 @@ import numpy as np +from beliefs.factors.discrete_factor import DiscreteFactor -class TabularCPD: +class TabularCPD(DiscreteFactor): """ Defines the conditional probability table for a discrete variable whose parents are also discrete. - - TODO: have this inherit from DiscreteFactor implementing explicit factor methods """ def __init__(self, variable, variable_card, - parents=[], parents_card=[], values=[]): + parents=[], parents_card=[], values=[], state_names=None): """ Args: variable: int or string @@ -17,16 +16,15 @@ class TabularCPD: parents: optional, list of int and/or strings parents_card: optional, list of int values: optional, 2d list or array + state_names: dictionary (optional), + mapping variables to their states, of format {label_name: ['state1', 'state2']} """ + super().__init__(variables=[variable] + parents, + cardinality=[variable_card] + parents_card, + values=values, + state_names=state_names) self.variable = variable self.parents = parents - self.variables = [variable] + parents - self.cardinality = [variable_card] + parents_card - self._values = np.array(values) - - @property - def values(self): - return self._values def get_values(self): """ -- cgit v1.2.3 From 70bdf07d25f41de1a9510b64267bfa29791760c7 Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Tue, 12 Dec 2017 16:11:54 -0800 Subject: change all msg datatypes from np.array -> DiscreteFactor --- beliefs/inference/belief_propagation.py | 20 +++--- beliefs/models/belief_update_node_model.py | 110 ++++++++++++++++++----------- 2 files changed, 77 insertions(+), 53 deletions(-) diff --git a/beliefs/inference/belief_propagation.py b/beliefs/inference/belief_propagation.py index 7ec648d..128f645 100644 --- a/beliefs/inference/belief_propagation.py +++ b/beliefs/inference/belief_propagation.py @@ -74,9 +74,9 @@ class BeliefPropagation: if node_to_update_label_id not in evidence: node.compute_pi_agg() - logging.info("belief propagation pi_agg: %s", np.array2string(node.pi_agg)) + logging.info("belief propagation pi_agg: %s", np.array2string(node.pi_agg.values)) node.compute_lambda_agg() - logging.info("belief propagation lambda_agg: %s", np.array2string(node.lambda_agg)) + logging.info("belief propagation lambda_agg: %s", np.array2string(node.lambda_agg.values)) for parent_id in parent_ids: try: @@ -97,7 +97,6 @@ class BeliefPropagation: new_value=new_pi_msg) nodes_to_update.add(MsgPassers(msg_receiver=child_id, msg_sender=node_to_update_label_id)) - self._belief_propagation(nodes_to_update, evidence) def initialize_model(self): @@ -115,8 +114,8 @@ class BeliefPropagation: for node in self.model.nodes_dict.values(): ones_vector = np.ones([node.cardinality]) + node.update_lambda_agg(ones_vector) - node.lambda_agg = ones_vector for child in node.lambda_received_msgs.keys(): node.update_lambda_msg_from_child(child=child, new_value=ones_vector) @@ -131,7 +130,7 @@ class BeliefPropagation: node_sending_msg = self.model.nodes_dict[node_id] child_ids = node_sending_msg.children - if node_sending_msg.pi_agg is None: + if node_sending_msg.pi_agg.values is None: node_sending_msg.compute_pi_agg() for child_id in child_ids: @@ -150,22 +149,19 @@ class BeliefPropagation: a dict key, value pair as {var: state_of_var observed} """ for evidence_id, observed_value in evidence.items(): - nodes_to_update = set() - if evidence_id not in self.model.nodes_dict.keys(): raise KeyError("Evidence supplied for non-existent label_id: {}" .format(evidence_id)) if is_kronecker_delta(observed_value): # specific evidence - self.model.nodes_dict[evidence_id].lambda_agg = observed_value + self.model.nodes_dict[evidence_id].update_lambda_agg(observed_value) else: # virtual evidence - self.model.nodes_dict[evidence_id].lambda_agg = \ - self.model.nodes_dict[evidence_id].lambda_agg * observed_value - + self.model.nodes_dict[evidence_id].update_lambda_agg( + self.model.nodes_dict[evidence_id].lambda_agg.values * observed_value + ) nodes_to_update = [MsgPassers(msg_receiver=evidence_id, msg_sender=None)] - self._belief_propagation(nodes_to_update=set(nodes_to_update), evidence=evidence) diff --git a/beliefs/models/belief_update_node_model.py b/beliefs/models/belief_update_node_model.py index 1c3ba6e..1765ed9 100644 --- a/beliefs/models/belief_update_node_model.py +++ b/beliefs/models/belief_update_node_model.py @@ -7,6 +7,7 @@ from functools import reduce import networkx as nx from beliefs.models.base_models import BayesianModel +from beliefs.factors.discrete_factor import DiscreteFactor from beliefs.factors.bernoulli_or_cpd import BernoulliOrCPD from beliefs.factors.bernoulli_and_cpd import BernoulliAndCPD @@ -88,10 +89,10 @@ class BeliefUpdateNodeModel(BayesianModel): to an (unnormalized) unit vector, of length the cardinality of x. """ for root in self.get_roots(): - self.nodes_dict[root].pi_agg = self.nodes_dict[root].cpd.values + self.nodes_dict[root].update_pi_agg(self.nodes_dict[root].cpd.values) for leaf in self.get_leaves(): - self.nodes_dict[leaf].lambda_agg = np.ones([self.nodes_dict[leaf].cardinality]) + self.nodes_dict[leaf].update_lambda_agg(np.ones([self.nodes_dict[leaf].cardinality])) @property def all_nodes_are_fully_initialized(self): @@ -135,17 +136,18 @@ class Node: cpd: an instance of a conditional probability distribution, e.g. BernoulliOrCPD or TabularCPD """ - self.label_id = label_id + self.label_id = label_id # this can be obtained from cpd.variable self.children = children - self.parents = parents - self.cardinality = cardinality + self.parents = parents # this can be obtained from cpd.variables[1:] + self.cardinality = cardinality # this can be obtained from cpd.cardinality[0] self.cpd = cpd - self.pi_agg = None # np.array dimensions [1, cardinality] - self.lambda_agg = None # np.array dimensions [1, cardinality] + # instances of DiscreteFactor with `values` an np.array of dimensions [1, cardinality] + self.pi_agg = self._init_aggregate_values() + self.lambda_agg = self._init_aggregate_values() - self.pi_received_msgs = self._init_received_msgs(parents) - self.lambda_received_msgs = self._init_received_msgs(children) + self.pi_received_msgs = self._init_pi_received_msgs(parents) + self.lambda_received_msgs = {child: self._init_aggregate_values() for child in children} @classmethod def from_cpd_class(cls, @@ -159,8 +161,8 @@ class Node: @property def belief(self): - if self.pi_agg.any() and self.lambda_agg.any(): - belief = np.multiply(self.pi_agg, self.lambda_agg) + if any(self.pi_agg.values) and any(self.lambda_agg.values): + belief = (self.lambda_agg * self.pi_agg).values return self._normalize(belief) else: return None @@ -168,9 +170,21 @@ class Node: def _normalize(self, value): return value/value.sum() - @staticmethod - def _init_received_msgs(keys): - return {k: None for k in keys} + def _init_aggregate_values(self): + return DiscreteFactor(variables=[self.cpd.variable], + cardinality=[self.cardinality], + values=None, + state_names=None) + + def _init_pi_received_msgs(self, parents): + msgs = {} + for k in parents: + kth_cardinality = self.cpd.cardinality[self.cpd.variables.index(k)] + msgs[k] = DiscreteFactor(variables=[k], + cardinality=[kth_cardinality], + values=None, + state_names=None) + return msgs def _return_msgs_received_for_msg_type(self, message_type): """ @@ -181,9 +195,9 @@ class Node: msg_values: list of message values (each an np.array) """ if message_type == MessageType.LAMBDA: - msg_values = [msg for msg in self.lambda_received_msgs.values()] + msg_values = [msg.values for msg in self.lambda_received_msgs.values()] elif message_type == MessageType.PI: - msg_values = [msg for msg in self.pi_received_msgs.values()] + msg_values = [msg.values for msg in self.pi_received_msgs.values()] return msg_values def validate_and_return_msgs_received_for_msg_type(self, message_type): @@ -214,13 +228,20 @@ class Node: def compute_lambda_agg(self): if len(self.children) == 0: - return self.lambda_agg + return self.lambda_agg.values else: - lambda_msg_values = self.validate_and_return_msgs_received_for_msg_type(MessageType.LAMBDA) - self.lambda_agg = reduce(np.multiply, lambda_msg_values) - return self.lambda_agg + lambda_msg_values =\ + self.validate_and_return_msgs_received_for_msg_type(MessageType.LAMBDA) + self.update_lambda_agg(reduce(np.multiply, lambda_msg_values)) + return self.lambda_agg.values + + def update_pi_agg(self, new_value): + self.pi_agg.update_values(np.array(new_value).reshape(self.cardinality)) + + def update_lambda_agg(self, new_value): + self.lambda_agg.update_values(np.array(new_value).reshape(self.cardinality)) - def _update_received_msg_by_key(self, received_msg_dict, key, new_value): + def _update_received_msg_by_key(self, received_msg_dict, key, new_value, message_type): if key not in received_msg_dict.keys(): raise ValueError("Label id '{}' to update message isn't in allowed set of keys: {}" .format(key, received_msg_dict.keys())) @@ -229,23 +250,30 @@ class Node: raise TypeError("Expected a new value of type numpy.ndarray, but got type {}" .format(type(new_value))) - if new_value.shape != (self.cardinality,): - raise ValueError("Expected new value to be of dimensions ({},) but got {} instead" - .format(self.cardinality, new_value.shape)) - received_msg_dict[key] = new_value + if message_type == MessageType.LAMBDA: + expected_shape = (self.cardinality,) + elif message_type == MessageType.PI: + expected_shape = (self.cpd.cardinality[self.cpd.variables.index(key)],) + + if new_value.shape != expected_shape: + raise ValueError("Expected new value to be of dimensions ({},) but got {} instead" + .format(expected_shape, new_value.shape)) + received_msg_dict[key]._values = new_value def update_pi_msg_from_parent(self, parent, new_value): self._update_received_msg_by_key(received_msg_dict=self.pi_received_msgs, key=parent, - new_value=new_value) + new_value=new_value, + message_type=MessageType.PI) def update_lambda_msg_from_child(self, child, new_value): self._update_received_msg_by_key(received_msg_dict=self.lambda_received_msgs, key=child, - new_value=new_value) + new_value=new_value, + message_type=MessageType.LAMBDA) def compute_pi_msg_to_child(self, child_k): - lambda_msg_from_child = self.lambda_received_msgs[child_k] + lambda_msg_from_child = self.lambda_received_msgs[child_k].values if lambda_msg_from_child is not None: with np.errstate(divide='ignore', invalid='ignore'): # 0/0 := 0 @@ -272,7 +300,7 @@ class Node: if any(msg is None for msg in pi_msgs): return False - if (self.pi_agg is None) or (self.lambda_agg is None): + if (self.pi_agg.values is None) or (self.lambda_agg.values is None): return False return True @@ -291,7 +319,7 @@ class BernoulliOrNode(Node): def compute_pi_agg(self): if len(self.parents) == 0: - self.pi_agg = self.cpd.values + self.update_pi_agg(self.cpd.values) else: pi_msg_values = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) parents_p0 = [p[0] for p in pi_msg_values] @@ -299,19 +327,19 @@ class BernoulliOrNode(Node): # of p = [P(False), P(True)] p_0 = reduce(lambda x, y: x*y, parents_p0) p_1 = 1 - p_0 - self.pi_agg = np.array([p_0, p_1]) + self.update_pi_agg(np.array([p_0, p_1])) return self.pi_agg def compute_lambda_msg_to_parent(self, parent_k): - if np.array_equal(self.lambda_agg, np.ones([self.cardinality])): + if np.array_equal(self.lambda_agg.values, np.ones([self.cardinality])): return np.ones([self.cardinality]) else: # TODO: cleanup this validation _ = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) - p0_excluding_k = [p[0] for par_id, p in self.pi_received_msgs.items() if par_id != parent_k] + p0_excluding_k = [p.values[0] for par_id, p in self.pi_received_msgs.items() if par_id != parent_k] p0_product = reduce(lambda x, y: x*y, p0_excluding_k, 1) - lambda_0 = self.lambda_agg[1] + (self.lambda_agg[0] - self.lambda_agg[1])*p0_product - lambda_1 = self.lambda_agg[1] + lambda_0 = self.lambda_agg.values[1] + (self.lambda_agg.values[0] - self.lambda_agg.values[1])*p0_product + lambda_1 = self.lambda_agg.values[1] lambda_msg = np.array([lambda_0, lambda_1]) if not any(lambda_msg): raise InvalidLambdaMsgToParent @@ -331,7 +359,7 @@ class BernoulliAndNode(Node): def compute_pi_agg(self): if len(self.parents) == 0: - self.pi_agg = self.cpd.values + self.update_pi_agg(self.cpd.values) else: pi_msg_values = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) parents_p1 = [p[1] for p in pi_msg_values] @@ -339,19 +367,19 @@ class BernoulliAndNode(Node): # of p = [P(False), P(True)] p_1 = reduce(lambda x, y: x*y, parents_p1) p_0 = 1 - p_1 - self.pi_agg = np.array([p_0, p_1]) + self.update_pi_agg(np.array([p_0, p_1])) return self.pi_agg def compute_lambda_msg_to_parent(self, parent_k): - if np.array_equal(self.lambda_agg, np.ones([self.cardinality])): + if np.array_equal(self.lambda_agg.values, np.ones([self.cardinality])): return np.ones([self.cardinality]) else: # TODO: cleanup this validation _ = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) - p1_excluding_k = [p[1] for par_id, p in self.pi_received_msgs.items() if par_id != parent_k] + p1_excluding_k = [p.values[1] for par_id, p in self.pi_received_msgs.items() if par_id != parent_k] p1_product = reduce(lambda x, y: x*y, p1_excluding_k, 1) - lambda_0 = self.lambda_agg[0] - lambda_1 = self.lambda_agg[0] + (self.lambda_agg[1] - self.lambda_agg[0])*p1_product + lambda_0 = self.lambda_agg.values[0] + lambda_1 = self.lambda_agg.values[0] + (self.lambda_agg.values[1] - self.lambda_agg.values[0])*p1_product lambda_msg = np.array([lambda_0, lambda_1]) if not any(lambda_msg): raise InvalidLambdaMsgToParent -- cgit v1.2.3 From 76090e3f03c01e208d41203a6286ea432714090a Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Tue, 12 Dec 2017 19:06:30 -0800 Subject: clean up node class, simpler initialization --- beliefs/models/belief_update_node_model.py | 42 ++++++------------------------ 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/beliefs/models/belief_update_node_model.py b/beliefs/models/belief_update_node_model.py index 1765ed9..820ee0c 100644 --- a/beliefs/models/belief_update_node_model.py +++ b/beliefs/models/belief_update_node_model.py @@ -121,44 +121,26 @@ class Node: Implemented from Pearl's belief propagation algorithm. """ - def __init__(self, - label_id, - children, - parents, - cardinality, - cpd): + def __init__(self, children, cpd): """ Args - label_id: str - children: set of strings - parents: set of strings - cardinality: int, cardinality of the random variable the node represents + children: list of strings cpd: an instance of a conditional probability distribution, e.g. BernoulliOrCPD or TabularCPD """ - self.label_id = label_id # this can be obtained from cpd.variable + self.label_id = cpd.variable self.children = children - self.parents = parents # this can be obtained from cpd.variables[1:] - self.cardinality = cardinality # this can be obtained from cpd.cardinality[0] + self.parents = cpd.parents + self.cardinality = cpd.cardinality[0] self.cpd = cpd # instances of DiscreteFactor with `values` an np.array of dimensions [1, cardinality] self.pi_agg = self._init_aggregate_values() self.lambda_agg = self._init_aggregate_values() - self.pi_received_msgs = self._init_pi_received_msgs(parents) + self.pi_received_msgs = self._init_pi_received_msgs(self.parents) self.lambda_received_msgs = {child: self._init_aggregate_values() for child in children} - @classmethod - def from_cpd_class(cls, - label_id, - children, - parents, - cardinality, - cpd_class): - cpd = cpd_class(label_id, parents) - return cls(label_id, children, parents, cardinality, cpd) - @property def belief(self): if any(self.pi_agg.values) and any(self.lambda_agg.values): @@ -311,11 +293,7 @@ class BernoulliOrNode(Node): label_id, children, parents): - super().__init__(label_id=label_id, - children=children, - parents=parents, - cardinality=2, - cpd=BernoulliOrCPD(label_id, parents)) + super().__init__(children=children, cpd=BernoulliOrCPD(label_id, parents)) def compute_pi_agg(self): if len(self.parents) == 0: @@ -351,11 +329,7 @@ class BernoulliAndNode(Node): label_id, children, parents): - super().__init__(label_id=label_id, - children=children, - parents=parents, - cardinality=2, - cpd=BernoulliAndCPD(label_id, parents)) + super().__init__(children=children, cpd=BernoulliAndCPD(label_id, parents)) def compute_pi_agg(self): if len(self.parents) == 0: -- cgit v1.2.3 From 2f4de4ae0b28e0e5ee2a5be6955366267fbc2404 Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Tue, 12 Dec 2017 14:27:11 -0800 Subject: init Bernoulli And,Or CPDs w/ default state names 'False','True' --- beliefs/factors/bernoulli_and_cpd.py | 3 ++- beliefs/factors/bernoulli_or_cpd.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/beliefs/factors/bernoulli_and_cpd.py b/beliefs/factors/bernoulli_and_cpd.py index fdb0c25..adf5ed5 100644 --- a/beliefs/factors/bernoulli_and_cpd.py +++ b/beliefs/factors/bernoulli_and_cpd.py @@ -20,7 +20,8 @@ class BernoulliAndCPD(TabularCPD): variable_card=2, parents=parents, parents_card=[2]*len(parents), - values=[]) + values=[], + state_names={var: ['False', 'True'] for var in [variable] + parents}) self._values = None @property diff --git a/beliefs/factors/bernoulli_or_cpd.py b/beliefs/factors/bernoulli_or_cpd.py index 12ee2f6..6e01cf9 100644 --- a/beliefs/factors/bernoulli_or_cpd.py +++ b/beliefs/factors/bernoulli_or_cpd.py @@ -20,7 +20,8 @@ class BernoulliOrCPD(TabularCPD): variable_card=2, parents=parents, parents_card=[2]*len(parents), - values=[]) + values=[], + state_names={var: ['False', 'True'] for var in [variable] + parents}) self._values = None @property -- cgit v1.2.3 From b3b8bb68d6d590175a07dfc4022b4903d63222e5 Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Tue, 12 Dec 2017 19:58:12 -0800 Subject: Bernoulli Or/And Node access msg values by state names --- beliefs/factors/bernoulli_and_cpd.py | 2 +- beliefs/factors/bernoulli_or_cpd.py | 2 +- beliefs/models/belief_update_node_model.py | 91 ++++++++++++++++++++++-------- 3 files changed, 69 insertions(+), 26 deletions(-) diff --git a/beliefs/factors/bernoulli_and_cpd.py b/beliefs/factors/bernoulli_and_cpd.py index adf5ed5..15802c2 100644 --- a/beliefs/factors/bernoulli_and_cpd.py +++ b/beliefs/factors/bernoulli_and_cpd.py @@ -20,7 +20,7 @@ class BernoulliAndCPD(TabularCPD): variable_card=2, parents=parents, parents_card=[2]*len(parents), - values=[], + values=None, state_names={var: ['False', 'True'] for var in [variable] + parents}) self._values = None diff --git a/beliefs/factors/bernoulli_or_cpd.py b/beliefs/factors/bernoulli_or_cpd.py index 6e01cf9..5b661a1 100644 --- a/beliefs/factors/bernoulli_or_cpd.py +++ b/beliefs/factors/bernoulli_or_cpd.py @@ -20,7 +20,7 @@ class BernoulliOrCPD(TabularCPD): variable_card=2, parents=parents, parents_card=[2]*len(parents), - values=[], + values=None, state_names={var: ['False', 'True'] for var in [variable] + parents}) self._values = None diff --git a/beliefs/models/belief_update_node_model.py b/beliefs/models/belief_update_node_model.py index 820ee0c..cd8ba8c 100644 --- a/beliefs/models/belief_update_node_model.py +++ b/beliefs/models/belief_update_node_model.py @@ -174,13 +174,13 @@ class Node: message_type: MessageType enum Returns: - msg_values: list of message values (each an np.array) + msg_values: list of DiscreteFactors containing message values (np.arrays) """ if message_type == MessageType.LAMBDA: - msg_values = [msg.values for msg in self.lambda_received_msgs.values()] + msgs = [msg for msg in self.lambda_received_msgs.values()] elif message_type == MessageType.PI: - msg_values = [msg.values for msg in self.pi_received_msgs.values()] - return msg_values + msgs = [msg for msg in self.pi_received_msgs.values()] + return msgs def validate_and_return_msgs_received_for_msg_type(self, message_type): """ @@ -192,17 +192,17 @@ class Node: message_type: MessageType enum Returns: - msg_values: list of message values (each an np.array) + msgs: list of DiscreteFactors containing message values (np.array) """ - msg_values = self._return_msgs_received_for_msg_type(message_type) + msgs = self._return_msgs_received_for_msg_type(message_type) - if any(msg is None for msg in msg_values): + if any(msg.values is None for msg in msgs): raise ValueError( "Missing value for {msg_type} msg from child: can't compute {msg_type}_agg." .format(msg_type=message_type.value) ) else: - return msg_values + return msgs def compute_pi_agg(self): # TODO: implement explict factor product operation @@ -212,8 +212,10 @@ class Node: if len(self.children) == 0: return self.lambda_agg.values else: - lambda_msg_values =\ - self.validate_and_return_msgs_received_for_msg_type(MessageType.LAMBDA) + lambda_msg_values = [ + msg.values for msg in + self.validate_and_return_msgs_received_for_msg_type(MessageType.LAMBDA) + ] self.update_lambda_agg(reduce(np.multiply, lambda_msg_values)) return self.lambda_agg.values @@ -295,14 +297,30 @@ class BernoulliOrNode(Node): parents): super().__init__(children=children, cpd=BernoulliOrCPD(label_id, parents)) + def _init_aggregate_values(self): + variable = self.cpd.variable + return DiscreteFactor(variables=[self.cpd.variable], + cardinality=[self.cardinality], + values=None, + state_names={variable: self.cpd.state_names[variable]}) + + def _init_pi_received_msgs(self, parents): + msgs = {} + for k in parents: + kth_cardinality = self.cpd.cardinality[self.cpd.variables.index(k)] + msgs[k] = DiscreteFactor(variables=[k], + cardinality=[kth_cardinality], + values=None, + state_names={k: self.cpd.state_names[k]}) + return msgs + def compute_pi_agg(self): if len(self.parents) == 0: self.update_pi_agg(self.cpd.values) else: - pi_msg_values = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) - parents_p0 = [p[0] for p in pi_msg_values] - # Parents are Bernoulli variables with pi_msg_values (surrogate prior probabilities) - # of p = [P(False), P(True)] + pi_msgs = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) + parents_p0 = [p.get_value_for_state_vector({p.variables[0]: 'False'}) + for p in pi_msgs] p_0 = reduce(lambda x, y: x*y, parents_p0) p_1 = 1 - p_0 self.update_pi_agg(np.array([p_0, p_1])) @@ -314,10 +332,14 @@ class BernoulliOrNode(Node): else: # TODO: cleanup this validation _ = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) - p0_excluding_k = [p.values[0] for par_id, p in self.pi_received_msgs.items() if par_id != parent_k] + p0_excluding_k = [p.get_value_for_state_vector({p.variables[0]: 'False'}) + for par_id, p in self.pi_received_msgs.items() if par_id != parent_k] p0_product = reduce(lambda x, y: x*y, p0_excluding_k, 1) - lambda_0 = self.lambda_agg.values[1] + (self.lambda_agg.values[0] - self.lambda_agg.values[1])*p0_product - lambda_1 = self.lambda_agg.values[1] + + lambda_agg_0 = self.lambda_agg.get_value_for_state_vector({self.label_id: 'False'}) + lambda_agg_1 = self.lambda_agg.get_value_for_state_vector({self.label_id: 'True'}) + lambda_0 = lambda_agg_1 + (lambda_agg_0 - lambda_agg_1)*p0_product + lambda_1 = lambda_agg_1 lambda_msg = np.array([lambda_0, lambda_1]) if not any(lambda_msg): raise InvalidLambdaMsgToParent @@ -331,14 +353,30 @@ class BernoulliAndNode(Node): parents): super().__init__(children=children, cpd=BernoulliAndCPD(label_id, parents)) + def _init_aggregate_values(self): + variable = self.cpd.variable + return DiscreteFactor(variables=[self.cpd.variable], + cardinality=[self.cardinality], + values=None, + state_names={variable: self.cpd.state_names[variable]}) + + def _init_pi_received_msgs(self, parents): + msgs = {} + for k in parents: + kth_cardinality = self.cpd.cardinality[self.cpd.variables.index(k)] + msgs[k] = DiscreteFactor(variables=[k], + cardinality=[kth_cardinality], + values=None, + state_names={k: self.cpd.state_names[k]}) + return msgs + def compute_pi_agg(self): if len(self.parents) == 0: self.update_pi_agg(self.cpd.values) else: - pi_msg_values = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) - parents_p1 = [p[1] for p in pi_msg_values] - # Parents are Bernoulli variables with pi_msg_values (surrogate prior probabilities) - # of p = [P(False), P(True)] + pi_msgs = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) + parents_p1 = [p.get_value_for_state_vector({p.variables[0]: 'True'}) + for p in pi_msgs] p_1 = reduce(lambda x, y: x*y, parents_p1) p_0 = 1 - p_1 self.update_pi_agg(np.array([p_0, p_1])) @@ -350,10 +388,15 @@ class BernoulliAndNode(Node): else: # TODO: cleanup this validation _ = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) - p1_excluding_k = [p.values[1] for par_id, p in self.pi_received_msgs.items() if par_id != parent_k] + p1_excluding_k = [p.get_value_for_state_vector({p.variables[0]: 'True'}) + for par_id, p in self.pi_received_msgs.items() if par_id != parent_k] p1_product = reduce(lambda x, y: x*y, p1_excluding_k, 1) - lambda_0 = self.lambda_agg.values[0] - lambda_1 = self.lambda_agg.values[0] + (self.lambda_agg.values[1] - self.lambda_agg.values[0])*p1_product + + lambda_agg_0 = self.lambda_agg.get_value_for_state_vector({self.label_id: 'False'}) + lambda_agg_1 = self.lambda_agg.get_value_for_state_vector({self.label_id: 'True'}) + + lambda_0 = lambda_agg_0 + lambda_1 = lambda_agg_0 + (lambda_agg_1 - lambda_agg_0)*p1_product lambda_msg = np.array([lambda_0, lambda_1]) if not any(lambda_msg): raise InvalidLambdaMsgToParent -- cgit v1.2.3 From 10f5c49ea6767f54d59f88eb4064bb4959d14c6b Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Tue, 12 Dec 2017 21:28:26 -0800 Subject: implement explicit factor methods for compute_pi_agg and compute_lambda_msg_to_parent in Node --- beliefs/factors/discrete_factor.py | 7 ++++ beliefs/models/belief_update_node_model.py | 37 ++++++++++++----- tests/test_belief_propagation.py | 64 +++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/beliefs/factors/discrete_factor.py b/beliefs/factors/discrete_factor.py index da8e6bf..b75da28 100644 --- a/beliefs/factors/discrete_factor.py +++ b/beliefs/factors/discrete_factor.py @@ -86,9 +86,16 @@ class DiscreteFactor: right = copy.deepcopy(other) left.add_new_variables_from_other_factor(right) right.add_new_variables_from_other_factor(left) + print('var', left.variables) + print(left.cardinality) + print(left.values) + print('var', right.variables) + print(right.cardinality) + print(right.values) # reorder variables in right factor to match order in left source_axes = list(range(right.values.ndim)) + print('source_axes', source_axes) destination_axes = [right.variables.index(var) for var in left.variables] right.variables = [right.variables[idx] for idx in destination_axes] diff --git a/beliefs/models/belief_update_node_model.py b/beliefs/models/belief_update_node_model.py index cd8ba8c..17e98fa 100644 --- a/beliefs/models/belief_update_node_model.py +++ b/beliefs/models/belief_update_node_model.py @@ -205,25 +205,30 @@ class Node: return msgs def compute_pi_agg(self): - # TODO: implement explict factor product operation - raise NotImplementedError + if len(self.parents) == 0: + self.update_pi_agg(self.cpd.values) + else: + factors_to_multiply = [self.cpd] + pi_msgs = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) + factors_to_multiply.extend(pi_msgs) + + factor_product = reduce(lambda phi1, phi2: phi1*phi2, factors_to_multiply) + self.update_pi_agg(factor_product.marginalize(self.parents).values) + pi_msgs = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) def compute_lambda_agg(self): - if len(self.children) == 0: - return self.lambda_agg.values - else: + if len(self.children) != 0: lambda_msg_values = [ msg.values for msg in self.validate_and_return_msgs_received_for_msg_type(MessageType.LAMBDA) ] self.update_lambda_agg(reduce(np.multiply, lambda_msg_values)) - return self.lambda_agg.values def update_pi_agg(self, new_value): - self.pi_agg.update_values(np.array(new_value).reshape(self.cardinality)) + self.pi_agg.update_values(new_value) def update_lambda_agg(self, new_value): - self.lambda_agg.update_values(np.array(new_value).reshape(self.cardinality)) + self.lambda_agg.update_values(new_value) def _update_received_msg_by_key(self, received_msg_dict, key, new_value, message_type): if key not in received_msg_dict.keys(): @@ -242,7 +247,8 @@ class Node: if new_value.shape != expected_shape: raise ValueError("Expected new value to be of dimensions ({},) but got {} instead" .format(expected_shape, new_value.shape)) - received_msg_dict[key]._values = new_value + # received_msg_dict[key]._values = new_value + received_msg_dict[key].update_values(new_value) def update_pi_msg_from_parent(self, parent, new_value): self._update_received_msg_by_key(received_msg_dict=self.pi_received_msgs, @@ -267,8 +273,17 @@ class Node: raise ValueError("Can't compute pi message to child_{} without having received a lambda message from that child.") def compute_lambda_msg_to_parent(self, parent_k): - # TODO: implement explict factor product operation - raise NotImplementedError + if np.array_equal(self.lambda_agg.values, np.ones([self.cardinality])): + return np.ones([self.cardinality]) + else: + factors_to_multiply = [self.cpd] + pi_msgs_excl_k = [msg for par_id, msg in self.pi_received_msgs.items() + if par_id != parent_k] + factors_to_multiply.extend(pi_msgs_excl_k) + factor_product = reduce(lambda phi1, phi2: phi1*phi2, factors_to_multiply) + new_factor = factor_product.marginalize(list(set(self.parents) - set([parent_k]))) + lambda_msg_to_k = (self.lambda_agg * new_factor).marginalize([self.lambda_agg.variables[0]]) + return self._normalize(lambda_msg_to_k.values) @property def is_fully_initialized(self): diff --git a/tests/test_belief_propagation.py b/tests/test_belief_propagation.py index 7a77311..1b8c0ac 100644 --- a/tests/test_belief_propagation.py +++ b/tests/test_belief_propagation.py @@ -3,10 +3,12 @@ import pytest from pytest import approx from beliefs.inference.belief_propagation import BeliefPropagation, ConflictingEvidenceError +from beliefs.factors.cpd import TabularCPD from beliefs.models.belief_update_node_model import ( BeliefUpdateNodeModel, BernoulliOrNode, - BernoulliAndNode + BernoulliAndNode, + Node ) @@ -89,6 +91,41 @@ def mixed_cpd_model(edges_five_nodes): 'w': w_node}) +@pytest.fixture(scope='function') +def custom_cpd_model(): + """ + Y-shaped model, with parents ,'u' and 'v' as Or-nodes, 'x' a node with + cardinality 3 and custom CPD, 'y' a node with cardinality 2 and custom CPD. + """ + custom_cpd_x = TabularCPD(variable='x', + variable_card=3, + parents=['u', 'v'], + parents_card=[2, 2], + values=[[0.2, 0, 0.3, 0.1], + [0.4, 1, 0.7, 0.2], + [0.4, 0, 0, 0.7]], + state_names={'x': ['lo', 'med', 'hi'], + 'u': ['False', 'True'], + 'v': ['False', 'True']}) + custom_cpd_y = TabularCPD(variable='y', + variable_card=2, + parents=['x'], + parents_card=[3], + values=[[0.3, 0.1, 0], + [0.7, 0.9, 1]], + state_names={'x': ['lo', 'med', 'hi'], + 'y': ['False', 'True']}) + + u_node = BernoulliOrNode(label_id='u', children=['x'], parents=[]) + v_node = BernoulliOrNode(label_id='v', children=['x'], parents=[]) + x_node = Node(children=['y'], cpd=custom_cpd_x) + y_node = Node(children=[], cpd=custom_cpd_y) + return BeliefUpdateNodeModel(nodes_dict={'u': u_node, + 'v': v_node, + 'x': x_node, + 'y': y_node}) + + def get_label_mapped_to_positive_belief(query_result): """Return a dictionary mapping each label_id to the probability of the label being True.""" @@ -355,3 +392,28 @@ def test_conflicting_evidence_and_model(many_parents_and_model): with pytest.raises(ConflictingEvidenceError) as err: query_result = infer.query(evidence={'62': np.array([0, 1]), '112': np.array([1, 0])}) assert "Can't run belief propagation with conflicting evidence" in str(err) + + +#============================================================================================== +# Model with two custom cpds + + +def test_no_evidence_custom_cpd_model(custom_cpd_model): + expected = {'x': np.array([0.15, 0.575, 0.275]), + 'v': np.array([0.5, 0.5]), + 'u': np.array([0.5, 0.5]), + 'y': np.array([0.1025, 0.8975])} + infer = BeliefPropagation(custom_cpd_model) + query_result = infer.query(evidence={}) + compare_dictionaries(expected, query_result) + + +def test_evidence_custom_cpd_model(custom_cpd_model): + """Custom node is observed to be in 'med' state.""" + expected = {'x': np.array([0., 1., 0.]), + 'u': np.array([0.60869565, 0.39130435]), + 'v': np.array([0.47826087, 0.52173913]), + 'y': np.array([0.1, 0.9])} + infer = BeliefPropagation(custom_cpd_model) + query_result = infer.query(evidence={'x': np.array([0, 1, 0])}) + compare_dictionaries(expected, query_result) -- cgit v1.2.3 From 7053fefc6f9e43b1e252d1f551401a7a70b52e93 Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Wed, 13 Dec 2017 18:47:32 -0800 Subject: cleanup print statements, stale comments, minor TODOs --- beliefs/factors/bernoulli_and_cpd.py | 7 +- beliefs/factors/bernoulli_or_cpd.py | 7 +- beliefs/factors/cpd.py | 29 ++-- beliefs/factors/discrete_factor.py | 32 ++-- beliefs/inference/belief_propagation.py | 77 +++++----- beliefs/models/base_models.py | 90 ++++++----- beliefs/models/belief_update_node_model.py | 238 +++++++++++++++++++---------- beliefs/utils/math_helper.py | 14 +- beliefs/utils/random_variables.py | 17 ++- 9 files changed, 306 insertions(+), 205 deletions(-) diff --git a/beliefs/factors/bernoulli_and_cpd.py b/beliefs/factors/bernoulli_and_cpd.py index 15802c2..291398f 100644 --- a/beliefs/factors/bernoulli_and_cpd.py +++ b/beliefs/factors/bernoulli_and_cpd.py @@ -12,9 +12,10 @@ class BernoulliAndCPD(TabularCPD): """ def __init__(self, variable, parents=[]): """ - Args: - variable: int or string - parents: optional, list of int and/or strings + Args + variable: int or string + parents: list, + (optional) list of int and/or strings """ super().__init__(variable=variable, variable_card=2, diff --git a/beliefs/factors/bernoulli_or_cpd.py b/beliefs/factors/bernoulli_or_cpd.py index 5b661a1..b5e6ae5 100644 --- a/beliefs/factors/bernoulli_or_cpd.py +++ b/beliefs/factors/bernoulli_or_cpd.py @@ -12,9 +12,10 @@ class BernoulliOrCPD(TabularCPD): """ def __init__(self, variable, parents=[]): """ - Args: - variable: int or string - parents: optional, list of int and/or strings + Args + variable: int or string + parents: list, + (optional) list of int and/or strings """ super().__init__(variable=variable, variable_card=2, diff --git a/beliefs/factors/cpd.py b/beliefs/factors/cpd.py index 9e7191f..c7883c9 100644 --- a/beliefs/factors/cpd.py +++ b/beliefs/factors/cpd.py @@ -1,3 +1,4 @@ +import copy import numpy as np from beliefs.factors.discrete_factor import DiscreteFactor @@ -7,16 +8,18 @@ class TabularCPD(DiscreteFactor): Defines the conditional probability table for a discrete variable whose parents are also discrete. """ - def __init__(self, variable, variable_card, - parents=[], parents_card=[], values=[], state_names=None): + def __init__(self, variable, variable_card, parents=[], parents_card=[], + values=[], state_names=None): """ - Args: - variable: int or string - variable_card: int - parents: optional, list of int and/or strings - parents_card: optional, list of int - values: optional, 2d list or array - state_names: dictionary (optional), + Args + variable: int or string + variable_card: int + parents: list, + (optional) list of int and/or strings + parents_card: list, + (optional) list of int + values: 2-d list or array (optional) + state_names: dictionary (optional), mapping variables to their states, of format {label_name: ['state1', 'state2']} """ super().__init__(variables=[variable] + parents, @@ -24,7 +27,7 @@ class TabularCPD(DiscreteFactor): values=values, state_names=state_names) self.variable = variable - self.parents = parents + self.parents = list(parents) def get_values(self): """ @@ -36,8 +39,4 @@ class TabularCPD(DiscreteFactor): return self.values.reshape(self.cardinality[0], np.prod(self.cardinality[1:])) def copy(self): - return self.__class__(self.variable, - self.cardinality[0], - self.parents, - self.cardinality[1:], - self._values) + return copy.deepcopy(self) diff --git a/beliefs/factors/discrete_factor.py b/beliefs/factors/discrete_factor.py index b75da28..708f00c 100644 --- a/beliefs/factors/discrete_factor.py +++ b/beliefs/factors/discrete_factor.py @@ -18,7 +18,7 @@ class DiscreteFactor: mapping variables to their states, of format {label_name: ['state1', 'state2']} """ self.variables = list(variables) - self.cardinality = cardinality + self.cardinality = list(cardinality) if values is None: self._values = None else: @@ -28,6 +28,13 @@ class DiscreteFactor: def __mul__(self, other): return self.product(other) + def copy(self): + """Return a copy of the factor""" + return self.__class__(self.variables, + self.cardinality, + self._values, + copy.deepcopy(self.state_names)) + @property def values(self): return self._values @@ -56,7 +63,7 @@ class DiscreteFactor: return self.values[tuple(state_coordinates)] def add_new_variables_from_other_factor(self, other): - """Add new variables to the factor.""" + """Add new variables from `other` factor to the factor.""" extra_vars = set(other.variables) - set(self.variables) # if all of these variables already exist there is nothing to do if len(extra_vars) == 0: @@ -69,33 +76,24 @@ class DiscreteFactor: new_card_var = other.get_cardinality(extra_vars) self.cardinality.extend([new_card_var[var] for var in extra_vars]) - return def get_cardinality(self, variables): return {var: self.cardinality[self.variables.index(var)] for var in variables} def product(self, other): - left = copy.deepcopy(self) + left = self.copy() if isinstance(other, (int, float)): - # TODO: handle case of multiplication by constant - pass + return self.values * other else: - # assert right is a class or subclass of DiscreteFactor - # that has attributes: variables, values; method: get_cardinality - right = copy.deepcopy(other) + assert isinstance(other, DiscreteFactor), \ + "__mul__ is only defined between subclasses of DiscreteFactor" + right = other.copy() left.add_new_variables_from_other_factor(right) right.add_new_variables_from_other_factor(left) - print('var', left.variables) - print(left.cardinality) - print(left.values) - print('var', right.variables) - print(right.cardinality) - print(right.values) # reorder variables in right factor to match order in left source_axes = list(range(right.values.ndim)) - print('source_axes', source_axes) destination_axes = [right.variables.index(var) for var in left.variables] right.variables = [right.variables[idx] for idx in destination_axes] @@ -110,7 +108,7 @@ class DiscreteFactor: vars: list, variables over which to marginalize the factor Returns - DiscreteFactor + DiscreteFactor, whose scope is set(self.variables) - set(vars) """ phi = copy.deepcopy(self) diff --git a/beliefs/inference/belief_propagation.py b/beliefs/inference/belief_propagation.py index 128f645..acd93d4 100644 --- a/beliefs/inference/belief_propagation.py +++ b/beliefs/inference/belief_propagation.py @@ -28,10 +28,10 @@ class ConflictingEvidenceError(Exception): class BeliefPropagation: def __init__(self, model, inplace=True): """ - Input: - model: an instance of BeliefUpdateNodeModel - inplace: bool - modify in-place the nodes in the model during belief propagation + Args + model: an instance of BeliefUpdateNodeModel + inplace: bool, + modify in-place the nodes in the model during belief propagation """ if not isinstance(model, BeliefUpdateNodeModel): raise TypeError("Model must be an instance of BeliefUpdateNodeModel") @@ -43,21 +43,20 @@ class BeliefPropagation: def _belief_propagation(self, nodes_to_update, evidence): """ Implementation of Pearl's belief propagation algorithm for polytrees. - ref: "Fusion, Propagation, and Structuring in Belief Networks" Artificial Intelligence 29 (1986) 241-288 - Input: - nodes_to_update: list - list of MsgPasser namedtuples. - evidence: dict, - a dict key, value pair as {var: state_of_var observed} + Args + nodes_to_update: list, + list of MsgPasser namedtuples. + evidence: dict, + a dict key, value pair as {var: state_of_var observed} """ if len(nodes_to_update) == 0: return node_to_update_label_id, msg_sender_label_id = nodes_to_update.pop() - logging.info("Node: %s", node_to_update_label_id) + logging.debug("Node: %s", node_to_update_label_id) node = self.model.nodes_dict[node_to_update_label_id] @@ -65,8 +64,8 @@ class BeliefPropagation: # outgoing msg from the node to update parent_ids = set(node.parents) - set([msg_sender_label_id]) child_ids = set(node.children) - set([msg_sender_label_id]) - logging.info("parent_ids: %s", str(parent_ids)) - logging.info("child_ids: %s", str(child_ids)) + logging.debug("parent_ids: %s", str(parent_ids)) + logging.debug("child_ids: %s", str(child_ids)) if msg_sender_label_id is not None: # update triggered by receiving a message, not pinning to evidence @@ -74,9 +73,9 @@ class BeliefPropagation: if node_to_update_label_id not in evidence: node.compute_pi_agg() - logging.info("belief propagation pi_agg: %s", np.array2string(node.pi_agg.values)) + logging.debug("belief propagation pi_agg: %s", np.array2string(node.pi_agg.values)) node.compute_lambda_agg() - logging.info("belief propagation lambda_agg: %s", np.array2string(node.lambda_agg.values)) + logging.debug("belief propagation lambda_agg: %s", np.array2string(node.lambda_agg.values)) for parent_id in parent_ids: try: @@ -101,14 +100,14 @@ class BeliefPropagation: def initialize_model(self): """ - Apply boundary conditions: + 1. Apply boundary conditions: - Set pi_agg equal to prior probabilities for root nodes. - Set lambda_agg equal to vector of ones for leaf nodes. - - Set lambda_agg, lambda_received_msgs to vectors of ones (same effect as - actually passing lambda messages up from leaf nodes to root nodes). - - Calculate pi_agg and pi_received_msgs for all nodes without evidence. - (Without evidence, belief equals pi_agg.) + 2. Set lambda_agg, lambda_received_msgs to vectors of ones (same effect as + actually passing lambda messages up from leaf nodes to root nodes). + 3. Calculate pi_agg and pi_received_msgs for all nodes without evidence. + (Without evidence, belief equals pi_agg.) """ self.model.set_boundary_conditions() @@ -119,13 +118,13 @@ class BeliefPropagation: for child in node.lambda_received_msgs.keys(): node.update_lambda_msg_from_child(child=child, new_value=ones_vector) - logging.info("Finished initializing Lambda(x) and lambda_received_msgs per node.") + logging.debug("Finished initializing Lambda(x) and lambda_received_msgs per node.") - logging.info("Start downward sweep from nodes. Sending Pi messages only.") + logging.debug("Start downward sweep from nodes. Sending Pi messages only.") topdown_order = self.model.get_topologically_sorted_nodes(reverse=False) for node_id in topdown_order: - logging.info('label in iteration through top-down order: %s', str(node_id)) + logging.debug('label in iteration through top-down order: %s', str(node_id)) node_sending_msg = self.model.nodes_dict[node_id] child_ids = node_sending_msg.children @@ -134,9 +133,9 @@ class BeliefPropagation: node_sending_msg.compute_pi_agg() for child_id in child_ids: - logging.info("child: %s", str(child_id)) + logging.debug("child: %s", str(child_id)) new_pi_msg = node_sending_msg.compute_pi_msg_to_child(child_k=child_id) - logging.info("new_pi_msg: %s", np.array2string(new_pi_msg)) + logging.debug("new_pi_msg: %s", np.array2string(new_pi_msg)) child_node = self.model.nodes_dict[child_id] child_node.update_pi_msg_from_parent(parent=node_id, @@ -144,9 +143,12 @@ class BeliefPropagation: def _run_belief_propagation(self, evidence): """ - Input: - evidence: dict - a dict key, value pair as {var: state_of_var observed} + Sequentially perturb nodes with observed values, running belief propagation + after each perturbation. + + Args + evidence: dict, + a dict key, value pair as {var: state_of_var observed} """ for evidence_id, observed_value in evidence.items(): if evidence_id not in self.model.nodes_dict.keys(): @@ -162,21 +164,20 @@ class BeliefPropagation: self.model.nodes_dict[evidence_id].lambda_agg.values * observed_value ) nodes_to_update = [MsgPassers(msg_receiver=evidence_id, msg_sender=None)] - self._belief_propagation(nodes_to_update=set(nodes_to_update), - evidence=evidence) + self._belief_propagation(nodes_to_update=set(nodes_to_update), evidence=evidence) def query(self, evidence={}): """ - Run belief propagation given evidence. + Run belief propagation given 0 or more pieces of evidence. - Input: - evidence: dict - a dict key, value pair as {var: state_of_var observed}, - e.g. {'3': np.array([0,1])} if label '3' is True. + Args + evidence: dict, + a dict key, value pair as {var: state_of_var observed}, + e.g. {'3': np.array([0,1])} if label '3' is True. - Returns: - beliefs: dict - a dict key, value pair as {var: belief} + Returns + a dict key, value pair as {var: belief}, where belief is an np.array of the + marginal probability of each state of the variable given the evidence. Example ------- diff --git a/beliefs/models/base_models.py b/beliefs/models/base_models.py index cb91566..71af0cb 100644 --- a/beliefs/models/base_models.py +++ b/beliefs/models/base_models.py @@ -9,9 +9,11 @@ class DirectedGraph(nx.DiGraph): """ def __init__(self, edges=None, node_labels=None): """ - Input: - edges: an edge list, e.g. [(parent1, child1), (parent1, child2)] - node_labels: a list of strings of node labels + Args + edges: list, + a list of edge tuples, e.g. [(parent1, child1), (parent1, child2)] + node_labels: list, + a list of strings or integers representing node label ids """ super().__init__() if edges is not None: @@ -20,18 +22,15 @@ class DirectedGraph(nx.DiGraph): self.add_nodes_from(node_labels) def get_leaves(self): - """ - Returns a list of leaves of the graph. - """ + """Return a list of leaves of the graph""" return [node for node, out_degree in self.out_degree() if out_degree == 0] def get_roots(self): - """ - Returns a list of roots of the graph. - """ + """Return a list of roots of the graph""" return [node for node, in_degree in self.in_degree() if in_degree == 0] def get_topologically_sorted_nodes(self, reverse=False): + """Return a list of nodes in topological sort order""" if reverse: return list(reversed(list(nx.topological_sort(self)))) else: @@ -47,12 +46,12 @@ class BayesianModel(DirectedGraph): """ Base class for Bayesian model. - Input: - edges: (optional) list of edges, + Args + edges: (optional) list of edges, tuples of form ('parent', 'child') - variables: (optional) list of str or int + variables: (optional) list of str or int labels for variables - cpds: (optional) list of CPDs + cpds: (optional) list of CPDs TabularCPD class or subclass """ super().__init__() @@ -61,20 +60,17 @@ class BayesianModel(DirectedGraph): self.cpds = cpds def copy(self): - """ - Returns a copy of the model. - """ - copy_model = self.__class__(edges=list(self.edges()).copy(), - variables=list(self.nodes()).copy(), - cpds=[cpd.copy() for cpd in self.cpds]) - return copy_model + """Return a copy of the model""" + return self.__class__(edges=list(self.edges()).copy(), + variables=list(self.nodes()).copy(), + cpds=[cpd.copy() for cpd in self.cpds]) def get_variables_in_definite_state(self): """ - Returns a set of labels of all nodes in a definite state, i.e. with - label values that are kronecker deltas. + Get labels of all nodes in a definite state, i.e. with label values + that are kronecker deltas. - RETURNS + Returns set of strings (labels) """ return {label for label, node in self.nodes_dict.items() if is_kronecker_delta(node.belief)} @@ -84,14 +80,14 @@ class BayesianModel(DirectedGraph): Returns a set of labels that are inferred to be in definite state, given list of labels that were directly observed (e.g. YES/NOs, but not MAYBEs). - INPUT - observed: set of strings, directly observed labels - RETURNS - set of strings, labels inferred to be in a definite state + Args + observed: set, + set of strings, directly observed labels + Returns + set of strings, the labels inferred to be in a definite state """ - - # Assert that beliefs of directly observed vars are kronecker deltas for label in observed: + # beliefs of directly observed vars should be kronecker deltas assert is_kronecker_delta(self.nodes_dict[label].belief), \ ("Observed label has belief {} but should be kronecker delta" .format(self.nodes_dict[label].belief)) @@ -101,28 +97,40 @@ class BayesianModel(DirectedGraph): "Expected set of observed labels to be a subset of labels in definite state." return vars_in_definite_state - observed - def _get_ancestors_of(self, observed): - """Return list of ancestors of observed labels""" + def _get_ancestors_of(self, labels): + """ + Get set of ancestors of an iterable of labels. + + Args + observed: iterable, + label ids for which ancestors should be retrieved + + Returns + ancestors: set, + set of label ids of ancestors of the input labels + """ ancestors = set() - for label in observed: + for label in labels: ancestors.update(nx.ancestors(self, label)) return ancestors def reachable_observed_variables(self, source, observed=set()): """ - Returns list of observed labels (labels with direct evidence to be in a definite + Get list of directly observed labels (labels with evidence in a definite state) that are reachable from the source. - INPUT - source: string, label of node for which to evaluate reachable observed labels - observed: set of strings, directly observed labels - RETURNS - reachable_observed_vars: set of strings, observed labels (variables with direct - evidence) that are reachable from the source label. + Args + source: string, + label of node for which to evaluate reachable observed labels + observed: set, + set of strings, directly observed labels + Returns + reachable_observed_vars: set, + set of strings, observed labels (variables with direct evidence) + that are reachable from the source label """ - # ancestors of observed labels, including observed labels ancestors_of_observed = self._get_ancestors_of(observed) - ancestors_of_observed.update(observed) + ancestors_of_observed.update(observed) # include observed labels visit_list = set() visit_list.add((source, 'up')) diff --git a/beliefs/models/belief_update_node_model.py b/beliefs/models/belief_update_node_model.py index 17e98fa..1a9ab19 100644 --- a/beliefs/models/belief_update_node_model.py +++ b/beliefs/models/belief_update_node_model.py @@ -33,9 +33,9 @@ class BeliefUpdateNodeModel(BayesianModel): """ def __init__(self, nodes_dict): """ - Input: - nodes_dict: dict - a dict key, value pair as {label_id: instance_of_node_class_or_subclass} + Args + nodes_dict: dict + a dict key, value pair as {label_id: instance_of_node_class_or_subclass} """ super().__init__(edges=self._get_edges_from_nodes(nodes_dict.values()), variables=list(nodes_dict.keys()), @@ -45,12 +45,15 @@ class BeliefUpdateNodeModel(BayesianModel): @classmethod def init_from_edges(cls, edges, node_class): - """Create nodes from the same node class. + """ + Create model from edges where all nodes are a from the same node class. - Input: - edges: list of edge tuples of form ('parent', 'child') - node_class: the Node class or subclass from which to - create all the nodes from edges. + Args + edges: list, + list of edge tuples of form [('parent', 'child')] + node_class: Node class or subclass, + class from which to create all the nodes automatically from edges, + e.g. BernoulliAndNode or BernoulliOrNode """ nodes = set() g = nx.DiGraph(edges) @@ -68,10 +71,12 @@ class BeliefUpdateNodeModel(BayesianModel): """ Return list of all directed edges in nodes. - Args: - nodes: an iterable of objects of the Node class or subclass - Returns: - edges: list of edge tuples + Args + nodes: iterable, + iterable of objects of the Node class or subclass + Returns + edges: list, + list of edge tuples """ edges = set() for node in nodes: @@ -82,11 +87,13 @@ class BeliefUpdateNodeModel(BayesianModel): def set_boundary_conditions(self): """ - 1. Root nodes: if x is a node with no parents, set Pi(x) = prior - probability of x. + Set boundary conditions for nodes in the model. + + 1. Root nodes: if x is a node with no parents, set Pi(x) = prior + probability of x. - 2. Leaf nodes: if x is a node with no children, set Lambda(x) - to an (unnormalized) unit vector, of length the cardinality of x. + 2. Leaf nodes: if x is a node with no children, set Lambda(x) + to an (unnormalized) unit vector, of length the cardinality of x. """ for root in self.get_roots(): self.nodes_dict[root].update_pi_agg(self.nodes_dict[root].cpd.values) @@ -97,8 +104,11 @@ class BeliefUpdateNodeModel(BayesianModel): @property def all_nodes_are_fully_initialized(self): """ - Returns True if, for all nodes in the model, all lambda and pi - messages and lambda_agg and pi_agg are not None, else False. + Check if all nodes in the model are initialized, i.e. lambda and pi messages and + lambda_agg and pi_agg are not None for every node. + + Returns + bool, True if all nodes in the model are initialized, else False. """ for node in self.nodes_dict.values(): if not node.is_fully_initialized: @@ -106,27 +116,27 @@ class BeliefUpdateNodeModel(BayesianModel): return True def copy(self): - """ - Returns a copy of the model. - """ + """Return a copy of the model.""" copy_nodes = copy.deepcopy(self.nodes_dict) copy_model = self.__class__(nodes_dict=copy_nodes) return copy_model class Node: - """A node in a DAG with methods to compute the belief (marginal probability - of the node given evidence) and compute pi/lambda messages to/from its neighbors + """ + A node in a DAG with methods to compute the belief (marginal probability of + the node given evidence) and compute pi/lambda messages to/from its neighbors to update its belief. - Implemented from Pearl's belief propagation algorithm. + Implemented from Pearl's belief propagation algorithm for polytrees. """ def __init__(self, children, cpd): """ Args - children: list of strings - cpd: an instance of a conditional probability distribution, - e.g. BernoulliOrCPD or TabularCPD + children: list, + list of strings + cpd: an instance of TabularCPD or one of its subclasses, + e.g. BernoulliOrCPD or BernoulliAndCPD """ self.label_id = cpd.variable self.children = children @@ -134,15 +144,20 @@ class Node: self.cardinality = cpd.cardinality[0] self.cpd = cpd - # instances of DiscreteFactor with `values` an np.array of dimensions [1, cardinality] - self.pi_agg = self._init_aggregate_values() - self.lambda_agg = self._init_aggregate_values() + self.pi_agg = self._init_factor_for_variable() + self.lambda_agg = self._init_factor_for_variable() self.pi_received_msgs = self._init_pi_received_msgs(self.parents) - self.lambda_received_msgs = {child: self._init_aggregate_values() for child in children} + self.lambda_received_msgs = {child: self._init_factor_for_variable() for child in children} @property def belief(self): + """ + Calculate the marginal probability of the variable from its aggregate values. + + Returns + belief, an np.array of ndim 1 and shape (self.cardinality,) + """ if any(self.pi_agg.values) and any(self.lambda_agg.values): belief = (self.lambda_agg * self.pi_agg).values return self._normalize(belief) @@ -152,29 +167,50 @@ class Node: def _normalize(self, value): return value/value.sum() - def _init_aggregate_values(self): + def _init_factor_for_variable(self): + """ + Returns + instance of a DiscreteFactor, where DiscreteFactor.values is an np.array of + ndim 1 and shape (self.cardinality,) + """ return DiscreteFactor(variables=[self.cpd.variable], cardinality=[self.cardinality], values=None, state_names=None) def _init_pi_received_msgs(self, parents): + """ + Args + parents: list, + list of strings, parent ids of the node + Returns + msgs: dict, + a dict with key, value pair as {parent_id: instance of a DiscreteFactor}, + where DiscreteFactor.values is an np.array of ndim 1 and + shape (cardinality of parent_id,) + """ msgs = {} for k in parents: + if self.cpd.state_names is not None: + state_names = {k: self.cpd.state_names[k]} + else: + state_names = None + kth_cardinality = self.cpd.cardinality[self.cpd.variables.index(k)] msgs[k] = DiscreteFactor(variables=[k], cardinality=[kth_cardinality], values=None, - state_names=None) + state_names=state_names) return msgs def _return_msgs_received_for_msg_type(self, message_type): """ - Input: - message_type: MessageType enum - - Returns: - msg_values: list of DiscreteFactors containing message values (np.arrays) + Args + message_type: MessageType enum + Returns + msg_values: list, + list of DiscreteFactors with property `values` containing + the values of the messages (np.arrays) """ if message_type == MessageType.LAMBDA: msgs = [msg for msg in self.lambda_received_msgs.values()] @@ -188,11 +224,12 @@ class Node: Raise error if all messages have not been received. Called before calculating lambda_agg (pi_agg). - Input: - message_type: MessageType enum - - Returns: - msgs: list of DiscreteFactors containing message values (np.array) + Args + message_type: MessageType enum + Returns + msgs: list, + list of DiscreteFactors with property `values` containing + the values of the messages (np.arrays) """ msgs = self._return_msgs_received_for_msg_type(message_type) @@ -205,6 +242,10 @@ class Node: return msgs def compute_pi_agg(self): + """ + Compute and update pi_agg, the prior probability, given the current state + of messages received from parents. + """ if len(self.parents) == 0: self.update_pi_agg(self.cpd.values) else: @@ -217,6 +258,10 @@ class Node: pi_msgs = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) def compute_lambda_agg(self): + """ + Compute and update lambda_agg, the likelihood, given the current state + of messages received from children. + """ if len(self.children) != 0: lambda_msg_values = [ msg.values for msg in @@ -245,9 +290,8 @@ class Node: expected_shape = (self.cpd.cardinality[self.cpd.variables.index(key)],) if new_value.shape != expected_shape: - raise ValueError("Expected new value to be of dimensions ({},) but got {} instead" - .format(expected_shape, new_value.shape)) - # received_msg_dict[key]._values = new_value + raise ValueError("Expected new value to be of dimensions ({},) but got {} instead" + .format(expected_shape, new_value.shape)) received_msg_dict[key].update_values(new_value) def update_pi_msg_from_parent(self, parent, new_value): @@ -263,6 +307,15 @@ class Node: message_type=MessageType.LAMBDA) def compute_pi_msg_to_child(self, child_k): + """ + Compute pi_msg to child. + + Args + child_k: string or int, + the label_id of the child receiving the pi_msg + Returns + np.array of ndim 1 and shape (self.cardinality,) + """ lambda_msg_from_child = self.lambda_received_msgs[child_k].values if lambda_msg_from_child is not None: with np.errstate(divide='ignore', invalid='ignore'): @@ -273,6 +326,15 @@ class Node: raise ValueError("Can't compute pi message to child_{} without having received a lambda message from that child.") def compute_lambda_msg_to_parent(self, parent_k): + """ + Compute lambda_msg to parent. + + Args + parent_k: string or int, + the label_id of the parent receiving the lambda_msg + Returns + np.array of ndim 1 and shape (cardinality of parent_k,) + """ if np.array_equal(self.lambda_agg.values, np.ones([self.cardinality])): return np.ones([self.cardinality]) else: @@ -306,30 +368,31 @@ class Node: class BernoulliOrNode(Node): - def __init__(self, - label_id, - children, - parents): + """ + A node in a DAG associated with a Bernoulli random variable with state_names ['False', 'True'] + and conditional probability distribution described by 'Or' logic. + """ + def __init__(self, label_id, children, parents): super().__init__(children=children, cpd=BernoulliOrCPD(label_id, parents)) - def _init_aggregate_values(self): + def _init_factor_for_variable(self): + """ + Returns + instance of a DiscreteFactor, where DiscreteFactor.values is an np.array of + ndim 1 and shape (self.cardinality,) + """ variable = self.cpd.variable return DiscreteFactor(variables=[self.cpd.variable], cardinality=[self.cardinality], values=None, state_names={variable: self.cpd.state_names[variable]}) - def _init_pi_received_msgs(self, parents): - msgs = {} - for k in parents: - kth_cardinality = self.cpd.cardinality[self.cpd.variables.index(k)] - msgs[k] = DiscreteFactor(variables=[k], - cardinality=[kth_cardinality], - values=None, - state_names={k: self.cpd.state_names[k]}) - return msgs - def compute_pi_agg(self): + """ + Compute and update pi_agg, the prior probability, given the current state + of messages received from parents. Sidestep explicit factor product and + marginalization. + """ if len(self.parents) == 0: self.update_pi_agg(self.cpd.values) else: @@ -339,9 +402,18 @@ class BernoulliOrNode(Node): p_0 = reduce(lambda x, y: x*y, parents_p0) p_1 = 1 - p_0 self.update_pi_agg(np.array([p_0, p_1])) - return self.pi_agg def compute_lambda_msg_to_parent(self, parent_k): + """ + Compute lambda_msg to parent. Sidestep explicit factor product and + marginalization. + + Args + parent_k: string or int, + the label_id of the parent receiving the lambda_msg + Returns + np.array of ndim 1 and shape (cardinality of parent_k,) + """ if np.array_equal(self.lambda_agg.values, np.ones([self.cardinality])): return np.ones([self.cardinality]) else: @@ -362,30 +434,31 @@ class BernoulliOrNode(Node): class BernoulliAndNode(Node): - def __init__(self, - label_id, - children, - parents): + """ + A node in a DAG associated with a Bernoulli random variable with state_names ['False', 'True'] + and conditional probability distribution described by 'And' logic. + """ + def __init__(self, label_id, children, parents): super().__init__(children=children, cpd=BernoulliAndCPD(label_id, parents)) - def _init_aggregate_values(self): + def _init_factor_for_variable(self): + """ + Returns + instance of a DiscreteFactor, where DiscreteFactor.values is an np.array of + ndim 1 and shape (self.cardinality,) + """ variable = self.cpd.variable return DiscreteFactor(variables=[self.cpd.variable], cardinality=[self.cardinality], values=None, state_names={variable: self.cpd.state_names[variable]}) - def _init_pi_received_msgs(self, parents): - msgs = {} - for k in parents: - kth_cardinality = self.cpd.cardinality[self.cpd.variables.index(k)] - msgs[k] = DiscreteFactor(variables=[k], - cardinality=[kth_cardinality], - values=None, - state_names={k: self.cpd.state_names[k]}) - return msgs - def compute_pi_agg(self): + """ + Compute and update pi_agg, the prior probability, given the current state + of messages received from parents. Sidestep explicit factor product and + marginalization. + """ if len(self.parents) == 0: self.update_pi_agg(self.cpd.values) else: @@ -395,9 +468,18 @@ class BernoulliAndNode(Node): p_1 = reduce(lambda x, y: x*y, parents_p1) p_0 = 1 - p_1 self.update_pi_agg(np.array([p_0, p_1])) - return self.pi_agg def compute_lambda_msg_to_parent(self, parent_k): + """ + Compute lambda_msg to parent. Sidestep explicit factor product and + marginalization. + + Args + parent_k: string or int, + the label_id of the parent receiving the lambda_msg + Returns + np.array of ndim 1 and shape (cardinality of parent_k,) + """ if np.array_equal(self.lambda_agg.values, np.ones([self.cardinality])): return np.ones([self.cardinality]) else: diff --git a/beliefs/utils/math_helper.py b/beliefs/utils/math_helper.py index a25ea68..12325e1 100644 --- a/beliefs/utils/math_helper.py +++ b/beliefs/utils/math_helper.py @@ -1,10 +1,16 @@ -"""Random math utils.""" +"""Math utils""" def is_kronecker_delta(vector): - """Returns True if vector is a kronecker delta vector, False otherwise. - Specific evidence ('YES' or 'NO') is a kronecker delta vector, whereas - virtual evidence ('MAYBE') is not. + """ + Check if vector is a kronecker delta. + + Args: + vector: iterable of numbers + Returns: + bool, True if vector is a kronecker delta vector, False otherwise. + In belief propagation, specific evidence (variable is directly observed) + is a kronecker delta vector, but virtual evidence is not. """ count = 0 for x in vector: diff --git a/beliefs/utils/random_variables.py b/beliefs/utils/random_variables.py index 1a0b0f7..cad07aa 100644 --- a/beliefs/utils/random_variables.py +++ b/beliefs/utils/random_variables.py @@ -1,3 +1,4 @@ +"""Utilities for working with models and random variables.""" def get_reachable_observed_variables_for_inferred_variables(model, observed=set()): @@ -6,12 +7,16 @@ def get_reachable_observed_variables_for_inferred_variables(model, observed=set( ("reachable observed variables") that influenced the beliefs of variables inferred to be in a definite state. - INPUT - model: instance of BayesianModel class or subclass - observed: set of labels (strings) corresponding to vars pinned to definite - state during inference. - RETURNS - dict, of form key - source label (a string), value - a list of strings + Args + model: instance of BayesianModel class or subclass + observed: set, + set of labels (strings) corresponding to variables pinned to a definite + state during inference. + Returns + dict, + key, value pairs {source_label_id: reachable_observed_vars}, where + source_label_id is an int or string, and reachable_observed_vars is a list + of label_ids """ if not observed: return {} -- cgit v1.2.3 From d92ed9f14baead60fdd6c1d823345cc3ddd1bc04 Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Wed, 17 Jan 2018 15:04:57 -0800 Subject: consolidate Node methods _init_factor_for_variable, _init_pi_received_msgs, into a single method _init_factors_for_variables overrides of _init_factor_for_variable for BernoulliOr/AndNode not needed --- beliefs/models/belief_update_node_model.py | 79 ++++++++++-------------------- 1 file changed, 25 insertions(+), 54 deletions(-) diff --git a/beliefs/models/belief_update_node_model.py b/beliefs/models/belief_update_node_model.py index 1a9ab19..743bbcb 100644 --- a/beliefs/models/belief_update_node_model.py +++ b/beliefs/models/belief_update_node_model.py @@ -144,11 +144,14 @@ class Node: self.cardinality = cpd.cardinality[0] self.cpd = cpd - self.pi_agg = self._init_factor_for_variable() - self.lambda_agg = self._init_factor_for_variable() + self.pi_agg = self._init_factors_for_variables([self.label_id])[self.label_id] + self.lambda_agg = self._init_factors_for_variables([self.label_id])[self.label_id] + + self.pi_received_msgs = self._init_factors_for_variables(self.parents) + self.lambda_received_msgs = \ + {child: self._init_factors_for_variables([self.label_id])[self.label_id] + for child in children} - self.pi_received_msgs = self._init_pi_received_msgs(self.parents) - self.lambda_received_msgs = {child: self._init_factor_for_variable() for child in children} @property def belief(self): @@ -167,41 +170,33 @@ class Node: def _normalize(self, value): return value/value.sum() - def _init_factor_for_variable(self): - """ - Returns - instance of a DiscreteFactor, where DiscreteFactor.values is an np.array of - ndim 1 and shape (self.cardinality,) - """ - return DiscreteFactor(variables=[self.cpd.variable], - cardinality=[self.cardinality], - values=None, - state_names=None) - - def _init_pi_received_msgs(self, parents): + def _init_factors_for_variables(self, variables): """ Args - parents: list, - list of strings, parent ids of the node + variables: list, + list of ints/strings, e.g. the single node variable or list + of parent ids of the node Returns - msgs: dict, - a dict with key, value pair as {parent_id: instance of a DiscreteFactor}, + factors: dict, + where the dict has key, value pair as {variable_id: instance of a DiscreteFactor}, where DiscreteFactor.values is an np.array of ndim 1 and - shape (cardinality of parent_id,) + shape (cardinality of variable_id,) """ - msgs = {} - for k in parents: + variables = list(variables) + factors = {} + + for var in variables: if self.cpd.state_names is not None: - state_names = {k: self.cpd.state_names[k]} + state_names = {var: self.cpd.state_names[var]} else: state_names = None - kth_cardinality = self.cpd.cardinality[self.cpd.variables.index(k)] - msgs[k] = DiscreteFactor(variables=[k], - cardinality=[kth_cardinality], - values=None, - state_names=state_names) - return msgs + cardinality = self.cpd.cardinality[self.cpd.variables.index(var)] + factors[var] = DiscreteFactor(variables=[var], + cardinality=[cardinality], + values=None, + state_names=state_names) + return factors def _return_msgs_received_for_msg_type(self, message_type): """ @@ -375,18 +370,6 @@ class BernoulliOrNode(Node): def __init__(self, label_id, children, parents): super().__init__(children=children, cpd=BernoulliOrCPD(label_id, parents)) - def _init_factor_for_variable(self): - """ - Returns - instance of a DiscreteFactor, where DiscreteFactor.values is an np.array of - ndim 1 and shape (self.cardinality,) - """ - variable = self.cpd.variable - return DiscreteFactor(variables=[self.cpd.variable], - cardinality=[self.cardinality], - values=None, - state_names={variable: self.cpd.state_names[variable]}) - def compute_pi_agg(self): """ Compute and update pi_agg, the prior probability, given the current state @@ -441,18 +424,6 @@ class BernoulliAndNode(Node): def __init__(self, label_id, children, parents): super().__init__(children=children, cpd=BernoulliAndCPD(label_id, parents)) - def _init_factor_for_variable(self): - """ - Returns - instance of a DiscreteFactor, where DiscreteFactor.values is an np.array of - ndim 1 and shape (self.cardinality,) - """ - variable = self.cpd.variable - return DiscreteFactor(variables=[self.cpd.variable], - cardinality=[self.cardinality], - values=None, - state_names={variable: self.cpd.state_names[variable]}) - def compute_pi_agg(self): """ Compute and update pi_agg, the prior probability, given the current state -- cgit v1.2.3 From 1a9286b0c1698fe5329a8e0b2a886f0a98286d2b Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Wed, 17 Jan 2018 17:32:07 -0800 Subject: compute_pi_agg -> compute_and_update_pi_agg, compute_lambda_agg -> compute_and_update_lambda_agg --- beliefs/inference/belief_propagation.py | 6 +++--- beliefs/models/belief_update_node_model.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/beliefs/inference/belief_propagation.py b/beliefs/inference/belief_propagation.py index acd93d4..e6e7b18 100644 --- a/beliefs/inference/belief_propagation.py +++ b/beliefs/inference/belief_propagation.py @@ -72,9 +72,9 @@ class BeliefPropagation: assert len(node.parents) + len(node.children) - 1 == len(parent_ids) + len(child_ids) if node_to_update_label_id not in evidence: - node.compute_pi_agg() + node.compute_and_update_pi_agg() logging.debug("belief propagation pi_agg: %s", np.array2string(node.pi_agg.values)) - node.compute_lambda_agg() + node.compute_and_update_lambda_agg() logging.debug("belief propagation lambda_agg: %s", np.array2string(node.lambda_agg.values)) for parent_id in parent_ids: @@ -130,7 +130,7 @@ class BeliefPropagation: child_ids = node_sending_msg.children if node_sending_msg.pi_agg.values is None: - node_sending_msg.compute_pi_agg() + node_sending_msg.compute_and_update_pi_agg() for child_id in child_ids: logging.debug("child: %s", str(child_id)) diff --git a/beliefs/models/belief_update_node_model.py b/beliefs/models/belief_update_node_model.py index 743bbcb..ec329ca 100644 --- a/beliefs/models/belief_update_node_model.py +++ b/beliefs/models/belief_update_node_model.py @@ -236,7 +236,7 @@ class Node: else: return msgs - def compute_pi_agg(self): + def compute_and_update_pi_agg(self): """ Compute and update pi_agg, the prior probability, given the current state of messages received from parents. @@ -252,7 +252,7 @@ class Node: self.update_pi_agg(factor_product.marginalize(self.parents).values) pi_msgs = self.validate_and_return_msgs_received_for_msg_type(MessageType.PI) - def compute_lambda_agg(self): + def compute_and_update_lambda_agg(self): """ Compute and update lambda_agg, the likelihood, given the current state of messages received from children. @@ -370,7 +370,7 @@ class BernoulliOrNode(Node): def __init__(self, label_id, children, parents): super().__init__(children=children, cpd=BernoulliOrCPD(label_id, parents)) - def compute_pi_agg(self): + def compute_and_update_pi_agg(self): """ Compute and update pi_agg, the prior probability, given the current state of messages received from parents. Sidestep explicit factor product and @@ -424,7 +424,7 @@ class BernoulliAndNode(Node): def __init__(self, label_id, children, parents): super().__init__(children=children, cpd=BernoulliAndCPD(label_id, parents)) - def compute_pi_agg(self): + def compute_and_update_pi_agg(self): """ Compute and update pi_agg, the prior probability, given the current state of messages received from parents. Sidestep explicit factor product and -- cgit v1.2.3 From f91a0717df714f4852911dcc230b8025c46c0c88 Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Thu, 18 Jan 2018 18:05:41 -0800 Subject: example jupyter notebook comparing pgmpy inference results to beliefs results --- examples/compare_pgmpy_belief_propagation.ipynb | 990 ++++++++++++++++++++++++ 1 file changed, 990 insertions(+) create mode 100644 examples/compare_pgmpy_belief_propagation.ipynb diff --git a/examples/compare_pgmpy_belief_propagation.ipynb b/examples/compare_pgmpy_belief_propagation.ipynb new file mode 100644 index 0000000..c886aea --- /dev/null +++ b/examples/compare_pgmpy_belief_propagation.ipynb @@ -0,0 +1,990 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Run and compare belief propagation in pgmpy for a Bayesian Network\n", + "The purpose of this tutorial is to compare the results of inference from implementations of belief propagation by the `pgmpy` and `beliefs` libraries.\n", + "\n", + "We consider 4 different networks:\n", + " 1. Simple Model - 14 nodes, each with Bernoulli OR CPDs\n", + " 2. Custom CPD model - 4 nodes, where 2 of the nodes have custom CPDs (not OR or AND)\n", + " 3. Mixed AND and OR CPD model - 6 nodes, 5 with Bernoulli OR CPDs and 1 with Bernoulli AND CPD \n", + " 4. Many parents model* - 18 parent nodes sharing a single child node with a Bernoulli OR CPD\n", + "\n", + "For each network, we initialize a Bayesian model from a list of directed edges (tuples), visualize the network, then run inference using `pgmpy`. All models are polytrees -- only one path exists between any two nodes in the graph.\n", + "\n", + "*Note: We don't actually run inference with `pgmpy` on the \"Many parents model\", where one child node has 18 parents. The algorithm never converges because of the amount of memory (exponential in the number of parents of a node) required to work with explicit tabular conditional probability distributions (CPDs). In contrast, `beliefs` implements shortcuts (see [BernoulliAndNode and BernoulliOrNode)](https://github.com/drivergroup/beliefs/blob/generic_discrete_factor/beliefs/models/belief_update_node_model.py) for computing the marginal probabilities of nodes with deterministic CPDs like ANDs and ORs.\n", + "\n", + "Finally, note that `pgmpy` does not provide the ability to provide virtual evidence, i.e. to modify the likelihood of an unobserved node in the graph based on evidence that does not correspond to any nodes in the graph (see the method [`_run_belief_propagation`](https://github.com/drivergroup/beliefs/blob/13053909cb47464a800f708c0954f7def8a0deb4/beliefs/inference/belief_propagation.py)), so comparisons with `beliefs` involving inference with virtual evidence are not available.\n", + "\n", + "\n", + "### Requirements\n", + "Note that `pgmpy` is pinned to `networkx` v1.11, whereas `beliefs` is written for v2.0, which is incompatible with `pgmpy`. We therefore can't use `pgmpy` and `beliefs` in the same environment. Requirements for running this notebook are:\n", + "- pgmpy\n", + "- numpy\n", + "- networkx==1.11\n", + "- matplotlib" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "import networkx as nx\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\") # draw_networkx triggers Matplotlib deprecated warnings\n", + "\n", + "from pgmpy.models import BayesianModel\n", + "from pgmpy.factors.discrete import TabularCPD\n", + "from pgmpy.inference import BeliefPropagation\n", + "\n", + "import matplotlib as mpl\n", + "%matplotlib inline\n", + "mpl.rcParams['figure.figsize'] = (8, 8)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# I. Simple model\n", + "- 14 nodes, each corresponding to a Bernoulli random variable (a variable that only has two possible states: e.g. False/True or 0/1).\n", + "- Each node is a Bernoulli random variable, with an 'OR' conditional probability distribution, i.e. if at least one of the node's parents is True, then the node is True, otherwise False. Nodes without parents default to a flat prior probability.\n", + "- The same model is defined as `simple_model` in [test_belief_propagation.py](https://github.com/drivergroup/beliefs/blob/master/tests/test_belief_propagation.py)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Initialize Bayesian Model and visualize network" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nodes in the graph: ['14', '10', '11', '1', '4', '2', '6', '13', '7', '5', '12', '9', 'x', '8', '3']\n" + ] + } + ], + "source": [ + "simple_edges = [('1', '3'), ('2', '3'), ('3', '5'), ('4', '5'), ('5', '10'), ('5', '9'), ('6', '8'), ('7', '8'), \n", + " ('8', '9'), ('9', '11'), ('9', 'x'), ('14', 'x'), ('x', '12'), ('x', '13')]\n", + "simple_model = BayesianModel(simple_edges)\n", + "print(\"Nodes in the graph: \", simple_model.nodes())" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\nc+6c6wKXlFJ/KKWige+A5HMHegPzlVJhAEqpUMuGaXuSnB1L5cqVeeedd5gwYYLJz23xfbh8+TIN\nGzYkJCSEkJAQWrVqZdXziYw7evQot2/ftupiItmpMlhy9evXJzAw0Kzk2r59e3bu3EnBggWNti9f\nvpy33nqLyMhIa4WZLZiTnJ8HkpZ0+fvJtqSqAdU0TTukadpRTdNeN9WQpmkfapoWpGla0O3btzMX\nsY1IcnY8Y8eOZcOGDSYLJFj7+/Dtt99Sv359unfvzrZt2yhZsqTVziUyz9/fn0GDBlltMYrY2Fh+\n++23bFuCtWjRopQrV46TJ0+atX/Tpk3Zu3dvirrkmzdvxtvbO1ML1OQU5iRnUyNQkj8UyA1UBZoC\n3YBlmqalGOqslFqilPJUSnlauki8pUlydjzFixdnxIgR+Pn5pfjMWt+Hhw8f8vbbbzN58mR+/fVX\nhgwZIoO+7NT169fZuXMnPj4+VjvHmTNnKFu2LIUKFbLaOazNnOfOSXl4eHDw4EHKlStntH3v3r24\nublx/vx5S4eYLZiTnP8GyiZ5Xwb4x8Q+W5RSMUqpK8B5DMk6W1JKyVQqBzVo0CB+++039u/fb7Td\nGms5Hz16FHd3d5555hmOHz9OrVq1LNq+sKxFixbRvXt3q14XAgICsm2XdgJznzsnVa1aNQ4dOkSN\nGkbDmbhy5Qo1a9YkICDAkiFmC+Yk50CgqqZpFTVNywt0BX5Mts9moBmApmmuGLq5/7BkoLYUHh5O\nrly5cHZ21jsUYWPOzs58/vnn+Pr6Eh8fn7jdknfOcXFxfP7557Rr144ZM2awePFiXFxcLNK2sI7I\nyEiWLFlitelTCbLzSO0EqRUjSU+ZMmXYv38/9erVM9oeHR2Nl5cXO3bssFSI2UK6yVkpFQsMBHYB\nvwPrlVJnNE2bqGnaG0922wXc1TTtLLAHGK6UumutoK1NurQdW9euXYmPj2fdunXA0+Uikw9cyYzr\n16/TsmVLdu3aRVBQUIqyhsI+fffdd3h4eFh9SltOSM5VqlQhIiIiQ0uyJihevDi//PJLijWh4+Li\naNOmTbrFgnISs+Y5K6W2K6WqKaUqK6UmP9n2qVLqxyf/rpRSHyulaiilXlFKfWfNoK1NkrNjc3Jy\nYubMmYwePZqoqCiLLRe5efNmPDw8aN68Obt376Zs2bLpHyR0p5Rizpw5VlmzOanw8HDOnz+f7R9v\n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\nDRs2ZO/evZQvX95ibYeFhZks6WmuadOmMW3aNO7evWuxmETWhISEcPnyZTp27KhbDI7apf3nn3/S\noEEDnn32WXx8fChVqpTeIdk3c2+xLf3StVs7qSwUTT906JAqW7as8vX1VdHR0ers2bOqb9++Kn/+\n/Gl2eb/44otq4cKF6tGjRzr8gXOG8PBwNXDgQFW+fHl14MABq5zjyJEjqm7dullqo2/fvmro0KEW\nikhklY+Pj5o8ebKuMXTo0EGtXbtW1xhsbf/+/ap06dJq9uzZatmyZapHjx56h6QLMtCtLck5QWio\nUtOnK/XOO0q1bWv45/Tphu1puHPnjmrdurWqX7+++uuvv5RSSt29e1dNnTpVlSlTJs0kXbRoUeXn\n56euXr1qiz9hjnH69Gn18ssvq7feekvdu3fPaufZvn27atWqVZbauHnzpipevLi6dOmShaISmRUa\nGqqKFCmibt++rWscZcqUcajvw5IlS1TJkiXVrl27lFJKnTt3TlWoUEHnqPQhydnG4uLi1PTp01XJ\nkiXV1q1bE7dHR0er7777TtWvXz/NJJ0rVy7VpUsXdeTIER3/FPYvPj5eLViwQLm6uqqvvvoqUwO1\nMiK95SLN9dlnn6m33nrLAhGJrJg0aZJ6//33dY3hn3/+UcWKFbP6d9ceREdHq4EDB6oXXnhBnT9/\nPnF7fHy8Kl68uLp+/bqO0elDkrNODh48aNTNndTRo0dVt27dVO7cudNM1HXr1lVr1qxJcbyju337\ntmrXrp3y8PBQ586ds8k5582bp/r165fldh4/fqyef/55+fGlo+joaPXcc8+pkJAQXePYsmVLlntj\nsoM7d+6o5s2bK29vb5PrVbdt21Zt2LBBh8j0lZHk7LgDwqygQYMGnDhxgrNnz9KkSROuXr2a+Fm9\nevVYs2YNV65cYdSoUakONAoICKB79+5UrFiRKVOmyGAiYPfu3bi5uVGlShUOHz7MCy+8YJPzZmaO\nsykuLi5MnDgRX19fwy9iYXMbN26katWq1Kplquaf7ThC8ZEzZ85Qt25dateuzdatW02uV52RFaoc\nlrlZ3NKvnHjnnCBhzm3ybu6kHj9+rBYvXqyqV6+e5p20s7Oz+vDDD9Xp06dt/KfQX/T/2TvvsCiu\nLoy/Q1FaDFIVQUDFiqDUgIgFC6KiEiv2xBLsJnY0RkE0iA0DlhhEY0nssYEKFhBpgkpsWEBsUSyo\n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\n448r7Hc4HKxZs4bnnnuuWq28ZcuWVehmbt++vds0nb4KDQ1l/vz5fPjhh0RHR1c4drmbe8GCBT53\nc4eFhfH888+TkpLil65ykPFmYX+SnIXP4uLi+Oc//0lwcDDdu3cnJyfH6pCqZMeW87Zt20hMTOTY\nsWMV9rdo0YKMjAwefvjhapWfn5/PihUrKux78sknUVWt9+ul/v37c/DgQbcVsJxOJzNnzuSBBx7w\nuZt7+PDhlJaWut217gutNfv375eWs7A1Sc6iWhwOB2lpaTzyyCN07dqV7du3Wx3SVdkpOWutSU1N\nZcCAAW6Pp3Xr1u3KuKjXTp+GhQth1CgYOJAf7rmHCRcvElN2OCYmhhEjRlT/C/AgNjaWXbt2eVy8\nYtu2bcTHx5OZmel1uUFBQaSmpvL0009z6dKlasV46tQptNaVLgoihC2YfSDa35tMQlL37NixQzdr\n1kwvXry4yoUXrGKX5SILCwv1mDFjPE4skpyc7NtiHPv3G0uGhoUZW7npWvNBF4D+APSy5GT/f0Ee\nbNmyRUdHR7t9fcHBwXrBggU+TVrywAMP6MWLF1crrg8++MDjXOJC1DRkhjBhlZycHB0fH69HjBhR\no1Mv+sp1IYeffvop4DH8+OOPOjEx0eNMW0uWLPHtHxuTC52UgHY6HMb5AZCbm6u7devm8Z+QAQMG\n6LNnz3pV3ldffaVjYmL0uXPnfI5pxowZes6cOT5fL4SvvEnO0q0t/CouLo7MzEyCgoJsNw6ttba8\nWzs7O5uEhAT27dtXYX9UVBRbt27lj3/8o/djwa+8AikpUFBgpOCrCAaCCguN8195xcvovRcbG8vu\n3buZNm2a27EPP/yQDh06sHfvXtPl3Xbbbfzud7/j+eef9zkmuVNb1Apms7i/N2k5122lpaX6hRde\n0M2aNdPbt2+3OhyttftykQ6HI6Cf/9577+mwsDC3FuQtt9yiv/nmG98K3b9f6/Bw/RLoTqAbgH6k\nXEv5M9B9QDcGHQN6KOgfLx8PD7+yIlkgpKen68aNG3vs5l64cKHpbu6ffvpJR0dH6+PHj3sdg9Pp\n1FFRUV4tFCKEvyAtZ2E1pRSTJ09m1apVjBgxghdeeMEYR7GQa6vZ9bGfmlJaWsqsWbMYOXIkRUVF\nFY7169ePrKws32eqmjcPCgu5HngGGOdy+ALwByAH+B64Bhh7+WBhoXF9gAwcOJCDBw/StWvXCvud\n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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "nx.draw_networkx(simple_model, pos=nx.spring_layout(simple_model, k=0.12, iterations=20))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Define and associate CPDs for each node in the Bayesian Model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define a helper function for generating the values of an OR conditional probability distribution (CPD) for a variable given the number of parents." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def get_tabular_OR_cpd_values(n_parents):\n", + " if n_parents == 0:\n", + " values = [[0.5], [0.5]]\n", + " else:\n", + " values = (np.array([1.,] + [0.]*(2**n_parents-1) + [0.,] + [1.]*(2**n_parents-1))\n", + " .reshape(2, 2**n_parents)\n", + " .tolist())\n", + " return values" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Example OR CPD values for Bernoulli variable with 2 parents: [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 1.0, 1.0]]\n" + ] + } + ], + "source": [ + "print(\"Example OR CPD values for Bernoulli variable with 2 parents: \", get_tabular_OR_cpd_values(2))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "simple_model_cpds = {}\n", + "for node in simple_model.nodes():\n", + " parents = simple_model.predecessors(node)\n", + " n_parents = len(parents)\n", + " cpd_values = get_tabular_OR_cpd_values(n_parents)\n", + " simple_model_cpds[node] = TabularCPD(variable=node,\n", + " variable_card=2,\n", + " values=cpd_values,\n", + " evidence=parents,\n", + " evidence_card=[2]*n_parents)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPD for variable x, with parents '9' and '14'\n", + "╒═════╤══════╤══════╤══════╤══════╕\n", + "│ 9 │ 9_0 │ 9_0 │ 9_1 │ 9_1 │\n", + "├─────┼──────┼──────┼──────┼──────┤\n", + "│ 14 │ 14_0 │ 14_1 │ 14_0 │ 14_1 │\n", + "├─────┼──────┼──────┼──────┼──────┤\n", + "│ x_0 │ 1.0 │ 0.0 │ 0.0 │ 0.0 │\n", + "├─────┼──────┼──────┼──────┼──────┤\n", + "│ x_1 │ 0.0 │ 1.0 │ 1.0 │ 1.0 │\n", + "╘═════╧══════╧══════╧══════╧══════╛\n" + ] + } + ], + "source": [ + "print(\"CPD for variable x, with parents '9' and '14'\")\n", + "print(simple_model_cpds['x'])" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPD for variable '7', with no parents\n", + "╒═════╤═════╕\n", + "│ 7_0 │ 0.5 │\n", + "├─────┼─────┤\n", + "│ 7_1 │ 0.5 │\n", + "╘═════╧═════╛\n" + ] + } + ], + "source": [ + "print(\"CPD for variable '7', with no parents\")\n", + "print(simple_model_cpds['7'])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# Associate the CPDs we just defined per node with the Bayesian model\n", + "simple_model.add_cpds(*simple_model_cpds.values())" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Is the model valid?\n", + "simple_model.check_model()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Run inference using pgmpy's implementation of the belief propagation algorithm" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# instantiate the inference model\n", + "infer_simple_model = BeliefPropagation(simple_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define four cases for which we want to run inference. Each case corresponds to a set of observations of variables, i.e. a set of evidence.\n", + "- 'test' is the name of the corresponding beliefs tests in [test_belief_propagation.py](https://github.com/drivergroup/beliefs/blob/master/tests/test_belief_propagation.py)\n", + "- 'evidence' is the set of variables and their observed values for that case\n", + "- 'expected' is the expected outcome of the inference, which matches the expected outcomes in test_belief_propagation.py" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "cases = [{'test': 'test_no_evidence_simple_model', \n", + " 'evidence': {},\n", + " 'expected': {'x': 0.984375, '14': 0.5, '7': 0.5, '2': 0.5, '3': 0.75, '13': 0.984375,\n", + " '6': 0.5, '4': 0.5, '8': 0.75, '10': 0.875, '1': 0.5, '9': 0.96875,\n", + " '12': 0.984375, '5': 0.875, '11': 0.96875}},\n", + " {'test': 'test_positive_evidence_node_13',\n", + " 'evidence': {'13': 1},\n", + " 'expected': {'6': 0.50793650793650791, '3': 0.76190476190476186, \n", + " '9': 0.98412698412698407, '8': 0.76190476190476186, \n", + " 'x': 1.0, '4': 0.50793650793650791, '11': 0.98412698412698407,\n", + " '1': 0.50793650793650791, '5': 0.88888888888888884, \n", + " '2': 0.50793650793650791, '12': 1.0, '14': 0.50793650793650791, \n", + " '13': 1, '10': 0.88888888888888884, '7': 0.50793650793650791}},\n", + " {'test': 'test_positive_evidence_node_5',\n", + " 'evidence': {'5': 1},\n", + " 'expected': {'1': 0.5714285714285714, '5': 1, '3': 0.8571428571428571, '10': 1.0, \n", + " '8': 0.75, '2': 0.5714285714285714, '4': 0.5714285714285714, '6': 0.5, \n", + " '7': 0.5, '14': 0.5, '12': 1.0, '13': 1.0, '11': 1.0, '9': 1.0, \n", + " 'x': 1.0}},\n", + " {'test': 'test_positive_evidence_node_5_negative_evidence_node_14',\n", + " 'evidence': {'5': 1, '14': 0},\n", + " 'expected': {'6': 0.5, '7': 0.5, '9': 1.0, '3': 0.8571428571428571, \n", + " '1': 0.57142857142857151, '12': 1.0, 'x': 1.0, '11': 1.0, '14': 0.0, \n", + " '2': 0.57142857142857151, '4': 0.5714285714285714, '5': 1.0, '10': 1.0, \n", + " '13': 1.0, '8': 0.75}}]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define helper functions for extracting and comparing inference results." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def get_probability_of_True(query):\n", + " \"\"\"Return the marginal probability of a Bernoulli variable being True\n", + " \n", + " Args\n", + " query: a result returned by inference, \n", + " e.g. query = belief_propagation.query(variables, evidence)\n", + " Returns\n", + " infer: dict,\n", + " key, value pairs of {var: P(var is True)}\n", + " \"\"\"\n", + " infer = {}\n", + " for variable, factor in query.items():\n", + " infer[variable] = factor.values[1]\n", + " return infer" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def compare_expected_to_observed(expected, observed):\n", + " \"\"\"Compare inference results between expected and observed, where both are dictionaries\n", + " of key, value pairs of {var: P(var is True)}\n", + " \"\"\"\n", + " assert set(expected.keys()) == set(observed.keys()), set(observed.keys())-set(expected.keys())\n", + " for node in expected:\n", + " assert np.allclose(expected[node], observed[node], rtol=1e-05, atol=1e-10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run inference for each test case, then compare to expected results" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Test case: test_no_evidence_simple_model\n", + "Marginal probability of True: {'14': 0.5, '10': 0.87500000000000011, '11': 0.96875000000000011, '1': 0.5, '4': 0.50000000000000011, '2': 0.50000000000000011, '6': 0.5, '13': 0.984375, '9': 0.96875, '5': 0.875, '12': 0.984375, '7': 0.5, 'x': 0.984375, '8': 0.75, '3': 0.75000000000000011}\n", + "\n", + "Test case: test_positive_evidence_node_13\n", + "Marginal probability of True: {'14': 0.50793650793650791, '10': 0.88888888888888895, '11': 0.98412698412698418, '1': 0.50793650793650791, '4': 0.50793650793650802, '2': 0.50793650793650791, '6': 0.50793650793650791, '7': 0.50793650793650791, '5': 0.88888888888888895, '12': 1.0, '9': 0.98412698412698418, 'x': 1.0, '8': 0.76190476190476186, '3': 0.76190476190476197}\n", + "\n", + "Test case: test_positive_evidence_node_5\n", + "Marginal probability of True: {'14': 0.5, '10': 1.0, '11': 1.0, '1': 0.5714285714285714, '4': 0.5714285714285714, '2': 0.5714285714285714, '6': 0.5, '13': 1.0, '9': 1.0, '12': 1.0, '7': 0.5, 'x': 1.0, '8': 0.74999999999999989, '3': 0.85714285714285721}\n", + "\n", + "Test case: test_positive_evidence_node_5_negative_evidence_node_14\n", + "Marginal probability of True: {'10': 1.0, '11': 1.0, '1': 0.5714285714285714, '4': 0.5714285714285714, '2': 0.5714285714285714, '6': 0.5, '13': 1.0, '9': 1.0, '12': 1.0, '7': 0.5, 'x': 1.0, '8': 0.74999999999999989, '3': 0.85714285714285721}\n" + ] + } + ], + "source": [ + "for test_case in cases:\n", + " print(\"\\nTest case: \", test_case['test'])\n", + " \n", + " # pgmpy doesn't allow you to include evidence vars in the set of query variables:\n", + " evidence_vars = test_case['evidence'].keys()\n", + " variables = set(simple_model.nodes()) - set(evidence_vars)\n", + " [test_case['expected'].pop(var) for var in evidence_vars]\n", + " \n", + " query = infer_simple_model.query(variables=variables, evidence=test_case['evidence'])\n", + " results = get_probability_of_True(query)\n", + " print(\"Marginal probability of True: \", results)\n", + " \n", + " compare_expected_to_observed(test_case['expected'], results)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Special case: conflicting evidence\n", + "Notice how in the case of conflicting evidence, inference with pgmpy returns `nan`s, whereas beliefs throws a `ConflictingEvidenceError`." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Test case: test_conflicting_evidence\n", + "Results: {'14': nan, '10': nan, '11': nan, '1': nan, '4': nan, '2': nan, '6': nan, '13': nan, '9': nan, '12': nan, '7': nan, '8': nan, '3': nan}\n" + ] + } + ], + "source": [ + "query = infer_simple_model.query(variables=set(simple_model.nodes()) - {'x', '5'}, evidence={'x': 0, '5': 1})\n", + "results = get_probability_of_True(query)\n", + "\n", + "print(\"Test case: test_conflicting_evidence\")\n", + "print(\"Results: \", results)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# II. Custom CPD model\n", + "- 4 nodes\n", + "- Y-shaped model, with parents 'u' and 'v' as Bernoulli variables, 'x' a node with cardinality 3 and custom CPD, 'y' a node with cardinality 2 and custom CPD\n", + "- The same model is defined as `custom_cpd_model` in [test_belief_propagation.py](https://github.com/drivergroup/beliefs/blob/master/tests/test_belief_propagation.py)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Initialize Bayesian Model and visualize network" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nodes in the graph: ['y', 'v', 'x', 'u']\n" + ] + } + ], + "source": [ + "custom_cpd_model = BayesianModel([('u', 'x'), ('v', 'x'), ('x', 'y')])\n", + "print(\"Nodes in the graph: \", custom_cpd_model.nodes())" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAecAAAHVCAYAAADLvzPyAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4xLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvAOZPmwAAIABJREFUeJzs3XmczfXix/HXZwZjLIloG1uJ4mbL\niBahnfuzDNkZpBkzGEuNoigUSihLhiHLZMsy45aUpOVKlBGNpasQ2Yok5Y5tZr6/P0auwzCDM/M5\ny/v5eHjcOZ/zPd/zNlfzns/3fD/fr3EcBxEREfEcAbYDiIiIiCuVs4iIiIdROYuIiHgYlbOIiIiH\nUTmLiIh4GJWziIiIh1E5i4iIeBiVs4iIiIdROYuIiHiYfLbeuGTJkk758uVtvb2IiEieWr9+/W+O\n45TKybbWyrl8+fIkJyfbensREZE8ZYzZndNtdVhbRETEw6icRUREPIzKWURExMOonEVERDyMyllE\nRMTDqJxFREQ8jMpZRETEw6icRUREPIzKWURExMOonEVERDyMyllERMTDqJxFREQ8jMpZRETEw6ic\nRUREPIzKWURExMOonEVERDyMyllERMTDqJxFREQ8jMpZRETEw6icRUREPIzKWURExMOonEVERDyM\nyllERMTDqJxFREQ8jMpZRETEw6icRUREPIzKWURExMPksx1ARES8zMGDMHMmpKTA0aNQrBhUqwZd\nu0KpUrbT+QSVs4iI5My6dTByJHz4YebjEyf+91xiIrz0EjRqBAMHQu3adjL6CB3WFhGR7MXFQYMG\nsGRJZimfW8wAx49nji1ZkrldXJyNlD5DM2cREbm0uDiIjYXU1Oy3dZzM7WJjMx9HR+duNh+lmbOI\niFzcunU5L+Zz/V3Qycm5k8vHZVvOxpjpxpiDxpjNF3neGGPGG2O2G2NSjDF3uT+miIhYMXJk5iHr\nK3H8eObr5bLlZOY8E3j8Es83Aiqe+RMJ6IMGERFfcPBg5slfjuMybIDt5zzuAgzK6vWOA8uWwaFD\nuRbRV2Vbzo7j/Bv4/RKbNAMSnExrgWuNMTe5K6CIiFgyc+bV78MY9+zHz7jjM+cQYM85j/eeGRMR\nEW+WknLhWdmX6/hx2LTJPXn8iDvK2WQx5mQxhjEm0hiTbIxJPqTDHCIinu3oUffs58gR9+zHj7ij\n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\nTpZOnjxJZGSky1ilSpUYOHCgpUQi4i4qZ5GLeOGFFyhZsiT9+vWzHSVLo0aN4vvvv3cZi4+Pp2DB\ngpYSiYi7qJxFLiIgIICEhARWrlzJzJkzbcdx8cMPP1xw28snn3yS+vXrW0okIu6kE8JELqFYsWIk\nJSVRv3597rzzTkJDQ21HwnEcunfvzsmTJ8+OlSpVitdff91iKhFxJ82cRbJRuXJlJk+eTMuWLTl0\n6JDtOMycOZPPP//cZezNN9+kRIkSdgKJiNupnEVyoEWLFnTo0IG2bduSlpZmLcfBgweJjY11GXv0\n0Udp166dpUQikhtUziI59PLLL5M/f34GDBhgLcPTTz/N77//fvZxcHAwcXFxGGOsZRIR91M5i+RQ\nYGAgc+fOJTExkfnz5+f5+69YsYI5c+a4jA0ZMoRbb701z7OISO5SOYtchhIlSpCUlERMTAwpKSl5\n9r6pqalERUW5jFWrVs1jl3mJyNVROYtcpurVqzNu3DhatGjBkSNH8uQ9hw0bxs6dO88+NsYwdepU\n8ufPnyfvLyJ5S+UscgXat29PkyZN6NChA+np6bn6XikpKYwePdplrFevXtx99925+r4iYo/KWeQK\njRo1itTUVIYMGZJr75Genk5ERITLLwClS5e+4AIkIuJbVM4iVyh//vy8++67zJo1i3/961+58h5x\ncXF88803LmMTJ06kaNGiufJ+IuIZVM4iV+GGG25g0aJFREREsG3bNrfue+/evTz//PMuY2FhYTRr\n1syt7yMinkflLHKV7r77bkaMGEFYWBh//fWX2/YbExPjsr+iRYsyYcIEt+1fRDyXylnEDZ566inq\n1atHly5dcBznqveXlJTEkiVLXMZGjhxJSEjIVe9bRDyfylnETcaPH8/+/ft59dVXr2o/f/75JzEx\nMS5jdevWvWCds4j4Lt2VSsRNgoKCWLRoEbVr1+auu+7iscceu6L9vPDCC+zbt+/s43z58hEfH09g\nYKC7ooqIh9PMWcSNQkJCePfddwkPD3e5aEhOrV27lrfeestlrH///lStWtVdEUXEC6icRdysXr16\nDBo0iBYtWpCamprj150+fZrIyEiXz6wrVKjA4MGDcyOmiHgwlbNILujVqxfVqlUjIiIixyeIjR07\nlk2bNrmMTZ48meDg4NyIKCIeTJ85i+QCYwxTpkzhvvvuY9y4cfTt2/d/Tx48CDNnQkoKHD0KxYrx\n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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "nx.draw_networkx(custom_cpd_model, pos=nx.spring_layout(custom_cpd_model, k=0.10, iterations=10))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Define and associate CPDs for each node in the Bayesian Model" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "custom_cpd_x = TabularCPD(variable='x',\n", + " variable_card=3,\n", + " values=[[0.2, 0, 0.3, 0.1],\n", + " [0.4, 1, 0.7, 0.2],\n", + " [0.4, 0, 0, 0.7]],\n", + " evidence=['u', 'v'],\n", + " evidence_card=[2, 2])\n", + "\n", + "custom_cpd_y = TabularCPD(variable='y',\n", + " variable_card=2,\n", + " evidence=['x'],\n", + " evidence_card=[3],\n", + " values=[[0.3, 0.1, 0],\n", + " [0.7, 0.9, 1]])\n", + "\n", + "cpd_u = TabularCPD(variable='u', variable_card=2, values=[[0.5], [0.5]])\n", + "cpd_v = TabularCPD(variable='v', variable_card=2, values=[[0.5], [0.5]])" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "custom_cpd_model.add_cpds(custom_cpd_x, custom_cpd_y, cpd_u, cpd_v)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "custom_cpd_model.check_model()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Run inference using pgmpy's implementation of the belief propagation algorithm" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# instantiate the inference model\n", + "infer_custom_cpd_model = BeliefPropagation(custom_cpd_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Two cases for which we want to run inference." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "cases = [{'test': 'test_no_evidence_custom_cpd_model', \n", + " 'evidence': {},\n", + " 'expected': {'x': [0.15, 0.575, 0.275],\n", + " 'v': [0.5, 0.5],\n", + " 'u': [0.5, 0.5],\n", + " 'y': [0.1025, 0.8975]}},\n", + " {'test': 'test_evidence_custom_cpd_model',\n", + " 'evidence': {'x': 1},\n", + " 'expected': {'x': [0., 1., 0.],\n", + " 'v': [0.47826087, 0.52173913],\n", + " 'u': [0.60869565, 0.39130435],\n", + " 'y': [0.1, 0.9]}}]" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Test case: test_no_evidence_custom_cpd_model\n", + "Marginal probability of True: {'y': [0.10250000000000001, 0.8975000000000001], 'v': [0.5, 0.5], 'x': [0.15, 0.5750000000000001, 0.275], 'u': [0.5, 0.5]}\n", + "\n", + "Test case: test_evidence_custom_cpd_model\n", + "Marginal probability of True: {'y': [0.1, 0.9], 'v': [0.47826086956521735, 0.5217391304347826], 'u': [0.6086956521739131, 0.391304347826087]}\n" + ] + } + ], + "source": [ + "for test_case in cases:\n", + " print(\"\\nTest case: \", test_case['test'])\n", + " \n", + " # pgmpy doesn't allow you to include evidence vars in the set of query variables:\n", + " evidence_vars = test_case['evidence'].keys()\n", + " variables = set(custom_cpd_model.nodes()) - set(evidence_vars)\n", + " [test_case['expected'].pop(var, None) for var in evidence_vars]\n", + " \n", + " query = infer_custom_cpd_model.query(variables=variables, evidence=test_case['evidence'])\n", + " results = {var: factor.values.tolist() for var, factor in query.items()}\n", + " print(\"Marginal probability of True: \", results)\n", + " \n", + " compare_expected_to_observed(test_case['expected'], results)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# III. Mixed AND and OR CPD model\n", + "- 6 nodes total\n", + "- All nodes are Bernoulli with OR CPDs, except 'z' has an AND CPD\n", + "- The same model is defined as `mixed_cpd_model` in [test_belief_propagation.py](https://github.com/drivergroup/beliefs/blob/master/tests/test_belief_propagation.py)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Initialize Bayesian Model and visualize network" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nodes in the graph: ['v', 'z', 'y', 'w', 'x', 'u']\n" + ] + } + ], + "source": [ + "mixed_cpd_model = BayesianModel([('u', 'x'), ('v', 'x'), ('x', 'y'), ('x', 'z'), ('w', 'z')])\n", + "print(\"Nodes in the graph: \", mixed_cpd_model.nodes())" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "nx.draw_networkx(mixed_cpd_model, pos=nx.spring_layout(mixed_cpd_model, k=0.10, iterations=10))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Define and associate CPDs for each node in the Bayesian Model" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mixed_cpd_model_cpds = {}\n", + "\n", + "# First define CPDs for OR nodes\n", + "for node in {'y', 'u', 'v', 'w', 'x'}:\n", + " parents = mixed_cpd_model.predecessors(node)\n", + " n_parents = len(parents)\n", + " cpd_values = get_tabular_OR_cpd_values(n_parents)\n", + " mixed_cpd_model_cpds[node] = TabularCPD(variable=node,\n", + " variable_card=2,\n", + " values=cpd_values,\n", + " evidence=parents,\n", + " evidence_card=[2]*n_parents)\n", + " values = (np.array([1.,] + [0.]*(2**n_parents-1) + [0.,] + [1.]*(2**n_parents-1))\n", + " .reshape(2, 2**n_parents)\n", + " .tolist())\n", + "\n", + "# Define CPD for the single AND node\n", + "mixed_cpd_model_cpds['z'] = TabularCPD(variable='z',\n", + " variable_card=2,\n", + " values=[[1,1,1,0], [0,0,0,1]],\n", + " evidence=['w', 'x'],\n", + " evidence_card=[2, 2])" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mixed_cpd_model.add_cpds(*mixed_cpd_model_cpds.values())" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mixed_cpd_model.check_model()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Run inference using pgmpy's implementation of the belief propagation algorithm" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# instantiate the inference model\n", + "infer_mixed_cpd_model = BeliefPropagation(mixed_cpd_model)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "cases = [{'test': 'test_no_evidence_mixed_cpd_model', \n", + " 'evidence': {},\n", + " 'expected': {'x': 1-0.5**2, 'z': 0.5*(1-0.5**2), 'v': 0.5, 'u': 0.5, 'y': 0.75, 'w': 0.5}},\n", + " {'test': 'test_x_false_w_true_mixed_cpd_model',\n", + " 'evidence': {'x': 0, 'w': 1},\n", + " 'expected': {'u': 0, 'v': 0, 'y': 0, 'z': 0}},\n", + " {'test': 'test_x_true_w_true_mixed_cpd_model',\n", + " 'evidence': {'x': 1, 'w': 1},\n", + " 'expected': {'y': 1, 'z': 1, 'u': 2/3, 'v': 2/3}}]" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Test case: test_no_evidence_mixed_cpd_model\n", + "Marginal probability of True: {'w': 0.5, 'z': 0.375, 'y': 0.75, 'x': 0.75, 'v': 0.5, 'u': 0.5}\n", + "\n", + "Test case: test_x_false_w_true_mixed_cpd_model\n", + "Marginal probability of True: {'y': 0.0, 'v': 0.0, 'z': 0.0, 'u': 0.0}\n", + "\n", + "Test case: test_x_true_w_true_mixed_cpd_model\n", + "Marginal probability of True: {'y': 1.0, 'v': 0.66666666666666663, 'z': 1.0, 'u': 0.66666666666666663}\n" + ] + } + ], + "source": [ + "for test_case in cases:\n", + " print(\"\\nTest case: \", test_case['test'])\n", + " \n", + " # pgmpy doesn't allow you to include evidence vars in the set of query variables:\n", + " evidence_vars = test_case['evidence'].keys()\n", + " variables = set(mixed_cpd_model.nodes()) - set(evidence_vars)\n", + " [test_case['expected'].pop(var, None) for var in evidence_vars]\n", + " \n", + " query = infer_mixed_cpd_model.query(variables=variables, evidence=test_case['evidence'])\n", + " results = get_probability_of_True(query)\n", + "\n", + " print(\"Marginal probability of True: \", results)\n", + " \n", + " compare_expected_to_observed(test_case['expected'], results)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# IV. Many parents model\n", + "- 19 nodes: 18 parents sharing a single child, node '62.'\n", + "- All nodes are Bernoulli, and the child has an OR CPD\n", + "- The same model is defined as `many_parents_model` in [test_belief_propagation.py](https://github.com/drivergroup/beliefs/blob/master/tests/test_belief_propagation.py)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Initialize Bayesian Model and visualize network" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nodes in the graph: ['100', '86', '106', '122', '94', '62', '104', '110', '96', '116', '112', '114', '108', '98', '102', '64', '80', '118', '70']\n" + ] + } + ], + "source": [ + "many_parents_edges = [('96', '62'), ('80', '62'), ('98', '62'), ('100', '62'), ('86', '62'), \n", + " ('102', '62'), ('104', '62'), ('64', '62'), ('106', '62'), ('108', '62'), \n", + " ('110', '62'), ('112', '62'), ('114', '62'), ('116', '62'), ('118', '62'),\n", + " ('122', '62'), ('70', '62'), ('94', '62')]\n", + "many_parents_model = BayesianModel(many_parents_edges)\n", + "print(\"Nodes in the graph: \", many_parents_model.nodes())" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAecAAAHVCAYAAADLvzPyAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4xLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvAOZPmwAAIABJREFUeJzs3Xl8TNf/x/HXzZ5YYg3yUzsR+5II\nLRFb7dmsXztVFUtVv3ZBQpSiWqq22mkpUhH9UlsVVUssbexVqlrV0lJLkyDJ+f1xQ9NQWczMuTM5\nz8djHsncuXfOm5DP3HPPPUcTQqAoiqIoinHYyQ6gKIqiKMo/qeKsKIqiKAajirOiKIqiGIwqzoqi\nKIpiMKo4K4qiKIrBqOKsKIqiKAajirOiKIqiGIwqzoqiKIpiMKo4K4qiKIrBOMhquEiRIqJMmTKy\nmlcURVEUizp27NjvQoiiWdlXWnEuU6YMR48eldW8oiiKoliUpmk/ZnVf1a2tKIqiKAajirOiKIqi\nGIwqzoqiKIpiMKo4K4qiKIrBqOKsKIqiKAajirOiKIqiGIwqzoqiKIpiMKo4K4qiKIrBqOKsKIqi\nKAajirOiKIqiGIwqzoqiKIpiMKo4K4qiKIrBqOKsKIqiKAajirOiKIqiGIwqzoqiKIpiMKo4K4qi\nKIrBqOKsKIqiKAajirOiKIqiGIwqzoqiKIpiMKo4K4qiKIrBqOKsKIqiKAajirOiKIqiGIwqzoqi\nKIpiMKo4K4qiKIrBqOKsKIqiKAajirOiKIqiGIwqzoqiKIpiMA6yAyhKlly/DitWQHw83L4N7u5Q\nowb07QtFi8pOpyiKYlKqOCvGFhcH06bBtm3686Skv1/79FOYNAlat4axY8HXV05GRVEUE1Pd2opx\nLVgAAQEQE6MX5fSFGSAxUd8WE6Pvt2CBjJSKoigmp86cFWNasABGjICEhMz3FULfb8QI/XlYmHmz\nKYqimJk6c1YMZd68efh4e+M8aBB9MhTm3UBlwA1oAvyY7rX7QL+EBPIPGkTxwoWZPXu2xTIriqKY\nWqbFWdO0ZZqmXdc07dS/vK5pmjZX07TvNU2L1zStjuljKrmFp6cn4e7u9Muw/XcgFJgC3AR8gC7p\nXo8ALqAX7D116jBjxgw+//xzCyRWFEUxvaycOa8AWj3j9dZAxbTHAEBd+FNyLLRhQ4K//ZbCGbZ/\nClQFOgEu6MX4W+Bc2uurgAlAQcD7q694tXt3VqxYYZHMiqIoppZpcRZC7EM/Wfk3QcAqoTsEFNA0\nrYSpAiq5zL8U1NNAzXTP8wDl07bfAn5J/7qmUfP6dU6fPm22mIqiKOZkimvO/wf8lO75z2nbFCX7\n4uOfHJUN3APcM2xzB+6mvUb61xMTcb92jbt375orpaIoilmZojhrT9kmnrqjpg3QNO2opmlHb9y4\nYYKmFZtz+/ZTN+cF7mTYdgfIl/YaGV6/8+ef5MuXz+TxFEVRLMEUxfln4IV0z0ui9zI+QQixWAjh\nI4TwKapmdVKexj3j+bGuKvo15kf+Ai6mbS8IlMjw+rcPHlC1alUzhVQURTEvUxTnWKBX2qjt+sBt\nIcQ1E7yvkgslV61KkrMzKUAKkAQkAyHAKSA6bdtkoAb6rVUAvYAo9OvP55yd+fDyZfr06WPh9Iqi\nKKaRlVup1gIHAS9N037WNO0VTdMGapo2MG2XrcAl4HvgQ2CQ2dIqNi/q5k1c799nOrAGcEUvukXR\nC/N49DPlw8C6dMdFog8QKw00vn+fkSNG0KrVs24yUBRFMS5NiKdeHjY7Hx8fcfToUSltKwYXGqpP\nyZmTf5uaBiEhEB1t+lyKoijPQdO0Y0IIn6zsq2YIU4xn7Fhwdc3Zsa6u+vGKoihWTBVnxXh8fWHW\nLHBzy9ZhSXZ2JL/9Nvhk6YOpoiiKYanirBhTWNjfBVp72t166Wgaws2NJd7eDDt71jL5FEVRzEgV\nZ8W4wsJg7179GrKLy5Nd3a6u+vaQELS9e+l54ABffPEFCxculJNXURTFRNSSkYqx+fjog7tu3NCn\n9jx5Em7dgoIFoXp16NMH0u6ZdwdiY2Np2LAh3t7eNG7cWGZyRVGUHFOjtRWbs3PnTnr27MnBgwcp\nW7as7DiKrbl+Xf+gGB+vz2jn7g41akDfvo8/KCrK02RntLYqzopNmjt3Lh9++CFff/21msZTMY24\nOJg2DbZt05+nnwPe1VW/9a91a/1uAV9fORkVQ1O3Uim53tChQ/Hz86Nnz56kpqbKjqNYuwULICBA\nv/8+KenJxVkSE/VtMTH6fgvUyrnK81HFWbFJmqYxf/58/vjjDyZNmiQ7jmLNFiyAESMgISHziXGE\n0PcbMUIVaOW5qOKs2CwnJyeio6NZtWoVn3zyiew4ijWKi4MRIzibkEBT9EGHFYBNT9k1En2Jvl3w\nd4FWl+6UHFLFWbFpHh4ebN68mSFDhnD8+HHZcRRrM20ayQkJBAHtgJvAYqAH8F263S4CG9FXR3ss\nMVG/Rq0oOaCKs2LzatWqxcKFCwkODubXX3+VHUexFtevw7ZtnENfA3c4YA80BV4CVqfbdQjwNuCU\n/nghYOtW/TZARckmVZyVXKFDhw7069eP0NBQ7t+/LzuOYg1WrADgaVeZBfoSpgAb0Itym6e9h6Y9\nfh9FyQ5VnJVcY+LEiXh6ejJw4EBk3UKoWJH4eEhKojLgAcwEHgI7gL1AAnAPGAe892/vkZioT5yj\nKNmkirOSa9jZ2bFy5UpOnDjBe+/9669TRdHdvg2AIxAD/A8oDrwDdAZKApOAnsAzp7q5dcucKRUb\npYqzkqvkyZOHzZs3M2PGDLZv3y47jmJk7u6Pv62Bfrb8B7AduATUA3YDc9GLdnHgJ/TC/Xb69ylY\n0CJxFduiirOS65QuXZoNGzbQs2dPzp8/LzuOYlQ1augLqwDxQBJ6V/Ys4BrQB704nwK+SXt4AouA\nwY/ew9VVnwNeUbJJFWclV2rYsCFvvfUWgYGB3FLdjsrT9Onz+NvV6LdJeaAX5J2AM1CYv8+ai6OP\n5i4I5H10oBD/eB9FySpVnJVcq3///rRq1Yr//Oc/JCcny46jGI2Hhz5XtqYxE7iFPgBsG/pEJE9z\nGWj+6ImmQZs2ajEMJUdUcVZytXfeeYeUlBRGjx4tO4piRGPHPrmOeFa5uurHK0oOqOKs5GoODg58\n8sknxMbGskLdj6pk5OsLs2aBm1v2jnNz04/zydICRIryBAfZARRFtkKFChEbG0vjxo2pVKkSL774\nouxIipGEhelfR4zQ71t+1j3ymqafMc+a9fdxipID6sxZUQBvb29WrFhBx44d+emnn2THUYwmLAz2\n7oWQEH0Ed8aubldXfXtIiL6fKszKc1LFWVHStGnThuHDhxMUFERCQoLsOIrR+PhAdDRcuQKRkdCz\nJ6lt27LWwYGE0aP17dHRqitbMQlN1jSGPj4+4qhaTk0xGCEEvXv35v79+6xbtw5N02RHUgyuadOm\njBo1ilatWsmOohicpmnHhBBZ+vSmzpwV87h+HWbMgB49oH17/euMGYZfoUfTNBYvXszly5eZOnWq\n7DiKFahXrx5HjhyRHUOxMWpAmGJacXH6GrbbtunPk5L+fu3TT2HSJP3e0bFj9ZGwBuTi4sKmTZvw\n8/OjatWqhISEyI5kPNev66stxcfrc1C7u+szavXtm+vu661Xrx7Lli2THUOxMapbWzGdBQtsakRr\nXFwcbdq0Yffu3dSoUUN2HGN41ocvV1f9527wD1+m9vPPP1OnTh1+++03dRlEeSbVra1Y3qPCnJDw\n7MIM+usJCfr+CxZYJl8O+Pr6MmfOHIKCgrhh8O54i1iwAAICICZGL8rpCzPoH8qSkvTXAwIM/bM1\npZIlS+Lo6MiPP/4oO4piQ1RxVnJs3rx5+Pj44OzkRJ+hQ/WCm2Y3UBlwA5oA6X9tjQAqAvkSEqg8\neDCrIiMtGTtbunXrRteuXenYsSMPHjyQHUceG/zwZUrqurNiaqo4Kznm6elJeHg4/UqWhJSUx9t/\nB0KBKcBNwAfoku64PMAW4DawUgiGTZ3K119/bbng2TR16lTc3d15/fXXkXUZSKq4uL8Lc3Y8KtC5\n4PKVKs6KqanirORYaGgowS++SOErV/6x/VOgKtAJcAEigG+Bc2mvR6KfVdsBfkCjlBQO7txpodTZ\nZ2dnx5o1a9i/fz8LcsmZIKTrGalfnz4ZCvOzekZuon8YKwIUSUige2god+7csVBqOVRxVkxNFWfl\n+TxlPurTQM10z/MA5dO2Z5QIxKWmUtXgs3Llz5+f2NhYJk+ezBdffCE7jkV4enoSPmQI/TIMcsqs\nZyQcfQWnS8BF4LerV4kYNcoimWXx8fHhxIkTanUzxWRUcVaeT3z8P7q0QV9Wzz3Dbu7A3accPhC9\nkLe8f98s8UypfPnyfPzxx/znP//h4sWLsuOYXWhoKMHXr1M4w/bMekZ+AIKB/Og/9xB7e07v2WOR\nzLK4u7tTsmRJzpw5IzuKYiNUcVaez+3bT2zKC2TsxLwD5MuwbSRwClgPaH/+aY50Jte0aVMmTpxI\nYGCgzXfVAk/98JVZz8hg4DP0s+dbQPTDh7TOn9/8WSVTXduKKanirDwf94znyPpZ1bfpnv+F3r1Z\nNd22SeiL1u9AP8OiYEGzRTS1QYMG0ahRI3r06EFKhsJlc57y4SuznpE6wAOgcNrDHhjk4WG+jAah\nirNiSqo4KzmWnJxMkrc3Kfb2pABJQDIQgn5GHJ22bTJQA30AEcA04GNgJ/ovb1xdoXp1C6fPOU3T\nmDt3Lrdv32bChAmy45jXUz58ZdYz0gmohF6s76CfVfc4/bQRB7ZFFWfFlFRxVnIsKioK1/Bwpqek\nsAZwBaKAouiFeTxQEDgMrEt33DjgCvq9znmBvImJvGUl3dqPODk5sXHjRtauXcvHH38sO4751KgB\n9vb/2JRZz8i3wGvo3d15gYHOzmy9ds38WSWrUaMGFy5c4K+//pIdRbEBqjgrORYREYEQAhESgtA0\nBPrgIIDm6AOEEoEvgTLpjhPAffTu0Xuaxr3QUMZZ4SITRYsWZfPmzQwbNoy4uDjZcUwuOTmZpK5d\nSYFs9Yz4AkvQf/aJwOLkZGrWrImtc3Z2plq1apw4cUJ2FMUGqOKsPL+xY59cfD6LhIuLfryVqlGj\nBosXLyY0NJRffvlFdhyTioqKwrV06Wz3jCwDLgMlgf8DLhUuzIqPPrJkdGlU17ZiKqo4K8/P11df\nxMLNLVuHPXB0JCJfPq5Y+WChkJAQXnvtNUJCQkjKON+0FXvcM3LkCMLNLcs9I2XRZ4D7A7jp5sbn\n//sfFStWtFhumVRxVkxFFWfFNMLC/i7Qma3Mo2ng5obTnDkUGDMGf39/vvvuO8vkNJPx48dTunRp\nBgwYYHtTfObwwxdubvpxPllahMcmqOKsmIoqzorphIXB3r0QEgIuLk92dbu66ttDQvT9wsIYPnw4\nEyZMICAggPj4eDm5TUDTNFasWMGpU6eYNWuW7Diml40PX6lpH76MviSoOVSsWJGbN2+qVcyU5+Yg\nO4BiY3x8IDoabtzQp/Y8eRJu3dLvY65eHfr0gaJF/3HIK6+8Qr58+WjRogWbN2+mfv36UqI/Lzc3\nNzZv3oyfnx9Vq1alTZs2siOZVliYfhY9bRps3aoX6cTEv193dSUlJYVdjo4027ULByv9OT4POzs7\nfH19OXLkCG3btpUdR7Fiqjgr5lG0KIwcmeXdO3fuTN68eQkMDGTdunU0bdrUjOHM54UXXmDjxo0E\nBwezd+9evL29ZUcyrUw+fNn17s20zp355exZ+ubC4gx/d22r4qw8D03W9TEfHx9xNBcsJadkz969\ne+nUqRNLly6lffv2suPk2PLly3nrrbc4fPgwhQoVkh3Hog4cOED37t05f/48zs7OsuNY3ObNm1m4\ncCHbtm2THcW0rl/XP5DFx+szx7m76/fB9+37RG+Y8nSaph0TQmRpEIYqzorhxMXF0b59e2bPnk23\nbt1kx8mxN998k5MnT7Jt2zYcHHJXJ1WbNm1o27YtgwcPlh3F4q5du0a1atX4/fff0TIbHGkN4uL0\nSxmPPmykvyPB1RWEgNat9VsifX3lZLQS2SnOakCYYji+vr7s2rWLUaNGsWjRItlxcmzGjBnY29vz\n3//+V3YUi5syZQpTp04lIcM60LlBiRIlyJMnD5cuXZId5fktWAABARAToxfljLcKJibq22Ji9P1y\n0Xrn5qaKs2JI1apVY+/evUyfPp2ZM2fKjpMjDg4OrFu3js8//5wlS5bIjmNRdevWpUGDBsyfP192\nFCls4paqBQtgxAhISNDPjp9FCH2/ESNUgTYRVZwVwypfvjz79+9n2bJlhIeHW+X9wwUKFCA2NpZx\n48bx1VdfyY5jUZMnT2bmzJm5Y2nNDKy1OM+bNw8fHx+cnZzoM3SoXnDRVxnriD7ZjIY+8Ux6AhgN\nFE5IoPCgQYzq1csq/78aiSrOiqGVLFmSffv2sXXrVoYNG0ZqaqrsSNnm5eXF6tWr6dSpEz/++KPs\nOBZTtWpVXn75Zd577z3ZUSzOWouzp6cn4eHh9CtZ8ol1vBsCa4DiTzluMRCDvuhJPPBZTIxVX5Iy\nAjUgTLEKf/75J+3ataNChQosWbLEKgdYzZ49m1WrVnHgwAHy5MkjO45FXLx4ET8/P7777rtcNWr9\n7t27lChRglu3buHo6Cg7TvZcv064pyc/p6Sw4ikvl0Qv0gHptr0I9AEGpD1f6uDAhzVrckj9jv8H\nNSBMsTkFChRg+/btXLt2ja5du3L//n3ZkbJt+PDh1K5dm969e1tlD0BOlC9fntDQUKsdN5BT+fLl\no0yZMpw6dUp2lOxbsSLbh5wG0q87VtPentMnT5oqUa6kirNiNfLkyUNsbCxCCAIDA61u3VxN01i4\ncCG//PILU6ZMkR3HYiZMmMDixYv59ddfZUexKGvt2iY+/oku7czcA9zTPXe/f597Dx6o687PQRVn\nxao4OzvzySefULx4cVq2bMnt27dlR8oWZ2dnPv30U5YuXUp0dLTsOBbxwgsv0LNnT6ZNmyY7ikVZ\nbXHOwf+pvED6YX93gLz29rZxn7ckqjgrVsfBwYHly5dTu3ZtmjZtanWLDBQvXpyYmBgGDhzIN998\nIzuORYwdO5Y1a9bw008/yY5iMVZbnN3dM98ng6rog8Ee+RaoWrCgqRLlSqo4K1bJzs6OuXPn0qpV\nK/z9/bl69arsSNlSp04d5s2bR3BwMNevX5cdx+yKFSvGgAEDclV3fvXq1bl06RJ3796VHSXLkpOT\nSfL2JsXenhQgCUhOe+1+2nPQb61KQr+FCqAXMBu4CvwCvKNp9GnUyHLBbZAqzorV0jSNqVOn0rdv\nXxo1asTFixdlR8qWLl260KNHDzp06MCDBw9kxzG7kSNH8umnn/L999/LjmIRjo6O1KxZk+PHj8uO\nkmVRUVG4hoczPSWFNYArEJX2mlfa86tAy7TvH90Y+BrQHqgOVAPa2tnx2sKFFs1ua1RxVqzeqFGj\nGDVqFI0bN+b06dOy42TL5MmTKVy4MIMGDbL5wTOFChVi2LBhREREyI5iMdbWtR0REYEQAhESgtA0\nBBCR9tpl9DPl9I8yaa9pwAzgJnBT05gRFITm4WHJ6DZHFWfFJgwcOJC3336bZs2aERcXJztOltnZ\n2bF69WoOHz7M+++/LzuO2Q0bNoydO3da3YeonLK24vzY2LH6ohY54eqqH688F1WcFZvRvXt3Fi9e\nTNu2bdm3b5/sOFmWL18+YmNjeeutt9i5c6fsOGaVP39+Ro4cycSJE2VHsYh69epx+PBh2TGyz9cX\nZs0CN7fsHefmph/nk6V5NpRnUMVZsSmBgYGsXbuWDh06sHXrVtlxsqxs2bKsW7eOHj16cOHCBdlx\nzGrQoEEcOnSIY8eOyY5iduXLl+fevXtcu3ZNdpTsCwv7u0BndkuUpv1dmMPCLJPPxqnirNicZs2a\nERsbS9++fVm/fr3sOFkWEBBAZGQkgYGBVnf/dna4ubkxbtw4wsPDZUcxO03TqFevnlVdavmHsDDY\nuxdCQsDF5cmubldXfXtIiL6fKswmk6XirGlaK03Tzmua9r2maWOe8nopTdP2aJp2QtO0eE3T2pg+\nqqJkXYMGDdixYwdvvPEGy5Ytkx0nywYOHEjTpk3p1q0bKdmcpcmavPrqq5w9ezZXrNRltdedH/Hx\ngehouHIFIiP5rFAhbjVsCD17QmSkvj06WnVlm5oQ4pkPwB64CJQDnNDvL6+SYZ/FQFja91WAy5m9\nb926dYWimNv58+dFqVKlxLvvvis7SpY9ePBANGnSRIwcOVJ2FLNatmyZaNy4sUhNTZUdxay2bNki\nWrRoITuGyZQvX1589913smNYJeCoyKQ2Pnpk5cy5HvC9EOKSEOIBsA4Iyljjgfxp37uj34euKNJV\nqlSJ/fv3M3/+fCIjI63idiVHR0c2bNhAdHQ0q1evlh3HbHr27Mm1a9fYtWuX7Chm9ahb21YWO/nz\nzz8pUKCA7Bg2Lyvr7v0fkH7OvZ8Bvwz7RAA7NE0bCuQBmpsknTW4fl1fxSU+Xp+T1t0datSAvn2h\naFHZ6RSgVKlS7N+/n5dffpk7d+4wa9Ysw8/5W7hwYTZv3kyTJk2oVKkSfn4Z/8tZPwcHByIjIwkP\nD6d58+aG/5nklIeHBwUKFOD777+nUqVKsuM8FyEEf/75J+45mOJTyZ6snDk/7X9MxtOP/wArhBAl\ngTbAak3TnnhvTdMGaJp2VNO0o9Y2H/IT4uIgNBRKl4ZJk+Cjj+Czz/SvERFQqpT+urUOBLExxYoV\n48svv+TAgQMMGDDAKq7nVqtWjaVLl9KhQwerm540qzp37kxSUhJbtmyRHcWsrP66c5qEhAScnJxw\ncnKSHcXmZaU4/wy8kO55SZ7stn4FWA8ghDgIuABFMr6REGKxEMJHCOFT1JrPKhcsgIAAiImBpCT9\nkV5ior4tJkbfb8ECGSmVDAoWLMjOnTu5dOkS3bt3t4opMwMDAxk8eDDBwcEkJibKjmNydnZ2TJky\nhQkTJthMt+/T2EpxVl3alpOV4hwHVNQ0raymaU5AVyA2wz5XgGYAmqZ5oxdnKz81/hcLFsCIEZCQ\nAJldvxRC32/ECFWgDSJfvnz873//IyEhgZCQEKsoeGPGjKFixYq88sorVnHNPLvat2+Pi4uLVd32\nll22VJxVl7ZlZFqchRDJwBBgO3AWWC+EOK1p2mRN0wLTdvsv8Kqmad8Ca4E+woZ+i8ybNw8fHx+c\nnZzoM3SoXnDT7AYqA25AE/6eCP6RXUCdhATyDBrEC8WK2fQvIGvh4uJCdHQ0BQoUoHXr1ty5cyfz\ngyTSNI2lS5fy3Xff8fbbb8uOY3KaphEVFcWkSZNITk7O/AArVKdOHU6ePGkVvTXPcvv2bXXmbCFZ\nus9ZCLFVCFFJCFFeCDE1bdtEIURs2vdnhBAvCSFqCiFqCSF2mDO0pXl6ehIeHk6/kiUh3bXK34FQ\nYAr6hO8+QJd0x50BugFTgdvAN76+1K1b12K5lX/n6OjIqlWrqFy5Ms2bN+ePP/6QHemZXF1diYmJ\n4f3337fJ67PNmzenRIkSNjs6PU+ePFSoUIH4+HjZUZ6L6ta2HDVDWBaEhoYS/OKLFL5y5R/bP0Vf\nZLwTej9+BPpN4OfSXo9CX0qtNfqw+MK7d1M+f34UY7C3t2fBggU0adKExo0bG36KxZIlSxIdHc0r\nr7xicwtHPDp7joyM5P79+7LjmIUtdG2rbm3LUcU5q1aseGLTaaBmuud5gPJp2wEOpX2tDpQAejx8\nyM35882XUck2TdOYPn063bp1o1GjRly+fFl2pGeqX78+s2bNIjAw0PBn+9nVsGFDvL29Wbp0qewo\nZmELxVl1a1uOKs5ZFR//jy5tgHvoM66k5w7cTfv+Z2A1EA1cABJTUhj6lCKvyKVpGuPGjWPYsGH4\n+/tz7ty5zA+SqFevXoSGhtKpUycePnwoO45JRUVFMXXqVBLSjeuwFbZQnFW3tuWo4pxVT1mIIC+Q\ncSjRHSBf2veuQF+gUtq+44CtNnq/qi0YOnQoU6ZMoUmTJpw4cUJ2nGeaPn06Li4uDB8+XHYUk6pb\nty7169dnvg32MFWtWpUrV65Y9aImqlvbclRxzqqn/IOsin6N+ZG/0Cchr5r2vAZPmcHFRmdBshW9\ne/dm3rx5tGzZkgMHDsiO86/s7e1Zu3Ytu3btYtGiRbLjmNTkyZOZOXOm4UfRZ5eDgwO1a9e26qUy\nVbe25ajinAXJyckkeXuTYm9PCpAEJAMhwCn0buskYDJ6Qa6cdlxfYDlwCUgA3ra3p121apaOr2RT\nhw4dWL16NcHBwezYYdwbD9zd3dmyZQsTJ05k7969suOYTNWqVWnRogVz5syRHcXkrL1rW3VrW44q\nzlkQFRWFa3g401NSWIPeXR0FFEUvzOOBgsBh9FVBHukH9EKfiLw04CwEc9etQzG+li1bsmnTJnr0\n6MGmTZtkx/lXFStWZM2aNXTp0oUffvhBdhyTiYiIYM6cOdy8eVN2FJOyheKsurUtQxXnLIiIiNCX\n8QoJQWgaAv22KdBX+DgHJAJfAmUyHBuJPlXar8DrJUuSlDevRTIrz69hw4Z8/vnnDBo0iFWrVsmO\n869atGjB2LFjCQoK4u7du5kfYAUqVKhAaGgoM2fOlB3FpOrVq8fhw4dlx8gx1a1tOao4Z8fYseDq\nmqND7VxdOdi4MdWrV2fWrFlWP1NQblGnTh2++OILxo8fzwcffCA7zr96/fXXqVevHr169bKZOarD\nw8NZtGgRv/76q+woJlOmTBkePHhgtQuZqG5ty1HFOTt8fWHWLHBzy95xbm5o77zD66tWceDAAXbv\n3k2NGjXYvn27eXIqJuXt7c2+ffuYPXs206ZNkx3nqTRN44MPPuDGjRtERETIjmMSpUqVomfPnkyf\nPl12FJPRNM2qu7ZVt7blqOKcXWFhfxfozEZea5q+36xZ+nGAl5cXW7duZebMmQwaNIjg4GAuXbpk\ngeDK8yhbtiz79+9nzZo1jBkzxpALUDg7OxMdHc3KlSttZg73cePGsXr1an766afMd7YS1lycVbe2\n5ajinBNhYbB3L4SEgIvLk13gg5pXAAAgAElEQVTdrq769pAQfb+0wvyIpmm0b9+e06dPU69ePXx9\nfZkwYYJNTrxgSzw9Pdm7dy+7d+9m0KBBhuw+LlasGJs3b2bw4MEcP35cdpznVqxYMQYMGMCUKVNk\nRzEZPz8/qyzO9+/f5+HDh7hlt+dQyRFN1hmAj4+POHr0qJS2TerGDX1qz5Mn4dYtKFgQqleHPn0g\ni2tW//TTT4wcOZKDBw8ya9YsOnbsiKbuhzasO3fu0L59e1544QWWL1+Oo6Oj7EhP2LhxI2+++SZH\njhyhePHisuM8l5s3b1KpUiUOHTpEhQoVZMd5bn/88QflypXj1q1b2NlZz/nR9evXqVKlCr///rvs\nKFZL07RjQgifLO2rirNx7N27l6FDh1KkSBHmzp1LNXVPtGElJCTQsWNHnJycWLduHS4uLrIjPSEi\nIoIdO3awZ88enJ2dZcd5LpMnT+bChQs2s2pVhQoV2LJlC97e3rKjZNmFCxdo3bo133//vewoVis7\nxdl6PrblAo0bN+b48eOEhobStGlThg0bxp9//ik7lvIUbm5uxMTE4OTkRLt27bh3757sSE+YOHEi\nJUqUYODAgYa8Rp4db7zxBjt27LCZ1bis8bqzGqltWao4G4yDgwNDhgzhzJkzJCUlUblyZZYsWWLI\n65u5nZOTE2vXrqVMmTK0aNGCW7duyY70D3Z2dqxcuZLjx4/z3nvvyY7zXPLnz8/IkSOZOHGi7Cgm\nYa3FWY3UthxVnA2qSJEiLFq0iK1bt7J8+XL8/Pw4dOhQ5gcqFmVvb8+HH35I/fr1adKkCb/99pvs\nSP+QN29eYmNjmTFjhtXfujdo0CAOHTpk1XNTP2KNxVmN1LYsVZwNrk6dOnz11Ve8/vrrdOjQgT59\n+tjUpAy2QNM0Zs+eTXBwMP7+/ly5ckV2pH8oXbo069evp2fPnpw/f152nBxzc3Nj3LhxTJgwQXaU\n51a7du3HvWPWQnVrW5YqzlZA0zR69uzJuXPn8PDwoFq1asyePdvm1vK1ZpqmERERwcCBA/H39+fC\nhQuyI/1Do0aNeOuttwgMDLTqcQz9+/fnzJkzhl4xLCtcXV3x8vLi22+/zXxng1Dd2palirMVyZcv\nHzNmzODAgQPs2LGDGjVqsHPnTtmxlHSGDx9OeHg4AQEBxMfHy47zD/3796dly5Z07dqVlJQU2XFy\nxNnZmYkTJzJ+/HirH+RmbV3bqlvbslRxtkJeXl5s27aNt99+m4EDBxIaGmpTKxJZu/79+zN79mxa\ntGhhuHECs2fPJjk5mVGjRsmOkmO9evXi2rVr7N69W3aU52JtxVl1a1uWKs5WStM0AgMDOX36NHXr\n1sXX15dJkyapWcYMokuXLixbtoz27dvzxRdfyI7zmIODA+vXryc2NpYVK1bIjpMjDg4OREZGWv3Z\nszUWZ9WtbTmqOFs5FxcXxo8fz4kTJzh37hze3t5s3LjRqn9p2Yq2bduyceNGunbtypYtW2THeaxQ\noUJs3ryZUaNG8fXXX8uOkyOdO3cmMTHRUH+v2eXt7c21a9cMdwvev1Hd2palirONeOGFF/jkk09Y\nuXIlkZGRNG/e3GYmbLBmjRs35rPPPuPVV1/l448/lh3nsSpVqrB8+XI6duxolYtK2NnZMWXKFCZM\nmGC1cwDY29tTp04drGWmRNWtbVmqONuYgIAATpw4QXBwMAEBAbzxxhtWPTrXFtSrV49du3YxatQo\nFi9eLDvOY23btuWNN94gODjYKi+HBAYG4uLiwoYNG2RHyTFr6tpW3dqWpYqzDXJwcGDo0KGcOXOG\nhIQEKleuzNKlS632DMMWVKtWjS+//JJp06Yxc+ZM2XEeGzlyJFWqVKFfv35WdylE0zSioqKYOHEi\nycnJsuPkSL169Th8+LDsGFmiurUtSxVnG1a0aFEWL17MZ599xpIlS6hfv77V/CKwRRUqVGD//v0s\nW7aM8PBwQxRDTdP48MMP+eGHH3jrrbdkx8m25s2bU7x4catdEOPRmbMR/i1kRnVrW5YqzrmAj48P\nBw4cYMiQIYSEhNC3b1/DTTOZW5QsWZJ9+/axdetWhg0bZojeDBcXFzZt2sSCBQuIiYmRHSdbNE1j\n6tSpREZG8uDBA9lxsu2FF14AMPx1/9TUVO7du0e+fPlkR8k1VHHOJezs7OjVqxfnzp2jcOHCVK1a\nlXfffVfNMiZB0aJF+eKLLzh+/DivvPKKIbpkPT092bRpE6+++ionT56UHSdbGjZsiLe3N0uWLJEd\nJds0TbOK68537twhb9682Nvby46Sa6jinMvkz5+fWbNmsX//frZt20bNmjXZtWuX7Fi5ToECBdi+\nfTtXr16la9eu3L9/X3YkfH19ee+99wgKCuL333+XHSdboqKimDp1qlUObPPz8zN8cVZd2paninMu\n5e3tzfbt23nrrbd49dVX6dChA5cvX5YdK1fJkycPW7ZsITU1laCgIEMUlu7du9O5c2c6duxoVd3E\ndevWxc/Pj/nz58uOkm3WcOZ8+/Zt44/Uvn4dZsyAHj2gfXv964wZcOOG7GQ5oopzLqZpGsHBwZw5\nc4ZatWpRt25dIiIiSExMlB0t13B2dmb9+vUUK1aMli1bcvv2bdmRmDp1Kvny5WPYsGGyo2TLlClT\nmDlzJnfv3pUdJVt8fHw4duyYoec7N/SZc1wchIZC6dIwaRJ89BF89pn+NSICSpXSX4+Lk500W1Rx\nVnB1dWXChAkcP36cM2fO4O3tTXR0tFWMILUFDg4OLF++nJo1a9K0aVNuSP6kb29vz0cffcS+ffus\n6ky0atWqtGjRgvfee092lGwpWLAgnp6enD17VnaUf2XY4rxgAQQEQEwMJCXpj/QSE/VtMTH6fgsW\nyEiZI6o4K489Wvd32bJlTJo0iRYtWnDmzBnZsXIFOzs73n//fVq1aoW/vz9Xr16Vmid//vzExsYS\nGRlpqLnBMxMREcGcOXO4efOm7CjZYvSubUN2ay9YACNGQEICZHYiIYS+34gRVlOgVXFWntC0aVNO\nnDhBYGAgjRs3Zvjw4YbobrV1j24L6tu3L40aNeLSpUtS85QvX56PP/6Ybt26Sc+SVRUqVCAkJIRZ\ns2bJjpItRi/OhjtzjouDESM4m5BAU8AdqABsSrdLAjAIKJL2uj/8XaCtYMpUVZyVp3J0dOT111/n\n9OnT3L17l8qVK7N8+XJD3Jdr60aNGsXIkSPx9/eXPj96s2bNCA8PJzAwkDt37kjNklUTJkxg0aJF\nVnUvvyrO2TRtGskJCQQB7YCbwGKgB/Bd2i4D0rafTfv67qNjExNh2jTL5s0BVZyVZ/Lw8GDJkiXE\nxsayaNEiGjRoYOhfIrYiLCyMt99+m2bNmklfGGHw4MG89NJL9OjRwyo+nJUqVYoePXowzQp+AT9S\ns2ZNzp8/b9jBmIbq1r5+HbZt4xzwCzAcsAeaAi8Bq4HzQCx6wS6a9nrdR8cLAVu3Gn4UtyrOSpb4\n+vry9ddfExYWRlBQEK+88opVnZlYo+7du7No0SLatGnDvn37pOXQNI3333+f27dvEx4eLi1Hdowd\nO5ZVq1YZfuatR1xcXKhSpQonTpyQHeWpDHXmnLYO+dOuMgvgFHAYKA1MQu/Wrg5Ep99R0x6/j1Gp\n4qxkmZ2dHX369OHcuXMUKFCAatWq8d5776lZxswoKCiItWvX0qFDB7Zu3Soth5OTExs3bmTt2rWs\nXbtWWo6sKl68OAMGDCAqKkp2lCwzcte2oVakio+HpCQqAx7ATOAhsAPYi36t+Wf0Iu2OfnY9D+iN\n3sUN6F3bBp8JTxVnJdvc3d1555132LdvH//73/+oVasWu3fvlh3LZjVr1ozY2Fj69OkjdXnEokWL\nsnnzZl5//XXpXe1ZMWrUKKKjo7l48aLsKFli5OJsqBWp0ganOgIxwP+A4sA7QGegJOCa9no44AQ0\nBpqgF/DHbt2yVOIcUcVZyTFvb2927NhBVFQU/fv3p2PHjvz444+yY9mkBg0asHPnToYNG8ayZcuk\n5ahRowaLFy8mJCSEa9euScuRFYUKFeL1118nIiJCdpQsMXJxNlS3droz+BroZ8t/ANuBS0C9tO2Z\nKljQDOFMRxVn5blomkZISAhnzpyhRo0a1KlTh8jISMMObLFmNWvW5MsvvyQyMlLqRBshISEMGDCA\nkJAQkjJO+mAwb7zxBtu3b5c+6j0rvLy8uHHjBn/88YfsKE8wVLd2jRrg4gJAPJCE3pU9C7gG9EG/\nbaoUMA1IBg4AXwItH72HqytUr27B0NmnirNMNjQXrKurKxMnTuT48eOcPHmSKlWqsGnTJjXLmIlV\nqlSJ/fv3M3/+fCZPnizt7zc8PJxSpUoxYMAAQ/+M8+fPz8iRI5k0aZLsKJmys7PDx8eHOANOM2mo\nbu0+fR5/uxoogX7teTewE3BG79LeDGxFv+78KrAKqPzoQCH+8T6GJISQ8qhbt67ItY4cESIkRAgX\nF/2h/1PRH66u+raQEH0/K7Vr1y5RpUoV0aJFC3HmzBnZcWzOtWvXRPXq1cWbb74pUlNTpWS4d++e\nqF27tpg5c6aU9rPqr7/+EiVKlBDHjh2THSVTY8aMEREREbJj/ENqaqpwcHAQSUlJsqP8LSRECE37\n5+/OrD40TYjQUCmxgaMiizVSnTlbmg3PBZtes2bN+Oabb2jTpg3+/v7897//tZpJLKxB8eLF+fLL\nLzlw4AADBgyQsmhCnjx5iImJYfbs2VJHkmfGzc2NcePGWcVtYEa87pyQkICDgwPOzs6yo/xt7Fi9\nazonXF314w1OFWdLsvG5YDNydHTkjTfe4PTp0/z5559UrlyZFStWWMVEFtagUKFC7Ny5k4sXL9K9\ne3cpSzyWKlWKjRs30qdPH0Mv3PDqq69y5swZDhw4IDvKMz0qzsJAlwoM1aX9iK8vzJoFbm7ZO87N\nTT/Ox8c8uUwpq6fYpn7kum7tI0eEcHMTP4BoDaIAiGIgBoN4mNbdkgxiPIgSIPKCqAXiFgjh5iZE\nXJzsP8FzO3z4sKhXr56oX7++iLOBP49RJCYmivbt24u2bduKhIQEKRmWLl0qKlSoIG7evCml/axY\nunSpCAgIkHYZIKs8PT3FpUuXZMd47PTp06Jy5cqyYzzd/PlCuLmJlKx0Zbu56ftLhOrWNqBp0yAx\nkUHogxeuAd+g3wbwaFG+ScDXwEHgDvpgBxewmrlgM1OvXj0OHjzIgAEDaN++Pf379+f69euyY1k9\nFxcXoqOjyZ8/P61bt5Zy+aBfv360a9eOzp07k5ycbPH2s6JXr15cvXrV8Pfk+/n5Gapr21AjtTMK\nC0N8+SU78uQh1cnpya5uV1d9ZHdICOzdC2FhcnLmgCrOlpA2FyxC8AP6jfIu6DfOtwJOA7eA94AP\n0aed04BqaftZy1ywWWFnZ0ffvn05d+4c+fPnp2rVqsydO9ewv9CthaOjI6tXr8bLy4vmzZtLuR1n\n5syZ2NnZMWLECIu3nRUODg5ERkYyfvx4Q3UbZ2S0686G7NZO51ByMm+ULIn2008QGQk9e0K7dvrX\nyEi4cgWio62jKzsdVZwtId0crsOAdej35V0FtqEX6JOAA7ARvWhXAj5I/x5WMBdsdri7uzN79mz2\n7t1LbGwstWrVYs+ePbJjWTV7e3sWLlxIQEAAAQEBFp8kxMHBgXXr1rFt2zaWLl1q0bazqkuXLiQm\nJvLZZ5/JjvKvjFacDTUByVOsXr2anj17onl4wMiRsGoVbNmifx05EooWlR0xR1RxtoS0uWBBn0bu\nNJAffZo5HyAYfS7Y2+jLnf2AXqQj0O/bA6xiLticqFKlCjt37mTy5Mn07duXzp07c+XKFdmxrJam\nabz99tt07dqVRo0acfnyZYu2X7BgQWJjYxk7dixfffWVRdvOCjs7O6ZMmUJ4eLhhBybWrVuXEydO\nGKY3ycjd2g8ePGD9+vV0795ddhSTU8XZEtLmgk1Fn6EmFPgL+B29O3s0+lywABPTvq8BdEW/if4x\ng88Fm1OaphEaGsqZM2eoUqUKtWvXZsqUKYaffcqoNE1j/PjxDBs2DH9/f86dO2fR9r28vFi5ciWd\nOnUy5AetwMBAnJ2dpc5T/izu7u6UKlXKMLOaGblbe9u2bVStWpUyZcrIjmJyqjhbQtqnzpvAT8AQ\n9FlsCgN90Qvwo7lgtWe9j8Hngn1ebm5uREREcOzYMb755huqVKlCTEyMoa8PGtnQoUOZPHkyTZo0\nsfhShK1bt2bEiBEEBgby119/WbTtzGiaRlRUFBMnTjTM2WlGRuraNnK39qMubVukirMlpM0FWwQo\nCyxAn+/1T2AlUBMoDzQCpgL30Zc2+wRol/YWwgrmgjWVMmXKEB0dzeLFixk3bhytWrWy+NmfrejT\npw/z5s2jZcuWFr/H980336RWrVr07t3bcF3ILVq0oHjx4qxZs0Z2lKcyWnE2Yrf2rVu32LVrFx07\ndpQdxSxUcbaEdHO4fgp8DhQFKqAPAns37bW1wI/oZ9RtgSlAs7TX7icm8t6ff3LLRru2n6Z58+Z8\n++23tGrVioYNGzJixAg1y1gOdOjQgdWrVxMcHMzOnTszP8BENE1j4cKFXL16lSlTplis3ax4dPYc\nGRkpZfKWzBipOBu1W3vDhg28/PLLhsxmCqo4W4KHB7RuDZpGLfTVUW6hX3PegH7fM8D/oRfue+hL\nn7326HhN436zZnxz9Srly5dn2LBh/PDDDxb9I8ji6OjI8OHDOX36NDdv3qRy5cqsWrXKcGdiRtey\nZUs+/fRTunfvzqZNmyzWrouLC5s2bWLp0qVER0dbrN2saNSoEV5eXixZskR2lCfUqFGD77//3hCX\nBIzarW3LXdqAmiHMYtJmCMvRRO3pZgj7+eefxejRo0XhwoVF586dxRErXhwjJw4dOiR8fX1FgwYN\nxNGjR2XHsTrHjh0TxYsXFytXrrRou0ePHhVFihQR33zzjUXbzUxcXJzw9PSUNrPas/j5+Yl9+/bJ\njiHq168vvvrqK9kx/uHSpUuiaNGi4sGDB7KjZAtqhjADMtFcsP/3f//H9OnT+eGHH2jQoAEdO3ak\ncePGbNmyJVecTfr5+XHo0CH69+9Pu3btGDBgADdsYHIWS6lTpw5ffPEF48eP54MPPsj8ABOpW7cu\n8+bNIygoyFCzwvn4+ODn58f8+fMz39nCjNK1bcRu7TVr1tClSxccHR1lRzGfrFZxUz9y3ZnzI2lz\nwWa63FkW54J9+PChWLt2rahTp47w8vISixcvFomJiRb6w8h169YtMWzYMFGkSBExd+5c8fDhQ9mR\nrMalS5dEuXLlxFtvvWXRdsePHy8aNmwo7t+/b9F2n+XkyZPCw8ND3LlzR3aUf1i9erXo3Lmz7Bii\nRIkS4ueff5Yd47HU1FRRsWJFcfjwYdlRso1snDmr4ixDXJy+nqiLi75+89PWcw4NzdZiF6mpqWLP\nnj2ibdu2olixYmLy5Mnixo0bZvxDGMfJkydFkyZNRLVq1cSePXtkx7EaV69eFVWqVBGjR4+22GIQ\nKSkpIigoSPTv399QC1B069ZNTJ48WXaMfzh//rwoU6aM7BjC1dVV3L17V3aMxw4dOiQqVqxoqH8/\nWaWKs7W4fl2IGTOE6NlTiHbt9K8zZujbn8Pp06fFK6+8IgoUKCAGDRokLly4YKLAxpWamio2bNgg\nSpUqJTp37iyuXLkiO5JVuHHjhqhbt64YOHCgSElJsUibd+7cEdWqVRNz5861SHtZceHCBVG4cGHx\nxx9/yI7yWEpKiihQoID47bffpGW4f/++sLe3N1QhHDx4sOE+SGWVKs6KEEKIa9euifHjx4siRYqI\n0NBQ8fXXX8uOZHZ//fWXmDhxoihUqJCYMmVKrunifx63b98W/v7+onv37hYbYHPp0iVRrFgxsXPn\nTou0lxX9+/cXY8eOlR3jH1q0aCG2bNkirf3r16+LwoULS2s/o/v374siRYoYaknN7MhOcVYDwmxY\n8eLFiYqK4vLlyzRp0oTu3bvz0ksvsWnTJlJSUmTHMws3NzciIyM5evQox44do0qVKmzevFn/JKo8\nVf78+dm2bRs3b96kU6dOFpk2tWzZsqxbt47u3btz4cIFs7eXFRMmTGDRokX89ttvsqM8JntQmNEm\nIPn888+pXLkyZcuWlR3F7LJUnDVNa6Vp2nlN077XNG3Mv+zTWdO0M5qmndY07WPTxlSeR548eRgy\nZAgXLlxg+PDhTJ8+ncqVK7NgwQISEhJkxzOLsmXLsmnTJhYuXMiYMWNo3bo158+flx3LsNzc3IiJ\nicHJyYl27dpx7949s7cZEBBAZGQkQUFB3E6bf16mUqVK0b17d6YZaO102cXZaCO1bf7e5vQyO7UG\n7IGLQDnACfgWqJJhn4rACaBg2nOPzN5XdWvLk5qaKvbt2ycCAwNF0aJFxcSJE6Ve1zK3+/fvi1mz\nZonChQuLESNGiNu3b8uOZFjJycmiX79+okGDBuLmzZsWaTMsLEy0adNGJCcnW6S9Z7l27ZooWLCg\nYcYs/PLLL6JgwYLSrvnu3LlTNG3aVErbGd26dUvkz5/fYv8uzQETd2vXA74XQlwSQjxAX444KMM+\nrwIfCCFupRV849zIqDxB0zQaNWrE5s2b2b9/P7/++iteXl4MHDjQJs8unZyc+O9//8upU6e4ceMG\n3t7erF69OlfcF55d9vb2fPjhh/j5+dGkSROLdPHOmTOHhIQExo0bZ/a2MlO8eHEGDBhAVFSU7CgA\nlChRgrx583Lx4kUp7RupW3vDhg20aNGCgja+ANAjWSnO/4e+mNIjP6dtS68SUEnTtAOaph3SNK3V\n095I07QBmqYd1TTtqJo4whi8vLxYtGgR58+fp1ixYjRq1Ijg4GC++uorm7tOW7x4cVasWMHGjRuZ\nM2cODRs25Pjx47JjGY6dnR2zZ88mODgYf39/sy/76OjoyIYNG9iwYYMhFqIYOXIk0dHR0gpiRn5+\nftK6to3UrZ2rurTJWnF+2iqGGX9rO6B3bQcA/wGWaJr2xE9UCLFYCOEjhPApWrRodrMqZuTh4UFk\nZCSXL1+mZcuW9O3blwYNGrBx40abGzzWoEEDjhw5Qr9+/WjTpg2vvfYav//+u+xYhqJpGhEREbz2\n2mv4+/ubfdBWkSJFiI2NZfjw4Rw+fNisbWWmcOHCDB06lMjISKk5HpF53dko82pfvnyZs2fP0rp1\na9lRLCYrxfln4IV0z0sCvzxln81CiIdCiB+A8+jFWrEybm5uhIWFce7cOUaPHs27775LxYoVmTdv\nniEm4TcVOzs7+vfvz9mzZ3FxcaFKlSrMmzfPsOv7yvLmm28SHh5OQEAA8fHxZm2rWrVqLF26lA4d\nOnD16lWztpWZ4cOH8/nnn3PmzBmpOUB+cTZCt/ZHH31E586dcXJykh3FcjK7KI1+VnwJfSniRwPC\nqmbYpxWwMu37Iujd4IWf9b5qQJj1OHDggAgNDRVFihQR48ePF9euXZMdyeTi4+NFQECAqF69uvjy\nyy9lxzGcdevWCQ8PD3Ho0CGztzV16lTh4+MjfTGKt99+W3To0EFqBiGEuHv3rnBzc5OyyMPQoUPF\ne++9Z/F200tNTRVeXl7i4MGDUnOYAqYcECaESAaGANuBs8B6IcRpTdMma5oWmLbbduAPTdPOAHuA\nkUKIP0z2CUKR6sUXXyQ6OpqDBw9y69YtvL296d+/vyHOKkylevXqfPHFF0yYMIGePXvStWtXfvrp\np8wPzCW6dOnCsmXLaNeuHXv27DFrW2PHjqVChQr0799f6riHIUOG8PXXX0sfl5A3b17KlSvHyZMn\nLd62Ebq1jx49SmpqKn5+flJzWFqW7nMWQmwVQlQSQpQXQkxN2zZRCBGb9r0QQrwphKgihKguhFhn\nztCKHBUqVOCDDz7gwoULlC5dmqZNm9KuXTu+/PJLmxg8pmkanTp14uzZs1SqVIlatWoxdepUi0zK\nYQ3atm3Lhg0b6NKlC1u2bDFbO5qmsXTpUs6fP8+MGTPM1k5m3NzcGDduHBMmTJCW4RFZXdtG6NZe\nvXo1PXr0QNOeNvzJhmX1FNvUD9Wtbf0SEhLE4sWLhZeXl6hbt65Yu3atTa0MdfHiRREUFCTKly8v\nYmNjDTW/sEyHDx8WHh4e4uOPPzZrOz/99JPw9PQUsbGxZm3nWZKSkkSpUqXEgQMHpGUQQoiFCxeK\nvn37Wrxdf39/qYvJPHjwQHh4eIiLFy9Ky2BKqLm1FUtKSUkRsbGxwt/fX5QuXVq8++67hlt+73l8\n/vnnwsvLS7Rq1UqcP39edhxDOHnypPD09BSLFi0yazsHDx4URYoUEadOnTJrO8+yZMkSERAQIPXD\n2fHjx0XVqlUt3m6NGjXEiRMnLN7uI1u2bBEvvfSStPZNLTvFWc2trTw3Ozs72rdvz969e1m/fj0H\nDx6kbNmyjBkzRvqoW1No2bIl8fHxNGvWjBdffJHRo0dz9+5d2bGkqlatGnv37mXatGnMmjXLbO3U\nr1+fd955h6CgIP74Q84wlt69e3P16lV2794tpX3Q/74vX75s8X93sru1H3Vp50aqOCsmVa9ePT75\n5BPi4uJITEykevXq9OnTR8pgFlNycnJixIgRnDx5kl9//RVvb28++ugjm7jWnlMVKlRg//79LFmy\nhAkTJpjt76JXr16EhITQqVMnHj58aJY2nsXBwYHIyEjCw8Ol/bwdHR2pWbMmx44ds2i7MichuX37\nNtu3b6dz585S2pdNFWfFLMqWLcucOXP4/vvv8fLyomXLlrRq1Ypdu3ZZdUErUaIEK1euZP369cye\nPZtGjRpx4sQJ2bGkKVmyJPv27eOzzz5j2LBhZpsSdfr06Tg7O/Pmm2+a5f0z06VLF/766y8+++wz\nKe2D5QeFpaamcvfuXfLnz2+xNtPbuHEjTZs2pVChQlLal00VZ8WsChUqxNixY/nhhx/o0qULw4YN\no3bt2qxZs0bKWZCpvJZptUcAACAASURBVPjiixw5coTevXvTunVrwsLCpHW7yubh4cGePXs4duwY\nr7zyilkmcrG3t2ft2rXs3LmTxYsXm/z9M2NnZ8eUKVMIDw+XNie7pYvznTt3yJMnD/b29hZrM73c\nNl1nRqo4Kxbh7OxM3759OXXqFNOmTWP58uWUK1eOWbNmGWK5wJywt7fn1Vdf5ezZszg6OuLt7c38\n+fNz5SxjBQoUYMeOHVy9epWuXbty//59s7QRGxtLeHg4+/btM/n7ZyYoKAhnZ2c2btxo8bbB8sVZ\nZpf2jz/+yKlTp2jTpo2U9o1AFWfFojRNo3Xr1uzevZvNmzdz4sQJypUrx4gRI6x20o+CBQsyd+5c\ndu/ezfr16/Hx8ZFSPGTLkycPW7ZsITU1laCgILOsFV6pUiXWrFlDly5duHz5ssnf/1k0TSMqKoqJ\nEydK+QBWrlw5/vrrL65du2aR9mROQPLRRx/RqVMnnJ2dpbRvBKo4K9LUqVOHjz76iBMnTiCEoGbN\nmvTo0cNqr+FWr16dPXv2MG7cOHr06EG3bt34+eefZceyKGdnZ9avX4+HhwctW7Y0S6/Iyy+/zJgx\nYwgMDOTevXsmf/9nadGiBR4eHlJWz9I0zaJnz7JGagshcn2XNqjirBhAqVKleOedd7h06RI1a9ak\nffv2NG/enM8//9zqBo9pmkbnzp05e/Ys5cqVo1atWkybNs0s3bxG5eDgwIoVK6hZsyZNmzY1y4pf\nr7/+Or6+vvTq1cui14A1TWPq1KlERkby4MEDi7X7iCWLs6xu7WPHjvHw4UMaNGhg8baNRBVnxTAK\nFCjAyJEjuXTpEr1792b06NHUqFGDFStWWF1xy5MnD1FRURw+fJhDhw5RtWpVqSN9Lc3Ozo7333+f\nli1b4u/vb/L73TVNY/78+fz2229ERESY9L0z06hRI7y8vFi6dKlF2wXLFmdZ3dpr1qzJndN1ZpTV\n2UpM/VAzhCmZSU1NFTt27BAvv/yy8PT0FNOmTRM3b96UHStHtm3bJipVqiTatGkjvvvuO9lxLGr6\n9OmibNmyZpmC8ddffxWlSpUSn3zyicnf+1ni4uKEp6enxVfOun79unB3dxcpKSlmb2vOnDli8ODB\nZm8nvYcPH4pixYqJCxcuWLRdS0HNEKbYAk3TaNGiBdu3b2fr1q2cPXuW8uXL88Ybb1h8MNDzatWq\nFSdPniQgIIAGDRowZswYi18vlWX06NGMHDkSf39/Tp8+bdL3LlasGDExMQwePNiiq0f5+PhQr149\n5s+fb7E2AYoWLUqhQoW4cOGC2duS0a29Y8cOypUrR4UKFSzarhGp4qxYhZo1a7Jy5Uri4+Nxdnam\nbt26dO3alaNHj8qOlmVOTk6MHDmS+Ph4fvnlFypXrszHH39sddfVcyIsLIzp06fTrFkzk//Mateu\nzfz58wkODua3334z6Xs/y5QpU5gxY4bFp9S0VNe2jG7tbA0Eu34dZsyAHj2gfXv964wZcOOGeUNa\nSlZPsU39UN3ayvO4ffu2mD17tihVqpRo3Lix2LJli0W6+kzpq6++ErVr1xYNGzaUuriAJcXExIii\nRYuKvXv3mvy9J06cKF588UWRlJRk8vf+N926dRNTpkyxWHtCCPHOO++IIUOGmL2dfv36icWLF5u9\nnUdu374t3N3dxe+///7sHY8cESIkRAgXF/0Bfz9cXfVtISH6fgaDWpVKyS0ePHggPv74Y1G7dm1R\nuXJl8eGHH4rExETZsbIsOTlZLFq0SHh4eIiwsLDMfzHZgJ07d4oiRYqIrVu3mvR9U1JSREhIiOjb\nt6/FVpD67rvvROHChS06FmL//v2iXr16Zm+nQ4cOYv369WZv55Fly5aJ4ODgZ+80f74Qbm5CaNo/\ni3LGh6bp+82fb5nwWZSd4qy6tRWr5ujoyH/+8x+OHTvGBx98wKeffkqZMmWIioqyiuk07e3tGTBg\nAGf/n73zDovq+N74uyAoKEoVQRFQidhR7IolGhXFgi0q1iQWgprE2GssEUX9xoaG2Au22JNYYiVG\n7L3GCnaxoNLL7vv7Y1l+LLsLu7BL0fk8z31g7z1z7uyK+945c+bMrVswMjJClSpVsHz5ckil0vzu\nmsFo3bo19u7di4EDB+L333/Xm18jIyOsX78eFy5cwKJFi/TmNyvc3NzQpUsXzJs3L0/uB8jD+Nev\nXzf4Coa8DmtnG9JevhwYPRqIj5dLcFaQcrvRo+XtCiPaqri+DzFyFhiKa9eucdCgQbSysuLw4cN5\n7969/O6S1ly+fJnNmjVjrVq1+M8//+R3dwzKpUuXWKZMGa5atUqvfh8+fMgyZcrwwIEDevWricjI\nSFpbW/Ply5d5cj+SrFWrFs8aOGzr6enJM2fOGPQeCh49ekRra2vNUxJnz7I4oHQYARyeYbR8GGBl\ngGYAWwCMUFwzNyfPncuT95EdECNnwadM9erVsXr1aty4cQMWFhZo0KABevTogTNnzuR317KlVq1a\nOH78OMaPH48+ffrAz8/vo9gTWx0eHh44fvw4pk+frteRrouLC7Zt24Z+/frhzp07evOrifLly8PP\nzw+BgYEGv5eCvEgKy8ts7dDQUHTv3l1zuc7AQMRKJIgFEAvgJQAzAD3SLr8G0BXATABvAdQF8KWi\nbUICkIf/NnpDWxXX9yFGzoK8IiYmhosWLaKLiwubNm3K3bt3F4rksZiYGE6cOJE2NjYMDAzM00Sn\nvCQiIoKVKlXi9OnT9TpX/Ntvv7Fy5cqMjo7Wm09NPH/+nFZWVnz8+LHB70WSK1asYP/+/Q16D1tb\n2zyJBshkMlatWpUnTpxQb/DypUri11qArgBlaa9DADbKcD0WYDGAtxTnihUjo6IM/l6yA2LkLBD8\nPyVKlMDIkSNx9+5djBgxAj///DPc3d0REhKChISE/O6eRkqUKIGff/4Zp0+fxsmTJ1GjRg3s27cv\nv7uld5ydnXHixAls374dY8aMAbObT9SSwYMH44svvkDv3r0NPodfpkwZDB48GDNnzjTofRQYeuRM\nMs9qa1+6dAkJCQlo0qSJeoO1a1VOrQPQH4CihtgNALUyXC8OoGLaeQCARKLWT0FGiLPgk6FIkSLo\n2bMnzpw5g5UrV+Kvv/6Ci4sLpk+fjlcFeG1kpUqV8Mcff2DhwoX4/vvv4ePjg3v37uV3t/RKmTJl\ncPz4cZw4cQJDhw7Vm5j+8ssvSElJwbhx4/TiLyvGjh2LHTt24P79+wa/V9WqVfHkyRODbbeakJCA\nIkWK5MmuUBs2bMi6XOfVq0BiYvrLRwDCAAzIYBILIPNjRCkA6SvQExKAa9f01OO8QYiz4JNDIpGg\nWbNm2Lt3L8LCwvD06VN89tln8Pf3z5M5ypzSvn17XLt2DV5eXmjYsCEmTpz4UVUZs7a2xuHDh3Hv\n3j34+fkhJSUl1z6LFCmCbdu2Yffu3Vi3bp0eeqkZGxsbjBgxAtOnTzfofQD5+6pdu7bBivDkVaZ2\namoqNm/enHWWdqYHkPUAmgJwzXCuBIAPmZp9AGCR8UR0dC56mvcIcRZ80ri7u+O3337D7du3YWdn\nh6ZNm8LX1xcnT57UW3hVnxQtWhTjxo3D1atX8ejRI1SpUgWbN28ukH3NCRYWFti3bx/i4uLg6+ur\nl2kHa2tr7N27F2PGjMGpU6f00EvN/PDDDzhw4ABu3rxp0PsAhg1t51VI+9ChQ3BxcYGbm5tmo0z9\nWA/lUTMAVANwJcPrOAD3086nY2WVi57mPUKcBQLIazTPmDEDDx8+xBdffIEBAwagcePG2LFjR4Fc\nc+zo6IiNGzdi8+bNCAoKQvPmzXHlypXsGxYCihUrhp07d8LCwgLt27fXS3nMqlWrYvXq1ejevbtB\n99guWbIkRo8ejWnTphnsHgoMKc55lamtVbnOmjWBYsUAAOEAnuL/s7QV+AK4DmAHgEQAMwDUBOCu\nMDAzA2rU0Fe38wZtM8f0fYhsbUFBJjU1lTt27GDDhg1ZoUIFLl26lLGxsfndLbWkpqZy+fLltLOz\n47fffss3b97kd5f0QmpqKocMGcJ69erprXLanDlzWKdOHcbFxenFnzri4uLo4ODAixcvGuwepHw9\nt6Ojo0F879u3j23btjWIbwUfPnxgqVKl+OrVq6wNM2RrDwHYV0NVsENp65yLAWwO8GHG6yJbWyD4\nODA2NkbXrl1x6tQpbNiwAUeOHIGLiwumTJmSp5sraIOxsTGGDRuGW7duAQCqVKmCX3/9tUCO+HXB\n2NgYv/76K5o3b44WLVrg+fPnufY5duxYVKlSBV999ZXBpgLMzc0xYcIETJ482SD+FTg7OyMlJcUg\nkYC8CGvv3LkTzZs3h62tbdaGpUsD3t6ARIIQABs0mLUGcBtAAoDjAFwUFyQSoH17wM5OH93OM4Q4\nCwTZ0LhxY+zcuRPh4eF48+YN3N3dMXjw4HQxLCjY2NggODgYBw8exKZNm1C3bl38+++/+d2tXCGR\nSBAUFIQvv/wSzZo1y/VWoRKJBCtWrMCDBw8we/Zs/XRSDUOGDMH169cRHh5usHtIJBKDhbbzIqyt\n0w5UEybIQ9M5wcxM3r6QIcRZINASNzc3LFu2DHfu3IGTkxNatGiBjh07IiwsrEAlZHl4eCAsLAxj\nx45Fr1690LdvXzx79iy/u5VjJBIJJk+ejBEjRqBZs2a4fft2rvyZmZlh9+7dWL58Ofbs2aOnXipT\ntGhRTJ061eCjZ0OJs6GztZ88eYJLly7Bx8dHuwb16gHz5wPm5rrdyNxc3q5uXd07mc8IcRYIdMTO\nzg5Tp05FREQEOnbsiCFDhqB+/frYunUrUlNT87t7AOSC1rt3b9y+fRtOTk6oUaMG5s6da/DNEgzJ\nyJEjMWPGDLRs2RKXLl3KlS9HR0fs3LkT33zzDa4ZaP3rgAED8PjxYxw5csQg/gHDirMhw9qbNm1C\nt27dUCwt0Usr/P3B+fORaGQEmaY10Qokkv8XZn//3HU2v9B2clrfh0gIE3wsSKVS7tmzh15eXnR2\ndubChQv54cOH/O6WEnfu3GGHDh3o5uam960a85rff/+ddnZ2PHnyZK59bdy4ka6urtknJeWQ0NBQ\nNmjQwGBbWL5584YWFhZMTU3Vq99hw4YxODhYrz4VyGQyVq9ePUcbu+zbt4/dnJ0p7dJFnuRlZqac\nGKbYz7lr1wKz2UVGIPZzFgjyh9OnT7NHjx60sbHh+PHj+fTp0/zukhJ//vknK1WqRB8fn0K1W1dm\n9u/fT1tbW/7999+59jVu3Dg2b96cycnJeuiZMlKplNWrV+fevXv17ltBpUqVeOPGDb367NWrF0ND\nQ/XqU8GlS5fo4uKic3371NRUVq9enbt375afiIoig4LIfv1IHx/5z6CgApGVrQldxFmEtQUCPdKg\nQQNs27YNZ8+eRVxcHKpXr45Bgwbh+vXr+d01AECHDh1w/fp1NGnSBA0aNMCkSZMQFxeX393SmXbt\n2mHnzp3w8/PDrl27cuXr559/Tq+/rm+MjIwwc+ZMTJkyBTKZTO/+AcOEtg0Z1laU6zQy0k1+1q1b\nB0tLS3Tq1El+ws4OGDMGWL8e+OMP+c8xYwpdVrYmhDgLBAagQoUKWLx4Me7duwc3Nzd88cUX8Pb2\nxpEjR/I9eaxo0aIYP348rly5goiICFSpUgVbt27N937pipeXF/bv3w9/f39s2KBpgU32GBsbY9Om\nTQgLC8Py5cv12EM5nTt3homJCbZv365334BhxNlQ2dqpqanYtGkT+vbtq1O7+Ph4TJ06FfPmzdNc\ng/tjQ9shtr6PAhfWfvmSnDuX9POTh0j8/OSvC3CIRFB4SEhI4KpVq1ilShV6eHhw48aNBgmj5oR/\n/vmHtWrVYvPmzXnlypX87o7O3Lhxg+XKlcv1HOndu3dZunRpHj16VE89+38OHDjAypUrMyUlRe++\nw8PDqe/v0ypVqvD69et69UnKP4d69erp3O7nn39mjx499N6fvAZizlkHzp4lfX3lSQSZ9gxNTy7w\n9ZXbCQS5RCqV8q+//mLLli3p5OTE+fPn8/379/ndLaampnLZsmW0s7Pj8OHDC12VsQcPHrBChQqc\nPXt2rvwcPnyY9vb2vH//vp56Jkcmk9HLy4tr167Vq1+SjI+Pp7m5ORMSEvTm08HBwSB7U/v5+XHx\n4sU6tYmKiqKNjQ3v3r2r9/7kNUKctWXZMtLcnJRIqK4cXPohkcjtli3L7x4LPiLOnz/P3r1709ra\nmqNHj+ajR4/yu0t8/fo1/f39Wbp0aYaEhOg9C9iQPHnyhFWrVuW4ceNylR29ZMkSVqtWTe8Z92Fh\nYXRxcWFSUpJe/ZJknTp1eOrUKb35Mzc3Z0xMjN78kWRMTAxLlSrFKB2jkcOHD+eIESP02pf8Qoiz\nNiiEOStRznwIgRYYgIiICP7www+0srJi3759eenSpfzuEi9evMimTZuyTp06elmylFe8evWKnp6e\n9Pf31zkbWIFMJuPgwYPZqVOnHPvQRJs2bbjMAN8hw4YN46JFi/TiKykpicbGxnpf/rVu3Tr6+Pjo\n1ObOnTu0sbHRWdALKkKcs+Ps2XRh3gzQHaA5wAoA/8kkyD8BRFpR9XSBLoDr5wSFn+joaM6dO5eO\njo5s3bo1Dxw4YLD1sdogk8kYGhrKsmXLsl+/fnz27Fm+9UUX3r9/Ty8vL/bt2zfHc7xJSUn08vLi\nxIkT9dq3s2fP0tHRkfHx8Xr1u3r1avr5+enFV1RUFK2trfXiKyOtW7fm1q1bdWrTvXv3XE9VFCSE\nOGeHry8pkfBvgOUBngIoBfgk7VAI8z2A1QE6ZBRniUS+wF0gMBBJSUlct24da9SowRo1anDt2rUG\nCYVqy4cPHzhu3Dja2NgwKCgoX/uiLXFxcWzXrh07d+6c47nYqKgoOjs7c9OmTXrtW5cuXbhgwQK9\n+rx+/Trd3Nz04uvu3busUKGCXnwpePLkCa2srHR6KAkPD2e5cuUMuoNYXiPEOSsybD/WCODKLMLY\n7QD+BdA5ozgXoO3HBB83MpmMBw4cYOvWreno6Mg5c+YwOjo63/pz584dtm/fnpUrV+aBAwfyrR/a\nkpSUxB49erBVq1Y5nj+9fPkybW1teU6P0bKrV6+ydOnSep3TTk1NpYWFhV4S+c6dO8c6derooVf/\nT1BQEL/++mut7WUyGZs0acLVq1frtR/5jS7i/Omtc167FgAgBXAewCsAlQCUAzAc8u3GAOB3AKYA\n2qvzIZGk+xEIDIVEIkHbtm1x6NAh/PXXX7h+/ToqVKiAUaNGITIyMs/74+bmhr/++gvz589HQEAA\nOnfujAcPHuR5P7TF1NQUmzdvRvny5dGmTRu8e/dOZx+1atVCSEgIfH199bJlJQDUqFEDrVq1wqJF\ni/TiD5Cv1fb09MT58+dz7csQBUh02oEKwJ49e/Dhwwf0799fr/0oTHx64nz1KpCYiJcAUgBsB3AC\nwGUAlwDMAhALYCKAhZp8JCQABiqWLxCow8PDAxs2bMCVK1dgbGyMOnXqoE+fPrhw4UKe98XHxwc3\nbtxAw4YNUb9+fUyZMqXAVhkzNjbGypUrUb9+fbRo0QJRUVE6++jatSsGDx4MX19fJCYm6qVf06dP\nx8KFCxEdHa0Xf4D+ipHouwDJ1atX8f79e3h5eWlln5KSgnHjxiEoKAjGxsZ660dh49MT5/fvAQCK\nnUFHAHAAYAtgFIB9AKYB6AfANSs/evxPJRBoi5OTE+bNm4cHDx7A09MTXbp0weeff459+/YZrDyk\nOooWLYoJEybg8uXLuH//PqpUqYJt27bJ58oKGEZGRvjll1/QuXNneHl54fHjxzr7mDJlCsqXL48h\nQ4YgMTERmzZtylWf3Nzc0KVLF8yfPz9XfjKiL3HW93aRupbrXLlyJZycnNC2bVu99aFQom38W99H\nvs05+/mlzx2XA7guw1zydoAeAGsBtAFon3YYAbQCOCeD7VUPDx46dChf5wAFguTkZG7cuJEeHh6s\nWrUqV61axcTExDzvR1hYGGvWrMkWLVrw6tWreX5/bZk/fz6dnZ15584dndvGxsayWrVqdHZ2JgD+\n9ttvuepLREQEra2t+fLly1z5UfDo0SOWLl061xn+8+fP5/fff6+XPqWmptLR0ZG3bt3Syv7Dhw8s\nU6YML1y4oJf7FzQg5pyzoGZNIG0P0UEAlgCIAhANeRjbB8ARANchD3VfBuAIIARAQJqLVFNTRJQs\niZkzZ8LJyQmVK1dG3759sXjxYpw6dQoJCQkQCPICExMT+Pn54eLFi1i8eDF+//13uLi4YPbs2Xj7\n9m2e9aNZs2a4cOECevTogVatWmHkyJF6Ddnqix9//BGTJk1CixYtdN7H+c6dO3j79m36fH9AQABO\nnDiR4744OzujT58+CAwMzLGPjJQrVw5GRkZ49OhRrvzoM6x99OhRODo6wt3dXSv7+fPno3Xr1qhT\np45e7l+o0VbF9X0UhGztZID+AEuljZBHAExQk7WdVbZ2amoqr127xlWrVnHYsGGsU6cOzc3NWbt2\nbQ4dOpQrV67k1atXC1WlJUHh5tq1axw4cCCtrKw4YsQIvZeizI5Xr15x6NChtLe352+//VYg//Y3\nb97M0qVL8/Tp01q3uXTpEs3NzYm02gcAaGtry4iIiBz34/nz57S2ttZbqcxOnTpx27ZtufIxYsQI\nLly4UC/96devn9bFUZ49e0Zra+tcfZ4FHYilVNmQts5Zp+pgOqxzTkhI4KlTp7h48WL27duXlStX\nZokSJejl5cVRo0Zxy5YtvH//fr4WmBB8/Dx9+pTjx4+njY0Ne/TooZMQ6YMLFy6wcePG9PT0ZHh4\neJ7eWxv+/PNP2tra6rTRxfbt25XEGQBr1qyZq1KXY8eO5dChQ3PcPiOzZs3i6NGjc+WjX79+XLNm\nTa77EhsbS0tLS63D9oMHD8513ws6QpyzI0OFMJ2PHFYIi46O5uHDhzl79mz6+vqybNmytLGxobe3\nN6dOnco///xTb3NPAkFGPnz4wIULF9LZ2ZleXl7cs2eP3stSakImk3HDhg10dHRk//79+fz58zy5\nr7YcPXqUtra23Lt3r9Ztpk2bpiLQXbt2zfFn+vr1a1pbW+slwnHo0CE2a9YsVz46derEXbt25bov\nGzZsYIcOHbSyvXHjBu3s7Pj27dtc37cgI8RZGwpAbe2nT59y9+7dnDRpEr/44gtaWlrS2dmZ3bt3\nZ1BQEI8dO6b34vuCT5eUlBRu2bKFnp6e/OyzzxgSEqL3MpKa+PDhA8eMGUMbGxvOmzevQFUZO3Pm\nDO3t7bWuBCaVStmtWzcVgZ42bVqO+zBt2jT269cvx+1J8s2bN9yyZQtNTEzYtm3bHFcha9asGY8d\nO5arvpDyOuKbN2/WyrZjx456r5pWEBHirC0FbFcqmUzGO3fucOPGjfzuu+/YqFEjFi9enFWrVuXA\ngQMZHBzMc+fOFagvNkHhQyaT8fjx4/Tx8aG9vT2nT5/OV69e5cm9b9++zXbt2rFy5co8ePBgntxT\nG65evUpHR0eGhIRoZR8bG8tatWqpCHRO53vfvXtHOzs73rhxI0ftSXl97Yx98fb2zpGfmjVr8uLF\niznuBymfP7a0tNTq4e/48eN0cXHJl1UGeY0QZ104d04+h1ysmHz/5oyirNjPuWvXfNvsIjk5mRcv\nXmRISAi//vpr1qxZk+bm5qxfvz4DAgK4bt063rx5M8/ClIKPi5s3b/Kbb76hpaUl/f39c7TESFdk\nMhn37NnDChUqsHPnznmesKaJu3fv0sXFhfPmzdPKPiIignZ2dkqCaGZmlmNhmzNnDrt3756jtqS8\nvnbGvtjY2OQor8XZ2ZkPHjzIcT9I+XKsQYMGZWsnlUpZt25dhoaG5up+hQUhzjkhKooMCiL79SN9\nfOQ/g4IKZA3t2NhYnjhxggsWLGCvXr1YoUIFlixZki1btuS4ceO4fft2Pnr0SCScCbTm+fPnnDx5\nMm1tbenr65sn20QmJCRw1qxZtLa25pQpUwrEBgePHj1i5cqVOXny5PT/P1n9Pzpx4gRNTEyURNHJ\nyYkvXrzQ+d6xsbF0cHDIsbinpqayRIkSSn3JyYNPqVKlcj33W6tWLa0S7RTTLJ/K4EKI8yfI69ev\nuX//fs6YMYM+Pj4sXbo07e3t2bFjR86YMYMHDhzQS1F8wcdNbGwsly5dygoVKrBRo0bcsWOHwZdC\nPXr0iF9++SXLly/Pbdu25ftD5cuXL+nh4cGRI0dy586d9PHxyfLBYcWKFSrh7SZNmuQoTLt48WK2\nb9+eJHP0ubdo0UKpH9rO+SqQSqU0MjLK1b/51atX6eTklK3gJiYmskKFCjplyxd2hDgLKJPJGBkZ\nye3bt3Ps2LFs2bIlLSwsWLFiRfbq1Yv/+9//eOLEiQIxWhEUPFJTU7l9+3Y2aNCAFStWZHBwsMH/\nVo4dO8YaNWqwZcuWvHbtmkHvlR3R0dGsWrUqjYyMCIBeXl589+6dRvuRI0eqCPRXX32l84NGYmIi\ny5Yty759+9Ld3V3n7S7Hjh2r1IcffvhBp/bv3r2jhYWFTm0yM2bMGI4fPz5bu19++SX9QeRTQYiz\nQC1SqZQ3b97k2rVrGRAQwHr16tHMzIw1a9bkN998w5CQEF66dInJycn53VVBAUEmk/Hff/9lly5d\naGdnxylTpuQoZKstKSkpXLJkCW1tbTly5Mh8K4975swZlYIjderU0Zg4l5KSwtatW6sItK7FPIKC\ngmhmZpbeXtsCHgoyr8Nu0qSJTu0jIiLo5OSkU5uMKMp1ZpfYFh0dTTs7u3x/CMtrhDgLtCYxMZFn\nz57l0qVLOWDAAFapUoXFixdn48aN+f333zM0NJR3797N91CjIP/577//OGzYMFpaWnLw4MFa10vO\nCVFRURwyZAjt7e25YsWKPJ+TVIS2M4ttlSpV+OTJE7Vt3rx5w0qVKinZGxkZ6ZSV/v333yu1L126\nNGNjY7Vu/+jRDqu66QAAIABJREFUI5UENV0eti9fvszq1atrbZ+ZQ4cOabUX9Lhx43Ta3/ljQYiz\nIFe8f/+eR48e5dy5c9mtWzeWL1+eVlZWbNOmDSdNmsQ9e/bw2bNn+d1NQT4RFRXFn376iaVLl2bH\njh0ZFhZmsIe38+fPs1GjRqxbty5PnTplkHtoIjo6mo0bN1YRaFdXV42JVjdv3qSFhYWSvaWlpdZZ\n8C9fvmTx4sWV2gcGBmrdZ5lMxjJlyii1v3Tpktbtw8LC2LRpU63tM9O/f3/+8ssvWdpERkbS2tpa\n40POx4wQZ4HeefHiBf/44w9OmTKF7dq1o7W1NcuVK0dfX18GBgby8OHDWc7JCT4+4uPj+euvv9LN\nzY316tXj1q1bmZKSovf7SKVSrl+/ng4ODhwwYECeVhmLjY1VG652cHDg9evX1bb5888/KZFIlOzd\n3d21/v8xceJEpbZWVlY6hfc7deqk1F7btdskuWfPHvr4+GhtnxFFuc7spj369+/PSZMm5egehR0h\nzgKDI5PJeP/+fW7evJmjRo1i06ZNWaJECVauXJn9+vXj4sWLefr0aZ0TWgSFD6lUyt27d7Np06Z0\ncXHhokWLclVrWhPv37/n6NGjaWNjwwULFuRZbkRCQgK7dOmiItA2NjY8f/682jZz585Vsff29tYq\nC/rt27csVaqUUtupU6dq3d9Zs2YptdU2fPzdd9+xVatWrFatGufOncv3799rfU+SDA0NzbbwyaVL\nl2hvb6+z748FIc6CfCElJYVXrlzhypUrOWTIENauXZtmZmb09PTksGHDuHr1al67dq1A7lIk0A+n\nTp1i9+7daWNjwwkTJhhk+uPWrVts27Yt3d3d+ffff+vdvzpSUlLYr18/FcG1sLBgWFiYir1MJmPf\nvn1V7MeMGaPV/WbOnKnUrkSJElpXcfv777+V2taoUUOrdlZWVkrtdK0a165du2xLoLZp04ZLly7V\nye/HhBBnQYEhPj6e4eHhXLhwIf38/Ojm5kYLCws2a9aMo0eP5tatW/nw4UORcPaRce/ePQ4fPpxW\nVlYcNGiQxhBwTpHJZNy9ezddXV3p6+ub64pW2iCVSvntt9+qCG6xYsW4b98+FfuEhATWr19fxX7d\nunXZ3uvDhw+0tbVVaqftjk3R0dEqSWnZRTJkMln6sjHFoUtk4vnz57S0tMxyud3Bgwfp5ub2Sa8G\nEeIsKNC8ffuWf//9N3/++Wd27tyZDg4OtLW1Zfv27Tlt2jT+9ddfjCqAldkEuvP69WvOmjWLZcqU\nobe3N48cOaLXB7GEhATOnDmT1tbWnDp1qsHXYstkMo4fP15FcE1MTNTW1X769CkdHR2VbE1NTbVK\nbps/f77KQ4C2kYjKlSsrtVU3us/Ihw8flOzNzc21uo+C//3vfxw4cKDG66mpqaxVqxZ37Nihk9+P\nDb2LM4B2AP4DcA/A+Czsuqf949bNzqcQZ0FGnjx5wl27dnHChAls3bo1S5UqRRcXF/bo0YPz5s3j\n8ePHDTKPKcgbEhISuHLlSrq7u7N27doMDQ3V6wgqMjKSPXv2pLOzM7dv3864uDi2atWKe/fuNUhU\nJjAwUEWgjYyMuHr1ahXbs2fPsmjRokq2ZcqU4ePHj7O8R3x8vIqwBwQEaNW/zCH47OqFZ16C5ejo\nqNV9FNSuXZtHjhzReH3dunVs1KjRJx8h06s4AzAGcB9ABQCmAK4AqKrGzgLAPwBOC3EW5BapVMr/\n/vuPGzZs4IgRI9iwYUOam5uzWrVqHDRoEJctW8bz58+LHboKGVKplH/88QebN29OJycnLliwQK/b\noh49epTVq1enq6urUiLWf//9p7d7KAgODlYRaEB94ZHQ0FAVO09Pz2x3bVq2bJnKCP3hw4fZ9m3J\nkiVK7Xr06JGl/dWrV5Xsq1Spku09FFy/fp3lypXTuBY9Pj6eTk5O/Pfff7X2+bGib3FuBOBghtcT\nAExQY7cQgA+A40KcBYYgKSmJFy5c4PLly/nVV1+xevXqNDc3Z4MGDTh8+HCuX7+et2/f/mSK6Bd2\nzp49yy+//JLW1tYcO3as3ta93r17V2UzChMTE44dO1bv+6Nv2LCBxsbGKsI7Y8YMlVGiunB47969\nsxxNJiUl0cXFRamNNrs9nTlzRqmNs7NzlvYnTpxQsm/UqJFW75+UFxQZO3asxutz5syhr6+v1v4+\nZvQtzt0BrMzwuh+ApZlsagPYkfa7RnEGMATAeQDny5cvnycfhuDjJiYmhmFhYZw/fz579uxJV1dX\nlipViq1ateL48eO5c+dOPn78+JMPpxVkHj58yO+++45WVlbs378/r1y5kit/q1atUkluUhwODg7c\nuHGjXv8edu3aRVNTU5V7/fjjj0r3SU1NpY+Pj4rd7Nmzs/S/Zs0alfB5dpGAxMRElQeUrNYf//HH\nH0q22u4FLZVKWa5cOY1lOF+9ekUbGxuDRC4KI/oW5x5qxHlJhtdGaYLswmzEOeMhRs4CQxEVFcV9\n+/bxp59+YocOHWhnZ0cHBwd26tSJs2bN4sGDB8UOXQWQt2/fMjAwkA4ODmzTpg3//vvvHIvo5cuX\n6eXlpVagAXnN6ZxuzaiOQ4cOqdTiBsDBgwcrLR18//49q1atqmQjkUi4Z88ejb5TUlJUErx69eqV\nbZ/q1aun1OaPP/7QaLthwwad/ZPkkSNH6OHhofH6999/z2+//VYrX58CeRrWBlAKwGsAEWlHIoBn\n2Qm0EGdBXiGTyRgREcFt27ZxzJgxbN68OS0sLFipUiX26dOHv/zyC0+ePJnt/J8gb0hMTOSaNWtY\nrVo11qxZk+vXr89RboFMJuOmTZtYtmxZtQItkUg4bNgwvn79Wi/9PnnypErxEAD88ssvlZLf7t27\np7KmuESJElluArF161YVv9lFGAICApTsJ0+erNF26dKlSrbDhg3T6j0PHDiQCxYsUHvt/v37tLGx\nMehGKYUNfYtzEQAPALji/xPCqmVhL0bOggJPamoqr1+/zjVr1tDf359169almZkZPTw8OHjwYK5Y\nsYKXL182SDlKgXbIZDLu37+frVq1YtmyZRkUFJSjErExMTGcMGGC2tAzIC+PGRwcrJd/60uXLtHO\nzk7lHh06dFB6+Dty5IjKXLWrq6vGBwWpVMpatWop2Xfu3DnLvqxbt07Jvk2bNhptM1cV02bLx7i4\nOFpaWmpc3vXll19y5syZ2fr5lNCrOMv9oT2AO5BnbU9KOzcDQCc1tkKcBYWShIQEnj59mkuWLGG/\nfv3o7u7O4sWLs0mTJvzhhx+4adMm3rt3T8xf5wMXL16kn58fra2tOWrUKEZGRurs4+7du2rnfBVH\nrVq1sl0PrA23b99muXLlVPy3aNFCKSEt82gVAFu2bKlxidnevXtV7M+cOaOxH7du3VKytbS01Pi3\nO2bMGCVbbTbb2LRpE9u2bav22pkzZ+jo6KjTjlqfAnoXZ0McQpwFhYF3797xyJEjnDNnDrt27Uon\nJydaW1uzbdu2nDx5Mvfu3ZunGzF86jx69Ig//vgjra2t2adPH164cEFnH3/99ZfK1o4Zj969e2e7\nBjk7IiIi1N6jfv366fkOMpmMQ4YMUbHRNEcrk8nYoEEDJdsvvvhCYx+kUilLliypZK9pd6zBgwcr\n2S1fvjzb9+jt7c2NGzeq7Wfz5s25YsWKbH18aghxFggMyLNnz7hnzx5OnjyZbdq0oZWVFZ2cnNi1\na1fOmTOHR44c+WQL++cV796947x581iuXDl+/vnn3Ldvn04RjcTERM6ZM0dle0bFUbx4cc6ePZuJ\niYk57uPz589ZvXp1Fd/Vq1dPDwUnJSWxWbNmKjaaxPHw4cMqtsePH9fYh1atWinZqhNTkuzRo4eS\n3ebNm7N8by9evKClpaXakfHevXtZtWpVMSWkBiHOAkEeIpPJePfuXW7atInff/89mzRpwuLFi9Pd\n3Z39+/fnkiVLeObMmVx90QvUk5SUxA0bNrBWrVqsVq0aV69erdPn/OTJE/bp00fjKLpixYpZZjln\nx5s3b9TW165YsWJ6MZGoqCg6OzsrXS9SpIha0ZXJZGzRooWSbdOmTTU+mEyYMEHJduTIkWrt2rRp\no2SnrlY4Ka/b7eXlxW7durFnz54q11NSUlilShX++eefWn5CnxZCnAWCfCYlJYWXL1/mb7/9xsGD\nB7NWrVo0MzNj3bp16e/vzzVr1vD69etihy49IZPJeOjQIbZt25YODg6cPXs23759m35dKpVmWZzm\nxIkT9PDw0CjS7du31xgSzo4PHz6oCCoAlitXjrdv3yZJXrlyRWUUb2Njo3ZDj5MnT6r42r9/v9p7\n79q1S8muYcOGau0yP0CEh4ertfvtt9/SbYyNjTl8+HCV6y1atBB5GRoQ4iwQFEDi4uL477//8pdf\nfmHv3r1ZqVIlWlhYsHnz5hwzZgy3bdvGiIgI8cWWS65cucIBAwbQysqKI0eO5IMHD7h3715+9tln\nDAkJ0bhkLjU1lcuWLaO1tbVagTYxMeG4ceNyVOM9Pj5ebTKanZ0dL126RJLcuXOn2hC4uqpm7du3\nV7Lz9PRU+3fz9OlTJbuiRYuqXZb22WefKdndvHlT7fvIvHZ82rRp6ddiY2Pp6OjIc+fO6fz5fCoI\ncRYICglv3rzhwYMHOXPmTHbq1IllypShnZ0dO3TowOnTp3Pfvn0676srkPPkyROOGzeONjY2Ssub\n7OzsOH36dI2f6+vXr+nv76+xypijoyNDQ0N1fohKTk5mr169VPyVKlWKJ0+eJEnOmDFD5Xrnzp1V\nRv0XLlxQsdu5c6fa+2Ze533+/HkVG3t7eyWbp0+fqtg8fPhQ5Z53795Nvz59+nT27t1bp88k17x8\nSc6dS/r5kT4+8p9z55IFdFc7Ic4CQSFFJpPx8ePH3LFjB8ePH8/PP/+cJUuWpKurK7/88kvOnz+f\n//zzj1iiogNHjx5VK7LFihWjv7+/xnD1pUuX2LRpU42h7qZNm6aPerUlNTVVJTMakG/RqKiIljk5\nC1BfQKRbt25KNtWqVVM7TeLr66tkt2zZMhWbzLtmqdt6c+bMmUo2Getvv3jxgtbW1nmyrzZJ8uxZ\n0teXLFZMfgD/f5iZyc/5+srtChBCnAWCjwipVMpbt25x3bp1HD58OOvXr09zc3PWqFGDX331FX/9\n9VdeuHDhk97EPiuCg4NZpEgRjSIrkUjo6+ubPnrNiEwmY2hoqMrWjYrDyMiI/v7+OlUZk8lkHD16\ntIovU1NT7tq1i7Gxsaxdu7bK9S1btij5uXHjBiUSiZKNumzszNtbZt53OSEhQSV8nzkqIJPJVELf\nGUXe39+fP/zwg9afQa5Ytow0NyclEmVRznxIJHI7NQ8j+YUQZ4HgIycpKYnnzp3jsmXLOHDgQFar\nVo3m5uZs2LAhR44cyQ0bNvC///4TO3Sl8fjxY44ZM0Zl3W/mo2HDhty+fbvKCDQmJobjx49X2UxC\ncVhbW3PZsmVaJ/jJZDKVkSggT7Jav349IyMjWbp0aaVrZmZmKuu6M+/bXLFiRZWHtMyRg6pVqypd\nf/HihdJ1W1tblf5m3uXKxMQk/YHk9u3btLW11VsZ1CxRCHNWopz5KEACLcRZIPgE+fDhA48fP86g\noCD26NGDzs7OtLS0ZOvWrTlhwgTu2rVLb9syFlbev3/PBQsW0MnJKUuRrlixIpcuXaoyfXDnzh2V\nZKyMR61atfjPP/9o3Z+FCxeq9RMcHMyTJ0+qPAyUK1dOqejNvXv3VKICmYt/vH//XmmELZFIlNbh\n3759W6l9pUqVVPo5fPhwJZuMpUO7dOnCoKAgrd9zjjl7VndhzijQBSBRTYizQCAgSb58+ZJ//vkn\np02bRm9vb9ra2tLR0ZGdO3fmzz//zL///ltpydGnQnJyMkNDQ9WGjzOPiCdPnqyyecOff/6ZZZWx\n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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "nx.draw_networkx(many_parents_model, pos=nx.spring_layout(many_parents_model, k=0.10, iterations=10))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Define and associate CPDs for each node in the Bayesian Model" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "many_parents_model_cpds = {}\n", + "\n", + "for node in many_parents_model.nodes():\n", + " parents = many_parents_model.predecessors(node)\n", + " n_parents = len(parents)\n", + " cpd_values = get_tabular_OR_cpd_values(n_parents)\n", + " many_parents_model_cpds[node] = TabularCPD(variable=node,\n", + " variable_card=2,\n", + " values=cpd_values,\n", + " evidence=parents,\n", + " evidence_card=[2]*n_parents)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Associate the CPDs we just defined per node with the Bayesian model\n", + "many_parents_model.add_cpds(*many_parents_model_cpds.values())\n", + "many_parents_model.check_model()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Run inference using pgmpy's implementation of the belief propagation algorithm\n", + "\n", + "We don't run inference for this model because pgmpy's implementation of belief propagation does not converge. The code is provided below (if you run it, be prepared to have to force-quit or wait for it to time out). \n", + "```\n", + "# instantiate the inference model\n", + "infer_many_parents_model = BeliefPropagation(many_parents_model)\n", + "\n", + "query = infer_many_parents_model.query(variables=many_parents_model.nodes(), evidence={})\n", + "```" + ] + } + ], + "metadata": { + "anaconda-cloud": {}, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.4" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} -- cgit v1.2.3 From 6aa7175237982befb0b085696e466242d744976c Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Thu, 18 Jan 2018 21:51:59 -0800 Subject: bump version to 0.1.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index bcab45a..6e8bf73 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.3 +0.1.0 -- cgit v1.2.3 From c93c352b2f68a2bbcde2241e61d9fb52504a67a9 Mon Sep 17 00:00:00 2001 From: Cathy Yeh Date: Thu, 18 Jan 2018 21:56:32 -0800 Subject: explicitly require networkx >=2.0 in meta.yaml --- conda-build/meta.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda-build/meta.yaml b/conda-build/meta.yaml index b7d065b..2d2d8eb 100644 --- a/conda-build/meta.yaml +++ b/conda-build/meta.yaml @@ -23,7 +23,7 @@ requirements: - pyyaml - pytest - numpy - - networkx >=1.11 + - networkx >=2.0 anaconda_upload: True -- cgit v1.2.3