def __init__(self, table, vars, sort=False): if sort and not u.sorted(tuple(var.id for var in vars)): # used only for cpts as they may not be sorted table, vars = self.__sort(table, vars) assert type(table) is np.ndarray or (isinstance(table, Number) and not vars) assert u.sorted(tuple(var.id for var in vars)) assert isinstance(table, Number) or table.shape == tuple( var.card for var in vars) self.table = table # ndarray self.vars = vars # sequence of ordered vars self.rank = len(vars) # tvars are variables without batch if vars and vars[0].batch: self.tvars = vars[1:] self.has_batch = True else: self.tvars = vars self.has_bacth = False self.is_scalar = not self.vars or not self.tvars # no tbn nodes
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 __init__(self, table, vars): assert type(table) == np.ndarray and type(vars) == tuple assert all(not var.is_batch for var in vars) assert u.sorted(tuple(v.id for v in vars)) assert table.shape == tuple(var.card for var in vars) self.vars = vars self.table = table self.rank = len(self.vars)
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 __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 get(size,digits,testing,use_bk,tie_parameters,remove_common=False): assert size >= 7 assert len(digits) >= 2 assert all(d in (0,1,2,3,4,5,6,7,8,9) for d in digits) assert u.sorted(digits) # height is multiple of 7: 3 horizontal segments, 4 horizontal spaces # width is multiple of 4: 2 vertical segments, 2 vertical spaces h_inc = 7 w_inc = 4 net = TBN('digits') ### ### master START ### # nodes: # d digit # r upper-row (of bounding rectangle) # c left-col (of bounding rectangle) # h height (of bounding rectangle) # w width (of bounding rectangle) # t thickness (of lines) # values dvals = digits # for digits (root) rvals = range(0,size-h_inc+1) # for upper-row of digit (root) cvals = range(0,size-w_inc+1) # for left-column of digit (root) srange = range(1,size+1) # height, width, thickness (will be pruned) # constraints and functions # w is _constrained_ by c (length of segments) uniform = lambda values: [1./len(values)]*len(values) wct = lambda c, w, w_inc=w_inc, size=size: (w % w_inc) == 0 and w <= size-c # nodes dn = Node('d', values=dvals, parents=[], cpt=uniform(dvals)) rn = Node('r', values=rvals, parents=[], cpt=uniform(rvals)) cn = Node('c', values=cvals, parents=[], cpt=uniform(cvals)) wn = Node('w', values=srange, parents=[cn], cpt=wct, fixed_zeros=use_bk) for n in [dn,rn,cn,wn]: net.add(n) ### ### segments ### # seven segments: 0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G # 1,2,4,5 are vertical # 0,3,6 are horizontal # https://en.wikipedia.org/wiki/Seven-segment_display # each digit corresponds to some segments (activated segments) segments = (0,1,2,3,4,5,6) vsegments = (1,2,4,5) # vertical segments hsegments = (0,3,6) # horizontal segments # map digits to segments dsegments = {0:'012345', 1:'12', 2:'01346', 3:'01236', 4:'1256', 5:'02356', 6:'023456', 7:'012', 8:'0123456', 9:'012356'} # segments needed for the given digits segments = tuple(s for s in segments if any(str(s) in dsegments[d] for d in digits)) if remove_common: common = tuple(s for s in segments if all(str(s) in dsegments[d] for d in digits)) segments = tuple(s for s in segments if s not in common) u.show('Removing common segments',common) vsegments = tuple(s for s in vsegments if s in segments) hsegments = tuple(s for s in hsegments if s in segments) # nodes: # a segment is a rectangle # a segment s has upper-row srn[s], left-column scn[s], height shn[s], width swn[s], # whether segment is activated san[s], whether it will render in row i, sirn[s,i], # whether it will render in column i, sicn[s,i], and whether it will render in # pixel i,j, spn[s,i,j] # values irange = range(size) # for segment row and column srange = range(1,size+1) # for segment height and width # san[s] is whether segment s is activated given digit (True,False) # san[s] is a _function_ of node dn san = {} # maps segment to node for s in segments: fn = lambda d, s=s, dsegments=dsegments: str(s) in dsegments[d] node = Node(f's{s}', parents=[dn], cpt=fn, fixed_cpt=use_bk, functional=True) net.add(node) san[s] = node # srn[s] is upper-row for segment s # scn[s] is left-column for segment s # srn[s] and scn[s] are _functions_ of nodes rn and cn of master srn = {} # maps segment to node scn = {} # maps segment to node shifts = [(0,0), (0,3), (3,3), (6,0), (3,0), (0,0), (3,0)] # rshift: shift rn (digit) down to get srn (segment) # cshift: shift cn (digit) right to get scn (segment) fn = lambda r: r + 3 sr3n = Node(f'sr3', values=irange, parents=[rn], cpt=fn, fixed_cpt=use_bk, functional=True) fn = lambda r: r + 6 sr6n = Node(f'sr6', values=irange, parents=[rn], cpt=fn, fixed_cpt=use_bk, functional=True) fn = lambda c: c + 3 sc3n = Node(f'sc3', values=irange, parents=[cn], cpt=fn, fixed_cpt=use_bk, functional=True) for n in (sr3n,sr6n,sc3n): net.add(n) rsn = {0:rn, 3:sr3n, 6:sr6n} csn = {0:cn, 3:sc3n} for s in segments: rshift, cshift = shifts[s] srn[s] = rsn[rshift] scn[s] = csn[cshift] # sirn[s,i] is whether segment s will render in row i (True,False) # sicn[s,i] is whether segment s will render in col i (True,False) # sirn[s,i] is a _function_ of srn[s] and wn # sicn[s,i] is a _function_of scn[s] and wn sirn = {} # maps (segment,row) to node sicn = {} # maps (segment,col) to node for s in segments: for i in irange: name = f'in_r{s}_{i}' if s in vsegments: # vertical segment pa = [srn[s],wn] fn = lambda r, h, i=i: r <= i and i < r+h else: pa = [srn[s]] fn = lambda r, i=i: r == i node = Node(name, parents=pa, cpt=fn, fixed_cpt=use_bk, functional=True) net.add(node) sirn[(s,i)] = node name = f'in_c{s}_{i}' if s not in vsegments: # horizontal pa = [scn[s],wn] fn = lambda c, w, i=i: c <= i and i < c+w else: pa = [scn[s]] fn = lambda c, i=i: c == i node = Node(name, parents=pa, cpt=fn, fixed_cpt=use_bk, functional=True) net.add(node) sicn[s,i] = node # spn[s,i,j] is whether segment s will render in pixel i,j (True,False) # spn[s,i,j] is a _function_ of san[s] and sirn[s,i] and sicn[s,j] spn = {} # maps (segment,row,col) to node fn = lambda a, r, c: a and r and c for s in segments: for i in irange: for j in irange: name = f'p{s}_{i}_{j}' pa = [san[s],sirn[(s,i)],sicn[(s,j)]] node = Node(name, parents=pa, cpt=fn, fixed_cpt=use_bk, functional=True) net.add(node) spn[(s,i,j)] = node ### ### master END ### # image pixels: whether pixel i,j will render in image (iff some segment renders) output = dn.name inputs = [] fn = lambda *inputs: any(inputs) # or-gate tie = 'pixel' if tie_parameters else None for i in irange: for j in irange: name = f'p_{i}_{j}' pa = [spn[(s,i,j)] for s in segments] tie = tie if not testing else None pn = Node(name, parents=pa, cpt=fn, testing=testing, cpt_tie=tie) net.add(pn) inputs.append(name) return (net, inputs, output)