industrial-optimization-group / desdeo-mcdm

Contains traditional optimization techniques from the field of Multiple-criteria decision-making. For example, methods belonging to the NIMBUS and NAUTILUS families can be found here.
MIT License
6 stars 7 forks source link

Implement revised Nautilus & NIMBUS level checks #1

Closed JedStephens closed 4 years ago

JedStephens commented 4 years ago

Depending on the nature of the problem checking that the input value is: nadir =< input =< ideal is not possible because it assumes a certain type of problem in this case it assumes maximization. For example if nadir = 1, ideal = 5 and input is 3 then check would pass successfully. However, if the type of problem is minimization then nadir = -1, ideal = -5 and input = -3 would fail the check!

Because no type of problem parameter is defined the check needs to be agnostic.

This does not come up in the Kursawe function example because here the ideal point is for f1 and f2 to be minimized. But when working with a problem where f1 and f1 have ideal points in the maximum eg. image then the NIMBUS code (line 90):

            # some objectives classified to be improved until some level
            if not np.all(
                np.array(response["levels"])[improve_until_inds]
                >= self._method._ideal[improve_until_inds]
            ) or not np.all(
                np.array(response["levels"])[improve_until_inds]
                <= self._method._nadir[improve_until_inds]
            ):
                raise NimbusException(
                    f"Given levels must be between the nadir and ideal points!"
                )

fails and even though the level input is valid it will fail.

Similarly the NIMBUS version does not work

            if np.any(ref_point < self._content["ideal"]) or np.any(
                ref_point > self._content["nadir"]
            ):
                raise NautilusNavigatorException(
                    f"The given reference point {ref_point} "
                    "must be between the ranges imposed by the ideal and nadir points."
                )

I suggest then:

        comp_array = np.array([self._method._nadir, self._method._ideal])
        if not (
            np.all(comp_array.min(axis=0) <= response["levels"])
            and (np.all(response["levels"] <= comp_array.max(axis=0)))
        ):
            raise NimbusException(
                f"Given levels must equal or be between the nadir and ideal points!"
            )

This check would still need to index out the cases where:

  1. values should improve '<'
    1. values should improve until some desired aspiration level is reached '<='
    2. values with an acceptable level '='
    3. values which may be impaired until some upper bound is reached '>='
    4. values which are free to change '0'

1, 3, or 5 occur.

Let me know if you want this in PR form. Currently the Nimbus function is not working for my use case due to this issue. I have fixed it locally.

JedStephens commented 4 years ago

Here is a file to see the bug for yourself. Note also that the Pareto front function seems to run into troubles. This is not so much a problem for me though because there are efficient C++ routines in this regard.

Please change file extension to .ipynb to get notebook back. GitHub only allow .txt upload. synchronous_nimbus-debug.txt

gialmisi commented 4 years ago

Thank you @JedStephens for your input, and I'm sorry I noticed this issue only now. NIMBUS was implemented with the assumption that each objective is to be always minimized. This assumptions is valid throughout the DESDEO framework. When using the methods, it is assumed that proper handling of the objectives is done beforehand. That is, multiplying the objective values by -1 for objectives being maximized. In other words, the behavior you are observing is intended. By doing this improve will always mean to decrease in value and impair will always mean to increase in value in the NIMBUS method here. This is one example of the benefits of this assumption, but also I realize this decision comes with trade-offs.

I understand this assumption has not been made clear anywhere (yet). Proper documentation should be available in the coming weeks. What we have done in our own implementations so far, is to multiply objective values being maximized by -1 before visualizing them, and then asking for feedback from a decision maker. Then, the feedback is transformed back. This way the decision maker will always see the objectives in the original scale, but internally we have not to worry about which objective is being minimized and which maximized.

One way to also keep track of which objective is being maximized, is to pass the keyword argument maximize to _ScalarObjective. For example _ScalarObjective(name="f1", evaluator=f_1, maximize=True). Then, when invoking evaluate on an MOProblem object, you can get the fitness objectives value of the resulting EvaluationResult object. These values will result in the original objective values. However, internally only the values defined in the fitness attribute of EvaluationResult are used should be used in NIMBUS (I will fix this). See EvalutationResult [here](https://github.com/industrial-optimization-group/desdeo-problem/blob/1abc8c02588dad7a2ade6a74b85cbae73bcb0904 /desdeo_problem/Problem.py#L37). Edit: Got it the wrong way. objectives results in the original values and fitness results in the always-to-be-minimized objective values. So this should be changed in the Synchronous NIMBUS code.

I hope this clears things up.

light-weaver commented 4 years ago

https://github.com/industrial-optimization-group/desdeo-mcdm/blob/92e14156b6245f851cd6f77f19e8bae558c24ec8/desdeo_mcdm/interactive/NIMBUS.py#L347

changing problem.evaluate(x).objectives to problem.evaluate(x).fitness should make it easier to handle maximization. "fitness" is always minimized, so you don't need to change the code. You only need to change the code for interaction.

JedStephens commented 4 years ago

Thanks