概率编程实战

978-7-115-44874-3
作者: 【美】Avi Pfeffer(艾维·费弗)
译者: 姚军
编辑: 王峰松

图书目录:

详情

本书讲解了如何使用Figaro语言建立垃圾邮件过滤器,以及如何应用贝叶斯和马尔可夫网络诊断计算机系统的数据问题、恢复数字图像。同时,本书也探讨了一些算法的核心原则。最后,本书讲解了如何对动态系统建模,并且解释了概率模型如何在广告宣传活动中帮助决策。

图书摘要

版权信息

书名:概率编程实战

ISBN:978-7-115-44874-3

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Avi Pfeffer

  译    姚 军

  责任编辑 王峰松

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Original English language edition, entitled Practical Probabilistic Programming by Avi Pfeffer, published by Manning Publications, USA. Copyright © 2016 by Manning Publications. Simplified Chinese-language edition, Copyright © 2017 by Posts & Telecom Press. All rights reserved.

本书中文简体字版由Manning Publications授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制本书任何内容。

版权所有,侵权必究。


概率推理是不确定性条件下做出决策的重要方法,在许多领域都已经得到了广泛的应用。概率编程充分结合了概率推理模型和现代计算机编程语言,使这一方法的实施更加简便,现已在许多领域(包括炙手可热的机器学习)中崭露头角,各种概率编程系统也如雨后春笋般出现。本书的作者Avi Pfeffer正是主流概率编程系统Figaro的首席开发者,他以详尽的实例、清晰易懂的解说引领读者进入这一过去令人望而生畏的领域。通读本书,可以发现概率编程并非“疯狂科学家”们的专利,无需艰深的数学知识,就可以构思出解决许多实际问题的概率模型,进而利用现代概率编程系统的强大功能解题。本书既可以作为概率编程的入门读物,也可以帮助已经有一定基础的读者熟悉Figaro这一概率编程利器。


1814年,皮埃尔•西蒙•拉普拉斯写道,“在很大程度上,人生最重要的问题就是概率问题。”此后过了100多年,回答这些问题(这一格言依然正确)的唯一方法是用笔和纸分析每个问题,得到结果的公式,手工填入数字以求得公式值。计算机的出现对这一情况并没有很大的改变,只是能够为包含更多数字的更复杂公式求值,纸笔分析也变得更加雄心勃勃,往往用纸数百页。

概率问题的分析需要构思概率模型,这种模型以某种方式规划概率空间,为其指定数值化概率。过去,概率模型用自然语言文本和半正式的数学标记法的组合写下。从模型中,经过进一步数学处理得出计算答案的公式或者算法。这些阶段都十分费时费力、容易出错,而且存在特定于具体问题的难点,使概率理论的适用性受到了严重的限制。尽管拉普拉斯在多年前就已提出,但是这个生活中最重要的问题仍然没有答案。

解决上述问题的第一个重要进步是定义概率模型所用的形式语言的发展,例如贝叶斯网络和马尔科夫网络。形式语言具有定义正确表达式的精确语法,以及定义每种正确表达式含义的精确语义(即每个表达式表示哪种概率模型)。因此,用机器可理解的形式描述概率模型,开发一个算法计算任何可表达概率模型结果都成为可能。

在前面的叙述中,美中不足的是:可表达概率模型的缺乏。实际上,贝叶斯和马尔科夫网络等形式语言表达能力相当有限。从某种意义上说,它们只是布尔电路的概率模拟。为了对这一局限性的含义有所了解,我们考虑一个问题:编写大型公司所用的工资单软件。在Java等高级编程语言中,这可能涉及数万行代码。现在,想象一下将许多逻辑门电路连接起来完成相同的功能。这样的任务似乎完全无法完成。这样的电路规模、复杂度和清晰性都无法想象,因为电路缺乏捕捉问题结构的表达能力。

1997年,本书作者Avi Pfeffer(当时还是个学生)和他的导师Daphne Koller以及协作者David McAllester发表了一篇关于概率编程语言(PPL)的原创论文,提供了将概率理论与高级编程语言联系起来的关键思路。这一思路是通过引入随机元素使程序成为概率模型,并将程序的意义定义为每个可能执行路径的概率。这一思路以高效的方式结合了数学的两个最重要分支,我们接下来将要开始探索由此产生的新可能性。

本书使用Figaro语言阐述这些概念及其应用,逐步引领读者理解上述思路。书中避开了不必要的数学知识,集中于详细构思、认真解释的实例,适合于拥有典型编程背景的读者。通读本书还有一个副产品:读者能够比以往更轻松地熟练掌握贝叶斯推理和统计学的原理及技术。最重要的是,读者将学习建模技能,这是任何科学家或者工程师的最关键技能之一。Figaro和其他PPL使人们可以直接、快速、精确地表现这种技能。

本书是将概率编程从开发它的实验室中转移到真实世界的重要步骤。从某种程度上说,PPL系统的能力无疑还难以应对这种挑战,那些研究实验室也将停止工作。另一方面,本书的读者一定能找出应用Figaro的创新方法,它与各种新问题的相关性也绝非作者所能想象的。

Stuart Russell

加州大学伯克利分校计算机科学教授


概率编程是一个激动人心的新领域,正在快速地引起人们的兴趣,从学术领域进入程序员的世界中。本质上,概率编程是创建概率推理模型的新方法,这种模型用来根据观测预测或者推理未知的事物。概率推理很久以来都是机器学习的核心方法之一,在机器学习中,使用了概率模型来描述从经验中得到的知识。在概率编程之前,概率推理系统局限于包含贝叶斯网络等简单、固定结构的模型。而概率编程提供了编程语言的全部能力以表现模型,使概率推理系统摆脱了这些桎梏。这正如从电路转向高级编程语言。

我从青少年时代用BASIC语言开发一个足球模拟程序时就致力于概率编程,只是当时还没认识到。这个模拟程序使用“GOTO 1730 + RANDOM * 5”这样的指令表示随机的事件顺序。经过精心的调校,模拟程序已经很逼真,足以让我娱乐数个小时。当然,在随后的岁月中,概率编程已经逐渐成熟,不再只是包含随机目标的GOTO语句了。

1997年,我和Daphne Koller、David McAllester合作编撰了第一篇关于概率编程的论文。这篇论文引入了一种类似Lisp的概率语言,但是主要的创新是根据关于输出的证据,推理程序可能特征的一种算法。这一创新不仅提供了运行程序以获得可能执行方式的手段,还反向论证和推理了产生观测结果的原因,从而使概率语言超越了典型的概率模拟语言。

21世纪初,我开发了第一种基于函数式编程的通用概率编程系统IBAL。IBAL有很强的表达能力并包含新型推理算法,但是几年之后,我逐渐对其局限性感到不满,主要是难以与数据交互、与应用程序集成。这些局限性促使我在2009年开始开发新的概率编程系统,我将其定名为Figaro。Figaro以实用性作为首要目标,同时并没有牺牲概率编程能力。这导致了将Figaro作为Scala程序库的设计决策,该决策使得概率编程模型更容易与Java虚拟机应用集成。同时,Figaro具备了我所知的概率编程系统中最广泛的表现特征和推理算法。Figaro现在是一个开源GitHub项目,最新版本号为3.3。

概率编程可能是一种难以掌握的技术,因为它需要多种技能,其中主要的是编写概率模型和编写程序的能力。对于许多程序员来说,编写程序很自然,但是概率建模有些神秘。本书的目的是揭开概率建模的神秘面纱,告诉您如何在创建概率模型时高效编程,帮助您有效地使用概率编程系统。本书假定读者在机器学习或者概率推理上没有任何背景。函数式编程和Scala的经验有所帮助,但是要使用本书并不一定要成为Scala的奇才,Scala专业知识也可能因为阅读本书而增长。

阅读本书之后,您应该可以在没有机器学习博士学位的情况下,为许多应用程序设计概率模型,从数据中获得有意义的信息。如果您是某个领域的专家,本书能够帮助您表达脑海中或者纸面上的模型,使它们可以运算,实现对不同概率的计算和分析。如果您是一位数据科学家,本书可以帮助您开发比其他工具更丰富、更详细和更精确的模型。如果您是软件工程师或者架构师,正寻求在系统中加入不确定情形下的推理能力,本书不仅能够帮助您构建处理不确定性的模型,还能将这些模型集成到应用程序中。不管因为何种原因选择本书,我都希望您能够喜欢它,并从中得益。


1814年,皮埃尔•西蒙•拉普拉斯写道,“在很大程度上,人生最重要的问题就是概率问题。”此后过了100多年,回答这些问题(这一格言依然正确)的唯一方法是用笔和纸分析每个问题,得到结果的公式,手工填入数字以求得公式值。计算机的出现对这一情况并没有很大的改变,只是能够为包含更多数字的更复杂公式求值,纸笔分析也变得更加雄心勃勃,往往用纸数百页。

概率问题的分析需要构思概率模型,这种模型以某种方式规划概率空间,为其指定数值化概率。过去,概率模型用自然语言文本和半正式的数学标记法的组合写下。从模型中,经过进一步数学处理得出计算答案的公式或者算法。这些阶段都十分费时费力、容易出错,而且存在特定于具体问题的难点,使概率理论的适用性受到了严重的限制。尽管拉普拉斯在多年前就已提出,但是这个生活中最重要的问题仍然没有答案。

解决上述问题的第一个重要进步是定义概率模型所用的形式语言的发展,例如贝叶斯网络和马尔科夫网络。形式语言具有定义正确表达式的精确语法,以及定义每种正确表达式含义的精确语义(即每个表达式表示哪种概率模型)。因此,用机器可理解的形式描述概率模型,开发一个算法计算任何可表达概率模型结果都成为可能。

在前面的叙述中,美中不足的是:可表达概率模型的缺乏。实际上,贝叶斯和马尔科夫网络等形式语言表达能力相当有限。从某种意义上说,它们只是布尔电路的概率模拟。为了对这一局限性的含义有所了解,我们考虑一个问题:编写大型公司所用的工资单软件。在Java等高级编程语言中,这可能涉及数万行代码。现在,想象一下将许多逻辑门电路连接起来完成相同的功能。这样的任务似乎完全无法完成。这样的电路规模、复杂度和清晰性都无法想象,因为电路缺乏捕捉问题结构的表达能力。

1997年,本书作者Avi Pfeffer(当时还是个学生)和他的导师Daphne Koller以及协作者David McAllester发表了一篇关于概率编程语言(PPL)的原创论文,提供了将概率理论与高级编程语言联系起来的关键思路。这一思路是通过引入随机元素使程序成为概率模型,并将程序的意义定义为每个可能执行路径的概率。这一思路以高效的方式结合了数学的两个最重要分支,我们接下来将要开始探索由此产生的新可能性。

本书使用Figaro语言阐述这些概念及其应用,逐步引领读者理解上述思路。书中避开了不必要的数学知识,集中于详细构思、认真解释的实例,适合于拥有典型编程背景的读者。通读本书还有一个副产品:读者能够比以往更轻松地熟练掌握贝叶斯推理和统计学的原理及技术。最重要的是,读者将学习建模技能,这是任何科学家或者工程师的最关键技能之一。Figaro和其他PPL使人们可以直接、快速、精确地表现这种技能。

本书是将概率编程从开发它的实验室中转移到真实世界的重要步骤。从某种程度上说,PPL系统的能力无疑还难以应对这种挑战,那些研究实验室也将停止工作。另一方面,本书的读者一定能找出应用Figaro的创新方法,它与各种新问题的相关性也绝非作者所能想象的。

Stuart Russell

加州大学伯克利分校计算机科学教授


本书的创作经过了许多年:从关于概率编程的第一个思路,到IBAL和Figaro系统的创建,再到构思、编写并与Manning出版社一起完善。在这段时间里,许多人贡献了自己的力量,使本书得以面世。

本书的出版很大程度上归功于我在Charles River Analytics的团队的努力:Joe Gorman、Scott Harrison、Michael Howard、Lee Kellogg、Alison O’Connor、Mike Reposa、Brian Ruttenberg和Glenn Takata。还要感谢Scott Neal Reilly从一开始就支持Figaro。

在人工智能和机器学习方面,给我最大教益的是我的导师和合作者Daphne Koller。Stuart Russell为我提供了学习人工智能的第一个机会,在整个职业生涯中鼓励我,并成为最新的合作者和本书的序言撰写者。Mike Stonebraker在其Postgres项目中为我提供了第一个研究机会,在他的小组中工作时,我学到了许多关于系统构建的知识。Alon Halevy曾邀请我和他在AT&T实验室中一起度过一个夏季,我在那里第一次和David McAllester讨论关于概率编程的问题,成果就是和Daphne合作编写的Lisp概率论文。当这些思路刚刚萌芽时,我总是和我的合作者和同事Lise Getoor一起探讨。

我深深感谢Alex Ihler,他慷慨地贡献自己的专业知识,认真阅读本书以审核技术上的准确性。过去几年中,在所有与推理相关的事情上,Alex总是极好的意见反馈者。

在不同的发展阶段,还有其他许多人提供了意见,包括Ravishankar Rajagopalan、Shabeesh Balan、Chris Heneghan、Clemens Baader、Cristofer Weber、Earl Bingham、Giuseppe de Marco、Jaume Valls、Javier Guerra Giraldez、Kostas Passadis、Luca Campobasso、Lucas Gallindo、Mark Elston、Mark Miller、Nitin Gode、Odisseyas Pentakolos、Peter Rabinovitch、 Phillip Bradford、Stephen Wakely、Taposh Dutta Roy和Unnikrishnan Kumar。

感谢Manning Publications的许多出色员工对本书出版的帮助。特别要感谢编辑Dan Maharry使这本书远远超过了我自己完成的质量,还要感谢Frank Pohlmann鼓励我编写本书,并且帮助我准备写作过程。

感谢空军研究实验室(AFRL)和国防部高级研究计划署(DARPA)对本书所描述的先进机器学习概率编程(PPAML)项目中某些工作的投资。特别要感谢几位DARPA项目经理,Bob Kohout、Tony Falcone、Kathleen Fisher和Suresh Jagannathan,他们对概率编程深信不疑并致力于实现它。

最后,如果没有家人的爱和支持,本书也不可能出版。感谢我的妻子Debby Gelber和孩子们(Dina、Nomi和Ruti),你们都是了不起的人。永远感谢我的母亲Claire Pfeffer用自己的爱养育了我。谨以本书献给你们。


不管商业、科学、军事上还是日常生活中,许多决策都涉及不确定情况下的判断。当不同的因素将您引向不同方向,如何知道最应该注意的是哪个方面?概率模型可以表达关于您所处情况的所有相关信息。概率推理使用这些模型确定对决策影响最大的变量的概率。您可以使用概率推理预测最可能发生的情况:您的产品能否在目标价格上取得成功;患者对特定疗法的反应是否良好;您的候选人如果采用某种立场,能否赢得选举?您还可以使用概率推理推导出所发生情况的可能原因:如果产品失败,是不是因为价格太高?

概率推理也是机器学习的主要方法之一。您在概率模型中编码关于所在领域的初始信念,如用户对市场产品的一般反应。然后,提供训练数据(可能与特定产品的用户反应有关),更新信念以获得新模型。现在,可以使用新模型预测未来的结果,如规划中的产品是否成功,或者推导出观测结果的可能原因,如新产品失败的原因。

过去,概率推理使用专用语言表示概率模型。近年来,我们意识到可以使用常规的编程语言,这造就了概率编程。概率编程有三大好处。首先,在构建模型时,可以从编程语言的所有特征中获益,如丰富的数据结构和控制流。其次,概率模型很容易与其他应用程序集成。第三,可以从用于论证模型的通用推理算法中获益。

本书的目标是提供在日常活动中使用概率编程的知识。特别是:

如何构建概率模型并以概率程序表达。

概率推理的工作原理以及如何以各种推理算法实现。

如何使用Figaro概率编程系统构建实用的概率程序。

Figaro以Scala程序库的形式实现。和Scala一样,Figaro结合了函数式和面向对象编程风格。这对于不了解函数编程的人来说很有用。本书不使用高级函数式编程概念,所以您应该能在对此了解有限的情况下理解。同样,对Scala有所了解是有益的。尽管本书中常常会解释Scala的结构,但不是Scala的简介。同样,本书通常不使用Scala较为难懂的功能,所以略有涉猎就应该足够了。

本书的第1部分简介概率编程和Figaro。第1章首先解释概率编程的定义及其实用性,然后简单介绍Figaro。第2章是Figaro的使用教程,帮助您很快地了解概率程序的编写。第3章提供了一个完整的概率编程应用——一个垃圾邮件过滤器,包括论证给定电子邮件是常规邮件还是垃圾邮件的组件,以及从训练数据学习概率模型的组件。第3章的目标是在详细介绍建模技术之前,提供各种技术相互融合的全貌。

第2部分介绍概率程序的构建。第4章包含有关概率模型和概率程序的基本材料,这对理解它们,真正了解创建概率程序时需要做什么很重要。第5章提供了两种作为概率编程核心的建模框架——贝叶斯网络和马尔科夫网络。第6~8章描述了一组用于构建更高级程序的实用编程技术。第6章讨论使用Scala和Figaro集合组织涉及许多同类变量的程序的方法。第7章讨论面向对象编程,这种方法对于概率编程和常规程序同样有益。第8章介绍建模动态系统。动态系统是状态随时间推移而变化的系统,是这一章深入介绍的概率推理极其常见和重要的应用。

第3部分向您传授关于概率推理算法的知识。理解推理对于有效使用概率编程很重要,这样您就可以使用适合于任务的算法,对合适的方式配置,以支持有效推理的方式表达模型。第3部分在传授算法理论和使用这些算法的实践技巧之间达成了平衡。第9章是基础,介绍了捕捉概率推理中使用的主要思路的3条原则。第10章和第11章描述了两个主要的推理算法家族。第10章描述因子分解算法,包括对因子及其工作原理的介绍,以及变量消除和置信传播算法。第11章介绍抽样算法,特别关注重要性抽样和马尔科夫链蒙特卡洛算法。第10章和第11章专注于计算感兴趣的变量概率的基本查询,而第12章介绍如何使用因子分解和抽样算法计算其他查询,如多变量联合概率、变量最大可能值和观测证据的概率。最后,第13章讨论两个高级而重要的推理任务:监视随时变化的动态系统,从数据中学习概率模型的数值参数。

每章都有一组练习,涵盖了从简单计算、编程任务到开放思维练习的范围。

本书还包括两个附录。附录A是Figaro的安装指南。附录B是其他概率编程系统的概况。

本书的代码以等宽字体显示,以便和正文分开。许多代码清单中含有代码注释,强调了重要的概念。在某些情况下,清单之后有链接到解释的编号项目。

本书包含许多代码示例,其中大部分可以从本书网站www.manning.com/books/practical-probabilistic-programming 的在线代码库中找到。该网站还包含部分练习答案。


Avi Pfeffer是概率编程的先驱,从一开始就活跃于这个领域。Avi是Figaro的首席设计者和开发者。在Charles River Analytics,Avi参与了Figaro在多个问题上的应用,包括恶意软件分析、汽车健康监控、气象模型建立和工程系统评估。

在闲暇时,Avi是一位歌手、作曲家和音乐制作人。他和妻子及三个孩子在马萨诸塞州坎布里奇生活。

购买本书就可以免费访问Manning Publications运营的一个私有网络论坛,在那里可以评论本书,提出技术问题,讨论书中的练习,从作者和社区那里得到帮助。在www.manning.com/books/practical-probabilistic-programming 可以访问和订阅该论坛。这个页面提供了关于注册后如何访问论坛、论坛提供的帮助类型以及行为准则的信息。

Manning对读者的承诺是,提供读者之间和读者与作者之间有意义对话的途径。我们不能承诺作者的参与度,他们对论坛的贡献完全是自愿(无偿)的。我们建议读者向作者提出挑战性的问题,以免他们失去兴趣!

只要本书仍在销售中,作者在线论坛和过去讨论的存档都可以在Manning网站上访问。

关于封面

本书封面上的插图题为“威尼斯人”。这幅插图取自一本法国旅游图书——J. G. St. Saveur于1796年出版的《旅游百科全书》。当时,旅游消遣还是相当新颖的现象,这样的旅游指南很受欢迎,它向旅游者和空谈旅游家介绍了法国和海外其他地区的风土人情。

《旅游百科全书》中丰富的插图生动地讲述了200年前世界各个城市和地区的独特个性。当时,在两个距离仅为几十英里的地区,人们的穿着就足以独特地反映所属地区。这本旅游指南展示了当时与其他历史时代(除了快节奏的现在)的孤立感和距离感。

当时的着装规范已经变化,各个地区的多样化也逐渐消失。现在,往往难以分辨不同大陆的居民。从乐观的角度看,我们用文化和视觉上的多样性换来了更多彩的个人生活——或者更丰富、有趣的知识和技术生活。

Manning通过复活这本旅游指南中的插图,用两个世纪前丰富多彩的地域性差别赞美计算机行业的创造性和乐趣。


什么是概率编程?它有什么用处?如何使用它?这些问题是第1部分的主题。第1章介绍概率编程的基本思路。首先介绍概率推理系统的概念,说明概率编程如何将传统的概率推理系统概念和编程语言技术相结合。

在本书中,您将使用Figaro概率编程系统。第1章简要介绍Figaro,第2章提供所有Figaro主要概念的简单教程,帮助您快速开始编写概率程序。第3章介绍一个完整的概率编程应用程序,为您提供实际应用程序组合的全貌。虽然这一章接近全书的开头,因此您从一开始就一窥全局,但是在阅读本书的更多章节,已经学习到更深入的概念时,仍值得不时复习。


本章介绍如下内容:

  • 什么是概率编程?
  • 为什么应该关心概率编程?为什么我的老板应该关心概率编程?
  • 概率编程的工作原理是什么?
  • Figaro——概率编程所用的系统
  • 使用和不使用概率编程的情况下,概率应用程序编写的对比

在本章中,您将学习如何使用概率推理系统的两个主要组成部分(概率模型和推理算法)做出日常决策,还将了解现代概率编程语言是如何比Java或Python等通用语言更轻松地创建这种推理系统的。本章还将介绍Figaro,这是本书自始至终使用的基于Scala的概率编程语言。

概率编程是一种系统创建方法,它所创建的系统能够帮助我们在面对不确定性时做出决策。许多日常决策涉及在确定无法直接观测的相关因素时的判断能力。历史上,帮助在不确定性下做出决策的方法之一是使用概率推理系统。概率推理将我们对某种情况的认识和概率法则结合起来,确定无法观测的决策关键因素。直到最近,概率推理系统的范围仍然有限,难以应用到许多现实情况中。概率编程是一种新方法,它使概率推理系统更容易构建,适用范围更广。

要理解概率编程,首先要观察不确定性条件下的决策过程和涉及的主观判断。然后,您将了解概率推理是如何帮助您做出决策的。您将注意到概率推理系统所能进行的3种推理,也就能理解概率编程,以及通过编程语言的能力用概率编程构建概率推理系统的方法。

在现实世界中,我们所关心的问题很少有非此即彼的答案。例如,如果您打算启动一个新产品,想要知道它的销路如何。您可能认为它将取得成功,因为您相信它设计精良,市场调查也表明有需求,但是无法确定。您的竞争者可能推出更好的产品,或者您的产品可能有市场不能容许的致命缺陷,经济也可能突然衰退。如果要求百分之百的确定,就无法做出是否投放该产品的决策(见图1-1)。

图1-1 去年所有人都喜爱我的产品,但是明年会怎么样呢?

概率语言有助于做出此类决策。在投放某个产品时,可以使用类似产品的先期经验估算产品的成功概率。然后,用这一概率帮助决定是否继续推进并投放该产品。您可能不仅关心产品能否成功,还关心它能带来多少收入,或者失败将导致多大的损失。您可以使用不同结果的概率做出更明智的决策。

概率论思想可以帮助您做出艰难的决策和判断,但是,您该怎么做呢?一般原则在下面列出。

事实:主观判断基于知识+逻辑

您对感兴趣的问题有某些知识。例如,您对产品有深入的了解,可能进行了一些市场调查以找出客户的需求。您还可能有关于竞争对手的情报和经济预测。同时,逻辑帮助您运用知识获得问题的答案。

您需要一种规格化知识的方法,还需要运用知识得出问题答案的逻辑。概率编程提供了规格化知识和回答问题的逻辑。在我描述概率编程系统概念之前,我将描述概率推理系统的一般概念,这种系统提供了规格化知识和提供逻辑的基本手段。

概率推理是使用您的领域模型做出不确定条件下决策的一种方法。举个足球界的例子。假定统计显示9%的角球造成进球。您的任务是预测某次角球的结果。攻方的中锋身高6英尺4英寸(约1.93米),以头球能力著称。守方正选门将刚刚受伤,被第一次出场的替补门将换下。除此之外,咆哮的大风使长传难以控制。那么,如何计算进球的概率?

图1-2展示了使用概率推理系统找出答案的途径。您在一个角球模型中编码关于角球和所有相关因素的知识。然后,提供特定角球的证据,也就是中锋个子很高、守门员缺乏经验以及强风。您告诉该系统,希望知道这次角球是否进球。推理算法返回答案——有20%的概率进球。

图1-2 概率推理系统预测角球结果的方法

关键定义

一般知识——不考虑特定情况细节时,对领域相关情况的概括了解。

概率模型——用定量的概率术语编码的领域一般知识。

证据——关于特定情况的具体信息。

查询——您希望知道的情况属性。

推理——概率模型根据证据回答查询的过程。

在概率推理中,您创建一个模型,以定量的概率术语捕捉领域的所有相关一般知识。在我们的例子中,这个模型可能是对角球情况和影响结果的所有球员相关特征及条件的描述。然后,对于某个特定情况,您将该模型应用于所拥有的具体信息,得出结论。这些具体信息称为证据。在本例中,证据是中锋身材高大,守门员缺乏经验,风力很大。所得出的结论可以帮助您决策——例如,您是否应该在下一场比赛中更换不同的守门员。结论本身以概率的方式描述,比如守门员的不同技能水平的概率。

模型、您所提供的信息和查询答案之间的关系由数学上的概率法则定义。根据证据,运用模型回答查询的过程称作概率推理或者简单地称作推理。幸运的是,计算机算法已经有了很大的发展,能够为您完成这些数学题,自动进行所有必要的计算。这些算法被称作推理算法

图1-3总结了您所学到的知识。

图1-3 概率推理系统的基本组成部分

简言之,我们刚刚讨论的是概率推理系统的组成,以及与之互动的方式。但是,如何利用这样的系统?它如何帮助您决策?下一小节描述了概率推理系统所能执行的3类推理。

概率推理系统很灵活。它们可以根据任何方面的证据,回答关于情况其他特征的查询。在实践中,概率推理系统执行3类推理。

图1-4 改变查询和证据,系统现在可以推断出进球的原因

图1-5 通过将上一次角球的结果考虑在内,概率推理系统可以在下一次角球时做出更好的预测

这些类型的查询能够帮助您做出许多层次上的决策。

学习更好的模型

上述3种推理模式提供了特定情况、给定证据下的推理手段,利用概率推理系统,还可以从过去的情况中学习,改善您的一般知识。在第三种推理模式中,您了解到如何从特定的过去经验学习,更好地预测未来的情况。另一种从过去的经验中学习的方法是改善模型本身。特别是在拥有许多过去的经验可以吸取时(如许多次角球),您可能希望学习一个新模型,以表示角球通常发生情况的一般知识。如图1-6所示,这可以通过一个学习算法实现。与推理算法有些不同,学习算法的目标是产生新的模型而不是回答查询。学习算法从原始模型入手,根据经验更新之,产生新的模型。新模型可以用于回答未来的问题。可以推测,使用新模型产生的答案应该比原始模型更明智。

图1-6 可以使用学习算法,以一组经验为基础学习新的模型。然后,这个新模型可以用于未来的推断

概率推理系统与精确的预测


和任何机器学习系统一样,概率推理系统得到的数据越多,预测就越精确。预测的质量取决于两个因素:原始模型精确反映现实情况的程度和您所提供的数据量。一般来说,提供的数据越多,原始模型就越不重要,这是因为新模型是原始模型和数据所包含信息之间的一个平衡。如果您的数据很少,原始模型占据统治地位,所以它的质量必须很高才能得出准确的预测。如果您拥有许多数据,数据将占据统治地位,新模型倾向于忘掉不那么重要的原始模型。例如,如果您从整个足球赛季中学习,应该能够准确地学习到影响角球的因素。如果只有一场比赛的数据,就需要首先对精确预测比赛所需的因素有出色的想法。概率推理系统将很好地利用给定的模型和可用数据,尽可能精确地做出预测。

现在,您已经了解了概率推理的概念。那么,什么是概率编程?

每个概率推理系统都使用某种表示语言表达其概率模型。表示语言有许多种,您可能已经听说了其中一些,如贝叶斯网络(也称作置信网络)和隐含马尔科夫模型。表示语言控制系统可处理的模型以及模型的情况。语言所能表示的一组模型称作语言的表达能力。对于实际应用,您肯定希望表达能力尽可能强。

简单地说,概率编程系统是以编程语言作为表示语言的概率推理系统。我所说的编程语言是指具有编程语言所有预期特征(如变量、丰富的数据类型、控制流、函数等)的语言。正如您将要看到的,概率编程语言可以表达极其广泛的概率模型,超越传统的概率推理框架。概率编程语言有极强的表达能力。

图1-7说明了概率编程系统与概率推理系统的关系。可以将该图与图1-3比较,以凸显两种系统之间的差别。主要的变化是,模型以编程语言编写的程序表达,而不使用贝叶斯网络等数学结构。由于这种变化,证据、查询和答案都应用到程序中的变量。证据可能指定程序变量的特定值,查询询问程序变量的值,答案是不同查询变量值的概率。此外,概率编程系统通常带有一套推理算法。这些算法适用于以该语言编写的程序。

图1-7 概率编程系统是使用编程语言表示概率模型的概率推理系统

尽管存在许多类概率编程系统(参见附录B),本书的重点是函数式的图灵完备系统。函数式意味着它们基于函数式编程,但是不要被它吓住——使用函数式概率编程系统并不需要知道λ函数(lambda)等概念。这一切只意味着,函数式编程提供了这些语言表示概率模型的理论基础。同时,图灵完备是一句行话,表示编程语言可以编写任何能在数字计算机上完成的计算。如果某一运算可以在数字计算机上完成,就可以由任何图灵完备语言实现。您所熟悉的大部分编程语言,如C、Java和Python,都是图灵完备的。因为概率编程语言构建于图灵完备编程语言基础上,它们可以构建的模型类型极其灵活。

关键定义

表示语言——用于编码关于模型领域知识的语言。

表达能力——表示语言编码模型中不同类型知识的能力。

图灵完备——能够表示可在数字计算机完成的任何计算的语言。

概率编程语言——使用图灵完备编程语言表示知识的概率表示语言。

附录B论述了除本书使用的Figaro之外的一些概率编程系统。这些系统大部分都使用图灵完备语言。有一些系统(包括BUGS和Dimple)没有使用图灵完备语言,但是它们对目标应用很实用。本书主要关注图灵完备概率编程语言的能力。

将概率模型表示为程序

但是,编程语言如何成为概率建模语言?如何将概率模型表示为程序?我将在这里提出回答这一问题的一些线索,将更深入的讨论放在稍后的章节,那时您已经对概率程序有所了解。

编程语言的核心思路之一是执行。您执行一个程序以产生输出。概率程序也类似,但是它可以有许多执行路径,每个路径产生不同的输出。在程序中随机选择执行路径,每个随机的选择有许多可能的结果,程序编码每种结果的概率。因此,概率程序可以视为随机执行以产生输出的一个程序。

图1-8说明了上述概念。在图中,概率编程系统包含了一个角球程序。这个程序描述生成角球结果的随机过程,它取得一些输入;在我们的例子中,这些输入是中锋的身高、守门员的经验和风力。根据这些输入,程序随机执行以生成输出。每次随机执行产生特定的输出。因为每个随机选择都有多种可能结果,存在许多可能的执行路径,造成不同的输出。任何给定输出(如进球)可能由多个执行路径产生。

让我们来看看,这种程序如何定义概率模型。从一系列随机选择形成的任何特定执行路径都有特定的结果。每个随机选择都有发生的概率。如果将这些概率相乘,就可以得到执行路径的概率。这样,程序定义了每个执行路径的概率。想象一下,如果将该程序运行许多次,生成任何给定执行路径的次数比例等于其概率。输出的概率就是产生该输出的程序运行次数比例。在图1-8中,1/4的运行产生进球的结果,所以进球概率为1/4。

图1-8 概率程序定义按照输入随机生成输出的过程

注意:


您可能疑惑于为什么图1-8中的块标签为“随机执行”而不是其他插图中的推理算法。图1-8展示了概率程序的含义——定义一个随机执行过程,而不是使用概率编程系统的方式——使用推理算法根据证据回答查询。所以,尽管上述插图的结构类似,但是表达了不同的概念。事实上,随机执行形成了某些推理算法的基础,但是许多算法并不基于简单的随机执行。

利用概率编程决策

使用概率编程预测未来很容易理解。只要随机多次执行程序,使用当前已知的信息作为输入,并观察每个输出的出现次数。在图1-8的角球示例中,多次执行该程序,以高中锋、缺乏经验的守门员和强风作为输入。因为1/4的运行得出进球的结果,您可以认定在这些输入条件下,进球概率为25%。

但是,概率编程的魔法在于,它还可以用于1.3.1小节中描述的各类概率推理。概率编程不仅可用于预测未来,还可以推断导致特定结果的事实;您可以“展开”程序,发现结果的根源,还可以在某种情况下应用程序,从结果中学习,在未来使用学习到的信息做出更好的决策。可以使用概率编程做出所有通过概率思想得到的决策。

概率编程是如何工作的?当人们意识到,在较简单的表示语言(如贝叶斯网络)上有效的推理算法可以扩展到程序上时,概率编程就变得实用了。本书的第3部分介绍实现这一扩展的各种推理算法。幸运的是,概率编程系统自带一些内建的推理算法,这些算法可以自动地应用到您的程序中。您所需要做的是以概率程序的形式提供领域知识并指明证据,系统负责推断和学习。

在本书中,您将学习通过概率编程进行概率推理。首先,您将学习概率模型的概念以及使用它得出结论的方法。您还将学习一些从简单组件构成的模型中得出那些结论所需进行的操作。您将学习各种建模技术,以及使用概率编程实现它们的方法,还将了解概率推理算法的工作原理,以便有效地设计和使用自己的模型。在阅读完本书之后,您将能够自信地使用概率编程得出有益的结论,帮助您在面对不确定性时做出决策。

概率推理是机器学习的基础技术之一。Google、Amazon和Microsoft等公司使用它理解可用数据。概率推理已经用于各种各样的应用程序,如预测股价、推荐电影、诊断计算机和检测网络入侵。许多应用都使用了本书中将要学习的技术。

前一小节中,有两个引人注目的要点。

将上面两个要点结合起来,可以得到如下表示。

事实:概率推理+图灵完备=概率编程

概率编程的动机是将两个本身就很强大的概念结合起来,结果是使用计算机辅助不确定性下决策的更简单、更灵活方法。

大部分现有概率表示语言在所能表示的系统丰富性上都很有限。有些相对简单的语言(如贝叶斯网络)假定固定的变量集,其灵活性不足,不能建立变量本身可能变化的领域模型。近年来,已经有一些具有更高灵活性的先进语言开发出来。其中一些语言(如BUGS)还提供了编程语言的特征,包括循环和数组,但是没有达到图灵完备。BUGS等语言的成功说明了更丰富、结构更严整的表示方式的必要性。但是,向成熟的图灵完备语言转移,为概率推理开拓了一个新领域。现在,可以建立具有许多交互实体及事件的长期运行过程的模型。

我们再次考虑足球的例子,但是这次想象一下,您的工作是体育分析,希望为一支球队做出人员配备决策的建议。您可以使用积累的统计数字做出决策,但是统计数字不能捕捉积累它们时所处的背景。您可以建立赛季的细致模型,实现粒度更细、情境感知的分析。这要求建立许多相关事件以及相互作用的球员和球队的模型。如果没有完整的编程语言所提供的数据结构和控制流,构建这种模型是难以想象的。

现在,让我们再次思考产品投放的例子,从综合的角度观察业务决策过程。产品投放不是孤立事件,而是经过市场分析、研究和开发的过程,各个过程的结果都有不确定性。产品投放的结果取决于所有阶段,以及市场中其他产品的分析。全面的分析还需要关注竞争对手对您的产品的反应,以及他们可能提出的新产品。这一问题很困难,因为您必须对竞争产品做出推测。甚至有一些竞争对手尚不为人所知。在这个例子中,产品是复杂过程产生的数据结构。同样,用完整的编程语言创建模型很有益处。

不过,概率编程的好处之一是,可以使用更简单的概率推理框架。概率编程系统可以表示广泛的现有框架,以及这些框架所不能表示的系统。本书将传授许多使用概率编程的此类框架。所以,在概率编程的学习中,您还能够精通许多当今常用的概率推理框架。

图灵完备的概率建模语言已经存在。它们常常被称作模拟语言。我们知道,使用编程语言模拟足球赛季等复杂过程是可能的。在这种情境下,我使用模拟语言这一术语描述能够表示复杂过程随机执行的语言。正如概率程序,这些模拟随机执行,以产生不同输出。模拟和概率推理一样应用广泛,涵盖了从军事计划到组件设计以及公共卫生及体育比赛预测等范围。确实,精密模拟的广泛使用说明了对丰富概率建模语言的需求。

但是,概率程序远不仅是模拟。使用模拟,您只能完成概率程序的一项功能:预测未来。无法用它推断观测结果的根源。而且,尽管可以不断地用已知的当前信息更新模拟,但是很难包含必须推断的未知信息。因此,从过去经验中学习以改善未来预测和分析的能力很有限。不能将模拟用于机器学习。

概率程序就像不仅可以运行,而且可以分析的模拟一样。开发概率编程的关键要点是,推理算法既可用于较简单的建模框架,也可用于模拟。因此,您有能力编写一个模拟并在其基础上执行推理,以创建概率模型。

最后一点,概率推理系统已经出现了一段时间,Hugin、Netica和BayesiaLab等软件提供了贝叶斯网络系统。但是概率编程更有表现力的表示语言很新颖,我们刚刚开始发现其强大的应用。老实说,我不能告诉您概率编程已经用于大量现有应用,但是有一些重要的应用。Microsoft已经能够使用概率编程,确定在线游戏玩家的真正技能水平。加州大学伯克利分校的Stuart Russell编写了一个程序,通过识别表明核爆炸的地震活动,帮助联合国《全面禁止核试验条约》的实施。麻省理工学院(MIT)的Josh Tenenbaum和斯坦福大学的Noah Goodman已经创建了建立人类识别模型的概率程序,并在试验中取得了很大的成功。在Charles River Analytics,我们已经使用概率编程推断恶意软件实例的组件并确定它们的演变。但是,我相信这些应用仅仅是个开始。将会有越来越多的人用概率编程系统做出所在领域的决策。阅读本书,您也有机会成为这一新技术的尝鲜者。

在本书中,您将使用一种称为Figaro的概率编程系统。(我用莫扎特的歌剧《费加罗的婚礼》中的角色为其命名。我喜爱莫扎特,并在该剧于波士顿的一次演出中饰演巴尔托洛医生。)本书的主要目标是教授概率编程的原则,在本书中学到的技术应该可以在其他概率编程系统上沿用。附录B简单描述了现有的一些系统。但是,本书还有第二个目标——帮助您获得创建使用概率程序的亲身体验,并提供可以立即使用的工具。因此,许多例子都用Figaro代码实现。

Figaro是从2009年开始开发的一个开源软件,在GitHub上维护。它以Scala库的形式实现。图1-9说明Figaro如何使用Scala实现概率编程系统。该图详细说明了图1-7,后者描述了概率编程系统的主要组成部分。让我们从概率模型开始,在Figaro中,该模型由任意数量的数据结构(称作“元素”)组成。每个元素代表在您的情境中可取任意数量值的一个变量。这些数据结构用Scala实现,您可以用这些数据结构编写Scala程序创建模型。可以通过关于元素值的信息提供证据,也可以指定希望在查询中了解的元素。至于推理算法,您可以选择一个Figaro内建推理算法并应用到模型上,根据证据回答您的查询。推理算法以Scala实现,其调用就是一个Scala函数调用。推理结果是查询元素不同值的概率。

图1-9 Figaro使用Scala提供概率编程系统的方法

Figaro内嵌于Scala提供了一些重大优势。其中一些来自内嵌于通用宿主语言相对于独立概率语言的优势。其他优势则是因为Scala的良好特性。下面是在通用宿主语言中内嵌概率编程语言的好处。

下面是选择Scala作为内嵌概率编程系统的宿主语言的一些理由。

最后,Figaro还有嵌入Scala之外的一些优势,包括:

由于多种原因,Figaro是学习概率编程的出色语言。

本书强调使用的技术和实用的示例。只要有可能,我都会解释建模的一般原则,并描述在Figaro中的实现方法。不管您最终使用哪一种概率编程系统,这对您都将大有裨益。并不是所有系统都能轻松地实现本书中的所有技术。例如,现有的面向对象概率编程系统很少。但是有了好的基础,您就可以找出用所选语言表达需求的方法。

使用Scala


因为Figaro是一个Scala库,需要Scala的知识才能使用Figaro。本书是关于概率编程的,所以在本书中不教授Scala的知识。Scala的出色学习资源很多,比如Twitter的Scala School(http://twitter.github.io/scala_school)。但是为了防止您对Scala不自信,我在本书中对代码中使用的Scala功能加以说明。即使您还不了解Scala,也能够跟上本书的进度。

从概率编程和Figaro中获益并不要求您是一位Scala奇才,在本书中也避免使用一些较为高级和晦涩的特性。但是,增强Scala技能有助于成为更好的Figaro程序员。您甚至会发现,阅读本书也可以提高Scala的技能。

为了说明概率编程和Figaro的好处,我将展示以两种方式编写的简单概率应用。首先,我说明用Java(您可能对它很熟悉)编写这种应用的方法。然后,我将展示用Figaro编写的Scala应用。尽管Scala相对Java有一定的优势,但是这不是我要指出的主要差别。关键的思路是,Figaro提供了表示概率模型和用这些模型进行推理的能力,如果没有概率编程,这些能力就不存在

我们的小应用将作为Figaro的“Hello,World”示例。想象一下,有个人早上起床,查看天气是否晴朗,并根据天气发出问候。每天发出连续两天的问候。而且,第二天的天气取决于第一天:如果第一天是晴天,第二天就更可能是晴天。这些陈述可以由表1-1中的数字量化。

表1-1 量化“你好,世界”示例的概率

今天的天气
晴天 0.2
不是晴天 0.8
今天的问候语
如果今天是晴天 “Hello, world!” 0.6
“Howdy, universe!” 0.4
如果今天不是晴天 “Hello, world!” 0.2
“ Oh no, not again” 0.8
明天的天气
如果今天是晴天 晴天 0.8
不是晴天 0.2
如果今天不是晴天 晴天 0.05
不是晴天 0.95
明天的问候语
如果明天是晴天 “Hello, world!” 0.6
“Howdy, universe!” 0.4
如果明天不是晴天 “Hello, world!” 0.2
“Oh no, not again” 0.8

下面几章将明确解释这些数字的含义。现在,我们直观地认为今天是晴天的概率为0.2,也就是说,今天有20%的可能放晴。同样,如果明天是晴天,明天的问候语为“Hello, world!”的概率为0.6,也就是说问候语为“Hello, world!”有60%的可能性,“Howdy, universe!”的可能性为40%。

我们为自己设定了用这个模型执行3种推理任务的目标。在1.1.3小节中您已经知道,用概率模型能够进行3类推理:预测未来,推断导致观测结果的过去事件,从过去事件中学习以更好地预测未来。您将用我们的简单模型完成这三种任务。具体任务如下。

1.预测今天的问候语。

2.如果观测发现今天的问候语是“Hello, world!”,推断今天是不是晴天。

3.从今天对问候语是“Hello, world!”这一观测值的学习,预测明天的问候语。

下面是用Java完成这些任务的方法。

程序清单1-1 用Java实现的Hello World 程序

class HelloWorldJava {              ◁——●  //定义问候语
  static String greeting1 = "Hello, world!";
  static String greeting2 = "Howdy, universe!";
  static String greeting3 = "Oh no, not again";

  static Double pSunnyToday = 0.2;  ◁——●  //指定模型的数值参数
  static Double pNotSunnyToday = 0.8;
  static Double pSunnyTomorrowIfSunnyToday = 0.8;
  static Double pNotSunnyTomorrowIfSunnyToday = 0.2;
  static Double pSunnyTomorrowIfNotSunnyToday = 0.05;
  static Double pNotSunnyTomorrowIfNotSunnyToday = 0.95;
  static Double pGreeting1TodayIfSunnyToday = 0.6;
  static Double pGreeting2TodayIfSunnyToday = 0.4;
  static Double pGreeting1TodayIfNotSunnyToday = 0.2;
  static Double pGreeting3TodayIfNotSunnyToday = 0.8;
  static Double pGreeting1TomorrowIfSunnyTomorrow = 0.6;
  static Double pGreeting2TomorrowIfSunnyTomorrow = 0.4;
  static Double pGreeting1TomorrowIfNotSunnyTomorrow = 0.2;
  static Double pGreeting3TomorrowIfNotSunnyTomorrow = 0.8;

  static void predict() {          ◁——●  //用概率推理规则预测今天的问候语
     Double pGreeting1Today =
            pSunnyToday * pGreeting1TodayIfSunnyToday +
            pNotSunnyToday * pGreeting1TodayIfNotSunnyToday;
     System.out.println("Today's greeting is " + greeting1 +
        "with probability " + pGreeting1Today + ".");
  }

    static void infer() {       ◁——●  //按照今天的问候语是“Hello, world!”这一观测值,运用概率推理原则推断今天的天气
       Double pSunnyTodayAndGreeting1Today =
              pSunnyToday * pGreeting1TodayIfSunnyToday;
       Double pNotSunnyTodayAndGreeting1Today =
              pNotSunnyToday * pGreeting1TodayIfNotSunnyToday;
       Double pSunnyTodayGivenGreeting1Today =
              pSunnyTodayAndGreeting1Today /
               (pSunnyTodayAndGreeting1Today +
              pNotSunnyTodayAndGreeting1Today);
       System.out.println("If today's greeting is " + greeting1 +
           ", today's weather is sunny with probability " +
           pSunnyTodayGivenGreeting1Today + ".");
    }

    static void learnAndPredict() {       ◁——●  //从今天问候语是“Hello, world!”的观测中学习,运用概率推理原则预测明天的问候语
       Double pSunnyTodayAndGreeting1Today =
              pSunnyToday * pGreeting1TodayIfSunnyToday;
       Double pNotSunnyTodayAndGreeting1Today =
              pNotSunnyToday * pGreeting1TodayIfNotSunnyToday;
       Double pSunnyTodayGivenGreeting1Today =
              pSunnyTodayAndGreeting1Today /
               (pSunnyTodayAndGreeting1Today +
                    pNotSunnyTodayAndGreeting1Today);
       Double pNotSunnyTodayGivenGreeting1Today =
              1 - pSunnyTodayGivenGreeting1Today;
       Double pSunnyTomorrowGivenGreeting1Today =
              pSunnyTodayGivenGreeting1Today *
                   pSunnyTomorrowIfSunnyToday +
              pNotSunnyTodayGivenGreeting1Today *
                   pSunnyTomorrowIfNotSunnyToday;
       Double pNotSunnyTomorrowGivenGreeting1Today =
              1 - pSunnyTomorrowGivenGreeting1Today;
       Double pGreeting1TomorrowGivenGreeting1Today =
              pSunnyTomorrowGivenGreeting1Today *
                   pGreeting1TomorrowIfSunnyTomorrow +
              pNotSunnyTomorrowGivenGreeting1Today *
                   pGreeting1TomorrowIfNotSunnyTomorrow;
       System.out.println("If today's greeting is " + greeting1 +
           ", tomorrow's greeting will be " + greeting1 +
           " with probability " +
           pGreeting1TomorrowGivenGreeting1Today);
    } 

    public static void main(String[] args) {         ◁——●  //执行所有任务的主方法
      predict();
      infer();
      learnAndPredict();
    }
  }

在此,我不对使用推理规则进行计算的方法做出描述。上述代码使用了3条推理规则:链式法则、全概率公式和贝叶斯法则。这些规则将在第9章中详细解释。现在,我们指出这段代码的两个主要问题。

模型定义包含在一个变量名与双精度值的列表中。当我在本节的开始描述模型,在表1-1中展示数值时,模型有许多结构,尽管不算很直观,但也算相对容易理解。变量定义的列表毫无结构。变量的含义埋藏在变量名之中,这一定不是好主意。因此,以这种方式记下模型很难,该过程也很容易出错。以后阅读理解和维护这些代码也很困难。如果需要修改模型(例如,问候语还取决于您睡得好不好),就可能需要重写模型的很大一部分。

上述代码的第二个主要问题是使用概率推理规则回答查询。您必须有关于推理规则的详细知识才能编写这段代码。即使有了这种知识,正确编写代码也很难。测试答案是否正确也很困难,而这只是一个极其简单的例子。对于复杂的应用,以此方式创建推理代码可能不现实。

下面看看Scala/Figaro代码。

程序清单1-2 用Figaro实现的Hello World 程序

import com.cra.figaro.language.{Flip, Select}
import com.cra.figaro.library.compound.If
import com.cra.figaro.algorithm.factored.VariableElimination         ◁——●  //导入Figaro结构

object HelloWorld {
  val sunnyToday = Flip(0.2)
  val greetingToday = If(sunnyToday,
       Select(0.6 -> "Hello, world!", 0.4 -> "Howdy, universe!"),    ◁——●  //定义模型
       Select(0.2 -> "Hello, world!", 0.8 -> "Oh no, not again"))
  val sunnyTomorrow = If(sunnyToday, Flip(0.8), Flip(0.05))
  val greetingTomorrow = If(sunnyTomorrow,
       Select(0.6 -> "Hello, world!", 0.4 -> "Howdy, universe!"),
       Select(0.2 -> "Hello, world!", 0.8 -> "Oh no, not again"))

  def predict() {                                ◁——●  //用推理算法预测今天的问候语
    val result = VariableElimination.probability(greetingToday,
                   "Hello, world!")
    println("Today’s greeting is \"Hello, world!\" " +
            "with probability " + result + ".")  ◁——●  //根据今天的问候语是“Hello,world!”这一事实,使用推理算法推理今天的天气
  }
    def infer() {
      greetingToday.observe("Hello, world!")
      val result = VariableElimination.probability(sunnyToday, true)
      println("If today's greeting is \"Hello, world!\", today’s " +
              "weather is sunny with probability " + result + ".")
    }

    def learnAndPredict() {         ◁——●  //从对今天的问候语是“Hello,world!”这一观察中学习,用推理算法预测明天的问候语
      greetingToday.observe("Hello, world!")
      val result = VariableElimination.probability(greetingTomorrow,
                     "Hello, world!")
      println("If today's greeting is \"Hello, world!\", " +
              "tomorrow's greeting will be \"Hello, world!\" " +
              "with probability " + result + ".")
    }

    def main(args: Array[String]) {  ◁——●  //执行所有任务的主方法
      predict()
      infer()
      learnAndPredict()
    }
  }

我将等到下一章才详细解释这段代码。现在,我希望指出,它解决了Java代码的两个问题。首先,模型定义准确描述了对应于表1-1的模型结构。您定义了4个变量:sunnyToday、greetingToday、sunnyTomorrow和greetingTomorrow,它们都对应于表1-1。例如,greetingToday的定义如下:

val greetingToday = If(sunnyToday,
       Select(0.6 -> "Hello, world!", 0.4 -> "Howdy, universe!"),
       Select(0.2 -> "Hello, world!", 0.8 -> "Oh no, not again"))

这段代码说明,如果今天是晴天,则问候语为“Hello,world!”的概率为0.6,“Howdy, universe!”的概率是0.4。如果今天不是晴天,问候语为“Hello,world!”的概率为0.2,“Oh no, not again”的概率为0.8。这正是表1-1对今天问候语的规定。因为代码明确地描述了模型,构造、阅读和维护就容易得多了。如果需要更改模型(例如,添加sleepQuality变量),可以用模块化的方式完成。

现在,我们来看看执行推理任务的代码,它没有包含任何计算,而是实例化一个算法(在本例中是一个变量消除算法,Figaro中可用的几个算法之一),并查询该算法以获得所需的概率。现在,按照第3部分中的说明,这个算法基于和Java程序相同的概率推理规则。组织和应用推理规则的艰苦工作由该算法负责。即使对于大而复杂的模型,也可以运行该算法,完成所有推理。

部分练习的解答可在www.manning.com/books/practical-probabilistic-programming 上找到。

1.想象一下,您打算用一个概率推理系统推理扑克牌型的结果。

  a)您可以在模型中编码哪种一般知识?

  b)描述如何使用系统预测未来。什么是证据?什么是查询?

  c)描述如何使用系统推断当前观测结果的根源。什么是证据?什么是查询?

  d)描述推断出的过去根源如何帮助您预测未来。

2.在Hello World示例中,根据如下表格改变今天天气是否晴朗的概率。程序输出有何变化?为什么您认为将出现这样的变化?

今天的天气
晴朗 0.9
不晴朗 0.1

3.修改Hello World示例,添加一个新问候:“Hi,galaxy!”。提供这个问候语在天气晴朗时的概率,降低其他问候语的概率使总概率保持为1。还要修改程序,使所有查询打印“Hi,galaxy!”的概率而不是“Hello, world!”的概率。用Java和Figaro版本的Hello World程序进行这一修改。比较两种语言的过程。


本章介绍如下内容:

  • 模型创建、证据陈述、推理运行和查询的回答
  • 理解模型基本构件
  • 从这些构件构造复杂模型

现在,您已经了解了概率编程的含义,可以详细了解Figaro,以便编写自己的简单程序,用它们回答查询。本章的目标是尽快介绍Figaro的最重要概念。后面的章节详细解释模型的含义以及对它们的理解。让我们开始吧。

首先,我们对Figaro做一个概述。在第1章中已经介绍过,Figaro是一种概率推理系统。在查看其组件之前,我们先回顾概率推理系统的一般组件,使您可以和Figaro比较。图2-1重现了第1章中介绍的概率推理系统要点。提醒一下,关于情况的一般知识在概率模型中编码,而证据提供关于特定情况的具体信息。推理算法使用模型和证据回答关于情况的查询。

图2-1 概率推理要点回顾

现在我们来看看Figaro。图2-2展示了Figaro的关键概念。可以看到,该图和图2-1有着相同的组件。Figaro模型用来表达一般知识。您以证据的形式提供关于某种情况的具体知识。查询告诉系统您所感兴趣的发现。Figaro推理算法取得证据并用模型提供查询的答案

图2-2 Figaro的关键概念及其组合

现在,我们来观察每一个组件。Figaro的大部分接口提供了指定Figaro模型的方法。Figaro模型由一组称为元素的数据结构组成。每个元素代表在您的情况中可以取一组值之一的变量。元素编码定义不同值概率的信息。您将在2.2.1小节中Hello World示例的上下文中看到元素的基本定义。

元素主要有两类:原子元素和复合元素。您可以将Figaro视为构建模型的构造工具箱。原子元素是基本构件,表示不依赖于其他元素的基本概率变量。2.3小节讨论原子元素并提供各种示例。复合元素是连接器,它们依赖于一个或者多个元素以构成更复杂的元素。您将在2.4小节中学习关于复合元素的知识。Figaro提供种种不同的复合元素,其中两种特别重要——Apply应用)和Chain),您将在2.5小节中学习如何使用它们。

接下来的是证据。Figaro提供了说明证据的丰富机制。大部分时候,您将使用证据的最简单形式——观测值。观测值指定已知有某个特定值的元素。您将在2.2.3小节学习如何指定观测值。有时候,您需要更通用的证据说明方法。为此Figaro提供了条件约束。条件和约束及其用法在2.6小节中描述。

Figaro的查询通过指明目标元素和您想知道的有关情况指明。您使用某种算法,按照证据找出有关目标元素的信息。通常,您必须实例化某种算法,运行并在之后清理。我已经提供了使用默认设置执行所有步骤的简单方法。在运行算法之后,您可以获得查询的答案。这些算法最常采取目标元素的各种取值概率。有时候,它们没有告诉您概率,而是得出每个目标元素的最可能取值。对于每个目标元素,答案告诉您最高概率的取值。您将在2.2.2小节中看到如何指定查询、运行算法和获得答案。

您已经概要了解了Figaro概念,接下来看看它们是如何融合在一起的。您将回顾第1章的Hello World示例,特别注意图2-2中的所有概念是如何出现在这个例子中的。您将关注如何从原子和复合元素中构建模型,观测证据,提出查询,运行推理算法,得到答案。

本章的代码可以两种方式运行。一种是使用Scala控制台,逐行输入语句并获得即时响应。为此,进入本书项目根目录PracticalProbProg/examples并输入sbt console,将会看到Scala提示符。然后输入每行代码,查看响应。

第二种方式是通常的方法:编写一个包含main方法的程序,该方法包含想要执行的代码。在本章中,我不提供将代码转换为可运行程序的模板,只提供与Figaro相关的代码。我将确保指出您需要导入的内容及将其导入的位置。

首先,您将构建最简单的Figaro模型。这个模型包含一个原子元素。构建模型之前,必须导入必要的Figaro结构:

import com.cra.figaro.language._

上述语句导入com.cra.figaro.language包中的所有类,该包包含最基本的Figaro结构。这些类中有一个称为Flip。可以用Flip构建一个简单模型:

val sunnyToday = Flip(0.2)

图2-3解释了这一行代码。搞清楚哪一部分是Scala,哪一部分是Figaro,是很重要的。在这行代码中,创建了一个名为sunnyToday的变量,并赋值Flip(0.2)。Scala值Flip(0.2)是一个Figaro元素,表示true值概率为0.2、false值概率为0.8的一个随机过程。元素是表示随机产生一个值的过程的数据结构。随机过程可能产生任意数量的结果。每个可能结果被称为过程的一个。因此,Flip(0.2)是可能取值为布尔值true及false的元素。总结起来就是,您有了一个包含Scala值的Scala变量。该Scala值是Figaro元素,它包含表示过程不同结果的任意个可能取值。

图2-3 Scala变量与值以及Figaro元素与可能值之间的关系

在Scala中,类型可以由另外一种描述其内容的类型参数化。您可能从Java泛型中已经熟悉了这个概念,例如,在Java中可以得到一个整数或者字符串的列表。所有Figaro元素都是Element类的实例。Element类由元素可能取值的类型参数化。这种类型称作元素的值类型。因为Flip(0.2)可以取布尔值,Flip(0.2)的值类型为Boolean。这一事实的标记方法是:Flip(0.2)是Element[Boolean]的一个实例。

关键定义

元素——代表一个随机过程的Figaro数据结构。

——随机过程的一个可能结果。

值类型——代表元素可能取值的Scala类型。

关于这个简单模型有许多值得说明的地方。幸运的是,您已经学到的知识适用于所有Figaro模型。Figaro模型通过取得和组合简单的Figaro元素(构件)创建更复杂的元素和相关元素集合而创建。您刚刚学到的元素、值和值类型的定义是Figaro中最为重要的定义。

在继续构建更复杂的模型之前,我们先来看看如何用这个简单模型进行推理。

您已经构建了一个简单模型。我们运行推理,查询sunnyToday为true的概率。首先,需要导入将要使用的推理算法:

import com.cra.figaro.algorithm.factored.VariableElimination

上述语句导入所谓的“变量消除”(variable elimination)算法,这是一种精确的推理算法,也就是说,它可以准确地计算您的模型和证据隐含的概率。概率推理很复杂,所以精确的算法有时候需要花费很长时间,或者耗尽内存。Figaro提供近似算法,这种算法通常提供与准确答案大致相同的答案。本章使用简单的模型,所以变量消除算法在大部分情况下可行。

现在,Figaro提供一个简单命令以指定查询、运行算法并获得答案。可以编写如下的代码:

println(VariableElimination.probability(sunnyToday, true))

上述命令打印输出0.2。您的模型只包含元素Flip(0.2),结果为true的概率为0.2。变量消除算法正确计算出sunnyToday为true的概率是0.2。

详细说来,您刚刚看到的命令完成好几件工作:首先创建变量消除算法的一个实例,告诉实例查询目标是sunnyToday。然后运行算法并返回sunnyToday值为true的概率。这条命令还负责执行完毕的清理,释放算法所用的任何资源。

现在,我们开始构建一个更有趣的模型。您需要一个Figaro结构——If,因此要导入它。还需要一个名为Select的结构,但是这已经随着com.cra.figaro.language导入:

import com.cra.figaro.library.compound.If

我们使用If和Select构建更复杂的元素:

val greetingToday = If(sunnyToday,
     Select(0.6 -> "Hello, world!", 0.4 -> "Howdy, universe!"),
     Select(0.2 -> "Hello, world!", 0.8 -> "Oh no, not again"))

对此的思维方式是元素代表一个随机过程。在本例中,名为greetingToday的元素代表着这样的过程:首先检查sunnyToday的值,如果为true,选择“Hello,world!”的概率为0.6,“Howdy, universe!”的概率为0.4。如果sunnyToday的值为false,选择“Hello, world!”的概率为0.2,“Oh no, not again”的概率为0.8。greetingToday是一个复合元素,因为它由3个元素构建而成。由于greetingToday的可能取值为字符串,所以它是Element[String]。

现在,假定您已经看到今天的问候语是“Hello, world!”,您可以使用一个观测值说明这一证据:

greetingToday.observe("Hello, world!")

接下来,您可以根据问候语是“Hello, world!”算出今天是晴天的概率:

println(VariableElimination.probability(sunnyToday, true))

这条命令打印输出0.4285714285714285。注意,结果明显高于前一个答案(0.2)。这是因为问候语是“Hello, world!”时,今天是晴天的可能性高于其他情况,所以证据支持今天是晴天。这一推理是贝叶斯法则的简单实例,第9章将介绍这一法则。

您打算扩展该模型,用不同证据运行更多查询,所以应该移除变量greetingToday的观测值。用如下命令可以完成:

greetingToday.unobserve()

现在,如果发出如下查询:

println(VariableElimination.probability(sunnyToday, true))

您将得到和指定证据之前一样的答案0.2。

在本节的最后,我们进一步细化模型:

val sunnyTomorrow = If(sunnyToday, Flip(0.8), Flip(0.05))
val greetingTomorrow = If(sunnyTomorrow,
     Select(0.6 -> "Hello, world!", 0.4 -> "Howdy, universe!"),
     Select(0.2 -> "Hello, world!", 0.8 -> "Oh no, not again"))

您可以计算在有无关于今天问候语的证据情况下,明天的问候语为“Hello, world!”的概率:

println(VariableElimination.probability(greetingTomorrow, "Hello, world!"))
// prints 0.27999999999999997

greetingToday.observe("Hello, world!")
println(VariableElimination.probability(greetingTomorrow, "Hello, world!"))
// prints 0.3485714285714286

可以看到,在观察到今天的问候语是“Hello, world!”时,明天的问候语是“Hello, world!”的概率增大,为什么?因为今天的问候语是“Hello, world!”,今天就更有可能是晴天,明天是晴天的可能性也就更大,最终使明天的问候语更可能是“Hello, world!”。正如在第1章中所看到的,这是推断过去更好预测未来的一个例子,Figaro负责所有的计算。

现在,您已经看到了创建模型、指定证据和查询、运行推理得到答案的所有步骤,接下来我们更仔细地观察Hello World模型,理解如何从构件(原子元素)和连接器(复合元素)构建它。

图2-4是模型的图形描述。该图首先重现了模型定义,每个Scala变量在一个单独的方框中。在下半部分中,每个节点表示模型中的对应元素,同样在单独的方框中显示。有些元素本身就是Scala变量值。例如,Scala变量sunnyToday的值是Flip(0.2)元素。如果元素是Scala变量值,图上显示变量名称和元素。该模型还包含了一些不是特定Scala变量值,但仍出现在模型中的元素。例如,因为sunnyTomorrow的定义是If(sunnyToday, Flip(0.8), Flip(0.05)),Flip(0.8) 和Flip(0.05)也是模型的一部分,所以它们显示为图中的节点。

图2-4 以图的形式显示的Hello World模型结构。图中的每个节点是一个元素。图中的边显示何时某个元素被另一个元素使用

该图包含了元素之间的边。例如,从Flip(0.8)到取sunnyTomorrow值的If元素之间有一条边,表明If元素使用Flip(0.8)元素。一般来说,如果第二个元素的定义中使用了第一个元素,则两者之间存在一条边。因为只有复合元素是由其他元素构建而成的,所以只有复合元素可能成为边的终点。

需要注意的一点是,Select(0.6 -> "Hello, world!", 0.4 -> "Howdy, universe!")在图中出现了两次,Select(0.2 -> "Hello, world!", 0.8 -> "Oh no, not again")也是如此。这是因为代码中定义出现了两次,一次用于greetingToday,另一次用于greetingTomorrow。尽管定义相同,但是这是两个不同的元素。它们在Figaro模型定义的随机过程的同一次执行中可能取不同的值。例如,该元素的第一个实例可能取值“Hello, world!”,而第二个实例可能取值“Howdy, universe!”。这是有意义的,因为第一个元素实例用于定义greetingToday,第二个则用于定义greetingTomorrow。今天和明天的问候语很可能不一样。

这和常规编程类似,想象一下您有一个Greeting类和如下代码:

class Greeting {
  var string = "Hello, world!"
}
val greetingToday = new Greeting
val greetingTomorrow = new Greeting
greetingTomorrow.string = "Howdy, universe!"

尽管定义完全相同,greetingToday 和 greetingTomorrow是Greeting类的两个不同实例。因此,greetingTomorrow.string和greetingToday.string可能取不同值,后者仍然等于“Hello, world!”。同样,Figaro构造函数(如Select)创建对应元素类的新实例。所以greetingToday和greetingTomorrow是Select元素的两个不同实例,因此在一次运行中可能取不同的值。

另一方面,注意Scala变量sunnyToday也出现了两次,一次在greetingToday的定义中,另一次在sunnyTomorrow中。但是本身是sunnyToday值的元素在图中仅出现一次。为什么?因为sunnyToday是一个Scala变量,而不是Figaro元素定义。当Scala变量在一段代码中出现超过一次时,它是相同的变量,所以使用相同的值。在我们的模型中,这是有意义的;它表示同一天的天气,用于greetingToday和sunnyTomorrow的定义中,所以在模型的任何随机执行中都取相同的值。

常规代码中也会发生相同的事情。如果编写如下代码:

val greetingToday = new Greeting
val anotherGreetingToday = greetingToday
anotherGreetingToday.string = "Howdy, universe!"

anotherGreetingToday和greetingToday是相同的Scala变量,所以运行上述代码之后,greetingToday的值也是“Howdy, universe!”,同样,如果同一个Scala变量代表程序中出现多次的一个元素,它在每次运行中也取相同的值。

理解这一点对于了解Figaro模型的构建方式是必不可少的,所以我建议反复阅读本节以确保理解。此时,您应该已经概要了解所有的Figaro主要概念以及它们的组合方式。在下面几节中,您将更详细地研究其中一些概念,下一节首先介绍原子元素。

现在正是积累Figaro元素知识的时机。我将首先介绍模型的基本构件——原子元素。原子这一名称意味着,它们不依赖于任何其他元素,完全是独立的。在此我不提供完整的原子元素列表,只介绍最常见的例子。

原子元素根据值的类型分为离散元素和连续元素。离散原子元素取Boolean和Integer等类型的值,而连续原子元素通常使用Double类型的值。从技术上说,离散意味着值之间有很清晰的分隔。例如,整数1和2有很清晰的分隔,其中没有任何整数。而连续意味着值处于一个没有分隔的连续域中,如实数。在任何两个实数之间都有更多的实数。离散和连续元素之间的差异造成了概率定义的差异,第4章中将做介绍。

警告:


有些人认为离散就意味着有限。这是错误的。例如,整数有无穷多个,但是它们是清晰分隔的,所以是离散值。

关键定义

原子元素——不依赖于任何其他元素的独立元素。

复合元素——由其他元素构成的元素。

离散元素——值类型清晰分隔的元素。

连续元素——值类型没有分隔的元素。

让我们来看一些离散原子元素的例子:Flip、Select和Binomial。

Flip

您已经看到了离散原子元素Flip。Flip包含在com.cra.figaro.language包中,该包中有许多最常用的元素。我建议始终在程序开始处导入该包的所有内容。一般来说,Flip取一个参数p,表示元素值为真的概率。p应该是0和1(包含)之间的数值。该元素值为假的概率是1-p。例如:

import com.cra.figaro.language._
val sunnyToday = Flip(0.2)
println(VariableElimination.probability(sunnyToday, true))
// prints 0.2

println(VariableElimination.probability(sunnyToday, false))
// prints 0.8

Flip(0.2)的正式类型是AtomicFlip,是Element[Boolean]的子类。这使其区别于后面将会看到的CompoundFlip。

Select

您已经在Hello World程序中看到了Select元素。下面是一个例子:Select(0.6 -> "Hello, world!", 0.3 -> "Howdy, universe!", 0.1 -> "Oh no, not again")。图2-5展示了这个元素的构成。在圆括号中是一些子句。每个子句由一个概率、一个右箭头和一个可能结果组成。子句的数量是可变的,您可以使用任意个子句。图中有3个子句。因为所有结果的类型都是String,所以这个元素是Element[String]。同样,其正式类型是AtomicSelect[String]——Element[String]的子类。

图2-5 Select元素结构

很自然,Select元素对应于一个过程,其中每个可能的结果按照对应的概率而选择。下面是其工作方式:

val greeting = Select(0.6 -> "Hello, world!", 0.3 -> "Howdy, universe!", 0.1
     -> "Oh no, not again")
println(VariableElimination.probability(greeting, "Howdy, universe!"))
// prints 0.30000000000000004

注意,在Select中,概率累加起来不一定等于1。如果它们加起来不等于1,概率将被规格化——加起来等于1,同时保持概率之间的比例。在下面的例子中,每个概率都等于前一个例子中的两倍,因此加总起来等于2。规格化之后恢复成前一个例子中的概率,因此得到相同的结果。

val greeting = Select(1.2 -> "Hello, world!", 0.6 -> "Howdy, universe!", 0.2
     -> "Oh no, not again")
println(VariableElimination.probability(greeting, "Howdy, universe!"))
//prints 0.30000000000000004

Binomial

Binomial是一个有用的离散元素。想象一下一周有7天,每天都有一个“晴天”元素Flip(0.2)。现在您想要一个元素,其值为一周中放晴的天数。这可以用元素Binomial(7, 0.2)实现。这个元素的值是总共尝试7次,每次尝试为true的概率为0.2的情况下,尝试结果为true的次数。可以这样使用它:

import com.cra.figaro.library.atomic.discrete.Binomial
val numSunnyDaysInWeek = Binomial(7, 0.2)
   println(VariableElimination.probability(numSunnyDaysInWeek, 3))
//prints 0.114688

一般来说,Binomial取两个参数:尝试次数和每次尝试得出结果true的概率。Binomial的定义假定所有尝试是独立的,第一次尝试为真不会改变第二次尝试得出true的概率。

本节介绍两个连续原子元素的常见例子——Normal和Uniform。第4章详细说明连续元素。连续概率分布与离散分布略有不同,指定的不是每个值的概率,而是每个值的概率密度,概率密度描述的是以该值为中心的每个区间的概率。您仍然可以将概率密度视为和常规概率类似的概念,表明某个值与其他值可能性的对比。因为本章是Figaro的教程,第4章解释概率模型,我将把进一步的讨论推迟到那个时候。请放心,在后面这一点将会更加清晰。

Normal

正态分布是您可能熟悉的一种连续概率分布。正态分布还有其他一些名称,包括钟形曲线和高斯分布。图2-6展示了正态分布的概率密度函数。(如果非要吹毛求疵,可能称之为“单变量正态分布”更为合适,因为它定义了单一实数变量上的概率,您还可以在多个变量上定义多变量正态分布,但是我们不是那样的人,所以就称之为正态分布。)这个函数有一个均值——中心点(图中为1),以及一个标准差——函数沿中心点分布的程度(图中为0.5)。大约有68%的情况下,从正态分布生成一个值将得到与均值相差一个标准差的区间内的值。在统计和概率推理中,正态分布通常用均值和方差描述,后者是标准差的平方。图中的标准差为0.5,方差为0.25。因此,这个特定正态分布的标准规格描述是Normal(1,0.25)。

图2-6 正态分布的概率密度函数

Figaro遵循上述约定。它提供一个Normal元素,以均值和方差作为参数。可以这样定义Normal元素:

import com.cra.figaro.library.atomic.continuous.Normal
val temperature = Normal(40, 100)

均值为40,方差为100,意味着标准差为10。现在,假定您想要用这一元素进行推理。Figaro的变量消除算法只适用于可能取值个数有限的元素。特别是,它不能用于连续元素。所以,需要一个不同的算法。您将使用称作重要性抽样的算法,这是一种很适合于连续元素的近似算法。算法的运行方法如下:

import com.cra.figaro.algorithm.sampling.Importance
def greaterThan50(d: Double) = d > 50
println(Importance.probability(temperature, greaterThan50 _))

重要性抽样是一种每次产生不同答案的随机算法,这些答案通常应该在真值的附近。您得到的答案应该与0.1567接近,但是很有可能得到稍有不同的答案。

注意,这里的查询和之前略有不同。对于连续元素,取特定值(如50)的概率通常为0。原因是,连续元素中有无穷多个没有分隔的值。该过程得出50而非50.000000000000001或者之间其他值的可能性无限小。所以,通常不要向连续元素发出特定值概率的查询。

相反,您可以查询该值落入某个区间的概率。例子中的查询是预测greaterThan50,这个预测以双精度(Double)类型值作为参数,如果参数大于50则返回true。预测是一个元素值的布尔函数。当您查询某个元素是否满足预测时,询问的是将预测应用到某个元素值返回true的概率。在这个例子中,您的查询计算温度高于50的概率。

Scala注释:


greaterThan50之后的下划线告诉Scala,greaterThan50是一个传递给Importance. probability方法的函数值。如果没有这个下划线,Scala可能认为您试图将该函数应用到0参数上,从而出错。有时候,Scala可能自动理解这一点而无需明确地提供下划线。但有时候它无法做到,而会告诉您提供下划线。

Uniform

我们再来介绍一个同样熟悉的连续元素例子。Uniform元素取指定区间中的值,区间中的每个值可能性相同。您可以这样创建和使用Uniform元素:

import com.cra.figaro.library.atomic.continuous.Uniform
val temperature = Uniform(10, 70)
Importance.probability(temperature, greaterThan50 _)
// prints something like 0.3334

Uniform元素取两个参数:最小值和最大值。从最小值到最大值的所有值概率密度相同。在前一个例子中,最小值为10,最大值为70,所以范围的大小为60。您的查询预测是该值是否在50~70——区间大小为20。所以,预测的概率为20/60或者1/3,可以看到,重要性抽样得到的结果与此接近。

最后说明一下:这个元素的官方名称是连续均匀分布(continuous uniform)。在com.cra.figaro.library.atomic.discrete包中还可以找到离散均匀分布。正如您的预期,离散均匀分布明确列出一组值,其中的每个值出现可能性相同。

好了,现在您已经了解了构件,下面我们来看看如何组合它们,创建更大的模型。

在本节中,您将看到一些复合元素。前面已经说过,复合元素是构建于其他元素之上的更为复杂的元素。复合元素的例子很多,您将首先考察两个特殊的例子,If和Dist,然后了解如何使用大部分原子元素的复合版本。

您已经看到了If复合元素的一个例子。它由3个元素组成:一个测试,一个then子句和一个else子句。If表示这样的随机过程:首先检查测试的结果,如果测试值为true,则生成then子句的值;否则,生成else子句的值。图2-7展示了If元素的一个例子。If取3个参数,第一个参数是Element[Boolean],例中是sunnyToday元素。这个参数表示测试。第二个参数是then子句,当测试元素的值为true,则选中这个元素。如果测试元素值为false,则选择第三个参数——else子句。then和else子句必须有相同的值类型,这将成为If元素的值类型。

图2-7 If元素结构

下面是If元素的执行情况:

val sunnyToday = Flip(0.2)
val greetingToday = If(sunnyToday,
     Select(0.6 -> "Hello, world!", 0.4 -> "Howdy, universe!"),
     Select(0.2 -> "Hello, world!", 0.8 -> "Oh no, not again"))
println(VariableElimination.probability(greetingToday, "Hello world!"))
// prints 0.27999999999999997

上述代码打印输出0.28(在舍入误差范围内)是因为then子句被选中的概率为0.2(当sunnyToday为true),而这种情况下“Hello, world!”出现的概率为0.6。同时,else子句被选中的概率为0.8,此时“Hello, world!”出现的概率为0.2。所以,“Hello, world!”出现的总概率为(0.2 × 0.6) + (0.8 × 0.2) = 0.28。您可以通过明确评估两种情况看到这一结果:

sunnyToday.observe(true)
println(VariableElimination.probability(greetingToday, "Hello, world!"))
// prints 0.6 because the then clause is always taken

sunnyToday.observe(false)
println(VariableElimination.probability(greetingToday, "Hello, world!"))
// prints 0.2 because the else clause is always taken

Dist是一个实用的复合元素,Dist与Select类似,但是它选择一组元素中的一个而不是选择一组值中的一个。每个选择本身是一个元素,这也就是Dist是复合元素的原因。如果您想要在本身是随机过程的复杂选择之间选择,Dist很实用。例如,足球中的角球可能从短传或者高吊传中开始。这可以由一个Dist元素表示,该元素在两个过程之间选择,一个从短传开始,另一个从高吊传中开始。

Dist在com.cra.figaro.language包中,下面是Dist元素的一个例子:

val goodMood = Dist(0.2 -> Flip(0.6), 0.8 -> Flip(0.2))

如图2-8所示,Dist元素的结构类似于图2-5中Select的结构。不同之处在于,选择的不是结果值而是结果元素。您可以这样认为:Select直接选择一组可能值中的一个,没有任何中间过程。Dist是间接的:它选择一组过程中的一个运行,在运行中产生一个值。在例子中,由Flip(0.6)表示的过程被选中的概率为0.2,由Flip(0.2)表示的过程被选中的概率为0.8。

图2-8 Dist元素结构

下面是查询这一元素得到的结果:

println (VariableElimination.probability(goodMood, true))
// prints 0.28 = 0.2 * 0.6 + 0.8 * 0.2

您已经看到了采用数值参数的原子元素。例如,Flip以概率为参数,Normal以均值和方差为参数。如果您对这些数值参数不确定该怎么办?在Figaro中,答案很简单:将它们自身作为元素。

制作数值参数元素时,您就得到了原子元素的复合版本。例如,下面的代码定义一个复合Flip元素,并用它进行推理:

val sunnyTodayProbability = Uniform(0, 0.5)
val sunnyToday = Flip(sunnyTodayProbability)
println(Importance.probability(sunnyToday, true))
// prints something like 0.2548

这里,sunnyTodayProbability表示对今天是晴天的概率的不确定性,您认为0~0.5的任何值可能性一样大。那么,sunnyToday为true的概率等于sunnyTodayProbability元素的值。一般来说,复合Flip取单一参数,即表示Flip为true概率的一个Element[Double]。

Normal提供了更多可能性。在概率推理中,假定正态分布有一个特定的已知方差,通过均值表示不确定性的情况很常见。例如,您可能假定温度的方差为100,而均值在40左右,但是对此不太确定。此时可以用如下代码捕捉这种不确定性:

val tempMean = Normal(40, 9)
val temperature = Normal(tempMean, 100)
println(Importance.probability(temperature, (d: Double) => d > 50))
// prints something like 0.164

此外,您也可能对方差不确定,认为它可能是80或者105。此时可以使用如下代码:

val tempMean = Normal(40, 9)
val tempVariance = Select(0.5 -> 80.0, 0.5 -> 105.0)
val temperature = Normal(tempMean, tempVariance)
println(Importance.probability(temperature, (d: Double) => d > 50)
// prints something like 0.1549

更多信息请参见:


本章只说明了Figaro提供的原子和复合元素中的一小部分。您可以查看Figaro的Scaladoc,以了解更多实用的例子。Scaladoc(www.cra.com/Figaro)和Javadoc类似,是自动生成的Figaro库HTML文档。它还包含在Figaro二进制下载中,可从Figaro网页上下载。这一下载包含一个形如figaro_2.11-2.2.2.0-javadoc.jar的文件。该档案的内容可以使用7-Zip或者WinZip等程序解压。

Figaro提供两种构建模型的有用工具,称作Apply和Chain。这两种工具都是重要的元素。Apply可以在Figaro中引入Scala,利用其全部能力。Chain可以无限的方式创建元素之间的相互依赖关系。目前为止您所看到的复合元素(如If和复合Flip)可以特定的预定义方式创建依赖关系。Chain可以超越这些预定义依赖关系,创建您所需要的任何依赖。

我们从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型参数,因此两者互相匹配。该函数可以返回任何类型的值,在我们的例子中,函数返回一个字符串。

图2-9 Apply元素结构

下面介绍Apply元素定义随机过程的方式。它首先生成第一个元素参数的值。在我们的例子中,生成当月中晴天的特定数量,我们假定生成的是7。然后,该过程取得第二个函数参数,并将其应用到生成的值。在我们的例子中,过程将函数getQuality应用到7,获得结果average。该结果成为Apply元素的值。从这里可以看出,Apply的可能值和函数参数的返回值类型相同。在我们的例子中,Apply元素是一个Element[String]。

如果您对Scala还很陌生,那么我来介绍一下匿名函数。如果只想在一个位置上使用,为每个Apply元素定义单独的函数可能令人烦恼,尤其是上例中的简短函数。Scala提供了匿名函数,可以直接在使用的位置定义。图2-10展示了一个匿名函数的结构,它的定义和getQuality相同。这一结构的组件类似于命名函数。函数有一个参数i,类型为Integer。=>符号表示定义的是匿名函数。最后是一个函数体,与getQuality相同。

图2-10 匿名函数结构

您可以使用匿名函数定义和前一个元素等价的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的这个例子是人为的,但是使用它有许多实际的理由。下面是几个例子。

多参数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的情况下也很难计算。


熟悉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标记。

可以编写如下代码:

顾名思义,Chain(链)用于将元素链接为一个模型,模型中的元素依赖于另一个元素,那个元素又依赖其他的元素,依次类推。这与概率的链式法则相关,第5章将介绍这一法则。但是,理解Chain并不需要知道链式法则。

Chain也包含在com.cra.figaro.language包中。解释Chain的最简单方式是通过一张图。图2-11展示了两个元素:goodMood元素依赖monthQuality元素。如果您将其看作一个随机过程,该过程首先生成monthQuality的值,然后使用该值生成goodMood的值。这是贝叶斯网络的一个简单例子,第4章中您将学习这种方法。图中借用了贝叶斯网络的术语:monthQuality称为父节点,goodMood称为子节点

图2-11 一个变量依赖于另一个变量的双变量模型

因为goodMood依赖于monthQuality,goodMood使用Chain定义。monthQuality元素在前一小节已经定义。下面是goodMood的定义:

for { winProb <- Uniform(0, 0.5); win <- Flip(winProb) } yield !win
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]。

图2-12 Chain元素结构

这个Chain元素定义的随机过程如图2-13所示。这一过程有3个步骤。首先,为父节点生成一个值。在本例中,为monthQuality生成值average。其次,对该值应用链函数以获得一个元素,该元素称作结果元素。在例子中,您可以检查链函数的定义,发现结果元素是Flip(0.6)。第三,从结果元素生成一个值,例子中生成的是true。这个值成为子节点的值。

图2-13 Chain元素定义的随机过程。首先,为父节点生成一个值。接下来,根据链函数选择结果元素。最后,从结果元素生成一个值

下面我们总结Chain中涉及的所有类型。Chain由两个类型参数化——父节点的值类型(称作T)和子节点的值类型(称作U)。

在我们的例子中,goodMood是Element[Boolean],可以查询其值为true的概率:

println(VariableElimination.probability(goodMood, true))
// prints 0.3939286578054374

多参数Chain

让我们来考虑一个稍微复杂些的模型,其中goodMood依赖monthQuality 和 sunnyToday,如图2-14所示。

图2-14 一个三变量模型,其中goodMood取决于其他两个变量

可以使用一个双参数的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

这就是我们用简单元素构建复杂模型的一段旅程。在结束本章之前,我将描述一些应用证据和约束模型的方法。

现在,我已经相当详细地介绍了构建模型的方法。使用Figaro时,您的很大一部分精力将花在创建模型上。但是不要忽略证据的指定。Figaro提供了3种说明证据的机制:观测值,条件和约束。

您已经看到使用观测值指定证据的方法。在Hello World程序中已经提供了一个例子,使用如下的语句:

greetingToday.observe("Hello, world!")

一般来说,observe是元素上定义的一个方法,以元素的某个可能取值作为参数。它的效果是规定该元素必须采用该值。元素采用不同值的任何随机执行都被排除。这个观测值对相关元素的概率有影响。例如,在Hello World程序中,您可以看到声明这个观测值使sunnyToday更可能为true,因为根据该模型,今天是晴天比不是晴天更可能造成“Hello, world!”的问候语。

您还看到了关联观测值的另一种方法:

greetingToday.unobserve()

上述语句删除greetingToday上的观测值(如果有的话),这样就不会排除任何随机执行。结果是,关于greetingToday的证据不影响其他元素的概率。

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

顺便说一句,观测值只是条件的一个特例,要求某个元素取单一特定值。下面总结与条件相关的方法。

约束提供了规定元素相关情况的更通用手段,它通常有两个目的:(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

约束提供了和条件类似的一组方法。

条件和约束是相互独立的,所以设置条件或者删除所有条件不会删除任何现有约束,反之亦然。

作为连接元素的约束

约束有一种强大的用途。假定您认为两个元素的值相关,但是无法在元素定义中捕捉。例如,假设您的垒球队胜率为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能够描述无方向模型,如马尔科夫网络。

我们的Figaro“旋风之旅”就要结束。在下一章中,您将看到在应用程序中使用Figaro的完整示例。

www.manning.com/books/practical-probabilistic-programming 上可以找到部分练习的解答。

1.扩展Hello World程序,添加表示下床的一侧(正确或者错误)的变量。如果从错误的一侧下床,问候语总为“Oh no, not again!”,如果从正确的一侧下床,问候语的逻辑和之前相同。

2.在原始Hello World程序中,观测到今天的问候语是“Oh no, not again!”,查询今天的天气。现在,观测相同的证据并在练习1中修改的程序上提出相同的查询。查询答案发生了什么变化?能否直观地解释该结果?

3.在Figaro中,可以使用代码x === z作为如下代码的简写:

Apply(x, z, (b1: Boolean, b2: Boolean) => b1 === b2)

换言之,如果两个参数的值相等,产生一个值为true的元素。如果不运行Figaro,猜测下面两段程序生成的结果:

a) val x = Flip(0.4)
   val y = Flip(0.4)
   val z = x
   val w = x === z
   println(VariableElimination.probability(w, true))
b) val x = Flip(0.4)
   val y = Flip(0.4)
   val z = y
   val w = x === z
   println(VariableElimination.probability(w, true))

现在,运行Figaro程序检查您的答案。

4.在下面的练习中,您将发现FromRange元素很有用。FromRange有两个整数参数mn,生成mn-1的随机整数。例如,FromRange(0, 3)生成0、1、2的概率相同。编写一段Figaro程序计算掷两个骰子得出总数11的概率。

5.编写一个Figaro程序计算第一个骰子掷出6时,两个骰子总数大于8的概率。

6.在“地产大亨”游戏中,当两个骰子掷出相同数字时可以多玩一个回合。如果连续三次出现这种情况,您就会入狱。编写一段Figaro程序计算任何一个回合中发生这种情况的概率。

7.想象一个游戏,您有一个轮盘和5个面数不同的骰子。轮盘有5个概率相等的结果:4、6、8、12和20。在游戏中,首先转动轮盘,然后滚动面数与轮盘结果相同的骰子。编写一个Figaro程序表现这个游戏。

  a)计算滚动12面骰子的概率。

  b)计算掷出数字7的概率。

  c)已知掷出的是7,计算滚动的是12面骰子的概率。

  d)已知滚动的是12面骰子,计算掷出数字7的概率。

8.现在,修改练习7中的游戏,轮盘有卡住的趋势,在连续两次转动时停在同一个结果处。使用与makeStreaky类似的逻辑,编写一个约束,说明两次相邻的转动得到相同值的概率高于不同值的概率。连续玩该游戏两次。

  a)计算第二次掷骰子得到7的概率。

  b)已知第一次掷骰子得到7,计算第二次掷出7的概率。


相关图书

ChatGPT原理与应用开发
ChatGPT原理与应用开发
动手学机器学习
动手学机器学习
机器学习与数据挖掘
机器学习与数据挖掘
机器学习公式详解 第2版
机器学习公式详解 第2版
自然语言处理迁移学习实战
自然语言处理迁移学习实战
AI医学图像处理(基于Python语言的Dragonfly)
AI医学图像处理(基于Python语言的Dragonfly)

相关文章

相关课程