def test_disconnected(): model = PairWiseFiniteModel(size=4, al_size=5) model.add_interaction(0, 1, np.random.random(size=(5, 5))) model.add_interaction(2, 3, np.random.random(size=(5, 5))) max_lh_gt = model.max_likelihood(algorithm='bruteforce') max_lh = max_lh_path_dp(model) assert np.allclose(max_lh, max_lh_gt)
def test_pairs(): n = 5 j = np.array([[0, 0, 10], [0, 0, 0], [0, 0, 0]]) model = PairWiseFiniteModel(2 * n, 3) for i in range(n): model.add_interaction(2 * i, 2 * i + 1, j) expected = np.array([0, 2] * n) result = model.max_likelihood(algorithm='tree_dp') assert np.allclose(expected, result)
def test_fully_isolated(): # Create model where all variables are independent with given # distributions. Then calculate empirical distributions for every # variable - they should be close to original distributions. gr_size, al_size, num_samples = 10, 5, 200 probs = np.random.random(size=(gr_size, al_size)) probs /= probs.sum(axis=1).reshape(-1, 1) model = PairWiseFiniteModel(gr_size, al_size) model.set_field(np.log(probs)) samples = model.sample(num_samples=num_samples, algorithm='tree_dp') check_samples(samples=samples, true_marg_probs=probs, tol=2e-3)
def sample_tree_dp(model: PairWiseFiniteModel, num_samples: int): """Draws iid samples with dynamic programming on tree.""" assert not model.get_dfs_result().had_cycles, "Graph has cycles." log_z = infer_tree_dp(model, subtree_mp=True).marg_prob log_z = log_z.astype(np.float64, copy=False) assert log_z.shape == (model.gr_size, model.al_size) dfs_edges = model.get_dfs_result().dfs_edges ints = model.get_interactions_for_edges(dfs_edges) num_samples = numba.types.int32(num_samples) return _sample_internal(log_z, dfs_edges, ints, num_samples)
def test_to_fg_factor(): model = PairWiseFiniteModel(3, 2) x0, x1, x2 = model.get_symbolic_variables() model *= (x0 + x1) model *= (2 * x1 + x2) tmp_file = os.path.join(LibDaiInterop().tmp_path, 'tmp.fg') LibDaiInterop.write_fg_file(model, tmp_file) with open(tmp_file, 'r') as f: lines = [x.strip() for x in f.readlines()] assert lines == [ '2', '', '2', '0 1', '2 2', '3', '1 1.0000000000', '2 1.0000000000', '3 2.0000000000', '', '2', '1 2', '2 2', '3', '1 2.0000000000', '2 1.0000000000', '3 3.0000000000', '']
def test_infer_disconnected(): pw_model = PairWiseFiniteModel(5, al_size=2) pw_model.set_field(np.random.random(size=(5, 2))) pw_model.add_interaction(2, 3, np.random.random(size=(2, 2))) true_pf = np.exp(pw_model.infer(algorithm='bruteforce').log_pf) nfg_model = inferlo.NormalFactorGraphModel.from_model(pw_model) pf = infer_edge_elimination(nfg_model) assert np.allclose(true_pf, pf)
def test_disconnected(): model = PairWiseFiniteModel(size=4, al_size=5) model.add_interaction(0, 1, np.random.random(size=(5, 5))) model.add_interaction(2, 3, np.random.random(size=(5, 5))) max_lh_gt = model.max_likelihood(algorithm='path_dp') max_lh_gt_value = np.log(model.evaluate(max_lh_gt)) max_lh_lower_bound = lp_relaxation(model).lower_bound max_lh_upper_bound = lp_relaxation(model).upper_bound assert (max_lh_lower_bound <= max_lh_gt_value <= max_lh_upper_bound)
def test_create_from_model(): field = np.zeros((4, 2)) edges = [[0, 1], [0, 2], [0, 3]] j1 = np.array([[0, 0], [0, 1]]) interactions = [j1, j1, j1] model1 = PairWiseFiniteModel.create(field, edges, interactions) expected_edges = [[0, 3], [0, 4], [1, 5], [2, 6], [1, 3], [2, 3]] kron_delta = np.array([[[1, 0], [0, 0]], [[0, 0], [0, 1]]]) unit_factor = np.array([1, 1]) expected_factor_tables = [ np.exp(j1)] * 3 + [kron_delta] + [ unit_factor] * 3 model2 = NormalFactorGraphModel.from_model(model1) assert model2.num_variables == 6 assert len(model2.factors) == 7 assert model2.edges == expected_edges for i in range(7): assert np.allclose(model2.factors[i].values, expected_factor_tables[i])
def minimal_cycle(model: PairWiseFiniteModel) -> sherali_adams_result: """ This is an implementation of Cycle relaxation. In fact, cycle relaxation is a simplified version of the third level of Sherali-Adams hierarchy. It may result in worse upper bounds but has much fewer constraints and solved faster. The idea behind this relaxation is to consider all cycles in the graph and to add cycle-to-edge marginalization constraints to the local consistency constraints. In some cases cycle relaxation coincides with the third level of Sherali-Adams. It may also be shown that instead of all cycles constraints, it is enough to consider only chordless cycles. In this code we consider the set of minimal cycles found by networkx. More on LP hierarchies may be found in D.Sontag's thesis "Approximate Inference in Graphical Models using LP relaxations". https://people.csail.mit.edu/dsontag/papers/sontag_phd_thesis.pdf """ al_size = model.al_size var_size = model.gr_size edge_list = model.edges # check if graph is acyclic if model.is_graph_acyclic(): print("Graph is acyclic!") print("Cycle relaxation is equivalent to the local LP relaxation.") # introduce cluster variables and constraints clusters = {} constraints = [] # add local consistency constraints first for cluster_size in [1, 2]: variables = list(combinations(range(var_size), cluster_size)) values = list(product(range(al_size), repeat=cluster_size)) for cluster_ids in variables: cluster = {} for x in values: cluster[x] = cp.Variable(nonneg=True) clusters[cluster_ids] = cluster # add normalization constraint constraints += [sum(list(cluster.values())) == 1] # add marginalization constraints for cluster_subset_size in range(1, cluster_size): all_cluster_subsets = list( combinations(list(cluster_ids), cluster_subset_size)) subset_values = list( product(range(al_size), repeat=cluster_subset_size)) for subset in all_cluster_subsets: subset_ids = list(subset) for subset_x in subset_values: marginal_sum = 0.0 for value, cp_variable in cluster.items(): consistency = [ value[cluster_ids.index( subset_ids[i])] == subset_x[i] for i in range(cluster_subset_size) ] if sum(consistency) == len(subset): marginal_sum += cp_variable constraints += [ marginal_sum == clusters[subset][subset_x] ] # add cycle consistency graph = model.get_graph() cycles = nx.cycle_basis(graph) for cycle in cycles: cycle.append(cycle[0]) cycle_edges = [] for i in range(len(cycle) - 1): edge = sorted([cycle[i], cycle[i + 1]]) cycle_edges.append(tuple(edge)) cycle_values = list(product(range(al_size), repeat=len(cycle))) cluster_ids = tuple(sorted(cycle)) cluster = {} for x in cycle_values: cluster[x] = cp.Variable(nonneg=True) clusters[cluster_ids] = cluster # add normalization constraint constraints += [sum(list(cluster.values())) == 1] # add marginalization constraints for edge in cycle_edges: edge_values = list(product(range(al_size), repeat=2)) first_node_position = cluster_ids.index(edge[0]) second_node_position = cluster_ids.index(edge[1]) for edge_value in edge_values: marginal_sum = 0.0 edge_variable = clusters[edge][edge_value] for x in cycle_values: if (x[first_node_position] == edge_value[0]) \ and (x[second_node_position] == edge_value[1]): marginal_sum += clusters[cluster_ids][x] constraints += [marginal_sum == edge_variable] # define objective objective = 0 # add field in every node for node in range(var_size): for letter in range(al_size): objective += model.field[node, letter] * \ clusters[(node,)][(letter,)] # add pairwise interactions # a and b iterate over all values of the finite field for edge in edge_list: for a in range(model.al_size): for b in range(model.al_size): J = model.get_interaction_matrix(edge[0], edge[1]) if (edge[0] <= edge[1]): objective += J[a, b] * clusters[(edge[0], edge[1])][(a, b)] else: objective += J[a, b] * clusters[(edge[1], edge[0])][(b, a)] prob = cp.Problem(cp.Maximize(objective), constraints) prob.solve(solver=cp.SCS, eps=1e-8) projected_variables = [] for node in range(var_size): projected_variables.append(clusters[(node, )].values()) return sherali_adams_result(upper_bound=prob.value, projection=projected_variables)
def map_lp(model: PairWiseFiniteModel) -> map_lp_result: """LP relaxation of MAP problem for pairwise model. This function implements linear programming (LP) relaxation of maximum a posteriori assignment problem (MAP) for pairwise graphical model with finite alphabet. The goal of MAP estimation is to find most probable assignment of original variables by maximizing probability density function. For the case of pairwise finite model it reduces to maximization of quadratic function over finite field. For every node, we introduce Q non-negative belief variables where Q is the size of the alphabet. Every such variable is our 'belief' that variable at node takes particular value. Analogously, for every edge we introduce Q*Q pairwise beliefs. For both node and edge beliefs we require normalization constraints: 1) for every node, the sum of beliefs equals one and 2) for every edge the sum of edge-beliefs equals one. We also add marginalization constraint: for every edge, summing edge beliefs over one of the nodes must equal to the node belief of the second node. Finally we get a linear program and its solution is an upper bound on the MAP value. We restore the lower bound on MAP value as the solution of the dual relaxation. More details may be found in "MAP Estimation, Linear Programming and BeliefPropagation with Convex Free Energies" by Yair Weiss, Chen Yanover and Talya Meltzer. https://arxiv.org/pdf/1206.5286.pdf :param model: Model for which to solve MAP problem. :return: Object with the following fields: ``upper_bound`` - upper bound on MAP value (solution of LP); ``lower_bound`` - lower bound on MAP value (dual solution); ``node_beliefs`` - optimal values of node beliefs; ``edge_beliefs`` - optimal values of edge beliefs; ``normalization_duals`` - optimal values of dual variables that correspond to normalization constraints; ``marginalization_duals`` - optimal values of dual variables that correspond to marginalization constraints. """ edge_list = model.edges node_beliefs = cp.Variable((model.gr_size, model.al_size), nonneg=True) edge_beliefs = [] for edge in edge_list: edge_beliefs.append( cp.Variable((model.al_size, model.al_size), nonneg=True)) objective = 0 constraints = [] # add field in every node for node in range(model.gr_size): for letter in range(model.al_size): objective += model.field[node, letter] * node_beliefs[node, letter] # add pairwise interactions # a and b iterate over all values of the finite field for edge in edge_list: for a in range(model.al_size): for b in range(model.al_size): J = model.get_interaction_matrix(edge[0], edge[1]) objective += J[a, b] * edge_beliefs[edge_list.index(edge)][a, b] # normalization constraints for edge in edge_list: constraints += [ sum([ edge_beliefs[edge_list.index(edge)][a, b] for a in range(model.al_size) for b in range(model.al_size) ]) == 1 ] # marginalization constraints for edge in edge_list: for a in range(model.al_size): marginal_left = 0.0 for b in range(model.al_size): marginal_left += edge_beliefs[edge_list.index(edge)][a, b] constraints += [marginal_left == node_beliefs[edge[0], a]] for a in range(model.al_size): marginal_right = 0.0 for b in range(model.al_size): marginal_right += edge_beliefs[edge_list.index(edge)][b, a] constraints += [marginal_right == node_beliefs[edge[1], a]] prob = cp.Problem(cp.Maximize(objective), constraints) prob.solve(solver=cp.SCS, eps=1e-8) normal_dual_vars = [ constraints[i].dual_value for i in range(len(edge_list)) ] marginal_dual_vars = [ constraints[i].dual_value for i in range(len(edge_list), len(constraints)) ] dual_objective = sum(normal_dual_vars) return map_lp_result(upper_bound=prob.value, lower_bound=dual_objective, node_beliefs=node_beliefs.value, edge_beliefs=[edge.value for edge in edge_beliefs], normalization_duals=normal_dual_vars, marginalization_duals=marginal_dual_vars)
def test_fully_isolated(): model = PairWiseFiniteModel(10, 2) model.set_field(np.random.random(size=(10, 2))) ground_truth = model.infer(algorithm='bruteforce') result = model.infer(algorithm='tree_dp') assert_results_close(result, ground_truth)
def test_fully_isolated(): model = PairWiseFiniteModel(10, 2) model.set_field(np.random.random(size=(10, 2))) ground_truth = model.max_likelihood(algorithm='bruteforce') result = model.max_likelihood(algorithm='tree_dp') assert np.allclose(ground_truth, result)
def lp_relaxation(model: PairWiseFiniteModel) -> LPRelaxResult: """Max Likelihood for pairwise model by solving LP relaxation. 1) Reformulates the original problem of maximizing ``sum F[i][X_i] + 0.5*sum J[i][j][X[i]][X[j]])`` as a binary optimization problem by introducing new variables ``y_i``, ``a`` and ``z_i,j,a,b``:: maximize (sum_i sum_a F[i][a] * y_i,a) + (sum_i sum_j sum_a sum_b 0.5*sum J[i][j][a][b] * z_i,j,a,b)) subject to y_i,a in {0, 1} z_i,j,a,b in {0, 1} (for all i) sum_a y_i,a = 1 z_i,j,a,b <= y_i,a z_i,j,a,b <= y_j,b z_i,j,a,b >= y_i,a + y_j,b - 1 2) Solves the LP relaxation by relaxing binary constraints to:: y_i,a in [0, 1] z_i,j,a,b in [0, 1] Note that ``z`` will be reshaped to matrix (cvxpy does not support tensor variables). :param model: Model for which to find most likely state. :return: Solution of LP relaxation of the Max Likelihood problem. """ edge_list = model.edges y = cp.Variable((model.gr_size, model.al_size), nonneg=True) z = [] for e in edge_list: z.append(cp.Variable((model.al_size, model.al_size), nonneg=True)) obj = 0 cons = [] # add field for i in range(model.gr_size): for a in range(model.al_size): obj += model.field[i, a] * y[i, a] # add pairwise # a and b iterate over all values of the finite field for e in edge_list: for a in range(model.al_size): for b in range(model.al_size): J = model.get_interaction_matrix(e[0], e[1]) obj += J[a, b] * z[edge_list.index(e)][a, b] # add y constraints: for i in range(model.gr_size): cons += [sum(y[i, :]) == 1] for a in range(model.al_size): cons += [y[i, a] <= 1] # add z constraints for e in edge_list: for a in range(model.al_size): for b in range(model.al_size): cons += [z[edge_list.index(e)][a, b] <= 1] cons += [z[edge_list.index(e)][a, b] <= y[e[0], a]] cons += [z[edge_list.index(e)][a, b] <= y[e[1], b]] cons += [z[edge_list.index(e)][a, b] >= y[e[0], a] + y[e[1], b] - 1] prob = cp.Problem(cp.Maximize(obj), cons) prob.solve(solver=cp.SCS) # in fact, rounding LP relaxation is sampling rounded = np.array(np.random.randint(low=0, high=model.al_size, size=model.gr_size)) lower = np.log(model.evaluate(rounded)) return LPRelaxResult( upper_bound=prob.value, lower_bound=lower, rounded_solution=rounded )