IPython-交互式计算和可视化秘籍第二版-全-
IPython 交互式计算和可视化秘籍第二版(全)
原文:
annas-archive.org/md5/62516af4e05f6a3b0523d8aa07fd5acb
译者:飞龙
前言
我们正被来自科学研究、工程学、经济学、政治学、新闻学、商业等多个领域的数字数据洪流所淹没。因此,分析、可视化和利用数据成为了越来越多人的职业。编程、数值计算、数学、统计学和数据挖掘等量化技能构成了数据科学的核心,并在看似无尽的各个领域中愈加受到重视。
我之前的书籍,《Learning IPython for Interactive Computing and Data Visualization》,Packt Publishing,2013 年出版,是一本面向初学者的数据科学与 Python 数值计算入门书籍。这种广泛使用的编程语言也是这些学科最受欢迎的平台之一。
本书继续了这一旅程,展示了 100 多个高级数据科学和数学建模的案例。这些案例不仅涵盖了编程和计算主题,如交互式计算、数值计算、高性能计算、并行计算和交互式可视化,还包括数据分析主题,如统计学、数据挖掘、机器学习、信号处理等多个领域。
本书中的所有代码均在 IPython 笔记本中编写。IPython 是 Python 数据分析平台的核心。最初为了增强默认的 Python 控制台,IPython 现在以其广受好评的笔记本而闻名。这个基于 Web 的交互式计算环境将代码、丰富的文本、图片、数学方程式和图表整合成一个文档。它是进行 Python 数据分析和高性能数值计算的理想入口。
本书是什么
本书包含超过一百个专注的实用案例,回答了在 IPython 上进行数值计算和数据分析的具体问题,涉及内容包括:
-
如何使用 pandas、PyMC 和 SciPy 探索公共数据集
-
如何在 IPython 笔记本中创建交互式图表、小部件和图形用户界面
-
如何创建具有自定义魔法命令的可配置 IPython 扩展
-
如何在 IPython 中并行分配异步任务
-
如何使用 OpenMP、MPI、Numba、Cython、OpenCL、CUDA 和 Julia 编程语言加速代码
-
如何从数据集中估计概率密度
-
如何在笔记本中开始使用 R 统计编程语言
-
如何使用 scikit-learn 训练分类器或回归器
-
如何在高维数据集中找到有趣的投影
-
如何在图像中检测人脸
-
如何模拟反应-扩散系统
-
如何在路网中计算行程
本书的选择是介绍各种不同的主题,而不是深入探讨少数几种方法。目标是让你了解 Python 在数据科学领域令人难以置信的丰富能力。所有方法都在多种真实世界的例子上应用。
本书的每个配方不仅演示了如何应用某个方法,还解释了它如何以及为什么有效。理解这些方法背后的数学概念和思想,而不仅仅是盲目应用它们,是非常重要的。
此外,每个配方都附有许多参考资料,供有兴趣的读者深入了解。由于在线参考资料经常变动,它们将会在本书的网站上保持更新(ipython-books.github.io
)。
本书涵盖的内容
本书分为两部分:
第一部分(第 1 到 6 章)涵盖了交互式数值计算、高性能计算和数据可视化中的高级方法。
第二部分(第 7 到 15 章)介绍了数据科学和数学建模中的标准方法。所有这些方法都应用于真实世界的数据。
第一部分 – 高性能交互式计算
第一章,使用 IPython 进行交互式计算简介,简要而深刻地介绍了如何使用 IPython 进行数据分析和数值计算。它不仅涵盖了 Python、NumPy、pandas 和 matplotlib 等常用包,还涉及了 IPython 的高级主题,如笔记本中的交互式小部件、自定义魔法命令、可配置的 IPython 扩展以及新的语言内核。
第二章,交互式计算中的最佳实践,详细讲解了编写可重复、高质量代码的最佳实践:任务自动化、使用 Git 进行版本控制、与 IPython 的工作流、使用 nose 进行单元测试、持续集成、调试以及其他相关主题。这些主题在计算研究和数据分析中的重要性不言而喻。
第三章,精通笔记本,涵盖了与 IPython 笔记本相关的高级主题,特别是笔记本格式、笔记本转换和 CSS/JavaScript 定制。自 IPython 2.0 以来推出的新交互式小部件也得到了广泛的介绍。这些技术使得在笔记本中进行数据分析比以往更加互动。
第四章,性能分析与优化,介绍了使代码更快、更高效的方法:Python 中的 CPU 和内存性能分析,NumPy 的高级优化技术(包括大数组操作),以及使用 HDF5 文件格式和 PyTables 库进行巨型数组的内存映射。这些技术对于大数据分析至关重要。
第五章,高性能计算,介绍了一些使代码更高效的高级技术:使用 Numba 和 Cython 加速代码、通过 ctypes 将 C 库封装到 Python 中、使用 IPython、OpenMP 和 MPI 进行并行计算,以及使用 CUDA 和 OpenCL 进行图形处理单元(GPGPU)的通用计算。章节最后介绍了近年来的 Julia 语言,这种语言专为高性能数值计算而设计,并且可以在 IPython 笔记本中轻松使用。
第六章,高级可视化,介绍了一些在样式或编程接口方面超越 matplotlib 的数据可视化库。它还涵盖了在笔记本中使用 Bokeh、mpld3 和 D3.js 进行交互式可视化。章节最后介绍了 Vispy,一个利用图形处理单元(GPU)强大计算能力进行大数据高性能交互式可视化的库。
第二部分 – 数据科学与应用数学的标准方法
第七章,统计数据分析,介绍了获得数据洞察力的方法。它介绍了经典的频率学派和贝叶斯方法,用于假设检验、参数估计和非参数估计、模型推断等。章节中利用了 Python 的 pandas、SciPy、statsmodels 和 PyMC 等库。最后一个示例介绍了统计语言 R,可以轻松地在 IPython 笔记本中使用。
第八章,机器学习,讲解了从数据中学习和做出预测的方法。本章使用 scikit-learn Python 包,阐释了数据挖掘和机器学习中的基本概念,如监督学习和无监督学习、分类、回归、特征选择、特征提取、过拟合、正则化、交叉验证和网格搜索。本章涉及的算法包括逻辑回归、朴素贝叶斯、K 近邻、支持向量机、随机森林等。这些方法被应用到不同类型的数据集:数值数据、图像和文本。
第九章,数值优化,主要讲解如何最小化或最大化数学函数。这个话题在数据科学中无处不在,尤其是在统计学、机器学习和信号处理领域。本章通过 SciPy 展示了一些根求解、最小化和曲线拟合的常用方法。
第十章,信号处理,讲解如何从复杂且嘈杂的数据中提取相关信息。在运行统计和数据挖掘算法之前,有时需要先进行这些步骤。本章介绍了标准的信号处理方法,如傅里叶变换和数字滤波器。
第十一章,图像与音频处理,涵盖了图像和声音的信号处理方法。它介绍了图像过滤、分割、计算机视觉和人脸检测,使用 scikit-image 和 OpenCV。还介绍了音频处理和合成的方法。
第十二章,确定性动力学系统,描述了特定数据类型背后的动态过程。它演示了离散时间动力学系统的模拟技术,以及常微分方程和偏微分方程的模拟方法。
第十三章,随机动力学系统,描述了特定数据类型背后的动态随机过程。它演示了离散时间马尔可夫链、点过程和随机微分方程的模拟技术。
第十四章,图形、几何与地理信息系统,涵盖了图形、社交网络、道路网络、地图和地理数据的分析与可视化方法。
第十五章,符号与数值数学,介绍了 SymPy,这是一种将符号计算引入 Python 的计算机代数系统。本章最后介绍了 Sage,这是另一种基于 Python 的计算数学系统。
本书所需内容
你需要了解本书前一篇的内容,《学习 IPython 进行交互式计算与数据可视化》:Python 编程,IPython 控制台与笔记本,使用 NumPy 进行数值计算,利用 pandas 进行基本数据分析,以及使用 matplotlib 绘图。本书涉及高级科学编程主题,你需要熟悉科学 Python 生态系统。
在第二部分,你需要掌握微积分、线性代数和概率论的基础知识。这些章节介绍了数据科学和应用数学中的不同主题(统计学、机器学习、数值优化、信号处理、动力学系统、图论等)。如果你了解基本概念,如实值函数、积分、矩阵、向量空间、概率等,你将更好地理解这些内容。
安装 Python
安装 Python 有多种方式。我们强烈推荐免费的 Anaconda 发行版(store.continuum.io/cshop/anaconda/
)。这个 Python 发行版包含了本书中大部分将要使用的包。它还包括一个强大的打包系统,名为 conda。书籍网站提供了安装 Anaconda 和运行代码示例的所有说明。你应该学习如何安装包(conda install packagename
)以及如何使用 conda 创建多个 Python 环境。
本书的代码是为 Python 3 编写的(更准确地说,代码已在 Python 3.4.1、Anaconda 2.0.1、Windows 8.1 64 位环境下进行测试,尽管它在 Linux 和 Mac OS X 上也可以运行),但它同样适用于 Python 2.7。我们会在需要时提到任何兼容性问题。由于 NumPy 在大多数情况下都承担了繁重的计算任务,所以这些问题在本书中很少出现。NumPy 的接口在 Python 2 和 Python 3 之间没有变化。
如果你不确定应该使用哪个 Python 版本,选择 Python 3。只有在你确实需要的情况下才选择 Python 2(例如,如果你必须使用一个不支持 Python 3 的 Python 包,或者你的部分用户群体仍然使用 Python 2)。我们在第二章《交互式计算最佳实践》中详细讨论了这个问题。
使用 Anaconda 时,你可以通过 conda 环境同时安装 Python 2 和 Python 3。这样你就可以轻松运行本书中需要 Python 2 的几个食谱。
GitHub 仓库
本书配有主页和两个 GitHub 仓库:
-
主要 GitHub 仓库,包含所有食谱的代码和参考资料:
github.com/ipython-books/cookbook-code
-
本书某些食谱使用的数据集:
github.com/ipython-books/cookbook-data
主要 GitHub 仓库是你可以找到的地方:
-
查找所有的代码示例,格式为 IPython 笔记本
-
查找所有最新的参考文献
-
查找最新的安装说明
-
通过问题跟踪器报告勘误、不准确或错误
-
通过 Pull Requests 提出修复建议
-
通过 Pull Requests 添加笔记、评论或进一步的参考资料
-
通过 Pull Requests 添加新食谱
在线参考文献列表是一个特别重要的资源。它包含了许多关于本书涵盖主题的教程、课程、书籍和视频链接。
你也可以通过我的网站(cyrille.rossant.net
)和我的 Twitter 账号(@cyrillerossant
)跟进本书的最新动态。
本书适合谁阅读
本书面向学生、研究人员、教师、工程师、数据科学家、分析师、记者、经济学家以及对数据分析和数值计算感兴趣的爱好者。
对于熟悉科学 Python 生态系统的读者,会发现许多资源可以帮助他们在 IPython 中进行高性能交互式计算时提升技能。
需要为领域特定应用实现算法的读者会喜欢这本书中对数据分析和应用数学各种主题的介绍。
对于刚接触 Python 数值计算的读者,应该先从本书的前传《Learning IPython for Interactive Computing and Data Visualization》入手,作者是Cyrille Rossant,由Packt Publishing出版,2013 年。第二版计划于 2015 年发布。
约定
本书中包含了多种文本样式,用以区分不同类型的信息。以下是这些样式的示例及其含义解释。
文字中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名如下所示:“笔记本可以通过%run notebook.ipynb
在交互式会话中运行。”
代码块如下所示:
def do_complete(self, code, cursor_pos):
return {'status': 'ok',
'cursor_start': ...,
'cursor_end': ...,
'matches': [...]}
所有命令行输入或输出如下所示:
from IPython import embed
embed()
新术语和重要词汇以粗体显示。例如,在屏幕上、菜单或对话框中的词语,通常呈现为这样:“最简单的选项是从笔记本仪表板中的Clusters选项卡启动它们。”
注意
警告或重要提示会以这种框的形式呈现。
提示
提示和技巧以这种方式呈现。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对本书的看法——您喜欢或不喜欢的内容。读者的反馈对我们开发更有价值的书籍至关重要,帮助我们提供真正对您有帮助的书籍。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>
,并在邮件主题中提及书名。
如果您在某个主题领域具有专业知识,并且有兴趣撰写或参与书籍创作,请参考我们的作者指南,网址为:www.packtpub.com/authors。
客户支持
现在您已经是 Packt 书籍的自豪拥有者,我们有许多资源帮助您最大限度地利用您的购买。
下载示例代码
您可以从自己账户的www.packtpub.com
下载所有购买的 Packt 书籍的示例代码文件。如果您从其他地方购买本书,可以访问www.packtpub.com/support
并注册,文件将直接通过电子邮件发送给您。
下载彩色图片
我们还为您提供了一份包含本书中截图/图表彩色图片的 PDF 文件。彩色图片有助于您更好地理解输出的变化。您可以通过以下链接下载该文件:www.packtpub.com/sites/default/files/downloads/4818OS_ColoredImages.pdf
。
勘误
虽然我们已尽一切努力确保内容的准确性,但错误仍然会发生。如果您在我们的书籍中发现错误——可能是文本或代码的错误——我们将非常感激您能将其报告给我们。通过这样做,您不仅能为其他读者避免困扰,还能帮助我们改进书籍的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata
进行报告,选择您的书籍,点击勘误 提交 表单链接,并填写勘误详细信息。您的勘误一旦验证通过,我们将接受您的提交,并将勘误上传到我们的网站,或者添加到该书籍现有勘误列表中的勘误部分。任何现有勘误可以通过访问www.packtpub.com/support
并选择您的书名来查看。
盗版
网络上盗版版权材料的问题在所有媒体中都很普遍。在 Packt,我们非常重视对版权和许可的保护。如果您在互联网上遇到任何形式的非法复制品,请立即提供相关地址或网站名称,以便我们采取相应措施。
如发现疑似盗版内容,请通过<copyright@packtpub.com>
与我们联系,并提供相关链接。
感谢您的帮助,保护我们的作者权益,使我们能够为您提供有价值的内容。
问题
如果您在书籍的任何部分遇到问题,可以通过<questions@packtpub.com>
与我们联系,我们将尽力解决。
第一章:IPython 互动计算概述
在本章中,我们将讨论以下主题:
-
介绍 IPython 笔记本
-
开始在 IPython 中进行探索性数据分析
-
介绍 NumPy 中的多维数组以进行快速数组计算
-
创建带有自定义魔法命令的 IPython 扩展
-
掌握 IPython 的配置系统
-
为 IPython 创建一个简单的内核
引言
本书面向熟悉 Python、IPython 和科学计算的中高级用户。在本章中,我们将简要回顾一下本书中将使用的基础工具:IPython、笔记本、pandas、NumPy 和 matplotlib。
在本章节中,我们将对 IPython 和 Python 科学计算栈进行广泛的概述,以支持高性能计算和数据科学。
什么是 IPython?
IPython是一个开源平台,用于互动和并行计算。它提供了强大的交互式命令行和基于浏览器的笔记本。笔记本将代码、文本、数学表达式、内联图形、交互式图表及其他丰富的媒体内容结合在一个可共享的 Web 文档中。该平台为互动科学计算和数据分析提供了理想的框架,IPython 已成为研究人员、数据科学家和教师的必备工具。
IPython 可以与 Python 编程语言一起使用,但该平台也支持其他多种语言,如 R、Julia、Haskell 或 Ruby。该项目的架构确实是与语言无关的,包含了消息协议和交互式客户端(包括基于浏览器的笔记本)。这些客户端与内核相连,后者实现了核心的交互式计算功能。因此,该平台对于使用 Python 以外语言的技术和科学社区也非常有用。
2014 年 7 月,Jupyter 项目由 IPython 开发者宣布。该项目将专注于 IPython 中与语言无关的部分(包括笔记本架构),而 IPython 这个名称将保留用于 Python 内核。在本书中,为了简便起见,我们将使用 IPython 这个术语来指代平台或 Python 内核。
Python 作为科学计算环境的简要历史回顾
Python 是一种高级通用编程语言,由 Guido van Rossum 于 1980 年代末期构思(其名称灵感来源于英国喜剧Monty Python's Flying Circus)。这种易于使用的语言是许多脚本程序的基础,这些程序将不同的软件组件(胶水语言)连接在一起。此外,Python 还自带一个极其丰富的标准库(内置电池理念),涵盖了字符串处理、互联网协议、操作系统接口等多个领域。
在 1990 年代末,Travis Oliphant 和其他人开始构建用于处理 Python 中的数值数据的高效工具:Numeric、Numarray,最终是NumPy。SciPy,实现了许多数值计算算法,也是建立在 NumPy 之上的。在 2000 年代初,John Hunter 创建了matplotlib,将科学图形带到了 Python。与此同时,Fernando Perez 创建了 IPython,以提高 Python 中的交互性和生产力。所有基本工具都在这里,将 Python 打造成一个出色的开源高性能框架,用于科学计算和数据分析。
注
值得注意的是,Python 作为科学计算平台是在原本不是为此目的设计的编程语言的基础上逐步构建起来的。这个事实可能解释了平台的一些细微不一致或弱点,但这并不妨碍它成为当今最受欢迎的科学计算开源框架之一。(您也可以参考cyrille.rossant.net/whats-wrong-with-scientific-python/
。)
用于数值计算和数据分析的值得注意的竞争性开源平台包括 R(专注于统计)和 Julia(一个年轻的、高级的语言,专注于高性能和并行计算)。我们将在本书中简要介绍这两种语言,因为它们可以从 IPython 笔记本中使用。
在 2000 年代末,Wes McKinney 创建了用于操作和分析数值表和时间序列的pandas。与此同时,IPython 开发人员开始着手开发受数学软件如Sage、Maple和Mathematica启发的笔记本客户端。最终,2011 年 12 月发布的 IPython 0.12 引入了现在已经流行的基于 HTML 的笔记本。
2013 年,IPython 团队获得了 Sloan 基金会的资助和微软的捐赠,以支持笔记本的开发。2014 年初发布的 IPython 2.0 带来了许多改进和期待已久的功能。
IPython 2.0 有什么新功能?
这里是 IPython 2.0(接替 v1.1)带来的变化的简要总结:
-
笔记本带有一个新的模态用户界面:
-
在编辑模式下,我们可以通过输入代码或文本来编辑单元格。
-
在命令模式下,我们可以通过移动单元格、复制或删除它们、更改它们的类型等来编辑笔记本。在这种模式下,键盘映射到一组快捷键,让我们能够高效地执行笔记本和单元格操作。
-
-
笔记本小部件是基于 JavaScript 的 GUI 小部件,可以与 Python 对象动态交互。这一重要功能极大地扩展了 IPython 笔记本的可能性。在笔记本中编写 Python 代码不再是与内核唯一可能的交互方式。JavaScript 小部件,更一般地说,任何基于 JavaScript 的交互元素,现在都可以实时与内核交互。
-
现在,我们可以通过仪表板在不同的子文件夹中打开笔记本,并使用相同的服务器。一个 REST API 将本地 URI 映射到文件系统。
-
现在,笔记本已被签名,以防止在打开笔记本时执行不受信任的代码。
-
仪表板现在包含一个正在运行的标签,显示运行中的内核列表。
-
提示框现在在按下Shift + Tab时显示,而不是Tab。
-
可以通过
%run notebook.ipynb
在交互式会话中运行笔记本。 -
不推荐使用
%pylab
魔法命令,建议使用%matplotlib inline
(将图形嵌入到笔记本中)和import matplotlib.pyplot as plt
。主要原因是%pylab
通过导入大量变量使交互命名空间变得杂乱无章。此外,它可能会影响笔记本的可重复性和可重用性。 -
Python 2.6 和 3.2 不再支持。IPython 现在需要 Python 2.7 或>=3.3。
IPython 3.0 和 4.0 的路线图
计划于 2014 年底/2015 年初发布的 IPython 3.0 和 4.0 应该有助于使用非 Python 内核,并为笔记本提供多用户功能。
参考资料
以下是一些参考资料:
-
Python 官方网站:www.python.org
-
Wikipedia 上的 Python 条目:
en.wikipedia.org/wiki/Python_%28programming_language%29
-
Python 的标准库:
docs.python.org/2/library/
-
Guido van Rossum 的 Wikipedia 条目:
en.wikipedia.org/wiki/Guido_van_Rossum
-
Guido van Rossum 谈论 Python 的诞生:www.artima.com/intv/pythonP.html
-
关于科学 Python 的历史:
fr.slideshare.net/shoheihido/sci-pyhistory
-
IPython 2.0 的新特性:
ipython.org/ipython-doc/2/whatsnew/version2.0.html
-
IPython 的 Wikipedia 条目:
en.wikipedia.org/wiki/IPython
-
IPython 笔记本的历史:
blog.fperez.org/2012/01/ipython-notebook-historical.html
介绍 IPython 笔记本
笔记本是 IPython 的旗舰功能。这个基于 Web 的交互式环境将代码、富文本、图像、视频、动画、数学公式和图表集成到一个文档中。这个现代化的工具是 Python 中高性能数值计算和数据科学的理想门户。整本书都是在笔记本中编写的,每个教程的代码都可以在本书的 GitHub 仓库中的笔记本里找到,地址是:github.com/ipython-books/cookbook-code
。
在本教程中,我们介绍了 IPython 及其笔记本。在准备工作中,我们还给出了有关安装 IPython 和 Python 科学计算栈的一般说明。
准备工作
本章中你将需要 Python、IPython、NumPy、pandas 和 matplotlib。与 SciPy 和 SymPy 一起,这些库构成了 Python 科学计算栈的核心(www.scipy.org/about.html)。
注意
你可以在本书的 GitHub 仓库 github.com/ipython-books/cookbook-code
上找到完整的详细安装说明。
我们这里只给出这些说明的总结;请参考上面的链接以获取更详细的更新信息。
如果你刚开始接触 Python 中的科学计算,最简单的选择是安装一个一体化的 Python 发行版。最常见的发行版有:
-
Anaconda(免费或商业许可)可在
store.continuum.io/cshop/anaconda/
获取。 -
Canopy(免费或商业许可)可在 www.enthought.com/products/canopy/ 获取。
-
Python(x,y),仅限 Windows 的免费发行版,可在
code.google.com/p/pythonxy/
获取。
我们强烈推荐使用 Anaconda。这些发行版包含了你开始所需的一切。你还可以根据需要安装其他包。你可以在之前提到的链接中找到所有安装说明。
注意
本书假设你已经安装了 Anaconda。如果你使用其他发行版,可能无法获得我们的支持。
另外,如果你敢于挑战,可以手动安装 Python、IPython、NumPy、pandas 和 matplotlib。你可以在以下网站找到所有安装说明:
-
Python 是支撑整个生态系统的编程语言。安装说明可在 www.python.org/ 找到。
-
IPython 提供用于 Python 的交互式计算工具。安装说明可在
ipython.org/install.html
找到。 -
NumPy/SciPy 用于 Python 中的数值计算。安装说明可在 www.scipy.org/install.html 找到。
-
pandas 提供数据结构和数据分析工具。安装说明可在
pandas.pydata.org/getpandas.html
找到。 -
matplotlib 帮助在 Python 中创建科学图形。安装说明可在
matplotlib.org/index.html
找到。
注意
Python 2 还是 Python 3?
尽管 Python 3 是目前最新版本,但许多人仍在使用 Python 2。Python 3 引入了一些不兼容的变更,这些变更减缓了它的普及。如果你刚刚开始进行科学计算,完全可以选择 Python 3。本书中的所有代码都是为 Python 3 编写的,但也可以在 Python 2 中运行。我们将在第二章中详细介绍这个问题,交互式计算中的最佳实践。
一旦你安装了一个全功能的 Python 发行版(我们强烈推荐使用 Anaconda)或 Python 和所需的包,你就可以开始了!本书中的大多数示例都使用了 IPython 笔记本工具。这个工具让你能够通过浏览器访问 Python。我们在《学习 IPython 进行交互式计算与数据可视化》一书中介绍了笔记本的基础内容。你也可以在 IPython 的官网找到更多信息(ipython.org/ipython-doc/stable/notebook/index.html
)。
要运行 IPython 笔记本服务器,请在终端(也叫命令提示符)中输入 ipython notebook
。默认情况下,您的浏览器会自动打开,并加载 127.0.0.1:8888
地址。然后,你可以在仪表盘中创建一个新的笔记本,或者打开一个已有的笔记本。默认情况下,笔记本服务器会在当前目录下启动(即你执行命令的目录)。它会列出该目录下所有的笔记本(文件扩展名为 .ipynb
)。
注意
在 Windows 上,你可以通过按下 Windows 键和 R,然后在提示符中输入 cmd
,最后按 Enter 打开命令提示符。
如何做...
-
我们假设已经安装了带有 IPython 的 Python 发行版,并且现在处于 IPython 笔记本中。我们在一个单元格中输入以下命令,并按 Shift + Enter 进行计算:
In [1]: print("Hello world!") Hello world!
IPython 笔记本的截图
笔记本包含一系列线性的单元格和输出区域。一个单元格中包含 Python 代码,可能有一行或多行。代码的输出显示在对应的输出区域中。
-
现在,我们进行一个简单的算术操作:
In [2]: 2+2 Out[2]: 4
操作的结果会显示在输出区域。让我们更具体地说明一下。输出区域不仅显示单元格中任何命令打印的文本,还会显示最后返回对象的文本表示。这里,最后返回的对象是
2+2
的结果,也就是4
。 -
在下一个单元格中,我们可以通过
_
(下划线)特殊变量来恢复上一个返回对象的值。实际上,将对象赋值给命名变量(如myresult = 2+2
)可能更为方便。In [3]: _ * 3 Out[3]: 12
-
IPython 不仅接受 Python 代码,还接受 shell 命令。这些命令由操作系统定义(主要是 Windows、Linux 和 Mac OS X)。我们需要在单元格中输入
!
,然后输入 shell 命令。在这里,假设是 Linux 或 Mac OS X 系统,我们可以获得当前目录中所有笔记本的列表:In [4]: !ls *.ipynb notebook1.ipynb ...
在 Windows 上,你应将
ls
替换为dir
。 -
IPython 附带了一些魔法命令库。这些命令是常见操作的便捷快捷方式,所有魔法命令都以
%
(百分号字符)开头。我们可以通过%lsmagic
获取所有魔法命令的列表:In [5]: %lsmagic Out[5]: Available line magics: %alias %alias_magic %autocall %automagic %autosave %bookmark %cd %clear %cls %colors %config %connect_info %copy %ddir %debug %dhist %dirs %doctest_mode %echo %ed %edit %env %gui %hist %history %install_default_config %install_ext %install_profiles %killbgscripts %ldir %less %load %load_ext %loadpy %logoff %logon %logstart %logstate %logstop %ls %lsmagic %macro %magic %matplotlib %mkdir %more %notebook %page %pastebin %pdb %pdef %pdoc %pfile %pinfo %pinfo2 %popd %pprint %precision %profile %prun %psearch %psource %pushd %pwd %pycat %pylab %qtconsole %quickref %recall %rehashx %reload_ext %ren %rep %rerun %reset %reset_selective %rmdir %run %save %sc %store %sx %system %tb %time %timeit %unalias %unload_ext %who %who_ls %whos %xdel %xmode Available cell magics: %%! %%HTML %%SVG %%bash %%capture %%cmd %%debug %%file %%html %%javascript %%latex %%perl %%powershell %%prun %%pypy %%python %%python3 %%ruby %%script %%sh %%svg %%sx %%system %%time %%timeit %%writefile
单元魔法命令以
%%
为前缀;它们涉及整个代码单元。 -
例如,
%%writefile
单元魔法命令可以让我们轻松创建文本文件。此魔法命令接受文件名作为参数。单元中的所有剩余行都将直接写入该文本文件。在这里,我们创建一个文件test.txt
并写入Hello world!
:In [6]: %%writefile test.txt Hello world! Writing test.txt In [7]: # Let's check what this file contains. with open('test.txt', 'r') as f: print(f.read()) Hello world!
-
正如我们在
%lsmagic
的输出中看到的,IPython 中有许多魔法命令。我们可以通过在命令后添加?
来获取有关任何命令的更多信息。例如,要获取有关%run
魔法命令的帮助,我们可以输入%run?
,如下所示:In [9]: %run? Type: Magic function Namespace: IPython internal ... Docstring: Run the named file inside IPython as a program. [full documentation of the magic command...]
-
我们已经介绍了 IPython 和笔记本的基础知识。现在让我们转向笔记本的丰富显示和交互功能。到目前为止,我们只创建了代码单元(包含代码)。IPython 支持其他类型的单元。在笔记本工具栏中,有一个下拉菜单可以选择单元的类型。在代码单元之后,最常见的单元类型是Markdown 单元。
Markdown 单元包含用Markdown格式化的丰富文本,这是一种流行的纯文本格式化语法。该格式支持普通文本、标题、粗体、斜体、超文本链接、图像、LaTeX(一种为数学适配的排版系统)中的数学方程式、代码、HTML 元素以及其他功能,如下所示:
### New paragraph This is *rich* **text** with [links](http://ipython.org), equations: $$\hat{f}(\xi) = \int_{-\infty}^{+\infty} f(x)\, \mathrm{e}^{-i \xi x}$$ code with syntax highlighting: ```python print("Hello world!") ```py and images: 
运行一个 Markdown 单元(例如,按 Shift + Enter)将显示输出,如下图所示:
在 IPython 笔记本中使用 Markdown 进行丰富文本格式化
提示
LaTeX 方程式使用
MathJax
库渲染。我们可以使用$...$
输入内联方程式,使用$$...$$
输入显示的方程式。我们还可以使用如equation
、eqnarray
或align
等环境。这些功能对于科学用户非常有用。通过结合代码单元和 Markdown 单元,我们可以创建一个独立的交互式文档,结合计算(代码)、文本和图形。
-
IPython 还带有一个复杂的显示系统,允许我们在笔记本中插入丰富的网页元素。在这里,我们展示如何在笔记本中添加 HTML、SVG(可缩放矢量图形)甚至 YouTube 视频。
首先,我们需要导入一些类:
In [11]: from IPython.display import HTML, SVG, YouTubeVideo
我们使用 Python 动态创建 HTML 表格,并将其显示在笔记本中:
In [12]: HTML(''' <table style="border: 2px solid black;"> ''' + ''.join(['<tr>' + ''.join(['<td>{row},{col}</td>'.format( row=row, col=col ) for col in range(5)]) + '</tr>' for row in range(5)]) + ''' </table> ''')
笔记本中的 HTML 表格
类似地,我们可以动态创建 SVG 图形:
In [13]: SVG('''<svg width="600" height="80">''' + ''.join(['''<circle cx="{x}" cy="{y}" r="{r}" fill="red" stroke-width="2" stroke="black"> </circle>'''.format(x=(30+3*i)*(10-i), y=30, r=3.*float(i) ) for i in range(10)]) + '''</svg>''')
笔记本中的 SVG
最后,我们通过将 YouTube 视频的标识符传递给
YoutubeVideo
来显示 YouTube 视频:In [14]: YouTubeVideo('j9YpkSX7NNM')
笔记本中的 YouTube
-
现在,我们展示了 IPython 2.0+ 中的最新交互式功能,即 JavaScript 小部件。在这里,我们创建了一个下拉菜单来选择视频:
In [15]: from collections import OrderedDict from IPython.display import (display, clear_output, YouTubeVideo) from IPython.html.widgets import DropdownWidget In [16]: # We create a DropdownWidget, with a dictionary # containing the keys (video name) and the values # (Youtube identifier) of every menu item. dw = DropdownWidget(values=OrderedDict([ ('SciPy 2012', 'iwVvqwLDsJo'), ('PyCon 2012', '2G5YTlheCbw'), ('SciPy 2013', 'j9YpkSX7NNM')] ) ) # We create a callback function that displays the # requested Youtube video. def on_value_change(name, val): clear_output() display(YouTubeVideo(val)) # Every time the user selects an item, the # function `on_value_change` is called, and the # `val` argument contains the value of the selected # item. dw.on_trait_change(on_value_change, 'value') # We choose a default value. dw.value = dw.values['SciPy 2013'] # Finally, we display the widget. display(dw)
笔记本中的交互式小部件
IPython 2.0 的交互式功能为笔记本带来了全新的维度,我们可以期待未来会有更多的发展。
还有更多……
笔记本以结构化文本文件(JSON 格式)保存,这使得它们易于共享。以下是一个简单笔记本的内容:
{
"metadata": {
"name": ""
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "code",
"collapsed": false,
"input": [
"print(\"Hello World!\")"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"Hello World!\n"
]
}
],
"prompt_number": 1
}
],
"metadata": {}
}
]
}
IPython 附带了一个特殊工具 nbconvert,它将笔记本转换为其他格式,如 HTML 和 PDF (ipython.org/ipython-doc/stable/notebook/index.html
)。
另一个在线工具 nbviewer 允许我们直接在浏览器中呈现公开可用的笔记本,访问地址为 nbviewer.ipython.org
。
我们将在后续章节中讨论这些可能性,特别是在 第三章,精通笔记本。
这里是关于笔记本的一些参考资料:
-
官方笔记本页面,访问地址为
ipython.org/notebook
-
笔记本的文档,访问地址为
ipython.org/ipython-doc/dev/notebook/index.html
-
官方笔记本示例,访问地址为
github.com/ipython/ipython/tree/master/examples/Notebook
-
用户策划的有趣笔记本画廊,访问地址为
github.com/ipython/ipython/wiki/A-gallery-of-interesting-IPython-Notebooks
-
关于交互式小部件的官方教程,访问地址为
nbviewer.ipython.org/github/ipython/ipython/tree/master/examples/Interactive%20Widgets/
另见
- 在 IPython 中开始数据探索性分析 配方
在 IPython 中开始探索性数据分析
在本配方中,我们将介绍 IPython 在数据分析中的应用。大部分内容已在 学习 IPython 进行交互式计算和数据可视化 一书中涵盖,但我们将在这里回顾基础知识。
我们将下载并分析关于蒙特利尔自行车轨道的出勤数据集。这个示例主要受到 Julia Evans 的演讲启发(可以在 nbviewer.ipython.org/github/jvns/talks/blob/master/mtlpy35/pistes-cyclables.ipynb
中查看)。具体来说,我们将介绍以下内容:
-
使用 pandas 进行数据处理
-
使用 matplotlib 进行数据可视化
-
使用 IPython 2.0+ 的交互式小部件
如何操作...
-
第一步是导入我们在此示例中将要使用的科学计算包,即 NumPy、pandas 和 matplotlib。我们还指示 matplotlib 将图形渲染为 notebook 中的内联图像:
In [1]: import numpy as np import pandas as pd import matplotlib.pyplot as plt %matplotlib inline
-
现在,我们创建一个新的 Python 变量
url
,它包含一个 CSV(逗号分隔值)数据文件的地址。该标准文本格式用于存储表格数据:In [2]: url = "http://donnees.ville.montreal.qc.ca/storage/f/2014-01-20T20%3A48%3A50.296Z/2013.csv"
-
pandas 定义了一个
read_csv()
函数,可以读取任何 CSV 文件。在这里,我们传入文件的 URL。pandas 会自动下载并解析文件,然后返回一个DataFrame
对象。我们需要指定一些选项,以确保日期能够正确解析:In [3]: df = pd.read_csv(url, index_col='Date', parse_dates=True, dayfirst=True)
-
df
变量包含一个DataFrame
对象,这是 pandas 特定的数据结构,包含二维表格数据。head(n)
方法显示表格的前 n 行。在 notebook 中,pandas 会以 HTML 表格的形式显示DataFrame
对象,如以下截图所示:In [4]: df.head(2)
DataFrame
的前几行在这里,每一行包含每一天全年在城市各个轨道上的自行车数量。
-
我们可以通过
describe()
方法获得表格的一些摘要统计信息:In [5]: df.describe()
DataFrame
的摘要统计信息 -
让我们展示一些图形。我们将绘制两条轨道的每日出勤数据。首先,选择两列数据,
Berri1
和PierDup
。然后,我们调用plot()
方法:In [6]: df[['Berri1', 'PierDup']].plot()
-
现在,我们进入稍微复杂一点的分析。我们将查看所有轨道的出勤情况与星期几的关系。我们可以通过 pandas 很容易地获取星期几:
DataFrame
对象的index
属性包含表格中所有行的日期。这个索引有一些与日期相关的属性,包括weekday
:In [7]: df.index.weekday Out[7]: array([1, 2, 3, 4, 5, 6, 0, 1, 2, ..., 0, 1, 2])
然而,我们希望将 0 到 6 之间的数字替换为星期几的名称(例如星期一、星期二等)。这可以很容易地做到。首先,我们创建一个
days
数组,包含所有星期几的名称。然后,我们通过df.index.weekday
对其进行索引。这个操作将索引中的每个整数替换为days
中对应的名称。第一个元素是Monday
,索引为 0,因此df.index.weekday
中的每个 0 会被替换为Monday
,以此类推。我们将这个新的索引赋给DataFrame
中的新列Weekday
:In [8]: days = np.array(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']) df['Weekday'] = days[df.index.weekday]
-
为了按星期几获取出勤情况,我们需要按星期几对表格元素进行分组。
groupby()
方法可以帮助我们做到这一点。分组后,我们可以对每个组中的所有行进行求和:In [9]: df_week = df.groupby('Weekday').sum() In [10]: df_week
使用 pandas 对数据进行分组
-
我们现在可以在图形中显示这些信息了。我们首先需要使用
ix
(索引操作)按星期几重新排序表格。然后,我们绘制表格,指定线条宽度:In [11]: df_week.ix[days].plot(lw=3) plt.ylim(0); # Set the bottom axis to 0.
-
最后,让我们说明 IPython 2.0 中新的交互式功能。我们将绘制一个平滑的轨迹出勤情况与时间的关系图(滚动平均)。其思路是计算某一天邻近区域的平均值。邻域越大,曲线越平滑。我们将在笔记本中创建一个交互式滑块,以实时调整这个参数。我们只需要在绘图函数上方添加
@interact
装饰器:In [12]: from IPython.html.widgets import interact @interact def plot(n=(1, 30)): pd.rolling_mean(df['Berri1'], n).dropna().plot() plt.ylim(0, 8000) plt.show()
笔记本中的交互式小部件
还有更多内容...
pandas 是加载和操作数据集的正确工具。对于更高级的分析(信号处理、统计和数学建模),通常需要其他工具和方法。我们将在本书的第二部分中讲解这些步骤,从第七章开始,统计数据分析。
下面是一些关于使用 pandas 进行数据操作的参考资料:
-
《学习 IPython 进行互动计算和数据可视化》,Packt Publishing,我们之前的书籍
-
Python 数据分析,O'Reilly Media,由 pandas 的创建者 Wes McKinney 编写
-
pandas 的文档可通过
pandas.pydata.org/pandas-docs/stable/
获取
另见
- 介绍 NumPy 中的多维数组用于快速数组计算 示例
介绍 NumPy 中的多维数组用于快速数组计算
NumPy 是科学 Python 生态系统的主要基础。这个库提供了一种用于高性能数值计算的特定数据结构:多维数组。NumPy 背后的原理是:Python 作为一种高级动态语言,使用起来更方便,但比起 C 这样的低级语言要慢。NumPy 在 C 中实现了多维数组结构,并提供了一个便捷的 Python 接口,从而将高性能与易用性结合在一起。许多 Python 库都使用了 NumPy。例如,pandas 就是建立在 NumPy 之上的。
在这个示例中,我们将说明多维数组的基本概念。有关该主题的更全面的讲解可以在《学习 IPython 进行互动计算和数据可视化》一书中找到。
如何实现...
-
让我们导入内置的
random
Python 模块和 NumPy:In [1]: import random import numpy as np
我们使用
%precision
魔法命令(在 IPython 中定义)来仅显示 Python 输出中的三位小数。这只是为了减少输出文本中的数字位数。In [2]: %precision 3 Out[2]: u'%.3f'
-
我们生成两个 Python 列表,
x
和y
,每个列表包含 100 万个介于 0 和 1 之间的随机数:In [3]: n = 1000000 x = [random.random() for _ in range(n)] y = [random.random() for _ in range(n)] In [4]: x[:3], y[:3] Out[4]: ([0.996, 0.846, 0.202], [0.352, 0.435, 0.531])
-
让我们计算所有这些数字的逐元素和:
x
的第一个元素加上y
的第一个元素,以此类推。我们使用for
循环在列表推导中进行操作:In [5]: z = [x[i] + y[i] for i in range(n)] z[:3] Out[5]: [1.349, 1.282, 0.733]
-
这个计算需要多长时间?IPython 定义了一个方便的
%timeit
魔法命令,可以快速评估单个语句所花费的时间:In [6]: %timeit [x[i] + y[i] for i in range(n)] 1 loops, best of 3: 273 ms per loop
-
现在,我们将使用 NumPy 执行相同的操作。NumPy 处理多维数组,因此我们需要将列表转换为数组。
np.array()
函数正好完成了这个操作:In [7]: xa = np.array(x) ya = np.array(y) In [8]: xa[:3] Out[8]: array([ 0.996, 0.846, 0.202])
xa
和ya
数组包含与我们原始列表x
和y
相同的数字。这些列表是list
内建类的实例,而我们的数组是ndarray
NumPy 类的实例。这些类型在 Python 和 NumPy 中的实现方式非常不同。在这个例子中,我们将看到,使用数组代替列表可以大幅提高性能。 -
现在,为了计算这些数组的逐元素和,我们不再需要做
for
循环。在 NumPy 中,添加两个数组意味着逐个元素地将数组的元素相加。这是线性代数中的标准数学符号(对向量和矩阵的操作):In [9]: za = xa + ya za[:3] Out[9]: array([ 1.349, 1.282, 0.733])
我们看到,
z
列表和za
数组包含相同的元素(x
和y
中数字的和)。 -
让我们将这个 NumPy 操作的性能与原生 Python 循环进行比较:
In [10]: %timeit xa + ya 100 loops, best of 3: 10.7 ms per loop
我们观察到,在 NumPy 中,这个操作比在纯 Python 中快了一个数量级以上!
-
现在,我们将计算其他内容:
x
或xa
中所有元素的和。尽管这不是逐元素操作,但 NumPy 在这里仍然非常高效。纯 Python 版本使用内建的sum()
函数对可迭代对象求和,而 NumPy 版本使用np.sum()
函数对 NumPy 数组求和:In [11]: %timeit sum(x) # pure Python %timeit np.sum(xa) # NumPy 100 loops, best of 3: 17.1 ms per loop 1000 loops, best of 3: 2.01 ms per loop
我们在这里也观察到令人印象深刻的加速。
-
最后,让我们执行最后一个操作:计算我们两个列表中任意一对数字之间的算术距离(我们只考虑前 1000 个元素,以保持计算时间合理)。首先,我们用两个嵌套的
for
循环在纯 Python 中实现这个操作:In [12]: d = [abs(x[i] - y[j]) for i in range(1000) for j in range(1000)] In [13]: d[:3] Out[13]: [0.230, 0.037, 0.549]
-
现在,我们使用 NumPy 实现,提出两个稍微高级的概念。首先,我们考虑一个二维数组(或矩阵)。这就是我们如何处理两个索引,i 和 j。其次,我们使用广播在 2D 数组和 1D 数组之间执行操作。我们将在它是如何工作的...部分提供更多细节。
In [14]: da = np.abs(xa[:1000,None] - ya[:1000]) In [15]: da Out[15]: array([[ 0.23 , 0.037, ..., 0.542, 0.323, 0.473], ..., [ 0.511, 0.319, ..., 0.261, 0.042, 0.192]]) In [16]: %timeit [abs(x[i] - y[j]) for i in range(1000) for j in range(1000)] %timeit np.abs(xa[:1000,None] - ya[:1000]) 1 loops, best of 3: 292 ms per loop 100 loops, best of 3: 18.4 ms per loop
在这里,我们再次观察到显著的加速。
它是如何工作的...
NumPy 数组是一个均匀的数据块,按照多维有限网格组织。数组中的所有元素都具有相同的数据类型,也叫做 dtype(整数、浮动小数等)。数组的形状是一个 n 元组,表示每个维度的大小。
一维数组是一个向量;它的形状只是组件的数量。
二维数组是一个矩阵;它的形状是(行数,列数)。
下图展示了一个包含 24 个元素的 3D(3, 4, 2)数组的结构:
一个 NumPy 数组
Python 中的切片语法与 NumPy 中的数组索引非常匹配。此外,我们可以通过在索引中使用 None
或 np.newaxis
为现有数组添加一个额外的维度。我们在之前的例子中使用了这一技巧。
可以对具有相同形状的 NumPy 数组执行逐元素的算术操作。然而,广播机制放宽了这一条件,它允许在特定条件下对具有不同形状的数组进行操作。特别是,当一个数组的维度比另一个数组少时,它可以被虚拟地拉伸以匹配另一个数组的维度。这就是我们如何计算 xa
和 ya
中任何两个元素之间的成对距离的方式。
数组操作为什么比 Python 循环快得多?有几个原因,我们将在第四章,性能分析与优化中详细回顾这些原因。我们在这里可以先说:
-
在 NumPy 中,数组操作是通过 C 循环而非 Python 循环实现的。由于 Python 是解释型语言且具有动态类型的特性,它通常比 C 慢。
-
NumPy 数组中的数据存储在 RAM 中一个连续的内存块中。这一特性使得 CPU 周期和缓存的使用更为高效。
还有更多内容...
显然,这个话题还有很多内容可以讨论。我们之前的书籍 Learning IPython for Interactive Computing and Data Visualization 包含了更多关于基本数组操作的细节。本书将经常使用数组数据结构。特别地,第四章,性能分析与优化,涵盖了使用 NumPy 数组的高级技巧。
这里有一些参考文献:
-
NumPy 文档中关于
ndarray
的介绍可参考docs.scipy.org/doc/numpy/reference/arrays.ndarray.html
-
NumPy 数组教程可参考
wiki.scipy.org/Tentative_NumPy_Tutorial
-
SciPy 讲义中关于 NumPy 数组的介绍,参见
scipy-lectures.github.io/intro/numpy/array_object.html
另见
-
开始进行 IPython 中的探索性数据分析 的教程
-
第四章中的理解 NumPy 的内部机制以避免不必要的数组复制,性能分析与优化
创建带有自定义魔法命令的 IPython 扩展
尽管 IPython 提供了各种各样的魔法命令,但在某些情况下,我们需要在新的魔法命令中实现自定义功能。在本教程中,我们将展示如何创建行魔法和单元魔法,并且如何将它们集成到 IPython 扩展中。
如何做...
-
让我们从 IPython 魔法系统中导入一些函数:
In [1]: from IPython.core.magic import (register_line_magic, register_cell_magic)
-
定义一个新的行魔法非常简单。首先,我们创建一个接受行内容的函数(不包括前面的
%
-前缀名称)。这个函数的名称就是魔法的名称。然后,我们使用@register_line_magic
装饰这个函数:In [2]: @register_line_magic def hello(line): if line == 'french': print("Salut tout le monde!") else: print("Hello world!") In [3]: %hello Hello world! In [4]: %hello french Salut tout le monde!
-
让我们创建一个稍微有用一些的
%%csv
单元魔法,它解析 CSV 字符串并返回一个 pandasDataFrame
对象。这次,函数的参数是紧随%%csv
之后的字符(位于第一行)和单元格的内容(从单元格的第二行到最后一行)。In [5]: import pandas as pd #from StringIO import StringIO # Python 2 from io import StringIO # Python 3 @register_cell_magic def csv(line, cell): # We create a string buffer containing the # contents of the cell. sio = StringIO(cell) # We use pandas' read_csv function to parse # the CSV string. return pd.read_csv(sio) In [6]: %%csv col1,col2,col3 0,1,2 3,4,5 7,8,9 Out[6]: col1 col2 col3 0 0 1 2 1 3 4 5 2 7 8 9
我们可以通过
_
访问返回的对象。In [7]: df = _ df.describe() Out[7]: col1 col2 col3 count 3.000000 3.000000 3.000000 mean 3.333333 4.333333 5.333333 ... min 0.000000 1.000000 2.000000 max 7.000000 8.000000 9.000000
-
我们描述的方法在交互式会话中非常有用。如果我们想在多个笔记本中使用相同的魔法,或者想要分发它,那么我们需要创建一个实现自定义魔法命令的IPython 扩展。第一步是创建一个 Python 脚本(这里是
csvmagic.py
),它实现了魔法功能。我们还需要定义一个特殊的函数load_ipython_extension(ipython)
:In [8]: %%writefile csvmagic.py import pandas as pd #from StringIO import StringIO # Python 2 from io import StringIO # Python 3 def csv(line, cell): sio = StringIO(cell) return pd.read_csv(sio) def load_ipython_extension(ipython): """This function is called when the extension is loaded. It accepts an IPython InteractiveShell instance. We can register the magic with the `register_magic_function` method.""" ipython.register_magic_function(csv, 'cell') Overwriting csvmagic.py
-
一旦扩展模块创建完成,我们需要将其导入到 IPython 会话中。我们可以通过
%load_ext
魔法命令做到这一点。在这里,加载我们的扩展立即在交互式 shell 中注册我们的%%csv
魔法函数:In [9]: %load_ext csvmagic In [10]: %%csv col1,col2,col3 0,1,2 3,4,5 7,8,9 Out[10]: col1 col2 col3 0 0 1 2 1 3 4 5 2 7 8 9
它是如何工作的...
IPython 扩展是一个 Python 模块,它实现了顶级的load_ipython_extension(ipython)
函数。当 %load_ext
魔法命令被调用时,模块会被加载,且会调用load_ipython_extension(ipython)
函数。这个函数会将当前的InteractiveShell
实例作为参数传入。这个对象实现了几个方法,我们可以用来与当前的 IPython 会话进行交互。
InteractiveShell 类
一个交互式 IPython 会话由InteractiveShell
类的(单例)实例表示。这个对象处理历史记录、交互式命名空间以及会话中大多数可用功能。
在交互式 shell 中,我们可以通过get_ipython()
函数获取当前的InteractiveShell
实例。
InteractiveShell
的所有方法列表可以在参考 API 中找到(请参见本教程末尾的链接)。以下是最重要的属性和方法:
-
user_ns
: 用户命名空间(一个字典)。 -
push()
: 推送(或注入)Python 变量到交互命名空间中。 -
ev()
: 评估用户命名空间中的 Python 表达式。 -
ex()
: 执行用户命名空间中的 Python 语句。 -
run_cell()
: 运行一个单元格(以字符串形式给出),可能包含 IPython 魔法命令。 -
safe_execfile()
: 安全地执行一个 Python 脚本。 -
system()
: 执行一个系统命令。 -
write()
: 写入一个字符串到默认输出。 -
write_err()
: 写入一个字符串到默认的错误输出。 -
register_magic_function()
: 注册一个独立的函数作为 IPython魔法函数。我们在本食谱中使用了此方法。
加载扩展
使用%load_ext
时,Python 扩展模块需要是可导入的。这里,我们的模块位于当前目录。在其他情况下,它必须在 Python 路径中。它还可以存储在~\.ipython\extensions
中,系统会自动将其加入 Python 路径。
为了确保我们的魔法命令在 IPython 配置文件中自动定义,我们可以指示 IPython 在启动新的交互式 Shell 时自动加载我们的扩展。为此,我们需要打开~/.ipython/profile_default/ipython_config.py
文件,并将'csvmagic'
加入到c.InteractiveShellApp.extensions
列表中。csvmagic
模块需要是可导入的。通常会创建一个Python 包,该包实现 IPython 扩展,并定义自定义魔法命令。
还有更多...
存在许多第三方扩展和魔法命令,特别是cythonmagic
、octavemagic
和rmagic
,它们都允许我们在单元格中插入非 Python 代码。例如,使用cythonmagic
,我们可以在单元格中创建一个 Cython 函数,并在笔记本的其余部分中导入它。
这里有一些参考资料:
-
IPython 扩展系统的文档请参见
ipython.org/ipython-doc/dev/config/extensions/
-
定义新的魔法命令请参见
ipython.org/ipython-doc/dev/interactive/reference.html#defining-magics
-
IPython 扩展的索引请参见
github.com/ipython/ipython/wiki/Extensions-Index
-
InteractiveShell
的 API 参考请参见ipython.org/ipython-doc/dev/api/generated/IPython.core.interactiveshell.html
另请参见
- 掌握 IPython 的配置系统食谱
掌握 IPython 的配置系统
IPython 实现了一个真正强大的配置系统。这个系统贯穿整个项目,但也可以被 IPython 扩展使用,甚至可以在全新的应用程序中使用。
在本食谱中,我们展示了如何使用这个系统来编写一个可配置的 IPython 扩展。我们将创建一个简单的魔法命令来显示随机数。这个魔法命令带有可配置的参数,用户可以在他们的 IPython 配置文件中进行设置。
如何做到这一点...
-
我们在
random_magics.py
文件中创建了一个 IPython 扩展。让我们先导入一些对象。注意
确保将步骤 1-5 中的代码放在一个名为
random_magics.py
的外部文本文件中,而不是笔记本的输入中!from IPython.utils.traitlets import Int, Float, Unicode, Bool from IPython.core.magic import (Magics, magics_class, line_magic) import numpy as np
-
我们创建了一个从
Magics
派生的RandomMagics
类。这个类包含一些可配置参数:@magics_class class RandomMagics(Magics): text = Unicode(u'{n}', config=True) max = Int(1000, config=True) seed = Int(0, config=True)
-
我们需要调用父类的构造函数。然后,我们用一个种子初始化一个随机数生成器:
def __init__(self, shell): super(RandomMagics, self).__init__(shell) self._rng = np.random.RandomState(self.seed or None)
-
我们创建了一个
%random
行魔法,它显示一个随机数:@line_magic def random(self, line): return self.text.format(n=self._rng.randint(self.max))
-
最后,当扩展加载时,我们注册这个魔法:
def load_ipython_extension(ipython): ipython.register_magics(RandomMagics)
-
让我们在笔记本中测试我们的扩展:
In [1]: %load_ext random_magics In [2]: %random Out[2]: '635' In [3]: %random Out[3]: '47'
-
我们的魔法命令有一些可配置参数。这些变量是用户在 IPython 配置文件中或启动 IPython 时在控制台中配置的。要在终端中配置这些变量,我们可以在系统 shell 中输入以下命令:
ipython --RandomMagics.text='Your number is {n}.' --RandomMagics.max=10 --RandomMagics.seed=1
在此会话中,我们得到以下行为:
In [1]: %load_ext random_magics In [2]: %random Out[2]: u'Your number is 5.' In [3]: %random Out[3]: u'Your number is 8.'
-
要在 IPython 配置文件中配置变量,我们必须打开
~/.ipython/profile_default/ipython_config.py
文件,并添加以下行:c.RandomMagics.text = 'random {n}'
启动 IPython 后,我们得到以下行为:
In [4]: %random Out[4]: 'random 652'
它是如何工作的...
IPython 的配置系统定义了几个概念:
-
用户配置文件 是一组特定于用户的参数、日志和命令历史记录。用户在处理不同项目时可以有不同的配置文件。一个
xxx
配置文件存储在~/.ipython/profile_xxx
中,其中~
是用户的主目录。-
在 Linux 上,路径通常是
/home/yourname/.ipython/profile_xxx
-
在 Windows 上,路径通常是
C:\Users\YourName\.ipython\profile_xxx
-
-
配置对象,或者说
Config
,是一个特殊的 Python 字典,包含键值对。Config
类继承自 Python 的dict
。 -
HasTraits
类是一个可以拥有特殊trait
属性的类。Traits 是复杂的 Python 属性,具有特定类型和默认值。此外,当 trait 的值发生变化时,回调函数会自动且透明地被调用。这个机制允许一个类在 trait 属性发生变化时得到通知。 -
Configurable
类是所有希望受益于配置系统的类的基类。一个Configurable
类可以拥有可配置的属性。这些属性在类定义中直接指定了默认值。Configurable
类的主要特点是其 trait 的默认值可以通过配置文件逐个类地被覆盖。然后,Configurables
的实例可以随意更改这些值。 -
配置文件 是一个包含
Configurable
类参数的 Python 或 JSON 文件。
Configurable
类和配置文件支持继承模型。一个 Configurable
类可以从另一个 Configurable
类派生并重写其参数。类似地,一个配置文件可以被包含在另一个文件中。
配置项
这里是一个简单的 Configurable
类的示例:
from IPython.config.configurable import Configurable
from IPython.utils.traitlets import Float
class MyConfigurable(Configurable):
myvariable = Float(100.0, config=True)
默认情况下,MyConfigurable
类的实例将其 myvariable
属性设置为 100
。现在,假设我们的 IPython 配置文件包含以下几行:
c = get_config()
c.MyConfigurable.myvariable = 123.
然后,myvariable
属性将默认为 123
。实例化后可以自由更改此默认值。
get_config()
函数是一个特殊的函数,可以在任何配置文件中使用。
此外,Configurable
参数可以在命令行界面中指定,正如我们在本配方中所看到的那样。
这个配置系统被所有 IPython 应用程序使用(特别是 console
、qtconsole
和 notebook
)。这些应用程序有许多可配置的属性。你将在配置文件中找到这些属性的列表。
魔法命令
Magics 类继承自 Configurable
类,可以包含可配置的属性。此外,可以通过 @line_magic
或 @cell_magic
装饰的方法来定义魔法命令。与前面的配方中使用函数魔法不同,定义类魔法的优势在于我们可以在多个魔法调用之间保持状态(因为我们使用的是类而不是函数)。
还有更多内容...
这里有一些参考资料:
-
在
ipython.org/ipython-doc/dev/config/index.html
配置和定制 IPython -
配置系统的详细概述,访问
ipython.org/ipython-doc/dev/development/config.html
-
定义自定义魔法,详见
ipython.org/ipython-doc/dev/interactive/reference.html#defining-magics
-
可用的 traitlets 模块,访问
ipython.org/ipython-doc/dev/api/generated/IPython.utils.traitlets.html
参见
- 创建带有自定义魔法命令的 IPython 扩展 配方
为 IPython 创建一个简单的内核
为 IPython 开发的架构,以及将成为 Project Jupyter 核心的架构,正在变得越来越独立于语言。客户端与内核之间的解耦使得可以用任何语言编写内核。客户端通过基于套接字的消息协议与内核通信。因此,可以用任何支持套接字的语言编写内核。
然而,消息协议是复杂的。从头编写一个新的内核并不简单。幸运的是,IPython 3.0 提供了一个轻量级的内核语言接口,可以用 Python 包装。
此接口还可用于在 IPython 笔记本(或其他客户端应用程序,如控制台)中创建完全定制的体验。通常,Python 代码必须在每个代码单元中编写;但是,我们可以为任何领域特定语言编写一个内核。我们只需编写一个接受代码字符串作为输入(代码单元的内容)的 Python 函数,并发送文本或丰富数据作为输出。我们还可以轻松实现代码完成和代码检查。
我们可以想象许多有趣的交互式应用程序,远远超出了 IPython 最初用例的范围。这些应用程序对于非程序员终端用户(如高中学生)可能特别有用。
在此配方中,我们将创建一个简单的图形计算器。计算器透明地由 NumPy 和 matplotlib 支持。我们只需在代码单元中编写函数,如 y = f(x)
,即可获取这些函数的图形。
准备工作
此配方已在 IPython 3.0 开发版本上进行了测试。它应该可以在 IPython 3.0 最终版本上无或最小更改地运行。我们将所有有关包装器内核和消息协议的引用都放在此配方的结尾。
如何操作……
注意
警告:此配方仅适用于 IPython >= 3.0!
-
首先,我们创建一个
plotkernel.py
文件。该文件将包含我们自定义内核的实现。让我们导入一些模块:注意
请确保将步骤 1-6 的代码放在名为
plotkernel.py
的外部文本文件中,而不是笔记本的输入中!from IPython.kernel.zmq.kernelbase import Kernel import numpy as np import matplotlib.pyplot as plt from io import BytesIO import urllib, base64
-
我们编写一个函数,返回 matplotlib 图形的 base64 编码的 PNG 表示:
def _to_png(fig): """Return a base64-encoded PNG from a matplotlib figure.""" imgdata = BytesIO() fig.savefig(imgdata, format='png') imgdata.seek(0) return urllib.parse.quote( base64.b64encode(imgdata.getvalue()))
-
现在,我们编写一个函数,解析具有
y = f(x)
形式的代码字符串,并返回一个 NumPy 函数。这里,f
是一个可以使用 NumPy 函数的任意 Python 表达式:_numpy_namespace = {n: getattr(np, n) for n in dir(np)} def _parse_function(code): """Return a NumPy function from a string 'y=f(x)'.""" return lambda x: eval(code.split('=')[1].strip(), _numpy_namespace, {'x': x})
-
对于我们的新包装器内核,我们创建一个派生自
Kernel
的类。我们需要提供一些元数据字段:class PlotKernel(Kernel): implementation = 'Plot' implementation_version = '1.0' language = 'python' # will be used for # syntax highlighting language_version = '' banner = "Simple plotting"
-
在此类中,我们实现了一个
do_execute()
方法,接受代码作为输入并向客户端发送响应:def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): # We create the plot with matplotlib. fig = plt.figure(figsize=(6,4), dpi=100) x = np.linspace(-5., 5., 200) functions = code.split('\n') for fun in functions: f = _parse_function(fun) y = f(x) plt.plot(x, y) plt.xlim(-5, 5) # We create a PNG out of this plot. png = _to_png(fig) if not silent: # We send the standard output to the client. self.send_response(self.iopub_socket, 'stream', { 'name': 'stdout', 'data': 'Plotting {n} function(s)'. \ format(n=len(functions))}) # We prepare the response with our rich data # (the plot). content = { 'source': 'kernel', # This dictionary may contain different # MIME representations of the output. 'data': { 'image/png': png }, # We can specify the image size # in the metadata field. 'metadata' : { 'image/png' : { 'width': 600, 'height': 400 } } } # We send the display_data message with the # contents. self.send_response(self.iopub_socket, 'display_data', content) # We return the execution results. return {'status': 'ok', 'execution_count': self.execution_count, 'payload': [], 'user_expressions': {}, }
-
最后,在文件末尾添加以下行:
if __name__ == '__main__': from IPython.kernel.zmq.kernelapp import IPKernelApp IPKernelApp.launch_instance(kernel_class=PlotKernel)
-
我们的内核准备就绪!下一步是告诉 IPython 此新内核已可用。为此,我们需要创建一个内核规范
kernel.json
文件,并将其放置在~/.ipython/kernels/plot/
中。此文件包含以下行:{ "argv": ["python", "-m", "plotkernel", "-f", "{connection_file}"], "display_name": "Plot", "language": "python" }
plotkernel.py
文件需要 Python 能够导入。例如,我们可以简单地将其放在当前目录中。 -
在 IPython 3.0 中,我们可以从 IPython 笔记本仪表板启动具有此内核的笔记本。笔记本界面的右上角有一个下拉菜单,其中包含可用内核的列表。选择绘图内核以使用它。
-
最后,在由我们定制的绘图内核支持的新笔记本中,我们只需简单地编写数学方程
y = f(x)
。相应的图形将显示在输出区域。这里是一个例子:我们自定义绘图包装器内核的示例
如何运行……
我们将在第三章《精通笔记本》中提供更多关于 IPython 和笔记本架构的细节。这里我们仅做一个简要总结。请注意,这些细节可能会在未来版本的 IPython 中发生变化。
内核和客户端运行在不同的进程中。它们通过在网络套接字上实现的消息协议进行通信。目前,这些消息采用 JSON 编码,这是一种结构化的基于文本的文档格式。
我们的内核接收来自客户端(例如,笔记本)的代码。每当用户发送一个单元格的代码时,do_execute()
函数就会被调用。
内核可以通过self.send_response()
方法将消息发送回客户端:
-
第一个参数是套接字,这里是IOPub套接字
-
第二个参数是消息类型,在这里是
stream
,用于返回标准输出或标准错误,或者是display_data
,用于返回富数据 -
第三个参数是消息的内容,表示为一个 Python 字典
数据可以包含多种 MIME 表示:文本、HTML、SVG、图片等。由客户端来处理这些数据类型。特别是,HTML 笔记本客户端知道如何在浏览器中呈现这些类型。
该函数返回一个包含执行结果的字典。
在这个示例中,我们始终返回ok
状态。在生产代码中,最好检测到错误(例如函数定义中的语法错误),并返回错误状态。
所有消息协议的细节可以在本食谱末尾给出的链接中找到。
还有更多...
包装内核可以实现可选方法,特别是用于代码补全和代码检查。例如,为了实现代码补全,我们需要编写以下方法:
def do_complete(self, code, cursor_pos):
return {'status': 'ok',
'cursor_start': ...,
'cursor_end': ...,
'matches': [...]}
当用户请求代码补全且光标位于代码单元格中的某个cursor_pos
位置时,会调用此方法。在该方法的响应中,cursor_start
和cursor_end
字段表示代码补全应覆盖的输出区间。matches
字段包含建议的列表。
这些细节可能会随着 IPython 3.0 的发布而发生变化。你可以在以下参考资料中找到所有最新的信息:
-
包装内核,访问地址:
ipython.org/ipython-doc/dev/development/wrapperkernels.html
-
消息协议,访问地址:
ipython.org/ipython-doc/dev/development/messaging.html
-
KernelBase API 参考,访问地址:
ipython.org/ipython-doc/dev/api/generated/IPython.kernel.zmq.kernelbase.html
第二章:交互式计算中的最佳实践
在本章中,我们将涵盖以下主题:
-
选择(或不选择)Python 2 和 Python 3
-
使用 IPython 进行高效的交互式计算工作流
-
学习分布式版本控制系统 Git 的基础
-
使用 Git 分支的典型工作流
-
进行可重现交互式计算实验的十条技巧
-
编写高质量的 Python 代码
-
使用 nose 编写单元测试
-
使用 IPython 调试代码
引言
这是关于交互式计算中良好实践的特别章节。如果本书的其余部分讨论的是内容,那么本章讨论的是形式。它描述了如何高效且正确地使用本书所讨论的工具。我们将在讨论可重现计算实验之前,先介绍版本控制系统 Git 的基本要点(特别是在 IPython notebook 中)。
我们还将讨论一些软件开发中的一般主题,例如代码质量、调试和测试。关注这些问题可以极大地提高我们最终产品的质量(例如软件、研究和出版物)。我们这里只是略微涉及,但你将找到许多参考资料,帮助你深入了解这些重要主题。
选择(或不选择)Python 2 和 Python 3
在这第一个食谱中,我们将简要讨论一个横向且有些平凡的话题:Python 2 还是 Python 3?
Python 3 自 2008 年推出以来,许多 Python 用户仍然停留在 Python 2。通过改进了 Python 2 的多个方面,Python 3 打破了与之前版本的兼容性。因此,迁移到 Python 3 可能需要投入大量精力。
即使没有太多破坏兼容性的变化,一个在 Python 2 中运行良好的程序可能在 Python 3 中完全无法运行。例如,你的第一个 Hello World
Python 2 程序在 Python 3 中无法运行;print "Hello World!"
在 Python 3 中会引发 SyntaxError
。实际上,print
现在是一个函数,而不是一个语句。你应该写成 print("Hello World!")
,这在 Python 2 中也能正常工作。
无论你是开始一个新项目,还是需要维护一个旧的 Python 库,选择 Python 2 还是 Python 3 的问题都会出现。在这里,我们提供一些论点和提示,帮助你做出明智的决策。
注意
当我们提到 Python 2 时,我们特别指的是 Python 2.6 或 Python 2.7,因为这些 Python 2.x 的最后版本比 Python 2.5 及更早版本更接近 Python 3。支持 Python 2.5+ 和 Python 3.x 同时运行更为复杂。
同样,当我们提到 Python 3 或 Python 3.x 时,我们特别指的是 Python 3.3 或更高版本。
如何做到……
首先,Python 2 和 Python 3 之间有什么区别?
Python 3 相对于 Python 2 的主要差异
这里是一些差异的部分列表:
-
print
不再是一个语句,而是一个函数(括号是必须的)。 -
整数除法会返回浮点数,而不是整数。
-
一些内置函数返回的是迭代器或视图,而不是列表。例如,
range
在 Python 3 中的行为类似于 Python 2 中的xrange
,而后者在 Python 3 中已经不存在。 -
字典不再有
iterkeys()
、iteritems()
和itervalues()
方法了。你应该改用keys()
、items()
和values()
函数。 -
以下是来自官方 Python 文档的引用:
“你曾经以为自己了解的二进制数据和 Unicode,现在都变了。”
-
使用
%
进行字符串格式化已不推荐使用;请改用str.format
。 -
exec
是一个函数,而不是一个语句。
Python 3 带来了许多关于语法和标准库内容的改进和新特性。你将在本食谱末尾的参考资料中找到更多细节。
现在,你的项目基本上有两种选择:坚持使用单一分支(Python 2 或 Python 3),或同时保持与两个分支的兼容性。
Python 2 还是 Python 3?
自然,许多人会偏爱 Python 3;它代表着未来,而 Python 2 是过去。为什么还要去支持一个已被废弃的 Python 版本呢?不过,这里有一些可能需要保持 Python 2 兼容性的情况:
-
你需要维护一个用 Python 2 编写的大型项目,而更新到 Python 3 会花费太高(即使存在半自动化的更新工具)。
-
你的项目有一些依赖项无法与 Python 3 一起使用。
注意
本书中我们将使用的大多数库支持 Python 2 和 Python 3。并且本书的代码兼容这两个分支。
-
你的最终用户所使用的环境不太支持 Python 3。例如,他们可能在某个大型机构工作,在许多服务器上部署新的 Python 版本成本太高。
在这种情况下,你可以选择继续使用 Python 2,但这意味着你的代码可能在不久的将来变得过时。或者,你可以选择 Python 3 和它那一堆崭新的功能,但也有可能会把 Python 2 的用户抛在后头。你也可以用 Python 2 编写代码,并为 Python 3 做好准备。这样,你可以减少将来迁移到 Python 3 时所需的修改。
幸运的是,你不一定要在 Python 2 和 Python 3 之间做出选择。实际上,有方法可以同时支持这两个版本。即使这可能比单纯选择一个分支多花一些工作,但在某些情况下它可能会非常有趣。不过,需要注意的是,如果选择这种做法,你可能会错过许多仅支持 Python 3 的特性。
同时支持 Python 2 和 Python 3
支持两个分支的基本方法有两种:使用 2to3 工具,或者编写在两个分支中都能正常运行的代码。
使用 2to3 工具
2to3
是标准库中的一个程序,能够自动将 Python 2 代码转换为 Python 3。例如,运行 2to3 -w example.py
可以将单个 Python 2 模块迁移到 Python 3。你可以在 docs.python.org/2/library/2to3.html
找到更多关于 2to3 工具的信息。
你可以配置安装脚本,使得 2to3
在用户安装你的包时自动运行。Python 3 用户将自动获得转换后的 Python 3 版本的包。
这个解决方案要求你的程序有一个坚实的测试套件,并且有一个持续集成系统,能够测试 Python 2 和 Python 3(请参阅本章稍后的单元测试食谱)。这是确保你的代码在两个版本中都能正常工作的方式。
编写在 Python 2 和 Python 3 中都能运行的代码。
你还可以编写既能在 Python 2 中运行,又能在 Python 3 中运行的代码。如果从头开始一个新项目,这个解决方案会更简单。一个广泛使用的方法是依赖一个轻量且成熟的模块,名为 six,由 Benjamin Peterson 开发。这个模块只有一个文件,因此你可以轻松地将其与包一起分发。无论何时你需要使用一个仅在某个 Python 分支中支持的函数或特性时,都需要使用 six 中实现的特定函数。这个函数要么包装,要么模拟相应的功能,因此它能在两个分支中正常工作。你可以在 pythonhosted.org/six/
上找到关于 six 的更多信息。
这种方法要求你改变一些习惯。例如,在 Python 2 中迭代字典的所有项时,你会写如下代码:
for k, v in d.iteritems():
# ...
现在,不再使用前面的代码,而是使用 six 编写以下代码:
from six import iteritems
for k, v in iteritems(d):
# ...
Python 2 中字典的 iteritems()
方法在 Python 3 中被 items()
替代。six 模块的 iteritems
函数根据 Python 版本内部调用一个方法。
提示
下载示例代码
你可以从你的账号中下载所有已购买的 Packt 图书的示例代码文件,网址为 www.packtpub.com
。如果你是在其他地方购买的此书,可以访问 www.packtpub.com/support
注册并直接将文件通过电子邮件发送给你。
还有更多...
正如我们所看到的,关于 Python 2 或 Python 3 的问题,有很多选择可以考虑。简而言之,你应该考虑以下选项:
-
请仔细决定是否真的需要支持 Python 2:
-
如果是,请通过避免使用 Python 2 专有的语法或特性,为 Python 3 准备好你的代码。你可以使用 six、2to3 或类似的工具。
-
如果没有必要,坚决使用 Python 3。
-
-
在所有情况下,确保你的项目拥有一个坚实的测试套件、出色的代码覆盖率(接近 100%),并且有一个持续集成系统,能够在你正式支持的所有 Python 版本中测试你的代码。
这里有几个相关的参考资料:
-
一本关于将代码迁移到 Python 3 的优秀免费书籍,作者:Lennart Regebro,访问地址:
python3porting.com/
-
关于将代码迁移到 Python 3 的官方推荐,访问地址:
docs.python.org/3/howto/pyporting.html
-
关于 Python 2/Python 3 问题的官方维基页面,访问地址:
wiki.python.org/moin/Python2orPython3
-
Nick Coghlan 提供的 Python 3 问答,访问地址:
python-notes.curiousefficiency.org/en/latest/python3/questions_and_answers.html
-
Python 3 中的新特性,请见
docs.python.org/3.3/whatsnew/3.0.html
-
你无法使用的 Python 十大酷炫特性,因为你拒绝升级到 Python 3,由 Aaron Meurer 提供的演讲,访问地址:
asmeurer.github.io/python3-presentation/slides.html
-
在编写兼容性代码时使用
__future__
模块,访问地址:docs.python.org/2/library/__future__.html
-
Python 2 和 Python 3 的关键区别,访问地址:
sebastianraschka.com/Articles/2014_python_2_3_key_diff.html
另见
-
编写高质量 Python 代码 这篇食谱
-
使用 nose 编写单元测试 这篇食谱
使用 IPython 进行高效的交互式计算工作流
有多种方法可以使用 IPython 进行交互式计算。其中一些在灵活性、模块化、可重用性和可复现性方面更为优秀。我们将在本节中回顾和讨论这些方法。
任何交互式计算工作流都基于以下循环:
-
编写一些代码
-
执行它
-
解释结果
-
重复
这个基本循环(也叫做读取-求值-打印循环,或称REPL)在进行数据或模型模拟的探索性研究时特别有用,或者在逐步构建复杂算法时也很有用。一个更经典的工作流(编辑-编译-运行-调试循环)通常是编写一个完整的程序,然后进行全面分析。这种方法通常比较繁琐。更常见的做法是通过做小规模实验、调整参数来迭代地构建算法解决方案,这正是交互式计算的核心。
集成开发环境(IDEs),提供了全面的软件开发设施(如源代码编辑器、编译器和调试器),广泛应用于经典工作流。然而,在交互式计算方面,存在一些替代 IDE 的工具。我们将在这里进行回顾。
如何实现...
以下是几个可能的交互式计算工作流程,按复杂度递增排列。当然,IPython 是所有这些方法的核心。
IPython 终端
IPython 是 Python 中交互式计算的事实标准。IPython 终端(ipython
命令)提供了一个专门为 REPL 设计的命令行界面。它比本地 Python 解释器(python
命令)更强大。IPython 终端是一个便捷的工具,用于快速实验、简单的 Shell 交互以及查找帮助。忘记了 NumPy 的 savetxt
函数的输入参数吗?只需在 IPython 中输入numpy.savetxt?
(当然,你首先需要使用import numpy
)。有些人甚至把 IPython 终端当作(复杂的)计算器来使用!
然而,当单独使用时,终端很快变得有限。主要问题是终端不是一个代码编辑器,因此输入超过几行的代码可能会变得不方便。幸运的是,有多种方法可以解决这个问题,下面的章节将详细介绍这些方法。
IPython 与文本编辑器
非文本编辑器问题的最简单解决方案或许并不令人意外,那就是结合使用 IPython 和文本编辑器。在这种工作流程中,%run
魔法命令成为了核心工具:
-
在你喜欢的文本编辑器中编写一些代码,并将其保存在
myscript.py
Python 脚本文件中。 -
在 IPython 中,假设你处于正确的目录,输入
%run myscript.py
。 -
脚本被执行。标准输出会实时显示在 IPython 终端中,并且会显示可能的错误。脚本中定义的顶级变量在脚本执行完毕后,可以在 IPython 终端中访问。
-
如果需要在脚本中进行代码更改,请重复此过程。
提示
通过适当的键盘快捷键,IPython-文本编辑器工作流程可以变得更加高效。例如,你可以自动化你的文本编辑器,当按下F8时,在正在运行的 IPython 解释器中执行以下命令:
%run <CURRENT_FILE_NAME>
这里描述了这种方法(在 Windows 上,使用 Notepad++和 AutoHotKey):
cyrille.rossant.net/python-ide-windows/
使用一个好的文本编辑器,这个工作流程可以非常高效。由于每次执行%run
时都会重新加载脚本,因此你的更改会自动生效。当你的脚本导入了其他 Python 模块并且你修改了这些模块时,情况会变得更复杂,因为这些模块不会随着%run
被重新加载。你可以使用深度重新加载来解决这个问题:
import myscript
from IPython.lib.deepreload import reload as dreload
dreload(myscript)
myscript
中导入的模块将会被重新加载。一个相关的 IPython 魔法命令是%autoreload
(你首先需要执行%load_ext autoreload
)。此命令会尝试自动重新加载交互命名空间中导入的模块,但并不总是成功。你可能需要显式地重新加载已更改的模块,使用reload(module)
(在 Python 2 中)或imp.reload(module)
(Python 3 中)。
IPython 笔记本
IPython 笔记本在高效的交互式工作流程中起着核心作用。它是代码编辑器和终端的精心设计的结合,将两者的优点融为一体,提供一个统一的环境。
你可以在笔记本的单元格中开始编写所有代码。你可以在同一地方编写、执行和测试代码,从而提高生产力。你可以在 Markdown 单元格中加入长注释,并使用 Markdown 标题来结构化你的笔记本。
一旦你的一部分代码足够成熟且不再需要进一步修改,你可以(并且应该)将它们重构为可重用的 Python 组件(函数、类和模块)。这将清理你的笔记本,并促进代码的未来重用。需要强调的是,不断将代码重构为可重用组件非常重要。IPython 笔记本当前不容易被第三方代码重用,并且它们并不针对这一点进行设计。笔记本适合进行初步分析和探索性研究,但它们不应该阻止你定期清理并将代码重构为 Python 组件。
笔记本的一个主要优势是,它们能为你提供一份文档,记录你在代码中所做的一切。这对可重复研究极为有用。笔记本保存在人类可读的 JSON 文档中,因此它们与 Git 等版本控制系统相对兼容。
集成开发环境
集成开发环境(IDEs)特别适用于经典的软件开发,但它们也可以用于交互式计算。一款好的 Python IDE 将强大的文本编辑器(例如,包含语法高亮和自动补全功能的编辑器)、IPython 终端和调试器结合在统一的环境中。
对于大多数平台,有多个商业和开源 IDE。Eclipse/PyDev 是一个流行的(尽管略显笨重的)开源跨平台环境。Spyder 是另一个开源 IDE,具有良好的 IPython 和 matplotlib 集成。PyCharm 是众多支持 IPython 的商业环境之一。
微软的 Windows IDE,Visual Studio,有一个名为 Python Tools for Visual Studio(PTVS)的开源插件。这个工具为 Visual Studio 带来了 Python 支持。PTVS 原生支持 IPython。你不一定需要付费版本的 Visual Studio;你可以下载一个免费的包,将 PTVS 和 Visual Studio 打包在一起。
还有更多…
以下是一些 Python IDE 的链接:
-
pydev.org
是 PyDev for Eclipse 的官方网站。 -
code.google.com/p/spyderlib/
是 Spyder,一个开源 IDE。 -
www.jetbrains.com/pycharm/ 是 PyCharm 的官方网站。
-
pytools.codeplex.com
是微软 Visual Studio 在 Windows 上的 PyTools。 -
code.google.com/p/pyscripter/
是 PyScripter 的官方网站。 -
www.iep-project.org 为 IEP,Python 的交互式编辑器
另见
-
学习分布式版本控制系统 Git 的基础 方法
-
使用 IPython 调试代码 的方法
学习分布式版本控制系统 Git 的基础
使用 分布式版本控制系统 在当今已经变得非常自然,如果你正在阅读本书,你可能已经在使用某种版本控制系统。然而,如果你还没有,请认真阅读这个方法。你应该始终为你的代码使用版本控制系统。
准备工作
著名的分布式版本控制系统包括 Git、Mercurial 和 Bazaar。在这一章中,我们选择了流行的 Git 系统。你可以从 git-scm.com
下载 Git 程序和 Git GUI 客户端。在 Windows 上,你也可以安装 msysGit(msysgit.github.io
)和 TortoiseGit(code.google.com/p/tortoisegit/
)。
注意
与 SVN 或 CVS 等集中式系统相比,分布式系统通常更受欢迎。分布式系统允许本地(离线)更改,并提供更灵活的协作系统。
支持 Git 的在线服务商包括 GitHub(github.com
)、Bitbucket(bitbucket.org
)、Google code(code.google.com
)、Gitorious(gitorious.org
)和 SourceForge(sourceforge.net
)。在撰写本书时,所有这些网站创建账户都是免费的。GitHub 提供免费的无限制公共仓库,而 Bitbucket 提供免费的无限制公共和私有仓库。GitHub 为学术用户提供特殊功能和折扣(github.com/edu
)。将你的 Git 仓库同步到这样的网站,在你使用多台计算机时特别方便。
你需要安装 Git(可能还需要安装 GUI)才能使用此方法(参见 git-scm.com/downloads
)。我们还建议你在以下这些网站之一创建账户。GitHub 是一个很受欢迎的选择,特别是因为它用户友好的网页界面和发达的社交功能。GitHub 还提供了非常好的 Windows 客户端(windows.github.com
)和 Mac OS X 客户端(mac.github.com
)。我们在本书中使用的大多数 Python 库都是在 GitHub 上开发的。
如何操作…
我们将展示两种初始化仓库的方法。
创建一个本地仓库
这种方法最适合开始在本地工作时使用。可以通过以下步骤实现:
-
在开始一个新项目或计算实验时,最先要做的就是在本地创建一个新文件夹:
$ mkdir myproject $ cd myproject
-
我们初始化一个 Git 仓库:
$ git init
-
让我们设置我们的姓名和电子邮件地址:
$ git config --global user.name "My Name" $ git config --global user.email "me@home"
-
我们创建一个新文件,并告诉 Git 跟踪它:
$ touch __init__.py $ git add __init__.py
-
最后,让我们创建我们的第一次提交:
$ git commit -m "Initial commit."
克隆一个远程仓库
当仓库需要与 GitHub 等在线提供商同步时,这种方法最好。我们来执行以下步骤:
-
我们在在线提供商的网页界面上创建了一个新的仓库。
-
在新创建项目的主页面上,我们点击克隆按钮并获取仓库 URL,然后在终端输入:
$ git clone /path/to/myproject.git
-
我们设置我们的姓名和电子邮件地址:
$ git config --global user.name "My Name" $ git config --global user.email "me@home"
-
让我们创建一个新文件并告诉 Git 跟踪它:
$ touch __init__.py $ git add __init__.py
-
我们创建我们的第一次提交:
$ git commit -m "Initial commit."
-
我们将本地更改推送到远程服务器:
$ git push origin
当我们拥有一个本地仓库(通过第一种方法创建)时,我们可以使用 git remote add
命令将其与远程服务器同步。
它是如何工作的…
当你开始一个新项目或新的计算实验时,在你的计算机上创建一个新文件夹。你最终会在这个文件夹中添加代码、文本文件、数据集和其他资源。分布式版本控制系统会跟踪你在项目发展过程中对文件所做的更改。它不仅仅是一个简单的备份,因为你对任何文件所做的每个更改都会保存相应的时间戳。你甚至可以随时恢复到之前的状态;再也不用担心破坏你的代码了!
具体来说,你可以随时通过执行提交来拍摄项目的快照。该快照包括所有已暂存(或已跟踪)的文件。你完全控制哪些文件和更改将被跟踪。使用 Git,你可以通过 git add
将文件标记为下次提交的暂存文件,然后用 git commit
提交你的更改。git commit -a
命令允许你提交所有已经被跟踪的文件的更改。
在提交时,你需要提供一个消息来描述你所做的更改。这样可以使仓库的历史更加详细和富有信息。
注
你应该多频繁地提交?
答案是非常频繁的。Git 只有在你提交更改时才会对你的工作负责。在两次提交之间发生的内容可能会丢失,所以你最好定期提交。此外,提交是快速且便宜的,因为它们是本地的;也就是说,它们不涉及与外部服务器的任何远程通信。
Git 是一个分布式版本控制系统;你的本地仓库不需要与外部服务器同步。然而,如果你需要在多台计算机上工作,或者如果你希望拥有远程备份,你应该进行同步。与远程仓库的同步可以通过 git push
(将你的本地提交发送到远程服务器)、git fetch
(下载远程分支和对象)或 git pull
(同步远程更改到本地仓库)来完成。
还有更多…
本教程中展示的简化工作流是线性的。然而,在实际操作中,Git 的工作流通常是非线性的;这就是分支的概念。我们将在下一个教程中描述这个概念,使用 Git 分支的典型工作流。
这里有一些关于 Git 的优秀参考资料:
-
实操教程,见于
try.github.io
-
Git 指导游,见于
gitimmersion.com
-
Atlassian Git 教程,见于 www.atlassian.com/git
-
Lars Vogel 编写的 Git 教程,见于 www.vogella.com/tutorials/Git/article.html
-
GitHub Git 教程,见于
git-lectures.github.io
-
针对科学家的 Git 教程,见于
nyuccl.org/pages/GitTutorial/
-
GitHub 帮助,见于
help.github.com
-
由 Scott Chacon 编写的 Pro Git,见于
git-scm.com
另见
- Git 分支的典型工作流 方案
Git 分支的典型工作流
像 Git 这样的分布式版本控制系统是为复杂的、非线性的工作流设计的,这类工作流通常出现在交互式计算和探索性研究中。一个核心概念是分支,我们将在本方案中讨论这一点。
准备工作
你需要在本地 Git 仓库中工作才能进行此方案(见前一方案,学习分布式版本控制系统 Git 的基础知识)。
如何执行…
-
我们创建一个名为
newidea
的新分支:$ git branch newidea
-
我们切换到这个分支:
$ git checkout newidea
-
我们对代码进行更改,例如,创建一个新文件:
$ touch newfile.py
-
我们添加此文件并提交我们的更改:
$ git add newfile.py $ git commit -m "Testing new idea."
-
如果我们对更改感到满意,我们将该分支合并到 master 分支(默认分支):
$ git checkout master $ git merge newidea
否则,我们删除该分支:
$ git checkout master $ git branch -d newidea
其他感兴趣的命令包括:
-
git status
:查看仓库的当前状态 -
git log
:显示提交日志 -
git branch
:显示现有的分支并突出当前分支 -
git diff
:显示提交或分支之间的差异
暂存
可能发生的情况是,当我们正在进行某项工作时,需要在另一个提交或另一个分支中进行其他更改。我们可以提交尚未完成的工作,但这并不理想。更好的方法是将我们正在工作的副本暂存到安全位置,以便稍后恢复所有未提交的更改。它是如何工作的:
-
我们使用以下命令保存我们的未提交更改:
$ git stash
-
我们可以对仓库进行任何操作:检出一个分支、提交更改、从远程仓库拉取或推送等。
-
当我们想要恢复未提交的更改时,输入以下命令:
$ git stash pop
我们可以在仓库中有多个暂存的状态。有关暂存的更多信息,请使用 git stash --help
。
它是如何工作的…
假设为了测试一个新想法,你需要对多个文件中的代码进行非琐碎的修改。你创建了一个新的分支,测试你的想法,并最终得到了修改过的代码版本。如果这个想法是死路一条,你可以切换回原始的代码分支。然而,如果你对这些修改感到满意,你可以合并它到主分支。
这种工作流的优势在于,主分支可以独立于包含新想法的分支进行发展。当多个协作者在同一个仓库中工作时,这特别有用。然而,这也是一种很好的习惯,尤其是当只有一个贡献者时。
合并并不总是一个简单的操作,因为它可能涉及到两个分歧的分支,且可能存在冲突。Git 会尝试自动解决冲突,但并不总是成功。在这种情况下,你需要手动解决冲突。
合并的替代方法是重基(rebasing),当你在自己的分支上工作时,如果主分支发生了变化,重基将非常有用。将你的分支重基到主分支上,可以让你将分支点移到一个更近期的点。这一过程可能需要你解决冲突。
Git 分支是轻量级对象。创建和操作它们的成本很低。它们是为了频繁使用而设计的。掌握所有相关概念和git
命令(尤其是 checkout
、merge
和 rebase
)非常重要。前面的食谱中包含了许多很好的参考资料。
还有更多……
许多人曾思考过有效的工作流。例如,一个常见但复杂的工作流,叫做 git-flow,可以在 nvie.com/posts/a-successful-git-branching-model/
中找到描述。然而,在小型和中型项目中,使用一个更简单的工作流可能更为适宜,比如 scottchacon.com/2011/08/31/github-flow.html
中描述的工作流。后者详细阐述了这个食谱中展示的简化示例。
与分支相关的概念是分叉(forking)。同一个仓库可以在不同的服务器上有多个副本。假设你想为存储在 GitHub 上的 IPython 代码做贡献。你可能没有权限修改他们的仓库,但你可以将其复制到你的个人账户中——这就是所谓的分叉。在这个副本中,你可以创建一个分支,并提出一个新功能或修复一个 bug。然后,你可以提出一个拉取请求(pull request),请求 IPython 的开发者将你的分支合并到他们的主分支。他们可以审核你的修改,提出建议,并最终决定是否合并你的工作(或不合并)。GitHub 就是围绕这个想法构建的,因此提供了一种清晰、现代的方式来协作开发开源项目。
在合并 pull 请求之前进行代码审查,有助于提高协作项目中的代码质量。当至少两个人审查任何一段代码时,合并错误代码或不正确代码的可能性就会降低。
当然,关于 Git 还有很多要说的。版本控制系统通常是复杂且功能强大的,Git 也不例外。掌握 Git 需要时间和实验。之前的食谱中包含了许多优秀的参考资料。
这里有一些关于分支和工作流的进一步参考资料:
-
可用的 Git 工作流,见于 www.atlassian.com/git/workflows
-
在
pcottle.github.io/learnGitBranching/
学习 Git 分支 -
NumPy 项目(以及其他项目)推荐的 Git 工作流,描述于
docs.scipy.org/doc/numpy/dev/gitwash/development_workflow.html
-
Fernando Perez 在 IPython 邮件列表上关于高效 Git 工作流的帖子,见于
mail.scipy.org/pipermail/ipython-dev/2010-October/006746.html
另见
- 学习分布式版本控制系统 Git 的基础 食谱
进行可重现的互动计算实验的十个技巧
在这篇食谱中,我们提出了十个技巧,帮助你进行高效且可重现的互动计算实验。这些更多是指导性建议,而非绝对规则。
首先,我们将展示如何通过减少重复性任务的时间、增加思考核心工作的时间来提高生产力。
其次,我们将展示如何在计算工作中实现更高的可重现性。值得注意的是,学术研究要求实验可重现,以便任何结果或结论可以被其他研究者独立验证。方法中的错误或操控往往会导致错误的结论,从而产生有害的后果。例如,在 2010 年 Carmen Reinhart 和 Kenneth Rogoff 发表的经济学研究论文《债务时期的增长》中,计算错误部分导致了一项存在全球影响力的 flawed 研究,对政策制定者产生了影响(请见 en.wikipedia.org/wiki/Growth_in_a_Time_of_Debt
)。
如何做…
-
仔细而一致地组织你的目录结构。具体的结构并不重要,重要的是在整个项目中保持文件命名规范、文件夹、子文件夹等的一致性。以下是一个简单的例子:
-
my_project/
-
data/
-
code/
-
common.py
-
idea1.ipynb
-
idea2.ipynb
-
-
figures/
-
notes/
-
README.md
-
-
-
使用轻量级标记语言(如 Markdown (
daringfireball.net/projects/markdown/
) 或 reStructuredText (reST))在文本文件中写下笔记。所有与项目、文件、数据集、代码、图形、实验室笔记本等相关的元信息应写入文本文件。 -
与此相关,在代码中记录所有非平凡的内容,包括注释、文档字符串等。你可以使用文档生成工具,如 Sphinx (
sphinx-doc.org
)。然而,在你工作时,不要花费太多时间记录不稳定和前沿的代码;它可能频繁变化,且你的文档很快就会过时。编写代码时要确保它易于理解,无需过多注释(为变量和函数命名合理,使用 Pythonic 编程模式等)。另请参见下一个章节,编写高质量的 Python 代码。 -
对于所有基于文本的文件,使用分布式版本控制系统,如 Git,但不要用于二进制文件(除非是非常小的文件且确实需要)。每个项目应使用一个版本库。将版本库同步到远程服务器上,使用免费或付费的托管服务提供商(如 GitHub 或 Bitbucket)或你自己的服务器(你的主办机构可能能够为你设置一个)。使用特定的系统来存储和共享二进制数据文件,例如 figshare.com 或 datadryad.org。
-
首先在 IPython 笔记本中编写所有交互式计算代码,只有在代码成熟和稳定时,才将其重构为独立的 Python 组件。
-
为了完全可重现性,确保记录下整个软件堆栈中所有组件的确切版本(操作系统、Python 发行版、模块等)。一种选择是使用虚拟环境,如 virtualenv 或 conda。
-
使用 Python 的原生 pickle 模块、dill (
pypi.python.org/pypi/dill
) 或 Joblib (pythonhosted.org/joblib/
) 缓存长时间计算的中间结果。Joblib 特别实现了一个 NumPy 友好的 memoize 模式(不要与 memorize 混淆),该模式允许你缓存计算密集型函数的结果。还可以查看 ipycache IPython 扩展 (github.com/rossant/ipycache
);它在笔记本中实现了一个%%cache
单元魔法。注意
在 Python 中保存持久化数据
对于纯内部使用,你可以使用 Joblib、NumPy 的
save
和savez
函数来保存数组,使用 pickle 来保存任何其他 Python 对象(尽量选择原生类型,如列表和字典,而非自定义类)。对于共享用途,建议使用文本文件来保存小型数据集(少于 10k 个数据点),例如,使用 CSV 格式存储数组,使用 JSON 或 YAML 格式存储高度结构化的数据。对于较大的数据集,你可以使用 HDF5(请参见第四章中的使用 HDF5 和 PyTables 操作大型数组和使用 HDF5 和 PyTables 操作大型异构表格的配方)。 -
在开发并测试大数据集上的算法时,先在数据的小部分上运行并进行比较,再转向整个数据集。
-
在批量运行任务时,使用并行计算来充分利用你的多核处理单元,例如,使用
IPython.parallel
、Joblib、Python 的多处理包,或任何其他并行计算库。 -
尽可能使用 Python 函数或脚本来自动化你的工作。对于用户公开的脚本,使用命令行参数,但在可能的情况下,更倾向于使用 Python 函数而非脚本。在 Unix 系统中,学习终端命令以提高工作效率。对于 Windows 或基于 GUI 的系统上的重复性任务,使用自动化工具,如 AutoIt(www.autoitscript.com/site/autoit/)或 AutoHotKey(www.autohotkey.com)。学习你经常使用程序的快捷键,或者创建你自己的快捷键。
提示
例如,你可以创建一个键盘快捷键来启动当前目录下的 IPython 笔记本服务器。以下链接包含一个 AutoHotKey 脚本,可以在 Windows 资源管理器中实现这一操作:
cyrille.rossant.net/start-an-ipython-notebook-server-in-windows-explorer/
它是如何工作的…
本文档中的建议最终旨在优化你的工作流程,涵盖人类时间、计算机时间和质量。使用一致的约定和结构来编写代码,可以让你更轻松地组织工作。记录所有内容可以节省每个人的时间,包括(最终)你自己!如果明天你被公交车撞了(我真心希望你不会),你应该确保你的替代者能够迅速接手,因为你的文档写得非常认真细致。(你可以在en.wikipedia.org/wiki/Bus_factor
找到更多关于“公交车因子”的信息。)
使用分布式版本控制系统和在线托管服务可以让你在多个地点协同工作同一个代码库,且无需担心备份问题。由于你可以回溯代码历史,因此几乎不可能无意间破坏代码。
IPython 笔记本是一个用于可重复交互计算的出色工具。它让你可以详细记录工作过程。此外,IPython 笔记本的易用性意味着你无需担心可重复性;只需在笔记本中进行所有交互式工作,将其放入版本控制中,并定期提交。不要忘记将你的代码重构为独立的可重用组件。
确保优化你在电脑前花费的时间。当处理一个算法时,经常会发生这样的循环:你做了一点修改,运行代码,获取结果,再做另一个修改,依此类推。如果你需要尝试很多修改,你应该确保执行时间足够快(不超过几秒钟)。在实验阶段,使用高级优化技术未必是最佳选择。你应该缓存结果,在数据子集上尝试算法,并以较短的时间运行模拟。当你想测试不同的参数值时,也可以并行启动批处理任务。
最后,极力避免重复任务。对于日常工作中频繁发生的任务,花时间将其自动化是值得的。虽然涉及 GUI 的任务更难自动化,但借助 AutoIt 或 AutoHotKey 等免费工具,还是可以实现自动化的。
还有更多…
以下是一些关于计算可重复性的参考资料:
-
高效的可重复科学工作流程,Trevor Bekolay 的演讲, 可在
bekolay.org/scipy2013-workflow/
找到。 -
可重复计算研究的十条简单规则,Sandve 等,PLoS 计算生物学,2013 年,可在
dx.doi.org/10.1371/journal.pcbi.1003285
找到。 -
Konrad Hinsen 的博客,
khinsen.wordpress.com
。 -
Software Carpentry 是一个为科学家举办研讨会的志愿者组织;这些研讨会涵盖了科学编程、交互式计算、版本控制、测试、可重复性和任务自动化等内容。你可以在
software-carpentry.org
找到更多信息。
另见
-
高效的交互式计算工作流程与 IPython 配方
-
编写高质量的 Python 代码 配方
编写高质量的 Python 代码
编写代码很容易,编写高质量的代码则要困难得多。质量不仅体现在实际代码(变量名、注释、文档字符串等)上,还包括架构(函数、模块、类等)。通常,设计一个良好的代码架构比实现代码本身更具挑战性。
在本配方中,我们将提供一些如何编写高质量代码的建议。这是学术界一个特别重要的话题,因为越来越多没有软件开发经验的科学家需要编程。
本教程末尾给出的参考资料包含了比我们在此提到的更多细节。
如何实现...
-
花时间认真学习 Python 语言。查看标准库中所有模块的列表——你可能会发现你已经实现的某些函数已经存在。学会编写 Pythonic 代码,不要将其他语言(如 Java 或 C++)的编程习惯直接翻译到 Python 中。
-
学习常见的 设计模式;这些是针对软件工程中常见问题的通用可复用解决方案。
-
在代码中使用断言(
assert
关键字)来防止未来的 bug (防御性编程)。 -
采用自下而上的方法开始编写代码;编写实现专注任务的独立 Python 函数。
-
不要犹豫定期重构你的代码。如果你的代码变得过于复杂,思考如何简化它。
-
尽量避免使用类。如果可以使用函数代替类,请选择函数。类只有在需要在函数调用之间存储持久状态时才有用。尽量让你的函数保持 纯净(没有副作用)。
-
一般来说,优先使用 Python 原生类型(列表、元组、字典和 Python collections 模块中的类型)而不是自定义类型(类)。原生类型能带来更高效、更可读和更具可移植性的代码。
-
在函数中选择关键字参数而不是位置参数。参数名称比参数顺序更容易记住。它们使你的函数自文档化。
-
小心命名你的变量。函数和方法的名称应以动词开头。变量名应描述它是什么。函数名应描述它做什么。命名的正确性至关重要,不能被低估。
-
每个函数都应该有一个描述其目的、参数和返回值的文档字符串,如下例所示。你还可以查看像 NumPy 这样的流行库中所采用的约定。重要的是在你的代码中保持一致性。你可以使用 Markdown 或 reST 等标记语言:
def power(x, n): """Compute the power of a number. Arguments: * x: a number. * n: the exponent. Returns: * c: the number x to the power of n. """ return x ** n
-
遵循(至少部分遵循)Guido van Rossum 的 Python 风格指南,也叫 Python 增强提案第 8 号(PEP8),可在 www.python.org/dev/peps/pep-0008/ 查阅。这是一本长篇指南,但它能帮助你编写可读性强的 Python 代码。它涵盖了许多小细节,如操作符之间的空格、命名约定、注释和文档字符串。例如,你会了解到将代码行限制在 79 个字符以内(如果有助于提高可读性,可以例外为 99 个字符)是一个良好的实践。这样,你的代码可以在大多数情况下(如命令行界面或移动设备上)正确显示,或者与其他文件并排显示。或者,你可以选择忽略某些规则。总的来说,在涉及多个开发人员的项目中,遵循常见的指南是有益的。
-
你可以通过pep8 Python 包自动检查代码是否符合 PEP8 中的大多数编码风格规范。使用
pip install pep8
进行安装,并通过pep8 myscript.py
执行。 -
使用静态代码分析工具,如 Pylint(www.pylint.org)。它可以让你静态地找到潜在的错误或低质量的代码,即在不运行代码的情况下进行检查。
-
使用空行来避免代码杂乱(参见 PEP8)。你也可以通过显著的注释来标记一个较长 Python 模块的各个部分,例如:
# Imports # ------- import numpy # Utility functions # ----------------- def fun(): pass
-
一个 Python 模块不应包含超过几百行代码。一个模块的代码行数过多可能意味着你需要将其拆分成多个模块。
-
将重要的项目(包含数十个模块)组织为子包,例如:
-
core/
-
io/
-
utils/
-
__init__.py
-
-
看看主要的 Python 项目是如何组织的。例如,IPython 的代码就很有条理,按照具有明确职责的子包层次结构进行组织。阅读这些代码本身也非常有启发性。
-
学习创建和分发新 Python 包的最佳实践。确保你了解 setuptools、pip、wheels、virtualenv、PyPI 等工具。此外,我们强烈建议你认真研究 conda(
conda.pydata.org
),这是由 Continuum Analytics 开发的一个强大且通用的打包系统。打包是 Python 中一个混乱且快速发展的领域,因此请只阅读最新的参考资料。在更多内容…部分中有一些参考资料。
它是如何工作的…
编写可读的代码意味着其他人(或者几年后你自己)会更快地理解它,也更愿意使用它。这也有助于 bug 追踪。
模块化代码也更容易理解和重用。将程序的功能实现为独立的函数,并按照包和模块的层次结构进行组织,是实现高质量代码的绝佳方式。
使用函数而不是类可以更容易地保持代码的松耦合。意大利面式代码(Spaghetti code)真的很难理解、调试和重用。
在处理一个新项目时,可以在自下而上的方法和自上而下的方法之间交替进行。自下而上的方法让你在开始思考程序的整体架构之前先对代码有一定的经验。然而,仍然要确保通过思考组件如何协同工作来知道自己最终的目标。
还有更多内容…
已经有很多关于如何编写优美代码的文章——请参阅以下参考资料。你可以找到许多关于这个主题的书籍。在接下来的教程中,我们将介绍确保代码不仅看起来漂亮,而且能够按预期工作的标准技术:单元测试、代码覆盖率和持续集成。
这里有一些参考资料:
-
《Python 厨房秘籍》,由 David Beazley 和 Brian K. Jones 编写,包含许多 Python 3 高级配方,可在
shop.oreilly.com/product/0636920027072.do
获取 -
《Python 旅行者指南!》,可在
docs.python-guide.org/en/latest/
获取 -
维基百科上的设计模式,详见
en.wikipedia.org/wiki/Software_design_pattern
-
Python 设计模式,描述于
github.com/faif/python-patterns
-
Tahoe-LAFS 编码标准,详见
tahoe-lafs.org/trac/tahoe-lafs/wiki/CodingStandards
-
《如何成为一名伟大的软件开发者》,由 Peter Nixey 编写,可在
peternixey.com/post/83510597580/how-to-be-a-great-software-developer
阅读 -
《为什么你应该用尽可能少的功能编写有缺陷的软件》,由 Brian Granger 主讲,可在 www.youtube.com/watch?v=OrpPDkZef5I 观看
-
《程序包指南》,可在
guide.python-distribute.org
获取 -
Python 包装用户指南,可在
python-packaging-user-guide.readthedocs.org
获取
另见
-
《进行可重复交互式计算实验的十个技巧》 配方
-
使用 nose 编写单元测试 配方
使用 nose 编写单元测试
手动测试对确保我们的软件按预期工作并且不包含关键性错误至关重要。然而,手动测试存在严重限制,因为每次更改代码时,可能会引入新的缺陷。我们不可能在每次提交时都手动测试整个程序。
现如今,自动化测试已经成为软件工程中的标准实践。在这个配方中,我们将简要介绍自动化测试的重要方面:单元测试、测试驱动开发、测试覆盖率和持续集成。遵循这些实践对于开发高质量的软件是绝对必要的。
准备工作
Python 有一个原生的单元测试模块(unittest
),你可以直接使用。还有其他第三方单元测试包,如 py.test 或 nose,我们在这里选择了 nose。nose 使得编写测试套件变得稍微容易一些,并且拥有一个外部插件库。除非用户自己想运行测试套件,否则他们不需要额外的依赖。你可以通过 pip install nose
安装 nose。
如何实现...
在这个示例中,我们将为一个从 URL 下载文件的函数编写单元测试。即使在没有网络连接的情况下,测试套件也应能够运行并成功通过。我们通过使用一个模拟的 HTTP 服务器来欺骗 Python 的 urllib
模块,从而解决这一问题。
注意
本食谱中使用的代码片段是为 Python 3 编写的。要使它们在 Python 2 中运行,需要做一些更改,我们在代码中已标明了这些更改。Python 2 和 Python 3 的版本都可以在本书的网站上找到。
你可能对requests
模块也感兴趣;它为 HTTP 请求提供了一个更简单的 API(docs.python-requests.org/en/latest/
)。
-
我们创建了一个名为
datautils.py
的文件,里面包含以下代码:In [1]: %%writefile datautils.py # Version 1. import os from urllib.request import urlopen # Python 2: use urllib2 def download(url): """Download a file and save it in the current folder. Return the name of the downloaded file.""" # Get the filename. file = os.path.basename(url) # Download the file unless it already exists. if not os.path.exists(file): with open(file, 'w') as f: f.write(urlopen(url).read()) return file Writing datautils.py
-
我们创建了一个名为
test_datautils.py
的文件,里面包含以下代码:In [2]: %%writefile test_datautils.py # Python 2: use urllib2 from urllib.request import (HTTPHandler, install_opener, build_opener, addinfourl) import os import shutil import tempfile from io import StringIO # Python 2: use StringIO from datautils import download TEST_FOLDER = tempfile.mkdtemp() ORIGINAL_FOLDER = os.getcwd() class TestHTTPHandler(HTTPHandler): """Mock HTTP handler.""" def http_open(self, req): resp = addinfourl(StringIO('test'), '', req.get_full_url(), 200) resp.msg = 'OK' return resp def setup(): """Install the mock HTTP handler for unit tests.""" install_opener(build_opener(TestHTTPHandler)) os.chdir(TEST_FOLDER) def teardown(): """Restore the normal HTTP handler.""" install_opener(build_opener(HTTPHandler)) # Go back to the original folder. os.chdir(ORIGINAL_FOLDER) # Delete the test folder. shutil.rmtree(TEST_FOLDER) def test_download1(): file = download("http://example.com/file.txt") # Check that the file has been downloaded. assert os.path.exists(file) # Check that the file contains the contents of # the remote file. with open(file, 'r') as f: contents = f.read() print(contents) assert contents == 'test' Writing test_datautils.py
-
现在,为了启动测试,我们在终端中执行以下命令:
$ nosetests . Ran 1 test in 0.042s OK
-
我们的第一个单元测试通过了!现在,让我们添加一个新的测试。我们在
test_datautils.py
文件的末尾添加一些代码:In [4]: %%writefile test_datautils.py -a def test_download2(): file = download("http://example.com/") assert os.path.exists(file) Appending to test_datautils.py
-
我们使用
nosetests
命令再次启动测试:$ nosetests .E ERROR: test_datautils.test_download2 Traceback (most recent call last): File "datautils.py", line 12, in download with open(file, 'wb') as f: IOError: [Errno 22] invalid mode ('wb') or filename: '' Ran 2 tests in 0.032s FAILED (errors=1)
-
第二个测试失败了。在实际应用中,我们可能需要调试程序。这应该不难,因为错误被隔离在一个单独的测试函数中。在这里,通过检查回溯错误和代码,我们发现错误是由于请求的 URL 没有以正确的文件名结尾。因此,推断的文件名
os.path.basename(url)
为空。我们通过以下方法来修复这个问题:将datautils.py
中的download
函数替换为以下函数:In [6]: %%file datautils.py # Version 2. import os from urllib.request import urlopen # Python 2: use urllib2 def download(url): """Download a file and save it in the current folder. Return the name of the downloaded file.""" # Get the filename. file = os.path.basename(url) # Fix the bug, by specifying a fixed filename if the # URL does not contain one. if not file: file = 'downloaded' # Download the file unless it already exists. if not os.path.exists(file): with open(file, 'w') as f: f.write(urlopen(url).read()) return file Overwriting datautils.py
-
最后,让我们再次运行测试:
$ nosetests .. Ran 2 tests in 0.036s OK
提示
默认情况下,nosetests
会隐藏标准输出(除非发生错误)。如果你希望标准输出显示出来,可以使用nosetests --nocapture
。
它是如何工作的...
每个名为xxx.py
的 Python 模块应该有一个对应的test_xxx.py
模块。这个测试模块包含执行并测试xxx.py
模块中功能的函数(单元测试)。
根据定义,一个给定的单元测试必须专注于一个非常具体的功能。所有单元测试应该是完全独立的。将程序编写为一组经过充分测试、通常是解耦的单元,迫使你编写更易于维护的模块化代码。
然而,有时你的模块函数在运行之前需要一些预处理工作(例如,设置环境、创建数据文件或设置 Web 服务器)。单元测试框架可以处理这些事情;只需编写setup()
和teardown()
函数(称为fixtures),它们将分别在测试模块开始和结束时被调用。请注意,测试模块运行前后的系统环境状态应该完全相同(例如,临时创建的文件应在teardown
中删除)。
这里,datautils.py
模块包含一个名为 download
的函数,该函数接受一个 URL 作为参数,下载文件并将其保存到本地。这个模块还带有一个名为 test_datautils.py
的测试模块。你应该在你的程序中使用相同的约定(test_<modulename>
作为 modulename
模块的测试模块)。这个测试模块包含一个或多个以 test_
为前缀的函数。nose 就是通过这种方式自动发现项目中的单元测试。nose 也接受其他类似的约定。
提示
nose 会运行它在你的项目中找到的所有测试,但你当然可以更精确地控制要运行的测试。键入 nosetests --help
可以获取所有选项的列表。你也可以查阅 nose.readthedocs.org/en/latest/testing.html
上的文档。
测试模块还包含 setup
和 teardown
函数,这些函数会被 nose 自动识别为测试夹具。在 setup
函数中创建一个自定义的 HTTP 处理程序对象。该对象会捕获所有 HTTP 请求,即使是那些具有虚构 URL 的请求。接着,setup
函数会进入一个测试文件夹(该文件夹是通过 tempfile
模块创建的),以避免下载的文件和现有文件之间可能的冲突。一般来说,单元测试不应该留下任何痕迹;这也是我们确保测试完全可重复的方式。同样,teardown
函数会删除测试文件夹。
提示
在 Python 3.2 及更高版本中,你还可以使用 tempfile.TemporaryDirectory
来创建一个临时目录。
第一个单元测试从一个虚拟 URL 下载文件,并检查它是否包含预期的内容。默认情况下,如果单元测试没有抛出异常,则视为通过。这时,assert
语句就非常有用,如果语句为 False
,则会抛出异常。nose 还提供了方便的例程和装饰器,用于精确地确定某个单元测试期望通过或失败的条件(例如,它应该抛出某个特定的异常才算通过,或者它应该在 X 秒内完成等)。
提示
NumPy 提供了更多方便的类似 assert 的函数(请参见 docs.scipy.org/doc/numpy/reference/routines.testing.html
)。这些函数在处理数组时特别有用。例如,np.testing.assert_allclose(x, y)
会断言 x
和 y
数组几乎相等,精度可以指定。
编写完整的测试套件需要时间。它对你代码的架构提出了严格(但良好的)约束。这是一次真正的投资,但从长远来看总是值得的。此外,知道你的项目有一个完整的测试套件支持,真是让人放心。
首先,从一开始就考虑单元测试迫使你思考模块化架构。对于一个充满相互依赖的单体程序,编写单元测试是非常困难的。
其次,单元测试使得发现和修复 bug 变得更加容易。如果在程序中引入了更改后,某个单元测试失败,隔离并重现 bug 就变得非常简单。
第三,单元测试帮助你避免回归,即已修复的 bug 在后续版本中悄然重现。当你发现一个新 bug 时,应该为它编写一个特定的失败单元测试。修复它时,让这个测试通过。现在,如果这个 bug 后来再次出现,这个单元测试将会失败,你就可以立即解决它。
假设你编写了一个多层次的复杂程序,每一层的基础上都有一个基于第 n层的n+1层。有了一套成功的单元测试作为第 n层的保障,你就能确信它按预期工作。当你在处理n+1层时,你可以专注于这一层,而不必总是担心下面一层是否有效。
单元测试并不是全部,它只关注独立的组件。为了确保程序中各组件的良好集成,还需要进一步的测试层级。
还有更多...
单元测试是一个广泛的话题,我们在这个配方中仅仅触及了表面。这里提供了一些进一步的信息。
测试覆盖率
使用单元测试是好事,但测量测试覆盖率更好:它量化了我们的代码有多少被你的测试套件覆盖。Ned Batchelder 的coverage模块(nedbatchelder.com/code/coverage/
)正是做这件事。它与 nose 非常好地集成。
首先,使用pip install coverage
安装 coverage。然后使用以下命令运行你的测试套件:
$ nosetests --with-cov --cover-package datautils
该命令指示 nose 仅为datautils
包启动测试套件并进行覆盖率测量。
coveralls.io服务将测试覆盖率功能引入持续集成服务器(参见单元测试与持续集成部分)。它与 GitHub 无缝集成。
带有单元测试的工作流
注意我们在这个例子中使用的特定工作流。在编写download
函数后,我们创建了第一个通过的单元测试。然后我们创建了第二个失败的测试。我们调查了问题并修复了函数,第二个测试通过了。我们可以继续编写越来越复杂的单元测试,直到我们确信该函数在大多数情况下按预期工作。
提示
运行nosetests --pdb
以在失败时进入 Python 调试器。这对于快速找出单元测试失败的原因非常方便。
这就是测试驱动开发,它要求在编写实际代码之前编写单元测试。这个工作流迫使我们思考代码的功能和使用方式,而不是它是如何实现的。
单元测试与持续集成
养成每次提交时运行完整测试套件的好习惯。实际上,我们甚至可以通过 持续集成 完全透明且自动地做到这一点。我们可以设置一个服务器,在每次提交时自动在云端运行我们的测试套件。如果某个测试失败,我们会收到一封自动邮件,告诉我们问题所在,以便我们修复。
有很多持续集成系统和服务:Jenkins/Hudson、drone.io
、stridercd.com
、travis-ci.org
等等。其中一些与 GitHub 项目兼容。例如,要在 GitHub 项目中使用 Travis CI,可以在 Travis CI 上创建账户,将 GitHub 项目与此账户关联,然后在仓库中添加一个 .travis.yml
文件,其中包含各种设置(有关更多详情,请参见以下参考资料)。
总结来说,单元测试、代码覆盖率和持续集成是所有重大项目应遵循的标准实践。
这里有一些参考资料:
-
未经测试的代码是坏代码:企业软件交付中的测试自动化,由 Martin Aspeli 编写,详见 www.deloittedigital.com/eu/blog/untested-code-is-broken-code-test-automation-in-enterprise-software-deliver
-
Travis CI 在 Python 中的文档,见
about.travis-ci.org/docs/user/languages/python/
使用 IPython 调试代码
调试是软件开发和交互式计算的一个不可或缺的部分。一种常见的调试技术是在代码中的各个地方放置 print
语句。谁没做过这个呢?它可能是最简单的解决方案,但肯定不是最有效的(它是穷人版的调试器)。
IPython 完美适配调试,集成的调试器非常易于使用(实际上,IPython 只是提供了一个友好的界面来访问原生的 Python 调试器 pdb
)。特别是,IPython 调试器中支持 Tab 补全。本节内容描述了如何使用 IPython 调试代码。
注意
早期版本的 IPython 笔记本不支持调试器,也就是说,调试器可以在 IPython 终端和 Qt 控制台中使用,但在笔记本中无法使用。这个问题在 IPython 1.0 中得到了解决。
如何做到这一点...
在 Python 中有两种非互斥的调试方式。在事后调试模式下,一旦抛出异常,调试器会立即进入代码,这样我们就可以调查导致异常的原因。在逐步调试模式下,我们可以在断点处停止解释器,并逐步恢复执行。这个过程使我们能够在代码执行时仔细检查变量的状态。
两种方法其实可以同时使用;我们可以在事后调试模式下进行逐步调试。
事后调试模式
当在 IPython 中抛出异常时,执行 %debug
魔法命令启动调试器并逐步进入代码。并且,%pdb on
命令告诉 IPython 一旦抛出异常,就自动启动调试器。
一旦进入调试器,你可以访问几个特殊命令,下面列出的是最重要的一些:
-
p varname
打印一个变量的值 -
w
显示你在堆栈中的当前位置 -
u
在堆栈中向上跳 -
d
在堆栈中向下跳 -
l
显示你当前位置周围的代码行 -
a
显示当前函数的参数
调用堆栈包含代码执行当前位置的所有活动函数的列表。你可以轻松地在堆栈中上下导航,检查函数参数的值。虽然这个模式使用起来相当简单,但它应该能帮助你解决大部分问题。对于更复杂的问题,可能需要进行逐步调试。
步骤调试
你有几种方法来启动逐步调试模式。首先,为了在代码中某处设置断点,插入以下命令:
import pdb; pdb.set_trace()
其次,你可以使用以下命令从 IPython 运行一个脚本:
%run -d -b extscript.py:20 script
这个命令在调试器控制下运行 script.py
文件,并在 extscript.py
的第 20 行设置一个断点(该文件在某个时刻由 script.py
导入)。最后,一旦进入调试器,你就可以开始逐步调试。
步骤调试就是精确控制解释器的执行过程。从脚本开始或者从断点处,你可以使用以下命令恢复解释器的执行:
-
s
执行当前行并尽快停下来(逐步调试,也就是最细粒度的执行模式) -
n
继续执行直到到达当前函数中的下一行 -
r
继续执行直到当前函数返回 -
c
继续执行直到到达下一个断点 -
j 30
将你带到当前文件的第 30 行
你可以通过 b
命令或 tbreak
(临时断点)动态添加断点。你还可以清除所有或部分断点,启用或禁用它们,等等。你可以在 docs.python.org/3/library/pdb.html
找到调试器的完整细节。
还有更多...
要使用 IPython 调试代码,你通常需要先通过 IPython 执行它,例如使用 %run
。然而,你可能并不总是有一个简单的方法来做到这一点。例如,你的程序可能通过一个自定义的命令行 Python 脚本运行,可能是由一个复杂的 bash 脚本启动,或者集成在一个 GUI 中。在这些情况下,你可以在代码的任何位置嵌入一个 IPython 解释器(由 Python 启动),而不是用 IPython 运行整个程序(如果你只需要调试代码的一小部分,使用整个程序可能会显得过于复杂)。
要将 IPython 嵌入到你的程序中,只需在代码中的某个地方插入以下命令:
from IPython import embed
embed()
当你的 Python 程序执行到这段代码时,它会暂停并在该特定位置启动一个交互式的 IPython 终端。你将能够检查所有局部变量,执行你想要的任何代码,并且在恢复正常执行之前,可能会调试你的代码。
提示
rfoo,访问链接 code.google.com/p/rfoo/
,让你可以远程检查和修改正在运行的 Python 脚本的命名空间。
GUI 调试器
大多数 Python 集成开发环境(IDE)都提供图形化调试功能(参见 使用 IPython 的高效交互式计算工作流)。有时候,GUI 比命令行调试器更为方便。我们还可以提到 Winpdb(winpdb.org),一个图形化、平台无关的 Python 调试器。
第三章:掌握笔记本
本章将涵盖以下主题:
-
在笔记本中使用 IPython 块教授编程
-
使用 nbconvert 将 IPython 笔记本转换为其他格式
-
在笔记本工具栏中添加自定义控件
-
自定义笔记本中的 CSS 样式
-
使用交互式小部件——笔记本中的钢琴
-
在笔记本中创建自定义 JavaScript 小部件——一个用于 pandas 的电子表格编辑器
-
从笔记本实时处理网络摄像头图像
介绍
在本章中,我们将了解笔记本的许多功能,包括 IPython 2.0 带来的交互式小部件。由于在前几章中我们仅了解了基本功能,这里将深入探讨笔记本的架构。
什么是笔记本?
笔记本于 2011 年发布,比 IPython 创建晚了十年。它的发展有着悠久且复杂的历史,费尔南多·佩雷斯在他的博客上对其进行了很好的总结,blog.fperez.org/2012/01/ipython-notebook-historical.html
。受到数学软件如 Maple、Mathematica 或 Sage 的启发,笔记本极大地推动了 IPython 的流行。
通过将代码、文本、图像、图表、超链接和数学方程式混合在一个文档中,笔记本为交互式计算带来了可复现性。正确使用笔记本时,它能够彻底改变科学计算中的工作流程。在笔记本出现之前,人们不得不在文本编辑器和交互式提示之间来回切换;而现在,人们可以在一个统一的环境中保持专注。
笔记本不仅是一个工具,还是一个强大且健壮的架构。此外,这个架构大多是语言独立的,因此它不再仅限于 Python。笔记本定义了一套消息传递协议、API 和 JavaScript 代码,其他语言也可以使用这些协议和代码。实际上,我们现在已经看到非 Python 内核能够与笔记本进行交互,如 IJulia、IHaskell、IRuby 等。
在 2014 年 7 月的 SciPy 大会上,IPython 开发者甚至宣布他们决定将项目拆分为以下两部分:
-
新的Jupyter 项目将实现所有语言独立部分:笔记本、消息传递协议和整体架构。欲了解更多详情,请访问
jupyter.org
。 -
IPython 将作为 Python 内核的名称。
在本书中,我们并不做语义上的区分,而是使用 IPython 一词来指代整个项目(包括语言独立部分和 Python 内核)。
笔记本生态系统
笔记本被表示为JavaScript 对象 表示法(JSON)文档。JSON 是一种语言独立的、基于文本的文件格式,用于表示结构化文档。因此,笔记本可以被任何编程语言处理,并且可以转换为其他格式,如 Markdown、HTML、LaTeX/PDF 等。
一个围绕笔记本的生态系统正在建立,我们可以期待在不久的将来看到越来越多的使用案例。例如,Google 正在努力将 IPython 笔记本引入 Google Drive,以实现协作数据分析。此外,笔记本还被用来创建幻灯片、教学材料、博客文章、研究论文,甚至是书籍。实际上,本书完全是用笔记本写成的。
IPython 2.0 引入了交互式小部件(widgets)。这些小部件使 Python 和浏览器之间的联系更加紧密。我们现在可以创建实现 IPython 内核与浏览器之间双向通信的应用程序。此外,任何 JavaScript 交互式库原则上都可以集成到笔记本中。例如,D3.js
JavaScript 可视化库现在被多个 Python 项目使用,以便为笔记本提供交互式可视化功能。我们可能会在不久的将来看到这些交互式功能的许多有趣应用。
IPython 笔记本的架构
IPython 实现了一个双进程模型,包含一个内核和一个客户端。客户端是提供给用户发送 Python 代码到内核的接口。内核执行代码并将结果返回给客户端以供显示。在读-评估-打印循环(REPL)术语中,内核实现了评估,而客户端则实现了读取和打印过程。
客户端可以是 Qt 控件(如果我们运行 Qt 控制台),也可以是浏览器(如果我们运行笔记本)。在笔记本中,内核一次接收完整的单元,因此并不认识笔记本的存在。包含笔记本的线性文档与底层内核之间有很强的解耦。这是一个很强的限制,可能会限制某些可能性,但它仍然带来了极大的简洁性和灵活性。
整个架构中的另一个基本假设是,每个笔记本至多只能连接一个内核。然而,IPython 3.0 提供了选择任何笔记本的语言内核的可能性。
在考虑笔记本的新使用场景时,牢记这些要点非常重要。
在笔记本中,除了 Python 内核和浏览器客户端外,还有一个基于Tornado的 Python 服务器(www.tornadoweb.org)。该进程为 HTML 基础的笔记本界面提供服务。
不同进程之间的所有通信过程都建立在ZeroMQ(或ZMQ)消息协议之上(zeromq.org
)。笔记本通过WebSocket(一种基于 TCP 的协议,现代浏览器中已实现)与底层内核进行通信。
官方支持 IPython 2.x 笔记本的浏览器如下:
-
Chrome ≥ 13
-
Safari ≥ 5
-
Firefox ≥ 6
该笔记本还应在 Internet Explorer ≥ 10 上运行。这些要求本质上是 WebSocket 的要求。
将多个客户端连接到一个内核
在笔记本中,在一个单元格输入 %connect_info
会显示我们连接新客户端(例如 Qt 控制台)到底层内核所需的信息:
In [1]: %connect_info
{
"stdin_port": 53978,
"ip": "127.0.0.1",
"control_port": 53979,
"hb_port": 53980,
"signature_scheme": "hmac-sha256",
"key": "053...349",
"shell_port": 53976,
"transport": "tcp",
"iopub_port": 53977
}
Paste the above JSON code into a file, and connect with:
$> ipython <app> --existing <file>
or, if you are local, you can connect with just:
$> ipython <app> --existing kernel-6e0...b92.json
or even just:
$> ipython <app> --existing
if this is the most recent IPython session you have started.
这里,<app>
是 console
、qtconsole
或 notebook
。
甚至可以让内核和客户端运行在不同的机器上。你可以在 IPython 文档中找到运行公共笔记本服务器的说明,详见 ipython.org/ipython-doc/dev/notebook/public_server.html#running-a-public-notebook-server
。
笔记本中的安全性
有可能有人在 IPython 笔记本中插入恶意代码。由于笔记本中可能包含单元格输出中的隐藏 JavaScript 代码,因此理论上,当用户打开笔记本时,恶意代码有可能在不被察觉的情况下执行。
因此,IPython 2.0 引入了一个安全模型,在该模型中,笔记本中的 HTML 和 JavaScript 代码可以被标记为受信任或不受信任。用户生成的输出始终是受信任的。然而,当用户首次打开一个现有笔记本时,已存在的输出被视为不受信任。
安全模型基于每个笔记本中的加密签名。这个签名是通过每个用户拥有的私钥生成的。
你可以在接下来的部分中找到有关安全模型的更多参考资料。
参考文献
以下是一些关于笔记本架构的参考资料:
-
IPython 双进程模型,详见
ipython.org/ipython-doc/stable/overview.html#decoupled-two-process-model
-
该笔记本的文档,请参阅
ipython.org/ipython-doc/stable/interactive/notebook.html
-
笔记本中的安全性,详见
ipython.org/ipython-doc/dev/notebook/security.html
-
笔记本服务器,详见
ipython.org/ipython-doc/dev/interactive/public_server.html
-
IPython 消息协议,详见
ipython.org/ipython-doc/dev/development/messaging.html
-
关于如何为笔记本编写自定义内核的教程,详见
andrew.gibiansky.com/blog/ipython/ipython-kernels/
以下是一些(主要是实验性的)非 Python 语言内核,用于笔记本:
-
IJulia,请参阅
github.com/JuliaLang/IJulia.jl
-
IRuby,请参阅
github.com/isotope11/iruby
-
IHaskell,请参阅
github.com/gibiansky/IHaskell
-
IGo,可在
github.com/takluyver/igo
找到 -
IScala,可在
github.com/mattpap/IScala
找到
在 IPython 块中进行编程教学
IPython 笔记本不仅是科学研究和数据分析的工具,还是一款很棒的教学工具。在这个示例中,我们展示了一个简单有趣的 Python 库,用于教授编程概念:IPython Blocks(可在ipythonblocks.org
找到)。这个库允许你或你的学生创建五颜六色的方块网格。你可以改变每个方块的颜色和大小,甚至可以让网格动起来。通过这个工具,你可以演示许多基本的技术概念。这个工具的可视化特点使得学习过程更加高效且富有吸引力。
在这个示例中,我们将主要执行以下任务:
-
用动画演示矩阵乘法
-
将图像显示为方块网格
这个示例部分灵感来源于nbviewer.ipython.org/gist/picken19/b0034ba7ec690e89ea79
中的一个例子。
准备工作
你需要为这个示例安装 IPython Blocks。你只需在终端中输入pip install ipythonblocks
即可。请注意,你也可以通过在 IPython 笔记本中在命令前加上!
来执行这个命令。
In [1]: !pip install ipythonblocks
在这个示例的最后部分,你还需要安装 Pillow,相关文档可在pillow.readthedocs.org/en/latest/
找到;更多说明请参见第十一章,图像和音频处理。如果你使用 Anaconda,可以在终端执行conda install pillow
。
最后,你需要从本书网站下载Portrait数据集(github.com/ipython-books/cookbook-data
)并在当前目录下解压。你也可以使用你自己的图像进行实验!
如何操作...
-
首先,我们导入一些模块,代码如下:
In [1]: import time from IPython.display import clear_output from ipythonblocks import BlockGrid, colors
-
现在,我们创建一个五列五行的方块网格,并将每个方块填充为紫色:
In [2]: grid = BlockGrid(width=5, height=5, fill=colors['Purple']) grid.show()
-
我们可以通过二维索引来访问单独的方块。这演示了 Python 中的索引语法。我们还可以用
:
(冒号)来访问整个行或列。每个方块都是由 RGB 颜色表示的。这个库附带了一个便捷的颜色字典,将 RGB 元组分配给标准颜色名称,如下所示:In [3]: grid[0,0] = colors['Lime'] grid[-1,0] = colors['Lime'] grid[:,-1] = colors['Lime'] grid.show()
-
现在,我们将演示矩阵乘法,这是线性代数中的一个基本概念。我们将表示两个(
n,n
)矩阵,A
(青色)和B
(青柠色),并与C = A B
(黄色)对齐。为了实现这一点,我们采用一个小技巧,创建一个大小为(2n+1,2n+1
)的大白网格。矩阵A
、B
和C
只是网格部分的视图。In [4]: n = 5 grid = BlockGrid(width=2*n+1, height=2*n+1, fill=colors['White']) A = grid[n+1:,:n] B = grid[:n,n+1:] C = grid[n+1:,n+1:] A[:,:] = colors['Cyan'] B[:,:] = colors['Lime'] C[:,:] = colors['Yellow'] grid.show()
-
让我们转向矩阵乘法本身。我们对所有行和列进行循环,并突出显示在矩阵乘法过程中相乘的 A 和 B 中对应的行和列。我们结合使用 IPython 的
clear_output()
方法与grid.show()
和time.sleep()
(暂停)来实现动画,如下所示:In [5]: for i in range(n): for j in range(n): # We reset the matrix colors. A[:,:] = colors['Cyan'] B[:,:] = colors['Lime'] C[:,:] = colors['Yellow'] # We highlight the adequate rows # and columns in red. A[i,:] = colors['Red'] B[:,j] = colors['Red'] C[i,j] = colors['Red'] # We animate the grid in the loop. clear_output() grid.show() time.sleep(0.25)
-
最后,我们将使用 IPython Blocks 显示一张图片。我们使用
Image.open()
导入 JPG 图片,并通过getdata()
获取数据,具体如下:In [6]: from PIL import Image imdata = Image.open('data/photo.jpg').getdata()
-
现在,我们创建一个
BlockGrid
实例,设置适当的行数和列数,并将每个块的颜色设置为图片中相应像素的颜色。我们使用较小的块大小,并去除块之间的线条,如下所示:In [7]: rows, cols = imdata.size grid = BlockGrid(width=rows, height=cols, block_size=4, lines_on=False) for block, rgb in zip(grid, imdata): block.rgb = rgb grid.show()
还有更多...
正如本食谱中所演示的,笔记本是一个理想的教育平台,适用于所有层次的教育活动。
该库是在 IPython 2.0 引入交互式笔记本功能之前开发的。现在,我们可以期待更多交互式的发展。
使用 nbconvert 将 IPython 笔记本转换为其他格式
一个 IPython 笔记本会以 JSON 文本文件的形式保存。该文件包含笔记本的所有内容:文本、代码和输出。matplotlib 图形会以 base64 字符串的形式编码在笔记本内,导致笔记本文件既独立又可能较大。
注意
JSON 是一种人类可读、基于文本的开放标准格式,可以表示结构化数据。尽管源自 JavaScript,它是语言独立的。其语法与 Python 字典有一些相似之处。JSON 可以在多种语言中解析,包括 JavaScript 和 Python(Python 标准库中的 json
模块)。
IPython 提供了一个名为 nbconvert 的工具,可以将笔记本转换为其他格式:纯文本、Markdown、HTML、LaTeX/PDF,甚至是使用 reveal.js
库的幻灯片。你可以在 nbconvert 文档中找到有关不同支持格式的更多信息。
在本食谱中,我们将了解如何操作笔记本的内容,并将其转换为其他格式。
准备工作
你需要安装 pandoc,可以在 johnmacfarlane.net/pandoc/
获取,这是一个用于将文件从一种标记语言转换为另一种标记语言的工具。
要将笔记本转换为 PDF,你需要一个 LaTeX 发行版,可以在 latex-project.org/ftp.html
找到。你还需要从本书网站下载 Notebook 数据集(github.com/ipython-books/cookbook-data
),并将其解压到当前目录中。
在 Windows 上,你可能需要安装 pywin32
包。如果你使用 Anaconda,可以通过 conda install pywin32
来安装它。
如何操作...
-
让我们打开
data
文件夹中的test
笔记本。笔记本只是一个普通的文本文件(JSON),所以我们以文本模式(r
模式)打开它,如下所示:In [1]: with open('data/test.ipynb', 'r') as f: contents = f.read() print(len(contents)) 3787
这是
test.ipynb
文件的摘录:{ "metadata": { "celltoolbar": "Edit Metadata", "name": "", "signature": "sha256:50db..." }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { ... "source": [ "# First chapter" ] }, ... ], "metadata": {} } ] }
-
现在我们已经将笔记本加载为字符串,让我们使用
json
模块进行解析,如下所示:In [3]: import json nb = json.loads(contents)
-
让我们来看看笔记本字典中的键:
In [4]: print(nb.keys()) print('nbformat ' + str(nb['nbformat']) + '.' + str(nb['nbformat_minor'])) [u'nbformat', u'nbformat_minor', u'worksheets', u'metadata'] nbformat 3.0
注意
笔记本格式的版本在
nbformat
和nbformat_minor
中指示。笔记本格式的向后不兼容更改预计将在未来的 IPython 版本中出现。此食谱已在 IPython 2.x 分支和笔记本格式 v3 中进行测试。 -
主要字段是
worksheets
,默认情况下只有一个。一个工作表包含单元格列表和一些元数据。worksheets
字段在未来版本的笔记本格式中可能会消失。让我们来看看一个工作表的内容:In [5]: nb['worksheets'][0].keys() Out[5]: [u'cells', u'metadata']
-
每个单元格都有类型、可选的元数据、一些内容(文本或代码)、可能有一个或多个输出以及其他信息。让我们看看一个 Markdown 单元格和一个代码单元格:
In [6]: nb['worksheets'][0]['cells'][1] Out[6]: {u'cell_type': u'markdown', u'metadata': {u'my_field': [u'value1', u'2405']}, u'source': [u"Let's write ...:\n", ...]} In [7]: nb['worksheets'][0]['cells'][2] Out[7]: {u'cell_type': u'code', u'collapsed': False, u'input': [u'import numpy as np\n', ...], u'language': u'python', u'metadata': {}, u'outputs': [ {u'metadata': {}, u'output_type': u'display_data', u'png': u'iVB...mCC\n', u'prompt_number': 1}]}
-
一旦被解析,笔记本将表示为一个 Python 字典。因此,在 Python 中操作它是非常方便的。在这里,我们按照如下方式计算 Markdown 和代码单元格的数量:
In [8]: cells = nb['worksheets'][0]['cells'] nm = len([cell for cell in cells if cell['cell_type'] == 'markdown']) nc = len([cell for cell in cells if cell['cell_type'] == 'code']) print(("There are {nm} Markdown cells and " "{nc} code cells.").format(nm=nm, nc=nc)) There are 2 Markdown cells and 1 code cells.
-
让我们仔细看看包含 matplotlib 图形的单元格的图像输出:
In [9]: png = cells[2]['outputs'][0]['png'] cells[2]['outputs'][0] Out[9]: {u'metadata': {}, u'output_type': u'display_data', u'png': u'iVBORwoAAAANSUhE...ErAAAElTkQmCC\n'}
-
通常,可以有零个、一个或多个输出。此外,每个输出可以有多个表示。在这里,matplotlib 图形有 PNG 表示(base64 编码的图像)和文本表示(图形的内部表示)。
-
现在,我们将使用 nbconvert 将我们的文本笔记本转换为其他格式。此工具可以从命令行使用。请注意,nbconvert 的 API 在未来版本中可能会发生变化。在这里,我们将笔记本转换为 HTML 文档,如下所示:
In [10]: !ipython nbconvert --to html data/test.ipynb [NbConvertApp] Writing 187617 bytes to test.html
-
让我们在
<iframe>
中显示此文档(这是一个在笔记本中显示外部 HTML 文档的小窗口):In [11]: from IPython.display import IFrame IFrame('test.html', 600, 200)
-
我们还可以将笔记本转换为 LaTeX 和 PDF。为了指定文档的标题和作者,我们需要扩展默认的 LaTeX 模板。首先,我们创建一个名为
mytemplate.tplx
的文件,扩展 nbconvert 提供的默认article.tplx
模板。我们指定作者和标题块的内容如下:In [12]: %%writefile mytemplate.tplx ((*- extends 'article.tplx' -*)) ((* block author *)) \author{Cyrille Rossant} ((* endblock author *)) ((* block title *)) \title{My document} ((* endblock title *)) Writing mytemplate.tplx
-
然后,我们可以通过指定自定义模板来运行 nbconvert,如下所示:
In [13]: !ipython nbconvert --to latex --template mytemplate data/test.ipynb !pdflatex test.tex [NbConvertApp] PDF successfully created
我们使用 nbconvert 将笔记本转换为 LaTeX,然后使用
pdflatex
(随 LaTeX 发行版一起提供)将 LaTeX 文档编译为 PDF。以下截图展示了笔记本的 PDF 版本:
它是如何工作的...
正如我们在这个食谱中看到的,.ipynb
文件包含了笔记本的结构化表示。这种 JSON 文件可以在 Python 中轻松解析和操作。
nbconvert 是一个用于将笔记本转换为其他格式的工具。转换可以通过多种方式进行自定义。在这里,我们使用 jinja2
模板包扩展了现有模板。你可以在 nbconvert 的文档中找到更多信息。
还有更多...
有一个免费的在线服务,nbviewer,它允许我们在云中动态地将 IPython 笔记本渲染为 HTML。其理念是,我们提供一个原始笔记本(JSON 格式)的 URL 给 nbviewer,便可获得一个渲染后的 HTML 输出。nbviewer 的主页(nbviewer.ipython.org
)包含了一些示例。
该服务由 IPython 开发者维护,并托管在 Rackspace(www.rackspace.com)上。
这里有一些更多的参考资料:
-
nbconvert 的文档,参见
ipython.org/ipython-doc/dev/interactive/nbconvert.html
-
一个 nbconvert 转换示例的列表,参见
github.com/ipython/nbconvert-examples
-
Wikipedia 上的 JSON 文章,网址为
en.wikipedia.org/wiki/JSON
在笔记本工具栏中添加自定义控件
HTML 笔记本的 CSS 和 JavaScript 可以通过~/.ipython/profile_default/static/custom
目录中的文件进行自定义,其中~
代表你的主目录,default
是你的 IPython 配置文件。
在本食谱中,我们将使用这些自定义选项在笔记本工具栏中添加一个新的按钮,能够线性地重新编号所有代码单元。
如何操作...
-
首先,我们将直接在笔记本中注入 JavaScript 代码。这对于测试目的很有用,或者如果我们不希望更改是永久性的,JavaScript 代码将仅在该笔记本中加载。为了做到这一点,我们可以使用如下的
%%javascript
单元魔法:In [1]: %%javascript // This function allows us to add buttons // to the notebook toolbar. IPython.toolbar.add_buttons_group([ { // The button's label. 'label': 'renumber all code cells', // The button's icon. // See a list of Font-Awesome icons here: // http://fortawesome.github.io/Font- // Awesome/icons/ 'icon': 'icon-list-ol', // The callback function. 'callback': function () { // We retrieve the lists of all cells. var cells = IPython.notebook.get_cells(); // We only keep the code cells. cells = cells.filter(function(c) { return c instanceof IPython.CodeCell; }) // We set the input prompt of all code // cells. for (var i = 0; i < cells.length; i++) { cells[i].set_input_prompt(i + 1); } } }]);
-
运行上述代码单元将在工具栏中添加一个按钮,如下图所示。点击此按钮会自动更新所有代码单元的提示编号。
添加重新编号工具栏按钮
-
为了使这些更改成为永久性的,也就是说,要在当前配置文件下的每个笔记本中添加此按钮,我们可以打开
~/.ipython/profile_default/static/custom/custom.js
文件,并添加以下代码:$([IPython.events]).on('app_initialized.NotebookApp', function(){ // Copy the JavaScript code (in step 1) here. });
上述代码将自动加载到笔记本中,并且重新编号按钮将出现在当前配置文件下每个笔记本的顶部。
还有更多...
允许我们将按钮添加到笔记本工具栏的 IPython 笔记本 JavaScript API 在撰写本文时仍不稳定。它可能随时发生变化,且文档不完整。本食谱仅在 IPython 2.0 中测试过。你仍然可以在此页面找到一个非正式且部分的 API 文档:ipjsdoc.herokuapp.com
。
我们应当期待未来会有更稳定的 API。
另请参见
- 在笔记本中自定义 CSS 样式配方
在笔记本中自定义 CSS 样式
在本配方中,我们展示了如何在笔记本界面和导出的 HTML 笔记本中自定义 CSS。
准备工作
你需要对 CSS3 有一定了解才能进行此配方。你可以在网上找到许多教程(参见本配方末尾的参考资料)。
你还需要从书籍网站下载Notebook数据集(链接:ipython-books.github.io
),并将其解压到当前目录。
如何操作...
-
首先,我们创建一个新的 IPython 配置文件,以避免使默认配置文件杂乱无章,具体步骤如下:
In [1]: !ipython profile create custom_css
-
在 Python 中,我们检索此配置文件(
~/.ipython
)的路径以及custom.css
文件的路径(默认为空)。In [2]: dir = !ipython locate profile custom_css dir = dir[0] In [3]: import os csspath = os.path.realpath(os.path.join( dir, 'static/custom/custom.css')) In [4]: csspath Out[4]: '~\.ipython\profile_custom_css\static\ custom\custom.css'
-
我们现在可以在这里编辑此文件。我们更改背景颜色、代码单元的字体大小、某些单元的边框,并在编辑模式中突出显示选定的单元。
In [5]: %%writefile {csspath} body { /* Background color for the whole notebook. */ background-color: #f0f0f0; } /* Level 1 headers. */ h1 { text-align: right; color: red; } /* Code cells. */ div.input_area > div.highlight > pre { font-size: 10px; } /* Output images. */ div.output_area img { border: 3px #ababab solid; border-radius: 8px; } /* Selected cells. */ div.cell.selected { border: 3px #ababab solid; background-color: #ddd; } /* Code cells in edit mode. */ div.cell.edit_mode { border: 3px red solid; background-color: #faa; } Overwriting C:\Users\Cyrille\.ipython\profile_custom_css\static\custom\custom.css
使用
custom_css
配置文件打开笔记本(使用ipython notebook --profile=custom_css
命令)会得到如下的自定义样式:交互式笔记本界面中的自定义 CSS
-
我们还可以将此样式表与 nbconvert 一起使用。只需将笔记本转换为静态 HTML 文档,并将
custom.css
文件复制到当前目录。在这里,我们使用的是从书籍网站下载的测试笔记本(参见准备工作):In [6]: !cp {csspath} custom.css !ipython nbconvert --to html data/test.ipynb [NbConvertApp] Writing 187617 bytes to test.html
-
这是该 HTML 文档的显示效果:
In [7]: from IPython.display import IFrame IFrame('test.html', 600, 650)
还有更多...
这里有一些关于 CSS 的教程和参考资料:
-
w3schools 上的 CSS 教程,链接:www.w3schools.com/css/DEFAULT.asp
-
Mozilla 开发者网络上的 CSS 教程,链接:
developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_started
-
Matthias Bussonnier 撰写的关于如何自定义笔记本 CSS 的博客文章,链接:
nbviewer.ipython.org/github/Carreau/posts/blob/master/Blog1.ipynb
另请参见
- 在笔记本工具栏中添加自定义控件配方
使用交互式小部件——笔记本中的钢琴
从 IPython 2.0 开始,我们可以将交互式小部件放入笔记本中,以创建与 Python 内核交互的丰富 GUI 应用程序。IPython 提供了一组丰富的图形控件,如按钮、滑块和下拉菜单。我们可以完全控制它们的位置和外观。我们可以将不同的小部件组合成复杂的布局。我们甚至可以像在下一个配方中所展示的那样,从头开始创建我们自己的交互式小部件——在笔记本中创建自定义 Javascript 小部件——一个用于 pandas 的电子表格编辑器。
在本配方中,我们将展示 IPython 2.0+交互式小部件 API 提供的多种可能性。我们将在笔记本中创建一个非常基础的钢琴。
准备工作
你需要从本书网站下载 Piano 数据集(http://ipython-books.github.io)。该数据集包含在 archive.org
上获得的合成钢琴音符(CC0 1.0 公共领域授权)。它可以在 archive.org/details/SynthesizedPianoNotes
下载。
如何实现...
-
让我们导入一些模块,如下所示:
In [1]: import numpy as np import os from IPython.display import (Audio, display, clear_output) from IPython.html import widgets from functools import partial
-
要创建一个钢琴,我们将为每个音符绘制一个按钮。用户点击按钮时,播放相应的音符。这是通过如下方式显示
<audio>
元素来实现的:In [2]: dir = 'data/synth' In [3]: # This is the list of notes. notes = 'C,C#,D,D#,E,F,F#,G,G#,A,A#,B,C'.split(',') In [4]: def play(note, octave=0): """This function displays an HTML Audio element that plays automatically when it appears.""" f = os.path.join(dir, "piano_{i}.mp3".format(i=note+12*octave)) clear_output() display(Audio(filename=f, autoplay=True))
-
我们将把所有按钮放置在一个容器小部件内。在 IPython 2.0 中,小部件可以按层次结构组织。一个常见的使用场景是将多个小部件组织在一个特定的布局中。在这里,
piano
将包含 12 个按钮,代表 12 个音符:In [5]: piano = widgets.ContainerWidget()
注意
用于创建容器小部件(如水平或垂直框)的 API 在 IPython 3.0 中发生了变化。有关更多详细信息,请参考 IPython 的文档。
-
我们创建第一个小部件:一个滑块控件,用于指定音阶(此处为 0 或 1):
In [6]: octave_slider = widgets.IntSliderWidget() octave_slider.max = 1 octave_slider
-
现在,我们创建按钮。有几个步骤。首先,我们为每个音符实例化一个
ButtonWidget
对象。然后,我们指定一个callback()
函数,用于在给定的音阶(由当前的音阶滑块值决定)上播放相应的音符(由索引给出)。最后,我们设置每个按钮的 CSS,特别是白色或黑色的颜色。In [7]: buttons = [] for i, note in enumerate(notes): button = widgets.ButtonWidget(description=note) def on_button_clicked(i, _): play(i+1, octave_slider.value) button.on_click(partial(on_button_clicked, i)) button.set_css({ 'width': '30px', 'height': '60px', 'padding': '0', 'color': ('black', 'white')['#' in note], 'background': ('white', 'black')['#' in note], 'border': '1px solid black', 'float': 'left'}) buttons.append(button)
-
最后,我们将所有小部件安排在容器内。
piano
容器包含按钮,而主容器(container
)包含滑块和钢琴。这可以通过以下方式实现:In [8]: piano.children = buttons In [9]: container = widgets.ContainerWidget() container.children = [octave_slider, piano]
-
默认情况下,小部件在容器内垂直组织。在这里,音阶滑块将位于钢琴上方。在钢琴中,我们希望所有音符水平排列。我们通过将默认的
vbox
CSS 类替换为hbox
类来实现这一点。下图显示了 IPython 笔记本中的钢琴:In [10]: display(container) piano.remove_class('vbox') piano.add_class('hbox')
它是如何工作的...
IPython 小部件由丰富的对象表示,这些对象在 Python 内核和浏览器之间共享。一个小部件包含特殊的属性,称为 trait 属性。例如,SliderWidget
的 value
trait 属性动态且自动地与用户在笔记本滑块中选择的值相关联。
这个链接是双向的。在 Python 中更改这个属性会更新笔记本中的滑块。
小部件的位置由容器小部件和 CSS 类控制。你可以在文档中找到更多信息。
这种架构使得在笔记本中创建丰富的图形应用程序成为可能,且这些应用程序由 Python 代码支持。
还有更多内容...
- 小部件示例在
nbviewer.ipython.org/github/ipython/ipython/blob/master/examples/Interactive%20Widgets/Index.ipynb
另见
- 在笔记本中创建自定义的 JavaScript 小部件——一个用于 pandas 的电子表格编辑器 食谱
在笔记本中创建自定义的 JavaScript 小部件——一个用于 pandas 的电子表格编辑器
我们之前介绍了 IPython 笔记本 2.0 的新交互式功能。在这个食谱中,我们通过展示如何超越 IPython 2.0 提供的现有小部件,深入探讨了这个主题。具体来说,我们将创建一个自定义的基于 JavaScript 的小部件,它与 Python 内核进行通信。
具体来说,我们将在 IPython 笔记本中创建一个基本的交互式类 Excel 数据网格编辑器,兼容 pandas 的 DataFrame
。从一个 DataFrame
对象开始,我们将能够在笔记本中的 GUI 内进行编辑。该编辑器基于 Handsontable
JavaScript 库(handsontable.com
)。也可以使用其他 JavaScript 数据网格编辑器。
准备工作
你需要 IPython 2.0+ 和 Handsontable JavaScript 库才能执行此食谱。以下是将此 JavaScript 库加载到 IPython 笔记本中的说明:
-
首先,访问
github.com/handsontable/jquery-handsontable/tree/master/dist
。 -
然后,下载
jquery.handsontable.full.css
和jquery.handsontable.full.js
,并将这两个文件放入~\.ipython\profile_default\static\custom\
。 -
在此文件夹中,在
custom.js
中添加以下行:require(['/static/custom/jquery.handsontable.full.js']);
-
在此文件夹中,在
custom.css
中添加以下行:@import "/static/custom/jquery.handsontable.full.css"
-
现在,刷新笔记本!
如何操作...
-
让我们导入以下几个函数和类:
In [1]: from IPython.html import widgets from IPython.display import display from IPython.utils.traitlets import Unicode
-
我们创建一个新小部件。
value
特性将包含整个表格的 JSON 表示。由于 IPython 2.0 的小部件机制,这个特性将在 Python 和 JavaScript 之间进行同步。In [2]: class HandsonTableWidget(widgets.DOMWidget): _view_name = Unicode('HandsonTableView', sync=True) value = Unicode(sync=True)
-
现在,我们为小部件编写 JavaScript 代码。负责同步的三个重要函数如下:
-
render
用于小部件初始化 -
update
用于 Python 到 JavaScript 的更新 -
handle_table_change
用于 JavaScript 到 Python 的更新In [3]: %%javascript var table_id = 0; require(["widgets/js/widget"], function(WidgetManager){ // Define the HandsonTableView var HandsonTableView = IPython.DOMWidgetView.extend({ render: function(){ // Initialization: creation of the HTML elements // for our widget. // Add a <div> in the widget area. this.$table = $('<div />') .attr('id', 'table_' + (table_id++)) .appendTo(this.$el); // Create the Handsontable table. this.$table.handsontable({ }); }, update: function() { // Python --> Javascript update. // Get the model's JSON string, and parse it. var data = $.parseJSON(this.model.get('value')); // Give it to the Handsontable widget. this.$table.handsontable({data: data}); return HandsonTableView.__super__. update.apply(this); }, // Tell Backbone to listen to the change event // of input controls. events: {"change": "handle_table_change"}, handle_table_change: function(event) { // Javascript --> Python update. // Get the table instance. var ht = this.$table.handsontable('getInstance'); // Get the data, and serialize it in JSON. var json = JSON.stringify(ht.getData()); // Update the model with the JSON string. this.model.set('value', json); this.touch(); }, }); // Register the HandsonTableView with the widget manager. WidgetManager.register_widget_view( 'HandsonTableView', HandsonTableView); });
-
-
现在,我们有了一个已经可以使用的同步表格小部件。然而,我们希望将其与 pandas 集成。为此,我们在
DataFrame
实例周围创建一个轻量级的包装器。我们创建了两个回调函数,用于将 pandas 对象与 IPython 小部件同步。GUI 中的更改将自动触发DataFrame
的更改,但反之则不行。如果我们在 Python 中更改了DataFrame
实例,我们需要重新显示小部件:In [4]: from io import StringIO import numpy as np import pandas as pd In [5]: class HandsonDataFrame(object): def __init__(self, df): self._df = df self._widget = HandsonTableWidget() self._widget.on_trait_change( self._on_data_changed, 'value') self._widget.on_displayed(self._on_displayed) def _on_displayed(self, e): # DataFrame ==> Widget (upon initialization) json = self._df.to_json(orient='values') self._widget.value = json def _on_data_changed(self, e, val): # Widget ==> DataFrame (called every time the # user changes a value in the widget) buf = StringIO(val) self._df = pd.read_json(buf, orient='values') def to_dataframe(self): return self._df def show(self): display(self._widget)
-
现在,让我们测试一下所有这些!我们首先创建一个随机的
DataFrame
实例:In [6]: data = np.random.randint(size=(3, 5), low=100, high=900) df = pd.DataFrame(data) df Out[6]: 352 201 859 322 352 326 519 848 802 642 171 480 213 619 192
-
我们将其包装在
HandsonDataFrame
中,并显示如下:In [7]: ht = HandsonDataFrame(df) ht.show()
-
现在我们可以交互式地更改值,它们将在 Python 中相应地改变:
In [8]: ht.to_dataframe() Out[8]: 352 201 859 322 352 326 519 848 1024 642 171 480 213 619 192
它是如何工作的……
让我们简要解释一下 IPython 2.0+ 中交互式 Python-JavaScript 通信的架构。
该实现遵循模型-视图-控制器(MVC)设计模式,这是图形界面应用程序中常用的模式。在后端(Python 内核)有一个模型,保存一些数据。在前端(浏览器),有一个或多个该模型的视图。视图与模型动态同步。当 Python 端的模型属性发生变化时,JavaScript 端的视图也会发生变化,反之亦然。我们可以实现 Python 和 JavaScript 函数来响应模型的变化。这些变化通常是由用户操作触发的。
在 Python 中,动态属性实现为特性(traits)。这些特殊的类属性在更新时会自动触发回调函数。在 JavaScript 中,使用的是 Backbone.js
MVC 库。Python 和浏览器之间的通信是通过Comms完成的,这是一种在 IPython 中的特殊通信协议。
要创建一个新的小部件,我们需要创建一个继承自 DOMWidget
的类。然后,我们定义可以在 Python 和 JavaScript 之间同步的特性属性,如果传递 sync=True
给特性构造函数。我们可以注册回调函数来响应特性变化(无论是来自 Python 还是 JavaScript),使用 widget.on_trait_change(callback, trait_name)
。callback()
函数可以具有以下签名之一:
-
callback()
-
callback(trait_name)
-
callback(trait_name, new_value)
-
callback(trait_name, old_value, new_value)
在 JavaScript 中,render()
函数会在初始化时创建单元格小部件区域的 HTML 元素。update()
方法允许我们响应后端(Python)中模型的变化。此外,我们还可以使用 Backbone.js
来响应前端(浏览器)中的变化。通过用 {"change": "callback"}
事件扩展小部件,我们告诉 Backbone.js
在 HTML 输入控件变化时,调用 callback()
JavaScript 函数。这就是我们在这里响应用户触发的操作的方式。
还有更多……
以下是此概念验证的改进方式:
-
只同步变化,而不是每次都同步整个数组(这里使用的方法在大型表格上会比较慢)
-
避免在每次更改时重新创建一个新的
DataFrame
实例,而是在原地更新相同的DataFrame
实例 -
支持命名列
-
隐藏包装器,即使得
DataFrame
在 notebook 中的默认丰富表示为HandsonDataFrame
-
将一切实现为一个易于使用的扩展
以下是关于 IPython notebook 2.0+ 中小部件架构的一些参考资料:
-
关于自定义小部件的官方示例,网址:
nbviewer.ipython.org/github/ipython/ipython/tree/master/examples/Interactive%20Widgets
-
Wikipedia 上的 MVC 模式,网址:
en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
-
Backbone.js,网址:
backbonejs.org/
-
Backbone.js
课程,网址:www.codeschool.com/courses/anatomy-of-backbonejs -
IPEP 21:小部件消息(comms),网址:
github.com/ipython/ipython/wiki/IPEP-21%3A-Widget-Messages
-
IPEP 23:IPython 小部件,网址:
github.com/ipython/ipython/wiki/IPEP-23%3A-Backbone.js-Widgets
另见
- 实时处理笔记本中的摄像头图像教程
从笔记本实时处理摄像头图像
在这个教程中,我们展示了如何让笔记本与 Python 内核进行双向通信。
具体来说,我们将使用 HTML5 的<video>
元素从浏览器获取摄像头视频流,并通过 IPython notebook 2.0+的交互功能实时传递给 Python。然后,我们将在 Python 中使用边缘检测器(在 scikit-image 中实现)处理图像,并实时在笔记本中显示它。
这个教程的大部分代码来自 Jason Grout 的示例,网址:github.com/jasongrout/ipywidgets
。
准备工作
你需要安装 Pillow 和 scikit-image 库来实现这个教程。(更多信息,请参见第十一章,图像和音频处理。)
你还需要一个支持 HTML5 捕获 API 的现代浏览器。你可以在dev.w3.org/2011/webrtc/editor/getusermedia.html
找到规范。
如何实现...
-
我们需要导入几个模块,如下所示:
In [1]: from IPython.html.widgets import DOMWidget from IPython.utils.traitlets import (Unicode, Bytes, Instance) from IPython.display import display from skimage import io, filter, color import urllib import base64 from PIL import Image from io import BytesIO # to change in Python 2 import numpy as np from numpy import array, ndarray import matplotlib.pyplot as plt
-
我们定义了两个函数,用于将图像转换为和从 base64 字符串转换。这种转换是进程之间传递二进制数据的常见方法(在我们的案例中是浏览器和 Python 之间):
In [2]: def to_b64(img): imgdata = BytesIO() pil = Image.fromarray(img) pil.save(imgdata, format='PNG') imgdata.seek(0) return urllib.parse.quote( base64.b64encode( imgdata.getvalue())) In [3]: def from_b64(b64): im = Image.open(BytesIO( base64.b64decode(b64))) return array(im)
-
我们定义了一个 Python 函数,它将在实时处理网络摄像头图像时工作。它接受并返回一个 NumPy 数组。这个函数应用了一个边缘检测器,使用的是 scikit-image 中的
roberts()
函数,如下所示:In [4]: def process_image(image): img = filter.roberts(image[:,:,0]/255.) return (255-img*255).astype(np.uint8)
-
现在,我们创建一个自定义小部件来处理浏览器和 Python 之间视频流的双向通信:
In [5]: class Camera(DOMWidget): _view_name = Unicode('CameraView', sync=True) # This string contains the base64-encoded raw # webcam image (browser -> Python). imageurl = Unicode('', sync=True) # This string contains the base64-encoded processed # webcam image(Python -> browser). imageurl2 = Unicode('', sync=True) # This function is called whenever the raw webcam # image is changed. def _imageurl_changed(self, name, new): head, data = new.split(',', 1) if not data: return # We convert the base64-encoded string # to a NumPy array. image = from_b64(data) # We process the image. image = process_image(image) # We convert the processed image # to a base64-encoded string. b64 = to_b64(image) self.imageurl2 = 'data:image/png;base64,' + b64
-
下一步是为小部件编写 JavaScript 代码。由于代码较长,这里仅突出重要部分。完整代码可以在本书网站上找到:
In [6]: %%javascript var video = $('<video>')[0]; var canvas = $('<canvas>')[0]; var canvas2 = $('<img>')[0]; [...] require(["widgets/js/widget"], function(WidgetManager){ var CameraView = IPython.DOMWidgetView.extend({ render: function(){ var that = this; // We append the HTML elements. setTimeout(function() { that.$el.append(video). append(canvas). append(canvas2);}, 200); // We initialize the webcam. [...] // We initialize the size of the canvas. video.addEventListener('canplay', function(ev){ if (!streaming) { height = video.videoHeight / ( video.videoWidth/width); video.setAttribute('width', width); video.setAttribute('height', height); [...] streaming = true; } }, false); // Play/Pause functionality. var interval; video.addEventListener('play', function(ev){ // We get the picture every 100ms. interval = setInterval(takepicture, 100); }) video.addEventListener('pause', function(ev){ clearInterval(interval); }) // This function is called at each time step. // It takes a picture and sends it to the model. function takepicture() { canvas.width = width; canvas.height = height; canvas2.width = width; canvas2.height = height; video.style.display = 'none'; canvas.style.display = 'none'; // We take a screenshot from the webcam feed and // we put the image in the first canvas. canvas.getContext('2d').drawImage(video, 0, 0, width, height); // We export the canvas image to the model. that.model.set('imageurl', canvas.toDataURL('image/png')); that.touch(); } }, update: function(){ // This function is called whenever Python modifies // the second (processed) image. We retrieve it and // we display it in the second canvas. var img = this.model.get('imageurl2'); canvas2.src = img; return CameraView.__super__.update.apply(this); } }); // Register the view with the widget manager. WidgetManager.register_widget_view('CameraView', CameraView); });
-
最后,我们创建并显示小部件,如下所示:
In [7]: c = Camera() display(c)
它是如何工作的…
让我们解释一下这个实现的原理。该模型有两个属性:从浏览器传入的(原始)图像和从 Python 传出的(处理后的)图像。每 100 毫秒,JavaScript 都会捕获网络摄像头画面(在<video>
HTML 元素中),并将其复制到第一个画布上。画布图像被序列化为 base64 格式,并赋值给第一个模型属性。然后,Python 函数_imageurl_changed()
被调用。图像被反序列化,通过 scikit-image 进行处理,并重新序列化。接着,第二个属性由 Python 修改,并设置为序列化后的处理图像。最后,JavaScript 中的update()
函数会反序列化处理后的图像,并将其显示在第二个画布中。
还有更多…
通过从 Python 而非浏览器捕获网络摄像头图像,可以大大提高这个例子的速度。在这里,瓶颈可能来自于每次时间步长中从浏览器到 Python 以及反向传输的两次操作。
使用诸如OpenCV
或SimpleCV
等库从 Python 捕获网络摄像头图像会更高效。然而,由于这些库可能难以安装,因此让浏览器直接访问网络摄像头设备要简单得多。
另见
- 在笔记本中创建自定义 JavaScript 小部件——用于 pandas 的电子表格编辑器配方
第四章:性能分析与优化
本章将涵盖以下主题:
-
在 IPython 中评估语句所花费的时间
-
使用 cProfile 和 IPython 轻松分析代码
-
使用
line_profiler
逐行分析代码的性能 -
使用
memory_profiler
分析代码的内存使用情况 -
理解 NumPy 的内部机制,以避免不必要的数组复制
-
使用 NumPy 的跨步技巧
-
使用跨步技巧实现高效的滚动平均算法
-
在 NumPy 中进行高效的数组选择
-
使用内存映射处理超大的 NumPy 数组
-
使用 HDF5 和 PyTables 操作大数组
-
使用 HDF5 和 PyTables 操作大规模异构数据表
引言
尽管 Python 通常被认为是(有点不公平地)较慢的语言,但通过使用正确的方法,实际上可以实现非常好的性能。这就是本章和下一章的目标。本章将介绍如何评估(分析)程序变慢的原因,以及如何利用这些信息来优化代码,使其更加高效。下一章将讨论一些更高级的高性能计算方法,只有在本章中描述的方法不足以解决问题时才应采用。
本章的内容分为三个部分:
-
时间和内存性能分析:评估代码的性能
-
NumPy 优化:更高效地使用 NumPy,特别是在处理大数组时
-
内存映射与数组:为超大数组的外存计算实现内存映射技术,特别是使用 HDF5 文件格式
在 IPython 中评估语句所花费的时间
%timeit
魔法命令和 %%timeit
单元格魔法命令(适用于整个代码单元)允许你快速评估一个或多个 Python 语句所花费的时间。对于更全面的性能分析,你可能需要使用本章后续介绍的更高级方法。
如何实现...
我们将估算计算所有正整数的倒数平方和,直到给定的 n
所需的时间:
-
让我们定义
n
:In [1]: n = 100000
-
让我们在纯 Python 中计时这段计算:
In [2]: %timeit sum([1\. / i**2 for i in range(1, n)]) 10 loops, best of 3: 131 ms per loop
-
现在,我们使用
%%timeit
单元格魔法命令来计时将相同的计算分成两行代码:In [3]: %%timeit s = 0. for i in range(1, n): s += 1\. / i**2 10 loops, best of 3: 137 ms per loop
-
最后,让我们计时使用 NumPy 版本的计算:
In [4]: import numpy as np In [5]: %timeit np.sum(1\. / np.arange(1., n) ** 2) 1000 loops, best of 3: 1.71 ms per loop
它是如何工作的...
%timeit
命令接受多个可选参数。其中一个参数是语句评估的次数。默认情况下,这个次数会自动选择,以确保 %timeit
命令在几秒钟内返回。然而,你也可以通过 -r
和 -n
参数直接指定这个次数。在 IPython 中输入 %timeit?
以获取更多信息。
%%timeit
单元格魔法命令还接受一个可选的设置语句(位于 %%timeit
的同一行),该语句会被执行,但不计时。所有在此语句中创建的变量都可以在单元格内部使用。
还有更多内容...
如果你不在 IPython 交互式会话中,可以使用 timeit.timeit()
。这个函数定义在 Python 的 timeit
模块中,用于基准测试存储在字符串中的 Python 语句。IPython 的 %timeit
魔法命令是 timeit()
的一个方便封装,适用于交互式会话。有关 timeit
模块的更多信息,请参阅 docs.python.org/3/library/timeit.html
。
另请参见
-
使用 cProfile 和 IPython 轻松分析代码 配方
-
逐行分析代码性能的 line_profiler 配方
使用 cProfile 和 IPython 轻松分析你的代码
%timeit
魔法命令通常很有用,但当你需要详细了解代码中哪些部分占用了最多执行时间时,它的功能略显有限。这个魔法命令更适用于基准测试(比较不同版本函数的执行时间),而不是性能分析(获取按函数细分的执行时间报告)。
Python 包含一个名为 cProfile
的性能分析器,可以将执行时间分解为所有调用函数的贡献。IPython 提供了在交互式会话中方便使用此工具的方法。
如何实现...
IPython 提供了 %prun
行魔法命令和 %%prun
单元格魔法命令,可以轻松地分析一行或多行代码的性能。%run
魔法命令也接受 -p
标志,用于在性能分析器的控制下运行 Python 脚本。这些命令有许多选项,你可能希望查看它们的文档,可以通过 %prun?
和 %run?
进行查询。
在这个示例中,我们将分析一个从原点开始的随机漫步数值模拟。我们将在 第十三章 中更详细地介绍这些类型的模拟,随机动力学系统。
-
让我们导入 NumPy 和 matplotlib:
In [1]: import numpy as np import matplotlib.pyplot as plt In [2]: %matplotlib inline
-
让我们创建一个生成随机 +1 和 -1 值的函数,并将其存储在数组中:
In [3]: def step(*shape): # Create a random n-vector with +1 or -1 # values. return 2 * (np.random.random_sample(shape) < .5) - 1
-
现在,让我们在一个以
%%prun
开头的单元格中编写模拟代码,以便分析整个模拟过程的性能。各种选项允许我们将报告保存到文件中,并按累计时间对前 10 个结果进行排序。我们将在 原理介绍 部分更详细地解释这些选项。In [4]: %%prun -s cumulative -q -l 10 -T prun0 n = 10000 iterations = 50 x = np.cumsum(step(iterations, n), axis=0) bins = np.arange(-30, 30, 1) y = np.vstack([np.histogram(x[i,:], bins)[0] for i in range(iterations)])
-
性能分析报告已保存为名为
prun0
的文本文件。让我们展示一下它(以下输出是经过简化的版本,以适应本页面):In [5]: print(open('prun0', 'r').read()) 2960 function calls in 0.075 seconds Ordered by: cumulative time ncalls cumtime percall function 50 0.037 0.001 histogram 1 0.031 0.031 step 50 0.024 0.000 sort 1 0.019 0.019 rand 1 0.005 0.005 cumsum
在这里,我们观察了在代码中直接或间接涉及的不同函数的执行时间。
-
如果我们将模拟的迭代次数从 50 增加到 500,那么运行相同的模拟,我们将得到以下结果:
29510 function calls in 1.359 seconds ncalls cumtime percall function 500 0.566 0.001 histogram 1 0.388 0.388 cumsum 1 0.383 0.383 step 500 0.339 0.001 sort 1 0.241 0.241 rand
我们可以观察到,迭代次数对涉及的函数(特别是
cumsum
函数)的相对性能开销有很大影响。
原理介绍...
Python 的性能分析器会生成关于我们代码执行时间的详细报告,按函数进行分类。在这里,我们可以观察到 histogram
、cumsum
、step
、sort
和 rand
函数的调用次数,以及在代码执行过程中这些函数的总时间。内部函数也会被分析。对于每个函数,我们会得到总调用次数、总时间和累积时间,以及每次调用的对应值(通过 ncalls
除以总值)。总时间表示解释器在某个函数中停留的时间,不包括在调用子函数时所花费的时间。累积时间类似,但包括在调用子函数时所花费的时间。文件名、函数名和行号会显示在最后一列。
%prun
和 %%prun
魔法命令接受多个可选参数(输入 %prun?
查看详细信息)。在示例中,-s
允许我们按特定列排序报告,-q
用于抑制(抑制)分页器输出(当我们想将输出整合到笔记本中时很有用),-l
用于限制显示的行数或按函数名筛选结果(当我们关注某个特定函数时非常有用),-T
用于将报告保存为文本文件。此外,我们还可以选择使用 -D
保存(转储)二进制报告到文件中,或者使用 -r
在 IPython 中返回报告。这个类似数据库的对象包含所有分析信息,可以通过 Python 的 pstats
模块进行分析。
注意
每个性能分析器都有其自身的开销,可能会影响分析结果(探测效应)。换句话说,一个被分析过的程序可能比未分析的程序运行得慢得多。这一点需要记住。
“过早的优化是万恶之源”
正如 Donald Knuth 的名言所示,过早地优化代码通常被认为是一种不良实践。代码优化应仅在真正需要时进行,也就是说,当代码在正常情况下真的运行得很慢时。此外,我们应当准确知道需要优化代码的地方;通常,执行时间的大部分来自于代码的相对小部分。了解这一点的唯一方法是对代码进行性能分析;优化永远不应在没有初步分析的情况下进行。
提示
我曾经处理一些相当复杂的代码,速度比预期慢。我以为我对问题的原因和如何解决有相当好的想法。解决方案将涉及对代码的重大更改。幸运的是,我首先对我的代码进行了性能分析,只是为了确保。我的诊断似乎完全错误;我在某处错误地写成了 max(x)
而不是 np.max(x)
,其中 x
是一个非常大的向量。调用的是 Python 的内置函数,而不是 NumPy 为数组高度优化的例程。如果我没有对代码进行性能分析,我可能会永远错过这个错误。程序运行得非常好,只是慢了 150 倍!
有关编程优化的更一般建议,请参阅 en.wikipedia.org/wiki/Program_optimization
。
还有更多...
在 IPython 中对代码进行性能分析特别简单(尤其在笔记本中),正如我们在本方法中所见。但是,从 IPython 执行我们需要分析的代码可能是不可取或困难的(例如 GUI)。在这种情况下,我们可以直接使用 cProfile
。这比在 IPython 中稍微复杂一些。
-
首先,我们调用以下命令:
$ python -m cProfile -o profresults myscript.py
文件
profresults
将包含myscript.py
的性能分析结果的转储。 -
然后,我们从 Python 或 IPython 执行以下代码,以以人类可读的形式显示性能分析结果:
import pstats p = pstats.Stats('profresults') p.strip_dirs().sort_stats("cumulative").print_stats()
探索 cProfile
和 pstats
模块的文档,以了解您可以对性能分析报告执行的所有分析。
提示
位于 github.com/rossant/easy_profiler
的存储库包含一个简单的命令行工具,可帮助分析 Python 脚本的性能。
有一些 GUI 工具可用于探索和可视化性能分析会话的输出。例如,RunSnakeRun 允许您在 GUI 程序中查看性能分析结果。
以下是一些参考资料:
-
cProfile
和pstats
的文档,可在docs.python.org/3/library/profile.html
获取 -
RunSnakeRun,在 www.vrplumber.com/programming/runsnakerun/ 上
-
Python 的性能分析工具,可在
blog.ionelmc.ro/2013/06/08/python-profiling-tools/
获取
另请参阅
- 使用
line_profiler
逐行分析您的代码性能 方法
使用 line_profiler
逐行分析您的代码性能
Python 的原生 cProfile
模块和相应的 %prun
魔术将代码的执行时间逐个函数地分解。有时,我们可能需要更细粒度的代码性能分析,以逐行报告。这样的报告可能比 cProfile
的报告更易读。
要按行分析代码,我们需要一个名为line_profiler
的外部 Python 模块,由 Robert Kern 创建,模块可从pythonhosted.org/line_profiler/
获得。在本教程中,我们将演示如何在 IPython 中使用该模块。
准备好
要安装line_profiler
,在终端中输入pip install line_profiler
,或在 IPython 中输入!pip install line_profiler
(你需要一个 C 编译器)。
在 Windows 上,你可以使用 Chris Gohlke 提供的非官方包,下载地址为www.lfd.uci.edu/~gohlke/pythonlibs/#line_profiler。
怎么做...
我们将逐行分析与上一教程相同的模拟代码:
-
首先,让我们导入 NumPy 和随包一起提供的
line_profiler
IPython 扩展模块:In [1]: import numpy as np In [2]: %load_ext line_profiler
-
这个 IPython 扩展模块提供了一个
%lprun
魔法命令,用于逐行分析 Python 函数。它在函数定义在文件中而不是在交互式命名空间或笔记本中时效果最好。因此,在这里,我们将代码写入 Python 脚本,并使用%%writefile
单元魔法:In [3]: %%writefile simulation.py import numpy as np def step(*shape): # Create a random n-vector with +1 or -1 # values. return (2 * (np.random.random_sample(shape) < .5) - 1) def simulate(iterations, n=10000): s = step(iterations, n) x = np.cumsum(s, axis=0) bins = np.arange(-30, 30, 1) y = np.vstack([np.histogram(x[i,:], bins)[0] for i in range(iterations)]) return y
-
现在,让我们将这个脚本导入到交互式命名空间中,以便执行和分析我们的代码:
In [4]: import simulation
-
我们在行级分析器的控制下执行函数。需要分析的函数必须在
%lprun
魔法命令中明确指定。我们还将报告保存在一个文件中,命名为lprof0
:In [5]: %lprun -T lprof0 -f simulation.simulate simulation.simulate(50)
-
让我们展示报告(以下输出是经过精简的版本,以适应页面):
In [6]: print(open('lprof0', 'r').read()) File: simulation.py Function: simulate at line 7 Total time: 0.114508 s Line # % Time Line Contents 7 def simulate(iterations, n=10000): 8 36.3 s = step(iterations, n) 9 5.6 x = np.cumsum(s, axis=0) 10 0.1 bins = np.arange(-30, 30, 1) 11 58.1 y = np.vstack([np.histogram(...)]) 12 0.0 return y
-
如果我们用比之前多 10 倍的迭代次数(
simulation.simulate(500)
)执行相同的分析,我们会得到如下报告:Total time: 1.28704 s 7 def simulate(iterations, n=10000): 8 29.2 s = step(iterations, n) 9 30.9 x = np.cumsum(s, axis=0) 10 0.0 bins = np.arange(-30, 30, 1) 11 39.9 y = np.vstack([np.histogram(...)]) 12 0.0 return y
它是如何工作的...
%lprun
命令接受一个 Python 语句作为其主要参数。需要分析的函数必须通过-f
明确指定。其他可选参数包括-D
、-T
和-r
,它们的工作方式类似于%prun
魔法命令的对应参数。
line_profiler
模块显示每一行分析函数所花费的时间,可以以计时单位或总执行时间的分数形式显示。当我们在查找代码热点时,这些详细信息至关重要。
还有更多内容...
与上一教程一样,可能需要对一个独立的 Python 程序运行逐行分析器,该程序无法从 IPython 轻松启动。这个过程稍显复杂。
-
我们从
github.com/rkern/line_profiler/blob/master/kernprof.py
下载kernprof
文件,并将其保存在代码的目录中。 -
在代码中,我们用
@profile
装饰器来装饰我们希望分析的函数。我们需要在分析会话结束后删除这些装饰器,因为如果代码正常执行(即不在行级分析器的控制下),它们会引发NameError
异常:@profile def thisfunctionneedstobeprofiled(): pass
提示
参见
stackoverflow.com/questions/18229628/python-profiling-using-line-profiler-clever-way-to-remove-profile-statements
链接,了解一种巧妙的方法来移除配置文件语句。 -
我们在终端中执行以下命令:
python -m kernprof -l -v myscript.py > lprof.txt
将执行
myscript.py
脚本,并将报告保存到lprof.txt
中。提示
github.com/rossant/easy_profiler
上的代码库提供了一个稍微简化的逐行分析工具使用方法。
跟踪 Python 程序逐步执行过程
我们还将讨论跟踪工具,它们对于性能分析、调试程序或用于教育目的非常有用。
Python 的trace
模块允许我们跟踪 Python 代码的程序执行。这在深入调试和性能分析过程中非常有用。我们可以跟踪 Python 解释器执行的所有指令序列。有关 trace 模块的更多信息,请访问docs.python.org/3/library/trace.html
。
此外,在线 Python Tutor 是一个在线交互式教育工具,帮助我们逐步理解 Python 解释器在执行程序源代码时的操作。在线 Python Tutor 可通过pythontutor.com/
访问。
另见
-
使用 cProfile 和 IPython 轻松进行代码性能分析的技巧
-
使用 memory_profiler 分析代码的内存使用情况的技巧
使用 memory_profiler 分析代码的内存使用情况
前一篇配方中描述的方法是关于 CPU 时间的性能分析。这可能是代码性能分析中最显著的因素。然而,内存也是一个关键因素。例如,运行np.zeros(500000000)
很可能会立即崩溃你的计算机!这个命令可能会分配超过系统可用内存的内存;你的计算机会在几秒钟内进入无响应状态。
编写内存优化代码并不简单,但能显著提升程序的运行速度。尤其在处理大型 NumPy 数组时,这一点尤为重要,正如我们将在本章后面看到的那样。
在这个配方中,我们将介绍一个简单的内存分析工具。这个库,毫不奇怪地叫做memory_profiler
,由 Fabian Pedregosa 创建。它的使用方式与line_profiler
非常相似,且可以方便地从 IPython 中使用。你可以从pypi.python.org/pypi/memory_profiler
下载它。
准备工作
你可以通过pip install memory_profiler
来安装memory_profiler
。
在 Windows 上,您还需要 psutil
,它可以在 pypi.python.org/pypi/psutil
上找到。您可以使用 pip install psutil
安装,或者从 code.google.com/p/psutil/
下载该包。您也可以从 Chris Gohlke 的网页 www.lfd.uci.edu/~gohlke/pythonlibs/ 下载安装程序。
本方法中的示例是前一个方法的延续。
如何操作...
-
假设仿真代码已经如前一个方法中所示加载,我们加载内存分析器的 IPython 扩展:
In [9]: %load_ext memory_profiler
-
现在,让我们在内存分析器的控制下运行代码:
In [10]: %mprun -T mprof0 -f simulation.simulate simulation.simulate(50)
-
让我们展示结果:
In [11]: print(open('mprof0', 'r').read()) Filename: simulation.py Line # Mem usage Increment Line Contents 7 39.672 MB 0.000 MB def simulate(...): 8 41.977 MB 2.305 MB s = step(iterations, n) 9 43.887 MB 1.910 MB x = np.cumsum(...) 10 43.887 MB 0.000 MB bins = np.arange(...) 11 43.887 MB 0.000 MB y = np.vstack(...) 12 43.887 MB 0.000 MB return y
-
最后,这是进行 500 次迭代的报告:
Line # Mem usage Increment Line Contents 7 40.078 MB 0.000 MB def simulate(...): 8 59.191 MB 19.113 MB s = step(iterations, n) 9 78.301 MB 19.109 MB x = np.cumsum(...) 10 78.301 MB 0.000 MB bins = np.arange(...) 11 78.301 MB 0.000 MB y = np.vstack(...) 12 78.301 MB 0.000 MB return y
它是如何工作的...
memory_profiler
包检查每行代码的内存使用情况。增量 列帮助我们发现代码中分配大量内存的地方。当处理数组时,这一点尤其重要。不必要的数组创建和复制会显著减慢程序速度。我们将在接下来的几个方法中解决这个问题。
还有更多...
我们可以在没有 IPython 的情况下使用 memory_profiler
,也可以在 IPython 中对单个命令进行快速内存基准测试。
在独立的 Python 程序中使用 memory_profiler
在独立的 Python 程序中使用内存分析器与使用 line_profiler
类似,但稍微简单一些。
-
首先,在我们的 Python 脚本中,我们通过
@profile
装饰器标记我们希望分析的函数。 -
然后,我们运行:
$ python -m memory_profiler myscript.py > mprof.txt
分析报告将保存在
myprof.txt
中。
在 IPython 中使用 %memit 魔法命令
memory_profiler
的 IPython 扩展还附带了一个 %memit
魔法命令,让我们可以基准测试单个 Python 语句所使用的内存。这里是一个简单的例子:
In [14]: %memit np.random.randn(1000, 1000)
maximum of 1: 46.199219 MB per loop
其他工具
还有其他工具可以监控 Python 程序的内存使用情况,特别是 Guppy-PE (guppy-pe.sourceforge.net/
)、PySizer (pysizer.8325.org/
) 和 Pympler (code.google.com/p/pympler/
)。与 IPython 及 Python 的自省功能结合使用时,这些工具允许您分析命名空间或特定对象的内存使用情况。
另见
-
逐行分析代码并使用 line_profiler 方法
-
理解 NumPy 的内部机制以避免不必要的数组复制 方法
理解 NumPy 的内部机制以避免不必要的数组复制
使用 NumPy,我们可以显著提高性能,特别是当我们的计算遵循 单指令多数据 (SIMD) 模式时。然而,也有可能无意中写出非优化的 NumPy 代码。
在接下来的几个案例中,我们将看到一些可以帮助我们编写优化 NumPy 代码的技巧。在这个案例中,我们将看到如何避免不必要的数组复制,从而节省内存。在这方面,我们需要深入了解 NumPy 的内部实现。
准备工作
首先,我们需要一种方法来检查两个数组是否共享相同的底层数据缓冲区。我们可以定义一个返回底层数据缓冲区内存位置的函数id()
:
def id(x):
# This function returns the memory
# block address of an array.
return x.__array_interface__['data'][0]
两个具有相同数据位置(由id
返回的)数组共享相同的底层数据缓冲区。然而,只有在数组具有相同偏移量(意味着它们的第一个元素相同)时,才会发生这种情况。具有不同偏移量的共享数组会有稍微不同的内存位置,下面的示例说明了这一点:
In [1]: id(a), id(a[1:])
Out[1]: (71211328, 71211336)
在接下来的几个案例中,我们将确保使用相同偏移量的数组。以下是一个更通用且可靠的解决方案,用于判断两个数组是否共享相同的数据:
In [2]: def get_data_base(arr):
"""For a given Numpy array, finds the
base array that "owns" the actual data."""
base = arr
while isinstance(base.base, np.ndarray):
base = base.base
return base
def arrays_share_data(x, y):
return get_data_base(x) is get_data_base(y)
In [3]: print(arrays_share_data(a,a.copy()),
arrays_share_data(a,a[1:]))
False True
感谢 Michael Droettboom 指出这一点并提出这种替代解决方案。
如何做到...
使用 NumPy 数组进行计算可能涉及内存块之间的内部复制。这些复制并非总是必要的,如果没有必要,应避免它们,正如我们将在以下提示中看到的:
-
我们有时需要对数组进行复制;例如,如果我们需要在保留原始副本的情况下操作数组:
In [3]: a = np.zeros(10); aid = id(a); aid Out[3]: 65527008L In [4]: b = a.copy(); id(b) == aid Out[4]: False
-
数组计算可以涉及就地操作(以下代码中的第一个示例:数组被修改)或隐式复制操作(第二个示例:创建了一个新数组):
In [5]: a *= 2; id(a) == aid Out[5]: True In [6]: a = a*2; id(a) == aid Out[6]: False
确保选择你实际需要的操作类型。隐式复制操作显著较慢,如下所示:
In [7]: %%timeit a = np.zeros(10000000) a *= 2 10 loops, best of 3: 23.9 ms per loop In [8]: %%timeit a = np.zeros(10000000) a = a*2 10 loops, best of 3: 77.9 ms per loop
-
重塑数组可能会或可能不会涉及复制。原因将在它是如何工作的...部分解释。例如,重塑一个二维矩阵不会涉及复制,除非它被转置(或者更一般地说,非连续):
In [9]: a = np.zeros((10, 10)); aid = id(a) In [10]: b = a.reshape((1, -1)); id(b) == aid Out[10]: True In [11]: c = a.T.reshape((1, -1)); id(c) == aid Out[11]: False
因此,后者的操作将显著慢于前者。
-
数组的
flatten
和ravel
方法都会将其重塑为一维向量(一个扁平化的数组)。然而,flatten
方法总是返回一个副本,而ravel
方法仅在必要时返回副本(因此它更快,尤其是在处理大数组时)。In [12]: d = a.flatten(); id(d) == aid Out[12]: False In [13]: e = a.ravel(); id(e) == aid Out[13]: True In [14]: %timeit a.flatten() 1000000 loops, best of 3: 1.65 µs per loop In [15]: %timeit a.ravel() 1000000 loops, best of 3: 566 ns per loop
-
广播规则允许我们对形状不同但兼容的数组进行计算。换句话说,我们不总是需要重塑或拼接数组来使它们的形状匹配。以下示例演示了在两个向量之间进行外积的两种方法:第一种方法涉及数组拼接,第二种方法(更快)涉及广播:
In [16]: n = 1000 In [17]: a = np.arange(n) ac = a[:, np.newaxis] # Column vector. ar = a[np.newaxis, :] # Row vector. In [18]: %timeit np.tile(ac, (1, n)) * np.tile(ar, (n, 1)) 10 loops, best of 3: 25 ms per loop In [19]: %timeit ar * ac 100 loops, best of 3: 4.63 ms per loop
它是如何工作的...
在本节中,我们将看到在使用 NumPy 时,内部发生了什么,以及这些知识如何帮助我们理解本食谱中的技巧。
为什么 NumPy 数组高效?
NumPy 数组基本上由元数据描述(特别是维数、形状和数据类型)和实际数据组成。数据存储在一个同质且连续的内存块中,位于系统内存中的特定地址(随机存取存储器,或者RAM)。这个内存块称为数据缓冲区。与纯 Python 结构(如列表)相比,其中项目分散在系统内存中,这是主要区别。这个方面是使 NumPy 数组如此高效的关键特性。
为什么这么重要呢?以下是主要原因:
-
在低级语言(如 C)中,可以非常高效地编写数组上的计算(NumPy 的大部分实际上是用 C 编写的)。只要知道内存块的地址和数据类型,就可以简单地进行循环遍历所有项目,例如。在 Python 中使用列表进行这样的操作会有很大的开销。
-
空间局部性在内存访问模式中导致性能提升,这主要是由于 CPU 缓存。事实上,缓存会将字节以块的形式从 RAM 加载到 CPU 寄存器中。然后,相邻的项目会被非常高效地加载(顺序局部性,或者引用局部性)。
-
最后,项目在内存中连续存储的事实使得 NumPy 能够利用现代 CPU 的矢量化指令,例如英特尔的SSE和AVX,AMD 的 XOP 等。例如,多个连续的浮点数可以加载到 128、256 或 512 位寄存器中,用作实现为 CPU 指令的矢量化算术计算。
注意
另外,NumPy 可以通过ATLAS或英特尔数学核心库(MKL)与高度优化的线性代数库(如BLAS和LAPACK)链接。一些特定的矩阵计算也可以是多线程的,利用现代多核处理器的强大性能。
总之,将数据存储在连续的内存块中确保了现代 CPU 架构在内存访问模式、CPU 缓存和矢量化指令方面的最佳利用。
原地操作和隐式复制操作之间有什么区别?
让我们解释第 2 步中的示例。诸如 a *= 2
这样的表达式对应于原地操作,其中数组的所有值都乘以了二。相比之下,a = a*2
意味着创建了一个包含 a*2
值的新数组,并且变量 a
现在指向这个新数组。旧数组变得没有引用,并将被垃圾回收器删除。与第二种情况相反,第一种情况中不会发生内存分配。
更一般地,诸如 a[i:j]
这样的表达式是数组的视图;它们指向包含数据的内存缓冲区。使用原地操作修改它们会改变原始数组。因此,a[:] = a*2
是一个原地操作,不同于 a = a*2
。
了解 NumPy 的这种微妙之处可以帮助你修复一些错误(因为一个数组由于对视图的操作而被隐式和无意中修改),并通过减少不必要的复制次数来优化代码的速度和内存消耗。
为什么有些数组不能在没有复制的情况下重塑?
我们在这里解释第 3 步的示例,其中一个转置的 2D 矩阵不能在没有复制的情况下被展平。一个 2D 矩阵包含由两个数字(行和列)索引的项目,但它在内部存储为一个 1D 连续的内存块,可以用一个数字访问。有多种将矩阵项目存储在 1D 内存块中的方法:我们可以先放第一行的元素,然后是第二行,依此类推,或者先放第一列的元素,然后是第二列,依此类推。第一种方法称为行主序,而后者称为列主序。在这两种方法之间的选择只是内部约定的问题:NumPy 使用行主序,类似于 C,但不同于 FORTRAN。
内部数组布局:行主序和列主序
更一般地说,NumPy 使用strides
的概念来在多维索引和底层(1D)元素序列的内存位置之间进行转换。array[i1, i2]
与内部数据的相关字节地址之间的具体映射如下:
offset = array.strides[0] * i1 + array.strides[1] * i2
在重塑数组时,NumPy 通过修改strides
属性来尽可能避免复制。例如,当转置矩阵时,步幅的顺序被颠倒,但底层数据保持不变。然而,简单通过修改步幅来展平一个转置数组是不可能的(试试看!),因此需要进行复制(感谢 Chris Beaumont 澄清了本段早期版本)。
内部数组布局还可以解释一些非常相似的 NumPy 操作之间的意外性能差异。作为一个小练习,你能解释以下基准测试吗?
In [20]: a = np.random.rand(5000, 5000)
In [21]: %timeit a[0,:].sum()
100000 loops, best of 3: 17.9 µs per loop
In [22]: %timeit a[:,0].sum()
10000 loops, best of 3: 60.6 µs per loop
NumPy 广播规则是什么?
广播规则描述了具有不同维度和/或形状的数组如何用于计算。一般规则是当两个维度相等或其中一个为 1 时,它们是兼容的。NumPy 使用此规则逐个元素地比较两个数组的形状,从尾部维度开始,逐步向前工作。最小的维度在内部被拉伸以匹配另一个维度,但此操作不涉及任何内存复制。
还有更多...
以下是一些参考资料:
-
广播规则和示例,可在
docs.scipy.org/doc/numpy/user/basics.broadcasting.html
找到。 -
NumPy 中的数组接口,位于
docs.scipy.org/doc/numpy/reference/arrays.interface.html
-
SciPy 讲座笔记中的 NumPy 内部,可在
scipy-lectures.github.io/advanced/advanced_numpy/
找到 -
Nicolas Rougier 编写的 100 个 NumPy 练习,可在www.loria.fr/~rougier/teaching/numpy.100/index.html找到
另请参阅
- 使用 NumPy 的步幅技巧示例
使用 NumPy 的步幅技巧
在这个示例中,我们将深入研究 NumPy 数组的内部,通过将行优先和列优先顺序的概念推广到多维数组。一般概念是步幅,描述多维数组的项在一维数据缓冲区内的组织方式。步幅大多是一个实现细节,但在特定情况下也可以用于优化一些算法。
准备工作
我们假设已经导入了 NumPy,并且id
函数已经定义(参见前一个示例,了解 NumPy 的内部以避免不必要的数组复制)。
如何做...
-
步幅是描述每个维度在连续内存块中的字节步长的整数。
In [3]: x = np.zeros(10); x.strides Out[3]: (8,)
这个向量
x
包含双精度浮点数(float64,8 字节);从一个项目到下一个项目需要向前移动 8 字节。 -
现在,让我们看一下一个二维数组的步幅:
In [4]: y = np.zeros((10, 10)); y.strides Out[4]: (80, 8)
在第一维(垂直)中,从一个项目到下一个项目需要向前移动 80 字节(10 个 float64 项目),因为项目在内部以行优先顺序存储。在第二维(水平)中,从一个项目到下一个项目需要向前移动 8 字节。
-
让我们展示如何使用步幅重新审视前一个示例中的广播规则:
In [5]: n = 1000; a = np.arange(n)
我们将创建一个新数组
b
,指向与a
相同的内存块,但形状和步幅不同。这个新数组看起来像是a
的垂直平铺版本。我们使用 NumPy 中的一个特殊函数来改变数组的步幅:In [6]: b = np.lib.stride_tricks.as_strided(a, (n, n), (0, 4)) Out[7]: array([[ 0, 1, 2, ..., 997, 998, 999], ..., [ 0, 1, 2, ..., 997, 998, 999]]) In [8]: b.size, b.shape, b.nbytes Out[8]: (1000000, (1000, 1000), 4000000)
NumPy 认为这个数组包含一百万个不同的元素,而实际上数据缓冲区只包含与
a
相同的 1000 个元素。 -
现在我们可以使用与广播规则相同的原则执行高效的外积:
In [9]: %timeit b * b.T 100 loops, best of 3: 5.03 ms per loop In [10]: %timeit np.tile(a, (n, 1)) \ * np.tile(a[:, np.newaxis], (1, n)) 10 loops, best of 3: 28 ms per loop
工作原理...
每个数组都有一定数量的维度、形状、数据类型和步幅。步幅描述了多维数组中的项是如何在数据缓冲区中组织的。有许多不同的方案来安排多维数组的项在一维块中。NumPy 实现了一个跨步索引方案,其中任何元素的位置是维度的线性组合,系数是步幅。换句话说,步幅描述了在任何维度中,我们需要跳过多少字节在数据缓冲区中从一个项到下一个项。
多维数组中任何元素的位置由其索引的线性组合给出,如下所示:
人为改变步幅允许我们使一些数组操作比标准方法更有效,后者可能涉及数组复制。在内部,这就是 NumPy 中广播的工作原理。
as_strided
方法接受一个数组、一个形状和步幅作为参数。它创建一个新数组,但使用与原始数组相同的数据缓冲区。唯一改变的是元数据。这个技巧让我们像平常一样操作 NumPy 数组,只是它们可能比 NumPy 认为的占用更少的内存。在这里,使用步幅中的 0 意味着任何数组项都可以由许多多维索引来寻址,从而节省内存。
提示
对于跨步数组要小心!as_strided
函数不会检查你是否保持在内存块边界内。这意味着你需要手动处理边缘效应;否则,你的数组中可能会出现垃圾值。
我们将在下一个食谱中看到跨步技巧的更有用的应用。
参见
- 使用跨步技巧实现高效的滚动平均算法食谱
使用跨步技巧实现高效的滚动平均算法
当在数组上进行局部计算时,跨步技巧可以很有用,当给定位置的计算值取决于相邻值时。示例包括动力系统、数字滤波器和细胞自动机。
在这个食谱中,我们将使用 NumPy 跨步技巧实现一个高效的滚动平均算法(一种特殊类型的基于卷积的线性滤波器)。1D 向量的滚动平均在每个位置包含原始向量中该位置周围元素的平均值。粗略地说,这个过程滤除信号的嘈杂成分,以保留只有较慢的成分。
准备工作
确保重用了解 NumPy 内部以避免不必要的数组复制食谱中的id()
函数。这个函数返回 NumPy 数组的内部数据缓冲区的内存地址。
如何做...
思路是从一个 1D 向量开始,并创建一个虚拟的 2D 数组,其中每一行都是前一行的移位版本。使用跨步技巧时,这个过程非常高效,因为它不涉及任何复制。
-
让我们生成一个一维向量:
In [1]: import numpy as np from numpy.lib.stride_tricks import as_strided In [2]: n = 5; k = 2 In [3]: a = np.linspace(1, n, n); aid = id(a)
-
让我们改变
a
的步幅以添加移位行:In [4]: as_strided(a, (k, n), (8, 8)) Out[4]: array([[ 1e+00, 2e+00, 3e+00, 4e+00, 5e+00], [ 2e+00, 3e+00, 4e+00, 5e+00, -1e-23]])
最后一个值表示越界问题:步幅技巧可能存在危险,因为内存访问没有经过检查。在这里,我们应该通过限制数组的形状来考虑边缘效应。
In [5]: as_strided(a, (k, n-k+1), (8, 8)) Out[5]: array([[ 1., 2., 3., 4.], [ 2., 3., 4., 5.]])
-
现在,让我们实现滚动平均值的计算。第一个版本(标准方法)涉及显式数组复制,而第二个版本使用步幅技巧:
In [6]: def shift1(x, k): return np.vstack([x[i:n-k+i+1] for i in range(k)]) In [7]: def shift2(x, k): return as_strided(x, (k, n-k+1), (x.itemsize,)*2)
-
这两个函数返回相同的结果,只是第二个函数返回的数组指向原始数据缓冲区:
In [8]: b = shift1(a, k); b, id(b) == aid Out[8]: (array([[ 1., 2., 3., 4.], [ 2., 3., 4., 5.]]), False) In [9]: c = shift2(a, k); c, id(c) == aid Out[9]: (array([[ 1., 2., 3., 4.], [ 2., 3., 4., 5.]]), True)
-
让我们生成一个信号:
In [10]: n, k = 100, 10 t = np.linspace(0., 1., n) x = t + .1 * np.random.randn(n)
-
我们通过创建信号的移位版本,并沿着垂直维度进行平均,来计算信号的滚动平均值。结果显示在下一个图中:
In [11]: y = shift2(x, k) x_avg = y.mean(axis=0)
一个信号及其滚动平均值
-
让我们评估第一种方法所花费的时间:
In [15]: %timeit shift1(x, k) 10000 loops, best of 3: 163 µs per loop In [16]: %%timeit y = shift1(x, k) z = y.mean(axis=0) 10000 loops, best of 3: 63.8 µs per loop
-
以及第二种方法:
In [17]: %timeit shift2(x, k) 10000 loops, best of 3: 23.3 µs per loop In [18]: %%timeit y = shift2(x, k) z = y.mean(axis=0) 10000 loops, best of 3: 35.8 µs per loop
在第一个版本中,大部分时间都花在数组复制上,而在步幅技巧版本中,大部分时间都花在平均值的计算上。
参见
- 使用 NumPy 的步幅技巧方法
在 NumPy 中进行高效的数组选择
NumPy 提供了几种选择数组切片的方法。数组视图指的是数组的原始数据缓冲区,但具有不同的偏移、形状和步幅。它们只允许步进选择(即,具有线性间隔的索引)。NumPy 还提供了特定函数来沿一个轴进行任意选择。最后,花式索引是最通用的选择方法,但也是最慢的,正如我们在这个方法中所看到的。在可能的情况下,应选择更快的替代方法。
准备工作
我们假设 NumPy 已经被导入,并且id
函数已经被定义(参见了解 NumPy 的内部以避免不必要的数组复制方法)。
如何实现...
-
让我们创建一个具有大量行的数组。我们将沿着第一个维度选择这个数组的切片:
In [3]: n, d = 100000, 100 In [4]: a = np.random.random_sample((n, d)); aid = id(a)
-
让我们选择每 10 行中的一行,使用两种不同的方法(数组视图和花式索引):
In [5]: b1 = a[::10] b2 = a[np.arange(0, n, 10)] In [6]: np.array_equal(b1, b2) Out[6]: True
-
视图指向原始数据缓冲区,而花式索引产生一个副本:
In [7]: id(b1) == aid, id(b2) == aid Out[7]: (True, False)
-
让我们比较两种方法的性能:
In [8]: %timeit a[::10] 100000 loops, best of 3: 2.03 µs per loop In [9]: %timeit a[np.arange(0, n, 10)] 10 loops, best of 3: 46.3 ms per loop
花式索引比涉及复制大数组的速度慢几个数量级。
-
当需要沿着一个维度进行非步进选择时,数组视图不是一个选择。然而,在这种情况下,仍然存在替代花式索引的方法。给定一个索引列表,NumPy 的
take()
函数可以沿着一个轴进行选择:In [10]: i = np.arange(0, n, 10) In [11]: b1 = a[i] b2 = np.take(a, i, axis=0) In [12]: np.array_equal(b1, b2) Out[12]: True
第二种方法更快:
In [13]: %timeit a[i] 10 loops, best of 3: 50.2 ms per loop In [14]: %timeit np.take(a, i, axis=0) 100 loops, best of 3: 11.1 ms per loop
提示
最近的 NumPy 版本中改进了花式索引的性能;这个技巧在旧版本的 NumPy 中特别有用。
-
当沿着一个轴选择的索引由布尔掩码向量指定时,
compress()
函数是花式索引的一种替代方法:In [15]: i = np.random.random_sample(n) < .5 In [16]: b1 = a[i] b2 = np.compress(i, a, axis=0) In [17]: np.array_equal(b1, b2) Out[17]: True In [18]: %timeit a[i] 1 loops, best of 3: 286 ms per loop In [19]: %timeit np.compress(i, a, axis=0) 10 loops, best of 3: 41.3 ms per loop
第二种方法也比花式索引更快。
工作原理是怎样的...
花式索引是进行完全任意选择数组的最通用方式。然而,通常存在更具体且更快的方法,在可能的情况下应该优先选择这些方法。
当需要进行步进选择时,应使用数组视图,但我们需要小心,因为视图是引用原始数据缓冲区的。
还有更多内容…
这里有一些参考资料:
-
完整的 NumPy 例程列表可以在 NumPy 参考指南中找到,网址是
docs.scipy.org/doc/numpy/reference/
-
索引例程的列表可以在
docs.scipy.org/doc/numpy/reference/routines.indexing.html
查看
使用内存映射处理超大 NumPy 数组
有时,我们需要处理无法完全放入系统内存的超大 NumPy 数组。一种常见的解决方案是使用内存映射并实现核心外计算。数组被存储在硬盘上的文件中,我们创建一个内存映射对象来访问该文件,该对象可以像普通的 NumPy 数组一样使用。访问数组的一部分会自动从硬盘中获取相应的数据。因此,我们只消耗我们使用的部分。
如何操作…
-
让我们创建一个内存映射数组:
In [1]: import numpy as np In [2]: nrows, ncols = 1000000, 100 In [3]: f = np.memmap('memmapped.dat', dtype=np.float32, mode='w+', shape=(nrows, ncols))
-
让我们一次性喂入随机值,一次处理一列,因为我们的系统内存有限!
In [4]: for i in range(ncols): f[:,i] = np.random.rand(nrows)
我们保存数组的最后一列:
In [5]: x = f[:,-1]
-
现在,我们通过删除对象将内存中的更改刷新到磁盘:
In [6]: del f
-
从磁盘读取内存映射数组使用相同的
memmap
函数。数据类型和形状需要重新指定,因为这些信息不会保存在文件中:In [7]: f = np.memmap('memmapped.dat', dtype=np.float32, shape=(nrows, ncols)) In [8]: np.array_equal(f[:,-1], x) Out[8]: True In [9]: del f
提示
这种方法并不是最适合长期存储数据和数据共享的。接下来的章节将展示一种基于 HDF5 文件格式的更好的方法。
工作原理…
内存映射使你几乎可以像使用常规数组一样处理超大数组。接受 NumPy 数组作为输入的 Python 代码也可以接受memmap
数组。然而,我们需要确保高效使用该数组。也就是说,数组绝不能一次性全部加载(否则会浪费系统内存,并失去该技术的优势)。
当你有一个包含已知数据类型和形状的原始二进制格式的大文件时,内存映射也很有用。在这种情况下,另一种解决方案是使用 NumPy 的fromfile()
函数,并通过 Python 的原生open()
函数创建文件句柄。使用f.seek()
可以将光标定位到任何位置,并将指定数量的字节加载到 NumPy 数组中。
还有更多内容…
处理巨大的 NumPy 矩阵的另一种方法是通过 SciPy 的稀疏子包使用稀疏矩阵。当矩阵大部分包含零时,这种方法非常适用,这在偏微分方程的模拟、图算法或特定的机器学习应用中常常出现。将矩阵表示为稠密结构可能浪费内存,而稀疏矩阵提供了更高效的压缩表示。
在 SciPy 中使用稀疏矩阵并不简单,因为存在多种实现方法。每种实现最适合特定类型的应用。以下是一些参考资料:
-
关于稀疏矩阵的 SciPy 讲义,见
scipy-lectures.github.io/advanced/scipy_sparse/index.html
-
稀疏矩阵的参考文档,请见
docs.scipy.org/doc/scipy/reference/sparse.html
-
memmap 文档,请见
docs.scipy.org/doc/numpy/reference/generated/numpy.memmap.html
另请参见
-
使用 HDF5 和 PyTables 操作大型数组食谱
-
使用 HDF5 和 PyTables 操作大型异构表格食谱
使用 HDF5 和 PyTables 操作大型数组
NumPy 数组可以通过 NumPy 内建函数如np.savetxt
、np.save
或np.savez
持久化保存到磁盘,并使用类似的函数加载到内存中。当数组包含的点数少于几百万时,这些方法表现最好。对于更大的数组,这些方法面临两个主要问题:它们变得过于缓慢,并且需要将数组完全加载到内存中。包含数十亿个点的数组可能太大,无法适应系统内存,因此需要替代方法。
这些替代方法依赖于内存映射:数组存储在硬盘上,只有当 CPU 需要时,数组的部分数据才会被有选择性地加载到内存中。这种技术在节省内存方面非常高效,但由于硬盘访问会带来轻微的开销。缓存机制和优化可以缓解这个问题。
上一个食谱展示了一种使用 NumPy 的基本内存映射技术。在这个食谱中,我们将使用一个名为PyTables的包,它专门用于处理非常大的数据集。它通过一个广泛使用的开放文件格式规范层次数据格式(HDF5)实现了快速的内存映射技术。一个 HDF5 文件包含一个或多个数据集(数组或异构表格),这些数据集按类 Unix 的 POSIX 层次结构组织。数据集的任何部分都可以高效且轻松地访问,而无需不必要地浪费系统内存。
正如我们在下面的食谱中将看到的,PyTables 的一个替代方案是h5py。在某些情况下,它比 PyTables 更加轻量且更适应需求。
在这个食谱中,我们将学习如何使用 HDF5 和 PyTables 操作大数组。下一个食谱将讲解类似 pandas 的异构表格。
准备工作
本食谱和下一个食谱需要 PyTables 3.0+ 版本。使用 Anaconda,可以通过 conda install tables
安装 PyTables。你还可以在 pytables.github.io/usersguide/installation.html
找到二进制安装程序。Windows 用户可以在 www.lfd.uci.edu/~gohlke/pythonlibs/#pytables 上找到安装程序。
提示
在 3.0 版本之前,PyTables 使用驼峰命名法约定来命名属性和方法。最新版本使用更标准的 Python 约定,使用下划线。所以,例如,tb.open_file
在 3.0 版本之前是 tb.openFile
。
如何操作…
-
首先,我们需要导入 NumPy 和 PyTables(该包的名称是
tables
):In [1]: import numpy as np import tables as tb
-
让我们创建一个新的空 HDF5 文件:
In [2]: f = tb.open_file('myfile.h5', 'w')
-
我们创建了一个名为
experiment1
的新顶级组:In [3]: f.create_group('/', 'experiment1') Out[3]: /experiment1 (Group) u'' children := []
-
让我们还向这个组添加一些元数据:
In [4]: f.set_node_attr('/experiment1', 'date', '2014-09-01')
-
在这个组中,我们创建了一个 1000*1000 的数组,命名为
array1
:In [5]: x = np.random.rand(1000, 1000) f.create_array('/experiment1', 'array1', x) Out[5]: /experiment1/array1 (Array(1000L, 1000L))
-
最后,我们需要关闭文件以提交磁盘上的更改:
In [6]: f.close()
-
现在,让我们打开这个文件。我们也可以在另一个 Python 会话中完成这项操作,因为数组已经保存在 HDF5 文件中。
In [7]: f = tb.open_file('myfile.h5', 'r')
-
我们可以通过提供组路径和属性名称来检索属性:
In [8]: f.get_node_attr('/experiment1', 'date') Out[8]: '2014-09-01'
-
我们可以使用属性访问文件中的任何项目,通过用点替代路径中的斜杠,并以
root
开头(对应路径/
)。IPython 的自动补全功能在互动式探索文件时特别有用。In [9]: y = f.root.experiment1.array1 # /experiment1/array1 type(y) Out[9]: tables.array.Array
-
这个数组可以像 NumPy 数组一样使用,但一个重要的区别是它存储在磁盘上,而不是系统内存中。在这个数组上执行计算时,会自动将请求的数组部分加载到内存中,因此只访问数组的视图会更高效。
In [10]: np.array_equal(x[0,:], y[0,:]) Out[10]: True
-
也可以通过绝对路径获取节点,这在路径只在运行时已知时非常有用:
In [11]: f.get_node('/experiment1/array1') Out[11]: /experiment1/array1 (Array(1000, 1000))
-
本食谱完成了,接下来让我们做一些清理工作:
In [12]: f.close() In [13]: import os os.remove('myfile.h5')
它是如何工作的…
在这个食谱中,我们将一个单独的数组存储到文件中,但当需要在一个文件中保存多个数组时,HDF5 特别有用。HDF5 通常用于大型项目,特别是当大数组必须在层次结构中组织时。例如,它在 NASA、NOAA 和许多其他科学机构中被广泛使用(请参见 www.hdfgroup.org/users.html)。研究人员可以在多个设备、多个试验和多个实验中存储记录的数据。
在 HDF5 中,数据是以树的形式组织的。树的节点可以是组(类似于文件系统中的文件夹)或数据集(类似于文件)。一个组可以包含子组和数据集,而数据集只包含数据。组和数据集都可以包含具有基本数据类型(整数或浮点数、字符串等)的属性(元数据)。HDF5 还支持内部和外部链接;某个路径可以引用同一文件中的另一个组或数据集,或者引用另一个文件中的组或数据集。如果需要为同一实验或项目使用不同的文件,这一特性可能会很有用。
能够在不将整个数组及文件加载到内存中的情况下访问单个数组的一个块是非常方便的。此外,加载后的数组可以使用标准的 NumPy 切片语法进行多态访问。接受 NumPy 数组作为参数的代码原则上也可以接受 PyTables 数组对象作为参数。
还有更多...
在本食谱中,我们创建了一个 PyTables Array
对象来存储我们的 NumPy 数组。其他类似的对象类型包括 CArrays
,它们将大数组分块存储并支持无损压缩。此外,EArray
对象在最多一个维度上是可扩展的,这在创建文件中的数组时,如果数组的维度未知时,非常有用。一个常见的使用场景是在在线实验中记录数据。
另一个主要的 HDF5 对象类型是 Table
,它在一个二维表中存储异构数据类型的表格数据。在 PyTables 中,Table
与 Array
的关系就像 pandas 的 DataFrame
与 NumPy 的 ndarray
之间的关系。我们将在下一个食谱中看到它们。
HDF5 文件的一个有趣特点是它们不依赖于 PyTables。由于 HDF5 是一个开放的格式规范,因此几乎所有语言(C、FORTRAN、MATLAB 等)都有相关的库,因此可以轻松地在这些语言中打开 HDF5 文件。
在 HDF5 中,数据集可以存储在连续的内存块中,或者分块存储。块(Chunks)是原子对象,HDF5/PyTables 只能读取和写入整个块。块在内部通过一种树形数据结构组织,称为B 树。当我们创建一个新的数组或表时,可以指定块形状。这是一个内部细节,但它在写入和读取数据集的部分时可能会极大影响性能。
最优的块形状取决于我们计划如何访问数据。小块的数量较多(由于管理大量块导致较大的开销)与大块较少(磁盘 I/O 效率低)之间存在权衡。一般来说,建议块的大小小于 1 MB。块缓存也是一个重要参数,可能会影响性能。
相关地,在我们创建 EArray
或 Table
对象时,应该指定一个可选参数,预期的行数,以便优化文件的内部结构。如果你打算在大型 HDF5 文件(超过 100 MB)上进行任何稍复杂的操作,PyTables 用户指南中的优化部分(参见以下参考链接)是必须阅读的。
最后,我们应该提到另一个名为 h5py 的 Python HDF5 库。这个轻量级库提供了一个简单的接口来操作 HDF5 文件,侧重于数组而非表格。它为从 NumPy 访问 HDF5 数组提供了非常自然的方式,如果你不需要 PyTables 中类似数据库的功能,它可能已经足够。有关 h5py 的更多信息,请参见 www.h5py.org。
下面是一些参考资料:
-
HDF5 分块,详见 www.hdfgroup.org/HDF5/doc/Advanced/Chunking/
-
PyTables 优化指南,详见
pytables.github.io/usersguide/optimization.html
-
PyTables 和 h5py 的区别,从 h5py 的角度来看,详见
github.com/h5py/h5py/wiki/FAQ#whats-the-difference-between-h5py-and-pytables
-
PyTables 和 h5py 的区别,从 PyTables 的角度来看,详见 www.pytables.org/moin/FAQ#HowdoesPyTablescomparewiththeh5pyproject.3F
另请参见
-
处理巨大 NumPy 数组的内存映射 配方
-
使用 HDF5 和 PyTables 操作大型异构表格 配方
-
在 第二章,交互计算的最佳实践 中的 进行可重复交互计算实验的十个提示 配方
使用 HDF5 和 PyTables 操作大型异构表格
PyTables 可以将同质数据块存储为类似 NumPy 数组的形式在 HDF5 文件中。它也可以存储异构表格,如我们将在这个配方中看到的那样。
准备工作
你需要 PyTables 来实现这个配方(有关安装说明,请参见前面的配方)。
如何实现...
-
让我们导入 NumPy 和 PyTables:
In [1]: import numpy as np import tables as tb
-
让我们创建一个新的 HDF5 文件:
In [2]: f = tb.open_file('myfile.h5', 'w')
-
我们将创建一个 HDF5 表格,包含两列:城市名称(最多 64 个字符的字符串)和其人口(32 位整数)。我们可以通过创建一个复杂的数据类型,并使用 NumPy 来指定这些列:
In [3]: dtype = np.dtype([('city','S64'), ('population', 'i4')])
-
现在,我们在
/table1
中创建表格:In [4]: table = f.create_table('/', 'table1', dtype)
-
让我们添加几行数据:
In [5]: table.append([('Brussels', 1138854), ('London', 8308369), ('Paris', 2243833)])
-
添加行数据后,我们需要刷新表格以提交更改到磁盘:
In [6]: table.flush()
-
有多种方式可以访问表格中的数据。最简单但效率不高的方式是将整个表格加载到内存中,这将返回一个 NumPy 数组:
In [7]: table[:] Out[7]: array([('Brussels', 1138854), ('London', 8308369), ('Paris', 2243833)], dtype=[('city', 'S64'), ('population', '<i4')])
-
也可以加载特定的列(所有行):
In [8]: table.col('city') Out[8]: array(['Brussels', 'London', 'Paris'], dtype='|S64')
-
在处理大量行时,我们可以在表格中执行类似 SQL 的查询,加载所有满足特定条件的行:
In [9]: [row['city'] for row in table.where('population>2e6')] Out[9]: ['London', 'Paris']
-
最后,如果已知索引,我们可以访问特定的行:
In [10]: table[1] Out[10]: ('London', 8308369)
工作原理...
表格可以像本教程中那样从头开始创建,也可以从现有的 NumPy 数组或 pandas DataFrame
创建。在第一种情况下,可以使用 NumPy 数据类型(如这里所示)、字典或继承自IsDescription
的类来描述列。在第二种情况下,表格描述将根据给定的数组或表格自动推断。
可以通过table.append()
高效地在表格末尾添加行。要添加一行,首先使用row = table.row
获取一个新行实例,像操作字典一样设置行的字段,然后调用row.append()
将新行添加到表格的末尾。在一组写操作后调用flush()
可以确保这些更改同步到磁盘。PyTables 使用复杂的缓存机制来确保在表格中读写数据时获得最大性能,因此新行不会立即写入磁盘。
PyTables 支持高效的 SQL-like 查询,称为内核查询。包含查询表达式的字符串会在所有行上编译并评估。较低效的方法是使用table.iterrows()
遍历所有行,并对行的字段使用if
语句。
还有更多内容...
这里有一些参考资料:
-
PyTables 和 HDF5 的替代方案可能来自 Blaze 项目,该项目在撰写时仍处于早期开发阶段。有关 Blaze 的更多信息,请参考
blaze.pydata.org
。
另见
- 使用 HDF5 和 PyTables 操作大数组的教程
第五章:高性能计算
在本章中,我们将涵盖以下主题:
-
使用 Numba 和即时编译加速纯 Python 代码
-
使用 Numexpr 加速数组计算
-
使用 ctypes 将 C 库包装为 Python
-
使用 Cython 加速 Python 代码
-
通过编写更少的 Python 代码和更多的 C 代码来优化 Cython 代码
-
使用 Cython 和 OpenMP 释放 GIL,利用多核处理器
-
使用 CUDA 为 NVIDIA 图形卡(GPU)编写大规模并行代码
-
使用 OpenCL 为异构平台编写大规模并行代码
-
使用 IPython 将 Python 代码分配到多个核心
-
在 IPython 中与异步并行任务进行交互
-
在 IPython 中使用 MPI 并行化代码
-
在 notebook 中尝试 Julia 语言
介绍
上一章介绍了代码优化的技术。有时,这些方法不足以满足需求,我们需要诉诸于更高级的高性能计算技术。
在本章中,我们将看到三种广泛的方法类别,它们并不互相排斥:
-
即时编译(JIT)Python 代码
-
从 Python 转向低级语言,如 C
-
使用并行计算将任务分配到多个计算单元
通过即时编译,Python 代码会动态编译成低级语言。编译发生在运行时,而不是执行之前。由于代码是编译的而非解释的,因此运行速度更快。JIT 编译是一种流行的技术,因为它能够同时实现快速和高级语言,而这两个特点在过去通常是互相排斥的。
JIT 编译技术已经在如 Numba、Numexpr、Blaze 等包中实现。在本章中,我们将介绍前两个包。Blaze 是一个有前景的项目,但在写本书时,它仍处于起步阶段。
我们还将介绍一种新的高级语言,Julia,它使用 JIT 编译实现高性能。这种语言可以在 IPython notebook 中有效使用,得益于 IJulia 包。
注意
PyPy (pypy.org
),Psyco 的继任者,是另一个相关的项目。这个 Python 的替代实现(参考实现是 CPython)集成了 JIT 编译器。因此,它通常比 CPython 快得多。然而,在本书写作时,PyPy 还不完全支持 NumPy。此外,PyPy 和 SciPy 往往形成各自独立的社区。
求助于如 C 这样的低级语言是另一种有趣的方法。流行的库包括ctypes、SWIG 或 Cython。使用 ctypes 需要编写 C 代码并能够访问 C 编译器,或者使用编译过的 C 库。相比之下,Cython 允许我们在 Python 的超集语言中编写代码,然后将其转译为 C,并带来不同的性能结果。不幸的是,编写高效的 Cython 代码并不总是容易的。在本章中,我们将介绍 ctypes 和 Cython,并展示如何在复杂示例中实现显著的加速。
最后,我们将介绍两类并行计算技术:使用 IPython 利用多个 CPU 核心和使用大规模并行架构,如图形处理单元(GPU)。
这里有一些参考资料:
-
Travis Oliphant 关于 PyPy 和 NumPy 的博客文章,内容可以在
technicaldiscovery.blogspot.com/2011/10/thoughts-on-porting-numpy-to-pypy.html
找到 -
与 C 语言接口的 Python 教程,详细内容请参考
scipy-lectures.github.io/advanced/interfacing_with_c/interfacing_with_c.html
CPython 和并发编程
Python 有时因为对多核处理器的本地支持较差而受到批评。让我们来解释一下原因。
Python 语言的主流实现是用 C 编写的CPython。CPython 集成了一种机制,称为全局解释器锁(GIL)。正如在wiki.python.org/moin/GlobalInterpreterLock
中所提到的:
GIL 通过防止多个本地线程同时执行 Python 字节码来促进内存管理。
换句话说,通过禁用一个 Python 进程中的并发线程,GIL 大大简化了内存管理系统。因此,内存管理在 CPython 中并不是线程安全的。
一个重要的影响是,使用 CPython 时,一个纯 Python 程序无法轻松地在多个核心上并行执行。这是一个重要问题,因为现代处理器的核心数量越来越多。
我们有什么可能的解决方案,以便利用多核处理器?
-
移除 CPython 中的 GIL。这个解决方案曾经被尝试过,但从未被纳入 CPython。这会在 CPython 的实现中带来太多复杂性,并且会降低单线程程序的性能。
-
使用多个进程而非多个线程。这是一个流行的解决方案;可以使用原生的multiprocessing模块,或者使用 IPython。在本章中,我们将介绍后者。
-
用 Cython 重写代码的特定部分,并用 C 语言变量替换所有 Python 变量。这样可以暂时移除 GIL,使得在循环中能够使用多核处理器。我们将在释放 GIL 以利用 Cython 和 OpenMP 实现多核处理器的配方中讨论这一解决方案。
-
用一种对多核处理器有更好支持的语言实现代码的特定部分,并从你的 Python 程序中调用它。
-
让你的代码使用可以从多核处理器中受益的 NumPy 函数,如
numpy.dot()
。NumPy 需要用 BLAS/LAPACK/ATLAS/MKL 进行编译。
关于 GIL 的必读参考资料可以在www.dabeaz.com/GIL/
找到。
与编译器相关的安装说明
在本节中,我们将提供一些使用编译器与 Python 配合的说明。使用场景包括使用 ctypes、使用 Cython 以及构建 Python 的 C 扩展。
Linux
在 Linux 上,你可以安装GCC(GNU 编译器集合)。在 Ubuntu 或 Debian 上,你可以通过命令sudo apt-get install build-essential
安装 GCC。
Mac OS X
在 Mac OS X 上,你可以安装 Apple XCode。从 XCode 4.3 开始,你必须通过 XCode 菜单中的Preferences | Downloads | Command Line Tools手动安装命令行工具。
Windows
在 Windows 上,使用编译器与 Python 配合使用是著名的麻烦。通常很难在线找到所有必要的说明。我们在这里详细说明这些说明(你也可以在本书的 GitHub 仓库中找到它们):
说明根据你使用的是 32 位还是 64 位版本的 Python,以及你使用的是 Python 2.x 还是 Python 3.x 有所不同。要快速了解这些信息,你可以在 Python 终端中键入以下命令:
import sys
print(sys.version)
print(64 if sys.maxsize > 2**32 else 32)
Python 32 位
-
首先,你需要安装一个 C 编译器。对于 Python 32 位,你可以从
www.mingw.org
下载并安装 MinGW,它是 GCC 的开源发行版。 -
根据你所使用的
distutils
库的版本,你可能需要手动修复其源代码中的一个 bug。用文本编辑器打开C:\Python27\Lib\distutils\cygwinccompiler.py
(或者根据你的具体配置路径类似的文件),并将所有-mno-cygwin
的出现位置替换为空字符串。 -
打开或创建一个名为
distutils.cfg
的文本文件,路径为C:\Python27\Lib\distutils\
,并添加以下行:[build] compiler = mingw32
Python 64 位
-
对于 Python 2.x,你需要 Visual Studio 2008 Express。对于 Python 3.x,你需要 Visual Studio 2010 Express。
-
你还需要微软 Windows SDK(根据你的 Python 版本选择 2008 或 2010 版本):
-
Python 2.x:可在
www.microsoft.com/en-us/download/details.aspx?id=3138
下载 Windows 7 和.NET Framework 3.5 的微软 Windows SDK。 -
Python 3.x:适用于 Windows 7 和.NET Framework 4 的 Microsoft Windows SDK,下载地址:
www.microsoft.com/en-us/download/details.aspx?id=8279
-
-
确保包含
cl.exe
的文件夹路径在系统的PATH
环境变量中。该路径应类似于C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\amd64
(使用适用于 Windows 7 和.NET Framework 3.5 的 Microsoft Windows SDK 中的 Visual Studio 2008 C 编译器)。 -
每次你想要在 Python 中使用编译器时(例如,在输入
ipython notebook
之前),需要在 Windows 的命令行终端中执行几个命令:call "C:\Program Files\Microsoft SDKs\Windows\v7.0\Bin\SetEnv.Cmd" /x64 /release set DISTUTILS_USE_SDK=1
DLL 地狱
使用编译包时,特别是那些在 Chris Gohlke 的网页上获取的包(www.lfd.uci.edu/~gohlke/pythonlibs/
),可能会遇到一些晦涩的 DLL 相关错误。要解决这些问题,你可以使用 Dependency Walker 打开这些无效的 DLL,Dependency Walker 的下载地址为www.dependencywalker.com
。该程序可以告诉你缺少了哪个 DLL 文件,你可以在电脑上搜索它并将其位置添加到PATH
环境变量中。
参考资料
以下是一些参考资料:
-
在 Windows 上安装 Cython,参考
wiki.cython.org/InstallingOnWindows
-
在 Windows 64 位上使用 Cython,参考
github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows
-
为 Windows 构建 Python wheel,参考
cowboyprogrammer.org/building-python-wheels-for-windows/
使用 Numba 和即时编译加速纯 Python 代码
Numba(numba.pydata.org
)是由 Continuum Analytics(www.continuum.io
)创建的一个包。截止本文撰写时,Numba 仍然是一个较新的、相对实验性的包,但其技术前景可期。Numba 会自动(即时)将纯 Python 代码翻译成优化后的机器码。实际上,这意味着我们可以在纯 Python 中编写一个非矢量化的函数,使用for
循环,并通过使用一个装饰器自动将该函数矢量化。与纯 Python 代码相比,性能的提升可以达到几个数量级,甚至可能超越手动矢量化的 NumPy 代码。
在本节中,我们将展示如何加速生成曼德尔布罗特分形的纯 Python 代码。
准备工作
安装 Numba 最简单的方法是使用 Anaconda 发行版(也是 Continuum Analytics 维护的),然后在终端输入conda install numba
。在 Windows 上,另一种选择是从 Chris Gohlke 的网页下载二进制安装程序,网址为www.lfd.uci.edu/~gohlke/pythonlibs/#numba
。在这种情况下,有一些依赖项(Numpy-MKL、LLVMPy、llvmmath 和 Meta),它们都可以在同一页面上找到。
如何实现…
-
让我们导入 NumPy 并定义几个变量:
In [1]: import numpy as np In [2]: size = 200 iterations = 100
-
以下函数使用纯 Python 生成分形。它接受一个空数组
m
作为参数。In [3]: def mandelbrot_python(m, size, iterations): for i in range(size): for j in range(size): c = -2 + 3./size*j + 1j*(1.5-3./size*i) z = 0 for n in range(iterations): if np.abs(z) <= 10: z = z*z + c m[i, j] = n else: break
-
让我们运行这个模拟并显示分形图:
In [4]: m = np.zeros((size, size)) mandelbrot_python(m, size, iterations) In [5]: import matplotlib.pyplot as plt %matplotlib inline plt.imshow(np.log(m), cmap=plt.cm.hot) plt.xticks([]); plt.yticks([])
曼德尔布罗特分形
-
现在,我们评估这个函数所用的时间:
In [6]: %%timeit m = np.zeros((size, size)) mandelbrot_python(m, size, iterations) 1 loops, best of 1: 6.18 s per loop
-
让我们尝试使用 Numba 加速这个函数。首先,我们导入这个包:
In [7]: import numba from numba import jit, complex128
-
接下来,我们在函数定义上方添加
@jit
装饰器。Numba 会尝试自动推断局部变量的类型,但我们也可以显式地指定类型:In [8]: @jit(locals=dict(c=complex128, z=complex128)) def mandelbrot_numba(m, size, iterations): for i in range(size): for j in range(size): c = -2 + 3./size*j + 1j*(1.5-3./size*i) z = 0 for n in range(iterations): if np.abs(z) <= 10: z = z*z + c m[i, j] = n else: break
-
这个函数与纯 Python 版本的功能完全相同。它到底快了多少呢?
In [10]: %%timeit m = np.zeros((size, size)) mandelbrot_numba(m, size, iterations) 1 loops, best of 10: 44.8 ms per loop
这里 Numba 版本的速度比纯 Python 版本快超过 100 倍!
它是如何工作的…
Python 字节码通常在运行时由 Python 解释器(例如 CPython)解释执行。相比之下,Numba 函数会在执行前直接解析并转换为机器代码,使用名为LLVM(低级虚拟机)的强大编译器架构。引用官方文档:
Numba 能识别 NumPy 数组作为已类型化的内存区域,因此可以加速使用 NumPy 数组的代码。其他类型不太明确的代码将被转换为 Python C-API 调用,从而有效地去除了“解释器”,但并未去除动态间接性。
Numba 并不是能够编译所有 Python 函数。对于局部变量的类型也有一些微妙的限制。Numba 会尝试自动推断函数变量的类型,但并不总是成功。在这种情况下,我们可以显式地指定类型。
Numba 通常在涉及 NumPy 数组的紧密循环(如本食谱中的示例)时能提供最显著的加速。
注意
Blaze 是 Continuum Analytics 的另一个项目,它是 NumPy 的下一代版本。它将提供比 NumPy 数组更具灵活性的数据结构,并且支持外存计算。与 Numba 一起,Blaze 将形成一个高效的类似编译器的基础设施,用于大数据算法和复杂的数值模拟。我们可以预期 Blaze 在未来会发挥重要作用,因为它应该将 Python 简洁易用的语法与原生代码的性能和并行处理技术(特别是多核处理器和图形处理单元)相结合。其他值得一提的相关项目,虽然稍微比 Blaze 和 Numba 旧,但仍有价值,包括Theano和Numexpr(我们将在下一个食谱中看到它们)。
还有更多内容…
让我们比较 Numba 与使用 NumPy 手动向量化代码的性能,后者是加速纯 Python 代码的标准方法,例如本教程中的代码。实际上,这意味着将 i
和 j
循环中的代码替换为数组计算。在这里,这相对容易,因为这些操作紧跟 单指令多数据 (SIMD) 的范式:
In [1]: import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
In [2]: def initialize(size):
x, y = np.meshgrid(np.linspace(-2, 1, size),
np.linspace(-1.5, 1.5, size))
c = x + 1j*y
z = c.copy()
m = np.zeros((size, size))
return c, z, m
In [3]: size = 200
iterations = 100
def mandelbrot_numpy(c, z, m, iterations):
for n in range(iterations):
indices = np.abs(z) <= 10
z[indices] = z[indices]**2 + c[indices]
m[indices] = n
In [4]: %%timeit -n1 -r10 c, z, m = initialize(size)
mandelbrot_numpy(c, z, m, iterations)
1 loops, best of 10: 245 ms per loop
在这里,Numba 超过了 NumPy。然而,我们不能仅凭这一个实验得出明确的结论。Numba 或 NumPy 哪个更快,取决于算法的具体实现、仿真参数、机器特性等因素。
这里有一些参考资料:
-
Numba 文档,请见
numba.pydata.org/doc.html
-
Numba 支持的类型,请见
numba.pydata.org/numba-doc/dev/types.html
-
Numba 示例请见
numba.pydata.org/numba-doc/dev/examples.html
-
Blaze 可用,请见
blaze.pydata.org
-
Theano 可用,请见
deeplearning.net/software/theano/
另见
- 使用 Numexpr 加速数组计算 的方案
使用 Numexpr 加速数组计算
Numexpr 是一个改进了 NumPy 弱点的包;复杂数组表达式的评估有时会很慢。原因是,在中间步骤中会创建多个临时数组,这在处理大数组时不是最优的。Numexpr 评估涉及数组的代数表达式,对其进行解析、编译,最后比 NumPy 更快地执行它们。
这个原理与 Numba 有些相似,因为普通的 Python 代码是通过 JIT 编译器动态编译的。然而,Numexpr 只处理代数数组表达式,而不是任意的 Python 代码。我们将在本教程中看到它是如何工作的。
准备工作
你可以在 github.com/pydata/numexpr
的文档中找到安装 Numexpr 的说明。
如何实现……
-
让我们导入 NumPy 和 Numexpr:
In [1]: import numpy as np import numexpr as ne
-
然后,我们生成三个大向量:
In [2]: x, y, z = np.random.rand(3, 1000000)
-
现在,我们评估 NumPy 计算涉及我们向量的复杂代数表达式所需的时间:
In [3]: %timeit x + (y**2 + (z*x + 1)*3) 10 loops, best of 3: 48.1 ms per loop
-
让我们用 Numexpr 执行相同的计算。我们需要将表达式作为字符串给出:
In [4]: %timeit ne.evaluate('x + (y**2 + (z*x + 1)*3)') 100 loops, best of 3: 11.5 ms per loop
-
Numexpr 可以使用多个核心。在这里,我们有 2 个物理核心和 4 个虚拟线程,支持英特尔超线程技术。我们可以使用
set_num_threads()
函数指定希望 Numexpr 使用的核心数:In [5]: ne.ncores Out[5]: 4 In [6]: for i in range(1, 5): ne.set_num_threads(i) %timeit ne.evaluate('x + (y**2 + (z*x + 1)*3)') 10 loops, best of 3: 19.4 ms per loop 10 loops, best of 3: 14 ms per loop 10 loops, best of 3: 12.8 ms per loop 10 loops, best of 3: 11.5 ms per loop
它是如何工作的……
Numexpr 会分析数组表达式,对其进行解析,并将其编译成低级语言。Numexpr 知道 CPU 向量化指令和 CPU 缓存特性。因此,Numexpr 可以动态优化向量化计算。
Numexpr、Numba 和 Blaze 之间存在一定的重叠。我们可以预期将来这些项目之间会有一些交叉影响。
另见
- 使用 Numba 和即时编译加速纯 Python 代码 的食谱
使用 ctypes 在 Python 中包装 C 库
在 Python 中包装 C 库使我们能够利用现有的 C 代码,或在像 C 这样快速的语言中实现代码的关键部分。
使用 Python 调用外部编译的库相对简单。第一种方式是通过 os.system
命令调用命令行可执行文件,但这种方法不能扩展到已编译的库(在 Windows 上,动态链接库,或称 DLL)。更强大的方法是使用一个名为 ctypes 的本地 Python 模块。该模块允许我们从 Python 调用在编译库中定义的函数(这些库是用 C 编写的)。ctypes
模块负责 C 和 Python 之间的数据类型转换。此外,numpy.ctypeslib
模块提供了在外部库使用数据缓冲区的地方使用 NumPy 数组的功能。
在这个示例中,我们将用 C 重写 Mandelbrot 分形的代码,将其编译成共享库,并从 Python 调用它。
准备工作
这段代码是为 Windows 编写的,可以通过一些小修改适配到其他系统。
需要一个 C 编译器。你将在本章的介绍部分找到所有与编译器相关的指令。特别是,要使 C 编译器在 Windows 上工作,你需要在启动 IPython 笔记本之前,在 Windows 终端执行一系列指令。你将在书籍的仓库中找到包含本章代码的文件夹中的批处理脚本,里面有适当的指令。
如何操作…
第一步是用 C 编写并编译 Mandelbrot 示例。第二步是使用 ctypes 从 Python 访问库。如果你只关心如何访问一个已编译的库,可以直接跳到第 3 步,假设 mandelbrot.dll
是一个已编译的库,定义了名为 mandelbrot()
的函数。
-
让我们用 C 编写 Mandelbrot 分形的代码:
In [1]: %%writefile mandelbrot.c // Needed when creating a DLL. #define EXPORT __declspec(dllexport) #include "stdio.h" #include "stdlib.h" // This function will be available in the DLL. EXPORT void __stdcall mandelbrot(int size, int iterations, int *col) { // Variable declarations. int i, j, n, index; double cx, cy; double z0, z1, z0_tmp, z0_2, z1_2; // Loop within the grid. for (i = 0; i < size; i++) { cy = -1.5 + (double)i / size * 3; for (j = 0; j < size; j++) { // We initialize the loop of the // system. cx = -2.0 + (double)j / size * 3; index = i * size + j; // Let's run the system. z0 = 0.0; z1 = 0.0; for (n = 0; n < iterations; n++) { z0_2 = z0 * z0; z1_2 = z1 * z1; if (z0_2 + z1_2 <= 100) { // Update the system. z0_tmp = z0_2 - z1_2 + cx; z1 = 2 * z0 * z1 + cy; z0 = z0_tmp; col[index] = n; } else { break; } } } } }
-
现在,让我们使用 Microsoft Visual Studio 的
cl.exe
将这个 C 源文件构建成一个 DLL。/LD
选项指定创建 DLL:In [2]: !cl /LD mandelbrot.c /out:mandelbrot.dll Creating library mandelbrot.lib and object mandelbrot.exp
-
让我们使用 ctypes 访问库:
In [3]: import ctypes In [4]: # Load the DLL file in Python. lb = ctypes.CDLL('mandelbrot.dll') lib = ctypes.WinDLL(None, handle=lb._handle) # Access the mandelbrot function. mandelbrot = lib.mandelbrot
-
NumPy 和 ctypes 使我们能够包装 DLL 中定义的 C 函数:
In [5]: from numpy.ctypeslib import ndpointer In [6]: # Define the types of the output and arguments. mandelbrot.restype = None mandelbrot.argtypes = [ctypes.c_int, ctypes.c_int, ndpointer(ctypes.c_int)]
-
要使用这个函数,我们首先需要初始化一个空数组,并将其作为参数传递给
mandelbrot()
包装函数:In [7]: import numpy as np # We initialize an empty array. size = 200 iterations = 100 col = np.empty((size, size), dtype=np.int32) # We execute the C function. mandelbrot(size, iterations, col) In [8]: %timeit mandelbrot(size, iterations, col) 100 loops, best of 3: 12.5 ms per loop
-
我们在脚本的最后释放库:
In [9]: from ctypes.wintypes import HMODULE ctypes.windll.kernel32.FreeLibrary.argtypes = [HMODULE] ctypes.windll.kernel32.FreeLibrary(lb._handle)
如何运作…
在 C 代码中,__declspec(dllexport)
命令声明该函数在 DLL 中可见。__stdcall
关键字声明 Windows 上的标准调用约定。
mandelbrot()
函数接受以下参数:
-
col
缓冲区的大小(col
值是对应点位于原点周围圆盘内的最后一次迭代) -
迭代次数的数量
-
指针指向整数缓冲区
mandelbrot()
不返回任何值;相反,它更新了通过引用传递给函数的缓冲区(它是一个指针)。
为了在 Python 中包装这个函数,我们需要声明输入参数的类型。ctypes 模块为不同的数据类型定义了常量。此外,numpy.ctypeslib.ndpointer()
函数允许我们在 C 函数中需要指针的地方使用 NumPy 数组。传递给ndpointer()
的参数数据类型需要与传递给函数的 NumPy 数组的数据类型相对应。
一旦函数正确地被包装,它就可以像标准 Python 函数一样调用。这里,在调用mandelbrot()
之后,最初为空的 NumPy 数组被填充上了曼德布罗特分形。
还有更多……
SciPy 包含一个名为weave的模块,提供类似的功能。我们可以在 Python 字符串中编写 C 代码,然后让 weave 在运行时使用 C 编译器编译并执行它。这个模块似乎维护得不好,且似乎与 Python 3 不兼容。Cython 或 ctypes 可能是更好的选择。
ctypes 的一个更新替代品是 cffi(cffi.readthedocs.org
),它可能会更快且更方便使用。你还可以参考eli.thegreenplace.net/2013/03/09/python-ffi-with-ctypes-and-cffi/
。
使用 Cython 加速 Python 代码
Cython既是一种语言(Python 的超集),也是一个 Python 库。使用 Cython,我们从一个常规的 Python 程序开始,并添加有关变量类型的注释。然后,Cython 将这段代码翻译为 C,并将结果编译为 Python 扩展模块。最后,我们可以在任何 Python 程序中使用这个编译后的模块。
虽然 Python 中的动态类型会带来性能开销,但 Cython 中的静态类型变量通常会导致代码执行更快。
性能提升在 CPU 密集型程序中最为显著,特别是在紧密的 Python 循环中。相比之下,I/O 密集型程序预计从 Cython 实现中受益不大。
在这个示例中,我们将看到如何使用 Cython 加速曼德布罗特代码示例。
准备工作
需要一个 C 编译器。你将在本章的介绍部分找到所有与编译器相关的说明。
你还需要从www.cython.org
安装 Cython。在 Anaconda 中,你可以在终端输入conda install cython
。
如何做……
我们假设size
和iterations
变量已如前面的示例中定义。
-
要在 IPython 笔记本中使用 Cython,我们首先需要导入 IPython 提供的
cythonmagic
扩展:In [6]: %load_ext cythonmagic
-
作为第一次尝试,我们只需在
mandelbrot()
函数定义之前添加%%cython
魔法命令即可。内部, 该单元魔法会将单元编译为独立的 Cython 模块,因此所有必需的导入操作都需要在同一单元中完成。此单元无法访问交互式命名空间中定义的任何变量或函数:In [6]: %%cython import numpy as np def mandelbrot_cython(m, size, iterations): # The exact same content as in # mandelbrot_python (first recipe of # this chapter).
-
这个版本有多快?
In [7]: %%timeit -n1 -r1 m = np.zeros((size, size), dtype=np.int32) mandelbrot_cython(m, size, iterations) 1 loops, best of 1: 5.7 s per loop
这里几乎没有加速效果。我们需要指定 Python 变量的类型。
-
让我们使用类型化内存视图为 NumPy 数组添加类型信息(我们将在如何工作……部分进行解释)。我们还采用了稍微不同的方式来测试粒子是否已经逃离了域(
if
测试):In [8]: %%cython import numpy as np def mandelbrot_cython(int[:,::1] m, int size, int iterations): cdef int i, j, n cdef complex z, c for i in range(size): for j in range(size): c = -2 + 3./size*j + 1j*(1.5-3./size*i) z = 0 for n in range(iterations): if z.real**2 + z.imag**2 <= 100: z = z*z + c m[i, j] = n else: break
-
这个新版本有多快?
In [9]: %%timeit -n1 -r1 m = np.zeros((size, size), dtype=np.int32) mandelbrot_cython(m, size, iterations) 1 loops, best of 1: 230 ms per loop
我们所做的只是指定了局部变量和函数参数的类型,并且在计算
z
的绝对值时绕过了 NumPy 的np.abs()
函数。这些变化帮助 Cython 从 Python 代码生成了更优化的 C 代码。
如何工作……
cdef
关键字将变量声明为静态类型的 C 变量。C 变量能加速代码执行,因为它减少了 Python 动态类型带来的开销。函数参数也可以声明为静态类型的 C 变量。
通常情况下,在紧密循环中使用的变量应当使用cdef
声明。为了确保代码得到良好的优化,我们可以使用注解。只需在%%cython
魔法命令后添加-a
标志,未优化的行将以黄色渐变显示(白色行表示较快,黄色行表示较慢)。这一点可以通过以下截图看到。颜色的变化取决于每行相对的 Python API 调用次数。
Cython 中的注解
有两种方法可以使用 Cython 将 NumPy 数组声明为 C 变量:使用数组缓冲区或使用类型化内存视图。在这个示例中,我们使用了类型化内存视图。在下一个示例中,我们将介绍数组缓冲区。
类型化内存视图允许使用类似 NumPy 的索引语法高效地访问数据缓冲区。例如,我们可以使用int[:,::1]
来声明一个按 C 顺序存储的二维 NumPy 数组,数组元素类型为整数,::1
表示该维度是连续布局。类型化内存视图可以像 NumPy 数组一样进行索引。
然而,内存视图并不实现像 NumPy 那样的逐元素操作。因此,内存视图在紧密的for
循环中充当便捷的数据容器。对于逐元素的 NumPy 类似操作,应使用数组缓冲区。
通过将调用np.abs
替换为更快的表达式,我们可以显著提高性能。原因是np.abs
是一个 NumPy 函数,具有一定的调用开销。它是为处理较大数组而设计的,而不是标量值。这种开销在紧密的循环中会导致性能大幅下降。使用 Cython 注解可以帮助发现这一瓶颈。
还有更多内容……
使用 Cython 从 IPython 中进行操作非常方便,尤其是通过%%cython
单元魔法。然而,有时我们需要使用 Cython 创建一个可重用的 C 扩展模块。实际上,IPython 的%%cython
单元魔法在后台正是这么做的。
-
第一步是编写一个独立的 Cython 脚本,保存在
.pyx
文件中。它应当与%%cython
单元魔法的完整内容完全一致。 -
第二步是创建一个
setup.py
文件,我们将用它来编译 Cython 模块。以下是一个基本的setup.py
文件,假设有一个mandelbrot.pyx
文件:from distutils.core import setup from distutils.extension import Extension from Cython.Distutils import build_ext setup( cmdclass = {'build_ext': build_ext}, ext_modules = [Extension("mandelbrot", ["mandelbrot.pyx"])] )
-
第三步是用 Python 执行这个设置脚本:
In [3]: !python setup.py build_ext --inplace running build_ext cythoning mandelbrot.pyx to mandelbrot.c building 'mandelbrot' extension
-
在构建过程中创建了两个文件:C 源文件和已编译的 Python 扩展。文件扩展名在 Windows 上是
.pyd
(DLL 文件),在 UNIX 上是.so
:In [4]: !dir mandelbrot.* mandelbrot.c mandelbrot.pyd mandelbrot.pyx
-
最后,我们可以像往常一样加载已编译的模块(使用
from mandelbrot import mandelbrot
)。
使用这种技术,Cython 代码也可以集成到 Python 包中。以下是一些参考资料:
-
分发 Cython 模块,详见
docs.cython.org/src/userguide/source_files_and_compilation.html
-
使用 Cython 进行编译,详见
docs.cython.org/src/reference/compilation.html
另见
-
通过编写更少的 Python 代码和更多的 C 代码来优化 Cython 代码的食谱
-
释放 GIL 以利用多核处理器,使用 Cython 和 OpenMP的食谱
通过编写更少的 Python 代码和更多的 C 代码来优化 Cython 代码
在这个食谱中,我们将考虑一个更复杂的 Cython 示例。从一个纯 Python 中的慢实现开始,我们将逐步使用不同的 Cython 特性来加速它。
我们将实现一个非常简单的光线追踪引擎。光线追踪通过模拟光传播的物理属性来渲染场景。这种渲染方法能够生成照片级真实感的场景,但计算量非常大。
在这里,我们将渲染一个单一的球体,带有漫反射和镜面反射光照。首先,我们将给出纯 Python 版本的示例代码。然后,我们将逐步使用 Cython 加速它。
注意
代码很长,包含许多函数。我们将首先给出纯 Python 版本的完整代码。然后,我们将描述用 Cython 加速代码所需的更改。所有脚本可以在本书网站上找到。
如何操作…
-
首先,让我们实现纯 Python 版本:
In [1]: import numpy as np import matplotlib.pyplot as plt In [2]: %matplotlib inline In [3]: w, h = 200, 200 # Size of the window in pixels.
我们为向量创建一个归一化函数:
def normalize(x): # This function normalizes a vector. x /= np.linalg.norm(x) return x
现在,我们创建一个计算光线与球体交点的函数:
def intersect_sphere(O, D, S, R): # Return the distance from O to the intersection # of the ray (O, D) and the sphere (S, R), or # +inf if there is no intersection. # O and S are 3D points, D (direction) is a # normalized vector, R is a scalar. a = np.dot(D, D) OS = O - S b = 2 * np.dot(D, OS) c = np.dot(OS, OS) - R*R disc = b*b - 4*a*c if disc > 0: distSqrt = np.sqrt(disc) q = (-b - distSqrt) / 2.0 if b < 0 \ else (-b + distSqrt) / 2.0 t0 = q / a t1 = c / q t0, t1 = min(t0, t1), max(t0, t1) if t1 >= 0: return t1 if t0 < 0 else t0 return np.inf
以下函数进行光线追踪:
def trace_ray(rayO, rayD): # Find first point of intersection with the scene. t = intersect_sphere(rayO, rayD, position, radius) # No intersection? if t == np.inf: return # Find the point of intersection on the object. M = rayO + rayD * t N = normalize(M - position) toL = normalize(L - M) toO = normalize(O - M) # Ambient color. col = ambient # Diffuse color. col += diffuse * max(np.dot(N, toL), 0) * color # Specular color. col += specular_c * color_light * \ max(np.dot(N, normalize(toL + toO)), 0) \ ** specular_k return col
最后,主循环在以下函数中实现:
def run(): img = np.zeros((h, w, 3)) # Loop through all pixels. for i, x in enumerate(np.linspace(-1.,1.,w)): for j, y in enumerate(np.linspace(-1.,1.,h)): # Position of the pixel. Q[0], Q[1] = x, y # Direction of the ray going through the # optical center. D = normalize(Q - O) depth = 0 rayO, rayD = O, D # Launch the ray and get the # color of the pixel. col = trace_ray(rayO, rayD) if col is None: continue img[h - j - 1, i, :] = np.clip(col, 0, 1) return img
现在,我们初始化场景并定义一些参数:
In [4]: # Sphere properties. position = np.array([0., 0., 1.]) radius = 1. color = np.array([0., 0., 1.]) diffuse = 1. specular_c = 1. specular_k = 50 # Light position and color. L = np.array([5., 5., -10.]) color_light = np.ones(3) ambient = .05 # Camera. O = np.array([0., 0., -1.]) # Position. Q = np.array([0., 0., 0.]) # Pointing to.
让我们渲染场景:
In [5]: img = run() In [6]: plt.imshow(img) plt.xticks([]); plt.yticks([])
使用 Python 和 Cython 进行光线追踪。左侧:这个食谱的结果。右侧:扩展版本的结果。
-
这个实现有多慢?
In [7]: %timeit run() 1 loops, best of 1: 3.58 s per loop
-
如果我们只使用
%%cython
魔法并在单元格顶部添加适当的import numpy as np
和cimport numpy as np
命令,我们只能获得适度的改进,仅比原来快了十分之一秒。 -
我们可以通过提供变量类型的信息来做得更好。由于我们在 NumPy 数组上执行向量化计算,我们不能轻易使用内存视图。因此,我们将使用数组缓冲区。首先,在 Cython 模块(或
%%cython
单元格)的最开始,我们声明 NumPy 数据类型,如下所示:import numpy as np cimport numpy as np DBL = np.double ctypedef np.double_t DBL_C
然后,我们声明一个 NumPy 数组,使用
cdef np.ndarray[DBL_C, ndim=1]
(在此示例中,是一个一维的双精度浮点数数组)。这里有一个难点,因为 NumPy 数组只能在函数内部声明,不能在顶层声明。因此,我们需要稍微调整代码的整体架构,将一些数组作为函数参数传递,而不是使用全局变量。然而,即使声明了所有变量的类型,我们几乎没有获得任何加速。 -
在当前的实现中,由于大量对小数组(三个元素)的 NumPy 函数调用,我们遭遇了性能下降。NumPy 设计上是处理大数组的,对于这么小的数组使用它并没有多大意义。
在这个特定的情况下,我们可以尝试通过使用 C 标准库重写一些函数来绕过 NumPy。我们使用
cdef
关键字声明一个 C 风格的函数。这些函数可以带来显著的性能加速。在这里,通过将normalize()
Python 函数替换为以下 C 函数,我们获得了 2-3 倍的加速:from libc.math cimport sqrt cdef normalize(np.ndarray[DBL_C, ndim=1] x): cdef double n n = sqrt(x[0]*x[0] + x[1]*x[1] + x[2]*x[2]) x[0] /= n x[1] /= n x[2] /= n return x
-
为了获得最显著的加速,我们需要完全绕过 NumPy。那么,究竟在什么地方使用了 NumPy 呢?
-
许多变量是 NumPy 数组(主要是一维向量,包含三个元素)。
-
按元素操作会产生隐式的 NumPy API 调用。
-
我们还使用了一些 NumPy 内置函数,比如
numpy.dot()
。
为了在我们的示例中绕过 NumPy,我们需要根据具体需求重新实现这些功能。第一个选择是使用原生 Python 类型(例如元组)来表示向量,并编写 C 风格的函数来实现对元组的操作(假设它们总是有三个元素)。例如,两个元组相加可以实现如下:
cdef tuple add(tuple x, tuple y): return (x[0]+y[0], x[1]+y[1], x[2]+y[2])
我们获得了一个有趣的加速(相比纯 Python 快了 30 倍),但是通过使用纯 C 数据类型,我们可以做到更好。
-
-
我们将定义一个纯 C 结构体,而不是使用 Python 类型来表示我们的向量。换句话说,我们不仅绕过了 NumPy,还通过转向纯 C 代码绕过了 Python。为了在 Cython 中声明表示 3D 向量的 C 结构体,我们可以使用以下代码:
cdef struct Vec3: double x, y, z
要创建一个新的
Vec3
变量,我们可以使用以下函数:cdef Vec3 vec3(double x, double y, double z): cdef Vec3 v v.x = x v.y = y v.z = z return v
作为一个例子,以下是用于加法操作两个
Vec3
对象的函数:cdef Vec3 add(Vec3 u, Vec3 v): return vec3(u.x + v.x, u.y + v.y, u.z + v.z)
代码可以更新,以利用这些快速的 C 风格函数。最后,图像可以声明为 3D 内存视图。经过这些改动,Cython 实现的速度达到了约 12 毫秒,比纯 Python 版本快了 300 倍!
总结来说,我们通过将整个实现基本重写为 C 语言,并结合改进的 Python 语法,获得了非常有趣的加速效果。
如何运作…
简要说明一下光线追踪代码是如何工作的。我们将三维场景建模为包含平面和球体等物体(此处仅有一个球体)。此外,还有一个相机和一个表示渲染图像的平面:
光线追踪原理(“光线追踪图”由 Henrik 提供,来自 Wikimedia Commons)
代码中有一个主循环,遍历图像的所有像素。对于每个像素,我们从相机中心发射一条光线,穿过当前像素并与场景中的物体相交,计算该光线与物体的第一个交点。然后,我们根据物体材质的颜色、光源的位置、物体在交点处的法线等因素,计算像素的颜色。这里有几个受物理启发的光照方程,描述了颜色如何依赖于这些参数。在这里,我们使用Blinn-Phong 着色模型,包括环境光、漫反射光和镜面反射光组件:
Blinn-Phong 着色模型(“Phong 组件”,来自 Wikimedia Commons)
当然,完整的光线追踪引擎远比我们在这个示例中实现的复杂。我们还可以模拟其他光学和光照特性,如反射、折射、阴影、景深等。也可以将光线追踪算法实现到显卡上,进行实时的真实感渲染。这里有几个参考资料:
-
Blinn-Phong 着色模型的 Wikipedia 页面,详见
en.wikipedia.org/wiki/Blinn-Phong_shading_model
-
光线追踪的 Wikipedia 页面,详见
en.wikipedia.org/wiki/Ray_tracing_(graphics)
还有更多...
尽管功能强大,Cython 仍然需要对 Python、NumPy 和 C 有较好的理解。最显著的性能提升来自于将动态类型的 Python 变量转换为静态类型的 C 变量,尤其是在紧凑的循环中。
这里有几个参考资料:
-
可用的 Cython 扩展类型,详情见
docs.cython.org/src/userguide/extension_types.html
-
我们的光线追踪引擎的扩展版本,详见
gist.github.com/rossant/6046463
另见:
-
使用 Cython 加速 Python 代码 配方
-
使用 Cython 和 OpenMP 释放 GIL 以利用多核处理器 配方
使用 Cython 和 OpenMP 释放 GIL,以充分利用多核处理器
如我们在本章介绍中所看到的,CPython 的 GIL 阻止了纯 Python 代码利用多核处理器。使用 Cython,我们可以在代码的某个部分临时释放 GIL,从而启用多核计算。这是通过OpenMP完成的,OpenMP 是一个多处理 API,大多数 C 编译器都支持它。
在本食谱中,我们将看到如何在多个核心上并行化前一个食谱的代码。
准备工作
要在 Cython 中启用 OpenMP,您只需为编译器指定一些选项。除了一个好的 C 编译器之外,您无需在计算机上安装任何特殊的软件。有关更多详细信息,请参阅本章介绍中的说明。
在本食谱中,我们使用的是微软的 Visual C++编译器(适用于 Windows),但代码可以轻松适应其他系统。
如何实现…
我们的简单光线追踪引擎实现是极其并行的;有一个遍历所有像素的主循环,在这个循环中,完全相同的函数会被重复调用。循环迭代之间没有串扰。因此,理论上可以在并行中执行所有迭代。
在这里,我们将并行执行一个循环(遍历图像中的所有列),使用 OpenMP。
您可以在书籍网站上找到完整的代码。我们这里只展示最重要的步骤:
-
我们在
%%cython
魔法命令中添加以下选项:--compile-args=/openmp --link-args=/openmp
。具体语法可能依赖于您的编译器和/或系统。例如,/openmp
应该替换为 GCC 中的-fopenmp
。 -
我们导入了
prange()
函数:from cython.parallel import prange
-
我们在每个函数定义后添加
nogil
,以移除 GIL。在标记为nogil
的函数中,不能使用任何 Python 变量或函数。例如:cdef Vec3 add(Vec3 x, Vec3 y) nogil: return vec3(x.x + y.x, x.y + y.y, x.z + y.z)
-
要使用 OpenMP 在多个核心上并行运行循环,我们使用
prange()
:with nogil: for i in prange(w): # ...
在使用任何并行计算功能(如
prange()
)之前,需要释放 GIL。 -
通过这些更改,我们在四核处理器上达到了 4 倍的加速。
原理…
GIL(全局解释器锁)已经在本章的介绍中描述过。nogil
关键字告诉 Cython,某个特定的函数或代码段应该在没有 GIL 的情况下执行。当 GIL 被释放时,无法进行任何 Python API 调用,这意味着只能使用 C 变量和 C 函数(用cdef
声明的函数)。
另见
-
用 Cython 加速 Python 代码 食谱
-
通过编写更少的 Python 代码和更多的 C 代码来优化 Cython 代码 食谱
-
通过 IPython 将 Python 代码分配到多个核心 食谱
为 NVIDIA 显卡(GPU)编写大规模并行代码,使用 CUDA
图形处理单元(GPU)是专门用于实时渲染的强大处理器。我们几乎在任何计算机、笔记本、视频游戏主机、平板或智能手机中都能找到 GPU。它们的并行架构包含数十到数千个核心。视频游戏产业在过去二十年里一直在推动更强大 GPU 的发展。
GPUs(图形处理单元)已被广泛应用于现代超级计算机中(例如,位于橡树岭国家实验室的 Cray Titan,约 20 佩塔 FLOPS,约 20,000 个 CPU,以及大量的 NVIDIA GPUs)。今天,一个高端 $1000 的 GPU 大致相当于 2000 年的一个价值 $1 亿的超级计算机(约几个 teraFLOPS)。
注意
FLOPS 指的是每秒浮点运算次数(FLoating-point Operations Per Second)。一个 1 teraFLOPS 的 GPU 每秒可以执行高达一万亿次浮点运算。
自 2000 年代中期以来,GPU 已不再局限于图形处理。我们现在可以在 GPU 上实现科学算法。唯一的条件是算法必须遵循 SIMD(单指令多数据)范式,即一系列指令并行执行多个数据。这被称为 图形处理单元上的通用计算(GPGPU)。GPGPU 已被应用于多个领域:气象学、数据挖掘、计算机视觉、图像处理、金融、物理学、生物信息学等多个领域。为 GPU 编写代码可能具有挑战性,因为它需要理解硬件的内部架构。
CUDA 是由 NVIDIA 公司于 2007 年创建的专有 GPGPU 框架,是主要的 GPU 制造商之一。用 CUDA 编写的程序仅能在 NVIDIA 显卡上运行。还有另一个竞争性的 GPGPU 框架,称为 OpenCL,这是一个由其他主要公司支持的开放标准。OpenCL 程序可以在大多数厂商的 GPU 和 CPU 上运行(特别是 NVIDIA、AMD 和英特尔)。
在这个示例中,我们将展示一个非常基础的 GPGPU 示例。我们将使用 CUDA 实现 Mandelbrot 分形的非常简单的并行计算。在下一个示例中,我们将使用 OpenCL 实现完全相同的例子。
提示
对于一个新项目,你应该选择 OpenCL 还是 CUDA?答案主要取决于你用户群体的硬件配置。如果你需要在实验室中针对所有配备 NVIDIA 显卡的计算机实现尽可能高的性能,并且发布你的程序到全球并不是优先考虑的事情,你可以选择 CUDA。如果你计划将程序分发给使用不同平台的多人,你应该选择 OpenCL。在功能上,这两者大体上是相当的。
我们可以通过 PyCUDA 在 Python 中使用 CUDA,这是由 Andreas Klöckner 编写的一个 Python 包 (documen.tician.de/pycuda/
)。
准备工作
安装和配置 PyCUDA 通常不简单。首先,您需要一块 NVIDIA GPU。然后,您需要安装 CUDA SDK。最后,您需要安装并配置 PyCUDA。请注意,PyCUDA 依赖于一些外部包,特别是 pytools。
在 Windows 上,您应该使用 Chris Gohlke 的包。确保您的 CUDA 版本与 PyCUDA 包中使用的版本匹配。如果遇到 DLL 相关的问题,使用 Dependency Walker 检查 PyCUDA 安装文件夹中的*.pyd
文件(在 Anaconda 下,它应该位于C:\anaconda\lib\site-packages\pycuda
)。如果您使用的是 Windows 64 位,确保C:\Windows\SysWOW64
在您的系统 PATH 中。最后,确保您有与 Python 版本相对应的 Visual Studio 版本(请参阅本章开头关于 C 编译器的说明)。
您可以在以下链接找到更多信息:
-
可在
developer.nvidia.com/cuda-downloads
下载 CUDA SDK -
PyCUDA 维基可以在
wiki.tiker.net
找到
如何做……
-
让我们导入 PyCUDA:
In [1]: import pycuda.driver as cuda import pycuda.autoinit from pycuda.compiler import SourceModule import numpy as np
-
我们初始化一个将包含分形的 NumPy 数组:
In [2]: size = 200 iterations = 100 col = np.empty((size, size), dtype=np.int32)
-
我们为这个数组分配 GPU 内存:
In [3]: col_gpu = cuda.mem_alloc(col.nbytes)
-
我们将 CUDA 内核写入字符串中。
mandelbrot()
函数的参数包括:-
图像的大小
-
迭代次数
-
指针指向内存缓冲区
这个函数在每个像素上执行。它更新
col
缓冲区中的像素颜色:In [4]: code = """ __global__ void mandelbrot(int size, int iterations, int *col) { // Get the row and column of the thread. int i = blockIdx.y * blockDim.y + threadIdx.y; int j = blockIdx.x * blockDim.x + threadIdx.x; int index = i * size + j; // Declare and initialize the variables. double cx, cy; double z0, z1, z0_tmp, z0_2, z1_2; cx = -2.0 + (double)j / size * 3; cy = -1.5 + (double)i / size * 3; // Main loop. z0 = z1 = 0.0; for (int n = 0; n < iterations; n++) { z0_2 = z0 * z0; z1_2 = z1 * z1; if (z0_2 + z1_2 <= 100) { // Need to update both z0 and z1, // hence the need for z0_tmp. z0_tmp = z0_2 - z1_2 + cx; z1 = 2 * z0 * z1 + cy; z0 = z0_tmp; col[index] = n; } else break; } } """
-
-
现在,我们编译 CUDA 程序:
In [5]: prg = SourceModule(code) mandelbrot = prg.get_function("mandelbrot")
-
我们定义了块大小和网格大小,指定线程如何根据数据进行并行化:
In [6]: block_size = 10 block = (block_size, block_size, 1) grid = (size // block_size, size // block_size, 1)
-
我们调用编译后的函数:
In [7]: mandelbrot(np.int32(size), np.int32(iterations), col_gpu, block=block, grid=grid)
-
一旦函数执行完毕,我们将 CUDA 缓冲区的内容复制回 NumPy 数组
col
:In [8]: cuda.memcpy_dtoh(col, col_gpu)
-
现在,
col
数组包含了曼德尔布罗特分形。我们发现这个 CUDA 程序在移动 GeForce GPU 上执行时间为 0.7 毫秒。
它是如何工作的……
GPU 编程是一个丰富且高度技术性的主题,涉及 GPU 的低级架构细节。当然,我们这里只用最简单的范式(“极其并行”的问题)浅尝辄止。我们将在后续部分提供更多参考。
一个 CUDA GPU 有多个多处理器,每个多处理器有多个流处理器(也称为CUDA 核心)。每个多处理器与其他多处理器并行执行。在一个多处理器中,流处理器在相同时间执行相同的指令,但操作不同的数据位(SIMD 范式)。
CUDA 编程模型的核心概念包括内核、线程、块和网格:
-
内核是用类似 C 的语言编写的程序,运行在 GPU 上。
-
线程代表在一个流处理器上执行的一个内核。
-
一个块包含多个在一个多处理器上执行的线程。
-
一个网格包含多个块。
每个块的线程数受多处理器大小的限制,并且取决于显卡型号(在最近的型号中为 1024)。然而,一个网格可以包含任意数量的块。
在一个块内,线程通常以 warp(通常是 32 个线程)的形式执行。当内核中的条件分支组织成 32 个线程一组时,性能会更好。
块内的线程可以通过 __syncthreads()
函数在同步屏障处进行同步。此功能允许一个块内的线程之间进行通信。然而,块是独立执行的,因此来自不同块的两个线程不能同步。
在一个块内,线程被组织成 1D、2D 或 3D 结构,网格中的块也是如此,如下图所示。这个结构非常方便,因为它与现实世界问题中常见的多维数据集相匹配。
CUDA 编程模型(显示线程、块和网格 — 图像来自 NVIDIA 公司)
内核可以检索块内的线程索引(threadIdx
)以及网格内的块索引(blockIdx
),以确定它应该处理的数据部分。在这个配方中,分形的 2D 图像被划分为 10 x 10 的块,每个块包含 100 个像素,每个像素对应一个线程。内核 mandelbrot
计算单个像素的颜色。
GPU 上有多个内存层次,从块内少数线程共享的小而快的本地内存,到所有块共享的大而慢的全局内存。我们需要调整代码中的内存访问模式,以匹配硬件约束并实现更高的性能。特别地,当 warp 内的线程访问全局内存中的 连续 地址时,数据访问效率更高;硬件会将所有内存访问合并为对连续 DRAM(动态随机存取内存)位置的单次访问。
PyCUDA 让我们可以将数据从 NumPy 数组上传/下载到驻留在 GPU 上的缓冲区。这项操作通常非常耗费资源。复杂的现实世界问题常常涉及在 CPU 和 GPU 上都进行迭代步骤,因此二者之间的通信成为常见的性能瓶颈。当这些交换较少时,性能可以得到提升。
在 (Py)CUDA 的 C/Python 端有一些模板代码,涉及初始化 GPU、分配数据、将数据上传/下载到/从 GPU、编译内核、执行内核等等。你可以在 CUDA/PyCUDA 文档中找到所有的详细信息,但作为一种初步方法,你也可以直接复制并粘贴这个配方或任何教程中的代码。
还有更多内容…
这里有一些参考资料:
-
官方 CUDA 门户:
developer.nvidia.com/category/zone/cuda-zone
-
CUDA 的教育与培训,详情请见
developer.nvidia.com/cuda-education-training
-
关于 CUDA 的推荐书籍,详见
developer.nvidia.com/suggested-reading
-
关于选择 CUDA 还是 OpenCL,详见
wiki.tiker.net/CudaVsOpenCL
-
关于 CUDA 和 OpenCL 的博客文章,详见
streamcomputing.eu/blog/2011-06-22/opencl-vs-cuda-misconceptions/
另见
- 为异构平台编写大规模并行代码与 OpenCL食谱
为异构平台编写大规模并行代码与 OpenCL
在前一个食谱中,我们介绍了 CUDA,这是由 NVIDIA 公司创建的专有GPGPU 框架。在这个食谱中,我们介绍了 OpenCL,这是一个由苹果公司在 2008 年启动的开放框架。现在,主流公司,包括 Intel、NVIDIA、AMD、Qualcomm、ARM 等,均已采用。它们都属于非盈利的技术联盟Khronos Group(该联盟还维护着 OpenGL 实时渲染规范)。用 OpenCL 编写的程序可以在 GPU 和 CPU 上运行(异构计算)。
提示
在概念、语法和特性上,CUDA 和 OpenCL 相对相似。由于 CUDA 的 API 与硬件的匹配度比 OpenCL 的通用 API 更高,CUDA 有时能带来稍微更高的性能。
我们可以通过PyOpenCL在 Python 中使用 OpenCL,这是 Andreas Klöckner 编写的一个 Python 包(documen.tician.de/pyopencl/
)。
在这个食谱中,我们将使用 OpenCL 实现曼德布罗特分形。OpenCL 内核与前一个食谱中的 CUDA 内核非常相似。用于访问 OpenCL 的 Python API 与 PyCUDA 略有不同,但概念是相同的。
准备工作
安装 PyOpenCL 通常并不简单。第一步是安装适用于你硬件(CPU 和/或 GPU)的 OpenCL SDK。然后,你需要安装并配置 PyOpenCL。在 Windows 上,你应使用 Chris Gohlke 的包。上一食谱中的一些安装说明在这里同样适用。
这里有一些参考资料:
-
PyOpenCL Wiki 可在
wiki.tiker.net
访问 -
PyOpenCL 的文档可在
documen.tician.de/pyopencl/
查看
这里是各个 OpenCL SDK 的链接:
-
Intel 的 SDK 可在
software.intel.com/en-us/vcsource/tools/opencl-sdk
下载 -
AMD 的 SDK 可在
developer.amd.com/tools-and-sdks/heterogeneous-computing/
下载 -
NVIDIA 的 SDK 可在
developer.nvidia.com/opencl
下载
如何操作…
-
让我们导入 PyOpenCL:
In [1]: import pyopencl as cl import numpy as np
-
以下对象定义了一些与设备内存管理相关的标志:
In [2]: mf = cl.mem_flags
-
我们创建一个 OpenCL 上下文和一个命令队列:
In [3]: ctx = cl.create_some_context() queue = cl.CommandQueue(ctx)
-
现在,我们初始化将包含分形的 NumPy 数组:
In [4]: size = 200 iterations = 100 col = np.empty((size, size), dtype=np.int32)
-
我们为这个数组分配 GPU 内存:
In [5]: col_buf = cl.Buffer(ctx, mf.WRITE_ONLY, col.nbytes)
-
我们将 OpenCL 内核写入字符串中:
In [6]: code = """ __kernel void mandelbrot(int size, int iterations, global int *col) { // Get the row and column index of the thread. int i = get_global_id(1); int j = get_global_id(0); int index = i * size + j; // Declare and initialize the variables. double cx, cy; double z0, z1, z0_tmp, z0_2, z1_2; cx = -2.0 + (double)j / size * 3; cy = -1.5 + (double)i / size * 3; // Main loop. z0 = z1 = 0.0; for (int n = 0; n < iterations; n++) { z0_2 = z0 * z0; z1_2 = z1 * z1; if (z0_2 + z1_2 <= 100) { // Need to update both z0 and z1. z0_tmp = z0_2 - z1_2 + cx; z1 = 2 * z0 * z1 + cy; z0 = z0_tmp; col[index] = n; } else break; } } """
-
现在,我们编译 OpenCL 程序:
In [7]: prg = cl.Program(ctx, code).build() Build on <pyopencl.Device 'Intel(R) Core(TM) i3-2365M CPU @ 1.40GHz' on 'Intel(R) OpenCL' at 0x765b188> succeeded.
-
我们调用已编译的函数,并将命令队列、网格大小和缓冲区作为参数传递:
In [8]: prg.mandelbrot(queue, col.shape, None, np.int32(size), np.int32(iterations), col_buf).wait()
-
一旦函数执行完成,我们将 OpenCL 缓冲区的内容复制回 NumPy 数组
col
:In [9]: cl.enqueue_copy(queue, col, col_buf)
-
最后,我们可以通过
imshow()
显示 NumPy 数组col
来检查函数是否成功。我们还可以使用%timeit
进行快速基准测试,结果显示该函数在 Intel i3 双核 CPU 上大约需要 3.7 毫秒完成。
它是如何工作的……
前面食谱中详细介绍的原理同样适用于此。CUDA 和 OpenCL 之间存在术语上的差异:
-
CUDA 线程相当于 OpenCL 的工作项。
-
CUDA 块相当于 OpenCL 的工作组。
-
CUDA 网格相当于 OpenCL 的NDRange。
-
CUDA 流处理器相当于 OpenCL 的计算单元。
在内核中,我们可以使用 get_local_id(dim)
、get_group_id(dim)
和 get_global_id(dim)
获取工作项的索引。函数参数中的 global
限定符表示某个变量对应于全局内存中的对象。
OpenCL 上下文是工作项执行的环境。它包括带有内存和命令队列的设备。命令队列是主机应用程序用来将工作提交到设备的队列。
该程序在 CPU 或 GPU 上的表现相同,取决于安装的 OpenCL SDK 和可用的 OpenCL 上下文。如果存在多个上下文,PyOpenCL 可能会要求用户选择设备。上下文也可以通过编程方式指定(参见 documen.tician.de/pyopencl/runtime.html#pyopencl.Context
)。在 CPU 上,代码通过多核和使用 SSE 或 AVX 等向量指令进行并行化和向量化。
还有更多内容……
OpenCL 是一个相对年轻的标准,但我们应该预期它在未来会变得越来越重要。它得到了 GPU 行业内最大公司的支持。它支持与 OpenGL 的互操作性,OpenGL 是实时硬件加速计算机图形的行业标准(由同样的 Khronos Group 维护)。它正在逐步支持移动平台(智能手机和平板电脑),并且在浏览器中也在逐步支持,使用WebCL(在撰写本文时仍为草案阶段)。
这里是一些 OpenCL 资源:
-
OpenCL 教程可用:
opencl.codeplex.com
-
可用的课程列表:
developer.amd.com/partners/university-programs/opencl-university-course-listings/
-
关于 OpenCL 的书籍,见
streamcomputing.eu/knowledge/for-developers/books/
另见
- 为 NVIDIA 显卡(GPU)编写大规模并行代码(CUDA)的配方
使用 IPython 将 Python 代码分发到多个核心。
尽管 CPython 的 GIL 存在,仍然可以通过使用多个进程而不是多个线程,在多核计算机上并行执行多个任务。Python 提供了一个本地的multiprocessing模块。IPython 提供了一个更简单的界面,带来了强大的并行计算功能,并且支持交互式环境。我们将在这里描述这个工具。
如何操作…
-
首先,我们在单独的进程中启动四个 IPython 引擎。我们基本上有两个选项来做到这一点:
-
在系统 shell 中执行
ipcluster start -n 4
。 -
通过点击 Clusters 标签并启动四个引擎,使用 IPython 笔记本主页提供的网页界面。
-
-
然后,我们创建一个客户端,作为 IPython 引擎的代理。该客户端会自动检测正在运行的引擎:
In [2]: from IPython.parallel import Client rc = Client()
-
让我们检查一下正在运行的引擎数量:
In [3]: rc.ids Out[3]: [0, 1, 2, 3]
-
要在多个引擎上并行运行命令,我们可以使用
%px
行魔法命令或%%px
单元格魔法命令:In [4]: %%px import os print("Process {0:d}.".format(os.getpid())) [stdout:0] Process 2988. [stdout:1] Process 5192. [stdout:2] Process 4484. [stdout:3] Process 1360.
-
我们可以使用
--targets
或-t
选项指定在哪些引擎上运行命令:In [5]: %%px -t 1,2 # The os module has already been imported in # the previous cell. print("Process {0:d}.".format(os.getpid())) [stdout:1] Process 5192. [stdout:2] Process 4484.
-
默认情况下,
%px
魔法命令在阻塞模式下执行命令;只有当所有引擎上的命令完成时,单元格才会返回。也可以使用--noblock
或-a
选项运行非阻塞命令。在这种情况下,单元格会立即返回,任务的状态和结果可以从 IPython 的交互式会话中异步轮询:In [6]: %%px -a import time time.sleep(5) Out[6]: <AsyncResult: execute>
-
之前的命令返回了一个
ASyncResult
实例,我们可以用它来轮询任务的状态:In [7]: print(_.elapsed, _.ready()) (0.061, False)
-
%pxresult
会阻塞,直到任务完成:In [8]: %pxresult In [9]: print(_.elapsed, _.ready()) (5.019, True)
-
IPython 提供了适用于常见用例的方便函数,例如并行的
map
函数:In [10]: v = rc[:] res = v.map(lambda x: x*x, range(10)) In [11]: print(res.get()) [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
它是如何工作的…
将代码分发到多个核心的步骤如下:
-
启动多个 IPython 引擎(通常每个核心对应一个进程)。
-
创建一个作为这些引擎代理的
Client
。 -
使用客户端在引擎上启动任务并获取结果。
引擎是执行代码的 Python 进程,它们运行在不同的计算单元上。它们与 IPython 内核非常相似。
有两种主要的接口用于访问引擎:
-
使用直接接口,我们可以直接并显式地通过它们的标识符访问引擎。
-
使用负载均衡接口,我们通过一个接口访问引擎,该接口会自动并动态地将任务分配到合适的引擎。
我们也可以为替代的并行方式创建自定义接口。
在这个例子中,我们使用了直接接口;通过在%px
魔法命令中明确指定引擎的标识符,我们显式地访问了各个引擎。
正如我们在这个示例中所看到的,任务可以同步或异步启动。%px*
魔法命令在笔记本中尤其方便,因为它们让我们能够在多个引擎上无缝并行工作。
还有更多…
IPython 的并行计算功能为我们提供了一种简单的方式,可以在多个核心上并行启动独立的任务。一个更高级的用例是任务之间有 依赖 的情况。
依赖并行任务
依赖有两种类型:
-
功能依赖:它决定了给定任务是否可以在特定引擎上执行,依据引擎的操作系统、特定 Python 模块的存在与否或其他条件。IPython 提供了一个
@require
装饰器,用于那些需要特定 Python 模块才能在引擎上运行的函数。另一个装饰器是@depend
,它让我们可以定义任意的条件,这些条件在 Python 函数中实现,并返回True
或False
。 -
图依赖:它决定了给定任务是否可以在给定时间、特定引擎上执行。我们可能要求某个任务仅在一个或多个其他任务完成后才能执行。此外,我们可以在任何单独的引擎中强制执行这个条件;某个引擎可能需要先执行一组特定任务,才会执行我们的任务。例如,下面是如何要求任务 B 和 C(它们的异步结果分别为
arB
和arC
)在任务 A 启动前完成:with view.temp_flags(after=[arB, arC]): arA = view.apply_async(f)
IPython 提供了选项,指定任务运行所需的依赖是否全部或部分满足。此外,我们还可以指定是否应将依赖于成功和/或失败的任务视为满足条件。
当一个任务的依赖未满足时,调度器会将任务重新分配到一个引擎,再到另一个引擎,依此类推,直到找到合适的引擎。如果无法在任何引擎上满足依赖条件,任务将引发 ImpossibleDependency
错误。
在 IPython.parallel 中,将数据传递给依赖任务并不特别容易。一种可能性是在交互式会话中使用阻塞调用;等待任务完成,获取结果并将其发送回下一个任务。另一种可能性是通过文件系统在引擎之间共享数据,但这种方案在多个计算机上效果不好。一个替代方案可以参考:nbviewer.ipython.org/gist/minrk/11415238
。
替代的并行计算解决方案
除了 IPython,还有许多其他的 Python 并行计算框架,包括 ParallelPython、joblib 等等。
还有一些第三方(通常是商业)服务提供基于 Python 的云服务,如 PythonAnywhere 或 Wakari。它们通常有两种使用方式:
-
将大量计算任务分配到多个节点并行执行:我们可以使用数百或数千台服务器进行并行计算,而无需担心整个基础设施的维护(由公司负责处理)。
-
在线托管 Python 应用程序,通常带有 Web 界面:例如,使用 Wakari,IPython 笔记本可以在云端运行。一个有趣的应用场景是教学,学生可以通过连接到互联网的 Web 浏览器即时使用 IPython,而无需在本地安装任何软件。
参考资料
以下是关于 IPython.parallel 的一些参考资料:
-
IPython.parallel 文档,见
ipython.org/ipython-doc/dev/parallel/
-
IPython 开发者提供的 IPython 并行教程,见
nbviewer.ipython.org/github/minrk/IPython-parallel-tutorial/blob/master/Index.ipynb
-
IPython.parallel 中的依赖关系解释,见
ipython.org/ipython-doc/dev/parallel/parallel_task.html#dependencies
-
DAG 依赖关系的描述,见
ipython.org/ipython-doc/dev/parallel/dag_dependencies.html
-
有关 IPython.parallel 的高级技巧示例,见
github.com/ipython/ipython/tree/master/examples/Parallel%20Computing
以下是关于 Python 中替代并行计算解决方案的一些参考资料:
-
Parallel Python,见
www.parallelpython.com
-
可用的并行计算包列表,见
wiki.python.org/moin/ParallelProcessing
-
Python Anywhere,见
www.pythonanywhere.com
-
Wakari,见
wakari.io
-
在 Wakari 上使用 IPCluster 的介绍,见
continuum.io/blog/ipcluster-wakari-intro
-
使用 Wakari 进行教学的介绍,见
continuum.io/blog/teaching-with-wakari
另见
-
在 IPython 中与异步并行任务交互的示例
-
在 IPython 中使用 MPI 并行化代码的示例
在 IPython 中与异步并行任务交互
在这个示例中,我们将展示如何与在 IPython 中并行运行的异步任务交互。
准备工作
你需要启动 IPython 引擎(参见前面的步骤)。最简单的选项是从笔记本仪表板的 Clusters 标签页启动它们。在这个步骤中,我们使用了四个引擎。
如何做…
-
让我们导入一些模块:
In [1]: import time import sys from IPython import parallel from IPython.display import clear_output, display from IPython.html import widgets
-
我们创建一个
Client
:In [2]: rc = parallel.Client()
-
现在,我们创建一个基于 IPython 引擎的负载均衡视图:
In [3]: view = rc.load_balanced_view()
-
我们为并行任务定义一个简单的函数:
In [4]: def f(x): import time time.sleep(.1) return x*x
-
我们将在 100 个整数上并行运行此函数:
In [5]: numbers = list(range(100))
-
我们使用
map_async()
在所有引擎上并行执行f()
函数,作用对象是我们的numbers
列表。该函数会立即返回一个AsyncResult
对象,允许我们交互式地检索任务的相关信息:In [6]: ar = view.map_async(f, numbers)
-
该对象有一个
metadata
属性:所有引擎的字典列表。我们可以获取提交和完成日期、状态、标准输出和错误以及其他信息:In [7]: ar.metadata[0] Out[7]: { 'execute_result': None, 'engine_id': None, ... 'submitted': datetime.datetime(2014, 1, 1, 10, 30, 38, 9487), 'follow': None}
-
遍历
AsyncResult
实例时工作正常;迭代实时进行,任务在完成时进展:In [8]: for _ in ar: print(_, end=', ') 0, 1, 4,..., 9409, 9604, 9801,
-
现在,我们为异步任务创建一个简单的进度条。其思路是创建一个循环,每秒轮询任务状态。
IntProgressWidget
小部件实时更新,显示任务的进度:In [9]: def progress_bar(ar): # We create a progress bar. w = widgets.IntProgressWidget() # The maximum value is the number of tasks. w.max = len(ar.msg_ids) # We display the widget in the output area. display(w) # Repeat every second: while not ar.ready(): # Update the widget's value with the # number of tasks that have finished # so far. w.value = ar.progress time.sleep(1) w.value = w.max In [10]: ar = view.map_async(f, numbers) In [11]: progress_bar(ar)
以下截图显示了进度条:
-
最后,调试引擎上的并行任务非常容易。我们可以通过在
%%px
单元魔法中调用%qtconsole
来启动远程内核上的 Qt 客户端:In [12]: %%px -t 1 %qtconsole
Qt 控制台允许我们检查远程命名空间进行调试或分析,如下图所示:
用于调试 IPython 引擎的 Qt 控制台
它是如何工作的…
AsyncResult
实例由异步并行函数返回。它们实现了几个有用的属性和方法,特别是:
-
elapsed
: 自提交以来的经过时间 -
progress
: 到目前为止已完成的任务数量 -
serial_time
: 所有并行任务计算时间的总和 -
metadata
: 包含任务更多信息的字典 -
ready()
: 返回调用是否已完成 -
successful()
: 返回调用是否没有抛出异常(如果任务尚未完成则抛出异常) -
wait()
: 阻塞直到任务完成(有一个可选的超时参数) -
get()
: 阻塞直到任务完成并返回结果(有一个可选的超时参数)
还有更多…
这里是一些参考资料:
-
AsyncResult
类的文档请参阅ipython.org/ipython-doc/dev/parallel/asyncresult.html
-
任务接口的文档请参阅
ipython.org/ipython-doc/dev/parallel/parallel_task.html
-
实时打印引擎输出,示例代码可在
github.com/ipython/ipython/blob/master/examples/Parallel%20Computing/iopubwatcher.py
中找到
另见
-
使用 IPython 跨多个核心分发 Python 代码的配方
-
使用 MPI 在 IPython 中并行化代码的配方
使用 MPI 在 IPython 中并行化代码
消息传递接口(MPI)是一个用于并行系统的标准化通信协议。它在许多并行计算应用中被用来在节点之间交换数据。MPI 的入门门槛较高,但它非常高效且功能强大。
IPython 的并行计算系统从底层设计时就考虑到了与 MPI 的兼容。如果你是 MPI 的新手,从 IPython 开始使用它是个不错的选择。如果你是有经验的 MPI 用户,你会发现 IPython 与你的并行应用无缝集成。
在这个配方中,我们将看到如何通过一个非常简单的例子,使用 MPI 与 IPython 结合。
准备工作
要在 IPython 中使用 MPI,你需要:
-
mpi4py 包可以在
mpi4py.scipy.org
找到
例如,下面是 Ubuntu 和 Anaconda 上安装 MPI 的命令:
conda install mpich2
conda install mpi4py
你还可以使用pip install mpi4py
来安装 mpi4py。MPI 也可以在 Windows 上使用。可以参考Python Tools for Visual Studio的网站,网址为pytools.codeplex.com
,该网站提供了相关的安装指导。
如何实现……
-
我们首先需要创建一个 MPI 配置文件,命令为:
In [1]: !ipython profile create --parallel --profile=mpi
-
然后,我们打开
~/.ipython/profile_mpi/ipcluster_config.py
文件并添加一行:c.IPClusterEngines.engine_launcher_class = 'MPI'
。 -
一旦 MPI 配置文件创建并配置好,我们可以通过在终端中输入:
ipcluster start -n 2 --engines MPI --profile=mpi
来启动引擎。 -
现在,为了真正使用引擎,我们在笔记本中创建一个客户端:
In [2]: import numpy as np from IPython.parallel import Client In [3]: c = Client(profile='mpi')
-
让我们在所有引擎上创建一个视图:
In [4]: view = c[:]
-
在这个例子中,我们通过两个核心并行计算 0 到 15 之间所有整数的总和。我们首先将包含 16 个值的数组分配给各个引擎(每个引擎获取一个子数组):
In [5]: view.scatter('a', np.arange(16., dtype='float')) Out[5]: <AsyncResult: scatter>
-
我们使用 MPI 的
allreduce()
函数并行计算总和。每个节点执行相同的计算并返回相同的结果:In [6]: %%px from mpi4py import MPI import numpy as np print MPI.COMM_WORLD.allreduce(np.sum(a), op=MPI.SUM) [stdout:0] 120.0 [stdout:1] 120.0
提示
如果你得到了不同的结果,意味着引擎实际上并没有使用 MPI 启动(请参见stackoverflow.com/a/20159018/1595060
)。
它是如何工作的……
在这个例子中,每个节点:
-
接收整数的一个子集
-
计算这些整数的局部和
-
将这个局部和发送到所有其他引擎
-
接收其他引擎的局部和
-
计算这些局部和的总和
这是allreduce()
在 MPI 中的工作原理;其原理是首先分发数据到各个引擎,然后通过一个全局操作符(此处为MPI.SUM
)减少本地计算。
IPython 的直接接口也本地支持 scatter/gather 范式,而不需要使用 MPI。然而,这些操作只能在交互式会话中启动,而不能在引擎本身启动。
MPI 中还有许多其他的并行计算范式。你可以在这里找到更多信息:
-
Wes Kendall 的 MPI 教程,可以在
mpitutorial.com
找到 -
Blaise Barney(劳伦斯·利弗莫尔国家实验室)的 MPI 教程,可以在
computing.llnl.gov/tutorials/mpi/
找到
另见
-
使用 IPython 将 Python 代码分布到多个核心的食谱
-
在 IPython 中与异步并行任务交互的食谱
在笔记本中尝试 Julia 语言
Julia(julialang.org
)是一种年轻的高层次动态语言,专为高性能数值计算而设计。它的第一个版本在 2012 年发布,在 MIT 进行了三年的开发。Julia 借鉴了 Python、R、MATLAB、Ruby、Lisp、C 等语言的思想。它的主要优势是将高层次动态语言的表现力和易用性与 C 语言(几乎)的速度相结合。这是通过基于 LLVM 的即时编译器(JIT)实现的,目标是 x86-64 架构的机器代码。
在这个食谱中,我们将使用IJulia包在 IPython 笔记本中尝试 Julia,IJulia 包可以在github.com/JuliaLang/IJulia.jl
找到。我们还将展示如何从 Julia 使用 Python 包(如 NumPy 和 matplotlib)。具体来说,我们将计算并显示一个 Julia 集。
这个食谱灵感来自 David P. Sanders 在 SciPy 2014 会议上提供的 Julia 教程(nbviewer.ipython.org/github/dpsanders/scipy_2014_julia/tree/master/
)。
准备工作
首先,你需要安装 Julia。你可以在 Julia 官网julialang.org/downloads/
上找到适用于 Windows、Mac OS X 和 Linux 的安装包。在 Ubuntu 上,你可以在终端输入sudo apt-get install julia
。对于 IJulia,你还需要一个 C++编译器。在 Ubuntu 上,你可以输入sudo apt-get install build-essential
。
然后,使用julia
命令打开一个 Julia 终端,并在 Julia 终端中输入Pkg.add("IJulia")
来安装 IJulia。这个包还会在你的 IPython 安装中创建一个julia
配置文件。
最后,要启动 Julia 笔记本,请在终端中运行ipython notebook --profile=julia
。你将看到 IPython 笔记本的仪表板。唯一的区别是,在笔记本中使用的是 Julia 语言,而不是 Python。
这个食谱已在 Ubuntu 14.04 和 Julia 0.2.1 版本上进行测试。
如何实现…
-
我们无法避免传统的Hello World示例。
println()
函数用于显示一个字符串,并在末尾添加换行符:In [1]: println("Hello world!") Hello world!
-
我们创建了一个多态函数
f
,计算表达式z*z+c
。我们将在数组上评估这个函数,因此我们使用带点(.
)前缀的逐元素操作符:In [2]: f(z, c) = z.*z .+ c Out[2]: f (generic function with 1 method)
-
让我们在标量复数上评估
f
(虚数* i *是1im
)。In [3]: f(2.0 + 1.0im, 1.0) Out[3]: 4.0 + 4.0im
-
现在,我们创建一个(2, 2)矩阵。组件之间用空格隔开,行之间用分号(
;
)隔开。这个Array
的类型会根据其组件自动推断出来。Array
类型是 Julia 中的内建数据类型,类似但不完全等同于 NumPy 的ndarray
类型:In [4]: z = [-1.0 - 1.0im 1.0 - 1.0im; -1.0 + 1.0im 1.0 + 1.0im] Out[4]: 2x2 Array{Complex{Float64},2}: -1.0-1.0im 1.0-1.0im -1.0+1.0im 1.0+1.0im
-
我们可以使用方括号
[]
对数组进行索引。与 Python 的一个显著不同之处在于,索引是从 1 开始的,而不是 0。MATLAB 也有相同的惯例。此外,关键字end
表示该维度中的最后一个元素:In [5]: z[1,end] Out[5]: 1.0 - 1.0im
-
我们可以在矩阵
z
和标量c
(多态)上评估f
:In [6]: f(z, 0) Out[6]: 2x2 Array{Complex{Float64},2}: 0.0+2.0im 0.0-2.0im 0.0-2.0im 0.0+2.0im
-
现在,我们创建一个函数
julia
,用于计算 Julia 集。可选的命名参数与位置参数通过分号(;
)分隔。Julia 的流程控制语法接近 Python,但省略了冒号,缩进不重要,并且end
关键字是必需的:In [7]: function julia(z, c; maxiter=200) for n = 1:maxiter if abs2(z) > 4.0 return n-1 end z = f(z, c) end return maxiter end Out[7]: julia (generic function with 1 method)
-
我们可以在 Julia 中使用 Python 包。首先,我们需要通过 Julia 的内置包管理器(
Pkg
)安装PyCall
包。安装完成后,我们可以通过using PyCall
在交互式会话中使用它:In [8]: Pkg.add("PyCall") using PyCall
-
我们可以使用
@pyimport
宏(Julia 中的元编程特性)导入 Python 包。这个宏相当于 Python 中的import
命令:In [9]: @pyimport numpy as np
-
np
命名空间现在在 Julia 的交互式会话中可用。NumPy 数组会自动转换为 Julia 的Array
对象:In [10]: z = np.linspace(-1., 1., 100) Out[10]: 100-element Array{Float64,1}: -1.0 -0.979798 ... 0.979798 1.0
-
我们可以使用列表推导式来在多个参数上评估函数
julia
:In [11]: m = [julia(z[i], 0.5) for i=1:100] Out[11]: 100-element Array{Int64,1}: 2 ... 2
-
让我们尝试一下 Gadfly 绘图包。这个库提供了一个高级绘图接口,灵感来源于 Leland Wilkinson 博士的教材《图形语法》。在笔记本中,借助D3.js库,图表是交互式的:
In [12]: Pkg.add("Gadfly") using Gadfly In [13]: plot(x=1:100, y=m, Geom.point, Geom.line) Out[13]: Plot(...)
这是一个截图:
在 IPython 笔记本中用 Julia 绘制的 Gadfly 图:
-
现在,我们通过使用两个嵌套的循环来计算 Julia 集。通常,与 Python 不同,使用
for
循环而不是向量化操作并不会显著影响性能。高性能的代码可以通过向量化操作或for
循环来编写:In [14]: @time m = [julia(complex(r, i), complex(-0.06, 0.67)) for i = 1:-.001:-1, r = -1.5:.001:1.5]; elapsed time: 0.881234749 seconds (48040248 bytes allocated)
-
最后,我们使用
PyPlot
包来绘制 Julia 中的 matplotlib 图形:In [15]: Pkg.add("PyPlot") using PyPlot In [16]: imshow(m, cmap="RdGy", extent=[-1.5, 1.5, -1, 1]);
它是如何工作的……
过去,编程语言通常是低级的,使用起来困难但运行快速(例如 C);或者是高级的,使用方便但运行较慢(例如 Python)。在 Python 中,解决这个问题的方法包括 NumPy 和 Cython 等。
Julia 开发者选择创建一种新的高层次但快速的语言,将两者的优点结合在一起。这本质上通过使用 LLVM 实现的现代即时编译技术来实现。
Julia 动态解析代码并生成低级代码,采用 LLVM 中间表示。这种表示具有独立于语言的指令集,然后编译为机器码。使用显式循环编写的代码会直接编译为机器码。这也解释了为什么在 Julia 中通常不需要进行性能驱动的向量化代码。
还有更多……
Julia 的优势包括:
-
基于多重分派的强大灵活的动态类型系统,用于参数化多态性
-
支持元编程的功能
-
简单的接口,用于从 Julia 调用 C、FORTRAN 或 Python 代码
-
内建对精细粒度并行和分布式计算的支持
-
内建的多维数组数据类型和数值计算库
-
基于 Git 的内建包管理器
-
用于数据分析的外部包,如 DataFrames(相当于 pandas)和 Gadfly(统计绘图库)
-
在 IPython notebook 中的集成
Python 相对于 Julia 的优势是什么?截至本文撰写时,Julia 比 Python 和 SciPy 更年轻且不够成熟。因此,Julia 的包和文档比 Python 少。Julia 语言的语法仍在变化。此外,Python 在生产环境中的使用比 Julia 更为普遍。因此,将数值计算代码引入生产环境时,Python 代码会更容易。
话虽如此,Julia 生态系统和社区正在快速增长。我们可以合理预期,Julia 在未来将变得越来越受欢迎。而且,既然两种语言都可以在 IPython notebook 中使用,我们不必在 Python 和 Julia 之间选择。我们可以从 Julia 调用 Python 代码并使用 Python 模块,反之亦然。
我们在这篇食谱中仅仅触及了 Julia 语言的表面。我们未能详细覆盖的一些有趣话题包括 Julia 的类型系统、元编程功能、并行计算支持和包管理器等。
这里有一些参考资料:
-
Wikipedia 上的 Julia 语言条目可在
en.wikipedia.org/wiki/Julia_%28programming_language%29
查阅 -
Julia 官方文档可在
docs.julialang.org/en/latest/
阅读 -
我们为何创建 Julia 博客文章,可在
julialang.org/blog/2012/02/why-we-created-julia/
阅读 -
用于从 Julia 调用 Python 的 PyCall.jl,可在
github.com/stevengj/PyCall.jl
获取 -
用于在 Julia 中使用 matplotlib 的 PyPlot.jl 可在
github.com/stevengj/PyPlot.jl
获取 -
Gadfly.jl,一个用于绘图的 Julia 库,访问地址:
dcjones.github.io/Gadfly.jl/
-
DataFrames.jl,Julia 中类似 pandas 的库,访问地址:
juliastats.github.io/DataFrames.jl/
-
Julia Studio,适用于 Julia 的集成开发环境,访问地址:
forio.com/labs/julia-studio/
第六章:高级可视化
本章将涵盖以下主题:
-
使用 prettyplotlib 创建更漂亮的 matplotlib 图形
-
使用 seaborn 创建漂亮的统计图
-
使用 Bokeh 创建交互式 Web 可视化
-
在 IPython 笔记本中使用 D3.js 可视化 NetworkX 图形
-
使用 mpld3 将 matplotlib 图形转换为 D3.js 可视化
-
使用 Vispy 开始进行高性能交互式数据可视化
介绍
可视化是本书的一个核心主题。我们在大多数食谱中创建图形,因为这是传达定量信息的最有效方式。在大多数情况下,我们使用 matplotlib 来创建图表。在本章中,我们将看到 Python 中更高级的可视化功能。
首先,我们将看到几个包,它们可以让我们改善 matplotlib 图形的默认样式和类似 MATLAB 的 pyplot 接口。还有其他一些高层次的可视化编程接口,在某些情况下可能更方便。
此外,Web 平台正在越来越接近 Python。IPython 笔记本就是这种趋势的一个很好例子。在本章中,我们将看到一些技术和库,帮助我们在 Python 中创建交互式 Web 可视化。这些技术让我们能够将 Python 在数据分析方面的强大功能与 Web 在交互性方面的优势结合起来。
最后,我们将介绍 Vispy,这是一个新的高性能交互式可视化库,专为大数据而设计。
使用 prettyplotlib 创建更漂亮的 matplotlib 图形
matplotlib 有时因其图形的默认外观而受到批评。例如,默认的颜色映射既不具美学吸引力,也没有清晰的感知信息。
有很多方法试图规避这个问题。在本食谱中,我们将介绍prettyplotlib,这是由 Olga Botvinnik 创建的。这款轻量级 Python 库显著改善了许多 matplotlib 图形的默认样式。
准备工作
你可以在项目页面找到 prettyplotlib 的安装说明,网址是 github.com/olgabot/prettyplotlib
。你基本上只需在终端中执行 pip install prettyplotlib
即可。
如何实现……
-
首先,让我们导入 NumPy 和 matplotlib:
In [1]: import numpy as np import matplotlib.pyplot as plt import matplotlib as mpl %matplotlib inline
-
然后,我们用 matplotlib 绘制几条曲线:
In [2]: np.random.seed(12) for i in range(8): x = np.arange(1000) y = np.random.randn(1000).cumsum() plt.plot(x, y, label=str(i)) plt.legend()
注意
如果你正在阅读这本书的印刷版,你将看不到颜色。你可以在本书的网站上找到彩色图像。
-
现在,我们用 prettyplotlib 创建完全相同的图表。我们只需将
matplotlib.pyplot
命名空间替换为prettyplotlib
:In [3]: import prettyplotlib as ppl np.random.seed(12) for i in range(8): x = np.arange(1000) y = np.random.randn(1000).cumsum() ppl.plot(x, y, label=str(i)) ppl.legend()
-
让我们通过一个图像展示另一个示例。我们首先使用 matplotlib 的
pcolormesh()
函数将 2D 数组显示为图像:In [4]: np.random.seed(12) plt.pcolormesh(np.random.rand(16, 16)) plt.colorbar()
默认的彩虹颜色映射被认为会导致可视化数据被误解。
-
现在,我们使用 prettyplotlib 显示完全相同的图像:
In [5]: np.random.seed(12) ppl.pcolormesh(np.random.rand(16, 16))
这种可视化方式要更加清晰,因为高值或低值比彩虹色图更明显。
它是如何工作的…
prettyplotlib 只是对 matplotlib 的默认样式选项进行了微调。它的绘图接口基本上与 matplotlib 相同。要了解如何修改 matplotlib 的样式,值得查看 prettyplotlib 的代码。
还有更多内容…
改进 matplotlib 样式的其他方法有很多:
-
Randal Olson 的一篇博客文章解释了如何使用 matplotlib 创建干净且美观的图表;你可以在
www.randalolson.com/2014/06/28/how-to-make-beautiful-data-visualizations-in-python-with-matplotlib/
阅读到这篇文章。 -
matplotlib 正在进行一些工作,添加样式表支持;更多信息可以在
github.com/matplotlib/matplotlib/blob/master/doc/users/style_sheets.rst
找到。 -
关于为什么彩虹色图是误导性的,可以查看
eagereyes.org/basics/rainbow-color-map
另见
- 使用 seaborn 创建美丽的统计图 方案
使用 seaborn 创建美丽的统计图
matplotlib 配有一个名为 pyplot 的高级绘图 API。受 MATLAB 启发(MATLAB 是一个广泛使用的数值计算商业软件),这个接口对于科学家来说可能有些过于底层,因为它可能会导致难以阅读和维护的样板代码。然而,它可能是科学 Python 社区中最广泛使用的绘图接口之一。
存在更高级、更便捷的绘图接口。在这个方案中,我们介绍了 seaborn,它由 Michael Waskom 创建。这个库提供了一个高层次的绘图 API,专门为统计图形量身定制,同时与 pandas 紧密集成。
准备工作
你可以在项目页面 github.com/mwaskom/seaborn
上找到 seaborn 的安装说明。你只需要在终端输入 pip install seaborn
。
如何操作…
-
让我们导入 NumPy、matplotlib 和 seaborn:
In [1]: import numpy as np import matplotlib.pyplot as plt import seaborn as sns %matplotlib inline
-
我们生成一个随机数据集(参考 seaborn 网站上的示例
nbviewer.ipython.org/github/mwaskom/seaborn/blob/master/examples/linear_models.ipynb
):In [2]: x1 = np.random.randn(80) x2 = np.random.randn(80) x3 = x1 * x2 y1 = .5 + 2 * x1 - x2 + 2.5 * x3 + \ 3 * np.random.randn(80) y2 = .5 + 2 * x1 - x2 + 2.5 * np.random.randn(80) y3 = y2 + np.random.randn(80)
-
Seaborn 实现了许多易于使用的统计绘图函数。例如,下面是如何创建一个小提琴图。这种类型的图可以展示数据点的详细分布,而不仅仅是像箱形图那样显示四分位数:
In [3]: sns.violinplot([x1,x2, x3])
-
Seaborn 也实现了全功能的统计可视化函数。例如,我们可以使用一个单独的函数(
regplot()
)来执行并且显示两个变量之间的线性回归:In [4]: sns.regplot(x2, y2)
-
Seaborn 内建对 pandas 数据结构的支持。在这里,我们显示了
DataFrame
对象中所有变量之间的成对相关性:In [5]: df = pd.DataFrame(dict(x1=x1, x2=x2, x3=x3, y1=y1, y2=y2, y3=y3)) sns.corrplot(df)
还有更多…
除了 seaborn,还有其他高级绘图接口:
-
The Grammar of Graphics 是 Dr. Leland Wilkinson 撰写的一本书,影响了许多高级绘图接口,如 R 的 ggplot2、Python 中 yhat 的 ggplot 等。
-
Vega,由 Trifacta 提供,是一种声明式可视化语法,可以转换为 D3.js(一种 JavaScript 可视化库)。此外,Vincent 是一个 Python 库,让我们使用 Vega 创建可视化。
-
Tableau 的 VizQL 是一种面向商业数据库的可视化语言。
这里还有更多参考资料:
-
Vega 可在
trifacta.github.io/vega/
获取 -
Vincent 可在
vincent.readthedocs.org/en/latest/
获取 -
ggplot2 可在
ggplot2.org/
获取 -
Python 的 ggplot 可在
blog.yhathq.com/posts/ggplot-for-python.html
获取 -
VizQL 可在
www.tableausoftware.com/fr-fr/products/technology
获取
参见
- 使用 prettyplotlib 美化 matplotlib 图形 的配方
使用 Bokeh 创建交互式网页可视化
Bokeh 是一个用于在浏览器中创建丰富交互式可视化的库。图形在 Python 中设计,并完全在浏览器中渲染。在本配方中,我们将学习如何在 IPython notebook 中创建并渲染交互式 Bokeh 图形。
准备工作
按照网站上的说明在 bokeh.pydata.org
安装 Bokeh。原则上,你可以在终端中输入 pip install bokeh
。在 Windows 上,你也可以从 Chris Gohlke 的网站下载二进制安装程序,网址为 www.lfd.uci.edu/~gohlke/pythonlibs/#bokeh
。
如何实现…
-
让我们导入 NumPy 和 Bokeh。我们需要调用
output_notebook()
函数来告诉 Bokeh 在 IPython notebook 中渲染图形:In [1]: import numpy as np import bokeh.plotting as bkh bkh.output_notebook()
-
我们创建一些随机数据:
In [2]: x = np.linspace(0., 1., 100) y = np.cumsum(np.random.randn(100))
-
让我们画一条曲线:
In [3]: bkh.line(x, y, line_width=5) bkh.show()
一个交互式图形已在 notebook 中渲染。我们可以通过点击图形上方的按钮来平移和缩放:
使用 Bokeh 创建的交互式图形
-
让我们继续另一个例子。我们首先加载一个示例数据集(鸢尾花)。我们还根据花的种类生成一些颜色:
In [4]: from bokeh.sampledata.iris import flowers colormap = {'setosa': 'red', 'versicolor': 'green', 'virginica': 'blue'} flowers['color'] = flowers['species'].map( lambda x: colormap[x])
-
现在,我们渲染一个交互式散点图:
In [5]: bkh.scatter(flowers["petal_length"], flowers["petal_width"], color=flowers["color"], fill_alpha=0.25, size=10,) bkh.show()
使用 Bokeh 的交互式散点图
还有更多…
即使没有 Python 服务器,Bokeh 图表在笔记本中也是互动的。例如,我们的图表可以在 nbviewer 中进行互动。Bokeh 还可以从我们的图表生成独立的 HTML/JavaScript 文档。更多示例可以在图库中找到,网址为 bokeh.pydata.org/docs/gallery.html
。
Bokeh 提供了一个 IPython 扩展,简化了在笔记本中集成互动图表的过程。这个扩展可以在 github.com/ContinuumIO/bokeh/tree/master/extensions
找到。
同样地,我们还要提到 plot.ly,这是一个在线商业服务,提供用于 Web 基于的互动可视化的 Python 接口,网址为 plot.ly
。
另见
- 使用 mpld3 将 matplotlib 图表转换为 D3.js 可视化 这个食谱
使用 D3.js 在 IPython 笔记本中可视化 NetworkX 图表
D3.js (d3js.org
) 是一个流行的 Web 互动可视化框架。它是用 JavaScript 编写的,允许我们基于 Web 技术(如 HTML、SVG 和 CSS)创建数据驱动的可视化。虽然还有许多其他 JavaScript 可视化和图表库,但在本食谱中我们将重点介绍 D3.js。
作为纯 JavaScript 库,D3.js 原则上与 Python 无关。然而,基于 HTML 的 IPython 笔记本可以无缝集成 D3.js 可视化。
在这个食谱中,我们将使用 Python 的 NetworkX 创建一个图,并在 IPython 笔记本中使用 D3.js 进行可视化。
准备工作
本食谱需要你了解 HTML、JavaScript 和 D3.js 的基础知识。
如何操作…
-
让我们导入所需的包:
In [1]: import json import numpy as np import networkx as nx import matplotlib.pyplot as plt %matplotlib inline
-
我们加载了一个著名的社交图,该图于 1977 年发布,名为 Zachary's Karate Club graph。这个图展示了空手道俱乐部成员之间的友谊。俱乐部的主席和教练发生了争执,导致该小组分裂。在这里,我们只用 matplotlib(使用
networkx.draw()
函数)显示图表:In [2]: g = nx.karate_club_graph() nx.draw(g)
-
现在,我们将使用 D3.js 在笔记本中显示这个图。第一步是将图表导入到 JavaScript 中。我们选择将图表导出为 JSON 格式。D3.js 通常期望每个边都是一个具有源(source)和目标(target)的对象。此外,我们还指定了每个成员所代表的“俱乐部”属性(
club
)。NetworkX 提供了一个内置的导出功能,我们可以在这里使用:In [3]: from networkx.readwrite import json_graph data = json_graph.node_link_data(g) with open('graph.json', 'w') as f: json.dump(data, f, indent=4)
-
下一步是创建一个 HTML 对象,容纳可视化图表。我们在笔记本中创建了一个
<div>
元素,并为节点和链接(也叫边)指定了一些 CSS 样式:In [4]: %%html <div id="d3-example"></div> <style> .node {stroke: #fff; stroke-width: 1.5px;} .link {stroke: #999; stroke-opacity: .6;} </style>
-
最后一步比较复杂。我们编写 JavaScript 代码,从 JSON 文件加载图表并使用 D3.js 显示它。这里需要了解 D3.js 的基础知识(请参阅 D3.js 的文档)。代码较长,您可以在本书网站上找到完整代码。在这里,我们突出了最重要的步骤:
In [5]: %%javascript // We load the d3.js library. require(["d3"], function(d3) { // The code in this block is executed when the // d3.js library has been loaded. [...] // We create a force-directed dynamic graph // layout. var force = d3.layout.force().charge(-120). linkDistance(30).size([width, height]); [...] // In the <div> element, we create a <svg> graphic // that will contain our interactive // visualization. var svg = d3.select("#d3-example").select("svg"); [...] // We load the JSON file. d3.json("graph.json", function(error, graph) { // We create the graph here. force.nodes(graph.nodes).links(graph.links) .start(); // We create a <line> SVG element for each // link in the graph. var link = svg.selectAll(".link") .data(graph.links) .enter().append("line") .attr("class", "link"); // We create <circle> SVG elements for the // nodes. var node = svg.selectAll(".node") .data(graph.nodes) .enter().append("circle") [...] .style("fill", function(d) { return color(d.club); }) .call(force.drag); [...] }); });
当我们执行此单元格时,前一个单元格中创建的 HTML 对象会被更新。图表是动画的并且是互动的;我们可以点击节点,查看其标签,并在画布中移动它们:
使用 D3.js 在笔记本中的互动图
还有更多…
D3.js 的画廊包含更多 Web 上美丽的互动可视化示例。它们可以在 github.com/mbostock/d3/wiki/Gallery
找到。
在这个示例中,我们通过一个静态数据集创建了一个 HTML/JavaScript 互动可视化。使用 IPython 2.0 及以上版本,我们还可以创建涉及浏览器与 Python 内核之间双向通信的动态、实时可视化。Brian Granger 提供了一个实验性实现,可以在 nbviewer.ipython.org/github/ellisonbg/talk-2014-strata-sc/blob/master/Graph%20Widget.ipynb
访问。
另外,我们还要提到 Vincent,一个 Python 到 Vega 的翻译器。Vega 是一种基于 JSON 的可视化语法,可以被翻译为 D3.js。Vincent 使得在 Python 中设计互动可视化并在浏览器中渲染成为可能。更多信息可以在 vincent.readthedocs.org/en/latest/
找到。
另见
-
使用 Bokeh 创建互动 Web 可视化 示例
-
将 matplotlib 图形转换为 D3.js 可视化图表,使用 mpld3 示例
使用 mpld3 将 matplotlib 图形转换为 D3.js 可视化
mpld3 库会自动将 matplotlib 图形转换为互动 D3.js 可视化图表。在本示例中,我们将展示如何在笔记本中使用该库。
准备工作
要安装 mpld3 库,只需在终端中输入 pip install mpld3
。更多信息请参见主网站 mpld3.github.io
。
如何做…
-
首先,我们像往常一样加载 NumPy 和 matplotlib:
In [1]: import numpy as np import matplotlib.pyplot as plt %matplotlib inline
-
然后,我们通过一个函数调用在笔记本中启用 mpld3 图形:
In [2]: from mpld3 import enable_notebook enable_notebook()
-
现在,让我们使用 matplotlib 创建一个散点图:
In [3]: X = np.random.normal(0, 1, (100, 3)) color = np.random.random(100) size = 500 * np.random.random(100) plt.scatter(X[:,0], X[:,1], c=color, s=size, alpha=0.5, linewidths=2) plt.grid(color='lightgray', alpha=0.7)
matplotlib 图形通过 D3.js 渲染,而不是使用标准的 matplotlib 后端。特别是,图形是互动的(我们可以平移和缩放图形):
使用 mpld3 的互动 matplotlib 图
-
现在,我们创建一个更复杂的示例,其中包含多个子图,表示一个 3D 数据集的不同 2D 投影。我们使用 matplotlib 的
subplots()
函数中的sharex
和sharey
关键字来自动绑定不同图形的 x 和 y 轴。在任何子图上进行平移和缩放都会自动更新所有其他子图:In [4]: fig, ax = plt.subplots(3, 3, figsize=(6, 6), sharex=True, sharey=True) fig.subplots_adjust(hspace=0.3) X[::2,2] += 3 for i in range(3): for j in range(3): ax[i,j].scatter(X[:,i], X[:,j], c=color, s=.1*size, alpha=0.5, linewidths=2) ax[i,j].grid(color='lightgray', alpha=0.7)
这个用例完全可以通过 mpld3 处理;D3.js 子图是动态联动的:
在 mpld3 中的互动联动子图
它是如何工作的…
mpld3 的工作原理是首先爬取并将 matplotlib 图形导出为 JSON(在 mplexporter 框架的上下文中)。然后,库从该 JSON 表示生成 D3.js 代码。这种架构可以支持除了 D3.js 之外的其他 matplotlib 后端。
还有更多…
这里有一些参考资料:
-
mplexporter 可用:
github.com/mpld3/mplexporter
-
mpld3 在 GitHub 上可用:
github.com/jakevdp/mpld3
另见
-
使用 Bokeh 创建交互式 Web 可视化 食谱
-
在 IPython 笔记本中使用 D3.js 可视化 NetworkX 图 食谱
入门 Vispy 进行高性能互动数据可视化
大多数现有的 Python 绘图或可视化库可以显示小型或中型数据集(包含不超过几万个点的数据集)。在 大数据 时代,有时需要显示更大的数据集。
Vispy (vispy.org
) 是一个年轻的 2D/3D 高性能可视化库,可以显示非常大的数据集。Vispy 通过 OpenGL 库利用现代图形处理单元(GPU)的计算能力。
在过去的二十年里,视频游戏行业促进了 GPU 的强大功能。GPU 专注于高性能、实时渲染。因此,它们非常适合互动式科学绘图。
Vispy 提供了一个 Pythonic 的面向对象接口,用于 OpenGL,适用于那些了解 OpenGL 或愿意学习 OpenGL 的人。更高级别的图形接口也在写作时开发中,实验版本已经可以使用。这些接口不需要任何 OpenGL 知识。
在本食谱中,我们将简要介绍 OpenGL 的基本概念。在以下两种情况中,你需要了解这些概念:
-
如果你今天想使用 Vispy,在高级绘图接口可用之前
-
如果你想创建自定义的、复杂的、高性能的可视化,这些可视化在 Vispy 中尚未实现
在这里,我们使用 Vispy 的面向对象接口来显示一个数字信号。
准备就绪
Vispy 依赖于 NumPy。需要一个后端库(例如,PyQt4 或 PySide)。
本食谱已在 github.com/vispy/vispy
上可用的 Vispy 开发版本中进行测试。你应该克隆 GitHub 仓库并使用以下命令安装 Vispy:
python setup.py install
本食谱中使用的 API 可能会在未来版本中发生变化。
如何做到…
-
让我们导入 NumPy,
vispy.app
(用于显示画布)和vispy.gloo
(面向对象的 OpenGL 接口):In [1]: import numpy as np from vispy import app from vispy import gloo
-
为了显示一个窗口,我们需要创建一个 画布:
In [2]: c = app.Canvas(keys='interactive')
-
使用
vispy.gloo
时,我们需要编写着色器。这些程序使用类似 C 语言的语言编写,运行在 GPU 上,为我们的可视化提供完全的灵活性。在这里,我们创建一个简单的顶点着色器,它直接在画布上显示 2D 数据点(存储在a_position
变量中)。接下来我们将在下一节中看到更多细节:In [3]: vertex = """ attribute vec2 a_position; void main (void) { gl_Position = vec4(a_position, 0.0, 1.0); } """
-
我们需要创建的另一个着色器是片段着色器。它让我们控制像素的颜色。在这里,我们将所有数据点显示为黑色:
In [4]: fragment = """ void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } """
-
接下来,我们创建一个OpenGL
Program
。这个对象包含着色器并将着色器变量链接到 NumPy 数据:In [5]: program = gloo.Program(vertex, fragment)
-
我们将
a_position
变量链接到一个(1000, 2)的 NumPy 数组,该数组包含 1000 个数据点的坐标。在默认坐标系中,四个画布角的坐标为(+/-1, +/-1):In [6]: program['a_position'] = np.c_[ np.linspace(-1.0, +1.0, 1000), np.random.uniform(-0.5, +0.5, 1000)]
-
我们在窗口调整大小时创建一个回调函数。更新OpenGL 视口可以确保 Vispy 使用整个画布:
In [7]: @c.connect def on_resize(event): gloo.set_viewport(0, 0, *event.size)
-
当画布需要刷新时,我们创建一个回调函数。这个
on_draw()
函数渲染整个场景。首先,我们将窗口清空为白色(每一帧都需要这样做)。然后,我们使用 OpenGL 程序绘制一系列线段:In [8]: @c.connect def on_draw(event): gloo.clear((1,1,1,1)) program.draw('line_strip')
-
最后,我们显示画布并运行应用程序:
In [9]: c.show() app.run()
下图显示了一个截图:
使用 Vispy 的基本可视化示例
它是如何工作的……
OpenGL 是一种硬件加速的互动可视化开放标准。它广泛应用于视频游戏、工业(计算机辅助设计,或CAD)、虚拟现实和科学应用(医学成像、计算机图形学等)。
OpenGL 是一项成熟的技术,创建于 1990 年代初期。在 2000 年代初,OpenGL 2.0 引入了一个重大的新特性:可以自定义渲染管线的基本步骤。这个管线定义了数据如何在 GPU 上处理,以进行实时渲染。许多 OpenGL 课程和教程讲解的是旧的、固定的管线。然而,Vispy 仅支持现代的可编程管线。
在这里,我们将介绍本食谱中使用的可编程管线的基本概念。OpenGL 比我们在这里能覆盖的要复杂得多。然而,Vispy 为 OpenGL 的最常见功能提供了一个大大简化的 API。
注意
Vispy 基于OpenGL ES 2.0,这是 OpenGL 的一种变体,支持桌面计算机、移动设备和现代网页浏览器(通过WebGL)。现代图形卡可以支持额外的功能。这些功能将在未来版本的 Vispy 中提供。
在给定 OpenGL 程序的渲染管线中,有四个主要元素:
-
数据缓冲区将数值数据存储在 GPU 上。缓冲区的主要类型有顶点缓冲区、索引缓冲区和纹理。
-
变量在着色器中是可用的。主要有四种类型的变量:属性、常量、变化量和纹理采样器。
-
着色器是用一种类似 C 语言的语言编写的 GPU 程序,称为OpenGL 着色语言(GLSL)。着色器的两种主要类型是顶点着色器和片段着色器。
-
图元类型定义了数据点的渲染方式。主要类型有点、线和三角形。
这是渲染管线的工作方式:
-
数据被发送到 GPU 并存储在缓冲区中。
-
顶点着色器并行处理数据,并生成多个 4D 点,这些点位于归一化坐标系中(+/-1, +/-1)。第四维是齐次坐标(通常为 1)。
-
图形图元(点、线和三角形)是通过顶点着色器返回的数据点生成的(图元组装和光栅化)。
-
片段着色器并行处理所有图元像素,并返回每个像素的颜色作为 RGBA 组件。
在本例中,只有一个 GPU 变量:a_position
属性。属性是每个数据点取一个值的变量。常量是全局变量(所有数据点共享),而变化量则用于将值从顶点着色器传递到片段着色器(通过对两个或三个顶点之间的像素进行自动线性插值)。
在vispy.gloo
中,Program
是通过顶点着色器和片段着色器创建的。然后,可以使用program['varname'] = value
语法设置在着色器中声明的变量。当varname
是一个属性变量时,值可以是一个 NumPy 二维数组。在这个数组中,每一行包含每个数据点的组成部分。
同样,我们也可以在程序中声明常量和纹理。
最后,program.draw()
函数使用指定的图元类型渲染数据。这里,line_strip
图元类型告诉 GPU 遍历所有顶点(由顶点缓冲区返回),并绘制从一个点到下一个点的线段。如果有n个点,则会有n-1条线段。
其他图元类型包括点和三角形,有几种方法可以从顶点列表中生成线或三角形。
此外,还可以提供索引缓冲区。索引缓冲区包含指向顶点缓冲区的索引。使用索引缓冲区可以让我们在图元组装阶段多次复用同一个顶点。例如,当使用triangles
图元类型渲染一个立方体时(每三个点生成一个三角形),我们可以使用包含八个数据点的顶点缓冲区和包含三十六个索引的索引缓冲区(三个点构成一个三角形,每个面有两个三角形,共六个面)。
还有更多内容……
这里展示的示例非常简单。然而,OpenGL 和 Vispy 提供的方法却特别强大。它使我们能够完全控制渲染管线,并且几乎可以以最优方式利用 GPU 的计算能力。
高性能是通过最小化数据传输到 GPU 的次数来实现的。当显示静态数据(例如散点图)时,可以仅在初始化时将数据发送到 GPU。而渲染动态数据的速度也相当快;数据传输的数量级大约为 1 GBps。
此外,尽量减少 OpenGL 绘制调用的次数至关重要。每次绘制都会产生显著的开销。通过一次性渲染所有相似的原始类型来实现高性能(批量渲染)。即使点的属性不同(例如,不同大小和颜色的点),GPU 在批量渲染时也特别高效。
最后,可以通过着色器在 GPU 上以非常高的性能执行几何或像素变换。当变换在着色器中实现时,GPU 强大的架构(由数百或数千个计算单元组成)得到了充分利用。
在可视化的上下文中,可以在着色器中执行通用计算。与适当的 GPGPU 框架(如 CUDA 或 OpenCL)相比,存在一个主要缺点:在顶点着色器中,给定线程只能访问一个数据点。同样,在片段着色器中,线程只能访问一个像素。然而,某些类型的仿真或可视化效果需要顶点或像素之间的交互。虽然有方法可以缓解这个问题,但这会导致性能下降。
然而,OpenGL 可以与 CUDA/OpenCL 进行互操作。缓冲区可以在 OpenGL 和 GPGPU 框架之间共享。复杂的 CUDA/OpenCL 计算可以实时地在顶点缓冲区或纹理上实现,从而实现高效的数值仿真渲染。
Vispy 用于科学可视化
正如我们在这个示例中看到的,Vispy 要求用户了解 OpenGL 和 GLSL。然而,当前正在开发更高级的图形接口。这些接口将为科学家们带来 GPU 的强大能力,用于高性能交互式可视化。
视觉组件将提供可重用的、响应式的图形组件,如形状、多边形、3D 网格、图形等。这些组件将是完全可定制的,并且可以在不需要了解 OpenGL 的情况下使用。着色器组合系统将允许高级用户以模块化的方式重用 GLSL 代码片段。
视觉组件将被组织在一个 场景图 中,执行基于 GPU 的 变换。
科学绘图接口将会实现。Vispy 还可以作为现有绘图库(如 matplotlib)的高性能后端。
Vispy 还将支持通过 WebGL 在 IPython notebook 中的完全集成。
最终,Vispy 将能够实现多种科学可视化:
-
散点图可以通过点精灵高效渲染,每个数据点使用一个顶点。平移和缩放可以在顶点着色器中实现,从而实现对数百万点的快速交互式可视化。
-
静态或动态(实时)数字信号可以通过折线显示。使用 Anti-Grain Geometry 的 OpenGL 实现可以实现曲线的高质量渲染,这是一个高质量的 2D 渲染库。
-
图表可以通过组合点和线段来显示。
-
3D 网格可以使用三角形和索引缓冲区显示。几何变换和逼真的光照可以在顶点和片段着色器中实现。
-
实时图像流可以有效地通过纹理显示。
-
轴线、网格、刻度、文本和标签可以在片段着色器中高效渲染。
在 Vispy 的画廊中可以找到许多示例。
以下是一些参考资料:
-
Vispy 的画廊位于
vispy.org/gallery.html
-
由 Nicolas P. Rougier 编写的现代 OpenGL 教程,位于
www.loria.fr/~rougier/teaching/opengl/
-
Python 中的神经科学硬件加速交互式数据可视化,一篇文章位于
journal.frontiersin.org/Journal/10.3389/fninf.2013.00036/full
-
Vispy 用户邮件列表位于
groups.google.com/forum/#!forum/vispy
-
Vispy-dev 邮件列表位于
groups.google.com/forum/#!forum/vispy-dev
-
Anti-Grain Geometry 库在维基百科上的页面,位于
en.wikipedia.org/wiki/Anti-Grain_Geometry
第七章。统计数据分析
本章将涵盖以下主题:
-
使用 pandas 和 matplotlib 探索数据集
-
开始进行统计假设检验——简单的 z 检验
-
开始使用贝叶斯方法
-
使用列联表和卡方检验估计两个变量之间的相关性
-
使用最大似然法拟合数据的概率分布
-
使用核密度估计法非参数地估计概率分布
-
通过马尔可夫链蒙特卡洛方法从后验分布中抽样拟合贝叶斯模型
-
在 IPython 笔记本中使用 R 编程语言分析数据
介绍
在前面的章节中,我们回顾了 Python 中高性能交互式计算的技术方面。现在,我们开始本书的第二部分,展示可以使用 Python 解决的各种科学问题。
在本章中,我们介绍了用于数据分析的统计方法。除了涉及诸如 pandas、statsmodels 和 PyMC 等统计包外,我们还将解释这些方法背后的数学原理基础。因此,如果你有一定的概率论和微积分基础,本章将会最有益。
下一章,第八章,机器学习,与本章密切相关;其基础数学非常相似,但目标略有不同。在本章中,我们展示了如何从现实世界数据中获得洞见,以及如何在不确定性面前做出明智的决策。而在下一章,目标是从数据中学习,即从部分观察中进行归纳和预测结果。
在本引言中,我们将对本章中所涉及的方法进行广泛的、高层次的概述。
什么是统计数据分析?
统计数据分析的目标是从部分和不确定的观察中理解复杂的现实现象。数据中的不确定性导致我们对现象的知识也存在不确定性。理论的主要目标是量化这种不确定性。
在进行统计数据分析时,重要的是区分数学理论与分析后做出的决策。前者是完美严谨的;或许令人惊讶的是,数学家们能够建立一个精确的数学框架来处理不确定性。然而,统计分析得出的实际人类决策中有主观的部分。在决策过程中,理解统计结果背后的风险和不确定性至关重要。
在本章中,我们将看到统计数据分析背后的基本概念、原则和理论,特别是如何在量化风险的情况下做出决策。当然,我们将始终展示如何使用 Python 实现这些方法。
一些术语
在我们开始这些食谱之前,有许多术语需要介绍。这些概念使我们能够在多个维度上对统计技术进行分类。
探索、推断、决策和预测
探索性方法 使我们能够通过基本的统计聚合和交互式可视化对数据集进行初步查看。我们在本书的第一章以及《学习 IPython 用于交互式计算和数据可视化》一书(Packt Publishing)中介绍了这些基本方法。本章的第一条食谱,使用 pandas 和 matplotlib 探索数据集,展示了另一个例子。
统计推断 是通过部分和不确定的观察获取关于未知过程的信息。特别地,估计 是指为描述该过程的数学变量获得近似值。本章的三条食谱涉及统计推断:
-
通过最大似然法拟合概率分布 食谱
-
通过核密度估计非参数地估计概率分布 食谱
-
通过从后验分布中采样来拟合贝叶斯模型,使用马尔可夫链蒙特卡罗方法 食谱
决策理论 使我们能够通过随机观察在可控风险下对未知过程做出决策。以下两条食谱展示了如何做出统计决策:
-
开始进行统计假设检验:简单的 z 检验 食谱
-
通过列联表和卡方检验估计两个变量之间的相关性 食谱
预测 是指从数据中学习,即根据有限的观察预测随机过程的结果。这是下章的主题,第八章,机器学习。
单变量和多变量方法
在大多数情况下,你可以考虑数据中的两个维度:
-
观察值(或 样本,对于机器学习人员)
-
变量(或 特征)
通常,观察值是同一随机过程的独立实现。每个观察值由一个或多个变量组成。大多数时候,变量要么是数字,要么是属于有限集合的元素(即取有限数量的值)。分析的第一步是理解你的观察值和变量是什么。
如果你有一个变量,则你的问题是 单变量。如果你有两个变量,则是 双变量,如果你至少有两个变量,则是 多变量。单变量方法通常较为简单。话虽如此,单变量方法也可以应用于多变量数据,每次使用一个维度。尽管这种方法无法探索变量之间的相互作用,但它通常是一个有趣的初步方法。
频率学派和贝叶斯方法
至少有两种不同的方式来考虑不确定性,从而产生两类用于推断、决策和其他统计问题的方法。它们分别被称为频率派方法和贝叶斯方法。一些人偏好频率派方法,而另一些人则偏好贝叶斯方法。
频率派解释概率为多个独立试验的统计平均(大数法则)。贝叶斯派则解释为信念程度(不需要多个试验)。贝叶斯解释在只考虑单次试验时非常有用。此外,贝叶斯理论还考虑了我们对随机过程的先验知识。随着数据的增加,先验概率分布会被更新为后验分布。
频率派和贝叶斯派方法各有其优缺点。例如,有人可能会说,频率派方法比贝叶斯方法更容易应用,但解释起来更为困难。有关频率派方法的经典误用,见www.refsmmat.com/statistics/。
无论如何,如果你是统计数据分析的初学者,你可能希望在选择立场之前学习这两种方法的基础知识。本章将向你介绍这两种方法。
以下的配方完全是贝叶斯方法:
-
入门贝叶斯方法配方
-
通过马尔可夫链蒙特卡洛方法从后验分布中采样拟合贝叶斯模型配方
Jake Vanderplas 曾写过几篇关于频率派和贝叶斯派的博客文章,并且在文章中提供了 Python 的示例。该系列的第一篇文章可以在jakevdp.github.io/blog/2014/03/11/frequentism-and-bayesianism-a-practical-intro/
找到。
参数化和非参数化推断方法
在许多情况下,你会基于概率模型来进行分析。该模型描述了你的数据是如何生成的。概率模型并没有实际存在;它仅是一个数学对象,指导你进行分析。一个好的模型是有帮助的,而一个不好的模型可能会误导你。
使用参数化方法时,你假设你的模型属于一个已知的概率分布族。该模型有一个或多个数值的参数,你可以对其进行估计。
使用非参数化模型时,你的模型不需要做出这样的假设。这给你带来了更多的灵活性。然而,这些方法通常实现起来更为复杂,且难以解释。
以下配方分别是参数化和非参数化的:
-
使用最大似然法拟合概率分布配方
-
使用核密度估计非参数化估计概率分布配方
本章仅为你提供了 Python 在统计数据分析方面广泛可能性的一个大致概念。你可以找到许多书籍和在线课程,详细讲解统计方法,例如:
-
维基教科书上的统计学内容:
en.wikibooks.org/wiki/Statistics
-
免费的统计学教材可以在
stats.stackexchange.com/questions/170/free-statistical-textbooks
找到
使用 pandas 和 matplotlib 探索数据集
在这第一个食谱中,我们将展示如何使用 pandas 对数据集进行初步分析。这通常是在获得数据后进行的第一步。pandas 让我们能够非常轻松地加载数据、探索变量,并使用 matplotlib 创建基本的图表。
我们将查看一个数据集,该数据集包含了四名网球选手直到 2012 年为止的所有 ATP 比赛。在这里,我们将重点关注罗杰·费德勒。
准备工作
从本书的 GitHub 仓库 github.com/ipython-books/cookbook-data
下载 Tennis 数据集,并将其解压到当前目录。
如何做...
-
我们导入 NumPy、pandas 和 matplotlib:
In [1]: import numpy as np import pandas as pd import matplotlib.pyplot as plt %matplotlib inline
-
数据集是一个 CSV 文件,即一个以逗号分隔值的文本文件。pandas 让我们能够用一个函数加载这个文件:
In [2]: player = 'Roger Federer' filename = "data/{name}.csv".format( name=player.replace(' ', '-')) df = pd.read_csv(filename)
我们可以通过在 IPython 笔记本中直接显示数据集来进行首次查看:
In [3]: df Out[3]: Int64Index: 1179 entries, 0 to 1178 Data columns (total 70 columns): year 1179 non-null values tournament 1179 non-null values ... player2 total points total 1027 non-null values dtypes: float64(49), int64(2), object(19)
-
数据集有很多列。每一行对应罗杰·费德勒的一场比赛。我们来添加一个布尔变量,表示他是否赢得了比赛。
tail()
方法显示列的最后几行:In [4]: df['win'] = df['winner'] == player df['win'].tail() Out[4]: 1174 False 1175 True 1176 True 1177 True 1178 False Name: win, dtype: bool
-
df['win']
是一个Series
对象。它与 NumPy 数组非常相似,只是每个值都有一个索引(这里是比赛索引)。这个对象具有一些标准的统计函数。例如,让我们查看获胜比赛的比例:In [5]: print(("{player} has won {vic:.0f}% " "of his ATP matches.").format( player=player, vic=100*df['win'].mean())) Roger Federer has won 82% of his ATP matches.
-
现在,我们将观察一些变量随时间的变化。
df['start date']
字段包含了比赛的开始日期,格式为字符串。我们可以使用pd.to_datetime()
函数将其转换为日期类型:In [6]: date = pd.to_datetime(df['start date'])
-
我们现在正在查看每场比赛中的双误比例(考虑到在更长时间的比赛中,双误通常更多!)。这个数字是球员心理状态的一个指标,反映了他的自信水平、在发球时的冒险精神以及其他一些参数。
In [7]: df['dblfaults'] = (df['player1 double faults'] / df['player1 total points total'])
-
我们可以使用
head()
和tail()
方法查看列的开头和结尾,并使用describe()
获取摘要统计数据。特别地,我们需要注意,有些行包含 NaN 值(即,并不是所有比赛的双误数据都有记录)。In [8]: df['dblfaults'].tail() Out[8]: 1174 0.018116 1175 0.000000 1176 0.000000 1177 0.011561 1178 NaN Name: dblfaults, dtype: float64 In [9]: df['dblfaults'].describe() Out[9]: count 1027.000000 mean 0.012129 std 0.010797 min 0.000000 25% 0.004444 50% 0.010000 75% 0.018108 max 0.060606 dtype: float64
-
pandas 中的一个非常强大的功能是
groupby()
。这个函数允许我们将具有相同列值的行组合在一起。然后,我们可以通过该值对该组进行聚合,计算每个组中的统计数据。例如,下面是我们如何根据比赛场地表面类型来获取胜利比例:In [10]: df.groupby('surface')['win'].mean() Out[10]: surface Indoor: Carpet 0.736842 Indoor: Clay 0.833333 Indoor: Hard 0.836283 Outdoor: Clay 0.779116 Outdoor: Grass 0.871429 Outdoor: Hard 0.842324 Name: win, dtype: float64
-
现在,我们将显示双误差的比例与比赛日期的关系,以及每年的平均值。为此,我们也使用
groupby()
:In [11]: gb = df.groupby('year')
-
gb
是一个GroupBy
实例。它类似于DataFrame
对象,但每个组中有多行(每年比赛的所有场次)。我们可以使用mean()
操作对这些行进行聚合。我们使用 matplotlib 的plot_date()
函数,因为 x 轴包含日期:In [12]: plt.plot_date(date, df['dblfaults'], alpha=.25, lw=0) plt.plot_date(gb['start date'].max(), gb['dblfaults'].mean(), '-', lw=3) plt.xlabel('Year') plt.ylabel('Proportion of double faults per match.')
还有更多...
pandas 是一个用于数据整理和探索性分析的优秀工具。pandas 支持各种格式(文本格式和二进制文件),并允许我们以多种方式操作表格。特别是,groupby()
函数非常强大。Wes McKinney 的《Python for Data Analysis》一书对这个库进行了更为详细的讲解。
我们在这里所讲述的仅仅是数据分析过程中的第一步。我们需要更高级的统计方法来获得关于基础现象的可靠信息,做出决策和预测,等等。这个内容将在接下来的章节中讨论。
此外,更复杂的数据集需要更复杂的分析方法。例如,数字记录、图像、声音和视频在应用统计技术之前需要特定的信号处理处理。这些问题将在后续章节中讨论。
开始统计假设检验——一个简单的 z 检验
统计假设检验使我们能够在数据不完全的情况下做出决策。根据定义,这些决策是有不确定性的。统计学家已经开发了严格的方法来评估这种风险。然而,决策过程中总是涉及一些主观性。理论只是帮助我们在不确定的世界中做出决策的工具。
在这里,我们介绍统计假设检验背后的最基本思想。我们将跟随一个极其简单的例子:抛硬币。更准确地说,我们将展示如何进行z 检验,并简要解释其背后的数学思想。这种方法(也称为频率主义方法)尽管在科学中被广泛使用,但也受到了许多批评。稍后我们将展示一种基于贝叶斯理论的更现代的方法。理解这两种方法非常有帮助,因为许多研究和出版物仍然采用频率主义方法。
准备工作
你需要具备基本的概率论知识(随机变量、分布、期望、方差、中心极限定理等)才能理解此方法。
如何实现...
许多频率主义假设检验方法大致包括以下步骤:
-
写下假设,特别是零假设,它是我们想要证明的假设的对立面(以一定的置信度)。
-
计算检验统计量,这是一个依赖于检验类型、模型、假设和数据的数学公式。
-
使用计算得出的值来接受假设、拒绝假设或无法得出结论。
在这个实验中,我们抛掷硬币n次,并观察到h次正面朝上。我们想知道硬币是否公平(零假设)。这个例子非常简单,但对于教学目的非常有用。此外,它是许多更复杂方法的基础。
我们用B(q)表示伯努利分布,其中未知参数为q。您可以访问en.wikipedia.org/wiki/Bernoulli_distribution
获取更多信息。
伯努利变量是:
-
0(反面)出现的概率为1-q
-
1(正面朝上)出现的概率为q
以下是进行简单统计z-检验所需的步骤:
-
假设我们抛掷了n=100次硬币,得到了h=61次正面朝上。我们选择显著性水平为 0.05:硬币是否公平?我们的零假设是:硬币是公平的(q = 1/2):
In [1]: import numpy as np import scipy.stats as st import scipy.special as sp In [2]: n = 100 # number of coin flips h = 61 # number of heads q = .5 # null-hypothesis of fair coin
-
让我们计算z 分数,它由以下公式定义(
xbar
是分布的估计平均值)。我们将在下一部分它是如何工作的...中解释此公式。In [3]: xbar = float(h)/n z = (xbar - q) * np.sqrt(n / (q*(1-q))); z Out[3]: 2.1999999999999997
-
现在,根据 z 分数,我们可以按以下方式计算 p 值:
In [4]: pval = 2 * (1 - st.norm.cdf(z)); pval Out[4]: 0.02780689502699718
-
该 p 值小于 0.05,因此我们拒绝零假设,并得出结论:硬币可能不公平。
它是如何工作的...
投币实验被建模为一系列独立的随机变量,,遵循伯努利分布B(q)。每个x[i]代表一次投币实验。经过我们的实验后,我们获得这些变量的实际值(样本)。有时为了区分随机变量(概率对象)和实际值(样本),会使用不同的符号表示。
以下公式给出了样本均值(这里是正面朝上的比例):
知道分布B(q)的期望值和方差
,我们计算:
z 检验是的标准化版本(我们去除其均值,并除以标准差,因此我们得到一个均值为 0,标准差为 1 的变量):
在零假设下,获得大于某个数量z[0]的 z 检验的概率是多少?这个概率称为(双尾)p 值。根据中心极限定理,当n较大时,z 检验大致服从标准正态分布N(0,1),因此我们得到:
下图展示了 z 分数和 p 值:
z 分数和 p 值的示意图
在这个公式中, 是标准正态分布的累积分布函数。在 SciPy 中,我们可以通过
scipy.stats.norm.cdf
获取它。因此,给定从数据中计算得出的 z 检验,我们计算 p 值:在原假设下,观察到比所观察到的检验值更极端的 z 检验的概率。
如果 p 值小于 5%(这是一个常用的显著性水平,出于任意和历史原因),我们得出结论:
-
原假设为假,因此我们得出结论:硬币是不公平的。
-
原假设为真,如果我们得到了这些值,那就是运气不好。我们无法得出结论。
在这个框架中,我们无法对这两个选项进行歧义消解,但通常选择第一个选项。我们达到了频率主义统计的极限,尽管有一些方法可以缓解这个问题(例如,通过进行几项独立研究并查看它们的所有结论)。
还有更多内容...
存在许多遵循这种模式的统计检验。回顾所有这些检验远超出本书的范围,但你可以查看 en.wikipedia.org/wiki/Statistical_hypothesis_testing
中的参考资料。
由于 p 值不容易解释,它可能导致错误的结论,即使是在同行评审的科学出版物中也是如此。关于该主题的深入探讨,请参见 www.refsmmat.com/statistics/。
另见
- 入门贝叶斯方法部分
入门贝叶斯方法
在上一个例子中,我们使用了频率主义方法对不完全数据进行假设检验。在这里,我们将看到一种基于贝叶斯理论的替代方法。主要思想是认为未知参数是随机变量,就像描述实验的变量一样。关于这些参数的先验知识被整合到模型中。随着数据的不断增加,这些知识会被更新。
频率主义者和贝叶斯主义者对概率的解释不同。频率主义者将概率解释为样本数量趋于无穷大时频率的极限。而贝叶斯主义者则将其解释为一种信念;随着观察到的数据越来越多,这种信念会不断更新。
这里,我们以贝叶斯方法重新审视之前的抛硬币例子。这个例子足够简单,可以进行分析处理。通常,如我们将在本章后面看到的,无法得到解析结果,数值方法变得至关重要。
准备开始
这是一个数学密集的过程。建议具备基本概率论(随机变量、分布、贝叶斯公式)和微积分(导数、积分)的知识。我们使用与前一个方法相同的符号。
如何做...
让q表示获得正面的概率。而在之前的公式中,q只是一个固定的数字,在这里我们认为它是一个随机变量。最初,这个变量遵循一种被称为先验概率分布的分布。它表示我们在开始抛硬币之前,对q的知识。我们将在每次试验后更新这个分布(即后验分布)。
-
首先,我们假设q是区间[0, 1]上的一个均匀随机变量。这是我们的先验分布:对于所有q,P(q)=1。
-
然后,我们抛硬币n次。我们用x[i]表示第i次抛掷的结果(0 代表反面,1 代表正面)。
-
知道观察到的结果x[i]后,q的概率分布是多少?贝叶斯定理使我们能够解析地计算出后验分布(数学细节见下一部分):
-
我们根据之前的数学公式定义后验分布。我们注意到,这个表达式是(n+1)倍的概率质量函数(PMF)二项分布的形式,二项分布可以直接通过
scipy.stats
获得。(有关二项分布的更多信息,请参见en.wikipedia.org/wiki/Binomial_distribution
。)In [1]: import numpy as np import scipy.stats as st import matplotlib.pyplot as plt %matplotlib inline In [2]: posterior = lambda n, h, q: ((n+1) * st.binom(n, q).pmf(h))
-
让我们为观察到h=61次正面朝上的结果和n=100次抛掷总数,绘制这个分布:
In [3]: n = 100 h = 61 q = np.linspace(0., 1., 1000) d = posterior(n, h, q) In [4]: plt.plot(q, d, '-k') plt.ylim(0, d.max()+1)
这个曲线表示我们在观察到 61 次正面朝上的结果后,对参数q的信念。
它是如何工作的...
在这一部分,我们解释了贝叶斯定理,并给出了这个例子背后的数学细节。
贝叶斯定理
数据科学中有一个非常通用的思想,那就是用数学模型来解释数据。这通过一个单向过程模型 → 数据来形式化。
一旦这个过程被形式化,数据科学家的任务就是利用数据恢复关于模型的信息。换句话说,我们希望逆转原始过程并得到数据 → 模型。
在一个概率设置中,直接过程表示为条件概率分布 P(data|model)。这是在模型完全指定的情况下,观察到数据的概率。
类似地,逆向过程是P(model|data)。它在知道观察结果(我们拥有的数据)的基础上,给我们关于模型(我们所寻找的)的信息。
贝叶斯定理是一个通用框架的核心,用于逆转模型 → 数据的概率过程。它可以表述如下:
这个方程为我们提供了关于模型的信息,前提是我们知道观测数据。贝叶斯方程广泛应用于信号处理、统计学、机器学习、逆问题以及许多其他科学领域。
在贝叶斯方程中,P(model) 反映了我们关于模型的先验知识。同时,P(data) 是数据的分布。通常它表示为 P(data|model)P(model) 的积分。
总之,贝叶斯方程为我们提供了数据推断的总体路线图:
-
为直接过程 model → data 指定一个数学模型(P(data|model) 项)。
-
为模型指定一个先验概率分布 (P(model) 项)。
-
执行解析或数值计算以求解这个方程。
后验分布的计算
在这个例子中,我们通过以下方程(直接源自贝叶斯定理)找到了后验分布:
由于 x[i] 是独立的,我们得到(h 为正面次数):
此外,我们可以解析计算以下积分(使用分部积分法和归纳法):
最后,我们得到:
最大后验估计
我们可以从后验分布中得到一个点估计。例如,最大后验估计 (MAP) 是通过考虑后验分布的 最大 值来作为 q 的估计。我们可以通过解析方法或数值方法找到这个最大值。有关 MAP 的更多信息,请参阅 en.wikipedia.org/wiki/Maximum_a_posteriori_estimation
。
在这里,我们可以通过对 q 求导直接推导出后验分布,得到这个估计(假设 1 h
* n-1*):
当 q = h/n 时,该表达式为零。这就是参数 q 的 MAP 估计。这个值恰好是实验中获得的正面比例。
还有更多...
在这个例子中,我们展示了贝叶斯理论中的一些基本概念,并通过一个简单的例子进行说明。我们能够解析推导出后验分布在现实应用中并不常见。尽管如此,这个例子仍然具有启发性,因为它解释了我们接下来将看到的复杂数值方法背后的核心数学思想。
可信区间
后验分布表示在给定观察值的情况下 q 的合理取值。我们可以利用它推导出 可信区间,它很可能包含实际值。可信区间是贝叶斯统计中的类比于频率统计中的置信区间。有关可信区间的更多信息,请参阅 en.wikipedia.org/wiki/Credible_interval
。
共轭分布
在这道菜谱中,先验分布和后验分布是共轭的,意味着它们属于同一个分布族(即贝塔分布)。因此,我们能够解析计算后验分布。你可以在en.wikipedia.org/wiki/Conjugate_prior
找到有关共轭分布的更多细节。
非信息性(客观)先验分布
我们选择了均匀分布作为未知参数q的先验分布。这是一个简单的选择,它使得计算变得可处理。它反映了一个直观的事实,即我们在先验上并不偏向任何特定的值。然而,也有一些严格的选择完全无信息性先验的方法(参见en.wikipedia.org/wiki/Prior_probability#Uninformative_priors
)。一个例子是 Jeffreys 先验,基于这样的思想:先验分布不应依赖于参数化的选择。更多关于 Jeffreys 先验的信息,请参阅en.wikipedia.org/wiki/Jeffreys_prior
。在我们的例子中,Jeffreys 先验为:
另见
- 通过马尔科夫链蒙特卡罗方法从后验分布中抽样拟合贝叶斯模型菜谱
使用列联表和卡方检验估计两个变量之间的相关性
而单变量方法处理的是单一变量的观测数据,多变量方法则考虑包含多个特征的观测数据。多变量数据集允许研究变量之间的关系,特别是它们之间的相关性或独立性。
在本菜谱中,我们将查看本章第一道菜谱中的相同网球数据集。采用频率学派方法,我们将估计发球得分数与网球选手获胜的点数比例之间的相关性。
准备工作
从本书的 GitHub 仓库下载网球数据集,链接为github.com/ipython-books/cookbook-data
,并将其解压到当前目录。
如何操作...
-
让我们导入 NumPy、pandas、SciPy.stats 和 matplotlib:
In [1]: import numpy as np import pandas as pd import scipy.stats as st import matplotlib.pyplot as plt %matplotlib inline
-
我们加载对应于罗杰·费德勒的数据集:
In [2]: player = 'Roger Federer' filename = "data/{name}.csv".format( name=player.replace(' ', '-')) df = pd.read_csv(filename)
-
每行对应一场比赛,70 列包含该比赛中许多选手的特征:
In [3]: print("Number of columns: " + str(len(df.columns))) df[df.columns[:4]].tail() Number of columns: 70 year tournament start date 1174 2012 Australian Open, Australia 16.01.2012 1175 2012 Doha, Qatar 02.01.2012 1176 2012 Doha, Qatar 02.01.2012 1177 2012 Doha, Qatar 02.01.2012 1178 2012 Doha, Qatar 02.01.2012
-
在这里,我们仅关注获胜点数的比例和(相对的)发球得分数:
In [4]: npoints = df['player1 total points total'] points = df['player1 total points won'] / npoints aces = df['player1 aces'] / npoints In [5]: plt.plot(points, aces, '.') plt.xlabel('% of points won') plt.ylabel('% of aces') plt.xlim(0., 1.) plt.ylim(0.)
如果这两个变量是独立的,我们就不会在点云中看到任何趋势。在这个图上,稍微有点难以看出。让我们使用 pandas 计算一个相关系数。
-
我们创建一个新的
DataFrame
对象,仅包含这些字段(请注意,这一步不是强制性的)。我们还删除了缺失某个字段的行(使用dropna()
):In [6]: df_bis = pd.DataFrame({'points': points, 'aces': aces}).dropna() df_bis.tail() Out[6]: aces points 1173 0.024390 0.585366 1174 0.039855 0.471014 1175 0.046512 0.639535 1176 0.020202 0.606061 1177 0.069364 0.531792
-
让我们计算比赛中 A 球的相对数量与赢得的点数之间的皮尔逊相关系数:
In [7]: df_bis.corr() Out[7]: aces points aces 1.000000 0.255457 points 0.255457 1.000000
约为 0.26 的相关性似乎表明我们的两个变量之间存在正相关关系。换句话说,在一场比赛中 A 球越多,球员赢得的点数就越多(这并不令人惊讶!)。
-
现在,为了确定变量之间是否存在统计上显著的相关性,我们使用卡方检验来检验列联表中变量的独立性。
-
首先,我们将变量二值化。在这里,如果球员在比赛中发球 A 球比平常多,值为
True
,否则为False
:In [8]: df_bis['result'] = df_bis['points'] > \ df_bis['points'].median() df_bis['manyaces'] = df_bis['aces'] > \ df_bis['aces'].median()
-
然后,我们创建一个列联表,其中包含所有四种可能性(真和真,真和假,依此类推)的频率:
In [9]: pd.crosstab(df_bis['result'], df_bis['manyaces']) Out[9]: manyaces False True result False 300 214 True 214 299
-
最后,我们计算卡方检验统计量和相关的 P 值。零假设是变量之间的独立性。SciPy 在
scipy.stats.chi2_contingency
中实现了这个测试,返回了几个对象。我们对第二个结果感兴趣,即 P 值:In [10]: st.chi2_contingency(_) Out[10]: (27.809858855369555, 1.3384233799633629e-07, 1L, array([[ 257.25024343, 256.74975657], [ 256.74975657, 256.25024343]]))
P 值远低于 0.05,因此我们拒绝零假设,并得出结论:在一场比赛中赢得的点数与赢得的 A 球比例之间存在统计学上显著的相关性(对于罗杰·费德勒!)。
提示
如常,相关性并不意味着因果关系。在这里,外部因素很可能影响两个变量。有关更多细节,请参阅en.wikipedia.org/wiki/Correlation_does_not_imply_causation
。
工作原理...
我们在这里提供了一些有关本文中使用的统计概念的细节。
皮尔逊相关系数
皮尔逊相关系数衡量了两个随机变量X和Y之间的线性相关性。它是协方差的归一化版本:
可以通过将这个公式中的期望值替换为样本均值,方差替换为样本方差来估计。关于其推断的更多细节可以在en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient
找到。
列联表和卡方检验
列联表包含所有组合结果的频率O[ij],当存在多个随机变量可以取有限数量的值时。在独立性的零假设下,我们可以基于边际和(每行的总和)计算期望频率E[ij]。卡方统计量的定义如下:
当观察足够多时,这个变量大致遵循卡方分布(正态变量平方和的分布)。一旦我们得到了 p 值,就像开始统计假设检验 - 一个简单的 z 检验中所解释的那样,我们可以拒绝或接受独立性的零假设。然后,我们可以得出(或不得出)变量之间存在显著相关性的结论。
还有更多...
还有许多其他类型的卡方检验,即测试统计量遵循卡方分布的测试。这些测试广泛用于测试分布的拟合度,或测试变量的独立性。更多信息可以在以下页面找到:
-
SciPy 文档中的 Chi2 测试可在
docs.scipy.org/doc/scipy/reference/generated/scipy.stats.chi2_contingency.html
找到
参见
- 开始统计假设检验 - 一个简单的 z 检验食谱
使用最大似然方法将概率分布拟合到数据
解释数据集的一个好方法是对其应用概率模型。找到一个合适的模型可能是一项工作。选择模型后,有必要将其与数据进行比较。这就是统计估计的内容。在这个食谱中,我们对心脏移植后存活时间(1967-1974 年研究)的数据集应用最大似然方法。
准备工作
与本章中通常一样,建议具有概率论和实分析背景。此外,您需要 statsmodels 包来检索测试数据集。有关 statsmodels 的更多信息,请参考statsmodels.sourceforge.net
。在 Anaconda 上,您可以使用conda install statsmodels
命令安装 statsmodel。
如何做...
-
statsmodels 是一个用于进行统计数据分析的 Python 包。它还包含我们在尝试新方法时可以使用的真实数据集。在这里,我们加载heart数据集:
In [1]: import numpy as np import scipy.stats as st import statsmodels.datasets as ds import matplotlib.pyplot as plt %matplotlib inline In [2]: data = ds.heart.load_pandas().data
-
让我们来看看这个
DataFrame
:In [3]: data.tail() Out[3]: survival censors age 64 14 1 40.3 65 167 0 26.7 66 110 0 23.7 67 13 0 28.9 68 1 0 35.2
这个数据集包含被审查和未被审查的数据:0 的审查意味着患者在研究结束时仍然存活,因此我们不知道确切的存活时间。我们只知道患者至少存活了指定的天数。为简单起见,我们只保留未被审查的数据(这样我们就引入了对未能在移植后存活很长时间的患者的偏见):
In [4]: data = data[data.censors==1] survival = data.survival
-
让我们通过绘制原始存活数据和直方图来以图形方式查看数据:
In [5]: plt.subplot(121) plt.plot(sorted(survival)[::-1], 'o') plt.xlabel('Patient') plt.ylabel('Survival time (days)') plt.subplot(122) plt.hist(survival, bins=15) plt.xlabel('Survival time (days)') plt.ylabel('Number of patients')
-
我们观察到直方图正在迅速下降。幸运的是,今天的生存率要高得多(5 年后的生存率约为 70%)。让我们尝试将指数分布拟合到数据中(关于指数分布的更多信息可以参见
en.wikipedia.org/wiki/Exponential_distribution
)。根据这个模型,S(生存天数)是一个带有参数的指数随机变量,观察值s[i]是从这个分布中抽样得到的。令样本均值为:
指数分布的似然函数如下,按照定义(证明见下节):
最大似然估计对于速率参数的定义是:值
,它最大化了似然函数。换句话说,这是最大化观察到数据的概率的参数,假设这些观察值是从指数分布中抽样得到的。
在这里,可以证明当
时,似然函数取得最大值,这就是速率参数的最大似然估计。让我们通过数值方法计算这个参数:
In [6]: smean = survival.mean() rate = 1./smean
-
为了将拟合的指数分布与数据进行比较,我们首先需要为 x 轴(天数)生成线性间隔的值:
In [7]: smax = survival.max() days = np.linspace(0., smax, 1000) dt = smax / 999\. # bin size: interval between two # consecutive values in `days`
我们可以使用 SciPy 获得指数分布的概率密度函数。参数是尺度,即估计速率的倒数。
In [8]: dist_exp = st.expon.pdf(days, scale=1./rate)
-
现在,让我们绘制直方图和得到的分布。我们需要将理论分布重新缩放到直方图上(这取决于箱子大小和数据点的总数):
In [9]: nbins = 30 plt.hist(survival, nbins) plt.plot(days, dist_exp*len(survival)*smax/nbins, '-r', lw=3)
拟合结果远非完美。我们能够找到最大似然估计的解析公式。在更复杂的情况下,这并不总是可能的。因此,我们可能需要求助于数值方法。SciPy 实际上集成了针对大量分布的数值最大似然程序。在这里,我们使用这种其他方法来估计指数分布的参数。
In [10]: dist = st.expon args = dist.fit(survival); args Out[10]: (0.99999999994836486, 222.28880590143666)
-
我们可以使用这些参数进行Kolmogorov-Smirnov 检验,该检验评估分布相对于数据的拟合优度。这个检验是基于数据的经验分布函数与参考分布的累积分布函数(CDF)之间的距离。
In [11]: st.kstest(survival, dist.cdf, args) Out[11]: (0.36199685486406347, 8.6470960143358866e-06)
第二个输出值是 p 值。在这里,它非常低:零假设(即观察到的数据来自具有最大似然速率参数的指数分布)可以被高置信度地拒绝。我们试试另一个分布,Birnbaum-Sanders 分布,它通常用于建模故障时间。(关于 Birnbaum-Sanders 分布的更多信息,请访问
en.wikipedia.org/wiki/Birnbaum-Saunders_distribution
。)In [12]: dist = st.fatiguelife args = dist.fit(survival) st.kstest(survival, dist.cdf, args) Out[12]: (0.18773446101946889, 0.073211497000863268)
这次,p 值是 0.07,因此我们在 5%的置信水平下不会拒绝零假设。绘制结果分布时,我们观察到比指数分布更好的拟合:
In [13]: dist_fl = dist.pdf(days, *args) nbins = 30 plt.hist(survival, nbins) plt.plot(days, dist_exp*len(survival)*smax/nbins, '-r', lw=3, label='exp') plt.plot(days, dist_fl*len(survival)*smax/nbins, '-g', lw=3, label='BS') plt.xlabel("Survival time (days)") plt.ylabel("Number of patients") plt.legend()
它是如何工作的...
在这里,我们给出了计算过程,推导出指数分布的速率参数的最大似然估计:
这里, 是样本均值。在更复杂的情况下,我们将需要数值优化方法,其中的原理是使用标准的数值优化算法来最大化似然函数(请参阅第九章,数值优化)。
为了找到该函数的最大值,我们需要计算它关于 的导数:
因此,这个导数的根是
还有更多内容...
这里列出了一些参考资料:
-
最大似然法在维基百科上的介绍,详情请见
en.wikipedia.org/wiki/Maximum_likelihood
-
Kolmogorov-Smirnov 检验在维基百科上的介绍,详情请见
en.wikipedia.org/wiki/Kolmogorov-Smirnov_test
-
拟合优度在
en.wikipedia.org/wiki/Goodness_of_fit
上有详细介绍
最大似然法是参数化的:模型属于一个预先指定的参数分布族。在下一个方法中,我们将看到一种基于核的方法,它是非参数化的。
另见
- 通过核密度估计非参数地估计概率分布 的方法
使用核密度估计法非参数地估计概率分布
在之前的方法中,我们应用了参数估计方法。我们有一个统计模型(指数分布)来描述我们的数据,并且我们估计了一个参数(分布的速率)。非参数估计处理那些不属于已知分布族的统计模型。这样,参数空间就是无限维的,而不是有限维的(也就是说,我们估计的是函数而不是数值)。
在这里,我们使用 核密度估计 (KDE) 来估算空间分布的概率密度。我们查看了 1848 到 2013 年期间热带气旋的地理位置,数据由 NOAA(美国国家海洋和大气管理局)提供。
准备工作
从本书的 GitHub 仓库下载 Storms 数据集 github.com/ipython-books/cookbook-data
,并将其解压到当前目录。数据来自 www.ncdc.noaa.gov/ibtracs/index.php?name=wmo-data。
你还需要 matplotlib 的工具包 basemap,可以通过 matplotlib.org/basemap/
获取。使用 Anaconda,你可以通过 conda install basemap
安装它。Windows 用户还可以在 www.lfd.uci.edu/~gohlke/pythonlibs/ 找到安装程序。
如何操作...
-
让我们导入常用的包。使用高斯核的核密度估计在 SciPy.stats 中有实现:
In [1]: import numpy as np import pandas as pd import scipy.stats as st import matplotlib.pyplot as plt from mpl_toolkits.basemap import Basemap %matplotlib inline
-
让我们使用 pandas 打开数据:
In [2]: df = pd.read_csv( "data/Allstorms.ibtracs_wmo.v03r05.csv")
-
数据集包含了自 1848 年以来大多数风暴的信息。单个风暴可能在多个连续的日子中出现多次。
In [3]: df[df.columns[[0,1,3,8,9]]].head() Out[3]: Serial_Num Season Basin Latitude Longitude 0 1848011S09080 1848 SI -8.6 79.8 1 1848011S09080 1848 SI -9.0 78.9 2 1848011S09080 1848 SI -10.4 73.2 3 1848011S09080 1848 SI -12.8 69.9 4 1848011S09080 1848 SI -13.9 68.9
-
我们使用 pandas 的
groupby()
函数获取每个风暴的平均位置:In [4]: dfs = df.groupby('Serial_Num') pos = dfs[['Latitude', 'Longitude']].mean() y, x = pos.values.T pos.head() Out[4]: Latitude Longitude Serial_Num 1848011S09080 -15.918182 71.854545 1848011S15057 -24.116667 52.016667 1848061S12075 -20.528571 65.342857 1851080S15063 -17.325000 55.400000 1851080S21060 -23.633333 60.200000
-
我们使用 basemap 在地图上显示风暴。这个工具包让我们能够轻松地将地理坐标投影到地图上。
In [5]: m = Basemap(projection='mill', llcrnrlat=-65, urcrnrlat=85, llcrnrlon=-180, urcrnrlon=180) x0, y0 = m(-180, -65) x1, y1 = m(180, 85) m.drawcoastlines() m.fillcontinents(color='#dbc8b2') xm, ym = m(x, y) m.plot(xm, ym, '.r', alpha=.1)
-
为了执行核密度估计,我们将风暴的
x
和y
坐标堆叠成一个形状为(2, N)
的数组:In [6]: h = np.vstack((xm, ym)) In [7]: kde = st.gaussian_kde(h)
-
gaussian_kde()
函数返回了一个 Python 函数。为了在地图上查看结果,我们需要在覆盖整个地图的二维网格上评估该函数。我们通过meshgrid()
创建此网格,并将x
和y
值传递给kde
函数。kde
接受一个形状为(2, N)
的数组作为输入,因此我们需要调整数组的形状:In [8]: k = 50 tx, ty = np.meshgrid(np.linspace(x0, x1, 2*k), np.linspace(y0, y1, k)) v = kde(np.vstack((tx.ravel(), ty.ravel()))).reshape((k, 2*k))
-
最后,我们使用
imshow()
显示估算的密度:In [9]: m.drawcoastlines() m.fillcontinents(color='#dbc8b2') xm, ym = m(x, y) m.imshow(v, origin='lower', extent=[x0,x1,y0,y1], cmap=plt.get_cmap('Reds'))
它是如何工作的...
一组 n 点 {x[i]} 的 核密度估计器 表示为:
在这里,h>0 是一个缩放参数(带宽),K(u) 是 核函数,它是一个对称函数,其积分为 1。这个估算器与经典的直方图进行比较,其中核是一个 顶帽 函数(一个取值在 {0,1} 中的矩形函数),但是这些块将位于规则的网格上,而不是数据点上。关于核密度估计器的更多信息,请参考 en.wikipedia.org/wiki/Kernel_density_estimation
。
可以选择多个核。这里,我们选择了 高斯核,因此 KDE 是以所有数据点为中心的高斯函数的叠加,它是密度的估算。
带宽的选择并非 trivial(简单的);它在一个过低的值(小偏差,高方差:过拟合)和一个过高的值(高偏差,小方差:欠拟合)之间存在权衡。我们将在下一章回到这个重要的偏差-方差权衡概念。有关偏差-方差权衡的更多信息,请参考en.wikipedia.org/wiki/Bias-variance_dilemma
。
以下图示意了 KDE。数据集包含四个位于* [0,1] *的点(黑线)。估计的密度是平滑曲线,这里使用了多个带宽值表示。
核密度估计
小贴士
在 statsmodels 和 scikit-learn 中有其他的 KDE 实现。你可以在jakevdp.github.io/blog/2013/12/01/kernel-density-estimation/
找到更多信息。
另见
- 使用最大似然法拟合概率分布到数据食谱
通过从后验分布中采样,使用马尔可夫链蒙特卡罗方法拟合贝叶斯模型
在本食谱中,我们展示了一种非常常见且有用的贝叶斯模型后验分布特征化方法。假设你有一些数据,想要获取关于潜在随机现象的信息。在频率学派的方法中,你可以尝试在给定的分布族中拟合一个概率分布,使用类似最大似然法的参数化方法。优化过程将得出最大化观察数据的概率的参数,假设为零假设。
在贝叶斯方法中,你将参数本身视为随机变量。它们的先验分布反映了你对这些参数的初始知识。在观察后,你的知识得到更新,并在参数的后验分布中体现出来。
贝叶斯推断的一个典型目标是特征化后验分布。贝叶斯定理提供了一种分析方法来实现这一目标,但由于模型的复杂性和维度的数量,它在实际问题中通常不切实际。马尔可夫链 蒙特卡罗方法,例如Metropolis-Hastings 算法,提供了一种数值方法来逼近后验分布。
这里,我们介绍了PyMC包,它提供了一种有效且自然的接口,用于在贝叶斯框架中拟合数据的概率模型。我们将使用来自美国国家海洋和大气管理局(NOAA)的数据,研究自 1850 年代以来北大西洋地区风暴的年频率。
本食谱主要受 PyMC 官网教程的启发(请参见更多内容...部分的链接)。
准备工作
你可以在该软件包的网站上找到安装 PyMC 的说明。在本示例中,我们将使用 PyMC2。新版本(PyMC3)在编写时仍在开发中,可能会有显著差异。有关 PyMC 的更多信息,请参阅pymc-devs.github.io/pymc/
。对于 Anaconda 用户,可以尝试conda install -c https://conda.binstar.org/pymc pymc
。Windows 用户也可以在www.lfd.uci.edu/~gohlke/pythonlibs/找到安装程序。
你还需要从书籍的 GitHub 仓库下载风暴数据集,链接为github.com/ipython-books/cookbook-data
,并将其解压到当前目录。
如何操作...
-
让我们导入标准包和 PyMC:
In [1]: import numpy as np import pandas as pd import pymc import matplotlib.pyplot as plt %matplotlib inline
-
让我们使用 pandas 导入数据:
In [2]: df = pd.read_csv( "data/Allstorms.ibtracs_wmo.v03r05.csv", delim_whitespace=False)
-
使用 pandas,只需一行代码就能得到北大西洋地区的年风暴数。我们首先选择该海域的风暴(
NA
),然后按年份(Season
)对行进行分组,再计算独特风暴的数量(Serial_Num
),因为每个风暴可能跨越几天(使用nunique()
方法):In [3]: cnt = df[df['Basin'] == ' NA'].groupby('Season') \ ['Serial_Num'].nunique() years = cnt.index y0, y1 = years[0], years[-1] arr = cnt.values plt.plot(years, arr, '-ok') plt.xlim(y0, y1) plt.xlabel("Year") plt.ylabel("Number of storms")
-
现在,我们定义我们的概率模型。我们假设风暴遵循时间依赖的泊松过程,并且具有一个确定性的速率。我们假设该速率是一个分段常数函数,在切换点
switchpoint
之前取值early_mean
,在切换点之后取值late_mean
。这三个未知参数被视为随机变量(我们将在它是如何工作的…部分中详细描述)。提示
泊松过程(
en.wikipedia.org/wiki/Poisson_process
)是一种特殊的点过程,即描述瞬时事件随机发生的随机过程。泊松过程是完全随机的:事件以给定速率独立发生。另见第十三章,随机动力学系统。In [4]: switchpoint = pymc.DiscreteUniform('switchpoint', lower=0, upper=len(arr)) early_mean = pymc.Exponential('early_mean', beta=1) late_mean = pymc.Exponential('late_mean', beta=1)
-
我们将分段常数速率定义为一个 Python 函数:
In [5]: @pymc.deterministic(plot=False) def rate(s=switchpoint, e=early_mean, l=late_mean): out = np.empty(len(arr)) out[:s] = e out[s:] = l return out
-
最后,观察变量是年风暴数。它遵循一个具有随机均值的泊松变量(底层泊松过程的速率)。这是泊松过程的一个已知数学性质。
In [6]: storms = pymc.Poisson('storms', mu=rate, value=arr, observed=True)
-
现在,我们使用 MCMC 方法从后验分布中进行采样,给定观察数据。
sample()
方法启动拟合的迭代过程:In [7]: model = pymc.Model([switchpoint, early_mean, late_mean, rate, storms]) In [8]: mcmc = pymc.MCMC(model) mcmc.sample(iter=10000, burn=1000, thin=10) [---- 17% ] 1774 of 10000 complete [-----------100%-----------] 10000 of 10000 complete
-
让我们绘制采样的马尔科夫链。它们的平稳分布对应我们想要表征的后验分布。
In [9]: plt.subplot(311) plt.plot(mcmc.trace('switchpoint')[:]) plt.ylabel("Switch point") plt.subplot(312) plt.plot(mcmc.trace('early_mean')[:]) plt.ylabel("Early mean") plt.subplot(313) plt.plot(mcmc.trace('late_mean')[:]) plt.xlabel("Iteration") plt.ylabel("Late mean")
-
我们还绘制了样本的分布,这些样本对应我们参数的后验分布,数据点已被考虑在内:
In [10]: plt.subplot(131) plt.hist(mcmc.trace('switchpoint')[:] + y0, 15) plt.xlabel("Switch point") plt.ylabel("Distribution") plt.subplot(132) plt.hist(mcmc.trace('early_mean')[:], 15) plt.xlabel("Early mean") plt.subplot(133) plt.hist(mcmc.trace('late_mean')[:], 15) plt.xlabel("Late mean")
-
通过这些分布的样本均值,我们得到三个未知参数的后验估计,包括风暴频率突然增加的年份:
In [11]: yp = y0 + mcmc.trace('switchpoint')[:].mean() em = mcmc.trace('early_mean')[:].mean() lm = mcmc.trace('late_mean')[:].mean() print((yp, em, lm)) (1966.681111111111, 8.2843072252292682, 16.728831395584947)
-
现在,我们可以将估计的比率绘制在观察数据之上:
In [12]: plt.plot(years, arr, '-ok') plt.axvline(yp, color='k', ls='--') plt.plot([y0, yp], [em, em], '-b', lw=3) plt.plot([yp, y1], [lm, lm], '-r', lw=3) plt.xlim(y0, y1) plt.xlabel("Year") plt.ylabel("Number of storms")
它是如何工作的……
一般思路是定义一个贝叶斯概率模型,并将其拟合到数据中。该模型可能是估计或决策任务的起点。模型本质上由通过有向无环图(DAG)连接的随机或确定性变量描述。A与B相连,如果B完全或部分由A决定。下图展示了本示例中使用的模型图:
In [13]: graph = pymc.graph.graph(model)
from IPython.display import display_png
display_png(graph.create_png(), raw=True)
提示
如你所见,PyMC 可以创建模型的图形表示。你需要安装 GraphViz(参考www.graphviz.org)、pydot 和 pyparsing。由于一个不幸的 bug,你可能需要安装 pyparsing 的特定版本:
pip install pyparsing==1.5.7
pip install pydot
随机变量遵循可以通过模型中的固定数字或其他变量进行参数化的分布。参数本身也可以是随机变量,反映了观察之前的知识。这是贝叶斯建模的核心。
分析的目标是将观察结果纳入模型中,以便随着更多数据的可用,更新我们的知识。尽管贝叶斯定理为我们提供了计算后验分布的精确方法,但在现实世界的问题中很少能实际应用。这主要是由于模型的复杂性。为了应对这一问题,已经开发了数值方法。
这里使用的马尔科夫链蒙特卡洛(MCMC)方法使我们能够通过模拟一个具有所需分布作为平衡分布的马尔科夫链,从复杂的分布中采样。梅特罗波利斯-哈斯廷斯算法是这种方法在当前示例中的具体应用。
该算法在 PyMC 的MCMC
类中实现。burn
参数决定丢弃多少初始迭代次数。这是必要的,因为马尔科夫链需要经过若干次迭代才能收敛到其平衡分布。thin
参数对应于在评估分布时跳过的步数,以尽量减少样本的自相关性。你可以在pymc-devs.github.io/pymc/modelfitting.html
找到更多信息。
还有更多内容……
这里有一些参考资料:
-
我们大量借鉴的一个优秀的 PyMC 教程可以在
pymc-devs.github.io/pymc/tutorial.html
找到。 -
由 Cameron Davidson-Pilon 撰写的关于该主题的必读免费电子书,完全使用 IPython 笔记本编写,可通过
camdavidsonpilon.github.io/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers/
获取。 -
在
en.wikipedia.org/wiki/Markov_chain_Monte_Carlo
介绍的马尔可夫链蒙特卡洛方法 -
在
en.wikipedia.org/wiki/Metropolis-Hastings_algorithm
介绍的 Metropolis-Hastings 算法
另见
- 贝叶斯方法入门配方
在 IPython 笔记本中使用 R 编程语言分析数据
R (www.r-project.org)是一种免费的特定领域编程语言,用于统计学。其语法非常适合统计建模和数据分析。相比之下,Python 的语法通常更适用于通用编程。幸运的是,IPython 使你可以同时享受两者的优势。例如,你可以在普通的 IPython 笔记本中随时插入 R 代码片段。你可以继续使用 Python 和 pandas 进行数据加载和整理,然后切换到 R 来设计和拟合统计模型。使用 R 代替 Python 进行这些任务,不仅仅是语法问题;R 自带一个令人印象深刻的统计工具箱,目前 Python 尚无匹敌。
在这个配方中,我们将展示如何在 IPython 中使用 R,并通过一个简单的数据分析示例来展示 R 的最基本功能。
准备工作
你需要 statsmodels 包来进行此配方的操作。你可以在之前的配方中找到安装说明,使用最大似然方法拟合概率分布到数据。
你还需要安装 R。使用 R 从 IPython 的步骤有三个。首先,安装 R 和 rpy2(R 到 Python 的接口)。当然,这一步只需要做一次。然后,为了在 IPython 会话中使用 R,你需要加载 IPython 的 R 扩展。
-
从
cran.r-project.org/mirrors.html
下载适合你操作系统的 R 并进行安装。在 Ubuntu 上,你可以执行sudo apt-get install r-base-dev
。 -
从
rpy.sourceforge.net/rpy2.html
下载 rpy2 并进行安装。在 Linux 上使用 Anaconda 时,你可以尝试conda install -c https://conda.binstar.org/r rpy2
。或者,你也可以执行pip install rpy2
。 -
然后,要在 IPython 笔记本中执行 R 代码,首先执行
%load_ext rmagic
。提示
rpy2 似乎在 Windows 上不太兼容。我们建议使用 Linux 或 Mac OS X。
如何操作...
在这里,我们将使用以下工作流程:首先,从 Python 加载数据。然后,使用 R 设计和拟合模型,并在 IPython 笔记本中绘制一些图表。我们也可以从 R 加载数据,或者使用 Python 的 statsmodels 包设计和拟合统计模型,等等。特别地,我们在这里做的分析本可以完全在 Python 中进行,而无需依赖 R 语言。这个示例仅展示了 R 的基础知识,并说明了 R 和 Python 如何在 IPython 会话中协同工作。
-
让我们使用 statsmodels 包加载 longley 数据集。这个数据集包含了美国从 1947 年到 1962 年的一些经济指标。我们还加载了 IPython 的 R 扩展:
In [1]: import statsmodels.datasets as sd In [2]: data = sd.longley.load_pandas() In [3]: %load_ext rmagic
-
我们将
x
和y
定义为外生(自变量)和内生(因变量)变量。内生变量量化了国家的总就业量。In [4]: data.endog_name, data.exog_name Out[4]: ('TOTEMP', ['GNPDEFL', 'GNP', 'UNEMP', 'ARMED', 'POP', 'YEAR']) In [5]: y, x = data.endog, data.exog
-
为了方便,我们将内生变量添加到
x
数据框中:In [6]: x['TOTEMP'] = y In [7]: x Out[7]: GNPDEFL GNP UNEMP POP YEAR TOTEMP 0 83.0 234289 2356 107608 1947 60323 1 88.5 259426 2325 108632 1948 61122 2 88.2 258054 3682 109773 1949 60171 ... 13 114.2 502601 3931 125368 1960 69564 14 115.7 518173 4806 127852 1961 69331 15 116.9 554894 4007 130081 1962 70551
-
我们将在 R 中绘制一个简单的图表。首先,我们需要将 Python 变量传递给 R。我们可以使用
%R -i var1,var2
魔法。然后,我们可以调用 R 的plot()
命令:In [8]: gnp = x['GNP'] totemp = x['TOTEMP'] In [9]: %R -i totemp,gnp plot(gnp, totemp)
-
现在,数据已传递给 R,我们可以对数据进行线性回归。
lm()
函数让我们进行线性回归。在这里,我们希望将totemp
(总就业)表示为国家 GNP 的函数:In [10]: %%R # Least-squares regression fit <- lm(totemp ~ gnp); # Display the coefficients of the fit. print(fit$coefficients) plot(gnp, totemp) # Plot the data points. abline(fit) # And plot the linear regression. (Intercept) gnp 5.184359e+04 3.475229e-02
它是如何工作的...
%R
魔法中的 -i
和 -o
选项允许我们在 IPython 和 R 之间传递变量。变量名称需要用逗号分隔。你可以在 rpy.sourceforge.net/
的文档中找到有关 %R
魔法的更多信息。
在 R 中,波浪符号 (~
) 表示因变量对一个或多个自变量的依赖关系。lm()
函数允许我们对数据拟合一个简单的线性回归模型。在这里,totemp
被表示为 gnp
的函数:
在这里,b(截距)和 a 是线性回归模型的系数。这两个值由 fit$coefficients
在 R 中返回,其中 fit
是拟合的模型。
当然,我们的数据点并不完全满足这个关系。系数的选择是为了最小化这个线性预测与实际值之间的误差。这通常通过最小化以下最小二乘误差来完成:
数据点是 (gnp[i], totemp[i])。由 lm()
返回的系数 a 和 b 使得这个和最小化:它们最适合数据。
还有更多...
回归分析是一个重要的统计概念,我们将在下一章中详细讨论。以下是一些参考资料:
-
维基百科上的回归分析,链接:
en.wikipedia.org/wiki/Regression_analysis
-
维基百科上的最小二乘法,网址为 en.wikipedia.org/wiki/Linear_least_squares_(mathematics)
R 是一个非常适合高级统计分析的平台。虽然 Python 有一些统计包,如 pandas 和 statsmodels,能够实现许多常见功能,但目前 R 提供的统计工具箱数量仍然无可匹敌。然而,Python 在统计学以外有着更广泛的应用范围,并且是一个出色的通用编程语言,配备了大量不同的包。
由于 IPython 支持多种语言,你不必在这些语言之间做出选择。你可以继续使用 Python,在需要 Python 仍未涵盖的高度特定统计功能时,切换到 R。
这里有一些关于 R 的参考资料:
-
R 教程,网址为 www.cyclismo.org/tutorial/R/
-
CRAN(综合 R 存档网络),包含许多 R 包,网址为
cran.r-project.org
-
可以在
nbviewer.ipython.org/github/ipython/ipython/blob/master/examples/Builtin%20Extensions/R%20Magics.ipynb
查阅 IPython 和 R 教程
另请参见
- 使用 pandas 和 matplotlib 探索数据集 方案
第八章:机器学习
在本章中,我们将涵盖以下主题:
-
开始使用 scikit-learn
-
使用逻辑回归预测谁能在泰坦尼克号上生还
-
使用 K 近邻分类器学习识别手写数字
-
从文本中学习 – 使用朴素贝叶斯进行自然语言处理
-
使用支持向量机进行分类任务
-
使用随机森林选择回归任务中的重要特征
-
使用主成分分析(PCA)降低数据集的维度
-
使用聚类方法发现数据集中的隐藏结构
介绍
在上一章中,我们关注的是通过部分观察获得数据的洞察、理解复杂现象,并在不确定性面前做出明智决策。在这里,我们仍然关注使用统计工具分析和处理数据。然而,目标不一定是理解数据,而是从数据中学习。
从数据中学习类似于我们人类的学习方式。从经验中,我们直觉地学习世界的普遍事实和关系,尽管我们未必完全理解其复杂性。随着计算机计算能力的增强,它们也能从数据中学习。这就是机器学习的核心,机器学习是人工智能、计算机科学、统计学和应用数学中的一个现代而迷人的分支。有关机器学习的更多信息,请参考 en.wikipedia.org/wiki/Machine_learning
。
本章是对机器学习一些最基础方法的实践介绍。这些方法是数据科学家日常使用的。我们将使用scikit-learn,一个流行且用户友好的 Python 机器学习包,来运用这些方法。
一些词汇
在本介绍中,我们将解释机器学习的基本定义和概念。
从数据中学习
在机器学习中,大部分数据可以表示为一个数值表格。每一行称为观察值、样本或数据点。每一列称为特征或变量。
设N为行数(或数据点的数量),D为列数(或特征的数量)。数字 D 也被称为数据的维度。原因是我们可以将这个表格视为一个在 D 维空间中的向量集 E(或向量空间)。在这里,一个向量 x 包含 D 个数字 (x[1], ..., x[D]),也叫做分量。这种数学视角非常有用,我们将在本章中持续使用它。
我们通常区分监督学习和无监督学习:
-
监督学习是指我们有一个与数据点x相关联的标签y。目标是从我们的数据中学习从x到y的映射。数据为我们提供了一个有限点集的映射,但我们希望将这个映射泛化到整个集合E。
-
无监督学习是指我们没有任何标签。我们希望做的是发现数据中的某种隐藏结构。
监督学习
从数学角度看,监督学习包括找到一个函数f,将点集E映射到标签集F,并且已知一个有限的关联集(x, y),这些数据来自我们的数据集。这就是泛化的意义所在:在观察了对(x[i], y[i])后,给定一个新的x,我们能够通过将函数f应用到x上来找到相应的y。关于监督学习的更多信息,请参阅en.wikipedia.org/wiki/Supervised_learning
。
通常做法是将数据集分为两个子集:训练集和测试集。我们在训练集上学习函数f,并在测试集上进行测试。这对于评估模型的预测能力至关重要。如果在同一数据集上训练和测试模型,我们的模型可能无法很好地泛化。这就是过拟合的基本概念,我们将在本章后面详细讲解。
我们通常区分分类和回归,它们是监督学习的两种特定实例。
分类是指我们的标签y只能取有限的值(类别)。例如:
-
手写数字识别:x是一个包含手写数字的图像;y是一个 0 到 9 之间的数字
-
垃圾邮件过滤:x是电子邮件,y是 1 或 0,取决于该电子邮件是否是垃圾邮件
回归是指我们的标签y可以取任何实数(连续值)。例如:
-
股票市场数据预测
-
销售预测
-
从图片中检测一个人的年龄
分类任务将我们的空间E划分为不同的区域(也称为划分),每个区域与标签y的某个特定值相关联。回归任务则产生一个数学模型,将一个实数与空间E中的任何点x关联。这一差异可以通过下图来说明:
分类与回归的区别
分类与回归可以结合使用。例如,在probit 模型中,尽管因变量是二元的(分类),但该变量属于某一类别的概率也可以通过回归来建模。我们将在关于逻辑回归的示例中看到这一点。关于 probit 模型的更多信息,请参阅en.wikipedia.org/wiki/Probit_model
。
无监督学习
广义而言,非监督学习帮助我们发现数据中的系统性结构。这比监督学习更难理解,因为通常没有明确的问题和答案。欲了解更多非监督学习的信息,请参考en.wikipedia.org/wiki/Unsupervised_learning
。
这里有一些与非监督学习相关的重要术语:
-
聚类:将相似的点聚集在簇内
-
密度估计:估计一个概率密度函数,用以解释数据点的分布
-
降维:通过将数据点投影到低维空间,获取高维数据点的简单表示(特别是用于数据可视化)
-
流形学习:找到包含数据点的低维流形(也称为非线性降维)
特征选择和特征提取
在监督学习的背景下,当我们的数据包含许多特征时,有时需要选择其中的一个子集。我们想保留的特征是那些与我们问题最相关的特征。这就是特征选择的问题。
此外,我们可能希望通过对原始数据集应用复杂的变换来提取新特征。这就是特征提取。例如,在计算机视觉中,直接在像素上训练分类器通常不是最有效的方法。我们可能希望提取相关的兴趣点或进行适当的数学变换。这些步骤取决于我们的数据集和我们希望回答的问题。
例如,在使用学习模型之前,通常需要对数据进行预处理。特征缩放(或数据归一化)是一个常见的预处理步骤,其中特征会线性地重新缩放,以适应范围[-1,1]或[0,1]。
特征提取和特征选择涉及领域专业知识、直觉和数学方法的平衡组合。这些早期步骤至关重要,可能比学习步骤本身还要重要。原因是与我们问题相关的少数维度通常隐藏在数据集的高维度中。我们需要揭示感兴趣的低维结构,以提高学习模型的效率。
在本章中,我们将看到一些特征选择和特征提取方法。特定于信号、图像或声音的方法将在第十章,信号处理,和第十一章,图像和音频处理中介绍。
这里有一些进一步的参考资料:
-
在 scikit-learn 中的特征选择,文档说明请参见
scikit-learn.org/stable/modules/feature_selection.html
-
维基百科上的特征选择
en.wikipedia.org/wiki/Feature_selection
过拟合、欠拟合与偏差-方差权衡
机器学习中的一个核心概念是过拟合与欠拟合之间的权衡。一个模型可能能够准确地表示我们的数据。然而,如果它过于准确,它可能无法很好地推广到未见过的数据。例如,在面部识别中,过于精确的模型可能无法识别当天发型不同的人。原因是我们的模型可能会在训练数据中学习到无关的特征。相反,一个训练不足的模型也无法很好地推广。例如,它可能无法正确识别双胞胎。有关过拟合的更多信息,请参考 en.wikipedia.org/wiki/Overfitting
。
一个减少过拟合的常见方法是向模型中添加结构,例如,通过正则化。这种方法在训练过程中倾向于选择更简单的模型(奥卡姆剃刀原则)。你可以在 en.wikipedia.org/wiki/Regularization_%28mathematics%29
找到更多信息。
偏差-方差困境与过拟合和欠拟合问题密切相关。一个模型的偏差量化了它在多个训练集上的准确性。方差量化了模型对训练集小变化的敏感性。一个鲁棒的模型不会对小的变化过于敏感。这个困境涉及到同时最小化偏差和方差;我们希望模型既精确又鲁棒。简单的模型通常不太准确,但更鲁棒;复杂的模型往往更准确,但鲁棒性差。有关偏差-方差困境的更多信息,请参考 en.wikipedia.org/wiki/Bias-variance_dilemma
。
这个权衡的重要性无法被过分强调。这个问题贯穿了整个机器学习领域。我们将在本章中看到具体的例子。
模型选择
正如我们在本章中所看到的,存在许多监督学习和无监督学习的算法。例如,本章将讨论一些著名的分类器,包括逻辑回归、最近邻、朴素贝叶斯和支持向量机。还有许多其他算法我们无法在这里讨论。
没有任何模型能在所有情况下都表现得比其他模型更好。一个模型可能在某个数据集上表现良好,而在另一个数据集上表现差。这就是模型选择的问题。
我们将看到一些系统化的方法来评估模型在特定数据集上的质量(特别是交叉验证)。实际上,机器学习并不是一门“精确的科学”,因为它通常涉及试错过程。我们需要尝试不同的模型,并通过经验选择表现最好的那个。
尽管如此,理解学习模型的细节使我们能够直观地了解哪个模型最适合当前问题。
这里有一些关于这个问题的参考资料:
-
维基百科上的模型选择,详见
en.wikipedia.org/wiki/Model_selection
-
scikit-learn 文档中的模型评估,详见
scikit-learn.org/stable/modules/model_evaluation.html
-
关于如何选择分类器的博客文章,详见
blog.echen.me/2011/04/27/choosing-a-machine-learning-classifier/
机器学习参考资料
这里有一些优秀的、数学内容较多的机器学习教科书:
-
模式识别与机器学习,Christopher M. Bishop,(2006),Springer
-
机器学习——一种概率视角,Kevin P. Murphy,(2012),MIT Press
-
统计学习的元素,Trevor Hastie,Robert Tibshirani,Jerome Friedman,(2009),Springer
这里有一些更适合没有强数学背景的程序员的书籍:
-
黑客的机器学习,Drew Conway,John Myles White,(2012),O'Reilly Media
-
机器学习实战,Peter Harrington,(2012),Manning Publications Co.
你可以在线找到更多的参考资料。
本章中我们未能涵盖的机器学习方法的重要类别包括神经网络和深度学习。深度学习是机器学习中一个非常活跃的研究领域。许多最先进的成果目前都是通过使用深度学习方法实现的。有关深度学习的更多信息,请参见 en.wikipedia.org/wiki/Deep_learning
。
开始使用 scikit-learn
在这个食谱中,我们介绍了机器学习 scikit-learn 包的基础知识 (scikit-learn.org
)。这个包是我们在本章中将要使用的主要工具。它简洁的 API 使得定义、训练和测试模型变得非常容易。而且,scikit-learn 专为速度和(相对)大数据而设计。
我们将在这里展示一个非常基础的线性回归示例,应用于曲线拟合的背景下。这个玩具示例将帮助我们说明关键概念,如线性模型、过拟合、欠拟合、正则化和交叉验证。
准备工作
你可以在主文档中找到安装 scikit-learn 的所有指令。更多信息,请参考 scikit-learn.org/stable/install.html
。使用 anaconda 时,你可以在终端中输入 conda install scikit-learn
。
如何操作...
我们将生成一个一维数据集,使用一个简单的模型(包括一些噪声),并尝试拟合一个函数到这些数据上。通过这个函数,我们可以对新的数据点进行预测。这是一个曲线拟合回归问题。
-
首先,让我们进行所有必要的导入:
In [1]: import numpy as np import scipy.stats as st import sklearn.linear_model as lm import matplotlib.pyplot as plt %matplotlib inline
-
我们现在定义一个确定性的非线性函数,作为我们生成模型的基础:
In [2]: f = lambda x: np.exp(3 * x)
-
我们生成在 [0,2] 范围内的曲线值:
In [3]: x_tr = np.linspace(0., 2, 200) y_tr = f(x_tr)
-
现在,让我们生成在 [0,1] 范围内的数据点。我们使用函数 f 并加入一些高斯噪声:
In [4]: x = np.array([0, .1, .2, .5, .8, .9, 1]) y = f(x) + np.random.randn(len(x))
-
让我们在 [0,1] 范围内绘制我们的数据点:
In [5]: plt.plot(x_tr[:100], y_tr[:100], '--k') plt.plot(x, y, 'ok', ms=10)
在图像中,虚线曲线表示生成模型。
-
现在,我们使用 scikit-learn 将线性模型拟合到数据上。这个过程有三个步骤。首先,我们创建模型(
LinearRegression
类的一个实例)。然后,我们将模型拟合到我们的数据上。最后,我们从训练好的模型中预测值。In [6]: # We create the model. lr = lm.LinearRegression() # We train the model on our training dataset. lr.fit(x[:, np.newaxis], y) # Now, we predict points with our trained model. y_lr = lr.predict(x_tr[:, np.newaxis])
注意
我们需要将
x
和x_tr
转换为列向量,因为在 scikit-learn 中,一般约定观测值是行,特征是列。在这里,我们有七个观测值,每个观测值有一个特征。 -
我们现在绘制训练后的线性模型结果。这里我们得到了一条绿色的回归线:
In [7]: plt.plot(x_tr, y_tr, '--k') plt.plot(x_tr, y_lr, 'g') plt.plot(x, y, 'ok', ms=10) plt.xlim(0, 1) plt.ylim(y.min()-1, y.max()+1) plt.title("Linear regression")
-
线性拟合在这里并不适用,因为数据点是根据非线性模型(指数曲线)生成的。因此,我们现在将拟合一个非线性模型。更准确地说,我们将为数据点拟合一个多项式函数。我们仍然可以使用线性回归来做到这一点,方法是预先计算数据点的指数。这是通过生成范德蒙德矩阵来完成的,使用的是
np.vander
函数。我们将在它是如何工作的...部分解释这个技巧。在下面的代码中,我们执行并绘制拟合结果:In [8]: lrp = lm.LinearRegression() plt.plot(x_tr, y_tr, '--k') for deg in [2, 5]: lrp.fit(np.vander(x, deg + 1), y) y_lrp = lrp.predict(np.vander(x_tr, deg + 1)) plt.plot(x_tr, y_lrp, label='degree ' + str(deg)) plt.legend(loc=2) plt.xlim(0, 1.4) plt.ylim(-10, 40) # Print the model's coefficients. print(' '.join(['%.2f' % c for c in lrp.coef_])) plt.plot(x, y, 'ok', ms=10) plt.title("Linear regression") 25.00 -8.57 0.00 -132.71 296.80 -211.76 72.80 -8.68 0.00
我们拟合了两个多项式模型,分别是 2 次和 5 次的多项式。2 次多项式似乎拟合数据点的精度不如 5 次多项式。然而,它似乎更稳健;5 次多项式在预测数据点外的值时表现得非常差(例如,可以查看 x
* 1* 部分)。这就是我们所说的过拟合;通过使用一个过于复杂的模型,我们在训练数据集上获得了更好的拟合,但在数据集外的模型却不够稳健。
注意
注意 5 次多项式的系数非常大;这通常是过拟合的一个标志。
-
我们现在将使用一种不同的学习模型,称为岭回归。它与线性回归类似,不同之处在于它防止多项式系数变得过大。这正是前一个例子中发生的情况。通过在损失函数中添加正则化****项,岭回归对基础模型施加了一些结构。我们将在下一节中看到更多细节。
岭回归模型有一个超参数,表示正则化项的权重。我们可以使用
Ridge
类通过反复试验尝试不同的值。然而,scikit-learn 提供了另一个叫做RidgeCV
的模型,它包括 交叉验证的参数搜索。实际上,这意味着我们无需手动调整该参数——scikit-learn 会为我们完成。由于 scikit-learn 的模型始终遵循 fit-predict API,我们只需在之前的代码中将lm.LinearRegression()
替换为lm.RidgeCV()
。我们将在下一节中提供更多细节。In [9]: ridge = lm.RidgeCV() plt.plot(x_tr, y_tr, '--k') for deg in [2, 5]: ridge.fit(np.vander(x, deg + 1), y); y_ridge = ridge.predict(np.vander(x_tr, deg+1)) plt.plot(x_tr, y_ridge, label='degree ' + str(deg)) plt.legend(loc=2) plt.xlim(0, 1.5) plt.ylim(-5, 80) # Print the model's coefficients. print(' '.join(['%.2f' % c for c in ridge.coef_])) plt.plot(x, y, 'ok', ms=10) plt.title("Ridge regression") 11.36 4.61 0.00 2.84 3.54 4.09 4.14 2.67 0.00
这次,5 次多项式似乎比简单的 2 次多项式更加精确(后者现在导致 欠拟合)。岭回归在此处缓解了过拟合问题。观察 5 次多项式的系数比前一个例子中要小得多。
它是如何工作的...
本节我们将解释本食谱中涉及的所有方面。
scikit-learn API
scikit-learn 为监督学习和无监督学习实现了一个简洁且一致的 API。我们的数据点应该存储在一个 (N,D) 矩阵 X 中,其中 N 是观测值的数量,D 是特征的数量。换句话说,每一行都是一个观测值。机器学习任务的第一步是明确矩阵 X 的确切含义。
在监督学习中,我们还需要一个 目标,一个长度为 N 的向量 y,每个观测值对应一个标量值。这个值是连续的或离散的,具体取决于我们是回归问题还是分类问题。
在 scikit-learn 中,模型通过包含 fit()
和 predict()
方法的类来实现。fit()
方法接受数据矩阵 X 作为输入,对于监督学习模型,还接受 y。此方法用于在给定数据上训练模型。
predict()
方法也接受数据点作为输入(作为 (M,D) 矩阵)。它返回训练模型预测的标签或转换后的点。
普通最小二乘回归
普通最小二乘回归 是最简单的回归方法之一。它通过 X[ij] 的线性组合来逼近输出值 y[i]:
这里,w = (w[1], ..., w[D]) 是(未知的)参数向量。另外, 代表模型的输出。我们希望这个向量与数据点 y 尽可能匹配。当然,精确的相等式
通常是不可能成立的(总会有一些噪声和不确定性——模型始终是对现实的理想化)。因此,我们希望最小化这两个向量之间的差异。普通最小二乘回归方法的核心是最小化以下 损失函数:
这些组件的平方和称为L² 范数。它之所以方便,是因为它导致了可微分的损失函数,从而可以计算梯度,并进行常见的优化过程。
使用线性回归的多项式插值
普通最小二乘回归将线性模型拟合到数据上。该模型在数据点x[i]和参数w[j]中都是线性的。在我们的例子中,由于数据点是根据非线性生成模型(一个指数函数)生成的,因此我们获得了较差的拟合。
然而,我们仍然可以使用线性回归方法,模型在w[j]上是线性的,但在x[i]上是非线性的。为此,我们需要通过使用多项式函数的基来增加数据集的维度。换句话说,我们考虑以下数据点:
这里,D是最大阶数。因此,输入矩阵X是与原始数据点x[i]相关的范德蒙德矩阵。有关范德蒙德矩阵的更多信息,请参见en.wikipedia.org/wiki/Vandermonde_matrix
。
在这里,很容易看出,在这些新数据点上训练线性模型等同于在原始数据点上训练多项式模型。
岭回归
使用线性回归的多项式插值如果多项式的阶数过大,可能导致过拟合。通过捕捉随机波动(噪声)而不是数据的一般趋势,模型失去了部分预测能力。这对应于多项式系数w[j]的发散。
解决这个问题的方法是防止这些系数无限增大。通过岭回归(也称为Tikhonov 正则化),这是通过向损失函数中添加正则化项来实现的。有关 Tikhonov 正则化的更多信息,请参见en.wikipedia.org/wiki/Tikhonov_regularization
。
通过最小化这个损失函数,我们不仅最小化了模型与数据之间的误差(第一项,与偏差相关),还最小化了模型系数的大小(第二项,与方差相关)。偏差-方差权衡通过超参数量化,它指定了损失函数中两项之间的相对权重。
在这里,岭回归导致了具有较小系数的多项式,因此得到了更好的拟合。
交叉验证和网格搜索
岭回归模型相较于普通最小二乘法模型的一个缺点是多了一个额外的超参数 。预测的质量取决于该参数的选择。一种可能的做法是手动微调这个参数,但这个过程可能非常繁琐,并且可能会导致过拟合问题。
为了解决这个问题,我们可以使用 网格搜索;我们遍历多个可能的 值,并评估每个可能值下模型的性能。然后,我们选择使性能最好的参数。
我们如何评估具有给定 值的模型性能?一种常见的解决方案是使用 交叉验证。该过程将数据集分为训练集和测试集。我们在训练集上拟合模型,然后在 测试集 上测试其预测性能。通过在与训练集不同的数据集上测试模型,我们可以减少过拟合的风险。
有许多方法可以将初始数据集分成两部分。一个方法是移除 一个 样本,形成训练集,并将这个样本放入测试集中。这被称为 留一法 交叉验证。对于 N 个样本,我们将得到 N 组训练集和测试集。交叉验证性能是所有这些数据集划分的平均性能。
如我们稍后所见,scikit-learn 实现了几个易于使用的函数,用于进行交叉验证和网格搜索。在这个示例中,存在一个名为RidgeCV
的特殊估计器,它实现了特定于岭回归模型的交叉验证和网格搜索过程。使用这个类可以自动为我们找到最佳的超参数 。
还有更多内容…
这里有一些关于最小二乘法的参考资料:
-
维基百科上的普通最小二乘法,详情请见
en.wikipedia.org/wiki/Ordinary_least_squares
-
维基百科上的线性最小二乘法,详情请见
en.wikipedia.org/wiki/Linear_least_squares_(mathematics)
这里有一些关于交叉验证和网格搜索的参考资料:
-
scikit-learn 文档中的交叉验证,详情请见
scikit-learn.org/stable/modules/cross_validation.html
-
scikit-learn 文档中的网格搜索,详情请见
scikit-learn.org/stable/modules/grid_search.html
-
维基百科上的交叉验证,详情请见
en.wikipedia.org/wiki/Cross-validation_%28statistics%29
这里有一些关于 scikit-learn 的参考资料:
-
scikit-learn 基础教程,见
scikit-learn.org/stable/tutorial/basic/tutorial.html
-
scikit-learn 在 SciPy 2013 年会上的教程,见
github.com/jakevdp/sklearn_scipy2013
另请参见
- 使用支持向量机进行分类任务 的实例
使用逻辑回归预测谁会在泰坦尼克号上生还
在这个实例中,我们将介绍 逻辑回归,一种基本的分类器。我们还将展示如何使用 网格搜索 和 交叉验证。
我们将在 Kaggle 数据集上应用这些技术,该数据集的目标是根据真实数据预测泰坦尼克号上的生还情况。
小贴士
Kaggle (www.kaggle.com/competitions) 主办机器学习比赛,任何人都可以下载数据集,训练模型,并在网站上测试预测结果。最佳模型的作者甚至可能获得奖品!这是一个很好的入门机器学习的方式。
准备就绪
从本书的 GitHub 仓库下载 Titanic 数据集,链接:github.com/ipython-books/cookbook-data
。
数据集来源于 www.kaggle.com/c/titanic-gettingStarted。
如何做...
-
我们导入标准库:
In [1]: import numpy as np import pandas as pd import sklearn import sklearn.linear_model as lm import sklearn.cross_validation as cv import sklearn.grid_search as gs import matplotlib.pyplot as plt %matplotlib inline
-
我们使用 pandas 加载训练集和测试集:
In [2]: train = pd.read_csv('data/titanic_train.csv') test = pd.read_csv('data/titanic_test.csv') In [3]: train[train.columns[[2,4,5,1]]].head() Out[3]: Pclass Sex Age Survived 0 3 male 22 0 1 1 female 38 1 2 3 female 26 1 3 1 female 35 1 4 3 male 35 0
-
为了简化示例,我们只保留少数几个字段,并将
sex
字段转换为二进制变量,以便它能被 NumPy 和 scikit-learn 正确处理。最后,我们移除包含NaN
值的行:In [4]: data = train[['Sex', 'Age', 'Pclass', 'Survived']].copy() data['Sex'] = data['Sex'] == 'female' data = data.dropna()
-
现在,我们将
DataFrame
对象转换为 NumPy 数组,以便将其传递给 scikit-learn:In [5]: data_np = data.astype(np.int32).values X = data_np[:,:-1] y = data_np[:,-1]
-
让我们看看男性和女性乘客的生还情况,按年龄分类:
In [6]: # We define a few boolean vectors. female = X[:,0] == 1 survived = y == 1 # This vector contains the age of the passengers. age = X[:,1] # We compute a few histograms. bins_ = np.arange(0, 81, 5) S = {'male': np.histogram(age[survived & ~female], bins=bins_)[0], 'female': np.histogram(age[survived & female], bins=bins_)[0]} D = {'male': np.histogram(age[~survived & ~female], bins=bins_)[0], 'female': np.histogram(age[~survived & female], bins=bins_)[0]} In [7]: # We now plot the data. bins = bins_[:-1] for i, sex, color in zip((0, 1), ('male', 'female'), ('#3345d0', '#cc3dc0')): plt.subplot(121 + i) plt.bar(bins, S[sex], bottom=D[sex], color=color, width=5, label='survived') plt.bar(bins, D[sex], color='k', width=5, label='died') plt.xlim(0, 80) plt.grid(None) plt.title(sex + " survival") plt.xlabel("Age (years)") plt.legend()
-
让我们尝试训练一个
LogisticRegression
分类器,以预测人们基于性别、年龄和舱位的生还情况。我们首先需要创建一个训练集和一个测试集:In [8]: # We split X and y into train and test datasets. (X_train, X_test, y_train, y_test) = cv.train_test_split(X, y, test_size=.05) In [9]: # We instanciate the classifier. logreg = lm.LogisticRegression()
-
我们训练模型,并在测试集上获取预测值:
In [10]: logreg.fit(X_train, y_train) y_predicted = logreg.predict(X_test)
下图显示了实际结果和预测结果:
In [11]: plt.imshow(np.vstack((y_test, y_predicted)), interpolation='none', cmap='bone') plt.xticks([]); plt.yticks([]) plt.title(("Actual and predicted survival " "outcomes on the test set"))
在这个截图中,第一行显示了测试集中几个人的生还情况(白色表示生还,黑色表示未生还)。第二行显示了模型的预测值。
-
为了评估模型的性能,我们使用
cross_val_score()
函数计算交叉验证分数。默认情况下,该函数使用三折分层交叉验证过程,但可以通过cv
关键字参数进行修改:In [12]: cv.cross_val_score(logreg, X, y) Out[12]: array([ 0.78661088, 0.78991597, 0.78059072])
这个函数返回每一对训练集和测试集的预测分数(我们在如何运作…中提供了更多细节)。
-
LogisticRegression
类接受C超参数作为参数。该参数量化了正则化强度。为了找到合适的值,我们可以使用通用的GridSearchCV
类进行网格搜索。它接受一个估算器作为输入以及一个包含参数值的字典。这个新的估算器使用交叉验证来选择最佳参数:In [13]: grid = gs.GridSearchCV(logreg, {'C': np.logspace(-5, 5, 50)}) grid.fit(X_train, y_train) grid.best_params_ Out[13]: {'C': 5.35}
-
这是最佳估算器的性能:
In [14]: cv.cross_val_score(grid.best_estimator_, X, y) Out[14]: array([ 0.78661088, 0.79831933, 0.78481013])
在使用C超参数通过网格搜索选择后,性能略有提高。
它是如何工作的...
逻辑回归不是一个回归模型,而是一个分类模型。然而,它与线性回归有着密切的关系。该模型通过将sigmoid 函数(更准确地说是逻辑函数)应用于变量的线性组合,预测一个二元变量为 1 的概率。sigmoid 函数的方程为:
下图展示了一个逻辑函数:
逻辑函数
如果需要得到一个二元变量,我们可以将值四舍五入为最接近的整数。
参数w是在学习步骤中通过优化过程得到的。
还有更多...
以下是一些参考资料:
-
维基百科上的逻辑回归,网址:
en.wikipedia.org/wiki/Logistic_regression
-
scikit-learn 文档中的逻辑回归,网址:
scikit-learn.org/stable/modules/linear_model.html#logistic-regression
另见
-
开始使用 scikit-learn的教程
-
使用 K 近邻分类器识别手写数字的教程
-
使用支持向量机进行分类任务的教程
使用 K 近邻分类器识别手写数字
在本教程中,我们将展示如何使用K 近邻(K-NN)分类器来识别手写数字。这个分类器是一个简单但强大的模型,适用于复杂的、高度非线性的数据集,如图像。稍后我们将在本教程中详细解释它的工作原理。
操作步骤...
-
我们导入模块:
In [1]: import numpy as np import sklearn import sklearn.datasets as ds import sklearn.cross_validation as cv import sklearn.neighbors as nb import matplotlib.pyplot as plt %matplotlib inline
-
让我们加载digits数据集,这是 scikit-learn 的
datasets
模块的一部分。这个数据集包含已手动标记的手写数字:In [2]: digits = ds.load_digits() X = digits.data y = digits.target print((X.min(), X.max())) print(X.shape) 0.0 16.0 (1797L, 64L)
在矩阵
X
中,每一行包含8 * 8=64个像素(灰度值,范围在 0 到 16 之间)。使用行主序排列。 -
让我们显示一些图像及其标签:
In [3]: nrows, ncols = 2, 5 plt.gray() for i in range(ncols * nrows): ax = plt.subplot(nrows, ncols, i + 1) ax.matshow(digits.images[i,...]) plt.xticks([]); plt.yticks([]) plt.title(digits.target[i])
-
现在,让我们对数据拟合一个 K 近邻分类器:
In [4]: (X_train, X_test, y_train, y_test) = cv.train_test_split(X, y, test_size=.25) In [5]: knc = nb.KNeighborsClassifier() In [6]: knc.fit(X_train, y_train);
-
让我们在测试数据集上评估训练后的分类器的得分:
In [7]: knc.score(X_test, y_test) Out[7]: 0.98888888888888893
-
现在,让我们看看我们的分类器能否识别手写数字!
In [8]: # Let's draw a 1. one = np.zeros((8, 8)) one[1:-1, 4] = 16 # The image values are # in [0,16]. one[2, 3] = 16 In [9]: plt.imshow(one, interpolation='none') plt.grid(False) plt.xticks(); plt.yticks() plt.title("One")
我们的模型能识别这个数字吗?让我们来看看:
In [10]: knc.predict(one.ravel()) Out[10]: array([1])
干得好!
它是如何工作的...
这个例子展示了如何处理 scikit-learn 中的图像。图像是一个 2D 的(N, M)矩阵,有NM个特征。在组合数据矩阵时,这个矩阵需要被展平;每一行都是一个完整的图像。
K 最近邻算法的思想如下:在特征空间中给定一个新点,找到训练集中距离最近的K个点,并将它们中大多数点的标签赋给该新点。
距离通常是欧氏距离,但也可以使用其他距离。
下图展示了在玩具数据集上使用 15 最近邻分类器获得的空间划分(带有三个标签):
K 最近邻空间划分
数字K是模型的一个超参数。如果K太小,模型泛化能力不好(高方差)。特别是,它对异常值非常敏感。相反,如果K太大,模型的精度会变差。在极端情况下,如果K等于总数据点数,模型将始终预测相同的值,而不考虑输入(高偏差)。有一些启发式方法来选择这个超参数(请参阅下一节)。
需要注意的是,K 最近邻算法并没有学习任何模型;分类器只是存储所有数据点,并将任何新的目标点与它们进行比较。这是基于实例的学习的一个例子。这与其他分类器(如逻辑回归模型)形成对比,后者明确地在训练数据上学习一个简单的数学模型。
K 最近邻方法在复杂的分类问题上表现良好,这些问题具有不规则的决策边界。但是,对于大型训练数据集,它可能计算密集,因为必须为测试计算大量距离。可以使用专用的基于树的数据结构(如 K-D 树或球树)来加速最近邻的搜索。
K 最近邻方法可以用于分类(如本例)以及回归问题。该模型分配最近邻居的目标值的平均值。在这两种情况下,可以使用不同的加权策略。
还有更多内容…
下面是一些参考资料:
-
scikit-learn 文档中的 K-NN 算法,网址为
scikit-learn.org/stable/modules/neighbors.html
-
维基百科上的 K-NN 算法,网址为
en.wikipedia.org/wiki/K-nearest_neighbors_algorithm
-
博客文章介绍如何选择 K 超参数,网址为
datasciencelab.wordpress.com/2013/12/27/finding-the-k-in-k-means-clustering/
-
维基百科上的基于实例的学习,网址为
en.wikipedia.org/wiki/Instance-based_learning
另请参阅
-
用逻辑回归预测谁将在泰坦尼克号上幸存食谱
-
使用支持向量机进行分类任务食谱
从文本中学习 – 自然语言处理中的朴素贝叶斯
在这个食谱中,我们展示了如何使用 scikit-learn 处理文本数据。处理文本需要仔细的预处理和特征提取。同时,处理高稀疏矩阵也很常见。
我们将学习识别在公共讨论中发布的评论是否被认为是侮辱某个参与者的。我们将使用来自 Impermium 的标注数据集,该数据集是在 Kaggle 竞赛中发布的。
准备工作
从书籍的 GitHub 仓库下载Troll数据集,网址是github.com/ipython-books/cookbook-data
。
该数据集可以从 Kaggle 下载,网址是www.kaggle.com/c/detecting-insults-in-social-commentary。
如何做到...
-
让我们导入我们的库:
In [1]: import numpy as np import pandas as pd import sklearn import sklearn.cross_validation as cv import sklearn.grid_search as gs import sklearn.feature_extraction.text as text import sklearn.naive_bayes as nb import matplotlib.pyplot as plt %matplotlib inline
-
让我们用 pandas 打开 CSV 文件:
In [2]: df = pd.read_csv("data/troll.csv")
-
每一行是一个评论。我们将考虑两个列:评论是否为侮辱性(1)还是非侮辱性(0),以及评论的 Unicode 编码内容:
In [3]: df[['Insult', 'Comment']].tail() Insult Comment 3942 1 "you are both morons and that is..." 3943 0 "Many toolbars include spell check... 3944 0 "@LambeauOrWrigley\xa0\xa0@K.Moss\xa0\n... 3945 0 "How about Felix? He is sure turning into... 3946 0 "You're all upset, defending this hipster...
-
现在,我们将定义特征矩阵
X
和标签y
:In [4]: y = df['Insult']
从文本中获得特征矩阵并不是一件简单的事。scikit-learn 只能处理数值矩阵。那么,我们如何将文本转换成数值矩阵呢?一种经典的解决方案是,首先提取一个词汇表,即语料库中使用的所有词汇的列表。然后,我们为每个样本计算每个词汇的出现频率。我们最终得到一个稀疏矩阵,这是一个大矩阵,主要包含零。这里,我们只用两行代码来完成。我们将在它是如何工作的…部分提供更多细节。
注意
这里的一般规则是,当我们的特征是分类变量时(即,某个词的出现,属于固定集合的n种颜色中的一种,等等),我们应该通过考虑每个类别项的二元特征来向量化它。例如,
color
特征如果是red
、green
或blue
,我们应该考虑三个二元特征:color_red
、color_green
和color_blue
。我们将在更多内容…部分提供更多参考资料。In [5]: tf = text.TfidfVectorizer() X = tf.fit_transform(df['Comment']) print(X.shape) (3947, 16469)
-
共有 3947 条评论和 16469 个不同的词汇。让我们估算一下这个特征矩阵的稀疏性:
In [6]: print(("Each sample has ~{0:.2f}% non-zero" "features.").format( 100 * X.nnz / float(X.shape[0] * X.shape[1]))) Each sample has ~0.15% non-zero features.
-
现在,我们像往常一样训练分类器。我们首先将数据划分为训练集和测试集:
In [7]: (X_train, X_test, y_train, y_test) = cv.train_test_split(X, y, test_size=.2)
-
我们使用伯努利朴素贝叶斯分类器,并对
参数进行网格搜索:
In [8]: bnb = gs.GridSearchCV(nb.BernoulliNB(), param_grid={ 'alpha': np.logspace(-2., 2., 50)}) bnb.fit(X_train, y_train)
-
让我们检查一下这个分类器在测试数据集上的表现:
In [9]: bnb.score(X_test, y_test) Out[9]: 0.76455696202531642
-
让我们来看一下与最大系数对应的词汇(我们在侮辱性评论中经常找到的词汇):
In [10]: # We first get the words corresponding # to each feature. names = np.asarray(tf.get_feature_names()) # Next, we display the 50 words with the largest # coefficients. print(','.join(names[np.argsort( bnb.best_estimator_.coef_[0,:])[::-1][:50]])) you,are,your,to,the,and,of,that,is,it,in,like,on,have,for,not,re,just,an,with,so,all,***,***be,get,***,***up,this,what,xa0,don,***,***go,no,do,can,but,***,***or,as,if,***,***who,know,about,because,here,***,***me,was
-
最后,让我们在几个测试句子上测试我们的估算器:
In [11]: print(bnb.predict(tf.transform([ "I totally agree with you.", "You are so stupid.", "I love you." ]))) [0 1 1]
结果不错,但我们可能能够做得更好。
它是如何工作的...
scikit-learn 实现了多个实用函数,可以从文本数据中获取稀疏特征矩阵。像CountVectorizer()这样的向量化器从语料库中提取词汇(fit
),并基于该词汇构建语料库的稀疏表示(transform
)。每个样本由词汇的词频表示。训练后的实例还包含属性和方法,用于将特征索引映射到对应的词汇(get_feature_names()
)及反向映射(vocabulary_
)。
也可以提取 N-gram。这些是连续出现的词对或元组(ngram_range
关键字)。
词频可以以不同的方式加权。这里,我们使用了tf-idf,即词频-逆文档频率。这个量度反映了一个词对于语料库的重要性。评论中频繁出现的词语有较高的权重,除非它们出现在大多数评论中(这意味着它们是常见词汇,例如“the”和“and”会被这种技术过滤掉)。
朴素贝叶斯算法是基于特征之间独立性假设的贝叶斯方法。这一强假设极大简化了计算,从而使分类器非常快速且效果不错。
还有更多内容……
以下是一些参考资料:
-
scikit-learn 文档中的文本特征提取,见
scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction
-
维基百科上的词频-逆文档频率,见
en.wikipedia.org/wiki/tf-idf
-
scikit-learn 文档中的向量化器,见
scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.DictVectorizer.html
-
维基百科上的朴素贝叶斯分类器,见
en.wikipedia.org/wiki/Naive_Bayes_classifier
-
scikit-learn 文档中的朴素贝叶斯,见
scikit-learn.org/stable/modules/naive_bayes.html
-
Impermium Kaggle 挑战,见
blog.kaggle.com/2012/09/26/impermium-andreas-blog/
-
scikit-learn 文档中的文档分类示例,见
scikit-learn.org/stable/auto_examples/document_classification_20newsgroups.html
小贴士
除了 scikit-learn,它在文本处理方面有很好的支持,我们还应该提到 NLTK(可在www.nltk.org获取),这是一个 Python 中的自然语言工具包。
另见
-
使用逻辑回归预测谁会在泰坦尼克号上生还的教程
-
使用 K 最近邻分类器学习识别手写数字的教程
-
使用支持向量机进行分类任务的教程
使用支持向量机进行分类任务
在本教程中,我们介绍了支持向量机,或SVM。这些强大的模型可以用于分类和回归。在这里,我们展示了如何在一个简单的分类任务中使用线性和非线性 SVM。
如何操作...
-
让我们导入所需的包:
In [1]: import numpy as np import pandas as pd import sklearn import sklearn.datasets as ds import sklearn.cross_validation as cv import sklearn.grid_search as gs import sklearn.svm as svm import matplotlib.pyplot as plt %matplotlib inline
-
我们生成二维点,并根据坐标的线性操作分配二进制标签:
In [2]: X = np.random.randn(200, 2) y = X[:, 0] + X[:, 1] > 1
-
现在我们拟合一个线性支持向量分类器(SVC)。该分类器尝试用一个线性边界(这里是线,但更一般地是超平面)将两组点分开:
In [3]: # We train the classifier. est = svm.LinearSVC() est.fit(X, y)
-
我们定义了一个函数,用于显示训练好的分类器的边界和决策函数:
In [4]: # We generate a grid in the square [-3,3 ]². xx, yy = np.meshgrid(np.linspace(-3, 3, 500), np.linspace(-3, 3, 500)) # This function takes a SVM estimator as input. def plot_decision_function(est): # We evaluate the decision function on the # grid. Z = est.decision_function(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) cmap = plt.cm.Blues # We display the decision function on the grid. plt.imshow(Z, extent=(xx.min(), xx.max(), yy.min(), yy.max()), aspect='auto', origin='lower', cmap=cmap) # We display the boundaries. plt.contour(xx, yy, Z, levels=[0], linewidths=2, colors='k') # We display the points with their true labels. plt.scatter(X[:, 0], X[:, 1], s=30, c=.5+.5*y, lw=1, cmap=cmap, vmin=0, vmax=1) plt.axhline(0, color='k', ls='--') plt.axvline(0, color='k', ls='--') plt.xticks(()) plt.yticks(()) plt.axis([-3, 3, -3, 3])
-
让我们看看线性 SVC 的分类结果:
In [5]: plot_decision_function(est) plt.title("Linearly separable, linear SVC")
线性 SVC 尝试用一条线将点分开,它在这里做得相当不错。
-
现在,我们使用
XOR
函数修改标签。如果坐标的符号不同,则该点的标签为 1。这个分类任务是不可线性分割的。因此,线性 SVC 完全失败:In [6]: y = np.logical_xor(X[:, 0]>0, X[:, 1]>0) # We train the classifier. est = gs.GridSearchCV(svm.LinearSVC(), {'C': np.logspace(-3., 3., 10)}) est.fit(X, y) print("Score: {0:.1f}".format( v.cross_val_score(est, X, y).mean())) # Plot the decision function. plot_decision_function(est) plt.title("XOR, linear SVC") Score: 0.6
-
幸运的是,可以通过使用非线性核函数来使用非线性 SVC。核函数指定将点进行非线性变换,映射到更高维的空间中。假设该空间中的变换点更容易线性可分。默认情况下,scikit-learn 中的
SVC
分类器使用径向基函数(RBF)核函数:In [7]: y = np.logical_xor(X[:, 0]>0, X[:, 1]>0) est = gs.GridSearchCV(svm.SVC(), {'C': np.logspace(-3., 3., 10), 'gamma': np.logspace(-3., 3., 10)}) est.fit(X, y) print("Score: {0:.3f}".format( cv.cross_val_score(est, X, y).mean())) plot_decision_function(est.best_estimator_) plt.title("XOR, non-linear SVC") Score: 0.975
这次,非线性 SVC 成功地将这些非线性可分的点进行了分类。
如何操作...
二分类线性 SVC 尝试找到一个超平面(定义为线性方程),以最好地分离两组点(根据它们的标签分组)。另外,还有一个约束条件是,这个分离超平面需要尽可能远离点。这种方法在存在这样的超平面时效果最好。否则,这种方法可能完全失败,就像我们在XOR
示例中看到的那样。XOR
被认为是一个非线性可分的操作。
scikit-learn 中的 SVM 类有一个C超参数。这个超参数在训练样本的误分类与决策面简洁性之间进行权衡。较低的C值使得决策面平滑,而较高的C值则试图正确分类所有训练样本。这是另一个超参数量化偏差-方差权衡的例子。可以通过交叉验证和网格搜索来选择这个超参数。
线性 SVC 也可以扩展到多分类问题。多分类 SVC 在 scikit-learn 中直接实现。
非线性 SVC 通过考虑从原始空间到更高维空间的非线性转换来工作。这个非线性转换可以增加类别之间的线性可分性。在实践中,所有点积都被
核所替代。
非线性 SVC
有几种广泛使用的非线性核。默认情况下,SVC 使用高斯径向基函数:
在这里,是模型的超参数,可以通过网格搜索和交叉验证来选择。
函数不需要显式计算。这就是核技巧;只需要知道核函数k(x, x')即可。给定核函数k(x, x')所对应的函数的存在是由函数分析中的数学定理所保证的。
还有更多……
这里有一些关于支持向量机的参考资料:
-
维基百科上的异或,访问地址:
en.wikipedia.org/wiki/Exclusive_or
-
维基百科上的支持向量机,访问地址:
en.wikipedia.org/wiki/Support_vector_machine
-
scikit-learn 文档中的 SVM,访问地址:
scikit-learn.org/stable/modules/svm.html
-
维基百科上的核技巧,访问地址:
en.wikipedia.org/wiki/Kernel_method
-
关于核技巧的说明,访问地址:www.eric-kim.net/eric-kim-net/posts/1/kernel_trick.html
-
一个关于非线性 SVM 的示例,访问地址:
scikit-learn.org/0.11/auto_examples/svm/plot_svm_nonlinear.html
(这个示例启发了这个配方)
另见:
-
使用逻辑回归预测谁会在泰坦尼克号上生存的配方
-
使用 K 近邻分类器识别手写数字的配方
使用随机森林选择回归的重要特征
决策树经常被用来表示工作流程或算法。它们还构成了一种非参数监督学习方法。在训练集上学习一个将观察值映射到目标值的树,并且可以对新观察值做出预测。
随机森林是决策树的集成。通过训练多个决策树并将其结果聚合,形成的模型比任何单一的决策树更具表现力。这个概念就是集成学习的目的。
有许多类型的集成方法。随机森林是自助聚合(也称为bagging)的一种实现,其中模型是在从训练集中随机抽取的子集上训练的。
随机森林能提供每个特征在分类或回归任务中的重要性。在本实例中,我们将使用一个经典数据集,利用该数据集中的各类房屋邻里指标来找出波士顿房价的最具影响力特征。
如何实现...
-
我们导入所需的包:
In [1]: import numpy as np import sklearn as sk import sklearn.datasets as skd import sklearn.ensemble as ske import matplotlib.pyplot as plt %matplotlib inline
-
我们加载波士顿数据集:
In [2]: data = skd.load_boston()
数据集的详细信息可以在
data['DESCR']
中找到。以下是一些特征的描述:-
CRIM:各镇的人均犯罪率
-
NOX:氮氧化物浓度(每千万分之一)
-
RM:每套住房的平均房间数
-
AGE:1940 年前建造的自有住房单位的比例
-
DIS:到波士顿五个就业中心的加权距离
-
PTRATIO:各镇的师生比
-
LSTAT:低社会经济地位人口的比例
-
MEDV:以 $1000 为单位的自有住房中位数价格
目标值是 MEDV。
-
-
我们创建一个
RandomForestRegressor
模型:In [3]: reg = ske.RandomForestRegressor()
-
我们从这个数据集中获取样本和目标值:
In [4]: X = data['data'] y = data['target']
-
让我们来拟合模型:
In [5]: reg.fit(X, y)
-
特征的重要性可以通过
reg.feature_importances_
查看。我们按重要性递减顺序对其进行排序:In [6]: fet_ind = np.argsort(reg.feature_importances_) \ [::-1] fet_imp = reg.feature_importances_[fet_ind]
-
最后,我们绘制特征重要性的直方图:
In [7]: ax = plt.subplot(111) plt.bar(np.arange(len(fet_imp)), fet_imp, width=1, lw=2) plt.grid(False) ax.set_xticks(np.arange(len(fet_imp))+.5) ax.set_xticklabels(data['feature_names'][fet_ind]) plt.xlim(0, len(fet_imp))
我们发现 LSTAT(低社会经济地位人口比例)和 RM(每套住房的房间数)是决定房价的最重要特征。作为说明,这里是房价与 LSTAT 的散点图:
In [8]: plt.scatter(X[:,-1], y) plt.xlabel('LSTAT indicator') plt.ylabel('Value of houses (k$)')
工作原理...
训练决策树可以使用多种算法。scikit-learn 使用 CART(分类与回归树)算法。该算法利用在每个节点上产生最大信息增益的特征和阈值构建二叉树。终端节点给出输入值的预测结果。
决策树易于理解。它们还可以通过 pydot 这一 Python 包进行可视化,用于绘制图形和树形结构。当我们希望了解决策树究竟学到了什么时,这非常有用(白盒模型);每个节点上的观测条件可以通过布尔逻辑简洁地表达。
然而,决策树可能会受到过拟合的影响,特别是在树过深时,且可能不稳定。此外,尤其是在使用贪婪算法训练时,全局收敛到最优模型并没有保证。这些问题可以通过使用决策树集成方法来缓解,特别是随机森林。
在随机森林中,多个决策树在训练数据集的自助样本上进行训练(通过有放回的随机抽样)。预测结果通过各个树的预测结果平均值来得出(自助聚合或袋装法)。此外,在每个节点上会随机选择特征的子集(随机子空间方法)。这些方法能使得整体模型优于单棵树。
还有更多...
以下是一些参考资料:
-
scikit-learn 文档中的集成学习,链接:
scikit-learn.org/stable/modules/ensemble.html
-
RandomForestRegressor
的 API 参考,链接:scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestRegressor.html
-
维基百科上的随机森林,链接:
en.wikipedia.org/wiki/Random_forest
-
维基百科上的决策树学习,链接:
en.wikipedia.org/wiki/Decision_tree_learning
-
维基百科上的自助法聚合,链接:
en.wikipedia.org/wiki/Bootstrap_aggregating
-
维基百科上的随机子空间方法,链接:
en.wikipedia.org/wiki/Random_subspace_method
-
维基百科上的集成学习,链接:
en.wikipedia.org/wiki/Ensemble_learning
另见
- 使用支持向量机进行分类任务的食谱
通过主成分分析(PCA)来减少数据集的维度
在之前的食谱中,我们介绍了有监督学习方法;我们的数据点带有离散或连续的标签,算法能够学习从点到标签的映射关系。
从这个食谱开始,我们将介绍无监督学习方法。这些方法在运行有监督学习算法之前可能会有所帮助,能够为数据提供初步的洞察。
假设我们的数据由没有标签的点x[i]组成。目标是发现这些点集合中的某种隐藏结构。数据点通常具有内在的低维性:少量特征就足以准确描述数据。然而,这些特征可能被许多与问题无关的特征所掩盖。降维可以帮助我们找到这些结构。这些知识可以显著提升后续有监督学习算法的性能。
无监督学习的另一个有用应用是数据可视化;高维数据集很难在二维或三维中进行可视化。将数据点投影到子空间或子流形上,可以得到更有趣的可视化效果。
在本食谱中,我们将演示一种基本的无监督线性方法——主成分分析(PCA)。该算法可以让我们将数据点线性地投影到低维子空间上。在主成分方向上,这些向量构成了该低维子空间的基,数据点的方差最大。
我们将使用经典的鸢尾花数据集作为示例。这个数据集包含了 150 朵鸢尾花的花瓣和萼片的宽度和长度。这些花属于三种类别之一:Iris setosa、Iris virginica和Iris versicolor。我们可以在这个数据集中获取到类别信息(有标签数据)。然而,由于我们感兴趣的是展示一种无监督学习方法,我们将只使用不带标签的数据矩阵。
如何操作...
-
我们导入 NumPy、matplotlib 和 scikit-learn:
In [1]: import numpy as np import sklearn import sklearn.decomposition as dec import sklearn.datasets as ds import matplotlib.pyplot as plt %matplotlib inline
-
鸢尾花数据集可以在 scikit-learn 的
datasets
模块中找到:In [2]: iris = ds.load_iris() X = iris.data y = iris.target print(X.shape) (150L, 4L)
-
每一行包含与花朵形态学相关的四个参数。让我们展示前两个维度。颜色反映了花朵的鸢尾花种类(标签,介于 0 和 2 之间):
In [3]: plt.scatter(X[:,0], X[:,1], c=y, s=30, cmap=plt.cm.rainbow)
提示
如果你正在阅读这本书的印刷版,可能无法区分颜色。你可以在本书的网站上找到彩色图像。
-
我们现在对数据集应用 PCA 以获得变换后的矩阵。这个操作可以通过 scikit-learn 中的一行代码完成:我们实例化一个
PCA
模型并调用fit_transform()
方法。这个函数计算主成分并将数据投影到这些主成分上:In [4]: X_bis = dec.PCA().fit_transform(X)
-
我们现在展示相同的数据集,但采用新的坐标系统(或者等效地,是初始数据集的线性变换版本):
In [5]: plt.scatter(X_bis[:,0], X_bis[:,1], c=y, s=30, cmap=plt.cm.rainbow)
属于同一类别的点现在被分到了一起,即使
PCA
估计器没有使用标签。PCA 能够找到一个最大化方差的投影,这里对应的是一个类别分得很好的投影。 -
scikit.decomposition
模块包含了经典PCA
估计器的几个变体:ProbabilisticPCA
、SparsePCA
、RandomizedPCA
、KernelPCA
等。作为示例,让我们来看一下KernelPCA
,PCA 的非线性版本:In [6]: X_ter = dec.KernelPCA(kernel='rbf'). \ fit_transform(X) plt.scatter(X_ter[:,0], X_ter[:,1], c=y, s=30, cmap=plt.cm.rainbow)
它是如何工作的...
让我们来看看 PCA 背后的数学思想。这种方法基于一种叫做奇异值分解(SVD)的矩阵分解:
这里,X是(N,D)的数据矩阵,U和V是正交矩阵,而是一个(N,D)的对角矩阵。
PCA 将X转换为由以下定义的X':
的对角线元素是X的奇异值。根据惯例,它们通常按降序排列。U的列是称为X的左奇异向量的正交归一化向量。因此,X'的列是左奇异向量乘以奇异值。
最终,PCA 将初始的一组观测值(这些观测值可能包含相关变量)转换为一组线性无关的变量,称为主成分。
第一个新特性(或第一个主成分)是将所有原始特征进行转换,使得数据点的离散度(方差)在该方向上最大。在随后的主成分中,方差逐渐减小。换句话说,PCA 为我们提供了一种数据的新表示,其中新特征按照它们对数据点变异性的解释程度进行排序。
还有更多...
这里有一些进一步的参考:
-
维基百科上的鸢尾花数据集,访问
en.wikipedia.org/wiki/Iris_flower_data_set
-
维基百科上的 PCA,访问
en.wikipedia.org/wiki/Principal_component_analysis
-
维基百科上的 SVD 分解,访问
en.wikipedia.org/wiki/Singular_value_decomposition
-
鸢尾花数据集示例,访问
scikit-learn.org/stable/auto_examples/datasets/plot_iris_dataset.html
-
scikit-learn 文档中的分解方法,访问
scikit-learn.org/stable/modules/decomposition.html
-
使用 scikit-learn 的无监督学习教程,访问
scikit-learn.org/dev/tutorial/statistical_inference/unsupervised_learning.html
另见
- 使用聚类检测数据集中的隐藏结构的配方
使用聚类检测数据集中的隐藏结构
无监督学习的一个重要部分是聚类问题。其目标是以完全无监督的方式将相似的点聚集在一起。聚类是一个困难的问题,因为簇(或组)的定义并不总是明确的。在大多数数据集中,声称两个点应该属于同一簇可能是依赖于上下文,甚至可能是主观的。
聚类算法有很多种。我们将在这个配方中看到其中的几个,并将它们应用于一个玩具示例。
如何操作...
-
让我们导入库:
In [1]: from itertools import permutations import numpy as np import sklearn import sklearn.decomposition as dec import sklearn.cluster as clu import sklearn.datasets as ds import sklearn.grid_search as gs import matplotlib.pyplot as plt %matplotlib inline
-
让我们生成一个包含三个簇的随机数据集:
In [2]: X, y = ds.make_blobs(n_samples=200, n_features=2, centers=3)
-
我们需要一些函数来重新标记并展示聚类算法的结果:
In [3]: def relabel(cl): """Relabel a clustering with three clusters to match the original classes.""" if np.max(cl) != 2: return cl perms = np.array(list(permutations((0, 1, 2)))) i = np.argmin([np.sum(np.abs(perm[cl] - y)) for perm in perms]) p = perms[i] return p[cl] In [4]: def display_clustering(labels, title): """Plot the data points with the cluster colors.""" # We relabel the classes when there are 3 # clusters. labels = relabel(labels) # Display the points with the true labels on # the left, and with the clustering labels on # the right. for i, (c, title) in enumerate(zip( [y, labels], ["True labels", title])): plt.subplot(121 + i) plt.scatter(X[:,0], X[:,1], c=c, s=30, linewidths=0, cmap=plt.cm.rainbow) plt.xticks([]); plt.yticks([]) plt.title(title)
-
现在,我们使用K-means算法对数据集进行聚类,这是一种经典且简单的聚类算法:
In [5]: km = clu.KMeans() km.fit(X) display_clustering(km.labels_, "KMeans")
小贴士
如果你正在阅读本书的印刷版,可能无法区分颜色。你可以在本书网站上找到彩色图片。
-
这个算法在初始化时需要知道簇的数量。然而,一般来说,我们并不一定知道数据集中簇的数量。在这里,让我们尝试使用
n_clusters=3
(这算是作弊,因为我们恰好知道有 3 个簇!):In [6]: km = clu.KMeans(n_clusters=3) km.fit(X) display_clustering(km.labels_, "KMeans(3)")
-
让我们尝试一些在 scikit-learn 中实现的其他聚类算法。由于 API 非常简洁,尝试不同的方法变得非常容易;只需要改变类名即可:
In [7]: plt.subplot(231) plt.scatter(X[:,0], X[:,1], c=y, s=30, linewidths=0, cmap=plt.cm.rainbow) plt.xticks([]); plt.yticks([]) plt.title("True labels") for i, est in enumerate([clu.SpectralClustering(3), clu.AgglomerativeClustering(3), clu.MeanShift(), clu.AffinityPropagation(), clu.DBSCAN()]): est.fit(X) c = relabel(est.labels_) plt.subplot(232 + i) plt.scatter(X[:,0], X[:,1], c=c, s=30, linewidths=0, cmap=plt.cm.rainbow) plt.xticks([]); plt.yticks([]) plt.title(est.__class__.__name__)
前两个算法需要输入簇的数量。接下来的两个算法不需要,但它们能够找到正确的簇数:3。最后一个算法没有找到正确的簇数(这就是过度聚类——发现了过多的簇)。
它是如何工作的...
K-means 聚类算法通过将数据点 x[j] 分配到 K 个簇 S[i] 中,从而最小化簇内平方和:
在这里, 是簇 i 的中心(即 S[i] 中所有点的平均值)。
尽管精确地解决这个问题非常困难,但存在一些近似算法。一个流行的算法是 Lloyd's 算法。它的基本过程是从一组初始的 K 均值出发 ,并交替进行两个步骤:
-
在分配步骤中,数据点会被分配到与最近均值相关的簇
-
在更新步骤中,从最后的分配中重新计算均值
该算法收敛到一个解,但并不能保证是最优解。
期望最大化算法可以看作是 K-means 算法的概率版本。它在 scikit-learn 的 mixture
模块中实现。
本方案中使用的其他聚类算法在 scikit-learn 文档中有说明。没有哪种聚类算法在所有情况下都能表现得比其他算法更好,每种算法都有其优缺点。你可以在下一节的参考资料中找到更多细节。
还有更多内容...
以下是一些参考文献:
-
维基百科上的 K-means 聚类算法,链接为
en.wikipedia.org/wiki/K-means_clustering
-
维基百科上的期望最大化算法,链接为
en.wikipedia.org/wiki/Expectation-maximization_algorithm
-
scikit-learn 文档中的聚类内容,可在
scikit-learn.org/stable/modules/clustering.html
查看
另请参见
- 使用主成分分析(PCA)降维数据集的方案
第九章:数值优化
在本章中,我们将讨论以下主题:
-
寻找数学函数的根
-
最小化数学函数
-
使用非线性最小二乘法拟合数据
-
通过最小化潜在能量找到物理系统的平衡状态
引言
数学优化是应用数学的一个广泛领域,它涉及到寻找给定问题的最佳解。许多现实世界的问题可以用优化框架来表示。比如,从 A 点到 B 点的最短路径是什么?解决一个难题的最佳策略是什么?汽车的最节能形状是什么(汽车空气动力学)?数学优化在许多领域都有应用,包括工程学、经济学、金融学、运筹学、图像处理、数据分析等。
从数学角度看,优化问题通常包括找到一个函数的最大值或最小值。根据函数变量是实值的还是离散的,我们有时会使用连续优化或离散优化这两个术语。
在本章中,我们将重点讨论解决连续优化问题的数值方法。许多优化算法都在scipy.optimize
模块中实现。我们将在本书的其他几章中遇到其他类型的优化问题。例如,我们将在第十四章中看到离散优化问题,图形、几何学与地理信息系统。在本引言中,我们将介绍一些与数学优化相关的重要定义和关键概念。
目标函数
我们将研究找到实值函数f的根或极值的方法,称为目标函数。极值可以是函数的最大值或最小值。这个数学函数通常在 Python 函数中实现。它可以接受一个或多个变量,可以是连续的或不连续的,等等。我们对函数的假设越多,优化起来就越容易。
注意
f的最大值即为-f的最小值,因此任何最小化算法都可以通过考虑该函数的对立面来实现函数的最大化。因此,从现在开始,当我们谈论最小化时,实际上指的是最小化或最大化。
凸函数通常比非凸函数更容易优化,因为它们满足某些有用的性质。例如,任何局部最小值必然是全局最小值。凸优化领域处理的是专门用于优化凸函数在凸领域上算法的研究。凸优化是一个高级主题,我们在这里不能深入讨论。
可微函数 具有梯度,这些梯度在优化算法中尤为有用。同样,连续函数 通常比非连续函数更容易优化。
此外,单变量函数比多变量函数更容易优化。
选择最合适的优化算法取决于目标函数所满足的属性。
局部和全局最小值
函数 f 的最小值是一个点 x[0],满足 f(x) f(x[0]**),对于 E 中的特定点集 x。当这个不等式在整个 E 集合上满足时,我们称 x[0] 为 全局最小值。当仅在局部(围绕点 x[0])满足时,我们称 x[0] 为 局部最小值。最大值 的定义类似。
如果 f 可微,则极值 x[0] 满足:
因此,寻找目标函数的极值与寻找导数的根密切相关。然而,满足此性质的点 x[0] 不一定是极值。
找到全局最小值比找到局部最小值更难。通常,当一个算法找到局部最小值时,并不能保证它也是全局最小值。经常有算法在寻找全局最小值时会被 卡住 在局部最小值。这一问题需要特别在全局最小化算法中考虑。然而,对于凸函数,情况较为简单,因为这些函数没有严格的局部最小值。此外,很多情况下找到局部最小值已经足够好(例如,在寻找一个问题的良好解决方案,而不是绝对最优解时)。最后,需要注意的是,全局最小值或最大值不一定存在(函数可能趋向无穷大)。在这种情况下,可能需要约束搜索空间;这就是 约束优化 的主题。
局部和全局极值
约束与无约束优化
-
无约束优化:在函数 f 定义的整个集合 E 上寻找最小值
-
约束优化:在 E 的子集 E' 上寻找函数 f 的最小值;该集合通常通过等式和不等式来描述:
这里,g[i] 和 h[j] 是定义约束的任意函数。
例如,优化汽车的空气动力学形状需要对汽车的体积、质量、生产过程的成本等参数进行约束。
约束优化通常比无约束优化更难。
确定性算法和随机算法
一些全局优化算法是 确定性的,而其他则是 随机的。通常,确定性方法适用于表现良好的函数,而随机方法则可能适用于高度不规则和嘈杂的函数。
原因在于确定性算法可能会陷入局部最小值,尤其是在存在多个非全局局部最小值的情况下。通过花时间探索空间 E,随机算法有可能找到全局最小值。
参考文献
-
SciPy 讲义是一个关于使用 SciPy 进行数学优化的极好参考,访问
scipy-lectures.github.io/advanced/mathematical_optimization/index.html
-
scipy.optimize
的参考手册,访问docs.scipy.org/doc/scipy/reference/optimize.html
-
维基百科上的数学优化概述,访问
en.wikipedia.org/wiki/Mathematical_optimization
-
维基百科上的极值、最小值和最大值,访问
en.wikipedia.org/wiki/Maxima_and_minima
-
维基百科上的凸优化,访问
en.wikipedia.org/wiki/Convex_optimization
-
Gabriel Peyré 提供的图像处理高级优化方法,访问
github.com/gpeyre/numerical-tours
寻找数学函数的根
在这个简短的教程中,我们将展示如何使用 SciPy 寻找单一实数变量的简单数学函数的根。
如何做…
-
让我们导入 NumPy、SciPy、
scipy.optimize
和 matplotlib:In [1]: import numpy as np import scipy as sp import scipy.optimize as opt import matplotlib.pyplot as plt %matplotlib inline
-
我们在 Python 中定义数学函数 f(x)=cos(x)-x,并尝试通过数值方法寻找该函数的根。这里,根对应于余弦函数的固定点:
In [2]: f = lambda x: np.cos(x) - x
-
让我们在区间 [-5, 5] 上绘制该函数(使用 1000 个样本):
In [3]: x = np.linspace(-5, 5, 1000) y = f(x) plt.plot(x, y) plt.axhline(0, color='k') plt.xlim(-5,5)
-
我们看到该函数在此区间内有唯一根(这是因为函数在该区间内的符号发生了变化)。
scipy.optimize
模块包含几个根寻找函数,这里进行了相应的适配。例如,bisect()
函数实现了 二分法(也称为 二分法法)。它的输入是要寻找根的函数和区间:In [4]: opt.bisect(f, -5, 5) Out[4]: 0.7390851332155535
让我们在图表上可视化根的位置:
In [5]: plt.plot(x, y) plt.axhline(0, color='k') plt.scatter([_], [0], c='r', s=100) plt.xlim(-5,5)
-
更快且更强大的方法是
brentq()
(布伦特法)。该算法同样要求 f 为连续函数,并且 f(a) 与 f(b) 具有不同的符号:In [6]: opt.brentq(f, -5, 5) Out[6]: 0.7390851332151607
brentq()
方法比bisect()
更快。如果条件满足,首先尝试布伦特法是一个好主意:In [7]: %timeit opt.bisect(f, -5, 5) %timeit opt.brentq(f, -5, 5) 1000 loops, best of 3: 331 µs per loop 10000 loops, best of 3: 71 µs per loop
它是如何工作的…
二分法通过反复将区间一分为二,选择一个必定包含根的子区间来进行。该方法基于这样一个事实:如果 f 是一个单一实变量的连续函数,且 f(a)>0 且 f(b)<0,则 f 在 (a,b) 区间内必有根(中值定理)。
Brent 方法 是一种流行的混合算法,结合了根的括起来、区间二分法和反向二次插值。它是一个默认方法,在许多情况下都能工作。
让我们也提一下牛顿法。其基本思想是通过切线近似 f(x)(由 f'(x) 求得),然后找到与 y=0 线的交点。如果 f 足够规则,那么交点会更接近 f 的实际根。通过反复执行此操作,算法通常会收敛到所寻求的解。
还有更多……
下面是一些参考文献:
-
scipy.optimize
的文档,地址为docs.scipy.org/doc/scipy/reference/optimize.html#root-finding
-
一个关于 SciPy 根查找的课程,地址为
quant-econ.net/scipy.html#roots-and-fixed-points
-
二分法的维基百科页面,地址为
en.wikipedia.org/wiki/Bisection_method
-
中值定理的维基百科页面,地址为
en.wikipedia.org/wiki/Intermediate_value_theorem
-
Brent 方法的维基百科页面,地址为
en.wikipedia.org/wiki/Brent%27s_method
-
牛顿法的维基百科页面,地址为
en.wikipedia.org/wiki/Newton%27s_method
另见
- 最小化数学函数 的教程
最小化数学函数
数学优化主要涉及寻找数学函数的最小值或最大值的问题。现实世界中的许多数值问题可以表达为函数最小化问题。这类问题可以在统计推断、机器学习、图论等领域中找到。
尽管有许多函数最小化算法,但并没有一个通用且普适的方法。因此,理解现有算法类别之间的差异、它们的特点以及各自的使用场景非常重要。我们还应该对问题和目标函数有清晰的了解;它是连续的、可微的、凸的、多维的、规则的,还是有噪声的?我们的优化问题是约束的还是无约束的?我们是在寻找局部最小值还是全局最小值?
在本教程中,我们将演示在 SciPy 中实现的几种函数最小化算法的使用示例。
如何实现……
-
我们导入库:
In [1]: import numpy as np import scipy as sp import scipy.optimize as opt import matplotlib.pyplot as plt %matplotlib inline
-
首先,我们定义一个简单的数学函数(基准正弦的反函数)。这个函数有许多局部最小值,但只有一个全局最小值(
en.wikipedia.org/wiki/Sinc_function
):In [2]: f = lambda x: 1-np.sin(x)/x
-
让我们在区间[-20, 20]上绘制这个函数(使用 1000 个样本):
In [3]: x = np.linspace(-20., 20., 1000) y = f(x) In [4]: plt.plot(x, y)
-
scipy.optimize
模块包含许多函数最小化的例程。minimize()
函数提供了一个统一的接口,适用于多种算法。Broyden–Fletcher–Goldfarb–Shanno(BFGS)算法(minimize()
中的默认算法)通常能给出良好的结果。minimize()
函数需要一个初始点作为参数。对于标量一元函数,我们还可以使用minimize_scalar()
:In [5]: x0 = 3 xmin = opt.minimize(f, x0).x
从x[0]**=3开始,算法能够找到实际的全局最小值,如下图所示:
In [6]: plt.plot(x, y) plt.scatter(x0, f(x0), marker='o', s=300) plt.scatter(xmin, f(xmin), marker='v', s=300) plt.xlim(-20, 20)
-
现在,如果我们从一个更远离实际全局最小值的初始点开始,算法只会收敛到一个局部最小值:
In [7]: x0 = 10 xmin = opt.minimize(f, x0).x In [8]: plt.plot(x, y) plt.scatter(x0, f(x0), marker='o', s=300) plt.scatter(xmin, f(xmin), marker='v', s=300) plt.xlim(-20, 20)
-
像大多数函数最小化算法一样,BFGS 算法在找到局部最小值时效率很高,但不一定能找到全局最小值,尤其是在复杂或嘈杂的目标函数上。克服这个问题的一个通用策略是将此类算法与初始点的探索性网格搜索相结合。另一个选择是使用基于启发式和随机方法的不同类型算法。一个流行的例子是模拟退火方法:
In [9]: xmin = opt.minimize(f, x0, method='Anneal').x In [10]: plt.plot(x, y) plt.scatter(x0, f(x0), marker='o', s=300) plt.scatter(xmin, f(xmin), marker='v', s=300) plt.xlim(-20, 20)
这次,算法成功找到了全局最小值。
-
现在,让我们定义一个新的函数,这次是二维的,称为Lévi 函数:
这个函数非常不规则,通常可能难以最小化。它是许多优化测试函数之一,研究人员为研究和基准测试优化算法而开发的(
en.wikipedia.org/wiki/Test_functions_for_optimization
):In [11]: def g(X): # X is a 2*N matrix, each column contains # x and y coordinates. x, y = X return (np.sin(3*np.pi*x)**2 + (x-1)**2 * (1+np.sin(3*np.pi*y)**2) + (y-1)**2 * (1+np.sin(2*np.pi*y)**2))
-
让我们使用
imshow()
在正方形区域[-10,10]²上显示这个函数:In [12]: n = 200 k = 10 X, Y = np.mgrid[-k:k:n*1j,-k:k:n*1j] In [13]: Z = g(np.vstack((X.ravel(), Y.ravel()))).reshape(n,n) In [14]: # We use a logarithmic scale for the color here. plt.imshow(np.log(Z), cmap=plt.cm.hot_r) plt.xticks([]); plt.yticks([])
-
BFGS 算法也适用于多维:
In [15]: x0, y0 = opt.minimize(g, (8, 3)).x In [16]: plt.imshow(np.log(Z), cmap=plt.cm.hot_r, extent=(-k, k, -k, k), origin=0) plt.scatter([x0], [y0], c=['r'], s=100) plt.xticks([]); plt.yticks([])
它是如何工作的…
许多函数最小化算法基于梯度下降的基本思想。如果一个函数f是可微的,那么在每个点,其梯度的相反方向指向函数下降速率最大的方向。沿着这个方向前进,我们可以预期找到一个局部最小值。
这个操作通常是通过迭代进行的,沿着梯度方向以小步长进行。这个步长的计算方法取决于优化方法。
牛顿法也可以在函数最小化的上下文中使用。其思路是利用牛顿法寻找 f' 的根,从而利用 二阶 导数 f''。换句话说,我们用一个 二次 函数来逼近 f,而不是用 线性 函数。在多维情况下,通过计算 f 的 Hessian(二阶导数),我们可以实现这一过程。通过迭代执行此操作,我们期望算法能够收敛到局部最小值。
当计算 Hessian 矩阵的代价过高时,我们可以计算 Hessian 的近似值。此类方法称为 准牛顿法。BFGS 算法属于这一类算法。
这些算法利用目标函数的梯度。如果我们能够计算梯度的解析表达式,应当将其提供给最小化程序。否则,算法将计算一个近似的梯度,可能不可靠。
模拟退火算法是一种通用的概率元启发式算法,用于全局优化问题。它基于热力学系统的类比:通过升高和降低温度,系统的配置可能会收敛到一个低能量状态。
有许多基于元启发式算法的随机全局优化方法。它们通常没有之前描述的确定性优化算法那样理论扎实,并且并不总是能保证收敛。然而,在目标函数非常不规则且含有许多局部最小值的情况下,这些方法可能会非常有用。协方差矩阵适应进化策略(CMA-ES)算法是一种在许多情况下表现良好的元启发式算法。它目前在 SciPy 中没有实现,但在稍后给出的参考文献中有 Python 实现。
SciPy 的 minimize()
函数接受一个 method
关键字参数,用于指定要使用的最小化算法。该函数返回一个包含优化结果的对象。x
属性是达到最小值的点。
还有更多内容……
下面是一些进一步的参考资料:
-
scipy.optimize
参考文档,链接地址:docs.scipy.org/doc/scipy/reference/optimize.html
-
一堂关于使用 SciPy 进行数学优化的精彩讲座,链接地址:
scipy-lectures.github.io/advanced/mathematical_optimization/
-
维基百科上关于梯度的定义,链接地址:
en.wikipedia.org/wiki/Gradient
-
维基百科上的牛顿法,链接地址:
en.wikipedia.org/wiki/Newton%27s_method_in_optimization
-
维基百科上的准牛顿法,链接地址:
en.wikipedia.org/wiki/Quasi-Newton_method
-
维基百科上的函数最小化元启发式方法,链接:
en.wikipedia.org/wiki/Metaheuristic
-
维基百科上的模拟退火,链接:
en.wikipedia.org/wiki/Simulated_annealing
-
维基百科上的 CMA-ES 算法描述,链接:
en.wikipedia.org/wiki/CMA-ES
-
可在
www.lri.fr/~hansen/cmaes_inmatlab.html#python
获取 CMA-ES 的 Python 实现
参见其他资料
- 求解数学函数的根教程
用非线性最小二乘法拟合数据函数
在这个教程中,我们将展示数值优化应用于非线性最小二乘曲线拟合的一个例子。目标是根据多个参数拟合一个函数到数据点上。与线性最小二乘法不同,这个函数在这些参数上不必是线性的。
我们将用人工数据来演示这种方法。
如何实现…
-
让我们导入常用的库:
In [1]: import numpy as np import scipy.optimize as opt import matplotlib.pyplot as plt %matplotlib inline
-
我们定义了一个具有四个参数的逻辑斯蒂函数:
In [2]: def f(x, a, b, c, d): return a/(1 + np.exp(-c * (x-d))) + b
-
让我们定义四个随机参数:
In [3]: a, c = np.random.exponential(size=2) b, d = np.random.randn(2)
-
现在,我们通过使用 sigmoid 函数并添加一点噪声来生成随机数据点:
In [4]: n = 100 x = np.linspace(-10., 10., n) y_model = f(x, a, b, c, d) y = y_model + a * .2 * np.random.randn(n)
-
这里是数据点的图示,图中显示了用于生成数据的特定 sigmoid(用虚线黑色表示):
In [5]: plt.plot(x, y_model, '--k') plt.plot(x, y, 'o')
-
我们现在假设我们只能访问数据点,而无法访问底层的生成函数。这些点可能是在实验中获得的。从数据来看,这些点似乎大致符合一个 sigmoid 曲线,因此我们可能希望尝试将这样的曲线拟合到这些点上。这就是曲线拟合的含义。SciPy 的
curve_fit()
函数允许我们将由任意 Python 函数定义的曲线拟合到数据上:In [6]: (a_, b_, c_, d_), _ = opt.curve_fit(f, x, y, (a, b, c, d))
-
现在,让我们来看一下拟合后的 sigmoid 曲线:
In [7]: y_fit = f(x, a_, b_, c_, d_) In [8]: plt.plot(x, y_model, '--k') plt.plot(x, y, 'o') plt.plot(x, y_fit, '-')
拟合后的 sigmoid 曲线似乎与用于数据生成的原始 sigmoid 曲线非常接近。
它是如何工作的…
在 SciPy 中,非线性最小二乘曲线拟合是通过最小化以下代价函数来实现的:
这里, 是参数的向量(在我们的示例中,
=(a,b,c,d))。
非线性最小二乘法实际上与线性回归中的线性最小二乘法非常相似。在线性最小二乘法中,函数 f 在参数上是线性的,而在这里则不是线性的。因此,S( ) 的最小化不能通过解析地解出 S 对
的导数来完成。SciPy 实现了一种称为 Levenberg-Marquardt 算法 的迭代方法(高斯-牛顿算法的扩展)。
还有更多…
这里是更多的参考资料:
-
curvefit
的参考文档,访问链接docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html
-
Wikipedia 上的非线性最小二乘法,访问链接
en.wikipedia.org/wiki/Non-linear_least_squares
-
Wikipedia 上的 Levenberg-Marquardt 算法,访问链接
en.wikipedia.org/wiki/Levenberg%E2%80%93Marquardt_algorithm
另见
- 最小化数学函数的食谱
通过最小化潜在能量找到物理系统的平衡状态
在这个例子中,我们将给出前面描述的函数最小化算法的应用实例。我们将尝试通过最小化物理系统的潜在能量来数值地寻找其平衡状态。
更具体地说,我们将考虑一个由质量和弹簧构成的结构,弹簧固定在垂直墙面上,并受到重力作用。从初始位置开始,我们将寻找重力和弹性力相互平衡的平衡配置。
如何操作…
-
让我们导入 NumPy、SciPy 和 matplotlib:
In [1]: import numpy as np import scipy.optimize as opt import matplotlib.pyplot as plt %matplotlib inline
-
我们在国际单位制中定义一些常数:
In [2]: g = 9.81 # gravity of Earth m = .1 # mass, in kg n = 20 # number of masses e = .1 # initial distance between the masses l = e # relaxed length of the springs k = 10000 # spring stiffness
-
我们定义质量的初始位置。它们排列在一个二维网格上,具有两行和n/2列:
In [3]: P0 = np.zeros((n, 2)) P0[:,0] = np.repeat(e*np.arange(n//2), 2) P0[:,1] = np.tile((0,-e), n//2)
-
现在,让我们定义质量之间的连接矩阵。系数(i,j)为 1 表示质量i和j之间通过弹簧连接,若没有连接则为 0:
In [4]: A = np.eye(n, n, 1) + np.eye(n, n, 2)
-
我们还指定了每个弹簧的刚度。它是l,除了对角线上的弹簧,其刚度为
:
In [5]: L = l * (np.eye(n, n, 1) + np.eye(n, n, 2)) for i in range(n//2-1): L[2*i+1,2*i+2] *= np.sqrt(2)
-
我们获取弹簧连接的索引:
In [6]: I, J = np.nonzero(A)
-
这个
dist
函数计算距离矩阵(任何一对质量之间的距离):In [7]: dist = lambda P: np.sqrt( (P[:,0]-P[:,0][:, np.newaxis])**2 + (P[:,1]-P[:,1][:, np.newaxis])**2)
-
我们定义一个函数来显示系统。弹簧根据其张力着色:
In [8]: def show_bar(P): # Wall. plt.axvline(0, color='k', lw=3) # Distance matrix. D = dist(P) # We plot the springs. for i, j in zip(I, J): # The color depends on the spring tension, # which is proportional to the spring # elongation. c = D[i,j] - L[i,j] plt.plot(P[[i,j],0], P[[i,j],1], lw=2, color=plt.cm.copper(c*150)) # We plot the masses. plt.plot(P[[I,J],0], P[[I,J],1], 'ok',) # We configure the axes. plt.axis('equal') plt.xlim(P[:,0].min()-e/2, P[:,0].max()+e/2) plt.ylim(P[:,1].min()-e/2, P[:,1].max()+e/2) plt.xticks([]); plt.yticks([])
-
这里是系统的初始配置:
In [9]: show_bar(P0) plt.title("Initial configuration")
-
要找到平衡状态,我们需要最小化系统的总潜在能量。以下函数根据质量的位置计算系统的能量。此函数在如何工作…部分中进行了说明:
In [10]: def energy(P): # The argument P is a vector (flattened # matrix). We convert it to a matrix here. P = P.reshape((-1, 2)) # We compute the distance matrix. D = dist(P) # The potential energy is the sum of the # gravitational and elastic potential # energies. return (g * m * P[:,1].sum() + .5 * (k * A * (D - L)**2).sum())
-
让我们计算初始配置的潜在能量:
In [11]: energy(P0.ravel()) Out[11]: -0.98099
-
现在,让我们使用函数最小化方法来最小化潜在能量。我们需要一个约束优化算法,因为我们假设前两个质量被固定在墙壁上。因此,它们的位置不能改变。L-BFGS-B算法是 BFGS 算法的一个变种,支持边界约束。在这里,我们强制前两个点保持在初始位置,而其他点没有约束。
minimize()
函数接受一个包含每个维度的[min, max]值对的bounds
列表:In [12]: bounds = np.c_[P0[:2,:].ravel(), P0[:2,:].ravel()].tolist() + \ [[None, None]] * (2*(n-2)) In [13]: P1 = opt.minimize(energy, P0.ravel(), method='L-BFGS-B', bounds=bounds).x \ .reshape((-1, 2))
-
让我们显示稳定的配置:
In [14]: show_bar(P1) plt.title("Equilibrium configuration")
这个配置看起来很逼真。张力似乎在靠近墙壁的顶部弹簧上达到了最大值。
它是如何工作的……
这个例子在概念上很简单。系统的状态仅由质量的位置描述。如果我们能写出一个返回系统总能量的 Python 函数,那么找到平衡状态就只需要最小化这个函数。这就是最小总势能原理,源于热力学第二定律。
这里,我们给出了系统总能量的表达式。由于我们只关心平衡状态,我们省略了任何动能方面的内容,只考虑由重力(重力作用)和弹簧力(弹性势能)引起的势能。
令 U 为系统的总势能,U 可以表示为质量的重力势能与弹簧的弹性势能之和。因此:
这里:
-
m 是质量
-
g 是地球的重力
-
k 是弹簧的刚度
-
p[i] = (x[i], y[i]) 是质量 i 的位置
-
a[ij] 如果质量 i 和 j 通过弹簧连接,则为 1,否则为 0
-
l[ij] 是弹簧 (i,j) 的放松长度,如果质量 i 和 j 没有连接,则为 0
energy()
函数使用 NumPy 数组上的向量化计算来实现这个公式。
还有更多……
以下参考资料包含关于这个公式背后物理学的详细信息:
-
维基百科上的势能,详情请见
en.wikipedia.org/wiki/Potential_energy
-
维基百科上的弹性势能,详情请见
en.wikipedia.org/wiki/Elastic_potential_energy
-
胡克定律是弹簧响应的线性近似,详情请见
en.wikipedia.org/wiki/Hooke%27s_law
-
维基百科上的最小能量原理,详情请见
en.wikipedia.org/wiki/Minimum_total_potential_energy_principle
这是关于优化算法的参考资料:
- 维基百科上的 L-BFGS-B 算法,详情请见
en.wikipedia.org/wiki/Limited-memory_BFGS#L-BFGS-B
另见
- 最小化数学函数的操作步骤
第十章:信号处理
在本章中,我们将涵盖以下主题:
-
使用快速傅里叶变换分析信号的频率成分
-
应用线性滤波器于数字信号
-
计算时间序列的自相关
引言
信号是描述一个量在时间或空间上变化的数学函数。依赖于时间的信号通常被称为时间序列。时间序列的例子包括股价,它们通常呈现为在均匀时间间隔内的连续时间点。在物理学或生物学中,实验设备记录如电磁波或生物过程等变量的演变。
在信号处理中,一般目标是从原始的、有噪声的测量数据中提取有意义和相关的信息。信号处理的主题包括信号获取、变换、压缩、滤波和特征提取等。当处理复杂数据集时,先清理数据可能对应用更先进的数学分析方法(例如机器学习)有帮助。
在本简短的章节中,我们将阐明并解释信号处理的主要基础。在下一章,第十一章中,我们将看到针对图像和声音的特定信号处理方法。
首先,在本引言中我们将给出一些重要的定义。
模拟信号与数字信号
信号可以是时间依赖的或空间依赖的。在本章中,我们将专注于时间依赖的信号。
设x(t)为时变信号。我们可以说:
-
如果t是连续变量,且x(t)是实数,则该信号为模拟信号
-
如果t是离散变量(离散时间信号),且x(t)只能取有限个值(量化信号),则该信号为数字信号
下图展示了模拟信号(连续曲线)与数字信号(点)的区别:
模拟信号与数字(量化)信号之间的区别
模拟信号存在于数学中以及大多数物理系统中,如电路。然而,由于计算机是离散的机器,它们只能理解数字信号。这就是计算科学特别处理数字信号的原因。
由实验设备记录的数字信号通常由两个重要的量来描述:
-
采样率:每秒记录的值(或样本)数量(以赫兹为单位)
-
分辨率:量化的精度,通常以每个样本的位数(也称为位深度)表示
具有高采样率和位深度的数字信号更为精确,但它们需要更多的内存和处理能力。这两个参数受到记录信号的实验设备的限制。
奈奎斯特–香农采样定理
假设我们考虑一个连续(模拟)时变信号x(t)。我们使用实验设备记录这个物理信号,并以采样率f[s]获取数字信号。由于原始模拟信号具有无限精度,而记录的信号具有有限精度,我们预计在模拟到数字的过程中会丢失信息。
奈奎斯特-香农采样定理指出,在某些条件下,模拟信号和采样率可以确保采样过程中不丢失任何信息。换句话说,在这些条件下,我们可以从采样的数字信号中恢复原始的连续信号。更多细节,请参见en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem
。
让我们定义这些条件。傅里叶变换 是通过以下公式定义的:
在这里,傅里叶变换是时间依赖信号在频域中的表示。奈奎斯特准则指出:
换句话说,信号必须是带限的,这意味着它不能包含任何高于某个截止频率B的频率。此外,采样率f[s]需要至少是该频率B的两倍。以下是几个定义:
-
奈奎斯特率是2B。对于给定的带限模拟信号,它是无损采样该信号所需的最小采样率。
-
奈奎斯特频率是f[s]/2。对于给定的采样率,它是信号可以包含的最大频率,以便无损采样。
在这些条件下,我们理论上可以从采样的数字信号中重建原始的模拟信号。
压缩感知
压缩感知是一种现代且重要的信号处理方法。它承认许多现实世界的信号本质上是低维的。例如,语音信号具有非常特定的结构,取决于人类发音道的普遍物理约束。
即使一个语音信号在傅里叶域中有许多频率,它也可以通过在合适的基(字典)上进行稀疏分解来很好地逼近。根据定义,稀疏分解是指大多数系数为零。如果字典选择得当,每个信号都是少数基信号的组合。
这本词典包含了特定于给定问题中考虑的信号的基本信号。这与将信号分解为通用正弦函数基础的傅里叶变换不同。换句话说,通过稀疏表示,可以规避奈奎斯特条件。我们可以从包含比奈奎斯特条件要求的样本更少的稀疏表示中精确重建连续信号。
稀疏分解可以通过复杂的算法找到。特别是,这些问题可以转化为凸优化问题,可以用特定的数值优化方法解决。
压缩感知在信号压缩、图像处理、计算机视觉、生物医学成像以及许多其他科学和工程领域中有许多应用。
关于压缩感知的更多参考资料:
参考文献
这里有几个参考资料:
-
理解数字信号处理,作者 Richard G. Lyons,出版社 Pearson Education,(2010)。
-
要详细了解压缩感知,请参阅《信号处理的小波之旅:稀疏方法》一书,作者 Mallat Stéphane,出版社 Academic Press,(2008)。
-
《Python 信号处理》一书由 Jose Unpingco 编写,比我们在本章中涵盖的内容要详细得多。代码可以在 GitHub 上的 IPython 笔记本上找到(
python-for-signal-processing.blogspot.com
)。 -
在 WikiBooks 上提供的数字信号处理可参考数字信号处理。
用快速傅里叶变换分析信号的频率成分
在这个案例中,我们将展示如何使用快速傅里叶变换(FFT)计算信号的频谱密度。频谱表示与频率相关联的能量(编码信号中的周期波动)。它是通过傅里叶变换获得的,这是信号的频率表示。信号可以在两种表示之间来回转换而不丢失信息。
在这个案例中,我们将阐述傅里叶变换的几个方面。我们将应用这一工具于从美国国家气候数据中心获取的法国 20 年的天气数据。
准备工作
从书籍的 GitHub 存储库中下载Weather数据集(github.com/ipython-books/cookbook-data
),并将其提取到当前目录中。
数据来自www.ncdc.noaa.gov/cdo-web/datasets#GHCND。
如何做……
-
让我们导入包,包括
scipy.fftpack
,其中包括许多与 FFT 相关的例程:In [1]: import datetime import numpy as np import scipy as sp import scipy.fftpack import pandas as pd import matplotlib.pyplot as plt %matplotlib inline
-
我们从 CSV 文件中导入数据。数字
-9999
用于表示 N/A 值,pandas 可以轻松处理。此外,我们告诉 pandas 解析DATE
列中的日期:In [2]: df0 = pd.read_csv('data/weather.csv', na_values=(-9999), parse_dates=['DATE']) In [3]: df = df0[df0['DATE']>='19940101'] In [4]: df.head() Out[4]: STATION DATE PRCP TMAX TMIN 365 FR013055001 1994-01-01 00:00:00 0 104 72 366 FR013055001 1994-01-02 00:00:00 4 128 49
-
每一行包含法国一个气象站每天记录的降水量和极端温度。对于日历中的每个日期,我们想得到整个国家的单一平均温度。pandas 提供的
groupby()
方法可以轻松实现这一点。我们还使用dropna()
去除任何 N/A 值:In [5]: df_avg = df.dropna().groupby('DATE').mean() In [6]: df_avg.head() Out[6]: DATE PRCP TMAX TMIN 1994-01-01 178.666667 127.388889 70.333333 1994-01-02 122.000000 152.421053 81.736842
-
现在,我们得到日期列表和相应的温度列表。单位是十分之一度,我们计算的是最小和最大温度之间的平均值,这也解释了为什么我们要除以 20。
In [7]: date = df_avg.index.to_datetime() temp = (df_avg['TMAX'] + df_avg['TMIN']) / 20. N = len(temp)
-
让我们来看一下温度的变化:
In [8]: plt.plot_date(date, temp, '-', lw=.5) plt.ylim(-10, 40) plt.xlabel('Date') plt.ylabel('Mean temperature')
-
现在,我们计算信号的傅里叶变换和谱密度。第一步是使用
fft()
函数计算信号的 FFT:In [9]: temp_fft = sp.fftpack.fft(temp)
-
一旦得到 FFT,我们需要取其绝对值的平方来获得功率谱密度(PSD):
In [10]: temp_psd = np.abs(temp_fft) ** 2
-
下一步是获取与 PSD 值对应的频率。
fftfreq()
工具函数正是用来做这个的。它以 PSD 向量的长度和频率单位作为输入。在这里,我们选择年度单位:频率为 1 对应 1 年(365 天)。我们提供1/365,因为原始单位是天数。In [11]: fftfreq = sp.fftpack.fftfreq(len(temp_psd), 1./365)
-
fftfreq()
函数返回正频率和负频率。这里我们只关心正频率,因为我们有一个真实信号(这将在本配方的它是如何工作的...部分中解释)。In [12]: i = fftfreq>0
-
现在我们绘制信号的功率谱密度,作为频率的函数(单位为1 年)。我们选择对y轴使用对数尺度(分贝)。
In [13]: plt.plot(fftfreq[i], 10*np.log10(temp_psd[i])) plt.xlim(0, 5) plt.xlabel('Frequency (1/year)') plt.ylabel('PSD (dB)')
因为信号的基频是温度的年度变化,我们观察到在f=1时有一个峰值。
-
现在,我们去除高于基频的频率:
In [14]: temp_fft_bis = temp_fft.copy() temp_fft_bis[np.abs(fftfreq) > 1.1] = 0
-
接下来,我们执行逆 FFT将修改后的傅里叶变换转换回时间域。这样,我们恢复了一个主要包含基频的信号,如下图所示:
In [15]: temp_slow = np.real(sp.fftpack.ifft(temp_fft_bis)) In [16]: plt.plot_date(date, temp, '-', lw=.5) plt.plot_date(date, temp_slow, '-') plt.xlim(datetime.date(1994, 1, 1), datetime.date(2000, 1, 1)) plt.ylim(-10, 40) plt.xlabel('Date') plt.ylabel('Mean temperature')
我们得到信号的平滑版本,因为当我们去除傅里叶变换中的高频时,快速变化的部分已经丢失。
它是如何工作的...
广义而言,傅立叶变换是信号的周期分量的超级位置的替代表示。这是一个重要的数学结果,任何良好行为的函数都可以用这种形式表示。而时间变化的信号最自然的考虑方式是作为时间的函数,傅立叶变换则将其表示为频率的函数。每个频率都与一个大小和相位相关联,这两者都被编码为单一复数。
离散傅立叶变换
让我们考虑一个由向量(x[0], ..., x[(N-1)])表示的数字信号x。我们假设这个信号是定期采样的。x的离散傅立叶变换(DFT)被定义为X = (X[0], ..., X[(N-1)]):
FFT 可以高效地计算离散傅立叶变换(DFT),这是一种利用该定义中的对称性和冗余性以显著加快计算速度的算法。FFT 的复杂度是O(N log N),而朴素 DFT 的复杂度是O(N²)。FFT 是数字宇宙中最重要的算法之一。
这是关于 DFT 描述的直观解释。我们不是在实线上表示我们的信号,而是在圆上表示它。我们可以通过在圆上进行 1、2 或任意数量k的圈来播放整个信号。因此,当k固定时,我们用一个角度和与原始距离相等的x[n]表示信号的每个值x[n]。
如果信号显示出某种周期性的k圈,意味着许多相关值将在该确切频率上叠加,以致系数X[k]将会很大。换句话说,第k个系数的模|X[k]|表示与该频率相关的信号的能量。
在下面的图中,信号是频率f=3 Hz的正弦波。该信号的点以蓝色显示,位于一个角度的位置。它们在复平面上的代数和以红色显示。这些向量表示信号 DFT 的不同系数。
DFT 的插图
下一张图表示前一个信号的功率谱密度(PSD):
前述示例中信号的 PSD
逆傅立叶变换
通过考虑所有可能的频率,我们在频率域中对我们的数字信号进行了精确表示。我们可以通过计算逆快速傅立叶变换来恢复初始信号,这计算了逆离散傅立叶变换。这个公式与 DFT 非常相似:
当需要寻找周期性模式时,DFT 非常有用。然而,通常来说,傅里叶变换无法检测到特定频率下的瞬态变化。此时需要更局部的谱方法,例如小波变换。
还有更多…
以下链接包含有关傅里叶变换的更多详细信息:
-
SciPy 中的 FFT 简介,链接地址为
scipy-lectures.github.io/intro/scipy.html#fast-fourier-transforms-scipy-fftpack
-
SciPy 中 fftpack 的参考文档,链接地址为
docs.scipy.org/doc/scipy/reference/fftpack.html
-
维基百科上的傅里叶变换,链接地址为
en.wikipedia.org/wiki/Fourier_transform
-
维基百科上的离散傅里叶变换,链接地址为
en.wikipedia.org/wiki/Discrete_Fourier_transform
-
维基百科上的快速傅里叶变换,链接地址为
en.wikipedia.org/wiki/Fast_Fourier_transform
-
维基百科上的分贝,链接地址为
en.wikipedia.org/wiki/Decibel
另见
-
将线性滤波器应用于数字信号方法
-
计算时间序列的自相关方法
将线性滤波器应用于数字信号
线性滤波器在信号处理中发挥着基础作用。通过线性滤波器,可以从数字信号中提取有意义的信息。
在本方法中,我们将展示两个使用股市数据(NASDAQ 股票交易所)的例子。首先,我们将使用低通滤波器平滑一个非常嘈杂的信号,以提取其慢速变化。我们还将对原始时间序列应用高通滤波器,以提取快速变化。这只是线性滤波器应用中常见的两个例子,实际应用有很多种。
做好准备
从本书的 GitHub 仓库下载纳斯达克数据集,链接地址为github.com/ipython-books/cookbook-data
,并将其解压到当前目录。
数据来自finance.yahoo.com/q/hp?s=^IXIC&a=00&b=1&c=1990&d=00&e=1&f=2014&g=d
。
如何操作…
-
让我们导入所需的包:
In [1]: import numpy as np import scipy as sp import scipy.signal as sg import pandas as pd import matplotlib.pyplot as plt %matplotlib inline
-
我们使用 pandas 加载 NASDAQ 数据:
In [2]: nasdaq_df = pd.read_csv('data/nasdaq.csv') In [3]: nasdaq_df.head() Out[3]: Date Open High Low Close 0 2013-12-31 4161.51 4177.73 4160.77 4176.59 1 2013-12-30 4153.58 4158.73 4142.18 4154.20
-
让我们提取两列数据:日期和每日收盘值:
In [4]: date = pd.to_datetime(nasdaq_df['Date']) nasdaq = nasdaq_df['Close']
-
让我们来看一下原始信号:
In [5]: plt.plot_date(date, nasdaq, '-')
-
现在,我们将采用第一种方法来获取信号的慢速变化。我们将信号与三角窗进行卷积,这相当于FIR 滤波器。我们将在本食谱的它是如何工作的...部分解释该方法背后的原理。现在,暂且说,我们将每个值替换为该值周围信号的加权平均值:
In [6]: # We get a triangular window with 60 samples. h = sg.get_window('triang', 60) # We convolve the signal with this window. fil = sg.convolve(nasdaq, h/h.sum()) In [7]: # We plot the original signal... plt.plot_date(date, nasdaq, '-', lw=1) # ... and the filtered signal. plt.plot_date(date, fil[:len(nasdaq)-1], '-')
-
现在,让我们使用另一种方法。我们创建一个 IIR 巴特沃斯低通滤波器来提取信号的慢速变化。
filtfilt()
方法允许我们前后应用滤波器,以避免相位延迟:In [8]: plt.plot_date(date, nasdaq, '-', lw=1) # We create a 4-th order Butterworth low-pass # filter. b, a = sg.butter(4, 2./365) # We apply this filter to the signal. plt.plot_date(date, sg.filtfilt(b, a, nasdaq), '-')
-
最后,我们使用相同的方法来创建一个高通滤波器,并提取信号的快速变化:
In [9]: plt.plot_date(date, nasdaq, '-', lw=1) b, a = sg.butter(4, 2*5./365, btype='high') plt.plot_date(date, sg.filtfilt(b, a, nasdaq), '-', lw=.5)
2000 年左右的快速变化对应于互联网泡沫的破裂,反映了当时股市的高波动性和股市指数的快速波动。更多细节,请参见
en.wikipedia.org/wiki/Dot-com_bubble
。
它是如何工作的...
在本节中,我们将解释在数字信号背景下线性滤波器的基本原理。
数字信号是一个离散的序列(x[n]),由n索引!它是如何工作的... * 0。尽管我们常假设信号是无限序列,但在实际应用中,信号通常由有限大小N的向量*表示。
在连续情况下,我们更倾向于操作依赖时间的函数f(t)。宽泛地说,我们可以通过离散化时间并将积分转化为求和,从连续信号转化为离散信号。
什么是线性滤波器?
线性滤波器 F 将输入信号 x = (x[n]) 转换为输出信号 y = (y[n])。这种转换是线性的——两个信号之和的转换是转换后的信号之和:F(x+y) = F(x)+F(y)。
除此之外,将输入信号乘以常数!什么是线性滤波器?会产生与将原始输出信号乘以相同常数相同的输出:。
线性时不变 (LTI) 滤波器有一个额外的性质:如果信号(x[n])被转换为(y[n]),那么移位信号(x[(n-k)])将被转换为(y[(n-k)]),对于任何固定的k。换句话说,系统是时不变的,因为输出不依赖于输入应用的具体时间。
注意
从现在开始,我们只考虑 LTI 滤波器。
线性滤波器与卷积
LTI 系统理论中的一个非常重要的结果是,LTI 滤波器可以通过一个单一的信号来描述:冲激响应h。这是滤波器对冲激信号的响应输出。对于数字滤波器,冲激信号为(1, 0, 0, 0, ...)。
可以证明,x = (x[n])通过卷积与冲激响应h及信号x变换为y = (y[n]):
卷积是信号处理中一个基本的数学运算。从直觉上讲,考虑到一个在零点附近有峰值的卷积函数,卷积相当于对信号(这里是x)进行加权的局部平均,权重由给定的窗口(这里是h)决定。
根据我们的符号,隐含着我们将自己限制在因果滤波器(h[n] = 0 当n < 0)中。这个特性意味着信号的输出仅依赖于输入的当前和过去,而不是未来。这在许多情况下是一个自然的特性。
FIR 和 IIR 滤波器
一个信号(h[n])的支持是满足!FIR 和 IIR 滤波器的n集合。LTI 滤波器可以分为两类:
-
一个有限冲激响应(FIR)滤波器具有有限支持的冲激响应
-
一个无限冲激响应(IIR)滤波器具有无限支持的冲激响应
一个 FIR 滤波器可以通过大小为N(一个向量)的有限冲激响应来描述。它通过将信号与其冲激响应进行卷积来工作。我们定义b[n] = h[n],当n满足!FIR 和 IIR 滤波器 * N时。然后,y[n]是输入信号最后N+1*个值的线性组合:
另一方面,IIR 滤波器通过具有无限冲激响应来描述,这种形式下无法精确表示。出于这个原因,我们通常使用替代表示:
这个差分方程将y[n]表示为输入信号的最后N+1个值的线性组合(前馈项,类似于 FIR 滤波器),以及输出信号的最后M个值的线性组合(反馈项)。反馈项使得 IIR 滤波器比 FIR 滤波器更复杂,因为输出不仅依赖于输入,还依赖于输出的先前值(动态性)。
频域中的滤波器
我们只描述了时域中的滤波器。其他域中的替代表示方法如拉普拉斯变换、Z 变换和傅里叶变换等也存在。
特别地,傅里叶变换具有一个非常方便的特性:它将卷积转化为频域中的乘法。换句话说,在频域中,一个 LTI 滤波器将输入信号的傅里叶变换与冲激响应的傅里叶变换相乘。
低通、高通和带通滤波器
滤波器可以通过它们对输入信号频率幅度的影响来表征。具体如下:
-
低通滤波器衰减高于截止频率的信号分量
-
高通滤波器衰减低于截止频率的信号成分,低频部分
-
带通滤波器通过特定频率范围内的信号成分,并衰减范围外的信号成分
在这个配方中,我们首先将输入信号与一个三角形窗口进行卷积(窗口有限支持)。可以证明,这个操作相当于一个低通 FIR 滤波器。这是移动平均方法的一个特殊案例,该方法通过计算每个值的局部加权平均来平滑信号。
接着,我们应用了两次巴特沃斯滤波器,这是一种特定类型的 IIR 滤波器,能够作为低通、高通或带通滤波器。在这个配方中,我们首先将其用作低通滤波器来平滑信号,接着再用作高通滤波器来提取信号中的快速变化部分。
还有更多...
这里有一些关于数字信号处理和线性滤波器的常规参考资料:
-
维基百科上的数字信号处理,详见
en.wikipedia.org/wiki/Digital_signal_processing
-
维基百科上的线性滤波器,详见
en.wikipedia.org/wiki/Linear_filter
-
维基百科上的 LTI 滤波器,详见
en.wikipedia.org/wiki/LTI_system_theory
这里有一些关于冲激响应、卷积以及 FIR/IIR 滤波器的参考资料:
-
FIR 滤波器在
en.wikipedia.org/wiki/Finite_impulse_response
中有描述 -
IIR 滤波器在
en.wikipedia.org/wiki/Infinite_impulse_response
中有描述 -
低通滤波器在
en.wikipedia.org/wiki/Low-pass_filter
中有描述 -
高通滤波器在
en.wikipedia.org/wiki/High-pass_filter
中有描述 -
带通滤波器在
en.wikipedia.org/wiki/Band-pass_filter
中有描述
另请参见
- 使用快速傅里叶变换分析信号的频率成分 配方
计算时间序列的自相关
时间序列的自相关可以帮助我们了解重复的模式或序列相关性。后者是指信号在某一时刻与稍后时刻之间的相关性。自相关分析可以告诉我们波动的时间尺度。在这里,我们利用这一工具分析美国婴儿名字的变化,数据来源于美国社会保障管理局提供的数据。
准备工作
从本书的 GitHub 仓库下载Babies数据集,链接:github.com/ipython-books/cookbook-data
,并将其解压到当前目录。
数据来自于www.data.gov(catalog.data.gov/dataset/baby-names-from-social-security-card-applications-national-level-data-6315b
)。
如何操作...
-
我们导入以下包:
In [1]: import os import numpy as np import pandas as pd import matplotlib.pyplot as plt %matplotlib inline
-
我们使用 pandas 读取数据。数据集每年包含一个 CSV 文件。每个文件包含该年所有婴儿名字及其相应的频率。我们将数据加载到一个字典中,字典中每年对应一个
DataFrame
:In [2]: files = [file for file in os.listdir('data/') if file.startswith('yob')] In [3]: years = np.array(sorted([int(file[3:7]) for file in files])) In [4]: data = {year: pd.read_csv( 'data/yob{y:d}.txt'.format(y=year), index_col=0, header=None, names=['First name', 'Gender', 'Number']) for year in years} In [5]: data[2012].head() Out[5]: Gender Number First name Sophia F 22158 Emma F 20791 Isabella F 18931 Olivia F 17147 Ava F 15418
-
我们编写函数以根据名字、性别和出生年份检索婴儿名字的频率:
In [6]: def get_value(name, gender, year): """Return the number of babies born a given year, with a given gender and a given name.""" try: return data[year] \ [data[year]['Gender'] == gender] \ ['Number'][name] except KeyError: return 0 In [7]: def get_evolution(name, gender): """Return the evolution of a baby name over the years.""" return np.array([get_value(name, gender, year) for year in years])
-
让我们定义一个计算信号自相关的函数。这个函数本质上是基于 NumPy 的
correlate()
函数。In [8]: def autocorr(x): result = np.correlate(x, x, mode='full') return result[result.size/2:]
-
现在,我们创建一个显示婴儿名字及其(归一化)自相关演变的函数:
In [9]: def autocorr_name(name, gender): x = get_evolution(name, gender) z = autocorr(x) # Evolution of the name. plt.subplot(121) plt.plot(years, x, '-o', label=name) plt.title("Baby names") # Autocorrelation. plt.subplot(122) plt.plot(z / float(z.max()), '-', label=name) plt.legend() plt.title("Autocorrelation")
-
让我们看一下两个女性名字:
In [10]: autocorr_name('Olivia', 'F') autocorr_name('Maria', 'F')
Olivia 的自相关衰减速度比 Maria 的要快得多。这主要是因为 Olivia 在 20 世纪末的急剧增加。相比之下,Maria 的变化较为缓慢,且其自相关衰减也较为缓慢。
它是如何工作的...
时间序列是按时间索引的序列。其重要应用包括股市、产品销售、天气预报、生物信号等。时间序列分析是统计数据分析、信号处理和机器学习的重要部分。
自相关有多种定义。在这里,我们将时间序列(x[n])的自相关定义为:
在之前的图表中,我们通过最大值归一化自相关,以便比较两个信号的自相关。自相关量化了信号与其自身平移版本之间的平均相似性,这一相似性是延迟时间的函数。换句话说,自相关可以为我们提供有关重复模式以及信号波动时间尺度的信息。自相关衰减至零的速度越快,信号变化的速度越快。
还有更多...
以下是一些参考资料:
-
NumPy 的相关函数文档,见
docs.scipy.org/doc/numpy/reference/generated/numpy.correlate.html
-
statsmodels 中的自相关函数,文档可见
statsmodels.sourceforge.net/stable/tsa.html
-
维基百科上的时间序列,见
en.wikipedia.org/wiki/Time_series
-
Wikipedia 上的序列依赖,详情请见
en.wikipedia.org/wiki/Serial_dependence
-
Wikipedia 上的自相关,详情请见
en.wikipedia.org/wiki/Autocorrelation
参见
- 使用快速傅里叶变换分析信号频率成分 的方法
第十一章:图像与音频处理
在本章中,我们将讨论以下主题:
-
操作图像的曝光度
-
对图像应用滤波器
-
对图像进行分割
-
在图像中找到兴趣点
-
使用 OpenCV 检测图像中的人脸
-
应用数字滤波器于语音声音
-
在笔记本中创建一个声音合成器
介绍
在前一章中,我们讨论了针对一维时间依赖信号的信号处理技术。在本章中,我们将看到针对图像和声音的信号处理技术。
通用信号处理技术可以应用于图像和声音,但许多图像或音频处理任务需要专门的算法。例如,我们将看到用于图像分割、检测图像中的兴趣点或检测人脸的算法。我们还将听到线性滤波器对语音声音的影响。
scikit-image是 Python 中的主要图像处理包之一。在本章的大多数图像处理实例中,我们将使用它。有关 scikit-image 的更多信息,请参阅scikit-image.org
。
我们还将使用OpenCV(opencv.org
),这是一个 C++计算机视觉库,具有 Python 包装器。它实现了专门的图像和视频处理任务的算法,但使用起来可能有些困难。一个有趣的(且更简单的)替代方案是SimpleCV(simplecv.org
)。
在本介绍中,我们将从信号处理的角度讨论图像和声音的特点。
图像
灰度图像是一个二维信号,由一个函数f表示,该函数将每个像素映射到一个强度。强度可以是一个在 0(暗)和 1(亮)之间的实数值。在彩色图像中,该函数将每个像素映射到强度的三元组,通常是红色、绿色和蓝色(RGB)分量。
在计算机上,图像是数字化采样的。其强度不再是实数值,而是整数或浮动点数。一方面,连续函数的数学公式使我们能够应用诸如导数和积分之类的分析工具。另一方面,我们需要考虑我们处理的图像的数字特性。
声音
从信号处理的角度来看,声音是一个时间依赖的信号,在听觉频率范围内(约 20 Hz 到 20 kHz)具有足够的功率。然后,根据奈奎斯特-香农定理(在第十章,信号处理中介绍),数字声音信号的采样率需要至少为 40 kHz。44100 Hz 的采样率是常选的采样率。
参考资料
以下是一些参考资料:
-
维基百科上的图像处理,网址:
en.wikipedia.org/wiki/Image_processing
-
由 Gabriel Peyré 撰写的高级图像处理算法,网址为
github.com/gpeyre/numerical-tours
-
维基百科上的音频信号处理,网址为
en.wikipedia.org/wiki/Audio_signal_processing
-
44100 Hz 采样率的特殊性解释在
en.wikipedia.org/wiki/44,100_Hz
操纵图像的曝光
图像的曝光告诉我们图像是太暗、太亮还是平衡的。可以通过所有像素的强度值直方图来衡量。改善图像的曝光是一项基本的图像编辑操作。正如我们将在本篇中看到的,使用 scikit-image 可以轻松实现。
准备工作
您需要 scikit-image 来完成这个步骤。您可以在scikit-image.org/download.html
找到安装说明。使用 Anaconda,您只需在终端中输入conda install scikit-image
。
您还需要从该书的 GitHub 仓库下载Beach数据集,网址为github.com/ipython-books/cookbook-data
。
操作步骤...
-
让我们导入包:
In [1]: import numpy as np import matplotlib.pyplot as plt import skimage.exposure as skie %matplotlib inline
-
我们使用 matplotlib 打开一幅图像。我们只取一个 RGB 分量,以获得灰度图像(有更好的方法将彩色图像转换为灰度图像):
In [2]: img = plt.imread('data/pic1.jpg')[...,0]
-
我们创建一个函数,显示图像及其强度值直方图(即曝光):
In [3]: def show(img): # Display the image. plt.subplot(121) plt.imshow(img, cmap=plt.cm.gray) plt.axis('off') # Display the histogram. plt.subplot(122) plt.hist(img.ravel(), lw=0, bins=256) plt.xlim(0, img.max()) plt.yticks([]) plt.show()
-
让我们显示图像及其直方图:
In [4]: show(img)
一幅图像及其直方图
直方图不平衡,图像看起来过曝(许多像素太亮)。
-
现在,我们使用 scikit-image 的
rescale_intensity
函数重新调整图像的强度。in_range
和out_range
参数定义了从原始图像到修改后图像的线性映射。超出in_range
范围的像素被剪切到out_range
的极值。在这里,最暗的像素(强度小于 100)变为完全黑色(0),而最亮的像素(>240)变为完全白色(255):In [5]: show(skie.rescale_intensity(img, in_range=(100, 240), out_range=(0, 255)))
一个粗糙的曝光操作技术
直方图中似乎缺少许多强度值,这反映了这种基本曝光校正技术的质量较差。
-
现在我们使用一种更高级的曝光校正技术,称为对比有限自适应直方图均衡化(CLAHE):
In [6]: show(skie.equalize_adapthist(img))
对曝光校正的对比有限自适应直方图均衡化方法的结果
直方图看起来更平衡,图像现在显得更加对比。
工作原理...
图像的直方图代表像素强度值的分布。它是图像编辑、图像处理和计算机视觉中的一个核心工具。
rescale_intensity()
函数可以伸缩图像的强度级别。一个使用案例是确保图像使用数据类型允许的整个值范围。
equalize_adapthist()
函数的工作原理是将图像分割成矩形区域,并计算每个区域的直方图。然后,像素的强度值被重新分配,以改善对比度并增强细节。
还有更多内容...
这里是一些参考资料:
-
图像直方图 相关内容可以在 Wikipedia 上找到。
-
直方图均衡化 相关内容可以在 Wikipedia 上找到。
-
自适应直方图均衡化 相关内容可以在 Wikipedia 上找到。
-
对比度 相关内容可以在 Wikipedia 上找到。
参见
- 在图像上应用滤波器 示例
在图像上应用滤波器
在这个示例中,我们对图像应用了多种滤波器,以实现不同的目的:模糊、去噪和边缘检测。
如何运作...
-
让我们导入相关包:
In [1]: import numpy as np import matplotlib.pyplot as plt import skimage import skimage.filter as skif import skimage.data as skid %matplotlib inline
-
我们创建一个函数来显示灰度图像:
In [2]: def show(img): plt.imshow(img, cmap=plt.cm.gray) plt.axis('off') plt.show()
-
现在,我们加载 Lena 图像(包含在 scikit-image 中)。我们选择一个单一的 RGB 组件以获取灰度图像:
In [3]: img = skimage.img_as_float(skid.lena())[...,0] In [4]: show(img)
-
让我们对图像应用模糊的高斯滤波器:
In [5]: show(skif.gaussian_filter(img, 5.))
-
现在,我们应用一个Sobel 滤波器,它增强了图像中的边缘:
In [6]: sobimg = skif.sobel(img) show(sobimg)
-
我们可以对过滤后的图像进行阈值处理,得到素描效果。我们得到一个只包含边缘的二值图像。我们使用笔记本小部件来找到适当的阈值值;通过添加
@interact
装饰器,我们在图像上方显示一个滑块。这个小部件让我们可以动态控制阈值。In [7]: from IPython.html import widgets @widgets.interact(x=(0.01, .4, .005)) def edge(x): show(sobimg<x)
-
最后,我们向图像中添加一些噪声,以展示去噪滤波器的效果:
In [8]: img = skimage.img_as_float(skid.lena()) # We take a portion of the image to show the # details. img = img[200:-100, 200:-150] # We add Gaussian noise. img = np.clip(img + 0.3*np.random.rand(*img.shape), 0, 1) In [9]: show(img)
-
denoise_tv_bregman()
函数实现了使用 Split Bregman 方法的全变差去噪:In [10]: show(skimage.restoration.denoise_tv_bregman(img, 5.))
如何运作...
图像处理中使用的许多滤波器都是线性滤波器。这些滤波器与第十章中的滤波器非常相似,信号处理;唯一的区别是它们在二维中工作。对图像应用线性滤波器等同于对图像与特定函数进行离散卷积。高斯滤波器通过与高斯函数卷积来模糊图像。
Sobel 滤波器计算图像梯度的近似值。因此,它能够检测图像中快速变化的空间变化,通常这些变化对应于边缘。
图像去噪是指从图像中去除噪声的过程。总变差去噪通过找到一个与原始(有噪声)图像接近的规则图像来工作。规则性由图像的总变差来量化:
Split Bregman 方法是基于 L¹范数的变种。它是压缩感知的一个实例,旨在找到真实世界中有噪声测量的规则和稀疏近似值。
还有更多内容...
以下是一些参考资料:
-
skimage.filter 模块的 API 参考,链接:
scikit-image.org/docs/dev/api/skimage.filter.html
-
噪声去除,Wikipedia 上有介绍,链接:
en.wikipedia.org/wiki/Noise_reduction
-
Wikipedia 上的高斯滤波器,链接:
en.wikipedia.org/wiki/Gaussian_filter
-
Sobel 滤波器,Wikipedia 上有介绍,链接:
en.wikipedia.org/wiki/Sobel_operator
-
图像去噪,Wikipedia 上有介绍,链接:
en.wikipedia.org/wiki/Noise_reduction
-
总变差去噪,Wikipedia 上有介绍,链接:
en.wikipedia.org/wiki/Total_variation_denoising
-
Split Bregman 算法的解释,链接:www.ece.rice.edu/~tag7/Tom_Goldstein/Split_Bregman.html
另请参见
- 图像曝光调整的配方
图像分割
图像分割包括将图像分割成具有某些特征的不同区域。这是计算机视觉、面部识别和医学成像中的一项基本任务。例如,图像分割算法可以自动检测医学图像中器官的轮廓。
scikit-image 提供了几种分割方法。在这个配方中,我们将演示如何分割包含不同物体的图像。
如何操作...
-
让我们导入相关包:
In [1]: import numpy as np import matplotlib.pyplot as plt from skimage.data import coins from skimage.filter import threshold_otsu from skimage.segmentation import clear_border from skimage.morphology import closing, square from skimage.measure import regionprops, label from skimage.color import lab2rgb %matplotlib inline
-
我们创建一个显示灰度图像的函数:
In [2]: def show(img, cmap=None): cmap = cmap or plt.cm.gray plt.imshow(img, cmap=cmap) plt.axis('off') plt.show()
-
我们使用 scikit-image 中捆绑的测试图像,展示了几枚硬币放置在简单背景上的样子:
In [3]: img = coins() In [4]: show(img)
-
分割图像的第一步是找到一个强度阈值,将(明亮的)硬币与(暗色的)背景分开。Otsu 方法定义了一个简单的算法来自动找到这个阈值。
In [5]: threshold_otsu(img) Out[5]: 107 In [6]: show(img>107)
使用 Otsu 方法得到的阈值图像
-
图像的左上角似乎存在问题,背景的一部分过于明亮。让我们使用一个笔记本小部件来找到更好的阈值:
In [7]: from IPython.html import widgets @widgets.interact(t=(10, 240)) def threshold(t): show(img>t)
使用手动选择的阈值的阈值化图像
-
阈值 120 看起来更好。下一步是通过平滑硬币并去除边界来清理二值图像。scikit-image 提供了一些功能来实现这些目的。
In [8]: img_bin = clear_border(closing(img>120, square(5))) show(img_bin)
清理过边界的阈值化图像
-
接下来,我们使用
label()
函数执行分割任务。此函数检测图像中的连接组件,并为每个组件分配唯一标签。在这里,我们在二值图像中为标签上色:In [9]: labels = label(img_bin) show(labels, cmap=plt.cm.rainbow)
分割后的图像
-
图像中的小伪影会导致虚假的标签,这些标签并不对应硬币。因此,我们只保留大于 100 像素的组件。
regionprops()
函数允许我们检索组件的特定属性(在这里是面积和边界框):In [10]: regions = regionprops(labels, ['Area', 'BoundingBox']) boxes = np.array([label['BoundingBox'] for label in regions if label['Area'] > 100]) print("There are {0:d} coins.".format(len(boxes))) There are 24 coins.
-
最后,我们在原始图像中每个组件上方显示标签号:
In [11]: plt.imshow(img, cmap=plt.cm.gray) plt.axis('off') xs = boxes[:,[1,3]].mean(axis=1) ys = boxes[:,[0,2]].mean(axis=1) for i, box in enumerate(boxes): plt.text(xs[i]-5, ys[i]+5, str(i))
它是如何工作的...
为了清理阈值化图像中的硬币,我们使用了数学形态学技术。这些方法基于集合理论、几何学和拓扑学,使我们能够操控形状。
例如,首先让我们解释膨胀和腐蚀。假设 A 是图像中的一组像素,b 是一个二维向量,我们表示 A[b] 为通过 b 平移的 A 集合,如下所示:
设 B 为一个整数分量的向量集合。我们称 B 为结构元素(在这里我们使用了方形结构元素)。此集合表示一个像素的邻域。A 通过 B 的膨胀操作是:
A通过 B 的腐蚀操作是:
膨胀操作通过在边界附近添加像素来扩展集合。腐蚀操作移除集合中与边界过于接近的像素。闭合操作是先膨胀后腐蚀。这一操作可以去除小的黑点并连接小的亮裂缝。在本配方中,我们使用了一个方形结构元素。
还有更多内容...
以下是一些参考资料:
-
有关图像处理的 SciPy 讲义,链接:
scipy-lectures.github.io/packages/scikit-image/
-
维基百科上的图像分割,链接:
en.wikipedia.org/wiki/Image_segmentation
-
Otsu 方法用于寻找阈值,详细解释见:
en.wikipedia.org/wiki/Otsu's_method
-
使用 scikit-image 进行分割教程(本配方的灵感来源)可见:
scikit-image.org/docs/dev/user_guide/tutorial_segmentation.html
-
维基百科上的数学形态学,查看
en.wikipedia.org/wiki/Mathematical_morphology
-
skimage.morphology
模块的 API 参考,查看scikit-image.org/docs/dev/api/skimage.morphology.html
另见
- 第十四章中的计算图像的连通分量食谱,图形、几何学与地理信息系统
在图像中找到兴趣点
在图像中,兴趣点是可能包含边缘、角点或有趣物体的位置。例如,在一幅风景画中,兴趣点可能位于房屋或人物附近。检测兴趣点在图像识别、计算机视觉或医学影像中非常有用。
在本食谱中,我们将使用 scikit-image 在图像中找到兴趣点。这将使我们能够围绕图像中的主题裁剪图像,即使该主题不在图像的中心。
准备就绪
从本书的 GitHub 仓库下载Child数据集,链接:github.com/ipython-books/cookbook-data
,并将其解压到当前目录。
如何操作...
-
我们导入所需的包:
In [1]: import numpy as np import matplotlib.pyplot as plt import skimage import skimage.feature as sf %matplotlib inline
-
我们创建一个函数来显示彩色或灰度图像:
In [2]: def show(img, cmap=None): cmap = cmap or plt.cm.gray plt.imshow(img, cmap=cmap) plt.axis('off')
-
我们加载一张图像:
In [3]: img = plt.imread('data/pic2.jpg') In [4]: show(img)
-
让我们使用哈里斯角点法在图像中找到显著点。第一步是使用
corner_harris()
函数计算哈里斯角点响应图像(我们将在如何工作...中解释这个度量)。该函数需要一个灰度图像,因此我们选择第一个 RGB 分量:In [5]: corners = sf.corner_harris(img[:,:,0]) In [6]: show(corners)
我们看到这个算法能很好地检测到孩子外套上的图案。
-
下一步是使用
corner_peaks()
函数从这个度量图像中检测角点:In [7]: peaks = sf.corner_peaks(corners) In [8]: show(img) plt.plot(peaks[:,1], peaks[:,0], 'or', ms=4)
-
最后,我们在角点周围创建一个框,定义我们的兴趣区域:
In [9]: ymin, xmin = peaks.min(axis=0) ymax, xmax = peaks.max(axis=0) w, h = xmax-xmin, ymax-ymin In [10]: k = .25 xmin -= k*w xmax += k*w ymin -= k*h ymax += k*h In [11]: show(img[ymin:ymax,xmin:xmax])
如何工作...
让我们解释本食谱中使用的方法。第一步是计算图像的结构张量(或哈里斯矩阵):
这里,I(x,y)是图像,I[x]和I[y]是偏导数,括号表示围绕邻近值的局部空间平均。
这个张量在每个点上关联一个(2,2)的正对称矩阵。该矩阵用于计算图像在每个点上的自相关。
让 和
成为这个矩阵的两个特征值(该矩阵是可对角化的,因为它是实数且对称的)。大致上,角点是通过各个方向的自相关变化很大来表征的,或者通过较大的正特征值
和
。角点度量图像定义为:
在这里,k 是一个可调节的参数。当存在角点时,M 会很大。最后,corner_peaks()
通过查找角点度量图像中的局部最大值来找到角点。
更多内容...
以下是一些参考资料:
-
一个使用 scikit-image 进行角点检测的示例,链接地址:
scikit-image.org/docs/dev/auto_examples/plot_corner.html
-
一个使用 scikit-image 进行图像处理的教程,链接地址:
blog.yhathq.com/posts/image-processing-with-scikit-image.html
-
维基百科上的角点检测,链接地址:
en.wikipedia.org/wiki/Corner_detection
-
维基百科上的结构张量,链接地址:
en.wikipedia.org/wiki/Structure_tensor
-
维基百科上的兴趣点检测,链接地址:
en.wikipedia.org/wiki/Interest_point_detection
-
skimage.feature
模块的 API 参考,链接地址:scikit-image.org/docs/dev/api/skimage.feature.html
使用 OpenCV 检测图像中的人脸
OpenCV(开放计算机视觉)是一个开源的 C++ 库,用于计算机视觉。它包含图像分割、物体识别、增强现实、人脸检测以及其他计算机视觉任务的算法。
在这个教程中,我们将使用 Python 中的 OpenCV 来检测图片中的人脸。
准备工作
你需要安装 OpenCV 和 Python 的包装器。你可以在 OpenCV 的官方网站找到安装说明:docs.opencv.org/trunk/doc/py_tutorials/py_tutorials.html
。
在 Windows 上,你可以安装 Chris Gohlke 的包,链接地址:www.lfd.uci.edu/~gohlke/pythonlibs/#opencv。
你还需要从书本的 GitHub 仓库下载 Family 数据集,链接地址:github.com/ipython-books/cookbook-data
。
注意
在写这篇文章时,OpenCV 尚不兼容 Python 3。因此,本教程要求使用 Python 2。
如何操作...
-
首先,我们导入所需的包:
In [1]: import numpy as np import cv2 import matplotlib.pyplot as plt %matplotlib inline
-
我们用 OpenCV 打开 JPG 图像:
In [2]: img = cv2.imread('data/pic3.jpg')
-
然后,我们使用 OpenCV 的
cvtColor()
函数将其转换为灰度图像。对于人脸检测,使用灰度图像已经足够且更快速。In [3]: gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
-
为了检测人脸,我们将使用Viola–Jones 物体检测框架。一个 Haar-like 分类器级联已经在大量图像上训练,以检测人脸(更多细节将在下一节提供)。训练的结果存储在一个 XML 文件中(该文件属于Family数据集,数据集可在本书的 GitHub 仓库中找到)。我们使用 OpenCV 的
CascadeClassifier
类从这个 XML 文件中加载该级联:In [4]: face_cascade = cv2.CascadeClassifier( 'data/haarcascade_frontalface_default.xml')
-
最后,分类器的
detectMultiScale()
方法在灰度图像上检测物体,并返回围绕这些物体的矩形框列表:In [5]: for x,y,w,h in \ face_cascade.detectMultiScale(gray, 1.3): cv2.rectangle(gray, (x,y), (x+w,y+h), (255,0,0), 2) plt.imshow(gray, cmap=plt.cm.gray) plt.axis('off')
我们可以看到,尽管所有检测到的物体确实是人脸,但每四张脸中就有一张没有被检测到。这可能是因为这张脸并没有完全正对摄像头,而训练集中的人脸则都是正对着摄像头的。这表明该方法的有效性受限于训练集的质量和通用性。
它是如何工作的...
Viola–Jones 物体检测框架通过训练一系列使用 Haar-like 特征的提升分类器来工作。首先,我们考虑一组特征:
Haar-like 特征
一个特征被定位在图像中的特定位置和大小。它覆盖了图像中的一个小窗口(例如 24 x 24 像素)。黑色区域的所有像素和白色区域的所有像素之和相减。这一操作可以通过积分图像高效地完成。
然后,所有分类器集通过提升技术进行训练;在训练过程中,只有最好的特征会被保留下来进入下一阶段。训练集包含正负图像(有脸和没有脸的图像)。尽管每个分类器单独的表现较差,但是这些提升分类器的级联方式既高效又快速。因此,这种方法非常适合实时处理。
XML 文件已从 OpenCV 包中获得。该文件对应多个训练集。你也可以使用自己的训练集训练自己的级联分类器。
还有更多...
以下是一些参考资料:
-
一个关于级联教程的 OpenCV (C++)教程可在
docs.opencv.org/doc/tutorials/objdetect/cascade_classifier/cascade_classifier.html
找到 -
训练级联的文档可在
docs.opencv.org/doc/user_guide/ug_traincascade.html
找到 -
Haar 级联库可在
github.com/Itseez/opencv/tree/master/data/haarcascades
找到 -
OpenCV 的级联分类 API 参考可在
docs.opencv.org/modules/objdetect/doc/cascade_classification.html
找到 -
维基百科上的 Viola–Jones 目标检测框架,网址为
en.wikipedia.org/wiki/Viola%E2%80%93Jones_object_detection_framework
-
提升或如何从许多弱分类器创建一个强分类器,解释在
en.wikipedia.org/wiki/Boosting_(machine_learning)
将数字滤波器应用于语音
在这个示例中,我们将展示如何在笔记本中播放声音。我们还将说明简单数字滤波器对语音的影响。
准备工作
您需要pydub包。您可以使用pip install pydub
安装它,或从github.com/jiaaro/pydub/
下载。
该软件包需要开源多媒体库 FFmpeg 来解压缩 MP3 文件,网址为www.ffmpeg.org。
这里给出的代码适用于 Python 3。您可以在书的 GitHub 存储库中找到 Python 2 版本。
如何做…
-
让我们导入这些包:
In [1]: import urllib from io import BytesIO import numpy as np import scipy.signal as sg import pydub import matplotlib.pyplot as plt from IPython.display import Audio, display %matplotlib inline
-
我们创建一个 Python 函数,从英文句子生成声音。这个函数使用 Google 的文本转语音(TTS)API。我们以 MP3 格式检索声音,并用 pydub 将其转换为 Wave 格式。最后,我们通过删除 NumPy 中的波形头检索原始声音数据:
In [2]: def speak(sentence): url = ("http://translate.google.com/" "translate_tts?tl=en&q=") + urllib.parse.quote_plus(sentence) req = urllib.request.Request(url, headers={'User-Agent': ''}) mp3 = urllib.request.urlopen(req).read() # We convert the mp3 bytes to wav. audio = pydub.AudioSegment.from_mp3( BytesIO(mp3)) wave = audio.export('_', format='wav') wave.seek(0) wave = wave.read() # We get the raw data by removing the 24 # first bytes of the header. x = np.frombuffer(wave, np.int16)[24:] / 2.**15
-
我们创建一个函数,在笔记本中播放声音(由 NumPy 向量表示),使用 IPython 的
Audio
类:In [3]: def play(x, fr, autoplay=False): display(Audio(x, rate=fr, autoplay=autoplay))
-
让我们播放声音“Hello world.”,并使用 matplotlib 显示波形:
In [4]: x, fr = speak("Hello world") play(x, fr) t = np.linspace(0., len(x)/fr, len(x)) plt.plot(t, x, lw=1)
-
现在,我们将听到应用于此声音的 Butterworth 低通滤波器的效果(500 Hz 截止频率):
In [5]: b, a = sg.butter(4, 500./(fr/2.), 'low') x_fil = sg.filtfilt(b, a, x) In [6]: play(x_fil, fr) plt.plot(t, x, lw=1) plt.plot(t, x_fil, lw=1)
我们听到了一个沉闷的声音。
-
现在,使用高通滤波器(1000 Hz 截止频率):
In [7]: b, a = sg.butter(4, 1000./(fr/2.), 'high') x_fil = sg.filtfilt(b, a, x) In [8]: play(x_fil, fr) plt.plot(t, x, lw=1) plt.plot(t, x_fil, lw=1)
听起来像一个电话。
-
最后,我们可以创建一个简单的小部件,快速测试高通滤波器的效果,带有任意截止频率:
In [9]: from IPython.html import widgets @widgets.interact(t=(100., 5000., 100.)) def highpass(t): b, a = sg.butter(4, t/(fr/2.), 'high') x_fil = sg.filtfilt(b, a, x) play(x_fil, fr, autoplay=True)
我们得到一个滑块,让我们改变截止频率并实时听到效果。
工作原理…
人耳可以听到高达 20 kHz 的频率。人声频带范围大约从 300 Hz 到 3000 Hz。
数字滤波器在第十章中有描述,信号处理。这里给出的示例允许我们听到低通和高通滤波器对声音的影响。
还有更多…
这里有一些参考资料:
-
维基百科上的音频信号处理,网址为
en.wikipedia.org/wiki/Audio_signal_processing
-
维基百科上的音频滤波器,网址为
en.wikipedia.org/wiki/Audio_filter
-
维基百科上的声音频率,网址为
en.wikipedia.org/wiki/Voice_frequency
-
PyAudio,一个使用 PortAudio 库的音频 Python 包,链接在
people.csail.mit.edu/hubert/pyaudio/
另见
- 在笔记本中创建声音合成器食谱
在笔记本中创建声音合成器
在这个食谱中,我们将在笔记本中创建一个小型电子钢琴。我们将使用 NumPy 合成正弦声音,而不是使用录制的音频。
如何操作...
-
我们导入模块:
In [1]: import numpy as np import matplotlib.pyplot as plt from IPython.display import (Audio, display, clear_output) from IPython.html import widgets from functools import partial %matplotlib inline
-
我们定义音符的采样率和持续时间:
In [2]: rate = 16000. duration = 0.5 t = np.linspace(0., duration, rate * duration)
-
我们创建一个函数,使用 NumPy 和 IPython 的
Audio
类生成并播放指定频率的音符(正弦函数)声音:In [3]: def synth(f): x = np.sin(f * 2\. * np.pi * t) display(Audio(x, rate=rate, autoplay=True))
-
这是基础的 440 Hz 音符:
In [4]: synth(440)
-
现在,我们生成钢琴的音符频率。十二平均律是通过一个公比为 2^(1/12) 的几何级数得到的:
In [5]: notes = zip(('C,C#,D,D#,E,F,F#,G,G#,' 'A,A#,B,C').split(','), 440\. * 2 ** (np.arange(3, 17) / 12.))
-
最后,我们使用笔记本小部件创建钢琴。每个音符是一个按钮,所有按钮都包含在一个水平框容器中。点击某个音符会播放对应频率的声音。钢琴布局与《第三章》中的使用交互式小部件——笔记本中的钢琴食谱相同,掌握笔记本一书中的布局相同。
In [6]: container = widgets.ContainerWidget() buttons = [] for note, f in notes: button = widgets.ButtonWidget(description=note) def on_button_clicked(f, b): clear_output() synth(f) button.on_click(partial(on_button_clicked, f)) button.set_css({...}) buttons.append(button) container.children = buttons display(container) container.remove_class('vbox') container.add_class('hbox')
注意
此处使用的 IPython API 设计布局基于 IPython 2.x;在 IPython 3.0 中会有所不同。
它是如何工作的...
纯音是具有正弦波形的音调。它是表示音乐音符的最简单方式。由乐器发出的音符通常更加复杂,尽管声音包含多个频率,但我们通常感知到的是音乐音调(基频)。
通过生成另一个周期性函数代替正弦波形,我们会听到相同的音调,但音色(timbre)不同。电子音乐合成器基于这个原理。
还有更多...
以下是一些参考资料:
-
维基百科上的合成器,链接在
en.wikipedia.org/wiki/Synthesizer
-
维基百科上的均等律,链接在
en.wikipedia.org/wiki/Equal_temperament
-
维基百科上的音阶,链接在
en.wikipedia.org/wiki/Chromatic_scale
-
维基百科上的纯音,链接在
en.wikipedia.org/wiki/Pure_tone
-
维基百科上的音色,链接在
en.wikipedia.org/wiki/Timbre
另见
-
应用数字滤波器于语音声音食谱
-
《第三章》中的使用交互式小部件——笔记本中的钢琴食谱,掌握笔记本。
第十二章:确定性动力学系统
在本章中,我们将讨论以下主题:
-
绘制混沌动力学系统的分叉图
-
模拟一个初级细胞自动机
-
使用 SciPy 模拟常微分方程
-
模拟偏微分方程——反应扩散系统与图灵模式
介绍
前几章讨论了数据科学中的经典方法:统计学、机器学习和信号处理。在本章和下一章中,我们将介绍另一种方法。我们不会直接分析数据,而是模拟代表数据生成方式的数学模型。一个具有代表性的模型能为我们提供数据背后现实世界过程的解释。
具体而言,我们将介绍几个动力学系统的例子。这些数学方程描述了量随时间和空间的演变。它们可以表示物理、化学、生物学、经济学、社会科学、计算机科学、工程学等学科中各种现实世界的现象。
在本章中,我们将讨论确定性动力学系统。该术语与随机系统相对,后者的规则中包含了随机性。我们将在下一章讨论随机系统。
动力学系统的类型
我们将在这里讨论的确定性动力学系统类型包括:
-
离散时间动力学系统(迭代函数)
-
细胞自动机
-
常微分方程(ODEs)
-
偏微分方程(PDEs)
在这些模型中,感兴趣的量依赖于一个或多个独立变量。这些变量通常包括时间和/或空间。独立变量可以是离散的或连续的,从而导致不同类型的模型和不同的分析与仿真技术。
离散时间动力学系统是通过在初始点上迭代应用一个函数来描述的:f(x), f(f(x)), f(f(f(x))),以此类推。这种类型的系统可能会导致复杂且混沌的行为。
细胞自动机由一个离散的单元格网格表示,每个单元格可以处于有限个状态之一。规则描述了单元格状态如何根据相邻单元格的状态演化。这些简单的模型可以导致极其复杂的行为。
一个常微分方程描述了一个连续函数如何依赖于其相对于独立变量的导数。在微分方程中,未知变量是一个函数而不是数字。常微分方程特别出现在量的变化率依赖于该量的当前值的情况。例如,在经典力学中,运动定律(包括行星和卫星的运动)可以通过常微分方程来描述。
PDEs 与 ODEs 相似,但它们涉及多个独立变量(例如时间和空间)。这些方程包含关于不同独立变量的偏导数。例如,PDEs 描述波的传播(声波、电磁波或机械波)和流体(流体力学)。它们在量子力学中也很重要。
微分方程
ODE 和 PDE 可以是单维或多维的,取决于目标空间的维度。多个微分方程的系统可以看作是多维方程。
ODE 或 PDE 的阶数指的是方程中最大导数的阶数。例如,一阶方程仅涉及简单的导数,二阶方程则涉及二阶导数(导数的导数),以此类推。
常微分方程或偏微分方程有额外的规则:初始 和 边界条件。这些公式描述了所求函数在空间和时间域边界上的行为。例如,在经典力学中,边界条件包括物体在力作用下的初始位置和初速度。
动态系统通常根据规则是否线性,分为线性和非线性系统(相对于未知函数)。非线性方程通常比线性方程在数学和数值上更加难以研究。它们可能会导致极其复杂的行为。
例如,Navier–Stokes 方程,一组描述流体物质运动的非线性 PDEs,可能会导致湍流,这是一种在许多流体流动中出现的高度混乱的行为。尽管在气象学、医学和工程学中具有重要意义,但 Navier-Stokes 方程的基本性质目前仍未为人所知。例如,三维中的存在性和平滑性问题是七个克雷数学研究所千年奖问题之一。对于任何能够提出解决方案的人,奖励为一百万美元。
参考文献
以下是一些参考文献:
-
维基百科上的动态系统概述,链接:
en.wikipedia.org/wiki/Dynamical_system
-
动态系统的数学定义,链接:
en.wikipedia.org/wiki/Dynamical_system_%28definition%29
-
动态系统主题列表,链接:
en.wikipedia.org/wiki/List_of_dynamical_systems_and_differential_equations_topics
-
维基百科上的 Navier-Stokes 方程,链接:
en.wikipedia.org/wiki/Navier%E2%80%93Stokes_equations
-
Prof. Lorena Barba 的《计算流体动力学》课程,使用 IPython 笔记本编写,课程内容可以在
github.com/barbagroup/CFDPython
获取
绘制混沌动力系统的分叉图
混沌动力系统对初始条件非常敏感;在任何给定时刻的微小扰动都会产生完全不同的轨迹。混沌系统的轨迹通常具有复杂且不可预测的行为。
许多现实世界的现象是混沌的,特别是那些涉及多个主体之间非线性相互作用的现象(复杂系统)。气象学、经济学、生物学和其他学科中都有著名的例子。
在这个示例中,我们将模拟一个著名的混沌系统:逻辑映射。这是一个经典的例子,展示了如何从一个非常简单的非线性方程中产生混沌。逻辑映射模型描述了一个种群的演化,考虑到繁殖和密度依赖性死亡(饥饿)。
我们将绘制系统的分叉图,它展示了作为系统参数函数的可能长期行为(平衡点、固定点、周期轨道和混沌轨迹)。我们还将计算系统的李雅普诺夫指数的近似值,以表征模型对初始条件的敏感性。
如何实现...
-
让我们导入 NumPy 和 matplotlib:
In [1]: import numpy as np import matplotlib.pyplot as plt %matplotlib inline
-
我们通过以下方式定义逻辑函数:
我们的离散动力系统由逻辑函数的递归应用定义:
-
这是该函数在 Python 中的实现:
In [2]: def logistic(r, x): return r*x*(1-x)
-
我们为 10000 个 r 值进行模拟,这些值在线性间隔的
2.5
和4
之间,并通过 NumPy 向量化模拟,考虑到一个独立系统的向量(每个参数值对应一个动力系统):In [3]: n = 10000 r = np.linspace(2.5, 4.0, n)
-
让我们模拟 1000 次逻辑映射的迭代,并保留最后 100 次迭代,以显示分叉图:
In [4]: iterations = 1000 last = 100
-
我们使用相同的初始条件 x[0] = 0.00001 初始化我们的系统:
In [5]: x = 1e-5 * np.ones(n)
-
我们还计算了每个 r 值的李雅普诺夫指数近似值。李雅普诺夫指数的定义是:
-
我们首先初始化
lyapunov
向量:In [6]: lyapunov = np.zeros(n)
-
现在,我们模拟系统并绘制分叉图。该模拟仅涉及在我们的向量
x
上迭代评估logistic()
函数。然后,为了显示分叉图,我们在最后 100 次迭代期间每个点 x[n]^((r)) 绘制一个像素:In [7]: plt.subplot(211) for i in range(iterations): x = logistic(r, x) # We compute the partial sum of the # Lyapunov exponent. lyapunov += np.log(abs(r-2*r*x)) # We display the bifurcation diagram. if i >= (iterations - last): plt.plot(r, x, ',k', alpha=.02) plt.xlim(2.5, 4) plt.title("Bifurcation diagram") # We display the Lyapunov exponent. plt.subplot(212) plt.plot(r[lyapunov<0], lyapunov[lyapunov<0] / iterations, ',k', alpha=0.1) plt.plot(r[lyapunov>=0], lyapunov[lyapunov>=0] / iterations, ',r', alpha=0.25) plt.xlim(2.5, 4) plt.ylim(-2, 1) plt.title("Lyapunov exponent")
逻辑映射的分叉图和李雅普诺夫指数
分叉图揭示了在 r<3 时存在一个固定点,然后是两个和四个平衡点,当 r 属于参数空间的某些区域时,出现混沌行为。
我们观察到李雅普诺夫指数的一个重要特性:当系统处于混沌状态时,它是正值(此处为红色)。
还有更多…
以下是一些参考资料:
-
维基百科上的混沌理论,可以通过
en.wikipedia.org/wiki/Chaos_theory
查看。 -
维基百科上的复杂系统,可以通过
en.wikipedia.org/wiki/Complex_system
查看。 -
维基百科上的逻辑映射,可以通过
en.wikipedia.org/wiki/Logistic_map
查看。 -
维基百科上的迭代函数(离散动力学系统),可以通过
en.wikipedia.org/wiki/Iterated_function
查看。 -
维基百科上的分叉图,可以通过
en.wikipedia.org/wiki/Bifurcation_diagram
查看。 -
维基百科上的李雅普诺夫指数,可以通过
en.wikipedia.org/wiki/Lyapunov_exponent
查看。
另见
- 使用 SciPy 模拟常微分方程示例
模拟基础细胞自动机
细胞自动机是离散的动力学系统,在一个单元格网格上演化。这些单元格可以处于有限的状态(例如,开/关)。细胞自动机的演化遵循一组规则,描述了每个单元格的状态如何根据其邻居的状态发生变化。
尽管这些模型非常简单,但它们可以引发高度复杂和混乱的行为。细胞自动机可以模拟现实世界中的现象,例如汽车交通、化学反应、森林中的火灾传播、流行病传播等。细胞自动机也存在于自然界中。例如,一些海洋贝壳的图案就是由自然细胞自动机生成的。
基础细胞自动机是一种二进制的一维自动机,其中规则涉及每个单元格的直接左右邻居。
在这个实例中,我们将使用 NumPy 模拟基础细胞自动机,并使用它们的沃尔夫拉姆代码。
如何操作…
-
我们导入 NumPy 和 matplotlib:
In [1]: import numpy as np import matplotlib.pyplot as plt %matplotlib inline
-
我们将使用以下向量来获得二进制表示的数字:
In [2]: u = np.array([[4], [2], [1]])
-
让我们编写一个函数,执行网格上的迭代,根据给定的规则一次性更新所有单元格,规则以二进制表示(我们将在它是如何工作的...部分进行详细解释)。第一步是通过堆叠循环移位版本的网格,得到每个单元格的 LCR(左,中,右)三元组(
y
)。然后,我们将这些三元组转换为 3 位二进制数(z
)。最后,我们使用指定的规则计算每个单元格的下一个状态:In [3]: def step(x, rule_binary): """Compute a single stet of an elementary cellular automaton.""" # The columns contain the L, C, R values # of all cells. y = np.vstack((np.roll(x, 1), x, np.roll(x, -1))).astype(np.int8) # We get the LCR pattern numbers # between 0 and 7. z = np.sum(y * u, axis=0).astype(np.int8) # We get the patterns given by the rule. return rule_binary[7-z]
-
现在,我们编写一个函数来模拟任何基础细胞自动机。首先,我们计算规则的二进制表示(沃尔夫拉姆代码)。然后,我们用随机值初始化网格的第一行。最后,我们在网格上迭代应用函数
step()
:In [4]: def generate(rule, size=80, steps=80): """Simulate an elementary cellular automaton given its rule (number between 0 and 255).""" # Compute the binary representation of the # rule. rule_binary = np.array( [int(x) for x in np.binary_repr(rule, 8)], dtype=np.int8) x = np.zeros((steps, size), dtype=np.int8) # Random initial state. x[0,:] = np.random.rand(size) < .5 # Apply the step function iteratively. for i in range(steps-1): x[i+1,:] = step(x[i,:], rule_binary) return x
-
现在,我们模拟并显示九种不同的自动机:
In [5]: rules = [ 3, 18, 30, 90, 106, 110, 158, 154, 184] for i, rule in enumerate(rules): x = generate(rule) plt.subplot(331+i) plt.imshow(x, interpolation='none', cmap=plt.cm.binary) plt.xticks([]); plt.yticks([]) plt.title(str(rule))
它是如何工作的…
让我们考虑一个一维的初等细胞自动机。每个细胞C有两个邻居(L和R),并且它可以是关闭的(0)或开启的(1)。因此,一个细胞的未来状态依赖于其邻居 L、C 和 R 的当前状态。这个三元组可以编码为一个 0 到 7 之间的数字(二进制表示为三位数)。
一个特定的初等细胞自动机完全由这些八种配置的结果决定。因此,存在 256 种不同的初等细胞自动机(2⁸)。每一个这样的自动机由一个介于 0 和 255 之间的数字表示。
我们按顺序考虑所有八种 LCR 状态:111、110、101、...、001、000。自动机数字的二进制表示中的每一位对应一个 LCR 状态(使用相同的顺序)。例如,在规则 110 自动机(其二进制表示为01101110
)中,状态 111 会产生中心细胞的 0,状态 110 产生 1,状态 101 产生 1,依此类推。已有研究表明,这种特定的自动机是图灵完备的(或称通用的);理论上,它能够模拟任何计算机程序。
还有更多内容...
其他类型的细胞自动机包括康威的生命游戏,这是一个二维系统。这个著名的系统可以产生各种动态模式。它也是图灵完备的。
以下是一些参考资料:
-
维基百科上的细胞自动机,可通过
en.wikipedia.org/wiki/Cellular_automaton
访问 -
维基百科上的初等细胞自动机,可通过
en.wikipedia.org/wiki/Elementary_cellular_automaton
访问 -
规则 110,描述见
en.wikipedia.org/wiki/Rule_110
-
解释见
en.wikipedia.org/wiki/Wolfram_code
的 Wolfram 代码,将一个一维初等细胞自动机分配给 0 到 255 之间的任何数字 -
维基百科上的《康威的生命游戏》,可通过
en.wikipedia.org/wiki/Conway's_Game_of_Life
访问
使用 SciPy 模拟常微分方程
常微分方程(ODEs)描述了一个系统在内外部动态影响下的演变。具体来说,ODE 将依赖于单一自变量(例如时间)的量与其导数联系起来。此外,系统还可能受到外部因素的影响。一个一阶 ODE 通常可以写成:
更一般地说,一个n阶 ODE 涉及到y的连续导数直到阶数n。ODE 被称为线性或非线性,取决于f是否在y中是线性的。
当一个量的变化率依赖于其值时,常微分方程自然出现。因此,ODE 在许多科学领域都有应用,如力学(受动力学力作用的物体演化)、化学(反应产物的浓度)、生物学(流行病的传播)、生态学(种群的增长)、经济学和金融等。
虽然简单的常微分方程(ODE)可以通过解析方法求解,但许多 ODE 需要数值处理。在这个例子中,我们将模拟一个简单的线性二阶自治 ODE,描述一个在重力和粘性阻力作用下的空气中粒子的演化。虽然这个方程可以通过解析方法求解,但在这里我们将使用 SciPy 进行数值模拟。
如何实现...
-
让我们导入 NumPy、SciPy(
integrate
包)和 matplotlib:In [1]: import numpy as np import scipy.integrate as spi import matplotlib.pyplot as plt %matplotlib inline
-
我们定义了一些在模型中出现的参数:
In [2]: m = 1\. # particle's mass k = 1\. # drag coefficient g = 9.81 # gravity acceleration
-
我们有两个变量:x和y(二维)。我们记u=(x,y)。我们将要模拟的 ODE 是:
这里,g是重力加速度向量。
提示
时间导数用变量上的点表示(一个点表示一阶导数,两个点表示二阶导数)。
为了使用 SciPy 模拟这个二阶 ODE,我们可以将其转换为一阶 ODE(另一种选择是先求解u'再进行积分)。为此,我们考虑两个二维变量:u和u'。我们记v = (u, u')。我们可以将v'表示为v的函数。现在,我们创建初始向量v[0],其时间为t=0,它有四个分量。
In [3]: # The initial position is (0, 0). v0 = np.zeros(4) # The initial speed vector is oriented # to the top right. v0[2] = 4. v0[3] = 10.
-
让我们创建一个 Python 函数f,该函数接受当前向量v(t[0])和时间t[0]作为参数(可以有可选参数),并返回导数v'(t[0]):
In [4]: def f(v, t0, k): # v has four components: v=[u, u']. u, udot = v[:2], v[2:] # We compute the second derivative u'' of u. udotdot = -k/m * udot udotdot[1] -= g # We return v'=[u', u'']. return np.r_[udot, udotdot]
-
现在,我们使用不同的k值来模拟系统。我们使用 SciPy 的
odeint()
函数,它定义在scipy.integrate
包中。In [5]: # We want to evaluate the system on 30 linearly # spaced times between t=0 and t=3. t = np.linspace(0., 3., 30) # We simulate the system for different values of k. for k in np.linspace(0., 1., 5): # We simulate the system and evaluate $v$ on # the given times. v = spi.odeint(f, v0, t, args=(k,)) # We plot the particle's trajectory. plt.plot(v[:,0], v[:,1], 'o-', mew=1, ms=8, mec='w', label='k={0:.1f}'.format(k)) plt.legend() plt.xlim(0, 12) plt.ylim(0, 6)
在上图中,最外层的轨迹(蓝色)对应于无阻力运动(没有空气阻力)。它是一条抛物线。在其他轨迹中,我们可以观察到空气阻力的逐渐增加,它通过
k
来参数化。
它是如何工作的...
让我们解释一下如何从我们的模型中获得微分方程。设* u = (x,y)* 表示粒子在二维空间中的位置,粒子质量为m。该粒子受到两个力的作用:重力 g = (0, -9.81)(单位:m/s)和空气阻力 F = -ku'。最后一项依赖于粒子的速度,并且只在低速下有效。对于更高的速度,我们需要使用更复杂的非线性表达式。
现在,我们使用牛顿第二定律来处理经典力学中的运动。该定律表明,在惯性参考系中,粒子的质量乘以其加速度等于作用于粒子的所有力的合力。在这里,我们得到了:
我们立即得到我们的二阶 ODE:
我们将其转化为一个一阶常微分方程系统,定义 v=(u, u'):
最后一项可以仅以 v 的函数表示。
SciPy 的 odeint()
函数是一个黑箱求解器;我们只需指定描述系统的函数,SciPy 会自动求解。
该函数利用了 FORTRAN 库 ODEPACK,它包含了经过充分测试的代码,已经被许多科学家和工程师使用了几十年。
一个简单的数值求解器示例是 欧拉法。为了数值求解自治常微分方程 y'=f(y),该方法通过用时间步长 dt 离散化时间,并将 y' 替换为一阶近似:
然后,从初始条件 y[0] = y(t[0]) 开始,方法通过以下递推关系依次计算 y:
还有更多...
下面是一些参考资料:
-
SciPy 中
integrate
包的文档,可以在docs.scipy.org/doc/scipy/reference/integrate.html
查阅 -
维基百科上的常微分方程(ODEs)条目,可以在
en.wikipedia.org/wiki/Ordinary_differential_equation
查阅 -
维基百科上的牛顿运动定律条目,可以在
en.wikipedia.org/wiki/Newton's_laws_of_motion
查阅 -
维基百科上的空气阻力条目,可以在
en.wikipedia.org/wiki/Drag_%28physics%29
查阅 -
描述常微分方程的数值方法,可以在
en.wikipedia.org/wiki/Numerical_methods_for_ordinary_differential_equations
查阅 -
维基百科上的欧拉法条目,可以在
en.wikipedia.org/wiki/Euler_method
查阅 -
FORTRAN 中 ODEPACK 包的文档,可以在 www.netlib.org/odepack/opks-sum 查阅
另见:
- 绘制混沌动力系统的分叉图 这个配方
模拟偏微分方程 —— 反应-扩散系统和图灵模式
偏微分方程(PDEs)描述了包含时间和空间的动力系统的演化。物理学中的例子包括声音、热、电子磁学、流体流动和弹性等。生物学中的例子包括肿瘤生长、种群动态和疫情传播。
偏微分方程(PDEs)很难通过解析方法求解。因此,偏微分方程通常通过数值模拟来研究。
在这个教程中,我们将演示如何模拟由称为FitzHugh–Nagumo 方程的偏微分方程(PDE)描述的反应扩散系统。反应扩散系统模拟了一个或多个变量的演变,这些变量受到两种过程的影响:反应(变量之间的相互转化)和扩散(在空间区域中的传播)。一些化学反应可以用这种模型来描述,但在物理学、生物学、生态学以及其他学科中也有其他应用。
在这里,我们模拟了艾伦·图灵提出的一个系统,用作动物皮毛图案形成的模型。两种影响皮肤着色的化学物质根据反应扩散模型相互作用。这个系统负责形成类似斑马、美洲豹和长颈鹿皮毛的图案。
我们将使用有限差分法模拟这个系统。该方法通过离散化时间和空间,并将导数替换为其离散等价物来实现。
如何实现...
-
让我们导入必要的包:
In [1]: import numpy as np import matplotlib.pyplot as plt %matplotlib inline
-
我们将在域E=[-1,1]²上模拟以下偏微分方程系统:
变量u表示有助于皮肤着色的物质浓度,而v表示另一种与第一种物质反应并抑制着色的物质。
在初始化时,我们假设每个网格点上的u和v包含独立的随机数。我们还采用Neumann 边界条件:要求变量相对于法向量的空间导数在域的边界上为零。
-
让我们定义模型的四个参数:
In [2]: a = 2.8e-4 b = 5e-3 tau = .1 k = -.005
-
我们离散化时间和空间。以下条件确保我们在此使用的离散化方案是稳定的:
In [3]: size = 80 # size of the 2D grid dx = 2./size # space step In [4]: T = 10.0 # total time dt = .9 * dx**2/2 # time step n = int(T/dt)
-
我们初始化变量u和v。矩阵
U
和V
包含这些变量在二维网格顶点上的值。这些变量被初始化为介于 0 和 1 之间的均匀噪声:In [5]: U = np.random.rand(size, size) V = np.random.rand(size, size)
-
现在,我们定义一个函数,使用五点模板有限差分法计算网格上二维变量的离散拉普拉斯算符。这个算符定义为:
我们可以通过向量化的矩阵操作计算网格上该算符的值。由于矩阵边缘的副作用,我们需要在计算中去除网格的边界:
In [6]: def laplacian(Z): Ztop = Z[0:-2,1:-1] Zleft = Z[1:-1,0:-2] Zbottom = Z[2:,1:-1] Zright = Z[1:-1,2:] Zcenter = Z[1:-1,1:-1] return (Ztop + Zleft + Zbottom + Zright \ - 4 * Zcenter) / dx**2
-
现在,我们使用有限差分法模拟方程系统。在每个时间步长上,我们使用离散空间导数(拉普拉斯算符)计算网格上两个方程的右侧。然后,我们使用离散时间导数更新变量:
In [7]: for i in range(n): # We compute the Laplacian of u and v. deltaU = laplacian(U) deltaV = laplacian(V) # We take the values of u and v # inside the grid. Uc = U[1:-1,1:-1] Vc = V[1:-1,1:-1] # We update the variables. U[1:-1,1:-1], V[1:-1,1:-1] = ( Uc + dt * (a*deltaU + Uc - Uc**3 - Vc + k), Vc + dt * (b*deltaV + Uc - Vc) / tau) # Neumann conditions: derivatives at the edges # are null. for Z in (U, V): Z[0,:] = Z[1,:] Z[-1,:] = Z[-2,:] Z[:,0] = Z[:,1] Z[:,-1] = Z[:,-2]
-
最后,我们展示在时间
T
的模拟后变量u
的状态:In [8]: plt.imshow(U, cmap=plt.cm.copper, extent=[-1,1,-1,1])
尽管变量在初始化时是完全随机的,但在足够长的模拟时间后,我们观察到模式的形成。
它是如何工作的...
让我们解释有限差分法是如何帮助我们实现更新步骤的。我们从以下方程系统开始:
我们首先使用以下方案来离散化拉普拉斯算子:
我们也使用这个方案来处理 u 和 v 的时间导数:
最终,我们得到以下的迭代更新步骤:
在这里,我们的诺依曼边界条件规定,法向量方向上的空间导数在区域 E 的边界上为零:
我们通过在矩阵 U
和 V
的边缘复制值来实现这些边界条件(请参见前面的代码)。
为了确保我们的数值方案收敛到一个接近实际(未知)数学解的数值解,需要确定该方案的稳定性。可以证明,稳定性的充要条件是:
还有更多...
以下是关于偏微分方程、反应-扩散系统及其数值模拟的进一步参考资料:
-
维基百科上的偏微分方程,详情请见
en.wikipedia.org/wiki/Partial_differential_equation
-
维基百科上的反应-扩散系统,详情请见
en.wikipedia.org/wiki/Reaction%E2%80%93diffusion_system
-
维基百科上的 FitzHugh-Nagumo 系统,详情请见
en.wikipedia.org/wiki/FitzHugh%E2%80%93Nagumo_equation
-
维基百科上的诺依曼边界条件,详情请见
en.wikipedia.org/wiki/Neumann_boundary_condition
-
维基百科上的冯·诺依曼稳定性分析,详情请见
en.wikipedia.org/wiki/Von_Neumann_stability_analysis
-
由 Lorena Barba 教授讲授的计算流体力学课程,使用 IPython 笔记本编写,详情请见
github.com/barbagroup/CFDPython
另见
-
模拟一个基础的元胞自动机 方案
-
使用 SciPy 模拟常微分方程 方案
第十三章:随机动力学系统
在本章中,我们将讨论以下主题:
-
模拟离散时间马尔科夫链
-
模拟泊松过程
-
模拟布朗运动
-
模拟随机微分方程
介绍
随机动力学系统是受噪声影响的动力学系统。噪声带来的随机性考虑了现实世界现象中观察到的变化性。例如,股价的演变通常表现为长期行为,并伴有较快、幅度较小的振荡,反映了日常或小时的波动。
随机系统在数据科学中的应用包括统计推断方法(如马尔科夫链蒙特卡洛)和用于时间序列或地理空间数据的随机建模。
随机离散时间系统包括离散时间马尔科夫链。马尔科夫性质意味着系统在时间n+1时刻的状态仅依赖于它在时间n时刻的状态。随机元胞自动机是元胞自动机的随机扩展,是特殊的马尔科夫链。
至于连续时间系统,带噪声的常微分方程会得到随机微分方程(SDEs)。带噪声的偏微分方程会得到随机偏微分方程(SPDEs)。
点过程是另一种随机过程。这些过程建模了随时间(例如排队中顾客的到达或神经系统中的动作电位)或空间(例如森林中树木的位置、区域中的城市或天空中的星星)随机发生的瞬时事件。
从数学上讲,随机动力学系统的理论基于概率论和测度论。连续时间随机系统的研究建立在随机微积分的基础上,随机微积分是对微积分(包括导数和积分)的扩展,适用于随机过程。
在本章中,我们将看到如何使用 Python 模拟不同种类的随机系统。
参考资料
这里有一些相关的参考资料:
-
随机动力学系统概述,见于 www.scholarpedia.org/article/Stochastic_dynamical_systems
-
维基百科上的马尔科夫性质,见于
en.wikipedia.org/wiki/Markov_property
模拟离散时间马尔科夫链
离散时间马尔科夫链是随机过程,它在状态空间中从一个状态转换到另一个状态。每个时间步都会发生状态转换。马尔科夫链的特点是没有记忆,即从当前状态到下一个状态的转换概率仅依赖于当前状态,而不依赖于之前的状态。这些模型在科学和工程应用中得到了广泛的使用。
连续时间马尔可夫过程也存在,我们将在本章稍后讨论特定的实例。
马尔可夫链在数学上相对容易研究,并且可以通过数值方法进行模拟。在这个案例中,我们将模拟一个简单的马尔可夫链,模拟种群的演化。
如何执行...
-
让我们导入 NumPy 和 matplotlib:
In [1]: import numpy as np import matplotlib.pyplot as plt %matplotlib inline
-
我们考虑一个最大只能包含N=100个个体的种群,并定义出生和死亡率:
In [2]: N = 100 # maximum population size a = 0.5/N # birth rate b = 0.5/N # death rate
-
我们在有限空间{0, 1, ..., N}上模拟一个马尔可夫链。每个状态代表一个种群大小。
x
向量将在每个时间步包含种群大小。我们将初始状态设置为x[0]=25(即初始化时种群中有 25 个个体):In [3]: nsteps = 1000 x = np.zeros(nsteps) x[0] = 25
-
现在我们模拟我们的链。在每个时间步t,有一个以ax[t]的概率出生,并且独立地,有一个以bx[t]的概率死亡。这些概率与当时的种群大小成正比。如果种群大小达到0或N,演化停止:
In [4]: for t in range(nsteps - 1): if 0 < x[t] < N-1: # Is there a birth? birth = np.random.rand() <= a*x[t] # Is there a death? death = np.random.rand() <= b*x[t] # We update the population size. x[t+1] = x[t] + 1*birth - 1*death # The evolution stops if we reach 0 or N. else: x[t+1] = x[t]
-
让我们看看种群大小的演变:
In [5]: plt.plot(x)
我们看到,在每个时间步,种群大小可以保持稳定,增加,或者减少 1。
-
现在,我们将模拟多个独立的马尔可夫链试验。我们可以用循环运行之前的仿真,但那样会非常慢(两个嵌套的
for
循环)。相反,我们通过一次性考虑所有独立的试验来向量化仿真。这里只有一个关于时间的循环。在每个时间步,我们用向量化操作同时更新所有试验的结果。x
向量现在包含所有试验在特定时间的种群大小。在初始化时,种群大小被设置为介于0和N之间的随机数:In [6]: ntrials = 100 x = np.random.randint(size=ntrials, low=0, high=N)
-
我们定义一个执行仿真的函数。在每个时间步,我们通过生成随机向量来找到经历出生和死亡的试验,并通过向量操作更新种群大小:
In [7]: def simulate(x, nsteps): """Run the simulation.""" for _ in range(nsteps - 1): # Which trials to update? upd = (0 < x) & (x < N-1) # In which trials do births occur? birth = 1*(np.random.rand(ntrials) <= a*x) # In which trials do deaths occur? death = 1*(np.random.rand(ntrials) <= b*x) # We update the population size for all # trials. x[upd] += birth[upd] - death[upd]
-
现在,让我们来看一下在不同时间点的种群大小的直方图。这些直方图代表了马尔可夫链的概率分布,通过独立试验(蒙特卡洛方法)进行估计:
In [8]: bins = np.linspace(0, N, 25) In [9]: nsteps_list = [10, 1000, 10000] for i, nsteps in enumerate(nsteps_list): plt.subplot(1, len(nsteps_list), i + 1) simulate(x, nsteps) plt.hist(x, bins=bins) plt.xlabel("Population size") if i == 0: plt.ylabel("Histogram") plt.title("{0:d} time steps".format(nsteps))
而且,最初,种群大小看起来在0和N之间均匀分布,但经过足够长的时间后,它们似乎会收敛到0或N。这是因为0和N是吸收状态;一旦到达这些状态,链就无法离开这些状态。而且,这些状态可以从任何其他状态到达。
它是如何工作的...
从数学上讲,一个离散时间马尔可夫链在空间E上是一个随机变量序列X[1], X[2], ...,它满足马尔可夫性质:
一个(平稳的)马尔可夫链的特点是转移概率 P(X[j] | X[i])。这些值构成一个矩阵,称为转移矩阵。这个矩阵是一个有向图的邻接矩阵,称为状态图。每个节点是一个状态,如果链条在这些节点之间有非零转移概率,则节点 i 会连接到节点 j。
还有更多内容...
在 Python 中模拟单个马尔可夫链效率并不高,因为我们需要使用 for
循环。然而,通过向量化和并行化,可以高效地模拟许多独立的链条,这些链条遵循相同的过程(所有任务都是独立的,因此该问题是令人尴尬地并行的)。当我们关注链条的统计性质时,这种方法非常有用(例如蒙特卡罗方法的例子)。
关于马尔可夫链的文献非常广泛。许多理论结果可以通过线性代数和概率论来建立。你可以在维基百科上找到相关参考文献和教科书。
离散时间马尔可夫链有很多推广。马尔可夫链可以定义在无限状态空间上,或者在连续时间上定义。此外,马尔可夫性质在广泛的随机过程类别中非常重要。
以下是一些参考文献:
-
维基百科上的马尔可夫链,网址为
en.wikipedia.org/wiki/Markov_chain
-
吸收马尔可夫链,维基百科上有相关介绍,网址为
en.wikipedia.org/wiki/Absorbing_Markov_chain
-
蒙特卡罗方法,维基百科上有相关介绍,网址为
en.wikipedia.org/wiki/Monte_Carlo_method
另见
- 模拟布朗运动 示例
模拟泊松过程
泊松过程是一种特定类型的点过程,它是一种随机模型,表示瞬时事件的随机发生。大致来说,泊松过程是最不结构化或最随机的点过程。
泊松过程是一个特定的连续时间马尔可夫过程。
点过程,特别是泊松过程,可以用来模拟随机瞬时事件,比如客户在队列或服务器中的到达、电话呼叫、放射性衰变、神经细胞的动作电位等许多现象。
在这个示例中,我们将展示模拟齐次平稳泊松过程的不同方法。
如何实现...
-
让我们导入 NumPy 和 matplotlib:
In [1]: import numpy as np import matplotlib.pyplot as plt %matplotlib inline
-
让我们指定
rate
值,即每秒钟发生事件的平均次数:In [2]: rate = 20\. # average number of events per second
-
首先,我们将使用 1 毫秒的小时间单元模拟这个过程:
In [3]: dt = .001 # time step n = int(1./dt) # number of time steps
-
在每个时间区间,事件发生的概率大约是 rate * dt,如果 dt 足够小。此外,由于泊松过程没有记忆性,事件的发生在不同的区间之间是独立的。因此,我们可以在向量化的方式中采样伯努利随机变量(分别表示实验的成功或失败,即 1 或 0),以此来模拟我们的过程:
In [4]: x = np.zeros(n) x[np.random.rand(n) <= rate*dt] = 1
x
向量包含了所有时间区间上的零和一,1
表示事件的发生:In [5]: x[:5] Out[5]: array([ 0., 1., 0., 0., 0\. ])
-
我们来展示模拟过程。我们为每个事件绘制一条垂直线:
In [6]: plt.vlines(np.nonzero(x)[0], 0, 1) plt.xticks([]); plt.yticks([])
-
表示同一对象的另一种方式是考虑相关的 计数过程 N(t),它表示在时间 t 之前发生的事件数。在这里,我们可以使用
cumsum()
函数来展示这个过程:In [7]: plt.plot(np.linspace(0., 1., n), np.cumsum(x)) plt.xlabel("Time") plt.ylabel("Counting process")
-
模拟均匀泊松过程的另一种(更高效的)方式是利用两个连续事件之间的时间间隔遵循指数分布这一性质。此外,这些间隔是独立的。因此,我们可以以向量化的方式对它们进行采样。最终,通过累加这些间隔,我们得到了我们的过程:
In [8]: y = np.cumsum(np.random.exponential( 1./rate, size=int(rate)))
y
向量包含了我们的泊松过程的另一个实现,但数据结构不同。向量的每个分量都是一个事件发生的时间:In [9]: y[:5] Out[9]: array([ 0.006, 0.111, 0.244, 0.367, 0.365])
-
最后,我们来展示模拟过程:
In [10]: plt.vlines(y, 0, 1) plt.xticks([]); plt.yticks([])
它是如何工作的...
对于一个速率为 的泊松过程,长度为
的时间窗口内的事件数服从泊松分布:
当 很小时,我们可以证明,按照一阶近似,这个概率大约是
。
此外,保持时间(两个连续事件之间的延迟)是独立的,并且遵循指数分布。泊松过程满足其他有用的性质,如独立和稳定增量。这一性质证明了本方法中使用的第一次模拟方法。
还有更多...
在这个示例中,我们只考虑了均匀时间相关的泊松过程。其他类型的泊松过程包括不均匀(或非均匀)过程,其特点是速率随时间变化,以及多维空间泊松过程。
这里有更多的参考资料:
-
维基百科上的泊松过程,链接:
en.wikipedia.org/wiki/Poisson_process
-
维基百科上的点过程,链接:
en.wikipedia.org/wiki/Point_process
-
维基百科上的连续时间过程,链接:
en.wikipedia.org/wiki/Continuous-time_process
-
维基百科上的更新理论,可在
en.wikipedia.org/wiki/Renewal_theory
中查看 -
维基百科上的空间泊松过程,可在
en.wikipedia.org/wiki/
Spatial_Poisson_process中查看
另请参见
- 模拟离散时间马尔可夫链方法
模拟布朗运动
布朗运动(或维纳过程)是数学、物理学以及许多其他科学和工程学科中的基本对象。该模型描述了粒子在流体中由于与流体中快速分子发生随机碰撞而产生的运动(扩散)。更一般地,布朗运动模型描述了一种连续时间随机游走,其中粒子通过在所有方向上做独立的随机步骤在空间中演化。
从数学角度来看,布朗运动是一个特殊的马尔可夫连续随机过程。布朗运动是随机微积分和随机过程理论等数学领域的核心,但在量化金融、生态学和神经科学等应用领域也占有重要地位。
在这个方法中,我们将展示如何在二维空间中模拟和绘制布朗运动。
如何实现...
-
让我们导入 NumPy 和 matplotlib:
In [1]: import numpy as np import matplotlib.pyplot as plt %matplotlib inline
-
我们用 5000 个时间步骤来模拟布朗运动:
In [2]: n = 5000
-
我们模拟两个独立的一维布朗过程来形成一个二维布朗过程。(离散)布朗运动在每个时间步骤上做独立的高斯跳跃。因此,我们只需要计算独立正态随机变量的累积和(每个时间步骤一个):
In [3]: x = np.cumsum(np.random.randn(n)) y = np.cumsum(np.random.randn(n))
-
现在,为了展示布朗运动,我们可以直接使用
plot(x, y)
。然而,这样的结果会是单色的,稍显乏味。我们希望使用渐变色来展示运动随时间的进展(色调是时间的函数)。matplotlib 迫使我们使用基于scatter()
的小技巧。这个函数允许我们为每个点分配不同的颜色,代价是丢失了点与点之间的线段。为了解决这个问题,我们线性插值这个过程,以便呈现出连续线条的错觉:In [4]: k = 10 # We add 10 intermediary points between two # successive points. # We interpolate x and y. x2 = np.interp(np.arange(n*k), np.arange(n)*k, x) y2 = np.interp(np.arange(n*k), np.arange(n)*k, y) In [5]: # Now, we draw our points with a gradient of # colors. plt.scatter(x2, y2, c=range(n*k), linewidths=0, marker='o', s=3, cmap=plt.cm.jet) plt.axis('equal') plt.xticks([]); plt.yticks([])
如何实现...
布朗运动W(t)具有几个重要的性质。首先,它几乎肯定会产生连续的轨迹。其次,它的增量在不重叠的区间上是独立的。第三,这些增量是高斯随机变量。更准确地说:
特别地,W(t)的密度是一个方差为t的正态分布。
此外,布朗运动以及一般的随机过程与偏微分方程有着深刻的联系。在这里,W(t) 的密度是 热方程 的解,这是一种特殊的扩散方程。更一般地说,Fokker-Planck 方程 是由随机微分方程的解的密度所满足的偏微分方程。
还有更多...
布朗运动是具有无限小步长的随机游走的极限。在这里,我们利用这一特性来模拟该过程。
这里有一些参考文献:
-
在
en.wikipedia.org/wiki/Brownian_motion
上描述了布朗运动(物理现象) -
在
en.wikipedia.org/wiki/Wiener_process
上解释了维纳过程(数学对象) -
布朗运动是 Lévy 过程的一种特例;参见
en.wikipedia.org/wiki/L%C3%A9vy_process
-
Fokker-Planck 方程将随机过程与偏微分方程联系起来;参见
en.wikipedia.org/wiki/Fokker%E2%80%93Planck_equation
另见
- 模拟随机微分方程的步骤
模拟随机微分方程
随机微分方程(SDEs)模拟受噪声影响的动态系统。它们广泛应用于物理学、生物学、金融学以及其他学科。
在本步骤中,我们模拟一个 Ornstein-Uhlenbeck 过程,它是 Langevin 方程的解。该模型描述了在摩擦作用下粒子在流体中的随机演化。粒子的运动是由于与流体分子发生碰撞(扩散)。与布朗运动的区别在于存在摩擦。
Ornstein-Uhlenbeck 过程是平稳的、高斯的和马尔可夫的,这使得它成为表示平稳随机噪声的良好候选模型。
我们将通过一种叫做 Euler-Maruyama 方法 的数值方法来模拟这个过程。它是对常微分方程(ODE)中欧拉方法的一个简单推广,适用于随机微分方程(SDE)。
如何做...
-
让我们导入 NumPy 和 matplotlib:
In [1]: import numpy as np import matplotlib.pyplot as plt %matplotlib inline
-
我们为模型定义了一些参数:
In [2]: sigma = 1\. # Standard deviation. mu = 10.0 # Mean. tau = 0.05 # Time constant.
-
让我们定义一些模拟参数:
In [3]: dt = 0.001 # Time step. T = 1.0 # Total time. n = int(T/dt) # Number of time steps. t = np.linspace(0., T, n) # Vector of times.
-
我们还定义了重正规化变量(以避免在每个时间步长重新计算这些常数):
In [4]: sigma_bis = sigma * np.sqrt(2\. / tau) sqrtdt = np.sqrt(dt)
-
我们创建一个向量,在模拟过程中包含所有连续的过程值:
In [5]: x = np.zeros(n)
-
现在,让我们使用 Euler-Maruyama 方法来模拟该过程。它实际上是常规欧拉法在常微分方程(ODE)中的一种推广,但增加了一个额外的随机项(它仅仅是一个缩放的标准正态随机变量)。我们将在 如何工作... 部分给出该过程的方程和该方法的详细信息:
In [6]: for i in range(n-1): x[i+1] = x[i] + dt*(-(x[i]-mu)/tau) + \ sigma_bis * sqrtdt * np.random.randn()
-
让我们展示过程的演变:
In [7]: plt.plot(t, x)
-
现在,我们将查看该过程分布随时间演化的情况。为此,我们将以向量化方式模拟该过程的多个独立实现。我们定义一个向量
X
,它将包含在给定时间点所有实现的过程(也就是说,我们不会在所有时间点都将所有实现保留在内存中)。该向量将在每个时间步覆盖。我们将在多个时间点展示估计的分布(直方图):In [8]: ntrials = 10000 X = np.zeros(ntrials) In [9]: # We create bins for the histograms. bins = np.linspace(-2., 14., 50); for i in range(n): # We update the process independently for all # trials. X += dt*(-(X-mu)/tau) + \ sigma_bis*sqrtdt*np.random.randn(ntrials) # We display the histogram for a few points in # time. if i in (5, 50, 900): hist, _ = np.histogram(X, bins=bins) plt.plot((bins[1:]+bins[:-1])/2, hist, label="t={0:.2f}".format(i*dt)) plt.legend()
该过程的分布趋近于均值为
和标准差为
的高斯分布。如果初始分布也是具有适当参数的高斯分布,那么该过程将是平稳的。
它是如何工作的...
我们在这个实例中使用的 Langevin 方程是以下随机微分方程:
在这里,x(t) 是我们的随机过程,dx 是无穷小增量, 是均值,
是标准差,
是时间常数。此外,W 是布朗运动(或维纳过程),它是我们随机微分方程的基础。
右边的第一个项是确定性项(在 dt 中),而第二个项是随机项。如果没有最后一个项,该方程将是一个常规的确定性常微分方程。
布朗运动的无穷小步长是一个高斯随机变量。具体来说,布朗运动的导数(以某种意义上)是 白噪声,即一系列独立的高斯随机变量。
欧拉-马鲁亚马方法涉及时间离散化,并在每个时间步向过程添加无穷小步长。该方法包含一个确定性项(如常规欧拉方法在常微分方程中的应用)和一个随机项(随机高斯变量)。具体来说,对于一个方程:
数值方案是(其中 t=n * dt):
在这里, 是一个方差为 1 的随机高斯变量(在每个时间步独立)。归一化因子
来源于布朗运动的无穷小步长,其标准差为
。
还有更多...
随机微分方程的数学理论包括随机微积分、伊藤微积分、马尔可夫过程等主题。尽管这些理论相当复杂,但正如我们在这个实例中看到的那样,数值模拟随机过程相对简单。
欧拉-马鲁亚马方法的误差是 dt 量级的。Milstein 方法是一种更精确的数值方案,误差量级为 dt。
这里有一些相关参考资料:
-
维基百科上的随机微分方程
-
维基百科上的朗之万方程
-
奥恩斯坦-乌伦贝克过程的描述
-
欧拉-马鲁亚马方法,解释于
en.wikipedia.org/wiki/Euler%E2%80%93Maruyama_method
-
维基百科上的米尔斯坦方法
另请参见
- 模拟布朗运动的方案
第十四章:图、几何学和地理信息系统
在本章中,我们将涵盖以下主题:
-
使用 NetworkX 操作和可视化图
-
使用 NetworkX 分析社交网络
-
使用拓扑排序解决有向无环图中的依赖关系
-
计算图像中的连通分量
-
计算一组点的沃罗诺伊图
-
使用 Shapely 和 basemap 操作地理空间数据
-
为路网创建路径规划器
介绍
在本章中,我们将涵盖 Python 在图论、几何学和地理学方面的能力。
图是描述物体之间关系的数学对象。它们在科学和工程中无处不在,因为它们可以表示许多现实世界的关系:社交网络中的朋友、分子中的原子、网站链接、神经网络中的细胞、图像中的相邻像素等。图还是计算机科学中的经典数据结构。最后,许多领域特定的问题可以重新表达为图论问题,然后使用著名的算法来解决。
我们还将看到一些与几何和地理信息系统(GIS)相关的内容,GIS 指的是对任何类型的空间、地理或地形数据的处理和分析。
在本介绍中,我们将简要概述这些主题。
图
从数学角度来看,图 G = (V, E) 由一组顶点或节点 V 和一组边 E(V 的二元子集)定义。若 (v, v') 是一条边(E 的元素),则称两个节点 v 和 v' 是连接的。
-
如果边是无序的(意味着 (v,v') = (v',v)),则图被称为无向图
-
如果边是有序的(意味着 (v,v') ≠ (v',v)),则图被称为有向图
在无向图中,边由连接两个节点的线段表示。在有向图中,它由箭头表示。
无向图和有向图
图可以通过不同的数据结构表示,特别是邻接表(每个顶点的邻接顶点列表)或邻接矩阵(顶点之间连接的矩阵)。
图论中的问题
这里有几个经典的图论问题的示例:
-
图遍历:如何遍历图,详细讨论见
en.wikipedia.org/wiki/Graph_traversal
-
图着色:如何给图中的节点着色,使得相邻的两个顶点不共享相同的颜色,详细讨论见
en.wikipedia.org/wiki/Graph_coloring
-
连通分量:如何在图中找到连通分量,详细解释见
en.wikipedia.org/wiki/Connected_component_%28graph_theory%29
-
最短路径:在给定图中,从一个节点到另一个节点的最短路径是什么?讨论见
en.wikipedia.org/wiki/Shortest_path_problem
-
哈密尔顿路径:一个图是否包含哈密尔顿路径,访问每个顶点恰好一次?详细解释见
en.wikipedia.org/wiki/Hamiltonian_path
-
欧拉路径:一个图是否包含欧拉路径,访问每条 边 恰好一次?讨论见
en.wikipedia.org/wiki/Eulerian_path
-
旅行商问题:访问每个节点恰好一次(哈密尔顿路径)的最短路径是什么?详细解释见
en.wikipedia.org/wiki/Traveling_salesman_problem
随机图
随机图 是一种特殊的图,通过概率规则定义。它们有助于理解大规模现实世界图的结构,如社交图。
特别地,小世界网络具有稀疏的连接,但大多数节点可以在少数几步内从其他任何节点到达。这一特性源于少数几个具有大量连接的 枢纽 的存在。
Python 中的图
尽管可以使用原生的 Python 结构操作图,但使用专门的库来实现特定数据结构和操作例程会更方便。在本章中,我们将使用 NetworkX,一个纯 Python 库。其他 Python 库包括 python-graph 和 graph-tool(主要用 C++ 编写)。
NetworkX 实现了一个灵活的图数据结构,并包含许多算法。NetworkX 还允许我们使用 matplotlib 轻松绘制图。
Python 中的几何学
Shapely 是一个 Python 库,用于处理二维几何图形,如点、线和多边形。它在地理信息系统中尤其有用。
将 Shapely 与 matplotlib 结合并非易事。幸运的是,descartes 包使这个任务变得更加简单。
Python 中的地理信息系统(GIS)
有几个 Python 模块用于处理地理数据和绘制地图。
在本章中,我们将使用 matplotlib 的 basemap、Shapely、descartes 和 Fiona 来处理 GIS 文件。
ESRI Shapefile 是一种流行的地理空间矢量数据格式。它可以被 basemap、NetworkX 和 Fiona 读取。
我们还将使用 OpenStreetMap 服务,这是一个免费的开源协作服务,提供全球地图。
本章未涉及的其他 Python 中的 GIS/地图系统包括 GeoPandas、Kartograph、Vincent 和 cartopy。
参考资料
以下是关于图的一些参考资料:
-
维基百科中的图论,访问
en.wikipedia.org/wiki/Graph_theory
-
描述图的数据结构,请参考
en.wikipedia.org/wiki/Graph_(abstract_data_type)
-
可在维基百科上查看随机图的页面,网址为
en.wikipedia.org/wiki/Random_graph
-
可在维基百科上查看小世界图的页面,网址为
en.wikipedia.org/wiki/Small-world_network
-
NetworkX 软件包的网址为
networkx.github.io
-
python-graph 软件包的网址为
code.google.com/p/python-graph/
-
可以在
graph-tool.skewed.de
获取 graph-tool 软件包。
以下是关于 Python 中几何和地图的一些参考资料:
-
Basemap 的网址为
matplotlib.org/basemap/
-
Shapely 的网址为
toblerity.org/shapely/project.html
-
Fiona 的网址为
toblerity.org/fiona/
-
descartes 的网址为
pypi.python.org/pypi/descartes
-
Shapefile 的网址为
en.wikipedia.org/wiki/Shapefile
-
OpenStreetMap 的网址为www.openstreetmap.org
-
Folium 的网址为
github.com/wrobstory/folium
-
GeoPandas 的网址为
geopandas.org
-
Kartograph 的网址为
kartograph.org
-
Cartopy 的网址为
scitools.org.uk/cartopy/
-
Vincent 的网址为
github.com/wrobstory/vincent
使用 NetworkX 操作和可视化图形
在这个示例中,我们将展示如何使用 NetworkX 创建、操作和可视化图形。
准备工作
您可以在官方文档中找到 NetworkX 的安装说明,网址为networkx.github.io/documentation/latest/install.html
。
使用 Anaconda,您可以在终端中输入conda install networkx
。或者,您也可以输入pip install networkx
。在 Windows 上,您还可以使用 Chris Gohlke 的安装程序,网址为www.lfd.uci.edu/~gohlke/pythonlibs/#networkx。
如何做…
-
让我们导入 NumPy、NetworkX 和 matplotlib:
In [1]: import numpy as np import networkx as nx import matplotlib.pyplot as plt %matplotlib inline
-
创建图形的方法有很多种。在这里,我们创建了一个边的列表(节点索引的对):
In [2]: n = 10 # Number of nodes in the graph. # Each node is connected to the two next nodes, # in a circular fashion. adj = [(i, (i+1)%n) for i in range(n)] adj += [(i, (i+2)%n) for i in range(n)]
-
我们使用边的列表实例化一个
Graph
对象:In [3]: g = nx.Graph(adj)
-
让我们检查图的节点和边的列表,以及其邻接矩阵:
In [4]: print(g.nodes()) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] In [5]: print(g.edges()) [(0, 8), (0, 1), (0, 2), ..., (7, 9), (8, 9)] In [6]: print(nx.adjacency_matrix(g)) [[ 0\. 1\. 1\. 0\. 0\. 0\. 0\. 0\. 1\. 1.] [ 1\. 0\. 1\. 1\. 0\. 0\. 0\. 0\. 0\. 1.] ... [ 1\. 0\. 0\. 0\. 0\. 0\. 1\. 1\. 0\. 1.] [ 1\. 1\. 0\. 0\. 0\. 0\. 0\. 1\. 1\. 0.]]
-
让我们显示这个图。NetworkX 自带各种绘图函数。我们可以明确指定节点的位置,也可以使用算法自动计算一个有趣的布局。在这里,我们使用了
draw_circular()
函数,它会将节点线性地放置在一个圆上:In [7]: nx.draw_circular(g)
-
图可以很容易地修改。在这里,我们添加一个连接到所有现有节点的新节点。我们还为此节点指定了一个
color
属性。在 NetworkX 中,每个节点和边都带有一个方便的 Python 字典,包含任意属性。In [8]: g.add_node(n, color='#fcff00') # We add an edge from every existing # node to the new node. for i in range(n): g.add_edge(i, n)
-
现在,让我们再次绘制修改后的图。这次,我们明确指定节点的位置和颜色:
In [9]: # We define custom node positions on a circle # except the last node which is at the center. t = np.linspace(0., 2*np.pi, n) pos = np.zeros((n+1, 2)) pos[:n,0] = np.cos(t) pos[:n,1] = np.sin(t) # A node's color is specified by its 'color' # attribute, or a default color if this attribute # doesn't exist. color = [g.node[i].get('color', '#88b0f3') for i in range(n+1)] # We now draw the graph with matplotlib. nx.draw_networkx(g, pos=pos, node_color=color) plt.axis('off')
-
让我们也使用自动布局算法:
In [10]: nx.draw_spectral(g, node_color=color) plt.axis('off')
还有更多…
在 NetworkX 中,节点不一定是整数。它们可以是数字、字符串、元组和任何可哈希的 Python 类的实例。
此外,每个节点和边都带有可选属性(形成一个字典)。
在 NetworkX 中实现了一些布局算法。draw_spectral()
函数使用图的拉普拉斯矩阵的特征向量。
draw_spring()
函数实现了Fruchterman-Reingold 力导向算法。节点被视为受边缘相关力的质点。力导向图绘制算法通过最小化系统的能量来找到平衡配置。这将导致一个美观的布局,尽可能少地交叉边。
这里有一些参考资料:
-
维基百科上的拉普拉斯矩阵,网址为
en.wikipedia.org/wiki/Laplacian_matrix
-
描述在
en.wikipedia.org/wiki/Force-directed_graph_drawing
的力导向图绘制
另请参阅
- 使用 NetworkX 分析社交网络配方
使用 NetworkX 分析社交网络
在这个配方中,我们将展示如何在 Python 中分析社交数据。社交数据是由人们在社交网络上的活动生成的,如 Facebook、Twitter、Google+、GitHub 等。
在这个配方中,我们将使用 NetworkX 分析和可视化 Twitter 用户的社交网络。
准备工作
首先,您需要安装Twitter Python 包。您可以使用pip install twitter
进行安装。您可以在pypi.python.org/pypi/twitter
找到更多信息。
然后,您需要获取认证代码以访问您的 Twitter 数据。该过程是免费的。除了 Twitter 账号外,您还需要在 Twitter 开发者网站上创建一个应用程序,网址为dev.twitter.com/apps
。然后,您将能够检索到此配方所需的OAuth 认证代码。
您需要在当前文件夹中创建一个twitter.txt
文本文件,其中包含四个私有认证密钥。每行必须有一个密钥,顺序如下:
-
API 密钥
-
API 秘钥
-
访问令牌
-
访问令牌密钥
请注意,对 Twitter API 的访问是有限制的。大多数方法在给定时间窗口内只能调用几次。除非您研究小网络或查看大网络的小部分,否则您将需要节流您的请求。在这个示例中,我们只考虑网络的一小部分,因此不应该达到 API 限制。否则,您将需要等待几分钟,直到下一个时间窗口开始。API 限制可在dev.twitter.com/docs/rate-limiting/1.1/limits
查看。
如何做…
-
让我们导入一些包:
In [1]: import math import json import twitter import numpy as np import pandas as pd import networkx as nx import matplotlib.pyplot as plt %matplotlib inline from IPython.display import Image
-
我们从我们的
twitter.txt
文件中获取秘密的消费者和 OAuth 密钥:In [2]: (CONSUMER_KEY, CONSUMER_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) = open( 'twitter.txt', 'r').read().splitlines()
-
现在我们创建一个
Twitter
类的实例,这将使我们能够访问 Twitter API:In [3]: auth = twitter.oauth.OAuth(OAUTH_TOKEN, OAUTH_TOKEN_SECRET, CONSUMER_KEY, CONSUMER_SECRET) tw = twitter.Twitter(auth=auth)
-
在这个示例中,我们使用 Twitter API 的 1.1 版本。
twitter
库在Twitter
实例的属性之间定义了 REST API 和直接映射。在这里,我们执行account/verify_credentials
REST 请求来获取认证用户的标识符(这里是我,或者如果您自己执行这个笔记本,则是您!):In [4]: me = tw.account.verify_credentials() In [5]: myid = me['id']
-
让我们定义一个简单的函数,返回给定用户(默认为认证用户)的所有关注者的标识符:
In [6]: def get_followers_ids(uid=None): # Retrieve the list of followers' ids of the # specified user. return tw.followers.ids(user_id=uid)['ids'] In [7]: # We get the list of my followers. my_followers_ids = get_followers_ids()
-
现在,我们定义一个函数,用于检索 Twitter 用户的完整资料。由于
users/lookup
批量请求每次限制为 100 个用户,并且在时间窗口内只允许少量调用,我们只查看所有关注者的一个子集:In [8]: def get_users_info(users_ids, max=500): n = min(max, len(users_ids)) # Get information about those users, # using batch requests. users = [tw.users.lookup( user_id=users_ids[100*i:100*(i+1)]) for i in range(int(math.ceil(n/100.)))] # We flatten this list of lists. users = [item for sublist in users for item in sublist] return {user['id']: user for user in users} In [9]: users_info = get_users_info(my_followers_ids) In [10]: # Let's save this dictionary on the disk. with open('my_followers.json', 'w') as f: json.dump(users_info, f, indent=1)
-
我们还开始用关注者定义图形,使用邻接表(技术上来说,是一个列表的字典)。这被称为自我图。这个图表示我们的关注者之间的所有关注连接:
In [11]: adjacency = {myid: my_followers_ids}
-
现在,我们将查看与 Python 相关的自我图部分。具体来说,我们将考虑包含“Python”描述的前 10 位最受关注用户的关注者:
In [12]: my_followers_python = \[user for user in users_info.values() if 'python' in user['description'].lower()] In [13]: my_followers_python_best = \ sorted(my_followers_python, key=lambda u: u['followers_count'])[::-1][:10]
检索给定用户的关注者的请求受到速率限制。让我们来看看我们还剩下多少次调用:
In [14]: tw.application.rate_limit_status( resources='followers') \ ['resources']['followers']['/followers/ids'] Out[14]: {u'limit': 15, u'remaining': 0, u'reset': 1388865551} In [15]: for user in my_followers_python_best: # The call to get_followers_ids is # rate-limited. adjacency[user['id']] = list(set( get_followers_ids(user['id'])). \ intersection(my_followers_ids))
-
现在我们的图被定义为字典中的邻接表,我们将在 NetworkX 中加载它:
In [16]: g = nx.Graph(adjacency) In [17]: # We only restrict the graph to the users # for which we were able to retrieve the profile. g = g.subgraph(users_info.keys()) In [18]: # We also save this graph on disk. with open('my_graph.json', 'w') as f: json.dump(nx.to_dict_of_lists(g), f, indent=1) In [19]: # We remove isolated nodes for simplicity. g.remove_nodes_from([k for k, d in g.degree().items() if d <= 1]) In [20]: # Since I am connected to all nodes, # by definition, we can remove me for simplicity. g.remove_nodes_from([myid])
-
让我们看一下图的统计数据:
In [21]: len(g.nodes()), len(g.edges()) Out[21]: (197, 1037)
-
现在我们将绘制这个图。我们将根据每个用户的关注者数量和推文数量使用不同的大小和颜色来绘制节点。最受关注的用户将更大。最活跃的用户将更红。
In [22]: # Update the dictionary. deg = g.degree() for user in users_info.values(): fc = user['followers_count'] sc = user['statuses_count'] # Is this user a Pythonista? user['python'] = 'python' in \ user['description'].lower() # We compute the node size as a function of # the number of followers. user['node_size'] = math.sqrt(1 + 10 * fc) # The color is function of its activity user['node_color'] = 10 * math.sqrt(1 + sc) # We only display the name of the most # followed users. user['label'] = user['screen_name'] \ if fc > 2000 else ''
-
最后,我们使用
draw()
函数来显示图形。我们需要将节点的大小和颜色指定为列表,标签指定为字典:In [23]: node_size = [users_info[uid]['node_size'] for uid in g.nodes()] In [24]: node_color = [users_info[uid]['node_color'] for uid in g.nodes()] In [25]: labels = {uid: users_info[uid]['label'] for uid in g.nodes()} In [26]: nx.draw(g, cmap=plt.cm.OrRd, alpha=.8, node_size=node_size, node_color=node_color, labels=labels, font_size=4, width=.1)
还有更多…
一本关于使用 Python 进行社交数据分析的重要参考资料是 Matthew A. Russel 的书籍《Mining the Social Web》,由O'Reilly Media出版。 代码可以在 GitHub 上的 IPython 笔记本中找到,网址为github.com/ptwobrussell/Mining-the-Social-Web-2nd-Edition
。 以下网络被涵盖:Twitter、Facebook、LinkedIn、Google+、GitHub、邮箱、网站等。
参见
- 使用 NetworkX 操纵和可视化图形示例
使用拓扑排序在有向无环图中解析依赖关系
在这个示例中,我们将展示一个著名的图算法应用:拓扑排序。 让我们考虑描述项目之间依赖关系的有向图。 例如,在软件包管理器中,在安装给定软件包P之前,我们可能需要安装依赖软件包。
依赖关系集合形成一个有向图。 通过拓扑排序,软件包管理器可以解析依赖关系并找到软件包的正确安装顺序。
拓扑排序有许多其他应用。 在这里,我们将在 Debian 软件包管理器的真实数据上说明这个概念。 我们将找到 IPython 所需软件包的安装顺序。
准备工作
您需要python-apt
软件包才能构建软件包依赖关系图。 该软件包可在pypi.python.org/pypi/python-apt/
上找到。
我们还假设此笔记本在 Debian 系统(如 Ubuntu)上执行。 如果您没有这样的系统,可以直接从书的 GitHub 存储库github.com/ipython-books/cookbook-data
下载Debian数据集。 将其解压缩到当前目录,并直接从此笔记本的第 7 步开始。
如何做…
-
我们导入
apt
模块并构建软件包列表:In [1]: import json import apt cache = apt.Cache()
-
graph
字典将包含依赖关系图的一小部分的邻接表:In [2]: graph = {}
-
我们定义一个返回软件包依赖关系列表的函数:
In [3]: def get_dependencies(package): if package not in cache: return [] pack = cache[package] ver = pack.candidate or pack.versions[0] # We flatten the list of dependencies, # and we remove the duplicates. return sorted(set([item.name for sublist in ver.dependencies for item in sublist]))
-
现在我们定义一个递归函数,用于构建特定软件包的依赖关系图。 此函数更新
graph
变量:In [4]: def get_dep_recursive(package): if package not in cache: return [] if package not in graph: dep = get_dependencies(package) graph[package] = dep for dep in graph[package]: if dep not in graph: graph[dep] = get_dep_recursive(dep) return graph[package]
-
让我们为 IPython 构建依赖关系图:
In [5]: get_dep_recursive('ipython')
-
最后,我们将邻接表保存为 JSON:
In [6]: with open('data/apt.json', 'w') as f: json.dump(graph, f, indent=1)
提示
如果您没有 Debian 操作系统,请从书籍的存储库中下载Debian数据集。
-
我们导入一些包:
In [7]: import json import numpy as np import networkx as nx import matplotlib.pyplot as plt %matplotlib inline
-
让我们从 JSON 文件中加载邻接表:
In [8]: with open('data/apt.json', 'r') as f: graph = json.load(f)
-
现在,我们从邻接表创建一个有向图(NetworkX 中的
DiGraph
)。 我们反转图以获得更自然的顺序:In [9]: g = nx.DiGraph(graph).reverse()
-
当图形是有向无环图(DAG)时才存在拓扑排序。 这意味着图中没有循环,也就是说,没有循环依赖。 我们的图是 DAG 吗? 让我们看看:
In [10]: nx.is_directed_acyclic_graph(g) Out[10]: False
-
哪些软件包负责循环? 我们可以使用
simple_cycles()
函数找到它们:In [11]: set([cycle[0] for cycle in nx.simple_cycles(g)]) Out[11]: {u'coreutils', u'libc6', u'multiarch-support', u'python-minimal', u'tzdata'}
-
在这里,我们可以尝试移除这些包。在实际的包管理器中,这些循环需要仔细考虑。
In [12]: g.remove_nodes_from(_) In [13]: nx.is_directed_acyclic_graph(g) Out[13]: True
-
现在,图是一个 DAG。让我们先展示它:
In [14]: ug = g.to_undirected() deg = ug.degree() In [15]: # The size of the nodes depends on the number # of dependencies. nx.draw(ug, font_size=6, node_size=[20*deg[k] for k in ug.nodes()])
-
最后,我们可以执行拓扑排序,从而获得一个满足所有依赖关系的线性安装顺序:
In [16]: nx.topological_sort(g) Out[16]: [u'libexpat1', u'libdb5.1', u'debconf-2.0', ... u'python-pexpect', u'python-configobj', u'ipython']
还有更多内容…
有向无环图在许多应用中都有出现。它们可以表示因果关系、影响图、依赖关系以及其他概念。例如,像 Git 这样的分布式版本控制系统的版本历史就是用 DAG 来描述的。
拓扑排序在一般的调度任务中非常有用(项目管理和指令调度)。
这里有一些参考资料:
-
维基百科上的拓扑排序,链接:
en.wikipedia.org/wiki/Topological_sorting
计算图像中的连通分量
在本食谱中,我们将展示图论在图像处理中的应用。我们将计算图像中的连通分量。这种方法将允许我们标记图像中的连续区域,类似于绘图程序中的桶形填充工具。
找到连通分量在许多益智类视频游戏中也很有用,如扫雷、泡泡龙等。在这些游戏中,需要自动检测同一颜色的连续物品集。
如何做到这一点…
-
让我们导入这些包:
In [1]: import itertools import numpy as np import networkx as nx import matplotlib.colors as col import matplotlib.pyplot as plt %matplotlib inline
-
我们创建一个10 x 10的图像,每个像素可以取三个可能的标签(或颜色)之一:
In [2]: n = 10 In [3]: img = np.random.randint(size=(n, n), low=0, high=3)
-
现在,我们创建一个底层的二维网格图,编码图像的结构。每个节点是一个像素,且一个节点与其最近的邻居相连。NetworkX 定义了一个
grid_2d_graph
函数来生成这个图:In [4]: g = nx.grid_2d_graph(n, n)
-
让我们创建两个函数来显示图像和相应的图形:
In [5]: def show_image(img, **kwargs): plt.imshow(img, origin='lower', interpolation='none', **kwargs) plt.axis('off') In [6]: def show_graph(g, **kwargs): nx.draw(g, pos={(i, j): (j, i) for (i, j) in g.nodes()}, node_color=[img[i, j] for (i, j) in g.nodes()], linewidths=1, edge_color='w', with_labels=False, node_size=30, **kwargs) In [7]: cmap = plt.cm.Blues
-
这是原始图像与底层图叠加后的效果:
In [8]: show_image(img, cmap=cmap, vmin=-1) show_graph(g, cmap=cmap, vmin=-1)
-
现在,我们将找出所有包含超过三个像素的连续深蓝色区域。首先,我们考虑对应所有深蓝色像素的子图:
In [9]: g2 = g.subgraph(zip(*np.nonzero(img==2))) In [10]: show_image(img, cmap=cmap, vmin=-1) show_graph(g2, cmap=cmap, vmin=-1)
-
我们看到,所请求的连续区域对应于包含超过三个节点的连通分量。我们可以使用 NetworkX 的
connected_components
函数来找到这些分量:In [11]: components = [np.array(comp) for comp in nx.connected_components(g2) if len(comp)>=3] len(components) Out[11]: 3
-
最后,我们为每个分量分配一个新颜色,并显示新的图像:
In [12]: # We copy the image, and assign a new label # to each found component. img_bis = img.copy() for i, comp in enumerate(components): img_bis[comp[:,0], comp[:,1]] = i + 3 In [13]: # We create a new discrete color map extending # the previous map with new colors. colors = [cmap(.5), cmap(.75), cmap(1.), '#f4f235', '#f4a535', '#f44b35'] cmap2 = col.ListedColormap(colors, 'indexed') In [14]: show_image(img_bis, cmap=cmap2)
它是如何工作的…
我们解决的问题被称为连通分量标记。它也与洪水填充算法密切相关。
将网格图与图像关联的想法在图像处理领域非常常见。在这里,连续的颜色区域对应于连通组件的子图。一个连通组件可以定义为可达性关系的等价类。如果两个节点之间有一条路径,则它们在图中是连通的。一个等价类包含可以互相到达的节点。
最后,这里描述的简单方法仅适用于小图像上的基本任务。更高级的算法将在第十一章,图像与音频处理中讲解。
还有更多…
这里有一些参考资料:
-
Wikipedia 上的连通组件,访问
en.wikipedia.org/wiki/Connected_component_%28graph_theory%29
-
Wikipedia 上的连通组件标记算法,访问
en.wikipedia.org/wiki/Connected-component_labeling
-
Wikipedia 上的 Flood-fill 算法,访问
en.wikipedia.org/wiki/Flood_fill
计算一组点的 Voronoi 图
一组种子点的Voronoi 图将空间划分为多个区域。每个区域包含所有离某个种子点比任何其他种子点更近的点。
Voronoi 图是计算几何中的基本结构。它广泛应用于计算机科学、机器人学、地理学和其他学科。例如,一组地铁站的 Voronoi 图可以告诉我们城市中任意一个点离哪个地铁站最近。
在这个示例中,我们使用 SciPy 计算巴黎地铁站集的 Voronoi 图。
准备工作
你需要 Smopy 模块来显示巴黎的 OpenStreetMap 地图。你可以使用pip install smopy
安装该包。
你还需要从书籍的 GitHub 仓库下载RATP数据集,网址为github.com/ipython-books/cookbook-data
,并将其解压到当前目录。数据来自 RATP 的开放数据网站(巴黎的公共交通运营商,data.ratp.fr
)。
如何实现…
-
让我们导入 NumPy、pandas、matplotlib 和 SciPy:
In [1]: import numpy as np import pandas as pd import scipy.spatial as spatial import matplotlib.pyplot as plt import matplotlib.path as path import matplotlib as mpl import smopy %matplotlib inline
-
让我们使用 pandas 加载数据集:
In [2]: df = pd.read_csv('data/ratp.csv', sep='#', header=None) In [3]: df[df.columns[1:]].tail(2) Out[3]: 1 2 3 4 5 11609 2.30 48.93 TIMBAUD GENNEVILLIERS tram 11610 2.23 48.91 VICTOR BASCH COLOMBES tram
-
DataFrame
对象包含站点的坐标、名称、城市、区域和类型。让我们选择所有地铁站:In [4]: metro = df[(df[5] == 'metro')] In [5]: metro[metro.columns[1:]].tail(3) Out[5]: 305 2.308 48.841 Volontaires PARIS-15EME metro 306 2.379 48.857 Voltaire PARIS-11EME metro 307 2.304 48.883 Wagram PARIS-17EME metro
-
我们将提取巴黎各地铁站的区域编号。使用 pandas 时,我们可以通过相应列的
str
属性进行向量化的字符串操作。In [6]: # We only extract the district from stations # in Paris. paris = metro[4].str.startswith('PARIS').values In [7]: # We create a vector of integers with the district # number of the corresponding station, or 0 # if the station is not in Paris. districts = np.zeros(len(paris), dtype=np.int32) districts[paris] = metro[4][paris].\ str.slice(6, 8).astype(np.int32) districts[~paris] = 0 ndistricts = districts.max() + 1
-
我们还提取所有地铁站的坐标:
In [8]: lon = metro[1] lat = metro[2]
-
现在,让我们使用 OpenStreetMap 获取巴黎的地图。我们通过所有地铁站的极端纬度和经度坐标来指定地图的边界。我们使用轻量级的 Smopy 模块来生成地图:
In [9]: box = (lat[paris].min(), lon[paris].min(), lat[paris].max(), lon[paris].max()) m = smopy.Map(box, z=12)
-
我们现在使用 SciPy 计算车站的 Voronoi 图。一个
Voronoi
对象通过点的坐标创建。它包含了我们将用于显示的几个属性:In [10]: vor = spatial.Voronoi(np.c_[lat, lon])
-
我们创建了一个通用函数来显示 Voronoi 图。SciPy 已经实现了这样一个函数,但该函数没有考虑无限点。我们将使用的实现来自 Stack Overflow,地址是
stackoverflow.com/a/20678647/1595060
。这个函数相对较长,我们不会在这里完全复制它。完整版本可以在书籍的 GitHub 仓库中找到。In [11]: def voronoi_finite_polygons_2d(vor, radius=None): """Reconstruct infinite Voronoi regions in a 2D diagram to finite regions.""" ...
-
voronoi_finite_polygons_2d()
函数返回一个区域列表和一个顶点列表。每个区域都是顶点索引的列表。所有顶点的坐标存储在vertices
中。从这些结构中,我们可以创建一个单元格列表。每个单元格表示一个多边形,作为顶点坐标的数组。我们还使用smopy.Map
实例的to_pixels()
方法。此函数将纬度和经度的地理坐标转换为图像中的像素。In [12]: regions, vertices = \ voronoi_finite_polygons_2d(vor) cells = [m.to_pixels(vertices[region]) for region in regions]
-
现在,我们计算每个多边形的颜色:
In [13]: cmap = plt.cm.Set3 # We generate colors for districts using # a color map. colors_districts = cmap( np.linspace(0., 1., ndistricts))[:,:3] # The color of every polygon, grey by default. colors = .25 * np.ones((len(districts), 3)) # We give each polygon in Paris the color of # its district. colors[paris] = colors_districts[districts[paris]]
-
最后,我们使用
Map
实例的show_mpl()
方法来显示带有 Voronoi 图的地图:In [14]: ax = m.show_mpl() ax.add_collection( mpl.collections.PolyCollection(cells, facecolors=colors, edgecolors='k', alpha=0.35,))
它是如何工作的…
让我们给出欧几里得空间中 Voronoi 图的数学定义。如果(x[i])是一个点集,那么这个点集的 Voronoi 图就是由以下定义的子集V[i](称为单元格或区域)的集合:
Voronoi 图的双重图是德劳内三角剖分。这个几何对象通过三角形覆盖了点集的凸包。
SciPy 使用Qhull(一个 C++计算几何库)来计算 Voronoi 图。
还有更多内容……
这里有更多参考资料:
-
维基百科上的 Voronoi 图,地址是
en.wikipedia.org/wiki/Voronoi_diagram
-
维基百科上的德劳内三角剖分,地址是
en.wikipedia.org/wiki/Delaunay_triangulation
-
scipy.spatial.voronoi
文档可在docs.scipy.org/doc/scipy-dev/reference/generated/scipy.spatial.Voronoi.html
找到 -
可在www.qhull.org获取 Qhull 库
另见
- 使用 Shapely 和 basemap 操作地理空间数据食谱
使用 Shapely 和 basemap 操作地理空间数据
在这个食谱中,我们将展示如何加载和显示 Shapefile 格式的地理数据。具体而言,我们将使用自然地球(www.naturalearthdata.com)的数据来显示非洲各国,并通过其人口和国内生产总值(GDP)进行着色。
Shapefile(en.wikipedia.org/wiki/Shapefile
)是一种广泛使用的地理空间矢量数据格式,适用于 GIS 软件。它可以通过Fiona读取,Fiona 是GDAL/OGR(一个支持 GIS 文件格式的 C++库)的 Python 封装库。我们还将使用Shapely,这是一个用于处理二维几何图形的 Python 包,以及descartes,它用于在 matplotlib 中渲染 Shapely 图形。最后,我们将使用basemap绘制地图。
准备工作
你需要以下包:
-
GDAL/OGR 可以在www.gdal.org/ogr/找到
-
Fiona 可以在
toblerity.org/fiona/README.html
找到 -
Shapely 可以在
toblerity.org/shapely/project.html
找到 -
descartes 可以在
pypi.python.org/pypi/descartes
找到 -
Basemap 可以在
matplotlib.org/basemap/
找到
使用 Anaconda,你可以执行以下操作:
conda install gdal
conda install fiona
conda install basemap
Shapely 和 descartes 可以通过以下命令安装:
pip install shapely
pip install descartes
在 Windows 系统中,除了 descartes,你可以在 Chris Gohlke 的网页www.lfd.uci.edu/~gohlke/pythonlibs/上找到所有这些包的二进制安装程序。
在其他系统中,你可以在项目网站上找到安装说明。GDAL/OGR 是 Fiona 所必需的 C++库。其他包是常规的 Python 包。
最后,你需要在本书的 GitHub 仓库中下载非洲数据集,链接为github.com/ipython-books/cookbook-data
。数据来自 Natural Earth 网站,地址为www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-admin-0-countries/。
如何操作……
-
让我们导入相关包:
In [1]: import numpy as np import matplotlib.pyplot as plt import matplotlib.collections as col from mpl_toolkits.basemap import Basemap import fiona import shapely.geometry as geom from descartes import PolygonPatch %matplotlib inline
-
我们使用 Fiona 加载Shapefile数据集。这个数据集包含了全世界所有国家的边界。
In [2]: # Natural Earth data countries = fiona.open( "data/ne_10m_admin_0_countries.shp")
-
我们选择非洲的国家:
In [3]: africa = [c for c in countries if c['properties']['CONTINENT'] == 'Africa']
-
现在,我们创建一个显示非洲大陆的底图:
In [4]: m = Basemap(llcrnrlon=-23.03, llcrnrlat=-37.72, urcrnrlon=55.20, urcrnrlat=40.58)
-
让我们编写一个函数,将国家边界的地理坐标转换为地图坐标。这将使我们能够在底图中显示边界:
In [5]: def _convert(poly, m): if isinstance(poly, list): return [_convert(_, m) for _ in poly] elif isinstance(poly, tuple): return m(*poly) In [6]: for _ in africa: _['geometry']['coordinates'] = _convert( _['geometry']['coordinates'], m)
-
下一步是创建来自 Fiona 加载的Shapefile数据集的 matplotlib
PatchCollection
对象。我们将使用 Shapely 和 descartes 来完成:In [7]: def get_patch(shape, **kwargs): """Return a matplotlib PatchCollection from a geometry object loaded with fiona.""" # Simple polygon. if isinstance(shape, geom.Polygon): return col.PatchCollection( [PolygonPatch(shape, **kwargs)], match_original=True) # Collection of polygons. elif isinstance(shape, geom.MultiPolygon): return col.PatchCollection( [PolygonPatch(c, **kwargs) for c in shape], match_original=True) In [8]: def get_patches(shapes, fc=None, ec=None, **kwargs): """Return a list of matplotlib PatchCollection objects from a Shapefile dataset.""" # fc and ec are the face and edge colors of the # countries. We ensure these are lists of # colors, with one element per country. if not isinstance(fc, list): fc = [fc] * len(shapes) if not isinstance(ec, list): ec = [ec] * len(shapes) # We convert each polygon to a matplotlib # PatchCollection object. return [get_patch(geom.shape(s['geometry']), fc=fc_, ec=ec_, **kwargs) for s, fc_, ec_ in zip(shapes, fc, ec)]
-
我们还定义了一个函数,根据Shapefile数据集中的特定字段获取国家的颜色。实际上,我们的数据集不仅包含国家边界,还包含每个国家的行政、经济和地理属性。在这里,我们将根据国家的人口和 GDP 选择颜色:
In [9]: def get_colors(field, cmap): """Return one color per country, depending on a specific field in the dataset.""" values = [country['properties'][field] for country in africa] values_max = max(values) return [cmap(v / values_max) for v in values]
-
最后,我们显示地图。我们使用 basemap 显示海岸线,并使用我们的Shapefile数据集显示国家:
In [10]: # Display the countries color-coded with # their population. ax = plt.subplot(121) m.drawcoastlines() patches = get_patches(africa, fc=get_colors('POP_EST', plt.cm.Reds), ec='k') for p in patches: ax.add_collection(p) plt.title("Population") # Display the countries color-coded with # their population. ax = plt.subplot(122) m.drawcoastlines() patches = get_patches(africa, fc=get_colors('GDP_MD_EST', plt.cm.Blues), ec='k') for p in patches: ax.add_collection(p) plt.title("GDP")
另见
- 创建道路网络路线规划器的配方
创建一个道路网络的路线规划器
在这个实例中,我们基于前面几个实例中描述的技术,创建了一个简单的类似 GPS 的路线规划器。我们将从美国人口普查局获取加利福尼亚州的道路网络数据,用来在道路网络图中寻找最短路径。这使我们能够显示加利福尼亚州任意两地之间的道路路线。
准备工作
本实例需要 NetworkX 和 Smopy。在让 NetworkX 读取 Shapefile 数据集时,还需要 GDAL/OGR。更多信息可以参考前面的实例。
你还需要从书籍的 GitHub 仓库下载 Road 数据集:github.com/ipython-books/cookbook-data
,并将其解压到当前目录。
注意
在写这篇文章时,NetworkX 对 Shapefile 的支持似乎与 Python 3.x 不兼容。因此,这个方法仅在 Python 2.x 下成功测试过。
如何实现…
-
让我们导入相关包:
In [1]: import networkx as nx import numpy as np import pandas as pd import json import smopy import matplotlib.pyplot as plt %matplotlib inline
-
我们使用 NetworkX 加载数据(Shapefile 数据集)。这个数据集包含了加利福尼亚州主要道路的详细信息。NetworkX 的
read_shp()
函数返回一个图,其中每个节点是一个地理位置,每条边包含连接两个节点的道路信息。数据来自美国人口普查局网站 www.census.gov/geo/maps-data/data/tiger.html。In [2]: g = nx.read_shp("data/tl_2013_06_prisecroads.shp")
-
这个图不一定是连通的,但为了计算最短路径,我们需要一个连通图。在这里,我们使用
connected_component_subgraphs()
函数获取最大的连通子图:In [3]: sgs = list(nx.connected_component_subgraphs( g.to_undirected())) largest = np.argmax([len(sg) for sg in sgs]) sg = sgs[largest] len(sg) Out[3]: 464
-
我们定义了两个位置(包含纬度和经度),并找出这两个位置之间的最短路径:
In [4]: pos0 = (36.6026, -121.9026) pos1 = (34.0569, -118.2427)
-
图中的每条边包含道路的详细信息,包括沿道路的点的列表。我们首先创建一个函数,返回图中任意一条边的坐标数组:
In [5]: def get_path(n0, n1): """If n0 and n1 are connected nodes in the graph, this function returns an array of point coordinates along the road linking these two nodes.""" return np.array(json.loads( sg[n0][n1]['Json'])['coordinates'])
-
我们可以通过道路路径来计算其长度。我们首先需要定义一个函数,计算地理坐标系中任意两点之间的距离。这个函数可以在 Stack Overflow 上找到 (
stackoverflow.com/questions/8858838/need-help-calculating-geographical-distance
):In [6]: EARTH_R = 6372.8 def geocalc(lat0, lon0, lat1, lon1): """Return the distance (in km) between two points in geographical coordinates.""" lat0 = np.radians(lat0) lon0 = np.radians(lon0) lat1 = np.radians(lat1) lon1 = np.radians(lon1) dlon = lon0 - lon1 y = np.sqrt( (np.cos(lat1)*np.sin(dlon))**2 +(np.cos(lat0)*np.sin(lat1) -np.sin(lat0)*np.cos(lat1)* \ np.cos(dlon))**2) x = np.sin(lat0)*np.sin(lat1) + \ np.cos(lat0)*np.cos(lat1)*np.cos(dlon) c = np.arctan2(y, x) return EARTH_R * c
-
现在,我们定义一个计算路径长度的函数:
In [7]: def get_path_length(path): return np.sum(geocalc( path[1:,0], path[1:,1], path[:-1,0], path[:-1,1]))
-
现在,我们通过计算任何两个连接节点之间的距离来更新我们的图。我们将这个信息添加到边的
distance
属性中:In [8]: # Compute the length of the road segments. for n0, n1 in sg.edges_iter(): path = get_path(n0, n1) distance = get_path_length(path) sg.edge[n0][n1]['distance'] = distance
-
在我们能够找到图中的最短路径之前,最后一步是找出图中最接近两个请求位置的两个节点:
In [9]: nodes = np.array(sg.nodes()) # Get the closest nodes in the graph. pos0_i = np.argmin(np.sum( (nodes[:,::-1] - pos0)**2, axis=1)) pos1_i = np.argmin(np.sum( (nodes[:,::-1] - pos1)**2, axis=1))
-
现在,我们使用 NetworkX 的
shortest_path()
函数来计算两个位置之间的最短路径。我们指定每条边的权重为它们之间道路的长度:In [10]: # Compute the shortest path. path = nx.shortest_path(sg, source=tuple(nodes[pos0_i]), target=tuple(nodes[pos1_i]), weight='distance') len(path) Out[10]: 19
-
行程已计算完成。
path
变量包含了构成我们两点之间最短路径的边的列表。现在,我们可以通过 pandas 获取有关行程的信息。数据集中有几个感兴趣的字段,包括道路的名称和类型(州道、州际公路等):In [11]: roads = pd.DataFrame([ sg.edge[path[i]][path[i + 1]] for i in range(len(path)-1)], columns=['FULLNAME', 'MTFCC', 'RTTYP', 'distance']) roads Out[11]: FULLNAME MTFCC RTTYP distance 0 State Rte 1 S1200 S 100.657768 1 State Rte 1 S1200 S 33.419581 ... 16 Hollywood Fwy S1200 M 14.087627 17 Hollywood Fwy S1200 M 0.010107
这是此行程的总长度:
In [12]: roads['distance'].sum() Out[12]: 508.66421585288725
-
最后,让我们在地图上展示该行程。我们首先通过 Smopy 获取地图:
In [13]: map = smopy.Map(pos0, pos1, z=7, margin=.1)
-
我们的路径包含图中的连接节点。每两个节点之间的边由一系列点(构成道路的一部分)来描述。因此,我们需要定义一个函数,将路径中每个边上的位置串联起来。我们必须按照正确的顺序连接这些位置。我们选择的顺序是基于这样一个事实:一条边的最后一个点需要接近下一条边的第一个点:
In [14]: def get_full_path(path): """Return the positions along a path.""" p_list = [] curp = None for i in range(len(path)-1): p = get_path(path[i], path[i+1]) if curp is None: curp = p if np.sum((p[0]-curp)**2) > \ np.sum((p[-1]-curp)**2): p = p[::-1,:] p_list.append(p) curp = p[-1] return np.vstack(p_list)
-
我们将路径转换为像素,以便在 Smopy 地图上显示:
In [15]: linepath = get_full_path(path) x, y = map.to_pixels(linepath[:,1], linepath[:,0])
-
最后,让我们展示地图,标出我们的两个位置以及它们之间计算出的行程:
In [16]: map.show_mpl() # Plot the itinerary. plt.plot(x, y, '-k', lw=1.5) # Mark our two positions. plt.plot(x[0], y[0], 'ob', ms=10) plt.plot(x[-1], y[-1], 'or', ms=10)
它是如何工作的…
我们使用 NetworkX 的 shortest_path()
函数计算了最短路径。这里,这个函数使用了Dijkstra 算法。这个算法有着广泛的应用,例如网络路由协议。
计算两个点之间的地理距离有多种方法。在这里,我们使用了一个相对精确的公式:正弦距离(也叫做大圆距离),该公式假设地球是一个完美的球体。我们也可以使用一个更简单的公式,因为在一条路上两个连续点之间的距离很小。
还有更多内容…
您可以在以下参考资料中找到关于最短路径问题和 Dijkstra 算法的更多信息:
-
维基百科上的最短路径,
en.wikipedia.org/wiki/Shortest_path_problem
-
Dijkstra 算法,描述见
en.wikipedia.org/wiki/Dijkstra%27s_algorithm
这里有一些关于地理距离的参考资料:
-
维基百科上的地理距离,
en.wikipedia.org/wiki/Geographical_distance
-
维基百科上的大圆,
en.wikipedia.org/wiki/Great_circle
-
维基百科上的大圆距离,
en.wikipedia.org/wiki/Great-circle_distance
第十五章:符号和数值数学
在本章中,我们将涵盖以下主题:
-
使用 SymPy 进行符号计算
-
解方程和不等式
-
分析实值函数
-
计算精确概率和操作随机变量
-
用 SymPy 进行一点数论
-
从真值表中找到布尔命题公式
-
分析非线性微分系统 - Lotka-Volterra(捕食者-猎物)方程
-
开始使用 Sage
介绍
在本章中,我们将介绍SymPy,这是一个用于符号数学的 Python 库。虽然本书大部分内容涉及数值方法,但在这里我们将看到符号计算更适合的示例。
SymPy 对符号计算就像 NumPy 对数值计算一样重要。例如,SymPy 可以帮助我们在运行模拟之前分析数学模型。
尽管 SymPy 相当强大,但与其他计算机代数系统相比有点慢。主要原因是 SymPy 是用纯 Python 编写的。一个更快更强大的数学系统是Sage(也请参阅本章中的Getting started with Sage示例)。Sage 是一个庞大的独立程序,有许多大型依赖项(包括 SymPy!),并且在撰写本文时仅使用 Python 2。它主要用于交互式使用。Sage 包括类似 IPython 的笔记本。
LaTeX
LaTeX是一种广泛用于编写出版质量数学方程式的文档标记语言。使用 LaTeX 编写的方程式可以在浏览器中使用MathJax JavaScript 库显示。SymPy 使用此系统在 IPython 笔记本中显示方程式。
LaTeX 方程式也可以在 matplotlib 中使用。在这种情况下,建议在本地计算机上安装 LaTeX。
这里有一些参考资料:
-
Wikipedia 上的 LaTeX,网址为
en.wikipedia.org/wiki/LaTeX
-
MathJax,可在www.mathjax.org找到
-
matplotlib 中的 LaTeX,描述在
matplotlib.org/users/usetex.html
-
用于显示 SymPy 方程式的文档,网址为
docs.sympy.org/latest/tutorial/printing.html
-
要在计算机上安装 LaTeX,请参考
latex-project.org/ftp.html
使用 SymPy 进行符号计算
在这个示例中,我们将简要介绍使用 SymPy 进行符号计算。我们将在接下来的示例中看到 SymPy 的更高级功能。
准备工作
SymPy 是一个纯 Python 包,没有其他依赖项,因此非常容易安装。使用 Anaconda,您可以在终端中键入conda install sympy
。在 Windows 上,您可以使用 Chris Gohlke 的软件包(www.lfd.uci.edu/~gohlke/pythonlibs/#sympy)。最后,您可以使用pip install sympy
命令。
如何做...
SymPy 可以在 Python 模块中使用,或者在 IPython 中交互式使用。在笔记本中,所有数学表达式都通过 LaTeX 显示,得益于 MathJax JavaScript 库。
这里是 SymPy 的介绍:
-
首先,我们导入 SymPy 并在 IPython 笔记本中启用 LaTeX 打印:
In [1]: from sympy import * init_printing()
-
要处理符号变量,我们首先需要声明它们:
In [2]: var('x y') Out[2]: (x, y)
-
var()
函数用于创建符号并将其注入到命名空间中。这个函数仅应在交互模式下使用。在 Python 模块中,最好使用symbols()
函数来返回符号:In [3]: x, y = symbols('x y')
-
我们可以用这些符号创建数学表达式:
In [4]: expr1 = (x + 1)**2 expr2 = x**2 + 2*x + 1
-
这些表达式相等吗?
In [5]: expr1 == expr2 Out[5]: False
-
这些表达式在数学上相等,但在语法上并不相同。为了测试它们是否在数学上相等,我们可以让 SymPy 代数化地简化差异:
In [6]: simplify(expr1-expr2) Out[6]: 0
-
处理符号表达式时,一个非常常见的操作是通过符号表达式的
subs()
方法将一个符号替换为另一个符号、表达式或数字:在 SymPy 表达式中的替代
-
一个有理数不能简单地写作
1/2
,因为这个 Python 表达式会计算出 0。一个解决方法是将数字1
转换为 SymPy 整数对象,例如使用S()
函数:In [9]: expr1.subs(x, S(1)/2) Out[9]: 9/4
-
精确表示的数字可以通过
evalf
进行数值求值:In [10]: _.evalf() Out[10]: 2.25000000000000
-
我们可以通过
lambdify()
函数轻松地从 SymPy 符号表达式创建 Python 函数。生成的函数可以特别地在 NumPy 数组上进行求值。这在我们需要从符号世界转到数值世界时非常方便:In [11]: f = lambdify(x, expr1) In [12]: import numpy as np f(np.linspace(-2., 2., 5)) Out[12]: array([ 1., 0., 1., 4., 9.])
它是如何工作的...
SymPy 的一个核心思想是使用标准的 Python 语法来操作精确的表达式。虽然这非常方便和自然,但也有一些注意事项。像x
这样的符号,代表数学变量,在被实例化之前不能在 Python 中使用(否则,解释器会抛出NameError
异常)。这与大多数其他计算机代数系统不同。因此,SymPy 提供了提前声明符号变量的方法。
另一个例子是整数除法;由于1/2
会计算为0
(在 Python 2 中),SymPy 无法知道用户原本打算写一个分数。我们需要先将数值整数1
转换为符号整数1
,然后再除以2
。
此外,Python 的等式指的是语法树之间的相等,而不是数学表达式之间的相等。
另见
-
求解方程和不等式食谱
-
Sage 入门食谱
求解方程和不等式
SymPy 提供了多种方法来求解线性和非线性方程及方程组。当然,这些函数并不总能成功找到封闭形式的精确解。在这种情况下,我们可以退回到数值求解器并得到近似解。
准备就绪
我们首先需要导入 SymPy。我们还需要在笔记本中初始化美观打印(参见本章的第一个示例)。
如何实现...
-
定义一些符号:
In [2]: var('x y z a') Out[2]: (x, y, z, a)
-
我们使用
solve()
函数来解方程(右侧默认是0
):In [3]: solve(x**2 - a, x) Out[3]: [-sqrt(a), sqrt(a)]
-
我们还可以解不等式。在这里,我们需要使用
solve_univariate_inequality()
函数来解这个实数域中的单变量不等式:In [4]: x = Symbol('x') solve_univariate_inequality(x**2 > 4, x) Out[4]: Or(x < -2, x > 2)
-
solve()
函数也接受方程组(这里是一个线性系统):In [5]: solve([x + 2*y + 1, x - 3*y - 2], x, y) Out[5]: {x: 1/5, y: -3/5}
-
非线性系统也可以处理:
In [6]: solve([x**2 + y**2 - 1, x**2 - y**2 - S(1)/2], x, y) Out[6]: [(-sqrt(3)/2, -1/2), (-sqrt(3)/2, 1/2), (sqrt(3)/2, -1/2), (sqrt(3)/2, 1/2)]
-
奇异线性系统也可以求解(这里有无限多个解,因为两个方程是共线的):
In [7]: solve([x + 2*y + 1, -x - 2*y - 1], x, y) Out[7]: {x: -2*y - 1}
-
现在,让我们使用包含符号变量的矩阵来求解线性系统:
In [8]: var('a b c d u v') Out[8]: (a, b, c, d, u, v)
-
我们创建了增广矩阵,这是系统矩阵与线性系数和右侧向量的水平拼接。这个矩阵对应于以下系统 x,y: ax+by=u, cx+dy=v:
In [9]: M = Matrix([[a, b, u], [c, d, v]]); M Out[9]: Matrix([[a, b, u], [c, d, v]]) In [10]: solve_linear_system(M, x, y) Out[10]: {x: (-b*v + d*u)/(a*d - b*c), y: ( a*v - c*u)/(a*d - b*c)}
-
这个系统需要是非奇异的,才能有唯一解,这等同于说系统矩阵的行列式需要非零(否则前述分数的分母将为零):
In [11]: det(M[:2,:2]) Out[11]: a*d - b*c
还有更多内容...
SymPy 中的矩阵支持非常丰富;我们可以执行大量的操作和分解(请参阅docs.sympy.org/latest/modules/matrices/matrices.html
的参考指南)。
这里有更多关于线性代数的参考资料:
-
维基百科上的线性代数,
en.wikipedia.org/wiki/Linear_algebra#Further_reading
-
维基教科书上的线性代数,
en.wikibooks.org/wiki/Linear_Algebra
分析实值函数
SymPy 包含了丰富的微积分工具箱,用于分析实值函数:极限、幂级数、导数、积分、傅里叶变换等等。在这个示例中,我们将展示这些功能的基本用法。
做好准备
我们首先需要导入 SymPy。我们还需要在笔记本中初始化美观打印(参见本章的第一个示例)。
如何实现...
-
定义一些符号和一个函数(它只是一个依赖于
x
的表达式):In [1]: var('x z') Out[1]: (x, z) In [2]: f = 1/(1+x**2)
-
让我们在
1
处评估这个函数:In [3]: f.subs(x, 1) Out[3]: 1/2
-
我们可以计算这个函数的导数:
In [4]: diff(f, x) Out[4]: -2*x/(x**2 + 1)**2
-
f
的极限是无限大吗?(注意符号的双oo
(无穷大)表示):In [5]: limit(f, x, oo) Out[5]: 0
-
下面是如何计算泰勒级数(这里是围绕
0
,阶数为9
)。大 O 可以通过removeO()
方法去除。In [6]: series(f, x0=0, n=9) Out[6]: 1 - x**2 + x**4 - x**6 + x**8 + O(x**9)
-
我们可以计算定积分(这里是对整个实数轴的积分):
In [7]: integrate(f, (x, -oo, oo)) Out[7]: pi
-
SymPy 还可以计算不定积分:
In [8]: integrate(f, x) Out[8]: atan(x)
-
最后,让我们计算
f
的傅里叶变换:In [9]: fourier_transform(f, x, z) Out[9]: pi*exp(-2*pi*z)
还有更多内容...
SymPy 还包括许多其他的积分变换,除了傅里叶变换(docs.sympy.org/dev/modules/integrals/integrals.html
)。然而,SymPy 并不总是能够找到闭式解。
这里有一些关于实分析和微积分的参考书目:
-
维基百科上的实分析内容,访问
en.wikipedia.org/wiki/Real_analysis#Bibliography
-
维基书上的微积分内容,访问
en.wikibooks.org/wiki/Calculus
计算精确概率并操作随机变量
SymPy 包括一个名为stats
的模块,允许我们创建和操作随机变量。当我们处理概率或统计模型时,这非常有用;我们可以计算符号期望值、方差、概率和随机变量的密度。
如何操作...
-
我们来导入 SymPy 和 stats 模块:
In [1]: from sympy import * from sympy.stats import * init_printing()
-
让我们掷两个骰子,
X
和Y
,每个都有六个面:In [2]: X, Y = Die('X', 6), Die('Y', 6)
-
我们可以计算由等式(使用
Eq
运算符)或不等式定义的概率:In [3]: P(Eq(X, 3)) Out[3]: 1/6 In [4]: P(X>3) Out[4]: 1/2
-
条件也可以涉及多个随机变量:
In [5]: P(X>Y) Out[5]: 5/12
-
我们可以计算条件概率:
In [6]: P(X+Y>6, X<5) Out[6]: 5/12
-
我们也可以处理任意的离散或连续随机变量:
In [7]: Z = Normal('Z', 0, 1) # Gaussian variable In [8]: P(Z>pi) Out[8]: -erf(sqrt(2)*pi/2)/2 + 1/2
-
我们可以计算期望值和方差:
In [9]: E(Z**2), variance(Z**2) Out[9]: (1, 2)
-
我们也可以计算密度:
In [10]: f = density(Z) In [11]: var('x') f(x) Out[11]: sqrt(2)*exp(-x**2/2)/(2*sqrt(pi))
-
我们可以绘制这些密度:
In [12]: %matplotlib inline plot(f(x), (x, -6, 6))
高斯密度
它是如何工作的...
SymPy 的stats
模块包含许多函数,用于定义具有经典分布(如二项分布、指数分布等)的随机变量,无论是离散的还是连续的。它通过利用 SymPy 强大的积分算法来计算概率分布的积分,从而精确计算概率量。例如,就是:
请注意,等式条件是使用Eq
运算符而非更常见的 Python 语法==
来表示的。这是 SymPy 的一个通用特性;==
表示 Python 变量之间的相等,而Eq
表示符号表达式之间的数学运算。
一点关于数论的内容与 SymPy
SymPy 包含许多与数论相关的例程:获取质数、整数分解等等。我们将在这里展示几个例子。
准备就绪
要在 matplotlib 中使用 LaTeX 显示图例,你需要在计算机上安装 LaTeX(请参见本章的简介)。
如何操作...
-
我们来导入 SymPy 和数论包:
In [1]: from sympy import * init_printing() In [2]: import sympy.ntheory as nt
-
我们可以测试一个数字是否是质数:
In [3]: nt.isprime(2011) Out[3]: True
-
我们可以找出给定数字后的下一个质数:
In [4]: nt.nextprime(2011) Out[4]: 2017
-
第 1000 个质数是什么?
In [5]: nt.prime(1000) Out[5]: 7919
-
小于 2011 的质数有多少个?
In [6]: nt.primepi(2011) Out[6]: 305
-
我们可以绘制
,素数计数函数(小于或等于某个数字 x 的素数的数量)。著名的素数定理指出,这个函数在渐近意义上等价于 x/log(x)。这个表达式大致量化了素数在所有整数中的分布情况:
In [7]: import numpy as np import matplotlib.pyplot as plt %matplotlib inline x = np.arange(2, 10000) plt.plot(x, map(nt.primepi, x), '-k', label='$\pi(x)$') plt.plot(x, x / np.log(x), '--k', label='$x/\log(x)$') plt.legend(loc=2)
素数分布
-
让我们计算一个数字的整数因式分解:
In [8]: nt.factorint(1998) Out[8]: {2: 1, 3: 3, 37: 1} In [9]: 2 * 3**3 * 37 Out[9]: 1998
-
最后,一个小问题。一个懒惰的数学家在数他的珠子。当它们排列成三行时,最后一列有一颗珠子。当它们排列成四行时,最后一列有两颗珠子,排列成五行时有三颗珠子。那么珠子总共有多少颗?(提示:懒惰的数学家少于 100 颗珠子。)
使用中国剩余定理计数珠子
中国剩余定理给我们提供了答案:
In [10]: from sympy.ntheory.modular import solve_congruence In [11]: solve_congruence((1, 3), (2, 4), (3, 5)) Out[11]: (58, 60)
有无限多的解:58 加上任何 60 的倍数。由于珠子总数少于 100 颗,58 就是正确答案。
它是如何工作的…
SymPy 包含许多与数论相关的函数。在这里,我们使用中国剩余定理来求解以下算术方程组的解:
中国剩余定理
三重条是模同余的符号。这里,它表示 m[i] 整除 a[i]-n。换句话说,n 和 a[i] 相等,直到 m[i] 的倍数。处理同余时,当涉及周期性尺度时非常方便。例如,12 小时制的时钟操作是按模 12 运算的。数字 11 和 23 在模 12 下是等价的(它们在时钟上表示相同的时刻),因为它们的差是 12 的倍数。
在这个例子中,必须满足三个同余:珠子数在除以 3 时的余数是 1(表示这种排列中多了一颗珠子),除以 4 时余数是 2,除以 5 时余数是 3。使用 SymPy,我们只需在solve_congruence()
函数中指定这些值,就能得到解。
该定理指出,当 m[i] 彼此互质时(它们之间的任意两个数都是互质的),解是存在的。所有解在 m[i] 的乘积下是同余的。这个数论中的基本定理有许多应用,特别是在密码学中。
还有更多…
这里有一些关于数论的教材:
-
本科水平:《初等数论》,Gareth A. Jones,Josephine M. Jones,Springer,(1998)
-
研究生水平:《现代数论的经典介绍》,Kenneth Ireland,Michael Rosen,Springer,(1982)
这里有一些参考资料:
-
SymPy 的数论模块文档,访问地址:
docs.sympy.org/dev/modules/ntheory.html
-
中国剩余定理的应用,见于
mathoverflow.net/questions/10014/applications-of-the-chinese-remainder-theorem
从真值表中找到一个布尔命题公式
SymPy 中的逻辑模块让我们操作复杂的布尔表达式,也就是命题公式。
这个例子将展示这个模块的一个实际应用。假设在一个程序中,我们需要根据三个布尔变量写一个复杂的if
语句。我们可以考虑八种可能的情况(真、真与假、依此类推),并评估每种情况下应该得到什么结果。SymPy 提供了一个函数,可以生成一个满足我们真值表的紧凑逻辑表达式。
如何操作...
-
让我们导入 SymPy:
In [1]: from sympy import * init_printing()
-
让我们定义一些符号:
In [2]: var('x y z')
-
我们可以使用符号和几个运算符来定义命题公式:
In [3]: P = x & (y | ~z); P Out[3]: And(Or(Not(z), y), x)
-
我们可以使用
subs()
来对实际的布尔值进行公式求值:In [4]: P.subs({x: True, y: False, z: True}) Out[4]: False
-
现在,我们希望根据
x
、y
和z
找到一个命题公式,以下是其真值表:一张真值表
-
让我们列出所有我们希望求值为
True
的组合,以及那些结果无关紧要的组合:In [6]: minterms = [[1,0,1], [1,0,0], [0,0,0]] dontcare = [[1,1,1], [1,1,0]]
-
现在,我们使用
SOPform()
函数来推导出一个合适的公式:In [7]: Q = SOPform(['x', 'y', 'z'], minterms, dontcare); Q Out[7]: Or(And(Not(y), Not(z)), x)
-
让我们测试一下这个命题是否有效:
In [8]: Q.subs({x: True, y: False, z: False}), Q.subs({x: False, y: True, z: True}) Out[8]: (True, False)
它是如何工作的...
SOPform()
函数生成一个与真值表对应的完整表达式,并使用Quine-McCluskey 算法简化它。它返回最小的积和形式(或合取的析取)。类似地,POSform()
函数返回一个和的积。
给定的真值表可能出现在这种情况下:假设我们希望在文件不存在时写入文件(z
),或者用户希望强制写入(x
)。此外,用户可以阻止写入(y
)。当文件需要被写入时,表达式的值为True
。得到的 SOP 公式在我们首先显式禁止x
和y
时有效(强制和禁止写入同时发生是禁止的)。
还有更多内容...
这里有一些参考资料:
分析非线性微分系统 – Lotka-Volterra(捕食-被捕食)方程
在这里,我们将对著名的非线性微分方程系统——洛特卡-沃尔特拉方程进行简要的分析,这也被称为捕食者-猎物方程。这些方程是描述两种相互作用的种群(例如鲨鱼和沙丁鱼)演化的一级微分方程,其中捕食者捕食猎物。这个例子展示了如何使用 SymPy 获得关于固定点及其稳定性的精确表达式和结果。
准备好
对于这个公式,建议了解线性和非线性微分方程的基础知识。
如何做到...
-
让我们创建一些符号:
In [1]: from sympy import * init_printing() In [2]: var('x y') var('a b c d', positive=True) Out[2]: (a, b, c, d)
-
变量
x
和y
分别代表猎物和捕食者的种群。参数a
、b
、c
和d
是严格为正的参数(在这个公式的如何实现...部分中更精确地描述)。方程为:洛特卡-沃尔特拉方程
In [3]: f = x * (a - b*y) g = -y * (c - d*x)
-
让我们找到系统的固定点(解f(x,y) = g(x,y) = 0)。我们将它们称为(x[0], y[0])和(x[1], y[1]):
In [4]: solve([f, g], (x, y)) Out[4]: [(0, 0), (c/d, a/b)] In [5]: (x0, y0), (x1, y1) = _
-
让我们用两个方程来写出二维向量:
In [6]: M = Matrix((f, g)); M Out[6]: Matrix([[ x*(a - b*y)], [-y*(c - d*x)]])
-
现在,我们可以计算系统的雅可比矩阵,作为
(x, y)
的函数:In [7]: J = M.jacobian((x, y)); J Out[7]: Matrix([ [a - b*y, -b*x], [ d*y, -c + d*x]])
-
让我们通过观察雅可比矩阵在这个点的特征值来研究第一个固定点的稳定性。第一个固定点对应的是种群灭绝:
In [8]: M0 = J.subs(x, x0).subs(y, y0); M0 Out[8]: Matrix([a, 0], [0, -c]]) In [9]: M0.eigenvals() Out[9]: {a: 1, -c: 1}
参数
a
和c
是严格为正的,因此特征值是实数且符号相反,这个固定点是鞍点。由于这个点不稳定,因此在这个模型中两种种群都灭绝的可能性很小。 -
现在让我们考虑第二个固定点:
In [10]: M1 = J.subs(x, x1).subs(y, y1); M1 Out[10]: Matrix([[ 0, -b*c/d], [a*d/b, 0]]) In [11]: M1.eigenvals() Out[11]: {-I*sqrt(a)*sqrt(c): 1, I*sqrt(a)*sqrt(c): 1}
特征值是纯虚数;因此,这个固定点不是双曲的。因此,我们不能从这个线性分析中得出关于该固定点周围系统定性行为的结论。然而,我们可以通过其他方法证明,在这个点附近会发生振荡。
如何实现...
洛特卡-沃尔特拉方程模拟了捕食者和猎物种群的增长,考虑了它们的相互作用。在第一个方程中,ax项表示猎物的指数增长,-bxy表示捕食者导致的死亡。同样,在第二个方程中,-yc表示捕食者的自然死亡,dxy表示捕食者因捕食更多猎物而增长。
要找到系统的平衡点,我们需要找到* x, y 的值,使得 dx/dt = dy/dt = 0,即f(x, y) = g(x, y) = 0*,这样变量就不再变化了。在这里,我们能够通过solve()
函数获得这些平衡点的解析值。
为了分析它们的稳定性,我们需要对非线性方程进行线性分析,通过在这些平衡点计算雅可比矩阵。这个矩阵代表了线性化系统,它的特征值告诉我们关于平衡点附近系统的稳定性。哈特曼–格罗曼定理指出,如果平衡点是双曲的(意味着矩阵的特征值没有实部为零的情况),那么原始系统的行为在定性上与线性化系统在平衡点附近的行为是匹配的。在这里,第一个平衡点是双曲的,因为a, c > 0,而第二个则不是。
在这里,我们能够计算出平衡点处雅可比矩阵及其特征值的符号表达式。
还有更多...
即使一个差分系统无法解析求解(如这里所示),数学分析仍然可以为我们提供关于系统解的行为的定性信息。当我们关心定性结果时,纯粹的数值分析并不总是相关的,因为数值误差和近似可能导致关于系统行为的错误结论。
这里有一些参考资料:
-
SymPy 中的矩阵文档,网址:
docs.sympy.org/dev/modules/matrices/matrices.html
-
维基百科上的动态系统,
en.wikipedia.org/wiki/Dynamical_system
-
Scholarpedia 上的平衡点,www.scholarpedia.org/article/Equilibrium
-
维基百科上的分岔理论,
en.wikipedia.org/wiki/Bifurcation_theory
-
维基百科上的混沌理论,
en.wikipedia.org/wiki/Chaos_theory
-
关于动态系统的进一步阅读,网址:
en.wikipedia.org/wiki/Dynamical_system#Further_reading
开始使用 Sage
Sage (www.sagemath.org) 是一个基于 Python 的独立数学软件。它是商业产品如 Mathematica、Maple 或 MATLAB 的开源替代品。Sage 提供了一个统一的接口,连接了许多开源数学库。这些库包括 SciPy、SymPy、NetworkX 和其他 Python 科学包,也包括非 Python 库,如 ATLAS、BLAS、GSL、LAPACK、Singular 等。
在这个教程中,我们将简要介绍 Sage。
准备工作
你可以选择:
-
在本地计算机上安装 Sage (www.sagemath.org/doc/installation/)
-
在云端远程创建 Sage 笔记本 (
cloud.sagemath.com/
)
由于依赖于如此多的库,Sage 很庞大且难以从源代码编译。除了 Windows 系统外,其他大多数系统都有现成的二进制文件,而在 Windows 上通常需要使用 VirtualBox(一种虚拟化解决方案:www.virtualbox.org)。
另外,你也可以通过在云端运行 IPython 笔记本,在浏览器中使用 Sage。
请注意,Sage 在写作时与 Python 3 不兼容。
通常,Sage 是通过内置的笔记本进行交互式使用(它类似于 IPython 笔记本)。如果你想在 Python 程序中使用 Sage(也就是从 Python 导入 Sage),你需要运行 Sage 的内置 Python 解释器(www.sagemath.org/doc/faq/faq-usage.html#how-do-i-import-sage-into-a-python-script)。
如何做...
在这里,我们将创建一个新的 Sage 笔记本,并介绍最基本的功能:
-
Sage 接受数学表达式,就像我们预期的那样:
sage: 3*4 12
-
由于基于 Python,Sage 的语法几乎与 Python 相同,但也有一些差异。例如,幂指数使用的是更经典的
^
符号:sage: 2³ 8
-
就像在 SymPy 中一样,符号变量需要事先使用
var()
函数声明。然而,x
变量总是预定义的。在这里,我们定义一个新的数学函数:sage: f=1-sin(x)²
-
让我们简化
f
的表达式:sage: f.simplify_trig() cos(x)²
-
让我们在给定的点上评估
f
:sage: f(x=pi) 1
-
函数可以进行微分和积分:
sage: f.diff(x) -2*cos(x)*sin(x) sage: f.integrate(x) 1/2*x + 1/4*sin(2*x)
-
除了符号计算,Sage 还支持数值计算:
sage: find_root(f-x, 0, 2) 0.6417143708729723
-
Sage 还具有丰富的绘图功能(包括交互式绘图控件):
sage: f.plot((x, -2*pi, 2*pi))
还有更多内容...
这个(也)简短的教程无法充分展示 Sage 提供的广泛功能。Sage 涉及数学的许多方面:代数、组合数学、数值数学、数论、微积分、几何、图论等。以下是一些参考资料:
-
关于 Sage 的深入教程可以在 www.sagemath.org/doc/tutorial/ 查阅
-
Sage 的参考手册可以在 www.sagemath.org/doc/reference/ 查阅
-
关于 Sage 的视频,可以在 www.sagemath.org/help-video.html 查阅
另见
- 深入学习符号计算与 SymPy 课程