In [1]:
#!/usr/bin/env python
#
# Copyright 2017 MIT Lincoln Laboratory, Massachusetts Institute of Technology
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use these files except in compliance with
# the License.
#
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
#
"""
Authors: Steven Smith, Edward Kao
Date: 9 January 2017
Installation: Python 2.7
Description: This Python script performs the baseline graph partition algorithm based on the degree-corrected stochastic block model.
References:
Peixoto, Tiago P. "Entropy of stochastic blockmodel ensembles." Physical Review E 85, no. 5 (2012): 056122.
Peixoto, Tiago P. "Parsimonious module inference in large networks." Physical review letters 110, no. 14 (2013): 148701.
Karrer, Brian, and Mark EJ Newman. "Stochastic blockmodels and community structure in networks." Physical Review E 83, no. 1 (2011): 016107.
"""
Out[1]:
In [2]:
import pandas as pd # for loading input graph TSV files
import numpy as np
from scipy import sparse as sparse
import scipy.misc as misc
from munkres import Munkres # for correctness evaluation
use_timeit = True # for timing runs (optional)
if use_timeit:
import timeit
use_graph_tool_options = True # for visualiziing graph partitions (optional)
if use_graph_tool_options:
import graph_tool.all as gt
A graph is represented as a list of numpy arrays. Each element in the outer array represents the array of vertex neighbors, and each row in the inner arrays has three values representing a directed edge betwee the neighbors (from, to) with edge weight (default to 1).
A vertex label hash is stored from the input triple store file format. Vertices are internally indexed by an integer value from 0 to N–1, where N is the number of vertices.
The truth and output partitions are stored as numpy arrays.
The tsv file is assumed to be of the form "from to [weight]" (tab delimited). If available, the true partition is assumed to be stored in the file filename_truePartition.tsv
. Nodes are assumed to be indexed from 0 to N-1 in the input file.
input_filename : str
input file name not including the .tsv extension
true_partition_available : bool
whether the truth partition is available
strm_piece_num : int, optional
specify which stage of the streaming graph to load
out_neighbors, in_neighbors : list of ndarray, optional
existing graph to add to. This is used when loading the streaming graphs one stage at a time. Note that
the truth partition is loaded all together at once.
out_neighbors : list of ndarray; list length is N, the number of nodes
each element of the list is a ndarray of out neighbors, where the first column is the node indices
and the second column the corresponding edge weights
in_neighbors : list of ndarray; list length is N, the number of nodes
each element of the list is a ndarray of in neighbors, where the first column is the node indices
and the second column the corresponding edge weights
N : int
number of nodes in the graph
E : int
number of edges in the graph
true_b : ndarray (int) optional
array of truth block assignment for each node
In [3]:
def load_graph(input_filename, load_true_partition, strm_piece_num = None, out_neighbors = None, in_neighbors = None):
# read the entire graph CSV into rows of edges
if (strm_piece_num == None):
edge_rows = pd.read_csv('{}.tsv'.format(input_filename), delimiter='\t', header=None).as_matrix()
else:
edge_rows = pd.read_csv('{}_{}.tsv'.format(input_filename, strm_piece_num), delimiter='\t', header=None).as_matrix()
if (out_neighbors == None): # no previously loaded streaming pieces
N = edge_rows[:, 0:2].max() # number of nodes
out_neighbors = [[] for i in range(N)]
in_neighbors = [[] for i in range(N)]
else: # add to previously loaded streaming pieces
N = max(edge_rows[:, 0:2].max(), len(out_neighbors)) # number of nodes
out_neighbors = [list(out_neighbors[i]) for i in range(len(out_neighbors))]
out_neighbors.extend([[] for i in range(N-len(out_neighbors))])
in_neighbors = [list(in_neighbors[i]) for i in range(len(in_neighbors))]
in_neighbors.extend([[] for i in range(N-len(in_neighbors))])
weights_included = edge_rows.shape[1] == 3
# load edges to list of lists of out and in neighbors
for i in range(edge_rows.shape[0]):
if weights_included:
edge_weight = edge_rows[i, 2]
else:
edge_weight = 1
# -1 on the node index since Python is 0-indexed and the standard graph TSV is 1-indexed
out_neighbors[edge_rows[i, 0]-1].append([edge_rows[i, 1]-1, edge_weight])
in_neighbors[edge_rows[i, 1]-1].append([edge_rows[i, 0]-1, edge_weight])
# convert each neighbor list to neighbor numpy arrays for faster access
for i in range(N):
if len(out_neighbors[i]) > 0:
out_neighbors[i] = np.array(out_neighbors[i], dtype=int)
else:
out_neighbors[i] = np.array(out_neighbors[i], dtype=int).reshape((0,2))
for i in range(N):
if len(in_neighbors[i]) > 0:
in_neighbors[i] = np.array(in_neighbors[i], dtype=int)
else:
in_neighbors[i] = np.array(in_neighbors[i], dtype=int).reshape((0,2))
E = sum(len(v) for v in out_neighbors) # number of edges
if load_true_partition:
# read the entire true partition CSV into rows of partitions
true_b_rows = pd.read_csv('{}_truePartition.tsv'.format(input_filename), delimiter='\t', header=None).as_matrix()
true_b = np.ones(true_b_rows.shape[0], dtype=int) * -1 # initialize truth assignment to -1 for 'unknown'
for i in range(true_b_rows.shape[0]):
true_b[true_b_rows[i, 0]-1] = int(true_b_rows[i, 1]-1) # -1 since Python is 0-indexed and the TSV is 1-indexed
if load_true_partition:
return out_neighbors, in_neighbors, N, E, true_b
else:
return out_neighbors, in_neighbors, N, E
This function returns initialized variables for the iterations to find the best partition with the optimal number of blocks
optimal_B_found : bool
flag for whether the optimal block has been found
old_b : list of length 3
holds the best three partitions so far
old_M : list of length 3
holds the edge count matrices for the best three partitions so far
old_d : list of length 3
holds the block degrees for the best three partitions so far
old_d_out : list of length 3
holds the out block degrees for the best three partitions so far
old_d_in : list of length 3
holds the in block degrees for the best three partitions so far
old_S : list of length 3
holds the overall entropy for the best three partitions so far
old_B : list of length 3
holds the number of blocks for the best three partitions so far
graph_object : list
empty for now and will store the graph object if graphs will be visualized
In [4]:
def initialize_partition_variables():
optimal_B_found = False
old_b = [[], [], []] # partition for the high, best, and low number of blocks so far
old_M = [[], [], []] # edge count matrix for the high, best, and low number of blocks so far
old_d = [[], [], []] # block degrees for the high, best, and low number of blocks so far
old_d_out = [[], [], []] # out block degrees for the high, best, and low number of blocks so far
old_d_in = [[], [], []] # in block degrees for the high, best, and low number of blocks so far
old_S = [np.Inf, np.Inf, np.Inf] # overall entropy for the high, best, and low number of blocks so far
old_B = [[], [], []] # number of blocks for the high, best, and low number of blocks so far
graph_object = None
return optimal_B_found, old_b, old_M, old_d, old_d_out, old_d_in, old_S, old_B, graph_object
out_neighbors : list of ndarray; list length is N, the number of nodes
each element of the list is a ndarray of out neighbors, where the first column is the node indices
and the second column the corresponding edge weights
B : int
total number of blocks in the current partition
b : ndarray (int)
array of block assignment for each node
use_sparse : bool
whether the edge count matrix is stored as a sparse matrix
M : ndarray or sparse matrix (int), shape = (#blocks, #blocks)
edge count matrix between all the blocks.
d_out : ndarray (int)
the current out degree of each block
d_in : ndarray (int)
the current in degree of each block
d : ndarray (int)
the current total degree of each block
Compute the edge count matrix and the block degrees from scratch
In [5]:
def initialize_edge_counts(out_neighbors, B, b, use_sparse):
if use_sparse: # store interblock edge counts as a sparse matrix
M = sparse.lil_matrix((B, B), dtype=int)
else:
M = np.zeros((B, B), dtype=int)
# compute the initial interblock edge count
for v in range(len(out_neighbors)):
if len(out_neighbors[v]) > 0:
k1 = b[v]
k2, inverse_idx = np.unique(b[out_neighbors[v][:, 0]], return_inverse=True)
count = np.bincount(inverse_idx, weights=out_neighbors[v][:, 1]).astype(int)
M[k1, k2] += count
# compute initial block degrees
d_out = np.asarray(M.sum(axis=1)).ravel()
d_in = np.asarray(M.sum(axis=0)).ravel()
d = d_out + d_in
return M, d_out, d_in, d
r : int
current block assignment for the node under consideration
neighbors_out : ndarray (int) of two columns
out neighbors array where the first column is the node indices and the second column is the edge weight
neighbors_in : ndarray (int) of two columns
in neighbors array where the first column is the node indices and the second column is the edge weight
b : ndarray (int)
array of block assignment for each node
M : ndarray or sparse matrix (int), shape = (#blocks, #blocks)
edge count matrix between all the blocks.
d : ndarray (int)
total number of edges to and from each block
B : int
total number of blocks
agg_move : bool
whether the proposal is a block move
use_sparse : bool
whether the edge count matrix is stored as a sparse matrix
s : int
proposed block assignment for the node under consideration
k_out : int
the out degree of the node
k_in : int
the in degree of the node
k : int
the total degree of the node
Randomly select a neighbor of the current node, and obtain its block assignment $u$. With probability $\frac{B}{d_u + B}$, randomly propose a block. Otherwise, randomly selects a neighbor to block $u$ and propose its block assignment. For block (agglomerative) moves, avoid proposing the current block.
In [6]:
def propose_new_partition(r, neighbors_out, neighbors_in, b, M, d, B, agg_move, use_sparse):
neighbors = np.concatenate((neighbors_out, neighbors_in))
k_out = sum(neighbors_out[:,1])
k_in = sum(neighbors_in[:,1])
k = k_out + k_in
if k==0: # this node has no neighbor, simply propose a block randomly
s = np.random.randint(B)
return s, k_out, k_in, k
rand_neighbor = np.random.choice(neighbors[:,0], p=neighbors[:,1]/float(k))
u = b[rand_neighbor]
# propose a new block randomly
if np.random.uniform() <= B/float(d[u]+B): # chance inversely prop. to block_degree
if agg_move: # force proposal to be different from current block
candidates = set(range(B))
candidates.discard(r)
s = np.random.choice(list(candidates))
else:
s = np.random.randint(B)
else: # propose by random draw from neighbors of block partition[rand_neighbor]
if use_sparse:
multinomial_prob = (M[u, :].toarray().transpose() + M[:, u].toarray()) / float(d[u])
else:
multinomial_prob = (M[u, :].transpose() + M[:, u]) / float(d[u])
if agg_move: # force proposal to be different from current block
multinomial_prob[r] = 0
if multinomial_prob.sum() == 0: # the current block has no neighbors. randomly propose a different block
candidates = set(range(B))
candidates.discard(r)
s = np.random.choice(list(candidates))
return s, k_out, k_in, k
else:
multinomial_prob = multinomial_prob / multinomial_prob.sum()
candidates = multinomial_prob.nonzero()[0]
s = candidates[np.flatnonzero(np.random.multinomial(1, multinomial_prob[candidates].ravel()))[0]]
return s, k_out, k_in, k
M : ndarray or sparse matrix (int), shape = (#blocks, #blocks)
edge count matrix between all the blocks.
r : int
current block assignment for the node under consideration
s : int
proposed block assignment for the node under consideration
b_out : ndarray (int)
blocks of the out neighbors
count_out : ndarray (int)
edge counts to the out neighbor blocks
b_in : ndarray (int)
blocks of the in neighbors
count_in : ndarray (int)
edge counts to the in neighbor blocks
count_self : int
edge counts to self
agg_move : bool
whether the proposal is a block move
use_sparse : bool
whether the edge count matrix is stored as a sparse matrix
M_r_row : ndarray or sparse matrix (int)
the current block row of the new edge count matrix under proposal
M_s_row : ndarray or sparse matrix (int)
the proposed block row of the new edge count matrix under proposal
M_r_col : ndarray or sparse matrix (int)
the current block col of the new edge count matrix under proposal
M_s_col : ndarray or sparse matrix (int)
the proposed block col of the new edge count matrix under proposal
The updates only involve changing the entries to and from the neighboring blocks
In [7]:
def compute_new_rows_cols_interblock_edge_count_matrix(M, r, s, b_out, count_out, b_in, count_in, count_self, agg_move, use_sparse):
B = M.shape[0]
if agg_move: # the r row and column are simply empty after this merge move
if use_sparse:
M_r_row = sparse.lil_matrix(M[r, :].shape, dtype=int)
M_r_col = sparse.lil_matrix(M[:, r].shape, dtype=int)
else:
M_r_row = np.zeros((1, B), dtype=int)
M_r_col = np.zeros((B, 1), dtype=int)
else:
if use_sparse:
M_r_row = M[r, :].copy()
M_r_col = M[:, r].copy()
else:
M_r_row = M[r, :].copy().reshape(1, B)
M_r_col = M[:, r].copy().reshape(B, 1)
M_r_row[0, b_out] -= count_out
M_r_row[0, r] -= np.sum(count_in[np.where(b_in == r)])
M_r_row[0, s] += np.sum(count_in[np.where(b_in == r)])
M_r_col[b_in, 0] -= count_in.reshape(M_r_col[b_in, 0].shape)
M_r_col[r, 0] -= np.sum(count_out[np.where(b_out == r)])
M_r_col[s, 0] += np.sum(count_out[np.where(b_out == r)])
if use_sparse:
M_s_row = M[s, :].copy()
M_s_col = M[:, s].copy()
else:
M_s_row = M[s, :].copy().reshape(1, B)
M_s_col = M[:, s].copy().reshape(B, 1)
M_s_row[0, b_out] += count_out
M_s_row[0, r] -= np.sum(count_in[np.where(b_in == s)])
M_s_row[0, s] += np.sum(count_in[np.where(b_in == s)])
M_s_row[0, r] -= count_self
M_s_row[0, s] += count_self
M_s_col[b_in, 0] += count_in.reshape(M_s_col[b_in, 0].shape)
M_s_col[r, 0] -= np.sum(count_out[np.where(b_out == s)])
M_s_col[s, 0] += np.sum(count_out[np.where(b_out == s)])
M_s_col[r, 0] -= count_self
M_s_col[s, 0] += count_self
return M_r_row, M_s_row, M_r_col, M_s_col
r : int
current block assignment for the node under consideration
s : int
proposed block assignment for the node under consideration
d_out : ndarray (int)
the current out degree of each block
d_in : ndarray (int)
the current in degree of each block
d : ndarray (int)
the current total degree of each block
k_out : int
the out degree of the node
k_in : int
the in degree of the node
k : int
the total degree of the node
d_out_new : ndarray (int)
the new out degree of each block under proposal
d_in_new : ndarray (int)
the new in degree of each block under proposal
d_new : ndarray (int)
the new total degree of each block under proposal
The updates only involve changing the degrees of the current and proposed block
In [8]:
def compute_new_block_degrees(r, s, d_out, d_in, d, k_out, k_in, k):
new = []
for old, degree in zip([d_out, d_in, d], [k_out, k_in, k]):
new_d = old.copy()
new_d[r] -= degree
new_d[s] += degree
new.append(new_d)
return new
b_out : ndarray (int)
blocks of the out neighbors
count_out : ndarray (int)
edge counts to the out neighbor blocks
b_in : ndarray (int)
blocks of the in neighbors
count_in : ndarray (int)
edge counts to the in neighbor blocks
s : int
proposed block assignment for the node under consideration
M : ndarray or sparse matrix (int), shape = (#blocks, #blocks)
edge count matrix between all the blocks.
M_r_row : ndarray or sparse matrix (int)
the current block row of the new edge count matrix under proposal
M_r_col : ndarray or sparse matrix (int)
the current block col of the new edge count matrix under proposal
B : int
total number of blocks
d : ndarray (int)
total number of edges to and from each block
d_new : ndarray (int)
new block degrees under the proposal
use_sparse : bool
whether the edge count matrix is stored as a sparse matrix
Hastings_correction : float
term that corrects for the transition asymmetry between the current block and the proposed block
The Hastings correction is:
$\huge \frac{p_{i, s \rightarrow r}}{p_{i, r \rightarrow s}}$
where
$\Large p_{i, r \rightarrow s} = \sum_{t \in \{\mathbf{b}_{\mathcal{N}_i}^-\}} \left[ {\frac{k_{i,t}}{k_i} \frac{M_{ts}^- + M_{st}^- + 1}{d^-_t+B}}\right]$
$\Large p_{i, s \rightarrow r} = \sum_{t \in \{\mathbf{b}_{\mathcal{N}_i}^-\}} \left[ {\frac{k_{i,t}}{k_i} \frac{M_{tr}^+ + M_{rt}^+ +1}{d_t^++B}}\right]$
summed over all the neighboring blocks $t$
In [9]:
def compute_Hastings_correction(b_out, count_out, b_in, count_in, s, M, M_r_row, M_r_col, B, d, d_new, use_sparse):
t, idx = np.unique(np.append(b_out, b_in), return_inverse=True) # find all the neighboring blocks
count = np.bincount(idx, weights=np.append(count_out, count_in)).astype(int) # count edges to neighboring blocks
if use_sparse:
M_t_s = M[t, s].toarray().ravel()
M_s_t = M[s, t].toarray().ravel()
M_r_row = M_r_row[0, t].toarray().ravel()
M_r_col = M_r_col[t, 0].toarray().ravel()
else:
M_t_s = M[t, s].ravel()
M_s_t = M[s, t].ravel()
M_r_row = M_r_row[0, t].ravel()
M_r_col = M_r_col[t, 0].ravel()
p_forward = np.sum(count*(M_t_s + M_s_t + 1) / (d[t] + float(B)))
p_backward = np.sum(count*(M_r_row + M_r_col + 1) / (d_new[t] + float(B)))
return p_backward / p_forward
Reduction in entropy means the proposed block is better than the current block.
r : int
current block assignment for the node under consideration
s : int
proposed block assignment for the node under consideration
M : ndarray or sparse matrix (int), shape = (#blocks, #blocks)
edge count matrix between all the blocks.
M_r_row : ndarray or sparse matrix (int)
the current block row of the new edge count matrix under proposal
M_s_row : ndarray or sparse matrix (int)
the proposed block row of the new edge count matrix under proposal
M_r_col : ndarray or sparse matrix (int)
the current block col of the new edge count matrix under proposal
M_s_col : ndarray or sparse matrix (int)
the proposed block col of the new edge count matrix under proposal
d_out : ndarray (int)
the current out degree of each block
d_in : ndarray (int)
the current in degree of each block
d_out_new : ndarray (int)
the new out degree of each block under proposal
d_in_new : ndarray (int)
the new in degree of each block under proposal
use_sparse : bool
whether the edge count matrix is stored as a sparse matrix
delta_entropy : float
entropy under the proposal minus the current entropy
The difference in entropy is computed as:
$\large \Delta S = \sum_{t_1, t_2} {\left[ -M_{t_1 t_2}^+ \log\left(\frac{M_{t_1 t_2}^+}{d_{t_1, \rm out}^+ d_{t_2, \rm in}^+}\right) + M_{t_1 t_2}^- \log\left(\frac{M_{t_1 t_2}^-}{d_{t_1, \rm out}^- d_{t_2, \rm in}^-}\right)\right]}$
where the sum runs over all entries $(t_1, t_2)$ in rows and cols $r$ and $s$ of the edge count matrix
In [10]:
def compute_delta_entropy(r, s, M, M_r_row, M_s_row, M_r_col, M_s_col, d_out, d_in, d_out_new, d_in_new, use_sparse):
if use_sparse: # computation in the sparse matrix is slow so convert to numpy arrays since operations are on only two rows and cols
M_r_row = M_r_row.toarray()
M_s_row = M_s_row.toarray()
M_r_col = M_r_col.toarray()
M_s_col = M_s_col.toarray()
M_r_t1 = M[r, :].toarray()
M_s_t1 = M[s, :].toarray()
M_t2_r = M[:, r].toarray()
M_t2_s = M[:, s].toarray()
else:
M_r_t1 = M[r, :]
M_s_t1 = M[s, :]
M_t2_r = M[:, r]
M_t2_s = M[:, s]
# remove r and s from the cols to avoid double counting
idx = range(len(d_in_new))
del idx[max(r,s)]
del idx[min(r,s)]
M_r_col = M_r_col[idx]
M_s_col = M_s_col[idx]
M_t2_r = M_t2_r[idx]
M_t2_s = M_t2_s[idx]
d_out_new_ = d_out_new[idx]
d_out_ = d_out[idx]
# only keep non-zero entries to avoid unnecessary computation
d_in_new_r_row = d_in_new[M_r_row.ravel().nonzero()]
d_in_new_s_row = d_in_new[M_s_row.ravel().nonzero()]
M_r_row = M_r_row[M_r_row.nonzero()]
M_s_row = M_s_row[M_s_row.nonzero()]
d_out_new_r_col = d_out_new_[M_r_col.ravel().nonzero()]
d_out_new_s_col = d_out_new_[M_s_col.ravel().nonzero()]
M_r_col = M_r_col[M_r_col.nonzero()]
M_s_col = M_s_col[M_s_col.nonzero()]
d_in_r_t1 = d_in[M_r_t1.ravel().nonzero()]
d_in_s_t1 = d_in[M_s_t1.ravel().nonzero()]
M_r_t1= M_r_t1[M_r_t1.nonzero()]
M_s_t1 = M_s_t1[M_s_t1.nonzero()]
d_out_r_col = d_out_[M_t2_r.ravel().nonzero()]
d_out_s_col = d_out_[M_t2_s.ravel().nonzero()]
M_t2_r = M_t2_r[M_t2_r.nonzero()]
M_t2_s = M_t2_s[M_t2_s.nonzero()]
# sum over the two changed rows and cols
delta_entropy = 0
delta_entropy -= np.sum(M_r_row * np.log(M_r_row.astype(float) / d_in_new_r_row / d_out_new[r]))
delta_entropy -= np.sum(M_s_row * np.log(M_s_row.astype(float) / d_in_new_s_row / d_out_new[s]))
delta_entropy -= np.sum(M_r_col * np.log(M_r_col.astype(float) / d_out_new_r_col / d_in_new[r]))
delta_entropy -= np.sum(M_s_col * np.log(M_s_col.astype(float) / d_out_new_s_col / d_in_new[s]))
delta_entropy += np.sum(M_r_t1 * np.log(M_r_t1.astype(float) / d_in_r_t1 / d_out[r]))
delta_entropy += np.sum(M_s_t1 * np.log(M_s_t1.astype(float) / d_in_s_t1 / d_out[s]))
delta_entropy += np.sum(M_t2_r * np.log(M_t2_r.astype(float) / d_out_r_col / d_in[r]))
delta_entropy += np.sum(M_t2_s * np.log(M_t2_s.astype(float) / d_out_s_col / d_in[s]))
return delta_entropy
delta_entropy_for_each_block : ndarray (float)
the delta entropy for merging each block
best_merge_for_each_block : ndarray (int)
the best block to merge with for each block
b : ndarray (int)
array of block assignment for each node
B : int
total number of blocks in the current partition
B_to_merge : int
the number of blocks to merge
b : ndarray (int)
array of new block assignment for each node after the merge
B : int
total number of blocks after the merge
In [11]:
def carry_out_best_merges(delta_entropy_for_each_block, best_merge_for_each_block, b, B, B_to_merge):
bestMerges = delta_entropy_for_each_block.argsort()
block_map = np.arange(B)
num_merge = 0
counter = 0
while num_merge < B_to_merge:
mergeFrom = bestMerges[counter]
mergeTo = block_map[best_merge_for_each_block[bestMerges[counter]]]
counter += 1
if mergeTo != mergeFrom:
block_map[np.where(block_map == mergeFrom)] = mergeTo
b[np.where(b == mergeFrom)] = mergeTo
num_merge += 1
remaining_blocks = np.unique(b)
mapping = -np.ones(B, dtype=int)
mapping[remaining_blocks] = np.arange(len(remaining_blocks))
b = mapping[b]
B -= B_to_merge
return b, B
b : ndarray (int)
current array of new block assignment for each node
ni : int
current node index
r : int
current block assignment for the node under consideration
s : int
proposed block assignment for the node under consideration
M : ndarray or sparse matrix (int), shape = (#blocks, #blocks)
edge count matrix between all the blocks.
M_r_row : ndarray or sparse matrix (int)
the current block row of the new edge count matrix under proposal
M_s_row : ndarray or sparse matrix (int)
the proposed block row of the new edge count matrix under proposal
M_r_col : ndarray or sparse matrix (int)
the current block col of the new edge count matrix under proposal
M_s_col : ndarray or sparse matrix (int)
the proposed block col of the new edge count matrix under proposal
d_out_new : ndarray (int)
the new out degree of each block under proposal
d_in_new : ndarray (int)
the new in degree of each block under proposal
d_new : ndarray (int)
the new total degree of each block under proposal
use_sparse : bool
whether the edge count matrix is stored as a sparse matrix
b : ndarray (int)
array of block assignment for each node after the move
M : ndarray or sparse matrix (int), shape = (#blocks, #blocks)
edge count matrix between all the blocks after the move
d_out_new : ndarray (int)
the out degree of each block after the move
d_in_new : ndarray (int)
the in degree of each block after the move
d_new : ndarray (int)
the total degree of each block after the move
In [12]:
def update_partition(b, ni, r, s, M, M_r_row, M_s_row, M_r_col, M_s_col, d_out_new, d_in_new, d_new, use_sparse):
b[ni] = s
M[r, :] = M_r_row
M[s, :] = M_s_row
if use_sparse:
M[:, r] = M_r_col
M[:, s] = M_s_col
else:
M[:, r] = M_r_col.reshape(M[:, r].shape)
M[:, s] = M_s_col.reshape(M[:, s].shape)
return b, M, d_out_new, d_in_new, d_new
Compute the overall entropy, including the model entropy as well as the data entropy, on the current partition. The best partition with an optimal number of blocks will minimize this entropy.
M : ndarray or sparse matrix (int), shape = (#blocks, #blocks)
edge count matrix between all the blocks.
d_out : ndarray (int)
the current out degrees of each block
d_in : ndarray (int)
the current in degrees of each block
B : int
the number of blocks in the partition
N : int
number of nodes in the graph
E : int
number of edges in the graph
use_sparse : bool
whether the edge count matrix is stored as a sparse matrix
S : float
the overall entropy of the current partition
The overall entropy of the partition is computed as:
$\large S = E\;h\left(\frac{B^2}{E}\right) + N \log(B) - \sum_{t_1, t_2} {M_{t_1 t_2} \log\left(\frac{M_{t_1 t_2}}{d_{t_1, \rm out} d_{t_2, \rm in}}\right)} + C$
where the function $h(x)=(1+x)\log(1+x) - x\log(x)$ and the sum runs over all entries $(t_1, t_2)$ in the edge count matrix
In [13]:
def compute_overall_entropy(M, d_out, d_in, B, N, E, use_sparse):
nonzeros = M.nonzero() # all non-zero entries
edge_count_entries = M[nonzeros[0], nonzeros[1]]
if use_sparse:
edge_count_entries = edge_count_entries.toarray()
entries = edge_count_entries * np.log(edge_count_entries / (d_out[nonzeros[0]] * d_in[nonzeros[1]]).astype(float))
data_S = -np.sum(entries)
model_S_term = B**2 / float(E)
model_S = E * (1 + model_S_term) * np.log(1 + model_S_term) - model_S_term * np.log(model_S_term) + N*np.log(B)
S = model_S + data_S
return S
This function checks to see whether the current partition has the optimal number of blocks. If not, the next number of blocks to try is determined and the intermediate variables prepared.
S : float
the overall entropy of the current partition
b : ndarray (int)
current array of block assignment for each node
M : ndarray or sparse matrix (int), shape = (#blocks, #blocks)
edge count matrix between all the blocks.
d : ndarray (int)
the current total degree of each block
d_out : ndarray (int)
the current out degree of each block
d_in : ndarray (int)
the current in degree of each block
B : int
the number of blocks in the current partition
old_b : list of length 3
holds the best three partitions so far
old_M : list of length 3
holds the edge count matrices for the best three partitions so far
old_d : list of length 3
holds the block degrees for the best three partitions so far
old_d_out : list of length 3
holds the out block degrees for the best three partitions so far
old_d_in : list of length 3
holds the in block degrees for the best three partitions so far
old_S : list of length 3
holds the overall entropy for the best three partitions so far
old_B : list of length 3
holds the number of blocks for the best three partitions so far
B_rate : float
the ratio on the number of blocks to reduce before the golden ratio bracket is established
b : ndarray (int)
starting array of block assignment on each node for the next number of blocks to try
M : ndarray or sparse matrix (int), shape = (#blocks, #blocks)
starting edge count matrix for the next number of blocks to try
d : ndarray (int)
the starting total degree of each block for the next number of blocks to try
d_out : ndarray (int)
the starting out degree of each block for the next number of blocks to try
d_in : ndarray (int)
the starting in degree of each block for the next number of blocks to try
B : int
the starting number of blocks before the next block merge
B_to_merge : int
number of blocks to merge next
old_b : list of length 3
holds the best three partitions including the current partition
old_M : list of length 3
holds the edge count matrices for the best three partitions including the current partition
old_d : list of length 3
holds the block degrees for the best three partitions including the current partition
old_d_out : list of length 3
holds the out block degrees for the best three partitions including the current partition
old_d_in : list of length 3
holds the in block degrees for the best three partitions including the current partition
old_S : list of length 3
holds the overall entropy for the best three partitions including the current partition
old_B : list of length 3
holds the number of blocks for the best three partitions including the current partition
optimal_B_found : bool
flag for whether the optimal block has been found
The holders for the best three partitions so far and their statistics will be stored in the order of the number of blocks, starting from the highest to the lowest. The middle entry is always the best so far. The number of blocks is reduced by a fixed rate until the golden ratio bracket (three best partitions with the middle one being the best) is established. Once the golden ratio bracket is established, perform golden ratio search until the bracket is narrowed to consecutive number of blocks where the middle one is identified as the optimal number of blocks.
In [14]:
def prepare_for_partition_on_next_num_blocks(S, b, M, d, d_out, d_in, B, old_b, old_M, old_d, old_d_out, old_d_in, old_S, old_B, B_rate):
optimal_B_found = False
B_to_merge = 0
# update the best three partitions so far and their statistics
if S <= old_S[1]: # if the current partition is the best so far
# if the current number of blocks is smaller than the previous best number of blocks
old_index = 0 if old_B[1] > B else 2
old_b[old_index] = old_b[1]
old_M[old_index] = old_M[1]
old_d[old_index] = old_d[1]
old_d_out[old_index] = old_d_out[1]
old_d_in[old_index] = old_d_in[1]
old_S[old_index] = old_S[1]
old_B[old_index] = old_B[1]
index = 1
else: # the current partition is not the best so far
# if the current number of blocks is smaller than the best number of blocks so far
index = 2 if old_B[1] > B else 0
old_b[index] = b
old_M[index] = M
old_d[index] = d
old_d_out[index] = d_out
old_d_in[index] = d_in
old_S[index] = S
old_B[index] = B
# find the next number of blocks to try using golden ratio bisection
if old_S[2] == np.Inf: # if the three points in the golden ratio bracket has not yet been established
B_to_merge = int(B*B_rate)
if (B_to_merge==0): # not enough number of blocks to merge so done
optimal_B_found = True
b = old_b[1].copy()
M = old_M[1].copy()
d = old_d[1].copy()
d_out = old_d_out[1].copy()
d_in = old_d_in[1].copy()
else: # golden ratio search bracket established
if old_B[0] - old_B[2] == 2: # we have found the partition with the optimal number of blocks
optimal_B_found = True
B = old_B[1]
b = old_b[1]
else: # not done yet, find the next number of block to try according to the golden ratio search
if (old_B[0]-old_B[1]) >= (old_B[1]-old_B[2]): # the higher segment in the bracket is bigger
index = 0
else: # the lower segment in the bracket is bigger
index = 1
next_B_to_try = old_B[index + 1] + np.round((old_B[index] - old_B[index + 1]) * 0.618).astype(int)
B_to_merge = old_B[index] - next_B_to_try
B = old_B[index]
b = old_b[index].copy()
M = old_M[index].copy()
d = old_d[index].copy()
d_out = old_d_out[index].copy()
d_in = old_d_in[index].copy()
return b, M, d, d_out, d_in, B, B_to_merge, old_b, old_M, old_d, old_d_out, old_d_in, old_S, old_B, optimal_B_found
out_neighbors : list of ndarray; list length is N, the number of nodes
each element of the list is a ndarray of out neighbors, where the first column is the node indices
and the second column the corresponding edge weights
b : ndarray (int)
array of block assignment for each node
graph_object : graph tool object, optional
if a graph object already exists, use it to plot the graph
pos : ndarray (float) shape = (#nodes, 2), optional
if node positions are given, plot the graph using them
graph_object : graph tool object
the graph tool object containing the graph and the node position info
In [15]:
def plot_graph_with_partition(out_neighbors, b, graph_object = None, pos = None):
if len(out_neighbors) <= 5000:
if graph_object is None:
graph_object = gt.Graph()
edge_list = [(i,j) for i in range(len(out_neighbors)) if len(out_neighbors[i])>0 for j in out_neighbors[i][:,0]]
graph_object.add_edge_list(edge_list)
if pos is None:
graph_object.vp['pos'] = gt.sfdp_layout(graph_object)
else:
graph_object.vp['pos'] = graph_object.new_vertex_property("vector<float>")
for v in graph_object.vertices():
graph_object.vp['pos'][v] = pos[graph_object.vertex_index[v],:]
block_membership = graph_object.new_vertex_property("int")
vertex_shape = graph_object.new_vertex_property("int")
block_membership.a = b[0:len(out_neighbors)]
vertex_shape.a = np.mod(block_membership.a,10)
gt.graph_draw(graph_object, inline=True, output_size=(400,400), pos=graph_object.vp['pos'], vertex_shape=vertex_shape,
vertex_fill_color=block_membership, edge_pen_width=0.07, edge_marker_size=1, vertex_size = 10)
else:
print('That\'s a big graph!')
return graph_object
Compare the algorithm output partition against the truth partition, using only the nodes that have known truth block assignment.
true_b : ndarray (int)
array of truth block assignment for each node. If the truth block is not known for a node, -1 is used
to indicate unknown blocks.
alg_b : ndarray (int)
array of output block assignment for each node. The length of this array corresponds to the number of
nodes observed and processed so far.
In [16]:
def evaluate_partition(true_b, alg_b):
blocks_b1 = true_b
blocks_b1_set = set(true_b)
blocks_b1_set.discard(-1) # -1 is the label for 'unknown'
B_b1 = len(blocks_b1_set)
blocks_b2 = alg_b
B_b2 = max(blocks_b2) + 1
print('\nPartition Correctness Evaluation\n')
print('Number of nodes: {}'.format(len(alg_b)))
print('Number of partitions in truth partition: {}'.format(B_b1))
print('Number of partitions in alg. partition: {}'.format(B_b2))
# populate the confusion matrix between the two partitions
contingency_table = np.zeros((B_b1, B_b2))
for i in range(len(alg_b)): # evaluation based on nodes observed so far
if true_b[i] != -1: # do not include nodes without truth in the evaluation
contingency_table[blocks_b1[i], blocks_b2[i]] += 1
N = contingency_table.sum()
# associate the labels between two partitions using linear assignment
assignment = Munkres() # use the Hungarian algorithm / Kuhn-Munkres algorithm
if B_b1 > B_b2: # transpose matrix for linear assignment (this implementation assumes #col >= #row)
contingency_table = contingency_table.transpose()
indexes = assignment.compute(-contingency_table)
total = 0
contingency_table_before_assignment = np.array(contingency_table)
for row, column in indexes:
contingency_table[:, row] = contingency_table_before_assignment[:, column]
total += contingency_table[row, row]
# fill in the un-associated columns
unassociated_col = set(range(contingency_table.shape[1])) - set(np.array(indexes)[:, 1])
counter = 0;
for column in unassociated_col:
contingency_table[:, contingency_table.shape[0] + counter] = contingency_table_before_assignment[:, column]
counter += 1
if B_b1 > B_b2: # transpose back
contingency_table = contingency_table.transpose()
print('Contingency Table: \n{}'.format(contingency_table))
joint_prob = contingency_table / sum(
sum(contingency_table)) # joint probability of the two partitions is just the normalized contingency table
accuracy = sum(joint_prob.diagonal())
print('Accuracy (with optimal partition matching): {}'.format(accuracy))
print('\n')
# Compute pair-counting-based metrics
def nchoose2(a):
return misc.comb(a, 2)
num_pairs = nchoose2(N)
colsum = np.sum(contingency_table, axis=0)
rowsum = np.sum(contingency_table, axis=1)
# compute counts of agreements and disagreement (4 types) and the regular rand index
sum_table_squared = sum(sum(contingency_table ** 2))
sum_colsum_squared = sum(colsum ** 2)
sum_rowsum_squared = sum(rowsum ** 2)
count_in_each_b1 = np.sum(contingency_table, axis=1)
count_in_each_b2 = np.sum(contingency_table, axis=0)
num_same_in_b1 = sum(count_in_each_b1 * (count_in_each_b1 - 1)) / 2
num_same_in_b2 = sum(count_in_each_b2 * (count_in_each_b2 - 1)) / 2
num_agreement_same = 0.5 * sum(sum(contingency_table * (contingency_table - 1)));
num_agreement_diff = 0.5 * (N ** 2 + sum_table_squared - sum_colsum_squared - sum_rowsum_squared);
num_agreement = num_agreement_same + num_agreement_diff
rand_index = num_agreement / num_pairs
vectorized_nchoose2 = np.vectorize(nchoose2)
sum_table_choose_2 = sum(sum(vectorized_nchoose2(contingency_table)))
sum_colsum_choose_2 = sum(vectorized_nchoose2(colsum))
sum_rowsum_choose_2 = sum(vectorized_nchoose2(rowsum))
adjusted_rand_index = (sum_table_choose_2 - sum_rowsum_choose_2 * sum_colsum_choose_2 / num_pairs) / (
0.5 * (sum_rowsum_choose_2 + sum_colsum_choose_2) - sum_rowsum_choose_2 * sum_colsum_choose_2 / num_pairs)
print('Rand Index: {}'.format(rand_index))
print('Adjusted Rand Index: {}'.format(adjusted_rand_index))
print('Pairwise Recall: {}'.format(num_agreement_same / (num_same_in_b1)))
print('Pairwise Precision: {}'.format(num_agreement_same / (num_same_in_b2)))
print('\n')
# compute the information theoretic metrics
marginal_prob_b2 = np.sum(joint_prob, 0)
marginal_prob_b1 = np.sum(joint_prob, 1)
idx1 = np.nonzero(marginal_prob_b1)
idx2 = np.nonzero(marginal_prob_b2)
conditional_prob_b2_b1 = np.zeros(joint_prob.shape)
conditional_prob_b1_b2 = np.zeros(joint_prob.shape)
conditional_prob_b2_b1[idx1,:] = joint_prob[idx1,:] / marginal_prob_b1[idx1, None]
conditional_prob_b1_b2[:,idx2] = joint_prob[:,idx2] / marginal_prob_b2[None, idx2]
# compute entropy of the non-partition2 and the partition2 version
H_b2 = -np.sum(marginal_prob_b2[idx2] * np.log(marginal_prob_b2[idx2]))
H_b1 = -np.sum(marginal_prob_b1[idx1] * np.log(marginal_prob_b1[idx1]))
# compute the conditional entropies
idx = np.nonzero(joint_prob)
H_b2_b1 = -np.sum(np.sum(joint_prob[idx] * np.log(conditional_prob_b2_b1[idx])))
H_b1_b2 = -np.sum(np.sum(joint_prob[idx] * np.log(conditional_prob_b1_b2[idx])))
# compute the mutual information (symmetric)
marginal_prod = np.dot(marginal_prob_b1[:, None], np.transpose(marginal_prob_b2[:, None]))
MI_b1_b2 = np.sum(np.sum(joint_prob[idx] * np.log(joint_prob[idx] / marginal_prod[idx])))
if H_b1>0:
fraction_missed_info = H_b1_b2 / H_b1
else:
fraction_missed_info = 0
if H_b2>0:
fraction_err_info = H_b2_b1 / H_b2
else:
fraction_err_info = 0
print('Entropy of truth partition: {}'.format(abs(H_b1)))
print('Entropy of alg. partition: {}'.format(abs(H_b2)))
print('Conditional entropy of truth partition given alg. partition: {}'.format(abs(H_b1_b2)))
print('Conditional entropy of alg. partition given truth partition: {}'.format(abs(H_b2_b1)))
print('Mututal informationion between truth partition and alg. partition: {}'.format(abs(MI_b1_b2)))
print('Fraction of missed information: {}'.format(abs(fraction_missed_info)))
print('Fraction of erroneous information: {}'.format(abs(fraction_err_info)))
In [17]:
def main():
input_filename = '../../data/static/simulated_blockmodel_graph_500_nodes'
true_partition_available = True
visualize_graph = True # whether to plot the graph layout colored with intermediate partitions
verbose = True # whether to print updates of the partitioning
out_neighbors, in_neighbors, N, E, true_partition = load_graph(input_filename, true_partition_available)
if verbose:
print('Number of nodes: {}'.format(N))
print('Number of edges: {}'.format(E))
if use_timeit:
t0 = timeit.default_timer()
# initialize by putting each node in its own block (N blocks)
num_blocks = N
partition = np.array(range(num_blocks))
# partition update parameters
beta = 3 # exploitation versus exploration (higher value favors exploitation)
use_sparse_matrix = False # whether to represent the edge count matrix using sparse matrix
# Scipy's sparse matrix is slow but this may be necessary for large graphs
# agglomerative partition update parameters
num_agg_proposals_per_block = 10 # number of proposals per block
num_block_reduction_rate = 0.5 # fraction of blocks to reduce until the golden ratio bracket is established
# nodal partition updates parameters
max_num_nodal_itr = 100 # maximum number of iterations
delta_entropy_threshold1 = 5e-4 # stop iterating when the change in entropy falls below this fraction of the overall entropy
# lowering this threshold results in more nodal update iterations and likely better performance, but longer runtime
delta_entropy_threshold2 = 1e-4 # threshold after the golden ratio bracket is established (typically lower to fine-tune to partition)
delta_entropy_moving_avg_window = 3 # width of the moving average window for the delta entropy convergence criterion
# initialize edge counts and block degrees
interblock_edge_count, block_degrees_out, block_degrees_in, block_degrees = initialize_edge_counts(out_neighbors, num_blocks, partition, use_sparse_matrix)
# initialize items before iterations to find the partition with the optimal number of blocks
optimal_num_blocks_found, old_partition, old_interblock_edge_count, old_block_degrees, old_block_degrees_out, old_block_degrees_in, old_overall_entropy, old_num_blocks, graph_object = initialize_partition_variables()
num_blocks_to_merge = int(num_blocks*num_block_reduction_rate)
# begin partitioning by finding the best partition with the optimal number of blocks
while not optimal_num_blocks_found:
# begin agglomerative partition updates (i.e. block merging)
if verbose:
print("\nMerging down blocks from {} to {}".format(num_blocks, num_blocks - num_blocks_to_merge))
best_merge_for_each_block = np.ones(num_blocks, dtype = int)*-1 # initialize to no merge
delta_entropy_for_each_block = np.ones(num_blocks)*np.Inf # initialize criterion
block_partition = range(num_blocks)
for current_block in range(num_blocks): # evalaute agglomerative updates for each block
for proposal_idx in range(num_agg_proposals_per_block):
# populate edges to neighboring blocks
if use_sparse_matrix:
out_blocks = interblock_edge_count[current_block,:].nonzero()[1]
out_blocks = np.hstack((out_blocks.reshape([len(out_blocks),1]), interblock_edge_count[current_block,out_blocks].toarray().transpose()))
else:
out_blocks = interblock_edge_count[current_block,:].nonzero()
out_blocks = np.hstack((np.array(out_blocks).transpose(), interblock_edge_count[current_block,out_blocks].transpose()))
if use_sparse_matrix:
in_blocks = interblock_edge_count[:,current_block].nonzero()[0]
in_blocks = np.hstack((in_blocks.reshape([len(in_blocks),1]), interblock_edge_count[in_blocks,current_block].toarray()))
else:
in_blocks = interblock_edge_count[:,current_block].nonzero()
in_blocks = np.hstack((np.array(in_blocks).transpose(), interblock_edge_count[in_blocks,current_block].transpose()))
# propose a new block to merge with
proposal, num_out_neighbor_edges, num_in_neighbor_edges, num_neighbor_edges = propose_new_partition(current_block, out_blocks, in_blocks, block_partition, interblock_edge_count, block_degrees, num_blocks, 1, use_sparse_matrix)
# compute the two new rows and columns of the interblock edge count matrix
new_interblock_edge_count_current_block_row, new_interblock_edge_count_new_block_row, new_interblock_edge_count_current_block_col, new_interblock_edge_count_new_block_col = \
compute_new_rows_cols_interblock_edge_count_matrix(interblock_edge_count, current_block, proposal, out_blocks[:,0], out_blocks[:,1], in_blocks[:,0], in_blocks[:,1], interblock_edge_count[current_block, current_block], 1, use_sparse_matrix)
# compute new block degrees
block_degrees_out_new, block_degrees_in_new, block_degrees_new = compute_new_block_degrees(current_block, proposal, block_degrees_out, block_degrees_in, block_degrees, num_out_neighbor_edges, num_in_neighbor_edges, num_neighbor_edges)
# compute change in entropy / posterior
delta_entropy = compute_delta_entropy(current_block, proposal, interblock_edge_count, new_interblock_edge_count_current_block_row, new_interblock_edge_count_new_block_row, new_interblock_edge_count_current_block_col, new_interblock_edge_count_new_block_col, block_degrees_out, block_degrees_in, block_degrees_out_new, block_degrees_in_new, use_sparse_matrix)
if delta_entropy < delta_entropy_for_each_block[current_block]: # a better block candidate was found
best_merge_for_each_block[current_block] = proposal
delta_entropy_for_each_block[current_block] = delta_entropy
# carry out the best merges
partition, num_blocks = carry_out_best_merges(delta_entropy_for_each_block, best_merge_for_each_block, partition, num_blocks, num_blocks_to_merge)
# re-initialize edge counts and block degrees
interblock_edge_count, block_degrees_out, block_degrees_in, block_degrees = initialize_edge_counts(out_neighbors, num_blocks, partition, use_sparse_matrix)
# perform nodal partition updates
if verbose:
print("Beginning nodal updates")
total_num_nodal_moves = 0
itr_delta_entropy = np.zeros(max_num_nodal_itr)
# compute the global entropy for MCMC convergence criterion
overall_entropy = compute_overall_entropy(interblock_edge_count, block_degrees_out, block_degrees_in, num_blocks, N, E, use_sparse_matrix)
for itr in range(max_num_nodal_itr):
num_nodal_moves = 0;
itr_delta_entropy[itr] = 0
for current_node in range(N):
current_block = partition[current_node]
# propose a new block for this node
proposal, num_out_neighbor_edges, num_in_neighbor_edges, num_neighbor_edges = propose_new_partition(current_block, out_neighbors[current_node], in_neighbors[current_node], partition, interblock_edge_count, block_degrees, num_blocks, 0, use_sparse_matrix)
# determine whether to accept or reject the proposal
if (proposal != current_block):
# compute block counts of in and out neighbors
blocks_out, inverse_idx_out = np.unique(partition[out_neighbors[current_node][:,0]], return_inverse = True)
count_out = np.bincount(inverse_idx_out, weights=out_neighbors[current_node][:,1]).astype(int)
blocks_in, inverse_idx_in = np.unique(partition[in_neighbors[current_node][:,0]], return_inverse = True)
count_in = np.bincount(inverse_idx_in, weights=in_neighbors[current_node][:,1]).astype(int)
# compute the two new rows and columns of the interblock edge count matrix
self_edge_weight = np.sum(out_neighbors[current_node][np.where(out_neighbors[current_node][:,0]==current_node),1]) # check if this node has a self edge
new_interblock_edge_count_current_block_row, new_interblock_edge_count_new_block_row, new_interblock_edge_count_current_block_col, new_interblock_edge_count_new_block_col = \
compute_new_rows_cols_interblock_edge_count_matrix(interblock_edge_count, current_block, proposal, blocks_out, count_out, blocks_in, count_in, self_edge_weight, 0, use_sparse_matrix)
# compute new block degrees
block_degrees_out_new, block_degrees_in_new, block_degrees_new = compute_new_block_degrees(current_block, proposal, block_degrees_out, block_degrees_in, block_degrees, num_out_neighbor_edges, num_in_neighbor_edges, num_neighbor_edges)
# compute the Hastings correction
if num_neighbor_edges>0:
Hastings_correction = compute_Hastings_correction(blocks_out, count_out, blocks_in, count_in, proposal, interblock_edge_count, new_interblock_edge_count_current_block_row, new_interblock_edge_count_current_block_col, num_blocks, block_degrees, block_degrees_new, use_sparse_matrix)
else: # if the node is an island, proposal is random and symmetric
Hastings_correction = 1
# compute change in entropy / posterior
delta_entropy = compute_delta_entropy(current_block, proposal, interblock_edge_count, new_interblock_edge_count_current_block_row, new_interblock_edge_count_new_block_row, new_interblock_edge_count_current_block_col, new_interblock_edge_count_new_block_col, block_degrees_out, block_degrees_in, block_degrees_out_new, block_degrees_in_new, use_sparse_matrix)
# compute probability of acceptance
p_accept = np.min([np.exp(-beta*delta_entropy)*Hastings_correction, 1])
# if accept the proposal, update the partition, inter_block_edge_count, and block degrees
if (np.random.uniform() <= p_accept):
total_num_nodal_moves += 1
num_nodal_moves += 1
itr_delta_entropy[itr] += delta_entropy
partition, interblock_edge_count, block_degrees_out, block_degrees_in, block_degrees = update_partition(partition, current_node, current_block, proposal, interblock_edge_count, new_interblock_edge_count_current_block_row, new_interblock_edge_count_new_block_row, new_interblock_edge_count_current_block_col, new_interblock_edge_count_new_block_col, block_degrees_out_new, block_degrees_in_new, block_degrees_new, use_sparse_matrix)
if verbose:
print("Itr: {}, number of nodal moves: {}, delta S: {:0.5f}".format(itr, num_nodal_moves, itr_delta_entropy[itr]/float(overall_entropy)))
if itr>=(delta_entropy_moving_avg_window-1): # exit MCMC if the recent change in entropy falls below a small fraction of the overall entropy
if not(np.all(np.isfinite(old_overall_entropy))): # golden ratio bracket not yet established
if (-np.mean(itr_delta_entropy[(itr-delta_entropy_moving_avg_window+1):itr]) < (delta_entropy_threshold1*overall_entropy)):
break
else: # golden ratio bracket is established. Fine-tuning partition.
if (-np.mean(itr_delta_entropy[(itr-delta_entropy_moving_avg_window+1):itr]) < (delta_entropy_threshold2*overall_entropy)):
break
# compute the global entropy for determining the optimal number of blocks
overall_entropy = compute_overall_entropy(interblock_edge_count, block_degrees_out, block_degrees_in, num_blocks, N, E, use_sparse_matrix)
if verbose:
print("Total number of nodal moves: {}, overall_entropy: {:0.2f}".format(total_num_nodal_moves, overall_entropy))
if visualize_graph & use_graph_tool_options:
graph_object = plot_graph_with_partition(out_neighbors, partition, graph_object)
# check whether the partition with optimal number of block has been found; if not, determine and prepare for the next number of blocks to try
partition, interblock_edge_count, block_degrees, block_degrees_out, block_degrees_in, num_blocks, num_blocks_to_merge, old_partition, old_interblock_edge_count, old_block_degrees, old_block_degrees_out, old_block_degrees_in, old_overall_entropy, old_num_blocks, optimal_num_blocks_found = \
prepare_for_partition_on_next_num_blocks(overall_entropy, partition, interblock_edge_count, block_degrees, block_degrees_out, block_degrees_in, num_blocks, old_partition, old_interblock_edge_count, old_block_degrees, old_block_degrees_out, old_block_degrees_in, old_overall_entropy, old_num_blocks, num_block_reduction_rate)
if verbose:
print('Overall entropy: {}'.format(old_overall_entropy))
print('Number of blocks: {}'.format(old_num_blocks))
if optimal_num_blocks_found:
print('\nOptimal partition found with {} blocks'.format(num_blocks))
if use_timeit:
t1 = timeit.default_timer()
print('\nGraph partition took {} seconds'.format(t1-t0))
# evaluate output partition against the true partition
evaluate_partition(true_partition, partition)
In [18]:
main()