quaquel / EMAworkbench

workbench for performing exploratory modeling and analysis
BSD 3-Clause "New" or "Revised" License
124 stars 88 forks source link

Save time by giving optimizer initial policies from previous optimization run #71

Open jasonrwang opened 4 years ago

jasonrwang commented 4 years ago

Problem

In practice, most EMAworkbench work is iterative. When optimizing, picking a useful value of function evaluations (nfe) is especially uncertain. Users are probably most likely to first pick a number of evaluations that fits their computational or temporal resources. If this is insufficient, they might then pick a larger number to run, or run the computations on a bigger computer (or cluster).

In these scenarios, it would be useful to take the policies on the Pareto front of the previous runs and give them to the optimizer as initial values/parents.

I see that robust_optimize() will eventually call _optimize() (as does optimize()), which then runs optimizer.run(). optimizer itself is declared in line 817 of em_framework.optimization as the chosen Platypus algorithm.

Attempted solution

Every algorithm in platypus is a child of platypus.algorithms.AbstractGeneticAlgorithm, which has the method initialize.

def initialize(self):
        self.population = [self.generator.generate(self.problem) for _ in range(self.population_size)]
        self.evaluate_all(self.population)

I replaced self.population with my own set, which was defined as a list that is syntactically compatible with platypus. Unfortunately, this actually took some time to figure out exactly how platypus defines its solutions. In pseudo-code, this is what we did:

  1. Slice results data frame for just the policies and their lever choices
  2. Define a little function to convert the levers into a format for platypus (binary)
  3. Import ema_workbench.em_framework.optimization.to_robust_problem and platypus.EpsNSGAII (platypus.NSGAII also works; we didn't test other algorithms).
  4. Create policies properly in platypus grammar
    problem = to_robust_problem(dike_model, scenarios, robustness_functions, constraints=constraints,)
    optimizer = EpsNSGAII(problem, epsilons, nfe=nfe,convergence=convergence)
  5. Generate as many solutions as we have Pareto policies (this step is mostly for proper syntax) init_policies = [optimizer.generator.generate(optimizer.problem) for n in range(num_paretos)]
  6. Replace the variables object in those initial solutions with our own
  7. Print this list, and then copy and paste it into platypus's algorithms.py

Note: Regarding 2, our lever types were integers, but platypus stores them as some form of binary. For example, the lever 4 was saved in self.population.solution.variables (Solution() being the relevant object type here) as [[False], [True], [False], [False]] because the levers could range from 0-9 (9 being 1001 in binary). The size of this list seems to be set to that required by the largest value. When the lever was only 0 or 1, the platypus Solution variables were only either [True] or [False].

However, this approach didn't quite work. I don't have the debug codes anymore, but from what I recall, Platypus couldn't read our initial variables array even though it, to my eye, looked identical to theirs. Obviously, I missed something!

Further solution ideas

It seems we might be able to pass to NSGAII an archive argument.

We might be able to take advantage of the fact that, in the method platypus.NSGAII, we find on lines 179-188:

def step(self):
        if self.nfe == 0:
            self.initialize()
        else:
            self.iterate()

        if self.archive is not None:
            self.result = self.archive
        else:
            self.result = self.population

which is housed in Platypus's highest parent class, Algorithm. Here, in a while loop, it calls the child method step(), which is listed above.

Therefore, I think we could most succinctly achieve this goal by overriding ... something in Platypus by setting .nfe==1, which reflects that we are actually building off of an initial set of policies, and .archive or .population to a set of policy solutions that we have, which need to be a list of platypus.Solution() objects. I haven't gone back to try and understand how Platypus uses those objects though.


Additional notes:

jasonrwang commented 4 years ago

I don't know why I've been sitting on this so long, but I've now been on holiday/doing other work too long to naturally slide back into this very quickly. I'm happy to look at it again in September though!