def test_infer_isolated(): gr_size = 1000 model = PairWiseFiniteModel(gr_size, 2) model.set_field(np.array([[0, 1]] * gr_size)) res = model.infer(algorithm='bruteforce') assert np.allclose(res.log_pf, gr_size * np.log(1 + np.exp(1))) assert np.allclose(res.marg_prob - softmax([0, 1]), 0)
def test_infer_ising_2_variables(): model = PairWiseFiniteModel(2, 2) j = 5 * np.random.random() model.add_interaction(0, 1, np.array([[j, -j], [-j, j]])) result = model.infer(algorithm='bruteforce') assert np.allclose(result.log_pf, np.log(4 * np.cosh(j))) assert np.allclose(result.marg_prob, 0.5 * np.ones((2, 2)))
def test_infer_1_variable(): al_size = 10 probs = _stochastic_vector(al_size) model = PairWiseFiniteModel(1, al_size) model.set_field(np.log(probs).reshape(1, al_size)) result = infer_bruteforce(model) assert np.allclose(result.log_pf, 0) assert np.allclose(result.marg_prob, probs)
def ising_model_on_graph(graph: networkx.Graph, field_range=0.1, interaction_range=0.1, seed=0) -> PairWiseFiniteModel: """Builds random Ising model on given graph. :param graph: Graph for the model. Vertices are variables, nodes are interactions. :param field_range: Fields will be sampled uniformly from ``[-field_range, field_range]``. :param interaction_range: Interactions will be sampled uniformly from ``[-interaction_range, interaction_range]``. :param seed: Random seed. :return: Generated model. """ # Remap arbitrary variable labels to integers. nodes = list(graph.nodes) var_index = {nodes[i]: i for i in range(len(nodes))} edges = [(var_index[u], var_index[v]) for u, v in graph.edges()] np.random.seed(seed) field = np.random.uniform(low=-field_range, high=field_range, size=(len(nodes), )) field = np.einsum('a,b->ab', field, [-1, 1]) inter = np.random.uniform(low=-interaction_range, high=interaction_range, size=(len(edges), )) inter = np.einsum('a,bc->abc', inter, [[1, -1], [-1, 1]]) return PairWiseFiniteModel.create(field, edges, inter)
def grid_potts_model(height, width, al_size=3, seed=111, zero_field=False) -> PairWiseFiniteModel: """Generates random PairWiseFinteModel on a grid. :param height: Heigth of the grid. :param width: Wwidth of the grid. :param al_size: Alphabet size. :param seed: Random seed. :param zero_field: Whether model should be zero-field. :return: Generated Potts Model. """ np.random.seed(seed) gr_size = width * height edges_num = 2 * width * height - width - height edges = [] for x in range(height): for y in range(width): v = x * width + y if x != height - 1: edges.append((v, v + width)) # down if y != width - 1: edges.append((v, v + 1)) # right field = 0.1 * np.random.random(size=(gr_size, al_size)) if zero_field: field *= 0 inter = np.random.random(size=(edges_num, al_size, al_size)) * 5.0 return PairWiseFiniteModel.create(field, edges, inter)
def test_max_likelihood(): model = PairWiseFiniteModel(3, 2) model.set_field(np.array([[0.4, 0.6], [0.4, 0.6], [0.4, 0.6]])) model.add_interaction(0, 1, np.array([[0, 10], [10, 0]])) model.add_interaction(1, 2, np.array([[0, 10], [10, 0]])) state = model.max_likelihood(algorithm='bruteforce') assert np.allclose(state, np.array([1, 0, 1]))
def tree_potts_model(gr_size=5, al_size=2, seed=111, same_j=None, zero_field=False) -> PairWiseFiniteModel: """Generates random PairWiseFinteModel on a random tree. :param gr_size: Size of the graph (number of variables). :param al_size: Alphabet size. :param seed: Random set. :param same_j: If set, interaction matrix for all edges. :param zero_field: Whether model should be zero-field. :return: Generated Potts Model. """ np.random.seed(seed) tree = networkx.generators.trees.random_tree(gr_size, seed=seed) model = PairWiseFiniteModel(gr_size, al_size) if not zero_field: model.set_field(-3.0 + 6.0 * np.random.random((gr_size, al_size))) for v1, v2 in tree.edges: J = np.random.random((al_size, al_size)) * 5.0 if same_j is not None: J = same_j model.add_interaction(v1, v2, J) return model
def test_cycle3(): np.random.seed(0) model = PairWiseFiniteModel(3, 3) for i, j in [(0, 1), (1, 2), (0, 2)]: model.add_interaction(i, j, np.random.random(size=(3, 3))) gt = model.infer(algorithm='bruteforce') result = model.infer(algorithm='mean_field') assert_results_close(result, gt, log_pf_tol=0.1, mp_mse_tol=1e-4)
def clique_potts_model(gr_size=5, al_size=2, seed=0) -> PairWiseFiniteModel: """Generates random PairWiseFinteModel on a clique.""" np.random.seed(seed) model = PairWiseFiniteModel(gr_size, al_size) model.set_field(-3.0 + 6.0 * np.random.random((gr_size, al_size))) for i in range(gr_size): for j in range(i + 1, gr_size): inter = np.random.random((al_size, al_size)) * 5.0 model.add_interaction(i, j, inter) return model
def test_isolated_exact(): np.random.seed(0) gr_size = 1000 al_size = 5 model = PairWiseFiniteModel(gr_size, al_size) model.set_field(np.random.random((gr_size, al_size))) gt = model.infer(algorithm='bruteforce') result = model.infer(algorithm='mean_field') assert_results_close(result, gt)
def pairwise_model_on_graph(graph, al_size=2, zero_field=False, seed=0): """Builds random pairwise model with given interaction graph. :param graph: Interaction graph. Nodes must be labeled with consecutive integers, starting with 0. :param al_size: Alphabet size. :param zero_field: Whether model should be zero-field. :param seed: Random seed. :return: Generated model. """ np.random.seed(seed) gr_size = len(graph.nodes()) field = np.random.random(size=(gr_size, al_size)) if zero_field: field *= 0 edges = np.array(list(graph.edges())) interactions = np.random.random(size=(len(edges), al_size, al_size)) return PairWiseFiniteModel.create(field, edges, interactions)
def line_potts_model(gr_size=5, al_size=2, seed=111, same_j=None, zero_field=False) -> PairWiseFiniteModel: """Generates random PairWiseFinteModel on a line graph. :param gr_size: Size of the graph (number of variables). :param al_size: Alphabet size. :param seed: Random seed. :param same_j: If set, interaction matrix for all edges. :param zero_field: Whether model should be zero-field. :return: Generated model. """ np.random.seed(seed) field = np.zeros((gr_size, al_size)) if not zero_field: field = -3.0 + 6.0 * np.random.random(field.shape) edges = [[i, i + 1] for i in range(gr_size - 1)] inter = np.random.random(size=(gr_size - 1, al_size, al_size)) * 5.0 if same_j is not None: inter = np.tile(same_j, (gr_size - 1, 1, 1)) return PairWiseFiniteModel.create(field, edges, inter)
def test_sample_bruteforce(): gr_size, num_samples = 3, 50 model = PairWiseFiniteModel(gr_size, 2) model.set_field(np.array([[0, 20]] * gr_size)) samples = sample_bruteforce(model, num_samples=num_samples) assert np.allclose(samples, np.ones((num_samples, gr_size)))
def test_max_likelihood_isolated(): gr_size = 1000 model = PairWiseFiniteModel(gr_size, 2) model.set_field(np.array([[0, 1]] * gr_size)) result = model.max_likelihood(algorithm='bruteforce') assert np.allclose(result, np.ones(gr_size))
def to_junction_tree_model(model, algorithm) -> JunctionizedModel: """Builds equivalent model on a junction tree. First, builds a junction tree using algorithm from NetworkX which uses Minimum Fill-in heuristic. Then, builds a new model in which variables correspond to nodes in junction tree - we will call them "supervariables". Values of new supervariables are encoded values of original variables. New alphabet size is original alphabet size to the power of maximaljunction size. If some supervariables have less variables than others, we just don't use all available for encoding "address space". We mark those impossible values as having probability 0 (i.e log probability -inf). Fields in new model are calculated by multiplying all field and interaction factors on variables in the same supervariable. While doing this, we make sure that every factor is counted only once. If some factor was accounted for in one supervariable field, it won't be accounted for again in other supervariables. Interaction factors in new model contain consistency requirement. If a variable of original model appears in multiple supervariables, we allow only those states where it takes the same value in all supervariables. We achieve that by using interaction factors which are equal to 1 if values of the same original variable in different supervariables are equal, and 0 if they are not equal. We actually use values 0 and -inf, because we work with logarithms. See https://en.wikipedia.org/wiki/Tree_decomposition. :param model: original model. :param algorithm: decomposition algorithm. :return: JunctionizedModel object, which contains junction tree and the new model, which is equivalent to original model, but whose graph is a tree. """ # Build junction tree. graph = model.get_graph() if algorithm == 'min_fill_in': tree_width, junc_tree = treewidth_min_fill_in(graph) elif algorithm == 'min_degree': tree_width, junc_tree = treewidth_min_degree(graph) elif algorithm == 'auto': tree_width_1, junc_tree_1 = treewidth_min_fill_in(graph) tree_width_2, junc_tree_2 = treewidth_min_degree(graph) if tree_width_1 < tree_width_2: tree_width, junc_tree = tree_width_1, junc_tree_1 else: tree_width, junc_tree = tree_width_2, junc_tree_2 else: raise ValueError('Unknown treewidth decomposition algorithm %s' % algorithm) jt_nodes = list(junc_tree.nodes()) sv_size = tree_width + 1 # Supervariable size. new_gr_size = len(jt_nodes) # New graph size. new_al_size = model.al_size**sv_size # New alphabet size. if new_al_size > 1e6: raise TooMuchStatesError("New domain size is too large: %d." % new_al_size) # Build edge list in terms of indices in new graph. nodes_lookup = {jt_nodes[i]: i for i in range(len(jt_nodes))} new_edges = np.array([[nodes_lookup[u], nodes_lookup[v]] for u, v in junc_tree.edges()]) # Convert node lists to numpy arrays. jt_nodes = [np.fromiter(node, dtype=np.int32) for node in jt_nodes] # Calculate fields which describe interaction beteen supervariables. # If supervariable has less than ``sv_size`` variables, pad with -inf. # Then, when decoding, we will just throw away values from the left. # We should account for each factor of the old graph in exactly one factor # in the new graph. So, for field and interaction factors of the old graph # we keep track of whether we already took them, and don't take them for # the second time. new_field = np.ones((new_gr_size, new_al_size), dtype=np.float64) * -np.inf used_node_fields = set() for new_node_id in range(new_gr_size): old_nodes = jt_nodes[new_node_id] node_field = model.get_subgraph_factor_values( old_nodes, vars_skip=used_node_fields) new_field[new_node_id, 0:len(node_field)] = node_field used_node_fields.update(old_nodes) # Now, for every edge in new graph - add interaction factor requiring that # the same variable appearing in two supervariables always has the same # values. # We achieve this by using Kroenker delta function. # As we working with logarithms, we populate -inf for impossible states, # and 0 for possible states. new_interactions = np.zeros((len(new_edges), new_al_size, new_al_size)) for edge_id in range(len(new_edges)): u, v = new_edges[edge_id] allowed = build_multi_delta(sv_size, model.al_size, jt_nodes[u], jt_nodes[v]) new_interactions[edge_id, np.logical_not(allowed)] = -np.inf from inferlo.pairwise.pwf_model import PairWiseFiniteModel new_model = PairWiseFiniteModel.create(new_field, new_edges, new_interactions) return JunctionizedModel(new_model, jt_nodes, model.gr_size, model.al_size)