@@ -1100,12 +1100,14 @@ def __init__(self, sample,
11001100 if calc_observables_r is None or get_multipliers_r is None :
11011101 self .get_multipliers_r , self .calc_observables_r = define_pseudo_ising_helper_functions (self .n )
11021102
1103- def solve (self , * args , ** kwargs ):
1103+ def solve (self , force_general = False , ** kwargs ):
11041104 """Uses a general all-purpose optimization to solve the problem using functions
11051105 defined in self.get_multipliers_r and self.calc_observables_r.
11061106
11071107 Parameters
11081108 ----------
1109+ force_general : bool, False
1110+ If True, force use of "general" algorithm.
11091111 initial_guess : ndarray, None
11101112 Initial guess for the parameter values.
11111113 solver_kwargs : dict, {}
@@ -1117,13 +1119,91 @@ def solve(self, *args, **kwargs):
11171119 multipliers
11181120 """
11191121
1120- return self ._solve_general (* args , ** kwargs )
1122+ if type (self .model ) is Ising and not force_general :
1123+ return self ._solve_ising (** kwargs )
1124+ return self ._solve_general (** kwargs )
1125+
1126+ def _solve_ising (self ,
1127+ initial_guess = None ,
1128+ full_output = False ,
1129+ solver_kwargs = {}):
1130+ """Solve for Langrangian parameters according to pseudolikelihood algorithm.
1131+
1132+ Parameters
1133+ ----------
1134+ initial_guess : ndarray, None
1135+ Initial guess for the parameter values.
1136+ full_output : bool, False
1137+ If True, return output from scipy.minimize() routine.
1138+ solver_kwargs : dict, {}
1139+ Keyword arguments for scipy.optimize.minimize.
1140+
1141+ Returns
1142+ -------
1143+ ndarray
1144+ Solved multipliers.
1145+ dict (optional)
1146+ Output from scipy.optimize.minimize.
1147+ """
1148+
1149+ if initial_guess is None :
1150+ initial_guess = np .zeros (self .calc_observables (self .sample [0 ][None ,:]).size )
1151+
1152+ # reformat initial_guess for easy looping later
1153+ initial_guessAsMat = replace_diag (squareform (initial_guess [self .n :]), initial_guess [:self .n ])
1154+ for i in range (1 ,self .n ):
1155+ tmp = initial_guessAsMat [i ,i ]
1156+ initial_guessAsMat [i ,i ] = initial_guessAsMat [i ,0 ]
1157+ initial_guessAsMat [i ,0 ] = tmp
1158+ # initialize variables that will be used later
1159+ obs = [self .calc_observables_r (r , self .sample ) for r in range (self .n )]
1160+ soln = [] # list of output from scipy.optimize.minimize for each spin
1161+ Jmat = np .zeros ((self .n , self .n )) # couplings stored in matrix format with fields along diagonal
1162+
1163+ # iterate through each spin and solve for parameters for each one
1164+ for r in range (self .n ):
1165+ # to use below...
1166+ multipliersrix = self .get_multipliers_r (r , initial_guess )[1 ]
1167+ guess = initial_guess .copy ()
1168+
1169+ # params only change the terms relevant to the particular spin being considered
1170+ def f (params ):
1171+ guess [multipliersrix ] = params
1172+ multipliers = self .get_multipliers_r (r , guess )[0 ]
1173+ E = - obs [r ].dot (multipliers )
1174+ loglikelihood = - np .log ( 1 + np .exp (2 * E ) ).sum ()
1175+ dloglikelihood = ( - (1 / (1 + np .exp (2 * E )) * np .exp (2 * E ))[:,None ] * 2 * obs [r ] ).sum (0 )
1176+ return - loglikelihood , dloglikelihood
1177+
1178+ soln .append (minimize (f , initial_guessAsMat [r ], jac = True , ** solver_kwargs ))
1179+ thisMultipliers = soln [- 1 ]['x' ]
1180+ Jmat [r ,r ] = thisMultipliers [0 ]
1181+ Jmat [r ,np .delete (np .arange (self .n ),r )] = thisMultipliers [1 :]
1182+
1183+ # symmetrize couplings
1184+ Jmat = (Jmat + Jmat .T )/ 2
1185+ self .multipliers = np .concatenate ((Jmat .diagonal (), squareform (zero_diag (Jmat ))))
1186+
1187+ if full_output :
1188+ return self .multipliers , soln
1189+ return self .multipliers
11211190
11221191 def _solve_general (self ,
11231192 initial_guess = None ,
11241193 full_output = False ,
11251194 solver_kwargs = {}):
1126- """Solve for Langrangian parameters according to pseudolikelihood algorithm.
1195+ """Solve for Langrangian parameters according to a variation on the
1196+ pseudolikelihood algorithm detailed in Aurell and Ekeberg (PRL, 2012). There, the
1197+ conditional log-likelihoods per spin are minimized independently and then the
1198+ resulting couplings are combined in a way that ensures that the interactions are
1199+ symmetric. The generalization is straightforward for higher-order interactions
1200+ (normalize by the order of the interaction), but here present a different approach
1201+ that is somewhat computationally simpler.
1202+
1203+ The *sum* of the conditional likelihoods over each spin is minimized, ensuring
1204+ that the parameters are equal across all conditional likelihood equations by
1205+ construction. In general, this gives different results from the normal
1206+ pseudolikelihood formulation, but they agree closely in many cases.
11271207
11281208 Parameters
11291209 ----------
@@ -1132,61 +1212,62 @@ def _solve_general(self,
11321212 full_output : bool, False
11331213 If True, return output from scipy.minimize() routine.
11341214 solver_kwargs : dict, {}
1135- kwargs for scipy.minimize() .
1215+ Keyword arguments for scipy.optimize. minimize.
11361216
11371217 Returns
11381218 -------
11391219 ndarray
1140- multipliers
1220+ Solved multipliers.
11411221 dict (optional)
1142- Output from scipy.minimize.
1222+ Output from scipy.optimize. minimize.
11431223 """
11441224
11451225 if initial_guess is None :
11461226 initial_guess = np .zeros (self .calc_observables (self .sample [0 ][None ,:]).size )
1227+ obs = [self .calc_observables_r (r , self .sample ) for r in range (self .n )]
11471228
1148- def f (params ,
1149- n = self .n ,
1150- calc_observables_r = self .calc_observables_r ,
1151- get_multipliers_r = self .get_multipliers_r ):
1229+ def f (params ):
1230+ # running sums of function evaluations over all spins
11521231 loglikelihood = 0
11531232 dloglikelihood = np .zeros_like (initial_guess ) # gradient
11541233
11551234 # iterate through each spin
1156- for r in range (n ):
1157- obs = calc_observables_r (r , self .sample )
1158- multipliers , multipliersrix = get_multipliers_r (r ,params )
1159- E = - obs .dot (multipliers )
1235+ for r in range (self .n ):
1236+ multipliers , multipliersrix = self .get_multipliers_r (r , params )
1237+ E = - obs [r ].dot (multipliers )
11601238 loglikelihood += - np .log ( 1 + np .exp (2 * E ) ).sum ()
1161- dloglikelihood [multipliersrix ] += ( - (1 / (1 + np .exp (2 * E )) * np .exp (2 * E ))[:,None ] * 2 * obs ).sum (0 )
1239+ dloglikelihood [multipliersrix ] += ( - (1 / (1 + np .exp (2 * E )) *
1240+ np .exp (2 * E ))[:,None ] *
1241+ 2 * obs [r ] ).sum (0 )
11621242 return - loglikelihood , dloglikelihood
11631243
11641244 soln = minimize (f , initial_guess , jac = True , ** solver_kwargs )
11651245 self .multipliers = soln ['x' ]
11661246 if full_output :
1167- return soln ['x' ],soln
1247+ return soln ['x' ], soln
11681248 return soln ['x' ]
11691249
1170- def _solve_ising (self , initial_guess = None , full_output = False ):
1171- """Solve Ising model specifically with Pseudo .
1250+ def _solve_ising_deprecated (self , initial_guess = None , full_output = False ):
1251+ """Deprecated .
11721252
11731253 Parameters
11741254 ----------
11751255 initial_guess : ndarray, None
11761256 Pseudo for Ising doesn't use a starting point. This is syntactic sugar.
1257+ full_output : bool, False
11771258
11781259 Returns
11791260 -------
11801261 ndarray
1181- multipliers
1262+ Solved multipliers.
11821263 """
11831264
11841265 X = self .sample
11851266 X = (X + 1 )/ 2 # change from {-1,1} to {0,1}
11861267
11871268 # start at freq. model params?
11881269 freqs = np .mean (X , axis = 0 )
1189- hList = - np .log (freqs / (1. - freqs ))
1270+ hList = - np .log (freqs / (1. - freqs ))
11901271 Jfinal = np .zeros ((self .n ,self .n ))
11911272
11921273 for r in range (self .n ):
@@ -1216,6 +1297,8 @@ def _solve_ising(self, initial_guess=None, full_output=False):
12161297
12171298 def cond_log_likelihood (self , r , X , Jr ):
12181299 """Equals the conditional log likelihood -L_r.
1300+
1301+ Deprecated.
12191302
12201303 Parameters
12211304 ----------
@@ -1246,6 +1329,8 @@ def cond_log_likelihood(self, r, X, Jr):
12461329
12471330 def cond_jac (self , r , X , Jr ):
12481331 """Returns d cond_log_likelihood / d Jr, with shape (dimension of system)
1332+
1333+ Deprecated.
12491334 """
12501335
12511336 X ,Jr = np .array (X ),np .array (Jr )
@@ -1267,6 +1352,8 @@ def cond_hess(self, r, X, Jr, pairCoocRhat=None):
12671352 Current implementation uses more memory for speed. For large sample size, it may
12681353 make sense to break up differently if too much memory is being used.
12691354
1355+ Deprecated.
1356+
12701357 Parameters
12711358 ----------
12721359 pairCooc : ndarray, None
@@ -1296,14 +1383,18 @@ def pair_cooc_mat(self, X):
12961383 For use with cond_hess.
12971384
12981385 Slow because I haven't thought of a better way of doing it yet.
1386+
1387+ Deprecated.
12991388 """
13001389
13011390 p = [ np .outer (f ,f ) for f in X ]
13021391 return np .transpose (p ,(1 ,0 ,2 ))
13031392
1304- def pseudo_log_likelhood (self , X , J ):
1393+ def pseudo_log_likelihood (self , X , J ):
13051394 """TODO: Could probably be made more efficient.
13061395
1396+ Deprecated.
1397+
13071398 Parameters
13081399 ----------
13091400 X : ndarray
0 commit comments