特定应用的贝叶斯网络的Python实现

34

这就是我提出这个问题的原因: 去年我编写了一些C++代码来计算一种特定类型模型(由贝叶斯网络描述)的后验概率。该模型工作得很好,其他人开始使用我的软件。现在我想改进我的模型。由于我已经为新模型编写了略有不同的推理算法,所以我决定使用Python,因为运行时间不是关键,而且Python可能让我编写更优雅和易于管理的代码。

通常在这种情况下,我会搜索已有的Python贝叶斯网络包,但是我所使用的推理算法是自己编写的,而且我认为这也是学习Python良好设计的绝佳机会。

我已经找到了一个非常好的用于网络图的Python模块(networkx),它允许您将字典附加到每个节点和每条边。基本上,这可以让我给节点和边属性。

对于特定网络及其观察数据,我需要编写一个函数来计算模型中未分配变量的似然性。

例如,在经典的“Asia”网络(http://www.bayesserver.com/Resources/Images/AsiaNetwork.png)中,"XRay Result"和"Dyspnea"状态已知,我需要编写一个函数来计算其他变量具有某些值的概率(根据某个模型)。

这是我的编程问题: 我打算尝试几个模型,在未来可能会尝试另一个模型。例如,一个模型可能看起来与Asia网络完全相同。在另一个模型中,可能会从“Visit to Asia”指向“Has Lung Cancer”的有向边添加。另一个模型可能使用原始有向图,但“Tuberculosis or Cancer”和“Has Bronchitis”节点给出“Dyspnea”节点的概率模型可能不同。所有这些模型都将以不同的方式计算似然性。

所有模型都会有很大的重叠;例如,多个边进入一个 "Or" 节点,如果所有输入都是 "0",则将始终产生 "0",否则为 "1"。但是有些模型将具有在某个范围内取整数值的节点,而其他模型将是布尔值。
以前我曾苦恼如何编写此类内容。我不想撒谎;代码被复制并粘贴了不少,有时我需要将单个方法的更改传播到多个文件中。这一次,我真的希望花时间用正确的方式来做这件事。
几个选项:
  1. 我已经按照正确的方式进行了此操作。先编写代码,后提问。将代码复制并粘贴,并为每个模型创建一个类会更快。世界是一个黑暗而杂乱无序的地方...
  2. 每个模型都是自己的类,同时也是通用 BayesianNetwork 模型的子类。这个通用模型将使用一些将被覆盖的函数。斯特鲁斯特鲍尔德会为此感到自豪。
  3. 在同一个类中创建多个函数来计算不同的可能性。
  4. 编写通用 BayesianNetwork 库,并将我的推理问题实现为由该库读取的特定图形。节点和边应给出像“布尔”和“OrFunction”这样的属性,这些属性可以在已知父节点状态的情况下用于计算不同结果的概率。这些属性字符串(如 "OrFunction")甚至可以用于查找并调用正确的函数。也许几年后我会制作类似于 Mathematica 1988 版本的东西!
非常感谢您的帮助。
更新:面向对象的思想在这里非常有帮助(每个节点都有一组指定的先驱节点,具有某个节点子类型,并且每个节点都有一个可能性函数,该函数计算其在给定前任节点的状态下的不同结果状态的可能性等)。面向对象编程真是太棒了!
4个回答

22

我已经在业余时间里研究这类问题有一段时间了。目前,我正在第三或第四个版本中解决同一个问题。实际上,我正准备发布另一个版本的Fathom(https://github.com/davidrichards/fathom/wiki),其中包括动态贝叶斯模型和不同的持久性层。

随着我的回答变得更加清晰明了,它变得非常冗长。对此我深感歉意。以下是我攻击这个问题的方式,它似乎间接地回答了你的一些问题:

我首先从Judea Pearl在贝叶斯网络中关于信念传播的分解开始。也就是说,这是一个图形,其中来自父节点的先验几率(因果支持)和来自子节点的似然(诊断支持)。因此,基本类只是一个BeliefNode,就像你描述的那样,它在BeliefNodes之间增加了一个额外的节点,即LinkMatrix。这样,我通过使用不同类型的LinkMatrix显式选择所使用的似然类型。同时,这也使得后续更容易解释信念网络所执行的任务,并保持计算的简单性。

我对基本BeliefNode进行任何子类化或更改,只是为了将连续变量分组,而不是更改传播规则或节点关联。

我决定将所有数据都保留在BeliefNode内部,只有LinkedMatrix中的固定数据。这是为了确保我以最小的网络活动维护干净的信念更新。这意味着我的BeliefNode存储:

  • 一个子节点引用数组,以及来自每个子节点的过滤后的似然和执行该子节点过滤的链接矩阵
  • 一个父节点引用数组,以及来自每个父节点的过滤后的先验概率和执行该父节点过滤的链接矩阵
  • 节点的组合似然值
  • 节点的组合先验几率
  • 计算得出的信念或后验概率
  • 所有先验概率和似然值所遵循的属性的有序列表

LinkMatrix可以使用多种不同的算法构建,具体取决于节点之间的关系性质。你所描述的所有模型都只是要使用的不同类。最简单的方法可能是默认采用或门,然后在节点之间存在特殊关系时选择其他处理LinkMatrix的方式。

我使用MongoDB进行持久化和缓存。我在事件模型内访问此数据以获取速度和异步访问。这样可以使网络表现相当高效,同时也有机会变得非常庞大。由于我是以这种方式使用Mongo,因此可以轻松地为相同的知识库创建新的上下文。例如,如果有一个诊断树,则某些诊断支持将来自患者的症状和测试。我所做的是为该患者创建一个上下文,然后基于该特定患者的证据传播我的信念。同样,如果医生说患者可能经历了两种或更多疾病,则我可以改变一些链接矩阵以不同的方式传播信念更新。

如果您不想为系统使用类似Mongo这样的东西,但计划让多个消费者在知识库上工作,则需要采用某种缓存系统以确保您始终在更新时间内对节点进行操作。

我的工作是开源的,所以您可以跟随这个项目。它全部使用Ruby编写,因此与您的Python类似,但不一定是直接替换。我喜欢我的设计之一是所有人类解释结果所需的信息都可以在节点本身中找到,而不是在代码中找到。这可以在定性描述或网络结构中完成。

因此,以下是我与您的设计有重要差异的一些方面:

  • 我不会在类内计算似然模型,而是在节点之间,在链接矩阵内进行计算。这样,我就不存在将多个似然函数合并在同一个类内的问题。我也不需要处理一个模型与另一个模型之间的问题,只需为相同的知识库使用两个不同的上下文并比较结果即可。
  • 我正在增加透明度,使人类决策变得更加明显。例如,如果我决定在两个节点之间使用默认的或门,我知道什么时候添加了它,以及这仅仅是一个默认决策。如果我稍后回来改变链接矩阵并重新计算知识库,我有一个关于为什么这样做的注释,而不仅仅是选择某种方法的应用程序。你可以让你的客户记录那种事情。无论你如何解决,从分析师那里获取关于为什么他们以一种方式设置事物而不是另一种方式的步骤式对话可能是个好主意。
  • 我可以更明确地说明先验概率和似然。我不确定这一点,我只看到你在使用不同的模型来改变你的似然数值。如果你计算后验信念的模型没有这种分解方式,那么我说的大部分可能完全不相关。我有一个优点,就是能够进行三个异步步骤,这些步骤可以按任意顺序调用:将更改的似然性传递到网络中,将更改的先验几率向下传递到网络中,并重新计算节点本身的组合信念(后验概率)。

一个重要的警告:我谈论的一些内容尚未发布。我一直在处理我所说的内容,直到昨晚大约2点左右,所以它肯定是最新的,并且我一直在关注,但并不是所有内容都可供公众使用。因为这是我的热情,如果你想合作做一个项目,我很乐意回答任何问题或与你合作。


3
Mozart/Oz3 约束推理系统解决类似问题: 您可以通过对有限域变量、约束传播器和分配器以及代价函数进行约束描述您的问题。当不能再进行推理但仍存在未绑定变量时,它使用您的代价函数将问题空间划分为最有可能减少搜索代价的未绑定变量:即如果 X 位于 [a,c] 之间(a
您可以在基于图的库中实现写时复制方案(提示:numpy 使用各种策略来最小化复制;如果您将图形表示法基于它,可能会免费获得写时复制语义),从而达到您的目标。

2
我不太熟悉贝叶斯网络,希望以下内容有用:
过去我曾经遇到一个看起来类似的问题,是针对高斯过程回归器而不是贝叶斯分类器。
最终我使用了继承,这种方法非常有效。所有模型特定的参数都在构造函数中被设置。calculate() 函数是虚拟的。 以这种方式级联不同的方法(例如,一个将任意数量的其他方法组合在一起的求和方法)也非常好用。

2

我认为你需要询问一些影响设计的问题。

  1. 您将多久添加一个模型?
  2. 预计库的使用者会添加新的模型吗?
  3. 添加模型的用户占总用户的比例是多少,而使用现有模型的用户占比又是多少?

如果大部分时间都用于使用现有模型,而新模型较少,则继承可能是我会使用的设计。 这使得文档易于结构化,使用它的代码也很容易理解。

如果库的主要目的是提供一个平台来尝试不同的模型,则可以选择具有映射到基于父项计算函数子句的属性的图表。该库将更加复杂,图形创建也将更加复杂,但它将更加强大,因为它允许您执行根据节点更改计算函数的混合图形。

无论您最终采用什么设计,我建议先从简单的一类一实现设计开始。 通过一组自动化测试进行测试,然后在完成后进行重构以进入更完整的设计。 另外,别忘了版本控制 ;-)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接