医疗AI的通用规则引擎设计方案|@攻城狮

2022-11-18

本期 @攻城狮栏目邀请了惠每科技高级算法专家吴哲夫,介绍医疗AI领域一种通用规则引擎的设计方案,这套规则引擎可让医师理解AI规则执行逻辑,极大地释放编码人员的创新能力。

 

规则引擎是指将专家规则,包含⽂献规则、临床专家规则、产品逻辑等,进行系统化、⾃动化、标准化的软件引擎。在临床决策支持系统(CDSS)业务应用层面,规则引擎是重要的基础设施,能够将一些医学逻辑形成标准的执行流程,依托于后结构化的电子病历信息,形成一套完整的建议方案,实时提供给临床医生。

随着CDSS在病历质控、风险预警、临床路径等领域的应用深入,各业务线规则呈爆炸性增长,加之医学领域具有服务器性能限制、规则实时性、规则体量巨大等特点(如几秒内要完成数十用户、几万条规则的执行,并在医生修改病历时及时更新规则结果),亟需一套统一高效的规则引擎。为此,惠每科技打造了一款通用、可统一各类规则系统的规则引擎——飞天,该引擎具有单机、高效、可回溯、低代码或无代码等特点,在多项业务中展现了巨大的包容性和价值。

图1 飞天引擎界面示例

设计思路:“原语、句法、结构”三要素的设计

传统的CDSS规则引擎开发流程包括:需求评审-排期-文档-开发-测试-联调-上线,如果需要追踪用户使用反馈,还需要执行线上评测-查因-优化等一系列流程。在传统的需求研发周期中,经常会遇到现场查因困难、规则逻辑黑盒、产品思想与研发思想不一致等一系列问题。如何通过统一固定的方式来描述统一规则就显得尤为重要。

一套完整的推理方案,由数据、结点、条件、分支和结论组成。如图2的静脉血栓栓塞症防治流程,这种也是产品设置流程时最常用的流程图。我们知道图的基本构成为结点和边。在传统的流程图中,结点表示一种操作,更细分一下有断言类型操作、聚合类型操作。而边负责信息传递,比如传递条件、否单值信息,上游结点操作后的结果信息。但是如果按照这种流程图的设计思路来设计规则引擎,就会误入歧途,你会发现表达能力有限,但是抽象程度和复用性不强。所以,我们要从代码的执行逻辑入手,从编译原理的基本操作入手,来设计真正可以无限表达的规则引擎。                    

                         图2 静脉血栓栓塞症(VTE)防治流程与逻辑

为此,我们将代码规则逻辑进行原语、句法与结构的拆分。这有些类似编译器的工作过程,但是应用在无关底层语言的更上层的规则逻辑引擎设计场景中。(如果你有幸翻到《编译原理》〔龙书〕第二版,你会发现编译器的概念也结合当前软件开发和设计,被赋予了新的更广泛的意义。)

原语

一个规则执行引擎服务能力的上限,取决于其表达能力的完备程度。为此,我们定义了如下基础的原语语法,这些原语是我们Layer操作的视图,也指导了后面我们要讲到的“学习器”的具体设计。

设计的原语不要求多,而是要求精和包容性,要求在实际应用中绝大多数的操作,可以由这几类原语来完成,这样我们既可以归类不同的操作和表达,又可以统一优化性能。当然,如果生搬硬套,有些实际操作可以用多条原语套用,但是会增加嵌套程度和降低理解。我们的本质,是在规范统一规则执行的同时,又能够提升规则的表达能力,提高开发设计人员制定规则的上限。

原语是整个结构中最重要的一环,形如HQL的各种表达式,都可以在该原语框架中实现,比如单表的分组聚合+窗口函数,可以通过“原语1+分组聚合函数+窗口函数参数(args)”来实现。实际上,很多语法结构都可以用特例的上述原语来表达。而原语内的function函数,又根据需要抽象出了各类数学表达式,甚至是一个复杂的机器学习推断模型,这一点在后面的“结构”章节中介绍。

句法

此处的句法是原语之间的串联关系,并不是NLP(自然语言处理)中的主谓宾等语言结构,是传统意义上的流程图的线段连接的机器表述。传统流程图中如if True then ... else ....这种分支句法是最常见的一种。

可以理解成使用飞天的语法,但是语法更适合于表达后面我们fuction函数所包含的内容,因此我们用句法来表示这种连接关系。我们在句法上没有设计过多形态,只设计成一个最简单的自动机形态,支持了任意的真值逻辑表达式,优先级也与数学逻辑表达式相同。因为我们发现,只需要在原语结果上进行多状态管理(见后面介绍的激活器),这种简单的逻辑表达式句法配合原语的结果,会比复杂的自动机有更好的直观性和可理解性。

不过对于固定产品形态,我们也可以通过接口方便的包装句法结构。比如针对病历质控,我们声明if (not) satisfy '语句1' and (not) satisfy '语句2'... then '提示xxx1,xxx2缺陷',其中'xxx'是相应语句产出的结果。针对其他业务,我们也可以声明相关的句法,对于该句法对应的数据结构和形式,也会做相应的封装。这样我们可以在统一的规则执行逻辑上,满足不同业务和数据返回结构的需求。

结构

除却原语、句法这两种基本要素,还有一个要素就是结构。不同的是,结构是确保原语与句法能够执行的必要条件,但是又不如原语和句法的抽象程度高、具有指导意义,因为结构可能跟系统、变成语言、个人设计风格有关。

在句法与原语之间,有一个信息存储结构,是连接句法与原语的桥梁,这种结构只要设计可以支撑原语与句法不间断的嵌套执行就算达成目标,没有特定的程式。我们可以参考Spark中RDD的设计思路,将数据与结果都设计成统一的读取和存储方式。由于规则流程中很多具有信息的连贯性,上游的信息可能会不断的向下游传递并最终应用,所以我们设计让结构和信息有相同的读取方法。在一些解释性语言中这种设计很容易实现,而在一些面向对象或面向过程的语言中,就需要一些反射机制或者额外的数据结构封装来达成这种目的。

架构(设计细节):生成-推断-回溯

生成

生成对应着我们“原语”设计这部分内容,当前生成结构如下图:

图3 生成结构示例

简单的做一下说明:

1)学习函数:对应原语的代码实现,在真实数据中学习知识。

学习函数举例:实体抽取(ML、AC、regex、dict match),排序(max、min、topK),比较(大小、长度、相似度),归并(合并、笛卡尔积、两两对应)。

2)激活函数:对应对于原语结果的处理(对于断言类型、聚合类型)有不同的激活方法,比如有时满足正则的需要激活,不满足的需要静默,有时候恰恰相反。

比如,根据正则匹配的方式,我们区分捕获式、非捕获式,各种预查和断言监测。而触发的结果会根据激活设置的不同出处在遗传数据的不同字段下。

3)损失函数:实际对应原语中satisfy / not satisfy的语言实现,但是对于不同类型的function,实际还会有细微的差别,也为了使后续流程更好的进行,更简洁。

损失函数配合激活函数的使用,可以极大地降低一些特殊逻辑的编写复杂性,比如any satisfy E ===  all not satisfy 非 E

4)遗传数据:实际是现在激活函数的一部分,对于未满足的数据、满足条件的数据,和进入计算流程但是为满足计算逻辑的数据进行区别存储,而且要保证尽量少的进行数据copy操作,而进行引用的传递。

在这种结构中,我们可以定义更多的语法糖,来支持当前即使最强大的HQL + UDF也无法支持的语法设计方案。简单的比如窗口函数、分组聚合等SQL语法,复杂的比如只能通过一些级联查询、跨表查询加UDF才能实现的一些函数功能,已经作为内置方便可用的学习器方法。特别是在学习器学习方法底层的设计中,我们打破传统one target one function的设计思路,也尽量少用重载和覆盖,而是从循环结构、循环次数出发,聚合归类相同的方法,对循环体内的内部子方法进行参数(甚至执行数据)控制。

也就是,只有真正到初始化阶段,编译器才知道学习器真正的学习方法。这有点类似于多态的思想,但是通过执行器的预处理,尽可能将“多态”的判定和分支路径的执行过程提前保存在堆中,比多态的方式更加高效。

推断

推断即对应设计语言中的“句法”。一个流程可能有多重不同的推断,用于不同的业务/业务项。

我们从一个最简单例子来完整描述“生成”与“推断”的过程。

在上述过程中,并没有描述X与A的连接关系和信息传输内容,连接关系是由“引擎管理组件”控制动态生成DAG图,这就是为何这部分内容叫做“生成”,而信息继承方案是由激活器的参数来指导激活函数和损失函数来完成的。

这只是一个简单的病案规则来展示执行过程,真实的其他业务规则逻辑要比这个复杂得多。即使是病案细分完整性,我们也可以将全部需要细分的诊断用列表保存,将相应的正确或错误书写方式用词典表示,然后通过分组聚合的高阶函数由一条规则逻辑实现全部需要细分的诊断。

回溯

回溯功能是进行过程检验和人工评测的基础。在通常的规则引擎中,为了提高效率,我们只需关注和存储业务结果,而不用额外保存中间过程和中间结果,研发人员进行测试会进行断点检测。由于我们的生成结构图本身就很清晰,所以我们也增加了回溯功能。

首先解决最大的问题是,回溯功能要回溯什么内容?规则评测专家可能并不关心参数,而是中间激活结果。研发人员需要展示参数和底层学习器执行的全部状态(激活、沉默、其他)来判定学习器是否按照预期运行。

第二个要解决的问题就是,如何设计回溯组件?根据生成的思想,在实现底层架构的时候,其实有很多可选的方案。为了在满足尽量详实的中间信息,并降低内存占用,我们选择了减少新对象的方式。因为每个用户的数据不同,可能导致真实执行状态中执行逻辑和分支不同,而我们将所有的执行逻辑(即“生成”相关的对象),会在规则初始化时将所有可能分支保留在栈中,而数据是每次新用户请求时唯一需要创建的对象。

如图4,是我们某个质控项单用户的执行过程:

我们将回溯组件的信息与前述“结构”进行绑定,这种绑定并非“耦合”,而是按照回溯需求来进行保存必要的中间过程。而我们将统一的结果信息挂在每个用户的执行过程中,由激活器进行填充,填充动作由“引擎管理组件”控制。跟“生成”结构的设计类似,我们将结果结构在初始化时保存在“栈”,结果不断进行填充和擦除,而最重要的是每个回溯结果的生存周期和擦除逻辑。

规则可视化:让用户易理解,让规则开发更高效

为了让专家能够审视规则合理性或者在指导医师时让医师能理解规则执行逻辑,我们将规则图的动态执行、结点的静态参数以及动态执行结果,都通过可视化的界面展示出来。由于我们结点设计的统一性,可以让规则设计人员在页面上完成规则设计以及执行逻辑的全部过程,而不需要研发同学的参与。由于可以动态地展示执行结果,可视化的过程还能兼具上线调试的功能以及大数据验证的过程。

图5 飞天的拖拽生成方式

更进一步,为了让一些业务相关的人员减少对于执行图(逻辑)、结点(函数操作)、参数的理解,我们在“专业编辑”功能的基础上,又简化(封装)了一套规则模板,模板极大程度地降低了参数设计数量以及业务人员无需关注的返回结果、返回结构的设计,研发人员只需要在模板结果上定制化业务需要的数据结构和展示文案(比如默认执行的最后一个结点的结果字段填充进入),就可以让规则设计者产出业务需要的逻辑。

图6 飞天模板规则编辑

小公司,大智能。短目标,长规划。抓住机遇,不忘初心。

飞天的产生,绝非重蹈低代码陷阱中的降低代码数量的覆辙,限制开发能力。相反,它是在解决并规范了底层数据存储、信息流转、原子操作、内存管理、流程统一等一系列系统工程的条件下,极大地释放了编码人员的创新能力,提高了代码质量和开发效率。并且配合开放部分可视化的工具,让非编码人员的产品专家可以自由地定制规则逻辑,使规则制定与开发人员更专注在规则的专业性、客户的体验感和产品的丰富度上面,而非受限于数据处理、函数操作、机器原理等软件工程层面。

此文章公开飞天的设计思想与思路,是希望能对大家(特别是信息化服务体量属于中小量级的企业)有所启发和触动,进一步提升服务质量和服务能力。

特邀专家:吴哲夫-惠每科技高级算法专家

成为我们的

合作伙伴

医院演示预约
提交以下真实信息,我们将尽快联系您