本节书摘来异步社区《概率编程实战》一书中的第2章,第2.6节,作者:【美】Avi Pfeffer(艾维·费弗),更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.6 使用条件和约束指定证据
现在,我已经相当详细地介绍了构建模型的方法。使用Figaro时,您的很大一部分精力将花在创建模型上。但是不要忽略证据的指定。Figaro提供了3种说明证据的机制:观测值,条件和约束。
2.6.1 观测值
您已经看到使用观测值指定证据的方法。在Hello World程序中已经提供了一个例子,使用如下的语句:
greetingToday.observe("Hello, world!")```
一般来说,observe是元素上定义的一个方法,以元素的某个可能取值作为参数。它的效果是规定该元素必须采用该值。元素采用不同值的任何随机执行都被排除。这个观测值对相关元素的概率有影响。例如,在Hello World程序中,您可以看到声明这个观测值使sunnyToday更可能为true,因为根据该模型,今天是晴天比不是晴天更可能造成“Hello, world!”的问候语。
您还看到了关联观测值的另一种方法:
greetingToday.unobserve()`
上述语句删除greetingToday上的观测值(如果有的话),这样就不会排除任何随机执行。结果是,关于greetingToday的证据不影响其他元素的概率。
2.6.2 条件
observe指定元素的特定值。如果您知道元素值的某些相关情况,但是不确定该值,该怎么办?Figaro允许将任何预测作为证据。这称作条件。条件指定一个布尔函数,该函数为真某个值才可能出现。例如:
val sunnyDaysInMonth = Binomial(30, 0.2)
println(VariableElimination.probability(sunnyDaysInMonth, 5) ◁——● //打印0.172279182850003
sunnyDaysInMonth.setCondition((i: Int) => i > 8) ◁——● //晴天超过8天的证据
println(VariableElimination.probability(sunnyDaysInMonth, 5) ◁——● //打印0,因为5个晴天与证据不符```
观察证据并用它推断其他变量的概率是概率推理的核心。例如,通过观察当月的晴天数量,可以推断某人是否可能拥有好心情。使用来自2.5节的例子:
val sunnyDaysInMonth = Binomial(30, 0.2)
val monthQuality = Apply(sunnyDaysInMonth,
(i: Int) => if (i > 10) "good"; else if (i > 5) "average"; else "poor")
val goodMood = Chain(monthQuality, (s: String) =>
if (s == "good") Flip(0.9)
else if (s == "average") Flip(0.6)
else Flip(0.1))
println(VariableElimination.probability(goodMood, true))
// prints 0.3939286578054374 with no evidence`
设定条件,规定sunnyDaysInMonth的值必须大于8。然后,可以看到它对goodMood的影响:
sunnyDaysInMonth.setCondition((i: Int) => i > 8)
println(VariableElimination.probability(goodMood, true))
// prints 0.6597344078195809```
好心情的概率明显上升,因为您排除了晴天数量少于8的“坏”月份。
Figaro允许指定某个元素满足的多个条件。这通过使用addCondition方法实现,该方法向元素的现有条件中增添一个条件。增添条件的结果是元素值必须同时满足新条件和现有条件。
sunnyDaysInMonth.addCondition((i: Int) => i % 3 == 2)`
上述语句说明,除了大于8之外,sunnyDaysInMonth的值还必须比3的倍数大2。这就排除了9和10等可能值,所以,最小的可能值为11。当然,当您查询好心情的概率时,可以看到它再次上升:
println(VariableElimination.probability(goodMood, true))
// prints 0.9```
可以用removeConditions删除某个元素上的所有条件:
sunnyDaysInMonth.removeConditions()
println(VariableElimination.probability(goodMood, true))
// prints 0.3939286578054374 again`
顺便说一句,观测值只是条件的一个特例,要求某个元素取单一特定值。下面总结与条件相关的方法。
setCondition是元素上的一个方法,以一个预测作为参数。预测必须是从元素值类型到布尔类型的函数。setCondition方法使这个预测成为元素上的唯一条件,删除现有条件和观测值。
addCondition也是元素上的方法,以预测作为参数,预测必须是从元素值类型到布尔类型的函数。addCondition在现有条件和观测值之上添加这个预测。
removeConditions是元素上的方法,删除元素的所有条件和观测值。
2.6.3 约束
约束提供了规定元素相关情况的更通用手段,它通常有两个目的:(1)作为指定“软”证据的手段;(2)作为提供模型中元素间附加关系的手段。
用作软证据的约束
假定您有关于元素的某种证据,但是不是很确定。例如,您认为我看上去脾气暴躁,但是从外表无法得知我的情绪。所以您不指定goodMood为false这样的硬证据,而是指定软证据:goodMood为false的可能性大于true的可能性。
这可以使用约束实现。约束是一个从元素值到Double类型值的函数。虽然Figaro没有强制规定,但是约束在这个函数值始终在0和1(含)之间时工作得最好。例如,为了表示我的情绪似乎暴躁但是不确定的证据,您可以为goodMood添加一个约束,当goodMood为true时生成值0.5,在goodMood为false时生成1.0:
goodMood.addConstraint((b: Boolean) => if (b) 0.5; else 1.0)```
这个软约束可以这样解读:在其他条件不变的情况下,goodMood为true的可能性只有false的一半,因为0.5是1.0的一半。第4章将讨论这方面的数学运算,简而言之,某个元素值的概率乘以该值的约束结果。所以,在这个例子中,true的概率乘以0.5,false的概率乘以1.0。这样做之后,概率加总不一定等于1,所以将被按比例调整,使之加起来等于1。如果没有这个证据,我认为goodMood为true和false的可能性相同,所以它们的概率均为0.5。看到这个证据之后,首先将两个概率乘以约束结果,得出true的概率为0.25,false的概率为0.5,这两个数加起来不为1,经过比例调整,最后的答案是goodMood为true的概率是1/3,为false的概率是2/3。
条件和约束之间的不同是,在条件中,声明某些状态是不可能的(概率为0)。相比之下,约束改变不同状态的概率,但是除非结果为0,否则不会使某个状态变为不可能。条件有时候称作硬条件,因为它们设置了可能状态不能违反的规则,而约束被称作软约束。
回到示例程序,在添加这个约束之后查询goodMood,将看到概率因为这个软证据而下降,但是不像设置“我很暴躁”的硬证据那样为0:
println(VariableElimination.probability(goodMood, true))
// prints 0.24527469450215497`
约束提供了和条件类似的一组方法。
setConstraint是元素上的一个方法,以预测作为参数。预测必须是从元素值类型到Double类型值的函数。setConstraint方法使该预测成为元素上的唯一约束。
addConstraint也是元素上的一个方法,以预测作为参数,该参数必须是从元素值类型到Double类型值的函数。addConstraint方法在任何现有约束上添加这个预测。
removeConstraints是元素上的一个方法,从元素中删除所有约束。
条件和约束是相互独立的,所以设置条件或者删除所有条件不会删除任何现有约束,反之亦然。
作为连接元素的约束
约束有一种强大的用途。假定您认为两个元素的值相关,但是无法在元素定义中捕捉。例如,假设您的垒球队胜率为40%,每场比赛可以通过Flip(0.4)定义。再假设您认为自己的球队有连续性,所以相邻的比赛可能有相同的结果。您可以添加对相邻比赛的约束以捕捉这一信念,这个约束说明相邻比赛得到相同值的可能性大于得到不同值的可能性。用如下的代码可以实现上述模型。我介绍的是3场比赛的代码。但是可以用数组和for循环将其推广到任何数量的比赛。
首先,定义3场比赛的结果:
val result1 = Flip(0.4)
val result2 = Flip(0.4)
val result3 = Flip(0.4)```
现在,为了表示发生的情况,创建一个allWins元素,当所有结果都为真时,其值为true:
val allWins = Apply(result1, result2, result3,
(w1: Boolean, w2: Boolean, w3: Boolean) => w1 && w2 && w3)`
我们来看看添加任何约束之前所有比赛全胜的概率:
println(VariableElimination.probability(allWins, true))
// prints 0.064000000000000002```
现在添加约束。您将定义一个makeStreaky函数,取得两个比赛结果并对其添加连续性约束:
def makeStreaky(r1: Element[Boolean], r2: Element[Boolean]) {
val pair = Apply(r1, r2, (b1: Boolean, b2: Boolean) => (b1, b2))
pair.setConstraint((bb: (Boolean, Boolean)) =>
if (bb._1 == bb._2) 1.0; else 0.5
)}`
这个函数以两个Boolean元素为参数,这两个参数表示两场比赛的结果。因为约束只能应用到一个元素,您希望使用约束创建两个元素之间的关系,所以首先将两个元素打包成单一元素,该元素的值是一个二元组。这通过Apply(r1, r2, (b1: Boolean, b2: Boolean) => (b1, b2))实现。现在,您有了一个值为两场比赛结果配对的元素。然后,设置该配对的约束为一个函数,该函数取一对Boolean变量bb,如果bb._1 ==bb._2(配对的第一部分和第二部分相等)则返回1,否则返回0.5。这个约束说明,在其他条件不变时,两场比赛结果相同的概率两倍于不同的概率。
现在,您可以使相邻两场比赛的结果保持延续性,并查询所有比赛全胜的概率:
makeStreaky(result1, result2)
makeStreaky(result2, result3)
println(VariableElimination.probability(allWins, true))
// prints 0.11034482758620691```
可以看到,概率因为球队的连续性而明显上升。
注意:
了解概率图模型的人将会注意到,使用本节中阐述的约束使Figaro能够描述无方向模型,如马尔科夫网络。