信念网络,也称为贝叶斯网络或图形模型,是一种图,其中节点带有条件概率表(CPT)代表随机变量,节点之间的链接或箭头表示影响。如图1所示的信念网络,可以通过1-P(X=F)来获得P(X=T)。信念网络在数学上等同于联合概率分布,它为提供了一种自然的方式来模拟一组感兴趣变量之间的概率关系。由于其概率性质,信念网络在编码不确定知识方面非常有用。一旦掌握了网络,就可以从中进行推理,无论是诊断性的(从效果到原因),还是因果性的(从原因到效果),或者是混合的。然后可以根据推理结果做出决策或建议。这就是信念网络在人工智能中找到其应用方式的原因。
信念网络是一个在理论和应用上都值得探索的丰富领域。然而,由于篇幅有限,本文不会讨论任何细节。相反,将通过一个简单的例子来引导,而不提供证明。希望这篇文章能让感受到信念网络是如何工作的。如果感兴趣,可以阅读像[1]这样的教科书,或者在互联网上搜索教程。
条件概率,写作P(A|B,C),告诉在事件B和C发生的情况下事件A发生的概率。一个联合概率,如P(A, B),表示事件A和B同时发生的概率。根据概率演算的乘法规则,一个联合概率可以被分解为先验概率和条件概率的乘积,P(A, B) = P(A|B)P(B),其中P(B)被称为先验或无条件概率。
推理的任务是计算一组查询变量的条件下概率P(query|evidence),给定一些证据变量。考虑图1中给出的信念网络,如果想知道草湿时不多云的概率,将计算P(cloudy=0|wetGrass=1)。通过重新排列公式(1),有:P(c=0|w=1) = P(c=0, w=1)/P(w=1)。
为了计算P(w=1),必须包括W节点的父节点的影响,这在信念网络中用箭头表示。W节点有两个父节点S和R。W的父节点也受到它们父节点的影响。重复这个过程直到达到先验概率,得到P(w=1) = ∑s,rP(s,r)P(w=1|s,r) = ∑c,s,rP(c)P(s|c)P(r|c)P(w=1|s,r)。
以类似的方式计算P(c=0, w=1),有P(c=0|w=1) = P(c=0)∑s,rP(s|c=0)P(r|c=0)P(w=1|s,r)/P(w=1)。现在所有右侧的概率都在信念网络的CPT中明确定义了。
从上面的例子中可以看到,进行推理就是计算联合概率。如果有一个算法来计算联合概率P(c,s,r,w),就可以进行信念网络中的任何推理。
让将公式(3)重写为P(w=1) = ∑cP(c)∑sP(s|c)∑rP(r|c)P(w=1|s,r)。桶消除算法[2],或者更一般的变量消除算法的思想是将求和分布在乘积上。从公式(5)的最内层(最右边)开始,P(w=1) = ∑cP(c)∑sP(s|c)∑rP(r|c)λw(s,r),其中λw(s,r) = P(w=1|s,r) = ∑cP(c)∑sP(s|c)λr(c,s),其中λr(c,s) = ∑rP(r|c)λw(s,r) = ∑cP(c)λs(c),其中λs(c) = ∑sP(s|c)λr(c,s) = λc。
现在节点被替换为包含相应节点的CPT和子节点桶的桶λ。计算过程以相反的顺序进行。它从最外层的节点开始,即根节点。如果一个节点是证据或查询,使用提供的值,否则对所有可能的值求和。然后这个过程可以递归实现。
实现有一个如图2所示的静态结构。下载的源代码是一个简化版本。它只适用于二元节点,即节点只取0和1两个值。也移除了所有的异常控制,如try-catch和asserts,并且移除了所有的优化,尽管其中一些是显而易见和直接的。目的是使代码更容易阅读。一旦理解了它,调整代码应该不会太困难。
节点在XML文件中定义。有两个文件,BN_WetGrass.xml和BN_Alarm.xml,包含在下载中,都取自[1]。它们具有完全不同的拓扑结构。可以设计自己的节点。只要记住节点的顺序很重要,任何节点的父节点必须在节点之前定义。方法BNet::Build("nodes.xml")从XML文件中加载节点,然后使用这些节点创建一个信念网络。
要进行推理,可以这样做:
C#
BNet net = new BNet();
net.Build("bn_wetGrass.xml");
BNInference infer = new BElim(net)
double pr = infer.GetBelief("sprinkler=1; cloudy=0", "WetGrass=0; Rain=1");
桶在BElim::PrepareBucket()中初始化。仔细阅读例子公式(6)后,会发现每个桶不仅需要从其对应节点的父节点输入,还需要从其子桶询问的节点输入。
算法的核心在BElim::Sum(int id, int para)中,如下所示:
C#
protected double Sum(int node_id, int para)
{
// skipped ...
double pr = 0.0;
foreach (int value in theNode.Range)
{
if (theNode.IsEvidence && theNode.Evidence != value)
continue;
double tmpPr = theNode.CPT;
foreach (Bucket nxtBuck in theBuck.childBuckets)
{
int next_para = 0;
foreach (BNode node in nxtBuck.parentNodes)
{
// calculate next_para;
}
tmpPr *= Sum(nxtBuck.id, next_para);
}
pr += tmpPr;
}
return pr;
}
在这里利用了二元节点的优势。整数para用作大小为32的位数组。它允许对其进行位操作,从而使代码更有效和紧凑。求和遍历所有可能的值,除非节点有提供的值。然后它考虑其子桶的贡献。要调用子的贡献,求和必须提供参数,并让下一个桶知道哪个节点有什么值,等等。不会进一步解释,因为这些事情最好由代码而不是文字来解释。
WetGrass网络非常简单,但它已经展示了一些微妙之处。例如,可以计算在草湿的情况下喷水器工作的概率。有P(sprinkler=1|wetGrass=1) = 0.43。这种推理是诊断性的,因为它告诉从效果到原因。另一方面,如果知道是否下雨,那么P(sprinkler=1|wetGrass=1; rain=1) = 0.19,或者P(sprinkler=1|wetGrass=1; rain=0) = 1.0。看到额外的下雨信息改变了sprinkler=1的概率,尽管喷水器和雨并不直接相互影响。这种效应被称为解释性。
可以玩代码,尝试WetGrass和Alarm网络的不同推理。可能会有惊喜。推理并不总是直接的。常识并不总是正确的。
使用C#的原因是因为想学习它。使用C#作为更好的C++,而不是更好的C :)。作为一个长期的C++程序员,花了一些时间来适应使用new而不考虑在哪里放置delete。唯一让烦恼的是ArrayList。在看来,似乎没有办法在声明时指定类型;类型是在添加一个项目时决定的。这可能是一个很好的特性。然而,如果要将设计和实现分开,必须依赖注释来提醒实现者列表包含的项目类型。也许错过了一些东西。
代码是用Emacs编辑器编辑的,并由从Microsoft网站下载的C# SDK中的csc.exe编译。Emacs编辑器限制了编写更好的UI。