def generate_weights(self, initializer=tf.initializers.constant(1.0), trainable=True, input_sizes=None, log=False, name=None): """Generate a weights node matching this sum node and connect it to this sum. The function calculates the number of weights based on the number of input values of this sum. Therefore, weights should be generated once all inputs are added to this node. Args: initializer: Initial value of the weights. trainable (bool): See :class:`~libspn.Weights`. input_sizes (list of int): Pre-computed sizes of each input of this node. If given, this function will not traverse the graph to discover the sizes. log (bool): If "True", the weights are represented in log space. name (str): Name of the weighs node. If ``None`` use the name of the sum + ``_Weights``. Return: Weights: Generated weights node. """ if not self._values: raise StructureError("%s is missing input values" % self) if name is None: name = self._name + "_Weights" # Set sum node sizes to inferred _sum_input_sizes sum_input_sizes = self._sum_sizes max_size = self._max_sum_size # Mask is used to select the indices to assign the value to, since the weights tensor can # be larger than the total number of weights being modeled due to padding mask = self._build_mask().reshape((-1, )) # Generate weights weights = Weights(initializer=initializer, num_weights=max_size, num_sums=len(sum_input_sizes), log=log, trainable=trainable, mask=mask.tolist(), name=name) self.set_weights(weights) return weights
def generate_permutations(self, factors): if not factors: raise StructureError( "{}: factors needs to be a non-empty sequence.") num_input_scopes = self._get_num_input_scopes() factor_cumprod = np.cumprod(factors) factor_prod = factor_cumprod[-1] if factor_prod < num_input_scopes: raise StructureError( "{}: not enough factors to cover all variables ({} vs. {}).". format(self, factor_prod, num_input_scopes)) for i, fc in enumerate(factor_cumprod[:-1]): if fc >= num_input_scopes: raise StructureError( "{}: too many factors, taking out the bottom {} products still " "results in {} factors while {} are needed.".format( self, len(factors) - i - 1, fc, num_input_scopes)) # Now we generate the random index permutations perms = [ np.random.permutation(num_input_scopes).astype(int).tolist() for _ in range(self._num_decomps) ] num_m1 = factor_prod - num_input_scopes if num_m1 > 0: # e.g. num_m1 == 2 and factor_prod = 32. Then rate_m1 is 16, so once every 16 values # we should leave a variable slot empty rate_m1 = int(np.floor(factor_prod / num_m1)) for p in perms: for i in range(num_m1): p.insert(i * rate_m1, -1) self._permutations = perms = np.asarray(perms) return perms
def _compute_valid(self, *value_scopes): if not self._values: raise StructureError("%s is missing input values." % self) value_scopes_ = self._gather_input_scopes(*value_scopes) # If already invalid, return None if any(s is None for s in value_scopes_): return None # Check product decomposability flat_value_scopes = list(chain.from_iterable(value_scopes_)) for s1, s2 in combinations(flat_value_scopes, 2): if s1 & s2: self.__info( "%s is not decomposable with input value scopes %s", self, flat_value_scopes) return None return self._compute_scope(*value_scopes)
def _compute_log_value(self, *input_tensors): # Check inputs if not self._inputs: raise StructureError("%s is missing inputs." % self) @tf.custom_gradient def value_gradient(*input_tensors): def gradient(gradients): scattered_grads = self._compute_log_mpe_path( gradients, *input_tensors) return [sg for sg in scattered_grads if sg is not None] gathered_inputs = self._gather_input_tensors(*input_tensors) # Concatenate inputs return tf.concat(gathered_inputs, 1), gradient return value_gradient(*input_tensors)
def generate_weights(self, initializer=tf.initializers.constant(1.0), trainable=True, input_sizes=None, log=False, name=None): """Generate a weights node matching this sum node and connect it to this sum. The function calculates the number of weights based on the number of input values of this sum. Therefore, weights should be generated once all inputs are added to this node. Args: initializer: Initial value of the weights. trainable (bool): See :class:`~libspn.Weights`. input_sizes (list of int): Pre-computed sizes of each input of this node. If given, this function will not traverse the graph to discover the sizes. log (bool): If "True", the weights are represented in log space. name (str): Name of the weighs node. If ``None`` use the name of the sum + ``_Weights``. Return: Weights: Generated weights node. """ if not self._values: raise StructureError("%s is missing input values" % self) if name is None: name = self._name + "_Weights" # Count all input values if input_sizes: num_values = sum( input_sizes[2:]) # Skip latent_indicators, weights else: num_values = max(self._sum_sizes) # Generate weights weights = Weights(initializer=initializer, num_weights=num_values, num_sums=self._num_sums, log=log, trainable=trainable, name=name) self.set_weights(weights) return weights
def generate_weights(self, initializer=tf.initializers.constant(1.0), trainable=True, log=False, name=None, input_sizes=None): """Generate a weights node matching this sum node and connect it to this sum. The function calculates the number of weights based on the number of input values of this sum. Therefore, weights should be generated once all inputs are added to this node. Args: init_value: Initial value of the weights. For possible values, see :meth:`~libspn.utils.broadcast_value`. trainable (bool): See :class:`~libspn.Weights`. input_sizes (list of int): Pre-computed sizes of each input of this node. If given, this function will not traverse the graph to discover the sizes. log (bool): If "True", the weights are represented in log space. name (str): Name of the weighs node. If ``None`` use the name of the sum + ``_Weights``. Return: Weights: Generated weights node. """ if not self._values: raise StructureError("%s is missing input values" % self) if name is None: name = self._name + "_Weights" # Count all input values num_inputs = self.child.dim_nodes # Generate weights weights = BlockWeights(num_inputs=num_inputs, num_outputs=self._num_sums, num_decomps=self.dim_decomps, num_scopes=self.dim_scope, name=name, trainable=trainable, in_logspace=log, initializer=initializer) self.set_weights(weights) return weights
def _compute_log_mpe_path(self, counts, *value_values, use_unweighted=False, sample=False, sample_prob=None): # Check inputs if not self._values: raise StructureError("%s is missing input values." % self) # For each unique (input, index) pair in the values list, collect counts # index of all counts for which the pair is a child of gather_counts_indices, unique_inputs = self._collect_count_indices_per_input() if self._num_prods > 1: # Gather columns from the counts tensor, per unique (input, index) pair reducible_values = utils.gather_cols_3d(counts, gather_counts_indices) # Sum gathered counts together per unique (input, index) pair summed_counts = tf.reduce_sum(reducible_values, axis=-1) else: # Calculate total inputs size inputs_size = sum([v_input.get_size(v_value) for v_input, v_value in zip(self._values, value_values)]) # Tile counts only if input is larger than 1 summed_counts = (tf.tile(counts, [1, inputs_size]) if inputs_size > 1 else counts) # For each unique input in the values list, calculate the number of # unique indices unique_inp_sizes = [len(v) for v in unique_inputs.values()] # Split the summed-counts tensor per unique input, based on input-sizes unique_input_counts = tf.split(summed_counts, unique_inp_sizes, axis=-1) \ if len(unique_inp_sizes) > 1 else [summed_counts] # Scatter each unique-counts tensor to the respective input, only once # per unique input in the values list scattered_counts = [None] * len(self._values) for (node, inds), cnts in zip(unique_inputs.items(), unique_input_counts): for i, (inp, val) in enumerate(zip(self._values, value_values)): if inp.node == node: scattered_counts[i] = utils.scatter_cols( cnts, inds, int(val.get_shape()[0 if val.get_shape().ndims == 1 else 1])) break return scattered_counts
def _compute_value_common(self, *value_tensors, padding_value=0.0): """Common actions when computing value.""" # Check inputs if not self._values: raise StructureError("%s is missing input values." % self) # Prepare values if self._num_prods > 1: indices, value_tensor = self._combine_values_and_indices(value_tensors) # Create a 3D tensor with dimensions [batch, num-prods, max-prod-input-sizes] # The last axis will have zeros or ones (for log or non-log) when the # prod-input-size < max-prod-input-sizes reducible_values = utils.gather_cols_3d(value_tensor, indices, pad_elem=padding_value) return reducible_values else: # Gather input tensors value_tensors = self._gather_input_tensors(*value_tensors) return tf.concat(value_tensors, 1)
def assign(self, value): """Return a TF operation assigning values to the weights. Args: value: The value to assign to the weights. Returns: Tensor: The assignment operation. """ if self._log: raise StructureError( "Trying to assign non-log values to log-weights.") value = tf.where(tf.is_nan(value), tf.ones_like(value) * 0.01, value) if self._mask and not all(self._mask): # Only perform masking if mask is given and mask contains any 'False' value *= tf.cast(tf.reshape(self._mask, value.shape), dtype=conf.dtype) value = value / tf.reduce_sum(value, axis=-1, keepdims=True) return tf.assign(self._variable, value)
def _get_flat_value_scopes(self, weight_scopes, ivs_scopes, *value_scopes): """Get a flat representation of the value scopes per sum. Args: weight_scopes (list): A list of ``Scope``s corresponding to the weights. ivs_scopes (list): A list of ``Scope``s corresponding to the IVs. value_scopes (tuple): A ``tuple`` of ``list``s of ``Scope``s corresponding to the scope lists of the children of this node. Returns: A tuple of flat value scopes corresponding to this node's output. The IVs scopes and the value scopes. """ if not self._values: raise StructureError("%s is missing input values" % self) _, ivs_scopes, *value_scopes = self._gather_input_scopes( weight_scopes, ivs_scopes, *value_scopes) return list( chain.from_iterable(value_scopes)), ivs_scopes, value_scopes
def update_log(self, value): """Return a TF operation adding the log-values to the log-weights. Args: value: The log-value to be added to the log-weights. Returns: Tensor: The assignment operation. """ if not self._log: raise StructureError( "Trying to update non-log weights with log values.") if self._mask and not all(self._mask): # Only perform masking if mask is given and mask contains any 'False' value += tf.log( tf.cast(tf.reshape(self._mask, value.shape), dtype=conf.dtype)) # w_ij: w_ij + Δw_ij update_value = self._variable + value normalized_value = tf.nn.log_softmax(update_value, axis=-1) return tf.assign(self._variable, normalized_value)
def _compute_log_value(self, *value_tensors): if self._permutations is None: raise StructureError("First need to determine permutations") # [batch, scope, node] child, = value_tensors dim_scope_in = self.child.num_vars dim_nodes_in = self.child.num_vals if isinstance( self.child, IndicatorLeaf) else self.child.num_components zero_padded = tf.concat([ tf.zeros([1, tf.shape(child)[0], dim_nodes_in]), tf.transpose(tf.reshape(child, [-1, dim_scope_in, dim_nodes_in]), (1, 0, 2)) ], axis=0) gather_indices = self._permutations + 1 permuted = self._gather_op = tf.gather(zero_padded, np.transpose(gather_indices)) self._zero_padded_shape = tf.shape(zero_padded) return permuted
def _compute_valid(self, *value_scopes): if not self._values: raise StructureError("%s is missing input values." % self) value_scopes_ = self._gather_input_scopes(*value_scopes) # If already invalid, return None if any(s is None for s in value_scopes_): return None # Check product decomposability flat_value_scopes = list(chain.from_iterable(value_scopes_)) # Divide gathered and flattened value scopes into sublists, one per # modeled product op. prod_input_sizes = np.cumsum(np.array(self._prod_input_sizes)).tolist() prod_input_sizes.insert(0, 0) value_scopes_lists = [flat_value_scopes[start:stop] for start, stop in zip(prod_input_sizes[:-1], prod_input_sizes[1:])] for scopes in value_scopes_lists: for s1, s2 in combinations(scopes, 2): if s1 & s2: ProductsLayer.info("%s is not decomposable", self) return None return self._compute_scope(*value_scopes)
def learn(self, loss=None, optimizer=None, post_gradient_ops=True, name="LearnGD"): """Assemble TF operations performing GD learning of the SPN. This includes setting up the loss function (with regularization), setting up the optimizer and setting up post gradient-update ops. Args: loss (Tensor): The operation corresponding to the loss to minimize. optimizer (tf.train.Optimizer): A TensorFlow optimizer to use for minimizing the loss. post_gradient_ops (bool): Whether to use post-gradient ops such as normalization. Returns: A tuple of grouped update Ops and a loss Op. """ if self._learning_task_type == LearningTaskType.SUPERVISED and self._root.latent_indicators is None: raise StructureError( "{}: the SPN rooted at {} does not have a latent IndicatorLeaf node, so cannot " "setup conditional class probabilities.".format( self._name, self._root)) # If a loss function is not provided, define the loss function based # on learning-type and learning-method with tf.name_scope(name): with tf.name_scope("Loss"): if loss is None: if self._learning_method == LearningMethodType.GENERATIVE: loss = self.negative_log_likelihood() else: loss = self.cross_entropy_loss() # Assemble TF ops for optimizing and weights normalization with tf.name_scope("ParameterUpdate"): minimize = optimizer.minimize(loss=loss) if post_gradient_ops: return self.post_gradient_update(minimize), loss else: return minimize, loss
def _compute_mpe_path(self, counts, *value_values, add_random=False, use_unweighted=False): # Check inputs if not self._values: raise StructureError("%s is missing input values." % self) def process_input(v_input, v_value): input_size = v_input.get_size(v_value) # Tile the counts if input is larger than 1 return (tf.tile(counts, [1, input_size]) if input_size > 1 else counts) # For each input, pass counts to all elements selected by indices value_counts = [(process_input(v_input, v_value), v_value) for v_input, v_value in zip(self._values, value_values) ] # TODO: Scatter to input tensors can be merged with tiling to reduce # the amount of operations. return self._scatter_to_input_tensors(*value_counts)
def create_products(self, input_sizes=None, num_inputs=None): """Based on the number and size of inputs connected to this node, model products by permuting over the inputs. """ if not self._values: raise StructureError("%s is missing input values." % self) self._input_sizes = input_sizes if input_sizes is not None \ else list(self.get_input_sizes()) self._num_inputs = num_inputs if num_inputs is not None \ else len(self._input_sizes) # Calculate number of products this node would model. if self._num_inputs == 1: self._num_prods = 1 else: self._num_prods = int(np.prod(self._input_sizes)) # Create indices by permuting over the input space, such that inputs # for the products can be generated by gathering from concatenated # input values. self._permuted_indices = self.permute_indices(self._input_sizes)
def _compute_valid(self, *value_scopes): if not self._values: raise StructureError("%s is missing input values." % self) value_scopes_ = self._gather_input_scopes(*value_scopes) # If already invalid, return None if any(s is None for s in value_scopes_): return None if self._num_prods == 1: for s1, s2 in combinations(chain(*value_scopes_), 2): if s1 & s2: PermuteProducts.info( "%s is not decomposable with input value " "scopes %s", self, value_scopes_[:10]) return None # Check product decomposability for perm_val_scope in product(*value_scopes_): for s1, s2 in combinations(perm_val_scope, 2): if s1 & s2: PermuteProducts.info("%s is not decomposable", self) return None return self._compute_scope(*value_scopes)
def _compute_valid(self, weight_scopes, latent_indicators_scopes, *value_scopes): flat_value_scopes, latent_indicators_scopes_, *value_scopes_ = self._get_flat_value_scopes( weight_scopes, latent_indicators_scopes, *value_scopes) # If already invalid, return None if (any(s is None for s in value_scopes_) or (self._latent_indicators and latent_indicators_scopes_ is None)): return None # Split the flat value scopes based on value input sizes split_indices = np.cumsum(self._sum_sizes)[:-1] # IndicatorLeaf if self._latent_indicators: # Verify number of IndicatorLeaf if len(latent_indicators_scopes_) != len(flat_value_scopes): raise StructureError( "Number of IndicatorLeaf (%s) and values (%s) does " "not match for %s" % (len(latent_indicators_scopes_), len(flat_value_scopes), self)) # Go over IndicatorLeaf involved for each sum. Scope size should be exactly one for iv_scopes_for_sum in np.split(latent_indicators_scopes_, split_indices): if len(Scope.merge_scopes(iv_scopes_for_sum)) != 1: return None # Go over value input scopes for each sum being modeled. Within a single sum, the scope of # all the inputs should be the same for scope_slice in np.split(flat_value_scopes, split_indices): first_scope = scope_slice[0] if any(s != first_scope for s in scope_slice[1:]): self.info("%s is not complete with input value scopes %s", self, flat_value_scopes) return None return self._compute_scope(weight_scopes, latent_indicators_scopes, *value_scopes)
def _compute_mpe_path_common(self, counts, *input_values): if not self._values: raise StructureError("{} is missing input values.".format(self)) # Concatenate inputs along channel axis, should already be done during forward pass inp_concat = self._prepare_convolutional_processing(*input_values) spatial_counts = tf.reshape(counts, (-1, ) + self.output_shape_spatial) input_counts = tf.nn.conv2d_backprop_input( input_sizes=tf.shape(inp_concat), filter=self._dense_connections, out_backprop=spatial_counts, strides=[1] + self._strides + [1], padding='VALID', dilations=[1] + self._dilation_rate + [1], data_format="NHWC") # In case we have explicitly padded the tensor before forward convolution, we should # slice the counts now pad_left, pad_right, pad_bottom, pad_top = self.pad_sizes() if not any([pad_bottom, pad_left, pad_right, pad_top]): return self._split_to_children(input_counts) return self._split_to_children(input_counts[:, pad_top:-pad_bottom, pad_left:-pad_right, :])
def learn(self, loss=None, optimizer=None, post_gradient_ops=True): """Assemble TF operations performing GD learning of the SPN. This includes setting up the loss function (with regularization), setting up the optimizer and setting up post gradient-update ops. loss (Tensor): The operation corresponding to the loss to minimize. optimizer (tf.train.Optimizer): A TensorFlow optimizer to use for minimizing the loss. Returns: A tuple of grouped update Ops and a loss Op. """ if self._learning_task_type == LearningTaskType.SUPERVISED and self._root.ivs is None: raise StructureError( "{}: the SPN rooted at {} does not have a latent IVs node, so cannot setup " "conditional class probabilities.".format( self._name, self._root)) # If a loss function is not provided, define the loss function based # on learning-type and learning-method with tf.name_scope("Loss"): if loss is None: loss = (self.negative_log_likelihood() if self._learning_method == LearningMethodType.GENERATIVE else self.cross_entropy_loss()) if self._l1_regularize_coeff is not None or self._l2_regularize_coeff is not None: loss += self.regularization_loss() # Assemble TF ops for optimizing and weights normalization optimizer = optimizer if optimizer is not None else self._optimizer if optimizer is None: raise ValueError("Did not specify GD optimizer") with tf.name_scope("ParameterUpdate"): minimize = optimizer.minimize(loss=loss) if post_gradient_ops: return self.post_gradient_update(minimize), loss else: return minimize, loss
def generate_weights(self, init_value=1, trainable=True, input_sizes=None, name=None): """Generate a weights node matching this sum node and connect it to this sum. The function calculates the number of weights based on the number of input values of this sum. Therefore, weights should be generated once all inputs are added to this node. Args: init_value: Initial value of the weights. For possible values, see :meth:`~libspn.utils.broadcast_value`. trainable (bool): See :class:`~libspn.Weights`. input_sizes (list of int): Pre-computed sizes of each input of this node. If given, this function will not traverse the graph to discover the sizes. name (str): Name of the weighs node. If ``None`` use the name of the sum + ``_Weights``. Return: Weights: Generated weights node. """ if not self._values: raise StructureError("%s is missing input values" % self) if name is None: name = self._name + "_Weights" # Count all input values if not input_sizes: input_sizes = self.get_input_sizes() num_values = sum(input_sizes[2:]) # Skip ivs, weights # Generate weights weights = Weights(init_value=init_value, num_weights=num_values, trainable=trainable, name=name) self.set_weights(weights) return weights
def _compute_reducible(self, w_tensor, ivs_tensor, *input_tensors, weighted=True, dropconnect_keep_prob=None): """Computes a reducible ``Tensor`` so that reducing it over the last axis can be used for marginal inference, MPE inference and MPE path computation. Args: w_tensor (Tensor): A ``Tensor`` with the value of the weights of shape ``[num_sums, max_sum_size]`` ivs_tensor (Tensor): A ``Tensor`` with the value of the IVs corresponding to this node of shape ``[batch, num_sums * max_sum_size]``. input_tensors (tuple): A ``tuple`` of ``Tensors``s with the values of the children of this node. weighted (bool): Whether to apply the weights to the reducible values if possible. dropconnect_keep_prob (Tensor or float): A scalar ``Tensor`` or float that holds the dropconnect keep probability. By default it is None, in which case no dropconnect is being used. Returns: A ``Tensor`` of shape ``[batch, num_sums, max_sum_size]`` that can be used for computing marginal inference, MPE inference, gradients or MPE paths. """ if not self._values: raise StructureError("%s is missing input values" % self) if not self._weights: raise StructureError("%s is missing weights" % self) # Prepare tensors for component-wise application of weights and IVs w_tensor, ivs_tensor, reducible = self._prepare_component_wise_processing( w_tensor, ivs_tensor, *input_tensors, zero_prob_val=-float('inf')) # Apply latent IVs if self._ivs: reducible = utils.cwise_add(reducible, ivs_tensor) # Apply weights if weighted: # Maybe apply dropconnect dropconnect_keep_prob = utils.maybe_first( dropconnect_keep_prob, self._dropconnect_keep_prob) if dropconnect_keep_prob is not None and dropconnect_keep_prob != 1.0: if self._ivs: self.logger.warn( "Using dropconnect and latent IVs simultaneously. " "This might result in zero probabilities throughout and unpredictable " "behavior of learning. Therefore, dropconnect is turned off for node {}." .format(self)) else: mask = self._create_dropout_mask(dropconnect_keep_prob, tf.shape(reducible), log=True) w_tensor = utils.cwise_add(w_tensor, mask) if conf.renormalize_dropconnect: w_tensor = tf.nn.log_softmax(w_tensor, axis=-1) reducible = utils.cwise_add(reducible, w_tensor) return reducible
def _assert_generated(self): if self._perms is None: raise StructureError( "{}: First need to generate decompositions.".format(self))
def _compute_log_mpe_path(self, counts, *value_values, use_unweighted=False, sample=False, sample_prob=None): # Path per product node is calculated by permuting backwards to the # input nodes, then adding the appropriate counts per input, and then # scattering the summed counts to value inputs # Check inputs if not self._values: raise StructureError("%s is missing input values." % self) def permute_counts(input_sizes): # Function that permutes count values, backward to inputs. counts_indices_list = [] def range_with_blocksize(start, stop, block_size, step): # A function that produces an arithmetic progression (Similar to # Python's range() function), but for a given block-size of # consecutive numbers. # E.g: range_with_blocksize(start=0, stop=20, block_size=3, step=5) # = [0, 1, 2, 5, 6, 7, 10, 11, 12, 15, 16, 17] counts_indices = [] it = 0 low = start high = low + block_size while low < stop: counts_indices = counts_indices + list(range(low, high)) it += 1 low = start + (it * step) high = low + block_size return counts_indices for inp, inp_size in enumerate(input_sizes): block_size = int(self._num_prods / np.prod(input_sizes[:inp + 1])) step = int(np.prod(input_sizes[inp:])) for i in range(inp_size): start = i * block_size stop = self._num_prods - (block_size * (inp_size - i - 1)) counts_indices_list.append( range_with_blocksize(start, stop, block_size, step)) return counts_indices_list if (len(self._input_sizes) > 1): permuted_indices = permute_counts(self._input_sizes) summed_counts = tf.reduce_sum(utils.gather_cols_3d( counts, permuted_indices), axis=-1) processed_counts_list = tf.split(summed_counts, self._input_sizes, axis=-1) else: # For single input case, i.e, when _num_prods = 1 summed_counts = self._input_sizes[0] * [counts] processed_counts_list = [tf.concat(values=summed_counts, axis=-1)] # Zip lists of processed counts and value_values together for scattering value_counts = zip(processed_counts_list, value_values) return self._scatter_to_input_tensors(*value_counts)
def convert_to_layer_nodes(root): """ At each level in the SPN rooted in the 'root' node, model all the nodes as a single layer-node. Args: root (Node): The root of the SPN graph. Returns: root (Node): The root of the SPN graph, with each layer modelled as a single layer-node. """ parents = defaultdict(list) depths = defaultdict(list) node_to_depth = OrderedDict() node_to_depth[root] = 1 def get_parents(node): # Add to Parents dict if node.is_op: for i in node.inputs: if (i and # Input not empty not (i.is_param or i.is_var)): parents[i.node].append(node) node_to_depth[i.node] = node_to_depth[node] + 1 def permute_inputs(input_values, input_sizes): # For a given list of inputs and their corresponding sizes, create a # nested-list of (input, index) pairs. # E.g: input_values = [(A, [2, 5]), (B, None)] # input_sizes = [2, 3] # inputs = [[('A', 2), ('A', 5)], # [('B', 0), ('B', 1), ('B', 2)]] inputs = [ list(product([inp.node], inp.indices)) if inp and inp.indices else list(product([inp.node], list(range(inp_size)))) for inp, inp_size in zip(input_values, input_sizes) ] # For a given nested-list of (input, index) pairs, permute over the inputs # E.g: permuted_inputs = [('A', 2), ('B', 0), # ('A', 2), ('B', 1), # ('A', 2), ('B', 2), # ('A', 5), ('B', 0), # ('A', 5), ('B', 1), # ('A', 5), ('B', 2)] permuted_inputs = list(product(*[inps for inps in inputs])) return list(chain(*permuted_inputs)) # Create a parents dictionary of the SPN graph traverse_graph(root, fun=get_parents, skip_params=True) # Create a depth dictionary of the SPN graph for key, value in node_to_depth.items(): depths[value].append(key) spn_depth = len(depths) # Iterate through each depth of the SPN, starting from the deepest layer, # moving up to the root node for depth in range(spn_depth, 1, -1): if isinstance(depths[depth][0], (Sum, ParallelSums)): # A Sums Layer # Create a default SumsLayer node with tf.name_scope("Layer%s" % depth): sums_layer = SumsLayer(name="SumsLayer-%s.%s" % (depth, 1)) # Initialize a counter for keeping track of number of sums # modelled in the layer node layer_num_sums = 0 # Initialize an empty list for storing sum-input-sizes of sums # modelled in the layer node num_or_size_sums = [] # Iterate through each node at the current depth of the SPN for node in depths[depth]: # TODO: To be replaced with node.num_sums once AbstractSums # class is introduced # No. of sums modelled by the current node node_num_sums = (1 if isinstance(node, Sum) else node.num_sums) # Add Input values of the current node to the SumsLayer node sums_layer.add_values(*node.values * node_num_sums) # Add sum-input-size, of each sum modelled in the current node, # to the list num_or_size_sums += [sum(node.get_input_sizes()[2:]) ] * node_num_sums # Visit each parent of the current node for parent in parents[node]: try: # 'Values' in case parent is an Op node values = list(parent.values) except AttributeError: # 'Inputs' in case parent is a Concat node values = list(parent.inputs) # Iterate through each input value of the current parent node for i, value in enumerate(values): # If the value is the current node if value.node == node: # Check if it has indices if value.indices is not None: # If so, then just add the num-sums of the # layer-op as offset indices = (np.asarray(value.indices) + layer_num_sums).tolist() else: # If not, then create a list accrodingly indices = list( range(layer_num_sums, (layer_num_sums + node_num_sums))) # Replace previous (node) Input value in the # current parent node, with the new layer-node value values[i] = (sums_layer, indices) break # Once child-node found, don't have to search further # Reset values of the current parent node, by including # the new child (Layer-node) try: # set 'values' in case parent is an Op node parent.set_values(*values) except AttributeError: # set 'inputs' in case parent is a Concat node parent.set_inputs(*values) # Increment num-sums-counter of the layer-node layer_num_sums += node_num_sums # Disconnect node.disconnect_inputs() # After all nodes at a certain depth are modelled into a Layer-node, # set num-sums parameter accordingly sums_layer.set_sum_sizes(num_or_size_sums) elif isinstance(depths[depth][0], (Product, PermuteProducts)): # A Products Layer with tf.name_scope("Layer%s" % depth): prods_layer = ProductsLayer(name="ProductsLayer-%s.%s" % (depth, 1)) # Initialize a counter for keeping track of number of prods # modelled in the layer node layer_num_prods = 0 # Initialize an empty list for storing prod-input-sizes of prods # modelled in the layer node num_or_size_prods = [] # Iterate through each node at the current depth of the SPN for node in depths[depth]: # Get input values and sizes of the product node input_values = list(node.values) input_sizes = list(node.get_input_sizes()) if isinstance(node, PermuteProducts): # Permute over input-values to model permuted products input_values = permute_inputs(input_values, input_sizes) node_num_prods = node.num_prods prod_input_size = len(input_values) // node_num_prods elif isinstance(node, Product): node_num_prods = 1 prod_input_size = int(sum(input_sizes)) # Add Input values of the current node to the ProductsLayer node prods_layer.add_values(*input_values) # Add prod-input-size, of each product modelled in the current # node, to the list num_or_size_prods += [prod_input_size] * node_num_prods # Visit each parent of the current node for parent in parents[node]: values = list(parent.values) # Iterate through each input value of the current parent node for i, value in enumerate(values): # If the value is the current node if value.node == node: # Check if it has indices if value.indices is not None: # If so, then just add the num-prods of the # layer-op as offset indices = value.indices + layer_num_prods else: # If not, then create a list accrodingly indices = list( range(layer_num_prods, (layer_num_prods + node_num_prods))) # Replace previous (node) Input value in the # current parent node, with the new layer-node value values[i] = (prods_layer, indices) # Reset values of the current parent node, by including # the new child (Layer-node) parent.set_values(*values) # Increment num-prods-counter of the layer node layer_num_prods += node_num_prods # Disconnect node.disconnect_inputs() # After all nodes at a certain depth are modelled into a Layer-node, # set num-prods parameter accordingly prods_layer.set_prod_sizes(num_or_size_prods) elif isinstance(depths[depth][0], (SumsLayer, ProductsLayer, Concat)): # A Concat node pass else: raise StructureError("Unknown node-type: {}".format( depths[depth][0])) return root
def convert(input_like): inpt = Input.as_input(input_like) if inpt and inpt.node.tf_graph is not self.tf_graph: raise StructureError("%s is in a different TF graph than %s" % (inpt.node, self)) return inpt
def _compute_scope(self, *value_scopes): if not self._values: raise StructureError("%s is missing input values." % self) value_scopes = self._gather_input_scopes(*value_scopes) return [Scope.merge_scopes(chain.from_iterable(value_scopes))]
def _compute_scope(self, *input_scopes): if not self._inputs: raise StructureError("%s is missing inputs." % self) input_scopes = self._gather_input_scopes(*input_scopes) return list(chain.from_iterable(input_scopes))
def _compute_out_size(self, *input_out_sizes): if not self._inputs: raise StructureError("%s is missing inputs." % self) return sum(self._gather_input_sizes(*input_out_sizes))
def _get_num_input_scopes(self): if not self._values: raise StructureError("{}: cannot get num input scopes since this " "node has no children.".format(self)) return self._values[0].node.num_vars