OptaPlanner中三种主要的分数计算方式对比

释放双眼,带上耳机,听听看~!
本文介绍了在OptaPlanner中约束设计的过程,以及三种主要的分数计算方式:Easy Java, Constraint Streams, 和 Incremental Java的对比。

引言

约束设计的过程就是定义的规则的过程,包括硬约束(必须满足的条件)和软约束(尽量满足的条件)。硬约束通常用于描述问题的基本限制,比如每台计算机不能出现超频的情况。软约束用于描述优化目标,如尽量降低维护成本。

约束设计

在 OptaPlanner 中,有三种主要的分数计算方式:Easy Java, Constraint Streams, 和 Incremental Java。下面是这三种方法的对比。

Easy Java

实现EasyScoreCalculator的calculateScore(Solution)方法,使其返回一个HardSoftScore实例。

public class CloudBalancingEasyScoreCalculator
    implements EasyScoreCalculator<CloudBalance, HardSoftScore> {

    /**
     * A very simple implementation. The double loop can easily be removed by using Maps as shown in
     * {@link CloudBalancingMapBasedEasyScoreCalculator#calculateScore(CloudBalance)}.
     */
    @Override
    public HardSoftScore calculateScore(CloudBalance cloudBalance) {
        int hardScore = 0;
        int softScore = 0;
        for (CloudComputer computer : cloudBalance.getComputerList()) {
            int cpuPowerUsage = 0;
            int memoryUsage = 0;
            int networkBandwidthUsage = 0;
            boolean used = false;

            // Calculate usage
            for (CloudProcess process : cloudBalance.getProcessList()) {
                if (computer.equals(process.getComputer())) {
                    cpuPowerUsage += process.getRequiredCpuPower();
                    memoryUsage += process.getRequiredMemory();
                    networkBandwidthUsage += process.getRequiredNetworkBandwidth();
                    used = true;
                }
            }

            // Hard constraints
            int cpuPowerAvailable = computer.getCpuPower() - cpuPowerUsage;
            if (cpuPowerAvailable < 0) {
                hardScore += cpuPowerAvailable;
            }
            int memoryAvailable = computer.getMemory() - memoryUsage;
            if (memoryAvailable < 0) {
                hardScore += memoryAvailable;
            }
            int networkBandwidthAvailable = computer.getNetworkBandwidth() - networkBandwidthUsage;
            if (networkBandwidthAvailable < 0) {
                hardScore += networkBandwidthAvailable;
            }

            // Soft constraints
            if (used) {
                softScore -= computer.getCost();
            }
        }
        return HardSoftScore.of(hardScore, softScore);
    }

}

每次的求解就会触发这个方法重新计算分数,从代码上来看,这个显然是无法达到性能要求的,但是并不意味着EasyJava的方式是无用的。

Constraint Streams

Constraint Streams是一种函数式编程形式的增量分数计算方法,可以在普通Java中轻松阅读、编写和调试。如果熟悉Java流或SQL,API应该会感觉很熟悉。

// ************************************************************************
    // Hard constraints
    // ************************************************************************

    Constraint requiredCpuPowerTotal(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(CloudProcess.class)
                .groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredCpuPower))
                .filter((computer, requiredCpuPower) -> requiredCpuPower > computer.getCpuPower())
                .penalize(HardSoftScore.ONE_HARD,
                        (computer, requiredCpuPower) -> requiredCpuPower - computer.getCpuPower())
                .asConstraint("requiredCpuPowerTotal");
    }

    Constraint requiredMemoryTotal(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(CloudProcess.class)
                .groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredMemory))
                .filter((computer, requiredMemory) -> requiredMemory > computer.getMemory())
                .penalize(HardSoftScore.ONE_HARD,
                        (computer, requiredMemory) -> requiredMemory - computer.getMemory())
                .asConstraint("requiredMemoryTotal");
    }

    Constraint requiredNetworkBandwidthTotal(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(CloudProcess.class)
                .groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredNetworkBandwidth))
                .filter((computer, requiredNetworkBandwidth) -> requiredNetworkBandwidth > computer.getNetworkBandwidth())
                .penalize(HardSoftScore.ONE_HARD,
                        (computer, requiredNetworkBandwidth) -> requiredNetworkBandwidth - computer.getNetworkBandwidth())
                .asConstraint("requiredNetworkBandwidthTotal");
    }

    // ************************************************************************
    // Soft constraints
    // ************************************************************************

    Constraint computerCost(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(CloudComputer.class)
                .ifExists(CloudProcess.class, equal(Function.identity(), CloudProcess::getComputer))
                .penalize(HardSoftScore.ONE_SOFT, CloudComputer::getCost)
                .asConstraint("computerCost");
    }

在求解过程中任何实例发生变化,约束流会自动检测到变化,并仅重新计算受变化影响的最小必要问题部分。下图说明了这种增量分数计算的过程:
OptaPlanner中三种主要的分数计算方式对比
从上图看得出,3feb4feb更换了班次后,计算分数只计算变更的部分。

Incremental Java

实现接口IncrementalScoreCalculator的所有方法:

public interface IncrementalScoreCalculator<Solution_, Score_ extends Score<Score_>> {
    void resetWorkingSolution(Solution_ workingSolution);
    void beforeEntityAdded(Object entity);
    void afterEntityAdded(Object entity);
    void beforeVariableChanged(Object entity, String variableName);
    void afterVariableChanged(Object entity, String variableName);
    void beforeEntityRemoved(Object entity);
    void afterEntityRemoved(Object entity);
    Score_ calculateScore();
}

完整的代码请看Java类org.optaplanner.examples.cloudbalancing.optional.score.CloudBalancingIncrementalScoreCalculator

private void insert(CloudProcess process) {
        CloudComputer computer = process.getComputer();
        if (computer != null) {
            int cpuPower = computer.getCpuPower();
            int oldCpuPowerUsage = cpuPowerUsageMap.get(computer);
            int oldCpuPowerAvailable = cpuPower - oldCpuPowerUsage;
            int newCpuPowerUsage = oldCpuPowerUsage + process.getRequiredCpuPower();
            int newCpuPowerAvailable = cpuPower - newCpuPowerUsage;
            hardScore += Math.min(newCpuPowerAvailable, 0) - Math.min(oldCpuPowerAvailable, 0);
            cpuPowerUsageMap.put(computer, newCpuPowerUsage);

            int memory = computer.getMemory();
            int oldMemoryUsage = memoryUsageMap.get(computer);
            int oldMemoryAvailable = memory - oldMemoryUsage;
            int newMemoryUsage = oldMemoryUsage + process.getRequiredMemory();
            int newMemoryAvailable = memory - newMemoryUsage;
            hardScore += Math.min(newMemoryAvailable, 0) - Math.min(oldMemoryAvailable, 0);
            memoryUsageMap.put(computer, newMemoryUsage);

            int networkBandwidth = computer.getNetworkBandwidth();
            int oldNetworkBandwidthUsage = networkBandwidthUsageMap.get(computer);
            int oldNetworkBandwidthAvailable = networkBandwidth - oldNetworkBandwidthUsage;
            int newNetworkBandwidthUsage = oldNetworkBandwidthUsage + process.getRequiredNetworkBandwidth();
            int newNetworkBandwidthAvailable = networkBandwidth - newNetworkBandwidthUsage;
            hardScore += Math.min(newNetworkBandwidthAvailable, 0) - Math.min(oldNetworkBandwidthAvailable, 0);
            networkBandwidthUsageMap.put(computer, newNetworkBandwidthUsage);

            int oldProcessCount = processCountMap.get(computer);
            if (oldProcessCount == 0) {
                softScore -= computer.getCost();
            }
            int newProcessCount = oldProcessCount + 1;
            processCountMap.put(computer, newProcessCount);
        }
    }

这段代码是一个私有方法,其作用是将变更的实例CloudProcess进行分数计算,可见其实现非常复杂,可以预见到后期的维护是很困难的,未来若调整约束条件等,这种方法无疑是代价最高的。

三种约束实现对比

下面是这三种方法的对比:

  1. Easy Java:
  • 优点:易于理解和实现,适合初学者。
  • 缺点:性能较差,因为它需要重新计算整个解决方案的分数,而不是仅计算变化的部分。
  • 使用场景:适用于简单的问题和快速原型开发。
  1. Constraint Streams:
  • 优点:性能较好,因为它可以增量地计算分数。它使用了类似于 Java Stream API 的声明式编程风格,使代码更简洁易读。
  • 缺点:学习曲线稍微陡峭,需要熟悉 OptaPlanner 的 API 和约束流的概念。
  • 使用场景:适用于复杂的问题和需要高性能的场景。
  1. Incremental Java:
  • 优点:性能最佳,因为它可以在每次移动后立即更新分数,而无需重新计算整个解决方案。
  • 缺点:实现复杂,需要手动管理分数的增量更新。容易出错,需要仔细编写和测试代码。
  • 使用场景:适用于对性能要求极高的场景,或者在其他方法无法满足性能需求时使用。

结尾

总之,Easy Java 适合初学者和简单问题,Constraint Streams 提供了较好的性能和易读性,适用于大多数场景,而 Incremental Java 则适用于对性能要求极高的场景。在实际应用中,可以根据问题的复杂性和性能需求选择合适的分数计算方式。

如果您想转载我的文章,请注明原作者和出处,谢谢。

本网站的内容主要来自互联网上的各种资源,仅供参考和信息分享之用,不代表本网站拥有相关版权或知识产权。如您认为内容侵犯您的权益,请联系我们,我们将尽快采取行动,包括删除或更正。
AI教程

将ChatGLM集成进LangChain工具链中

2023-12-13 8:52:14

AI教程

GPU云主机搭建AI大语言模型详解

2023-12-13 9:01:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索