From 87b1672bf6d77b1ff744514965f67c4f8c6d965a Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Feb 2025 11:18:26 -0300 Subject: [PATCH] feat: check LA diversity --- .../LateAcceptanceAcceptor.java | 88 ++++++++++++++++++- .../AbstractGeometricStuckCriterion.java | 4 +- .../DiminishedReturnsStuckCriterion.java | 2 +- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index ad906b5d9e..e645a7b619 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -3,6 +3,8 @@ import java.util.ArrayDeque; import java.util.Arrays; import java.util.Deque; +import java.util.HashMap; +import java.util.Map; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.RestartableAcceptor; @@ -14,6 +16,13 @@ public class LateAcceptanceAcceptor extends RestartableAcceptor { + // Using a start window of 30 seconds and a geometric factor of 1.4, + // five restarts will result in 300 seconds without improvement. + protected static final int MAX_RESTART_WITHOUT_IMPROVEMENT = 4; + protected static final double MIN_DIVERSITY_RATIO = 0.05; + // The goal is to increase from hundreds to thousands in the first restart event and then increment it linearly + protected static final int SCALING_FACTOR = 10; + protected int lateAcceptanceSize = -1; protected boolean hillClimbingEnabled = true; @@ -22,10 +31,13 @@ public class LateAcceptanceAcceptor extends RestartableAcceptor currentBestScore; + // Keep track of the best scores accumulated so far. This list will be used to reseed the later elements list. private Deque> bestScoreQueue; - protected static final int SCALING_FACTOR = 10; + // Map to count late elements and allow faster diversity calculation + private Map lateElementCountMap; protected int defaultLateAcceptanceSize; protected int coefficient; + protected int countRestartWithoutImprovement; public LateAcceptanceAcceptor(StuckCriterion stuckCriterionDetection) { super(stuckCriterionDetection); @@ -58,10 +70,14 @@ public void phaseStarted(LocalSearchPhaseScope phaseScope) { Arrays.fill(previousScores, initialScore); lateScoreIndex = 0; coefficient = 0; + countRestartWithoutImprovement = 0; defaultLateAcceptanceSize = lateAcceptanceSize; + // The maximum size is three times the size of the initial element list maxBestScoreSize = defaultLateAcceptanceSize * 3 * SCALING_FACTOR; bestScoreQueue = new ArrayDeque<>(maxBestScoreSize); bestScoreQueue.addLast(initialScore); + lateElementCountMap = new HashMap<>(); + lateElementCountMap.put(initialScore.toString(), new MutableInt(lateAcceptanceSize)); } private void validate() { @@ -90,37 +106,101 @@ public boolean accept(LocalSearchMoveScope moveScope) { @SuppressWarnings({ "rawtypes", "unchecked" }) public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); - previousScores[lateScoreIndex] = stepScope.getScore(); - lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize; + updateLateElement(stepScope); if (((Score) currentBestScore).compareTo(stepScope.getScore()) < 0) { + if (countRestartWithoutImprovement > 0 && coefficient > 0) { + // We decrease the coefficient + // if there is an improvement + // to avoid altering the late element size in the next restart event. + // This action is performed only once after a restart event + coefficient--; + } + countRestartWithoutImprovement = 0; if (bestScoreQueue.size() < maxBestScoreSize) { bestScoreQueue.addLast(stepScope.getScore()); } else { + // // When the collection is full, we remove the lowest score and add the new best value. bestScoreQueue.poll(); bestScoreQueue.addLast(stepScope.getScore()); } } } + private void updateLateElement(LocalSearchStepScope stepScope) { + var previousScoreStr = previousScores[lateScoreIndex].toString(); + var count = lateElementCountMap.computeIfPresent(previousScoreStr, ((s, c) -> { + var value = c.decrement(); + if (value == 0) { + return null; + } + return c; + })); + if (count == null) { + lateElementCountMap.remove(previousScoreStr); + } + var stepScoreStr = stepScope.getScore().toString(); + lateElementCountMap.compute(stepScoreStr, (s, c) -> { + if (c == null) { + return new MutableInt(1); + } + c.increment(); + return c; + }); + previousScores[lateScoreIndex] = stepScope.getScore(); + lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize; + } + @Override public void restart(LocalSearchStepScope stepScope) { + countRestartWithoutImprovement++; + var diversity = lateElementCountMap.size() == 1 ? 0 : lateElementCountMap.size() / (double) lateAcceptanceSize; + if (countRestartWithoutImprovement <= MAX_RESTART_WITHOUT_IMPROVEMENT && diversity > MIN_DIVERSITY_RATIO) { + // We prefer not to restart until an initial time window of at least 5 minutes has passed. + // We have observed that this approach works better for more complex datasets. + // However, when the diversity is zero, it indicates that the LA may be stuck in a local minimum, + // and in such cases, we should restart before the initial five minutes. + logger.info("Restart event delayed. Diversity ({}), Distinct Elements ({}), Restart without Improvement ({})", + diversity, lateElementCountMap.size(), countRestartWithoutImprovement); + return; + } coefficient++; var newLateAcceptanceSize = defaultLateAcceptanceSize * coefficient * SCALING_FACTOR; rebuildLateElementsList(newLateAcceptanceSize); } + /** + * The method recreates the late elements list with the new size {@code newLateAcceptanceSize}. + * The aim is to recreate elements using previous best scores. + * This method provides diversity + * while avoiding the initialization phase + * that occurs when the later elements are set to the most recent best score. + *

+ * The approach is based on the work: + * Parameter-less Late Acceptance Hill-climbing: Foundations & Applications by Mosab Bazargani. + */ private void rebuildLateElementsList(int newLateAcceptanceSize) { var newPreviousScores = new Score[newLateAcceptanceSize]; if (logger.isInfoEnabled()) { - logger.info("Changing the lateAcceptanceSize from %d to %d.".formatted(lateAcceptanceSize, newLateAcceptanceSize)); + var diversity = lateElementCountMap.size() == 1 ? 0 : lateElementCountMap.size() / (double) lateAcceptanceSize; + if (lateAcceptanceSize == newLateAcceptanceSize) { + logger.info("Keeping the lateAcceptanceSize as {}. Diversity ({})", lateAcceptanceSize, diversity); + } else { + logger.info( + "Changing the lateAcceptanceSize from {} to {}. Diversity ({})", lateAcceptanceSize, + newLateAcceptanceSize, diversity); + } } var countPerScore = newLateAcceptanceSize / bestScoreQueue.size() + 1; var count = new MutableInt(newLateAcceptanceSize - 1); var iterator = bestScoreQueue.descendingIterator(); + lateElementCountMap.clear(); while (count.intValue() >= 0 && iterator.hasNext()) { var score = iterator.next(); + var scoreCount = new MutableInt(0); + lateElementCountMap.put(score.toString(), scoreCount); for (var i = 0; i < countPerScore; i++) { newPreviousScores[count.intValue()] = score; + scoreCount.increment(); count.decrement(); if (count.intValue() < 0) { break; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/AbstractGeometricStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/AbstractGeometricStuckCriterion.java index d0b40f2632..2b68f6650c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/AbstractGeometricStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/AbstractGeometricStuckCriterion.java @@ -54,8 +54,8 @@ public boolean isSolverStuck(LocalSearchMoveScope moveScope) { var triggered = evaluateCriterion(moveScope); if (triggered) { logger.info( - "Restart triggered with geometric factor ({}), scaling factor of ({}), nextRestart ({}), best score ({})", - currentGeometricGrowFactor, scalingFactor, nextRestart, + "Restart triggered with geometric factor ({}), scaling factor of ({}), best score ({})", + currentGeometricGrowFactor, scalingFactor, moveScope.getStepScope().getPhaseScope().getBestScore()); currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR); nextRestart = calculateNextRestart(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterion.java index 07cf1ebe40..a57c0eb81b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterion.java @@ -9,7 +9,7 @@ public class DiminishedReturnsStuckCriterion> extends AbstractGeometricStuckCriterion { - protected static final long TIME_WINDOW_MILLIS = 300_000; + protected static final long TIME_WINDOW_MILLIS = 30_000; private static final double MINIMAL_IMPROVEMENT = 0.0001; private DiminishedReturnsTermination diminishedReturnsCriterion;