Ben
Ben

Reputation: 29

How do I terminate an async scipy.optimize based on time?

Really struggling with this one... Forgive the longish post.

I have an experiment that on each trial displays some stimulus, collects a response, and then moves on to the next trial.

I would like to incorporate an optimizer that runs in between trials and therefore must have a specific time-window designated by me to run, or it should be terminated. If it's terminated, I would like to return the last set of parameters it tried so that I can use it later.

Generally speaking, here's the order of events I'd like to happen:

In between trials:

  1. Display stimulus ("+") for some number of seconds.
  2. While this is happening, run the optimizer.
  3. If the time for displaying the "+" has elapsed and the optimizer has not finished, terminate the optimizer, return the most recent set of parameters it tried, and move on.

Here is some of the relevant code I'm working with so far:

do_bns() is the objective function. In it I include NLL['par'] = par or q.put(par)

from scipy.optimize import minimize
from multiprocessing import Process, Manager, Queue
from psychopy import core #for clock, and other functionality

clock = core.Clock()

def optim(par, NLL, q)::
    a = minimize(do_bns, (par), method='L-BFGS-B', args=(NLL, q),
        bounds=[(0.2, 1.5), (0.01, 0.8), (0.001, 0.3), (0.1, 0.4), (0.1, 1), (0.001, 0.1)],
        options={"disp": False, 'maxiter': 1, 'maxfun': 1, "eps": 0.0002}, tol=0.00002)

if __name__ == '__main__':
    print('starting optim')
    max_time = 1.57
    with Manager() as manager:
        par = manager.list([1, 0.1, 0.1, 0.1, 0.1, 0.1])
        NLL = manager.dict()
        q = Queue()
        p = Process(target=optim, args=(par, NLL, q))
        p.start()
        start = clock.getTime()

        while clock.getTime() - start < max_time:
            p.join(timeout=0)
            if not p.is_alive():
                break

        if p.is_alive():
            res = q.get()
            p.terminate()
            stop = clock.getTime()
            print(NLL['a'])
            print('killed after: ' + str(stop - start))

        else:
            res = q.get()
            stop = clock.getTime()
            print('terminated successfully after: ' + str(stop - start))
            print(NLL)
            print(res)

This code, on its own, seems to sort of do what I want. For example, the res = q.get() right above the p.terminate() actually takes something like 200ms so it will not terminate exactly at max_time if max_time < ~1.5s

If I wrap this code in a while-loop that checks to see if it's time to stop presenting the stimulus:

stim_start = clock.getTime()
stim_end = 5
print('showing stim')
textStim.setAutoDraw(True)
win.flip()
while clock.getTime() - stim_start < stim_end:

    # insert the code above

print('out of loop')

I get weird behavior such as multiple iterations of the whole code from the beginning...

showing stim
starting optim
showing stim
out of loop
showing stim
out of loop
[1.0, 0.10000000000000001, 0.10000000000000001, 0.10000000000000001, 0.10000000000000001, 0.10000000000000001]
killed after: 2.81303440395

Note the multiple 'showing stim's' and 'out of loop's.

I'm open to any solution that accomplishes my goal :|

Help and thank you! Ben

Upvotes: 0

Views: 738

Answers (1)

sascha
sascha

Reputation: 33522

General remark

Your solution would give me nightmares! I don't see a reason to use multiprocessing here and i'm not even sure how you grab those updated results before termination. Maybe you got your reason for this approach, but i highly recommend something else (which has a limitation).

Callback-based approach

The general idea i would pursue is the following:

  • fire up your optimizer with some additional time-limit information and some callback enforcing this
    • the callback is called in each iteration of this optimizer
      • if time-limit reached: raise a customized Exception

The limits:

  • as the callback is only called once in each iteration, there is some limited sequence of points in time where the optimizer might get stopped
    • the potential difference is highly dependent on iteration-time for your problem! (numerical-differentiation, huge-data, slow function eval; all this matters)
    • if not exceeding some given time is of highest priority, this approach might be not right or you would need some kind of safeguarded interpolation to reason if one more iteration is possible in time
    • or: combine your kind of killing off workers with my approach of updating intermediate-results through some callback

Example code (bit hacky):

import time
import numpy as np
import scipy.sparse as sp
import scipy.optimize as opt
np.random.seed(1)

""" Fake task: sparse NNLS """
M, N, D = 2500, 2500, 0.1
A = sp.random(M, N, D)
b = np.random.random(size=M)

""" Optimization-setup """
class TimeOut(Exception):
    """Raise for my specific kind of exception"""

def opt_func(x, A, b):
    return 0.5 * np.linalg.norm(A.dot(x) - b)**2

def opt_grad(x, A, b):
    Ax = A.dot(x) - b
    grad = A.T.dot(Ax)
    return grad

def callback(x):
    time_now = time.time()             # probably not the right tool in general!
    callback.result = [np.copy(x)]     # better safe than sorry -> copy

    if time_now - callback.time_start >= callback.time_max:
        raise TimeOut("Time out")

def optimize(x0, A, b, time_max):
    result = [np.copy(x0)]                                   # hack: mutable type
    time_start = time.time()
    try:
        """ Add additional info to callback (only takes x as param!) """
        callback.time_start = time_start
        callback.time_max = time_max
        callback.result = result

        res = opt.minimize(opt_func, x0, jac=opt_grad,
                     bounds=[(0, np.inf) for i in range(len(x0))],  # NNLS
                     args=(A, b), callback=callback, options={'disp': False})

    except TimeOut:
        print('time out')
        return result[0], opt_func(result[0], A, b)

    return res.x, res.fun

print('experiment 1')
start_time = time.perf_counter()
x, res = optimize(np.zeros(len(b)), A, b, 0.1)  # 0.1 seconds max!
end_time = time.perf_counter()
print(res)
print('used secs: ', end_time - start_time)

print('experiment 2')
start_time = time.perf_counter()
x_, res_ = optimize(np.zeros(len(b)), A, b, 5)  # 5 seconds max!
end_time = time.perf_counter()
print(res_)
print('used secs: ', end_time - start_time)

Example output:

experiment 1
time out
422.392771467
used secs:  0.10226839151517493

experiment 2
72.8470708728
used secs:  0.3943936788825996

Upvotes: 1

Related Questions