DEAP / deap

Distributed Evolutionary Algorithms in Python
http://deap.readthedocs.org/
GNU Lesser General Public License v3.0
5.75k stars 1.12k forks source link

Documentation doesn't clearly state how selection operators behave with multiple objectives #38

Open cmd-ntrf opened 10 years ago

cmd-ntrf commented 10 years ago

Reported by cgranade, Jun 4 (6 days ago) In the documentation posted at [0], it is not made clear which selection operators are single-objective, which ones treat the fitness vector in a lexographic fashion, and which are useful in MOEA contexts. For instance, selTournament seems to act lexographically, as it uses the sorted built-in with a tuple-valued fitness as the key, but this is not made clear in the documentation at [1].

Links: [0] http://deap.gel.ulaval.ca/doc/default/api/tools.html [1] http://deap.gel.ulaval.ca/doc/default/api/tools.html#deap.tools.selTournament

kanosek commented 7 years ago

The current documentation clearly suggests using weights to vary the importance of each objective one against another. This is very misleading with selection operators that treat the fitness vector in a lexographic fashion.

blthayer commented 5 years ago

Just stopping by to say I agree with @kanosek and the original filer of this ticket.

Take for example selBest vs. selTournament.

selBest uses sorted with the reverse=True argument. However, selTournament uses max.

This can lead to tricky behavior (note I'm using deap v1.3.0):

I would like to use deap to create a GA in which my weights represent, well, the weights of my objectives. All my weights will be negative, indicating I want the smallest possible value for each objective. So, I went ahead and subclassed base.Fitness like so to get around the lexicographical sorting (which doesn't meet my use case):

my_module.py:

from deap import base

class MultiMinFitness(base.Fitness):
    """Compare with sum of wvalues. Smaller (more negative) is better."""
    def __le__(self, other):
        return sum(self.wvalues) <= sum(other.values)

    def __lt__(self, other):
        return sum(self.wvalues) < sum(other.values)

Now, if I create some unittests to ensure my subclass is behaving correctly, we get inconsistent results between selBest and selTournament: test_my_module.py:

import unittest
from unittest.mock import patch

from .my_module import MultiMinFitness

from deap import creator, tools

class MultiFitnessTestCase(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        """Use the deap creator to set things up."""
        creator.create('FitnessMin', MultiMinFitness, weights=(-2, -4, -8))

    def test_selection(self):

        class MyClass:
            """Stub in a class to assign fitnesses to."""
            def __init__(self, f):
                self.fitness = f

            def __repr__(self):
                return str('MyClass: ' + str(self.fitness))

        # Initialize some fitness objects. Note that f2 is "more fit"
        # since the weighted sum is "more negative"
        f1 = creator.FitnessMin((1, 2, 3))
        f2 = creator.FitnessMin((2, 4, 6))

        # Create a couple class members.
        c1 = MyClass(f1)
        c2 = MyClass(f2)

        # Use selBest to ensure c2 (and thus f2) is best.
        best = tools.selBest([c1, c2], k=2)

        # c2 is the 'most negative' and therefore the best.
        self.assertIs(best[0], c2)  # Succeeds

        # Now perform a tournament selection. Patch selRandom so it just
        # returns our list of MyClass objects.
        with patch('deap.tools.selection.selRandom', return_value=[c1, c2]):
            best_tourn = tools.selTournament([c1, c2], k=1, tournsize=2)

        self.assertIs(best_tourn[0], c2)  # Fails.

Results of running test_my_module.py:

Failure
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/unittest/case.py", line 59, in testPartExecutor
    yield
  File "/usr/local/lib/python3.7/unittest/case.py", line 615, in run
    testMethod()
  File "/opt/project/tmp/test_my_module.py", line 39, in test_selection
    self.assertIs(best_tourn[0], c2)  # Fails.
  File "/usr/local/lib/python3.7/unittest/case.py", line 1120, in assertIs
    self.fail(self._formatMessage(msg, standardMsg))
  File "/usr/local/lib/python3.7/unittest/case.py", line 680, in fail
    raise self.failureException(msg)
AssertionError: MyClass: (1.0, 2.0, 3.0) is not MyClass: (2.0, 4.0, 6.0)
fmder commented 5 years ago

Excellent point, thanks for your contribution. We'll consider this as part of the 2.0 refactoring that is underway.