def _psi_solve(y, eps=0.00001): """ Solve psi(c)-log(c)=y by dichotomy """ if y > 0: print "y", y raise ValueError("y>0, the problem cannot be solved") u = 1.0 if y > sp.psi(u) - sp.log(u): while sp.psi(u) - sp.log(u) < y: u *= 2 u /= 2 else: while sp.psi(u) - sp.log(u) > y: u /= 2 return _dichopsi_log(u, 2 * u, y, eps)
def _psi_solve(y, eps=0.00001): """ This function solves solve psi(c)-log(c)=y by dichotomy """ if y>0: print "y", y raise ValueError, "y>0, the problem cannot be solved" u = 1. if y>sp.psi(u)-sp.log(u): while sp.psi(u)-sp.log(u)<y: u = 2*u u = u/2 else: while sp.psi(u)-sp.log(u)>y: u = u/2 return _dichopsi_log(u,2*u,y,eps)
def _dichopsi_log(u, v, y, eps=0.00001): """ Implements the dichotomic part of the solution of psi(c)-log(c)=y """ if u > v: u, v = v, u t = (u + v) / 2 if np.absolute(u - v) < eps: return t else: if sp.psi(t) - sp.log(t) > y: return _dichopsi_log(u, t, y, eps) else: return _dichopsi_log(t, v, y, eps)
def learn(A,K,net0={},opts={}): """ runs variational bayes for inference of network modules (i.e. community detection) under a constrained stochastic block model. net0 and opts inputs are optional. if provided, length(net0.a0) must equal K. inputs: A: N-by-N undirected (symmetric), binary adjacency matrix w/o self-edges (note: fastest for sparse and logical A) K: (maximum) number of modules net0: initialization/hyperparameter structure for network net0['Q0']: N-by-K initial mean-field matrix (rows sum to 1) net0['ap0']: alpha_{+0}, hyperparameter for prior on \theta_+ net0['bp0']: beta_{+0}, hyperparameter for prior on \theta_+ net0['am0']: alpha_{-0}, hyperparameter for prior on \theta_- net0['bm0']: beta_{-0}, hyperparameter for prior on \theta_- net0['a0']: alpha_{\mu0}, 1-by-K vector of hyperparameters for prior on \pi opts: options opts['TOL_DF']: tolerance on change in F (outer loop) opts['MAX_FITER']: maximum number of F iterations (outer loop) opts['VERBOSE']: verbosity (0=quiet (default),1=print, 2=figures) outputs: net: posterior structure for network net['F']: converged free energy (same as net.F_iter(end)) net['F_iter']: free energy over iterations (learning curve) net['Q']: N-by-K mean-field matrix (rows sum to 1) net['K']: K, passed for compatibility with vbmod_restart net['ap']: alpha_+, hyperparameter for posterior on \theta_+ net['bp']: beta_+, hyperparameter for posterior on \theta_+ net['am']: alpha_-, hyperparameter for posterior on \theta_- net['bm']: beta_-, hyperparameter for posterior on \theta_- net['a']: alpha_{\mu}, 1-by-K vector of hyperparameters for posterior on \pi """ # default options TOL_DF=1e-2 MAX_FITER=30 VERBOSE=0 SAVE_ITER=0 #print "get options from opts struct" if (type(opts) == type({})) and (len(opts) > 0): if 'TOL_DF' in opts: TOL_DF=opts['TOL_DF'] if 'MAX_FITER' in opts: MAX_FITER=opts['MAX_FITER'] if 'VERBOSE' in opts: VERBOSE=opts['VERBOSE'] if 'SAVE_ITER' in opts: SAVE_ITER=opts['SAVE_ITER'] N=A.shape[0] # number of nodes M=0.5*A.sum(0).sum(1) # total number of non-self edges M=M[0,0] C=comb(N,2) # total number of possible edges between N nodes uk=mat(ones([K,1])) un=mat(ones([N,1])) #print "default prior hyperparameters" ap0=2; bp0=1; am0=1; bm0=2; a0=ones([1,K]); #print "get initial Q0 matrix and prior hyperparameters from net0 struct" if (type(net0) == type({})) and (len(net0) > 0): if 'Q0' in net0: Q=net0['Q0'] if 'ap0' in net0: ap0=net0['ap0'] if 'bp0' in net0: bp0=net0['bp0'] if 'am0' in net0: am0=net0['am0'] if 'bm0' in net0: bm0=net0['bm0'] if 'a0' in net0: a0=net0['a0'] #print "initialize Q if not provided" try: Q except NameError: Q=init(N,K) Qmat=mat(Q) #print "size of Q=", Q.shape #Q=init(N,K) # ensure a0 is a 1-by-K vector assert(a0.shape == (1,K)) #print "intialize variational distribution hyperparameters to be equal to prior hyperparameters" ap=ap0 bp=bp0 am=am0 bm=bm0 a=a0 n=Q.sum(0) # get indices of non-zero row/columns # to be passed to vbmod_estep_inline # jntj: must be better way (rows,cols)=A.nonzero() # vector to store free energy over iterations F=[] for i in range(MAX_FITER): #################### #VBE-step, to update mean-field Q matrix over module assignments #################### # compute local and global coupling constants, JL and JG and # chemical potentials -lnpi psiap=digamma(ap) psibp=digamma(bp) psiam=digamma(am) psibm=digamma(bm) psip=digamma(ap+bp) psim=digamma(am+bm) JL=psiap-psibp-psiam+psibm JG=psibm-psim-psibp+psip lnpi=digamma(a)-digamma(sum(a)) estep_inline(rows,cols,Q,float(JL),float(JG),array(lnpi),array(n)) """ # local update (technically correct, but slow) for l in range(N): # exclude Q[l,:] from contributing to its own update Q[l,:]=zeros([1,K]) # jntj: doesn't take advantage of sparsity Al=mat(A.getrow(l).toarray()) AQl=multiply((Al.T*uk.T),Q).sum(0) nl=Q.sum(0) lnQl=JL*AQl-JG*nl+lnpi lnQl=lnQl-lnQl.max() Q[l,:]=exp(lnQl) Q[l,:]=Q[l,:]/Q[l,:].sum() """ #################### #VBM-step, update distribution over parameters #################### # compute expected occupation numbers <n*>s n=Qmat.sum(0) # compute expected edge counts <n**>s #QTAQ=mat((Q.T*A*Q).toarray()) #npp=0.5*trace(QTAQ) npp=0.5*(Qmat.T*A*Qmat).diagonal().sum() npm=0.5*trace(Qmat.T*(un*n-Qmat))-npp nmp=M-npp nmm=C-M-npm # compute hyperparameters for beta and dirichlet distributions over # theta_+, theta_-, and pi_mu ap=npp+ap0 bp=npm+bp0 am=nmp+am0 bm=nmm+bm0 a=n+a0 #print ap, bp, am, bm, a # evaluate variational free energy, an approximation to the # negative log-evidence F.append(betaln(ap,bp)-betaln(ap0,bp0)+betaln(am,bm)-betaln(am0,bm0)+sum(gammaln(a))-gammaln(sum(a))-(sum(gammaln(a0))-gammaln(sum(a0)))-sum(multiply(Qmat,log(Qmat)))) F[i]=-F[i] print "iteration", i+1 , ": F =", F[i] # F should always decrease if (i>1) and F[i] > F[i-1]: print "\twarning: F increased from", F[i-1] ,"to", F[i] if (i>1) and (abs(F[i]-F[i-1]) < TOL_DF): break return dict(F=F[-1],F_iter=F,Q=Q,K=K,ap=ap,bp=bp,am=am,bm=bm,a=a)