草庐IT

OptaPlanner将弃用DRL(Drools)评分方式!!!

kentzhang 2023-04-15 原文

  本来这段时间一直都在加紧我家“三胎”(易排通用智能规划平台)建设,毕竟我们的通用规划平台原定6月初就能上线,但因为其中遇到的各种技术问题及其它项目的突发情况,导致也只能跟随国家的003号航母,只能推迟上线,进度紧迫。经过近两个星期的奋战,终于将我们的【易排通用智能规划平台】的主要功能上线了,并做了一些基本的使用资料,供各位小伙伴先得试用。

  因为我们的平台还处在刚上线提供试用阶段,后续还有数不清的功能、平台设计、应用小视频需要我日以继夜地奋力补充(这些资料里不仅仅有平台的介绍资料喔,也有我们做规划或供应链信息化设计过程中的一些满满干货,敬请期待),理应是安排不了时间发布新的文章,但发现OptaPlanner已官宣,将在8.x版开始弃用对Drools评分机制,并在9.x不再支持Drools作为评分,作为替换的,将使用Constraint Stream(约束流)方式作为评分。当然,之前支持的Easy Java score calculation和Incremental Java score calculation这两种方式必然不会取消的。其中,我们的【易排通用规划平台】使用的就是Incremental Java score calculation,这种方式的优点是极度高效,有能力的小伙伴可以通过官言的示例尝试一下,目前大多数案例都有多种评分方式的,可以通过修改配置来尝试不同的评分方式,当问题空间足够大时(相同的问题模型,问题空间基本上取决于数据量大小),其运算时间差异可以是一个数量级的,甚至当数量大到一定程度时,使用Drools的评分方式会出现崩溃的情况(之前我们做过一个VRP项目遇到到这种情况,节点足够多时,不是放一两个小时就能跑出来,而是跑着跑着就出现JVM级别的溢出异常了)。

  关于Drools评分方式(下称DRL评分方式,官方也使用类似的叫法)的优劣,及未来补充的ConstraintStream方式,在本文中作一些概括性介绍,更详细的资料及从DRL到ConstraintStream的转换,可以参考OptaPlanner官方发布的博文:

https://www.optaplanner.org/blog/2022/05/26/optaplanner-deprecates-score-drl.html

使用DRL评分方式的优点

逻辑表达丰富,可用较简短的脚本表达较复杂的业务逻辑

  因为Drools本身是与OptaPlanner同属一项目群(KIE)的开源规划引擎软件,它具有一套成熟的规划描述脚本及规则推理引擎。算是目前世界上最成熟的开源规则引擎了(有没有之一不清晰,也许是我见识不够广)。因此,当我们使用Drools脚本,对OptaPlanner中涉及到的各种各样业务规则(都会抽象成约束)进行评分描述时,是相当有优势的。OptaPlanner这个引擎与Drools有相当丰富的接口,可实现两个引擎之间的高度匹配,从而则让OptaPlanner具备更丰富的约束表达能力。

自主生成OptaPlanner中规划实体的约束违反统计信息

  我们做OptaPlanner程序的时候,归根到底是要以高效、合理的方式对让引擎对各个规划实体(Planning Entity)中的规划变量(Planning Variable)进行不同的取值组合,并照我们编写好的评分约束对各个组合方案进行评分,从中找到评分最高的组合方案。而在此过程中,若我们使用DRL评分方式,哪个规划实体的哪个取值违反了哪条约束被扣了多少分,通过DRL的评分方式在运算过程,会把上述做详细的记录。因此,当我们完成规划运算,得到一个方案时,通过这个方案和OptaPlanner的评分API,就可以完整、精确地描述出,究竟是哪个规划实体的哪个变量取了什么值违反了哪个约束,导致被扣了多少分了。这样看来这个功能是不是很炫?目前我见到的其它求解器,应该还没有提供这种功能吧?当然也可能是我孤陋寡闻。而这个功能在业务场景上来说,是相当有用的,就是说,当你的规划程序跑出一个生产计划方案,这个计划方案哪个任务放在哪个机台上,导致扣了多少分,你直接就可以通过它的约束违反信息,就可以直接找到违反约束的原因和具体数据。而不需要我们人工对整个计划方案中的各个数据,一个一个寻找。

  相同的功能要求,如果我们使用的评分方式是Incremental Java score calculation,是没有自动的功能来记录这些约束违反信息的,而是需要我们在实现这个约束逻辑Java代码里,编写相应的评分逻辑,来保存这些信息的。完成了些逻辑后,最终生成的计划方案,才能统计出各个任务的约束违反情况,否则你只能从方案知道它的分数概况,也就是仅能知道哪个约束扣了多少分,但是哪个任务分配到哪个机台,因为与哪个任务冲突引起的扣分,不通过人工去编写逻辑,你是看不到的。

使用DRL评分方式的优点

  老师教导我们,所有事物都要辩证地看,因为事物必然具有两面性的。那么DRL的评分方式的缺点在哪里呢?

学习成本

  这个缺点对于我们没有涉及到规划引擎的小伙伴来说,肯定是一个巨大的差评。毕竟我们去掌握一门新的开发语言都需要花费不少精力了,况且还要学习掌握一门专门编写规则的脚本,虽然它是一种描述性的脚本语言(我们在学校进而学计算课的时候,把这种语言称为第四代语言,例如:SQL脚本),但丝毫不代表它简单。在面对一些复杂的场景时,要实现一些约束的逻辑,我们写脚本虽然不长,但其实要实现它,还是要花费不少心思的。总不会有人说,SQL脚本就比C++、Java简单吧?SQL脚本简单的仅仅是表达的方法,但处理一大堆复杂关联查询时,一条SQL的难点不一定比C++低,毕竟C++还是针对具体的每个对象进行运算的,而SQL则主要设计来针对一个数据集合进行处理的。所以并不能说它就简单。

  要完成对Drools引擎在OptaPlanner上的应用,除了要了解Drools脚本以外,还需要对Drools这个规则引擎的一些基本原理与结构有一定理解,才能更好的理解每个评分约束中,每个变更的含义,例如:哪些变量代表的是一个对象,什么情况下一个变量代表的是一个对象列表。

  当然若完全掌握了Drools的评分方式,并积累了一定用DRL表达OptaPlanner约束的经验后,面对一些约束,还是很有“拿着锤子你看到的满眼都是钉子”感觉的。也就是,当你面对一个约束时,会自然而然地很快通过Drools脚本来构思出它的大概逻辑结构。

运算效率

  运算效率指的是使用DRL评分时,引擎的计算速度。若你深入了解了OptaPlanner引擎的主要工作就知道,它所谓的运算,绝大多数时间都是在进行分数计算,还有小部分是使用各种启发式算法对搜寻过程的搜寻方向进行控制。因此,在这过程中,若使用DRL评分方式,Drools规则引擎就是担起了一个非常大运算责任。但Drools其实还是需要把这些约束转换成Java字节码进行运算的,而且是大量的集合运算,因此,很多情况下,性能上相对直接用Java编写的逻辑会更慢的。大家回忆一下各自的系统的性能卡顿点,是不是很多时候都是一些超长超复杂的SQL存储过程造成的?而其实它卡的原因还不一定是它的逻辑复杂,而是一些逻辑涉到数据集的运算,当我们处理不好这些数据集的关联查询时,就会引起很大规模的集合与集合之间的关联运算(就是各种表、视图进行的各种Join),从而成为绝对的性能瓶。

  Drools在进行约束评分运算时,实际上它就是一个规则推导过程,也有可能涉及很多对象集合的操作与判断。因此,当数据量大,评分约束相当复杂,或关联到众多对象列表时,也有可能引起如SQL查询一样的效果,从而大大影响了运算性能。

DRL评分的替代 - ConstraintStream.

  这种评分方式大概是从7.8x版本开始提出,8.x版本后开始慢慢完善。它充分使用了Java8以后的版本中出现的Stream特性,对大量的场景使用了函数式编程,从而大大地简化了集合数据处理的难度。也就是说,如果我们在使用Java的Stream功能,能很灵活地编写其中的Lambda表达式,ConstraintStream应用起来也是得心应手的。只是因为ConstraintStream是专门针对约束编程的,因此,它提供了一些特定的API供我们使用,我们要理解这引起API的前提,还是要理解评分过程的一些原理与原则。这也是我们学习ConstraintStream最大难点。

  我目前主要投入的精力都在我们的“三胎” - 【易排通用规划平台】上,这个平台因为基于灵活性与运算性能的考虑,我并没有使用DRL或ConstraintStream方式,而是直接使用Incremental Java score calculation的方式。这种方式与前两种对比,最贴切就理解就是单反相机与傻瓜相机的差别吧。正是因为我能在没有时间压力的前提下,把我目前掌握到的各种排产约束通过Incremental Java score calculation方式精心实现,从而让这个平台在运算大数据量订单时,性能远比使用Drools或ConstraintStream高。期待大家可能尝试该平台,这个平台我们将会使用租用方式,以SaaS方式提供一些有规划要求的场景,例如MES、MOM的计划模块。令专门提供这类系统、方案的小伙伴可以开箱即用地拥有一个规划引擎。当然,当你们遇到一些客户的个性化需求、约束或优化目标,而在我们平台上还没有提供的,也欢大家向我们提出,我们可以通过项目合作的方式定制这些功能。

  最后,我做了一个关于这个平台的操作说明。因为这个平台主要是针对我们业内小伙伴,他们都有相应的软件技术能力,且目前时间与精力都比较紧张;因此暂时未提供操作界面供大家使用,而是使用WebAPI方式供各位小伙伴集成到自己的系统中。当然要调用这些接口,其中一个很重要的步骤是把你们自己的系统中的业务数据,构建成这个接口的数据规范,才能把它传进来调用这些接口。关于这个接口说明视频,我已经发布在我的公众号上,当然知呼视频我也会上传。后续我会把构建这些数据的各个要点以短视频方式给大家提供讲解,从这些短视频中,并仅仅可以掌握这个平台接口的一些约束,还可以学到一些不错的干货,因为这两个月来,为了构建这个接口数据,里面表达业务的各种原理、规则、方案,设计一个推翻一个,经过无数次的迭代,最终才形成现在相对科学合理的结构。

接口说明:

https://mp.weixin.qq.com/s/DFLFNgRbJBP-iSONdeTPEQ

 

平台讲解说明视频:

https://mp.weixin.qq.com/s/x46p0en7AQVxRIsioSAmhg

 

  本系列文章在公众号不定时连载,请关注公众号(搜“让APS成为可能”)

  如需了解更多关于OptaPlanner的应用,请发电邮致:12977379@
若有需要可添加本人微信(13631823503)或QQ(12977379)实时沟通,但因本人日常工作繁忙,通过微信,QQ等工具可能无法深入沟通,较复杂的问题,建议以邮件或讨论组方式提出。(讨论组属于google邮件列表,国内网络可能较难访问,需自行解决)

 

有关OptaPlanner将弃用DRL(Drools)评分方式!!!的更多相关文章

  1. ruby - 如何以所有可能的方式将字符串拆分为长度最多为 3 的连续子字符串? - 2

    我试图获取一个长度在1到10之间的字符串,并输出将字符串分解为大小为1、2或3的连续子字符串的所有可能方式。例如:输入:123456将整数分割成单个字符,然后继续查找组合。该代码将返回以下所有数组。[1,2,3,4,5,6][12,3,4,5,6][1,23,4,5,6][1,2,34,5,6][1,2,3,45,6][1,2,3,4,56][12,34,5,6][12,3,45,6][12,3,4,56][1,23,45,6][1,2,34,56][1,23,4,56][12,34,56][123,4,5,6][1,234,5,6][1,2,345,6][1,2,3,456][123

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby-on-rails - 正确的 Rails 2.1 做事方式 - 2

    question的一些答案关于redirect_to让我想到了其他一些问题。基本上,我正在使用Rails2.1编写博客应用程序。我一直在尝试自己完成大部分工作(因为我对Rails有所了解),但在需要时会引用Internet上的教程和引用资料。我设法让一个简单的博客正常运行,然后我尝试添加评论。靠我自己,我设法让它进入了可以从script/console添加评论的阶段,但我无法让表单正常工作。我遵循的其中一个教程建议在帖子Controller中创建一个“评论”操作,以添加评论。我的问题是:这是“标准”方式吗?我的另一个问题的答案之一似乎暗示应该有一个CommentsController参

  4. ruby-on-rails - 我更新了 ruby​​ gems,现在到处都收到解析树错误和弃用警告! - 2

    简而言之错误:NOTE:Gem::SourceIndex#add_specisdeprecated,useSpecification.add_spec.Itwillberemovedonorafter2011-11-01.Gem::SourceIndex#add_speccalledfrom/opt/local/lib/ruby/site_ruby/1.8/rubygems/source_index.rb:91./opt/local/lib/ruby/gems/1.8/gems/rails-2.3.8/lib/rails/gem_dependency.rb:275:in`==':und

  5. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  6. ruby - 鸭子输入字符串、符号和数组的优雅方式? - 2

    这是针对我无法破坏的现有公共(public)API,但我确实希望对其进行扩展。目前,该方法采用字符串或符号或任何其他在作为第一个参数传递给send时有意义的内容我想添加发送字符串、符号等列表的功能。我可以只使用is_a吗?数组,但还有其他发送列表的方法,这不是很像ruby​​。我将调用列表中的map,所以第一个倾向是使用respond_to?:map。但是字符串也会响应:map,所以这行不通。 最佳答案 如何将它们全部视为数组?String的行为与仅包含String的Array相同:deffoo(obj,arg)[*arg].eac

  7. ruby - 如何以编程方式删除实例上的 "singleton information"以使其编码(marshal)? - 2

    我创建了一个由于“在运行时执行的单例元类定义”而无法编码的对象(这段代码的描述是否正确?)。这是通过以下代码执行的:#defineclassXthatmyusesingletonclassmetaprogrammingfeatures#throughcallofmethod:break_marshalling!classXdefbreak_marshalling!meta_class=class我该怎么做才能使对象编码正确?是否可以从对象instance_of_x的classX中“移除”单例组件?我真的需要一个建议,因为我们的一些对象需要通过Marshal.dump序列化机制进行缓存。

  8. ruby - Paperclip:以编程方式分配图像并设置其名称 - 2

    使用Paperclip,我想从这样的URL抓取图像:require'open-uri'user.photo=open(url)问题是我最后得到一个像“open-uri20110915-4852-1o7k5uw”这样的文件名。有什么方法可以更改user.photo上的文件名?作为一个额外的变化,Paperclip将我的文件存储在S3上,所以如果我可以在初始分配中设置我想要的文件名就更好了,这样图像就会上传到正确的S3key。像这样:user.photo=open(url),:filename=>URI.parse(url).path 最佳答案

  9. ruby - 如何以编程方式检查证书是否已被吊销? - 2

    我正在开发一个xcode自动构建系统。在执行一些预构建验证时,我想检查指定的证书文件是否已被撤销。我了解securityverify-cert验证其他证书属性但不验证吊销。我如何检查撤销?我正在用Ruby编写构建系统,但我对任何语言的想法都持开放态度。我阅读了这个答案(Openssl-Howtocheckifacertificateisrevokedornot),但指向底部的链接(DoesOpenSSLautomaticallyhandleCRLs(CertificateRevocationLists)now?)进入的Material对我的目的来说有点过于复杂(用户上传已撤销的证书是一

  10. ruby-on-rails - 以 DRY 方式覆盖 ActiveRecord 中的 "find" - 2

    我有一些模型需要在它们上面放置自定义查找条件。例如,如果我有一个联系人模型,每次调用Contact.find时,我都想限制返回的联系人只属于正在使用的帐户。我通过Google找到了这个(我对其进行了一些自定义):defself.find(*args)with_scope(:find=>{:conditions=>"account_id=#{$account.id}"})dosuper(*args)endend这很好用,除了少数情况下account_id不明确,所以我将其调整为:defself.find(*args)with_scope(:find=>{:conditions=>"#{s

随机推荐