IBM ILOG Solver User's Manual > Evolutionary Algorithms > Using More Advanced EA Features > Using listeners and comparators

In this section, you will learn how to:

You can monitor operator improvements by attaching listeners to operators. Such monitoring can be done in two ways:

Make a copy of the example file YourSolverHome/examples/src/tutorial/ea1max_listen_partial.cpp and open this copy in your development environment. After filling in the blanks in each step in this section, you will have completed the example and you can compile and run the program.

In this example, you will show operator statistics; you define a small structure to hold these statistics for a particular operator.

Step 1   -  

Stores invocation and improvement statistics for operators

Add the following code after the comment
//Stores invocation and improvement statistics for operators

struct OperatorStatistics {
  IloInt invocations;
  IloInt improvements;
  IloInt successes;
  OperatorStatistics() : invocations(0), improvements(0), successes(0) { }
};

The following procedure checks if the operator statistics record already exists and creates it if required. Each operator's statistics object are stored and retrieved using setObject and getObject respectively.

Step 2   -  

Retrieve operator statistics (and create if necessary)

Add the following code after the comment
//Retrieve operator statistics (and create if necessary)

OperatorStatistics * GetOperatorStatistics(IloPoolOperator op) {
  OperatorStatistics * stat = (OperatorStatistics *)op.getObject();
  if (stat == 0) {
    stat = new (op.getEnv()) OperatorStatistics();
    op.setObject(stat);
  }
  return stat;
}

Objects of type IloPoolOperator define the basic genetic operators that will be used to modify the population, and it is upon these objects that monitoring will be done, and statistics gathered. However, to execute an operator it must be cast to a pool processor (IloPoolProc), albeit automatically most of the time. It is thus convenient to be able to have access to an operator which was used to create a processor. You perform this using a wrapping function and setObject. We also introduce a function which retrieves operator statistics give a processor.

Step 3   -  

Set up processor to operator mapping

Add the following code after the comment
// Setup the processor to operator mapping

IloPoolProc BuildGAProcessor(IloPoolOperator op) {
  IloPoolProc proc(op);
  proc.setObject(op.getImpl());
  return proc;
}

OperatorStatistics * GetOperatorStatistics(IloPoolProc proc) {
  return GetOperatorStatistics((IloPoolOperatorI*)proc.getObject());
}

You are now in a position to define a listener object which will be called when a new solution is produced by an operator. In the listener, you update operator statistics:

Listener objects can be defined via macros. You should pass dynamic information by reference or pointer so that you have the up-to-date information available when the listener is invoked; here the current generation.

Step 4   -  

Defines a listener which records solution improvements

Add the following code after the comment
//Defines a listener which records solution improvements

ILOIIMLISTENER2(ImprovementListener, IloPoolOperator::SuccessEvent, event, 
                IloSolution&, best, 
                IloInt&, generation) {
  IloSolution newSolution = event.getSolution();
  IloPoolOperator op = event.getOperator();
  OperatorStatistics * stat = GetOperatorStatistics(op);
  stat->successes++;
  if (newSolution.isBetterThan(best)) {
    best.end();
    best = newSolution.makeClone(getEnv());
    cout << " IMPROVEMENT " << best.getObjectiveValue()
         << " BY " << op.getDisplayName()
         << " GENERATION " << generation 
         << endl;
    stat->improvements++;
  }
}

The previous listener was to be called whenever a new solution was produced. You now create a listener which keeps track of when a particular genetic operator is invoked.

Step 5   -  

Defines a listener which records operator invocations

Add the following code after the comment
//Defines a listener which records operator invocations

ILOIIMLISTENER0(OperatorListener, IloPoolOperator::InvocationEvent, event) {
  IloPoolOperator op = event.getOperator();
  OperatorStatistics * stat = GetOperatorStatistics(op);
  stat->invocations++;
}

It can be useful to rank or score genetic operators according to their past performance so that they can be more intelligently invoked in the future. For example, this can mean choosing more often operators which seem to work well. In this example, to sort genetic operators depending on their performance, you use two criteria:

To do so, you define a first an IloComparator which will compare operator improvement counts, followed by one to compare invocations. The final idea is to combine the two comparisons such that operators with higher improvement counts and lower invocation counts are preferred. You use the ILOCOMPARATOR macro to perform custom comparison. Two parameters, left and right, are always present and are the operators to be compared. This macro should return a true value if and only if left is strictly better than right. Here you see that an operator with more improvements is better than one with less.

Step 6   -  

Define the operator improvement comparator

Add the following code after the comment
//Define the operator improvement comparator

ILOCOMPARATOR0(OperatorImprovementComparator, IloPoolProc, left, right) {
  OperatorStatistics * s1 = GetOperatorStatistics(left);
  OperatorStatistics * s2 = GetOperatorStatistics(right);
  return s1->improvements > s2->improvements;
}

You also define a second IloComparator devoted to comparing the numbers of operator invocations. Here, an operator which has been invoked less is deemed better than one which has been invoked more.

Step 7   -  

Define the operator invocation comparator

Add the following code after the comment

//Define the operator invocation comparator

ILOCOMPARATOR0(OperatorInvocationComparator, IloPoolProc, left, right) {
  OperatorStatistics * s1 = GetOperatorStatistics(left);
  OperatorStatistics * s2 = GetOperatorStatistics(right);
  return s1->invocations < s2->invocations;
}

In the main routine you use the predefined tournament selector instead of the customized one.

Step 8   -  

Use tournament selection to choose parents

Add the following code after the comment
//Use tournament selection to choose parents

    IloTournamentSelector<IloSolution, IloSolutionPool> tsel(
      env, 2, IloBestSolutionComparator(env)
    );
    IloPoolProc selector = IloSelectSolutions(env, tsel, IloTrue);

You attach the listeners before starting the generation loop. It is possible to attach listeners directly to the operator objects themselves. However, often a more convenient method is to add the listeners to the operator factory. These listeners will then be added to all operators produced by the factory after this point.

Step 9   -  

Add the operator invocation listener

Add the following code after the comment
// Add the operator invocation listener

    factory.addListener(OperatorListener(env));

To record generated solutions, you add the improvement listener to the factory.

Step 10   -  

Add the improvement listener

Add the following code after the comment // Add the improvement listener

    factory.addListener(ImprovementListener(env, best, generation));

This last listener will display a message each time the best solution found is improved upon.

Creation of the operators to use is done as before, with the exception that you make use of the BuildGAProcessor function to maintain a link from the processor at the operator on which it is based. This code is provided for you:

    ops.add(BuildGAProcessor(factory.mutate(1.0 / problemSize, "mutate")));
    ops.add(BuildGAProcessor(factory.uniformXover(0.5, "uXover")));

Once the generational loop exits, you display the goal statistics. The idea is to compare the performance of the operators according to two criteria. First, you compare the number of improvements which each operator provided. If the number of improvements is different for each operator, we know definitively which operator is considered better. If the number of improvements is the same, however, the number of invocations of each operator is examined. The operator which was invoked less is preferred. This type of hierarchical comparison is termed a lexicographical comparison and you will use it below.

Step 11   -  

Create composite operator comparator

Add the following code after the comment
//Create composite operator comparator

    IloComparator<IloPoolProc> opComparator = IloComposeLexical(
       OperatorImprovementComparator(env),
       OperatorInvocationComparator(env)
    );

Using this comparator, you sort the operator pool.

Step 12   -  

Sort the operator pool

Add the following code after the comment //Sort the operator pool

    IloBool done;
    do {
      done = IloTrue;
      for (IloInt i = 1; i < ops.getSize(); i++) {
        if (opComparator.isBetterThan(ops[i], ops[i-1])) {
          IloPoolProc tmp = ops[i-1]; ops[i-1] = ops[i]; ops[i] = tmp;
          done = IloFalse;
        }
      }
    } while (!done);

Finally, you loop over the sorted processors in best-first manner.

Step 13   -  

Loop over the processor pool and display statistics

Add the following code after the comment
//Loop over the processor pool and display statistics

    for (IloInt i = 0; i < ops.getSize(); i++) {
      IloPoolProc op = ops[i];
      OperatorStatistics* stat = GetOperatorStatistics(op);
      env.out() << " " << op.getDisplayName()
                << " INVOKE " << stat->invocations
                << " SUCCESS " << stat->successes
                << " IMPROVE " << stat->improvements
                << endl;
    }

You have now learned how to use listeners and comparators. In the next section, you will learn how to use pool evaluators.

Step 14   -  

Compile and run the program

Compile and run the program. You should get the following results:

Problem size: 50
Population size: 50
Max generation: 60
GENERATION 0 WORST 17 BEST 34 AVERAGE 25.06
Optimum value: 50
 IMPROVEMENT 39 BY IloArrayUniformXover GENERATION 1
GENERATION 1 WORST 20 BEST 39 AVERAGE 27.18
GENERATION 2 WORST 23 BEST 35 AVERAGE 28.2
GENERATION 3 WORST 24 BEST 35 AVERAGE 29.42
GENERATION 4 WORST 25 BEST 38 AVERAGE 30.6
GENERATION 5 WORST 28 BEST 37 AVERAGE 32.06
GENERATION 6 WORST 28 BEST 39 AVERAGE 33.38
 IMPROVEMENT 40 BY IloArrayUniformXover GENERATION 7
GENERATION 7 WORST 26 BEST 40 AVERAGE 33.76
GENERATION 8 WORST 29 BEST 40 AVERAGE 34.86
 IMPROVEMENT 41 BY IloArrayUniformXover GENERATION 9
GENERATION 9 WORST 28 BEST 41 AVERAGE 35.44
GENERATION 10 WORST 31 BEST 40 AVERAGE 35.74
GENERATION 11 WORST 31 BEST 41 AVERAGE 36.76
 IMPROVEMENT 42 BY IloArrayUniformXover GENERATION 12
 IMPROVEMENT 43 BY IloArrayUniformXover GENERATION 12
GENERATION 12 WORST 30 BEST 43 AVERAGE 37.64
GENERATION 13 WORST 33 BEST 43 AVERAGE 38.64
GENERATION 14 WORST 36 BEST 43 AVERAGE 39.54
 IMPROVEMENT 45 BY IloArrayUniformXover GENERATION 15
GENERATION 15 WORST 36 BEST 45 AVERAGE 40.54
 IMPROVEMENT 47 BY IloArrayUniformXover GENERATION 16
GENERATION 16 WORST 37 BEST 47 AVERAGE 41.74
GENERATION 17 WORST 38 BEST 47 AVERAGE 42.42
GENERATION 18 WORST 40 BEST 47 AVERAGE 43.54
 IMPROVEMENT 48 BY IloArrayUniformXover GENERATION 19
GENERATION 19 WORST 41 BEST 48 AVERAGE 44.1
GENERATION 20 WORST 41 BEST 48 AVERAGE 44.72
GENERATION 21 WORST 40 BEST 48 AVERAGE 45.16
 IMPROVEMENT 49 BY IloArrayUniformXover GENERATION 22
GENERATION 22 WORST 42 BEST 49 AVERAGE 45.88
GENERATION 23 WORST 43 BEST 49 AVERAGE 46.32
GENERATION 24 WORST 43 BEST 48 AVERAGE 46.06
GENERATION 25 WORST 43 BEST 49 AVERAGE 46.56
GENERATION 26 WORST 42 BEST 49 AVERAGE 46.76
GENERATION 27 WORST 44 BEST 49 AVERAGE 46.96
 IMPROVEMENT 50 BY IloArrayUniformXover GENERATION 28
GENERATION 28 WORST 44 BEST 50 AVERAGE 47.44
OPTIMUM FOUND 
 IloArrayUniformXover INVOKE 705 SUCCESS 705 IMPROVE 10
 IloArrayMutate INVOKE 695 SUCCESS 695 IMPROVE 0

The complete program is available in the YourSolverHome/examples/src/ea1max_listen.cpp file.