本节书摘来异步社区《概率编程实战》一书中的第2章,第2.5节,作者:【美】Avi Pfeffer(艾维·费弗),更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.5 用Apply和Chain构建更复杂的模型
Figaro提供两种构建模型的有用工具,称作Apply和Chain。这两种工具都是重要的元素。Apply可以在Figaro中引入Scala,利用其全部能力。Chain可以无限的方式创建元素之间的相互依赖关系。目前为止您所看到的复合元素(如If和复合Flip)可以特定的预定义方式创建依赖关系。Chain可以超越这些预定义依赖关系,创建您所需要的任何依赖。
2.5.1 Apply
我们从Apply开始,这个元素在com.cra.figaro.language包中。Apply以一个元素和一个Scala函数作为参数,它代表将Scala函数应用到该元素值以获得新值的过程。例如:
val sunnyDaysInMonth = Binomial(30, 0.2)
def getQuality(i: Int): String =
if (i > 10) "good"; else if (i > 5) "average"; else "poor"
val monthQuality = Apply(sunnyDaysInMonth, getQuality)
println(VariableElimination.probability(monthQuality, "good"))
// prints 0.025616255335326698```
上述代码中的第2行和第3行定义一个名为getQuality的函数。这个函数取一个Integer型参数,getQuality函数中的参数局部名称为i。根据第3行中的代码,该函数返回一个字符串。
第4行定义一个Apply元素monthQuality。Apply元素的结构如图2-9所示,它取两个参数,第一个是元素,在本例中是Element[Int]类型的sunnyDaysInMonth。第二个参数是函数,参数类型与元素的值类型相同。在我们的例子中,函数getQuality取得一个Integer型参数,因此两者互相匹配。该函数可以返回任何类型的值,在我们的例子中,函数返回一个字符串。
<div style="text-align: center"><img src="https://yqfile.alicdn.com/f1e137d41c82a7bcc250cc1250ad1e380334cac4.png" width="" height="">
</div>
下面介绍Apply元素定义随机过程的方式。它首先生成第一个元素参数的值。在我们的例子中,生成当月中晴天的特定数量,我们假定生成的是7。然后,该过程取得第二个函数参数,并将其应用到生成的值。在我们的例子中,过程将函数getQuality应用到7,获得结果average。该结果成为Apply元素的值。从这里可以看出,Apply的可能值和函数参数的返回值类型相同。在我们的例子中,Apply元素是一个Element[String]。
如果您对Scala还很陌生,那么我来介绍一下匿名函数。如果只想在一个位置上使用,为每个Apply元素定义单独的函数可能令人烦恼,尤其是上例中的简短函数。Scala提供了匿名函数,可以直接在使用的位置定义。图2-10展示了一个匿名函数的结构,它的定义和getQuality相同。这一结构的组件类似于命名函数。函数有一个参数i,类型为Integer。=>符号表示定义的是匿名函数。最后是一个函数体,与getQuality相同。
<div style="text-align: center"><img src="https://yqfile.alicdn.com/753fae657cee68ae4303e0d04daccb9a50e2c784.png" width="" height="">
</div>
您可以使用匿名函数定义和前一个元素等价的Apply元素:
val monthQuality = Apply(sunnyDaysInMonth,
(i: Int) => if (i > 10) "good"; else if (i > 5) "average"; else "poor")`
现在,您可以查询monthQuality。不管使用哪一个版本的monthQuality,得到的答案都相同:
println(VariableElimination.probability(monthQuality, "good"))
// prints 0.025616255335326698```
虽然Apply的这个例子是人为的,但是使用它有许多实际的理由。下面是几个例子。
您有一个双精度元素,希望将其值舍入为最近的整数。
您有一个元素的值类型是一个数据结构,希望概括该数据结构的一个属性。您可能有一个以列表为基础的元素,希望知道列表项目数量大于10的概率。
两个元素之间的关系最好由一个物理学模型编码。在这种情况下,您可以使用一个Scala函数表示物理关系,使用Apply将物理模型嵌入Figaro。
多参数Apply
使用Apply时,并不限于只有一个参数的Scala函数。Figaro中定义的Apply可以使用最多5个参数。使用多于一个参数的Apply,是将多个元素结合在一起承载另一个元素的好办法。例如,下面是两个参数的Apply:
val teamWinsInMonth = Binomial(5, 0.4)
val monthQuality = Apply(sunnyDaysInMonth, teamWinsInMonth,
(days: Int, wins: Int) => {
val x = days * wins
if (x > 20) "good"; else if (x > 10) "average"; else "poor"
})```
这里,Apply的两个元素参数是sunnyDaysInMonth 和teamWinsInMonth,它们都是Element[Int]。函数参数有名为days和wins的Integer参数。这个函数创建一个局部变量x,其值等于days * wins。注意,因为days和wins是常规的Scala Integer变量,x也是常规Scala变量,而不是Figaro元素。实际上,Apply函数参数中的任何东西都是常规的Scala内容。Apply取得在常规Scala值上操作的Scala函数,将其“提升”为在Figaro元素上操作的函数。
现在,查询这个版本的monthQuality:
println(VariableElimination.probability(monthQuality, "good"))
// prints 0.15100056576418375```
得出的概率值略微提升。似乎,我的垒球队有机会振奋人心。更重要的是,尽管这是一个简单的例子,但是例中的概率在没有Figaro的情况下也很难计算。
####2.5.2 Chain
顾名思义,Chain(链)用于将元素链接为一个模型,模型中的元素依赖于另一个元素,那个元素又依赖其他的元素,依次类推。这与概率的链式法则相关,第5章将介绍这一法则。但是,理解Chain并不需要知道链式法则。
Chain也包含在com.cra.figaro.language包中。解释Chain的最简单方式是通过一张图。图2-11展示了两个元素:goodMood元素依赖monthQuality元素。如果您将其看作一个随机过程,该过程首先生成monthQuality的值,然后使用该值生成goodMood的值。这是贝叶斯网络的一个简单例子,第4章中您将学习这种方法。图中借用了贝叶斯网络的术语:monthQuality称为父节点,goodMood称为子节点。
<div style="text-align: center"><img src="https://yqfile.alicdn.com/48523d3d1e5f43d1bcadd254164fe34341e5c050.png" width="" height="">
</div>
因为goodMood依赖于monthQuality,goodMood使用Chain定义。monthQuality元素在前一小节已经定义。下面是goodMood的定义:
val goodMood = Chain(monthQuality, (s: String) =>
if (s == "good") Flip(0.9)
else if (s == "average") Flip(0.6)
else Flip(0.1))```
图2-12展示了这个元素的结构。和Apply类似,Chain取两个参数:一个元素和一个函数。在本例中,元素是父节点,函数被称为链函数。Chain和Apply之间的差别是Apply中的函数返回常规的Scala值,而Chain中的函数返回一个元素。在本例中,函数返回一个Flip,选择哪一个Flip取决于monthQuality的值。所以,这个函数取得类型为字符串的参数,返回一个Element[Boolean]。
这个Chain元素定义的随机过程如图2-13所示。这一过程有3个步骤。首先,为父节点生成一个值。在本例中,为monthQuality生成值average。其次,对该值应用链函数以获得一个元素,该元素称作结果元素。在例子中,您可以检查链函数的定义,发现结果元素是Flip(0.6)。第三,从结果元素生成一个值,例子中生成的是true。这个值成为子节点的值。
图2-13 Chain元素定义的随机过程。首先,为父节点生成一个值。接下来,根据链函数选择结果元素。最后,从结果元素生成一个值
下面我们总结Chain中涉及的所有类型。Chain由两个类型参数化——父节点的值类型(称作T)和子节点的值类型(称作U)。
父节点类型为Element[T]。
父节点值类型为T。
链函数类型为T => Element[U]。这意味着该函数从T类型得出Element[U]。
结果元素的类型为Element[U]。
链值类型为U。
子节点的类型为Element[U]。这是整个Chain元素的类型。
在我们的例子中,goodMood是Element[Boolean],可以查询其值为true的概率:
println(VariableElimination.probability(goodMood, true))
// prints 0.3939286578054374```
多参数Chain
让我们来考虑一个稍微复杂些的模型,其中goodMood依赖monthQuality 和 sunnyToday,如图2-14所示。
<div style="text-align: center"><img src="https://yqfile.alicdn.com/5dc6df2f9b859e0adc934163f1f846e68d3902c6.png" width="" height="">
</div>
可以使用一个双参数的Chain捕捉上述事实。在本例中,链中的函数有两个参数——名为quality的字符串参数和名为sunny的布尔型参数,返回一个Element[Boolean]。goodMood同样是Element[Boolean]。
下面是代码:
val sunnyToday = Flip(0.2)
val goodMood = Chain(monthQuality, sunnyToday,
(quality: String, sunny: Boolean) =>
if (sunny) {
if (quality == "good") Flip(0.9)
else if (quality == "average") Flip(0.7)
else Flip(0.4)
} else {
if (quality == "good") Flip(0.6)
else if (quality == "average") Flip(0.3)
else Flip(0.05)
})
println(VariableElimination.probability(goodMood, true))
// prints 0.2896316752495942`
注意:
和Apply不同,Chain结构仅定义为一个或者两个参数。如果需要更多参数,可以结合Chain和Apply。首先使用Apply将参数元素打包为单一元素,该元素的取值是参数值的元组。将这个元素传递给Chain。这样Chain就得到了元素求值中需要的所有信息。
使用Apply和Chain的map及flatMap
熟悉Scala的读者请注意,在某种程度上,Figaro元素类似于Scala集合(如列表)。列表包含一组值,而元素包含一个随机值。正如可以对列表中的每个值应用函数以获得新列表那样,您可以对包含在元素中的随机值应用函数以得到一个新元素。这正是Apply所做的!对于列表,对列表中的每个值应用某个函数是通过使用map方法实现的。类似地,使用Apply就为元素定义了map操作。因此,可以将Apply(Flip(0.2), (b: Boolean) => !b)写作Flip(0.2).map(!_)。
同样,对于列表,可以对每个值应用某个函数返回列表,然后用flatMap将所有结果列表扁平化为单一列表。Chain以同样的方式对元素中包含的随机值应用函数,获得另一个元素,然后取出元素中的值。所以,元素的flatMap用Chain定义。因此,您可以将Chain(Uniform(0, 0.5), (d: Double) => Flip(d))写做Uniform(0, 0.5).flatMap(Flip(_))。(顺便说一句,要注意使用Chain定义复合Flip的方式。许多Figaro复合元素可以用Chain定义。)
Scala中最棒的特性之一是任何定义了map和flatMap的类型都可以用于for循环。可以对元素使用for标记。
可以编写如下代码:
for { winProb <- Uniform(0, 0.5); win <- Flip(winProb) } yield !win```