def __prepare_for_inference(self): # the following attributes are updated in decouple.py, which replicates # functional cpts and handles nodes with hard evidence, creating clones # of nodes in the process (clones are added to another 'decoupled' network) self._original = None # tbn node cloned by this one self._master = None # exactly one clone is declared as master self._clamped = False # whether tbn node has hard evidence # the following attributes with _cpt, _cpt1, _cpt2 are updated in cpt.y self._values_org = self.values # original node values before pruning self._card_org = self.card # original node cardinality before pruning self._values_idx = None # indices of unpruned values, if pruning happens # -process node and its cpts # -prune node values & parents and expand/prune cpts into tabular form tbn.cpt.set_cpts(self) # the following attributes will be updated next self._all01_cpt = None # whether cpt is 0/1 (not applicable for testing nodes) self._cpt_label = None # for saving to file (updated when processing cpts) # identify 0/1 cpts if self.testing: # selected cpt is not necessarily all zero-one even if cpt1 and cpt2 are self._all01_cpt = False else: self._all01_cpt = np.all( np.logical_or(self.cpt == 0, self.cpt == 1)) u.check( not (self.fixed_cpt and self._functional) or self._all01_cpt, f'node {self.name} is declared functional but its fixed cpt is not functional', f'specifying TBN node') # -pruning node values or parents changes the shape of cpt for node # -a set of tied cpts may end up having different shapes due to pruning # -we create refined ties between groups that continue to have the same shape """ this is not really proper and needs to be updated """ if self.cpt_tie is not None: # s = '.'.join([str(hash(n.values)) for n in self.family]) self._cpt_tie = f'{self.cpt_tie}__{self.shape()}' self.__set_cpt_labels() # we need to sort parents & family and also adjust the cpt accordingly # this must be done after processing cpts which may prune parents self.__sort() assert u.sorted(u.map('id', self.parents)) assert u.sorted(u.map('id', self.family))
def __shrink(self): fvars_ = lambda i: set([i.var]) if i.var.func else set() sep_size = lambda i: reduce(lambda x,y: x*y, u.map('card',i.sep),1) # heuristic for deciding which branch to sum from for i in self.nodes: # bottom-up if i.leaf(): i.size = 0 else: c1, c2 = i.left, i.right i.size = c1.size + c2.size + sep_size(c1) + sep_size(c2) # MUST process siblings simultaneously before processing their children # this is important to enforce the running intersection property def down_sum(i,p): if i.leaf(): return c1, c2 = i.left, i.right sum = c1.fvars & c2.fvars if sum: if c1.size < c2.size or (c1.size == c2.size and c1.id < c2.id): c1.sep -= sum else: c2.sep -= sum # propagate separator shrinking c2.sep &= c1.sep | i.sep c1.sep &= c2.sep | i.sep down_sum(c1,i) down_sum(c2,i) r, h = self.root, self.host r.sep -= r.fvars & fvars_(h) down_sum(r,h) # update clusters self.__set_cls()
def elm_order(self): pq = PQ(self.nodes) pi = [] width_l = 0 width_s = 0 size = 0 cliques = [] while True: n = pq.pop() if n is None: break # all nodes have been eliminated pi.append(n) clique = set(i.tbn_node for i in n.neighbors) clique.add(n.tbn_node) cliques.append(clique) l = 1 + len(n.neighbors) s = n.clique_size() width_l = max(width_l, l) # before eliminating node width_s = max(width_s, s) # before eliminating node size += s # before eliminating node affected_nodes = self.eliminate(n) # set affected_nodes = list(affected_nodes) # list affected_nodes.sort() # for deterministic behavior for n in affected_nodes: # n has new fillin count pq.add(n, replace=True) assert pq.empty() assert (self.count == len(pi)) pi = u.map('tbn_node', pi) return pi, cliques, width_l, width_s, size
def restructure_for_mulpro(dims1, dims2, vars): assert u.sorted(u.map('id', vars)) x, y, c, s = Dims.decompose_for_mulpro(dims1, dims2, vars) # dims1 has vard (c x s), dims2 has vars (c y s) # result of multiply-project will have vars (c x y) # s are variables that will be summed out (in dims1 and dims2 but not vars) # c are common to dims1, dims2 and vars # x are in dims1 but not in dims2 (must be im vars) # y are in dims2 but not in dims1 (must be in vars) # c, x, y, s are mutually disjoint; c, x, y can be empty # s cannot be empty otherwise dims1, dims2 and vars all have the # same variables and we would have used multiply instead of mulpro assert s # matmul requires c be first, s be last or before last (summed out) key = lambda vars: vars[0].id if vars else -1 less = lambda a, b: key(a) < key(b) dvars1 = [x, s] if less(x, s) else [s, x] dvars2 = [y, s] if less(y, s) else [s, y] if less(x, y): dvars, invert = [x, y], False # matmul(dims1,dims2) else: dvars, invert = [y, x], True # matmul(dims2,dims1) squeeze = not x and not y # result will two trivial dimensions if c: for dvars_ in (dvars1, dvars2, dvars): dvars_.insert(0, c) # matrix multiplication (matmul) requires that the dimensions of dvars1 # and dvars2 be ordered as follows: dvars1=(c,*,s) and dvars2=(c,s,+) # to yield vars=(c,*,+). the current ordering of dvars1 and dvars2 only # ensures that c is first so it may violate this pattern, but we can # instruct matmul to transpose the last two dimensions on the fly if needed s_index1 = -1 if not invert else -2 s_index2 = -2 if not invert else -1 transpose1 = dvars1[ s_index1] != s # transpose last two dimensions of dims1_ transpose2 = dvars2[ s_index2] != s # transpose last two dimensions of dims2_ assert not transpose1 or not transpose2 # at most one tensor will be transposed # add batch/trivial dimension to dvars1, dvars2 and dvars if needed # (when either dims1 or dims2 has batch) Dims.insert_batch(dims1, dims2, dvars1, dvars2, dvars) dvars1, dvars2, dvars = tuple(dvars1), tuple(dvars2), tuple(dvars) # returned dims object will have at most 3 dimensions (plus batch if any) # dims may have up to two trivial dimensions (when x and y are empty) dims1_, dims2_, dims = get_dims(dvars1), get_dims(dvars2), get_dims( dvars) assert dims1_.rank <= 4 and dims2_.rank <= 4 and dims.rank <= 4 return (((dims1_, transpose1), (dims2_, transpose2)), dims, invert, squeeze)
def __compile(self, net, inputs, output, hard_inputs, trainable, elm_method, elm_wait, profile): if profile: u.show('\n***PROFILER ON***') u.show(f'\nCompiling {self.network_type} into {self.circuit_type}') start_compile_time = time.time() # net1 and net2 have nodes corresponding to inputs and output (same names) net1 = net.copy_for_inference() self.tbn = net1 self.input_nodes = u.map(net1.node, inputs) self.output_node = net1.node(output) self.hard_input_nodes = u.map(net1.node, hard_inputs) # decouple net1 for more efficient compilation # net2 is only used to build jointree (duplicate functional cpts) net2, elm_order, _ = decouple.get(net1, self.hard_input_nodes, trainable, elm_method, elm_wait) # net2 may be equal to net1 (no decoupling) # if net2 != net1 (decoupling happened), then net2._decoupling_of = net1 # compile tbn into an ops_graph jt = jointree.Jointree(net2, elm_order, self.hard_input_nodes, trainable) ops_graph = og.OpsGraph(trainable, net.testing) # empty # inference will populate ops_graph with operations that construct tac_graph inference.trace(self.output_node, self.input_nodes, net1, jt, ops_graph) if u.verbose: ops_graph.print_stats() # construct tac_graph by executing operations of ops_graph self.ops_graph = ops_graph self.tac_graph = tg.TacGraph(ops_graph, profile) self.size = self.tac_graph.size self.rank = self.tac_graph.rank self.binary_rank = self.tac_graph.binary_rank self.parameter_count = self.tac_graph.parameter_count compile_time = time.time() - start_compile_time u.show(f'Compile Time: {compile_time:.3f} sec')
def simulate(self, size, evidence_type, *, hard_evidence=False): u.input_check(evidence_type is 'grid' or evidence_type is 'random', f'evidence type {evidence_type} not supported') cards = u.map('card', self.input_nodes) if evidence_type is 'grid': assert len(cards) == 2 and all(card == 2 for card in cards) assert not hard_evidence evidence = data.evd_grid(size) else: evidence = data.evd_random(size, cards, hard_evidence) marginals = self.tac_graph.evaluate(evidence) return (evidence, marginals)
def __sort(self): assert type(self.parents) is list and type(self.family) is list if u.sorted(u.map('id', self.family)): # already sorted self._parents = tuple(self.parents) self._family = tuple(self.family) return self._parents.sort() self._parents = tuple(self.parents) # save original order of nodes in family (needed for transposing cpt) original_order = [(n.id, i) for i, n in enumerate(self.family)] self.family.sort() self._family = tuple(self.family) # sort cpt to match sorted family original_order.sort() # by node id to match sorted family sorted_axes = [i for (_, i) in original_order] # new order of axes if self.testing: self._cpt1 = np.transpose(self.cpt1, sorted_axes) self._cpt2 = np.transpose(self.cpt2, sorted_axes) else: self._cpt = np.transpose(self.cpt, sorted_axes)
def sep_size(self, i): sep = self.sep(i) return reduce(lambda x, y: x * y, u.map('card', sep), 1)
def cls_size(self, i): cls = self.cls(i) return reduce(lambda x, y: x * y, u.map('card', cls), 1)