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(当然)工作正常。我有一个概念上的误解。
我专注于我正在计算的 Score
s,而不是专注于 OptaPlanner 实际使用的分数。 OptaPlanner 可以通过添加表示未初始化 @PlanningVariable
s 数量的“init”值来增加从评分函数返回的 Score
s。困难和中等分数为 0 的分数可能看起来合理,但可能仍未初始化,这优先于 Hard/Medium/Soft 值。
一旦模拟的持续时间设置得足够长,所有 @PlanningVariable
s 都被设置。此后没有创建init为0的分数,并计算了完整的优化方案(无论是不可行还是可行)。
查看 Geoffrey 概述的调试步骤。如果您正在使用 JDK 日志记录,您可能需要将 slf4j-api 和 slf4j-jdk14 jar 添加到您的路径中。
请参阅文档第 2.3.6 节“在规划解决方案中收集域对象”,其中描述了“未初始化”、“不可行”和“可行”解决方案之间的区别。
注意:当迄今为止的最佳解决方案未初始化时,求解器似乎不会通知 SolverEventListener
s。
我有什么
在 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。我意识到这可能是
如果我增加可用时间,分数有时会变得更差!那是不可能的(如果它在第二个运行中获得更多的步骤,它应该, 它是可重现的 运行, 它是).
DEBUG
日志是否显示 运行 有更多“LS step”行?如果是这样,您可能会损坏分数,但 NON_INTRUSIVE_FULL_ASSERT 或 FULL_ASSERT 会检测到它们是否 运行 足够长(比没有的时间长得多,因为它们速度较慢)。
通常情况下,optaplanner 运行s 是 100% 可重复的,给定相同的步数(〜相同的时间给予或采取一些步骤)。如果是这种情况,请检查您的 DEBUG
日志。模拟退火不可重现,但默认情况下处于关闭状态。
OptaPlanner Benchmark 是您最好的朋友。 尤其是 BEST_SCORE 图形和分数计算速度数字。 运行 它长了 4 倍,因此您可以看到 BEST_SCORE 图在给定更多时间后的表现。此外,subSingleCount
可能是一件有趣的事情,在这里打开,看看优化对一个好的随机种子有多敏感(它不应该是)。
哎呀,等等。您编写了自己的解决方案克隆器?可能就是这样,因为解决方案克隆器很难正确编写。禁用它,看看会发生什么。
OptaPlanner(当然)工作正常。我有一个概念上的误解。
我专注于我正在计算的 Score
s,而不是专注于 OptaPlanner 实际使用的分数。 OptaPlanner 可以通过添加表示未初始化 @PlanningVariable
s 数量的“init”值来增加从评分函数返回的 Score
s。困难和中等分数为 0 的分数可能看起来合理,但可能仍未初始化,这优先于 Hard/Medium/Soft 值。
一旦模拟的持续时间设置得足够长,所有 @PlanningVariable
s 都被设置。此后没有创建init为0的分数,并计算了完整的优化方案(无论是不可行还是可行)。
查看 Geoffrey 概述的调试步骤。如果您正在使用 JDK 日志记录,您可能需要将 slf4j-api 和 slf4j-jdk14 jar 添加到您的路径中。
请参阅文档第 2.3.6 节“在规划解决方案中收集域对象”,其中描述了“未初始化”、“不可行”和“可行”解决方案之间的区别。
注意:当迄今为止的最佳解决方案未初始化时,求解器似乎不会通知 SolverEventListener
s。