diff options
author | Cathy Yeh <cathy@driver.xyz> | 2017-11-20 17:05:37 -0800 |
---|---|---|
committer | Cathy Yeh <cathy@driver.xyz> | 2017-11-21 13:18:34 -0800 |
commit | d166e36eaf5803af035e444628c67701322b0eb6 (patch) | |
tree | 3e715d2ab34ce447222ccfa11bcde31065faae26 | |
parent | 71e384a741e52f94882b14062a3dc10e5f391533 (diff) | |
download | beliefs-d166e36eaf5803af035e444628c67701322b0eb6.tar.gz beliefs-d166e36eaf5803af035e444628c67701322b0eb6.tar.bz2 beliefs-d166e36eaf5803af035e444628c67701322b0eb6.zip |
refactor msg passing methods to BeliefUpdateNodeModel from BayesianModel
-rw-r--r-- | beliefs/factors/BernoulliOrCPD.py | 13 | ||||
-rw-r--r-- | beliefs/factors/CPD.py | 15 | ||||
-rw-r--r-- | beliefs/inference/belief_propagation.py | 7 | ||||
-rw-r--r-- | beliefs/models/BayesianModel.py | 76 | ||||
-rw-r--r-- | beliefs/models/BernoulliOrModel.py | 17 | ||||
-rw-r--r-- | beliefs/models/beliefupdate/BeliefUpdateNodeModel.py | 91 | ||||
-rw-r--r-- | beliefs/models/beliefupdate/BernoulliOrNode.py (renamed from beliefs/types/BernoulliOrNode.py) | 2 | ||||
-rw-r--r-- | beliefs/models/beliefupdate/Node.py (renamed from beliefs/types/Node.py) | 4 | ||||
-rw-r--r-- | beliefs/types/__init__.py | 0 | ||||
-rw-r--r-- | beliefs/utils/edges_helper.py | 2 | ||||
-rw-r--r-- | tests/test_belief_propagation.py | 13 |
11 files changed, 142 insertions, 98 deletions
diff --git a/beliefs/factors/BernoulliOrCPD.py b/beliefs/factors/BernoulliOrCPD.py index e4fcbf1..2c6a31e 100644 --- a/beliefs/factors/BernoulliOrCPD.py +++ b/beliefs/factors/BernoulliOrCPD.py @@ -10,17 +10,22 @@ class BernoulliOrCPD(TabularCPD): If at least one of the variable's parents is True, then the variable is True, and False otherwise. """ - def __init__(self, variable, parents=set()): + def __init__(self, variable, parents=[]): + """ + Args: + variable: int or string + parents: optional, list of int and/or strings + """ super().__init__(variable=variable, variable_card=2, parents=parents, parents_card=[2]*len(parents), - values=None) - self._values = None + values=[]) + self._values = [] @property def values(self): - if self._values is None: + if not any(self._values): self._values = self._build_kwise_values_array(len(self.variables)) self._values = self._values.reshape(self.cardinality) return self._values diff --git a/beliefs/factors/CPD.py b/beliefs/factors/CPD.py index 8de47b3..a286aaa 100644 --- a/beliefs/factors/CPD.py +++ b/beliefs/factors/CPD.py @@ -6,7 +6,7 @@ class TabularCPD: Defines the conditional probability table for a discrete variable whose parents are also discrete. - TODO: have this inherit from DiscreteFactor + TODO: have this inherit from DiscreteFactor implementing explicit factor methods """ def __init__(self, variable, variable_card, parents=[], parents_card=[], values=[]): @@ -22,9 +22,11 @@ class TabularCPD: self.parents = parents self.variables = [variable] + parents self.cardinality = [variable_card] + parents_card + self._values = np.array(values) - if values: - self.values = np.array(values) + @property + def values(self): + return self._values def get_values(self): """ @@ -34,3 +36,10 @@ class TabularCPD: return self.values.reshape(1, np.prod(self.cardinality)) else: 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) diff --git a/beliefs/inference/belief_propagation.py b/beliefs/inference/belief_propagation.py index 37aa437..02f5595 100644 --- a/beliefs/inference/belief_propagation.py +++ b/beliefs/inference/belief_propagation.py @@ -1,7 +1,8 @@ import numpy as np from collections import namedtuple -from beliefs.types.Node import InvalidLambdaMsgToParent +from beliefs.models.beliefupdate.Node import InvalidLambdaMsgToParent +from beliefs.models.beliefupdate.BeliefUpdateNodeModel import BeliefUpdateNodeModel from beliefs.utils.math_helper import is_kronecker_delta @@ -22,10 +23,12 @@ class BeliefPropagation: def __init__(self, model, inplace=True): """ Input: - model: an instance of BayesianModel class or subclass + 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") if inplace is False: self.model = model.copy() else: diff --git a/beliefs/models/BayesianModel.py b/beliefs/models/BayesianModel.py index 6257a57..b57f968 100644 --- a/beliefs/models/BayesianModel.py +++ b/beliefs/models/BayesianModel.py @@ -1,9 +1,7 @@ import copy -import numpy as np import networkx as nx from beliefs.models.DirectedGraph import DirectedGraph -from beliefs.utils.edges_helper import EdgesHelper from beliefs.utils.math_helper import is_kronecker_delta @@ -12,74 +10,30 @@ class BayesianModel(DirectedGraph): Bayesian model stores nodes and edges described by conditional probability distributions. """ - def __init__(self, edges, nodes_dict=None): + def __init__(self, edges=[], variables=[], cpds=[]): """ - Input: - edges: list of edge tuples of form ('parent', 'child') - nodes: (optional) dict - a dict key, value pair as {label_id: instance_of_node_class_or_subclass} - """ - if nodes_dict is not None: - super().__init__(edges, nodes_dict.keys()) - else: - super().__init__(edges) - self.nodes_dict = nodes_dict - - @classmethod - def from_node_class(cls, edges, node_class): - """Automatically create all nodes from the same node class + Base class for Bayesian model. Input: - edges: list of edge tuples of form ('parent', 'child') - node_class: (optional) the Node class or subclass from which to - create all the nodes from edges. - """ - nodes = cls.create_nodes(edges, node_class) - return cls.__init__(edges=edges, nodes=nodes) - - @staticmethod - def create_nodes(edges, node_class): - """Returns list of Node instances created from edges using - the default node_class""" - edges_helper = EdgesHelper(edges) - nodes = edges_helper.create_nodes_from_edges(node_class=node_class) - label_to_node = dict() - for node in nodes: - label_to_node[node.label_id] = node - return label_to_node - - def set_boundary_conditions(self): - """ - 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. - """ - for root in self.get_roots(): - self.nodes_dict[root].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]) - - @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. + edges: (optional) list of edges, + tuples of form ('parent', 'child') + variables: (optional) list of str or int + labels for variables + cpds: (optional) list of CPDs + TabularCPD class or subclass """ - for node in self.nodes_dict.values(): - if not node.is_fully_initialized: - return False - return True + super().__init__() + super().add_edges_from(edges) + super().add_nodes_from(variables) + self.cpds = cpds def copy(self): """ Returns a copy of the model. """ - copy_edges = list(self.edges()).copy() - copy_nodes = copy.deepcopy(self.nodes_dict) - copy_model = self.__class__(edges=copy_edges, nodes=copy_nodes) + 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 def get_variables_in_definite_state(self): diff --git a/beliefs/models/BernoulliOrModel.py b/beliefs/models/BernoulliOrModel.py deleted file mode 100644 index bf2b44c..0000000 --- a/beliefs/models/BernoulliOrModel.py +++ /dev/null @@ -1,17 +0,0 @@ -from beliefs.models.BayesianModel import BayesianModel -from beliefs.types.BernoulliOrNode import BernoulliOrNode - - -class BernoulliOrModel(BayesianModel): - """ - BernoulliOrModel stores node instances of BernoulliOrNodes (Bernoulli - variables associated with an OR conditional probability distribution). - """ - def __init__(self, edges, nodes=None): - """ - Input: - edges: an edge list, e.g. [(parent1, child1), (parent1, child2)] - """ - if nodes is None: - nodes = self.create_nodes(edges, node_class=BernoulliOrNode) - super().__init__(edges, nodes_dict=nodes) diff --git a/beliefs/models/beliefupdate/BeliefUpdateNodeModel.py b/beliefs/models/beliefupdate/BeliefUpdateNodeModel.py new file mode 100644 index 0000000..d74eaa7 --- /dev/null +++ b/beliefs/models/beliefupdate/BeliefUpdateNodeModel.py @@ -0,0 +1,91 @@ +import copy +import numpy as np + +from beliefs.models.BayesianModel import BayesianModel +from beliefs.utils.edges_helper import EdgesHelper + + +class BeliefUpdateNodeModel(BayesianModel): + """ + A Bayesian model storing nodes (e.g. Node or BernoulliOrNode) implementing properties + and methods for Pearl's belief update algorithm. + + ref: "Fusion, Propagation, and Structuring in Belief Networks" + Artificial Intelligence 29 (1986) 241-288 + + """ + def __init__(self, nodes_dict): + """ + Input: + 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()), + cpds=[node.cpd for node in nodes_dict.values()]) + + self.nodes_dict = nodes_dict + + @classmethod + def from_edges(cls, edges, node_class): + """Create nodes 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. + """ + edges_helper = EdgesHelper(edges) + nodes = edges_helper.create_nodes_from_edges(node_class) + nodes_dict = {node.label_id: node for node in nodes} + return cls(nodes_dict) + + @staticmethod + def _get_edges_from_nodes(nodes): + """ + 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 + """ + edges = set() + for node in nodes: + if node.parents: + edge_tuples = zip(node.parents, [node.label_id]*len(node.parents)) + edges.update(edge_tuples) + return list(edges) + + def set_boundary_conditions(self): + """ + 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. + """ + for root in self.get_roots(): + self.nodes_dict[root].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]) + + @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. + """ + for node in self.nodes_dict.values(): + if not node.is_fully_initialized: + return False + return True + + def copy(self): + """ + Returns a copy of the model. + """ + copy_nodes = copy.deepcopy(self.nodes_dict) + copy_model = self.__class__(nodes_dict=copy_nodes) + return copy_model diff --git a/beliefs/types/BernoulliOrNode.py b/beliefs/models/beliefupdate/BernoulliOrNode.py index ce497b9..3386275 100644 --- a/beliefs/types/BernoulliOrNode.py +++ b/beliefs/models/beliefupdate/BernoulliOrNode.py @@ -1,7 +1,7 @@ import numpy as np from functools import reduce -from beliefs.types.Node import ( +from beliefs.models.beliefupdate.Node import ( Node, MessageType, InvalidLambdaMsgToParent diff --git a/beliefs/types/Node.py b/beliefs/models/beliefupdate/Node.py index a496acf..daa2f14 100644 --- a/beliefs/types/Node.py +++ b/beliefs/models/beliefupdate/Node.py @@ -27,9 +27,9 @@ class Node: cardinality, cpd): """ - Input: + Args label_id: str - children: str + children: set of strings parents: set of strings cardinality: int, cardinality of the random variable the node represents cpd: an instance of a conditional probability distribution, diff --git a/beliefs/types/__init__.py b/beliefs/types/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/beliefs/types/__init__.py +++ /dev/null diff --git a/beliefs/utils/edges_helper.py b/beliefs/utils/edges_helper.py index c959a3b..130686c 100644 --- a/beliefs/utils/edges_helper.py +++ b/beliefs/utils/edges_helper.py @@ -1,6 +1,6 @@ from collections import defaultdict -from beliefs.types.Node import Node +from beliefs.models.beliefupdate.Node import Node from beliefs.factors.BernoulliOrCPD import BernoulliOrCPD diff --git a/tests/test_belief_propagation.py b/tests/test_belief_propagation.py index 24ee94b..264ddae 100644 --- a/tests/test_belief_propagation.py +++ b/tests/test_belief_propagation.py @@ -3,8 +3,8 @@ import pytest from pytest import approx from beliefs.inference.belief_propagation import BeliefPropagation, ConflictingEvidenceError -from beliefs.models.BernoulliOrModel import BernoulliOrModel -from beliefs.types.BernoulliOrNode import BernoulliOrNode +from beliefs.models.beliefupdate.BeliefUpdateNodeModel import BeliefUpdateNodeModel +from beliefs.models.beliefupdate.BernoulliOrNode import BernoulliOrNode @pytest.fixture(scope='module') @@ -37,24 +37,23 @@ def many_parents_edges(): @pytest.fixture(scope='function') def four_node_model(edges_four_nodes): - return BernoulliOrModel(edges_four_nodes) + return BeliefUpdateNodeModel.from_edges(edges_four_nodes, BernoulliOrNode) @pytest.fixture(scope='function') def simple_model(simple_edges): - return BernoulliOrModel(simple_edges) + return BeliefUpdateNodeModel.from_edges(simple_edges, BernoulliOrNode) @pytest.fixture(scope='function') def many_parents_model(many_parents_edges): - return BernoulliOrModel(many_parents_edges) + return BeliefUpdateNodeModel.from_edges(many_parents_edges, BernoulliOrNode) @pytest.fixture(scope='function') def one_node_model(): a_node = BernoulliOrNode(label_id='x', children=[], parents=[]) - # a_node = BernoulliOrNode(label_id='x', children=set(), parents=set()) - return BernoulliOrModel(edges=None, nodes={'x': a_node}) + return BeliefUpdateNodeModel(nodes_dict={'x': a_node}) def get_label_mapped_to_positive_belief(query_result): |