Solver.solve() returns 看似随机的解决方案和分数,不是最好的

Solver.solve() returns seeming random solution and score, not best

我有什么

在 Java 8.

上使用 OptaPlanner 7.45.0.Final

我正在研究排班的概念验证,我在其中创建了 CrewAssignment 对象作为 @PlanningEntity

我已经创建了自己的 EasyScoreCalculator 实现(来自新的未弃用的 EasyScoreCalculator)。

得分函数returns a HardMediumSoftScore,用Hard表示物理可能性(不能同时在2个地方,不能取一个开始于的activity与之前不同的地方 activity 结束),Medium 代表法律问题(最长工作日等),而 Soft 大致代表财务问题。

我得到的行为

对于有很多可能 Solution 的小型数据集,该时间表似乎很有效。然而,当我转向更大的数据集时,返回良好的最终时间表变得更加困难。会有像-60hard/-390060medium/-1280457soft这样的分数,这看起来真的很糟糕。而且,如果我增加可用时间,分数有时会变得更差!

我尝试过的东西

我在我的分数函数中打印了正在计算的分数。它通常会给出诸如 0hard/0medium/-2250411soft 之类的分数,这与最终分数相比非常棒!但是,最后的结果还是差了一个分数。

我在Solver中加了一个SolverEventListener,在求解的最后只调用了一次

我认为 Solution 的克隆可能存在问题,所以我创建了自己的 SolutionCloner。它在开始时被调用一次,在结束时被调用两次。由于它很少被调用,我怀疑求解器使用第一个 Solution 克隆作为最佳分数,尝试将当前 Solution 迭代中的值复制到最佳 Solution,但是给定SolverEventListener 只被调用一次,这可能表明它没有识别出最佳解决方案。

我尝试通过组合硬值和中值来简化为 HardSoftScore,但行为是相同的。

我尝试调用 solverConfig.setEnvironmentMode(EnvironmentMode.NON_INTRUSIVE_FULL_ASSERT);(是的,我正在以编程方式创建 SolverConfig,但行为没有改变。

相关代码片段

CrewAssignment @PlanningEntity 的子集:

@PlanningEntity
public final class CrewAssignment
{
    private Long crewMemberId;
    @PlanningVariable(valueRangeProviderRefs = {"crewMemberId"})
    public Long getCrewMemberId() { return crewMemberId; }
    public void setCrewMemberId(Long value) { crewMemberId = value; }

    @Override
    protected CrewAssignment clone()
    {
        CrewAssignment newAssignment = new CrewAssignment(getActivities());
        newAssignment.setCrewMemberId(crewMemberId);

        return newAssignment;
    }
}

Solution 的子集:

@PlanningSolution(solutionCloner = CrewSchedSolutionCloner.class)
public final class CrewSchedSolution
{
    private final List<Long> crewMemberIds;
    @ValueRangeProvider(id="crewMemberId")
    @ProblemFactCollectionProperty
    public List<Long> getCrewMemberIds() { return crewMemberIds; }

    // assignments
    private List<CrewAssignment> crewAssignments = new ArrayList<>();
    @PlanningEntityCollectionProperty
    public List<CrewAssignment> getCrewAssignments() { return crewAssignments; }

    HardSoftScore score;
    @PlanningScore
    public HardSoftScore getScore() { return score; }
    public void setScore(HardSoftScore value) { score = value; }

    public CrewSchedSolution cloneSolution()
    {
        List<CrewAssignment> newCrewAssignments = new ArrayList<>(crewAssignments);
        for (int i = 0; i < newCrewAssignments.size(); i++)
        {
            CrewAssignment existingAssignment = newCrewAssignments.get(i);
            CrewAssignment newAssignment = existingAssignment.clone();
            newCrewAssignments.set(i, newAssignment);
        }
        return new CrewSchedSolution(/* Various additional data */,
            crewMemberIds,
            newCrewAssignments, score);
    }
}

SolutionCloner:

public final class CrewSchedSolutionCloner
    implements SolutionCloner<CrewSchedSolution>
{
    @Override
    public CrewSchedSolution cloneSolution(CrewSchedSolution originalSolution)
    {
        return originalSolution.cloneSolution();
    }
}

总结

我有 运行 明显的(对我而言)进一步调试的方法。感觉好的价值正在被改写,但我还不能证明这一点。我也许可以想出一种方法来保存 CrewAssignment @PlanningEntity 中设置的值,只是为了让它工作,但 OptaPlanner 似乎就是为了能够处理而设计的。

问题太大,无法 post SSCCE。我已经 post 编辑了我认为可能相关的内容。让我知道您是否需要查看代码的任何其他部分。提前致谢!

P.S。我意识到这可能是 的副本,但原始 poster 从未跟进 posted 的答案。

如果我增加可用时间,分数有时会变得更差!那是不可能的(如果它在第二个运行中获得更多的步骤,它应该, 它是可重现的 运行, 它是).

DEBUG 日志是否显示 运行 有更多“LS step”行?如果是这样,您可能会损坏分数,但 NON_INTRUSIVE_FULL_ASSERT 或 FULL_ASSERT 会检测到它们是否 运行 足够长(比没有的时间长得多,因为它们速度较慢)。

通常情况下,optaplanner 运行s 是 100% 可重复的,给定相同的步数(〜相同的时间给予或采取一些步骤)。如果是这种情况,请检查您的 DEBUG 日志。模拟退火不可重现,但默认情况下处于关闭状态。

OptaPlanner Benchmark 是您最好的朋友。 尤其是 BEST_SCORE 图形和分数计算速度数字。 运行 它长了 4 倍,因此您可以看到 BEST_SCORE 图在给定更多时间后的表现。此外,subSingleCount 可能是一件有趣的事情,在这里打开,看看优化对一个好的随机种子有多敏感(它不应该是)。

哎呀,等等。您编写了自己的解决方案克隆器?可能就是这样,因为解决方案克隆器很难正确编写。禁用它,看看会发生什么。

OptaPlanner(当然)工作正常。我有一个概念上的误解。

我专注于我正在计算的 Scores,而不是专注于 OptaPlanner 实际使用的分数。 OptaPlanner 可以通过添加表示未初始化 @PlanningVariables 数量的“init”值来增加从评分函数返回的 Scores。困难和中等分数为 0 的分数可能看起来合理,但可能仍未初始化,这优先于 Hard/Medium/Soft 值。

一旦模拟的持续时间设置得足够长,所有 @PlanningVariables 都被设置。此后没有创建init为0的分数,并计算了完整的优化方案(无论是不可行还是可行)。

查看 Geoffrey 概述的调试步骤。如果您正在使用 JDK 日志记录,您可能需要将 slf4j-api 和 slf4j-jdk14 jar 添加到您的路径中。

请参阅文档第 2.3.6 节“在规划解决方案中收集域对象”,其中描述了“未初始化”、“不可行”和“可行”解决方案之间的区别。

注意:当迄今为止的最佳解决方案未初始化时,求解器似乎不会通知 SolverEventListeners。