I'm trying to refactor some code for fitting parametric models that are defined symbolically using Theano. My goal is for all models to expose a common interface so that, as far as possible, they can be drop-in replacements for one another. To that end I've tried to encapsulate each model within a separate class. A key requirement is that I'm able to parallelize fitting the same model to multiple datasets using
multiprocessing (I'm using the joblib wrapper for this).
Here's a runnable example of what I'm doing at the moment:
import numpy as np import theano from theano import tensor as te from theano.gradient import jacobian, hessian from scipy.optimize import minimize from joblib import Parallel, delayed class Rosenbrock(object): """The Rosenbrock function: f(x, y) = (a - x)^2 + b(y - x^2)^2 """ # symbolic variables - only used internally _P = te.dvector('P') _a, _b = _P, _P _xy = te.dmatrix('xy') _x, _y = _xy, _xy _z = te.dvector('z') _z_hat = (_a - _x) ** 2 + _b * (_y - _x ** 2) ** 2 _diff = _z - _z_hat _loss = 0.5 * te.dot(_diff, _diff) _jac = jacobian(_loss, _P) _hess = hessian(_loss, _P) # theano functions - part of the interface forward = theano.function([_P, _xy], _z_hat) loss = theano.function([_P, _xy, _z], _loss) jacobian = theano.function([_P, _xy, _z], _jac) hessian = theano.function([_P, _xy, _z], _hess) @staticmethod def initialize(xy, z): """ make some sensible estimate of what the initial parameters should be, based on xy and z """ P0 = xy[:, np.argmin(z)] return P0 @staticmethod def _postfit(P): """ sometimes I want to make some adjustments to the parameters post- fitting, e.g. wrapping angles between 0 and 2pi """ return P def do_fit(model, *args): """ wrapper function that performs the fitting """ # initialize the model P0 = model.initialize(*args) # do the fit res = minimize(model.loss, P0, args=args, method='Newton-CG', jac=model.jacobian, hess=model.hessian) P = res.x # tweak the parameters P = model._postfit(P) # return the tweaked parameters return P def run(niter=2000): # I don't actually need to instantiate this, since everything is # effectively a class method... model = Rosenbrock() # some example data xy = np.mgrid[-3:3:100j, -3:3:100j].reshape(2, -1) P = np.r_[1., 100.] z = model.forward(P, xy) # run multiple fits in parallel pool = Parallel(n_jobs=-1, verbose=1, pre_dispatch='all') results = pool(delayed(do_fit)(model, xy, z) for _ in xrange(niter)) if __name__ == '__main__': run()
The core functions
hessian() behave like staticmethods of the class. I've found that in order to be able to parallelize
the fitting, the Theano functions must be attributes of the class rather than
of an instance. Otherwise (i.e. if I define these functions within the
method of the class), what happens when I try to call them in parallel using
multiprocessing is that the worker threads block one another, meaning that they effectively only use a single core. Presumably this is because the GIL is no longer being circumvented, although I don't really understand why this should be the case.
Declaring the Theano functions as class methods has two rather undesirable consequences:
The class namespace gets cluttered up with all of the intermediate Theano symbolic variables (
betc.), which aren't really needed after
theano.function()has been called. To hide them from the user I have to prepend the variable names with underscores, which makes the code harder to read.
theano.function()triggers the auto-generation and compilation of C code, which is a slow process. It would be most convenient to define all of my models in the same source file, but that means that whenever I import or reload that file I have to wait for all of my models to re-compile.
Can anyone (especially someone with experience in Theano) suggest a better way to structure this code?