def test_max_likelihood_all_methods(): # Sanity check that all algorithms work on very simple model. all_methods = ['auto', 'bruteforce', 'tree_dp', 'path_dp', 'junction_tree'] model = PairWiseFiniteModel(2, 2) model.add_interaction(0, 1, np.array([[0, 0], [0, 1]])) expected_result = np.array([1, 1]) for method in all_methods: result = model.max_likelihood(algorithm=method) assert np.allclose(result, expected_result)
def test_sample_all_methods(): # Sanity check that all algorithms work on very simple model. all_methods = ['auto', 'bruteforce', 'tree_dp', 'junction_tree'] model = PairWiseFiniteModel(2, 2) model.set_field(np.array([[100, 0], [100, 0]])) num_samples = 10 expected_result = np.zeros((num_samples, 2)) for method in all_methods: result = model.sample(algorithm=method, num_samples=num_samples) assert result.shape == expected_result.shape assert np.allclose(result, expected_result)
def infer_message_passing(model: PairWiseFiniteModel, max_iter=None) -> InferenceResult: """Inference with Message Passing. For acyclic graph returns exact partition function and marginal probabilities. For graph with loops may return good approximation to the true marginal probabilities, but partition function will be a useless number. This is an iterative algorithm which terminates when it converged or when `max_iter` iterations were made. :param model: Pairwise model for which to perform inference. :param max_iter: How many iterations without convergence should happen for algorithm to terminate. Defaults to maximal diameter of connected component. :return: InferenceResult object. Reference [1] Wainwright, Jordan. Graphical Models, Exponential Families, and Variational Inference. 2008. Section 2.5.1 (p. 26). """ if max_iter is None: graph = networkx.Graph() graph.add_edges_from(model.get_edges_connected()) max_iter = networkx.diameter(graph) # Build list of directed edges. edges = model.get_edges_connected() dir_edges = np.concatenate([edges, np.flip(edges, axis=1)]) # Sort edges by end vertex. This ensures that edges ending with the same # vertex are sequential, which allows for efficient lookup. dir_edges.view('i4,i4').sort(order=['f1'], axis=0) # Compact representation of interactions. intrn = model.get_interactions_for_edges(dir_edges) # Main algorithm. lmu = _message_passing(dir_edges, intrn, model.field, max_iter) # Restore partition function for fixed values in nodes. log_marg_pf = np.array(model.field) for edge_id in range(len(dir_edges)): log_marg_pf[dir_edges[edge_id][1], :] += lmu[edge_id] log_pf = scipy.special.logsumexp(log_marg_pf, axis=-1) marg_prob = scipy.special.softmax(log_marg_pf, axis=-1) marg_prob /= np.sum(marg_prob, axis=-1).reshape(-1, 1) return InferenceResult(np.min(log_pf), marg_prob)
def test_get_dfs_result(): model = PairWiseFiniteModel(4, 2) j = np.ones((2, 2)) model.add_interaction(2, 3, j) model.add_interaction(2, 1, j) model.get_dfs_result() # To test cache invalidation. model.add_interaction(1, 0, j) dfs_edges = model.get_dfs_result().dfs_edges assert np.allclose(dfs_edges, np.array([[0, 1], [1, 2], [2, 3]]))
def max_likelihood_tree_dp(model: PairWiseFiniteModel): """Max Likelihood for the pairwise model. Performs dynamic programming on tree. Applicable only if the interaction graph is a tree or a forest. Otherwise throws exception. :param model: Model for which to find most likely state. :return: Most likely state. np.array of ints. """ assert not model.get_dfs_result().had_cycles, "Graph has cycles." field = model.field.astype(np.float64, copy=False) dfs_edges = model.get_dfs_result().dfs_edges ints = model.get_interactions_for_edges(dfs_edges) return _max_likelihood_internal(field, dfs_edges, ints)
def test_inference_all_methods(): # Sanity check that all algorithms work on very simple model. all_methods = [ 'auto', 'bruteforce', 'mean_field', 'message_passing', 'tree_dp', 'path_dp', 'junction_tree' ] model = PairWiseFiniteModel(2, 2) model.add_interaction(0, 1, np.array([[0, 0], [0, 1]])) m = np.array([2, 1 + np.exp(1)]) expected_result = InferenceResult(np.log(3 + np.exp(1)), np.array([m, m]) / np.sum(m)) for method in all_methods: result = model.infer(algorithm=method) assert_results_close(result, expected_result, log_pf_tol=1.0, mp_mse_tol=0.1)
def test_build_from_factors(): model = PairWiseFiniteModel(5, 2) x = model.get_symbolic_variables() # Field. model *= np.exp(5 * x[1]) model *= np.exp(10 * x[2]) model *= DiscreteFactor(model, [3], np.exp([7, 8])) # Interactions. model *= np.exp(2 * x[0] * x[1]) model *= (1 + x[2] + x[3]) model *= np.exp(10 * x[0] * x[1]) # Should accumulate. model *= DiscreteFactor(model, [0, 4], np.exp([[1, 2], [3, 4]])) assert np.allclose(model.field, np.array([[0, 0], [0, 5], [0, 10], [7, 8], [0, 0]])) assert model.edges == [(0, 1), (2, 3), (0, 4)] assert np.allclose(model.get_interaction_matrix(0, 1), [[0, 0], [0, 12]]) assert np.allclose(model.get_interaction_matrix(2, 3), np.log([[1, 2], [2, 3]])) assert np.allclose(model.get_interaction_matrix(0, 4), [[1, 2], [3, 4]])
def test_build_from_interactions(): model = PairWiseFiniteModel(10, 5) j1 = np.random.random(size=(5, 5)) j2 = np.random.random(size=(5, 5)) model.add_interaction(0, 1, j1) model.add_interaction(1, 0, j1) model.add_interaction(1, 2, j2) assert np.allclose(model.field, np.zeros((10, 5))) assert model.edges == [(0, 1), (1, 2)] assert np.allclose(model.get_interaction_matrix(0, 1), j1 + j1.T) assert np.allclose(model.get_interaction_matrix(2, 1), j2.T)
def prepare_path_dp(model: PairWiseFiniteModel) -> PwPathDecomposition: """Prepares model for path decomposition dynamic programming. Builds path decomposition and calculates matrices A describing interactions with layers and matrices B describing interactions between them. :param model: Pairwise model. :return: PwPathDecomposition object with path decomposition and calculated matrices A and B. """ layers = path_decomposition(model.get_graph()) sum_layers_size = sum([len(layer) for layer in layers]) assert sum_layers_size == model.gr_size, "Graph is not connected." max_layer_size = max([len(layer) for layer in layers]) assert model.al_size**max_layer_size <= 1e5, ( "Algorithm won't handle this complexity.") a = [model.get_subgraph_factor_values(layer) for layer in layers] b = [ get_b(model, layers[i], layers[i + 1]) for i in range(len(layers) - 1) ] return PwPathDecomposition(layers=layers, a=a, b=b)
def infer_tree_dp(model: PairWiseFiniteModel, subtree_mp=False) -> InferenceResult: """Inference using DP on tree. Performs dynamic programming on tree. Applicable only if the interaction graph is a tree or a forest. Otherwise throws exception. :param model: Model for which to perform inference. :param subtree_mp: If true, will return marginal probabilities for subtrees, i.e. for each node will return probability of it having different values if we leave only it and its subtree. :return: InferenceResult object. """ assert not model.get_dfs_result().had_cycles, "Graph has cycles." dfs_edges = model.get_dfs_result().dfs_edges dfs_j = model.get_interactions_for_edges(dfs_edges) lz = model.field.astype(dtype=np.float64, copy=True) # log(z) lzc = np.zeros_like(lz) # log(zc) # Log(z_r). z_r is partition function for all tree except subtree of given # vertex, when value of given vertex is fixed. lzr = np.zeros((model.gr_size, model.al_size)) _dfs1(lz, lzc, dfs_edges, dfs_j) log_pf = logsumexp(lz[0, :]) if subtree_mp: return InferenceResult(log_pf, lz) _dfs2(lz, lzc, lzr, dfs_edges, dfs_j) marg_proba = np.exp(lz + lzr - log_pf) return InferenceResult(log_pf, marg_proba)
def test_from_model(): model1 = GenericGraphModel(3) model1[0].domain = DiscreteDomain.binary() model1[2].domain = DiscreteDomain([0, 10]) model1[1].domain = DiscreteDomain.range(3) x0, x1, x2 = model1.get_symbolic_variables() model1.add_factor(np.exp(x0 + x1)) model1.add_factor(np.exp(x1 + x2)) model2 = PairWiseFiniteModel.from_model(model1) assert np.allclose(model2.field, np.zeros((3, 3))) assert model2.edges == [(0, 1), (1, 2)] assert np.allclose( model2.get_all_interactions(), np.array([[[0, 1, 2], [1, 2, 3], [-np.inf, -np.inf, -np.inf]], [[0, 10, -np.inf], [1, 11, -np.inf], [2, 12, -np.inf]]]))
def _compute_all_probs(model: PairWiseFiniteModel) -> np.ndarray: """For all possible states finds their probabilities (not normed).""" if model.al_size**model.gr_size > 2e7: raise TooMuchStatesError() return _compute_all_probs_internal(model.field, model.get_edges_array(), model.get_all_interactions())
def test_encode_state(): model = PairWiseFiniteModel(5, 2) assert model.encode_state([1, 1, 0, 1, 0]) == 11 model = PairWiseFiniteModel(5, 10) assert model.encode_state([5, 4, 3, 2, 1]) == 12345
def test_decode_state(): model = PairWiseFiniteModel(5, 2) assert model.decode_state(11) == [1, 1, 0, 1, 0] model = PairWiseFiniteModel(5, 10) assert model.decode_state(12345) == [5, 4, 3, 2, 1]