Python极客编程:用代码探索世界

978-7-115-58711-4
作者: [美]李·沃恩(Lee Vaughan)
译者: 王海鹏
编辑: 刘雅思
分类: Python

图书目录:

详情

本书包含16个有趣的编程项目,共分为12章。每章从一个明确的项目目标开始,引导读者像程序员一样思考解决问题的方法并完成任务。本书介绍用贝叶斯法则确定事件概率,用自然语言处理技术分析语料库,用collections和random等模块加密字符,用OpenCV和NumPy等库实现图像差异检测、图像属性测量、人脸检测、人脸识别等计算机视觉应用,用turtle模块模拟图像移动轨迹,用pandas库分析数据,用bokeh等库进行数据可视化。通过对本书的学习,读者将学会使用Python创建完整、实用的Python程序。 本书能帮助Python初学者理解编程思想并培养Python编程技能,也能帮助有一定编程基础的Python程序员从项目实战中获得解决实际问题的启发。

图书摘要

版权信息

书名:Python极客编程:用代码探索世界

ISBN:978-7-115-58711-4

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

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

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

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

著    [美] 李•沃恩(Lee Vaughan)

译    王海鹏

责任编辑 刘雅思

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

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

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

读者服务热线:(010)81055410

反盗版热线:(010)81055315

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e58711”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


Copyright © 2021 by Lee Vaughan. Title of English-language original: Real-World Python: A Hacker’s Guide to Solving Problems with Code, ISBN 9781718500624, published by No Starch Press. Simplified Chinese-language edition copyright ©2021 by Posts & Telecom Press. All rights reserved.

本书中文简体字版由美国No Starch出版社授权人民邮电出版社出版。未经出版者书面许可,对本书任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


本书包含16个有趣的编程项目,共分为12章。每章从一个明确的项目目标开始,引导读者像程序员一样思考解决问题的方法并完成任务。本书介绍用贝叶斯法则确定事件概率,用自然语言处理技术分析语料库,用collections和random等模块加密字符,用OpenCV和NumPy等库实现图像差异检测、图像属性测量、人脸检测、人脸识别等计算机视觉应用,用turtle模块模拟图像移动轨迹,用pandas库分析数据,用bokeh等库进行数据可视化。通过对本书的学习,读者将学会使用Python创建完整、实用的Python程序。

本书能帮助Python初学者理解编程思想并培养Python编程技能,也能帮助有一定编程基础的Python程序员从项目实战中获得解决实际问题的启发。


李•沃恩(Lee Vaughan)是一位程序员、流行文化爱好者、教育工作者。作为埃克森美孚公司的前主管级科学家,他负责构建并审查计算机模型,开发和测试软件,并培训地球科学家和工程师。他撰写了《Python编程实战—妙趣横生的项目之旅》和本书,帮助自学者磨炼Python技能,并从中获得乐趣!


克里斯•克伦(Chris Kren)毕业于南阿拉巴马大学,获得信息系统硕士学位。他目前在网络安全领域工作,经常使用Python进行报告、数据分析和自动化。

埃里克•莫滕松(Eric Mortenson)毕业于威斯康星大学麦迪逊分校,获得数学博士学位。他曾在宾夕法尼亚州立大学、昆士兰大学和马克斯-普朗克数学研究所担任研究和教学职务。他是圣彼得堡州立大学的数学副教授。


如果你已经学会了Python编程的基础知识,就已经准备好编写执行现实世界任务的完整程序了。

在本书中,我们将使用Python编程语言编写程序以拯救失事船只的船员、向朋友发送超级秘密信息、帮助克莱德·汤博(Clyde Tombaugh)发现冥王星、模拟阿波罗8号的自由返回轨迹、选择火星着陆点、定位系外行星、与怪异的“变种人”战斗及逃离僵尸等。在这个过程中,你将应用强大的计算机视觉、自然语言处理和科学模块,如OpenCV、NLTK、NumPy、pandas和matplotlib,以及其他一系列旨在让你的计算工作更轻松的软件包。

你可以将本书看作一本为大学二年级学生准备的Python图书。它不是一本学习编程基础的教程,而是提供了一种让你基于项目继续提升Python技能的方式。这样一来,你就不必浪费金钱和书架空间来重学已经学过的概念了。我仍然会解释项目的每一步,你会看到关于如何使用库和模块的详细说明,包括如何安装它们。

本书的这些项目将吸引所有想要利用Python编程来进行实验、测试理论、模拟大自然或者只是想玩一玩儿的人。当你完成这些项目时,会增加对Python库和模块的了解,学会快捷方法、有用的函数和实用的技术。这些项目并不专注于孤立的模块代码片段,而致力于教会你如何构建完整的、可工作的程序,这些程序涉及现实世界的应用、数据集和问题。

Python是一种高级的、解释型的、通用的编程语言。它是免费的、高度交互的,并且可以向所有主要的平台和微控制器(如树莓派)移植。Python支持函数式编程和面向对象编程,并且可以与许多用其他编程语言(如C++)编写的代码进行交互。

由于Python对初学者来说很容易上手,对专家来说也很有用,因此它已经渗透到了学校、公司、金融机构,以及大多数科学领域。Python现在是机器学习、数据科学和人工智能应用中最流行的语言之一。

以下是对本书各章内容的概述。读者不必按顺序学习,但我会在首次介绍新模块和新技术时,对它们进行更为详尽的解释。

第1章:用贝叶斯法则营救失事船只的船员

利用贝叶斯概率有效地指导海岸警卫队在蟒蛇角附近进行搜救工作。读者将从本章获得使用OpenCV、NumPy和itertools模块的经验。

第2章:用计量文体学来确定作者的身份

使用自然语言处理来确定是阿瑟·柯南·道尔爵士(Sir Arthur Conan Doyle)还是H.G.威尔斯(H.G.Wells)写了小说《失落的世界》(The Lost World)。读者将从本章获得使用NLTK、matplotlib和风格测量技术(如停顿词、词性、词汇丰富度和雅卡尔相似度)的经验。

第3章:用自然语言处理总结演讲

从互联网上抓取著名的演讲稿,自动生成要点摘要,然后将小说的文本变成酷炫的广告或宣传材料进行展示。读者将从本章获得使用requests、Beautiful Soup、regex、NLTK、collections、wordcloud和matplotlib的经验。

第4章:使用书籍密码发送超级秘密消息

通过数字方式重现肯·福莱特(Ken Follett)的畅销间谍小说《燃烧的密码》(The Key to Rebecca)中使用的一次性密码本方法,与你的朋友分享无法破解的密码。读者将从本章获得使用collections模块的经验。

第5章:发现冥王星

重现1930年克莱德·汤博(Clyde Tombaugh)发现冥王星时使用的闪烁比较器装置,然后使用现代计算机视觉技术来自动寻找和跟踪微小的瞬变天体(如彗星和小行星)相对于星域的移动。读者将从本章获得使用OpenCV和NumPy的经验。

第6章:模拟阿波罗8号的自由返回轨迹

策划并执行巧妙的自由返回飞行路线,说服NASA提前一年登月。读者将从本章获得使用turtle模块的经验。

第7章:选择火星着陆点

根据现实的任务目标,确定火星着陆器的潜在着陆点;在火星地图上显示候选地点以及地点的统计摘要。读者将从本章获得使用OpenCV、Python图像库、NumPy和tkinter的经验。

第8章:探测遥远的系外行星

模拟一个系外行星在其太阳前经过,绘制相对亮度的变化,并估计该行星的直径;最后模拟新的詹姆斯·韦伯太空望远镜对一颗系外行星的直接观测,包括估计该行星上一天的时长。读者将从本章获得使用OpenCV、NumPy和matplotlib的经验。

第9章:识别朋友或敌人

对机器人哨兵炮进行编程,从视觉上区分人类和邪恶的变种人。读者将从本章获得使用OpenCV、NumPy、playsound、pyttsx3和datetime的经验。

第10章:用人脸识别限制访问

利用人脸识别来保护访问安全实验室的通道。读者将从本章获得使用OpenCV、NumPy、playsound、pyttsx3和datetime的经验。

第11章:创建交互式僵尸逃离地图

构建一个人口密度地图,帮助电视剧《行尸走肉》(The Walking Dead)中的幸存者逃离亚特兰大,前往美国西部的安全地带。读者将从本章获得使用pandas、bokeh、holoviews和webbrowser的经验。

第12章:我们生活在计算机模拟中吗

找出一种方法,让模拟生物(也许是我们)找到他们生活在计算机模拟中的证据。读者将从本章获得使用turtle、statistics和perf_counter的经验。

每章最后都有至少一个实践项目或挑战项目。读者可以在附录或本书网站上找到实践项目的解决方案。这些解决方案并不是唯一的,也不一定是最好的,你可能会想出更好的解决方案。

然而,涉及挑战项目时,你就要靠自己了。要么沉下去,要么浮起来,这是一种很好的学习方式!我希望这本书能激励你创造新的项目,所以请将挑战项目当成自己想象力沃土中的种子。

读者可以从本书网站下载本书的所有代码,包括实践项目的解决方案,网址是https://nostarch.com/real-world-python/;你还可以在这里找到勘误表,以及其他更新内容。

编写这样一本书,几乎不可能不出现一些错误。如果你发现了问题,请将它提交给出版商:errata@nostarch.com。我们将在勘误表中添加所有必要的更正,并在本书未来的印次中进行修正。

我在Microsoft Windows 10环境下使用Python 3.7.2构建了本书中的所有项目。如果你使用的是其他操作系统,也没有问题:我在适当的时候,为其他平台提出了建议使用的兼容模块。

本书中的代码示例来自 Python IDLE 文本编辑器或交互式 shell。IDLE是Integrated Development and Learning Environment(集成开发和学习环境)的缩写。它是在一个集成开发环境(Integrated Development Environment,IDE)中加了一个L,这样这个首字母缩写暗指了Monty Python的Eric Idle。交互式的shell(也称“解释器”)是一个窗口,它可以让你在不需要创建文件的情况下立即执行命令或运行测试代码。

IDLE有许多缺点(如没有行号列),但它是免费的,并且与Python捆绑在一起,所以每个人都可以使用它。你可以使用任何你喜欢的IDE,流行的选择包括Visual Studio Code、Atom、Geany、PyCharm和Sublime Text。这些都可以在各种操作系统上工作,包括Linux、macOS和Windows。另一个IDE—PyScripter,只适用于Windows操作系统。关于可用的Python编辑器和兼容平台的列表,可在网上搜索“PythonEditors”查看。

你可以选择在计算机上直接安装Python,也可以通过发行版安装。要直接安装,请在Python官方网站的下载页面找到针对你的操作系统的安装说明。配置了Linux和macOS操作系统的计算机通常预装了Python,但你可能希望升级这个安装版本。每一个新的Python发行版都会增加一些特性,也会有一些特性被废弃,所以如果你的版本早于Python 3.6,我建议升级。

点击Python站点上的下载按钮(图1),可以下载默认的32位Python。

图1 Python官方网站的下载页面,带有Windows平台的快捷按钮

如果你想安装64位版本,则可在具体版本的列表(图2)中点击相应的链接。

图2 Python官方网站下载页面中的具体版本列表

点击特定的版本,将进入图3所示的页面。在这里,点击64位可执行安装程序,将启动一个安装向导。按照向导的指示,采用默认的建议。

本书中的一些项目要求使用非标准包,需要单独安装。这并不困难,但是你可以安装一个Python发行版,从而让事情变得更简单。发行版可以有效地加载和管理数百个Python包。你可以将它看作一站式购物。这些发行版中的包管理器会自动找到并下载包的最新版本,包括它的所有依赖关系。

图3 Python 3.8.2版本在Python官方网站上的文件列表

Anaconda是Continuum Analytics提供的一个流行的免费Python发行版。可以从Anaconda官方网站下载它。另一个Python发行版是Enthought Canopy,不过只有基本版是免费的,可以在Enthought Canopy官方网站找到它。无论你是单独安装Python和它的包,还是通过发行版安装,完成本书中的项目都没有任何问题。

安装后,Python应该已经出现在操作系统的应用程序列表中了。当你启动它时,应该会出现shell窗口,如图4(背景)所示。你可以使用这个交互式环境来运行和测试代码片段。但如果要编写较大的程序,会用到文本编辑器,它能够保存代码,如图4(前景)所示。

图4 本地Python shell窗口(背景)和文本编辑器(前景)

要在IDLE文本编辑器中创建一个新文件,请点击File►New File。要打开一个现有的文件,请点击File►Open或File►Recent Files。在这里,你可以通过点击Run►Run Module或在编辑器窗口中点击某处后按F5键来运行你的代码。注意,如果你选择使用像Anaconda这样的包管理器,或选择使用像PyCharm这样的IDE,你的环境可能看起来与图4不同。

也可以通过在PowerShell或终端中输入程序名来启动Python程序。你需要在Python程序所在的目录下启动Python程序。例如,如果你没有从正确的目录下启动Windows PowerShell,就需要使用cd命令更改目录路径(图5)。

图5 在Windows PowerShell中更改目录和运行Python程序

要了解更多信息,可参见Python Tutorial网站的“Execute Python scripts”页面。

最后,你可能希望将每一章的依赖安装在一个单独的虚拟环境中。在Python中,虚拟环境是一个自足的目录树,它包括一个Python安装和一些附加包。当你安装多个版本的Python时,它们是很有用的,因为一些包可能在某个版本中工作,但在其他版本中不能工作。此外,可能有一些项目需要用到同一个包的不同版本。将这些安装分开,可以防止兼容性问题。

本书中的项目不需要使用虚拟环境,按照我的说明,你将在整个操作系统中安装所需的包。但是,如果你确实需要将软件包与操作系统隔离开来,可以考虑为本书的每一章安装不同的虚拟环境(参见Python官方网站的“venv—Creation of virtual environments”页面和“12. Virtual Environments and Packages”页面)。

本书中的许多项目涉及一些统计和科学概念,它们有几百年的历史,但通过手工来应用它们是不切实际的。不过,随着1975年个人计算机的问世,我们存储、处理和共享信息的能力已经提高了许多数量级。

在现代人类20万年的历史中,只有生活在最近47年的我们才有幸使用这种神奇的设备,实现遥不可及的梦想。引用莎士比亚的一句话:“我们是少数。我们是少数幸运儿。”

让我们充分利用这个机会。在本书接下来的部分中,你将轻松完成那些让过去的天才们感到沮丧的任务。你将会对我们最近取得的一些惊人成就刮目相看。你甚至会开始想象未来的新发现。


No Starch出版社的团队在本书的制作上又交出了一份出色的答卷。他们是非常出色的专业人员,没有他们就不会有这本书。我对他们深表感谢和尊敬。

还要感谢Chris Kren和Eric Evenchick为本书所做的代码审核工作,感谢Joseph B.Paul、Sarah和Lora Vaughan为本书的人脸识别项目进行的角色扮演,感谢Hannah Vaughan提供的非常有用的照片。

特别感谢Eric T. Mortenson细致的技术审查以及许多有益的建议和补充。Eric为第1章提出了建议,并提供了大量的挑战和实践项目,包括将蒙特卡洛模拟应用于贝叶斯法则、按章节来总结小说、建立月球和阿波罗8号之间的相互作用模型、以3D方式观察火星及计算有轨道卫星的系外行星的光照曲线等。这本书因为他的努力而变得无比精彩。

最后,感谢Stack Overflow的所有贡献者。Python最棒的地方之一就是它广泛而包容的用户社区。无论你有什么问题,都有人可以回答;无论你想做什么奇怪的事情,都可能有人曾经做过,你可以在Stack Overflow上找到他们。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供配套源代码下载。要获得配套资源,请在异步社区本书页面中点击“配套资源”,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e58711”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受您的建议后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

扫描下方二维码,您将会在异步社区微信服务号中看到本书信息及相关的服务提示。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书审校等工作,可以发邮件给本书的责任编辑(liuyasi@ptpress.com.cn)。

如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端和网络技术等。

异步社区

微信服务号


在1740年左右,托马斯·贝叶斯(Thomas Bayes)创造了贝叶斯法则,后来该法则成为有史以来最成功的统计概念之一。但200多年来,它一直被束之高阁,基本上被人们忽视了,因为它的烦琐数学运算用手工完成是不切实际的。现代计算机的发明才让贝叶斯法则发挥了它的全部潜力。如今,得益于快速计算机处理器,它已经成为数据科学和机器学习的一个关键组成部分。

贝叶斯法则向我们展示了用于接受新的数据和重新计算概率估计的数学上的正确方法。从破解密码到证明高胆固醇会导致心脏病发作,它几乎渗透到了所有人类的工作中。贝叶斯法则的应用清单可以很容易地填满这一章。但是,因为没有什么比拯救生命更重要,所以我们将重点讨论如何利用贝叶斯法则来帮助营救在海上遇难的船员。

在本章中,我们将为海岸警卫队的搜救工作创建一个模拟游戏。玩家将使用贝叶斯法则来指导他们决策,以便能够尽快地找到船员。在这个过程中,我们将开始使用流行的计算机视觉和数据科学工具,如开源计算机视觉库(Open Source Computer Vision Library,OpenCV)和NumPy。

贝叶斯法则帮助研究者确定在新的证据下某件事情为真的概率。正如伟大的法国数学家拉普拉斯所说:“给定果时因的概率,正比于给定因时果的概率。”贝叶斯法则的基本公式是:

其中,是假设,是数据。表示给定的概率,表示给定的概率。例如,假设我们知道某种癌症的检测方法并不总是准确的,可能会给出假阳性的结果(显示被测者患癌症,而实际却没有)。贝叶斯的表达式为:

最初的概率是根据临床研究得出的。例如,在1000名癌症患者中,有800人可能得到阳性检测结果,并且1000人中有100人可能被误诊。根据疾病发生率,一个人患癌症的总概率可能只有万分之五十。因此,如果患癌症的总概率较低,而阳性检测的总概率相对较高,那么给定阳性检测而患癌症的概率就会下降。如果研究记录了不准确的测试结果的频率,贝叶斯法则就可以纠正测量误差!

现在你已经看到了一个应用示例,请看图1-1,它展示了贝叶斯法则中各种术语的名称,以及它们与癌症检测示例的关系。

图1-1 贝叶斯法则的术语定义及其与癌症检测示例的关系

为了进一步说明问题,让我们考虑一位女士,她在家里丢了她的阅读眼镜。她记得最后一次戴眼镜是在书房里。她走到那里,四处查看。她没有看到眼镜,但看到了一个茶杯,并且记起她曾去过厨房。此时,她必须做出选择:更彻底地搜索书房,或者离开书房并查看厨房。她决定去厨房。她在不知不觉中做了一个贝叶斯决策。

她先去了书房,因为她觉得在那里找到眼镜的概率最大。在贝叶斯术语中,这个在书房找到眼镜的初始概率被称为先验概率。经过粗略的搜索,她根据两个新的信息改变了她的决定:她不容易找到眼镜,而且她看到了茶杯。这代表了贝叶斯更新,即当有更多的证据可用时,一个新的后验估计(图1-1中的)就会被计算出来。

让我们想象一下,这位女士决定使用贝叶斯法则进行搜索。对于眼镜是在书房还是厨房,以及在这两个房间搜索的有效性,她会指定实际的概率。现在,她的决定不再是直觉,而是建立在数学的基础上,如果未来的搜索失败,数学可以不断给出更新。

图1-2展示了这位女士在指定这些概率后寻找眼镜的过程。

如图1-2所示,左图代表初始情况,右图代表利用贝叶斯法则更新后的情况。最初,假设眼镜在书房被找到的概率为85%,在厨房被找到的概率为10%。在其他可能的房间被找到的概率为1%,因为贝叶斯法则无法更新目标概率为0的情况(另外总有较小的概率是女士将眼镜放在了其他房间)。

图1-2 眼镜位置和搜索有效性的初始情况(左)与更新后的眼镜目标情况(右)

图1-2左图中斜线(/)后的数字代表搜索有效性概率(Search Effectiveness Probability,SEP)。SEP是对搜索一个区域的有效程度的估计。因为此时该女士只在书房内搜索过,所以对其他所有房间来说,这个值为0。在贝叶斯更新后(发现茶杯),她可以根据搜索结果重新计算概率,如图1-2右图所示。现在厨房是最有可能要找的地方,但其他房间的概率也会增加。

人类的直觉告诉我们,如果某个东西不在我们认为的地方,那么它在其他地方的概率就会增加。贝叶斯法则考虑到了这一点,因此眼镜在其他房间的概率也会增加。然而这只有在它们一开始就有可能在其他房间的情况下才会发生。

在给定搜索有效性的情况下,用于计算眼镜在某个房间的概率的公式是

其中,是眼镜在一个房间里的概率,是搜索有效性,是接收新证据前的先验(或初始)概率估计。

可以将目标概率和搜索有效性概率插入如下公式中,从而获得眼镜在书房中的最新概率。

如你所见,贝叶斯法则背后的简单数学,如果手工来做,很快就会变得枯燥乏味。幸运的是,我们生活在神奇的计算机时代,所以我们可以让Python来处理这些枯燥的工作!

在这个项目中,我们将编写一个Python程序,利用贝叶斯法则来寻找一名在蟒蛇角(Cape Python)失踪的孤独渔民。作为海岸警卫队在该地区搜救行动的主管,你已经与他的妻子谈过话,并确定了他最后的已知位置,现在已经超过了6小时。他在无线电中说他要弃船,但没有人知道他是在救生筏上还是在海中漂浮。蟒蛇角周围的海水是温暖的,但如果他泡在水中,12小时左右就会出现体温过低的情况。如果他戴着个人漂浮装置,运气好的话,也许能撑3天。

蟒蛇角附近的洋流很复杂(图1-3),目前风从西南方向吹来。海面能见度不错,但海浪波涛汹涌,人头很难被发现。

图1-3 蟒蛇角附近的洋流

在现实生活中,我们的下一步行动就是将掌握的所有信息输入海岸警卫队的搜救最佳规划系统(Search and Rescue Optimal Planning System,SAROPS)。这个软件会考虑风向、潮汐、水流、身体是在水中还是在船上等因素。然后,它生成矩形搜索区域,计算在每个区域找到船员的初始概率,并绘制最有效的飞行模式。

在这个项目中,假设SAROPS已经确定了3个搜索区域。我们需要做的就是编写应用贝叶斯法则的程序。我们也有足够的资源,可以在一天内搜索3个区域中的2个。我们要决定如何分配这些资源。这个压力很大,但我们有一个强大的帮手:贝叶斯法则。

目标

创建一个搜救游戏,利用贝叶斯法则来告知玩家应选择如何进行搜索。

搜索船员就像之前的例子中寻找丢失的眼镜一样。我们会从船员位置的初始目标概率开始,并根据搜索结果进行更新。如果我们实现了对一个区域的有效搜索,但什么也没找到,那么船员在另一个区域的概率就会增加。

然而,就像在现实生活中一样,有两种情况可能会导致出错:我们彻底搜索了一个区域,但还是错过了船员,或者搜索进行得不顺,浪费了一天的努力。将它对应为搜索有效性分数,在第一种情况下,我们可能得到0.85的SEP,但在剩下15%的区域没有搜索到船员。在第二种情况下,SEP是0.2,而我们留下了80%的区域没有搜索到!

我们可以看到现实中指挥官所面临的困境。要不要凭着直觉,无视贝叶斯?是否坚持贝叶斯的纯粹、冷酷的逻辑,因为我们相信这是最好的答案?或者我们是否会便宜行事,即使在怀疑的时候也要用贝叶斯来保护自己的事业和声誉?

为了方便玩家,我们将使用OpenCV库来建立一个使用该程序的界面。虽然界面中可能只有一些简单的东西,如在命令行环境中建立的菜单,但我们也希望有一张海角和搜索区域的地图。我们将使用这张地图来显示船员最后已知的位置,以及他被找到时的位置。OpenCV库是这个游戏的很好的选择,因为它可以用于显示图像、添加图形和文字。

OpenCV是主流的计算机视觉库。计算机视觉是深度学习的一个领域,它能让计算机像人类一样看到、识别和处理图像。OpenCV始于1999年的英特尔研究计划,现在由OpenCV基金会维护,这是一个非营利性基金会,免费提供软件。

OpenCV是用C++编写的,但也提供其他语言的接口,如Python和Java。OpenCV虽然主要针对实时计算机视觉应用,但也包括常见的图像处理工具,如Python Imaging Library中的那些工具。截至本书撰写时,最新的版本是OpenCV 4.1。

OpenCV需要Numerical Python(NumPy)和SciPy软件包,以便在Python中执行数值和科学计算。OpenCV将图像视为三维NumPy数组(图1-4)。这为其他Python科学库提供了最大限度的互操作性。

图1-4 三通道彩色图像数组的可视化表示

OpenCV将属性存储为行、列和通道。对于图1-4所示的图像,它的“形状”将是一个三元素元组(4,5,3)。每一组数字(如0-20-40或19-39-59)代表1像素。显示的数字是该像素的每个颜色通道的强度值。

因为本书中的许多项目都需要像NumPy和matplotlib这样的科学计算Python库,所以最好现在安装它们。

安装这些软件包有许多方法。一种方法是使用SciPy,它是一个用于科学和技术计算的开源Python库。

或者,如果你打算做大量的数据分析和绘图,你可能希望下载并使用一个免费的Python发行版,如Anaconda或Enthought Canopy,它们可以在Windows、Linux和macOS下工作。这些发行版让你不必去寻找和安装所有需要数据科学库的正确版本,如NumPy、SciPy等。

1.使用pip安装NumPy和其他科学软件包

如果你想直接安装产品,则可以使用pip(Preferred Installer Program)。pip是一个包管理系统,可以轻松安装基于Python的软件(参见Python官方网站)。在Windows和macOS中,Python 3.4和更新的版本都预装了pip。Linux用户可能需要单独安装pip。要安装或升级pip,可参考pip官方的安装说明,或在网上搜索关于在特定操作系统上安装pip的说明。

利用SciPy官方网站上的说明,我用pip安装了科学包。因为matplotlib需要多个依赖程序,所以你也需要安装这些程序。对于Windows,利用PowerShell执行下面的Python 3特有的命令,从包含当前Python安装文件的文件夹中启动(按住Shift键的同时单击鼠标右键)。

$ python -m pip install --user numpy scipy matplotlib ipython jupyter pandas sympy nose

如果你同时安装了Python 2和Python 3,则可使用python3代替python

要验证NumPy是否已经安装并且可以用于OpenCV,可打开Python shell并输入以下内容。

>>> import numpy

如果没有看到错误,你就可以安装OpenCV了。

2.使用pip安装OpenCV

可以在PyPI官方网站的“opencv_python”页面中找到OpenCV的安装说明。要为标准桌面环境(Windows、macOS和几乎所有GNU/Linux发行版)安装OpenCV,可在PowerShell或终端窗口中输入:

pip install opencv-contrib-python

或者:

python -m pip install opencv-contrib-python

如果安装了多个Python版本(如Python 2.7和Python 3.7),则需要指定要使用的Python版本:

py -3.7 -m pip install --user opencv-contrib-python

如果使用Anaconda作为分发媒质,则可以执行以下命令:

conda install opencv

要检查一切是否正确加载,可在shell中输入以下内容:

>>> import cv2

如果程序没有报错,就说明OpenCV安装成功。如果程序报错,则可查阅PyPI官方网站的“opencv_python”页面提供的故障排除列表。

本节涉及的bayes.py程序将模拟在3个连续的搜索区域内搜索一个失踪船员。程序结果将显示一个地图,为用户输出一个搜索选项菜单,为船员随机选择一个位置,要么在搜索到失踪船员时显示他的位置,要么对每个搜索区域找到失踪船员的概率进行贝叶斯更新。用户可以从本书网站下载相关代码和地图图片(cape_python.png)。

1.导入模块

代码清单 1-1 通过导入所需的模块和赋值一些常量来启动bayes.py程序。用户在代码中使用这些模块时会看到它们的作用。

代码清单1-1 导入模块,并为bayes.py程序中使用的常量赋值

bayes.py, part 1
import sys
import random
import itertools
import numpy as np
import cv2 as cv

MAP_FILE = 'cape_python.png'

SA1_CORNERS = (130, 265, 180, 315) # (UL-X, UL-Y, LR-X, LR-Y)
SA2_CORNERS = (80, 255, 130, 305) # (UL-X, UL-Y, LR-X, LR-Y)
SA3_CORNERS = (105, 205, 155, 255) # (UL-X, UL-Y, LR-X, LR-Y)

将模块导入程序时,最好依次导入Python标准库模块、第三方模块、用户定义模块。sys模块包括操作系统的命令,如退出。random模块允许用户生成伪随机数。itertools模块支持用户进行循环操作。最后,numpycv2分别导入NumPy和OpenCV。用户也可以指定速记名(np,cv),以减少以后的击键次数。

接下来指定一些常量。根据PEP8 Python风格指南(参见Python官网的“PEP 8 -- Style Guide for Python Code”页面),常量名应该是全大写的。这并不能使变量真正成为不可改变的,但它确实提醒了其他开发者,他们不应该改变这些变量。

用于虚构的蟒蛇角地区的地图是一个名为cape_python.png的图像文件(图1-5)。将这个图像文件赋给一个名为MAP_FILE的常量变量。

图1-5 蟒蛇角灰度基本地图(cape_python.png)

在该图像上,我们将搜索区域绘制为矩形。OpenCV会通过角点的像素数来定义每个矩形,因此我们要分配一个变量来存放这4个点,作为一个元组。要求的顺序是左上角、左上角、右下角、右下角。变量名为SA,表示搜索区域(search area)。

2.定义Search类

是面向对象编程(Object-Oriented Programming,OOP)中的一种数据类型。OOP是一种替代函数式/过程式编程的方法,它对大型复杂的程序特别有用,因为它产生的代码更容易更新、维护和复用,同时减少代码重复。OOP是围绕名为对象的数据结构而建立的。对象由数据、方法及其交互组成。因此,OOP能很好地适用于游戏程序,因为游戏程序通常使用交互对象,如宇宙飞船和小行星。

类是一个模板。用户利用这个模板可以创建多个对象。例如,你可以创建一个类,用于在游戏中建造战舰。每艘战舰都会继承某些一致的特性,如吨位、巡航速度、燃料水平、伤害水平、武器装备等。你也可以为每个战舰对象赋予独特的特性,如不同的名字。一旦创建或实例化,每艘战舰的个体特征就会根据舰船燃烧的燃料量、受到的伤害、使用的弹药量等开始分化。

在bayes.py中,我们将使用一个类作为模板来创建一个搜救任务,允许有3个搜索区域。代码清单 1-2 定义了Search类,它将作为游戏的蓝本。

代码清单1-2 定义Search类和__init__()方法

bayes.py, part 2
class Search():
    """Bayesian Search &  Rescue game  with 3 search areas."""

    def __init__(self, name):
        self.name = name
      ❶ self.img = cv.imread(MAP_FILE, cv.IMREAD_COLOR) 
        if self.img  is None:
            print('Could not load map file {}'.format(MAP_FILE),
                  file=sys.stderr)
            sys.exit(1)

      ❷ self.area_actual  =  0
        self.sailor_actual = [0, 0] # As "local" coords within search area

      ❸ self.sa1 = self.img[SA1_CORNERS[1] : SA1_CORNERS[3],
                            SA1_CORNERS[0] : SA1_CORNERS[2]]

        self.sa2 = self.img[SA2_CORNERS[1] : SA2_CORNERS[3],
                            SA2_CORNERS[0] : SA2_CORNERS[2]]

        self.sa3 = self.img[SA3_CORNERS[1] : SA3_CORNERS[3],
                            SA3_CORNERS[0] : SA3_CORNERS[2]]

      ❹ self.p1 = 0.2
        self.p2 = 0.5
        self.p3 = 0.3

        self.sep1 = 0
        self.sep2 = 0
        self.sep3 = 0

首先定义一个名为Search的类。根据PEP8风格指南,类名的第一个字母应该大写。

接下来,定义一个方法,为对象设置初始属性值。在OOP中,属性是一个与对象相关联的命名的值。如果对象是一个人,则属性可能是人的体重或眼睛的颜色。方法也是属性,只是碰巧是一个函数,当它们运行时,它们会传入一个对其实例的引用。__init__()方法是Python在创建新对象时自动调用的特殊内置方法。它绑定了每个新创建的类的实例的属性。在这个例子中,我们向它传入两个参数:self和希望让该对象使用的名字。

self参数是对正在创建的类的实例的引用,或者说是方法调用时作用的实例,技术上,被称为上下文实例。例如,如果你创建了一艘名为Missouri的战舰,那么对这个对象来说,self就变成了Missouri,你可以为这个对象调用一个方法,如大炮开火的方法,用点符号表示:Missouri.fire_big_guns()。通过在实例化对象时赋予其唯一的名称,每个对象的属性作用域就会与其他所有对象分开。这样一来,一艘战舰所受到的伤害就不会影响舰队的其他战舰。

__init__()方法下列出一个对象的所有初始属性值是一个好的做法。这样,用户可以看到对象的所有关键属性。这些属性将在以后的各种方法中使用,代码的可读性和可维护性会更好。在代码清单1-2中,这些都是self属性,如self.name

指定给self的属性也会表现得像程序化编程中的全局变量。类中的方法将能够直接访问它们,而不需要参数。因为这些属性“躲”在类的保护伞下,所以它们并不像真正的全局变量那样不被鼓励使用。不鼓励使用全局变量是因为全局变量是在全局作用域内指定的,并且在各个函数的局部作用域内修改。

使用OpenCV的imread()方法将MAP_FILE变量赋给self.img属性❶。MAP_FILE图像是灰度的,但我们希望在搜索过程中给它添加一些颜色。因此,使用ImreadFlag,即cv.IMREAD_COLOR,以彩色模式加载图像。这将设置3个颜色通道(B,G,R),供以后使用。

如果图像文件不存在(或者用户输入了错误的文件名),则OpenCV会抛出一个令人困惑的错误:NoneType object is not subscriptable。要处理这个问题,需要使用一个条件语句来检查self.img是否为None。如果是,则输出一条错误信息,然后使用sys模块退出程序。向它传入一个退出代码1,表示程序以错误方式终止。设置file=stderr将导致在Python解释器窗口中使用标准的“错误红”文本颜色,尽管在其他窗口中不会(如在PowerShell中)。

接下来,为找到船员时他的实际位置指定两个属性。第一个属性将保存搜索区域的编号❷,第二个属性是预先设定的 位置,现在所赋值是占位符。稍后,我们会定义一个方法来随机选择最终的值。注意,这里使用一个列表来表示位置坐标,因为需要一个可改变的容器。

地图图像是以数组的形式加载的。数组是相同类型的对象的固定大小的集合。数组是内存效率高的容器,可以提供快速的数值运算,并有效地使用计算机的寻址逻辑。一个使NumPy特别强大的概念是向量化,它用更有效的数组表达式代替了显式循环。基本上,操作发生在整个数组上,而不是单个元素上。使用NumPy,内部循环会转到高效的C和Fortran函数上,这些函数比标准的Python技术更快。

为了能够在搜索区域内使用局部坐标,我们可以从数组中创建一个子数组❸。注意,这是用索引来完成的。我们首先提供从左上角的到右下角的的范围,然后提供从左上角的到右下角的的范围。这是一个NumPy的特性,我们需要一些时间来适应,尤其是我们大多数人习惯在笛卡儿坐标中将放在之前。

对接下来的两个搜索区域重复这个过程,然后设置在每个搜索区域找到船员的预搜索概率❹。在现实生活中,这些将来自SAROPS程序。当然,p1代表区域1,p2代表区域2,以此类推。最后是一些占位符属性,用于搜索有效性概率。

3.绘制地图

Search类中,我们使用OpenCV中的功能来创建一个显示基本地图的方法。这个地图包括搜索区域、一个比例尺和船员的最后已知位置(图1-6)。

图1-6 Bayes.py的初始游戏画面(基本地图)

代码清单1-3定义了显示初始地图的draw_map()方法。

代码清单1-3 定义显示基本地图的方法

bayes.py, part 3
def draw_map(self, last_known):
    """Display basemap with scale, last known xy location, search areas.""" 
    cv.line(self.img,  (20,  370),  (70,  370),  (0,  0,  0),  2)
    cv.putText(self.img, '0', (8, 370), cv.FONT_HERSHEY_PLAIN, 1, (0, 0, 0))
    cv.putText(self.img, '50 Nautical Miles', (71, 370),
               cv.FONT_HERSHEY_PLAIN, 1, (0, 0, 0))

  ❶ cv.rectangle(self.img, (SA1_CORNERS[0], SA1_CORNERS[1]),
                           (SA1_CORNERS[2], SA1_CORNERS[3]), (0, 0, 0), 1)
    cv.putText(self.img, '1',
              (SA1_CORNERS[0] + 3, SA1_CORNERS[1] + 15),
              cv.FONT_HERSHEY_PLAIN, 1, 0)
    cv.rectangle(self.img, (SA2_CORNERS[0], SA2_CORNERS[1]), 
                 (SA2_CORNERS[2], SA2_CORNERS[3]), (0, 0, 0), 1)
    cv.putText(self.img, '2',
               (SA2_CORNERS[0] + 3, SA2_CORNERS[1] + 15),
               cv.FONT_HERSHEY_PLAIN, 1, 0)
    cv.rectangle(self.img, (SA3_CORNERS[0], SA3_CORNERS[1]), 
                 (SA3_CORNERS[2], SA3_CORNERS[3]), (0, 0, 0), 1)
    cv.putText(self.img, '3',
              (SA3_CORNERS[0] + 3, SA3_CORNERS[1] + 15),
              cv.FONT_HERSHEY_PLAIN, 1, 0)

  ❷ cv.putText(self.img, '+', (last_known),
               cv.FONT_HERSHEY_PLAIN, 1, (0, 0, 255))
    cv.putText(self.img,  '+  =  Last  Known  Position',  (274,  355), 
               cv.FONT_HERSHEY_PLAIN, 1, (0, 0, 255))
    cv.putText(self.img, '* = Actual Position', (275, 370), 
               cv.FONT_HERSHEY_PLAIN, 1, (255, 0, 0))

  ❸ cv.imshow('Search Area', self.img) 
    cv.moveWindow('Search Area', 750, 10) 
    cv.waitKey(500)

定义draw_map()方法,以self和船员最后的已知坐标last_known作为其两个参数。然后用OpenCV的line()方法来绘制一个比例尺。向它传入的参数包括基本地图的图像,左、右坐标的两个元组,线条颜色元组和线条宽度。

现在为第一个搜索区域绘制一个矩形❶。像往常一样,传入基本地图图像,然后传入代表框的4个角的变量,最后传入颜色元组和线条宽度。再次使用putText(),将搜索区域编号放在左上角。针对搜索区域2和3,重复这些步骤。

使用putText(),在船员最后已知的位置绘制一个“+”❷。注意,这个符号是红色的,但是颜色元组是(0, 0, 255),而不是(255, 0, 0)。这是因为OpenCV使用的是蓝-绿-红(BGR)颜色格式,而不是更常见的红-绿-蓝(RGB)格式。

继续为图例放置文字,描述最后已知位置和实际位置的符号,当玩家搜索找到船员时,显示器应该显示这些符号。实际位置使用蓝色标记。

通过显示基本地图来完成该方法,使用OpenCV的imshow()方法❸,向它传入窗口的标题和图像。

为了尽量避免基本地图和解释器窗口相互干扰,强制基本地图显示在显示器的右上角(可能需要根据你的计算机调整坐标)。使用OpenCV的moveWindow()方法,并向它传入窗口的名称、'Search Area'和左上角的坐标。

最后使用waitKey()方法,在图像渲染到Windows时,该方法引入n ms的延时。向它传入500,即延时500 ms。这将导致游戏菜单在基本地图后半秒出现。

4.选择船员的最终位置

代码清单 1-4 定义了一种随机选择船员实际位置的方法。方便起见,坐标最初是在搜索区域子数组内找到的,然后相对于整个基本地图图像转换为全局坐标。这种方法之所以有效,是因为所有搜索区域的大小和形状都相同,所以可以使用相同的内部坐标。

代码清单1-4 定义一种随机选择船员实际位置的方法

bayes.py, part 4
def sailor_final_location(self,  num_search_areas):
    """Return the actual x,y location of the missing sailor."""
    # Find sailor coordinates with respect to any Search Area subarray. 
    self.sailor_actual[0] = np.random.choice(self.sa1.shape[1], 1) 
    self.sailor_actual[1] = np.random.choice(self.sa1.shape[0], 1)

  ❶ area = int(random.triangular(1, num_search_areas + 1)) 

    if area == 1:
        x  =  self.sailor_actual[0] +  SA1_CORNERS[0]
        y  =  self.sailor_actual[1] +  SA1_CORNERS[1]
      ❷ self.area_actual  =  1 
     elif area == 2:
        x =  self.sailor_actual[0] +  SA2_CORNERS[0] 
        y = self.sailor_actual[1] + SA2_CORNERS[1] 
        self.area_actual   =   2
     elif area  ==  3:
        x =  self.sailor_actual[0] +  SA3_CORNERS[0] 
        y = self.sailor_actual[1] + SA3_CORNERS[1] 
        self.area_actual   =   3
     return x, y

定义sailor_final_location()方法,它有两个参数:self和正在使用的搜索区域编号。对于self.sailor_actual列表中的第一个()坐标,使用NumPy的random.choice()方法从区域1子数组中选择一个值。请记住,搜索区域是从大图像数组中复制出来的NumPy数组。因为搜索区域/子数组的大小都是一样的,所以从其中一个区域选择的坐标将适用于所有区域。

可以用shape得到一个数组的坐标,如下所示:

>>> print(np.shape(self.SA1))
(50, 50, 3)

NumPy数组的shape属性必定是一个元组,其元素数与数组中的维数一样多。而且要记住,对于OpenCV中的数组,元组中元素的顺序是行、列,然后是通道。

现有的每个搜索区域都是大小为50像素×50像素的三维数组。因此,的内部坐标范围都是0~49。针对[0]使用random.choice()意味着从行中选择,最后一个参数1表示选择单个元素。[1]意味着从列中选择。

random.choice()产生的坐标范围是0~49。要在完整的基本地图图像中使用这些坐标,首先需要选择一个搜索区域❶。用random模块来做这件事,它在程序开始时导入。根据SAROPS的输出,船员最有可能在区域2,其次是区域3。因为这些初始目标概率是猜测的,不会直接对应于现实,所以使用triangular(三角)分布来选择包含船员的区域,参数是低的端点和高的端点。如果没有提供最终的模式参数,则模式默认为端点之间的中点。这将与SAROPS的结果一致,区域2最常被选中。

注意,我们在方法中使用局部变量area,而不是self.area属性,因为没有必要与其他方法共享这个变量。

要在基本地图上绘制船员的位置,我们需要添加相应的搜索区域角点坐标。这将“局部”搜索区域坐标转换为完整基本地图图像的“全局”坐标。我们还想跟踪搜索区域,所以更新self.area_actual属性❷。

搜索区域2和区域3时重复这些步骤,然后返回坐标。

提示 在现实生活中,船员会漂流,他移动到区域3的概率会随着每次搜索而增加。然而,我选择使用静态位置,以使贝叶斯法则背后的逻辑尽可能清晰。因此,这个场景的表现更像是搜索一艘沉没的潜艇。

5.计算搜索有效性并进行搜索

在现实生活中,天气和机械问题会导致搜索有效性得分较低。因此,每次搜索的策略将是生成一个搜索区域内所有可能的位置的列表,对该列表进行乱序,然后根据搜索有效性的值进行采样。因为SEP永远不会是1.0,所以如果我们只从列表的开头或结尾取样(不进行乱序),就永远无法访问藏在其“尾部”的坐标。

代码清单 1-5 还是在Search类中定义一个方法来随机计算给定搜索的有效性,并定义另一个方法来进行搜索。

代码清单1-5 定义随机计算搜索有效性和进行搜索的方法

bayes.py, part 5
  def calc_search_effectiveness(self):
      """Set decimal search effectiveness value per search area.""" 
      self.sep1 = random.uniform(0.2, 0.9)
      self.sep2 = random.uniform(0.2, 0.9) 
      self.sep3 = random.uniform(0.2, 0.9)

❶ def conduct_search(self, area_num, area_array, effectiveness_prob): 
      """Return search results and list of searched coordinates.""" 
      local_y_range = range(area_array.shape[0])
      local_x_range = range(area_array.shape[1])
    ❷ coords = list(itertools.product(local_x_range, local_y_range)) 
      random.shuffle(coords)
      coords = coords[:int((len(coords) * effectiveness_prob))]
    ❸ loc_actual = (self.sailor_actual[0], self.sailor_actual[1]) 
      if area_num == self.area_actual and loc_actual in coords:
          return 'Found in Area {}.'.format(area_num), coords 
      else:
          return 'Not Found', coords

首先定义搜索有效性方法。唯一需要的参数是self。对于每个搜索有效性属性(如E1),随机选择一个介于0.2~0.9的值。这些是任意值,意味着你将始终搜索至少20%的区域,但永远不会超过90%。

你可能会认为这3个搜索区域的搜索有效性属性是相互依赖的。例如,雾可能会影响所有3个地区,使得产生的结果都很差。另外,一些直升机可能有红外成像设备,会使结果更好。不管怎样,像这里所做的那样,使这些因素独立,可以进行更动态的模拟。

接下来,定义执行搜索的方法❶。必要的参数是对象本身、区域编号(由用户选择)、所选区域的子阵列和随机选择的搜索有效性值。

这里需要生成给定搜索区域内所有坐标的列表。将一个变量命名为local_y_range,并根据数组形状元组(表示行)中的第一个索引为其分配一个范围。对x_range值重复上述步骤。

要生成搜索区域中所有坐标的列表,可使用itertools模块❷。此模块是Python标准库中的一组函数,用于创建迭代器以实现高效循环。product()函数的作用是返回给定序列中所有重复排列的元组。在本例中,你将找到在搜索区域中组合的所有可能方法。要查看列表的运行情况,请在shell中输入以下命令:

>>> import itertools
>>> x_range = [1, 2, 3]
>>> y_range = [4, 5, 6]
>>>   coords = list(itertools.product(x_range, y_range))
>>> coords
[(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6), (3, 4), (3, 5), (3, 6)]

如你所见,坐标列表包含了x_rangey_range列表中元素的所有可能的配对组合。

接下来,对坐标列表进行乱序。这是为了避免在每次搜索事件中一直搜索列表的同一端。在下一行中,利用索引切片,根据搜索有效性概率来剪切列表。例如,较差的搜索有效性是0.3,这意味着一个区域中只有约三分之一的可能坐标位置被包含在列表中。因为我们会根据这个列表检查船员的实际位置,所以实际上会留下约三分之二的区域“未搜索”。

指定一个局部变量loc_actual,用来存放船员的实际位置❸,然后使用一个条件来检查是否已经找到了船员。如果用户选择了正确的搜索区域,乱序且剪切后的coords列表包含了船员的位置,则返回一个字符串,说明船员已经被找到,同时返回coords列表;否则,返回一个表示没有找到船员的字符串和coords列表。

6.应用贝叶斯法则并绘制菜单

代码清单 1-6 还是在Search类中,作用是定义一个方法和一个函数。revise_target_ probs()方法使用贝叶斯法则来更新目标概率,这些目标概率表示在每个搜索区域找到船员的概率。draw_menu()函数定义在Search类之外,用于显示一个菜单,作为图形用户界面(graphical user interface,GUI)来运行该游戏。

代码清单1-6 定义应用贝叶斯规则和在Python shell中绘制菜单的方法

bayes.py, part 6
    def revise_target_probs(self):
        """Update area target probabilities based on search effectiveness.""" 
        denom = self.p1 *  (1 - self.sep1) + self.p2 * (1 - self.sep2) \
                + self.p3 *  (1 - self.sep3)
        self.p1 = self.p1 * (1 - self.sep1) / denom 
        self.p2 = self.p2 * (1 - self.sep2) / denom 
        self.p3 = self.p3 * (1 - self.sep3) / denom

def draw_menu(search_num):
    """Print menu of choices for conducting area searches."""
    print('\nSearch {}'.format(search_num))
    print(
        """
        Choose next areas to search:

        0 - Quit
        1 - Search Area 1 twice
        2 - Search Area 2 twice
        3 - Search Area 3 twice
        4 - Search Areas 1 & 2
        5 - Search Areas 1 & 3
        6 - Search Areas 2 & 3
        7 - Start Over 
        """
        )

定义revise_target_probs()方法来更新船员在每个搜索区域的概率。它唯一的参数是self

为了方便,将贝叶斯方程分成两部分。从分母开始,我们需要将之前的目标概率乘以当前的搜索有效性值(参见1.1节,回顾其工作原理)。

计算出分母后,用它来完成贝叶斯方程。在OOP中,我们不需要返回任何东西,可以简单地在方法中直接更新属性,就像在过程式编程中声明全局变量一样。

接下来,在全局空间中,定义draw_menu()函数来绘制菜单。它的唯一参数是正在进行搜索的编号。因为这个函数没有“使用self”,所以不必把它包含在类定义中,尽管这也是一个有效的选择。

首先输出搜索编号。我们需要用搜索编号来跟踪是否在必要的搜索次数中找到了船员,目前设置为3。

使用三引号与print()函数来显示菜单。注意,用户将可以选择将两个搜索队分配到一个给定的区域,或者将它们分到两个区域。

7.定义main()函数

既然已经完成了Search类,我们就已经准备好将所有这些属性和方法投入工作了!代码清单1-7开始定义main()函数,用于运行程序。

代码清单1-7 定义main()函数的开始,用于运行程序

bayes.py, part 7
def main():
    app = Search('Cape_Python') 
    app.draw_map(last_known=(160, 290))
    sailor_x, sailor_y = app.sailor_final_location(num_search_areas=3) 
    print("-" * 65)
    print("\nInitial Target (P) Probabilities:")
    print("P1 = {:.3f}, P2 = {:.3f}, P3 = {:.3f}".format(app.p1, app.p2, app.p3))
    search_num = 1

main()函数不需要参数。首先使用Search类创建一个游戏应用程序,命名为app。将对象命名为Cape_Python

接下来,调用显示地图的方法。向该方法传入船员最后的已知位置,即坐标的元组。注意关键字参数last_known=(160, 290)的用法,确保清晰。

现在,通过调用该任务的方法并向它传入搜索区域的编号来获取船员的位置。然后输出初始目标概率,即先验概率,它是由你的海岸警卫队下属用蒙特卡洛模拟而不是贝叶斯法则计算出来的。最后,命名一个变量search_num并为其赋值1。这个变量将跟踪我们进行了多少次搜索。

8.评估菜单选项

代码清单 1-8 启动了main()中用于运行游戏的while循环。在这个循环中,玩家评估并选择菜单选项。选项包括搜索一个区域两次;在两个区域分头搜索、重新开始游戏,以及退出游戏。注意,玩家可以进行无限多次搜索来找到船员;我们的3天限制还没有“硬编码”到游戏中。

代码清单1-8 使用循环来评估菜单选项并运行游戏

bayes.py, part 8
    while True:
        app.calc_search_effectiveness() 
        draw_menu(search_num)
        choice = input("Choice: ")

        if choice == "0": 
            sys.exit()
    ❶ elif choice == "1":
          results_1, coords_1 = app.conduct_search(1, app.sa1, app.sep1) 
          results_2, coords_2 = app.conduct_search(1, app.sa1,  app.sep1)
        ❷ app.sep1 = (len(set(coords_1 + coords_2))) / (len(app.sa1)**2) 
          app.sep2 = 0
          app.sep3 = 0

      elif choice == "2":
          results_1, coords_1 = app.conduct_search(2, app.sa2, app.sep2) 
          results_2, coords_2 = app.conduct_search(2, app.sa2, app.sep2) 
          app.sep1 = 0
          app.sep2 = (len(set(coords_1 + coords_2))) / (len(app.sa2)**2) 
          app.sep3 = 0

      elif choice == "3":
          results_1, coords_1 = app.conduct_search(3, app.sa3, app.sep3) 
          results_2, coords_2 = app.conduct_search(3, app.sa3, app.sep3) 
          app.sep1 = 0
          app.sep2 = 0
          app.sep3 = (len(set(coords_1 + coords_2))) / (len(app.sa3)**2)

    ❸ elif choice == "4":
          results_1, coords_1 = app.conduct_search(1, app.sa1, app.sep1) 
          results_2, coords_2 = app.conduct_search(2, app.sa2, app.sep2) 
          app.sep3 = 0

      elif choice == "5":
          results_1, coords_1 = app.conduct_search(1, app.sa1, app.sep1) 
          results_2, coords_2 = app.conduct_search(3, app.sa3, app.sep3) 
          app.sep2 = 0

      elif choice == "6":
          results_1, coords_1 = app.conduct_search(2, app.sa2, app.sep2) 
          results_2, coords_2 = app.conduct_search(3, app.sa3, app.sep3) 
          app.sep1 = 0

    ❹ elif choice == "7": 
          main()

      else:
          print("\nSorry,  but  that  isn't  a  valid  choice.",  file=sys.stderr) 
          continue

启动一个while循环,它将运行到用户退出为止。首先使用点符号调用计算搜索有效性的方法。然后调用显示游戏菜单的函数,并向它传入搜索编号。通过使用input()函数要求用户做出选择,完成准备阶段。

玩家的选择将通过一系列的条件状态进行评估。如果选择0,则用户退出游戏。用户退出游戏时使用了在程序开始时导入的sys模块。

如果玩家选择1、2或3,这意味着他们想把两个搜索队都投入相应编号的区域。我们需要两次调用conduct_search()方法来生成两组结果和坐标❶。这里棘手的部分是确定整体的SEP,因为每个搜索都有自己的SEP。要做到这一点,可将两个坐标列表加在一起,并将结果合并为一个集合,以去除所有重复的结果❷。得到集合的长度,然后除以50×50搜索区域的像素数。因为我们没有搜索其他区域,所以将它们的SEP设置为0。

针对搜索区域2和3,复制并修改前面的代码。使用elif语句,因为每个循环只有一个菜单选项有效。这比使用另外的if语句更有效,因为true响应之后的所有elif语句都会被跳过。

如果玩家选择了4、5或6,这意味着他们想将两个搜索队分配到两个区域。在这种情况下,不需要重新计算SEP❸。

如果玩家找到了船员并想重新开始游戏,或者只是想重新开始,就调用main()函数❹。这将重置游戏并刷新地图。

如果玩家做了一个无效的选择,如“Bob”,用一个消息通知他们,然后用continue跳回循环的开始,再次要求玩家选择。

9.完成和调用main( )

代码清单1-9仍然在while循环中,完成main()函数,然后调用它来运行程序。

代码清单1-9 完成并调用main()函数

bayes.py, part 9
        app.revise_target_probs() # Use Bayes' rule to update target probs.

        print("\nSearch {} Results 1 = {}"
              .format(search_num, results_1), file=sys.stderr)
        print("Search {} Results 2 = {}\n"
              .format(search_num, results_2), file=sys.stderr)
        print("Search {} Effectiveness (E):".format(search_num))
        print("E1 = {:.3f}, E2 = {:.3f}, E3 = {:.3f}"
              .format(app.sep1, app.sep2, app.sep3))

      ❶ if results_1 == 'Not Found' and results_2 == 'Not Found':
            print("\nNew Target Probabilities (P) for Search {}:"
                  .format(search_num + 1))
            print("P1 = {:.3f}, P2 = {:.3f}, P3 = {:.3f}"
                  .format(app.p1, app.p2, app.p3))
        else:
            cv.circle(app.img, (sailor_x, sailor_y), 3, (255, 0, 0), -1)
          ❷ cv.imshow('Search Area', app.img)
            cv.waitKey(1500)
            main()
        search_num += 1

if __name__ == '__main__':
    main()

调用revise_target_probs()方法,对于给定搜索结果,应用贝叶斯法则,重新计算船员在每个搜索区域的概率。接下来,在shell中显示搜索结果和搜索有效性概率。

如果两个搜索队的结果都是未成功,则显示更新的目标概率,玩家将利用它来指导他们的下一次搜索❶;否则,在地图上显示船员的位置。使用OpenCV绘制一个圆,并向该方法传入基本地图图像、船员的元组的中心点、半径(以像素数表示)、颜色、宽度值(−1)。宽度值为负将使圆圈充满该颜色。

通过使用类似于代码清单 1-3 的代码显示基本地图来完成main()❷。在游戏调用main()函数并自动复位之前,向waitKey()方法传入1500,显示船员的实际位置1.5秒。在循环结束时,将搜索次数变量递增1。用户需要在循环结束后这样做,这样无效的选择就不会被算作搜索。

回到全局空间,利用一些代码,让程序作为模块导入或以独立模式运行。__name__变量是一个内置变量,用于评估程序是自主运行还是被导入另一个程序中。如果直接运行这个程序,则将__name__设置为__main__,满足if语句的条件,main()就会自动调用。如果程序被导入,则main()函数不会被运行,除非有意调用。

玩游戏时,在文本编辑器中选择Run►Run Module,或者直接按F5键。图1-7和图1-8是最终的游戏画面,即第一次搜索成功的结果。

图1-7 搜索结果成功的Python解释器窗口

在这次示例搜索中,玩家选择将两次搜索都投入区域2,该区域有50%的初始概率包含船员。第一次搜索不成功,但第二次搜索找到了船员。注意,搜索有效性只比50%略好。这意味着在第一次搜索中找到船员的概率只有1/4(0.5×0.521≈0.260)。虽然选择很明智,但玩家最后还是要靠一点运气!

在玩游戏时,玩家可试着让自己沉浸在这个场景中。玩家的决策决定了一个人的生死,而玩家的时间并不多。如果船员漂浮在水上,玩家只有3次猜测机会来取得成功。请聪明地利用机会!

根据游戏开始时的目标概率,船员最有可能在区域2,其次是区域3。因此,一个好的初始策略是搜索区域2两次(菜单选项2)或者同时搜索区域2和3(菜单选项6)。你需要密切关注搜索效果的输出。如果一个区域获得了很高的搜索有效性分数,这就意味着它已经被彻底搜索过了。在游戏剩下的时间里,你可能要把精力集中在其他地方。

图1-8 搜索结果成功的基本地图图像

下面的输出代表了你作为一个决策者,可能遇到的糟糕的情况之一。

Search 2 Results 1 = Not Found
Search 2 Results 2 = Not Found

Search 2 Effectiveness (E):
E1 = 0.000, E2 = 0.234, E3 = 0.610

New Target Probabilities (P) for Search 3:
P1 = 0.382, P2 = 0.395, P3 = 0.223

搜索两次之后,只剩下一次搜索,目标概率非常相似,这对下一步的搜索方向没有什么指导意义。在这种情况下,最好的办法是将搜索分到两个区域,并希望有最好的结果。

按照初始概率的顺序盲目地搜索几个区域,在区域2上加倍搜索,然后在区域3上加倍搜索,然后在区域1上加倍搜索。然后试着虔诚地遵从贝叶斯的结果,总是在当前目标概率最高的区域加倍搜索。接下来,尝试在两个最高概率的区域分头搜索。在那之后,根据自己的直觉,在你觉得合适的时候推翻贝叶斯。可以想象,搜索区域多了,搜索天数多了,人类的直觉很快就受不了了。

在本章中,我们学习了贝叶斯法则。贝叶斯法则是一个简单的统计定理,在现代世界中被广泛应用。我们编写了一个程序,以估计搜索有效性的形式,利用贝叶斯法则来获取新的信息并更新在每个搜索区域找到失事船员的概率。

我们也加载并使用了多个科学软件包,如NumPy和OpenCV,我们在本书中应用了这些软件包。我们还应用了Python标准库中有用的itertools、sys和random模块。

Sharon Bertsch McGrayne所著的The Theory That Would Not Die: How Bayes' Rule Cracked the Enigma Code, Hunted Down Russian Submarines, and Emerged Triumphant from Two Centuries of Controversy(Yale University Press, 2011)叙述了贝叶斯法则的发现和争议历史。

NumPy的主要文档来源参见SciPy官方文档。

目前,bayes.py程序将搜索区域内的所有坐标放入一个列表中,并随机打乱顺序。随后在同一区域内的搜索可能导致重复之前的轨迹。从现实生活的角度来看,这不一定是坏事,因为船员会一直漂流,但总体来说,最好尽可能多地覆盖区域而不重复。

复制并编辑程序,跟踪一个区域内哪些坐标已经搜索过,并将它们从未来的搜索中排除(直到再次调用main(),要么因为玩家找到了船员,要么因为选择菜单选项7重新开始)。测试两个版本的游戏,看看改动是否对结果有明显影响。

蒙特卡洛模拟(Monte Carlo Simulation,MCS)使用重复的随机抽样,在指定范围的条件下,预测不同的结果。创建一个bayes.py的版本,它可以自动选择菜单项并跟踪结果,让玩家确定最成功的搜索策略。例如,让程序根据最高的贝叶斯目标概率选择菜单项1、2或3,然后记录找到船员时的搜索次数;重复这个过程10000次,取所有搜索次数的平均值;然后再次循环,根据最高综合目标概率,选择菜单项4、5或6;比较最后的平均值,是在一个区域内加倍搜索好,还是在两个区域内分开搜索好?

在现实生活中的搜救行动中,你会在搜索之前对每个地区的预期搜索有效性概率进行估计。这个预期概率或计划概率主要根据天气报告来确定。例如,大雾可能会覆盖一个搜索区域,而另外两个区域则晴空万里。

将目标概率乘以计划SEP,就得到了一个区域的检测概率(Probability of Detection,PoD)。PoD是给定所有已知误差和噪声源的情况下,一个目标被检测到的概率。

编写一个bayes.py版本,其中包括为每个搜索区域随机生成的计划SEP。将每个区域的目标概率(如self.p1self.p2self.p3)乘以这些新变量,产生该区域的PoD。例如,如果区域3的贝叶斯目标概率是0.90,但计划SEP只有0.1,那么检测的概率是0.09。

在shell中向玩家显示每个区域的目标概率、计划的SEP和PoD,如下所示。玩家可以利用这些信息,指导他们从搜索菜单中进行选择。

Actual  Search  1  Effectiveness  (E): 
E1 = 0.190, E2 = 0.000, E3 = 0.000

New Planned Search Effectiveness and Target Probabilities (P) for Search  2: 
E1 = 0.509, E2 = 0.826, E3 = 0.686
P1 = 0.168, P2 = 0.520, P3 = 0.312

Search 2

    Choose next areas to search:

    0 - Quit

    1 - Search Area 1 twice 
      Probability of detection: 0.164

    2 - Search Area 2 twice 
      Probability of detection: 0.674

    3 - Search Area 3 twice 
      Probability of detection: 0.382

    4 - Search Areas 1 & 2 
      Probability of detection: 0.515

    5 - Search Areas 1 & 3 
      Probability  of  detection:  0.3

    6 - Search Areas 2 & 3 
      Probability of detection: 0.643

    7 - Start Over 

Choice:

当两次搜索同一区域时,要合并PoD,可使用以下公式:

1−(1−PoD)2

否则,只需要将概率相加。

计算一个地区的实际SEP时,要将它限制在预期值的范围内,这考虑到了只提前一天的天气报告的总体准确性。用一个围绕计划SEP值建立的分布(如三角分布)来代替random.uniform()方法。关于可用分布类型的列表,可参见Python官方文档的“random—Generate pseudo-random numbers”页面。当然,未搜索区域的实际SEP永远是0。

纳入计划SEP对游戏有什么影响?是更容易赢还是更难赢呢?是否更难把握贝叶斯法则的应用方式?如果你负责一次真实的搜索,会如何处理一个目标概率很高,但由于海面波涛汹涌,计划SEP很低的区域?你会进行搜索还是取消搜索,或者是将搜索转移到一个目标概率低但天气较好的区域?

微信扫码关注【异步社区】微信公众号,回复“e58711”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


相关图书

深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
动手学自然语言处理
动手学自然语言处理
Web应用安全
Web应用安全
Python高性能编程(第2版)
Python高性能编程(第2版)
图像处理与计算机视觉实践——基于OpenCV和Python
图像处理与计算机视觉实践——基于OpenCV和Python
Python数据科学实战
Python数据科学实战

相关文章

相关课程